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] 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 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 true 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 true 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)