@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.
Files changed (244) hide show
  1. package/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +147 -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 +208 -0
  76. package/dist/net/protocol.js +8 -0
  77. package/dist/net/room-client.d.ts +57 -0
  78. package/dist/net/room-client.js +144 -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.txt +183 -0
  244. package/package.json +82 -3
package/GAME-DESIGN.md ADDED
@@ -0,0 +1,195 @@
1
+ # Game design playbook
2
+
3
+ Rules every game on this engine follows. They come from measured play: the
4
+ first survivor build killed every player in 12 seconds, and a fun one kept
5
+ the same bot alive for minutes. The playtest gate measures most of them, and
6
+ a game that breaks them scores low.
7
+
8
+ ## Every genre
9
+
10
+ 1. **A readable opening, with real danger by 0:30.** The first threats
11
+ reach the player by about 0:20, and health moves by 0:30 to 0:45. The
12
+ first elite comes at about 0:45 and a mini-boss by 1:30 to 2:00. Nobody
13
+ dies in the first minute. (The first cut's playtest found no danger for
14
+ three minutes and scored 4/10. A quiet opening is not an empty one.)
15
+ 2. **Reward fast.** The first reward (a level, a kill, a pickup) comes within
16
+ about 10 seconds, then every 6 to 10 seconds for the first minute.
17
+ 3. **Never spawn on screen.** Enemies arrive from just off screen:
18
+ `director(...)` or `spawnEdge(prefab, { count })`, never `spawn()` or
19
+ `spawnRing()` at short radii. Pressure follows a moving player, because
20
+ stragglers far behind are recycled ahead of them.
21
+ 4. **Hits are survivable.** Contact damage gives i-frames
22
+ (`players.hitIframes`, 0.5 s). Twenty enemies touching you means one hit,
23
+ not twenty.
24
+ 5. **Slower than the player unless telegraphed.** Players run at 7 m/s.
25
+ Fodder moves at 45 to 60% of that, and fast runners up to 85% but fragile.
26
+ Anything faster or harder-hitting shows a telegraph first.
27
+ 6. **Telegraph every big hit.** Charges, slams, blasts and boss patterns
28
+ show a ground marker for 0.5 to 1 s, using shape and fill and not only
29
+ red. Over half of late deaths should be telegraphed ones, which means the
30
+ player misread the threat rather than the game being unfair.
31
+ 7. **Death is not a waiting room.** A downed player bleeds for about 10 s
32
+ and can crawl toward help. Each player gets one free self-revive per run.
33
+ A player who is out comes back beside a teammate within seconds
34
+ (`players.downed`), never "out until the next wave".
35
+ 8. **A run has a timeline.** It plays like a story with moments: new enemy
36
+ roles join one at a time, then elites, a mini-boss, an event, a flood, and
37
+ a final boss. Announce each moment so players always know what just
38
+ happened and where to go.
39
+ 9. **Bosses are moments.** A boss has a silhouette no mob shares and one or
40
+ two telegraphed attacks to learn. The first fight is short (about 45 s
41
+ solo), and the reward pops: a hoard chest that brings an evolution onto
42
+ the cards (`pickup: { effect: "chest", evolve: true }`).
43
+ 10. **Tougher, not more.** Past the crowd cap, the director makes new enemies
44
+ tougher instead of adding bodies. A boss alive halves the spawn rate, and
45
+ bosses scale with players present.
46
+ 11. **Builds have a goal.** Upgrades have numbers players can read ("+1 bolt",
47
+ "-20% cooldown"). Each weapon pairs with a passive toward an evolution.
48
+ Solo cards pause the game. Co-op cards do not pause, but they shove
49
+ enemies back.
50
+ 12. **Measure fun with bots.** The gate's human-like bot reacts 200 ms late,
51
+ kites, and reads cards for a second. Fix what it reports before
52
+ shipping.
53
+
54
+ ## Horde (survivor-like) targets
55
+
56
+ | Metric | Target |
57
+ |---|---|
58
+ | First level | 4-10 s |
59
+ | First damage taken | 15-45 s |
60
+ | First elite / first mini-boss | ~0:45 / 1:30-2:00 |
61
+ | First mini-boss fight, solo | ~45 s |
62
+ | Level at 1:00 / 3:00 / 6:00 | ~5 / ~10 / ~15 |
63
+ | Deaths before 1:00 | 0 |
64
+ | Enemies on screen at 0:30 | 30-50 (wave-3 density in minute 1) |
65
+ | Touches taken in minute 1 | 1-3 |
66
+ | Spawns on screen | 0% |
67
+ | Time spent downed or out | under 10% |
68
+ | Solo survival, random picks | 4-7 min |
69
+
70
+ A good curve is `director({ alive: [[0, 20], [30, 55], [60, 100], [180, 220], [300, 560], [540, 1900]], rate: [[0, 2], [30, 4], [60, 5], [180, 8], [300, 16], [540, 64]], speed: [[0, 1], [240, 1], [600, 1.6]], elites: { from: 45, every: [40, 55] } })`.
71
+ Health and damage curves grow roughly ×1.35 per minute early and flatten
72
+ later. Early gems are worth a little less (`xp` 0.7 at first), so the dense
73
+ first minute does not run the levels hot. Late `speed` keeps endless
74
+ kiting from working. The 2,000-enemy crowd is a late-run payoff, not an
75
+ opening wall.
76
+
77
+ ## Arena shooter
78
+
79
+ 1. **First kill or pickup within about 20 s.** Spawns should be close enough
80
+ to find a fight, and pickups worth walking to.
81
+ 2. **Spawn safety.** No damage in the first 2 s after spawning, and under a
82
+ quarter of deaths within 3 s of spawning. Spawn points sit out of sight
83
+ lines, away from the middle.
84
+ 3. **Contest the map.** Heals, power weapons and high ground make places
85
+ worth fighting over. Put them in the open between spawns.
86
+ 4. **Readable hits.** Hits flash, kills pop, the feed names who killed whom,
87
+ and your own shot shows instantly on your screen, before the server
88
+ agrees.
89
+ 5. **Moments.** Announce streaks, put a bounty on the leader, and run a
90
+ late-round event, so the lead changes hands. When one player has 3x the
91
+ average kills, a weapon or a position dominates.
92
+ 6. **Bots that ramp.** Seat bots fill rooms, play below a new player's level
93
+ at first and get sharper as people win. They never stomp a newcomer.
94
+ They never earn, and they are never a coin source: killing a bot scores
95
+ but pays no earn rule, and "round.top" pays only with another person in
96
+ the round. Earn comes from beating people.
97
+
98
+ ## How a change request maps to the API
99
+
100
+ - **"Harder"**: raise the director's `alive`, `rate`, `health` or `damage`
101
+ keyframes late in the run, never at 0 s.
102
+ - **"Easier"**: lower them, raise `players.hitIframes` or `magnet`, or give
103
+ the starting weapon more range.
104
+ - **"More enemy types"**: add a prefab and a pool entry with `from: <seconds>`,
105
+ one new idea at a time. Use `group: [8, 12]` for streams.
106
+ - **"Add a boss"**: add a prefab with `boss: { attacks: [...], rank }` and
107
+ spawn it with `spawnEdge(id, { count: 1 })` from `after(seconds, ...)`.
108
+ Give mini-bosses 3 attacks and the final boss 4. Its health scales with
109
+ players present.
110
+ - **"Smarter" or "trickier enemies"**: give prefabs roles (`ai: "charger"`,
111
+ `"ranged"`, `"bomber"`, `"shielder"`, `"summoner"`), or add elites.
112
+ - **"Add events" or "waves"**: use `every` or `after` in a system, with
113
+ `spawnEdge(id, { count, side: "north" })` for a tide from one side,
114
+ `xpBoost`, `surge` or a fleeing carrier with a chest. `announce` each one,
115
+ never repeat the same event twice in a row, and skip events while
116
+ `bossAlive()`.
117
+ - **"More loot" or "a map worth exploring"**: special pickups (heal,
118
+ vacuum, bomb, chest) as rare drops, props to `scatter`, and a bigger `field`.
119
+ - **"Longer or shorter run"**: move the boss's `after(...)` and stretch the
120
+ curves' keyframe times.
121
+ - **"More weapons" or "better builds"**: declare 5-8 `levels` per weapon
122
+ (each a readable change), pair weapons with passives through `evolve`, and
123
+ give evolutions a new shape. Keep weapons + evolutions within 15.
124
+ - **"Add a character" or "more heroes"**: `keeper(id, { weapon, passive,
125
+ unlock })`, unlocked by an `achievement`, or declared without an unlock
126
+ and sold (`store.sku(id, { coins, grants: { keeper: id } })`): earned or
127
+ bought, never both. A keeper is a different way to play, never a stronger
128
+ one, and the first keeper is always free.
129
+ - **"Reasons to come back"**: `achievement`s that unlock keepers,
130
+ `mission`s for daily goals, `title`s for the end screen. None pay coins;
131
+ coin rewards are earn rules, which are an economy decision. The room
132
+ server grants them from the run and saves them per player, so they follow
133
+ the player to every device.
134
+ - **"Sell character unlocks"**: a SKU per keeper with `grants: { keeper }`,
135
+ on keepers declared without an `unlock`.
136
+ - **"Cosmetics only" or "no pay to win"**: `economy({ store: "cosmetics" })`
137
+ and only SKUs with `grants: { cosmetic: n }`.
138
+ - **"Let players buy permanent upgrades with coins"**:
139
+ `economy({ metaUpgrades: true })` and a few `metaUpgrade(...)`s (below).
140
+ - **"Better co-op"**: `players.share` ("nearby" is the default), a
141
+ `reviveBuff`, `lateJoinBehind`, and `upcoming(...)` countdowns everyone
142
+ sees.
143
+
144
+ ## The recommended coin model, and why
145
+
146
+ Every game has coins (1 coin = 1 of the game's own token). How they are
147
+ spent decides whether the game stays fair to the player who never pays, and
148
+ whether a token holder can buy a win. The engine ships one recommended
149
+ model and makes the alternatives declarations, so a
150
+ change request is a one-line change, never new code.
151
+
152
+ **The default (no declaration; every template uses it).** Coins buy:
153
+
154
+ - **Cosmetics**: a look owned for good,
155
+ `store.sku("skin_aurora", { coins: 300, grants: { cosmetic: 1 } })`.
156
+ - **Unlocks**: a keeper (a character, a starting loadout, a sidegrade)
157
+ declared without an `unlock`,
158
+ `store.sku("unlock_tinker", { coins: 400, grants: { keeper: "tinker" } })`.
159
+ An unlock is a different way to play at the same power as the keepers
160
+ anyone can earn by playing. A keeper is earned (by an achievement) or
161
+ bought, never both, so a sold keeper cannot be opened another way.
162
+ - **In-run consumables**: used once, inside one run,
163
+ `store.sku("revive", { coins: 50 })` (no `grants`).
164
+
165
+ Coins never buy permanent stat power by default. A run is decided by how it
166
+ was played, and a newcomer next to a long-time holder plays the same game.
167
+
168
+ **Cosmetics only (opt-in):** `economy({ store: "cosmetics" })`. The build
169
+ refuses any SKU that is not a look.
170
+
171
+ **Meta upgrades (opt-in, never the default):** permanent stat upgrades
172
+ bought with coins, the way many survivor-likes do between runs.
173
+
174
+ ```ts
175
+ economy({ metaUpgrades: true });
176
+ metaUpgrade("hearth", { stat: "maxHealth", step: 0.05, levels: 3, cost: 120, growth: 2 }); // +5% a level: 120, 240, 480
177
+ metaUpgrade("stride", { stat: "speed", step: 0.03, levels: 3, cost: 100, growth: 1.8 });
178
+ ```
179
+
180
+ The engine enforces hard caps that no game can raise: +15% to any one stat
181
+ from meta upgrades, +30% across all of them, at most 5 levels each and 8 per
182
+ game, and geometric costs (each level at least 1.5 times the last). The
183
+ room server applies a player's levels at the start of every run, capped
184
+ again, and the store shows each level's bonus and price. Use them only when
185
+ the creator asks for permanent upgrades, and keep steps small (3-5%).
186
+
187
+ **Before launch** coins are play money, and play money buys only in-run
188
+ items. Looks, unlocks and meta levels need real coins, so they open at
189
+ launch; nothing bought with play money persists or carries over.
190
+
191
+ **Where it is enforced.** Prices come from the manifest, never the client.
192
+ A purchase is a coin spend (earned coins first, then the wallet) that the
193
+ room server checks against that price before the item is owned, and the
194
+ item is saved in the player's profile on the server. Spent coins recycle
195
+ to the reward pool for other players, as every coin spend does.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NetNet Capital Management
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,30 @@
1
+ @netforge/engine
2
+ Copyright (c) 2026 NetNet Capital Management
3
+
4
+ This package depends on, or takes patterns from, the following works.
5
+ No source was copied from copyleft (GPL/AGPL) projects. The demo uses
6
+ only generated primitives; no third-party art, models, sounds or fonts.
7
+
8
+ Dependencies (unmodified, via npm)
9
+ - three.js 0.186.1 (https://github.com/mrdoob/three.js), MIT. Includes the
10
+ GLTFLoader, KTX2Loader and meshopt decoder addons it ships.
11
+ - meshoptimizer decoder (bundled with three.js addons), MIT, Arseny Kapoulkine.
12
+ - Basis Universal transcoder (bundled with three.js addons), Apache-2.0, Binomial LLC.
13
+ - nipplejs 1.0.4 (https://github.com/yoannmoinet/nipplejs), MIT.
14
+ - zod 4.4.3 (https://github.com/colinhacks/zod), MIT.
15
+ - @colyseus/sdk 0.18.5 (https://github.com/colyseus/colyseus), MIT, Endel Dreyer.
16
+ Used by src/net/room-client.ts.
17
+ - @netforge/engine-sim (this repository); see its NOTICE for bitECS
18
+ (MPL-2.0) and Rapier (Apache-2.0).
19
+
20
+ Patterns
21
+ - src/net: client prediction and server
22
+ reconciliation after Gabriel Gambetta's articles; snapshot interpolation
23
+ and delta compression after Valve's Source networking write-ups, Glenn
24
+ Fiedler's snapshot compression articles and the Quake 3 model (GPL,
25
+ ideas only, no code); clock sync for one-input-per-tick play after the
26
+ Overwatch GDC netcode talk (idea only).
27
+ - Render interpolation between fixed ticks after Glenn Fiedler, "Fix Your
28
+ Timestep!"; idea only.
29
+ - Writing instance matrices directly into InstancedMesh.instanceMatrix
30
+ follows the three.js instancing examples (MIT).
package/README.md CHANGED
@@ -1,3 +1,148 @@
1
- # Temporary Holding Version
1
+ # @netforge/engine
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The client side of the Net Forge game engine.
4
+
5
+ Two entry points, on purpose:
6
+
7
+ | Import | Who uses it | What it holds |
8
+ |---|---|---|
9
+ | `@netforge/engine` | generated game code (the model) | `game`, `prefab`, `system`, `earn`, `store`, `engine.test.run`: declarative, Zod-validated, no DOM, no renderer, no netcode |
10
+ | `@netforge/engine/runtime` | templates, the Net Forge shell, the demo | three.js renderer (WebGPU, WebGL2 fallback), `InstancedCrowd`, `CameraRig`, `InputController` (keyboard/mouse, nipplejs touch), `AssetLoader` (glTF + meshopt + KTX2), `ArenaClient`, `SystemScheduler`, `collectManifest` |
11
+
12
+ A game module never reaches the renderer, netcode or physics.
13
+ Bad declarations throw `GameDefinitionError` with one actionable line per
14
+ issue, e.g. `model: reference assets by library id (e.g. "style.lowpoly.bat"), never by URL or path`.
15
+
16
+ ## Status (Template 1)
17
+
18
+ - Declarations, validation, manifest cross-checks and the tick-based
19
+ `SystemScheduler`, plus `weapon()`, `upgrade()`, pickup prefabs, bosses,
20
+ and systems' `alive()`, `on(event)`, `respawnDead()`, `end(outcome)`.
21
+ - `@netforge/engine/game` runs a declared game on engine-sim:
22
+ `compileGame` (manifest to sim content), `GameRunner` (systems on a run
23
+ clock, game events, run phases), `EarnTracker`, the engine-sim
24
+ `SimAdapter` (`engineSimAdapter`, wire schema `engineSchema`) and a
25
+ playtest bot. `engine.test.run` plays the declared game when there is one
26
+ (kills, waves, levels, earn by rule, softlock, ms/tick).
27
+ - `@netforge/engine/net` is the client half of the netcode (the server
28
+ half runs in the hosted Net Forge room server): prediction and reconciliation, interpolation, delta
29
+ snapshots with per-kind fields, input packets, client clock adjustment
30
+ for one-input-per-tick sims, the wire protocol and the Colyseus room client.
31
+ - `AssetLoader` is built but no template uses it yet (Template 1 art is
32
+ procedural, see packages/template-survivor/client/library.ts).
33
+
34
+ ## Status (Template 2, the arena shooter)
35
+
36
+ - `game({ template: "arena", arena, round, players: { loadout, bots,
37
+ respawn, drop } })`, weapon kinds `blaster` (with `spread`) and `beam`,
38
+ and `emit(event, players?)` for crediting chosen players.
39
+ - `compileGame` builds the yard (floor, pillars, spawns) and the arena
40
+ rules; it refuses survivor weapons in an arena and arena weapons in a
41
+ survivor game, with a line the model can act on.
42
+ - `GameRunner` adds the round clock and frag limit (a round is a run),
43
+ `opponent.killed` / `player.died` / `round.top` / `round.finished`
44
+ events, kills and deaths per player, a kill feed in `RunInfo`, and seat
45
+ bots (`players.bots`) that fill empty seats while a human is present and
46
+ never earn. Killing a bot scores and shows in the feed but pays no earn
47
+ rule, and `round.top` pays only when another person was in the round
48
+ (both arrive as unpaid events, and so does anything a system emits while
49
+ handling one). Bots play through `arenaBotInput` (game/arena-bot.ts), a pure
50
+ function of the present, so a room's run replays exactly.
51
+ - `engineSimAdapter` gives arena games the lag-compensation hooks the room
52
+ server's core already calls (`hitscans`, `hitboxes`, `applyHit`), runs
53
+ the client without authority, and grants store upgrades by name.
54
+ - `engine.test.run` plays arena games with the bots and reports kills,
55
+ deaths, rounds, `topScore` and `scorers`.
56
+ - The registry seals a game once its manifest is collected, so one process
57
+ (the room server) can load several game modules in turn.
58
+
59
+ ## Board genres (`@netforge/engine/rules`)
60
+
61
+ Tower defense, idle, card battler and monster collector do not run on
62
+ engine-sim's action world. Each is a deterministic rules module (integer
63
+ state, a seeded PRNG word, no clocks) behind the same `SimAdapter` seam,
64
+ so the room server, the netcode, the referee, run verification, the store,
65
+ the stake and losable gear are the ones every template uses.
66
+
67
+ - `RulesGenre<S>` (rules/kit.ts): `create`, `join`, `act` (a player's
68
+ validated action), `step` (one tick), `outcome`, `view(state, player)`
69
+ (what that player may see: hidden hands stay hidden), `entities` (moving
70
+ things, interpolated on the client) and the genre's own bot.
71
+ - `rulesAdapter(genre, manifest)`: earn rules per player per run
72
+ (`EarnTracker`), seat bots that never earn, unpaid events, the late-start rule
73
+ (world progress never pays a run that started after the world began),
74
+ store upgrades and gear as recorded room commands capped at maxLevel,
75
+ cosmetics, the end screen and `run.reset`. Each player's avatar (a cursor,
76
+ a builder, a walking hero) is predicted from its own input.
77
+ - `SimAdapter.infoFor(world, player)`: run state sent to one player only
78
+ (the room sends it when it changed, every `infoEveryTicks`).
79
+ - Declarations: each genre adds its own typed functions to this package's
80
+ root (tower defense: `towerBoard`, `tower`, `waves`), validated by its own
81
+ Zod schemas and stored with `declareContent`. `engine.test.run` plays a
82
+ board genre through `playRules` and reports its own checks under
83
+ `rules.checks`.
84
+
85
+ ## Game feel kit (`runtime/feel`)
86
+
87
+ One-line components every template uses. `FeelKit` wraps them:
88
+ `hit`, `kill`, `hurt`, `levelUp`, `land`, `dash` and `footstep`, with the
89
+ presets `subtle`, `default` and `heavy`. The parts work alone too:
90
+
91
+ - `Trauma`: seeded-noise trauma shake.
92
+ - `FeelClock`: hit-stop and slow motion with a smooth catch-up; the netcode never pauses.
93
+ - `HitFlash` and `KnockbackOffsets`: flash and spring squash per entity id.
94
+ - `Spring` and `Spring3`.
95
+ - `DamageNumbers`: pooled, and merges rapid hits on one id.
96
+ - `Tweens` and `Ease`, with `pop`, `shakeEl` and `countUp` for UI.
97
+ - `FeelAudio`: procedural cues with throttles, voice limits, pan and distance falloff; `addBuffer` takes CC0 samples later. `Footsteps` alongside it.
98
+ - `Rumble`: Gamepad dual-rumble, with a `navigator.vibrate` fallback.
99
+
100
+ `CameraRig` adds `addTrauma`, `punch` (an FOV kick), a smoothed and
101
+ clamped lead and a dead zone. Its old `shake(amplitude, seconds)` still
102
+ works.
103
+
104
+ ## Render pipeline (`runtime/render`)
105
+
106
+ `createRenderStack(handle, scene, camera, { preset, quality, bloom, outline })`
107
+ returns the whole pipeline. Call `stack.frame(dt, focus)` in place of
108
+ `renderer.render(scene, camera)`. It includes:
109
+
110
+ - `LightingRig`: hemisphere light plus a sun whose shadow follows the focus, texel-snapped.
111
+ - `PostFX`: bloom, colour grade, vignette and hurt pulse, tone mapping applied once, FXAA, and an optional depth outline.
112
+ - `Atmosphere`: fog and a sky gradient.
113
+ - `QualityManager`: tiers and dynamic resolution.
114
+ - `GpuParticles`: one draw, with motion on the GPU.
115
+ - `GroundDecals` and `GroundRings`.
116
+
117
+ It runs on WebGPU and on WebGL2. Animated characters:
118
+
119
+ - `AnimatedCrowd`: procedural vertex animation for instanced crowds.
120
+ - `VatCrowd` and `bakeVertexAnimation`: vertex-animation textures for skinned crowds.
121
+ - `AnimatedCharacter`: glTF skinned meshes on an `AnimationMixer` with locomotion blending.
122
+ - `buildSegmentRig`: a procedural rig for tests and demos.
123
+
124
+ The demo shows all of it with `?fx=1` (`&outline=1`, `&crowd=vat`,
125
+ `&preset=dusk`, `&quality=low`).
126
+
127
+ Game code declares the look and feel it wants: `game({ look })`,
128
+ `prefab({ feel })` and `weapon({ feel })`, validated by `lookSchema` and
129
+ `feelSchema`. See `llms.txt`.
130
+
131
+ ## Demo
132
+
133
+ ```sh
134
+ pnpm --filter @netforge/engine-sim build
135
+ pnpm --filter @netforge/engine demo # http://localhost:5310
136
+ # ?enemies=2000 ?webgl=1 (force the WebGL2 backend) ?autopilot=1 (scripted input)
137
+ ```
138
+
139
+ Measure in headless Chrome (raw CDP, no puppeteer dependency):
140
+
141
+ ```sh
142
+ pnpm --filter @netforge/engine demo:build
143
+ pnpm --filter @netforge/engine exec vite preview --config demo/vite.config.ts --port 5311
144
+ CHROME_BIN=/path/to/chrome node packages/engine/scripts/measure-demo.mjs "http://localhost:5311/?autopilot=1" 20 --enable-unsafe-webgpu --use-angle=metal
145
+ ```
146
+
147
+ The script always launches with `--use-mock-keychain --password-store=basic`
148
+ and a throwaway profile, so it never prompts for the macOS keychain.
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * APPEARANCE (asset library section). An appearance
4
+ * dresses a library model so one base makes many distinct originals: a
5
+ * body colour, an accent colour, a size, and up to four attachments from a
6
+ * fixed list. The model writes appearances; it never writes geometry. The
7
+ * same recipe draws the same character on every client.
8
+ *
9
+ * Not to be confused with game({ look }), the scene's render look.
10
+ * Kept in its own file so schema changes elsewhere rebase mechanically.
11
+ */
12
+ /** Attachments an appearance can add. Each sits on an anchor the body plan has (head, face, back, tail, body, wheels). */
13
+ export declare const APPEARANCE_PARTS: readonly ["horns", "ears_long", "ears_round", "antenna", "crown", "hat_cap", "hat_top", "mustache", "tail", "tail_bolt", "wings", "armor", "backpack", "big_wheels"];
14
+ export declare const appearanceSchema: z.ZodObject<{
15
+ tint: z.ZodOptional<z.ZodString>;
16
+ accent: z.ZodOptional<z.ZodString>;
17
+ scale: z.ZodOptional<z.ZodNumber>;
18
+ parts: z.ZodDefault<z.ZodArray<z.ZodEnum<{
19
+ horns: "horns";
20
+ ears_long: "ears_long";
21
+ ears_round: "ears_round";
22
+ antenna: "antenna";
23
+ crown: "crown";
24
+ hat_cap: "hat_cap";
25
+ hat_top: "hat_top";
26
+ mustache: "mustache";
27
+ tail: "tail";
28
+ tail_bolt: "tail_bolt";
29
+ wings: "wings";
30
+ armor: "armor";
31
+ backpack: "backpack";
32
+ big_wheels: "big_wheels";
33
+ }>>>;
34
+ }, z.core.$strict>;
35
+ export type AppearanceInput = z.input<typeof appearanceSchema>;
36
+ export type AppearanceDef = z.output<typeof appearanceSchema>;
37
+ export type AppearancePart = (typeof APPEARANCE_PARTS)[number];
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * APPEARANCE (asset library section). An appearance
4
+ * dresses a library model so one base makes many distinct originals: a
5
+ * body colour, an accent colour, a size, and up to four attachments from a
6
+ * fixed list. The model writes appearances; it never writes geometry. The
7
+ * same recipe draws the same character on every client.
8
+ *
9
+ * Not to be confused with game({ look }), the scene's render look.
10
+ * Kept in its own file so schema changes elsewhere rebase mechanically.
11
+ */
12
+ /** Attachments an appearance can add. Each sits on an anchor the body plan has (head, face, back, tail, body, wheels). */
13
+ export const APPEARANCE_PARTS = [
14
+ "horns",
15
+ "ears_long",
16
+ "ears_round",
17
+ "antenna",
18
+ "crown",
19
+ "hat_cap",
20
+ "hat_top",
21
+ "mustache",
22
+ "tail",
23
+ "tail_bolt",
24
+ "wings",
25
+ "armor",
26
+ "backpack",
27
+ "big_wheels",
28
+ ];
29
+ const HEX = /^#[0-9a-fA-F]{6}$/;
30
+ export const appearanceSchema = z
31
+ .object({
32
+ /** Main body colour, "#rrggbb". Shifts the model's palette toward it. */
33
+ tint: z.string().regex(HEX, 'colours are "#rrggbb", e.g. "#ffd23f"').optional(),
34
+ /** Colour of the attachments, "#rrggbb". Default: a darker tint. */
35
+ accent: z.string().regex(HEX, 'colours are "#rrggbb", e.g. "#3a2a1a"').optional(),
36
+ /** Size multiplier on the library model's height (0.4 to 3). Visual only; the prefab's radius is the hit size. */
37
+ scale: z.number().min(0.4, "scale is at least 0.4").max(3, "scale is at most 3").optional(),
38
+ /** Up to four attachments, e.g. ["ears_long", "tail_bolt"]. */
39
+ parts: z.array(z.enum(APPEARANCE_PARTS)).max(4, "an appearance carries at most 4 parts").default([]),
40
+ })
41
+ .strict();
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Card battler declarations (game({ template: "cards" })). The template
3
+ * already runs the duel (heroes, mana, draws, a board of units, spells,
4
+ * guards, the turn timer), the gauntlet of duels with a draft between
5
+ * them, the server-side shuffle, hidden hands, the AI opponents and the
6
+ * referee. A game declares its table rules and its cards.
7
+ *
8
+ * cardTable({ health: 20, duels: 4, starter: ["blot_imp", "blot_imp", ...], ai: [{ health: 14, smart: 0.3 }] });
9
+ * card("ink_frog", { model: "style.lowpoly.frog", kind: "unit", cost: 2, attack: 2, health: 3 });
10
+ * card("splash", { model: "style.lowpoly.crystal", kind: "spell", cost: 1, effect: { kind: "damage", amount: 2, target: "enemy" } });
11
+ */
12
+ import { z } from "zod";
13
+ import type { GameManifest } from "./registry.js";
14
+ export declare const EFFECT_KINDS: readonly ["damage", "heal", "draw", "buff"];
15
+ /**
16
+ * Who an effect hits. "enemy": the player picks an enemy unit or the enemy
17
+ * hero; "enemy_hero"; "all_enemies": every enemy unit; "own_hero";
18
+ * "own_unit": the player picks one of their units; "all_own": every unit
19
+ * they have. draw ignores the target.
20
+ */
21
+ export declare const EFFECT_TARGETS: readonly ["enemy", "enemy_hero", "all_enemies", "own_hero", "own_unit", "all_own"];
22
+ export declare const cardEffectSchema: z.ZodObject<{
23
+ kind: z.ZodEnum<{
24
+ damage: "damage";
25
+ heal: "heal";
26
+ draw: "draw";
27
+ buff: "buff";
28
+ }>;
29
+ amount: z.ZodNumber;
30
+ target: z.ZodDefault<z.ZodEnum<{
31
+ enemy: "enemy";
32
+ enemy_hero: "enemy_hero";
33
+ all_enemies: "all_enemies";
34
+ own_hero: "own_hero";
35
+ own_unit: "own_unit";
36
+ all_own: "all_own";
37
+ }>>;
38
+ }, z.core.$strict>;
39
+ /**
40
+ * A card. A "unit" stands on the board with attack and health; `guard`
41
+ * units must be attacked first; `rush` units may attack the turn they are
42
+ * played. A unit's `effect` happens when it is played. A "spell" is only
43
+ * its effect. cost is mana (0 to 10).
44
+ */
45
+ export declare const cardSchema: z.ZodObject<{
46
+ model: z.ZodString;
47
+ kind: z.ZodEnum<{
48
+ unit: "unit";
49
+ spell: "spell";
50
+ }>;
51
+ cost: z.ZodNumber;
52
+ attack: z.ZodDefault<z.ZodNumber>;
53
+ health: z.ZodDefault<z.ZodNumber>;
54
+ guard: z.ZodDefault<z.ZodBoolean>;
55
+ rush: z.ZodDefault<z.ZodBoolean>;
56
+ effect: z.ZodOptional<z.ZodObject<{
57
+ kind: z.ZodEnum<{
58
+ damage: "damage";
59
+ heal: "heal";
60
+ draw: "draw";
61
+ buff: "buff";
62
+ }>;
63
+ amount: z.ZodNumber;
64
+ target: z.ZodDefault<z.ZodEnum<{
65
+ enemy: "enemy";
66
+ enemy_hero: "enemy_hero";
67
+ all_enemies: "all_enemies";
68
+ own_hero: "own_hero";
69
+ own_unit: "own_unit";
70
+ all_own: "all_own";
71
+ }>>;
72
+ }, z.core.$strict>>;
73
+ }, z.core.$strict>;
74
+ export declare const cardTableSchema: z.ZodObject<{
75
+ health: z.ZodDefault<z.ZodNumber>;
76
+ startHand: z.ZodDefault<z.ZodNumber>;
77
+ handMax: z.ZodDefault<z.ZodNumber>;
78
+ boardMax: z.ZodDefault<z.ZodNumber>;
79
+ manaMax: z.ZodDefault<z.ZodNumber>;
80
+ turnSeconds: z.ZodDefault<z.ZodNumber>;
81
+ draftSeconds: z.ZodDefault<z.ZodNumber>;
82
+ duels: z.ZodDefault<z.ZodNumber>;
83
+ starter: z.ZodArray<z.ZodString>;
84
+ pool: z.ZodOptional<z.ZodArray<z.ZodString>>;
85
+ ai: z.ZodArray<z.ZodObject<{
86
+ health: z.ZodNumber;
87
+ smart: z.ZodNumber;
88
+ }, z.core.$strict>>;
89
+ turnCap: z.ZodDefault<z.ZodNumber>;
90
+ }, z.core.$strict>;
91
+ export type CardInput = z.input<typeof cardSchema>;
92
+ export type CardDef = z.output<typeof cardSchema>;
93
+ export type CardTableInput = z.input<typeof cardTableSchema>;
94
+ export type CardTableDef = z.output<typeof cardTableSchema>;
95
+ /** The table: hero health, hand and board sizes, the gauntlet, the starter deck and the AI tiers (one per game). */
96
+ export declare function cardTable(def: CardTableInput): CardTableDef;
97
+ /** A card (at most 40). */
98
+ export declare function card(id: string, def: CardInput): CardDef;
99
+ /** Store upgrade stats a card battler reads: hero health, unit attack, starting mana, starting hand. */
100
+ export declare const CARD_STATS: readonly ["maxHealth", "might", "haste", "amount"];
101
+ /** The card battler content of a manifest, cross-checked (one line per issue). */
102
+ export declare function cardsContent(m: GameManifest): {
103
+ table: CardTableDef;
104
+ cards: [string, CardDef][];
105
+ };