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:
|
||||
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/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/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
|
||||
|
||||
@@ -41,3 +41,22 @@ static func make(kind: Cause, at: Vector3, src: Node = null) -> DamageEvent:
|
||||
## ("gored" / "mauled" / "thrown") during the migration off string causes.
|
||||
func cause_name() -> String:
|
||||
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
|
||||
_hit_this_swing[body] = true
|
||||
_did_hit = true
|
||||
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)
|
||||
if body.has_method(&"receive_damage"):
|
||||
var event := DamageEvent.make(DamageEvent.cause_from(damage_cause), body.global_position, self)
|
||||
if knock_up > 0.0:
|
||||
event.knock_up = _knock_up_for(body)
|
||||
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.
|
||||
|
||||
@@ -1222,6 +1222,15 @@ func apply_knock_up(v: float) -> void:
|
||||
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
|
||||
# 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.
|
||||
|
||||
@@ -18,6 +18,14 @@ echo ""
|
||||
echo "=== Logic tests ==="
|
||||
"$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 "=== Console focus tests ==="
|
||||
"$GODOT" --headless --script tests/console_focus_test.gd
|
||||
|
||||
@@ -87,6 +87,24 @@ func _run() -> void:
|
||||
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")
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user