extends SceneTree const SANDBOX_STAGE: PackedScene = preload("res://scenes/stage/sandbox_stage.tscn") const STAGE_SCENE_IO: Script = preload("res://scripts/stage/stage_scene_io.gd") const PLAIN_STARTER_PATH: String = "res://content/stages/starters/plain_background.scene" var _checks: int = 0 var _failures: int = 0 var _stage: SandboxStage func _initialize() -> void: call_deferred("_run") func _run() -> void: root.size = Vector2i(1280, 720) _test_version_migration_and_validation() var starter_result: Dictionary = STAGE_SCENE_IO.read_file(PLAIN_STARTER_PATH) _expect(bool(starter_result.get("ok", false)), "Plain Background starter is valid v1.7 data") _stage = SANDBOX_STAGE.instantiate() as SandboxStage root.add_child(_stage) await process_frame await process_frame _test_live_backdrop_default() await _save_visual_capture("phase19_open_sky_stage.png") await _test_plain_background_picker_flow() await _test_draft_cancel_on_mode_switch() await _test_draft_cancel_on_popup_hide() await _test_draft_cancel_on_scene_replacement(starter_result.get("data", {}) as Dictionary) _test_recent_color_history_limit() print("----------------------------------------") if _failures == 0: print("RESULT: ALL PASSED (%d checks)" % _checks) quit(0) else: print("RESULT: %d FAILURE(S) out of %d checks" % [_failures, _checks]) quit(1) func _expect(condition: bool, message: String) -> void: _checks += 1 if condition: print("PASS: " + message) else: _failures += 1 push_error("Phase 19 Plain Background check failed: " + message) func _test_version_migration_and_validation() -> void: for version: String in ["1.0", "1.4", "1.5", "1.6"]: var legacy: Dictionary = { "version": version, "scene_name": "Migration fixture", "sky_id": "sky_sunset", "camera": {"position": {"x": 0.0, "y": -400.0}, "zoom": 0.5}, "objects": [], "event_rules": [], } var migrated: Dictionary = STAGE_SCENE_IO._migrate_to_current(legacy) var expected_sky: String = "sky_sunset" if version in ["1.5", "1.6"] else "sky_neutral" _expect(String(migrated.get("version", "")) == "1.7", "%s scene migrates to v1.7" % version) _expect(String(migrated.get("sky_id", "")) == expected_sky, "%s migration preserves the compatible Sky choice" % version) _expect(String(migrated.get("background_kind", "")) == "sky", "%s migration remains visually Sky-backed" % version) _expect(String(migrated.get("plain_color", "")) == STAGE_SCENE_IO.DEFAULT_PLAIN_COLOR, "%s migration adds the default remembered color" % version) _expect(bool(STAGE_SCENE_IO._validate_root(migrated).get("ok", false)), "%s migrated empty scene passes v1.7 root validation" % version) var valid: Dictionary = _valid_document() _expect(bool(STAGE_SCENE_IO._validate_root(valid).get("ok", false)), "valid v1.7 Plain Background fields pass validation") var missing_kind: Dictionary = valid.duplicate(true) missing_kind.erase("background_kind") _expect(not bool(STAGE_SCENE_IO._validate_root(missing_kind).get("ok", false)), "v1.7 rejects a missing background kind") var missing_sky: Dictionary = valid.duplicate(true) missing_sky.erase("sky_id") _expect(not bool(STAGE_SCENE_IO._validate_root(missing_sky).get("ok", false)), "v1.7 rejects a missing remembered Sky") var missing_color: Dictionary = valid.duplicate(true) missing_color.erase("plain_color") _expect(not bool(STAGE_SCENE_IO._validate_root(missing_color).get("ok", false)), "v1.7 rejects a missing remembered Plain Background color") var alpha_color: Dictionary = valid.duplicate(true) alpha_color["plain_color"] = "#F5EFE0FF" _expect(not bool(STAGE_SCENE_IO._validate_root(alpha_color).get("ok", false)), "v1.7 rejects alpha-bearing Plain Background colors") var bad_color: Dictionary = valid.duplicate(true) bad_color["plain_color"] = "F5EFE0" _expect(not bool(STAGE_SCENE_IO._validate_root(bad_color).get("ok", false)), "v1.7 requires canonical hash-prefixed six-digit color") func _test_live_backdrop_default() -> void: var background: StageBackgroundView = _stage.get("_background_view") as StageBackgroundView var sky_view: StageSkyView = _stage.get("_sky_view") as StageSkyView var plain_fill: ColorRect = background.get_node("PlainFill") as ColorRect if background != null else null _expect(background != null and sky_view != null and plain_fill != null, "live Stage owns the shared Sky and Plain renderer") if background == null or sky_view == null or plain_fill == null: return _expect(background.get_background_kind() == &"sky" and sky_view.visible and not plain_fill.visible, "default live Stage shows Sky with Plain fill hidden") _expect(background.get_plain_color().to_html(false).to_upper() == "F5EFE0", "default remembered Plain color is warm off-white") var plain_starter: Dictionary = STAGE_SCENE_IO.read_file(PLAIN_STARTER_PATH).get("data", {}) as Dictionary _expect(String(plain_starter.get("background_kind", "")) == "plain", "Plain Background starter selects a flat backdrop") _expect(String(plain_starter.get("plain_color", "")) == "#F5EFE0" and (plain_starter.get("objects", []) as Array).is_empty(), "Plain Background starter is empty and uses the approved color") func _test_plain_background_picker_flow() -> void: var browser: StageAssetBrowser = _stage.get("_asset_browser") as StageAssetBrowser _stage.call("_open_selector", "scenery") var selector: AssetSelector = browser.get_selector() selector.call("_set_gallery_tab", "backgrounds") await process_frame var grid: GridContainer = selector.find_child("GridContainer", true, false) as GridContainer var plain_card: AssetCard = _find_card(grid, "plain_background") _expect(selector.visible and plain_card != null, "real Scenery gallery shows the Plain Background card") if plain_card == null: return await _activate_card(plain_card) var title: Label = selector.find_child("TitleLabel", true, false) as Label var controls: StageSurfaceColorControls = selector.find_child("PlainBackgroundColorControls", true, false) as StageSurfaceColorControls _expect(String(_stage.get("_background_kind")) == "plain" and selector.visible, "Plain card immediately switches the Stage and keeps the gallery open") _expect(title != null and title.text == "Choose background color", "Plain color choices use the approved heading") _expect(controls != null and controls.get("_current_chip") != null, "color choices expose the current-color chip and swatch sections") if controls == null: return await _save_visual_capture("phase19_plain_color_choices.png") var recent_before: Array[String] = _stage.call("_surface_color_history_strings") as Array[String] _expect(not recent_before.has("#F5EFE0"), "opening at the default does not add that color to recents") var empty_message: Label = controls.find_child("RecentColorsEmptyLabel", true, false) as Label if recent_before.is_empty(): _expect(empty_message != null and empty_message.text == "Colors you use will appear here.", "empty recent colors use the approved helper text") var confirmed_color: Color = _stage.get("_plain_color") as Color var confirmed_hex: String = _stage.call("_canonical_surface_color", confirmed_color) as String var dirty_before: bool = bool(_stage.get("_dirty")) var current_chip: Button = controls.get("_current_chip") as Button await _activate_button(current_chip) var picker_view: StageSurfaceColorPickerView = selector.get("_color_picker_view") as StageSurfaceColorPickerView var picker: ColorPicker = picker_view.find_child("ColorPickerControl", true, false) as ColorPicker if picker_view != null else null var use_button: Button = selector.get("_color_use_button") as Button var back_button: Button = selector.get("_color_back_button") as Button _expect(picker != null and use_button != null and back_button != null, "current-color chip opens the full custom picker with Back and Use color") if picker == null or use_button == null or back_button == null: return var draft_color: Color = Color("#B34FD8") _set_picker_draft(picker_view, picker, draft_color) await process_frame var background: StageBackgroundView = _stage.get("_background_view") as StageBackgroundView var plain_fill: ColorRect = background.get_node("PlainFill") as ColorRect var draft_snapshot: Dictionary = _stage.call("_collect_scene_data", false) as Dictionary _expect(bool(_stage.get("_plain_color_draft_active")) and plain_fill.color.to_html(false).to_upper() == "B34FD8", "custom picker movement previews an opaque flat color on the Stage") await _save_visual_capture("phase19_plain_picker_draft.png") _expect(String(draft_snapshot.get("plain_color", "")) == confirmed_hex, "scene snapshots keep the confirmed color during a custom draft") _expect(bool(_stage.get("_dirty")) == dirty_before and (_stage.call("_surface_color_history_strings") as Array[String]) == recent_before, "custom preview does not change dirty state or recents") await _activate_button(back_button) _expect(not bool(_stage.get("_plain_color_draft_active")) and plain_fill.color.to_html(false).to_upper() == confirmed_hex.trim_prefix("#"), "Back discards the draft and restores the confirmed Stage fill") await _save_visual_capture("phase19_plain_picker_rollback.png") _expect(bool(_stage.get("_dirty")) == dirty_before and (_stage.call("_surface_color_history_strings") as Array[String]) == recent_before, "Back leaves dirty state and recents unchanged") _expect(selector.visible and title.text == "Choose background color", "Back returns to color choices with the scene picker still open") await _activate_button(controls.get("_current_chip") as Button) picker_view = selector.get("_color_picker_view") as StageSurfaceColorPickerView picker = picker_view.find_child("ColorPickerControl", true, false) as ColorPicker if picker_view != null else null _set_picker_draft(picker_view, picker, Color("#26A8D6")) await _press_escape() _expect(not bool(_stage.get("_plain_color_draft_active")) and plain_fill.color.to_html(false).to_upper() == confirmed_hex.trim_prefix("#"), "Escape from the custom picker restores the prior fill") _expect(selector.visible and title.text == "Choose background color", "Escape returns to color choices instead of closing the Stage chooser") await _activate_button(controls.get("_current_chip") as Button) picker_view = selector.get("_color_picker_view") as StageSurfaceColorPickerView picker = picker_view.find_child("ColorPickerControl", true, false) as ColorPicker if picker_view != null else null _set_picker_draft(picker_view, picker, draft_color) await _activate_button(selector.get("_color_use_button") as Button) var committed_hex: String = _stage.call("_canonical_surface_color", _stage.get("_plain_color")) as String var recent_after_commit: Array[String] = _stage.call("_surface_color_history_strings") as Array[String] _expect(committed_hex == "#B34FD8" and plain_fill.color.to_html(false).to_upper() == "B34FD8", "Use color confirms the custom color on the Stage") _expect(bool(_stage.get("_dirty")) and recent_after_commit.count(committed_hex) == 1 and recent_after_commit[0] == committed_hex, "one confirmation marks changed scene dirty and records the color once") _expect(selector.visible and title.text == "Choose background color", "Use color returns to the choices view") var path: String = "user://phase19_plain_background_roundtrip.scene" var saved: bool = _stage.call("_save_scene_to_path", path, false) as bool var read: Dictionary = STAGE_SCENE_IO.read_file(path) var saved_data: Dictionary = read.get("data", {}) as Dictionary _expect(saved and bool(read.get("ok", false)) and String(saved_data.get("background_kind", "")) == "plain" and String(saved_data.get("plain_color", "")) == committed_hex, "production save/reopen boundary stores the confirmed Plain color") _stage.call("_close_selector") _stage.call("_open_selector", "scenery") selector = browser.get_selector() selector.call("_set_gallery_tab", "backgrounds") await process_frame grid = selector.find_child("GridContainer", true, false) as GridContainer var sky_card: AssetCard = _find_card(grid, "sky_clear_day") if sky_card != null: await _activate_card(sky_card) _expect(String(_stage.get("_background_kind")) == "sky" and _stage.call("_canonical_surface_color", _stage.get("_plain_color")) == committed_hex, "switching to Sky retains the remembered Plain color") var sky_saved: Dictionary = _stage.call("_collect_scene_data", false) as Dictionary _expect(String(sky_saved.get("background_kind", "")) == "sky" and String(sky_saved.get("plain_color", "")) == committed_hex, "Sky-active scene snapshot preserves the remembered Plain color") grid = selector.find_child("GridContainer", true, false) as GridContainer var return_plain_card: AssetCard = _find_card(grid, "plain_background") if return_plain_card != null: await _activate_card(return_plain_card) _expect(String(_stage.get("_background_kind")) == "plain" and _stage.call("_canonical_surface_color", _stage.get("_plain_color")) == committed_hex, "returning from Sky restores the confirmed Plain color") controls = selector.find_child("PlainBackgroundColorControls", true, false) as StageSurfaceColorControls var preset_swatches: ColorSwatchSelector = controls.get("_preset_swatches") as ColorSwatchSelector if controls != null else null var preset_button: Button = preset_swatches.get_child(0) as Button if preset_swatches != null and preset_swatches.get_child_count() > 0 else null var preset_color: Color = Color.WHITE if preset_button != null: preset_color = preset_button.get_meta("surface_color", Color.WHITE) if preset_button != null: await _activate_button(preset_button) var preset_hex: String = _stage.call("_canonical_surface_color", preset_color) as String var preset_recents: Array[String] = _stage.call("_surface_color_history_strings") as Array[String] _expect(preset_button != null and preset_button.button_pressed and preset_button.text == "✓", "preset swatch selection displays its selected check") _expect(_stage.call("_canonical_surface_color", _stage.get("_plain_color")) == preset_hex and preset_recents[0] == preset_hex, "preset swatch immediately confirms the Stage color and moves it to recent colors") await _save_visual_capture("phase19_plain_preset_selected.png") _stage.call("_close_selector") var loaded: bool = _stage.call("_load_scene_from_path", path, false) as bool _expect(loaded and String(_stage.get("_background_kind")) == "plain" and _stage.call("_canonical_surface_color", _stage.get("_plain_color")) == committed_hex, "production Stage load restores the confirmed Plain background") if FileAccess.file_exists(path): DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) func _test_draft_cancel_on_mode_switch() -> void: var browser: StageAssetBrowser = _stage.get("_asset_browser") as StageAssetBrowser _stage.call("_open_selector", "scenery") var selector: AssetSelector = browser.get_selector() selector.call("_set_gallery_tab", "backgrounds") await process_frame var grid: GridContainer = selector.find_child("GridContainer", true, false) as GridContainer var plain_card: AssetCard = _find_card(grid, "plain_background") if plain_card != null: await _activate_card(plain_card) var controls: StageSurfaceColorControls = selector.find_child("PlainBackgroundColorControls", true, false) as StageSurfaceColorControls await _activate_button(controls.get("_current_chip") as Button) var picker_view: StageSurfaceColorPickerView = selector.get("_color_picker_view") as StageSurfaceColorPickerView var picker: ColorPicker = picker_view.find_child("ColorPickerControl", true, false) as ColorPicker if picker_view != null else null _set_picker_draft(picker_view, picker, Color("#E66A21")) await process_frame var confirmed_hex: String = _stage.call("_canonical_surface_color", _stage.get("_plain_color")) as String _stage.call("set_mode", SandboxStage.StageMode.DIRECT) await process_frame var background: StageBackgroundView = _stage.get("_background_view") as StageBackgroundView var fill: ColorRect = background.get_node("PlainFill") as ColorRect _expect(not bool(_stage.get("_plain_color_draft_active")) and not selector.visible, "mode switch closes an active custom picker and cancels its draft") _expect(fill.color.to_html(false).to_upper() == confirmed_hex.trim_prefix("#"), "mode-switch cancel restores the confirmed backdrop") _stage.call("set_mode", SandboxStage.StageMode.EDIT) await process_frame func _test_draft_cancel_on_popup_hide() -> void: var browser: StageAssetBrowser = _stage.get("_asset_browser") as StageAssetBrowser _stage.call("_open_selector", "scenery") var selector: AssetSelector = browser.get_selector() selector.call("_set_gallery_tab", "backgrounds") await process_frame var grid: GridContainer = selector.find_child("GridContainer", true, false) as GridContainer var plain_card: AssetCard = _find_card(grid, "plain_background") if plain_card != null: await _activate_card(plain_card) var controls: StageSurfaceColorControls = selector.find_child("PlainBackgroundColorControls", true, false) as StageSurfaceColorControls await _activate_button(controls.get("_current_chip") as Button) var picker_view: StageSurfaceColorPickerView = selector.get("_color_picker_view") as StageSurfaceColorPickerView var picker: ColorPicker = picker_view.find_child("ColorPickerControl", true, false) as ColorPicker if picker_view != null else null _set_picker_draft(picker_view, picker, Color("#AC5E31")) var confirmed_hex: String = _stage.call("_canonical_surface_color", _stage.get("_plain_color")) as String selector.hide() await process_frame await process_frame var background: StageBackgroundView = _stage.get("_background_view") as StageBackgroundView var fill: ColorRect = background.get_node("PlainFill") as ColorRect _expect(not bool(_stage.get("_plain_color_draft_active")), "unexpected popup dismissal cancels the custom-color draft") _expect(fill.color.to_html(false).to_upper() == confirmed_hex.trim_prefix("#") and selector.visible, "popup dismissal restores the confirmed fill and keeps the chooser available") browser.close() await process_frame func _test_draft_cancel_on_scene_replacement(starter_data: Dictionary) -> void: var browser: StageAssetBrowser = _stage.get("_asset_browser") as StageAssetBrowser _stage.call("_open_selector", "scenery") var selector: AssetSelector = browser.get_selector() selector.call("_set_gallery_tab", "backgrounds") await process_frame var grid: GridContainer = selector.find_child("GridContainer", true, false) as GridContainer var plain_card: AssetCard = _find_card(grid, "plain_background") if plain_card != null: await _activate_card(plain_card) var controls: StageSurfaceColorControls = selector.find_child("PlainBackgroundColorControls", true, false) as StageSurfaceColorControls await _activate_button(controls.get("_current_chip") as Button) var picker_view: StageSurfaceColorPickerView = selector.get("_color_picker_view") as StageSurfaceColorPickerView var picker: ColorPicker = picker_view.find_child("ColorPickerControl", true, false) as ColorPicker if picker_view != null else null _set_picker_draft(picker_view, picker, Color("#46A45F")) await process_frame var replaced: bool = _stage.call("_apply_scene_data", starter_data, "", false) as bool var background: StageBackgroundView = _stage.get("_background_view") as StageBackgroundView var fill: ColorRect = background.get_node("PlainFill") as ColorRect _expect(replaced and not bool(_stage.get("_plain_color_draft_active")), "scene replacement applies after canceling the custom-color draft") _expect(not selector.visible and String(_stage.get("_background_kind")) == "plain", "scene replacement closes the old selector and uses the new Plain starter") _expect(fill.color.to_html(false).to_upper() == "F5EFE0", "replacement renders the new scene's confirmed starter color") await _save_visual_capture("phase19_plain_offwhite_stage.png") _stage.call("_commit_plain_color", Color("#262B36")) await _save_visual_capture("phase19_plain_dark_stage.png") _stage.call("set_mode", SandboxStage.StageMode.DIRECT) await process_frame await _save_visual_capture("phase19_plain_direct_mode.png") _stage.call("set_mode", SandboxStage.StageMode.PLAY) await process_frame await _save_visual_capture("phase19_plain_play_mode.png") var camera: Camera2D = _stage.get("_camera") as Camera2D _stage.call("set_mode", SandboxStage.StageMode.EDIT) _stage.set("_cli_startup", true) var hovered_control: Control = root.gui_get_hovered_control() var saved_mouse_filter: Control.MouseFilter = hovered_control.mouse_filter if hovered_control != null else Control.MOUSE_FILTER_STOP if hovered_control != null: hovered_control.mouse_filter = Control.MOUSE_FILTER_IGNORE await process_frame camera.global_position = Vector2(0.0, -400.0) camera.zoom = Vector2.ONE * 0.5 var wheel: InputEventMouseButton = InputEventMouseButton.new() wheel.button_index = MOUSE_BUTTON_WHEEL_UP wheel.pressed = true wheel.position = Vector2(root.size) * 0.5 _stage.call("_input", wheel) var pan_press: InputEventMouseButton = InputEventMouseButton.new() pan_press.button_index = MOUSE_BUTTON_MIDDLE pan_press.pressed = true pan_press.position = wheel.position _stage.call("_input", pan_press) var pan_motion: InputEventMouseMotion = InputEventMouseMotion.new() pan_motion.position = pan_press.position + Vector2(80.0, -30.0) pan_motion.relative = Vector2(80.0, -30.0) _stage.call("_input", pan_motion) var pan_release: InputEventMouseButton = InputEventMouseButton.new() pan_release.button_index = MOUSE_BUTTON_MIDDLE pan_release.position = pan_motion.position _stage.call("_input", pan_release) await process_frame _expect(camera.zoom.x > 0.5 and not camera.global_position.is_equal_approx(Vector2(0.0, -400.0)), "plain backdrop remains visible through production zoom and pan input") _stage.set("_cli_startup", false) if hovered_control != null and is_instance_valid(hovered_control): hovered_control.mouse_filter = saved_mouse_filter await _save_visual_capture("phase19_plain_dark_pan_zoom.png") root.size = Vector2i(640, 480) await process_frame await process_frame await _save_visual_capture("phase19_plain_dark_narrow_viewport.png") browser = _stage.get("_asset_browser") as StageAssetBrowser _stage.call("_open_selector", "scenery") selector = browser.get_selector() selector.call("_set_gallery_tab", "backgrounds") await process_frame grid = selector.find_child("GridContainer", true, false) as GridContainer plain_card = _find_card(grid, "plain_background") if plain_card != null: await _activate_card(plain_card) await process_frame var selector_rect: Rect2 = Rect2(Vector2(selector.position), Vector2(selector.size)) var narrow_size: Vector2 = Vector2(root.size) _expect(selector.visible and selector_rect.position.x >= 0.0 and selector_rect.position.y >= 0.0 and selector_rect.end.x <= narrow_size.x and selector_rect.end.y <= narrow_size.y, "Plain color choices stay inside a narrow viewport") await _save_visual_capture("phase19_plain_narrow_choices.png") browser.close() root.size = Vector2i(1280, 720) await process_frame func _test_recent_color_history_limit() -> void: for index: int in range(9): _stage.call("_record_recent_surface_color", Color.from_hsv(float(index) / 9.0, 0.8, 0.9)) var recent: Array[String] = _stage.call("_surface_color_history_strings") as Array[String] _expect(recent.size() == 8, "recent surface color history is capped at eight colors") var newest: String = _stage.call("_canonical_surface_color", Color.from_hsv(8.0 / 9.0, 0.8, 0.9)) as String _expect(not recent.is_empty() and recent[0] == newest, "recent surface color history is most-recent-first") func _valid_document() -> Dictionary: return { "version": "1.7", "scene_name": "Validation fixture", "sky_id": "sky_neutral", "background_kind": "plain", "plain_color": "#F5EFE0", "camera": {"position": {"x": 0.0, "y": -400.0}, "zoom": 0.5}, "objects": [], "event_rules": [], } func _find_card(grid: GridContainer, entry_id: String) -> AssetCard: if grid == null: return null for child: Node in grid.get_children(): var card: AssetCard = child as AssetCard if card != null and String(card.get_meta("entry_id", "")) == entry_id: return card return null func _activate_card(card: AssetCard) -> void: card.activated.emit(card.payload()) await process_frame func _activate_button(button: Button) -> void: if button == null: _expect(false, "expected production button exists") return button.pressed.emit() await process_frame func _press_escape() -> void: var viewport: Viewport = root var press := InputEventKey.new() press.keycode = KEY_ESCAPE press.pressed = true viewport.push_input(press) await process_frame var release := InputEventKey.new() release.keycode = KEY_ESCAPE release.pressed = false viewport.push_input(release) await process_frame func _set_picker_draft(view: StageSurfaceColorPickerView, picker: ColorPicker, color: Color) -> void: picker.color = color view.call("_on_picker_color_changed", color) await process_frame func _save_visual_capture(file_name: String) -> void: if DisplayServer.get_name() == "headless": return await RenderingServer.frame_post_draw var capture: Image = root.get_texture().get_image() var capture_dir: String = ProjectSettings.globalize_path("res://.phase19_captures") DirAccess.make_dir_recursive_absolute(capture_dir) var error: Error = capture.save_png(capture_dir.path_join(file_name)) _expect(error == OK, "rendered capture saved: %s" % file_name)