package game import ( "fmt" "math/rand" "strings" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func (g *Game) executeBurn(sess *net.Session, args []string, rawInput string) { p := sess.Player g.cancelAction(p) g.doBurn(sess, p, strings.Join(args, " ")) } func (g *Game) executeStoke(sess *net.Session, args []string, rawInput string) { p := sess.Player g.cancelAction(p) g.doStoke(sess, p, strings.Join(args, " ")) } func (g *Game) doBurn(sess *net.Session, p *player.Player, input string) { if p.Action != nil { g.cancelAction(p) } g.cancelBgAction(p) itemID, fromGround, ambiguous := g.resolveBurnTarget(p, input) if ambiguous { sess.WriteLine("Burn what?") return } if itemID == "" { sess.WriteLine("Burn what?") return } g.startBurn(sess, p, itemID, fromGround) } func (g *Game) doStoke(sess *net.Session, p *player.Player, input string) { if p.Action != nil { g.cancelAction(p) } g.cancelBgAction(p) if !g.hasFireObject(p.RoomID) { sess.WriteLine("There's no fire here to stoke.") return } itemID, ambiguous := g.resolveStokeTarget(p, input) if ambiguous { sess.WriteLine("Stoke what?") return } if itemID == "" { sess.WriteLine("You have no logs to stoke with!") return } g.startStoke(sess, p, itemID) } func (g *Game) startBurn(sess *net.Session, p *player.Player, itemID string, fromGround bool) { if p.FirstFreeSlot() == -1 && !fromGround { sess.WriteLine("Your inventory is too full!") return } if g.hasFireObject(p.RoomID) { sess.WriteLine("There is already a fire burning here.") return } logDef, err := g.ItemStore.Load(itemID) if err != nil { sess.WriteLine("Something is wrong with that item.") return } if logDef.BurnTicks <= 0 { sess.WriteLine(fmt.Sprintf("You can't burn the %s.", g.itemColorize(sess, logDef, logDef.Name))) return } skillLevel := p.Level(player.Firemaking) if logDef.FireLevel > 0 && skillLevel < logDef.FireLevel { sess.WriteLine(fmt.Sprintf("You need level %d firemaking to burn %s.", logDef.FireLevel, g.itemColorize(sess, logDef, logDef.Name))) return } toolSpeed, ok := g.findFireTool(sess, p) if !ok { return } if !fromGround { if !p.HasItem(itemID) { sess.WriteLine(fmt.Sprintf("You don't have any %s.", g.itemColorize(sess, logDef, logDef.Name))) return } } else { ground := g.World.GroundItems(p.RoomID) if ground[itemID] <= 0 { sess.WriteLine(fmt.Sprintf("There are no %s on the ground here.", g.itemColorize(sess, logDef, logDef.Name))) return } } phase := 1 if !fromGround { phase = 0 } g.broadcastAction(sess, "%s starts building a fire.", p.Name) p.Action = &behavior.Action{ Type: behavior.TypeBurn, TargetID: itemID, TargetName: logDef.Name, WaitLeft: 0, Data: &behavior.BurnData{ ItemID: itemID, ToolSpeed: toolSpeed, Level: logDef.FireLevel, XP: logDef.FireXP, BurnTicks: logDef.BurnTicks, Phase: phase, }, } } func (g *Game) startStoke(sess *net.Session, p *player.Player, itemID string) { fireKey := g.findFireObjectKey(p.RoomID) if fireKey == "" { sess.WriteLine("There's no fire here to stoke.") return } logDef, err := g.ItemStore.Load(itemID) if err != nil || logDef.BurnTicks <= 0 { sess.WriteLine(fmt.Sprintf("You can't stoke the fire with that.")) return } if !p.HasItem(itemID) { sess.WriteLine(fmt.Sprintf("You don't have any %s.", g.itemColorize(sess, logDef, logDef.Name))) return } sess.WriteLine("You begin to tend to the fire.") g.broadcastAction(sess, "%s tends to the fire.", p.Name) p.Action = &behavior.Action{ Type: behavior.TypeStoke, TargetID: itemID, TargetName: logDef.Name, WaitLeft: engine.ToTicks(10), Data: &behavior.BurnData{ ItemID: itemID, BurnTicks: logDef.BurnTicks, XP: logDef.FireXP, }, } } func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { d := p.Action.Data.(*behavior.BurnData) itemID := d.ItemID toolSpeed := d.ToolSpeed phase := d.Phase logDef, err := g.ItemStore.Load(itemID) if err != nil { g.cancelAction(p) return } if phase == 0 { if !p.HasItem(itemID) { sess.WriteLine(fmt.Sprintf("You're out of %s.", g.itemColorize(sess, logDef, logDef.Name))) g.cancelAction(p) return } p.RemoveItem(itemID, 1) g.World.AddGroundItem(p.RoomID, itemID, 1) g.AccountStore.SaveCharacter(p) sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You drop the %s and begin to make a fire...", g.itemColorize(sess, logDef, logDef.Name)))) g.broadcastDrop(p, itemID, logDef.Name) d.Phase = 1 p.Action.WaitLeft = engine.ToTicks(toolSpeed) return } ground := g.World.GroundItems(p.RoomID) if ground[itemID] <= 0 { sess.WriteLine("The thing you were trying to burn is gone.") g.cancelAction(p) return } if phase == 1 { sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You try burning the %s on the ground...", g.itemColorize(sess, logDef, logDef.Name)))) d.Phase = 2 p.Action.WaitLeft = engine.ToTicks(toolSpeed) return } if phase == 2 { skillLevel := p.Level(player.Firemaking) chance := 0.5 + float64(skillLevel-logDef.FireLevel)*0.02 if chance > 0.95 { chance = 0.95 } if chance < 0.05 { chance = 0.05 } if rand.Float64() < chance { g.World.RemoveGroundItem(p.RoomID, itemID, 1) g.World.AddObjInstance(p.RoomID, "fire", logDef.BurnTicks) xp := logDef.FireXP if xp > 0 { g.awardSkillXP(sess, p, player.Firemaking, xp) } g.AccountStore.SaveCharacter(p) g.cancelAction(p) msg := g.colorize(sess, "fire", "You manage to get a fire going!") if xp > 0 && p.OptionBool("xp_drops") { msg += g.formatXpDropSingle(sess, p, player.Firemaking, xp) } sess.WriteLine(msg) g.broadcastAction(sess, "%s started a fire!", p.Name) return } sess.WriteLine("You can't seem to get a fire going.") d.Phase = 1 p.Action.WaitLeft = engine.ToTicks(toolSpeed) } } func (g *Game) advanceStoke(sess *net.Session, p *player.Player) { d := p.Action.Data.(*behavior.BurnData) itemID := d.ItemID fireKey := g.findFireObjectKey(p.RoomID) burnTicks := d.BurnTicks xp := d.XP st := g.World.GetObjStateByKey(fireKey) if st == nil || st.Quality <= 0 { sess.WriteLine("The fire went out!") g.cancelAction(p) return } logDef, _ := g.ItemStore.Load(itemID) name := itemID if logDef != nil { name = logDef.Name } if !p.HasItem(itemID) { sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You're out of %s and the fire is roaring.", g.itemColorize(sess, logDef, name)))) g.cancelAction(p) return } p.RemoveItem(itemID, 1) st.Quality += burnTicks / 2 if xp > 0 { g.awardSkillXP(sess, p, player.Firemaking, xp) } g.AccountStore.SaveCharacter(p) msg := g.colorize(sess, "fire", fmt.Sprintf("You throw %s onto the fire.", g.itemColorize(sess, logDef, name))) if xp > 0 && p.OptionBool("xp_drops") { msg += g.formatXpDropSingle(sess, p, player.Firemaking, xp) } sess.WriteLine(msg) if !p.HasItem(itemID) { sess.WriteLine(g.colorize(sess, "fire", fmt.Sprintf("You're out of %s and the fire is roaring.", g.itemColorize(sess, logDef, name)))) g.cancelAction(p) return } p.Action.WaitLeft = engine.ToTicks(10.0) } func (g *Game) findFireTool(sess *net.Session, p *player.Player) (toolSpeed float64, ok bool) { toolSpeed = -1 var toolItemID string if itemID, equipped := p.Equipment[item.SlotMainHand]; equipped { def, err := g.ItemStore.Load(itemID) if err == nil && def.ToolType() == "fire" { toolSpeed = def.ToolSpeed() toolItemID = itemID } } if toolSpeed < 0 { for _, slot := range p.Inventory { if slot == nil || slot.Quantity <= 0 { continue } def, err := g.ItemStore.Load(slot.ItemID) if err != nil || def.ToolType() != "fire" { continue } toolSpeed = def.ToolSpeed() toolItemID = slot.ItemID break } } if toolSpeed < 0 { sess.WriteLine("You need a fire starter to do that.") return 0, false } if !g.consumeFireTool(sess, p, toolItemID) { return 0, false } return toolSpeed, true } func (g *Game) consumeFireTool(sess *net.Session, p *player.Player, toolItemID string) bool { def, _ := g.ItemStore.Load(toolItemID) if def != nil && def.MaxQuality > 0 { for _, slot := range p.Inventory { if slot != nil && slot.ItemID == toolItemID && slot.MaxQuality > 0 { if slot.Quality <= 0 { sess.WriteLine("The lighter is out of butane!") return false } slot.Quality-- g.AccountStore.SaveCharacter(p) return true } } if q, ok := p.EquipQuality[toolItemID]; ok { if q <= 0 { sess.WriteLine("The lighter is out of butane!") return false } p.EquipQuality[toolItemID] = q - 1 g.AccountStore.SaveCharacter(p) return true } } if def != nil && def.Stackable { if p.HasItem(toolItemID) { p.RemoveItem(toolItemID, 1) g.AccountStore.SaveCharacter(p) return true } for slot, itemID := range p.Equipment { if itemID == toolItemID { delete(p.Equipment, slot) g.AccountStore.SaveCharacter(p) return true } } sess.WriteLine("You need a fire starter to do that.") return false } return true } func (g *Game) findFireObjectKey(roomID int) string { objs := g.World.AllObjInstances(roomID) for _, st := range objs { if st.DefID == "fire" && st.Quality > 0 { return g.World.ObjStateKey(st.RoomID, st.DefID, st.Index) } } return "" } func (g *Game) hasFireObject(roomID int) bool { return g.findFireObjectKey(roomID) != "" } func (g *Game) broadcastDrop(p *player.Player, itemID, itemName string) { itemDef, _ := g.ItemStore.Load(itemID) for _, other := range g.Hub.PlayersInRoom(p.RoomID) { op := other.Player if op != nil && op.Name != p.Name { colored := itemName if itemDef != nil { colored = g.itemColorize(other, itemDef, itemName) } other.WriteLine(fmt.Sprintf("%s drops a %s.", p.Name, colored)) } } } func (g *Game) FireTick() { for _, st := range g.World.AllFireStates() { st.Quality-- if st.Quality <= 0 { g.World.RemoveObjInstance(st.RoomID, st.DefID) g.cancelStokers(st.RoomID) if def, err := g.ObjectStore.Load(st.DefID); err == nil && def.RemovalItem != "" { g.World.AddGroundItem(st.RoomID, def.RemovalItem, 1) } } } } func (g *Game) cancelStokers(roomID int) { for _, other := range g.Hub.PlayersInRoom(roomID) { op := other.Player if op != nil && op.Action != nil && op.Action.Type == behavior.TypeStoke { other.WriteLine("The fire went out!") g.cancelAction(op) g.writePrompt(other) } } } func (g *Game) resolveBurnTarget(p *player.Player, input string) (itemID string, fromGround bool, ambiguous bool) { if input != "" { return g.namedBurnTarget(p, input) } return g.defaultBurnTarget(p) } func (g *Game) burnableGround(roomID int, name string) []string { var out []string for itemID := range g.World.GroundItems(roomID) { def, err := g.ItemStore.Load(itemID) if err != nil || def.BurnTicks <= 0 { continue } if name == "" || def.MatchesName(name) { out = append(out, itemID) } } return out } func (g *Game) burnableInv(p *player.Player, name string) []string { var out []string seen := make(map[string]bool) for _, slot := range p.Inventory { if slot == nil || slot.Quantity <= 0 { continue } def, err := g.ItemStore.Load(slot.ItemID) if err != nil || def.BurnTicks <= 0 || seen[slot.ItemID] { continue } if name == "" || def.MatchesName(name) { seen[slot.ItemID] = true out = append(out, slot.ItemID) } } return out } func (g *Game) namedBurnTarget(p *player.Player, input string) (itemID string, fromGround bool, ambiguous bool) { groundMatches := g.burnableGround(p.RoomID, input) if len(groundMatches) > 1 { return "", false, true } if len(groundMatches) == 1 { return groundMatches[0], true, false } invMatches := g.burnableInv(p, input) if len(invMatches) > 1 { return "", false, true } if len(invMatches) == 1 { return invMatches[0], false, false } return "", false, false } func (g *Game) defaultBurnTarget(p *player.Player) (itemID string, fromGround bool, ambiguous bool) { groundBurnable := g.burnableGround(p.RoomID, "") if len(groundBurnable) == 1 { return groundBurnable[0], true, false } if len(groundBurnable) > 1 { return "", false, true } invBurnable := g.burnableInv(p, "") if len(invBurnable) == 1 { return invBurnable[0], false, false } if len(invBurnable) > 1 { return "", false, true } return "", false, false } func (g *Game) resolveStokeTarget(p *player.Player, input string) (string, bool) { invBurnable := g.burnableInv(p, input) if len(invBurnable) == 1 { return invBurnable[0], false } if len(invBurnable) > 1 { return "", true } return "", false }