@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.
Files changed (245) hide show
  1. package/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +100 -2
  5. package/dist/api/appearance.d.ts +37 -0
  6. package/dist/api/appearance.js +41 -0
  7. package/dist/api/cards.d.ts +105 -0
  8. package/dist/api/cards.js +133 -0
  9. package/dist/api/collector.d.ts +114 -0
  10. package/dist/api/collector.js +204 -0
  11. package/dist/api/dungeon.d.ts +58 -0
  12. package/dist/api/dungeon.js +216 -0
  13. package/dist/api/harness.d.ts +149 -0
  14. package/dist/api/harness.js +549 -0
  15. package/dist/api/idle.d.ts +71 -0
  16. package/dist/api/idle.js +104 -0
  17. package/dist/api/race-harness.d.ts +3 -0
  18. package/dist/api/race-harness.js +148 -0
  19. package/dist/api/registry.d.ts +241 -0
  20. package/dist/api/registry.js +394 -0
  21. package/dist/api/scheduler.d.ts +56 -0
  22. package/dist/api/scheduler.js +160 -0
  23. package/dist/api/schemas.d.ts +958 -0
  24. package/dist/api/schemas.js +859 -0
  25. package/dist/api/tower.d.ts +127 -0
  26. package/dist/api/tower.js +183 -0
  27. package/dist/game/adapter.d.ts +69 -0
  28. package/dist/game/adapter.js +341 -0
  29. package/dist/game/arena-bot.d.ts +66 -0
  30. package/dist/game/arena-bot.js +188 -0
  31. package/dist/game/bot.d.ts +95 -0
  32. package/dist/game/bot.js +285 -0
  33. package/dist/game/compile.d.ts +97 -0
  34. package/dist/game/compile.js +476 -0
  35. package/dist/game/director.d.ts +42 -0
  36. package/dist/game/director.js +179 -0
  37. package/dist/game/earn-source.d.ts +122 -0
  38. package/dist/game/earn-source.js +386 -0
  39. package/dist/game/earn.d.ts +49 -0
  40. package/dist/game/earn.js +55 -0
  41. package/dist/game/index.d.ts +37 -0
  42. package/dist/game/index.js +25 -0
  43. package/dist/game/platformer.d.ts +156 -0
  44. package/dist/game/platformer.js +619 -0
  45. package/dist/game/profile.d.ts +97 -0
  46. package/dist/game/profile.js +140 -0
  47. package/dist/game/race.d.ts +177 -0
  48. package/dist/game/race.js +386 -0
  49. package/dist/game/racer.d.ts +98 -0
  50. package/dist/game/racer.js +379 -0
  51. package/dist/game/runner.d.ts +286 -0
  52. package/dist/game/runner.js +824 -0
  53. package/dist/game/threats.d.ts +23 -0
  54. package/dist/game/threats.js +49 -0
  55. package/dist/game/view.d.ts +47 -0
  56. package/dist/game/view.js +123 -0
  57. package/dist/index.d.ts +33 -0
  58. package/dist/index.js +24 -0
  59. package/dist/net/bits.d.ts +33 -0
  60. package/dist/net/bits.js +135 -0
  61. package/dist/net/client.d.ts +75 -0
  62. package/dist/net/client.js +151 -0
  63. package/dist/net/index.d.ts +23 -0
  64. package/dist/net/index.js +23 -0
  65. package/dist/net/input.d.ts +49 -0
  66. package/dist/net/input.js +112 -0
  67. package/dist/net/interpolation.d.ts +37 -0
  68. package/dist/net/interpolation.js +97 -0
  69. package/dist/net/netsim.d.ts +22 -0
  70. package/dist/net/netsim.js +52 -0
  71. package/dist/net/prediction.d.ts +31 -0
  72. package/dist/net/prediction.js +101 -0
  73. package/dist/net/profile-client.d.ts +75 -0
  74. package/dist/net/profile-client.js +173 -0
  75. package/dist/net/protocol.d.ts +215 -0
  76. package/dist/net/protocol.js +10 -0
  77. package/dist/net/room-client.d.ts +65 -0
  78. package/dist/net/room-client.js +156 -0
  79. package/dist/net/schema.d.ts +78 -0
  80. package/dist/net/schema.js +121 -0
  81. package/dist/net/sim-adapter.d.ts +134 -0
  82. package/dist/net/sim-adapter.js +22 -0
  83. package/dist/net/snapshot.d.ts +61 -0
  84. package/dist/net/snapshot.js +301 -0
  85. package/dist/rules/adapter.d.ts +53 -0
  86. package/dist/rules/adapter.js +261 -0
  87. package/dist/rules/cards.d.ts +115 -0
  88. package/dist/rules/cards.js +547 -0
  89. package/dist/rules/collector.d.ts +161 -0
  90. package/dist/rules/collector.js +732 -0
  91. package/dist/rules/genres.d.ts +13 -0
  92. package/dist/rules/genres.js +38 -0
  93. package/dist/rules/harness.d.ts +16 -0
  94. package/dist/rules/harness.js +127 -0
  95. package/dist/rules/idle.d.ts +115 -0
  96. package/dist/rules/idle.js +370 -0
  97. package/dist/rules/index.d.ts +16 -0
  98. package/dist/rules/index.js +14 -0
  99. package/dist/rules/kit.d.ts +171 -0
  100. package/dist/rules/kit.js +88 -0
  101. package/dist/rules/tower.d.ts +214 -0
  102. package/dist/rules/tower.js +655 -0
  103. package/dist/runtime/arena.d.ts +86 -0
  104. package/dist/runtime/arena.js +269 -0
  105. package/dist/runtime/assets.d.ts +26 -0
  106. package/dist/runtime/assets.js +35 -0
  107. package/dist/runtime/camera.d.ts +113 -0
  108. package/dist/runtime/camera.js +247 -0
  109. package/dist/runtime/crowd.d.ts +18 -0
  110. package/dist/runtime/crowd.js +62 -0
  111. package/dist/runtime/feel/audio.d.ts +150 -0
  112. package/dist/runtime/feel/audio.js +327 -0
  113. package/dist/runtime/feel/clock.d.ts +52 -0
  114. package/dist/runtime/feel/clock.js +80 -0
  115. package/dist/runtime/feel/flash.d.ts +28 -0
  116. package/dist/runtime/feel/flash.js +60 -0
  117. package/dist/runtime/feel/index.d.ts +27 -0
  118. package/dist/runtime/feel/index.js +16 -0
  119. package/dist/runtime/feel/kit.d.ts +159 -0
  120. package/dist/runtime/feel/kit.js +226 -0
  121. package/dist/runtime/feel/knockback.d.ts +41 -0
  122. package/dist/runtime/feel/knockback.js +70 -0
  123. package/dist/runtime/feel/noise.d.ts +6 -0
  124. package/dist/runtime/feel/noise.js +30 -0
  125. package/dist/runtime/feel/numbers.d.ts +91 -0
  126. package/dist/runtime/feel/numbers.js +172 -0
  127. package/dist/runtime/feel/rumble.d.ts +40 -0
  128. package/dist/runtime/feel/rumble.js +58 -0
  129. package/dist/runtime/feel/sound.d.ts +96 -0
  130. package/dist/runtime/feel/sound.js +176 -0
  131. package/dist/runtime/feel/spring.d.ts +42 -0
  132. package/dist/runtime/feel/spring.js +95 -0
  133. package/dist/runtime/feel/trauma.d.ts +48 -0
  134. package/dist/runtime/feel/trauma.js +76 -0
  135. package/dist/runtime/feel/tween.d.ts +66 -0
  136. package/dist/runtime/feel/tween.js +180 -0
  137. package/dist/runtime/feel/types.d.ts +30 -0
  138. package/dist/runtime/feel/types.js +5 -0
  139. package/dist/runtime/index.d.ts +28 -0
  140. package/dist/runtime/index.js +26 -0
  141. package/dist/runtime/input.d.ts +38 -0
  142. package/dist/runtime/input.js +159 -0
  143. package/dist/runtime/interpolate.d.ts +6 -0
  144. package/dist/runtime/interpolate.js +13 -0
  145. package/dist/runtime/render/animation.d.ts +63 -0
  146. package/dist/runtime/render/animation.js +128 -0
  147. package/dist/runtime/render/atmosphere.d.ts +32 -0
  148. package/dist/runtime/render/atmosphere.js +59 -0
  149. package/dist/runtime/render/crowd-motion.d.ts +68 -0
  150. package/dist/runtime/render/crowd-motion.js +163 -0
  151. package/dist/runtime/render/decals.d.ts +64 -0
  152. package/dist/runtime/render/decals.js +222 -0
  153. package/dist/runtime/render/highlight.d.ts +29 -0
  154. package/dist/runtime/render/highlight.js +110 -0
  155. package/dist/runtime/render/index.d.ts +35 -0
  156. package/dist/runtime/render/index.js +22 -0
  157. package/dist/runtime/render/lighting.d.ts +50 -0
  158. package/dist/runtime/render/lighting.js +142 -0
  159. package/dist/runtime/render/particles.d.ts +117 -0
  160. package/dist/runtime/render/particles.js +289 -0
  161. package/dist/runtime/render/post.d.ts +92 -0
  162. package/dist/runtime/render/post.js +191 -0
  163. package/dist/runtime/render/presets.d.ts +57 -0
  164. package/dist/runtime/render/presets.js +116 -0
  165. package/dist/runtime/render/quality.d.ts +113 -0
  166. package/dist/runtime/render/quality.js +180 -0
  167. package/dist/runtime/render/rig.d.ts +29 -0
  168. package/dist/runtime/render/rig.js +107 -0
  169. package/dist/runtime/render/sprite.d.ts +34 -0
  170. package/dist/runtime/render/sprite.js +58 -0
  171. package/dist/runtime/render/stack.d.ts +74 -0
  172. package/dist/runtime/render/stack.js +132 -0
  173. package/dist/runtime/render/vat.d.ts +78 -0
  174. package/dist/runtime/render/vat.js +219 -0
  175. package/dist/runtime/renderer.d.ts +19 -0
  176. package/dist/runtime/renderer.js +18 -0
  177. package/dist/runtime/stats.d.ts +11 -0
  178. package/dist/runtime/stats.js +29 -0
  179. package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
  180. package/dist/runtime/styles/handdrawn/ground.js +282 -0
  181. package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
  182. package/dist/runtime/styles/handdrawn/index.js +94 -0
  183. package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
  184. package/dist/runtime/styles/handdrawn/ink.js +68 -0
  185. package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
  186. package/dist/runtime/styles/handdrawn/looks.js +42 -0
  187. package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
  188. package/dist/runtime/styles/handdrawn/paint.js +129 -0
  189. package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
  190. package/dist/runtime/styles/handdrawn/palette.js +70 -0
  191. package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
  192. package/dist/runtime/styles/handdrawn/textures.js +150 -0
  193. package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
  194. package/dist/runtime/styles/handdrawn/vfx.js +73 -0
  195. package/dist/runtime/styles/index.d.ts +9 -0
  196. package/dist/runtime/styles/index.js +32 -0
  197. package/dist/runtime/styles/kit.d.ts +193 -0
  198. package/dist/runtime/styles/kit.js +1 -0
  199. package/dist/runtime/styles/pixel/index.d.ts +46 -0
  200. package/dist/runtime/styles/pixel/index.js +167 -0
  201. package/dist/runtime/styles/ps1/index.d.ts +29 -0
  202. package/dist/runtime/styles/ps1/index.js +41 -0
  203. package/dist/runtime/styles/ps1/lights.d.ts +47 -0
  204. package/dist/runtime/styles/ps1/lights.js +146 -0
  205. package/dist/runtime/styles/ps1/materials.d.ts +62 -0
  206. package/dist/runtime/styles/ps1/materials.js +285 -0
  207. package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
  208. package/dist/runtime/styles/ps1/pipeline.js +111 -0
  209. package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
  210. package/dist/runtime/styles/ps1/surfaces.js +228 -0
  211. package/dist/runtime/styles/toon/index.d.ts +5 -0
  212. package/dist/runtime/styles/toon/index.js +178 -0
  213. package/dist/runtime/styles/toon/ink.d.ts +39 -0
  214. package/dist/runtime/styles/toon/ink.js +73 -0
  215. package/dist/runtime/styles/toon/look.d.ts +10 -0
  216. package/dist/runtime/styles/toon/look.js +112 -0
  217. package/dist/runtime/styles/toon/material.d.ts +74 -0
  218. package/dist/runtime/styles/toon/material.js +208 -0
  219. package/dist/runtime/styles/toon/normals.d.ts +26 -0
  220. package/dist/runtime/styles/toon/normals.js +127 -0
  221. package/dist/runtime/styles/toon/shadows.d.ts +24 -0
  222. package/dist/runtime/styles/toon/shadows.js +42 -0
  223. package/dist/runtime/styles/toon/vfx.d.ts +15 -0
  224. package/dist/runtime/styles/toon/vfx.js +47 -0
  225. package/dist/runtime/styles/view.d.ts +35 -0
  226. package/dist/runtime/styles/view.js +34 -0
  227. package/dist/runtime/styles/voxel/core.d.ts +114 -0
  228. package/dist/runtime/styles/voxel/core.js +577 -0
  229. package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
  230. package/dist/runtime/styles/voxel/geometry.js +40 -0
  231. package/dist/runtime/styles/voxel/index.d.ts +21 -0
  232. package/dist/runtime/styles/voxel/index.js +23 -0
  233. package/dist/runtime/styles/voxel/kit.d.ts +6 -0
  234. package/dist/runtime/styles/voxel/kit.js +11 -0
  235. package/dist/runtime/styles/voxel/look.d.ts +10 -0
  236. package/dist/runtime/styles/voxel/look.js +16 -0
  237. package/dist/runtime/styles/voxel/props.d.ts +9 -0
  238. package/dist/runtime/styles/voxel/props.js +52 -0
  239. package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
  240. package/dist/runtime/styles/voxel/shapes.js +83 -0
  241. package/dist/runtime/styles/voxel/world.d.ts +49 -0
  242. package/dist/runtime/styles/voxel/world.js +422 -0
  243. package/llms-full.txt +460 -0
  244. package/llms.txt +183 -0
  245. package/package.json +104 -3
