GDScript 259 lines
extends GutTest
## Behaviour checks on the bot's ability casting. The bot walks toward the nearest
## enemy (the walking-skeleton behaviour) and, once it is a kitted hero, layers a
## cast onto that intent: it shifts form to keep a hit (and its heal) in reach, then
## self-heals when hurt, otherwise fires the first damaging ability of its active
## form that can actually reach the target. These pin the selection order, the
## per-targeting-mode reach gate, the form-shift policy, plus one end-to-end check
## that a chosen cast lands in the sim. Headless and deterministic, creep waves off.
const WILDKIN_SPIRIT_BOLT_SLOT := 0 # human SKILLSHOT, range 600 / radius 60
const WILDKIN_MEND_SLOT := 1 # human HEAL
const TRANSFORM_SLOT := 3 # the R slot holds the form swap in every kit
const LION_HEAL_ID := 11 # Mane Guard, the Lion's human-form heal
func _bot() -> BotController:
return BotController.new()
func _hero(sim: SimCore, kit_id: String, pos: Vector2) -> int:
var id := sim.add_hero(0, pos, 300.0)
sim.equip_kit(id, kit_id)
return id
## Steps the hero into its animal form by casting the form-swap slot, so a test can
## start from the animal kit. No enemy need be present — a transform is self-cast.
func _to_animal(sim: SimCore, hero_id: int) -> void:
var shift := InputCommand.new()
shift.ability_slot = TRANSFORM_SLOT
sim.step({hero_id: shift})
# --- The is-a-kitted-hero gate ----------------------------------------------
func test_a_bot_without_a_kit_only_moves() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var mover := sim.add_hero(0, Vector2.ZERO, 300.0) # never equipped -> not a caster
sim.add_entity(1, Vector2(400.0, 0.0), 0.0, 600)
var command := _bot().decide(sim.state, mover)
assert_eq(command.ability_slot, -1, "a kit-less hero never casts")
assert_ne(command.move_dir, Vector2.ZERO, "but it still advances on the enemy")
# --- Selection: which slot, by reach and effect -----------------------------
func test_bot_fires_a_skillshot_at_an_enemy_in_its_band() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "wildkin", Vector2.ZERO)
sim.add_entity(1, Vector2(600.0, 0.0), 0.0, 600) # at the skillshot's exact range
var command := _bot().decide(sim.state, id)
assert_eq(command.ability_slot, WILDKIN_SPIRIT_BOLT_SLOT, "it casts the reachable skillshot")
assert_eq(command.target_point, Vector2(600.0, 0.0), "aimed straight at the enemy")
func test_bot_holds_fire_when_no_ability_can_reach() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "wildkin", Vector2.ZERO)
# Far outside the skillshot's [range-radius, range+radius] band: a cast would
# strike empty air, so the bot must not spend it.
sim.add_entity(1, Vector2(1200.0, 0.0), 0.0, 600)
var command := _bot().decide(sim.state, id)
assert_eq(command.ability_slot, -1, "a full-health bot out of reach casts nothing")
assert_ne(command.move_dir, Vector2.ZERO, "it closes the distance instead")
func test_bot_picks_a_ground_ability_that_can_reach() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "hyena", Vector2.ZERO) # human slot 0 = Bone-Hex, GROUND
sim.add_entity(1, Vector2(400.0, 0.0), 0.0, 600) # inside range + radius
var command := _bot().decide(sim.state, id)
assert_eq(command.ability_slot, 0, "the ground area is cast on a reachable enemy")
assert_eq(command.target_point, Vector2(400.0, 0.0), "dropped on the target")
func test_a_unit_ability_locks_the_target() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "snake", Vector2.ZERO) # a brawler, so it commits to its animal kit
# Shift to the animal kit, whose slot 0 (Fang Strike) is unit-targeted.
var beast := InputCommand.new()
beast.ability_slot = 3
sim.step({id: beast})
var enemy := sim.add_entity(1, Vector2(200.0, 0.0), 0.0, 600) # inside Fang Strike's 360
var command := _bot().decide(sim.state, id)
assert_eq(command.ability_slot, 0, "the unit ability is selected")
assert_eq(command.target_id, enemy, "and locked onto the nearest enemy")
func test_a_hurt_bot_heals_before_it_attacks() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "wildkin", Vector2.ZERO)
sim.state.get_entity(id).hp = 100 # well under the 60% heal threshold of 600
sim.add_entity(1, Vector2(600.0, 0.0), 0.0, 600) # a damage target is also in reach
var command := _bot().decide(sim.state, id)
assert_eq(command.ability_slot, WILDKIN_MEND_SLOT, "survival comes first: it heals, not pokes")
# --- Stance: shifting form to keep a hit (and a heal) in reach ---------------
func test_bot_transforms_to_engage_when_only_the_other_form_reaches() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "lion", Vector2.ZERO) # human slot 0 = Sunfire Lash, a skillshot
# Inside the skillshot's dead zone (its band is around the 450 range) but well
# within the animal kit's reach: the human poke would whiff, so the bot shifts.
sim.add_entity(1, Vector2(150.0, 0.0), 0.0, 600)
var command := _bot().decide(sim.state, id)
assert_eq(command.ability_slot, TRANSFORM_SLOT, "it transforms instead of whiffing the poke")
assert_eq(
sim.state.get_entity(id).form, AbilitySpec.FORM_HUMAN, "the shift is queued, not yet applied"
)
func test_bot_does_not_transform_while_its_current_form_can_hit() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "lion", Vector2.ZERO)
# At the skillshot's exact range the human poke lands; the animal kit reaches
# too, but a form that can already hit does not give up its turn to shift.
sim.add_entity(1, Vector2(450.0, 0.0), 0.0, 600)
var command := _bot().decide(sim.state, id)
assert_eq(command.ability_slot, 0, "it pokes with the reachable human ability, no transform")
func test_bot_transforms_back_when_the_enemy_outruns_the_animal_kit() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "cheetah", Vector2.ZERO)
_to_animal(sim, id) # the animal kit reaches only to 280 (Hamstring / Killing Blow)
# A kiter holds its ranged form: caught in the animal kit, it shifts back toward the
# longer human Spear Throw rather than fighting from melee.
sim.add_entity(1, Vector2(750.0, 0.0), 0.0, 600)
var command := _bot().decide(sim.state, id)
assert_eq(command.ability_slot, TRANSFORM_SLOT, "it shifts back toward the human poke")
assert_eq(
sim.state.get_entity(id).form, AbilitySpec.FORM_ANIMAL, "still animal until the cast resolves"
)
func test_a_hurt_bot_in_animal_form_retreats_to_the_human_heal() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "lion", Vector2.ZERO)
_to_animal(sim, id)
sim.state.get_entity(id).hp = 100 # under the 60% heal threshold of 600
sim.add_entity(1, Vector2(100.0, 0.0), 0.0, 600) # within the animal kit's reach
var command := _bot().decide(sim.state, id)
assert_eq(command.ability_slot, TRANSFORM_SLOT, "it abandons the brawl to reach its heal")
func test_a_hurt_bot_stays_in_animal_form_when_the_heal_is_on_cooldown() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "lion", Vector2.ZERO)
_to_animal(sim, id)
var bot := sim.state.get_entity(id)
bot.hp = 100
bot.ability_cooldowns[LION_HEAL_ID] = 50 # Mane Guard not ready: no point flipping for it
sim.add_entity(1, Vector2(100.0, 0.0), 0.0, 600)
var command := _bot().decide(sim.state, id)
assert_eq(command.ability_slot, 0, "with no heal to reach it fights on with the animal kit")
# --- End to end: the chosen cast lands --------------------------------------
func test_a_bot_cast_lands_in_the_sim() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "wildkin", Vector2.ZERO)
sim.state.get_entity(id).move_speed = 0.0 # hold position so the cast geometry is exact
# At the skillshot's range (600), and beyond the 250 auto-attack range, so only the cast lands.
var enemy := sim.add_entity(1, Vector2(600.0, 0.0), 0.0, 600)
sim.step({id: _bot().decide(sim.state, id)})
assert_eq(sim.state.get_entity(enemy).hp, 520, "Spirit Bolt's 80 lands on the enemy")
assert_eq(sim.state.get_entity(id).resource, 80, "and its 20 cost is booked")
# --- Kite stance: a skirmisher holds its poke range -------------------------
func test_equip_stamps_the_kit_stance() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var cheetah := _hero(sim, "cheetah", Vector2.ZERO)
var lion := _hero(sim, "lion", Vector2(50.0, 0.0))
assert_eq(sim.state.get_entity(cheetah).stance, AbilityData.STANCE_KITE, "the cheetah kites")
assert_eq(sim.state.get_entity(lion).stance, AbilityData.STANCE_BRAWL, "the lion brawls")
# Equip also stamps the kit id — the hero's identity the renderer tints by.
assert_eq(sim.state.get_entity(cheetah).kit_id, "cheetah", "the cheetah carries its kit id")
assert_eq(sim.state.get_entity(lion).kit_id, "lion", "the lion carries its kit id")
func test_a_kiter_backs_off_a_point_blank_enemy_instead_of_meleeing() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "cheetah", Vector2.ZERO)
sim.add_entity(1, Vector2(150.0, 0.0), 0.0, 600) # inside the Spear's dead zone, point-blank
var command := _bot().decide(sim.state, id)
assert_lt(command.move_dir.x, 0.0, "the kiter retreats from the enemy, not into it")
assert_eq(command.ability_slot, -1, "and does not drop to a melee form to engage")
func test_a_kiter_holds_and_pokes_from_its_band() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "cheetah", Vector2.ZERO)
sim.add_entity(1, Vector2(750.0, 0.0), 0.0, 600) # at the Spear Throw's landing range
var command := _bot().decide(sim.state, id)
assert_eq(command.move_dir, Vector2.ZERO, "inside its poke band the kiter holds position")
assert_eq(command.ability_slot, 0, "and fires its long skillshot")
func test_a_kiter_closes_on_an_enemy_beyond_its_poke() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "cheetah", Vector2.ZERO)
sim.add_entity(1, Vector2(1000.0, 0.0), 0.0, 600) # past the band: it must close to poke
var command := _bot().decide(sim.state, id)
assert_gt(command.move_dir.x, 0.0, "beyond its band the kiter advances to bring the poke in")
func test_a_brawler_closes_a_point_blank_enemy() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var id := _hero(sim, "snake", Vector2.ZERO) # a brawler: the contrast to the kiter
sim.add_entity(1, Vector2(150.0, 0.0), 0.0, 600)
var command := _bot().decide(sim.state, id)
assert_gt(command.move_dir.x, 0.0, "a brawler closes the gap rather than kiting away")
func test_a_brawler_routes_around_a_blocking_obstacle() -> void:
var sim := SimCore.new()
sim.spawn_creeps = false
var center := MapData.nexus_for_team(0)
var bot := _hero(sim, "snake", center + Vector2(700.0, 0.0))
var enemy_pos := center - Vector2(700.0, 0.0)
sim.add_entity(1, enemy_pos, 0.0, 600) # an enemy on the far side of the obstacle
var bot_pos := sim.state.get_entity(bot).position
var nav := NavGrid.shared()
assert_false(nav.segment_clear(bot_pos, enemy_pos), "the straight line runs through the obstacle")
var command := _bot().decide(sim.state, bot)
var straight := (enemy_pos - bot_pos).normalized()
assert_gt(command.move_dir.length(), 0.0, "the bot advances on the enemy")
assert_gt(command.move_dir.distance_to(straight), 0.01, "it steers around, not straight in")
assert_true(
nav.segment_clear(bot_pos, bot_pos + command.move_dir * 300.0),
"the step it takes is onto clear ground",
)