Commit
Theria
feat: author the Verdani roster and field it against the Solane
modified CHANGELOG.md
@@ -35,6 +35,14 @@ protocol version.
### Added
- A second hero roster, the **Verdani** — jungle venom-and-shadow shifters (snake,
spider, chameleon) — joins the Solane as the opposing Volk, authored on the same
ability primitives as a deliberate foil: the snake is a venom striker with the
longest single-target lock, the spider a trapper laying the widest, lowest-power
ground webs, and the chameleon an ambusher carrying the single heaviest hit of either
roster. A practice match now fields the player's Solane squad against a bot Verdani
squad rather than a Solane mirror, so both rosters are exercised at once. Sim-side
content only; the netcode protocol is unchanged.
- Bots now shapeshift mid-fight instead of fighting from one form: a bot transforms
toward the form that can land a hit when its current one cannot — closing into its
harder-hitting animal kit as an enemy slips inside the human poke's range, and back
modified README.md
@@ -34,11 +34,12 @@ lanes and an equatorial jungle to break each other's nexus.
The first milestone is a **walking skeleton**: one player-controlled hero and
one bot moving on the 3v3 arena under a server-authoritative, fixed-timestep
simulation. With that authority model proven, networked play over a
listen-server, the hero ability layer, and the first roster of heroes — the
**Solane**, savanna big-cat shifters (lion, cheetah, hyena) — now run on top of
it. A practice match fields the full Solane squad per team — the player drives
one hero, bots fill the rest — so all three kits are on the field at once; the
opposing Volk, multi-hero teams over the wire, and the art direction come next.
listen-server, the hero ability layer, and two full rosters of heroes — the
**Solane**, savanna big-cat shifters (lion, cheetah, hyena), and the opposing
**Verdani**, jungle venom-and-shadow shifters (snake, spider, chameleon) — now
run on top of it. A practice match fields the Solane squad against the Verdani —
the player drives one Solane hero, bots fill the rest — so both rosters are on
the field at once; multi-hero teams over the wire and the art direction come next.
## Architecture
@@ -86,9 +87,10 @@ godot --path .
A connect screen opens: choose **Practice** for a single-machine match, **Host** to
start a listen-server, or type an address and **Join** one. Practice fields the full
Solane squad on each team — you drive one hero (the lion by default, or pass
`--hero cheetah`/`--hero hyena` on the command line), bots drive the rest. A hosted
or joined match is still a one-hero-per-team duel on the lion until multi-hero play
Solane squad against the opposing Verdani squad — you drive one Solane hero (the lion
by default, or pass `--hero cheetah`/`--hero hyena` on the command line), bots drive
the rest. A hosted or joined match is still a one-hero-per-team duel on the lion until
multi-hero play
reaches the wire. Move the hero with **WASD** or the **arrow keys**; the bots close
on the nearest enemy and cast their own kits — healing when hurt, otherwise firing
the reachable ability of their form, and shifting form to keep a hit in reach
modified src/client/main.gd
@@ -72,13 +72,15 @@ const CREEP_HP_BAR_OFFSET := Vector2(-35.0, -55.0)
const HP_BAR_BG := Color(0.0, 0.0, 0.0, 0.6)
const HP_BAR_FG := Color(0.4, 0.85, 0.4)
## The Solane roster each team fields in a LOCAL practice match, one hero per kit
## (see AbilityData). The player drives one of them — picked with `--hero`, default
## the first — and the rest are bot-driven allies and opponents, so all three
## authored kits are reachable in-game. HOST/CLIENT still seat the one-per-team duel
## (DUEL_KIT below): the wire identifies a hero by its team, so a networked squad
## waits on the protocol step that gives each client a controlled-entity id.
## The two Volk a LOCAL practice match fields, one hero per kit (see AbilityData): the
## player's squad draws the Solane, the bot squad the opposing Verdani, so a single
## match exercises both rosters and all four targeting modes. The player drives one
## Solane hero — picked with `--hero`, default the first — and every other seat on both
## teams is bot-driven. HOST/CLIENT still seat the one-per-team duel (DUEL_KIT below):
## the wire identifies a hero by its team, so a networked squad waits on the protocol
## step that gives each client a controlled-entity id.
const SOLANE_ROSTER: Array[String] = ["lion", "cheetah", "hyena"]
const VERDANI_ROSTER: Array[String] = ["snake", "spider", "chameleon"]
## The kit both heroes mirror in a HOST/CLIENT duel — the one-per-team walking
## skeleton the netcode is built around until the multi-hero wire step lands.
@@ -288,23 +290,23 @@ func _close_menu_and_enter() -> void:
_enter_match()
## Practice: both teams field the full Solane squad, one hero per roster kit. The
## player drives the seat picked by `--hero`; the other five are bot-driven, so all
## three authored kits are on the field at once.
## Practice: the player's team fields the full Solane squad, the bot team the opposing
## Verdani squad, one hero per roster kit. The player drives the Solane seat picked by
## `--hero`; the other five are bot-driven, so both rosters are on the field at once.
func _start_local() -> void:
_sim = _new_world()
_seat_squad(HERO_TEAM, HERO_SPEED, _player_slot())
_seat_squad(BOT_TEAM, BOT_SPEED, -1)
## Seats one Solane hero per roster kit for `team`, each fanned across the base
## fountain and equipped with its kit. The seat at `player_slot` becomes the
## player's hero (`_hero_id`); every other seat is bot-driven (appended to
## `_bot_ids`). A `player_slot` of -1 leaves the whole team bot-driven.
func _seat_squad(team: int, speed: float, player_slot: int) -> void:
for i in SOLANE_ROSTER.size():
var id := _sim.add_hero(team, MapData.squad_spawn(team, i, SOLANE_ROSTER.size()), speed)
_sim.equip_kit(id, SOLANE_ROSTER[i])
_seat_squad(HERO_TEAM, HERO_SPEED, SOLANE_ROSTER, _player_slot())
_seat_squad(BOT_TEAM, BOT_SPEED, VERDANI_ROSTER, -1)
## Seats one hero per kit in `roster` for `team`, each fanned across the base fountain
## and equipped with its kit. The seat at `player_slot` becomes the player's hero
## (`_hero_id`); every other seat is bot-driven (appended to `_bot_ids`). A
## `player_slot` of -1 leaves the whole team bot-driven.
func _seat_squad(team: int, speed: float, roster: Array[String], player_slot: int) -> void:
for i in roster.size():
var id := _sim.add_hero(team, MapData.squad_spawn(team, i, roster.size()), speed)
_sim.equip_kit(id, roster[i])
if i == player_slot:
_hero_id = id
else:
modified src/sim/ability_data.gd
@@ -20,8 +20,19 @@ extends RefCounted
## 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.
## v0.1 is a mirror match, so both teams draw from this one Volk; the opposing Volk
## (the Verdani draft) is authored when the second Volk lands.
##
## The opposing Volk, 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 for pure attrition, on the deepest, slowest-regen pool.
## - **Chameleon** — an ambusher: a short hard skillshot and the single heaviest hit
## in either Volk, 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 two
## Volk are still effect-mirrors (DAMAGE/HEAL/TRANSFORM); the venom/web flavor is carried
## by their targeting mix, tuning, and economy until a richer effect schema lands.
## 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
@@ -335,6 +346,236 @@ const ABILITIES := {
"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": 70,
},
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 Volk
"cost": 15,
"cooldown_ticks": 18, # cheap and fast: harass on repeat
"effect": AbilitySpec.EFFECT_DAMAGE,
"power": 75,
},
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": 150,
},
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": 50, # the lowest per-hit power in either Volk: pure attrition
},
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": 85,
},
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 Volk
"cost": 35,
"cooldown_ticks": 46,
"effect": AbilitySpec.EFFECT_DAMAGE,
"power": 55,
},
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 Volk
},
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,
},
}
## Hero kits keyed by kit id. A kit names, per form, the resource pool (`max` and
@@ -403,6 +644,52 @@ const KITS := {
AbilitySpec.FORM_ANIMAL: {0: 33, 2: 34, 3: 35},
},
},
# --- Verdani (jungle venom-and-shadow), the opposing Volk ----------------
"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 Volk.
"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},
},
},
}
added test/unit/test_verdani.gd
@@ -0,0 +1,173 @@
extends GutTest
## Data checks on the Verdani roster — the second Volk's three hero kits (Snake, Spider,
## Chameleon), the jungle foil to the Solane. The executor itself is proven by
## test_ability.gd; these tests prove the *content*: the right ability sits in the right
## slot and form, the tuned numbers land, the transforms flip, the three economies are
## tiered as designed, and the Verdani ids stay clear of the Solane the squad shares the
## arena with. Headless and deterministic, creep waves off, so only the ability under
## test changes the world.
const VERDANI := ["snake", "spider", "chameleon"]
const SOLANE := ["lion", "cheetah", "hyena"]
func _verdani(sim: SimCore, kit_id: String, team: int, pos: Vector2) -> int:
var id := sim.add_hero(team, pos, 320.0)
sim.equip_kit(id, kit_id)
return id
## Equips a Verdani hero and transforms it to its animal form with a real cast (human
## slot 3 = that hero's beast-form ability), so the animal-kit tests start from the form
## the transform actually produces.
func _verdani_animal(sim: SimCore, kit_id: String, team: int, pos: Vector2) -> int:
var id := _verdani(sim, kit_id, team, pos)
var beast := InputCommand.new()
beast.ability_slot = 3
sim.step({id: beast})
return id
# --- Roster shape -----------------------------------------------------------
func test_verdani_kits_are_well_formed() -> void:
for kit_id in VERDANI:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _verdani(sim, kit_id, 0, Vector2.ZERO)
var h := sim.state.get_entity(id)
assert_true(h.is_hero, "%s is an ability caster once equipped" % kit_id)
assert_eq(h.form, AbilitySpec.FORM_HUMAN, "%s starts in human form" % kit_id)
for form in [AbilitySpec.FORM_HUMAN, AbilitySpec.FORM_ANIMAL]:
var slots: Dictionary = h.kit[form]
assert_true(slots.has(3), "%s form %d carries a transform in slot 3" % [kit_id, form])
for slot in slots:
var spec := AbilityData.spec(slots[slot])
assert_true(AbilityData.has_ability(spec.id), "%s slot %d is a real ability" % [kit_id, slot])
assert_eq(spec.form, form, "%s slot %d ability is in its own form" % [kit_id, slot])
assert_eq(spec.slot, slot, "%s ability %d sits in its kit slot" % [kit_id, spec.id])
assert_eq(
AbilityData.spec(slots[3]).effect,
AbilitySpec.EFFECT_TRANSFORM,
"%s slot 3 is the transform" % kit_id,
)
func test_verdani_kits_use_disjoint_ability_ids() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var seen := {}
var total := 0
for kit_id in VERDANI:
var id := _verdani(sim, kit_id, 0, Vector2.ZERO)
var kit: Dictionary = sim.state.get_entity(id).kit
for form in kit:
for slot in kit[form]:
seen[kit[form][slot]] = true
total += 1
assert_eq(seen.size(), total, "no two Verdani kits share an ability id (a copy-paste guard)")
func test_verdani_ids_are_clear_of_the_solane() -> void:
# Both Volk are on the field at once in a practice match, so their catalog ids must
# not collide — a Solane id and a Verdani id resolving to one row would cross-wire
# the two rosters.
var sim := SimCore.new()
sim.spawn_creeps = false
var solane_ids := {}
for kit_id in SOLANE:
var kit: Dictionary = sim.state.get_entity(_verdani(sim, kit_id, 0, Vector2.ZERO)).kit
for form in kit:
for slot in kit[form]:
solane_ids[kit[form][slot]] = true
for kit_id in VERDANI:
var kit: Dictionary = sim.state.get_entity(_verdani(sim, kit_id, 1, Vector2.ZERO)).kit
for form in kit:
for slot in kit[form]:
var vid: int = kit[form][slot]
assert_false(solane_ids.has(vid), "Verdani id %d is clear of the Solane" % vid)
# --- Each hero's signature ability lands its tuning --------------------------
func test_snake_fang_strike_locks_the_longest_single_target() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _verdani_animal(sim, "snake", 0, Vector2.ZERO)
sim.state.get_entity(id).attack_damage = 0 # isolate Fang Strike from the auto-attack
var far := sim.add_entity(1, Vector2(360.0, 0.0), 0.0, 600) # at Fang Strike's full 360 lock
var cast := InputCommand.new()
cast.ability_slot = 0 # animal Q = Fang Strike
cast.target_id = far
sim.step({id: cast})
assert_eq(sim.state.get_entity(far).hp, 525, "Fang Strike locks a target 360 away for its 75")
assert_eq(sim.state.get_entity(id).resource, 75, "and spends its cheap 15 from the 90 pool")
func test_spider_web_nest_zones_the_widest_area() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _verdani_animal(sim, "spider", 0, Vector2.ZERO)
sim.state.get_entity(id).attack_damage = 0 # isolate Web Nest from the auto-attack
# Web Nest: GROUND, range 340, radius 220 — the widest in either Volk. Land it at
# (300,0) and bracket an enemy just inside the radius against one just outside.
var inside := sim.add_entity(1, Vector2(300.0, 210.0), 0.0, 600) # 210 from centre -> hit
var outside := sim.add_entity(1, Vector2(300.0, 235.0), 0.0, 600) # 235 from centre -> spared
var cast := InputCommand.new()
cast.ability_slot = 2 # animal E = Web Nest
cast.target_point = Vector2(300.0, 0.0)
sim.step({id: cast})
assert_eq(sim.state.get_entity(inside).hp, 545, "an enemy in the wide nest takes Web Nest's 55")
assert_eq(sim.state.get_entity(outside).hp, 600, "an enemy beyond its 220 radius is spared")
func test_chameleon_ambush_is_the_heaviest_single_hit() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _verdani_animal(sim, "chameleon", 0, Vector2.ZERO)
sim.state.get_entity(id).attack_damage = 0 # isolate Ambush from the auto-attack
var enemy := sim.add_entity(1, Vector2(180.0, 0.0), 0.0, 600) # inside Ambush's 200 range
var cast := InputCommand.new()
cast.ability_slot = 2 # animal E = Ambush
cast.target_id = enemy
sim.step({id: cast})
assert_eq(
sim.state.get_entity(enemy).hp, 435, "Ambush deals its 165 — the hardest hit in either Volk"
)
assert_eq(sim.state.get_entity(id).resource, 35, "and spends its 35 from the lean 70 pool")
# --- Forms and economy ------------------------------------------------------
func test_each_verdani_hero_transforms_to_its_beast() -> void:
for kit_id in VERDANI:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _verdani(sim, kit_id, 0, Vector2.ZERO)
var beast := InputCommand.new()
beast.ability_slot = 3
sim.step({id: beast})
assert_eq(
sim.state.get_entity(id).form,
AbilitySpec.FORM_ANIMAL,
"%s flips to its animal form on the slot-3 transform" % kit_id,
)
func test_verdani_resource_economies_are_tiered() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var snake := sim.state.get_entity(_verdani(sim, "snake", 0, Vector2(0, 0)))
var spider := sim.state.get_entity(_verdani(sim, "spider", 0, Vector2(50, 0)))
var chameleon := sim.state.get_entity(_verdani(sim, "chameleon", 0, Vector2(100, 0)))
assert_eq(chameleon.resource_max, 70, "the chameleon runs the leanest pool")
assert_eq(snake.resource_max, 90, "the snake sits between, a precise mid-tier pool")
assert_eq(spider.resource_max, 110, "the spider carries the deepest pool")
assert_true(
chameleon.resource_regen_ticks < snake.resource_regen_ticks
and snake.resource_regen_ticks < spider.resource_regen_ticks,
"and the leaner the pool, the faster it refills",
)