diff options
| author | historia <[not public]> | 2026-06-15 00:03:55 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-15 00:03:55 -0400 |
| commit | b6fdde0aa6fcefd735e7c550aaa7fca879023f1c (patch) | |
| tree | 924be72a906522d60512213cdbaba1f55e694bd3 | |
| parent | eb37418b933d7bd996658209e867ea3f3e3806ec (diff) | |
| download | thehouseoficarus-b6fdde0aa6fcefd735e7c550aaa7fca879023f1c.tar.gz | |
feat: unicode option, fixed up options and tables
| -rw-r--r-- | AGENTS.md | 2 | ||||
| -rw-r--r-- | TODO.md | 2 | ||||
| -rw-r--r-- | WORLDBUILDING.md | 10 | ||||
| -rw-r--r-- | data/help/queue_silently.yaml (renamed from data/help/queue_actions_silently.yaml) | 6 | ||||
| -rw-r--r-- | data/help/room_desc_width.yaml | 14 | ||||
| -rw-r--r-- | data/help/unicode.yaml | 14 | ||||
| -rw-r--r-- | internal/game/action_burn.go | 4 | ||||
| -rw-r--r-- | internal/game/action_gather.go | 9 | ||||
| -rw-r--r-- | internal/game/action_state.go | 7 | ||||
| -rw-r--r-- | internal/game/action_use.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_attack.go | 4 | ||||
| -rw-r--r-- | internal/game/cmd_look.go | 27 | ||||
| -rw-r--r-- | internal/game/cmd_map.go | 2 | ||||
| -rw-r--r-- | internal/game/cmd_option.go | 9 | ||||
| -rw-r--r-- | internal/game/game.go | 8 | ||||
| -rw-r--r-- | internal/game/map.go | 76 | ||||
| -rw-r--r-- | internal/game/map_test.go | 8 | ||||
| -rw-r--r-- | internal/game/table.go | 100 | ||||
| -rw-r--r-- | internal/game/tick.go | 4 | ||||
| -rw-r--r-- | internal/player/player.go | 16 |
20 files changed, 249 insertions, 75 deletions
@@ -201,7 +201,7 @@ Each 600ms tick, `ProcessQueuedCommands` in `internal/game/game.go`: **ActionState** (`internal/game/action_state.go`) tracks what a player is doing for `look` display. Timed actions (gather, combat, burn, stoke, use) persist the state until interrupted. One-tick actions (move, get, drop) clear after one tick. -Queue feedback is controlled by the `queue_actions_silently` player option (default on). +Queue feedback is controlled by the `queue_silently` player option (default on). ## Places to Be Careful @@ -214,7 +214,7 @@ See WORLDBUILDING.md for full examples of every data type with explanations. ### Phase 1: ASCII map implemented with BFS - [x] Replaced `toggle` command with `option`/`options` — supports bool (on/off), string (choice lists), and int option types -- [x] All old toggles migrated to the option system (tinymap, left-tinymap, xpdrops, exits, mobenter, mobleave, mobspawn, reserve, depletion, description) +- [x] All old toggles migrated to the option system (tiny_map, xp_drops, exits, mob_enter, mob_leave, mob_spawn, reserve, depletion, description) - [x] Added `color` option (none, ansi, xterm256) - [x] Added `mapwidth` and `mapheight` options for the `map` command viewport size - [x] `map` command — renders a full ASCII map centered on the player using BFS graph traversal with geometric coordinate assignment diff --git a/WORLDBUILDING.md b/WORLDBUILDING.md index 93fa16d..93c40aa 100644 --- a/WORLDBUILDING.md +++ b/WORLDBUILDING.md @@ -905,12 +905,12 @@ Players can toggle personal settings with the `toggle` command: | Toggle | Effect | |---|---| | `description` | Show full room description when moving | -| `tinymap` | Mini-map display (not yet implemented) | -| `xpdrops` | Show XP gained in gather/craft/combat messages | +| `tiny_map` | Mini-map display: `off`/`right`/`left` | +| `xp_drops` | Show XP gained in gather/craft/combat messages | | `exits` | Show exit destinations inline in look output | -| `mobenter` | Notify when a mob enters the room | -| `mobleave` | Notify when a mob leaves the room | -| `mobspawn` | Notify when a mob spawns in the area | +| `mob_enter` | Notify when a mob enters the room | +| `mob_leave` | Notify when a mob leaves the room | +| `mob_spawn` | Notify when a mob spawns in the area | | `reserve` | Show full reserved item details in look | | `depletion` | Show depletion and despawn timers on objects | diff --git a/data/help/queue_actions_silently.yaml b/data/help/queue_silently.yaml index 8101c5c..f1ca8b6 100644 --- a/data/help/queue_actions_silently.yaml +++ b/data/help/queue_silently.yaml @@ -1,4 +1,4 @@ -name: "queue_actions_silently" +name: "queue_silently" category: "Options" description: | Controls whether queued game actions produce confirmation messages. @@ -9,7 +9,7 @@ description: | When off: A brief "You prepare to X." message is shown for each queued action, confirming that it has been added to the queue. - Usage: option queue_actions_silently on - option queue_actions_silently off + Usage: option queue_silently on + option queue_silently off See also: help queued, help actions, help option diff --git a/data/help/room_desc_width.yaml b/data/help/room_desc_width.yaml new file mode 100644 index 0000000..7c67a38 --- /dev/null +++ b/data/help/room_desc_width.yaml @@ -0,0 +1,14 @@ +name: "room_desc_width" +category: "Options" +description: | + Maximum width for room description text when displayed alongside + the mini-map. + + Type: integer + Default: 70 + + When the tinymap option is enabled, room descriptions are wrapped + to this width and displayed side-by-side with the mini-map. + + Usage: option room_desc_width 80 + option room_desc_width 50 diff --git a/data/help/unicode.yaml b/data/help/unicode.yaml new file mode 100644 index 0000000..77cd847 --- /dev/null +++ b/data/help/unicode.yaml @@ -0,0 +1,14 @@ +name: "unicode" +category: "Options" +description: | + Enable unicode box-drawing characters for map and table display. + + Type: boolean (on/off) + Default: on + + When on, maps use box-drawing characters for borders and the + options table uses a formatted grid. When off, ASCII fallback + characters are used instead (. and : for maps, + and | for tables). + + Usage: option unicode off + option unicode on diff --git a/internal/game/action_burn.go b/internal/game/action_burn.go index 917a6fe..b16ba42 100644 --- a/internal/game/action_burn.go +++ b/internal/game/action_burn.go @@ -225,7 +225,7 @@ func (g *Game) advanceBurn(sess *net.Session, p *player.Player) { g.CancelAction(p) msg := fmt.Sprintf("You manage to get a fire going!") - if xp > 0 && p.OptionBool("xpdrops") { + if xp > 0 && p.OptionBool("xp_drops") { msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking]) } sess.WriteLine(msg) @@ -282,7 +282,7 @@ func (g *Game) advanceStoke(sess *net.Session, p *player.Player) { g.AccountStore.SaveCharacter(p) msg := fmt.Sprintf("You throw %s onto the fire.", name) - if xp > 0 && p.OptionBool("xpdrops") { + if xp > 0 && p.OptionBool("xp_drops") { msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.Firemaking]) } sess.WriteLine(msg) diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go index 512be81..4e1e609 100644 --- a/internal/game/action_gather.go +++ b/internal/game/action_gather.go @@ -138,7 +138,12 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje } } } - p.ActionState = &ActionState{Type: ActionGathering, TargetName: obj.Name, ToolName: toolName} + verb := cfg.Skill + switch cfg.Skill { + case "woodcutting": + verb = "chopping" + } + p.ActionState = &ActionState{Type: ActionGathering, TargetName: obj.Name, ToolName: toolName, Verb: verb} data := map[string]any{ "behavior_id": obj.BehaviorID, @@ -257,7 +262,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { if msg == "" { msg = fmt.Sprintf("You manage to get some %s.", itemName) } - if xp > 0 && p.OptionBool("xpdrops") { + if xp > 0 && p.OptionBool("xp_drops") { msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.SkillName(cfg.Skill)]) } sess.WriteLine(msg) diff --git a/internal/game/action_state.go b/internal/game/action_state.go index fba5319..8db4a7b 100644 --- a/internal/game/action_state.go +++ b/internal/game/action_state.go @@ -25,6 +25,7 @@ type ActionState struct { TargetName string ToolName string Direction string + Verb string } func (a *ActionState) Description() string { @@ -33,7 +34,11 @@ func (a *ActionState) Description() string { } switch a.Type { case ActionGathering: - return "mining a " + a.TargetName + desc := a.Verb + " a " + a.TargetName + if a.ToolName != "" { + desc += " with a " + a.ToolName + } + return desc case ActionCombating: return "fighting a " + a.TargetName case ActionMoving: diff --git a/internal/game/action_use.go b/internal/game/action_use.go index 8987ee0..ec63723 100644 --- a/internal/game/action_use.go +++ b/internal/game/action_use.go @@ -108,7 +108,7 @@ func (g *Game) advanceUse(sess *net.Session, p *player.Player) { itemName = def.Name } line := fmt.Sprintf("You make a %s.", itemName) - if cfg.XP > 0 && p.OptionBool("xpdrops") { + if cfg.XP > 0 && p.OptionBool("xp_drops") { line += fmt.Sprintf(" (+%dxp %s)", cfg.XP, player.SkillAbbr[player.SkillName(cfg.Skill)]) } sess.WriteLine(line) diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go index ef80189..ca5bc4c 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -215,7 +215,7 @@ func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobI prefix := fmt.Sprintf(" You hit %s for %d damage.", mobName, dmg) hpPart := fmt.Sprintf("[%d/%dhp]", mob.HP, mob.MaxHP) line := fmt.Sprintf("%-*s %s", g.combatPadWidth, prefix, hpPart) - if p.OptionBool("xpdrops") && len(gains) > 0 { + if p.OptionBool("xp_drops") && len(gains) > 0 { var parts []string for _, gain := range gains { parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)])) @@ -462,7 +462,7 @@ func (g *Game) respawnMob(instanceID string) { if g.Hub != nil { for _, sess := range g.Hub.PlayersInRoom(homeRoom) { - if p, ok := sess.Player.(*player.Player); ok && p.OptionBool("mobspawn") { + if p, ok := sess.Player.(*player.Player); ok && p.OptionBool("mob_spawn") { sess.WriteLine(fmt.Sprintf("\n%s (level %d) spawns in the area.", mobDisplayName(inst, false), mobCombatLevel(inst))) } } diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 3b81f80..7efb65a 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -25,20 +25,23 @@ func (g *Game) doLook(sess *net.Session) { room.Name, ) - mapWidth := p.OptionInt("max_room_description_width") - if mapWidth <= 0 { - mapWidth = 70 + descWidth := p.OptionInt("room_desc_width") + if descWidth <= 0 { + descWidth = 70 } - if p.OptionBool("tinymap") && mapWidth > 0 { - descLines := wrapText(room.Description, mapWidth) - mapLines := buildTinyMap(g, p.RoomID) + descLines := wrapText(room.Description, descWidth) + wroteDesc := false + if p.OptionString("tiny_map") != "off" { + mapLines := buildTinyMap(g, p.RoomID, mapGlyphsForPlayer(p.OptionBool("unicode"))) if len(mapLines) == 7 { g.writeLookSideBySide(sess, p, descLines, mapLines) - } else { - sess.WriteLine(room.Description) + wroteDesc = true + } + } + if !wroteDesc { + for _, line := range descLines { + sess.WriteLine(line) } - } else { - sess.WriteLine(room.Description) } mobs := g.MobStore.MobsInRoom(p.RoomID) @@ -574,8 +577,8 @@ func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, descLine if len(mapLines) > total { total = len(mapLines) } - leftMap := p.OptionBool("left_tinymap") - mapWidth := p.OptionInt("max_room_description_width") + leftMap := p.OptionString("tiny_map") == "left" + mapWidth := p.OptionInt("room_desc_width") if mapWidth <= 0 { mapWidth = 70 } diff --git a/internal/game/cmd_map.go b/internal/game/cmd_map.go index e0413d3..f4ec11c 100644 --- a/internal/game/cmd_map.go +++ b/internal/game/cmd_map.go @@ -16,7 +16,7 @@ func (g *Game) doMap(sess *net.Session) { mapHeight = 5 } - lines := buildFullMap(g, p.RoomID, mapWidth, mapHeight) + lines := buildFullMap(g, p.RoomID, mapWidth, mapHeight, mapGlyphsForPlayer(p.OptionBool("unicode"))) mode := p.OptionString("mappadding") if mode == "none" || mode == "x" { diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go index 49085db..9302baa 100644 --- a/internal/game/cmd_option.go +++ b/internal/game/cmd_option.go @@ -14,11 +14,16 @@ func (g *Game) doOption(sess *net.Session, input string) { if input == "" { sess.WriteLine("") - sess.WriteLine(fmt.Sprintf(" %-14s %-10s %-22s %s", "Option", "Value", "Valid", "Description")) + table := &Table{ + Columns: []string{"Option", "Value", "Valid", "Description"}, + } for _, def := range player.OptionDefs { val := formatOptionValue(p, &def) valid := formatValidValues(&def) - sess.WriteLine(fmt.Sprintf(" %-14s %-10s %-22s %s", def.Name, val, valid, def.Description)) + table.Rows = append(table.Rows, []string{def.Name, val, valid, def.Description}) + } + for _, line := range table.Render(p.OptionBool("unicode")) { + sess.WriteLine(line) } return } diff --git a/internal/game/game.go b/internal/game/game.go index 37a1f3f..7447215 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -177,10 +177,9 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { Args: strings.Join(parts[1:], " "), Timestamp: time.Now(), }) - if !p.OptionBool("queue_actions_silently") { + if !p.OptionBool("queue_silently") { sess.WriteLine(fmt.Sprintf("\nYou prepare to %s.", cmd)) } - sess.Write("\r\n> ") return } @@ -191,10 +190,9 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { Timestamp: time.Now(), } g.cancelRest(p.Name) - if !p.OptionBool("queue_actions_silently") { + if !p.OptionBool("queue_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) { @@ -401,6 +399,7 @@ func (g *Game) ProcessQueuedCommands() { if len(parts) > 0 { g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args) } + qc.Session.Write("\r\n> ") } delete(g.freeQueue, p.Name) } @@ -418,6 +417,7 @@ func (g *Game) ProcessQueuedCommands() { if len(parts) > 0 { g.executeCommand(qc.Session, parts[0], parts[1:], qc.Command+" "+qc.Args) } + qc.Session.Write("\r\n> ") } g.activeQueue = make(map[string]*QueuedCommand) diff --git a/internal/game/map.go b/internal/game/map.go index e7738bd..8b14e25 100644 --- a/internal/game/map.go +++ b/internal/game/map.go @@ -6,6 +6,30 @@ import ( "thirdcollapse/internal/world" ) +type mapGlyphs struct { + topLeft, topRight rune + bottomLeft, bottomRight rune + side rune + topFill rune + connectorH, connectorV rune + upArrow, downArrow rune +} + +func mapGlyphsForPlayer(unicode bool) mapGlyphs { + if unicode { + return mapGlyphs{ + topLeft: '╔', topRight: '╗', bottomLeft: '╚', bottomRight: '╝', + side: '║', topFill: '═', connectorH: '─', connectorV: '│', + upArrow: '↑', downArrow: '↓', + } + } + return mapGlyphs{ + topLeft: '.', topRight: '.', bottomLeft: ':', bottomRight: ':', + side: ':', topFill: '.', connectorH: '-', connectorV: '|', + upArrow: '^', downArrow: 'v', + } +} + type mapGraph struct { posToRoom map[[2]int]int roomToPos map[int][2]int @@ -62,8 +86,8 @@ func buildGraph(g *Game, startRoomID int) *mapGraph { return mg } -func buildTinyMap(g *Game, roomID int) []string { - mg := buildGraph(g, roomID) +func buildTinyMap(g *Game, roomID int, mg mapGlyphs) []string { + bg := buildGraph(g, roomID) grid := make([][]rune, 5) for i := range grid { @@ -76,7 +100,7 @@ func buildTinyMap(g *Game, roomID int) []string { for y := -1; y <= 1; y++ { for x := -1; x <= 1; x++ { pos := [2]int{x, y} - rid, ok := mg.posToRoom[pos] + rid, ok := bg.posToRoom[pos] if !ok { continue } @@ -94,13 +118,13 @@ func buildTinyMap(g *Game, roomID int) []string { for x := -1; x <= 0; x++ { leftPos := [2]int{x, y} rightPos := [2]int{x + 1, y} - leftRoom, leftOK := mg.posToRoom[leftPos] - rightRoom, rightOK := mg.posToRoom[rightPos] + leftRoom, leftOK := bg.posToRoom[leftPos] + rightRoom, rightOK := bg.posToRoom[rightPos] if !leftOK || !rightOK { continue } if exitsConnect(g, leftRoom, rightRoom, world.East, world.West) { - grid[(y+1)*2][(x+1)*2+1] = '─' + grid[(y+1)*2][(x+1)*2+1] = mg.connectorH } } } @@ -109,13 +133,13 @@ func buildTinyMap(g *Game, roomID int) []string { for x := -1; x <= 1; x++ { topPos := [2]int{x, y} bottomPos := [2]int{x, y + 1} - topRoom, topOK := mg.posToRoom[topPos] - bottomRoom, bottomOK := mg.posToRoom[bottomPos] + topRoom, topOK := bg.posToRoom[topPos] + bottomRoom, bottomOK := bg.posToRoom[bottomPos] if !topOK || !bottomOK { continue } if exitsConnect(g, topRoom, bottomRoom, world.South, world.North) { - grid[(y+1)*2+1][(x+1)*2] = '│' + grid[(y+1)*2+1][(x+1)*2] = mg.connectorV } } } @@ -123,19 +147,21 @@ func buildTinyMap(g *Game, roomID int) []string { cur, _ := loadRoom(g, roomID) if cur != nil { if _, ok := exitTarget(cur, world.Up); ok { - grid[1][3] = '↑' + grid[1][3] = mg.upArrow } if _, ok := exitTarget(cur, world.Down); ok { - grid[3][1] = '↓' + grid[3][1] = mg.downArrow } } + topFill := strings.Repeat(string(mg.topFill), 5) lines := make([]string, 7) - lines[0] = "╔═════╗" + lines[0] = string(mg.topLeft) + topFill + string(mg.topRight) for row := 0; row < 5; row++ { - lines[row+1] = "║" + string(grid[row]) + "║" + lines[row+1] = string(mg.side) + string(grid[row]) + string(mg.side) } - lines[6] = "╚═════╝" + botFill := strings.Repeat(string(mg.topFill), 5) + lines[6] = string(mg.bottomLeft) + botFill + string(mg.bottomRight) return lines } @@ -152,10 +178,8 @@ func exitsConnect(g *Game, room1, room2 int, dir12, dir21 world.ExitDir) bool { if !ok { return false } - if id, ok := exitTarget(r2, dir21); ok && id == room1 { - return true - } - return false + id, ok := exitTarget(r2, dir21) + return ok && id == room1 } func exitTarget(room *world.Room, dir world.ExitDir) (int, bool) { @@ -234,8 +258,8 @@ func leftTrimCommon(lines []string) []string { return result } -func buildFullMap(g *Game, roomID, mapWidth, mapHeight int) []string { - mg := buildGraph(g, roomID) +func buildFullMap(g *Game, roomID, mapWidth, mapHeight int, mg mapGlyphs) []string { + bg := buildGraph(g, roomID) grid := make([][]rune, mapHeight) for i := range grid { @@ -248,7 +272,7 @@ func buildFullMap(g *Game, roomID, mapWidth, mapHeight int) []string { cx := mapWidth / 2 cy := mapHeight / 2 - for pos, rid := range mg.posToRoom { + for pos, rid := range bg.posToRoom { gr := cy + pos[1]*2 gc := cx + pos[0]*2 if gr < 0 || gr >= mapHeight || gc < 0 || gc >= mapWidth { @@ -261,25 +285,25 @@ func buildFullMap(g *Game, roomID, mapWidth, mapHeight int) []string { } } - for pos, rid := range mg.posToRoom { + for pos, rid := range bg.posToRoom { x, y := pos[0], pos[1] - if rightID, ok := mg.posToRoom[[2]int{x + 1, y}]; ok { + if rightID, ok := bg.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] = '─' + grid[gr][gc] = mg.connectorH } } } - if bottomID, ok := mg.posToRoom[[2]int{x, y + 1}]; ok { + if bottomID, ok := bg.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] = '│' + grid[gr][gc] = mg.connectorV } } } diff --git a/internal/game/map_test.go b/internal/game/map_test.go index 3453117..8c82f4a 100644 --- a/internal/game/map_test.go +++ b/internal/game/map_test.go @@ -12,6 +12,8 @@ func TestBuildTinyMap(t *testing.T) { World: world.New("../../data"), } + mg := mapGlyphsForPlayer(true) + tests := []struct { name string roomID int @@ -49,7 +51,7 @@ func TestBuildTinyMap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - lines := buildTinyMap(g, tt.roomID) + lines := buildTinyMap(g, tt.roomID, mg) if len(lines) != 7 { t.Fatalf("expected 7 lines, got %d", len(lines)) } @@ -97,6 +99,8 @@ func TestBuildFullMap(t *testing.T) { World: world.New("../../data"), } + mg := mapGlyphsForPlayer(true) + tests := []struct { name string roomID int @@ -111,7 +115,7 @@ func TestBuildFullMap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - lines := buildFullMap(g, tt.roomID, tt.width, tt.height) + lines := buildFullMap(g, tt.roomID, tt.width, tt.height, mg) if len(lines) != tt.height { t.Errorf("expected %d lines, got %d", tt.height, len(lines)) } diff --git a/internal/game/table.go b/internal/game/table.go new file mode 100644 index 0000000..e3dad97 --- /dev/null +++ b/internal/game/table.go @@ -0,0 +1,100 @@ +package game + +import ( + "fmt" + "strings" +) + +type tableGlyphs struct { + topLeft, topSep, topRight rune + side, colSep rune + sepLeft, sepCross, sepRight rune + botLeft, botSep, botRight rune + fillH, fillHSep rune +} + +func tableGlyphSet(unicode bool) tableGlyphs { + if unicode { + return tableGlyphs{ + topLeft: '╔', topSep: '╤', topRight: '╗', side: '║', colSep: '│', + sepLeft: '╟', sepCross: '┼', sepRight: '╢', + botLeft: '╚', botSep: '╧', botRight: '╝', fillH: '═', fillHSep: '─', + } + } + return tableGlyphs{ + topLeft: '+', topSep: '+', topRight: '+', side: '|', colSep: '|', + sepLeft: '+', sepCross: '+', sepRight: '+', + botLeft: '+', botSep: '+', botRight: '+', fillH: '-', fillHSep: '-', + } +} + +type Table struct { + Columns []string + Rows [][]string +} + +func (t *Table) Render(unicode bool) []string { + g := tableGlyphSet(unicode) + + nCols := len(t.Columns) + if nCols == 0 { + return nil + } + + colWidths := make([]int, nCols) + for i, h := range t.Columns { + colWidths[i] = len(h) + } + for _, row := range t.Rows { + for i, cell := range row { + if i >= nCols { + break + } + if len(cell) > colWidths[i] { + colWidths[i] = len(cell) + } + } + } + + makeSep := func(left, cross, right rune, fill rune) string { + var b strings.Builder + b.WriteRune(left) + for i := 0; i < nCols; i++ { + if i > 0 { + b.WriteRune(cross) + } + b.WriteString(strings.Repeat(string(fill), colWidths[i]+2)) + } + b.WriteRune(right) + return b.String() + } + + makeRow := func(cells []string) string { + var b strings.Builder + b.WriteRune(g.side) + for i := 0; i < nCols; i++ { + if i > 0 { + b.WriteRune(g.colSep) + } + cell := "" + if i < len(cells) { + cell = cells[i] + } + b.WriteString(fmt.Sprintf(" %-*s ", colWidths[i], cell)) + } + b.WriteRune(g.side) + return b.String() + } + + var out []string + + out = append(out, makeSep(g.topLeft, g.topSep, g.topRight, g.fillH)) + out = append(out, makeRow(t.Columns)) + out = append(out, makeSep(g.sepLeft, g.sepCross, g.sepRight, g.fillHSep)) + for _, row := range t.Rows { + out = append(out, makeRow(row)) + } + out = append(out, makeSep(g.botLeft, g.botSep, g.botRight, g.fillH)) + + return out +} diff --git a/internal/game/tick.go b/internal/game/tick.go index 4d037ec..767109e 100644 --- a/internal/game/tick.go +++ b/internal/game/tick.go @@ -126,14 +126,14 @@ func (g *Game) WanderTick() { if !ok { continue } - if p.RoomID == m.fromRoom && p.OptionBool("mobleave") { + if p.RoomID == m.fromRoom && p.OptionBool("mob_leave") { if m.level > 0 { sess.WriteLine(fmt.Sprintf("\n%s (level %d) leaves.", m.name, m.level)) } else { sess.WriteLine(fmt.Sprintf("\n%s moves away.", m.name)) } } - if p.RoomID == m.toRoom && p.OptionBool("mobenter") { + if p.RoomID == m.toRoom && p.OptionBool("mob_enter") { if m.level > 0 { sess.WriteLine(fmt.Sprintf("\n%s (level %d) enters.", m.name, m.level)) } else { diff --git a/internal/player/player.go b/internal/player/player.go index 15521a3..95cf948 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -106,13 +106,12 @@ type OptionDef struct { var OptionDefs = []OptionDef{ {"description", OptBool, true, nil, "Long room descriptions when moving"}, - {"tinymap", OptBool, true, nil, "Mini-map display"}, - {"left_tinymap", OptBool, false, nil, "Mini-map on left side of descriptions"}, - {"xpdrops", OptBool, true, nil, "XP drop messages"}, + {"tiny_map", OptString, "right", []string{"off", "right", "left"}, "Mini-map display (off/right/left)"}, + {"xp_drops", OptBool, true, nil, "XP drop messages"}, {"exits", OptBool, true, nil, "Long exit display in look"}, - {"mobenter", OptBool, true, nil, "Messages when mobs enter the room"}, - {"mobleave", OptBool, true, nil, "Messages when mobs leave the room"}, - {"mobspawn", OptBool, true, nil, "Messages when mobs spawn in the area"}, + {"mob_enter", OptBool, true, nil, "Messages when mobs enter the room"}, + {"mob_leave", OptBool, true, nil, "Messages when mobs leave the room"}, + {"mob_spawn", OptBool, true, nil, "Messages when mobs spawn in the area"}, {"reserve", OptBool, true, nil, "Show full reserved item details"}, {"depletion", OptBool, false, nil, "Show depletion and despawn timers"}, {"despawn", OptBool, false, nil, "Show ground item despawn timers"}, @@ -121,8 +120,9 @@ var OptionDefs = []OptionDef{ {"mapheight", OptInt, 20, nil, "Map height for the map command"}, {"mappadding", OptString, "none", []string{"none", "x", "y", "xy"}, "Map output padding mode"}, {"automap", OptBool, false, nil, "Show map automatically after moving"}, - {"queue_actions_silently", OptBool, true, nil, "Suppress 'You prepare to...' messages for queued actions"}, - {"max_room_description_width", OptInt, 70, nil, "Maximum width for room descriptions"}, + {"queue_silently", OptBool, true, nil, "Suppress messages for queued tick actions"}, + {"room_desc_width", OptInt, 70, nil, "Maximum width for room descriptions"}, + {"unicode", OptBool, true, nil, "Unicode box-drawing characters"}, } var optionByName map[string]*OptionDef |
