extends Node const QUEST := "debug_probe_quest" const ITEM := "debug_probe_item" const ROOM := "debug_probe_room" const SPECIAL := "debug_probe_special" const SCENE := "debug_probe_scene" const DIALOGUE := preload("res://scripts/core/dialogue_overlay.gd") var is_probe := false var failures := 0 func _ready() -> void: if is_probe: return GameState.save_locked = true var probe := Node.new() probe.set_script(get_script()) probe.set("is_probe", true) get_tree().root.add_child.call_deferred(probe) probe.call_deferred("_run") func check(ok: bool, message: String) -> void: if not ok: failures += 1 push_error("DEBUG PROGRESSION: " + message) func _frames(count: int = 5) -> void: for frame: int in count: await get_tree().process_frame func _choose(picker: OptionButton, id: String) -> void: for index: int in picker.item_count: if str(picker.get_item_metadata(index)) == id: picker.select(index) picker.item_selected.emit(index) return check(false, "Missing choice: " + id) func _click(button: BaseButton) -> void: await _frames(2) check(button.is_visible_in_tree() and not button.disabled, "Button inaccessible: " + button.text) var point := button.get_global_transform_with_canvas() * (button.size * 0.5) for pressed: bool in [true, false]: var event := InputEventMouseButton.new() event.position = point event.global_position = point event.button_index = MOUSE_BUTTON_LEFT event.pressed = pressed get_viewport().push_input(event, true) await _frames(1) await _frames() ## The bridge reads F3 from the keyboard state: press, then release. func _press_f3() -> void: for pressed: bool in [true, false]: var key := InputEventKey.new() key.keycode = KEY_F3 key.physical_keycode = KEY_F3 key.pressed = pressed Input.parse_input_event(key) await _frames(2) func _run() -> void: GameState._reset_progress_state() GameState.story_triggers.process_mode = Node.PROCESS_MODE_DISABLED GameState.debug_invincible = true GameState.debug_infinite_stamina = true DataCatalog.QUESTS[QUEST] = {"display_name": "Debug quest", "scope": "run", "stages": [ {"title": "Start", "description": "First"}, {"title": "Middle", "description": "Second"}, {"title": "End", "description": "Third"}]} DataCatalog.KEY_ITEMS[ITEM] = {"display_name": "Debug item", "scope": "run", "max_owned": 3} var dimensions := DataCatalog.cycle_dimension_ids() var dimension := dimensions[1] DataCatalog.ROOMS[ROOM] = {"display_name": "Locked debug room", "dimension": dimension, "level": 3, "biome": str(DataCatalog.BIOMES.keys()[0]), "waves": [], "conditions": [ {"path": "quests.%s.state" % QUEST, "operator": "==", "value": 3}]} DataCatalog.ROOMS[SPECIAL] = {"display_name": "Special debug room", "dimension": "", "level": 0, "biome": str(DataCatalog.BIOMES.keys()[0]), "waves": []} DataCatalog.STORY_SCENES[SCENE] = {"display_name": "Debug scene", "description": "", "backdrop_color": Color(0, 0, 0, 0.4), "backdrop_visual": "", "skippable": true, "actors": [], "events": [{"id": "line", "type": "text", "text": "Arrival.", "duration": 0.0}, {"id": "close", "type": "end"}]} get_tree().change_scene_to_file("res://scenes/restaurant/restaurant.tscn") await _frames(10) GameState.clear_announcements() Stage.clear() await _press_f3() check(is_instance_valid(Bridge3D._panel) and Bridge3D._panel.visible, "F3 did not open the panel") if not is_instance_valid(Bridge3D._panel): await _finish() return var menu: TabContainer = Bridge3D._panel.get_node("Progression") _choose(menu.quest_picker, QUEST) await _click(menu.quest_buttons.activate) check(GameState.quest_value(QUEST, "state") == 1, "Activate button did not start quest") await _click(menu.quest_buttons.advance) check(GameState.quest_value(QUEST, "stage") == 1, "Advance button did not advance") menu.stage_picker.select(2) menu.stage_picker.item_selected.emit(2) await _click(menu.quest_buttons.apply_stage) check(GameState.quest_value(QUEST, "stage") == 2, "Direct stage selection failed") check(menu.quest_buttons.advance.disabled, "Advance must stop at last stage") await _click(menu.quest_buttons.complete) check(GameState.quest_value(QUEST, "state") == 2, "Complete button failed") await _click(menu.quest_buttons.reset) check(GameState.quest_value(QUEST, "state") == 0 and GameState.quest_value(QUEST, "stage") == 0, "Reset failed") menu.stage_picker.select(1) await _click(menu.quest_buttons.apply_stage) check(GameState.quest_value(QUEST, "state") == 1 and not GameState.has_key_item(ITEM), "Stage selection granted an item") menu.current_tab = 1 # Scroll the last item into view, as the player must do. await _frames() (menu.get_child(1) as ScrollContainer).ensure_control_visible(menu.item_toggles[ITEM]) await _click(menu.item_toggles[ITEM]) check(GameState.key_item_amount(ITEM) == 1, "Item toggle did not grant one item") GameState.set_key_item_amount(ITEM, 3) await _click(menu.item_toggles[ITEM]) check(not GameState.has_key_item(ITEM), "Item toggle did not remove every copy") await _click(menu.item_toggles[ITEM]) check(GameState.quest_value(QUEST, "stage") == 1, "Item toggle changed quest stage") GameState.coins = 1234 var ingredient := str(DataCatalog.INGREDIENTS.keys()[0]) GameState.ingredients[ingredient] = 17 GameState.purchased_upgrades.append("atk1") var upgrades := GameState.purchased_upgrades.duplicate() var cleared: Variant = GameState.get_fact_value("dimensions_cleared") menu.current_tab = 2 _choose(menu.dimension_picker, dimension) _choose(menu.room_picker, ROOM) check(not DiveMap.room_is_open(ROOM), "Fixture room must be locked") await _click(menu.travel_button) await _frames(10) check(get_tree().current_scene.scene_file_path.ends_with("dungeon.tscn"), "Travel button did not load dungeon") check(GameState.cycle_dimension_id() == dimension, "Travel did not change progression dimension") check(DiveMap.current_room_id() == ROOM and DiveMap.level() == 3, "Exact locked room was replaced or started at level one") check(GameState.coins == 1234 and GameState.ingredients.get(ingredient) == 17, "Travel lost resources") check(GameState.purchased_upgrades == upgrades, "Travel lost upgrades") check(GameState.quest_value(QUEST, "state") == 1 and GameState.quest_value(QUEST, "stage") == 1, "Travel lost run quest") check(GameState.has_key_item(ITEM), "Travel lost run quest item") check(GameState.get_fact_value("dimensions_cleared") == cleared, "Travel completed skipped dimensions") # Line 97: travel closes the panel. Left open, it remembered the old place's # running game and unfroze the scene played on arrival when F3 closed it. check(not Bridge3D._panel.is_open and not get_tree().paused, "Travel left the debug panel open over the new place") var arrival := DIALOGUE.play_scene(get_tree().current_scene, SCENE) await _frames(3) check(arrival != null and get_tree().paused, "The arrival scene did not freeze the room") # The player closes F3 after landing, if it is still open. if Bridge3D._panel.is_open: await _press_f3() check(get_tree().paused and DIALOGUE.current == arrival, "Closing F3 after travel unfroze the scene played on arrival") await _press_f3() check(Bridge3D._panel.is_open, "F3 did not reopen the panel over a scene") GameState.collect_dive_coins(50) GameState.collect_dive_ingredient(ingredient, 2) _choose(menu.room_picker, SPECIAL) await _click(menu.travel_button) check(DIALOGUE.current == null and not is_instance_valid(arrival) and not get_tree().paused, "Travel carried a playing scene into the new place") check(DiveMap.current_room_id() == SPECIAL, "Special room travel failed") check(GameState.cycle_dimension_id() == dimension, "Special room changed progression dimension") check(GameState.coins == 1284 and GameState.ingredients.get(ingredient) == 19, "Travel did not settle the dive bag") await _press_f3() await _click(menu.return_button) await _frames(10) check(get_tree().current_scene.scene_file_path.ends_with("restaurant.tscn"), "Debug return did not load restaurant") check(GameState.cycle_dimension_id() == dimension and GameState.run_values.is_empty(), "Return restored wrong dimension or retained dive") check(GameState.quest_value(QUEST, "stage") == 1 and GameState.has_key_item(ITEM), "Debug return lost quest or item") # The next ordinary dive must follow the new progression and start normally. check(DiveMap.begin_dive() == dimension and DiveMap.level() == 1 and DiveMap.current_room_id() != SPECIAL, "Debug destination leaked into a later dive") GameState.end_run() check(not GameState.has_key_item(ITEM) and GameState.quest_value(QUEST, "state") == 0, "Normal end_run no longer resets run-scoped quests and items") # Real departures keep a restaurant alive offstage. Debug travel must retire # it, otherwise the new dimension keeps the old restaurant's geometry. get_tree().current_scene.call("_leave_for_scene", "res://scenes/dungeon/dungeon.tscn") await _frames(10) check(GameState.restaurant_is_offstage(), "Fixture did not park a live restaurant") await _press_f3() _choose(menu.dimension_picker, dimensions[0]) await _click(menu.return_button) check(not GameState.restaurant_is_offstage() and GameState.cycle_dimension_id() == dimensions[0], "Backward dimension travel retained the offstage restaurant") check(GameState.purchased_upgrades == upgrades, "Backward travel lost upgrades") check(not GameState.debug_prepare_travel("missing_dimension", ROOM), "Invalid dimension accepted") check(not GameState.debug_prepare_travel(dimensions[0], ROOM), "Room from wrong dimension accepted") await _finish() func _finish() -> void: AudioDirector.shutdown() await _frames(16) print("DEBUG PROGRESSION: PASSED" if failures == 0 else "DEBUG PROGRESSION: FAILED (%d)" % failures) get_tree().quit(0 if failures == 0 else 1)