577 lines
15 KiB
Go
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
|
|
}
|