diff --git a/CLAUDE.md b/CLAUDE.md index d01d2f1..a2934ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/damage_event.gd b/damage_event.gd index c731717..33ea963 100644 --- a/damage_event.gd +++ b/damage_event.gd @@ -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 diff --git a/hitbox.gd b/hitbox.gd index da0dd59..bfba987 100644 --- a/hitbox.gd +++ b/hitbox.gd @@ -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. diff --git a/player.gd b/player.gd index f3a4d2a..e14f35f 100644 --- a/player.gd +++ b/player.gd @@ -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. diff --git a/run_tests.sh b/run_tests.sh index 92625fb..d016d17 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -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 diff --git a/tests/damage_pipeline_test.gd b/tests/damage_pipeline_test.gd index 1deb4e6..66ff8cf 100644 --- a/tests/damage_pipeline_test.gd +++ b/tests/damage_pipeline_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()