eq2go/internal/transmute/transmute.go

415 lines
12 KiB
Go

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
}