package tradeskills import ( "fmt" "log" "math/rand" "time" ) // NewTradeskillManager creates a new tradeskill manager with default configuration. func NewTradeskillManager() *TradeskillManager { return &TradeskillManager{ tradeskillList: make(map[uint32]*Tradeskill), critFailChance: DefaultCritFailChance, critSuccessChance: DefaultCritSuccessChance, failChance: DefaultFailChance, successChance: DefaultSuccessChance, eventChance: DefaultEventChance, stats: TradeskillManagerStats{ LastUpdate: time.Now(), }, } } // Process handles periodic updates for all active tradeskill sessions. // This should be called regularly (typically every 50ms) by the server. func (tm *TradeskillManager) Process() { tm.mutex.Lock() defer tm.mutex.Unlock() currentTime := time.Now() // Process each active tradeskill session for playerID, tradeskill := range tm.tradeskillList { if tradeskill == nil { continue } // Check if this tradeskill needs an update if !tradeskill.NeedsUpdate() { continue } outcome := tm.processCraftingUpdate(tradeskill) // TODO: Send update packets to client // This would need integration with the packet system log.Printf("Crafting update for player %d: Progress=%d, Durability=%d, Success=%v", playerID, tradeskill.CurrentProgress, tradeskill.CurrentDurability, outcome.Success) // Check if crafting is complete or failed if outcome.Completed { tm.completeCrafting(playerID, tradeskill, true) } else if outcome.Failed { tm.completeCrafting(playerID, tradeskill, false) } else { // Schedule next update tradeskill.NextUpdateTime = currentTime.Add(CraftingUpdateInterval) tradeskill.LastUpdate = currentTime } } // Update statistics tm.stats.LastUpdate = currentTime } // processCraftingUpdate calculates the outcome of a single crafting update. func (tm *TradeskillManager) processCraftingUpdate(ts *Tradeskill) CraftingOutcome { outcome := CraftingOutcome{} // Roll for outcome type based on configured chances roll := rand.Float32() * 100.0 var progressChange, durabilityChange int32 // Determine base outcome if roll <= tm.critFailChance { // Critical failure progressChange = -50 durabilityChange = -100 outcome.CriticalFailure = true log.Printf("Critical failure for crafting session") } else if roll <= tm.critFailChance+tm.critSuccessChance { // Critical success progressChange = 100 durabilityChange = 10 outcome.CriticalSuccess = true outcome.Success = true log.Printf("Critical success for crafting session") } else if roll <= tm.critFailChance+tm.critSuccessChance+tm.failChance { // Regular failure progressChange = 0 durabilityChange = -50 outcome.Success = false } else { // Regular success progressChange = 50 durabilityChange = -10 outcome.Success = true } // Apply event effects if there's an active event if ts.CurrentEvent != nil { if ts.EventCountered { progressChange += int32(ts.CurrentEvent.SuccessProgress) durabilityChange += int32(ts.CurrentEvent.SuccessDurability) } else { progressChange += int32(ts.CurrentEvent.FailProgress) durabilityChange += int32(ts.CurrentEvent.FailDurability) } } // Apply changes ts.CurrentProgress += progressChange ts.CurrentDurability += durabilityChange // Clamp values to valid ranges if ts.CurrentProgress < MinProgress { ts.CurrentProgress = MinProgress } else if ts.CurrentProgress > MaxProgress { ts.CurrentProgress = MaxProgress } if ts.CurrentDurability < MinDurability { ts.CurrentDurability = MinDurability } else if ts.CurrentDurability > MaxDurability { ts.CurrentDurability = MaxDurability } outcome.ProgressChange = progressChange outcome.DurabilityChange = durabilityChange outcome.Completed = ts.IsComplete() outcome.Failed = ts.IsFailed() // Reset event state ts.CurrentEvent = nil ts.EventChecked = false ts.EventCountered = false // Roll for new event eventRoll := rand.Float32() * 100.0 if eventRoll <= tm.eventChance { // TODO: Select random event from master list based on technique // This would need integration with the master events list tm.stats.TotalEventsTriggered++ } return outcome } // BeginCrafting starts a new crafting session for a player. func (tm *TradeskillManager) BeginCrafting(request CraftingRequest) error { tm.mutex.Lock() defer tm.mutex.Unlock() // Check if player is already crafting if _, exists := tm.tradeskillList[request.PlayerID]; exists { return fmt.Errorf("player %d is already crafting", request.PlayerID) } // Validate request if request.RecipeID == 0 { return fmt.Errorf("invalid recipe ID") } if len(request.Components) == 0 { return fmt.Errorf("no components provided") } // TODO: Validate recipe exists and player has it // TODO: Validate components are available in player inventory // TODO: Validate crafting table is correct for recipe // Create new tradeskill session now := time.Now() tradeskill := &Tradeskill{ PlayerID: request.PlayerID, TableSpawnID: request.TableSpawnID, RecipeID: request.RecipeID, CurrentProgress: MinProgress, CurrentDurability: MaxDurability, NextUpdateTime: now.Add(500 * time.Millisecond), // Initial delay before first update UsedComponents: request.Components, StartTime: now, LastUpdate: now, } // Add to active sessions tm.tradeskillList[request.PlayerID] = tradeskill tm.stats.ActiveSessions++ tm.stats.TotalSessionsStarted++ // TODO: Send crafting UI packet to client // TODO: Lock inventory items being used // TODO: Unlock tradeskill spells log.Printf("Started crafting session for player %d with recipe %d", request.PlayerID, request.RecipeID) return nil } // StopCrafting ends a crafting session for a player. func (tm *TradeskillManager) StopCrafting(playerID uint32) error { tm.mutex.Lock() defer tm.mutex.Unlock() tradeskill, exists := tm.tradeskillList[playerID] if !exists { return fmt.Errorf("player %d is not crafting", playerID) } // Determine completion status completed := tradeskill.IsComplete() return tm.completeCrafting(playerID, tradeskill, completed) } // completeCrafting handles the completion of a crafting session. func (tm *TradeskillManager) completeCrafting(playerID uint32, ts *Tradeskill, success bool) error { // TODO: Calculate rewards based on progress/durability // TODO: Give items to player based on completion stage // TODO: Award tradeskill experience // TODO: Unlock inventory items // TODO: Lock tradeskill spells // TODO: Send stop crafting packet log.Printf("Completed crafting session for player %d: success=%v, progress=%d, durability=%d", playerID, success, ts.CurrentProgress, ts.CurrentDurability) // Remove from active sessions delete(tm.tradeskillList, playerID) tm.stats.ActiveSessions-- if success { tm.stats.TotalSessionsCompleted++ } else { tm.stats.TotalSessionsCancelled++ } return nil } // IsClientCrafting checks if a player is currently crafting. func (tm *TradeskillManager) IsClientCrafting(playerID uint32) bool { tm.mutex.RLock() defer tm.mutex.RUnlock() _, exists := tm.tradeskillList[playerID] return exists } // GetTradeskill returns the tradeskill session for a player. func (tm *TradeskillManager) GetTradeskill(playerID uint32) *Tradeskill { tm.mutex.RLock() defer tm.mutex.RUnlock() return tm.tradeskillList[playerID] } // CheckTradeskillEvent processes a player's attempt to counter a tradeskill event. func (tm *TradeskillManager) CheckTradeskillEvent(request EventCounterRequest) error { tm.mutex.Lock() defer tm.mutex.Unlock() tradeskill, exists := tm.tradeskillList[request.PlayerID] if !exists { return fmt.Errorf("player %d is not crafting", request.PlayerID) } // Check if there's an active event that hasn't been checked yet if tradeskill.CurrentEvent == nil || tradeskill.EventChecked { return fmt.Errorf("no active event to counter") } // Mark event as checked tradeskill.EventChecked = true // Check if the counter was successful (icon matches) countered := request.SpellIcon == tradeskill.CurrentEvent.Icon tradeskill.EventCountered = countered if countered { tm.stats.TotalEventsCountered++ } // TODO: Send counter reaction packet to client log.Printf("Player %d %s event %s", request.PlayerID, map[bool]string{true: "countered", false: "failed to counter"}[countered], tradeskill.CurrentEvent.Name) return nil } // GetStats returns current statistics for the tradeskill manager. func (tm *TradeskillManager) GetStats() TradeskillStats { tm.mutex.RLock() defer tm.mutex.RUnlock() return TradeskillStats{ ActiveSessions: tm.stats.ActiveSessions, RecentCompletions: tm.stats.TotalSessionsCompleted, // TODO: Track hourly completions AverageSessionTime: time.Minute * 5, // TODO: Calculate actual average } } // GetTechniqueSuccessAnim returns the success animation for a technique and client version. func (tm *TradeskillManager) GetTechniqueSuccessAnim(clientVersion int16, technique uint32) uint32 { switch technique { case TechniqueSkillTransmuting: // Sculpting if clientVersion <= 561 { return 3007 // leatherworking_success } return 11785 case TechniqueSkillArtistry: if clientVersion <= 561 { return 2319 // cooking_success } return 11245 case TechniqueSkillFletching: if clientVersion <= 561 { return 2356 // woodworking_success } return 13309 case TechniqueSkillMetalworking, TechniqueSkillMetalshaping: if clientVersion <= 561 { return 2442 // metalworking_success } return 11813 case TechniqueSkillTailoring: if clientVersion <= 561 { return 2352 // tailoring_success } return 13040 case TechniqueSkillAlchemy: if clientVersion <= 561 { return 2298 // alchemy_success } return 10749 case TechniqueSkillJewelcrafting: if clientVersion <= 561 { return 2304 // artificing_success } return 10767 case TechniqueSkillScribing: // No known animations for scribing return 0 } return 0 } // GetTechniqueFailureAnim returns the failure animation for a technique and client version. func (tm *TradeskillManager) GetTechniqueFailureAnim(clientVersion int16, technique uint32) uint32 { switch technique { case TechniqueSkillTransmuting: // Sculpting if clientVersion <= 561 { return 3005 // leatherworking_failure } return 11783 case TechniqueSkillArtistry: if clientVersion <= 561 { return 2317 // cooking_failure } return 11243 case TechniqueSkillFletching: if clientVersion <= 561 { return 2354 // woodworking_failure } return 13307 case TechniqueSkillMetalworking, TechniqueSkillMetalshaping: if clientVersion <= 561 { return 2441 // metalworking_failure } return 11811 case TechniqueSkillTailoring: if clientVersion <= 561 { return 2350 // tailoring_failure } return 13038 case TechniqueSkillAlchemy: if clientVersion <= 561 { return 2298 // alchemy_failure (same as success in C++ - typo?) } return 10749 case TechniqueSkillJewelcrafting: if clientVersion <= 561 { return 2302 // artificing_failure } return 10765 case TechniqueSkillScribing: // No known animations for scribing return 0 } return 0 } // GetTechniqueIdleAnim returns the idle animation for a technique and client version. func (tm *TradeskillManager) GetTechniqueIdleAnim(clientVersion int16, technique uint32) uint32 { switch technique { case TechniqueSkillTransmuting: // Sculpting if clientVersion <= 561 { return 3006 // leatherworking_idle } return 11784 case TechniqueSkillArtistry: if clientVersion <= 561 { return 2318 // cooking_idle } return 11244 case TechniqueSkillFletching: if clientVersion <= 561 { return 2355 // woodworking_idle } return 13308 case TechniqueSkillMetalworking, TechniqueSkillMetalshaping: if clientVersion <= 561 { return 1810 // metalworking_idle } return 11812 case TechniqueSkillTailoring: if clientVersion <= 561 { return 2351 // tailoring_idle } return 13039 case TechniqueSkillAlchemy: if clientVersion <= 561 { return 2297 // alchemy_idle } return 10748 case TechniqueSkillJewelcrafting: if clientVersion <= 561 { return 2303 // artificing_idle } return 10766 case TechniqueSkillScribing: if clientVersion <= 561 { return 3131 // scribing_idle } return 12193 } return 0 } // GetMissTargetAnim returns the miss target animation for client version. func (tm *TradeskillManager) GetMissTargetAnim(clientVersion int16) uint32 { if clientVersion <= 561 { return 1144 } return 11814 } // GetKillMissTargetAnim returns the kill miss target animation for client version. func (tm *TradeskillManager) GetKillMissTargetAnim(clientVersion int16) uint32 { if clientVersion <= 561 { return 33912 } return 44582 } // UpdateConfiguration updates the manager's configuration from rules. func (tm *TradeskillManager) UpdateConfiguration(critFail, critSuccess, fail, success, eventChance float32) error { tm.mutex.Lock() defer tm.mutex.Unlock() // Validate that chances add up to 100% (excluding event chance) total := critFail + critSuccess + fail + success if total != 100.0 { log.Printf("Warning: Tradeskill chances don't add up to 100%% (got %.1f%%), using defaults", total) tm.critFailChance = DefaultCritFailChance tm.critSuccessChance = DefaultCritSuccessChance tm.failChance = DefaultFailChance tm.successChance = DefaultSuccessChance } else { tm.critFailChance = critFail / 100.0 // Convert to 0-1 range tm.critSuccessChance = critSuccess / 100.0 tm.failChance = fail / 100.0 tm.successChance = success / 100.0 } tm.eventChance = eventChance return nil } // NewMasterTradeskillEventsList creates a new master events list. func NewMasterTradeskillEventsList() *MasterTradeskillEventsList { return &MasterTradeskillEventsList{ eventList: make(map[uint32][]*TradeskillEvent), totalEvents: 0, } } // AddEvent adds a tradeskill event to the master list. func (mtel *MasterTradeskillEventsList) AddEvent(event *TradeskillEvent) { if event == nil { return } mtel.mutex.Lock() defer mtel.mutex.Unlock() mtel.eventList[event.Technique] = append(mtel.eventList[event.Technique], event) mtel.totalEvents++ } // GetEventByTechnique returns all events for a given technique. func (mtel *MasterTradeskillEventsList) GetEventByTechnique(technique uint32) []*TradeskillEvent { mtel.mutex.RLock() defer mtel.mutex.RUnlock() events, exists := mtel.eventList[technique] if !exists { return nil } // Return a copy to avoid race conditions result := make([]*TradeskillEvent, len(events)) copy(result, events) return result } // Size returns the total number of events in the master list. func (mtel *MasterTradeskillEventsList) Size() int32 { mtel.mutex.RLock() defer mtel.mutex.RUnlock() return mtel.totalEvents } // GetStats returns statistics about the events list. func (mtel *MasterTradeskillEventsList) GetStats() TradeskillStats { mtel.mutex.RLock() defer mtel.mutex.RUnlock() eventsByTechnique := make(map[uint32]int32) for technique, events := range mtel.eventList { eventsByTechnique[technique] = int32(len(events)) } return TradeskillStats{ TotalEvents: mtel.totalEvents, EventsByTechnique: eventsByTechnique, } } // Clear removes all events from the master list. func (mtel *MasterTradeskillEventsList) Clear() { mtel.mutex.Lock() defer mtel.mutex.Unlock() mtel.eventList = make(map[uint32][]*TradeskillEvent) mtel.totalEvents = 0 }