v0.4.2 fixed passenger trains not stopping at office. changeed revenue for train completing run - 1 point to every player when train exits division
This commit is contained in:
+16
-1
@@ -116,6 +116,12 @@ export type Game = {
|
||||
* clear it when the card is played — no element carries the class once the card leaves the hand.
|
||||
*/
|
||||
justDrawn: CardId | null;
|
||||
/**
|
||||
* A one-line announcement to flash, once. Drained like `scheduled` rather than read like `Frame`,
|
||||
* because it marks a MOMENT — the only event in the game that scores for everybody without anyone
|
||||
* having taken a turn to cause it.
|
||||
*/
|
||||
announced: string | null;
|
||||
};
|
||||
|
||||
/** How each intent kind is introduced in the action list, in the order they should appear. */
|
||||
@@ -150,7 +156,7 @@ export const SOLO_PLAYER = 'Solitaire';
|
||||
|
||||
export function newGame(seed: number, config: GameConfig = SOLO_CONFIG): Game {
|
||||
const state = createGame({ id: `web-${seed}`, seed, config, playerNames: [SOLO_PLAYER] });
|
||||
const game: Game = { state, seed, history: [], log: [], mustPlayCard: false, cues: [], scheduled: null, justDrawn: null };
|
||||
const game: Game = { state, seed, history: [], log: [], mustPlayCard: false, cues: [], scheduled: null, justDrawn: null, announced: null };
|
||||
// A history that opens mid-Stage reads as though something was missed. Say what the game IS
|
||||
// first, then let the clock take over.
|
||||
game.log.push({ text: 'Game Begins', tone: 'start' });
|
||||
@@ -685,6 +691,15 @@ function record(game: Game, events: GameEvent[], actor: PlayerIndex | null = nul
|
||||
for (const e of events) if (e.type === 'trainScheduled') game.scheduled = e.slot;
|
||||
// Which card just came into hand, so the row can badge it. Last one wins for the same reason.
|
||||
for (const e of events) if (e.type === 'cardDrawn') game.justDrawn = e.cardId;
|
||||
// A train that ran the whole Division pays EVERY player, and nobody did anything on this turn to
|
||||
// make it happen — so it is announced rather than left to be found in the log.
|
||||
for (const e of events) {
|
||||
if (e.type === 'trainCompleted') {
|
||||
game.announced =
|
||||
`Train ${e.isExtra ? 'X' : ''}${e.trainNumber} has completed its run, leaving via the ` +
|
||||
`${e.side === 'east' ? 'Eastern' : 'Western'} Division Point. All players get 1 Revenue.`;
|
||||
}
|
||||
}
|
||||
// Keep the log bounded; the full history lives in `history` and can be replayed.
|
||||
if (game.log.length > 400) game.log.splice(0, game.log.length - 400);
|
||||
}
|
||||
|
||||
@@ -459,6 +459,20 @@ function render(): void {
|
||||
}
|
||||
lastPhase = f.phaseKey;
|
||||
|
||||
/**
|
||||
* A train completing its run pays every player and nobody took a turn to cause it, so it is said
|
||||
* out loud rather than left in the log. Drained, so it shows once and does not re-fire on a redraw.
|
||||
*/
|
||||
const announcement = session.takeAnnouncement();
|
||||
if (announcement) {
|
||||
const el = $('announce');
|
||||
el.textContent = announcement;
|
||||
el.className = 'shown';
|
||||
window.setTimeout(() => {
|
||||
if (el.textContent === announcement) el.className = '';
|
||||
}, 4200);
|
||||
}
|
||||
|
||||
renderDistrict(f);
|
||||
renderActions(menu, f, justSet);
|
||||
renderUndo();
|
||||
|
||||
@@ -88,6 +88,14 @@ button.cardact.discard{color:#d6b48a}
|
||||
box-shadow:0 6px 22px rgba(0,0,0,.5)}
|
||||
#phasenote.shown{opacity:1;transform:translateX(-50%) translateY(0)}
|
||||
@media(prefers-reduced-motion:reduce){#phasenote{transition:none}}
|
||||
/* A train completing its run pays EVERY player, so it is announced rather than left to the log —
|
||||
green like a revenue gain, and its own element so it cannot fight the phase note for the slot. */
|
||||
#announce{position:fixed;left:50%;top:14px;transform:translateX(-50%) translateY(-14px);z-index:41;
|
||||
background:#16301f;border:1px solid #4c9a6a;color:#c8f0d8;border-radius:14px;padding:7px 18px;
|
||||
font-size:14px;font-weight:700;opacity:0;pointer-events:none;transition:opacity .18s,transform .18s;
|
||||
box-shadow:0 6px 22px rgba(0,0,0,.5);max-width:min(680px,90vw);text-align:center}
|
||||
#announce.shown{opacity:1;transform:translateX(-50%) translateY(0)}
|
||||
@media(prefers-reduced-motion:reduce){#announce{transition:none}}
|
||||
/* The die's answer, said where the player is looking. Green like a revenue gain — it is a good
|
||||
thing that just happened — and it disappears with the next render, as a moment should. */
|
||||
.scheduled{display:block;font-size:11.5px;margin:0 0 7px;padding:3px 9px;border-radius:5px;
|
||||
@@ -185,6 +193,7 @@ ul.blocked li{padding:2px 0}
|
||||
<!-- Announces a phase CHANGE. The turn chart says where you are; this says that it moved, which is
|
||||
what you miss when the automatic phases run between two clicks. -->
|
||||
<div id="phasenote"></div>
|
||||
<div id="announce"></div>
|
||||
|
||||
<main>
|
||||
<div>
|
||||
|
||||
@@ -86,6 +86,8 @@ export type Session = {
|
||||
takeCues(): string[];
|
||||
/** The timetable slot the last 1D12 filled, once. Draining. */
|
||||
takeScheduled(): number | null;
|
||||
/** A one-line announcement to flash, once. Draining. */
|
||||
takeAnnouncement(): string | null;
|
||||
/** The card most recently drawn into this seat's hand. Persists until another draw replaces it. */
|
||||
justDrawn(): string | null;
|
||||
};
|
||||
@@ -146,6 +148,11 @@ export function createLocalSession(seed: number, config?: GameConfig): LocalSess
|
||||
game.scheduled = null;
|
||||
return slot;
|
||||
},
|
||||
takeAnnouncement: () => {
|
||||
const text = game.announced;
|
||||
game.announced = null;
|
||||
return text;
|
||||
},
|
||||
justDrawn: () => game.justDrawn,
|
||||
|
||||
seed: () => game.seed,
|
||||
@@ -159,6 +166,7 @@ export function createLocalSession(seed: number, config?: GameConfig): LocalSess
|
||||
back.cues.length = 0;
|
||||
back.scheduled = null;
|
||||
back.justDrawn = null;
|
||||
back.announced = null;
|
||||
changed();
|
||||
return true;
|
||||
},
|
||||
@@ -166,6 +174,7 @@ export function createLocalSession(seed: number, config?: GameConfig): LocalSess
|
||||
game = fromSave(save);
|
||||
// Restoring replays the whole history and re-records every draw; none of it is news.
|
||||
game.justDrawn = null;
|
||||
game.announced = null;
|
||||
changed();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -124,6 +124,17 @@ export function playCue(cue: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cue === 'completed') {
|
||||
// A train clearing the Division and whistling off: two long horn notes falling away, with the
|
||||
// second quieter and lower so it reads as distance rather than repetition. Longer than any other
|
||||
// cue on purpose — it is the only one that pays every player, and it should land as an event.
|
||||
tone(t, 392, 0.55, 0.13, 'sawtooth', 370);
|
||||
tone(t, 262, 0.55, 0.09, 'triangle', 247);
|
||||
tone(t + 0.42, 330, 0.75, 0.08, 'sawtooth', 300);
|
||||
tone(t + 0.42, 220, 0.75, 0.055, 'triangle', 200);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cue === 'day') {
|
||||
// The grade-crossing bell: two strikes. Inharmonic partials separate a bell from a beep.
|
||||
for (const at of [t, t + 0.22]) {
|
||||
|
||||
Reference in New Issue
Block a user