diff options
| author | historia <[not public]> | 2026-06-11 04:46:27 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-11 08:58:37 +0000 |
| commit | 1b9e2da3b3c438d8dc53d3489725dd5ba0022777 (patch) | |
| tree | 62264f24420bf4cfaa734942c65cc8dd45ba3d3b /internal/game/action.go | |
| parent | 06c02a697e5daf8832a462de5970dd4c0e13a02c (diff) | |
| download | thehouseoficarus-1b9e2da3b3c438d8dc53d3489725dd5ba0022777.tar.gz | |
feat: web client, get/drop updates and fixes
Diffstat (limited to 'internal/game/action.go')
| -rw-r--r-- | internal/game/action.go | 593 |
1 files changed, 18 insertions, 575 deletions
diff --git a/internal/game/action.go b/internal/game/action.go index 62cdf62..018c4fb 100644 --- a/internal/game/action.go +++ b/internal/game/action.go @@ -2,7 +2,6 @@ package game import ( "fmt" - "math/rand" "sort" "strconv" "strings" @@ -48,6 +47,15 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) { var behaviorID string if len(instances) > 0 { + uniqueDefs := make(map[string]bool) + for _, ist := range instances { + uniqueDefs[ist.DefID] = true + } + if len(uniqueDefs) > 1 { + sess.WriteLine("That's ambiguous, which one?") + return + } + var chosen *world.ObjState if instanceIdx >= 0 && instanceIdx < len(instances) { chosen = &instances[instanceIdx] @@ -127,7 +135,6 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) { 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 @@ -187,389 +194,16 @@ func (g *Game) AdvanceActions() { } } -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 normalizeVerb(v string) string { + switch v { + case "mine", "chop", "fish", "cut": + return "gather" + case "talk", "speak", "ask": + return "talk" + case "pull", "push": + return "toggle" } + return v } func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool { @@ -619,194 +253,3 @@ func (g *Game) checkCondition(sess *net.Session, c *action.Condition) bool { } 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)) - } - } - } -} |
