The reel learns to shine: gliding bodies, warp-threaded shots, a clip worth sharing
Three polishes aimed at the instant-replay dream. Other wizards and creatures now GLIDE between steps in first person instead of blinking cell to cell — a stride tweens, a teleport still simply arrives. A projectile whose sight line runs through a warp now flies as two simultaneous legs under the mouths' rigid motion, so the fireball plunges into the opening on one side and bursts from the pair on the other, whichever room the camera stands in. And the saved video is no longer a bare canvas grab: it composites into a shareable 1280x720 frame with the step's caption burned in — gold actor name, the reel's own words — and the game's name in the corner, so a clip passed along explains itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e8c6b93c71
commit
b82d526a79
@@ -6,6 +6,7 @@
|
||||
|
||||
import { fxForEvents } from "../fx";
|
||||
import { CELL } from "../fx-sprites/geom";
|
||||
import { castRay, warpMotion } from "./raycast";
|
||||
import type { GameEvent, GameView } from "@wizwar/engine";
|
||||
|
||||
export type FpFx =
|
||||
@@ -34,6 +35,33 @@ const IMPACT_ART: Record<string, string> = {
|
||||
shield: "shield", absorb: "shield",
|
||||
};
|
||||
|
||||
/** One flight, possibly seen from two rooms: a projectile whose straight
|
||||
* line is broken but whose sight line runs through a warp flies as TWO
|
||||
* simultaneous legs — one in the near mouth's virtual space (visible
|
||||
* through the opening), one in the far room's real space. Each leg is
|
||||
* clipped by the depth buffer to the side the camera stands on. */
|
||||
function projectileLegs(
|
||||
view: GameView, a: { x: number; y: number }, b: { x: number; y: number },
|
||||
): { from: { x: number; y: number }; to: { x: number; y: number } }[] {
|
||||
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
||||
if (len < 0.05) return [{ from: a, to: b }];
|
||||
const direct = castRay(view, a.x, a.y, Math.atan2(b.y - a.y, b.x - a.x));
|
||||
if (!direct.warped && direct.dist >= len - 0.4) return [{ from: a, to: b }];
|
||||
for (const w of view.board.warps) {
|
||||
const motion = warpMotion(w);
|
||||
const vt = motion.toVirtual(b);
|
||||
const vlen = Math.hypot(vt.x - a.x, vt.y - a.y);
|
||||
const hit = castRay(view, a.x, a.y, Math.atan2(vt.y - a.y, vt.x - a.x));
|
||||
if (hit.warped && hit.dist >= vlen - 0.4) {
|
||||
return [
|
||||
{ from: a, to: vt },
|
||||
{ from: motion.toReal(a), to: b },
|
||||
];
|
||||
}
|
||||
}
|
||||
return [{ from: a, to: b }];
|
||||
}
|
||||
|
||||
/** Translate one step's events into first-person moments with start delays. */
|
||||
export function fpFxForEvents(
|
||||
events: GameEvent[], view: GameView, povId: string,
|
||||
@@ -42,15 +70,18 @@ export function fpFxForEvents(
|
||||
for (const { fx, delay } of fxForEvents(events, view)) {
|
||||
if (fx.kind === "fireball" || fx.kind === "bolt" || fx.kind === "waterbolt" || fx.kind === "streak") {
|
||||
const art = fx.kind === "streak" ? "spark" : fx.kind;
|
||||
out.push({
|
||||
fx: {
|
||||
id: nextId++, kind: "projectile", art,
|
||||
from: { x: fx.a.x / CELL, y: fx.a.y / CELL },
|
||||
to: { x: fx.b.x / CELL, y: fx.b.y / CELL },
|
||||
t0: 0, dur: PROJECTILE_DUR[art]!,
|
||||
},
|
||||
delay,
|
||||
});
|
||||
for (const leg of projectileLegs(
|
||||
view, { x: fx.a.x / CELL, y: fx.a.y / CELL }, { x: fx.b.x / CELL, y: fx.b.y / CELL },
|
||||
)) {
|
||||
out.push({
|
||||
fx: {
|
||||
id: nextId++, kind: "projectile", art,
|
||||
from: leg.from, to: leg.to,
|
||||
t0: 0, dur: PROJECTILE_DUR[art]!,
|
||||
},
|
||||
delay,
|
||||
});
|
||||
}
|
||||
} else if (fx.kind === "portal") {
|
||||
out.push({
|
||||
fx: { id: nextId++, kind: "impact", art: "shimmer", at: { x: fx.cell.x + 0.5, y: fx.cell.y + 0.5 }, t0: 0, dur: 550 },
|
||||
|
||||
Reference in New Issue
Block a user