@netforge/engine 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GAME-DESIGN.md +195 -0
- package/LICENSE +21 -0
- package/NOTICE +30 -0
- package/README.md +100 -2
- package/dist/api/appearance.d.ts +37 -0
- package/dist/api/appearance.js +41 -0
- package/dist/api/cards.d.ts +105 -0
- package/dist/api/cards.js +133 -0
- package/dist/api/collector.d.ts +114 -0
- package/dist/api/collector.js +204 -0
- package/dist/api/dungeon.d.ts +58 -0
- package/dist/api/dungeon.js +216 -0
- package/dist/api/harness.d.ts +149 -0
- package/dist/api/harness.js +549 -0
- package/dist/api/idle.d.ts +71 -0
- package/dist/api/idle.js +104 -0
- package/dist/api/race-harness.d.ts +3 -0
- package/dist/api/race-harness.js +148 -0
- package/dist/api/registry.d.ts +241 -0
- package/dist/api/registry.js +394 -0
- package/dist/api/scheduler.d.ts +56 -0
- package/dist/api/scheduler.js +160 -0
- package/dist/api/schemas.d.ts +958 -0
- package/dist/api/schemas.js +859 -0
- package/dist/api/tower.d.ts +127 -0
- package/dist/api/tower.js +183 -0
- package/dist/game/adapter.d.ts +69 -0
- package/dist/game/adapter.js +341 -0
- package/dist/game/arena-bot.d.ts +66 -0
- package/dist/game/arena-bot.js +188 -0
- package/dist/game/bot.d.ts +95 -0
- package/dist/game/bot.js +285 -0
- package/dist/game/compile.d.ts +97 -0
- package/dist/game/compile.js +476 -0
- package/dist/game/director.d.ts +42 -0
- package/dist/game/director.js +179 -0
- package/dist/game/earn-source.d.ts +122 -0
- package/dist/game/earn-source.js +386 -0
- package/dist/game/earn.d.ts +49 -0
- package/dist/game/earn.js +55 -0
- package/dist/game/index.d.ts +37 -0
- package/dist/game/index.js +25 -0
- package/dist/game/platformer.d.ts +156 -0
- package/dist/game/platformer.js +619 -0
- package/dist/game/profile.d.ts +97 -0
- package/dist/game/profile.js +140 -0
- package/dist/game/race.d.ts +177 -0
- package/dist/game/race.js +386 -0
- package/dist/game/racer.d.ts +98 -0
- package/dist/game/racer.js +379 -0
- package/dist/game/runner.d.ts +286 -0
- package/dist/game/runner.js +824 -0
- package/dist/game/threats.d.ts +23 -0
- package/dist/game/threats.js +49 -0
- package/dist/game/view.d.ts +47 -0
- package/dist/game/view.js +123 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +24 -0
- package/dist/net/bits.d.ts +33 -0
- package/dist/net/bits.js +135 -0
- package/dist/net/client.d.ts +75 -0
- package/dist/net/client.js +151 -0
- package/dist/net/index.d.ts +23 -0
- package/dist/net/index.js +23 -0
- package/dist/net/input.d.ts +49 -0
- package/dist/net/input.js +112 -0
- package/dist/net/interpolation.d.ts +37 -0
- package/dist/net/interpolation.js +97 -0
- package/dist/net/netsim.d.ts +22 -0
- package/dist/net/netsim.js +52 -0
- package/dist/net/prediction.d.ts +31 -0
- package/dist/net/prediction.js +101 -0
- package/dist/net/profile-client.d.ts +75 -0
- package/dist/net/profile-client.js +173 -0
- package/dist/net/protocol.d.ts +215 -0
- package/dist/net/protocol.js +10 -0
- package/dist/net/room-client.d.ts +65 -0
- package/dist/net/room-client.js +156 -0
- package/dist/net/schema.d.ts +78 -0
- package/dist/net/schema.js +121 -0
- package/dist/net/sim-adapter.d.ts +134 -0
- package/dist/net/sim-adapter.js +22 -0
- package/dist/net/snapshot.d.ts +61 -0
- package/dist/net/snapshot.js +301 -0
- package/dist/rules/adapter.d.ts +53 -0
- package/dist/rules/adapter.js +261 -0
- package/dist/rules/cards.d.ts +115 -0
- package/dist/rules/cards.js +547 -0
- package/dist/rules/collector.d.ts +161 -0
- package/dist/rules/collector.js +732 -0
- package/dist/rules/genres.d.ts +13 -0
- package/dist/rules/genres.js +38 -0
- package/dist/rules/harness.d.ts +16 -0
- package/dist/rules/harness.js +127 -0
- package/dist/rules/idle.d.ts +115 -0
- package/dist/rules/idle.js +370 -0
- package/dist/rules/index.d.ts +16 -0
- package/dist/rules/index.js +14 -0
- package/dist/rules/kit.d.ts +171 -0
- package/dist/rules/kit.js +88 -0
- package/dist/rules/tower.d.ts +214 -0
- package/dist/rules/tower.js +655 -0
- package/dist/runtime/arena.d.ts +86 -0
- package/dist/runtime/arena.js +269 -0
- package/dist/runtime/assets.d.ts +26 -0
- package/dist/runtime/assets.js +35 -0
- package/dist/runtime/camera.d.ts +113 -0
- package/dist/runtime/camera.js +247 -0
- package/dist/runtime/crowd.d.ts +18 -0
- package/dist/runtime/crowd.js +62 -0
- package/dist/runtime/feel/audio.d.ts +150 -0
- package/dist/runtime/feel/audio.js +327 -0
- package/dist/runtime/feel/clock.d.ts +52 -0
- package/dist/runtime/feel/clock.js +80 -0
- package/dist/runtime/feel/flash.d.ts +28 -0
- package/dist/runtime/feel/flash.js +60 -0
- package/dist/runtime/feel/index.d.ts +27 -0
- package/dist/runtime/feel/index.js +16 -0
- package/dist/runtime/feel/kit.d.ts +159 -0
- package/dist/runtime/feel/kit.js +226 -0
- package/dist/runtime/feel/knockback.d.ts +41 -0
- package/dist/runtime/feel/knockback.js +70 -0
- package/dist/runtime/feel/noise.d.ts +6 -0
- package/dist/runtime/feel/noise.js +30 -0
- package/dist/runtime/feel/numbers.d.ts +91 -0
- package/dist/runtime/feel/numbers.js +172 -0
- package/dist/runtime/feel/rumble.d.ts +40 -0
- package/dist/runtime/feel/rumble.js +58 -0
- package/dist/runtime/feel/sound.d.ts +96 -0
- package/dist/runtime/feel/sound.js +176 -0
- package/dist/runtime/feel/spring.d.ts +42 -0
- package/dist/runtime/feel/spring.js +95 -0
- package/dist/runtime/feel/trauma.d.ts +48 -0
- package/dist/runtime/feel/trauma.js +76 -0
- package/dist/runtime/feel/tween.d.ts +66 -0
- package/dist/runtime/feel/tween.js +180 -0
- package/dist/runtime/feel/types.d.ts +30 -0
- package/dist/runtime/feel/types.js +5 -0
- package/dist/runtime/index.d.ts +28 -0
- package/dist/runtime/index.js +26 -0
- package/dist/runtime/input.d.ts +38 -0
- package/dist/runtime/input.js +159 -0
- package/dist/runtime/interpolate.d.ts +6 -0
- package/dist/runtime/interpolate.js +13 -0
- package/dist/runtime/render/animation.d.ts +63 -0
- package/dist/runtime/render/animation.js +128 -0
- package/dist/runtime/render/atmosphere.d.ts +32 -0
- package/dist/runtime/render/atmosphere.js +59 -0
- package/dist/runtime/render/crowd-motion.d.ts +68 -0
- package/dist/runtime/render/crowd-motion.js +163 -0
- package/dist/runtime/render/decals.d.ts +64 -0
- package/dist/runtime/render/decals.js +222 -0
- package/dist/runtime/render/highlight.d.ts +29 -0
- package/dist/runtime/render/highlight.js +110 -0
- package/dist/runtime/render/index.d.ts +35 -0
- package/dist/runtime/render/index.js +22 -0
- package/dist/runtime/render/lighting.d.ts +50 -0
- package/dist/runtime/render/lighting.js +142 -0
- package/dist/runtime/render/particles.d.ts +117 -0
- package/dist/runtime/render/particles.js +289 -0
- package/dist/runtime/render/post.d.ts +92 -0
- package/dist/runtime/render/post.js +191 -0
- package/dist/runtime/render/presets.d.ts +57 -0
- package/dist/runtime/render/presets.js +116 -0
- package/dist/runtime/render/quality.d.ts +113 -0
- package/dist/runtime/render/quality.js +180 -0
- package/dist/runtime/render/rig.d.ts +29 -0
- package/dist/runtime/render/rig.js +107 -0
- package/dist/runtime/render/sprite.d.ts +34 -0
- package/dist/runtime/render/sprite.js +58 -0
- package/dist/runtime/render/stack.d.ts +74 -0
- package/dist/runtime/render/stack.js +132 -0
- package/dist/runtime/render/vat.d.ts +78 -0
- package/dist/runtime/render/vat.js +219 -0
- package/dist/runtime/renderer.d.ts +19 -0
- package/dist/runtime/renderer.js +18 -0
- package/dist/runtime/stats.d.ts +11 -0
- package/dist/runtime/stats.js +29 -0
- package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
- package/dist/runtime/styles/handdrawn/ground.js +282 -0
- package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
- package/dist/runtime/styles/handdrawn/index.js +94 -0
- package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
- package/dist/runtime/styles/handdrawn/ink.js +68 -0
- package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
- package/dist/runtime/styles/handdrawn/looks.js +42 -0
- package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
- package/dist/runtime/styles/handdrawn/paint.js +129 -0
- package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
- package/dist/runtime/styles/handdrawn/palette.js +70 -0
- package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
- package/dist/runtime/styles/handdrawn/textures.js +150 -0
- package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
- package/dist/runtime/styles/handdrawn/vfx.js +73 -0
- package/dist/runtime/styles/index.d.ts +9 -0
- package/dist/runtime/styles/index.js +32 -0
- package/dist/runtime/styles/kit.d.ts +193 -0
- package/dist/runtime/styles/kit.js +1 -0
- package/dist/runtime/styles/pixel/index.d.ts +46 -0
- package/dist/runtime/styles/pixel/index.js +167 -0
- package/dist/runtime/styles/ps1/index.d.ts +29 -0
- package/dist/runtime/styles/ps1/index.js +41 -0
- package/dist/runtime/styles/ps1/lights.d.ts +47 -0
- package/dist/runtime/styles/ps1/lights.js +146 -0
- package/dist/runtime/styles/ps1/materials.d.ts +62 -0
- package/dist/runtime/styles/ps1/materials.js +285 -0
- package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
- package/dist/runtime/styles/ps1/pipeline.js +111 -0
- package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
- package/dist/runtime/styles/ps1/surfaces.js +228 -0
- package/dist/runtime/styles/toon/index.d.ts +5 -0
- package/dist/runtime/styles/toon/index.js +178 -0
- package/dist/runtime/styles/toon/ink.d.ts +39 -0
- package/dist/runtime/styles/toon/ink.js +73 -0
- package/dist/runtime/styles/toon/look.d.ts +10 -0
- package/dist/runtime/styles/toon/look.js +112 -0
- package/dist/runtime/styles/toon/material.d.ts +74 -0
- package/dist/runtime/styles/toon/material.js +208 -0
- package/dist/runtime/styles/toon/normals.d.ts +26 -0
- package/dist/runtime/styles/toon/normals.js +127 -0
- package/dist/runtime/styles/toon/shadows.d.ts +24 -0
- package/dist/runtime/styles/toon/shadows.js +42 -0
- package/dist/runtime/styles/toon/vfx.d.ts +15 -0
- package/dist/runtime/styles/toon/vfx.js +47 -0
- package/dist/runtime/styles/view.d.ts +35 -0
- package/dist/runtime/styles/view.js +34 -0
- package/dist/runtime/styles/voxel/core.d.ts +114 -0
- package/dist/runtime/styles/voxel/core.js +577 -0
- package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
- package/dist/runtime/styles/voxel/geometry.js +40 -0
- package/dist/runtime/styles/voxel/index.d.ts +21 -0
- package/dist/runtime/styles/voxel/index.js +23 -0
- package/dist/runtime/styles/voxel/kit.d.ts +6 -0
- package/dist/runtime/styles/voxel/kit.js +11 -0
- package/dist/runtime/styles/voxel/look.d.ts +10 -0
- package/dist/runtime/styles/voxel/look.js +16 -0
- package/dist/runtime/styles/voxel/props.d.ts +9 -0
- package/dist/runtime/styles/voxel/props.js +52 -0
- package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
- package/dist/runtime/styles/voxel/shapes.js +83 -0
- package/dist/runtime/styles/voxel/world.d.ts +49 -0
- package/dist/runtime/styles/voxel/world.js +422 -0
- package/llms-full.txt +460 -0
- package/llms.txt +183 -0
- package/package.json +104 -3
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platformer template's machinery: compile a manifest's levels into the
|
|
3
|
+
* engine-sim platform sim, drive it with the race runner (race.ts), seat
|
|
4
|
+
* bots that run the course, a route planner that proves every level can be
|
|
5
|
+
* finished (the gate's softlock check), and the netcode SimAdapter.
|
|
6
|
+
*/
|
|
7
|
+
import { Button, BOT_PLAYER_BASE, PlatFlag, PLAT_HALF_W, PLAT_HEIGHT, PLAT_STATS, addPlatformPlayer, applyPlatformPlayer, capturePlatformPlayer, createPlatformWorld, grantPlatformStat, moverBox, platformProgress, platformStep, removePlatformPlayer, resetPlatformPlayer, setPlatformFrozen, setPlatformLevel, } from "@netforge/engine-sim";
|
|
8
|
+
import { GameDefinitionError } from "../api/schemas.js";
|
|
9
|
+
import { RaceRunner } from "./race.js";
|
|
10
|
+
/** Platformer defaults: a 3 minute limit, 60 Hz, three seat bots. */
|
|
11
|
+
export const PLATFORMER_DEFAULTS = { roundSeconds: 180, tickRate: 60, bots: 3 };
|
|
12
|
+
const box = (x, y, w, h) => [x, y, x + w, y + h];
|
|
13
|
+
/** Compile the platformer parts of a manifest. Issues are agent-readable. */
|
|
14
|
+
export function compilePlatformer(m, tickRate, issues) {
|
|
15
|
+
const g = m.game;
|
|
16
|
+
if (!g.levels || g.levels.length === 0)
|
|
17
|
+
issues.push("a platformer game sets levels: [{ start, flag, blocks, ... }] (the courses)");
|
|
18
|
+
if (m.weapons.size > 0)
|
|
19
|
+
issues.push("weapon(...) is not part of the platformer template: hazards, springs and movers make the challenge");
|
|
20
|
+
if (m.director)
|
|
21
|
+
issues.push("director(...) is not part of the platformer template");
|
|
22
|
+
if (g.arena || g.field)
|
|
23
|
+
issues.push("arena and field are for the arena and survivor templates; a platformer declares levels");
|
|
24
|
+
if (g.players.loadout)
|
|
25
|
+
issues.push("players.loadout is for the arena template");
|
|
26
|
+
if (m.keepers.size > 0)
|
|
27
|
+
issues.push("keeper(...) is for the survivor template");
|
|
28
|
+
for (const [id, p] of m.prefabs)
|
|
29
|
+
if ((p.kind ?? "enemy") === "enemy" && !p.prop)
|
|
30
|
+
issues.push(`prefab "${id}" is an enemy; the platformer template has no enemies (use level hazards)`);
|
|
31
|
+
const upgradeStat = [];
|
|
32
|
+
const statSteps = PLAT_STATS.map(() => 0);
|
|
33
|
+
for (const [id, u] of m.upgrades) {
|
|
34
|
+
const i = PLAT_STATS.indexOf(u.stat);
|
|
35
|
+
if (i < 0) {
|
|
36
|
+
issues.push(`upgrade "${id}" raises ${u.stat}; platformer upgrades raise one of: ${PLAT_STATS.join(", ")}`);
|
|
37
|
+
upgradeStat.push("speed");
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (u.stat === "speed" && u.step > 0.5)
|
|
41
|
+
issues.push(`upgrade "${id}": a speed step above 0.5 (+50% a level) breaks the courses`);
|
|
42
|
+
if (u.stat === "amount" && u.step > 1)
|
|
43
|
+
issues.push(`upgrade "${id}": at most 1 extra air jump a level`);
|
|
44
|
+
statSteps[i] = u.step;
|
|
45
|
+
upgradeStat.push(u.stat);
|
|
46
|
+
}
|
|
47
|
+
const names = [];
|
|
48
|
+
const levels = (g.levels ?? []).map((l, li) => {
|
|
49
|
+
names.push(l.name ?? `Level ${li + 1}`);
|
|
50
|
+
const solids = l.blocks.map(([x, y, w, h]) => box(x, y, w, h));
|
|
51
|
+
const where = `levels[${li}]`;
|
|
52
|
+
const inside = (x, y) => solids.some((b) => x > b[0] + 0.05 && x < b[2] - 0.05 && y > b[1] + 0.05 && y < b[3] - 0.05);
|
|
53
|
+
const body = (x, y) => solids.some((b) => x + PLAT_HALF_W > b[0] + 0.01 && x - PLAT_HALF_W < b[2] - 0.01 && y + PLAT_HEIGHT > b[1] + 0.01 && y < b[3] - 0.01);
|
|
54
|
+
if (body(l.start[0], l.start[1]))
|
|
55
|
+
issues.push(`${where}.start [${l.start.join(", ")}] is inside a block: put it on top of one (y = the block's top)`);
|
|
56
|
+
if (l.flag[0] <= l.start[0] + 5)
|
|
57
|
+
issues.push(`${where}.flag must be at least 5 m right of start (x runs from start to flag)`);
|
|
58
|
+
if (body(l.flag[0], l.flag[1]))
|
|
59
|
+
issues.push(`${where}.flag [${l.flag.join(", ")}] is inside a block: stand it on top of one`);
|
|
60
|
+
l.coins.forEach(([x, y], i) => {
|
|
61
|
+
if (inside(x, y))
|
|
62
|
+
issues.push(`${where}.coins[${i}] [${x}, ${y}] is inside a block`);
|
|
63
|
+
});
|
|
64
|
+
let lastX = -Infinity;
|
|
65
|
+
l.checkpoints.forEach(([x, y], i) => {
|
|
66
|
+
if (body(x, y))
|
|
67
|
+
issues.push(`${where}.checkpoints[${i}] [${x}, ${y}] is inside a block`);
|
|
68
|
+
if (x <= lastX)
|
|
69
|
+
issues.push(`${where}.checkpoints are in course order: each one further right than the last`);
|
|
70
|
+
lastX = x;
|
|
71
|
+
});
|
|
72
|
+
const lowest = Math.min(...solids.map((b) => b[1]), l.start[1]);
|
|
73
|
+
return {
|
|
74
|
+
start: l.start,
|
|
75
|
+
flag: l.flag,
|
|
76
|
+
killY: l.killY ?? lowest - 8,
|
|
77
|
+
solids,
|
|
78
|
+
ledges: l.ledges.map(([x, y, w]) => box(x, y - 0.3, w, 0.3)),
|
|
79
|
+
hazards: l.hazards.map(([x, y, w, h]) => box(x, y, w, h)),
|
|
80
|
+
springs: l.springs.map(([x, y, power]) => ({ x, y, power })),
|
|
81
|
+
movers: l.movers.map((mv) => ({
|
|
82
|
+
box: box(mv.at[0], mv.at[1] - 0.3, mv.at[2], 0.3),
|
|
83
|
+
dx: mv.to[0],
|
|
84
|
+
dy: mv.to[1],
|
|
85
|
+
periodTicks: Math.max(2, Math.round(mv.seconds * tickRate)),
|
|
86
|
+
phaseTicks: Math.round(mv.phase * mv.seconds * tickRate),
|
|
87
|
+
})),
|
|
88
|
+
coins: l.coins,
|
|
89
|
+
checkpoints: l.checkpoints,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
const mo = g.motor ?? {};
|
|
93
|
+
const tuning = Object.fromEntries(Object.entries(mo).filter(([, v]) => v !== undefined));
|
|
94
|
+
return { levels, names, tuning, statSteps, upgradeStat };
|
|
95
|
+
}
|
|
96
|
+
const surfaces = (lv, tick) => [...lv.solids, ...lv.ledges, ...lv.movers.map((m) => moverBox(m, tick))];
|
|
97
|
+
/** Is there something to stand on at x, at most `drop` metres below y (and not above y + up)? */
|
|
98
|
+
function groundAt(lv, x, y, drop, tick, up = 0.05) {
|
|
99
|
+
for (const b of surfaces(lv, tick))
|
|
100
|
+
if (x > b[0] - 0.05 && x < b[2] + 0.05 && b[3] <= y + up && b[3] >= y - drop)
|
|
101
|
+
return true;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* What a course-running bot does this tick: run toward the flag, jump at
|
|
106
|
+
* walls, gaps and hazards, wait for a moving platform, hold Jump for a full
|
|
107
|
+
* jump, dash across a gap it is falling into. Deterministic.
|
|
108
|
+
*/
|
|
109
|
+
export function platformBotInput(w, p, mem, clock, skill) {
|
|
110
|
+
const lv = w.level;
|
|
111
|
+
const idle = { seq: 0, tick: clock, moveX: 0, moveZ: 0, yaw: 0, buttons: 0 };
|
|
112
|
+
if ((p.flags & (PlatFlag.Finished | PlatFlag.Dead | PlatFlag.Frozen)) !== 0)
|
|
113
|
+
return idle;
|
|
114
|
+
const dir = lv.flag[0] >= p.x ? 1 : -1;
|
|
115
|
+
const grounded = (p.flags & PlatFlag.Grounded) !== 0;
|
|
116
|
+
const t = w.tick;
|
|
117
|
+
const look = 0.55 + Math.abs(p.vx) * 0.12;
|
|
118
|
+
const ax = p.x + dir * look;
|
|
119
|
+
const wall = lv.solids.some((b) => ax + PLAT_HALF_W > b[0] && ax - PLAT_HALF_W < b[2] && p.y + 0.15 < b[3] && p.y + PLAT_HEIGHT > b[1]);
|
|
120
|
+
const gap = !groundAt(lv, p.x + dir * (PLAT_HALF_W + 0.35), p.y, 0.6, t);
|
|
121
|
+
const hazard = lv.hazards.some((b) => b[0] < p.x + dir * 2.4 + 0.3 && b[2] > p.x - 0.3 && (dir > 0 ? b[2] > p.x : b[0] < p.x) && b[3] > p.y - 0.4 && b[1] < p.y + 1.2);
|
|
122
|
+
// A higher ledge just ahead with nothing to walk onto: jump up to it.
|
|
123
|
+
const step = lv.solids.concat(lv.ledges).some((b) => (dir > 0 ? b[0] - p.x : p.x - b[2]) < 2.6 && (dir > 0 ? b[2] > p.x : b[0] < p.x) && b[3] > p.y + 0.5 && b[3] < p.y + 3.1);
|
|
124
|
+
let moveX = dir * (0.6 + 0.4 * skill);
|
|
125
|
+
let buttons = 0;
|
|
126
|
+
if (grounded) {
|
|
127
|
+
// A moving platform across the gap: wait until it is close.
|
|
128
|
+
const ridingMover = (p.flags & PlatFlag.OnMover) !== 0;
|
|
129
|
+
if (gap && !ridingMover) {
|
|
130
|
+
const mv = lv.movers.find((m) => {
|
|
131
|
+
const b0 = moverBox(m, t);
|
|
132
|
+
const span0 = Math.min(b0[0], b0[0] + m.dx);
|
|
133
|
+
const span1 = Math.max(b0[2], b0[2] + m.dx);
|
|
134
|
+
return (dir > 0 ? span1 > p.x : span0 < p.x) && Math.abs((dir > 0 ? span0 : span1) - p.x) < 7;
|
|
135
|
+
});
|
|
136
|
+
if (mv) {
|
|
137
|
+
const soon = moverBox(mv, t + Math.round(0.35 * w.tickRate));
|
|
138
|
+
const near = dir > 0 ? soon[0] - (p.x + PLAT_HALF_W) : p.x - PLAT_HALF_W - soon[2];
|
|
139
|
+
if (near > 1.6 || soon[3] > p.y + 2.8 || soon[3] < p.y - 3)
|
|
140
|
+
moveX = 0;
|
|
141
|
+
else
|
|
142
|
+
buttons |= Button.Jump;
|
|
143
|
+
}
|
|
144
|
+
else
|
|
145
|
+
buttons |= Button.Jump;
|
|
146
|
+
}
|
|
147
|
+
else if (wall || hazard || (gap && !ridingMover) || (step && wall))
|
|
148
|
+
buttons |= Button.Jump;
|
|
149
|
+
if (ridingMover) {
|
|
150
|
+
// Ride until the next solid ground is in reach, then jump off.
|
|
151
|
+
const land = groundAt(lv, p.x + dir * 2.5, p.y + 1.5, 3, t, 1.6);
|
|
152
|
+
if (land)
|
|
153
|
+
buttons |= Button.Jump;
|
|
154
|
+
else
|
|
155
|
+
moveX = 0;
|
|
156
|
+
}
|
|
157
|
+
mem.holdJump = buttons & Button.Jump ? Math.round(0.4 * w.tickRate) : 0;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
if (mem.holdJump > 0 && p.vy > 0) {
|
|
161
|
+
mem.holdJump--;
|
|
162
|
+
buttons |= Button.Jump;
|
|
163
|
+
}
|
|
164
|
+
// Falling into a gap: dash toward the flag if there is ground within a dash.
|
|
165
|
+
if (p.vy < -2 && (p.flags & PlatFlag.DashReady) !== 0 && !groundAt(lv, p.x, p.y, 4, t) && groundAt(lv, p.x + dir * 3.2, p.y + 0.5, 2.5, t, 1))
|
|
166
|
+
buttons |= Button.Sprint;
|
|
167
|
+
// Do not drift into a hazard below.
|
|
168
|
+
const below = lv.hazards.some((b) => p.x + PLAT_HALF_W > b[0] && p.x - PLAT_HALF_W < b[2] && b[3] <= p.y && b[3] > p.y - 3);
|
|
169
|
+
if (below)
|
|
170
|
+
moveX = dir;
|
|
171
|
+
}
|
|
172
|
+
if (p.x * dir > mem.bestX * dir + 0.3) {
|
|
173
|
+
mem.bestX = p.x;
|
|
174
|
+
mem.since = clock;
|
|
175
|
+
}
|
|
176
|
+
return { seq: 0, tick: clock, moveX, moveZ: 0, yaw: 0, buttons };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Search for a run from the start to the flag (the gate's softlock check:
|
|
180
|
+
* every level must be finishable). Best-first over short macro moves (run,
|
|
181
|
+
* jump of three heights, dash, wait) on a one-player copy of the sim, keyed
|
|
182
|
+
* by position cells so it never re-explores. Moving platforms run on the
|
|
183
|
+
* real clock from `startTick`.
|
|
184
|
+
*/
|
|
185
|
+
export function planCourse(level, opts) {
|
|
186
|
+
const rate = opts.tickRate;
|
|
187
|
+
const w = createPlatformWorld({ tickRate: rate, levels: [level], ...(opts.tuning ? { tuning: opts.tuning } : {}), ...(opts.statSteps ? { statSteps: opts.statSteps } : {}) });
|
|
188
|
+
// From a given state (a climber mid-course), or from the start at `startTick`.
|
|
189
|
+
const p0 = opts.from ? applyPlatformPlayer(w, { ...opts.from, id: 1, cosmetic: opts.from.cosmetic & 0xff }) : addPlatformPlayer(w, 1);
|
|
190
|
+
if (!opts.from)
|
|
191
|
+
w.tick = opts.startTick ?? 0;
|
|
192
|
+
p0.flags &= ~(PlatFlag.Frozen | PlatFlag.Bot);
|
|
193
|
+
const seg = Math.max(4, Math.round(rate / 7.5));
|
|
194
|
+
const mk = (moveX, buttons) => ({ seq: 0, tick: 0, moveX, moveZ: 0, yaw: 0, buttons });
|
|
195
|
+
const macros = [];
|
|
196
|
+
for (const d of [1, -1]) {
|
|
197
|
+
macros.push(Array.from({ length: seg }, () => mk(d, 0)));
|
|
198
|
+
for (const hold of [3, Math.round(rate * 0.16), Math.round(rate * 0.5)])
|
|
199
|
+
macros.push(Array.from({ length: Math.max(seg, hold + 2) }, (_, i) => mk(d, i < hold ? Button.Jump : 0)));
|
|
200
|
+
macros.push(Array.from({ length: seg }, (_, i) => mk(d, i === 0 ? Button.Sprint : 0)));
|
|
201
|
+
macros.push(Array.from({ length: seg }, (_, i) => mk(d, i === 1 ? Button.Jump : 0)));
|
|
202
|
+
}
|
|
203
|
+
macros.push(Array.from({ length: seg }, () => mk(0, 0)));
|
|
204
|
+
macros.push(Array.from({ length: seg * 3 }, () => mk(0, 0)));
|
|
205
|
+
const key = (p) => `${Math.round(p.x * 2.5)},${Math.round(p.y * 2.5)},${p.flags & PlatFlag.Grounded ? 1 : 0},${p.vy > 0 ? 1 : 0},${Math.floor(w.tick / Math.max(1, Math.round(rate * 0.5))) % 64}`;
|
|
206
|
+
const seen = new Set();
|
|
207
|
+
const open = [{ s: capturePlatformPlayer(w, p0), tape: [], score: p0.x }];
|
|
208
|
+
let best = p0.x;
|
|
209
|
+
let expanded = 0;
|
|
210
|
+
const budget = opts.budget ?? 25_000;
|
|
211
|
+
const span = Math.max(1, level.flag[0] - level.start[0]);
|
|
212
|
+
while (open.length > 0 && expanded < budget) {
|
|
213
|
+
// Pop the most promising (furthest right, then shortest).
|
|
214
|
+
let bi = 0;
|
|
215
|
+
for (let i = 1; i < open.length; i++)
|
|
216
|
+
if (open[i].score > open[bi].score)
|
|
217
|
+
bi = i;
|
|
218
|
+
const node = open[bi];
|
|
219
|
+
open[bi] = open[open.length - 1];
|
|
220
|
+
open.pop();
|
|
221
|
+
expanded++;
|
|
222
|
+
for (const mac of macros) {
|
|
223
|
+
const p = applyPlatformPlayer(w, node.s);
|
|
224
|
+
let fell = false;
|
|
225
|
+
let done = false;
|
|
226
|
+
const tape = node.tape.concat(mac);
|
|
227
|
+
for (const c of mac) {
|
|
228
|
+
const ev = platformStep(w, new Map([[1, c]]));
|
|
229
|
+
if (ev.some((e) => e.type === "fell"))
|
|
230
|
+
fell = true;
|
|
231
|
+
if (p.flags & PlatFlag.Finished) {
|
|
232
|
+
done = true;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (fell)
|
|
237
|
+
continue;
|
|
238
|
+
if (done)
|
|
239
|
+
return { found: true, seconds: tape.length / rate, bestX: level.flag[0], expanded, tape };
|
|
240
|
+
const k = key(p);
|
|
241
|
+
if (seen.has(k))
|
|
242
|
+
continue;
|
|
243
|
+
seen.add(k);
|
|
244
|
+
best = Math.max(best, p.x);
|
|
245
|
+
// Progress first; a little weight on staying high (ledges lead up).
|
|
246
|
+
open.push({ s: capturePlatformPlayer(w, p), tape, score: p.x + p.y * 0.15 - tape.length * (span / (rate * 600)) });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { found: false, seconds: 0, bestX: best, expanded, tape: [] };
|
|
250
|
+
}
|
|
251
|
+
// ---------------------------------------------------------------- core
|
|
252
|
+
/** Planned runs by starting state (course, tick, body), shared by every room of a process (courses never change). */
|
|
253
|
+
const ROUTES = new Map();
|
|
254
|
+
const ROUTE_CACHE_MAX = 256;
|
|
255
|
+
/** Courses the planner found no route through from the start: bots there run on reflexes, not routes. */
|
|
256
|
+
const UNROUTABLE = new WeakSet();
|
|
257
|
+
/** True when routeFor found no way from this course's start to its flag. */
|
|
258
|
+
export function unroutable(level) {
|
|
259
|
+
return UNROUTABLE.has(level);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* A planned run from a climber's exact state (cached), or null when the
|
|
263
|
+
* planner found none. Without `from`, from the start at `startTick`.
|
|
264
|
+
*/
|
|
265
|
+
export function routeFor(level, tickRate, tuning, startTick, from) {
|
|
266
|
+
const body = from ? { ...from, id: 0, cosmetic: 0, flags: from.flags & ~PlatFlag.Bot, clock: startTick } : null;
|
|
267
|
+
const key = JSON.stringify([level.start, level.flag, level.solids.length, level.movers, level.solids[0], tickRate, tuning, startTick, body]);
|
|
268
|
+
const hit = ROUTES.get(key);
|
|
269
|
+
if (hit !== undefined)
|
|
270
|
+
return hit;
|
|
271
|
+
if (UNROUTABLE.has(level))
|
|
272
|
+
return null;
|
|
273
|
+
const r = planCourse(level, { tickRate, tuning, ...(body ? { from: body } : { startTick }) });
|
|
274
|
+
const tape = r.found ? r.tape : null;
|
|
275
|
+
if (!tape && !body)
|
|
276
|
+
UNROUTABLE.add(level);
|
|
277
|
+
if (ROUTES.size >= ROUTE_CACHE_MAX)
|
|
278
|
+
ROUTES.delete(ROUTES.keys().next().value);
|
|
279
|
+
ROUTES.set(key, tape);
|
|
280
|
+
return tape;
|
|
281
|
+
}
|
|
282
|
+
/** Seconds a bot of this skill waits after the start signal before running its route (quarter seconds). */
|
|
283
|
+
export function botDelaySeconds(id, skill) {
|
|
284
|
+
return Math.round(((1 - Math.max(0, Math.min(1, skill))) * 3 + (id % 4) * 0.25) * 4) / 4;
|
|
285
|
+
}
|
|
286
|
+
/** The platform sim as the race runner drives it. */
|
|
287
|
+
export class PlatformCore {
|
|
288
|
+
compiled;
|
|
289
|
+
tickRate;
|
|
290
|
+
world;
|
|
291
|
+
memory = new Map();
|
|
292
|
+
/** World tick of this round's start signal (-1 while frozen). */
|
|
293
|
+
startTick = -1;
|
|
294
|
+
/** Players present at the start signal: they may run a planned route. */
|
|
295
|
+
starters = new Set();
|
|
296
|
+
constructor(compiled, tickRate) {
|
|
297
|
+
this.compiled = compiled;
|
|
298
|
+
this.tickRate = tickRate;
|
|
299
|
+
this.world = createPlatformWorld({ tickRate, levels: compiled.levels, tuning: compiled.tuning, statSteps: compiled.statSteps });
|
|
300
|
+
}
|
|
301
|
+
ids() {
|
|
302
|
+
return [...this.world.players.keys()].sort((a, b) => a - b);
|
|
303
|
+
}
|
|
304
|
+
add(id, bot, frozen) {
|
|
305
|
+
if (this.world.players.has(id))
|
|
306
|
+
return;
|
|
307
|
+
const p = addPlatformPlayer(this.world, id, { bot, frozen });
|
|
308
|
+
resetPlatformPlayer(this.world, p, frozen);
|
|
309
|
+
this.memory.set(id, { bestX: p.x, since: 0, holdJump: 0 });
|
|
310
|
+
}
|
|
311
|
+
remove(id) {
|
|
312
|
+
removePlatformPlayer(this.world, id);
|
|
313
|
+
this.memory.delete(id);
|
|
314
|
+
this.starters.delete(id);
|
|
315
|
+
}
|
|
316
|
+
reset(course, frozen) {
|
|
317
|
+
setPlatformLevel(this.world, course);
|
|
318
|
+
this.world.tick = 0;
|
|
319
|
+
for (const p of this.world.players.values()) {
|
|
320
|
+
p.stats = 0;
|
|
321
|
+
resetPlatformPlayer(this.world, p, frozen);
|
|
322
|
+
}
|
|
323
|
+
for (const m of this.memory.values()) {
|
|
324
|
+
m.bestX = this.world.level.start[0];
|
|
325
|
+
m.since = 0;
|
|
326
|
+
m.holdJump = 0;
|
|
327
|
+
delete m.tape;
|
|
328
|
+
delete m.tapeAt;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
setFrozen(frozen) {
|
|
332
|
+
setPlatformFrozen(this.world, frozen);
|
|
333
|
+
this.starters.clear();
|
|
334
|
+
this.startTick = frozen ? -1 : this.world.tick;
|
|
335
|
+
if (!frozen)
|
|
336
|
+
for (const id of this.world.players.keys())
|
|
337
|
+
this.starters.add(id);
|
|
338
|
+
}
|
|
339
|
+
finished(id) {
|
|
340
|
+
return ((this.world.players.get(id)?.flags ?? 0) & PlatFlag.Finished) !== 0;
|
|
341
|
+
}
|
|
342
|
+
progress(id) {
|
|
343
|
+
const p = this.world.players.get(id);
|
|
344
|
+
return p ? platformProgress(this.world, p) : 0;
|
|
345
|
+
}
|
|
346
|
+
position(id) {
|
|
347
|
+
const p = this.world.players.get(id);
|
|
348
|
+
return p ? [p.x, p.y, 0] : [0, 0, 0];
|
|
349
|
+
}
|
|
350
|
+
step(inputs) {
|
|
351
|
+
const out = [];
|
|
352
|
+
for (const e of platformStep(this.world, inputs)) {
|
|
353
|
+
if (e.type === "coin")
|
|
354
|
+
out.push({ name: "coin.collected", player: e.player });
|
|
355
|
+
else if (e.type === "checkpoint")
|
|
356
|
+
out.push({ name: "checkpoint.reached", player: e.player });
|
|
357
|
+
else if (e.type === "finish")
|
|
358
|
+
out.push({ name: "player.finished", player: e.player });
|
|
359
|
+
else if (e.type === "fell")
|
|
360
|
+
out.push({ name: "player.fell", player: e.player });
|
|
361
|
+
else if (e.type === "spring")
|
|
362
|
+
out.push({ name: "spring.bounced", player: e.player });
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
botInput(id, clock, skill) {
|
|
367
|
+
const p = this.world.players.get(id);
|
|
368
|
+
let mem = this.memory.get(id);
|
|
369
|
+
if (!mem) {
|
|
370
|
+
mem = { bestX: p?.x ?? 0, since: clock, holdJump: 0 };
|
|
371
|
+
this.memory.set(id, mem);
|
|
372
|
+
}
|
|
373
|
+
const idle = { seq: 0, tick: clock, moveX: 0, moveZ: 0, yaw: 0, buttons: 0 };
|
|
374
|
+
if (!p)
|
|
375
|
+
return idle;
|
|
376
|
+
if ((p.flags & PlatFlag.Frozen) !== 0)
|
|
377
|
+
mem.since = clock;
|
|
378
|
+
// A bot that heard the start signal runs a planned route, after a wait its skill sets (looser bots start later).
|
|
379
|
+
// The route is planned from the bot's exact state when it sets off (cached: every bot that waited the same
|
|
380
|
+
// time stands in the same state), so playing it back is exact.
|
|
381
|
+
if (this.startTick >= 0 && this.starters.has(id) && (p.flags & PlatFlag.Frozen) === 0) {
|
|
382
|
+
const wait = Math.round(botDelaySeconds(id, skill) * this.tickRate);
|
|
383
|
+
const at = this.world.tick - (this.startTick + wait);
|
|
384
|
+
if (at < 0)
|
|
385
|
+
return idle;
|
|
386
|
+
if (at === 0 || mem.tapeAt === undefined) {
|
|
387
|
+
mem.tape = routeFor(this.world.level, this.tickRate, this.compiled.tuning, this.world.tick, capturePlatformPlayer(this.world, p));
|
|
388
|
+
mem.tapeAt = this.world.tick;
|
|
389
|
+
}
|
|
390
|
+
if (mem.tape) {
|
|
391
|
+
const c = mem.tape[this.world.tick - (mem.tapeAt ?? 0)];
|
|
392
|
+
return c ? { ...c, tick: clock } : idle;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// A seat bot stuck for 5 s skips ahead to the next checkpoint (bots never earn; a stuck bot is no race).
|
|
396
|
+
if (id >= BOT_PLAYER_BASE && clock - mem.since > 5 * this.tickRate && (p.flags & (PlatFlag.Finished | PlatFlag.Dead)) === 0) {
|
|
397
|
+
const next = this.world.level.checkpoints[p.cp + 1];
|
|
398
|
+
if (next) {
|
|
399
|
+
p.x = next[0];
|
|
400
|
+
p.y = next[1];
|
|
401
|
+
p.vx = p.vy = 0;
|
|
402
|
+
}
|
|
403
|
+
mem.since = clock;
|
|
404
|
+
mem.bestX = p.x;
|
|
405
|
+
}
|
|
406
|
+
return platformBotInput(this.world, p, mem, clock, skill);
|
|
407
|
+
}
|
|
408
|
+
grant(id, upgrade) {
|
|
409
|
+
const stat = this.compiled.upgradeStat[upgrade];
|
|
410
|
+
return stat ? grantPlatformStat(this.world, id, stat) : false;
|
|
411
|
+
}
|
|
412
|
+
setCosmetic(id, cosmetic) {
|
|
413
|
+
const p = this.world.players.get(id);
|
|
414
|
+
if (p)
|
|
415
|
+
p.cosmetic = cosmetic & 0xff;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// ---------------------------------------------------------------- netcode
|
|
419
|
+
/** Field indices of the platformer wire schema. */
|
|
420
|
+
export const PF = { x: 0, y: 1, vx: 2, vy: 3, flags: 4, timers: 5, timers2: 6, cp: 7, coinsLo: 8, coinsHi: 9, finishTick: 10, stats: 11, cosmetic: 12, clock: 13 };
|
|
421
|
+
export const platformSchema = {
|
|
422
|
+
fields: [
|
|
423
|
+
{ name: "x", kind: "int", bits: 21, deltaBits: 12 },
|
|
424
|
+
{ name: "y", kind: "int", bits: 19, deltaBits: 12 },
|
|
425
|
+
{ name: "vx", kind: "int", bits: 17, deltaBits: 12 },
|
|
426
|
+
{ name: "vy", kind: "int", bits: 17, deltaBits: 12 },
|
|
427
|
+
{ name: "flags", kind: "uint", bits: 16, interp: "step" },
|
|
428
|
+
{ name: "timers", kind: "uint", bits: 32, interp: "step" },
|
|
429
|
+
{ name: "timers2", kind: "uint", bits: 32, interp: "step" },
|
|
430
|
+
{ name: "cp", kind: "uint", bits: 5, interp: "step" },
|
|
431
|
+
{ name: "coinsLo", kind: "uint", bits: 31, interp: "step" },
|
|
432
|
+
{ name: "coinsHi", kind: "uint", bits: 31, interp: "step" },
|
|
433
|
+
{ name: "finishTick", kind: "uint", bits: 20, interp: "step" },
|
|
434
|
+
{ name: "stats", kind: "uint", bits: 20, interp: "step" },
|
|
435
|
+
{ name: "cosmetic", kind: "uint", bits: 16, interp: "step" },
|
|
436
|
+
{ name: "clock", kind: "uint", bits: 20, interp: "step", deltaBits: 6 },
|
|
437
|
+
],
|
|
438
|
+
xField: PF.x,
|
|
439
|
+
yField: PF.y,
|
|
440
|
+
unitsPerWorld: 1024,
|
|
441
|
+
};
|
|
442
|
+
/** The engine InputCommand on the wire (moves int8, yaw u16, buttons u16), shared by the race templates. */
|
|
443
|
+
export const raceInputCodec = {
|
|
444
|
+
idle: { seq: 0, tick: 0, moveX: 0, moveZ: 0, yaw: 0, buttons: 0 },
|
|
445
|
+
write(w, i) {
|
|
446
|
+
const axis = (v) => (Number.isFinite(v) ? Math.round(Math.max(-1, Math.min(1, v)) * 127) : 0);
|
|
447
|
+
w.writeSigned(axis(i.moveX), 8);
|
|
448
|
+
w.writeSigned(axis(i.moveZ), 8);
|
|
449
|
+
const TAU = Math.PI * 2;
|
|
450
|
+
w.writeBits(Number.isFinite(i.yaw) ? Math.round(((((i.yaw % TAU) + TAU) % TAU) / TAU) * 65536) & 0xffff : 0, 16);
|
|
451
|
+
w.writeBits(i.buttons & 0xffff, 16);
|
|
452
|
+
},
|
|
453
|
+
read(r) {
|
|
454
|
+
const moveX = r.readSigned(8) / 127;
|
|
455
|
+
const moveZ = r.readSigned(8) / 127;
|
|
456
|
+
const yaw = (r.readBits(16) / 65536) * Math.PI * 2;
|
|
457
|
+
return { seq: 0, tick: 0, moveX, moveZ, yaw, buttons: r.readBits(16) };
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
function toValues(s) {
|
|
461
|
+
return [s.x, s.y, s.vx, s.vy, s.flags, s.timers, s.timers2, s.cp, s.coinsLo, s.coinsHi, s.finishTick, s.stats, s.cosmetic, s.clock];
|
|
462
|
+
}
|
|
463
|
+
function fromValues(e) {
|
|
464
|
+
const v = (i) => Math.round(e.values[i] ?? 0);
|
|
465
|
+
return {
|
|
466
|
+
id: e.id,
|
|
467
|
+
x: v(PF.x),
|
|
468
|
+
y: v(PF.y),
|
|
469
|
+
vx: v(PF.vx),
|
|
470
|
+
vy: v(PF.vy),
|
|
471
|
+
flags: v(PF.flags),
|
|
472
|
+
timers: v(PF.timers) >>> 0,
|
|
473
|
+
timers2: v(PF.timers2) >>> 0,
|
|
474
|
+
cp: v(PF.cp),
|
|
475
|
+
coinsLo: v(PF.coinsLo) >>> 0,
|
|
476
|
+
coinsHi: v(PF.coinsHi) >>> 0,
|
|
477
|
+
finishTick: v(PF.finishTick),
|
|
478
|
+
stats: v(PF.stats) >>> 0,
|
|
479
|
+
cosmetic: v(PF.cosmetic),
|
|
480
|
+
clock: v(PF.clock),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/** The platformer's SimAdapter: the room runs the race; a client predicts only its own runner. */
|
|
484
|
+
export function platformAdapter(o) {
|
|
485
|
+
const upgradeIds = [...o.manifest.upgrades.keys()];
|
|
486
|
+
const gear = new Set([...o.manifest.skus.values()].flatMap((s) => (s.grants && "gear" in s.grants ? [s.grants.gear] : [])));
|
|
487
|
+
return {
|
|
488
|
+
name: o.name,
|
|
489
|
+
version: o.version,
|
|
490
|
+
tickHz: o.tickRate,
|
|
491
|
+
schema: platformSchema,
|
|
492
|
+
input: raceInputCodec,
|
|
493
|
+
maxInputsPerTick: 1,
|
|
494
|
+
createWorld(seed, role) {
|
|
495
|
+
const core = new PlatformCore(o.compiled, o.tickRate);
|
|
496
|
+
const runner = role === "server" ? new RaceRunner(o.manifest, core, { bots: o.bots, roundSeconds: o.roundSeconds, courses: o.compiled.levels.length, seed, ...o.runner }) : null;
|
|
497
|
+
return { role, core, runner };
|
|
498
|
+
},
|
|
499
|
+
addPlayer(w, player) {
|
|
500
|
+
if (w.runner)
|
|
501
|
+
w.runner.joined(player);
|
|
502
|
+
else
|
|
503
|
+
w.core.add(player, false, false);
|
|
504
|
+
return player;
|
|
505
|
+
},
|
|
506
|
+
removePlayer(w, player) {
|
|
507
|
+
if (w.runner)
|
|
508
|
+
w.runner.left(player);
|
|
509
|
+
else
|
|
510
|
+
w.core.remove(player);
|
|
511
|
+
},
|
|
512
|
+
step(w, inputsByPlayer) {
|
|
513
|
+
const inputs = new Map();
|
|
514
|
+
for (const [p, list] of inputsByPlayer) {
|
|
515
|
+
const c = list[0];
|
|
516
|
+
if (c)
|
|
517
|
+
inputs.set(p, c);
|
|
518
|
+
}
|
|
519
|
+
if (!w.runner) {
|
|
520
|
+
w.core.step(inputs);
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
w.runner.beforeStep(inputs);
|
|
524
|
+
const r = w.runner.afterStep(w.core.step(inputs));
|
|
525
|
+
const out = [];
|
|
526
|
+
for (const a of r.awards)
|
|
527
|
+
out.push({ type: "earn", player: a.player, rule: a.rule, units: a.units });
|
|
528
|
+
for (const p of w.runner.takeProgress())
|
|
529
|
+
out.push({ type: "progress", player: p.player, kind: p.kind, id: p.id });
|
|
530
|
+
if (r.reset)
|
|
531
|
+
out.push({ type: "run.reset" });
|
|
532
|
+
return out;
|
|
533
|
+
},
|
|
534
|
+
snapshot(w) {
|
|
535
|
+
return w.core.ids().map((id) => {
|
|
536
|
+
const p = w.core.world.players.get(id);
|
|
537
|
+
return { id, kind: 1, owner: id, values: toValues(capturePlatformPlayer(w.core.world, p)) };
|
|
538
|
+
});
|
|
539
|
+
},
|
|
540
|
+
applySnapshot(w, entities) {
|
|
541
|
+
for (const e of entities)
|
|
542
|
+
applyPlatformPlayer(w.core.world, fromValues(e));
|
|
543
|
+
},
|
|
544
|
+
info(w) {
|
|
545
|
+
return w.runner ? { ...w.runner.info(), level: o.compiled.names[w.runner.courseNow] ?? null } : null;
|
|
546
|
+
},
|
|
547
|
+
startRun(w, player) {
|
|
548
|
+
w.runner?.startRun(player);
|
|
549
|
+
},
|
|
550
|
+
command(w, player, cmd) {
|
|
551
|
+
if (cmd.name === "cosmetic") {
|
|
552
|
+
w.core.setCosmetic(player, cmd.value ?? 0);
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
const up = upgradeIds.indexOf(cmd.name);
|
|
556
|
+
if (up < 0)
|
|
557
|
+
return false;
|
|
558
|
+
// A store SKU named after an upgrade, or gear the room issues at a staked run's start.
|
|
559
|
+
if (!o.manifest.skus.has(cmd.name) && !gear.has(cmd.name))
|
|
560
|
+
return false;
|
|
561
|
+
const stat = o.compiled.upgradeStat[up];
|
|
562
|
+
const def = o.manifest.upgrades.get(cmd.name);
|
|
563
|
+
const p = w.core.world.players.get(player);
|
|
564
|
+
if (!stat || !def || !p)
|
|
565
|
+
return false;
|
|
566
|
+
// One level per SKU a round, up to the upgrade's maxLevel.
|
|
567
|
+
const lvl = (p.stats >>> (PLAT_STATS.indexOf(stat) * 4)) & 0xf;
|
|
568
|
+
if (lvl >= def.maxLevel)
|
|
569
|
+
return false;
|
|
570
|
+
return w.core.grant(player, up);
|
|
571
|
+
},
|
|
572
|
+
disposeWorld() { },
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/** Throw the collected compile issues, agent-readable. */
|
|
576
|
+
export function assertNoIssues(issues) {
|
|
577
|
+
if (issues.length > 0)
|
|
578
|
+
throw new GameDefinitionError("game module", issues);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* A playing autopilot for a client (measurement, previews, load tests): from
|
|
582
|
+
* the newest predicted state of our own climber it plans a route to the flag
|
|
583
|
+
* (cached by state), plays it back tick for tick on the course clock, and
|
|
584
|
+
* replans after a fall, a new course or the end of the tape. Falls back to
|
|
585
|
+
* the course bot's reflexes while no route is known.
|
|
586
|
+
*/
|
|
587
|
+
export function platformAutopilot(compiled, tickRate) {
|
|
588
|
+
const core = new PlatformCore(compiled, tickRate);
|
|
589
|
+
let tape = null;
|
|
590
|
+
let lastPlan = -1e9;
|
|
591
|
+
return (self, course, tick) => {
|
|
592
|
+
const idle = { seq: 0, tick, moveX: 0, moveZ: 0, yaw: 0, buttons: 0 };
|
|
593
|
+
if (!self)
|
|
594
|
+
return idle;
|
|
595
|
+
const s = fromValues(self);
|
|
596
|
+
if (s.flags & (PlatFlag.Frozen | PlatFlag.Finished | PlatFlag.Dead)) {
|
|
597
|
+
if (s.flags & PlatFlag.Dead)
|
|
598
|
+
tape = null;
|
|
599
|
+
return idle;
|
|
600
|
+
}
|
|
601
|
+
if (tape && (tape.course !== course || s.clock - tape.start >= tape.inputs.length || s.clock < tape.start))
|
|
602
|
+
tape = null;
|
|
603
|
+
const level = compiled.levels[course];
|
|
604
|
+
if (!tape && level && tick - lastPlan > tickRate * 1.5) {
|
|
605
|
+
lastPlan = tick;
|
|
606
|
+
const r = routeFor(level, tickRate, compiled.tuning, s.clock, s);
|
|
607
|
+
if (r)
|
|
608
|
+
tape = { inputs: r, start: s.clock, course };
|
|
609
|
+
}
|
|
610
|
+
if (!tape) {
|
|
611
|
+
const p = applyPlatformPlayer(core.world, s);
|
|
612
|
+
if (core.world.levelIndex !== course)
|
|
613
|
+
setPlatformLevel(core.world, course);
|
|
614
|
+
return platformBotInput(core.world, p, { bestX: p.x, since: tick, holdJump: 0 }, tick, 1);
|
|
615
|
+
}
|
|
616
|
+
const c = tape.inputs[s.clock - tape.start];
|
|
617
|
+
return c ? { ...c, tick } : idle;
|
|
618
|
+
};
|
|
619
|
+
}
|