577 lines
15 KiB
Go

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
}