Show the whole Division mainline. district now has option for auto hiding on phases where not needed. scaffold for multi-player geometry showing division. regions on mainline cards implemented. bug fix on laying rail cards. optimized replay (carried links forward). Add sounds, first pass
This commit is contained in:
+82
-14
@@ -5,8 +5,9 @@
|
||||
* is answered by the engine or by the shared view helpers.
|
||||
*/
|
||||
|
||||
import { BOARD_CSS, divisionSvg, ghostSvg, officeSvg } from '../sim/board-svg.ts';
|
||||
import { BOARD_CSS, divisionSvg, officeSvg } from '../sim/board-svg.ts';
|
||||
import { TOOLTIP_CSS, installTooltips } from './tooltip.ts';
|
||||
import { play } from './sound.ts';
|
||||
import type { Game } from './game.ts';
|
||||
import {
|
||||
actionMenu,
|
||||
@@ -26,6 +27,28 @@ let game: Game;
|
||||
let selected: string | null = null;
|
||||
/** A square picked on the board, waiting for a rotation. */
|
||||
let pendingAt: string | null = null;
|
||||
/**
|
||||
* AUTO-FOCUS on the district.
|
||||
*
|
||||
* It is only worth the vertical space during the phases that change it — Local Operations, where
|
||||
* cards and track are placed and the crew switches, and Cargo, where loads move. During New Train,
|
||||
* Mainline and Supervisor Shift nothing in the district moves and the Division map is what matters.
|
||||
*
|
||||
* 'auto' follows the phase; 'open' and 'closed' are the player overriding it and stay put until
|
||||
* they change it again. Display only — in a multiplayer game two players may reasonably want it
|
||||
* set differently, so this must never become part of game state.
|
||||
*/
|
||||
let districtMode: 'auto' | 'open' | 'closed' = 'auto';
|
||||
/**
|
||||
* Sound, OFF until asked for.
|
||||
*
|
||||
* Everything it plays is synthesised rather than recorded, so it is a placeholder for real audio
|
||||
* rather than the finished thing — and a playtester who did not ask for noise should not get any.
|
||||
* One click in the title bar turns it on, and that click is also the gesture browsers require
|
||||
* before any audio may start.
|
||||
*/
|
||||
let soundOn = false;
|
||||
const FOCUS_PHASES = new Set(['localOps', 'loadUnload']);
|
||||
|
||||
const $ = (id: string): HTMLElement => {
|
||||
const el = document.getElementById(id);
|
||||
@@ -149,7 +172,16 @@ function render(): void {
|
||||
// -- board. Both renderers are shared with the replay so the two can never draw different
|
||||
// pictures of the same position.
|
||||
const grid = $('grid');
|
||||
grid.innerHTML = officeSvg(f.cells, f.runningRow);
|
||||
// The empty squares a legal placement would extend the district onto. Drawn by `officeSvg` in the
|
||||
// same pass so they share its origin and its canvas: a target outside the canvas is a legal move
|
||||
// the player cannot click.
|
||||
const ghostCoords = [...spotsAt.keys()]
|
||||
.filter((k) => !f.cells.some((c) => `${c.row},${c.col}` === k))
|
||||
.map((k) => {
|
||||
const [gr, gc] = k.split(',').map(Number);
|
||||
return { row: gr!, col: gc! };
|
||||
});
|
||||
grid.innerHTML = officeSvg(f.cells, f.runningRow, ghostCoords);
|
||||
|
||||
// Highlighting rides on top of the drawing: outline the legal squares and make them clickable.
|
||||
for (const [key, list] of spotsAt) {
|
||||
@@ -160,18 +192,11 @@ function render(): void {
|
||||
(g as unknown as HTMLElement).onclick = () => pick(key, list);
|
||||
}
|
||||
}
|
||||
// A legal EMPTY square has no card to outline, so draw a target for it.
|
||||
const ghosts = [...spotsAt.entries()].filter(([k]) => !f.cells.some((c) => `${c.row},${c.col}` === k));
|
||||
if (ghosts.length > 0) {
|
||||
const coords = ghosts.map(([k]) => {
|
||||
const [gr, gc] = k.split(',').map(Number);
|
||||
return { row: gr!, col: gc! };
|
||||
});
|
||||
grid.innerHTML = grid.innerHTML.replace('</svg>', ghostSvg(coords, f.cells, f.runningRow) + '</svg>');
|
||||
for (const [key, list] of ghosts) {
|
||||
const g = grid.querySelector(`g[data-ghost="${key}"]`);
|
||||
if (g) (g as unknown as HTMLElement).onclick = () => pick(key, list);
|
||||
}
|
||||
// Wire the targets. They are already in the SVG, so nothing is re-serialised here.
|
||||
for (const [key, list] of spotsAt) {
|
||||
if (f.cells.some((c) => `${c.row},${c.col}` === key)) continue;
|
||||
const g = grid.querySelector(`g[data-ghost="${key}"]`);
|
||||
if (g) (g as unknown as HTMLElement).onclick = () => pick(key, list);
|
||||
}
|
||||
|
||||
function pick(key: string, list: { label: string; index: number }[]): void {
|
||||
@@ -261,10 +286,38 @@ function render(): void {
|
||||
.join('');
|
||||
log.scrollTop = log.scrollHeight;
|
||||
|
||||
renderDistrict(f);
|
||||
renderActions(menu);
|
||||
|
||||
// Drain whatever the last batch of events earned. Cleared either way, so turning sound on does
|
||||
// not then play a backlog of everything that happened while it was off.
|
||||
const cues = game.cues.splice(0, game.cues.length);
|
||||
if (soundOn) for (const c of cues) play(c);
|
||||
|
||||
save();
|
||||
}
|
||||
|
||||
function renderDistrict(f: ReturnType<typeof view>): void {
|
||||
const open = districtMode === 'auto' ? FOCUS_PHASES.has(f.phaseKey) : districtMode === 'open';
|
||||
const sec = $('district');
|
||||
if (open) sec.classList.remove('folded');
|
||||
else sec.classList.add('folded');
|
||||
|
||||
const cars = f.cells.reduce((n, c) => n + c.cars.length, 0);
|
||||
const crew = f.cells.filter((c) => c.tray).length;
|
||||
$('districtsummary').textContent =
|
||||
`${f.cells.length} cards · ${f.facilities.length} facilities · ${cars} cars standing` +
|
||||
(crew > 0 ? ` · ${crew} crew on the board` : '');
|
||||
|
||||
const btn = $('districttoggle');
|
||||
btn.textContent = districtMode === 'auto' ? (open ? 'auto · open' : 'auto · folded') : districtMode;
|
||||
btn.onclick = () => {
|
||||
// auto -> the opposite of what it is doing now -> back to auto.
|
||||
districtMode = districtMode === 'auto' ? (open ? 'closed' : 'open') : 'auto';
|
||||
render();
|
||||
};
|
||||
}
|
||||
|
||||
function boxes(items: string[], cap: number): string {
|
||||
let out = '';
|
||||
for (let i = 0; i < Math.max(cap, items.length); i++) {
|
||||
@@ -439,4 +492,19 @@ installTooltips();
|
||||
const saveBtn = document.getElementById('savefile');
|
||||
if (saveBtn) saveBtn.onclick = downloadSave;
|
||||
|
||||
const soundBtn = document.getElementById('sound');
|
||||
if (soundBtn) {
|
||||
const paint = (): void => {
|
||||
soundBtn.textContent = soundOn ? '\u{1F50A} sound' : '\u{1F507} muted';
|
||||
};
|
||||
soundBtn.onclick = () => {
|
||||
soundOn = !soundOn;
|
||||
paint();
|
||||
// Confirm the change audibly — the one press where a sound is unambiguously wanted, and it
|
||||
// doubles as the user gesture the browser needs before any audio may start.
|
||||
if (soundOn) play('stage');
|
||||
};
|
||||
paint();
|
||||
}
|
||||
|
||||
start();
|
||||
|
||||
Reference in New Issue
Block a user