Files
room_glitch/editor/workspace/escape_room_workspace.gd
Jeff Luke c1b2195834 Expand Glitch operations hub, cabinet authoring, deployment, and RFID support
Operator hub
- Introduce Escape Gallery branding and Glitch: the Dark Machine room views
- Add cabinet illustrations, Pac-Man marquee/gameplay artwork, and waiting states
- Add device feedback panels with RFID readings, input/output states, and stale status
- Add logged operator hardware interventions with request deduplication

Room and cabinet authoring
- Add a visual custom-PCB connection editor with per-cabinet device inventory
- Support device IDs, bindings, wiring notes, input polarity, and validation
- Add physical-room label/dimension toggles and editable doorway annotations
- Preserve doorway changes through undo, save, and revert
- Expand cabinet profiles, room layout, and Glitch scenario resources

Scenarios and gameplay
- Add per-assignment game configuration and runtime feature validation
- Add timed test wins and Frogger individual-home objectives
- Add the three-cabinet timed/score/Frogger test scenario
- Add Cabinet 1 Pac-Man RFID credits, credit sound, and CREDIT 0/1 display
- Require physical Start after an accepted scan and prevent duplicate credits

Pi hardware and deployment
- Add GPIO feedback sampling and a separate multiplexed RFID helper
- Validate reader pin ownership and input/output conflicts
- Add SSH cabinet startup and Pi desktop-session service support
- Add master packaging, site setup, SSH enrollment, and staged cabinet releases
- Verify deployment manifests and support explicit activation/status/log commands

Documentation and verification
- Consolidate the root README around the current architecture and setup workflow
- Add the complete master/Pi/RFID export and deployment walkthrough
- Document network isolation, credentials, card identification, and rollback
- Add PCB, room-editor, feedback, credit, deployment, and controller tests
- Isolate protocol tests from designer-edited cabinet configurations

Known limitations
- RFID helper compilation and physical Pi commissioning remain unverified
- Fleet provisioning does not yet configure RFID-only installations completely
- Broader Pac-Man tests retain a ghost-capture assertion failure
- Staged exports must be rebuilt and checked before deploying runtime 2.3
2026-10-17 14:53:54 -07:00

3083 lines
52 KiB
GDScript

