package game import ( "fmt" "math/rand" "thirdcollapse/internal/action" "thirdcollapse/internal/net" "thirdcollapse/internal/object" "thirdcollapse/internal/player" ) func (g *Game) doBurn(sess *net.Session, p *player.Player, input string) { if p.Action != nil { g.CancelAction(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) } 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.", 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, 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.", 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.", logDef.Name)) return } } phase := 1 if !fromGround { phase = 0 } p.ActionState = &ActionState{Type: ActionBurning} p.Action = &action.Action{ Type: "burn", TargetID: itemID, TargetName: logDef.Name, WaitLeft: 0, Data: map[string]any{ "item_id": itemID, "tool_speed": toolSpeed, "level": logDef.FireLevel, "xp": logDef.FireXP, "burn_ticks": 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.", logDef.Name)) return } sess.WriteLine("You begin to tend to the fire.") p.ActionState = &ActionState{Type: ActionStoking} p.Action = &action.Action{ Type: "stoke", TargetID: itemID, TargetName: logDef.Name, WaitLeft: g.computeTicks(10), Data: map[string]any{ "item_id": itemID, "fire_key": fireKey, "burn_ticks": logDef.BurnTicks, "xp": logDef.FireXP, }, } } func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { data := p.Action.Data itemID := data["item_id"].(string) toolSpeed := data["tool_speed"].(float64) phase := data["phase"].(int) 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.", logDef.Name)) g.CancelAction(p) return } p.RemoveItem(itemID, 1) g.World.AddGroundItem(p.RoomID, itemID, 1) g.AccountStore.SaveCharacter(p) sess.WriteLine(fmt.Sprintf("You drop the %s and begin to make a fire...", logDef.Name)) g.broadcastDrop(p, logDef.Name) data["phase"] = 1 p.Action.WaitLeft = g.computeTicks(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(fmt.Sprintf("You try burning the %s on the ground...", logDef.Name)) data["phase"] = 2 p.Action.WaitLeft = g.computeTicks(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 { if newLevel := p.AddSkillXP(player.Firemaking, xp); newLevel > 0 { sess.WriteLine(fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel)) } } g.AccountStore.SaveCharacter(p) g.CancelAction(p) msg := fmt.Sprintf("You manage to get a fire going!") if xp > 0 && p.OptionBool("xp_drops") { msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking]) } sess.WriteLine(msg) for _, other := range g.Hub.PlayersInRoom(p.RoomID) { if other != sess { other.WriteLine(fmt.Sprintf("\n%s started a fire!", p.Name)) } } return } sess.WriteLine("You can't seem to get a fire going.") data["phase"] = 1 p.Action.WaitLeft = g.computeTicks(toolSpeed) } } func (g *Game) advanceStoke(sess *net.Session, p *player.Player) { data := p.Action.Data itemID := data["item_id"].(string) fireKey := data["fire_key"].(string) burnTicks := data["burn_ticks"].(float64) xp := data["xp"].(int) 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(fmt.Sprintf("You're out of %s and the fire is roaring.", name)) g.CancelAction(p) return } p.RemoveItem(itemID, 1) st.Quality += burnTicks / 2 if xp > 0 { if newLevel := p.AddSkillXP(player.Firemaking, xp); newLevel > 0 { sess.WriteLine(fmt.Sprintf("*** You are now level %d firemaking! ***", newLevel)) } } g.AccountStore.SaveCharacter(p) msg := fmt.Sprintf("You throw %s onto the fire.", name) if xp > 0 && p.OptionBool("xp_drops") { msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking]) } sess.WriteLine(msg) if !p.HasItem(itemID) { sess.WriteLine(fmt.Sprintf("You're out of %s and the fire is roaring.", name)) g.CancelAction(p) return } p.Action.WaitLeft = g.computeTicks(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[object.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, itemName string) { for _, other := range g.Hub.PlayersInRoom(p.RoomID) { op, ok := other.Player.(*player.Player) if ok && op.Name != p.Name { other.WriteLine(fmt.Sprintf("\n%s drops a %s.", p.Name, itemName)) } } } 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, ok := other.Player.(*player.Player) if ok && op.Action != nil && op.Action.Type == "stoke" { other.WriteLine("The fire went out!") g.CancelAction(op) } } } func (g *Game) resolveBurnTarget(p *player.Player, input string) (itemID string, fromGround bool, ambiguous bool) { if input != "" { return g.resolveNamedBurnTarget(p, input) } return g.resolveDefaultBurnTarget(p) } func (g *Game) collectBurnableGround(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) collectBurnableInv(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) resolveNamedBurnTarget(p *player.Player, input string) (itemID string, fromGround bool, ambiguous bool) { groundMatches := g.collectBurnableGround(p.RoomID, input) if len(groundMatches) > 1 { return "", false, true } if len(groundMatches) == 1 { return groundMatches[0], true, false } invMatches := g.collectBurnableInv(p, input) if len(invMatches) > 1 { return "", false, true } if len(invMatches) == 1 { return invMatches[0], false, false } return "", false, false } func (g *Game) resolveDefaultBurnTarget(p *player.Player) (itemID string, fromGround bool, ambiguous bool) { groundBurnable := g.collectBurnableGround(p.RoomID, "") if len(groundBurnable) == 1 { return groundBurnable[0], true, false } if len(groundBurnable) > 1 { return "", false, true } invBurnable := g.collectBurnableInv(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.collectBurnableInv(p, input) if len(invBurnable) == 1 { return invBurnable[0], false } if len(invBurnable) > 1 { return "", true } return "", false }