ajhahn.de
← Theria
GDScript 788 lines
class_name AbilityData
extends RefCounted
## The ability catalog: every ability as a data row, and every hero kit as a map
## from form and slot to an ability id plus that form's resource pool. Pure static
## data — the single source of truth the simulation reads to equip a hero and the
## executor reads to act. New abilities and heroes are added here, by value, with
## no engine or render coupling, so the whole roster stays unit-testable.
##
## "wildkin" is the schema-proving kit: a generic shapeshifter that exercises the
## whole catalog — all four targeting modes, the three effects, a per-form resource,
## and the human/animal transform. It stays as the reference the schema tests drive.
##
## The first tribe's roster is authored on top of it: the **Solane** — savanna big-cat
## shifters. Three mirror heroes, each a human kit plus an animal kit, built
## from the same DAMAGE/HEAL/TRANSFORM primitives but given distinct identities through
## their targeting mix, their tuning, and their resource economy:
##   - **Lion**    — a frontline bruiser: short-range poke, a heavy single-target Maul,
##                   and the deepest self-sustain, on a generous, slow-spending pool.
##   - **Cheetah** — a burst skirmisher: long-range pokes and a repeatable single-target
##                   shred, on a lean, fast-regenerating pool (hit and run).
##   - **Hyena**   — a zone controller: the widest ground areas in both forms for
##                   attrition, on a baseline pool.
##
## The opposing tribe, the **Verdani** — jungle venom-and-shadow shifters — is authored
## on the same primitives, a deliberate foil to the Solane archetypes:
##   - **Snake**     — a venom striker: a long single-target lock, a cheap low-cooldown
##                     Fang Strike, and a heavy Venom Coil payoff, on a mid-tier pool.
##   - **Spider**    — a trapper: the longest, widest, lowest-power ground webs in the
##                     game — a long snaring slow and a brief hard lock — on the deepest,
##                     slowest-regen pool.
##   - **Chameleon** — an ambusher: a short hard skillshot and the single heaviest hit
##                     in either tribe, on the leanest, fastest-refilling pool.
## In a practice match the player's squad fields the Solane and the bot squad the
## Verdani, so both rosters and all four targeting modes are exercised at once. The
## Verdani's venom and web are now mechanical, not just named: their striking abilities
## carry a lingering status (see AbilitySpec.STATUS_*) — venom is a damage-over-time, web
## a movement slow, and the Spider's nest a brief hard stun — so the bite keeps biting,
## the snare actually snares, and the nest locks. Each such ability trades part of its
## instant power for that lingering effect, so the Verdani lean attrition and control
## where the Solane stay burst.

