5c1e881a53
Bull charge damage: - Tunnel-proof swept detection (prev→now segment) for matador gore and bear ram, so a 66 m/s dash no longer skips clean over the thin HitArea. - Horns-first cone (bull_gore_arc): running into an enemy only wounds it when charging roughly at it, not a sideways brush. - Dash is a committed lunge: bypasses the horns cone AND the survival roll, so a dash connect is a guaranteed kill. Cruise charges keep a speed-scaled roll (mat_charge_pierce). Roll ability defers to its own pop (is_rolling guard). Bear: - Horns-first swept ram detection (edge-triggered: one charge = one wound); a tunnelling or point-blank charge now lands where body_entered wouldn't. - Overhead smash lane extends faster (0.1s) and further (20 m). - Leap slam launches the bull from anywhere in the circle (ring_frac 0). - Removed stray-quote syntax errors that broke the script. Matador: - Collider-driven stab (BladeHitbox) with recovery beat + taunt-after-hit so it stops machine-gunning the bull. - A taunting matador answers a charge: spear from range, sword up close. Spear projectile masks the BULL layer so throws connect. Tests: matador_charge/stab/spear/taunt_react, bear_ram/hitbox/fx (+ unified _fake_bull stand-in). Trimmed verbose comments. Note: bear claw-emitter/death work is still WIP (3 bear_boss_test failures). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
4.8 KiB
GDScript
122 lines
4.8 KiB
GDScript
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)
|