A House Divided A House DividedDocumentation
Changelog
Game Design/Economy & Finance

Subsidiary Corporations

Last updated 2026-08-21
Player wikiSubsidiary Corporations
Source files

Feature flag: subsidiaryCorporationsEnabled on the gameState singleton (_id: "current"), default ON for fresh worlds via featureFlagDefaults. Fail-closed on any world that never stamped the flag, isSubsidiaryCorporationsEnabled() returns false unless the field is explicitly true. Every subsidiary API route, and every effect below, is gated behind this flag.

A player-owned corporation can control other player-owned corporations as a holding group: one corp (the parent) formalizes >50% voting control of another (the subsidiary), gaining management rights over its CEO seat, funding it directly, and setting a dividend floor. The relationship is always derived, never stored as a parent pointer, a corp is a subsidiary of whoever currently controls its votes, full stop. This companion doc covers that system; see corporations.md for the base corporation model it extends.

Core Model: Derived Control, Not a Stored Edge#

There is no parentCorporationId field anywhere. Two facts combine to define "is this corp a managed subsidiary right now, and if so, of whom":

  1. Voting control, some corporation currently controls >50% of the target's voting power (SUBSIDIARY_OWNERSHIP_THRESHOLD_PERCENT = 50 in corporateOwnership.ts, kept in sync with SUBSIDIARY_FORMALIZATION_THRESHOLD_PERCENT = 50 in the subsidiaries module). Voting power, not raw share count, super-share multipliers are respected via acquirerOwnershipPercent() / getControllingCorporateParent().
  2. Formalization, Corporation.subsidiaryFormalizedAtTurn is set. This is an opt-in marker, not a parent pointer; it just records "yes, someone formalized this."

isFormalizedSubsidiary() requires both: a controlling parent AND the marker. If voting control lapses (parent sells down, dilution, etc.) the marker becomes stale and is cleared automatically by the turn processor's zombie cleanup (see below), there is no manual "unformalize because the numbers moved" step for players.

Unsold shares still sitting in the parent's own open sell orders or listings count toward its control for every derived-control check (loadReservedCorporatePositions / resolveControllingCorporateParent), so listing a controlling block for sale does not silently drop parent powers before the trade fills.

Eligibility#

Formalizing a Subsidiary#

POST /api/corporations/[id]/subsidiary/formalize, target is [id], parentCorporationId in the body.

Releasing a Subsidiary#

POST /api/corporations/[id]/subsidiary/release, dismissCaretaker optional in body.

Zombie Subsidiary Cleanup (turn processor)#

cleanupZombieSubsidiaries() runs every turn inside processCorporationTurn (gated by the feature flag), after the corp/sector lookups are built:

Authorization: canActOnCorporationAsParent#

Every subsidiary management action (release, capital injection, dividend floor, CEO appointment) shares one authorization check:

true iff:
  feature flag is ON, AND
  sub.subsidiaryFormalizedAtTurn is set, AND
  sub is not national/state-owned, AND
  the caller is the sitting (non-vacant) CEO/owner of whoever currently
  controls >50% of sub's voting power (recomputed live, reserved holdings included)

Parent authority is recomputed on every call from live share data, it is never cached on userId.

Capital Injection#

POST /api/corporations/[id]/subsidiary/capital-injection, { amount } in the parent's local currency.

Parent Dividend Floor#

POST /api/corporations/[id]/subsidiary/dividend-floor, { floorPct } (0 to MAX_DIVIDEND_RATE).

CEO Appointment on Subsidiaries#

POST /api/corporations/[id]/subsidiary/appoint-ceo, { ceoType: "character" | "npp", characterId?, forcedNppId? }.

Blocked While a Formalized Subsidiary: Share Issuance#

subsidiaryIssuanceBlockReason() in issuanceGuard.ts blocks all equity issuance (public issuance, self-issuance, going public) on any corp with subsidiaryFormalizedAtTurn set, whenever the feature flag is on. Rationale in-code: any issuance dilutes the parent's stake and could drop it below the >50% control threshold, letting the subsidiary's CEO escape parent oversight. The parent must fund it via capital injection instead, or release it first if dilution is genuinely wanted. No-op when the feature is off or the corp isn't formalized, non-subsidiary issuance is unaffected.

Spin-Off#

POST /api/corporations/[id]/subsidiary/spin-off, { sectorType, name, tickerSymbol?, appointedCeoType, appointedCeoCharacterId?, forcedNppId? }.

Moves one of the parent's sector types into a new, wholly parent-owned private corporation, immediately formalized as a subsidiary of the parent.

Ownership Cycle Guard on Purchase#

corpPurchaseWouldCycle() (cycleGuard.ts) is checked when a corporation (not a player character) buys shares in another corporation, at the point of purchase rather than only at formalization time. It reads the full corp cap-table graph (_id, shareholders, totalShares, superShareMultiplier) and reuses the same wouldCreateOwnershipCycle() walk. Refusal message: OWNERSHIP_CYCLE_ERROR.

Holding Groups: Tax Relief, Balance Sheet, Synergies#

A group is the set of corporations connected by formalized subsidiary edges, resolved fresh every turn by resolveFormalizedGroups() in groups/groupMembership.ts. A group edge requires BOTH de-facto control (>50% voting) AND formalization, de-facto control alone is not a group for any of the effects below. Cycles that slip past the guards are handled defensively: the root-resolution walk is bounded by edge count and canonicalizes on the smallest id in any detected cycle so the group doesn't fragment into singletons and silently lose relief.

Group Loss Relief (tax)#

computeGroupRelief() in groups/lossRelief.ts. Each corp is still taxed alone during normal turn processing; relief is applied as a rebate afterward, not a recomputation of the tax figure, arithmetically identical to filing consolidated, but additive rather than invasive to the turn's hot path.

Group Balance Sheet (read-only consolidation)#

loadGroupBalanceSheet() in groups/groupBalanceSheet.ts. Consolidates nothing in the engine, every member keeps its own cash, shares, and shareholders. This is purely a display aggregation for a parent corp's page:

Group Synergies (marketing & logistics)#

computeGroupSynergies() in groups/synergies.ts. An explicitly asymmetric model: members are pulled up toward the group's best marketingStrength / logisticsStrength, never dragged down, averaging was rejected because it would make acquiring a weak subsidiary strictly punish a strong parent.

Admin Toggle#

POST /api/admin/corporations/subsidiaries/toggle flips gameState.subsidiaryCorporationsEnabled. UI: .

UI#

Collections & Fields#

No new collection. All state lives on the existing corporations documents:

Field Meaning
subsidiaryFormalizedAtTurn?: number Presence = formalized; the turn it happened. Cleared automatically if control lapses.
isSpinOff?: boolean, spunOffFromCorpId?: ObjectId, spunOffAtTurn?: number Spin-off provenance; feeds the synergy brand-inheritance ceiling.
lastSpinOffTurn?: number Cooldown anchor on the parent for spin-offs it initiates.
lastCapitalInjectionTurn?: number Per-subsidiary cooldown anchor for capital injections received.
parentDividendFloorPct?: number, parentDividendFloorSetByCorpId?: ObjectId Dividend floor set by the controlling parent; only honored while that parent still controls >50%.
pendingDivestiture?: PendingDivestiture Merger-review remedy; measured against the controlled group, not discharged by a spin-off alone.

Key Files#