diff --git a/public/boardRender.js b/public/boardRender.js index bb72e0e..7b05665 100644 --- a/public/boardRender.js +++ b/public/boardRender.js @@ -24,22 +24,27 @@ const NODE_H = 60; const PLAYER_TOKEN_R = 12; const TYPE_STYLE = { - start: { icon: '๐ฉ', shape: 'circle' }, finish: { icon: '๐', shape: 'circle' }, choice: { icon: '๐', shape: 'diamond' }, - money: { icon: '๐ฐ', shape: 'rect' }, + stop: { icon: '๐', shape: 'diamond' }, + payday: { icon: '๐ต', shape: 'rect' }, + cash_bonus: { icon: '๐ฐ', shape: 'rect' }, + action_space: { icon: '๐ฏ', shape: 'rect' }, + dice_space: { icon: '๐ฒ', shape: 'rect' }, + roll_table_ref: { icon: '๐', shape: 'rect' }, + inline_table: { icon: '๐ฐ', shape: 'rect' }, event: { icon: 'โจ', shape: 'rect' }, }; /** Pure layout computation โ no DOM. Returns { positions, edges, width, height }. */ -export function computeLayout(board, colsPerRow = 14) { +export function computeLayout(board, colsPerRow = 20) { const ids = Object.keys(board.spaces); const outEdges = new Map(ids.map((id) => [id, []])); const predecessors = new Map(ids.map((id) => [id, []])); for (const id of ids) { const space = board.spaces[id]; - const targets = space.type === 'choice' ? space.choices : (space.next ? [space.next] : []); + const targets = space.type === 'choice' && space.choices?.length ? space.choices : (space.next ? [space.next] : []); for (const t of targets) { outEdges.get(id).push(t); predecessors.get(t).push(id); @@ -59,14 +64,15 @@ export function computeLayout(board, colsPerRow = 14) { children.forEach((childId, i) => { col.set(childId, Math.max(col.get(childId) ?? -Infinity, col.get(id) + 1)); - // Choice spaces fan their N branches out symmetrically around the - // parent's lane (works for 2-way, 3-way, ... forks alike); anything - // else just inherits the parent's lane unchanged. A join (>1 + // Any space with more than one outgoing edge fans its branches out + // symmetrically around the parent's lane (works for 2-way, 3-way, ... + // forks alike, whatever type the space is โ not just 'choice'); a + // single child just inherits the parent's lane unchanged. A join (>1 // predecessor) accumulates every incoming contribution and averages // once all of its predecessors have been processed (guaranteed by the // time `remaining` hits 0, since that only happens after every // in-edge is visited). - const offset = space.type === 'choice' ? i - (children.length - 1) / 2 : 0; + const offset = children.length > 1 ? i - (children.length - 1) / 2 : 0; const contribution = laneSum.get(id) + offset; laneSum.set(childId, (laneSum.get(childId) ?? 0) + contribution); @@ -294,8 +300,9 @@ export class BoardView { _buildNode(space, pos) { const style = TYPE_STYLE[space.type] ?? TYPE_STYLE.event; + const isStart = space.id === this.board.startSpaceId; const node = svgEl('g', { - class: `space space-${space.type}`, + class: `space space-${space.type}${isStart ? ' space-start-marker' : ''}`, transform: `translate(${pos.x}, ${pos.y})`, 'data-space-id': space.id, }); @@ -307,7 +314,7 @@ export class BoardView { shapeGroup.appendChild(this._shapeEl(style.shape, 3, 4, 'space-shadow')); shapeGroup.appendChild(this._shapeEl(style.shape, 0, 0, 'space-shape')); shapeGroup.appendChild(this._shapeEl(style.shape, 0, 0, 'space-gloss')); - if (space.type === 'choice') { + if (space.type === 'choice' || space.type === 'stop') { shapeGroup.appendChild(svgEl('rect', { x: -4, y: NODE_H / 2 + 2, width: 8, height: 22, class: 'choice-post' })); } node.appendChild(shapeGroup); @@ -315,15 +322,14 @@ export class BoardView { const fo = svgEl('foreignObject', { x: -NODE_W / 2, y: -NODE_H / 2, width: NODE_W, height: NODE_H }); const div = document.createElement('div'); div.className = 'space-label'; - div.innerHTML = `${escapeHtml(space.label)}`; + const icon = isStart ? '๐ฉ' : style.icon; + div.innerHTML = `${escapeHtml(space.label)}`; fo.appendChild(div); node.appendChild(fo); - if (space.cash) { - const badge = svgEl('text', { - class: `cash-badge ${space.cash > 0 ? 'pos' : 'neg'}`, y: NODE_H / 2 + 17, 'text-anchor': 'middle', - }); - badge.textContent = `${space.cash > 0 ? '+' : ''}${space.cash}`; + if (space.die) { + const badge = svgEl('text', { class: 'die-badge', y: NODE_H / 2 + 17, 'text-anchor': 'middle' }); + badge.textContent = space.die; node.appendChild(badge); } diff --git a/public/index.html b/public/index.html index 9e8b180..1e708ee 100644 --- a/public/index.html +++ b/public/index.html @@ -50,6 +50,8 @@ border: 1.5px solid var(--line); border-radius: 10px; background: #fffaf0; margin-bottom: 6px; font-size: 14px; } ul.player-list li.active { border-color: var(--marigold); background: #fff3dc; } + .stat-line { display: block; color: var(--ink-soft); font-size: 12px; margin-top: 2px; } + .todo-mark { font-size: 11px; opacity: .8; } .dot { width: 12px; height: 12px; border-radius: 50%; flex: none; } .hint { color: var(--ink-soft); font-size: 13px; } .turn-banner { font-family: "Baloo 2"; font-weight: 700; font-size: 18px; margin-bottom: 10px; } @@ -101,21 +103,25 @@ fill: #fffaf0; stroke: var(--line); stroke-width: 2.5; filter: drop-shadow(0 1px 1px rgba(0,0,0,.25)); } - .space-start .space-shape { fill: var(--good); stroke: #2c6b44; } .space-finish .space-shape { fill: var(--marigold); stroke: #b97f18; } .space-choice .space-shape { fill: var(--bad); stroke: #8f3120; } - .space-event .space-shape { fill: #e9d9f7; stroke: #b79bd6; } + .space-stop .space-shape { fill: #d6553b; stroke: #8f3120; } + .space-payday .space-shape { fill: #d7ecd0; stroke: #3f8f5f; } + .space-cash_bonus .space-shape { fill: #fbe4b8; stroke: #b97f18; } + .space-action_space .space-shape, + .space-dice_space .space-shape, + .space-roll_table_ref .space-shape { fill: #cfe0f0; stroke: #3f6ea5; } + .space-inline_table .space-shape { fill: #dde8f2; stroke: #7a97b5; stroke-dasharray: 3 2; } + .space-event .space-shape { fill: #fdf6e3; stroke: #c9b98a; } .space-label { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; font-family: "Baloo 2"; font-weight: 700; font-size: 10.5px; line-height: 1.15; color: var(--ink); pointer-events: none; gap: 1px; } - .space-start .space-label, .space-choice .space-label { color: #fff; } + .space-choice .space-label, .space-stop .space-label { color: #fff; } .space-icon { font-size: 15px; } .space-text { max-width: 80px; overflow: hidden; text-overflow: ellipsis; } - .cash-badge { font-family: "Baloo 2"; font-weight: 800; font-size: 11px; } - .cash-badge.pos { fill: var(--good); } - .cash-badge.neg { fill: var(--bad); } + .die-badge { font-family: "Baloo 2"; font-weight: 800; font-size: 10px; fill: var(--ink-soft); } .space.highlight .space-shape { stroke: #fff2c4; stroke-width: 4; animation: pulseGlow 1.1s ease-in-out infinite; } @@ -296,7 +302,8 @@ const spaceLabel = board.spaces[p.position]?.label ?? p.position; li.innerHTML = ` ${escapeHtml(p.name)}${p.id === self.playerId ? ' (you)' : ''} - โ ${escapeHtml(spaceLabel)} ยท $${p.cash}`; + โ ${escapeHtml(spaceLabel)} + ๐ต$${p.cash} ยท โค๏ธ${p.love} ยท ๐${p.education} ยท ๐${p.wealth} ยท ๐${p.age}`; els.gamePlayerList.appendChild(li); } @@ -325,9 +332,9 @@ els.logFeed.innerHTML = state.log.slice().reverse().map((entry) => { const p = state.players[entry.playerId]; - const sign = entry.cashDelta > 0 ? '+' : ''; - const cashPart = entry.cashDelta ? ` (${sign}${entry.cashDelta})` : ''; - return `