package game import ( "fmt" "math/rand" "sort" "strconv" "strings" "thirdcollapse/internal/action" "thirdcollapse/internal/combat" "thirdcollapse/internal/net" "thirdcollapse/internal/object" "thirdcollapse/internal/player" "thirdcollapse/internal/world" ) func (g *Game) StartAction(sess *net.Session, verb, target string) { p := sess.Player.(*player.Player) verb = normalizeVerb(verb) if combat.GetCombat(p.Name) != nil { sess.WriteLine("You can't do that during combat!") return } if p.Action != nil { g.CancelAction(p) } lower := strings.ToLower(target) instanceIdx := -1 if dotPos := strings.Index(lower, "."); dotPos > 0 { if n, err := strconv.Atoi(lower[:dotPos]); err == nil && n > 0 { instanceIdx = n - 1 lower = lower[dotPos+1:] } } instances := g.World.FindObjInstances(p.RoomID, lower) sort.Slice(instances, func(i, j int) bool { return instances[i].Index < instances[j].Index }) var obj *object.ObjectDef var mob *world.MobInstance var behaviorID string if len(instances) > 0 { var chosen *world.ObjState if instanceIdx >= 0 && instanceIdx < len(instances) { chosen = &instances[instanceIdx] } else { for i := range instances { if !instances[i].Depleted { chosen = &instances[i] break } } if chosen == nil { chosen = &instances[0] } } if chosen == nil { sess.WriteLine(fmt.Sprintf("There's nothing here to %s.", verb)) return } var err error obj, err = g.ObjectStore.Load(chosen.DefID) if err != nil || obj.BehaviorID == "" { sess.WriteLine(fmt.Sprintf("You can't %s the %s.", verb, obj.Name)) return } behaviorID = obj.BehaviorID } else { mobs := g.MobStore.MobsInRoom(p.RoomID) var candidates []*world.MobInstance for _, m := range mobs { if m.BehaviorID == "" { continue } if q := m.MatchQuality(lower); q >= world.MatchPrefix { candidates = append(candidates, m) } } if len(candidates) == 0 { sess.WriteLine(fmt.Sprintf("There's nothing here to %s.", verb)) return } sort.Slice(candidates, func(i, j int) bool { return candidates[i].InstanceID < candidates[j].InstanceID }) if instanceIdx >= 0 && instanceIdx < len(candidates) { mob = candidates[instanceIdx] } else if len(candidates) == 1 { mob = candidates[0] } else { sess.WriteLine("Which one?") return } behaviorID = mob.BehaviorID } bh, err := g.BehaviorStore.Load(behaviorID) if err != nil { if obj != nil { sess.WriteLine(fmt.Sprintf("Something is wrong with the %s.", obj.Name)) } else { sess.WriteLine("Something is wrong with that.") } return } if bh.Type != verb { if obj != nil { sess.WriteLine(fmt.Sprintf("You can't %s the %s.", verb, obj.Name)) } else { sess.WriteLine(fmt.Sprintf("You can't %s that.", verb)) } return } switch bh.Type { case "gather": if obj == nil { sess.WriteLine(fmt.Sprintf("You can't %s that.", verb)) return } // Find the actual ObjState for the chosen object chosenObj := g.World.FindObjInstances(p.RoomID, obj.ID) sort.Slice(chosenObj, func(i, j int) bool { return chosenObj[i].Index < chosenObj[j].Index }) var st *world.ObjState for i := range chosenObj { if !chosenObj[i].Depleted { st = &chosenObj[i] break } } if st == nil && len(chosenObj) > 0 { st = &chosenObj[0] } if st == nil { sess.WriteLine(fmt.Sprintf("There's nothing here to %s.", verb)) return } g.startGather(sess, p, obj, st) case "talk": if mob != nil { g.startMobTalk(sess, p, mob) } else { g.startTalk(sess, p, obj) } case "use": g.startUse(sess, p, obj) case "toggle": g.startToggle(sess, p, obj) default: sess.WriteLine(fmt.Sprintf("You can't %s that.", verb)) } } func (g *Game) CancelAction(p *player.Player) { p.Action = nil } func (g *Game) AdvanceActions() { if g.Hub == nil { return } for _, sess := range g.Hub.AllSessions() { p, ok := sess.Player.(*player.Player) if !ok || p.Action == nil { continue } if !p.Action.Advance() { continue } switch p.Action.Type { case "gather": g.advanceGather(sess, p) case "use": g.advanceUse(sess, p) } } } func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.ObjectDef, st *world.ObjState) { cfg, err := g.BehaviorStore.LoadGather(obj.BehaviorID) if err != nil { sess.WriteLine("Something is wrong with this object.") return } skillLevel := p.Level(player.SkillName(cfg.Skill)) if cfg.Level > 0 && skillLevel < cfg.Level { sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", cfg.Level, cfg.Skill)) return } if st.Depleted { sess.WriteLine(fmt.Sprintf("The %s is depleted for %d more ticks.", obj.Name, st.DepleteTimer)) return } wait := cfg.BaseWait if cfg.Tool != "" { bestSpeed := -1 for i := 0; i < 28; i++ { slot := p.InvSlot(i) if slot == nil { continue } def, err := g.ItemStore.Load(slot.ItemID) if err == nil && def.ToolType == cfg.Tool && def.ToolSpeed > bestSpeed { bestSpeed = def.ToolSpeed } } for _, itemID := range p.Toolbelt { def, err := g.ItemStore.Load(itemID) if err == nil && def.ToolType == cfg.Tool && def.ToolSpeed > bestSpeed { bestSpeed = def.ToolSpeed } } if bestSpeed < 0 { sess.WriteLine(fmt.Sprintf("You need a %s to mine the %s.", cfg.Tool, obj.Name)) return } wait = cfg.BaseWait - bestSpeed if wait < 1 { wait = 1 } } if p.FirstFreeSlot() == -1 { sess.WriteLine("Your inventory is too full!") return } p.Action = &action.Action{ Type: "gather", TargetID: st.DefID, TargetName: obj.Name, WaitLeft: 0, Data: map[string]any{ "behavior_id": obj.BehaviorID, "instance_key": g.World.ObjStateKey(st.RoomID, st.DefID, st.Index), "instance_idx": st.Index + 1, "effective_wait": wait, "step": 0, }, } } func (g *Game) advanceGather(sess *net.Session, p *player.Player) { behaviorID := p.Action.Data["behavior_id"].(string) cfg, err := g.BehaviorStore.LoadGather(behaviorID) if err != nil { g.CancelAction(p) return } step := p.Action.Data["step"].(int) wait := p.Action.Data["effective_wait"].(int) instanceKey := p.Action.Data["instance_key"].(string) if step == 0 { sess.WriteLine(fmt.Sprintf("\n%s", cfg.GatherMsg)) p.Action.Data["step"] = 1 p.Action.WaitLeft = wait return } if step == 2 { st := g.World.GetObjStateByKey(instanceKey) if st != nil && st.Depleted { p.Action.WaitLeft = 3 return } if cfg.RespawnMsg != "" { sess.WriteLine(fmt.Sprintf("\n%s", cfg.RespawnMsg)) } p.Action.Data["step"] = 0 p.Action.WaitLeft = wait return } skillLevel := p.Level(player.SkillName(cfg.Skill)) chance := action.SuccessChance(cfg.Success, skillLevel, cfg.Level) if rand.Float64() < chance { drop := g.BehaviorStore.ResolveDrop(cfg.Drops) if drop != nil { freeSlot := p.FirstFreeSlot() if freeSlot == -1 { sess.WriteLine("Your inventory is too full!") g.CancelAction(p) return } qty := drop.Quantity if qty <= 0 { qty = 1 } itemName := drop.ItemID if def, err := g.ItemStore.Load(drop.ItemID); err == nil { itemName = def.Name } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) msg := drop.Message if msg == "" { msg = fmt.Sprintf("You manage to get some %s.", itemName) } sess.WriteLine(msg) g.AccountStore.SaveCharacter(p) if drop.Depletes { delay := cfg.DepleteDelay if delay <= 0 { delay = 10 } st := g.World.GetObjStateByKey(instanceKey) if st != nil { st.Depleted = true st.DepleteTimer = delay } for _, other := range g.Hub.PlayersInRoom(p.RoomID) { if other == sess { continue } op, ok := other.Player.(*player.Player) if !ok || op.Action == nil || op.Action.Type != "gather" { continue } if op.Action.Data["instance_key"] == instanceKey { other.WriteLine(fmt.Sprintf("\nThe %s was depleted by %s!", p.Action.TargetName, p.Name)) g.CancelAction(op) } } p.Action.Data["step"] = 2 p.Action.WaitLeft = 1 return } // Non-depleting drop: loop back. Check inventory before looping. if p.FirstFreeSlot() == -1 { sess.WriteLine("Your inventory is too full!") g.CancelAction(p) return } p.Action.Data["step"] = 0 p.Action.WaitLeft = wait return } } // Failed: show fail message, loop back sess.WriteLine(cfg.FailMsg) if p.FirstFreeSlot() == -1 { sess.WriteLine("Your inventory is too full!") g.CancelAction(p) return } p.Action.Data["step"] = 0 p.Action.WaitLeft = wait } func (g *Game) startMobTalk(sess *net.Session, p *player.Player, mob *world.MobInstance) { cfg, err := g.BehaviorStore.LoadTalk(mob.BehaviorID) if err != nil { sess.WriteLine("This person has nothing to say.") return } if node, ok := cfg.Nodes["start"]; ok { p.Action = &action.Action{ Type: "talk", TargetID: mob.DefID, TargetName: mob.Name, Data: map[string]any{"node": "start", "behavior_id": mob.BehaviorID}, } g.showTalkNode(sess, node) } else { sess.WriteLine("This person has nothing to say.") } } func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.ObjectDef) { cfg, err := g.BehaviorStore.LoadTalk(obj.BehaviorID) if err != nil { sess.WriteLine("This person has nothing to say.") return } if node, ok := cfg.Nodes["start"]; ok { p.Action = &action.Action{ Type: "talk", TargetID: obj.ID, TargetName: obj.Name, Data: map[string]any{"node": "start", "behavior_id": obj.BehaviorID}, } g.showTalkNode(sess, node) } else { sess.WriteLine("This person has nothing to say.") } } func (g *Game) showTalkNode(sess *net.Session, node action.TalkNode) { sess.WriteLine(fmt.Sprintf("\n%s", node.Message)) if node.Action != nil { g.applyNodeAction(sess, node.Action) } if len(node.Options) == 0 { sess.State = net.StateGame sess.Write("\r\n> ") return } visible := 0 for _, opt := range node.Options { if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) { continue } visible++ sess.WriteLine(fmt.Sprintf(" %d. %s", visible, opt.Text)) } sess.State = net.StateTalk sess.Write("\nChoice: ") } func (g *Game) handleTalkInput(sess *net.Session, input string) { p, ok := sess.Player.(*player.Player) if !ok || p.Action == nil || p.Action.Type != "talk" { sess.State = net.StateGame sess.Write("\r\n> ") return } input = strings.TrimSpace(input) lower := strings.ToLower(input) if lower == "bye" || lower == "exit" || lower == "end" || lower == "quit" { g.CancelAction(p) sess.State = net.StateGame sess.Write("\r\n> ") return } behaviorID := p.Action.Data["behavior_id"].(string) cfg, err := g.BehaviorStore.LoadTalk(behaviorID) if err != nil { g.CancelAction(p) sess.State = net.StateGame sess.Write("\r\n> ") return } nodeKey := p.Action.Data["node"].(string) node, ok := cfg.Nodes[nodeKey] if !ok { g.CancelAction(p) sess.State = net.StateGame sess.Write("\r\n> ") return } idx, err := parseIndex(input) if err != nil || idx <= 0 { g.CancelAction(p) sess.State = net.StateGame sess.Write("\r\n> ") return } validIdx := 0 var chosen *action.TalkOption for i := range node.Options { opt := &node.Options[i] if opt.Condition != nil && !g.checkCondition(sess, opt.Condition) { continue } validIdx++ if validIdx == idx { chosen = opt break } } if chosen == nil { g.CancelAction(p) sess.State = net.StateGame sess.Write("\r\n> ") return } if chosen.End { g.CancelAction(p) sess.State = net.StateGame sess.Write("\r\n> ") return } if chosen.Goto != "" { if nextNode, ok := cfg.Nodes[chosen.Goto]; ok { p.Action.Data["node"] = chosen.Goto g.showTalkNode(sess, nextNode) return } } g.CancelAction(p) sess.State = net.StateGame sess.Write("\r\n> ") } func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) { p, ok := sess.Player.(*player.Player) if !ok { return } for k, v := range na.SetFlags { g.WorldFlags[k] = v } for k, v := range na.SetPlayerFlags { if p.Flags == nil { p.Flags = make(map[string]any) } p.Flags[k] = v } if na.TakeItem != "" { if p.HasItem(na.TakeItem) { p.RemoveItem(na.TakeItem, 1) } } if na.GiveItem != "" { slot := p.FirstFreeSlot() if slot == -1 { sess.WriteLine("Your inventory is too full to receive that.") } else { p.SetInvSlot(slot, &player.InventorySlot{ItemID: na.GiveItem, Quantity: 1}) g.AccountStore.SaveCharacter(p) } } if na.Teleport > 0 { p.RoomID = na.Teleport g.AccountStore.SaveCharacter(p) g.World.SeedGroundItems(p.RoomID) g.seedRoomMobs(p.RoomID) g.ensureRoomObjects(p.RoomID) if g.Hub != nil { g.Hub.EnterRoom(sess, p.RoomID) } g.doLook(sess) g.RunEnterSteps(sess, p.RoomID) } if na.Heal > 0 { p.HP += na.Heal if maxHP := p.MaxHP(); p.HP > maxHP { p.HP = maxHP } g.AccountStore.SaveCharacter(p) sess.WriteLine(fmt.Sprintf("You regain %d hitpoints.", na.Heal)) } } func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool { p, _ := sess.Player.(*player.Player) if c.AllOf != nil { for _, sub := range c.AllOf { if !g.checkCondition(sess, &sub) { return false } } return true } if c.AnyOf != nil { for _, sub := range c.AnyOf { if g.checkCondition(sess, &sub) { return true } } return false } if c.PlayerFlag != "" { if p == nil || p.Flags == nil { return c.Not } val, ok := p.Flags[c.PlayerFlag] if c.Not { return !ok || val != c.Value } return ok && val == c.Value } if c.Flag != "" { val, ok := g.WorldFlags[c.Flag] if c.Not { return !ok || val != c.Value } return ok && val == c.Value } if c.HasItem != "" { has := p != nil && p.HasItem(c.HasItem) if c.Not { return !has } return has } return true } func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectDef) { cfg, err := g.BehaviorStore.LoadUse(obj.BehaviorID) if err != nil { sess.WriteLine("You can't use this.") return } for itemID, qty := range cfg.Consume { if !p.HasItem(itemID) { defName := itemID if def, err := g.ItemStore.Load(itemID); err == nil { defName = def.Name } sess.WriteLine(fmt.Sprintf("You need %s to use this.", defName)) return } _ = qty } if cfg.Success == nil && p.FirstFreeSlot() == -1 { sess.WriteLine("Your inventory is full.") return } p.Action = &action.Action{ Type: "use", TargetID: obj.ID, TargetName: obj.Name, WaitLeft: 1, Data: map[string]any{"step": 0, "behavior_id": obj.BehaviorID}, } sess.WriteLine(fmt.Sprintf("\n%s", cfg.Message)) } func (g *Game) advanceUse(sess *net.Session, p *player.Player) { behaviorID := p.Action.Data["behavior_id"].(string) cfg, err := g.BehaviorStore.LoadUse(behaviorID) if err != nil { g.CancelAction(p) return } for itemID, qty := range cfg.Consume { if !p.HasItem(itemID) { defName := itemID if def, err := g.ItemStore.Load(itemID); err == nil { defName = def.Name } sess.WriteLine(fmt.Sprintf("You've run out of %s.", defName)) g.CancelAction(p) return } if !p.RemoveItem(itemID, qty) { sess.WriteLine(fmt.Sprintf("You need %d of %s.", qty, itemID)) g.CancelAction(p) return } } if cfg.Success != nil { skillLevel := p.Level(player.SkillName(cfg.Skill)) chance := action.SuccessChance(*cfg.Success, skillLevel, cfg.Level) if rand.Float64() >= chance { if cfg.FailMsg != "" { sess.WriteLine(cfg.FailMsg) } p.Action.WaitLeft = cfg.Wait return } } freeSlot := p.FirstFreeSlot() if freeSlot == -1 { sess.WriteLine("Your inventory is full.") g.CancelAction(p) return } qty := cfg.Reward.Quantity if qty <= 0 { qty = 1 } p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: cfg.Reward.ItemID, Quantity: qty}) g.AccountStore.SaveCharacter(p) itemName := cfg.Reward.ItemID if def, err := g.ItemStore.Load(cfg.Reward.ItemID); err == nil { itemName = def.Name } sess.WriteLine(fmt.Sprintf("You make a %s.", itemName)) if p.FirstFreeSlot() == -1 { sess.WriteLine("Your inventory is full.") g.CancelAction(p) return } p.Action.WaitLeft = cfg.Wait } func (g *Game) startToggle(sess *net.Session, p *player.Player, obj *object.ObjectDef) { cfg, err := g.BehaviorStore.LoadToggle(obj.BehaviorID) if err != nil { sess.WriteLine("Nothing happens.") return } if cfg.Check != nil { val, ok := g.WorldFlags[cfg.Check.Flag] if cfg.Check.Not { if ok && val == cfg.Check.Value { sess.WriteLine("Nothing happens.") return } } else { if !ok || val != cfg.Check.Value { sess.WriteLine("Nothing happens.") return } } } for flag, val := range cfg.SetFlags { g.WorldFlags[flag] = val } sess.WriteLine(fmt.Sprintf("\n%s", cfg.Message)) if g.Hub != nil { for _, other := range g.Hub.PlayersInRoom(p.RoomID) { if other != sess && other.Player != nil { other.WriteLine(fmt.Sprintf("\n%s uses the %s.", p.Name, obj.Name)) } } } } func normalizeVerb(v string) string { switch v { case "mine", "chop", "fish": return "gather" case "talk", "speak", "ask": return "talk" case "pull", "push": return "toggle" } return v } func (g *Game) RunEnterSteps(sess *net.Session, roomID int) { room, err := g.World.LoadRoom(roomID) if err != nil || len(room.OnEnter) == 0 { return } for _, step := range room.OnEnter { if step.Condition != nil && !g.checkCondition(sess, step.Condition) { continue } if step.Message != "" { sess.WriteLine(fmt.Sprintf("\n%s", step.Message)) } } } func (g *Game) BroadcastRespawns() { respawns := g.World.FlushObjRespawns() for _, st := range respawns { def, err := g.ObjectStore.Load(st.DefID) if err != nil || def.BehaviorID == "" { continue } cfg, err := g.BehaviorStore.LoadGather(def.BehaviorID) if err != nil || cfg.RespawnBroadcast == "" { continue } name := def.Name if idx := st.Index + 1; len(g.World.FindObjInstances(st.RoomID, st.DefID)) > 1 { name += fmt.Sprintf(" [%d]", idx) } msg := strings.ReplaceAll(cfg.RespawnBroadcast, "{name}", name) if g.Hub != nil { for _, sess := range g.Hub.PlayersInRoom(st.RoomID) { sess.WriteLine(fmt.Sprintf("\n%s", msg)) } } } }