eq2go/internal/heroic_ops/heroic_op_instance.go
2025-08-08 14:31:44 -05:00

704 lines
18 KiB
Go

package heroic_ops
import (
"fmt"
"time"
"eq2emu/internal/database"
)
// NewHeroicOP creates a new heroic opportunity instance
func NewHeroicOP(db *database.Database, instanceID int64, encounterID int32) *HeroicOP {
return &HeroicOP{
ID: instanceID,
EncounterID: encounterID,
State: HOStateInactive,
StartTime: time.Now(),
Participants: make(map[int32]bool),
CurrentStarters: make([]int32, 0),
TotalTime: DefaultWheelTimerSeconds * 1000, // Convert to milliseconds
TimeRemaining: DefaultWheelTimerSeconds * 1000,
db: db,
isNew: true,
SaveNeeded: false,
}
}
// LoadHeroicOP loads a heroic opportunity instance by ID
func LoadHeroicOP(db *database.Database, instanceID int64) (*HeroicOP, error) {
ho := &HeroicOP{
db: db,
isNew: false,
}
// Load basic HO data
query := `SELECT id, encounter_id, starter_id, wheel_id, state, start_time, wheel_start_time, time_remaining, total_time,
complete, shift_used, starter_progress, completed_by, spell_name, spell_description,
countered1, countered2, countered3, countered4, countered5, countered6
FROM heroic_op_instances WHERE id = ?`
var startTimeStr, wheelStartTimeStr string
err := db.QueryRow(query, instanceID).Scan(
&ho.ID,
&ho.EncounterID,
&ho.StarterID,
&ho.WheelID,
&ho.State,
&startTimeStr,
&wheelStartTimeStr,
&ho.TimeRemaining,
&ho.TotalTime,
&ho.Complete,
&ho.ShiftUsed,
&ho.StarterProgress,
&ho.CompletedBy,
&ho.SpellName,
&ho.SpellDescription,
&ho.Countered[0],
&ho.Countered[1],
&ho.Countered[2],
&ho.Countered[3],
&ho.Countered[4],
&ho.Countered[5],
)
if err != nil {
return nil, fmt.Errorf("failed to load heroic op instance: %w", err)
}
// Parse time fields
if startTimeStr != "" {
ho.StartTime, err = time.Parse(time.RFC3339, startTimeStr)
if err != nil {
return nil, fmt.Errorf("failed to parse start time: %w", err)
}
}
if wheelStartTimeStr != "" {
ho.WheelStartTime, err = time.Parse(time.RFC3339, wheelStartTimeStr)
if err != nil {
return nil, fmt.Errorf("failed to parse wheel start time: %w", err)
}
}
// Load participants
ho.Participants = make(map[int32]bool)
participantQuery := "SELECT character_id FROM heroic_op_participants WHERE instance_id = ?"
rows, err := db.Query(participantQuery, instanceID)
if err != nil {
return nil, fmt.Errorf("failed to load participants: %w", err)
}
defer rows.Close()
for rows.Next() {
var characterID int32
if err := rows.Scan(&characterID); err != nil {
return nil, fmt.Errorf("failed to scan participant: %w", err)
}
ho.Participants[characterID] = true
}
// Load current starters
ho.CurrentStarters = make([]int32, 0)
starterQuery := "SELECT starter_id FROM heroic_op_current_starters WHERE instance_id = ?"
starterRows, err := db.Query(starterQuery, instanceID)
if err != nil {
return nil, fmt.Errorf("failed to load current starters: %w", err)
}
defer starterRows.Close()
for starterRows.Next() {
var starterID int32
if err := starterRows.Scan(&starterID); err != nil {
return nil, fmt.Errorf("failed to scan current starter: %w", err)
}
ho.CurrentStarters = append(ho.CurrentStarters, starterID)
}
ho.SaveNeeded = false
return ho, nil
}
// GetID returns the instance ID
func (ho *HeroicOP) GetID() int64 {
ho.mu.RLock()
defer ho.mu.RUnlock()
return ho.ID
}
// Save persists the heroic op instance to the database
func (ho *HeroicOP) Save() error {
ho.mu.Lock()
defer ho.mu.Unlock()
if !ho.SaveNeeded {
return nil
}
if ho.isNew {
// Insert new record
query := `INSERT INTO heroic_op_instances (id, encounter_id, starter_id, wheel_id, state, start_time, wheel_start_time, time_remaining, total_time,
complete, shift_used, starter_progress, completed_by, spell_name, spell_description,
countered1, countered2, countered3, countered4, countered5, countered6)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
_, err := ho.db.Exec(query,
ho.ID,
ho.EncounterID,
ho.StarterID,
ho.WheelID,
ho.State,
ho.StartTime.Format(time.RFC3339),
ho.WheelStartTime.Format(time.RFC3339),
ho.TimeRemaining,
ho.TotalTime,
ho.Complete,
ho.ShiftUsed,
ho.StarterProgress,
ho.CompletedBy,
ho.SpellName,
ho.SpellDescription,
ho.Countered[0],
ho.Countered[1],
ho.Countered[2],
ho.Countered[3],
ho.Countered[4],
ho.Countered[5],
)
if err != nil {
return fmt.Errorf("failed to insert heroic op instance: %w", err)
}
ho.isNew = false
} else {
// Update existing record
query := `UPDATE heroic_op_instances SET encounter_id = ?, starter_id = ?, wheel_id = ?, state = ?, start_time = ?, wheel_start_time = ?,
time_remaining = ?, total_time = ?, complete = ?, shift_used = ?, starter_progress = ?, completed_by = ?,
spell_name = ?, spell_description = ?, countered1 = ?, countered2 = ?, countered3 = ?, countered4 = ?,
countered5 = ?, countered6 = ? WHERE id = ?`
_, err := ho.db.Exec(query,
ho.EncounterID,
ho.StarterID,
ho.WheelID,
ho.State,
ho.StartTime.Format(time.RFC3339),
ho.WheelStartTime.Format(time.RFC3339),
ho.TimeRemaining,
ho.TotalTime,
ho.Complete,
ho.ShiftUsed,
ho.StarterProgress,
ho.CompletedBy,
ho.SpellName,
ho.SpellDescription,
ho.Countered[0],
ho.Countered[1],
ho.Countered[2],
ho.Countered[3],
ho.Countered[4],
ho.Countered[5],
ho.ID,
)
if err != nil {
return fmt.Errorf("failed to update heroic op instance: %w", err)
}
}
// Save participants
if err := ho.saveParticipants(); err != nil {
return fmt.Errorf("failed to save participants: %w", err)
}
// Save current starters
if err := ho.saveCurrentStarters(); err != nil {
return fmt.Errorf("failed to save current starters: %w", err)
}
ho.SaveNeeded = false
return nil
}
// saveParticipants saves the participants to the database (internal helper)
func (ho *HeroicOP) saveParticipants() error {
// Delete existing participants
deleteQuery := "DELETE FROM heroic_op_participants WHERE instance_id = ?"
_, err := ho.db.Exec(deleteQuery, ho.ID)
if err != nil {
return err
}
// Insert current participants
for characterID := range ho.Participants {
insertQuery := "INSERT INTO heroic_op_participants (instance_id, character_id) VALUES (?, ?)"
_, err := ho.db.Exec(insertQuery, ho.ID, characterID)
if err != nil {
return err
}
}
return nil
}
// saveCurrentStarters saves the current starters to the database (internal helper)
func (ho *HeroicOP) saveCurrentStarters() error {
// Delete existing current starters
deleteQuery := "DELETE FROM heroic_op_current_starters WHERE instance_id = ?"
_, err := ho.db.Exec(deleteQuery, ho.ID)
if err != nil {
return err
}
// Insert current starters
for _, starterID := range ho.CurrentStarters {
insertQuery := "INSERT INTO heroic_op_current_starters (instance_id, starter_id) VALUES (?, ?)"
_, err := ho.db.Exec(insertQuery, ho.ID, starterID)
if err != nil {
return err
}
}
return nil
}
// Delete removes the heroic op instance from the database
func (ho *HeroicOP) Delete() error {
ho.mu.Lock()
defer ho.mu.Unlock()
if ho.isNew {
return nil // Nothing to delete
}
// Delete related records first (foreign key constraints)
deleteParticipants := "DELETE FROM heroic_op_participants WHERE instance_id = ?"
_, err := ho.db.Exec(deleteParticipants, ho.ID)
if err != nil {
return fmt.Errorf("failed to delete participants: %w", err)
}
deleteStarters := "DELETE FROM heroic_op_current_starters WHERE instance_id = ?"
_, err = ho.db.Exec(deleteStarters, ho.ID)
if err != nil {
return fmt.Errorf("failed to delete current starters: %w", err)
}
// Delete main record
query := "DELETE FROM heroic_op_instances WHERE id = ?"
_, err = ho.db.Exec(query, ho.ID)
if err != nil {
return fmt.Errorf("failed to delete heroic op instance: %w", err)
}
return nil
}
// Reload refreshes the heroic op instance data from the database
func (ho *HeroicOP) Reload() error {
ho.mu.Lock()
defer ho.mu.Unlock()
if ho.isNew {
return fmt.Errorf("cannot reload unsaved heroic op instance")
}
// Reload from database
reloaded, err := LoadHeroicOP(ho.db, ho.ID)
if err != nil {
return err
}
// Copy all fields except database connection and isNew flag
ho.EncounterID = reloaded.EncounterID
ho.StarterID = reloaded.StarterID
ho.WheelID = reloaded.WheelID
ho.State = reloaded.State
ho.StartTime = reloaded.StartTime
ho.WheelStartTime = reloaded.WheelStartTime
ho.TimeRemaining = reloaded.TimeRemaining
ho.TotalTime = reloaded.TotalTime
ho.Complete = reloaded.Complete
ho.Countered = reloaded.Countered
ho.ShiftUsed = reloaded.ShiftUsed
ho.StarterProgress = reloaded.StarterProgress
ho.Participants = reloaded.Participants
ho.CurrentStarters = reloaded.CurrentStarters
ho.CompletedBy = reloaded.CompletedBy
ho.SpellName = reloaded.SpellName
ho.SpellDescription = reloaded.SpellDescription
ho.SaveNeeded = false
return nil
}
// AddParticipant adds a character to the HO participants
func (ho *HeroicOP) AddParticipant(characterID int32) {
ho.mu.Lock()
defer ho.mu.Unlock()
ho.Participants[characterID] = true
ho.SaveNeeded = true
}
// RemoveParticipant removes a character from the HO participants
func (ho *HeroicOP) RemoveParticipant(characterID int32) {
ho.mu.Lock()
defer ho.mu.Unlock()
delete(ho.Participants, characterID)
ho.SaveNeeded = true
}
// IsParticipant checks if a character is participating in this HO
func (ho *HeroicOP) IsParticipant(characterID int32) bool {
ho.mu.RLock()
defer ho.mu.RUnlock()
return ho.Participants[characterID]
}
// GetParticipants returns a slice of participant character IDs
func (ho *HeroicOP) GetParticipants() []int32 {
ho.mu.RLock()
defer ho.mu.RUnlock()
participants := make([]int32, 0, len(ho.Participants))
for characterID := range ho.Participants {
participants = append(participants, characterID)
}
return participants
}
// Copy creates a deep copy of the HO instance
func (ho *HeroicOP) Copy() *HeroicOP {
ho.mu.RLock()
defer ho.mu.RUnlock()
newHO := &HeroicOP{
ID: ho.ID,
EncounterID: ho.EncounterID,
StarterID: ho.StarterID,
WheelID: ho.WheelID,
State: ho.State,
StartTime: ho.StartTime,
WheelStartTime: ho.WheelStartTime,
TimeRemaining: ho.TimeRemaining,
TotalTime: ho.TotalTime,
Complete: ho.Complete,
Countered: ho.Countered, // Arrays are copied by value
ShiftUsed: ho.ShiftUsed,
StarterProgress: ho.StarterProgress,
CompletedBy: ho.CompletedBy,
SpellName: ho.SpellName,
SpellDescription: ho.SpellDescription,
Participants: make(map[int32]bool, len(ho.Participants)),
CurrentStarters: make([]int32, len(ho.CurrentStarters)),
db: ho.db,
isNew: true, // Copy is always new unless explicitly saved
SaveNeeded: false,
}
// Deep copy participants map
for characterID, participating := range ho.Participants {
newHO.Participants[characterID] = participating
}
// Deep copy current starters slice
copy(newHO.CurrentStarters, ho.CurrentStarters)
return newHO
}
// StartStarterChain initiates the starter chain phase
func (ho *HeroicOP) StartStarterChain(availableStarters []int32) {
ho.mu.Lock()
defer ho.mu.Unlock()
ho.State = HOStateStarterChain
ho.CurrentStarters = make([]int32, len(availableStarters))
copy(ho.CurrentStarters, availableStarters)
ho.StarterProgress = 0
ho.StartTime = time.Now()
ho.SaveNeeded = true
}
// ProcessStarterAbility processes an ability during starter chain phase
func (ho *HeroicOP) ProcessStarterAbility(abilityIcon int16, masterList *MasterList) bool {
ho.mu.Lock()
defer ho.mu.Unlock()
if ho.State != HOStateStarterChain {
return false
}
// Filter out starters that don't match this ability at current position
newStarters := make([]int32, 0)
for _, starterID := range ho.CurrentStarters {
starter := masterList.GetStarter(starterID)
if starter != nil && starter.MatchesAbility(int(ho.StarterProgress), abilityIcon) {
// Check if this completes the starter
if starter.IsComplete(int(ho.StarterProgress)) {
// Starter completed, transition to wheel phase
ho.StarterID = starterID
ho.SaveNeeded = true
return true
}
newStarters = append(newStarters, starterID)
}
}
ho.CurrentStarters = newStarters
ho.StarterProgress++
ho.SaveNeeded = true
// If no starters remain, HO fails
return len(ho.CurrentStarters) > 0
}
// StartWheelPhase initiates the wheel phase
func (ho *HeroicOP) StartWheelPhase(wheel *HeroicOPWheel, timerSeconds int32) {
ho.mu.Lock()
defer ho.mu.Unlock()
ho.State = HOStateWheelPhase
ho.WheelID = wheel.ID
ho.WheelStartTime = time.Now()
ho.TotalTime = timerSeconds * 1000 // Convert to milliseconds
ho.TimeRemaining = ho.TotalTime
ho.SpellName = wheel.Name
ho.SpellDescription = wheel.Description
// Clear countered array
for i := range ho.Countered {
ho.Countered[i] = 0
}
ho.SaveNeeded = true
}
// ProcessWheelAbility processes an ability during wheel phase
func (ho *HeroicOP) ProcessWheelAbility(abilityIcon int16, characterID int32, wheel *HeroicOPWheel) bool {
ho.mu.Lock()
defer ho.mu.Unlock()
if ho.State != HOStateWheelPhase {
return false
}
// Check for shift attempt
if ho.ShiftUsed == ShiftNotUsed && wheel.CanShift(abilityIcon) {
// Allow shift only if no progress made (unordered) or at start (ordered)
canShift := false
if wheel.IsOrdered() {
// For ordered, can shift only if no abilities completed
canShift = true
for i := 0; i < MaxAbilities; i++ {
if ho.Countered[i] != 0 {
canShift = false
break
}
}
} else {
// For unordered, can shift only if no abilities completed
canShift = true
for i := 0; i < MaxAbilities; i++ {
if ho.Countered[i] != 0 {
canShift = false
break
}
}
}
if canShift {
ho.ShiftUsed = ShiftUsed
ho.SaveNeeded = true
return true // Caller should handle wheel shifting
}
return false
}
// Check if ability can be used
if !wheel.CanUseAbility(abilityIcon, ho.Countered) {
return false
}
// Find matching ability position and mark as countered
for i := 0; i < MaxAbilities; i++ {
if ho.Countered[i] == 0 && wheel.GetAbility(i) == abilityIcon {
ho.Countered[i] = 1
ho.AddParticipant(characterID)
ho.SaveNeeded = true
// Check if wheel is complete
complete := true
for j := 0; j < MaxAbilities; j++ {
if wheel.GetAbility(j) != AbilityIconNone && ho.Countered[j] == 0 {
complete = false
break
}
}
if complete {
ho.Complete = HOComplete
ho.State = HOStateComplete
ho.CompletedBy = characterID
}
return true
}
}
return false
}
// UpdateTimer updates the remaining time for the HO
func (ho *HeroicOP) UpdateTimer(deltaMS int32) bool {
ho.mu.Lock()
defer ho.mu.Unlock()
if ho.State != HOStateWheelPhase {
return true // Timer not active
}
ho.TimeRemaining -= deltaMS
if ho.TimeRemaining <= 0 {
ho.TimeRemaining = 0
ho.State = HOStateFailed
ho.SaveNeeded = true
return false // Timer expired
}
ho.SaveNeeded = true
return true
}
// IsComplete checks if the HO is successfully completed
func (ho *HeroicOP) IsComplete() bool {
ho.mu.RLock()
defer ho.mu.RUnlock()
return ho.Complete == HOComplete && ho.State == HOStateComplete
}
// IsFailed checks if the HO has failed
func (ho *HeroicOP) IsFailed() bool {
ho.mu.RLock()
defer ho.mu.RUnlock()
return ho.State == HOStateFailed
}
// IsActive checks if the HO is currently active (in progress)
func (ho *HeroicOP) IsActive() bool {
ho.mu.RLock()
defer ho.mu.RUnlock()
return ho.State == HOStateStarterChain || ho.State == HOStateWheelPhase
}
// GetProgress returns the completion percentage (0.0 - 1.0)
func (ho *HeroicOP) GetProgress() float32 {
ho.mu.RLock()
defer ho.mu.RUnlock()
if ho.State != HOStateWheelPhase {
return 0.0
}
completed := 0
total := 0
for i := 0; i < MaxAbilities; i++ {
if ho.Countered[i] != 0 {
completed++
}
if ho.Countered[i] != 0 || ho.Countered[i] == 0 { // All positions count
total++
}
}
if total == 0 {
return 0.0
}
return float32(completed) / float32(total)
}
// GetPacketData returns data formatted for client packets
func (ho *HeroicOP) GetPacketData(wheel *HeroicOPWheel) *PacketData {
ho.mu.RLock()
defer ho.mu.RUnlock()
data := &PacketData{
SpellName: ho.SpellName,
SpellDescription: ho.SpellDescription,
TimeRemaining: ho.TimeRemaining,
TotalTime: ho.TotalTime,
Complete: ho.Complete,
State: ho.State,
CanShift: false,
ShiftIcon: 0,
}
if wheel != nil {
data.Abilities = wheel.Abilities
data.CanShift = ho.ShiftUsed == ShiftNotUsed && wheel.HasShift()
data.ShiftIcon = wheel.ShiftIcon
}
data.Countered = ho.Countered
return data
}
// Validate checks if the HO instance is in a valid state
func (ho *HeroicOP) Validate() error {
ho.mu.RLock()
defer ho.mu.RUnlock()
if ho.ID <= 0 {
return fmt.Errorf("invalid HO instance ID: %d", ho.ID)
}
if ho.EncounterID <= 0 {
return fmt.Errorf("invalid encounter ID: %d", ho.EncounterID)
}
if ho.State < HOStateInactive || ho.State > HOStateFailed {
return fmt.Errorf("invalid HO state: %d", ho.State)
}
if ho.State == HOStateWheelPhase {
if ho.WheelID <= 0 {
return fmt.Errorf("wheel phase requires valid wheel ID")
}
if ho.TotalTime <= 0 {
return fmt.Errorf("wheel phase requires valid timer")
}
}
return nil
}
// GetElapsedTime returns the elapsed time since HO started
func (ho *HeroicOP) GetElapsedTime() time.Duration {
ho.mu.RLock()
defer ho.mu.RUnlock()
return time.Since(ho.StartTime)
}
// GetWheelElapsedTime returns the elapsed time since wheel phase started
func (ho *HeroicOP) GetWheelElapsedTime() time.Duration {
ho.mu.RLock()
defer ho.mu.RUnlock()
if ho.State != HOStateWheelPhase {
return 0
}
return time.Since(ho.WheelStartTime)
}