@homie-rocks/studio 0.46.6 → 0.48.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/CHANGELOG.md +83 -1
- package/guides/game/REWRITE.md +2 -1
- package/guides/game/RULES.md +207 -0
- package/guides/game/SKILL.md +24 -0
- package/lib/doctor.mjs +8 -6
- package/lib/mcp-tools.mjs +2 -1
- package/lib/physics-mesh.mjs +40 -0
- package/lib/provider-key.mjs +21 -0
- package/lib/rules-build.mjs +18 -10
- package/lib/rules-check-worker.mjs +5 -1
- package/lib/rules-check.mjs +1 -1
- package/lib/rules-guard.mjs +5 -4
- package/lib/template-history.json +84 -0
- package/package.json +11 -2
- package/rules/collision.ts +1 -0
- package/rules/core.ts +94 -34
- package/rules/host.ts +3 -1
- package/rules/interest.mjs +29 -14
- package/rules/live.ts +4 -3
- package/rules/map-index.ts +24 -0
- package/rules/map-state.ts +54 -0
- package/rules/math.ts +18 -6
- package/rules/nav/Flow.ts +53 -0
- package/rules/pack.ts +4 -2
- package/rules/physics-engine.ts +73 -0
- package/rules/physics.ts +26 -0
- package/rules/query.ts +19 -4
- package/rules/rules.ts +31 -4
- package/rules/substeps.ts +39 -0
- package/rules/tiles.ts +58 -0
- package/rules/topology.mjs +22 -0
- package/rules/types.ts +4 -3
- package/rules/view.ts +74 -23
- package/worker/build-id.mjs +1 -1
- package/worker/hosted.mjs +16 -0
- package/worker/index.mjs +19 -4
- package/worker/version.mjs +1 -1
package/lib/rules-guard.mjs
CHANGED
|
@@ -66,12 +66,13 @@ export const REFUSED_NAMES = ['constructor', 'prototype', '__proto__', 'stack',
|
|
|
66
66
|
/** The names JavaScript calls by itself to turn an object into a number or a text: no function may sit under one. */
|
|
67
67
|
export const HOOK_NAMES = ['valueOf', 'toString', 'toJSON'];
|
|
68
68
|
/** What the host runtime's own objects offer: `world`, `ctx`, the round, the map (rules/core.ts) and `world.math` (rules/math.ts). */
|
|
69
|
-
export const WORLD_METHODS = ['ticks', 'random', 'route', 'label', 'send', 'dispatch', 'sendRoom', 'announce', 'sendArea', 'after', 'emit', 'spawn', 'despawn', 'place', 'near', 'inBox', 'ray', 'rayAll', 'rays', 'sweep', 'ask', 'goalDone', 'finish', 'end', 'restart', 'spot', 'spots'];
|
|
69
|
+
export const WORLD_METHODS = ['ticks', 'random', 'route', 'label', 'send', 'dispatch', 'sendRoom', 'announce', 'sendArea', 'after', 'emit', 'spawn', 'despawn', 'place', 'near', 'inBox', 'ray', 'rayAll', 'rays', 'sweep', 'ask', 'goalDone', 'finish', 'end', 'restart', 'spot', 'spots', 'replace', 'tile', 'distance', 'flow'];
|
|
70
70
|
export const MATH_METHODS = ['sin', 'cos', 'tan', 'atan', 'atan2', 'asin', 'acos', 'exp', 'log', 'pow', 'hypot', 'rad', 'deg', 'clamp', 'lerp', 'vec', 'add', 'sub', 'scale', 'dot', 'cross', 'len', 'dist', 'norm', 'clampLen', 'lerpVec', 'dir', 'angle'];
|
|
71
71
|
const VALUE_METHODS = new Set([...ARRAY_METHODS, ...STRING_METHODS, ...MAPSET_METHODS]);
|
|
72
|
-
export const MOVE_METHODS = ['support', 'overlaps'];
|
|
73
|
-
const
|
|
74
|
-
const
|
|
72
|
+
export const MOVE_METHODS = ['substeps', 'support', 'overlaps'];
|
|
73
|
+
export const PHYSICS_METHODS = ['step', 'cast', 'overlap'];
|
|
74
|
+
const NEW_METHODS = new Set(['step','cast','overlap','substeps','route','label','dispatch','rayAll','rays','restart','replace','tile','distance','flow']);
|
|
75
|
+
const CALLABLE = new Set([...VALUE_METHODS, ...WORLD_METHODS, ...MOVE_METHODS, ...PHYSICS_METHODS, ...MATH_METHODS]);
|
|
75
76
|
const refusedName = (k) => REFUSED_NAMES.includes(k) || k.startsWith('toLocale');
|
|
76
77
|
/** Operators that turn each operand into a number or a text. (`+` is handled beside them; `==` and `!=` are refused.) */
|
|
77
78
|
const CONVERTING = new Set(['-', '*', '/', '%', '&', '|', '^', '<<', '>>', '>>>', '<', '>', '<=', '>=']);
|
|
@@ -2901,6 +2901,90 @@
|
|
|
2901
2901
|
}
|
|
2902
2902
|
}
|
|
2903
2903
|
}
|
|
2904
|
+
},
|
|
2905
|
+
"0.47.0": {
|
|
2906
|
+
"files": {
|
|
2907
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
2908
|
+
"README.md": "684b3cca8001673c",
|
|
2909
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
2910
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
2911
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
2912
|
+
"videos/README.md": "16babc3437d5545b",
|
|
2913
|
+
"posts/README.md": "37841aae476bd540",
|
|
2914
|
+
"site/README.md": "732894273a32dc3f",
|
|
2915
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
2916
|
+
},
|
|
2917
|
+
"sections": {
|
|
2918
|
+
"AGENTS.md": {
|
|
2919
|
+
"(intro)": "394cbbe934e80722",
|
|
2920
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
2921
|
+
"## Layout": "f3a280b8c96cd068",
|
|
2922
|
+
"## Apps: one screen, roles and parts": "8596f2b25edda3e2",
|
|
2923
|
+
"## Commands (all through the pinned CLI in node_modules)": "5222e08ca55e326e",
|
|
2924
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
2925
|
+
"## Making games": "bb715272fd1a615b",
|
|
2926
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
2927
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
2928
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
2929
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
2930
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
2931
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
2932
|
+
"## Rules": "27881d11d3ff3803",
|
|
2933
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
2934
|
+
"## Beta": "9d02638207a26e73"
|
|
2935
|
+
}
|
|
2936
|
+
},
|
|
2937
|
+
"named": {
|
|
2938
|
+
"Homie Arcade": {
|
|
2939
|
+
"sections": {
|
|
2940
|
+
"AGENTS.md": {
|
|
2941
|
+
"## Commands (all through the pinned CLI in node_modules)": "611b437f5b3303f0"
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
},
|
|
2947
|
+
"0.48.0": {
|
|
2948
|
+
"files": {
|
|
2949
|
+
"CLAUDE.md": "ddf2b2a0376202f6",
|
|
2950
|
+
"README.md": "684b3cca8001673c",
|
|
2951
|
+
"HANDOFF.md": "29337e2ace95b2e7",
|
|
2952
|
+
"games/README.md": "5ad32fe80c61f117",
|
|
2953
|
+
"music/README.md": "a3dc042dadd39df8",
|
|
2954
|
+
"videos/README.md": "16babc3437d5545b",
|
|
2955
|
+
"posts/README.md": "37841aae476bd540",
|
|
2956
|
+
"site/README.md": "732894273a32dc3f",
|
|
2957
|
+
"site/src/worker.mjs": "6cc5c6131eaf6fd0"
|
|
2958
|
+
},
|
|
2959
|
+
"sections": {
|
|
2960
|
+
"AGENTS.md": {
|
|
2961
|
+
"(intro)": "394cbbe934e80722",
|
|
2962
|
+
"## Version-matched authoring guides": "f6261a0e5801b96b",
|
|
2963
|
+
"## Layout": "f3a280b8c96cd068",
|
|
2964
|
+
"## Apps: one screen, roles and parts": "8596f2b25edda3e2",
|
|
2965
|
+
"## Commands (all through the pinned CLI in node_modules)": "5222e08ca55e326e",
|
|
2966
|
+
"## The site": "0cb8dd9a6f8a0096",
|
|
2967
|
+
"## Making games": "bb715272fd1a615b",
|
|
2968
|
+
"## The Game Codex and progress": "0738ef719cef3a4c",
|
|
2969
|
+
"## Art direction and models": "a48fff7bf0a5f2fc",
|
|
2970
|
+
"## Running live games (the back office)": "51ebf2ab756f6e4d",
|
|
2971
|
+
"## Servers and AI seats": "1ccfd549d2369a60",
|
|
2972
|
+
"## Selling things (the shop)": "9e020d69dd6a954e",
|
|
2973
|
+
"## Continuing a build from the Claude app": "0d68081e924ca419",
|
|
2974
|
+
"## Rules": "27881d11d3ff3803",
|
|
2975
|
+
"## Telling Homie": "80b46fdd9e578db8",
|
|
2976
|
+
"## Beta": "9d02638207a26e73"
|
|
2977
|
+
}
|
|
2978
|
+
},
|
|
2979
|
+
"named": {
|
|
2980
|
+
"Homie Arcade": {
|
|
2981
|
+
"sections": {
|
|
2982
|
+
"AGENTS.md": {
|
|
2983
|
+
"## Commands (all through the pinned CLI in node_modules)": "611b437f5b3303f0"
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2904
2988
|
}
|
|
2905
2989
|
}
|
|
2906
2990
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homie-rocks/studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "A studio for games, music and video on its own Cloudflare: scaffold, multiplayer starters on the netplay contract, a port toolkit that makes existing single-player web games multiplayer, the site Worker with public rooms (Table/Lobby Durable Objects and D1, all on the free Workers plan, no payment method), a site with the hub's sections (Home, Games, Music, Videos, Rooms, Posts with feeds) in the studio's own look, an epic landing page for every game, big songs and videos in the studio's own R2 once it has storage (uploaded, checked by SHA-256, served at the same addresses), deploy, and the homie.rocks directory.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -92,7 +92,8 @@
|
|
|
92
92
|
"./functions": {
|
|
93
93
|
"types": "./functions/functions.ts",
|
|
94
94
|
"default": "./functions/functions.ts"
|
|
95
|
-
}
|
|
95
|
+
},
|
|
96
|
+
"./rules/physics-engine": "./rules/physics-engine.ts"
|
|
96
97
|
},
|
|
97
98
|
"files": [
|
|
98
99
|
"docs/",
|
|
@@ -165,5 +166,13 @@
|
|
|
165
166
|
"scripts": {
|
|
166
167
|
"test": "node --test --test-concurrency=2 --test-timeout=1800000 test/*.test.mjs",
|
|
167
168
|
"prepack": "node lib/build-identity.mjs --check && node ../../scripts/authoring-guides.mjs --check"
|
|
169
|
+
},
|
|
170
|
+
"peerDependencies": {
|
|
171
|
+
"@homie-rocks/physics": "0.1.2"
|
|
172
|
+
},
|
|
173
|
+
"peerDependenciesMeta": {
|
|
174
|
+
"@homie-rocks/physics": {
|
|
175
|
+
"optional": true
|
|
176
|
+
}
|
|
168
177
|
}
|
|
169
178
|
}
|
package/rules/collision.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface BodyShape { shape: string; radius: number; height: number; terr
|
|
|
11
11
|
export interface Solid { min: Vec3; max: Vec3; r: number }
|
|
12
12
|
export interface Hit3 { t: number; nx: number; ny: number; nz: number; id?: string }
|
|
13
13
|
export interface Map3 {
|
|
14
|
+
wrap?: import('./rules.ts').MapWrap;
|
|
14
15
|
staticMap?: Map3;
|
|
15
16
|
bounds: MapBox; boxes: readonly MapBox[]; circles: readonly MapCircle[];
|
|
16
17
|
heightTiles?: readonly MapHeightTile[];
|
package/rules/core.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { physicsWorld } from './physics.ts';
|
|
2
|
+
import { substeps } from './substeps.ts';
|
|
3
|
+
import { prepareMapReplacement, type MapRevision } from './map-state.ts';
|
|
4
|
+
import { tileApi } from './tiles.ts';
|
|
5
|
+
import { boundPosition, periodicDelta, relativePoint, blendPosition, wrapPoint } from './topology.mjs';
|
|
1
6
|
import {routeGraph} from './navigation.ts';
|
|
2
7
|
import { rayQuery, raySnapshot, type QueryTarget } from './query.ts';
|
|
3
8
|
import { colliderSolid, castCollider2, colliderRow, collisionMap, collisionQueries, type CollisionRevision } from './live.ts';
|
|
@@ -140,7 +145,9 @@ export interface SavedCore {
|
|
|
140
145
|
v: number; tick: number; epoch: number; rng: number; nextId: number; seq: number;
|
|
141
146
|
round: [number, number, number, number]; overAt: number; match: [number, number]; trips: number;
|
|
142
147
|
shared: unknown[]; policy: CorePolicy; intent?: [number, number, number?];
|
|
148
|
+
physics?: number[];
|
|
143
149
|
collision?: [number,number];
|
|
150
|
+
maps?: {current:MapRevision|null;pending:MapRevision|null};
|
|
144
151
|
history?: [number, [string, number, number, number, number][]][];
|
|
145
152
|
asks: PendingAsk[];
|
|
146
153
|
guideViews: [number, Record<string, unknown>][];
|
|
@@ -174,6 +181,7 @@ export interface Core {
|
|
|
174
181
|
/** What happened since the last call: rounds, effects, a changed roster, changed shared state, a new epoch, a failure. */
|
|
175
182
|
drain(): CoreOut[];
|
|
176
183
|
save(): SavedCore;
|
|
184
|
+
dispose(): void;
|
|
177
185
|
/** The `world` object rules are handed (a test walks it to prove nothing else is reachable). */
|
|
178
186
|
readonly world: object;
|
|
179
187
|
/**
|
|
@@ -191,6 +199,11 @@ export interface Core {
|
|
|
191
199
|
* `Adjusted`; also `effect`, `think` and `decision`): the handler, what was done, the field's name and what was written.
|
|
192
200
|
*/
|
|
193
201
|
export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number) => import('./rules.ts').Character | null | undefined; characterSaved?: (seat: number, character: import('./rules.ts').Character) => void; rewindTicks?: (seat: number)=>number; label?: (seat:number,driver:Driver)=>string; moved?: (kind: string, tick: number, input: Readonly<Record<string, unknown>>, before: import('./pack.ts').MoveBody, after: import('./pack.ts').MoveBody, geometry: import('./math.ts').MapShapes, id: string) => void; observe?: (kind: string, handler: string, error?: string) => void; noted?: (kind: string, handler: string, what: string, at: string, written: string) => void; seed?: number; epoch?: number; restore?: SavedCore | null; restoreEpoch?: number; stage?: string; decisions?: boolean } = {}): Core {
|
|
202
|
+
if (opts.restore && (c.map.physics !== undefined) !== (opts.restore.physics !== undefined)) throw new Error('physics: save and game differ');
|
|
203
|
+
const physics = physicsWorld(c.map.physics, opts.restore?.physics);
|
|
204
|
+
let currentMap=c.map;
|
|
205
|
+
let mapRevision:MapRevision|null=null,pendingMap:MapRevision|null=null;
|
|
206
|
+
let pendingGeometry:typeof currentMap|null=null,mapCells=0,currentMapCells=0,pendingMapCells=0;
|
|
194
207
|
const dims = c.dims;
|
|
195
208
|
const tickHz = c.settings.tickHz;
|
|
196
209
|
const dt = 1 / tickHz;
|
|
@@ -356,7 +369,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
356
369
|
naming(kind.name, 'fields'); const f = coerceFields(kind.fields, init.fields, dims);
|
|
357
370
|
naming(kind.name, 'motion'); const m = coerceFields(kind.motion, init.motion, dims); naming();
|
|
358
371
|
const e: Ent = {
|
|
359
|
-
id: `e${n.toString(36)}`, n, kind, pos: clampIn(V(at), kind.body?.radius ?? 0, kind.body ? kind.body.height || 2 * kind.body.radius : 0), vel: ZERO, heading: init.heading === undefined ? AHEAD : dir(init.heading, dims), grounded: dims === 2 || restsOnMap(
|
|
372
|
+
id: `e${n.toString(36)}`, n, kind, pos: clampIn(V(at), kind.body?.radius ?? 0, kind.body ? kind.body.height || 2 * kind.body.radius : 0), vel: ZERO, heading: init.heading === undefined ? AHEAD : dir(init.heading, dims), grounded: dims === 2 || restsOnMap(currentMap, clampIn(V(at), kind.body?.radius ?? 0, kind.body ? kind.body.height || 2 * kind.body.radius : 0), kind.body ?? { shape: 'sphere', radius: 0, height: 0 }),
|
|
360
373
|
r: 0, born: tick, arrived: false, why, dead: false,
|
|
361
374
|
f, m,
|
|
362
375
|
seat: player ? player.seat : -1, owner: player ? player.owner : '', driver: player ? player.driver : 'bot', away: false, goal: null,
|
|
@@ -366,10 +379,15 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
366
379
|
return e;
|
|
367
380
|
}
|
|
368
381
|
function clampIn(p: Vec3, r: number, height = 0): Vec3 {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
382
|
+
if(!currentMap.wrap){
|
|
383
|
+
const b=currentMap.bounds;
|
|
384
|
+
const x=Math.max(b.min.x+r,Math.min(b.max.x-r,p.x));
|
|
385
|
+
const y=Math.max(b.min.y+r,Math.min(b.max.y-r,p.y));
|
|
386
|
+
const z=dims===3?Math.max(b.min.z,Math.min(b.max.z-height,p.z)):0;
|
|
387
|
+
return x===p.x&&y===p.y&&z===p.z?p:V({x,y,z});
|
|
388
|
+
}
|
|
389
|
+
const at = boundPosition(p,currentMap,r,height,dims);
|
|
390
|
+
return at.x===p.x&&at.y===p.y&&at.z===p.z?p:V(at);
|
|
373
391
|
}
|
|
374
392
|
|
|
375
393
|
/* ---------------------------------------------------------------- running a handler under the budget */
|
|
@@ -470,9 +488,15 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
470
488
|
return k;
|
|
471
489
|
};
|
|
472
490
|
const mapApi = brand(Object.freeze({
|
|
473
|
-
name
|
|
474
|
-
|
|
475
|
-
|
|
491
|
+
get name(){return currentMap.name;},
|
|
492
|
+
...tileApi(()=>currentMap),
|
|
493
|
+
replace:(raw:unknown)=>{
|
|
494
|
+
need('room','world.map.replace()');
|
|
495
|
+
const prepared=prepareMapReplacement(raw,c.map);
|
|
496
|
+
pendingMap=Object.freeze([tick+1,prepared.raw]);pendingGeometry=prepared.map;pendingMapCells=prepared.cells;mapCells=currentMapCells+pendingMapCells;
|
|
497
|
+
},
|
|
498
|
+
spot: (name: unknown): Vec3 | undefined => (typeof name === 'string' && Object.hasOwn(currentMap.spots, name) ? currentMap.spots[name][0] : undefined),
|
|
499
|
+
spots: (name: unknown): readonly Vec3[] => (typeof name === 'string' && Object.hasOwn(currentMap.spots, name) ? currentMap.spots[name] : NO_SPOTS),
|
|
476
500
|
}));
|
|
477
501
|
let roundApi: Readonly<Record<string, unknown>> = Object.freeze({});
|
|
478
502
|
const refreshRound = (): void => {
|
|
@@ -512,13 +536,13 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
512
536
|
const row=e.kind.collider?colliderRow(e.id,e.pos,e.kind.body,e.kind.collider,e.f):null;
|
|
513
537
|
if(e.kind.collider&&!row)continue;
|
|
514
538
|
let at=e.pos;
|
|
515
|
-
if(!e.kind.collider && wanted<tick && before && after){charge(4*(before[1].length+after[1].length));const a=before[1].find(p=>p[0]===e.id&&p[4]===e.r),b=after[1].find(p=>p[0]===e.id&&p[4]===e.r);if(a&&b){const mix=after[0]===before[0]?0:Math.max(0,Math.min(1,(wanted-before[0])/(after[0]-before[0])));at={x:a[1]
|
|
539
|
+
if(!e.kind.collider && wanted<tick && before && after){charge(4*(before[1].length+after[1].length));const a=before[1].find(p=>p[0]===e.id&&p[4]===e.r),b=after[1].find(p=>p[0]===e.id&&p[4]===e.r);if(a&&b){const mix=after[0]===before[0]?0:Math.max(0,Math.min(1,(wanted-before[0])/(after[0]-before[0])));at=blendPosition({x:a[1],y:a[2],z:a[3]},{x:b[1],y:b[2],z:b[3]},mix,currentMap.wrap);}}
|
|
516
540
|
targets.push({id:e.id,kind:e.kind.name,query:e.kind.query,fields:e.f,geometry:Boolean(row),solid:row?colliderSolid(row):solidAt(at,e.kind.body),at});
|
|
517
541
|
}
|
|
518
542
|
return targets;
|
|
519
543
|
}
|
|
520
544
|
function cast3(p: Vec3, d: Vec3, shape: BodyShape, ignored: Set<string>): Hit3 | null {
|
|
521
|
-
let best = castMap3(
|
|
545
|
+
let best = castMap3(currentMap, p, d, shape);
|
|
522
546
|
const solid = solidAt(p, shape);
|
|
523
547
|
for (const other of ents.values()) {
|
|
524
548
|
if (!other.kind.body || ignored.has(other.id)) continue;
|
|
@@ -535,6 +559,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
535
559
|
tick: () => tick, dt: () => dt, round: () => roundApi, shared: sharedView, level: () => policy.level, levelMax: () => policy.levelMax, guideLevel: () => policy.guideLevel ?? policy.level, guideSeats: () => policy.guideSeats ?? EMPTY_GUIDE_SEATS, kids: () => Boolean(policy.kids), levelSet: () => Boolean(policy.levelSet),
|
|
536
560
|
// What the running handler may still spend (0.46.3): a handler that plans can stop short of its share instead of being stopped.
|
|
537
561
|
unitsLeft: () => Math.max(0, Math.min(2147483647, Math.floor(G.left))),
|
|
562
|
+
...(physics ? {physics: () => physics.api} : {}),
|
|
538
563
|
stage: () => stage, tune: () => c.tune, math: () => math, map: () => mapApi,
|
|
539
564
|
};
|
|
540
565
|
for (const [name, get] of Object.entries(getters)) Object.defineProperty(world, name, { get, enumerable: true });
|
|
@@ -568,8 +593,16 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
568
593
|
const parent = cx, parentTouched = touched, note = G.note;
|
|
569
594
|
cx = {scope:'ent',ent:e}; touched = []; dispatchDepth++;
|
|
570
595
|
if (noted) G.note = (what, at, written) => noted(e.kind.name, `on.${String(ev)}`, what, at, written);
|
|
596
|
+
let failure: unknown;
|
|
571
597
|
try { fn(world,e.self,payload); }
|
|
572
|
-
|
|
598
|
+
catch(error) { failure=error; throw error; }
|
|
599
|
+
finally {
|
|
600
|
+
try { settle(); } catch(error) { failure=error; throw error; }
|
|
601
|
+
finally {
|
|
602
|
+
cx=parent; touched=parentTouched; G.note=note; dispatchDepth--;
|
|
603
|
+
opts.observe?.(e.kind.name, `on.${String(ev)}`, failure===undefined?undefined:errorText(failure));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
573
606
|
return true;
|
|
574
607
|
},
|
|
575
608
|
sendRoom: (ev: unknown, data?: unknown): void => {
|
|
@@ -651,7 +684,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
651
684
|
e.pos = clampIn(V(at), e.kind.body?.radius ?? 0, e.kind.body ? e.kind.body.height || 2 * e.kind.body.radius : 0);
|
|
652
685
|
e.vel = vel !== undefined ? V(vel) : ZERO;
|
|
653
686
|
if (heading !== undefined) e.heading = dir(heading, dims);
|
|
654
|
-
e.grounded = dims === 2 || restsOnMap(
|
|
687
|
+
e.grounded = dims === 2 || restsOnMap(currentMap, e.pos, e.kind.body ?? { shape: 'sphere', radius: 0, height: 0 });
|
|
655
688
|
e.r = (e.r + 1) & 0xffff;
|
|
656
689
|
e.allow = 0;
|
|
657
690
|
},
|
|
@@ -662,8 +695,8 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
662
695
|
let cost = 0;
|
|
663
696
|
for (const e of ents.values()) {
|
|
664
697
|
if (k && e.kind !== k) continue;
|
|
665
|
-
const dx = e.pos.x
|
|
666
|
-
const dz = dims === 3 ? e.pos.z
|
|
698
|
+
const dx = periodicDelta(p.x,e.pos.x,currentMap.wrap?.x); const dy = periodicDelta(p.y,e.pos.y,currentMap.wrap?.y);
|
|
699
|
+
const dz = dims === 3 ? periodicDelta(p.z,e.pos.z,currentMap.wrap?.z) : 0;
|
|
667
700
|
const d2 = dx * dx + dy * dy + dz * dz;
|
|
668
701
|
if (d2 <= R * R) { found.push({ d: d2, e }); cost += viewCost.get(e.kind) as number; }
|
|
669
702
|
}
|
|
@@ -678,12 +711,13 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
678
711
|
if (max.x - min.x > 2 * REACH_M || max.y - min.y > 2 * REACH_M || dims === 3 && max.z - min.z > 2 * REACH_M) throw new Error(`world.inBox reaches ${REACH_M} m from its centre at most`);
|
|
679
712
|
const found: Ent[] = [];
|
|
680
713
|
let cost = 0;
|
|
681
|
-
|
|
714
|
+
const centre={x:(min.x+max.x)/2,y:(min.y+max.y)/2,z:(min.z+max.z)/2};
|
|
715
|
+
for (const e of ents.values()) {const p=relativePoint(e.pos,centre,currentMap.wrap);if ((!k || e.kind === k) && p.x >= min.x && p.x <= max.x && p.y >= min.y && p.y <= max.y && (dims === 2 || p.z >= min.z && p.z <= max.z)) { found.push(e); cost += viewCost.get(e.kind) as number; }}
|
|
682
716
|
charge(cost);
|
|
683
717
|
return Object.freeze(found.map(viewOf));
|
|
684
718
|
},
|
|
685
719
|
ray: (from: unknown, direction: unknown, max: unknown, options?: unknown): unknown => {
|
|
686
|
-
if (options !== undefined) return rayQuery(
|
|
720
|
+
if (options !== undefined || currentMap.wrap) return rayQuery(currentMap, rayTargets(options), dims, from, direction, max, options, cx.ent?.id)[0];
|
|
687
721
|
// Charged before the cast, for every shape it may test: the map's, and every entity in the room.
|
|
688
722
|
charge(20 + 4 * ents.size);
|
|
689
723
|
const p = V(from); const d = dir(direction, dims); const far = reach(max, 'world.ray');
|
|
@@ -692,7 +726,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
692
726
|
const hit = cast3(p, delta, { shape: 'sphere', radius: 0, height: 0 }, new Set(cx.ent ? [cx.ent.id] : []));
|
|
693
727
|
return hit ? Object.freeze({ ...(hit.id ? { entity: hit.id } : {}), at: V({ x: p.x + delta.x * hit.t, y: p.y + delta.y * hit.t, z: p.z + delta.z * hit.t }), normal: V({ x: hit.nx, y: hit.ny, z: hit.nz }), dist: far * hit.t }) : undefined;
|
|
694
728
|
}
|
|
695
|
-
const cast = castMap(
|
|
729
|
+
const cast = castMap(currentMap, p.x, p.y, d.x * far, d.y * far, 0);
|
|
696
730
|
let best: Hit | null = cast.hit;
|
|
697
731
|
for (const e of ents.values()) {
|
|
698
732
|
if (!e.kind.body || e === cx.ent) continue;
|
|
@@ -711,12 +745,19 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
711
745
|
const named=typeof graph==='string';if(named&&!Object.hasOwn(c.navigation,graph))throw new Error('unknown navigation graph');return routeGraph(named?c.navigation[graph]:graph,from,to,rayTargets({geometryOnly:true}).map(t=>t.solid),radius,height,named);
|
|
712
746
|
},
|
|
713
747
|
label: (id: unknown): string => {charge(20);const e=typeof id==='string'?ents.get(id):undefined;if(!e?.kind.player)return '';return String(opts.label?.(e.seat,e.driver)??`Player ${e.seat+1}`).slice(0,40);},
|
|
714
|
-
rays: (options?: unknown): unknown => raySnapshot(
|
|
715
|
-
rayAll: (from: unknown, direction: unknown, max: unknown, options?: unknown): unknown => rayQuery(
|
|
748
|
+
rays: (options?: unknown): unknown => raySnapshot(currentMap,rayTargets(options),dims,options,cx.ent?.id),
|
|
749
|
+
rayAll: (from: unknown, direction: unknown, max: unknown, options?: unknown): unknown => rayQuery(currentMap, rayTargets(options), dims, from, direction, max, options, cx.ent?.id, true),
|
|
716
750
|
sweep: (self: unknown, delta: unknown, o?: unknown): unknown => {
|
|
717
751
|
const e = mine(self, 'world.sweep');
|
|
718
752
|
charge(20 + 4 * ents.size);
|
|
719
753
|
const d = V(delta);
|
|
754
|
+
if(currentMap.wrap){
|
|
755
|
+
const list=own(o,'ignore'),ignore:string[]=[];
|
|
756
|
+
if(Array.isArray(list))for(let i=0;i<list.length&&i<IGNORE_MAX;i++){const id=own(list,i);if(typeof id==='string')ignore.push(id);}
|
|
757
|
+
const scene=raySnapshot(currentMap,rayTargets(undefined),dims,{radius:e.kind.body?.radius??0,height:e.kind.body?.height||2*(e.kind.body?.radius??0),shape:e.kind.body?.shape==='circle'?'sphere':e.kind.body?.shape??'sphere',ignore},e.id);
|
|
758
|
+
const body={pos:e.pos,grounded:e.grounded};const hit=scene.sweep(body,d);
|
|
759
|
+
e.pos=V(wrapPoint(body.pos,currentMap.wrap));e.grounded=body.grounded;return hit;
|
|
760
|
+
}
|
|
720
761
|
const radius = e.kind.body?.radius ?? 0;
|
|
721
762
|
// The ids to pass through: the first few of a list, read as plain texts. A longer list is not searched once for every entity.
|
|
722
763
|
const list = own(o, 'ignore');
|
|
@@ -733,7 +774,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
733
774
|
e.grounded = Boolean(support && support.nz > 0.5);
|
|
734
775
|
return hit ? Object.freeze({ ...(hit.id ? { entity: hit.id } : {}), at: e.pos, normal: V({ x: hit.nx, y: hit.ny, z: hit.nz }) }) : undefined;
|
|
735
776
|
}
|
|
736
|
-
const cast = castMap(
|
|
777
|
+
const cast = castMap(currentMap, e.pos.x, e.pos.y, d.x, d.y, radius);
|
|
737
778
|
let best: Hit | null = cast.hit;
|
|
738
779
|
for (const other of ents.values()) {
|
|
739
780
|
if (other === e || !other.kind.body || ignore.has(other.id)) continue;
|
|
@@ -778,18 +819,20 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
778
819
|
const row = colliderRow(e.id,e.pos,e.kind.body,e.kind.collider,e.f,e.kind.name,e.kind.query);
|
|
779
820
|
return row ? [row] : [];
|
|
780
821
|
});
|
|
781
|
-
const
|
|
782
|
-
|
|
822
|
+
const metadata={...(currentMap.wrap?{wrap:currentMap.wrap}:{}),...((pendingMap??mapRevision)?{map:pendingMap??mapRevision,...(pendingMap&&mapRevision?{previousMap:mapRevision}:{})}:{})};
|
|
823
|
+
const text = JSON.stringify(Object.keys(metadata).length ? [rows,metadata] : rows);
|
|
824
|
+
if (!collisionPrevious || text !== collisionText) {collisionText=text;collisionPrevious=[...(collisionPrevious ? [tick,tick+1] : opts.restore?.collision ?? [tick,tick+1]) as [number,number],rows,...(Object.keys(metadata).length?[metadata]:[])];}
|
|
783
825
|
return collisionPrevious;
|
|
784
826
|
};
|
|
785
|
-
let moveGeometry =
|
|
827
|
+
let moveGeometry = currentMap as import('./math.ts').MapShapes;
|
|
786
828
|
const queries = collisionQueries(() => moveGeometry, () => moveShape, dims, () => cx.ent?.id);
|
|
787
829
|
const moveWorld = brand(Object.freeze(queries));
|
|
788
|
-
const moveMap = brand(Object.freeze({ name
|
|
830
|
+
const moveMap = brand(Object.freeze({ get name(){return currentMap.name;}, spot: mapApi.spot, spots: mapApi.spots, ...tileApi(()=>currentMap), ...queries }));
|
|
789
831
|
let moveShape = { shape: 'sphere', radius: 0, height: 0 };
|
|
790
832
|
const moveCtx = brand({} as Record<string, unknown>);
|
|
791
833
|
for (const [name, get] of Object.entries({ tick: () => tick, dt: () => dt, tune: () => c.publicTune, math: () => math, map: () => moveMap, world: () => moveWorld })) Object.defineProperty(moveCtx, name, { get, enumerable: true });
|
|
792
|
-
Object.assign(moveCtx, { ticks: (seconds: unknown): number => { charge(1); return ticks(seconds); } });
|
|
834
|
+
Object.assign(moveCtx, { substeps: substeps(c.settings.tickHz), ticks: (seconds: unknown): number => { charge(1); return ticks(seconds); } });
|
|
835
|
+
if (physics) Object.assign(moveCtx, {physics: physics.movement});
|
|
793
836
|
Object.freeze(moveCtx);
|
|
794
837
|
const joinCtx = brand({} as Record<string, unknown>);
|
|
795
838
|
for (const [name, get] of Object.entries({ map: () => mapApi, shared: sharedView, tune: () => c.tune, math: () => math, round: () => roundApi })) Object.defineProperty(joinCtx, name, { get, enumerable: true });
|
|
@@ -1037,6 +1080,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1037
1080
|
/* ---------------------------------------------------------------- one tick */
|
|
1038
1081
|
|
|
1039
1082
|
function inArea(a: any, p: Vec3): boolean {
|
|
1083
|
+
if(currentMap.wrap) p = relativePoint(p, a.k === 'b' ? {x:(a.min.x+a.max.x)/2,y:(a.min.y+a.max.y)/2,z:(a.min.z+a.max.z)/2} : a.at, currentMap.wrap);
|
|
1040
1084
|
if (a.k === 'b') return p.x >= a.min.x && p.x <= a.max.x && p.y >= a.min.y && p.y <= a.max.y && (dims === 2 || p.z >= a.min.z && p.z <= a.max.z);
|
|
1041
1085
|
const dx = p.x - a.at.x; const dy = p.y - a.at.y;
|
|
1042
1086
|
const dz = dims === 3 ? p.z - a.at.z : 0;
|
|
@@ -1049,6 +1093,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1049
1093
|
}
|
|
1050
1094
|
function step(inputs: ReadonlyMap<number, StepInput> = new Map(), guides?: () => void): void {
|
|
1051
1095
|
tick = (tick + 1) >>> 0;
|
|
1096
|
+
if(pendingMap&&pendingMap[0]<=tick){currentMap=pendingGeometry!;mapRevision=pendingMap;currentMapCells=pendingMapCells;mapCells=currentMapCells;pendingMapCells=0;pendingMap=null;pendingGeometry=null;moveGeometry=currentMap;}
|
|
1052
1097
|
// The tick pays first for the state it sends: what the last snapshot packed. A room that holds more than a tick can send has nothing left for its handlers, and ends as any room does whose budget trips on every tick.
|
|
1053
1098
|
left = c.settings.budget.tick - SNAP * snapCells;
|
|
1054
1099
|
cut = false; stopped = false; threw = null;
|
|
@@ -1107,7 +1152,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1107
1152
|
}
|
|
1108
1153
|
guides?.();
|
|
1109
1154
|
// All movers see the same world, independent of entity iteration order.
|
|
1110
|
-
if (hasColliders) { left -= 64 * ents.size; moveGeometry = collisionMap(
|
|
1155
|
+
if (hasColliders) { left -= 64 * ents.size; moveGeometry = collisionMap(currentMap, collisionState()[2]); }
|
|
1111
1156
|
// Phase 1: one input step and `move` for every body.
|
|
1112
1157
|
for (const e of [...ents.values()]) {
|
|
1113
1158
|
if (e.dead || !e.kind.body) continue;
|
|
@@ -1140,7 +1185,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1140
1185
|
// Owner movement: the browser says where its body is, and the server holds the claim to maxSpeed.
|
|
1141
1186
|
e.allow = Math.min(e.allow + body.maxSpeed * dt * 1.02, body.maxSpeed * 0.25 + 0.05);
|
|
1142
1187
|
if (claim && claim.r === e.r) {
|
|
1143
|
-
const dx =
|
|
1188
|
+
const dx = periodicDelta(e.pos.x,claim.pos.x,currentMap.wrap?.x); const dy = periodicDelta(e.pos.y,claim.pos.y,currentMap.wrap?.y);
|
|
1144
1189
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1145
1190
|
const go = Math.min(dist, e.allow);
|
|
1146
1191
|
// The browser ran the game's own `move` and came further than `maxSpeed` allows: the body is held back.
|
|
@@ -1153,7 +1198,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1153
1198
|
e.grounded = dims === 2;
|
|
1154
1199
|
// The support cast is map work even when a browser supplies the pose.
|
|
1155
1200
|
// Keep it under the same movement quota as a server-driven body's sweep.
|
|
1156
|
-
if (dims === 3) run(k.name, 'move', e, 'move', () => { e.grounded = restsOnMap(
|
|
1201
|
+
if (dims === 3) run(k.name, 'move', e, 'move', () => { e.grounded = restsOnMap(currentMap, e.pos, body); }, 'body');
|
|
1157
1202
|
}
|
|
1158
1203
|
continue;
|
|
1159
1204
|
}
|
|
@@ -1244,8 +1289,10 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1244
1289
|
}
|
|
1245
1290
|
function save(): SavedCore {
|
|
1246
1291
|
return {
|
|
1292
|
+
...(physics ? {physics: physics.save()} : {}),
|
|
1247
1293
|
v: SAVE_REVISION, tick, epoch, rng, nextId, seq, round: [round.n, round.phase === 'live' ? 1 : 0, round.endsAt, round.startedAt], overAt, match: [playing, restartAt], trips,
|
|
1248
|
-
...(hasColliders?{collision:collisionState().slice(0,2) as [number,number]}:{}),
|
|
1294
|
+
...((hasColliders||mapRevision||pendingMap)?{collision:collisionState().slice(0,2) as [number,number]}:{}),
|
|
1295
|
+
...((mapRevision||pendingMap)?{maps:{current:mapRevision,pending:pendingMap}}:{}),
|
|
1249
1296
|
...(historyTicks?{history:poseHistory.map(([at,rows])=>[at,rows.map(row=>[...row])] as SavedCore['history'])}:{}),
|
|
1250
1297
|
shared: packFields(c.shared, shared, dims), policy: { ...policy }, intent: restartAsked ? [endAsked ? 1 : 0, finishing ? 1 : 0, 1] : [endAsked ? 1 : 0, finishing ? 1 : 0], asks: pendingAsks, guideViews: [...guideViews],
|
|
1251
1298
|
ents: [...ents.values()].map(saveEnt), spawns: spawns.map(saveEnt),
|
|
@@ -1264,8 +1311,12 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1264
1311
|
check(Array.isArray(value) && JSON.stringify(value) === JSON.stringify(packFields(list, unpackFields(list, value, dims), dims)));
|
|
1265
1312
|
};
|
|
1266
1313
|
check(r.v === SAVE_REVISION);
|
|
1314
|
+
if(r.maps!==undefined){
|
|
1315
|
+
check(r.maps&&Object.keys(r.maps).length===2&&'current' in r.maps&&'pending' in r.maps&&Object.keys(r.maps).every(k=>k==='current'||k==='pending'));
|
|
1316
|
+
for(const [name,value] of Object.entries(r.maps)){if(value===null)continue;check(Array.isArray(value)&&value.length===2&&uint(value[0])&&(name==='pending'?value[0]===r.tick+1:value[0]<=r.tick));prepareMapReplacement(value[1],c.map);}
|
|
1317
|
+
}
|
|
1267
1318
|
if(r.collision!==undefined)check(Array.isArray(r.collision)&&r.collision.length===2&&uint(r.collision[0])&&uint(r.collision[1])&&r.collision[0]<=r.tick&&r.collision[1]===r.collision[0]+1);
|
|
1268
|
-
check(Object.keys(r).every(k => ['v', 'tick', 'epoch', 'rng', 'nextId', 'seq', 'round', 'overAt', 'match', 'trips', 'shared', 'policy', 'intent', 'asks', 'guideViews', 'ents', 'spawns', 'seats', 'queue', 'areas', 'ops', 'history', 'collision'].includes(k)));
|
|
1319
|
+
check(Object.keys(r).every(k => ['v', 'tick', 'epoch', 'rng', 'nextId', 'seq', 'round', 'overAt', 'match', 'trips', 'shared', 'policy', 'intent', 'asks', 'guideViews', 'ents', 'spawns', 'seats', 'queue', 'areas', 'ops', 'history', 'collision', 'maps', 'physics'].includes(k)));
|
|
1269
1320
|
const askNames = new Set();
|
|
1270
1321
|
check(Array.isArray(r.asks));
|
|
1271
1322
|
for (const a of r.asks) {
|
|
@@ -1311,7 +1362,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1311
1362
|
for (const i of [7, 8, 9]) check(JSON.stringify(w[i]) === JSON.stringify(packVec(unpackVec(w[i], 3), 3)));
|
|
1312
1363
|
const goal = w[15] as any;
|
|
1313
1364
|
if (goal !== null) check(goal && typeof goal === 'object' && !Array.isArray(goal) && Object.keys(goal).every(k => ['goal', 'args', 'from', 'at', 'asked', 'state'].includes(k)) && typeof goal.goal === 'string' && goal.args && typeof goal.args === 'object' && !Array.isArray(goal.args) && ['brain', 'floor'].includes(goal.from) && uint(goal.at) && goal.at <= r.tick && typeof goal.asked === 'boolean' && goal.state === 'active');
|
|
1314
|
-
const pos = unpackVec(w[7] as number[], 3), bounds =
|
|
1365
|
+
const pos = unpackVec(w[7] as number[], 3), bounds = currentMap.bounds;
|
|
1315
1366
|
check(pos.x >= bounds.min.x && pos.x <= bounds.max.x && pos.y >= bounds.min.y && pos.y <= bounds.max.y && (dims === 3 ? pos.z >= bounds.min.z && pos.z <= bounds.max.z : pos.z === 0));
|
|
1316
1367
|
check(Number.isInteger(w[11]) && (w[11] as number) >= -1 && (w[11] as number) < c.seats && ['person', 'bot', 'ai'].includes(w[12] as string) && typeof w[13] === 'string' && w[13].length <= 128);
|
|
1317
1368
|
if (k.player) { check(!bodySeats.has(w[11])); bodySeats.add(w[11]); }
|
|
@@ -1387,6 +1438,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1387
1438
|
}
|
|
1388
1439
|
}
|
|
1389
1440
|
const r = opts.restore;
|
|
1441
|
+
try {
|
|
1390
1442
|
if (r) {
|
|
1391
1443
|
if (r.v !== SAVE_REVISION) throw new Error('this save was written by another version of the runtime');
|
|
1392
1444
|
validateSave(r);
|
|
@@ -1398,6 +1450,12 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1398
1450
|
}
|
|
1399
1451
|
pendingAsks = r.asks.map(a => ({ ...a, state: deepFreeze(a.state) }));
|
|
1400
1452
|
if ([opts.restoreEpoch].some((e) => e !== undefined && (!Number.isSafeInteger(e) || e <= 0))) throw new Error('restored epoch is invalid');
|
|
1453
|
+
if(r.maps){
|
|
1454
|
+
mapRevision=r.maps.current;pendingMap=r.maps.pending;
|
|
1455
|
+
if(mapRevision){const p=prepareMapReplacement(mapRevision[1],c.map);mapRevision=Object.freeze([mapRevision[0],p.raw]);currentMap=p.map;currentMapCells=p.cells;mapCells=p.cells;}
|
|
1456
|
+
if(pendingMap){const p=prepareMapReplacement(pendingMap[1],c.map);pendingMap=Object.freeze([pendingMap[0],p.raw]);pendingGeometry=p.map;pendingMapCells=p.cells;mapCells=currentMapCells+pendingMapCells;}
|
|
1457
|
+
moveGeometry=currentMap;
|
|
1458
|
+
}
|
|
1401
1459
|
tick = r.tick; epoch = opts.restoreEpoch ?? r.epoch; rng = r.rng; nextId = r.nextId; seq = r.seq; overAt = r.overAt; playing = r.match[0]; restartAt = r.match[1]; trips = r.trips;
|
|
1402
1460
|
endAsked = r.intent?.[0] === 1; finishing = r.intent?.[1] === 1; restartAsked = r.intent?.[2] === 1;
|
|
1403
1461
|
round = { n: r.round[0], phase: r.round[1] === 1 ? 'live' : 'over', endsAt: r.round[2], startedAt: r.round[3] };
|
|
@@ -1416,8 +1474,9 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1416
1474
|
});
|
|
1417
1475
|
areas = r.areas.map((a: any[]) => ({ from: a[0], fromId: a[1], seq: a[2], shape: a[3], ev: a[4], data: deepFreeze(coerceFields(c.events[a[4]] ?? [], a[5], dims)) }));
|
|
1418
1476
|
ops = r.ops as typeof ops;
|
|
1419
|
-
if(hasColliders)collisionState();
|
|
1477
|
+
if(hasColliders||mapRevision||pendingMap)collisionState();
|
|
1420
1478
|
} else startMatch();
|
|
1479
|
+
} catch (error) { physics?.dispose(); throw error; }
|
|
1421
1480
|
|
|
1422
1481
|
function decisionResult(value: unknown): Record<string, unknown> {
|
|
1423
1482
|
const v = plainData(value, { n: ANSWER_MAX });
|
|
@@ -1501,8 +1560,8 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1501
1560
|
const list = [...ents.values()].map((e) => packEntity(e.kind, e, dims));
|
|
1502
1561
|
// Include all three components of position, velocity and heading in a 3D snapshot.
|
|
1503
1562
|
snapCells = packed.n + (dims === 3 ? 13 : 10) * list.length;
|
|
1504
|
-
const collision = hasColliders ? collisionState() : null;
|
|
1505
|
-
if (collision) snapCells += 3 + collision[2].length * 8;
|
|
1563
|
+
const collision = hasColliders||mapRevision||pendingMap ? collisionState() : currentMap.wrap ? [0,0,[],{wrap:currentMap.wrap}] : null;
|
|
1564
|
+
if (collision) snapCells += 3 + collision[2].length * 8 + mapCells;
|
|
1506
1565
|
return [[round.n, round.phase === 'live' ? 1 : 0, round.endsAt], list, ...(collision ? [collision] : [])];
|
|
1507
1566
|
},
|
|
1508
1567
|
shared: () => packFields(c.shared, shared, dims),
|
|
@@ -1512,6 +1571,7 @@ export function createCore(c: Compiled, opts: { characterForSeat?: (seat: number
|
|
|
1512
1571
|
bodyOf: (seat) => { const s = seats.get(seat); const e = s?.id ? ents.get(s.id) : null; return e ? { id: e.id, kind: e.kind, r: e.r, away: e.away, driver: e.driver } : null; },
|
|
1513
1572
|
drain: () => { const o = out; out = []; return o; },
|
|
1514
1573
|
save,
|
|
1574
|
+
dispose: () => physics?.dispose(),
|
|
1515
1575
|
world,
|
|
1516
1576
|
stats,
|
|
1517
1577
|
};
|
package/rules/host.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { periodicDelta } from './topology.mjs';
|
|
1
2
|
import { stageTimings } from './timing.mjs';
|
|
2
3
|
/*
|
|
3
4
|
* host.ts — the host runtime: a rules module playing the host's part on the wire.
|
|
@@ -675,7 +676,7 @@ export function createHost(o: HostOptions): Host {
|
|
|
675
676
|
const watching = all.filter((_, n) => { const a = x.list[n][3]; return !a || a[0] >= 0; });
|
|
676
677
|
for (const seat of present.keys()) {
|
|
677
678
|
const l = bodies.get(seat);
|
|
678
|
-
const mine = !l ? watching : all.filter((_, n) => { const a = x.list[n][3]; return !a || a[1].includes(l.id) || (a[0] >= 0 && a[2] !== null && Math.hypot(a[2][0]
|
|
679
|
+
const mine = !l ? watching : all.filter((_, n) => { const a = x.list[n][3]; return !a || a[1].includes(l.id) || (a[0] >= 0 && a[2] !== null && Math.hypot(periodicDelta(l.pos.x,a[2][0],o.compiled.map.wrap?.x), periodicDelta(l.pos.y,a[2][1],o.compiled.map.wrap?.y), periodicDelta(l.pos.z,a[2][2],o.compiled.map.wrap?.z)) <= a[0]); });
|
|
679
680
|
if (mine.length) o.send({ t: 'ev', from: null, k: 'fx', d: [x.tick, mine], to: seat });
|
|
680
681
|
}
|
|
681
682
|
if (watching.length) o.send({ t: 'ev', from: null, k: 'fx', d: [x.tick, watching], to: -1 });
|
|
@@ -931,6 +932,7 @@ export function createHost(o: HostOptions): Host {
|
|
|
931
932
|
}
|
|
932
933
|
function stop(): void {
|
|
933
934
|
running = false; paused = false; ended = true;
|
|
935
|
+
core.dispose();
|
|
934
936
|
disarm();
|
|
935
937
|
agents?.stop();
|
|
936
938
|
for (let i = recent.length - 1; i >= 0; i -= 1) if (recent[i].who === self) recent.splice(i, 1);
|