diff --git a/lib/render.js b/lib/render.js index 8a4681e..416ce3c 100755 --- a/lib/render.js +++ b/lib/render.js @@ -33,6 +33,7 @@ const { style } = require('bare-tui') const { makeMovingHeroSprite } = require('./sprites.js') const { WORLD_BOSS } = require('./world-boss.js') const { bossCamera } = require('./world-boss-event.js') +const { isSolid } = require('./map.js') /** Smallest terminal the split layout is designed for. */ const MIN_WIDTH = 64 @@ -798,6 +799,10 @@ function mapPane(map, w, h, opts = {}) { : sprite.length - 1 const top = hy - clamp(heroAnchorY, 0, sprite.length - 1) + // isSolid reads the model shape ({ rows, width, height }); this pane gets + // pre-render data ({ tiles }), so adapt the view once per frame (#16). + const solidView = { rows: tiles, width: mapW, height: mapH } + for (let sy = 0; sy < sprite.length; sy++) { const line = ascii(sprite[sy]).padEnd(spriteW) const first = line.search(/\S/) @@ -806,7 +811,14 @@ function mapPane(map, w, h, opts = {}) { while (last > first && line[last] === ' ') last-- for (let sx = first; sx <= last; sx++) { if (line[sx] === ' ') continue - const cell = top + sy + ',' + (left + sx) + const wx = left + sx + const wy = top + sy + // The body hangs above the feet and can reach over a wall or past the + // map edge. A hero glyph on a solid tile reads as a hole in that wall, + // so those cells stay untouched (#16). With no tiles at all there is + // nothing to clip against and the fallback keeps rendering whole. + if (tiles.length > 0 && isSolid(solidView, wx, wy)) continue + const cell = wy + ',' + wx over.set(cell, line[sx]) heroCells.add(cell) } diff --git a/test/hero-clip.test.js b/test/hero-clip.test.js new file mode 100644 index 0000000..c179ed8 --- /dev/null +++ b/test/hero-clip.test.js @@ -0,0 +1,44 @@ +const { test } = require('brittle') +const { style } = require('bare-tui') +const render = require('../lib/render.js') + +test('the hero never paints through a wall (#16)', (t) => { + // Dungeon-style arrival: the three-row hero stands with its feet two rows + // below a full wall, like arriving at (3,2) in the issue. The head row of + // the sprite lands on the wall and must be clipped away. + const tiles = ['######', '#....#', '#....#', '######'] + const pane = style.stripAnsi( + render.mapPane({ tiles, actors: [], hero: { x: 3, y: 2, sprite: [' O ', '/|\\', '/ \\'] } }, 24, 12, { + cellW: 1 + }) + ) + + t.ok(pane.includes('######'), 'the top wall stays a continuous wall') + t.ok(!pane.includes('O'), 'the head is clipped instead of punching a hole in the wall') + t.ok(pane.includes('|'), 'the torso still shows on the free row below') +}) + +test('a hero arriving at the top edge loses only the off-map rows (#16)', (t) => { + // Field arrival at (40,1): the upper body reaches past row 0. Off-map cells + // read as solid, so they get skipped, while the feet stay put. + const tiles = ['........', '........', '########'] + const pane = style.stripAnsi( + render.mapPane({ tiles, actors: [], hero: { x: 4, y: 0, sprite: ['O', '|', '^'] } }, 20, 8, { + cellW: 1 + }) + ) + + t.ok(!pane.includes('O'), 'nothing is painted above the map') + t.ok(pane.includes('^'), 'the feet remain exactly where the walker stands') + t.ok(style.stripAnsi(render.mapPane({ tiles, actors: [], hero: { x: 4, y: 0 } }, 20, 8)).includes('@'), 'glyph heroes keep rendering') +}) + +test('a hero without map data keeps rendering (fallback path unchanged)', (t) => { + const pane = style.stripAnsi( + render.mapPane({ tiles: [], actors: [], hero: { x: 5, y: 5, sprite: ['O', '|', '^'] } }, 20, 8, { + cellW: 1 + }) + ) + + t.ok(pane.includes('O'), 'with no tiles there is nothing solid to clip against') +}) \ No newline at end of file diff --git a/test/index.js b/test/index.js index 7be3ead..c7ec8c9 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('./hero-clip.test.js') function press(game, name) { return game.onKey({ type: 'key', is: (...keys) => keys.includes(name) })