diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/action.go | 3 | ||||
| -rw-r--r-- | internal/game/action_burn.go | 20 | ||||
| -rw-r--r-- | internal/game/action_gather.go | 140 | ||||
| -rw-r--r-- | internal/game/action_search.go | 122 | ||||
| -rw-r--r-- | internal/game/action_state.go | 61 | ||||
| -rw-r--r-- | internal/game/action_talk.go | 2 | ||||
| -rw-r--r-- | internal/game/action_toggle.go | 2 | ||||
| -rw-r--r-- | internal/game/action_use.go | 8 | ||||
| -rw-r--r-- | internal/game/cmd_attack.go | 14 | ||||
| -rw-r--r-- | internal/game/cmd_drop.go | 3 | ||||
| -rw-r--r-- | internal/game/cmd_get.go | 6 | ||||
| -rw-r--r-- | internal/game/cmd_look.go | 40 | ||||
| -rw-r--r-- | internal/game/cmd_move.go | 1 | ||||
| -rw-r--r-- | internal/game/cmd_queued.go | 66 | ||||
| -rw-r--r-- | internal/game/cmd_quit.go | 3 | ||||
| -rw-r--r-- | internal/game/cmd_search.go | 68 | ||||
| -rw-r--r-- | internal/game/doc.go | 5 | ||||
| -rw-r--r-- | internal/game/game.go | 215 | ||||
| -rw-r--r-- | internal/game/map_test.go | 6 | ||||
| -rw-r--r-- | internal/game/tick.go | 34 | ||||
| -rw-r--r-- | internal/game/utils.go | 23 |
21 files changed, 647 insertions, 195 deletions
diff --git a/internal/game/action.go b/internal/game/action.go index 7d2549f..ce29559 100644 --- a/internal/game/action.go +++ b/internal/game/action.go @@ -171,6 +171,7 @@ func (g *Game) StartAction(sess *net.Session, verb, target string) { func (g *Game) CancelAction(p *player.Player) { p.Action = nil + p.ActionState = nil } func (g *Game) AdvanceActions() { @@ -194,6 +195,8 @@ func (g *Game) AdvanceActions() { g.advanceBurn(sess, p) case "stoke": g.advanceStoke(sess, p) + case "search": + g.advanceSearch(sess, p) } } } diff --git a/internal/game/action_burn.go b/internal/game/action_burn.go index d57307a..917a6fe 100644 --- a/internal/game/action_burn.go +++ b/internal/game/action_burn.go @@ -102,6 +102,8 @@ func (g *Game) startBurn(sess *net.Session, p *player.Player, itemID string, fro phase = 0 } + p.ActionState = &ActionState{Type: ActionBurning} + p.Action = &action.Action{ Type: "burn", TargetID: itemID, @@ -138,11 +140,13 @@ func (g *Game) startStoke(sess *net.Session, p *player.Player, itemID string) { 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: 10, + WaitLeft: g.computeTicks(10), Data: map[string]any{ "item_id": itemID, "fire_key": fireKey, @@ -155,7 +159,7 @@ func (g *Game) startStoke(sess *net.Session, p *player.Player, itemID string) { func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { data := p.Action.Data itemID := data["item_id"].(string) - toolSpeed := data["tool_speed"].(int) + toolSpeed := data["tool_speed"].(float64) phase := data["phase"].(int) logDef, err := g.ItemStore.Load(itemID) @@ -178,7 +182,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { g.broadcastDrop(p, logDef.Name) data["phase"] = 1 - p.Action.WaitLeft = toolSpeed + p.Action.WaitLeft = g.computeTicks(toolSpeed) return } @@ -192,7 +196,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { if phase == 1 { sess.WriteLine(fmt.Sprintf("You try burning the %s on the ground...", logDef.Name)) data["phase"] = 2 - p.Action.WaitLeft = toolSpeed + p.Action.WaitLeft = g.computeTicks(toolSpeed) return } @@ -236,7 +240,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { sess.WriteLine("You can't seem to get a fire going.") data["phase"] = 1 - p.Action.WaitLeft = toolSpeed + p.Action.WaitLeft = g.computeTicks(toolSpeed) } } @@ -244,7 +248,7 @@ 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"].(int) + burnTicks := data["burn_ticks"].(float64) xp := data["xp"].(int) st := g.World.GetObjStateByKey(fireKey) @@ -289,10 +293,10 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) { return } - p.Action.WaitLeft = 10 + p.Action.WaitLeft = g.computeTicks(10.0) } -func (g *Game) findFireTool(sess *net.Session, p *player.Player) (toolSpeed int, ok bool) { +func (g *Game) findFireTool(sess *net.Session, p *player.Player) (toolSpeed float64, ok bool) { toolSpeed = -1 var toolItemID string diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go index 201a619..512be81 100644 --- a/internal/game/action_gather.go +++ b/internal/game/action_gather.go @@ -29,13 +29,13 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje if cfg.DepletedMessage != "" { msg := cfg.DepletedMessage if p.OptionBool("depletion") { - msg += fmt.Sprintf(" (%d ticks to respawn)", st.DepleteTimer) + msg += fmt.Sprintf(" (%d ticks to respawn)", int(st.DepleteTimer)) } sess.WriteLine(msg) } else if cfg.DepleteTimer > 0 { - sess.WriteLine(fmt.Sprintf("The %s has been cut down for %d more ticks.", obj.Name, st.DepleteTimer)) + sess.WriteLine(fmt.Sprintf("The %s has been cut down for %d more ticks.", obj.Name, int(st.DepleteTimer))) } else { - sess.WriteLine(fmt.Sprintf("The %s is depleted for %d more ticks.", obj.Name, st.DepleteTimer)) + sess.WriteLine(fmt.Sprintf("The %s is depleted for %d more ticks.", obj.Name, int(st.DepleteTimer))) } return } @@ -43,7 +43,7 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje wait := cfg.BaseWait if len(cfg.Tools) > 0 { - toolSpeed := -1 + var toolSpeed float64 = -1 if itemID, ok := p.Equipment[object.SlotMainHand]; ok { def, err := g.ItemStore.Load(itemID) if err == nil { @@ -107,6 +107,39 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje return } + toolName := "" + if itemID, ok := p.Equipment[object.SlotMainHand]; ok { + if def, err := g.ItemStore.Load(itemID); err == nil { + for _, t := range cfg.Tools { + if def.ToolType == t { + toolName = def.Name + break + } + } + } + } + if toolName == "" { + for _, slot := range p.Inventory { + if slot == nil || slot.Quantity <= 0 { + continue + } + def, err := g.ItemStore.Load(slot.ItemID) + if err != nil { + continue + } + for _, t := range cfg.Tools { + if def.ToolType == t { + toolName = def.Name + break + } + } + if toolName != "" { + break + } + } + } + p.ActionState = &ActionState{Type: ActionGathering, TargetName: obj.Name, ToolName: toolName} + data := map[string]any{ "behavior_id": obj.BehaviorID, "instance_key": g.World.ObjStateKey(st.RoomID, st.DefID, st.Index), @@ -135,7 +168,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { } step := p.Action.Data["step"].(int) - wait := p.Action.Data["effective_wait"].(int) + wait := p.Action.Data["effective_wait"].(float64) instanceKey := p.Action.Data["instance_key"].(string) _, shared := p.Action.Data["deplete_timer"] @@ -153,21 +186,21 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { } sess.WriteLine(fmt.Sprintf("\n%s", cfg.GatherMsg)) p.Action.Data["step"] = 1 - p.Action.WaitLeft = wait + p.Action.WaitLeft = g.computeTicks(wait) return } if step == 2 { st := g.World.GetObjStateByKey(instanceKey) if st != nil && st.Depleted { - p.Action.WaitLeft = 3 + p.Action.WaitLeft = g.computeTicks(3) return } if cfg.RespawnMsg != "" { sess.WriteLine(fmt.Sprintf("\n%s", cfg.RespawnMsg)) } p.Action.Data["step"] = 0 - p.Action.WaitLeft = wait + p.Action.WaitLeft = g.computeTicks(wait) return } @@ -231,23 +264,30 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { g.checkBirdNest(sess, p, cfg, freeSlot) - if shared { - st := g.World.GetObjStateByKey(instanceKey) - if st != nil && st.SharedTimer <= 0 { - g.depleteSharedTree(sess, p, st, cfg, instanceKey) - return - } + if shared { + st := g.World.GetObjStateByKey(instanceKey) + if st != nil && st.SharedTimer <= 0 { + g.pendingDepletions = append(g.pendingDepletions, pendingDepletion{ + instanceKey: instanceKey, + behaviorID: behaviorID, + targetName: p.Action.TargetName, + playerNames: []string{p.Name}, + }) + p.Action.Data["step"] = 2 + p.Action.WaitLeft = g.computeTicks(1) + return } + } if !shared && drop.Depletes { - delay := cfg.RespawnTimer + delay := g.computeTicks(cfg.RespawnTimer) if delay <= 0 { delay = 10 } st := g.World.GetObjStateByKey(instanceKey) if st != nil { st.Depleted = true - st.DepleteTimer = delay + st.DepleteTimer = float64(delay) } for _, other := range g.Hub.PlayersInRoom(p.RoomID) { @@ -265,7 +305,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { } p.Action.Data["step"] = 2 - p.Action.WaitLeft = 1 + p.Action.WaitLeft = g.computeTicks(1) return } @@ -275,7 +315,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { return } p.Action.Data["step"] = 0 - p.Action.WaitLeft = wait + p.Action.WaitLeft = g.computeTicks(wait) return } } @@ -287,7 +327,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { return } p.Action.Data["step"] = 0 - p.Action.WaitLeft = wait + p.Action.WaitLeft = g.computeTicks(wait) } func filterDropsByLevel(drops []action.DropEntry, level int) []action.DropEntry { @@ -303,34 +343,68 @@ func filterDropsByLevel(drops []action.DropEntry, level int) []action.DropEntry return eligible } -func (g *Game) depleteSharedTree(sess *net.Session, p *player.Player, st *world.ObjState, cfg *action.GatherConfig, instanceKey string) { +func (g *Game) depleteSharedTree(st *world.ObjState, cfg *action.GatherConfig, targetName string, playerNames []string) { st.Depleted = true - st.DepleteTimer = cfg.RespawnTimer + st.DepleteTimer = float64(g.computeTicks(cfg.RespawnTimer)) st.SharedTimer = 0 msg := cfg.ExhaustedMessage if msg == "" { - msg = fmt.Sprintf("The %s falls to the ground!", p.Action.TargetName) + msg = fmt.Sprintf("The %s falls to the ground!", targetName) } - sess.WriteLine(fmt.Sprintf("\n%s", msg)) - - for _, other := range g.Hub.PlayersInRoom(p.RoomID) { - if other == sess { + for _, sess := range g.Hub.PlayersInRoom(st.RoomID) { + op, ok := sess.Player.(*player.Player) + if !ok || op.Action == nil || op.Action.Type != "gather" { continue } - op, ok := other.Player.(*player.Player) - if !ok || op.Action == nil || op.Action.Type != "gather" { + if op.Action.Data["instance_key"] != g.World.ObjStateKey(st.RoomID, st.DefID, st.Index) { + continue + } + isDepleter := false + for _, name := range playerNames { + if op.Name == name { + isDepleter = true + break + } + } + if isDepleter { + sess.WriteLine(fmt.Sprintf("\n%s", msg)) continue } - if op.Action.Data["instance_key"] == instanceKey { - other.WriteLine(fmt.Sprintf("\n%s", msg)) - g.CancelAction(op) + sess.WriteLine(fmt.Sprintf("\n%s", msg)) + g.CancelAction(op) + } +} + +func (g *Game) flushPendingDepletions() { + if len(g.pendingDepletions) == 0 { + return + } + + merged := make(map[string]*pendingDepletion) + for _, pd := range g.pendingDepletions { + if existing, ok := merged[pd.instanceKey]; ok { + existing.playerNames = append(existing.playerNames, pd.playerNames...) + } else { + copy := pd + merged[pd.instanceKey] = © + } + } + + for _, pd := range merged { + st := g.World.GetObjStateByKey(pd.instanceKey) + if st == nil || st.Depleted { + continue + } + cfg, err := g.BehaviorStore.LoadGather(pd.behaviorID) + if err != nil { + continue } + g.depleteSharedTree(st, cfg, pd.targetName, pd.playerNames) } - p.Action.Data["step"] = 2 - p.Action.WaitLeft = 1 + g.pendingDepletions = nil } func (g *Game) checkBirdNest(sess *net.Session, p *player.Player, cfg *action.GatherConfig, currentDropSlot int) { diff --git a/internal/game/action_search.go b/internal/game/action_search.go new file mode 100644 index 0000000..07bae44 --- /dev/null +++ b/internal/game/action_search.go @@ -0,0 +1,122 @@ +package game + +import ( + "fmt" + + "thirdcollapse/internal/action" + "thirdcollapse/internal/net" + "thirdcollapse/internal/player" +) + +func (g *Game) startSearch(sess *net.Session, p *player.Player, itemID string, slotIdx int) { + def, err := g.ItemStore.Load(itemID) + if err != nil || def.SearchTable == "" { + sess.WriteLine("You can't search that.") + return + } + + ticks := def.SearchTicks + if ticks <= 0 { + ticks = 3 + } + + p.Action = &action.Action{ + Type: "search", + TargetID: itemID, + TargetName: def.Name, + WaitLeft: g.computeTicks(ticks), + Data: map[string]any{ + "item_id": itemID, + "slot_idx": slotIdx, + "started": false, + }, + } + + p.ActionState = &ActionState{Type: ActionSearching, TargetName: def.Name} +} + +func (g *Game) advanceSearch(sess *net.Session, p *player.Player) { + data := p.Action.Data + itemID := data["item_id"].(string) + slotIdx := data["slot_idx"].(int) + started := data["started"].(bool) + + def, err := g.ItemStore.Load(itemID) + if err != nil { + sess.WriteLine("Something went wrong.") + g.CancelAction(p) + return + } + + if !started { + msg := def.SearchMessage + if msg == "" { + msg = fmt.Sprintf("digging through the %s", def.Name) + } + sess.WriteLine(fmt.Sprintf("You begin %s.", msg)) + data["started"] = true + return + } + + slot := p.InvSlot(slotIdx) + if slot == nil || slot.ItemID != itemID || slot.Quantity <= 0 { + sess.WriteLine("The item is gone.") + g.CancelAction(p) + return + } + + if slot.Quantity > 1 { + slot.Quantity-- + } else { + p.SetInvSlot(slotIdx, nil) + } + + dt, err := g.BehaviorStore.LoadDropTable(def.SearchTable) + if err == nil && len(dt.Drops) > 0 { + drop := g.BehaviorStore.ResolveDrop(dt.Drops) + if drop != nil && drop.ItemID != "" { + g.giveSearchLoot(sess, p, drop) + } + } + + if def.SearchMiscTable != "" { + dt2, err := g.BehaviorStore.LoadDropTable(def.SearchMiscTable) + if err == nil && len(dt2.Drops) > 0 { + drop := g.BehaviorStore.ResolveDrop(dt2.Drops) + if drop != nil && drop.ItemID != "" { + g.giveSearchLoot(sess, p, drop) + } + } + } + + g.AccountStore.SaveCharacter(p) + g.CancelAction(p) +} + +func (g *Game) giveSearchLoot(sess *net.Session, p *player.Player, drop *action.DropEntry) { + qty := drop.Quantity + if qty <= 0 { + qty = 1 + } + + name := drop.ItemID + if def, err := g.ItemStore.Load(drop.ItemID); err == nil { + name = def.Name + } + + if drop.ItemID == "credits" { + p.Credits += qty + sess.WriteLine(fmt.Sprintf("You find %d credits.", qty)) + return + } + + freeSlot := p.FirstFreeSlot() + if freeSlot == -1 { + g.World.AddGroundItem(p.RoomID, drop.ItemID, qty) + sess.WriteLine(fmt.Sprintf("You find %s. It falls to the ground.", name)) + return + } + + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) + sess.WriteLine(fmt.Sprintf("You find %s.", name)) +} diff --git a/internal/game/action_state.go b/internal/game/action_state.go new file mode 100644 index 0000000..fba5319 --- /dev/null +++ b/internal/game/action_state.go @@ -0,0 +1,61 @@ +package game + +import "fmt" + +type ActionType string + +const ( + ActionIdle ActionType = "" + ActionGathering ActionType = "gathering" + ActionCombating ActionType = "combating" + ActionMoving ActionType = "moving" + ActionUsing ActionType = "using" + ActionTalking ActionType = "talking" + ActionToggling ActionType = "toggling" + ActionBurning ActionType = "burning" + ActionStoking ActionType = "stoking" + ActionPickingUp ActionType = "picking_up" + ActionDropping ActionType = "dropping" + ActionSearching ActionType = "searching" + ActionResting ActionType = "resting" +) + +type ActionState struct { + Type ActionType + TargetName string + ToolName string + Direction string +} + +func (a *ActionState) Description() string { + if a == nil || a.Type == ActionIdle { + return "" + } + switch a.Type { + case ActionGathering: + return "mining a " + a.TargetName + case ActionCombating: + return "fighting a " + a.TargetName + case ActionMoving: + return "walking in from the " + a.Direction + case ActionUsing: + return "using a " + a.TargetName + case ActionTalking: + return "talking to " + a.TargetName + case ActionToggling: + return fmt.Sprintf("pulling a %s", a.TargetName) + case ActionBurning: + return "trying to start a fire" + case ActionStoking: + return "tending to a fire" + case ActionPickingUp: + return "picking up " + a.TargetName + case ActionDropping: + return "dropping " + a.TargetName + case ActionSearching: + return "digging through a " + a.TargetName + case ActionResting: + return "resting" + } + return "" +} diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go index 4980c5a..8aa95be 100644 --- a/internal/game/action_talk.go +++ b/internal/game/action_talk.go @@ -19,6 +19,7 @@ func (g *Game) startMobTalk(sess *net.Session, p *player.Player, mob *world.MobI } if node, ok := cfg.Nodes["start"]; ok { + p.ActionState = &ActionState{Type: ActionTalking, TargetName: mob.Name} p.Action = &action.Action{ Type: "talk", TargetID: mob.DefID, @@ -39,6 +40,7 @@ func (g *Game) startTalk(sess *net.Session, p *player.Player, obj *object.Object } if node, ok := cfg.Nodes["start"]; ok { + p.ActionState = &ActionState{Type: ActionTalking, TargetName: obj.Name} p.Action = &action.Action{ Type: "talk", TargetID: obj.ID, diff --git a/internal/game/action_toggle.go b/internal/game/action_toggle.go index 3818d0d..f20d03c 100644 --- a/internal/game/action_toggle.go +++ b/internal/game/action_toggle.go @@ -34,6 +34,8 @@ func (g *Game) startToggle(sess *net.Session, p *player.Player, obj *object.Obje g.WorldFlags[flag] = val } + p.ActionState = &ActionState{Type: ActionToggling, TargetName: obj.Name} + sess.WriteLine(fmt.Sprintf("\n%s", cfg.Message)) if g.Hub != nil { diff --git a/internal/game/action_use.go b/internal/game/action_use.go index c47492f..8987ee0 100644 --- a/internal/game/action_use.go +++ b/internal/game/action_use.go @@ -33,11 +33,13 @@ func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectD return } + p.ActionState = &ActionState{Type: ActionUsing, TargetName: obj.Name} + p.Action = &action.Action{ Type: "use", TargetID: obj.ID, TargetName: obj.Name, - WaitLeft: 1, + WaitLeft: g.computeTicks(1), Data: map[string]any{"step": 0, "behavior_id": obj.BehaviorID}, } @@ -76,7 +78,7 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) { if cfg.FailMsg != "" { sess.WriteLine(cfg.FailMsg) } - p.Action.WaitLeft = cfg.Wait + p.Action.WaitLeft = g.computeTicks(cfg.Wait) return } } @@ -117,5 +119,5 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) { return } - p.Action.WaitLeft = cfg.Wait + p.Action.WaitLeft = g.computeTicks(cfg.Wait) } diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index 2f79c16..ef80189 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -131,8 +131,9 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")" } sess.WriteLine(fmt.Sprintf("\nYou attack %s!%s", mobDisplayName(mob, true), styleStr)) + p.ActionState = &ActionState{Type: ActionCombating, TargetName: mob.Name} - g.Ticks.Subscribe(playerSpeed, func() bool { + g.Ticks.Subscribe(g.computeTicks(playerSpeed), func() bool { cs := combat.GetCombat(p.Name) if cs == nil || !cs.Active { return false @@ -150,7 +151,7 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn return true }) - g.Ticks.Subscribe(mob.Speed, func() bool { + g.Ticks.Subscribe(g.computeTicks(mob.Speed), func() bool { cs := combat.GetCombat(p.Name) if cs == nil || !cs.Active { return false @@ -277,6 +278,7 @@ func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInst func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) { g.combatPadWidth = 0 combat.LeaveCombat(p.Name) + p.ActionState = nil if p.HP <= 0 { sess.WriteLine("\nOh dear, you are dead!") @@ -340,9 +342,9 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst } } - respawnTicks := mob.RespawnTicks + respawnTicks := g.computeTicks(mob.RespawnTicks) if respawnTicks <= 0 { - respawnTicks = 30 + respawnTicks = g.computeTicks(30) } instanceID := mob.InstanceID g.Ticks.Subscribe(respawnTicks, func() bool { @@ -467,12 +469,12 @@ func (g *Game) respawnMob(instanceID string) { } } -func (g *Game) playerWeaponSpeed(p *player.Player) int { +func (g *Game) playerWeaponSpeed(p *player.Player) float64 { if itemID, ok := p.Equipment[object.SlotMainHand]; ok { def, err := g.ItemStore.Load(itemID) if err == nil && def.Speed > 0 { return def.Speed } } - return 5 + return 5.0 } diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go index 149b86b..dc506f0 100644 --- a/internal/game/cmd_drop.go +++ b/internal/game/cmd_drop.go @@ -11,6 +11,7 @@ import ( func (g *Game) doDropAll(sess *net.Session) { p := sess.Player.(*player.Player) g.CancelAction(p) + p.ActionState = &ActionState{Type: ActionDropping, TargetName: "inventory"} count := 0 for _, slot := range p.Inventory { @@ -52,6 +53,7 @@ func (g *Game) doDropAllNamed(sess *net.Session, input string) { if def != nil { name = def.Name } + p.ActionState = &ActionState{Type: ActionDropping, TargetName: name} if def != nil && def.Stackable { slot := p.InvSlot(matches[0].Slot) @@ -113,6 +115,7 @@ func (g *Game) doDrop(sess *net.Session, input string) { if def != nil { name = def.Name } + p.ActionState = &ActionState{Type: ActionDropping, TargetName: name} if qty == 0 { if def != nil && def.Stackable { diff --git a/internal/game/cmd_get.go b/internal/game/cmd_get.go index 78a9621..7ec183f 100644 --- a/internal/game/cmd_get.go +++ b/internal/game/cmd_get.go @@ -39,10 +39,16 @@ func (g *Game) doGet(sess *net.Session, input string) { if itemID == "credits" { g.pickupCredits(sess, p, itemID) + p.ActionState = &ActionState{Type: ActionPickingUp, TargetName: "credits"} return } def, _ := g.ItemStore.Load(itemID) + name := itemID + if def != nil { + name = def.Name + } + p.ActionState = &ActionState{Type: ActionPickingUp, TargetName: name} available := 0 if gqty, ok := ground[itemID]; ok { diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index a840579..3b81f80 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -25,8 +25,12 @@ func (g *Game) doLook(sess *net.Session) { room.Name, ) - if p.OptionBool("tinymap") && g.MapWidth > 0 { - descLines := wrapText(room.Description, g.MapWidth) + mapWidth := p.OptionInt("max_room_description_width") + if mapWidth <= 0 { + mapWidth = 70 + } + if p.OptionBool("tinymap") && mapWidth > 0 { + descLines := wrapText(room.Description, mapWidth) mapLines := buildTinyMap(g, p.RoomID) if len(mapLines) == 7 { g.writeLookSideBySide(sess, p, descLines, mapLines) @@ -107,10 +111,10 @@ func (g *Game) doLook(sess *net.Session) { instances = append(instances, instInfo{ idx: i + 1, depleted: st.Depleted, - sharedMax: st.SharedMax, + sharedMax: int(st.SharedMax), sharedCur: st.SharedTimer, - respawnIn: st.DepleteTimer, - quality: st.Quality, + respawnIn: int(st.DepleteTimer), + quality: int(st.Quality), }) } multi := len(instances) > 1 @@ -302,8 +306,10 @@ func (g *Game) doLook(sess *net.Session) { } line += fmt.Sprintf(" (fighting %s)", name) } - } else if desc := playerActionDescription(op); desc != "" { - line += ", " + desc + } else if as, ok := op.ActionState.(*ActionState); ok && as != nil { + if desc := as.Description(); desc != "" { + line += ", " + desc + } } sess.WriteLine(line + ".") } @@ -380,15 +386,15 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { for _, ist := range instances { if ist.Depleted { if len(instances) > 1 { - sess.WriteLine(fmt.Sprintf(" %s %d: depleted, respawns in %d ticks.", def.Name, ist.Index+1, ist.DepleteTimer)) + sess.WriteLine(fmt.Sprintf(" %s %d: depleted, respawns in %d ticks.", def.Name, ist.Index+1, int(ist.DepleteTimer))) } else { - sess.WriteLine(fmt.Sprintf(" Depleted, respawns in %d ticks.", ist.DepleteTimer)) + sess.WriteLine(fmt.Sprintf(" Depleted, respawns in %d ticks.", int(ist.DepleteTimer))) } - } else if ist.SharedMax > 0 && ist.SharedTimer < ist.SharedMax { + } else if ist.SharedMax > 0 && float64(ist.SharedTimer) < ist.SharedMax { if len(instances) > 1 { - sess.WriteLine(fmt.Sprintf(" %s %d: despawn timer %d/%d.", def.Name, ist.Index+1, ist.SharedTimer, ist.SharedMax)) + sess.WriteLine(fmt.Sprintf(" %s %d: despawn timer %d/%.0f.", def.Name, ist.Index+1, ist.SharedTimer, ist.SharedMax)) } else { - sess.WriteLine(fmt.Sprintf(" Despawn timer: %d/%d ticks.", ist.SharedTimer, ist.SharedMax)) + sess.WriteLine(fmt.Sprintf(" Despawn timer: %d/%.0f ticks.", ist.SharedTimer, ist.SharedMax)) } } } @@ -396,7 +402,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { for _, ist := range instances { if ist.Quality > 0 { if qdesc, ok := def.Props["quality_description"].(string); ok && qdesc != "" { - qdesc = strings.ReplaceAll(qdesc, "{quality}", fmt.Sprintf("%d", ist.Quality)) + qdesc = strings.ReplaceAll(qdesc, "{quality}", fmt.Sprintf("%.0f", ist.Quality)) sess.WriteLine(fmt.Sprintf(" %s", qdesc)) } } @@ -568,7 +574,11 @@ func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, descLine if len(mapLines) > total { total = len(mapLines) } - leftMap := p.OptionBool("left-tinymap") + leftMap := p.OptionBool("left_tinymap") + mapWidth := p.OptionInt("max_room_description_width") + if mapWidth <= 0 { + mapWidth = 70 + } for i := 0; i < total; i++ { desc := "" if i < len(descLines) { @@ -581,7 +591,7 @@ func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, descLine if leftMap { sess.WriteLine(fmt.Sprintf("%s %s", mapLine, desc)) } else { - sess.WriteLine(fmt.Sprintf("%-*s %s", g.MapWidth, desc, mapLine)) + sess.WriteLine(fmt.Sprintf("%-*s %s", mapWidth, desc, mapLine)) } } } diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go index 9ef7a00..0b6571e 100644 --- a/internal/game/cmd_move.go +++ b/internal/game/cmd_move.go @@ -79,6 +79,7 @@ func (g *Game) doMove(sess *net.Session, dir string) { } sess.WriteLine(fmt.Sprintf("\nYou walk %s.", exitDir)) + p.ActionState = &ActionState{Type: ActionMoving, Direction: string(exitDir)} if p.OptionBool("description") { g.doLook(sess) } else { diff --git a/internal/game/cmd_queued.go b/internal/game/cmd_queued.go new file mode 100644 index 0000000..040ff31 --- /dev/null +++ b/internal/game/cmd_queued.go @@ -0,0 +1,66 @@ +package game + +import ( + "fmt" + "sort" + + "thirdcollapse/internal/net" + "thirdcollapse/internal/player" +) + +func (g *Game) doQueued(sess *net.Session) { + p := sess.Player.(*player.Player) + + freeCmds := g.freeQueue[p.Name] + activeCmd := g.activeQueue[p.Name] + + if len(freeCmds) == 0 && activeCmd == nil { + sess.WriteLine("\nNo actions queued.") + return + } + + sess.WriteLine("") + + if len(freeCmds) > 0 { + sess.WriteLine(" Queued free actions (will execute in order):") + for i, qc := range freeCmds { + cmd := qc.Command + if qc.Args != "" { + cmd += " " + qc.Args + } + sess.WriteLine(fmt.Sprintf(" %d) %s", i+1, cmd)) + } + } + + if activeCmd != nil { + if len(freeCmds) > 0 { + sess.WriteLine("") + } + cmd := activeCmd.Command + if activeCmd.Args != "" { + cmd += " " + activeCmd.Args + } + sess.WriteLine(fmt.Sprintf(" Queued active action (executes after free actions):")) + sess.WriteLine(fmt.Sprintf(" %s", cmd)) + } + + sess.WriteLine("") + sess.WriteLine(fmt.Sprintf(" All queued actions take effect on the next game tick (600ms).")) + sess.WriteLine(fmt.Sprintf(" Free actions stack. Only the most recent active action survives.")) + + var names []string + for name := range g.activeQueue { + names = append(names, name) + } + sort.Strings(names) + pos := 0 + for i, name := range names { + if name == p.Name { + pos = i + 1 + break + } + } + if pos > 0 && len(names) > 1 { + sess.WriteLine(fmt.Sprintf("\n Your active action will resolve as queue position #%d of %d.", pos, len(names))) + } +} diff --git a/internal/game/cmd_quit.go b/internal/game/cmd_quit.go index 51e1fee..90e7019 100644 --- a/internal/game/cmd_quit.go +++ b/internal/game/cmd_quit.go @@ -15,10 +15,11 @@ func (g *Game) doQuit(sess *net.Session) { } g.CancelAction(p) + p.ActionState = &ActionState{Type: ActionResting} g.cancelRest(p.Name) ticksLeft := 10 - id := g.Ticks.Subscribe(1, func() bool { + id := g.Ticks.Subscribe(g.computeTicks(1), func() bool { switch ticksLeft { case 10: sess.WriteLine("You sit down to rest...") diff --git a/internal/game/cmd_search.go b/internal/game/cmd_search.go index 341685e..d9a5b1e 100644 --- a/internal/game/cmd_search.go +++ b/internal/game/cmd_search.go @@ -10,6 +10,7 @@ import ( func (g *Game) doSearch(sess *net.Session, input string) { p := sess.Player.(*player.Player) + g.CancelAction(p) lower := strings.ToLower(strings.TrimSpace(input)) if lower == "" { @@ -23,65 +24,16 @@ func (g *Game) doSearch(sess *net.Session, input string) { return } - itemID := matches[0].ID - slotIdx := matches[0].Slot - - if itemID != "birds_nest" { - sess.WriteLine(fmt.Sprintf("You can't search %s.", matches[0].Name)) - return - } - - slot := p.InvSlot(slotIdx) - if slot == nil || slot.ItemID != "birds_nest" { - return - } - - if slot.Quantity > 1 { - slot.Quantity-- - } else { - p.SetInvSlot(slotIdx, nil) - } - - dt, err := g.BehaviorStore.LoadDropTable("birds_nest_drop") - if err != nil || len(dt.Drops) == 0 { - sess.WriteLine("You search the bird's nest but find nothing.") - g.AccountStore.SaveCharacter(p) - return - } - - drop := g.BehaviorStore.ResolveDrop(dt.Drops) - if drop == nil { - sess.WriteLine("You search the bird's nest but find nothing.") - g.AccountStore.SaveCharacter(p) - return - } - - qty := drop.Quantity - if qty <= 0 { - qty = 1 - } - - if drop.ItemID == "credits" { - p.Credits += qty - sess.WriteLine(fmt.Sprintf("You search the bird's nest and find %d credits.", qty)) - } else { - freeSlot := p.FirstFreeSlot() - if freeSlot == -1 { - g.World.AddGroundItem(p.RoomID, drop.ItemID, qty) - name := drop.ItemID - if def, err := g.ItemStore.Load(drop.ItemID); err == nil { - name = def.Name - } - sess.WriteLine(fmt.Sprintf("You search the bird's nest and find %s. It falls to the ground.", name)) - } else { - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) - name := drop.ItemID - if def, err := g.ItemStore.Load(drop.ItemID); err == nil { - name = def.Name - } - sess.WriteLine(fmt.Sprintf("You search the bird's nest and find %s.", name)) + unique := uniqueItemNames(matches) + if len(unique) > 1 { + sess.WriteLine("Which one?") + for _, name := range unique { + sess.WriteLine(fmt.Sprintf(" - %s", name)) } + return } - g.AccountStore.SaveCharacter(p) + itemID := matches[0].ID + slotIdx := matches[0].Slot + g.startSearch(sess, p, itemID, slotIdx) } diff --git a/internal/game/doc.go b/internal/game/doc.go index 97814e9..e276994 100644 --- a/internal/game/doc.go +++ b/internal/game/doc.go @@ -15,8 +15,9 @@ // Actions that take time (gather, use, burn, stoke) tick via // AdvanceActions, called each 600ms tick. // -// Tick handlers: DisconnectTick, RegenTick, WanderTick, WoodcuttingTick, -// and FireTick run each game tick to advance world simulation. +// Tick handlers: ProcessQueuedCommands, DisconnectTick, RegenTick, +// WanderTick, SharedDepletionTick, and FireTick run each game tick to +// advance world simulation. // // File organization: // cmd_*.go — player command handlers (doLook, doMove, doGet, etc.) diff --git a/internal/game/game.go b/internal/game/game.go index cecde2a..37a1f3f 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -2,8 +2,10 @@ package game import ( "fmt" + "sort" "strings" "sync" + "time" "thirdcollapse/internal/action" "thirdcollapse/internal/engine" @@ -13,6 +15,21 @@ import ( "thirdcollapse/internal/world" ) +type CommandClass int + +const ( + ClassInstant CommandClass = iota + ClassFree + ClassActive +) + +type QueuedCommand struct { + Session *net.Session + Command string + Args string + Timestamp time.Time +} + type Game struct { World *world.World ObjectStore *object.ObjectStore @@ -28,22 +45,28 @@ type Game struct { charsMu sync.Mutex loggedInChars map[string]*net.Session combatPadWidth int - MapWidth int + GameSpeed float64 + freeQueue map[string][]QueuedCommand + activeQueue map[string]*QueuedCommand + pendingDepletions []pendingDepletion } func New(dataDir string) *Game { return &Game{ - World: world.New(dataDir), - ObjectStore: object.NewObjectStore(dataDir), - ItemStore: object.NewItemStore(dataDir), - AccountStore: player.NewAccountStore(dataDir), - MobStore: world.NewMobStore(dataDir), - BehaviorStore: action.NewStore(dataDir), - Ticks: engine.New(), - WorldFlags: make(map[string]any), - dataDir: dataDir, - restTimers: make(map[string]uint64), - loggedInChars: make(map[string]*net.Session), + World: world.New(dataDir), + ObjectStore: object.NewObjectStore(dataDir), + ItemStore: object.NewItemStore(dataDir), + AccountStore: player.NewAccountStore(dataDir), + MobStore: world.NewMobStore(dataDir), + BehaviorStore: action.NewStore(dataDir), + Ticks: engine.New(), + WorldFlags: make(map[string]any), + dataDir: dataDir, + restTimers: make(map[string]uint64), + loggedInChars: make(map[string]*net.Session), + freeQueue: make(map[string][]QueuedCommand), + activeQueue: make(map[string]*QueuedCommand), + pendingDepletions: nil, } } @@ -94,17 +117,26 @@ func (g *Game) HandleSession(sess *net.Session, input string) { } } +func classifyCommand(cmd string) CommandClass { + switch cmd { + case "say", "score", "sc", "inventory", "i", "inv", + "equipment", "eq", "look", "l", "exits", "help", + "map", "option", "options", "alias", "unalias", + "description", "desc", "queued": + return ClassInstant + case "wear", "wield", "remove", "unwear", "unwield", "style": + return ClassFree + default: + return ClassActive + } +} + func (g *Game) handleGameCommand(sess *net.Session, input string) { if input == "" { sess.Write("> ") return } - p, _ := sess.Player.(*player.Player) - if p != nil { - g.cancelRest(p.Name) - } - if sess.Account != nil && sess.Account.Aliases != nil { firstSpace := strings.Index(input, " ") var firstWord, rest string @@ -125,7 +157,48 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { parts := strings.Fields(strings.ToLower(input)) cmd := parts[0] - args := parts[1:] + class := classifyCommand(cmd) + + if class == ClassInstant { + g.executeCommand(sess, cmd, parts[1:], input) + sess.Write("\r\n> ") + return + } + + p, _ := sess.Player.(*player.Player) + if p == nil { + return + } + + if class == ClassFree { + g.freeQueue[p.Name] = append(g.freeQueue[p.Name], QueuedCommand{ + Session: sess, + Command: cmd, + Args: strings.Join(parts[1:], " "), + Timestamp: time.Now(), + }) + if !p.OptionBool("queue_actions_silently") { + sess.WriteLine(fmt.Sprintf("\nYou prepare to %s.", cmd)) + } + sess.Write("\r\n> ") + return + } + + g.activeQueue[p.Name] = &QueuedCommand{ + Session: sess, + Command: cmd, + Args: strings.Join(parts[1:], " "), + Timestamp: time.Now(), + } + g.cancelRest(p.Name) + if !p.OptionBool("queue_actions_silently") { + sess.WriteLine(fmt.Sprintf("\nYou prepare to %s.", cmd)) + } + sess.Write("\r\n> ") +} + +func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawInput string) { + p, _ := sess.Player.(*player.Player) switch cmd { case "get", "take", "grab", "pick": @@ -153,6 +226,9 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { g.doDrop(sess, strings.Join(args, " ")) } case "attack", "kill": + if p == nil { + return + } if len(args) == 0 { target := g.resolveDefaultMob(p.RoomID) if target == "" { @@ -183,9 +259,9 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { if len(args) == 0 { sess.WriteLine("Say what?") } else { - msgStart := strings.Index(strings.ToLower(input), "say ") + 4 - if msgStart >= 4 && msgStart < len(input) { - g.doSay(sess, input[msgStart:]) + msgStart := strings.Index(strings.ToLower(rawInput), "say ") + 4 + if msgStart >= 4 && msgStart < len(rawInput) { + g.doSay(sess, rawInput[msgStart:]) } else { g.doSay(sess, strings.Join(args, " ")) } @@ -207,7 +283,8 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { g.doExits(sess) case "map": g.doMap(sess) - + case "queued": + g.doQueued(sess) case "help": if len(args) == 0 { g.doHelp(sess, "") @@ -215,6 +292,10 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { g.doHelp(sess, strings.Join(args, " ")) } case "mine", "chop", "fish", "cut", "use", "pull", "push": + if p == nil { + return + } + g.CancelAction(p) if len(args) == 0 { target := g.resolveDefaultTarget(p.RoomID, cmd) if target == "" { @@ -228,6 +309,10 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { return } case "talk", "speak", "ask": + if p == nil { + return + } + g.CancelAction(p) if len(args) == 0 { sess.WriteLine("Talk to whom?") } else { @@ -235,9 +320,17 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { return } case "burn": + if p == nil { + return + } + g.CancelAction(p) g.doBurn(sess, p, strings.Join(args, " ")) return case "stoke": + if p == nil { + return + } + g.CancelAction(p) g.doStoke(sess, p, strings.Join(args, " ")) return case "alias": @@ -247,6 +340,10 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { g.doUnalias(sess, args) return case "search": + if p == nil { + return + } + g.CancelAction(p) if len(args) == 0 { sess.WriteLine("Search what?") } else { @@ -254,14 +351,86 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { } return case "wear", "wield": + if p == nil { + return + } g.doWear(sess, strings.Join(args, " ")) return case "remove", "unwear", "unwield": + if p == nil { + return + } g.doRemove(sess, strings.Join(args, " ")) return default: sess.WriteLine("Unknown command.") } +} - sess.Write("\r\n> ") +func (g *Game) ProcessQueuedCommands() { + if g.Hub == nil { + return + } + + for _, sess := range g.Hub.AllSessions() { + p, ok := sess.Player.(*player.Player) + if !ok || p == nil || p.ActionState == nil { + continue + } + as, ok := p.ActionState.(*ActionState) + if !ok { + continue + } + switch as.Type { + case ActionGathering, ActionCombating, ActionUsing, ActionTalking, + ActionToggling, ActionBurning, ActionStoking, ActionResting: + default: + p.ActionState = nil + } + } + + for _, sess := range g.Hub.AllSessions() { + p, ok := sess.Player.(*player.Player) + if !ok || p == nil { + continue + } + cmds := g.freeQueue[p.Name] + for _, qc := range cmds { + g.cancelRest(p.Name) + parts := strings.Fields(qc.Command + " " + qc.Args) + if len(parts) > 0 { + g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args) + } + } + delete(g.freeQueue, p.Name) + } + + var actives []QueuedCommand + for _, qc := range g.activeQueue { + actives = append(actives, *qc) + } + sort.Slice(actives, func(i, j int) bool { + return actives[i].Timestamp.Before(actives[j].Timestamp) + }) + + for _, qc := range actives { + parts := strings.Fields(qc.Command + " " + qc.Args) + if len(parts) > 0 { + g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args) + } + } + g.activeQueue = make(map[string]*QueuedCommand) + + g.flushPendingDepletions() +} + +type pendingDepletion struct { + instanceKey string + behaviorID string + targetName string + playerNames []string +} + +func (g *Game) computeTicks(base float64) int { + return engine.FractionalTicks(base, g.GameSpeed) } diff --git a/internal/game/map_test.go b/internal/game/map_test.go index adba595..3453117 100644 --- a/internal/game/map_test.go +++ b/internal/game/map_test.go @@ -9,8 +9,7 @@ import ( func TestBuildTinyMap(t *testing.T) { g := &Game{ - World: world.New("../../data"), - MapWidth: 70, + World: world.New("../../data"), } tests := []struct { @@ -95,8 +94,7 @@ func TestWrapText(t *testing.T) { func TestBuildFullMap(t *testing.T) { g := &Game{ - World: world.New("../../data"), - MapWidth: 70, + World: world.New("../../data"), } tests := []struct { diff --git a/internal/game/tick.go b/internal/game/tick.go index 3c3f1d3..4d037ec 100644 --- a/internal/game/tick.go +++ b/internal/game/tick.go @@ -82,7 +82,7 @@ func (g *Game) WanderTick() { continue } inst.WanderTickCounter++ - if inst.WanderTickCounter < inst.WanderInterval { + if float64(inst.WanderTickCounter) < inst.WanderInterval { continue } inst.WanderTickCounter = 0 @@ -144,40 +144,36 @@ func (g *Game) WanderTick() { } } -func (g *Game) WoodcuttingTick() { +func (g *Game) SharedDepletionTick() { + activeKeys := make(map[string]bool) + for _, sess := range g.Hub.AllSessions() { + p, ok := sess.Player.(*player.Player) + if !ok || p == nil || p.Action == nil || p.Action.Type != "gather" { + continue + } + if key, ok := p.Action.Data["instance_key"].(string); ok { + activeKeys[key] = true + } + } + all := g.World.AllSharedObjStates() for _, st := range all { if st.Depleted { continue } key := g.World.ObjStateKey(st.RoomID, st.DefID, st.Index) - choppers := g.countChoppers(st.RoomID, key) - if choppers > 0 { + if activeKeys[key] { if st.SharedTimer > 0 { st.SharedTimer-- } } else { - if st.SharedTimer < st.SharedMax { + if st.SharedTimer < int(st.SharedMax) { st.SharedTimer++ } } } } -func (g *Game) countChoppers(roomID int, instanceKey string) int { - count := 0 - for _, sess := range g.Hub.PlayersInRoom(roomID) { - p, ok := sess.Player.(*player.Player) - if !ok || p.Action == nil || p.Action.Type != "gather" { - continue - } - if key, ok := p.Action.Data["instance_key"].(string); ok && key == instanceKey { - count++ - } - } - return count -} - func (g *Game) legalMobExits(inst *world.MobInstance) []int { room, err := g.World.LoadRoom(inst.RoomID) if err != nil || len(room.Exits) == 0 { diff --git a/internal/game/utils.go b/internal/game/utils.go index e80de24..729ea97 100644 --- a/internal/game/utils.go +++ b/internal/game/utils.go @@ -79,29 +79,6 @@ func formatPickupList(sess *net.Session, picked []string) { } } -func playerActionDescription(p *player.Player) string { - if p.Action == nil { - return "" - } - target := p.Action.TargetName - if idx, ok := p.Action.Data["instance_idx"]; ok { - target += fmt.Sprintf(" [%d]", idx) - } - switch p.Action.Type { - case "gather": - return "mining a " + target - case "talk": - return "talking to " + target - case "use": - return "using a " + target - case "burn": - return "trying to start a fire" - case "stoke": - return "tending to a fire" - } - return "" -} - func (g *Game) newInventorySlot(itemID string, qty int) *player.InventorySlot { slot := &player.InventorySlot{ItemID: itemID, Quantity: qty} if def, err := g.ItemStore.Load(itemID); err == nil && def.MaxQuality > 0 { |
