@jgengine/shell 0.8.0 → 0.10.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 +36 -0
- package/README.md +11 -0
- package/dist/GamePlayerShell.d.ts +10 -22
- package/dist/GamePlayerShell.js +709 -264
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +9 -0
- package/dist/audio/audioEngine.js +37 -0
- package/dist/audio/musicDirector.d.ts +31 -0
- package/dist/audio/musicDirector.js +125 -0
- package/dist/audio/musicVoices.d.ts +10 -0
- package/dist/audio/musicVoices.js +338 -0
- package/dist/audio/synthEngine.d.ts +9 -0
- package/dist/audio/synthEngine.js +57 -0
- package/dist/behaviour.d.ts +5 -24
- package/dist/behaviour.js +14 -50
- package/dist/behaviourAttach.d.ts +9 -0
- package/dist/behaviourAttach.js +32 -0
- package/dist/behaviourDriver.d.ts +7 -0
- package/dist/behaviourDriver.js +21 -0
- package/dist/camera/GameCameraRig.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +3 -0
- package/dist/camera/GameFirstPersonCamera.js +35 -4
- package/dist/camera/GameInspectionCamera.js +12 -1
- package/dist/camera/GameOrbitCamera.js +44 -1
- package/dist/camera/PlayerFov.d.ts +18 -0
- package/dist/camera/PlayerFov.js +48 -0
- package/dist/camera/cameraBlendMath.d.ts +13 -0
- package/dist/camera/cameraBlendMath.js +36 -0
- package/dist/camera/cameraRigs.d.ts +3 -0
- package/dist/camera/cameraRigs.js +36 -33
- package/dist/camera/fovPreference.d.ts +25 -0
- package/dist/camera/fovPreference.js +73 -0
- package/dist/camera/index.d.ts +3 -0
- package/dist/camera/index.js +3 -0
- package/dist/camera/orbitCameraMath.d.ts +18 -0
- package/dist/camera/orbitCameraMath.js +6 -1
- package/dist/camera/rigMath.d.ts +10 -0
- package/dist/camera/rigMath.js +36 -1
- package/dist/camera/shakeChannel.d.ts +3 -17
- package/dist/camera/shakeChannel.js +2 -21
- package/dist/camera/shakeChannelMath.d.ts +8 -0
- package/dist/camera/shakeChannelMath.js +21 -0
- package/dist/cartridge.d.ts +145 -0
- package/dist/cartridge.js +246 -0
- package/dist/commandSink.d.ts +20 -0
- package/dist/commandSink.js +27 -0
- package/dist/defineGame.js +4 -1
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +181 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +65 -1
- package/dist/devtools/DevtoolsOverlay.js +383 -41
- package/dist/devtools/collisionDebug.d.ts +57 -0
- package/dist/devtools/collisionDebug.js +127 -0
- package/dist/devtools/collisionDebugMath.d.ts +103 -0
- package/dist/devtools/collisionDebugMath.js +129 -0
- package/dist/environment/Daylight.d.ts +20 -8
- package/dist/environment/Daylight.js +63 -15
- package/dist/environment/EnvironmentScene.js +91 -35
- package/dist/environment/RoadRibbons.d.ts +7 -0
- package/dist/environment/RoadRibbons.js +57 -0
- package/dist/environment/groundPadMath.d.ts +2 -2
- package/dist/environment/groundPadMath.js +1 -1
- package/dist/environment/index.d.ts +2 -1
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +10 -0
- package/dist/environment/skyLightingPolicy.js +6 -0
- package/dist/input/mouseLook.d.ts +27 -0
- package/dist/input/mouseLook.js +35 -0
- package/dist/inputSink.d.ts +18 -0
- package/dist/inputSink.js +35 -0
- package/dist/materialOverride.d.ts +4 -6
- package/dist/materialOverride.js +12 -16
- package/dist/multiplayer.js +7 -3
- package/dist/pointer/PointerProbe.js +6 -2
- package/dist/pointer/pointerService.d.ts +3 -0
- package/dist/pointer/pointerService.js +6 -0
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +82 -0
- package/dist/postfx/gradeShader.d.ts +4 -0
- package/dist/postfx/gradeShader.js +64 -0
- package/dist/render/modelRender.d.ts +10 -1
- package/dist/render/modelRender.js +32 -5
- package/dist/render/resolveModel.d.ts +14 -0
- package/dist/render/resolveModel.js +24 -0
- package/dist/settings/QuickControls.d.ts +4 -0
- package/dist/settings/QuickControls.js +42 -0
- package/dist/settings/SettingsChrome.d.ts +1 -0
- package/dist/settings/SettingsChrome.js +10 -0
- package/dist/settings/SettingsMenu.d.ts +6 -0
- package/dist/settings/SettingsMenu.js +148 -0
- package/dist/settings/SettingsRuntime.d.ts +11 -0
- package/dist/settings/SettingsRuntime.js +19 -0
- package/dist/settings/appliedSettings.d.ts +14 -0
- package/dist/settings/appliedSettings.js +27 -0
- package/dist/settings/settingsController.d.ts +20 -0
- package/dist/settings/settingsController.js +165 -0
- package/dist/structures/GeneratedBuilding.d.ts +10 -0
- package/dist/structures/GeneratedBuilding.js +96 -8
- package/dist/structures/index.d.ts +1 -1
- package/dist/structures/index.js +1 -1
- package/dist/terrain/CarvedTerrain.d.ts +5 -1
- package/dist/terrain/CarvedTerrain.js +6 -5
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +4 -2
- package/dist/terrain/grassBudget.d.ts +3 -0
- package/dist/terrain/grassBudget.js +6 -0
- package/dist/terrain/grassGeometry.js +1 -1
- package/dist/terrain/terrainDetailMaterial.d.ts +14 -0
- package/dist/terrain/terrainDetailMaterial.js +90 -0
- package/dist/terrain/terrainMath.d.ts +8 -0
- package/dist/terrain/terrainMath.js +9 -0
- package/dist/touch/OrientationHint.d.ts +3 -0
- package/dist/touch/OrientationHint.js +13 -0
- package/dist/touch/TouchControlsOverlay.d.ts +17 -1
- package/dist/touch/TouchControlsOverlay.js +125 -9
- package/dist/visibility/CullingProvider.d.ts +21 -0
- package/dist/visibility/CullingProvider.js +134 -0
- package/dist/vision/FrustumSensorHud.d.ts +1 -6
- package/dist/vision/FrustumSensorHud.js +25 -27
- package/dist/vision/frustumSampleEqual.d.ts +2 -0
- package/dist/vision/frustumSampleEqual.js +10 -0
- package/dist/water/Ocean.js +1 -1
- package/dist/water/OceanConfig.d.ts +13 -0
- package/dist/water/OceanConfig.js +25 -17
- package/dist/water/index.d.ts +1 -1
- package/dist/water/index.js +1 -1
- package/dist/weather/FireSpreadLayer.js +7 -2
- package/dist/weather/RainField.d.ts +3 -1
- package/dist/weather/RainField.js +4 -4
- package/dist/weather/SnowField.d.ts +3 -1
- package/dist/weather/SnowField.js +4 -4
- package/dist/weather/fireSpreadPose.d.ts +2 -0
- package/dist/weather/fireSpreadPose.js +4 -0
- package/dist/weather/weatherMath.d.ts +5 -1
- package/dist/weather/weatherMath.js +7 -2
- package/dist/world/DataObjects.d.ts +1 -1
- package/dist/world/DataObjects.js +3 -1
- package/dist/world/SpriteBatch.d.ts +44 -0
- package/dist/world/SpriteBatch.js +112 -0
- package/dist/world/WorldHud.d.ts +6 -1
- package/dist/world/WorldHud.js +95 -48
- package/dist/world/entityPose.d.ts +14 -0
- package/dist/world/entityPose.js +10 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +4 -0
- package/dist/world/worldBarSamples.d.ts +30 -0
- package/dist/world/worldBarSamples.js +57 -0
- package/dist/worldSync.d.ts +10 -0
- package/dist/worldSync.js +19 -0
- package/llms.txt +1522 -1143
- package/package.json +4 -4
package/dist/camera/rigMath.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare function clamp(value: number, min: number, max: number): number;
|
|
|
11
11
|
export declare function lerp(from: number, to: number, blend: number): number;
|
|
12
12
|
export declare function smoothstep(t: number): number;
|
|
13
13
|
export declare function forwardVector(yaw: number): Vec3;
|
|
14
|
+
/** Screen-right of a yaw: `forward × up` with up = +Y. */
|
|
14
15
|
export declare function rightVector(yaw: number): Vec3;
|
|
15
16
|
export interface ResolvedTopDown {
|
|
16
17
|
height: number;
|
|
@@ -87,8 +88,17 @@ export interface ResolvedChase {
|
|
|
87
88
|
lookHeight: number;
|
|
88
89
|
springDamping: number;
|
|
89
90
|
shakePerSpeed: number;
|
|
91
|
+
leadTime: number;
|
|
92
|
+
leadMax: number;
|
|
93
|
+
bankPerYawRate: number;
|
|
94
|
+
bankMax: number;
|
|
95
|
+
bankDamping: number;
|
|
90
96
|
}
|
|
91
97
|
export declare function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase;
|
|
98
|
+
/** Velocity-lead the follow point (#286.9): aim `leadTime` seconds along the target's frame velocity, clamped to `leadMax`. */
|
|
99
|
+
export declare function leadFollowPoint(follow: Vec3, previous: Vec3, dt: number, resolved: ResolvedChase): Vec3;
|
|
100
|
+
/** One smoothing step of the chase rig's turn-bank roll (#286.10): roll opposes yaw rate, clamped and exponentially damped. */
|
|
101
|
+
export declare function bankRollStep(currentRoll: number, yaw: number, previousYaw: number, dt: number, resolved: ResolvedChase): number;
|
|
92
102
|
export interface ResolvedObserver {
|
|
93
103
|
distance: number;
|
|
94
104
|
height: number;
|
package/dist/camera/rigMath.js
CHANGED
|
@@ -24,8 +24,9 @@ export function smoothstep(t) {
|
|
|
24
24
|
export function forwardVector(yaw) {
|
|
25
25
|
return { x: Math.sin(yaw), y: 0, z: Math.cos(yaw) };
|
|
26
26
|
}
|
|
27
|
+
/** Screen-right of a yaw: `forward × up` with up = +Y. */
|
|
27
28
|
export function rightVector(yaw) {
|
|
28
|
-
return { x: Math.cos(yaw), y: 0, z:
|
|
29
|
+
return { x: -Math.cos(yaw), y: 0, z: Math.sin(yaw) };
|
|
29
30
|
}
|
|
30
31
|
export function resolveTopDown(config) {
|
|
31
32
|
return {
|
|
@@ -207,8 +208,42 @@ export function resolveChase(config) {
|
|
|
207
208
|
lookHeight: config?.lookHeight ?? 1.2,
|
|
208
209
|
springDamping: config?.springDamping ?? 6,
|
|
209
210
|
shakePerSpeed: config?.shakePerSpeed ?? 0,
|
|
211
|
+
leadTime: config?.lead?.time ?? 0,
|
|
212
|
+
leadMax: config?.lead?.max ?? 4,
|
|
213
|
+
bankPerYawRate: config?.bank?.perYawRate ?? 0,
|
|
214
|
+
bankMax: config?.bank?.max ?? 0.35,
|
|
215
|
+
bankDamping: config?.bank?.damping ?? 8,
|
|
210
216
|
};
|
|
211
217
|
}
|
|
218
|
+
/** Velocity-lead the follow point (#286.9): aim `leadTime` seconds along the target's frame velocity, clamped to `leadMax`. */
|
|
219
|
+
export function leadFollowPoint(follow, previous, dt, resolved) {
|
|
220
|
+
if (resolved.leadTime <= 0 || dt <= 0)
|
|
221
|
+
return follow;
|
|
222
|
+
let leadX = ((follow.x - previous.x) / dt) * resolved.leadTime;
|
|
223
|
+
let leadZ = ((follow.z - previous.z) / dt) * resolved.leadTime;
|
|
224
|
+
const magnitude = Math.hypot(leadX, leadZ);
|
|
225
|
+
if (magnitude > resolved.leadMax) {
|
|
226
|
+
leadX *= resolved.leadMax / magnitude;
|
|
227
|
+
leadZ *= resolved.leadMax / magnitude;
|
|
228
|
+
}
|
|
229
|
+
return { x: follow.x + leadX, y: follow.y, z: follow.z + leadZ };
|
|
230
|
+
}
|
|
231
|
+
function wrapAngle(angle) {
|
|
232
|
+
let wrapped = angle;
|
|
233
|
+
while (wrapped > Math.PI)
|
|
234
|
+
wrapped -= Math.PI * 2;
|
|
235
|
+
while (wrapped < -Math.PI)
|
|
236
|
+
wrapped += Math.PI * 2;
|
|
237
|
+
return wrapped;
|
|
238
|
+
}
|
|
239
|
+
/** One smoothing step of the chase rig's turn-bank roll (#286.10): roll opposes yaw rate, clamped and exponentially damped. */
|
|
240
|
+
export function bankRollStep(currentRoll, yaw, previousYaw, dt, resolved) {
|
|
241
|
+
if (resolved.bankPerYawRate <= 0)
|
|
242
|
+
return 0;
|
|
243
|
+
const yawRate = dt > 0 ? wrapAngle(yaw - previousYaw) / dt : 0;
|
|
244
|
+
const target = clamp(-yawRate * resolved.bankPerYawRate, -resolved.bankMax, resolved.bankMax);
|
|
245
|
+
return currentRoll + (target - currentRoll) * Math.min(1, resolved.bankDamping * dt);
|
|
246
|
+
}
|
|
212
247
|
export function resolveObserver(config) {
|
|
213
248
|
return {
|
|
214
249
|
distance: config?.distance ?? 8,
|
|
@@ -1,20 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
* Add a trauma impulse. `amplitude` is a 0..1 fraction (1 = a full boss slam);
|
|
5
|
-
* `decayPerSecond` optionally overrides how fast this and subsequent trauma
|
|
6
|
-
* bleed off. Any gameplay system (combat hitstop, explosions, vehicle crashes)
|
|
7
|
-
* can call this — the active rig reads the resulting shake every frame.
|
|
8
|
-
*/
|
|
9
|
-
shake(amplitude: number, decayPerSecond?: number): void;
|
|
10
|
-
/** Advance decay + the shake clock (called once per frame by the mounted rig). */
|
|
11
|
-
step(dt: number): void;
|
|
12
|
-
/** Current positional + roll shake for the given tuning. */
|
|
13
|
-
sample(config?: Parameters<typeof shakeOffset>[1]): ShakeOffset;
|
|
14
|
-
/** Live trauma in [0,1] (for HUD/debug). */
|
|
15
|
-
trauma(): number;
|
|
16
|
-
}
|
|
17
|
-
export declare function createCameraShakeChannel(defaultDecayPerSecond?: number): CameraShakeChannel;
|
|
1
|
+
import { type CameraShakeChannel } from "./shakeChannelMath.js";
|
|
2
|
+
export type { CameraShakeChannel } from "./shakeChannelMath.js";
|
|
3
|
+
export { createCameraShakeChannel } from "./shakeChannelMath.js";
|
|
18
4
|
/**
|
|
19
5
|
* Process-wide default channel. A shell mounts its own channel via
|
|
20
6
|
* `CameraShakeContext`, but game systems that have no React context (e.g. a
|
|
@@ -1,25 +1,6 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
-
import {
|
|
3
|
-
export
|
|
4
|
-
const state = createTrauma();
|
|
5
|
-
let decay = defaultDecayPerSecond;
|
|
6
|
-
return {
|
|
7
|
-
shake(amplitude, decayPerSecond) {
|
|
8
|
-
if (decayPerSecond !== undefined)
|
|
9
|
-
decay = decayPerSecond;
|
|
10
|
-
addTrauma(state, amplitude);
|
|
11
|
-
},
|
|
12
|
-
step(dt) {
|
|
13
|
-
stepTrauma(state, decay, dt);
|
|
14
|
-
},
|
|
15
|
-
sample(config) {
|
|
16
|
-
return shakeOffset(state, config);
|
|
17
|
-
},
|
|
18
|
-
trauma() {
|
|
19
|
-
return state.trauma;
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
}
|
|
2
|
+
import { createCameraShakeChannel, } from "./shakeChannelMath.js";
|
|
3
|
+
export { createCameraShakeChannel } from "./shakeChannelMath.js";
|
|
23
4
|
/**
|
|
24
5
|
* Process-wide default channel. A shell mounts its own channel via
|
|
25
6
|
* `CameraShakeContext`, but game systems that have no React context (e.g. a
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { shakeOffset, type ShakeOffset } from "./rigMath.js";
|
|
2
|
+
export interface CameraShakeChannel {
|
|
3
|
+
shake(amplitude: number, decayPerSecond?: number): void;
|
|
4
|
+
step(dt: number): void;
|
|
5
|
+
sample(config?: Parameters<typeof shakeOffset>[1]): ShakeOffset;
|
|
6
|
+
trauma(): number;
|
|
7
|
+
}
|
|
8
|
+
export declare function createCameraShakeChannel(defaultDecayPerSecond?: number): CameraShakeChannel;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { addTrauma, createTrauma, shakeOffset, stepTrauma } from "./rigMath.js";
|
|
2
|
+
export function createCameraShakeChannel(defaultDecayPerSecond = 1.6) {
|
|
3
|
+
const state = createTrauma();
|
|
4
|
+
let decay = defaultDecayPerSecond;
|
|
5
|
+
return {
|
|
6
|
+
shake(amplitude, decayPerSecond) {
|
|
7
|
+
if (decayPerSecond !== undefined)
|
|
8
|
+
decay = decayPerSecond;
|
|
9
|
+
addTrauma(state, amplitude);
|
|
10
|
+
},
|
|
11
|
+
step(dt) {
|
|
12
|
+
stepTrauma(state, decay, dt);
|
|
13
|
+
},
|
|
14
|
+
sample(config) {
|
|
15
|
+
return shakeOffset(state, config);
|
|
16
|
+
},
|
|
17
|
+
trauma() {
|
|
18
|
+
return state.trauma;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { type ComponentType } from "react";
|
|
2
|
+
import { type CartridgeSpec } from "@jgengine/core/cartridge/spec";
|
|
3
|
+
import type { AbilitySlotState } from "@jgengine/core/combat/abilityKit";
|
|
4
|
+
import type { HudAnchor } from "@jgengine/core/ui/hudLayout";
|
|
5
|
+
import { type GameConfig } from "./defineGame.js";
|
|
6
|
+
import type { PlayableGame } from "./registry.js";
|
|
7
|
+
export type CartridgePanelItem = {
|
|
8
|
+
kind: "vital";
|
|
9
|
+
stat: string;
|
|
10
|
+
label: string;
|
|
11
|
+
tone?: string;
|
|
12
|
+
width?: number;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "xp";
|
|
15
|
+
width?: number;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "timer";
|
|
18
|
+
label: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: "score";
|
|
21
|
+
source: "kills";
|
|
22
|
+
label: string;
|
|
23
|
+
digits?: number;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "abilityBar";
|
|
26
|
+
icons: Record<string, string>;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "component";
|
|
29
|
+
Component: ComponentType;
|
|
30
|
+
};
|
|
31
|
+
export interface CartridgeHudPanelSpec {
|
|
32
|
+
id: string;
|
|
33
|
+
anchor: HudAnchor;
|
|
34
|
+
inset?: {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
};
|
|
38
|
+
items: readonly CartridgePanelItem[];
|
|
39
|
+
}
|
|
40
|
+
export type CartridgeResultLine = {
|
|
41
|
+
label: string;
|
|
42
|
+
accent?: boolean;
|
|
43
|
+
} & ({
|
|
44
|
+
source: "kills" | "level";
|
|
45
|
+
} | {
|
|
46
|
+
value: string | number;
|
|
47
|
+
});
|
|
48
|
+
export interface CartridgeScreens {
|
|
49
|
+
start?: {
|
|
50
|
+
title: string;
|
|
51
|
+
subtitle?: string;
|
|
52
|
+
buttonLabel?: string;
|
|
53
|
+
};
|
|
54
|
+
win?: {
|
|
55
|
+
title: string;
|
|
56
|
+
lines: readonly CartridgeResultLine[];
|
|
57
|
+
};
|
|
58
|
+
lose?: {
|
|
59
|
+
title: string;
|
|
60
|
+
subtitle?: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export interface CartridgeAbilitySlot {
|
|
64
|
+
id: string;
|
|
65
|
+
icon: string;
|
|
66
|
+
state: AbilitySlotState;
|
|
67
|
+
cooldownFraction: number;
|
|
68
|
+
justCast: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface CartridgePanels {
|
|
71
|
+
Vital: ComponentType<{
|
|
72
|
+
value: {
|
|
73
|
+
current: number;
|
|
74
|
+
max: number;
|
|
75
|
+
min?: number;
|
|
76
|
+
};
|
|
77
|
+
label: string;
|
|
78
|
+
tone?: string;
|
|
79
|
+
width?: number;
|
|
80
|
+
}>;
|
|
81
|
+
Xp: ComponentType<{
|
|
82
|
+
fraction: number;
|
|
83
|
+
level?: number;
|
|
84
|
+
width?: number;
|
|
85
|
+
}>;
|
|
86
|
+
Timer: ComponentType<{
|
|
87
|
+
seconds: number;
|
|
88
|
+
label: string;
|
|
89
|
+
}>;
|
|
90
|
+
Score: ComponentType<{
|
|
91
|
+
value: number;
|
|
92
|
+
label: string;
|
|
93
|
+
digits?: number;
|
|
94
|
+
}>;
|
|
95
|
+
AbilityBar: ComponentType<{
|
|
96
|
+
slots: readonly CartridgeAbilitySlot[];
|
|
97
|
+
}>;
|
|
98
|
+
DraftModal: ComponentType<{
|
|
99
|
+
offers: readonly {
|
|
100
|
+
id: string;
|
|
101
|
+
label: string;
|
|
102
|
+
}[];
|
|
103
|
+
choose(id: string): void;
|
|
104
|
+
}>;
|
|
105
|
+
StartScreen: ComponentType<{
|
|
106
|
+
title: string;
|
|
107
|
+
subtitle?: string;
|
|
108
|
+
buttonLabel?: string;
|
|
109
|
+
begin(): void;
|
|
110
|
+
}>;
|
|
111
|
+
Countdown: ComponentType<{
|
|
112
|
+
seconds: number;
|
|
113
|
+
}>;
|
|
114
|
+
WinScreen: ComponentType<{
|
|
115
|
+
title: string;
|
|
116
|
+
lines: readonly {
|
|
117
|
+
label: string;
|
|
118
|
+
value: string | number;
|
|
119
|
+
accent?: boolean;
|
|
120
|
+
}[];
|
|
121
|
+
}>;
|
|
122
|
+
LoseScreen: ComponentType<{
|
|
123
|
+
title: string;
|
|
124
|
+
subtitle?: string;
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
127
|
+
export type CartridgeConfig = CartridgeSpec & {
|
|
128
|
+
name: string;
|
|
129
|
+
panels: CartridgePanels;
|
|
130
|
+
hud: {
|
|
131
|
+
storageKey?: string;
|
|
132
|
+
panels: readonly CartridgeHudPanelSpec[];
|
|
133
|
+
};
|
|
134
|
+
screens: CartridgeScreens;
|
|
135
|
+
theme?: Record<string, string>;
|
|
136
|
+
world?: GameConfig["world"];
|
|
137
|
+
physics?: GameConfig["physics"];
|
|
138
|
+
assets?: GameConfig["assets"];
|
|
139
|
+
save?: GameConfig["save"];
|
|
140
|
+
camera?: GameConfig["camera"];
|
|
141
|
+
entitySprites?: GameConfig["entitySprites"];
|
|
142
|
+
worldItem?: GameConfig["worldItem"];
|
|
143
|
+
settings?: GameConfig["settings"];
|
|
144
|
+
};
|
|
145
|
+
export declare function cartridge(config: CartridgeConfig): PlayableGame;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { useFrame } from "@react-three/fiber";
|
|
4
|
+
import { createCartridge } from "@jgengine/core/cartridge/runtime";
|
|
5
|
+
import { WASD_KEYBINDS, leveled } from "@jgengine/core/cartridge/spec";
|
|
6
|
+
import { validateCartridge } from "@jgengine/core/cartridge/validate";
|
|
7
|
+
import { HudCanvas, HudPanel, useHudLayout } from "@jgengine/react";
|
|
8
|
+
import { useAbilitySlots, useEntityStat } from "@jgengine/react/hooks";
|
|
9
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
10
|
+
import { defineGame } from "./defineGame.js";
|
|
11
|
+
function useTick(intervalMs) {
|
|
12
|
+
const [, setTick] = useState(0);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const id = window.setInterval(() => setTick((value) => value + 1), intervalMs);
|
|
15
|
+
return () => window.clearInterval(id);
|
|
16
|
+
}, [intervalMs]);
|
|
17
|
+
}
|
|
18
|
+
function useRunPhase(ctx, runtime) {
|
|
19
|
+
const run = runtime.run(ctx);
|
|
20
|
+
useSyncExternalStore(run.subscribe, () => `${run.phase}:${run.pendingOffers?.map((offer) => offer.id).join(",") ?? ""}`);
|
|
21
|
+
}
|
|
22
|
+
function winSeconds(config) {
|
|
23
|
+
return config.rules.win?.kind === "survive" ? config.rules.win.seconds : null;
|
|
24
|
+
}
|
|
25
|
+
function VitalItem({ config, item }) {
|
|
26
|
+
const ctx = useGameContext();
|
|
27
|
+
const value = useEntityStat(ctx.player.userId, item.stat);
|
|
28
|
+
if (value === null)
|
|
29
|
+
return null;
|
|
30
|
+
return _jsx(config.panels.Vital, { value: value, label: item.label, tone: item.tone, width: item.width });
|
|
31
|
+
}
|
|
32
|
+
function XpItem({ config, item }) {
|
|
33
|
+
const ctx = useGameContext();
|
|
34
|
+
const xp = useEntityStat(ctx.player.userId, "xp");
|
|
35
|
+
const level = useEntityStat(ctx.player.userId, "level");
|
|
36
|
+
if (xp === null)
|
|
37
|
+
return null;
|
|
38
|
+
const fraction = (xp.current - xp.min) / Math.max(1, xp.max - xp.min);
|
|
39
|
+
return _jsx(config.panels.Xp, { fraction: fraction, level: level?.current, width: item.width });
|
|
40
|
+
}
|
|
41
|
+
function TimerItem({ config, runtime, item, }) {
|
|
42
|
+
const ctx = useGameContext();
|
|
43
|
+
const target = winSeconds(config);
|
|
44
|
+
const elapsed = runtime.run(ctx).playingSeconds;
|
|
45
|
+
const seconds = target === null ? elapsed : Math.max(0, target - elapsed);
|
|
46
|
+
return _jsx(config.panels.Timer, { seconds: seconds, label: item.label });
|
|
47
|
+
}
|
|
48
|
+
function ScoreItem({ config, runtime, item, }) {
|
|
49
|
+
const ctx = useGameContext();
|
|
50
|
+
return _jsx(config.panels.Score, { value: runtime.run(ctx).kills, label: item.label, digits: item.digits });
|
|
51
|
+
}
|
|
52
|
+
function AbilityBarItem({ config, runtime, item, }) {
|
|
53
|
+
const ctx = useGameContext();
|
|
54
|
+
const slots = useAbilitySlots(runtime.weaponKit(ctx));
|
|
55
|
+
return (_jsx(config.panels.AbilityBar, { slots: slots.map((slot) => ({
|
|
56
|
+
id: slot.id,
|
|
57
|
+
icon: item.icons[slot.id] ?? slot.id,
|
|
58
|
+
state: slot.state,
|
|
59
|
+
cooldownFraction: slot.cooldownFraction,
|
|
60
|
+
justCast: slot.justCast,
|
|
61
|
+
})) }));
|
|
62
|
+
}
|
|
63
|
+
function PanelItems({ config, runtime, items, }) {
|
|
64
|
+
return (_jsx(_Fragment, { children: items.map((item, index) => {
|
|
65
|
+
switch (item.kind) {
|
|
66
|
+
case "vital":
|
|
67
|
+
return _jsx(VitalItem, { config: config, item: item }, index);
|
|
68
|
+
case "xp":
|
|
69
|
+
return _jsx(XpItem, { config: config, item: item }, index);
|
|
70
|
+
case "timer":
|
|
71
|
+
return _jsx(TimerItem, { config: config, runtime: runtime, item: item }, index);
|
|
72
|
+
case "score":
|
|
73
|
+
return _jsx(ScoreItem, { config: config, runtime: runtime, item: item }, index);
|
|
74
|
+
case "abilityBar":
|
|
75
|
+
return _jsx(AbilityBarItem, { config: config, runtime: runtime, item: item }, index);
|
|
76
|
+
case "component":
|
|
77
|
+
return _jsx(item.Component, {}, index);
|
|
78
|
+
}
|
|
79
|
+
}) }));
|
|
80
|
+
}
|
|
81
|
+
function WinScreenHost({ config, runtime }) {
|
|
82
|
+
const ctx = useGameContext();
|
|
83
|
+
const level = useEntityStat(ctx.player.userId, "level");
|
|
84
|
+
const win = config.screens.win;
|
|
85
|
+
if (win === undefined)
|
|
86
|
+
return null;
|
|
87
|
+
const run = runtime.run(ctx);
|
|
88
|
+
const lines = win.lines.map((line) => ({
|
|
89
|
+
label: line.label,
|
|
90
|
+
accent: line.accent,
|
|
91
|
+
value: "value" in line ? line.value : line.source === "kills" ? run.kills : (level?.current ?? 1),
|
|
92
|
+
}));
|
|
93
|
+
return _jsx(config.panels.WinScreen, { title: win.title, lines: lines });
|
|
94
|
+
}
|
|
95
|
+
function DraftModalHost({ config, runtime }) {
|
|
96
|
+
const ctx = useGameContext();
|
|
97
|
+
const offers = runtime.run(ctx).pendingOffers;
|
|
98
|
+
if (offers === null)
|
|
99
|
+
return null;
|
|
100
|
+
return _jsx(config.panels.DraftModal, { offers: offers, choose: (id) => runtime.chooseUpgrade(ctx, id) });
|
|
101
|
+
}
|
|
102
|
+
function CartridgeUI({ config, runtime }) {
|
|
103
|
+
const ctx = useGameContext();
|
|
104
|
+
const layout = useHudLayout({ storageKey: config.hud.storageKey ?? config.name });
|
|
105
|
+
useRunPhase(ctx, runtime);
|
|
106
|
+
useTick(200);
|
|
107
|
+
const run = runtime.run(ctx);
|
|
108
|
+
return (_jsxs("div", { style: { ...config.theme, display: "contents" }, children: [run.phase === "start" && (_jsx(config.panels.StartScreen, { title: config.screens.start?.title ?? config.name, subtitle: config.screens.start?.subtitle, buttonLabel: config.screens.start?.buttonLabel, begin: () => runtime.begin(ctx) })), run.phase === "countdown" && _jsx(config.panels.Countdown, { seconds: Math.ceil(run.countdownRemaining) }), run.phase === "lost" && config.screens.lose !== undefined && (_jsx(config.panels.LoseScreen, { title: config.screens.lose.title, subtitle: config.screens.lose.subtitle })), run.phase === "won" && _jsx(WinScreenHost, { config: config, runtime: runtime }), run.phase === "playing" && (_jsxs(HudCanvas, { layout: layout, children: [_jsx(DraftModalHost, { config: config, runtime: runtime }), config.hud.panels.map((panel) => (_jsx(HudPanel, { id: panel.id, anchor: panel.anchor, inset: panel.inset, style: panel.items.length > 1
|
|
109
|
+
? { display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }
|
|
110
|
+
: undefined, children: _jsx(PanelItems, { config: config, runtime: runtime, items: panel.items }) }, panel.id)))] }))] }));
|
|
111
|
+
}
|
|
112
|
+
const MAX_BOLTS = 10;
|
|
113
|
+
const MAX_PULSES = 6;
|
|
114
|
+
function hideMesh(mesh) {
|
|
115
|
+
if (mesh !== null)
|
|
116
|
+
mesh.visible = false;
|
|
117
|
+
}
|
|
118
|
+
function BoltPool({ runtime, weaponId, weapon, }) {
|
|
119
|
+
const ctx = useGameContext();
|
|
120
|
+
const refs = useRef(new Array(MAX_BOLTS).fill(null));
|
|
121
|
+
useFrame(() => {
|
|
122
|
+
const run = runtime.run(ctx);
|
|
123
|
+
const now = ctx.time.now();
|
|
124
|
+
const bolts = run.bolts.filter((bolt) => bolt.weaponId === weaponId);
|
|
125
|
+
for (let i = 0; i < MAX_BOLTS; i += 1) {
|
|
126
|
+
const mesh = refs.current[i];
|
|
127
|
+
const bolt = bolts[i];
|
|
128
|
+
if (mesh === null)
|
|
129
|
+
continue;
|
|
130
|
+
if (bolt === undefined) {
|
|
131
|
+
hideMesh(mesh);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const t = Math.min(1, Math.max(0, (now - bolt.firedAt) / bolt.travelSeconds));
|
|
135
|
+
mesh.visible = true;
|
|
136
|
+
mesh.position.set(bolt.origin[0] + (bolt.target[0] - bolt.origin[0]) * t, bolt.origin[1] + 0.9, bolt.origin[2] + (bolt.target[2] - bolt.origin[2]) * t);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return (_jsx(_Fragment, { children: Array.from({ length: MAX_BOLTS }, (_, i) => (_jsxs("mesh", { ref: (mesh) => {
|
|
140
|
+
refs.current[i] = mesh;
|
|
141
|
+
}, visible: false, children: [_jsx("sphereGeometry", { args: [0.16, 8, 8] }), _jsx("meshStandardMaterial", { color: weapon.fxColor ?? "#8be9f0", emissive: weapon.fxEmissive ?? weapon.fxColor ?? "#8be9f0", emissiveIntensity: 1.6 })] }, i))) }));
|
|
142
|
+
}
|
|
143
|
+
function PulsePool({ runtime, weaponId, weapon, }) {
|
|
144
|
+
const ctx = useGameContext();
|
|
145
|
+
const refs = useRef(new Array(MAX_PULSES).fill(null));
|
|
146
|
+
useFrame(() => {
|
|
147
|
+
const run = runtime.run(ctx);
|
|
148
|
+
const now = ctx.time.now();
|
|
149
|
+
const pulses = run.pulses.filter((pulse) => pulse.weaponId === weaponId);
|
|
150
|
+
for (let i = 0; i < MAX_PULSES; i += 1) {
|
|
151
|
+
const mesh = refs.current[i];
|
|
152
|
+
const pulse = pulses[i];
|
|
153
|
+
if (mesh === null)
|
|
154
|
+
continue;
|
|
155
|
+
if (pulse === undefined) {
|
|
156
|
+
hideMesh(mesh);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const t = Math.min(1, Math.max(0, (now - pulse.firedAt) / pulse.durationSeconds));
|
|
160
|
+
const radius = Math.max(0.01, pulse.maxRadius * t);
|
|
161
|
+
mesh.visible = true;
|
|
162
|
+
mesh.position.set(pulse.at[0], pulse.at[1] + 0.05, pulse.at[2]);
|
|
163
|
+
mesh.scale.set(radius, radius, radius);
|
|
164
|
+
const material = mesh.material;
|
|
165
|
+
if (!Array.isArray(material))
|
|
166
|
+
material.opacity = 0.55 * (1 - t);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return (_jsx(_Fragment, { children: Array.from({ length: MAX_PULSES }, (_, i) => (_jsxs("mesh", { ref: (mesh) => {
|
|
170
|
+
refs.current[i] = mesh;
|
|
171
|
+
}, rotation: [-Math.PI / 2, 0, 0], visible: false, children: [_jsx("ringGeometry", { args: [0.85, 1, 48] }), _jsx("meshBasicMaterial", { color: weapon.fxColor ?? "#a566d9", transparent: true, opacity: 0.5 })] }, i))) }));
|
|
172
|
+
}
|
|
173
|
+
function OrbitPool({ runtime, weaponId, weapon, }) {
|
|
174
|
+
const ctx = useGameContext();
|
|
175
|
+
const maxBlades = Math.ceil(leveled(weapon.blades, weapon.maxLevel));
|
|
176
|
+
const groupRef = useRef(null);
|
|
177
|
+
const refs = useRef(new Array(maxBlades).fill(null));
|
|
178
|
+
useFrame(() => {
|
|
179
|
+
const run = runtime.run(ctx);
|
|
180
|
+
const player = ctx.scene.entity.get(ctx.player.userId);
|
|
181
|
+
if (player === null || run.phase !== "playing") {
|
|
182
|
+
if (groupRef.current !== null)
|
|
183
|
+
groupRef.current.visible = false;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const level = run.weaponLevel(weaponId);
|
|
187
|
+
const blades = leveled(weapon.blades, level);
|
|
188
|
+
const radius = leveled(weapon.radius, level);
|
|
189
|
+
const angle = ctx.time.now() * weapon.angularSpeed;
|
|
190
|
+
if (groupRef.current !== null)
|
|
191
|
+
groupRef.current.visible = true;
|
|
192
|
+
for (let i = 0; i < maxBlades; i += 1) {
|
|
193
|
+
const mesh = refs.current[i];
|
|
194
|
+
if (mesh === null)
|
|
195
|
+
continue;
|
|
196
|
+
if (i >= blades) {
|
|
197
|
+
hideMesh(mesh);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const theta = angle + (i * Math.PI * 2) / blades;
|
|
201
|
+
mesh.visible = true;
|
|
202
|
+
mesh.position.set(player.position[0] + Math.cos(theta) * radius, player.position[1] + 0.55, player.position[2] + Math.sin(theta) * radius);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
return (_jsx("group", { ref: groupRef, children: Array.from({ length: maxBlades }, (_, i) => (_jsxs("mesh", { ref: (mesh) => {
|
|
206
|
+
refs.current[i] = mesh;
|
|
207
|
+
}, visible: false, children: [_jsx("boxGeometry", { args: [0.32, 0.08, 0.14] }), _jsx("meshStandardMaterial", { color: weapon.fxColor ?? "#dfe9ea", emissive: weapon.fxEmissive ?? weapon.fxColor ?? "#dfe9ea", emissiveIntensity: 0.8 })] }, i))) }));
|
|
208
|
+
}
|
|
209
|
+
function CartridgeFxOverlay({ config, runtime }) {
|
|
210
|
+
return (_jsx("group", { children: Object.entries(config.weapons).map(([weaponId, weapon]) => {
|
|
211
|
+
if (weapon.kind === "projectile") {
|
|
212
|
+
return _jsx(BoltPool, { runtime: runtime, weaponId: weaponId, weapon: weapon }, weaponId);
|
|
213
|
+
}
|
|
214
|
+
if (weapon.kind === "pulse") {
|
|
215
|
+
return _jsx(PulsePool, { runtime: runtime, weaponId: weaponId, weapon: weapon }, weaponId);
|
|
216
|
+
}
|
|
217
|
+
if (weapon.kind === "orbit") {
|
|
218
|
+
return _jsx(OrbitPool, { runtime: runtime, weaponId: weaponId, weapon: weapon }, weaponId);
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}) }));
|
|
222
|
+
}
|
|
223
|
+
export function cartridge(config) {
|
|
224
|
+
const problems = validateCartridge(config);
|
|
225
|
+
if (problems.length > 0) {
|
|
226
|
+
throw new Error(`cartridge "${config.name}": invalid spec:\n${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
227
|
+
}
|
|
228
|
+
const runtime = createCartridge(config);
|
|
229
|
+
return defineGame({
|
|
230
|
+
name: config.name,
|
|
231
|
+
assets: config.assets,
|
|
232
|
+
features: { leaderboard: config.rules.killLeaderboardStat !== undefined },
|
|
233
|
+
world: config.world,
|
|
234
|
+
physics: config.physics,
|
|
235
|
+
input: config.input ?? WASD_KEYBINDS,
|
|
236
|
+
save: config.save ?? "none",
|
|
237
|
+
content: runtime.content,
|
|
238
|
+
loop: runtime.loop,
|
|
239
|
+
GameUI: () => _jsx(CartridgeUI, { config: config, runtime: runtime }),
|
|
240
|
+
WorldOverlay: () => _jsx(CartridgeFxOverlay, { config: config, runtime: runtime }),
|
|
241
|
+
camera: config.camera,
|
|
242
|
+
entitySprites: config.entitySprites,
|
|
243
|
+
worldItem: config.worldItem,
|
|
244
|
+
settings: config.settings,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
2
|
+
import type { LiveGameBackend } from "@jgengine/core/runtime/transport";
|
|
3
|
+
/** Where a gameplay command goes when the shell dispatches it — run locally, or sent to the authoritative host. */
|
|
4
|
+
export interface CommandSink {
|
|
5
|
+
run(name: string, input: unknown): void;
|
|
6
|
+
}
|
|
7
|
+
/** Runs commands on the local `ctx` — the default, client-authoritative path. */
|
|
8
|
+
export declare function localCommandSink(ctx: GameContext): CommandSink;
|
|
9
|
+
/** Sends commands to the authoritative host over the transport; the result replicates back through world sync. */
|
|
10
|
+
export declare function remoteCommandSink(backend: Pick<LiveGameBackend, "transport">, serverId: string): CommandSink;
|
|
11
|
+
/**
|
|
12
|
+
* The sink a server-authoritative shell should dispatch gameplay commands through: remote when the game opts into
|
|
13
|
+
* `authority: "server"` and a server is joined, local otherwise. Client-only UI commands (targeting, hotbar
|
|
14
|
+
* scroll) keep calling `ctx.game.commands.run` directly — only authoritative gameplay verbs route to the host.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveCommandSink(ctx: GameContext, opts: {
|
|
17
|
+
serverAuthoritative: boolean;
|
|
18
|
+
backend: Pick<LiveGameBackend, "transport"> | null;
|
|
19
|
+
serverId: string | null;
|
|
20
|
+
}): CommandSink;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Runs commands on the local `ctx` — the default, client-authoritative path. */
|
|
2
|
+
export function localCommandSink(ctx) {
|
|
3
|
+
return {
|
|
4
|
+
run(name, input) {
|
|
5
|
+
ctx.game.commands.run(name, input);
|
|
6
|
+
},
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/** Sends commands to the authoritative host over the transport; the result replicates back through world sync. */
|
|
10
|
+
export function remoteCommandSink(backend, serverId) {
|
|
11
|
+
return {
|
|
12
|
+
run(name, input) {
|
|
13
|
+
void backend.transport.runCommand({ serverId, command: name, input }).catch(() => undefined);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The sink a server-authoritative shell should dispatch gameplay commands through: remote when the game opts into
|
|
19
|
+
* `authority: "server"` and a server is joined, local otherwise. Client-only UI commands (targeting, hotbar
|
|
20
|
+
* scroll) keep calling `ctx.game.commands.run` directly — only authoritative gameplay verbs route to the host.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveCommandSink(ctx, opts) {
|
|
23
|
+
if (opts.serverAuthoritative && opts.backend !== null && opts.serverId !== null) {
|
|
24
|
+
return remoteCommandSink(opts.backend, opts.serverId);
|
|
25
|
+
}
|
|
26
|
+
return localCommandSink(ctx);
|
|
27
|
+
}
|
package/dist/defineGame.js
CHANGED
|
@@ -10,7 +10,7 @@ function worldBackdrop(feature) {
|
|
|
10
10
|
}
|
|
11
11
|
const emptyUi = () => null;
|
|
12
12
|
export function defineGame(config) {
|
|
13
|
-
const { content, loop, GameUI, environment, camera, multiplayer, WorldOverlay, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, worldHealthBars, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting, backdrop, shadows, presentation, objectStyles, devtools, ...engineFields } = config;
|
|
13
|
+
const { content, loop, GameUI, environment, camera, multiplayer, WorldOverlay, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, orientation, worldHealthBars, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting, backdrop, postProcessing, shadows, presentation, objectStyles, devtools, ...engineFields } = config;
|
|
14
14
|
const game = defineEngineGame({ ...engineFields, multiplayer: multiplayer ?? offline() });
|
|
15
15
|
return {
|
|
16
16
|
game,
|
|
@@ -19,6 +19,7 @@ export function defineGame(config) {
|
|
|
19
19
|
onInit: loop?.onInit ?? noop,
|
|
20
20
|
onNewPlayer: loop?.onNewPlayer ?? noop,
|
|
21
21
|
onTick: loop?.onTick ?? noop,
|
|
22
|
+
onPlayerLeave: loop?.onPlayerLeave ?? noop,
|
|
22
23
|
},
|
|
23
24
|
GameUI: GameUI ?? emptyUi,
|
|
24
25
|
environment: environment ??
|
|
@@ -34,6 +35,7 @@ export function defineGame(config) {
|
|
|
34
35
|
prompts,
|
|
35
36
|
pointer,
|
|
36
37
|
touch,
|
|
38
|
+
orientation,
|
|
37
39
|
worldHealthBars,
|
|
38
40
|
audio,
|
|
39
41
|
entitySounds,
|
|
@@ -43,6 +45,7 @@ export function defineGame(config) {
|
|
|
43
45
|
movement,
|
|
44
46
|
lighting,
|
|
45
47
|
backdrop,
|
|
48
|
+
postProcessing,
|
|
46
49
|
shadows,
|
|
47
50
|
presentation,
|
|
48
51
|
objectStyles,
|