Add StageBackgroundView and related tests for plain background functionality

- Implemented StageBackgroundView class for managing sky and plain backgrounds in the stage.
- Added functionality to set background type and color, bind camera, and apply themes.
- Created test_phase19_plain_background.gd to validate plain background behavior and migration.
- Developed test_phase19_starter_chooser_layout.gd to ensure proper layout and functionality of the starter chooser.
- Included visual capture tests for UI elements and interactions.
This commit is contained in:
2026-10-22 16:18:25 -04:00
parent db677dc194
commit 59f1817063
43 changed files with 2403 additions and 160 deletions
+5 -2
View File
@@ -46,7 +46,7 @@ func _test_migration_and_schema() -> void:
var legacy: Dictionary = {"version": version, "objects": [{"file_id": "prop_a", "type": "prop"}]}
var migrated: Dictionary = STAGE_SCENE_IO._migrate_to_current(legacy)
var entry: Dictionary = migrated["objects"][0]
_expect(String(migrated["version"]) == "1.3" and entry.get("use_spots") == [] and int(entry.get("next_use_spot_number", 0)) == 1, "%s prop receives v1.3 empty Use spot defaults" % version)
_expect(String(migrated["version"]) == STAGE_SCENE_IO.FILE_VERSION and entry.get("use_spots") == [] and int(entry.get("next_use_spot_number", 0)) == 1, "%s prop receives current-format empty Use spot defaults" % version)
var valid_root: Dictionary = _valid_root()
_expect(bool(STAGE_SCENE_IO._validate_root(valid_root).get("ok", false)), "valid Use spot data passes root validation")
var duplicate: Dictionary = _valid_root()
@@ -176,7 +176,10 @@ func _on_use_failed(reason_code: String, _prop: PropBlock, _spot_id: String) ->
func _valid_root() -> Dictionary:
return {
"version": "1.3",
"version": STAGE_SCENE_IO.FILE_VERSION,
"sky_id": "sky_neutral",
"background_kind": "sky",
"plain_color": STAGE_SCENE_IO.DEFAULT_PLAIN_COLOR,
"objects": [{
"file_id": "prop_file",
"type": "prop",
+430
View File
@@ -0,0 +1,430 @@
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)
@@ -0,0 +1 @@
uid://iaplw44v1huj
+9 -8
View File
@@ -12,6 +12,7 @@ const STARTER_CARD_PREVIEW: Script = preload("res://scripts/stage/scenery/stage_
const STAGE_SKY_VIEW_SCRIPT: Script = preload("res://scripts/stage/scenery/stage_sky_view.gd")
const STARTER_PATHS: Array[String] = [
"res://content/stages/starters/blank.scene",
"res://content/stages/starters/plain_background.scene",
"res://content/stages/starters/town_street.scene",
"res://content/stages/starters/living_room.scene",
]
@@ -90,7 +91,7 @@ func _run() -> void:
)
var browser: StageAssetBrowser = _stage.get("_asset_browser") as StageAssetBrowser
var starter_entries: Array[Dictionary] = browser.scan_entries("starters")
_expect(starter_entries.size() == 3, "New Scene gallery lists Blank, Town Street, and Living Room")
_expect(starter_entries.size() == 4, "New Scene gallery lists Plain Background, Open Sky, Town Street, and Living Room")
var original_name: String = String(_stage.get("_scene_name"))
var original_world_count: int = (_stage.get_node("World") as Node2D).get_child_count()
var original_sky: String = String(_stage.get("_sky_id"))
@@ -107,7 +108,7 @@ func _run() -> void:
selector = browser.get_selector()
var starter_grid: GridContainer = selector.find_child("GridContainer", true, false) as GridContainer
await _test_starter_card_previews(starter_grid)
var town_card: AssetCard = starter_grid.get_child(1) as AssetCard if starter_grid != null and starter_grid.get_child_count() > 1 else null
var town_card: AssetCard = _find_card_by_id(starter_grid, "starter_town_street")
_expect(town_card != null, "Town Street is selectable as a real starter card")
if town_card != null:
await _activate_card(town_card)
@@ -212,16 +213,16 @@ func _run() -> void:
for preview_node: Node in card_content.get_children():
if preview_node.get_script() == BACKGROUND_CARD_PREVIEW:
background_preview_count += 1
_expect(background_grid != null and background_grid.get_child_count() == 6 and background_preview_count == 6, "four Sky looks and both surface setup cards render production-style previews")
_expect(background_grid != null and background_grid.get_child_count() == 7 and background_preview_count == 7, "Plain Background, four Sky looks, and both surface setup cards render production-style previews")
var cloudy_status: Label = cloudy_card.find_child("StatusLabel", true, false) as Label if cloudy_card != null else null
_expect(cloudy_card != null and cloudy_card.button_pressed and cloudy_status != null and cloudy_status.text == "Current Sky", "gallery marks the loaded Town Street Sky as Current Sky")
_expect(cloudy_card != null and cloudy_card.button_pressed and cloudy_status != null and cloudy_status.text == "Current background", "gallery marks the loaded Town Street Sky as the current background")
var clear_sky_card: AssetCard = _find_card_by_id(background_grid, "sky_clear_day")
if clear_sky_card != null:
await _activate_card(clear_sky_card)
var selected_clear_card: AssetCard = _find_card_by_id(selector.find_child("GridContainer", true, false) as GridContainer, "sky_clear_day")
var clear_status: Label = selected_clear_card.find_child("StatusLabel", true, false) as Label if selected_clear_card != null else null
_expect(selector.visible and String(_stage.get("_sky_id")) == "sky_clear_day", "choosing a Sky updates the Stage and keeps the chooser open")
_expect(selected_clear_card != null and selected_clear_card.button_pressed and clear_status != null and clear_status.text == "Current Sky", "Current Sky marker follows the newly chosen card")
_expect(selected_clear_card != null and selected_clear_card.button_pressed and clear_status != null and clear_status.text == "Current background", "current background marker follows the newly chosen card")
selector.call("_set_gallery_tab", "artwork")
await process_frame
var artwork_entries: Array[Dictionary] = browser.scan_entries("scenery")
@@ -614,7 +615,7 @@ func _test_starter_card_previews(starter_grid: GridContainer) -> void:
for card_node: Node in starter_grid.get_children():
if not card_node.is_queued_for_deletion():
live_card_count += 1
_expect(live_card_count == 3, "starter chooser has three scene cards to preview")
_expect(live_card_count == 4, "starter chooser has four scene cards to preview")
var preview_count: int = 0
var populated_scene_count: int = 0
for card_node: Node in starter_grid.get_children():
@@ -628,8 +629,8 @@ func _test_starter_card_previews(starter_grid: GridContainer) -> void:
if int(preview.call("get_loaded_object_count")) > 0:
populated_scene_count += 1
var viewport: SubViewport = preview.get_child(0) as SubViewport if preview.get_child_count() > 0 else null
_expect(viewport != null and preview.find_child("SkyView", true, false) != null, "starter preview has an isolated viewport and sky renderer")
_expect(preview_count == 3, "all three starter cards use scene previews instead of placeholder tiles")
_expect(viewport != null and preview.find_child("StageSkyView", true, false) != null, "starter preview has an isolated viewport and shared backdrop renderer")
_expect(preview_count == 4, "all four starter cards use scene previews instead of placeholder tiles")
_expect(populated_scene_count == 2, "Town Street and Living Room previews load their scene objects")
@@ -0,0 +1,144 @@
extends SceneTree
const SELECTOR_SCENE: PackedScene = preload("res://scenes/stage/asset_selector.tscn")
const SANDBOX_THEME_SCRIPT: Script = preload("res://scripts/stage/sandbox_theme.gd")
var _checks: int = 0
var _failures: int = 0
var _selector: AssetSelector
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
root.size = Vector2i(1280, 720)
_selector = SELECTOR_SCENE.instantiate() as AssetSelector
root.add_child(_selector)
await process_frame
var stage_theme: SandboxTheme = SANDBOX_THEME_SCRIPT.new() as SandboxTheme
stage_theme.load_from("res://sandbox_theme.json")
_selector.apply_theme(stage_theme)
_selector.open("starters", _starter_entries())
await process_frame
await process_frame
_test_roomy_layout()
await _save_capture("phase19_starter_chooser_wide.png")
await _test_hover_style_and_capture()
await _test_narrow_layout()
_selector.hide()
_selector.queue_free()
await process_frame
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 _starter_entries() -> Array[Dictionary]:
return [
{
"id": "starter_plain_background",
"name": "Plain Background",
"description": "An empty stage with one color. Choose a new color whenever you like.",
"path": "res://content/stages/starters/plain_background.scene",
},
{
"id": "starter_blank_scene",
"name": "Open Sky",
"description": "An empty stage with a soft sky. Sun and clouds, ready for your story.",
"path": "res://content/stages/starters/blank.scene",
},
{
"id": "starter_town_street",
"name": "Town Street",
"description": "Cloudy sky, shops, trees, sidewalk, and road.",
"path": "res://content/stages/starters/town_street.scene",
},
{
"id": "starter_living_room",
"name": "Living Room",
"description": "A cozy room with a window, rug, and furniture.",
"path": "res://content/stages/starters/living_room.scene",
},
]
func _test_roomy_layout() -> void:
var grid: GridContainer = _grid()
var close_button: Button = _selector.find_child("CloseButton", true, false) as Button
_expect(_selector.size.x == 1160, "roomy starter chooser uses the preferred 1160-pixel width")
_expect(int(_selector.get("_active_columns")) == 2 and grid != null and grid.columns == 2, "roomy chooser presents two card columns")
_expect(close_button != null and close_button.visible and close_button.custom_minimum_size.y >= 44.0, "roomy chooser keeps its visible Close action reachable")
var first_card: Control = grid.get_child(0) as Control if grid != null and grid.get_child_count() > 0 else null
var preview: Control = first_card.find_child("StarterScenePreview", true, false) as Control if first_card != null else null
var viewport: SubViewport = preview.get_child(0) as SubViewport if preview != null and preview.get_child_count() > 0 else null
var preview_aspect: float = float(viewport.size.x) / float(viewport.size.y) if viewport != null and viewport.size.y > 0 else 0.0
_expect(first_card != null and first_card.custom_minimum_size.x >= 500.0, "roomy starter cards expand to at least 500 pixels")
_expect(preview != null and preview.custom_minimum_size.x >= 500.0 and preview.custom_minimum_size.y >= 240.0, "roomy previews meet the approved dimensions")
_expect(viewport != null and viewport.size.x >= 500 and viewport.size.y >= 240 and preview_aspect >= 1.9 and preview_aspect <= 2.2, "preview render target preserves the wide scene shape")
var title: Label = _selector.find_child("TitleLabel", true, false) as Label
_expect(title != null and title.visible and title.text == "Choose a starter scene", "roomy chooser keeps its readable title")
func _test_narrow_layout() -> void:
root.size = Vector2i(550, 690)
await process_frame
_selector.call("_update_popup_layout")
_selector.call("_rebuild")
await process_frame
await process_frame
var grid: GridContainer = _grid()
var close_button: Button = _selector.find_child("CloseButton", true, false) as Button
_expect(_selector.size.x <= root.size.x - 24, "narrow chooser stays within the safe screen margins")
_expect(int(_selector.get("_active_columns")) == 1 and grid != null and grid.columns == 1, "narrow chooser switches to a single card column")
_expect(int(_selector.get("_page_capacity")) == 4 and grid != null and grid.get_child_count() == 4, "narrow chooser keeps all four cards in the scrollable body")
_expect(close_button != null and close_button.visible, "narrow chooser keeps Close visible outside the scrolling body")
var first_card: Control = grid.get_child(0) as Control if grid != null and grid.get_child_count() > 0 else null
var preview: Control = first_card.find_child("StarterScenePreview", true, false) as Control if first_card != null else null
_expect(preview != null and preview.custom_minimum_size.x >= 400.0 and preview.custom_minimum_size.y >= 200.0, "narrow single-column layout retains large previews")
await _save_capture("phase19_starter_chooser_narrow.png")
func _test_hover_style_and_capture() -> void:
var grid: GridContainer = _grid()
var hover_card: Button = grid.get_child(1) as Button if grid != null and grid.get_child_count() > 1 else null
if hover_card == null:
_expect(false, "starter card is available for hover preview")
return
var hover_style: StyleBoxFlat = hover_card.get_theme_stylebox("hover") as StyleBoxFlat
_expect(hover_style != null and hover_style.border_color.is_equal_approx(Color("#22C6FF")), "starter hover style uses the EDIT blue border")
if hover_style == null:
return
var normal_style: StyleBox = hover_card.get_theme_stylebox("normal")
hover_card.add_theme_stylebox_override("normal", hover_style)
await process_frame
await _save_capture("phase19_starter_chooser_hover.png")
hover_card.add_theme_stylebox_override("normal", normal_style)
func _grid() -> GridContainer:
return _selector.find_child("GridContainer", true, false) as GridContainer
func _expect(condition: bool, message: String) -> void:
_checks += 1
if condition:
print("PASS: " + message)
else:
_failures += 1
push_error("Starter chooser layout check failed: " + message)
func _save_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, "capture saved for visual inspection: %s" % file_name)
@@ -0,0 +1 @@
uid://dp47aexhannrg
+3 -2
View File
@@ -58,8 +58,9 @@ func _test_v15_migration() -> void:
]
}
var migrated: Dictionary = STAGE_SCENE_IO._migrate_to_current(source)
_expect(String(migrated.get("version", "")) == "1.6", "v1.5 scenes migrate to v1.6")
_expect(String(migrated.get("version", "")) == "1.7", "v1.5 scenes migrate to v1.7")
_expect(String(migrated.get("sky_id", "")) == "sky_cloudy_day", "v1.5 migration preserves the saved Sky")
_expect(String(migrated.get("background_kind", "")) == "sky" and String(migrated.get("plain_color", "")) == STAGE_SCENE_IO.DEFAULT_PLAIN_COLOR, "v1.5 migration adds the remembered Plain Background defaults")
var objects: Array = migrated.get("objects", []) as Array
_expect(String((objects[0] as Dictionary).get("finish_id", "")) == "finish_ground_sidewalk", "v1.5 migration preserves walkable terrain finishes")
var migrated_brick_color: String = String((objects[1] as Dictionary).get("color", "")).to_upper()
@@ -157,7 +158,7 @@ func _test_background_ground_model_and_serialization(world: Node2D, surface: Sta
_expect(selection.hit_test(surface.global_position) == surface, "Edit Scenery hit-testing can select background ground")
var data: Dictionary = _stage.call("_collect_scene_data", false)
var validation: Dictionary = STAGE_SCENE_IO._validate_root(data)
_expect(String(data.get("version", "")) == "1.6" and bool(validation.get("ok", false)), "background ground serializes and validates in the current scene schema")
_expect(String(data.get("version", "")) == STAGE_SCENE_IO.FILE_VERSION and bool(validation.get("ok", false)), "background ground serializes and validates in the current scene schema")
var found: Dictionary = {}
for entry_value: Variant in data.get("objects", []):
if entry_value is Dictionary and String((entry_value as Dictionary).get("type", "")) == "background_ground":