Files

106 lines
4.6 KiB
GDScript

class_name FinancePolicy
extends RefCounted
## Front (表金) and hidden (裏金) funds, the World Tree rank tax and dark bank interest.
## Values are the user-approved initial test values (decisions A09-A12, 2026-10-23).
## The front fund keeps the existing PLAYER_CASH account; saves made before the split
## carry no hidden fund account and are read with every Gold as front funds (A12).
const FRONT_CASH: StringName = &"PLAYER_CASH"
const BACK_CASH: StringName = &"BACK_CASH"
const RANK_TAX_EXPENSE: StringName = &"RANK_TAX_EXPENSE"
const RANK_TAX_PAYABLE: StringName = &"RANK_TAX_PAYABLE"
const LOAN_INTEREST_EXPENSE: StringName = &"LOAN_INTEREST_EXPENSE"
const LOAN_INTEREST_PAYABLE: StringName = &"LOAN_INTEREST_PAYABLE"
const LOAN_LIABILITY_PREFIX := "LOAN_LIABILITY:"
## A10: 10% of the closed day's sales (front + hidden), whole Gold rounded down.
const RANK_TAX_PERCENT := 10
## A11: 1% of the outstanding principal per day, whole Gold rounded down.
const LOAN_INTEREST_PERCENT_PER_DAY := 1
## Services of facilities with one of these tags earn hidden funds (原本: 成人施設、
## カジノ、脅迫金、ユニット売却は裏の収益). Every other service earns front funds.
const BACK_FUND_TAGS: Array[StringName] = [&"restricted_service", &"adult_only"]
## Ledger categories. Accruals are expenses of the closed day; payments, top-ups and
## the payable balances are not income or expense.
const TOP_UP_CATEGORY: StringName = &"BACK_FUND_TOP_UP"
const RANK_TAX_CATEGORY: StringName = &"RANK_TAX"
const RANK_TAX_PAYMENT_CATEGORY: StringName = &"RANK_TAX_PAYMENT"
const LOAN_INTEREST_CATEGORY: StringName = &"LOAN_INTEREST"
const LOAN_INTEREST_PAYMENT_CATEGORY: StringName = &"LOAN_INTEREST_PAYMENT"
const NON_PROFIT_CATEGORIES: Array[StringName] = [TOP_UP_CATEGORY, RANK_TAX_PAYMENT_CATEGORY, LOAN_INTEREST_PAYMENT_CATEGORY]
## A14 (2026-10-23): dismissing a general employee pays three days of the effective daily
## wage, from the front fund with the hidden fund covering the rest. It is booked to its
## own expense account, apart from WAGE, and counted with the operating costs.
const SEVERANCE_EXPENSE: StringName = &"STAFF_SEVERANCE_EXPENSE"
const SEVERANCE_CATEGORY: StringName = &"STAFF_SEVERANCE"
const SEVERANCE_DAYS := 3
static func rank_tax(sales: int) -> int:
return maxi(sales, 0) / 100 * RANK_TAX_PERCENT + maxi(sales, 0) % 100 * RANK_TAX_PERCENT / 100
static func loan_interest(outstanding_principal: int) -> int:
return maxi(outstanding_principal, 0) / 100 * LOAN_INTEREST_PERCENT_PER_DAY \
+ maxi(outstanding_principal, 0) % 100 * LOAN_INTEREST_PERCENT_PER_DAY / 100
static func severance(daily_wage: int) -> int:
return maxi(daily_wage, 0) * SEVERANCE_DAYS
static func is_back_fund_service(tags: Array) -> bool:
for tag: StringName in BACK_FUND_TAGS:
if tag in tags:
return true
return false
## Accounts of the split funds. A save without BACK_CASH predates the split and must
## not hold any of the other accounts either; they are then created empty (A12).
## Any other partial or incompatible set is rejected.
static func prepare_accounts(economy: EconomyService) -> bool:
if economy == null or not economy.has_account(FRONT_CASH) or economy.account_allows_negative(FRONT_CASH):
return false
var expected := {
BACK_CASH: false, RANK_TAX_EXPENSE: false, LOAN_INTEREST_EXPENSE: false,
RANK_TAX_PAYABLE: true, LOAN_INTEREST_PAYABLE: true,
}
if not economy.has_account(BACK_CASH):
for account_id: StringName in expected:
if economy.has_account(account_id):
return false
for account_id: StringName in expected:
if not economy.register_account(account_id, bool(expected[account_id])):
return false
return _prepare_severance_account(economy)
for account_id: StringName in expected:
if not economy.has_account(account_id) \
or economy.account_allows_negative(account_id) != bool(expected[account_id]):
return false
return _prepare_severance_account(economy)
## A save from before the severance gains its empty expense account the same way; the
## balances, the other accounts and the history are unchanged.
static func _prepare_severance_account(economy: EconomyService) -> bool:
if economy.has_account(SEVERANCE_EXPENSE):
return not economy.account_allows_negative(SEVERANCE_EXPENSE)
return economy.register_account(SEVERANCE_EXPENSE, false)
static func outstanding_principal(economy: EconomyService) -> int:
var total := 0
for account: Dictionary in economy.get_accounts():
if String(account[&"account_id"]).begins_with(LOAN_LIABILITY_PREFIX):
total += -int(account[&"balance"])
return total
static func available_funds(economy: EconomyService) -> int:
return economy.get_balance(FRONT_CASH) + economy.get_balance(BACK_CASH)