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
+174 -1
View File
@@ -3,6 +3,8 @@ extends VBoxContainer
const LayerSkin := preload("res://scripts/ui/layer_interface_skin.gd")
signal status_requested(message: String, success: bool)
## Projection only: authoritative close and ledger entries own all amounts.
var _initial_layer := 0
var _runtime: Variant
@@ -13,6 +15,18 @@ var _closed_label: Label
var _facility_label: Label
var _selected_day := -1
var _picker_days: Array[int] = []
var loan_option: OptionButton
var loan_amount: LineEdit
var repay_button: Button
var repay_all_button: Button
var _loan_box: VBoxContainer
var _loan_hint: Label
var _loan_result: Label
var _selected_loan: StringName = &""
var _loan_pending_command := 0
var _loan_queue_instance := 0
var _audio_submitter := Callable()
func setup(runtime: Variant, clock: Variant, style_button := Callable(), layer := 0) -> void:
@@ -27,6 +41,7 @@ func setup(runtime: Variant, clock: Variant, style_button := Callable(), layer :
LayerSkin.register_text(heading, _initial_layer)
add_child(heading)
_cash_label = _label()
_build_repayment_controls(style_button)
_day_picker = OptionButton.new()
_day_picker.name = "LedgerDayPicker"
if style_button.is_valid(): style_button.call(_day_picker)
@@ -36,6 +51,8 @@ func setup(runtime: Variant, clock: Variant, style_button := Callable(), layer :
add_child(_day_picker)
_closed_label = _label()
_facility_label = _label()
if _runtime != null and _runtime.has_signal(&"command_completed"):
_runtime.connect(&"command_completed", _on_repayment_completed)
refresh()
@@ -97,6 +114,7 @@ func refresh() -> void:
return
if _day_picker.get_popup().visible:
return
_refresh_repayment_controls()
var report := get_report()
_cash_label.text = "第%d営業日 · 手持ち %dG\n本日の入金 +%dG / 出金 −%dG\nうち建築 %dG / 給与支払 %dG / 維持費支払 %dG\n未払い維持費 %dG\n資金増減 %+dG(営業利益は下の決算)" % [report[&"business_day"], report[&"cash"], report[&"cash_in"], report[&"cash_out"], report[&"construction_cash_out"], report[&"wage_cash_out"], report[&"upkeep_cash_out"], report[&"upkeep_arrears"], report[&"cash_net"]]
var summary: Dictionary = report[&"selected_close"]
@@ -124,7 +142,11 @@ func refresh() -> void:
var other := 0
for key: StringName in [&"maintenance", &"utilities", &"taxes_and_interest", &"fines"]:
other += int(totals.get(key, 0))
_closed_label.text = "売上 %dG / 返金 %dG\n運営費(仕入れ・契約金等) %dG\n給与 %dG / その他 %dG\n営業利益 %+dG\n前日比 %+dG / 未払給与 %dG\n帳簿照合:%s" % [totals.get(&"revenue", 0), totals.get(&"refunds", 0), totals.get(&"cost_of_goods", 0), totals.get(&"wages", 0), other, summary.get(&"net_profit", 0), summary.get(&"previous_day_delta", 0), summary.get(&"arrears_total", 0), "一致" if bool(summary.get(&"balanced", false)) else "差額あり"]
_closed_label.text = "売上 %dG / 返金 %dG\n運営費(仕入れ・契約金等) %dG\n給与 %dG / その他 %dG\n日次利益 %+dG\n前日比 %+dG / 未払給与 %dG\n帳簿照合:%s" % [totals.get(&"revenue", 0), totals.get(&"refunds", 0), totals.get(&"cost_of_goods", 0), totals.get(&"wages", 0), other, summary.get(&"net_profit", 0), summary.get(&"previous_day_delta", 0), summary.get(&"arrears_total", 0), "一致" if bool(summary.get(&"balanced", false)) else "差額あり"]
if totals.has(&"financing_principal_inflows") or totals.has(&"financing_principal_outflows"):
_closed_label.text += "\n借入元本の入金 %dG(売上外) / 元本返済 %dG(決算経費)" % [int(totals.get(&"financing_principal_inflows", 0)), int(totals.get(&"financing_principal_outflows", 0))]
if _runtime.operations_coordinator.is_legacy_principal_profit_history(int(summary[&"day"])):
_closed_label.text += "\n旧版の確定履歴:元本返済を利益から除外した計算を保持"
var legacy: bool = _runtime.operations_coordinator.is_legacy_financial_history(int(summary[&"day"]))
_closed_label.text += "\n%s %d回%s" % ["旧版の連続未払い" if legacy else "連続赤字", int(summary.get(&"consecutive_insolvency_count", 0)), "" if legacy else "(3回でゲーム終了)"]
var close_minute := int(summary[&"closed_at"]) % 1440
@@ -160,3 +182,154 @@ func _label() -> Label:
label.add_theme_font_size_override("font_size", 13)
add_child(label)
return label
func configure_audio(submitter: Callable) -> void:
_audio_submitter = submitter
func _build_repayment_controls(style_button: Callable) -> void:
_loan_hint = _label()
_loan_hint.name = "LoanRepaymentHint"
_loan_box = VBoxContainer.new()
_loan_box.name = "LoanRepaymentControls"
_loan_box.add_theme_constant_override("separation", 6)
add_child(_loan_box)
loan_option = OptionButton.new()
loan_option.name = "LoanRepaymentOption"
if style_button.is_valid(): style_button.call(loan_option)
_loan_box.add_child(loan_option)
loan_option.item_selected.connect(func(index: int) -> void:
_selected_loan = StringName(loan_option.get_item_metadata(index))
loan_amount.text = ""
_refresh_repayment_controls())
loan_amount = LineEdit.new()
loan_amount.name = "LoanRepaymentAmount"
loan_amount.placeholder_text = "返済する元本(整数G)"
loan_amount.tooltip_text = "残元本と手持ちを超えない整数額を入力"
_loan_box.add_child(loan_amount)
loan_amount.text_changed.connect(func(_text: String) -> void: _refresh_repayment_controls())
loan_amount.text_submitted.connect(func(_text: String) -> void: submit_repayment(false))
repay_button = Button.new()
repay_button.name = "LoanRepaymentButton"
repay_button.text = "指定額を返済"
if style_button.is_valid(): style_button.call(repay_button)
_loan_box.add_child(repay_button)
repay_button.pressed.connect(submit_repayment.bind(false))
repay_all_button = Button.new()
repay_all_button.name = "LoanRepayAllButton"
repay_all_button.text = "元本を完済"
if style_button.is_valid(): style_button.call(repay_all_button)
_loan_box.add_child(repay_all_button)
repay_all_button.pressed.connect(submit_repayment.bind(true))
_loan_result = _label()
_loan_result.name = "LoanRepaymentResult"
func _loan_report() -> Dictionary:
if _runtime == null or not _runtime.has_method(&"get_loan_principal_report"): return {}
return _runtime.call(&"get_loan_principal_report")
func _loan_row(report: Dictionary) -> Dictionary:
for row: Dictionary in report.get(&"loans", []):
if StringName(row[&"loan_id"]) == _selected_loan: return row
return {}
func _refresh_repayment_controls() -> void:
if loan_option == null: return
var queue: Variant = _runtime.get(&"command_queue") if _runtime != null else null
var instance: int = queue.get_instance_id() if queue != null else 0
if instance != _loan_queue_instance:
_loan_queue_instance = instance
_loan_pending_command = 0
_loan_result.text = ""
loan_amount.text = ""
var report := _loan_report()
var loans: Array = report.get(&"loans", [])
_loan_box.visible = not loans.is_empty()
if not loan_option.get_popup().visible:
loan_option.clear()
var selection := -1
for index: int in loans.size():
var row: Dictionary = loans[index]
var remaining := int(row[&"outstanding_principal"])
loan_option.add_item("借入 %d · %s" % [index + 1, "完済" if remaining == 0 else "残元本 %dG" % remaining])
loan_option.set_item_metadata(index, row[&"loan_id"])
if StringName(row[&"loan_id"]) == _selected_loan: selection = index
if selection < 0 and not loans.is_empty():
selection = 0
_selected_loan = StringName(loans[0][&"loan_id"])
if selection >= 0: loan_option.select(selection)
var row := _loan_row(report)
var remaining := int(row.get(&"outstanding_principal", 0))
var cash := int(report.get(&"cash", 0))
var waiting := not (row.get(&"pending_command_ids", []) as Array).is_empty()
var allowed := bool(report.get(&"available", false)) and bool(report.get(&"ledger_valid", false)) and not bool(report.get(&"terminal", false)) and remaining > 0 and not waiting
var text := loan_amount.text.strip_edges()
var amount := int(text) if text.is_valid_int() else 0
repay_button.disabled = not allowed or amount <= 0 or amount > remaining or amount > cash
repay_all_button.disabled = not allowed or remaining > cash
loan_amount.editable = allowed
if not bool(report.get(&"available", false)) or not bool(report.get(&"ledger_valid", false)):
_loan_hint.text = "返済の帳簿を確認できません。"
elif bool(report.get(&"terminal", false)):
_loan_hint.text = "運営は終了しました。借入の記録を確認できます。"
elif loans.is_empty():
_loan_hint.text = "借入はありません。"
elif waiting:
_loan_hint.text = "返済を受理済みです。次の時間更新で実行し、手持ちと残元本を再確認します。"
elif remaining == 0:
_loan_hint.text = "この借入は完済しています。"
else:
_loan_hint.text = "手持ち %dGから元本を返済します。残元本 %dG。返済額は日次経費に含まれます。" % [cash, remaining]
if remaining > cash: _loan_hint.text += "\n完済には手持ちが不足しています。"
if not text.is_empty() and repay_button.disabled: _loan_hint.text += "\n残元本と手持ちを超えない正の整数額を入力してください。"
func submit_repayment(full: bool = false) -> void:
var report := _loan_report()
var row := _loan_row(report)
var remaining := int(row.get(&"outstanding_principal", 0))
var text := loan_amount.text.strip_edges()
var amount := remaining if full else (int(text) if text.is_valid_int() else 0)
if not bool(report.get(&"ledger_valid", false)) or bool(report.get(&"terminal", false)) or row.is_empty() or not (row.get(&"pending_command_ids", []) as Array).is_empty() or amount <= 0 or amount > remaining or amount > int(report.get(&"cash", 0)):
_set_repayment_result("返済できません。手持ち・残元本・返済待ちを確認してください。", false)
_refresh_repayment_controls()
return
var key := StringName("ui.loan.repay.%d" % int(report[&"next_command_id"]))
var payload := {&"loan_id": _selected_loan, &"amount": amount}
var result: Variant
if _audio_submitter.is_valid():
result = _audio_submitter.call(&"repay_loan_principal", payload, &"player", _selected_loan, 0, -1, key)
else:
result = _runtime.call(&"submit_command", &"repay_loan_principal", payload, &"player", _selected_loan, 0, -1, key)
var after := _loan_row(_loan_report())
if result.is_success() and (after.get(&"pending_command_ids", []) as Array).has(int(result.command_id)):
_loan_pending_command = int(result.command_id)
_set_repayment_result("返済を受理しました。実行時に手持ちと残元本を確認します。", true)
else:
_show_repayment_result(result)
_refresh_repayment_controls()
func _show_repayment_result(result: Variant) -> void:
if result.is_success():
_set_repayment_result("元本 %dGを返済しました。残元本 %dG。" % [int(result.details.get(&"paid_principal", 0)), int(result.details.get(&"outstanding_principal", 0))], true)
loan_amount.text = ""
else:
_set_repayment_result("返済を実行できませんでした。手持ちと残元本を確認してください。", false)
func _on_repayment_completed(result: Variant) -> void:
if int(result.command_id) != _loan_pending_command: return
_loan_pending_command = 0
_show_repayment_result(result)
_refresh_repayment_controls()
func _set_repayment_result(message: String, success: bool) -> void:
_loan_result.text = message
LayerSkin.register_status_text(_loan_result, success, LayerSkin.active_layer(self))
status_requested.emit(message, success)