## Ability rows keyed by catalog id. Each row is parsed on demand into a typed
## AbilitySpec by `spec`; a sparse row leans on the spec defaults. The dictionary's
## insertion order is stable, which keeps any iteration over the roster
## deterministic, like the rest of the simulation.
const ABILITIES := {
	# --- wildkin, human form -------------------------------------------------
	1:
	{
		"id": 1,
		"name": "Spirit Bolt",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_SKILLSHOT,
		"range": 600.0,
		"radius": 60.0,  # a tight bolt: clips enemies at its landing point
		"cost": 20,
		"cooldown_ticks": 30,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 80,
	},
	2:
	{
		"id": 2,
		"name": "Mend",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 1,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 30,
		"cooldown_ticks": 120,
		"effect": AbilitySpec.EFFECT_HEAL,
		"power": 100,
	},
	3:
	{
		"id": 3,
		"name": "Beast Form",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- wildkin, animal form ------------------------------------------------
	4:
	{
		"id": 4,
		"name": "Pounce",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_GROUND,
		"range": 400.0,
		"radius": 150.0,  # an area slam: every enemy inside the circle is struck
		"cost": 20,
		"cooldown_ticks": 24,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 60,
	},
	5:
	{
		"id": 5,
		"name": "Rend",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 2,
		"target_kind": AbilitySpec.TARGET_UNIT,
		"range": 200.0,
		"cost": 30,
		"cooldown_ticks": 48,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 120,
	},
	6:
	{
		"id": 6,
		"name": "Human Form",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Solane: Lion, human form (a short-range bruiser with deep self-sustain) ---
	10:
	{
		"id": 10,
		"name": "Sunfire Lash",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_SKILLSHOT,
		"range": 450.0,
		"radius": 70.0,
		"cost": 25,
		"cooldown_ticks": 36,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 65,
	},
	11:
	{
		"id": 11,
		"name": "Mane Guard",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 1,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 35,
		"cooldown_ticks": 150,
		"effect": AbilitySpec.EFFECT_HEAL,
		"power": 150,  # the deepest heal in the tribe: the bruiser's staying power
	},
	12:
	{
		"id": 12,
		"name": "Lion Form",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Solane: Lion, animal form (engage area, then a heavy melee burst) --------
	13:
	{
		"id": 13,
		"name": "Pounce",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_GROUND,
		"range": 350.0,
		"radius": 160.0,
		"cost": 25,
		"cooldown_ticks": 30,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 70,
	},
	14:
	{
		"id": 14,
		"name": "Maul",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 2,
		"target_kind": AbilitySpec.TARGET_UNIT,
		"range": 190.0,  # melee: the bruiser must close to land its payoff
		"cost": 35,
		"cooldown_ticks": 48,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 160,  # the hardest single hit in the tribe
	},
	15:
	{
		"id": 15,
		"name": "Human Form",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Solane: Cheetah, human form (long pokes on a lean, fast pool) ------------
	20:
	{
		"id": 20,
		"name": "Spear Throw",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_SKILLSHOT,
		"range": 750.0,  # the longest reach in the tribe
		"radius": 50.0,  # but a tight line: it must be aimed
		"cost": 20,
		"cooldown_ticks": 24,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 90,
	},
	21:
	{
		"id": 21,
		"name": "Second Wind",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 1,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 25,
		"cooldown_ticks": 100,
		"effect": AbilitySpec.EFFECT_HEAL,
		"power": 80,  # a skirmisher's top-up, not the Lion's wall
	},
	22:
	{
		"id": 22,
		"name": "Cheetah Form",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Solane: Cheetah, animal form (single-target shred, repeatable) -----------
	23:
	{
		"id": 23,
		"name": "Hamstring",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_UNIT,
		"range": 280.0,
		"cost": 15,
		"cooldown_ticks": 18,  # the shortest cooldown in the tribe: harass on repeat
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 70,
	},
	24:
	{
		"id": 24,
		"name": "Killing Blow",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 2,
		"target_kind": AbilitySpec.TARGET_UNIT,
		"range": 220.0,
		"cost": 35,
		"cooldown_ticks": 50,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 140,
	},
	25:
	{
		"id": 25,
		"name": "Human Form",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Solane: Hyena, human form (the widest ground zone for attrition) ---------
	30:
	{
		"id": 30,
		"name": "Bone-Hex",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_GROUND,
		"range": 600.0,
		"radius": 190.0,
		"cost": 30,
		"cooldown_ticks": 40,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 55,
	},
	31:
	{
		"id": 31,
		"name": "Scavenge",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 1,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 30,
		"cooldown_ticks": 120,
		"effect": AbilitySpec.EFFECT_HEAL,
		"power": 100,
	},
	32:
	{
		"id": 32,
		"name": "Hyena Form",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Solane: Hyena, animal form (a bite, and a wide pack slam) ----------------
	33:
	{
		"id": 33,
		"name": "Rending Bite",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_UNIT,
		"range": 200.0,
		"cost": 20,
		"cooldown_ticks": 30,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 90,
	},
	34:
	{
		"id": 34,
		"name": "Pack Frenzy",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 2,
		"target_kind": AbilitySpec.TARGET_GROUND,
		"range": 320.0,
		"radius": 210.0,  # the widest area in the tribe
		"cost": 35,
		"cooldown_ticks": 44,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 60,
	},
	35:
	{
		"id": 35,
		"name": "Human Form",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Verdani: Snake, human form (a long venom poke on a precise pool) ----------
	40:
	{
		"id": 40,
		"name": "Venom Spit",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_SKILLSHOT,
		"range": 650.0,  # a long reach, just shy of the Cheetah's signature spear
		"radius": 55.0,  # but a thin line: it must be aimed
		"cost": 20,
		"cooldown_ticks": 24,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 50,  # trimmed from a pure poke: the rest of the bite is the venom below
		"status": AbilitySpec.STATUS_DOT,  # venom: lingering damage over two seconds
		"status_power": 6,
		"status_duration": 120,
		"status_interval": 30,
	},
	41:
	{
		"id": 41,
		"name": "Shed Skin",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 1,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 30,
		"cooldown_ticks": 110,
		"effect": AbilitySpec.EFFECT_HEAL,
		"power": 90,
	},
	42:
	{
		"id": 42,
		"name": "Serpent Form",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Verdani: Snake, animal form (the longest single-target lock, then a payoff) ---
	43:
	{
		"id": 43,
		"name": "Fang Strike",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_UNIT,
		"range": 360.0,  # the longest single-target lock in either tribe
		"cost": 15,
		"cooldown_ticks": 18,  # cheap and fast: harass on repeat
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 55,  # a lighter strike now that the fang leaves venom
		"status": AbilitySpec.STATUS_DOT,
		"status_power": 5,
		"status_duration": 120,
		"status_interval": 30,
	},
	44:
	{
		"id": 44,
		"name": "Venom Coil",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 2,
		"target_kind": AbilitySpec.TARGET_UNIT,
		"range": 300.0,
		"cost": 35,
		"cooldown_ticks": 50,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 105,  # the heavy payoff, now split between the coil and its deep venom
		"status": AbilitySpec.STATUS_DOT,  # the tribe's strongest venom: a heavy lingering bleed
		"status_power": 11,
		"status_duration": 120,
		"status_interval": 30,
	},
	45:
	{
		"id": 45,
		"name": "Human Form",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Verdani: Spider, human form (the longest, widest web for attrition) -------
	50:
	{
		"id": 50,
		"name": "Web Snare",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_GROUND,
		"range": 620.0,
		"radius": 200.0,
		"cost": 30,
		"cooldown_ticks": 38,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 45,  # the lowest per-hit power in either tribe: attrition, now with a snare
		"status": AbilitySpec.STATUS_SLOW,  # web: the strongest, longest slow — the trapper's lock
		"status_power": 45,  # a 45% slow
		"status_duration": 150,
	},
	51:
	{
		"id": 51,
		"name": "Silk Mend",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 1,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 30,
		"cooldown_ticks": 120,
		"effect": AbilitySpec.EFFECT_HEAL,
		"power": 100,
	},
	52:
	{
		"id": 52,
		"name": "Spider Form",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Verdani: Spider, animal form (a close bite, then the widest nest) ----------
	53:
	{
		"id": 53,
		"name": "Venom Bite",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_UNIT,
		"range": 210.0,
		"cost": 20,
		"cooldown_ticks": 30,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 65,  # a lighter bite, the rest delivered as venom
		"status": AbilitySpec.STATUS_DOT,
		"status_power": 5,
		"status_duration": 120,
		"status_interval": 30,
	},
	54:
	{
		"id": 54,
		"name": "Web Nest",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 2,
		"target_kind": AbilitySpec.TARGET_GROUND,
		"range": 340.0,
		"radius": 220.0,  # the widest area in either tribe
		"cost": 35,
		"cooldown_ticks": 60,  # a longer recharge: a hard lock is a deliberate engage, not spam
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 35,  # trimmed hard — the nest's payoff is now the lock, not the hit
		"status": AbilitySpec.STATUS_STUN,  # web: a brief hard lock (no move, cast, or attack)
		"status_duration": 30,  # half a second frozen
	},
	55:
	{
		"id": 55,
		"name": "Human Form",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Verdani: Chameleon, human form (a short, hard skillshot on a lean pool) ----
	60:
	{
		"id": 60,
		"name": "Tongue Lash",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_SKILLSHOT,
		"range": 380.0,  # short: the ambusher fights up close
		"radius": 60.0,
		"cost": 25,
		"cooldown_ticks": 30,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 95,  # a heavy poke for its range
	},
	61:
	{
		"id": 61,
		"name": "Blend",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 1,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 25,
		"cooldown_ticks": 100,
		"effect": AbilitySpec.EFFECT_HEAL,
		"power": 75,  # a skirmisher's top-up, not a wall
	},
	62:
	{
		"id": 62,
		"name": "Chameleon Form",
		"form": AbilitySpec.FORM_HUMAN,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
	# --- Verdani: Chameleon, animal form (a cheap dart, then the heaviest ambush) ---
	63:
	{
		"id": 63,
		"name": "Color Dart",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 0,
		"target_kind": AbilitySpec.TARGET_SKILLSHOT,
		"range": 300.0,
		"radius": 50.0,
		"cost": 15,
		"cooldown_ticks": 20,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 60,
	},
	64:
	{
		"id": 64,
		"name": "Ambush",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 2,
		"target_kind": AbilitySpec.TARGET_UNIT,
		"range": 200.0,  # melee: the payoff for closing the gap
		"cost": 35,
		"cooldown_ticks": 52,
		"effect": AbilitySpec.EFFECT_DAMAGE,
		"power": 165,  # the single heaviest hit in either tribe
	},
	65:
	{
		"id": 65,
		"name": "Human Form",
		"form": AbilitySpec.FORM_ANIMAL,
		"slot": 3,
		"target_kind": AbilitySpec.TARGET_SELF,
		"cost": 0,
		"cooldown_ticks": 60,
		"effect": AbilitySpec.EFFECT_TRANSFORM,
	},
}

## Bot stance per kit: how a bot positions a hero it drives. BRAWL — the default for
## any kit that names no stance — closes the gap and shifts toward whichever form can
## land a hit. KITE holds the kit's ranged poke and keeps the enemy at arm's length,
## fighting hit-and-run from its skillshot band rather than committing to melee. Read by
## BotController and stamped onto the hero at equip; a player-driven hero ignores it.
const STANCE_BRAWL := 0
const STANCE_KITE := 1


## Hero kits keyed by kit id. A kit names, per form, the resource pool (`max` and
## the regen interval `regen_ticks` — one resource point restored every that many
## ticks, 0 for none) and the slot-to-ability-id bar. A hero equipped with a kit
## starts in human form with that form's resource full. Integer regen on a tick
## interval keeps resource growth deterministic, like the cooldown counters.
const KITS := {
	"wildkin":
	{
		"resource":
		{
			# Human "Focus" and animal "Ferocity": same shape, distinct pools the
			# transform swaps between, so each stance meters its own casts.
			AbilitySpec.FORM_HUMAN: {"max": 100, "regen_ticks": 12},
			AbilitySpec.FORM_ANIMAL: {"max": 100, "regen_ticks": 12},
		},
		"abilities":
		{
			AbilitySpec.FORM_HUMAN: {0: 1, 1: 2, 3: 3},
			AbilitySpec.FORM_ANIMAL: {0: 4, 2: 5, 3: 6},
		},
	},
	# --- Solane (savanna big-cats), the v0.1 mirror tribe ---------------------
	"lion":
	{
		# A bruiser: a generous pool that spends slowly, to back the deep heal and
		# the heavy Maul.
		"resource":
		{
			AbilitySpec.FORM_HUMAN: {"max": 120, "regen_ticks": 10},
			AbilitySpec.FORM_ANIMAL: {"max": 120, "regen_ticks": 10},
		},
		"abilities":
		{
			AbilitySpec.FORM_HUMAN: {0: 10, 1: 11, 3: 12},
			AbilitySpec.FORM_ANIMAL: {0: 13, 2: 14, 3: 15},
		},
	},
	"cheetah":
	{
		# A skirmisher: a lean pool that refills fast, to chain cheap pokes and the
		# low-cooldown Hamstring. A kiter — it holds its long Spear Throw range.
		"stance": STANCE_KITE,
		"resource":
		{
			AbilitySpec.FORM_HUMAN: {"max": 80, "regen_ticks": 8},
			AbilitySpec.FORM_ANIMAL: {"max": 80, "regen_ticks": 8},
		},
		"abilities":
		{
			AbilitySpec.FORM_HUMAN: {0: 20, 1: 21, 3: 22},
			AbilitySpec.FORM_ANIMAL: {0: 23, 2: 24, 3: 25},
		},
	},
	"hyena":
	{
		# A zone controller: a baseline pool feeding the wide ground areas.
		"resource":
		{
			AbilitySpec.FORM_HUMAN: {"max": 100, "regen_ticks": 12},
			AbilitySpec.FORM_ANIMAL: {"max": 100, "regen_ticks": 12},
		},
		"abilities":
		{
			AbilitySpec.FORM_HUMAN: {0: 30, 1: 31, 3: 32},
			AbilitySpec.FORM_ANIMAL: {0: 33, 2: 34, 3: 35},
		},
	},
	# --- Verdani (jungle venom-and-shadow), the opposing tribe ----------------
	"snake":
	{
		# A striker: a precise mid-tier pool, between the Cheetah's lean and the
		# Hyena's baseline, to feed the cheap Fang Strike and the heavy Coil.
		"resource":
		{
			AbilitySpec.FORM_HUMAN: {"max": 90, "regen_ticks": 9},
			AbilitySpec.FORM_ANIMAL: {"max": 90, "regen_ticks": 9},
		},
		"abilities":
		{
			AbilitySpec.FORM_HUMAN: {0: 40, 1: 41, 3: 42},
			AbilitySpec.FORM_ANIMAL: {0: 43, 2: 44, 3: 45},
		},
	},
	"spider":
	{
		# A trapper: the deepest pool on the slowest regen, to sustain the wide,
		# cheap-per-cast webs over a long attrition.
		"resource":
		{
			AbilitySpec.FORM_HUMAN: {"max": 110, "regen_ticks": 13},
			AbilitySpec.FORM_ANIMAL: {"max": 110, "regen_ticks": 13},
		},
		"abilities":
		{
			AbilitySpec.FORM_HUMAN: {0: 50, 1: 51, 3: 52},
			AbilitySpec.FORM_ANIMAL: {0: 53, 2: 54, 3: 55},
		},
	},
	"chameleon":
	{
		# An ambusher: the leanest pool on the fastest regen, to land a burst and
		# refill for the next one — the most boom-and-bust economy of either tribe. A
		# kiter — it darts in and out at its Tongue Lash range rather than brawling.
		"stance": STANCE_KITE,
		"resource":
		{
			AbilitySpec.FORM_HUMAN: {"max": 70, "regen_ticks": 7},
			AbilitySpec.FORM_ANIMAL: {"max": 70, "regen_ticks": 7},
		},
		"abilities":
		{
			AbilitySpec.FORM_HUMAN: {0: 60, 1: 61, 3: 62},
			AbilitySpec.FORM_ANIMAL: {0: 63, 2: 64, 3: 65},
		},
	},
}


## The tribes: each tribe's hero roster, in seating order. The single source of which
## heroes form which tribe — the client reads it to seat a tribe-vs-tribe match, and the
## roster order fixes each hero's squad slot. The wildkin reference kit is deliberately
## in no tribe. v0.1 ships two tribes; a match pairs one against another (see
## `opposing_tribe`).
const TRIBE := {
	"solane": ["lion", "cheetah", "hyena"],
	"verdani": ["snake", "spider", "chameleon"],
}


## The typed spec for a catalog id. Parses the row on demand — the catalog is small
## and the executor caches nothing, so a spec is always read fresh by value.
static func spec(id: int) -> AbilitySpec:
	return AbilitySpec.from_dict(ABILITIES.get(id, {}))


## Whether an ability id exists in the catalog.
static func has_ability(id: int) -> bool:
	return ABILITIES.has(id)


## A kit definition by id, or an empty dictionary if unknown.
static func kit(kit_id: String) -> Dictionary:
	return KITS.get(kit_id, {})


## The tribe a hero kit belongs to, or "" if the kit is in no tribe (the wildkin reference
## kit, or an unknown name). A pure lookup over the roster data.
static func tribe_of(kit_id: String) -> String:
	for tribe in TRIBE:
		if (TRIBE[tribe] as Array).has(kit_id):
			return tribe
	return ""


## A hero kit's seat index within its tribe (0..n-1, the order it sits in `TRIBE`), or -1
## if the kit is in no tribe (the wildkin reference kit, or an unknown name). The renderer
## shades a hero's team colour by this index so squadmates read apart; a pure roster lookup.
static func roster_index(kit_id: String) -> int:
	var tribe := tribe_of(kit_id)
	if tribe == "":
		return -1
	return (TRIBE[tribe] as Array).find(kit_id)


## The tribe a given tribe is matched against — the next other tribe in declaration order.
## v0.1 fields exactly two, so this is simply "the other one"; returns `tribe` itself if
## it is the only tribe defined.
static func opposing_tribe(tribe: String) -> String:
	for other in TRIBE:
		if other != tribe:
			return other
	return tribe