Files
TheTree_mock/scripts/ui/operating_ledger_panel.gd
T

362 lines
18 KiB
GDScript
Raw 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
var back_inflow := 0
var back_top_up := 0
var rank_tax_outflow := 0
var loan_interest_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 == FinancePolicy.BACK_CASH:
back_inflow += entry.amount
if entry.credit_account == FinancePolicy.BACK_CASH and entry.category == FinancePolicy.TOP_UP_CATEGORY:
back_top_up += entry.amount
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
elif entry.category == FinancePolicy.RANK_TAX_PAYMENT_CATEGORY:
rank_tax_outflow += entry.amount
elif entry.category == FinancePolicy.LOAN_INTEREST_PAYMENT_CATEGORY:
loan_interest_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(),
&"back_cash": _runtime.economy_service.get_balance(FinancePolicy.BACK_CASH),
&"back_cash_in": back_inflow, &"back_top_up": back_top_up,
&"rank_tax_cash_out": rank_tax_outflow, &"loan_interest_cash_out": loan_interest_outflow,
&"finance_arrears": -_runtime.economy_service.get_balance(FinancePolicy.RANK_TAX_PAYABLE)
- _runtime.economy_service.get_balance(FinancePolicy.LOAN_INTEREST_PAYABLE),
&"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"]]
# 表金は上の手持ち。裏金は裏のサービスの売上で、表金で払えない分だけを補う(A09)。
_cash_label.text += "\n裏金 %dG(本日の入金 +%dG / 表金へ補填 −%dG)\n本日の税 %dG / 利息 %dG(未払い %dG)" % [report[&"back_cash"], report[&"back_cash_in"], report[&"back_top_up"], report[&"rank_tax_cash_out"], report[&"loan_interest_cash_out"], report[&"finance_arrears"]]
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 "差額あり"]
var funds: Dictionary = (summary.get(&"settlement", {}) as Dictionary).get(&"funds", {})
if not funds.is_empty():
# The day's every top-up; a report saved before the version mark counted the close's own only.
var top_up_scope := "" if funds.has(&"fund_report_version") or funds.has("fund_report_version") else "(締めの時の分のみ)"
_closed_label.text += "\nうち表の売上 %dG / 裏の売上 %dG\n世界樹ランク税 %dG(支払 %dG)/ 利息 %dG(支払 %dG)\n裏金から表金へ補填%s %dG / 未払い 税 %dG・利息 %dG" % [int(funds.get(&"revenue_front", 0)), int(funds.get(&"revenue_back", 0)), int(funds.get(&"rank_tax", 0)), int(funds.get(&"rank_tax_paid", 0)), int(funds.get(&"loan_interest", 0)), int(funds.get(&"loan_interest_paid", 0)), top_up_scope, int(funds.get(&"back_top_up", 0)), int(funds.get(&"rank_tax_arrears", 0)), int(funds.get(&"loan_interest_arrears", 0))]
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))
# A09: the front fund pays first and the hidden fund covers the rest in the same posting.
var funds := int(report.get(&"available_funds", cash))
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 > funds
repay_all_button.disabled = not allowed or remaining > funds
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(計 %dG)から元本を返済します。表金で足りない分は裏金から補填します。残元本 %dG。返済額は日次経費に含まれます。" % [cash, funds - cash, funds, remaining]
if remaining > funds: _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(&"available_funds", 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)