@dreadwood-types/types 0.1.190 → 0.1.192
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +9 -0
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lanternWakeGallery.d.ts +3 -2
- package/dist/lanternWakeGallery.d.ts.map +1 -1
- package/dist/lanternWakeGallery.js +0 -2
- package/dist/lanternWakeGallery.js.map +1 -1
- package/dist/lanternWakeQuests.d.ts +69 -0
- package/dist/lanternWakeQuests.d.ts.map +1 -0
- package/dist/lanternWakeQuests.js +100 -0
- package/dist/lanternWakeQuests.js.map +1 -0
- package/dist/lanternWakeTown.d.ts +3 -2
- package/dist/lanternWakeTown.d.ts.map +1 -1
- package/dist/lanternWakeTown.js +15 -9
- package/dist/lanternWakeTown.js.map +1 -1
- package/dist/messages.d.ts +7 -0
- package/dist/messages.d.ts.map +1 -1
- package/dist/quests.d.ts.map +1 -1
- package/dist/quests.js +7 -0
- package/dist/quests.js.map +1 -1
- package/dist/storyQuests.d.ts +37 -1
- package/dist/storyQuests.d.ts.map +1 -1
- package/dist/storyQuests.js +102 -1
- package/dist/storyQuests.js.map +1 -1
- package/dist/westernTrain.d.ts +247 -100
- package/dist/westernTrain.d.ts.map +1 -1
- package/dist/westernTrain.js +460 -166
- package/dist/westernTrain.js.map +1 -1
- package/package.json +2 -2
package/dist/westernTrain.js
CHANGED
|
@@ -24,8 +24,13 @@
|
|
|
24
24
|
* client draws the same train from the clock sent with the chunks (`trainClock`). The trains run the same timetable
|
|
25
25
|
* WESTERN_TRAIN_COUNT apart. Stops other than the platform are data (WESTERN_TRAIN_STOPS, `enabled: false`) until their
|
|
26
26
|
* places are built; switching one on adds its dwell to the cycle.
|
|
27
|
+
*
|
|
28
|
+
* THE CIRCUIT VERSION (slice 2-0, plan .cursor/task-plans/2026-10-11-western-phase2-circuit-plan.md): the legs, stops and
|
|
29
|
+
* train count are one WesternTrainCircuit with a version and a key. The game deploys on a push while the backend waits
|
|
30
|
+
* for a release, so the server sends its circuit's key (`trainCircuit`, with the clock) and the client draws and times
|
|
31
|
+
* THAT circuit (westernCircuitForKey); every train sum takes the circuit as its last argument (the server's by default).
|
|
27
32
|
*/
|
|
28
|
-
import { CAPITAL_BUILDING_MODELS, HEIGHT_STEP, UNDERGROUND_END_X } from './constants.js';
|
|
33
|
+
import { CAPITAL_BUILDING_MODELS, CHUNK_SIZE, CHUNK_VIEW_RADIUS, HEIGHT_STEP, UNDERGROUND_END_X, worldRegionAt } from './constants.js';
|
|
29
34
|
import { WESTERN_CARS, WESTERN_CAUSEWAY, WESTERN_GANGWAYS, WESTERN_GAP_ROW, WESTERN_JOINTS, WESTERN_KIT_DECK_METRES, WESTERN_KNOLL, WESTERN_PLATFORM, WESTERN_PLATFORM_PIECES, WESTERN_PRECINCT_HEIGHT, WESTERN_RAKE_SLOTS, WESTERN_SEA_END, WESTERN_SEA_KERB, WESTERN_STEPS, WESTERN_TRACK, WESTERN_TRACK_Z, WESTERN_TUNNEL_MOUTH, westernCausewayHeight, } from './westernEntrance.js';
|
|
30
35
|
/* ------------------------------------------------------------------ *
|
|
31
36
|
* The docked rake (the cart-local frame).
|
|
@@ -198,20 +203,50 @@ export function inWesternIsleStub(x, z) {
|
|
|
198
203
|
return (x >= b.minX && x <= b.maxX && z >= b.minZ && z <= b.maxZ) ||
|
|
199
204
|
(x >= WESTERN_ISLE_CAUSEWAY.minX && x <= WESTERN_ISLE_CAUSEWAY.maxX && z >= WESTERN_ISLE_CAUSEWAY.minZ && z <= WESTERN_ISLE_CAUSEWAY.maxZ);
|
|
200
205
|
}
|
|
206
|
+
/* ------------------------------------------------------------------ *
|
|
207
|
+
* The timetable's constants.
|
|
208
|
+
* ------------------------------------------------------------------ */
|
|
209
|
+
/** Every train counts from this instant (a fixed date, so a restart never moves a train). */
|
|
210
|
+
export const WESTERN_TRAIN_EPOCH_MS = Date.UTC(2026, 9, 10, 0, 0, 0);
|
|
211
|
+
/** Cruising speed (tiles a second) and the rate it gets up to it and slows from it. */
|
|
212
|
+
export const WESTERN_TRAIN_TOP_SPEED = 6;
|
|
213
|
+
export const WESTERN_TRAIN_ACCEL = 0.5;
|
|
214
|
+
/** The bell rings this long before a departure; the step plates shut this long before it. */
|
|
215
|
+
export const WESTERN_TRAIN_BELL_BEFORE_MS = 15_000;
|
|
216
|
+
export const WESTERN_TRAIN_DOORS_SHUT_BEFORE_MS = 5_000;
|
|
217
|
+
/** The whistle and the bell are heard this far (tiles) from the loco. */
|
|
218
|
+
export const WESTERN_TRAIN_SOUND_RANGE = 40;
|
|
219
|
+
export const WESTERN_TRAIN_MOVING_TEXT = 'The train is moving.';
|
|
220
|
+
export const WESTERN_TRAIN_DOORS_SHUT_TEXT = 'The doors are shut: the train is about to leave.';
|
|
221
|
+
export const WESTERN_TRAIN_ABOARD_COMBAT_TEXT = 'There is no fighting aboard the train.';
|
|
222
|
+
export const WESTERN_TRAIN_ALIGHT_MOVING_TEXT = 'The train is moving. Wait until it stops at the platform.';
|
|
223
|
+
/** A stop's arriving announcement goes to its riders this long before the train stands there. */
|
|
224
|
+
export const WESTERN_TRAIN_ANNOUNCE_BEFORE_MS = 10_000;
|
|
225
|
+
export const WESTERN_TRAIN_MEMBERS_ONLY_TEXT = 'Only members may step off here. Stay aboard: the train runs on to the next stop.';
|
|
226
|
+
export const WESTERN_BORE_A = {
|
|
227
|
+
id: 'bore-a',
|
|
228
|
+
x: WESTERN_TUNNEL_X,
|
|
229
|
+
z0: WESTERN_TUNNEL_START_Z,
|
|
230
|
+
length: WESTERN_TUNNEL_LENGTH,
|
|
231
|
+
halfWidth: WESTERN_TUNNEL_HALF_WIDTH,
|
|
232
|
+
floorHeight: WESTERN_TUNNEL_FLOOR_HEIGHT,
|
|
233
|
+
sconceEvery: WESTERN_TUNNEL_SCONCE_EVERY,
|
|
234
|
+
idPrefix: 'western-tunnel',
|
|
235
|
+
};
|
|
236
|
+
/** A bore's floor tiles, walls excluded (bore A's is WESTERN_TUNNEL_BOX). */
|
|
237
|
+
export function westernBoreBox(bore) {
|
|
238
|
+
return { minX: bore.x - bore.halfWidth, maxX: bore.x + bore.halfWidth, minZ: bore.z0, maxZ: bore.z0 + bore.length - 1 };
|
|
239
|
+
}
|
|
201
240
|
const SURFACE_LENGTH = WESTERN_SEA_END.x - (WESTERN_TRACK.minX - 1) + 1; // x 199 to x 26 (the mouth tile): 174
|
|
202
241
|
/** The spike's third leg: the isle causeway's sea end west to the tile in front of the stub's portal (102 tiles). */
|
|
203
242
|
const ISLE_LENGTH = WESTERN_ISLE_SEA_END_X - (WESTERN_ISLE_MOUTH.x + 1) + 1;
|
|
204
243
|
const ISLE_C0 = SURFACE_LENGTH + WESTERN_TUNNEL_LENGTH;
|
|
205
|
-
export const WESTERN_TRAIN_SEGMENTS = [
|
|
206
|
-
{ id: 'surface', c0: 0, length: SURFACE_LENGTH, x0: WESTERN_SEA_END.x, z0: WESTERN_TRACK_Z, dx: -1, dz: 0 },
|
|
207
|
-
{ id: 'tunnel', c0: SURFACE_LENGTH, length: WESTERN_TUNNEL_LENGTH, x0: WESTERN_TUNNEL_X, z0: WESTERN_TUNNEL_START_Z, dx: 0, dz: 1 },
|
|
208
|
-
{ id: 'isle', c0: ISLE_C0, length: ISLE_LENGTH, x0: WESTERN_ISLE_SEA_END_X, z0: WESTERN_ISLE_TRACK_Z, dx: -1, dz: 0 },
|
|
209
|
-
];
|
|
210
244
|
/** c of the nose when the train stands at the isle stub (the nose on the platform's west end, as at home). */
|
|
211
245
|
export const WESTERN_ISLE_STOP_C = ISLE_C0 + (WESTERN_ISLE_SEA_END_X - WESTERN_ISLE_PLATFORM.minX);
|
|
212
|
-
export const WESTERN_TRAIN_CIRCUIT_LENGTH = WESTERN_TRAIN_SEGMENTS.reduce((n, s) => n + s.length, 0);
|
|
213
246
|
/** c of the nose when the train stands at the platform (loco's nose at x 30). */
|
|
214
247
|
export const WESTERN_TRAIN_PLATFORM_C = WESTERN_SEA_END.x - WESTERN_TRAIN_NOSE_X;
|
|
248
|
+
/** The surface leg's x range the rake is drawn on (the mouth hides it to the west, the sea to the east). */
|
|
249
|
+
export const WESTERN_TRAIN_SURFACE_DRAW = { minX: 18, maxX: WESTERN_SEA_END.x + 8 };
|
|
215
250
|
/** The lateral unit: the docked frame's +z (the sea side) for a leg heading (dx, dz). */
|
|
216
251
|
function lateralOf(seg) {
|
|
217
252
|
return { lx: seg.dz, lz: -seg.dx };
|
|
@@ -221,16 +256,256 @@ export function westernTrainSegmentYaw(seg) {
|
|
|
221
256
|
// A model facing +z yawed by t faces (sin t, cos t); the docked heading (-1, 0) is t = -pi/2.
|
|
222
257
|
return Math.atan2(seg.dx, seg.dz) + Math.PI / 2;
|
|
223
258
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
259
|
+
/** Duration (ms) of a run of D tiles from a stop to a stop: up to speed, cruise, slow to a stand. */
|
|
260
|
+
export function westernTrainRunMs(distance) {
|
|
261
|
+
const v = WESTERN_TRAIN_TOP_SPEED, a = WESTERN_TRAIN_ACCEL;
|
|
262
|
+
const ramp = (v * v) / a; // tiles spent getting up to speed and slowing again
|
|
263
|
+
const s = distance >= ramp ? distance / v + v / a : 2 * Math.sqrt(distance / a);
|
|
264
|
+
return s * 1000;
|
|
265
|
+
}
|
|
266
|
+
/** Tiles covered after t ms of a run of D tiles. */
|
|
267
|
+
export function westernTrainRunTiles(distance, tMs) {
|
|
268
|
+
const v = WESTERN_TRAIN_TOP_SPEED, a = WESTERN_TRAIN_ACCEL;
|
|
269
|
+
const total = westernTrainRunMs(distance) / 1000;
|
|
270
|
+
const t = Math.max(0, Math.min(total, tMs / 1000));
|
|
271
|
+
const peak = Math.min(v, Math.sqrt(distance * a));
|
|
272
|
+
const ta = peak / a;
|
|
273
|
+
if (t <= ta)
|
|
274
|
+
return 0.5 * a * t * t;
|
|
275
|
+
if (t >= total - ta)
|
|
276
|
+
return distance - 0.5 * a * (total - t) ** 2;
|
|
277
|
+
return 0.5 * a * ta * ta + peak * (t - ta);
|
|
278
|
+
}
|
|
279
|
+
const wrapOn = (c, n) => ((c % n) + n) % n;
|
|
280
|
+
function runningStops(stops) {
|
|
281
|
+
return stops.filter((s) => s.enabled).sort((a, b) => a.c - b.c);
|
|
282
|
+
}
|
|
283
|
+
function timetableOf(stops, length) {
|
|
284
|
+
const running = runningStops(stops);
|
|
285
|
+
const legs = [];
|
|
286
|
+
let t = 0;
|
|
287
|
+
running.forEach((stop, i) => {
|
|
288
|
+
const next = running[(i + 1) % running.length];
|
|
289
|
+
const runTiles = wrapOn(next.c - stop.c, length) || length;
|
|
290
|
+
const startMs = t;
|
|
291
|
+
const departMs = startMs + stop.dwellMs;
|
|
292
|
+
const endMs = departMs + westernTrainRunMs(runTiles);
|
|
293
|
+
legs.push({ stop, startMs, departMs, runTiles, endMs });
|
|
294
|
+
t = endMs;
|
|
295
|
+
});
|
|
296
|
+
return { cycleMs: t, legs };
|
|
297
|
+
}
|
|
298
|
+
/** 32-bit FNV-1a, hex: the circuit key's hash (stable across engines, no crypto needed). */
|
|
299
|
+
function fnv1a(text) {
|
|
300
|
+
let h = 0x811c9dc5;
|
|
301
|
+
for (let i = 0; i < text.length; i++) {
|
|
302
|
+
h ^= text.charCodeAt(i);
|
|
303
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
304
|
+
}
|
|
305
|
+
return h.toString(16).padStart(8, '0');
|
|
227
306
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
307
|
+
/**
|
|
308
|
+
* Build and check a circuit. Throws on a circuit the line cannot run: legs that are not unit steps along x or z, a
|
|
309
|
+
* surface leg not heading west (the causeway profile is the home line's), stops off the track or on the same c, no home
|
|
310
|
+
* stop or more than one, a home stop whose rake is not the docked frame itself, a train count outside 1-8.
|
|
311
|
+
*/
|
|
312
|
+
export function defineWesternCircuit(def) {
|
|
313
|
+
if (!Number.isInteger(def.version) || def.version < 1)
|
|
314
|
+
throw new Error(`circuit version ${def.version}: a whole number from 1`);
|
|
315
|
+
if (!Number.isInteger(def.trainCount) || def.trainCount < 1 || def.trainCount > 8)
|
|
316
|
+
throw new Error(`circuit v${def.version}: ${def.trainCount} trains (1-8)`);
|
|
317
|
+
if (def.segments.length === 0)
|
|
318
|
+
throw new Error(`circuit v${def.version}: no legs`);
|
|
319
|
+
let c0 = 0;
|
|
320
|
+
const segments = def.segments.map((s) => {
|
|
321
|
+
if (Math.abs(s.dx) + Math.abs(s.dz) !== 1 || !Number.isInteger(s.dx) || !Number.isInteger(s.dz))
|
|
322
|
+
throw new Error(`leg ${s.id}: not a unit step along x or z`);
|
|
323
|
+
if (!Number.isInteger(s.length) || s.length < 1)
|
|
324
|
+
throw new Error(`leg ${s.id}: length ${s.length}`);
|
|
325
|
+
if (s.kind === 'surface' && s.dx !== -1)
|
|
326
|
+
throw new Error(`leg ${s.id}: a surface leg heads west (the causeway profile)`);
|
|
327
|
+
if (s.kind === 'bore' && (!s.bore || s.dx !== 0 || s.x0 !== s.bore.x))
|
|
328
|
+
throw new Error(`leg ${s.id}: a bore leg runs down its bore's line`);
|
|
329
|
+
const seg = { ...s, c0, drawBefore: s.drawBefore ?? 4, drawAfter: s.drawAfter ?? 4 };
|
|
330
|
+
c0 += s.length;
|
|
331
|
+
return seg;
|
|
332
|
+
});
|
|
333
|
+
if (new Set(segments.map((s) => s.id)).size !== segments.length)
|
|
334
|
+
throw new Error(`circuit v${def.version}: two legs share an id`);
|
|
335
|
+
const length = c0;
|
|
336
|
+
const track = { segments, length };
|
|
337
|
+
const stops = def.stops.map((s) => ({ ...s }));
|
|
338
|
+
if (new Set(stops.map((s) => s.id)).size !== stops.length)
|
|
339
|
+
throw new Error(`circuit v${def.version}: two stops share an id`);
|
|
340
|
+
for (const s of stops)
|
|
341
|
+
if (!(s.c >= 0 && s.c < length))
|
|
342
|
+
throw new Error(`stop ${s.id}: c ${s.c} is off the track (0-${length - 1})`);
|
|
343
|
+
const running = runningStops(stops);
|
|
344
|
+
if (new Set(running.map((s) => s.c)).size !== running.length)
|
|
345
|
+
throw new Error(`circuit v${def.version}: two running stops on one c`);
|
|
346
|
+
const homes = stops.filter((s) => s.home);
|
|
347
|
+
if (homes.length !== 1 || !homes[0].enabled)
|
|
348
|
+
throw new Error(`circuit v${def.version}: exactly one home stop, running`);
|
|
349
|
+
const home = homes[0];
|
|
350
|
+
// The home stop's rake is the docked frame: standing there, every docked tile is its own world tile.
|
|
351
|
+
for (const [x, z] of [[WESTERN_TRAIN_RIDE_BOX.minX, WESTERN_TRAIN_RIDE_BOX.minZ], [WESTERN_TRAIN_RIDE_BOX.maxX, WESTERN_TRAIN_RIDE_BOX.maxZ]]) {
|
|
352
|
+
const w = riderTileOn(track, home.c, x, z);
|
|
353
|
+
if (w.x !== x || w.y !== z)
|
|
354
|
+
throw new Error(`circuit v${def.version}: the home stop's rake is not the docked frame`);
|
|
355
|
+
}
|
|
356
|
+
const homeLeg = segmentOn(track, home.c);
|
|
357
|
+
const timetable = timetableOf(stops, length);
|
|
358
|
+
const stopSteps = running.filter((s) => s.steps).flatMap((s) => stepsOn(track, s));
|
|
359
|
+
const bores = [];
|
|
360
|
+
for (const s of segments)
|
|
361
|
+
if (s.bore && !bores.includes(s.bore))
|
|
362
|
+
bores.push(s.bore);
|
|
363
|
+
const canon = JSON.stringify({
|
|
364
|
+
v: def.version,
|
|
365
|
+
n: def.trainCount,
|
|
366
|
+
clock: [WESTERN_TRAIN_EPOCH_MS, WESTERN_TRAIN_TOP_SPEED, WESTERN_TRAIN_ACCEL, WESTERN_TRAIN_DOORS_SHUT_BEFORE_MS, WESTERN_TRAIN_NOSE_X, WESTERN_TRAIN_RAKE_LENGTH],
|
|
367
|
+
legs: segments.map((s) => [s.id, s.kind, s.c0, s.length, s.x0, s.z0, s.dx, s.dz, s.drawBefore, s.drawAfter,
|
|
368
|
+
s.bore ? [s.bore.id, s.bore.x, s.bore.z0, s.bore.length, s.bore.halfWidth, s.bore.floorHeight] : null]),
|
|
369
|
+
stops: stops.map((s) => [s.id, s.c, s.dwellMs, s.enabled, !!s.home, !!s.steps, s.stepPrefix ?? null, !!s.membersOnly]),
|
|
370
|
+
});
|
|
371
|
+
return {
|
|
372
|
+
version: def.version,
|
|
373
|
+
key: `v${def.version}-${fnv1a(canon)}`,
|
|
374
|
+
segments,
|
|
375
|
+
length,
|
|
376
|
+
stops,
|
|
377
|
+
trainCount: def.trainCount,
|
|
378
|
+
timetable,
|
|
379
|
+
home,
|
|
380
|
+
homeLeg,
|
|
381
|
+
stopSteps,
|
|
382
|
+
bores,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/* The position sums on a track (exported below with the server's circuit as their default). */
|
|
386
|
+
function segmentOn(track, c) {
|
|
387
|
+
const w = wrapOn(c, track.length);
|
|
388
|
+
for (const seg of track.segments)
|
|
231
389
|
if (w >= seg.c0 && w < seg.c0 + seg.length)
|
|
232
390
|
return seg;
|
|
233
|
-
return
|
|
391
|
+
return track.segments[0];
|
|
392
|
+
}
|
|
393
|
+
function riderTileOn(track, noseC, dockedX, dockedZ) {
|
|
394
|
+
const c = westernDockedC(noseC, dockedX);
|
|
395
|
+
const p = westernTrainPointOn(segmentOn(track, c), wrapOn(c, track.length), dockedZ - WESTERN_TRACK_Z);
|
|
396
|
+
return { x: Math.round(p.x), y: Math.round(p.z) };
|
|
397
|
+
}
|
|
398
|
+
function stepsOn(track, stop) {
|
|
399
|
+
return WESTERN_STEPS.map((s) => {
|
|
400
|
+
const plate = riderTileOn(track, stop.c, s.x, s.z);
|
|
401
|
+
return {
|
|
402
|
+
stopId: stop.id,
|
|
403
|
+
car: s.car,
|
|
404
|
+
id: `${stop.stepPrefix ?? `${stop.id}-step`}-${s.car}`,
|
|
405
|
+
x: plate.x,
|
|
406
|
+
z: plate.y,
|
|
407
|
+
platform: riderTileOn(track, stop.c, s.platform[0], s.platform[1]),
|
|
408
|
+
doorway: { x: s.doorway[0], y: s.doorway[1] },
|
|
409
|
+
};
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* VERSION 1: the line as it ran from 2026-10-10 (the moving train) and 2026-10-10 (Lantern Wake): the home platform's
|
|
414
|
+
* surface leg, bore A south under the sea, Lantern Wake's leg west into its rock cap, two trains. Frozen: never edit.
|
|
415
|
+
*/
|
|
416
|
+
export const WESTERN_TRAIN_CIRCUIT_V1 = defineWesternCircuit({
|
|
417
|
+
version: 1,
|
|
418
|
+
trainCount: 2,
|
|
419
|
+
segments: [
|
|
420
|
+
{
|
|
421
|
+
id: 'surface', kind: 'surface', length: SURFACE_LENGTH, x0: WESTERN_SEA_END.x, z0: WESTERN_TRACK_Z, dx: -1, dz: 0,
|
|
422
|
+
cover: { area: 'western-line', title: 'The Western Entrance' },
|
|
423
|
+
// The mouth hides the rake to the west, the sea to the east (WESTERN_TRAIN_SURFACE_DRAW, in tiles along the leg).
|
|
424
|
+
drawBefore: WESTERN_TRAIN_SURFACE_DRAW.maxX - WESTERN_SEA_END.x,
|
|
425
|
+
drawAfter: WESTERN_SEA_END.x - WESTERN_TRAIN_SURFACE_DRAW.minX - SURFACE_LENGTH,
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
id: 'tunnel', kind: 'bore', bore: WESTERN_BORE_A, length: WESTERN_TUNNEL_LENGTH, x0: WESTERN_TUNNEL_X, z0: WESTERN_TUNNEL_START_Z, dx: 0, dz: 1,
|
|
429
|
+
cover: { area: 'western-line', title: 'Under the sea' },
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
id: 'isle', kind: 'surface', length: ISLE_LENGTH, x0: WESTERN_ISLE_SEA_END_X, z0: WESTERN_ISLE_TRACK_Z, dx: -1, dz: 0,
|
|
433
|
+
// Coming up into the Western Kingdoms: the land's own title card (a small train over its name, phase 1b).
|
|
434
|
+
cover: { area: 'western-kingdoms-train' },
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
stops: [
|
|
438
|
+
{ id: 'platform', name: 'The Western Entrance', c: WESTERN_TRAIN_PLATFORM_C, dwellMs: 60_000, enabled: true, home: true, note: 'The platform on the headland: boarding by the step plates.' },
|
|
439
|
+
// Lantern Wake, the Western Kingdoms' first stop (phase 0c's stub, made the real station in phase 1c-1; types
|
|
440
|
+
// lanternWake.ts). The id stays 'western-isle' so nothing keyed on it moves.
|
|
441
|
+
{ id: 'western-isle', name: 'Lantern Wake', c: WESTERN_ISLE_STOP_C, dwellMs: 30_000, enabled: true, steps: true, stepPrefix: 'lw-station-step', note: "Lantern Wake: the Western Kingdoms' first stop, the station on the south-east bay where the train climbs out of the sea." },
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
/** Every circuit this build knows, oldest first: a client follows whichever of them its server runs. */
|
|
445
|
+
export const WESTERN_TRAIN_CIRCUITS = [WESTERN_TRAIN_CIRCUIT_V1];
|
|
446
|
+
/** The circuit this build's SERVER runs (the client's default before the server has said). */
|
|
447
|
+
export const WESTERN_TRAIN_CIRCUIT = WESTERN_TRAIN_CIRCUIT_V1;
|
|
448
|
+
/** Every bore of every known circuit (lighting and sounds: a bore the server has not cut is never reached). */
|
|
449
|
+
export const WESTERN_BORES = (() => {
|
|
450
|
+
const out = [];
|
|
451
|
+
for (const c of WESTERN_TRAIN_CIRCUITS)
|
|
452
|
+
for (const b of c.bores)
|
|
453
|
+
if (!out.some((o) => o.id === b.id))
|
|
454
|
+
out.push(b);
|
|
455
|
+
return out;
|
|
456
|
+
})();
|
|
457
|
+
/** In a bore (its floor or its wall tiles), of every known circuit or the bores given. */
|
|
458
|
+
export function inWesternBore(x, z, bores = WESTERN_BORES) {
|
|
459
|
+
for (const b of bores) {
|
|
460
|
+
const B = westernBoreBox(b);
|
|
461
|
+
if (x >= B.minX - 1 && x <= B.maxX + 1 && z >= B.minZ - 1 && z <= B.maxZ + 1)
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
/** The version a key names (`v3-...` is 3), or null for a key that is not one. */
|
|
467
|
+
export function westernCircuitVersionOf(key) {
|
|
468
|
+
const m = typeof key === 'string' ? /^v(\d+)-[0-9a-f]{8}$/.exec(key) : null;
|
|
469
|
+
return m ? Number(m[1]) : null;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The circuit a server runs, from the key it sent: no key is a server from before the version (slice 2-0), which runs
|
|
473
|
+
* version 1; a key this build knows is that circuit; anything else is null (a newer server than this page, or a circuit
|
|
474
|
+
* changed without a new version): the client then draws no moving train and asks for a reload.
|
|
475
|
+
*/
|
|
476
|
+
export function westernCircuitForKey(key, known = WESTERN_TRAIN_CIRCUITS) {
|
|
477
|
+
if (key === undefined || key === null || key === '')
|
|
478
|
+
return known.find((c) => c.version === 1) ?? null;
|
|
479
|
+
return known.find((c) => c.key === key) ?? null;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* How this build stands with the server's circuit: `same` (the one it would run), `older` (the server runs an older
|
|
483
|
+
* circuit this build still knows: follow it), `newer` (the server runs a later version than any this build knows: reload),
|
|
484
|
+
* `unknown` (a version this build knows with different numbers: someone changed a circuit without a new version).
|
|
485
|
+
*/
|
|
486
|
+
export function westernCircuitAgreement(key, own = WESTERN_TRAIN_CIRCUIT, known = WESTERN_TRAIN_CIRCUITS) {
|
|
487
|
+
const circuit = westernCircuitForKey(key, known);
|
|
488
|
+
if (circuit)
|
|
489
|
+
return { status: circuit.key === own.key ? 'same' : circuit.version < own.version ? 'older' : 'newer', circuit };
|
|
490
|
+
const v = westernCircuitVersionOf(key);
|
|
491
|
+
const top = Math.max(0, ...known.map((c) => c.version));
|
|
492
|
+
return { status: v !== null && v > top ? 'newer' : 'unknown', circuit: null };
|
|
493
|
+
}
|
|
494
|
+
/** What a client tells its player when it cannot follow the server's circuit. */
|
|
495
|
+
export const WESTERN_TRAIN_RELOAD_TEXT = 'The Western Line has changed since this page loaded. Reload the page to see the trains.';
|
|
496
|
+
/* The server's circuit, as the constants the line has always had (code that must follow the SERVER's circuit on the
|
|
497
|
+
* client passes the circuit it was sent instead of reading these). */
|
|
498
|
+
export const WESTERN_TRAIN_SEGMENTS = WESTERN_TRAIN_CIRCUIT.segments;
|
|
499
|
+
export const WESTERN_TRAIN_CIRCUIT_LENGTH = WESTERN_TRAIN_CIRCUIT.length;
|
|
500
|
+
/** How many trains run the circuit, evenly spaced in time (James: "1-3 trains running constantly"). */
|
|
501
|
+
export const WESTERN_TRAIN_COUNT = WESTERN_TRAIN_CIRCUIT.trainCount;
|
|
502
|
+
export const WESTERN_TRAIN_STOPS = WESTERN_TRAIN_CIRCUIT.stops;
|
|
503
|
+
export const WESTERN_TRAIN_CYCLE_MS = WESTERN_TRAIN_CIRCUIT.timetable.cycleMs;
|
|
504
|
+
export function wrapWesternC(c, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
505
|
+
return wrapOn(c, circuit.length);
|
|
506
|
+
}
|
|
507
|
+
export function westernTrainSegmentAt(c, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
508
|
+
return segmentOn(circuit, c);
|
|
234
509
|
}
|
|
235
510
|
/** A point on a leg (c may run past its ends: the client extrapolates a piece into the sea or the rock). */
|
|
236
511
|
export function westernTrainPointOn(seg, c, lateral) {
|
|
@@ -239,24 +514,23 @@ export function westernTrainPointOn(seg, c, lateral) {
|
|
|
239
514
|
return { x: seg.x0 + seg.dx * along + lx * lateral, z: seg.z0 + seg.dz * along + lz * lateral };
|
|
240
515
|
}
|
|
241
516
|
/** The world point of track coordinate c (wrapped) and a lateral offset (docked z - track z). */
|
|
242
|
-
export function westernTrainWorldPoint(c, lateral) {
|
|
243
|
-
const segment =
|
|
244
|
-
return { ...westernTrainPointOn(segment,
|
|
517
|
+
export function westernTrainWorldPoint(c, lateral, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
518
|
+
const segment = segmentOn(circuit, c);
|
|
519
|
+
return { ...westernTrainPointOn(segment, wrapOn(c, circuit.length), lateral), segment };
|
|
245
520
|
}
|
|
246
|
-
/** The ground under the track at c on a leg
|
|
521
|
+
/** The ground under the track at c on a leg: a bore's floor, or the causeway stepping down to the waterline, then the sea. */
|
|
247
522
|
export function westernTrainGroundHeight(seg, c) {
|
|
248
|
-
if (seg.
|
|
249
|
-
return WESTERN_TUNNEL_FLOOR_HEIGHT;
|
|
523
|
+
if (seg.kind === 'bore')
|
|
524
|
+
return seg.bore?.floorHeight ?? WESTERN_TUNNEL_FLOOR_HEIGHT;
|
|
525
|
+
// A surface leg comes up out of the sea at its x0 (heading west) and climbs the home line's causeway, moved along x.
|
|
250
526
|
const x = seg.x0 + seg.dx * (c - seg.c0);
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
const causewayFrom = isle ? WESTERN_ISLE_CAUSEWAY.minX : WESTERN_CAUSEWAY.minX;
|
|
255
|
-
const causeway = isle ? westernIsleCausewayHeight : westernCausewayHeight;
|
|
527
|
+
const seaEnd = seg.x0;
|
|
528
|
+
const shift = seaEnd - WESTERN_SEA_END.x;
|
|
529
|
+
const causewayFrom = WESTERN_CAUSEWAY.minX + shift;
|
|
256
530
|
if (x > seaEnd)
|
|
257
531
|
return -(x - seaEnd) * 1.4; // under the sea past the line's end
|
|
258
532
|
const lo = Math.floor(x), t = x - lo;
|
|
259
|
-
const h = (xi) => (xi >= causewayFrom ?
|
|
533
|
+
const h = (xi) => (xi >= causewayFrom ? westernCausewayHeight(Math.min(xi, seaEnd) - shift) : WESTERN_PRECINCT_HEIGHT);
|
|
260
534
|
return h(lo) * (1 - t) + h(lo + 1) * t;
|
|
261
535
|
}
|
|
262
536
|
/** Metres: the deck a rider stands on at c (the ground plus the kit's deck height). */
|
|
@@ -268,195 +542,208 @@ export function westernDockedC(noseC, dockedX) {
|
|
|
268
542
|
return noseC - (dockedX - WESTERN_TRAIN_NOSE_X);
|
|
269
543
|
}
|
|
270
544
|
/** The world tile of a docked tile when the nose stands at integer c (the server's step). */
|
|
271
|
-
export function westernRiderWorldTile(noseC, dockedX, dockedZ) {
|
|
272
|
-
|
|
273
|
-
return { x: Math.round(p.x), y: Math.round(p.z) };
|
|
545
|
+
export function westernRiderWorldTile(noseC, dockedX, dockedZ, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
546
|
+
return riderTileOn(circuit, noseC, dockedX, dockedZ);
|
|
274
547
|
}
|
|
275
548
|
/**
|
|
276
549
|
* A world tile back to the docked frame of a train whose nose is at c (a rider's click on the moving deck), or null when
|
|
277
550
|
* the tile is not on the rake.
|
|
278
551
|
*/
|
|
279
|
-
export function westernWorldToDocked(noseC, x, z) {
|
|
280
|
-
for (const seg of
|
|
552
|
+
export function westernWorldToDocked(noseC, x, z, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
553
|
+
for (const seg of circuit.segments) {
|
|
281
554
|
const { lx, lz } = lateralOf(seg);
|
|
282
555
|
const along = (x - seg.x0) * seg.dx + (z - seg.z0) * seg.dz;
|
|
283
556
|
const lateral = (x - seg.x0) * lx + (z - seg.z0) * lz;
|
|
284
557
|
if (along < 0 || along >= seg.length || Math.abs(lateral) > WESTERN_TRAIN_HALF_ACROSS)
|
|
285
558
|
continue;
|
|
286
|
-
const behind =
|
|
559
|
+
const behind = wrapOn(noseC - (seg.c0 + along), circuit.length);
|
|
287
560
|
if (behind > WESTERN_TRAIN_RAKE_LENGTH - 1)
|
|
288
561
|
continue;
|
|
289
562
|
return { x: WESTERN_TRAIN_NOSE_X + behind, y: WESTERN_TRACK_Z + lateral };
|
|
290
563
|
}
|
|
291
564
|
return null;
|
|
292
565
|
}
|
|
566
|
+
/** The compass side of the track a stop's platform is on (the docked platform side, turned with the leg). */
|
|
567
|
+
export function westernStopPlatformSide(stop, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
568
|
+
const { lx, lz } = lateralOf(segmentOn(circuit, stop.c));
|
|
569
|
+
// The platform is lateral -1.
|
|
570
|
+
if (-lz < 0)
|
|
571
|
+
return 'north';
|
|
572
|
+
if (-lz > 0)
|
|
573
|
+
return 'south';
|
|
574
|
+
return -lx > 0 ? 'east' : 'west';
|
|
575
|
+
}
|
|
576
|
+
/** How far (tiles, Chebyshev) a rider may be carried in one step before it counts as a jump needing the cover: the view ring. */
|
|
577
|
+
export const WESTERN_TRAIN_JUMP_TILES = CHUNK_VIEW_RADIUS * CHUNK_SIZE;
|
|
578
|
+
/**
|
|
579
|
+
* A rider's tile carried from one server step to the next: a JUMP (the loading cover first) when it lands in another
|
|
580
|
+
* region of the grid OR farther than the view ring (two bores in the same region, slice 2-0), with the cover of the leg
|
|
581
|
+
* it lands on; null for an ordinary step.
|
|
582
|
+
*/
|
|
583
|
+
export function westernRiderJump(prevNoseStep, noseStep, dockedX, dockedZ, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
584
|
+
const from = riderTileOn(circuit, prevNoseStep, dockedX, dockedZ);
|
|
585
|
+
const to = riderTileOn(circuit, noseStep, dockedX, dockedZ);
|
|
586
|
+
const far = Math.max(Math.abs(to.x - from.x), Math.abs(to.y - from.y)) > WESTERN_TRAIN_JUMP_TILES;
|
|
587
|
+
if (!far && worldRegionAt(from.x) === worldRegionAt(to.x))
|
|
588
|
+
return null;
|
|
589
|
+
const segment = segmentOn(circuit, westernDockedC(noseStep, dockedX));
|
|
590
|
+
return { from, to, segment, cover: segment.cover };
|
|
591
|
+
}
|
|
293
592
|
/* ------------------------------------------------------------------ *
|
|
294
593
|
* The timetable.
|
|
295
594
|
* ------------------------------------------------------------------ */
|
|
296
|
-
/** Every train counts from this instant (a fixed date, so a restart never moves a train). */
|
|
297
|
-
export const WESTERN_TRAIN_EPOCH_MS = Date.UTC(2026, 9, 10, 0, 0, 0);
|
|
298
|
-
/** How many trains run the circuit, evenly spaced in time (James: "1-3 trains running constantly"). */
|
|
299
|
-
export const WESTERN_TRAIN_COUNT = 2;
|
|
300
|
-
/** Cruising speed (tiles a second) and the rate it gets up to it and slows from it. */
|
|
301
|
-
export const WESTERN_TRAIN_TOP_SPEED = 6;
|
|
302
|
-
export const WESTERN_TRAIN_ACCEL = 0.5;
|
|
303
|
-
/** The bell rings this long before a departure; the step plates shut this long before it. */
|
|
304
|
-
export const WESTERN_TRAIN_BELL_BEFORE_MS = 15_000;
|
|
305
|
-
export const WESTERN_TRAIN_DOORS_SHUT_BEFORE_MS = 5_000;
|
|
306
|
-
/** The whistle and the bell are heard this far (tiles) from the loco. */
|
|
307
|
-
export const WESTERN_TRAIN_SOUND_RANGE = 40;
|
|
308
|
-
export const WESTERN_TRAIN_MOVING_TEXT = 'The train is moving.';
|
|
309
|
-
export const WESTERN_TRAIN_DOORS_SHUT_TEXT = 'The doors are shut: the train is about to leave.';
|
|
310
|
-
export const WESTERN_TRAIN_ABOARD_COMBAT_TEXT = 'There is no fighting aboard the train.';
|
|
311
|
-
export const WESTERN_TRAIN_ALIGHT_MOVING_TEXT = 'The train is moving. Wait until it stops at the platform.';
|
|
312
|
-
export const WESTERN_TRAIN_STOPS = [
|
|
313
|
-
{ id: 'platform', name: 'The Western Entrance', c: WESTERN_TRAIN_PLATFORM_C, dwellMs: 60_000, enabled: true, note: 'The platform on the headland: boarding by the step plates.' },
|
|
314
|
-
// Lantern Wake, the Western Kingdoms' first stop (phase 0c's stub, made the real station in phase 1c-1; types
|
|
315
|
-
// lanternWake.ts). The id stays 'western-isle' so nothing keyed on it moves.
|
|
316
|
-
{ id: 'western-isle', name: 'Lantern Wake', c: WESTERN_ISLE_STOP_C, dwellMs: 30_000, enabled: true, steps: true, stepPrefix: 'lw-station-step', note: "Lantern Wake: the Western Kingdoms' first stop, the station on the south-east bay where the train climbs out of the sea." },
|
|
317
|
-
{ id: 'dwarf-hold', name: 'The Dwarf Hold', c: SURFACE_LENGTH + 300, dwellMs: 45_000, enabled: false, note: 'An underground dwarf dungeon beside the bore; its exit is blocked for now.' },
|
|
318
|
-
{ id: 'capital', name: 'The Capital', c: SURFACE_LENGTH + 450, dwellMs: 60_000, enabled: false, note: 'Not reachable by a surface line (the city is ~900 rows north); needs an underground halt on this bore and a stair or lift from the city.' },
|
|
319
|
-
];
|
|
320
|
-
/** Duration (ms) of a run of D tiles from a stop to a stop: up to speed, cruise, slow to a stand. */
|
|
321
|
-
export function westernTrainRunMs(distance) {
|
|
322
|
-
const v = WESTERN_TRAIN_TOP_SPEED, a = WESTERN_TRAIN_ACCEL;
|
|
323
|
-
const ramp = (v * v) / a; // tiles spent getting up to speed and slowing again
|
|
324
|
-
const s = distance >= ramp ? distance / v + v / a : 2 * Math.sqrt(distance / a);
|
|
325
|
-
return s * 1000;
|
|
326
|
-
}
|
|
327
|
-
/** Tiles covered after t ms of a run of D tiles. */
|
|
328
|
-
export function westernTrainRunTiles(distance, tMs) {
|
|
329
|
-
const v = WESTERN_TRAIN_TOP_SPEED, a = WESTERN_TRAIN_ACCEL;
|
|
330
|
-
const total = westernTrainRunMs(distance) / 1000;
|
|
331
|
-
const t = Math.max(0, Math.min(total, tMs / 1000));
|
|
332
|
-
const peak = Math.min(v, Math.sqrt(distance * a));
|
|
333
|
-
const ta = peak / a;
|
|
334
|
-
if (t <= ta)
|
|
335
|
-
return 0.5 * a * t * t;
|
|
336
|
-
if (t >= total - ta)
|
|
337
|
-
return distance - 0.5 * a * (total - t) ** 2;
|
|
338
|
-
return 0.5 * a * ta * ta + peak * (t - ta);
|
|
339
|
-
}
|
|
340
595
|
/** The cycle: for each running stop in order, its dwell then the run to the next running stop (wrapping). */
|
|
341
|
-
export function westernTrainTimetable(stops =
|
|
342
|
-
|
|
343
|
-
const legs = [];
|
|
344
|
-
let t = 0;
|
|
345
|
-
running.forEach((stop, i) => {
|
|
346
|
-
const next = running[(i + 1) % running.length];
|
|
347
|
-
const runTiles = wrapWesternC(next.c - stop.c) || WESTERN_TRAIN_CIRCUIT_LENGTH;
|
|
348
|
-
const startMs = t;
|
|
349
|
-
const departMs = startMs + stop.dwellMs;
|
|
350
|
-
const endMs = departMs + westernTrainRunMs(runTiles);
|
|
351
|
-
legs.push({ stop, startMs, departMs, runTiles, endMs });
|
|
352
|
-
t = endMs;
|
|
353
|
-
});
|
|
354
|
-
return { cycleMs: t, legs };
|
|
596
|
+
export function westernTrainTimetable(stops = WESTERN_TRAIN_CIRCUIT.stops, length = WESTERN_TRAIN_CIRCUIT.length) {
|
|
597
|
+
return timetableOf(stops, length);
|
|
355
598
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
/** Is any piece of the rake on the surface leg with the nose at c? */
|
|
359
|
-
function rakeOnSurface(noseC) {
|
|
360
|
-
const surface = WESTERN_TRAIN_SEGMENTS[0];
|
|
599
|
+
/** Is any piece of the rake on the leg with the nose at c? */
|
|
600
|
+
function rakeOnLeg(noseC, leg) {
|
|
361
601
|
const tail = noseC - (WESTERN_TRAIN_RAKE_LENGTH - 1);
|
|
362
|
-
return noseC >=
|
|
602
|
+
return noseC >= leg.c0 && tail < leg.c0 + leg.length;
|
|
363
603
|
}
|
|
364
|
-
/** Where train `train` (0-based) is at server time `nowMs
|
|
365
|
-
export function westernTrainAt(nowMs, train = 0,
|
|
604
|
+
/** Where train `train` (0-based) is at server time `nowMs`, on a circuit (the server's by default). */
|
|
605
|
+
export function westernTrainAt(nowMs, train = 0, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
606
|
+
const timetable = circuit.timetable;
|
|
607
|
+
const home = circuit.home;
|
|
366
608
|
const cycle = timetable.cycleMs;
|
|
367
|
-
const offset = (train * cycle) /
|
|
609
|
+
const offset = (train * cycle) / circuit.trainCount;
|
|
368
610
|
const t = (((nowMs - WESTERN_TRAIN_EPOCH_MS + offset) % cycle) + cycle) % cycle;
|
|
369
|
-
const
|
|
370
|
-
const msToPlatform =
|
|
371
|
-
|
|
611
|
+
const homeLeg = timetable.legs.find((l) => l.stop.id === home.id);
|
|
612
|
+
const msToPlatform = homeLeg ? ((homeLeg.startMs - t) % cycle + cycle) % cycle : 0;
|
|
613
|
+
const legs = timetable.legs;
|
|
614
|
+
for (let i = 0; i < legs.length; i++) {
|
|
615
|
+
const leg = legs[i];
|
|
372
616
|
if (t < leg.startMs || t >= leg.endMs)
|
|
373
617
|
continue;
|
|
374
618
|
if (t < leg.departMs) {
|
|
375
|
-
const noseC =
|
|
619
|
+
const noseC = wrapOn(leg.stop.c, circuit.length);
|
|
620
|
+
const atHome = leg.stop.id === home.id;
|
|
376
621
|
return {
|
|
377
622
|
train, noseC, noseStep: Math.floor(noseC), moving: false,
|
|
378
|
-
phase:
|
|
623
|
+
phase: atHome ? 'boarding' : 'away',
|
|
379
624
|
stopId: leg.stop.id, msToDepart: leg.departMs - t,
|
|
380
|
-
msToPlatform:
|
|
625
|
+
msToPlatform: atHome ? 0 : msToPlatform, cycleMs: t,
|
|
626
|
+
nextStopId: leg.stop.id, msToNextStop: 0,
|
|
381
627
|
};
|
|
382
628
|
}
|
|
383
629
|
const ran = westernTrainRunTiles(leg.runTiles, t - leg.departMs);
|
|
384
|
-
const noseC =
|
|
385
|
-
const
|
|
630
|
+
const noseC = wrapOn(leg.stop.c + ran, circuit.length);
|
|
631
|
+
const onHomeLeg = rakeOnLeg(noseC, circuit.homeLeg);
|
|
386
632
|
// The last instant of a run into the platform is still the approach (its noseC lands exactly on the platform's c).
|
|
387
633
|
const arriving = leg.runTiles - ran < 0.5;
|
|
388
|
-
const phase = !
|
|
389
|
-
|
|
634
|
+
const phase = !onHomeLeg ? 'away' : noseC < home.c || arriving ? 'approaching' : 'departing';
|
|
635
|
+
const next = legs[(i + 1) % legs.length];
|
|
636
|
+
return {
|
|
637
|
+
train, noseC, noseStep: Math.floor(noseC), moving: true, phase, stopId: null, msToDepart: null, msToPlatform, cycleMs: t,
|
|
638
|
+
nextStopId: next.stop.id, msToNextStop: leg.endMs - t,
|
|
639
|
+
};
|
|
390
640
|
}
|
|
391
641
|
// Rounding at the cycle's very end: treat as the first leg's start.
|
|
392
642
|
const first = timetable.legs[0];
|
|
643
|
+
const atHome = first.stop.id === home.id;
|
|
393
644
|
return {
|
|
394
|
-
train, noseC:
|
|
395
|
-
phase:
|
|
396
|
-
msToPlatform: 0, cycleMs: 0,
|
|
645
|
+
train, noseC: wrapOn(first.stop.c, circuit.length), noseStep: Math.floor(wrapOn(first.stop.c, circuit.length)), moving: false,
|
|
646
|
+
phase: atHome ? 'boarding' : 'away', stopId: first.stop.id, msToDepart: first.stop.dwellMs,
|
|
647
|
+
msToPlatform: 0, cycleMs: 0, nextStopId: first.stop.id, msToNextStop: 0,
|
|
397
648
|
};
|
|
398
649
|
}
|
|
399
|
-
/**
|
|
400
|
-
export function
|
|
401
|
-
|
|
402
|
-
|
|
650
|
+
/** Every train of a circuit at `nowMs`. */
|
|
651
|
+
export function westernTrainsAt(nowMs, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
652
|
+
return Array.from({ length: circuit.trainCount }, (_, i) => westernTrainAt(nowMs, i, circuit));
|
|
653
|
+
}
|
|
654
|
+
/** The stop a train stands at, or the next running stop it reaches. */
|
|
655
|
+
export function westernNextStop(state, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
656
|
+
return circuit.stops.find((s) => s.id === state.nextStopId) ?? null;
|
|
657
|
+
}
|
|
658
|
+
/** The running stop after this one (wrapping). */
|
|
659
|
+
export function westernStopAfter(stopId, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
660
|
+
const running = runningStops(circuit.stops);
|
|
661
|
+
const i = running.findIndex((s) => s.id === stopId);
|
|
662
|
+
return i < 0 ? null : running[(i + 1) % running.length];
|
|
663
|
+
}
|
|
664
|
+
/** A stop's name as said mid-sentence ("for the Western Entrance"). */
|
|
665
|
+
export function westernStopNamePhrase(stop) {
|
|
666
|
+
return stop.name.startsWith('The ') ? `the ${stop.name.slice(4)}` : stop.name;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* The closest two trains come on a circuit (tiles from one's nose to the tail ahead of it, along the line), sampled over
|
|
670
|
+
* a cycle; Infinity with one train. The line's own test wants at least 20.
|
|
671
|
+
*/
|
|
672
|
+
export function westernTrainMinGap(circuit = WESTERN_TRAIN_CIRCUIT, stepMs = 100) {
|
|
673
|
+
let minGap = Infinity;
|
|
674
|
+
const L = circuit.length;
|
|
675
|
+
for (let t = 0; t < circuit.timetable.cycleMs; t += stepMs) {
|
|
676
|
+
const states = westernTrainsAt(WESTERN_TRAIN_EPOCH_MS + t, circuit);
|
|
677
|
+
for (const a of states)
|
|
678
|
+
for (const b of states) {
|
|
679
|
+
if (a === b)
|
|
680
|
+
continue;
|
|
681
|
+
const gap = wrapOn(a.noseC - (WESTERN_TRAIN_RAKE_LENGTH - 1) - b.noseC, L);
|
|
682
|
+
if (gap < L / 2)
|
|
683
|
+
minGap = Math.min(minGap, gap);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return minGap;
|
|
687
|
+
}
|
|
688
|
+
/** The train standing at the home platform at `nowMs`, or null. */
|
|
689
|
+
export function westernTrainAtPlatform(nowMs, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
690
|
+
for (let i = 0; i < circuit.trainCount; i++) {
|
|
691
|
+
const s = westernTrainAt(nowMs, i, circuit);
|
|
403
692
|
if (s.phase === 'boarding')
|
|
404
693
|
return s;
|
|
405
694
|
}
|
|
406
695
|
return null;
|
|
407
696
|
}
|
|
408
697
|
/** Ms until the next train stands at the platform (0 if one is there). */
|
|
409
|
-
export function westernTrainNextArrivalMs(nowMs) {
|
|
698
|
+
export function westernTrainNextArrivalMs(nowMs, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
410
699
|
let best = Infinity;
|
|
411
|
-
for (let i = 0; i <
|
|
412
|
-
best = Math.min(best, westernTrainAt(nowMs, i).msToPlatform);
|
|
700
|
+
for (let i = 0; i < circuit.trainCount; i++)
|
|
701
|
+
best = Math.min(best, westernTrainAt(nowMs, i, circuit).msToPlatform);
|
|
413
702
|
return best;
|
|
414
703
|
}
|
|
415
704
|
/** The refusal text for the step plates while no train can be boarded. */
|
|
416
|
-
export function westernTrainNoBoardingText(nowMs) {
|
|
417
|
-
const here = westernTrainAtPlatform(nowMs);
|
|
705
|
+
export function westernTrainNoBoardingText(nowMs, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
706
|
+
const here = westernTrainAtPlatform(nowMs, circuit);
|
|
418
707
|
if (here)
|
|
419
708
|
return WESTERN_TRAIN_DOORS_SHUT_TEXT;
|
|
420
|
-
for (let i = 0; i <
|
|
421
|
-
const s = westernTrainAt(nowMs, i);
|
|
709
|
+
for (let i = 0; i < circuit.trainCount; i++) {
|
|
710
|
+
const s = westernTrainAt(nowMs, i, circuit);
|
|
422
711
|
if (s.phase === 'approaching' || s.phase === 'departing')
|
|
423
712
|
return WESTERN_TRAIN_MOVING_TEXT;
|
|
424
713
|
}
|
|
425
|
-
const secs = Math.max(1, Math.round(westernTrainNextArrivalMs(nowMs) / 1000));
|
|
714
|
+
const secs = Math.max(1, Math.round(westernTrainNextArrivalMs(nowMs, circuit) / 1000));
|
|
426
715
|
return `No train stands at the platform. The next one is due in about ${secs} seconds.`;
|
|
427
716
|
}
|
|
428
717
|
/**
|
|
429
718
|
* The station's word on the next train (the station hub, 2026-10-10: the All Aboard board's click and the station
|
|
430
719
|
* master's "Next train?"): a train standing with its doors open, shut, or the next one due.
|
|
431
720
|
*/
|
|
432
|
-
export function westernStationNextTrainText(nowMs) {
|
|
433
|
-
const here = westernTrainAtPlatform(nowMs);
|
|
721
|
+
export function westernStationNextTrainText(nowMs, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
722
|
+
const here = westernTrainAtPlatform(nowMs, circuit);
|
|
434
723
|
if (here) {
|
|
435
724
|
const secs = Math.round(((here.msToDepart ?? 0) - WESTERN_TRAIN_DOORS_SHUT_BEFORE_MS) / 1000);
|
|
436
725
|
return secs >= 1
|
|
437
726
|
? `A train is standing at the platform: its doors shut in about ${secs} seconds.`
|
|
438
727
|
: 'A train is standing at the platform, but its doors are shut: it is about to leave.';
|
|
439
728
|
}
|
|
440
|
-
const secs = Math.max(1, Math.round(westernTrainNextArrivalMs(nowMs) / 1000));
|
|
729
|
+
const secs = Math.max(1, Math.round(westernTrainNextArrivalMs(nowMs, circuit) / 1000));
|
|
441
730
|
return `The next train is due at the platform in about ${secs} seconds.`;
|
|
442
731
|
}
|
|
443
732
|
const COUNT_WORDS = ['No', 'One', 'Two', 'Three', 'Four'];
|
|
444
733
|
/** What the All Aboard board says: the line's rules from the timetable, then the next train. */
|
|
445
|
-
export function westernStationNoticeText(nowMs) {
|
|
446
|
-
const dwell = Math.round(
|
|
447
|
-
const every = Math.round(
|
|
448
|
-
const trains = COUNT_WORDS[
|
|
734
|
+
export function westernStationNoticeText(nowMs, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
735
|
+
const dwell = Math.round(circuit.home.dwellMs / 1000);
|
|
736
|
+
const every = Math.round(circuit.timetable.cycleMs / circuit.trainCount / 1000);
|
|
737
|
+
const trains = COUNT_WORDS[circuit.trainCount] ?? String(circuit.trainCount);
|
|
449
738
|
return `ALL ABOARD the Western Line. ${trains} trains run the loop under the sea, one about every ${every} seconds, and each stands ${dwell} seconds at the platform. ` +
|
|
450
739
|
`Board by the brass step plates while a train stands; the doors shut ${WESTERN_TRAIN_DOORS_SHUT_BEFORE_MS / 1000} seconds before it leaves. No fighting aboard. No fares while the line is new. ` +
|
|
451
|
-
westernStationNextTrainText(nowMs);
|
|
740
|
+
westernStationNextTrainText(nowMs, circuit);
|
|
452
741
|
}
|
|
453
742
|
/** Can the step plates be used at `nowMs` (a train standing, more than the shut time to go)? */
|
|
454
|
-
export function westernTrainDoorsOpen(nowMs) {
|
|
455
|
-
const here = westernTrainAtPlatform(nowMs);
|
|
743
|
+
export function westernTrainDoorsOpen(nowMs, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
744
|
+
const here = westernTrainAtPlatform(nowMs, circuit);
|
|
456
745
|
return !!here && (here.msToDepart ?? 0) > WESTERN_TRAIN_DOORS_SHUT_BEFORE_MS;
|
|
457
746
|
}
|
|
458
|
-
/** The surface leg's x range the rake is drawn on (the mouth hides it to the west, the sea to the east). */
|
|
459
|
-
export const WESTERN_TRAIN_SURFACE_DRAW = { minX: 18, maxX: WESTERN_SEA_END.x + 8 };
|
|
460
747
|
/** The platform tile in front of the nearest step plate to a docked x (where someone left on the docked rake is put). */
|
|
461
748
|
export function westernPlatformTileNear(x) {
|
|
462
749
|
let best = WESTERN_STEPS[0];
|
|
@@ -465,54 +752,44 @@ export function westernPlatformTileNear(x) {
|
|
|
465
752
|
best = s;
|
|
466
753
|
return { x: best.platform[0], y: best.platform[1] };
|
|
467
754
|
}
|
|
468
|
-
export function westernStopSteps(stop) {
|
|
469
|
-
return
|
|
470
|
-
const plate = westernRiderWorldTile(stop.c, s.x, s.z);
|
|
471
|
-
return {
|
|
472
|
-
stopId: stop.id,
|
|
473
|
-
car: s.car,
|
|
474
|
-
id: `${stop.stepPrefix ?? `${stop.id}-step`}-${s.car}`,
|
|
475
|
-
x: plate.x,
|
|
476
|
-
z: plate.y,
|
|
477
|
-
platform: westernRiderWorldTile(stop.c, s.platform[0], s.platform[1]),
|
|
478
|
-
doorway: { x: s.doorway[0], y: s.doorway[1] },
|
|
479
|
-
};
|
|
480
|
-
});
|
|
755
|
+
export function westernStopSteps(stop, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
756
|
+
return stepsOn(circuit, stop);
|
|
481
757
|
}
|
|
482
|
-
/** Every running stop's own plates (not the home platform's). */
|
|
483
|
-
export const WESTERN_STOP_STEPS =
|
|
484
|
-
export function westernStopStepAt(x, z) {
|
|
485
|
-
return
|
|
758
|
+
/** Every running stop's own plates (not the home platform's), on the server's circuit. */
|
|
759
|
+
export const WESTERN_STOP_STEPS = WESTERN_TRAIN_CIRCUIT.stopSteps;
|
|
760
|
+
export function westernStopStepAt(x, z, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
761
|
+
return circuit.stopSteps.find((s) => s.x === x && s.z === z) ?? null;
|
|
486
762
|
}
|
|
487
|
-
export function westernStopStepById(id) {
|
|
488
|
-
return
|
|
763
|
+
export function westernStopStepById(id, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
764
|
+
return circuit.stopSteps.find((s) => s.id === id) ?? null;
|
|
489
765
|
}
|
|
490
766
|
/** The train standing at a stop (not the home platform) at `nowMs`, or null. */
|
|
491
|
-
export function westernTrainAtStop(nowMs, stopId) {
|
|
492
|
-
for (let i = 0; i <
|
|
493
|
-
const s = westernTrainAt(nowMs, i);
|
|
767
|
+
export function westernTrainAtStop(nowMs, stopId, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
768
|
+
for (let i = 0; i < circuit.trainCount; i++) {
|
|
769
|
+
const s = westernTrainAt(nowMs, i, circuit);
|
|
494
770
|
if (!s.moving && s.stopId === stopId)
|
|
495
771
|
return s;
|
|
496
772
|
}
|
|
497
773
|
return null;
|
|
498
774
|
}
|
|
499
775
|
/** The train at a stop with its doors open (more than the shut time to go), or null. */
|
|
500
|
-
export function westernStopDoorsOpen(nowMs, stopId) {
|
|
501
|
-
const s = westernTrainAtStop(nowMs, stopId);
|
|
776
|
+
export function westernStopDoorsOpen(nowMs, stopId, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
777
|
+
const s = westernTrainAtStop(nowMs, stopId, circuit);
|
|
502
778
|
return s && (s.msToDepart ?? 0) > WESTERN_TRAIN_DOORS_SHUT_BEFORE_MS ? s : null;
|
|
503
779
|
}
|
|
504
780
|
/**
|
|
505
781
|
* Ms until the next train stands at a stop (0 while one stands there), from the timetable; Infinity for a stop that is
|
|
506
782
|
* not running. The station master's sum for the home platform is westernTrainNextArrivalMs.
|
|
507
783
|
*/
|
|
508
|
-
export function westernStopNextArrivalMs(nowMs, stopId,
|
|
784
|
+
export function westernStopNextArrivalMs(nowMs, stopId, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
785
|
+
const timetable = circuit.timetable;
|
|
509
786
|
const leg = timetable.legs.find((l) => l.stop.id === stopId);
|
|
510
787
|
if (!leg)
|
|
511
788
|
return Infinity;
|
|
512
789
|
const cycle = timetable.cycleMs;
|
|
513
790
|
let best = Infinity;
|
|
514
|
-
for (let i = 0; i <
|
|
515
|
-
const offset = (i * cycle) /
|
|
791
|
+
for (let i = 0; i < circuit.trainCount; i++) {
|
|
792
|
+
const offset = (i * cycle) / circuit.trainCount;
|
|
516
793
|
const t = (((nowMs - WESTERN_TRAIN_EPOCH_MS + offset) % cycle) + cycle) % cycle;
|
|
517
794
|
if (t >= leg.startMs && t < leg.departMs)
|
|
518
795
|
return 0;
|
|
@@ -521,10 +798,27 @@ export function westernStopNextArrivalMs(nowMs, stopId, timetable = TIMETABLE) {
|
|
|
521
798
|
return best;
|
|
522
799
|
}
|
|
523
800
|
/** Why a stop's plates refuse at `nowMs`. */
|
|
524
|
-
export function westernStopNoBoardingText(nowMs, stopId) {
|
|
525
|
-
if (westernTrainAtStop(nowMs, stopId))
|
|
801
|
+
export function westernStopNoBoardingText(nowMs, stopId, circuit = WESTERN_TRAIN_CIRCUIT) {
|
|
802
|
+
if (westernTrainAtStop(nowMs, stopId, circuit))
|
|
526
803
|
return WESTERN_TRAIN_DOORS_SHUT_TEXT;
|
|
527
|
-
const stop =
|
|
804
|
+
const stop = circuit.stops.find((s) => s.id === stopId);
|
|
528
805
|
return `No train stands at ${stop?.name ?? 'this platform'}.`;
|
|
529
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* A stop of any known circuit whose closed pocket (a hall reached only by its train) holds this tile and which the given
|
|
809
|
+
* circuit does not run: someone saved there wakes at home (the stop was switched off, or the server runs an older circuit).
|
|
810
|
+
*/
|
|
811
|
+
export function westernClosedPocketAt(x, z, circuit = WESTERN_TRAIN_CIRCUIT, known = WESTERN_TRAIN_CIRCUITS) {
|
|
812
|
+
for (const c of [circuit, ...known]) {
|
|
813
|
+
for (const s of c.stops) {
|
|
814
|
+
const p = s.pocket;
|
|
815
|
+
if (!p || x < p.minX || x > p.maxX || z < p.minZ || z > p.maxZ)
|
|
816
|
+
continue;
|
|
817
|
+
if (circuit.stops.some((r) => r.id === s.id && r.enabled))
|
|
818
|
+
continue;
|
|
819
|
+
return s;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
530
824
|
//# sourceMappingURL=westernTrain.js.map
|