@netforge/engine 0.0.0-stage → 0.1.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/GAME-DESIGN.md +195 -0
- package/LICENSE +21 -0
- package/NOTICE +30 -0
- package/README.md +147 -2
- package/dist/api/appearance.d.ts +37 -0
- package/dist/api/appearance.js +41 -0
- package/dist/api/cards.d.ts +105 -0
- package/dist/api/cards.js +133 -0
- package/dist/api/collector.d.ts +114 -0
- package/dist/api/collector.js +204 -0
- package/dist/api/dungeon.d.ts +58 -0
- package/dist/api/dungeon.js +216 -0
- package/dist/api/harness.d.ts +149 -0
- package/dist/api/harness.js +549 -0
- package/dist/api/idle.d.ts +71 -0
- package/dist/api/idle.js +104 -0
- package/dist/api/race-harness.d.ts +3 -0
- package/dist/api/race-harness.js +148 -0
- package/dist/api/registry.d.ts +241 -0
- package/dist/api/registry.js +394 -0
- package/dist/api/scheduler.d.ts +56 -0
- package/dist/api/scheduler.js +160 -0
- package/dist/api/schemas.d.ts +958 -0
- package/dist/api/schemas.js +859 -0
- package/dist/api/tower.d.ts +127 -0
- package/dist/api/tower.js +183 -0
- package/dist/game/adapter.d.ts +69 -0
- package/dist/game/adapter.js +341 -0
- package/dist/game/arena-bot.d.ts +66 -0
- package/dist/game/arena-bot.js +188 -0
- package/dist/game/bot.d.ts +95 -0
- package/dist/game/bot.js +285 -0
- package/dist/game/compile.d.ts +97 -0
- package/dist/game/compile.js +476 -0
- package/dist/game/director.d.ts +42 -0
- package/dist/game/director.js +179 -0
- package/dist/game/earn-source.d.ts +122 -0
- package/dist/game/earn-source.js +386 -0
- package/dist/game/earn.d.ts +49 -0
- package/dist/game/earn.js +55 -0
- package/dist/game/index.d.ts +37 -0
- package/dist/game/index.js +25 -0
- package/dist/game/platformer.d.ts +156 -0
- package/dist/game/platformer.js +619 -0
- package/dist/game/profile.d.ts +97 -0
- package/dist/game/profile.js +140 -0
- package/dist/game/race.d.ts +177 -0
- package/dist/game/race.js +386 -0
- package/dist/game/racer.d.ts +98 -0
- package/dist/game/racer.js +379 -0
- package/dist/game/runner.d.ts +286 -0
- package/dist/game/runner.js +824 -0
- package/dist/game/threats.d.ts +23 -0
- package/dist/game/threats.js +49 -0
- package/dist/game/view.d.ts +47 -0
- package/dist/game/view.js +123 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +24 -0
- package/dist/net/bits.d.ts +33 -0
- package/dist/net/bits.js +135 -0
- package/dist/net/client.d.ts +75 -0
- package/dist/net/client.js +151 -0
- package/dist/net/index.d.ts +23 -0
- package/dist/net/index.js +23 -0
- package/dist/net/input.d.ts +49 -0
- package/dist/net/input.js +112 -0
- package/dist/net/interpolation.d.ts +37 -0
- package/dist/net/interpolation.js +97 -0
- package/dist/net/netsim.d.ts +22 -0
- package/dist/net/netsim.js +52 -0
- package/dist/net/prediction.d.ts +31 -0
- package/dist/net/prediction.js +101 -0
- package/dist/net/profile-client.d.ts +75 -0
- package/dist/net/profile-client.js +173 -0
- package/dist/net/protocol.d.ts +208 -0
- package/dist/net/protocol.js +8 -0
- package/dist/net/room-client.d.ts +57 -0
- package/dist/net/room-client.js +144 -0
- package/dist/net/schema.d.ts +78 -0
- package/dist/net/schema.js +121 -0
- package/dist/net/sim-adapter.d.ts +134 -0
- package/dist/net/sim-adapter.js +22 -0
- package/dist/net/snapshot.d.ts +61 -0
- package/dist/net/snapshot.js +301 -0
- package/dist/rules/adapter.d.ts +53 -0
- package/dist/rules/adapter.js +261 -0
- package/dist/rules/cards.d.ts +115 -0
- package/dist/rules/cards.js +547 -0
- package/dist/rules/collector.d.ts +161 -0
- package/dist/rules/collector.js +732 -0
- package/dist/rules/genres.d.ts +13 -0
- package/dist/rules/genres.js +38 -0
- package/dist/rules/harness.d.ts +16 -0
- package/dist/rules/harness.js +127 -0
- package/dist/rules/idle.d.ts +115 -0
- package/dist/rules/idle.js +370 -0
- package/dist/rules/index.d.ts +16 -0
- package/dist/rules/index.js +14 -0
- package/dist/rules/kit.d.ts +171 -0
- package/dist/rules/kit.js +88 -0
- package/dist/rules/tower.d.ts +214 -0
- package/dist/rules/tower.js +655 -0
- package/dist/runtime/arena.d.ts +86 -0
- package/dist/runtime/arena.js +269 -0
- package/dist/runtime/assets.d.ts +26 -0
- package/dist/runtime/assets.js +35 -0
- package/dist/runtime/camera.d.ts +113 -0
- package/dist/runtime/camera.js +247 -0
- package/dist/runtime/crowd.d.ts +18 -0
- package/dist/runtime/crowd.js +62 -0
- package/dist/runtime/feel/audio.d.ts +150 -0
- package/dist/runtime/feel/audio.js +327 -0
- package/dist/runtime/feel/clock.d.ts +52 -0
- package/dist/runtime/feel/clock.js +80 -0
- package/dist/runtime/feel/flash.d.ts +28 -0
- package/dist/runtime/feel/flash.js +60 -0
- package/dist/runtime/feel/index.d.ts +27 -0
- package/dist/runtime/feel/index.js +16 -0
- package/dist/runtime/feel/kit.d.ts +159 -0
- package/dist/runtime/feel/kit.js +226 -0
- package/dist/runtime/feel/knockback.d.ts +41 -0
- package/dist/runtime/feel/knockback.js +70 -0
- package/dist/runtime/feel/noise.d.ts +6 -0
- package/dist/runtime/feel/noise.js +30 -0
- package/dist/runtime/feel/numbers.d.ts +91 -0
- package/dist/runtime/feel/numbers.js +172 -0
- package/dist/runtime/feel/rumble.d.ts +40 -0
- package/dist/runtime/feel/rumble.js +58 -0
- package/dist/runtime/feel/sound.d.ts +96 -0
- package/dist/runtime/feel/sound.js +176 -0
- package/dist/runtime/feel/spring.d.ts +42 -0
- package/dist/runtime/feel/spring.js +95 -0
- package/dist/runtime/feel/trauma.d.ts +48 -0
- package/dist/runtime/feel/trauma.js +76 -0
- package/dist/runtime/feel/tween.d.ts +66 -0
- package/dist/runtime/feel/tween.js +180 -0
- package/dist/runtime/feel/types.d.ts +30 -0
- package/dist/runtime/feel/types.js +5 -0
- package/dist/runtime/index.d.ts +28 -0
- package/dist/runtime/index.js +26 -0
- package/dist/runtime/input.d.ts +38 -0
- package/dist/runtime/input.js +159 -0
- package/dist/runtime/interpolate.d.ts +6 -0
- package/dist/runtime/interpolate.js +13 -0
- package/dist/runtime/render/animation.d.ts +63 -0
- package/dist/runtime/render/animation.js +128 -0
- package/dist/runtime/render/atmosphere.d.ts +32 -0
- package/dist/runtime/render/atmosphere.js +59 -0
- package/dist/runtime/render/crowd-motion.d.ts +68 -0
- package/dist/runtime/render/crowd-motion.js +163 -0
- package/dist/runtime/render/decals.d.ts +64 -0
- package/dist/runtime/render/decals.js +222 -0
- package/dist/runtime/render/highlight.d.ts +29 -0
- package/dist/runtime/render/highlight.js +110 -0
- package/dist/runtime/render/index.d.ts +35 -0
- package/dist/runtime/render/index.js +22 -0
- package/dist/runtime/render/lighting.d.ts +50 -0
- package/dist/runtime/render/lighting.js +142 -0
- package/dist/runtime/render/particles.d.ts +117 -0
- package/dist/runtime/render/particles.js +289 -0
- package/dist/runtime/render/post.d.ts +92 -0
- package/dist/runtime/render/post.js +191 -0
- package/dist/runtime/render/presets.d.ts +57 -0
- package/dist/runtime/render/presets.js +116 -0
- package/dist/runtime/render/quality.d.ts +113 -0
- package/dist/runtime/render/quality.js +180 -0
- package/dist/runtime/render/rig.d.ts +29 -0
- package/dist/runtime/render/rig.js +107 -0
- package/dist/runtime/render/sprite.d.ts +34 -0
- package/dist/runtime/render/sprite.js +58 -0
- package/dist/runtime/render/stack.d.ts +74 -0
- package/dist/runtime/render/stack.js +132 -0
- package/dist/runtime/render/vat.d.ts +78 -0
- package/dist/runtime/render/vat.js +219 -0
- package/dist/runtime/renderer.d.ts +19 -0
- package/dist/runtime/renderer.js +18 -0
- package/dist/runtime/stats.d.ts +11 -0
- package/dist/runtime/stats.js +29 -0
- package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
- package/dist/runtime/styles/handdrawn/ground.js +282 -0
- package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
- package/dist/runtime/styles/handdrawn/index.js +94 -0
- package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
- package/dist/runtime/styles/handdrawn/ink.js +68 -0
- package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
- package/dist/runtime/styles/handdrawn/looks.js +42 -0
- package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
- package/dist/runtime/styles/handdrawn/paint.js +129 -0
- package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
- package/dist/runtime/styles/handdrawn/palette.js +70 -0
- package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
- package/dist/runtime/styles/handdrawn/textures.js +150 -0
- package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
- package/dist/runtime/styles/handdrawn/vfx.js +73 -0
- package/dist/runtime/styles/index.d.ts +9 -0
- package/dist/runtime/styles/index.js +32 -0
- package/dist/runtime/styles/kit.d.ts +193 -0
- package/dist/runtime/styles/kit.js +1 -0
- package/dist/runtime/styles/pixel/index.d.ts +46 -0
- package/dist/runtime/styles/pixel/index.js +167 -0
- package/dist/runtime/styles/ps1/index.d.ts +29 -0
- package/dist/runtime/styles/ps1/index.js +41 -0
- package/dist/runtime/styles/ps1/lights.d.ts +47 -0
- package/dist/runtime/styles/ps1/lights.js +146 -0
- package/dist/runtime/styles/ps1/materials.d.ts +62 -0
- package/dist/runtime/styles/ps1/materials.js +285 -0
- package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
- package/dist/runtime/styles/ps1/pipeline.js +111 -0
- package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
- package/dist/runtime/styles/ps1/surfaces.js +228 -0
- package/dist/runtime/styles/toon/index.d.ts +5 -0
- package/dist/runtime/styles/toon/index.js +178 -0
- package/dist/runtime/styles/toon/ink.d.ts +39 -0
- package/dist/runtime/styles/toon/ink.js +73 -0
- package/dist/runtime/styles/toon/look.d.ts +10 -0
- package/dist/runtime/styles/toon/look.js +112 -0
- package/dist/runtime/styles/toon/material.d.ts +74 -0
- package/dist/runtime/styles/toon/material.js +208 -0
- package/dist/runtime/styles/toon/normals.d.ts +26 -0
- package/dist/runtime/styles/toon/normals.js +127 -0
- package/dist/runtime/styles/toon/shadows.d.ts +24 -0
- package/dist/runtime/styles/toon/shadows.js +42 -0
- package/dist/runtime/styles/toon/vfx.d.ts +15 -0
- package/dist/runtime/styles/toon/vfx.js +47 -0
- package/dist/runtime/styles/view.d.ts +35 -0
- package/dist/runtime/styles/view.js +34 -0
- package/dist/runtime/styles/voxel/core.d.ts +114 -0
- package/dist/runtime/styles/voxel/core.js +577 -0
- package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
- package/dist/runtime/styles/voxel/geometry.js +40 -0
- package/dist/runtime/styles/voxel/index.d.ts +21 -0
- package/dist/runtime/styles/voxel/index.js +23 -0
- package/dist/runtime/styles/voxel/kit.d.ts +6 -0
- package/dist/runtime/styles/voxel/kit.js +11 -0
- package/dist/runtime/styles/voxel/look.d.ts +10 -0
- package/dist/runtime/styles/voxel/look.js +16 -0
- package/dist/runtime/styles/voxel/props.d.ts +9 -0
- package/dist/runtime/styles/voxel/props.js +52 -0
- package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
- package/dist/runtime/styles/voxel/shapes.js +83 -0
- package/dist/runtime/styles/voxel/world.d.ts +49 -0
- package/dist/runtime/styles/voxel/world.js +422 -0
- package/llms.txt +183 -0
- package/package.json +82 -3
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { GameDefinitionError, achievementSchema, keeperSchema, missionSchema, titleSchema, directorSchema, earnRuleSchema, earnStakeSchema, earnPotSchema, economySchema, metaUpgradeSchema, META_MAX_PER_STAT, META_MAX_TOTAL, META_MAX_UPGRADES, gameSchema, idSchema, parseOrThrow, prefabSchema, skuSchema, upgradeSchema, weaponSchema, } from "./schemas.js";
|
|
2
|
+
const fresh = () => ({
|
|
3
|
+
game: null,
|
|
4
|
+
director: null,
|
|
5
|
+
prefabs: new Map(),
|
|
6
|
+
weapons: new Map(),
|
|
7
|
+
upgrades: new Map(),
|
|
8
|
+
systems: new Map(),
|
|
9
|
+
earnRules: new Map(),
|
|
10
|
+
stake: null,
|
|
11
|
+
pot: null,
|
|
12
|
+
skus: new Map(),
|
|
13
|
+
keepers: new Map(),
|
|
14
|
+
achievements: new Map(),
|
|
15
|
+
missions: new Map(),
|
|
16
|
+
titles: new Map(),
|
|
17
|
+
economy: null,
|
|
18
|
+
metaUpgrades: new Map(),
|
|
19
|
+
content: new Map(),
|
|
20
|
+
});
|
|
21
|
+
let reg = fresh();
|
|
22
|
+
/**
|
|
23
|
+
* Set by `collectManifest`: the module that declared this registry is done.
|
|
24
|
+
* The next declaration starts a fresh registry, so a process can load more
|
|
25
|
+
* than one game module in turn (the room server hosts every template), each
|
|
26
|
+
* manifest collecting its own game right after importing it.
|
|
27
|
+
*/
|
|
28
|
+
let sealed = false;
|
|
29
|
+
function unseal() {
|
|
30
|
+
if (!sealed)
|
|
31
|
+
return;
|
|
32
|
+
reg = fresh();
|
|
33
|
+
sealed = false;
|
|
34
|
+
}
|
|
35
|
+
const unique = (map, kind, id) => {
|
|
36
|
+
if (map.has(id))
|
|
37
|
+
throw new GameDefinitionError(`${kind} "${id}"`, [`${kind} "${id}" is declared twice; ids must be unique`]);
|
|
38
|
+
};
|
|
39
|
+
const checkId = (kind, id) => parseOrThrow(idSchema, id, `${kind} id`);
|
|
40
|
+
/** Declare the game. Exactly one per game module; it is the module's default export. */
|
|
41
|
+
export function game(def) {
|
|
42
|
+
unseal();
|
|
43
|
+
if (reg.game)
|
|
44
|
+
throw new GameDefinitionError("game()", ["game() is called twice; a game module declares exactly one game"]);
|
|
45
|
+
const parsed = parseOrThrow(gameSchema, def, "game()");
|
|
46
|
+
reg.game = parsed;
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
/** Declare a prefab: a spawnable entity template. */
|
|
50
|
+
export function prefab(id, def) {
|
|
51
|
+
unseal();
|
|
52
|
+
checkId("prefab", id);
|
|
53
|
+
unique(reg.prefabs, "prefab", id);
|
|
54
|
+
const parsed = parseOrThrow(prefabSchema, def, `prefab("${id}")`);
|
|
55
|
+
reg.prefabs.set(id, parsed);
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
/** Declare a weapon (survivor template): it fires by itself; players gain and level weapons at level-up. */
|
|
59
|
+
export function weapon(id, def) {
|
|
60
|
+
unseal();
|
|
61
|
+
checkId("weapon", id);
|
|
62
|
+
unique(reg.weapons, "weapon", id);
|
|
63
|
+
const parsed = parseOrThrow(weaponSchema, def, `weapon("${id}")`);
|
|
64
|
+
reg.weapons.set(id, parsed);
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
/** Declare a passive upgrade offered at level-up (survivor template). */
|
|
68
|
+
export function upgrade(id, def) {
|
|
69
|
+
unseal();
|
|
70
|
+
checkId("upgrade", id);
|
|
71
|
+
unique(reg.upgrades, "upgrade", id);
|
|
72
|
+
if (reg.upgrades.size >= 8)
|
|
73
|
+
throw new GameDefinitionError(`upgrade("${id}")`, ["at most 8 passive upgrades per game"]);
|
|
74
|
+
const parsed = parseOrThrow(upgradeSchema, def, `upgrade("${id}")`);
|
|
75
|
+
reg.upgrades.set(id, parsed);
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Declare the spawn director (at most one per game): who spawns, how many,
|
|
80
|
+
* how fast and how tough, over the run's clock. See directorSchema.
|
|
81
|
+
*/
|
|
82
|
+
export function director(def) {
|
|
83
|
+
if (reg.director)
|
|
84
|
+
throw new GameDefinitionError("director()", ["director() is called twice; a game has at most one director"]);
|
|
85
|
+
const parsed = parseOrThrow(directorSchema, def, "director()");
|
|
86
|
+
reg.director = parsed;
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
/** Declare a system. `setup` runs once when a run starts and schedules work with `every`, `after`, `onTick`. */
|
|
90
|
+
export function system(name, setup) {
|
|
91
|
+
unseal();
|
|
92
|
+
checkId("system", name);
|
|
93
|
+
unique(reg.systems, "system", name);
|
|
94
|
+
if (typeof setup !== "function")
|
|
95
|
+
throw new GameDefinitionError(`system("${name}")`, ["the second argument must be a function"]);
|
|
96
|
+
reg.systems.set(name, setup);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Declare a keeper (a playable character): a start weapon and a signature
|
|
100
|
+
* passive. Players pick one before their first level; at most 15.
|
|
101
|
+
*/
|
|
102
|
+
export function keeper(id, def) {
|
|
103
|
+
unseal();
|
|
104
|
+
checkId("keeper", id);
|
|
105
|
+
unique(reg.keepers, "keeper", id);
|
|
106
|
+
if (reg.keepers.size >= 15)
|
|
107
|
+
throw new GameDefinitionError(`keeper("${id}")`, ["at most 15 keepers per game"]);
|
|
108
|
+
const parsed = parseOrThrow(keeperSchema, def, `keeper("${id}")`);
|
|
109
|
+
reg.keepers.set(id, parsed);
|
|
110
|
+
return parsed;
|
|
111
|
+
}
|
|
112
|
+
/** Declare an achievement (counted per player per run; it unlocks keepers, never coins). */
|
|
113
|
+
export function achievement(id, def) {
|
|
114
|
+
unseal();
|
|
115
|
+
checkId("achievement", id);
|
|
116
|
+
unique(reg.achievements, "achievement", id);
|
|
117
|
+
const parsed = parseOrThrow(achievementSchema, def, `achievement("${id}")`);
|
|
118
|
+
reg.achievements.set(id, parsed);
|
|
119
|
+
return parsed;
|
|
120
|
+
}
|
|
121
|
+
/** Declare a daily goal (the HUD shows three a day; it pays nothing). */
|
|
122
|
+
export function mission(id, def) {
|
|
123
|
+
unseal();
|
|
124
|
+
checkId("mission", id);
|
|
125
|
+
unique(reg.missions, "mission", id);
|
|
126
|
+
const parsed = parseOrThrow(missionSchema, def, `mission("${id}")`);
|
|
127
|
+
reg.missions.set(id, parsed);
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
/** Declare an end-screen title ("The Butcher": the most kills). */
|
|
131
|
+
export function title(id, def) {
|
|
132
|
+
unseal();
|
|
133
|
+
checkId("title", id);
|
|
134
|
+
unique(reg.titles, "title", id);
|
|
135
|
+
const parsed = parseOrThrow(titleSchema, def, `title("${id}")`);
|
|
136
|
+
reg.titles.set(id, parsed);
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Declare the game's coin model (at most once). Without it a game uses the
|
|
141
|
+
* recommended default: coins buy cosmetics, unlocks and in-run consumables,
|
|
142
|
+
* never permanent stat power. economy({ store: "cosmetics" }) sells looks
|
|
143
|
+
* only; economy({ metaUpgrades: true }) opts in to metaUpgrade(...).
|
|
144
|
+
*/
|
|
145
|
+
export function economy(def = {}) {
|
|
146
|
+
unseal();
|
|
147
|
+
if (reg.economy)
|
|
148
|
+
throw new GameDefinitionError("economy()", ["economy() is called twice; a game has one coin model"]);
|
|
149
|
+
const parsed = parseOrThrow(economySchema, def, "economy()");
|
|
150
|
+
reg.economy = parsed;
|
|
151
|
+
return parsed;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Declare a meta upgrade: a permanent stat bonus a player buys with coins,
|
|
155
|
+
* level by level (opt-in: economy({ metaUpgrades: true })). The engine caps
|
|
156
|
+
* meta power at +15% per stat and +30% in total, and costs grow
|
|
157
|
+
* geometrically. The store shows every level's bonus and price.
|
|
158
|
+
*/
|
|
159
|
+
export function metaUpgrade(id, def) {
|
|
160
|
+
unseal();
|
|
161
|
+
checkId("meta upgrade", id);
|
|
162
|
+
unique(reg.metaUpgrades, "meta upgrade", id);
|
|
163
|
+
if (reg.metaUpgrades.size >= META_MAX_UPGRADES)
|
|
164
|
+
throw new GameDefinitionError(`metaUpgrade("${id}")`, [`at most ${META_MAX_UPGRADES} meta upgrades per game`]);
|
|
165
|
+
const parsed = parseOrThrow(metaUpgradeSchema, def, `metaUpgrade("${id}")`);
|
|
166
|
+
reg.metaUpgrades.set(id, parsed);
|
|
167
|
+
return parsed;
|
|
168
|
+
}
|
|
169
|
+
/** Earn rules (every game must have at least one). */
|
|
170
|
+
export const earn = {
|
|
171
|
+
rule(id, def) {
|
|
172
|
+
unseal();
|
|
173
|
+
checkId("earn rule", id);
|
|
174
|
+
unique(reg.earnRules, "earn rule", id);
|
|
175
|
+
const parsed = parseOrThrow(earnRuleSchema, def, `earn.rule("${id}")`);
|
|
176
|
+
reg.earnRules.set(id, parsed);
|
|
177
|
+
return parsed;
|
|
178
|
+
},
|
|
179
|
+
/**
|
|
180
|
+
* The buy-in run, the mandatory sink loop: earning
|
|
181
|
+
* above free play takes a stake of the game's coins at run start. One per
|
|
182
|
+
* game, written on one line so Net Forge's Launch panel can read and edit it.
|
|
183
|
+
*/
|
|
184
|
+
stake(def) {
|
|
185
|
+
unseal();
|
|
186
|
+
if (reg.stake)
|
|
187
|
+
throw new GameDefinitionError("earn.stake", ["a game declares earn.stake(...) once"]);
|
|
188
|
+
const parsed = parseOrThrow(earnStakeSchema, def, "earn.stake");
|
|
189
|
+
reg.stake = parsed;
|
|
190
|
+
return parsed;
|
|
191
|
+
},
|
|
192
|
+
/**
|
|
193
|
+
* A PvP pot, optional, for the arena
|
|
194
|
+
* template: entrants pay `entry` coins before a round, the result splits
|
|
195
|
+
* the pot, and `recycle` of it stays in the reward pool.
|
|
196
|
+
*/
|
|
197
|
+
pot(def) {
|
|
198
|
+
unseal();
|
|
199
|
+
if (reg.pot)
|
|
200
|
+
throw new GameDefinitionError("earn.pot", ["a game declares earn.pot(...) once"]);
|
|
201
|
+
const parsed = parseOrThrow(earnPotSchema, def, "earn.pot");
|
|
202
|
+
reg.pot = parsed;
|
|
203
|
+
return parsed;
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
/** Store SKUs: upgrades and cosmetics priced in the game's coins (and/or USDG). */
|
|
207
|
+
export const store = {
|
|
208
|
+
sku(id, def) {
|
|
209
|
+
unseal();
|
|
210
|
+
checkId("sku", id);
|
|
211
|
+
unique(reg.skus, "sku", id);
|
|
212
|
+
const parsed = parseOrThrow(skuSchema, def, `store.sku("${id}")`);
|
|
213
|
+
reg.skus.set(id, parsed);
|
|
214
|
+
return parsed;
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Collect and cross-check what the game module declared. Called by the
|
|
219
|
+
* build and the runtime after importing the module, not by game code.
|
|
220
|
+
*/
|
|
221
|
+
export function collectManifest() {
|
|
222
|
+
const issues = [];
|
|
223
|
+
if (!reg.game)
|
|
224
|
+
issues.push("no game() declaration; export default game({...})");
|
|
225
|
+
if (reg.earnRules.size === 0)
|
|
226
|
+
issues.push("every game needs at least one earn.rule(...)");
|
|
227
|
+
if (!reg.stake)
|
|
228
|
+
issues.push("every game needs a buy-in run: earn.stake({ min, max, multiple }) (the sink loop)");
|
|
229
|
+
if (reg.pot && reg.game && reg.game.template !== "arena")
|
|
230
|
+
issues.push("earn.pot(...) is for player-versus-player games: the arena template");
|
|
231
|
+
for (const id of [...reg.skus.keys(), ...reg.metaUpgrades.keys()])
|
|
232
|
+
if (id === POT_REASON)
|
|
233
|
+
issues.push(`"${POT_REASON}" is the pot's coin spend reason; pick another id`);
|
|
234
|
+
for (const id of reg.skus.keys())
|
|
235
|
+
if (id === STAKE_REASON)
|
|
236
|
+
issues.push(`store.sku("${id}"): "${STAKE_REASON}" is the buy-in run's coin spend reason; pick another id`);
|
|
237
|
+
for (const id of reg.metaUpgrades.keys())
|
|
238
|
+
if (id === STAKE_REASON)
|
|
239
|
+
issues.push(`metaUpgrade("${id}"): "${STAKE_REASON}" is the buy-in run's coin spend reason; pick another id`);
|
|
240
|
+
for (const [id, p] of reg.prefabs) {
|
|
241
|
+
for (const d of p.drops) {
|
|
242
|
+
const did = typeof d === "string" ? d : d.id;
|
|
243
|
+
if (!reg.prefabs.has(did))
|
|
244
|
+
issues.push(`prefab "${id}" drops "${did}", which is not a declared prefab`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (reg.weapons.size > 15)
|
|
248
|
+
issues.push("at most 15 weapons per game");
|
|
249
|
+
const start = reg.game?.players.weapon;
|
|
250
|
+
if (start !== undefined && !reg.weapons.has(start))
|
|
251
|
+
issues.push(`players.weapon is "${start}", which is not a declared weapon`);
|
|
252
|
+
for (const id of reg.game?.players.loadout ?? [])
|
|
253
|
+
if (!reg.weapons.has(id))
|
|
254
|
+
issues.push(`players.loadout has "${id}", which is not a declared weapon`);
|
|
255
|
+
const drop = reg.game?.players.drop;
|
|
256
|
+
if (drop !== undefined && !reg.prefabs.has(drop))
|
|
257
|
+
issues.push(`players.drop is "${drop}", which is not a declared prefab`);
|
|
258
|
+
for (const e of reg.director?.pool ?? []) {
|
|
259
|
+
if (!reg.prefabs.has(e.prefab))
|
|
260
|
+
issues.push(`director pool has "${e.prefab}", which is not a declared prefab`);
|
|
261
|
+
if (Array.isArray(e.group) && e.group[0] > e.group[1])
|
|
262
|
+
issues.push(`director pool "${e.prefab}": group [min, max] needs min <= max`);
|
|
263
|
+
if (e.until !== undefined && e.until <= e.from)
|
|
264
|
+
issues.push(`director pool "${e.prefab}": until must be after from`);
|
|
265
|
+
}
|
|
266
|
+
for (const [id, k] of reg.keepers) {
|
|
267
|
+
if (!reg.weapons.has(k.weapon))
|
|
268
|
+
issues.push(`keeper "${id}" starts with "${k.weapon}", which is not a declared weapon`);
|
|
269
|
+
if (k.passive !== undefined && !reg.upgrades.has(k.passive))
|
|
270
|
+
issues.push(`keeper "${id}" has passive "${k.passive}", which is not a declared upgrade`);
|
|
271
|
+
if (k.unlock !== undefined && !reg.achievements.has(k.unlock))
|
|
272
|
+
issues.push(`keeper "${id}" unlocks with "${k.unlock}", which is not a declared achievement`);
|
|
273
|
+
}
|
|
274
|
+
if (reg.keepers.size > 0 && [...reg.keepers.values()].every((k) => k.unlock !== undefined)) {
|
|
275
|
+
issues.push("at least one keeper needs no unlock (everyone must have a keeper to start with)");
|
|
276
|
+
}
|
|
277
|
+
const [firstKeeper, first] = [...reg.keepers][0] ?? [];
|
|
278
|
+
if (first?.unlock !== undefined)
|
|
279
|
+
issues.push(`keeper "${firstKeeper}" is declared first, so it is everyone's default: it cannot have an unlock (declare a free keeper first)`);
|
|
280
|
+
issues.push(...economyIssues(reg));
|
|
281
|
+
if (issues.length > 0 || !reg.game || !reg.stake)
|
|
282
|
+
throw new GameDefinitionError("game module", issues);
|
|
283
|
+
sealed = true;
|
|
284
|
+
return {
|
|
285
|
+
game: reg.game,
|
|
286
|
+
prefabs: reg.prefabs,
|
|
287
|
+
weapons: reg.weapons,
|
|
288
|
+
upgrades: reg.upgrades,
|
|
289
|
+
systems: reg.systems,
|
|
290
|
+
earnRules: reg.earnRules,
|
|
291
|
+
stake: reg.stake,
|
|
292
|
+
pot: reg.pot,
|
|
293
|
+
skus: reg.skus,
|
|
294
|
+
director: reg.director,
|
|
295
|
+
keepers: reg.keepers,
|
|
296
|
+
achievements: reg.achievements,
|
|
297
|
+
missions: reg.missions,
|
|
298
|
+
titles: reg.titles,
|
|
299
|
+
economy: reg.economy ?? parseOrThrow(economySchema, {}, "economy()"),
|
|
300
|
+
metaUpgrades: reg.metaUpgrades,
|
|
301
|
+
content: reg.content,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* A genre declaration (rules/genres: tower(...), board(...), card(...)):
|
|
306
|
+
* stored under "<kind>:<id>" for the genre's compile step to read. Ids are
|
|
307
|
+
* unique per kind. Game code calls the genre's own typed function, never this.
|
|
308
|
+
*/
|
|
309
|
+
export function declareContent(kind, id, value) {
|
|
310
|
+
unseal();
|
|
311
|
+
const key = `${kind}:${id}`;
|
|
312
|
+
if (reg.content.has(key))
|
|
313
|
+
throw new GameDefinitionError(`${kind}("${id}")`, [`${kind} "${id}" is declared twice; ids must be unique`]);
|
|
314
|
+
reg.content.set(key, value);
|
|
315
|
+
}
|
|
316
|
+
/** Genre content of one kind, in declaration order (id, value). */
|
|
317
|
+
export function contentOf(m, kind) {
|
|
318
|
+
const out = [];
|
|
319
|
+
for (const [k, v] of m.content)
|
|
320
|
+
if (k.startsWith(`${kind}:`))
|
|
321
|
+
out.push([k.slice(kind.length + 1), v]);
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
/** The coin spend reason a buy-in run's stake is spent under (the referee reads it at run start). */
|
|
325
|
+
export const STAKE_REASON = "stake";
|
|
326
|
+
/** The coin spend reason a PvP pot entry is spent under. */
|
|
327
|
+
export const POT_REASON = "pot";
|
|
328
|
+
/** A coin spend's reason (the SKU or meta upgrade id) must fit the referee's ledger. */
|
|
329
|
+
const SPEND_REASON = /^[a-z][a-z0-9_]{1,30}$/;
|
|
330
|
+
/** The coin model's cross-checks. */
|
|
331
|
+
function economyIssues(r) {
|
|
332
|
+
const issues = [];
|
|
333
|
+
const e = r.economy ?? { store: "default", metaUpgrades: false };
|
|
334
|
+
const firstKeeper = [...r.keepers.keys()][0];
|
|
335
|
+
const looks = new Map();
|
|
336
|
+
for (const [id, sku] of r.skus) {
|
|
337
|
+
if (sku.coins !== undefined && !SPEND_REASON.test(id))
|
|
338
|
+
issues.push(`store.sku("${id}"): a coin-priced SKU id is 2-31 characters (a-z, 0-9, _)`);
|
|
339
|
+
const g = sku.grants;
|
|
340
|
+
if (e.store === "cosmetics" && !(g && "cosmetic" in g)) {
|
|
341
|
+
issues.push(`store.sku("${id}"): economy({ store: "cosmetics" }) sells looks only; give it grants: { cosmetic: <id> } or remove it`);
|
|
342
|
+
}
|
|
343
|
+
if (!g)
|
|
344
|
+
continue;
|
|
345
|
+
if ("keeper" in g) {
|
|
346
|
+
const k = r.keepers.get(g.keeper);
|
|
347
|
+
if (!k)
|
|
348
|
+
issues.push(`store.sku("${id}") unlocks keeper "${g.keeper}", which is not a declared keeper`);
|
|
349
|
+
else if (g.keeper === firstKeeper)
|
|
350
|
+
issues.push(`store.sku("${id}") sells keeper "${g.keeper}", the free default keeper`);
|
|
351
|
+
else if (k.unlock !== undefined) {
|
|
352
|
+
issues.push(`keeper "${g.keeper}" is both unlocked by an achievement ("${k.unlock}") and sold by store.sku("${id}"); a keeper is earned or bought, never both (drop its unlock, or the SKU)`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if ("gear" in g) {
|
|
356
|
+
if (!r.upgrades.has(g.gear))
|
|
357
|
+
issues.push(`store.sku("${id}") is gear for "${g.gear}", which is not a declared upgrade`);
|
|
358
|
+
if (sku.coins === undefined || sku.usdg !== undefined)
|
|
359
|
+
issues.push(`store.sku("${id}"): gear sells for coins only (its coins stay in the reward pool when it is lost)`);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
const other = looks.get(g.cosmetic);
|
|
363
|
+
if (other !== undefined)
|
|
364
|
+
issues.push(`store.sku("${id}") and store.sku("${other}") both grant cosmetic ${g.cosmetic}`);
|
|
365
|
+
looks.set(g.cosmetic, id);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (r.metaUpgrades.size > 0 && !e.metaUpgrades) {
|
|
369
|
+
issues.push("metaUpgrade(...) is opt-in: declare economy({ metaUpgrades: true }) to sell permanent stat upgrades for coins");
|
|
370
|
+
}
|
|
371
|
+
const perStat = new Map();
|
|
372
|
+
let total = 0;
|
|
373
|
+
for (const [id, m] of r.metaUpgrades) {
|
|
374
|
+
if (!SPEND_REASON.test(id))
|
|
375
|
+
issues.push(`metaUpgrade("${id}"): its id is 2-31 characters (a-z, 0-9, _)`);
|
|
376
|
+
if (r.skus.has(id))
|
|
377
|
+
issues.push(`metaUpgrade("${id}") and store.sku("${id}") share an id; ids are coin spend reasons and must differ`);
|
|
378
|
+
const v = m.step * m.levels;
|
|
379
|
+
perStat.set(m.stat, (perStat.get(m.stat) ?? 0) + v);
|
|
380
|
+
total += v;
|
|
381
|
+
}
|
|
382
|
+
for (const [stat, v] of perStat) {
|
|
383
|
+
if (v > META_MAX_PER_STAT + 1e-9)
|
|
384
|
+
issues.push(`meta upgrades raise ${stat} by +${Math.round(v * 100)}% at their top levels; the engine caps one stat at +${Math.round(META_MAX_PER_STAT * 100)}%`);
|
|
385
|
+
}
|
|
386
|
+
if (total > META_MAX_TOTAL + 1e-9)
|
|
387
|
+
issues.push(`meta upgrades add +${Math.round(total * 100)}% in total at their top levels; the engine caps all meta power at +${Math.round(META_MAX_TOTAL * 100)}%`);
|
|
388
|
+
return issues;
|
|
389
|
+
}
|
|
390
|
+
/** Forget all declarations (the build and tests load several game modules in one process). */
|
|
391
|
+
export function resetRegistry() {
|
|
392
|
+
reg = fresh();
|
|
393
|
+
sealed = false;
|
|
394
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { GameManifest, PlayerView, SpawnEdgeOptions } from "./registry.js";
|
|
2
|
+
/** What the scheduler needs from the simulation it drives. */
|
|
3
|
+
export interface SystemHost {
|
|
4
|
+
readonly tickRate: number;
|
|
5
|
+
tick(): number;
|
|
6
|
+
players(): readonly PlayerView[];
|
|
7
|
+
spawn(prefab: string, at: readonly [number, number, number], opts?: {
|
|
8
|
+
readonly elite?: boolean;
|
|
9
|
+
}): void;
|
|
10
|
+
random(): number;
|
|
11
|
+
emit(event: string, players?: readonly number[]): void;
|
|
12
|
+
/** Living enemies of a prefab (or all). Hosts that do not simulate enemies may omit it. */
|
|
13
|
+
alive?(prefab?: string): number;
|
|
14
|
+
respawnDead?(): void;
|
|
15
|
+
end?(outcome: "won" | "lost"): void;
|
|
16
|
+
/**
|
|
17
|
+
* A ring point around (cx, cz) inside the arena and clear of obstacles,
|
|
18
|
+
* or null. Hosts without one get plain ring math.
|
|
19
|
+
*/
|
|
20
|
+
ringPoint?(cx: number, cz: number, radius: number, prefab: string): readonly [number, number] | null;
|
|
21
|
+
/** Spawn just off screen (the host knows the camera). Hosts without one fall back to a ring at 24 m. */
|
|
22
|
+
spawnEdge?(prefab: string, opts: SpawnEdgeOptions): number;
|
|
23
|
+
spawnPickup?(prefab: string, at: readonly [number, number, number]): void;
|
|
24
|
+
announce?(text: string, tone: "info" | "danger" | "reward"): void;
|
|
25
|
+
upcoming?(label: string, seconds: number): void;
|
|
26
|
+
bossAlive?(): boolean;
|
|
27
|
+
pauseDirector?(seconds: number): void;
|
|
28
|
+
surge?(multiplier: number, seconds: number): void;
|
|
29
|
+
xpBoost?(multiplier: number, seconds: number): void;
|
|
30
|
+
scatter?(prefab: string, count: number): number;
|
|
31
|
+
clear?(prefab: string): number;
|
|
32
|
+
onScreen?(x: number, z: number): boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Runs the declared systems' schedules against a host, one fixed tick at a
|
|
36
|
+
* time. Timers are tick-based (seconds are rounded to whole ticks at
|
|
37
|
+
* declaration), so the schedule is identical on the server and in client
|
|
38
|
+
* prediction. Timers fire in declaration order within a tick.
|
|
39
|
+
*/
|
|
40
|
+
export declare class SystemScheduler {
|
|
41
|
+
private readonly manifest;
|
|
42
|
+
private readonly host;
|
|
43
|
+
private readonly timers;
|
|
44
|
+
private readonly perTick;
|
|
45
|
+
private readonly listeners;
|
|
46
|
+
private started;
|
|
47
|
+
constructor(manifest: GameManifest, host: SystemHost);
|
|
48
|
+
private toTicks;
|
|
49
|
+
private context;
|
|
50
|
+
/** Run every system's setup once. Call at the start of a run, before the first `tick`. */
|
|
51
|
+
start(): void;
|
|
52
|
+
/** Deliver a game event to the systems that subscribed with `on`. */
|
|
53
|
+
dispatch(event: string, players: readonly number[]): void;
|
|
54
|
+
/** Fire everything due at the host's current tick. Call once per fixed tick. */
|
|
55
|
+
tick(): void;
|
|
56
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { GameDefinitionError } from "./schemas.js";
|
|
2
|
+
/**
|
|
3
|
+
* Runs the declared systems' schedules against a host, one fixed tick at a
|
|
4
|
+
* time. Timers are tick-based (seconds are rounded to whole ticks at
|
|
5
|
+
* declaration), so the schedule is identical on the server and in client
|
|
6
|
+
* prediction. Timers fire in declaration order within a tick.
|
|
7
|
+
*/
|
|
8
|
+
export class SystemScheduler {
|
|
9
|
+
manifest;
|
|
10
|
+
host;
|
|
11
|
+
timers = [];
|
|
12
|
+
perTick = [];
|
|
13
|
+
listeners = new Map();
|
|
14
|
+
started = false;
|
|
15
|
+
constructor(manifest, host) {
|
|
16
|
+
this.manifest = manifest;
|
|
17
|
+
this.host = host;
|
|
18
|
+
}
|
|
19
|
+
toTicks(seconds, what) {
|
|
20
|
+
if (!(Number.isFinite(seconds) && seconds > 0)) {
|
|
21
|
+
throw new GameDefinitionError(what, [`seconds must be a positive number, got ${seconds}`]);
|
|
22
|
+
}
|
|
23
|
+
return Math.max(1, Math.round(seconds * this.host.tickRate));
|
|
24
|
+
}
|
|
25
|
+
context(system) {
|
|
26
|
+
const h = this.host;
|
|
27
|
+
const prefabs = this.manifest.prefabs;
|
|
28
|
+
const checkPrefab = (id) => {
|
|
29
|
+
if (!prefabs.has(id))
|
|
30
|
+
throw new GameDefinitionError(`system("${system}")`, [`spawns "${id}", which is not a declared prefab`]);
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
tick: () => h.tick(),
|
|
34
|
+
time: () => h.tick() / h.tickRate,
|
|
35
|
+
players: () => h.players(),
|
|
36
|
+
every: (seconds, fn) => {
|
|
37
|
+
const period = this.toTicks(seconds, `system("${system}").every`);
|
|
38
|
+
this.timers.push({ due: h.tick() + period, period, n: 0, fn, repeat: true });
|
|
39
|
+
},
|
|
40
|
+
after: (seconds, fn) => {
|
|
41
|
+
const period = this.toTicks(seconds, `system("${system}").after`);
|
|
42
|
+
this.timers.push({ due: h.tick() + period, period, n: 0, fn: () => fn(), repeat: false });
|
|
43
|
+
},
|
|
44
|
+
onTick: (fn) => {
|
|
45
|
+
this.perTick.push(fn);
|
|
46
|
+
},
|
|
47
|
+
spawn: (id, at, opts) => {
|
|
48
|
+
checkPrefab(id);
|
|
49
|
+
h.spawn(id, at, opts);
|
|
50
|
+
},
|
|
51
|
+
spawnRing: (id, opts) => {
|
|
52
|
+
checkPrefab(id);
|
|
53
|
+
const all = h.players().filter((p) => p.alive);
|
|
54
|
+
const centre = opts.around !== undefined ? all.filter((p) => p.id === opts.around) : all;
|
|
55
|
+
const count = Math.max(0, Math.floor(opts.count));
|
|
56
|
+
for (let i = 0; i < count; i++) {
|
|
57
|
+
const p = centre.length > 0 ? centre[Math.floor(h.random() * centre.length)] : undefined;
|
|
58
|
+
const [cx, cy, cz] = p ? p.position : [0, 0, 0];
|
|
59
|
+
if (h.ringPoint) {
|
|
60
|
+
const at = h.ringPoint(cx, cz, opts.radius, id);
|
|
61
|
+
if (at)
|
|
62
|
+
h.spawn(id, [at[0], cy, at[1]]);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const a = h.random() * Math.PI * 2;
|
|
66
|
+
h.spawn(id, [cx + Math.cos(a) * opts.radius, cy, cz + Math.sin(a) * opts.radius]);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
spawnEdge: (id, opts) => {
|
|
70
|
+
checkPrefab(id);
|
|
71
|
+
const count = Math.max(0, Math.floor(opts.count));
|
|
72
|
+
if (count === 0)
|
|
73
|
+
return 0;
|
|
74
|
+
if (h.spawnEdge)
|
|
75
|
+
return h.spawnEdge(id, { ...opts, count });
|
|
76
|
+
const all = h.players().filter((p) => p.alive);
|
|
77
|
+
let n = 0;
|
|
78
|
+
for (let i = 0; i < count; i++) {
|
|
79
|
+
const p = all.length > 0 ? all[Math.floor(h.random() * all.length)] : undefined;
|
|
80
|
+
const [cx, cy, cz] = p ? p.position : [0, 0, 0];
|
|
81
|
+
const a = h.random() * Math.PI * 2;
|
|
82
|
+
h.spawn(id, [cx + Math.cos(a) * 24, cy, cz + Math.sin(a) * 24]);
|
|
83
|
+
n++;
|
|
84
|
+
}
|
|
85
|
+
return n;
|
|
86
|
+
},
|
|
87
|
+
emit: (event, players) => h.emit(event, players),
|
|
88
|
+
random: () => h.random(),
|
|
89
|
+
alive: (id) => {
|
|
90
|
+
if (id !== undefined)
|
|
91
|
+
checkPrefab(id);
|
|
92
|
+
return h.alive ? h.alive(id) : 0;
|
|
93
|
+
},
|
|
94
|
+
on: (event, fn) => {
|
|
95
|
+
const list = this.listeners.get(event) ?? [];
|
|
96
|
+
list.push(fn);
|
|
97
|
+
this.listeners.set(event, list);
|
|
98
|
+
},
|
|
99
|
+
respawnDead: () => h.respawnDead?.(),
|
|
100
|
+
spawnPickup: (id, at) => {
|
|
101
|
+
checkPrefab(id);
|
|
102
|
+
h.spawnPickup?.(id, at);
|
|
103
|
+
},
|
|
104
|
+
announce: (text, opts) => {
|
|
105
|
+
const t = String(text).slice(0, 60);
|
|
106
|
+
if (t)
|
|
107
|
+
h.announce?.(t, opts?.tone ?? "info");
|
|
108
|
+
},
|
|
109
|
+
upcoming: (label, seconds) => {
|
|
110
|
+
const t = String(label).slice(0, 40);
|
|
111
|
+
if (t)
|
|
112
|
+
h.upcoming?.(t, this.toTicks(seconds, `system("${system}").upcoming`) / h.tickRate);
|
|
113
|
+
},
|
|
114
|
+
bossAlive: () => h.bossAlive?.() ?? false,
|
|
115
|
+
pauseDirector: (seconds) => h.pauseDirector?.(this.toTicks(seconds, `system("${system}").pauseDirector`) / h.tickRate),
|
|
116
|
+
surge: (multiplier, seconds) => h.surge?.(Math.max(0, Math.min(10, multiplier)), this.toTicks(seconds, `system("${system}").surge`) / h.tickRate),
|
|
117
|
+
xpBoost: (multiplier, seconds) => h.xpBoost?.(Math.max(0, Math.min(10, multiplier)), this.toTicks(seconds, `system("${system}").xpBoost`) / h.tickRate),
|
|
118
|
+
scatter: (id, count) => {
|
|
119
|
+
checkPrefab(id);
|
|
120
|
+
return h.scatter?.(id, Math.max(0, Math.min(200, Math.floor(count)))) ?? 0;
|
|
121
|
+
},
|
|
122
|
+
clear: (id) => {
|
|
123
|
+
checkPrefab(id);
|
|
124
|
+
return h.clear?.(id) ?? 0;
|
|
125
|
+
},
|
|
126
|
+
end: (outcome) => h.end?.(outcome),
|
|
127
|
+
onScreen: (x, z) => h.onScreen?.(x, z) ?? false,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Run every system's setup once. Call at the start of a run, before the first `tick`. */
|
|
131
|
+
start() {
|
|
132
|
+
if (this.started)
|
|
133
|
+
return;
|
|
134
|
+
this.started = true;
|
|
135
|
+
for (const [name, setup] of this.manifest.systems)
|
|
136
|
+
setup(this.context(name));
|
|
137
|
+
}
|
|
138
|
+
/** Deliver a game event to the systems that subscribed with `on`. */
|
|
139
|
+
dispatch(event, players) {
|
|
140
|
+
for (const fn of this.listeners.get(event) ?? [])
|
|
141
|
+
fn(players);
|
|
142
|
+
}
|
|
143
|
+
/** Fire everything due at the host's current tick. Call once per fixed tick. */
|
|
144
|
+
tick() {
|
|
145
|
+
const now = this.host.tick();
|
|
146
|
+
for (const fn of this.perTick)
|
|
147
|
+
fn();
|
|
148
|
+
for (let i = 0; i < this.timers.length; i++) {
|
|
149
|
+
const t = this.timers[i];
|
|
150
|
+
if (!t || t.due > now)
|
|
151
|
+
continue;
|
|
152
|
+
t.n++;
|
|
153
|
+
t.fn(t.n);
|
|
154
|
+
if (t.repeat)
|
|
155
|
+
t.due += t.period;
|
|
156
|
+
else
|
|
157
|
+
this.timers.splice(i--, 1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|