From 480012aa02badf2e208cf918a4a45427c658face Mon Sep 17 00:00:00 2001 From: byblik Date: Tue, 25 Aug 2026 10:14:10 +0300 Subject: [PATCH] fix: keep ficha and log on city screens between 64 and 90 columns (#7) mapScreen dropped the sidebar whenever width - SIDE_W - 5 fell below a min(mapW, 60) estimate, which silenced both panels for every terminal from 64 to 90 columns even though the map scrolls inside its pane anyway - so working actions left an identical screen while model state moved. The split layout threshold is now simply MIN_WIDTH, matching field and shop screens, with the explicit sidebar:false opt-out preserved. --- lib/render.js | 13 +++++----- test/index.js | 4 ++- test/sidebar-threshold.test.js | 46 ++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 test/sidebar-threshold.test.js diff --git a/lib/render.js b/lib/render.js index 852bf3f..2b6d6d1 100755 --- a/lib/render.js +++ b/lib/render.js @@ -1314,13 +1314,14 @@ function compose(opts) { * @returns {string} */ function mapScreen(m) { - const tiles = (m.map && m.map.tiles) || [] const cellW = Math.max(1, m.cellW || CELL_W) - const mapW = tiles.reduce((widest, line) => Math.max(widest, String(line).length), 0) * cellW - // Large maps scroll inside the pane. Requiring the entire map to fit would - // hide the character sheet forever as soon as a world grew beyond one view. - const usefulMapW = Math.min(mapW, 60) - const sidebar = m.sidebar === undefined ? m.width - SIDE_W - 5 >= usefulMapW : m.sidebar + // Large maps scroll inside the pane, so the character sheet never has to + // compete with the world's full width (#7). Any terminal wide enough for the + // split layout keeps the sidebar: between 64 and 90 columns the previous + // estimate `width - SIDE_W - 5 >= min(mapW, 60)` silently dropped ficha and + // log, and actions that worked then left an unchanged screen on model state + // that had clearly moved. + const sidebar = m.sidebar === undefined ? m.width >= MIN_WIDTH : m.sidebar const stats = m.stats || {} const compactStats = 'nv ' + diff --git a/test/index.js b/test/index.js index 478ca41..a97696a 100644 --- a/test/index.js +++ b/test/index.js @@ -17,6 +17,7 @@ const { const render = require('../lib/render.js') require('./sage.test.js') +require('./sidebar-threshold.test.js') function press(game, name) { return game.onKey({ type: 'key', is: (...keys) => keys.includes(name) }) @@ -250,7 +251,8 @@ test('the larger city scrolls inside an 80-column console', (t) => { MAPS.city.rows.some((row) => row.includes('~~~~~~~~~~~~~~~~~~')), 'the city keeps its detailed fountain' ) - t.ok(screen.includes('hp 20/20'), 'compact stats remain visible in the title bar') + t.ok(screen.includes('ficha'), 'the character sheet stays in its sidebar (#7)') + t.ok(screen.includes('log'), 'the log panel survives narrow city terminals too (#7)') t.ok(!screen.includes('[#]'), 'a new hero does not display an unequipped shield') t.ok(!screen.includes('@'), 'the city no longer represents the hero with an at sign') diff --git a/test/sidebar-threshold.test.js b/test/sidebar-threshold.test.js new file mode 100644 index 0000000..69bcd3f --- /dev/null +++ b/test/sidebar-threshold.test.js @@ -0,0 +1,46 @@ +const { test } = require('brittle') +const { style } = require('bare-tui') +const render = require('../lib/render.js') + +function cityFrame(width) { + return style.stripAnsi( + render.mapScreen({ + width, + height: 20, + place: 'plaza', + stats: { hp: 9, maxhp: 12, gold: 7 }, + log: ['primer mensaje'], + cellW: 1, + map: { + tiles: ['........', '........', '........'], + actors: [], + hero: { x: 4, y: 1 } + } + }) + ) +} + +test('the city keeps ficha and log from MIN_WIDTH up to the old cutoff (#7)', (t) => { + for (const width of [64, 70, 80, 90]) { + const frame = cityFrame(width) + t.ok(frame.includes('ficha'), `${width} columns show the sheet`) + t.ok(frame.includes('log'), `${width} columns show the log panel`) + t.ok(frame.includes('primer mensaje'), `${width} columns surface new model state`) + } +}) + +test('below MIN_WIDTH nothing pretends to fit, and an explicit opt-out still works (#7)', (t) => { + t.ok(!cityFrame(63).includes('ficha'), '63 columns stay on the too-small screen') + + const wide = render.mapScreen({ + width: 100, + height: 20, + place: 'plaza', + stats: {}, + log: [], + sidebar: false, + cellW: 1, + map: { tiles: ['....'], actors: [], hero: { x: 0, y: 0 } } + }) + t.ok(!style.stripAnsi(wide).includes('ficha'), 'sidebar:false keeps its escape-hatch meaning') +}) \ No newline at end of file