@@ -0,0 +1,23 @@
1
+ import type { EnemyTypeDef } from "@netforge/engine-sim";
2
+ /** One warning on the ground. Lanes run from (x, z) along (dx, dz) for `len` m with half width `r`. */
3
+ export interface ThreatShape {
4
+ readonly kind: "circle" | "lane" | "aura";
5
+ readonly x: number;
6
+ readonly z: number;
7
+ readonly r: number;
8
+ readonly dx?: number;
9
+ readonly dz?: number;
10
+ readonly len?: number;
11
+ /** 0 when it appears, 1 when it lands. */
12
+ readonly progress: number;
13
+ /** True for an enemy shot in flight (a small moving circle). */
14
+ readonly shot?: boolean;
15
+ }
16
+ /** Lane length for an enemy type's telegraphed lane. */
17
+ export declare function laneLength(def: EnemyTypeDef | undefined): number;
18
+ /**
19
+ * The warning an entity shows now, or null. `kind`, `flags`, `type` and
20
+ * `level` are its snapshot fields; (x, z) its position, (vx, vz) its
21
+ * velocity (shots).
22
+ */
23
+ export declare function threatOf(enemyTypes: readonly EnemyTypeDef[], kind: number, flags: number, type: number, level: number, x: number, z: number, vx?: number, vz?: number): ThreatShape | null;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Telegraphs as shapes on the ground: what an attack about to land covers.
3
+ * One reader for the client's decals and the playtest bots' dodging, so
4
+ * what players see is exactly what the bots react to.
5
+ */
6
+ import { decodeTelegraph, EntityFlag, EntityKind, ROLE_DEFAULTS, Telegraph } from "@netforge/engine-sim";
7
+ /** Boss dash lane length, m (engine-sim roles.ts BOSS.dash.length). */
8
+ const BOSS_DASH = 11;
9
+ /** Lane length for an enemy type's telegraphed lane. */
10
+ export function laneLength(def) {
11
+ if (!def)
12
+ return 6;
13
+ if (def.pattern)
14
+ return BOSS_DASH;
15
+ // An aim line shows the direction, not the whole flight: 5 m is enough to read and dodge.
16
+ if (def.ai === "ranged")
17
+ return Math.min(5, (def.attack?.range ?? ROLE_DEFAULTS.ranged.range) * 1.6);
18
+ const C = ROLE_DEFAULTS.charger;
19
+ return def.speed * (def.attack?.speed ?? C.speed) * C.act + def.radius;
20
+ }
21
+ /**
22
+ * The warning an entity shows now, or null. `kind`, `flags`, `type` and
23
+ * `level` are its snapshot fields; (x, z) its position, (vx, vz) its
24
+ * velocity (shots).
25
+ */
26
+ export function threatOf(enemyTypes, kind, flags, type, level, x, z, vx = 0, vz = 0) {
27
+ if (kind === EntityKind.Projectile) {
28
+ if ((flags & EntityFlag.Hostile) === 0)
29
+ return null;
30
+ if ((flags & EntityFlag.Elite) !== 0) {
31
+ const t = decodeTelegraph(level);
32
+ return { kind: "circle", x, z, r: t.radius, progress: t.progress };
33
+ }
34
+ return { kind: "circle", x: x + vx * 0.3, z: z + vz * 0.3, r: 0.6, progress: 1, shot: true };
35
+ }
36
+ if (kind !== EntityKind.Enemy || level === 0 || (flags & EntityFlag.Dead) !== 0)
37
+ return null;
38
+ const t = decodeTelegraph(level);
39
+ const def = enemyTypes[type];
40
+ if (t.kind === Telegraph.Circle)
41
+ return { kind: "circle", x, z, r: t.radius, progress: t.progress };
42
+ if (t.kind === Telegraph.Aura)
43
+ return { kind: "aura", x, z, r: t.radius, progress: t.progress };
44
+ if (t.kind === Telegraph.Lane) {
45
+ const width = (def?.radius ?? 0.4) + 0.3;
46
+ return { kind: "lane", x, z, r: width, dx: Math.cos(t.angle), dz: Math.sin(t.angle), len: laneLength(def), progress: t.progress };
47
+ }
48
+ return null;
49
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * What a player can see: the ground footprint of a template's camera, as
3
+ * the sim's view radii (engine-sim ViewRules). The spawn director spawns
4
+ * just outside it, straggler recycling measures against it, and the
5
+ * playtest counts enemies inside it as "on screen".
6
+ *
7
+ * The footprint is the camera's screen rectangle projected onto the ground
8
+ * plane: a trapezoid, short toward the camera and long away from it. The
9
+ * union over the screen shapes a template supports (landscape monitors,
10
+ * portrait phones) is what "off screen" must clear.
11
+ */
12
+ /** A fixed top-down follow camera (yaw 0: it sits on +z and looks toward -z). */
13
+ export interface CameraView {
14
+ /** Distance from the focus point, m. */
15
+ readonly distance: number;
16
+ /** Elevation angle above the ground, radians. */
17
+ readonly pitch: number;
18
+ /** Vertical field of view, degrees. */
19
+ readonly fov: number;
20
+ /** Width / height of the screen. */
21
+ readonly aspect: number;
22
+ /** Height of the focus point above the ground, m. */
23
+ readonly focusHeight?: number;
24
+ }
25
+ /**
26
+ * EMBERWATCH's camera (client/scene.ts: distance 17 landscape, 24 portrait,
27
+ * fov 50, pitch 0.98, focus 0.5 m up) on a 16:9 and a 21:9 monitor and a
28
+ * 9:19.5 phone. Change these with the scene's rig. (Round 1 playtest: 20%
29
+ * closer than the first cut's 21 and 30, so the hero reads.)
30
+ */
31
+ export declare const SURVIVOR_VIEWS: readonly CameraView[];
32
+ /**
33
+ * The dungeon crawl's camera (template-dungeon client/scene.ts: distance 13
34
+ * landscape, 18 portrait, fov 50, pitch 1.02): closer than the field's, so
35
+ * a room fills the screen and the next one is out of sight.
36
+ */
37
+ export declare const DUNGEON_VIEWS: readonly CameraView[];
38
+ /** The camera views a template's client draws with (the spawn director and the playtest's "on screen" use them). */
39
+ export declare function viewsFor(template: string): readonly CameraView[];
40
+ /** The four ground corners of the camera's view, relative to the focus point's ground position [x, z]. */
41
+ export declare function footprint(v: CameraView): [number, number][];
42
+ /** View radii (engine-sim ViewRules) for the union of several camera views, at `n` evenly spaced headings. */
43
+ export declare function viewRadii(views: readonly CameraView[], n?: number): number[];
44
+ /** Radius at a heading of a radii table (linear between samples). */
45
+ export declare function radiusAt(radii: readonly number[], a: number): number;
46
+ /** True when (dx, dz) from a player is inside a radii table. */
47
+ export declare function insideView(radii: readonly number[], dx: number, dz: number): boolean;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * What a player can see: the ground footprint of a template's camera, as
3
+ * the sim's view radii (engine-sim ViewRules). The spawn director spawns
4
+ * just outside it, straggler recycling measures against it, and the
5
+ * playtest counts enemies inside it as "on screen".
6
+ *
7
+ * The footprint is the camera's screen rectangle projected onto the ground
8
+ * plane: a trapezoid, short toward the camera and long away from it. The
9
+ * union over the screen shapes a template supports (landscape monitors,
10
+ * portrait phones) is what "off screen" must clear.
11
+ */
12
+ /**
13
+ * EMBERWATCH's camera (client/scene.ts: distance 17 landscape, 24 portrait,
14
+ * fov 50, pitch 0.98, focus 0.5 m up) on a 16:9 and a 21:9 monitor and a
15
+ * 9:19.5 phone. Change these with the scene's rig. (Round 1 playtest: 20%
16
+ * closer than the first cut's 21 and 30, so the hero reads.)
17
+ */
18
+ export const SURVIVOR_VIEWS = [
19
+ { distance: 17, pitch: 0.98, fov: 50, aspect: 16 / 9, focusHeight: 0.5 },
20
+ { distance: 17, pitch: 0.98, fov: 50, aspect: 21 / 9, focusHeight: 0.5 },
21
+ { distance: 24, pitch: 0.98, fov: 50, aspect: 9 / 19.5, focusHeight: 0.5 },
22
+ ];
23
+ /**
24
+ * The dungeon crawl's camera (template-dungeon client/scene.ts: distance 13
25
+ * landscape, 18 portrait, fov 50, pitch 1.02): closer than the field's, so
26
+ * a room fills the screen and the next one is out of sight.
27
+ */
28
+ export const DUNGEON_VIEWS = [
29
+ { distance: 13, pitch: 1.02, fov: 50, aspect: 16 / 9, focusHeight: 0.5 },
30
+ { distance: 13, pitch: 1.02, fov: 50, aspect: 21 / 9, focusHeight: 0.5 },
31
+ { distance: 18, pitch: 1.02, fov: 50, aspect: 9 / 19.5, focusHeight: 0.5 },
32
+ ];
33
+ /** The camera views a template's client draws with (the spawn director and the playtest's "on screen" use them). */
34
+ export function viewsFor(template) {
35
+ return template === "dungeon" ? DUNGEON_VIEWS : SURVIVOR_VIEWS;
36
+ }
37
+ /** Farthest a footprint may reach (the camera can see the horizon on very flat pitches), m. */
38
+ const HORIZON = 80;
39
+ /** The four ground corners of the camera's view, relative to the focus point's ground position [x, z]. */
40
+ export function footprint(v) {
41
+ const h = v.focusHeight ?? 0;
42
+ const s = Math.sin(v.pitch);
43
+ const c = Math.cos(v.pitch);
44
+ const cy = h + s * v.distance;
45
+ const cz = c * v.distance;
46
+ const t = Math.tan(((v.fov / 2) * Math.PI) / 180);
47
+ const out = [];
48
+ for (const [nx, ny] of [
49
+ [-1, 1],
50
+ [1, 1],
51
+ [1, -1],
52
+ [-1, -1],
53
+ ]) {
54
+ // dir = forward + nx * tan * aspect * right + ny * tan * up, with
55
+ // forward (0, -s, -c), right (1, 0, 0), up (0, c, -s).
56
+ const dx = nx * t * v.aspect;
57
+ const dy = -s + ny * t * c;
58
+ const dz = -c - ny * t * s;
59
+ if (dy < -1e-6) {
60
+ const k = cy / -dy;
61
+ const gx = dx * k;
62
+ const gz = cz + dz * k;
63
+ const len = Math.hypot(gx, gz);
64
+ if (len <= HORIZON) {
65
+ out.push([gx, gz]);
66
+ continue;
67
+ }
68
+ }
69
+ // Above the horizon (or very far): clamp along the ray's ground direction.
70
+ const len = Math.hypot(dx, dz) || 1;
71
+ out.push([(dx / len) * HORIZON, cz + (dz / len) * HORIZON]);
72
+ }
73
+ return out;
74
+ }
75
+ /** Distance from the origin to the edge of a convex polygon along heading `a` (radians from +x toward +z). */
76
+ function exitDistance(poly, a) {
77
+ const ux = Math.cos(a);
78
+ const uz = Math.sin(a);
79
+ let best = 0;
80
+ for (let i = 0; i < poly.length; i++) {
81
+ const p = poly[i];
82
+ const q = poly[(i + 1) % poly.length];
83
+ const ex = q[0] - p[0];
84
+ const ez = q[1] - p[1];
85
+ const den = ux * ez - uz * ex;
86
+ if (Math.abs(den) < 1e-9)
87
+ continue;
88
+ const t = (p[0] * ez - p[1] * ex) / den;
89
+ const s = (p[0] * uz - p[1] * ux) / den;
90
+ if (t > 0 && s >= -1e-9 && s <= 1 + 1e-9)
91
+ best = Math.max(best, t);
92
+ }
93
+ return best;
94
+ }
95
+ /** View radii (engine-sim ViewRules) for the union of several camera views, at `n` evenly spaced headings. */
96
+ export function viewRadii(views, n = 32) {
97
+ const polys = views.map(footprint);
98
+ const out = [];
99
+ for (let i = 0; i < n; i++) {
100
+ const a = (i / n) * Math.PI * 2;
101
+ let r = 0;
102
+ for (const p of polys)
103
+ r = Math.max(r, exitDistance(p, a));
104
+ out.push(Math.round(Math.max(4, r) * 100) / 100);
105
+ }
106
+ return out;
107
+ }
108
+ /** Radius at a heading of a radii table (linear between samples). */
109
+ export function radiusAt(radii, a) {
110
+ const n = radii.length;
111
+ let t = (a / (Math.PI * 2)) * n;
112
+ t = ((t % n) + n) % n;
113
+ const i = Math.floor(t);
114
+ const f = t - i;
115
+ const r0 = radii[i % n] ?? 0;
116
+ const r1 = radii[(i + 1) % n] ?? 0;
117
+ return r0 + (r1 - r0) * f;
118
+ }
119
+ /** True when (dx, dz) from a player is inside a radii table. */
120
+ export function insideView(radii, dx, dz) {
121
+ const r = radiusAt(radii, Math.atan2(dz, dx));
122
+ return dx * dx + dz * dz < r * r;
123
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @netforge/engine: the API a game is written against.
3
+ *
4
+ * This entry point is all a generated game may import. It is declarative
5
+ * and typed (Zod-validated): game, prefab, system, earn, store, and the
6
+ * headless test harness. Rendering, input, netcode and physics are not
7
+ * reachable from here; they live in "@netforge/engine/runtime", which
8
+ * templates and the Launcher shell use and game code does not.
9
+ */
10
+ export { dungeon, dungeonSchema } from "./api/dungeon.js";
11
+ export type { DungeonLayout, DungeonRoom, DungeonSpec } from "./api/dungeon.js";
12
+ export { game, prefab, weapon, upgrade, system, director, earn, store, keeper, achievement, mission, title, economy, metaUpgrade, STAKE_REASON, POT_REASON } from "./api/registry.js";
13
+ export type { EdgeSide, GameManifest, PlayerView, SpawnEdgeOptions, SpawnRingOptions, SystemContext, SystemSetup } from "./api/registry.js";
14
+ export { GameDefinitionError, TEMPLATES, STYLES, AI_BEHAVIOURS, WEAPON_KINDS, STAT_KINDS, LOOK_PRESETS, PARTICLE_PRESETS, DECAL_PRESETS, SOUND_CUES, TITLE_STATS, META_STATS, META_MAX_PER_STAT, META_MAX_TOTAL, META_MAX_LEVELS, META_MAX_UPGRADES, META_MIN_GROWTH, STAKE_MAX_MULTIPLE, STAKE_MAX_COINS, } from "./api/schemas.js";
15
+ export type { AssetId, Curve, CurveInput, DirectorDef, DirectorInput, DecalPreset, FeelDef, LookDef, LookInput, LookPreset, ParticlePreset, SoundCue, EarnRuleDef, KeeperDef, KeeperInput, AchievementDef, AchievementInput, MissionDef, MissionInput, TitleDef, TitleInput, TitleStat, EarnRuleInput, EarnStakeDef, EarnStakeInput, EarnPotDef, EarnPotInput, GameDef, GameInput, PrefabDef, PrefabInput, SkuDef, SkuInput, SkuGrant, EconomyDef, EconomyInput, MetaUpgradeDef, MetaUpgradeInput, MetaStat, Style, Template, UpgradeDef, UpgradeInput, WeaponDef, WeaponInput, } from "./api/schemas.js";
16
+ export { towerBoard, tower, waves, TOWER_KINDS, TOWER_STATS } from "./api/tower.js";
17
+ export { saltFlat, plant, PLANT_KINDS, IDLE_STATS, IDLE_MIN_SHIFT, IDLE_MAX_TEND } from "./api/idle.js";
18
+ export type { SaltFlatDef, SaltFlatInput, PlantDef, PlantInput } from "./api/idle.js";
19
+ export { marsh, creature, move, ELEMENTS, MOVE_EFFECTS, COLLECTOR_STATS } from "./api/collector.js";
20
+ export type { MarshDef, MarshInput, CreatureDef, CreatureInput, MoveDef, MoveInput, Element } from "./api/collector.js";
21
+ export type { TowerBoardDef, TowerBoardInput, TowerDef, TowerInput, WavesDef, WavesInput } from "./api/tower.js";
22
+ export { card, cardTable, CARD_STATS, EFFECT_KINDS, EFFECT_TARGETS } from "./api/cards.js";
23
+ export type { CardDef, CardInput, CardTableDef, CardTableInput } from "./api/cards.js";
24
+ export { APPEARANCE_PARTS, appearanceSchema } from "./api/appearance.js";
25
+ export type { AppearanceDef, AppearanceInput, AppearancePart } from "./api/appearance.js";
26
+ export type { FunMetrics, HarnessAssertion, HarnessOptions, HarnessReport } from "./api/harness.js";
27
+ import { runHarness } from "./api/harness.js";
28
+ /** `engine.test.run({ ticks, bots, assert })`: the headless playtest (§12). */
29
+ export declare const engine: {
30
+ readonly test: {
31
+ readonly run: typeof runHarness;
32
+ };
33
+ };
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @netforge/engine: the API a game is written against.
3
+ *
4
+ * This entry point is all a generated game may import. It is declarative
5
+ * and typed (Zod-validated): game, prefab, system, earn, store, and the
6
+ * headless test harness. Rendering, input, netcode and physics are not
7
+ * reachable from here; they live in "@netforge/engine/runtime", which
8
+ * templates and the Launcher shell use and game code does not.
9
+ */
10
+ export { dungeon, dungeonSchema } from "./api/dungeon.js";
11
+ export { game, prefab, weapon, upgrade, system, director, earn, store, keeper, achievement, mission, title, economy, metaUpgrade, STAKE_REASON, POT_REASON } from "./api/registry.js";
12
+ export { GameDefinitionError, TEMPLATES, STYLES, AI_BEHAVIOURS, WEAPON_KINDS, STAT_KINDS, LOOK_PRESETS, PARTICLE_PRESETS, DECAL_PRESETS, SOUND_CUES, TITLE_STATS, META_STATS, META_MAX_PER_STAT, META_MAX_TOTAL, META_MAX_LEVELS, META_MAX_UPGRADES, META_MIN_GROWTH, STAKE_MAX_MULTIPLE, STAKE_MAX_COINS, } from "./api/schemas.js";
13
+ // Board genres (rules/): each template's own declarations.
14
+ export { towerBoard, tower, waves, TOWER_KINDS, TOWER_STATS } from "./api/tower.js";
15
+ export { saltFlat, plant, PLANT_KINDS, IDLE_STATS, IDLE_MIN_SHIFT, IDLE_MAX_TEND } from "./api/idle.js";
16
+ export { marsh, creature, move, ELEMENTS, MOVE_EFFECTS, COLLECTOR_STATS } from "./api/collector.js";
17
+ export { card, cardTable, CARD_STATS, EFFECT_KINDS, EFFECT_TARGETS } from "./api/cards.js";
18
+ // Asset library: model appearances.
19
+ export { APPEARANCE_PARTS, appearanceSchema } from "./api/appearance.js";
20
+ import { runHarness } from "./api/harness.js";
21
+ /** `engine.test.run({ ticks, bots, assert })`: the headless playtest (§12). */
22
+ export const engine = {
23
+ test: { run: runHarness },
24
+ };
@@ -0,0 +1,33 @@
1
+ export declare class BitWriter {
2
+ private buf;
3
+ private bitPos;
4
+ constructor(initialBytes?: number);
5
+ /** Writes the low `bits` bits of `value` (0 <= bits <= 32). */
6
+ writeBits(value: number, bits: number): void;
7
+ writeBool(b: boolean): void;
8
+ /** Signed integer in `bits` bits using zigzag encoding. */
9
+ writeSigned(value: number, bits: number): void;
10
+ /**
11
+ * Unsigned varint in 4-bit groups with a continuation bit (5 bits per
12
+ * group). Small counts (entity counts, id gaps) cost 5 bits, not 16.
13
+ */
14
+ writeVarUint(value: number): void;
15
+ get bitLength(): number;
16
+ finish(): Uint8Array;
17
+ private ensure;
18
+ }
19
+ export declare class BitReader {
20
+ private readonly buf;
21
+ private bitPos;
22
+ private readonly totalBits;
23
+ constructor(buf: Uint8Array);
24
+ readBits(bits: number): number;
25
+ readBool(): boolean;
26
+ readSigned(bits: number): number;
27
+ readVarUint(): number;
28
+ get remainingBits(): number;
29
+ }
30
+ export declare function zigzag(n: number): number;
31
+ export declare function unzigzag(z: number): number;
32
+ /** Bits needed for a zigzagged signed value of magnitude up to `maxAbs`. */
33
+ export declare function bitsForSigned(maxAbs: number): number;
@@ -0,0 +1,135 @@
1
+ // Bit-level writer and reader for snapshots and input packets.
2
+ //
3
+ // Values are written LSB-first into a growable byte buffer. Every field in a
4
+ // snapshot is a fixed or schema-declared bit width, so nothing here allocates
5
+ // per field; the buffer doubles when it runs out.
6
+ export class BitWriter {
7
+ buf;
8
+ bitPos = 0;
9
+ constructor(initialBytes = 256) {
10
+ this.buf = new Uint8Array(initialBytes);
11
+ }
12
+ /** Writes the low `bits` bits of `value` (0 <= bits <= 32). */
13
+ writeBits(value, bits) {
14
+ if (bits <= 0)
15
+ return;
16
+ if (bits > 32)
17
+ throw new RangeError(`writeBits: ${bits} > 32`);
18
+ this.ensure(bits);
19
+ let v = bits === 32 ? value >>> 0 : (value >>> 0) % 2 ** bits;
20
+ let remaining = bits;
21
+ while (remaining > 0) {
22
+ const byteIndex = this.bitPos >>> 3;
23
+ const bitOffset = this.bitPos & 7;
24
+ const room = 8 - bitOffset;
25
+ const take = remaining < room ? remaining : room;
26
+ const chunk = v & ((1 << take) - 1);
27
+ this.buf[byteIndex] = (this.buf[byteIndex] ?? 0) | (chunk << bitOffset);
28
+ v = Math.floor(v / (1 << take));
29
+ remaining -= take;
30
+ this.bitPos += take;
31
+ }
32
+ }
33
+ writeBool(b) {
34
+ this.writeBits(b ? 1 : 0, 1);
35
+ }
36
+ /** Signed integer in `bits` bits using zigzag encoding. */
37
+ writeSigned(value, bits) {
38
+ this.writeBits(zigzag(value), bits);
39
+ }
40
+ /**
41
+ * Unsigned varint in 4-bit groups with a continuation bit (5 bits per
42
+ * group). Small counts (entity counts, id gaps) cost 5 bits, not 16.
43
+ */
44
+ writeVarUint(value) {
45
+ let v = value >>> 0;
46
+ do {
47
+ const group = v & 0xf;
48
+ v >>>= 4;
49
+ this.writeBits(group, 4);
50
+ this.writeBool(v !== 0);
51
+ } while (v !== 0);
52
+ }
53
+ get bitLength() {
54
+ return this.bitPos;
55
+ }
56
+ finish() {
57
+ return this.buf.slice(0, (this.bitPos + 7) >>> 3);
58
+ }
59
+ ensure(bits) {
60
+ const need = (this.bitPos + bits + 7) >>> 3;
61
+ if (need <= this.buf.length)
62
+ return;
63
+ let size = this.buf.length * 2;
64
+ while (size < need)
65
+ size *= 2;
66
+ const next = new Uint8Array(size);
67
+ next.set(this.buf);
68
+ this.buf = next;
69
+ }
70
+ }
71
+ export class BitReader {
72
+ buf;
73
+ bitPos = 0;
74
+ totalBits;
75
+ constructor(buf) {
76
+ this.buf = buf;
77
+ this.totalBits = buf.length * 8;
78
+ }
79
+ readBits(bits) {
80
+ if (bits <= 0)
81
+ return 0;
82
+ if (bits > 32)
83
+ throw new RangeError(`readBits: ${bits} > 32`);
84
+ if (this.bitPos + bits > this.totalBits)
85
+ throw new RangeError('readBits: past end of buffer');
86
+ let out = 0;
87
+ let shift = 0;
88
+ let remaining = bits;
89
+ while (remaining > 0) {
90
+ const byteIndex = this.bitPos >>> 3;
91
+ const bitOffset = this.bitPos & 7;
92
+ const room = 8 - bitOffset;
93
+ const take = remaining < room ? remaining : room;
94
+ const byte = this.buf[byteIndex] ?? 0;
95
+ const chunk = (byte >>> bitOffset) & ((1 << take) - 1);
96
+ out += chunk * 2 ** shift;
97
+ shift += take;
98
+ remaining -= take;
99
+ this.bitPos += take;
100
+ }
101
+ return out;
102
+ }
103
+ readBool() {
104
+ return this.readBits(1) === 1;
105
+ }
106
+ readSigned(bits) {
107
+ return unzigzag(this.readBits(bits));
108
+ }
109
+ readVarUint() {
110
+ let out = 0;
111
+ let shift = 0;
112
+ for (;;) {
113
+ const group = this.readBits(4);
114
+ out += group * 2 ** shift;
115
+ shift += 4;
116
+ if (!this.readBool())
117
+ return out;
118
+ if (shift > 32)
119
+ throw new RangeError('readVarUint: too long');
120
+ }
121
+ }
122
+ get remainingBits() {
123
+ return this.totalBits - this.bitPos;
124
+ }
125
+ }
126
+ export function zigzag(n) {
127
+ return n >= 0 ? n * 2 : -n * 2 - 1;
128
+ }
129
+ export function unzigzag(z) {
130
+ return z % 2 === 0 ? z / 2 : -(z + 1) / 2;
131
+ }
132
+ /** Bits needed for a zigzagged signed value of magnitude up to `maxAbs`. */
133
+ export function bitsForSigned(maxAbs) {
134
+ return Math.max(1, Math.ceil(Math.log2(maxAbs * 2 + 2)));
135
+ }
@@ -0,0 +1,75 @@
1
+ import { type InterpolatedEntity } from './interpolation.js';
2
+ import { type EntityState } from './schema.js';
3
+ import type { PlayerId, SimAdapter } from './sim-adapter.js';
4
+ export interface NetClientOptions {
5
+ /** How far behind the server remote entities are drawn. Spec: ~100 ms. */
6
+ readonly interpDelayMs: number;
7
+ /** Server input-queue depth the client clock steers toward (one-input-per-tick sims). */
8
+ readonly inputBufferTarget?: number;
9
+ /**
10
+ * First input seq. A client rebuilt after a room rebase continues the old
11
+ * client's numbering (`nextSeq`), because the server's input queue ignores
12
+ * seqs it has already seen.
13
+ */
14
+ readonly firstSeq?: number;
15
+ }
16
+ export declare const DEFAULT_CLIENT: NetClientOptions;
17
+ export interface ClientStats {
18
+ snapshots: number;
19
+ snapshotBytes: number;
20
+ inputPackets: number;
21
+ inputBytes: number;
22
+ baselineMisses: number;
23
+ staleSnapshots: number;
24
+ }
25
+ export declare class NetClient<W, I> {
26
+ private readonly sim;
27
+ readonly player: PlayerId;
28
+ readonly selfEntity: number;
29
+ private readonly send;
30
+ private readonly opts;
31
+ private readonly history;
32
+ private readonly interp;
33
+ private readonly clock;
34
+ private readonly predictor;
35
+ private latestTick;
36
+ private latestSelf;
37
+ private lastRenderMs;
38
+ private readonly tickMs;
39
+ /** Smoothed server input-queue depth from snapshot headers; null until reported. */
40
+ private serverBuffer;
41
+ readonly stats: ClientStats;
42
+ constructor(sim: SimAdapter<W, I>, player: PlayerId, selfEntity: number, send: (bytes: Uint8Array) => void, opts?: NetClientOptions);
43
+ /** The seq the next input gets; pass as `firstSeq` to a client that replaces this one. */
44
+ get nextSeq(): number;
45
+ receiveSnapshot(bytes: Uint8Array, nowMs: number): void;
46
+ /** Call once per sim tick with the sampled input. Sends the input packet. */
47
+ tick(input: I, nowMs: number): void;
48
+ /**
49
+ * Milliseconds until the next input tick. Equal to the sim tick when the
50
+ * server reports no input queue; otherwise up to 8% shorter when the
51
+ * server's queue for us runs below target (we are behind, so input would
52
+ * starve) and up to 8% longer when it runs above (added latency). This is
53
+ * the client clock adjustment for one-input-per-tick sims.
54
+ */
55
+ tickIntervalMs(): number;
56
+ /** Smoothed input-queue depth the server last reported, or null. */
57
+ get serverInputBuffer(): number | null;
58
+ /** Server tick (fractional) that remote entities are drawn at. */
59
+ renderTick(nowMs: number): number;
60
+ render(nowMs: number): {
61
+ self: EntityState | undefined;
62
+ others: InterpolatedEntity[];
63
+ };
64
+ /** The newest server state of our own entity (what prediction is ahead of). */
65
+ authoritativeSelf(): EntityState | undefined;
66
+ /** Our own entity drawn like a remote one (what you'd see with no prediction). */
67
+ interpolatedSelf(nowMs: number): InterpolatedEntity | undefined;
68
+ /** Exact predicted state (no smoothing). */
69
+ predicted(): EntityState | undefined;
70
+ get ready(): boolean;
71
+ get corrections(): number;
72
+ get lastCorrection(): number;
73
+ get pendingInputs(): number;
74
+ private toState;
75
+ }