Pirate Holdings — Schema¶
Status: 📐 Design-only — PirateHolding is entirely unimplemented — no model file, no table, no service, no API routes; the doc itself acknowledges the model does not yet exist; all five sub-mechanics are absent. (impl audit 2026-06-16)
No PirateHolding model is committed yet. Canonical decision in ../ADR/0047-pirate-holdings.md. Player-facing reference in ../FEATURES/galaxy/pirate-holdings.md. Raid runtime in ../SYSTEMS/pirate-holding-raid.md.
A PirateHolding is the central row representing a pre-seeded hostile site in the universe — a Camp, Outpost, or Stronghold. Each holding aggregates its OutlawBase lodging anchor, optional SpecialFormation topology backing, named pirate NPC roster, and the planets/stations/sector defenses that compose its presence. The holding is also where ownership flips on player capture (atomically, with cross-table updates per the canonical capture transaction in pirate-holding-raid.md).
PirateHolding table¶
Source: services/gameserver/src/models/pirate_holding.py (target — does not yet exist).
| Column | Type | Constraints | Notes |
|---|---|---|---|
id |
UUID | PK | |
region_id |
UUID | FK regions.id, not null, indexed, CASCADE |
Holdings are region-scoped. |
tier |
Enum (camp, outpost, stronghold) |
not null | Drives recovery rate, capture difficulty, reward scale, concurrent-attacker lock policy. |
composition_profile |
Enum | not null | One of: hideout, trading_post, haven_planet, combined_small, roving_fleet, fortified_empty, fortress_planet, pirate_dock_cluster, mixed_outpost, defended_formation, fortress_complex, citadel_state, trade_nexus. Determines what physical pieces the holding contains; rolled at worldgen per the per-tier composition table in ADR-0047. |
name |
String(120) | not null, UNIQUE per (region_id, name) |
AI-generated per ADR-0044 — pirate-flavored, themed by zone (Frontier-rugged: "Ashpit", "Carrion Court"; Border-mercantile: "Crimson Profit"). |
outlaw_base_id |
UUID | FK outlaw_bases.id, not null |
The lodging row anchoring named pirate NPCs. The holding's "soul" — capture trigger fires when all NPCs anchored here are KIA. |
special_formation_id |
UUID | FK special_formations.id, nullable |
Set when the holding sits inside a Special Formation interior (typical for Outposts and Strongholds). NULL for open-sector Camps and Roving Fleets that have no formation backing. |
anchor_sector_id |
UUID | FK sectors.id, not null |
The "central" sector of the holding. For formation-anchored holdings, matches SpecialFormation.anchor_sector_id. For open-sector Camps, the captain's parked-ship sector. |
interior_sector_ids |
UUID[] | not null, default {} |
Sectors covered by this holding's defenses. Subset of the formation's interior_sector_ids for formation-anchored holdings; the patrol radius for Roving Fleets; the empty-sector perimeter for Fortified Empty Camps. |
owner_player_id |
UUID | FK players.id, nullable, indexed |
Set on capture; null while pirate-controlled. |
owner_team_id |
UUID | FK teams.id, nullable, indexed |
Set on team capture. Mutually exclusive with solo owner_player_id. |
captured_at |
DateTime | nullable | First capture timestamp; cleared on abandonment cascade. |
last_visit_at |
DateTime | nullable | Updated when the owner (or team-mate) visits a sector in interior_sector_ids. Used by the abandonment-cascade service. |
last_pirate_recovery_at |
DateTime | nullable | Last time the recovery service ticked this holding's defenses. Null for player-owned holdings (recovery is paused). |
combat_lock_held_by |
UUID | FK players.id, nullable |
The attacker holding the concurrent-attacker lock for Outpost/Stronghold tiers (per ../SYSTEMS/pirate-holding-raid.md). NULL for Camps (lock-free). |
combat_lock_held_until |
DateTime | nullable | 15-minute auto-expiry timestamp. |
combat_lock_team_snapshot |
UUID[] | nullable | Per ADR-0060 G-F2 — frozen team-mate set captured at lock acquisition. Team-mates listed here can engage the holding in parallel; players who join the lock-holder's team after this snapshot was taken cannot. Cleared when the lock releases. NULL when no lock is held; empty array when the lock-holder had no team. |
parent_holding_id |
UUID | FK pirate_holdings.id (self-reference), nullable |
Set when this holding was spawned as a daughter of another holding by the pirate ecosystem growth tick. NULL for initial Phase-12.6 seeds and seed-spawned bootstrap Camps after a region is cleansed. Useful for lineage tracking and analytics ("which Stronghold has spawned the most daughters"). |
growth_eligible_at |
DateTime | not null, default created_at + 7 days |
The earliest date this holding can promote a tier via the evolution service. Initial Phase-12.6 seeds get a 7-day grace; daughter holdings inherit created_at + 7 days similarly. |
last_damage_at |
DateTime | nullable | Last time the holding's defenses or NPCs took player damage. Used by the evolution tick to determine "untouched at full strength" duration. Updated by the combat resolver on every damage event against any defense layer. |
created_at |
DateTime | server default now | |
updated_at |
DateTime | server default now, auto-update on write |
Indexes:
- (region_id, tier) — "all holdings of tier X in region Y."
- (region_id, owner_player_id) — "all pirate-controlled holdings in region Y" (where owner_player_id IS NULL).
- (owner_player_id) — "what does this player own."
- (owner_team_id) — "what does this team own."
- (special_formation_id) — reverse lookup from formation.
- (anchor_sector_id) — sector-side lookup ("is this sector a holding's anchor?").
- GIN on interior_sector_ids — "is this sector inside any holding's interior?"
- (combat_lock_held_until) — for periodic lock-expiry sweeps.
- (parent_holding_id) — "what daughters has this holding spawned?" (ecosystem analytics).
- (growth_eligible_at) — used by the weekly evolution tick to filter eligible candidates.
Constraints:
- CHECK: owner_player_id IS NULL OR owner_team_id IS NULL (mutually exclusive).
- CHECK: tier != 'stronghold' OR special_formation_id IS NOT NULL — Strongholds are always formation-anchored. Per ADR-0060 R-F1 this is enforced at the DB layer (previously stated only as design intent in ADR-0048). Both tier-promotion paths (capture-evolution and worldgen pre-seed) set special_formation_id in the same statement; the constraint is the canonical defense.
- UNIQUE on (region_id, name) — names are unique within a region per ADR-0044.
Composition profile reference¶
Each profile is a worldgen-time roll (per the tier composition table in ADR-0047). The profile dictates which related entities exist for this holding:
| Profile | Has station? | Has planet? | Open-sector defenses? | Formation-anchored? | Tier |
|---|---|---|---|---|---|
hideout |
No | No | Yes (drones + mines) | Optional | Camp |
trading_post |
1 (Class 2–3) | No | Light | Optional | Camp |
haven_planet |
No | 1 (L1–L2 citadel) | Light | Optional | Camp |
combined_small |
1 | 1 (L1) | Modest | Optional | Camp |
roving_fleet |
No | No | No (just NPC ships) | No | Camp |
fortified_empty |
No | No | Heavy (multi-sector mines + drones + sometimes parked ships) | Optional | Camp |
fortress_planet |
50% chance, small | 1 (L3 + orbital platform) | Heavy | Typical | Outpost |
pirate_dock_cluster |
1–2 (Class 3–4) | No | Heavy | Typical | Outpost |
mixed_outpost |
1 | 1 (L2–L3) | Modest | Yes (Bubble or Tunnel) | Outpost |
defended_formation |
No | No | Heavy | Required (Bubble interior empty by design) | Outpost |
fortress_complex |
2 (Class 3–4) | 2 (L4 + orbital) | Heavy | Required (Bubble) | Stronghold |
citadel_state |
1 | 1 (L5 + orbital + rail guns) | Heavy | Required (Bubble or Dead-End Bubble) | Stronghold |
trade_nexus |
3 (Class 3–5) | 1 (L3) | Maximum | Required (Bubble or Dead-End Bubble) | Stronghold |
Relationships¶
region↔Region.pirate_holdings(cascade delete-orphan from region).outlaw_base↔OutlawBase.pirate_holding(1:1 — every holding has exactly one base).special_formation↔SpecialFormation.pirate_holding(0:1 — holding optionally references a formation).anchor_sector↔Sector.anchored_pirate_holdings(a sector can anchor multiple if formations overlap, but typically 1).owner_player/owner_team— set on capture; null while pirate-controlled.planetsandstationsare not direct FKs — they're queried viainterior_sector_idsagainst theSectortable:
def planets_in_holding(holding):
return db.query(Planet).filter(Planet.sector_id.in_(holding.interior_sector_ids)).all()
def stations_in_holding(holding):
return db.query(Station).filter(Station.sector_id.in_(holding.interior_sector_ids)).all()
When the holding flips ownership, the capture transaction updates these planets and stations atomically (per the capture trigger in ../SYSTEMS/pirate-holding-raid.md#capture-trigger).
State per defense layer (current_strength model)¶
Per ADR-0047, the holding's defensive state is the source of truth for raid progress (no per-player RaidProgress table). Each defense layer carries current_strength: Float [0.0, 1.0] and last_damage_at: DateTime nullable. The columns live on the entity that carries the defense:
| Defense surface | Where strength lives |
|---|---|
| Sector drones | Sector.defenses.drone_blocks[*].current_strength (existing JSONB extended) — canonical key is drone_blocks per ./jsonb-schema.md, not deployed_drones. Per ADR-0065 M-J1. |
| Sector mines | Sector.defenses.mines[*].current_strength (existing JSONB extended) |
| Pirate patrol ships | Sector.defenses.pirate_patrol_ships[*].current_strength (new JSONB sub-shape — see ./jsonb-schema.md) |
| Station structure | Station.defense_state.current_strength (new sub-shape on existing column) |
| Planet orbital platforms | per-platform row in the existing planet-defense schema, new column current_strength |
| Planet rail guns | per-battery row in the existing planet-defense schema, new column current_strength |
| Citadel | Planet.citadel_state.current_strength (new sub-shape) |
| OutlawBase / NPC presence | OutlawBase.current_strength (logical — derived as (active_npcs / target_count)) |
The recovery service in ../SYSTEMS/pirate-holding-raid.md#recovery-service ticks each layer per the holding's tier-bound recovery rate.
Lifecycle¶
- Created at galaxy generation by Phase 12.6 (Hostile-faction pre-seeding) per
../SYSTEMS/galaxy-generator-design.md. Tier and composition rolled deterministically from the region seed. - Defended in steady state by NPC scheduler Loop B (NPC spawn/maintenance), runtime sector-defense placement, and the daily recovery service.
- Engaged when a player initiates combat — concurrent-attacker lock claimed for Outpost/Stronghold tiers, atomic capture trigger evaluated on each NPC KIA.
- Captured atomically when all
HOSTILE_RAIDERNPCs anchored to theOutlawBaseare KIA. Ownership cross-flips planets, stations, formation, and converts OutlawBase to NPCBarracks in one transaction. - Recovered organically while pirate-controlled — defenses regen at the per-tier rate; KIA NPCs respawn on the per-tier cooldown.
- Player-owned: recovery service is paused; planets and stations operate as normal player-owned facilities. Owner can colonize, station fleets, withdraw treasury, etc.
- Abandoned after 30 days of player inactivity — fresh pirate Lord/captain spawns; ownership flips back to pirate; defenses repopulate at 50% strength (anti-grief); 90-day full re-fortification.
Source map¶
| Concern | Path (target) |
|---|---|
PirateHolding model |
services/gameserver/src/models/pirate_holding.py |
| Phase 12.6 seeding | services/gameserver/src/services/galaxy_service.py:GalaxyGenerator._seed_pirate_holdings |
| Recovery service (daily tick) | services/gameserver/src/services/pirate_holding_recovery_service.py |
| Capture trigger | services/gameserver/src/services/pirate_holding_service.py:attempt_capture |
| Abandonment service | pirate_holding_service.py:process_abandonment |
| Concurrent-attacker lock | pirate_holding_service.py:claim_holding_for_combat |
| API: list holdings | services/gameserver/src/api/routes/pirate_holdings.py |
Related¶
../ADR/0047-pirate-holdings.md— canonical decision.../FEATURES/galaxy/pirate-holdings.md— player-facing reference.../SYSTEMS/pirate-holding-raid.md— raid mechanics../special-formations.md—SpecialFormationschema; gainspirate_holding_idFK../npc-lodging.md—OutlawBaseschema; one-to-one withPirateHolding../npcs.md—NPCRoster,NPCCharacter,HOSTILE_RAIDERarchetype../jsonb-schema.md—Sector.defenses.pirate_patrol_shipssub-shape.../SYSTEMS/galaxy-generator-design.md— Phase 12.6 placement.