@jgengine/core 0.18.1 → 0.19.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 +158 -7
- package/dist/ai/decisionGraph.d.ts +48 -4
- package/dist/ai/decisionGraph.js +153 -23
- package/dist/ai/groupAssist.d.ts +10 -0
- package/dist/ai/groupAssist.js +11 -0
- package/dist/ai/pursuit.d.ts +1 -1
- package/dist/ai/pursuit.js +1 -1
- package/dist/ai/threat.d.ts +8 -0
- package/dist/ai/threat.js +10 -0
- package/dist/anim/animGraph.d.ts +11 -0
- package/dist/anim/animGraph.js +128 -0
- package/dist/anim/boneTexture.d.ts +55 -0
- package/dist/anim/boneTexture.js +43 -0
- package/dist/anim/footPlacement.d.ts +57 -0
- package/dist/anim/footPlacement.js +67 -0
- package/dist/anim/locomotionGraph.d.ts +9 -0
- package/dist/anim/locomotionGraph.js +29 -0
- package/dist/audio/audioFalloff.d.ts +2 -0
- package/dist/audio/doppler.d.ts +21 -0
- package/dist/audio/doppler.js +32 -0
- package/dist/audio/engineLayers.d.ts +83 -0
- package/dist/audio/engineLayers.js +115 -0
- package/dist/combat/abilityKit.d.ts +1 -0
- package/dist/combat/abilityKit.js +1 -0
- package/dist/combat/death.d.ts +7 -0
- package/dist/combat/shotOrigin.d.ts +6 -0
- package/dist/combat/shotOrigin.js +6 -0
- package/dist/combat/weaponFeedback.d.ts +36 -0
- package/dist/combat/weaponFeedback.js +42 -0
- package/dist/combat/weaponHandling.d.ts +161 -0
- package/dist/combat/weaponHandling.js +202 -0
- package/dist/combat/weaponPresentation.d.ts +115 -0
- package/dist/combat/weaponPresentation.js +96 -0
- package/dist/devtools/warnOnce.d.ts +8 -0
- package/dist/devtools/warnOnce.js +18 -0
- package/dist/editor/document.js +3 -1
- package/dist/editor/environment.js +8 -0
- package/dist/editor/types.d.ts +4 -0
- package/dist/faction/reputation.d.ts +6 -0
- package/dist/faction/reputation.js +11 -0
- package/dist/game/cameraConfig.d.ts +51 -1
- package/dist/game/events.d.ts +6 -0
- package/dist/game/lootTable.d.ts +4 -0
- package/dist/game/lootTable.js +16 -0
- package/dist/game/playableGame.d.ts +84 -17
- package/dist/game/toasts.d.ts +8 -0
- package/dist/game/toasts.js +7 -0
- package/dist/game/vfxInstance.d.ts +9 -0
- package/dist/game/vfxInstance.js +12 -1
- package/dist/game/viewports.d.ts +43 -0
- package/dist/game/viewports.js +46 -0
- package/dist/game/worldItem.d.ts +5 -1
- package/dist/game/worldItem.js +0 -1
- package/dist/input/actionBindings.d.ts +15 -1
- package/dist/input/actionBindings.js +14 -3
- package/dist/input/actionContexts.d.ts +25 -1
- package/dist/input/actionContexts.js +82 -25
- package/dist/input/axisInput.d.ts +4 -0
- package/dist/input/axisInput.js +4 -0
- package/dist/input/axisShaper.d.ts +73 -0
- package/dist/input/axisShaper.js +105 -0
- package/dist/input/gamepadModel.d.ts +36 -2
- package/dist/input/gamepadModel.js +69 -30
- package/dist/input/haptics.d.ts +50 -0
- package/dist/input/haptics.js +74 -0
- package/dist/input/inputBuffer.d.ts +40 -0
- package/dist/input/inputBuffer.js +59 -0
- package/dist/input/lookChannel.d.ts +10 -0
- package/dist/input/lookChannel.js +6 -0
- package/dist/input/pointer.d.ts +2 -0
- package/dist/input/pointer.js +4 -0
- package/dist/item/affix.d.ts +6 -0
- package/dist/item/affix.js +6 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +112 -1
- package/dist/movement/glideModel.d.ts +3 -0
- package/dist/movement/glideModel.js +6 -0
- package/dist/movement/grappleSwing.d.ts +2 -0
- package/dist/movement/grappleSwing.js +5 -1
- package/dist/movement/movementModel.d.ts +39 -0
- package/dist/movement/movementModel.js +62 -19
- package/dist/movement/movementProbe.d.ts +51 -0
- package/dist/movement/movementProbe.js +119 -0
- package/dist/movement/playerMovement.d.ts +21 -2
- package/dist/movement/playerMovement.js +89 -6
- package/dist/movement/solidObstacles.d.ts +9 -1
- package/dist/movement/solidObstacles.js +44 -2
- package/dist/movement/voxelController.js +7 -7
- package/dist/nav/navFromEnvironment.d.ts +9 -3
- package/dist/nav/navFromEnvironment.js +18 -12
- package/dist/nav/navMesh.d.ts +43 -2
- package/dist/nav/navMesh.js +534 -128
- package/dist/physics/aircraftDynamics.d.ts +390 -0
- package/dist/physics/aircraftDynamics.js +704 -0
- package/dist/physics/boatDynamics.d.ts +116 -0
- package/dist/physics/boatDynamics.js +160 -0
- package/dist/physics/drivableVehicle.d.ts +22 -8
- package/dist/physics/drivableVehicle.js +4 -2
- package/dist/physics/flightDynamics.d.ts +21 -4
- package/dist/physics/flightDynamics.js +28 -5
- package/dist/physics/handlingProbe.d.ts +249 -0
- package/dist/physics/handlingProbe.js +606 -0
- package/dist/physics/kinematicVehicle.d.ts +32 -2
- package/dist/physics/kinematicVehicle.js +15 -0
- package/dist/physics/physicsBackend.d.ts +12 -0
- package/dist/physics/physicsBackendConformance.js +34 -0
- package/dist/physics/physicsWorldBackend.d.ts +1 -0
- package/dist/physics/physicsWorldBackend.js +33 -0
- package/dist/physics/vehicleBackendLink.d.ts +49 -0
- package/dist/physics/vehicleBackendLink.js +102 -0
- package/dist/physics/vehicleDynamics.d.ts +374 -0
- package/dist/physics/vehicleDynamics.js +737 -0
- package/dist/physics/worldColliders.d.ts +1 -1
- package/dist/physics/worldColliders.js +35 -3
- package/dist/puzzle/cellGrid.d.ts +15 -9
- package/dist/puzzle/cellGrid.js +13 -9
- package/dist/render/lookPreset.d.ts +9 -2
- package/dist/render/lookPreset.js +29 -2
- package/dist/render/postProcessing.d.ts +30 -0
- package/dist/render/postProcessing.js +1 -1
- package/dist/runtime/cameraDirector.d.ts +17 -3
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/commandInput.d.ts +37 -0
- package/dist/runtime/commandInput.js +58 -0
- package/dist/runtime/commandRunner.d.ts +27 -0
- package/dist/runtime/commandRunner.js +31 -3
- package/dist/runtime/context/combat.d.ts +1 -0
- package/dist/runtime/context/combat.js +2 -1
- package/dist/runtime/context/deathLoot.d.ts +2 -0
- package/dist/runtime/context/deathLoot.js +1 -0
- package/dist/runtime/gameContext.js +7 -0
- package/dist/runtime/gameContextTypes.d.ts +19 -3
- package/dist/runtime/gameRuntime.d.ts +3 -1
- package/dist/runtime/gameRuntime.js +4 -0
- package/dist/runtime/inputSnapshot.d.ts +6 -0
- package/dist/runtime/inputSnapshot.js +10 -0
- package/dist/runtime/localPlayers.d.ts +73 -0
- package/dist/runtime/localPlayers.js +119 -0
- package/dist/runtime/transport.d.ts +9 -0
- package/dist/runtime/transport.js +7 -1
- package/dist/scene/behaviorRuntime.d.ts +12 -2
- package/dist/scene/behaviorRuntime.js +30 -14
- package/dist/scene/behaviors.d.ts +5 -1
- package/dist/scene/sceneKinds.d.ts +9 -0
- package/dist/scene/vehicleSeat.d.ts +5 -1
- package/dist/scene/vehicleSeat.js +5 -1
- package/dist/sensor/concealment.d.ts +6 -0
- package/dist/sensor/concealment.js +8 -0
- package/dist/sensor/freezeMonitor.d.ts +6 -0
- package/dist/sensor/freezeMonitor.js +8 -0
- package/dist/sensor/perception.d.ts +11 -1
- package/dist/sensor/perception.js +57 -10
- package/dist/sensor/recordingBuffer.d.ts +3 -0
- package/dist/sensor/recordingBuffer.js +8 -0
- package/dist/session/roundState.d.ts +4 -0
- package/dist/session/roundState.js +11 -0
- package/dist/settings/frameRateLimit.d.ts +16 -0
- package/dist/settings/frameRateLimit.js +37 -0
- package/dist/settings/graphicsProfile.d.ts +2 -0
- package/dist/settings/graphicsProfile.js +12 -0
- package/dist/settings/graphicsSettings.d.ts +40 -0
- package/dist/settings/graphicsSettings.js +81 -0
- package/dist/settings/settingsModel.d.ts +9 -1
- package/dist/settings/settingsModel.js +10 -0
- package/dist/stats/eventMeter.d.ts +1 -1
- package/dist/stats/eventMeter.js +1 -1
- package/dist/vfx/feedbackMixer.d.ts +61 -0
- package/dist/vfx/feedbackMixer.js +106 -0
- package/dist/visibility/simulationCulling.d.ts +8 -1
- package/dist/visibility/simulationCulling.js +7 -0
- package/dist/visibility/spatialIndex.d.ts +21 -0
- package/dist/visibility/spatialIndex.js +16 -0
- package/dist/world/authoredSolids.d.ts +46 -0
- package/dist/world/authoredSolids.js +75 -0
- package/dist/world/buildingGenerator.js +2 -2
- package/dist/world/buildingKit.d.ts +3 -1
- package/dist/world/buildings.d.ts +34 -2
- package/dist/world/buildings.js +11 -1
- package/dist/world/cityKind.d.ts +7 -0
- package/dist/world/cityKind.js +52 -1
- package/dist/world/features.d.ts +29 -2
- package/dist/world/features.js +6 -0
- package/dist/world/footprintGrid.d.ts +7 -0
- package/dist/world/footprintGrid.js +16 -0
- package/dist/world/interiors.d.ts +1 -0
- package/dist/world/interiors.js +5 -3
- package/dist/world/lod.d.ts +6 -0
- package/dist/world/lod.js +8 -0
- package/dist/world/terraform.d.ts +8 -0
- package/dist/world/terraform.js +7 -0
- package/dist/world/walls.d.ts +7 -0
- package/dist/world/walls.js +7 -0
- package/dist/world/worldSolids.d.ts +84 -0
- package/dist/world/worldSolids.js +240 -0
- package/dist/world.d.ts +2 -2
- package/dist/world.js +1 -1
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +7 -5
- package/skills/jgengine/api.md +118 -89
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-combat/SKILL.md +5 -0
- package/skills/jgengine-combat/api.md +138 -103
- package/skills/jgengine-combat/capabilities.md +29 -1
- package/skills/jgengine-combat/recipes/weapon-feel.md +50 -0
- package/skills/jgengine-gameplay/api.md +462 -436
- package/skills/jgengine-gameplay/capabilities.md +24 -0
- package/skills/jgengine-gameplay/reference-systems.md +4 -0
- package/skills/jgengine-world/SKILL.md +15 -3
- package/skills/jgengine-world/api.md +873 -626
- package/skills/jgengine-world/capabilities.md +130 -6
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
|
@@ -14,6 +14,7 @@ export function applyLethalLoot(input) {
|
|
|
14
14
|
const resolved = resolveDeathDrops([...input.drops], {
|
|
15
15
|
mode: "world",
|
|
16
16
|
origin: input.position,
|
|
17
|
+
rng: input.rng,
|
|
17
18
|
resolveRarity: (itemId) => input.content.itemById?.(itemId)?.rarity ?? DEFAULT_RARITY,
|
|
18
19
|
resolveBaseType: (itemId) => input.content.itemById?.(itemId)?.baseType ?? itemId,
|
|
19
20
|
scatter: normalizedOnDeath.scatter,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createCommandRegistry } from "../commands/commandRegistry.js";
|
|
2
2
|
import {} from "../game/cosmetics.js";
|
|
3
3
|
import { groundFieldFor } from "../world/terrain.js";
|
|
4
|
+
import { createWorldSolids, structureSolids } from "../world/worldSolids.js";
|
|
4
5
|
import { createGameEvents } from "../game/events.js";
|
|
5
6
|
import { createGameFeed } from "../game/feed.js";
|
|
6
7
|
import { setGamePhase } from "../game/gamePhase.js";
|
|
@@ -43,6 +44,10 @@ export function createGameContext(options) {
|
|
|
43
44
|
const activeUserId = () => actingUserId ?? player.userId;
|
|
44
45
|
const time = createSimClock({ config: definition.time, onChange: signal.notify });
|
|
45
46
|
const ground = groundFieldFor(definition.world);
|
|
47
|
+
const solids = createWorldSolids();
|
|
48
|
+
if (definition.world?.kind === "environment") {
|
|
49
|
+
solids.set("environment:structures", structureSolids(definition.world, ground.sampleHeight));
|
|
50
|
+
}
|
|
46
51
|
const rawEvents = createGameEvents();
|
|
47
52
|
const events = {
|
|
48
53
|
on: rawEvents.on,
|
|
@@ -215,6 +220,7 @@ export function createGameContext(options) {
|
|
|
215
220
|
commandRegistry.run(ctxRef, name, args);
|
|
216
221
|
},
|
|
217
222
|
localUserId: player.userId,
|
|
223
|
+
rng,
|
|
218
224
|
...(definition.physics !== undefined ? { physics: definition.physics } : {}),
|
|
219
225
|
});
|
|
220
226
|
scene.setOnAfterSpawn((instanceId) => combat.death.revive(instanceId));
|
|
@@ -406,6 +412,7 @@ export function createGameContext(options) {
|
|
|
406
412
|
world: {
|
|
407
413
|
ground,
|
|
408
414
|
groundHeightAt: ground.sampleHeight,
|
|
415
|
+
solids,
|
|
409
416
|
},
|
|
410
417
|
game: {
|
|
411
418
|
territory: options.territory,
|
|
@@ -11,6 +11,7 @@ import type { ChargeOptions as WalletChargeOptions } from "../economy/wallet.js"
|
|
|
11
11
|
import type { Cosmetics } from "../game/cosmetics.js";
|
|
12
12
|
import type { GameDefinition } from "../game/defineGame.js";
|
|
13
13
|
import type { TerrainField } from "../world/terrain.js";
|
|
14
|
+
import type { WorldSolids } from "../world/worldSolids.js";
|
|
14
15
|
import type { GameEventMap, GameEvents, VfxKind } from "../game/events.js";
|
|
15
16
|
import type { VfxInstanceStore } from "../game/vfxInstance.js";
|
|
16
17
|
import type { GameFeed } from "../game/feed.js";
|
|
@@ -344,6 +345,11 @@ export type WorldItemPickupResult = {
|
|
|
344
345
|
status: "rejected";
|
|
345
346
|
reason: string;
|
|
346
347
|
};
|
|
348
|
+
/**
|
|
349
|
+
* `ctx.scene.worldItem`: items lying in the world — spawn, list, find the nearest, and pick up into a player's bag.
|
|
350
|
+
*
|
|
351
|
+
* @capability world-item-pickup pick up dropped items: ctx.scene.worldItem nearestInRadius → pickup into the player's inventory
|
|
352
|
+
*/
|
|
347
353
|
export interface SceneWorldItemContext {
|
|
348
354
|
spawn(input: WorldItemSpawnInput): WorldItemRecord;
|
|
349
355
|
get(instanceId: string): WorldItemRecord | null;
|
|
@@ -390,6 +396,11 @@ export interface GameContextItemUse {
|
|
|
390
396
|
export interface GameContextWorld {
|
|
391
397
|
ground: TerrainField;
|
|
392
398
|
groundHeightAt(x: number, z: number): number;
|
|
399
|
+
/**
|
|
400
|
+
* Static collision for world geometry that is not a scene object. `environment({ structures })`
|
|
401
|
+
* fills the `environment:structures` layer; `syncAuthoredSolids` fills one layer per studio object.
|
|
402
|
+
*/
|
|
403
|
+
solids: WorldSolids;
|
|
393
404
|
}
|
|
394
405
|
export interface GameContextCards {
|
|
395
406
|
/** Lazily creates (on first call, `config` required) or returns the existing notify-wrapped pile for `id`. */
|
|
@@ -420,14 +431,19 @@ export interface GameAudio {
|
|
|
420
431
|
}): void;
|
|
421
432
|
/**
|
|
422
433
|
* Live-update retained loop `id` via `audio.loopSet`: `rate` re-pitches it (1 = authored pitch, clamped
|
|
423
|
-
* 0.25–4 by the shell), `gain` rescales volume (0–1), `at` repositions its emitter.
|
|
424
|
-
*
|
|
425
|
-
*
|
|
434
|
+
* 0.25–4 by the shell), `gain` rescales volume (0–1), `at` repositions its emitter. `lowpass`/`highpass`
|
|
435
|
+
* set filter cutoffs in Hz (muffle an engine off-throttle or behind a wall); `velocity` is the emitter's
|
|
436
|
+
* world velocity, which pitches the loop by doppler when its sound declares `doppler`. Cheap to call every
|
|
437
|
+
* tick (~60 Hz) — the shell ramps rate/gain/cutoffs over ~20 ms to avoid zipper noise. A no-op when `id`
|
|
438
|
+
* is not a live loop (an update may race a stop) (#1051).
|
|
426
439
|
*/
|
|
427
440
|
setLoop(id: string, params: {
|
|
428
441
|
rate?: number;
|
|
429
442
|
gain?: number;
|
|
430
443
|
at?: readonly [number, number, number];
|
|
444
|
+
velocity?: readonly [number, number, number];
|
|
445
|
+
lowpass?: number;
|
|
446
|
+
highpass?: number;
|
|
431
447
|
}): void;
|
|
432
448
|
/** Stop and dispose retained loop `id` (emits `audio.loopStop`); an unknown `id` is ignored (#1051). */
|
|
433
449
|
stopLoop(id: string): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SaveConfig } from "./save.js";
|
|
2
|
-
import type { CommandDef, CommandScope } from "./commandRunner.js";
|
|
2
|
+
import type { CommandAccess, CommandDef, CommandScope } from "./commandRunner.js";
|
|
3
3
|
import { runCommand } from "./commandRunner.js";
|
|
4
4
|
import { type GameRuntimeSnapshot, type RuntimeChunkRow, type RuntimePlayerRow, type RuntimeProfileRow, type RuntimeServerRow } from "./snapshot.js";
|
|
5
5
|
export type ServerLoopHooks = {
|
|
@@ -69,6 +69,8 @@ export type GameRuntime = {
|
|
|
69
69
|
* and nothing else. Missing scopes load only the actor and no chunks; `{}` explicitly requests all state.
|
|
70
70
|
*/
|
|
71
71
|
commandScope: (commandName: string, input: unknown, actorUserId: string) => CommandScope | undefined;
|
|
72
|
+
/** Who may send a command, or `undefined` when the runtime has no command by that name. */
|
|
73
|
+
commandAccess: (commandName: string) => CommandAccess | undefined;
|
|
72
74
|
/** What a join has to hydrate. `undefined` means the whole world, as when `onNewPlayer` is unscoped. */
|
|
73
75
|
joinScope: (userId: string, isNew: boolean) => CommandScope | undefined;
|
|
74
76
|
/** `nowMs` is the host wall clock at the start of the tick, in ms; defaults to `Date.now()`. */
|
|
@@ -38,6 +38,10 @@ export function createGameRuntime(definition) {
|
|
|
38
38
|
commandScope(commandName, input, actorUserId) {
|
|
39
39
|
return resolveCommandScope(definition.commands, commandName, input, actorUserId);
|
|
40
40
|
},
|
|
41
|
+
commandAccess(commandName) {
|
|
42
|
+
const command = definition.commands[commandName];
|
|
43
|
+
return command === undefined ? undefined : (command.access ?? "client");
|
|
44
|
+
},
|
|
41
45
|
joinScope(userId, isNew) {
|
|
42
46
|
if (!loop?.onNewPlayer)
|
|
43
47
|
return { players: [userId], chunkKeys: [] };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PointerAxisState } from "../input/pointerAxis.js";
|
|
2
2
|
import { type AxisBinding, type AxisRange } from "../input/axisInput.js";
|
|
3
|
+
import { type HapticChannels } from "../input/haptics.js";
|
|
3
4
|
/** One client's input for a tick — the semantic held-action set plus pointer state, the serializable, over-the-wire counterpart of {@link InputSnapshot} the host stores per connected player. */
|
|
4
5
|
export interface InputFrame {
|
|
5
6
|
held: readonly string[];
|
|
@@ -16,6 +17,11 @@ export interface InputSnapshot {
|
|
|
16
17
|
weak: number;
|
|
17
18
|
ms: number;
|
|
18
19
|
}): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Continuous rumble channels for a local user (`engine`, `road`, `impact`, …): set levels each tick
|
|
22
|
+
* and the shell mixes them by priority onto that user's pad. Local only — never in a snapshot or on the wire.
|
|
23
|
+
*/
|
|
24
|
+
haptics(userId: string): HapticChannels;
|
|
19
25
|
/** Replaces the held-action set for this frame, rolling the previous held set forward for edge detection (#671). Called by the shell before `onTick` each frame; does not bump `ctx.version()` or notify `ctx.subscribe` listeners — per-frame publishes would storm subscribers. */
|
|
20
26
|
publish(held: readonly string[]): void;
|
|
21
27
|
/** Replaces the normalized pointer-position state for this frame (#293). Same no-notify contract as `publish`. */
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { sampleAxisBindings } from "../input/axisInput.js";
|
|
2
|
+
import { createHapticChannels } from "../input/haptics.js";
|
|
2
3
|
export function createInputSnapshot() {
|
|
3
4
|
let heldSet = new Set();
|
|
4
5
|
let previousHeldSet = new Set();
|
|
5
6
|
let heldList = [];
|
|
6
7
|
let pointerState = null;
|
|
7
8
|
let analogValues = null;
|
|
9
|
+
const haptics = new Map();
|
|
8
10
|
const value = (action) => {
|
|
9
11
|
const analog = analogValues?.[action];
|
|
10
12
|
if (analog !== undefined)
|
|
@@ -13,6 +15,14 @@ export function createInputSnapshot() {
|
|
|
13
15
|
};
|
|
14
16
|
return {
|
|
15
17
|
rumble: async () => false,
|
|
18
|
+
haptics(userId) {
|
|
19
|
+
let channels = haptics.get(userId);
|
|
20
|
+
if (channels === undefined) {
|
|
21
|
+
channels = createHapticChannels();
|
|
22
|
+
haptics.set(userId, channels);
|
|
23
|
+
}
|
|
24
|
+
return channels;
|
|
25
|
+
},
|
|
16
26
|
publish(held) {
|
|
17
27
|
previousHeldSet = heldSet;
|
|
18
28
|
heldList = Object.freeze([...held]);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { GameContext } from "./gameContext.js";
|
|
2
|
+
import { type InputSnapshot } from "./inputSnapshot.js";
|
|
3
|
+
/** One local seat on a shared screen: a stable slot id, the user id the game spawns for it, and the device driving it. */
|
|
4
|
+
export interface LocalPlayerSlot {
|
|
5
|
+
readonly slotId: string;
|
|
6
|
+
readonly index: number;
|
|
7
|
+
readonly userId: string;
|
|
8
|
+
/** Device that claimed the seat (`"gamepad:1"`), `null` for the primary seat before a device claims it. */
|
|
9
|
+
readonly deviceId: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** Retunable seat policy. */
|
|
12
|
+
export interface LocalPlayersConfig {
|
|
13
|
+
/** Seats on this screen, including the primary (default `1`: every device drives the primary player). */
|
|
14
|
+
maxSlots: number;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the first device to press something claims the primary seat alongside keyboard and touch
|
|
17
|
+
* (`"first"`, default), or the primary stays keyboard/touch-only and every pad opens a new seat (`"none"`).
|
|
18
|
+
*/
|
|
19
|
+
claimPrimary?: "first" | "none";
|
|
20
|
+
}
|
|
21
|
+
/** Options for {@link createLocalPlayers}. */
|
|
22
|
+
export interface LocalPlayersOptions extends LocalPlayersConfig {
|
|
23
|
+
/** User id of seat 0 — `ctx.player.userId`. */
|
|
24
|
+
primaryUserId: string;
|
|
25
|
+
/** Input of seat 0 — `ctx.input`; other seats get their own snapshot. */
|
|
26
|
+
primaryInput?: InputSnapshot;
|
|
27
|
+
/** User id for seat `index >= 1` (default `` `${primaryUserId}:p${index + 1}` ``). */
|
|
28
|
+
userIdFor?: (index: number) => string;
|
|
29
|
+
}
|
|
30
|
+
/** Result of {@link LocalPlayers.assign}. */
|
|
31
|
+
export interface LocalPlayerAssignment {
|
|
32
|
+
slot: LocalPlayerSlot;
|
|
33
|
+
/** True when this call opened the seat: spawn its entity through `onNewPlayer`. */
|
|
34
|
+
joined: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** Serializable seat table. */
|
|
37
|
+
export interface LocalPlayersSnapshot {
|
|
38
|
+
maxSlots: number;
|
|
39
|
+
claimPrimary: "first" | "none";
|
|
40
|
+
slots: LocalPlayerSlot[];
|
|
41
|
+
}
|
|
42
|
+
/** Seats for couch co-op: devices hot-join into slots, each slot has its own input. */
|
|
43
|
+
export interface LocalPlayers {
|
|
44
|
+
/** Seat for a device that just produced input: its existing seat, the primary seat, a newly opened seat, or `null` when full. */
|
|
45
|
+
assign(deviceId: string): LocalPlayerAssignment | null;
|
|
46
|
+
/** Free a seat other than the primary; its device may hot-join again. */
|
|
47
|
+
release(slotId: string): boolean;
|
|
48
|
+
slots(): readonly LocalPlayerSlot[];
|
|
49
|
+
slot(slotId: string): LocalPlayerSlot | null;
|
|
50
|
+
slotForDevice(deviceId: string): LocalPlayerSlot | null;
|
|
51
|
+
/** The seat's user id and input, or `null` for an unknown seat. Seat 0 returns `ctx.player`'s id and `ctx.input`. */
|
|
52
|
+
local(slotId: string): {
|
|
53
|
+
userId: string;
|
|
54
|
+
input: InputSnapshot;
|
|
55
|
+
} | null;
|
|
56
|
+
/** Called after every assign that changes the table, release, restore and retune; returns an unsubscribe. */
|
|
57
|
+
subscribe(listener: () => void): () => void;
|
|
58
|
+
config(): Required<LocalPlayersConfig>;
|
|
59
|
+
/** Change seat count or claim policy mid-game; seats above a lowered `maxSlots` stay until released. */
|
|
60
|
+
retune(next: LocalPlayersConfig): void;
|
|
61
|
+
snapshot(): LocalPlayersSnapshot;
|
|
62
|
+
restore(snapshot: LocalPlayersSnapshot): void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Seat table for local multiplayer on one screen.
|
|
66
|
+
* @capability local-players Couch co-op seats: devices hot-join into slots, each with its own user id and input snapshot.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createLocalPlayers(options: LocalPlayersOptions): LocalPlayers;
|
|
69
|
+
/**
|
|
70
|
+
* The seat table of a game context, created on first use with one seat for `ctx.player` and `ctx.input`.
|
|
71
|
+
* The shell retunes it from `defineGame({ localPlayers })` and hot-joins pads into it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function localPlayers(ctx: GameContext): LocalPlayers;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createInputSnapshot } from "./inputSnapshot.js";
|
|
2
|
+
const PRIMARY_SLOT = "slot:0";
|
|
3
|
+
/**
|
|
4
|
+
* Seat table for local multiplayer on one screen.
|
|
5
|
+
* @capability local-players Couch co-op seats: devices hot-join into slots, each with its own user id and input snapshot.
|
|
6
|
+
*/
|
|
7
|
+
export function createLocalPlayers(options) {
|
|
8
|
+
const userIdFor = options.userIdFor ?? ((index) => `${options.primaryUserId}:p${index + 1}`);
|
|
9
|
+
const primaryInput = options.primaryInput ?? createInputSnapshot();
|
|
10
|
+
let config = {
|
|
11
|
+
maxSlots: Math.max(1, Math.floor(options.maxSlots)),
|
|
12
|
+
claimPrimary: options.claimPrimary ?? "first",
|
|
13
|
+
};
|
|
14
|
+
let slots = [];
|
|
15
|
+
const inputs = new Map();
|
|
16
|
+
const listeners = new Set();
|
|
17
|
+
const changed = () => {
|
|
18
|
+
for (const listener of [...listeners])
|
|
19
|
+
listener();
|
|
20
|
+
};
|
|
21
|
+
const primarySlot = (deviceId) => Object.freeze({ slotId: PRIMARY_SLOT, index: 0, userId: options.primaryUserId, deviceId });
|
|
22
|
+
const reset = (next) => {
|
|
23
|
+
slots = next.map((slot) => Object.freeze({ ...slot }));
|
|
24
|
+
if (!slots.some((slot) => slot.index === 0))
|
|
25
|
+
slots.unshift(primarySlot(null));
|
|
26
|
+
slots.sort((a, b) => a.index - b.index);
|
|
27
|
+
for (const slotId of [...inputs.keys()]) {
|
|
28
|
+
if (!slots.some((slot) => slot.slotId === slotId))
|
|
29
|
+
inputs.delete(slotId);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
reset([]);
|
|
33
|
+
const inputFor = (slot) => {
|
|
34
|
+
if (slot.index === 0)
|
|
35
|
+
return primaryInput;
|
|
36
|
+
let input = inputs.get(slot.slotId);
|
|
37
|
+
if (input === undefined) {
|
|
38
|
+
input = createInputSnapshot();
|
|
39
|
+
inputs.set(slot.slotId, input);
|
|
40
|
+
}
|
|
41
|
+
return input;
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
assign(deviceId) {
|
|
45
|
+
const existing = slots.find((slot) => slot.deviceId === deviceId);
|
|
46
|
+
if (existing !== undefined)
|
|
47
|
+
return { slot: existing, joined: false };
|
|
48
|
+
if (config.claimPrimary === "first" || config.maxSlots === 1) {
|
|
49
|
+
const primary = slots[0];
|
|
50
|
+
if (primary.deviceId === null) {
|
|
51
|
+
slots[0] = primarySlot(deviceId);
|
|
52
|
+
changed();
|
|
53
|
+
return { slot: slots[0], joined: false };
|
|
54
|
+
}
|
|
55
|
+
if (config.maxSlots === 1)
|
|
56
|
+
return { slot: primary, joined: false };
|
|
57
|
+
}
|
|
58
|
+
if (slots.length >= config.maxSlots)
|
|
59
|
+
return null;
|
|
60
|
+
let index = 1;
|
|
61
|
+
while (slots.some((slot) => slot.index === index))
|
|
62
|
+
index += 1;
|
|
63
|
+
const slot = Object.freeze({ slotId: `slot:${index}`, index, userId: userIdFor(index), deviceId });
|
|
64
|
+
slots = [...slots, slot].sort((a, b) => a.index - b.index);
|
|
65
|
+
changed();
|
|
66
|
+
return { slot, joined: true };
|
|
67
|
+
},
|
|
68
|
+
release(slotId) {
|
|
69
|
+
if (slotId === PRIMARY_SLOT)
|
|
70
|
+
return false;
|
|
71
|
+
const next = slots.filter((slot) => slot.slotId !== slotId);
|
|
72
|
+
if (next.length === slots.length)
|
|
73
|
+
return false;
|
|
74
|
+
slots = next;
|
|
75
|
+
inputs.delete(slotId);
|
|
76
|
+
changed();
|
|
77
|
+
return true;
|
|
78
|
+
},
|
|
79
|
+
slots: () => slots,
|
|
80
|
+
slot: (slotId) => slots.find((slot) => slot.slotId === slotId) ?? null,
|
|
81
|
+
slotForDevice: (deviceId) => slots.find((slot) => slot.deviceId === deviceId) ?? null,
|
|
82
|
+
local(slotId) {
|
|
83
|
+
const slot = slots.find((candidate) => candidate.slotId === slotId);
|
|
84
|
+
return slot === undefined ? null : { userId: slot.userId, input: inputFor(slot) };
|
|
85
|
+
},
|
|
86
|
+
subscribe(listener) {
|
|
87
|
+
listeners.add(listener);
|
|
88
|
+
return () => {
|
|
89
|
+
listeners.delete(listener);
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
config: () => config,
|
|
93
|
+
retune(next) {
|
|
94
|
+
config = { maxSlots: Math.max(1, Math.floor(next.maxSlots)), claimPrimary: next.claimPrimary ?? config.claimPrimary };
|
|
95
|
+
changed();
|
|
96
|
+
},
|
|
97
|
+
snapshot() {
|
|
98
|
+
return { ...config, slots: slots.map((slot) => ({ ...slot })) };
|
|
99
|
+
},
|
|
100
|
+
restore(snapshot) {
|
|
101
|
+
config = { maxSlots: Math.max(1, Math.floor(snapshot.maxSlots)), claimPrimary: snapshot.claimPrimary };
|
|
102
|
+
reset(snapshot.slots);
|
|
103
|
+
changed();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const LOCAL_PLAYERS = new WeakMap();
|
|
108
|
+
/**
|
|
109
|
+
* The seat table of a game context, created on first use with one seat for `ctx.player` and `ctx.input`.
|
|
110
|
+
* The shell retunes it from `defineGame({ localPlayers })` and hot-joins pads into it.
|
|
111
|
+
*/
|
|
112
|
+
export function localPlayers(ctx) {
|
|
113
|
+
let players = LOCAL_PLAYERS.get(ctx);
|
|
114
|
+
if (players === undefined) {
|
|
115
|
+
players = createLocalPlayers({ maxSlots: 1, primaryUserId: ctx.player.userId, primaryInput: ctx.input });
|
|
116
|
+
LOCAL_PLAYERS.set(ctx, players);
|
|
117
|
+
}
|
|
118
|
+
return players;
|
|
119
|
+
}
|
|
@@ -63,16 +63,25 @@ export type GameRuntimeFeedView = {
|
|
|
63
63
|
entries: unknown[];
|
|
64
64
|
};
|
|
65
65
|
export type GameRuntimeTransport = {
|
|
66
|
+
/**
|
|
67
|
+
* Join a server. `sessionId` names this client session (one tab, one mounted view) so the host keeps
|
|
68
|
+
* the user a member while any of their sessions is still live.
|
|
69
|
+
*/
|
|
66
70
|
joinServer: (args: {
|
|
67
71
|
gameId: string;
|
|
68
72
|
serverId?: string;
|
|
69
73
|
role?: MultiplayerRole;
|
|
74
|
+
sessionId?: string;
|
|
70
75
|
}) => Promise<JoinServerOutcome>;
|
|
76
|
+
/** Leave a server. With `sessionId` only that session ends; the user leaves once no session remains. Without it the user leaves outright. */
|
|
71
77
|
leaveServer: (args: {
|
|
72
78
|
serverId: string;
|
|
79
|
+
sessionId?: string;
|
|
73
80
|
}) => Promise<void>;
|
|
74
81
|
runCommand: (args: RunCommandArgs) => Promise<TransportRunCommandResult>;
|
|
75
82
|
};
|
|
83
|
+
/** Fresh id for one client session's join/leave pair (see {@link GameRuntimeTransport.joinServer}). */
|
|
84
|
+
export declare function createTransportSessionId(): string;
|
|
76
85
|
export type FeedUnsubscribe = () => void;
|
|
77
86
|
export type GameRuntimeFeeds = {
|
|
78
87
|
subscribeServer: (serverId: string, onChange: (view: GameRuntimeServerView | null) => void) => FeedUnsubscribe;
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/** Fresh id for one client session's join/leave pair (see {@link GameRuntimeTransport.joinServer}). */
|
|
2
|
+
export function createTransportSessionId() {
|
|
3
|
+
const cryptoApi = globalThis.crypto;
|
|
4
|
+
if (cryptoApi?.randomUUID)
|
|
5
|
+
return cryptoApi.randomUUID();
|
|
6
|
+
return `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
7
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DecisionAction, DecisionGraphRuntime } from "../ai/decisionGraph.js";
|
|
1
|
+
import type { Blackboard, DecisionAbort, DecisionAction, DecisionGraphRuntime } from "../ai/decisionGraph.js";
|
|
2
2
|
import { type InterestGateState } from "../ai/interestScheduler.js";
|
|
3
3
|
import { type PathFollowState, type PathProgress, type Waypoint } from "../nav/pathFollow.js";
|
|
4
4
|
import type { GameContext } from "../runtime/gameContext.js";
|
|
@@ -13,10 +13,15 @@ export interface BehaviorActionContext {
|
|
|
13
13
|
}
|
|
14
14
|
/** Callback used by a registered decision graph behavior action. */
|
|
15
15
|
export type BehaviorAction = DecisionAction<BehaviorActionContext>;
|
|
16
|
+
/** Optional hooks for a registered behavior action set. */
|
|
17
|
+
export interface BehaviorActionOptions {
|
|
18
|
+
/** Called when an action running last think is pre-empted by another branch, to stop movement or release claims. */
|
|
19
|
+
onAbort?: DecisionAbort<BehaviorActionContext>;
|
|
20
|
+
}
|
|
16
21
|
/** Register the named actions used by decision graph behavior descriptors.
|
|
17
22
|
* @capability behavior-actions register named callbacks for decision graph behavior descriptors
|
|
18
23
|
*/
|
|
19
|
-
export declare function registerBehaviorActions(id: string, actions: Record<string, BehaviorAction
|
|
24
|
+
export declare function registerBehaviorActions(id: string, actions: Record<string, BehaviorAction>, options?: BehaviorActionOptions): () => void;
|
|
20
25
|
/**
|
|
21
26
|
* Catch-up policy applied when a paused instance resumes. `freeze` (default, deterministic) discards
|
|
22
27
|
* the time spent paused; `advance` silently fast-forwards the instance by the paused duration without
|
|
@@ -36,6 +41,9 @@ export type BehaviorSnapshot = {
|
|
|
36
41
|
readonly kind: "decisionGraph";
|
|
37
42
|
readonly graph: ReturnType<DecisionGraphRuntime["snapshot"]>;
|
|
38
43
|
readonly gate: InterestGateState;
|
|
44
|
+
readonly blackboard?: Blackboard;
|
|
45
|
+
/** Seconds accrued since the last think. */
|
|
46
|
+
readonly pending?: number;
|
|
39
47
|
};
|
|
40
48
|
/** Inspection readout for editor/debug tooling, from {@link BehaviorControl.inspect}/{@link BehaviorControl.list}. */
|
|
41
49
|
export interface BehaviorInspection {
|
|
@@ -72,6 +80,8 @@ export interface BehaviorControl {
|
|
|
72
80
|
inspect(id: string): BehaviorInspection | null;
|
|
73
81
|
/** Inspect every live instance (bounded by spawned behavior entities). */
|
|
74
82
|
list(): BehaviorInspection[];
|
|
83
|
+
/** The live, persistent blackboard of a decision graph instance for perception and game systems to write facts into, or `null`. */
|
|
84
|
+
blackboard(id: string): Blackboard | null;
|
|
75
85
|
}
|
|
76
86
|
/**
|
|
77
87
|
* Advance every spawned entity carrying a `patrol` or `wander` {@link BehaviorDescriptor} one tick — the
|
|
@@ -11,14 +11,17 @@ const behaviorActions = new Map();
|
|
|
11
11
|
/** Register the named actions used by decision graph behavior descriptors.
|
|
12
12
|
* @capability behavior-actions register named callbacks for decision graph behavior descriptors
|
|
13
13
|
*/
|
|
14
|
-
export function registerBehaviorActions(id, actions) {
|
|
15
|
-
|
|
14
|
+
export function registerBehaviorActions(id, actions, options = {}) {
|
|
15
|
+
const entry = { actions, options };
|
|
16
|
+
behaviorActions.set(id, entry);
|
|
16
17
|
return () => {
|
|
17
|
-
if (behaviorActions.get(id) ===
|
|
18
|
+
if (behaviorActions.get(id) === entry)
|
|
18
19
|
behaviorActions.delete(id);
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
+
function thinkCadence(behavior) {
|
|
23
|
+
return { wakeRadius: Number.POSITIVE_INFINITY, activeInterval: Math.max(0, behavior.thinkInterval ?? 0) };
|
|
24
|
+
}
|
|
22
25
|
function patrolOf(entity) {
|
|
23
26
|
return entity.behaviors.find((b) => b.kind === "patrol") ?? null;
|
|
24
27
|
}
|
|
@@ -66,14 +69,21 @@ const runtimeOf = perContext((ctx) => {
|
|
|
66
69
|
}
|
|
67
70
|
const decisionGraph = decisionGraphOf(entity);
|
|
68
71
|
if (decisionGraph !== null) {
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
72
|
+
const registered = behaviorActions.get(decisionGraph.actions);
|
|
73
|
+
if (registered === undefined)
|
|
71
74
|
continue;
|
|
75
|
+
const cadence = thinkCadence(decisionGraph);
|
|
72
76
|
nav.set(entity.id, {
|
|
73
77
|
kind: "decisionGraph",
|
|
74
78
|
behavior: decisionGraph,
|
|
75
|
-
runtime: createDecisionGraphRuntime(decisionGraph.graph, actions
|
|
76
|
-
|
|
79
|
+
runtime: createDecisionGraphRuntime(decisionGraph.graph, registered.actions, {
|
|
80
|
+
rng: () => ctx.rng(),
|
|
81
|
+
...(registered.options.onAbort === undefined ? {} : { onAbort: registered.options.onAbort }),
|
|
82
|
+
}),
|
|
83
|
+
gate: createInterestGateState(cadence, ctx.rng()),
|
|
84
|
+
cadence,
|
|
85
|
+
blackboard: { ...(decisionGraph.blackboard ?? {}) },
|
|
86
|
+
pending: 0,
|
|
77
87
|
status: "active",
|
|
78
88
|
reason: null,
|
|
79
89
|
pausedElapsed: 0,
|
|
@@ -174,13 +184,13 @@ export function advanceBehaviors(ctx, dt) {
|
|
|
174
184
|
const entity = ctx.scene.entity.get(id);
|
|
175
185
|
if (entity === null)
|
|
176
186
|
continue;
|
|
177
|
-
|
|
187
|
+
entry.pending += dt;
|
|
188
|
+
const step = advanceInterestGate(entry.gate, entry.cadence, dt, { proximity: 0 });
|
|
178
189
|
if (!step.active)
|
|
179
190
|
continue;
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
entry.runtime.restore({ runningPath: null });
|
|
191
|
+
const elapsed = entry.pending;
|
|
192
|
+
entry.pending = 0;
|
|
193
|
+
entry.runtime.tick({ ctx, entityId: id, dt: elapsed }, entry.blackboard, elapsed);
|
|
184
194
|
}
|
|
185
195
|
}
|
|
186
196
|
}
|
|
@@ -252,7 +262,7 @@ export function behaviorControl(ctx) {
|
|
|
252
262
|
return { kind: "patrol", state: { ...entry.state } };
|
|
253
263
|
if (entry.kind === "wander")
|
|
254
264
|
return { kind: "wander", origin: [...entry.origin], target: entry.target === null ? null : [...entry.target] };
|
|
255
|
-
return { kind: "decisionGraph", graph: entry.runtime.snapshot(), gate: { ...entry.gate } };
|
|
265
|
+
return { kind: "decisionGraph", graph: entry.runtime.snapshot(), gate: { ...entry.gate }, blackboard: { ...entry.blackboard }, pending: entry.pending };
|
|
256
266
|
},
|
|
257
267
|
restore: (id, snapshot) => {
|
|
258
268
|
const entry = nav.get(id);
|
|
@@ -265,6 +275,8 @@ export function behaviorControl(ctx) {
|
|
|
265
275
|
if (entry.kind === "decisionGraph" && snapshot.kind === "decisionGraph") {
|
|
266
276
|
entry.runtime.restore(snapshot.graph);
|
|
267
277
|
entry.gate = { ...snapshot.gate };
|
|
278
|
+
entry.blackboard = { ...(snapshot.blackboard ?? entry.behavior.blackboard ?? {}) };
|
|
279
|
+
entry.pending = snapshot.pending ?? 0;
|
|
268
280
|
return true;
|
|
269
281
|
}
|
|
270
282
|
if (entry.kind === "wander" && snapshot.kind === "wander") {
|
|
@@ -286,5 +298,9 @@ export function behaviorControl(ctx) {
|
|
|
286
298
|
out.push({ id, kind: entry.kind, status: entry.status, reason: entry.reason });
|
|
287
299
|
return out;
|
|
288
300
|
},
|
|
301
|
+
blackboard: (id) => {
|
|
302
|
+
const entry = nav.get(id);
|
|
303
|
+
return entry?.kind === "decisionGraph" ? entry.blackboard : null;
|
|
304
|
+
},
|
|
289
305
|
};
|
|
290
306
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ProximityPrompt } from "../interaction/proximityPrompt.js";
|
|
2
2
|
import type { PathProgress, Waypoint } from "../nav/pathFollow.js";
|
|
3
|
-
import type { DecisionGraph } from "../ai/decisionGraph.js";
|
|
3
|
+
import type { Blackboard, DecisionGraph } from "../ai/decisionGraph.js";
|
|
4
4
|
export interface WanderBehavior {
|
|
5
5
|
kind: "wander";
|
|
6
6
|
radius: number;
|
|
@@ -33,6 +33,10 @@ export interface DecisionGraphBehavior {
|
|
|
33
33
|
kind: "decisionGraph";
|
|
34
34
|
graph: DecisionGraph;
|
|
35
35
|
actions: string;
|
|
36
|
+
/** Initial facts for this entity's persistent blackboard; systems write more through `behaviorControl(ctx).blackboard(id)`. */
|
|
37
|
+
blackboard?: Blackboard;
|
|
38
|
+
/** Seconds between graph ticks (`0` = every frame). Siblings are staggered; actions receive the elapsed time. */
|
|
39
|
+
thinkInterval?: number;
|
|
36
40
|
}
|
|
37
41
|
export type BehaviorDescriptor = WanderBehavior | PatrolBehavior | PromptableBehavior | PlayerBehavior | DecisionGraphBehavior;
|
|
38
42
|
export declare function wander({ radius }: {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* party ship a new authorable "studio" (pole line, water, bookcase) — schema + resolver here, a
|
|
6
6
|
* matching renderer in `shell` — without editing editor or engine files. Scatter is the proof adopter.
|
|
7
7
|
*/
|
|
8
|
+
import type { WorldSolid } from "../world/worldSolids.js";
|
|
8
9
|
/** Which document collection a scene kind lives in. Drives placement + which `meta` bag holds params. */
|
|
9
10
|
export type SceneKindTarget = "path" | "marker" | "volume";
|
|
10
11
|
/** A closed-polygon area vs an open polyline, for path-target kinds. Ignored for marker/volume kinds. */
|
|
@@ -212,6 +213,12 @@ export interface SceneKindDefinition<TResolved = unknown> {
|
|
|
212
213
|
* marker/volume/path; `params` is `parseParams(schema, object.meta)`.
|
|
213
214
|
*/
|
|
214
215
|
resolve?: (object: SceneKindObject, params: ParsedParams, context: SceneKindResolveContext) => TResolved;
|
|
216
|
+
/**
|
|
217
|
+
* World-space collision for a resolved object. Omitted, the kind is render-only. `syncAuthoredSolids`
|
|
218
|
+
* (`world/authoredSolids`) writes these into `ctx.world.solids`, so movement, NPCs, physics
|
|
219
|
+
* backends and nav grids all collide with what the studio draws.
|
|
220
|
+
*/
|
|
221
|
+
solids?: (resolved: TResolved, object: SceneKindObject, params: ParsedParams, context: SceneKindResolveContext) => readonly WorldSolid[];
|
|
215
222
|
/** Optional one-line readout under the inspector fields (estimate, dimensions) — owned by the studio. */
|
|
216
223
|
note?: (object: SceneKindObject, params: ParsedParams) => string;
|
|
217
224
|
/**
|
|
@@ -258,6 +265,8 @@ export interface SceneKindObject {
|
|
|
258
265
|
export interface SceneKindResolveContext {
|
|
259
266
|
sampleHeight?: (x: number, z: number) => number;
|
|
260
267
|
sampleNormal?: (x: number, z: number) => readonly [number, number, number];
|
|
268
|
+
/** Every studio object in the same document, for kinds that read siblings (a `city` reads its `cityzone` volumes). */
|
|
269
|
+
objects?: readonly SceneKindObject[];
|
|
261
270
|
}
|
|
262
271
|
/**
|
|
263
272
|
* Register a scene kind — the plug-in point for a new parametric studio. Idempotent per `kind` (last
|
|
@@ -64,5 +64,9 @@ export declare class VehicleSeats {
|
|
|
64
64
|
driverOf(vehicleId: string): string | null;
|
|
65
65
|
get mounts(): MountController;
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Builds a {@link VehicleSeats}, optionally over an existing `MountController` to share its occupancy.
|
|
69
|
+
*
|
|
70
|
+
* @capability vehicle-seats enter, exit and swap seats in a car or mount (GTA-style boarding) with driver and passenger slots
|
|
71
|
+
*/
|
|
68
72
|
export declare function createVehicleSeats(controller?: MountController): VehicleSeats;
|
|
@@ -53,7 +53,11 @@ export class VehicleSeats {
|
|
|
53
53
|
return this.controller;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Builds a {@link VehicleSeats}, optionally over an existing `MountController` to share its occupancy.
|
|
58
|
+
*
|
|
59
|
+
* @capability vehicle-seats enter, exit and swap seats in a car or mount (GTA-style boarding) with driver and passenger slots
|
|
60
|
+
*/
|
|
57
61
|
export function createVehicleSeats(controller) {
|
|
58
62
|
return new VehicleSeats(controller);
|
|
59
63
|
}
|
|
@@ -14,9 +14,15 @@ export interface ConcealmentSample {
|
|
|
14
14
|
concealed: boolean;
|
|
15
15
|
dwellSeconds: number;
|
|
16
16
|
}
|
|
17
|
+
/** Plain JSON state of a {@link ConcealmentSensor}: seconds each target has stayed concealed. */
|
|
18
|
+
export interface ConcealmentSensorState {
|
|
19
|
+
dwell: Record<string, number>;
|
|
20
|
+
}
|
|
17
21
|
export interface ConcealmentSensor {
|
|
18
22
|
tick(targets: readonly ConcealmentTarget[], dt: number): ConcealmentSample[];
|
|
19
23
|
reset(id?: string): void;
|
|
24
|
+
snapshot(): ConcealmentSensorState;
|
|
25
|
+
restore(next: ConcealmentSensorState): void;
|
|
20
26
|
}
|
|
21
27
|
/** @internal */
|
|
22
28
|
export declare function createConcealmentSensor(config?: {
|
|
@@ -62,5 +62,13 @@ export function createConcealmentSensor(config) {
|
|
|
62
62
|
else
|
|
63
63
|
dwell.delete(id);
|
|
64
64
|
},
|
|
65
|
+
snapshot() {
|
|
66
|
+
return { dwell: Object.fromEntries(dwell) };
|
|
67
|
+
},
|
|
68
|
+
restore(next) {
|
|
69
|
+
dwell.clear();
|
|
70
|
+
for (const [id, seconds] of Object.entries(next.dwell))
|
|
71
|
+
dwell.set(id, seconds);
|
|
72
|
+
},
|
|
65
73
|
};
|
|
66
74
|
}
|