diff options
| author | historia <[not public]> | 2026-06-11 20:41:06 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-11 20:41:06 -0400 |
| commit | d6ab0e93a64525ce34f89e26d5272efbac513c32 (patch) | |
| tree | d0ec59a72a32a15d0917d9c6c3bfa2f2a424c1f1 | |
| parent | 6b2b4bf470b655f01c5a21c5f558a6102402c214 (diff) | |
| download | thehouseoficarus-d6ab0e93a64525ce34f89e26d5272efbac513c32.tar.gz | |
feat: big map and options changes
73 files changed, 1012 insertions, 120 deletions
@@ -195,7 +195,7 @@ See WORLDBUILDING.md for full examples of every data type with explanations. ### Phase 1: Tiny ASCII Map -- [ ] Limit the room description to 70 characters wide. Add a setting named "maxWidth" in an appropriate section of config.yaml along with comment explaining it. +- [x] Limit the room description to 70 characters wide. Add a setting named "maxWidth" in an appropriate section of config.yaml along with comment explaining it. Next to the block of room description text should be an ASCII mini-map showing the current room as @ and a grid of the rooms and connections surrounding it. A full-size ASCII map for the "map" command will eventually be implemented so keep that in mind for methodology and potential functions we can reuse. Each line of the map should print on the maxWidth+2 character after the description (so there's always at least one space between the description and map) @@ -246,11 +246,11 @@ voices, a symphony of commerce and humanity that hasn't changed in â•‘o—o†generations. Overhead, September clouds drift lazily across the sky, â•‘| |↑|â•‘ occasionally releasing weak sunlight that transforms the ancient â•‘o—@—oâ•‘ stone beneath countless feet into patches of burnished gold. â•‘|↓| |â•‘ - â•‘o—o—oâ•‘ - ╚â•â•â•â•â•â• + â•‘o—o—oâ•‘ + ╚â•â•â•â•â•â• -- [ ] This is a feature that can be toggled with the "tinymap" toggle. If the toggle is off, don't print the map at all. -- [ ] Add a toggle "left-tinymap". If this is enabled, the map should be printed on each line before the room description. For example: +- [x] This is a feature that can be toggled with the "tinymap" toggle. If the toggle is off, don't print the map at all. +- [x] Add a toggle "left-tinymap". If this is enabled, the map should be printed on each line before the room description. For example: Central Market â•”â•â•â•â•â•â•— The market square pulses with the unbridled energy of a thousand @@ -261,6 +261,17 @@ Central Market â•‘o—o—oâ•‘ ╚â•â•â•â•â•â• +### Phase 1b: Map & Option System + +- [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] 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 +- [x] Shared BFS graph builder (`buildGraph`) reused by both the tiny map (in `look`) and the `map` command +- [x] Added `mappadding` option (none, x, y, xy) — controls blank-row stripping and left-trimming of map output +- [x] Tiny map built via BFS viewport rendering (3x3 room window at 2x zoom) — no hardcoded corner/cardinal rules, no room duplication + ### Phase 2: Core Skill Chains (breadth over depth) - [x] Woodcutting — tree objects + axes, shared depletion, bird's nests diff --git a/data/help/automap.yaml b/data/help/automap.yaml new file mode 100644 index 0000000..32b84cb --- /dev/null +++ b/data/help/automap.yaml @@ -0,0 +1,13 @@ +name: "automap" +category: "Options" +description: | + Show the full map automatically after each movement. + + Type: boolean (on/off) + Default: off + + When enabled, the map command output is printed after each + room movement, so you always know where you are. + + Map size and padding are controlled by the mapwidth, + mapheight, and mappadding options. diff --git a/data/help/color.yaml b/data/help/color.yaml new file mode 100644 index 0000000..8171ab9 --- /dev/null +++ b/data/help/color.yaml @@ -0,0 +1,13 @@ +name: "color" +category: "Options" +description: | + Color output mode. + + Type: string + Default: none + Values: none, ansi, xterm256 + + Controls the color output format used by the game. + none - No color output + ansi - Standard 16-color ANSI codes + xterm256 - 256-color xterm codes diff --git a/data/help/depletion.yaml b/data/help/depletion.yaml new file mode 100644 index 0000000..d4d8390 --- /dev/null +++ b/data/help/depletion.yaml @@ -0,0 +1,11 @@ +name: "depletion" +category: "Options" +description: | + Show depletion and despawn timers on objects. + + Type: boolean (on/off) + Default: off + + When enabled, resource objects (rocks, trees, etc.) + show their remaining despawn or respawn timers in + the room look output. diff --git a/data/help/description.yaml b/data/help/description.yaml index ab6afa1..3436c5b 100644 --- a/data/help/description.yaml +++ b/data/help/description.yaml @@ -1,11 +1,12 @@ name: "description" -category: "Character" +category: "Options" description: | - View or change your character description. + Show full room descriptions when moving between rooms. - Usage: description, desc + Type: boolean (on/off) + Default: on - Shows your current description (if any) and prompts you to enter a new - one. Other players can see your description by looking at you with - "look <name>". Press enter without typing anything to keep your - current description. + When enabled, moving into a new room shows the full + room look output including description, mobs, objects, + items, and exits. + When disabled, only the room name is shown on move. diff --git a/data/help/exits.yaml b/data/help/exits.yaml index 673e789..fbfe3a6 100644 --- a/data/help/exits.yaml +++ b/data/help/exits.yaml @@ -1,9 +1,11 @@ name: "exits" -category: "Exploration" +category: "Options" description: | - List exits from the current room. + Show exit destinations in look output. - Usage: exits + Type: boolean (on/off) + Default: on - Shows each exit and the name of the room it leads to. Also toggleable - via "toggle exits" to show destinations inline in the look output. + When enabled, look shows each exit direction and the + name of the room it leads to. + When disabled, only the exit directions are listed. diff --git a/data/help/left-tinymap.yaml b/data/help/left-tinymap.yaml new file mode 100644 index 0000000..689aa89 --- /dev/null +++ b/data/help/left-tinymap.yaml @@ -0,0 +1,11 @@ +name: "left-tinymap" +category: "Options" +description: | + Show the mini-map on the left side of room descriptions. + + Type: boolean (on/off) + Default: off + + When both tinymap and left-tinymap are enabled, the + mini-map appears on the left side of the room description + rather than the right. diff --git a/data/help/map.yaml b/data/help/map.yaml new file mode 100644 index 0000000..3b4917a --- /dev/null +++ b/data/help/map.yaml @@ -0,0 +1,23 @@ +name: "map" +category: "Exploration" +description: | + Display a full map of the surrounding area. + + Usage: map + + The map shows all explored rooms and their connections, + centered on your current location. You are shown as @. + + Map size is controlled by: + option mapwidth <num> (default 30) + option mapheight <num> (default 20) + + Padding/trimming is controlled by: + option mappadding <mode> + none - strip blank lines + left-trim (default) + x - strip blank lines only + y - left-trim only + xy - no trimming + + To show the map automatically after each move: + option automap on diff --git a/data/help/mapheight.yaml b/data/help/mapheight.yaml new file mode 100644 index 0000000..ee3920b --- /dev/null +++ b/data/help/mapheight.yaml @@ -0,0 +1,10 @@ +name: "mapheight" +category: "Options" +description: | + Height of the full map display in characters. + + Type: integer + Default: 20 + + Controls how many rows tall the map command + output is. Must be at least 5. diff --git a/data/help/mappadding.yaml b/data/help/mappadding.yaml new file mode 100644 index 0000000..201ef53 --- /dev/null +++ b/data/help/mappadding.yaml @@ -0,0 +1,13 @@ +name: "mappadding" +category: "Options" +description: | + Controls how the map command trims its output. + + Type: string + Default: none + Values: none, x, y, xy + + none - Strip blank rows and left-trim whitespace + x - Strip blank rows only (horizontal trim) + y - Left-trim whitespace only (vertical trim) + xy - No trimming (raw output) diff --git a/data/help/mapwidth.yaml b/data/help/mapwidth.yaml new file mode 100644 index 0000000..7e372ef --- /dev/null +++ b/data/help/mapwidth.yaml @@ -0,0 +1,10 @@ +name: "mapwidth" +category: "Options" +description: | + Width of the full map display in characters. + + Type: integer + Default: 30 + + Controls how many columns wide the map command + output is. Must be at least 5. diff --git a/data/help/mobenter.yaml b/data/help/mobenter.yaml new file mode 100644 index 0000000..529a5a5 --- /dev/null +++ b/data/help/mobenter.yaml @@ -0,0 +1,10 @@ +name: "mobenter" +category: "Options" +description: | + Show messages when mobs enter the room. + + Type: boolean (on/off) + Default: on + + When enabled, you are notified when a mob wanders + into your current room. diff --git a/data/help/mobleave.yaml b/data/help/mobleave.yaml new file mode 100644 index 0000000..06ddeab --- /dev/null +++ b/data/help/mobleave.yaml @@ -0,0 +1,10 @@ +name: "mobleave" +category: "Options" +description: | + Show messages when mobs leave the room. + + Type: boolean (on/off) + Default: on + + When enabled, you are notified when a mob wanders + out of your current room. diff --git a/data/help/mobspawn.yaml b/data/help/mobspawn.yaml new file mode 100644 index 0000000..d6c096f --- /dev/null +++ b/data/help/mobspawn.yaml @@ -0,0 +1,10 @@ +name: "mobspawn" +category: "Options" +description: | + Show messages when mobs spawn in the area. + + Type: boolean (on/off) + Default: on + + When enabled, you are notified when a mob respawns + anywhere in the area. diff --git a/data/help/option.yaml b/data/help/option.yaml new file mode 100644 index 0000000..1948b7a --- /dev/null +++ b/data/help/option.yaml @@ -0,0 +1,20 @@ +name: "option" +category: "General" +description: | + View or change character settings. + + Usage: option - List all options and their current values + options - Same as option + option <name> - Show details for a specific option + option <name> <val> - Set an option to a value + + Boolean options accept on/off, true/false, yes/no, or 1/0. + String options show available choices with option <name>. + Numeric options accept any integer. + + Examples: + option tinymap off + option color ansi + option mapwidth 50 + + Use help <option> for details on a specific option. diff --git a/data/help/reserve.yaml b/data/help/reserve.yaml new file mode 100644 index 0000000..e7308fd --- /dev/null +++ b/data/help/reserve.yaml @@ -0,0 +1,11 @@ +name: "reserve" +category: "Options" +description: | + Show full reserved item details in look. + + Type: boolean (on/off) + Default: on + + When enabled, reserved ground items show the player + name and remaining ticks. + When disabled, reserved items just show "(reserved)". diff --git a/data/help/tinymap.yaml b/data/help/tinymap.yaml new file mode 100644 index 0000000..cbff629 --- /dev/null +++ b/data/help/tinymap.yaml @@ -0,0 +1,11 @@ +name: "tinymap" +category: "Options" +description: | + Show the mini-map alongside room descriptions in look. + + Type: boolean (on/off) + Default: on + + When enabled, a 3x3 mini-map is displayed next to room + descriptions. Your current room is shown as @, with + surrounding rooms and connections. diff --git a/data/help/toggle.yaml b/data/help/toggle.yaml deleted file mode 100644 index 27080be..0000000 --- a/data/help/toggle.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: "toggle" -category: "General" -description: | - Toggle character settings on or off. - - Usage: toggle - List all toggles and their current status - toggle <name> - Toggle a setting on or off - - Toggles: - description - Long room descriptions when moving between rooms - tinymap - Mini-map display (not yet implemented) - xpdrops - Show XP gained in gather, crafting, and combat messages - exits - Show exit destinations 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 - reserve - Show full reserved item details in look - depletion - Show depletion and despawn timers on objects diff --git a/data/help/xpdrops.yaml b/data/help/xpdrops.yaml new file mode 100644 index 0000000..f873a85 --- /dev/null +++ b/data/help/xpdrops.yaml @@ -0,0 +1,10 @@ +name: "xpdrops" +category: "Options" +description: | + Show XP gained in gather, crafting, and combat messages. + + Type: boolean (on/off) + Default: on + + When enabled, XP gains are appended to action messages. + For example: "You get some copper ore. (+10xp min)" diff --git a/data/rooms/1.yaml b/data/rooms/1.yaml index c8d8aa1..ba42be0 100644 --- a/data/rooms/1.yaml +++ b/data/rooms/1.yaml @@ -5,7 +5,7 @@ map_symbol: "S" exits: east: 2 west: 22 - up: 30 + up: 100 down: 31 objects: - id: lumby_fountain diff --git a/data/rooms/100.yaml b/data/rooms/100.yaml new file mode 100644 index 0000000..b8b0f1e --- /dev/null +++ b/data/rooms/100.yaml @@ -0,0 +1,7 @@ +id: 100 +name: "Grand Concourse" +description: "The expansive white platform of Station X1's main thoroughfare. Neon strips pulse along the ceiling, reflecting off polished permacrete floors. Citizens and synthetics stream past in a constant dance of commerce and purpose." +map_symbol: "+" +exits: + east: 101 + north: 110 diff --git a/data/rooms/101.yaml b/data/rooms/101.yaml new file mode 100644 index 0000000..fa5af97 --- /dev/null +++ b/data/rooms/101.yaml @@ -0,0 +1,8 @@ +id: 101 +name: "Customs & Immigration" +description: "A long hall lined with automated kiosks. Holographic scanners bathe travelers in cool blue light as bio-signatures are verified. The air smells of ozone and recycled stationery." +map_symbol: "#" +exits: + west: 100 + east: 102 + north: 111 diff --git a/data/rooms/102.yaml b/data/rooms/102.yaml new file mode 100644 index 0000000..1bc8573 --- /dev/null +++ b/data/rooms/102.yaml @@ -0,0 +1,8 @@ +id: 102 +name: "Port Authority Office" +description: "Cluttered desks float in low gravity, terminals displaying shipping manifests and docking schedules. A large viewscreen shows the constant traffic of orbital freighters beyond the station." +map_symbol: "P" +exits: + west: 101 + east: 103 + north: 112 diff --git a/data/rooms/103.yaml b/data/rooms/103.yaml new file mode 100644 index 0000000..f216fc4 --- /dev/null +++ b/data/rooms/103.yaml @@ -0,0 +1,8 @@ +id: 103 +name: "Interstellar Cargo Bay 3" +description: "Massive crates stamped with corporate logos are stacked to the ceiling. Loading clamps hum with magnetic tension. Echoes of distant announcements reverberate through the spacious bay." +map_symbol: "C" +exits: + west: 102 + east: 104 + north: 113 diff --git a/data/rooms/104.yaml b/data/rooms/104.yaml new file mode 100644 index 0000000..c7821c1 --- /dev/null +++ b/data/rooms/104.yaml @@ -0,0 +1,7 @@ +id: 104 +name: "Freight Elevator Lobby" +description: "A utilitarian chamber where cargo lifters descend into the station's underbelly. Indicator lights blink in sequence as massive elevator doors cycle open and closed with hydraulic sighs." +map_symbol: "E" +exits: + west: 103 + east: 105 diff --git a/data/rooms/105.yaml b/data/rooms/105.yaml new file mode 100644 index 0000000..f07c8aa --- /dev/null +++ b/data/rooms/105.yaml @@ -0,0 +1,8 @@ +id: 105 +name: "Hydroponic Distribution Hub" +description: "Towering racks of fresh produce await sorting. Leafy greens spill from aeroponic towers as workers in sterile suits package the station's food supply for distribution." +map_symbol: "H" +exits: + west: 104 + east: 106 + north: 115 diff --git a/data/rooms/106.yaml b/data/rooms/106.yaml new file mode 100644 index 0000000..a264b01 --- /dev/null +++ b/data/rooms/106.yaml @@ -0,0 +1,7 @@ +id: 106 +name: "Recycled Water Processing" +description: "The air is humid and warm. Pipes of all sizes crisscross the ceiling, carrying reclaimed water through filtration stages. The low rumble of pumps is a constant companion." +map_symbol: "W" +exits: + west: 105 + east: 107 diff --git a/data/rooms/107.yaml b/data/rooms/107.yaml new file mode 100644 index 0000000..34401bc --- /dev/null +++ b/data/rooms/107.yaml @@ -0,0 +1,8 @@ +id: 107 +name: "Atmospheric Scrubber Array" +description: "Wall-mounted scrubbers cycle station air through banks of moss and activated carbon. Green LED strips indicate optimal CO2 absorption. The room breathes like a mechanical lung." +map_symbol: "S" +exits: + west: 106 + east: 108 + north: 117 diff --git a/data/rooms/108.yaml b/data/rooms/108.yaml new file mode 100644 index 0000000..3f6914a --- /dev/null +++ b/data/rooms/108.yaml @@ -0,0 +1,7 @@ +id: 108 +name: "Power Relay Station Gamma" +description: "Crackling conduits channel raw solar energy from the exterior arrays. Warning signs in three languages adorn every surface. The hum is powerful enough to feel in your chest." +map_symbol: "G" +exits: + west: 107 + east: 109 diff --git a/data/rooms/109.yaml b/data/rooms/109.yaml new file mode 100644 index 0000000..c22b9d0 --- /dev/null +++ b/data/rooms/109.yaml @@ -0,0 +1,7 @@ +id: 109 +name: "Solar Array Access Corridor" +description: "A narrow passage leading to the station's exterior skin. Small viewports reveal the blindingly bright solar panels stretching into the void, harvesting the star's energy." +map_symbol: "A" +exits: + west: 108 + north: 119 diff --git a/data/rooms/110.yaml b/data/rooms/110.yaml new file mode 100644 index 0000000..fd9c061 --- /dev/null +++ b/data/rooms/110.yaml @@ -0,0 +1,8 @@ +id: 110 +name: "Cryo-Sleep Recovery Ward" +description: "Pale blue light fills this sterile room. Recovery pods line the walls, their occupants slowly reacclimating to standard gravity after decades in suspended animation." +map_symbol: "R" +exits: + south: 100 + east: 111 + north: 120 diff --git a/data/rooms/111.yaml b/data/rooms/111.yaml new file mode 100644 index 0000000..d3a5a2a --- /dev/null +++ b/data/rooms/111.yaml @@ -0,0 +1,9 @@ +id: 111 +name: "Medical Bay Alpha" +description: "State-of-the-art diagnostic bays with hovering holographic displays. Automated medbots glide silently between stations. The scent of antiseptic gel lingers in the recycled air." +map_symbol: "+" +exits: + south: 101 + west: 110 + east: 112 + north: 121 diff --git a/data/rooms/112.yaml b/data/rooms/112.yaml new file mode 100644 index 0000000..d5d46c0 --- /dev/null +++ b/data/rooms/112.yaml @@ -0,0 +1,9 @@ +id: 112 +name: "Pharmacy & Genetek" +description: "Racks of color-coded pharmaceuticals glow under UV-protected lighting. A geneprinter in the corner hums as it synthesizes custom therapies and genetic patches on demand." +map_symbol: "G" +exits: + south: 102 + west: 111 + east: 113 + north: 122 diff --git a/data/rooms/113.yaml b/data/rooms/113.yaml new file mode 100644 index 0000000..4a3333e --- /dev/null +++ b/data/rooms/113.yaml @@ -0,0 +1,7 @@ +id: 113 +name: "Café Synthesizer" +description: "A cozy establishment where food printers whir and steam. Patrons sit at floating tables sipping replicated coffee that is surprisingly close to the real thing." +map_symbol: "c" +exits: + west: 112 + south: 103 diff --git a/data/rooms/115.yaml b/data/rooms/115.yaml new file mode 100644 index 0000000..b0c9a4f --- /dev/null +++ b/data/rooms/115.yaml @@ -0,0 +1,7 @@ +id: 115 +name: "Community Garden Atrium" +description: "Sunlight-mimicking lamps nourish a lush indoor forest of ferns, flowering vines, and small fruit trees. A path of stepping stones winds through the greenery. Birds chirp from hidden perches." +map_symbol: "G" +exits: + south: 105 + east: 116 diff --git a/data/rooms/116.yaml b/data/rooms/116.yaml new file mode 100644 index 0000000..9c6c258 --- /dev/null +++ b/data/rooms/116.yaml @@ -0,0 +1,8 @@ +id: 116 +name: "School of Zero-G Arts" +description: "Students float in a vast open chamber, practicing zero-gravity dance and sculpture. Paint droplets hang suspended mid-air, forming impromptu art installations that drift lazily." +map_symbol: "Z" +exits: + west: 115 + east: 117 + north: 126 diff --git a/data/rooms/117.yaml b/data/rooms/117.yaml new file mode 100644 index 0000000..5a1ecf0 --- /dev/null +++ b/data/rooms/117.yaml @@ -0,0 +1,9 @@ +id: 117 +name: "Library of Forgotten Code" +description: "Data crystals line the walls from floor to ceiling. Vintage terminals allow access to archived knowledge from before the Collapse. The soft click of crystal readers fills the reverent silence." +map_symbol: "L" +exits: + west: 116 + east: 118 + south: 107 + north: 127 diff --git a/data/rooms/118.yaml b/data/rooms/118.yaml new file mode 100644 index 0000000..95a4a1f --- /dev/null +++ b/data/rooms/118.yaml @@ -0,0 +1,8 @@ +id: 118 +name: "Quiet Meditation Spire" +description: "A narrow tower lined with sound-dampening panels. Cushions arranged in concentric circles face a central fountain that flows upward into a recycling grate. No words are spoken here." +map_symbol: "M" +exits: + west: 117 + east: 119 + north: 128 diff --git a/data/rooms/119.yaml b/data/rooms/119.yaml new file mode 100644 index 0000000..dbe9402 --- /dev/null +++ b/data/rooms/119.yaml @@ -0,0 +1,8 @@ +id: 119 +name: "Sleep Pod Cluster 7" +description: "Hundreds of stacked sleep pods line the corridor, their occupants visible through frosted transparencies. A few are open, revealing neatly made bunks with personal hologram projections." +map_symbol: "p" +exits: + west: 118 + south: 109 + north: 129 diff --git a/data/rooms/120.yaml b/data/rooms/120.yaml new file mode 100644 index 0000000..a03d2f2 --- /dev/null +++ b/data/rooms/120.yaml @@ -0,0 +1,8 @@ +id: 120 +name: "The Neon Drake Tavern" +description: "Neon dragons coil across the ceiling in animated loops. The bar serves synthesized spirits that pack a surprising punch. A jukebox plays synthwave classics from the pre-Collapse era." +map_symbol: "T" +exits: + south: 110 + east: 121 + north: 130 diff --git a/data/rooms/121.yaml b/data/rooms/121.yaml new file mode 100644 index 0000000..8adf8c3 --- /dev/null +++ b/data/rooms/121.yaml @@ -0,0 +1,9 @@ +id: 121 +name: "Dance Floor Theta-9" +description: "The floor pulses with reactive LED tiles that shift color with every step. A spherical DJ booth hovers in the center, surrounded by a crowd of moving silhouettes lost in the beat." +map_symbol: "D" +exits: + west: 120 + east: 122 + south: 111 + north: 131 diff --git a/data/rooms/122.yaml b/data/rooms/122.yaml new file mode 100644 index 0000000..d4ec067 --- /dev/null +++ b/data/rooms/122.yaml @@ -0,0 +1,7 @@ +id: 122 +name: "VR Combat Arena" +description: "Rows of neural-interface pods line the walls. Spectators watch live feeds of digital battles where players fight with light and code in simulated zero-G deathmatches." +map_symbol: "V" +exits: + west: 121 + south: 112 diff --git a/data/rooms/126.yaml b/data/rooms/126.yaml new file mode 100644 index 0000000..a76b7d1 --- /dev/null +++ b/data/rooms/126.yaml @@ -0,0 +1,8 @@ +id: 126 +name: "Holographic Tattoo Parlor" +description: "Clients float in zero-g chairs while artists etch animated holograms onto their skin. A woman displays a sleeve of swimming koi that ripple and dart as she moves her arm." +map_symbol: "t" +exits: + south: 116 + east: 127 + north: 136 diff --git a/data/rooms/127.yaml b/data/rooms/127.yaml new file mode 100644 index 0000000..f7565c8 --- /dev/null +++ b/data/rooms/127.yaml @@ -0,0 +1,9 @@ +id: 127 +name: "Underground Fight Club" +description: "In a repurposed cargo container, bare-knuckle brawls draw crowds of gamblers. The ring is marked by glow tape. A hulking enforcer named Jax watches the crowd with narrowed eyes." +map_symbol: "F" +exits: + west: 126 + east: 128 + south: 117 + north: 137 diff --git a/data/rooms/128.yaml b/data/rooms/128.yaml new file mode 100644 index 0000000..93abe6c --- /dev/null +++ b/data/rooms/128.yaml @@ -0,0 +1,9 @@ +id: 128 +name: "Ration Exchange Trade Post" +description: "Bulletin boards display trading offers for ration cards, spare parts, and favors. A grumpy quartermaster oversees the exchange from behind a counter of reinforced polyglass." +map_symbol: "X" +exits: + west: 127 + east: 129 + south: 118 + north: 138 diff --git a/data/rooms/129.yaml b/data/rooms/129.yaml new file mode 100644 index 0000000..35a5087 --- /dev/null +++ b/data/rooms/129.yaml @@ -0,0 +1,7 @@ +id: 129 +name: "Viewing Platform Nebula Vista" +description: "A panoramic window overlooks the Crab Nebula in all its colorful glory. Telescopes mounted on tracks allow close-up viewing of distant stars and planets. Benches face the void." +map_symbol: "V" +exits: + west: 128 + south: 119 diff --git a/data/rooms/130.yaml b/data/rooms/130.yaml new file mode 100644 index 0000000..35c0b35 --- /dev/null +++ b/data/rooms/130.yaml @@ -0,0 +1,8 @@ +id: 130 +name: "Reactor Core Access" +description: "Warning lights pulse a deep amber. The massive blast door is reinforced with thermal shielding. A placard reads 'Authorized Personnel Only — Class 3 Radiation Zone.'" +map_symbol: "R" +exits: + south: 120 + east: 131 + north: 140 diff --git a/data/rooms/131.yaml b/data/rooms/131.yaml new file mode 100644 index 0000000..2825c15 --- /dev/null +++ b/data/rooms/131.yaml @@ -0,0 +1,8 @@ +id: 131 +name: "Engineering Bay 4" +description: "Tools hang on magnetic strips along every wall. Schematic blueprints float in holographic displays. Engineers in grease-stained jumpsuits argumentatively debate power allocation." +map_symbol: "E" +exits: + west: 130 + south: 121 + north: 141 diff --git a/data/rooms/136.yaml b/data/rooms/136.yaml new file mode 100644 index 0000000..a5a87ae --- /dev/null +++ b/data/rooms/136.yaml @@ -0,0 +1,8 @@ +id: 136 +name: "Research Lab Xenobotany" +description: "Terrariums contain alien flora from a dozen star systems. A pulsating purple fungus emits a low-frequency hum. The lead researcher takes meticulous notes on bioluminescent patterns." +map_symbol: "L" +exits: + south: 126 + east: 137 + north: 146 diff --git a/data/rooms/137.yaml b/data/rooms/137.yaml new file mode 100644 index 0000000..a14651b --- /dev/null +++ b/data/rooms/137.yaml @@ -0,0 +1,9 @@ +id: 137 +name: "Zero-G Forge" +description: "Molten metal floats in perfect spheres as magnetic fields shape it into precision components. A welder in a full environmental suit guides the process with practiced hand gestures." +map_symbol: "F" +exits: + west: 136 + east: 138 + south: 127 + north: 147 diff --git a/data/rooms/138.yaml b/data/rooms/138.yaml new file mode 100644 index 0000000..c00d30d --- /dev/null +++ b/data/rooms/138.yaml @@ -0,0 +1,9 @@ +id: 138 +name: "Tool Locker Fabrication" +description: "Walls covered with every tool imaginable, from micro-screwdrivers to plasma cutters. A 3D fabricator in the corner hums quietly, ready to print custom tool heads on demand." +map_symbol: "T" +exits: + west: 137 + east: 139 + south: 128 + north: 148 diff --git a/data/rooms/139.yaml b/data/rooms/139.yaml new file mode 100644 index 0000000..a8226fa --- /dev/null +++ b/data/rooms/139.yaml @@ -0,0 +1,6 @@ +id: 139 +name: "Maintenance Tunnel 9" +description: "A narrow, cramped tunnel lined with exposed conduit and crawling with maintenance drones. The flickering fluorescent light makes the shadows dance along the curved walls." +map_symbol: "M" +exits: + west: 138 diff --git a/data/rooms/140.yaml b/data/rooms/140.yaml new file mode 100644 index 0000000..c4d7db3 --- /dev/null +++ b/data/rooms/140.yaml @@ -0,0 +1,7 @@ +id: 140 +name: "Captain's Quarter Balcony" +description: "A private balcony overlooking the grand concourse below. Potted orchids thrive in the artificial light. The captain's telescope is aimed at a distant binary star system." +map_symbol: "K" +exits: + south: 130 + east: 141 diff --git a/data/rooms/141.yaml b/data/rooms/141.yaml new file mode 100644 index 0000000..d1ba9bf --- /dev/null +++ b/data/rooms/141.yaml @@ -0,0 +1,8 @@ +id: 141 +name: "Executive Lounge Nebula" +description: "Chrome and glass furniture floats in arranged clusters. A fully stocked bar offers genuine Earth spirits at exorbitant prices. The view of the nebula through the window is breathtaking." +map_symbol: "E" +exits: + west: 140 + east: 142 + south: 131 diff --git a/data/rooms/142.yaml b/data/rooms/142.yaml new file mode 100644 index 0000000..e903d24 --- /dev/null +++ b/data/rooms/142.yaml @@ -0,0 +1,7 @@ +id: 142 +name: "Board Room Round Table" +description: "A circular table dominates the room, its surface a holographic display of the sector map. Chairs hover at precise intervals. Decisions that affect thousands are made around this table." +map_symbol: "B" +exits: + west: 141 + east: 143 diff --git a/data/rooms/143.yaml b/data/rooms/143.yaml new file mode 100644 index 0000000..7e16905 --- /dev/null +++ b/data/rooms/143.yaml @@ -0,0 +1,6 @@ +id: 143 +name: "Observation Dome" +description: "The entire ceiling is a transparent dome offering an unobstructed view of deep space. Stars wheel slowly overhead as the station rotates. A gentle chime marks each passing hour." +map_symbol: "O" +exits: + west: 142 diff --git a/data/rooms/145.yaml b/data/rooms/145.yaml new file mode 100644 index 0000000..fc8948e --- /dev/null +++ b/data/rooms/145.yaml @@ -0,0 +1,6 @@ +id: 145 +name: "AI Core Chamber" +description: "A massive crystalline structure pulses with internal light. The station's artificial intelligence, AURA, processes billions of operations per second. A respectful silence is observed here." +map_symbol: "A" +exits: + east: 146 diff --git a/data/rooms/146.yaml b/data/rooms/146.yaml new file mode 100644 index 0000000..b3512b4 --- /dev/null +++ b/data/rooms/146.yaml @@ -0,0 +1,8 @@ +id: 146 +name: "Master Timekeeper's Office" +description: "Clocks of every conceivable design cover the walls — analog, digital, atomic, celestial. The Master Timekeeper ensures all station systems are synchronized to the galactic standard." +map_symbol: "T" +exits: + west: 145 + east: 147 + south: 136 diff --git a/data/rooms/147.yaml b/data/rooms/147.yaml new file mode 100644 index 0000000..4c89429 --- /dev/null +++ b/data/rooms/147.yaml @@ -0,0 +1,8 @@ +id: 147 +name: "Archives of Station Law" +description: "A solemn chamber where the station's founding charter is displayed in a sealed crys-glass case. Legal precedents and arbitration records line the walls in data-slate form." +map_symbol: "A" +exits: + west: 146 + east: 148 + south: 137 diff --git a/data/rooms/148.yaml b/data/rooms/148.yaml new file mode 100644 index 0000000..603536d --- /dev/null +++ b/data/rooms/148.yaml @@ -0,0 +1,8 @@ +id: 148 +name: "Comm Spire Antenna Array" +description: "A vertical shaft rises into a transparent dome bristling with antennae and dishes. Signal boosters amplify transmissions across the sector. The air crackles with silent communication." +map_symbol: "C" +exits: + west: 147 + east: 149 + south: 138 diff --git a/data/rooms/149.yaml b/data/rooms/149.yaml new file mode 100644 index 0000000..ba79aac --- /dev/null +++ b/data/rooms/149.yaml @@ -0,0 +1,6 @@ +id: 149 +name: "Escape Pod Bay 0" +description: "A circular chamber lined with escape pods, their hatches open for inspection. A sign reminds all personnel to familiarize themselves with emergency procedures before departure." +map_symbol: "E" +exits: + west: 148 diff --git a/internal/game/action_gather.go b/internal/game/action_gather.go index e1e724e..62637c3 100644 --- a/internal/game/action_gather.go +++ b/internal/game/action_gather.go @@ -216,7 +216,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.Toggles["xpdrops"] { + if xp > 0 && p.OptionBool("xpdrops") { msg += fmt.Sprintf(" (+%dxp %s)", xp, player.SkillAbbr[player.SkillName(cfg.Skill)]) } sess.WriteLine(msg) diff --git a/internal/game/action_use.go b/internal/game/action_use.go index c1a07ed..69d29c1 100644 --- a/internal/game/action_use.go +++ b/internal/game/action_use.go @@ -105,7 +105,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.Toggles["xpdrops"] { + if cfg.XP > 0 && p.OptionBool("xpdrops") { 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 4e2d2c0..f6ccf0c 100644 --- a/internal/game/cmd_attack.go +++ b/internal/game/cmd_attack.go @@ -210,7 +210,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.Toggles["xpdrops"] && len(gains) > 0 { + if p.OptionBool("xpdrops") && 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)])) @@ -453,7 +453,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.Toggles["mobspawn"] { + if p, ok := sess.Player.(*player.Player); ok && p.OptionBool("mobspawn") { 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 0bec915..6d19023 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -25,7 +25,7 @@ func (g *Game) doLook(sess *net.Session) { room.Name, ) - if p.Toggles["tinymap"] && g.MapWidth > 0 { + if p.OptionBool("tinymap") && g.MapWidth > 0 { descLines := wrapText(room.Description, g.MapWidth) mapLines := buildTinyMap(g, p.RoomID) if len(mapLines) == 7 { @@ -112,7 +112,7 @@ func (g *Game) doLook(sess *net.Session) { }) } multi := len(instances) > 1 - showTimers := p.Toggles["depletion"] + showTimers := p.OptionBool("depletion") var freshIdxs []int var timed, depleted []instInfo @@ -205,7 +205,7 @@ func (g *Game) doLook(sess *net.Session) { } reserved := "" if info.ReservedFor != "" { - if p.Toggles["reserve"] { + if p.OptionBool("reserve") { reserved = fmt.Sprintf(" (reserved for %s for %d ticks)", info.ReservedFor, info.ReserveTimer) } else { reserved = " (reserved)" @@ -225,7 +225,7 @@ func (g *Game) doLook(sess *net.Session) { if len(room.Exits) > 0 { sess.WriteLine("") - if p.Toggles["exits"] { + if p.OptionBool("exits") { sess.WriteLine("Exits:") for _, dir := range world.ExitOrder { exitDef, ok := room.Exits[dir] @@ -345,7 +345,7 @@ func (g *Game) doLookTarget(sess *net.Session, input string) { sess.WriteLine(fmt.Sprintf(" %s", desc)) } - if p.Toggles["depletion"] { + if p.OptionBool("depletion") { for _, ist := range instances { if ist.Depleted { if len(instances) > 1 { @@ -525,7 +525,7 @@ func (g *Game) writeLookSideBySide(sess *net.Session, p *player.Player, descLine if len(mapLines) > total { total = len(mapLines) } - leftMap := p.Toggles["left-tinymap"] + leftMap := p.OptionBool("left-tinymap") for i := 0; i < total; i++ { desc := "" if i < len(descLines) { diff --git a/internal/game/cmd_map.go b/internal/game/cmd_map.go new file mode 100644 index 0000000..7c506f9 --- /dev/null +++ b/internal/game/cmd_map.go @@ -0,0 +1,140 @@ +package game + +import ( + "strings" + + "thirdcollapse/internal/net" + "thirdcollapse/internal/player" + "thirdcollapse/internal/world" +) + +func (g *Game) doMap(sess *net.Session) { + p := sess.Player.(*player.Player) + mapWidth := p.OptionInt("mapwidth") + mapHeight := p.OptionInt("mapheight") + if mapWidth < 5 { + mapWidth = 5 + } + if mapHeight < 5 { + mapHeight = 5 + } + + lines := buildFullMap(g, p.RoomID, mapWidth, mapHeight) + + mode := p.OptionString("mappadding") + if mode == "none" || mode == "x" { + lines = stripBlankRows(lines) + } + if mode == "none" || mode == "y" { + lines = leftTrimCommon(lines) + } + + if len(lines) == 0 { + sess.WriteLine("\nNo map data to display.") + return + } + sess.WriteLine("") + for _, line := range lines { + 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 79ca45e..d62701d 100644 --- a/internal/game/cmd_move.go +++ b/internal/game/cmd_move.go @@ -79,7 +79,7 @@ func (g *Game) doMove(sess *net.Session, dir string) { } sess.WriteLine(fmt.Sprintf("\nYou walk %s.", exitDir)) - if p.Toggles["description"] { + if p.OptionBool("description") { g.doLook(sess) } else { targetRoom, _ := g.World.LoadRoom(targetID) @@ -88,6 +88,10 @@ func (g *Game) doMove(sess *net.Session, dir string) { } } + if p.OptionBool("automap") { + g.doMap(sess) + } + g.RunEnterSteps(sess, targetID) } diff --git a/internal/game/cmd_option.go b/internal/game/cmd_option.go new file mode 100644 index 0000000..49085db --- /dev/null +++ b/internal/game/cmd_option.go @@ -0,0 +1,116 @@ +package game + +import ( + "fmt" + "strconv" + "strings" + + "thirdcollapse/internal/net" + "thirdcollapse/internal/player" +) + +func (g *Game) doOption(sess *net.Session, input string) { + p := sess.Player.(*player.Player) + + if input == "" { + sess.WriteLine("") + sess.WriteLine(fmt.Sprintf(" %-14s %-10s %-22s %s", "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)) + } + return + } + + parts := strings.Fields(input) + name := strings.ToLower(parts[0]) + def := player.GetOptionDef(name) + if def == nil { + sess.WriteLine(fmt.Sprintf("\nUnknown option: %s", name)) + return + } + + if len(parts) == 1 { + val := formatOptionValue(p, def) + valid := formatValidValues(def) + sess.WriteLine(fmt.Sprintf("\n%s = %s [%s]", def.Name, val, valid)) + sess.WriteLine(fmt.Sprintf(" %s", def.Description)) + return + } + + value := strings.ToLower(parts[1]) + parsed, ok := parseOptionValue(def, value) + if !ok { + valid := formatValidValues(def) + sess.WriteLine(fmt.Sprintf("\nInvalid value for %s: %s [%s]", def.Name, value, valid)) + return + } + + if p.Options == nil { + p.Options = make(map[string]any) + } + p.Options[def.Name] = parsed + g.AccountStore.SaveCharacter(p) + sess.WriteLine(fmt.Sprintf("\n%s set to %s.", def.Name, formatOptionValue(p, def))) +} + +func formatOptionValue(p *player.Player, def *player.OptionDef) string { + switch def.Type { + case player.OptBool: + if p.OptionBool(def.Name) { + return "on" + } + return "off" + case player.OptString: + return p.OptionString(def.Name) + case player.OptInt: + return strconv.Itoa(p.OptionInt(def.Name)) + } + return "" +} + +func formatValidValues(def *player.OptionDef) string { + switch def.Type { + case player.OptBool: + return "on/off" + case player.OptString: + if len(def.ValidValues) > 0 { + return strings.Join(def.ValidValues, "/") + } + return "string" + case player.OptInt: + return "num" + } + return "" +} + +func parseOptionValue(def *player.OptionDef, input string) (any, bool) { + switch def.Type { + case player.OptBool: + switch input { + case "on", "true", "yes", "1": + return true, true + case "off", "false", "no", "0": + return false, true + } + return nil, false + case player.OptString: + if len(def.ValidValues) == 0 { + return input, true + } + for _, v := range def.ValidValues { + if strings.EqualFold(v, input) { + return v, true + } + } + return nil, false + case player.OptInt: + n, err := strconv.Atoi(input) + if err != nil { + return nil, false + } + return n, true + } + return nil, false +} diff --git a/internal/game/cmd_toggle.go b/internal/game/cmd_toggle.go deleted file mode 100644 index a399483..0000000 --- a/internal/game/cmd_toggle.go +++ /dev/null @@ -1,56 +0,0 @@ -package game - -import ( - "fmt" - "strings" - - "thirdcollapse/internal/net" - "thirdcollapse/internal/player" -) - -var toggles = []struct { - Name string - Description string -}{ - {"description", "Long room descriptions"}, - {"tinymap", "Mini-map display"}, - {"left-tinymap", "Mini-map on left side of descriptions"}, - {"xpdrops", "XP drop messages"}, - {"exits", "Long exit display in look"}, - {"mobenter", "Messages when mobs enter the room"}, - {"mobleave", "Messages when mobs leave the room"}, - {"mobspawn", "Messages when mobs spawn in the area"}, - {"reserve", "Show full reserved item details"}, - {"depletion", "Show depletion and despawn timers on objects"}, -} - -func (g *Game) doToggle(sess *net.Session, input string) { - p := sess.Player.(*player.Player) - - if input == "" { - sess.WriteLine("") - for _, t := range toggles { - status := "Off" - if p.Toggles[t.Name] { - status = "On" - } - sess.WriteLine(fmt.Sprintf(" %-12s %-3s %s", t.Name, status, t.Description)) - } - g.AccountStore.SaveCharacter(p) - return - } - - for _, t := range toggles { - if strings.ToLower(input) == t.Name { - p.Toggles[t.Name] = !p.Toggles[t.Name] - status := "Off" - if p.Toggles[t.Name] { - status = "On" - } - sess.WriteLine(fmt.Sprintf("\n%s %s.", t.Description, status)) - g.AccountStore.SaveCharacter(p) - return - } - } - sess.WriteLine(fmt.Sprintf("\nUnknown toggle: %s", input)) -} diff --git a/internal/game/game.go b/internal/game/game.go index 0675a13..8151877 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -194,10 +194,12 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) { return case "description", "desc": g.doDescription(sess) - case "toggle": - g.doToggle(sess, strings.Join(args, " ")) + case "option", "options": + g.doOption(sess, strings.Join(args, " ")) case "exits": g.doExits(sess) + case "map": + g.doMap(sess) case "help": if len(args) == 0 { diff --git a/internal/game/map_test.go b/internal/game/map_test.go index a50c2c2..adba595 100644 --- a/internal/game/map_test.go +++ b/internal/game/map_test.go @@ -92,3 +92,89 @@ func TestWrapText(t *testing.T) { } } } + +func TestBuildFullMap(t *testing.T) { + g := &Game{ + World: world.New("../../data"), + MapWidth: 70, + } + + tests := []struct { + name string + roomID int + width int + height int + }{ + {"room 1 30x20", 1, 30, 20}, + {"room 1 20x10", 1, 20, 10}, + {"room 25 30x20", 25, 30, 20}, + {"min size 5x5", 1, 5, 5}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + lines := buildFullMap(g, tt.roomID, tt.width, tt.height) + if len(lines) != tt.height { + t.Errorf("expected %d lines, got %d", tt.height, len(lines)) + } + for i, line := range lines { + if len([]rune(line)) != tt.width { + t.Errorf("line %d: expected %d runes, got %d in %q", i, tt.width, len([]rune(line)), line) + } + } + t.Logf("Room %d %dx%d map:\n%s", tt.roomID, tt.width, tt.height, strings.Join(lines, "\n")) + }) + } +} + +func TestStripBlankRows(t *testing.T) { + input := []string{" ", "hello", "", " world ", " "} + want := []string{"hello", " world "} + got := stripBlankRows(input) + if len(got) != len(want) { + t.Fatalf("expected %d lines, got %d", len(want), len(got)) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("line %d: want %q, got %q", i, want[i], got[i]) + } + } +} + +func TestLeftTrimCommon(t *testing.T) { + input := []string{" hello", " world", " ", " foo"} + got := leftTrimCommon(input) + // min leading spaces across non-blank lines: " hello"=4, " world"=4, " foo"=2 → min=2 + // trim 2 from all lines + if got[0] != " hello" { + t.Errorf("line 0: want %q, got %q", " hello", got[0]) + } + if got[1] != " world" { + t.Errorf("line 1: want %q, got %q", " world", got[1]) + } + if got[2] != "" { + t.Errorf("line 2: want %q, got %q", "", got[2]) + } + if got[3] != "foo" { + t.Errorf("line 3: want %q, got %q", "foo", got[3]) + } +} + +func TestLeftTrimCommonEmpty(t *testing.T) { + input := []string{" ", "", " "} + got := leftTrimCommon(input) + if len(got) != 3 { + t.Fatalf("expected 3 lines, got %d", len(got)) + } +} + +func TestLeftTrimCommonZero(t *testing.T) { + input := []string{"hello", "world"} + got := leftTrimCommon(input) + if got[0] != "hello" { + t.Errorf("line 0: want %q, got %q", "hello", got[0]) + } + if got[1] != "world" { + t.Errorf("line 1: want %q, got %q", "world", got[1]) + } +} diff --git a/internal/game/tick.go b/internal/game/tick.go index 7eb63eb..3c3f1d3 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.Toggles["mobleave"] { + if p.RoomID == m.fromRoom && p.OptionBool("mobleave") { 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.Toggles["mobenter"] { + if p.RoomID == m.toRoom && p.OptionBool("mobenter") { 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 56ab06c..97874e3 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -86,6 +86,53 @@ type InventorySlot struct { Quantity int `yaml:"quantity"` } +type OptionType int + +const ( + OptBool OptionType = iota + OptString + OptInt +) + +type OptionDef struct { + Name string + Type OptionType + Default any + ValidValues []string + Description string +} + +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"}, + {"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"}, + {"reserve", OptBool, true, nil, "Show full reserved item details"}, + {"depletion", OptBool, false, nil, "Show depletion and despawn timers"}, + {"color", OptString, "none", []string{"none", "ansi", "xterm256"}, "Color output mode"}, + {"mapwidth", OptInt, 30, nil, "Map width for the map command"}, + {"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"}, +} + +var optionByName map[string]*OptionDef + +func init() { + optionByName = make(map[string]*OptionDef, len(OptionDefs)) + for i := range OptionDefs { + optionByName[OptionDefs[i].Name] = &OptionDefs[i] + } +} + +func GetOptionDef(name string) *OptionDef { + return optionByName[name] +} + type Player struct { Name string `yaml:"name"` Skills map[SkillName]int `yaml:"skills"` @@ -96,12 +143,73 @@ type Player struct { Credits int `yaml:"credits"` Description string `yaml:"description"` AttackStyle AttackStyle `yaml:"attack_style"` - Toggles map[string]bool `yaml:"toggles"` + Options map[string]any `yaml:"options"` Flags map[string]any `yaml:"flags"` RegenerateTick int Action *action.Action `yaml:"-"` } +func (p *Player) OptionBool(name string) bool { + def := GetOptionDef(name) + if def == nil || def.Type != OptBool { + return false + } + if p.Options == nil { + return def.Default.(bool) + } + val, ok := p.Options[name] + if !ok { + return def.Default.(bool) + } + b, ok := val.(bool) + if !ok { + return def.Default.(bool) + } + return b +} + +func (p *Player) OptionString(name string) string { + def := GetOptionDef(name) + if def == nil || def.Type != OptString { + return "" + } + if p.Options == nil { + return def.Default.(string) + } + val, ok := p.Options[name] + if !ok { + return def.Default.(string) + } + s, ok := val.(string) + if !ok { + return def.Default.(string) + } + return s +} + +func (p *Player) OptionInt(name string) int { + def := GetOptionDef(name) + if def == nil || def.Type != OptInt { + return 0 + } + if p.Options == nil { + return def.Default.(int) + } + val, ok := p.Options[name] + if !ok { + return def.Default.(int) + } + switch v := val.(type) { + case int: + return v + case int64: + return int(v) + case float64: + return int(v) + } + return def.Default.(int) +} + func (p *Player) InvSlot(i int) *InventorySlot { if p.Inventory == nil { return nil @@ -140,18 +248,11 @@ func New(name string) *Player { Skills: make(map[SkillName]int), Equipment: make(map[object.EquipSlot]string), AttackStyle: Accurate, - Toggles: map[string]bool{ - "description": true, - "tinymap": true, - "left-tinymap": false, - "xpdrops": true, - "exits": true, - "mobenter": true, - "mobleave": true, - "mobspawn": true, - "reserve": true, - }, - RoomID: 0, + Options: make(map[string]any), + RoomID: 0, + } + for _, def := range OptionDefs { + p.Options[def.Name] = def.Default } for _, s := range AllSkills { p.Skills[s] = 0 |
