Exact source used by this build. SHA-256: 10dccde820fa279c88b86b463efa3dd836eceb0dc8b628711980a5c7280a84ea.

1 import { z } from "zod";
2 import type { IntentResult, Player, SimRules, Warp, WorldState } from "../../types";
3 import { withPlayer } from "../../state";
4 import { defineIntent, defineSystem, type SystemContext } from "../registry";
5 import { FacingSchema } from "../schema";
6 import { isBlocked, movementPath } from "./geometry";
7 export * from "./geometry";
8 import { getMineEquipmentEffects } from "../mines/model";
9 export function movementSpeed(player: Player, rules: SimRules): number { return 4 + getMineEquipmentEffects(player, rules).speed; }
10 function contains(warp: Warp, x: number, y: number): boolean {
11   return x >= warp.x && y >= warp.y && x < warp.x + warp.width && y < warp.y + warp.height;
12 }
13 function transition(state: WorldState, player: Player, warp: Warp, context: Pick<SystemContext, "isWarpUnlocked" | "positionError">): IntentResult {
14   const destination = state.areas[warp.targetAreaId];
15   if ((warp.locked || destination?.locked) && !context.isWarpUnlocked?.(warp)) return { state, events: [{ type: "repairsNeeded", playerId: player.id, areaId: player.areaId, message: "Repairs needed" }] };
16   if (!destination || isBlocked(destination, warp.targetX, warp.targetY)) return { state, events: [], error: "Destination is unavailable" };
17   const error = context.positionError?.({ ...player, areaId: destination.id }, warp.targetX, warp.targetY);
18   if (error) return { state, events: [], error };
19   return { state: withPlayer(state, { ...player, areaId: destination.id, x: warp.targetX, y: warp.targetY }), events: [{ type: "areaChanged", playerId: player.id, areaId: destination.id, snapshot: true }] };
20 }
21 export const movementSystem = defineSystem({
22   id: "movement", worldDefaults: () => ({}), playerDefaults: () => ({}), events: ["areaChanged", "repairsNeeded"],
23   movementSpeedMultiplier: (_state, player, rules) => movementSpeed(player, rules) / 4,
24   initializePlayer: (state, player) => {
25     const spawn = player.areaId === state.starterPlayer.areaId ? state.starterPlayer : state.areas[player.areaId]!.spawn;
26     return { x: spawn.x, y: spawn.y, facing: player.areaId === state.starterPlayer.areaId ? state.starterPlayer.facing : "down" };
27   },
28   intents: [
29     defineIntent("move", z.object({ type: z.literal("move"), x: z.number(), y: z.number(), facing: FacingSchema }), ({ state, player, elapsedMs, rules, movementSpeed: registeredSpeed, positionError, isWarpUnlocked }, intent) => {
30       const area = state.areas[player.areaId];
31       if (!area || !Number.isFinite(intent.x) || !Number.isFinite(intent.y)) return { state, events: [], error: "Invalid move" };
32       if (intent.x === player.x && intent.y === player.y && intent.facing === player.facing) return { state, events: [], timingCostMs: 0 };
33       const elapsed = Number.isFinite(elapsedMs) ? Math.min(1000, Math.max(0, elapsedMs ?? 0)) : 0;
34       const distance = Math.hypot(intent.x - player.x, intent.y - player.y);
35       const speed = registeredSpeed ?? movementSpeed(player, rules);
36       if (distance > speed * (elapsed / 1000) + 0.001) return { state, events: [], error: "Move exceeds speed limit" };
37       if (intent.x < 0 || intent.y < 0 || intent.x >= area.width || intent.y >= area.height) return { state, events: [], error: "Move is out of bounds" };
38       for (const { x, y, progress, collisionOnly } of movementPath(area, player, intent)) {
39         if (isBlocked(area, x, y)) return { state, events: [], error: "Move is blocked" };
40         const error = positionError?.(player, x, y);
41         if (error) return { state, events: [], error };
42         if (collisionOnly) continue;
43         const warp = area.warps.find((entry) => contains(entry, x, y) && !contains(entry, player.x, player.y));
44         if (warp) {
45           const result = transition(state, { ...player, facing: intent.facing }, warp, { positionError, isWarpUnlocked });
46           return { ...result, timingCostMs: result.error || result.state === state ? 0 : distance * progress / speed * 1000 };
47         }
48       }
49       return { state: withPlayer(state, { ...player, x: intent.x, y: intent.y, facing: intent.facing }), events: [], timingCostMs: distance / speed * 1000 };
50     }, { timing: "movement", timingBufferMs: 100, persistence: "batched" }),
51     defineIntent("interact", z.object({ type: z.literal("interact"), target: z.string().min(1) }), ({ state, player, positionError, isWarpUnlocked }, intent) => {
52       const warp = state.areas[player.areaId]?.warps.find((entry) => entry.id === intent.target);
53       if (!warp) return { state, events: [], error: "Unknown interaction" };
54       const nearestX = Math.max(warp.x, Math.min(player.x, warp.x + warp.width));
55       const nearestY = Math.max(warp.y, Math.min(player.y, warp.y + warp.height));
56       if (Math.hypot(player.x - nearestX, player.y - nearestY) > 2) return { state, events: [], error: "Interaction is out of reach" };
57       return transition(state, player, warp, { positionError, isWarpUnlocked });
58     }),
59   ],
60 });
61