GDScript 283 lines
extends GutTest
## Deterministic checks on the hero ability layer — targeting, the per-form
## resource, cooldowns, and the shapeshift transform. These run headless against the
## exact step function the live client (later) drives, with creep waves off so only
## the ability under test changes the world.
func _hero(sim: SimCore, team: int, pos: Vector2) -> int:
var id := sim.add_hero(team, pos, 320.0)
sim.equip_kit(id, "wildkin")
return id
## Equips a wildkin hero and transforms it to its animal form with a real Beast Form
## cast, so the animal-kit tests start from the form the transform actually produces.
func _animal_hero(sim: SimCore, team: int, pos: Vector2) -> int:
var id := _hero(sim, team, pos)
var beast := InputCommand.new()
beast.ability_slot = 3 # human slot 3 = Beast Form
sim.step({id: beast})
return id
# --- Equip + form -----------------------------------------------------------
func test_equip_kit_makes_a_human_caster_with_a_full_pool() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, 0, Vector2.ZERO)
var h := sim.state.get_entity(id)
assert_true(h.is_hero, "an equipped hero is an ability caster")
assert_eq(h.form, AbilitySpec.FORM_HUMAN, "a freshly equipped hero starts human")
assert_eq(h.resource, 100, "and with its human pool full")
assert_eq(h.resource_max, 100)
assert_eq(h.kit[AbilitySpec.FORM_HUMAN][0], 1, "Spirit Bolt sits in the human Q slot")
# --- Targeting: skillshot, ground, unit -------------------------------------
func test_skillshot_strikes_an_enemy_at_its_landing_point() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, 0, Vector2.ZERO)
# Range 600 along +x; the bolt flies the full range and clips an enemy there.
var enemy := sim.add_entity(1, Vector2(600.0, 0.0), 0.0, 600)
var cast := InputCommand.new()
cast.ability_slot = 0
cast.target_point = Vector2(100.0, 0.0) # any point along +x: a skillshot flies through it
sim.step({id: cast})
assert_eq(sim.state.get_entity(enemy).hp, 520, "Spirit Bolt deals its power at the landing point")
assert_eq(sim.state.get_entity(id).resource, 80, "and the cast spends its resource")
func test_skillshot_misses_when_aimed_away() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, 0, Vector2.ZERO)
var enemy := sim.add_entity(1, Vector2(600.0, 0.0), 0.0, 600)
var cast := InputCommand.new()
cast.ability_slot = 0
cast.target_point = Vector2(0.0, 100.0) # aimed up +y: lands at (0,600), nowhere near the enemy
sim.step({id: cast})
assert_eq(sim.state.get_entity(enemy).hp, 600, "a bolt aimed away does not hit")
func test_ground_area_strikes_every_enemy_in_its_radius() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _animal_hero(sim, 0, Vector2.ZERO)
# Pounce: GROUND, range 400, radius 150. Land it at (400,0).
var inside := sim.add_entity(1, Vector2(400.0, 100.0), 0.0, 600) # 100 from centre -> hit
var outside := sim.add_entity(1, Vector2(400.0, 300.0), 0.0, 600) # 300 from centre -> spared
var cast := InputCommand.new()
cast.ability_slot = 0 # animal Q = Pounce
cast.target_point = Vector2(400.0, 0.0)
sim.step({id: cast})
assert_eq(sim.state.get_entity(inside).hp, 540, "an enemy inside the area is struck")
assert_eq(sim.state.get_entity(outside).hp, 600, "an enemy outside the radius is spared")
func test_unit_ability_strikes_its_locked_target() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _animal_hero(sim, 0, Vector2.ZERO)
sim.state.get_entity(id).attack_damage = 0 # isolate Rend from the auto-attack (same range band)
var enemy := sim.add_entity(1, Vector2(150.0, 0.0), 0.0, 600) # inside Rend's 200 range
var cast := InputCommand.new()
cast.ability_slot = 2 # animal E = Rend
cast.target_id = enemy
sim.step({id: cast})
assert_eq(sim.state.get_entity(enemy).hp, 480, "Rend deals its power to the locked target")
func test_unit_ability_whiffs_on_an_out_of_range_target() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _animal_hero(sim, 0, Vector2.ZERO)
sim.state.get_entity(id).attack_damage = 0
var enemy := sim.add_entity(1, Vector2(300.0, 0.0), 0.0, 600) # beyond Rend's 200 range
var cast := InputCommand.new()
cast.ability_slot = 2
cast.target_id = enemy
sim.step({id: cast})
assert_eq(sim.state.get_entity(enemy).hp, 600, "an out-of-range unit cast lands no damage")
assert_eq(sim.state.get_entity(id).resource, 70, "but the whiffed cast still books its cost")
# --- Unit target acquisition (the driver's cursor pick) ---------------------
func test_pick_unit_target_returns_the_nearest_enemy() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var near := sim.add_entity(1, Vector2(100.0, 0.0), 0.0, 600)
sim.add_entity(1, Vector2(500.0, 0.0), 0.0, 600)
var picked := AbilityExecutor.pick_unit_target(sim.state, 0, Vector2(120.0, 0.0))
assert_eq(picked, near, "the enemy nearest the point is acquired")
func test_pick_unit_target_ignores_allies_and_empties() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
sim.add_entity(0, Vector2(100.0, 0.0), 0.0, 600) # an ally near the point
assert_eq(
AbilityExecutor.pick_unit_target(sim.state, 0, Vector2(100.0, 0.0)),
0,
"no enemy in the world acquires nothing, never an ally",
)
# --- Effects: heal, transform -----------------------------------------------
func test_self_heal_restores_hp_clamped_to_max() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, 0, Vector2.ZERO)
var h := sim.state.get_entity(id)
h.hp = 550 # Mend heals 100; from 550 the clamp caps the result at max_hp, not 650
var cast := InputCommand.new()
cast.ability_slot = 1 # human W = Mend
sim.step({id: cast})
assert_eq(sim.state.get_entity(id).hp, 600, "a self-heal never overfills past max_hp")
assert_eq(sim.state.get_entity(id).resource, 70, "Mend spends its cost")
func test_transform_swaps_form_and_keeps_cooldowns_running() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, 0, Vector2.ZERO)
# Put Spirit Bolt on cooldown, then transform.
var bolt := InputCommand.new()
bolt.ability_slot = 0
bolt.target_point = Vector2(100.0, 0.0)
sim.step({id: bolt})
var beast := InputCommand.new()
beast.ability_slot = 3 # human R = Beast Form
sim.step({id: beast})
var h := sim.state.get_entity(id)
assert_eq(h.form, AbilitySpec.FORM_ANIMAL, "Beast Form swaps the hero to its animal form")
assert_true(h.ability_cooldowns[1] > 0, "the human bolt cooldown keeps running across the swap")
assert_eq(h.resource_max, 100, "the animal pool is active after the swap")
# --- Gates: form, resource, cooldown ----------------------------------------
func test_can_cast_gates_on_form_resource_and_cooldown() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, 0, Vector2.ZERO)
var h := sim.state.get_entity(id)
var bolt := AbilityData.spec(1) # human Spirit Bolt, cost 20
var pounce := AbilityData.spec(4) # animal Pounce
assert_true(AbilityExecutor.can_cast(h, bolt), "a human, full, ready hero can cast in form")
assert_false(AbilityExecutor.can_cast(h, pounce), "an animal ability is not castable while human")
h.resource = 10
assert_false(AbilityExecutor.can_cast(h, bolt), "a cast is refused without enough resource")
h.resource = 100
h.ability_cooldowns[1] = 5
assert_false(AbilityExecutor.can_cast(h, bolt), "a cast is refused while on cooldown")
func test_cooldown_blocks_recast_until_it_elapses() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, 0, Vector2.ZERO)
var cast := InputCommand.new()
cast.ability_slot = 0
cast.target_point = Vector2(100.0, 0.0)
sim.step({id: cast}) # cast 1: Spirit Bolt -> cooldown 30
var h := sim.state.get_entity(id)
assert_eq(h.ability_cooldowns[1], 30, "the ability enters its full cooldown when cast")
sim.step({id: cast}) # a recast one tick later is refused
assert_eq(h.ability_cooldowns[1], 29, "the cooldown ticks down and the recast is dropped")
for _i in 29:
sim.step({})
assert_eq(h.ability_cooldowns[1], 0, "the cooldown reaches zero")
sim.step({id: cast}) # now the recast lands
assert_eq(h.ability_cooldowns[1], 30, "off cooldown, the recast lands and re-enters cooldown")
func test_resource_regenerates_on_its_interval() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, 0, Vector2.ZERO)
var cast := InputCommand.new()
cast.ability_slot = 0
cast.target_point = Vector2(100.0, 0.0)
sim.step({id: cast}) # spend 20 -> 80
var h := sim.state.get_entity(id)
assert_eq(h.resource, 80, "the cast leaves the pool down by its cost")
for _i in 12: # regen is one point every 12 ticks
sim.step({})
assert_eq(h.resource, 81, "one point regenerates after the interval")
for _i in 12:
sim.step({})
assert_eq(h.resource, 82, "and another after the next interval")
# --- Inert without a kit + determinism --------------------------------------
func test_an_unequipped_hero_ignores_ability_intent() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := sim.add_hero(0, Vector2.ZERO, 320.0) # no equip_kit
var enemy := sim.add_entity(1, Vector2(600.0, 0.0), 0.0, 600)
var cast := InputCommand.new()
cast.ability_slot = 0
cast.target_point = Vector2(100.0, 0.0)
sim.step({id: cast}) # must not cast or crash
assert_true(sim.state.get_entity(id).kit.is_empty(), "a bare hero carries no kit to cast from")
assert_eq(sim.state.get_entity(enemy).hp, 600, "and its ability intent does nothing")
func test_an_ability_run_replays_identically() -> void:
var a := _run_abilities()
var b := _run_abilities()
assert_eq(a, b, "the ability layer must be a pure function of state + input")
## A scripted ability sequence over a fixed window: a hero casts a bolt, transforms,
## pounces, and idles, against two enemies. Returns a deterministic digest of the
## survivors so two runs can be compared field-for-field.
func _run_abilities() -> Array:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, 0, Vector2.ZERO)
sim.add_entity(1, Vector2(600.0, 0.0), 0.0, 600)
sim.add_entity(1, Vector2(400.0, 80.0), 0.0, 600)
var script: Array[InputCommand] = []
for i in 90:
var cmd := InputCommand.new()
if i == 0:
cmd.ability_slot = 0 # Spirit Bolt
cmd.target_point = Vector2(100.0, 0.0)
elif i == 5:
cmd.ability_slot = 3 # Beast Form
elif i == 10:
cmd.ability_slot = 0 # Pounce (animal)
cmd.target_point = Vector2(400.0, 0.0)
else:
cmd.ability_slot = -1
script.append(cmd)
for cmd in script:
sim.step({id: cmd})
return _digest(sim.state)
## A stable, comparable digest of the world: every surviving entity's id, hp, and
## rounded position, ordered by id.
func _digest(state: SimState) -> Array:
var ids := state.entities.keys()
ids.sort()
var rows: Array = []
for id in ids:
var e: SimEntity = state.entities[id]
rows.append([id, e.hp, e.position.round()])
return rows