@jgengine/core 0.8.0 → 0.9.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 +16 -0
- package/dist/ai/flock.d.ts +36 -0
- package/dist/ai/flock.js +182 -0
- package/dist/ai/groupAssist.d.ts +19 -0
- package/dist/ai/groupAssist.js +59 -0
- package/dist/ai/laneSelect.d.ts +21 -0
- package/dist/ai/laneSelect.js +37 -0
- package/dist/ai/mobBrain.d.ts +68 -0
- package/dist/ai/mobBrain.js +115 -0
- package/dist/ai/spawnDirector.js +1 -1
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/combat/abilityKit.d.ts +17 -2
- package/dist/combat/abilityKit.js +62 -13
- package/dist/combat/castRunner.d.ts +40 -0
- package/dist/combat/castRunner.js +56 -0
- package/dist/combat/comboPoints.d.ts +14 -0
- package/dist/combat/comboPoints.js +52 -0
- package/dist/combat/defensiveWindow.d.ts +1 -1
- package/dist/combat/defensiveWindow.js +5 -3
- package/dist/combat/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +201 -129
- package/dist/combat/resistance.d.ts +16 -4
- package/dist/combat/resistance.js +43 -7
- package/dist/combat/resourcePool.d.ts +20 -0
- package/dist/combat/resourcePool.js +55 -0
- package/dist/combat/shotOrigin.d.ts +40 -0
- package/dist/combat/shotOrigin.js +57 -0
- package/dist/combat/telegraph.d.ts +25 -0
- package/dist/combat/telegraph.js +47 -0
- package/dist/crafting/recipe.js +4 -1
- package/dist/devtools/devtools.d.ts +72 -24
- package/dist/devtools/devtools.js +449 -101
- package/dist/devtools/transformTunables.js +3 -3
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -0
- package/dist/faction/factions.d.ts +33 -0
- package/dist/faction/factions.js +76 -0
- package/dist/faction/reputation.d.ts +32 -0
- package/dist/faction/reputation.js +82 -0
- package/dist/game/chat.d.ts +2 -0
- package/dist/game/chat.js +9 -1
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +2 -0
- package/dist/game/events.d.ts +10 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +50 -1
- package/dist/game/playableGame.js +1 -1
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +39 -1
- package/dist/game/race.js +96 -11
- package/dist/game/recordBook.d.ts +29 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/input/axisInput.d.ts +26 -3
- package/dist/input/axisInput.js +44 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +6 -0
- package/dist/input/touchScheme.js +45 -7
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +23 -1
- package/dist/movement/dash.d.ts +3 -0
- package/dist/movement/dash.js +19 -5
- package/dist/movement/glideModel.d.ts +44 -0
- package/dist/movement/glideModel.js +54 -0
- package/dist/movement/grappleSwing.d.ts +34 -0
- package/dist/movement/grappleSwing.js +56 -0
- package/dist/movement/leaderTrail.d.ts +29 -0
- package/dist/movement/leaderTrail.js +67 -0
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/presenceModel.d.ts +14 -2
- package/dist/multiplayer/presenceModel.js +15 -3
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navGrid.js +30 -4
- package/dist/nav/railGraph.d.ts +66 -0
- package/dist/nav/railGraph.js +129 -0
- package/dist/nav/timetable.d.ts +41 -0
- package/dist/nav/timetable.js +94 -0
- package/dist/physics/buoyancy.d.ts +6 -0
- package/dist/physics/buoyancy.js +14 -0
- package/dist/physics/flowTube.d.ts +32 -0
- package/dist/physics/flowTube.js +64 -0
- package/dist/physics/forceVolume.d.ts +27 -0
- package/dist/physics/forceVolume.js +38 -0
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +13 -6
- package/dist/physics/physicsWorld.js +169 -40
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/vehicleBody.js +2 -1
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/runtime/cameraDirector.d.ts +6 -1
- package/dist/runtime/cameraDirector.js +6 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +21 -0
- package/dist/runtime/gameContext.js +184 -9
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +9 -1
- package/dist/runtime/hostPersistence.js +23 -8
- package/dist/runtime/inputSnapshot.d.ts +5 -0
- package/dist/runtime/inputSnapshot.js +5 -0
- package/dist/runtime/motionIntents.d.ts +14 -6
- package/dist/runtime/motionIntents.js +20 -2
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/scene/colliders.d.ts +44 -0
- package/dist/scene/colliders.js +77 -0
- package/dist/scene/entityStore.d.ts +22 -5
- package/dist/scene/entityStore.js +59 -12
- package/dist/scene/objectQuery.d.ts +13 -2
- package/dist/scene/objectQuery.js +34 -2
- package/dist/scene/objectStore.d.ts +2 -0
- package/dist/scene/objectStore.js +31 -12
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +11 -16
- package/dist/scene/spatial.js +99 -38
- package/dist/sensor/recordingBuffer.d.ts +6 -0
- package/dist/sensor/recordingBuffer.js +7 -0
- package/dist/sensor/replayLoop.d.ts +39 -0
- package/dist/sensor/replayLoop.js +64 -0
- package/dist/sensor/visionCone.d.ts +31 -0
- package/dist/sensor/visionCone.js +72 -0
- package/dist/session/extraction.d.ts +2 -2
- package/dist/session/extraction.js +13 -2
- package/dist/settings/settingsModel.d.ts +97 -0
- package/dist/settings/settingsModel.js +100 -0
- package/dist/stats/statModifiers.d.ts +5 -2
- package/dist/stats/statModifiers.js +32 -14
- package/dist/store/observableKeyedStore.js +8 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/time/beatClock.d.ts +18 -0
- package/dist/time/beatClock.js +24 -0
- package/dist/time/idleProgress.d.ts +29 -0
- package/dist/time/idleProgress.js +31 -0
- package/dist/time/serverTick.d.ts +7 -1
- package/dist/time/serverTick.js +27 -4
- package/dist/time/stateSchedule.d.ts +57 -0
- package/dist/time/stateSchedule.js +121 -0
- package/dist/turn/commit.d.ts +2 -2
- package/dist/turn/commit.js +2 -2
- package/dist/ui/hudLayout.d.ts +58 -0
- package/dist/ui/hudLayout.js +207 -0
- package/dist/ui/hudScale.d.ts +44 -0
- package/dist/ui/hudScale.js +55 -0
- package/dist/visibility/assetStreaming.d.ts +66 -0
- package/dist/visibility/assetStreaming.js +201 -0
- package/dist/visibility/bounds.d.ts +66 -0
- package/dist/visibility/bounds.js +113 -0
- package/dist/visibility/camera.d.ts +21 -0
- package/dist/visibility/camera.js +3 -0
- package/dist/visibility/config.d.ts +21 -0
- package/dist/visibility/config.js +1 -0
- package/dist/visibility/diagnostics.d.ts +26 -0
- package/dist/visibility/diagnostics.js +36 -0
- package/dist/visibility/distance.d.ts +11 -0
- package/dist/visibility/distance.js +30 -0
- package/dist/visibility/frustum.d.ts +47 -0
- package/dist/visibility/frustum.js +186 -0
- package/dist/visibility/occlusion.d.ts +36 -0
- package/dist/visibility/occlusion.js +45 -0
- package/dist/visibility/settings.d.ts +75 -0
- package/dist/visibility/settings.js +49 -0
- package/dist/visibility/simulationCulling.d.ts +34 -0
- package/dist/visibility/simulationCulling.js +36 -0
- package/dist/visibility/spatialIndex.d.ts +45 -0
- package/dist/visibility/spatialIndex.js +198 -0
- package/dist/visibility/visibilitySystem.d.ts +90 -0
- package/dist/visibility/visibilitySystem.js +257 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/buildings.js +152 -0
- package/dist/world/carve.d.ts +7 -2
- package/dist/world/carve.js +17 -7
- package/dist/world/cellStates.d.ts +45 -0
- package/dist/world/cellStates.js +103 -0
- package/dist/world/environmentSummary.d.ts +10 -2
- package/dist/world/environmentSummary.js +11 -2
- package/dist/world/features.d.ts +56 -8
- package/dist/world/features.js +24 -3
- package/dist/world/gridInstances.d.ts +7 -8
- package/dist/world/gridInstances.js +145 -7
- package/dist/world/lod.d.ts +17 -0
- package/dist/world/lod.js +66 -0
- package/dist/world/mapLayers.d.ts +60 -0
- package/dist/world/mapLayers.js +44 -0
- package/dist/world/massing.d.ts +45 -0
- package/dist/world/massing.js +581 -0
- package/dist/world/minimap.d.ts +13 -1
- package/dist/world/minimap.js +21 -2
- package/dist/world/terrain.d.ts +42 -3
- package/dist/world/terrain.js +197 -16
- package/dist/world/water.d.ts +7 -0
- package/dist/world/water.js +19 -4
- package/dist/world/weather.d.ts +3 -18
- package/dist/world/weather.js +4 -17
- package/dist/world/windZones.d.ts +47 -0
- package/dist/world/windZones.js +59 -0
- package/llms.txt +2585 -1704
- package/package.json +12 -2
|
@@ -30,6 +30,7 @@ import { raycastObjects, raycastObjectsAll } from "../scene/objectQuery.js";
|
|
|
30
30
|
import { createObjectStore } from "../scene/objectStore.js";
|
|
31
31
|
import { createRoster } from "../scene/roster.js";
|
|
32
32
|
import { createPossession } from "../scene/possession.js";
|
|
33
|
+
import { createSceneRaycast, } from "../scene/sceneRaycast.js";
|
|
33
34
|
import { createSpatialApi } from "../scene/spatial.js";
|
|
34
35
|
import { createTargeting } from "../scene/targeting.js";
|
|
35
36
|
import { createStats } from "../stats/statModifiers.js";
|
|
@@ -37,12 +38,14 @@ import { createChangeSignal, notifyAfter } from "../store/changeSignal.js";
|
|
|
37
38
|
import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
|
|
38
39
|
import { createSimClock } from "../time/simClock.js";
|
|
39
40
|
import { createTurnLoop } from "../turn/turnLoop.js";
|
|
41
|
+
import { RaceState } from "../game/race.js";
|
|
40
42
|
import { createCameraDirector } from "./cameraDirector.js";
|
|
41
43
|
import { createInputSnapshot } from "./inputSnapshot.js";
|
|
42
44
|
import { createMotionIntents } from "./motionIntents.js";
|
|
43
45
|
export function createGameContext(options) {
|
|
44
46
|
const { definition, content, player } = options;
|
|
45
47
|
const now = options.now ?? Date.now;
|
|
48
|
+
const occluder = options.occluder;
|
|
46
49
|
const signal = createChangeSignal();
|
|
47
50
|
const time = createSimClock({ config: definition.time, onChange: signal.notify });
|
|
48
51
|
const ground = groundFieldFor(definition.world);
|
|
@@ -68,12 +71,46 @@ export function createGameContext(options) {
|
|
|
68
71
|
const object = objects.get(instanceId);
|
|
69
72
|
return object === null ? undefined : content.objectById?.(object.catalogId);
|
|
70
73
|
}
|
|
74
|
+
let spatialGeneration = 0;
|
|
75
|
+
const candidateIds = [];
|
|
76
|
+
let candidateIdsDirty = true;
|
|
77
|
+
function refreshCandidateIds() {
|
|
78
|
+
if (!candidateIdsDirty)
|
|
79
|
+
return candidateIds;
|
|
80
|
+
candidateIds.length = 0;
|
|
81
|
+
for (const entity of entities.list())
|
|
82
|
+
candidateIds.push(entity.id);
|
|
83
|
+
candidateIdsDirty = false;
|
|
84
|
+
return candidateIds;
|
|
85
|
+
}
|
|
71
86
|
const spatial = createSpatialApi({
|
|
72
87
|
resolvePosition: (instanceId) => entities.get(instanceId)?.position,
|
|
73
|
-
candidates:
|
|
88
|
+
candidates: refreshCandidateIds,
|
|
89
|
+
grid: { cellSize: 8 },
|
|
90
|
+
getVersion: () => spatialGeneration,
|
|
91
|
+
...(occluder !== undefined ? { occluder } : {}),
|
|
92
|
+
});
|
|
93
|
+
entities.subscribe(() => {
|
|
94
|
+
candidateIdsDirty = true;
|
|
95
|
+
spatialGeneration += 1;
|
|
96
|
+
spatial.invalidate();
|
|
74
97
|
});
|
|
98
|
+
const entityColliders = new Map();
|
|
99
|
+
const objectColliders = new Map();
|
|
100
|
+
function entityCollidersOf(instanceId) {
|
|
101
|
+
const override = entityColliders.get(instanceId);
|
|
102
|
+
if (override !== undefined)
|
|
103
|
+
return override;
|
|
104
|
+
return catalogEntry(instanceId)?.colliders ?? null;
|
|
105
|
+
}
|
|
106
|
+
function objectCollidersOf(instanceId) {
|
|
107
|
+
const override = objectColliders.get(instanceId);
|
|
108
|
+
if (override !== undefined)
|
|
109
|
+
return override;
|
|
110
|
+
return catalogObject(instanceId)?.colliders ?? null;
|
|
111
|
+
}
|
|
75
112
|
const targeting = notifyAfter(createTargeting({
|
|
76
|
-
candidates: () =>
|
|
113
|
+
candidates: () => [...refreshCandidateIds()],
|
|
77
114
|
classify(_fromId, toId) {
|
|
78
115
|
const role = catalogEntry(toId)?.role;
|
|
79
116
|
return role === "enemy" || role === "hostile" ? "hostile" : "friendly";
|
|
@@ -82,9 +119,43 @@ export function createGameContext(options) {
|
|
|
82
119
|
}), ["setTarget", "cycleTarget"], signal.notify);
|
|
83
120
|
const combatSpatial = {
|
|
84
121
|
inRadius: (center, radius) => spatial.inRadius(center, radius),
|
|
85
|
-
hasLineOfSight: (from, to) =>
|
|
122
|
+
hasLineOfSight: (from, to) => {
|
|
123
|
+
if (typeof from === "string")
|
|
124
|
+
return spatial.hasLineOfSight(from, to);
|
|
125
|
+
const toPos = entities.get(to)?.position;
|
|
126
|
+
if (toPos === undefined)
|
|
127
|
+
return false;
|
|
128
|
+
if (occluder === undefined)
|
|
129
|
+
return true;
|
|
130
|
+
return !occluder(from, toPos);
|
|
131
|
+
},
|
|
86
132
|
positionOf: (instanceId) => entities.get(instanceId)?.position,
|
|
87
133
|
};
|
|
134
|
+
const sceneRaycast = createSceneRaycast({
|
|
135
|
+
entities: {
|
|
136
|
+
list: () => entities.list().map((entity) => ({
|
|
137
|
+
id: entity.id,
|
|
138
|
+
position: entity.position,
|
|
139
|
+
rotationY: entity.rotationY,
|
|
140
|
+
name: entity.name,
|
|
141
|
+
})),
|
|
142
|
+
collidersOf: entityCollidersOf,
|
|
143
|
+
inRadius: (center, radius) => spatial.inRadius(center, radius),
|
|
144
|
+
get: (id) => {
|
|
145
|
+
const entity = entities.get(id);
|
|
146
|
+
if (entity === null)
|
|
147
|
+
return null;
|
|
148
|
+
return { id: entity.id, position: entity.position, rotationY: entity.rotationY, name: entity.name };
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
objects: {
|
|
152
|
+
list: () => objects.list(),
|
|
153
|
+
inBox: (min, max) => objects.inBox(min, max),
|
|
154
|
+
collidersOf: objectCollidersOf,
|
|
155
|
+
halfExtentsOf: (catalogId) => content.objectById?.(catalogId)?.halfExtents ?? null,
|
|
156
|
+
},
|
|
157
|
+
terrain: ground,
|
|
158
|
+
});
|
|
88
159
|
const weapon = createWeaponStats((itemId) => content.itemById?.(itemId));
|
|
89
160
|
const rawEvents = createGameEvents();
|
|
90
161
|
const events = {
|
|
@@ -282,6 +353,7 @@ export function createGameContext(options) {
|
|
|
282
353
|
statsByInstance.delete(instanceId);
|
|
283
354
|
targeting.clearAll(instanceId);
|
|
284
355
|
pose.clear(instanceId);
|
|
356
|
+
entityColliders.delete(instanceId);
|
|
285
357
|
return existed;
|
|
286
358
|
}
|
|
287
359
|
function resetAllToSpawn(filter) {
|
|
@@ -422,13 +494,14 @@ export function createGameContext(options) {
|
|
|
422
494
|
if (input.dir !== undefined)
|
|
423
495
|
telegraphEvent.dir = input.dir;
|
|
424
496
|
events.emit("combat.telegraph", telegraphEvent);
|
|
497
|
+
const cancelVisual = () => events.emit("combat.telegraphCancelled", { id });
|
|
425
498
|
const bound = input.effect;
|
|
426
499
|
if (bound === undefined)
|
|
427
|
-
return
|
|
500
|
+
return cancelVisual;
|
|
428
501
|
const config = { shape: input.shape, at: input.at, windupMs: input.windupMs };
|
|
429
502
|
if (input.dir !== undefined)
|
|
430
503
|
config.dir = input.dir;
|
|
431
|
-
|
|
504
|
+
const cancelEffect = time.after(input.windupMs / 1000, () => {
|
|
432
505
|
const targets = entities.list().filter((entity) => pointInTelegraph(config, entity.position));
|
|
433
506
|
for (const target of targets) {
|
|
434
507
|
applyEffectAndFloat({
|
|
@@ -439,6 +512,10 @@ export function createGameContext(options) {
|
|
|
439
512
|
});
|
|
440
513
|
}
|
|
441
514
|
});
|
|
515
|
+
return () => {
|
|
516
|
+
cancelEffect();
|
|
517
|
+
cancelVisual();
|
|
518
|
+
};
|
|
442
519
|
}
|
|
443
520
|
function applyHitReaction(input) {
|
|
444
521
|
const attacker = entities.get(input.from);
|
|
@@ -499,10 +576,51 @@ export function createGameContext(options) {
|
|
|
499
576
|
preview: effects.preview,
|
|
500
577
|
applyEffect: applyEffectAndFloat,
|
|
501
578
|
};
|
|
579
|
+
const projectileObstacles = definition.physics?.projectileObstacles === true;
|
|
580
|
+
const projectileSceneRaycast = {
|
|
581
|
+
raycast(input) {
|
|
582
|
+
return sceneRaycast.raycast({
|
|
583
|
+
...input,
|
|
584
|
+
filter: {
|
|
585
|
+
entities: true,
|
|
586
|
+
objects: projectileObstacles,
|
|
587
|
+
terrain: false,
|
|
588
|
+
walls: projectileObstacles,
|
|
589
|
+
...input.filter,
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
},
|
|
593
|
+
raycastAll(input) {
|
|
594
|
+
return sceneRaycast.raycastAll({
|
|
595
|
+
...input,
|
|
596
|
+
filter: {
|
|
597
|
+
entities: true,
|
|
598
|
+
objects: projectileObstacles,
|
|
599
|
+
terrain: false,
|
|
600
|
+
walls: projectileObstacles,
|
|
601
|
+
...input.filter,
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
},
|
|
605
|
+
};
|
|
502
606
|
const projectiles = notifyAfter(createProjectileSystem({
|
|
503
607
|
effects: floatingEffects,
|
|
504
608
|
spatial: combatSpatial,
|
|
505
609
|
getStat: weapon.getStat,
|
|
610
|
+
sceneRaycast: projectileSceneRaycast,
|
|
611
|
+
objects: projectileObstacles
|
|
612
|
+
? {
|
|
613
|
+
list: () => objects.list(),
|
|
614
|
+
inBox: (min, max) => objects.inBox(min, max),
|
|
615
|
+
halfExtents: (catalogId) => {
|
|
616
|
+
const half = content.objectById?.(catalogId)?.halfExtents;
|
|
617
|
+
return half === undefined ? null : [half[0], half[1], half[2]];
|
|
618
|
+
},
|
|
619
|
+
collidersOf: objectCollidersOf,
|
|
620
|
+
}
|
|
621
|
+
: undefined,
|
|
622
|
+
entityCollidersOf,
|
|
623
|
+
rotationYOf: (instanceId) => entities.get(instanceId)?.rotationY,
|
|
506
624
|
now,
|
|
507
625
|
onSettle(report) {
|
|
508
626
|
events.emit("projectile.settled", {
|
|
@@ -517,8 +635,16 @@ export function createGameContext(options) {
|
|
|
517
635
|
const sceneObjects = {
|
|
518
636
|
...objects,
|
|
519
637
|
catalog: (instanceId) => catalogObject(instanceId) ?? null,
|
|
520
|
-
raycast: (input) => raycastObjects(objects
|
|
521
|
-
raycastAll: (input) => raycastObjectsAll(objects
|
|
638
|
+
raycast: (input) => raycastObjects(objects, input),
|
|
639
|
+
raycastAll: (input) => raycastObjectsAll(objects, input),
|
|
640
|
+
setColliders(instanceId, colliders) {
|
|
641
|
+
if (colliders === null)
|
|
642
|
+
objectColliders.delete(instanceId);
|
|
643
|
+
else
|
|
644
|
+
objectColliders.set(instanceId, colliders);
|
|
645
|
+
signal.notify();
|
|
646
|
+
},
|
|
647
|
+
collidersOf: objectCollidersOf,
|
|
522
648
|
};
|
|
523
649
|
const store = notifyAfter(createObservableKeyedStore(), ["set", "delete"], signal.notify);
|
|
524
650
|
const cardPiles = new Map();
|
|
@@ -542,7 +668,7 @@ export function createGameContext(options) {
|
|
|
542
668
|
throw new Error(`turn loop "${id}" has not been created yet; pass a config on first access`);
|
|
543
669
|
}
|
|
544
670
|
const raw = createTurnLoop(config);
|
|
545
|
-
const wrappedCommit = notifyAfter(raw.commit, ["submit", "expected", "commit", "
|
|
671
|
+
const wrappedCommit = notifyAfter(raw.commit, ["submit", "expected", "commit", "discard", "clear"], signal.notify);
|
|
546
672
|
const wrapped = {
|
|
547
673
|
...notifyAfter(raw, [
|
|
548
674
|
"setOrder",
|
|
@@ -561,7 +687,43 @@ export function createGameContext(options) {
|
|
|
561
687
|
turnLoops.set(id, wrapped);
|
|
562
688
|
return wrapped;
|
|
563
689
|
}
|
|
564
|
-
|
|
690
|
+
class NotifyingRaceState extends RaceState {
|
|
691
|
+
addRacer(racerId, startTime) {
|
|
692
|
+
super.addRacer(racerId, startTime);
|
|
693
|
+
signal.notify();
|
|
694
|
+
}
|
|
695
|
+
removeRacer(racerId) {
|
|
696
|
+
super.removeRacer(racerId);
|
|
697
|
+
signal.notify();
|
|
698
|
+
}
|
|
699
|
+
reset() {
|
|
700
|
+
super.reset();
|
|
701
|
+
signal.notify();
|
|
702
|
+
}
|
|
703
|
+
eliminate(racerId) {
|
|
704
|
+
super.eliminate(racerId);
|
|
705
|
+
signal.notify();
|
|
706
|
+
}
|
|
707
|
+
update(now, positions) {
|
|
708
|
+
const raceEvents = super.update(now, positions);
|
|
709
|
+
if (raceEvents.length > 0)
|
|
710
|
+
signal.notify();
|
|
711
|
+
return raceEvents;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const raceStates = new Map();
|
|
715
|
+
function raceState(id, config) {
|
|
716
|
+
const existing = raceStates.get(id);
|
|
717
|
+
if (existing !== undefined)
|
|
718
|
+
return existing;
|
|
719
|
+
if (config === undefined) {
|
|
720
|
+
throw new Error(`race "${id}" has not been created yet; pass a config on first access`);
|
|
721
|
+
}
|
|
722
|
+
const created = new NotifyingRaceState(config);
|
|
723
|
+
raceStates.set(id, created);
|
|
724
|
+
return created;
|
|
725
|
+
}
|
|
726
|
+
const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic", "setChaseTuning"], signal.notify);
|
|
565
727
|
const input = createInputSnapshot();
|
|
566
728
|
const motion = createMotionIntents();
|
|
567
729
|
const ctx = {
|
|
@@ -572,6 +734,7 @@ export function createGameContext(options) {
|
|
|
572
734
|
despawn: despawnEntity,
|
|
573
735
|
update: entities.update,
|
|
574
736
|
setPose: entities.setPose,
|
|
737
|
+
setPoseConstraint: entities.setPoseConstraint,
|
|
575
738
|
get: entities.get,
|
|
576
739
|
list: entities.list,
|
|
577
740
|
spawnPoseOf: entities.spawnPoseOf,
|
|
@@ -595,6 +758,15 @@ export function createGameContext(options) {
|
|
|
595
758
|
hasLineOfSight: spatial.hasLineOfSight,
|
|
596
759
|
queryArc: spatial.queryArc,
|
|
597
760
|
moveToward: spatial.moveToward,
|
|
761
|
+
invalidateSpatial: spatial.invalidate,
|
|
762
|
+
setColliders(instanceId, colliders) {
|
|
763
|
+
if (colliders === null)
|
|
764
|
+
entityColliders.delete(instanceId);
|
|
765
|
+
else
|
|
766
|
+
entityColliders.set(instanceId, colliders);
|
|
767
|
+
signal.notify();
|
|
768
|
+
},
|
|
769
|
+
collidersOf: entityCollidersOf,
|
|
598
770
|
form: forms,
|
|
599
771
|
paint: paintLayer,
|
|
600
772
|
},
|
|
@@ -606,6 +778,8 @@ export function createGameContext(options) {
|
|
|
606
778
|
pickup: pickupWorldItem,
|
|
607
779
|
consume: worldItems.remove,
|
|
608
780
|
},
|
|
781
|
+
raycast: (input) => sceneRaycast.raycast(input),
|
|
782
|
+
raycastAll: (input) => sceneRaycast.raycastAll(input),
|
|
609
783
|
},
|
|
610
784
|
world: {
|
|
611
785
|
ground,
|
|
@@ -649,6 +823,7 @@ export function createGameContext(options) {
|
|
|
649
823
|
store,
|
|
650
824
|
cards: { pile },
|
|
651
825
|
turn: { loop },
|
|
826
|
+
race: { state: raceState },
|
|
652
827
|
},
|
|
653
828
|
player: {
|
|
654
829
|
userId: player.userId,
|
|
@@ -5,7 +5,7 @@ import { type GameRuntimeSnapshot, type RuntimeChunkRow, type RuntimePlayerRow,
|
|
|
5
5
|
export type ServerLoopHooks = {
|
|
6
6
|
onInit?: (ctx: RuntimeInitContext) => void;
|
|
7
7
|
onNewPlayer?: (ctx: RuntimeLoopContext) => void;
|
|
8
|
-
onTick?: (ctx:
|
|
8
|
+
onTick?: (ctx: RuntimeWorldContext, dtSeconds: number) => void;
|
|
9
9
|
};
|
|
10
10
|
export type RuntimeInitContext = {
|
|
11
11
|
snapshot: GameRuntimeSnapshot;
|
|
@@ -17,6 +17,9 @@ export type RuntimeLoopContext = RuntimeInitContext & {
|
|
|
17
17
|
isNew: boolean;
|
|
18
18
|
};
|
|
19
19
|
};
|
|
20
|
+
export type RuntimeWorldContext = RuntimeInitContext & {
|
|
21
|
+
playerIds: readonly string[];
|
|
22
|
+
};
|
|
20
23
|
export type GameRuntimeDefinition = {
|
|
21
24
|
gameId: string;
|
|
22
25
|
save: SaveConfig;
|
|
@@ -29,6 +32,7 @@ export type HydrateInput = {
|
|
|
29
32
|
serverRow: RuntimeServerRow;
|
|
30
33
|
playersByUserId: Record<string, RuntimePlayerRow>;
|
|
31
34
|
chunksByKey: Record<string, RuntimeChunkRow>;
|
|
35
|
+
revision?: number;
|
|
32
36
|
};
|
|
33
37
|
export type GameRuntime = {
|
|
34
38
|
gameId: string;
|
|
@@ -13,6 +13,7 @@ export function createGameRuntime(definition) {
|
|
|
13
13
|
server: input.serverRow,
|
|
14
14
|
players: input.playersByUserId,
|
|
15
15
|
chunks: input.chunksByKey,
|
|
16
|
+
revision: input.revision,
|
|
16
17
|
});
|
|
17
18
|
if (loop?.onInit && !initialized) {
|
|
18
19
|
initialized = true;
|
|
@@ -34,18 +35,18 @@ export function createGameRuntime(definition) {
|
|
|
34
35
|
if (!loop?.onTick)
|
|
35
36
|
return snapshot;
|
|
36
37
|
let current = snapshot;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
const ctx = {
|
|
39
|
+
get snapshot() {
|
|
40
|
+
return current;
|
|
41
|
+
},
|
|
42
|
+
get playerIds() {
|
|
43
|
+
return Object.keys(current.players);
|
|
44
|
+
},
|
|
45
|
+
setSnapshot(next) {
|
|
46
|
+
current = next;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
loop.onTick(ctx, dtSeconds);
|
|
49
50
|
return current;
|
|
50
51
|
},
|
|
51
52
|
joinPlayer(snapshot, userId, isNew) {
|
|
@@ -63,6 +63,7 @@ export declare function drainPendingLeaderboardIncrements(session: Record<string
|
|
|
63
63
|
session: Record<string, unknown>;
|
|
64
64
|
};
|
|
65
65
|
export declare const LEADERBOARD_TOP_LIMIT = 100;
|
|
66
|
+
export declare function clampLimit(value: number | undefined, fallback: number, max: number): number;
|
|
66
67
|
export type LeaderboardRow = {
|
|
67
68
|
gameId: string;
|
|
68
69
|
stat: string;
|
|
@@ -97,7 +98,12 @@ export type ServerListing = {
|
|
|
97
98
|
tags?: string[];
|
|
98
99
|
updatedAt: number;
|
|
99
100
|
};
|
|
100
|
-
export
|
|
101
|
+
export type ToServerListingOptions = {
|
|
102
|
+
includeJoinCode?: boolean;
|
|
103
|
+
};
|
|
104
|
+
export declare function toServerListing(record: GameServerRecord, options?: ToServerListingOptions): ServerListing;
|
|
105
|
+
export declare const OPEN_SERVER_LISTING_LIMIT = 20;
|
|
106
|
+
export declare const OPEN_SERVER_LISTING_MAX = 100;
|
|
101
107
|
export declare function toOpenServerListings(listings: Iterable<ServerListing>, limit?: number): ServerListing[];
|
|
102
108
|
export type HostPersistence = {
|
|
103
109
|
savePlan?: (plan: ServerPersistPlan) => Promise<void>;
|
|
@@ -112,6 +118,7 @@ export type HostPersistence = {
|
|
|
112
118
|
saveProfile: (record: PlayerProfileRecord) => Promise<void>;
|
|
113
119
|
loadChunks: (serverId: string) => Promise<WorldChunkRecord[]>;
|
|
114
120
|
saveChunks: (serverId: string, chunks: WorldChunkRecord[]) => Promise<void>;
|
|
121
|
+
deleteChunks: (serverId: string, chunkKeys: string[]) => Promise<void>;
|
|
115
122
|
loadFeed: (args: {
|
|
116
123
|
serverId: string;
|
|
117
124
|
action: string;
|
|
@@ -139,6 +146,7 @@ export type ServerPersistPlan = {
|
|
|
139
146
|
server: GameServerRecord;
|
|
140
147
|
profiles: PlayerProfileRecord[];
|
|
141
148
|
chunks: WorldChunkRecord[];
|
|
149
|
+
deletedChunks: string[];
|
|
142
150
|
leaderboard: LeaderboardIncrement[];
|
|
143
151
|
};
|
|
144
152
|
export declare function planServerPersist(server: GameServerRecord, snapshot: GameRuntimeSnapshot, save: SaveConfig, now: number): ServerPersistPlan;
|
|
@@ -21,6 +21,12 @@ export function drainPendingLeaderboardIncrements(session) {
|
|
|
21
21
|
return { increments: raw.filter(isLeaderboardIncrement), session: rest };
|
|
22
22
|
}
|
|
23
23
|
export const LEADERBOARD_TOP_LIMIT = 100;
|
|
24
|
+
export function clampLimit(value, fallback, max) {
|
|
25
|
+
const candidate = value ?? fallback;
|
|
26
|
+
if (!Number.isFinite(candidate))
|
|
27
|
+
return fallback;
|
|
28
|
+
return Math.min(Math.max(Math.floor(candidate), 1), max);
|
|
29
|
+
}
|
|
24
30
|
export function leaderboardRowKey(row) {
|
|
25
31
|
return [row.gameId, row.scope, row.stat, row.serverId ?? "", row.userId].join("|");
|
|
26
32
|
}
|
|
@@ -45,7 +51,7 @@ export function applyLeaderboardRows(rows, gameId, entries, now) {
|
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
export function topLeaderboardRows(rows, args) {
|
|
48
|
-
const limit =
|
|
54
|
+
const limit = clampLimit(args.limit, LEADERBOARD_TOP_LIMIT, LEADERBOARD_TOP_LIMIT);
|
|
49
55
|
return [...rows]
|
|
50
56
|
.filter((row) => row.gameId === args.gameId &&
|
|
51
57
|
row.stat === args.stat &&
|
|
@@ -79,7 +85,7 @@ export function shouldAutoSave(save, dirtyAt, lastSavedAt, now) {
|
|
|
79
85
|
const anchor = lastSavedAt ?? 0;
|
|
80
86
|
return now - anchor >= intervalMs;
|
|
81
87
|
}
|
|
82
|
-
export function toServerListing(record) {
|
|
88
|
+
export function toServerListing(record, options = {}) {
|
|
83
89
|
return {
|
|
84
90
|
serverId: record.serverId,
|
|
85
91
|
status: record.status,
|
|
@@ -88,16 +94,20 @@ export function toServerListing(record) {
|
|
|
88
94
|
mode: record.mode,
|
|
89
95
|
label: record.label,
|
|
90
96
|
visibility: record.visibility,
|
|
91
|
-
|
|
97
|
+
...(options.includeJoinCode === true && record.joinCode !== undefined
|
|
98
|
+
? { joinCode: record.joinCode }
|
|
99
|
+
: {}),
|
|
92
100
|
tags: record.tags,
|
|
93
101
|
updatedAt: record.updatedAt,
|
|
94
102
|
};
|
|
95
103
|
}
|
|
96
|
-
export
|
|
104
|
+
export const OPEN_SERVER_LISTING_LIMIT = 20;
|
|
105
|
+
export const OPEN_SERVER_LISTING_MAX = 100;
|
|
106
|
+
export function toOpenServerListings(listings, limit = OPEN_SERVER_LISTING_LIMIT) {
|
|
97
107
|
return [...listings]
|
|
98
108
|
.filter((listing) => listing.status === "running")
|
|
99
109
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
100
|
-
.slice(0, limit);
|
|
110
|
+
.slice(0, clampLimit(limit, OPEN_SERVER_LISTING_LIMIT, OPEN_SERVER_LISTING_MAX));
|
|
101
111
|
}
|
|
102
112
|
export function buildHydratePlayers(server, profiles) {
|
|
103
113
|
const players = {};
|
|
@@ -146,12 +156,16 @@ export function planServerPersist(server, snapshot, save, now) {
|
|
|
146
156
|
}
|
|
147
157
|
}
|
|
148
158
|
const chunks = [];
|
|
159
|
+
const deletedChunks = [];
|
|
149
160
|
if (isSaveEnabled(save) && saveScopeIncludesChunks(save.scope)) {
|
|
150
161
|
for (const chunkKey of snapshot.dirty.chunks) {
|
|
151
162
|
const chunk = snapshot.chunks[chunkKey];
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
if (chunk) {
|
|
164
|
+
chunks.push({ serverId: server.serverId, chunkKey, snapshot: chunk, updatedAt: now });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
deletedChunks.push(chunkKey);
|
|
168
|
+
}
|
|
155
169
|
}
|
|
156
170
|
}
|
|
157
171
|
return {
|
|
@@ -166,6 +180,7 @@ export function planServerPersist(server, snapshot, save, now) {
|
|
|
166
180
|
},
|
|
167
181
|
profiles,
|
|
168
182
|
chunks,
|
|
183
|
+
deletedChunks,
|
|
169
184
|
leaderboard,
|
|
170
185
|
};
|
|
171
186
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import type { PointerAxisState } from "../input/pointerAxis.js";
|
|
1
2
|
export interface InputSnapshot {
|
|
2
3
|
/** Replaces the held-action set for this frame. Called by the shell before `onTick` each frame; does not bump `ctx.version()` or notify `ctx.subscribe` listeners — per-frame publishes would storm subscribers. */
|
|
3
4
|
publish(held: readonly string[]): void;
|
|
5
|
+
/** Replaces the normalized pointer-position state for this frame (#293). Same no-notify contract as `publish`. */
|
|
6
|
+
publishPointer(state: PointerAxisState | null): void;
|
|
4
7
|
isDown(action: string): boolean;
|
|
5
8
|
held(): readonly string[];
|
|
9
|
+
/** Pointer position over the play surface, `[-1, 1]` per axis with `+y` down, published by the shell each frame; `null` until the first pointer move. */
|
|
10
|
+
pointer(): PointerAxisState | null;
|
|
6
11
|
}
|
|
7
12
|
export declare function createInputSnapshot(): InputSnapshot;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
export function createInputSnapshot() {
|
|
2
2
|
let heldSet = new Set();
|
|
3
3
|
let heldList = [];
|
|
4
|
+
let pointerState = null;
|
|
4
5
|
return {
|
|
5
6
|
publish(held) {
|
|
6
7
|
heldList = held;
|
|
7
8
|
heldSet = new Set(held);
|
|
8
9
|
},
|
|
10
|
+
publishPointer(state) {
|
|
11
|
+
pointerState = state;
|
|
12
|
+
},
|
|
9
13
|
isDown: (action) => heldSet.has(action),
|
|
10
14
|
held: () => heldList,
|
|
15
|
+
pointer: () => pointerState,
|
|
11
16
|
};
|
|
12
17
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Seam for game code to reach the
|
|
2
|
+
* Seam for game code to reach the motion the shell's FrameDriver
|
|
3
3
|
* otherwise owns privately (#162.4). Game code calls `impulse`,
|
|
4
|
-
* `setVerticalVelocity`, and/or `setY` from `onTick` or
|
|
5
|
-
* calls `takePending()` once per frame, before
|
|
6
|
-
* what accumulated. `setY` wins over physics
|
|
7
|
-
* the
|
|
8
|
-
* `setVerticalVelocity` replaces that velocity outright.
|
|
4
|
+
* `pushHorizontal`, `setVerticalVelocity`, and/or `setY` from `onTick` or
|
|
5
|
+
* commands; the shell calls `takePending()` once per frame, before
|
|
6
|
+
* integrating gravity, to drain what accumulated. `setY` wins over physics
|
|
7
|
+
* for that frame; impulses add to the velocity the driver is about to
|
|
8
|
+
* integrate; a later `setVerticalVelocity` replaces that velocity outright.
|
|
9
|
+
* Horizontal pushes compose with the walk controller (#282.4): they add to
|
|
10
|
+
* its horizontal velocity and decay naturally as it re-blends toward input —
|
|
11
|
+
* knockback, dashes, explosion shoves without raw `setPose` offsets.
|
|
9
12
|
*
|
|
10
13
|
* Intentionally not `ctx.version()`/`ctx.subscribe` wired: these fire at
|
|
11
14
|
* most once per frame and would otherwise storm subscribers with per-frame
|
|
@@ -13,13 +16,18 @@
|
|
|
13
16
|
*/
|
|
14
17
|
export interface MotionIntents {
|
|
15
18
|
impulse(velocityY: number): void;
|
|
19
|
+
/** Adds a horizontal velocity kick (world units/s) that the walk controller integrates and decays. */
|
|
20
|
+
pushHorizontal(velocityX: number, velocityZ: number): void;
|
|
16
21
|
setVerticalVelocity(velocityY: number): void;
|
|
17
22
|
setY(y: number): void;
|
|
18
23
|
takePending(): MotionIntentBatch | null;
|
|
19
24
|
}
|
|
20
25
|
export interface MotionIntentBatch {
|
|
21
26
|
impulses: readonly number[];
|
|
27
|
+
horizontalImpulses: readonly (readonly [number, number])[];
|
|
22
28
|
verticalVelocity: number | null;
|
|
23
29
|
y: number | null;
|
|
24
30
|
}
|
|
25
31
|
export declare function createMotionIntents(): MotionIntents;
|
|
32
|
+
/** Fold a batch's horizontal pushes into a controller's velocity pair — shared by the walk and voxel drivers. */
|
|
33
|
+
export declare function applyHorizontalImpulses(velocityX: number, velocityZ: number, batch: MotionIntentBatch | null): readonly [number, number];
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
export function createMotionIntents() {
|
|
2
2
|
let impulses = [];
|
|
3
|
+
let horizontalImpulses = [];
|
|
3
4
|
let verticalVelocity = null;
|
|
4
5
|
let y = null;
|
|
5
6
|
return {
|
|
6
7
|
impulse(velocityY) {
|
|
7
8
|
impulses.push(velocityY);
|
|
8
9
|
},
|
|
10
|
+
pushHorizontal(velocityX, velocityZ) {
|
|
11
|
+
horizontalImpulses.push([velocityX, velocityZ]);
|
|
12
|
+
},
|
|
9
13
|
setVerticalVelocity(velocityY) {
|
|
10
14
|
verticalVelocity = velocityY;
|
|
11
15
|
},
|
|
@@ -13,13 +17,27 @@ export function createMotionIntents() {
|
|
|
13
17
|
y = nextY;
|
|
14
18
|
},
|
|
15
19
|
takePending() {
|
|
16
|
-
if (impulses.length === 0 && verticalVelocity === null && y === null)
|
|
20
|
+
if (impulses.length === 0 && horizontalImpulses.length === 0 && verticalVelocity === null && y === null) {
|
|
17
21
|
return null;
|
|
18
|
-
|
|
22
|
+
}
|
|
23
|
+
const batch = { impulses, horizontalImpulses, verticalVelocity, y };
|
|
19
24
|
impulses = [];
|
|
25
|
+
horizontalImpulses = [];
|
|
20
26
|
verticalVelocity = null;
|
|
21
27
|
y = null;
|
|
22
28
|
return batch;
|
|
23
29
|
},
|
|
24
30
|
};
|
|
25
31
|
}
|
|
32
|
+
/** Fold a batch's horizontal pushes into a controller's velocity pair — shared by the walk and voxel drivers. */
|
|
33
|
+
export function applyHorizontalImpulses(velocityX, velocityZ, batch) {
|
|
34
|
+
if (batch === null || batch.horizontalImpulses.length === 0)
|
|
35
|
+
return [velocityX, velocityZ];
|
|
36
|
+
let x = velocityX;
|
|
37
|
+
let z = velocityZ;
|
|
38
|
+
for (const [ix, iz] of batch.horizontalImpulses) {
|
|
39
|
+
x += ix;
|
|
40
|
+
z += iz;
|
|
41
|
+
}
|
|
42
|
+
return [x, z];
|
|
43
|
+
}
|
|
@@ -77,6 +77,7 @@ export declare function createRuntimeSnapshot(args: {
|
|
|
77
77
|
server?: RuntimeServerRow;
|
|
78
78
|
players?: Record<string, RuntimePlayerRow>;
|
|
79
79
|
chunks?: Record<string, RuntimeChunkRow>;
|
|
80
|
+
revision?: number;
|
|
80
81
|
}): GameRuntimeSnapshot;
|
|
81
82
|
export declare function markServerDirty(snapshot: GameRuntimeSnapshot): GameRuntimeSnapshot;
|
|
82
83
|
export declare function markPlayerDirty(snapshot: GameRuntimeSnapshot, userId: string): GameRuntimeSnapshot;
|
package/dist/runtime/snapshot.js
CHANGED