extends SceneTree ## Bear attack-collider test. The colliders + their motion live in BearAttackHitboxes.tscn: the ## leap slam is a ground circle (cylinder) whose "leap_slam" clip pops it open; the overhead smash ## is a forward box lane whose "smash_travel" clip grows it out from the bear. bear.gd fires the ## clip, arms the knock-up shaping, and sweeps while it runs. This asserts: a bull anywhere in the ## leap circle is mauled AND launched by default (bear_leap_ring_frac 0), while raising ring_frac ## carves a grounded inner zone; the smash lane catches bulls near AND far as the front travels, ## throws the further one higher, and spares anything off the lane. ## Run: godot --headless --script res://tests/bear_hitbox_test.gd var _pass := 0 var _fail := 0 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 _make_bull(pos: Vector3) -> CharacterBody3D: var b := CharacterBody3D.new() b.collision_layer = 2 # PhysicsLayers.BULL — what the hitboxes mask b.collision_mask = 0 b.add_to_group(&"player") b.set_script(load("res://tests/_fake_bull.gd")) var cs := CollisionShape3D.new() var shape := SphereShape3D.new() shape.radius = 0.6 cs.shape = shape b.add_child(cs) root.add_child(b) b.global_position = pos return b # Drive one attack collider clip to completion, sweeping each physics frame like _physics_process. func _drive(bear: Node, fire: String) -> void: bear.call(fire) for i in 60: bear.call("_tick_attack_hitbox") await physics_frame if bear.get("_active_hitbox") == null: break func _run() -> void: var bear := (load("res://Bear.tscn") as PackedScene).instantiate() as Node3D root.add_child(bear) bear.set_physics_process(false) # freeze the AI; we fire the colliders ourselves await physics_frame var leap_hb := bear.find_child("LeapSlamHitbox", true, false) as Area3D var smash_hb := bear.find_child("SmashHitbox", true, false) as Area3D _ok(leap_hb != null, "leap slam cylinder hitbox exists (from BearAttackHitboxes.tscn)") _ok(smash_hb != null, "overhead smash lane hitbox exists (from BearAttackHitboxes.tscn)") if leap_hb == null or smash_hb == null: _finish() return var dp := root.get_node("/root/DP") var slam_r: float = (leap_hb.get_node("CollisionShape3D").shape as CylinderShape3D).radius var lane: float = (smash_hb.get_node("CollisionShape3D").shape as BoxShape3D).size.z # ── Leap slam: by default (bear_leap_ring_frac 0) the bull is launched from ANYWHERE in the # circle — no safe inner spot. Off the circle is still spared. dp.call("set_value", "bear_leap_ring_frac", 0.0) var center := _make_bull(Vector3(0.0, 0.5, 0.0)) var midring := _make_bull(Vector3(slam_r * 0.5, 0.5, 0.0)) var outside := _make_bull(Vector3(slam_r + 3.0, 0.5, 0.0)) await physics_frame await _drive(bear, "_fire_leap_hitbox") _ok(center.hits >= 1 and center.knocked_up > 0.0, "bull at the circle center is mauled AND launched") _ok(midring.hits >= 1 and midring.knocked_up > 0.0, "bull mid-circle is mauled AND launched") _ok(outside.hits == 0, "bull outside the slam radius is spared") center.queue_free() midring.queue_free() outside.queue_free() await physics_frame # Raising ring_frac restores a grounded inner zone that only mauls (the slider still works). dp.call("set_value", "bear_leap_ring_frac", 0.75) var frac: float = dp.f("bear_leap_ring_frac") var inner := _make_bull(Vector3(slam_r * (frac * 0.5), 0.5, 0.0)) var ring := _make_bull(Vector3(slam_r * (frac + (1.0 - frac) * 0.5), 0.5, 0.0)) await physics_frame await _drive(bear, "_fire_leap_hitbox") _ok(inner.hits >= 1 and inner.knocked_up == 0.0, "raised ring_frac keeps the inner circle grounded") _ok(ring.knocked_up > 0.0, "raised ring_frac still launches the outer ring") inner.queue_free() ring.queue_free() dp.call("set_value", "bear_leap_ring_frac", 0.0) await physics_frame # ── Overhead smash: the lane's leading edge travels the whole way, so bulls near AND far are # both caught as the front passes; the further one is thrown higher. Off-lane is spared. var near := _make_bull(Vector3(0.0, 0.5, lane * 0.2)) var far := _make_bull(Vector3(0.0, 0.5, lane * 0.8)) var beside := _make_bull(Vector3(lane * 0.5, 0.5, lane * 0.5)) var behind := _make_bull(Vector3(0.0, 0.5, -3.0)) await physics_frame await _drive(bear, "_begin_smash_hitbox") _ok(near.hits >= 1, "bull near the bear is caught as the shockwave starts") _ok(far.hits >= 1, "bull far down the lane is caught when the front reaches it") _ok(far.knocked_up > near.knocked_up, "the further hit is thrown higher") _ok(beside.hits == 0, "bull beside the lane is spared (a lane, not a circle)") _ok(behind.hits == 0, "bull behind the bear is spared") _finish() func _finish() -> void: print("Results: %d passed, %d failed" % [_pass, _fail]) quit(1 if _fail > 0 else 0)