@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
@@ -0,0 +1,341 @@
1
+ /**
2
+ * The engine-sim SimAdapter: the netcode's seam (net/sim-adapter.ts) over
3
+ * @netforge/engine-sim, used identically by the room server
4
+ * (authority, with a GameRunner) and the browser (prediction of its own
5
+ * character only).
6
+ *
7
+ * Wire schema: players carry their whole state at full precision (1/1024 m)
8
+ * so prediction replays exactly; enemies, gems and projectiles carry only
9
+ * x/z at 1/16 m plus type, health and flags, which keeps a crowd of 2,000
10
+ * at a few bits per moving entity.
11
+ */
12
+ import { addPlayer, applyBeamHit, applyState, arenaHitboxes, arenaSpawn, beamShot, captureState, createWorld, destroyWorld, grantUpgrade, removePlayer, revivePlayer, setCosmetic, setMetaBonus, step, EntityKind, KEEPER_MASK, KEEPER_SHIFT, } from "@netforge/engine-sim";
13
+ import { GameRunner } from "./runner.js";
14
+ import { metaBonus, profileSpec, unpackMeta } from "./profile.js";
15
+ /** Field indices of the engine wire schema. */
16
+ export const F = {
17
+ px: 0,
18
+ py: 1,
19
+ pz: 2,
20
+ vx: 3,
21
+ vy: 4,
22
+ vz: 5,
23
+ yaw: 6,
24
+ health: 7,
25
+ flags: 8,
26
+ cooldown: 9,
27
+ buttons: 10,
28
+ timers: 11,
29
+ type: 12,
30
+ level: 13,
31
+ xp: 14,
32
+ weapons: 15,
33
+ wcd: 16,
34
+ stats: 17,
35
+ choices: 18,
36
+ bleed: 19,
37
+ revive: 20,
38
+ iframes: 21,
39
+ dash: 22,
40
+ ex: 23,
41
+ ez: 24,
42
+ /** Players: rerolls used | reroll counter << 4 | keeper << 12 | revive buff ticks << 16 (prediction draws the same cards, moves at the buffed speed). */
43
+ value: 25,
44
+ };
45
+ /** Units per metre of the crowd position fields (ex, ez): 1/16 m (6 cm) is below what a crowd shows on screen. */
46
+ export const CROWD_UNITS = 16;
47
+ /** Units per metre of player positions (px, py, pz): engine-sim's own fixed point. */
48
+ export const PLAYER_UNITS = 1024;
49
+ const playerFields = [
50
+ F.px, F.py, F.pz, F.vx, F.vy, F.vz, F.yaw, F.health, F.flags, F.cooldown, F.buttons, F.timers,
51
+ F.type, F.level, F.xp, F.weapons, F.wcd, F.stats, F.choices, F.bleed, F.revive, F.iframes, F.dash, F.value,
52
+ ];
53
+ export const engineSchema = {
54
+ fields: [
55
+ { name: 'px', kind: 'int', bits: 19, deltaBits: 12 },
56
+ { name: 'py', kind: 'int', bits: 15, deltaBits: 10 },
57
+ { name: 'pz', kind: 'int', bits: 19, deltaBits: 12 },
58
+ { name: 'vx', kind: 'int', bits: 17, deltaBits: 10 },
59
+ { name: 'vy', kind: 'int', bits: 17, deltaBits: 10 },
60
+ { name: 'vz', kind: 'int', bits: 17, deltaBits: 10 },
61
+ { name: 'yaw', kind: 'angle16', deltaBits: 10 },
62
+ { name: 'health', kind: 'uint', bits: 20, interp: 'step', deltaBits: 8 },
63
+ { name: 'flags', kind: 'uint', bits: 8, interp: 'step' },
64
+ { name: 'cooldown', kind: 'uint', bits: 16, interp: 'step', deltaBits: 6 },
65
+ { name: 'buttons', kind: 'uint', bits: 16, interp: 'step' },
66
+ { name: 'timers', kind: 'uint', bits: 16, interp: 'step' },
67
+ { name: 'type', kind: 'uint', bits: 8, interp: 'step' },
68
+ { name: 'level', kind: 'uint', bits: 16, interp: 'step', deltaBits: 4 },
69
+ { name: 'xp', kind: 'uint', bits: 24, interp: 'step', deltaBits: 8 },
70
+ { name: 'weapons', kind: 'uint', bits: 32, interp: 'step' },
71
+ { name: 'wcd', kind: 'uint', bits: 32, interp: 'step' },
72
+ { name: 'stats', kind: 'uint', bits: 32, interp: 'step' },
73
+ { name: 'choices', kind: 'uint', bits: 32, interp: 'step' },
74
+ { name: 'bleed', kind: 'uint', bits: 16, interp: 'step', deltaBits: 6 },
75
+ { name: 'revive', kind: 'uint', bits: 16, interp: 'step', deltaBits: 6 },
76
+ { name: 'iframes', kind: 'uint', bits: 16, interp: 'step', deltaBits: 6 },
77
+ { name: 'dash', kind: 'uint', bits: 24, interp: 'step' },
78
+ { name: 'ex', kind: 'int', bits: 13, deltaBits: 5 },
79
+ { name: 'ez', kind: 'int', bits: 13, deltaBits: 5 },
80
+ { name: 'value', kind: 'uint', bits: 28, interp: 'step' },
81
+ ],
82
+ xField: F.px,
83
+ yField: F.pz,
84
+ unitsPerWorld: PLAYER_UNITS,
85
+ kindFields: {
86
+ [EntityKind.Player]: playerFields,
87
+ // level: the telegraph word (engine-sim Telegraph), 0 except while an attack winds up.
88
+ [EntityKind.Enemy]: [F.health, F.flags, F.type, F.level, F.ex, F.ez],
89
+ // flags: Hostile (an enemy's shot) and hazards; level: a hazard's telegraph.
90
+ [EntityKind.Projectile]: [F.type, F.flags, F.level, F.ex, F.ez],
91
+ [EntityKind.Pickup]: [F.type, F.ex, F.ez],
92
+ },
93
+ };
94
+ const TAU = Math.PI * 2;
95
+ const YAW_UNITS = 65536;
96
+ const idleInput = { seq: 0, tick: 0, moveX: 0, moveZ: 0, yaw: 0, buttons: 0 };
97
+ /** The engine InputCommand on the wire: moves as int8, yaw as u16 turn, buttons u16 (48 bits). */
98
+ const inputCodec = {
99
+ idle: idleInput,
100
+ write(w, i) {
101
+ w.writeSigned(axis(i.moveX), 8);
102
+ w.writeSigned(axis(i.moveZ), 8);
103
+ w.writeBits(Number.isFinite(i.yaw) ? Math.round((((i.yaw % TAU) + TAU) % TAU) / TAU * YAW_UNITS) & 0xffff : 0, 16);
104
+ w.writeBits(i.buttons & 0xffff, 16);
105
+ },
106
+ read(r) {
107
+ const moveX = r.readSigned(8) / 127;
108
+ const moveZ = r.readSigned(8) / 127;
109
+ const yaw = (r.readBits(16) / YAW_UNITS) * TAU;
110
+ const buttons = r.readBits(16);
111
+ return { seq: 0, tick: 0, moveX, moveZ, yaw, buttons };
112
+ },
113
+ };
114
+ function axis(v) {
115
+ if (!Number.isFinite(v))
116
+ return 0;
117
+ return Math.round(Math.max(-1, Math.min(1, v)) * 127);
118
+ }
119
+ /** engine-sim entity → netcode values in schema order. */
120
+ function toValues(e) {
121
+ const v = new Array(26).fill(0);
122
+ v[F.px] = e.px;
123
+ v[F.pz] = e.pz;
124
+ v[F.type] = e.type;
125
+ v[F.health] = e.health;
126
+ v[F.flags] = e.flags;
127
+ v[F.ex] = Math.round((e.px * CROWD_UNITS) / PLAYER_UNITS);
128
+ v[F.ez] = Math.round((e.pz * CROWD_UNITS) / PLAYER_UNITS);
129
+ v[F.level] = e.level;
130
+ // Enemies: the crit counter (timers bits 8..11) so clients can mark crit numbers.
131
+ if (e.kind === EntityKind.Enemy)
132
+ v[F.timers] = (e.timers >>> 8) & 0xf;
133
+ if (e.kind !== EntityKind.Player)
134
+ return v;
135
+ v[F.py] = e.py;
136
+ v[F.vx] = e.vx;
137
+ v[F.vy] = e.vy;
138
+ v[F.vz] = e.vz;
139
+ v[F.yaw] = (e.yaw / YAW_UNITS) * TAU;
140
+ v[F.cooldown] = e.cooldown;
141
+ v[F.buttons] = e.buttons;
142
+ v[F.timers] = e.timers;
143
+ v[F.level] = e.level;
144
+ v[F.xp] = e.xp;
145
+ v[F.weapons] = e.weapons;
146
+ v[F.wcd] = e.wcd;
147
+ v[F.stats] = e.stats;
148
+ v[F.choices] = e.choices;
149
+ v[F.bleed] = e.bleed;
150
+ v[F.revive] = e.revive;
151
+ v[F.iframes] = e.iframes;
152
+ v[F.dash] = e.dash;
153
+ v[F.value] = e.value & 0xfffffff;
154
+ return v;
155
+ }
156
+ /** Netcode values → an engine-sim entity (players exact; crowd positions at 1/16 m). */
157
+ function fromValues(s) {
158
+ const v = s.values;
159
+ const n = (i) => Math.round(v[i] ?? 0);
160
+ const player = s.kind === EntityKind.Player;
161
+ return {
162
+ netId: s.id,
163
+ kind: s.kind,
164
+ owner: s.owner,
165
+ px: player ? n(F.px) : (n(F.ex) * PLAYER_UNITS) / CROWD_UNITS,
166
+ py: n(F.py),
167
+ pz: player ? n(F.pz) : (n(F.ez) * PLAYER_UNITS) / CROWD_UNITS,
168
+ vx: n(F.vx),
169
+ vy: n(F.vy),
170
+ vz: n(F.vz),
171
+ yaw: Math.round(((((v[F.yaw] ?? 0) % TAU) + TAU) % TAU) / TAU * YAW_UNITS) % YAW_UNITS,
172
+ health: n(F.health),
173
+ flags: n(F.flags),
174
+ ack: 0,
175
+ cooldown: n(F.cooldown),
176
+ buttons: n(F.buttons),
177
+ timers: player ? n(F.timers) : s.kind === EntityKind.Enemy ? (n(F.timers) & 0xf) << 8 : 0,
178
+ type: n(F.type),
179
+ value: player ? n(F.value) : 0,
180
+ level: n(F.level),
181
+ xp: n(F.xp),
182
+ weapons: n(F.weapons) >>> 0,
183
+ wcd: n(F.wcd) >>> 0,
184
+ stats: n(F.stats),
185
+ choices: n(F.choices) >>> 0,
186
+ bleed: n(F.bleed),
187
+ revive: n(F.revive),
188
+ iframes: n(F.iframes),
189
+ dash: n(F.dash),
190
+ };
191
+ }
192
+ /**
193
+ * Build the SimAdapter. One instance serves every room of a template; each
194
+ * room gets its own world from `createWorld`. `maxInputsPerTick: 1`:
195
+ * engine-sim steps exactly one command per player per tick.
196
+ */
197
+ export function engineSimAdapter(opts) {
198
+ const game = opts.game;
199
+ const tickHz = game?.tickRate ?? opts.arena?.tickRate ?? 30;
200
+ const baseConfig = game ? game.sim : { ...opts.arena, tickRate: tickHz };
201
+ const arena = game?.mode === 'arena';
202
+ const spec = game ? profileSpec(game.manifest) : null;
203
+ /** Upgrades some gear SKU brings into a staked run (the room issues these commands; the room never takes them from a client). */
204
+ const gearUpgrades = new Set([...(game?.manifest.skus.values() ?? [])].flatMap((s) => (s.grants && "gear" in s.grants ? [s.grants.gear] : [])));
205
+ return {
206
+ name: opts.name,
207
+ version: opts.version,
208
+ tickHz,
209
+ schema: engineSchema,
210
+ input: inputCodec,
211
+ maxInputsPerTick: 1,
212
+ createWorld(seed, role) {
213
+ // The client predicts only its own character: no spawner, no runner.
214
+ // Arena: the client has no authority (no projectiles, hits or respawns
215
+ // of its own); the server resolves human beams with lag compensation.
216
+ const content = baseConfig.content;
217
+ const arenaCfg = arena && content ? { ...content, arena: { ...content.arena, authority: role === 'server', externalHitscan: role === 'server' } } : content;
218
+ // A prediction world holds only its own character: it takes "solo" from the server's flag.
219
+ const clientContent = arenaCfg ? { ...arenaCfg, coop: { ...arenaCfg.coop, soloFromFlag: true } } : arenaCfg;
220
+ const cfg = role === 'client'
221
+ ? { ...baseConfig, seed, enemies: { ...baseConfig.enemies, target: 0 }, ...(clientContent ? { content: clientContent } : {}) }
222
+ : { ...baseConfig, seed, ...(arenaCfg ? { content: arenaCfg } : {}) };
223
+ const sim = createWorld(cfg);
224
+ const runner = role === 'server' && game ? new GameRunner(game, sim, opts.runner) : null;
225
+ return { sim, role, runner, shots: new Map() };
226
+ },
227
+ addPlayer(w, player) {
228
+ const [sx, sy, sz] = w.sim.config.arena.spawn;
229
+ if (arena) {
230
+ const [x, z] = arenaSpawn(w.sim, player);
231
+ return addPlayer(w.sim, player, [x, sy, z]);
232
+ }
233
+ return addPlayer(w.sim, player, [sx + ((player - 1) % 8) * 1.5 - 5, sy, sz]);
234
+ },
235
+ removePlayer(w, player) {
236
+ removePlayer(w.sim, player);
237
+ },
238
+ step(w, inputsByPlayer) {
239
+ const inputs = new Map();
240
+ for (const [p, list] of inputsByPlayer) {
241
+ const cmd = list[0];
242
+ if (cmd)
243
+ inputs.set(p, cmd);
244
+ }
245
+ w.runner?.beforeStep(inputs);
246
+ w.shots.clear();
247
+ const res = step(w.sim, inputs);
248
+ const out = [];
249
+ if (w.runner) {
250
+ const r = w.runner.afterStep(res.events);
251
+ for (const a of r.awards)
252
+ out.push({ type: 'earn', player: a.player, rule: a.rule, units: a.units });
253
+ for (const p of w.runner.takeProgress())
254
+ out.push({ type: 'progress', player: p.player, kind: p.kind, id: p.id });
255
+ if (r.reset)
256
+ out.push({ type: 'run.reset' });
257
+ }
258
+ else if (w.role === 'server') {
259
+ for (const e of res.events)
260
+ if (e.type === 'enemy.killed' && e.by > 0)
261
+ out.push({ type: 'earn', player: e.by, rule: 'kill', units: 1 });
262
+ }
263
+ return out;
264
+ },
265
+ snapshot(w) {
266
+ return captureState(w.sim).entities.map((e) => ({ id: e.netId, kind: e.kind, owner: e.owner, values: toValues(e) }));
267
+ },
268
+ applySnapshot(w, entities) {
269
+ applyState(w.sim, { ...captureState(w.sim), entities: entities.map(fromValues) });
270
+ },
271
+ info(w) {
272
+ return w.runner?.info() ?? null;
273
+ },
274
+ restrictInput(input, rights) {
275
+ const allowed = rights.keepers;
276
+ if (!allowed)
277
+ return input;
278
+ const pick = ((input.buttons & KEEPER_MASK) >>> KEEPER_SHIFT) - 1;
279
+ if (pick < 0 || allowed.includes(pick))
280
+ return input;
281
+ // A locked keeper: play the first open one instead (the room's profile check).
282
+ const open = allowed[0];
283
+ const rest = input.buttons & ~KEEPER_MASK;
284
+ return { ...input, buttons: (open === undefined ? rest : rest | (((open + 1) & 0xf) << KEEPER_SHIFT)) >>> 0 };
285
+ },
286
+ startRun(w, player) {
287
+ w.runner?.startRun(player);
288
+ },
289
+ command(w, player, cmd) {
290
+ if (cmd.name === 'revive')
291
+ return revivePlayer(w.sim, player);
292
+ // Owned meta upgrades (issued by the room from the profile, never by a client): capped again here.
293
+ if (cmd.name === 'meta') {
294
+ if (!spec || spec.meta.size === 0 || !spec.economy.metaUpgrades)
295
+ return false;
296
+ setMetaBonus(w.sim, player, metaBonus(spec, unpackMeta(spec, cmd.value ?? 0)));
297
+ return true;
298
+ }
299
+ if (cmd.name === 'cosmetic') {
300
+ setCosmetic(w.sim, player, cmd.value ?? 0);
301
+ return true;
302
+ }
303
+ // A store upgrade: a SKU named after a declared upgrade grants one level of it for this run.
304
+ const up = game ? game.upgradeIds.indexOf(cmd.name) : -1;
305
+ if (game && up >= 0 && game.manifest.skus.has(cmd.name))
306
+ return grantUpgrade(w.sim, player, up);
307
+ // Gear (issued by the room from the profile at a staked run's start, never by a client): one level of its upgrade.
308
+ if (game && up >= 0 && gearUpgrades.has(cmd.name))
309
+ return grantUpgrade(w.sim, player, up);
310
+ return false;
311
+ },
312
+ // Lag compensation: arena beams from human players.
313
+ ...(arena
314
+ ? {
315
+ hitscans(w, player, input) {
316
+ const shot = beamShot(w.sim, player, input);
317
+ if (!shot)
318
+ return null;
319
+ w.shots.set(player, shot);
320
+ return { shooter: player, ox: shot.x, oy: shot.z, dx: shot.dx, dy: shot.dz, range: shot.range };
321
+ },
322
+ hitboxes(w) {
323
+ return arenaHitboxes(w.sim).map((h) => ({ entity: h.netId, owner: h.owner, x: h.x, y: h.z, r: h.r }));
324
+ },
325
+ applyHit(w, shooter, target) {
326
+ const shot = w.shots.get(shooter);
327
+ if (shot)
328
+ applyBeamHit(w.sim, shot, target.entity);
329
+ return [];
330
+ },
331
+ }
332
+ : {}),
333
+ disposeWorld(w) {
334
+ destroyWorld(w.sim);
335
+ },
336
+ };
337
+ }
338
+ /** Free a world's physics memory (rooms call this on dispose). */
339
+ export function disposeEngineWorld(w) {
340
+ destroyWorld(w.sim);
341
+ }
@@ -0,0 +1,66 @@
1
+ import type { InputCommand } from "@netforge/engine-sim";
2
+ /** One opponent as an arena bot sees it. */
3
+ export interface ArenaBotOpponent {
4
+ readonly id: number;
5
+ readonly x: number;
6
+ readonly z: number;
7
+ /** How sharply the bot plays against this opponent (overrides the view's skill): seat bots go easy on a newcomer. */
8
+ readonly skill?: number;
9
+ /** Other bots are already on this one: pick it only when nobody else is around. */
10
+ readonly crowded?: boolean;
11
+ }
12
+ export interface ArenaBotWeapon {
13
+ readonly kind: string;
14
+ readonly range: number;
15
+ readonly count: number;
16
+ }
17
+ /** What an arena bot knows this tick (server side: the present). */
18
+ export interface ArenaBotView {
19
+ readonly id: number;
20
+ readonly x: number;
21
+ readonly z: number;
22
+ readonly health: number;
23
+ readonly maxHealth: number;
24
+ readonly up: boolean;
25
+ /** The selected loadout slot (0..3). */
26
+ readonly slot: number;
27
+ /** The loadout by slot. */
28
+ readonly loadout: readonly ArenaBotWeapon[];
29
+ /** Living opponents. */
30
+ readonly opponents: readonly ArenaBotOpponent[];
31
+ readonly pickups: readonly {
32
+ readonly x: number;
33
+ readonly z: number;
34
+ }[];
35
+ readonly pillars: readonly {
36
+ readonly x: number;
37
+ readonly z: number;
38
+ readonly radius: number;
39
+ }[];
40
+ /** Arena half extent, m. */
41
+ readonly half: number;
42
+ /** Varies how bots strafe and miss between worlds (the world seed); default 0. */
43
+ readonly seed?: number;
44
+ /** 1 plays sharp; lower misses more, reacts later and fires in shorter bursts. Default 1. */
45
+ readonly skill?: number;
46
+ }
47
+ /** A bot's memory between ticks (the caller keeps one per bot). */
48
+ export interface ArenaBotMemory {
49
+ target: number;
50
+ since: number;
51
+ aimError: number;
52
+ lastHealth: number;
53
+ /** Tick the current target came into clear sight (-1: not in sight). The bot reacts after a delay. */
54
+ sightSince?: number;
55
+ }
56
+ export declare function arenaBotMemory(): ArenaBotMemory;
57
+ /** Does the segment from (x0, z0) to (x1, z1) pass through a pillar? */
58
+ export declare function sightBlocked(x0: number, z0: number, x1: number, z1: number, pillars: ArenaBotView["pillars"]): boolean;
59
+ /**
60
+ * One tick of an arena bot: pick the nearest opponent it can see (sticky
61
+ * for a while), keep a weapon-appropriate distance while strafing, go for a
62
+ * heal cell when hurt, switch to the right weapon for the range, and fire
63
+ * when the shot is clear. Aim carries a per-bot error, so bots are beatable.
64
+ * `tickRate` scales its timers.
65
+ */
66
+ export declare function arenaBotInput(v: ArenaBotView, mem: ArenaBotMemory, tick: number, tickRate?: number): InputCommand;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Arena bots: they fill empty seats in arena rooms (server side, through
3
+ * the GameRunner) and play the headless playtests. A bot is a pure function
4
+ * of what it sees, its small memory and the tick, so a room's run replays
5
+ * exactly (run verification) and bots never draw from the sim's PRNG.
6
+ */
7
+ import { Button } from "@netforge/engine-sim";
8
+ export function arenaBotMemory() {
9
+ return { target: 0, since: 0, aimError: 0, lastHealth: 0, sightSince: -1 };
10
+ }
11
+ /** A fixed integer hash to 0..1. */
12
+ function h01(a, b) {
13
+ let h = Math.imul(a ^ 0x9e3779b9, 0x85ebca6b) ^ Math.imul(b + 0x6d2b79f5, 0xc2b2ae35);
14
+ h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
15
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
16
+ return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
17
+ }
18
+ /** Does the segment from (x0, z0) to (x1, z1) pass through a pillar? */
19
+ export function sightBlocked(x0, z0, x1, z1, pillars) {
20
+ const dx = x1 - x0;
21
+ const dz = z1 - z0;
22
+ const len2 = dx * dx + dz * dz;
23
+ if (len2 < 1e-9)
24
+ return false;
25
+ for (const p of pillars) {
26
+ const t = Math.max(0, Math.min(1, ((p.x - x0) * dx + (p.z - z0) * dz) / len2));
27
+ const cx = x0 + dx * t - p.x;
28
+ const cz = z0 + dz * t - p.z;
29
+ if (cx * cx + cz * cz < (p.radius + 0.15) ** 2)
30
+ return true;
31
+ }
32
+ return false;
33
+ }
34
+ /**
35
+ * One tick of an arena bot: pick the nearest opponent it can see (sticky
36
+ * for a while), keep a weapon-appropriate distance while strafing, go for a
37
+ * heal cell when hurt, switch to the right weapon for the range, and fire
38
+ * when the shot is clear. Aim carries a per-bot error, so bots are beatable.
39
+ * `tickRate` scales its timers.
40
+ */
41
+ export function arenaBotInput(v, mem, tick, tickRate = 30) {
42
+ const sec = (s) => Math.max(1, Math.round(s * tickRate));
43
+ const salt = Math.imul((v.seed ?? 0) >>> 0, 0x9e3779b1) >>> 16;
44
+ const idle = { seq: 0, tick, moveX: 0, moveZ: 0, yaw: 0, buttons: 0 };
45
+ if (!v.up) {
46
+ mem.target = 0;
47
+ mem.lastHealth = v.maxHealth;
48
+ return idle;
49
+ }
50
+ const seen = (o) => !sightBlocked(v.x, v.z, o.x, o.z, v.pillars);
51
+ // Target: keep the current one for up to 6 s while it lives and stays in sight, else the nearest visible (or nearest).
52
+ let target = v.opponents.find((o) => o.id === mem.target);
53
+ if (!target || tick - mem.since > sec(6) || (!seen(target) && tick - mem.since > sec(1.5))) {
54
+ let best;
55
+ let bestScore = Infinity;
56
+ for (const o of v.opponents) {
57
+ const score = Math.hypot(o.x - v.x, o.z - v.z) + (seen(o) ? 0 : 25) + (o.crowded ? 18 : 0);
58
+ if (score < bestScore) {
59
+ bestScore = score;
60
+ best = o;
61
+ }
62
+ }
63
+ if (best && best.id !== mem.target) {
64
+ mem.target = best.id;
65
+ mem.since = tick;
66
+ mem.sightSince = -1;
67
+ }
68
+ else if (best)
69
+ mem.since = tick;
70
+ target = best;
71
+ }
72
+ let mx = 0;
73
+ let mz = 0;
74
+ let yaw = 0;
75
+ let buttons = 0;
76
+ let skill = Math.max(0.05, Math.min(1, v.skill ?? 1));
77
+ /** Fraction of full speed: a loose bot strafes slowly in a fight, so it can be hit. */
78
+ let pace = 1;
79
+ if (target) {
80
+ const dx = target.x - v.x;
81
+ const dz = target.z - v.z;
82
+ const dist = Math.hypot(dx, dz) || 1e-6;
83
+ const clear = seen(target);
84
+ // Skill against this target: aim, reaction and bursts all scale with it.
85
+ skill = Math.max(0.05, Math.min(1, target.skill ?? skill));
86
+ if (!clear)
87
+ mem.sightSince = -1;
88
+ else if ((mem.sightSince ?? -1) < 0)
89
+ mem.sightSince = tick;
90
+ // The weapon for the range: the spread gun up close, the beam far away, the rifle between.
91
+ let want = v.slot;
92
+ let wantScore = -Infinity;
93
+ v.loadout.forEach((w, s) => {
94
+ let score;
95
+ if (w.kind === "beam")
96
+ score = dist > 12 ? 3 : 0;
97
+ else if (w.kind === "nova")
98
+ score = dist < w.range * 0.9 ? 4 : -2;
99
+ else if (w.count > 1)
100
+ score = dist < 7 ? 3 : -1;
101
+ else
102
+ score = 2;
103
+ if (dist > w.range)
104
+ score -= 5;
105
+ if (score > wantScore) {
106
+ wantScore = score;
107
+ want = s;
108
+ }
109
+ });
110
+ if (want !== v.slot && want <= 2 && tick % 4 === 0)
111
+ buttons |= Button.Choice1 << want;
112
+ const weapon = v.loadout[v.slot];
113
+ const range = weapon?.range ?? 12;
114
+ // Keep distance (60% of the weapon's reach, 4 to 14 m) and strafe; flip direction every few seconds.
115
+ // A loose bot fights closer, where it is easier to hit.
116
+ const keep = Math.max(4, Math.min(14, range * 0.6)) * (0.55 + 0.45 * skill);
117
+ const radial = Math.max(-1, Math.min(1, (dist - keep) / 4));
118
+ const flip = h01(v.id ^ salt, Math.floor((tick + v.id * 37) / sec(3.5))) < 0.5 ? 1 : -1;
119
+ const ux = dx / dist;
120
+ const uz = dz / dist;
121
+ mx += ux * radial - uz * flip * 0.85;
122
+ mz += uz * radial + ux * flip * 0.85;
123
+ if (!clear) {
124
+ mx -= uz * flip;
125
+ mz += ux * flip;
126
+ }
127
+ else
128
+ pace = 0.4 + 0.6 * skill;
129
+ // Aim with a per-bot error that drifts every third of a second; a loose bot misses by a lot more.
130
+ const sloppy = 1 + (1 - skill) * (1 + 2.4 * (1 - skill));
131
+ if (tick % sec(0.33) === 0)
132
+ mem.aimError = (h01((v.id * 3 + 1) ^ salt, tick) - 0.5) * (0.06 + 0.16 * h01(v.id ^ salt, 7)) * sloppy;
133
+ yaw = Math.atan2(-dx, -dz) + mem.aimError;
134
+ // React: hold fire for a moment after the target comes into sight (0.2 s sharp, 0.8 s loose).
135
+ const reacted = tick - (mem.sightSince ?? tick) >= sec(0.2 + 0.6 * (1 - skill));
136
+ // Bursts: a loose bot pauses between them (sharp bots fire without a break).
137
+ const cycle = sec(0.9 + 1.4 * (1 - skill));
138
+ const burst = (tick + v.id * 29) % cycle < sec(0.9);
139
+ if (clear && reacted && burst && dist <= range * 0.95)
140
+ buttons |= Button.Fire;
141
+ }
142
+ else {
143
+ // Nobody to fight: drift around the middle.
144
+ const a = (tick / sec(8) + v.id) % (Math.PI * 2);
145
+ mx = Math.cos(a) * 6 - v.x;
146
+ mz = Math.sin(a) * 6 - v.z;
147
+ }
148
+ // Hurt: go for the nearest heal cell within 20 m.
149
+ if (v.health < v.maxHealth * 0.45) {
150
+ let cell = null;
151
+ for (const p of v.pickups) {
152
+ const d = Math.hypot(p.x - v.x, p.z - v.z);
153
+ if (d < 20 && (!cell || d < cell.d))
154
+ cell = { x: p.x, z: p.z, d };
155
+ }
156
+ if (cell) {
157
+ mx = ((cell.x - v.x) / Math.max(0.5, cell.d)) * 2 + mx * 0.2;
158
+ mz = ((cell.z - v.z) / Math.max(0.5, cell.d)) * 2 + mz * 0.2;
159
+ }
160
+ }
161
+ // Keep off pillars and walls.
162
+ for (const p of v.pillars) {
163
+ const ox = v.x - p.x;
164
+ const oz = v.z - p.z;
165
+ const d = Math.hypot(ox, oz);
166
+ const reach = p.radius + 2;
167
+ if (d < reach && d > 1e-6) {
168
+ mx += (ox / d) * (reach - d);
169
+ mz += (oz / d) * (reach - d);
170
+ }
171
+ }
172
+ const edge = v.half - 3;
173
+ if (v.x > edge)
174
+ mx -= (v.x - edge) * 0.8;
175
+ if (v.x < -edge)
176
+ mx -= (v.x + edge) * 0.8;
177
+ if (v.z > edge)
178
+ mz -= (v.z - edge) * 0.8;
179
+ if (v.z < -edge)
180
+ mz -= (v.z + edge) * 0.8;
181
+ // Dodge: dash when hit hard, and now and then (a loose bot rarely thinks to).
182
+ if ((skill >= 0.4 && mem.lastHealth - v.health >= 25) || (skill >= 0.5 && (tick + v.id * 13) % sec(5.8) === 0))
183
+ buttons |= Button.Sprint;
184
+ mem.lastHealth = v.health;
185
+ const len = Math.hypot(mx, mz) || 1;
186
+ // Input frame (yaw 0): moveX is +x, moveZ is -z.
187
+ return { seq: 0, tick, moveX: (mx / len) * pace, moveZ: (-mz / len) * pace, yaw, buttons };
188
+ }
@@ -0,0 +1,95 @@
1
+ import type { InputCommand } from "@netforge/engine-sim";
2
+ export interface BotView {
3
+ readonly x: number;
4
+ readonly z: number;
5
+ /** The player's packed `choices` field (offers pending when non-zero). */
6
+ readonly choices: number;
7
+ /** Arena half extent, m. */
8
+ readonly half: number;
9
+ }
10
+ /** One tick of input. `enemies` are nearby enemy positions [x, z]; `id` varies the bot's path. */
11
+ export declare function botInput(me: BotView, enemies: Iterable<readonly [number, number]>, tick: number, id: number): InputCommand;
12
+ /** A danger the bot can see coming: a telegraphed area that lands soon (circle, or a lane from (x, z) along (dx, dz)). */
13
+ export interface BotDanger {
14
+ readonly x: number;
15
+ readonly z: number;
16
+ /** Circle radius, or lane half width, m. */
17
+ readonly r: number;
18
+ /** Lane: unit direction and length; omit for a circle. */
19
+ readonly dx?: number;
20
+ readonly dz?: number;
21
+ readonly len?: number;
22
+ }
23
+ /** What a human-like bot sees this tick (the caller passes the present; the bot reacts to it late). */
24
+ export interface HumanBotView {
25
+ readonly x: number;
26
+ readonly z: number;
27
+ readonly up: boolean;
28
+ /** The player's packed `choices` field. */
29
+ readonly choices: number;
30
+ /** The player's packed weapon slots (to pick upgrades greedily). */
31
+ readonly weapons: number;
32
+ /** Arena half extent, m. */
33
+ readonly half: number;
34
+ /** Living enemies on (or just off) screen. */
35
+ readonly enemies: readonly (readonly [number, number])[];
36
+ /** Gems and pickups on screen. */
37
+ readonly gems: readonly (readonly [number, number])[];
38
+ readonly pillars: readonly {
39
+ readonly x: number;
40
+ readonly z: number;
41
+ readonly radius: number;
42
+ }[];
43
+ /** Telegraphed attacks about to land. */
44
+ readonly dangers?: readonly BotDanger[];
45
+ /** How far the player's weapons reach, m (the bot orbits about there; default 5). */
46
+ readonly reach?: number;
47
+ /** Bosses on screen: players focus them, circling at their weapons' reach. */
48
+ readonly bosses?: readonly (readonly [number, number])[];
49
+ /** Teammates who are down: players go and stand next to them to revive. */
50
+ readonly downed?: readonly (readonly [number, number])[];
51
+ }
52
+ export interface HumanBotOptions {
53
+ /** Ticks between seeing something and acting on it (6 ticks = 200 ms at 30 Hz). */
54
+ readonly reactionTicks?: number;
55
+ /** "random": any offer; "greedy": level owned weapons, then new weapons, then passives. */
56
+ readonly picks?: "random" | "greedy";
57
+ /** Number of weapons the game declares (offer indices below it are weapons). */
58
+ readonly weaponCount?: number;
59
+ /** Weapon indices that are evolutions (greedy picks always take them). */
60
+ readonly evolved?: readonly number[];
61
+ /** Evolution pairings: [weapon index, offer index of the passive it needs]. Greedy picks value a passive an owned weapon needs. */
62
+ readonly pairs?: readonly (readonly [number, number])[];
63
+ /** Varies the bot's path and picks. */
64
+ readonly seed?: number;
65
+ }
66
+ /**
67
+ * A playtest bot that plays like a person, for fun metrics
68
+ * (GAME-DESIGN.md "measure fun with bots"): it reacts about 200 ms late,
69
+ * kites in wide arcs instead of circling the centre, walks over gems when
70
+ * it is safe, dashes when something is about to touch it or a telegraph is
71
+ * about to land under it, and takes a second or so to read each card.
72
+ * Template-agnostic: it needs positions only. Deterministic for a given
73
+ * input sequence (its own xorshift, never the sim's PRNG).
74
+ */
75
+ export declare class HumanBot {
76
+ private readonly delay;
77
+ private readonly picks;
78
+ private readonly nWeapons;
79
+ private readonly evolved;
80
+ private readonly pairs;
81
+ private s;
82
+ private readonly seen;
83
+ private arc;
84
+ private heading;
85
+ private downSince;
86
+ private cardSince;
87
+ private cardWait;
88
+ private lastDash;
89
+ private lastTurn;
90
+ constructor(opts?: HumanBotOptions);
91
+ private rnd;
92
+ /** One tick of input from what the player sees now. */
93
+ input(now: HumanBotView, tick: number): InputCommand;
94
+ private choose;
95
+ }