package transmute import ( "fmt" "math" "math/rand" "sync" ) // Transmuter manages the transmutation system type Transmuter struct { tiers []*TransmutingTier activeRequests map[int32]*TransmuteRequest itemMaster ItemMaster spellMaster SpellMaster packetBuilder PacketBuilder mutex sync.RWMutex requestMutex sync.Mutex } // SpellMaster represents the spell system interface type SpellMaster interface { GetSpell(spellID int32, tier int32) Spell } // NewTransmuter creates a new transmuter instance func NewTransmuter(itemMaster ItemMaster, spellMaster SpellMaster, packetBuilder PacketBuilder) *Transmuter { return &Transmuter{ tiers: make([]*TransmutingTier, 0), activeRequests: make(map[int32]*TransmuteRequest), itemMaster: itemMaster, spellMaster: spellMaster, packetBuilder: packetBuilder, } } // LoadTransmutingTiers loads transmuting tiers from database func (t *Transmuter) LoadTransmutingTiers(database Database) error { t.mutex.Lock() defer t.mutex.Unlock() tiers, err := database.LoadTransmutingTiers() if err != nil { return fmt.Errorf("failed to load transmuting tiers: %w", err) } t.tiers = tiers return nil } // GetTransmutingTiers returns a copy of the transmuting tiers func (t *Transmuter) GetTransmutingTiers() []*TransmutingTier { t.mutex.RLock() defer t.mutex.RUnlock() // Return a copy to prevent external modification tiers := make([]*TransmutingTier, len(t.tiers)) for i, tier := range t.tiers { tiers[i] = &TransmutingTier{ MinLevel: tier.MinLevel, MaxLevel: tier.MaxLevel, FragmentID: tier.FragmentID, PowderID: tier.PowderID, InfusionID: tier.InfusionID, ManaID: tier.ManaID, } } return tiers } // IsItemTransmutable checks if an item can be transmuted func (t *Transmuter) IsItemTransmutable(item Item) bool { // Item level > 0 AND Item is not LORE_EQUP, LORE, NO_VALUE etc AND item rarity is >= 5 // (4 is treasured but the rarity used for journeyman spells) // Flag 16384 is NO-TRANSMUTE disqualifyFlags := NoZone | NoValue | Temporary | NoDestroy | NoTransmute disqualifyFlags2 := Ornate if item.GetAdventureDefaultLevel() > 0 && (item.GetItemFlags()&disqualifyFlags) == 0 && (item.GetItemFlags2()&disqualifyFlags2) == 0 && item.GetTier() >= ItemTagLegendary && item.GetStackCount() <= 1 { return true } return false } // CreateItemRequest creates a new transmutation item selection request func (t *Transmuter) CreateItemRequest(client Client, player Player) (int32, error) { // Generate unique request ID var requestID int32 for { // Generate random signed 32-bit integer (excluding 0) requestID = rand.Int31() if requestID != 0 && rand.Intn(2) == 1 { requestID = -requestID // Make it negative sometimes like C++ } if requestID != 0 { break } } // Get player's item list itemList := player.GetItemList() transmutables := make([]int32, 0) // Find all transmutable items for itemID, item := range itemList { if item != nil && t.IsItemTransmutable(item) { transmutables = append(transmutables, itemID) } } // Build and send packet packet, err := t.packetBuilder.BuildItemRequestPacket(requestID, transmutables, client.GetVersion()) if err != nil { return 0, fmt.Errorf("failed to build item request packet: %w", err) } client.QueuePacket(packet) client.SetTransmuteID(requestID) // Store the request t.requestMutex.Lock() t.activeRequests[requestID] = &TransmuteRequest{ RequestID: requestID, ClientID: 0, // TODO: Get client ID when available Phase: PhaseItemSelection, } t.requestMutex.Unlock() return requestID, nil } // HandleItemResponse handles the player's item selection response func (t *Transmuter) HandleItemResponse(client Client, player Player, requestID int32, itemID int32) error { // Find the item item := player.GetItemFromUniqueID(itemID) if item == nil { client.SimpleMessage(ChannelColorRed, "Could not find the item you wish to transmute. Please try again.") return fmt.Errorf("item not found: %d", itemID) } // Verify item is transmutable if !t.IsItemTransmutable(item) { client.Message(ChannelColorRed, "%s is not transmutable.", item.GetName()) return fmt.Errorf("item not transmutable: %s", item.GetName()) } // Check transmuting skill requirement itemLevel := item.GetAdventureDefaultLevel() skill := player.GetSkillByName("Transmuting") requiredSkill := int32(math.Max(float64(itemLevel-5), 0) * 5) itemStatBonus := player.GetStat(ItemStatTransmuting) // TODO: Define this constant currentSkill := int32(0) if skill != nil { currentSkill = skill.GetCurrentValue() + itemStatBonus } if skill == nil || currentSkill < requiredSkill { client.Message(ChannelColorRed, "You need at least %d Transmuting skill to transmute the %s. You have %d Transmuting skill.", requiredSkill, item.GetName(), currentSkill) return fmt.Errorf("insufficient transmuting skill: need %d, have %d", requiredSkill, currentSkill) } // Update request state t.requestMutex.Lock() if request, exists := t.activeRequests[requestID]; exists { request.ItemID = itemID request.Phase = PhaseConfirmation } t.requestMutex.Unlock() client.SetTransmuteID(itemID) // Send confirmation request return t.SendConfirmRequest(client, requestID, item) } // SendConfirmRequest sends a confirmation dialog to the client func (t *Transmuter) SendConfirmRequest(client Client, requestID int32, item Item) error { packet, err := t.packetBuilder.BuildConfirmationPacket(requestID, item, client.GetVersion()) if err != nil { client.SimpleMessage(ChannelColorRed, "Struct error for transmutation. Let a dev know.") return fmt.Errorf("failed to build confirmation packet: %w", err) } client.QueuePacket(packet) return nil } // HandleConfirmResponse handles the player's confirmation response func (t *Transmuter) HandleConfirmResponse(client Client, player Player, itemID int32) error { // Find the item item := player.GetItemFromUniqueID(itemID) if item == nil { client.SimpleMessage(ChannelColorRed, "Item no longer exists!") return fmt.Errorf("item no longer exists: %d", itemID) } client.SetTransmuteID(itemID) // Get the zone zone := player.GetZone() if zone == nil { return fmt.Errorf("player not in zone") } // Get the transmute spell spell := t.spellMaster.GetSpell(TransmuteItemSpellID, 1) if spell == nil { return fmt.Errorf("could not find transmute item spell: %d", TransmuteItemSpellID) } // Process the spell (this will call CompleteTransmutation when finished) return zone.ProcessSpell(spell, player) } // CompleteTransmutation completes the transmutation process func (t *Transmuter) CompleteTransmutation(client Client, player Player) error { itemID := client.GetTransmuteID() item := player.GetItemFromUniqueID(itemID) if item == nil { client.SimpleMessage(ChannelColorRed, "Item no longer exists!") return fmt.Errorf("item no longer exists: %d", itemID) } // Determine materials based on item level and tier result, err := t.calculateTransmuteResult(item) if err != nil { client.SimpleMessage(ChannelColorRed, "Could not complete transmutation! Tell a dev!") return fmt.Errorf("failed to calculate transmute result: %w", err) } if !result.Success { client.SimpleMessage(ChannelColorRed, result.ErrorMessage) return fmt.Errorf("transmutation failed: %s", result.ErrorMessage) } // Remove the original item if !player.RemoveItem(item, true) { return fmt.Errorf("failed to remove transmuted item") } // Send completion message client.Message(ChannelYellow, "You transmute %s and create: ", item.CreateItemLink(client.GetVersion(), false)) // Add the resulting materials rewardItems := make([]Item, 0, 2) if result.CommonMaterial != nil { result.CommonMaterial.SetCount(1) client.Message(ChannelYellow, " %s", result.CommonMaterial.CreateItemLink(client.GetVersion(), false)) var itemDeleted bool if err := client.AddItem(result.CommonMaterial, &itemDeleted); err != nil { return fmt.Errorf("failed to add common material: %w", err) } if !itemDeleted { rewardItems = append(rewardItems, result.CommonMaterial) } } if result.RareMaterial != nil { result.RareMaterial.SetCount(1) client.Message(ChannelYellow, " %s", result.RareMaterial.CreateItemLink(client.GetVersion(), false)) var itemDeleted bool if err := client.AddItem(result.RareMaterial, &itemDeleted); err != nil { return fmt.Errorf("failed to add rare material: %w", err) } if !itemDeleted { rewardItems = append(rewardItems, result.RareMaterial) } } // Send reward packet if there are items if len(rewardItems) > 0 { packet, err := t.packetBuilder.BuildRewardPacket(rewardItems, client.GetVersion()) if err == nil { client.QueuePacket(packet) } } // Handle skill up return t.handleSkillUp(player, item) } // calculateTransmuteResult determines what materials are produced from transmutation func (t *Transmuter) calculateTransmuteResult(item Item) (*TransmuteResult, error) { t.mutex.RLock() defer t.mutex.RUnlock() itemLevel := item.GetAdventureDefaultLevel() var tier *TransmutingTier // Find the correct tier for _, t := range t.tiers { if t.MinLevel <= itemLevel && t.MaxLevel >= itemLevel { tier = t break } } if tier == nil { return &TransmuteResult{ Success: false, ErrorMessage: "No transmuting tier found for item level", }, nil } // Determine material types based on item tier itemTier := item.GetTier() var commonMatID, rareMatID int32 if itemTier >= ItemTagFabled { commonMatID = tier.InfusionID rareMatID = tier.ManaID } else if itemTier >= ItemTagLegendary { commonMatID = tier.PowderID rareMatID = tier.InfusionID } else { commonMatID = tier.FragmentID rareMatID = tier.PowderID } if commonMatID == 0 || rareMatID == 0 { return &TransmuteResult{ Success: false, ErrorMessage: "Invalid material IDs for transmutation", }, nil } // Do the loot roll result := &TransmuteResult{Success: true} roll := rand.Intn(100) + 1 if roll <= BothItemsChancePercent { // Both items result.CommonMaterial = t.itemMaster.CreateItem(commonMatID) result.RareMaterial = t.itemMaster.CreateItem(rareMatID) } else if roll <= CommonMatChancePercent { // Common material only result.CommonMaterial = t.itemMaster.CreateItem(commonMatID) } else { // Rare material only result.RareMaterial = t.itemMaster.CreateItem(rareMatID) } return result, nil } // handleSkillUp processes potential skill increases from transmutation func (t *Transmuter) handleSkillUp(player Player, item Item) error { skill := player.GetSkillByName("Transmuting") if skill == nil { return fmt.Errorf("unable to find transmuting skill for player %s", player.GetName()) } // Calculate skill up chance itemLevel := item.GetAdventureDefaultLevel() maxTransLevel := skill.GetCurrentValue()/5 + 5 levelDif := int32(maxTransLevel) - itemLevel // No skill up if level difference is too high or skill is maxed if levelDif > MaxSkillUpLevelDif || skill.GetCurrentValue() >= skill.GetMaxValue() { return nil } // Calculate skill up probability // 50% base chance at max item level, 20% decrease per level difference baseChance := float64(SkillUpPercentChanceMax) penalty := 0.0 if itemLevel > 5 { penalty = float64(levelDif) * 0.2 } requiredRoll := int32(baseChance * (1.0 - penalty)) roll := rand.Intn(100) + 1 if int32(roll) <= requiredRoll { return player.IncreaseSkill("Transmuting", 1) } return nil } // CleanupRequest removes a completed or expired request func (t *Transmuter) CleanupRequest(requestID int32) { t.requestMutex.Lock() defer t.requestMutex.Unlock() delete(t.activeRequests, requestID) } // GetActiveRequest returns an active request by ID func (t *Transmuter) GetActiveRequest(requestID int32) *TransmuteRequest { t.requestMutex.Lock() defer t.requestMutex.Unlock() if request, exists := t.activeRequests[requestID]; exists { // Return a copy to prevent external modification return &TransmuteRequest{ RequestID: request.RequestID, ClientID: request.ClientID, ItemID: request.ItemID, Phase: request.Phase, } } return nil }