mirror of
https://github.com/ElHombreAlex/FTL-Faster-Than-Light-VR.git
synced 2026-10-06 01:00:06 +02:00
221 lines
11 KiB
GDScript
221 lines
11 KiB
GDScript
extends SceneTree
|
||
|
||
const ShipModel = preload("res://scripts/ship_model.gd")
|
||
const TargetLocks = preload("res://scripts/target_locks.gd")
|
||
const UiAssets = preload("res://scripts/ftl_ui_assets.gd")
|
||
var failures := 0
|
||
|
||
|
||
func _initialize() -> void:
|
||
call_deferred("_run")
|
||
|
||
|
||
func _check(condition: bool, message: String) -> void:
|
||
if not condition:
|
||
failures += 1
|
||
push_error(message)
|
||
|
||
|
||
func _run() -> void:
|
||
var encounter := Node3D.new()
|
||
root.add_child(encounter)
|
||
var player := ShipModel.new()
|
||
encounter.add_child(player)
|
||
player.build("__target_locks_fixture__", false)
|
||
player.set_process(false)
|
||
var enemy := ShipModel.new()
|
||
encounter.add_child(enemy)
|
||
enemy.build("__target_locks_fixture__", true)
|
||
enemy.set_process(false)
|
||
enemy.layout_data.x_offset = 2
|
||
enemy.layout_data.y_offset = 1
|
||
var renderer := TargetLocks.new()
|
||
root.add_child(renderer)
|
||
var laser := {"slot": 0, "kind": "laser", "target_ship": 1, "autofire": false,
|
||
"targets": [{"x": 105, "y": 70}, {"x": 175, "y": 140}]}
|
||
var beam := {"slot": 1, "kind": "beam", "beam_length": 100, "target_ship": 1, "autofire": true,
|
||
"targets": [{"x": 245, "y": 140}, {"x": 105, "y": 70}]}
|
||
var flak := {"slot": 2, "kind": "flak", "target_ship": 1, "target_radius": 30,
|
||
"targets": [{"x": 175, "y": 70}]}
|
||
var state := {"ready": true, "combat": true, "paused": true, "weapon_selected": -1,
|
||
"targeting": {"active": false}, "player": {"weapons": [laser, beam, flak]},
|
||
"enemy": {"rooms": [{"id": 0, "visible": false}], "weapons": [laser]}}
|
||
var original := JSON.stringify(state)
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks.size() == 3, "Native locks must persist after the armed cursor closes, including during pause")
|
||
_check(JSON.stringify(state) == original, "Target lock presentation must never mutate the game snapshot")
|
||
var local_target: Vector3 = enemy.pixel_point(laser.targets[0], TargetLocks.HEIGHT)
|
||
_check(renderer.locks[0].end.is_equal_approx(local_target), "Native target conversion must remove layout offsets")
|
||
_check(Vector2(local_target.x, local_target.z).is_equal_approx(enemy.room_bounds[0].get_center()), "The lock must remain centered on the native room")
|
||
_check(renderer.locks[0].node.get_child_count() <= 2, "Charged weapon extra points must not invent reticles absent from the native aiming renderer")
|
||
_check(renderer.locks[1].start.is_equal_approx(enemy.pixel_point(beam.targets[1], TargetLocks.HEIGHT))
|
||
and renderer.locks[1].end.is_equal_approx(enemy.pixel_point(beam.targets[0], TargetLocks.HEIGHT)), "Beam direction must use native targets[1] to targets[0]")
|
||
var radius_mesh: MeshInstance3D = renderer.locks[2].node.get_node("FlakRadius")
|
||
_check(is_equal_approx(radius_mesh.mesh.top_radius, 30.0 * ShipModel.TILE / 35.0), "Flak circle must use the actual native spread radius")
|
||
_check(renderer.locks[1].node.get_meta("autofire") and renderer.locks[1].node.get_node("WeaponSlot").modulate == TargetLocks.AUTOFIRE,
|
||
"Autofire must use the native yellow cue")
|
||
var retained_id: int = renderer.locks[0].node.get_instance_id()
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks[0].node.get_instance_id() == retained_id, "Identical snapshots must retain existing lock geometry")
|
||
encounter.position = Vector3(1.7, 0.4, -1.2)
|
||
encounter.rotation_degrees = Vector3(17, 39, -12)
|
||
encounter.scale = Vector3(0.65, 1.2, 0.83)
|
||
enemy.position = Vector3(0.23, 0.17, -0.48)
|
||
enemy.rotation_degrees = Vector3(-8, -23, 14)
|
||
renderer.refresh_transforms()
|
||
_check(renderer.locks[0].node.to_global(renderer.locks[0].end).is_equal_approx(enemy.to_global(local_target)),
|
||
"Locks must follow moving, rotated and nonuniformly scaled ships without waiting for a new snapshot")
|
||
_check(renderer.locks[1].node.global_transform.is_equal_approx(enemy.global_transform), "Beam endpoint geometry must follow the same ship transform")
|
||
enemy.hide()
|
||
renderer.refresh_transforms()
|
||
_check(not renderer.locks[0].node.visible, "Hidden ships must hide their locks immediately")
|
||
enemy.show()
|
||
renderer.refresh_transforms()
|
||
_check(renderer.locks[0].node.visible, "Visible native locks must return with their ship")
|
||
laser.targets = [{"x": 175, "y": 140}]
|
||
laser.autofire = true
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks[0].node.get_instance_id() != retained_id and renderer.locks[0].node.get_meta("autofire"),
|
||
"Native retargeting and autofire colour changes must replace stale geometry immediately")
|
||
laser.targets = []
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(not renderer.locks.has(0) and renderer.locks.size() == 2, "Clearing one native target must remove only that weapon's lock")
|
||
state.player.weapons = []
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks.is_empty(), "Enemy weapon intent must never create a lock on either ship")
|
||
var self_bomb := {"slot": 0, "kind": "bomb", "target_ship": 0, "targets": [{"x": 35, "y": 35}]}
|
||
state.player.weapons = [self_bomb]
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks[0].receiver == player, "A player-owned bomb must honor its actual friendly target ship")
|
||
state.combat = false
|
||
state.player.weapons = [self_bomb, beam]
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks.size() == 1 and renderer.locks.has(0) and renderer.locks[0].receiver == player,
|
||
"A native self-target bomb must retain its lock without an enemy, while enemy targets disappear")
|
||
state.combat = true
|
||
state.player.weapons = [self_bomb]
|
||
self_bomb.target_ship = -1
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks.is_empty(), "Invalid native target ship IDs must clear previous locks")
|
||
state.player.weapons = [beam]
|
||
beam.targets = [{"x": 245, "y": 140}]
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks.is_empty(), "A partially placed real beam must not show a fabricated second endpoint")
|
||
beam.beam_length = 1
|
||
beam.targets = [{"x": 241, "y": 136}]
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(not renderer.locks[1].beam and renderer.locks[1].end.is_equal_approx(enemy.pixel_point({"x": 227.5, "y": 122.5}, TargetLocks.HEIGHT)),
|
||
"Pinpoint beams must use the native tile center and a placed reticle")
|
||
beam.targets = [{"x": NAN, "y": 140}]
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks.is_empty(), "Nonfinite native coordinates must not reach render geometry")
|
||
beam.targets = [{"x": 245, "y": 140}]
|
||
for gate in ["combat", "ready"]:
|
||
state[gate] = false
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks.is_empty(), "An ended encounter or inactive game must remove every lock")
|
||
state[gate] = true
|
||
state.transition = true
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks.is_empty(), "Jump transition must remove old encounter locks")
|
||
state.transition = false
|
||
state.enemy.destroyed = true
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks.is_empty(), "Native enemy destruction must remove its locks")
|
||
state.enemy.destroyed = false
|
||
renderer.apply_snapshot(state, player, enemy)
|
||
_check(renderer.locks.size() == 1, "Current native target state must recreate a valid lock")
|
||
enemy.queue_free()
|
||
await process_frame
|
||
renderer.refresh_transforms()
|
||
_check(renderer.locks.is_empty(), "Freed receivers must remove locks without accessing their transforms")
|
||
# Exercise the source-only fallback even when owned slot 1–4 art is present.
|
||
var fallback := Node3D.new()
|
||
root.add_child(fallback)
|
||
renderer._make_reticle(fallback, Vector3.ZERO, 8, false)
|
||
renderer._finish_lines(fallback)
|
||
_check(fallback.get_node("LockLines").mesh.get_surface_count() == 1 and fallback.get_child_count() == 2,
|
||
"Procedural reticles must batch all strokes into one surface and retain the weapon slot cue")
|
||
fallback.queue_free()
|
||
encounter.queue_free()
|
||
renderer.queue_free()
|
||
await process_frame
|
||
if failures == 0 and OS.get_cmdline_user_args().has("--capture"):
|
||
await _capture_fixture()
|
||
print("TARGET_LOCK_TESTS ", "PASS" if failures == 0 else "FAIL %d" % failures)
|
||
quit(0 if failures == 0 else 1)
|
||
|
||
|
||
func _capture_fixture() -> void:
|
||
# Renderer-only oracle: no native process, game commands, input or save writes.
|
||
root.size = Vector2i(1400, 900)
|
||
var world := WorldEnvironment.new()
|
||
var environment := Environment.new()
|
||
environment.background_mode = Environment.BG_COLOR
|
||
environment.background_color = Color("101923")
|
||
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||
environment.ambient_light_color = Color("d4e7f2")
|
||
environment.ambient_light_energy = 0.85
|
||
world.environment = environment
|
||
root.add_child(world)
|
||
var camera := Camera3D.new()
|
||
camera.projection = Camera3D.PROJECTION_ORTHOGONAL
|
||
camera.size = 1.45
|
||
camera.position = Vector3(0, 2.0, 1.35)
|
||
root.add_child(camera)
|
||
camera.look_at(Vector3.ZERO)
|
||
var enemy := ShipModel.new()
|
||
root.add_child(enemy)
|
||
enemy.build("__target_locks_fixture__", true)
|
||
enemy.set_process(false)
|
||
enemy.inspect_rooms = true
|
||
var rooms: Array = []
|
||
for id in range(6):
|
||
rooms.append({"id": id, "visible": id != 2})
|
||
enemy.apply_live({"rooms": rooms})
|
||
var renderer := TargetLocks.new()
|
||
root.add_child(renderer)
|
||
var state := {"ready": true, "combat": true, "player": {"weapons": [
|
||
{"slot": 0, "kind": "laser", "target_ship": 1, "targets": [{"x": 35, "y": 35}]},
|
||
{"slot": 1, "kind": "laser", "autofire": true, "target_ship": 1, "targets": [{"x": 105, "y": 35}]},
|
||
{"slot": 2, "kind": "flak", "target_ship": 1, "target_radius": 32, "targets": [{"x": 175, "y": 35}]},
|
||
{"slot": 3, "kind": "beam", "beam_length": 140, "target_ship": 1, "targets": [{"x": 175, "y": 105}, {"x": 35, "y": 105}]}]},
|
||
"enemy": {"rooms": rooms}}
|
||
renderer.apply_snapshot(state, null, enemy)
|
||
var title := _caption("NATIVE WEAPON TARGET LOCKS", Vector2(40, 32), 34)
|
||
var footer := _caption("Red: single volley / Yellow: autofire / Circle: flak spread / Beam: actual sweep direction", Vector2(40, 837), 22)
|
||
await _capture("target_locks_fixture.png")
|
||
enemy.position = Vector3(0.12, 0.03, 0.05)
|
||
enemy.rotation_degrees = Vector3(13, 31, -9)
|
||
enemy.scale = Vector3(1.2, 0.8, 1.08)
|
||
renderer.refresh_transforms()
|
||
camera.size = 1.75
|
||
title.text = "LOCKS FOLLOW THE MOVED AND SCALED SHIP"
|
||
footer.text = "Room locks and both beam endpoints remain attached during pause. Sensor fog reveals no crew or system details."
|
||
await _capture("target_locks_transformed_fixture.png")
|
||
state.player.weapons = []
|
||
renderer.apply_snapshot(state, null, enemy)
|
||
title.text = "NATIVE TARGET CLEAR REMOVES ALL LOCKS"
|
||
footer.text = "Weapon target lists are authoritative; cleared targets disappear immediately."
|
||
await _capture("target_locks_cleared_fixture.png")
|
||
|
||
|
||
func _capture(filename: String) -> void:
|
||
for i in range(12):
|
||
await process_frame
|
||
await RenderingServer.frame_post_draw
|
||
var path := ProjectSettings.globalize_path("res://local_game_data/" + filename)
|
||
root.get_texture().get_image().save_png(path)
|
||
print("TARGET_LOCK_CAPTURE ", path)
|
||
|
||
|
||
func _caption(text: String, point: Vector2, size: int) -> Label:
|
||
var label := Label.new()
|
||
label.text = text
|
||
label.position = point
|
||
label.add_theme_font_override("font", UiAssets.font("body"))
|
||
label.add_theme_font_size_override("font_size", size)
|
||
label.modulate = Color("fff2c4")
|
||
root.add_child(label)
|
||
return label
|