class_name SupplyManagementPanel extends VBoxContainer ## Read models and commands only. Prices, stock, recipes and delivery progress ## belong to Runtime; changing a control never mutates inventory directly. signal status_requested(message: String, success: bool) signal action_requested(action: StringName) const KEY_PREFIX := "ui.supply." const PURCHASE: StringName = &"purchase_supplies" const PRODUCTION: StringName = &"set_kitchen_production" const ITEM_NAMES := {&"ingredient/raw_food": "食材", &"ingredient/basic_meal": "完成料理"} const STATE_NAMES := { &"OPEN": "補給待ち", &"RESERVED": "運搬員待ち", &"ASSIGNED": "荷元へ移動", &"PICKING_UP": "積込中", &"DELIVERING": "配送中", &"COMPLETED": "納品済み", &"BLOCKED": "配送停止", &"CANCELLED": "取消済み", &"WORKING": "調理中", &"PRODUCING": "調理中", &"IDLE": "待機中", &"DISABLED": "自動生産停止", &"TARGET_REACHED": "目標在庫に到達", &"READY": "稼働可能", } const REASONS := { &"INSUFFICIENT_FUNDS": "資金が不足しています。", &"INSUFFICIENT_CASH": "資金が不足しています。", &"INVALID_QUANTITY": "購入数量を確認してください。", &"INVALID_ITEM": "この品目は購入できません。", &"INVALID_PAYLOAD": "入力した設定を確認してください。", &"ITEM_NOT_PURCHASABLE": "この品目は購入できません。", &"ITEM_NOT_FOUND": "品目が見つかりません。", &"WAREHOUSE_NOT_FOUND": "完成した倉庫を選んでください。", &"FACILITY_NOT_FOUND": "施設が見つかりません。", &"NOT_WAREHOUSE": "受入先には倉庫を選んでください。", &"WAREHOUSE_FULL": "倉庫の空きが不足しています。", &"CAPACITY_EXCEEDED": "保管容量を超えています。", &"STORAGE_FULL": "倉庫の空きが不足しています。", &"FACILITY_CLOSED": "受入先の施設が閉鎖されています。", &"NO_STOCK": "荷元の在庫が不足しています。", &"INSUFFICIENT_STOCK": "荷元の在庫が不足しています。", &"NO_INPUTS": "調理用の食材が不足しています。", &"NO_STAFF": "必要な職員を配属してください。", &"NO_INPUT": "必要な在庫が不足しています。", &"DISABLED": "自動生産は停止中です。", &"INVENTORY_ERROR": "在庫への入庫に失敗しました。", &"PAYMENT_FAILED": "代金を支払えませんでした。", &"NO_CARRIER": "勤務中の運搬員がいません。", &"NO_ELIGIBLE_CARRIER": "運搬できる職員がいません。", &"NO_AVAILABLE_PORTER_OR_ROUTE": "勤務中の運搬員と配送経路を確認してください。", &"FACILITY_UNAVAILABLE": "荷元・供給先の施設が利用できません。", &"DESTINATION_FULL": "供給先の保管容量が不足しています。", &"DESTINATION_FULL_OR_CLOSED": "供給先の空き容量と営業状態を確認してください。", &"NO_DELIVERY_ROUTE": "供給先までの配送経路がありません。", &"ROUTE_BLOCKED": "配送経路が通れません。通路とドアを確認してください。", &"CARRIER_CAPACITY_EXCEEDED": "運搬員が持てる量を超えています。", &"TASK_ASSIGNMENT_FAILED": "運搬作業を割り当てられません。", &"CARRIER_TASK_MISSING": "運搬作業との接続を確認してください。", &"CARRIER_UNAVAILABLE": "担当の運搬員が現在作業できません。", &"SOURCE_STOCK_EXPIRED": "荷元の在庫が消費期限を過ぎました。", &"WAITING_FOR_SOURCE_STOCK": "荷元への入荷を待っています。", &"SOURCE_WEIGHT_CHANGED": "荷物の重量が変わったため再手配が必要です。", &"PICKUP_FAILED": "積込できませんでした。再手配を待っています。", &"DELIVERY_RETRY": "納品を再試行しています。", &"RETURNING_EXPIRED_CARGO": "期限切れの荷物を荷元へ返送しています。", &"EXPIRED_CARGO_RETURN_BLOCKED": "期限切れの荷物を返送できません。経路を確認してください。", &"NO_ROUTE": "通路と従業員ドアの接続を確認してください。", &"NO_ACCESS": "施設への経路がありません。", &"NO_POWER": "施設へ魔力が届いていません。", &"CLOSED": "施設が閉鎖されています。", &"NOT_KITCHEN": "厨房を選んでください。", &"INVALID_TARGET": "目標在庫の上限を確認してください。", &"BANKRUPT": "運営終了後は変更できません。", &"UNAVAILABLE": "現在は操作できません。", } var summary_label: Label var warehouse_option: OptionButton var item_option: OptionButton var quantity_spin: SpinBox var purchase_quote_label: Label var purchase_button: Button var warehouse_stock_label: Label var kitchen_option: OptionButton var production_enabled: CheckButton var target_spin: SpinBox var production_button: Button var kitchen_status_label: Label var delivery_label: Label var result_label: Label var tab_buttons: Dictionary = {} var selected_warehouse_id: StringName = &"" var selected_item_id: StringName = &"" var selected_kitchen_id: StringName = &"" var selected_tab: StringName = &"purchase" var _runtime: Variant var _style_button: Callable var _report: Dictionary = {} var _quote: Dictionary = {} var _pages: Dictionary = {} var _kitchen_drafts: Dictionary = {} var _pending_commands: Dictionary = {} var _queue_instance_id := 0 var _sequence := 1 var _refreshing := false var _built := false func configure(runtime: Variant, style_button := Callable()) -> void: _runtime = runtime _style_button = style_button func _ready() -> void: name = "SupplyManagementPanel" size_flags_horizontal = Control.SIZE_EXPAND_FILL add_theme_constant_override("separation", 5) _build_interface() _built = true if _runtime != null: if _runtime.has_signal(&"snapshot_published"): _runtime.connect(&"snapshot_published", _on_snapshot_published) if _runtime.has_signal(&"command_completed"): _runtime.connect(&"command_completed", _on_command_completed) refresh() func _build_interface() -> void: summary_label = _label("商品 · 仕入れと補給", "SupplySummary") add_child(summary_label) var tabs := HBoxContainer.new() tabs.add_theme_constant_override("separation", 3) add_child(tabs) for entry: Array in [[&"purchase", "仕入れ"], [&"kitchen", "厨房"], [&"delivery", "配送"]]: var button := _button(entry[1], "SupplyTab_%s" % entry[0], select_tab.bind(entry[0])) button.toggle_mode = true button.size_flags_horizontal = Control.SIZE_EXPAND_FILL tab_buttons[entry[0]] = button tabs.add_child(button) var page := VBoxContainer.new() page.name = "SupplyPage_%s" % entry[0] page.add_theme_constant_override("separation", 5) _pages[entry[0]] = page add_child(page) var purchase_page: VBoxContainer = _pages[&"purchase"] purchase_page.add_child(_label("受入倉庫")) warehouse_option = _option("SupplyWarehouseOption") warehouse_option.item_selected.connect(func(index: int) -> void: selected_warehouse_id = StringName(warehouse_option.get_item_metadata(index)) refresh()) purchase_page.add_child(warehouse_option) warehouse_stock_label = _label("", "SupplyWarehouseStock") purchase_page.add_child(warehouse_stock_label) purchase_page.add_child(_label("購入する品目")) item_option = _option("SupplyItemOption") item_option.item_selected.connect(func(index: int) -> void: selected_item_id = StringName(item_option.get_item_metadata(index)) refresh()) purchase_page.add_child(item_option) quantity_spin = _spin("SupplyQuantity", 1, 1) quantity_spin.value_changed.connect(_on_purchase_quantity_changed) purchase_page.add_child(_label("数量")) purchase_page.add_child(quantity_spin) purchase_quote_label = _label("", "SupplyPurchaseQuote") purchase_page.add_child(purchase_quote_label) purchase_button = _button("購入", "PurchaseSuppliesButton", submit_purchase) purchase_page.add_child(purchase_button) var kitchen_page: VBoxContainer = _pages[&"kitchen"] kitchen_page.add_child(_label("設定する厨房")) kitchen_option = _option("SupplyKitchenOption") kitchen_option.item_selected.connect(func(index: int) -> void: selected_kitchen_id = StringName(kitchen_option.get_item_metadata(index)) refresh()) kitchen_page.add_child(kitchen_option) production_enabled = CheckButton.new() production_enabled.name = "SupplyProductionEnabled" production_enabled.text = "自動生産" for color_name: StringName in [&"font_color", &"font_pressed_color", &"font_hover_color", &"font_hover_pressed_color", &"font_focus_color"]: production_enabled.add_theme_color_override(color_name, Color("55371e")) production_enabled.add_theme_color_override("font_disabled_color", Color("766650")) production_enabled.toggled.connect(_on_production_enabled_changed) kitchen_page.add_child(production_enabled) kitchen_page.add_child(_label("目標在庫 · 完成料理")) target_spin = _spin("SupplyTargetStock", 0, 0) target_spin.value_changed.connect(_on_target_changed) kitchen_page.add_child(target_spin) production_button = _button("生産設定を保存", "SaveKitchenProductionButton", submit_production) kitchen_page.add_child(production_button) kitchen_status_label = _label("", "SupplyKitchenStatus") kitchen_page.add_child(kitchen_status_label) var delivery_page: VBoxContainer = _pages[&"delivery"] delivery_label = _label("", "SupplyDeliveryStatus") delivery_page.add_child(delivery_label) var actions := HBoxContainer.new() for entry: Array in [[&"staff", "従業員を開く"], [&"facility", "施設をつくる"]]: var button := _button(entry[1], "SupplyAction_%s" % entry[0], func() -> void: action_requested.emit(entry[0])) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL actions.add_child(button) add_child(actions) result_label = _label("", "SupplyResult") add_child(result_label) select_tab(&"purchase") func select_tab(tab: StringName) -> void: if not _pages.has(tab): return selected_tab = tab for key: StringName in _pages: (_pages[key] as Control).visible = key == tab (tab_buttons[key] as Button).set_pressed_no_signal(key == tab) refresh() func get_report() -> Dictionary: return _report.duplicate(true) func refresh() -> void: if not _built: return var queue: Variant = _runtime.get(&"command_queue") if _runtime != null else null if queue != null and queue.get_instance_id() != _queue_instance_id: _queue_instance_id = queue.get_instance_id() _kitchen_drafts.clear() for picker: OptionButton in [warehouse_option, item_option, kitchen_option]: picker.get_popup().hide() _synchronize_commands(queue) _report = {} if _runtime != null and _runtime.has_method(&"get_supply_management_report"): var value: Variant = _runtime.call(&"get_supply_management_report") if value is Dictionary: _report = value _refreshing = true summary_label.text = "商品 · 手持ち %dG" % int(_report.get(&"cash", 0)) if not _report.is_empty() else "商品 · 補給情報はまだありません" match selected_tab: &"purchase": _refresh_purchase() &"kitchen": _refresh_kitchen() &"delivery": _refresh_deliveries() _refreshing = false func _refresh_purchase() -> void: selected_warehouse_id = _fill_option(warehouse_option, _rows(&"warehouses"), &"facility_id", selected_warehouse_id, "完成した倉庫がありません") selected_item_id = _fill_option(item_option, _rows(&"catalog"), &"item_id", selected_item_id, "購入できる品目がありません") var warehouse := _row(&"warehouses", &"facility_id", selected_warehouse_id) var catalog := _row(&"catalog", &"item_id", selected_item_id) warehouse_stock_label.text = "倉庫を完成させると、有料で食材を仕入れられます。" if warehouse.is_empty() else "保管 %d / %d\n%s" % [warehouse.get(&"used", 0), warehouse.get(&"capacity", 0), _stock_text(warehouse.get(&"stock", []))] _quote = {} if _runtime != null and _runtime.has_method(&"preview_supply_purchase") and not warehouse.is_empty() and not catalog.is_empty(): var value: Variant = _runtime.call(&"preview_supply_purchase", selected_warehouse_id, selected_item_id, int(quantity_spin.value)) if value is Dictionary: _quote = value # The upper bound comes from Runtime. Retain a rejected draft so the player # can see why it cannot be bought instead of silently reducing its quantity. quantity_spin.max_value = maxi(int(quantity_spin.value), maxi(1, int(_quote.get(&"max_quantity", 1)))) var pending := _has_pending(PURCHASE) quantity_spin.editable = not pending and not catalog.is_empty() and not warehouse.is_empty() warehouse_option.disabled = pending or _rows(&"warehouses").is_empty() item_option.disabled = pending or _rows(&"catalog").is_empty() purchase_button.disabled = pending or not bool(_quote.get(&"ok", false)) purchase_button.text = "購入を受理済み…" if pending else "購入" if catalog.is_empty() or warehouse.is_empty(): purchase_quote_label.text = "受入倉庫と品目を選んでください。" return purchase_quote_label.text = "単価 %dG / %s\n合計 %dG · 購入後 %dG" % [catalog.get(&"unit_price", 0), catalog.get(&"unit_name", "個"), _quote.get(&"total_cost", 0), _quote.get(&"cash_after", _report.get(&"cash", 0))] if not bool(_quote.get(&"ok", false)): purchase_quote_label.text += "\n" + _reason_text(_quote.get(&"reason", &"UNAVAILABLE")) func _refresh_kitchen() -> void: selected_kitchen_id = _fill_option(kitchen_option, _rows(&"kitchens"), &"facility_id", selected_kitchen_id, "完成した厨房がありません") var row := _row(&"kitchens", &"facility_id", selected_kitchen_id) var draft: Dictionary = _kitchen_drafts.get(selected_kitchen_id, row) var pending := _has_pending(PRODUCTION, selected_kitchen_id) kitchen_option.disabled = _rows(&"kitchens").is_empty() production_enabled.disabled = pending or row.is_empty() target_spin.editable = not pending and not row.is_empty() var target_max := maxi(0, int(row.get(&"target_max", 0))) target_spin.min_value = int(row.get(&"target_min", 4)) if not row.is_empty() else 0 target_spin.step = int(row.get(&"target_step", 4)) if not row.is_empty() else 1 target_spin.max_value = maxi(target_max, int(draft.get(&"target_stock", 0))) target_spin.set_value_no_signal(int(draft.get(&"target_stock", 0))) production_enabled.set_pressed_no_signal(bool(draft.get(&"enabled", false))) production_button.disabled = pending or row.is_empty() or int(target_spin.value) > target_max production_button.text = "設定を受理済み…" if pending else "生産設定を保存" if row.is_empty(): kitchen_status_label.text = "厨房をつくり、調理係を配属してください。" return kitchen_status_label.text = "設定上限 %d · %s\n材料 %s → 生産 %s\n%s" % [target_max, _state_text(row.get(&"state", &"IDLE")), _items_text(row.get(&"inputs", {})), _items_text(row.get(&"outputs", {})), _stock_text(row.get(&"stock", []))] var reasons := _reasons_text(row.get(&"reason_codes", [])) if not reasons.is_empty(): kitchen_status_label.text += "\n" + reasons var batch: Variant = row.get(&"active_batch", {}) if batch is Dictionary and not batch.is_empty(): if batch.has(&"remaining_minutes"): kitchen_status_label.text += "\n調理完了まで %d分" % int(batch[&"remaining_minutes"]) elif row.has(&"duration_minutes"): kitchen_status_label.text += "\n調理 %d / %d分" % [row.get(&"progress_minutes", 0), row[&"duration_minutes"]] kitchen_status_label.text += "\n食材は倉庫から補給し、完成料理は不足する食堂へ配送します。" func _refresh_deliveries() -> void: var lines: PackedStringArray = ["食堂の在庫"] for row: Dictionary in _rows(&"destinations"): var missing := _items_text(row.get(&"missing_items", {})) lines.append("%s\n不足:%s" % [_facility_name(row), missing]) var reasons := _reasons_text(row.get(&"reason_codes", [])) if not reasons.is_empty(): lines.append(reasons) if _rows(&"destinations").is_empty(): lines.append("食堂はまだありません。") lines.append("\n配送状況") for row: Dictionary in _rows(&"deliveries"): var state := StringName(String(row.get(&"state", &"OPEN"))) lines.append("%s → %s\n%s · %s" % [_facility_by_id(StringName(row.get(&"source_facility_id", &""))), _facility_by_id(StringName(row.get(&"destination_facility_id", &""))), _items_text(row.get(&"item_requirements", {})), _state_text(state)]) if state not in [&"COMPLETED", &"CANCELLED"]: lines.append("担当:%s" % (row.get(&"carrier_name", "未割当") if not String(row.get(&"carrier_name", "")).is_empty() else "未割当")) var reasons := _reasons_text(row.get(&"reason_codes", [])) if not reasons.is_empty(): lines.append(reasons) if _rows(&"deliveries").is_empty(): lines.append("配送要求はまだありません。") delivery_label.text = "\n".join(lines) func submit_purchase() -> void: if _has_pending(PURCHASE): return if _runtime == null or not _runtime.has_method(&"preview_supply_purchase"): _set_result("仕入れサービスへ接続できません。", false) return var quote: Variant = _runtime.call(&"preview_supply_purchase", selected_warehouse_id, selected_item_id, int(quantity_spin.value)) if not quote is Dictionary or not bool(quote.get(&"ok", false)): _set_result(_reason_text(quote.get(&"reason", &"UNAVAILABLE")) if quote is Dictionary else "購入条件を確認できません。", false) refresh() return _submit(PURCHASE, {&"warehouse_id": selected_warehouse_id, &"item_id": selected_item_id, &"quantity": int(quantity_spin.value)}) func submit_production() -> void: if selected_kitchen_id.is_empty() or _has_pending(PRODUCTION, selected_kitchen_id): return _submit(PRODUCTION, {&"facility_id": selected_kitchen_id, &"enabled": production_enabled.button_pressed, &"target_stock": int(target_spin.value)}) func _submit(type: StringName, payload: Dictionary) -> void: if _runtime == null or not _runtime.has_method(&"submit_command"): _set_result("補給サービスへ接続できません。", false) return var context := {&"type": type, &"payload": payload.duplicate(true)} var key := StringName("%s%08d" % [KEY_PREFIX, _sequence]) _sequence += 1 var result: Variant = _runtime.call(&"submit_command", type, payload, &"player", StringName(payload.get(&"facility_id", payload.get(&"warehouse_id", &""))), 75, -1, key) if result == null or not result.has_method(&"is_success"): _set_result("操作に応答がありません。", false) return var queued := false var queue: Variant = _runtime.get(&"command_queue") if queue != null: for command: Dictionary in queue.create_snapshot().get("pending", []): queued = queued or int(command.get("command_id", 0)) == int(result.command_id) if result.is_success() and queued: _pending_commands[int(result.command_id)] = context _set_result("操作を受理しました。次の時間更新で反映します。", true) else: _show_result(result, context) refresh() func _show_result(result: Variant, context: Dictionary) -> void: var details: Dictionary = result.details if result.details is Dictionary else {} if not result.is_success(): _set_result(_reason_text(details.get(&"reason", &"UNAVAILABLE")), false) return var payload: Dictionary = context[&"payload"] if context[&"type"] == PRODUCTION: var id := StringName(payload.get(&"facility_id", &"")) _kitchen_drafts.erase(id) _set_result("%s:自動生産%s、目標在庫 %dに変更しました。" % [_facility_by_id(id), "ON" if bool(details.get(&"enabled", payload.get(&"enabled", false))) else "OFF", details.get(&"target_stock", payload.get(&"target_stock", 0))], true) else: var cost_text := "(%dG)" % int(details[&"total_cost"]) if details.has(&"total_cost") else "" _set_result("%sを%d仕入れました%s。" % [_item_name(StringName(payload.get(&"item_id", &""))), details.get(&"quantity", payload.get(&"quantity", 0)), cost_text], true) func _synchronize_commands(queue: Variant) -> void: _pending_commands.clear() for metadata: Dictionary in queue.get_accepted_command_metadata(): var key := String(metadata.get("idempotency_key", "")) if key.begins_with(KEY_PREFIX) and key.trim_prefix(KEY_PREFIX).is_valid_int(): _sequence = maxi(_sequence, int(key.trim_prefix(KEY_PREFIX)) + 1) for command: Dictionary in queue.create_snapshot().get("pending", []): var type := StringName(command.get("command_type", &"")) if type in [PURCHASE, PRODUCTION]: _pending_commands[int(command.get("command_id", 0))] = {&"type": type, &"payload": command.get("payload", {}).duplicate(true)} func _has_pending(type: StringName, facility_id: StringName = &"") -> bool: for context: Dictionary in _pending_commands.values(): if context[&"type"] == type and (facility_id.is_empty() or StringName(context[&"payload"].get(&"facility_id", &"")) == facility_id): return true return false func _on_command_completed(result: Variant) -> void: if not _pending_commands.has(int(result.command_id)): return var context: Dictionary = _pending_commands[int(result.command_id)] _pending_commands.erase(int(result.command_id)) _show_result(result, context) refresh() func _on_snapshot_published(_snapshot: Dictionary, _digest: String) -> void: if is_visible_in_tree(): refresh() func _on_purchase_quantity_changed(_value: float) -> void: if not _refreshing: refresh() func _on_production_enabled_changed(_enabled: bool) -> void: _store_kitchen_draft() func _on_target_changed(_value: float) -> void: _store_kitchen_draft() func _store_kitchen_draft() -> void: if _refreshing or selected_kitchen_id.is_empty(): return _kitchen_drafts[selected_kitchen_id] = {&"enabled": production_enabled.button_pressed, &"target_stock": int(target_spin.value)} refresh() func _fill_option(picker: OptionButton, rows: Array[Dictionary], id_key: StringName, selected: StringName, empty_text: String) -> StringName: if picker.get_popup().visible: return selected picker.clear() if rows.is_empty(): picker.add_item(empty_text) picker.set_item_metadata(0, &"") return &"" var index := 0 for row: Dictionary in rows: picker.add_item(_facility_name(row) if id_key == &"facility_id" else String(row.get(&"display_name", row.get(id_key, "")))) var added := picker.item_count - 1 picker.set_item_metadata(added, StringName(row.get(id_key, &""))) if StringName(row.get(id_key, &"")) == selected: index = added picker.select(index) return StringName(picker.get_item_metadata(index)) func _rows(key: StringName) -> Array[Dictionary]: var rows: Array[Dictionary] = [] var value: Variant = _report.get(key, []) if value is Array: for row: Variant in value: if row is Dictionary: rows.append(row) return rows func _row(key: StringName, id_key: StringName, id: StringName) -> Dictionary: for row: Dictionary in _rows(key): if StringName(row.get(id_key, &"")) == id: return row return {} func _facility_name(row: Dictionary) -> String: var label := String(row.get(&"display_name", row.get(&"facility_id", "施設"))) var origin: Variant = row.get(&"origin", null) if origin is Vector3i: label += " (%d,%d・%s)" % [origin.x, origin.y, "裏" if int(row.get(&"layer", 0)) == 1 else "表"] elif origin is Dictionary: label += " (%d,%d・%s)" % [origin.get(&"x", 0), origin.get(&"y", 0), "裏" if int(row.get(&"layer", 0)) == 1 else "表"] return label func _facility_by_id(id: StringName) -> String: for group: StringName in [&"warehouses", &"kitchens", &"destinations"]: var row := _row(group, &"facility_id", id) if not row.is_empty(): return _facility_name(row) return "施設未指定" if id.is_empty() else String(id) func _item_name(id: StringName) -> String: return String(_row(&"catalog", &"item_id", id).get(&"display_name", ITEM_NAMES.get(id, String(id)))) func _items_text(value: Variant) -> String: var parts: PackedStringArray = [] if value is Dictionary: for id: Variant in value: parts.append("%s %s" % [_item_name(StringName(id)), value[id]]) elif value is Array: for row: Variant in value: if row is Dictionary: parts.append("%s %d" % [_item_name(StringName(row.get(&"item_id", &""))), row.get(&"quantity", row.get(&"missing", 0))]) return " / ".join(parts) if not parts.is_empty() else "なし" func _stock_text(value: Variant) -> String: var lines: PackedStringArray = [] if value is Array: for row: Variant in value: if row is Dictionary: lines.append("%s %d(利用可 %d / 予約 %d / 配送中 %d)" % [_item_name(StringName(row.get(&"item_id", &""))), row.get(&"total", 0), row.get(&"available", 0), row.get(&"reserved", 0), row.get(&"in_transit", 0)]) return "\n".join(lines) if not lines.is_empty() else "在庫なし" func _state_text(value: Variant) -> String: var state := StringName(String(value)) return String(STATE_NAMES.get(state, REASONS.get(state, String(value)))) func _reason_text(value: Variant) -> String: var reason := StringName(String(value)) return String(REASONS.get(reason, "状態を確認してください(%s)。" % reason)) func _reasons_text(value: Variant) -> String: var lines: PackedStringArray = [] if value is Array: for reason: Variant in value: lines.append(_reason_text(reason)) return "\n".join(lines) func _set_result(message: String, success: bool) -> void: result_label.text = message result_label.add_theme_color_override("font_color", Color("397346") if success else Color("a7402e")) status_requested.emit(message, success) func _label(value: String, node_name := "") -> Label: var label := Label.new() if not node_name.is_empty(): label.name = node_name label.text = value label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART label.size_flags_horizontal = Control.SIZE_EXPAND_FILL label.add_theme_font_size_override("font_size", 13) label.add_theme_color_override("font_color", Color("55371e")) return label func _button(value: String, node_name: String, callback: Callable) -> Button: var button := Button.new() button.name = node_name button.text = value button.custom_minimum_size.y = 30 button.add_theme_font_size_override("font_size", 13) button.pressed.connect(callback) if _style_button.is_valid(): _style_button.call(button) return button func _option(node_name: String) -> OptionButton: var picker := OptionButton.new() picker.name = node_name picker.fit_to_longest_item = false picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL picker.custom_minimum_size.y = 30 if _style_button.is_valid(): _style_button.call(picker) return picker func _spin(node_name: String, minimum: int, value: int) -> SpinBox: var spin := SpinBox.new() spin.name = node_name spin.min_value = minimum spin.max_value = minimum spin.step = 1 spin.value = value spin.size_flags_horizontal = Control.SIZE_EXPAND_FILL spin.custom_minimum_size.y = 30 return spin