package heroic_ops import ( "fmt" "eq2emu/internal/database" ) // NewHeroicOPWheel creates a new heroic opportunity wheel func NewHeroicOPWheel(db *database.Database) *HeroicOPWheel { return &HeroicOPWheel{ db: db, isNew: true, Abilities: [6]int16{}, Chance: 1.0, SaveNeeded: false, } } // LoadHeroicOPWheel loads a heroic opportunity wheel by ID func LoadHeroicOPWheel(db *database.Database, id int32) (*HeroicOPWheel, error) { wheel := &HeroicOPWheel{ db: db, isNew: false, } query := `SELECT id, starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6, spell_id, name, description, required_players FROM heroic_op_wheels WHERE id = ?` err := db.QueryRow(query, id).Scan( &wheel.ID, &wheel.StarterLinkID, &wheel.Order, &wheel.ShiftIcon, &wheel.Chance, &wheel.Abilities[0], &wheel.Abilities[1], &wheel.Abilities[2], &wheel.Abilities[3], &wheel.Abilities[4], &wheel.Abilities[5], &wheel.SpellID, &wheel.Name, &wheel.Description, &wheel.RequiredPlayers, ) if err != nil { return nil, fmt.Errorf("failed to load heroic op wheel: %w", err) } wheel.SaveNeeded = false return wheel, nil } // GetID returns the wheel ID func (how *HeroicOPWheel) GetID() int32 { how.mu.RLock() defer how.mu.RUnlock() return how.ID } // Save persists the wheel to the database func (how *HeroicOPWheel) Save() error { how.mu.Lock() defer how.mu.Unlock() if !how.SaveNeeded { return nil } if how.isNew { // Insert new record query := `INSERT INTO heroic_op_wheels (id, starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6, spell_id, name, description, required_players) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` _, err := how.db.Exec(query, how.ID, how.StarterLinkID, how.Order, how.ShiftIcon, how.Chance, how.Abilities[0], how.Abilities[1], how.Abilities[2], how.Abilities[3], how.Abilities[4], how.Abilities[5], how.SpellID, how.Name, how.Description, how.RequiredPlayers, ) if err != nil { return fmt.Errorf("failed to insert heroic op wheel: %w", err) } how.isNew = false } else { // Update existing record query := `UPDATE heroic_op_wheels SET starter_link_id = ?, chain_order = ?, shift_icon = ?, chance = ?, ability1 = ?, ability2 = ?, ability3 = ?, ability4 = ?, ability5 = ?, ability6 = ?, spell_id = ?, name = ?, description = ?, required_players = ? WHERE id = ?` _, err := how.db.Exec(query, how.StarterLinkID, how.Order, how.ShiftIcon, how.Chance, how.Abilities[0], how.Abilities[1], how.Abilities[2], how.Abilities[3], how.Abilities[4], how.Abilities[5], how.SpellID, how.Name, how.Description, how.RequiredPlayers, how.ID, ) if err != nil { return fmt.Errorf("failed to update heroic op wheel: %w", err) } } how.SaveNeeded = false return nil } // Delete removes the wheel from the database func (how *HeroicOPWheel) Delete() error { how.mu.Lock() defer how.mu.Unlock() if how.isNew { return nil // Nothing to delete } query := "DELETE FROM heroic_op_wheels WHERE id = ?" _, err := how.db.Exec(query, how.ID) if err != nil { return fmt.Errorf("failed to delete heroic op wheel: %w", err) } return nil } // Reload refreshes the wheel data from the database func (how *HeroicOPWheel) Reload() error { how.mu.Lock() defer how.mu.Unlock() if how.isNew { return fmt.Errorf("cannot reload unsaved heroic op wheel") } query := `SELECT starter_link_id, chain_order, shift_icon, chance, ability1, ability2, ability3, ability4, ability5, ability6, spell_id, name, description, required_players FROM heroic_op_wheels WHERE id = ?` err := how.db.QueryRow(query, how.ID).Scan( &how.StarterLinkID, &how.Order, &how.ShiftIcon, &how.Chance, &how.Abilities[0], &how.Abilities[1], &how.Abilities[2], &how.Abilities[3], &how.Abilities[4], &how.Abilities[5], &how.SpellID, &how.Name, &how.Description, &how.RequiredPlayers, ) if err != nil { return fmt.Errorf("failed to reload heroic op wheel: %w", err) } how.SaveNeeded = false return nil } // Copy creates a deep copy of the wheel func (how *HeroicOPWheel) Copy() *HeroicOPWheel { how.mu.RLock() defer how.mu.RUnlock() newWheel := &HeroicOPWheel{ ID: how.ID, StarterLinkID: how.StarterLinkID, Order: how.Order, ShiftIcon: how.ShiftIcon, Chance: how.Chance, Abilities: how.Abilities, // Arrays are copied by value SpellID: how.SpellID, Name: how.Name, Description: how.Description, RequiredPlayers: how.RequiredPlayers, db: how.db, isNew: true, // Copy is always new unless explicitly saved SaveNeeded: false, } return newWheel } // GetAbility returns the ability icon at the specified position func (how *HeroicOPWheel) GetAbility(position int) int16 { how.mu.RLock() defer how.mu.RUnlock() if position < 0 || position >= MaxAbilities { return AbilityIconNone } return how.Abilities[position] } // SetAbility sets the ability icon at the specified position func (how *HeroicOPWheel) SetAbility(position int, abilityIcon int16) bool { how.mu.Lock() defer how.mu.Unlock() if position < 0 || position >= MaxAbilities { return false } how.Abilities[position] = abilityIcon how.SaveNeeded = true return true } // IsOrdered checks if this wheel requires ordered completion func (how *HeroicOPWheel) IsOrdered() bool { how.mu.RLock() defer how.mu.RUnlock() return how.Order >= WheelOrderOrdered } // HasShift checks if this wheel has a shift ability func (how *HeroicOPWheel) HasShift() bool { how.mu.RLock() defer how.mu.RUnlock() return how.ShiftIcon > 0 } // CanShift checks if shifting is possible with the given ability func (how *HeroicOPWheel) CanShift(abilityIcon int16) bool { how.mu.RLock() defer how.mu.RUnlock() return how.ShiftIcon > 0 && how.ShiftIcon == abilityIcon } // GetNextRequiredAbility returns the next required ability for ordered wheels func (how *HeroicOPWheel) GetNextRequiredAbility(countered [6]int8) int16 { how.mu.RLock() defer how.mu.RUnlock() if !how.IsOrdered() { return AbilityIconNone // Any uncompleted ability works for unordered } // Find first uncompleted ability in order for i := 0; i < MaxAbilities; i++ { if countered[i] == 0 && how.Abilities[i] != AbilityIconNone { return how.Abilities[i] } } return AbilityIconNone } // CanUseAbility checks if an ability can be used on this wheel func (how *HeroicOPWheel) CanUseAbility(abilityIcon int16, countered [6]int8) bool { how.mu.RLock() defer how.mu.RUnlock() // Check if this is a shift attempt if how.CanShift(abilityIcon) { return true } if how.IsOrdered() { // For ordered wheels, only the next required ability can be used nextRequired := how.GetNextRequiredAbility(countered) return nextRequired == abilityIcon } else { // For unordered wheels, any uncompleted matching ability can be used for i := 0; i < MaxAbilities; i++ { if countered[i] == 0 && how.Abilities[i] == abilityIcon { return true } } } return false } // Validate checks if the wheel is properly configured func (how *HeroicOPWheel) Validate() error { how.mu.RLock() defer how.mu.RUnlock() if how.ID <= 0 { return fmt.Errorf("invalid wheel ID: %d", how.ID) } if how.StarterLinkID <= 0 { return fmt.Errorf("invalid starter link ID: %d", how.StarterLinkID) } if how.Chance < MinChance || how.Chance > MaxChance { return fmt.Errorf("invalid chance: %f (must be %f-%f)", how.Chance, MinChance, MaxChance) } if how.SpellID <= 0 { return fmt.Errorf("invalid spell ID: %d", how.SpellID) } // Check for at least one non-zero ability hasAbility := false for _, ability := range how.Abilities { if ability != AbilityIconNone { hasAbility = true break } } if !hasAbility { return fmt.Errorf("wheel must have at least one ability") } return nil } // SetID sets the wheel ID func (how *HeroicOPWheel) SetID(id int32) { how.mu.Lock() defer how.mu.Unlock() how.ID = id how.SaveNeeded = true } // SetStarterLinkID sets the starter link ID func (how *HeroicOPWheel) SetStarterLinkID(id int32) { how.mu.Lock() defer how.mu.Unlock() how.StarterLinkID = id how.SaveNeeded = true } // SetOrder sets the wheel order func (how *HeroicOPWheel) SetOrder(order int8) { how.mu.Lock() defer how.mu.Unlock() how.Order = order how.SaveNeeded = true } // SetShiftIcon sets the shift icon func (how *HeroicOPWheel) SetShiftIcon(icon int16) { how.mu.Lock() defer how.mu.Unlock() how.ShiftIcon = icon how.SaveNeeded = true } // SetChance sets the wheel chance func (how *HeroicOPWheel) SetChance(chance float32) { how.mu.Lock() defer how.mu.Unlock() how.Chance = chance how.SaveNeeded = true } // SetSpellID sets the spell ID func (how *HeroicOPWheel) SetSpellID(id int32) { how.mu.Lock() defer how.mu.Unlock() how.SpellID = id how.SaveNeeded = true } // SetName sets the wheel name func (how *HeroicOPWheel) SetName(name string) { how.mu.Lock() defer how.mu.Unlock() how.Name = name how.SaveNeeded = true } // SetDescription sets the wheel description func (how *HeroicOPWheel) SetDescription(description string) { how.mu.Lock() defer how.mu.Unlock() how.Description = description how.SaveNeeded = true } // SetRequiredPlayers sets the required players func (how *HeroicOPWheel) SetRequiredPlayers(required int8) { how.mu.Lock() defer how.mu.Unlock() how.RequiredPlayers = required how.SaveNeeded = true }