feat(tavern): proper tavern — real bar, kegs, bottles, tables+chairs, windows, rug

Used the sheet's hand-labelled roguelikeSheet_meta.json to pick the RIGHT tiles
instead of guessing:
- New tile kinds + coords (window, counter, keg/barrel, shelf, table, chair,
  crate, rug, door) in sceneModel TAVERN_LEGEND/TILE_OF + assetPacks tileMap.
- tavernMap redesigned: windowed walls, a bar counter with a shelf of bottles
  behind it and a keg (beer!) at each end, six tables each with a chair, a
  central rug, corner barrels/crates, and a door. Chairs are walkable so standing
  on one reads as sitting. Warm wood floor color (was a dark green in the fallback).
- Bartender now stands BEHIND the bar (spawn + roam confined to the row between
  the top wall and the counter, sealed off by it).

Still TODO from feedback: a cat (no cat sprite on the sheet — needs an asset) and
richer bar bottles. Suite 57 suites / 413 tests green.
This commit is contained in:
jwinterm
2026-07-11 06:50:05 -04:00
parent 3eb4959e9a
commit 24e738c733
5 changed files with 79 additions and 51 deletions
+18 -10
View File
@@ -141,17 +141,25 @@
// keep one coherent tile per kind. (TileRenderer still accepts an array here if a
// future theme wants subtle, same-material variation.)
tiles: {
floor: [5, 2], // tile 119 — wood floor (uniform)
floor2: [6, 2], // tile 120 — stone floor (accent)
wall: [13, 12], // tile 697 — stone masonry
bar: [28, 0], // tile 28 — counter
table: [20, 3], // tile 191 — table
floor: [5, 2], // 119 — wood floor
floor2: [6, 2], // 120 — stone floor (accent)
wall: [13, 12], // 697 — stone masonry
// Tavern furnishings (coords from roguelikeSheet_meta.json labels: index -> [i%57, i/57]).
window: [42, 2], // 156 — window (in the wall)
bar: [29, 0], // 29 — bar counter
keg: [23, 0], // 23 — barrel / beer keg (behind the bar)
shelf: [30, 1], // 87 — shelf of bottles (behind the bar)
table: [27, 5], // 312 — table
chair: [19, 3], // 190 — chair
barrel: [23, 0], // 23 — barrel (decor)
crate: [25, 0], // 25 — crate (decor)
rug: [11, 16], // 923 — rug
door: [37, 9], // 550 — door
// Dungeon features drawn as real tiles instead of monospace glyphs.
entrance: [37, 9], // tile 550 — door
exit: [37, 9], // tile 550 — door
treasure: [42, 15], // tile 897 — item
torch: [50, 10] // tile 620 — rubble / prop
entrance: [37, 9], // 550 — door
exit: [37, 9], // 550 — door
treasure: [42, 15], // 897 — item
torch: [50, 10] // 620 — rubble / prop
}
}
};
+17 -5
View File
@@ -8,14 +8,26 @@
// Tile kinds and how each renderer treats them (glyph for ASCII, colour for tiled/fancy).
var TAVERN_LEGEND = {
wall: { char: '#', color: '#20262e', solid: true },
floor: { char: '·', color: '#0c1410', solid: false },
bar: { char: '=', color: '#5a4632', solid: true },
table: { char: 'o', color: '#4a3a28', solid: true }
wall: { char: '#', color: '#3a3f4b', solid: true },
window: { char: 'W', color: '#4a6a8a', solid: true },
floor: { char: '·', color: '#6b4d33', solid: false }, // warm wood, not the old dark green
rug: { char: '=', color: '#7a3b39', solid: false },
bar: { char: '=', color: '#6a4a2a', solid: true },
keg: { char: 'k', color: '#5a4028', solid: true },
shelf: { char: 'h', color: '#4a3a28', solid: true },
table: { char: 'T', color: '#6a4a2a', solid: true },
chair: { char: 'c', color: '#5a4028', solid: false }, // walkable — stand on it to "sit"
barrel: { char: 'B', color: '#5a4028', solid: true },
crate: { char: 'C', color: '#7a5a38', solid: true },
door: { char: 'D', color: '#3fb950', solid: false }
};
// Layout character -> tile kind.
var TAVERN_TILE_OF = { '#': 'wall', '.': 'floor', '@': 'floor', '=': 'bar', 'o': 'table' };
var TAVERN_TILE_OF = {
'#': 'wall', 'W': 'window', '.': 'floor', '@': 'floor', 'r': 'rug',
'=': 'bar', 'k': 'keg', 'h': 'shelf', 'T': 'table', 'c': 'chair',
'B': 'barrel', 'C': 'crate', 'D': 'door'
};
var AVATAR_COLORS = { 'default': '#9aa4b2', green: '#3fb950', amber: '#d29922', red: '#f85149' };
+5 -4
View File
@@ -69,8 +69,9 @@
}
function bartendersArea(cols, rows) {
// Bartender roams along the top interior (bar counter row ~ y=2).
return { minY: 1, maxY: 3, minX: 2, maxX: Math.max(5, cols - 3) };
// Behind the bar: the row between the top wall and the counter (y=1). Sealed off by the
// counter, so the bartender stays back there and never wanders onto the customer floor.
return { minY: 1, maxY: 1, minX: 3, maxX: Math.max(5, cols - 5) };
}
function patronsArea(cols, rows) {
// Patrons roam the middle/lower floor.
@@ -195,8 +196,8 @@
return { x: 1, y: 1 };
}
// Bartender near the bar (top area).
var barSpot = findFloor(Math.floor(cols / 2), 3);
// Bartender BEHIND the bar (row 1, between the top wall and the counter).
var barSpot = findFloor(Math.floor(cols / 2), 1);
var bartender = makeNPC('bartender', 'bartender', 'char-merchant', barSpot.x, barSpot.y, {
label: 'Bartender', color: '#c9a25e', role: 'bartender',
roamArea: bartendersArea(cols, rows)
+38 -31
View File
@@ -1,26 +1,22 @@
/**
* Default Tavern layout — a small room with hand-placed props (bar, tables), generated
* procedurally so every row is guaranteed the same width.
* Default Tavern layout — a hand-built room rendered through the theme atlas. Every row is the same
* width (ragged ASCII maps are a classic off-by-one collision bug), and the legend below drives both
* the client tiles (sceneModel TAVERN_LEGEND/TILE_OF) and Room walkability.
*
* Legend:
* '#' wall '=' bar counter 'o' table / stool (all solid)
* '.' floor '@' spawn point (walkable)
* '#' wall 'W' window '=' bar counter 'k' keg (barrel) 'h' shelf of bottles
* 'T' table 'c' chair 'B' barrel 'C' crate 'D' door
* '.' floor 'r' rug '@' spawn
*
* Anything NOT in FLOOR_CHARS is treated as solid by the Room engine. The layout is
* generated procedurally so every row is guaranteed the same width (ragged ASCII maps
* are a classic source of off-by-one collision bugs).
* Walkable = FLOOR_CHARS ('.', '@', 'r' rug, 'c' chair, 'D' door). Everything else is solid, so
* players weave between the furniture. The bartender stands BEHIND the bar (the row between the top
* wall and the counter).
*/
// Characters an Occupant may stand on.
const FLOOR_CHARS = new Set(['.', '@']);
// Characters an Occupant may stand on. Chairs are walkable so standing on one reads as "sitting".
const FLOOR_CHARS = new Set(['.', '@', 'r', 'c', 'D']);
/**
* Build the default tavern as an array of equal-length strings.
* @param {number} cols
* @param {number} rows
* @returns {string[]}
*/
function buildDefaultTavern(cols = 26, rows = 13) {
function buildDefaultTavern(cols = 28, rows = 15) {
const grid = [];
for (let y = 0; y < rows; y++) {
const row = new Array(cols);
@@ -30,25 +26,36 @@ function buildDefaultTavern(cols = 26, rows = 13) {
}
grid.push(row);
}
// set: overwrite anything (incl. walls, for windows/door). put: don't clobber walls.
const set = (x, y, ch) => { if (grid[y] && x >= 0 && x < cols) grid[y][x] = ch; };
const put = (x, y, ch) => { if (grid[y] && grid[y][x] !== undefined && grid[y][x] !== '#') grid[y][x] = ch; };
const put = (x, y, ch) => {
if (grid[y] && grid[y][x] !== undefined && grid[y][x] !== '#') grid[y][x] = ch;
};
// Windows punched into the top wall and the two side walls.
for (let x = 3; x < cols - 3; x += 4) set(x, 0, 'W');
for (let y = 4; y < rows - 3; y += 4) { set(0, y, 'W'); set(cols - 1, y, 'W'); }
// Bar counter along the top interior wall.
for (let x = 3; x <= 16; x++) put(x, 2, '=');
// The bar: a counter across the top interior (row 2), a shelf of bottles behind it (row 1),
// and a keg (barrel) bracketing each end. Row 1 is the bartender's space, sealed off by the
// counter — players can't get behind it.
const barY = 2, barX0 = 3, barX1 = cols - 5;
for (let x = barX0; x <= barX1; x++) set(x, barY, '=');
for (let x = barX0; x <= barX1; x += 2) set(x, barY - 1, 'h'); // bottles behind the bar
set(barX0 - 1, barY, 'k'); set(barX1 + 1, barY, 'k'); // kegs bracket the counter
// Table + stool clusters (2x2 blocks of 'o').
const tables = [[5, 5], [12, 5], [18, 5], [5, 9], [12, 9], [18, 9]];
for (const [tx, ty] of tables) {
put(tx, ty, 'o'); put(tx + 1, ty, 'o');
put(tx, ty + 1, 'o'); put(tx + 1, ty + 1, 'o');
}
// Tables (each with a chair below it), spread across the main floor with walkable aisles.
const tables = [[5, 6], [12, 6], [19, 6], [5, 10], [12, 10], [19, 10]];
for (const [tx, ty] of tables) { put(tx, ty, 'T'); put(tx, ty + 1, 'c'); }
// Spawn pad near the entrance (bottom-center). Two tiles so early tests have a
// deterministic adjacent pair to exercise occupant-collision rules.
put(12, 11, '@');
put(13, 11, '@');
// A rug in the centre of the room.
for (let y = 8; y <= 9; y++) for (let x = 11; x <= 15; x++) put(x, y, 'r');
// Corner decor — barrels and crates.
put(1, 1, 'B'); put(cols - 2, 1, 'C'); put(1, rows - 2, 'C'); put(cols - 2, rows - 2, 'B');
// Door + spawn pad at the bottom-centre (two spawn tiles for deterministic adjacency tests).
const mid = Math.floor(cols / 2);
set(mid, rows - 1, 'D');
put(mid, rows - 2, '@'); put(mid - 1, rows - 2, '@');
return grid.map(r => r.join(''));
}
+1 -1
View File
@@ -9,7 +9,7 @@ describe('sceneFromTavern (render kit adapter)', () => {
};
const state = {
cols: 3, rows: 2,
layout: ['#=@', 'o..'],
layout: ['#=@', 'T..'],
occupants: [{ id: 'a', x: 2, y: 0, avatar: 'green', appearance, facing: 'left', name: 'Al' }]
};
const s = scene.sceneFromTavern(state, 'a');