Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
579 lines
27 KiB
GDScript
579 lines
27 KiB
GDScript
class_name SupplyManagementPanel
|
||
extends VBoxContainer
|
||
|
||
const LayerSkin := preload("res://scripts/ui/layer_interface_skin.gd")
|
||
|
||
## 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 := {
|
||
&"SUSPENDED": "破壊・摘発による休止",
|
||
&"RELATED_FACILITY_INACTIVE": "関連施設が未稼働",
|
||
&"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(&"state_updated"):
|
||
_runtime.connect(&"state_updated", _on_snapshot_published)
|
||
elif _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"))
|
||
LayerSkin.register_toggle_text(production_enabled, LayerSkin.active_layer(self))
|
||
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
|
||
LayerSkin.register_status_text(result_label, success, LayerSkin.active_layer(self))
|
||
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"))
|
||
LayerSkin.register_text(label, LayerSkin.active_layer(self))
|
||
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
|