Save The Tree implementation and acceptance work in progress
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.
This commit is contained in:
+254
-28
@@ -1,6 +1,19 @@
|
||||
extends Control
|
||||
|
||||
const UIAudioFeedbackScript := preload("res://scripts/ui/ui_audio_feedback.gd")
|
||||
const UIAudioSettingsDialogScript := preload("res://scripts/ui/ui_audio_settings_dialog.gd")
|
||||
const UIAudioOwnerShutdownScript := preload("res://scripts/ui/ui_audio_owner_shutdown.gd")
|
||||
const PairedBGMScript := preload("res://scripts/ui/paired_bgm.gd")
|
||||
var _paired_bgm: Variant
|
||||
signal ui_audio_window_close_drained(report: Dictionary)
|
||||
var audio_settings_path := "user://settings/audio.cfg"
|
||||
var _audio_feedback: Variant
|
||||
var _audio_settings_dialog: Variant
|
||||
var _window_close_in_progress := false
|
||||
|
||||
const RankDialogScript := preload("res://scripts/ui/world_tree_rank_dialog.gd")
|
||||
const EventLogDialogScript := preload("res://scripts/ui/event_log_dialog.gd")
|
||||
const ManualSaveSlotsDialogScript := preload("res://scripts/ui/manual_save_slots_dialog.gd")
|
||||
|
||||
const OpeningGuideScript := preload("res://scripts/ui/inn_opening_guide.gd")
|
||||
const OperatingLedgerPanelScript := preload("res://scripts/ui/operating_ledger_panel.gd")
|
||||
@@ -113,11 +126,13 @@ const STAFF_SKILL_NAMES := {
|
||||
|
||||
@export var visual_theme: VisualThemeProfile = DefaultVisualTheme
|
||||
var manual_save_path := MANUAL_SAVE_PATH
|
||||
var manual_save_slots_dialog: ConfirmationDialog
|
||||
|
||||
var time_label: Label
|
||||
var phase_label: Label
|
||||
var rank_button: Button
|
||||
var rank_dialog: ConfirmationDialog
|
||||
var event_log_dialog: AcceptDialog
|
||||
var weather_label: Label
|
||||
var guest_count_label: Button
|
||||
var dimension_migration_dialog: ConfirmationDialog
|
||||
@@ -205,6 +220,9 @@ func _ready() -> void:
|
||||
if not bool(current_operations.get(&"enabled", false)):
|
||||
_operations_start_report = _runtime.call(&"enable_production_operations")
|
||||
_synchronize_ui_command_state()
|
||||
_setup_ui_audio()
|
||||
UIAudioOwnerShutdownScript.register_owner(get_tree(), self)
|
||||
get_tree().root.close_requested.connect(_on_window_close_requested)
|
||||
_build_interface()
|
||||
_connect_signals()
|
||||
_populate_build_palette()
|
||||
@@ -225,6 +243,43 @@ func _ready() -> void:
|
||||
_focus_initial_view.call_deferred()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var tree := get_tree()
|
||||
if tree != null:
|
||||
if tree.root.close_requested.is_connected(_on_window_close_requested):
|
||||
tree.root.close_requested.disconnect(_on_window_close_requested)
|
||||
UIAudioOwnerShutdownScript.unregister_owner(tree, self)
|
||||
|
||||
|
||||
func _on_window_close_requested() -> void:
|
||||
# The shared coordinator drains every registered Main before one quit.
|
||||
# Main stays alive while its audio child is freed and the backend releases.
|
||||
var tree := get_tree()
|
||||
UIAudioOwnerShutdownScript.close_owners(tree)
|
||||
|
||||
|
||||
func _report_audio_close_drained(report: Dictionary) -> void:
|
||||
ui_audio_window_close_drained.emit(report)
|
||||
|
||||
|
||||
func _prepare_owned_audio_close() -> Array[Dictionary]:
|
||||
_window_close_in_progress = true
|
||||
set_process(false)
|
||||
set_process_input(false)
|
||||
set_process_unhandled_input(false)
|
||||
set_process_unhandled_key_input(false)
|
||||
hide()
|
||||
var audio: Node = _audio_feedback as Node
|
||||
_audio_feedback = null
|
||||
var targets := UIAudioOwnerShutdownScript.capture_and_release(audio)
|
||||
if is_instance_valid(_paired_bgm):
|
||||
targets.append_array(_paired_bgm.capture_owned_targets())
|
||||
_paired_bgm.prepare_owner_shutdown()
|
||||
_paired_bgm.queue_free()
|
||||
_paired_bgm = null
|
||||
return targets
|
||||
|
||||
|
||||
func _focus_initial_view() -> void:
|
||||
await get_tree().process_frame
|
||||
world_preview.focus_operating_area()
|
||||
@@ -281,6 +336,8 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
_:
|
||||
handled = false
|
||||
if handled:
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.request_click()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
@@ -377,6 +434,9 @@ func _create_top_bar() -> Control:
|
||||
title_box.add_child(rank_button)
|
||||
rank_dialog=RankDialogScript.new()
|
||||
rank_dialog.configure(_runtime)
|
||||
rank_dialog.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
_audio_feedback.bind_button(rank_dialog.get_ok_button())
|
||||
_audio_feedback.bind_button(rank_dialog.get_cancel_button())
|
||||
rank_dialog.status_requested.connect(func(message: String, success: bool) -> void:
|
||||
_set_status(message,success)
|
||||
_refresh_dashboard()
|
||||
@@ -561,6 +621,7 @@ func _create_side_panel() -> Control:
|
||||
column.add_child(candidate_heading_row)
|
||||
staff_management_panel = StaffManagementPanelScript.new()
|
||||
staff_management_panel.configure(_runtime, _apply_button_style)
|
||||
staff_management_panel.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
staff_management_panel.status_requested.connect(_on_staff_management_status)
|
||||
staff_management_panel.visible = false
|
||||
column.add_child(staff_management_panel)
|
||||
@@ -624,15 +685,19 @@ func _create_side_panel() -> Control:
|
||||
operating_ledger_panel = OperatingLedgerPanelScript.new()
|
||||
root_column.add_child(operating_ledger_panel)
|
||||
operating_ledger_panel.setup(_runtime, GameClock, _apply_button_style, GameState.active_layer)
|
||||
operating_ledger_panel.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
operating_ledger_panel.status_requested.connect(_on_staff_management_status)
|
||||
operating_ledger_panel.visible = false
|
||||
supply_management_panel = SupplyManagementPanelScript.new()
|
||||
supply_management_panel.configure(_runtime, _apply_button_style)
|
||||
supply_management_panel.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
supply_management_panel.status_requested.connect(_on_staff_management_status)
|
||||
supply_management_panel.action_requested.connect(_activate_management_mode)
|
||||
supply_management_panel.visible = false
|
||||
root_column.add_child(supply_management_panel)
|
||||
business_management_panel = BusinessManagementPanelScript.new()
|
||||
business_management_panel.configure(_runtime, _apply_button_style)
|
||||
business_management_panel.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
business_management_panel.status_requested.connect(_on_staff_management_status)
|
||||
business_management_panel.action_requested.connect(_activate_management_mode)
|
||||
business_management_panel.visible = false
|
||||
@@ -676,7 +741,7 @@ func _create_bottom_bar() -> Control:
|
||||
pan_button = _button("画面移動", _toggle_pan_mode, 90)
|
||||
pan_button.name = "PanModeButton"
|
||||
pan_button.toggle_mode = true
|
||||
pan_button.tooltip_text = "左ドラッグで画面を移動。もう一度押すか Esc で解除。ホイールで拡大縮小、Homeで全体表示。"
|
||||
pan_button.tooltip_text = "左ドラッグとホイールで画面を上下移動。Ctrl+ホイールで拡大縮小。もう一度押すか Esc で解除、Homeで全体表示。"
|
||||
row.add_child(pan_button)
|
||||
var focus_button := _button("営業階", func() -> void: world_preview.focus_operating_area(), 64)
|
||||
focus_button.name = "FocusOperatingAreaButton"
|
||||
@@ -689,9 +754,20 @@ func _create_bottom_bar() -> Control:
|
||||
management_toggle_button.name = "ManagementToggleButton"
|
||||
management_toggle_button.toggle_mode = true
|
||||
row.add_child(management_toggle_button)
|
||||
event_log_dialog = EventLogDialogScript.new()
|
||||
event_log_dialog.configure(_runtime)
|
||||
add_child(event_log_dialog)
|
||||
var event_log_button := _button("記録", event_log_dialog.show_events, 62)
|
||||
event_log_button.name = "EventLogButton"
|
||||
event_log_button.tooltip_text = "最近のイベントを確認"
|
||||
row.add_child(event_log_button)
|
||||
var save_button := _button("保存", _save_manual, 62)
|
||||
save_button.name = "ManualSaveButton"
|
||||
_apply_button_icon(save_button, visual_theme.control_icon(&"save") if visual_theme != null else null, 16)
|
||||
row.add_child(save_button)
|
||||
var audio_button := _button("音設定", _show_audio_settings, 62)
|
||||
audio_button.name = "AudioSettingsButton"
|
||||
row.add_child(audio_button)
|
||||
var load_menu := MenuButton.new()
|
||||
load_menu.name = "SaveLoadMenu"
|
||||
load_menu.text = "読込 ▾"
|
||||
@@ -700,9 +776,11 @@ func _create_bottom_bar() -> Control:
|
||||
_apply_button_style(load_menu)
|
||||
load_menu.get_popup().add_item("手動セーブを読み込む",0)
|
||||
load_menu.get_popup().add_item("旧寸法を修正して読み込む",1)
|
||||
load_menu.get_popup().add_item("保存スロットを選んで読込",2)
|
||||
load_menu.get_popup().id_pressed.connect(func(id: int):
|
||||
if id == 0: _load_manual()
|
||||
elif id == 1: _preview_dimension_migration())
|
||||
elif id == 1: _preview_dimension_migration()
|
||||
elif id == 2: _show_manual_load_slots())
|
||||
row.add_child(load_menu)
|
||||
|
||||
palette_scroll = ScrollContainer.new()
|
||||
@@ -865,11 +943,14 @@ func _connect_signals() -> void:
|
||||
GameClock.phase_changed.connect(_on_phase_changed)
|
||||
GameState.resource_changed.connect(_on_resource_changed)
|
||||
world_preview.facility_selected.connect(_on_facility_selected)
|
||||
world_preview.facility_selected.connect(_on_audio_world_selection)
|
||||
world_preview.staff_assignment_dropped.connect(func(data: Dictionary, facility_id: StringName) -> void:
|
||||
staff_management_panel.assign_from_drop(data, facility_id)
|
||||
)
|
||||
world_preview.legacy_stair_selected.connect(_on_legacy_stair_selected)
|
||||
world_preview.legacy_stair_selected.connect(_on_audio_world_selection)
|
||||
world_preview.construction_selected.connect(_on_construction_selected)
|
||||
world_preview.construction_selected.connect(_on_audio_world_selection)
|
||||
world_preview.cell_clicked.connect(_on_map_cell_clicked)
|
||||
world_preview.build_preview_changed.connect(_on_build_preview_changed)
|
||||
world_preview.excavation_preview_changed.connect(_on_excavation_preview_changed)
|
||||
@@ -1085,9 +1166,7 @@ func _hire_staff_candidate(candidate_id: StringName) -> void:
|
||||
var idempotency_key := StringName(
|
||||
"ui.hire.%s.%08d" % [String(candidate_id), sequence]
|
||||
)
|
||||
var result: Variant = _runtime.call(
|
||||
&"submit_command",
|
||||
&"hire_staff_candidate",
|
||||
var result: Variant = _submit_ui_command(&"hire_staff_candidate",
|
||||
{&"candidate_id": candidate_id},
|
||||
&"player",
|
||||
candidate_id,
|
||||
@@ -1215,7 +1294,7 @@ func _toggle_excavation_mode() -> void:
|
||||
|
||||
func _toggle_pan_mode() -> void:
|
||||
_set_pan_mode(not world_preview.pan_mode)
|
||||
_set_status("画面移動中:左ドラッグで移動、ホイールで拡大縮小。Escで解除。" if world_preview.pan_mode else "画面移動を解除しました。施設をクリックすると情報を表示します。", true, true)
|
||||
_set_status("画面移動中:左ドラッグで移動、ホイールで上下移動、Ctrl+ホイールで拡大縮小。Escで解除。" if world_preview.pan_mode else "画面移動を解除しました。施設をクリックすると情報を表示します。", true, true)
|
||||
|
||||
|
||||
func _set_pan_mode(enabled: bool) -> void:
|
||||
@@ -1273,6 +1352,8 @@ func _rotate_build_preview() -> void:
|
||||
|
||||
|
||||
func _on_map_cell_clicked(cell: Vector3i) -> void:
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.request_click()
|
||||
if _excavation_mode:
|
||||
_start_excavation(cell)
|
||||
return
|
||||
@@ -1287,6 +1368,8 @@ func _on_map_cell_clicked(cell: Vector3i) -> void:
|
||||
return
|
||||
var preview := world_preview.get_build_preview_report(cell)
|
||||
if not bool(preview.get(&"can_build", false)):
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.reject_local(&"BUILD_PREVIEW_REJECTED")
|
||||
_set_status(_preview_failure_message(preview), false)
|
||||
return
|
||||
if _runtime == null or not _runtime.has_method(&"submit_command"):
|
||||
@@ -1294,9 +1377,7 @@ func _on_map_cell_clicked(cell: Vector3i) -> void:
|
||||
return
|
||||
var idempotency_key := StringName("ui.build.%08d" % _build_sequence)
|
||||
_build_sequence += 1
|
||||
var result: Variant = _runtime.call(
|
||||
&"submit_command",
|
||||
&"build_facility",
|
||||
var result: Variant = _submit_ui_command(&"build_facility",
|
||||
{&"definition_id": _selected_definition_id, &"origin": cell, &"rotation": _build_rotation},
|
||||
&"player",
|
||||
&"",
|
||||
@@ -1324,7 +1405,9 @@ func _on_build_preview_changed(report: Dictionary) -> void:
|
||||
return
|
||||
if bool(report.get(&"can_build", false)):
|
||||
var cell: Vector3i = world_preview.get_hovered_cell()
|
||||
_set_status("配置可能 (%d, %d)・%d° — クリックで建設" % [cell.x, cell.y, _build_rotation], true, true)
|
||||
var connection: Dictionary = report.get(&"connection", {})
|
||||
var warning := " · 入口への経路未接続" if connection.get(&"status", &"") == &"DISCONNECTED" else " · 経路確認不可" if connection.get(&"status", &"") == &"UNAVAILABLE" else ""
|
||||
_set_status("配置可能 (%d, %d)・%d°%s — クリックで建設" % [cell.x, cell.y, _build_rotation, warning], true, true)
|
||||
else:
|
||||
_set_status(_preview_failure_message(report), false, true)
|
||||
|
||||
@@ -1369,9 +1452,7 @@ func _start_excavation(cell: Vector3i) -> void:
|
||||
_set_status("このマスの掘削指示はすでに受理されています。", false)
|
||||
return
|
||||
var idempotency_key := _excavation_idempotency_key(cell)
|
||||
var result: Variant = _runtime.call(
|
||||
&"submit_command",
|
||||
&"start_excavation",
|
||||
var result: Variant = _submit_ui_command(&"start_excavation",
|
||||
{&"cell": Vector3i(cell.x, cell.y, world_preview.layer_mode)},
|
||||
&"player",
|
||||
&"",
|
||||
@@ -1436,6 +1517,8 @@ func _on_construction_selected(job_id: StringName) -> void:
|
||||
|
||||
|
||||
func _refresh_dashboard() -> void:
|
||||
if event_log_dialog != null and event_log_dialog.visible:
|
||||
event_log_dialog.refresh()
|
||||
if rank_button != null and _service(&"progression_service") != null:
|
||||
rank_button.text="世界樹ランク %d" % _service(&"progression_service").current_rank
|
||||
if rank_dialog != null and rank_dialog.visible: rank_dialog.refresh()
|
||||
@@ -1544,6 +1627,8 @@ func _refresh_foreground_event(event_service: Variant) -> void:
|
||||
foreground_event_panel.visible = false
|
||||
return
|
||||
foreground_event_panel.visible = true
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.observe_foreground(foreground)
|
||||
_set_ledger_panel_open(false)
|
||||
_set_supply_panel_open(false)
|
||||
_set_management_panel_open(true)
|
||||
@@ -1611,9 +1696,7 @@ func _submit_foreground_event_choice(
|
||||
var idempotency_key := StringName(
|
||||
"ui.event.choice.%s.%s" % [String(choice_instance_id), String(choice_id)]
|
||||
)
|
||||
var result: Variant = _runtime.call(
|
||||
&"submit_command",
|
||||
&"submit_event_choice",
|
||||
var result: Variant = _submit_ui_command(&"submit_event_choice",
|
||||
{
|
||||
&"event_instance_id": event_instance_id,
|
||||
&"choice_instance_id": choice_instance_id,
|
||||
@@ -1774,7 +1857,7 @@ func _on_related_facility_selected(index: int) -> void:
|
||||
if _runtime == null or _selected_entity_id.is_empty():
|
||||
return
|
||||
var related_id := StringName(related_facility_picker.get_item_metadata(index))
|
||||
var result: Variant = _runtime.call(&"submit_command", &"set_facility_conditions", {&"facility_id": _selected_entity_id, &"conditions": {&"related_facility_id": related_id}})
|
||||
var result: Variant = _submit_ui_command(&"set_facility_conditions", {&"facility_id": _selected_entity_id, &"conditions": {&"related_facility_id": related_id}})
|
||||
if result.is_success() and not result.details.has(&"instance_id"):
|
||||
_pending_related_commands[int(result.command_id)] = true
|
||||
_set_status("関連施設の変更を受け付けました。" if result.is_success() else result.message, result.is_success(), true)
|
||||
@@ -1908,7 +1991,7 @@ func _on_elevator_operation_pressed() -> void:
|
||||
var key := StringName("ui.elevator.%08d" % _elevator_sequence)
|
||||
_elevator_sequence += 1
|
||||
var open_value := not bool(shaft.get(&"operator_open", true))
|
||||
var result: Variant = _runtime.call(&"submit_command", &"set_elevator_operation", {&"shaft_id": shaft_id, &"operator_open": open_value}, &"player", &"", 50, -1, key)
|
||||
var result: Variant = _submit_ui_command(&"set_elevator_operation", {&"shaft_id": shaft_id, &"operator_open": open_value}, &"player", &"", 50, -1, key)
|
||||
if result == null or not result.is_success():
|
||||
_set_status("昇降機の指示を反映できませんでした。", false)
|
||||
return
|
||||
@@ -2118,21 +2201,55 @@ func _command_failure_message(result: Variant) -> String:
|
||||
|
||||
|
||||
func _save_manual() -> void:
|
||||
_save_manual_at_path(manual_save_path, "手動保存")
|
||||
|
||||
|
||||
func _save_manual_at_path(save_path: String, display_name: String) -> bool:
|
||||
if _runtime == null or not _runtime.has_method(&"save_to_path"):
|
||||
_set_status("保存サービスへ接続できません。", false)
|
||||
return
|
||||
var directory_error := DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(manual_save_path).get_base_dir())
|
||||
return false
|
||||
var directory_error := DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(save_path).get_base_dir())
|
||||
if directory_error != OK and directory_error != ERR_ALREADY_EXISTS:
|
||||
_set_status("保存フォルダを作成できませんでした。", false)
|
||||
return
|
||||
var result: Variant = _runtime.call(&"save_to_path", manual_save_path, {&"display_name": "手動保存", &"interface_selection": _capture_interface_selection()})
|
||||
return false
|
||||
var metadata := {&"display_name": display_name, &"interface_selection": _capture_interface_selection()}
|
||||
var thumbnail := _capture_manual_thumbnail_png()
|
||||
if not thumbnail.is_empty(): metadata[&"thumbnail_png"] = thumbnail
|
||||
var result: Variant = _runtime.call(&"save_to_path", save_path, metadata)
|
||||
if result is Dictionary and bool(result.get(&"ok", false)):
|
||||
manual_save_path = save_path
|
||||
_set_status("手動セーブを保存しました。", true)
|
||||
return true
|
||||
if result is Dictionary and StringName(result.get(&"code", &"")) == &"FORMAT_MIGRATION_REQUIRED":
|
||||
_set_status("元の保存を残すため、別のスロットへ保存してください。", false)
|
||||
_ensure_manual_save_slots().show_new_save(save_path)
|
||||
else:
|
||||
_set_status("保存に失敗しました: %s" % _result_message(result), false)
|
||||
return false
|
||||
|
||||
|
||||
func _capture_manual_thumbnail_png() -> PackedByteArray:
|
||||
# Capture the existing rendered game; no simulation update or new art source.
|
||||
# Dummy/headless rendering retains the existing SaveService fallback.
|
||||
if DisplayServer.get_name() == "headless": return PackedByteArray()
|
||||
var viewport := get_viewport()
|
||||
if viewport == null: return PackedByteArray()
|
||||
var texture := viewport.get_texture()
|
||||
if texture == null: return PackedByteArray()
|
||||
var image := texture.get_image()
|
||||
if image == null or image.is_empty(): return PackedByteArray()
|
||||
var scale := minf(320.0 / float(image.get_width()), 180.0 / float(image.get_height()))
|
||||
var width := maxi(1, roundi(float(image.get_width()) * scale))
|
||||
var height := maxi(1, roundi(float(image.get_height()) * scale))
|
||||
image.resize(width, height, Image.INTERPOLATE_LANCZOS)
|
||||
return image.save_png_to_buffer()
|
||||
|
||||
|
||||
func _load_manual(strict: bool = false) -> void:
|
||||
_load_manual_at_path(manual_save_path, strict)
|
||||
|
||||
|
||||
func _load_manual_at_path(save_path: String, strict: bool = false) -> void:
|
||||
if _runtime == null or (
|
||||
not _runtime.has_method(&"load_with_recovery")
|
||||
and not _runtime.has_method(&"load_from_path")
|
||||
@@ -2140,11 +2257,12 @@ func _load_manual(strict: bool = false) -> void:
|
||||
_set_status("読込サービスへ接続できません。", false)
|
||||
return
|
||||
var result: Variant = (
|
||||
_runtime.call(&"load_with_recovery", manual_save_path)
|
||||
_runtime.call(&"load_with_recovery", save_path)
|
||||
if not strict and _runtime.has_method(&"load_with_recovery")
|
||||
else _runtime.call(&"load_from_path", manual_save_path)
|
||||
else _runtime.call(&"load_from_path", save_path)
|
||||
)
|
||||
if result is Dictionary and bool(result.get(&"ok", false)):
|
||||
manual_save_path = save_path
|
||||
# Real Runtime already emitted its common completion notification.
|
||||
if not _runtime.has_signal(&"save_operation_completed"):
|
||||
_apply_loaded_interface(result)
|
||||
@@ -2153,8 +2271,37 @@ func _load_manual(strict: bool = false) -> void:
|
||||
_set_status("読み込みに失敗しました: %s" % _result_message(result), false)
|
||||
|
||||
|
||||
func _ensure_manual_save_slots() -> Variant:
|
||||
if manual_save_slots_dialog == null:
|
||||
manual_save_slots_dialog = ManualSaveSlotsDialogScript.new()
|
||||
manual_save_slots_dialog.connect(&"new_slot_requested", _save_new_manual_slot)
|
||||
manual_save_slots_dialog.connect(&"load_slot_requested", _load_selected_manual_slot)
|
||||
add_child(manual_save_slots_dialog)
|
||||
return manual_save_slots_dialog
|
||||
|
||||
|
||||
func _save_new_manual_slot(path: String, display_name: String) -> void:
|
||||
if not ManualSaveSlotsDialogScript.is_fresh_slot(path) or path.get_base_dir() != manual_save_path.get_base_dir():
|
||||
_set_status("新しい保存先を用意できませんでした。元の保存は変更していません。", false)
|
||||
return
|
||||
_save_manual_at_path(path, display_name)
|
||||
|
||||
|
||||
func _show_manual_load_slots() -> void:
|
||||
_ensure_manual_save_slots().show_load_slots(manual_save_path)
|
||||
|
||||
|
||||
func _load_selected_manual_slot(path: String) -> void:
|
||||
if path.get_base_dir() != manual_save_path.get_base_dir() or not ManualSaveSlotsDialogScript.is_owned_slot_name(path.get_file()):
|
||||
_set_status("この保存先を読み込めません。", false)
|
||||
return
|
||||
_load_manual_at_path(path)
|
||||
|
||||
func _capture_interface_selection() -> Dictionary:
|
||||
var selection := {&"version": 1, &"kind": String(_selected_entity_kind), &"id": String(_selected_entity_id), &"staff_open": _staff_management_panel_open, &"guest_open": false}
|
||||
selection[&"product_open"] = _supply_panel_open
|
||||
if supply_management_panel != null:
|
||||
selection[&"product_tab"] = String(supply_management_panel.selected_tab)
|
||||
if staff_management_panel != null:
|
||||
selection[&"staff_id"] = String(staff_management_panel.get("selected_staff_id"))
|
||||
if guest_observer != null:
|
||||
@@ -2181,6 +2328,8 @@ func _on_save_operation_completed(result: Dictionary) -> void:
|
||||
|
||||
|
||||
func _apply_loaded_interface(result: Dictionary) -> void:
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.begin_loaded_epoch(_current_audio_foreground())
|
||||
_pending_build_commands.clear()
|
||||
_pending_excavation_commands.clear()
|
||||
_pending_hire_commands.clear()
|
||||
@@ -2200,6 +2349,7 @@ func _apply_loaded_interface(result: Dictionary) -> void:
|
||||
_restore_interface_selection(result.get(&"manifest", {}))
|
||||
_restore_actor_interface_selection(result.get(&"manifest", {}))
|
||||
_restore_defense_interface_selection(result.get(&"manifest", {}))
|
||||
_restore_product_interface_selection(result.get(&"manifest", {}))
|
||||
_refresh_dashboard()
|
||||
_update_pause_and_speed()
|
||||
|
||||
@@ -2244,13 +2394,12 @@ func _restore_actor_interface_selection(manifest: Variant) -> void:
|
||||
# then restore each optional inspector independently of the map selection.
|
||||
_set_staff_management_panel_open(false)
|
||||
if staff_management_panel != null:
|
||||
staff_management_panel.call("select_staff", &"")
|
||||
staff_management_panel.call("clear_selection")
|
||||
if guest_observer != null:
|
||||
guest_observer.hide()
|
||||
var old_panel := guest_observer.find_child("GuestObservationPanel", true, false) as GuestObservationPanel
|
||||
if old_panel != null:
|
||||
old_panel.selected_guest_id = &""
|
||||
old_panel.refresh()
|
||||
old_panel.clear_selection()
|
||||
if not manifest is Dictionary:
|
||||
return
|
||||
var selection: Variant = manifest.get("interface_selection", {})
|
||||
@@ -2270,6 +2419,34 @@ func _restore_actor_interface_selection(manifest: Variant) -> void:
|
||||
_show_guest_observer()
|
||||
|
||||
|
||||
func _restore_product_interface_selection(manifest: Variant) -> void:
|
||||
# Additive optional presentation: old packages keep their existing behavior.
|
||||
# No purchase draft, quantity, policy, assignment or command is restored here.
|
||||
if supply_management_panel == null or not manifest is Dictionary:
|
||||
return
|
||||
var selection: Variant = manifest.get("interface_selection", {})
|
||||
if not selection is Dictionary:
|
||||
return
|
||||
var version: Variant = selection.get("version", 0)
|
||||
if not (version is int or version is float) or version != 1:
|
||||
return
|
||||
var open_value: Variant = selection.get("product_open", null)
|
||||
var tab_value: Variant = selection.get("product_tab", null)
|
||||
if not open_value is bool or not tab_value is String:
|
||||
return
|
||||
if tab_value not in ["purchase", "kitchen", "delivery"]:
|
||||
return
|
||||
# A captured main panel cannot be both staff and product. Ignore a
|
||||
# contradictory optional product claim without overriding restored staff.
|
||||
if open_value and selection.get("staff_open", false) is bool and selection.get("staff_open", false):
|
||||
return
|
||||
supply_management_panel.select_tab(StringName(tab_value))
|
||||
if open_value:
|
||||
_activate_management_mode(&"product")
|
||||
else:
|
||||
_set_supply_panel_open(false)
|
||||
|
||||
|
||||
func _result_message(result: Variant) -> String:
|
||||
if result is Dictionary:
|
||||
var message := String(result.get(&"message", result.get(&"code", "保存データがありません")))
|
||||
@@ -2434,6 +2611,8 @@ func _on_command_completed(result: Variant) -> void:
|
||||
|
||||
func _set_layer(value: int) -> void:
|
||||
var normalized := clampi(value, 0, 1)
|
||||
if is_instance_valid(_paired_bgm):
|
||||
_paired_bgm.set_layer(normalized)
|
||||
if world_preview != null:
|
||||
world_preview.layer_mode = normalized
|
||||
GameState.active_layer = normalized
|
||||
@@ -2448,6 +2627,8 @@ func _set_layer(value: int) -> void:
|
||||
if _interface_layer >= 0 and _interface_layer != normalized:
|
||||
LayerSkin.apply_tree(self, normalized)
|
||||
if rank_dialog != null: rank_dialog.apply_layer_skin(normalized)
|
||||
if event_log_dialog != null: event_log_dialog.apply_layer_skin(normalized)
|
||||
if manual_save_slots_dialog != null: manual_save_slots_dialog.apply_layer_skin(normalized)
|
||||
_interface_layer = normalized
|
||||
if foreground_event_label != null:
|
||||
foreground_event_label.add_theme_color_override("font_color", Color("ffb498") if normalized == 1 else Color("8f3f29"))
|
||||
@@ -2627,6 +2808,8 @@ func _apply_button_icon(button: Button, icon_texture: Texture2D, maximum_width:
|
||||
|
||||
|
||||
func _apply_button_style(button: Button) -> void:
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.bind_button(button)
|
||||
button.add_theme_color_override("font_color", _visual_color(&"text_light", Color("fff0c8")))
|
||||
button.add_theme_color_override("font_hover_color", Color("fff8e5"))
|
||||
button.add_theme_color_override("font_pressed_color", Color("fff8e5"))
|
||||
@@ -2818,6 +3001,7 @@ func _ensure_defense_manager() -> DefenseManagementPanel:
|
||||
_apply_button_style(defense_manager.get_ok_button())
|
||||
var panel := DefenseManagementPanelScript.new()
|
||||
panel.configure(_runtime, _apply_button_style)
|
||||
panel.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
panel.status_requested.connect(_on_staff_management_status)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.name = "DefenseManagementScroll"
|
||||
@@ -2841,7 +3025,7 @@ func _restore_defense_interface_selection(manifest: Variant) -> void:
|
||||
if defense_manager != null:
|
||||
defense_manager.hide()
|
||||
var existing := defense_manager.find_child("DefenseManagementPanel", true, false) as DefenseManagementPanel
|
||||
if existing != null: existing.select_raid(&"")
|
||||
if existing != null: existing.clear_selection()
|
||||
if not manifest is Dictionary: return
|
||||
var selection: Variant = manifest.get("interface_selection", {})
|
||||
if not selection is Dictionary or selection.get("version", 0) != 1: return
|
||||
@@ -2852,3 +3036,45 @@ func _restore_defense_interface_selection(manifest: Variant) -> void:
|
||||
_ensure_defense_manager().select_raid(raid_id)
|
||||
if selection.get("defense_open", false) is bool and selection.get("defense_open", false):
|
||||
_show_defense_manager()
|
||||
|
||||
|
||||
func _setup_ui_audio() -> void:
|
||||
_paired_bgm = PairedBGMScript.new()
|
||||
_paired_bgm.name = "PairedBGM"
|
||||
_paired_bgm.configure(GameState.active_layer)
|
||||
add_child(_paired_bgm)
|
||||
_audio_feedback = UIAudioFeedbackScript.new()
|
||||
_audio_feedback.name = "UIAudioFeedback"
|
||||
_audio_feedback.configure(_runtime, audio_settings_path)
|
||||
add_child(_audio_feedback)
|
||||
_audio_feedback.begin_loaded_epoch(_current_audio_foreground())
|
||||
_audio_settings_dialog = UIAudioSettingsDialogScript.new()
|
||||
_audio_settings_dialog.configure(_audio_feedback, _apply_button_style)
|
||||
add_child(_audio_settings_dialog)
|
||||
|
||||
|
||||
func _current_audio_foreground() -> Variant:
|
||||
var events: Variant = _service(&"event_service")
|
||||
if events != null and events.has_method(&"get_foreground_instance"):
|
||||
return events.call(&"get_foreground_instance")
|
||||
return null
|
||||
|
||||
|
||||
func _show_audio_settings() -> void:
|
||||
if _audio_settings_dialog != null:
|
||||
_audio_settings_dialog.show_settings()
|
||||
|
||||
|
||||
func _on_audio_world_selection(_id: StringName) -> void:
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.request_click()
|
||||
|
||||
|
||||
func _submit_ui_command(
|
||||
command_type: StringName, payload: Dictionary = {}, actor_id: StringName = &"player",
|
||||
target_id: StringName = &"", priority := 0, execute_at := -1,
|
||||
idempotency_key: StringName = &""
|
||||
) -> Variant:
|
||||
if _audio_feedback != null:
|
||||
return _audio_feedback.submit_command(command_type, payload, actor_id, target_id, priority, execute_at, idempotency_key)
|
||||
return _runtime.call(&"submit_command", command_type, payload, actor_id, target_id, priority, execute_at, idempotency_key)
|
||||
|
||||
Reference in New Issue
Block a user