The workshop walks through warps; conjurations wear their own inks
A stride into a warp mouth now carries the walker out of the paired mouth mid-glide, body turned exactly as the rays turn — canWalk admits a rim edge only where a mouth waits, and the crawler splits the stride at the crossing. And with painted conjuration sprites arriving, the fx pass composites them normally instead of additively: painted art is ink, not light, and additive blending would bloom its outlines white. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b82d526a79
commit
bfece01468
@@ -220,10 +220,9 @@
|
||||
const img = imageFor(s.src) ?? (s.fallback ? fxFallback(s.fallback) : null);
|
||||
const iw = img instanceof HTMLImageElement ? img.naturalWidth : (img?.width ?? 0);
|
||||
const ih = img instanceof HTMLImageElement ? img.naturalHeight : (img?.height ?? 0);
|
||||
if (s.glow) {
|
||||
ctx.globalCompositeOperation = "lighter";
|
||||
ctx.globalAlpha = s.alpha ?? 1;
|
||||
}
|
||||
// Conjurations are painted art, not light sources: normal
|
||||
// compositing keeps their inks true; only the fade is borrowed.
|
||||
if (s.glow) ctx.globalAlpha = s.alpha ?? 1;
|
||||
for (let col = Math.max(0, s.left | 0); col < Math.min(W, s.right); col++) {
|
||||
if (s.depth >= zbuf[col]!) continue;
|
||||
const texX = ((col - s.left) / (s.right - s.left));
|
||||
@@ -243,10 +242,7 @@
|
||||
ctx.fillRect(col, s.top, 1, s.bottom - s.top);
|
||||
}
|
||||
}
|
||||
if (s.glow) {
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
if (s.glow) ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// A whisper of vignette holds the torchlit mood together.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { GameEvent, GameState, GameView } from "@wizwar/engine";
|
||||
import FirstPerson from "./FirstPerson.svelte";
|
||||
import Replay from "../Replay.svelte";
|
||||
import { canWalk } from "./raycast";
|
||||
import { canWalk, edgeMid, SIDE_ANGLE, OPPOSITE } from "./raycast";
|
||||
|
||||
const q = new URLSearchParams(location.search);
|
||||
const seed = Number(q.get("seed") ?? 42);
|
||||
@@ -44,6 +44,9 @@
|
||||
type Anim =
|
||||
| { kind: "glide"; fx: number; fy: number; tx: number; ty: number; t0: number; dur: number }
|
||||
| { kind: "turn"; from: number; to: number; t0: number; dur: number }
|
||||
| { kind: "warpglide"; fx: number; fy: number; mx: number; my: number;
|
||||
nx: number; ny: number; tx: number; ty: number; dturn: number;
|
||||
turned: boolean; t0: number; dur: number }
|
||||
| null;
|
||||
let anim: Anim = null;
|
||||
const ease = (w: number) => w * w * (3 - 2 * w);
|
||||
@@ -63,6 +66,27 @@
|
||||
const side = SIDES4[idx]!;
|
||||
const cell = { x: Math.floor(x), y: Math.floor(y) };
|
||||
if (!canWalk(view, cell, side)) return;
|
||||
const to = {
|
||||
x: cell.x + (side === "E" ? 1 : side === "W" ? -1 : 0),
|
||||
y: cell.y + (side === "S" ? 1 : side === "N" ? -1 : 0),
|
||||
};
|
||||
// An open rim edge canWalk allowed is a warp mouth: stride into it,
|
||||
// and the stride carries on out of the paired mouth, the body turned
|
||||
// exactly as the rays turn.
|
||||
const warp = !view.board.cells[cellKey(to)]
|
||||
? view.board.warps.find((w) => cellKey(w.from.cell) === cellKey(cell) && w.from.side === side)
|
||||
: undefined;
|
||||
if (warp) {
|
||||
const mouth = edgeMid(cell, side);
|
||||
const far = edgeMid(warp.to.cell, warp.to.side);
|
||||
anim = {
|
||||
kind: "warpglide", fx: x, fy: y, mx: mouth.x, my: mouth.y,
|
||||
nx: far.x, ny: far.y, tx: warp.to.cell.x + 0.5, ty: warp.to.cell.y + 0.5,
|
||||
dturn: SIDE_ANGLE[OPPOSITE[warp.to.side]] - SIDE_ANGLE[warp.from.side],
|
||||
turned: false, t0: now, dur: 320,
|
||||
};
|
||||
return;
|
||||
}
|
||||
const tx = x + (side === "E" ? 1 : side === "W" ? -1 : 0);
|
||||
const ty = y + (side === "S" ? 1 : side === "N" ? -1 : 0);
|
||||
anim = { kind: "glide", fx: x, fy: y, tx, ty, t0: now, dur: 280 };
|
||||
@@ -76,6 +100,18 @@
|
||||
if (anim.kind === "glide") {
|
||||
x = anim.fx + (anim.tx - anim.fx) * ease(w);
|
||||
y = anim.fy + (anim.ty - anim.fy) * ease(w);
|
||||
} else if (anim.kind === "warpglide") {
|
||||
// Half the stride reaches the mouth; the moment it crosses, the
|
||||
// eye stands at the far mouth, turned, finishing the stride.
|
||||
const e = ease(w);
|
||||
if (e < 0.5) {
|
||||
x = anim.fx + (anim.mx - anim.fx) * (e * 2);
|
||||
y = anim.fy + (anim.my - anim.fy) * (e * 2);
|
||||
} else {
|
||||
if (!anim.turned) { facing += anim.dturn; anim.turned = true; }
|
||||
x = anim.nx + (anim.tx - anim.nx) * ((e - 0.5) * 2);
|
||||
y = anim.ny + (anim.ty - anim.ny) * ((e - 0.5) * 2);
|
||||
}
|
||||
} else {
|
||||
facing = anim.from + (anim.to - anim.from) * ease(w);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ export interface Hit {
|
||||
warped?: boolean;
|
||||
}
|
||||
|
||||
const SIDE_ANGLE: Record<Side, number> = { E: 0, S: Math.PI / 2, W: Math.PI, N: -Math.PI / 2 };
|
||||
const OPPOSITE: Record<Side, Side> = { E: "W", W: "E", N: "S", S: "N" };
|
||||
export const SIDE_ANGLE: Record<Side, number> = { E: 0, S: Math.PI / 2, W: Math.PI, N: -Math.PI / 2 };
|
||||
export const OPPOSITE: Record<Side, Side> = { E: "W", W: "E", N: "S", S: "N" };
|
||||
|
||||
/** Is this edge passable to the EYE (rays), and if not, what is it? */
|
||||
function edgeObstacle(view: GameView, key: string): Hit["kind"] | null {
|
||||
@@ -155,7 +155,8 @@ export function castRay(view: GameView, ox: number, oy: number, angle: number):
|
||||
}
|
||||
|
||||
/** Can a wizard's body (not just their eye) cross this edge? Workshop
|
||||
* collision: walls and closed doors stop you, fire and warps do not. */
|
||||
* collision: walls and closed doors stop you; fire does not, and an open
|
||||
* rim edge carries you only where a warp mouth waits. */
|
||||
export function canWalk(view: GameView, from: Cell, side: Side): boolean {
|
||||
const key = edgeKey(from, side);
|
||||
const e = view.board.edges[key] ?? "open";
|
||||
@@ -166,11 +167,23 @@ export function canWalk(view: GameView, from: Cell, side: Side): boolean {
|
||||
x: from.x + (side === "E" ? 1 : side === "W" ? -1 : 0),
|
||||
y: from.y + (side === "S" ? 1 : side === "N" ? -1 : 0),
|
||||
};
|
||||
if (!view.board.cells[cellKey(to)]) return false; // the rim (warp-walk later)
|
||||
if (!view.board.cells[cellKey(to)]) {
|
||||
return view.board.warps.some(
|
||||
(w) => cellKey(w.from.cell) === cellKey(from) && w.from.side === side,
|
||||
);
|
||||
}
|
||||
if (view.squareContents[cellKey(to)]?.kind === "stone") return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The midpoint of one cell edge, in world units. */
|
||||
export function edgeMid(cell: Cell, side: Side): { x: number; y: number } {
|
||||
if (side === "E") return { x: cell.x + 1, y: cell.y + 0.5 };
|
||||
if (side === "W") return { x: cell.x, y: cell.y + 0.5 };
|
||||
if (side === "S") return { x: cell.x + 0.5, y: cell.y + 1 };
|
||||
return { x: cell.x + 0.5, y: cell.y };
|
||||
}
|
||||
|
||||
export interface Billboard {
|
||||
/** World position (cell-centered). */
|
||||
x: number;
|
||||
|
||||
Reference in New Issue
Block a user