@jgengine/shell 0.11.0 → 0.12.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 +26 -0
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +25 -1
- package/dist/GameHost.js +60 -4
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -0
- package/dist/GamePlayerShell.js +48 -358
- package/dist/GameUiPreview.js +3 -0
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +111 -26
- package/dist/audio/audioWire.d.ts +22 -4
- package/dist/audio/audioWire.js +46 -3
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/camera/GameCameraRig.js +6 -1
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- package/dist/camera/rigMath.d.ts +20 -0
- package/dist/camera/rigMath.js +29 -0
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +26 -4
- package/dist/devtools/DevtoolsOverlay.d.ts +4 -4
- package/dist/devtools/DevtoolsOverlay.js +28 -12
- package/dist/devtools/agentBridge.js +4 -0
- package/dist/devtools/perfDiagnose.js +1 -1
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +7 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +23 -12
- package/dist/environment/EnvironmentScene.js +12 -3
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +11 -4
- package/dist/presentationResolve.d.ts +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +1 -0
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/modelLoad.js +4 -1
- package/dist/render/resolveModel.d.ts +15 -0
- package/dist/render/resolveModel.js +37 -2
- package/dist/scatter/InstancedScatter.js +5 -0
- package/dist/scatter/scatterProxies.js +24 -0
- package/dist/scene/AuthoredScene.d.ts +3 -1
- package/dist/scene/AuthoredScene.js +7 -2
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GrassFieldRenderer.js +53 -5
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.js +70 -7
- package/dist/scene/WaterRenderer.js +34 -10
- package/dist/scene/builtinSceneKindRenderers.js +4 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +2 -0
- package/dist/settings/appliedSettings.d.ts +2 -1
- package/dist/settings/appliedSettings.js +4 -2
- package/dist/structures/TransformGizmo.d.ts +11 -1
- package/dist/structures/TransformGizmo.js +9 -5
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/grassGeometry.d.ts +33 -1
- package/dist/terrain/grassGeometry.js +83 -21
- package/dist/terrain/grassMaterial.d.ts +18 -0
- package/dist/terrain/grassMaterial.js +66 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/soilPatchMaterial.d.ts +15 -2
- package/dist/terrain/soilPatchMaterial.js +46 -8
- package/dist/terrain/terrainDetailMaterial.js +15 -2
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +9 -0
- package/dist/water/OceanConfig.js +6 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/WorldScene.js +29 -2
- package/dist/worldSky.d.ts +7 -1
- package/dist/worldSky.js +11 -1
- package/package.json +4 -4
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +15 -10
- package/skills/jgengine-ui/api.md +202 -47
- package/skills/jgengine-ui/capabilities.md +71 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +20 -7
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +419 -15
- package/skills/jgengine-world/capabilities.md +108 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge/runtime.d.ts +0 -15
- package/dist/cartridge/runtime.js +0 -556
- package/dist/cartridge/spec.d.ts +0 -188
- package/dist/cartridge/spec.js +0 -19
- package/dist/cartridge/validate.d.ts +0 -2
- package/dist/cartridge/validate.js +0 -78
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/skills/jgengine/reference-cartridge.md +0 -72
package/dist/cartridge/spec.d.ts
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import type { SpawnDirectorConfig } from "@jgengine/core/ai/spawnDirector";
|
|
2
|
-
import type { Curve } from "@jgengine/core/game/progression";
|
|
3
|
-
import type { ActionCodesMap } from "@jgengine/core/input/actionBindings";
|
|
4
|
-
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
5
|
-
export type Leveled = number | {
|
|
6
|
-
base: number;
|
|
7
|
-
perLevel: number;
|
|
8
|
-
min?: number;
|
|
9
|
-
max?: number;
|
|
10
|
-
} | {
|
|
11
|
-
table: readonly number[];
|
|
12
|
-
};
|
|
13
|
-
export declare function leveled(value: Leveled, level: number): number;
|
|
14
|
-
export interface CartridgePlayer {
|
|
15
|
-
kind: string;
|
|
16
|
-
health: number;
|
|
17
|
-
walkSpeed: number;
|
|
18
|
-
spawnAt?: readonly [number, number, number];
|
|
19
|
-
}
|
|
20
|
-
export interface CartridgeEnemy {
|
|
21
|
-
label: string;
|
|
22
|
-
health: number;
|
|
23
|
-
walkSpeed: number;
|
|
24
|
-
xp: number;
|
|
25
|
-
contact: {
|
|
26
|
-
damage: number;
|
|
27
|
-
intervalSeconds: number;
|
|
28
|
-
};
|
|
29
|
-
behavior?: "chase" | "none";
|
|
30
|
-
}
|
|
31
|
-
export interface CartridgeFlow {
|
|
32
|
-
start?: "auto" | "gate";
|
|
33
|
-
countdownSeconds?: number;
|
|
34
|
-
restart?: boolean;
|
|
35
|
-
}
|
|
36
|
-
export interface WeaponCommon {
|
|
37
|
-
label: string;
|
|
38
|
-
damage: Leveled;
|
|
39
|
-
cooldownMs: Leveled;
|
|
40
|
-
maxLevel: number;
|
|
41
|
-
fxColor?: string;
|
|
42
|
-
fxEmissive?: string;
|
|
43
|
-
}
|
|
44
|
-
export type CartridgeWeapon = (WeaponCommon & {
|
|
45
|
-
kind: "projectile";
|
|
46
|
-
range: number;
|
|
47
|
-
speed: number;
|
|
48
|
-
}) | (WeaponCommon & {
|
|
49
|
-
kind: "orbit";
|
|
50
|
-
blades: Leveled;
|
|
51
|
-
radius: Leveled;
|
|
52
|
-
hitRadius: number;
|
|
53
|
-
angularSpeed: number;
|
|
54
|
-
}) | (WeaponCommon & {
|
|
55
|
-
kind: "pulse";
|
|
56
|
-
radius: Leveled;
|
|
57
|
-
durationSeconds: number;
|
|
58
|
-
}) | (WeaponCommon & {
|
|
59
|
-
kind: "custom";
|
|
60
|
-
fire(ctx: GameContext, run: CartridgeRun, args: WeaponFireArgs): void;
|
|
61
|
-
});
|
|
62
|
-
export interface WeaponFireArgs {
|
|
63
|
-
weaponId: string;
|
|
64
|
-
level: number;
|
|
65
|
-
damage: number;
|
|
66
|
-
playerId: string;
|
|
67
|
-
playerPosition: readonly [number, number, number];
|
|
68
|
-
enemyIds: readonly string[];
|
|
69
|
-
dt: number;
|
|
70
|
-
}
|
|
71
|
-
export type UpgradeEffect = {
|
|
72
|
-
kind: "weaponLevel";
|
|
73
|
-
weapon: string;
|
|
74
|
-
} | {
|
|
75
|
-
kind: "statBonus";
|
|
76
|
-
stat: string;
|
|
77
|
-
amount: number;
|
|
78
|
-
} | {
|
|
79
|
-
kind: "fieldAdd";
|
|
80
|
-
field: string;
|
|
81
|
-
amount: number;
|
|
82
|
-
} | {
|
|
83
|
-
kind: "fieldMultiply";
|
|
84
|
-
field: string;
|
|
85
|
-
factor: number;
|
|
86
|
-
} | {
|
|
87
|
-
kind: "custom";
|
|
88
|
-
apply(ctx: GameContext, run: CartridgeRun, stacks: number): void;
|
|
89
|
-
};
|
|
90
|
-
export interface CartridgeUpgrade {
|
|
91
|
-
id: string;
|
|
92
|
-
label: string;
|
|
93
|
-
weight: number;
|
|
94
|
-
maxStacks: number;
|
|
95
|
-
effect: UpgradeEffect;
|
|
96
|
-
}
|
|
97
|
-
export interface CartridgeProgression {
|
|
98
|
-
xp: Curve;
|
|
99
|
-
maxLevel: number;
|
|
100
|
-
draft: {
|
|
101
|
-
choices: number;
|
|
102
|
-
upgrades: readonly CartridgeUpgrade[];
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
export interface CartridgeSpawning {
|
|
106
|
-
director: SpawnDirectorConfig;
|
|
107
|
-
placement: {
|
|
108
|
-
kind: "ring";
|
|
109
|
-
radius: number;
|
|
110
|
-
} | {
|
|
111
|
-
kind: "custom";
|
|
112
|
-
position(ctx: GameContext, run: CartridgeRun): readonly [number, number, number];
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
export interface CartridgeXpGems {
|
|
116
|
-
collectRadius: number;
|
|
117
|
-
pullSpeed: number;
|
|
118
|
-
rarityThresholds: readonly (readonly [number, string])[];
|
|
119
|
-
defaultRarity: string;
|
|
120
|
-
}
|
|
121
|
-
export interface CartridgeRules {
|
|
122
|
-
win?: {
|
|
123
|
-
kind: "survive";
|
|
124
|
-
seconds: number;
|
|
125
|
-
} | {
|
|
126
|
-
kind: "custom";
|
|
127
|
-
check(ctx: GameContext, run: CartridgeRun): boolean;
|
|
128
|
-
};
|
|
129
|
-
lose?: {
|
|
130
|
-
kind: "playerDeath";
|
|
131
|
-
} | {
|
|
132
|
-
kind: "custom";
|
|
133
|
-
check(ctx: GameContext, run: CartridgeRun): boolean;
|
|
134
|
-
};
|
|
135
|
-
killLeaderboardStat?: string;
|
|
136
|
-
}
|
|
137
|
-
export interface CartridgeSpec {
|
|
138
|
-
seed?: string | number;
|
|
139
|
-
flow?: CartridgeFlow;
|
|
140
|
-
player: CartridgePlayer;
|
|
141
|
-
enemies: Record<string, CartridgeEnemy>;
|
|
142
|
-
combat: {
|
|
143
|
-
contactRadius: number;
|
|
144
|
-
};
|
|
145
|
-
spawning: CartridgeSpawning;
|
|
146
|
-
weapons: Record<string, CartridgeWeapon>;
|
|
147
|
-
progression: CartridgeProgression;
|
|
148
|
-
xpGems: CartridgeXpGems;
|
|
149
|
-
rules: CartridgeRules;
|
|
150
|
-
fields?: Record<string, number>;
|
|
151
|
-
input?: ActionCodesMap;
|
|
152
|
-
systems?: readonly ((ctx: GameContext, run: CartridgeRun, dt: number) => void)[];
|
|
153
|
-
}
|
|
154
|
-
export interface BoltFx {
|
|
155
|
-
id: number;
|
|
156
|
-
weaponId: string;
|
|
157
|
-
origin: readonly [number, number, number];
|
|
158
|
-
target: readonly [number, number, number];
|
|
159
|
-
firedAt: number;
|
|
160
|
-
travelSeconds: number;
|
|
161
|
-
}
|
|
162
|
-
export interface PulseFx {
|
|
163
|
-
id: number;
|
|
164
|
-
weaponId: string;
|
|
165
|
-
at: readonly [number, number, number];
|
|
166
|
-
firedAt: number;
|
|
167
|
-
durationSeconds: number;
|
|
168
|
-
maxRadius: number;
|
|
169
|
-
}
|
|
170
|
-
export type CartridgePhase = "start" | "countdown" | "playing" | "won" | "lost";
|
|
171
|
-
export interface CartridgeRun {
|
|
172
|
-
phase: CartridgePhase;
|
|
173
|
-
playingSeconds: number;
|
|
174
|
-
countdownRemaining: number;
|
|
175
|
-
kills: number;
|
|
176
|
-
weaponLevel(weaponId: string): number;
|
|
177
|
-
field(name: string): number;
|
|
178
|
-
pendingOffers: readonly {
|
|
179
|
-
id: string;
|
|
180
|
-
label: string;
|
|
181
|
-
}[] | null;
|
|
182
|
-
bolts: readonly BoltFx[];
|
|
183
|
-
pulses: readonly PulseFx[];
|
|
184
|
-
subscribe(listener: () => void): () => void;
|
|
185
|
-
}
|
|
186
|
-
export declare const WASD_KEYBINDS: ActionCodesMap;
|
|
187
|
-
export declare const XP_GEM_BASE_TYPE = "xp_gem";
|
|
188
|
-
export declare const HIT_EFFECT = "hit";
|
package/dist/cartridge/spec.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export function leveled(value, level) {
|
|
2
|
-
if (typeof value === "number")
|
|
3
|
-
return value;
|
|
4
|
-
if ("table" in value) {
|
|
5
|
-
const index = Math.min(value.table.length - 1, Math.max(0, level - 1));
|
|
6
|
-
return value.table[index] ?? 0;
|
|
7
|
-
}
|
|
8
|
-
const raw = value.base + value.perLevel * (level - 1);
|
|
9
|
-
const floored = value.min === undefined ? raw : Math.max(value.min, raw);
|
|
10
|
-
return value.max === undefined ? floored : Math.min(value.max, floored);
|
|
11
|
-
}
|
|
12
|
-
export const WASD_KEYBINDS = {
|
|
13
|
-
moveForward: ["KeyW"],
|
|
14
|
-
moveBack: ["KeyS"],
|
|
15
|
-
moveLeft: ["KeyA"],
|
|
16
|
-
moveRight: ["KeyD"],
|
|
17
|
-
};
|
|
18
|
-
export const XP_GEM_BASE_TYPE = "xp_gem";
|
|
19
|
-
export const HIT_EFFECT = "hit";
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { leveled } from "./spec.js";
|
|
2
|
-
export function validateCartridge(spec) {
|
|
3
|
-
const problems = [];
|
|
4
|
-
const enemyIds = new Set(Object.keys(spec.enemies));
|
|
5
|
-
const weaponIds = new Set(Object.keys(spec.weapons));
|
|
6
|
-
if (spec.player.health <= 0)
|
|
7
|
-
problems.push(`player "${spec.player.kind}": health must be positive`);
|
|
8
|
-
if (spec.player.walkSpeed <= 0)
|
|
9
|
-
problems.push(`player "${spec.player.kind}": walkSpeed must be positive`);
|
|
10
|
-
if (enemyIds.has(spec.player.kind))
|
|
11
|
-
problems.push(`player kind "${spec.player.kind}" collides with an enemy id`);
|
|
12
|
-
if (spec.flow?.countdownSeconds !== undefined && spec.flow.countdownSeconds < 0) {
|
|
13
|
-
problems.push("flow.countdownSeconds must not be negative");
|
|
14
|
-
}
|
|
15
|
-
for (const [id, enemy] of Object.entries(spec.enemies)) {
|
|
16
|
-
if (enemy.health <= 0)
|
|
17
|
-
problems.push(`enemy "${id}": health must be positive`);
|
|
18
|
-
if (enemy.walkSpeed <= 0)
|
|
19
|
-
problems.push(`enemy "${id}": walkSpeed must be positive`);
|
|
20
|
-
if (enemy.xp <= 0)
|
|
21
|
-
problems.push(`enemy "${id}": xp must be positive`);
|
|
22
|
-
if (enemy.contact.damage <= 0)
|
|
23
|
-
problems.push(`enemy "${id}": contact.damage must be positive`);
|
|
24
|
-
if (enemy.contact.intervalSeconds <= 0)
|
|
25
|
-
problems.push(`enemy "${id}": contact.intervalSeconds must be positive`);
|
|
26
|
-
}
|
|
27
|
-
for (const wave of spec.spawning.director.waves) {
|
|
28
|
-
for (const entry of wave.entries) {
|
|
29
|
-
if (!enemyIds.has(entry.id))
|
|
30
|
-
problems.push(`spawning wave references unknown enemy "${entry.id}"`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
for (const [id, weapon] of Object.entries(spec.weapons)) {
|
|
34
|
-
for (let level = 1; level <= weapon.maxLevel; level += 1) {
|
|
35
|
-
if (leveled(weapon.damage, level) <= 0)
|
|
36
|
-
problems.push(`weapon "${id}": damage at level ${level} must be positive`);
|
|
37
|
-
if (leveled(weapon.cooldownMs, level) <= 0)
|
|
38
|
-
problems.push(`weapon "${id}": cooldownMs at level ${level} must be positive`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const seenUpgrades = new Set();
|
|
42
|
-
for (const upgrade of spec.progression.draft.upgrades) {
|
|
43
|
-
if (seenUpgrades.has(upgrade.id))
|
|
44
|
-
problems.push(`upgrade "${upgrade.id}": duplicate id`);
|
|
45
|
-
seenUpgrades.add(upgrade.id);
|
|
46
|
-
if (upgrade.maxStacks <= 0)
|
|
47
|
-
problems.push(`upgrade "${upgrade.id}": maxStacks must be positive`);
|
|
48
|
-
const effect = upgrade.effect;
|
|
49
|
-
if (effect.kind === "weaponLevel") {
|
|
50
|
-
if (!weaponIds.has(effect.weapon)) {
|
|
51
|
-
problems.push(`upgrade "${upgrade.id}": references unknown weapon "${effect.weapon}"`);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
const weapon = spec.weapons[effect.weapon];
|
|
55
|
-
if (1 + upgrade.maxStacks > weapon.maxLevel) {
|
|
56
|
-
problems.push(`upgrade "${upgrade.id}": maxStacks ${upgrade.maxStacks} exceeds weapon "${effect.weapon}" maxLevel ${weapon.maxLevel}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
if ((effect.kind === "fieldAdd" || effect.kind === "fieldMultiply") && spec.fields?.[effect.field] === undefined) {
|
|
61
|
-
problems.push(`upgrade "${upgrade.id}": references undeclared field "${effect.field}"`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (spec.progression.draft.choices <= 0)
|
|
65
|
-
problems.push("progression.draft.choices must be positive");
|
|
66
|
-
if (spec.progression.maxLevel <= 1)
|
|
67
|
-
problems.push("progression.maxLevel must exceed 1");
|
|
68
|
-
const thresholds = spec.xpGems.rarityThresholds;
|
|
69
|
-
for (let i = 1; i < thresholds.length; i += 1) {
|
|
70
|
-
if (thresholds[i][0] >= thresholds[i - 1][0]) {
|
|
71
|
-
problems.push("xpGems.rarityThresholds must be sorted by descending value");
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (spec.rules.win?.kind === "survive" && spec.rules.win.seconds <= 0) {
|
|
75
|
-
problems.push("rules.win.seconds must be positive");
|
|
76
|
-
}
|
|
77
|
-
return problems;
|
|
78
|
-
}
|
package/dist/cartridge.d.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { type ComponentType } from "react";
|
|
2
|
-
import type { AbilitySlotState } from "@jgengine/core/combat/abilityKit";
|
|
3
|
-
import type { HudAnchor } from "@jgengine/core/ui/hudLayout";
|
|
4
|
-
import { type CartridgeSpec } from "./cartridge/spec.js";
|
|
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;
|
package/dist/cartridge.js
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
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 { HudCanvas, HudPanel, useHudLayout } from "@jgengine/react";
|
|
5
|
-
import { useAbilitySlots, useEntityStat } from "@jgengine/react/hooks";
|
|
6
|
-
import { useGameContext } from "@jgengine/react/provider";
|
|
7
|
-
import { createCartridge } from "./cartridge/runtime.js";
|
|
8
|
-
import { WASD_KEYBINDS, leveled } from "./cartridge/spec.js";
|
|
9
|
-
import { validateCartridge } from "./cartridge/validate.js";
|
|
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
|
-
}
|