@netforge/engine 0.0.0-stage → 0.2.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 +100 -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 +215 -0
- package/dist/net/protocol.js +10 -0
- package/dist/net/room-client.d.ts +65 -0
- package/dist/net/room-client.js +156 -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-full.txt +460 -0
- package/llms.txt +183 -0
- package/package.json +104 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal saves and the coin model, as the engine declares them.
|
|
3
|
+
*
|
|
4
|
+
* A player's progress (achievements, unlocked keepers, today's goals,
|
|
5
|
+
* titles, owned cosmetics and unlocks, meta upgrade levels, settings) is
|
|
6
|
+
* kept per player on the room server, keyed by the signed-in wallet or a
|
|
7
|
+
* guest id, never in the browser. The room grants achievements, goals and
|
|
8
|
+
* titles from the authoritative sim, checks keeper picks against what the
|
|
9
|
+
* profile has unlocked, and applies owned meta upgrades at the start of
|
|
10
|
+
* every run. This module is the shared, pure part: what a game declares
|
|
11
|
+
* that a profile can hold, and the rules both the server and clients use
|
|
12
|
+
* (unlocks, today's goals, meta prices and caps). No I/O here.
|
|
13
|
+
*/
|
|
14
|
+
import type { GameManifest } from "../api/registry.js";
|
|
15
|
+
import { type EconomyDef, type MetaStat, type MetaUpgradeDef, type SkuGrant } from "../api/schemas.js";
|
|
16
|
+
/** What a SKU is in the coin model: an in-run consumable, a look owned for good, or an unlock owned for good. */
|
|
17
|
+
export type SkuKind = "consumable" | "cosmetic" | "unlock" | "gear";
|
|
18
|
+
export interface ProfileSku {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly kind: SkuKind;
|
|
21
|
+
/** Whole coins, or null when the SKU sells for dollars only. */
|
|
22
|
+
readonly coins: number | null;
|
|
23
|
+
readonly grants: SkuGrant | null;
|
|
24
|
+
}
|
|
25
|
+
/** What one game lets a profile hold, from its manifest. */
|
|
26
|
+
export interface ProfileSpec {
|
|
27
|
+
/**
|
|
28
|
+
* Keepers in index order (the index rides the input bits); `unlock` is the
|
|
29
|
+
* achievement that opens it; `sold`: a store SKU sells it (then it has no
|
|
30
|
+
* unlock, and it is open only once bought).
|
|
31
|
+
*/
|
|
32
|
+
readonly keepers: readonly {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly unlock: string | null;
|
|
35
|
+
readonly sold: boolean;
|
|
36
|
+
}[];
|
|
37
|
+
readonly achievements: readonly string[];
|
|
38
|
+
readonly missions: ReadonlyMap<string, {
|
|
39
|
+
readonly when: string;
|
|
40
|
+
readonly n: number;
|
|
41
|
+
}>;
|
|
42
|
+
readonly titles: readonly string[];
|
|
43
|
+
readonly skus: ReadonlyMap<string, ProfileSku>;
|
|
44
|
+
readonly meta: ReadonlyMap<string, MetaUpgradeDef>;
|
|
45
|
+
readonly economy: EconomyDef;
|
|
46
|
+
}
|
|
47
|
+
/** Event a run reports for a player's profile (the room writes it; the browser never does). */
|
|
48
|
+
export interface RunProgress {
|
|
49
|
+
readonly player: number;
|
|
50
|
+
readonly kind: "achievement" | "goal" | "title";
|
|
51
|
+
/** The declared achievement, mission or title id. */
|
|
52
|
+
readonly id: string;
|
|
53
|
+
}
|
|
54
|
+
/** Which keepers a profile may play (by index), from its achievements and owned unlocks. */
|
|
55
|
+
export interface ProfileUnlockState {
|
|
56
|
+
readonly achievements: Iterable<string>;
|
|
57
|
+
/** Achievements the profile holds only through the one-time local import: they never open a sold keeper. */
|
|
58
|
+
readonly imported?: Iterable<string>;
|
|
59
|
+
/** Owned SKU ids. */
|
|
60
|
+
readonly owned: Iterable<string>;
|
|
61
|
+
}
|
|
62
|
+
export declare function skuKind(grants: SkuGrant | undefined | null): SkuKind;
|
|
63
|
+
/** Owned gear (losable, staked runs only): the SKU, the run upgrade it brings, and its loss line. */
|
|
64
|
+
export declare function ownedGear(spec: ProfileSpec, owned: Iterable<string>): Array<{
|
|
65
|
+
sku: string;
|
|
66
|
+
upgrade: string;
|
|
67
|
+
loseBelow: number;
|
|
68
|
+
coins: number;
|
|
69
|
+
}>;
|
|
70
|
+
/** The profile view of a manifest. */
|
|
71
|
+
export declare function profileSpec(m: GameManifest): ProfileSpec;
|
|
72
|
+
/** UTC day number (days since 1970-01-01): daily goals roll over at 00:00 UTC. */
|
|
73
|
+
export declare function utcDay(ms: number): number;
|
|
74
|
+
/** Today's goals: `n` of the declared missions, the same for everyone on a UTC day. */
|
|
75
|
+
export declare function todaysGoals(ids: readonly string[], day: number, n?: number): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Keeper indices a profile may play: free keepers, keepers whose achievement
|
|
78
|
+
* it reached, keepers it bought. A sold keeper opens only by purchase: an
|
|
79
|
+
* imported achievement never opens it (defense in depth; the build already
|
|
80
|
+
* refuses a keeper that is both earned and sold).
|
|
81
|
+
*/
|
|
82
|
+
export declare function openKeepers(spec: ProfileSpec, state: ProfileUnlockState): number[];
|
|
83
|
+
/** Cosmetic ids a profile owns (0, the default look, always). */
|
|
84
|
+
export declare function ownedCosmetics(spec: ProfileSpec, owned: Iterable<string>): number[];
|
|
85
|
+
/** Coins for the next level of a meta upgrade from `level` (0 = none owned), or null at the top. Geometric: cost x growth^level. */
|
|
86
|
+
export declare function metaPrice(def: MetaUpgradeDef, level: number): number | null;
|
|
87
|
+
/**
|
|
88
|
+
* The stat bonus a profile's meta levels give, with the engine's caps
|
|
89
|
+
* applied again (+15% per stat, +30% in total, scaled down evenly past
|
|
90
|
+
* it), whatever the declarations or the stored levels say.
|
|
91
|
+
*/
|
|
92
|
+
export declare function metaBonus(spec: ProfileSpec, levels: Readonly<Record<string, number>>): Partial<Record<MetaStat, number>>;
|
|
93
|
+
/** Meta levels as one integer (3 bits per upgrade, declaration order), the payload of the room's "meta" sim command. */
|
|
94
|
+
export declare function packMeta(spec: ProfileSpec, levels: Readonly<Record<string, number>>): number;
|
|
95
|
+
export declare function unpackMeta(spec: ProfileSpec, value: number): Record<string, number>;
|
|
96
|
+
/** Room command names a client may never send: the room issues them from the profile. */
|
|
97
|
+
export declare const RESERVED_COMMANDS: ReadonlySet<string>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { META_MAX_PER_STAT, META_MAX_TOTAL } from "../api/schemas.js";
|
|
2
|
+
export function skuKind(grants) {
|
|
3
|
+
if (!grants)
|
|
4
|
+
return "consumable";
|
|
5
|
+
return "cosmetic" in grants ? "cosmetic" : "gear" in grants ? "gear" : "unlock";
|
|
6
|
+
}
|
|
7
|
+
/** Owned gear (losable, staked runs only): the SKU, the run upgrade it brings, and its loss line. */
|
|
8
|
+
export function ownedGear(spec, owned) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const id of owned) {
|
|
11
|
+
const s = spec.skus.get(id);
|
|
12
|
+
if (s?.grants && "gear" in s.grants)
|
|
13
|
+
out.push({ sku: id, upgrade: s.grants.gear, loseBelow: s.grants.loseBelow, coins: s.coins ?? 0 });
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
/** The profile view of a manifest. */
|
|
18
|
+
export function profileSpec(m) {
|
|
19
|
+
const skus = new Map();
|
|
20
|
+
const sold = new Set();
|
|
21
|
+
for (const [id, s] of m.skus) {
|
|
22
|
+
skus.set(id, { id, kind: skuKind(s.grants), coins: s.coins ?? null, grants: s.grants ?? null });
|
|
23
|
+
if (s.grants && "keeper" in s.grants)
|
|
24
|
+
sold.add(s.grants.keeper);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
keepers: [...m.keepers].map(([id, k]) => ({ id, unlock: k.unlock ?? null, sold: sold.has(id) })),
|
|
28
|
+
achievements: [...m.achievements.keys()],
|
|
29
|
+
missions: new Map([...m.missions].map(([id, d]) => [id, { when: d.when, n: d.n }])),
|
|
30
|
+
titles: [...m.titles.keys()],
|
|
31
|
+
skus,
|
|
32
|
+
meta: m.metaUpgrades,
|
|
33
|
+
economy: m.economy,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** UTC day number (days since 1970-01-01): daily goals roll over at 00:00 UTC. */
|
|
37
|
+
export function utcDay(ms) {
|
|
38
|
+
return Math.floor(ms / 86_400_000);
|
|
39
|
+
}
|
|
40
|
+
/** Today's goals: `n` of the declared missions, the same for everyone on a UTC day. */
|
|
41
|
+
export function todaysGoals(ids, day, n = 3) {
|
|
42
|
+
const pool = [...ids];
|
|
43
|
+
const out = [];
|
|
44
|
+
let h = (day * 2654435761) >>> 0;
|
|
45
|
+
while (out.length < n && pool.length > 0) {
|
|
46
|
+
h = (Math.imul(h ^ (h >>> 15), 0x2c1b3c6d) + 0x9e3779b9) >>> 0;
|
|
47
|
+
out.push(...pool.splice(h % pool.length, 1));
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Keeper indices a profile may play: free keepers, keepers whose achievement
|
|
53
|
+
* it reached, keepers it bought. A sold keeper opens only by purchase: an
|
|
54
|
+
* imported achievement never opens it (defense in depth; the build already
|
|
55
|
+
* refuses a keeper that is both earned and sold).
|
|
56
|
+
*/
|
|
57
|
+
export function openKeepers(spec, state) {
|
|
58
|
+
const reached = new Set(state.achievements);
|
|
59
|
+
const imported = new Set(state.imported ?? []);
|
|
60
|
+
const bought = new Set();
|
|
61
|
+
for (const id of state.owned) {
|
|
62
|
+
const g = spec.skus.get(id)?.grants;
|
|
63
|
+
if (g && "keeper" in g)
|
|
64
|
+
bought.add(g.keeper);
|
|
65
|
+
}
|
|
66
|
+
const out = [];
|
|
67
|
+
spec.keepers.forEach((k, i) => {
|
|
68
|
+
if (bought.has(k.id))
|
|
69
|
+
return void out.push(i);
|
|
70
|
+
if (k.sold) {
|
|
71
|
+
if (k.unlock !== null && reached.has(k.unlock) && !imported.has(k.unlock))
|
|
72
|
+
out.push(i);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (k.unlock === null || reached.has(k.unlock))
|
|
76
|
+
out.push(i);
|
|
77
|
+
});
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** Cosmetic ids a profile owns (0, the default look, always). */
|
|
81
|
+
export function ownedCosmetics(spec, owned) {
|
|
82
|
+
const out = new Set([0]);
|
|
83
|
+
for (const id of owned) {
|
|
84
|
+
const g = spec.skus.get(id)?.grants;
|
|
85
|
+
if (g && "cosmetic" in g)
|
|
86
|
+
out.add(g.cosmetic);
|
|
87
|
+
}
|
|
88
|
+
return [...out].sort((a, b) => a - b);
|
|
89
|
+
}
|
|
90
|
+
/** Coins for the next level of a meta upgrade from `level` (0 = none owned), or null at the top. Geometric: cost x growth^level. */
|
|
91
|
+
export function metaPrice(def, level) {
|
|
92
|
+
if (!Number.isInteger(level) || level < 0 || level >= def.levels)
|
|
93
|
+
return null;
|
|
94
|
+
return Math.round(def.cost * Math.pow(def.growth, level));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The stat bonus a profile's meta levels give, with the engine's caps
|
|
98
|
+
* applied again (+15% per stat, +30% in total, scaled down evenly past
|
|
99
|
+
* it), whatever the declarations or the stored levels say.
|
|
100
|
+
*/
|
|
101
|
+
export function metaBonus(spec, levels) {
|
|
102
|
+
const out = {};
|
|
103
|
+
for (const [id, def] of spec.meta) {
|
|
104
|
+
const lv = Math.max(0, Math.min(def.levels, Math.floor(levels[id] ?? 0)));
|
|
105
|
+
if (lv > 0)
|
|
106
|
+
out[def.stat] = (out[def.stat] ?? 0) + def.step * lv;
|
|
107
|
+
}
|
|
108
|
+
let total = 0;
|
|
109
|
+
for (const k of Object.keys(out)) {
|
|
110
|
+
out[k] = Math.min(META_MAX_PER_STAT, out[k] ?? 0);
|
|
111
|
+
total += out[k] ?? 0;
|
|
112
|
+
}
|
|
113
|
+
if (total > META_MAX_TOTAL)
|
|
114
|
+
for (const k of Object.keys(out))
|
|
115
|
+
out[k] = ((out[k] ?? 0) * META_MAX_TOTAL) / total;
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/** Meta levels as one integer (3 bits per upgrade, declaration order), the payload of the room's "meta" sim command. */
|
|
119
|
+
export function packMeta(spec, levels) {
|
|
120
|
+
let v = 0;
|
|
121
|
+
let i = 0;
|
|
122
|
+
for (const [id, def] of spec.meta) {
|
|
123
|
+
const lv = Math.max(0, Math.min(def.levels, 7, Math.floor(levels[id] ?? 0)));
|
|
124
|
+
v += lv * Math.pow(8, i++);
|
|
125
|
+
}
|
|
126
|
+
return v;
|
|
127
|
+
}
|
|
128
|
+
export function unpackMeta(spec, value) {
|
|
129
|
+
const out = {};
|
|
130
|
+
let v = Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
131
|
+
for (const [id, def] of spec.meta) {
|
|
132
|
+
const lv = Math.min(def.levels, v % 8);
|
|
133
|
+
if (lv > 0)
|
|
134
|
+
out[id] = lv;
|
|
135
|
+
v = Math.floor(v / 8);
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
/** Room command names a client may never send: the room issues them from the profile. */
|
|
140
|
+
export const RESERVED_COMMANDS = new Set(["meta"]);
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The race machinery shared by the platformer and the racer templates: a
|
|
3
|
+
* round is a run (countdown, race, results), finishing order, seat bots that
|
|
4
|
+
* fill empty places and never earn, the declared systems on the race clock,
|
|
5
|
+
* earn rules, achievements and missions. Each template plugs its own sim in
|
|
6
|
+
* through `RaceCore` (platformer.ts, racer.ts).
|
|
7
|
+
*
|
|
8
|
+
* Runs only on the authoritative side (room server, headless harness).
|
|
9
|
+
* Clients predict their own body and read the rest from snapshots and the
|
|
10
|
+
* broadcast `RaceInfo`.
|
|
11
|
+
*/
|
|
12
|
+
import type { InputCommand } from "@netforge/engine-sim";
|
|
13
|
+
import type { GameManifest } from "../api/registry.js";
|
|
14
|
+
import type { EarnAward, GameEvent } from "./earn.js";
|
|
15
|
+
import type { RunProgress } from "./profile.js";
|
|
16
|
+
/** What the sim says happened this tick, as named game events for one player. */
|
|
17
|
+
export interface RaceSimEvent {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly player: number;
|
|
20
|
+
}
|
|
21
|
+
/** A template's sim as the race runner drives it. */
|
|
22
|
+
export interface RaceCore {
|
|
23
|
+
readonly tickRate: number;
|
|
24
|
+
/** Player ids present, ascending. */
|
|
25
|
+
ids(): number[];
|
|
26
|
+
add(id: number, bot: boolean, frozen: boolean): void;
|
|
27
|
+
remove(id: number): void;
|
|
28
|
+
/** A fresh round on course `course`: everyone back to the start, run upgrades cleared, frozen or not. */
|
|
29
|
+
reset(course: number, frozen: boolean): void;
|
|
30
|
+
setFrozen(frozen: boolean): void;
|
|
31
|
+
finished(id: number): boolean;
|
|
32
|
+
/** Course progress, 0..1 (1: finished). */
|
|
33
|
+
progress(id: number): number;
|
|
34
|
+
position(id: number): readonly [number, number, number];
|
|
35
|
+
/** One fixed tick. Events: "player.finished" plus the template's own ("coin.collected", "lap.completed", ...). */
|
|
36
|
+
step(inputs: ReadonlyMap<number, InputCommand>): RaceSimEvent[];
|
|
37
|
+
/** What a bot does now: deterministic, `skill` 0..1. */
|
|
38
|
+
botInput(id: number, clock: number, skill: number): InputCommand;
|
|
39
|
+
/** One level of a declared upgrade (store SKU or gear) for the rest of the round. */
|
|
40
|
+
grant(id: number, upgrade: number): boolean;
|
|
41
|
+
setCosmetic(id: number, cosmetic: number): void;
|
|
42
|
+
}
|
|
43
|
+
export type RacePhase = "countdown" | "racing" | "results";
|
|
44
|
+
export interface RacePlayerInfo {
|
|
45
|
+
readonly id: number;
|
|
46
|
+
readonly bot: boolean;
|
|
47
|
+
/** Finishing place (1 = first), or null while racing. */
|
|
48
|
+
readonly place: number | null;
|
|
49
|
+
/** Seconds from the start to the finish line, or null. */
|
|
50
|
+
readonly time: number | null;
|
|
51
|
+
/** 0..1 along the course. */
|
|
52
|
+
readonly progress: number;
|
|
53
|
+
/** Live position among everyone (finished first, then by progress). */
|
|
54
|
+
readonly rank: number;
|
|
55
|
+
readonly counts: Readonly<Record<string, number>>;
|
|
56
|
+
readonly ready: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface RaceInfo {
|
|
59
|
+
readonly run: number;
|
|
60
|
+
readonly phase: RacePhase;
|
|
61
|
+
/** The course (level or track) index this round runs. */
|
|
62
|
+
readonly course: number;
|
|
63
|
+
/** Seconds left of the countdown (countdown phase). */
|
|
64
|
+
readonly countdown: number;
|
|
65
|
+
/** Seconds since the start signal. */
|
|
66
|
+
readonly seconds: number;
|
|
67
|
+
/** Seconds left before the round closes (time limit), or null. */
|
|
68
|
+
readonly left: number | null;
|
|
69
|
+
readonly players: readonly RacePlayerInfo[];
|
|
70
|
+
readonly announce: {
|
|
71
|
+
readonly id: number;
|
|
72
|
+
readonly text: string;
|
|
73
|
+
readonly tone: "info" | "danger" | "reward";
|
|
74
|
+
} | null;
|
|
75
|
+
readonly next: {
|
|
76
|
+
readonly label: string;
|
|
77
|
+
readonly in: number;
|
|
78
|
+
} | null;
|
|
79
|
+
/** Seconds until the next round while results show. */
|
|
80
|
+
readonly nextRunIn: number | null;
|
|
81
|
+
/** Results: each human's coins by earn rule this round. */
|
|
82
|
+
readonly coins: readonly {
|
|
83
|
+
readonly id: number;
|
|
84
|
+
readonly rules: readonly {
|
|
85
|
+
readonly rule: string;
|
|
86
|
+
readonly units: number;
|
|
87
|
+
}[];
|
|
88
|
+
}[];
|
|
89
|
+
}
|
|
90
|
+
export interface RaceRunnerOptions {
|
|
91
|
+
/** Seconds of countdown before each round. Default 3. */
|
|
92
|
+
readonly countdownSeconds?: number;
|
|
93
|
+
/** Seconds the results show. Default 8. */
|
|
94
|
+
readonly endSeconds?: number;
|
|
95
|
+
/** After every person present has finished, seconds the others get. Default 4. */
|
|
96
|
+
readonly graceSeconds?: number;
|
|
97
|
+
/** After the first person finishes, seconds everyone else has to reach the finish (an idle player never holds the round). Default 25. */
|
|
98
|
+
readonly finishWindowSeconds?: number;
|
|
99
|
+
}
|
|
100
|
+
export interface RaceStep {
|
|
101
|
+
readonly events: readonly GameEvent[];
|
|
102
|
+
readonly awards: readonly EarnAward[];
|
|
103
|
+
readonly reset: boolean;
|
|
104
|
+
}
|
|
105
|
+
export declare class RaceRunner {
|
|
106
|
+
readonly manifest: GameManifest;
|
|
107
|
+
readonly core: RaceCore;
|
|
108
|
+
private readonly opts;
|
|
109
|
+
private scheduler;
|
|
110
|
+
private runN;
|
|
111
|
+
private runTick;
|
|
112
|
+
private clock;
|
|
113
|
+
private phase;
|
|
114
|
+
private phaseTicks;
|
|
115
|
+
private course;
|
|
116
|
+
private pending;
|
|
117
|
+
private dispatchingUnpaid;
|
|
118
|
+
private readonly lateStart;
|
|
119
|
+
private readonly earn;
|
|
120
|
+
private readonly achievements;
|
|
121
|
+
private readonly missions;
|
|
122
|
+
private readonly tracked;
|
|
123
|
+
private reached;
|
|
124
|
+
private readonly places;
|
|
125
|
+
private readonly times;
|
|
126
|
+
private readonly coins;
|
|
127
|
+
private readonly ready;
|
|
128
|
+
private announcement;
|
|
129
|
+
private announceN;
|
|
130
|
+
private upcomingAt;
|
|
131
|
+
private allDoneTick;
|
|
132
|
+
/** Run tick the round closes at once someone has finished (-1: nobody yet). */
|
|
133
|
+
private closeAt;
|
|
134
|
+
private readonly windowTicks;
|
|
135
|
+
private rng;
|
|
136
|
+
private readonly countdownTicks;
|
|
137
|
+
private readonly endTicks;
|
|
138
|
+
private readonly graceTicks;
|
|
139
|
+
private readonly limitTicks;
|
|
140
|
+
constructor(manifest: GameManifest, core: RaceCore, opts: {
|
|
141
|
+
readonly bots: number;
|
|
142
|
+
readonly roundSeconds: number | null;
|
|
143
|
+
readonly courses: number;
|
|
144
|
+
readonly seed: number;
|
|
145
|
+
} & RaceRunnerOptions);
|
|
146
|
+
private random;
|
|
147
|
+
private newRun;
|
|
148
|
+
private playerViews;
|
|
149
|
+
get phaseNow(): RacePhase;
|
|
150
|
+
get runNumber(): number;
|
|
151
|
+
get courseNow(): number;
|
|
152
|
+
placeOf(id: number): number | null;
|
|
153
|
+
/** Seats bots fill up to, with at least one person present; one change a tick. */
|
|
154
|
+
private fillSeats;
|
|
155
|
+
/** A player joined (room addPlayer): they race from the start, held if the countdown runs. */
|
|
156
|
+
joined(id: number): void;
|
|
157
|
+
left(id: number): void;
|
|
158
|
+
/** Call before each sim step: seats, systems, the clock, and every bot's input. */
|
|
159
|
+
beforeStep(inputs: Map<number, InputCommand>): void;
|
|
160
|
+
private humansPresent;
|
|
161
|
+
private record;
|
|
162
|
+
/** The round is over: "round.finished" for everyone present, then results. */
|
|
163
|
+
private closeRound;
|
|
164
|
+
/** Call after each sim step with its events. */
|
|
165
|
+
afterStep(sim: readonly RaceSimEvent[]): RaceStep;
|
|
166
|
+
private allReady;
|
|
167
|
+
private progress;
|
|
168
|
+
takeProgress(): RunProgress[];
|
|
169
|
+
/** A referee run starts for this player: counts start over; a run begun mid-race is not paid world progress. */
|
|
170
|
+
startRun(player: number): void;
|
|
171
|
+
count(player: number, event: string): number;
|
|
172
|
+
/** Live order: finished by place, then everyone else by progress (ties by id). */
|
|
173
|
+
standings(): number[];
|
|
174
|
+
/** Ticks until the round closes (the time limit, or the finish window once someone has finished), or null. */
|
|
175
|
+
private leftTicks;
|
|
176
|
+
info(): RaceInfo;
|
|
177
|
+
}
|