package game import ( "fmt" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func (g *Game) startFletchAction(sess *net.Session, p *player.Player, def *item.ItemDef, count int) { p.EnsureFlags() g.setPlayerFlag(p, "last_fletch", def.ID) g.saveCharacter(p) idx := g.usableFletchCraftIndex(p, def) if idx < 0 { sess.WriteLine("You don't have the materials for that.") return } if def.Craft[idx].TicksPerCycle <= 0 { g.doInstantFletch(sess, p, def, idx, count) return } g.startTimedFletch(sess, p, def, idx, count) } // usableFletchCraftIndex selects the first fletching recipe the player both // satisfies and has the tools for. Availability scans every recipe, so // execution must use the same one instead of always Craft[0]. func (g *Game) usableFletchCraftIndex(p *player.Player, def *item.ItemDef) int { for i := range def.Craft { c := &def.Craft[i] if p.Level(player.Fletching) < c.Level { continue } if !craftHasAllItemsQty(c, p.CountItem) { continue } if !g.hasFletchTools(p, def) { continue } return i } return -1 } func (g *Game) doInstantFletch(sess *net.Session, p *player.Player, def *item.ItemDef, craftIdx, count int) { c := &def.Craft[craftIdx] outputQty := c.OutputQty if outputQty <= 0 { outputQty = 1 } batches := 0 totalOutput := 0 failures := 0 skill := player.SkillName(c.EffectiveSkill()) tmpItem := &item.ItemDef{Craft: item.CraftList{*c}} inventoryFull := false for { if !g.canDoFletchItem(p, tmpItem) { break } // A full inventory must never destroy materials: check placement of // BOTH the output and any fail replacement BEFORE consuming. if !craftOutputFits(p, def.ID, def) || !g.craftFailFits(p, c) { inventoryFull = true break } byproducts := g.collectByproducts(p, c) if !consumeIngredients(c, p.CountItem, p.RemoveItem) { break } if craftRollSuccess(c, p.Level(skill)) { placeCraftOutput(p, def.ID, def, outputQty) g.placeCraftByproducts(p, byproducts) if c.XP > 0 { g.awardSkillXP(sess, p, skill, c.XP) } batches++ totalOutput += outputQty } else { g.placeCraftFail(p, c) failures++ } if count > 0 { count-- if count <= 0 { break } } } if batches == 0 { if inventoryFull { sess.WriteLine("Your inventory is too full!") } else if failures > 0 { sess.WriteLine("You fail to fletch anything.") } else { sess.WriteLine("You don't have the materials for that.") } return } g.saveCharacter(p) outDef, _ := g.ItemStore.Load(def.ID) outputName := def.Name if outDef != nil { outputName = g.itemColorize(sess, outDef, outDef.Name) } msg := fmt.Sprintf("You fletch %d %s.", totalOutput, outputName) if p.OptionBool("xp_drops") && c.XP > 0 { msg += g.formatXpDropSingle(sess, p, skill, c.XP*batches) } sess.WriteLine(msg) } func (g *Game) startTimedFletch(sess *net.Session, p *player.Player, def *item.ItemDef, craftIdx, count int) { craft := &def.Craft[craftIdx] startMsg := g.resolveCraftMessage(sess, craft.StartMessage, "You begin fletching %i1.", craft, def.ID, nil, p.HasItem) sess.WriteLine(startMsg) broadcastName := def.Name if dd, _ := g.ItemStore.Load(def.ID); dd != nil { broadcastName = dd.Name } g.broadcastAction(sess, "%s starts fletching.", p.Name) _ = broadcastName p.BackgroundAction = &behavior.Action{ Type: "fletch", TargetID: def.ID, TargetName: def.Name, Data: &behavior.FletchData{ ItemID: def.ID, CraftIndex: craftIdx, Phase: 0, TicksPerCycle: craft.TicksPerCycle, Remaining: count, }, WaitLeft: engine.ToTicks(1), } } func (g *Game) advanceFletch(sess *net.Session, p *player.Player) { data, ok := p.BackgroundAction.Data.(*behavior.FletchData) if !ok { g.cancelBgAction(p) return } itemID := data.ItemID phase := data.Phase wait := data.TicksPerCycle remaining := data.Remaining def := g.loadCraftItem(itemID) if def == nil { g.cancelBgAction(p) return } idx := data.CraftIndex if idx < 0 || idx >= len(def.Craft) { idx = 0 } c := &def.Craft[idx] if phase == 0 { data.Phase = 1 p.BackgroundAction.WaitLeft = engine.ToTicks(wait) return } tmpItem := &item.ItemDef{Craft: item.CraftList{*c}} if !g.canDoFletchItem(p, tmpItem) { sess.WriteLine("You've run out of fletching materials.") g.cancelBgAction(p) return } outputQty := c.OutputQty if outputQty <= 0 { outputQty = 1 } // A full inventory must never destroy materials: verify BOTH the output // and any fail replacement can be placed BEFORE consuming the batch. if !craftOutputFits(p, itemID, def) || !g.craftFailFits(p, c) { sess.WriteLine("Your inventory is too full!") g.cancelBgAction(p) return } skill := player.SkillName(c.EffectiveSkill()) byproducts := g.collectByproducts(p, c) hasItem := craftConsumedHasItem(c, p.CountItem, p.HasItem) if !consumeIngredients(c, p.CountItem, p.RemoveItem) { g.cancelBgAction(p) return } if craftRollSuccess(c, p.Level(skill)) { if !placeCraftOutput(p, itemID, def, outputQty) { // Defensive: the fits check above should have guaranteed a // stack or slot. sess.WriteLine("Your inventory is too full!") g.cancelBgAction(p) return } g.placeCraftByproducts(p, byproducts) if c.XP > 0 { g.awardSkillXP(sess, p, skill, c.XP) } g.saveCharacter(p) msg := g.resolveCraftMessage(sess, c.SuccessMessage, "You fletch a %n.", c, def.ID, byproducts, hasItem) if p.OptionBool("xp_drops") && c.XP > 0 { msg += g.formatXpDropSingle(sess, p, skill, c.XP) } sess.WriteLine(msg) } else { g.placeCraftFail(p, c) g.saveCharacter(p) msg := g.resolveCraftMessage(sess, c.FailMessage, "", c, def.ID, nil, hasItem) if msg != "" { sess.WriteLine(g.colorize(sess, "damage_taken", msg)) } } if remaining > 0 { remaining-- data.Remaining = remaining if remaining <= 0 { sess.WriteLine("You've finished fletching.") g.cancelBgAction(p) return } } if !g.canContinueProduction(p, def, c) { sess.WriteLine("You've run out of fletching materials.") g.cancelBgAction(p) return } p.BackgroundAction.WaitLeft = engine.ToTicks(wait) }