@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,732 @@
1
+ import { collectorContent, ELEMENTS } from "../api/collector.js";
2
+ import { entity, rand, randInt, RulesKind, UNITS } from "./kit.js";
3
+ /** Metres per tile. */
4
+ export const MARSH_TILE = 2;
5
+ /** Fixed units per tile. */
6
+ const TU = MARSH_TILE * UNITS;
7
+ /** Avatar radius, fixed units (0.4 m). */
8
+ const R = Math.round(0.4 * UNITS);
9
+ /** Actions (RulesInput.act). */
10
+ export const CollectorAct = { Pick: 1, Move: 2, Swap: 3, Snare: 4, Flee: 5, Challenge: 6 };
11
+ /** What a battle log line says happened. */
12
+ export const LogKind = { Hit: 1, Miss: 2, Heal: 3, Guard: 4, Swap: 5, Snared: 6, Slipped: 7, Fled: 8, Won: 9, Fainted: 10, Wiped: 11, Level: 12, Met: 13, Warden: 14 };
13
+ export function compileCollector(m) {
14
+ const c = collectorContent(m);
15
+ const mm = c.marsh;
16
+ const moves = c.moves.map(([id, d]) => ({ ...d, id, el: ELEMENTS.indexOf(d.element) }));
17
+ const moveIdx = new Map(moves.map((mv, i) => [mv.id, i]));
18
+ const creatures = c.creatures.map(([id, d]) => ({ ...d, id, el: ELEMENTS.indexOf(d.element), moveIdx: d.moves.map((x) => moveIdx.get(x) ?? 0) }));
19
+ const cIdx = new Map(creatures.map((x, i) => [x.id, i]));
20
+ const [w, h] = mm.size;
21
+ const blocked = new Uint8Array(w * h);
22
+ const water = new Uint8Array(w * h);
23
+ const grass = new Uint8Array(w * h);
24
+ const fill = (arr, [x, y, rw, rh], v) => {
25
+ for (let j = y; j < y + rh && j < h; j++)
26
+ for (let i = x; i < x + rw && i < w; i++)
27
+ arr[j * w + i] = v;
28
+ };
29
+ for (const r of mm.blocked)
30
+ fill(blocked, r, 1);
31
+ for (const r of mm.water) {
32
+ fill(water, r, 1);
33
+ fill(blocked, r, 1);
34
+ }
35
+ mm.grass.forEach((g, gi) => fill(grass, g.at, gi + 1));
36
+ return {
37
+ marsh: mm,
38
+ map: { w, h, blocked, water, grass, camp: mm.camp, warden: mm.warden.at },
39
+ creatures,
40
+ moves,
41
+ starters: mm.starters.map((s) => cIdx.get(s) ?? 0),
42
+ wardenSpecies: cIdx.get(mm.warden.creature) ?? 0,
43
+ pools: mm.grass.map((g) => ({ rate: g.rate, pool: g.pool.map((p) => ({ species: cIdx.get(p.creature) ?? 0, weight: p.weight, level: p.level })) })),
44
+ };
45
+ }
46
+ /** Strong (1), weak (-1) or plain (0): each element beats the next in the cycle. */
47
+ export function effectiveness(attack, defend) {
48
+ if ((attack + 1) % 4 === defend)
49
+ return 1;
50
+ if ((defend + 1) % 4 === attack)
51
+ return -1;
52
+ return 0;
53
+ }
54
+ /** A stat at a level: +12% a level. */
55
+ export const statAt = (base, level) => Math.floor(base * (1 + 0.12 * (level - 1)));
56
+ /** Max health at a level. */
57
+ export const hpAt = (base, level) => statAt(base, level) + level * 2;
58
+ /** XP to the next level. */
59
+ export const xpNeed = (level) => level * 15 + 10;
60
+ /** The tile at a fixed-unit point (marsh centred on the origin), or -1 off the map. */
61
+ export function marshTile(map, x, z) {
62
+ const tx = Math.floor(x / TU + map.w / 2);
63
+ const ty = Math.floor(z / TU + map.h / 2);
64
+ return tx >= 0 && ty >= 0 && tx < map.w && ty < map.h ? ty * map.w + tx : -1;
65
+ }
66
+ /** A tile's centre in fixed units. */
67
+ export function marshCenter(map, tx, ty) {
68
+ return { x: Math.round((tx + 0.5 - map.w / 2) * TU), z: Math.round((ty + 0.5 - map.h / 2) * TU) };
69
+ }
70
+ /** Tiles from `from` to `goal` (excluding `from`), around blocked tiles; empty when there is no way. */
71
+ export function marshRoute(map, from, goal) {
72
+ const prev = new Int32Array(map.w * map.h).fill(-2);
73
+ const q = [from];
74
+ prev[from] = -1;
75
+ for (let qi = 0; qi < q.length; qi++) {
76
+ const c = q[qi];
77
+ if (c === goal)
78
+ break;
79
+ const cx = c % map.w;
80
+ const cy = Math.floor(c / map.w);
81
+ for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
82
+ const nx = cx + dx;
83
+ const ny = cy + dy;
84
+ if (nx < 0 || ny < 0 || nx >= map.w || ny >= map.h)
85
+ continue;
86
+ const n = ny * map.w + nx;
87
+ if (prev[n] !== -2 || (map.blocked[n] && n !== goal))
88
+ continue;
89
+ prev[n] = c;
90
+ q.push(n);
91
+ }
92
+ }
93
+ if (prev[goal] === -2)
94
+ return [];
95
+ const out = [];
96
+ for (let c = goal; c !== from && c >= 0; c = prev[c])
97
+ out.push(c);
98
+ return out.reverse();
99
+ }
100
+ const TICK = 30;
101
+ export function collectorGenre(m) {
102
+ const g = compileCollector(m);
103
+ const map = g.map;
104
+ const mm = g.marsh;
105
+ const tickRate = TICK;
106
+ const step = Math.max(1, Math.round((mm.walk * UNITS) / tickRate));
107
+ const halfW = (map.w * TU) / 2;
108
+ const halfH = (map.h * TU) / 2;
109
+ /** True when a circle at (x, z) touches a blocked tile or the marsh's edge. */
110
+ const solid = (x, z) => {
111
+ if (x - R < -halfW || x + R > halfW || z - R < -halfH || z + R > halfH)
112
+ return true;
113
+ const x0 = Math.floor((x - R + halfW) / TU);
114
+ const x1 = Math.floor((x + R - 1 + halfW) / TU);
115
+ const z0 = Math.floor((z - R + halfH) / TU);
116
+ const z1 = Math.floor((z + R - 1 + halfH) / TU);
117
+ for (let j = z0; j <= z1; j++)
118
+ for (let i = x0; i <= x1; i++)
119
+ if (i >= 0 && j >= 0 && i < map.w && j < map.h && map.blocked[j * map.w + i])
120
+ return true;
121
+ return false;
122
+ };
123
+ const humans = (s) => [...s.keepers.keys()].filter((p) => !s.ctx.isBot(p));
124
+ const sp = (mon) => g.creatures[mon.species] ?? g.creatures[0];
125
+ const maxHp = (s, p, mon, foe = false) => Math.max(1, Math.floor(hpAt(sp(mon).hp, mon.level) * (foe ? 1 : 1 + s.ctx.bonus(p, "maxHealth"))));
126
+ const atk = (s, p, mon, foe = false) => statAt(sp(mon).atk, mon.level) * (foe ? 1 : 1 + s.ctx.bonus(p, "might"));
127
+ const def = (s, p, mon, foe = false) => statAt(sp(mon).def, mon.level) * (foe ? 1 : 1 + s.ctx.bonus(p, "armor"));
128
+ const spd = (mon) => statAt(sp(mon).spd, mon.level);
129
+ const newMon = (species, level) => {
130
+ const c = g.creatures[species] ?? g.creatures[0];
131
+ return { species, level, hp: hpAt(c.hp, level), xp: 0, guard: false };
132
+ };
133
+ const log = (k, line) => {
134
+ k.logN++;
135
+ k.log.push({ n: k.logN, ...line });
136
+ if (k.log.length > 12)
137
+ k.log.shift();
138
+ };
139
+ /** One strike. Returns the damage dealt. */
140
+ const strike = (s, p, k, user, target, moveI, who, foeSide) => {
141
+ const mv = g.moves[moveI];
142
+ if (!mv)
143
+ return 0;
144
+ if (mv.effect === "heal") {
145
+ const max = foeSide ? maxHp(s, p, user, true) : maxHp(s, p, user);
146
+ const heal = Math.max(1, Math.floor(max * mv.amount));
147
+ user.hp = Math.min(max, user.hp + heal);
148
+ log(k, { kind: LogKind.Heal, who, move: moveI, value: heal });
149
+ return 0;
150
+ }
151
+ if (mv.effect === "guard")
152
+ user.guard = true;
153
+ if (mv.power <= 0) {
154
+ log(k, { kind: LogKind.Guard, who, move: moveI });
155
+ return 0;
156
+ }
157
+ if (rand(s.rng) > mv.accuracy) {
158
+ log(k, { kind: LogKind.Miss, who, move: moveI });
159
+ return 0;
160
+ }
161
+ // Wild creatures hit at 80%: a fair fight is one a keeper usually wins (the warden hits at full strength).
162
+ const wild = foeSide && !(k.battle?.warden ?? false);
163
+ const a = (foeSide ? atk(s, p, user, true) : atk(s, p, user)) * (wild ? 0.8 : 1);
164
+ const d = who === 0 ? def(s, p, target, true) : def(s, p, target);
165
+ const eff = effectiveness(mv.el, sp(target).el);
166
+ let dmg = Math.floor((((2 * user.level) / 5 + 2) * mv.power * a) / Math.max(1, d) / 8) + 2;
167
+ dmg = Math.floor(dmg * (eff > 0 ? 1.5 : eff < 0 ? 0.67 : 1) * (0.85 + rand(s.rng) * 0.15));
168
+ if (target.guard) {
169
+ dmg = Math.floor(dmg / 2);
170
+ target.guard = false;
171
+ }
172
+ dmg = Math.max(1, dmg);
173
+ target.hp = Math.max(0, target.hp - dmg);
174
+ if (mv.effect === "drain") {
175
+ const max = foeSide ? maxHp(s, p, user, true) : maxHp(s, p, user);
176
+ user.hp = Math.min(max, user.hp + Math.floor(dmg / 2));
177
+ }
178
+ log(k, { kind: LogKind.Hit, who, move: moveI, dmg, eff });
179
+ s.activity++;
180
+ return dmg;
181
+ };
182
+ /** The foe's move: the strongest it has against your creature, with a little chance. */
183
+ const foePick = (s, foe, mine) => {
184
+ const moves = sp(foe).moveIdx;
185
+ let best = moves[0] ?? 0;
186
+ let score = -1;
187
+ for (const mi of moves) {
188
+ const mv = g.moves[mi];
189
+ if (!mv)
190
+ continue;
191
+ const eff = effectiveness(mv.el, sp(mine).el);
192
+ const v = (mv.power || 15) * (eff > 0 ? 1.5 : eff < 0 ? 0.67 : 1) * mv.accuracy * (0.7 + rand(s.rng) * 0.6);
193
+ if (v > score) {
194
+ score = v;
195
+ best = mi;
196
+ }
197
+ }
198
+ return best;
199
+ };
200
+ const endBattle = (k) => {
201
+ k.battle = null;
202
+ k.cooldown = Math.round(3 * tickRate);
203
+ for (const mon of k.team)
204
+ mon.guard = false;
205
+ };
206
+ const gainXp = (s, p, k, foe, emit) => {
207
+ const xp = foe.level * 10;
208
+ k.team.forEach((mon, i) => {
209
+ if (mon.hp <= 0)
210
+ return;
211
+ mon.xp += i === k.active ? xp : Math.floor(xp / 2);
212
+ while (mon.xp >= xpNeed(mon.level) && mon.level < 50) {
213
+ mon.xp -= xpNeed(mon.level);
214
+ const before = maxHp(s, p, mon);
215
+ mon.level++;
216
+ mon.hp += maxHp(s, p, mon) - before;
217
+ log(k, { kind: LogKind.Level, who: 0, value: mon.level });
218
+ emit({ name: "creature.levelup", players: [p], own: true });
219
+ if (mon.level === 5)
220
+ emit({ name: "level.five", players: [p], own: true });
221
+ if (mon.level === 10)
222
+ emit({ name: "level.ten", players: [p], own: true });
223
+ }
224
+ });
225
+ };
226
+ /** Your creature fainted: the next one steps in, or the whole team is down and goes home. */
227
+ const afterFaint = (s, p, k, emit) => {
228
+ const mine = k.team[k.active];
229
+ if (!mine || mine.hp > 0)
230
+ return false;
231
+ log(k, { kind: LogKind.Fainted, who: 0, value: mine.species });
232
+ const next = k.team.findIndex((x) => x.hp > 0);
233
+ if (next >= 0) {
234
+ k.active = next;
235
+ log(k, { kind: LogKind.Swap, who: 0, value: k.team[next]?.species ?? 0 });
236
+ return false;
237
+ }
238
+ k.wipes++;
239
+ s.wipes++;
240
+ log(k, { kind: LogKind.Wiped, who: 0 });
241
+ emit({ name: "team.fainted", players: [p], own: true, unpaid: true });
242
+ for (const x of k.team)
243
+ x.hp = Math.max(1, Math.floor(maxHp(s, p, x) / 2));
244
+ k.active = 0;
245
+ k.toCamp = true;
246
+ endBattle(k);
247
+ s.activity++;
248
+ return true;
249
+ };
250
+ /** The foe strikes back (it is alive, you are too). */
251
+ const foeTurn = (s, p, k, emit) => {
252
+ const b = k.battle;
253
+ const mine = k.team[k.active];
254
+ if (!b || !mine)
255
+ return;
256
+ const foe = b.foe;
257
+ strike(s, p, k, foe, mine, foePick(s, foe, mine), 1, true);
258
+ afterFaint(s, p, k, emit);
259
+ };
260
+ /** The foe is down: XP, the win, and for the warden the whole marsh's win. */
261
+ const foeDown = (s, p, k, emit) => {
262
+ const b = k.battle;
263
+ if (!b)
264
+ return;
265
+ if (b.warden)
266
+ return; // the warden's fall is settled in step for everyone at once
267
+ k.wins++;
268
+ s.battlesWon++;
269
+ log(k, { kind: LogKind.Won, who: 0, value: b.foe.species });
270
+ gainXp(s, p, k, b.foe, emit);
271
+ emit({ name: "creature.defeated", players: [p], own: true });
272
+ endBattle(k);
273
+ };
274
+ const startBattle = (s, k, foe, warden) => {
275
+ k.battle = { foe, warden, turn: 0 };
276
+ k.log = [];
277
+ log(k, { kind: warden ? LogKind.Warden : LogKind.Met, who: 1, value: foe.species });
278
+ s.activity++;
279
+ if (k.team[k.active]?.hp === 0)
280
+ k.active = Math.max(0, k.team.findIndex((x) => x.hp > 0));
281
+ };
282
+ /** A wild creature from the grass zone's pool. */
283
+ const roll = (s, zone) => {
284
+ const z = g.pools[zone];
285
+ if (!z)
286
+ return null;
287
+ const total = z.pool.reduce((a, x) => a + x.weight, 0);
288
+ let r = rand(s.rng) * total;
289
+ for (const x of z.pool) {
290
+ r -= x.weight;
291
+ if (r <= 0)
292
+ return newMon(x.species, x.level[0] + randInt(s.rng, x.level[1] - x.level[0] + 1));
293
+ }
294
+ const last = z.pool[z.pool.length - 1];
295
+ return last ? newMon(last.species, last.level[0]) : null;
296
+ };
297
+ const wardenFoe = (s) => ({ species: g.wardenSpecies, level: mm.warden.level, hp: Math.max(0, s.wardenHp), xp: 0, guard: false });
298
+ const campC = marshCenter(map, map.camp[0], map.camp[1]);
299
+ const wardenC = marshCenter(map, map.warden[0], map.warden[1]);
300
+ // Bot memory: the path each playtest player follows (harness only; seat bots are not used here).
301
+ const paths = new Map();
302
+ const bfs = (from, goal) => marshRoute(map, from, goal);
303
+ return {
304
+ template: "collector",
305
+ tickRate,
306
+ endSeconds: 8,
307
+ infoEveryTicks: 3,
308
+ game: g,
309
+ create(seed, ctx) {
310
+ return {
311
+ rng: { s: seed | 0 },
312
+ ctx,
313
+ tick: 0,
314
+ dusk: Math.round(mm.dusk * tickRate),
315
+ wardenHp: mm.warden.hp,
316
+ wardenMax: mm.warden.hp,
317
+ wardenDown: false,
318
+ wardenHurt: new Set(),
319
+ keepers: new Map(),
320
+ activity: 0,
321
+ battlesWon: 0,
322
+ catches: 0,
323
+ wipes: 0,
324
+ peakBattles: 0,
325
+ };
326
+ },
327
+ spawn(_s, player) {
328
+ const k = player % 5;
329
+ return { x: campC.x + (k - 2) * Math.round(0.5 * UNITS), z: campC.z + Math.round(0.6 * UNITS) };
330
+ },
331
+ moveAvatar(a, i) {
332
+ let mx = Math.max(-127, Math.min(127, i.mx));
333
+ let mz = Math.max(-127, Math.min(127, i.mz));
334
+ if (mx === 0 && mz === 0)
335
+ return;
336
+ const len2 = mx * mx + mz * mz;
337
+ if (len2 > 127 * 127) {
338
+ const k = 127 / Math.sqrt(len2);
339
+ mx = Math.round(mx * k);
340
+ mz = Math.round(mz * k);
341
+ }
342
+ const dx = Math.round((mx * step) / 127);
343
+ const dz = Math.round((mz * step) / 127);
344
+ if (dx !== 0 && !solid(a.x + dx, a.z))
345
+ a.x += dx;
346
+ if (dz !== 0 && !solid(a.x, a.z + dz))
347
+ a.z += dz;
348
+ a.yaw = Math.round((Math.atan2(-mx, -mz) / (Math.PI * 2)) * 65536) & 0xffff;
349
+ },
350
+ join(s, player) {
351
+ s.keepers.set(player, {
352
+ team: [],
353
+ active: 0,
354
+ box: 0,
355
+ snares: mm.snares,
356
+ snaresBought: 0,
357
+ battle: null,
358
+ log: [],
359
+ logN: 0,
360
+ lastTile: -1,
361
+ cooldown: 0,
362
+ toCamp: false,
363
+ catches: 0,
364
+ wins: 0,
365
+ wipes: 0,
366
+ wardenDamage: 0,
367
+ x: campC.x,
368
+ z: campC.z,
369
+ yaw: 0,
370
+ joinedTick: s.tick,
371
+ });
372
+ // The warden grows with the marsh's crowd: +50% health a player past the first.
373
+ if (!s.ctx.isBot(player) && humans(s).length > 1 && !s.wardenDown) {
374
+ const add = Math.floor(mm.warden.hp * 0.5);
375
+ s.wardenMax += add;
376
+ s.wardenHp += add;
377
+ }
378
+ s.activity++;
379
+ },
380
+ leave(s, player) {
381
+ s.keepers.delete(player);
382
+ paths.delete(player);
383
+ },
384
+ act(s, player, i, _a, emit) {
385
+ const k = s.keepers.get(player);
386
+ if (!k)
387
+ return;
388
+ if (i.act === CollectorAct.Pick) {
389
+ if (k.team.length > 0)
390
+ return;
391
+ const species = g.starters[i.a];
392
+ if (species === undefined)
393
+ return;
394
+ k.team.push(newMon(species, mm.starterLevel));
395
+ s.activity++;
396
+ emit({ name: "starter.picked", players: [player], own: true, unpaid: true });
397
+ return;
398
+ }
399
+ if (i.act === CollectorAct.Challenge) {
400
+ if (k.battle || k.team.length === 0 || s.wardenDown || !k.team.some((x) => x.hp > 0))
401
+ return;
402
+ const d2 = (k.x - wardenC.x) ** 2 + (k.z - wardenC.z) ** 2;
403
+ if (d2 > (3 * TU) ** 2)
404
+ return;
405
+ startBattle(s, k, wardenFoe(s), true);
406
+ return;
407
+ }
408
+ const b = k.battle;
409
+ const mine = k.team[k.active];
410
+ if (!b || !mine)
411
+ return;
412
+ b.turn++;
413
+ if (b.warden)
414
+ b.foe.hp = Math.max(0, s.wardenHp);
415
+ if (i.act === CollectorAct.Flee) {
416
+ log(k, { kind: LogKind.Fled, who: 0 });
417
+ endBattle(k);
418
+ s.activity++;
419
+ return;
420
+ }
421
+ if (i.act === CollectorAct.Swap) {
422
+ const to = k.team[i.a];
423
+ if (!to || to.hp <= 0 || i.a === k.active)
424
+ return;
425
+ k.active = i.a;
426
+ log(k, { kind: LogKind.Swap, who: 0, value: to.species });
427
+ foeTurn(s, player, k, emit);
428
+ return;
429
+ }
430
+ if (i.act === CollectorAct.Snare) {
431
+ if (b.warden || k.snares <= 0)
432
+ return;
433
+ k.snares--;
434
+ const foe = b.foe;
435
+ const max = maxHp(s, player, foe, true);
436
+ const chance = Math.min(0.95, sp(foe).catchRate * (0.25 + 0.75 * (1 - foe.hp / max)));
437
+ s.activity++;
438
+ if (rand(s.rng) < chance) {
439
+ k.catches++;
440
+ s.catches++;
441
+ log(k, { kind: LogKind.Snared, who: 0, value: foe.species });
442
+ if (k.team.length < 3)
443
+ k.team.push({ ...foe, guard: false });
444
+ else
445
+ k.box++;
446
+ emit({ name: "creature.caught", players: [player], own: true });
447
+ endBattle(k);
448
+ return;
449
+ }
450
+ log(k, { kind: LogKind.Slipped, who: 0, value: foe.species });
451
+ foeTurn(s, player, k, emit);
452
+ return;
453
+ }
454
+ if (i.act === CollectorAct.Move) {
455
+ const mi = sp(mine).moveIdx[i.a];
456
+ if (mi === undefined)
457
+ return;
458
+ const foe = b.foe;
459
+ const meFirst = spd(mine) >= spd(foe);
460
+ const mineGo = () => {
461
+ const dmg = strike(s, player, k, mine, foe, mi, 0, false);
462
+ if (b.warden && dmg > 0) {
463
+ s.wardenHp = Math.max(0, s.wardenHp - dmg);
464
+ k.wardenDamage += dmg;
465
+ s.wardenHurt.add(player);
466
+ foe.hp = s.wardenHp;
467
+ }
468
+ };
469
+ if (meFirst) {
470
+ mineGo();
471
+ if (foe.hp <= 0)
472
+ return foeDown(s, player, k, emit);
473
+ foeTurn(s, player, k, emit);
474
+ }
475
+ else {
476
+ foeTurn(s, player, k, emit);
477
+ if (!k.battle || mine.hp <= 0)
478
+ return;
479
+ mineGo();
480
+ if (foe.hp <= 0)
481
+ foeDown(s, player, k, emit);
482
+ }
483
+ }
484
+ },
485
+ tend(s, player, a, emit) {
486
+ const k = s.keepers.get(player);
487
+ if (!k)
488
+ return;
489
+ if (k.toCamp) {
490
+ k.toCamp = false;
491
+ a.x = campC.x;
492
+ a.z = campC.z;
493
+ }
494
+ k.x = a.x;
495
+ k.z = a.z;
496
+ k.yaw = a.yaw;
497
+ if (k.cooldown > 0)
498
+ k.cooldown--;
499
+ // Snares bought in the store (the "amount" upgrade): added once.
500
+ const want = Math.floor(s.ctx.bonus(player, "amount"));
501
+ if (want > k.snaresBought) {
502
+ k.snares += want - k.snaresBought;
503
+ k.snaresBought = want;
504
+ }
505
+ if (k.battle || k.team.length === 0)
506
+ return;
507
+ const tile = marshTile(map, a.x, a.z);
508
+ // Rest at camp, and the regen upgrade anywhere out of battle: once a second.
509
+ if (s.tick % tickRate === 0) {
510
+ const atCamp = Math.abs(a.x - campC.x) <= TU * 1.5 && Math.abs(a.z - campC.z) <= TU * 1.5;
511
+ const regen = s.ctx.bonus(player, "regen");
512
+ for (const mon of k.team) {
513
+ const max = maxHp(s, player, mon);
514
+ const heal = Math.floor(max * ((atCamp ? 0.2 : 0) + regen));
515
+ if (heal > 0 && mon.hp < max) {
516
+ mon.hp = Math.min(max, mon.hp + heal);
517
+ s.activity++;
518
+ }
519
+ }
520
+ }
521
+ if (tile === k.lastTile)
522
+ return;
523
+ k.lastTile = tile;
524
+ s.activity++;
525
+ const zone = tile >= 0 ? (map.grass[tile] ?? 0) : 0;
526
+ if (zone === 0 || k.cooldown > 0 || !k.team.some((x) => x.hp > 0))
527
+ return;
528
+ const z = g.pools[zone - 1];
529
+ if (!z || rand(s.rng) >= z.rate)
530
+ return;
531
+ const foe = roll(s, zone - 1);
532
+ if (foe)
533
+ startBattle(s, k, foe, false);
534
+ void emit;
535
+ },
536
+ step(s, emit) {
537
+ s.tick++;
538
+ if (s.dusk > 0)
539
+ s.dusk--;
540
+ let battles = 0;
541
+ for (const k of s.keepers.values())
542
+ if (k.battle)
543
+ battles++;
544
+ s.peakBattles = Math.max(s.peakBattles, battles);
545
+ if (!s.wardenDown && s.wardenHp <= 0) {
546
+ s.wardenDown = true;
547
+ s.activity++;
548
+ const hurt = [...s.wardenHurt].filter((p) => s.keepers.has(p));
549
+ for (const p of hurt) {
550
+ const k = s.keepers.get(p);
551
+ if (!k)
552
+ continue;
553
+ if (k.battle?.warden) {
554
+ k.wins++;
555
+ log(k, { kind: LogKind.Won, who: 0, value: g.wardenSpecies });
556
+ gainXp(s, p, k, wardenFoe(s), emit);
557
+ endBattle(k);
558
+ }
559
+ }
560
+ for (const k of s.keepers.values())
561
+ if (k.battle?.warden)
562
+ endBattle(k);
563
+ // Everyone who hurt the warden shares its fall; the run is won for the whole marsh.
564
+ if (hurt.length > 0)
565
+ emit({ name: "warden.defeated", players: hurt, own: true });
566
+ emit({ name: "run.won", players: humans(s) });
567
+ }
568
+ },
569
+ outcome(s) {
570
+ if (s.wardenDown)
571
+ return "won";
572
+ return s.dusk <= 0 ? "lost" : null;
573
+ },
574
+ view(s, player) {
575
+ const k = player !== null ? s.keepers.get(player) : undefined;
576
+ const pub = {
577
+ genre: "collector",
578
+ dusk: Math.ceil(s.dusk / tickRate),
579
+ warden: { hp: Math.max(0, s.wardenHp), max: s.wardenMax, down: s.wardenDown, fighting: [...s.keepers.values()].filter((x) => x.battle?.warden).length },
580
+ players: [...s.keepers].map(([id, x]) => ({ id, catches: x.catches, level: x.team.reduce((a, m) => Math.max(a, m.level), 0), battling: x.battle !== null })),
581
+ };
582
+ if (!k || player === null)
583
+ return pub;
584
+ const mon = (x, foe = false) => ({ species: x.species, level: x.level, hp: x.hp, max: maxHp(s, player, x, foe), xp: x.xp, next: xpNeed(x.level) });
585
+ return {
586
+ ...pub,
587
+ team: k.team.map((x) => mon(x)),
588
+ active: k.active,
589
+ box: k.box,
590
+ snares: k.snares,
591
+ picked: k.team.length > 0,
592
+ battle: k.battle ? { foe: mon(k.battle.foe, true), warden: k.battle.warden, turn: k.battle.turn } : null,
593
+ log: k.log,
594
+ catches: k.catches,
595
+ wins: k.wins,
596
+ };
597
+ },
598
+ entities(s, out) {
599
+ let id = 1;
600
+ for (const [p, k] of s.keepers) {
601
+ const mine = k.team[k.active];
602
+ // The active creature walks a step behind its keeper; in battle it faces the foe beside them.
603
+ const yaw = (k.yaw / 65536) * Math.PI * 2;
604
+ const bx = -Math.sin(yaw);
605
+ const bz = -Math.cos(yaw);
606
+ if (mine) {
607
+ const off = k.battle ? 0.9 : -1.1;
608
+ out.push(entity(id, RulesKind.Unit, p, Math.round(k.x + bx * off * UNITS + bz * 0.7 * UNITS), Math.round(k.z + bz * off * UNITS - bx * 0.7 * UNITS), k.yaw, mine.species, mine.hp, mine.hp > 0 ? 0 : 1, mine.level, 0));
609
+ }
610
+ id++;
611
+ if (k.battle && !k.battle.warden) {
612
+ const foe = k.battle.foe;
613
+ out.push(entity(id, RulesKind.Unit, p, Math.round(k.x + bx * 2.6 * UNITS), Math.round(k.z + bz * 2.6 * UNITS), (k.yaw + 32768) & 0xffff, foe.species, foe.hp, 2, foe.level, 0));
614
+ }
615
+ id++;
616
+ }
617
+ // The warden at its lair: hp in the pool, a down flag once it falls.
618
+ out.push(entity(4000, RulesKind.Structure, 0, wardenC.x, wardenC.z, 32768, g.wardenSpecies, Math.max(0, s.wardenHp), s.wardenDown ? 1 : 0, Math.round((Math.max(0, s.wardenHp) / Math.max(1, s.wardenMax)) * 1000), 0));
619
+ },
620
+ bot(s, player, a, tick, skill) {
621
+ const k = s.keepers.get(player);
622
+ const idle = { mx: 0, mz: 0, cx: 0, cz: 0, act: 0, a: 0, b: 0, c: 0 };
623
+ if (!k)
624
+ return idle;
625
+ if (k.team.length === 0)
626
+ return { ...idle, act: CollectorAct.Pick, a: player % g.starters.length };
627
+ const b = k.battle;
628
+ const period = Math.max(2, Math.round(tickRate * (skill >= 1 ? 0.4 : 1.1)));
629
+ if (b) {
630
+ if ((tick + player) % period !== 0)
631
+ return idle;
632
+ const mine = k.team[k.active];
633
+ if (!mine)
634
+ return idle;
635
+ const max = maxHp(s, player, mine);
636
+ const healthier = k.team.findIndex((x, i) => i !== k.active && x.hp > maxHp(s, player, x) * 0.6);
637
+ if (mine.hp < max * 0.25 && healthier >= 0)
638
+ return { ...idle, act: CollectorAct.Swap, a: healthier };
639
+ const total = k.team.reduce((x, m) => x + m.hp, 0);
640
+ const totalMax = k.team.reduce((x, m) => x + maxHp(s, player, m), 0);
641
+ // A keeper on their last legs runs from a wild fight (the warden's is worth staying in).
642
+ if (!b.warden && total < totalMax * 0.3 && mine.hp < max * 0.4 && skill >= 0.5)
643
+ return { ...idle, act: CollectorAct.Flee };
644
+ const foeMax = maxHp(s, player, b.foe, true);
645
+ if (!b.warden && k.snares > 0 && b.foe.hp < foeMax * 0.5)
646
+ return { ...idle, act: CollectorAct.Snare };
647
+ const moves = sp(mine).moveIdx;
648
+ let best = 0;
649
+ let score = -1;
650
+ moves.forEach((mi, j) => {
651
+ const mv = g.moves[mi];
652
+ if (!mv)
653
+ return;
654
+ const eff = effectiveness(mv.el, sp(b.foe).el);
655
+ const v = mv.effect === "heal" ? (mine.hp < max * 0.4 ? 999 : 0) : mv.power * (eff > 0 ? 1.5 : eff < 0 ? 0.67 : 1) * mv.accuracy;
656
+ if (v > score) {
657
+ score = v;
658
+ best = j;
659
+ }
660
+ });
661
+ return { ...idle, act: CollectorAct.Move, a: best };
662
+ }
663
+ // Out of battle: the warden once the team is ready or dusk is close, home when hurt, else the grass.
664
+ const here = marshTile(map, a.x, a.z);
665
+ const lvl = k.team.reduce((x, m) => Math.max(x, m.level), 0);
666
+ const hurt = k.team.reduce((x, m) => x + m.hp, 0) < k.team.reduce((x, m) => x + maxHp(s, player, m), 0) * 0.45;
667
+ const late = s.dusk < mm.dusk * tickRate * 0.45;
668
+ let goal;
669
+ if (!s.wardenDown && (lvl >= mm.warden.level - 1 || late) && !hurt) {
670
+ goal = map.warden[1] * map.w + map.warden[0];
671
+ const d2 = (a.x - wardenC.x) ** 2 + (a.z - wardenC.z) ** 2;
672
+ if (d2 <= (2.5 * TU) ** 2)
673
+ return { ...idle, act: CollectorAct.Challenge };
674
+ }
675
+ else if (hurt) {
676
+ goal = map.camp[1] * map.w + map.camp[0];
677
+ }
678
+ else {
679
+ // A grass tile, a new one every few seconds so the walk crosses many tiles.
680
+ const tiles = [];
681
+ for (let t = 0; t < map.grass.length; t++)
682
+ if (map.grass[t] && !map.blocked[t])
683
+ tiles.push(t);
684
+ const pick = ((Math.floor(tick / (tickRate * 4)) * 2654435761) ^ (player * 40503)) >>> 0;
685
+ goal = tiles[pick % Math.max(1, tiles.length)] ?? here;
686
+ }
687
+ let mem = paths.get(player);
688
+ if (!mem || mem.goal !== goal || tick >= mem.until || mem.path.length === 0) {
689
+ mem = { goal, path: here >= 0 ? bfs(here, goal) : [], until: tick + tickRate * 3 };
690
+ paths.set(player, mem);
691
+ }
692
+ while (mem.path.length > 0 && mem.path[0] === here)
693
+ mem.path.shift();
694
+ const next = mem.path[0] ?? goal;
695
+ const c = marshCenter(map, next % map.w, Math.floor(next / map.w));
696
+ const dx = c.x - a.x;
697
+ const dz = c.z - a.z;
698
+ const d = Math.sqrt(dx * dx + dz * dz);
699
+ if (d < 4)
700
+ return idle;
701
+ return { ...idle, mx: Math.round((dx / d) * 127), mz: Math.round((dz / d) * 127) };
702
+ },
703
+ stats(s) {
704
+ let top = 0;
705
+ let scorers = 0;
706
+ let level = 0;
707
+ for (const k of s.keepers.values()) {
708
+ top = Math.max(top, k.catches);
709
+ if (k.catches > 0)
710
+ scorers++;
711
+ for (const mon of k.team)
712
+ level = Math.max(level, mon.level);
713
+ }
714
+ return { kills: s.battlesWon + s.catches, wave: 1 + s.catches, level, deaths: s.wipes, peakUnits: s.peakBattles, scorers, topScore: top };
715
+ },
716
+ activity(s) {
717
+ return s.activity;
718
+ },
719
+ checks(run) {
720
+ const min = Math.round(run.seconds / 6) / 10;
721
+ return [
722
+ { ok: run.stats.kills > 0, line: `in ${min} minutes of ${run.players}-player play nobody won a battle: check the grass rates, that the starters' moves have power, and that wild levels are near the starters' level` },
723
+ { ok: run.stats.wave > 1 || run.seconds < 150, line: `nobody caught a creature in ${min} minutes: raise the grass rates or catchRate, or start players with more snares` },
724
+ { ok: run.stats.deaths <= run.players * 2, line: `${run.players} playtest players lost their whole team ${run.stats.deaths} times in ${min} minutes: wild creatures are too strong for the starters (lower their levels or stats)` },
725
+ ];
726
+ },
727
+ progress(run) {
728
+ const p = Math.max(1, run.players);
729
+ return Math.min(1, (run.stats.wave - 1) / (p * 2)) * 0.4 + Math.min(1, run.stats.level / 8) * 0.3 + Math.min(1, run.stats.kills / (p * 6)) * 0.3;
730
+ },
731
+ };
732
+ }