aboutsummaryrefslogtreecommitdiff
path: root/internal/game/action.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-11 04:46:27 -0400
committerhistoria <[not public]>2026-06-11 08:58:37 +0000
commit1b9e2da3b3c438d8dc53d3489725dd5ba0022777 (patch)
tree62264f24420bf4cfaa734942c65cc8dd45ba3d3b /internal/game/action.go
parent06c02a697e5daf8832a462de5970dd4c0e13a02c (diff)
downloadthehouseoficarus-1b9e2da3b3c438d8dc53d3489725dd5ba0022777.tar.gz
feat: web client, get/drop updates and fixes
Diffstat (limited to 'internal/game/action.go')
-rw-r--r--internal/game/action.go593
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))
- }
- }
- }
-}