package game import ( "fmt" "strings" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func (g *Game) triggerUtility(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) { switch mod.ID { case "low_process", "high_process": g.triggerProcessing(sess, p, mod, targetArg) case "bones_to_nutrients": g.triggerBonesToNuts(sess, p, mod) case "em_grab": g.triggerEmGrab(sess, p, mod, targetArg) case "superheat": g.triggerSuperheat(sess, p, mod, targetArg) case "plank_make": g.triggerPlankMake(sess, p, mod, targetArg) } } func (g *Game) triggerProcessing(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) { if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } if targetArg == "" { sess.WriteLine(fmt.Sprintf("Usage: trigger %s ", strings.ReplaceAll(mod.ID, "_", " "))) return } slot, inv := g.findInventoryItem(p, targetArg) if slot < 0 { sess.WriteLine("You don't have that item.") return } itemDef, err := g.ItemStore.Load(inv.ItemID) if err != nil || itemDef.Value <= 0 { sess.WriteLine("That item has no value.") return } if p.Action != nil { g.cancelAction(p) } creditValue := itemDef.Value if mod.ID == "low_process" { creditValue = itemDef.Value / 2 if creditValue < 1 { creditValue = 1 } } if inv.Quantity > 1 { inv.Quantity-- } else { p.SetInvSlot(slot, nil) } g.grantCredits(p, creditValue) g.triggerModReward(sess, p, mod, 1.0) sess.WriteLine(fmt.Sprintf("You process the %s. You receive %s credits.", g.itemColorize(sess, itemDef, itemDef.Name), g.colorize(sess, "currency_pickup", fmt.Sprint(creditValue)))) } func (g *Game) triggerBonesToNuts(sess *net.Session, p *player.Player, mod *ModDef) { if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } boneCount := p.CountItem("bones") if boneCount == 0 { sess.WriteLine("You don't have any bones.") return } if p.Action != nil { g.cancelAction(p) } for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot != nil && slot.ItemID == "bones" { slot.ItemID = "nutrient_bar" } } g.triggerModReward(sess, p, mod, float64(boneCount)) sess.WriteLine(fmt.Sprintf("You convert %d bones into nutrient bars.", boneCount)) } func (g *Game) triggerEmGrab(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) { if targetArg == "" { sess.WriteLine("Grab what? Usage: trigger em grab ") return } if p.FirstFreeSlot() < 0 { sess.WriteLine("Your inventory is full.") return } groundItems := g.World.GroundItems(p.RoomID) var matchedID string for itemID := range groundItems { def, _ := g.ItemStore.Load(itemID) if def != nil && def.MatchesName(targetArg) { matchedID = itemID break } if strings.HasPrefix(itemID, strings.ToLower(targetArg)) { matchedID = itemID break } } if matchedID == "" { sess.WriteLine("You don't see that here.") return } if p.Action != nil { g.cancelAction(p) } qty := groundItems[matchedID] def, _ := g.ItemStore.Load(matchedID) name := matchedID if def != nil { name = def.Name } // Credits are currency, not an inventory item: collect them the same way // the get command does. if matchedID == "credits" { g.World.RemoveGroundItem(p.RoomID, matchedID, qty) g.grantCredits(p, qty) g.triggerModReward(sess, p, mod, 1.0) sess.WriteLine(fmt.Sprintf("You magnetically pull %d credits toward you.", qty)) return } if def != nil && def.Stackable { g.World.RemoveGroundItem(p.RoomID, matchedID, qty) if !placeCraftOutput(p, matchedID, def, qty) { g.World.AddGroundItem(p.RoomID, matchedID, qty) sess.WriteLine("Your inventory is full.") return } } else { // Non-stackable items take one slot each; leave any remainder on the // ground instead of cramming N items into a single slot. taken := 0 for taken < qty { freeSlot := p.FirstFreeSlot() if freeSlot < 0 { break } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: matchedID, Quantity: 1}) taken++ } if taken == 0 { sess.WriteLine("Your inventory is full.") return } g.World.RemoveGroundItem(p.RoomID, matchedID, taken) if taken < qty { sess.WriteLine(fmt.Sprintf("Your inventory fills up; %d %s remain on the ground.", qty-taken, name)) } } g.triggerModReward(sess, p, mod, 1.0) sess.WriteLine(fmt.Sprintf("You magnetically pull the %s toward you.", g.itemColorize(sess, def, name))) } func (g *Game) triggerSuperheat(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) { if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } if targetArg == "" { sess.WriteLine("Superheat what? Usage: trigger superheat ") return } slot, inv := g.findInventoryItem(p, targetArg) if slot < 0 { sess.WriteLine("You don't have that item.") return } var match *item.ItemDef for _, item := range g.CraftIndex.BySubtype("smelting") { if craftMatchesEntry(item.FirstCraft(), inv.ItemID) { match = item break } } if match == nil { sess.WriteLine("You can't superheat that.") return } c := match.FirstCraft() if c.Level > 0 && p.Level(player.SkillName(c.Type)) < c.Level { sess.WriteLine(fmt.Sprintf("You need level %d %s to smelt that.", c.Level, c.Type)) return } // Availability resolves each entry the way consumeIngredients will: ONE // listed alternative must cover the entry's full quantity. The previous // hand-rolled loop required EVERY listed alternative, e.g. both a // bucket AND a jug of water to superheat clay, and could not name the // missing choice. for i := range c.Ingredients { id, qty := craftIngredientSource(&c.Ingredients[i], p.CountItem) if id != "" { continue } names := make([]string, 0, len(c.Ingredients[i].Items)) for _, itemID := range c.Ingredients[i].Items { def, _ := g.ItemStore.Load(itemID) name := itemID if def != nil { name = def.Name } names = append(names, fmt.Sprintf("%d %s", qty, name)) } sess.WriteLine(fmt.Sprintf("You need one of: %s.", strings.Join(names, " or "))) return } if p.Action != nil { g.cancelAction(p) } // Verify the output can be placed BEFORE consuming: a full inventory // must never destroy ingredients without producing the bar. if !craftOutputFits(p, match.ID, match) { sess.WriteLine("Your inventory is too full!") return } consumeIngredients(c, p.CountItem, p.RemoveItem) outputQty := c.OutputQty if outputQty <= 0 { outputQty = 1 } placed := false for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot != nil && slot.ItemID == match.ID { slot.Quantity += outputQty placed = true break } } if !placed { freeSlot := p.FirstFreeSlot() if freeSlot < 0 { sess.WriteLine("Your inventory is too full!") return } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: match.ID, Quantity: outputQty}) } var extraGains []xpGain if c.XP > 0 { extraGains = []xpGain{{c.EffectiveSkill(), c.XP}} } g.triggerModReward(sess, p, mod, 1.0, extraGains...) outputName := match.Name inputDef, _ := g.ItemStore.Load(inv.ItemID) inputName := inv.ItemID if inputDef != nil { inputName = inputDef.Name } sess.WriteLine(fmt.Sprintf("You superheat the %s and produce a %s.", g.itemColorize(sess, inputDef, inputName), g.itemColorize(sess, match, outputName))) } func (g *Game) triggerPlankMake(sess *net.Session, p *player.Player, mod *ModDef, targetArg string) { if g.Combat.Get(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } logTypes := map[string]struct { plankID string name string cost int }{ "logs": {"planks", "planks", 3}, "oak_logs": {"oak_planks", "oak planks", 7}, "teak_logs": {"teak_planks", "teak planks", 14}, "mahogany_logs": {"mahogany_planks", "mahogany planks", 28}, } if targetArg != "" { slot, inv := g.findInventoryItem(p, targetArg) if slot < 0 { sess.WriteLine("You don't have that item.") return } info, ok := logTypes[inv.ItemID] if !ok { sess.WriteLine("You can't turn that into planks.") return } if p.Credits < info.cost { sess.WriteLine(fmt.Sprintf("You need %d credits for Plank Make (70%% of sawmill cost).", info.cost)) return } plankDef, _ := g.ItemStore.Load(info.plankID) // Check placement BEFORE consuming the log and credits: a full // inventory must never delete materials without producing planks. if !craftOutputFits(p, info.plankID, plankDef) { sess.WriteLine("Your inventory is full.") return } if p.Action != nil { g.cancelAction(p) } logID := inv.ItemID p.RemoveItem(logID, 1) p.Credits -= info.cost if !placeCraftOutput(p, info.plankID, plankDef, 1) { // Defensive: craftOutputFits guaranteed placement. Refund the // log (to the ground, so it can never be lost) and the credits. p.Credits += info.cost g.World.AddGroundItem(p.RoomID, logID, 1) sess.WriteLine("Your inventory is full.") return } g.triggerModReward(sess, p, mod, 1.0, xpGain{Skill: string(player.Construction), XP: 10}) sess.WriteLine(fmt.Sprintf("You convert the log into %s.", info.name)) return } for logID, info := range logTypes { qty := p.CountItem(logID) if qty == 0 { continue } totalCost := info.cost * qty if p.Credits < totalCost { continue } if p.Action != nil { g.cancelAction(p) } p.RemoveItem(logID, qty) p.Credits -= totalCost processed := 0 for i := 0; i < qty; i++ { freeSlot := p.FirstFreeSlot() if freeSlot == -1 { g.World.AddGroundItem(p.RoomID, info.plankID, qty-i) break } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: info.plankID, Quantity: 1}) processed++ } g.triggerModReward(sess, p, mod, float64(qty), xpGain{Skill: string(player.Construction), XP: 10 * qty}) sess.WriteLine(fmt.Sprintf("You convert %d logs into %s for %d credits.", processed, info.name, totalCost)) if processed < qty { sess.WriteLine(fmt.Sprintf("Your inventory is full. The remaining %d planks fall to the ground.", qty-processed)) } return } sess.WriteLine("You don't have any logs to convert.") }