Files
TheTree_mock/scripts/ui/operating_ledger_panel.gd
Takeshi b84bade045 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.
2026-10-11 15:58:00 +09:00

336 lines
16 KiB
GDScript
Raw Permalink 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 OperatingLedgerPanel
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
var _clock: Variant
var _cash_label: Label
var _day_picker: OptionButton
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:
_initial_layer = layer
_runtime = runtime
_clock = clock
name = "OperatingLedgerPanel"
add_theme_constant_override("separation", 8)
var heading := Label.new()
heading.text = "帳簿 · 00:00締め"
heading.add_theme_color_override("font_color", Color("55371e"))
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)
_day_picker.item_selected.connect(func(index: int) -> void:
_selected_day = _day_picker.get_item_id(index)
refresh())
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()
func select_latest_close() -> void:
_selected_day = -1
_day_picker.get_popup().hide()
refresh()
func get_report() -> Dictionary:
if _runtime == null:
return {}
var minute: int = _clock.absolute_minute
var business_day := minute / 1440 + 1
var start := (business_day - 1) * 1440
if _runtime.operations_coordinator != null:
business_day = _runtime.operations_coordinator.get_open_financial_day()
start = _runtime.operations_coordinator.get_financial_period_start(business_day)
var inflow := 0
var outflow := 0
var construction_outflow := 0
var wage_outflow := 0
var upkeep_outflow := 0
for entry: LedgerEntry in _runtime.economy_service.get_entries():
if entry.absolute_minute < start or entry.absolute_minute > minute or entry.category == &"INITIALIZATION":
continue
if entry.debit_account == &"PLAYER_CASH":
inflow += entry.amount
if entry.credit_account == &"PLAYER_CASH":
outflow += entry.amount
if entry.category == &"CONSTRUCTION":
construction_outflow += entry.amount
elif entry.category == &"MAINTENANCE_PAYMENT":
upkeep_outflow += entry.amount
elif entry.category == &"WAGE":
wage_outflow += entry.amount
var summaries: Array[Dictionary] = []
if _runtime.operations_coordinator != null:
summaries = _runtime.operations_coordinator.get_day_summaries()
var selected: Dictionary = {}
for summary: Dictionary in summaries:
if int(summary.get(&"day", -1)) == _selected_day:
selected = summary
if selected.is_empty() and not summaries.is_empty():
selected = summaries.back()
return {
&"business_day": business_day, &"period_start": start,
&"cash": _runtime.economy_service.get_balance(&"PLAYER_CASH"),
&"cash_in": inflow, &"cash_out": outflow, &"cash_net": inflow - outflow,
&"construction_cash_out": construction_outflow, &"wage_cash_out": wage_outflow,
&"upkeep_cash_out": upkeep_outflow,
&"upkeep_arrears": _runtime.upkeep_settlement.get_arrears_total(),
&"closed_days": summaries, &"selected_close": selected.duplicate(true),
}
func refresh() -> void:
if _runtime == null or _cash_label == null:
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"]
if summary.is_empty():
if not _day_picker.disabled or _day_picker.item_count == 0:
_day_picker.clear()
_picker_days.clear()
_day_picker.add_item("決算は翌日00:00に確定します")
_day_picker.disabled = true
_closed_label.text = "建築後の手持ち資金だけで営業成績は決まりません。宿泊などの売上と日々の費用をここで確認できます。"
_facility_label.text = ""
return
_day_picker.disabled = false
var days: Array[int] = []
for day: Dictionary in report[&"closed_days"]:
days.append(int(day[&"day"]))
if not _day_picker.get_popup().visible:
if days != _picker_days:
_day_picker.clear()
_picker_days = days
for day_number: int in days:
_day_picker.add_item("第%d営業日 · 確定" % day_number, day_number)
_day_picker.select(_picker_days.find(int(summary[&"day"])))
var totals: Dictionary = summary.get(&"category_totals", {})
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 "差額あり"]
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
_closed_label.text += "\n確定時刻 %02d:%02d" % [close_minute / 60, close_minute % 60]
var lines: PackedStringArray = ["施設別 · 売上 / 費用 / 利益"]
var rows: Dictionary = summary.get(&"facility_profit_loss", {})
var ids := rows.keys()
ids.sort()
for id: Variant in ids:
var facility: FacilityInstance = _runtime.facility_service.get_instance(StringName(id))
var label := String(id)
if facility != null:
var definition: FacilityDefinition = _runtime.definition_registry.get_facility(facility.definition_id)
label = "%s (%d,%d)" % [definition.display_name if definition != null else facility.definition_id, facility.origin.x, facility.origin.y]
else:
# Corridors and stairs are completed construction entities without
# service instances, but their ownership upkeep still appears here.
var completed: Dictionary = _runtime.construction_service.get_facility(StringName(id))
if not completed.is_empty():
var definition: FacilityDefinition = _runtime.definition_registry.get_facility(completed[&"definition_id"])
var origin: Vector3i = completed[&"origin"]
label = "%s (%d,%d)" % [definition.display_name if definition != null else completed[&"definition_id"], origin.x, origin.y]
var row: Dictionary = rows[id]
lines.append("%s\n%dG / %dG / %+dG" % [label, row.get(&"revenue", 0), row.get(&"expenses", 0), row.get(&"net_profit", 0)])
_facility_label.text = "\n".join(lines) if not rows.is_empty() else "施設別の記帳はまだありません。"
func _label() -> Label:
var label := Label.new()
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.add_theme_color_override("font_color", Color("55371e"))
LayerSkin.register_text(label, _initial_layer)
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)