package game import ( "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func (g *Game) startCombine(sess *net.Session, p *player.Player, item *item.ItemDef, craft *item.CraftDef, count int) { g.cancelAction(p) g.cancelBgAction(p) if !craftHasAllItemsQty(craft, p.CountItem) { sess.WriteLine("You don't have the required materials.") g.reprompt(sess) return } outDef, _ := g.ItemStore.Load(item.ID) outputName := item.ID if outDef != nil { outputName = outDef.Name } startMsg := g.resolveCraftMessage(sess, craft.StartMessage, "You start combining %i1.", craft, item.ID, nil, p.HasItem) endMsg := g.resolveCraftMessage(sess, craft.EndMessage, "You've finished combining.", craft, item.ID, nil, p.HasItem) p.Action = &behavior.Action{ Type: behavior.TypeCombine, TargetID: item.ID, TargetName: outputName, Data: &behavior.CombineData{ ItemID: item.ID, Phase: 0, StepIndex: 0, Remaining: count, StartMessage: startMsg, EndMessage: endMsg, TicksPerCycle: craft.TicksPerCycle, }, WaitLeft: engine.ToTicks(1), } g.broadcastAction(sess, "%s starts combining.", p.Name) } func (g *Game) advanceCombine(sess *net.Session, p *player.Player) { d, ok := p.Action.Data.(*behavior.CombineData) if !ok { g.cancelAction(p) return } item := g.loadCraftItem(d.ItemID) if item == nil { g.cancelAction(p) return } craft := item.FirstCraft() switch d.Phase { case 0: if d.StartMessage != "" { sess.WriteLine(d.StartMessage) } d.Phase = 1 if len(craft.Steps) > 0 { p.Action.WaitLeft = engine.ToTicks(craft.Steps[0].Delay) } else { firstWait := craft.TicksPerCycle if firstWait <= 0 { firstWait = 4 } p.Action.WaitLeft = engine.ToTicks(firstWait) } return case 1: if len(craft.Steps) > 0 && d.StepIndex < len(craft.Steps) { step := craft.Steps[d.StepIndex] sess.WriteLine(g.resolveCraftVar(sess, step.Message, craft, item.ID, nil, p.HasItem)) d.StepIndex++ if d.StepIndex < len(craft.Steps) { p.Action.WaitLeft = engine.ToTicks(craft.Steps[d.StepIndex].Delay) return } } d.Phase = 2 p.Action.WaitLeft = engine.ToTicks(1) return case 2: if !craftHasAllItemsQty(craft, p.CountItem) { if d.EndMessage != "" { sess.WriteLine(d.EndMessage) } g.cancelAction(p) return } // A full inventory must never destroy materials: verify the output // can be placed BEFORE collecting byproducts and consuming. outDef, _ := g.ItemStore.Load(item.ID) if !craftOutputFits(p, item.ID, outDef) { sess.WriteLine("Your inventory is too full!") g.cancelAction(p) return } // Byproducts must be collected BEFORE consumption: the resolver maps // each ingredient entry to the alternative actually being consumed // (bucket vs jug of water), which requires the items still present. byproducts := g.collectByproducts(p, item) consumeIngredients(craft, p.CountItem, p.RemoveItem) outputQty := craft.OutputQty if outputQty <= 0 { outputQty = 1 } if !placeCraftOutput(p, item.ID, outDef, outputQty) { // Defensive: the fits check above should have guaranteed a // stack or slot. sess.WriteLine("Your inventory is too full!") g.cancelAction(p) return } // Return the empty containers (and any other byproducts) alongside // the output; without this, combine crafts silently discarded them. for _, bp := range byproducts { if slot := p.FirstFreeSlot(); slot >= 0 { p.SetInvSlot(slot, &player.InventorySlot{ItemID: bp, Quantity: 1}) } else { // Never silently delete a byproduct: drop the remainder on // the ground where the player can pick it up. g.World.AddGroundItem(p.RoomID, bp, 1) } } g.saveCharacter(p) if d.Remaining > 0 { d.Remaining-- if d.Remaining <= 0 { if d.EndMessage != "" { sess.WriteLine(d.EndMessage) } g.cancelAction(p) return } } if !craftHasAllItemsQty(craft, p.CountItem) { if d.EndMessage != "" { sess.WriteLine(d.EndMessage) } g.cancelAction(p) return } d.Phase = 0 d.StepIndex = 0 p.Action.WaitLeft = engine.ToTicks(1) } }