612 lines
19 KiB
Go
612 lines
19 KiB
Go
package tradeskills
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// PlayerManager defines the interface for player-related operations needed by tradeskills.
|
|
type PlayerManager interface {
|
|
// GetPlayer retrieves player information by ID
|
|
GetPlayer(playerID uint32) (Player, error)
|
|
|
|
// GetPlayerTarget gets the current target of a player
|
|
GetPlayerTarget(playerID uint32) (Spawn, error)
|
|
|
|
// SetPlayerVisualState sets the visual animation state for a player
|
|
SetPlayerVisualState(playerID uint32, animationID uint32) error
|
|
|
|
// SendMessageToPlayer sends a message to a player
|
|
SendMessageToPlayer(playerID uint32, channel int8, message string) error
|
|
}
|
|
|
|
// ItemManager defines the interface for item-related operations needed by tradeskills.
|
|
type ItemManager interface {
|
|
// GetItem retrieves item information by ID
|
|
GetItem(itemID uint32) (Item, error)
|
|
|
|
// GetPlayerItem gets a specific item from a player's inventory
|
|
GetPlayerItem(playerID uint32, uniqueID uint32) (Item, error)
|
|
|
|
// GetPlayerItemsByID gets all items of a specific type from player inventory
|
|
GetPlayerItemsByID(playerID uint32, itemID uint32) ([]Item, error)
|
|
|
|
// ConsumePlayerItems removes items from player inventory
|
|
ConsumePlayerItems(playerID uint32, components []ComponentUsage) error
|
|
|
|
// GiveItemToPlayer adds an item to player inventory
|
|
GiveItemToPlayer(playerID uint32, itemID uint32, quantity int16, creator string) error
|
|
|
|
// LockPlayerItems locks items in player inventory for crafting
|
|
LockPlayerItems(playerID uint32, components []ComponentUsage) error
|
|
|
|
// UnlockPlayerItems unlocks previously locked items
|
|
UnlockPlayerItems(playerID uint32, components []ComponentUsage) error
|
|
}
|
|
|
|
// RecipeManager defines the interface for recipe-related operations.
|
|
type RecipeManager interface {
|
|
// GetRecipe retrieves recipe information by ID
|
|
GetRecipe(recipeID uint32) (Recipe, error)
|
|
|
|
// GetPlayerRecipe gets a player's copy of a recipe (with progress tracking)
|
|
GetPlayerRecipe(playerID uint32, recipeID uint32) (PlayerRecipe, error)
|
|
|
|
// UpdatePlayerRecipe updates a player's recipe progress
|
|
UpdatePlayerRecipe(playerID uint32, recipeID uint32, highestStage int8) error
|
|
|
|
// ValidateRecipeComponents checks if player has required components
|
|
ValidateRecipeComponents(playerID uint32, recipeID uint32, components []ComponentUsage) error
|
|
}
|
|
|
|
// SpellManager defines the interface for spell-related operations needed by tradeskills.
|
|
type SpellManager interface {
|
|
// GetPlayerTradeskillSpells gets tradeskill spells for a player
|
|
GetPlayerTradeskillSpells(playerID uint32, technique uint32) ([]Spell, error)
|
|
|
|
// LockTradeskillSpells locks tradeskill spells for a player
|
|
LockTradeskillSpells(playerID uint32) error
|
|
|
|
// UnlockTradeskillSpells unlocks tradeskill spells for a player
|
|
UnlockTradeskillSpells(playerID uint32) error
|
|
}
|
|
|
|
// ZoneManager defines the interface for zone-related operations.
|
|
type ZoneManager interface {
|
|
// GetSpawn retrieves spawn information by ID
|
|
GetSpawn(spawnID uint32) (Spawn, error)
|
|
|
|
// PlayAnimation plays an animation for a spawn
|
|
PlayAnimation(spawnID uint32, animationID uint32) error
|
|
|
|
// ValidateCraftingTable checks if a spawn is a valid crafting table for a recipe
|
|
ValidateCraftingTable(spawnID uint32, requiredDevice string) error
|
|
}
|
|
|
|
// ExperienceManager defines the interface for experience-related operations.
|
|
type ExperienceManager interface {
|
|
// CalculateTradeskillXP calculates XP for a recipe level
|
|
CalculateTradeskillXP(playerID uint32, recipeLevel int16) (float32, error)
|
|
|
|
// AwardTradeskillXP gives tradeskill XP to a player
|
|
AwardTradeskillXP(playerID uint32, xp int32) (bool, error) // Returns true if level changed
|
|
|
|
// GetPlayerTradeskillLevel gets a player's current tradeskill level
|
|
GetPlayerTradeskillLevel(playerID uint32) (int16, error)
|
|
}
|
|
|
|
// QuestManager defines the interface for quest-related operations.
|
|
type QuestManager interface {
|
|
// CheckCraftingQuests checks for quest updates related to crafting
|
|
CheckCraftingQuests(playerID uint32, itemID uint32, quantity int8) error
|
|
}
|
|
|
|
// RuleManager defines the interface for rules/configuration access.
|
|
type RuleManager interface {
|
|
// GetTradeskillSuccessChance gets the base success chance percentage
|
|
GetTradeskillSuccessChance() float32
|
|
|
|
// GetTradeskillCritSuccessChance gets the critical success chance percentage
|
|
GetTradeskillCritSuccessChance() float32
|
|
|
|
// GetTradeskillFailChance gets the base failure chance percentage
|
|
GetTradeskillFailChance() float32
|
|
|
|
// GetTradeskillCritFailChance gets the critical failure chance percentage
|
|
GetTradeskillCritFailChance() float32
|
|
|
|
// GetTradeskillEventChance gets the event trigger chance percentage
|
|
GetTradeskillEventChance() float32
|
|
}
|
|
|
|
// Data structures used by the interfaces
|
|
|
|
// Player represents a player in the game.
|
|
type Player struct {
|
|
ID uint32
|
|
Name string
|
|
CurrentRecipe uint32
|
|
TradeskillLevel int16
|
|
SuccessModifier int16 // Stat bonus to success chance
|
|
ProgressModifier int16 // Stat bonus to progress
|
|
DurabilityModifier int16 // Stat bonus to durability
|
|
}
|
|
|
|
// Item represents an item in the game.
|
|
type Item struct {
|
|
ID uint32
|
|
UniqueID uint32
|
|
Name string
|
|
Icon uint32
|
|
Count int16
|
|
Creator string
|
|
StackCount int16
|
|
}
|
|
|
|
// Recipe represents a crafting recipe.
|
|
type Recipe struct {
|
|
ID uint32
|
|
Name string
|
|
Level int16
|
|
Tier int8
|
|
Technique uint32
|
|
Device string
|
|
ProductID uint32
|
|
ProductQuantity int16
|
|
PrimaryComponentTitle string
|
|
PrimaryComponentQuantity int16
|
|
Build1ComponentTitle string
|
|
Build1ComponentQuantity int16
|
|
Build2ComponentTitle string
|
|
Build2ComponentQuantity int16
|
|
Build3ComponentTitle string
|
|
Build3ComponentQuantity int16
|
|
Build4ComponentTitle string
|
|
Build4ComponentQuantity int16
|
|
FuelComponentTitle string
|
|
FuelComponentQuantity int16
|
|
Components map[int8][]uint32 // Component slot -> item IDs
|
|
Products map[int8]*RecipeProduct // Stage -> product
|
|
}
|
|
|
|
// PlayerRecipe represents a player's version of a recipe with progress tracking.
|
|
type PlayerRecipe struct {
|
|
RecipeID uint32
|
|
PlayerID uint32
|
|
HighestStage int8 // Bitmask of completed stages
|
|
}
|
|
|
|
// RecipeProduct represents a product from a recipe stage.
|
|
type RecipeProduct struct {
|
|
ProductID uint32
|
|
ProductQty int16
|
|
ByproductID uint32
|
|
ByproductQty int16
|
|
}
|
|
|
|
// Spell represents a spell/ability.
|
|
type Spell struct {
|
|
ID uint32
|
|
Name string
|
|
Icon int16
|
|
TechniqueSlot int8 // Location index for tradeskill UI
|
|
}
|
|
|
|
// Spawn represents a spawn (NPC, object, etc.) in the game world.
|
|
type Spawn struct {
|
|
ID uint32
|
|
Name string
|
|
IsObject bool
|
|
DeviceID uint32 // For crafting tables
|
|
}
|
|
|
|
// TradeskillSystemAdapter provides a high-level interface to the complete tradeskill system.
|
|
type TradeskillSystemAdapter struct {
|
|
manager *TradeskillManager
|
|
eventsList *MasterTradeskillEventsList
|
|
database DatabaseService
|
|
packetBuilder PacketBuilder
|
|
playerManager PlayerManager
|
|
itemManager ItemManager
|
|
recipeManager RecipeManager
|
|
spellManager SpellManager
|
|
zoneManager ZoneManager
|
|
experienceManager ExperienceManager
|
|
questManager QuestManager
|
|
ruleManager RuleManager
|
|
}
|
|
|
|
// NewTradeskillSystemAdapter creates a new system adapter with all dependencies.
|
|
func NewTradeskillSystemAdapter(
|
|
manager *TradeskillManager,
|
|
eventsList *MasterTradeskillEventsList,
|
|
database DatabaseService,
|
|
packetBuilder PacketBuilder,
|
|
playerManager PlayerManager,
|
|
itemManager ItemManager,
|
|
recipeManager RecipeManager,
|
|
spellManager SpellManager,
|
|
zoneManager ZoneManager,
|
|
experienceManager ExperienceManager,
|
|
questManager QuestManager,
|
|
ruleManager RuleManager,
|
|
) *TradeskillSystemAdapter {
|
|
return &TradeskillSystemAdapter{
|
|
manager: manager,
|
|
eventsList: eventsList,
|
|
database: database,
|
|
packetBuilder: packetBuilder,
|
|
playerManager: playerManager,
|
|
itemManager: itemManager,
|
|
recipeManager: recipeManager,
|
|
spellManager: spellManager,
|
|
zoneManager: zoneManager,
|
|
experienceManager: experienceManager,
|
|
questManager: questManager,
|
|
ruleManager: ruleManager,
|
|
}
|
|
}
|
|
|
|
// Initialize sets up the tradeskill system (loads events, updates config, etc.).
|
|
func (tsa *TradeskillSystemAdapter) Initialize() error {
|
|
// Load tradeskill events from database
|
|
err := tsa.database.LoadTradeskillEvents(tsa.eventsList)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update manager configuration from rules
|
|
err = tsa.updateManagerConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// StartCrafting begins a crafting session with full validation and setup.
|
|
func (tsa *TradeskillSystemAdapter) StartCrafting(playerID uint32, recipeID uint32, components []ComponentUsage) error {
|
|
// Get player info
|
|
_, err := tsa.playerManager.GetPlayer(playerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get target (crafting table)
|
|
target, err := tsa.playerManager.GetPlayerTarget(playerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get recipe
|
|
recipe, err := tsa.recipeManager.GetRecipe(recipeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate crafting table
|
|
err = tsa.zoneManager.ValidateCraftingTable(target.ID, recipe.Device)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate components
|
|
err = tsa.recipeManager.ValidateRecipeComponents(playerID, recipeID, components)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Lock inventory items
|
|
err = tsa.itemManager.LockPlayerItems(playerID, components)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Send recipe UI packet
|
|
err = tsa.packetBuilder.SendCreateFromRecipe(playerID, recipeID)
|
|
if err != nil {
|
|
tsa.itemManager.UnlockPlayerItems(playerID, components) // Cleanup on error
|
|
return err
|
|
}
|
|
|
|
// Send item creation UI packet
|
|
err = tsa.packetBuilder.SendItemCreationUI(playerID, recipeID)
|
|
if err != nil {
|
|
tsa.itemManager.UnlockPlayerItems(playerID, components) // Cleanup on error
|
|
return err
|
|
}
|
|
|
|
// Start crafting session
|
|
request := CraftingRequest{
|
|
PlayerID: playerID,
|
|
RecipeID: recipeID,
|
|
TableSpawnID: target.ID,
|
|
Components: components,
|
|
Quantity: 1, // TODO: Support mass production
|
|
}
|
|
|
|
err = tsa.manager.BeginCrafting(request)
|
|
if err != nil {
|
|
tsa.itemManager.UnlockPlayerItems(playerID, components) // Cleanup on error
|
|
return err
|
|
}
|
|
|
|
// Unlock tradeskill spells
|
|
err = tsa.spellManager.UnlockTradeskillSpells(playerID)
|
|
if err != nil {
|
|
// Not critical, just log warning
|
|
tsa.playerManager.SendMessageToPlayer(playerID, 1, "Warning: Failed to unlock tradeskill spells")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// StopCrafting ends a crafting session with full cleanup and rewards.
|
|
func (tsa *TradeskillSystemAdapter) StopCrafting(playerID uint32) error {
|
|
// Get tradeskill session
|
|
tradeskill := tsa.manager.GetTradeskill(playerID)
|
|
if tradeskill == nil {
|
|
return nil // Not crafting
|
|
}
|
|
|
|
// Calculate completion stage and rewards
|
|
err := tsa.processCompletionRewards(playerID, tradeskill)
|
|
if err != nil {
|
|
// Log error but continue with cleanup
|
|
tsa.playerManager.SendMessageToPlayer(playerID, 1, "Warning: Failed to process completion rewards")
|
|
}
|
|
|
|
// Stop the crafting session
|
|
err = tsa.manager.StopCrafting(playerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Send stop crafting packet
|
|
err = tsa.packetBuilder.StopCrafting(playerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Unlock inventory items
|
|
err = tsa.itemManager.UnlockPlayerItems(playerID, tradeskill.UsedComponents)
|
|
if err != nil {
|
|
// Log warning but continue
|
|
tsa.playerManager.SendMessageToPlayer(playerID, 1, "Warning: Failed to unlock inventory items")
|
|
}
|
|
|
|
// Lock tradeskill spells
|
|
err = tsa.spellManager.LockTradeskillSpells(playerID)
|
|
if err != nil {
|
|
// Not critical, just log warning
|
|
tsa.playerManager.SendMessageToPlayer(playerID, 1, "Warning: Failed to lock tradeskill spells")
|
|
}
|
|
|
|
// Reset player visual state
|
|
err = tsa.playerManager.SetPlayerVisualState(playerID, 0)
|
|
if err != nil {
|
|
// Not critical, just log warning
|
|
tsa.playerManager.SendMessageToPlayer(playerID, 1, "Warning: Failed to reset visual state")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ProcessCraftingUpdates handles periodic processing with full integration.
|
|
func (tsa *TradeskillSystemAdapter) ProcessCraftingUpdates() {
|
|
// Run the core manager processing
|
|
tsa.manager.Process()
|
|
|
|
// TODO: Handle any additional processing needed
|
|
// This could include sending update packets, triggering events, etc.
|
|
}
|
|
|
|
// HandleEventCounter processes a player's attempt to counter a tradeskill event.
|
|
func (tsa *TradeskillSystemAdapter) HandleEventCounter(playerID uint32, spellIcon int16) error {
|
|
request := EventCounterRequest{
|
|
PlayerID: playerID,
|
|
SpellIcon: spellIcon,
|
|
}
|
|
|
|
// Process the counter attempt
|
|
err := tsa.manager.CheckTradeskillEvent(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the result and send reaction packet
|
|
tradeskill := tsa.manager.GetTradeskill(playerID)
|
|
if tradeskill != nil && tradeskill.EventChecked {
|
|
err = tsa.packetBuilder.CounterReaction(playerID, tradeskill.EventCountered)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Send message to player
|
|
action := "failed to counter"
|
|
if tradeskill.EventCountered {
|
|
action = "successfully countered"
|
|
}
|
|
|
|
if tradeskill.CurrentEvent != nil {
|
|
message := fmt.Sprintf("You %s %s.", action, tradeskill.CurrentEvent.Name)
|
|
tsa.playerManager.SendMessageToPlayer(playerID, 2, message) // CHANNEL_NARRATIVE
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// updateManagerConfig updates the manager with current rule values.
|
|
func (tsa *TradeskillSystemAdapter) updateManagerConfig() error {
|
|
critFail := tsa.ruleManager.GetTradeskillCritFailChance()
|
|
critSuccess := tsa.ruleManager.GetTradeskillCritSuccessChance()
|
|
fail := tsa.ruleManager.GetTradeskillFailChance()
|
|
success := tsa.ruleManager.GetTradeskillSuccessChance()
|
|
eventChance := tsa.ruleManager.GetTradeskillEventChance()
|
|
|
|
return tsa.manager.UpdateConfiguration(critFail, critSuccess, fail, success, eventChance)
|
|
}
|
|
|
|
// processCompletionRewards handles giving rewards when crafting completes.
|
|
func (tsa *TradeskillSystemAdapter) processCompletionRewards(playerID uint32, ts *Tradeskill) error {
|
|
// Get recipe
|
|
recipe, err := tsa.recipeManager.GetRecipe(ts.RecipeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Determine completion stage based on progress and durability
|
|
stage := tsa.calculateCompletionStage(ts.CurrentProgress, ts.CurrentDurability)
|
|
|
|
// Give appropriate rewards for the stage
|
|
if stage >= 0 && recipe.Products != nil {
|
|
if product, exists := recipe.Products[stage]; exists {
|
|
// Give main product
|
|
if product.ProductID > 0 {
|
|
err = tsa.itemManager.GiveItemToPlayer(playerID, product.ProductID, product.ProductQty, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update quests
|
|
tsa.questManager.CheckCraftingQuests(playerID, product.ProductID, int8(product.ProductQty))
|
|
}
|
|
|
|
// Give byproduct if any
|
|
if product.ByproductID > 0 {
|
|
err = tsa.itemManager.GiveItemToPlayer(playerID, product.ByproductID, product.ByproductQty, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update quests
|
|
tsa.questManager.CheckCraftingQuests(playerID, product.ByproductID, int8(product.ByproductQty))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Award tradeskill experience
|
|
baseXP, err := tsa.experienceManager.CalculateTradeskillXP(playerID, recipe.Level)
|
|
if err == nil && baseXP > 0 {
|
|
// Apply stage-based XP reduction
|
|
xpMultiplier := tsa.getXPMultiplierForStage(stage)
|
|
finalXP := int32(baseXP * xpMultiplier)
|
|
|
|
if finalXP > 0 {
|
|
levelChanged, err := tsa.experienceManager.AwardTradeskillXP(playerID, finalXP)
|
|
if err == nil {
|
|
// Notify player of XP gain
|
|
message := fmt.Sprintf("You gain %d Tradeskill XP!", finalXP)
|
|
tsa.playerManager.SendMessageToPlayer(playerID, 3, message) // CHANNEL_REWARD
|
|
|
|
// Handle level change if needed
|
|
if levelChanged {
|
|
// TODO: Handle tradeskill level up
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update player recipe progress
|
|
playerRecipe, err := tsa.recipeManager.GetPlayerRecipe(playerID, ts.RecipeID)
|
|
if err == nil {
|
|
newStage := tsa.updatePlayerRecipeStage(playerRecipe.HighestStage, stage)
|
|
if newStage != playerRecipe.HighestStage {
|
|
tsa.recipeManager.UpdatePlayerRecipe(playerID, ts.RecipeID, newStage)
|
|
}
|
|
}
|
|
|
|
// Play success/failure animation
|
|
technique := recipe.Technique
|
|
clientVersion := int16(1200) // TODO: Get actual client version
|
|
|
|
var animationID uint32
|
|
if stage == 4 { // Full completion
|
|
animationID = tsa.manager.GetTechniqueSuccessAnim(clientVersion, technique)
|
|
} else {
|
|
animationID = tsa.manager.GetTechniqueFailureAnim(clientVersion, technique)
|
|
}
|
|
|
|
if animationID > 0 {
|
|
tsa.zoneManager.PlayAnimation(playerID, animationID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// calculateCompletionStage determines the completion stage based on progress/durability.
|
|
func (tsa *TradeskillSystemAdapter) calculateCompletionStage(progress, durability int32) int8 {
|
|
if durability >= 800 && progress >= 1000 {
|
|
return 4 // Perfect completion
|
|
} else if (durability >= 200 && durability < 800 && progress >= 800) || (durability >= 800 && progress >= 800 && progress < 1000) {
|
|
return 3 // Stage 3
|
|
} else if (durability < 200 && progress >= 600) || (durability >= 200 && progress >= 600 && progress < 800) {
|
|
return 2 // Stage 2
|
|
} else if progress >= 400 && progress < 600 {
|
|
return 1 // Stage 1
|
|
} else if progress < 400 {
|
|
return 0 // Stage 0 (fuel/byproduct)
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// getXPMultiplierForStage returns the XP multiplier for a completion stage.
|
|
func (tsa *TradeskillSystemAdapter) getXPMultiplierForStage(stage int8) float32 {
|
|
switch stage {
|
|
case 4:
|
|
return 1.0 // Full XP for perfect completion
|
|
case 3:
|
|
return 0.85 // 85% XP for stage 3
|
|
case 2:
|
|
return 0.70 // 70% XP for stage 2
|
|
case 1:
|
|
return 0.55 // 55% XP for stage 1
|
|
case 0:
|
|
return 0.0 // No XP for stage 0
|
|
default:
|
|
return 0.0
|
|
}
|
|
}
|
|
|
|
// updatePlayerRecipeStage updates the player's recipe stage progress bitmask.
|
|
func (tsa *TradeskillSystemAdapter) updatePlayerRecipeStage(currentStage int8, completedStage int8) int8 {
|
|
// Set the bit for the completed stage
|
|
switch completedStage {
|
|
case 1:
|
|
if (currentStage & 1) == 0 {
|
|
return currentStage + 1
|
|
}
|
|
case 2:
|
|
if (currentStage & 2) == 0 {
|
|
return currentStage + 2
|
|
}
|
|
case 3:
|
|
if (currentStage & 4) == 0 {
|
|
return currentStage + 4
|
|
}
|
|
case 4:
|
|
if (currentStage & 8) == 0 {
|
|
return currentStage + 8
|
|
}
|
|
}
|
|
|
|
return currentStage
|
|
}
|
|
|
|
// GetSystemStats returns comprehensive statistics about the tradeskill system.
|
|
func (tsa *TradeskillSystemAdapter) GetSystemStats() map[string]interface{} {
|
|
managerStats := tsa.manager.GetStats()
|
|
eventsStats := tsa.eventsList.GetStats()
|
|
|
|
return map[string]interface{}{
|
|
"active_sessions": managerStats.ActiveSessions,
|
|
"recent_completions": managerStats.RecentCompletions,
|
|
"average_session_time": managerStats.AverageSessionTime,
|
|
"total_events": eventsStats.TotalEvents,
|
|
"events_by_technique": eventsStats.EventsByTechnique,
|
|
"last_update": time.Now(),
|
|
}
|
|
}
|