Fix caching, Limits placement, and explain the board better
This commit is contained in:
+31
-2
@@ -32,7 +32,14 @@ import type { GameConfig, GameState, PlayerIndex } from '../engine/state.ts';
|
||||
import { narrate } from '../sim/narrate.ts';
|
||||
// Import from the view module, NOT replay.ts — replay.ts writes files and reads process.argv,
|
||||
// which would pull node:fs into a browser bundle.
|
||||
import { cardName, describeIntent, geometryLabel, snapshot, variantLabel } from '../sim/view.ts';
|
||||
import {
|
||||
cardName,
|
||||
describeIntent,
|
||||
geometryLabel,
|
||||
snapshot,
|
||||
trainName,
|
||||
variantLabel,
|
||||
} from '../sim/view.ts';
|
||||
import type { TrackGeometry } from '../engine/content.ts';
|
||||
import { variantsFor } from '../engine/track.ts';
|
||||
import type { Frame } from '../sim/view.ts';
|
||||
@@ -258,6 +265,25 @@ export function submit(game: Game, intent: Intent): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which cards in hand can be played RIGHT NOW, in hand order.
|
||||
*
|
||||
* A hand where some cards are simply unplayable — a Station upgrade before the Office is a Depot, a
|
||||
* Modifier with no Facility to sit beside — looks identical to one where everything is available.
|
||||
* Derived from `legalActions`, so it cannot disagree with what the buttons offer.
|
||||
*/
|
||||
export function handPlayable(game: Game): boolean[] {
|
||||
const hand = game.state.decks.hands.get(0) ?? [];
|
||||
const actor = currentActor(game);
|
||||
if (actor === null) return hand.map(() => false);
|
||||
const playable = new Set(
|
||||
legalActions(game.state, actor)
|
||||
.filter((i) => i.type === 'card.play')
|
||||
.map((i) => (i as Extract<Intent, { type: 'card.play' }>).cardId),
|
||||
);
|
||||
return hand.map((id) => playable.has(id));
|
||||
}
|
||||
|
||||
/** The board as the replay draws it, so the live game and the replay agree. */
|
||||
export function view(game: Game): Frame {
|
||||
return snapshot(game.state, [], null);
|
||||
@@ -267,7 +293,10 @@ function record(game: Game, events: GameEvent[]): void {
|
||||
for (const e of events) {
|
||||
// The same filter the replay uses: actor changes and phase bookkeeping are noise on screen.
|
||||
if (e.type === 'actorChanged') continue;
|
||||
const n = narrate(e, { cardName: (id) => cardName(game.state, id) });
|
||||
const n = narrate(e, {
|
||||
cardName: (id) => cardName(game.state, id),
|
||||
trainName: (id) => trainName(game.state, id),
|
||||
});
|
||||
game.log.push({ text: n.text, tone: n.tone });
|
||||
}
|
||||
// Keep the log bounded; the full history lives in `history` and can be replayed.
|
||||
|
||||
+5
-1
@@ -24,6 +24,8 @@ header b{font-size:16px}
|
||||
.handcard{border-top:1px solid var(--line);padding:4px 0}
|
||||
.handcard:first-child{border-top:0}
|
||||
.handcard .what{font-size:11px;line-height:1.35}
|
||||
.handcard.unplayable{opacity:.55}
|
||||
.tag{font-size:10px;background:rgba(190,120,40,.3);border-radius:3px;padding:0 5px}
|
||||
main{display:grid;grid-template-columns:minmax(0,1fr) 400px;gap:14px;padding:14px;align-items:start}
|
||||
@media(max-width:1100px){main{grid-template-columns:1fr}}
|
||||
section{background:var(--panel);border:1px solid var(--line);border-radius:7px;
|
||||
@@ -50,6 +52,8 @@ section{background:var(--panel);border:1px solid var(--line);border-radius:7px;
|
||||
display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center}
|
||||
.cell.ghost .nm{font-weight:400;color:#5aa9e6;font-size:11px}
|
||||
.cell .nm{font-size:12px;font-weight:600}
|
||||
.cell .what{font-size:10px;line-height:1.25;margin-top:2px}
|
||||
.cell{cursor:help}
|
||||
.coord{font-size:10px}
|
||||
.enh{font-size:10px;background:rgba(90,140,220,.30);border-radius:3px;padding:1px 4px;margin-top:2px}
|
||||
.crew{font-size:11px;margin-top:3px;color:#8fd6a0}
|
||||
@@ -107,7 +111,7 @@ ul.blocked li{padding:2px 0}
|
||||
<main>
|
||||
<div>
|
||||
<section><h2>The Division — west to east</h2><div id="division"></div></section>
|
||||
<section><h2>Your Office Area</h2><div id="grid"></div></section>
|
||||
<section><h2>Your Office Area <span class="dim" style="text-transform:none;letter-spacing:0">— hover any card for the full explanation</span></h2><div id="grid"></div></section>
|
||||
<section><h2>History</h2><div id="log"></div></section>
|
||||
</div>
|
||||
|
||||
|
||||
+17
-5
@@ -8,6 +8,7 @@
|
||||
import type { Game } from './game.ts';
|
||||
import {
|
||||
actionMenu,
|
||||
handPlayable,
|
||||
currentActor,
|
||||
fromSave,
|
||||
newGame,
|
||||
@@ -130,12 +131,15 @@ function render(): void {
|
||||
: '<div></div>';
|
||||
continue;
|
||||
}
|
||||
// The full explanation goes in `title` so hovering any card says what it does, with a short
|
||||
// version printed on the card itself — a name alone tells a player nothing once it is played.
|
||||
cells +=
|
||||
`<div class="cell ${cell.running ? 'run ' : ''}${cell.kind === 'office' ? 'office ' : ''}` +
|
||||
`${cell.kind === 'modifier' ? 'modifier ' : ''}${spots ? 'legal' : ''}"` +
|
||||
`${spots ? ` data-at="${r},${c}"` : ''}>` +
|
||||
`${spots ? ` data-at="${r},${c}"` : ''} title="${esc(cell.label)} — ${esc(cell.what)}">` +
|
||||
`<div class="nm">${esc(cell.label)}</div>` +
|
||||
`<div class="dim coord">(${r},${c})</div>` +
|
||||
`<div class="dim what">${esc(short(cell.what))}</div>` +
|
||||
(cell.enhancements.length
|
||||
? `<div class="enh">${cell.enhancements.map(esc).join(' · ')}</div>`
|
||||
: '') +
|
||||
@@ -194,16 +198,18 @@ function render(): void {
|
||||
.join('');
|
||||
|
||||
// Name AND effect. A hand of names alone tells a player nothing about what they can do.
|
||||
const cardRow = (name: string, why: string): string =>
|
||||
`<div class="handcard"><b>${esc(name)}</b>` +
|
||||
const cardRow = (name: string, why: string, playable: boolean | null): string =>
|
||||
`<div class="handcard${playable === false ? ' unplayable' : ''}"><b>${esc(name)}</b>` +
|
||||
(playable === false ? ' <span class="tag">not playable yet</span>' : '') +
|
||||
(why ? `<div class="dim what">${esc(why)}</div>` : '') +
|
||||
`</div>`;
|
||||
|
||||
const canPlay = handPlayable(game);
|
||||
$('hand').innerHTML = f.hand.length
|
||||
? f.hand.map((h, i) => cardRow(h, f.handWhat[i] ?? '')).join('')
|
||||
? f.hand.map((h, i) => cardRow(h, f.handWhat[i] ?? '', canPlay[i] ?? null)).join('')
|
||||
: '<span class="dim">empty</span>';
|
||||
$('depts').innerHTML = f.departments.length
|
||||
? f.departments.map((d, i) => cardRow(d, f.departmentsWhat[i] ?? '')).join('')
|
||||
? f.departments.map((d, i) => cardRow(d, f.departmentsWhat[i] ?? '', null)).join('')
|
||||
: '<span class="dim">none</span>';
|
||||
|
||||
const total = f.trackSupply.reduce((n, t) => n + t.left, 0);
|
||||
@@ -235,6 +241,12 @@ function render(): void {
|
||||
save();
|
||||
}
|
||||
|
||||
/** First clause only — the cards are small, and the full text is on the tooltip. */
|
||||
function short(what: string): string {
|
||||
const head = what.split(' · ')[0] ?? what;
|
||||
return head.length > 64 ? head.slice(0, 61) + '…' : head;
|
||||
}
|
||||
|
||||
function boxes(items: string[], cap: number): string {
|
||||
let out = '';
|
||||
for (let i = 0; i < Math.max(cap, items.length); i++) {
|
||||
|
||||
Reference in New Issue
Block a user