@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
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import { towerContent } from "../api/tower.js";
|
|
2
|
+
import { entity, RulesKind, UNITS } from "./kit.js";
|
|
3
|
+
/** Fixed-point units per tile. */
|
|
4
|
+
export const Q = 256;
|
|
5
|
+
/** Metres per tile. */
|
|
6
|
+
export const TILE_M = 2;
|
|
7
|
+
/** Actions (RulesInput.act). */
|
|
8
|
+
export const TowerAct = { Build: 1, Upgrade: 2, Sell: 3, Ready: 4 };
|
|
9
|
+
/** Creep flags on the wire. */
|
|
10
|
+
export const CreepFlag = { Slowed: 1, Boss: 2, Dead: 4, Leaked: 8 };
|
|
11
|
+
/** Tile key. */
|
|
12
|
+
export const tileKey = (x, y) => y * 64 + x;
|
|
13
|
+
/** A tile's centre in metres, board centred on the origin. */
|
|
14
|
+
export function tileCenter(b, tx, ty) {
|
|
15
|
+
return { x: (tx + 0.5 - b.w / 2) * TILE_M, z: (ty + 0.5 - b.h / 2) * TILE_M };
|
|
16
|
+
}
|
|
17
|
+
/** The tile under a point in metres, or null off the board. */
|
|
18
|
+
export function tileAt(b, x, z) {
|
|
19
|
+
const tx = Math.floor(x / TILE_M + b.w / 2);
|
|
20
|
+
const ty = Math.floor(z / TILE_M + b.h / 2);
|
|
21
|
+
return tx >= 0 && ty >= 0 && tx < b.w && ty < b.h ? { tx, ty } : null;
|
|
22
|
+
}
|
|
23
|
+
export function compileBoard(def) {
|
|
24
|
+
const [w, h] = def.size;
|
|
25
|
+
const road = new Set();
|
|
26
|
+
const cum = [0];
|
|
27
|
+
for (let i = 1; i < def.path.length; i++) {
|
|
28
|
+
const a = def.path[i - 1];
|
|
29
|
+
const b = def.path[i];
|
|
30
|
+
const len = Math.abs(b[0] - a[0]) + Math.abs(b[1] - a[1]);
|
|
31
|
+
cum.push((cum[i - 1] ?? 0) + len * Q);
|
|
32
|
+
const sx = Math.sign(b[0] - a[0]);
|
|
33
|
+
const sy = Math.sign(b[1] - a[1]);
|
|
34
|
+
for (let k = 0; k <= len; k++)
|
|
35
|
+
road.add(tileKey(a[0] + sx * k, a[1] + sy * k));
|
|
36
|
+
}
|
|
37
|
+
if (def.path.length === 1) {
|
|
38
|
+
const p = def.path[0];
|
|
39
|
+
road.add(tileKey(p[0], p[1]));
|
|
40
|
+
}
|
|
41
|
+
const blocked = new Set(def.blocked.map(([x, y]) => tileKey(x, y)));
|
|
42
|
+
return { w, h, path: def.path, cum, length: cum[cum.length - 1] ?? 0, road, blocked };
|
|
43
|
+
}
|
|
44
|
+
/** Position on the road at progress `d` (Q units), in Q units with the board's corner at 0, and the heading (u16 turn). */
|
|
45
|
+
export function roadPoint(b, d) {
|
|
46
|
+
let i = 1;
|
|
47
|
+
while (i < b.cum.length - 1 && d > (b.cum[i] ?? 0))
|
|
48
|
+
i++;
|
|
49
|
+
const a = b.path[i - 1];
|
|
50
|
+
const c = b.path[i] ?? a;
|
|
51
|
+
const t = Math.max(0, d - (b.cum[i - 1] ?? 0));
|
|
52
|
+
const sx = Math.sign(c[0] - a[0]);
|
|
53
|
+
const sy = Math.sign(c[1] - a[1]);
|
|
54
|
+
const qx = a[0] * Q + Q / 2 + sx * t;
|
|
55
|
+
const qy = a[1] * Q + Q / 2 + sy * t;
|
|
56
|
+
// Facing along the road: yaw 0 looks toward -z (the engine's convention).
|
|
57
|
+
const yaw = sx > 0 ? 49152 : sx < 0 ? 16384 : sy > 0 ? 32768 : 0;
|
|
58
|
+
return { qx, qy, yaw };
|
|
59
|
+
}
|
|
60
|
+
function statsAt(def, level) {
|
|
61
|
+
const s = { damage: def.damage, rate: def.rate, range: def.range, radius: def.radius, slow: def.slow, slowSeconds: def.slowSeconds, chains: def.chains };
|
|
62
|
+
for (let i = 0; i < level && i < def.levels.length; i++) {
|
|
63
|
+
const l = def.levels[i];
|
|
64
|
+
if (!l)
|
|
65
|
+
continue;
|
|
66
|
+
if (l.damage !== undefined)
|
|
67
|
+
s.damage = l.damage;
|
|
68
|
+
if (l.rate !== undefined)
|
|
69
|
+
s.rate = l.rate;
|
|
70
|
+
if (l.range !== undefined)
|
|
71
|
+
s.range = l.range;
|
|
72
|
+
if (l.radius !== undefined)
|
|
73
|
+
s.radius = l.radius;
|
|
74
|
+
if (l.slow !== undefined)
|
|
75
|
+
s.slow = l.slow;
|
|
76
|
+
if (l.chains !== undefined)
|
|
77
|
+
s.chains = l.chains;
|
|
78
|
+
}
|
|
79
|
+
return s;
|
|
80
|
+
}
|
|
81
|
+
export function compileTowerGame(m) {
|
|
82
|
+
const c = towerContent(m);
|
|
83
|
+
const creepIds = [...m.prefabs].filter(([, p]) => p.kind !== "pickup").map(([id]) => id);
|
|
84
|
+
const creeps = creepIds.map((id) => {
|
|
85
|
+
const p = m.prefabs.get(id);
|
|
86
|
+
if (!p)
|
|
87
|
+
throw new Error(id);
|
|
88
|
+
return { id, model: p.model, health: p.health, speed: p.speed, value: p.value ?? 1, damage: p.damage ?? 1, boss: p.boss !== undefined && p.boss !== false };
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
board: compileBoard(c.board),
|
|
92
|
+
towers: c.towers.map(([id, def]) => ({
|
|
93
|
+
id,
|
|
94
|
+
def,
|
|
95
|
+
levels: Array.from({ length: def.levels.length + 1 }, (_, i) => statsAt(def, i)),
|
|
96
|
+
upgradeCost: def.levels.map((l) => l.cost),
|
|
97
|
+
})),
|
|
98
|
+
creeps,
|
|
99
|
+
lives: c.board.lives,
|
|
100
|
+
gold: c.board.gold,
|
|
101
|
+
prep: c.board.prep,
|
|
102
|
+
breather: c.board.breather,
|
|
103
|
+
share: c.board.share,
|
|
104
|
+
waveGold: c.board.waveGold,
|
|
105
|
+
refund: c.board.refund,
|
|
106
|
+
totalWaves: c.waves.total,
|
|
107
|
+
healthGrowth: c.waves.health,
|
|
108
|
+
groups: c.waves.groups.map((g) => ({ type: creepIds.indexOf(g.prefab), from: g.from, until: g.until ?? c.waves.total, every: g.every, count: g.count, grow: g.grow, gap: g.gap, delay: g.delay })),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** The spawns of wave `n` (1-based), by tick offset into the wave. */
|
|
112
|
+
export function waveSpawns(g, n, tickRate) {
|
|
113
|
+
const out = [];
|
|
114
|
+
g.groups.forEach((grp, gi) => {
|
|
115
|
+
if (n < grp.from || n > grp.until || (n - grp.from) % grp.every !== 0)
|
|
116
|
+
return;
|
|
117
|
+
const k = (n - grp.from) / grp.every;
|
|
118
|
+
const count = Math.max(1, Math.floor(grp.count + grp.grow * k));
|
|
119
|
+
for (let i = 0; i < count; i++)
|
|
120
|
+
out.push({ tick: Math.round((grp.delay + i * grp.gap) * tickRate), type: grp.type, order: gi * 10000 + i });
|
|
121
|
+
});
|
|
122
|
+
out.sort((a, b) => a.tick - b.tick || a.order - b.order);
|
|
123
|
+
return out.map(({ tick, type }) => ({ tick, type }));
|
|
124
|
+
}
|
|
125
|
+
/** What the next wave brings, by creep type (the HUD's preview). */
|
|
126
|
+
export function wavePreview(g, n) {
|
|
127
|
+
const counts = new Map();
|
|
128
|
+
for (const s of waveSpawns(g, n, 1))
|
|
129
|
+
counts.set(s.type, (counts.get(s.type) ?? 0) + 1);
|
|
130
|
+
return [...counts].map(([type, count]) => ({ type, count }));
|
|
131
|
+
}
|
|
132
|
+
const TICK = 30;
|
|
133
|
+
/** Alive and on the road: not a corpse, not leaked. */
|
|
134
|
+
const alive = (c) => c.gone === 0 && c.hp > 0 && (c.flags & CreepFlag.Leaked) === 0;
|
|
135
|
+
/** Road tiles within reach of each tile, per board and range (the bots' placement score), cached. */
|
|
136
|
+
const coverageCache = new WeakMap();
|
|
137
|
+
export function roadCoverage(b, rangeQ) {
|
|
138
|
+
let byRange = coverageCache.get(b);
|
|
139
|
+
if (!byRange)
|
|
140
|
+
coverageCache.set(b, (byRange = new Map()));
|
|
141
|
+
let c = byRange.get(rangeQ);
|
|
142
|
+
if (c)
|
|
143
|
+
return c;
|
|
144
|
+
c = new Int32Array(64 * 64);
|
|
145
|
+
const r2 = rangeQ * rangeQ;
|
|
146
|
+
const roadTiles = [...b.road].map((k) => [k % 64, Math.floor(k / 64)]);
|
|
147
|
+
for (let y = 0; y < b.h; y++)
|
|
148
|
+
for (let x = 0; x < b.w; x++) {
|
|
149
|
+
let n = 0;
|
|
150
|
+
for (const [rx, ry] of roadTiles) {
|
|
151
|
+
const dx = (rx - x) * Q;
|
|
152
|
+
const dy = (ry - y) * Q;
|
|
153
|
+
if (dx * dx + dy * dy <= r2)
|
|
154
|
+
n++;
|
|
155
|
+
}
|
|
156
|
+
c[tileKey(x, y)] = n;
|
|
157
|
+
}
|
|
158
|
+
byRange.set(rangeQ, c);
|
|
159
|
+
return c;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The best free tile for a tower of `type` (the most road in reach), with a
|
|
163
|
+
* per-player tie-break and `jitter` (0 for the sharpest choice). Null when
|
|
164
|
+
* no free tile reaches the road. Used by the playtest bots and the
|
|
165
|
+
* client's autopilot alike.
|
|
166
|
+
*/
|
|
167
|
+
export function bestTowerTile(g, occupied, type, salt, jitter = 0) {
|
|
168
|
+
const b = g.board;
|
|
169
|
+
const tt = g.towers[type];
|
|
170
|
+
if (!tt)
|
|
171
|
+
return null;
|
|
172
|
+
const cov = roadCoverage(b, Math.round((tt.levels[0]?.range ?? 1) * Q));
|
|
173
|
+
let best = -1;
|
|
174
|
+
let bx = 0;
|
|
175
|
+
let by = 0;
|
|
176
|
+
for (let y = 0; y < b.h; y++)
|
|
177
|
+
for (let x = 0; x < b.w; x++) {
|
|
178
|
+
const k = tileKey(x, y);
|
|
179
|
+
if (b.road.has(k) || b.blocked.has(k) || occupied.has(k))
|
|
180
|
+
continue;
|
|
181
|
+
const h = ((x * 73856093) ^ (y * 19349663) ^ (salt * 83492791)) >>> 0;
|
|
182
|
+
const score = (cov[k] ?? 0) * 8 + (h % (1 + jitter * 8));
|
|
183
|
+
if (score > best) {
|
|
184
|
+
best = score;
|
|
185
|
+
bx = x;
|
|
186
|
+
by = y;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return best > 8 ? { tx: bx, ty: by } : null;
|
|
190
|
+
}
|
|
191
|
+
export function towerGenre(m) {
|
|
192
|
+
const g = compileTowerGame(m);
|
|
193
|
+
const b = g.board;
|
|
194
|
+
const tickRate = TICK;
|
|
195
|
+
const halfW = (b.w * TILE_M) / 2;
|
|
196
|
+
const halfH = (b.h * TILE_M) / 2;
|
|
197
|
+
/** Avatar speed, fixed units a tick (24 m/s). */
|
|
198
|
+
const AVATAR_STEP = Math.round((24 * UNITS) / tickRate);
|
|
199
|
+
const humans = (s) => [...s.players.keys()].filter((p) => !s.ctx.isBot(p));
|
|
200
|
+
const buildable = (s, tx, ty) => tx >= 0 && ty >= 0 && tx < b.w && ty < b.h && !b.road.has(tileKey(tx, ty)) && !b.blocked.has(tileKey(tx, ty)) && !s.towers.some((t) => t.tx === tx && t.ty === ty);
|
|
201
|
+
const effective = (s, t) => {
|
|
202
|
+
const base = g.towers[t.type]?.levels[t.level] ?? g.towers[0].levels[0];
|
|
203
|
+
const might = s.ctx.bonus(t.owner, "might");
|
|
204
|
+
const haste = s.ctx.bonus(t.owner, "haste");
|
|
205
|
+
const area = s.ctx.bonus(t.owner, "area");
|
|
206
|
+
return { ...base, damage: base.damage * (1 + might), rate: base.rate * (1 + haste), range: base.range * (1 + area) };
|
|
207
|
+
};
|
|
208
|
+
const startWave = (s) => {
|
|
209
|
+
s.wave++;
|
|
210
|
+
s.phase = "wave";
|
|
211
|
+
s.queue = waveSpawns(g, s.wave, tickRate);
|
|
212
|
+
s.waveTick = 0;
|
|
213
|
+
for (const p of s.players.values())
|
|
214
|
+
p.ready = false;
|
|
215
|
+
s.activity++;
|
|
216
|
+
};
|
|
217
|
+
const kill = (s, c, owner, emit) => {
|
|
218
|
+
c.hp = 0;
|
|
219
|
+
c.gone = 8;
|
|
220
|
+
c.flags |= CreepFlag.Dead;
|
|
221
|
+
c.by = owner;
|
|
222
|
+
s.kills++;
|
|
223
|
+
s.activity++;
|
|
224
|
+
const pay = c.value;
|
|
225
|
+
const own = s.players.get(owner);
|
|
226
|
+
if (own) {
|
|
227
|
+
own.gold += pay;
|
|
228
|
+
own.kills++;
|
|
229
|
+
emit({ name: "enemy.killed", players: [owner], own: true });
|
|
230
|
+
}
|
|
231
|
+
const share = Math.floor(pay * g.share);
|
|
232
|
+
if (share > 0)
|
|
233
|
+
for (const [p, pl] of s.players)
|
|
234
|
+
if (p !== owner)
|
|
235
|
+
pl.gold += share;
|
|
236
|
+
if (c.boss)
|
|
237
|
+
emit({ name: "boss.killed", players: humans(s) });
|
|
238
|
+
};
|
|
239
|
+
const hit = (s, c, dmg, owner, emit) => {
|
|
240
|
+
if (!alive(c))
|
|
241
|
+
return;
|
|
242
|
+
c.hp -= Math.max(1, Math.round(dmg));
|
|
243
|
+
if (c.hp <= 0)
|
|
244
|
+
kill(s, c, owner, emit);
|
|
245
|
+
};
|
|
246
|
+
const creepQ = (c) => roadPoint(b, c.d);
|
|
247
|
+
const fire = (s, t, emit) => {
|
|
248
|
+
const st = effective(s, t);
|
|
249
|
+
if (t.cd > 0) {
|
|
250
|
+
t.cd--;
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const cx = t.tx * Q + Q / 2;
|
|
254
|
+
const cy = t.ty * Q + Q / 2;
|
|
255
|
+
const r = Math.round(st.range * Q);
|
|
256
|
+
const r2 = r * r;
|
|
257
|
+
let best = null;
|
|
258
|
+
for (const c of s.creeps) {
|
|
259
|
+
if (!alive(c))
|
|
260
|
+
continue;
|
|
261
|
+
const p = creepQ(c);
|
|
262
|
+
const dx = p.qx - cx;
|
|
263
|
+
const dy = p.qy - cy;
|
|
264
|
+
if (dx * dx + dy * dy > r2)
|
|
265
|
+
continue;
|
|
266
|
+
if (!best || c.d > best.d)
|
|
267
|
+
best = c;
|
|
268
|
+
}
|
|
269
|
+
if (!best)
|
|
270
|
+
return;
|
|
271
|
+
const kind = g.towers[t.type]?.def.kind ?? "bolt";
|
|
272
|
+
const p = creepQ(best);
|
|
273
|
+
t.yaw = Math.round((Math.atan2(-(p.qx - cx), -(p.qy - cy)) / (Math.PI * 2)) * 65536) & 0xffff;
|
|
274
|
+
t.cd = Math.max(1, Math.round(tickRate / Math.max(0.1, st.rate))) - 1;
|
|
275
|
+
t.shots = (t.shots + 1) & 0xffff;
|
|
276
|
+
t.target = best.id;
|
|
277
|
+
s.activity++;
|
|
278
|
+
const before = s.kills;
|
|
279
|
+
if (kind === "splash") {
|
|
280
|
+
const rr = Math.round(st.radius * Q);
|
|
281
|
+
for (const c of s.creeps) {
|
|
282
|
+
if (!alive(c))
|
|
283
|
+
continue;
|
|
284
|
+
const q = creepQ(c);
|
|
285
|
+
const dx = q.qx - p.qx;
|
|
286
|
+
const dy = q.qy - p.qy;
|
|
287
|
+
if (dx * dx + dy * dy <= rr * rr)
|
|
288
|
+
hit(s, c, st.damage, t.owner, emit);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (kind === "chain") {
|
|
292
|
+
let at = best;
|
|
293
|
+
const hitIds = new Set();
|
|
294
|
+
let dmg = st.damage;
|
|
295
|
+
for (let j = 0; j < st.chains && at; j++) {
|
|
296
|
+
hitIds.add(at.id);
|
|
297
|
+
hit(s, at, dmg, t.owner, emit);
|
|
298
|
+
dmg *= 0.7;
|
|
299
|
+
const from = creepQ(at);
|
|
300
|
+
let next = null;
|
|
301
|
+
let nd = (Q * 1.6) ** 2;
|
|
302
|
+
for (const c of s.creeps) {
|
|
303
|
+
if (!alive(c) || hitIds.has(c.id))
|
|
304
|
+
continue;
|
|
305
|
+
const q = creepQ(c);
|
|
306
|
+
const d2 = (q.qx - from.qx) ** 2 + (q.qy - from.qy) ** 2;
|
|
307
|
+
if (d2 < nd) {
|
|
308
|
+
nd = d2;
|
|
309
|
+
next = c;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
at = next;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
hit(s, best, st.damage, t.owner, emit);
|
|
317
|
+
if (kind === "frost" && alive(best) && st.slow > 0) {
|
|
318
|
+
best.slow = Math.max(best.slow, st.slow);
|
|
319
|
+
best.slowTicks = Math.round(st.slowSeconds * tickRate);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
t.kills += s.kills - before;
|
|
323
|
+
};
|
|
324
|
+
return {
|
|
325
|
+
template: "tower-defense",
|
|
326
|
+
tickRate,
|
|
327
|
+
endSeconds: 8,
|
|
328
|
+
infoEveryTicks: 3,
|
|
329
|
+
game: g,
|
|
330
|
+
create(seed, ctx) {
|
|
331
|
+
return {
|
|
332
|
+
rng: { s: seed | 0 },
|
|
333
|
+
ctx,
|
|
334
|
+
tick: 0,
|
|
335
|
+
wave: 0,
|
|
336
|
+
phase: "build",
|
|
337
|
+
countdown: Math.round(g.prep * tickRate),
|
|
338
|
+
lives: g.lives,
|
|
339
|
+
livesMax: g.lives,
|
|
340
|
+
livesBought: new Map(),
|
|
341
|
+
queue: [],
|
|
342
|
+
waveTick: 0,
|
|
343
|
+
creeps: [],
|
|
344
|
+
towers: [],
|
|
345
|
+
players: new Map(),
|
|
346
|
+
nextId: 1,
|
|
347
|
+
kills: 0,
|
|
348
|
+
leaks: 0,
|
|
349
|
+
peak: 0,
|
|
350
|
+
won: false,
|
|
351
|
+
activity: 0,
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
spawn(_s, player) {
|
|
355
|
+
const end = b.path[b.path.length - 1] ?? [0, 0];
|
|
356
|
+
const c = tileCenter(b, end[0], end[1]);
|
|
357
|
+
const k = (player % 8) - 4;
|
|
358
|
+
return { x: Math.round((c.x + k * 0.6) * UNITS), z: Math.round((c.z - 1) * UNITS) };
|
|
359
|
+
},
|
|
360
|
+
moveAvatar(a, i) {
|
|
361
|
+
const tx = Math.max(-halfW - 2, Math.min(halfW + 2, i.cx / 16)) * UNITS;
|
|
362
|
+
const tz = Math.max(-halfH - 2, Math.min(halfH + 2, i.cz / 16)) * UNITS;
|
|
363
|
+
if (i.cx === 0 && i.cz === 0 && i.act === 0)
|
|
364
|
+
return;
|
|
365
|
+
const dx = Math.round(tx) - a.x;
|
|
366
|
+
const dz = Math.round(tz) - a.z;
|
|
367
|
+
const d = Math.sqrt(dx * dx + dz * dz);
|
|
368
|
+
if (d < 1)
|
|
369
|
+
return;
|
|
370
|
+
if (d <= AVATAR_STEP) {
|
|
371
|
+
a.x += dx;
|
|
372
|
+
a.z += dz;
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
a.x += Math.round((dx * AVATAR_STEP) / d);
|
|
376
|
+
a.z += Math.round((dz * AVATAR_STEP) / d);
|
|
377
|
+
}
|
|
378
|
+
if (d > 8)
|
|
379
|
+
a.yaw = Math.round((Math.atan2(-dx, -dz) / (Math.PI * 2)) * 65536) & 0xffff;
|
|
380
|
+
},
|
|
381
|
+
join(s, player) {
|
|
382
|
+
// A late joiner gets the gold the waves so far would have paid a builder.
|
|
383
|
+
const gold = g.gold + Math.max(0, s.wave - 1) * (g.waveGold.base + g.waveGold.perWave * s.wave);
|
|
384
|
+
s.players.set(player, { gold, kills: 0, ready: false, built: 0 });
|
|
385
|
+
s.activity++;
|
|
386
|
+
},
|
|
387
|
+
leave(s, player) {
|
|
388
|
+
s.players.delete(player);
|
|
389
|
+
// Their towers keep standing for the team; their kills credit nobody.
|
|
390
|
+
for (const t of s.towers)
|
|
391
|
+
if (t.owner === player)
|
|
392
|
+
t.owner = 0;
|
|
393
|
+
},
|
|
394
|
+
act(s, player, i, a, emit) {
|
|
395
|
+
const pl = s.players.get(player);
|
|
396
|
+
if (!pl)
|
|
397
|
+
return;
|
|
398
|
+
if (i.act === TowerAct.Ready) {
|
|
399
|
+
if (s.phase === "build")
|
|
400
|
+
pl.ready = i.a !== 0;
|
|
401
|
+
a.flags = pl.ready ? a.flags | 1 : a.flags & ~1;
|
|
402
|
+
s.activity++;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const tx = i.b;
|
|
406
|
+
const ty = i.c;
|
|
407
|
+
if (i.act === TowerAct.Build) {
|
|
408
|
+
const tt = g.towers[i.a];
|
|
409
|
+
if (!tt || !buildable(s, tx, ty) || pl.gold < tt.def.cost)
|
|
410
|
+
return;
|
|
411
|
+
pl.gold -= tt.def.cost;
|
|
412
|
+
pl.built++;
|
|
413
|
+
s.towers.push({ id: s.nextId++, type: i.a, owner: player, tx, ty, level: 0, cd: 0, spent: tt.def.cost, shots: 0, target: 0, yaw: 0, kills: 0 });
|
|
414
|
+
s.activity++;
|
|
415
|
+
emit({ name: "tower.built", players: [player], own: true });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const t = s.towers.find((x) => x.tx === tx && x.ty === ty);
|
|
419
|
+
if (!t)
|
|
420
|
+
return;
|
|
421
|
+
if (i.act === TowerAct.Upgrade) {
|
|
422
|
+
const cost = g.towers[t.type]?.upgradeCost[t.level];
|
|
423
|
+
if (cost === undefined || pl.gold < cost)
|
|
424
|
+
return;
|
|
425
|
+
pl.gold -= cost;
|
|
426
|
+
t.level++;
|
|
427
|
+
if (t.owner === player)
|
|
428
|
+
t.spent += cost;
|
|
429
|
+
s.activity++;
|
|
430
|
+
emit({ name: "tower.upgraded", players: [player], own: true });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (i.act === TowerAct.Sell) {
|
|
434
|
+
if (t.owner !== player)
|
|
435
|
+
return;
|
|
436
|
+
pl.gold += Math.floor(t.spent * g.refund);
|
|
437
|
+
s.towers = s.towers.filter((x) => x !== t);
|
|
438
|
+
s.activity++;
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
step(s, emit) {
|
|
442
|
+
s.tick++;
|
|
443
|
+
// Team lives bought in the store (maxHealth upgrades): each player's bonus counts once.
|
|
444
|
+
for (const p of s.players.keys()) {
|
|
445
|
+
const want = Math.floor(s.ctx.bonus(p, "maxHealth"));
|
|
446
|
+
const had = s.livesBought.get(p) ?? 0;
|
|
447
|
+
if (want > had) {
|
|
448
|
+
s.lives += want - had;
|
|
449
|
+
s.livesMax += want - had;
|
|
450
|
+
s.livesBought.set(p, want);
|
|
451
|
+
s.activity++;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (s.phase === "build") {
|
|
455
|
+
const people = humans(s);
|
|
456
|
+
const allReady = people.length > 0 && people.every((p) => s.players.get(p)?.ready);
|
|
457
|
+
if (allReady && s.countdown > 0) {
|
|
458
|
+
// Calling the wave early pays a little gold for the seconds skipped.
|
|
459
|
+
const bonus = Math.floor(s.countdown / tickRate);
|
|
460
|
+
for (const pl of s.players.values())
|
|
461
|
+
pl.gold += bonus;
|
|
462
|
+
s.countdown = 0;
|
|
463
|
+
}
|
|
464
|
+
if (s.countdown > 0)
|
|
465
|
+
s.countdown--;
|
|
466
|
+
else if (s.players.size > 0)
|
|
467
|
+
startWave(s);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
// Spawns due this tick.
|
|
471
|
+
while (s.queue.length > 0 && (s.queue[0]?.tick ?? 0) <= s.waveTick) {
|
|
472
|
+
const sp = s.queue.shift();
|
|
473
|
+
const ct = g.creeps[sp.type];
|
|
474
|
+
if (!ct)
|
|
475
|
+
continue;
|
|
476
|
+
// Creeps toughen each wave, and with every extra builder on the board (more towers): +30% a player past the first.
|
|
477
|
+
const team = 1 + 0.3 * Math.max(0, humans(s).length - 1);
|
|
478
|
+
const hp = Math.max(1, Math.round(ct.health * (1 + g.healthGrowth * (s.wave - 1)) * team));
|
|
479
|
+
s.creeps.push({
|
|
480
|
+
id: s.nextId++,
|
|
481
|
+
type: sp.type,
|
|
482
|
+
hp,
|
|
483
|
+
maxHp: hp,
|
|
484
|
+
d: 0,
|
|
485
|
+
speed: Math.max(1, Math.round(((ct.speed / TILE_M) * Q) / tickRate)),
|
|
486
|
+
slowTicks: 0,
|
|
487
|
+
slow: 0,
|
|
488
|
+
value: ct.value,
|
|
489
|
+
dmg: ct.damage,
|
|
490
|
+
boss: ct.boss,
|
|
491
|
+
gone: 0,
|
|
492
|
+
flags: ct.boss ? CreepFlag.Boss : 0,
|
|
493
|
+
by: 0,
|
|
494
|
+
});
|
|
495
|
+
s.activity++;
|
|
496
|
+
}
|
|
497
|
+
s.waveTick++;
|
|
498
|
+
}
|
|
499
|
+
// Creeps walk.
|
|
500
|
+
for (const c of s.creeps) {
|
|
501
|
+
if (c.gone > 0) {
|
|
502
|
+
c.gone--;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const slowed = c.slowTicks > 0;
|
|
506
|
+
if (slowed)
|
|
507
|
+
c.slowTicks--;
|
|
508
|
+
c.flags = slowed ? c.flags | CreepFlag.Slowed : c.flags & ~CreepFlag.Slowed;
|
|
509
|
+
c.d += slowed ? Math.max(1, Math.round(c.speed * (1 - c.slow))) : c.speed;
|
|
510
|
+
if (!slowed)
|
|
511
|
+
c.slow = 0;
|
|
512
|
+
if (c.d >= b.length) {
|
|
513
|
+
c.d = b.length;
|
|
514
|
+
c.gone = 3;
|
|
515
|
+
c.flags |= CreepFlag.Leaked;
|
|
516
|
+
s.lives = Math.max(0, s.lives - c.dmg);
|
|
517
|
+
s.leaks += c.dmg;
|
|
518
|
+
s.activity++;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Corpses and leaks stay a few ticks (clients see them fall or reach the spire), then go.
|
|
522
|
+
s.creeps = s.creeps.filter((c) => c.gone > 0 || alive(c));
|
|
523
|
+
// Towers fire, in build order.
|
|
524
|
+
for (const t of s.towers)
|
|
525
|
+
fire(s, t, emit);
|
|
526
|
+
const living = s.creeps.filter(alive).length;
|
|
527
|
+
s.peak = Math.max(s.peak, living);
|
|
528
|
+
if (s.lives <= 0)
|
|
529
|
+
return;
|
|
530
|
+
if (s.phase === "wave" && s.queue.length === 0 && living === 0) {
|
|
531
|
+
emit({ name: "wave.cleared", players: humans(s) });
|
|
532
|
+
const pay = g.waveGold.base + g.waveGold.perWave * s.wave;
|
|
533
|
+
for (const pl of s.players.values())
|
|
534
|
+
pl.gold += pay;
|
|
535
|
+
s.activity++;
|
|
536
|
+
if (s.wave >= g.totalWaves) {
|
|
537
|
+
s.won = true;
|
|
538
|
+
emit({ name: "run.won", players: humans(s) });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
s.phase = "build";
|
|
542
|
+
s.countdown = Math.round(g.breather * tickRate);
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
outcome(s) {
|
|
546
|
+
if (s.lives <= 0)
|
|
547
|
+
return "lost";
|
|
548
|
+
return s.won ? "won" : null;
|
|
549
|
+
},
|
|
550
|
+
view(s, player) {
|
|
551
|
+
const boss = s.creeps.find((c) => c.boss && alive(c));
|
|
552
|
+
const me = player !== null ? s.players.get(player) : undefined;
|
|
553
|
+
return {
|
|
554
|
+
genre: "tower",
|
|
555
|
+
wave: s.wave,
|
|
556
|
+
waves: g.totalWaves,
|
|
557
|
+
stage: s.phase,
|
|
558
|
+
countdown: Math.ceil(s.countdown / tickRate),
|
|
559
|
+
lives: s.lives,
|
|
560
|
+
livesMax: s.livesMax,
|
|
561
|
+
left: s.queue.length + s.creeps.filter(alive).length,
|
|
562
|
+
next: wavePreview(g, Math.min(g.totalWaves, s.wave + (s.phase === "build" ? 1 : 0) || 1)),
|
|
563
|
+
boss: boss ? { type: boss.type, hp: boss.hp, maxHp: boss.maxHp } : null,
|
|
564
|
+
players: [...s.players].map(([id, p]) => ({ id, gold: p.gold, kills: p.kills, ready: p.ready, built: p.built })),
|
|
565
|
+
...(me ? { gold: me.gold, ready: me.ready } : {}),
|
|
566
|
+
};
|
|
567
|
+
},
|
|
568
|
+
entities(s, out) {
|
|
569
|
+
const toUnits = (q) => Math.round((q * TILE_M * UNITS) / Q);
|
|
570
|
+
const ox = (b.w * Q) / 2;
|
|
571
|
+
const oy = (b.h * Q) / 2;
|
|
572
|
+
for (const c of s.creeps) {
|
|
573
|
+
const p = roadPoint(b, c.d);
|
|
574
|
+
out.push(entity(c.id, RulesKind.Unit, c.by, toUnits(p.qx - ox), toUnits(p.qy - oy), p.yaw, c.type, Math.max(0, c.hp), c.flags, Math.min(0xffff, Math.round((c.hp / Math.max(1, c.maxHp)) * 1000)), Math.min(0xffff, c.maxHp)));
|
|
575
|
+
}
|
|
576
|
+
for (const t of s.towers) {
|
|
577
|
+
out.push(entity(t.id, RulesKind.Structure, t.owner, toUnits(t.tx * Q + Q / 2 - ox), toUnits(t.ty * Q + Q / 2 - oy), t.yaw, t.type, t.target, 0, t.level, t.shots));
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
bot(s, player, a, tick, skill) {
|
|
581
|
+
const pl = s.players.get(player);
|
|
582
|
+
const idle = { mx: 0, mz: 0, cx: Math.round((a.x / UNITS) * 16), cz: Math.round((a.z / UNITS) * 16), act: 0, a: 0, b: 0, c: 0 };
|
|
583
|
+
if (!pl)
|
|
584
|
+
return idle;
|
|
585
|
+
const period = Math.max(3, Math.round(tickRate * (skill >= 1 ? 0.5 : 1.4)));
|
|
586
|
+
if ((tick + player * 7) % period !== 0)
|
|
587
|
+
return idle;
|
|
588
|
+
const at = (tx, ty, act, aa = 0) => {
|
|
589
|
+
const c = tileCenter(b, tx, ty);
|
|
590
|
+
return { mx: 0, mz: 0, cx: Math.round(c.x * 16), cz: Math.round(c.z * 16), act, a: aa, b: tx, c: ty };
|
|
591
|
+
};
|
|
592
|
+
const mine = s.towers.filter((t) => t.owner === player);
|
|
593
|
+
const order = g.towers.map((t, i) => i).sort((x, y) => (g.towers[x]?.def.cost ?? 0) - (g.towers[y]?.def.cost ?? 0));
|
|
594
|
+
const want = order[(mine.length + player) % order.length] ?? 0;
|
|
595
|
+
const pick = g.towers[want] && pl.gold >= (g.towers[want]?.def.cost ?? Infinity) ? want : order.find((i) => (g.towers[i]?.def.cost ?? Infinity) <= pl.gold);
|
|
596
|
+
// Build while there is a good tile and gold; a weaker player builds less often and less well.
|
|
597
|
+
const upgradeable = mine.filter((t) => (g.towers[t.type]?.upgradeCost[t.level] ?? Infinity) <= pl.gold).sort((x, y) => x.level - y.level || x.id - y.id);
|
|
598
|
+
const preferUpgrade = mine.length >= 6 && upgradeable.length > 0;
|
|
599
|
+
if (pick !== undefined && !preferUpgrade) {
|
|
600
|
+
const occupied = new Set(s.towers.map((t) => tileKey(t.tx, t.ty)));
|
|
601
|
+
const tile = bestTowerTile(g, occupied, pick, player, skill >= 1 ? 0 : 3);
|
|
602
|
+
if (tile)
|
|
603
|
+
return at(tile.tx, tile.ty, 1, pick);
|
|
604
|
+
}
|
|
605
|
+
const up = upgradeable[0];
|
|
606
|
+
if (up)
|
|
607
|
+
return at(up.tx, up.ty, 2);
|
|
608
|
+
if (s.phase === "build" && !pl.ready && skill >= 0.5)
|
|
609
|
+
return { ...idle, act: 4, a: 1 };
|
|
610
|
+
return idle;
|
|
611
|
+
},
|
|
612
|
+
stats(s) {
|
|
613
|
+
let top = 0;
|
|
614
|
+
let scorers = 0;
|
|
615
|
+
for (const p of s.players.values()) {
|
|
616
|
+
top = Math.max(top, p.kills);
|
|
617
|
+
if (p.kills > 0)
|
|
618
|
+
scorers++;
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
kills: s.kills,
|
|
622
|
+
wave: Math.max(1, s.wave),
|
|
623
|
+
level: s.towers.reduce((m, t) => Math.max(m, t.level + 1), 0),
|
|
624
|
+
deaths: s.leaks,
|
|
625
|
+
peakUnits: s.peak,
|
|
626
|
+
scorers,
|
|
627
|
+
topScore: top,
|
|
628
|
+
};
|
|
629
|
+
},
|
|
630
|
+
activity(s) {
|
|
631
|
+
return s.activity;
|
|
632
|
+
},
|
|
633
|
+
checks(run) {
|
|
634
|
+
const min = Math.round(run.seconds / 6) / 10;
|
|
635
|
+
const waveDone = run.stats.wave;
|
|
636
|
+
const out = [];
|
|
637
|
+
out.push({
|
|
638
|
+
ok: run.stats.kills > 0,
|
|
639
|
+
line: `in ${min} minutes of ${run.players}-player play no creep was killed: check that the towers' damage, rate and range reach the road, and that waves() spawns creeps`,
|
|
640
|
+
});
|
|
641
|
+
out.push({
|
|
642
|
+
ok: run.runs.lost === 0 || waveDone >= 4,
|
|
643
|
+
line: `${run.players} playtest players lost the spire before wave 4: the opening is too punishing (fewer or slower first creeps, more starting gold or lives)`,
|
|
644
|
+
});
|
|
645
|
+
out.push({
|
|
646
|
+
ok: waveDone >= 3 || run.seconds < 150,
|
|
647
|
+
line: `only ${waveDone} wave(s) started in ${min} minutes: shorten prep and breather, or the gaps between creeps`,
|
|
648
|
+
});
|
|
649
|
+
return out;
|
|
650
|
+
},
|
|
651
|
+
progress(run) {
|
|
652
|
+
return Math.min(1, run.stats.wave / Math.max(1, Math.min(g.totalWaves, 8))) * 0.6 + (run.stats.level >= 2 ? 0.2 : 0) + Math.min(0.2, run.stats.kills / 500);
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
}
|