@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
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,101 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @netforge/engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Net Forge game engine client: a typed, declarative game API and a three.js renderer (WebGPU with a WebGL2 fallback) for multiplayer browser games.
|
|
4
|
+
|
|
5
|
+
One simulation is shared by client and server (an ECS with Rapier physics, a fixed tick and snapshots, in `@netforge/engine-sim`), ten finished multiplayer games ship as templates (`@netforge/template-*`), and the whole API is built to be written by your own coding agent.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @netforge/engine
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Node 20 or later. The package ships ES modules with TypeScript types.
|
|
14
|
+
|
|
15
|
+
## Example
|
|
16
|
+
|
|
17
|
+
A game is one file, `src/game.ts`, that declares what the game is. It imports only `@netforge/engine` and never touches the renderer, the netcode or the physics. This is a cut-down version of the arena shooter template:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { earn, game, prefab, store, weapon } from "@netforge/engine";
|
|
21
|
+
|
|
22
|
+
export default game({
|
|
23
|
+
template: "arena",
|
|
24
|
+
style: "lowpoly",
|
|
25
|
+
places: { yard: { chunks: "places/yard", spawn: [0, 1, 0] } },
|
|
26
|
+
players: { prefab: "style.lowpoly.mini_male_f", max: 16, loadout: ["arc_rifle"], bots: 6, respawn: 3, drop: "volt_cell" },
|
|
27
|
+
arena: { halfSize: 26, pillars: [[0, 0, 2.6]], spawns: [[22, 22], [-22, -22], [22, -22], [-22, 22]] },
|
|
28
|
+
round: { seconds: 180, frags: 25 },
|
|
29
|
+
});
|
|
30
|
+
prefab("volt_cell", { model: "style.lowpoly.cell_heal", health: 1, speed: 0, kind: "pickup", value: 35 });
|
|
31
|
+
weapon("arc_rifle", { kind: "blaster", damage: 12, cooldown: 0.15, range: 26, speed: 44 });
|
|
32
|
+
earn.rule("live_wire", { when: "opponent.killed", n: 3, pay: 4, oncePer: "run" });
|
|
33
|
+
earn.stake({ min: 10, max: 100, multiple: 2 });
|
|
34
|
+
store.sku("skin_neon", { coins: 300, grants: { cosmetic: 1 } });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then play it headless with bots and check it:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { engine } from "@netforge/engine";
|
|
41
|
+
|
|
42
|
+
const report = await engine.test.run({
|
|
43
|
+
ticks: 60 * 240,
|
|
44
|
+
bots: 4,
|
|
45
|
+
assert: [{ name: "someone scores", check: (r) => r.kills > 0 }],
|
|
46
|
+
});
|
|
47
|
+
console.log(report.kills, report.earned, report.softlock, report.msPerTick);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Bad declarations throw `GameDefinitionError` with one actionable line per issue, for example `model: reference assets by library id (e.g. "style.lowpoly.bat"), never by URL or path`.
|
|
51
|
+
|
|
52
|
+
## Entry points
|
|
53
|
+
|
|
54
|
+
| Import | Who uses it | What it holds |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `@netforge/engine` | game code (`src/game.ts`) | `game`, `prefab`, `weapon`, `upgrade`, `system`, `director`, `earn`, `store`, `keeper`, `achievement`, `mission`, `title`, `economy`, `metaUpgrade`, `dungeon`, the board genre declarations (`towerBoard`, `tower`, `waves`, `saltFlat`, `plant`, `marsh`, `creature`, `move`, `card`, `cardTable`), and `engine.test.run`. Declarative, Zod-validated, no DOM. |
|
|
57
|
+
| `@netforge/engine/runtime` | template clients | the three.js renderer (`createRenderer`), `createRenderStack`, `CameraRig`, `InputController` (keyboard, mouse, gamepad, touch), `AssetLoader` (glTF, meshopt, KTX2), `FeelKit` and its parts, `SoundPlayer`, `ArenaClient`, `SystemScheduler`, `collectManifest`. |
|
|
58
|
+
| `@netforge/engine/game` | templates, room hosts, tests | `compileGame`, `collectManifest`, `GameRunner`, `EarnTracker`, `engineSimAdapter` (the SimAdapter on `@netforge/engine-sim`). |
|
|
59
|
+
| `@netforge/engine/net` | template clients | the client half of the netcode: prediction and reconciliation, interpolation, delta snapshots, input packets, clock adjustment and the room client. |
|
|
60
|
+
| `@netforge/engine/rules` | board genres | `rulesAdapter`, `playRules` and the genre modules: deterministic rules (tower defense, idle, cards, collector) behind the same SimAdapter seam. |
|
|
61
|
+
| `@netforge/engine/voxel` | template clients | the voxel style's core. |
|
|
62
|
+
| `@netforge/engine/earn-source` | tools | reads and rewrites the `earn.rule(...)` lines of a `src/game.ts` as source text. |
|
|
63
|
+
|
|
64
|
+
Game code imports only the root. Everything else is template machinery.
|
|
65
|
+
|
|
66
|
+
## What the root API covers
|
|
67
|
+
|
|
68
|
+
- **Templates**: `TEMPLATES` lists them: `survivor`, `arena`, `dungeon`, `racer`, `platformer`, `tower-defense`, `party`, `idle`, `cards` and `collector`.
|
|
69
|
+
- **Art**: `game({ style })` picks one of `STYLES`: `pixel`, `lowpoly`, `toon`, `voxel`, `ps1` or `handdrawn`. Models are asset library ids such as `"style.lowpoly.ghost"`, never URLs or file paths.
|
|
70
|
+
- **Look and feel**: `game({ look })`, `prefab({ feel })` and `weapon({ feel })` are presentation only and never change the simulation.
|
|
71
|
+
- **Earn**: every game declares at least one `earn.rule(...)` and exactly one `earn.stake({ min, max, multiple })`. The build fails without them. Ids are lower_snake_case.
|
|
72
|
+
- **Coins and the store**: `store.sku(...)` sells in-run consumables, cosmetics and unlocks by default. Permanent stat upgrades are opt-in through `economy({ metaUpgrades: true })` and `metaUpgrade(...)`, capped by the engine (`META_MAX_PER_STAT`, `META_MAX_TOTAL`).
|
|
73
|
+
- **Systems**: `system(id, ctx => ...)` gets `every`, `after`, `on`, `emit`, `spawn`, `spawnEdge`, `players`, `end` and a seeded `random`, on a tick clock.
|
|
74
|
+
- **Playtest**: `engine.test.run({ ticks, bots, seed?, enemies?, bot?, assert? })` plays the game on the server sim and reports kills, deaths, waves, runs, earn by rule, softlocks, ms per tick and fun metrics.
|
|
75
|
+
|
|
76
|
+
`GAME-DESIGN.md` holds the design rules the playtest gate measures (a readable opening, fast rewards, no on-screen spawns) and the recommended coin model.
|
|
77
|
+
|
|
78
|
+
## For AI agents
|
|
79
|
+
|
|
80
|
+
- Read `llms.txt` (look, feel, physics and rendering) and `llms-full.txt` (`llms.txt`, `GAME-DESIGN.md` and the API reference in one file).
|
|
81
|
+
- Each template package (`@netforge/template-*`) ships its own `llms.txt`, `llms-full.txt` and `AGENTS.md` for its genre.
|
|
82
|
+
- The Net Forge MCP server gives an agent the same tools over stdio: `npx -y @netforge/mcp` (package `@netforge/mcp`, bin `netforge-mcp`). Tools: `netforge_list_templates`, `netforge_create_game`, `netforge_explain_api`, `netforge_search_docs`, `netforge_validate_game`, `netforge_run_playtest`, `netforge_play_locally`.
|
|
83
|
+
|
|
84
|
+
To set up a coding agent in a game project, run `npx netforge setup agent`. It writes AGENTS.md, CLAUDE.md, Cursor rules and `.mcp.json` into the project. In Claude Code you can add the server with `claude mcp add netforge -- npx -y @netforge/mcp`. Any other MCP client takes this config:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{ "mcpServers": { "netforge": { "command": "npx", "args": ["-y", "@netforge/mcp"] } } }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Play a game locally
|
|
91
|
+
|
|
92
|
+
`@netforge/kit-play` runs a game folder (`netnet.json` plus `src/game.ts`) on your own computer with play money. See its README.
|
|
93
|
+
|
|
94
|
+
## Docs
|
|
95
|
+
|
|
96
|
+
- https://forge.netnet.capital/docs/packages/engine
|
|
97
|
+
- `llms.txt`, `llms-full.txt` and `GAME-DESIGN.md` in this package
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT. See `LICENSE` and `NOTICE`.
|
|
@@ -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
|
+
};
|