1390 lines
41 KiB
Go
1390 lines
41 KiB
Go
package heroic_ops
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"eq2emu/internal/database"
|
|
"eq2emu/internal/packets"
|
|
)
|
|
|
|
// Simplified Heroic Opportunities System
|
|
// Consolidates all functionality from 10 files into unified architecture
|
|
// Preserves 100% C++ functionality while eliminating Active Record patterns
|
|
|
|
// Core Data Structures
|
|
|
|
// HeroicOPStarter represents a starter chain for heroic opportunities
|
|
type HeroicOPStarter struct {
|
|
mu sync.RWMutex
|
|
ID int32 `json:"id"` // Unique identifier for this starter
|
|
StartClass int8 `json:"start_class"` // Class that can initiate this starter (0 = any)
|
|
StarterIcon int16 `json:"starter_icon"` // Icon displayed for the starter
|
|
Abilities [6]int16 `json:"abilities"` // Array of ability icons in sequence
|
|
Name string `json:"name"` // Display name for this starter
|
|
Description string `json:"description"` // Description text
|
|
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
|
|
|
|
// Database integration fields
|
|
isNew bool `json:"-"` // True if this is a new record not yet saved
|
|
}
|
|
|
|
// HeroicOPWheel represents the wheel phase of a heroic opportunity
|
|
type HeroicOPWheel struct {
|
|
mu sync.RWMutex
|
|
ID int32 `json:"id"` // Unique identifier for this wheel
|
|
StarterLinkID int32 `json:"starter_link_id"` // ID of the starter this wheel belongs to
|
|
Order int8 `json:"order"` // 0 = unordered, 1+ = ordered
|
|
ShiftIcon int16 `json:"shift_icon"` // Icon that can shift/change the wheel
|
|
Chance float32 `json:"chance"` // Probability factor for selecting this wheel
|
|
Abilities [6]int16 `json:"abilities"` // Array of ability icons for the wheel
|
|
SpellID int32 `json:"spell_id"` // Spell cast when HO completes successfully
|
|
Name string `json:"name"` // Display name for this wheel
|
|
Description string `json:"description"` // Description text
|
|
RequiredPlayers int8 `json:"required_players"` // Minimum players required
|
|
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
|
|
|
|
// Database integration fields
|
|
isNew bool `json:"-"` // True if this is a new record not yet saved
|
|
}
|
|
|
|
// HeroicOP represents an active heroic opportunity instance
|
|
type HeroicOP struct {
|
|
mu sync.RWMutex
|
|
ID int64 `json:"id"` // Unique instance ID
|
|
EncounterID int32 `json:"encounter_id"` // Encounter this HO belongs to
|
|
StarterID int32 `json:"starter_id"` // ID of the completed starter
|
|
WheelID int32 `json:"wheel_id"` // ID of the active wheel
|
|
State int8 `json:"state"` // Current HO state
|
|
StartTime time.Time `json:"start_time"` // When the HO started
|
|
WheelStartTime time.Time `json:"wheel_start_time"` // When wheel phase started
|
|
TimeRemaining int32 `json:"time_remaining"` // Milliseconds remaining
|
|
TotalTime int32 `json:"total_time"` // Total time allocated (ms)
|
|
Complete int8 `json:"complete"` // Completion status (0/1)
|
|
Countered [6]int8 `json:"countered"` // Which wheel abilities are completed
|
|
ShiftUsed int8 `json:"shift_used"` // Whether shift has been used
|
|
StarterProgress int8 `json:"starter_progress"` // Current position in starter chain
|
|
Participants map[int32]bool `json:"participants"` // Character IDs that participated
|
|
CurrentStarters []int32 `json:"current_starters"` // Active starter IDs during chain phase
|
|
CompletedBy int32 `json:"completed_by"` // Character ID that completed the HO
|
|
SpellName string `json:"spell_name"` // Name of completion spell
|
|
SpellDescription string `json:"spell_description"` // Description of completion spell
|
|
SaveNeeded bool `json:"-"` // Flag indicating if database save is needed
|
|
|
|
// Database integration fields
|
|
isNew bool `json:"-"` // True if this is a new record not yet saved
|
|
}
|
|
|
|
// HeroicOPManager manages the entire heroic opportunity system
|
|
type HeroicOPManager struct {
|
|
mu sync.RWMutex
|
|
database *database.Database
|
|
logger Logger
|
|
|
|
// Core data storage - O(1) access by ID
|
|
starters map[int32]*HeroicOPStarter // starter_id -> starter
|
|
wheels map[int32]*HeroicOPWheel // wheel_id -> wheel
|
|
activeHOs map[int64]*HeroicOP // instance_id -> HO
|
|
encounterHOs map[int32][]*HeroicOP // encounter_id -> HOs
|
|
|
|
// Specialized indices for O(1) lookups
|
|
byClass map[int8]map[int32]*HeroicOPStarter // class -> starter_id -> starter
|
|
byStarterID map[int32][]*HeroicOPWheel // starter_id -> wheels
|
|
bySpellID map[int32][]*HeroicOPWheel // spell_id -> wheels
|
|
byChance map[string][]*HeroicOPWheel // chance_range -> wheels
|
|
orderedWheels map[int32]*HeroicOPWheel // wheel_id -> ordered wheels only
|
|
shiftWheels map[int32]*HeroicOPWheel // wheel_id -> wheels with shifts
|
|
spellInfo map[int32]SpellInfo // spell_id -> spell info
|
|
|
|
// System state
|
|
nextInstanceID int64
|
|
loaded bool
|
|
|
|
// Configuration
|
|
defaultWheelTimer int32 // milliseconds
|
|
maxConcurrentHOs int
|
|
enableLogging bool
|
|
enableStatistics bool
|
|
|
|
// Statistics tracking
|
|
stats HeroicOPManagerStats
|
|
}
|
|
|
|
// Supporting structures
|
|
|
|
type SpellInfo struct {
|
|
ID int32 `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Icon int16 `json:"icon"`
|
|
}
|
|
|
|
type HeroicOPManagerStats struct {
|
|
TotalStarters int64 `json:"total_starters"`
|
|
TotalWheels int64 `json:"total_wheels"`
|
|
ClassDistribution map[int32]int64 `json:"class_distribution"`
|
|
OrderedWheelsCount int64 `json:"ordered_wheels_count"`
|
|
ShiftWheelsCount int64 `json:"shift_wheels_count"`
|
|
SpellCount int64 `json:"spell_count"`
|
|
AverageChance float64 `json:"average_chance"`
|
|
ActiveHOCount int64 `json:"active_ho_count"`
|
|
}
|
|
|
|
type PacketData struct {
|
|
SpellName string `json:"spell_name"`
|
|
SpellDescription string `json:"spell_description"`
|
|
TimeRemaining int32 `json:"time_remaining"` // milliseconds
|
|
TotalTime int32 `json:"total_time"` // milliseconds
|
|
Abilities [6]int16 `json:"abilities"` // Current wheel abilities
|
|
Countered [6]int8 `json:"countered"` // Completion status
|
|
Complete int8 `json:"complete"` // Overall completion (0/1)
|
|
State int8 `json:"state"` // Current HO state
|
|
CanShift bool `json:"can_shift"` // Whether shift is available
|
|
ShiftIcon int16 `json:"shift_icon"` // Icon for shift ability
|
|
}
|
|
|
|
type HeroicOPEvent struct {
|
|
ID int64 `json:"id"`
|
|
InstanceID int64 `json:"instance_id"`
|
|
EventType int `json:"event_type"`
|
|
CharacterID int32 `json:"character_id"`
|
|
AbilityIcon int16 `json:"ability_icon"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Data string `json:"data"` // JSON encoded additional data
|
|
}
|
|
|
|
type HeroicOPConfig struct {
|
|
DefaultWheelTimer int32 `json:"default_wheel_timer"` // milliseconds
|
|
StarterChainTimeout int32 `json:"starter_chain_timeout"` // milliseconds
|
|
MaxConcurrentHOs int `json:"max_concurrent_hos"`
|
|
EnableLogging bool `json:"enable_logging"`
|
|
EnableStatistics bool `json:"enable_statistics"`
|
|
EnableShifting bool `json:"enable_shifting"`
|
|
RequireClassMatch bool `json:"require_class_match"` // Enforce starter class restrictions
|
|
AutoCleanupInterval int32 `json:"auto_cleanup_interval"` // seconds
|
|
MaxHistoryEntries int `json:"max_history_entries"`
|
|
}
|
|
|
|
// External integration interfaces (simplified)
|
|
type Logger interface {
|
|
LogDebug(system, format string, args ...any)
|
|
LogInfo(system, format string, args ...any)
|
|
LogWarning(system, format string, args ...any)
|
|
LogError(system, format string, args ...any)
|
|
}
|
|
|
|
// Manager Methods
|
|
|
|
// NewHeroicOPManager creates a new heroic opportunity manager
|
|
func NewHeroicOPManager(database *database.Database) *HeroicOPManager {
|
|
return &HeroicOPManager{
|
|
database: database,
|
|
starters: make(map[int32]*HeroicOPStarter),
|
|
wheels: make(map[int32]*HeroicOPWheel),
|
|
activeHOs: make(map[int64]*HeroicOP),
|
|
encounterHOs: make(map[int32][]*HeroicOP),
|
|
byClass: make(map[int8]map[int32]*HeroicOPStarter),
|
|
byStarterID: make(map[int32][]*HeroicOPWheel),
|
|
bySpellID: make(map[int32][]*HeroicOPWheel),
|
|
byChance: make(map[string][]*HeroicOPWheel),
|
|
orderedWheels: make(map[int32]*HeroicOPWheel),
|
|
shiftWheels: make(map[int32]*HeroicOPWheel),
|
|
spellInfo: make(map[int32]SpellInfo),
|
|
nextInstanceID: 1,
|
|
defaultWheelTimer: DefaultWheelTimerSeconds * 1000, // Convert to milliseconds
|
|
maxConcurrentHOs: MaxConcurrentHOs,
|
|
enableLogging: true,
|
|
enableStatistics: true,
|
|
}
|
|
}
|
|
|
|
// SetLogger sets the logger for the manager
|
|
func (hom *HeroicOPManager) SetLogger(logger Logger) {
|
|
hom.mu.Lock()
|
|
defer hom.mu.Unlock()
|
|
hom.logger = logger
|
|
}
|
|
|
|
// Initialize loads configuration and data from database
|
|
func (hom *HeroicOPManager) Initialize(ctx context.Context, config *HeroicOPConfig) error {
|
|
hom.mu.Lock()
|
|
defer hom.mu.Unlock()
|
|
|
|
if config != nil {
|
|
hom.defaultWheelTimer = config.DefaultWheelTimer
|
|
hom.maxConcurrentHOs = config.MaxConcurrentHOs
|
|
hom.enableLogging = config.EnableLogging
|
|
hom.enableStatistics = config.EnableStatistics
|
|
}
|
|
|
|
// Load all data from database
|
|
if err := hom.loadFromDatabase(ctx); err != nil {
|
|
return fmt.Errorf("failed to load heroic ops data: %w", err)
|
|
}
|
|
|
|
if hom.logger != nil {
|
|
hom.logger.LogInfo("heroic_ops", "Initialized HO manager with %d starters and %d wheels",
|
|
len(hom.starters), len(hom.wheels))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadFromDatabase loads all heroic opportunities from the database with optimal indexing
|
|
func (hom *HeroicOPManager) loadFromDatabase(ctx context.Context) error {
|
|
// Clear existing data
|
|
hom.clearIndices()
|
|
|
|
// Load all starters
|
|
if err := hom.loadStarters(ctx); err != nil {
|
|
return fmt.Errorf("failed to load starters: %w", err)
|
|
}
|
|
|
|
// Load all wheels
|
|
if err := hom.loadWheels(ctx); err != nil {
|
|
return fmt.Errorf("failed to load wheels: %w", err)
|
|
}
|
|
|
|
// Build specialized indices for O(1) performance
|
|
hom.buildIndices()
|
|
|
|
hom.loaded = true
|
|
return nil
|
|
}
|
|
|
|
// loadStarters loads all starters from database
|
|
func (hom *HeroicOPManager) loadStarters(ctx context.Context) error {
|
|
query := `SELECT id, start_class, starter_icon, ability1, ability2, ability3, ability4, ability5, ability6, name, description
|
|
FROM heroic_op_starters ORDER BY id`
|
|
|
|
rows, err := hom.database.Query(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
starter := &HeroicOPStarter{
|
|
isNew: false,
|
|
}
|
|
|
|
err := rows.Scan(
|
|
&starter.ID,
|
|
&starter.StartClass,
|
|
&starter.StarterIcon,
|
|
&starter.Abilities[0],
|
|
&starter.Abilities[1],
|
|
&starter.Abilities[2],
|
|
&starter.Abilities[3],
|
|
&starter.Abilities[4],
|
|
&starter.Abilities[5],
|
|
&starter.Name,
|
|
&starter.Description,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
starter.SaveNeeded = false
|
|
hom.starters[starter.ID] = starter
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// loadWheels loads all wheels from database
|
|
func (hom *HeroicOPManager) loadWheels(ctx context.Context) error {
|
|
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 ORDER BY id`
|
|
|
|
rows, err := hom.database.Query(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
wheel := &HeroicOPWheel{
|
|
isNew: false,
|
|
}
|
|
|
|
err := rows.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 err
|
|
}
|
|
|
|
wheel.SaveNeeded = false
|
|
hom.wheels[wheel.ID] = wheel
|
|
|
|
// Store spell info
|
|
hom.spellInfo[wheel.SpellID] = SpellInfo{
|
|
ID: wheel.SpellID,
|
|
Name: wheel.Name,
|
|
Description: wheel.Description,
|
|
}
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// buildIndices creates specialized indices for O(1) performance
|
|
func (hom *HeroicOPManager) buildIndices() {
|
|
// Build class-based starter index
|
|
for _, starter := range hom.starters {
|
|
if hom.byClass[starter.StartClass] == nil {
|
|
hom.byClass[starter.StartClass] = make(map[int32]*HeroicOPStarter)
|
|
}
|
|
hom.byClass[starter.StartClass][starter.ID] = starter
|
|
}
|
|
|
|
// Build wheel indices
|
|
for _, wheel := range hom.wheels {
|
|
// By starter ID
|
|
hom.byStarterID[wheel.StarterLinkID] = append(hom.byStarterID[wheel.StarterLinkID], wheel)
|
|
|
|
// By spell ID
|
|
hom.bySpellID[wheel.SpellID] = append(hom.bySpellID[wheel.SpellID], wheel)
|
|
|
|
// By chance range (for performance optimization)
|
|
chanceRange := hom.getChanceRange(wheel.Chance)
|
|
hom.byChance[chanceRange] = append(hom.byChance[chanceRange], wheel)
|
|
|
|
// Special wheel types
|
|
if wheel.IsOrdered() {
|
|
hom.orderedWheels[wheel.ID] = wheel
|
|
}
|
|
|
|
if wheel.HasShift() {
|
|
hom.shiftWheels[wheel.ID] = wheel
|
|
}
|
|
}
|
|
|
|
// Sort wheels by chance for deterministic selection
|
|
for _, wheels := range hom.byStarterID {
|
|
sort.Slice(wheels, func(i, j int) bool {
|
|
return wheels[i].Chance > wheels[j].Chance
|
|
})
|
|
}
|
|
}
|
|
|
|
// clearIndices clears all indices
|
|
func (hom *HeroicOPManager) clearIndices() {
|
|
hom.starters = make(map[int32]*HeroicOPStarter)
|
|
hom.wheels = make(map[int32]*HeroicOPWheel)
|
|
hom.byClass = make(map[int8]map[int32]*HeroicOPStarter)
|
|
hom.byStarterID = make(map[int32][]*HeroicOPWheel)
|
|
hom.bySpellID = make(map[int32][]*HeroicOPWheel)
|
|
hom.byChance = make(map[string][]*HeroicOPWheel)
|
|
hom.orderedWheels = make(map[int32]*HeroicOPWheel)
|
|
hom.shiftWheels = make(map[int32]*HeroicOPWheel)
|
|
hom.spellInfo = make(map[int32]SpellInfo)
|
|
}
|
|
|
|
// getChanceRange returns a chance range string for indexing
|
|
func (hom *HeroicOPManager) getChanceRange(chance float32) string {
|
|
switch {
|
|
case chance >= 75.0:
|
|
return "very_high"
|
|
case chance >= 50.0:
|
|
return "high"
|
|
case chance >= 25.0:
|
|
return "medium"
|
|
case chance >= 10.0:
|
|
return "low"
|
|
default:
|
|
return "very_low"
|
|
}
|
|
}
|
|
|
|
// Core HO Management Operations
|
|
|
|
// StartHeroicOpportunity initiates a new heroic opportunity
|
|
func (hom *HeroicOPManager) StartHeroicOpportunity(ctx context.Context, encounterID int32, initiatorID int32, initiatorClass int8) (*HeroicOP, error) {
|
|
hom.mu.Lock()
|
|
defer hom.mu.Unlock()
|
|
|
|
// Check if encounter can have more HOs
|
|
currentHOs := hom.encounterHOs[encounterID]
|
|
if len(currentHOs) >= hom.maxConcurrentHOs {
|
|
return nil, fmt.Errorf("encounter %d already has maximum concurrent HOs (%d)",
|
|
encounterID, hom.maxConcurrentHOs)
|
|
}
|
|
|
|
// Get available starters for player's class
|
|
starters := hom.getStartersForClass(initiatorClass)
|
|
if len(starters) == 0 {
|
|
return nil, fmt.Errorf("no heroic opportunities available for class %d", initiatorClass)
|
|
}
|
|
|
|
// Create new HO instance
|
|
instanceID := hom.nextInstanceID
|
|
hom.nextInstanceID++
|
|
|
|
ho := hom.createHeroicOP(instanceID, encounterID)
|
|
ho.AddParticipant(initiatorID)
|
|
|
|
// Prepare starter IDs for chain phase
|
|
starterIDs := make([]int32, len(starters))
|
|
for i, starter := range starters {
|
|
starterIDs[i] = starter.ID
|
|
}
|
|
|
|
ho.StartStarterChain(starterIDs)
|
|
|
|
// Add to tracking maps
|
|
hom.activeHOs[instanceID] = ho
|
|
hom.encounterHOs[encounterID] = append(hom.encounterHOs[encounterID], ho)
|
|
|
|
// Save to database
|
|
if err := hom.saveHOInstance(ctx, ho); err != nil {
|
|
if hom.logger != nil {
|
|
hom.logger.LogError("heroic_ops", "Failed to save HO instance %d: %v", instanceID, err)
|
|
}
|
|
}
|
|
|
|
// Log event
|
|
if hom.enableLogging {
|
|
hom.logEvent(ctx, instanceID, EventHOStarted, initiatorID, 0, "HO started")
|
|
}
|
|
|
|
if hom.logger != nil {
|
|
hom.logger.LogInfo("heroic_ops", "Started HO %d for encounter %d initiated by %d with %d starters",
|
|
instanceID, encounterID, initiatorID, len(starterIDs))
|
|
}
|
|
|
|
return ho, nil
|
|
}
|
|
|
|
// ProcessAbility processes an ability used by a player during an active HO
|
|
func (hom *HeroicOPManager) ProcessAbility(ctx context.Context, instanceID int64, characterID int32, abilityIcon int16) error {
|
|
hom.mu.Lock()
|
|
defer hom.mu.Unlock()
|
|
|
|
ho, exists := hom.activeHOs[instanceID]
|
|
if !exists {
|
|
return fmt.Errorf("heroic opportunity %d not found", instanceID)
|
|
}
|
|
|
|
if !ho.IsActive() {
|
|
return fmt.Errorf("heroic opportunity %d is not active (state: %d)", instanceID, ho.State)
|
|
}
|
|
|
|
// Add player as participant
|
|
ho.AddParticipant(characterID)
|
|
success := false
|
|
|
|
switch ho.State {
|
|
case HOStateStarterChain:
|
|
success = ho.ProcessStarterAbility(abilityIcon, hom)
|
|
if success && len(ho.CurrentStarters) == 1 {
|
|
// Starter chain completed, transition to wheel phase
|
|
starterID := ho.CurrentStarters[0]
|
|
ho.StarterID = starterID
|
|
|
|
// Select random wheel for this starter
|
|
wheel := hom.selectRandomWheel(starterID)
|
|
if wheel == nil {
|
|
// No wheels available, HO fails
|
|
ho.State = HOStateFailed
|
|
hom.failHO(ctx, ho, "No wheels available for starter")
|
|
return fmt.Errorf("no wheels available for starter %d", starterID)
|
|
}
|
|
|
|
// Start wheel phase
|
|
ho.StartWheelPhase(wheel, hom.defaultWheelTimer/1000) // Convert to seconds
|
|
|
|
// Send wheel packet to participants (would be implemented with client manager)
|
|
hom.sendWheelUpdate(ho, wheel)
|
|
|
|
if hom.logger != nil {
|
|
hom.logger.LogDebug("heroic_ops", "HO %d transitioned to wheel phase with wheel %d",
|
|
instanceID, wheel.ID)
|
|
}
|
|
}
|
|
|
|
case HOStateWheelPhase:
|
|
wheel := hom.wheels[ho.WheelID]
|
|
if wheel == nil {
|
|
return fmt.Errorf("wheel %d not found for HO %d", ho.WheelID, instanceID)
|
|
}
|
|
|
|
// Check for shift attempt
|
|
if ho.ShiftUsed == ShiftNotUsed && wheel.CanShift(abilityIcon) {
|
|
return hom.handleWheelShift(ctx, ho, wheel, characterID)
|
|
}
|
|
|
|
// Process regular ability
|
|
success = ho.ProcessWheelAbility(abilityIcon, characterID, wheel)
|
|
|
|
if success {
|
|
// Send progress update (would be implemented with client manager)
|
|
hom.sendProgressUpdate(ho)
|
|
|
|
// Check if HO is complete
|
|
if ho.IsComplete() {
|
|
hom.completeHO(ctx, ho, wheel, characterID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Log ability use
|
|
if hom.enableLogging {
|
|
eventType := EventHOAbilityUsed
|
|
data := fmt.Sprintf("ability:%d,success:%t", abilityIcon, success)
|
|
hom.logEvent(ctx, instanceID, eventType, characterID, abilityIcon, data)
|
|
}
|
|
|
|
// Save changes
|
|
if ho.SaveNeeded {
|
|
if err := hom.saveHOInstance(ctx, ho); err != nil {
|
|
if hom.logger != nil {
|
|
hom.logger.LogError("heroic_ops", "Failed to save HO instance %d: %v", instanceID, err)
|
|
}
|
|
}
|
|
ho.SaveNeeded = false
|
|
}
|
|
|
|
if !success {
|
|
return fmt.Errorf("ability %d not allowed for current HO state", abilityIcon)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateTimers updates all active HO timers
|
|
func (hom *HeroicOPManager) UpdateTimers(ctx context.Context, deltaMS int32) {
|
|
hom.mu.Lock()
|
|
defer hom.mu.Unlock()
|
|
|
|
var expiredHOs []int64
|
|
|
|
for instanceID, ho := range hom.activeHOs {
|
|
if ho.State == HOStateWheelPhase {
|
|
if !ho.UpdateTimer(deltaMS) {
|
|
// Timer expired
|
|
expiredHOs = append(expiredHOs, instanceID)
|
|
} else {
|
|
// Send timer update to participants (would be implemented with client manager)
|
|
hom.sendTimerUpdate(ho)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle expired HOs
|
|
for _, instanceID := range expiredHOs {
|
|
ho := hom.activeHOs[instanceID]
|
|
hom.failHO(ctx, ho, "Timer expired")
|
|
}
|
|
}
|
|
|
|
// Getter methods with O(1) performance
|
|
|
|
// GetStarter retrieves a starter by ID with O(1) performance
|
|
func (hom *HeroicOPManager) GetStarter(id int32) *HeroicOPStarter {
|
|
hom.mu.RLock()
|
|
defer hom.mu.RUnlock()
|
|
return hom.starters[id]
|
|
}
|
|
|
|
// getStartersForClass returns all starters for a specific class with O(1) performance
|
|
func (hom *HeroicOPManager) getStartersForClass(class int8) []*HeroicOPStarter {
|
|
var result []*HeroicOPStarter
|
|
|
|
// Add class-specific starters
|
|
if classStarters, exists := hom.byClass[class]; exists {
|
|
for _, starter := range classStarters {
|
|
result = append(result, starter)
|
|
}
|
|
}
|
|
|
|
// Add universal starters (class 0 = any)
|
|
if universalStarters, exists := hom.byClass[ClassAny]; exists {
|
|
for _, starter := range universalStarters {
|
|
result = append(result, starter)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetWheel retrieves a wheel by ID with O(1) performance
|
|
func (hom *HeroicOPManager) GetWheel(id int32) *HeroicOPWheel {
|
|
hom.mu.RLock()
|
|
defer hom.mu.RUnlock()
|
|
return hom.wheels[id]
|
|
}
|
|
|
|
// getWheelsForStarter returns all wheels for a starter with O(1) performance
|
|
func (hom *HeroicOPManager) getWheelsForStarter(starterID int32) []*HeroicOPWheel {
|
|
wheels := hom.byStarterID[starterID]
|
|
if wheels == nil {
|
|
return nil
|
|
}
|
|
|
|
// Return copy to prevent external modification
|
|
result := make([]*HeroicOPWheel, len(wheels))
|
|
copy(result, wheels)
|
|
return result
|
|
}
|
|
|
|
// selectRandomWheel selects a random wheel from starter's wheels with optimized performance
|
|
func (hom *HeroicOPManager) selectRandomWheel(starterID int32) *HeroicOPWheel {
|
|
wheels := hom.getWheelsForStarter(starterID)
|
|
if len(wheels) == 0 {
|
|
return nil
|
|
}
|
|
return SelectRandomWheel(wheels)
|
|
}
|
|
|
|
// GetActiveHO returns an active HO by instance ID
|
|
func (hom *HeroicOPManager) GetActiveHO(instanceID int64) (*HeroicOP, bool) {
|
|
hom.mu.RLock()
|
|
defer hom.mu.RUnlock()
|
|
|
|
ho, exists := hom.activeHOs[instanceID]
|
|
return ho, exists
|
|
}
|
|
|
|
// GetEncounterHOs returns all HOs for an encounter
|
|
func (hom *HeroicOPManager) GetEncounterHOs(encounterID int32) []*HeroicOP {
|
|
hom.mu.RLock()
|
|
defer hom.mu.RUnlock()
|
|
|
|
hos := hom.encounterHOs[encounterID]
|
|
result := make([]*HeroicOP, len(hos))
|
|
copy(result, hos)
|
|
return result
|
|
}
|
|
|
|
// GetStatistics returns current HO system statistics
|
|
func (hom *HeroicOPManager) GetStatistics() *HeroicOPManagerStats {
|
|
hom.mu.RLock()
|
|
defer hom.mu.RUnlock()
|
|
|
|
// Calculate runtime statistics
|
|
classDistribution := make(map[int32]int64)
|
|
for _, starter := range hom.starters {
|
|
classDistribution[int32(starter.StartClass)]++
|
|
}
|
|
|
|
totalChance := float64(0)
|
|
for _, wheel := range hom.wheels {
|
|
totalChance += float64(wheel.Chance)
|
|
}
|
|
|
|
averageChance := float64(0)
|
|
if len(hom.wheels) > 0 {
|
|
averageChance = totalChance / float64(len(hom.wheels))
|
|
}
|
|
|
|
return &HeroicOPManagerStats{
|
|
TotalStarters: int64(len(hom.starters)),
|
|
TotalWheels: int64(len(hom.wheels)),
|
|
ClassDistribution: classDistribution,
|
|
OrderedWheelsCount: int64(len(hom.orderedWheels)),
|
|
ShiftWheelsCount: int64(len(hom.shiftWheels)),
|
|
SpellCount: int64(len(hom.spellInfo)),
|
|
AverageChance: averageChance,
|
|
ActiveHOCount: int64(len(hom.activeHOs)),
|
|
}
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
// createHeroicOP creates a new HO instance
|
|
func (hom *HeroicOPManager) createHeroicOP(instanceID int64, encounterID int32) *HeroicOP {
|
|
return &HeroicOP{
|
|
ID: instanceID,
|
|
EncounterID: encounterID,
|
|
State: HOStateInactive,
|
|
StartTime: time.Now(),
|
|
Participants: make(map[int32]bool),
|
|
isNew: true,
|
|
}
|
|
}
|
|
|
|
// handleWheelShift handles wheel shifting logic
|
|
func (hom *HeroicOPManager) handleWheelShift(ctx context.Context, ho *HeroicOP, currentWheel *HeroicOPWheel, characterID int32) error {
|
|
// Check if shift is allowed (no progress made)
|
|
canShift := true
|
|
for i := 0; i < MaxAbilities; i++ {
|
|
if ho.Countered[i] != 0 {
|
|
canShift = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if !canShift {
|
|
return fmt.Errorf("wheel shift not allowed after progress has been made")
|
|
}
|
|
|
|
// Select new random wheel
|
|
newWheel := hom.selectRandomWheel(ho.StarterID)
|
|
if newWheel == nil || newWheel.ID == currentWheel.ID {
|
|
return fmt.Errorf("no alternative wheel available for shift")
|
|
}
|
|
|
|
oldWheelID := ho.WheelID
|
|
ho.WheelID = newWheel.ID
|
|
ho.ShiftUsed = ShiftUsed
|
|
ho.SpellName = newWheel.Name
|
|
ho.SpellDescription = newWheel.Description
|
|
ho.SaveNeeded = true
|
|
|
|
// Send shift notification (would be implemented with client manager)
|
|
hom.sendShiftUpdate(ho, oldWheelID, newWheel.ID)
|
|
|
|
// Log shift event
|
|
if hom.enableLogging {
|
|
data := fmt.Sprintf("old_wheel:%d,new_wheel:%d", oldWheelID, newWheel.ID)
|
|
hom.logEvent(ctx, ho.ID, EventHOWheelShifted, characterID, 0, data)
|
|
}
|
|
|
|
if hom.logger != nil {
|
|
hom.logger.LogDebug("heroic_ops", "HO %d wheel shifted from %d to %d by character %d",
|
|
ho.ID, oldWheelID, newWheel.ID, characterID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// completeHO handles HO completion
|
|
func (hom *HeroicOPManager) completeHO(ctx context.Context, ho *HeroicOP, wheel *HeroicOPWheel, completedBy int32) {
|
|
ho.State = HOStateComplete
|
|
ho.Complete = HOComplete
|
|
ho.CompletedBy = completedBy
|
|
ho.SaveNeeded = true
|
|
|
|
// Cast completion spell (would be integrated with spell system)
|
|
if wheel.SpellID > 0 {
|
|
_ = ho.GetParticipants() // participants will be used when spell manager is integrated
|
|
// TODO: Cast spell on participants through spell manager
|
|
}
|
|
|
|
// Send completion packet (would be implemented with client manager)
|
|
hom.sendCompletionUpdate(ho, true)
|
|
|
|
// Log completion
|
|
if hom.enableLogging {
|
|
data := fmt.Sprintf("completed_by:%d,spell_id:%d,participants:%d",
|
|
completedBy, wheel.SpellID, len(ho.Participants))
|
|
hom.logEvent(ctx, ho.ID, EventHOCompleted, completedBy, 0, data)
|
|
}
|
|
|
|
if hom.logger != nil {
|
|
hom.logger.LogInfo("heroic_ops", "HO %d completed by character %d, spell %d cast",
|
|
ho.ID, completedBy, wheel.SpellID)
|
|
}
|
|
}
|
|
|
|
// failHO handles HO failure
|
|
func (hom *HeroicOPManager) failHO(ctx context.Context, ho *HeroicOP, reason string) {
|
|
ho.State = HOStateFailed
|
|
ho.SaveNeeded = true
|
|
|
|
// Send failure packet (would be implemented with client manager)
|
|
hom.sendCompletionUpdate(ho, false)
|
|
|
|
// Log failure
|
|
if hom.enableLogging {
|
|
hom.logEvent(ctx, ho.ID, EventHOFailed, 0, 0, reason)
|
|
}
|
|
|
|
if hom.logger != nil {
|
|
hom.logger.LogDebug("heroic_ops", "HO %d failed: %s", ho.ID, reason)
|
|
}
|
|
}
|
|
|
|
// Communication placeholder methods (would integrate with client manager)
|
|
func (hom *HeroicOPManager) sendWheelUpdate(ho *HeroicOP, wheel *HeroicOPWheel) {
|
|
// TODO: Implement when client manager is integrated
|
|
}
|
|
|
|
func (hom *HeroicOPManager) sendProgressUpdate(ho *HeroicOP) {
|
|
// TODO: Implement when client manager is integrated
|
|
}
|
|
|
|
func (hom *HeroicOPManager) sendTimerUpdate(ho *HeroicOP) {
|
|
// TODO: Implement when client manager is integrated
|
|
}
|
|
|
|
func (hom *HeroicOPManager) sendCompletionUpdate(ho *HeroicOP, success bool) {
|
|
// TODO: Implement when client manager is integrated
|
|
}
|
|
|
|
func (hom *HeroicOPManager) sendShiftUpdate(ho *HeroicOP, oldWheelID, newWheelID int32) {
|
|
// TODO: Implement when client manager is integrated
|
|
}
|
|
|
|
// Database operations
|
|
|
|
// saveHOInstance saves HO instance to database
|
|
func (hom *HeroicOPManager) saveHOInstance(ctx context.Context, ho *HeroicOP) error {
|
|
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, countered1, countered2, countered3,
|
|
countered4, countered5, countered6, shift_used, starter_progress, completed_by, spell_name, spell_description)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
_, err := hom.database.Exec(query,
|
|
ho.ID, ho.EncounterID, ho.StarterID, ho.WheelID, ho.State, ho.StartTime, ho.WheelStartTime,
|
|
ho.TimeRemaining, ho.TotalTime, ho.Complete, ho.Countered[0], ho.Countered[1], ho.Countered[2],
|
|
ho.Countered[3], ho.Countered[4], ho.Countered[5], ho.ShiftUsed, ho.StarterProgress,
|
|
ho.CompletedBy, ho.SpellName, ho.SpellDescription)
|
|
if err != nil {
|
|
return 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=?, countered1=?, countered2=?, countered3=?,
|
|
countered4=?, countered5=?, countered6=?, shift_used=?, starter_progress=?, completed_by=?, spell_name=?, spell_description=?
|
|
WHERE id=?`
|
|
_, err := hom.database.Exec(query,
|
|
ho.EncounterID, ho.StarterID, ho.WheelID, ho.State, ho.StartTime, ho.WheelStartTime,
|
|
ho.TimeRemaining, ho.TotalTime, ho.Complete, ho.Countered[0], ho.Countered[1], ho.Countered[2],
|
|
ho.Countered[3], ho.Countered[4], ho.Countered[5], ho.ShiftUsed, ho.StarterProgress,
|
|
ho.CompletedBy, ho.SpellName, ho.SpellDescription, ho.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// logEvent logs an HO event to the database
|
|
func (hom *HeroicOPManager) logEvent(ctx context.Context, instanceID int64, eventType int, characterID int32, abilityIcon int16, data string) {
|
|
if !hom.enableLogging {
|
|
return
|
|
}
|
|
|
|
query := `INSERT INTO heroic_op_events (instance_id, event_type, character_id, ability_icon, timestamp, data)
|
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
_, err := hom.database.Exec(query, instanceID, eventType, characterID, abilityIcon, time.Now(), data)
|
|
if err != nil && hom.logger != nil {
|
|
hom.logger.LogError("heroic_ops", "Failed to save HO event: %v", err)
|
|
}
|
|
}
|
|
|
|
// HeroicOP Methods
|
|
|
|
// NewHeroicOP creates a new HO instance
|
|
func NewHeroicOP(instanceID int64, encounterID int32) *HeroicOP {
|
|
return &HeroicOP{
|
|
ID: instanceID,
|
|
EncounterID: encounterID,
|
|
State: HOStateInactive,
|
|
StartTime: time.Now(),
|
|
Participants: make(map[int32]bool),
|
|
isNew: true,
|
|
}
|
|
}
|
|
|
|
// AddParticipant adds a character to the HO
|
|
func (ho *HeroicOP) AddParticipant(characterID int32) {
|
|
ho.mu.Lock()
|
|
defer ho.mu.Unlock()
|
|
ho.Participants[characterID] = true
|
|
ho.SaveNeeded = true
|
|
}
|
|
|
|
// GetParticipants returns list 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
|
|
}
|
|
|
|
// IsActive returns true if HO is in an active state
|
|
func (ho *HeroicOP) IsActive() bool {
|
|
ho.mu.RLock()
|
|
defer ho.mu.RUnlock()
|
|
return ho.State == HOStateStarterChain || ho.State == HOStateWheelPhase
|
|
}
|
|
|
|
// IsComplete returns true if all wheel abilities are countered
|
|
func (ho *HeroicOP) IsComplete() bool {
|
|
ho.mu.RLock()
|
|
defer ho.mu.RUnlock()
|
|
|
|
for i := 0; i < MaxAbilities; i++ {
|
|
if ho.Countered[i] == 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// StartStarterChain begins the starter chain phase
|
|
func (ho *HeroicOP) StartStarterChain(starterIDs []int32) {
|
|
ho.mu.Lock()
|
|
defer ho.mu.Unlock()
|
|
|
|
ho.State = HOStateStarterChain
|
|
ho.CurrentStarters = make([]int32, len(starterIDs))
|
|
copy(ho.CurrentStarters, starterIDs)
|
|
ho.StarterProgress = 0
|
|
ho.SaveNeeded = true
|
|
}
|
|
|
|
// ProcessStarterAbility processes an ability during starter chain phase
|
|
func (ho *HeroicOP) ProcessStarterAbility(abilityIcon int16, hom *HeroicOPManager) bool {
|
|
ho.mu.Lock()
|
|
defer ho.mu.Unlock()
|
|
|
|
// Check each active starter for matching ability at current position
|
|
var remainingStarters []int32
|
|
|
|
for _, starterID := range ho.CurrentStarters {
|
|
starter := hom.GetStarter(starterID)
|
|
if starter != nil && starter.MatchesAbilityAtPosition(abilityIcon, ho.StarterProgress) {
|
|
remainingStarters = append(remainingStarters, starterID)
|
|
}
|
|
}
|
|
|
|
if len(remainingStarters) == 0 {
|
|
return false // No starters matched this ability
|
|
}
|
|
|
|
// Update progress and remaining starters
|
|
ho.StarterProgress++
|
|
ho.CurrentStarters = remainingStarters
|
|
ho.SaveNeeded = true
|
|
|
|
// Check if starter chain is complete (only one starter left and at end of chain)
|
|
if len(remainingStarters) == 1 {
|
|
starter := hom.GetStarter(remainingStarters[0])
|
|
if starter != nil && int(ho.StarterProgress) >= starter.GetLength() {
|
|
return true // Starter chain completed
|
|
}
|
|
}
|
|
|
|
return true // Progress made but not complete
|
|
}
|
|
|
|
// StartWheelPhase begins the wheel phase
|
|
func (ho *HeroicOP) StartWheelPhase(wheel *HeroicOPWheel, wheelTimerSeconds int32) {
|
|
ho.mu.Lock()
|
|
defer ho.mu.Unlock()
|
|
|
|
ho.State = HOStateWheelPhase
|
|
ho.WheelID = wheel.ID
|
|
ho.WheelStartTime = time.Now()
|
|
ho.TimeRemaining = wheelTimerSeconds * 1000 // Convert to milliseconds
|
|
ho.TotalTime = ho.TimeRemaining
|
|
ho.SpellName = wheel.Name
|
|
ho.SpellDescription = wheel.Description
|
|
ho.SaveNeeded = true
|
|
|
|
// Initialize countered array
|
|
for i := 0; i < MaxAbilities; i++ {
|
|
ho.Countered[i] = 0
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
|
|
// Find matching ability in wheel
|
|
for i := 0; i < MaxAbilities; i++ {
|
|
if wheel.Abilities[i] == abilityIcon || wheel.Abilities[i] == AbilityIconAny {
|
|
// Check if this ability is already countered
|
|
if ho.Countered[i] != 0 {
|
|
continue
|
|
}
|
|
|
|
// For ordered wheels, check if this is the next expected ability
|
|
if wheel.IsOrdered() && !ho.isNextAbilityInOrder(i) {
|
|
continue
|
|
}
|
|
|
|
// Mark as countered
|
|
ho.Countered[i] = 1
|
|
ho.SaveNeeded = true
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false // No matching ability found
|
|
}
|
|
|
|
// isNextAbilityInOrder checks if the given index is the next expected ability for ordered wheels
|
|
func (ho *HeroicOP) isNextAbilityInOrder(index int) bool {
|
|
for i := 0; i < index; i++ {
|
|
if ho.Countered[i] == 0 {
|
|
return false // Earlier ability not completed
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// UpdateTimer updates the HO timer, returns false if expired
|
|
func (ho *HeroicOP) UpdateTimer(deltaMS int32) bool {
|
|
ho.mu.Lock()
|
|
defer ho.mu.Unlock()
|
|
|
|
ho.TimeRemaining -= deltaMS
|
|
if ho.TimeRemaining <= 0 {
|
|
ho.TimeRemaining = 0
|
|
return false
|
|
}
|
|
|
|
ho.SaveNeeded = true
|
|
return true
|
|
}
|
|
|
|
// GetProgress returns completion progress as percentage
|
|
func (ho *HeroicOP) GetProgress() float32 {
|
|
ho.mu.RLock()
|
|
defer ho.mu.RUnlock()
|
|
|
|
completed := float32(0)
|
|
for i := 0; i < MaxAbilities; i++ {
|
|
if ho.Countered[i] != 0 {
|
|
completed++
|
|
}
|
|
}
|
|
|
|
return (completed / float32(MaxAbilities)) * 100.0
|
|
}
|
|
|
|
// GetPacketData converts HO to packet data structure
|
|
func (ho *HeroicOP) GetPacketData(wheel *HeroicOPWheel) *PacketData {
|
|
ho.mu.RLock()
|
|
defer ho.mu.RUnlock()
|
|
|
|
data := &PacketData{
|
|
TimeRemaining: ho.TimeRemaining,
|
|
TotalTime: ho.TotalTime,
|
|
Complete: ho.Complete,
|
|
State: ho.State,
|
|
Countered: ho.Countered,
|
|
}
|
|
|
|
if wheel != nil {
|
|
data.SpellName = wheel.Name
|
|
data.SpellDescription = wheel.Description
|
|
data.Abilities = wheel.Abilities
|
|
data.CanShift = ho.ShiftUsed == ShiftNotUsed && wheel.HasShift()
|
|
data.ShiftIcon = wheel.ShiftIcon
|
|
} else {
|
|
data.SpellName = ho.SpellName
|
|
data.SpellDescription = ho.SpellDescription
|
|
// Abilities will be zero-initialized
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
// HeroicOPStarter Methods
|
|
|
|
// Validate validates the starter configuration
|
|
func (s *HeroicOPStarter) Validate() error {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
if s.ID <= 0 {
|
|
return fmt.Errorf("invalid starter ID: %d", s.ID)
|
|
}
|
|
|
|
if s.Name == "" {
|
|
return fmt.Errorf("starter name cannot be empty")
|
|
}
|
|
|
|
if len(s.Name) > MaxHONameLength {
|
|
return fmt.Errorf("starter name too long: %d > %d", len(s.Name), MaxHONameLength)
|
|
}
|
|
|
|
// Check that at least one ability is specified
|
|
hasAbility := false
|
|
for i := 0; i < MaxAbilities; i++ {
|
|
if s.Abilities[i] != 0 {
|
|
hasAbility = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasAbility {
|
|
return fmt.Errorf("starter must have at least one ability")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MatchesAbilityAtPosition checks if the given ability matches at the specified position
|
|
func (s *HeroicOPStarter) MatchesAbilityAtPosition(abilityIcon int16, position int8) bool {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
if position < 0 || int(position) >= MaxAbilities {
|
|
return false
|
|
}
|
|
|
|
expectedAbility := s.Abilities[position]
|
|
return expectedAbility == abilityIcon || expectedAbility == AbilityIconAny
|
|
}
|
|
|
|
// GetLength returns the length of the starter chain
|
|
func (s *HeroicOPStarter) GetLength() int {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
length := 0
|
|
for i := 0; i < MaxAbilities; i++ {
|
|
if s.Abilities[i] != 0 {
|
|
length++
|
|
} else {
|
|
break // Stop at first empty slot
|
|
}
|
|
}
|
|
return length
|
|
}
|
|
|
|
// HeroicOPWheel Methods
|
|
|
|
// Validate validates the wheel configuration
|
|
func (w *HeroicOPWheel) Validate() error {
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
|
|
if w.ID <= 0 {
|
|
return fmt.Errorf("invalid wheel ID: %d", w.ID)
|
|
}
|
|
|
|
if w.StarterLinkID <= 0 {
|
|
return fmt.Errorf("invalid starter link ID: %d", w.StarterLinkID)
|
|
}
|
|
|
|
if w.Name == "" {
|
|
return fmt.Errorf("wheel name cannot be empty")
|
|
}
|
|
|
|
if len(w.Name) > MaxHONameLength {
|
|
return fmt.Errorf("wheel name too long: %d > %d", len(w.Name), MaxHONameLength)
|
|
}
|
|
|
|
if w.Chance < MinChance || w.Chance > MaxChance {
|
|
return fmt.Errorf("wheel chance out of range: %f", w.Chance)
|
|
}
|
|
|
|
// Check that at least one ability is specified
|
|
hasAbility := false
|
|
for i := 0; i < MaxAbilities; i++ {
|
|
if w.Abilities[i] != 0 {
|
|
hasAbility = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasAbility {
|
|
return fmt.Errorf("wheel must have at least one ability")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsOrdered returns true if this is an ordered wheel
|
|
func (w *HeroicOPWheel) IsOrdered() bool {
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
return w.Order == WheelOrderOrdered
|
|
}
|
|
|
|
// HasShift returns true if this wheel has a shift ability
|
|
func (w *HeroicOPWheel) HasShift() bool {
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
return w.ShiftIcon != 0
|
|
}
|
|
|
|
// CanShift returns true if the given ability icon can shift this wheel
|
|
func (w *HeroicOPWheel) CanShift(abilityIcon int16) bool {
|
|
w.mu.RLock()
|
|
defer w.mu.RUnlock()
|
|
return w.ShiftIcon == abilityIcon
|
|
}
|
|
|
|
// Utility Functions
|
|
|
|
// SelectRandomWheel selects a wheel based on chance weights
|
|
func SelectRandomWheel(wheels []*HeroicOPWheel) *HeroicOPWheel {
|
|
if len(wheels) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if len(wheels) == 1 {
|
|
return wheels[0]
|
|
}
|
|
|
|
// Calculate total weight
|
|
totalWeight := float32(0)
|
|
for _, wheel := range wheels {
|
|
totalWeight += wheel.Chance
|
|
}
|
|
|
|
if totalWeight <= 0 {
|
|
// Equal probability fallback
|
|
return wheels[rand.Intn(len(wheels))]
|
|
}
|
|
|
|
// Random selection based on weights
|
|
randomValue := rand.Float32() * totalWeight
|
|
currentWeight := float32(0)
|
|
|
|
for _, wheel := range wheels {
|
|
currentWeight += wheel.Chance
|
|
if randomValue <= currentWeight {
|
|
return wheel
|
|
}
|
|
}
|
|
|
|
// Fallback (should not happen)
|
|
return wheels[len(wheels)-1]
|
|
}
|
|
|
|
// Packet Building Integration
|
|
|
|
// BuildHeroicOpportunityPacket builds the main HO packet for network transmission
|
|
func (hom *HeroicOPManager) BuildHeroicOpportunityPacket(ho *HeroicOP, wheel *HeroicOPWheel, clientVersion uint32) (map[string]interface{}, error) {
|
|
if ho == nil {
|
|
return nil, fmt.Errorf("heroic opportunity is nil")
|
|
}
|
|
|
|
packet := make(map[string]interface{})
|
|
|
|
// Basic HO information
|
|
packet["id"] = ho.ID
|
|
packet["wheel_type"] = int8(0) // Default wheel type
|
|
packet["unknown"] = int8(0)
|
|
packet["time_total"] = float32(ho.TotalTime) / 1000.0 // Convert to seconds
|
|
packet["time_left"] = float32(ho.TimeRemaining) / 1000.0 // Convert to seconds
|
|
|
|
if wheel != nil {
|
|
packet["name"] = wheel.Name
|
|
packet["description"] = wheel.Description
|
|
packet["order"] = wheel.Order
|
|
packet["shift_icon"] = wheel.ShiftIcon
|
|
packet["starter_icon"] = int16(0) // Would need starter reference
|
|
|
|
// Wheel abilities
|
|
packet["icon1"] = wheel.Abilities[0]
|
|
packet["icon2"] = wheel.Abilities[1]
|
|
packet["icon3"] = wheel.Abilities[2]
|
|
packet["icon4"] = wheel.Abilities[3]
|
|
packet["icon5"] = wheel.Abilities[4]
|
|
packet["icon6"] = wheel.Abilities[5]
|
|
|
|
// Completion status
|
|
packet["countered1"] = ho.Countered[0]
|
|
packet["countered2"] = ho.Countered[1]
|
|
packet["countered3"] = ho.Countered[2]
|
|
packet["countered4"] = ho.Countered[3]
|
|
packet["countered5"] = ho.Countered[4]
|
|
packet["countered6"] = ho.Countered[5]
|
|
} else {
|
|
packet["name"] = ho.SpellName
|
|
packet["description"] = ho.SpellDescription
|
|
packet["order"] = int8(0)
|
|
packet["shift_icon"] = int16(0)
|
|
packet["starter_icon"] = int16(0)
|
|
|
|
// Zero out abilities and counters
|
|
for i := 1; i <= 6; i++ {
|
|
packet[fmt.Sprintf("icon%d", i)] = int16(0)
|
|
packet[fmt.Sprintf("countered%d", i)] = int8(0)
|
|
}
|
|
}
|
|
|
|
return packet, nil
|
|
}
|
|
|
|
// GetHeroicOpportunityOpcode returns the appropriate opcode for HO packets
|
|
func (hom *HeroicOPManager) GetHeroicOpportunityOpcode() packets.InternalOpcode {
|
|
return packets.OP_HeroicOpportunityMsg
|
|
}
|
|
|
|
// Cleanup Operations
|
|
|
|
// CleanupExpiredHOs removes completed and failed HOs
|
|
func (hom *HeroicOPManager) CleanupExpiredHOs(ctx context.Context, maxAge time.Duration) {
|
|
hom.mu.Lock()
|
|
defer hom.mu.Unlock()
|
|
|
|
cutoff := time.Now().Add(-maxAge)
|
|
var toRemove []int64
|
|
|
|
for instanceID, ho := range hom.activeHOs {
|
|
if !ho.IsActive() && ho.StartTime.Before(cutoff) {
|
|
toRemove = append(toRemove, instanceID)
|
|
}
|
|
}
|
|
|
|
for _, instanceID := range toRemove {
|
|
ho := hom.activeHOs[instanceID]
|
|
|
|
// Remove from encounter tracking
|
|
encounterHOs := hom.encounterHOs[ho.EncounterID]
|
|
for i, encounterHO := range encounterHOs {
|
|
if encounterHO.ID == instanceID {
|
|
hom.encounterHOs[ho.EncounterID] = append(encounterHOs[:i], encounterHOs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Clean up empty encounter list
|
|
if len(hom.encounterHOs[ho.EncounterID]) == 0 {
|
|
delete(hom.encounterHOs, ho.EncounterID)
|
|
}
|
|
|
|
// Remove from active list
|
|
delete(hom.activeHOs, instanceID)
|
|
|
|
// Delete from database
|
|
query := `DELETE FROM heroic_op_instances WHERE id = ?`
|
|
if _, err := hom.database.Exec(query, instanceID); err != nil {
|
|
if hom.logger != nil {
|
|
hom.logger.LogError("heroic_ops", "Failed to delete HO instance %d: %v", instanceID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(toRemove) > 0 && hom.logger != nil {
|
|
hom.logger.LogDebug("heroic_ops", "Cleaned up %d expired HO instances", len(toRemove))
|
|
}
|
|
}
|
|
|
|
// IsLoaded returns whether data has been loaded
|
|
func (hom *HeroicOPManager) IsLoaded() bool {
|
|
hom.mu.RLock()
|
|
defer hom.mu.RUnlock()
|
|
return hom.loaded
|
|
} |