Files
TheTree_mock/scripts/ui/supply_management_panel.gd
T

571 lines
26 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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": "荷元の在庫が消費期限を過ぎました。",
&"SOURCE_CLOSED": "荷元の施設が休業しています。",
&"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