Skip to content

Commit 3b0cb67

Browse files
committed
Merge remote-tracking branch 'origin/main' into ee9-ee10-balance-instruments
2 parents bdd9059 + ef06ec1 commit 3b0cb67

10 files changed

Lines changed: 823 additions & 3 deletions

File tree

docs/game-design/WORKSHOP_2_BACKLOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,16 @@ more interesting."*
284284
unlosable w.r.t. compounding debt. Fed into #638's final iteration.
285285
- **DQ-25 · Desperation-lever revisit** — Pip wants the mechanic + flavor re-understood
286286
(ADR-0003's catch-up machinery); gather solver-bot data (EE-9) first, then a workshop
287-
beat.
287+
beat. First structural finding (EE-9, pre-migration data): the lever is a plan-phase
288+
action whose doom trigger only re-checks at month boundaries — candidate window-verb.
289+
- **DQ-26 · VC/equity depth revisit** *(Pip 2026-07-15, on approving #641)* — the L5
290+
instruments ship with equity dilution + board seats as inert standing terms (DQ-7
291+
stubs). Future work: (a) make VC investment *interesting* as a mechanic (what does a
292+
board seat DO — agenda pressure, window injections, exit pressure?); (b) build the
293+
measurement layer for equity and board seats (cap-table state, control thresholds);
294+
(c) Pip's framing: these are **set-at-config-or-accept-basics** knobs — fine-tunable
295+
as difficulty or scenario settings (ADR-0016 league/scenario surface), so the config
296+
schema should carry them even while the mechanics stay basic.
288297
- **L5 finance construction AUTHORIZED (Pip):** loans over months+, player optionality
289298
among offered instruments (interest-rate intuitions from ADR-0013), "orchestrate the
290299
construction… in parallel with the other finance ideas" — build lane L5 (#616)

godot/data/actions/financing.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,33 @@
3737
"action_points": 1
3838
},
3939
"category": "financing"
40+
},
41+
{
42+
"id": "seek_financing",
43+
"name": "Seek Financing",
44+
"description": "Run a raise (ADR-0013): the cost-of-debt engine quotes 2-3 concurrent offers priced by org type, counterparty, reputation and leverage. Offers stand until they expire; accept at plan speed.",
45+
"costs": {
46+
"action_points": 1
47+
},
48+
"category": "financing"
49+
},
50+
{
51+
"id": "accept_financing_offer",
52+
"name": "Accept Offer",
53+
"description": "Accept a standing financing offer by id (from Seek Financing) — cash now, its quoted terms mint a ledger entry.",
54+
"costs": {
55+
"action_points": 1
56+
},
57+
"category": "financing"
58+
},
59+
{
60+
"id": "pay_bills",
61+
"name": "Pay the Bill",
62+
"description": "Settle the soonest-due affordable loan early from cash (#566), retiring its balloon before it bills.",
63+
"costs": {
64+
"action_points": 1
65+
},
66+
"category": "financing"
4067
}
4168
]
4269
}

