Files
Bullosseum/tests/bear_boss_test.gd
T

222 lines
7.9 KiB
GDScript

extends SceneTree
## Bear boss behaviour test. Self-contained: a floor, a real bull (Player.tscn), and the
## bear — no level/shell scene needed. Parks the bull at different range bands and checks
## the bear commits to band-appropriate routines through wind-up→active→recover, that the
## leap lands on the ground (not mid-air), hyper-armour holds mid-swing, and phase 2 +
## death fire. Run: godot --headless --script res://tests/bear_boss_test.gd
var _pass := 0
var _fail := 0
var _bear: Node3D = null
var _bull: Node3D = null
func _init() -> void:
_run.call_deferred()
func _ok(c: bool, m: String) -> void:
if c:
_pass += 1
print(" PASS: ", m)
else:
_fail += 1
print(" FAIL: ", m)
func _base(s: String) -> String:
return s.split(":")[0].trim_suffix("*")
func _dt() -> float:
return 1.0 / Engine.get_physics_ticks_per_second()
# Keep the bull pinned at a flat distance from the bear and alive; report the routine
# bases the bear visits over `secs`.
func _observe(dist: float, secs: float) -> Dictionary:
var seen := {}
var t := 0.0
while t < secs and is_instance_valid(_bear):
_pin_bull(dist)
await physics_frame
t += _dt()
seen[_base(_bear.call("ai_state_name"))] = true
return seen
func _pin_bull(dist: float) -> void:
if _bull == null or not is_instance_valid(_bull):
return
if dist > 0.0:
_bull.global_position = _bear.global_position + Vector3(dist, 0, 0)
_bull.set("_hp", 100000) # never let the bull die and trip a game-over during the test
func _make_floor() -> StaticBody3D:
var body := StaticBody3D.new()
var cs := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(400, 1, 400) # oversized so the bear can't chase the bull off an edge
cs.shape = box
cs.position = Vector3(0, -0.5, 0) # top face at y=0
body.add_child(cs)
return body
func _run() -> void:
root.add_child(_make_floor())
_bull = (load("res://Player.tscn") as PackedScene).instantiate()
root.add_child(_bull)
_bull.global_position = Vector3(4, 1, 0)
_bear = (load("res://Bear.tscn") as PackedScene).instantiate()
root.add_child(_bear)
_bear.global_position = Vector3(0, 1, 0)
await physics_frame
await physics_frame
_ok(_bull.is_in_group(&"player"), "bull is in the player group")
print(" anims: idle=%s run=%s walk2=%s stand=%s swipe=%s smash=%s leap=%s" % [
_bear.get("_anim_idle"), _bear.get("_anim_run"), _bear.get("_anim_walk2"),
_bear.get("_anim_stand"), _bear.get("_anim_swipe"), _bear.get("_anim_smash"),
_bear.get("_anim_leap")])
for k in ["_anim_idle", "_anim_run", "_anim_walk2", "_anim_stand", "_anim_swipe", "_anim_smash", "_anim_leap"]:
_ok(String(_bear.get(k)) != "", "resolved %s" % k)
# Close band → melee (swipe / smash).
var close_seen := await _observe(2.5, 6.0)
print(" close-band states: ", close_seen.keys())
_ok(close_seen.has("SWIPE") or close_seen.has("SMASH"), "close range → melee routine")
# Charge was removed — the bear must never enter it.
_ok(not close_seen.has("CHARGE"), "no charge routine (removed)")
# It must not climb on top of the bull: pressed right up against a pinned bull while
# doing melee (never leaping), the grounded bear's height must stay near the floor.
var max_y := -INF
var ct := 0.0
while ct < 4.0 and is_instance_valid(_bear):
_pin_bull(1.5)
await physics_frame
ct += _dt()
if not String(_bear.call("ai_state_name")).begins_with("LEAP"):
max_y = maxf(max_y, _bear.global_position.y)
_ok(max_y < 0.6, "bear does not climb on top of the bull (max grounded y=%.2f)" % max_y)
# Claw slashes: an emitter is bound to each paw, and one fires during the 1-2 punch.
_ok(_bear.get("_claw_l") != null and _bear.get("_claw_r") != null, "claw emitters bound to both paws")
var clawed := false
var wt := 0.0
while wt < 8.0 and is_instance_valid(_bear) and not clawed:
_pin_bull(2.5)
await physics_frame
wt += _dt()
var cl: CPUParticles3D = _bear.get("_claw_l")
var cr: CPUParticles3D = _bear.get("_claw_r")
if (cl != null and cl.emitting) or (cr != null and cr.emitting):
clawed = true
_ok(clawed, "claw slash emits during the 1-2 punch")
# Mid band → walk-in (stalk) or leap.
var mid_seen := await _observe(7.0, 8.0)
print(" mid-band states: ", mid_seen.keys())
_ok(mid_seen.has("STALK") or mid_seen.has("LEAP"), "mid range → stalk/leap")
# Routines reach ACTIVE (hit) and RECOVER (punish) phases.
var saw_active := false
var saw_recover := false
var t := 0.0
while t < 8.0 and is_instance_valid(_bear):
_pin_bull(2.5)
await physics_frame
t += _dt()
var s: String = _bear.call("ai_state_name")
if s.contains(":ACTIVE"): saw_active = true
if s.contains(":RECOVER"): saw_recover = true
_ok(saw_active, "routines reach their ACTIVE (hit) phase")
_ok(saw_recover, "routines reach their RECOVER (punish) phase")
# Leap must actually get airborne and then LAND before it recovers — the slam lands on
# the ground (impact frame), not mid-air. Reset the bear to a known grounded spot, then
# force one leap deterministically and follow it.
_bear.call("_enter_neutral")
_bear.global_position = Vector3(0, 1, 0)
_bear.velocity = Vector3.ZERO
var settle := 0
while settle < 30 and not _bear.is_on_floor():
_pin_bull(7.0)
await physics_frame
settle += 1
var start_y: float = _bear.global_position.y
_bear.call("_start_leap")
var peak_y := start_y
var recovered_on_floor := false
var recovered_airborne := false
var reached_recover := false
t = 0.0
while t < 4.0 and is_instance_valid(_bear):
_pin_bull(7.0)
await physics_frame
t += _dt()
peak_y = maxf(peak_y, _bear.global_position.y)
var s: String = _bear.call("ai_state_name")
if s.begins_with("LEAP:RECOVER"):
reached_recover = true
if _bear.is_on_floor(): recovered_on_floor = true
else: recovered_airborne = true
break
_ok(peak_y > start_y + 0.3, "leap gets airborne (peak +%.2fm)" % (peak_y - start_y))
_ok(reached_recover, "leap reaches its slam/recovery")
_ok(recovered_on_floor and not recovered_airborne,
"leap lands on the ground before recovering (frame-25 contact, no mid-air finish)")
# Hyper-armour: let the bear commit to a swing, then hit it — it must NOT drop into
# STAGGER mid-swing (only its recovery is punishable), yet it must still take the wound.
var in_active := false
t = 0.0
while t < 8.0 and is_instance_valid(_bear) and not in_active:
_pin_bull(2.5)
await physics_frame
t += _dt()
in_active = String(_bear.call("ai_state_name")).contains(":ACTIVE")
_ok(in_active, "bear commits to an ACTIVE swing")
var hp_before := int(_bear.get("_hp"))
var staggered_mid_swing := false
var i := 0
while i < 8 and is_instance_valid(_bear) and String(_bear.call("ai_state_name")).contains(":ACTIVE"):
_pin_bull(2.5)
_bear.call("apply_ability_hit", Vector3.RIGHT, 10.0, 0.0)
await physics_frame
if String(_bear.call("ai_state_name")).begins_with("STAGGER"):
staggered_mid_swing = true
i += 1
_ok(not staggered_mid_swing, "hyper-armour: hits during a swing don't stagger it")
_ok(int(_bear.get("_hp")) < hp_before, "bear still takes damage during the swing")
# Drive HP down (spaced out so it isn't stun-locked) and confirm phase 2 engages.
t = 0.0
while t < 8.0 and is_instance_valid(_bear) and not bool(_bear.get("_enraged")):
_pin_bull(2.5)
_bear.call("apply_ability_hit", Vector3.RIGHT, 10.0, 0.0)
for _f in range(18): # ~0.3s gap between hits
_pin_bull(2.5)
await physics_frame
t += _dt()
_ok(bool(_bear.get("_enraged")), "phase 2 (enrage) triggered below the HP fraction")
# Finish it off and confirm death + free.
for _k in range(int(_bear.get("_hp")) + 2):
if is_instance_valid(_bear):
_bear.call("apply_ability_hit", Vector3.RIGHT, 12.0, 0.0)
for _f in range(3):
await physics_frame
await physics_frame
_ok(is_instance_valid(_bear) and not _bear.call("is_active"), "bear dead after HP drained")
await create_timer(6.0).timeout
_ok(not is_instance_valid(_bear), "bear freed after death topple")
print("Results: %d passed, %d failed" % [_pass, _fail])
quit(1 if _fail > 0 else 0)