The eyes watch through a warp mouth; a cutaway is for what no sight reaches
The Warp Sniper cut to another angle the moment the fireball landed, though the caster could see the whole thing: the director counted a warp mouth before the target as blocked sight. Sight threads warps as the rules say, and now the camera does too. director.sightline finds the heading that actually sees a point — straight, or through a mouth by aiming at the target's position in the far room and asking whether the bent ray reaches it — and both the aim and the turn-toward-the- actor rules use it. Only a point no ray reaches earns the cutaway. Warp Sniper's second beat re-blessed: Wanderer's eyes now find Rival through the mouth as the turn passes to him. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
7c0a761c59
commit
86856781df
Binary file not shown.
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 260 KiB |
@@ -7,7 +7,7 @@
|
|||||||
import { scheduleFx, type BoardFx } from "./fx";
|
import { scheduleFx, type BoardFx } from "./fx";
|
||||||
import { fpFxForEvents, type FpFx } from "./fpv/fx3d";
|
import { fpFxForEvents, type FpFx } from "./fpv/fx3d";
|
||||||
import { castRay, edgeMid } from "./fpv/raycast";
|
import { castRay, edgeMid } from "./fpv/raycast";
|
||||||
import { cutawayStand, deepestFacing, aimOfEvents, gatherGlides, hurledIn, shortestArc } from "./fpv/director";
|
import { cutawayStand, deepestFacing, aimOfEvents, gatherGlides, hurledIn, shortestArc, sightline } from "./fpv/director";
|
||||||
import { prefs } from "./prefs.svelte";
|
import { prefs } from "./prefs.svelte";
|
||||||
import { cardDef, isPermanentDuration, stackSightTrace } from "@wizwar/engine";
|
import { cardDef, isPermanentDuration, stackSightTrace } from "@wizwar/engine";
|
||||||
import type { GameEvent, GameView } from "@wizwar/engine";
|
import type { GameEvent, GameView } from "@wizwar/engine";
|
||||||
@@ -387,26 +387,20 @@
|
|||||||
}
|
}
|
||||||
else if (aim) {
|
else if (aim) {
|
||||||
const ax = aim.x + 0.5, ay = aim.y + 0.5;
|
const ax = aim.x + 0.5, ay = aim.y + 0.5;
|
||||||
const toAim = Math.hypot(ax - tx, ay - ty);
|
// The eyes watch their own magic land wherever they can see it —
|
||||||
const sight = castRay(v, tx, ty, Math.atan2(ay - ty, ax - tx));
|
// straight down a corridor or through a warp mouth. Only a target
|
||||||
// Blocked: a wall before the target, or a warp mouth before it —
|
// out of all sight earns the cutaway.
|
||||||
// a warp BEYOND the target bends nothing the eyes need.
|
const heading = sightline(v, tx, ty, ax, ay);
|
||||||
const warpBefore = sight.warped && (sight.warpDist ?? Infinity) < toAim - 0.4;
|
if (heading === null) return takeCutaway(ax, ay);
|
||||||
if (warpBefore || sight.dist < toAim - 0.4) {
|
targetFacing = heading;
|
||||||
return takeCutaway(ax, ay);
|
|
||||||
}
|
|
||||||
targetFacing = Math.atan2(ay - ty, ax - tx);
|
|
||||||
aimed = true;
|
aimed = true;
|
||||||
} else if (actor && actor.id !== povId &&
|
} else if (actor && actor.id !== povId &&
|
||||||
(actor.position.x !== me.position.x || actor.position.y !== me.position.y)) {
|
(actor.position.x !== me.position.x || actor.position.y !== me.position.y)) {
|
||||||
// Turn toward the actor — but only if these eyes could actually see
|
// Turn toward the actor — but only if these eyes could actually see
|
||||||
// them: an unbroken straight sight line, no warps bending it.
|
// them, straight or through a mouth.
|
||||||
const ax = actor.position.x + 0.5, ay = actor.position.y + 0.5;
|
const heading = sightline(v, tx, ty, actor.position.x + 0.5, actor.position.y + 0.5, 0.2);
|
||||||
const toActor = Math.hypot(ax - tx, ay - ty);
|
if (heading !== null) {
|
||||||
const ray = castRay(v, tx, ty, Math.atan2(ay - ty, ax - tx));
|
targetFacing = heading;
|
||||||
const warpBefore = ray.warped && (ray.warpDist ?? Infinity) < toActor - 0.2;
|
|
||||||
if (!warpBefore && ray.dist > toActor - 0.2) {
|
|
||||||
targetFacing = Math.atan2(ay - ty, ax - tx);
|
|
||||||
aimed = true;
|
aimed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// between one view and the next. One ladder, so the replay and the
|
// between one view and the next. One ladder, so the replay and the
|
||||||
// table never learn different instincts.
|
// table never learn different instincts.
|
||||||
|
|
||||||
import { edgeMid } from "./raycast";
|
import { edgeMid, warpMotion } from "./raycast";
|
||||||
import { fpFxForEvents } from "./fx3d";
|
import { fpFxForEvents } from "./fx3d";
|
||||||
import type { GameEvent, GameView } from "@wizwar/engine";
|
import type { GameEvent, GameView } from "@wizwar/engine";
|
||||||
import { castRay } from "./raycast";
|
import { castRay } from "./raycast";
|
||||||
@@ -95,6 +95,32 @@ export function aimOfEvents(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The heading down which the eyes at (tx,ty) actually SEE the point
|
||||||
|
* (ax,ay): straight, if an unbroken ray reaches it; through a warp
|
||||||
|
* mouth, if a ray bent by that mouth reaches the point's position in
|
||||||
|
* the far room — sight threads warps as the rules say, and so must the
|
||||||
|
* camera. Null when nothing reaches it: that is what a cutaway is for. */
|
||||||
|
export function sightline(
|
||||||
|
view: GameView, tx: number, ty: number, ax: number, ay: number, slack = 0.4,
|
||||||
|
): number | null {
|
||||||
|
const straight = Math.atan2(ay - ty, ax - tx);
|
||||||
|
const toAim = Math.hypot(ax - tx, ay - ty);
|
||||||
|
const direct = castRay(view, tx, ty, straight);
|
||||||
|
const warpBefore = direct.warped && (direct.warpDist ?? Infinity) < toAim - slack;
|
||||||
|
if (!warpBefore && direct.dist >= toAim - slack) return straight;
|
||||||
|
// Through each mouth: aim at where the target would stand if the far
|
||||||
|
// room continued past the opening, and ask whether the bent ray gets
|
||||||
|
// that far. The motion is rigid, so path length matches distance.
|
||||||
|
for (let i = 0; i < view.board.warps.length; i++) {
|
||||||
|
const virt = warpMotion(view.board.warps[i]!).toVirtual({ x: ax, y: ay });
|
||||||
|
const heading = Math.atan2(virt.y - ty, virt.x - tx);
|
||||||
|
const span = Math.hypot(virt.x - tx, virt.y - ty);
|
||||||
|
const ray = castRay(view, tx, ty, heading);
|
||||||
|
if (ray.warpId === i && ray.dist >= span - slack) return heading;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Does this batch hurl the pov wizard bodily — a blow to glide with,
|
/** Does this batch hurl the pov wizard bodily — a blow to glide with,
|
||||||
* eyes held steady, rather than a stride or a cut? */
|
* eyes held steady, rather than a stride or a cut? */
|
||||||
export function hurledIn(events: GameEvent[], povId: string): boolean {
|
export function hurledIn(events: GameEvent[], povId: string): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user