@jgengine/core 0.7.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 +63 -0
- package/README.md +2 -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.d.ts +5 -0
- package/dist/ai/spawnDirector.js +14 -2
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/cards/cardPile.d.ts +1 -0
- package/dist/cards/cardPile.js +12 -3
- 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 +22 -2
- package/dist/combat/abilityKit.js +72 -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/effects.d.ts +1 -1
- package/dist/combat/effects.js +9 -6
- package/dist/combat/projectiles.d.ts +64 -6
- package/dist/combat/projectiles.js +232 -74
- 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 +30 -1
- package/dist/combat/telegraph.js +61 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/crafting/recipe.js +4 -1
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +205 -0
- package/dist/devtools/devtools.js +832 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- 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 +68 -0
- package/dist/game/chat.js +173 -0
- 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 +10 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +35 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +286 -6
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +44 -1
- package/dist/game/race.js +116 -11
- package/dist/game/recordBook.d.ts +29 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/social.d.ts +42 -0
- package/dist/game/social.js +122 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/game/worldItem.d.ts +3 -0
- package/dist/game/worldItem.js +12 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -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/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +45 -0
- package/dist/input/pointer.js +48 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +89 -0
- package/dist/input/touchScheme.js +153 -0
- package/dist/item/affix.d.ts +1 -1
- package/dist/item/affix.js +1 -19
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +59 -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/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +17 -2
- package/dist/multiplayer/presenceModel.js +30 -4
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +10 -0
- package/dist/nav/navGrid.js +48 -5
- package/dist/nav/pathFollow.d.ts +4 -2
- package/dist/nav/pathFollow.js +5 -3
- 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/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -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 +43 -5
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +54 -8
- package/dist/physics/physicsWorld.js +400 -79
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +5 -0
- package/dist/physics/vehicleBody.d.ts +6 -0
- package/dist/physics/vehicleBody.js +18 -18
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +15 -0
- package/dist/runtime/cameraDirector.js +31 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +65 -2
- package/dist/runtime/gameContext.js +280 -9
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +30 -1
- package/dist/runtime/hostPersistence.js +70 -5
- package/dist/runtime/inputSnapshot.d.ts +12 -0
- package/dist/runtime/inputSnapshot.js +17 -0
- package/dist/runtime/motionIntents.d.ts +33 -0
- package/dist/runtime/motionIntents.js +43 -0
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/transport.d.ts +32 -0
- 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 +30 -5
- package/dist/scene/entityStore.js +83 -13
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +29 -0
- package/dist/scene/objectQuery.js +140 -0
- package/dist/scene/objectStore.d.ts +13 -0
- package/dist/scene/objectStore.js +130 -6
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +17 -1
- package/dist/scene/spatial.js +140 -17
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- 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/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +41 -17
- package/dist/session/roundState.js +67 -38
- 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/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/tacticalGrid.d.ts +11 -0
- package/dist/tactics/tacticalGrid.js +17 -0
- 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/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- 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/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +2 -0
- package/dist/turn/turnLoop.js +8 -1
- 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 +61 -0
- package/dist/world/environmentSummary.js +138 -0
- package/dist/world/features.d.ts +138 -9
- package/dist/world/features.js +73 -2
- package/dist/world/gridInstances.d.ts +12 -0
- package/dist/world/gridInstances.js +160 -0
- 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/placementController.d.ts +22 -1
- package/dist/world/placementController.js +56 -0
- package/dist/world/terrain.d.ts +62 -1
- package/dist/world/terrain.js +253 -9
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- 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 +4983 -0
- package/package.json +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,69 @@ Agents building on the published SDK can also read this programmatically:
|
|
|
11
11
|
same data as typed values, so an updater can diff its installed version against
|
|
12
12
|
the latest and surface the migration steps.
|
|
13
13
|
|
|
14
|
+
## Unreleased
|
|
15
|
+
|
|
16
|
+
## 0.9.0
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Mobile HUD fit — design-resolution UI scaling now applies to every game by default: `HudCanvas` measures the live viewport and scales the whole HUD from `PlayableGame.hudFit.designSize` (default 1600×900, clamps `minScale`/`maxScale` 0.4–1) so desktop-authored layouts shrink to fit a phone instead of overflowing it; `hudFit.mobile` overrides tune the phone fit separately. Pure math lives in `@jgengine/core/ui/hudScale` (`hudScaleForViewport`, `resolveHudFit`, `rectOverflow`, `overflowingPanels`); the shell mounts `@jgengine/react`'s `HudViewportProvider` around `GameUI` so games need no wiring.
|
|
21
|
+
- Graphics → UI scale — a player-facing `graphics.uiScale` slider (0.5–1.5, default 1) multiplies the computed HUD scale on every platform; the same resolution system drives desktop preference and mobile shrink.
|
|
22
|
+
- HUD overflow gate — `HudCanvas` measures every `HudPanel` against the viewport at runtime and reports offenders on a `data-hud-overflow` attribute (plus a console warning); `bun run shoot <game> --device mobile|both` now exits non-zero naming the panels that escape the viewport.
|
|
23
|
+
- Automatic visibility & streaming defaults — an engine-level `VisibilitySystem` (exported from `@jgengine/core`) gives every scene renderer-agnostic culling and asset-streaming policy with no per-object wiring: distance culling, preload margins, hysteresis, delayed unloading, multi-camera awareness (including cameras excluded from streaming), and per-object overrides (always-visible, never-unload, disabled culling/streaming, custom render distance and preload margin).
|
|
24
|
+
- Self-hosted asset mirror — `DEFAULT_RELEASE_BASE` now points at this repo's rolling `packs` release instead of the upstream host; a catalog-driven `mirror-assets` workflow (weekly cron + manual dispatch) keeps the mirror in sync with the asset catalog.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Mobile HUD fit is on by default — design-resolution HUD scaling now applies to every game with no config change (previously opt-in via `platforms: ["web", "mobile"]`). A desktop-only game keeps the legacy fixed 0.85 compact zoom by declaring `platforms: ["web"]` (without `"mobile"`).
|
|
29
|
+
|
|
30
|
+
## 0.8.0
|
|
31
|
+
|
|
32
|
+
Transport-agnostic multiplayer (socket.io, WebRTC p2p, LAN/Fly adapters) plus grid/voxel
|
|
33
|
+
and puzzle primitives, HUD-only presentation, cumulative leveling, and a round of
|
|
34
|
+
controller/camera/sensor additions. **Breaking:** the `gameui` component kit moved out of
|
|
35
|
+
`@jgengine/react` onto the shadcn registry.
|
|
36
|
+
|
|
37
|
+
### Migrate
|
|
38
|
+
|
|
39
|
+
- Bump every `@jgengine/*` dependency to `^0.8.0` (the eight packages version in lockstep).
|
|
40
|
+
- Additive only, except for `gameui` — every other 0.7.0 API is unchanged; opt into any of the below by importing it directly.
|
|
41
|
+
- **Breaking:** replace any `@jgengine/react/gameui` import with the equivalent registry component (`npx shadcn@latest add https://jgengine.com/r/<name>.json`), and swap `GameUiThemeProvider` for `--jg-*` CSS variables on a wrapper element. `GameIcon` and friends moved to `@jgengine/react/gameIcons`.
|
|
42
|
+
- `leveling({ thresholdMode: 'cumulative' })` is opt-in; the default `perLevel` behavior is unchanged.
|
|
43
|
+
- `defineGame.physics.gravity`/`jumpVelocity` are now read by the built-in kinematics controller every frame — if a game already set them expecting a no-op, jump/fall now actually reflects them; omit both to keep the previous defaults.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- Cumulative leveling — `leveling({ thresholdMode: 'cumulative' })` tracks xp as a lifetime total that resolves upward across levels and clamps at the max-level threshold once capped (#12).
|
|
48
|
+
- Direction-aware pool depletion — `EffectSystem.canReceive(instanceId, effect, magnitude?)` takes an optional signed magnitude; negative checks the opposite direction and returns `pools-depleted` only when every stat in the receive order is already at max, so heals reach fully-depleted targets (#168).
|
|
49
|
+
- Puzzle primitives — `puzzle/cellGrid` (uniform-cell boards: line-clear, match-3 cascade, run detection) and `puzzle/fallingPiece` (rotation-state shapes, ghost drop, lock delay, classic gravity/level/score curves) for Tetris/match-3 games (#166).
|
|
50
|
+
- Voxel field — `world/voxelField` (`createVoxelField`, chunked block lattice, neighbors/exposedFaces, 3D DDA raycast, dirty-tracked `chunkVersion`) for voxel games and instanced renderers; assert on `field.summary()` the way environment worlds assert on `summarizeEnvironment` (#166).
|
|
51
|
+
- `defineGame` games may omit `assets` (an empty catalog is injected); `PlayableGame.presentation: 'hud'` mounts no 3D canvas/camera/pointer for board/card/menu games; an `environment()` world auto-renders as the shell's backdrop when `PlayableGame.environment` is unset (#166).
|
|
52
|
+
- Declared-action intent board — `turn/intent` `createIntentBoard` for one-turn-ahead intents (Slay-the-Spire style): declare/peek/all/consume/clear (#168).
|
|
53
|
+
- `turnLoop` lifecycle hooks — `config.onTurnStart`/`onTurnEnd` fire on every `advanceTurn()`; `ctx.game.turn.loop(id, config)` lazily creates/returns a notify-wrapped `TurnLoop` so every mutation (`advanceTurn`/`advancePhase`/`advanceRound`/`spend`/`gain`/`refill`/`setOrder`/...) auto-bumps `ctx.version()` with no manual wiring (#163/#168).
|
|
54
|
+
- `ctx.game.store` — a reactive per-game keyed store (`set`/`delete`/`get`/`has`/`subscribe`/`mapSnapshot`/`arraySnapshot`) plus `@jgengine/react`'s `useGameStore` selector hook, replacing hand-rolled module-level stores for ad-hoc reactive game state; `ctx.game.cards.pile(id, config?)` lazily creates/returns a notify-wrapped `CardPile` the same way; `createCardPile` gained an `onChange` hook for headless use; `CommandDefinition.apply` may return void for side-effect-only commands (#163).
|
|
55
|
+
- Camera — `sideScroll` rig (fixed lateral follow for 2.5D platformers/beat-'em-ups), a `none` rig (no camera mounted; pairs with `PlayableGame.presentation: 'hud'`), `rts.pan: false` (static backdrop camera: no pan/edge-scroll/rotate/zoom, still re-centers on the follow target), and the `observer` rig now defaults to the local player when `bind` is unset (#167).
|
|
56
|
+
- Sensors + session — `sensor/concealment` (`colorDistance`/`concealmentScore`/`createConcealmentSensor`), `sensor/freezeMonitor` (`createFreezeMonitor`), `session/roles` (`assignRoles`); `createRoundState`'s `RoundConfig.teams` accepts per-team roles and an optional `winCondition` that `evaluate()` checks each tick, and takes an optional `phaseOrder` for arbitrary named phase cycles (`concludeRound`/`evaluate` settle only while the current phase is neither the first nor the last entry) (#151).
|
|
57
|
+
- Appearance replication — presence rows carry an optional per-slot appearance channel (cosmetic ids, hex tints, model keys) alongside pose, riding the existing pose message with no protocol bump; wire `ctx.player.cosmetics.get(userId)` into the outgoing pose (#151).
|
|
58
|
+
- `ctx.input` — a per-frame held-action snapshot (`publish(held)`/`isDown(action)`/`held()`) without bumping `ctx.version()`; action bindings gained `repeatMs` (repeat-fire while held); every command resolved from a bound action now carries `aim`; `pointer.secondaryCommand` runs a command on right-click off the same raycast as move/ping (#164).
|
|
59
|
+
- Object spatial queries + entity patching — `ctx.scene.object.at`/`inBox`/`raycast`/`raycastAll` over unit-box objects; `ctx.scene.entity.update(id, patch)` for name/position/rotation/role/movement/behaviors/meta; per-instance `renderObject`/`objectStyles` overrides; `pointerService.worldHitCenter()` + pointer-lock center-ray aiming (#165).
|
|
60
|
+
- Controller movement config — `PlayerMovementConfig` (`mode`: free/axis/grid, `axis`, `cellSize`, `collideObjects`, `beforeCommit` pre-commit hook) for the shell-driven walk controller; `defineGame.physics.gravity`/`jumpVelocity` are honored by the built-in walk controller (distinct from the standalone `physics/physicsWorld` rigid-body sim); `ctx.player.motion.impulse`/`setVerticalVelocity`/`setY`/`takePending` (`MotionIntents`); `entity.spawnPoseOf`/`resetToSpawn`/`resetAllToSpawn` (#162).
|
|
61
|
+
- Model material/animation + paint — `ModelConfig.tint`/`metalness`/`roughness`/`animation` (GLTF clip playback, paused pose holds); `PointerHit.uv` + `pointerService.sampleSurface()` for material-aware picking; `ctx.scene.entity.paint` runtime paint layer, auto-rendered via a per-instance canvas texture with no per-game wiring; remote-player appearance tint recolors the shell's default capsule (#151).
|
|
62
|
+
- **Transport pipe seam** (`@jgengine/ws/pipe`) — `createWsBackend` runs over any bidirectional string channel, not just a raw `WebSocket`: `TransportPipe`/`TransportPipeHandlers`/`TransportPipeFactory`, with `webSocketPipe(url, webSocketFactory?)` as the default. `createWsBackend({ userId, url?, pipe? })` — `url` stays the common case, `pipe` opens the seam to socket.io, WebRTC, and in-process loopback below.
|
|
63
|
+
- **Browser-safe authoritative host** — `createGameHost` and `memoryPersistence` moved from `@jgengine/node` to `@jgengine/ws/host` (zero Node dependencies; `@jgengine/node` re-exports both unchanged from `@jgengine/node/host` / `@jgengine/node/persistence`, so existing imports keep working). `@jgengine/ws/hostRouter`'s `createHostRouter({ host, authenticate?, poseRules?, positionHistoryMs?, chatRateLimit?, chatHistoryLimit?, chatMaxBodyLength?, now? })` extracts the ws wire-protocol session logic out of `createGameWsServer` into a transport-agnostic `HostRouter` (`connect(transport) → { handleRaw, close }`, `rewind`, `close`); `@jgengine/node`'s `createGameWsServer` is now a thin binding of this router onto the `ws` npm package, same public API. `loopbackPipe(router): TransportPipeFactory` connects a `createWsBackend` straight into an in-process router.
|
|
64
|
+
- **Socket.IO transport** — `@jgengine/ws/socketIoPipe` (`SocketIoLikeSocket` structural type, `socketIoPipe(socket)`, `createSocketIoBackend({ socket, userId, … })`) and `@jgengine/node/socketIoServer` (`attachGameSocketIoServer({ io, host, …router options }): { rewind, close }`, structural `SocketIoLikeServer`/`SocketIoLikeServerSocket`) ride the existing ws JSON protocol over socket.io's `send`/`message` frames — no socket.io dependency in either package's types. New `socketIo({ topology?, url? })` adapter in `@jgengine/core/runtime/adapter`.
|
|
65
|
+
- **WebRTC peer-to-peer** (`@jgengine/ws/peer`) — one browser tab hosts, authoritatively, with no server process. `createPeerHost({ userId, host?, runtimes?, persistence?, tickMs?, router?, rtc? })` runs a `GameHost` + `HostRouter` in the host tab (`backend` is the host player's own loopback connection, `accept(offerCode) → Promise<answerCode>` per guest); `createPeerGuest({ userId, token?, rtc? })` offers/connects from the joining side. `encodePeerSignal`/`decodePeerSignal` turn SDP into copy-pasteable base64url codes for manual cross-machine signaling; `broadcastChannelSignaling(room)` automates it for same-origin multi-tab play, with `announcePeerHost`/`joinPeerSession` wiring host/guest to a `PeerSignaling` in one call. New `p2p({ topology?, room? })` adapter (topology defaults `"private"`).
|
|
66
|
+
- **LAN adapter + Fly sugar** — `lan({ topology?, port?, path? })` in `@jgengine/core/runtime/adapter` resolves through `@jgengine/shell/multiplayer`'s `resolveShellMultiplayer` to `ws(s)://<page hostname>:<port ?? 8080><path ?? /ws>` derived from `window.location`, so any browser on the LAN auto-connects to whichever machine served the page — no URL configuration. `fly({ app, topology?, path? })` is `ws` sugar for a Fly.io deploy: resolves to `wss://<app>.fly.dev<path ?? "/ws">`. `apps/dev`'s Vite server now listens on the network (`server: { host: true }`) and exposes `?p2p=host` / `?p2p=join` query params wired through the new `resolvePeerShellMultiplayer({ gameId, role, room?, userId?, feedActions? })`.
|
|
67
|
+
- `ws()` (`@jgengine/core/runtime/adapter`) gained an optional `url` field, carried through by `resolveShellMultiplayer` (`args.url ?? adapter.url ?? default`).
|
|
68
|
+
|
|
69
|
+
### Removed
|
|
70
|
+
|
|
71
|
+
- **The `gameui` component kit** (`@jgengine/react/gameui`) — the themed HUD kit (bars, slots, feedback, meters, panels, screens, reticles, icons) and its `GameUiThemeProvider`/`useGameUiTheme` theming have been removed. **Breaking** for anyone importing `@jgengine/react/gameui` (or its subpaths/barrel). The components now ship as installable shadcn registry items at `https://jgengine.com/r/<name>.json` (`npx shadcn@latest add https://jgengine.com/r/<name>.json`), styled with Tailwind + `--jg-*` CSS variables instead of a theme object. The icon catalog (`GameIcon`, `iconForAction`, `iconForItemId`, `isGameIconName`, `GameIconName`) moved to `@jgengine/react/gameIcons`. To theme, set the `--jg-*` variables on a wrapper element (the registry's `jg-theme` presets mirror the old `ember`/`synthwave`/`fieldkit` palettes) instead of wrapping in `GameUiThemeProvider`.
|
|
72
|
+
|
|
73
|
+
### Docs
|
|
74
|
+
|
|
75
|
+
- Added [CREDITS.md](CREDITS.md) crediting [achrefelouafi](https://github.com/achrefelouafi) for the MIT Three.js reference projects behind the procedural building, water, rain, and snow renderers, with links from the root, `@jgengine/core`, and `@jgengine/shell` READMEs.
|
|
76
|
+
|
|
14
77
|
## 0.7.0
|
|
15
78
|
|
|
16
79
|
The engine-gaps release — 22 system-level additions across turn/tactics, cards & boards,
|
package/README.md
CHANGED
|
@@ -8,4 +8,6 @@ Import modules by path:
|
|
|
8
8
|
import { createGameRuntime } from "@jgengine/core/runtime/gameRuntime";
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
The renderer-free world layer includes a Gerstner `waterSurface` (`world/water`) and a seeded `generateBuilding` kit (`world/buildings`), whose wave math and building component vocabulary were shaped from **[achrefelouafi](https://github.com/achrefelouafi)**'s MIT [OceanThreejs](https://github.com/achrefelouafi/OceanThreejs) and [BuildingGeneratorThreeJS](https://github.com/achrefelouafi/BuildingGeneratorThreeJS) — see [CREDITS.md](../../CREDITS.md).
|
|
12
|
+
|
|
11
13
|
Part of [JGengine](https://github.com/Noisemaker111/jgengine). AGPL-3.0-only.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type FlockVec3 = readonly [number, number, number];
|
|
2
|
+
export interface FlockAgent {
|
|
3
|
+
position: FlockVec3;
|
|
4
|
+
velocity: FlockVec3;
|
|
5
|
+
}
|
|
6
|
+
export interface FlockConfig {
|
|
7
|
+
maxSpeed: number;
|
|
8
|
+
/** How hard steering can change velocity (units/s²); default `maxSpeed * 2`. */
|
|
9
|
+
accel?: number;
|
|
10
|
+
separationRadius: number;
|
|
11
|
+
separationWeight?: number;
|
|
12
|
+
/** Neighbors inside this radius pull toward the local centroid and average heading. */
|
|
13
|
+
neighborRadius: number;
|
|
14
|
+
cohesionWeight?: number;
|
|
15
|
+
alignmentWeight?: number;
|
|
16
|
+
/** Pull toward the seek target; default `1`. */
|
|
17
|
+
seekWeight?: number;
|
|
18
|
+
/** Beyond this distance from the flock centroid an agent's cohesion is boosted so stragglers rejoin. */
|
|
19
|
+
stragglerRadius?: number;
|
|
20
|
+
/** Cohesion multiplier applied to stragglers; default `3`. */
|
|
21
|
+
stragglerBoost?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Classic per-agent boid steering — seek + separation + cohesion + alignment with a straggler
|
|
25
|
+
* rescue radius — the many-agents-to-*each-other* primitive `ai/crowd`'s navmesh flow field
|
|
26
|
+
* (many-agents-to-POI) deliberately isn't. Pure: returns the desired acceleration for one agent;
|
|
27
|
+
* `stepFlock` is the convenience integrator. O(neighbors) per agent — pre-filter with a spatial
|
|
28
|
+
* grid past a few hundred agents.
|
|
29
|
+
*/
|
|
30
|
+
export declare function flockSteer(agent: FlockAgent, neighbors: readonly FlockAgent[], config: FlockConfig, target?: FlockVec3): FlockVec3;
|
|
31
|
+
export interface FlockStepAgent {
|
|
32
|
+
position: FlockVec3;
|
|
33
|
+
velocity: FlockVec3;
|
|
34
|
+
}
|
|
35
|
+
/** Integrate one tick in place: steer every agent, clamp to `maxSpeed`, advance positions. */
|
|
36
|
+
export declare function stepFlock(agents: FlockStepAgent[], config: FlockConfig, dt: number, target?: FlockVec3): void;
|
package/dist/ai/flock.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classic per-agent boid steering — seek + separation + cohesion + alignment with a straggler
|
|
3
|
+
* rescue radius — the many-agents-to-*each-other* primitive `ai/crowd`'s navmesh flow field
|
|
4
|
+
* (many-agents-to-POI) deliberately isn't. Pure: returns the desired acceleration for one agent;
|
|
5
|
+
* `stepFlock` is the convenience integrator. O(neighbors) per agent — pre-filter with a spatial
|
|
6
|
+
* grid past a few hundred agents.
|
|
7
|
+
*/
|
|
8
|
+
export function flockSteer(agent, neighbors, config, target) {
|
|
9
|
+
const separationWeight = config.separationWeight ?? 1.5;
|
|
10
|
+
const cohesionWeight = config.cohesionWeight ?? 1;
|
|
11
|
+
const alignmentWeight = config.alignmentWeight ?? 1;
|
|
12
|
+
const seekWeight = config.seekWeight ?? 1;
|
|
13
|
+
const accel = config.accel ?? config.maxSpeed * 2;
|
|
14
|
+
let sepX = 0;
|
|
15
|
+
let sepY = 0;
|
|
16
|
+
let sepZ = 0;
|
|
17
|
+
let cohX = 0;
|
|
18
|
+
let cohY = 0;
|
|
19
|
+
let cohZ = 0;
|
|
20
|
+
let alignX = 0;
|
|
21
|
+
let alignY = 0;
|
|
22
|
+
let alignZ = 0;
|
|
23
|
+
let flockmates = 0;
|
|
24
|
+
for (const other of neighbors) {
|
|
25
|
+
if (other === agent)
|
|
26
|
+
continue;
|
|
27
|
+
const dx = other.position[0] - agent.position[0];
|
|
28
|
+
const dy = other.position[1] - agent.position[1];
|
|
29
|
+
const dz = other.position[2] - agent.position[2];
|
|
30
|
+
const distance = Math.hypot(dx, dy, dz);
|
|
31
|
+
if (distance > config.neighborRadius)
|
|
32
|
+
continue;
|
|
33
|
+
flockmates += 1;
|
|
34
|
+
cohX += other.position[0];
|
|
35
|
+
cohY += other.position[1];
|
|
36
|
+
cohZ += other.position[2];
|
|
37
|
+
alignX += other.velocity[0];
|
|
38
|
+
alignY += other.velocity[1];
|
|
39
|
+
alignZ += other.velocity[2];
|
|
40
|
+
if (distance < config.separationRadius && distance > 1e-9) {
|
|
41
|
+
const push = (config.separationRadius - distance) / config.separationRadius / distance;
|
|
42
|
+
sepX -= dx * push;
|
|
43
|
+
sepY -= dy * push;
|
|
44
|
+
sepZ -= dz * push;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
let steerX = sepX * separationWeight;
|
|
48
|
+
let steerY = sepY * separationWeight;
|
|
49
|
+
let steerZ = sepZ * separationWeight;
|
|
50
|
+
if (flockmates > 0) {
|
|
51
|
+
const centroidX = cohX / flockmates;
|
|
52
|
+
const centroidY = cohY / flockmates;
|
|
53
|
+
const centroidZ = cohZ / flockmates;
|
|
54
|
+
const toCentroidX = centroidX - agent.position[0];
|
|
55
|
+
const toCentroidY = centroidY - agent.position[1];
|
|
56
|
+
const toCentroidZ = centroidZ - agent.position[2];
|
|
57
|
+
const centroidDistance = Math.hypot(toCentroidX, toCentroidY, toCentroidZ);
|
|
58
|
+
const straggling = config.stragglerRadius !== undefined && centroidDistance > config.stragglerRadius;
|
|
59
|
+
const cohesion = cohesionWeight * (straggling ? config.stragglerBoost ?? 3 : 1);
|
|
60
|
+
if (centroidDistance > 1e-9) {
|
|
61
|
+
steerX += (toCentroidX / centroidDistance) * cohesion;
|
|
62
|
+
steerY += (toCentroidY / centroidDistance) * cohesion;
|
|
63
|
+
steerZ += (toCentroidZ / centroidDistance) * cohesion;
|
|
64
|
+
}
|
|
65
|
+
const alignMagnitude = Math.hypot(alignX, alignY, alignZ);
|
|
66
|
+
if (alignMagnitude > 1e-9) {
|
|
67
|
+
steerX += (alignX / alignMagnitude) * alignmentWeight;
|
|
68
|
+
steerY += (alignY / alignMagnitude) * alignmentWeight;
|
|
69
|
+
steerZ += (alignZ / alignMagnitude) * alignmentWeight;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (target !== undefined) {
|
|
73
|
+
const toTargetX = target[0] - agent.position[0];
|
|
74
|
+
const toTargetY = target[1] - agent.position[1];
|
|
75
|
+
const toTargetZ = target[2] - agent.position[2];
|
|
76
|
+
const targetDistance = Math.hypot(toTargetX, toTargetY, toTargetZ);
|
|
77
|
+
if (targetDistance > 1e-9) {
|
|
78
|
+
steerX += (toTargetX / targetDistance) * seekWeight;
|
|
79
|
+
steerY += (toTargetY / targetDistance) * seekWeight;
|
|
80
|
+
steerZ += (toTargetZ / targetDistance) * seekWeight;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const magnitude = Math.hypot(steerX, steerY, steerZ);
|
|
84
|
+
if (magnitude <= 1e-9)
|
|
85
|
+
return [0, 0, 0];
|
|
86
|
+
const scale = accel / magnitude;
|
|
87
|
+
return [steerX * scale, steerY * scale, steerZ * scale];
|
|
88
|
+
}
|
|
89
|
+
const CELL_KEY_STRIDE = 1_048_576;
|
|
90
|
+
const gridCells = new Map();
|
|
91
|
+
const freeBuckets = [];
|
|
92
|
+
const neighborScratch = [];
|
|
93
|
+
let steerScratch = new Float64Array(0);
|
|
94
|
+
function cellKey(cx, cz) {
|
|
95
|
+
return cx * CELL_KEY_STRIDE + cz;
|
|
96
|
+
}
|
|
97
|
+
function takeBucket() {
|
|
98
|
+
return freeBuckets.pop() ?? [];
|
|
99
|
+
}
|
|
100
|
+
function ensureSteerCapacity(agentCount) {
|
|
101
|
+
const needed = agentCount * 3;
|
|
102
|
+
if (steerScratch.length < needed) {
|
|
103
|
+
steerScratch = new Float64Array(needed);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function buildAgentGrid(agents, invCell) {
|
|
107
|
+
for (const bucket of gridCells.values()) {
|
|
108
|
+
bucket.length = 0;
|
|
109
|
+
freeBuckets.push(bucket);
|
|
110
|
+
}
|
|
111
|
+
gridCells.clear();
|
|
112
|
+
for (let i = 0; i < agents.length; i += 1) {
|
|
113
|
+
const position = agents[i].position;
|
|
114
|
+
const key = cellKey(Math.floor(position[0] * invCell), Math.floor(position[2] * invCell));
|
|
115
|
+
let bucket = gridCells.get(key);
|
|
116
|
+
if (bucket === undefined) {
|
|
117
|
+
bucket = takeBucket();
|
|
118
|
+
gridCells.set(key, bucket);
|
|
119
|
+
}
|
|
120
|
+
bucket.push(i);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function fillNeighbors(agents, agentIndex, invCell) {
|
|
124
|
+
const agent = agents[agentIndex];
|
|
125
|
+
const cx = Math.floor(agent.position[0] * invCell);
|
|
126
|
+
const cz = Math.floor(agent.position[2] * invCell);
|
|
127
|
+
let count = 0;
|
|
128
|
+
for (let dz = -1; dz <= 1; dz += 1) {
|
|
129
|
+
for (let dx = -1; dx <= 1; dx += 1) {
|
|
130
|
+
const bucket = gridCells.get(cellKey(cx + dx, cz + dz));
|
|
131
|
+
if (bucket === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
for (let b = 0; b < bucket.length; b += 1) {
|
|
134
|
+
const otherIndex = bucket[b];
|
|
135
|
+
if (otherIndex === agentIndex)
|
|
136
|
+
continue;
|
|
137
|
+
neighborScratch[count] = agents[otherIndex];
|
|
138
|
+
count += 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
neighborScratch.length = count;
|
|
143
|
+
return count;
|
|
144
|
+
}
|
|
145
|
+
/** Integrate one tick in place: steer every agent, clamp to `maxSpeed`, advance positions. */
|
|
146
|
+
export function stepFlock(agents, config, dt, target) {
|
|
147
|
+
const count = agents.length;
|
|
148
|
+
if (count === 0)
|
|
149
|
+
return;
|
|
150
|
+
const cellSize = config.neighborRadius > 1e-6 ? config.neighborRadius : 1;
|
|
151
|
+
const invCell = 1 / cellSize;
|
|
152
|
+
buildAgentGrid(agents, invCell);
|
|
153
|
+
ensureSteerCapacity(count);
|
|
154
|
+
for (let i = 0; i < count; i += 1) {
|
|
155
|
+
fillNeighbors(agents, i, invCell);
|
|
156
|
+
const steer = flockSteer(agents[i], neighborScratch, config, target);
|
|
157
|
+
const base = i * 3;
|
|
158
|
+
steerScratch[base] = steer[0];
|
|
159
|
+
steerScratch[base + 1] = steer[1];
|
|
160
|
+
steerScratch[base + 2] = steer[2];
|
|
161
|
+
}
|
|
162
|
+
for (let i = 0; i < count; i += 1) {
|
|
163
|
+
const agent = agents[i];
|
|
164
|
+
const base = i * 3;
|
|
165
|
+
let vx = agent.velocity[0] + steerScratch[base] * dt;
|
|
166
|
+
let vy = agent.velocity[1] + steerScratch[base + 1] * dt;
|
|
167
|
+
let vz = agent.velocity[2] + steerScratch[base + 2] * dt;
|
|
168
|
+
const speed = Math.hypot(vx, vy, vz);
|
|
169
|
+
if (speed > config.maxSpeed) {
|
|
170
|
+
const clamp = config.maxSpeed / speed;
|
|
171
|
+
vx *= clamp;
|
|
172
|
+
vy *= clamp;
|
|
173
|
+
vz *= clamp;
|
|
174
|
+
}
|
|
175
|
+
agent.velocity = [vx, vy, vz];
|
|
176
|
+
agent.position = [
|
|
177
|
+
agent.position[0] + vx * dt,
|
|
178
|
+
agent.position[1] + vy * dt,
|
|
179
|
+
agent.position[2] + vz * dt,
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ThreatTable } from "./threat.js";
|
|
2
|
+
export interface AssistMember {
|
|
3
|
+
id: string;
|
|
4
|
+
groupId: string;
|
|
5
|
+
table: ThreatTable;
|
|
6
|
+
}
|
|
7
|
+
export interface AssistNetworkConfig {
|
|
8
|
+
radius?: number;
|
|
9
|
+
shareFraction?: number;
|
|
10
|
+
distanceBetween?: (a: string, b: string) => number;
|
|
11
|
+
}
|
|
12
|
+
export interface AssistNetwork {
|
|
13
|
+
register(member: AssistMember): void;
|
|
14
|
+
remove(memberId: string): void;
|
|
15
|
+
memberIds(groupId?: string): string[];
|
|
16
|
+
assistersOf(memberId: string): string[];
|
|
17
|
+
addThreat(memberId: string, sourceId: string, amount: number): string[];
|
|
18
|
+
}
|
|
19
|
+
export declare function createAssistNetwork(config?: AssistNetworkConfig): AssistNetwork;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function createAssistNetwork(config = {}) {
|
|
2
|
+
const radius = config.radius;
|
|
3
|
+
const shareFraction = Math.max(0, config.shareFraction ?? 1);
|
|
4
|
+
const distanceBetween = config.distanceBetween;
|
|
5
|
+
const members = new Map();
|
|
6
|
+
const order = [];
|
|
7
|
+
function inRange(memberId, otherId) {
|
|
8
|
+
if (radius === undefined || !distanceBetween)
|
|
9
|
+
return true;
|
|
10
|
+
return distanceBetween(memberId, otherId) <= radius;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
register(member) {
|
|
14
|
+
if (!members.has(member.id))
|
|
15
|
+
order.push(member.id);
|
|
16
|
+
members.set(member.id, member);
|
|
17
|
+
},
|
|
18
|
+
remove(memberId) {
|
|
19
|
+
if (!members.delete(memberId))
|
|
20
|
+
return;
|
|
21
|
+
const index = order.indexOf(memberId);
|
|
22
|
+
if (index !== -1)
|
|
23
|
+
order.splice(index, 1);
|
|
24
|
+
},
|
|
25
|
+
memberIds(groupId) {
|
|
26
|
+
if (groupId === undefined)
|
|
27
|
+
return [...order];
|
|
28
|
+
return order.filter((id) => members.get(id).groupId === groupId);
|
|
29
|
+
},
|
|
30
|
+
assistersOf(memberId) {
|
|
31
|
+
const member = members.get(memberId);
|
|
32
|
+
if (!member)
|
|
33
|
+
return [];
|
|
34
|
+
return order.filter((id) => {
|
|
35
|
+
if (id === memberId)
|
|
36
|
+
return false;
|
|
37
|
+
const other = members.get(id);
|
|
38
|
+
return other.groupId === member.groupId && inRange(memberId, id);
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
addThreat(memberId, sourceId, amount) {
|
|
42
|
+
const member = members.get(memberId);
|
|
43
|
+
if (!member || amount <= 0)
|
|
44
|
+
return [];
|
|
45
|
+
member.table.add(sourceId, amount);
|
|
46
|
+
if (shareFraction <= 0)
|
|
47
|
+
return [];
|
|
48
|
+
const assisters = order.filter((id) => {
|
|
49
|
+
if (id === memberId)
|
|
50
|
+
return false;
|
|
51
|
+
const other = members.get(id);
|
|
52
|
+
return other.groupId === member.groupId && inRange(memberId, id);
|
|
53
|
+
});
|
|
54
|
+
for (const id of assisters)
|
|
55
|
+
members.get(id).table.add(sourceId, amount * shareFraction);
|
|
56
|
+
return assisters;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface LaneCandidate<TId extends string = string> {
|
|
2
|
+
id: TId;
|
|
3
|
+
cost: number;
|
|
4
|
+
}
|
|
5
|
+
export interface PickLaneOptions<TId extends string = string> {
|
|
6
|
+
/** The lane currently taken — kept unless a rival is decisively cheaper. */
|
|
7
|
+
current?: TId | null;
|
|
8
|
+
/** How much cheaper a rival must be to displace `current`: it wins only when `cost < currentCost / stickiness`. Default `1.2`. */
|
|
9
|
+
stickiness?: number;
|
|
10
|
+
/** Candidates within this cost of the best tie for the win; the tie is broken by `rng`. Default `0`. */
|
|
11
|
+
tieEpsilon?: number;
|
|
12
|
+
rng?: () => number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Live cost-based selection over parallel corridors (#286.7) — the racing-line / lane-merge pick
|
|
16
|
+
* every AI driver hand-rolled. Evaluate each lane's cost this tick (crowding, hazards, distance)
|
|
17
|
+
* and let stickiness hysteresis stop the agent flip-flopping between near-equal lanes.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pickLane<TId extends string>(candidates: readonly LaneCandidate<TId>[], options?: PickLaneOptions<TId>): TId | null;
|
|
20
|
+
/** Sum a live cost function over a corridor's sample points — the standard way to build `LaneCandidate.cost`. */
|
|
21
|
+
export declare function corridorCost(points: readonly (readonly [number, number])[], costAt: (x: number, z: number) => number): number;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live cost-based selection over parallel corridors (#286.7) — the racing-line / lane-merge pick
|
|
3
|
+
* every AI driver hand-rolled. Evaluate each lane's cost this tick (crowding, hazards, distance)
|
|
4
|
+
* and let stickiness hysteresis stop the agent flip-flopping between near-equal lanes.
|
|
5
|
+
*/
|
|
6
|
+
export function pickLane(candidates, options = {}) {
|
|
7
|
+
if (candidates.length === 0)
|
|
8
|
+
return null;
|
|
9
|
+
const stickiness = options.stickiness ?? 1.2;
|
|
10
|
+
const tieEpsilon = options.tieEpsilon ?? 0;
|
|
11
|
+
const rng = options.rng ?? Math.random;
|
|
12
|
+
let best = candidates[0];
|
|
13
|
+
for (const candidate of candidates) {
|
|
14
|
+
if (candidate.cost < best.cost)
|
|
15
|
+
best = candidate;
|
|
16
|
+
}
|
|
17
|
+
if (tieEpsilon > 0) {
|
|
18
|
+
const tied = candidates.filter((candidate) => candidate.cost <= best.cost + tieEpsilon);
|
|
19
|
+
if (tied.length > 1)
|
|
20
|
+
best = tied[Math.min(tied.length - 1, Math.floor(rng() * tied.length))];
|
|
21
|
+
}
|
|
22
|
+
const current = options.current ?? null;
|
|
23
|
+
if (current !== null && current !== best.id) {
|
|
24
|
+
const currentCandidate = candidates.find((candidate) => candidate.id === current);
|
|
25
|
+
if (currentCandidate !== undefined && best.cost >= currentCandidate.cost / stickiness) {
|
|
26
|
+
return current;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return best.id;
|
|
30
|
+
}
|
|
31
|
+
/** Sum a live cost function over a corridor's sample points — the standard way to build `LaneCandidate.cost`. */
|
|
32
|
+
export function corridorCost(points, costAt) {
|
|
33
|
+
let total = 0;
|
|
34
|
+
for (const [x, z] of points)
|
|
35
|
+
total += costAt(x, z);
|
|
36
|
+
return total;
|
|
37
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type ThreatTable, type ThreatTableConfig } from "./threat.js";
|
|
2
|
+
export type MobBrainMode = "idle" | "wander" | "chase" | "engage" | "evade";
|
|
3
|
+
export type MobVec3 = readonly [number, number, number];
|
|
4
|
+
export interface MobWanderConfig {
|
|
5
|
+
/** Radius around home the wander points are drawn from. */
|
|
6
|
+
radius: number;
|
|
7
|
+
/** Seconds between wander-point picks; default `4`. */
|
|
8
|
+
intervalSeconds?: number;
|
|
9
|
+
/** Movement speed multiplier while wandering; default `0.35`. */
|
|
10
|
+
speedScale?: number;
|
|
11
|
+
/** Distance at which a wander point counts as reached; default `0.6`. */
|
|
12
|
+
arriveRadius?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface MobBrainConfig {
|
|
15
|
+
/** Distance at which an unaggroed candidate pulls threat and becomes the target. */
|
|
16
|
+
aggroRadius: number;
|
|
17
|
+
/** Distance inside which the step reports `inAttackRange` and stops chasing. */
|
|
18
|
+
attackRange: number;
|
|
19
|
+
/** Distance from home beyond which the mob drops all threat and evades back. */
|
|
20
|
+
leashDistance: number;
|
|
21
|
+
/** Idle wandering around home; omit or `false` to stand still. */
|
|
22
|
+
wander?: MobWanderConfig | false;
|
|
23
|
+
/** Movement speed multiplier while evading home; default `1.4`. */
|
|
24
|
+
evadeSpeedScale?: number;
|
|
25
|
+
/** Target-switch stickiness passed to the threat table's `highest`; default `1.15`. */
|
|
26
|
+
stickiness?: number;
|
|
27
|
+
/** Distance to home at which an evade completes; default `1.2`. */
|
|
28
|
+
homeArriveRadius?: number;
|
|
29
|
+
threat?: ThreatTableConfig;
|
|
30
|
+
}
|
|
31
|
+
export interface MobBrainDeps {
|
|
32
|
+
home: MobVec3;
|
|
33
|
+
/** The mob's current position; `null` pauses the brain (despawned). */
|
|
34
|
+
position(): MobVec3 | null;
|
|
35
|
+
/** A target's current position; `null` drops it from the threat table (despawned, dead). */
|
|
36
|
+
targetPosition(targetId: string): MobVec3 | null;
|
|
37
|
+
/** Potential aggro targets near the mob — the brain distance-filters against `aggroRadius`. */
|
|
38
|
+
candidates(): readonly string[];
|
|
39
|
+
rng?: () => number;
|
|
40
|
+
}
|
|
41
|
+
export interface MobBrainStep {
|
|
42
|
+
mode: MobBrainMode;
|
|
43
|
+
targetId: string | null;
|
|
44
|
+
/** Where to step this tick — feed to `moveToward`; `null` holds position. */
|
|
45
|
+
moveTo: MobVec3 | null;
|
|
46
|
+
/** Multiplier on the mob's base move speed for this step. */
|
|
47
|
+
speedScale: number;
|
|
48
|
+
/** The target is inside `attackRange` — the swing/cast window. */
|
|
49
|
+
inAttackRange: boolean;
|
|
50
|
+
/** Fires on the single step an evade run reaches home — the reset/full-heal hook. */
|
|
51
|
+
arrivedHome: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The wander → aggro → chase → engage → leash-evade loop every MMO-shaped game hand-rolls,
|
|
55
|
+
* composed over `ai/threat`. The brain decides intent; the game executes it (`moveToward`,
|
|
56
|
+
* ground-snap, facing, swings) and routes damage into `addThreat`. Pack aggro is game-side:
|
|
57
|
+
* call `addThreat` on nearby packmates' brains when one aggros.
|
|
58
|
+
*/
|
|
59
|
+
export interface MobBrain {
|
|
60
|
+
readonly threat: ThreatTable;
|
|
61
|
+
mode(): MobBrainMode;
|
|
62
|
+
/** Route damage/heal aggro here — sugar over `threat.add`; ignored while the mob is evading. */
|
|
63
|
+
addThreat(sourceId: string, amount: number): void;
|
|
64
|
+
tick(dt: number): MobBrainStep;
|
|
65
|
+
/** Back to idle at full rest (respawn) — clears threat, evade, and wander state. */
|
|
66
|
+
reset(): void;
|
|
67
|
+
}
|
|
68
|
+
export declare function createMobBrain(config: MobBrainConfig, deps: MobBrainDeps): MobBrain;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createThreatTable } from "./threat.js";
|
|
2
|
+
const HOLD = {
|
|
3
|
+
targetId: null,
|
|
4
|
+
moveTo: null,
|
|
5
|
+
speedScale: 1,
|
|
6
|
+
inAttackRange: false,
|
|
7
|
+
arrivedHome: false,
|
|
8
|
+
};
|
|
9
|
+
function distance(a, b) {
|
|
10
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
11
|
+
}
|
|
12
|
+
export function createMobBrain(config, deps) {
|
|
13
|
+
const threat = createThreatTable(config.threat);
|
|
14
|
+
const stickiness = config.stickiness ?? 1.15;
|
|
15
|
+
const evadeSpeedScale = config.evadeSpeedScale ?? 1.4;
|
|
16
|
+
const homeArriveRadius = config.homeArriveRadius ?? 1.2;
|
|
17
|
+
const wander = config.wander === false || config.wander === undefined ? null : config.wander;
|
|
18
|
+
const wanderInterval = wander?.intervalSeconds ?? 4;
|
|
19
|
+
const wanderArrive = wander?.arriveRadius ?? 0.6;
|
|
20
|
+
const rng = deps.rng ?? Math.random;
|
|
21
|
+
let currentMode = "idle";
|
|
22
|
+
let currentTarget = null;
|
|
23
|
+
let evading = false;
|
|
24
|
+
let wanderClock = 0;
|
|
25
|
+
let wanderTo = null;
|
|
26
|
+
function step(mode, patch = {}) {
|
|
27
|
+
currentMode = mode;
|
|
28
|
+
return { mode, ...HOLD, ...patch };
|
|
29
|
+
}
|
|
30
|
+
function acquireByProximity(position) {
|
|
31
|
+
let nearest = null;
|
|
32
|
+
let nearestDistance = config.aggroRadius;
|
|
33
|
+
for (const candidateId of deps.candidates()) {
|
|
34
|
+
const candidatePosition = deps.targetPosition(candidateId);
|
|
35
|
+
if (candidatePosition === null)
|
|
36
|
+
continue;
|
|
37
|
+
const candidateDistance = distance(position, candidatePosition);
|
|
38
|
+
if (candidateDistance <= nearestDistance) {
|
|
39
|
+
nearest = candidateId;
|
|
40
|
+
nearestDistance = candidateDistance;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (nearest !== null)
|
|
44
|
+
threat.add(nearest, 1);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
threat,
|
|
48
|
+
mode: () => currentMode,
|
|
49
|
+
addThreat(sourceId, amount) {
|
|
50
|
+
if (evading)
|
|
51
|
+
return;
|
|
52
|
+
threat.add(sourceId, amount);
|
|
53
|
+
},
|
|
54
|
+
tick(dt) {
|
|
55
|
+
const position = deps.position();
|
|
56
|
+
if (position === null)
|
|
57
|
+
return step(currentMode);
|
|
58
|
+
if (evading) {
|
|
59
|
+
if (distance(position, deps.home) <= homeArriveRadius) {
|
|
60
|
+
evading = false;
|
|
61
|
+
wanderClock = 0;
|
|
62
|
+
wanderTo = null;
|
|
63
|
+
return step("idle", { arrivedHome: true });
|
|
64
|
+
}
|
|
65
|
+
return step("evade", { moveTo: deps.home, speedScale: evadeSpeedScale });
|
|
66
|
+
}
|
|
67
|
+
threat.decay(dt);
|
|
68
|
+
if (threat.size() === 0)
|
|
69
|
+
acquireByProximity(position);
|
|
70
|
+
let targetId = threat.highest({ current: currentTarget, stickiness });
|
|
71
|
+
if (targetId !== null && deps.targetPosition(targetId) === null) {
|
|
72
|
+
threat.remove(targetId);
|
|
73
|
+
targetId = threat.highest({ stickiness });
|
|
74
|
+
}
|
|
75
|
+
currentTarget = targetId;
|
|
76
|
+
if (targetId !== null) {
|
|
77
|
+
if (distance(position, deps.home) > config.leashDistance) {
|
|
78
|
+
threat.clear();
|
|
79
|
+
currentTarget = null;
|
|
80
|
+
evading = true;
|
|
81
|
+
return step("evade", { moveTo: deps.home, speedScale: evadeSpeedScale });
|
|
82
|
+
}
|
|
83
|
+
const targetPosition = deps.targetPosition(targetId);
|
|
84
|
+
if (targetPosition === null)
|
|
85
|
+
return step(currentMode);
|
|
86
|
+
if (distance(position, targetPosition) <= config.attackRange) {
|
|
87
|
+
return step("engage", { targetId, inAttackRange: true });
|
|
88
|
+
}
|
|
89
|
+
return step("chase", { targetId, moveTo: targetPosition });
|
|
90
|
+
}
|
|
91
|
+
if (wander === null)
|
|
92
|
+
return step("idle");
|
|
93
|
+
wanderClock += dt;
|
|
94
|
+
if (wanderTo !== null && distance(position, wanderTo) <= wanderArrive)
|
|
95
|
+
wanderTo = null;
|
|
96
|
+
if (wanderTo === null && wanderClock >= wanderInterval) {
|
|
97
|
+
wanderClock = 0;
|
|
98
|
+
const angle = rng() * Math.PI * 2;
|
|
99
|
+
const radius = Math.sqrt(rng()) * wander.radius;
|
|
100
|
+
wanderTo = [deps.home[0] + Math.cos(angle) * radius, deps.home[1], deps.home[2] + Math.sin(angle) * radius];
|
|
101
|
+
}
|
|
102
|
+
if (wanderTo === null)
|
|
103
|
+
return step("idle");
|
|
104
|
+
return step("wander", { moveTo: wanderTo, speedScale: wander.speedScale ?? 0.35 });
|
|
105
|
+
},
|
|
106
|
+
reset() {
|
|
107
|
+
threat.clear();
|
|
108
|
+
currentMode = "idle";
|
|
109
|
+
currentTarget = null;
|
|
110
|
+
evading = false;
|
|
111
|
+
wanderClock = 0;
|
|
112
|
+
wanderTo = null;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -21,6 +21,8 @@ export interface SpawnDirectorConfig {
|
|
|
21
21
|
maxSpawnsPerTick?: number;
|
|
22
22
|
loop?: boolean;
|
|
23
23
|
seed?: number;
|
|
24
|
+
spawnPoints?: readonly NavPoint[];
|
|
25
|
+
spawnPointBias?: number;
|
|
24
26
|
}
|
|
25
27
|
export interface SpawnDirectorState {
|
|
26
28
|
wave: number;
|
|
@@ -36,11 +38,14 @@ export interface SpawnDirectorState {
|
|
|
36
38
|
export interface DirectorContext {
|
|
37
39
|
alive: number;
|
|
38
40
|
players?: number;
|
|
41
|
+
playerPositions?: readonly NavPoint[];
|
|
39
42
|
}
|
|
40
43
|
export interface SpawnRequest {
|
|
41
44
|
entryId: string;
|
|
42
45
|
cost: number;
|
|
43
46
|
wave: number;
|
|
47
|
+
point?: NavPoint;
|
|
48
|
+
laneId?: number;
|
|
44
49
|
}
|
|
45
50
|
export interface DirectorStep {
|
|
46
51
|
state: SpawnDirectorState;
|