Win, lose screen. Game version info. Matador a lot more aggressive/wins easier (tweaks needed). No more score/combo/waves

This commit is contained in:
2026-07-30 12:39:51 +03:00
parent ebb6ec6335
commit bf7a454ad0
28 changed files with 1229 additions and 451 deletions
+121 -46
View File
@@ -22,6 +22,15 @@ const TAIL_BONES: Array[StringName] = [
var _passed: int = 0
var _failed: int = 0
var _report: PackedStringArray = []
var _matador_scene: PackedScene = null
# Loaded lazily (not preloaded): as a --script SceneTree entry, a top-level
# preload of a matador scene compiles matador.gd before the DP autoload binds.
func _mat_scene() -> PackedScene:
if _matador_scene == null:
_matador_scene = load("res://Matador.tscn")
return _matador_scene
func _init() -> void:
@@ -44,9 +53,21 @@ func _run() -> void:
# Let physics, AI, and IK warm up.
await create_timer(0.5).timeout
# The matador is lethal on contact now, so a wandering one could end the match
# during the non-combat checks below. Park the bull on a temp pad far out in the
# void, well out of any matador's reach (the roll tests use this spot too).
_isolate_bull(scene)
_check_bull_animation()
await _check_overlays()
# The overlay check needed live matadors; now remove them. The aggressive matador
# pursues the bull, and the ragdoll check spawns its own fresh one, so none should
# be roaming during the capture / tail / roll phases.
for m: Node in get_nodes_in_group(&"matador"):
m.queue_free()
await physics_frame
# 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)
@@ -55,30 +76,54 @@ func _run() -> void:
# After ~2.5 s the tail Verlet chain and IK should be stable.
_check_tail_integrity()
# Trigger ragdoll on the first matador and check that bones don't explode.
var matadors := get_nodes_in_group(&"matador")
if matadors.size() > 0:
var mat: Node3D = matadors[0] as Node3D
# Roll first, in open space, before any matador dies (a kill ends the match).
await _check_roll_ability(scene)
await create_timer(0.1).timeout # let the roll pop-test's throwaway matador free
# Ragdoll check on a FRESH matador spawned at the origin (the isolated bull is far
# away, so this one can't reach it). Not wired to the spawner, so its death won't
# trip the win screen. Ragdoll it immediately and check the bones don't explode.
var mat_scene := _mat_scene()
if mat_scene != null:
var mat: Node3D = mat_scene.instantiate()
scene.add_child(mat)
mat.global_position = Vector3(0.0, 1.0, 0.0)
await create_timer(0.3).timeout
var start_pos: Vector3 = mat.global_position
mat._enter_ragdoll(Vector3(0.0, 0.0, 1.0), 10.0)
_capture_frame("ragdoll_trigger.png")
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")
_note("ragdoll check skipped — Matador.tscn missing")
# Last, so any matadors the roll bowls over don't perturb the score check above.
await _check_roll_ability(scene)
# Last — a bull hit raises the lose screen and pauses the tree.
await _check_game_over_wiring(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.
# Drop a small static floor at (-45, -45) and stand the bull on it — 60+ m from the
# arena, so no matador can close the gap within the test window.
func _isolate_bull(scene: Node) -> void:
var pad := StaticBody3D.new()
var cs := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(20, 1, 20)
cs.shape = box
pad.add_child(cs)
scene.add_child(pad)
pad.global_position = Vector3(-45, -0.5, -45) # top surface at y = 0
var players := get_nodes_in_group(&"player")
if not players.is_empty():
(players[0] as Node3D).global_position = Vector3(-45, 1, -45)
# ── Roll ability (Rammus Powerball) ────────────────────────────────────────────
# Two behaviours: the speed ramps up the longer the ball rolls (base → max), capped
# at roll_max_speed with no wall-boost overshoot; and ramming a matador pops the ball
# (the roll ends). Rolled in empty space so it doesn't touch the real match matadors.
func _check_roll_ability(scene: Node) -> void:
print("\n-- check_roll_ability --")
@@ -91,41 +136,62 @@ func _check_roll_ability(scene: Node) -> void:
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)
var restore_ramp: float = dp.f("roll_rampup_time")
dp.set_value("roll_duration", 1.2)
dp.set_value("roll_rampup_time", 0.7)
player.global_position = Vector3(-45, 1, -45) # empty void, away from the arena
player.velocity = Vector3.ZERO
player.cube_guy.rotation.y = 0.0
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 early := -1.0
var peak := 0.0
var reflected := false
for _n in 25:
for n in 30:
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")
var spd := Vector2(player.velocity.x, player.velocity.z).length()
if n == 1:
early = spd
peak = maxf(peak, spd)
_assert_true(peak > early + 5.0, "roll speed ramps up over time (Powerball)")
_assert_true(peak <= dp.f("roll_max_speed") + 2.0, "roll speed caps at roll_max_speed (no wall boost)")
dp.set_value("roll_duration", restore_dur)
wall.queue_free()
dp.set_value("roll_rampup_time", restore_ramp)
await _check_roll_pop(scene, player)
# Roll into a throwaway matador (not one of the match spawns, so its death doesn't
# end the game) and confirm the ball pops: the roll ability ends on contact.
func _check_roll_pop(scene: Node, player: Node) -> void:
var mat_scene := _mat_scene()
if mat_scene == null:
_note("roll pop: Matador.tscn missing")
return
var mat: Node3D = mat_scene.instantiate()
scene.add_child(mat)
mat.global_position = Vector3(-45, 1, -38) # ~7 m ahead of the bull along +Z
await create_timer(0.2).timeout
player.global_position = Vector3(-45, 1, -45)
player.velocity = Vector3.ZERO
player.cube_guy.rotation.y = 0.0 # face +Z, toward the matador
player.ability_cd[3] = 0.0
player._activate_roll()
var popped := false
for _n in 45:
await create_timer(0.03).timeout
if player._active_ability != 3:
popped = true
break
_assert_true(popped, "roll pops (ends) on ramming a matador")
if is_instance_valid(mat):
mat.queue_free()
# ── Bull animation ────────────────────────────────────────────────────────────
@@ -160,22 +226,31 @@ func _find_anim_player(node: Node) -> AnimationPlayer:
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.
# ── Win / lose wiring ─────────────────────────────────────────────────────────
# One clean sword hit must end the run in a loss, and the signal chain must be in
# place for a win (spawner.all_defeated → HUD). Runs last: it pauses the tree.
func _check_score_wiring(scene: Node) -> void:
print("\n-- check_score_wiring --")
func _check_game_over_wiring(scene: Node) -> void:
print("\n-- check_game_over_wiring --")
var hud: Node = _find_hud(scene)
if hud == null:
_note("score check: HUD not found in scene")
var players := get_nodes_in_group(&"player")
if hud == null or players.is_empty():
_note("game over check: HUD / player missing")
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)
var player: Node = players[0]
_assert_true(player.has_signal(&"died"), "player exposes a died signal")
var spawners := get_nodes_in_group(&"matador_spawn")
_assert_true(spawners.is_empty() or spawners[0].has_signal(&"all_defeated"),
"spawner exposes an all_defeated signal (win route)")
player.take_sword_hit()
await create_timer(0.05).timeout
_assert_true(hud._game_over, "a single sword hit raises the game-over screen")
_assert_true(not hud._result_win, "a bull hit is a loss, not a win")
func _find_hud(node: Node) -> Node:
if node is CanvasLayer and node.has_method(&"_on_matador_killed"):
if node is CanvasLayer and node.has_method(&"_show_game_over"):
return node
for child in node.get_children():
var r := _find_hud(child)