Connect paid food purchasing, kitchen production, and porter deliveries

This commit is contained in:
Takeshi
2026-09-09 09:46:33 +09:00
parent 29a660aed0
commit 627b200f00
42 changed files with 3787 additions and 84 deletions
+569
View File
@@ -0,0 +1,569 @@
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
@@ -0,0 +1 @@
uid://cu6exjai0evbv