@tool
class_name EscapeRoomWorkspace
extends Control
signal scenario_selected(
scenario: ScenarioDefinition
)
@onready var room_simulator: RoomSimulator = (
$RoomSimulator
)
@onready var scenario_select: OptionButton = (
$VBoxContainer/Toolbar/ScenarioSelect
)
@onready var status_label: Label = (
$VBoxContainer/Toolbar/StatusLabel
)
@onready var room_layout: RoomLayout = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
RoomLayoutPanel/RoomLayout
)
@onready var logic_graph: LogicGraph = (
$VBoxContainer/MainViews/
Logic/VBoxContainer/LogicGraph
)
@onready var test_cabinet_select: OptionButton = (
$VBoxContainer/
SimulationBar/TestCabinetSelect
)
@onready var test_event_select: OptionButton = (
$VBoxContainer/
SimulationBar/TestEventSelect
)
@onready var test_event_button: Button = (
$VBoxContainer/
SimulationBar/TestEventButton
)
@onready var selected_label: Label = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/SelectedLabel
)
@onready var x_spin_box: SpinBox = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/XContainer/XSpinBox
)
@onready var y_spin_box: SpinBox = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/YContainer/YSpinBox
)
@onready var rotation_spin_box: SpinBox = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/RotationContainer/RotationSpinBox
)
@onready var snap_check_box: CheckBox = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/SnapCheckBox
)
@onready var grid_spin_box: SpinBox = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/GridContainer/GridSpinBox
)
@onready var room_dirty_label: Label = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/DirtyLabel
)
@onready var scenario_dirty_label: Label = (
$VBoxContainer/MainViews/
Logic/VBoxContainer/
EditorBar/ScenarioDirtyLabel
)
@onready var show_connections_check_box: CheckBox = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/ShowConnectionsCheckBox
)
@onready var edit_boundary_button: Button = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/
PhysicalToolbar/EditBoundaryButton
)
@onready var select_tool_button: Button = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/
PhysicalToolbar/SelectButton
)
@onready var draw_wall_button: Button = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/
PhysicalToolbar/DrawWallButton
)
@onready var add_boundary_point_button: Button = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/
PhysicalToolbar/AddBoundaryPointButton
)
@onready var delete_boundary_point_button: Button = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/
PhysicalToolbar/DeleteBoundaryPointButton
)
@onready var focus_room_button: Button = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/
PhysicalToolbar/FocusRoomButton
)
@onready var reset_view_button: Button = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/
PhysicalToolbar/ResetViewButton
)
@onready var zoom_label: Label = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/
PhysicalToolbar/ZoomLabel
)
@onready var wall_start_x_spin_box: SpinBox = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/WallStartXContainer/WallStartXSpinBox
)
@onready var wall_start_y_spin_box: SpinBox = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/WallStartYContainer/WallStartYSpinBox
)
@onready var wall_end_x_spin_box: SpinBox = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/WallEndXContainer/WallEndXSpinBox
)
@onready var wall_end_y_spin_box: SpinBox = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/WallEndYContainer/WallEndYSpinBox
)
@onready var wall_thickness_spin_box: SpinBox = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/WallThicknessContainer/WallThicknessSpinBox
)
@onready var delete_wall_button: Button = (
$VBoxContainer/MainViews/
Physical/VBoxContainer/LayoutWorkspace/
PropertyPanel/MarginContainer/
VBoxContainer/DeleteWallButton
)
@onready var main_views: TabContainer = (
$VBoxContainer/MainViews
)
@onready
var simulation_timeline: SimulationTimeline = (
$VBoxContainer/SimulationTimeline
)
# Physical controls use feet; model values and UndoRedo remain in meters.
const FEET_TO_METERS: float = 0.3048
const METERS_TO_FEET: float = 1.0 / FEET_TO_METERS
const TEST_SEQUENCE_DIRECTORY := (
"res://data/test_sequences"
)
var test_sequences: Array[TestSequence] = []
var selected_test_sequence_index: int = -1
var undo_redo: EditorUndoRedoManager
var test_cabinet_ids: Array[StringName] = []
var test_event_ids: Array[StringName] = []
var context: EscapeRoomEditorContext
var scenario_ids: Array[StringName] = []
var simulation_driver := EditorSimulationDriver.new()
var editor_selection := EscapeRoomEditorSelection.new()
var doorway_tool: Button
var doorway_check: CheckBox
var swing_check: CheckBox
func _ready() -> void:
_setup_view_controls()
select_tool_button.button_pressed = true
room_layout.set_edit_mode(
RoomLayout.EditMode.SELECT
)
if not logic_graph.scenario_mapping_changed.is_connected(
_on_logic_mapping_changed
):
logic_graph.scenario_mapping_changed.connect(
_on_logic_mapping_changed
)
if not logic_graph.edit_rejected.is_connected(
_on_logic_edit_rejected
):
logic_graph.edit_rejected.connect(
_on_logic_edit_rejected
)
if not room_layout.layout_changed.is_connected(
_on_room_layout_changed
):
room_layout.layout_changed.connect(
_on_room_layout_changed
)
if not simulation_driver.action_activated.is_connected(
_on_editor_action_activated
):
simulation_driver.action_activated.connect(
_on_editor_action_activated
)
if not simulation_driver.action_deactivated.is_connected(
_on_editor_action_deactivated
):
simulation_driver.action_deactivated.connect(
_on_editor_action_deactivated
)
if not room_layout.item_selected.is_connected(
_on_room_layout_item_selected
):
room_layout.item_selected.connect(
_on_room_layout_item_selected
)
wall_thickness_spin_box.min_value = 0.01
wall_thickness_spin_box.max_value = 4.0
wall_thickness_spin_box.step = 0.01
wall_thickness_spin_box.suffix = " ft"
if not x_spin_box.value_changed.is_connected(
_on_x_spin_box_value_changed
):
x_spin_box.value_changed.connect(
_on_x_spin_box_value_changed
)
if not y_spin_box.value_changed.is_connected(
_on_y_spin_box_value_changed
):
y_spin_box.value_changed.connect(
_on_y_spin_box_value_changed
)
if not rotation_spin_box.value_changed.is_connected(
_on_rotation_spin_box_value_changed
):
rotation_spin_box.value_changed.connect(
_on_rotation_spin_box_value_changed
)
snap_check_box.set_pressed_no_signal(
room_layout.snap_enabled
)
grid_spin_box.set_value_no_signal(
room_layout.grid_size_meters * METERS_TO_FEET
)
if not snap_check_box.toggled.is_connected(
_on_snap_check_box_toggled
):
snap_check_box.toggled.connect(
_on_snap_check_box_toggled
)
if not grid_spin_box.value_changed.is_connected(
_on_grid_spin_box_value_changed
):
grid_spin_box.value_changed.connect(
_on_grid_spin_box_value_changed
)
show_connections_check_box.set_pressed_no_signal(
room_layout.show_connections
)
if not show_connections_check_box.toggled.is_connected(
_on_show_connections_check_box_toggled
):
show_connections_check_box.toggled.connect(
_on_show_connections_check_box_toggled
)
if not test_cabinet_select.item_selected.is_connected(
_on_test_cabinet_select_item_selected
):
test_cabinet_select.item_selected.connect(
_on_test_cabinet_select_item_selected
)
if not test_event_button.pressed.is_connected(
_on_test_event_button_pressed
):
test_event_button.pressed.connect(
_on_test_event_button_pressed
)
if not room_layout.wall_creation_requested.is_connected(
_on_wall_creation_requested
):
room_layout.wall_creation_requested.connect(
_on_wall_creation_requested
)
if not room_layout.wall_selected.is_connected(
_on_room_wall_selected
):
room_layout.wall_selected.connect(
_on_room_wall_selected
)
if not room_layout.wall_geometry_edit_requested.is_connected(
_on_wall_geometry_edit_requested
):
room_layout.wall_geometry_edit_requested.connect(
_on_wall_geometry_edit_requested
)
if not editor_selection.changed.is_connected(
_on_editor_selection_changed
):
editor_selection.changed.connect(
_on_editor_selection_changed
)
if not scenario_select.item_selected.is_connected(
_on_scenario_select_item_selected
):
scenario_select.item_selected.connect(
_on_scenario_select_item_selected
)
if not edit_boundary_button.pressed.is_connected(
_on_edit_boundary_button_pressed
):
edit_boundary_button.pressed.connect(
_on_edit_boundary_button_pressed
)
if not room_layout.boundary_geometry_edit_requested.is_connected(
_on_boundary_geometry_edit_requested
):
room_layout.boundary_geometry_edit_requested.connect(
_on_boundary_geometry_edit_requested
)
if not focus_room_button.pressed.is_connected(
_on_focus_room_button_pressed
):
focus_room_button.pressed.connect(
_on_focus_room_button_pressed
)
if not add_boundary_point_button.pressed.is_connected(
_on_add_boundary_point_button_pressed
):
add_boundary_point_button.pressed.connect(
_on_add_boundary_point_button_pressed
)
if not delete_boundary_point_button.pressed.is_connected(
_on_delete_boundary_point_button_pressed
):
delete_boundary_point_button.pressed.connect(
_on_delete_boundary_point_button_pressed
)
if not room_layout.edit_rejected.is_connected(
_on_room_layout_edit_rejected
):
room_layout.edit_rejected.connect(
_on_room_layout_edit_rejected
)
if not reset_view_button.pressed.is_connected(
_on_reset_view_button_pressed
):
reset_view_button.pressed.connect(
_on_reset_view_button_pressed
)
if not room_layout.zoom_changed.is_connected(
_on_room_layout_zoom_changed
):
room_layout.zoom_changed.connect(
_on_room_layout_zoom_changed
)
_on_room_layout_zoom_changed(
room_layout.zoom
)
if not room_layout.item_position_edit_requested.is_connected(
_on_room_layout_item_position_edit_requested
):
room_layout.item_position_edit_requested.connect(
_on_room_layout_item_position_edit_requested
)
if not simulation_driver.trace_entry_created.is_connected(
_on_simulation_trace_entry_created
):
simulation_driver.trace_entry_created.connect(
_on_simulation_trace_entry_created
)
if not simulation_timeline.pause_requested.is_connected(
_on_simulation_pause_requested
):
simulation_timeline.pause_requested.connect(
_on_simulation_pause_requested
)
if not simulation_timeline.resume_requested.is_connected(
_on_simulation_resume_requested
):
simulation_timeline.resume_requested.connect(
_on_simulation_resume_requested
)
if not simulation_timeline.step_requested.is_connected(
_on_simulation_step_requested
):
simulation_timeline.step_requested.connect(
_on_simulation_step_requested
)
if not simulation_timeline.reset_requested.is_connected(
_on_simulation_reset_requested
):
simulation_timeline.reset_requested.connect(
_on_simulation_reset_requested
)
if not simulation_timeline.sequence_selected.is_connected(
_on_test_sequence_selected
):
simulation_timeline.sequence_selected.connect(
_on_test_sequence_selected
)
if not simulation_timeline.run_sequence_requested.is_connected(
_on_run_test_sequence_requested
):
simulation_timeline.run_sequence_requested.connect(
_on_run_test_sequence_requested
)
if not simulation_timeline.stop_sequence_requested.is_connected(
_on_stop_test_sequence_requested
):
simulation_timeline.stop_sequence_requested.connect(
_on_stop_test_sequence_requested
)
if not simulation_timeline.replay_sequence_requested.is_connected(
_on_replay_test_sequence_requested
):
simulation_timeline.replay_sequence_requested.connect(
_on_replay_test_sequence_requested
)
if not simulation_driver.sequence_started.is_connected(
_on_test_sequence_started
):
simulation_driver.sequence_started.connect(
_on_test_sequence_started
)
if not simulation_driver.sequence_finished.is_connected(
_on_test_sequence_finished
):
simulation_driver.sequence_finished.connect(
_on_test_sequence_finished
)
if not simulation_driver.sequence_stopped.is_connected(
_on_test_sequence_stopped
):
simulation_driver.sequence_stopped.connect(
_on_test_sequence_stopped
)
if not room_layout.refresh_requested.is_connected(
_on_room_layout_refresh_requested
):
room_layout.refresh_requested.connect(
_on_room_layout_refresh_requested
)
if not room_layout.binding_route_edit_requested.is_connected(
_on_room_layout_binding_route_edit_requested
):
room_layout.binding_route_edit_requested.connect(
_on_room_layout_binding_route_edit_requested
)
_load_test_sequences()
func _process(
delta: float
) -> void:
if simulation_driver == null:
return
simulation_driver.advance(
delta
)
func configure(
project_database: ProjectDatabase,
room: RoomDefinition
) -> bool:
context = EscapeRoomEditorContext.new()
context.room_changed.connect(
_on_context_room_changed
)
context.scenario_changed.connect(
_on_context_scenario_changed
)
context.room_edit_session.dirty_changed.connect(
_on_room_dirty_changed
)
context.scenario_edit_session.dirty_changed.connect(
_on_scenario_dirty_changed
)
#
# Connect simulator output before configuring the context,
# because context.configure() may immediately emit room_changed.
#
_connect_simulation_signals()
if not context.configure(
project_database,
room
):
return false
#
# Opening the first scenario will trigger
# _on_context_scenario_changed(), which configures
# the simulation driver, layout, and graph.
#
_populate_scenarios()
return true
func _populate_scenarios() -> void:
scenario_select.clear()
scenario_ids.clear()
if context == null:
return
for scenario in context.database.scenarios:
if scenario == null:
continue
scenario_select.add_item(
scenario.display_name
)
scenario_ids.append(
scenario.scenario_id
)
if scenario_ids.is_empty():
status_label.text = "No scenarios"
return
scenario_select.set_block_signals(
true
)
scenario_select.select(
0
)
scenario_select.set_block_signals(
false
)
_open_selected_scenario()
func _open_selected_scenario() -> void:
var index := scenario_select.selected
if (
index < 0
or index >= scenario_ids.size()
):
return
var scenario := (
context.database.get_scenario(
scenario_ids[index]
)
)
if scenario == null:
return
if context.open_scenario(
scenario
):
scenario_selected.emit(
context.get_scenario()
)
func _on_scenario_select_item_selected(
_index: int
) -> void:
_open_selected_scenario()
func _on_context_scenario_changed(
scenario: ScenarioDefinition
) -> void:
if scenario == null:
return
var room := context.get_room()
if simulation_timeline != null:
simulation_timeline.clear()
simulation_driver.configure(
room,
scenario
)
room_layout.configure(
room,
scenario
)
_clear_property_panel()
logic_graph.configure(
room,
scenario
)
status_label.text = (
"%s v%d"
% [
scenario.display_name,
scenario.version
]
)
_populate_test_cabinets()
print(
"[SIM UI] cabinets=%d events=%d"
% [
test_cabinet_select.item_count,
test_event_select.item_count
]
)
func _on_simulation_trace_entry_created(
entry: Dictionary
) -> void:
if simulation_timeline == null:
return
simulation_timeline.add_trace_entry(
entry
)
func _on_context_room_changed(
room: RoomDefinition
) -> void:
if room == null:
return
room_simulator.configure(
room
)
var scenario := context.get_scenario()
simulation_driver.configure(
room,
scenario
)
if scenario != null:
room_layout.configure(
room,
scenario
)
_clear_property_panel()
logic_graph.configure(
room,
scenario
)
func _on_logic_mapping_changed() -> void:
if context == null:
return
context.scenario_edit_session.mark_dirty()
validate_setup()
func _on_room_layout_changed() -> void:
if context == null:
return
context.room_edit_session.mark_dirty()
_refresh_selected_property_values()
func _on_logic_edit_rejected(
message: String
) -> void:
status_label.text = (
"Edit rejected: %s"
% message
)
func validate_setup() -> ValidationResult:
if context == null:
var result := ValidationResult.new()
result.add_error(
"Editor workspace is not configured."
)
return result
if context.get_room() == null:
var result := ValidationResult.new()
result.add_error(
"No room is currently open."
)
_display_validation_result(
result
)
return result
if context.get_scenario() == null:
var result := ValidationResult.new()
result.add_error(
"No scenario is currently open."
)
_display_validation_result(
result
)
return result
var result := (
EscapeRoomSetupValidator.validate(
context.get_room(),
context.get_scenario(),
context.database
)
)
_display_validation_result(
result
)
return result
func _display_validation_result(
result: ValidationResult
) -> void:
if result.is_valid():
status_label.text = (
"✓ " + result.get_summary()
)
else:
status_label.text = (
"⚠ " + result.get_summary()
)
for error in result.errors:
print(
"[EDITOR VALIDATION] ERROR: ",
error
)
for warning in result.warnings:
print(
"[EDITOR VALIDATION] WARNING: ",
warning
)
func _on_save_room_button_pressed() -> void:
if context == null:
return
if context.save_room():
if Engine.is_editor_hint():
EditorInterface.get_resource_filesystem().scan()
status_label.text = (
"Room saved."
)
func _on_validate_button_pressed() -> void:
validate_setup()
func _on_revert_room_button_pressed() -> void:
if context == null:
return
context.revert_room()
func _on_revert_scenario_button_pressed() -> void:
if context == null:
return
context.revert_scenario()
func _on_save_scenario_button_pressed() -> void:
if context == null:
return
if context.save_scenario():
if Engine.is_editor_hint():
EditorInterface.get_resource_filesystem().scan()
status_label.text = (
"Scenario saved."
)
func _on_validate_scenario_button_pressed() -> void:
validate_setup()
func _on_room_dirty_changed(
dirty: bool
) -> void:
if dirty:
room_dirty_label.text = (
"● UNSAVED ROOM CHANGES"
)
else:
room_dirty_label.text = (
"✓ ROOM SAVED"
)
func _on_scenario_dirty_changed(
dirty: bool
) -> void:
if dirty:
scenario_dirty_label.text = (
"● UNSAVED SCENARIO CHANGES"
)
else:
scenario_dirty_label.text = (
"✓ SCENARIO SAVED"
)
func _connect_simulation_signals() -> void:
if not room_simulator.device_state_changed.is_connected(
_on_device_state_changed
):
room_simulator.device_state_changed.connect(
_on_device_state_changed
)
if not room_simulator.binding_activated.is_connected(
_on_binding_activated
):
room_simulator.binding_activated.connect(
_on_binding_activated
)
if not room_simulator.binding_deactivated.is_connected(
_on_binding_deactivated
):
room_simulator.binding_deactivated.connect(
_on_binding_deactivated
)
if not room_simulator.path_activated.is_connected(
_on_path_activated
):
room_simulator.path_activated.connect(
_on_path_activated
)
if not room_simulator.path_deactivated.is_connected(
_on_path_deactivated
):
room_simulator.path_deactivated.connect(
_on_path_deactivated
)
if not logic_graph.logic_path_selected.is_connected(
_on_logic_path_selected
):
logic_graph.logic_path_selected.connect(
_on_logic_path_selected
)
func _on_device_state_changed(
device_id: StringName,
active: bool
) -> void:
room_layout.set_device_active(
device_id,
active
)
func _on_binding_activated(
cabinet_id: StringName,
action_id: StringName,
device_id: StringName
) -> void:
room_layout.set_binding_active(
cabinet_id,
action_id,
device_id,
true
)
func _on_binding_deactivated(
cabinet_id: StringName,
action_id: StringName,
device_id: StringName
) -> void:
room_layout.set_binding_active(
cabinet_id,
action_id,
device_id,
false
)
func _on_path_activated(
_path_id: StringName,
cabinet_id: StringName,
event_id: StringName,
action_id: StringName,
device_id: StringName
) -> void:
print(
"[EDITOR PATH] %s :: %s -> %s -> %s"
% [
cabinet_id,
event_id,
action_id,
device_id
]
)
logic_graph.set_path_active(
cabinet_id,
event_id,
action_id,
device_id,
true
)
func _on_path_deactivated(
_path_id: StringName,
cabinet_id: StringName,
event_id: StringName,
action_id: StringName,
device_id: StringName
) -> void:
logic_graph.set_path_active(
cabinet_id,
event_id,
action_id,
device_id,
false
)
func simulate_game_event(
cabinet_id: StringName,
event_id: StringName
) -> void:
simulation_driver.simulate_game_event(
cabinet_id,
event_id
)
func _on_editor_action_activated(
cabinet_id: StringName,
action_id: StringName,
context_data: Dictionary
) -> void:
room_simulator.simulate_action_activated(
cabinet_id,
action_id,
context_data
)
func _on_editor_action_deactivated(
cabinet_id: StringName,
action_id: StringName,
context_data: Dictionary
) -> void:
room_simulator.simulate_action_deactivated(
cabinet_id,
action_id,
context_data
)
func _populate_test_cabinets() -> void:
test_cabinet_select.clear()
test_cabinet_ids.clear()
var scenario := context.get_scenario()
if scenario == null:
return
for assignment in scenario.assignments:
if assignment == null:
continue
if assignment.cabinet == null:
continue
var label := String(
assignment.cabinet.display_name
)
if label.is_empty():
label = String(
assignment.cabinet.cabinet_id
)
test_cabinet_select.add_item(
label
)
test_cabinet_ids.append(
assignment.cabinet.cabinet_id
)
if test_cabinet_ids.is_empty():
test_event_select.clear()
test_event_ids.clear()
return
test_cabinet_select.set_block_signals(
true
)
test_cabinet_select.select(
0
)
test_cabinet_select.set_block_signals(
false
)
_populate_test_events(
0
)
func _populate_test_events(
cabinet_index: int
) -> void:
test_event_select.clear()
test_event_ids.clear()
if (
cabinet_index < 0
or cabinet_index >= test_cabinet_ids.size()
):
return
var scenario := context.get_scenario()
if scenario == null:
return
var cabinet_id := (
test_cabinet_ids[
cabinet_index
]
)
var assignment := (
scenario.get_assignment_for_cabinet(
cabinet_id
)
)
if assignment == null:
return
if assignment.game == null:
return
for event_definition in assignment.game.events:
if event_definition == null:
continue
var label := String(
event_definition.display_name
)
if label.is_empty():
label = String(
event_definition.event_id
)
test_event_select.add_item(
label
)
test_event_ids.append(
event_definition.event_id
)
if not test_event_ids.is_empty():
test_event_select.set_block_signals(
true
)
test_event_select.select(
0
)
test_event_select.set_block_signals(
false
)
func _on_test_cabinet_select_item_selected(
index: int
) -> void:
_populate_test_events(
index
)
func _on_test_event_button_pressed() -> void:
var cabinet_index := (
test_cabinet_select.selected
)
var event_index := (
test_event_select.selected
)
#
# The editor can temporarily report -1 even when
# an OptionButton contains entries. Fall back to
# the first available item.
#
if (
cabinet_index < 0
and not test_cabinet_ids.is_empty()
):
cabinet_index = 0
if (
event_index < 0
and not test_event_ids.is_empty()
):
event_index = 0
if (
cabinet_index < 0
or cabinet_index >= test_cabinet_ids.size()
):
return
if (
event_index < 0
or event_index >= test_event_ids.size()
):
return
var cabinet_id := (
test_cabinet_ids[
cabinet_index
]
)
var event_id := (
test_event_ids[
event_index
]
)
print(
"[EDITOR SIMULATION] %s -> %s"
% [
cabinet_id,
event_id
]
)
simulate_game_event(
cabinet_id,
event_id
)
func _on_room_layout_item_selected(
item_id: StringName
) -> void:
_refresh_property_panel(
item_id
)
if room_layout.selected_item_is_cabinet():
editor_selection.select_cabinet(
item_id
)
else:
editor_selection.select_device(
item_id
)
func _refresh_property_panel(
item_id: StringName
) -> void:
if context == null:
return
var room := context.get_room()
if room == null:
return
var position := (
room_layout.get_selected_position()
)
x_spin_box.set_value_no_signal(
position.x * METERS_TO_FEET
)
y_spin_box.set_value_no_signal(
position.y * METERS_TO_FEET
)
if room_layout.selected_item_is_cabinet():
var placement := (
room.get_cabinet_placement(
item_id
)
)
selected_label.text = (
"Cabinet: %s"
% item_id
)
rotation_spin_box.editable = true
if placement != null:
rotation_spin_box.set_value_no_signal(
placement.rotation_degrees
)
else:
var device := (
room.get_device(
item_id
)
)
if device != null:
selected_label.text = (
"Device: %s"
% device.display_name
)
else:
selected_label.text = (
"Device: %s"
% item_id
)
#
# Room devices currently have position,
# but no rotation property.
#
rotation_spin_box.set_value_no_signal(
0.0
)
rotation_spin_box.editable = false
func _on_x_spin_box_value_changed(
value_ft: float
) -> void:
if room_layout.selected_item_id == &"":
return
var old_position := (
room_layout.get_selected_position()
)
var requested_position := Vector2(
value_ft * FEET_TO_METERS,
old_position.y
)
var new_position := (
room_layout.snap_item_position_for_editor(
room_layout.selected_item_id,
requested_position
)
)
_commit_position_change(
old_position,
new_position
)
func _on_y_spin_box_value_changed(
value_ft: float
) -> void:
if room_layout.selected_item_id == &"":
return
var old_position := (
room_layout.get_selected_position()
)
var requested_position := Vector2(
old_position.x,
value_ft * FEET_TO_METERS
)
var new_position := (
room_layout.snap_item_position_for_editor(
room_layout.selected_item_id,
requested_position
)
)
_commit_position_change(
old_position,
new_position
)
func _on_rotation_spin_box_value_changed(
value: float
) -> void:
if room_layout.selected_item_id == &"":
return
if not room_layout.selected_item_is_cabinet():
return
var old_rotation := (
room_layout.get_selected_rotation()
)
_commit_rotation_change(
old_rotation,
value
)
func _refresh_selected_property_values() -> void:
if room_layout.selected_item_id == &"":
return
var position := (
room_layout.get_selected_position()
)
x_spin_box.set_value_no_signal(
position.x * METERS_TO_FEET
)
y_spin_box.set_value_no_signal(
position.y * METERS_TO_FEET
)
rotation_spin_box.set_value_no_signal(
room_layout.get_selected_rotation()
)
func _on_snap_check_box_toggled(
enabled: bool
) -> void:
room_layout.snap_enabled = enabled
func _on_grid_spin_box_value_changed(
value_ft: float
) -> void:
room_layout.grid_size_meters = max(
value_ft * FEET_TO_METERS,
0.01
)
room_layout.queue_redraw()
func _on_show_connections_check_box_toggled(
enabled: bool
) -> void:
room_layout.set_connections_visible(
enabled
)
func _clear_property_panel() -> void:
if doorway_check != null:
doorway_check.disabled = true
swing_check.disabled = true
selected_label.text = (
"Nothing selected"
)
x_spin_box.set_value_no_signal(
0.0
)
y_spin_box.set_value_no_signal(
0.0
)
rotation_spin_box.set_value_no_signal(
0.0
)
rotation_spin_box.editable = false
func set_undo_redo_manager(
manager: EditorUndoRedoManager
) -> void:
undo_redo = manager
func _apply_selected_position(
position: Vector2
) -> void:
room_layout.set_selected_position(
position
)
_refresh_selected_property_values()
func _commit_position_change(
old_position: Vector2,
new_position: Vector2
) -> void:
if old_position == new_position:
_refresh_selected_property_values()
return
var item_id := (
room_layout.selected_item_id
)
if item_id == &"":
return
if undo_redo == null:
_apply_item_position(
item_id,
new_position
)
return
undo_redo.create_action(
"Move Room Item",
UndoRedo.MERGE_ENDS,
context.get_room()
)
undo_redo.add_do_method(
self,
"_apply_item_position",
item_id,
new_position
)
undo_redo.add_undo_method(
self,
"_apply_item_position",
item_id,
old_position
)
undo_redo.commit_action()
func _apply_selected_rotation(
rotation: float
) -> void:
room_layout.set_selected_rotation(
rotation
)
_refresh_selected_property_values()
func _commit_rotation_change(
old_rotation: float,
new_rotation: float
) -> void:
if is_equal_approx(
old_rotation,
new_rotation
):
return
var cabinet_id := (
room_layout.selected_item_id
)
if cabinet_id == &"":
return
if undo_redo == null:
_apply_cabinet_rotation(
cabinet_id,
new_rotation
)
return
undo_redo.create_action(
"Rotate Cabinet",
UndoRedo.MERGE_ENDS,
context.get_room()
)
undo_redo.add_do_method(
self,
"_apply_cabinet_rotation",
cabinet_id,
new_rotation
)
undo_redo.add_undo_method(
self,
"_apply_cabinet_rotation",
cabinet_id,
old_rotation
)
undo_redo.commit_action()
func _on_wall_creation_requested(
start: Vector2,
end: Vector2
) -> void:
if context == null:
return
var target_room := (
context.get_room()
)
if target_room == null:
return
var wall := (
RoomWallDefinition.new()
)
wall.wall_id = (
_generate_wall_id(
target_room
)
)
wall.start = start
wall.end = end
wall.thickness = 0.10
wall.is_doorway = doorway_tool.button_pressed
if undo_redo == null:
_add_wall(
target_room,
wall
)
return
undo_redo.create_action(
"Add Room Wall",
UndoRedo.MERGE_DISABLE,
target_room
)
undo_redo.add_do_method(
self,
"_add_wall",
target_room,
wall
)
undo_redo.add_undo_method(
self,
"_remove_wall",
target_room,
wall
)
undo_redo.commit_action()
func _generate_wall_id(
room: RoomDefinition
) -> StringName:
var index := 1
while true:
var candidate := StringName(
"wall_%03d"
% index
)
if not room.has_wall(
candidate
):
return candidate
index += 1
return &"wall"
func _add_wall(
target_room: RoomDefinition,
wall: RoomWallDefinition
) -> void:
if target_room == null:
return
if wall == null:
return
if target_room.walls.has(
wall
):
return
target_room.walls.append(
wall
)
context.room_edit_session.mark_dirty()
room_layout.queue_redraw()
func _remove_wall(
target_room: RoomDefinition,
wall: RoomWallDefinition
) -> void:
if target_room == null:
return
if wall == null:
return
target_room.walls.erase(
wall
)
context.room_edit_session.mark_dirty()
if (
room_layout.selected_wall_id
== wall.wall_id
):
room_layout.selected_wall_id = &""
_clear_property_panel()
room_layout.queue_redraw()
func _on_select_tool_button_pressed() -> void:
select_tool_button.button_pressed = true
room_layout.set_edit_mode(
RoomLayout.EditMode.SELECT
)
func _on_draw_wall_button_pressed() -> void:
draw_wall_button.button_pressed = true
room_layout.set_edit_mode(
RoomLayout.EditMode.DRAW_WALL
)
func _on_edit_boundary_button_pressed() -> void:
edit_boundary_button.button_pressed = true
room_layout.set_edit_mode(
RoomLayout.EditMode.EDIT_BOUNDARY
)
func _on_room_wall_selected(
wall_id: StringName
) -> void:
_refresh_wall_property_panel(
wall_id
)
editor_selection.select_wall(
wall_id
)
func _refresh_wall_property_panel(
wall_id: StringName
) -> void:
if context == null:
return
var room := context.get_room()
if room == null:
return
var wall := room.get_wall(
wall_id
)
if wall == null:
return
selected_label.text = (
("Doorway: %s" if wall.is_doorway else "Wall: %s")
% wall.wall_id
)
doorway_check.disabled = false
doorway_check.set_pressed_no_signal(wall.is_doorway)
swing_check.set_pressed_no_signal(wall.swing_reversed)
swing_check.disabled = not wall.is_doorway
wall_start_x_spin_box.set_value_no_signal(
wall.start.x * METERS_TO_FEET
)
wall_start_y_spin_box.set_value_no_signal(
wall.start.y * METERS_TO_FEET
)
wall_end_x_spin_box.set_value_no_signal(
wall.end.x * METERS_TO_FEET
)
wall_end_y_spin_box.set_value_no_signal(
wall.end.y * METERS_TO_FEET
)
wall_thickness_spin_box.set_value_no_signal(
wall.thickness_ft
)
func _apply_wall_geometry(
target_room: RoomDefinition,
wall: RoomWallDefinition,
start: Vector2,
end: Vector2
) -> void:
if target_room == null:
return
if wall == null:
return
if not target_room.walls.has(
wall
):
return
wall.start = start
wall.end = end
context.room_edit_session.mark_dirty()
room_layout.queue_redraw()
if (
room_layout.selected_wall_id
== wall.wall_id
):
_refresh_wall_property_panel(
wall.wall_id
)
func _commit_wall_geometry_change(
wall: RoomWallDefinition,
old_start: Vector2,
old_end: Vector2,
new_start: Vector2,
new_end: Vector2
) -> void:
if (
old_start == new_start
and old_end == new_end
):
_refresh_wall_property_panel(wall.wall_id)
return
if context == null:
return
var target_room := (
context.get_room()
)
if target_room == null:
return
if undo_redo == null:
_apply_wall_geometry(
target_room,
wall,
new_start,
new_end
)
return
undo_redo.create_action(
"Edit Wall",
UndoRedo.MERGE_ENDS,
target_room
)
undo_redo.add_do_method(
self,
"_apply_wall_geometry",
target_room,
wall,
new_start,
new_end
)
undo_redo.add_undo_method(
self,
"_apply_wall_geometry",
target_room,
wall,
old_start,
old_end
)
undo_redo.commit_action()
func _on_wall_start_x_spin_box_value_changed(
value_ft: float
) -> void:
var wall := room_layout.get_selected_wall()
if wall == null:
return
var old_start := wall.start
var old_end := wall.end
var new_start := Vector2(
value_ft * FEET_TO_METERS,
old_start.y
)
new_start = room_layout.snap_position_for_editor(
new_start
)
_commit_wall_geometry_change(
wall,
old_start,
old_end,
new_start,
old_end
)
func _on_wall_start_y_spin_box_value_changed(
value_ft: float
) -> void:
var wall := room_layout.get_selected_wall()
if wall == null:
return
var old_start := wall.start
var old_end := wall.end
var new_start := Vector2(
old_start.x,
value_ft * FEET_TO_METERS
)
new_start = room_layout.snap_position_for_editor(
new_start
)
_commit_wall_geometry_change(
wall,
old_start,
old_end,
new_start,
old_end
)
func _on_wall_end_x_spin_box_value_changed(
value_ft: float
) -> void:
var wall := room_layout.get_selected_wall()
if wall == null:
return
var old_start := wall.start
var old_end := wall.end
var new_end := Vector2(
value_ft * FEET_TO_METERS,
old_end.y
)
new_end = room_layout.snap_position_for_editor(
new_end
)
_commit_wall_geometry_change(
wall,
old_start,
old_end,
old_start,
new_end
)
func _on_wall_end_y_spin_box_value_changed(
value_ft: float
) -> void:
var wall := room_layout.get_selected_wall()
if wall == null:
return
var old_start := wall.start
var old_end := wall.end
var new_end := Vector2(
old_end.x,
value_ft * FEET_TO_METERS
)
new_end = room_layout.snap_position_for_editor(
new_end
)
_commit_wall_geometry_change(
wall,
old_start,
old_end,
old_start,
new_end
)
func _apply_wall_thickness(
target_room: RoomDefinition,
wall: RoomWallDefinition,
thickness_ft: float
) -> void:
if target_room == null:
return
if wall == null:
return
if not target_room.walls.has(
wall
):
return
wall.thickness_ft = thickness_ft
context.room_edit_session.mark_dirty()
room_layout.queue_redraw()
if (
room_layout.selected_wall_id
== wall.wall_id
):
_refresh_wall_property_panel(
wall.wall_id
)
func _on_wall_thickness_spin_box_value_changed(
value: float
) -> void:
var wall := (
room_layout.get_selected_wall()
)
if wall == null:
return
if context == null:
return
var target_room := (
context.get_room()
)
if target_room == null:
return
var old_value_ft := (
wall.thickness_ft
)
if is_equal_approx(
old_value_ft,
value
):
return
if undo_redo == null:
_apply_wall_thickness(
target_room,
wall,
value
)
return
undo_redo.create_action(
"Change Wall Thickness",
UndoRedo.MERGE_ENDS,
target_room
)
undo_redo.add_do_method(
self,
"_apply_wall_thickness",
target_room,
wall,
value
)
undo_redo.add_undo_method(
self,
"_apply_wall_thickness",
target_room,
wall,
old_value_ft
)
undo_redo.commit_action()
func _on_delete_wall_button_pressed() -> void:
if context == null:
return
var target_room := (
context.get_room()
)
if target_room == null:
return
var wall := (
room_layout.get_selected_wall()
)
if wall == null:
return
if undo_redo == null:
_remove_wall(
target_room,
wall
)
return
undo_redo.create_action(
"Delete Room Wall",
UndoRedo.MERGE_DISABLE,
target_room
)
undo_redo.add_do_method(
self,
"_remove_wall",
target_room,
wall
)
undo_redo.add_undo_method(
self,
"_add_wall",
target_room,
wall
)
undo_redo.commit_action()
func _on_wall_geometry_edit_requested(
wall_id: StringName,
old_start: Vector2,
old_end: Vector2,
new_start: Vector2,
new_end: Vector2
) -> void:
if context == null:
return
var room := context.get_room()
if room == null:
return
var wall := room.get_wall(
wall_id
)
if wall == null:
return
_commit_wall_geometry_change(
wall,
old_start,
old_end,
new_start,
new_end
)
func _on_editor_selection_changed() -> void:
room_layout.apply_editor_selection(
editor_selection
)
logic_graph.apply_editor_selection(
editor_selection
)
func _on_logic_path_selected(
cabinet_id: StringName,
event_id: StringName,
action_id: StringName,
device_id: StringName
) -> void:
editor_selection.select_logic_path(
cabinet_id,
event_id,
action_id,
device_id
)
func _apply_boundary_geometry(
points: PackedVector2Array
) -> void:
if context == null:
return
var room := context.get_room()
if room == null:
return
room.boundary_points = (
points.duplicate()
)
context.room_edit_session.mark_dirty()
room_layout.queue_redraw()
func _on_boundary_geometry_edit_requested(
old_points: PackedVector2Array,
new_points: PackedVector2Array
) -> void:
if old_points == new_points:
return
if undo_redo == null:
_apply_boundary_geometry(
new_points
)
return
undo_redo.create_action(
"Edit Room Boundary",
UndoRedo.MERGE_ENDS,
context.get_room()
)
undo_redo.add_do_method(
self,
"_apply_boundary_geometry",
new_points
)
undo_redo.add_undo_method(
self,
"_apply_boundary_geometry",
old_points
)
undo_redo.commit_action()
func _on_focus_room_button_pressed() -> void:
room_layout.focus_room()
func _on_add_boundary_point_button_pressed() -> void:
add_boundary_point_button.button_pressed = true
room_layout.set_edit_mode(
RoomLayout.EditMode.ADD_BOUNDARY_POINT
)
func _on_delete_boundary_point_button_pressed() -> void:
delete_boundary_point_button.button_pressed = true
room_layout.set_edit_mode(
RoomLayout.EditMode.DELETE_BOUNDARY_POINT
)
func _on_room_layout_edit_rejected(
message: String
) -> void:
status_label.text = (
"Edit rejected: %s"
% message
)
func _on_reset_view_button_pressed() -> void:
room_layout.reset_view()
func _on_room_layout_zoom_changed(
zoom_value: float
) -> void:
zoom_label.text = (
"%d%%"
% roundi(
zoom_value * 100.0
)
)
func _apply_cabinet_rotation(
cabinet_id: StringName,
rotation: float
) -> void:
if context == null:
return
var room := context.get_room()
if room == null:
return
var placement := (
room.get_cabinet_placement(
cabinet_id
)
)
if placement == null:
return
placement.rotation_degrees = rotation
context.room_edit_session.mark_dirty()
room_layout.queue_redraw()
room_layout._reposition_items()
func _unhandled_key_input(
event: InputEvent
) -> void:
if not Engine.is_editor_hint():
return
if not (
event is InputEventKey
):
return
if not event.pressed:
return
if event.echo:
return
var ctrl_pressed: bool = (
event.ctrl_pressed
or event.meta_pressed
)
if not ctrl_pressed:
return
if (
event.keycode == KEY_Z
and event.shift_pressed
):
redo_escape_room_action()
get_viewport().set_input_as_handled()
return
if event.keycode == KEY_Z:
undo_escape_room_action()
get_viewport().set_input_as_handled()
return
if event.keycode == KEY_Y:
redo_escape_room_action()
get_viewport().set_input_as_handled()
func _get_active_escape_room_undo_redo() -> UndoRedo:
if undo_redo == null:
return null
if context == null:
return null
var target: Object = null
var current_tab_name := (
main_views.get_tab_title(
main_views.current_tab
)
)
if current_tab_name == "Logic":
target = context.get_scenario()
else:
target = context.get_room()
if target == null:
return null
var history_id := (
undo_redo.get_object_history_id(
target
)
)
return (
undo_redo.get_history_undo_redo(
history_id
)
)
func undo_escape_room_action() -> void:
var history := (
_get_active_escape_room_undo_redo()
)
if history == null:
print("[UNDO] No history")
return
print(
"[UNDO] has_undo=",
history.has_undo()
)
if history.has_undo():
history.undo()
func redo_escape_room_action() -> void:
var history := (
_get_active_escape_room_undo_redo()
)
if history == null:
return
if not history.has_redo():
return
history.redo()
func _on_room_layout_item_position_edit_requested(
item_id: StringName,
old_position: Vector2,
new_position: Vector2
) -> void:
if old_position == new_position:
return
if undo_redo == null:
_apply_item_position(
item_id,
new_position
)
return
undo_redo.create_action(
"Move Room Item",
UndoRedo.MERGE_DISABLE,
context.get_room()
)
undo_redo.add_do_method(
self,
"_apply_item_position",
item_id,
new_position
)
undo_redo.add_undo_method(
self,
"_apply_item_position",
item_id,
old_position
)
undo_redo.commit_action()
func _on_room_layout_binding_route_edit_requested(
cabinet_id: StringName,
action_id: StringName,
device_id: StringName,
old_points: PackedVector2Array,
new_points: PackedVector2Array
) -> void:
if old_points == new_points:
return
if context == null:
return
if undo_redo == null:
_apply_binding_route_points(
cabinet_id,
action_id,
device_id,
new_points
)
return
undo_redo.create_action(
"Move Cable Route Point",
UndoRedo.MERGE_DISABLE,
context.get_room()
)
undo_redo.add_do_method(
self,
"_apply_binding_route_points",
cabinet_id,
action_id,
device_id,
new_points.duplicate()
)
undo_redo.add_undo_method(
self,
"_apply_binding_route_points",
cabinet_id,
action_id,
device_id,
old_points.duplicate()
)
undo_redo.commit_action()
func _apply_binding_route_points(
cabinet_id: StringName,
action_id: StringName,
device_id: StringName,
points: PackedVector2Array
) -> void:
if context == null:
return
var room := (
context.get_room()
)
if room == null:
return
var target_binding: RoomDeviceBinding = null
for binding in room.bindings:
if binding == null:
continue
if (
binding.cabinet_id == cabinet_id
and binding.hardware_action_id == action_id
and binding.device_id == device_id
):
target_binding = binding
break
if target_binding == null:
return
target_binding.route_points = (
points.duplicate()
)
context.room_edit_session.mark_dirty()
room_layout.refresh_item_positions()
func _apply_item_position(
item_id: StringName,
position: Vector2
) -> void:
if context == null:
return
var room := (
context.get_room()
)
if room == null:
return
var placement := (
room.get_cabinet_placement(
item_id
)
)
if placement != null:
placement.position = (
position
)
else:
var device := (
room.get_device(
item_id
)
)
if device == null:
return
device.position = (
position
)
context.room_edit_session.mark_dirty()
room_layout.refresh_item_positions()
if room_layout.selected_item_id == item_id:
_refresh_selected_property_values()
func _on_simulation_pause_requested() -> void:
simulation_driver.set_paused(
true
)
simulation_timeline.set_paused(
true
)
func _on_simulation_resume_requested() -> void:
simulation_driver.set_paused(
false
)
simulation_timeline.set_paused(
false
)
func _on_simulation_step_requested(
seconds: float
) -> void:
simulation_driver.step(
seconds
)
func _on_simulation_reset_requested() -> void:
simulation_driver.reset_simulation()
_reset_simulation_visuals()
simulation_timeline.clear()
simulation_timeline.set_paused(
false
)
simulation_timeline.set_sequence_running(
false
)
func _load_test_sequences() -> void:
test_sequences.clear()
var directory := DirAccess.open(
TEST_SEQUENCE_DIRECTORY
)
if directory == null:
push_warning(
"Could not open test sequence directory: %s"
% TEST_SEQUENCE_DIRECTORY
)
simulation_timeline.set_sequences(
test_sequences
)
return
directory.list_dir_begin()
while true:
var file_name := (
directory.get_next()
)
if file_name.is_empty():
break
if directory.current_is_dir():
continue
if not file_name.ends_with(
".tres"
):
continue
var path := (
TEST_SEQUENCE_DIRECTORY
.path_join(
file_name
)
)
var resource := (
ResourceLoader.load(
path
)
)
var sequence := (
resource as TestSequence
)
if sequence == null:
push_warning(
"Resource is not a TestSequence: %s"
% path
)
continue
test_sequences.append(
sequence
)
directory.list_dir_end()
test_sequences.sort_custom(
func(
a: TestSequence,
b: TestSequence
) -> bool:
return (
a.display_name
< b.display_name
)
)
simulation_timeline.set_sequences(
test_sequences
)
if test_sequences.is_empty():
selected_test_sequence_index = -1
else:
selected_test_sequence_index = 0
func _on_test_sequence_selected(
index: int
) -> void:
if (
index < 0
or index >= test_sequences.size()
):
selected_test_sequence_index = -1
return
selected_test_sequence_index = index
func _on_run_test_sequence_requested() -> void:
if (
selected_test_sequence_index < 0
or selected_test_sequence_index
>= test_sequences.size()
):
return
var sequence := (
test_sequences[
selected_test_sequence_index
]
)
if sequence == null:
return
var current_scenario := (
context.get_scenario()
)
if current_scenario == null:
status_label.text = (
"Cannot run rehearsal: no scenario is open."
)
return
if (
sequence.scenario_id != &""
and sequence.scenario_id
!= current_scenario.scenario_id
):
status_label.text = (
"Rehearsal '%s' requires scenario '%s'."
% [
sequence.display_name,
sequence.scenario_id
]
)
push_warning(
(
"Cannot run rehearsal '%s': "
+ "current scenario is '%s', "
+ "but rehearsal requires '%s'."
)
% [
sequence.display_name,
current_scenario.scenario_id,
sequence.scenario_id
]
)
return
_reset_simulation_visuals()
simulation_timeline.clear()
simulation_driver.start_sequence(
sequence
)
func _on_stop_test_sequence_requested() -> void:
simulation_driver.stop_sequence()
func _on_replay_test_sequence_requested() -> void:
_on_run_test_sequence_requested()
func _on_test_sequence_started(
_sequence: TestSequence
) -> void:
simulation_timeline.set_sequence_running(
true
)
func _on_test_sequence_finished(
_sequence: TestSequence
) -> void:
simulation_timeline.set_sequence_running(
false
)
func _on_test_sequence_stopped() -> void:
simulation_timeline.set_sequence_running(
false
)
func _reset_simulation_visuals() -> void:
if context == null:
return
var room := (
context.get_room()
)
if room == null:
return
room_simulator.configure(
room
)
for device in room.devices:
if device == null:
continue
room_layout.set_device_active(
device.device_id,
device.initial_active
)
for binding in room.bindings:
if binding == null:
continue
room_layout.set_binding_active(
binding.cabinet_id,
binding.hardware_action_id,
binding.device_id,
false
)
func _on_room_layout_refresh_requested() -> void:
if context == null:
return
if context.room_edit_session.is_dirty:
status_label.text = (
"Cannot refresh: save or revert "
+ "unsaved room changes first."
)
return
if not context.room_edit_session.reload_from_disk():
status_label.text = (
"Failed to refresh room."
)
return
status_label.text = (
"Physical room refreshed."
)
func _setup_view_controls() -> void:
var toolbar := draw_wall_button.get_parent()
doorway_tool = Button.new()
doorway_tool.text = "DRAW DOORWAY"
doorway_tool.toggle_mode = true
doorway_tool.button_group = draw_wall_button.button_group
doorway_tool.tooltip_text = "Drag from hinge to opposite jamb. Align with a wall to create an opening."
toolbar.add_child(doorway_tool)
toolbar.move_child(doorway_tool, draw_wall_button.get_index() + 1)
doorway_tool.pressed.connect(func(): room_layout.set_edit_mode(RoomLayout.EditMode.DRAW_WALL))
var view_bar := HBoxContainer.new()
toolbar.get_parent().add_child(view_bar)
toolbar.get_parent().move_child(view_bar, toolbar.get_index() + 1)
for title in ["Labels", "Dimensions"]:
var toggle := CheckBox.new()
toggle.text = title
toggle.button_pressed = true
view_bar.add_child(toggle)
if title == "Labels":
toggle.toggled.connect(room_layout.set_labels_visible)
else:
toggle.toggled.connect(room_layout.set_dimensions_visible)
doorway_check = CheckBox.new()
doorway_check.text = "Doorway opening"
doorway_check.disabled = true
doorway_check.toggled.connect(func(value: bool): _commit_door_setting("is_doorway", value))
swing_check = CheckBox.new()
swing_check.text = "Reverse door swing"
swing_check.disabled = true
swing_check.toggled.connect(func(value: bool): _commit_door_setting("swing_reversed", value))
var properties := delete_wall_button.get_parent()
properties.add_child(doorway_check)
properties.add_child(swing_check)
properties.move_child(doorway_check, delete_wall_button.get_index())
properties.move_child(swing_check, delete_wall_button.get_index())
func _commit_door_setting(property: String, value: bool) -> void:
if context == null:
return
var target := context.get_room()
var wall := target.get_wall(room_layout.selected_wall_id)
if wall == null:
doorway_check.set_pressed_no_signal(false)
swing_check.set_pressed_no_signal(false)
return
var old: bool = wall.get(property)
if old == value:
return
if undo_redo == null:
_apply_door_setting(target, wall, property, value)
return
undo_redo.create_action("Edit doorway", UndoRedo.MERGE_DISABLE, target)
undo_redo.add_do_method(self, "_apply_door_setting", target, wall, property, value)
undo_redo.add_undo_method(self, "_apply_door_setting", target, wall, property, old)
undo_redo.commit_action()
func _apply_door_setting(target: RoomDefinition, wall: RoomWallDefinition, property: String, value: bool) -> void:
if not target.walls.has(wall):
return
wall.set(property, value)
context.room_edit_session.mark_dirty()
room_layout.queue_redraw()
_refresh_wall_property_panel(wall.wall_id)