222 lines
7.9 KiB
GDScript
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)
|