From 0405c3f999011e77dcfe13bb082efb2447057bdf Mon Sep 17 00:00:00 2001 From: byblik Date: Tue, 25 Aug 2026 14:49:01 +0300 Subject: [PATCH] feat: give the ruins residents and a straight east wall (#10) The dungeon shipped structurally perfect and completely empty: across 941 walkable cells only the exit gate did anything, because MAPS.dungeon never defined npcs. Three residents now camp different chambers using the exact NPC machinery the city already runs, so the header hint, e-to-talk and dialogue all light up underground. The art itself was ragged on the right - rows ended at columns 59, 60 or 61 and padEnd hid that from the defineMap rectangularity check - so rows are normalized to one declared width with wall forced at column 60. Hero head clipping on arrival is already covered by the #16 solid-cell clip. Field-style encounters stay open as follow-up. --- lib/map.js | 50 ++++++++++++++++++++++++++++++++++-- test/dungeon-content.test.js | 49 +++++++++++++++++++++++++++++++++++ test/index.js | 1 + 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 test/dungeon-content.test.js diff --git a/lib/map.js b/lib/map.js index 886bc31..720bf3e 100755 --- a/lib/map.js +++ b/lib/map.js @@ -1163,7 +1163,52 @@ const DUNGEON_ROWS = [ '#..........................................................#', '#..........................~~~~~~...........................#', '############################################################' -].map((row) => row.padEnd(62, '#')) +// The ruins were drawn a few columns short on the right and every row ended +// at a different wall column; padEnd used to hide that from defineMap()'s +// rectangularity check (#10). Normalizing to one fixed width gives the ruins +// a straight east wall, so the check means something again. +].map((row) => (row + '###').slice(0, 60) + '#') + +/** + * The ruins are not empty anymore: three residents camp different chambers, + * reusing the exact NPC machinery the city already runs (nearbyNpc header, + * e-to-talk, interactNpc dialogue) - issue #10's minimum fix. + */ +const DUNGEON_NPCS = [ + { + id: 'eco', + name: 'Eco de las ruinas', + role: 'vidente', + x: 10, + y: 7, + sprite: NPC_SPRITES.resident, + anchorY: NPC_SPRITES.resident.length - 1, + color: 'cyan', + line: 'los muros recuerdan mas pasos que los vivos' + }, + { + id: 'tadeo', + name: 'Tadeo', + role: 'minero', + x: 30, + y: 13, + sprite: NPC_SPRITES.resident, + anchorY: NPC_SPRITES.resident.length - 1, + color: 'yellow', + line: 'la V fue salida, no entrada: alguien selló el resto' + }, + { + id: 'gata', + name: 'Gata de las ruinas', + role: 'gato', + x: 40, + y: 17, + sprite: NPC_SPRITES.resident, + anchorY: NPC_SPRITES.resident.length - 1, + color: 'gray', + line: 'miau' + } +] /** * Turn art plus metadata into a map, checking the art is rectangular. @@ -1212,7 +1257,8 @@ const MAPS = { name: 'las ruinas bajo el castillo', rows: DUNGEON_ROWS, spawn: { x: 3, y: 2 }, - arrive: { x: 3, y: 2 } + arrive: { x: 3, y: 2 }, + npcs: DUNGEON_NPCS }), coliseum: defineMap({ id: 'coliseum', diff --git a/test/dungeon-content.test.js b/test/dungeon-content.test.js new file mode 100644 index 0000000..2f9cf57 --- /dev/null +++ b/test/dungeon-content.test.js @@ -0,0 +1,49 @@ +const { test } = require('brittle') +const { Runa } = require('../lib/game.js') +const M = require('../lib/map.js') + +function press(game, name) { + return game.onKey({ type: 'key', is: (...keys) => keys.includes(name) }) +} + +function startGame(game, name = 'Tomas') { + press(game, 'enter') + for (const ch of String(name)) { + game.onKey({ type: 'key', sequence: ch, ctrl: false, meta: false, is: () => false }) + } + press(game, 'enter') +} + +test('the ruins host three residents on walkable ground (#10)', (t) => { + const npcs = M.MAPS.dungeon.npcs + t.ok(Array.isArray(npcs) && npcs.length >= 3, 'dungeon defines its own residents') + for (const n of npcs) { + t.ok(!M.isSolid(M.MAPS.dungeon, n.x, n.y), `${n.name} stands on open floor at ${n.x},${n.y}`) + } +}) + +test('the east wall of the ruins is one straight line now (#10)', (t) => { + const city = M.MAPS.dungeon + const w = city.width + let aligned = true + for (let y = 0; y < city.height; y++) { + if (city.rows[y].length !== w || city.rows[y][w - 1] !== '#') aligned = false + if (!M.isSolid(city, w - 1, y)) aligned = false + } + t.is(w, 61, 'normalized to a single declared width') + t.ok(aligned, 'column 60 is wall on every row - no more jagged mouth') +}) + +test('talking inside the ruins reaches a resident instead of void (#10)', (t) => { + const game = new Runa({ presence: false }) + startGame(game) + + const npc = M.MAPS.dungeon.npcs[0] + game.walker.placeAt('dungeon', npc.x + 1, npc.y) + press(game, 'e') + + t.ok( + !game.log.some((line) => String(line).includes('aca no hay nada')), + 'the resident answers instead of the void' + ) +}) \ No newline at end of file diff --git a/test/index.js b/test/index.js index 7be3ead..47f34eb 100644 --- a/test/index.js +++ b/test/index.js @@ -19,6 +19,7 @@ const { const render = require('../lib/render.js') require('./sage.test.js') +require('./dungeon-content.test.js') function press(game, name) { return game.onKey({ type: 'key', is: (...keys) => keys.includes(name) })