196 lines
7.1 KiB
GDScript
196 lines
7.1 KiB
GDScript
extends SceneTree
|
|
|
|
const DriverScript := preload("res://scenes/components/frame_collision_driver.gd")
|
|
const EmitterScript := preload("res://scenes/components/damage_emitter.gd")
|
|
const ReceiverScript := preload("res://scenes/components/damage_receiver.gd")
|
|
const ActionControllerScript := preload("res://scenes/components/action_controller.gd")
|
|
const ActionDataScript := preload("res://resources/action_data.gd")
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _init() -> void:
|
|
_run.call_deferred()
|
|
|
|
|
|
func _run() -> void:
|
|
var actor := _build_actor()
|
|
root.add_child(actor)
|
|
await process_frame
|
|
|
|
var driver: Node = actor.get_node("FrameCollisionDriver")
|
|
var receiver := actor.get_node("DamageReceiver") as Area2D
|
|
var receiver_shape := receiver.get_node("CollisionShape2D") as CollisionShape2D
|
|
var emitter := actor.get_node("DamageEmitter") as Area2D
|
|
var emitter_shape := emitter.get_node("CollisionShape2D") as CollisionShape2D
|
|
var sprite := actor.get_node("Visual/CharacterSprite") as Sprite2D
|
|
var controller: Node = actor.get_node("ActionController")
|
|
|
|
# --- hurtbox follows the displayed frame's opaque bounds ---
|
|
sprite.frame = 0
|
|
driver.call("refresh_now")
|
|
var body_size := (receiver_shape.shape as RectangleShape2D).size
|
|
_expect_float(body_size.x, 24.0, "Frame 0 hurtbox width should match the small pose")
|
|
_expect_float(body_size.y, 40.0, "Frame 0 hurtbox height should match the pose")
|
|
_expect_vector(receiver_shape.position, Vector2(0.0, -20.0), "Frame 0 hurtbox should centre on the pose")
|
|
|
|
sprite.frame = 2
|
|
driver.call("refresh_now")
|
|
var extended_size := (receiver_shape.shape as RectangleShape2D).size
|
|
_expect_float(extended_size.x, 44.0, "Frame 2 hurtbox should widen with the extended pose")
|
|
_expect_bool(extended_size.x > body_size.x, true, "Damage-receive matrix must change per frame")
|
|
|
|
# --- hitbox only monitors on reaching ACTIVE frames ---
|
|
var action: Resource = ActionDataScript.new()
|
|
action.set("hit_type", &"melee")
|
|
action.set("range", 40.0)
|
|
emitter.call("configure_hit", action, {"label": "perfect"})
|
|
controller.set("phase", ActionControllerScript.Phase.ACTIVE)
|
|
sprite.frame = 0
|
|
driver.call("refresh_now")
|
|
_expect_bool(emitter.monitoring, false, "Windup frame (no forward reach) must not send damage")
|
|
sprite.frame = 2
|
|
driver.call("refresh_now")
|
|
_expect_bool(emitter.monitoring, true, "Weapon-extended frame must send damage")
|
|
_expect_bool(emitter_shape.position.x < 0.0, true, "Facing left, the hit shape extends left")
|
|
_expect_bool((emitter_shape.shape as RectangleShape2D).size.x >= 30.0, true, "Hit reach should cover max(pose reach, action range)")
|
|
|
|
# recovery: leaving ACTIVE closes the window on the same frame
|
|
controller.set("phase", ActionControllerScript.Phase.RECOVERY)
|
|
driver.call("refresh_now")
|
|
_expect_bool(emitter.monitoring, false, "Leaving ACTIVE must close the hit window")
|
|
controller.set("phase", ActionControllerScript.Phase.ACTIVE)
|
|
|
|
# --- mirroring follows heading/Visual flip ---
|
|
actor.set("heading", Vector2.RIGHT)
|
|
(actor.get_node("Visual") as Node2D).scale.x = -2.0
|
|
driver.call("refresh_now")
|
|
_expect_bool(emitter.monitoring, true, "Mirrored reach frame still sends damage")
|
|
_expect_bool(emitter_shape.position.x > 0.0, true, "Facing right, the hit shape extends right")
|
|
|
|
# --- body matrix is re-asserted every tick ---
|
|
actor.collision_layer = 5
|
|
actor.collision_mask = 99
|
|
driver.call("refresh_now")
|
|
_expect_int(actor.collision_layer, 32, "Body collision layer must be re-applied per frame")
|
|
_expect_int(actor.collision_mask, 65, "Body collision mask must be re-applied per frame")
|
|
|
|
# --- one hit per swing even with per-frame windows ---
|
|
var target := Area2D.new()
|
|
target.add_to_group("damage_receivers")
|
|
root.add_child(target)
|
|
emitter.call("_on_area_entered", target)
|
|
emitter.call("_on_area_entered", target)
|
|
_expect_bool(emitter.call("has_already_hit", target), true, "Emitter should remember receivers hit this swing")
|
|
emitter.call("clear_hit")
|
|
_expect_bool(emitter.call("has_already_hit", target), false, "clear_hit should reset the per-swing memory")
|
|
|
|
target.free()
|
|
actor.free()
|
|
_finish()
|
|
|
|
|
|
func _build_actor() -> CharacterBody2D:
|
|
var actor_script := GDScript.new()
|
|
actor_script.source_code = "extends CharacterBody2D\nvar heading := Vector2.LEFT\n"
|
|
actor_script.reload()
|
|
var actor := CharacterBody2D.new()
|
|
actor.set_script(actor_script)
|
|
actor.collision_layer = 32
|
|
actor.collision_mask = 65
|
|
|
|
var visual := Node2D.new()
|
|
visual.name = "Visual"
|
|
visual.scale = Vector2(2, 2)
|
|
actor.add_child(visual)
|
|
|
|
var sprite := Sprite2D.new()
|
|
sprite.name = "CharacterSprite"
|
|
sprite.centered = false
|
|
sprite.offset = Vector2(-16, -32)
|
|
sprite.hframes = 4
|
|
sprite.vframes = 1
|
|
sprite.texture = _build_sheet()
|
|
visual.add_child(sprite)
|
|
|
|
var emitter := Area2D.new()
|
|
emitter.name = "DamageEmitter"
|
|
emitter.set_script(EmitterScript)
|
|
emitter.collision_layer = 8
|
|
emitter.collision_mask = 4
|
|
emitter.monitoring = false
|
|
var emitter_shape := CollisionShape2D.new()
|
|
emitter_shape.name = "CollisionShape2D"
|
|
emitter_shape.shape = RectangleShape2D.new()
|
|
emitter.add_child(emitter_shape)
|
|
actor.add_child(emitter)
|
|
|
|
var receiver := Area2D.new()
|
|
receiver.name = "DamageReceiver"
|
|
receiver.set_script(ReceiverScript)
|
|
receiver.collision_layer = 2
|
|
receiver.collision_mask = 16
|
|
var receiver_shape := CollisionShape2D.new()
|
|
receiver_shape.name = "CollisionShape2D"
|
|
receiver_shape.shape = RectangleShape2D.new()
|
|
receiver.add_child(receiver_shape)
|
|
actor.add_child(receiver)
|
|
|
|
var controller_script := GDScript.new()
|
|
controller_script.source_code = "extends Node\nvar phase := 0\n"
|
|
controller_script.reload()
|
|
var controller := Node.new()
|
|
controller.name = "ActionController"
|
|
controller.set_script(controller_script)
|
|
actor.add_child(controller)
|
|
|
|
var driver := Node.new()
|
|
driver.name = "FrameCollisionDriver"
|
|
driver.set_script(DriverScript)
|
|
driver.set("hurt_margin", Vector2.ZERO)
|
|
actor.add_child(driver)
|
|
return actor
|
|
|
|
|
|
func _build_sheet() -> ImageTexture:
|
|
# 4 frames of 32x32. Frame 0: compact body (x 10..21). Frame 2: weapon
|
|
# extended to the native-left edge (x 0..21). Frames 1/3 stay empty.
|
|
var image := Image.create(128, 32, false, Image.FORMAT_RGBA8)
|
|
image.fill(Color(0, 0, 0, 0))
|
|
for y: int in range(12, 32):
|
|
for x: int in range(10, 22):
|
|
image.set_pixel(x, y, Color.WHITE)
|
|
for x: int in range(64, 86):
|
|
image.set_pixel(x, y, Color.WHITE)
|
|
return ImageTexture.create_from_image(image)
|
|
|
|
|
|
func _expect_bool(actual: bool, expected: bool, label: String) -> void:
|
|
if actual != expected:
|
|
failures.append("%s: expected %s, got %s" % [label, expected, actual])
|
|
|
|
|
|
func _expect_int(actual: int, expected: int, label: String) -> void:
|
|
if actual != expected:
|
|
failures.append("%s: expected %d, got %d" % [label, expected, actual])
|
|
|
|
|
|
func _expect_float(actual: float, expected: float, label: String) -> void:
|
|
if absf(actual - expected) > 0.5:
|
|
failures.append("%s: expected %.2f, got %.2f" % [label, expected, actual])
|
|
|
|
|
|
func _expect_vector(actual: Vector2, expected: Vector2, label: String) -> void:
|
|
if actual.distance_to(expected) > 0.75:
|
|
failures.append("%s: expected %s, got %s" % [label, expected, actual])
|
|
|
|
|
|
func _finish() -> void:
|
|
if failures.is_empty():
|
|
print("PASS frame collision driver")
|
|
quit(0)
|
|
else:
|
|
for failure: String in failures:
|
|
push_error(failure)
|
|
quit(1)
|