extends SceneTree ## zhan_bo (S charge + Space) must release along the player's current facing — ## never auto-aim at the nearest enemy — and projectile collision circles must ## stay inside the visible bullet/wave art. var failures: Array[String] = [] func _init() -> void: _run.call_deferred() func _run() -> void: await _check_release_follows_facing() await _check_projectile_collision_fits_visual() _finish() func _check_release_follows_facing() -> void: var parent := Node2D.new() root.add_child(parent) var player_scene := load("res://scenes/characters/player.tscn") as PackedScene var action := load("res://resources/actions/zhan_bo_lv1.tres") as Resource _expect(player_scene != null, "player scene should load") _expect(action != null, "zhan_bo action should load") if player_scene == null or action == null: parent.free() return var player := player_scene.instantiate() as Node2D parent.add_child(player) player.global_position = Vector2(1000.0, 560.0) # An enemy on the RIGHT must not pull the release direction anymore. var decoy := Node2D.new() decoy.add_to_group("enemies") parent.add_child(decoy) decoy.global_position = Vector2(1400.0, 560.0) await process_frame player.set("heading", Vector2.LEFT) var left_direction := player.call("projectile_direction", action) as Vector2 _expect(left_direction.x < 0.0, "facing left should release the wave to the left even with an enemy on the right") var left_spawn := player.call("projectile_spawn_position", action) as Vector2 _expect(left_spawn.x < player.global_position.x, "left release should spawn the wave on the player's left side") player.set("heading", Vector2.RIGHT) var right_direction := player.call("projectile_direction", action) as Vector2 _expect(right_direction.x > 0.0, "facing right should release the wave to the right") var right_spawn := player.call("projectile_spawn_position", action) as Vector2 _expect(right_spawn.x > player.global_position.x, "right release should spawn the wave on the player's right side") parent.free() func _check_projectile_collision_fits_visual() -> void: var projectile_scene := load("res://scenes/combat/player_projectile.tscn") as PackedScene _expect(projectile_scene != null, "projectile scene should load") if projectile_scene == null: return var bullet := projectile_scene.instantiate() as Area2D root.add_child(bullet) var wave := projectile_scene.instantiate() as Area2D wave.set("visual_profile", &"wave") root.add_child(wave) await process_frame var bullet_shape := _circle_shape(bullet) var wave_shape := _circle_shape(wave) _expect(bullet_shape != null, "bullet projectile should keep a CircleShape2D") _expect(wave_shape != null, "wave projectile should keep a CircleShape2D") if bullet_shape != null: # effect_sheet flight frames are ~32x18px visible at scale 2. _expect(float(bullet_shape.radius) <= 9.0, "bullet collision radius (%s) must not exceed the visible bolt half-height 9" % str(bullet_shape.radius)) if wave_shape != null: # Smallest wave frame is ~66x65px visible at scale 0.55. _expect(float(wave_shape.radius) <= 32.5, "wave collision radius (%s) must fit inside the smallest wave frame" % str(wave_shape.radius)) if bullet_shape != null and wave_shape != null: _expect(bullet_shape != wave_shape, "profiles must not share one CircleShape2D subresource") _expect(float(wave_shape.radius) > float(bullet_shape.radius), "wave hit circle should stay larger than the small bullet's") bullet.queue_free() wave.queue_free() func _circle_shape(projectile: Area2D) -> CircleShape2D: if projectile == null: return null var shape_node := projectile.get_node_or_null("CollisionShape2D") as CollisionShape2D if shape_node == null: return null return shape_node.shape as CircleShape2D func _expect(condition: bool, label: String) -> void: if not condition: failures.append(label) func _finish() -> void: if failures.is_empty(): print("PASS zhan_bo release facing and projectile collision fit") quit(0) else: for failure: String in failures: push_error(failure) quit(1)