diff options
Diffstat (limited to 'internal/game/production_engine.go')
| -rw-r--r-- | internal/game/production_engine.go | 102 |
1 files changed, 85 insertions, 17 deletions
diff --git a/internal/game/production_engine.go b/internal/game/production_engine.go index 035b7e2..ddada6b 100644 --- a/internal/game/production_engine.go +++ b/internal/game/production_engine.go @@ -172,11 +172,11 @@ func (g *Game) loadCraftItem(itemID string) *item.ItemDef { return item } -func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { +func (g *Game) advanceProduction(sess *net.Session, p *player.Player) { d, ok := p.Action.Data.(*behavior.ProductionData) if !ok { g.cancelAction(p) - return false + return } itemID := d.ItemID phase := d.Phase @@ -188,7 +188,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { item := g.loadCraftItem(itemID) if item == nil { g.cancelAction(p) - return false + return } craft := item.FirstCraft() @@ -198,7 +198,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { } d.Phase = 1 p.Action.WaitLeft = engine.ToTicks(wait) - return true + return } skill := craft.EffectiveSkill() @@ -222,9 +222,14 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { if !craftOutputFits(p, itemID, item) || (failDef != nil && !craftOutputFits(p, craft.Fail, failDef)) { sess.WriteLine("Your inventory is too full!") g.cancelAction(p) - return false + return } + // Resolve %i<n> names from the alternative that will actually be + // consumed: consumption empties the inventory, so p.HasItem can no + // longer identify it when the message is rendered below. + hasItem := craftConsumedHasItem(craft, p.CountItem, p.HasItem) + if rand.Float64() < chance { outputQty := craft.OutputQty if outputQty <= 0 { @@ -240,14 +245,14 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { // Cannot happen after the availability gate above, but never // produce output from a failed consumption. g.cancelAction(p) - return false + return } if !placeCraftOutput(p, itemID, item, outputQty) { // Defensive: the fits check above should have guaranteed a // stack or slot. sess.WriteLine("Your inventory is too full!") g.cancelAction(p) - return false + return } for _, bp := range byproducts { @@ -273,7 +278,7 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { } else { defaultSuccess = "You produce %n." } - msg := g.resolveCraftMessage(sess, craft.SuccessMessage, defaultSuccess, craft, item.ID, byproducts, p.HasItem) + msg := g.resolveCraftMessage(sess, craft.SuccessMessage, defaultSuccess, craft, item.ID, byproducts, hasItem) if p.OptionBool("xp_drops") && craft.XP > 0 { msg += g.formatXpDropSingle(sess, p, player.SkillName(skill), craft.XP) } @@ -281,18 +286,15 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { } else { if !consumeIngredients(craft, p.CountItem, p.RemoveItem) { g.cancelAction(p) - return false + return } if craft.Fail != "" { - freeSlot := p.FirstFreeSlot() - if freeSlot >= 0 { - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: craft.Fail, Quantity: 1}) - } + g.placeCraftFail(p, craft) } g.saveCharacter(p) - msg := g.resolveCraftMessage(sess, craft.FailMessage, info.FailMessage, craft, item.ID, nil, p.HasItem) + msg := g.resolveCraftMessage(sess, craft.FailMessage, info.FailMessage, craft, item.ID, nil, hasItem) if msg != "" { sess.WriteLine(g.colorize(sess, "damage_taken", msg)) } @@ -306,20 +308,19 @@ func (g *Game) advanceProduction(sess *net.Session, p *player.Player) bool { sess.WriteLine(endMsg) } g.cancelAction(p) - return false + return } } if g.canContinueProduction(p, item) { p.Action.WaitLeft = engine.ToTicks(wait) - return true + return } if endMsg != "" { sess.WriteLine(endMsg) } g.cancelAction(p) - return false } func (g *Game) collectByproducts(p *player.Player, item *item.ItemDef) []string { @@ -354,6 +355,73 @@ func (g *Game) collectByproducts(p *player.Player, item *item.ItemDef) []string return byproducts } +// craftConsumedHasItem returns a hasItem predicate that reports the +// ingredient alternatives consumeIngredients will remove right now — the +// same aggregate plan availability and consumption use. A %i<n> message +// resolved AFTER consumption therefore still names the item the player +// actually used (p.HasItem is false for everything consumed). Falls back to +// the supplied predicate when no plan can be built. +func craftConsumedHasItem(craft *item.CraftDef, countItem func(string) int, fallback func(string) bool) func(string) bool { + plan := planIngredients(craft, countItem) + if plan == nil { + return fallback + } + consumed := make(map[string]bool, len(plan)) + for _, c := range plan { + consumed[c.id] = true + } + return func(id string) bool { return consumed[id] } +} + +// craftRollSuccess rolls one craft cycle's success: true when the recipe +// defines no success formula or the roll succeeds. +func craftRollSuccess(craft *item.CraftDef, level int) bool { + if craft == nil || craft.Success == nil { + return true + } + chance := behavior.SuccessChance(behavior.SuccessFormula(*craft.Success), level, craft.Level) + return rand.Float64() < chance +} + +// craftFailFits reports whether the recipe's fail replacement (if any) can +// be placed right now. Like the output fit check, it must run BEFORE +// consumption so a full inventory never destroys materials. +func (g *Game) craftFailFits(p *player.Player, craft *item.CraftDef) bool { + if craft == nil || craft.Fail == "" { + return true + } + def, _ := g.ItemStore.Load(craft.Fail) + if def == nil { + return true + } + return craftOutputFits(p, craft.Fail, def) +} + +// placeCraftFail delivers a failed craft's fail replacement into the +// inventory, merging into a matching stack when the fail item is stackable +// just like the success output does. FirstFreeSlot alone is not enough: the +// pre-roll gate counts a stack merge (craftFailFits), so a full inventory +// with a matching stack must not silently destroy the fail replacement. +func (g *Game) placeCraftFail(p *player.Player, craft *item.CraftDef) { + if craft == nil || craft.Fail == "" { + return + } + def, _ := g.ItemStore.Load(craft.Fail) + placeCraftOutput(p, craft.Fail, def, 1) +} + +// placeCraftByproducts delivers byproducts into free slots, dropping any +// remainder on the ground rather than silently deleting it. +func (g *Game) placeCraftByproducts(p *player.Player, byproducts []string) { + for _, bp := range byproducts { + if slot := p.FirstFreeSlot(); slot >= 0 { + p.SetInvSlot(slot, &player.InventorySlot{ItemID: bp, Quantity: 1}) + } else { + g.World.AddGroundItem(p.RoomID, bp, 1) + } + } +} + func (g *Game) canContinueProduction(p *player.Player, item *item.ItemDef) bool { craft := item.FirstCraft() if craft == nil { |
