@jgengine/core 0.7.0 → 0.8.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 +47 -0
- package/README.md +2 -0
- package/dist/ai/spawnDirector.d.ts +5 -0
- package/dist/ai/spawnDirector.js +13 -1
- 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/combat/abilityKit.d.ts +5 -0
- package/dist/combat/abilityKit.js +10 -0
- package/dist/combat/effects.d.ts +1 -1
- package/dist/combat/effects.js +9 -6
- package/dist/combat/projectiles.d.ts +42 -5
- package/dist/combat/projectiles.js +98 -12
- package/dist/combat/telegraph.d.ts +5 -1
- package/dist/combat/telegraph.js +14 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -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 +157 -0
- package/dist/devtools/devtools.js +484 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/game/chat.d.ts +66 -0
- package/dist/game/chat.js +165 -0
- package/dist/game/defineGame.d.ts +8 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +25 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/playableGame.d.ts +237 -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/race.d.ts +5 -0
- package/dist/game/race.js +20 -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/worldItem.d.ts +3 -0
- package/dist/game/worldItem.js +12 -6
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -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/touchScheme.d.ts +83 -0
- package/dist/input/touchScheme.js +115 -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 +37 -1
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- 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/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 +3 -0
- package/dist/multiplayer/presenceModel.js +15 -1
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -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 +18 -1
- package/dist/nav/pathFollow.d.ts +4 -2
- package/dist/nav/pathFollow.js +5 -3
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/forceVolume.js +5 -5
- package/dist/physics/physicsWorld.d.ts +41 -2
- package/dist/physics/physicsWorld.js +232 -40
- 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 +16 -17
- 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/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +10 -0
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/gameContext.d.ts +44 -2
- package/dist/runtime/gameContext.js +100 -4
- package/dist/runtime/hostPersistence.d.ts +21 -0
- package/dist/runtime/hostPersistence.js +50 -0
- package/dist/runtime/inputSnapshot.d.ts +7 -0
- package/dist/runtime/inputSnapshot.js +12 -0
- package/dist/runtime/motionIntents.d.ts +25 -0
- package/dist/runtime/motionIntents.js +25 -0
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/scene/entityStore.d.ts +8 -0
- package/dist/scene/entityStore.js +25 -2
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +18 -0
- package/dist/scene/objectQuery.js +108 -0
- package/dist/scene/objectStore.d.ts +11 -0
- package/dist/scene/objectStore.js +109 -4
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/spatial.d.ts +21 -0
- package/dist/scene/spatial.js +63 -1
- 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/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/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/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- 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/world/environmentSummary.d.ts +53 -0
- package/dist/world/environmentSummary.js +129 -0
- package/dist/world/features.d.ts +87 -6
- package/dist/world/features.js +52 -2
- package/dist/world/gridInstances.d.ts +13 -0
- package/dist/world/gridInstances.js +22 -0
- package/dist/world/placementController.d.ts +22 -1
- package/dist/world/placementController.js +56 -0
- package/dist/world/terrain.d.ts +23 -1
- package/dist/world/terrain.js +64 -1
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/llms.txt +4102 -0
- package/package.json +3 -2
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Touch control scheme derived from a game's existing action bindings: the
|
|
3
|
+
* capture layer renders a virtual joystick, on-screen buttons, and a gesture
|
|
4
|
+
* surface, and feeds synthetic `touch:<action>` codes into the same
|
|
5
|
+
* ActionStateTracker the keyboard uses, so games gain touch play without any
|
|
6
|
+
* per-game input code. Games refine the derived defaults through
|
|
7
|
+
* TouchControlsConfig (gesture bindings, button curation) or opt out entirely
|
|
8
|
+
* with `touch: false`.
|
|
9
|
+
*/
|
|
10
|
+
import { isHotbarSlotAction } from "./actionBindings.js";
|
|
11
|
+
export const TOUCH_CODE_PREFIX = "touch:";
|
|
12
|
+
export function touchCode(action) {
|
|
13
|
+
return `${TOUCH_CODE_PREFIX}${action}`;
|
|
14
|
+
}
|
|
15
|
+
/** Every action gains a synthetic touch code alongside its physical codes. */
|
|
16
|
+
export function withTouchCodes(map) {
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const action of Object.keys(map ?? {})) {
|
|
19
|
+
const codes = map[action];
|
|
20
|
+
const synthetic = touchCode(action);
|
|
21
|
+
if (Array.isArray(codes)) {
|
|
22
|
+
result[action] = [...codes, synthetic];
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const modes = codes;
|
|
26
|
+
const toggleOnly = (modes.toggle?.length ?? 0) > 0 && (modes.hold?.length ?? 0) === 0;
|
|
27
|
+
result[action] = toggleOnly
|
|
28
|
+
? { hold: modes.hold, toggle: [...(modes.toggle ?? []), synthetic] }
|
|
29
|
+
: { hold: [...(modes.hold ?? []), synthetic], toggle: modes.toggle };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
const DEFAULT_LOOK_SENSITIVITY = 0.005;
|
|
35
|
+
const JOYSTICK_AXES = {
|
|
36
|
+
up: ["moveForward"],
|
|
37
|
+
down: ["moveBack"],
|
|
38
|
+
left: ["moveLeft", "turnLeft"],
|
|
39
|
+
right: ["moveRight", "turnRight"],
|
|
40
|
+
};
|
|
41
|
+
const BUTTONABLE_RESERVED = new Set(["jump", "sprint", "interact", "useAbility"]);
|
|
42
|
+
export function touchActionLabel(action) {
|
|
43
|
+
const spaced = action.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
44
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
45
|
+
}
|
|
46
|
+
function gestureActions(gestures) {
|
|
47
|
+
const used = new Set();
|
|
48
|
+
if (gestures === undefined)
|
|
49
|
+
return used;
|
|
50
|
+
for (const action of [gestures.tap, gestures.swipeUp, gestures.swipeDown, gestures.swipeLeft, gestures.swipeRight]) {
|
|
51
|
+
if (action !== undefined)
|
|
52
|
+
used.add(action);
|
|
53
|
+
}
|
|
54
|
+
const drag = gestures.drag;
|
|
55
|
+
for (const action of [drag?.left, drag?.right, drag?.up, drag?.down]) {
|
|
56
|
+
if (action !== undefined)
|
|
57
|
+
used.add(action);
|
|
58
|
+
}
|
|
59
|
+
return used;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Null means "render no touch controls" — either the game opted out or there
|
|
63
|
+
* is nothing to synthesize.
|
|
64
|
+
*/
|
|
65
|
+
export function deriveTouchScheme(input, { reserved, firstPerson, config }) {
|
|
66
|
+
if (config === false)
|
|
67
|
+
return null;
|
|
68
|
+
const actions = Object.keys(input ?? {});
|
|
69
|
+
const bound = new Set(actions);
|
|
70
|
+
const joystickMapped = new Set();
|
|
71
|
+
let joystick = null;
|
|
72
|
+
if (config?.movement !== false) {
|
|
73
|
+
const resolveAxis = (candidates) => {
|
|
74
|
+
const action = candidates.find((candidate) => bound.has(candidate)) ?? null;
|
|
75
|
+
if (action !== null)
|
|
76
|
+
joystickMapped.add(action);
|
|
77
|
+
return action;
|
|
78
|
+
};
|
|
79
|
+
const axes = {
|
|
80
|
+
up: resolveAxis(JOYSTICK_AXES.up),
|
|
81
|
+
down: resolveAxis(JOYSTICK_AXES.down),
|
|
82
|
+
left: resolveAxis(JOYSTICK_AXES.left),
|
|
83
|
+
right: resolveAxis(JOYSTICK_AXES.right),
|
|
84
|
+
};
|
|
85
|
+
joystick = axes.up === null && axes.down === null && axes.left === null && axes.right === null ? null : axes;
|
|
86
|
+
}
|
|
87
|
+
const gestures = config?.gestures ?? null;
|
|
88
|
+
const consumedByGestures = gestureActions(config?.gestures);
|
|
89
|
+
const hidden = new Set(config?.hidden ?? []);
|
|
90
|
+
let buttons;
|
|
91
|
+
if (config?.buttons !== undefined) {
|
|
92
|
+
buttons = config.buttons.map((spec) => typeof spec === "string"
|
|
93
|
+
? { action: spec, label: touchActionLabel(spec), icon: null }
|
|
94
|
+
: { action: spec.action, label: spec.label ?? touchActionLabel(spec.action), icon: spec.icon ?? null });
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
buttons = actions
|
|
98
|
+
.filter((action) => !joystickMapped.has(action))
|
|
99
|
+
.filter((action) => !isHotbarSlotAction(action))
|
|
100
|
+
.filter((action) => !consumedByGestures.has(action))
|
|
101
|
+
.filter((action) => !hidden.has(action))
|
|
102
|
+
.filter((action) => !reserved.has(action) || BUTTONABLE_RESERVED.has(action))
|
|
103
|
+
.map((action) => ({ action, label: touchActionLabel(action), icon: null }));
|
|
104
|
+
}
|
|
105
|
+
const look = config?.look ?? firstPerson;
|
|
106
|
+
if (joystick === null && buttons.length === 0 && gestures === null && !look)
|
|
107
|
+
return null;
|
|
108
|
+
return {
|
|
109
|
+
joystick,
|
|
110
|
+
buttons,
|
|
111
|
+
gestures,
|
|
112
|
+
look,
|
|
113
|
+
lookSensitivity: config?.lookSensitivity ?? DEFAULT_LOOK_SENSITIVITY,
|
|
114
|
+
};
|
|
115
|
+
}
|
package/dist/item/affix.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export interface RolledItem {
|
|
|
45
45
|
affixes: readonly RolledAffix[];
|
|
46
46
|
stats: Record<string, number>;
|
|
47
47
|
}
|
|
48
|
-
export
|
|
48
|
+
export { seededRng } from "../random/rng.js";
|
|
49
49
|
export interface AffixRoller {
|
|
50
50
|
rollRarity(rng: () => number): RarityTier;
|
|
51
51
|
roll(base: ItemBaseDef, rarityId: string, rng: () => number): RolledItem;
|
package/dist/item/affix.js
CHANGED
|
@@ -1,23 +1,5 @@
|
|
|
1
1
|
import { pickWeighted } from "../world/scatterItems.js";
|
|
2
|
-
|
|
3
|
-
const text = typeof seed === "number" ? seed.toString() : seed;
|
|
4
|
-
let h = 2166136261 >>> 0;
|
|
5
|
-
for (let i = 0; i < text.length; i++) {
|
|
6
|
-
h ^= text.charCodeAt(i);
|
|
7
|
-
h = Math.imul(h, 16777619);
|
|
8
|
-
}
|
|
9
|
-
return h >>> 0;
|
|
10
|
-
}
|
|
11
|
-
export function seededRng(seed) {
|
|
12
|
-
let a = hashSeed(seed) >>> 0;
|
|
13
|
-
return () => {
|
|
14
|
-
a = (a + 0x6d2b79f5) >>> 0;
|
|
15
|
-
let t = a;
|
|
16
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
17
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
18
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
19
|
-
};
|
|
20
|
-
}
|
|
2
|
+
export { seededRng } from "../random/rng.js";
|
|
21
3
|
function resolveRoll(roll, rng) {
|
|
22
4
|
if (typeof roll === "number")
|
|
23
5
|
return roll;
|
package/dist/meta/changelog.d.ts
CHANGED
package/dist/meta/changelog.js
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.8.0";
|
|
2
2
|
export const CHANGELOG = {
|
|
3
|
+
"0.8.0": {
|
|
4
|
+
migrate: [
|
|
5
|
+
"Bump every @jgengine/* dependency to ^0.8.0 (the eight packages version in lockstep).",
|
|
6
|
+
"Additive only, except for gameui — every other 0.7.0 API is unchanged; opt into any of the below by importing it directly.",
|
|
7
|
+
"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.",
|
|
8
|
+
"leveling({ thresholdMode: 'cumulative' }) is opt-in; the default 'perLevel' behavior is unchanged.",
|
|
9
|
+
"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.",
|
|
10
|
+
],
|
|
11
|
+
added: [
|
|
12
|
+
"Transport pipe seam (@jgengine/ws/pipe) — createWsBackend runs over any bidirectional string channel, not just a raw WebSocket, via TransportPipe/TransportPipeHandlers/TransportPipeFactory, with webSocketPipe as the default.",
|
|
13
|
+
"Browser-safe authoritative host — createGameHost and memoryPersistence moved from @jgengine/node to @jgengine/ws/host (zero Node dependencies; @jgengine/node re-exports both unchanged). @jgengine/ws/hostRouter's createHostRouter extracts the ws wire-protocol session logic into a transport-agnostic HostRouter; @jgengine/node's createGameWsServer is now a thin binding of this router onto the ws npm package. loopbackPipe connects a createWsBackend straight into an in-process router.",
|
|
14
|
+
"Socket.IO transport — @jgengine/ws/socketIoPipe (socketIoPipe, createSocketIoBackend) and @jgengine/node/socketIoServer (attachGameSocketIoServer) ride the existing ws JSON protocol over socket.io's send/message frames, no socket.io dependency in either package's types. New socketIo() adapter in @jgengine/core/runtime/adapter.",
|
|
15
|
+
"WebRTC peer-to-peer (@jgengine/ws/peer) — one browser tab hosts authoritatively with no server process: createPeerHost/createPeerGuest, encodePeerSignal/decodePeerSignal for copy-pasteable signaling codes, broadcastChannelSignaling for same-origin multi-tab play. New p2p() adapter.",
|
|
16
|
+
"LAN adapter + Fly sugar — lan() in @jgengine/core/runtime/adapter resolves to the page's own hostname over ws, no URL configuration; fly() is ws sugar for a Fly.io deploy. apps/dev's Vite server listens on the network and exposes ?p2p=host/join query params.",
|
|
17
|
+
"ws() (@jgengine/core/runtime/adapter) gained an optional url field, carried through by resolveShellMultiplayer.",
|
|
18
|
+
"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).",
|
|
19
|
+
"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).",
|
|
20
|
+
"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).",
|
|
21
|
+
"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).",
|
|
22
|
+
"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).",
|
|
23
|
+
"Declared-action intent board — turn/intent createIntentBoard for one-turn-ahead intents (Slay-the-Spire style): declare/peek/all/consume/clear (#168).",
|
|
24
|
+
"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).",
|
|
25
|
+
"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).",
|
|
26
|
+
"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).",
|
|
27
|
+
"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).",
|
|
28
|
+
"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).",
|
|
29
|
+
"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).",
|
|
30
|
+
"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).",
|
|
31
|
+
"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).",
|
|
32
|
+
"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).",
|
|
33
|
+
],
|
|
34
|
+
changed: [],
|
|
35
|
+
removed: [
|
|
36
|
+
"The gameui component kit (@jgengine/react/gameui) — the themed HUD kit and its GameUiThemeProvider/useGameUiTheme theming. Breaking for anyone importing @jgengine/react/gameui. Now ships as installable shadcn registry items at https://jgengine.com/r/<name>.json, styled with Tailwind + --jg-* CSS variables. GameIcon and friends moved to @jgengine/react/gameIcons.",
|
|
37
|
+
],
|
|
38
|
+
},
|
|
3
39
|
"0.7.0": {
|
|
4
40
|
migrate: [
|
|
5
41
|
"Bump every @jgengine/* dependency to ^0.7.0 (the eight packages version in lockstep).",
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* integration. Keeping it framework-free means the kinematics controller,
|
|
7
7
|
* the keyboard handler and the on-screen touch D-pad all speak the same
|
|
8
8
|
* vocabulary, and the translation is unit testable without a renderer.
|
|
9
|
-
* DOM capture
|
|
10
|
-
*
|
|
9
|
+
* DOM capture lives in `@jgengine/shell`'s GamePlayerShell (keyboard and
|
|
10
|
+
* touch controls) and its camera rigs (pointer lock, the per-frame look
|
|
11
|
+
* channel).
|
|
11
12
|
*/
|
|
12
13
|
export type MovementKey = "w" | "a" | "s" | "d" | "shift" | "control" | "c" | "space";
|
|
13
14
|
export type MovementKeysState = Record<MovementKey, boolean>;
|
|
@@ -79,6 +80,14 @@ export interface MovementFrameStep {
|
|
|
79
80
|
stepX: number;
|
|
80
81
|
stepZ: number;
|
|
81
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Per-game overrides for the gravity/jump feel, sourced from
|
|
85
|
+
* `GameDefinition.physics`. Omitted fields fall back to {@link MOVEMENT_TUNING}.
|
|
86
|
+
*/
|
|
87
|
+
export interface MovementTuningOverrides {
|
|
88
|
+
gravityAcceleration?: number;
|
|
89
|
+
jumpVelocity?: number;
|
|
90
|
+
}
|
|
82
91
|
/**
|
|
83
92
|
* Advance one frame of avatar kinematics. Mutates `motion` (velocity, jump,
|
|
84
93
|
* gravity, grounded) and returns the horizontal step to commit through the
|
|
@@ -89,7 +98,31 @@ export interface MovementFrameStep {
|
|
|
89
98
|
* this out of the render loop is what lets us assert "held forward for N frames
|
|
90
99
|
* crosses the room" in a unit test instead of only in the live game.
|
|
91
100
|
*/
|
|
92
|
-
export declare function advancePlayerMotion(motion: PlayerMotionState, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number): MovementFrameStep;
|
|
101
|
+
export declare function advancePlayerMotion(motion: PlayerMotionState, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number, tuning?: MovementTuningOverrides): MovementFrameStep;
|
|
102
|
+
/** A placed scene object the walking player collides against as a circle-vs-AABB obstacle. */
|
|
103
|
+
export interface CollisionObstacle {
|
|
104
|
+
position: readonly [number, number, number];
|
|
105
|
+
/** Box half-size on each axis. Default `[0.5, 0.5, 0.5]` (unit-box scene objects). */
|
|
106
|
+
halfExtents?: readonly [number, number, number];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a horizontal step against nearby placed objects with classic
|
|
110
|
+
* axis-separated sliding (try X, then Z against the post-X position) so
|
|
111
|
+
* walking into a wall stops the blocked axis but keeps sliding along the
|
|
112
|
+
* other. Obstacles are treated as circle-vs-AABB in the X/Z plane, inflated
|
|
113
|
+
* by `playerRadius`; an obstacle whose vertical span misses the player's
|
|
114
|
+
* feet-to-head span is skipped entirely. Callers should pre-filter to nearby
|
|
115
|
+
* objects — this also early-exits per obstacle on horizontal distance.
|
|
116
|
+
*/
|
|
117
|
+
export declare function resolveObstacleStep(current: readonly [number, number, number], stepX: number, stepZ: number, obstacles: readonly CollisionObstacle[], playerRadius?: number): MovementFrameStep;
|
|
118
|
+
/** Zeroes the off-axis component so travel is locked to a single world axis (`PlayerMovementConfig.mode: "axis"`). */
|
|
119
|
+
export declare function constrainStepToAxis(stepX: number, stepZ: number, axis: "x" | "z"): MovementFrameStep;
|
|
120
|
+
/**
|
|
121
|
+
* Snap a world position to its containing cell's center, matching the
|
|
122
|
+
* bounds-free form of `navGrid`'s and `tacticalGrid`'s `floor` + half-cell
|
|
123
|
+
* convention (`PlayerMovementConfig.mode: "grid"`).
|
|
124
|
+
*/
|
|
125
|
+
export declare function snapPositionToGrid(x: number, z: number, cellSize: number): [number, number];
|
|
93
126
|
/** Peak jump height from MOVEMENT_TUNING.jumpVelocity + gravity, with small buffer. */
|
|
94
127
|
export declare const MAX_JUMP_OFFSET = 1.15;
|
|
95
128
|
/** Camera yaw looks along -Z; character mesh faces +Z at body rotation.y = 0. */
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* integration. Keeping it framework-free means the kinematics controller,
|
|
7
7
|
* the keyboard handler and the on-screen touch D-pad all speak the same
|
|
8
8
|
* vocabulary, and the translation is unit testable without a renderer.
|
|
9
|
-
* DOM capture
|
|
10
|
-
*
|
|
9
|
+
* DOM capture lives in `@jgengine/shell`'s GamePlayerShell (keyboard and
|
|
10
|
+
* touch controls) and its camera rigs (pointer lock, the per-frame look
|
|
11
|
+
* channel).
|
|
11
12
|
*/
|
|
12
13
|
export function createEmptyMovementKeys() {
|
|
13
14
|
return { w: false, a: false, s: false, d: false, shift: false, control: false, c: false, space: false };
|
|
@@ -98,9 +99,11 @@ export function createPlayerMotionState() {
|
|
|
98
99
|
* this out of the render loop is what lets us assert "held forward for N frames
|
|
99
100
|
* crosses the room" in a unit test instead of only in the live game.
|
|
100
101
|
*/
|
|
101
|
-
export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpeed, rawDeltaSeconds) {
|
|
102
|
+
export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpeed, rawDeltaSeconds, tuning) {
|
|
102
103
|
const deltaSeconds = Math.min(rawDeltaSeconds, MOVEMENT_TUNING.maxFrameSeconds);
|
|
103
104
|
const targetSpeed = resolveTargetSpeed(intent, baseSpeed);
|
|
105
|
+
const gravityAcceleration = tuning?.gravityAcceleration ?? MOVEMENT_TUNING.gravityAcceleration;
|
|
106
|
+
const jumpVelocity = tuning?.jumpVelocity ?? MOVEMENT_TUNING.jumpVelocity;
|
|
104
107
|
let targetVelocityX = 0;
|
|
105
108
|
let targetVelocityZ = 0;
|
|
106
109
|
if (intent.moving) {
|
|
@@ -145,12 +148,12 @@ export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpee
|
|
|
145
148
|
}
|
|
146
149
|
const jumpPressed = intent.jumping;
|
|
147
150
|
if (jumpPressed && !motion.jumpHeld && motion.grounded && !intent.crouching) {
|
|
148
|
-
motion.verticalVelocity =
|
|
151
|
+
motion.verticalVelocity = jumpVelocity;
|
|
149
152
|
motion.grounded = false;
|
|
150
153
|
}
|
|
151
154
|
motion.jumpHeld = jumpPressed;
|
|
152
155
|
if (!motion.grounded || motion.verticalVelocity > 0) {
|
|
153
|
-
motion.verticalVelocity -=
|
|
156
|
+
motion.verticalVelocity -= gravityAcceleration * deltaSeconds;
|
|
154
157
|
motion.jumpOffset += motion.verticalVelocity * deltaSeconds;
|
|
155
158
|
if (motion.jumpOffset <= 0) {
|
|
156
159
|
motion.jumpOffset = 0;
|
|
@@ -163,6 +166,79 @@ export function advancePlayerMotion(motion, intent, forwardX, forwardZ, baseSpee
|
|
|
163
166
|
stepZ: motion.horizontalVelocityZ * deltaSeconds,
|
|
164
167
|
};
|
|
165
168
|
}
|
|
169
|
+
const DEFAULT_OBSTACLE_HALF_EXTENTS = [0.5, 0.5, 0.5];
|
|
170
|
+
const DEFAULT_OBSTACLE_PLAYER_RADIUS = 0.3;
|
|
171
|
+
/** Feet-to-head span used for the obstacle's vertical overlap test; matches DEFAULT_VOXEL_DIMS.height. */
|
|
172
|
+
const OBSTACLE_PLAYER_HEIGHT = 1.8;
|
|
173
|
+
function clampAxisStep(originalCoord, step, min, max) {
|
|
174
|
+
if (step === 0)
|
|
175
|
+
return 0;
|
|
176
|
+
const target = originalCoord + step;
|
|
177
|
+
if (target <= min || target >= max)
|
|
178
|
+
return step;
|
|
179
|
+
if (step > 0)
|
|
180
|
+
return originalCoord <= min ? min - originalCoord : 0;
|
|
181
|
+
return originalCoord >= max ? max - originalCoord : 0;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Resolve a horizontal step against nearby placed objects with classic
|
|
185
|
+
* axis-separated sliding (try X, then Z against the post-X position) so
|
|
186
|
+
* walking into a wall stops the blocked axis but keeps sliding along the
|
|
187
|
+
* other. Obstacles are treated as circle-vs-AABB in the X/Z plane, inflated
|
|
188
|
+
* by `playerRadius`; an obstacle whose vertical span misses the player's
|
|
189
|
+
* feet-to-head span is skipped entirely. Callers should pre-filter to nearby
|
|
190
|
+
* objects — this also early-exits per obstacle on horizontal distance.
|
|
191
|
+
*/
|
|
192
|
+
export function resolveObstacleStep(current, stepX, stepZ, obstacles, playerRadius = DEFAULT_OBSTACLE_PLAYER_RADIUS) {
|
|
193
|
+
const feetY = current[1];
|
|
194
|
+
const headY = feetY + OBSTACLE_PLAYER_HEIGHT;
|
|
195
|
+
const reachX = Math.abs(stepX) + playerRadius;
|
|
196
|
+
const reachZ = Math.abs(stepZ) + playerRadius;
|
|
197
|
+
const nearby = [];
|
|
198
|
+
for (const obstacle of obstacles) {
|
|
199
|
+
const halfExtents = obstacle.halfExtents ?? DEFAULT_OBSTACLE_HALF_EXTENTS;
|
|
200
|
+
const obstacleBottom = obstacle.position[1] - halfExtents[1];
|
|
201
|
+
const obstacleTop = obstacle.position[1] + halfExtents[1];
|
|
202
|
+
if (obstacleTop <= feetY || obstacleBottom >= headY)
|
|
203
|
+
continue;
|
|
204
|
+
const dx = obstacle.position[0] - current[0];
|
|
205
|
+
const dz = obstacle.position[2] - current[2];
|
|
206
|
+
if (Math.abs(dx) > halfExtents[0] + reachX || Math.abs(dz) > halfExtents[2] + reachZ)
|
|
207
|
+
continue;
|
|
208
|
+
nearby.push({
|
|
209
|
+
minX: obstacle.position[0] - halfExtents[0] - playerRadius,
|
|
210
|
+
maxX: obstacle.position[0] + halfExtents[0] + playerRadius,
|
|
211
|
+
minZ: obstacle.position[2] - halfExtents[2] - playerRadius,
|
|
212
|
+
maxZ: obstacle.position[2] + halfExtents[2] + playerRadius,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
let resultX = stepX;
|
|
216
|
+
for (const box of nearby) {
|
|
217
|
+
if (current[2] <= box.minZ || current[2] >= box.maxZ)
|
|
218
|
+
continue;
|
|
219
|
+
resultX = clampAxisStep(current[0], resultX, box.minX, box.maxX);
|
|
220
|
+
}
|
|
221
|
+
const nextX = current[0] + resultX;
|
|
222
|
+
let resultZ = stepZ;
|
|
223
|
+
for (const box of nearby) {
|
|
224
|
+
if (nextX <= box.minX || nextX >= box.maxX)
|
|
225
|
+
continue;
|
|
226
|
+
resultZ = clampAxisStep(current[2], resultZ, box.minZ, box.maxZ);
|
|
227
|
+
}
|
|
228
|
+
return { stepX: resultX, stepZ: resultZ };
|
|
229
|
+
}
|
|
230
|
+
/** Zeroes the off-axis component so travel is locked to a single world axis (`PlayerMovementConfig.mode: "axis"`). */
|
|
231
|
+
export function constrainStepToAxis(stepX, stepZ, axis) {
|
|
232
|
+
return axis === "x" ? { stepX, stepZ: 0 } : { stepX: 0, stepZ };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Snap a world position to its containing cell's center, matching the
|
|
236
|
+
* bounds-free form of `navGrid`'s and `tacticalGrid`'s `floor` + half-cell
|
|
237
|
+
* convention (`PlayerMovementConfig.mode: "grid"`).
|
|
238
|
+
*/
|
|
239
|
+
export function snapPositionToGrid(x, z, cellSize) {
|
|
240
|
+
return [(Math.floor(x / cellSize) + 0.5) * cellSize, (Math.floor(z / cellSize) + 0.5) * cellSize];
|
|
241
|
+
}
|
|
166
242
|
/** Peak jump height from MOVEMENT_TUNING.jumpVelocity + gravity, with small buffer. */
|
|
167
243
|
export const MAX_JUMP_OFFSET = 1.15;
|
|
168
244
|
/** Camera yaw looks along -Z; character mesh faces +Z at body rotation.y = 0. */
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voxel character controller (pure model).
|
|
3
|
+
*
|
|
4
|
+
* The base first-person controller in `movementModel.ts` assumes flat ground at
|
|
5
|
+
* y=0 — it integrates gravity into a `jumpOffset` and never asks the world how
|
|
6
|
+
* tall the ground is. That is fine for a plaza demo but wrong for any game where
|
|
7
|
+
* the player walks on placed blocks and digs down into the world: they float on
|
|
8
|
+
* an invisible floor, clip through walls, and can never descend.
|
|
9
|
+
*
|
|
10
|
+
* This module resolves the player as an axis-aligned box against a set of solid
|
|
11
|
+
* unit cells (a `SolidQuery`), so the same held-keys → intent pipeline drives a
|
|
12
|
+
* body that stands on blocks, falls into holes, is stopped by walls, and steps
|
|
13
|
+
* up single-block ledges. Keeping it renderer-free means the shell can drive it
|
|
14
|
+
* from live scene objects while the descent/landing/step-up behaviour stays unit
|
|
15
|
+
* testable without three.js.
|
|
16
|
+
*
|
|
17
|
+
* Cell convention matches the shell's object rendering and the voxel raycast:
|
|
18
|
+
* cell (x,y,z) is the solid box X∈[x-0.5,x+0.5], Y∈[y,y+1], Z∈[z-0.5,z+0.5]. A
|
|
19
|
+
* block at cell y therefore has its top face at world y+1, so a player standing
|
|
20
|
+
* on the surface layer (cell y=-1) rests with their feet at y=0.
|
|
21
|
+
*/
|
|
22
|
+
import { type MovementIntent, type MovementTuningOverrides } from "./movementModel.js";
|
|
23
|
+
export type SolidQuery = (x: number, y: number, z: number) => boolean;
|
|
24
|
+
/** Footprint + height of the player box, in world units. */
|
|
25
|
+
export interface VoxelPlayerDims {
|
|
26
|
+
/** Half the box width on both horizontal axes. */
|
|
27
|
+
halfWidth: number;
|
|
28
|
+
/** Box height measured up from the feet. */
|
|
29
|
+
height: number;
|
|
30
|
+
/** Tallest ledge the player walks up without jumping. */
|
|
31
|
+
stepHeight: number;
|
|
32
|
+
}
|
|
33
|
+
export declare const DEFAULT_VOXEL_DIMS: VoxelPlayerDims;
|
|
34
|
+
/** Mutable kinematic state carried between frames by the voxel controller. */
|
|
35
|
+
export interface VoxelPlayerBody {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
z: number;
|
|
39
|
+
velocityX: number;
|
|
40
|
+
velocityY: number;
|
|
41
|
+
velocityZ: number;
|
|
42
|
+
grounded: boolean;
|
|
43
|
+
jumpHeld: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function createVoxelPlayerBody(x: number, y: number, z: number): VoxelPlayerBody;
|
|
46
|
+
/** Samples the terrain floor height at a point; unset means flat ground plays no part in support. */
|
|
47
|
+
export type GroundHeightQuery = (x: number, z: number) => number;
|
|
48
|
+
/**
|
|
49
|
+
* Advance one frame of voxel-collided player kinematics. Mutates `body`
|
|
50
|
+
* (position, velocity, grounded) in place.
|
|
51
|
+
*
|
|
52
|
+
* `forwardX`/`forwardZ` are the camera heading projected onto the ground plane
|
|
53
|
+
* (need not be normalized). Pulling this out of the render loop is what lets us
|
|
54
|
+
* assert "dig the block underfoot and the player drops a level" in a unit test.
|
|
55
|
+
*
|
|
56
|
+
* `groundHeight`, when supplied, is an additional solid floor sampled at the
|
|
57
|
+
* body's (x, z): the player never sinks below it and standing on it counts as
|
|
58
|
+
* supported (resets jump state like landing on a solid cell). Where terrain and
|
|
59
|
+
* a solid cell's top disagree, the higher of the two wins.
|
|
60
|
+
*/
|
|
61
|
+
export declare function advanceVoxelPlayer(body: VoxelPlayerBody, intent: MovementIntent, forwardX: number, forwardZ: number, baseSpeed: number, rawDeltaSeconds: number, isSolid: SolidQuery, dims?: VoxelPlayerDims, tuning?: MovementTuningOverrides, groundHeight?: GroundHeightQuery): void;
|