Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
629 lines
35 KiB
GDScript
629 lines
35 KiB
GDScript
extends SceneTree
|
|
const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd")
|
|
|
|
## Real Runtime commands through the staff panel controls, including queued
|
|
## assignment, draft preservation, a rejected role, and pending Save restoration.
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _main: Control
|
|
var _panel: StaffManagementPanel
|
|
var _runtime: Variant
|
|
var _clock: Variant
|
|
var _receptions: Array[StringName] = []
|
|
var _kitchen: StringName = &""
|
|
var _save_path := ""
|
|
|
|
|
|
func _test_staff_layer_skin() -> void:
|
|
_main.call("_set_layer", 0)
|
|
var front_color := _panel.staff_detail_label.get_theme_color("font_color")
|
|
_panel.select_staff(&"staff_akane")
|
|
var before: Dictionary = _runtime.staff_service.get_work_assignment(&"staff_akane")
|
|
_main.call("_set_layer", 1)
|
|
_panel.refresh()
|
|
_expect(_panel.staff_detail_label.get_theme_color("font_color") == Color("d8eeea") and _panel.summary_label.get_theme_color("font_color") == Color("d8eeea"), "staff text stays readable in BACK after refresh")
|
|
_panel.call("_set_result", "配属できません", false)
|
|
_expect(_panel.result_label.get_theme_color("font_color") == Color("ffb498"), "BACK rejection retains a readable warning colour")
|
|
_main.call("_set_layer", 0)
|
|
_expect(_panel.staff_detail_label.get_theme_color("font_color") == front_color and _panel.result_label.get_theme_color("font_color") == Color("a7402e"), "FRONT restores staff text and rejection colours")
|
|
_main.call("_set_layer", 1)
|
|
_panel.call("_set_result", "配属を確認しました", true)
|
|
_expect(_panel.result_label.get_theme_color("font_color") == Color("b5e7ba"), "BACK success retains distinct readable colour")
|
|
_expect(_runtime.staff_service.get_work_assignment(&"staff_akane") == before, "skin changes do not submit an assignment")
|
|
if OS.get_environment("THE_TREE_STAFF_SKIN_CAPTURE") == "1":
|
|
await process_frame;await RenderingServer.frame_post_draw
|
|
_expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path("res://artifacts/facility-dimensions-2026-09-29/staff-skin-back.png")) == OK, "capture staff BACK skin")
|
|
_main.call("_set_layer", 0)
|
|
_panel.call("_set_result", "", true)
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
_clock = root.get_node("GameClock")
|
|
_runtime = root.get_node("SimulationRuntime")
|
|
var state: Variant = root.get_node("GameState")
|
|
_clock.reset(1, 6, 0)
|
|
state.reset()
|
|
_runtime.initialize(_clock, state, 824242)
|
|
_clock.set_paused(false)
|
|
root.size = Vector2i(1280, 720)
|
|
_main = load("res://scenes/main/main.tscn").instantiate() as Control
|
|
root.add_child(_main)
|
|
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
_expect(_clock.paused, "a truly new opening pauses before the first construction")
|
|
await process_frame
|
|
await process_frame
|
|
_panel = _main.find_child("StaffManagementPanel", true, false) as StaffManagementPanel
|
|
_expect(_panel != null, "normal UI includes a separate staff management panel")
|
|
if _panel == null:
|
|
_finish()
|
|
return
|
|
for request: Array in [
|
|
[&"reception", Vector3i(-8, 0, 0)],
|
|
[&"reception", Vector3i(2, 0, 0)],
|
|
[&"kitchen", Vector3i(2, 0, 1)],
|
|
]:
|
|
_main.call("_select_build_definition", request[0])
|
|
_main.call("_on_map_cell_clicked", request[1])
|
|
_clock.advance_minutes(180)
|
|
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
|
if facility.definition_id == &"reception":
|
|
_receptions.append(facility.instance_id)
|
|
elif facility.definition_id == &"kitchen":
|
|
_kitchen = facility.instance_id
|
|
_expect(_receptions.size() == 2 and not _kitchen.is_empty(), "three real facilities are ready for assignment tests")
|
|
if _receptions.size() != 2 or _kitchen.is_empty():
|
|
_finish()
|
|
return
|
|
_main.call("_unhandled_key_input", _key(KEY_H))
|
|
_expect((_main.get("staff_candidate_panel") as Control).visible, "H opens the unchanged hiring candidates")
|
|
for candidate: StringName in [&"staff_akane", &"staff_bardo"]:
|
|
var hire := (_main.get("staff_candidate_buttons") as Dictionary).get(candidate) as Button
|
|
hire.pressed.emit()
|
|
(_main.get("staff_management_toggle_button") as Button).pressed.emit()
|
|
_expect(_panel.visible and not (_main.get("staff_candidate_panel") as Control).visible,
|
|
"hired-staff tab replaces the candidates without hiding the management panel")
|
|
_expect(_panel.roster_buttons.size() == 2 and "未配属 2人" in _panel.summary_label.text,
|
|
"roster shows both real employees and unassigned count")
|
|
await _test_staff_layer_skin()
|
|
await _test_drag_assignment()
|
|
await _test_drag_rejection_boundaries()
|
|
await _test_drag_state_change()
|
|
await _test_drag_across_load()
|
|
await _test_drag_target_demolition()
|
|
await _test_drag_defense_lock()
|
|
await _test_drag_defense_lock(false)
|
|
_panel.select_staff(&"staff_akane")
|
|
_choose_facility(_receptions[0])
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0],
|
|
"paused Apply assigns the receptionist to the chosen completed facility")
|
|
_expect(_runtime.staff_service.get_staff(&"staff_akane").state == StaffRuntime.State.COMMUTING
|
|
and _runtime.facility_service.get_instance(_receptions[0]).active_staff_ids.is_empty(),
|
|
"paused Apply visibly starts a commute and does not invent facility attendance")
|
|
_expect("出勤中" in _panel.staff_detail_label.text, "staff panel distinguishes physical commuting from active duty")
|
|
_expect("変更しました" in _panel.result_label.text and "毎日" in _panel.staff_detail_label.text,
|
|
"assignment success and recurring schedule are visible in Japanese")
|
|
_test_rejected_assignment()
|
|
_test_queued_assignment_preserves_other_draft()
|
|
_test_save_pending_assignment()
|
|
_test_guide_respects_current_assignments()
|
|
await _test_navigation_and_layout()
|
|
await _test_restored_opening_does_not_pause()
|
|
_finish()
|
|
|
|
|
|
func _test_rejected_assignment() -> void:
|
|
var before: Dictionary = _assignment(&"staff_akane").duplicate(true)
|
|
_choose_facility(_kitchen)
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(_assignment(&"staff_akane") == before, "wrong-role assignment never mutates the existing reception schedule")
|
|
_expect("役割" in _panel.result_label.text and "役割" in (_main.get("build_status_label") as Label).text,
|
|
"rejected assignment shows its actionable Japanese reason in both panel and status")
|
|
_expect(_chosen_facility() == _kitchen, "a rejected assignment preserves the attempted facility for correction")
|
|
|
|
|
|
func _test_queued_assignment_preserves_other_draft() -> void:
|
|
_choose_facility(_receptions[1])
|
|
_clock.set_paused(false)
|
|
_panel.apply_button.pressed.emit()
|
|
var queued: Array = _runtime.command_queue.create_snapshot().get("pending", [])
|
|
_expect(queued.size() == 1 and _panel.apply_button.disabled, "running-clock Apply queues one assignment and disables repeated submission")
|
|
_panel.submit_assignment()
|
|
_expect((_runtime.command_queue.create_snapshot().get("pending", []) as Array).size() == 1,
|
|
"repeated submit cannot create a second assignment for the same employee")
|
|
_panel.select_staff(&"staff_bardo")
|
|
_choose_facility(_receptions[0])
|
|
_panel.shift_option.select(1)
|
|
_panel.shift_option.item_selected.emit(1)
|
|
_clock.advance_minutes(1)
|
|
_clock.set_paused(true)
|
|
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[1], "queued reassignment commits on the command tick")
|
|
_expect(_panel.selected_staff_id == &"staff_bardo" and _chosen_facility() == _receptions[0]
|
|
and _panel.shift_option.selected == 1, "another employee's pending completion preserves the active facility and night-shift draft")
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(_assignment(&"staff_bardo").get(&"template_id") == &"NIGHT", "night selector creates an actual recurring night schedule")
|
|
_expect("18:00" in _panel.result_label.text and "反映" in _panel.staff_detail_label.text,
|
|
"future night shift clearly reports when it begins")
|
|
_panel.select_staff(&"staff_akane")
|
|
_choose_facility(&"")
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(StringName(_assignment(&"staff_akane").get(&"facility_id", &"missing")).is_empty(),
|
|
"unassigned option removes the facility duty through Runtime")
|
|
|
|
|
|
func _test_save_pending_assignment() -> void:
|
|
_choose_facility(_receptions[0])
|
|
_clock.set_paused(false)
|
|
_panel.apply_button.pressed.emit()
|
|
_save_path = "user://tests/staff-management-%d" % OS.get_process_id()
|
|
var saved: Dictionary = _runtime.save_to_path(_save_path)
|
|
_expect(bool(saved.get(&"ok", false)), "pending UI assignment saves to an isolated test slot")
|
|
_clock.advance_minutes(1)
|
|
var restored: Dictionary = _runtime.load_from_path(_save_path)
|
|
_expect(bool(restored.get(&"ok", false)), "pending UI assignment can be restored")
|
|
_panel.refresh()
|
|
_expect(_panel.apply_button.disabled and not (_panel.get("_pending_commands") as Dictionary).is_empty(),
|
|
"restored command metadata restores disabled pending controls")
|
|
var sequence_before := int(_panel.get("_sequence"))
|
|
_clock.advance_minutes(1)
|
|
_clock.set_paused(true)
|
|
_panel.refresh()
|
|
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0]
|
|
and (_panel.get("_pending_commands") as Dictionary).is_empty(),
|
|
"restored assignment completes once and releases its UI pending state")
|
|
_choose_facility(_receptions[1])
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(int(_panel.get("_sequence")) > sequence_before
|
|
and _assignment(&"staff_akane").get(&"facility_id") == _receptions[1],
|
|
"post-load Apply uses a fresh command key and changes the real assignment")
|
|
|
|
|
|
func _test_guide_respects_current_assignments() -> void:
|
|
# Remove both saved plans so an unrelated receptionist cannot satisfy the
|
|
# guide while this employee's assignment is being changed.
|
|
for staff_id: StringName in [&"staff_akane", &"staff_bardo"]:
|
|
_panel.select_staff(staff_id)
|
|
_choose_facility(&"")
|
|
_panel.apply_button.pressed.emit()
|
|
_build_guide_part(&"standard_room", Vector3i(-8, 2, 0))
|
|
_build_guide_part(&"standard_room", Vector3i(-6, 2, 0))
|
|
_build_guide_part(&"guest_stairs", Vector3i(-4, 1, 0))
|
|
_clock.advance_minutes(60)
|
|
var incomplete_guide := _main.find_child("InnOpeningGuide", true, false) as InnOpeningGuide
|
|
_expect(incomplete_guide.get_progress().get(&"stage") == &"access",
|
|
"incomplete stairs cannot advance guidance to staffing")
|
|
_build_guide_part(&"guest_stairs", Vector3i(-4, 2, 0))
|
|
_clock.advance_minutes(10)
|
|
for x: int in range(-8, -3):
|
|
_build_guide_part(&"guest_stairs" if x == -4 else &"guest_corridor", Vector3i(x, 3, 0))
|
|
_clock.advance_minutes(10)
|
|
var accessible_room := false
|
|
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
|
accessible_room = accessible_room or (facility.definition_id == &"standard_room" and facility.access_available)
|
|
var guide := _main.find_child("InnOpeningGuide", true, false) as InnOpeningGuide
|
|
_expect(accessible_room and guide.get_progress().get(&"stage") == &"staff",
|
|
"real accessible room still requires a reception assignment before first sale")
|
|
_main.call("_activate_management_mode", &"staff")
|
|
_panel.select_staff(&"staff_akane")
|
|
_choose_facility(_receptions[0])
|
|
_panel.shift_option.select(0)
|
|
_panel.shift_option.item_selected.emit(0)
|
|
_panel.apply_button.pressed.emit()
|
|
_expect(guide.get_progress().get(&"stage") == &"sale",
|
|
"day assignment advances the accessible inn to waiting for its first sale")
|
|
_choose_facility(&"")
|
|
_panel.apply_button.pressed.emit()
|
|
var historical_reception_shift := false
|
|
for shift: ShiftInstance in _runtime.staff_service.get_shifts():
|
|
if shift.staff_id == &"staff_akane" and shift.assigned_facility_id == _receptions[0]:
|
|
historical_reception_shift = historical_reception_shift or shift.cancelled or shift.end_at <= _clock.absolute_minute
|
|
_expect(historical_reception_shift and guide.get_progress().get(&"stage") == &"staff",
|
|
"unassigning reception returns guidance to staffing despite past or cancelled reception shifts")
|
|
_choose_facility(_receptions[0])
|
|
_panel.shift_option.select(1)
|
|
_panel.shift_option.item_selected.emit(1)
|
|
_panel.apply_button.pressed.emit()
|
|
var night_plan: Dictionary = _assignment(&"staff_akane")
|
|
var reception: FacilityInstance = _runtime.facility_service.get_instance(_receptions[0])
|
|
_expect(night_plan.get(&"template_id") == &"NIGHT" and int(night_plan.get(&"effective_from", -1)) > _clock.absolute_minute
|
|
and reception.active_staff_ids.is_empty(), "night assignment is genuinely future duty with nobody staffing reception during daytime")
|
|
_expect(guide.get_progress().get(&"stage") == &"sale",
|
|
"saved future night duty satisfies staffing guidance before its shift starts")
|
|
|
|
|
|
func _build_guide_part(definition_id: StringName, origin: Vector3i) -> void:
|
|
var before: int = _runtime.construction_service.get_jobs().size()
|
|
_main.call("_select_build_definition", definition_id)
|
|
_main.call("_on_map_cell_clicked", origin)
|
|
_expect(_runtime.construction_service.get_jobs().size() == before + 1,
|
|
"guide fixture builds %s through UI at %s" % [definition_id, origin])
|
|
|
|
|
|
func _test_navigation_and_layout() -> void:
|
|
_main.call("_clear_active_tool")
|
|
_expect(not _panel.visible, "Escape behavior closes the hired staff pane")
|
|
_main.call("_activate_management_mode", &"ledger")
|
|
var ledger := _main.find_child("OperatingLedgerPanel", true, false) as Control
|
|
_expect(ledger.visible and not (_main.get("operations_controls") as Control).visible,
|
|
"ledger navigation displays the actual ledger while hiding assignment controls")
|
|
_main.call("_unhandled_key_input", _key(KEY_H))
|
|
_expect(not ledger.visible and (_main.get("staff_candidate_panel") as Control).visible,
|
|
"H returns from the ledger to hiring candidates")
|
|
_main.call("_activate_management_mode", &"staff")
|
|
await process_frame
|
|
await process_frame
|
|
var guide := _main.find_child("InnOpeningGuide", true, false) as Control
|
|
var side := _main.find_child("SidePanel", true, false) as Control
|
|
_expect(not guide.get_global_rect().intersects(side.get_global_rect()),
|
|
"1280x720 keeps opening guidance and staff inspector apart")
|
|
_expect(_panel.get_global_rect().position.x >= side.get_global_rect().position.x
|
|
and _panel.get_global_rect().end.x <= side.get_global_rect().end.x,
|
|
"staff roster and assignment controls fit the compact sidebar width")
|
|
if OS.get_environment("THE_TREE_STAFF_UI_CAPTURE") == "1":
|
|
await RenderingServer.frame_post_draw
|
|
root.get_texture().get_image().save_png("/tmp/the-tree-staff-management.png")
|
|
|
|
|
|
func _test_restored_opening_does_not_pause() -> void:
|
|
var _audio_cleanup_transition: Dictionary = await _cleanup_fixture_owner(_main, "restored-transition")
|
|
if not bool(_audio_cleanup_transition.get(&"passed", false)):
|
|
push_error("Ordinary fixture audio cleanup failed: %s" % str(_audio_cleanup_transition))
|
|
quit(96)
|
|
return
|
|
await process_frame
|
|
_clock.reset(1, 6, 0)
|
|
var state: Variant = root.get_node("GameState")
|
|
state.reset()
|
|
_runtime.initialize(_clock, state, 824242)
|
|
_clock.set_paused(false)
|
|
var snapshot: Dictionary = _runtime.create_snapshot()
|
|
_expect(_runtime.restore_snapshot(snapshot), "restored opening fixture accepts its empty initial save")
|
|
_main = load("res://scenes/main/main.tscn").instantiate() as Control
|
|
root.add_child(_main)
|
|
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
_expect(not _clock.paused and int(_clock.absolute_minute) == 360,
|
|
"mounting a restored empty opening preserves its saved running state and time")
|
|
_clock.set_paused(true)
|
|
|
|
|
|
func _assignment(staff_id: StringName) -> Dictionary:
|
|
return _runtime.staff_service.get_work_assignment(staff_id)
|
|
|
|
|
|
func _choose_facility(facility_id: StringName) -> void:
|
|
for index: int in _panel.facility_option.item_count:
|
|
if StringName(_panel.facility_option.get_item_metadata(index)) == facility_id:
|
|
_panel.facility_option.select(index)
|
|
_panel.facility_option.item_selected.emit(index)
|
|
return
|
|
_expect(false, "requested facility is available in the UI: %s" % facility_id)
|
|
|
|
|
|
func _chosen_facility() -> StringName:
|
|
return StringName(_panel.facility_option.get_item_metadata(_panel.facility_option.selected))
|
|
|
|
|
|
func _key(code: Key) -> InputEventKey:
|
|
var event := InputEventKey.new()
|
|
event.keycode = code
|
|
event.pressed = true
|
|
return event
|
|
|
|
|
|
func _expect(condition: bool, message: String) -> void:
|
|
_checks += 1
|
|
if not condition:
|
|
_failures += 1
|
|
push_error(message)
|
|
|
|
|
|
func _finish() -> void:
|
|
if not _save_path.is_empty():
|
|
SaveService.delete_save(_save_path)
|
|
if is_instance_valid(_main):
|
|
var _audio_cleanup_final: Dictionary = await _cleanup_fixture_owner(_main, "finish")
|
|
if not bool(_audio_cleanup_final.get(&"passed", false)):
|
|
push_error("Ordinary fixture audio cleanup failed: %s" % str(_audio_cleanup_final))
|
|
quit(96)
|
|
return
|
|
print("Staff management UI: %d checks, %d failures" % [_checks, _failures])
|
|
quit(1 if _failures > 0 else 0)
|
|
|
|
|
|
func _test_drag_assignment() -> void:
|
|
var before: Dictionary = _runtime.create_snapshot()
|
|
var preview: WorldPreview = _main.get("world_preview")
|
|
_main.call("_set_layer", 0)
|
|
_main.call("_clear_build_selection")
|
|
await process_frame
|
|
var target: Vector3i
|
|
for room: Dictionary in _runtime.construction_service.get_facilities():
|
|
if room[&"instance_id"] == _receptions[0]: target = room[&"origin"]
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
|
|
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0], "viewport drag assigns selected employee to completed reception")
|
|
_expect("変更しました" in _panel.result_label.text, "drag reports authoritative assignment success")
|
|
_expect(_runtime.restore_snapshot(before), "restore drag fixture before invalid target")
|
|
_panel.refresh()
|
|
var unchanged: Dictionary = _runtime.create_snapshot()
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(Vector3i(-4,4,0)), Vector3i(-4,4,0))
|
|
_expect(_runtime.create_snapshot() == unchanged, "empty-map drop preserves all simulation state")
|
|
_expect("完成した配属先施設" in _panel.result_label.text, "invalid drop explains target requirement")
|
|
_expect(not preview._can_drop_data(Vector2.ZERO,{&"kind":&"unrelated"}), "map refuses unrelated drag payload")
|
|
_panel.refresh()
|
|
var schedule_before := _assignment(&"staff_akane").duplicate(true)
|
|
_main.call("_set_layer", 1)
|
|
await process_frame
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(Vector3i(2,0,1)), Vector3i(2,0,1))
|
|
_expect(_assignment(&"staff_akane") == schedule_before, "wrong-role drag preserves staff schedule")
|
|
_expect("役割" in _panel.result_label.text, "wrong-role drag displays authoritative refusal")
|
|
_main.call("_set_layer", 0)
|
|
_panel.select_staff(&"staff_akane")
|
|
_panel.shift_option.select(1)
|
|
_panel.shift_option.item_selected.emit(1)
|
|
await process_frame
|
|
_clock.set_paused(false)
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
|
|
_clock.set_paused(true)
|
|
_expect(_panel.apply_button.disabled and (_runtime.command_queue.create_snapshot()["pending"] as Array).size() == 1, "night drag queues exactly one assignment")
|
|
var drag_path := "user://tests/staff-drag-%d" % Time.get_ticks_usec()
|
|
_expect(_runtime.save_to_path(drag_path).get(&"ok", false), "save pending night drag")
|
|
_expect(_runtime.load_from_path(drag_path).get(&"ok", false), "restore pending night drag")
|
|
_panel.refresh()
|
|
_expect(_panel.apply_button.disabled, "loaded drag remains pending")
|
|
_clock.advance_one_minute()
|
|
_expect(_assignment(&"staff_akane").get(&"template_id") == &"NIGHT" and _assignment(&"staff_akane").get(&"facility_id") == _receptions[0], "restored drag applies selected night shift to exact facility")
|
|
_expect((_panel.get("_pending_commands") as Dictionary).is_empty(), "restored drag clears pending feedback after execution")
|
|
if OS.get_environment("THE_TREE_STAFF_DRAG_CAPTURE") == "1":
|
|
await process_frame; await RenderingServer.frame_post_draw
|
|
_expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path("res://artifacts/facility-dimensions-2026-09-29/staff-drag-night.png")) == OK, "capture restored night drag assignment")
|
|
SaveService.delete_save(drag_path)
|
|
_expect(_runtime.restore_snapshot(before), "restore drag boundary fixture for existing regression")
|
|
_panel.refresh()
|
|
|
|
|
|
|
|
func _drag_staff_to(target: Vector2, cell: Variant = null, before_release := Callable()) -> void:
|
|
# Let restored roster layout finish before reading the card to press.
|
|
await process_frame
|
|
var source: Button = _panel.roster_buttons[&"staff_akane"]
|
|
var start := source.get_global_rect().get_center()
|
|
var motion := InputEventMouseMotion.new()
|
|
motion.position=start; motion.global_position=start
|
|
motion.window_id = root.get_window_id()
|
|
_send_drag_input(motion)
|
|
var down := InputEventMouseButton.new()
|
|
down.position=start; down.global_position=start; down.button_index=MOUSE_BUTTON_LEFT
|
|
down.pressed=true; down.button_mask=MOUSE_BUTTON_MASK_LEFT
|
|
down.window_id = root.get_window_id()
|
|
_send_drag_input(down)
|
|
await process_frame
|
|
for point: Vector2 in [start+Vector2(18,0),target]:
|
|
var move := InputEventMouseMotion.new()
|
|
if point == target and cell is Vector3i:
|
|
var map: WorldPreview = _main.get("world_preview")
|
|
point = map.global_position + map.cell_to_canvas(cell)
|
|
target = point
|
|
move.position=point; move.global_position=point; move.relative=point-start
|
|
move.button_mask=MOUSE_BUTTON_MASK_LEFT
|
|
move.window_id = root.get_window_id()
|
|
_send_drag_input(move)
|
|
await process_frame
|
|
if before_release.is_valid():
|
|
_expect(root.gui_is_dragging(), "state changes only after the viewport has started dragging")
|
|
before_release.call()
|
|
var up := InputEventMouseButton.new()
|
|
up.position=target; up.global_position=target; up.button_index=MOUSE_BUTTON_LEFT; up.pressed=false
|
|
up.window_id = root.get_window_id()
|
|
_send_drag_input(up)
|
|
await process_frame
|
|
|
|
|
|
func _send_drag_input(event: InputEventMouse) -> void:
|
|
if DisplayServer.get_name() == "headless":
|
|
event.position *= Vector2(root.size) / root.get_visible_rect().size
|
|
event.global_position = event.position
|
|
Input.parse_input_event(event)
|
|
else:
|
|
if event is InputEventMouseButton and not event.pressed:
|
|
Input.warp_mouse(event.position * Vector2(root.size) / root.get_visible_rect().size)
|
|
root.push_input(event, true)
|
|
|
|
|
|
func _test_drag_rejection_boundaries() -> void:
|
|
# Controlled boundary fixtures; the action under test uses viewport input.
|
|
var before: Dictionary = _runtime.create_snapshot()
|
|
var preview: WorldPreview = _main.get("world_preview")
|
|
var target: Vector3i = _runtime.facility_service.get_instance(_receptions[0]).origin
|
|
_main.call("_set_layer", 0)
|
|
_panel.select_staff(&"staff_akane")
|
|
_panel.shift_option.select(0)
|
|
_panel.shift_option.item_selected.emit(0)
|
|
var role: StringName = _runtime.staff_service.get_staff(&"staff_akane").role_id
|
|
var maximum: int = _runtime.definition_registry.get_facility(&"reception").staff_max
|
|
for i in maximum:
|
|
var id := StringName("drag_capacity_%d" % i)
|
|
_expect(_runtime.staff_service.create_staff(id, role).is_success() and _runtime.staff_service.set_work_assignment(id, _receptions[0], &"DAY", _clock.absolute_minute).is_success(), "fill actual reception shift slots for capacity boundary")
|
|
var staff_before: Dictionary = _runtime.staff_service.create_snapshot()
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
|
|
_expect(_runtime.staff_service.create_snapshot() == staff_before and "人員上限" in _panel.result_label.text, "full-facility drag preserves all staff and explains capacity limit")
|
|
await _capture_drag_rejection("capacity")
|
|
_expect(_runtime.restore_snapshot(before), "restore after capacity drag fixture")
|
|
_panel.refresh()
|
|
_panel.select_staff(&"staff_akane")
|
|
_runtime.staff_service.get_staff(&"staff_akane").current_cell = Vector3i(999,999,0)
|
|
staff_before = _runtime.staff_service.create_snapshot()
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
|
|
_expect(_runtime.staff_service.create_snapshot() == staff_before and "通路と従業員ドア" in _panel.result_label.text, "unreachable drag preserves staff and explains route failure")
|
|
await _capture_drag_rejection("route")
|
|
_expect(_runtime.restore_snapshot(before), "restore after inaccessible route fixture")
|
|
_panel.refresh()
|
|
_panel.select_staff(&"staff_akane")
|
|
# Inject only the lock boundary, not a complete narrative event.
|
|
_runtime.event_service._actor_locks[&"staff_akane"] = &"drag_lock_fixture"
|
|
_panel.refresh()
|
|
staff_before = _runtime.staff_service.create_snapshot()
|
|
var queue_before: Dictionary = _runtime.command_queue.create_snapshot()
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
|
|
_expect(_runtime.staff_service.create_snapshot() == staff_before and _runtime.command_queue.create_snapshot() == queue_before, "locked employee cannot start an assignment drag or submit a command")
|
|
_expect("変更できません" in _panel.result_label.text, "locked drag explains why assignment is unavailable")
|
|
await _capture_drag_rejection("locked")
|
|
_runtime.event_service._actor_locks.erase(&"staff_akane")
|
|
_expect(_runtime.restore_snapshot(before), "restore after locked employee fixture")
|
|
_panel.refresh()
|
|
|
|
|
|
func _capture_drag_rejection(kind: String) -> void:
|
|
if OS.get_environment("THE_TREE_DRAG_REJECTION_CAPTURE") != "1": return
|
|
await process_frame
|
|
await RenderingServer.frame_post_draw
|
|
var path := "res://artifacts/facility-dimensions-2026-09-29/staff-drag-rejection-%s.png" % kind
|
|
_expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path(path)) == OK, "capture drag rejection %s" % kind)
|
|
|
|
|
|
func _test_drag_state_change() -> void:
|
|
var before: Dictionary = _runtime.create_snapshot()
|
|
var preview: WorldPreview = _main.get("world_preview")
|
|
var target: Vector3i = _runtime.facility_service.get_instance(_receptions[0]).origin
|
|
var feedback: Array[CommandResult] = []
|
|
var listener := func(result: CommandResult) -> void: feedback.append(result)
|
|
_runtime.command_completed.connect(listener)
|
|
for reason: StringName in [&"EVENT_LOCKED", &"STAFF_BUSY"]:
|
|
_panel.select_staff(&"staff_akane")
|
|
var assignment := _assignment(&"staff_akane").duplicate(true)
|
|
feedback.clear()
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target, func() -> void:
|
|
if reason == &"EVENT_LOCKED":
|
|
_runtime.event_service._actor_locks[&"staff_akane"] = &"drag_race_fixture"
|
|
else:
|
|
_runtime.staff_service.get_staff(&"staff_akane").current_task_id = &"drag_race_task"
|
|
)
|
|
_expect(not feedback.is_empty() and not feedback.back().is_success() and feedback.back().details.get(&"reason") == reason, "drop revalidates latest %s state through real command" % reason)
|
|
_expect(_assignment(&"staff_akane") == assignment and _panel.apply_button.disabled, "rejected race preserves assignment and reflects unavailable state")
|
|
var explanation := "イベント参加中" if reason == &"EVENT_LOCKED" else "作業中"
|
|
_expect(explanation in _panel.staff_detail_label.text and explanation in _panel.apply_button.tooltip_text and explanation in _panel.result_label.text and (_panel.get("_pending_commands") as Dictionary).is_empty(), "race rejection shows precise reason and clears pending UI")
|
|
_runtime.event_service._actor_locks.erase(&"staff_akane")
|
|
_runtime.staff_service.get_staff(&"staff_akane").current_task_id = &""
|
|
_expect(_runtime.restore_snapshot(before), "restore state after drag race")
|
|
_panel.refresh()
|
|
_runtime.command_completed.disconnect(listener)
|
|
|
|
|
|
func _test_drag_across_load() -> void:
|
|
var before: Dictionary = _runtime.create_snapshot()
|
|
var path := "user://tests/staff-drag-load-%d" % Time.get_ticks_usec()
|
|
_expect(_runtime.save_to_path(path).get(&"ok", false), "save before live drag reload")
|
|
var preview: WorldPreview = _main.get("world_preview")
|
|
var target: Vector3i = _runtime.facility_service.get_instance(_receptions[0]).origin
|
|
var loaded_staff: Array[Dictionary] = []
|
|
var loaded_queue: Array[Dictionary] = []
|
|
_panel.select_staff(&"staff_akane")
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target, func() -> void:
|
|
_expect(_runtime.load_from_path(path).get(&"ok", false), "load real save while pointer is dragging")
|
|
loaded_staff.append(_runtime.staff_service.create_snapshot())
|
|
loaded_queue.append(_runtime.command_queue.create_snapshot())
|
|
)
|
|
_expect(_runtime.staff_service.create_snapshot() == loaded_staff[0], "pre-load drag cannot alter restored staff or assignment")
|
|
_expect(_runtime.command_queue.create_snapshot() == loaded_queue[0], "pre-load drag cannot enqueue a command in restored world")
|
|
_expect("読み込み" in _panel.result_label.text, "stale drag explains that load requires a new drag")
|
|
if OS.get_environment("THE_TREE_DRAG_LOAD_CAPTURE") == "1":
|
|
await process_frame
|
|
await RenderingServer.frame_post_draw
|
|
_expect(root.get_texture().get_image().save_png(ProjectSettings.globalize_path("res://artifacts/staff-drag-load-2026-09-29/rejected.png")) == OK, "capture load cancellation feedback")
|
|
_panel.select_staff(&"staff_akane")
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
|
|
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0], "fresh drag after load still assigns normally")
|
|
SaveService.delete_save(path)
|
|
_expect(_runtime.restore_snapshot(before), "restore fixture after live drag reload")
|
|
_panel.refresh()
|
|
_panel.select_staff(&"staff_akane")
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target, func() -> void:
|
|
_expect(not _runtime.restore_snapshot({}), "invalid snapshot is refused during drag")
|
|
)
|
|
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0], "failed restore preserves current world and live drag")
|
|
_expect(_runtime.restore_snapshot(before), "restore fixture after refused load")
|
|
_panel.refresh()
|
|
|
|
|
|
func _test_drag_target_demolition() -> void:
|
|
var before: Dictionary = _runtime.create_snapshot()
|
|
var preview: WorldPreview = _main.get("world_preview")
|
|
var target: Vector3i = _runtime.facility_service.get_instance(_receptions[0]).origin
|
|
var staff_before: Dictionary = _runtime.staff_service.create_snapshot()
|
|
_panel.select_staff(&"staff_akane")
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target, func() -> void:
|
|
var removed: CommandResult = _runtime.submit_command(&"demolish_facility", {&"instance_id":_receptions[0]}, &"player", &"", 30, -1, &"ui.drag.target.demolition")
|
|
_expect(removed.is_success(), "normal demolition command succeeds while dragging toward unstaffed facility")
|
|
)
|
|
_expect(not _runtime.facility_service.has_instance(_receptions[0]), "drop cannot resurrect demolished facility")
|
|
_expect(_runtime.staff_service.create_snapshot() == staff_before, "demolition during drag preserves staff state and schedules")
|
|
_expect("完成した配属先施設" in _panel.result_label.text or "見つかりません" in _panel.result_label.text, "removed drop target has actionable rejection feedback")
|
|
_expect((_panel.get("_pending_commands") as Dictionary).is_empty(), "removed target leaves no stuck assignment UI")
|
|
_expect(_runtime.restore_snapshot(before), "restore after live demolition boundary")
|
|
_panel.refresh()
|
|
|
|
|
|
func _test_drag_defense_lock(register_objective := true) -> void:
|
|
var before: Dictionary = _runtime.create_snapshot()
|
|
var preview: WorldPreview = _main.get("world_preview")
|
|
var target: Vector3i = _runtime.facility_service.get_instance(_receptions[0]).origin
|
|
var assignment := _assignment(&"staff_akane").duplicate(true)
|
|
_panel.select_staff(&"staff_akane")
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target, func() -> void:
|
|
var created: CommandResult = _runtime.submit_command(&"create_defense_raid", {
|
|
&"raid_id": &"ui.drag.defense", &"definition": {
|
|
&"source_id": &"runtime_test", &"entry_points": [&"gate.north"],
|
|
&"objective_ids": [_receptions[0]], &"round_minutes": 5,
|
|
&"max_duration_minutes": 180, &"retreat_policy": &"HOLD", &"defender_retreat_threshold": 0,
|
|
&"waves": [{&"wave_id": &"wave.one", &"threat_strength": 15,
|
|
&"retreat_threshold": 0, &"max_rounds": 1, &"entry_points": [&"gate.north"]}],
|
|
}}, &"defense_controller")
|
|
_expect(created.is_success(), "create real defense raid during assignment drag")
|
|
if register_objective:
|
|
var asset: CommandResult = _runtime.submit_command(&"register_defense_asset", {
|
|
&"raid_id": &"ui.drag.defense", &"asset_id": _receptions[0], &"kind": &"objective",
|
|
&"max_integrity": 10000, &"defense": 40, &"options": {}}, &"defense_controller")
|
|
_expect(asset.is_success(), "register real facility as defense objective")
|
|
var registered: CommandResult = _runtime.submit_command(&"register_defense_combatant", {
|
|
&"raid_id": &"ui.drag.defense", &"actor_id": &"staff_akane", &"side": &"DEFENDER",
|
|
&"combat": 90, &"max_health": 100, &"options": {}}, &"defense_controller")
|
|
_expect(registered.is_success(), "register dragged employee as defender")
|
|
)
|
|
_expect(_assignment(&"staff_akane") == assignment and "防衛参加中" in _panel.result_label.text, "defense registration during drag rejects assignment with precise feedback")
|
|
_expect(_panel.apply_button.disabled and "防衛参加中" in _panel.staff_detail_label.text and "防衛参加中" in _panel.apply_button.tooltip_text, "locked editor explains defense restriction before retry")
|
|
var path := "user://tests/staff-defense-lock-%d" % Time.get_ticks_usec()
|
|
_expect(_runtime.save_to_path(path).get(&"ok", false), "save registered defense lock")
|
|
_expect(_runtime.load_from_path(path).get(&"ok", false), "restore registered defense lock")
|
|
_panel.refresh()
|
|
_panel.select_staff(&"staff_akane")
|
|
var queue_before: Dictionary = _runtime.command_queue.create_snapshot()
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
|
|
_expect(_runtime.command_queue.create_snapshot() == queue_before and "防衛参加中" in _panel.result_label.text, "restored defender cannot begin new drag and sees the reason")
|
|
var cancelled: CommandResult = _runtime.submit_command(&"cancel_defense_raid", {&"raid_id": &"ui.drag.defense"}, &"defense_controller")
|
|
_expect(cancelled.is_success(), "cancel raid through normal command: %s %s" % [cancelled.message, cancelled.details])
|
|
_panel.refresh()
|
|
_expect(not _panel.apply_button.disabled and not "防衛参加中" in _panel.staff_detail_label.text, "terminal raid releases editor lock and stale explanation")
|
|
await _drag_staff_to(preview.global_position + preview.cell_to_canvas(target), target)
|
|
_expect(_assignment(&"staff_akane").get(&"facility_id") == _receptions[0], "new drag after defense cancellation assigns normally")
|
|
SaveService.delete_save(path)
|
|
_expect(_runtime.restore_snapshot(before), "restore fixture after defense boundary")
|
|
_panel.refresh()
|
|
|
|
|
|
## QA-only control proof routing for sequential ordinary Main owners.
|
|
## The old02cd/6be99 helpers and original Clock/Runtime values stay exact.
|
|
func _cleanup_fixture_owner(owner: Node, phase: String) -> Dictionary:
|
|
var original_control: String = OS.get_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF")
|
|
if original_control.is_empty() or not original_control.is_absolute_path():
|
|
return {&"passed": false, &"reason": "fresh absolute owned control proof required"}
|
|
var phase_control: String = original_control.get_basename() + "-" + phase + ".json"
|
|
if FileAccess.file_exists(phase_control):
|
|
return {&"passed": false, &"reason": "ordinary owner control proof overwrite forbidden"}
|
|
OS.set_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF", phase_control)
|
|
var report: Dictionary = await FixtureCleanup.free_owner_and_wait(self, owner)
|
|
OS.set_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF", original_control)
|
|
return report
|