Quality Gate / Quality Gate (pull_request) Successful in 9m4s
Instance twelve Blender-scripted glTF models in the cockpit scene and rebuild the cabin around them: gauge panel, sonar, switch panel, alert box, speaker, porthole, hull wall, caged lamps, gas bottle, handrails and side consoles. Presentation only; no domain, application or world rule changes beyond enabling glow in the test environment. - Needles, alert lamps and sonar screen are driven by the existing snapshots; the alert box now carries SYS/ATTN/ALRM. - Depth dial reads percent of certified depth. - Sonar display restyled as a turquoise holographic relief with a shader grid and height scale; SubViewport matches the glass (960x604). - Cabin lighting retuned: warm pooled lamp light, cool porthole rim, lamp shadows, dial backlight. - Procedural stand-ins removed (bulkhead, tanks, consoles, levers, indicator columns, placeholder pipe run, dial face PNGs). - Tests adapted where nodes disappeared; one assertion removed (pipe_segment_count) until the pipe kit is placed. - Model .glb files are committed without LFS; textures Godot extracts next to them at import are ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
479 lines
19 KiB
GDScript
479 lines
19 KiB
GDScript
extends SceneTree
|
|
|
|
const VerticalSliceRoot = preload(
|
|
"res://game/composition/vertical_slice/vertical_slice_root.gd"
|
|
)
|
|
const MainMenuScreen = preload("res://game/presentation/main_menu/main_menu_screen.gd")
|
|
const CockpitHud = preload("res://game/presentation/cockpit/cockpit_hud.gd")
|
|
const CockpitDetailing = preload(
|
|
"res://game/presentation/cockpit/cockpit_detailing.gd"
|
|
)
|
|
const CockpitPressureBulkhead = preload(
|
|
"res://game/presentation/cockpit/cockpit_pressure_bulkhead.gd"
|
|
)
|
|
const CockpitAlertBox = preload("res://game/presentation/cockpit/cockpit_alert_box.gd")
|
|
const CockpitAnalogGauges = preload(
|
|
"res://game/presentation/cockpit/cockpit_analog_gauges.gd"
|
|
)
|
|
const CockpitSonarConsole = preload(
|
|
"res://game/presentation/cockpit/cockpit_sonar_console.gd"
|
|
)
|
|
const CockpitSwitchPanel = preload(
|
|
"res://game/presentation/cockpit/cockpit_switch_panel.gd"
|
|
)
|
|
const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd")
|
|
const PauseMenu = preload("res://game/presentation/pause/pause_menu.gd")
|
|
const ControlsSettingsScreen = preload(
|
|
"res://game/presentation/settings/controls_settings_screen.gd"
|
|
)
|
|
const ProductionInputAdapter = preload(
|
|
"res://game/infrastructure/input/production_input_adapter.gd"
|
|
)
|
|
|
|
const TEST_COUNT := 91
|
|
|
|
var _failures: Array[String] = []
|
|
|
|
|
|
func _init() -> void:
|
|
_run.call_deferred()
|
|
|
|
|
|
func _run() -> void:
|
|
change_scene_to_file(
|
|
"res://game/composition/vertical_slice/vertical_slice_root.tscn"
|
|
)
|
|
await process_frame
|
|
await physics_frame
|
|
|
|
var composition := current_scene as VerticalSliceRoot
|
|
_expect(composition != null, "production composition loads")
|
|
if composition != null:
|
|
await _test_vehicle_slice(composition)
|
|
|
|
_expect(
|
|
int(ProjectSettings.get_setting("physics/common/physics_ticks_per_second")) == 60,
|
|
"project physics runs at 60 Hz"
|
|
)
|
|
var project_features: PackedStringArray = ProjectSettings.get_setting(
|
|
"application/config/features"
|
|
)
|
|
_expect("Forward Plus" in project_features, "production project uses Forward+")
|
|
|
|
var exit_code := 0
|
|
if _failures.is_empty():
|
|
print("Vertical slice foundation integration: %d passed, 0 failed." % TEST_COUNT)
|
|
else:
|
|
for failure: String in _failures:
|
|
push_error(failure)
|
|
print("Vertical slice foundation integration: %d failed." % _failures.size())
|
|
exit_code = 1
|
|
|
|
Input.action_release(ProductionInputAdapter.ACTION_FORWARD)
|
|
Input.action_release(ProductionInputAdapter.ACTION_RIGHT)
|
|
if current_scene != null:
|
|
current_scene.queue_free()
|
|
await process_frame
|
|
await process_frame
|
|
quit(exit_code)
|
|
|
|
|
|
func _test_vehicle_slice(composition: VerticalSliceRoot) -> void:
|
|
var menu := composition.active_screen() as MainMenuScreen
|
|
_expect(menu != null, "composition starts on main menu")
|
|
if menu == null:
|
|
return
|
|
var continue_button := menu.get_node("%ContinueButton") as Button
|
|
_expect(continue_button.disabled, "continue is disabled without save")
|
|
var new_game_button := menu.get_node("%NewGameButton") as Button
|
|
new_game_button.pressed.emit()
|
|
await process_frame
|
|
await physics_frame
|
|
|
|
var actor = composition.submarine_actor()
|
|
var camera = composition.camera_controller()
|
|
var instruments = composition.cockpit_instrument_panel()
|
|
var detailing: CockpitDetailing = (
|
|
instruments.detailing() if instruments != null else null
|
|
)
|
|
var pressure_bulkhead := (
|
|
instruments.get_node_or_null("%CurvedPressureBulkhead") as CockpitPressureBulkhead
|
|
if instruments != null
|
|
else null
|
|
)
|
|
var analog_gauges := (
|
|
instruments.get_node_or_null("Dashboard/AnalogGauges") as CockpitAnalogGauges
|
|
if instruments != null
|
|
else null
|
|
)
|
|
var alert_box := (
|
|
instruments.get_node_or_null("AlertBox") as CockpitAlertBox
|
|
if instruments != null
|
|
else null
|
|
)
|
|
var switch_panel := (
|
|
instruments.get_node_or_null("SwitchPanel") as CockpitSwitchPanel
|
|
if instruments != null
|
|
else null
|
|
)
|
|
var adapter := composition.input_adapter() as ProductionInputAdapter
|
|
var hud := composition.active_screen() as CockpitHud
|
|
var sonar_service = composition.sonar_service()
|
|
var sonar_display = composition.sonar_display()
|
|
_expect(composition.active_world() != null, "vehicle test world is assembled")
|
|
_expect(actor != null, "production submarine actor is assembled")
|
|
_expect(camera != null, "cockpit camera is attached to the submarine")
|
|
_expect(instruments != null, "physical cockpit instruments are assembled")
|
|
_expect(detailing != null, "cockpit structural detailing is assembled")
|
|
_expect(pressure_bulkhead != null, "curved pressure bulkhead is assembled")
|
|
_expect(analog_gauges != null, "dashboard analog instruments are assembled")
|
|
_expect(alert_box != null, "alert box is assembled")
|
|
_expect(switch_panel != null, "switch panel is assembled")
|
|
_expect(hud != null, "critical HUD overlay is assembled")
|
|
_expect(sonar_service != null, "production sonar service is assembled")
|
|
_expect(sonar_display != null, "shared sonar presentation is assembled")
|
|
_expect(composition.latest_update() != null, "dive session starts with a typed update")
|
|
if (
|
|
actor == null
|
|
or camera == null
|
|
or instruments == null
|
|
or detailing == null
|
|
or pressure_bulkhead == null
|
|
or analog_gauges == null
|
|
or alert_box == null
|
|
or switch_panel == null
|
|
or adapter == null
|
|
or hud == null
|
|
):
|
|
return
|
|
|
|
_expect(pressure_bulkhead.porthole_frame() is MeshInstance3D, "cockpit has a physical porthole frame")
|
|
_expect(instruments.get_node("%Dashboard") is Node3D, "cockpit has a physical dashboard")
|
|
_expect(instruments.get_node("%SonarScreen") is MeshInstance3D, "cockpit reserves a physical sonar display")
|
|
var sonar_monitor := instruments.get_node("SonarMonitor") as CockpitSonarConsole
|
|
var sonar_screen := instruments.get_node("%SonarScreen") as MeshInstance3D
|
|
var sonar_housing := sonar_monitor.housing()
|
|
var cockpit_camera := camera.get_node("Camera") as Camera3D
|
|
var electrical_bank := detailing.get_node("SideEquipment/ElectricalBank") as Node3D
|
|
_expect(sonar_monitor.position.x > 0.5, "cockpit sonar is mounted on the right side")
|
|
_expect(
|
|
_sonar_screen_faces_camera(sonar_screen, cockpit_camera),
|
|
"right-side sonar display faces the pilot"
|
|
)
|
|
_expect(
|
|
_sonar_screen_is_ahead_of_bank(sonar_screen, electrical_bank),
|
|
"right-side electrical equipment does not hide the sonar display"
|
|
)
|
|
_expect(
|
|
_sonar_screen_clears_housing(sonar_screen, sonar_housing),
|
|
"sonar texture clears its housing without depth overlap"
|
|
)
|
|
_expect(
|
|
instruments.find_children("SonarReferenceTrace", "MeshInstance3D", true).is_empty(),
|
|
"cockpit sonar does not invent a decorative profile"
|
|
)
|
|
_expect(
|
|
_shell_surrounds(pressure_bulkhead.hull_shell(), cockpit_camera.global_position),
|
|
"pressure hull shell surrounds the pilot"
|
|
)
|
|
_expect(absf(detailing.battery_needle_degrees()) > 100.0, "analog battery gauge follows the dive snapshot")
|
|
_expect(detailing.textures_are_loaded(), "cockpit loads its dedicated analog gauge textures")
|
|
_expect(detailing.gauges_are_dashboard_mounted(), "analog gauges are mounted on the dashboard")
|
|
_expect(detailing.dashboard_control_count() >= 8, "dashboard exposes physical auxiliary controls")
|
|
_expect(detailing.side_equipment_count() >= 3, "cockpit sidewalls expose a gas bottle and junction boxes")
|
|
_expect(detailing.side_control_count() >= 3, "cockpit sidewalls expose a valve and pilot controls")
|
|
_expect(detailing.sonar_control_count() >= 4, "sonar console exposes physical lamps and gain control")
|
|
_expect(detailing.practical_light_count() == 2, "cockpit uses two caged practical lights")
|
|
_expect(pressure_bulkhead.panel_count() == 1, "porthole bulkhead uses one continuous curved shell")
|
|
_expect(pressure_bulkhead.rectangular_whitebox_is_hidden(), "porthole rectangular whitebox is hidden")
|
|
_expect(analog_gauges.gauge_count() == 4, "dashboard exposes four primary analog instruments")
|
|
_expect(alert_box.indicator_count() == 3, "alert box exposes three pilot lights")
|
|
_expect(alert_box.active_indicator_count() == 1, "exactly one pilot light reflects the alert state")
|
|
_expect(
|
|
alert_box.active_indicator_name() == "Lamp_Alerte",
|
|
"normal telemetry lights the green system lamp"
|
|
)
|
|
_expect(
|
|
analog_gauges.pilot_light_count() == 6 and analog_gauges.lit_pilot_light_count() == 0,
|
|
"gauge panel pilot lights rest dark without per-instrument data"
|
|
)
|
|
_expect(
|
|
switch_panel.control_count() == 10 and switch_panel.controls_are_at_rest(),
|
|
"decorative switches stay in their rest pose"
|
|
)
|
|
_expect(
|
|
_sonar_texture_matches_glass(instruments.sonar_texture(), sonar_screen),
|
|
"sonar render target has the aspect ratio of the monitor glass"
|
|
)
|
|
_expect("kWh" in instruments.resources_text() and "kW" in instruments.resources_text(), "battery instrument has exact units")
|
|
_expect("L/min" in instruments.resources_text(), "oxygen instrument has volume and consumption")
|
|
_expect("bar" in instruments.integrity_text(), "hull instrument has current and certified pressure")
|
|
_expect(not hud.alert_overlay_visible(), "normal telemetry stays on physical instruments only")
|
|
_expect(
|
|
instruments.sonar_texture() == sonar_display.sonar_texture(),
|
|
"physical and expanded sonar views share one texture"
|
|
)
|
|
_expect(sonar_display.idle_prompt_visible(), "idle sonar explains how to emit a scan")
|
|
|
|
adapter.sonar_scan_requested.emit()
|
|
var first_scan = sonar_service.current_scan()
|
|
_expect(first_scan != null, "manual sonar command publishes a typed scan")
|
|
_expect(first_scan.samples.observed_count() > 1000, "reference acquisition captures dense bathymetry")
|
|
_expect(first_scan.contacts.size() >= 1, "metal calibration target becomes a distinct contact")
|
|
_expect("RELEVÉ EN COURS" in sonar_display.status_text(), "sonar display reports progressive reveal")
|
|
_expect(not sonar_display.idle_prompt_visible(), "sonar data replaces the idle instruction")
|
|
for frame in 18:
|
|
await physics_frame
|
|
_expect(sonar_display.built_row_count() > 0, "renderer reveals near rows incrementally")
|
|
_expect(sonar_display.built_triangle_count() >= 90, "sonar renders a dense relief surface, not a line")
|
|
_expect(sonar_display.built_grid_segment_count() > 0, "sonar overlays a perspective grid on the relief")
|
|
var scale_texts: PackedStringArray = sonar_display.depth_scale_texts()
|
|
_expect(
|
|
scale_texts.size() >= 2
|
|
and scale_texts[0] == "%.0f m" % first_scan.maximum_elevation_m
|
|
and scale_texts[scale_texts.size() - 1] == "%.0f m" % first_scan.minimum_elevation_m,
|
|
"sonar height scale shows the real elevation span of the scan"
|
|
)
|
|
adapter.sonar_view_toggle_requested.emit()
|
|
_expect(sonar_display.is_expanded(), "sonar can expand without pausing the dive")
|
|
adapter.sonar_view_toggle_requested.emit()
|
|
_expect(not sonar_display.is_expanded(), "expanded sonar returns to cockpit view")
|
|
|
|
var start_position: Vector3 = actor.global_position
|
|
var starting_battery: float = composition.latest_update().snapshot.battery.remaining_kwh
|
|
Input.action_press(ProductionInputAdapter.ACTION_FORWARD)
|
|
for frame in 35:
|
|
await physics_frame
|
|
Input.action_release(ProductionInputAdapter.ACTION_FORWARD)
|
|
_expect(actor.global_position.distance_to(start_position) > 0.1, "keyboard propulsion moves the submarine")
|
|
_expect(actor.velocity.length() > 0.1, "propulsion builds inertial velocity")
|
|
_expect(
|
|
composition.latest_update().snapshot.battery.remaining_kwh < starting_battery,
|
|
"propulsion effort drains the domain battery"
|
|
)
|
|
|
|
var start_heading: float = actor.rotation.y
|
|
Input.action_press(ProductionInputAdapter.ACTION_RIGHT)
|
|
for frame in 24:
|
|
await physics_frame
|
|
Input.action_release(ProductionInputAdapter.ACTION_RIGHT)
|
|
_expect(absf(actor.rotation.y - start_heading) > 0.01, "keyboard steering changes submarine heading")
|
|
_expect(absf(actor.current_yaw_velocity_rps()) > 0.01, "yaw keeps angular inertia after release")
|
|
for frame in 90:
|
|
await physics_frame
|
|
|
|
var settled_heading: float = actor.rotation.y
|
|
var initial_look_yaw: float = camera.look_yaw_radians()
|
|
var cockpit_transform: Transform3D = instruments.global_transform
|
|
var mouse_motion := InputEventMouseMotion.new()
|
|
mouse_motion.relative = Vector2(40.0, -20.0)
|
|
adapter._unhandled_input(mouse_motion)
|
|
await physics_frame
|
|
_expect(absf(camera.look_yaw_radians() - initial_look_yaw) > 0.01, "mouse rotates the cockpit camera")
|
|
_expect(absf(actor.rotation.y - settled_heading) < 0.001, "mouse does not steer the submarine")
|
|
_expect(instruments.global_transform.is_equal_approx(cockpit_transform), "cockpit remains fixed while the player looks around")
|
|
|
|
var paused_sequence: int = composition.latest_update().snapshot.tick_sequence
|
|
var paused_sonar_progress: float = sonar_service.current_scan().reveal_progress
|
|
adapter.pause_requested.emit()
|
|
await process_frame
|
|
_expect(paused, "pause menu suspends the scene tree")
|
|
var pause_menu := composition.active_overlay() as PauseMenu
|
|
_expect(pause_menu != null, "pause overlay is displayed")
|
|
await process_frame
|
|
await process_frame
|
|
_expect(
|
|
composition.latest_update().snapshot.tick_sequence == paused_sequence,
|
|
"dive ticks are frozen while paused"
|
|
)
|
|
_expect(
|
|
is_equal_approx(sonar_service.current_scan().reveal_progress, paused_sonar_progress),
|
|
"sonar reveal is frozen while paused"
|
|
)
|
|
|
|
var controls_button := pause_menu.get_node("%ControlsButton") as Button
|
|
controls_button.pressed.emit()
|
|
await process_frame
|
|
var controls := composition.active_overlay() as ControlsSettingsScreen
|
|
_expect(controls != null, "controls settings open over the paused dive")
|
|
_expect(adapter.binding_snapshots().size() == 11, "all gameplay keyboard bindings are exposed")
|
|
|
|
var rebind_results: Array = []
|
|
adapter.rebind_result_available.connect(
|
|
func(result) -> void: rebind_results.append(result)
|
|
)
|
|
adapter.begin_rebind(ProductionInputAdapter.ACTION_FORWARD)
|
|
adapter._unhandled_input(_pressed_key(KEY_UP))
|
|
_expect(not rebind_results.is_empty() and rebind_results.back().accepted, "free key remapping is accepted")
|
|
_expect(
|
|
_binding_for(adapter, ProductionInputAdapter.ACTION_FORWARD) == "Up",
|
|
"binding snapshot reflects the remapped key"
|
|
)
|
|
|
|
adapter.begin_rebind(ProductionInputAdapter.ACTION_BACKWARD)
|
|
adapter._unhandled_input(_pressed_key(KEY_UP))
|
|
_expect(
|
|
not rebind_results.back().accepted
|
|
and rebind_results.back().conflicting_action_id == ProductionInputAdapter.ACTION_FORWARD,
|
|
"binding conflict identifies the existing action"
|
|
)
|
|
var swap_result = adapter.resolve_pending_conflict(true)
|
|
_expect(swap_result.accepted, "conflicting bindings can be swapped")
|
|
_expect(
|
|
_binding_for(adapter, ProductionInputAdapter.ACTION_BACKWARD) == "Up",
|
|
"requested action receives the conflicting key"
|
|
)
|
|
adapter.restore_default_bindings()
|
|
_expect(
|
|
_binding_for(adapter, ProductionInputAdapter.ACTION_FORWARD) == "W",
|
|
"default bindings can be restored"
|
|
)
|
|
|
|
adapter.pause_requested.emit()
|
|
await process_frame
|
|
_expect(composition.active_overlay() is PauseMenu, "escape from settings returns to pause")
|
|
adapter.pause_requested.emit()
|
|
await process_frame
|
|
_expect(not paused, "second escape resumes the dive")
|
|
for frame in 3:
|
|
await physics_frame
|
|
if composition.latest_update().snapshot.tick_sequence > paused_sequence:
|
|
break
|
|
_expect(
|
|
composition.latest_update().snapshot.tick_sequence > paused_sequence,
|
|
"fixed dive ticks resume after pause"
|
|
)
|
|
|
|
analog_gauges.present(0.0, 0.5, 1.0, 0.25)
|
|
_expect(
|
|
is_equal_approx(analog_gauges.needle_degrees("Needle_Oxygen"), 120.0),
|
|
"empty gauge needle rests at the lower-left end of the printed scale"
|
|
)
|
|
_expect(
|
|
is_zero_approx(analog_gauges.needle_degrees("Needle_Battery")),
|
|
"half-scale gauge needle points straight up"
|
|
)
|
|
_expect(
|
|
is_equal_approx(analog_gauges.needle_degrees("Needle_Hull"), -120.0),
|
|
"full gauge needle reaches the lower-right end clockwise"
|
|
)
|
|
_expect(
|
|
is_equal_approx(analog_gauges.needle_degrees("Needle_Depth"), 60.0),
|
|
"depth needle uses the same printed sweep"
|
|
)
|
|
_expect(
|
|
_alert_lamp_for(alert_box, DiveAlertLevel.Value.NORMAL) == "Lamp_Alerte",
|
|
"normal level lights only the system lamp"
|
|
)
|
|
_expect(
|
|
_alert_lamp_for(alert_box, DiveAlertLevel.Value.ATTENTION) == "Lamp_Attention"
|
|
and _alert_lamp_for(alert_box, DiveAlertLevel.Value.RETURN) == "Lamp_Attention",
|
|
"attention and return levels light only the attention lamp"
|
|
)
|
|
_expect(
|
|
_alert_lamp_for(alert_box, DiveAlertLevel.Value.CRITICAL) == "Lamp_Danger"
|
|
and _alert_lamp_for(alert_box, DiveAlertLevel.Value.FAILURE) == "Lamp_Danger",
|
|
"critical and failure levels light only the danger lamp"
|
|
)
|
|
|
|
|
|
func _pressed_key(keycode: Key) -> InputEventKey:
|
|
var event := InputEventKey.new()
|
|
event.physical_keycode = keycode
|
|
event.pressed = true
|
|
return event
|
|
|
|
|
|
func _sonar_screen_faces_camera(
|
|
sonar_screen: MeshInstance3D,
|
|
cockpit_camera: Camera3D
|
|
) -> bool:
|
|
if sonar_screen == null or cockpit_camera == null:
|
|
return false
|
|
var direction_to_camera := (
|
|
cockpit_camera.global_position - sonar_screen.global_position
|
|
).normalized()
|
|
return sonar_screen.global_basis.z.normalized().dot(direction_to_camera) > 0.85
|
|
|
|
|
|
func _sonar_screen_is_ahead_of_bank(
|
|
sonar_screen: MeshInstance3D,
|
|
electrical_bank: Node3D
|
|
) -> bool:
|
|
if sonar_screen == null or electrical_bank == null:
|
|
return false
|
|
for child: Node in electrical_bank.get_children():
|
|
if child is MeshInstance3D and child.name.begins_with("JunctionBox"):
|
|
if sonar_screen.global_position.z <= (child as MeshInstance3D).global_position.z:
|
|
return false
|
|
return true
|
|
|
|
|
|
## The imported housing is an arbitrary mesh, so the clearance is measured on
|
|
## its triangles: looking straight at the glass, nothing of the housing may sit
|
|
## level with or in front of the picture.
|
|
func _sonar_screen_clears_housing(
|
|
sonar_screen: MeshInstance3D,
|
|
sonar_housing: MeshInstance3D
|
|
) -> bool:
|
|
if sonar_screen == null or sonar_housing == null or sonar_housing.mesh == null:
|
|
return false
|
|
var glass := sonar_screen.mesh.get_aabb()
|
|
var screen_to_housing := (
|
|
sonar_housing.global_transform.affine_inverse() * sonar_screen.global_transform
|
|
)
|
|
var outward := (screen_to_housing.basis * Vector3.BACK).normalized()
|
|
var triangles := sonar_housing.mesh.get_faces()
|
|
if triangles.is_empty():
|
|
return false
|
|
for column in 5:
|
|
for row in 3:
|
|
var on_glass := Vector3(
|
|
lerpf(glass.position.x, glass.end.x, 0.1 + 0.2 * column),
|
|
lerpf(glass.position.y, glass.end.y, 0.1 + 0.4 * row),
|
|
glass.position.z - 0.005
|
|
)
|
|
var origin := screen_to_housing * on_glass
|
|
for index in range(0, triangles.size(), 3):
|
|
if Geometry3D.ray_intersects_triangle(
|
|
origin,
|
|
outward,
|
|
triangles[index],
|
|
triangles[index + 1],
|
|
triangles[index + 2]
|
|
) != null:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _shell_surrounds(shell: MeshInstance3D, point: Vector3) -> bool:
|
|
if shell == null or shell.mesh == null:
|
|
return false
|
|
var local := shell.global_transform.affine_inverse() * point
|
|
return shell.mesh.get_aabb().grow(-0.05).has_point(local)
|
|
|
|
|
|
func _sonar_texture_matches_glass(texture: Texture2D, sonar_screen: MeshInstance3D) -> bool:
|
|
if texture == null or sonar_screen == null:
|
|
return false
|
|
var glass := sonar_screen.mesh.get_aabb().size
|
|
var texture_ratio := float(texture.get_width()) / float(texture.get_height())
|
|
return absf(texture_ratio - glass.x / glass.y) < 0.01
|
|
|
|
|
|
func _alert_lamp_for(alert_box: CockpitAlertBox, level: DiveAlertLevel.Value) -> String:
|
|
alert_box.present(level)
|
|
return alert_box.active_indicator_name() if alert_box.active_indicator_count() == 1 else ""
|
|
|
|
|
|
func _binding_for(adapter: ProductionInputAdapter, action_id: StringName) -> String:
|
|
for binding in adapter.binding_snapshots():
|
|
if binding.action_id == action_id:
|
|
return binding.binding_text
|
|
return ""
|
|
|
|
|
|
func _expect(condition: bool, label: String) -> void:
|
|
if not condition:
|
|
_failures.append(label)
|