aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/action/behavior.go4
-rw-r--r--internal/action/doc.go11
-rw-r--r--internal/combat/doc.go11
-rw-r--r--internal/config/doc.go10
-rw-r--r--internal/engine/doc.go11
-rw-r--r--internal/game/action_gather.go12
-rw-r--r--internal/game/action_talk.go4
-rw-r--r--internal/game/action_use.go3
-rw-r--r--internal/game/cmd_drop.go2
-rw-r--r--internal/game/cmd_get.go22
-rw-r--r--internal/game/cmd_look.go6
-rw-r--r--internal/game/cmd_map.go103
-rw-r--r--internal/game/cmd_move.go8
-rw-r--r--internal/game/cmd_wear.go2
-rw-r--r--internal/game/doc.go29
-rw-r--r--internal/game/game.go2
-rw-r--r--internal/game/login_account.go (renamed from internal/game/login.go)226
-rw-r--r--internal/game/login_char.go236
-rw-r--r--internal/game/map.go102
-rw-r--r--internal/game/types.go29
-rw-r--r--internal/game/utils.go35
-rw-r--r--internal/net/doc.go15
-rw-r--r--internal/object/doc.go14
-rw-r--r--internal/object/item.go31
-rw-r--r--internal/player/doc.go14
-rw-r--r--internal/player/validate.go2
-rw-r--r--internal/world/doc.go18
-rw-r--r--internal/world/world.go18
28 files changed, 546 insertions, 434 deletions
diff --git a/internal/action/behavior.go b/internal/action/behavior.go
index 7abe0ae..e0b8cdf 100644
--- a/internal/action/behavior.go
+++ b/internal/action/behavior.go
@@ -13,10 +13,10 @@ type GatherConfig struct {
ExhaustedMessage string `yaml:"exhausted_message"`
FailMsg string `yaml:"fail_message"`
Drops []DropEntry `yaml:"drops"`
- DepleteDelay int `yaml:"deplete_delay"`
+ RespawnTimer int `yaml:"respawn_timer"`
RespawnMsg string `yaml:"respawn_message"`
RespawnBroadcast string `yaml:"respawn_broadcast"`
- SharedDeplete int `yaml:"shared_deplete"`
+ DepleteTimer int `yaml:"deplete_timer"`
NestChance int `yaml:"nest_chance"`
}
diff --git a/internal/action/doc.go b/internal/action/doc.go
new file mode 100644
index 0000000..c1a898d
--- /dev/null
+++ b/internal/action/doc.go
@@ -0,0 +1,11 @@
+// Package action defines the behavior type system, action state tracking,
+// and drop table resolution for the game world.
+//
+// Behaviors are YAML-driven and come in four types: gather, talk, use, and toggle.
+// This package provides configuration structs for each type, the Action struct
+// that tracks per-player action state (type, target, tick countdown), and the
+// Store for loading behaviors and resolving weighted drop tables.
+//
+// The SuccessChance function implements the OSRS-style skill check formula.
+// Drop table resolution supports nested sub-tables via the ResolveDrop method.
+package action
diff --git a/internal/combat/doc.go b/internal/combat/doc.go
new file mode 100644
index 0000000..758be47
--- /dev/null
+++ b/internal/combat/doc.go
@@ -0,0 +1,11 @@
+// Package combat implements OSRS-style combat formulas and per-player
+// combat state tracking.
+//
+// Combat uses attack/defense roll formulas, style bonuses, and max-hit
+// calculations. The State type tracks active combat between a player and
+// a mob instance, with a lock timer preventing immediate disengagement.
+//
+// Combat state is managed through package-level maps protected by a mutex.
+// TickCombat decrements lock timers each tick. EnterCombat and LeaveCombat
+// manage the relationship between player names and mob instance IDs.
+package combat
diff --git a/internal/config/doc.go b/internal/config/doc.go
new file mode 100644
index 0000000..f35aad3
--- /dev/null
+++ b/internal/config/doc.go
@@ -0,0 +1,10 @@
+// Package config loads YAML server configuration and provides sensible
+// defaults when no config file is present.
+//
+// Configuration covers three listener types: telnet (raw TCP), HTTP + WebSocket,
+// and HTTPS + WebSocket. Each has an enabled flag and port. The game section
+// controls display settings like max terminal width.
+//
+// Call Load(path) to read a YAML file merged with defaults, or Default()
+// for a configuration suitable for running with telnet on :4000.
+package config
diff --git a/internal/engine/doc.go b/internal/engine/doc.go
new file mode 100644
index 0000000..0ce25bd
--- /dev/null
+++ b/internal/engine/doc.go
@@ -0,0 +1,11 @@
+// Package engine provides a tick scheduler that drives all time-based
+// world simulation.
+//
+// The Engine runs on a 600ms interval. Subscribers register callbacks with
+// a tick interval (1 = every tick, N = every Nth tick). Callbacks return
+// true to remain subscribed or false to auto-unsubscribe.
+//
+// The main game loop subscribes a single callback that advances combat,
+// regeneration, wandering, woodcutting depletion, fire decay, and player
+// actions all on the same tick beat.
+package engine
diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go
index 7e53e87..201a619 100644
--- a/internal/game/action_gather.go
+++ b/internal/game/action_gather.go
@@ -32,7 +32,7 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje
msg += fmt.Sprintf(" (%d ticks to respawn)", st.DepleteTimer)
}
sess.WriteLine(msg)
- } else if cfg.SharedDeplete > 0 {
+ } else if cfg.DepleteTimer > 0 {
sess.WriteLine(fmt.Sprintf("The %s has been cut down for %d more ticks.", obj.Name, st.DepleteTimer))
} else {
sess.WriteLine(fmt.Sprintf("The %s is depleted for %d more ticks.", obj.Name, st.DepleteTimer))
@@ -114,8 +114,8 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje
"effective_wait": wait,
"step": 0,
}
- if cfg.SharedDeplete > 0 {
- data["shared_deplete"] = true
+ if cfg.DepleteTimer > 0 {
+ data["deplete_timer"] = true
}
p.Action = &action.Action{
Type: "gather",
@@ -137,7 +137,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
step := p.Action.Data["step"].(int)
wait := p.Action.Data["effective_wait"].(int)
instanceKey := p.Action.Data["instance_key"].(string)
- _, shared := p.Action.Data["shared_deplete"]
+ _, shared := p.Action.Data["deplete_timer"]
if step == 0 {
if cfg.Bait != "" {
@@ -240,7 +240,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) {
}
if !shared && drop.Depletes {
- delay := cfg.DepleteDelay
+ delay := cfg.RespawnTimer
if delay <= 0 {
delay = 10
}
@@ -305,7 +305,7 @@ func filterDropsByLevel(drops []action.DropEntry, level int) []action.DropEntry
func (g *Game) depleteSharedTree(sess *net.Session, p *player.Player, st *world.ObjState, cfg *action.GatherConfig, instanceKey string) {
st.Depleted = true
- st.DepleteTimer = cfg.DepleteDelay
+ st.DepleteTimer = cfg.RespawnTimer
st.SharedTimer = 0
msg := cfg.ExhaustedMessage
diff --git a/internal/game/action_talk.go b/internal/game/action_talk.go
index f4929e9..4980c5a 100644
--- a/internal/game/action_talk.go
+++ b/internal/game/action_talk.go
@@ -111,7 +111,7 @@ func (g *Game) handleTalkInput(sess *net.Session, input string) {
return
}
- idx, err := parseIndex(input)
+ idx, err := parseChoiceIndex(input)
if err != nil || idx <= 0 {
g.CancelAction(p)
sess.State = net.StateGame
@@ -193,7 +193,7 @@ func (g *Game) applyNodeAction(sess *net.Session, na *action.NodeAction) {
g.AccountStore.SaveCharacter(p)
g.World.SeedGroundItems(p.RoomID)
g.seedRoomMobs(p.RoomID)
- g.ensureRoomObjects(p.RoomID)
+ g.seedRoomObjects(p.RoomID)
if g.Hub != nil {
g.Hub.EnterRoom(sess, p.RoomID)
}
diff --git a/internal/game/action_use.go b/internal/game/action_use.go
index 497ceac..c47492f 100644
--- a/internal/game/action_use.go
+++ b/internal/game/action_use.go
@@ -17,7 +17,7 @@ func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectD
return
}
- for itemID, qty := range cfg.Consume {
+ for itemID := range cfg.Consume {
if !p.HasItem(itemID) {
defName := itemID
if def, err := g.ItemStore.Load(itemID); err == nil {
@@ -26,7 +26,6 @@ func (g *Game) startUse(sess *net.Session, p *player.Player, obj *object.ObjectD
sess.WriteLine(fmt.Sprintf("You need %s to use this.", defName))
return
}
- _ = qty
}
if cfg.Success == nil && p.FirstFreeSlot() == -1 {
diff --git a/internal/game/cmd_drop.go b/internal/game/cmd_drop.go
index cbe232d..149b86b 100644
--- a/internal/game/cmd_drop.go
+++ b/internal/game/cmd_drop.go
@@ -222,7 +222,7 @@ func (g *Game) handleDropAllConfirm(sess *net.Session, input string) {
sess.WriteLine("\nYou have nothing to drop.")
} else {
sess.Write(fmt.Sprintf("\nYou drop your "))
- innerPickupReport(sess, dropped)
+ formatPickupList(sess, dropped)
}
sess.Write("\r\n> ")
}
diff --git a/internal/game/cmd_get.go b/internal/game/cmd_get.go
index f4c7426..78a9621 100644
--- a/internal/game/cmd_get.go
+++ b/internal/game/cmd_get.go
@@ -73,12 +73,10 @@ func (g *Game) doGet(sess *net.Session, input string) {
for i := 0; i < 28; i++ {
slot := p.InvSlot(i)
if slot != nil && slot.ItemID == itemID {
- removed, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, qty, p.Name)
- if !ok {
+ if _, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, qty, p.Name); !ok {
sess.WriteLine("That's not yours!")
return
}
- _ = removed
slot.Quantity += qty
g.AccountStore.SaveCharacter(p)
sess.WriteLine(fmt.Sprintf("You pick up %d x %s. (now %d)", qty, def.Name, slot.Quantity))
@@ -90,13 +88,11 @@ func (g *Game) doGet(sess *net.Session, input string) {
sess.WriteLine("Your inventory is full.")
return
}
- removed, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, qty, p.Name)
- if !ok {
+ if _, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, qty, p.Name); !ok {
sess.WriteLine("That's not yours!")
return
}
- _ = removed
- p.SetInvSlot(freeSlot, g.newInvSlot(itemID, qty))
+ p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, qty))
g.AccountStore.SaveCharacter(p)
if qty == 1 {
sess.WriteLine(fmt.Sprintf("You pick up a %s.", def.Name))
@@ -112,11 +108,9 @@ func (g *Game) doGet(sess *net.Session, input string) {
if fs == -1 {
break
}
- removed, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, 1, p.Name)
- if !ok {
+ if _, ok := g.World.RemoveReservedGroundItem(p.RoomID, itemID, 1, p.Name); !ok {
break
}
- _ = removed
p.SetInvSlot(fs, &player.InventorySlot{ItemID: itemID, Quantity: 1})
picked++
}
@@ -318,7 +312,7 @@ func (g *Game) doGetAll(sess *net.Session) {
if freeSlot == -1 {
if len(picked) > 0 {
sess.WriteLine("You can't hold everything but you picked up:")
- innerPickupReport(sess, picked)
+ formatPickupList(sess, picked)
} else {
sess.WriteLine("Your inventory is full!")
}
@@ -329,7 +323,7 @@ func (g *Game) doGetAll(sess *net.Session) {
if !ok {
break
}
- p.SetInvSlot(freeSlot, g.newInvSlot(itemID, qty))
+ p.SetInvSlot(freeSlot, g.newInventorySlot(itemID, qty))
name := itemID
if def != nil {
name = def.Name
@@ -342,7 +336,7 @@ func (g *Game) doGetAll(sess *net.Session) {
if freeSlot == -1 {
if len(picked) > 0 {
sess.WriteLine("You can't hold everything but you picked up:")
- innerPickupReport(sess, picked)
+ formatPickupList(sess, picked)
} else {
sess.WriteLine("Your inventory is full!")
}
@@ -371,7 +365,7 @@ func (g *Game) doGetAll(sess *net.Session) {
sess.WriteLine("Your inventory is full.")
} else {
sess.Write("You picked up: ")
- innerPickupReport(sess, picked)
+ formatPickupList(sess, picked)
}
}
diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go
index e0e98fe..a840579 100644
--- a/internal/game/cmd_look.go
+++ b/internal/game/cmd_look.go
@@ -141,7 +141,7 @@ func (g *Game) doLook(sess *net.Session) {
}
var suffix string
if multi && (len(timed) > 0 || len(depleted) > 0) {
- suffix = fmt.Sprintf(" [%s]", intsJoin(freshIdxs))
+ suffix = fmt.Sprintf(" [%s]", joinInts(freshIdxs))
}
qualityTimer := ""
if showTimers && len(instances) > 0 && instances[0].quality > 0 {
@@ -302,7 +302,7 @@ func (g *Game) doLook(sess *net.Session) {
}
line += fmt.Sprintf(" (fighting %s)", name)
}
- } else if desc := actionDesc(op); desc != "" {
+ } else if desc := playerActionDescription(op); desc != "" {
line += ", " + desc
}
sess.WriteLine(line + ".")
@@ -512,7 +512,7 @@ func (g *Game) doExits(sess *net.Session) {
}
}
-func intsJoin(nums []int) string {
+func joinInts(nums []int) string {
var parts []string
for _, n := range nums {
parts = append(parts, strconv.Itoa(n))
diff --git a/internal/game/cmd_map.go b/internal/game/cmd_map.go
index 7c506f9..e0413d3 100644
--- a/internal/game/cmd_map.go
+++ b/internal/game/cmd_map.go
@@ -1,11 +1,8 @@
package game
import (
- "strings"
-
"thirdcollapse/internal/net"
"thirdcollapse/internal/player"
- "thirdcollapse/internal/world"
)
func (g *Game) doMap(sess *net.Session) {
@@ -38,103 +35,3 @@ func (g *Game) doMap(sess *net.Session) {
sess.WriteLine(line)
}
}
-
-func stripBlankRows(lines []string) []string {
- var out []string
- for _, line := range lines {
- if strings.TrimSpace(line) != "" {
- out = append(out, line)
- }
- }
- return out
-}
-
-func leftTrimCommon(lines []string) []string {
- min := -1
- for _, line := range lines {
- if strings.TrimSpace(line) == "" {
- continue
- }
- n := 0
- for _, r := range line {
- if r == ' ' {
- n++
- } else {
- break
- }
- }
- if min < 0 || n < min {
- min = n
- }
- }
- if min <= 0 {
- return lines
- }
- result := make([]string, len(lines))
- for i, line := range lines {
- if len(line) <= min {
- result[i] = ""
- } else {
- result[i] = line[min:]
- }
- }
- return result
-}
-
-func buildFullMap(g *Game, roomID, mapWidth, mapHeight int) []string {
- mg := buildGraph(g, roomID)
-
- grid := make([][]rune, mapHeight)
- for i := range grid {
- grid[i] = make([]rune, mapWidth)
- for j := range grid[i] {
- grid[i][j] = ' '
- }
- }
-
- cx := mapWidth / 2
- cy := mapHeight / 2
-
- for pos, rid := range mg.posToRoom {
- gr := cy + pos[1]*2
- gc := cx + pos[0]*2
- if gr < 0 || gr >= mapHeight || gc < 0 || gc >= mapWidth {
- continue
- }
- if rid == roomID {
- grid[gr][gc] = '@'
- } else {
- grid[gr][gc] = roomMapSymbol(g, rid)
- }
- }
-
- for pos, rid := range mg.posToRoom {
- x, y := pos[0], pos[1]
-
- if rightID, ok := mg.posToRoom[[2]int{x + 1, y}]; ok {
- if exitsConnect(g, rid, rightID, world.East, world.West) {
- gr := cy + y*2
- gc := cx + x*2 + 1
- if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth {
- grid[gr][gc] = '─'
- }
- }
- }
-
- if bottomID, ok := mg.posToRoom[[2]int{x, y + 1}]; ok {
- if exitsConnect(g, rid, bottomID, world.South, world.North) {
- gr := cy + y*2 + 1
- gc := cx + x*2
- if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth {
- grid[gr][gc] = '│'
- }
- }
- }
- }
-
- lines := make([]string, mapHeight)
- for i := range grid {
- lines[i] = string(grid[i])
- }
- return lines
-}
diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go
index d62701d..9ef7a00 100644
--- a/internal/game/cmd_move.go
+++ b/internal/game/cmd_move.go
@@ -62,7 +62,7 @@ func (g *Game) doMove(sess *net.Session, dir string) {
g.World.SeedGroundItems(p.RoomID)
g.seedRoomMobs(p.RoomID)
- g.ensureRoomObjects(p.RoomID)
+ g.seedRoomObjects(p.RoomID)
if g.Hub != nil {
for _, other := range g.Hub.PlayersInRoom(oldRoom) {
@@ -103,7 +103,7 @@ func (g *Game) seedRoomMobs(roomID int) {
g.MobStore.SeedMobs(roomID, room.Mobs)
}
-func (g *Game) ensureRoomObjects(roomID int) {
+func (g *Game) seedRoomObjects(roomID int) {
room, err := g.World.LoadRoom(roomID)
if err != nil {
return
@@ -124,8 +124,8 @@ func (g *Game) ensureRoomObjects(roomID int) {
}
if def.BehaviorID != "" {
cfg, err := g.BehaviorStore.LoadGather(def.BehaviorID)
- if err == nil && cfg.SharedDeplete > 0 {
- g.World.SetObjSharedDeplete(roomID, obj.ID, cfg.SharedDeplete)
+ if err == nil && cfg.DepleteTimer > 0 {
+ g.World.SetObjDepleteTimer(roomID, obj.ID, cfg.DepleteTimer)
}
}
}
diff --git a/internal/game/cmd_wear.go b/internal/game/cmd_wear.go
index 4554dfc..7c3515f 100644
--- a/internal/game/cmd_wear.go
+++ b/internal/game/cmd_wear.go
@@ -155,6 +155,6 @@ func (g *Game) doWearAll(sess *net.Session) {
} else {
g.AccountStore.SaveCharacter(p)
sess.Write("You wear: ")
- innerPickupReport(sess, equipped)
+ formatPickupList(sess, equipped)
}
}
diff --git a/internal/game/doc.go b/internal/game/doc.go
new file mode 100644
index 0000000..97814e9
--- /dev/null
+++ b/internal/game/doc.go
@@ -0,0 +1,29 @@
+// Package game is the core session handler, command dispatch, login flow,
+// and action system for the MUD.
+//
+// The Game struct holds all world state: room/world access, item/object
+// stores, mob instances, behavior definitions, the player hub, and the
+// tick engine. HandleSession routes incoming input through the login
+// state machine into the in-game command dispatch.
+//
+// Commands are dispatched via handleGameCommand in game.go, which
+// resolves account aliases before switching on the command word.
+// Most commands cancel the player's ongoing action (gathering, etc.).
+//
+// Action system: StartAction normalizes verbs, resolves object/mob
+// targets, loads behaviors, and routes to type-specific handlers.
+// 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.
+//
+// File organization:
+// cmd_*.go — player command handlers (doLook, doMove, doGet, etc.)
+// action_*.go — action lifecycle (startGather, advanceBurn, talk, toggle, etc.)
+// login.go — account creation, authentication, character management
+// map.go — BFS graph builder, tiny + full-map rendering
+// tick.go — per-tick world updates (disconnect, regen, wander, woodcutting)
+// utils.go — shared types and helper functions
+// help.go — help topic loading and display
+package game
diff --git a/internal/game/game.go b/internal/game/game.go
index 21dee76..cecde2a 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -59,7 +59,7 @@ func (g *Game) SetHub(hub *net.Hub) {
}
func (g *Game) HandleSession(sess *net.Session, input string) {
- input = player.StripControl(input)
+ input = player.StripControlCharacters(input)
switch sess.State {
case net.StateAccountName:
g.handleAccountName(sess, input)
diff --git a/internal/game/login.go b/internal/game/login_account.go
index 3de70bc..7243955 100644
--- a/internal/game/login.go
+++ b/internal/game/login_account.go
@@ -268,91 +268,6 @@ func (g *Game) handleMenu(sess *net.Session, input string) {
}
}
-func (g *Game) handleNewCharName(sess *net.Session, input string) {
- name := strings.TrimSpace(input)
-
- if sess.PendingChar == "connect" && len(sess.Account.Characters) > 0 {
- if idx, err := parseIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) {
- sess.PendingChar = ""
- g.connectCharacter(sess, sess.Account.Characters[idx-1])
- return
- }
- sess.WriteLine("Invalid choice.")
- sess.Write("\n> ")
- return
- }
-
- if name == "" {
- sess.Write("Character name: ")
- return
- }
-
- if verr := player.ValidName(name); verr != nil {
- sess.WriteLine(verr.Error())
- sess.Write("Character name: ")
- return
- }
-
- if g.AccountStore.CharacterExists(name) {
- sess.WriteLine("A character with that name already exists.")
- sess.Write("Character name: ")
- return
- }
-
- p := player.New(name)
- p.RoomID = 1
-
- if err := g.AccountStore.SaveCharacter(p); err != nil {
- sess.WriteLine(fmt.Sprintf("Error creating character: %v", err))
- sess.State = net.StateMenu
- g.showMenu(sess)
- return
- }
-
- acc, _ := g.AccountStore.LoadAccount(sess.Account.Name)
- acc.Characters = append(acc.Characters, name)
- g.AccountStore.SaveAccount(acc)
- sess.Account.Characters = acc.Characters
-
- g.connectCharacter(sess, name)
-}
-
-func (g *Game) connectCharacter(sess *net.Session, name string) {
- g.charsMu.Lock()
- if existing := g.loggedInChars[name]; existing != nil {
- g.charsMu.Unlock()
- sess.WriteLine("This character is logged in elsewhere.")
- sess.State = net.StateMenu
- g.showMenu(sess)
- return
- }
-
- p, err := g.AccountStore.LoadCharacter(name)
- if err != nil {
- g.charsMu.Unlock()
- sess.WriteLine(fmt.Sprintf("Error loading character: %v", err))
- sess.State = net.StateMenu
- g.showMenu(sess)
- return
- }
-
- sess.Player = p
- sess.State = net.StateGame
- g.loggedInChars[name] = sess
- g.charsMu.Unlock()
-
- 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)
- sess.Write("\r\n> ")
-}
-
func (g *Game) handleRenameAccount(sess *net.Session, input string) {
newName := strings.TrimSpace(input)
if newName == "" {
@@ -389,147 +304,6 @@ func (g *Game) handleRenameAccount(sess *net.Session, input string) {
g.showMenu(sess)
}
-func (g *Game) handleRenameChar(sess *net.Session, input string) {
- name := strings.TrimSpace(input)
- if idx, err := parseIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) {
- sess.PendingChar = sess.Account.Characters[idx-1]
- } else {
- sess.PendingChar = name
- }
-
- found := false
- for _, c := range sess.Account.Characters {
- if c == sess.PendingChar {
- found = true
- break
- }
- }
- if !found {
- sess.WriteLine(fmt.Sprintf("No character named %s on this account.", sess.PendingChar))
- g.showMenu(sess)
- return
- }
-
- sess.State = net.StateRenameCharName
- sess.WriteLine(fmt.Sprintf("Renaming %s.", sess.PendingChar))
- sess.Write("New name: ")
-}
-
-func (g *Game) handleRenameCharName(sess *net.Session, input string) {
- newName := strings.TrimSpace(input)
- oldName := sess.PendingChar
- if newName == "" {
- sess.Write("New name: ")
- return
- }
- if verr := player.ValidName(newName); verr != nil {
- sess.WriteLine(verr.Error())
- sess.Write("New name: ")
- return
- }
- if newName == oldName {
- sess.WriteLine("That's already the character's name.")
- g.showMenu(sess)
- return
- }
- if g.AccountStore.CharacterExists(newName) {
- sess.WriteLine("A character with that name already exists.")
- sess.Write("New name: ")
- return
- }
-
- oldPath := g.AccountStore.CharPath(oldName)
- newPath := g.AccountStore.CharPath(newName)
- if err := os.Rename(oldPath, newPath); err != nil {
- sess.WriteLine(fmt.Sprintf("Error renaming: %v", err))
- g.showMenu(sess)
- return
- }
-
- p, _ := g.AccountStore.LoadCharacter(newName)
- if p != nil {
- p.Name = newName
- g.AccountStore.SaveCharacter(p)
- }
-
- acc, _ := g.AccountStore.LoadAccount(sess.Account.Name)
- for i, c := range acc.Characters {
- if c == oldName {
- acc.Characters[i] = newName
- break
- }
- }
- g.AccountStore.SaveAccount(acc)
- sess.Account.Characters = acc.Characters
-
- sess.PendingChar = ""
- sess.WriteLine(fmt.Sprintf("%s renamed to %s.", oldName, newName))
- g.showMenu(sess)
-}
-
-func (g *Game) showDeleteConfirm(sess *net.Session) {
- sess.State = net.StateDeleteChar
- sess.WriteLine(fmt.Sprintf("\nDelete %s? Type DELETE %s to confirm, or anything else to cancel.", sess.PendingChar, sess.PendingChar))
-}
-
-func (g *Game) handleDeleteChar(sess *net.Session, input string) {
- if sess.PendingChar == "" {
- name := strings.TrimSpace(input)
- if idx, err := parseIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) {
- sess.PendingChar = sess.Account.Characters[idx-1]
- } else {
- sess.PendingChar = name
- }
- found := false
- for _, c := range sess.Account.Characters {
- if c == sess.PendingChar {
- found = true
- break
- }
- }
- if !found {
- sess.WriteLine(fmt.Sprintf("No character named %s on this account.", sess.PendingChar))
- sess.PendingChar = ""
- g.showMenu(sess)
- return
- }
- g.showDeleteConfirm(sess)
- return
- }
-
- input = strings.TrimSpace(input)
- expected := "DELETE " + sess.PendingChar
- if strings.ToUpper(input) != strings.ToUpper(expected) {
- sess.WriteLine("Delete cancelled.")
- sess.PendingChar = ""
- g.showMenu(sess)
- return
- }
-
- charPath := g.AccountStore.CharPath(sess.PendingChar)
- if err := os.Remove(charPath); err != nil {
- sess.WriteLine(fmt.Sprintf("Error deleting: %v", err))
- sess.PendingChar = ""
- g.showMenu(sess)
- return
- }
-
- acc, _ := g.AccountStore.LoadAccount(sess.Account.Name)
- var newChars []string
- for _, c := range acc.Characters {
- if c != sess.PendingChar {
- newChars = append(newChars, c)
- }
- }
- acc.Characters = newChars
- g.AccountStore.SaveAccount(acc)
- sess.Account.Characters = newChars
-
- sess.WriteLine(fmt.Sprintf("%s has been deleted.", sess.PendingChar))
- sess.PendingChar = ""
- g.showMenu(sess)
-}
-
func (g *Game) handlePurgeAccount(sess *net.Session, input string) {
input = strings.TrimSpace(input)
expected := "PURGE " + sess.Account.Name
diff --git a/internal/game/login_char.go b/internal/game/login_char.go
new file mode 100644
index 0000000..ba034bb
--- /dev/null
+++ b/internal/game/login_char.go
@@ -0,0 +1,236 @@
+package game
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "thirdcollapse/internal/net"
+ "thirdcollapse/internal/player"
+)
+
+func (g *Game) handleNewCharName(sess *net.Session, input string) {
+ name := strings.TrimSpace(input)
+
+ if sess.PendingChar == "connect" && len(sess.Account.Characters) > 0 {
+ if idx, err := parseChoiceIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) {
+ sess.PendingChar = ""
+ g.connectCharacter(sess, sess.Account.Characters[idx-1])
+ return
+ }
+ sess.WriteLine("Invalid choice.")
+ sess.Write("\n> ")
+ return
+ }
+
+ if name == "" {
+ sess.Write("Character name: ")
+ return
+ }
+
+ if verr := player.ValidName(name); verr != nil {
+ sess.WriteLine(verr.Error())
+ sess.Write("Character name: ")
+ return
+ }
+
+ if g.AccountStore.CharacterExists(name) {
+ sess.WriteLine("A character with that name already exists.")
+ sess.Write("Character name: ")
+ return
+ }
+
+ p := player.New(name)
+ p.RoomID = 1
+
+ if err := g.AccountStore.SaveCharacter(p); err != nil {
+ sess.WriteLine(fmt.Sprintf("Error creating character: %v", err))
+ sess.State = net.StateMenu
+ g.showMenu(sess)
+ return
+ }
+
+ acc, _ := g.AccountStore.LoadAccount(sess.Account.Name)
+ acc.Characters = append(acc.Characters, name)
+ g.AccountStore.SaveAccount(acc)
+ sess.Account.Characters = acc.Characters
+
+ g.connectCharacter(sess, name)
+}
+
+func (g *Game) connectCharacter(sess *net.Session, name string) {
+ g.charsMu.Lock()
+ if existing := g.loggedInChars[name]; existing != nil {
+ g.charsMu.Unlock()
+ sess.WriteLine("This character is logged in elsewhere.")
+ sess.State = net.StateMenu
+ g.showMenu(sess)
+ return
+ }
+
+ p, err := g.AccountStore.LoadCharacter(name)
+ if err != nil {
+ g.charsMu.Unlock()
+ sess.WriteLine(fmt.Sprintf("Error loading character: %v", err))
+ sess.State = net.StateMenu
+ g.showMenu(sess)
+ return
+ }
+
+ sess.Player = p
+ sess.State = net.StateGame
+ g.loggedInChars[name] = sess
+ g.charsMu.Unlock()
+
+ g.World.SeedGroundItems(p.RoomID)
+ g.seedRoomMobs(p.RoomID)
+ g.seedRoomObjects(p.RoomID)
+
+ if g.Hub != nil {
+ g.Hub.EnterRoom(sess, p.RoomID)
+ }
+
+ g.doLook(sess)
+ sess.Write("\r\n> ")
+}
+
+func (g *Game) handleRenameChar(sess *net.Session, input string) {
+ name := strings.TrimSpace(input)
+ if idx, err := parseChoiceIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) {
+ sess.PendingChar = sess.Account.Characters[idx-1]
+ } else {
+ sess.PendingChar = name
+ }
+
+ found := false
+ for _, c := range sess.Account.Characters {
+ if c == sess.PendingChar {
+ found = true
+ break
+ }
+ }
+ if !found {
+ sess.WriteLine(fmt.Sprintf("No character named %s on this account.", sess.PendingChar))
+ g.showMenu(sess)
+ return
+ }
+
+ sess.State = net.StateRenameCharName
+ sess.WriteLine(fmt.Sprintf("Renaming %s.", sess.PendingChar))
+ sess.Write("New name: ")
+}
+
+func (g *Game) handleRenameCharName(sess *net.Session, input string) {
+ newName := strings.TrimSpace(input)
+ oldName := sess.PendingChar
+ if newName == "" {
+ sess.Write("New name: ")
+ return
+ }
+ if verr := player.ValidName(newName); verr != nil {
+ sess.WriteLine(verr.Error())
+ sess.Write("New name: ")
+ return
+ }
+ if newName == oldName {
+ sess.WriteLine("That's already the character's name.")
+ g.showMenu(sess)
+ return
+ }
+ if g.AccountStore.CharacterExists(newName) {
+ sess.WriteLine("A character with that name already exists.")
+ sess.Write("New name: ")
+ return
+ }
+
+ oldPath := g.AccountStore.CharPath(oldName)
+ newPath := g.AccountStore.CharPath(newName)
+ if err := os.Rename(oldPath, newPath); err != nil {
+ sess.WriteLine(fmt.Sprintf("Error renaming: %v", err))
+ g.showMenu(sess)
+ return
+ }
+
+ p, _ := g.AccountStore.LoadCharacter(newName)
+ if p != nil {
+ p.Name = newName
+ g.AccountStore.SaveCharacter(p)
+ }
+
+ acc, _ := g.AccountStore.LoadAccount(sess.Account.Name)
+ for i, c := range acc.Characters {
+ if c == oldName {
+ acc.Characters[i] = newName
+ break
+ }
+ }
+ g.AccountStore.SaveAccount(acc)
+ sess.Account.Characters = acc.Characters
+
+ sess.PendingChar = ""
+ sess.WriteLine(fmt.Sprintf("%s renamed to %s.", oldName, newName))
+ g.showMenu(sess)
+}
+
+func (g *Game) showDeleteConfirm(sess *net.Session) {
+ sess.State = net.StateDeleteChar
+ sess.WriteLine(fmt.Sprintf("\nDelete %s? Type DELETE %s to confirm, or anything else to cancel.", sess.PendingChar, sess.PendingChar))
+}
+
+func (g *Game) handleDeleteChar(sess *net.Session, input string) {
+ if sess.PendingChar == "" {
+ name := strings.TrimSpace(input)
+ if idx, err := parseChoiceIndex(name); err == nil && idx > 0 && idx <= len(sess.Account.Characters) {
+ sess.PendingChar = sess.Account.Characters[idx-1]
+ } else {
+ sess.PendingChar = name
+ }
+ found := false
+ for _, c := range sess.Account.Characters {
+ if c == sess.PendingChar {
+ found = true
+ break
+ }
+ }
+ if !found {
+ sess.WriteLine(fmt.Sprintf("No character named %s on this account.", sess.PendingChar))
+ sess.PendingChar = ""
+ g.showMenu(sess)
+ return
+ }
+ g.showDeleteConfirm(sess)
+ return
+ }
+
+ input = strings.TrimSpace(input)
+ expected := "DELETE " + sess.PendingChar
+ if strings.ToUpper(input) != strings.ToUpper(expected) {
+ sess.WriteLine("Delete cancelled.")
+ sess.PendingChar = ""
+ g.showMenu(sess)
+ return
+ }
+
+ charPath := g.AccountStore.CharPath(sess.PendingChar)
+ if err := os.Remove(charPath); err != nil {
+ sess.WriteLine(fmt.Sprintf("Error deleting: %v", err))
+ sess.PendingChar = ""
+ g.showMenu(sess)
+ return
+ }
+
+ acc, _ := g.AccountStore.LoadAccount(sess.Account.Name)
+ var newChars []string
+ for _, c := range acc.Characters {
+ if c != sess.PendingChar {
+ newChars = append(newChars, c)
+ }
+ }
+ acc.Characters = newChars
+ g.AccountStore.SaveAccount(acc)
+ sess.Account.Characters = newChars
+
+ sess.WriteLine(fmt.Sprintf("%s has been deleted.", sess.PendingChar))
+ sess.PendingChar = ""
+ g.showMenu(sess)
+}
diff --git a/internal/game/map.go b/internal/game/map.go
index cd1012b..e7738bd 100644
--- a/internal/game/map.go
+++ b/internal/game/map.go
@@ -1,6 +1,8 @@
package game
import (
+ "strings"
+
"thirdcollapse/internal/world"
)
@@ -189,3 +191,103 @@ func roomMapSymbol(g *Game, roomID int) rune {
}
return 'o'
}
+
+func stripBlankRows(lines []string) []string {
+ var out []string
+ for _, line := range lines {
+ if strings.TrimSpace(line) != "" {
+ out = append(out, line)
+ }
+ }
+ return out
+}
+
+func leftTrimCommon(lines []string) []string {
+ min := -1
+ for _, line := range lines {
+ if strings.TrimSpace(line) == "" {
+ continue
+ }
+ n := 0
+ for _, r := range line {
+ if r == ' ' {
+ n++
+ } else {
+ break
+ }
+ }
+ if min < 0 || n < min {
+ min = n
+ }
+ }
+ if min <= 0 {
+ return lines
+ }
+ result := make([]string, len(lines))
+ for i, line := range lines {
+ if len(line) <= min {
+ result[i] = ""
+ } else {
+ result[i] = line[min:]
+ }
+ }
+ return result
+}
+
+func buildFullMap(g *Game, roomID, mapWidth, mapHeight int) []string {
+ mg := buildGraph(g, roomID)
+
+ grid := make([][]rune, mapHeight)
+ for i := range grid {
+ grid[i] = make([]rune, mapWidth)
+ for j := range grid[i] {
+ grid[i][j] = ' '
+ }
+ }
+
+ cx := mapWidth / 2
+ cy := mapHeight / 2
+
+ for pos, rid := range mg.posToRoom {
+ gr := cy + pos[1]*2
+ gc := cx + pos[0]*2
+ if gr < 0 || gr >= mapHeight || gc < 0 || gc >= mapWidth {
+ continue
+ }
+ if rid == roomID {
+ grid[gr][gc] = '@'
+ } else {
+ grid[gr][gc] = roomMapSymbol(g, rid)
+ }
+ }
+
+ for pos, rid := range mg.posToRoom {
+ x, y := pos[0], pos[1]
+
+ if rightID, ok := mg.posToRoom[[2]int{x + 1, y}]; ok {
+ if exitsConnect(g, rid, rightID, world.East, world.West) {
+ gr := cy + y*2
+ gc := cx + x*2 + 1
+ if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth {
+ grid[gr][gc] = '─'
+ }
+ }
+ }
+
+ if bottomID, ok := mg.posToRoom[[2]int{x, y + 1}]; ok {
+ if exitsConnect(g, rid, bottomID, world.South, world.North) {
+ gr := cy + y*2 + 1
+ gc := cx + x*2
+ if gr >= 0 && gr < mapHeight && gc >= 0 && gc < mapWidth {
+ grid[gr][gc] = '│'
+ }
+ }
+ }
+ }
+
+ lines := make([]string, mapHeight)
+ for i := range grid {
+ lines[i] = string(grid[i])
+ }
+ return lines
+}
diff --git a/internal/game/types.go b/internal/game/types.go
new file mode 100644
index 0000000..faed9e1
--- /dev/null
+++ b/internal/game/types.go
@@ -0,0 +1,29 @@
+package game
+
+import "thirdcollapse/internal/object"
+
+var EquipSlots = []object.EquipSlot{
+ object.SlotHead, object.SlotNeck, object.SlotTorso, object.SlotLegs,
+ object.SlotHands, object.SlotFeet, object.SlotBack, object.SlotAmmo,
+ object.SlotMainHand, object.SlotOffHand, object.SlotRing,
+}
+
+type itemMatch struct {
+ ID string
+ Name string
+ Slot int
+}
+
+type xpGain struct {
+ Skill string
+ XP int
+}
+
+type deathDrop struct {
+ itemID string
+ quantity int
+ totalVal int
+ isEquip bool
+ equipSlot object.EquipSlot
+ invSlot int
+}
diff --git a/internal/game/utils.go b/internal/game/utils.go
index 969eba4..e80de24 100644
--- a/internal/game/utils.go
+++ b/internal/game/utils.go
@@ -7,37 +7,10 @@ import (
"strings"
"thirdcollapse/internal/net"
- "thirdcollapse/internal/object"
"thirdcollapse/internal/player"
"thirdcollapse/internal/world"
)
-var EquipSlots = []object.EquipSlot{
- object.SlotHead, object.SlotNeck, object.SlotTorso, object.SlotLegs,
- object.SlotHands, object.SlotFeet, object.SlotBack, object.SlotAmmo,
- object.SlotMainHand, object.SlotOffHand, object.SlotRing,
-}
-
-type itemMatch struct {
- ID string
- Name string
- Slot int
-}
-
-type xpGain struct {
- Skill string
- XP int
-}
-
-type deathDrop struct {
- itemID string
- quantity int
- totalVal int
- isEquip bool
- equipSlot object.EquipSlot
- invSlot int
-}
-
func plural(n int) string {
if n == 1 {
return ""
@@ -55,7 +28,7 @@ func parseQty(input string) (int, string) {
return 0, input
}
-func parseIndex(s string) (int, error) {
+func parseChoiceIndex(s string) (int, error) {
var idx int
_, err := fmt.Sscanf(s, "%d", &idx)
return idx, err
@@ -94,7 +67,7 @@ func randInt(max int) int {
return rand.Intn(max)
}
-func innerPickupReport(sess *net.Session, picked []string) {
+func formatPickupList(sess *net.Session, picked []string) {
for i, name := range picked {
if i == len(picked)-1 {
sess.WriteLine(name)
@@ -106,7 +79,7 @@ func innerPickupReport(sess *net.Session, picked []string) {
}
}
-func actionDesc(p *player.Player) string {
+func playerActionDescription(p *player.Player) string {
if p.Action == nil {
return ""
}
@@ -129,7 +102,7 @@ func actionDesc(p *player.Player) string {
return ""
}
-func (g *Game) newInvSlot(itemID string, qty int) *player.InventorySlot {
+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 {
slot.Quality = def.Quality
diff --git a/internal/net/doc.go b/internal/net/doc.go
new file mode 100644
index 0000000..13e7f71
--- /dev/null
+++ b/internal/net/doc.go
@@ -0,0 +1,15 @@
+// Package net provides transport-agnostic network connections, session
+// management, and a multi-listener server.
+//
+// The Conn interface abstracts TCP (telnet) and WebSocket connections,
+// handling IAC echo control for password suppression. The Session type
+// tracks connection state through the login flow (account name, password,
+// menu navigation, character selection, and in-game play).
+//
+// The Hub tracks which sessions are in which rooms, enabling room-scoped
+// messaging. The Server manages telnet, HTTP, and HTTPS listeners and
+// dispatches incoming input to the game handler.
+//
+// Session I/O helpers (Write, WriteLine, WriteLines) handle CRLF line
+// endings required by telnet clients.
+package net
diff --git a/internal/object/doc.go b/internal/object/doc.go
new file mode 100644
index 0000000..89f6db9
--- /dev/null
+++ b/internal/object/doc.go
@@ -0,0 +1,14 @@
+// Package object defines item and object definitions with their YAML-backed
+// loading stores.
+//
+// ItemDef covers all game items: equipment stats, weapon types, tool
+// properties (tool_type, tool_speed for gathering/firemaking), firemaking
+// fields (burn_ticks, fire_level, fire_xp), and stackable/quality metadata.
+//
+// ObjectDef covers interactive world objects: their behavior reference,
+// hidden flag for unlisted-but-interactable objects, in-room description
+// overrides, removal items, and arbitrary props.
+//
+// ItemStore and ObjectStore load definitions from YAML files in the
+// data/items/ and data/objects/ directories, caching results in memory.
+package object
diff --git a/internal/object/item.go b/internal/object/item.go
index 009f10e..18fd034 100644
--- a/internal/object/item.go
+++ b/internal/object/item.go
@@ -1,6 +1,10 @@
package object
-import "strings"
+import (
+ "strings"
+
+ "thirdcollapse/internal/world"
+)
type EquipSlot string
@@ -67,34 +71,13 @@ func (d *ItemDef) MatchesName(input string) bool {
return true
}
}
- if wordPrefixMatch(lower, d.Name) {
+ if world.WordPrefixMatch(lower, d.Name) {
return true
}
for _, alias := range d.Aliases {
- if wordPrefixMatch(lower, alias) {
+ if world.WordPrefixMatch(lower, alias) {
return true
}
}
return false
}
-
-func wordPrefixMatch(input, name string) bool {
- inputWords := strings.Fields(input)
- if len(inputWords) == 0 {
- return false
- }
- nameWords := strings.Fields(strings.ToLower(name))
- for _, iw := range inputWords {
- found := false
- for _, nw := range nameWords {
- if strings.HasPrefix(nw, iw) || strings.HasPrefix(iw, nw) {
- found = true
- break
- }
- }
- if !found {
- return false
- }
- }
- return true
-}
diff --git a/internal/player/doc.go b/internal/player/doc.go
new file mode 100644
index 0000000..a163bd7
--- /dev/null
+++ b/internal/player/doc.go
@@ -0,0 +1,14 @@
+// Package player defines character data, skills, equipment, inventory,
+// and account management for the game.
+//
+// The Player struct is the full character sheet: 23 skills with RSC-style
+// XP progression, 28-slot inventory, 11-slot equipment, combat level
+// calculation, and death mechanics.
+//
+// Accounts store authentication (salted sha256 passwords), character lists,
+// and command aliases. Validation functions enforce name and password rules.
+// The AccountStore manages persistence to YAML files under data/players/.
+//
+// Player options (15 settings) support bool, string, and int types with
+// defined defaults and valid values.
+package player
diff --git a/internal/player/validate.go b/internal/player/validate.go
index 63f40d2..d836089 100644
--- a/internal/player/validate.go
+++ b/internal/player/validate.go
@@ -22,7 +22,7 @@ func ValidPassword(pw string) error {
return nil
}
-func StripControl(input string) string {
+func StripControlCharacters(input string) string {
runes := make([]rune, 0, len(input))
for _, r := range input {
if r == '\n' || r == '\t' {
diff --git a/internal/world/doc.go b/internal/world/doc.go
new file mode 100644
index 0000000..cc11535
--- /dev/null
+++ b/internal/world/doc.go
@@ -0,0 +1,18 @@
+// Package world manages rooms, exits, ground items, mob instances,
+// and object instance state for the game world.
+//
+// Rooms are loaded from YAML files on every access (live-editable). Exits
+// support conditional blocking via the action.Condition system. Ground
+// items have independent despawn timers and combat-loot reservations.
+//
+// Object instance state (ObjState) tracks per-instance properties:
+// depletion with regen timers, shared depletion for trees with countdown,
+// quality for fire objects, and wandering for fishing spots.
+//
+// MobDef and MobInstance cover mob definitions and runtime instances.
+// The MobStore manages YAML loading, instance seeding, and per-room
+// mob lists. Mobs wander via legal (unconditioned) room exits.
+//
+// WordPrefixMatch provides bidirectional prefix matching used by
+// commands and object lookups.
+package world
diff --git a/internal/world/world.go b/internal/world/world.go
index e2d12ba..ac5345e 100644
--- a/internal/world/world.go
+++ b/internal/world/world.go
@@ -192,7 +192,7 @@ func (w *World) FindObjInstances(roomID int, name string) []ObjState {
defer w.mu.Unlock()
var out []ObjState
lower := strings.ToLower(name)
- for key, st := range w.objStates {
+ for _, st := range w.objStates {
if st.RoomID != roomID {
continue
}
@@ -210,7 +210,6 @@ func (w *World) FindObjInstances(roomID int, name string) []ObjState {
SharedTimer: st.SharedTimer,
Quality: st.Quality,
})
- _ = key
}
sort.Slice(out, func(i, j int) bool {
if out[i].DefID != out[j].DefID {
@@ -225,14 +224,11 @@ func wordMatchesObj(lower, defID, objName string) bool {
if WordPrefixMatch(lower, objName) {
return true
}
- nameWords := strings.Fields(strings.ToLower(objName))
inputWords := strings.Fields(strings.ToLower(lower))
- // Check defID as whole (bidirectional)
if strings.HasPrefix(strings.ToLower(defID), lower) || strings.HasPrefix(lower, strings.ToLower(defID)) {
return true
}
- // Check each defID part (split by underscore) against each input word (bidirectional)
for _, part := range strings.Split(defID, "_") {
for _, iw := range inputWords {
pl := strings.ToLower(part)
@@ -241,42 +237,38 @@ func wordMatchesObj(lower, defID, objName string) bool {
}
}
}
- _ = nameWords
return false
}
func (w *World) SetObjName(roomID int, defID string, name string) {
w.mu.Lock()
defer w.mu.Unlock()
- for key, st := range w.objStates {
+ for _, st := range w.objStates {
if st.RoomID == roomID && st.DefID == defID {
st.Name = name
}
- _ = key
}
}
func (w *World) SetObjWander(roomID int, defID string, rooms []int, interval int) {
w.mu.Lock()
defer w.mu.Unlock()
- for key, st := range w.objStates {
+ for _, st := range w.objStates {
if st.RoomID == roomID && st.DefID == defID {
st.WanderRooms = rooms
st.WanderInterval = interval
}
- _ = key
}
}
-func (w *World) SetObjSharedDeplete(roomID int, defID string, max int) {
+func (w *World) SetObjDepleteTimer(roomID int, defID string, max int) {
w.mu.Lock()
defer w.mu.Unlock()
- for key, st := range w.objStates {
+ for _, st := range w.objStates {
if st.RoomID == roomID && st.DefID == defID && st.SharedMax == 0 {
st.SharedMax = max
st.SharedTimer = max
}
- _ = key
}
}