Skip to content

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

  • regionRegion.pirate_holdings (cascade delete-orphan from region).
  • outlaw_baseOutlawBase.pirate_holding (1:1 — every holding has exactly one base).
  • special_formationSpecialFormation.pirate_holding (0:1 — holding optionally references a formation).
  • anchor_sectorSector.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.
  • planets and stations are not direct FKs — they're queried via interior_sector_ids against the Sector table:
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

  1. 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.
  2. Defended in steady state by NPC scheduler Loop B (NPC spawn/maintenance), runtime sector-defense placement, and the daily recovery service.
  3. Engaged when a player initiates combat — concurrent-attacker lock claimed for Outpost/Stronghold tiers, atomic capture trigger evaluated on each NPC KIA.
  4. Captured atomically when all HOSTILE_RAIDER NPCs anchored to the OutlawBase are KIA. Ownership cross-flips planets, stations, formation, and converts OutlawBase to NPCBarracks in one transaction.
  5. Recovered organically while pirate-controlled — defenses regen at the per-tier rate; KIA NPCs respawn on the per-tier cooldown.
  6. Player-owned: recovery service is paused; planets and stations operate as normal player-owned facilities. Owner can colonize, station fleets, withdraw treasury, etc.
  7. 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