@jgengine/node 0.6.0 → 0.7.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 +167 -2
- package/dist/host.d.ts +13 -1
- package/dist/host.js +40 -1
- package/dist/wsServer.d.ts +11 -0
- package/dist/wsServer.js +45 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -11,9 +11,174 @@ 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
|
-
##
|
|
14
|
+
## 0.7.0
|
|
15
|
+
|
|
16
|
+
The engine-gaps release — 22 system-level additions across turn/tactics, cards & boards,
|
|
17
|
+
crafting, survival, navigation & AI, camera rigs, physics & vehicles, traversal &
|
|
18
|
+
destruction, world items & building, map/HUD/ping, combat feel & abilities, audio,
|
|
19
|
+
interaction, sensors, embodiment, multiplayer depth, and objective/session machines.
|
|
20
|
+
**Every 0.6.0 API is unchanged** — the whole release is additive, so upgrading is a version bump.
|
|
21
|
+
|
|
22
|
+
### Migrate
|
|
23
|
+
|
|
24
|
+
- Bump every `@jgengine/*` dependency to `^0.7.0` (the eight packages version in lockstep).
|
|
25
|
+
- No code change is required — 0.7.0 only adds surface; no 0.6.0 API moved or was removed. Existing games keep the orbit/first-person camera, single-player-entity control, and every existing primitive exactly as before.
|
|
26
|
+
- Opt into any new system by importing it directly: a camera rig via `camera.rig` + its config block; a `sensor/*` probe with `@jgengine/shell/vision` renderers; an `ai/*` director over the `nav/` navmesh; `turn/*` + `tactics/*` for turn-based/grid games; `cards/*` + `board/*` for deckbuilders; `crafting/*` for recipes/production/farming; `survival/*` + `world/{envField,weather,realm}` for survival; `combat/{abilityKit,animationState,defensiveWindow,…}` for action feel; `physics/{vehicleBody,traversal,structure,ragdoll,…}` for vehicles/destruction; `session/*` for contested/round/downed/ring/extraction machines; and `multiplayer/*` for lag-comp/hidden-commit/matchmaking.
|
|
27
|
+
- `entityStore.update()` now also accepts `name`, so possession/form can retarget an instance's catalog id without despawn/respawn — no action needed unless you relied on `name` being immutable.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Turn-based & tactics stack** — six pure, renderer-free `@jgengine/core` primitives for turn-based, grid-tactics, and card games (XCOM, BG3, Into the Breach, Slay the Spire, Marvel Snap, Tactical Breach Wizards, Divinity surfaces).
|
|
32
|
+
- `@jgengine/core/turn/turnLoop` (`createTurnLoop`) — initiative machine with configurable `phases` and per-turn action-economy `pools` (`{ id, max, start? }`) that reset when a participant enters their turn; `advanceTurn`/`advancePhase`/`advanceRound`, `spend`/`canSpend`/`gain`/`refill`, and `setOrder`/`addParticipant`/`removeParticipant`. Covers both Slay-the-Spire single-energy resets and BG3's Action/Bonus/Movement/Reaction set.
|
|
33
|
+
- `@jgengine/core/turn/commit` (`createCommitController`, also `turnLoop.commit`) — three commit modes: `immediate`, `simultaneous` (sealed hidden submissions → `reveal()` on `allReady()`), and `rewind` (visible `pending()` → `rewind()`/`commit()`).
|
|
34
|
+
- `@jgengine/core/tactics/tacticalGrid` (`createTacticalGrid`) — tile occupancy, `reachable(from, budget)` flood-fill, `path`, and `push(id, dir, { distance, chain })` discrete knockback-to-tile with chained collisions (Into the Breach).
|
|
35
|
+
- `@jgengine/core/tactics/predictiveQuery` (`predictAreaEffect`/`predictArcEffect`/`predictTiles`) — a would-this-effect-hit query for pre-commit overlays and enemy-intent telegraphs, reusing the exact AoE/LoS targeting behind `ctx.scene.entity.effect` (new shared `resolveAreaTargets` in `combat/effects`) so predictions match what the effect actually drains.
|
|
36
|
+
- `@jgengine/core/tactics/snapshot` (`createSnapshotStore`, `deepClone`) — cheap, repeatable turn-undo over registered `capture()/restore()` slices (the grid, surfaces, and turn loop all qualify), with a `push()/pop()` undo stack.
|
|
37
|
+
- `@jgengine/core/tactics/surface` (`createSurfaceLayer`) — a stateful tile surface layer with its own `tick(dt)` and a data-driven combination matrix (`reactions: [{ when: [a, b], result }]`) — grease+fire, water+lightning — distinct from terrain/water.
|
|
38
|
+
- `@jgengine/core/combat/effects` now exports `resolveAreaTargets` (+ `AreaTarget`, `AreaTargetInput`), the shared in-radius→LoS→falloff→accept targeting that both `applyEffect` and the predictive query run, guaranteeing parity by construction. No behavior change to `effect`.
|
|
39
|
+
|
|
40
|
+
Pure, renderer-free primitives for card, board, and deckbuilder games — they sit **beside** the slot inventory, never replace it.
|
|
41
|
+
|
|
42
|
+
- **`@jgengine/core/cards/cardPile`** — a `cardPile` of named ordered zones (deck/hand/discard/exhaust) with seeded `shuffle`, `draw(n)` (hand limits + reshuffle-on-empty), `discard`/`exhaust`/`move`. Reuses the engine's seeded RNG (`pileRng`), so shuffles are deterministic under a seed. Slay the Spire, Balatro.
|
|
43
|
+
- **`@jgengine/core/cards/modifierPipeline`** — an ordered `{ source, apply(value) → value }` pipeline (`runPipeline` / `createModifierPipeline`) with an inspectable per-step `trace` (before/after/changed) for Balatro-style scoring readouts. Generic over the scored value.
|
|
44
|
+
- **`@jgengine/core/board/laneBoard`** — N lanes, per-side power aggregate + optional per-lane `LaneRule` modifier, with `laneOutcome`/`boardTotals`/`lanesWon`. Marvel Snap, Inscryption.
|
|
45
|
+
- **`@jgengine/core/board/timelineBoard`** — N slots each on an independent cooldown, `tick(dtMs)` resolving fires in expiry order (then slot index), multiple fires per slot per tick. The Bazaar auto-battlers.
|
|
46
|
+
- **`@jgengine/core/inventory/shapedGrid`** — a polyomino inventory variant: `Footprint` placement, `rotateFootprint`, `canPlace` overlap/bounds check, plus `gridAdjacencyQuery` (orthogonal/diagonal neighbors) feeding synergy effects, and `cellFromPoint` for pointer→cell snap. Backpack Hero, Tetris inventory.
|
|
47
|
+
- **`@jgengine/react/dragLayer`** — a 2-D UI-space drag/rotate/drop/snap gesture layer over the above: `useDragLayer`, headless `DraggableCard` (right-click rotate), `DropZone` (cell snap + active state), `DragGhost`.
|
|
48
|
+
|
|
49
|
+
- `@jgengine/core/crafting/recipe` — a recipe graph primitive: `RecipeDef { inputs, outputs, seconds?, station?, stationRange?, requires? }` turns inputs + an optional required-workstation-in-range + time into outputs. `craft` / `canCraft` consume and produce on an `InventoryState` atomically (rejecting `missing-inputs` / `no-station` / `locked` / `no-output-space`); `stationSatisfied` does the range check against placed `{ catalogId, position }` stations; `createRecipeGraph` indexes recipes by `producing` / `using` / `category`. For Valheim/Enshrouded-style workbench tiers, Tarkov hideout stations, Palworld base craft. (#71)
|
|
50
|
+
- `@jgengine/core/economy/techTree` — a prerequisite-gated tech tree that **generalizes flat `unlocks`** rather than duplicating it: `TechNodeDef extends UnlockDef` adds `requires` (prereq ids), a `recipe` payload, and `grants`. `createTechTree(defs)` wraps `createUnlocks` and gates `unlock(userId, id)` on prerequisites, exposing `available` (frontier) and `recipes` (payloads unlocked). Flat unlocks are just nodes with no `requires`. For Once Human Memetics / Abiotic Factor branching trees. (#72)
|
|
51
|
+
- `@jgengine/core/crafting/production` — `productionBuilding({ inputs, outputs, rate, power? })` plus `tickProduction`, which consumes buffered inputs and emits outputs continuously through game-time `dt` (so pause/fast-forward apply for free). `feedProduction` / `drainOutput` are the buffer I/O, `advanceTransport` slides items along a conveyor path, `resolvePowerGrid` powers demands greedily against a supply. For Palworld/Satisfactory/Factorio automation. (#74)
|
|
52
|
+
- `@jgengine/core/crafting/crop` — a `cropTile` soil-and-growth state machine (`tillTile` / `plantCrop` / `waterTile` / `advanceCropDay` / `harvestCrop`, with regrow and daily-water rules) that advances stages on the simClock day tick, plus `applyToolToTiles(tiles, center, pattern, apply)` with `squarePattern` / `diamondPattern` / `rectPattern` for watering-can/hoe AoE under the cursor. `createCropField` wraps a tile grid; `createDayTicker` reads day rollovers off `ctx.time.calendar()`. For Stardew/Coral Island/Palia farming. (#75)
|
|
53
|
+
- **Survival & environment layer.** Renderer-free `@jgengine/core` primitives for survival games (Project Zomboid moodles, The Long Dark / Green Hell survival, DayZ / Tarkov per-limb health, weather-driven games, Nightingale realm cards), plus the `@jgengine/shell` fire visuals.
|
|
54
|
+
- `@jgengine/core/survival/decayMeter` — named `decayMeter`s (`createDecayMeterSet`) that drain/recover on game-time `dt`, refill from consumables/actions, raise threshold moodles, and take environment-driven rate modifiers (cold speeds warmth loss, toxic biomes drop oxygen).
|
|
55
|
+
- `@jgengine/core/survival/moodle` — a stacking status-effect display distinct from numeric bars: `stackMoodles(...)` folds meter/ailment/buff moodles worst-first, and `createMoodleStack()` holds timed buffs (Valheim-style concurrent food buffs) that expire on `tick(dt)`.
|
|
56
|
+
- `@jgengine/core/survival/regionHealth` — a multi-region health component (`createMultiRegionHealth`): per-part pools with vulnerability + vital death, a stacking ailment queue (bleed/fracture) that drains over time, and per-injury treatment items (`treat("bandage")`). Shares the moodle display via `ailmentMoodles()`.
|
|
57
|
+
- `@jgengine/core/world/envField` — sampleable environment fields (`createEnvironmentField`): temperature, wetness, light-exposure, and ambient-light at any world position and time, with sky occluders, heat sources, and a day/night sun cycle. Meters and spawn gating read it renderer-free.
|
|
58
|
+
- `@jgengine/core/world/weather` — weather → gameplay modifiers (`resolveWeather` → grip/visibility/structure-damage/chill/ignition/spread) over a game-owned table, plus a **coarse cellular** fire-spread grid (`createFireGrid`, not a fluid solver): wind-biased propagation, fuel burn-through, firebreaks, and rain/wetness suppression.
|
|
59
|
+
- `@jgengine/core/world/realm` — runtime realm composition (`composeRealm`): assemble a played instance from a deck of modifier cards (major biome + minor weather/day-length/spawn cards) that override environment params and spawn tables, recomposing a sampleable environment field on top of the weather hooks.
|
|
60
|
+
- `@jgengine/shell/weather` `FireSpreadLayer` — renders a `FireGrid`'s burning flames + scorched cells; a `survival-demo` game wires the whole stack (moodle stack, per-part body panel, condition meters, weather readout, spreading fire).
|
|
61
|
+
- `@jgengine/core/audio/audioFalloff` — the audio contract + pure distance→gain math. `SoundDef`/`AudioBusDef` catalog types, `computeFalloffGain(distance, config)` (`"linear" | "inverse" | "none"` curves), and `resolveEmitterGain`/`distance3` helpers. No Web Audio in core — this is the tested math the shell plays from.
|
|
62
|
+
- `@jgengine/core/time/beatClock` — a BPM tick signal, separate from `simClock`. `createBeatClock({ bpm, beatsPerBar? }, onBeat?)` advances on **game-time** `dt` and fires `onBeat` per crossed beat; `createBeatInputBuffer` auto-corrects an off-beat press to fire on the next beat tick (`nextBeatTime` is the underlying pure quantization). For rhythm-quantized combat (Hi-Fi Rush–style).
|
|
63
|
+
- `@jgengine/shell` positional audio — `PlayableGame.audio = { sounds, buses? }` + `entitySounds`/`objectSounds` (kind/catalog-id → sound id, same convention as `entityModels`) declare looping positional emitters; the shell (`shell/audio/audioEngine`, `shell/audio/AudioComponents`) owns the `AudioContext`, attaches the listener to the camera every frame, and drives emitter gain from the core falloff curve. Music/SFX buses are plain per-bus gain nodes. No per-game Web Audio glue.
|
|
64
|
+
- `@jgengine/ws/voiceChannel` — a thin, coarse voice-channel router riding the same transport/presence model (channel/falloff model only, no WebRTC media). `createVoiceChannelRouter(channels?)`: `join`/`leave` any number of channels at once, `updatePosition` for proximity falloff (reusing the core audio falloff curve), `setMuted`, and `resolveRoutes(listenerUserId)` — one `{ fromUserId, channelId, gain }` per shared channel, so a positional proximity channel and a flat-gain walkie/crew channel resolve simultaneously and independently.
|
|
65
|
+
- `@jgengine/core/interaction/skillCheck` — a moving-target-zone, timed minigame (`evaluateSkillCheck`, `skillCheckMarkerPosition`, `skillCheckZoneAt`) for casting/reeling, active-reload, and production minigames from an `item.use` handler.
|
|
66
|
+
- `@jgengine/core/interaction/qte` — a timed-prompt QTE sequencer (`evaluateQteSequence`, `pendingQteStep`, `qteProgress`).
|
|
67
|
+
- `@jgengine/core/scene/captureCheck` — `captureChance`/`rollCapture`, an hp%+catchPower→probability formula for capture/tame mechanics.
|
|
68
|
+
- `@jgengine/core/scene/roster` — `createRoster()`, a persisted per-owner roster (capture/release/list/setEquipped) wired onto the runtime as `ctx.game.roster`, distinct from the ephemeral `game.social.party`.
|
|
69
|
+
- `@jgengine/core/stats/rollCheck` — `rollCheck`, a d20-style roll vs. DC with advantage/disadvantage and critical detection.
|
|
70
|
+
- Dialogue choices can now gate a branch behind a roll: `DialogueChoice.check` (+ `onSuccess`/`onFailure`) on the `@jgengine/react/components` `DialogueDef`/`DialogueChoice` types, resolved via `resolveDialogueInvoke`.
|
|
71
|
+
- `@jgengine/react` gained `SkillCheckBar`, `QteTrack`, and `CaptureOdds` headless minigame UI components, plus a `useRoster(userId?)` hook and extra `DialogueBox` slot classNames (`lineClassName`, `speakerClassName`, `choicesClassName`, `choiceClassName`, `checkClassName`).
|
|
72
|
+
- **Navigation & pointer-driven input.** A renderer-free foundation for click-to-move, RTS unit command, and pointer verbs.
|
|
73
|
+
- `@jgengine/core/nav/navGrid` — a walkable grid + A* pathfinding (`createNavGrid`, `findPath`, `smoothPath`); blocked start/goal snap to the nearest walkable cell, paths are string-pulled, and one graph feeds both click-to-move and AI routing.
|
|
74
|
+
- `@jgengine/core/nav/pathFollow` — an authored-polyline mover for tower-defense creeps needing no navmesh (`createPathFollow` + pure `advancePathFollow`); `pathFromNav` lifts an A* route so the same follower drives click-to-move.
|
|
75
|
+
- `@jgengine/core/input/pointer` — the renderer-free `PointerHit` contract (`{ point, normal, entity, object }`) plus `aimToPoint` / `moveTargetFromHit` / `groundOf` so gameplay consumes cursor hits (aim, move-to, routing) without three.js.
|
|
76
|
+
- `@jgengine/core/scene/selection` — pure box-select math (`createSelectionSet`, `screenRect`, `selectWithinRect`, `isMarquee`).
|
|
77
|
+
- `@jgengine/core/interaction/contextMenu` — `contextVerb` / `buildContextMenu` / `contextVerbInput`; catalog entities/objects carry `verbs` for right-click menus.
|
|
78
|
+
- `PlayableGame.pointer` config (`moveCommand`, `select`, `orderCommand`, `contextMenu`, `aim`) — the `@jgengine/shell` `GamePlayerShell` casts the cursor (`pointer.worldHit()`), renders a drag-marquee + right-click verb menu, routes primary-ability aim to the cursor, and remaps orbit to middle-drag so the left button drives verbs.
|
|
79
|
+
- **AI over the navmesh.** A renderer-free `ai/` domain for directors, aggro, jobs, and crowds — all ticking on game-time `dt` (the `ctx.time` simClock delta, so pause/fast-forward come free) and routing over the `nav/` navmesh.
|
|
80
|
+
- `@jgengine/core/ai/spawnDirector` — a budget/escalation spawn director for wave shooters and difficulty directors (`createSpawnDirectorState` + pure `advanceSpawnDirector`). Per-`WaveManifest` budgets spent on weighted `SpawnEntry`s (`cost`/`weight`/`minWave`) under a `maxAlive` cap; `duration` auto-advances waves (or `advanceWave` on clear); budget trickles, ramps with sim-time (`escalationPerSecond`), scales per player, and surges on `raiseAlert`. Seeded/deterministic; `pickSpawnPoint` biases placement toward players.
|
|
81
|
+
- `@jgengine/core/ai/threat` — an aggro/threat table (`createThreatTable`): accumulate/`decay` per source, `highest({ current, stickiness })` for sticky MMO target selection feeding `scene/targeting`, `ranked` for a threat meter.
|
|
82
|
+
- `@jgengine/core/scene/behaviors` gained `patrol({ waypoints, speed, loop? })` — a waypoint route as a `BehaviorDescriptor` on top of `wander`, driven by `pathFollow` over `findPath`.
|
|
83
|
+
- `@jgengine/core/ai/jobBoard` — a task/job queue NPCs pull from (`createJobBoard`): `post`/`claim`/`assign`/`release` plus a per-tick `advance(worker, dt, { distanceToStation })` state machine (`travelling → working → done`, `repeat` for production loops) that reports on completion. For colony/companion assignment (Palworld, Schedule I, Sons of the Forest).
|
|
84
|
+
- `@jgengine/core/ai/crowd` — a flow field with congestion + POI routing for management sims (`computeFlowField` Dijkstra steering without per-agent A*, `createCrowdField` per-cell occupancy whose `penalty()` reroutes flow around crowds, `selectPoi` weighting appeal/proximity/capacity with a `findPath`-length distance override).
|
|
85
|
+
- **Dropped-item world entities & loot filter.** Loot no longer has to teleport straight to inventory — it can lie on the ground as a rarity-coded, beam-and-labelled `worldItem` you walk up to and grab (Borderlands/Diablo loot beams, ARPG ground loot, Apex/Lethal-Company pick-ups).
|
|
86
|
+
- `@jgengine/core/game/worldItem` — the `worldItem` scene-entity model (position + item ref + rarity): `ctx.scene.worldItem.spawn / get / list / nearestInRadius / pickup` (a third scene bucket alongside object/entity, never merged into inventory). Pure helpers `createWorldItemStore`, `resolveDeathDrops` (splits rolled drops into scattered ground items vs direct grants), `scatterOffset` / `scatterPosition` (the on-death scatter impulse), `selectNearestWorldItem` (pickup-radius nearest selection), and `resolveWorldItemPresentation` (rarity baseline + filter overlay → render binding). Emits `worldItem.dropped` / `worldItem.picked_up`.
|
|
87
|
+
- `onDeath.dropMode: "world"` (+ optional `onDeath.scatter`) routes an entity's death drops through scattered ground items instead of granting straight to the killer; currency still grants directly. Item catalog entries carry `rarity` / `baseType` read by the render binding + filter.
|
|
88
|
+
- `@jgengine/core/game/lootFilter` — a PoE/Last-Epoch-style rule evaluator (`lootFilter`, `evaluateLootFilter`) keyed on rarity + base type + affix-tier that hides/recolors/beams/labels ground items. Rules are **data the game supplies**; first match wins, field-by-field over the rarity baseline. The render binding is engine-owned.
|
|
89
|
+
- `PlayableGame.worldItem` config (`rarityStyle`, `filter`, `pickupRadius`, `beamHeight`) + `pointer.grabWorldItems` — `@jgengine/shell`'s `WorldItems` draws the rarity beam/color/label per drop and `GamePlayerShell` grants + despawns on a click within pickup radius (using `pointer.worldHit()`). `@jgengine/react`'s `useWorldItems()` / `useNearestWorldItem(radius)` drive a HUD pickup prompt.
|
|
90
|
+
|
|
91
|
+
- **Interactive placement, building & terraform.** Data-only placement becomes the build tooling of Valheim/Enshrouded/The Sims/Fortnite/Dinkum/Palia, all pure `@jgengine/core/world` driven by `pointer.worldHit()`, with shell renderers for the ghost/tint/brush.
|
|
92
|
+
- `@jgengine/core/world/placementController` — `createPlacementController(footprint)` owns the placement ghost: `hover(hit)` → `PlacementPreview` (valid/invalid tint wrapping `validatePlacement`), `rotate`, grid/free/surface `snapMode`, `commit` → `PlacementCommit`.
|
|
93
|
+
- `@jgengine/core/world/connectors` — typed connector sockets with `snapToNearest` snap-to-nearest-compatible (`socketsCompatible`, `worldSockets`).
|
|
94
|
+
- `@jgengine/core/world/support` — `solveSupport` walks the connector graph to ground (`supported`/`unsupported`/hop-`distance` for white→red decay); `toDebrisBodies` sinks collapsed pieces into the `PhysicsWorld`.
|
|
95
|
+
- `@jgengine/core/world/walls` — `createWallDrawTool` (drag walls → auto-enclose → `footprintFromWalls` → `autoRoof` hip/gable/flat) plus `createSurfacePaint` for per-tile floor/wall surfaces.
|
|
96
|
+
- `@jgengine/core/world/placedStructureStore` — `createPlacedStructureStore` save/load/select/move/delete with a `snapshot`↔`load` round-trip that survives reload.
|
|
97
|
+
- `@jgengine/core/world/terraform` — `createEditableTerrain({ bounds, base, cellSize })` makes a `TerrainField` you can **write back to** via `apply(edit)` (raise/lower/flatten/paint), and `createTerraformBrush` is the cursor tool. This height-offset grid is the shared terrain-edit write-back pattern.
|
|
98
|
+
- `@jgengine/core/world/buildPermissions` — `createPlotPermissions` (per-plot/guild `BuildRole` edit authority) + `createContributionPool` (co-op pooled-resource contribution model).
|
|
99
|
+
- `@jgengine/shell` renderers: `structures/PlacementGhost` (tinted ghost), `terrain/EditableGround` (renders an `EditableTerrain` with surface paint), `terrain/TerraformBrushCursor` (brush ring); the `builder-sandbox` demo game wires them to `pointer.worldHit()`.
|
|
100
|
+
- **Map, fog of war & contextual ping.** Minimap/world-map/fog/compass + a squad ping verb, built on renderer-free core state, a shell terrain bake, and react HUD components. (Stacked on the pointer foundation above.)
|
|
101
|
+
- `@jgengine/core/world/markers` — a reactive `createMarkerSet()` of `MapMarker`s (objective/entity/loot/ping) with `add`/`remove`/`query`/`prune`/`subscribe`; `DEFAULT_MARKER_KINDS` supplies content-agnostic colors + glyphs.
|
|
102
|
+
- `@jgengine/core/world/fog` — reveal-on-event fog: `createFogField({ bounds, cellSize })` with `reveal` (dig/act) and `revealAlong` (walked trail); revealed cells stay revealed and render from a stable `cells()` snapshot.
|
|
103
|
+
- `@jgengine/core/world/minimap` — pure projection + bearings (`projectToMinimap`, `clampToMinimapEdge`, `compassBearing`, `headingToBearing`, `bearingToCardinal`, `relativeBearing`).
|
|
104
|
+
- `@jgengine/core/game/ping` — `classifyPing(hit, …)` (hostile → enemy, tagged object → its category, ground → location) + `createPingSystem` that classifies, drops a categorized marker, and broadcasts a `PingPayload` over the existing party feed under `PING_FEED_ACTION`. `PlayableGame.pointer.pingCommand` binds the `ping` action → `worldHit()` → your command.
|
|
105
|
+
- `@jgengine/react` — `useMarkers` / `useFog` hooks and `Minimap` / `Compass` / `WorldMap` headless components (bind a core `MarkerSet`/`FogField`, override the `kindStyles` palette).
|
|
106
|
+
- `@jgengine/shell/map` — `bakeTerrainMap` (top-down terrain image for the map background) and `MapMarkerBeacons` world-space beacons; the `extraction-map` demo game wires the whole loop.
|
|
107
|
+
- **Physics joints & constraints** — `PhysicsWorld` gains `hingeJoint`/`fixedJoint`/`distanceJoint`/`springJoint(JointOptions)` between two bodies or a body and a fixed world point, plus `removeJoint`, `setJointAnchor` (move a follow point), `setJointRest`, and `readJointSegments`. The sim is translational: `hinge`/`fixed` pin the shared anchor, `distance` holds a separation, `spring` drives toward `restLength` with stiffness/damping. Foundation under vehicle suspension, ragdolls, grapples, and carry. Tune the buffers with `jointCapacity` / `jointCorrection` in `PhysicsWorldConfig`.
|
|
108
|
+
- **Physics collision → gameplay-event hook** — `PhysicsWorld.onCollision(listener, minApproachSpeed?)` delivers each impacting contact as a reused `CollisionEvent { a, b, nx, ny, nz, approachSpeed, impulse }` during `step`. The seam crash-damage and destruction read; contacts otherwise stay inside the sim.
|
|
109
|
+
- **`@jgengine/core/physics/ragdoll`** — `createRagdoll(world, { bones, links, balance? })` builds a jointed multi-body character on the joint API: floppy by default, or active-ragdoll when a balance motor drives the root toward a target height (`Ragdoll.balance(dt, moveX?, moveZ?)`), with `centerOfMass`, `applyImpulse`, `remove`.
|
|
110
|
+
- **`@jgengine/core/physics/carryable`** — `Carryable` grabs a physics body to a moving follow point via a spring constraint (the raycast pick is the caller's), supports shared multi-owner carry (follow point = owner average), drop/throw, and `carrySpeedMultiplier(mass, capacity, owners)` encumbrance.
|
|
111
|
+
- **`@jgengine/core/physics/forceVolume`** — `ForceVolume` (impulse/velocity/accelerate trigger region, `once` for boost pads vs continuous fans) and `PlatformCarry` (carry bodies standing on a moving platform by its per-`step` delta).
|
|
112
|
+
- **`@jgengine/core/physics/spatialGrid`** — `SpatialGrid`, a broad-phase uniform grid over the x/z plane, separate from the rigid-body sim, for cheap same-tick proximity across hundreds–thousands of simple movers: `rebuild(count, xs, zs)` then `queryCircle` (swarm enemies hitting a player/AoE) or `forEachPair` (mutual separation).
|
|
113
|
+
- **`@jgengine/shell/world/InstancedJoints`** — debug LineSegments overlay drawing a `PhysicsWorld`'s active joints from `readJointSegments`.
|
|
114
|
+
- **`@jgengine/core/physics/traversal`** — `Grapple` (a fired-anchor rope on the joint API: `fire`/`reel`/`payOut`/`moveAnchor`/`release` for grapple, zipline, and swing traversal) and `Glide` (reduced-gravity, forward-thrust wingsuit/glider via `apply(dt, steerX, steerZ)` before `step`). Grapple/zipline/glide (Sekiro, Deep Rock, Enshrouded) over the shared physics sim.
|
|
115
|
+
- **`@jgengine/core/world/carve`** — runtime destructible terrain. `VoxelVolume` is an editable dense voxel grid with `carve`/`deposit` sphere ops honouring per-material `strength` against a tool (Deep Rock dig, Astroneer deposit); `CarvableField` / `carvableTerrain(base)` writes craters and mounds back into any `TerrainField`'s height so ground-snap, collision, and the terrain mesh all see the deformation (Helldivers 2 craters).
|
|
116
|
+
- **`@jgengine/core/physics/structure`** — `StructureGraph`, a structural-integrity graph over a building: nodes (pieces), load-bearing edges, anchored foundations. `damage`/`damageEdge`/`severEdge` recompute reachability to an anchor and return one `CollapseEvent` of every newly-disconnected piece; `toDebris(world, event)` sinks the fallen pieces into a `PhysicsWorld` as rigid bodies. Coarse by design — the collapse **event** replicates, not each fragment (The Finals, Rainbow Six).
|
|
117
|
+
- **`@jgengine/shell/terrain/CarvedTerrain`** — meshes any `TerrainField` (a `CarvableField` with its craters/mounds) into a vertex-coloured deformed ground; `createFieldGroundGeometry` is the underlying geometry builder.
|
|
118
|
+
- **Analog axis input** (`@jgengine/core/input/axisInput`) — `AxisInput { throttle, brake, steer, handbrake }` continuous channel, distinct from the digital action bindings. `AxisChannel` ramps held keys into pedal-like analog values (`sample`) or takes a raw gamepad axis (`setAnalog`); `DRIVE_AXIS_BINDINGS` is a ready WASD/arrow map.
|
|
119
|
+
- **Vehicle controller** (`@jgengine/core/physics/vehicleBody`) — `createVehicleBody(world, config)`: chassis body + per-wheel suspension raycast + spring-damper (on `springJoint`) + a `GripCurve` (`sampleGripCurve`) that bleeds lateral velocity for cornering and, under handbrake, drift. Driven by an `AxisInput`; still a colliding body, so contact feeds crash damage. (Rocket League, Trackmania, Wreckfest.)
|
|
120
|
+
- **Buoyant boat** (`@jgengine/core/physics/buoyancy`) — `createBuoyantBody(world, { body, water, … })` floats a body on a CPU `waterSurface` (Archimedes per hull point + water drag) and, given an `AxisInput`, drives it as a boat (thrust + yaw + keel).
|
|
121
|
+
- **Mount / rideable controller** (`@jgengine/core/scene/mount`) — `createMountController()` transfers camera + input to a driven entity with its own movement kit: `register({ id, kit, seats })`, `mount`/`dismount`, `cameraTarget`/`driveTarget`, `driver`/`occupants`. Control seat drives, passenger seats ride (multi-seat shared vehicles). (Palworld, V Rising, Sea of Thieves.)
|
|
122
|
+
- **Crash-damage stages** (`@jgengine/core/physics/damageZones`) — `createDamageModel({ zones, disableAt })` maps accumulated `onCollision` impulse to coarse discrete stages (`absorb`/`routeCollision`), with an optional `detachStage` (part → debris) and a `disabled` threshold. Coarse stages, not soft-body. (Wreckfest.)
|
|
123
|
+
- **Race state machine** (`@jgengine/core/game/race`) — `raceTrack({ checkpoints, laps })` + `createRaceState({ track, win })` emit `checkpoint.hit` / `lap.completed` / `position.changed` / `race.finished` on game time, keep split times, resolve a pluggable win condition (`firstPastPost`, `topK`, `everyoneFinishes`, `lastStanding`), and `resetToCheckpoint`. (Trackmania, Mario Kart, Fall Guys.)
|
|
124
|
+
- **Lag-compensated hit registration** (`@jgengine/core/multiplayer/lagCompensation`) — `createPositionHistory({ historyMs })` retains an N-sample position ring per entity; `rewindTimestamp(now, rtt, interpDelay)` and `resolveHitscan(history, targets, ray, atMs)` (ray-sphere) register a shot where the target *was* at the shooter's perceived time. Coarse server-side rewind, not full rollback. The `@jgengine/node` ws host records accepted presence poses and exposes `server.rewind({ serverId, atMs })` (`positionHistoryMs` option). (Valorant, Apex.)
|
|
125
|
+
- **Simultaneous hidden-commit + reveal** (`@jgengine/core/multiplayer/simultaneousCommit`) — `createCommitRound({ participants })`: each player `seal`s a sealed action, nothing is readable until `allSealed()`, then `reveal()` returns commits in deterministic participant order (independent of network arrival) for `resolveCommits`. (Marvel Snap.)
|
|
126
|
+
- **Combat-snapshot replay** (`@jgengine/core/multiplayer/combatSnapshot`) — `serializeBoard({ ownerId, units, stats, seed })` deep-freezes a build into a portable `BoardSnapshot`; `replayCombat(a, b, rules)` resolves it deterministically (seeded PRNG) against a live opponent's snapshot — an async-PvP primitive distinct from the live-sync adapters. (The Bazaar.)
|
|
127
|
+
- **Shared-vehicle stations** (`@jgengine/core/scene/stationClaim`) — `createStationClaim(controller?)` layers facet stations (`steer`/`sails`/`cannon`) on `scene/mount`: `register({ id, kit, stations })`, `claim`/`release`, `controllerOf(vehicleId, facet)`, `facetOf`, `openFacets`, `crew`. One control station drives the hull; the rest ride but command their facet. (Sea of Thieves.)
|
|
128
|
+
- **Shared / group wallet** (`@jgengine/core/economy/sharedWallet`) — `createWalletBook()` holds per-`WalletScope` balances (`userScope`/`groupScope`) beside the per-user `economy/wallet`: `grantTo`/`chargeFrom`/`balanceIn`, with a `contributionOf`/`contributorsOf` ledger tracking who funded a shared pool. (Schedule I company funds, Lethal Company quota.)
|
|
129
|
+
- **Session matchmaking** (`@jgengine/core/multiplayer/matchmaking`) — data-driven browse/filter (`browseSessions`, `matchesFilter`, `quickMatch`) hiding private/closed lobbies, plus join-by-code (`findByJoinCode`, `normalizeJoinCode`, `generateJoinCode`). The node host carries generic `SessionAttributes` (`label`/`mode`/`visibility`/`joinCode`/`tags`) on `GameServerRecord`/`ServerListing` and gains `browseServers` + `joinByCode`; the ws backend exposes `browse` / `joinByCode` / `createSession`. (Fortnite island browse, Web Fishing code lobbies.)
|
|
130
|
+
- **Camera rig library** (`@jgengine/shell`, config types in `@jgengine/core/game/playableGame`) — the single orbit camera is now one of eight rigs, selected and tuned through `PlayableGame.camera` (never by writing camera positions from `onTick`). Set `camera.rig`:
|
|
131
|
+
- `topDown` — fixed height/pitch/yaw with decoupled follow for ARPG-iso and top-down (Diablo IV/PoE 2/Last Epoch, Hades II); `camera.topDown: { height, pitch, yaw, followSmoothing, zoom }`.
|
|
132
|
+
- `rts` — free-pan / edge-scroll / rotate / zoom independent of any avatar (The Sims 4, Manor Lords, Two Point Museum); `camera.rts: { panSpeed, edgeScroll, rotateSpeed, bounds, start }`.
|
|
133
|
+
- `shoulder` — over-the-shoulder with ADS transition and shoulder-swap, reticle decoupled from camera center (Remnant II, Helldivers 2, The First Descendant); `camera.shoulder: { shoulderOffset, distance, ads, side }`.
|
|
134
|
+
- `lockOn` — yaw bound to the player→target vector with the move axis reinterpreted as strafe (Elden Ring, Sekiro); `camera.lockOn: { targetEntityId?, distance, framingBias, yawSmoothing }`.
|
|
135
|
+
- `chase` — speed-reactive vehicle chase (speed→FOV curve, spring-arm damping, procedural shake) plus fixed `cockpit`/`hood`/`rear` views (Forza Horizon 5, Rocket League, Trackmania); `camera.chase: { distance, springDamping, fov, shakePerSpeed, view }`.
|
|
136
|
+
- **Every rig accepts `followEntityId: null`** — avatar-less games (city-builders, card games, auto-battlers) can now mount a camera; the orbit rig no longer bails when there is no follow target.
|
|
137
|
+
- **Camera-shake / trauma channel** — every rig reads a shared trauma channel; feed it from any system with `import { cameraShake } from "@jgengine/shell/camera"` (`cameraShake(amplitude, decayPerSecond?)`, amplitude 0..1) or from React via `useCameraShake()`. Tune with `camera.shake: { maxOffset, maxRoll, decayPerSecond, exponent, frequency }`. (Combat hitstop and other systems feed the same channel.)
|
|
138
|
+
- **Cinematic camera + mode-swap cross-fade** — `camera.cinematic: { keyframes: [{ position, lookAt, fov?, duration?, ease? }], loop? }` plays a scripted keyframe path over the active rig, and `camera.transitionSeconds` cross-fades the camera when the rig changes so mode swaps no longer hard-cut.
|
|
139
|
+
- Pure rig math (shake decay/trauma, spring-arm damping, speed→FOV, lerp/cross-fade, offset/strafe vectors, keyframe sampling) is exported from `@jgengine/shell/camera` as testable functions.
|
|
140
|
+
- **Sensors, vision & observer tools** (`@jgengine/core/sensor/*`, renderers in `@jgengine/shell/vision` and `@jgengine/shell/replay`) — a coherent "query hidden/tagged/framed world state and surface it" family:
|
|
141
|
+
- **Reveal vision** (`sensor/revealQuery`) — `createRevealQuery` is an occlusion-ignoring tagged-entity radius query (`inRadius` already never checks occlusion; this scopes it to catalog-declared tags for a vision readout) paired with a toggleable screen-space reveal effect (`shell/vision/RevealVision`: `RevealHighlights` for through-wall 3D highlights, `RevealScreenTint` for the full-screen tint) — Dark Sight / detective-vision / wallhack-style highlight (Hunt: Showdown).
|
|
142
|
+
- **Hidden-state probe** (`sensor/hiddenStateProbe`) — `probeHiddenState`/`probeHiddenStateAll` read a hidden zone/entity state variable in range and surface a distance-weighted reading; `shell/vision/HiddenStateProbeHud`'s `SensorReadoutMeter` renders it as a handheld sensor needle (EMF reader/spirit box/thermometer/geiger, Phasmophobia).
|
|
143
|
+
- **View-frustum sensor** (`sensor/frustumSensor`) — `projectToView`/`framingScore`/`createFrustumSensor` answer "what's in this held camera's view, how well framed, and for how long" (dwell time resets the instant a subject leaves frame); `shell/vision/FrustumSensorHud`'s `FrustumSensorReadout` drives it off the live render camera for a photo-mode HUD (Content Warning).
|
|
144
|
+
- **Session-recording buffer** (`sensor/recordingBuffer`) — `createRecordingBuffer` appends timestamped snapshots on game-time and seeks/scrubs them for replay, photo mode, or kill-cam; `shell/replay/useSessionRecorder` records an entity's pose every frame.
|
|
145
|
+
- **`observer` camera rig** (config in `@jgengine/core/game/playableGame`: `ObserverCameraConfig`) — a detached spectator/photo cam bound to any entity or fixed point that reads no player input at all (van CCTV spectate, Forza-style photo mode free cam, Trackmania ghost/kill-cam).
|
|
146
|
+
|
|
147
|
+
- **Possession** (`@jgengine/core/scene/possession`, `createPossession`) — a player can own N scene entities and switch which one is under active control, distinct from the social party. `ctx.player.possession.own/disown/owns/listOwned(userId, entityId)` tracks ownership; `possess(userId, entityId)` swaps active control (rejecting entities the user doesn't own), flips the previous/next entity's `EntityRole` between `"player"`/`"npc"` (reusing entity control, not forking it), and emits `possession.swapped`. `active(userId)` defaults to `userId` itself until a swap happens. `@jgengine/shell`'s `GamePlayerShell` rebinds WASD movement, targeting, hotbar `from`, and the camera rig's `followEntityId` to the active possessed entity on every swap — no per-game camera glue required.
|
|
148
|
+
- **Form / shapeshift** (`@jgengine/core/scene/form`, `createForms`) — a `form` bundles movement params + an ability-id list + a mesh (reusing the entity's catalog name, so mesh, movement defaults, and receive/role all follow the swap through the existing name-keyed resolution — no parallel mesh system). `ctx.scene.entity.form.register(defs)` in `onInit`; `shapeshift(instanceId, formId, durationSeconds?)` applies the bundle and optionally reverts automatically after `durationSeconds` of **game time** (`ctx.time.after`, so it obeys pause/fast-forward); `revert(instanceId)` reverts early. Emits `form.changed`.
|
|
149
|
+
- **Cosmetic loadouts + emote broadcast** (`@jgengine/core/game/cosmetics` `createCosmetics`; `@jgengine/core/game/social` `Social.emotes`) — `ctx.player.cosmetics.register(defs)` + `apply(userId, loadoutId)` / `equip(userId, slot, cosmeticId)` manage a per-player cosmetic slot map (skin/back/aura/…), emitting `cosmetics.changed`. `ctx.game.social.emotes.play(fromUserId, emoteId, radius?)` broadcasts to nearby **player**-role entities (reusing `scene.entity.inRadius`, not a parallel proximity system) and emits `emote.played` — bind it through the existing `ctx.game.feed` primitive (`feed.bind("emote.played")`) for a HUD feed, no new hook needed.
|
|
150
|
+
- `entityStore`'s `update()` patch now also accepts `name`, so possession/form (and any future system) can retarget an instance's catalog id without despawn/respawn.
|
|
151
|
+
|
|
152
|
+
An additive layer over effects/projectiles/death that adds melee/action feel. Every model is a renderer-free `@jgengine/core` factory a game composes per entity; `@jgengine/shell` renders the world/HUD side. No existing API moved.
|
|
153
|
+
|
|
154
|
+
- **Animation state machine** — `@jgengine/core/combat/animationState`: `createAnimationState({ clips })` over `AnimationClip`s whose `FrameRange`s tag `windup | active | recovery | cancel` windows. The root contract combat/defense subscribe to (`inPhase`, `isActive`, `canCancel`, `activeWindowMs`).
|
|
155
|
+
- **Shared accumulator meter** — `@jgengine/core/stats/accumulatorMeter`: `createAccumulatorMeter({ max, mode, decayPerSecond, decayDelayMs, tiers })`, the fill/decay/threshold-fire/tier primitive. `@jgengine/core/combat/breakMeters` builds `createStaggerMeter` (poise/posture break → riposte) and `createBuildupMeter` (bleed/frost/rot proc) on it.
|
|
156
|
+
- **Defensive window + attack tags** — `@jgengine/core/combat/defensiveWindow` (`resolveDefense` / `createDefensiveWindow`, parry/block/dodge evaluated against the attacker's active frames) reading `@jgengine/core/combat/attackTags` (`attackMeta`, unblockable/thrust/sweep/grab; `counters` for Mikiri-style reads).
|
|
157
|
+
- **Combo strings** — `@jgengine/core/combat/comboString`: `advanceCombo` / `createComboRunner` — ordered attacks with stance-conditioned cancel points over the animation SM.
|
|
158
|
+
- **Dash / dodge** — `@jgengine/core/movement/dash`: `createDashState` — directional burst + i-frame window + stamina/cooldown.
|
|
159
|
+
- **Hit reaction, telegraphs, typed damage numbers** — `@jgengine/core/combat/hitReaction` + `ctx.scene.entity.hitReaction(...)` (knockback impulse + hitstop + `combat.hitReaction` shake channel); `@jgengine/core/combat/telegraph` + `ctx.scene.entity.telegraph(...)` (windup→activation ground decal bound to an effect, drawn by the shell); `ctx.scene.entity.floatText({ crit, element, hitType, scale })` styled by `@jgengine/shell/world/floatTextStyle`.
|
|
160
|
+
|
|
161
|
+
Genre systems over the existing effects/projectiles/targeting/loot primitives. Every model is a renderer-free `@jgengine/core` factory the game ticks on game-time `dt`; `@jgengine/react` adds four-state slot binding hooks. No existing API moved. The ult/streak meters build on the `stats/accumulatorMeter` from the combat-feel layer above.
|
|
162
|
+
|
|
163
|
+
- **Ability kit** — `@jgengine/core/combat/abilityKit`: `createAbilityKit([{ id, cooldownMs, chargesMax?, resourceCost?, castType? }])` models an ability slot **separate from an inventory item**, exposing the four HUD states `ready | cooldown | no-resource | just-cast` plus charges + cooldown fraction. Resource-agnostic — reports `no-resource` against a supplied `resourceAvailable`, the game spends. (MOBA/ARPG action bars, hero-shooter kits.)
|
|
164
|
+
- **Event-fed meters** — `@jgengine/core/stats/eventMeter`: `createEventMeter({ max, mode, gains, resets?, tiers? })` on the shared accumulator. Mode `"hold"` = ult/adrenaline charge (`feed` combat tags, `ready()`, `consume()`); mode `"reset"` = kill-streak/combo with tiered thresholds that resets on a break tag. (Overwatch/Marvel Rivals ult, Returnal/DMC streak.)
|
|
165
|
+
- **Auto-target policy** — `@jgengine/core/scene/autoTarget`: `selectAutoTarget` / `createAutoTargeter` — zero-input per-tick target selection `nearest | farthest | random | strongest | weakest | first | last` (path-progress aware). (Vampire Survivors auto-fire, Bloons tower priority.)
|
|
166
|
+
- **Resistance matrix** — `@jgengine/core/combat/resistance`: `resolveResistance` / `resistanceScale` — damage-category × target-property → `immune | resist | normal | vulnerable` multiplier over the `receive` gate. (Bloons pop-types, elemental RPG weaknesses.)
|
|
167
|
+
- **Run draft** — `@jgengine/core/game/runDraft`: `createRunDraft` / `createRunModifierStack` — pause, present N weighted picks (`pickWeighted`), choose, stack the modifiers for the run (aggregated onto `stats/statModifiers`). (Vampire Survivors level-ups, Hades boons.)
|
|
168
|
+
- **React** — `@jgengine/react` `useAbilitySlots` / `useAbilitySlot` (four-state snapshots) and `useEventMeter` (ult/streak bar view).
|
|
169
|
+
|
|
170
|
+
- `@jgengine/core/item/durability` — per-instance item durability + repair. A catalog `DurabilitySpec` (`max`, `wearPerUse`/`wearPerHit`, `disableAtZero`, `repair`); `createDurability`/`wear`/`isDisabled`/`durabilityFraction` for the wear loop, `repairQuote(spec, state, { station?, to? })` for a quote-then-apply repair (material cost scaled by points restored, optional `qualityLossPerRepair` shrinking `max`), and `createDurabilityTracker()` to hold state per instance id. For weapon/tool/armor degradation repaired at stations.
|
|
171
|
+
- `@jgengine/core/item/affix` — rarity-weighted procgen roller. `createAffixRoller({ pools, rarities })` turns `base × rarity` into `{ rolled affixes, computed stats, name }`: draws `affixCount` distinct affixes without replacement (weighted via the engine `pickWeighted`), computes stats (base × rarity scale, then `add` then `mul` affixes), and composes a name from rarity + prefix/suffix parts. `seededRng(seed)` gives deterministic drops. For looter-shooter / ARPG generated weapons.
|
|
172
|
+
- `@jgengine/core/item/modularItem` — parts-in-typed-slots assembly. `ModularItemDef` with category-constrained `MountSlotDef`s; `install`/`uninstall` validate slot + category + occupancy, `computeEffectiveStats` rolls part `stats` (additive) then `multipliers` over the frame's `baseStats`, `missingRequiredSlots`/`isComplete` gate a buildable whole; `createModularItem(def)` is the stateful wrapper. For piece-by-piece guns and mech loadouts.
|
|
173
|
+
- `@jgengine/core/inventory/storageTier` — tiered extraction-economy inventory. A `tier: "carried" | "banked"` on inventory containers (`InventoryDeclaration.tier`); `partitionOnDeath` splits a death snapshot into kept (banked) vs lost (carried) with merged stacks, `createDeliveryQueue()` is the delayed-delivery (insurance) hook (`schedule`/`due`/`claimDue` on the game clock), `insureLost` filters the lost set to insured items with a delayed `deliverAt`, and `resolveConsolation` yields a baseline loadout id (apply via `applyLoadout`) for the post-death consolation grant. The inventory foundation the extraction session/round machines build on.
|
|
174
|
+
- `InventoryDeclaration` (`@jgengine/core/game/defineGame`) gained an optional `tier?: StorageTier` flag so containers declare carried-vs-banked storage directly.
|
|
175
|
+
- `@jgengine/core/session/contestedChannel` — the interrupt-on-damage progress objective (plant/defuse, cash-out, urn deposit, hold-to-extract). `createContestedChannel({ duration, interruptOnDamage?, resetOnInterrupt?, favorability?, ratePerOccupant?, contested?, decayRate? })`: `start(team)`, `tick(dt, occupants)` (per-team occupancy → `start`/`tick`/`contested`/`paused`/`complete` events), `damage()` interrupts. `favorability`/`ratePerOccupant` scale the fill rate; `contested: "pause" | "decay"` handles a contesting team.
|
|
176
|
+
- `@jgengine/core/session/roundState` — the buy→live→end match machine. `createRoundState({ phases, teams, maxRounds?, winReward?, lossBonus? })`: `tick(dt)` runs phase timers and auto-advances rounds, `concludeRound(winner)` settles win + escalating loss-bonus economy (`lossBonusFor`), `onPhaseEnd(hook)` gates commerce/spawns, `match.end` fires at `maxRounds`.
|
|
177
|
+
- `@jgengine/core/combat/downed` — the alive→downed→dead revive chain. `createDownedState({ bleedoutSeconds, reviveSeconds?, reviveHealthFraction?, banner? })`: `down`/`tick` (bleedout → `died` + optional banner), `revive(id, dt)` accumulates ally hold time, `finish` executes, `respawnFromBanner` beacons back. Sits in front of engine death resolution.
|
|
178
|
+
- `@jgengine/core/session/ring` — the shrinking battle-royale safe zone. `RingConfig` = `{ center, phases }`; `ringSampleAt(config, t)`/`createRing` give the live `{ center, radius, damagePerSecond }` (interpolated on the game clock), `isOutside`/`distanceOutside`/`damageOutside(t, dt, positions)` for out-of-bounds DoT.
|
|
179
|
+
- `@jgengine/core/session/extraction` — the raid-scoped extract-to-bank session, composed from the contested channel + `inventory/storageTier`. `createRaidSession({ extracts, insurance?, consolation? })`: `beginExtract`/`tickExtract`/`damage` drive hold-to-leave, `resolveExtraction` banks everything carried, `resolveDeath` partitions/insures/consoles via storage tiers, `claimDeliveries(now)` drains insured returns.
|
|
180
|
+
- `@jgengine/core/runtime/persistenceScope` — run-vs-meta persistence split with reset boundaries. `partitionScopes`/`resetRun`/`mergeScopes` over flat records, `clearRunFields`/`applyRunReset` over player rows/profiles, `planScenarioReset(...)` normalizes a season/scenario wipe applied through the new optional `HostPersistence.resetScenario` — implemented by `@jgengine/sql` (deletes a server's chunks + session and run-resets each profile in one transaction, keeping account meta).
|
|
15
181
|
|
|
16
|
-
_Nothing yet._
|
|
17
182
|
|
|
18
183
|
## 0.6.0
|
|
19
184
|
|
package/dist/host.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GameRuntime } from "@jgengine/core/runtime/gameRuntime";
|
|
2
|
-
import type { HostPersistence, ServerListing } from "@jgengine/core/runtime/hostPersistence";
|
|
2
|
+
import type { HostPersistence, ServerListing, SessionAttributes } from "@jgengine/core/runtime/hostPersistence";
|
|
3
|
+
import type { MatchFilter, SessionListing } from "@jgengine/core/multiplayer/matchmaking";
|
|
3
4
|
import type { GameRuntimePlayerView, GameRuntimeServerView, JoinServerResult, TransportRunCommandResult } from "@jgengine/core/runtime/transport";
|
|
4
5
|
export type HostChangeEvent = {
|
|
5
6
|
type: "server";
|
|
@@ -26,7 +27,18 @@ export type GameHost = {
|
|
|
26
27
|
userId: string;
|
|
27
28
|
gameId: string;
|
|
28
29
|
serverId?: string;
|
|
30
|
+
attributes?: SessionAttributes;
|
|
29
31
|
}) => Promise<JoinServerResult>;
|
|
32
|
+
browseServers: (args: {
|
|
33
|
+
gameId: string;
|
|
34
|
+
filter?: MatchFilter;
|
|
35
|
+
limit?: number;
|
|
36
|
+
}) => Promise<SessionListing[]>;
|
|
37
|
+
joinByCode: (args: {
|
|
38
|
+
userId: string;
|
|
39
|
+
gameId: string;
|
|
40
|
+
code: string;
|
|
41
|
+
}) => Promise<JoinServerResult | null>;
|
|
30
42
|
leaveServer: (args: {
|
|
31
43
|
userId: string;
|
|
32
44
|
serverId: string;
|
package/dist/host.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createGameRuntime } from "@jgengine/core/runtime/gameRuntime";
|
|
3
3
|
import { buildHydratePlayers, planServerPersist, shouldAutoSave, toServerListing, } from "@jgengine/core/runtime/hostPersistence";
|
|
4
|
+
import { browseSessions, findByJoinCode } from "@jgengine/core/multiplayer/matchmaking";
|
|
4
5
|
import { clearDirtyFlags, createEmptyServerRow, splitProfilePlayer } from "@jgengine/core/runtime/snapshot";
|
|
5
6
|
const builtinCommands = {
|
|
6
7
|
"engine.ping": {
|
|
@@ -132,7 +133,31 @@ export function createGameHost(options) {
|
|
|
132
133
|
}
|
|
133
134
|
return { ticked, saved };
|
|
134
135
|
});
|
|
135
|
-
|
|
136
|
+
const collectListings = async (gameId) => {
|
|
137
|
+
const byId = new Map();
|
|
138
|
+
for (const record of await persistence.listServers(gameId)) {
|
|
139
|
+
byId.set(record.serverId, record);
|
|
140
|
+
}
|
|
141
|
+
for (const entry of live.values()) {
|
|
142
|
+
if (entry.record.gameId !== gameId)
|
|
143
|
+
continue;
|
|
144
|
+
byId.set(entry.record.serverId, entry.record);
|
|
145
|
+
}
|
|
146
|
+
return [...byId.values()].map((record) => ({
|
|
147
|
+
serverId: record.serverId,
|
|
148
|
+
gameId: record.gameId,
|
|
149
|
+
status: record.status,
|
|
150
|
+
visibility: record.visibility ?? "public",
|
|
151
|
+
memberCount: record.memberUserIds.length,
|
|
152
|
+
slotsPerServer: record.slotsPerServer,
|
|
153
|
+
label: record.label,
|
|
154
|
+
mode: record.mode,
|
|
155
|
+
joinCode: record.joinCode,
|
|
156
|
+
tags: record.tags,
|
|
157
|
+
updatedAt: record.updatedAt,
|
|
158
|
+
}));
|
|
159
|
+
};
|
|
160
|
+
const host = {
|
|
136
161
|
joinServer: (args) => enqueue(async () => {
|
|
137
162
|
const timestamp = now();
|
|
138
163
|
const runtime = resolveRuntime(args.gameId);
|
|
@@ -141,6 +166,7 @@ export function createGameHost(options) {
|
|
|
141
166
|
throw new Error("Server belongs to a different game");
|
|
142
167
|
}
|
|
143
168
|
if (entry === null) {
|
|
169
|
+
const attributes = args.attributes;
|
|
144
170
|
const record = {
|
|
145
171
|
serverId: createServerId(),
|
|
146
172
|
gameId: args.gameId,
|
|
@@ -154,6 +180,11 @@ export function createGameHost(options) {
|
|
|
154
180
|
tickAnchorMs: timestamp,
|
|
155
181
|
createdAt: timestamp,
|
|
156
182
|
updatedAt: timestamp,
|
|
183
|
+
...(attributes?.label !== undefined ? { label: attributes.label } : {}),
|
|
184
|
+
...(attributes?.mode !== undefined ? { mode: attributes.mode } : {}),
|
|
185
|
+
...(attributes?.visibility !== undefined ? { visibility: attributes.visibility } : {}),
|
|
186
|
+
...(attributes?.joinCode !== undefined ? { joinCode: attributes.joinCode } : {}),
|
|
187
|
+
...(attributes?.tags !== undefined ? { tags: attributes.tags } : {}),
|
|
157
188
|
};
|
|
158
189
|
entry = await hydrate(record);
|
|
159
190
|
}
|
|
@@ -179,6 +210,13 @@ export function createGameHost(options) {
|
|
|
179
210
|
emit({ type: "player", serverId: entry.record.serverId, userId: args.userId });
|
|
180
211
|
return { serverId: entry.record.serverId, isNew };
|
|
181
212
|
}),
|
|
213
|
+
browseServers: async (args) => browseSessions(await collectListings(args.gameId), args.filter ?? {}, { limit: args.limit }),
|
|
214
|
+
joinByCode: async (args) => {
|
|
215
|
+
const match = findByJoinCode(await collectListings(args.gameId), args.code);
|
|
216
|
+
if (match === null)
|
|
217
|
+
return null;
|
|
218
|
+
return host.joinServer({ userId: args.userId, gameId: args.gameId, serverId: match.serverId });
|
|
219
|
+
},
|
|
182
220
|
leaveServer: (args) => enqueue(async () => {
|
|
183
221
|
const entry = await getLive(args.serverId);
|
|
184
222
|
if (entry === null)
|
|
@@ -333,4 +371,5 @@ export function createGameHost(options) {
|
|
|
333
371
|
return () => listeners.delete(listener);
|
|
334
372
|
},
|
|
335
373
|
};
|
|
374
|
+
return host;
|
|
336
375
|
}
|
package/dist/wsServer.d.ts
CHANGED
|
@@ -12,11 +12,22 @@ export type GameWsServerOptions = {
|
|
|
12
12
|
token?: string;
|
|
13
13
|
}) => Promise<string | null> | string | null;
|
|
14
14
|
poseRules?: PoseSyncRules;
|
|
15
|
+
positionHistoryMs?: number;
|
|
15
16
|
now?: () => number;
|
|
16
17
|
};
|
|
18
|
+
export type RewoundPosition = {
|
|
19
|
+
userId: string;
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
z: number;
|
|
23
|
+
};
|
|
17
24
|
export type GameWsServer = {
|
|
18
25
|
wss: WebSocketServer;
|
|
19
26
|
port: () => number;
|
|
27
|
+
rewind: (args: {
|
|
28
|
+
serverId: string;
|
|
29
|
+
atMs: number;
|
|
30
|
+
}) => RewoundPosition[];
|
|
20
31
|
close: () => Promise<void>;
|
|
21
32
|
};
|
|
22
33
|
export declare function createGameWsServer(options: GameWsServerOptions): GameWsServer;
|
package/dist/wsServer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { WebSocketServer } from "ws";
|
|
2
2
|
import { decidePoseSync } from "@jgengine/core/multiplayer/presenceModel";
|
|
3
|
+
import { createPositionHistory } from "@jgengine/core/multiplayer/lagCompensation";
|
|
3
4
|
import { decodeWsClientMessage, encodeWsMessage, subscriptionKey, } from "@jgengine/ws/protocol";
|
|
4
5
|
const DEFAULT_POSE_RULES = {
|
|
5
6
|
maxSpeed: 12,
|
|
@@ -18,6 +19,16 @@ export function createGameWsServer(options) {
|
|
|
18
19
|
: new WebSocketServer({ port: options.port ?? 0, path: options.path });
|
|
19
20
|
const connections = new Set();
|
|
20
21
|
const presence = new Map();
|
|
22
|
+
const positionHistoryMs = options.positionHistoryMs ?? 1_000;
|
|
23
|
+
const histories = new Map();
|
|
24
|
+
const historyFor = (serverId) => {
|
|
25
|
+
let history = histories.get(serverId);
|
|
26
|
+
if (history === undefined) {
|
|
27
|
+
history = createPositionHistory({ historyMs: positionHistoryMs });
|
|
28
|
+
histories.set(serverId, history);
|
|
29
|
+
}
|
|
30
|
+
return history;
|
|
31
|
+
};
|
|
21
32
|
const send = (connection, message) => {
|
|
22
33
|
if (connection.socket.readyState !== connection.socket.OPEN)
|
|
23
34
|
return;
|
|
@@ -106,6 +117,7 @@ export function createGameWsServer(options) {
|
|
|
106
117
|
rotationPitch: pose.rotationPitch,
|
|
107
118
|
lastSeenAtMs: timestamp,
|
|
108
119
|
});
|
|
120
|
+
historyFor(serverId).record(connection.userId, timestamp, { x: pose.x, y: pose.y, z: pose.z });
|
|
109
121
|
broadcastPresence(serverId);
|
|
110
122
|
return;
|
|
111
123
|
}
|
|
@@ -117,6 +129,7 @@ export function createGameWsServer(options) {
|
|
|
117
129
|
rotationPitch: decision.rotationPitch,
|
|
118
130
|
lastSeenAtMs: timestamp,
|
|
119
131
|
});
|
|
132
|
+
historyFor(serverId).record(connection.userId, timestamp, decision.position);
|
|
120
133
|
}
|
|
121
134
|
if (decision.changed)
|
|
122
135
|
broadcastPresence(serverId);
|
|
@@ -160,6 +173,25 @@ export function createGameWsServer(options) {
|
|
|
160
173
|
userId,
|
|
161
174
|
gameId: message.gameId,
|
|
162
175
|
serverId: message.serverId,
|
|
176
|
+
attributes: message.attributes,
|
|
177
|
+
});
|
|
178
|
+
reply(connection, message.id, result);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
case "joinByCode": {
|
|
182
|
+
const result = await host.joinByCode({
|
|
183
|
+
userId,
|
|
184
|
+
gameId: message.gameId,
|
|
185
|
+
code: message.code,
|
|
186
|
+
});
|
|
187
|
+
reply(connection, message.id, result);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
case "browse": {
|
|
191
|
+
const result = await host.browseServers({
|
|
192
|
+
gameId: message.gameId,
|
|
193
|
+
filter: message.filter,
|
|
194
|
+
limit: message.limit,
|
|
163
195
|
});
|
|
164
196
|
reply(connection, message.id, result);
|
|
165
197
|
return;
|
|
@@ -229,6 +261,19 @@ export function createGameWsServer(options) {
|
|
|
229
261
|
}
|
|
230
262
|
return address.port;
|
|
231
263
|
},
|
|
264
|
+
rewind: ({ serverId, atMs }) => {
|
|
265
|
+
const history = histories.get(serverId);
|
|
266
|
+
if (history === undefined)
|
|
267
|
+
return [];
|
|
268
|
+
const positions = [];
|
|
269
|
+
for (const userId of history.entities()) {
|
|
270
|
+
const sample = history.sampleAt(userId, atMs);
|
|
271
|
+
if (sample !== null) {
|
|
272
|
+
positions.push({ userId, x: sample.x, y: sample.y, z: sample.z });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return positions;
|
|
276
|
+
},
|
|
232
277
|
close: () => new Promise((resolve) => {
|
|
233
278
|
unsubscribeHost();
|
|
234
279
|
for (const connection of connections) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Standalone authoritative game host for JGengine: in-memory server snapshots, tick loop, save-cadence flush, WebSocket server, memory/file persistence.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"test": "bun test src"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@jgengine/core": "^0.
|
|
30
|
-
"@jgengine/ws": "^0.
|
|
29
|
+
"@jgengine/core": "^0.7.0",
|
|
30
|
+
"@jgengine/ws": "^0.7.0",
|
|
31
31
|
"ws": "^8.18.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|