Add supply auto-purchase and preserve save recovery state

This commit is contained in:
Takeshi
2026-10-02 07:35:03 +09:00
parent 285528b7a3
commit 39f6068461
44 changed files with 2625 additions and 61 deletions
+99 -5
View File
@@ -11,6 +11,7 @@ signal action_requested(action: StringName)
const KEY_PREFIX := "ui.supply."
const PURCHASE: StringName = &"purchase_supplies"
const PRODUCTION: StringName = &"set_kitchen_production"
const AUTO_PURCHASE: StringName = &"set_supply_auto_purchase"
const ITEM_NAMES := {&"ingredient/raw_food": "食材", &"ingredient/basic_meal": "完成料理"}
const STATE_NAMES := {
&"SUSPENDED": "破壊・摘発による休止",
@@ -56,6 +57,8 @@ const REASONS := {
&"NO_POWER": "施設へ魔力が届いていません。", &"CLOSED": "施設が閉鎖されています。",
&"NOT_KITCHEN": "厨房を選んでください。", &"INVALID_TARGET": "目標在庫の上限を確認してください。",
&"BANKRUPT": "運営終了後は変更できません。", &"UNAVAILABLE": "現在は操作できません。",
&"OPERATIONS_BLOCKED": "運営の停止理由を確認し、再開してください。",
&"REPLAY_MISMATCH": "仕入れ履歴と現在の設定を確認してください。",
}
var summary_label: Label
@@ -65,6 +68,10 @@ var quantity_spin: SpinBox
var purchase_quote_label: Label
var purchase_button: Button
var warehouse_stock_label: Label
var auto_purchase_enabled: CheckButton
var auto_purchase_target: SpinBox
var auto_purchase_button: Button
var auto_purchase_status: Label
var kitchen_option: OptionButton
var production_enabled: CheckButton
var target_spin: SpinBox
@@ -84,6 +91,7 @@ var _report: Dictionary = {}
var _quote: Dictionary = {}
var _pages: Dictionary = {}
var _kitchen_drafts: Dictionary = {}
var _auto_purchase_drafts: Dictionary = {}
var _pending_commands: Dictionary = {}
var _queue_instance_id := 0
var _sequence := 1
@@ -152,6 +160,23 @@ func _build_interface() -> void:
purchase_page.add_child(purchase_quote_label)
purchase_button = _button("購入", "PurchaseSuppliesButton", submit_purchase)
purchase_page.add_child(purchase_button)
auto_purchase_enabled = CheckButton.new()
auto_purchase_enabled.name = "SupplyAutoPurchaseEnabled"
auto_purchase_enabled.text = "自動仕入れ(不足分を有料購入)"
for color_name: StringName in [&"font_color", &"font_pressed_color", &"font_hover_color", &"font_hover_pressed_color", &"font_focus_color"]:
auto_purchase_enabled.add_theme_color_override(color_name, Color("55371e"))
auto_purchase_enabled.add_theme_color_override("font_disabled_color", Color("766650"))
LayerSkin.register_toggle_text(auto_purchase_enabled, LayerSkin.active_layer(self))
auto_purchase_enabled.toggled.connect(func(_value: bool) -> void: _store_auto_purchase_draft())
purchase_page.add_child(auto_purchase_enabled)
purchase_page.add_child(_label("倉庫の目標在庫 · 選択した品目"))
auto_purchase_target = _spin("SupplyAutoPurchaseTarget", 0, 0)
auto_purchase_target.value_changed.connect(func(_value: float) -> void: _store_auto_purchase_draft())
purchase_page.add_child(auto_purchase_target)
auto_purchase_button = _button("自動仕入れ設定を保存", "SaveSupplyAutoPurchaseButton", submit_auto_purchase)
purchase_page.add_child(auto_purchase_button)
auto_purchase_status = _label("", "SupplyAutoPurchaseStatus")
purchase_page.add_child(auto_purchase_status)
var kitchen_page: VBoxContainer = _pages[&"kitchen"]
kitchen_page.add_child(_label("設定する厨房"))
kitchen_option = _option("SupplyKitchenOption")
@@ -211,6 +236,7 @@ func refresh() -> void:
if queue != null and queue.get_instance_id() != _queue_instance_id:
_queue_instance_id = queue.get_instance_id()
_kitchen_drafts.clear()
_auto_purchase_drafts.clear()
for picker: OptionButton in [warehouse_option, item_option, kitchen_option]:
picker.get_popup().hide()
_synchronize_commands(queue)
@@ -246,11 +272,13 @@ func _refresh_purchase() -> void:
# 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)
var setting_pending := _has_pending(AUTO_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()
warehouse_option.disabled = pending or setting_pending or _rows(&"warehouses").is_empty()
item_option.disabled = pending or setting_pending or _rows(&"catalog").is_empty()
purchase_button.disabled = pending or not bool(_quote.get(&"ok", false))
purchase_button.text = "購入を受理済み…" if pending else "購入"
_refresh_auto_purchase(warehouse)
if catalog.is_empty() or warehouse.is_empty():
purchase_quote_label.text = "受入倉庫と品目を選んでください。"
return
@@ -259,6 +287,66 @@ func _refresh_purchase() -> void:
purchase_quote_label.text += "\n" + _reason_text(_quote.get(&"reason", &"UNAVAILABLE"))
func _refresh_auto_purchase(warehouse: Dictionary) -> void:
var row: Dictionary = {}
for value: Variant in warehouse.get(&"auto_purchase_plans", []):
if value is Dictionary and StringName(value.get(&"item_id", &"")) == selected_item_id:
row = value
break
var draft: Dictionary = _auto_purchase_drafts.get(_auto_purchase_key(), row)
var pending := _has_pending(AUTO_PURCHASE)
var target := int(draft.get(&"target_stock", 0))
var maximum := int(row.get(&"maximum_target", 0))
var enabled := bool(draft.get(&"enabled", false))
auto_purchase_enabled.disabled = pending or row.is_empty()
auto_purchase_enabled.set_pressed_no_signal(enabled)
auto_purchase_target.min_value = 0
auto_purchase_target.max_value = maxi(maximum, target)
auto_purchase_target.set_value_no_signal(target)
auto_purchase_target.editable = not pending and not row.is_empty()
auto_purchase_button.disabled = pending or row.is_empty() or target > maximum or (enabled and target <= 0)
auto_purchase_button.text = "設定を受理済み…" if pending else "自動仕入れ設定を保存"
if row.is_empty():
auto_purchase_status.text = "完成した倉庫と購入できる品目を選んでください。"
return
var state := StringName(row.get(&"state", &"DISABLED"))
var state_text := "停止中"
match state:
&"READY": state_text = "不足分の入荷待ち"
&"TARGET_REACHED": state_text = "目標在庫に到達"
&"ORDERED_THIS_MINUTE": state_text = "入庫済み · 次の不足判定を待機"
&"BLOCKED": state_text = _reason_text(row.get(&"reason", &"UNAVAILABLE"))
auto_purchase_status.text = "現在の設定:%s · 目標 %d / 上限 %d\n%s\n倉庫の現物 %d · 入荷予定 %d" % [
"ON" if bool(row.get(&"enabled", false)) else "OFF", row.get(&"target_stock", 0), maximum,
state_text, row.get(&"physical_quantity", 0), row.get(&"incoming_quantity", 0)]
var quote: Dictionary = row.get(&"quote", {})
if int(row.get(&"deficit", 0)) > 0 and not quote.is_empty():
auto_purchase_status.text += "\n不足 %d · 今の購入見積 %dG" % [row[&"deficit"], quote.get(&"total_cost", 0)]
if row.get(&"last_result", &"") == &"PURCHASED":
auto_purchase_status.text += "\n前回入庫 %d個 · 代金 %dG(単価 %dG)" % [row[&"last_quantity"], row[&"last_total_cost"], row[&"last_unit_price"]]
if _auto_purchase_drafts.has(_auto_purchase_key()):
auto_purchase_status.text += "\n未保存の変更です。設定を保存すると反映します。"
auto_purchase_status.text += "\n在庫が減ると目標まで不足分を購入します。料金は手動購入と同じです。"
func _auto_purchase_key() -> String:
return "%d:%s%d:%s" % [String(selected_warehouse_id).length(), selected_warehouse_id,
String(selected_item_id).length(), selected_item_id]
func _store_auto_purchase_draft() -> void:
if _refreshing or selected_warehouse_id.is_empty() or selected_item_id.is_empty(): return
_auto_purchase_drafts[_auto_purchase_key()] = {&"enabled": auto_purchase_enabled.button_pressed,
&"target_stock": int(auto_purchase_target.value)}
refresh()
func submit_auto_purchase() -> void:
if selected_warehouse_id.is_empty() or selected_item_id.is_empty() or _has_pending(AUTO_PURCHASE): return
_submit(AUTO_PURCHASE, {&"warehouse_id": selected_warehouse_id, &"item_id": selected_item_id,
&"enabled": auto_purchase_enabled.button_pressed, &"target_stock": int(auto_purchase_target.value)})
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)
@@ -365,7 +453,13 @@ func _show_result(result: Variant, context: Dictionary) -> void:
_set_result(_reason_text(details.get(&"reason", &"UNAVAILABLE")), false)
return
var payload: Dictionary = context[&"payload"]
if context[&"type"] == PRODUCTION:
if context[&"type"] == AUTO_PURCHASE:
var warehouse_id := StringName(payload.get(&"warehouse_id", &""))
var item_id := StringName(payload.get(&"item_id", &""))
_auto_purchase_drafts.erase("%d:%s%d:%s" % [String(warehouse_id).length(), warehouse_id, String(item_id).length(), item_id])
_set_result("%s:自動仕入れ%s、目標在庫 %dに変更しました。" % [
_facility_by_id(warehouse_id), "ON" if bool(details.get(&"enabled", false)) else "OFF", details.get(&"target_stock", 0)], true)
elif 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)
@@ -382,13 +476,13 @@ func _synchronize_commands(queue: Variant) -> void:
_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]:
if type in [PURCHASE, PRODUCTION, AUTO_PURCHASE]:
_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):
if context[&"type"] == type and (facility_id.is_empty() or StringName(context[&"payload"].get(&"facility_id", context[&"payload"].get(&"warehouse_id", &""))) == facility_id):
return true
return false