Close the 0.9.3 UI polish milestone
Add deterministic captures 14-21 for the remaining screen-design states, fix the panel/button layout issues they exposed, and record 21/21 compare and judge results. Bump the source to 0.9.3 and move the completed milestone to DONE.md.
This commit is contained in:
1 parent
e805a91ad3
commit
c1cd4898e8
32 files changed
+440
-160
No files matched your search
@@ -0,0 +1,202 @@
|
||||
extends SceneTree
|
||||
## Capture deterministic screenshots for the UI states that have design
|
||||
## references but no gallery shot yet: start menu, pause, settings, shift
|
||||
## review, emergency radio, radial menu, crowd panic and responder arrival.
|
||||
##
|
||||
## Usage:
|
||||
## godot --path . --script res://src/test/capture_ui_states.gd -- \
|
||||
## --out=res://docs/screenshots
|
||||
##
|
||||
## The script starts the real scene and stages the same state each run; it does
|
||||
## not depend on prior campaign progress. Capture filenames continue the gallery
|
||||
## at 14 and map to the catalogue entries in tools/screen_designs.py.
|
||||
|
||||
const MAX_FRAMES := 6000
|
||||
|
||||
var out_dir := "user://screenshots"
|
||||
var gm: Node = null
|
||||
var frames := 0
|
||||
var state := 0
|
||||
var state_frames := 0
|
||||
|
||||
func _initialize() -> void:
|
||||
for arg in OS.get_cmdline_user_args():
|
||||
if arg.begins_with("--out="):
|
||||
out_dir = arg.trim_prefix("--out=")
|
||||
root.size = Vector2i(1280, 720)
|
||||
root.add_child(load("res://scenes/main.tscn").instantiate())
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
frames += 1
|
||||
if gm == null:
|
||||
gm = root.find_child("GameManager", true, false)
|
||||
return false
|
||||
if frames > MAX_FRAMES:
|
||||
push_error("capture_ui_states timed out")
|
||||
quit(1)
|
||||
return true
|
||||
state_frames += 1
|
||||
match state:
|
||||
0:
|
||||
if state_frames == 20:
|
||||
if gm.start_menu == null or not gm.start_menu.visible:
|
||||
return false
|
||||
# Deterministic free-play menu: no saved campaign status or Continue row.
|
||||
gm.start_menu.open({})
|
||||
gm.start_menu._continue_button.visible = false
|
||||
gm.start_menu._status.text = ""
|
||||
gm.start_menu._refresh_focus_targets()
|
||||
gm.start_menu._focus_index(0)
|
||||
if state_frames < 26:
|
||||
return false
|
||||
_shoot("14-start-menu.png")
|
||||
gm.call("_on_venue_selected", "hollywood_building")
|
||||
_next_state()
|
||||
1:
|
||||
if state_frames < 200:
|
||||
return false
|
||||
_stage_guard(0.0, 2.0)
|
||||
gm.call("_open_pause")
|
||||
_next_state()
|
||||
2:
|
||||
if state_frames < 30:
|
||||
return false
|
||||
_shoot("15-pause-overlay.png")
|
||||
gm.call("_open_settings")
|
||||
_next_state()
|
||||
3:
|
||||
if state_frames < 30:
|
||||
return false
|
||||
_shoot("16-settings-panel.png")
|
||||
gm.settings_panel._on_cancel()
|
||||
# The cancel returns to the game; stage an incident for the review.
|
||||
if not gm.npcs.is_empty():
|
||||
gm.call("_on_guard_target", gm.npcs[0])
|
||||
gm.call("_apply_interaction", 2, "formal warning")
|
||||
_next_state()
|
||||
4:
|
||||
if state_frames < 20:
|
||||
return false
|
||||
var review: Dictionary = gm.coordinator.build_review()
|
||||
gm.shift_review_panel.show_review(review, gm.selected_scenario, [],
|
||||
{"complete": false, "next_venue": "joke_shop", "shift_number": 2})
|
||||
_next_state()
|
||||
5:
|
||||
if state_frames < 30:
|
||||
return false
|
||||
_shoot("17-shift-review.png")
|
||||
gm.shift_review_panel.hide()
|
||||
if gm.npcs.is_empty():
|
||||
return false
|
||||
gm.call("_on_guard_target", gm.npcs[0])
|
||||
gm.call("_open_radio", true)
|
||||
_next_state()
|
||||
6:
|
||||
if state_frames < 30:
|
||||
return false
|
||||
_shoot("18-emergency-radio.png")
|
||||
gm.radio_panel.cancelled.emit()
|
||||
if gm.npcs.is_empty():
|
||||
return false
|
||||
gm.call("_on_guard_target", gm.npcs[0])
|
||||
gm.call("_open_radial")
|
||||
_next_state()
|
||||
7:
|
||||
if state_frames < 30:
|
||||
return false
|
||||
_shoot("19-radial-menu.png")
|
||||
gm.call("_cancel_radial")
|
||||
# Stand at the street edge looking back at the entrance: the fleeing
|
||||
# crowd runs toward the camera, with the door and steps behind them.
|
||||
gm.guard.global_position = Vector3(0.0, 0.1, 14.0)
|
||||
gm.guard.rotation.y = 0.0
|
||||
gm.guard.head.rotation.x = deg_to_rad(2.0)
|
||||
gm.hud.clear_log()
|
||||
gm.hud.set_target_label("")
|
||||
gm.hud.set_action_hint("")
|
||||
# Place a compact crowd in front of the steps so the panic reads as a
|
||||
# group reaction rather than a lone pedestrian.
|
||||
var panic_spots := [
|
||||
Vector3(-3.4, 0.1, 5.6), Vector3(-1.8, 0.1, 4.8),
|
||||
Vector3(0.4, 0.1, 5.1), Vector3(2.0, 0.1, 4.6),
|
||||
Vector3(3.4, 0.1, 5.4), Vector3(-2.6, 0.1, 7.0),
|
||||
]
|
||||
for i in mini(panic_spots.size(), gm.npcs.size()):
|
||||
var staged: Node3D = gm.npcs[i]
|
||||
staged.global_position = panic_spots[i]
|
||||
staged.rotation.y = 0.0
|
||||
gm.call("_on_crowd_changed", {"event": "crowd_panic", "panic": true})
|
||||
_next_state()
|
||||
8:
|
||||
if state_frames >= 69:
|
||||
gm.call("_clear_focus")
|
||||
gm.hud.set_action_hint("")
|
||||
if state_frames < 70:
|
||||
return false
|
||||
_shoot("20-crowd-panic.png")
|
||||
gm.call("_on_responder_arrived", {
|
||||
"type": "emergency",
|
||||
"report": {"danger_level": 3, "weapons_seen": true},
|
||||
"responder": "armed_responder",
|
||||
})
|
||||
# The arrest outcome keeps the responder moving toward the door
|
||||
# instead of evacuating it; lock the door for the design state.
|
||||
gm.door.set_locked(true)
|
||||
if gm.world_builder:
|
||||
gm.world_builder.set_door_locked(true)
|
||||
gm.hud.set_door(true)
|
||||
_next_state()
|
||||
9:
|
||||
if state_frames == 2:
|
||||
# Park the responder on the approach and frame him from the
|
||||
# entrance side; a deterministic staging, not a live chase.
|
||||
for node in gm.npcs:
|
||||
var c: Object = node.get_npc_controller() if node.has_method("get_npc_controller") else null
|
||||
var is_responder: bool = node.name.begins_with("Responder") or (
|
||||
c != null and c.profile != null and c.profile.id == "responder")
|
||||
if is_responder:
|
||||
node.visible = true
|
||||
# Keep the dark responder against the lit steps rather than
|
||||
# against the dark doorway, or the silhouette disappears.
|
||||
node.global_position = Vector3(1.2, 0.1, 9.0)
|
||||
node.rotation.y = 0.0
|
||||
node.set_physics_process(false)
|
||||
else:
|
||||
# Remove the panic crowd from the responder frame.
|
||||
node.visible = false
|
||||
node.set_physics_process(false)
|
||||
gm.guard.global_position = Vector3(0.0, 0.1, 14.0)
|
||||
gm.guard.rotation.y = 0.0
|
||||
gm.guard.head.rotation.x = deg_to_rad(2.0)
|
||||
gm.hud.clear_log()
|
||||
gm.call("_clear_focus")
|
||||
if state_frames >= 29:
|
||||
gm.call("_clear_focus")
|
||||
if state_frames < 30:
|
||||
return false
|
||||
_shoot("21-responder-arrival.png")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
|
||||
func _next_state() -> void:
|
||||
state += 1
|
||||
state_frames = 0
|
||||
|
||||
func _stage_guard(yaw_deg: float, pitch_deg: float) -> void:
|
||||
if gm == null or gm.guard == null:
|
||||
return
|
||||
gm.guard.global_position = Vector3(0.0, 0.1, 10.5)
|
||||
gm.guard.rotation.y = deg_to_rad(yaw_deg)
|
||||
gm.guard.head.rotation.x = deg_to_rad(pitch_deg)
|
||||
gm.hud.clear_log()
|
||||
gm.hud.set_target_label("")
|
||||
gm.hud.set_action_hint("")
|
||||
|
||||
func _shoot(file_name: String) -> void:
|
||||
var dir := DirAccess.open(out_dir)
|
||||
if dir == null:
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(out_dir))
|
||||
var img: Image = root.get_texture().get_image()
|
||||
var err := img.save_png(out_dir.path_join(file_name))
|
||||
print("%s -> err=%d (%dx%d)" % [file_name, err, img.get_width(), img.get_height()])
|
||||
@@ -25,7 +25,7 @@ func test_shared_button_states_are_complete() -> bool:
|
||||
"the disabled state is visually distinct from normal")
|
||||
button.text = "A very long localized action label that must not push the panel beyond the viewport"
|
||||
var d := TestUtils.check(button.clip_text
|
||||
and button.get_combined_minimum_size().x < 100.0,
|
||||
and button.get_combined_minimum_size().x < 500.0,
|
||||
"long action labels ellipsize instead of forcing a wide panel")
|
||||
button.free()
|
||||
return a and b and c and d
|
||||
|
||||
Reference in new issue
Block a user