Save The Tree implementation and acceptance work in progress

Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
This commit is contained in:
Takeshi
2026-10-11 15:58:00 +09:00
parent 39f6068461
commit b84bade045
235 changed files with 495831 additions and 312 deletions
+160 -5
View File
@@ -9,6 +9,8 @@ signal action_requested(action: StringName)
const KEY_PREFIX := "ui.business."
const COMMAND: StringName = &"set_facility_policy"
const KITCHEN_KEY_PREFIX := "ui.business.kitchen."
const KITCHEN_COMMAND: StringName = &"set_facility_conditions"
const GUEST_POLICY_EXPLANATION := "料金変更は新規受付から。受付済みの料金は変わりません。\n休業すると待機中のお客様は別の施設を探します。開始済みのサービスは完了まで続きます。休業中も給与と施設維持費は発生します。"
const SUPPLY_POLICY_EXPLANATION := "休業中は調理と新しい補給を止めます。在庫と運搬中の荷物は保持し、積込済みの荷物は納品先の営業状況に従って配送します。消費期限は経過し、給与と施設維持費も発生します。"
const SERVICE_ATTRIBUTE_NAMES := {
@@ -26,6 +28,9 @@ const REASONS := {
}
var facility_option: OptionButton
var price_option: OptionButton
var kitchen_option: OptionButton
var kitchen_apply_button: Button
var kitchen_status_label: Label
var business_open_button: CheckButton
var quote_label: Label
var effect_label: Label
@@ -37,10 +42,13 @@ var result_label: Label
var selected_facility_id: StringName = &""
var _runtime: Variant
var _audio_submitter := Callable()
var _style_button: Callable
var _report: Dictionary = {}
var _drafts: Dictionary = {}
var _pending_commands: Dictionary = {}
var _kitchen_drafts: Dictionary = {}
var _pending_kitchen_commands: Dictionary = {}
var _queue_instance_id := 0
var _sequence := 1
var _refreshing := false
@@ -78,6 +86,16 @@ func _build_interface() -> void:
add_child(facility_option)
facility_status_label = _label("", "BusinessFacilityStatus")
add_child(facility_status_label)
kitchen_status_label = _label("", "BusinessKitchenStatus")
add_child(kitchen_status_label)
kitchen_option = _option("BusinessKitchenOption")
kitchen_option.item_selected.connect(func(index: int) -> void:
if not _refreshing and not _has_pending(selected_facility_id):
_kitchen_drafts[selected_facility_id] = StringName(kitchen_option.get_item_metadata(index))
refresh())
add_child(kitchen_option)
kitchen_apply_button = _button("関連厨房を設定", "ApplyBusinessKitchenButton", submit_kitchen_link)
add_child(kitchen_apply_button)
add_child(_label("新規の料金"))
price_option = _option("BusinessPriceOption")
price_option.item_selected.connect(func(_index: int) -> void: _store_draft())
@@ -122,6 +140,8 @@ func refresh() -> void:
if queue != null and queue.get_instance_id() != _queue_instance_id:
_queue_instance_id = queue.get_instance_id()
_drafts.clear()
_kitchen_drafts.clear()
kitchen_option.get_popup().hide()
facility_option.get_popup().hide()
price_option.get_popup().hide()
_synchronize_commands(queue)
@@ -134,6 +154,7 @@ func refresh() -> void:
_fill_facilities()
var row := _facility_row(selected_facility_id)
var draft := _selected_draft(row)
_fill_kitchens(row)
_fill_modes(StringName(draft.get(&"price_mode", &"standard")))
business_open_button.set_pressed_no_signal(bool(draft.get(&"business_open", true)))
var pending := _has_pending(selected_facility_id)
@@ -159,11 +180,11 @@ func refresh() -> void:
effect_label.text += "\n礼拝完了ごとに信仰+%d" % int(row[&"faith_per_service"])
today_label.text = "本日の実績はまだありません。"
else:
facility_status_label.text = "%s · %s\n待ち %d人 / 利用中 %d人\n同時利用上限 %s" % [("営業する設定" if is_supply else "受付を開く設定") if bool(row.get(&"business_open", true)) else ("休業する設定" if is_supply else "新規受付を停止中"), String(STATE_NAMES.get(StringName(row.get(&"state", &"")), "状態確認中")), int(row.get(&"queued_customer_count", 0)), int(row.get(&"active_customer_count", 0)), ("%d人" % int(row[&"customer_capacity"])) if int(row.get(&"customer_capacity", -1)) >= 0 else "未設定"]
facility_status_label.text = "%s · %s\n待ち %d人 / 利用中 %d人\n同時利用上限 %s" % [("営業する設定" if is_supply else "受付を開く設定") if bool(row.get(&"business_open", true)) else ("休業する設定" if is_supply else "新規受付を停止中"), _display_state(row), int(row.get(&"queued_customer_count", 0)), int(row.get(&"active_customer_count", 0)), ("%d人" % int(row[&"customer_capacity"])) if int(row.get(&"customer_capacity", -1)) >= 0 else "未設定"]
if is_supply:
facility_status_label.text = "%s · %s\n保管・補給の状況は「商品」で確認。" % ["営業する設定" if bool(row.get(&"business_open", true)) else "休業する設定", String(STATE_NAMES.get(StringName(row.get(&"state", &"")), "状態確認中"))]
facility_status_label.text = "%s · %s\n保管・補給の状況は「商品」で確認。" % ["営業する設定" if bool(row.get(&"business_open", true)) else "休業する設定", _display_state(row)]
if is_cleaning:
facility_status_label.text = "%s · %s\n顧客利用なし・清掃員の業務拠点" % ["清掃業務を有効にする設定" if bool(row.get(&"business_open", true)) else "清掃業務を停止中", String(STATE_NAMES.get(StringName(row.get(&"state", &"")), "状態確認中"))]
facility_status_label.text = "%s · %s\n顧客利用なし・清掃員の業務拠点" % ["清掃業務を有効にする設定" if bool(row.get(&"business_open", true)) else "清掃業務を停止中", _display_state(row)]
if int(row.get(&"cleanliness", -1)) >= 0:
facility_status_label.text += "\n清潔度 %d / 100" % int(row[&"cleanliness"])
if not String(row.get(&"cleaning_status", "")).is_empty():
@@ -192,6 +213,115 @@ func refresh() -> void:
_refreshing = false
func _display_state(row: Dictionary) -> String:
var state_name := StringName(row.get(&"state", &""))
var state_text := String(STATE_NAMES.get(state_name, "状態確認中"))
var dependency: Dictionary = row.get(&"kitchen_dependency", {})
# Preserve the facility's own fault/staff/access/power/closure reason.
# The separate kitchen status label shows the dependency at the same time.
if bool(row.get(&"business_open", true)) and state_name in [&"OPEN", &"DEGRADED"] and bool(dependency.get(&"required", false)) and not bool(dependency.get(&"operating", false)):
return "関連厨房の稼働待ち" if state_name == &"OPEN" else state_text + " · 関連厨房の稼働待ち"
return state_text
func _selected_kitchen(row: Dictionary) -> StringName:
for payload: Dictionary in _pending_kitchen_commands.values():
if StringName(payload.get(&"facility_id", &"")) == selected_facility_id:
return StringName(payload.get(&"conditions", {}).get(&"related_facility_id", &""))
var dependency: Dictionary = row.get(&"kitchen_dependency", {})
return StringName(_kitchen_drafts.get(selected_facility_id, dependency.get(&"related_facility_id", &"")))
func _kitchen_placement(id: StringName) -> Dictionary:
if _runtime == null: return {}
var facilities: Variant = _runtime.get(&"facility_service")
if facilities == null or not facilities.has_method(&"get_kitchen_placement_report"): return {}
return facilities.call(&"get_kitchen_placement_report", selected_facility_id, id)
func _fill_kitchens(row: Dictionary) -> void:
var dependency: Dictionary = row.get(&"kitchen_dependency", {})
var required := bool(dependency.get(&"required", false))
kitchen_option.visible = required
kitchen_apply_button.visible = required
kitchen_status_label.visible = required
if not required: return
var selected := _selected_kitchen(row)
if not kitchen_option.get_popup().visible:
kitchen_option.clear()
kitchen_option.add_item("未接続(飲食サービスを休止)")
kitchen_option.set_item_metadata(0, &"")
var selected_index := 0
var found := selected.is_empty()
for kitchen: Dictionary in _rows(&"kitchens"):
var id := StringName(kitchen.get(&"facility_id", &""))
var placement := _kitchen_placement(id)
var label := _facility_name(kitchen)
if StringName(placement.get(&"reason", &"")) == &"INVALID_RELATED_KITCHEN_PLACEMENT":
label += "(この飲食店の裏に収まりません)"
kitchen_option.add_item(label)
var index := kitchen_option.item_count - 1
kitchen_option.set_item_metadata(index, id)
if id == selected:
selected_index = index
found = true
if not found:
kitchen_option.add_item("既存設定 %s(利用できません)" % selected)
selected_index = kitchen_option.item_count - 1
kitchen_option.set_item_metadata(selected_index, selected)
kitchen_option.set_item_disabled(selected_index, true)
kitchen_option.select(selected_index)
var pending := _has_pending(selected_facility_id)
kitchen_option.disabled = pending
kitchen_apply_button.disabled = pending or selected == StringName(dependency.get(&"related_facility_id", &""))
kitchen_apply_button.text = "設定を受理済み…" if pending else "関連厨房を設定"
kitchen_status_label.text = "関連厨房:稼働中" if bool(dependency.get(&"operating", false)) else "表層の飲食店と同じ位置範囲・階に収まる裏層の厨房を1つ選んでください。厨房が稼働するまで飲食サービスは休止します。"
if StringName(dependency.get(&"reason", &"")) == &"INVALID_RELATED_KITCHEN_PLACEMENT":
kitchen_status_label.text = "関連厨房がこの飲食店の裏に収まっていません。同じ位置範囲・階に収まる裏層の厨房を明示的に選んでください。"
if StringName(dependency.get(&"reason", &"")) == &"KITCHEN_SUSPENDED":
kitchen_status_label.text = "関連厨房が持続休止中です。別の稼働する厨房を明示的に設定できます。"
func submit_kitchen_link() -> void:
var row := _facility_row(selected_facility_id)
var dependency: Dictionary = row.get(&"kitchen_dependency", {})
if row.is_empty() or not bool(dependency.get(&"required", false)) or _has_pending(selected_facility_id): return
var selected := _selected_kitchen(row)
if selected == StringName(dependency.get(&"related_facility_id", &"")): return
if _runtime == null or not _runtime.has_method(&"submit_command"):
_set_result("営業サービスへ接続できません。", false)
return
var payload := {&"facility_id": selected_facility_id, &"conditions": {&"related_facility_id": selected}}
var key := StringName("%s%08d" % [KITCHEN_KEY_PREFIX, _sequence])
_sequence += 1
var result: Variant = _submit_ui_command(KITCHEN_COMMAND, payload, &"player", selected_facility_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_kitchen_commands[int(result.command_id)] = payload.duplicate(true)
_set_result("関連厨房の設定を受理しました。次の時間更新で反映します。", true)
else:
_show_kitchen_result(result, payload)
refresh()
func _show_kitchen_result(result: Variant, payload: Dictionary) -> void:
if not result.is_success():
if StringName(result.details.get(&"reason", &"")) == &"INVALID_RELATED_KITCHEN_PLACEMENT":
_set_result("この厨房は飲食店の裏に収まりません。同じ位置範囲・階に収まる裏層の厨房を選び直してください。", false)
else:
_set_result("関連厨房を設定できません。裏層の実在する厨房を選び直してください。", false)
return
_kitchen_drafts.erase(StringName(payload.get(&"facility_id", &"")))
_set_result("関連厨房の設定を反映しました。", true)
func select_facility(id: StringName) -> void:
selected_facility_id = id
refresh()
@@ -217,7 +347,7 @@ func submit_policy() -> void:
var payload := {&"facility_id": selected_facility_id, &"patch": patch}
var key := StringName("%s%08d" % [KEY_PREFIX, _sequence])
_sequence += 1
var result: Variant = _runtime.call(&"submit_command", COMMAND, payload, &"player", selected_facility_id, 75, -1, key)
var result: Variant = _submit_ui_command(COMMAND, payload, &"player", selected_facility_id, 75, -1, key)
if result == null or not result.has_method(&"is_success"):
_set_result("操作に応答がありません。", false)
return
@@ -271,23 +401,34 @@ func _store_draft() -> void:
func _synchronize_commands(queue: Variant) -> void:
_pending_commands.clear()
_pending_kitchen_commands.clear()
for metadata: Dictionary in queue.get_accepted_command_metadata():
var key := String(metadata.get("idempotency_key", ""))
if key.begins_with(KITCHEN_KEY_PREFIX) and key.trim_prefix(KITCHEN_KEY_PREFIX).is_valid_int():
_sequence = maxi(_sequence, int(key.trim_prefix(KITCHEN_KEY_PREFIX)) + 1)
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", []):
if StringName(command.get("command_type", &"")) == COMMAND:
_pending_commands[int(command.get("command_id", 0))] = command.get("payload", {}).duplicate(true)
elif StringName(command.get("command_type", &"")) == KITCHEN_COMMAND and String(command.get("idempotency_key", "")).begins_with(KITCHEN_KEY_PREFIX):
_pending_kitchen_commands[int(command.get("command_id", 0))] = command.get("payload", {}).duplicate(true)
func _has_pending(id: StringName) -> bool:
for payload: Dictionary in _pending_commands.values():
for payload: Dictionary in _pending_commands.values() + _pending_kitchen_commands.values():
if StringName(payload.get(&"facility_id", &"")) == id:
return true
return false
func _on_command_completed(result: Variant) -> void:
if _pending_kitchen_commands.has(int(result.command_id)):
var payload: Dictionary = _pending_kitchen_commands[int(result.command_id)]
_pending_kitchen_commands.erase(int(result.command_id))
_show_kitchen_result(result, payload)
refresh()
return
if not _pending_commands.has(int(result.command_id)):
return
var payload: Dictionary = _pending_commands[int(result.command_id)]
@@ -415,3 +556,17 @@ func _option(node_name: String) -> OptionButton:
if _style_button.is_valid():
_style_button.call(picker)
return picker
func configure_audio(submitter: Callable) -> void:
_audio_submitter = submitter
func _submit_ui_command(
command_type: StringName, payload: Dictionary = {}, actor_id: StringName = &"player",
target_id: StringName = &"", priority := 0, execute_at := -1,
idempotency_key: StringName = &""
) -> Variant:
if _audio_submitter.is_valid():
return _audio_submitter.call(command_type, payload, actor_id, target_id, priority, execute_at, idempotency_key)
return _runtime.call(&"submit_command", command_type, payload, actor_id, target_id, priority, execute_at, idempotency_key)