diff options
| author | historia <[not public]> | 2026-06-25 04:46:40 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-25 04:46:40 -0400 |
| commit | 2725e2927a1595c7b100d942d1f14146252adeb7 (patch) | |
| tree | e8bc2292634a2d686c751981ddb3097517cccc6a /internal | |
| parent | 94a06fbbe682701ca4d4bd2a70cd74d8df29c932 (diff) | |
| download | thehouseoficarus-2725e2927a1595c7b100d942d1f14146252adeb7.tar.gz | |
feat: who, global chat, removed redundant YAML IDs
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/action/action.go | 1 | ||||
| -rw-r--r-- | internal/color/color.go | 42 | ||||
| -rw-r--r-- | internal/color/color_test.go | 55 | ||||
| -rw-r--r-- | internal/config/config.go | 1 | ||||
| -rw-r--r-- | internal/game/cmd_color.go | 1 | ||||
| -rw-r--r-- | internal/game/cmd_global.go | 36 | ||||
| -rw-r--r-- | internal/game/cmd_look.go | 88 | ||||
| -rw-r--r-- | internal/game/cmd_registry.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_verbs.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_who.go | 32 | ||||
| -rw-r--r-- | internal/game/core_course.go | 1 | ||||
| -rw-r--r-- | internal/game/sys_science.go | 1 | ||||
| -rw-r--r-- | internal/game/sys_technology.go | 1 | ||||
| -rw-r--r-- | internal/game/ui_help.go | 2 | ||||
| -rw-r--r-- | internal/net/server.go | 22 | ||||
| -rw-r--r-- | internal/object/item_store.go | 2 | ||||
| -rw-r--r-- | internal/object/store.go | 1 | ||||
| -rw-r--r-- | internal/player/player.go | 1 | ||||
| -rw-r--r-- | internal/world/hazard.go | 2 |
19 files changed, 243 insertions, 50 deletions
diff --git a/internal/action/action.go b/internal/action/action.go index a72f6ca..d66d88e 100644 --- a/internal/action/action.go +++ b/internal/action/action.go @@ -184,6 +184,5 @@ type DropEntry struct { } type DropTableDef struct { - ID string `yaml:"id"` Drops []DropEntry `yaml:"drops"` } diff --git a/internal/color/color.go b/internal/color/color.go index dcbb280..22fa143 100644 --- a/internal/color/color.go +++ b/internal/color/color.go @@ -180,6 +180,48 @@ func VisibleLen(s string) int { return utf8.RuneCountInString(ansiRe.ReplaceAllString(s, "")) } +// WrapANSI wraps text to the given visible width, ignoring ANSI color codes +// when measuring. Each input line (split on "\n") is handled on its own: a line +// that already fits is returned untouched, so indentation and aligned UI are +// preserved. Only over-long lines are word-wrapped. The result is joined with +// "\r\n". A width <= 0 disables wrapping. +func WrapANSI(s string, width int) string { + if width <= 0 { + return s + } + var out []string + for _, line := range strings.Split(s, "\n") { + line = strings.TrimRight(line, "\r") + out = append(out, wrapLine(line, width)...) + } + return strings.Join(out, "\r\n") +} + +func wrapLine(line string, width int) []string { + if VisibleLen(line) <= width { + return []string{line} + } + words := strings.Fields(line) + if len(words) == 0 { + return []string{line} + } + cur := words[0] + curWidth := VisibleLen(words[0]) + var lines []string + for _, w := range words[1:] { + ww := VisibleLen(w) + if curWidth+1+ww <= width { + cur += " " + w + curWidth += 1 + ww + } else { + lines = append(lines, cur) + cur = w + curWidth = ww + } + } + return append(lines, cur) +} + func ContrastFg(mode string, bgIndex int) string { r, g, b := Xterm256ToRGB(bgIndex) if r+g+b > 384 { diff --git a/internal/color/color_test.go b/internal/color/color_test.go new file mode 100644 index 0000000..c1f5e5b --- /dev/null +++ b/internal/color/color_test.go @@ -0,0 +1,55 @@ +package color + +import ( + "strings" + "testing" +) + +func TestWrapANSIDisabled(t *testing.T) { + s := "this is a very long line that would normally be wrapped if enabled" + if got := WrapANSI(s, 0); got != s { + t.Errorf("WrapANSI(_, 0) = %q, want unchanged", got) + } +} + +func TestWrapANSIShortLineUntouched(t *testing.T) { + s := " indented menu item" + if got := WrapANSI(s, 80); got != s { + t.Errorf("short line was modified: got %q, want %q", got, s) + } +} + +func TestWrapANSIWrapsLongLine(t *testing.T) { + s := "alpha bravo charlie delta echo foxtrot golf hotel india juliet" + got := WrapANSI(s, 20) + lines := strings.Split(got, "\r\n") + if len(lines) < 2 { + t.Fatalf("expected multiple lines, got %d: %q", len(lines), got) + } + for _, l := range lines { + if VisibleLen(l) > 20 { + t.Errorf("line exceeds width: %q (%d)", l, VisibleLen(l)) + } + } + if strings.Join(strings.Fields(got), " ") != s { + t.Errorf("words not preserved: %q", got) + } +} + +func TestWrapANSIIgnoresColorCodes(t *testing.T) { + colored := FgCode("xterm256", 182) + "hello world" + Reset + if VisibleLen(colored) > 80 { + t.Fatalf("test setup wrong, visible len = %d", VisibleLen(colored)) + } + if got := WrapANSI(colored, 80); got != colored { + t.Errorf("colored line within width was wrapped: %q", got) + } +} + +func TestWrapANSISplitsOnNewlines(t *testing.T) { + got := WrapANSI("first line\nsecond line", 80) + want := "first line\r\nsecond line" + if got != want { + t.Errorf("WrapANSI newline handling = %q, want %q", got, want) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index af00297..b01997c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,6 +40,7 @@ func DefaultColors() ColorsConfig { "miss": "243", "error": "209", "say": "230", + "global": "45", "dialog": "117", "broadcast": "215", "fire": "208", diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go index 51f8158..7bf073d 100644 --- a/internal/game/cmd_color.go +++ b/internal/game/cmd_color.go @@ -154,6 +154,7 @@ var colorCategoryOrder = []string{ "miss", "error", "say", + "global", "dialog", "broadcast", "fire", diff --git a/internal/game/cmd_global.go b/internal/game/cmd_global.go new file mode 100644 index 0000000..d06a233 --- /dev/null +++ b/internal/game/cmd_global.go @@ -0,0 +1,36 @@ +package game + +import ( + "fmt" + "strings" + + "thehouseoficarus/internal/net" +) + +func (g *Game) executeGlobal(sess *net.Session, args []string, rawInput string) { + if len(args) == 0 { + sess.WriteLine("Usage: global <message> — sends a message to everyone on the world.") + return + } + + msgStart := strings.Index(strings.ToLower(rawInput), "global ") + 7 + if msgStart >= 7 && msgStart < len(rawInput) { + g.doGlobal(sess, rawInput[msgStart:]) + } else { + g.doGlobal(sess, strings.Join(args, " ")) + } +} + +func (g *Game) doGlobal(sess *net.Session, msg string) { + p := sess.Player + + for _, other := range g.Hub.AllSessions() { + if other.Player == nil { + continue + } + other.WriteLine(fmt.Sprintf("\n%s%s: %s", + g.colorize(other, "global", "[Global] "), + g.colorize(other, "player_name", p.Name), + g.colorize(other, "global", msg))) + } +} diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 3f31a15..5cdc15d 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -540,26 +540,26 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { fmt.Sprintf("%s %s", g.colorize(sess, mobColor, best.Name), levelStr), ) if best.IdleDescription != "" { - sess.WriteLine(fmt.Sprintf(" %s", best.IdleDescription)) + sess.WriteLine(best.IdleDescription) } sess.WriteLines( "", - fmt.Sprintf(" Attack: %d", best.Attack), - fmt.Sprintf(" Strength: %d", best.Strength), - fmt.Sprintf(" Defense: %d", best.Defense), - fmt.Sprintf(" HP: %d/%d", best.HP, best.MaxHP), + fmt.Sprintf("Attack: %d", best.Attack), + fmt.Sprintf("Strength: %d", best.Strength), + fmt.Sprintf("Defense: %d", best.Defense), + fmt.Sprintf("HP: %d/%d", best.HP, best.MaxHP), ) if best.StabDefense != 0 || best.SlashDefense != 0 || best.CrushDefense != 0 || best.ScienceDefense != 0 || best.RangedDefense != 0 { sess.WriteLines( "", - " Defense bonuses:", - fmt.Sprintf(" Stab: %+d Slash: %+d Crush: %+d", best.StabDefense, best.SlashDefense, best.CrushDefense), - fmt.Sprintf(" Science: %+d Ranged: %+d", best.ScienceDefense, best.RangedDefense), + "Defense bonuses:", + fmt.Sprintf(" Stab: %+d Slash: %+d Crush: %+d", best.StabDefense, best.SlashDefense, best.CrushDefense), + fmt.Sprintf(" Science: %+d Ranged: %+d", best.ScienceDefense, best.RangedDefense), ) } if best.Weakness != "" { - sess.WriteLine(fmt.Sprintf(" Weakness: %s", best.Weakness)) + sess.WriteLine(fmt.Sprintf("Weakness: %s", best.Weakness)) } return } @@ -612,12 +612,10 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { sess.WriteLine("") if len(instances) > 1 { sess.WriteLine(fmt.Sprintf("%d %ss:", len(instances), def.Name)) - } else { - sess.WriteLine(def.Name) } if objDescText != "" && !strings.Contains(objDescText, "{quality}") { - sess.WriteLine(fmt.Sprintf(" %s", objDescText)) + sess.WriteLine(color.ExpandTags(g.colorMode(sess), objDescText)) } if def.Safespot != nil { @@ -635,9 +633,9 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { if def.Safespot.MaxBlockSize != "" { blockInfo = fmt.Sprintf("up to %s mobs", def.Safespot.MaxBlockSize) } - sess.WriteLine(fmt.Sprintf(" Safespot: %s — blocks %s", levelStr, blockInfo)) + sess.WriteLine(fmt.Sprintf("Safespot: %s — blocks %s", levelStr, blockInfo)) if len(realSt.SafespotOccupants) > 0 { - sess.WriteLine(fmt.Sprintf(" Occupied by: %s", strings.Join(realSt.SafespotOccupants, ", "))) + sess.WriteLine(fmt.Sprintf("Occupied by: %s", strings.Join(realSt.SafespotOccupants, ", "))) } } } @@ -646,15 +644,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, int(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.", int(ist.DepleteTimer))) + sess.WriteLine(fmt.Sprintf("Depleted, respawns in %d ticks.", int(ist.DepleteTimer))) } } else if ist.SharedMax > 0 && float64(ist.SharedTimer) < ist.SharedMax { if len(instances) > 1 { - sess.WriteLine(fmt.Sprintf(" %s %d: despawn timer %d/%.0f.", 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/%.0f ticks.", ist.SharedTimer, ist.SharedMax)) + sess.WriteLine(fmt.Sprintf("Despawn timer: %d/%.0f ticks.", ist.SharedTimer, ist.SharedMax)) } } } @@ -662,7 +660,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { for _, ist := range instances { if ist.Quality > 0 && strings.Contains(def.Description, "{quality}") { desc := strings.ReplaceAll(def.Description, "{quality}", fmt.Sprintf("%.0f", ist.Quality)) - sess.WriteLine(fmt.Sprintf(" %s", desc)) + sess.WriteLine(color.ExpandTags(g.colorMode(sess), desc)) } } @@ -682,8 +680,8 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { sess.WriteLines( "", g.itemColorize(sess, def, def.Name), - fmt.Sprintf(" %s", def.Description), - fmt.Sprintf(" Value: %d credits", def.Value), + color.ExpandTags(g.colorMode(sess), def.Description), + fmt.Sprintf("Value: %d credits", def.Value), ) g.showItemStats(sess, def) return @@ -701,11 +699,11 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { lines := []string{ "", g.itemColorize(sess, def, def.Name), - fmt.Sprintf(" %s", def.Description), - fmt.Sprintf(" Value: %d credits", def.Value), + color.ExpandTags(g.colorMode(sess), def.Description), + fmt.Sprintf("Value: %d credits", def.Value), } if slot.MaxQuality > 0 { - lines = append(lines, fmt.Sprintf(" Has %d units of butane left.", slot.Quality)) + lines = append(lines, fmt.Sprintf("Has %d units of butane left.", slot.Quality)) } sess.WriteLines(lines...) g.showItemStats(sess, def) @@ -735,18 +733,18 @@ func showPlayerInfo(g *Game, sess *net.Session, p *player.Player) { sess.WriteLines( "", p.Name, - fmt.Sprintf(" Combat Level: %s", levelStr), - fmt.Sprintf(" HP: %d/%d", p.HP, p.MaxHP()), + fmt.Sprintf("Combat Level: %s", levelStr), + fmt.Sprintf("HP: %d/%d", p.HP, p.MaxHP()), "", ) for _, s := range player.AllSkills { level := p.Level(s) - sess.WriteLine(fmt.Sprintf(" %-12s Level: %d", s, level)) + sess.WriteLine(fmt.Sprintf("%-12s Level: %d", s, level)) } sess.WriteLine("") - sess.WriteLine(" Equipment:") + sess.WriteLine("Equipment:") for _, slot := range EquipSlots { itemID, ok := p.Equipment[slot] if !ok { @@ -758,12 +756,12 @@ func showPlayerInfo(g *Game, sess *net.Session, p *player.Player) { name = def.Name itemDef = def } - sess.WriteLine(fmt.Sprintf(" %-12s %s", slot, g.itemColorize(sess, itemDef, name))) + sess.WriteLine(fmt.Sprintf(" %-12s %s", slot, g.itemColorize(sess, itemDef, name))) } if p.Description != "" { sess.WriteLine("") - sess.WriteLine(fmt.Sprintf(" %s", p.Description)) + sess.WriteLine(p.Description) } } @@ -916,40 +914,40 @@ func (g *Game) showItemStats(sess *net.Session, def *object.ItemDef) { sess.WriteLine("") if hasAttack || hasDefense { - sess.WriteLine(" Attack bonuses: Defense bonuses:") - sess.WriteLine(fmt.Sprintf(" Stab: %+4d Stab: %+4d", s.StabAttack, s.StabDefense)) - sess.WriteLine(fmt.Sprintf(" Slash: %+4d Slash: %+4d", s.SlashAttack, s.SlashDefense)) - sess.WriteLine(fmt.Sprintf(" Crush: %+4d Crush: %+4d", s.CrushAttack, s.CrushDefense)) - sess.WriteLine(fmt.Sprintf(" Science:%+4d Science:%+4d", s.ScienceAttack, s.ScienceDefense)) - sess.WriteLine(fmt.Sprintf(" Ranged: %+4d Ranged: %+4d", s.RangedAttack, s.RangedDefense)) + sess.WriteLine("Attack bonuses: Defense bonuses:") + sess.WriteLine(fmt.Sprintf(" Stab: %+4d Stab: %+4d", s.StabAttack, s.StabDefense)) + sess.WriteLine(fmt.Sprintf(" Slash: %+4d Slash: %+4d", s.SlashAttack, s.SlashDefense)) + sess.WriteLine(fmt.Sprintf(" Crush: %+4d Crush: %+4d", s.CrushAttack, s.CrushDefense)) + sess.WriteLine(fmt.Sprintf(" Science:%+4d Science:%+4d", s.ScienceAttack, s.ScienceDefense)) + sess.WriteLine(fmt.Sprintf(" Ranged: %+4d Ranged: %+4d", s.RangedAttack, s.RangedDefense)) } if hasOther { sess.WriteLine("") - sess.WriteLine(" Other bonuses:") + sess.WriteLine("Other bonuses:") if s.StrengthBonus != 0 { - sess.WriteLine(fmt.Sprintf(" Melee strength: %+d", s.StrengthBonus)) + sess.WriteLine(fmt.Sprintf(" Melee strength: %+d", s.StrengthBonus)) } if s.RangedStrength != 0 { - sess.WriteLine(fmt.Sprintf(" Ranged strength: %+d", s.RangedStrength)) + sess.WriteLine(fmt.Sprintf(" Ranged strength: %+d", s.RangedStrength)) } if s.ScienceDamage != 0 { - sess.WriteLine(fmt.Sprintf(" Science damage: %+d", s.ScienceDamage)) + sess.WriteLine(fmt.Sprintf(" Science damage: %+d", s.ScienceDamage)) } if s.TechnologyBonus != 0 { - sess.WriteLine(fmt.Sprintf(" Technology: %+d", s.TechnologyBonus)) + sess.WriteLine(fmt.Sprintf(" Technology: %+d", s.TechnologyBonus)) } } if def.AttackType != "" { - sess.WriteLine(fmt.Sprintf(" Attack type: %s", def.AttackType)) + sess.WriteLine(fmt.Sprintf("Attack type: %s", def.AttackType)) } if def.Speed > 0 { - sess.WriteLine(fmt.Sprintf(" Speed: %.0f", def.Speed)) + sess.WriteLine(fmt.Sprintf("Speed: %.0f", def.Speed)) } if len(def.Requirements) > 0 { sess.WriteLine("") - sess.WriteLine(" Requirements:") + sess.WriteLine("Requirements:") for skill, level := range def.Requirements { - sess.WriteLine(fmt.Sprintf(" %s: %d", skill, level)) + sess.WriteLine(fmt.Sprintf(" %s: %d", skill, level)) } } } diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go index be1c276..bc910ed 100644 --- a/internal/game/cmd_registry.go +++ b/internal/game/cmd_registry.go @@ -39,6 +39,8 @@ var commandRegistry = map[string]commandDef{ "down": {(*Game).executeMove, ClassActive}, "d": {(*Game).executeMove, ClassActive}, "say": {(*Game).executeSay, ClassInstant}, + "global": {(*Game).executeGlobal, ClassInstant}, + "who": {(*Game).executeWho, ClassInstant}, "sc": {(*Game).executeScore, ClassInstant}, "score": {(*Game).executeScore, ClassInstant}, "skills": {(*Game).executeSkills, ClassInstant}, diff --git a/internal/game/cmd_verbs.go b/internal/game/cmd_verbs.go index 9ee1607..f76ee6c 100644 --- a/internal/game/cmd_verbs.go +++ b/internal/game/cmd_verbs.go @@ -11,6 +11,8 @@ import ( var alwaysAvailable = []string{ "look", "l", "say", + "global", + "who", "exits", "map", "walk", diff --git a/internal/game/cmd_who.go b/internal/game/cmd_who.go new file mode 100644 index 0000000..87c8a9c --- /dev/null +++ b/internal/game/cmd_who.go @@ -0,0 +1,32 @@ +package game + +import ( + "fmt" + "sort" + "strings" + + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +func (g *Game) executeWho(sess *net.Session, args []string, rawInput string) { + myP := sess.Player + + var players []*player.Player + for _, other := range g.Hub.AllSessions() { + if other.Player == nil { + continue + } + players = append(players, other.Player) + } + sort.Slice(players, func(i, j int) bool { + return strings.ToLower(players[i].Name) < strings.ToLower(players[j].Name) + }) + + sess.WriteLine(fmt.Sprintf("\nPlayers online (%d):", len(players))) + for _, p := range players { + lvl := p.CombatLevel() + levelStr := g.levelColorize(sess, myP.CombatLevel(), lvl, fmt.Sprintf("(level %d)", lvl)) + sess.WriteLine(fmt.Sprintf(" %s %s", p.Name, levelStr)) + } +} diff --git a/internal/game/core_course.go b/internal/game/core_course.go index 908466d..7d7fe08 100644 --- a/internal/game/core_course.go +++ b/internal/game/core_course.go @@ -96,6 +96,7 @@ func (cs *CourseStore) loadAllLocked() { if err := yaml.Unmarshal(data, &cfg); err != nil { return nil } + cfg.ID = id cs.courses[cfg.ID] = &cfg totalObstacles := len(cfg.Obstacles) diff --git a/internal/game/sys_science.go b/internal/game/sys_science.go index 1129fee..517e051 100644 --- a/internal/game/sys_science.go +++ b/internal/game/sys_science.go @@ -49,6 +49,7 @@ func (g *Game) LoadMods() error { if err := yaml.Unmarshal(data, &m); err != nil { return nil } + m.ID = id AllMods = append(AllMods, &m) return nil }) diff --git a/internal/game/sys_technology.go b/internal/game/sys_technology.go index 6afc757..fa0301f 100644 --- a/internal/game/sys_technology.go +++ b/internal/game/sys_technology.go @@ -50,6 +50,7 @@ func (g *Game) LoadTechs() error { if err := yaml.Unmarshal(data, &t); err != nil { return nil } + t.ID = id AllTechs = append(AllTechs, &t) return nil }) diff --git a/internal/game/ui_help.go b/internal/game/ui_help.go index d482fce..28f4371 100644 --- a/internal/game/ui_help.go +++ b/internal/game/ui_help.go @@ -45,6 +45,7 @@ var commandList = []cmdEntry{ {"fish", "Active", "Fish at fishing spots (Fishing)"}, {"fletch", "Free", "Fletch logs into bows (Fletching)"}, {"get / take / pick", "Active", "Pick up items from the ground"}, + {"global", "Instant", "Chat with everyone on the server"}, {"help", "Instant", "Show help topics"}, {"id / identify", "Active", "Identify herbs (Pharmacy)"}, {"inventory / i / inv", "Instant", "Show your inventory"}, @@ -78,6 +79,7 @@ var commandList = []cmdEntry{ {"use", "Active", "Use an object (crafting)"}, {"walk", "Active", "Pathfind to a room or multi-step walk"}, {"wear / wield", "Free", "Equip items"}, + {"who", "Instant", "List everyone online and their combat level"}, } func LoadHelp(dataDir string) ([]HelpDef, error) { diff --git a/internal/net/server.go b/internal/net/server.go index 6626592..5c98b62 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -9,6 +9,7 @@ import ( "sync" "thehouseoficarus/internal/action" + "thehouseoficarus/internal/color" "thehouseoficarus/internal/config" "thehouseoficarus/internal/player" ) @@ -329,6 +330,21 @@ func (s *Server) handleSession(sess *Session, handler func(*Session, string)) { } } +// wrap applies the player's wrap_width option to outgoing text. It is a no-op +// before login (no Player) so the welcome banner is never reflowed. The width +// is clamped to a minimum of 80 columns. +func (sess *Session) wrap(msg string) string { + if sess.Player == nil { + return msg + } + width := sess.Player.OptionInt("wrap_width") + if width < 80 { + width = 80 + } + return color.WrapANSI(msg, width) +} + +// Write sends raw text with no wrapping. Used for prompts and partial lines. func (sess *Session) Write(msg string) { sess.writeMu.Lock() defer sess.writeMu.Unlock() @@ -336,6 +352,7 @@ func (sess *Session) Write(msg string) { } func (sess *Session) WriteLine(msg string) { + msg = sess.wrap(msg) sess.writeMu.Lock() defer sess.writeMu.Unlock() sess.Conn.Write([]byte(msg + "\r\n")) @@ -345,14 +362,15 @@ func (sess *Session) WriteLines(lines ...string) { sess.writeMu.Lock() defer sess.writeMu.Unlock() for _, l := range lines { - sess.Conn.Write([]byte(l + "\r\n")) + sess.Conn.Write([]byte(sess.wrap(l) + "\r\n")) } } func (sess *Session) Writef(format string, args ...interface{}) { + msg := sess.wrap(fmt.Sprintf(format, args...)) sess.writeMu.Lock() defer sess.writeMu.Unlock() - sess.Conn.Write([]byte(fmt.Sprintf(format, args...))) + sess.Conn.Write([]byte(msg)) } func (sess *Session) Close() error { diff --git a/internal/object/item_store.go b/internal/object/item_store.go index 8bc85e4..e930220 100644 --- a/internal/object/item_store.go +++ b/internal/object/item_store.go @@ -40,6 +40,7 @@ func (s *ItemStore) Load(id string) (*ItemDef, error) { if err := yaml.Unmarshal(data, &def); err != nil { return nil, fmt.Errorf("parse item %s: %w", id, err) } + def.ID = id s.cache[id] = &def return &def, nil } @@ -56,6 +57,7 @@ func (s *ItemStore) LoadAll() ([]*ItemDef, error) { if err := yaml.Unmarshal(data, &def); err != nil { return fmt.Errorf("parse item %s: %w", id, err) } + def.ID = id s.cache[id] = &def defs = append(defs, &def) return nil diff --git a/internal/object/store.go b/internal/object/store.go index a11ff7a..cf8b4f9 100644 --- a/internal/object/store.go +++ b/internal/object/store.go @@ -39,6 +39,7 @@ func (s *ObjectStore) Load(id string) (*ObjectDef, error) { if err := yaml.Unmarshal(data, &def); err != nil { return nil, fmt.Errorf("parse object %s: %w", id, err) } + def.ID = id s.cache[id] = &def return &def, nil } diff --git a/internal/player/player.go b/internal/player/player.go index 4b74db7..2cc19bb 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -126,6 +126,7 @@ var OptionDefs = []OptionDef{ {"automap", OptBool, false, nil, "Show map automatically after moving"}, {"queue_silently", OptBool, true, nil, "Suppress messages for queued tick actions"}, {"room_desc_width", OptInt, 70, nil, "Maximum width for room descriptions"}, + {"wrap_width", OptInt, 120, nil, "Wrap all output to this many columns (minimum 80)"}, {"unicode", OptBool, true, nil, "Unicode box-drawing characters"}, {"run_countdown", OptBool, false, nil, "Show countdown messages when fleeing combat"}, {"visual_ticks", OptBool, false, nil, "Display a tick marker every game tick"}, diff --git a/internal/world/hazard.go b/internal/world/hazard.go index 396343b..1361cff 100644 --- a/internal/world/hazard.go +++ b/internal/world/hazard.go @@ -21,7 +21,6 @@ import ( // Tech protection (Kinetic Barrier / Projectile Screen / Neural Firewall) keys // off AttackType automatically. type HazardDef struct { - ID string `yaml:"id"` Name string `yaml:"name"` Description string `yaml:"description"` AttackType string `yaml:"attack_type"` // stab/slash/crush/ranged/science @@ -59,7 +58,6 @@ func (w *World) LoadHazard(id string) (*HazardDef, error) { if err := yaml.Unmarshal(data, &def); err != nil { return nil, fmt.Errorf("parse hazard %s: %w", id, err) } - def.ID = id w.hazardMu.Lock() w.hazardDefs[id] = &def |
