Route the bull's hits through the typed DamageEvent pipeline
Hitbox now builds a DamageEvent (cause + hit position + shaped knock-up) and calls the bull's new receive_damage(), instead of two stringly Object.call()s. receive_damage adapts over the existing pip/knock-up logic so behaviour is identical -- the characterization test drives both the string and typed paths and asserts the same outcomes. Wire fx/damage/harness tests into run_tests.sh + CLAUDE.md. All hitbox guard tests green (stab, bear hitbox, ram, charge). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CmG3mQGVXZUPDVapmdbWZ1
This commit is contained in:
@@ -108,6 +108,9 @@ bash run_tests.sh
|
|||||||
# Individual tests:
|
# Individual tests:
|
||||||
gdlint *.gd levels/*.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv)
|
gdlint *.gd levels/*.gd tests/*.gd # GDScript lint (gdtoolkit, installed via uv)
|
||||||
godot --headless --script tests/logic_test.gd # pure logic, ~2 s
|
godot --headless --script tests/logic_test.gd # pure logic, ~2 s
|
||||||
|
godot --headless --script tests/harness_selftest.gd # shared test_helpers: seeded RNG + approx + tally, ~1 s
|
||||||
|
godot --headless --script tests/damage_pipeline_test.gd # bull's hit reception (take_sword_hit + typed receive_damage/DamageEvent), ~3 s
|
||||||
|
godot --headless --script tests/fx_gallery_test.gd # every registered Fx effect spawns + self-frees, ~2 s
|
||||||
godot --headless --script tests/performance_test.gd # frame-budget regression guard, ~4 s
|
godot --headless --script tests/performance_test.gd # frame-budget regression guard, ~4 s
|
||||||
godot --headless --script tests/ragdoll_perf_test.gd # on-hit spike breakdown: ragdoll build / sim-start / blood / mass hit, ~90 s
|
godot --headless --script tests/ragdoll_perf_test.gd # on-hit spike breakdown: ragdoll build / sim-start / blood / mass hit, ~90 s
|
||||||
godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s
|
godot --headless --script tests/gameplay_test.gd # full scene, bone sanity, ~4 s
|
||||||
|
|||||||
@@ -41,3 +41,22 @@ static func make(kind: Cause, at: Vector3, src: Node = null) -> DamageEvent:
|
|||||||
## ("gored" / "mauled" / "thrown") during the migration off string causes.
|
## ("gored" / "mauled" / "thrown") during the migration off string causes.
|
||||||
func cause_name() -> String:
|
func cause_name() -> String:
|
||||||
return String(Cause.keys()[cause]).to_lower()
|
return String(Cause.keys()[cause]).to_lower()
|
||||||
|
|
||||||
|
|
||||||
|
## Maps a legacy cause string onto the enum, so a Hitbox's exported damage_cause round-trips
|
||||||
|
## exactly (gored/mauled/thrown are the only ones in use). Unknown strings fall back to GENERIC.
|
||||||
|
static func cause_from(text: String) -> Cause:
|
||||||
|
match text.to_lower():
|
||||||
|
"gored":
|
||||||
|
return Cause.GORED
|
||||||
|
"mauled":
|
||||||
|
return Cause.MAULED
|
||||||
|
"thrown":
|
||||||
|
return Cause.THROWN
|
||||||
|
"slammed":
|
||||||
|
return Cause.SLAMMED
|
||||||
|
"kicked":
|
||||||
|
return Cause.KICKED
|
||||||
|
"dashed":
|
||||||
|
return Cause.DASHED
|
||||||
|
return Cause.GENERIC
|
||||||
|
|||||||
@@ -80,11 +80,17 @@ func _apply_hit(body: Node3D) -> void:
|
|||||||
return
|
return
|
||||||
_hit_this_swing[body] = true
|
_hit_this_swing[body] = true
|
||||||
_did_hit = true
|
_did_hit = true
|
||||||
body.call(&"take_sword_hit", damage_cause)
|
if body.has_method(&"receive_damage"):
|
||||||
if knock_up > 0.0:
|
var event := DamageEvent.make(DamageEvent.cause_from(damage_cause), body.global_position, self)
|
||||||
var v := _knock_up_for(body)
|
if knock_up > 0.0:
|
||||||
if v > 0.0:
|
event.knock_up = _knock_up_for(body)
|
||||||
body.call(&"apply_knock_up", v)
|
body.call(&"receive_damage", event)
|
||||||
|
else:
|
||||||
|
body.call(&"take_sword_hit", damage_cause)
|
||||||
|
if knock_up > 0.0:
|
||||||
|
var v := _knock_up_for(body)
|
||||||
|
if v > 0.0:
|
||||||
|
body.call(&"apply_knock_up", v)
|
||||||
|
|
||||||
|
|
||||||
# Scale the pop by how far the body is from this hitbox's origin (planar). See the export docs.
|
# Scale the pop by how far the body is from this hitbox's origin (planar). See the export docs.
|
||||||
|
|||||||
@@ -1222,6 +1222,15 @@ func apply_knock_up(v: float) -> void:
|
|||||||
velocity.y = maxf(velocity.y, v)
|
velocity.y = maxf(velocity.y, v)
|
||||||
|
|
||||||
|
|
||||||
|
# Typed entry point for the unified hit pipeline: a Hitbox (or any attacker) hands the bull a
|
||||||
|
# DamageEvent instead of calling take_sword_hit directly. A thin adapter over the existing pip /
|
||||||
|
# knock-up logic, so behaviour is unchanged while callers migrate off the stringly path.
|
||||||
|
func receive_damage(event: DamageEvent) -> void:
|
||||||
|
take_sword_hit(event.cause_name(), event.hit_position)
|
||||||
|
if event.knock_up > 0.0:
|
||||||
|
apply_knock_up(event.knock_up)
|
||||||
|
|
||||||
|
|
||||||
# Blood spurt from a spear wound: a short-lived spray of dark-red gobs flung out of the
|
# Blood spurt from a spear wound: a short-lived spray of dark-red gobs flung out of the
|
||||||
# entry point, biased outward from the bull's centre (so it reads as bursting from the
|
# entry point, biased outward from the bull's centre (so it reads as bursting from the
|
||||||
# hide) with a touch of lift before gravity drags it down.
|
# hide) with a touch of lift before gravity drags it down.
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ echo ""
|
|||||||
echo "=== Logic tests ==="
|
echo "=== Logic tests ==="
|
||||||
"$GODOT" --headless --script tests/logic_test.gd
|
"$GODOT" --headless --script tests/logic_test.gd
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Damage pipeline (bull's hit reception through DamageEvent) ==="
|
||||||
|
"$GODOT" --headless --script tests/damage_pipeline_test.gd
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== FX gallery smoke (every registered Fx effect spawns + frees) ==="
|
||||||
|
"$GODOT" --headless --script tests/fx_gallery_test.gd
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Console focus tests ==="
|
echo "=== Console focus tests ==="
|
||||||
"$GODOT" --headless --script tests/console_focus_test.gd
|
"$GODOT" --headless --script tests/console_focus_test.gd
|
||||||
|
|||||||
@@ -87,6 +87,24 @@ func _run() -> void:
|
|||||||
bull.call(&"apply_knock_up", 8.0)
|
bull.call(&"apply_knock_up", 8.0)
|
||||||
_ok(is_equal_approx(float((bull.get(&"velocity") as Vector3).y), 8.0), "a larger knock_up raises it further")
|
_ok(is_equal_approx(float((bull.get(&"velocity") as Vector3).y), 8.0), "a larger knock_up raises it further")
|
||||||
|
|
||||||
|
# The typed pipeline (Hitbox → DamageEvent → receive_damage) must produce identical outcomes to
|
||||||
|
# the string path it now wraps.
|
||||||
|
_last_hp = -1
|
||||||
|
bull.set(&"_hp", 3)
|
||||||
|
bull.set(&"_dead", false)
|
||||||
|
bull.set(&"_hit_iframes", 0.0)
|
||||||
|
bull.set(&"velocity", Vector3.ZERO)
|
||||||
|
bull.call(&"receive_damage", DamageEvent.make(DamageEvent.Cause.MAULED, Vector3.ZERO))
|
||||||
|
_ok(int(bull.get(&"_hp")) == 2, "receive_damage removes a pip like take_sword_hit")
|
||||||
|
_ok(_last_hp == 2, "receive_damage emits health_changed with the new pip count")
|
||||||
|
|
||||||
|
bull.set(&"_hit_iframes", 0.0)
|
||||||
|
bull.set(&"velocity", Vector3.ZERO)
|
||||||
|
var popping := DamageEvent.make(DamageEvent.Cause.SLAMMED, Vector3.ZERO)
|
||||||
|
popping.knock_up = 6.0
|
||||||
|
bull.call(&"receive_damage", popping)
|
||||||
|
_ok(is_equal_approx(float((bull.get(&"velocity") as Vector3).y), 6.0), "receive_damage applies the event's knock_up")
|
||||||
|
|
||||||
_finish()
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user