Read-only JSON describing the game exactly as the current prototype build plays it. Generated 2026-10-01 01:57 UTC. 102 cards · 42 enemies · 3 bosses · 100 maps · 49 artifacts · 5 specialTiles · 14 formulas.
Every endpoint is a static JSON file under /v1/ on the site hosting this page. Requests are plain GET, no key needed, and cross-origin requests are allowed (CORS), so you can fetch from a local dev server or another site.
// JavaScript
const BASE = "https://YOUR-SITE.pages.dev/v1/";
const cards = await (await fetch(BASE + "cards.json")).json();
const railgun = cards.find(c => c.code === "C-028");
# shell
curl https://YOUR-SITE.pages.dev/v1/enemies.json
Source of truth: card, enemy, event and rule data are authored in Supabase. This API is generated from the game build, so it matches what the game actually plays (including rules implemented in code). It is regenerated with every new build; check /v1/meta.json → generated. Breaking changes will move to /v2/.
[column, row] from the top-left of the board. One tile = 50 range units (runRules → tileSize).[dx, dy] are defined facing right (+x). Facing left is [-dx, -dy]; facing down is [-dy, dx]; facing up is [dy, -dx].speed = attacks per second. Enemy speed = range units per second (divide by 50 for tiles per second). Slowed enemies move 40% slower (slowPct), twice that if resonated, capped at 80%.eStats; level settings lvlHp, lvlShield, lvlSpd in runRules).route.difficultyCurve); it sets the map tier, number of waves and enemy levels. Step 11 is the world exit with the boss.C-###, enemies E-### (bosses E-B##), artifacts AR-##, events EV-##, clients CL-##./v1/meta.jsonBuild info: version, generation time, endpoint list and item counts. Check generated to know which build the data came from.
{
"api": "Contract: Zero game data API",
"version": "v1",
"generated": "2026-10-01T01:57:47.309Z",
"source": "Generated from the published prototype build (Isometric Edition). Card, enemy, event and rule data originate in Supabase; this API is exactly what the build uses.",
"endpoints": [
"/v1/cards.json",
"/v1/enemies.json",
"/v1/bosses.json",
"/v1/maps.json",
"/v1/rangePatterns.json",
"/v1/waves.json",
"/v1/route.json",
"/v1/runRules.json",
"/v1/decks.json",
"/v1/missions.json",
"/v1/events.json",
"/v1/artifacts.json",
"/v1/specialTiles.json",
"/v1/landmarks.json",
"/v1/formulas.json"
],
"counts": {
"cards": 102,
"enemies": 42,
"bosses": 3,
"maps": 100,
"artifacts": 49,
"specialTiles": 5,
"formulas": 14
}
}/v1/cards.jsonEvery card in the build: stats, text, range pattern and the exact tiles it covers, and its build roles from Supabase.
| Field | Type | Meaning |
|---|---|---|
code | string | Card id, e.g. C-001. Stable across builds. |
name | string | Display name. |
type | string | Attacking Tower, Utility Tower, Sentry, Module, Spell or Boon. |
rarity | string | Starting Deck, Token, Common, Uncommon, Rare. |
cost | number | null | Energy cost (null for Boons, which are unplayable). |
attack | number | null | Base attack per hit. |
speed | number | null | Attacks per second (the tower fires every 1/speed seconds). |
range | string | Legacy range value in range units (50 per tile), or line for whole-row towers. |
rangePattern | string | Range pattern name (see rangePatterns), or line. |
rangeCells | [dx,dy][] | 'line' | Tiles the tower can hit, relative to its own tile, facing right (+x). Rotate for other facings. |
keywords | string[] | Keyword tags, e.g. Sentry, starter. |
text | string | Card text as shown in game. |
inPool | boolean | True if the card can appear in rewards and shops in this prototype. |
sentryType | string | null | series, adaptive, cryo or fuse for sentry cards. |
buildRoles | object | Build key to role: enabler, payoff or both (Supabase build_roles). |
buildWeight | object | Build key to core or minor (Supabase build_weight). |
tags | string[] | Supabase tags, e.g. adjacency, anti-flying. |
{
"code": "C-001",
"name": "Attacker",
"type": "Attacking Tower",
"rarity": "Starting Deck",
"cost": 1,
"attack": 10,
"speed": 2,
"range": "100",
"rangePattern": "Y-6",
"rangeCells": [
[
0,
-2
],
[
1,
-2
],
[
-1,
-1
],
[
0,
-1
],
[
1,
-1
],
[
2,
-1
],
[
-1,
0
],
[
1,
0
],
[
2,
0
],
[
-1,
1
],
[
0,
1
],
[
1,
1
],
[
2,
1
],
[
0,
2
],
[
1,
2
]
],
"keywords": [
"starter"
],
"text": "A basic tower. No special effect.",
"inPool": true,
"sentryType": null,
"buildRoles": {},
"buildWeight": {},
"tags": []
}/v1/enemies.jsonEvery enemy, bosses included: base stats, behaviour text, the engine's behaviour flags, and stats at levels 1 to 30.
| Field | Type | Meaning |
|---|---|---|
code | string | Enemy id, e.g. E-001; bosses use E-B01 to E-B03. |
name | string | Display name. |
type | string | Category, e.g. Swarm, Tank, Boss. |
threat | number | 1 to 10. Wave generation picks enemies within a threat range per world and difficulty. |
flying | boolean | Flies straight from its spawn to its target, ignoring roads. |
hp / shield | number | Base values at level 1. Shield absorbs damage first. |
speed | number | Range units per second at level 1 (50 units = 1 tile). |
leak | number | HQ damage when it reaches the HQ. |
behaviourFlags | object | Engine flags that drive abilities, e.g. {"blink":{"dmg":50,"d":150,"cd":5}}. |
statsByLevel | object[] | hp, shield and speed at levels 1, 5, 10, 15, 20, 25, 30 (see formula eStats). |
{
"code": "E-001",
"name": "Swarmling",
"type": "Swarm",
"threat": 1,
"flying": false,
"boss": false,
"hp": 15,
"shield": 0,
"speed": 60,
"leak": 1,
"behaviour": "Spawns in groups of 6. No ability.",
"description": "A small rift organism that attacks in groups of six. Individually weak.",
"strategy": "Splash damage clears the pack: a Fuse on the road, Adaptive Mortar, or Chain Reaction. Arc Link chains between them.",
"behaviourFlags": {
"group": 6
},
"statsByLevel": [
{
"level": 1,
"hp": 15,
"shield": 0,
"speed": 60
},
{
"level": 5,
"hp": 27,
"shield": 0,
"speed": 67.2
},
{
"level": 10,
"hp": 42,
"shield": 0,
"speed": 76.2
},
{
"level": 15,
"hp": 57,
"shield": 0,
"speed": 85.2
},
{
"level": 20,
"hp": 72,
"shield": 0,
"speed": 94.2
},
{
"level": 25,
"hp": 87,
"shield": 0,
"speed": 103.2
},
{
"level": 30,
"hp": 102,
"shield": 0,
"speed": 112.2
}
]
}/v1/bosses.jsonThe three bosses, one per world, with their timed rules.
{
"code": "E-B01",
"world": 1,
"name": "Brood Mother",
"flying": false,
"hp": 960,
"shield": 0,
"speed": 24,
"leak": 10,
"behaviour": "Releases a Swarmling every 2s and lays a Brood Pod every 10s. At half HP it stops laying and moves 60% faster.",
"strategy": "Splash damage for the brood, sustained damage on the mother, and slows to keep her in the kill zone.",
"behaviourFlags": {
"spawner": {
"code": "E-001",
"n": 1,
"cd": 2
}
},
"timedRules": {
"podEvery": 10,
"podCode": "E-011",
"phaseAt": 0.5,
"phaseSpeed": 1.6
},
"appears": "Final wave of the world's exit stage (map step 11). Killing it: +100 gold and the stage's card picks become Rares."
}/v1/maps.jsonAll 100 stage maps. Coordinates are tiles: [column, row], origin at the top-left.
| Field | Type | Meaning |
|---|---|---|
index | number | Map id. |
name | string | Map name. |
tier | number | 1 to 10: the difficulty a map is used for. |
cols / rows | number | Board size in tiles. |
routes | object[] | One per spawn: spawn label and path, a polyline of tile coordinates. The first point is off the board (the entrance); the last point is the HQ. |
blocked | [c,r][] | Building tiles: not buildable, not walkable. |
{
"index": 0,
"name": "Switchback",
"tier": 1,
"cols": 11,
"rows": 7,
"routes": [
{
"spawn": "A",
"path": [
[
-1,
0
],
[
7,
0
],
[
7,
3
],
[
11,
3
],
[
11,
2
]
]
}
],
"blocked": [
[
5,
2
],
[
1,
2
],
[
0,
4
],
[
4,
5
],
[
3,
2
],
[
3,
4
]
]
}/v1/rangePatterns.jsonPattern name to the list of [dx, dy] offsets a tower hits, facing right (+x). Card rangeCells are already resolved.
{
"0-1": [],
"1-1": [
[
1,
0
]
],
"1-2": [
[
0,
-1
],
[
1,
0
],
[
0,
1
]
],
"1-3": [
[
1,
-1
],
[
1,
0
],
[
1,
1
]
],
"2-1": [
[
0,
-2
],
[
0,
-1
],
[
1,
-1
],
[
1,
0
],
[
2,
0
],
[
0,
1
],
[
1,
1
],
[
0,
2
]
],
"2-2": [
[
1,
0
],
[
2,
0
]
],
"2-3": [
[
0,
-1
],
[
1,
-1
],
[
1,
0
],
[
2,
0
],
[
0,
1
],
[
1,
1
]
],
"2-4": [
[
1,
-1
],
[
1,
0
],
[
2,
0
],
[
1,
1
]
],
"2-5": [
[
1,
-1
],
[
2,
-1
],
[
1,
0
],
[
2,
0
],
[
1,
1
],
[
2,
1
]
],
"2-6": [
[
2,
-2
],
[
1,
-1
],
[
2,
-1
],
[
1,
0
],
[
2,
0
],
[
1,
1
],
[
2,
1
],
[
2,
2
]
],
"3-1": [
[
0,
-1
],
[
1,
-1
],
[
2,
-1
],
[
1,
0
],
[
2,
0
],
[
3,
0
],
[
0,
1
],
[
1,
1
],
[
2,
1
]
],
"3-2": [
[
1,
0
],
[
2,
0
],
[
3,
0
]
],
"3-3": [
[
0,
-1
],
[
1,
-1
],
[
2,
-1
],
[
3,
-1
],
[
1,
0
],
[
2,
0
],
[
3,
0
],
[
0,
1
],
[
1,
1
],
[
2,
1
],
[
3,
1
]
],
"3-4": [
[
0,
-2
],
[
1,
-2
],
[
2,
-2
],
[
0,
-1
],
[
1,
-1
],
[
2,
-1
.../v1/waves.jsonHow waves are built: waves per stage for each world and difficulty, rules, and seeded samples of generated waves (normal and Elite).
| Field | Type | Meaning |
|---|---|---|
rules.wavesPerStage | object | world N, difficulty D: number of waves. |
samples[].world / difficulty / elite | number / boolean | Which stage the sample is for. |
samples[].sample | pack[][] | One array per wave; each pack is {enemy, code, count, level}. Seeded: real runs differ but follow the same rules. |
{
"rules": {
"wavesPerStage": {
"world1": {
"difficulty1": 3,
"difficulty2": 4,
"difficulty3": 4
}
}
},
"samples": [
{
"world": 1,
"difficulty": 1,
"elite": false,
"waves": 3,
"sample": [
[
{
"enemy": "Swarmling",
"code": "E-001",
"count": 6,
"level": 1
},
{
"enemy": "Swarmling",
"code": "E-001",
"count": 6,
"level": 1
},
{
"enemy": "Grunt",
"code": "E-002",
"count": 2,
"level": 1
}
],
[
{
"enemy": "Bloater",
"code": "E-012",
"count": 3,
"level": 1
},
{
"enemy": "Brood Pod",
"code": "E-011",
"count": 2,
"level": 1
},
{
"enemy": "Grunt",
"code": "E-002",
"count": 1,
"level": 1
},
{
"enemy": "Dasher",
"code": "E-004",
"count": 1,
"level": 1
},
{
"enemy": "Swarmling",
"code": "E-001",
"count": 1,
"level": 1
}
]
]
}
]
}/v1/route.jsonThe route map: 12 steps per world, the three lines, stop types, and the difficulty curve from Supabase node_difficulty.
{
"rows": 12,
"lines": [
"express",
"contracts",
"risk"
],
"linePlan": {
"express": {
"shop": 1,
"garage": 1,
"event": 3,
"elite": 1,
"skips": 3
},
"contracts": {
"shop": 0,
"garage": 1,
"event": 2,
"elite": 1,
"skips": 0
},
"risk": {
"shop": 2,
"garage": 1,
"event": 1,
"elite": 3,
"skips": 0
}
},
"stopTypes": {
"shop": "Shop",
"garage": "Garage",
"event": "Event"
},
"difficultyCurve": [
{
"world": 1,
"map_step": 0,
"line": "all",
"difficulty": 1
},
{
"world": 1,
"map_step": 1,
"line": "all",
"difficulty": 2
},
{
"world": 1,
"map_step": 2,
"line": "all",
"difficulty": 3
},
{
"world": 1,
"map_step": 3,
"line": "all",
"difficulty": 3
},
{
"world": 1,
"map_step": 4,
"line": "all",
"difficulty": 4
},
{
"world": 1,
"map_step": 5,
"line": "all",
"difficulty": 5
},
{
"world": 1,
"map_step": 6,
"line": "all",
"difficulty": 6
},
{
"world": 1,
"map_step": 7,
"line": "all",
"difficulty": 7
},
{
"world": 1,
"map_step": 8,
"line": "all",
"difficulty": 8
},
{
"world": 1,
"map_step": 9,
"line": "all",
"difficulty": 8
},
{
"world": 1,
"map_step": 10,
"line": "all",
"difficulty": 9
},
{
"world": 1,
"map_step": 11,
"line": "all",
"difficulty": 10
},
{
"world": 2,
"map_step": 0,
"line": "all",
"difficulty": 1
},
{
"world": 2,
"map_step": 1,
"line": "all",
"difficulty": 2
},
{
"world": 2,
"
.../v1/runRules.jsonEvery tunable rule with its default (Energy, hand size, HP, level scaling, rewards and so on) and every on/off switch.
| Field | Type | Meaning |
|---|---|---|
settings[].key / label / default / group | string / number | Rule key as used in code (CFG.key), description, default value, and group. |
switches[] | object | On/off rules with their defaults. |
worldPerks | object[] | Perks offered between worlds. |
{
"settings": [
{
"key": "tileSize",
"label": "Tile size (range units)",
"default": 50,
"group": "Board and rules"
},
{
"key": "blockedStart",
"label": "Blocked % of tiles near the road, first stage",
"default": 45,
"group": "Board and rules"
},
{
"key": "blockedEnd",
"label": "Blocked % of tiles near the road, last stage",
"default": 10,
"group": "Board and rules"
},
{
"key": "nearRoad",
"label": "\"Near the road\" = within this range of a road tile",
"default": 100,
"group": "Board and rules"
},
{
"key": "repairEvery",
"label": "Repair HQ after every N stages",
"default": 5,
"group": "Board and rules"
},
{
"key": "repairHp",
"label": "HQ HP repaired",
"default": 5,
"group": "Board and rules"
},
{
"key": "handDraw",
"label": "Cards drawn per turn",
"default": 5,
"group": "Board and rules"
},
{
"key": "energy",
"label": "Energy per turn at run start",
"default": 4,
"group": "Board and rules"
},
{
"key": "handLimit",
"label": "Hand limit",
"default": 8,
"group": "Board and rules"
},
{
"key": "baseHp",
"label": "HQ HP",
"default": 20,
"group": "Board and rules"
},
{
"key": "worldWaves",
"label": "Extra waves per stage in each later world",
"default": 1,
"group": "Board and rules"
},
{
"key": "shotsPerSpeed",
"label": "Shots per second per 1 Speed",
"default": 1,
"group": "Board and rules"
},
{
"key": "towerAtkPct",
"label": "All tower and sentry attack +% (prototype test, not in
.../v1/decks.jsonThe default starting deck and the built-in deck templates (card code to count).
{
"defaultStartingDeck": {
"C-001": 2,
"C-101": 1,
"C-006": 1,
"C-007": 1,
"C-049": 1,
"C-017": 1,
"C-039": 1,
"C-089": 1,
"C-070": 1,
"C-077": 1
},
"templates": [
{
"id": "starterA",
"name": "Starter Deck - A (11)",
"deck": {
"C-001": 2,
"C-101": 1,
"C-006": 1,
"C-007": 1,
"C-049": 1,
"C-017": 1,
"C-039": 1,
"C-089": 1,
"C-070": 1,
"C-077": 1
},
"random": false
},
{
"id": "starter",
"name": "Starter Deck (10)",
"deck": {
"C-001": 2,
"C-003": 2,
"C-004": 3,
"C-049": 1,
"C-006": 1,
"C-007": 1
},
"random": false
},
{
"id": "sentry",
"name": "Sentry build (15)",
"deck": {
"C-001": 2,
"C-004": 1,
"C-014": 1,
"C-012": 2,
"C-037": 1,
"C-020": 2,
"C-050": 2,
"C-084": 1,
"C-048": 1,
"C-017": 1,
"C-040": 1
},
"random": false
},
{
"id": "fuse",
"name": "Fuse build (15)",
"deck": {
"C-001": 3,
"C-003": 1,
"C-002": 2,
"C-015": 3,
"C-038": 1,
"C-032": 1,
"C-029": 2,
"C-028": 1,
"C-020": 1
},
"random": false
},
{
"id": "adaptive",
"name": "Adaptive build (15)",
"deck": {
"C-001": 2,
"C-004": 1,
"C-003": 1,
"C-010": 2,
"C-022": 2,
"C-079": 1,
"C-082": 2,
"C-090": 1,
"C-064": 1,
"C-062": 1,
"C-075": 1
},
"random": false
},
{
"id": "cryo",
"name": "Cryo and slow build (15)",
"deck": {
"C-001": 1,
"C-003": 1,
"C-011": 2,
"C-026": 1,
"C-025": 1,
"C-019": 1,
.../v1/missions.jsonClients, the 50 missions, the objective types and their rules.
{
"clients": [
{
"code": "CL-01",
"name": "Ichiro Banno",
"role": "City councillor",
"boon": "Kickback",
"boonText": "Unplayable. When you draw this, gain 1 Energy and lose 3 gold."
},
{
"code": "CL-02",
"name": "Fumiko Oda",
"role": "Retired calligraphy teacher",
"boon": "Lucky Cat",
"boonText": "Unplayable. When you draw this, gain 5 gold."
},
{
"code": "CL-03",
"name": "Tetsu Mogami",
"role": "Fishmonger at the port market",
"boon": "Cold Chain",
"boonText": "Unplayable. When you draw this, generate 1 Cryo."
},
{
"code": "CL-04",
"name": "Haruka Sen",
"role": "Night-shift head nurse",
"boon": "Night Nurse",
"boonText": "Unplayable. When you draw this, 10% chance to repair 1 HQ HP."
},
{
"code": "CL-05",
"name": "Daigo Ueno",
"role": "Factory line foreman",
"boon": "Line Quota",
"boonText": "Unplayable. When you draw this, generate 1 Series."
},
{
"code": "CL-06",
"name": "Mei Tanabe",
"role": "Startup founder",
"boon": "Beta Build",
"boonText": "Unplayable. When you draw this, generate 1 Adaptive."
},
{
"code": "CL-07",
"name": "Kenta Arima",
"role": "Insurance middle manager",
"boon": "Morning Coffee",
"boonText": "Unplayable. When you draw this, gain 1 Energy at the start of next turn."
},
{
"code": "CL-08",
"name": "Sayo Imai",
"role": "Local idol",
"boon": null,
"boonText": null
},
{
"code": "CL-09",
"name": "Goro Tsuda",
"role": "Shrine keeper",
"boon": null,
"boonText": null
},
{
"code": "CL
.../v1/events.jsonRoute events with their options and effects.
{
"events": [
{
"code": "EV-01",
"name": "Parking Structure Job",
"setup": "A property manager needs a rift nest cleared from level B3 before the morning commute. Cash, no paperwork.",
"options": [
{
"text": "Lose 4 HQ HP. Gain 60 gold.",
"label": "Take the job",
"effect": "hp:-4;gold:+60"
},
{
"text": "Nothing happens.",
"label": "Pass",
"effect": "none"
}
]
},
{
"code": "EV-02",
"name": "Vending Machine",
"setup": "A vending machine by the train platform stocks ration bars, energy drinks and, for some reason, spare turret parts.",
"options": [
{
"text": "Add a random Boon to your deck.",
"label": "Pay 20 gold",
"effect": "gold:-20;card:boon"
},
{
"text": "50%: gain 30 gold. 50%: lose 3 HQ HP.",
"label": "Kick it",
"effect": "chance:gold:+30|hp:-3"
}
]
},
{
"code": "EV-03",
"name": "Big Firm Surplus Sale",
"setup": "A big firm is clearing out last season's stock from a warehouse in the port.",
"options": [
{
"text": "Choose 1 of 3 Uncommon or Rare cards to add to your deck.",
"label": "Pay 40 gold",
"effect": "gold:-40;choose:3:uncommon+"
},
{
"text": "Nothing happens.",
"label": "Leave",
"effect": "none"
}
]
},
{
"code": "EV-04",
"name": "Festival Setup",
"setup": "The street festival goes ahead tonight, rifts or not. The organisers are short of hands.",
"options": [
{
"text": "Repair the van for 5 HQ HP.",
"label": "Help set up",
"effect": "hp:+5
.../v1/artifacts.jsonAll artifacts with rarity, category and effect text.
{
"code": "AR-01",
"name": "Spare Parts Crate",
"rarity": "Common",
"cat": "Sentry build",
"effect": "At the start of each stage, generate 2 random sentries."
}/v1/specialTiles.jsonThe five special tiles and their effects.
{
"key": "breaker",
"name": "Breaker Pad",
"effect": "+100% damage vs shielded enemies, -50% vs unshielded"
}/v1/landmarks.jsonLandmark building types and their bonus rules.
{
"types": {
"koban": "Police Box",
"konbini": "Convenience Store",
"mast": "Radio Mast"
},
"rules": "One landmark per map on its building slot. Its bonus is revealed after wave 1 and applies to every tower and sentry on the 8 adjacent tiles. Possible bonuses: +30% attack, +1 range tile, ignores shields, slows 1s, Vulnerable 2s, +1 gold per kill."
}/v1/formulas.jsonExact source code of the game's core formulas: wave budget and pools, enemy level scaling, card cost, sell value, difficulty curve, reward picks.
{
"name": "wavesCfg",
"source": "(world,st,cfg)=>Math.max(1,Math.min(10,Math.round((cfg.stageWaves||DEFAULT_STAGE_WAVES)[st-1]||5)+cfg.worldWaves*(world-1)))"
}/v1/all.jsonEverything above in one file.
Copied from the build. CFG values are the defaults in /v1/runRules.json; G is the live game state.
wavesCfg(world,st,cfg)=>Math.max(1,Math.min(10,Math.round((cfg.stageWaves||DEFAULT_STAGE_WAVES)[st-1]||5)+cfg.worldWaves*(world-1)))
budgetForfunction budgetFor(world,st,w,cfg=CFG){ const p=progOf(world,st,w,cfg), shape=(cfg.nodeShape===false)?1:((NODE_SHAPE[Math.max(1,Math.min(3,world))]||[])[st-1]||1);
return (cfg.bBase+cfg.bMul*waveCurve(w,cfg))*(1+cfg.bStage*(st-1)*p)*(1+cfg.bWorld*(world-1)*p)*shape; }topLevelForfunction topLevelFor(world,st,w,cfg=CFG){ const p=progOf(world,st,w,cfg);
return Math.max(1,Math.min(10*world,1+Math.round(cfg.lvlK*Math.pow(Math.max(0,w-1),cfg.lvlQ)+cfg.lvlPerStage*(st-1)*p+cfg.lvlPerWorld*(world-1)*p))); }threatRangefunction threatRange(world,st,w,cfg=CFG){
const lo=cfg["thLo"+world]||1, hi=cfg["thHi"+world]||10;
return [lo,Math.max(lo,Math.round(lo+(hi-lo)*progOf(world,st,w,cfg)))];
}packCost(code,n,L)=>(ENEMIES[code].hp+ENEMIES[code].shield)/60*n*lvlMult(L)
pickWeightedfunction pickWeighted(pool){
const wt=c=>(EB[c]&&EB[c].attack)?CFG.atkWeight:1, tot=pool.reduce((a,c)=>a+wt(c),0);
let x=Math.random()*tot; for(const c of pool){ x-=wt(c); if(x<=0) return c; } return pool[pool.length-1];
}planPacksfunction planPacks(world,st,w,elite){
let budget=budgetFor(world,st,w)*(elite?(world===1?CFG.eliteBudgetW1:CFG.eliteBudget):1);
// D10 is each world's climax: stronger enemies, not just more of them (levels past the world cap, tougher types)
const climax=st>=10, cl=climax?(world===1?CFG.climaxLevelsW1:CFG.climaxLevels)||0:0;
const top=Math.min(10*world+cl,topLevelFor(world,st,w)+(elite?CFG.eliteLevels:0)+cl), low=Math.max(1,top-Math.max(1,CFG.lvlSpread)+1);
let [tLo,tHi]=threatRange(world,st,w); if(climax) tHi=Math.min(10,tHi+((world===1?CFG.climaxThreatW1:CFG.climaxThreat)||0));
let pool=ENEMIES_RAW.map(r=>r[0]).filter(c=>ENEMIES[c].threat>=tLo&&ENEMIES[c].threat<=tHi);
if(!pool.length) pool=["E-002"];
const cheapest=pool.slice().sort((a,b)=>packCost(a,1,low)-packCost(b,1,low))[0];
const packs=[];
for(let guard=0;guard<400;guard++){
let code=pickWeighted(pool), L=packs.length?low+Math.floor(Math.random()*(top-low+1)):top, n=packSize(code,L);
while(n>1&&packCost(code,n,L)>budget) n--;
if(packCost(code,n,L)>budget){ code=cheapest; L=low; n=1; if(packCost(code,n,L)>budget) break; }
budget-=packCost(code,n,L); packs.push({code,n,lvl:L});
}
if(!packs.length) packs.push({code:cheapest,n:1,lvl:low});
return packs;
}eStatsfunction eStats(code,L){
const E=ENEMIES[code]; L=Math.max(1,Math.min(30,L||1));
return {hp:E.hp*(1+CFG.lvlHp/100*(L-1)),sh:E.shield*(1+CFG.lvlShield/100*(L-1)),spd:E.speed*(1+CFG.lvlSpd/100*(L-1)),leak:E.leak};
}cardCostfunction cardCost(card){
const cd=CARDS[card.code]; let c=cd.cost;
if(card.code==="C-028") c=Math.max(0,c-sentries().length);
if(card.disc) c=Math.max(0,c-card.disc);
if(card.fpTurn===G.turn&&CARDS[card.code].type!=="Boon") c+=card.fpN||1; // Fine Print
if(hasArt("AR-19")&&/Tower|Sentry/.test(cd.type)&&!G.freeTower) return 0;
if(hasArt("AR-02")&&cd.type==="Sentry"&&!G.freeSentryUsed) c=0;
if(hasArt("AR-27")&&cd.type==="Module"&&!G.freeModuleUsed) c=0;
if(hasArt("AR-29")&&cd.type==="Spell"&&!G.freeSpellUsed) c=Math.max(0,c-1);
if(c>0&&cd.type!=="Boon") c=Math.max(0,c-inHand("C-065")*boonMult());
return c;
}sellValuet=>Math.floor((t.paid||0)*CFG.sellPct/100)
rowToStage(r,rows)=>1+Math.round(9*r/(rows-1))
tableDifficultyfunction tableDifficulty(world,row,rows,lineId,fallback){ const step=rows===12?row:Math.round(11*row/Math.max(1,rows-1));
const hit=NODE_DIFFICULTY.find(r=>r.world===world&&r.map_step===step&&r.line===lineId)||NODE_DIFFICULTY.find(r=>r.world===world&&r.map_step===step&&r.line==="all");
return hit?hit.difficulty:fallback; }maxHp()=>CFG.baseHp+(hasArt("AR-38")?10:0)stageRewardPicksfunction(...a){ const picks=_srp.apply(this,a); if(!G.bossKilled||!Array.isArray(picks)) return picks;
const pool=Object.values(CARDS).filter(c=>c.rarity==="Rare"&&inProto(c.code)&&!REJECTED.has(c.code)&&(typeof usable!=="function"||usable(c.code))).map(c=>c.code);
for(let i=pool.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [pool[i],pool[j]]=[pool[j],pool[i]]; }
return picks.map((p,i)=>pool[i]||p); }Contract: Zero, Algorocks. Internal API for the development team.