extends Control const OpeningGuideScript := preload("res://scripts/ui/inn_opening_guide.gd") const OperatingLedgerPanelScript := preload("res://scripts/ui/operating_ledger_panel.gd") const StaffManagementPanelScript := preload("res://scripts/ui/staff_management_panel.gd") const SupplyManagementPanelScript := preload("res://scripts/ui/supply_management_panel.gd") const BusinessManagementPanelScript := preload("res://scripts/ui/business_management_panel.gd") const WorldPreviewScript := preload("res://scenes/main/world_preview.gd") const DefaultVisualTheme := preload("res://assets/themes/japanese_fantasy/visual-theme-v1.tres") const MANUAL_SAVE_PATH := "user://saves/manual_ui" const MVP_FACILITY_IDS: Array[StringName] = [ &"reception", &"standard_room", &"cafeteria", &"warehouse", &"kitchen", &"public_bath", &"staff_waiting_room", &"security_station", &"tavern", &"massage_room", &"guest_elevator", &"service_elevator", &"staff_door", &"guest_corridor", &"service_corridor", &"adult_private_room", ] const FACILITY_STATE_NAMES := [ "故障", "到達不能", "魔力不足", "人員不足", "閉鎖中", "低下運転", "営業中", ] const ELEVATOR_PHASE_NAMES := {&"IDLE": "待機", &"PICKUP": "迎車中", &"BOARDING": "乗車中", &"TRAVEL": "運行中", &"ALIGHTING": "降車中"} const FACILITY_REASON_NAMES := { &"BROKEN": "故障", &"NO_ACCESS": "経路なし", &"NO_POWER": "魔力供給なし", &"NO_STAFF": "必要人員不足", &"CLOSED_BY_OPERATOR": "手動閉鎖", &"OUTSIDE_OPENING_HOURS": "営業時間外", &"LOW_DURABILITY": "耐久度低下", } const PREVIEW_REASON_NAMES := { "INVALID_DEFINITION": "施設データが不正です", "MISSING_DEPENDENCY": "建設システムを利用できません", "OUT_OF_BOUNDS": "世界の範囲外です", "ZONE_MISMATCH": "地上・境界・地下の条件が合いません", "LAYER_MISMATCH": "表層/裏層が合いません", "OVERLAP": "ほかの施設または工事と重なります", "UNSUPPORTED": "構造を支えられません", "INSUFFICIENT_RESOURCES": "建設資源が不足しています", } const EXCAVATION_REASON_NAMES := { &"OUT_OF_BOUNDS": "世界の範囲外です", &"NOT_UNDERGROUND": "地下のマスだけ掘削できます", &"ALREADY_EXCAVATED": "すでに掘削済みです", &"ALREADY_RESERVED": "このマスは掘削中です", &"NO_EXCAVATED_NEIGHBOR": "掘削済みの隣接マスから延ばしてください", &"NO_ENTRANCE_CONNECTION": "地上入口までつながる経路がありません", &"SUPPORT_REQUIRED": "硬い地盤を支える補強施設が必要です", &"INSUFFICIENT_RESOURCES": "掘削資源が不足しています", &"MISSING_DEPENDENCY": "掘削システムを利用できません", } const EVENT_CHOICE_NAMES := { &"event.relationship.threshold.acknowledge": "向き合う", &"event.relationship.threshold.defer": "今は保留する", } const STAFF_ROLE_NAMES := { &"attendant": "接客", &"bartender": "酒場", &"clerk": "会計", &"cleaner": "清掃", &"cook": "調理", &"guard": "警備", &"maintenance": "保守", &"masseur": "施術", &"medic": "医療", &"porter": "物流", &"receptionist": "受付", } const STAFF_SHIFT_NAMES := { &"day": "日勤", &"night": "夜勤", &"any": "柔軟勤務", } const STAFF_SKILL_NAMES := { &"service": "接客", &"operation": "運営", &"combat": "戦闘", &"underworld": "裏社会", } @export var visual_theme: VisualThemeProfile = DefaultVisualTheme var manual_save_path := MANUAL_SAVE_PATH var time_label: Label var phase_label: Label var guest_count_label: Label var lodging_count_label: Label var alert_count_label: Label var alert_icon_node: TextureRect var resource_labels: Dictionary = {} var resource_icon_nodes: Dictionary = {} var world_preview: WorldPreview var front_button: Button var back_button: Button var excavation_button: Button var pause_button: Button var speed_buttons: Dictionary = {} var dashboard_summary_label: Label var foreground_event_panel: VBoxContainer var foreground_event_label: Label var foreground_event_choices: VBoxContainer var staff_management_panel: VBoxContainer var staff_management_toggle_button: Button var _staff_management_panel_open := false var staff_candidate_panel: VBoxContainer var staff_candidate_summary_label: Label var staff_candidate_toggle_button: Button var staff_candidate_buttons: Dictionary = {} var selected_heading_label: Label var selected_detail_label: Label var selected_entity_icon: TextureRect var selected_elevator_controls: VBoxContainer var elevator_operation_button: Button var elevator_operation_hint: Label var build_status_label: Label var build_buttons: Dictionary = {} var navigation_buttons: Dictionary = {} var opening_guide: Control var operating_ledger_panel: VBoxContainer var supply_management_panel: VBoxContainer var business_management_panel: VBoxContainer var _business_panel_open := false var _supply_panel_open := false var operations_controls: VBoxContainer var _ledger_panel_open := false var management_panel: Control var management_toggle_button: Button var palette_scroll: ScrollContainer var palette_toggle_button: Button var _management_panel_open := false var _build_palette_open := false var _runtime: Variant = null var _selected_entity_id: StringName = &"" var _selected_entity_kind: StringName = &"" var _selected_definition_id: StringName = &"" var _selected_definition: Variant = null var _build_rotation := 0 var _build_sequence := 1 var _excavation_sequence := 1 var _hire_sequence := 1 var _elevator_sequence := 1 var _pending_elevator_commands: Dictionary = {} var _pending_build_commands: Dictionary = {} var _excavation_mode := false var _pending_excavation_commands: Dictionary = {} var _pending_hire_commands: Dictionary = {} var _staff_candidate_panel_open := false var _operations_start_report: Dictionary = {} func _ready() -> void: _runtime = get_node_or_null("/root/SimulationRuntime") _pause_fresh_opening() if _runtime != null and _runtime.has_method("enable_production_save_policy"): _runtime.enable_production_save_policy() if _runtime != null \ and _runtime.has_method("get_operations_report") \ and _runtime.has_method("enable_production_operations"): var current_operations: Dictionary = _runtime.call(&"get_operations_report") if not bool(current_operations.get(&"enabled", false)): _operations_start_report = _runtime.call(&"enable_production_operations") _synchronize_ui_command_state() _build_interface() _connect_signals() _populate_build_palette() _populate_staff_candidates() _on_time_changed(GameClock.day, GameClock.get_hour(), GameClock.get_minute()) _on_phase_changed(GameClock.get_phase()) for resource_name: StringName in GameState.resources: _on_resource_changed(resource_name, GameState.get_resource(resource_name)) _set_layer(GameState.active_layer) _update_pause_and_speed() _refresh_dashboard() if not _operations_start_report.is_empty() \ and not bool(_operations_start_report.get(&"ok", false)): _set_status( "本番運営を開始できませんでした: %s" % _result_message(_operations_start_report), false ) func _pause_fresh_opening() -> void: if _runtime == null or GameClock.absolute_minute != 360: return if not (_runtime.get(&"latest_restore_safety_report") as Dictionary).is_empty(): return var operations: Dictionary = _runtime.call(&"get_operations_report") if bool(operations.get(&"enabled", false)): return if not _call_array(_service(&"construction_service"), &"get_facilities").is_empty() \ or not _call_array(_service(&"construction_service"), &"get_jobs").is_empty() \ or not _call_array(_service(&"staff_service"), &"get_staff_members").is_empty(): return var queue: Variant = _runtime.get(&"command_queue") if queue != null and not queue.get_accepted_command_metadata().is_empty(): return GameClock.set_paused(true) func _unhandled_key_input(event: InputEvent) -> void: if not event.pressed or event.echo: return var handled := true match event.keycode: KEY_ESCAPE: _clear_active_tool() KEY_E: _toggle_excavation_mode() KEY_H: _toggle_staff_candidate_panel() KEY_B: _toggle_build_palette() KEY_I: _set_management_panel_open(not _management_panel_open) KEY_SPACE: _toggle_pause() KEY_TAB: _set_layer(1 - world_preview.layer_mode) KEY_1: _set_speed(1) KEY_2: _set_speed(2) KEY_4: _set_speed(4) KEY_R: if _selected_definition == null: handled = false else: _rotate_build_preview() _: handled = false if handled: get_viewport().set_input_as_handled() func _build_interface() -> void: var background := ColorRect.new() background.color = _visual_color(&"window_background", Color("24170f")) background.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) background.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(background) var root_margin := MarginContainer.new() root_margin.add_theme_constant_override("margin_left", 6) root_margin.add_theme_constant_override("margin_top", 6) root_margin.add_theme_constant_override("margin_right", 6) root_margin.add_theme_constant_override("margin_bottom", 6) root_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(root_margin) var layout := VBoxContainer.new() layout.add_theme_constant_override("separation", 4) root_margin.add_child(layout) layout.add_child(_create_top_bar()) var content := HBoxContainer.new() content.name = "MainContent" content.size_flags_vertical = Control.SIZE_EXPAND_FILL content.add_theme_constant_override("separation", 4) layout.add_child(content) content.add_child(_create_navigation_rail()) var map_panel := PanelContainer.new() map_panel.name = "MapPanel" map_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL map_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL map_panel.add_theme_stylebox_override( "panel", _themed_panel_style(&"panel_outer", _visual_color(&"window_background", Color("1e1712")), _visual_color(&"border_gold", Color("9b6a32")), 8, 5) ) content.add_child(map_panel) world_preview = WorldPreviewScript.new() world_preview.name = "WorldPreview" world_preview.custom_minimum_size = Vector2(600, 380) world_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL world_preview.size_flags_vertical = Control.SIZE_EXPAND_FILL world_preview.set_visual_theme(visual_theme) map_panel.add_child(world_preview) opening_guide = OpeningGuideScript.new() world_preview.add_child(opening_guide) opening_guide.setup(_runtime, GameClock) opening_guide.position = Vector2(8, 8) opening_guide.size = Vector2(302, 0) opening_guide.action_requested.connect(_on_opening_action) # The inspector overlays the map only when requested, leaving the full # cross-section available while watching the simulation. management_panel = _create_side_panel() world_preview.add_child(management_panel) management_panel.set_anchors_and_offsets_preset(Control.PRESET_RIGHT_WIDE) management_panel.offset_left = -320 management_panel.offset_right = -8 management_panel.offset_top = 8 management_panel.offset_bottom = -8 management_panel.visible = false layout.add_child(_create_bottom_bar()) func _create_top_bar() -> Control: var panel := PanelContainer.new() panel.name = "TopBar" panel.custom_minimum_size.y = 46 panel.add_theme_stylebox_override("panel", _themed_panel_style(&"panel_wood", _visual_color(&"dark_panel", Color("6b3e1d")), _visual_color(&"border_gold", Color("d5a552")), 8, 8)) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 7) panel.add_child(row) var title_box := VBoxContainer.new() title_box.add_theme_constant_override("separation", 0) row.add_child(title_box) var title := Label.new() title.text = "THE TREE" title.custom_minimum_size.x = 98 title.add_theme_font_size_override("font_size", 17) title.add_theme_color_override("font_color", Color("fff0c8")) title_box.add_child(title) var subtitle := Label.new() subtitle.text = "世界樹の宿" subtitle.add_theme_font_size_override("font_size", 10) subtitle.add_theme_color_override("font_color", Color("e5c992")) title_box.add_child(subtitle) row.add_child(VSeparator.new()) time_label = Label.new() time_label.custom_minimum_size.x = 104 time_label.add_theme_font_size_override("font_size", 12) time_label.add_theme_color_override("font_color", Color("fff5d9")) row.add_child(time_label) phase_label = Label.new() phase_label.custom_minimum_size.x = 48 phase_label.add_theme_font_size_override("font_size", 12) phase_label.add_theme_color_override("font_color", Color("ffd37a")) row.add_child(phase_label) guest_count_label = _hud_value_label("来客 0人", 72) lodging_count_label = _hud_value_label("宿泊 0人", 72) row.add_child(guest_count_label) row.add_child(lodging_count_label) for resource_name: StringName in [&"gold", &"mana", &"materials", &"faith"]: var resource_block := HBoxContainer.new() resource_block.add_theme_constant_override("separation", 2) var resource_icon := TextureRect.new() resource_icon.name = "%sIcon" % String(resource_name).capitalize() resource_icon.texture = visual_theme.resource_icon(resource_name) if visual_theme != null else null resource_icon.custom_minimum_size = Vector2(16, 16) resource_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE resource_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED resource_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE resource_icon_nodes[resource_name] = resource_icon resource_block.add_child(resource_icon) var label := _hud_value_label("", 54) label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT resource_labels[resource_name] = label resource_block.add_child(label) row.add_child(resource_block) alert_count_label = _hud_value_label("警戒 0", 64) alert_count_label.add_theme_color_override("font_color", Color("ffb095")) alert_icon_node = _icon_rect(visual_theme.control_icon(&"alert") if visual_theme != null else null, 20) alert_icon_node.name = "AlertIcon" row.add_child(alert_icon_node) row.add_child(alert_count_label) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(spacer) pause_button = _button("停止", _toggle_pause, 52) row.add_child(pause_button) for value: int in [1, 2, 4]: var speed_button := _button("x%d" % value, _set_speed.bind(value), 42) speed_button.toggle_mode = true speed_buttons[value] = speed_button row.add_child(speed_button) return panel func _create_navigation_rail() -> Control: var panel := PanelContainer.new() panel.name = "NavigationRail" panel.custom_minimum_size.x = 64 panel.size_flags_vertical = Control.SIZE_EXPAND_FILL panel.add_theme_stylebox_override("panel", _themed_panel_style(&"panel_leather", _visual_color(&"dark_panel_alt", Color("513018")), _visual_color(&"border_gold", Color("bd8540")), 8, 7)) var column := VBoxContainer.new() column.add_theme_constant_override("separation", 4) panel.add_child(column) var entries := [ [&"staff", "従業員"], [&"facility", "施設"], [&"product", "商品"], [&"business", "営業"], [&"explore", "探索"], [&"investment", "投資"], [&"ledger", "帳簿"], ] for entry: Array in entries: var mode := StringName(entry[0]) var button := _button(String(entry[1]), _activate_management_mode.bind(mode), 50) button.custom_minimum_size.y = 46 button.toggle_mode = true button.add_theme_font_size_override("font_size", 12) _apply_button_icon(button, visual_theme.navigation_icon(&"ledger" if mode == &"business" else mode) if visual_theme != null else null, 16) navigation_buttons[mode] = button column.add_child(button) var nav_spacer := Control.new() nav_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL column.add_child(nav_spacer) var layer_hint := Label.new() layer_hint.text = "TAB\n表 / 裏" layer_hint.tooltip_text = "ホイール: 拡大縮小\n中ボタンドラッグ: 移動\nHome: 全体表示" layer_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER layer_hint.add_theme_font_size_override("font_size", 10) layer_hint.add_theme_color_override("font_color", Color("e2c38c")) column.add_child(layer_hint) return panel func _create_side_panel() -> Control: var panel := PanelContainer.new() panel.name = "SidePanel" panel.custom_minimum_size.x = 312 panel.size_flags_vertical = Control.SIZE_EXPAND_FILL panel.add_theme_stylebox_override("panel", _themed_panel_style(&"panel_parchment", _visual_color(&"light_panel", Color("f4e3bd")), _visual_color(&"border_gold", Color("a96f2f")), 8, 10)) var scroll := ScrollContainer.new() scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL panel.add_child(scroll) var column := VBoxContainer.new() column.custom_minimum_size.x = 278 column.size_flags_horizontal = Control.SIZE_EXPAND_FILL column.add_theme_constant_override("separation", 8) scroll.add_child(column) var root_column := column var inspector_heading := HBoxContainer.new() var inspector_title := _section_heading("運営状況") inspector_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL inspector_heading.add_child(inspector_title) inspector_heading.add_child(_button("閉じる I", _set_management_panel_open.bind(false), 64)) column.add_child(inspector_heading) dashboard_summary_label = Label.new() dashboard_summary_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART dashboard_summary_label.add_theme_color_override("font_color", Color("4d341f")) dashboard_summary_label.add_theme_font_size_override("font_size", 13) column.add_child(dashboard_summary_label) foreground_event_panel = VBoxContainer.new() foreground_event_panel.name = "ForegroundEventPanel" foreground_event_panel.add_theme_constant_override("separation", 6) foreground_event_panel.visible = false column.add_child(foreground_event_panel) foreground_event_panel.add_child(_section_heading("要対応イベント")) foreground_event_label = Label.new() foreground_event_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART foreground_event_label.add_theme_color_override("font_color", Color("8f3f29")) foreground_event_label.add_theme_font_size_override("font_size", 13) foreground_event_panel.add_child(foreground_event_label) foreground_event_choices = VBoxContainer.new() foreground_event_choices.name = "ForegroundEventChoices" foreground_event_choices.add_theme_constant_override("separation", 5) foreground_event_panel.add_child(foreground_event_choices) column.add_child(HSeparator.new()) operations_controls = VBoxContainer.new() operations_controls.name = "OperationsControls" operations_controls.add_theme_constant_override("separation", 8) column.add_child(operations_controls) column = operations_controls var candidate_heading_row := HBoxContainer.new() candidate_heading_row.add_child(_section_heading("スタッフ")) var candidate_heading_spacer := Control.new() candidate_heading_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL candidate_heading_row.add_child(candidate_heading_spacer) staff_management_toggle_button = _button("雇用済み", _toggle_staff_management_panel, 62) staff_management_toggle_button.name = "StaffManagementToggleButton" staff_management_toggle_button.toggle_mode = true candidate_heading_row.add_child(staff_management_toggle_button) staff_candidate_toggle_button = _button( "候補を見る H", _toggle_staff_candidate_panel, 108 ) staff_candidate_toggle_button.name = "StaffCandidateToggleButton" candidate_heading_row.add_child(staff_candidate_toggle_button) column.add_child(candidate_heading_row) staff_management_panel = StaffManagementPanelScript.new() staff_management_panel.configure(_runtime, _apply_button_style) staff_management_panel.status_requested.connect(_on_staff_management_status) staff_management_panel.visible = false column.add_child(staff_management_panel) staff_candidate_panel = VBoxContainer.new() staff_candidate_panel.name = "StaffCandidatePanel" staff_candidate_panel.visible = false staff_candidate_panel.add_theme_constant_override("separation", 7) column.add_child(staff_candidate_panel) staff_candidate_summary_label = Label.new() staff_candidate_summary_label.name = "StaffCandidateSummary" staff_candidate_summary_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART staff_candidate_summary_label.add_theme_color_override("font_color", Color("765438")) staff_candidate_summary_label.add_theme_font_size_override("font_size", 12) staff_candidate_panel.add_child(staff_candidate_summary_label) var candidate_list := VBoxContainer.new() candidate_list.name = "StaffCandidateList" candidate_list.add_theme_constant_override("separation", 7) staff_candidate_panel.add_child(candidate_list) column.add_child(HSeparator.new()) selected_heading_label = _section_heading("選択中の施設") column.add_child(selected_heading_label) var selected_row := HBoxContainer.new() selected_row.add_theme_constant_override("separation", 8) column.add_child(selected_row) selected_entity_icon = TextureRect.new() selected_entity_icon.name = "SelectedEntityIcon" selected_entity_icon.custom_minimum_size = Vector2(24, 24) selected_entity_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE selected_entity_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED selected_entity_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE selected_row.add_child(selected_entity_icon) selected_detail_label = Label.new() selected_detail_label.text = "地図上の施設をクリックすると、営業状態を確認できます。" selected_detail_label.custom_minimum_size.y = 112 selected_detail_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL selected_detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART selected_detail_label.add_theme_color_override("font_color", Color("4d341f")) selected_detail_label.add_theme_font_size_override("font_size", 13) selected_row.add_child(selected_detail_label) selected_elevator_controls = VBoxContainer.new() selected_elevator_controls.name = "SelectedElevatorControls" selected_elevator_controls.visible = false column.add_child(selected_elevator_controls) elevator_operation_button = _button("昇降機を停止", _on_elevator_operation_pressed, 160) elevator_operation_button.name = "ElevatorOperationButton" selected_elevator_controls.add_child(elevator_operation_button) elevator_operation_hint = Label.new() elevator_operation_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART elevator_operation_hint.add_theme_font_size_override("font_size", 12) elevator_operation_hint.add_theme_color_override("font_color", Color("654d37")) selected_elevator_controls.add_child(elevator_operation_hint) operating_ledger_panel = OperatingLedgerPanelScript.new() root_column.add_child(operating_ledger_panel) operating_ledger_panel.setup(_runtime, GameClock) operating_ledger_panel.visible = false supply_management_panel = SupplyManagementPanelScript.new() supply_management_panel.configure(_runtime, _apply_button_style) 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.status_requested.connect(_on_staff_management_status) business_management_panel.action_requested.connect(_activate_management_mode) business_management_panel.visible = false root_column.add_child(business_management_panel) return panel func _create_bottom_bar() -> Control: var panel := PanelContainer.new() panel.name = "BottomBar" panel.add_theme_stylebox_override("panel", _themed_panel_style(&"panel_wood", _visual_color(&"dark_panel", Color("24382e")), _visual_color(&"border_gold", Color("7d8862")), 0, 5)) var column := VBoxContainer.new() column.add_theme_constant_override("separation", 4) panel.add_child(column) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 5) column.add_child(row) palette_toggle_button = _button("建築 B", _toggle_build_palette, 76) palette_toggle_button.name = "BuildPaletteToggleButton" palette_toggle_button.toggle_mode = true row.add_child(palette_toggle_button) front_button = _button("客用・表層", _set_layer.bind(0), 96) back_button = _button("裏方・裏層", _set_layer.bind(1), 96) _apply_button_icon(front_button, visual_theme.control_icon(&"front_layer") if visual_theme != null else null, 16) _apply_button_icon(back_button, visual_theme.control_icon(&"back_layer") if visual_theme != null else null, 16) front_button.toggle_mode = true back_button.toggle_mode = true row.add_child(front_button) row.add_child(back_button) excavation_button = _button("掘削 E", _toggle_excavation_mode, 72) excavation_button.name = "ExcavationButton" excavation_button.toggle_mode = true _apply_button_icon(excavation_button, visual_theme.control_icon(&"excavate") if visual_theme != null else null, 16) row.add_child(excavation_button) row.add_child(VSeparator.new()) var rotate_button := _button("回転 R", _rotate_build_preview, 72) _apply_button_icon(rotate_button, visual_theme.control_icon(&"rotate") if visual_theme != null else null, 16) row.add_child(rotate_button) row.add_child(_button("解除 Esc", _clear_active_tool, 80)) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(spacer) management_toggle_button = _button("運営 I", _toggle_management_panel, 72) management_toggle_button.name = "ManagementToggleButton" management_toggle_button.toggle_mode = true row.add_child(management_toggle_button) var save_button := _button("保存", _save_manual, 62) _apply_button_icon(save_button, visual_theme.control_icon(&"save") if visual_theme != null else null, 16) row.add_child(save_button) row.add_child(_button("読込", _load_manual, 56)) palette_scroll = ScrollContainer.new() palette_scroll.name = "BuildPaletteScroll" palette_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO palette_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED palette_scroll.custom_minimum_size.y = 62 palette_scroll.visible = false column.add_child(palette_scroll) var grid := GridContainer.new() grid.name = "BuildPalette" grid.columns = MVP_FACILITY_IDS.size() grid.add_theme_constant_override("h_separation", 4) grid.add_theme_constant_override("v_separation", 0) palette_scroll.add_child(grid) build_status_label = Label.new() build_status_label.name = "BuildStatusLabel" build_status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL build_status_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS build_status_label.add_theme_font_size_override("font_size", 11) build_status_label.add_theme_color_override("font_color", Color("c7d3a8")) column.add_child(build_status_label) return panel func _toggle_build_palette() -> void: _set_build_palette_open(not _build_palette_open) func _set_build_palette_open(open: bool) -> void: _build_palette_open = open if palette_scroll != null: palette_scroll.visible = open if palette_toggle_button != null: palette_toggle_button.set_pressed_no_signal(open) palette_toggle_button.text = "建築を閉じる B" if open else "建築 B" func _toggle_management_panel() -> void: _set_management_panel_open(not _management_panel_open) func _set_management_panel_open(open: bool) -> void: _management_panel_open = open if management_panel != null: management_panel.visible = open if management_toggle_button != null: management_toggle_button.set_pressed_no_signal(open) if not open: _set_staff_candidate_panel_open(false) _set_staff_management_panel_open(false) _set_ledger_panel_open(false) _set_supply_panel_open(false) _set_business_panel_open(false) func _activate_management_mode(mode: StringName) -> void: _set_management_panel_open(mode != &"facility") _set_supply_panel_open(mode == &"product") _set_business_panel_open(mode == &"business") _set_ledger_panel_open(mode == &"ledger") for raw_mode: Variant in navigation_buttons: (navigation_buttons[raw_mode] as Button).set_pressed_no_signal(StringName(raw_mode) == mode) match mode: &"staff": _set_staff_management_panel_open(true) &"facility": _set_build_palette_open(true) _set_staff_candidate_panel_open(false) _set_status("建築する施設を下のパレットから選んでください。", true, true) &"business": _set_status("施設ごとの料金と新規受付の開閉を設定できます。", true, true) &"product": _set_status("倉庫への仕入れ、厨房の自動生産、施設への配送を確認できます。", true, true) &"explore": _set_status("探索管理は一覧画面の実装準備中です。", true, true) &"investment": _set_status("投資管理は一覧画面の実装準備中です。", true, true) &"ledger": _set_status("本日の資金増減と、06:00に確定した営業日の損益を表示します。", true, true) func _set_ledger_panel_open(open: bool) -> void: _ledger_panel_open = open if open: _set_supply_panel_open(false) _set_business_panel_open(false) if operating_ledger_panel != null: operating_ledger_panel.visible = open if open: operating_ledger_panel.refresh() if operations_controls != null: operations_controls.visible = not open and not _supply_panel_open and not _business_panel_open if dashboard_summary_label != null: dashboard_summary_label.visible = not open and not _supply_panel_open and not _business_panel_open if open: _set_staff_candidate_panel_open(false) _set_staff_management_panel_open(false) func _set_supply_panel_open(open: bool) -> void: _supply_panel_open = open if open: _set_business_panel_open(false) _set_ledger_panel_open(false) _set_staff_candidate_panel_open(false) _set_staff_management_panel_open(false) if supply_management_panel != null: supply_management_panel.visible = open if open: supply_management_panel.refresh() if operations_controls != null: operations_controls.visible = not open and not _ledger_panel_open and not _business_panel_open if dashboard_summary_label != null: dashboard_summary_label.visible = not open and not _ledger_panel_open and not _business_panel_open func _set_business_panel_open(open: bool) -> void: _business_panel_open = open if open: _set_ledger_panel_open(false) _set_supply_panel_open(false) _set_staff_candidate_panel_open(false) _set_staff_management_panel_open(false) if business_management_panel != null: business_management_panel.visible = open if open: business_management_panel.refresh() if operations_controls != null: operations_controls.visible = not open and not _ledger_panel_open and not _supply_panel_open if dashboard_summary_label != null: dashboard_summary_label.visible = not open and not _ledger_panel_open and not _supply_panel_open func _on_opening_action(action: StringName) -> void: match action: &"build_reception", &"build_room", &"build_corridor": _set_ledger_panel_open(false) _set_management_panel_open(false) _set_build_palette_open(true) var definitions := {&"build_reception": &"reception", &"build_room": &"standard_room", &"build_corridor": &"guest_corridor"} _select_build_definition(definitions[action]) &"staff": _activate_management_mode(&"staff") if _call_array(_service(&"staff_service"), &"get_staff_members").is_empty(): _set_staff_candidate_panel_open(true) &"resume": if GameClock.paused: _toggle_pause() &"ledger": _activate_management_mode(&"ledger") func _connect_signals() -> void: GameClock.time_changed.connect(_on_time_changed) GameClock.phase_changed.connect(_on_phase_changed) GameState.resource_changed.connect(_on_resource_changed) world_preview.facility_selected.connect(_on_facility_selected) world_preview.construction_selected.connect(_on_construction_selected) 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) if _runtime != null and _runtime is Object: if _runtime.has_signal(&"snapshot_published"): _runtime.connect(&"snapshot_published", Callable(self, "_on_snapshot_published")) if _runtime.has_signal(&"command_completed"): _runtime.connect(&"command_completed", Callable(self, "_on_command_completed")) func _populate_build_palette() -> void: var palette := find_child("BuildPalette", true, false) as GridContainer if palette == null: return var registry: Variant = _service(&"definition_registry") for definition_id: StringName in MVP_FACILITY_IDS: var definition: Variant = null if registry != null and registry.has_method(&"get_facility"): definition = registry.call(&"get_facility", definition_id) if definition == null: continue var button := Button.new() button.toggle_mode = true button.custom_minimum_size = Vector2(116, 50) button.text = _build_button_text(definition) button.tooltip_text = String(_definition_value(definition, &"description", "")) button.add_theme_font_size_override("font_size", 12) _apply_button_icon(button, visual_theme.facility_icon(definition_id) if visual_theme != null else null, 22) _apply_button_style(button) button.toggled.connect(_on_build_button_toggled.bind(definition_id)) build_buttons[definition_id] = button palette.add_child(button) if build_buttons.is_empty(): _set_status("施設定義を読み込めませんでした。", false) else: _set_status("B 建築 / H 雇用 / I 運営 ホイール 拡大縮小・中ボタン 移動・Home 全体表示", true, true) func _populate_staff_candidates() -> void: var list := find_child("StaffCandidateList", true, false) as VBoxContainer if list == null: return for child: Node in list.get_children(): list.remove_child(child) child.queue_free() staff_candidate_buttons.clear() var summaries := _staff_candidate_summaries() for summary: Dictionary in summaries: var candidate_id := StringName(summary.get(&"candidate_id", &"")) if candidate_id.is_empty(): continue var card := PanelContainer.new() card.name = "Candidate_%s" % String(candidate_id) card.add_theme_stylebox_override( "panel", _themed_panel_style(&"panel_leather", Color("132630"), Color("31505d"), 7, 8) ) var content := VBoxContainer.new() content.add_theme_constant_override("separation", 4) card.add_child(content) var detail := Label.new() detail.name = "CandidateDetail_%s" % String(candidate_id) detail.text = _staff_candidate_text(summary) detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART detail.add_theme_color_override("font_color", Color("c8d7dc")) detail.add_theme_font_size_override("font_size", 12) content.add_child(detail) var hire_button := Button.new() hire_button.name = "Hire_%s" % String(candidate_id) hire_button.text = "雇用する" hire_button.custom_minimum_size.y = 30 _apply_button_style(hire_button) hire_button.pressed.connect(_hire_staff_candidate.bind(candidate_id)) content.add_child(hire_button) staff_candidate_buttons[candidate_id] = hire_button list.add_child(card) _refresh_staff_candidates() func _staff_candidate_summaries() -> Array[Dictionary]: if _runtime == null or not _runtime.has_method(&"get_mvp_staff_candidate_summaries"): return [] var value: Variant = _runtime.call(&"get_mvp_staff_candidate_summaries") var result: Array[Dictionary] = [] if value is Array: for item: Variant in value: if item is Dictionary: result.append((item as Dictionary).duplicate(true)) return result func _staff_candidate_text(summary: Dictionary) -> String: var candidate_id := StringName(summary.get(&"candidate_id", &"")) var display_name := String(summary.get(&"display_name", candidate_id)) var role_id := StringName(summary.get(&"role_id", &"")) var shift_id := StringName(summary.get(&"preferred_shift", &"any")) var skills_value: Variant = summary.get(&"skills", {}) var skills: Dictionary = skills_value if skills_value is Dictionary else {} var skill_parts: Array[String] = [] for skill_id: StringName in [&"service", &"operation", &"combat", &"underworld"]: skill_parts.append("%s%d" % [ String(STAFF_SKILL_NAMES.get(skill_id, String(skill_id))), int(skills.get(skill_id, 0)), ]) return "%s(%d歳) %s / %s\n契約 G%d 日給 G%d\n技能 %s" % [ display_name, int(summary.get(&"age", 0)), String(STAFF_ROLE_NAMES.get(role_id, String(role_id))), String(STAFF_SHIFT_NAMES.get(shift_id, String(shift_id))), int(summary.get(&"signing_cost", 0)), int(summary.get(&"daily_wage", 0)), "・".join(skill_parts), ] func _toggle_staff_management_panel() -> void: _set_staff_management_panel_open(not _staff_management_panel_open) func _set_staff_management_panel_open(open: bool) -> void: _staff_management_panel_open = open if staff_management_panel != null: staff_management_panel.visible = open if staff_management_toggle_button != null: staff_management_toggle_button.set_pressed_no_signal(open) if open: _set_ledger_panel_open(false) _set_supply_panel_open(false) _set_business_panel_open(false) _set_staff_candidate_panel_open(false) _set_management_panel_open(true) staff_management_panel.refresh() _set_status("従業員を選び、配属先と毎日の勤務帯を変更できます。", true, true) func _on_staff_management_status(message: String, success: bool) -> void: _set_status(message, success) func _toggle_staff_candidate_panel() -> void: _set_staff_candidate_panel_open(not _staff_candidate_panel_open) func _set_staff_candidate_panel_open(open: bool) -> void: _staff_candidate_panel_open = open if staff_candidate_panel != null: staff_candidate_panel.visible = open _refresh_staff_candidates() if open: _set_ledger_panel_open(false) _set_supply_panel_open(false) _set_business_panel_open(false) _set_staff_management_panel_open(false) _set_management_panel_open(true) _set_status("雇用候補を選んでください。契約金と日給は確定前に表示されます。", true, true) func _refresh_staff_candidates() -> void: if staff_candidate_panel == null or staff_candidate_summary_label == null: return var summaries := _staff_candidate_summaries() var hired_count := 0 for summary: Dictionary in summaries: var candidate_id := StringName(summary.get(&"candidate_id", &"")) var hired := bool(summary.get(&"hired", false)) if hired: hired_count += 1 var button := staff_candidate_buttons.get(candidate_id) as Button if button == null: continue var pending := _pending_hire_commands.values().has(candidate_id) button.disabled = hired or pending button.text = "雇用済み" if hired else "手続き中…" if pending else "雇用する" staff_candidate_summary_label.text = "未雇用 %d人 / 雇用済み %d人" % [ maxi(summaries.size() - hired_count, 0), hired_count, ] if staff_candidate_toggle_button != null: staff_candidate_toggle_button.text = ( "候補を閉じる H" if _staff_candidate_panel_open else "候補を見る H(%d)" % maxi(summaries.size() - hired_count, 0) ) func _hire_staff_candidate(candidate_id: StringName) -> void: if _runtime == null or not _runtime.has_method(&"submit_command"): _set_status("雇用手続きを送信できません。", false) return if _pending_hire_commands.values().has(candidate_id): _set_status("この候補の雇用手続きはすでに受理されています。", false) return var summaries := _staff_candidate_summaries() var selected: Dictionary = {} for summary: Dictionary in summaries: if StringName(summary.get(&"candidate_id", &"")) == candidate_id: selected = summary break if selected.is_empty(): _set_status("雇用候補を読み込めませんでした。", false) return if bool(selected.get(&"hired", false)): _set_status("%sはすでに雇用済みです。" % String(selected.get(&"display_name", candidate_id)), false) _refresh_staff_candidates() return var sequence := _hire_sequence _hire_sequence += 1 var idempotency_key := StringName( "ui.hire.%s.%08d" % [String(candidate_id), sequence] ) var result: Variant = _runtime.call( &"submit_command", &"hire_staff_candidate", {&"candidate_id": candidate_id}, &"player", candidate_id, 75, -1, idempotency_key ) if result == null or not result.has_method(&"is_success"): _set_status("雇用手続きへ応答がありません。", false) return if not bool(result.call(&"is_success")): _set_status(_hire_failure_message(result), false) _refresh_staff_candidates() return var display_name := String(selected.get(&"display_name", candidate_id)) if result.details is Dictionary and result.details.has(&"staff_id"): _set_status("%sを雇用しました(契約 G%d / 日給 G%d)。" % [ display_name, int(selected.get(&"signing_cost", 0)), int(selected.get(&"daily_wage", 0)), ], true) else: _pending_hire_commands[int(result.command_id)] = candidate_id _set_status("%sの雇用手続きを受理しました。" % display_name, true) _refresh_dashboard() func _hire_failure_message(result: Variant) -> String: var details: Variant = result.details if details is Dictionary: var reason := StringName(details.get(&"reason", &"")) match reason: &"ARREARS_REMAIN": return "雇用不可: 未払給与 G%dを清算してください。" % int(details.get(&"arrears_total", 0)) &"BANKRUPT": return "雇用不可: 資金破綻後は新しい契約を結べません。" &"EXTERNAL_ROLLBACK_FAILED": return "雇用不可: 運営データの復旧が必要です。" var message := String(result.message).to_lower() if int(result.code) == int(CommandResult.Code.DUPLICATE_COMMAND_ID): return "この候補はすでに雇用済みです。" if "insufficient" in message or "balance" in message: return "雇用不可: 契約金が不足しています。" return _command_failure_message(result) func _build_button_text(definition: Variant) -> String: var name := String(_definition_value(definition, &"display_name", _definition_value(definition, &"id", "施設"))) var cost_value: Variant = _definition_value(definition, &"construction_cost", {}) var gold := 0 var materials := 0 if cost_value is Dictionary: gold = int(cost_value.get(&"gold", cost_value.get("gold", 0))) materials = int(cost_value.get(&"materials", cost_value.get("materials", 0))) return "%s\nG %d / 資材 %d" % [name, gold, materials] func _on_build_button_toggled(pressed: bool, definition_id: StringName) -> void: if not pressed: if _selected_definition_id == definition_id: _clear_build_selection() return _select_build_definition(definition_id) func _select_build_definition(definition_id: StringName) -> void: var registry: Variant = _service(&"definition_registry") if registry == null or not registry.has_method(&"get_facility"): _set_status("建設パレットを利用できません。", false) return var definition: Variant = registry.call(&"get_facility", definition_id) if definition == null: _set_status("選択した施設定義が見つかりません。", false) return _set_excavation_mode(false, false) _selected_definition_id = definition_id _selected_definition = definition _build_rotation = 0 for raw_id: Variant in build_buttons: (build_buttons[raw_id] as Button).set_pressed_no_signal(StringName(raw_id) == definition_id) var target_layer := clampi(int(_definition_value(definition, &"layer", 0)), 0, 1) _set_layer(target_layer) world_preview.set_build_definition(definition, _build_rotation) _set_status("%sを選択中。配置するマスをクリックしてください。" % String(_definition_value(definition, &"display_name", definition_id)), true, true) func _clear_build_selection(announce := true) -> void: _selected_definition_id = &"" _selected_definition = null _build_rotation = 0 for button_value: Variant in build_buttons.values(): (button_value as Button).set_pressed_no_signal(false) if world_preview != null: world_preview.clear_build_definition() if announce: _set_status("建設選択を解除しました。", true, true) func _clear_active_tool() -> void: if _supply_panel_open or _business_panel_open: _set_management_panel_open(false) return if _staff_management_panel_open: _set_staff_management_panel_open(false) return if _staff_candidate_panel_open: _set_staff_candidate_panel_open(false) return if _excavation_mode: _set_excavation_mode(false) return _clear_build_selection() _set_build_palette_open(false) _set_management_panel_open(false) func _toggle_excavation_mode() -> void: _set_excavation_mode(not _excavation_mode) func _set_excavation_mode(enabled: bool, announce := true) -> void: _excavation_mode = enabled if excavation_button != null: excavation_button.set_pressed_no_signal(_excavation_mode) if world_preview != null: world_preview.set_excavation_mode(_excavation_mode) if not _excavation_mode: if announce: _set_status("掘削モードを解除しました。", true, true) return _clear_build_selection(false) _selected_entity_id = &"" _selected_entity_kind = &"" world_preview.selected_entity_id = &"" selected_heading_label.text = "掘削モード" selected_detail_label.text = "地下のマスにカーソルを合わせると、費用と所要時間を確認できます。" var gate_message := _excavation_gate_message() _set_status( gate_message if not gate_message.is_empty() else "地下の掘削候補を選んでください。FRONT / BACKは同時に開通します。", gate_message.is_empty(), true ) func _rotate_build_preview() -> void: if _selected_definition == null: _set_status("先に建設する施設を選択してください。", false, true) return var allowed_value: Variant = _definition_value(_selected_definition, &"allowed_rotations", [0]) var allowed: Array[int] = [] if allowed_value is Array: for rotation_value: Variant in allowed_value: allowed.append(int(rotation_value)) allowed.sort() if allowed.is_empty(): allowed = [0] var index := allowed.find(_build_rotation) _build_rotation = allowed[(index + 1) % allowed.size()] world_preview.set_build_rotation(_build_rotation) _set_status("回転 %d°。地図のマスをクリックして建設します。" % _build_rotation, true, true) func _on_map_cell_clicked(cell: Vector3i) -> void: if _excavation_mode: _start_excavation(cell) return if _selected_definition == null: _selected_entity_id = &"" _selected_entity_kind = &"" world_preview.selected_entity_id = &"" selected_elevator_controls.visible = false selected_entity_icon.texture = null selected_heading_label.text = "選択中のマス" selected_detail_label.text = "座標 (%d, %d)\nレイヤー %s\n\n施設のないマスです。" % [cell.x, cell.y, "FRONT / 表層" if cell.z == 0 else "BACK / 裏層"] return var preview := world_preview.get_build_preview_report(cell) if not bool(preview.get(&"can_build", false)): _set_status(_preview_failure_message(preview), false) return if _runtime == null or not _runtime.has_method(&"submit_command"): _set_status("Simulation Runtimeへ接続できません。", false) return var idempotency_key := StringName("ui.build.%08d" % _build_sequence) _build_sequence += 1 var result: Variant = _runtime.call( &"submit_command", &"build_facility", {&"definition_id": _selected_definition_id, &"origin": cell, &"rotation": _build_rotation}, &"player", &"", 50, -1, idempotency_key ) if result == null or not result.has_method(&"is_success"): _set_status("建設指示を送信できませんでした。", false) return if not bool(result.call(&"is_success")): _set_status(_command_failure_message(result), false) return var name := String(_definition_value(_selected_definition, &"display_name", _selected_definition_id)) if result.details is Dictionary and result.details.has(&"job_id"): _set_status("%sの建設を開始しました(%d, %d / %d°)。" % [name, cell.x, cell.y, _build_rotation], true) else: _pending_build_commands[int(result.command_id)] = _selected_definition_id _set_status("%sの建設指示を受理しました(%d, %d / %d°)。" % [name, cell.x, cell.y, _build_rotation], true) _refresh_dashboard() func _on_build_preview_changed(report: Dictionary) -> void: if _selected_definition == null or world_preview.get_hovered_cell() == null: 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) else: _set_status(_preview_failure_message(report), false, true) func _on_excavation_preview_changed(report: Dictionary) -> void: if not _excavation_mode or world_preview.get_hovered_cell() == null: return var gate_message := _excavation_gate_message() if not gate_message.is_empty(): _set_status("掘削不可: %s%s" % [gate_message, _excavation_cost_and_time(report)], false, true) return var cell: Vector3i = world_preview.get_hovered_cell() if bool(report.get(&"ok", false)): _set_status( "掘削可能 (%d, %d)・%s%s — クリックでFRONT / BACKを開通" % [ cell.x, cell.y, "FRONT / 表層" if cell.z == 0 else "BACK / 裏層", _excavation_cost_and_time(report), ], true, true ) else: _set_status(_excavation_failure_message(report), false, true) func _start_excavation(cell: Vector3i) -> void: var report := world_preview.get_excavation_preview_report(cell) var gate_message := _excavation_gate_message() if not gate_message.is_empty(): _set_status("掘削不可: %s%s" % [gate_message, _excavation_cost_and_time(report)], false) return if not bool(report.get(&"ok", false)): _set_status(_excavation_failure_message(report), false) return if _runtime == null or not _runtime.has_method(&"submit_command"): _set_status("Simulation Runtimeへ接続できません。", false) return var spatial_position := Vector2i(cell.x, cell.y) if _pending_excavation_commands.values().has(spatial_position): _set_status("このマスの掘削指示はすでに受理されています。", false) return var idempotency_key := _excavation_idempotency_key(cell) var result: Variant = _runtime.call( &"submit_command", &"start_excavation", {&"cell": Vector3i(cell.x, cell.y, world_preview.layer_mode)}, &"player", &"", 50, -1, idempotency_key ) if result == null or not result.has_method(&"is_success"): _set_status("掘削指示を送信できませんでした。", false) return if not bool(result.call(&"is_success")): _set_status(_excavation_command_failure_message(result), false) return if result.details is Dictionary and result.details.has(&"job_id"): _select_excavation_job(StringName(result.details.get(&"job_id", &""))) _set_status("掘削を開始しました(%d, %d / FRONT・BACK)。" % [cell.x, cell.y], true) else: _pending_excavation_commands[int(result.command_id)] = spatial_position _set_status("掘削指示を受理しました(%d, %d / FRONT・BACK)。" % [cell.x, cell.y], true) _refresh_dashboard() func _excavation_idempotency_key(cell: Vector3i) -> StringName: # One click gets one stable retry identity. A later player retry receives a # new sequence so a previously cached validation failure cannot block it. var sequence := _excavation_sequence _excavation_sequence += 1 return StringName( "ui.excavation.%d.%d.%08d" % [cell.x, cell.y, sequence] ) func _on_facility_selected(instance_id: StringName) -> void: _set_ledger_panel_open(false) _set_supply_panel_open(false) _set_management_panel_open(true) _set_excavation_mode(false, false) _selected_entity_id = instance_id _selected_entity_kind = &"facility" _refresh_selected_entity() func _on_construction_selected(job_id: StringName) -> void: _set_ledger_panel_open(false) _set_supply_panel_open(false) _set_management_panel_open(true) _set_excavation_mode(false, false) _selected_entity_id = job_id _selected_entity_kind = &"construction" _refresh_selected_entity() func _refresh_dashboard() -> void: var guests := _call_array(_service(&"guest_service"), &"get_guests") var queued_guests := 0 var served_guests := 0 for guest: Variant in guests: if guest == null: continue if int(guest.state) == 4: queued_guests += 1 elif int(guest.state) == 5: served_guests += 1 var staff_members := _call_array(_service(&"staff_service"), &"get_staff_members") var available_staff := 0 var working_staff := 0 for staff: Variant in staff_members: if staff == null: continue if int(staff.state) == 2: available_staff += 1 elif int(staff.state) in [3, 4]: working_staff += 1 var event_service: Variant = _service(&"event_service") var events := _call_array(event_service, &"get_instances") var active_events := 0 for event_instance: Variant in events: if event_instance != null and int(event_instance.state) in [0, 1, 2, 7]: active_events += 1 var active_jobs := 0 for job: Variant in _call_array(_service(&"construction_service"), &"get_jobs"): if job != null and int(job.state) not in [2, 3]: active_jobs += 1 var heat_text := "—" var heat_value := 0 var authority: Variant = _service(&"authority_service") if authority != null and authority.has_method(&"get_authority_state"): var heat_state: Variant = authority.call(&"get_authority_state") if heat_state != null: heat_value = int(heat_state.heat) heat_text = "%d / 100" % heat_value var foreground_text := "なし" if event_service != null and event_service.has_method(&"get_foreground_instance"): var foreground: Variant = event_service.call(&"get_foreground_instance") if foreground != null: foreground_text = String(foreground.resolved_event_id) _refresh_foreground_event(event_service) var operations_text := _operations_dashboard_text() dashboard_summary_label.text = ( "客 %d人(待機 %d / 利用 %d)\n" % [guests.size(), queued_guests, served_guests] + "従業員 %d人(待機 %d / 作業 %d)\n" % [staff_members.size(), available_staff, working_staff] + "イベント %d件 前景: %s\n" % [active_events, foreground_text] + "警戒度 %s  建設中 %d件" % [heat_text, active_jobs] + operations_text ) if guest_count_label != null: guest_count_label.text = "来客 %d人" % guests.size() if lodging_count_label != null: var lodging_guests := 0 for guest: Variant in guests: if guest != null and int(guest.planned_exit_at) >= GameClock.absolute_minute + 360: lodging_guests += 1 lodging_count_label.text = "宿泊 %d人" % lodging_guests if alert_count_label != null: alert_count_label.text = "警戒 %d" % heat_value alert_count_label.add_theme_color_override( "font_color", Color("ffb498") if heat_value >= 60 else Color("ebca84") if heat_value >= 30 else Color("c7d3a8") ) _refresh_staff_candidates() _refresh_selected_entity() _update_pause_and_speed() if opening_guide != null: opening_guide.refresh() if operating_ledger_panel != null and _ledger_panel_open: operating_ledger_panel.refresh() func _refresh_foreground_event(event_service: Variant) -> void: if foreground_event_panel == null \ or foreground_event_label == null \ or foreground_event_choices == null: return for child: Node in foreground_event_choices.get_children(): foreground_event_choices.remove_child(child) child.queue_free() var foreground: Variant = null if event_service != null and event_service.has_method(&"get_foreground_instance"): foreground = event_service.call(&"get_foreground_instance") if foreground == null or int(foreground.state) != 2: foreground_event_panel.visible = false return foreground_event_panel.visible = true _set_ledger_panel_open(false) _set_supply_panel_open(false) _set_management_panel_open(true) var actors: Array[String] = [] for actor_value: Variant in foreground.actor_ids: actors.append(String(actor_value)) foreground_event_label.text = ( "%s\n関係者: %s" % [ String(foreground.resolved_event_id), "・".join(actors) if not actors.is_empty() else "なし", ] ) var definition: Variant = null if event_service.has_method(&"get_definition"): definition = event_service.call(&"get_definition", foreground.resolved_event_id) var choices_value: Variant = _definition_value(definition, &"choices", []) if not choices_value is Array: return for choice: Variant in choices_value: if choice == null: continue var choice_id := StringName(_definition_value(choice, &"choice_id", &"")) if choice_id.is_empty(): continue var choice_button := Button.new() choice_button.text = _event_choice_button_text(choice) choice_button.custom_minimum_size.y = 34 _apply_button_style(choice_button) choice_button.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART choice_button.pressed.connect( _submit_foreground_event_choice.bind( foreground.event_instance_id, foreground.choice_instance_id, choice_id ) ) foreground_event_choices.add_child(choice_button) func _event_choice_button_text(choice: Variant) -> String: var choice_id := StringName(_definition_value(choice, &"choice_id", &"")) var text_key := StringName(_definition_value(choice, &"text_key", choice_id)) var title := String(EVENT_CHOICE_NAMES.get(text_key, String(choice_id))) var cost_value: Variant = _definition_value(choice, &"cost", {}) if not cost_value is Dictionary or cost_value.is_empty(): return title var cost_parts: Array[String] = [] var cost_keys: Array = cost_value.keys() cost_keys.sort_custom(func(left: Variant, right: Variant) -> bool: return String(left) < String(right) ) for raw_key: Variant in cost_keys: cost_parts.append("%s %d" % [String(raw_key), int(cost_value[raw_key])]) return "%s(%s)" % [title, " / ".join(cost_parts)] func _submit_foreground_event_choice( event_instance_id: StringName, choice_instance_id: StringName, choice_id: StringName ) -> void: if _runtime == null or not _runtime.has_method(&"submit_command"): _set_status("イベント選択を送信できません。", false) return 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", { &"event_instance_id": event_instance_id, &"choice_instance_id": choice_instance_id, &"choice_id": choice_id, }, &"player", event_instance_id, 100, -1, idempotency_key ) var succeeded: bool = result != null \ and result.has_method(&"is_success") \ and bool(result.call(&"is_success")) _set_status( "イベントの選択を確定しました。" if succeeded \ else _command_failure_message(result) if result != null else "イベント選択に失敗しました。", succeeded ) _refresh_dashboard() _update_pause_and_speed() func _operations_dashboard_text() -> String: if _runtime == null or not _runtime.has_method(&"get_operations_report"): return "" var report_value: Variant = _runtime.call(&"get_operations_report") if not report_value is Dictionary: return "" var report: Dictionary = report_value if not bool(report.get(&"enabled", false)): return "\n運営日次処理 未開始" var coordinator_value: Variant = report.get(&"coordinator", {}) var coordinator: Dictionary = coordinator_value if coordinator_value is Dictionary else {} var terminal := StringName(coordinator.get(&"terminal_state", &"RUNNING")) var state_text := "営業中" if terminal == &"BANKRUPT": state_text = "破綻(終了)" elif bool(report.get(&"runtime_failure_latched", false)): state_text = "処理失敗・再試行待ち" elif bool(report.get(&"tick_blocked", false)): state_text = "確認待ち" var day_profit_text := "未確定" var summaries_value: Variant = coordinator.get(&"day_summaries", []) if summaries_value is Array and not summaries_value.is_empty(): var latest_value: Variant = summaries_value.back() if latest_value is Dictionary: day_profit_text = "G %+d" % int(latest_value.get(&"net_profit", 0)) return ( "\n行動点 %d / 6  未払給与 G %d\n" % [ int(report.get(&"personal_action_points", 0)), int(coordinator.get(&"arrears_total", 0)), ] + "運営状態 %s  前日損益 %s" % [state_text, day_profit_text] ) func _refresh_selected_entity() -> void: selected_elevator_controls.visible = false if _selected_entity_id.is_empty(): _move_selected_detail_first(false) selected_entity_icon.texture = null selected_heading_label.text = "掘削モード" if _excavation_mode else "選択中の施設" selected_detail_label.text = ( "地下のマスにカーソルを合わせると、費用と所要時間を確認できます。" if _excavation_mode else "地図上の施設をクリックすると、営業状態を確認できます。" ) return if _selected_entity_kind == &"construction": _move_selected_detail_first(false) _refresh_selected_construction() return if _selected_entity_kind == &"excavation": _move_selected_detail_first(false) _refresh_selected_excavation() return var shaft := _selected_elevator_report() if not shaft.is_empty(): _refresh_selected_elevator(shaft) return _move_selected_detail_first(false) var facility_service: Variant = _service(&"facility_service") if facility_service == null or not facility_service.has_method(&"get_instance"): selected_detail_label.text = "施設サービスを利用できません。" return var facility: Variant = facility_service.call(&"get_instance", _selected_entity_id) if facility == null: var construction_facility := _construction_facility(_selected_entity_id) if construction_facility.is_empty(): _selected_entity_id = &"" _selected_entity_kind = &"" world_preview.selected_entity_id = &"" selected_heading_label.text = "選択中の施設" selected_detail_label.text = "選択していた施設は現在の世界に存在しません。" selected_entity_icon.texture = null return var construction_definition_id := StringName(construction_facility.get(&"definition_id", &"")) selected_heading_label.text = String(construction_definition_id) selected_entity_icon.texture = visual_theme.facility_icon(construction_definition_id) if visual_theme != null else null selected_detail_label.text = "施設データを運営サービスへ同期しています。" return var definition: Variant = _facility_definition(facility.definition_id) selected_entity_icon.texture = visual_theme.facility_icon(facility.definition_id) if visual_theme != null else null var display_name := String(_definition_value(definition, &"display_name", facility.definition_id)) var state_index := clampi(int(facility.state), 0, FACILITY_STATE_NAMES.size() - 1) var reasons := _facility_reasons(facility.state_reasons) var queue_count: int = facility.queue_entries.size() var service_count: int = facility.active_services.size() var staff_count: int = facility.active_staff_ids.size() var occupied_slots: int = service_count selected_heading_label.text = display_name selected_detail_label.text = ( "状態 %s%s\n" % [FACILITY_STATE_NAMES[state_index], "(%s)" % reasons if not reasons.is_empty() else ""] + "待機列 %d / %d  サービス %d / %d\n" % [queue_count, int(facility.queue_capacity), occupied_slots, int(facility.capacity)] + "配置人員 %d / 必要 %d  耐久 %d%%\n" % [staff_count, int(facility.staff_min), int(facility.durability) / 100] + "基本料金 G %d  所要 %d分\n" % [int(facility.base_price), int(facility.base_service_minutes)] + "座標 (%d, %d) %s" % [facility.origin.x, facility.origin.y, "FRONT" if facility.origin.z == 0 else "BACK"] ) func _selected_elevator_report() -> Dictionary: if _selected_entity_kind != &"facility" or _selected_entity_id.is_empty(): return {} var transport: Variant = _service(&"elevators") if transport == null or not transport.has_method(&"get_report"): return {} for shaft: Dictionary in transport.call(&"get_report").get(&"shafts", []): if (shaft.get(&"module_ids", []) as Array).has(_selected_entity_id): return shaft return {} func _refresh_selected_elevator(shaft: Dictionary) -> void: _move_selected_detail_first(true) var construction_facility := _construction_facility(_selected_entity_id) var definition_id := StringName(construction_facility.get(&"definition_id", &"guest_elevator")) var definition: Variant = _facility_definition(definition_id) selected_heading_label.text = String(_definition_value(definition, &"display_name", "昇降機")) selected_entity_icon.texture = visual_theme.facility_icon(definition_id) if visual_theme != null else null var powered := bool(shaft.get(&"powered", true)) var operator_open := bool(shaft.get(&"operator_open", true)) var state_text := String(ELEVATOR_PHASE_NAMES.get(StringName(shaft.get(&"phase", &"IDLE")), "待機")) if not operator_open: state_text = "手動停止" elif not powered: state_text = "魔力供給なし" var floor_index := int(shaft.get(&"current_floor", 0)) var target_index := int(shaft.get(&"boarding_floor", floor_index) if StringName(shaft.get(&"phase", &"IDLE")) == &"PICKUP" else shaft.get(&"target_floor", floor_index)) var stops: Array = shaft.get(&"stops", []) var stop_text := "—" if not stops.is_empty(): stop_text = "%s 〜 %s" % [_elevator_floor_name(stops.front().y), _elevator_floor_name(stops.back().y)] selected_detail_label.text = ( "状態 %s\n" % state_text + "現在階 %s  目的階 %s\n" % [_elevator_floor_name(floor_index), _elevator_floor_name(target_index)] + "乗客 %d / %d人  待機 %d人\n" % [int(shaft.get(&"passenger_count", 0)), int(shaft.get(&"capacity", 0)), int(shaft.get(&"waiting_count", 0))] + "停止階 %s\n" % stop_text + "魔力 %s  %s" % ["供給中" if powered else "不足", "客・従業員用" if definition_id == &"guest_elevator" else "従業員・物資用"] ) selected_elevator_controls.visible = true elevator_operation_button.disabled = _pending_elevator_commands.values().has(shaft.get(&"shaft_id", &"")) elevator_operation_button.text = "指示を受理済み…" if elevator_operation_button.disabled else ("昇降機を停止" if operator_open else "運行を再開") elevator_operation_hint.text = "停止すると最寄りの階で降車し、待機中の呼び出しを解除します。" if operator_open else ("再開すると新しい呼び出しを受け付けます。" if powered else "魔力が供給されると運行を再開できます。") func _move_selected_detail_first(first: bool) -> void: var column := selected_heading_label.get_parent() var selected_row := selected_detail_label.get_parent() # The transport action fits without scrolling; other management views keep # their existing staff-first layout. var desired_index := 0 if first else column.get_child_count()-3 if selected_heading_label.get_index() == desired_index: return column.move_child(selected_heading_label, 0 if first else -1) column.move_child(selected_row, 1 if first else -1) column.move_child(selected_elevator_controls, 2 if first else -1) func _elevator_floor_name(floor_index: int) -> String: return "%dF" % (floor_index+1) if floor_index >= 0 else "B%d" % -floor_index func _on_elevator_operation_pressed() -> void: var shaft := _selected_elevator_report() if shaft.is_empty() or _runtime == null: return var shaft_id := StringName(shaft[&"shaft_id"]) if _pending_elevator_commands.values().has(shaft_id): return 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) if result == null or not result.is_success(): _set_status("昇降機の指示を反映できませんでした。", false) return if not result.details.has(&"shaft_id"): _pending_elevator_commands[int(result.command_id)] = shaft_id _set_status("昇降機の運行指示を受理しました。", true) else: var message := "昇降機を停止しました。" if open_value: message = "昇降機の運行を再開しました。" if bool(shaft.get(&"powered", true)) else "運行を許可しました。魔力の供給待ちです。" _set_status(message, true) _refresh_selected_entity() func _synchronize_elevator_pending() -> void: var queue: Variant = _service(&"command_queue") if queue == null: return if queue.pending_count() == 0: _pending_elevator_commands.clear() return _pending_elevator_commands.clear() for command: Dictionary in queue.create_snapshot().get("pending", []): if StringName(command.get("command_type", &"")) == &"set_elevator_operation": _pending_elevator_commands[int(command.get("command_id", 0))] = StringName((command.get("payload", {}) as Dictionary).get(&"shaft_id", &"")) func _refresh_selected_construction() -> void: var construction: Variant = _service(&"construction_service") if construction == null or not construction.has_method(&"get_job"): selected_detail_label.text = "建設サービスを利用できません。" return var job: Variant = construction.call(&"get_job", _selected_entity_id) if job == null: _selected_entity_id = &"" _selected_entity_kind = &"" world_preview.selected_entity_id = &"" selected_heading_label.text = "選択中の施設" selected_detail_label.text = "選択していた工事は現在の世界に存在しません。" selected_entity_icon.texture = null return var definition: Variant = _facility_definition(job.definition_id) selected_entity_icon.texture = visual_theme.facility_icon(job.definition_id) if visual_theme != null else null var display_name := String(_definition_value(definition, &"display_name", job.definition_id)) var state_names := ["計画済み", "建設中", "完成", "中止", "停止中"] var job_state := clampi(int(job.state), 0, state_names.size() - 1) selected_heading_label.text = "%s(工事)" % display_name selected_detail_label.text = ( "状態 %s\n" % state_names[job_state] + "進捗 %d%%  予定 %d分\n" % [int(job.progress) / 100, int(job.build_minutes)] + "座標 (%d, %d) %s\n" % [job.origin.x, job.origin.y, "FRONT" if int(job.layer) == 0 else "BACK"] + ("停止理由 %s" % String(job.state_reason) if not String(job.state_reason).is_empty() else "建設はシミュレーション時間とともに進みます。") ) func _select_excavation_job(job_id: StringName) -> void: if job_id.is_empty(): return _selected_entity_id = job_id _selected_entity_kind = &"excavation" world_preview.selected_entity_id = &"" world_preview.clear_hovered_cell() _refresh_selected_excavation() func _refresh_selected_excavation() -> void: selected_entity_icon.texture = visual_theme.control_icon(&"excavate") if visual_theme != null else null var excavation: Variant = _service(&"excavation_service") if excavation == null or not excavation.has_method(&"get_job"): selected_detail_label.text = "掘削サービスを利用できません。" return var job_value: Variant = excavation.call(&"get_job", _selected_entity_id) if not job_value is Dictionary or job_value.is_empty(): _selected_entity_id = &"" _selected_entity_kind = &"" selected_heading_label.text = "掘削モード" if _excavation_mode else "選択中の施設" selected_detail_label.text = "選択していた掘削作業は現在の世界に存在しません。" return var job: Dictionary = job_value var state := StringName(job.get(&"state", &"")) var state_text := "掘削中" if state == &"PAUSED": state_text = "一時停止" elif state == &"COMPLETED": state_text = "完了" var position: Vector2i = job.get(&"position", Vector2i.ZERO) var cost_value: Variant = job.get(&"cost", {}) var cost: Dictionary = cost_value if cost_value is Dictionary else {} var guidance := "両レイヤーが開通しました。" if state == &"ACTIVE": guidance = "掘削はシミュレーション時間とともに進みます。" elif state == &"PAUSED": guidance = "停止理由 %s" % String(job.get(&"state_reason", "")) selected_heading_label.text = "地下掘削(%d, %d)" % [position.x, position.y] selected_detail_label.text = ( "状態 %s  進捗 %d%%\n" % [state_text, clampi(int(job.get(&"progress", 0)) / 100, 0, 100)] + "所要 %d分  費用 G %d / 資材 %d\n" % [ int(job.get(&"build_minutes", 0)), int(cost.get(&"gold", cost.get("gold", 0))), int(cost.get(&"materials", cost.get("materials", 0))), ] + "対象 FRONT / 表層・BACK / 裏層\n" + guidance ) func _preview_failure_message(report: Dictionary) -> String: var codes_value: Variant = report.get(&"error_codes", []) if codes_value is Array and not codes_value.is_empty(): var code := String(codes_value[0]) return "配置不可: %s" % PREVIEW_REASON_NAMES.get(code, code) var reasons_value: Variant = report.get(&"reasons", []) if reasons_value is Array and not reasons_value.is_empty() and reasons_value[0] is Dictionary: return "配置不可: %s" % String(reasons_value[0].get(&"message", "条件を満たしていません")) return "このマスには配置できません。" func _excavation_failure_message(report: Dictionary) -> String: var errors_value: Variant = report.get(&"errors", []) var reason := "条件を満たしていません" if errors_value is Array and not errors_value.is_empty(): var code := StringName(errors_value[0]) reason = String(EXCAVATION_REASON_NAMES.get(code, String(code))) if code == &"INSUFFICIENT_RESOURCES": var missing_value: Variant = report.get(&"missing_resources", {}) if missing_value is Dictionary and not missing_value.is_empty(): var missing: Dictionary = missing_value reason += "(不足: G %d / 資材 %d)" % [ int(missing.get(&"gold", missing.get("gold", 0))), int(missing.get(&"materials", missing.get("materials", 0))), ] return "掘削不可: %s%s" % [reason, _excavation_cost_and_time(report)] func _excavation_cost_and_time(report: Dictionary) -> String: var cost_value: Variant = report.get(&"cost", {}) var cost: Dictionary = cost_value if cost_value is Dictionary else {} var minutes := int(report.get(&"build_minutes", 0)) if cost.is_empty() and minutes <= 0: return "" return "(費用 G %d / 資材 %d・所要 %d分)" % [ int(cost.get(&"gold", cost.get("gold", 0))), int(cost.get(&"materials", cost.get("materials", 0))), minutes, ] func _excavation_gate_message() -> String: var progression: Variant = _service(&"progression_service") if progression == null: return "世界樹ランクを確認できません" var current_rank := int(_definition_value(progression, &"current_rank", 1)) if current_rank < 2: return "世界樹ランク2で地下掘削が解禁されます(現在ランク%d)" % current_rank if _runtime == null or not _runtime.has_method(&"get_operations_report"): return "運営状態を確認できません" var report_value: Variant = _runtime.call(&"get_operations_report") if not report_value is Dictionary: return "運営状態を確認できません" var report: Dictionary = report_value if not bool(report.get(&"enabled", false)): return "本番運営が始まるまで掘削できません" var coordinator_value: Variant = report.get(&"coordinator", {}) var coordinator: Dictionary = coordinator_value if coordinator_value is Dictionary else {} if StringName(coordinator.get(&"terminal_state", &"RUNNING")) == &"BANKRUPT": return "資金破綻により新しい掘削を開始できません" if bool(coordinator.get(&"external_quarantined", false)): return "運営データの復旧が必要なため掘削を開始できません" var arrears := int(coordinator.get(&"arrears_total", 0)) if arrears > 0: return "未払給与 G %dを清算するまで掘削できません" % arrears return "" func _excavation_command_failure_message(result: Variant) -> String: var details: Variant = result.details if details is Dictionary: var report_value: Variant = details.get(&"report", null) if report_value is Dictionary: return _excavation_failure_message(report_value) var reason := StringName(details.get(&"reason", &"")) match reason: &"ARREARS_REMAIN": return "掘削不可: 未払給与 G %dを清算してください" % int(details.get(&"arrears_total", 0)) &"BANKRUPT": return "掘削不可: 資金破綻により新しい工事は開始できません" &"EXTERNAL_ROLLBACK_FAILED": return "掘削不可: 運営データの復旧が必要です" if details.has(&"required_rank"): return "掘削不可: 世界樹ランク%dが必要です(現在ランク%d)" % [ int(details.get(&"required_rank", 2)), int(details.get(&"current_rank", 1)), ] if int(result.code) == int(CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY): return "この掘削指示はすでに受理されています。" return _command_failure_message(result) func _command_failure_message(result: Variant) -> String: var message := String(result.message) var details: Variant = result.details if details is Dictionary: var report_value: Variant = details.get(&"placement_report", null) if report_value is Dictionary: return _preview_failure_message(report_value) return "操作に失敗しました: %s" % (message if not message.is_empty() else "不明なエラー") func _save_manual() -> void: 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()) 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": "手動保存"}) if result is Dictionary and bool(result.get(&"ok", false)): _set_status("手動セーブを保存しました。", true) else: _set_status("保存に失敗しました: %s" % _result_message(result), false) func _load_manual() -> void: if _runtime == null or ( not _runtime.has_method(&"load_with_recovery") and not _runtime.has_method(&"load_from_path") ): _set_status("読込サービスへ接続できません。", false) return var result: Variant = ( _runtime.call(&"load_with_recovery", manual_save_path) if _runtime.has_method(&"load_with_recovery") else _runtime.call(&"load_from_path", manual_save_path) ) if result is Dictionary and bool(result.get(&"ok", false)): _pending_build_commands.clear() _pending_excavation_commands.clear() _pending_hire_commands.clear() _pending_elevator_commands.clear() _synchronize_ui_command_state() if staff_management_panel != null: staff_management_panel.refresh() if supply_management_panel != null: supply_management_panel.refresh() if business_management_panel != null: business_management_panel.refresh() _selected_entity_id = &"" _selected_entity_kind = &"" world_preview.selected_entity_id = &"" world_preview.queue_redraw() _refresh_dashboard() _update_pause_and_speed() _set_status("手動セーブを読み込みました。", true) else: _set_status("読み込みに失敗しました: %s" % _result_message(result), false) func _result_message(result: Variant) -> String: if result is Dictionary: var message := String(result.get(&"message", result.get(&"code", "保存データがありません"))) return message if not message.is_empty() else "保存データがありません" return "サービスから応答がありません" func _synchronize_ui_command_state() -> void: if _runtime == null: return var queue: Variant = _runtime.get(&"command_queue") if queue == null: return if queue.has_method(&"get_accepted_command_metadata"): var metadata_value: Variant = queue.call(&"get_accepted_command_metadata") if metadata_value is Array: for raw_metadata: Variant in metadata_value: if not raw_metadata is Dictionary: continue _advance_ui_sequence_from_key(String(raw_metadata.get("idempotency_key", ""))) if not queue.has_method(&"create_snapshot"): return var snapshot_value: Variant = queue.call(&"create_snapshot") if not snapshot_value is Dictionary: return var pending_value: Variant = snapshot_value.get("pending", []) if not pending_value is Array: return for raw_pending: Variant in pending_value: if not raw_pending is Dictionary: continue var command_id := int(raw_pending.get("command_id", 0)) var command_type := StringName(raw_pending.get("command_type", &"")) var payload_value: Variant = raw_pending.get("payload", {}) var payload: Dictionary = payload_value if payload_value is Dictionary else {} if command_id <= 0: continue match command_type: &"set_elevator_operation": _pending_elevator_commands[command_id] = StringName(payload.get(&"shaft_id", &"")) &"build_facility": var definition_id := StringName(payload.get(&"definition_id", payload.get("definition_id", &""))) if not definition_id.is_empty(): _pending_build_commands[command_id] = definition_id &"start_excavation": var cell_value: Variant = payload.get(&"cell", payload.get("cell", null)) if cell_value is Vector3i: _pending_excavation_commands[command_id] = Vector2i(cell_value.x, cell_value.y) &"hire_staff_candidate": var candidate_id := StringName(payload.get(&"candidate_id", payload.get("candidate_id", &""))) if not candidate_id.is_empty(): _pending_hire_commands[command_id] = candidate_id func _advance_ui_sequence_from_key(key: String) -> void: if key.is_empty(): return var parts := key.split(".") if parts.is_empty(): return var suffix := String(parts[parts.size() - 1]) if not suffix.is_valid_int(): return var next_sequence := int(suffix) + 1 if key.begins_with("ui.build."): _build_sequence = maxi(_build_sequence, next_sequence) elif key.begins_with("ui.excavation."): _excavation_sequence = maxi(_excavation_sequence, next_sequence) elif key.begins_with("ui.hire."): _hire_sequence = maxi(_hire_sequence, next_sequence) elif key.begins_with("ui.elevator."): _elevator_sequence = maxi(_elevator_sequence, next_sequence) func _on_snapshot_published(_snapshot: Dictionary, _digest: String) -> void: _synchronize_elevator_pending() _refresh_dashboard() _update_pause_and_speed() func _on_command_completed(result: Variant) -> void: if result == null: return if _pending_elevator_commands.has(int(result.command_id)): _pending_elevator_commands.erase(int(result.command_id)) _set_status("昇降機の運行設定を反映しました。" if result.is_success() else "昇降機の運行設定を反映できませんでした。", result.is_success()) _refresh_selected_entity() return if _pending_hire_commands.has(int(result.command_id)): var candidate_id := StringName(_pending_hire_commands[int(result.command_id)]) _pending_hire_commands.erase(int(result.command_id)) var display_name := String(candidate_id) for summary: Dictionary in _staff_candidate_summaries(): if StringName(summary.get(&"candidate_id", &"")) == candidate_id: display_name = String(summary.get(&"display_name", candidate_id)) break if bool(result.call(&"is_success")): _set_status("%sを雇用しました。" % display_name, true) else: _set_status(_hire_failure_message(result), false) _refresh_dashboard() return if _pending_excavation_commands.has(int(result.command_id)): var position: Vector2i = _pending_excavation_commands[int(result.command_id)] _pending_excavation_commands.erase(int(result.command_id)) if bool(result.call(&"is_success")): _select_excavation_job(StringName(result.details.get(&"job_id", &""))) _set_status("掘削を開始しました(%d, %d / FRONT・BACK)。" % [position.x, position.y], true) else: _set_status(_excavation_command_failure_message(result), false) return if not _pending_build_commands.has(int(result.command_id)): return var definition_id := StringName(_pending_build_commands[int(result.command_id)]) _pending_build_commands.erase(int(result.command_id)) if bool(result.call(&"is_success")): var definition: Variant = _facility_definition(definition_id) _set_status("%sの建設を開始しました。" % String(_definition_value(definition, &"display_name", definition_id)), true) else: _set_status(_command_failure_message(result), false) func _set_layer(value: int) -> void: var normalized := clampi(value, 0, 1) if world_preview != null: world_preview.layer_mode = normalized GameState.active_layer = normalized if front_button != null: front_button.set_pressed_no_signal(normalized == 0) if back_button != null: back_button.set_pressed_no_signal(normalized == 1) EventBus.layer_changed.emit(normalized) func _toggle_pause() -> void: var event_service: Variant = _service(&"event_service") if event_service != null and event_service.has_method(&"get_foreground_instance"): var foreground: Variant = event_service.call(&"get_foreground_instance") if foreground != null and int(foreground.state) == 2: GameClock.set_paused(true) _set_status("先に要対応イベントの選択肢を決めてください。", false) _update_pause_and_speed() return if _runtime != null \ and _runtime.has_method(&"get_operations_report") \ and _runtime.has_method(&"acknowledge_operations_pause"): var report_value: Variant = _runtime.call(&"get_operations_report") if report_value is Dictionary: var report: Dictionary = report_value var coordinator_value: Variant = report.get(&"coordinator", {}) var coordinator: Dictionary = ( coordinator_value if coordinator_value is Dictionary else {} ) if StringName(coordinator.get(&"terminal_state", &"RUNNING")) == &"BANKRUPT": _set_status("資金破綻により運営は終了しています。", false) return if bool(report.get(&"enabled", false)) \ and int(report.get(&"deferred_runtime_minute", -1)) >= 0: var retry_value: Variant = _runtime.call(&"acknowledge_operations_pause") var retry_ok := retry_value is Dictionary and bool(retry_value.get(&"ok", false)) _set_status( "運営処理を再開しました。" if retry_ok \ else "運営処理を再開できません: %s" % _result_message(retry_value), retry_ok ) _refresh_dashboard() _update_pause_and_speed() return GameClock.toggle_pause() _update_pause_and_speed() func _set_speed(value: int) -> void: GameClock.set_speed(value) _update_pause_and_speed() func _update_pause_and_speed() -> void: var operations_deferred := false var operations_terminal := false var event_waiting := false if _runtime != null and _runtime.has_method(&"get_operations_report"): var report_value: Variant = _runtime.call(&"get_operations_report") if report_value is Dictionary: operations_deferred = bool(report_value.get(&"enabled", false)) \ and int(report_value.get(&"deferred_runtime_minute", -1)) >= 0 var coordinator_value: Variant = report_value.get(&"coordinator", {}) if coordinator_value is Dictionary: operations_terminal = StringName( coordinator_value.get(&"terminal_state", &"RUNNING") ) == &"BANKRUPT" var event_service: Variant = _service(&"event_service") if event_service != null and event_service.has_method(&"get_foreground_instance"): var foreground: Variant = event_service.call(&"get_foreground_instance") event_waiting = foreground != null and int(foreground.state) == 2 if phase_label != null: if operations_terminal: phase_label.text = "運営終了" elif operations_deferred: phase_label.text = "要確認" elif event_waiting: phase_label.text = "選択待ち" else: phase_label.text = "停止中" if GameClock.paused else _phase_name(GameClock.get_phase()) phase_label.add_theme_color_override("font_color", Color("e88972") if GameClock.paused else Color("f0c574")) if pause_button != null: pause_button.text = ( "破綻" if operations_terminal else ( "運営再試行" if operations_deferred else ( "イベント待ち" if event_waiting else ("再開" if GameClock.paused else "停止") ) ) ) pause_button.disabled = operations_terminal or event_waiting for raw_speed: Variant in speed_buttons: var button := speed_buttons[raw_speed] as Button button.set_pressed_no_signal(not GameClock.paused and int(raw_speed) == GameClock.speed) func _on_time_changed(current_day: int, hour: int, minute: int) -> void: time_label.text = "%d日目 %02d:%02d" % [current_day, hour, minute] _update_pause_and_speed() func _on_phase_changed(phase: GameClock.Phase) -> void: if not GameClock.paused: phase_label.text = _phase_name(phase) func _phase_name(phase: GameClock.Phase) -> String: match phase: GameClock.Phase.MORNING: return "朝" GameClock.Phase.DAY: return "昼" GameClock.Phase.EVENING: return "夕方" _: return "夜" func _on_resource_changed(resource_name: StringName, value: int) -> void: if not resource_labels.has(resource_name): return var titles := { &"gold": "所持金", &"mana": "マナ", &"materials": "資材", &"faith": "信仰", } (resource_labels[resource_name] as Label).text = "%s %d" % [titles[resource_name], value] func _button(text_value: String, callback: Callable, minimum_width := 82) -> Button: var button := Button.new() button.text = text_value button.custom_minimum_size = Vector2(minimum_width, 28) button.add_theme_font_size_override("font_size", 12) button.pressed.connect(callback) _apply_button_style(button) return button func _hud_value_label(text_value: String, minimum_width: float) -> Label: var label := Label.new() label.text = text_value label.custom_minimum_size.x = minimum_width label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER label.add_theme_font_size_override("font_size", 12) label.add_theme_color_override("font_color", _visual_color(&"text_light", Color("fff2d0"))) return label func _icon_rect(icon_texture: Texture2D, side: float) -> TextureRect: var icon := TextureRect.new() icon.texture = icon_texture icon.custom_minimum_size = Vector2(side, side) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.mouse_filter = Control.MOUSE_FILTER_IGNORE return icon func _apply_button_icon(button: Button, icon_texture: Texture2D, maximum_width: int) -> void: if icon_texture == null: return button.icon = icon_texture button.add_theme_constant_override("icon_max_width", maximum_width) button.expand_icon = true func _apply_button_style(button: Button) -> void: 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")) button.add_theme_color_override("font_disabled_color", Color("a98e68")) button.add_theme_stylebox_override("normal", _themed_panel_style(&"button_normal", _visual_color(&"button_normal", Color("75451f")), _visual_color(&"border_gold", Color("b77b37")), 7, 7)) button.add_theme_stylebox_override("hover", _themed_panel_style(&"button_hover", _visual_color(&"button_hover", Color("8d5628")), _visual_color(&"border_gold", Color("e4b45c")), 7, 7)) button.add_theme_stylebox_override("pressed", _themed_panel_style(&"button_pressed", _visual_color(&"button_pressed", Color("496528")), _visual_color(&"border_gold", Color("d8bd61")), 7, 7)) button.add_theme_stylebox_override("focus", _themed_panel_style(&"button_pressed", _visual_color(&"button_pressed", Color("496528")), _visual_color(&"border_gold", Color("f2d97d")), 7, 7)) button.add_theme_stylebox_override("disabled", _themed_panel_style(&"button_disabled", _visual_color(&"button_disabled", Color("574334")), Color("7b6955"), 7, 7)) func _section_heading(text_value: String) -> Label: var heading := Label.new() heading.text = text_value heading.add_theme_font_size_override("font_size", 14) heading.add_theme_color_override("font_color", _visual_color(&"text_dark", Color("55371e"))) return heading func _panel_style(color: Color, border_color: Color, radius := 8, margin := 12) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = color style.border_color = border_color style.set_border_width_all(1) style.set_corner_radius_all(mini(radius, 1)) style.set_content_margin_all(margin) style.shadow_size = 0 return style func _themed_panel_style(_skin_id: StringName, color: Color, border_color: Color, radius := 0, margin := 5) -> StyleBox: # Small flat frames keep attention on the animated cross-section. return _panel_style(color, border_color, mini(radius, 1), mini(margin, 6)) func _set_status(message: String, success: bool, quiet := false) -> void: if build_status_label == null: return build_status_label.text = message build_status_label.tooltip_text = message if quiet: build_status_label.add_theme_color_override("font_color", Color("c7d3a8") if success else Color("ffb498")) else: build_status_label.add_theme_color_override("font_color", Color("d8e8ad") if success else Color("ffb498")) func _visual_color(property_name: StringName, fallback: Color) -> Color: var interface_colors := { &"window_background": Color("17261f"), &"dark_panel": Color("24392e"), &"dark_panel_alt": Color("20342a"), &"light_panel": Color("e9e6cd"), &"border_gold": Color("778267"), &"text_light": Color("eae7cc"), &"text_dark": Color("344838"), &"button_normal": Color("354b39"), &"button_hover": Color("4c6246"), &"button_pressed": Color("79623c"), &"button_disabled": Color("354135"), } if interface_colors.has(property_name): return interface_colors[property_name] if visual_theme == null: return fallback var value: Variant = visual_theme.get(property_name) return value if value is Color else fallback func _service(property_name: StringName) -> Variant: if _runtime == null or not _runtime is Object: return null for property: Dictionary in _runtime.get_property_list(): if StringName(property.get(&"name", &"")) == property_name: return _runtime.get(property_name) return null func _call_array(service: Variant, method_name: StringName) -> Array: if service == null or not service.has_method(method_name): return [] var result: Variant = service.call(method_name) return result if result is Array else [] func _facility_definition(definition_id: StringName) -> Variant: var registry: Variant = _service(&"definition_registry") if registry == null or not registry.has_method(&"get_facility"): return null return registry.call(&"get_facility", definition_id) func _construction_facility(instance_id: StringName) -> Dictionary: var construction: Variant = _service(&"construction_service") if construction == null or not construction.has_method(&"get_facility"): return {} var result: Variant = construction.call(&"get_facility", instance_id) return result if result is Dictionary else {} func _facility_reasons(reasons_value: Variant) -> String: if not reasons_value is Array: return "" var names: Array[String] = [] for reason_value: Variant in reasons_value: var reason := StringName(reason_value) names.append(String(FACILITY_REASON_NAMES.get(reason, reason))) return "・".join(names) func _definition_value(source: Variant, property_name: StringName, fallback: Variant) -> Variant: if source is Dictionary: return source.get(property_name, source.get(String(property_name), fallback)) if source == null or not source is Object: return fallback for property: Dictionary in source.get_property_list(): if StringName(property.get(&"name", &"")) == property_name: return source.get(property_name) return fallback