godot/data/balance/defaults.json

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,117 @@
9393
"easy": { "money_multiplier": 1.5, "max_action_points": 4 },
9494
"standard": { "money_multiplier": 1.0, "max_action_points": 3 },
9595
"hard": { "money_multiplier": 0.75, "max_action_points": 2 }
96+
},
97+
"financing": {
98+
"_description": "Cost-of-debt pricing engine + financing instruments (ADR-0013, lane L5/#616). FinanceEngine.price() computes terms from org_type + counterparty + typed reputation + current leverage; generate_offers() mints 2-3 concurrent STANDING offers with an expiry (ADR-0012 — expiry mints no ledger entry). Engine: godot/scripts/core/finance_engine.gd. GOTCHA: computed interest rates are snapped to 0.001 (JSON save-safe, probe-verified against the real full-precision save path) and principals to whole units before entering a Ledger.Entry, so minted offers survive save/load (calibration open-Q #2). Baseline continuity: bank_loan at default context reproduces the calibrated ledger.loan terms (rate 0.008, ~77-tick fuse, 1.2x balloon); the sweep's take_loan path is UNCHANGED (routes Ledger.loan directly), so this block is additive and does not move the L1 medians.",
99+
"ticks_per_month": 22.0,
100+
"offer_ttl_turns": 44,
101+
"leverage_scale": 120000.0,
102+
"rate_floor": 0.002,
103+
"rate_ceiling": 0.05,
104+
"rep_rate_relief": 0.004,
105+
"leverage_rate_penalty": 0.004,
106+
"org_factors": {
107+
"_description": "Multiply the base interest (ADR-0013: org type is the pricing engine's biggest input). >1 = worse cost of capital; <1 = cheaper. for_profit borrows cheapest (equity as collateral); independent/solo pays a premium.",
108+
"nonprofit": 1.0,
109+
"for_profit": 0.9,
110+
"academic": 1.05,
111+
"independent": 1.15
112+
},
113+
"counterparty_factors": {
114+
"_description": "Who you owe changes what it costs (ADR-0013 major input; ADR-0007 counterparty dread). philanthropist = 0.0 => a gift, not a debt. shadow (desperation) = the punitive last resort.",
115+
"bank": 1.0,
116+
"vc": 0.85,
117+
"government": 0.75,
118+
"foundation": 0.8,
119+
"philanthropist": 0.0,
120+
"shadow": 2.2
121+
},
122+
"offer_tiers": {
123+
"_description": "Reputation-gated menu quality (ADR-0013: better standing => better menus). The instrument's rep_channel picks safety- or finance-rep; that value selects the highest threshold it clears; counts[tier] caps concurrent offers.",
124+
"thresholds": [0, 30, 60],
125+
"counts": [2, 3, 3]
126+
},
127+
"instruments": {
128+
"bank_loan": {
129+
"name": "Bank term loan",
130+
"factory": "loan",
131+
"counterparty": "bank",
132+
"rep_channel": "finance",
133+
"principal_min": 30000,
134+
"principal_max": 80000,
135+
"principal_multiplier": 1.2,
136+
"term_months": 3.5,
137+
"base_rate": 0.008,
138+
"non_cash": {},
139+
"availability": { "min_rep": 15, "max_leverage": 3.0, "org_types": [] }
140+
},
141+
"credit_line": {
142+
"name": "Revolving credit line",
143+
"factory": "loan",
144+
"counterparty": "bank",
145+
"rep_channel": "finance",
146+
"principal_min": 60000,
147+
"principal_max": 160000,
148+
"principal_multiplier": 1.15,
149+
"term_months": 4.0,
150+
"base_rate": 0.010,
151+
"non_cash": {},
152+
"availability": { "min_rep": 45, "max_leverage": 1.5, "org_types": [] }
153+
},
154+
"vc_equity": {
155+
"name": "VC equity round",
156+
"factory": "equity",
157+
"counterparty": "vc",
158+
"rep_channel": "finance",
159+
"principal_min": 120000,
160+
"principal_max": 400000,
161+
"principal_multiplier": 1.0,
162+
"term_months": 0.0,
163+
"base_rate": 0.0,
164+
"non_cash": { "equity_dilution": 0.12, "board_seat": true },
165+
"availability": { "min_rep": 30, "min_hype": 25, "max_leverage": 5.0, "org_types": ["for_profit"] }
166+
},
167+
"grant_strings": {
168+
"name": "Directed grant (agenda strings)",
169+
"factory": "funding_strings",
170+
"counterparty": "government",
171+
"rep_channel": "safety",
172+
"principal_min": 25000,
173+
"principal_max": 70000,
174+
"principal_multiplier": 0.10,
175+
"term_months": 4.0,
176+
"base_rate": 0.0025,
177+
"non_cash": { "agenda_narrowing": true },
178+
"availability": { "min_rep": 25, "org_types": [] }
179+
},
180+
"philanthropy": {
181+
"name": "Philanthropic gift",
182+
"factory": "philanthropy",
183+
"counterparty": "philanthropist",
184+
"rep_channel": "safety",
185+
"principal_min": 15000,
186+
"principal_max": 45000,
187+
"principal_multiplier": 1.0,
188+
"term_months": 0.0,
189+
"base_rate": 0.0,
190+
"appearance_chance": 0.35,
191+
"non_cash": {},
192+
"availability": { "min_rep": 30, "org_types": [] }
193+
},
194+
"desperation": {
195+
"name": "Desperation payroll lever",
196+
"factory": "desperation_payroll",
197+
"counterparty": "shadow",
198+
"rep_channel": "finance",
199+
"principal_min": 0,
200+
"principal_max": 0,
201+
"principal_multiplier": 1.0,
202+
"term_months": 2.5,
203+
"base_rate": 0.008,
204+
"non_cash": { "secret": true },
205+
"availability": { "min_rep": 0, "org_types": [] }
206+
}
207+
}
96208
}
97209
}

