415 lines
12 KiB
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
|
|
} |