@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)