godot/scripts/core/actions.gd

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,46 @@ static func execute_action(action_id: String, state: GameState) -> Dictionary:
370370
state.ledger.add(Ledger.staff_rider("Contractor"))
371371
result["message"] = "Brought on a contractor (+2 AP; a governance rider bills later)"
372372

373+
# --- ADR-0013 cost-of-debt engine (L5 #616): the raise-as-campaign flow ---
374+
"seek_financing":
375+
# Run a purpose-tagged raise: the pricing engine quotes a menu of standing offers
376+
# (ADR-0012 expiry). They persist on state.financing_offers for accept at plan speed.
377+
var rng: RandomNumberGenerator = state.rng if ("rng" in state and state.rng != null) else RandomNumberGenerator.new()
378+
var ctx: Dictionary = FinanceEngine.context_from_state(state)
379+
state.financing_offers = FinanceEngine.generate_offers(ctx, "general", rng)
380+
if state.financing_offers.is_empty():
381+
result["message"] = "No financiers are interested right now (reputation/standing too low)."
382+
else:
383+
var lines: Array = []
384+
for o in state.financing_offers:
385+
lines.append("%s ($%d @ %.1f%%/mo)" % [o["name"], int(o["principal"]), float(o["interest_rate"]) * FinanceEngine.ticks_per_month() * 100.0])
386+
result["message"] = "Offers on the table: " + "; ".join(lines)
387+
388+
"accept_financing_offer":
389+
# Accept the first still-live standing offer (the UI selects a specific one via
390+
# FinanceEngine.accept_offer(offer, state); the headless/action default is best-first).
391+
var accepted := false
392+
for o in state.financing_offers:
393+
if FinanceEngine.offer_live(o, int(state.turn)):
394+
var ar: Dictionary = FinanceEngine.accept_offer(o, state)
395+
result["message"] = ar.get("message", "")
396+
result["success"] = bool(ar.get("success", false))
397+
accepted = ar.get("success", false)
398+
if accepted:
399+
state.financing_offers.erase(o)
400+
break
401+
if not accepted:
402+
result["message"] = "No live offer to accept (seek financing first, or offers expired)."
403+
404+
"pay_bills":
405+
# The "pay the bill" interaction (#566): settle the soonest-due affordable loan early.
406+
if state.ledger:
407+
var pr: Dictionary = state.ledger.pay_soonest_payable(state)
408+
result["message"] = pr.get("message", "")
409+
result["success"] = bool(pr.get("success", false))
410+
else:
411+
result["message"] = "No ledger to pay against."
412+
373413
"submit_paper":
374414
# Paper submission action (Issue #468)
375415
# This is handled by UI - creates paper and adds to state

0 commit comments

Comments
 (0)