Bull roll ability, console screen, tests

This commit is contained in:
2026-07-28 23:10:38 +03:00
parent a0b248748c
commit ebb6ec6335
16 changed files with 1494 additions and 283 deletions
+148
View File
@@ -39,10 +39,14 @@ func _run() -> void:
var scene: Node = scene_res.instantiate()
root.add_child(scene)
current_scene = scene # match runtime: game code (sword throw, overlays) uses current_scene
# Let physics, AI, and IK warm up.
await create_timer(0.5).timeout
_check_bull_animation()
await _check_overlays()
# Capture 5 frames ~0.4 s apart (covers roughly one wander step and walk cycle).
for i in range(5):
_capture_frame("motion_%02d.png" % i)
@@ -61,12 +65,156 @@ func _run() -> void:
await create_timer(0.8).timeout
_capture_frame("ragdoll_result.png")
_check_ragdoll_sanity(mat, start_pos)
_check_score_wiring(scene)
else:
_note("ragdoll check skipped — no matadors found in scene")
# Last, so any matadors the roll bowls over don't perturb the score check above.
await _check_roll_ability(scene)
_finish()
# ── Roll ability ──────────────────────────────────────────────────────────────
# Roll into a synthetic wall and confirm the bank shot: the heading reflects and the
# speed boosts past roll_max_speed (the natural ramp caps at max, so any excess is a
# wall boost). A low roll_duration lets it end within the sample window.
func _check_roll_ability(scene: Node) -> void:
print("\n-- check_roll_ability --")
var dp: Node = root.get_node_or_null("/root/DP")
var players := get_nodes_in_group(&"player")
if dp == null or players.is_empty():
_note("roll check: DP / player missing")
return
var player: Node = players[0]
var ap: AnimationPlayer = _find_anim_player(player)
_assert_true(player.ability_cd.size() == 4, "bull has 4 ability slots (roll added)")
var wall := StaticBody3D.new()
var cs := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(10, 5, 1)
cs.shape = box
wall.add_child(cs)
scene.add_child(wall)
wall.global_position = Vector3(0, 1, 8)
var restore_dur: float = dp.f("roll_duration")
dp.set_value("roll_duration", 0.6)
player.global_position = Vector3(0, 1, 0)
player.cube_guy.rotation.y = 0.0 # face +Z, into the wall
player.velocity = Vector3(0, 0, 40)
player.ability_cd[3] = 0.0
player._activate_roll()
_assert_true(player._active_ability == 3, "roll activates in slot 3")
if ap != null:
_assert_true(ap.current_animation == &"Armature|ROLL", "roll plays the ROLL clip")
var peak := 0.0
var reflected := false
for _n in 25:
await create_timer(0.03).timeout
peak = maxf(peak, Vector2(player.velocity.x, player.velocity.z).length())
if player._roll_dir.z < -0.2:
reflected = true
_assert_true(reflected, "roll ricochets its heading off the wall")
_assert_true(peak > dp.f("roll_max_speed") + 1.0, "wall ricochet boosts speed past roll_max_speed")
_assert_true(peak <= dp.f("roll_wall_cap") + 1.0, "ricochet boost stays under the cap")
if ap != null:
_assert_true(ap.current_animation == &"Armature|IDLE", "roll returns to IDLE when it ends")
dp.set_value("roll_duration", restore_dur)
wall.queue_free()
# ── Bull animation ────────────────────────────────────────────────────────────
# The bull sits idle in this test (no input), so it must be playing its looping
# IDLE clip — guards the idle wiring and the KICK clip rename in player.gd.
func _check_bull_animation() -> void:
print("\n-- check_bull_animation --")
var players := get_nodes_in_group(&"player")
if players.is_empty():
_note("bull anim check: no player in scene")
return
var ap: AnimationPlayer = _find_anim_player(players[0])
if ap == null:
_note("bull anim check: no AnimationPlayer under player")
return
_assert_true(ap.has_animation(&"Armature|IDLE"), "bull has IDLE clip")
_assert_true(ap.has_animation(&"Armature|KICK"), "bull has KICK clip (not stale FOOTKICK)")
_assert_true(ap.current_animation == &"Armature|IDLE", "bull plays IDLE at rest")
if ap.has_animation(&"Armature|IDLE"):
_assert_true(ap.get_animation(&"Armature|IDLE").loop_mode == Animation.LOOP_LINEAR,
"bull IDLE clip loops")
func _find_anim_player(node: Node) -> AnimationPlayer:
if node is AnimationPlayer:
return node
for child in node.get_children():
var r := _find_anim_player(child)
if r != null:
return r
return null
# ── Scoreboard wiring ─────────────────────────────────────────────────────────
# The ragdoll above is one matador kill; confirm it flowed matador → spawner →
# HUD and scored base × combo-1 = 100. Guards the whole kill→score signal chain.
func _check_score_wiring(scene: Node) -> void:
print("\n-- check_score_wiring --")
var hud: Node = _find_hud(scene)
if hud == null:
_note("score check: HUD not found in scene")
return
_assert_true(hud._score == 100, "one kill scores 100 (base × combo 1) — got %d" % hud._score)
_assert_true(hud._combo == 1, "one kill sets combo to 1 — got %d" % hud._combo)
func _find_hud(node: Node) -> Node:
if node is CanvasLayer and node.has_method(&"_on_matador_killed"):
return node
for child in node.get_children():
var r := _find_hud(child)
if r != null:
return r
return null
# ── Debug overlays ────────────────────────────────────────────────────────────
# Turn on every DebugDraw overlay against the live scene, confirm it builds, then
# turn them off and confirm it tears down — guards the reconcile bookkeeping.
func _check_overlays() -> void:
print("\n-- check_overlays --")
var dp: Node = root.get_node_or_null("/root/DP")
var draw: Node = root.get_node_or_null("/root/DebugDraw")
if dp == null or draw == null:
_note("overlay check: DP / DebugDraw autoload missing")
return
var flags := [
"show_states", "show_stats", "show_collisions", "show_hitboxes", "show_bones",
"show_animation_name", "show_wireframe", "show_grid", "show_axes",
]
for flag: String in flags:
dp.set_value(flag, true)
await create_timer(0.3).timeout
_assert_true(draw._state_labels.size() > 0, "overlay builds matador state labels")
_assert_true(draw._overlays.size() > 0, "overlay builds collision meshes")
_assert_true(draw._anim_labels.size() > 0, "overlay builds animation-name labels")
_assert_true(draw._stats_layer != null and draw._stats_layer.visible, "stats overlay visible")
_assert_true(not draw._stats_label.text.is_empty(), "stats overlay composes text")
for flag: String in flags:
dp.set_value(flag, false)
await create_timer(0.3).timeout
_assert_true(draw._state_labels.is_empty(), "overlay clears state labels when off")
_assert_true(draw._overlays.is_empty(), "overlay clears collision meshes when off")
_assert_true(draw._anim_labels.is_empty(), "overlay clears animation-name labels when off")
# ── Tail integrity ────────────────────────────────────────────────────────────
func _check_tail_integrity() -> void: