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)