Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions game/src/enemies/jennyRam.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { JENNY_BOX } from './jenny';
import { frameAt, frameCount, type Clips } from '../combat/clips';

/**
* Jenny's ram on the clean creek path (docs/03 region 1, #54, P5) as one readable event, timed in seconds:
Expand Down Expand Up @@ -49,18 +50,23 @@ export function ramPlan(heroX: number, viewRight: number, heroHalfWidth = 14): R
/** Everything the ram shows `t` seconds after the trigger: pure, so the choreography is tested and replayable. */
export interface RamFrame {
phase: 'hush' | 'lights' | 'rev' | 'charge' | 'thrown' | 'cross';
/** Her centre x and frame name (boss-jenny atlas, 12 fps clips). */
/** Her centre x and frame name (boss-jenny atlas, at its clips' own speed). */
jennyX: number; jennyFrame: string;
/** Headlight alpha 0…1 and the x of the lamps (the right edge before she is seen, her nose after). */
lights: number; lightsX: number;
/** The hero thrown: undefined before the hit. */
hero?: { x: number; y: number; angle: number; frame: string };
}

export function ramFrame(t: number, plan: RamPlan, hero: { x: number; y: number }, viewRight: number): RamFrame {
/**
* `clips`: the loaded boss-jenny and hero-hurt anims.json (#156: one timing source); without them the authored
* 12 fps and frame counts.
*/
export function ramFrame(t: number, plan: RamPlan, hero: { x: number; y: number }, viewRight: number,
clips: { jenny?: Clips; hurt?: Clips } = {}): RamFrame {
const clip = (name: string, since: number, loop = true) => {
const n = Math.floor(Math.max(0, t - since) * 12);
return `${name}-0${loop ? n % 4 : Math.min(3, n)}`;
const n = frameAt(clips.jenny, name, Math.max(0, t - since)), frames = frameCount(clips.jenny, name, 4);
return `${name}-${String(loop ? n % frames : Math.min(frames - 1, n)).padStart(2, '0')}`;
};
const nose = (x: number) => Math.min(viewRight - 8, x - JENNY_BOX.w / 2);
if (t < RAM.hush) {
Expand All @@ -86,7 +92,7 @@ export function ramFrame(t: number, plan: RamPlan, hero: { x: number; y: number
return {
phase: t < plan.crossAt ? 'thrown' : 'cross', jennyX: x, jennyFrame: clip('crash', plan.impactAt, false),
lights: 1 - s * 0.6, lightsX: x - JENNY_BOX.w / 2,
hero: { ...thrown, frame: `hit-0${Math.min(7, Math.floor((t - plan.impactAt) * 12))}` },
hero: { ...thrown, frame: `hit-${String(Math.min(frameCount(clips.hurt, 'hit', 8) - 1, frameAt(clips.hurt, 'hit', t - plan.impactAt))).padStart(2, '0')}` },
};
}

Expand Down
4 changes: 3 additions & 1 deletion game/src/story/Flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export class Flow extends Phaser.Scene {
const camera = stage.cameras.main;
const viewRight = camera.scrollX + camera.width;
const plan = ramPlan(hero.x, viewRight);
// Her clips and the hero's hit at their own speed (#156: one timing source).
const clips = { jenny: stage.cache.json.get('boss-jenny.anims'), hurt: stage.cache.json.get('hero-hurt.anims') };
const lights = headlights(stage);
const jenny = stage.add.sprite(plan.startX, hero.y, 'boss-jenny', 'rev-00').setOrigin(0.5, 1).setDepth(150);
let thrown: Phaser.GameObjects.Sprite | undefined;
Expand All @@ -114,7 +116,7 @@ export class Flow extends Phaser.Scene {
// Driven by the stage clock (not tweens), so it pauses with the stage and steps in tests.
const step = (_time: number, delta: number) => {
t += delta / 1000;
const f = ramFrame(t, plan, hero, viewRight);
const f = ramFrame(t, plan, hero, viewRight, clips);
const entered = f.phase !== last;
last = f.phase;
jenny.setPosition(Math.round(f.jennyX), hero.y + (f.phase === 'rev' ? Math.floor(t * 24) % 2 : 0)).setFrame(f.jennyFrame);
Expand Down
10 changes: 8 additions & 2 deletions game/src/story/Prologue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export type Crash404 = (hero: CrossingHero, onLand: () => void) => void;
* `act`, the `secret` box with its item, the roof glint, and the `anomaly` box at the jetty edge.
*/
export class Prologue {
/** The shove clip's speed, from the dexter board (#156: one timing source). */
private get shoveFps(): number {
return (this.stage.cache.json.get('dexter.anims') as Record<string, { fps: number }> | undefined)?.shove?.fps ?? 12;
}
readonly guide: DexterGuide;
readonly finale: FinaleSequence;
private dexter: Phaser.GameObjects.Sprite;
Expand All @@ -77,7 +81,9 @@ export class Prologue {
const boxes = worlds.flatMap(w => w.interact);
const spawns = worlds.flatMap(w => w.spawn);
const bench = spawns.find(s => s.id === 'dexter-bench') ?? spawns.find(s => s.kind === 'dexter')!;
this.guide = new DexterGuide({ bench, lines, ...stopsFrom(boxes) });
// The hop's length is the dexter board's hop clip (#156: one timing source).
const hop = (stage.cache.json.get('dexter.anims') as Record<string, { frames: number; fps: number }> | undefined)?.hop;
this.guide = new DexterGuide({ bench, lines, ...stopsFrom(boxes), ...(hop && { hopClip: hop.frames / hop.fps * 1000 }) });
this.pondEdge = boxes.find(b => b.kind === 'anomaly');
this.finale = new FinaleSequence(() => this.toCrash());
createAnims(stage, ANIMS);
Expand Down Expand Up @@ -135,7 +141,7 @@ export class Prologue {
}
if (phase === 'shove') {
if (entered) this.play('dexter:shove');
if (!this.proxy && t * FINALE.shove >= (FINALE.contactFrame * 1000) / 12) this.knock(player);
if (!this.proxy && t * FINALE.shove >= FINALE.contactFrame * 1000 / this.shoveFps) this.knock(player);
}
if (phase === 'fall') {
if (entered) {
Expand Down
15 changes: 10 additions & 5 deletions game/src/story/prologueGuide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ export interface GuideSetup {
stops: Stop[];
/** walk + oneWay polylines in stage px. */
lines: Pt[][];
/** His hop clip's length (ms), from dexter.anims.json (#156: one timing source); HOP_CLIP without it. */
hopClip?: number;
}

/** The stage's scale (docs/09: the 76 px hero is 1.83 m) and Dexter's height in it (docs/03: a 27 px plush). */
export const PX_PER_M = 43;
export const PLUSH = 27;
/** His hop clip: dexter `hop`, 4 frames at 12 fps. */
const HOP_CLIP = (4 / 12) * 1000;
/** His hop clip's length (ms) when the dexter board's anims.json is not given: 4 frames at 12 fps. */
export const HOP_CLIP = (4 / 12) * 1000;

export const GUIDE = {
/** He keeps this far ahead of the hero (px): four plush-lengths, ~2.5 m. */
Expand Down Expand Up @@ -153,6 +156,8 @@ export class DexterGuide {
private lastHeroX?: number;
readonly route: Route;

/** His hop's length (ms): the hop clip's, from the board. */
private get hopMs() { return this.setup.hopClip ?? GUIDE.hopTime; }
constructor(private readonly setup: GuideSetup) {
this.x = setup.bench.x;
this.y = setup.bench.y;
Expand Down Expand Up @@ -232,7 +237,7 @@ export class DexterGuide {
const toY = this.route.groundAt(toX, this.y + PLUSH) ?? this.y;
this.facing = 1;
this.enter('hop-off');
this.hop = { from: { x: this.x, y: this.y }, to: { x: toX, y: toY }, t: 0, duration: GUIDE.hopOff, height: GUIDE.hopLift + 6 };
this.hop = { from: { x: this.x, y: this.y }, to: { x: toX, y: toY }, t: 0, duration: this.hopMs * 1.4, height: GUIDE.hopLift + 6 };
}

private waitFor(resumeAt: number): void {
Expand Down Expand Up @@ -276,7 +281,7 @@ export class DexterGuide {
const aheadX = this.x + GUIDE.lookAhead;
const ahead = this.route.groundAt(aheadX, this.y);
if (ahead !== undefined && Math.abs(this.y - ahead) <= GUIDE.stepRise) return false;
const duration = GUIDE.hopTime * 0.75;
const duration = this.hopMs * 0.75;
const reach = (this.hopSpeed() * duration) / 1000;
let toX = this.x + Math.max(GUIDE.lookAhead + 16, reach);
let toY = this.route.groundAt(toX, ahead ?? this.y);
Expand All @@ -297,7 +302,7 @@ export class DexterGuide {

/** The hero closed in: a hop ahead along the route at sprint speed (one hop clip long). */
private hopAhead(): boolean {
const distance = Math.min(this.end() - this.x, (this.hopSpeed() * GUIDE.hopTime) / 1000);
const distance = Math.min(this.end() - this.x, (this.hopSpeed() * this.hopMs) / 1000);
if (distance < PLUSH) return false;
let toX = this.x + distance;
let toY = this.route.groundAt(toX, this.y);
Expand All @@ -308,7 +313,7 @@ export class DexterGuide {
toY = this.route.groundAt(toX, this.y) ?? this.y;
}
this.v = Math.max(this.v, this.heroV, GUIDE.walk);
this.jump({ x: toX, y: toY }, GUIDE.hopTime, GUIDE.hopLift + Math.max(0, this.y - toY));
this.jump({ x: toX, y: toY }, this.hopMs, GUIDE.hopLift + Math.max(0, this.y - toY));
return true;
}

Expand Down
Loading