Skip to content

Mining

Active resource extraction from asteroid-field sectors. Mining is the canonical inflow path for raw ore, the secondary path for precious_metals, and a trace inflow for quantum_shards in deep-asteroid sectors. The loop is built around a per-attempt harvest action (5 turns), a Mining Laser equipment requirement, and an Astral Mining Consortium licensing system that gates lawful extraction inside AM-claimed sectors.

This doc covers the mining loop in isolation. For commodity pricing and station-side trading, see trading.md. For the Quantum Shard chain that mining trace-feeds, see ../galaxy/quantum-resources.md.

Status: ✅ Shipped — services/gameserver/src/services/mining_service.py implements the server-authoritative harvest and AM claim-license flow; the harvest and license endpoints are live in services/gameserver/src/api/routes/mining.py (registered in services/gameserver/src/api/api.py), and the ClaimLicense model is in services/gameserver/src/models/claim_license.py. The asteroid_richness, depletion_pool, and has_deep_asteroids keys live inside the existing Sector.resources JSONB and are derived on demand from Sector.resource_regeneration for sectors that predate the mining extensions. The Frontier Coalition unclaimed-asteroid rep hook (see Faction reputation hooks) is 📐 Design-only — it awaits a Frontier Coalition faction row.

Where mining happens

Mining is restricted to sectors where Sector.type = ASTEROID_FIELD. No other sector type accepts a mining attempt; the harvest endpoint rejects with not_an_asteroid_field outside of that set.

ASTEROID_FIELD is one of six special types in the SectorType enum (models/sector.py). Per ../galaxy/generation.md, galaxy generation flags 15% of sectors as special types; the remaining 85% are STANDARD. Inside that 15% slice, ASTEROID_FIELD share is cluster-driven:

  • RESOURCE_RICH clusters: ~60% of special-type rolls land on ASTEROID_FIELD (the dominant special type in resource-rich clusters).
  • Default clusters: ~25% of special-type rolls (15% / 4 evenly across ASTEROID_FIELD, RADIATION_ZONE, WARP_STORM, BLACK_HOLE).
  • MILITARY_ZONE and FRONTIER_OUTPOST clusters: 0% — those cluster profiles do not roll ASTEROID_FIELD.

Galaxy-wide, the realised asteroid-field density lands in the 3–10% of sectors band depending on the regional cluster mix. Players seeking high mining throughput are pushed toward RESOURCE_RICH cluster regions; see ../galaxy/generation.md#step-5--sectors and ../../SYSTEMS/bang-import-pipeline.md for the per-cluster distribution.

Equipment requirement

Mining requires a Mining Laser equipment slot (mining_laser key in EQUIPMENT_DEFINITIONS, services/gameserver/src/services/ship_upgrade_service.py). Without a Mining Laser equipped, the harvest endpoint rejects with no_mining_laser.

Attribute Value
Purchase cost (level 0) 35,000 cr
Compatible ships Cargo Hauler, Colony Ship, Defender
Effect mining_efficiency: 1.5 (multiplier on raw yield)
Slot key mining_laser
Removable Yes (refunds 25% of base cost)

Source: ../gameplay/ship-systems.md — equipment table.

Status: ✅ Shipped — the equipment definition lives in services/gameserver/src/services/ship_upgrade_service.py:EQUIPMENT_DEFINITIONS["mining_laser"], and its mining_efficiency multiplier is consumed by mining_service.py at harvest time via the laser-level efficiency ladder (_laser_efficiency_multiplier). The harvest endpoint rejects with no_mining_laser when no laser is fitted.

Mining Laser upgrade ladder

The base 35,000 cr Mining Laser ships at level 0. Three upgrade levels are purchasable at any Class 7+ Technology Port. Upgrades are applied to the equipped Mining Laser in place; uninstalling and re-installing the laser preserves the upgrade level.

Level Upgrade cost Cumulative cost Yield multiplier Notes
0 35,000 1.0× Stock laser.
1 50,000 85,000 1.25× Standard upgrade.
2 100,000 185,000 1.5× Required for quantum_shards trace drops.
3 200,000 385,000 2.0× End-game; raises the precious_metals rare-drop rate to its 11% ceiling (5% base + 2% per laser level, see § Output).

Status: ✅ Shipped — the ladder lives in services/gameserver/src/services/ship_upgrade_service.py:MINING_LASER_LADDER["mining_laser_level"], kept deliberately separate from the UpgradeType-keyed UPGRADE_DEFINITIONS dict (a string key there would break the upgrade-type iterators). The level is a level: int key inside Ship.equipment_slots["mining_laser"]; mining_service.py:_laser_level reads it and _laser_efficiency_multiplier maps it to the yield multiplier. The POST /mining/laser-upgrade endpoint raises the level via ShipUpgradeService.purchase_mining_laser_upgrade (the standard row-lock + credit-check purchase ritual).

The harvest action

A harvest is a single per-attempt action issued from the sector view of an ASTEROID_FIELD sector. Each attempt:

Cost / output Value
Turn cost 5 turns
Credit cost 0 cr
Real-time cooldown None (turn cost is the throttle)
Output ore primarily; precious_metals rare; quantum_shards trace
Ship state during attempt Ship.status = MINING (stationary, vulnerable)

Preconditions

  1. Player ship is in a sector where Sector.type = ASTEROID_FIELD.
  2. Ship has a Mining Laser equipped (Ship.equipment_slots["mining_laser"] present).
  3. Ship is undocked (in space, not at a station).
  4. Player has ≥ 5 turns.
  5. Cargo has ≥ 1 free unit.
  6. If the sector is AM-claimed and the player wants to avoid rep loss, a valid ClaimLicense for the sector is held (see Astral Mining Consortium claim licenses below).

Resolution

  1. Server reads the sector's Sector.resources.asteroid_richness ✅ Shipped — derived on demand from Sector.resource_regeneration (and persisted into the JSONB) by mining_service.py:_ensure_asteroid_richness when the key is absent.
  2. Set Ship.status = MINING for the duration of the resolution. The MINING status holds the ship in the sector and exposes it to PvP interrupts (see PvP interaction below).
  3. Roll the ore band from the yield matrix below using richness_tier × laser_level.
  4. Apply the Mining Laser's mining_efficiency multiplier (1.0× / 1.25× / 1.5× / 2.0× by upgrade level).
  5. Apply the planet-side Mining Engineers profession bonus if the player owns a planet in the same region with active Mining Engineers (+30% on ore/fuel production — see ../planets/professions.md). The mining bonus extends the same multiplier to direct asteroid extraction.
  6. Apply the Trade Specialist profession bonus if the player owns a planet with active Trade Specialists (+25% credit-equivalent multiplier on raw ore sold from the same trade run; see ../planets/professions.md#trade-specialists). Cross-link only — the Trade Specialist bonus applies at sale, not at harvest.
  7. Roll precious_metals rare drop: 5% per harvest at laser level 0; +2% per laser level (cap 11% at level 3).
  8. Roll quantum_shards trace drop: 1% per harvest, only if Sector.resources.has_deep_asteroids = true ✅ Shipped and the Mining Laser is at level ≥ 2.
  9. Decrement the sector's Sector.resources.depletion_pool ✅ Shipped by the rolled ore yield (precious metals and quantum shards do not count against the pool).
  10. Add commodities to player cargo, deduct turns, set Ship.status = IN_SPACE, emit the realtime event.
  11. Apply faction-rep deltas (see Faction reputation hooks below).

Yield matrix

Base ore yield per harvest, by sector richness tier × Mining Laser level. Richness tier is read from the sector's asteroid_richness JSONB field; tier 5 sectors are rare and concentrated in RESOURCE_RICH clusters.

Richness tier Laser L0 (1.0×) Laser L1 (1.25×) Laser L2 (1.5×) Laser L3 (2.0×)
1 (sparse) 2–4 ore 3–5 ore 3–6 ore 4–8 ore
2 4–8 ore 5–10 ore 6–12 ore 8–16 ore
3 (typical) 6–12 ore 8–15 ore 9–18 ore 12–24 ore
4 10–18 ore 13–23 ore 15–27 ore 20–36 ore
5 (deep core) 15–25 ore 19–31 ore 23–38 ore 30–50 ore

The min-of-band is a hard floor — see Asteroid depletion for the per-sector depletion modifier. Even at maximum depletion, the sector returns at least 1 ore per attempt.

Status: ✅ Shipped — the yield matrix is mining_service.py:_YIELD_MATRIX, indexed by (richness_tier, laser_level) with the bands above copied verbatim; the harvest rolls the band, applies the laser-efficiency and depletion multipliers, and enforces the 1-ore floor.

asteroid_richness derivation

Sector.resources.asteroid_richness is computed from Sector.resource_regeneration (a float 0.0–1.0 already on the Sector model) and stored as a JSONB sub-object. mining_service.py:_ensure_asteroid_richness performs this derivation lazily — on the first harvest or license purchase in a sector whose resources lacks the key — and persists the result, so sectors generated before the mining extensions backfill on demand:

resource_regeneration Tier Name Yield band (L0 laser) Cooldown
≥ 0.9 5 Abundant 15–25 None
0.6–0.89 4 Rich 10–18 None
0.3–0.59 3 Moderate 6–12 None
< 0.3 1 Depleted 0–1 24h between harvests

Stored shape:

"asteroid_richness": {
  "richness": "moderate",
  "richness_tier": 3,
  "yield_band": [6, 12],
  "harvest_cooldown_hours": null
}

Computed at Sector creation (worldgen or import from sw2102-bang); does not change during the game session. The yield-matrix lookup above multiplies richness_tier × laser_level for the harvest-time band; this stored representation is the canonical source of richness_tier.

Commodity output

Mining produces only raw materials — the loop never emits finished goods. Equipment, fuel, and other refined commodities are out of scope for direct mining yield.

Commodity Drop Rate Notes
ore Primary 100% Always rolled per the yield matrix. The canonical commodity sold to Class 1, Class 4, Class 5, Class 6 stations and AM-flagged refineries. Price band 15–45 cr/unit per trading.md.
precious_metals Secondary 5% per harvest at L0; +2% per laser level Rare drop on top of the base ore yield. Yield 1–3 units per drop. Price band 80–180 cr/unit (services/gameserver/src/core/commodity_economy.py), slotted between equipment and exotic_technology.
quantum_shards Trace 1% per harvest Only in sectors with Sector.resources.has_deep_asteroids = true and a Mining Laser at level ≥ 2. Yield 1 shard per drop. Cross-feed into the ../galaxy/quantum-resources.md chain — note that the primary Quantum Shard source remains nebula harvesting; the asteroid drop is a small secondary inflow that complements the dedicated Quantum Field Harvester loop.

Status: ✅ Shipped — mining_service.py emits all three commodities: ore per the yield matrix, precious_metals on the 5%+2%/level rare roll (cap 11%), and quantum_shards on the 1% trace roll gated by has_deep_asteroids and laser level ≥ 2. Each drop is clamped to free cargo space. precious_metals is priced at 80–180 cr/unit in commodity_economy.py; has_deep_asteroids is a Sector.resources JSONB key (default false, set by worldgen/import for deep-asteroid sectors).

Astral Mining Consortium claim licenses

Sectors flagged as AM-claimed (the sector's dominant Faction row has Faction.faction_type == MINING per ADR-0033; Sector.controlling_faction is a String(50) snake-case faction code — canonical at launch. The previously proposed Sector.controlling_faction_id FK alternative is dropped from the schema target; normalization to a foreign key is a post-launch follow-up tracked on the code-side punch list, not a launch-blocking decision) require a claim license to mine without rep penalty. AM controls a portion of RESOURCE_RICH clusters in Federation and Border zones — exact share is regional and tracked in ../gameplay/factions-and-teams.md.

License model

Field Value
Model ClaimLicense ✅ Shipped (services/gameserver/src/models/claim_license.py)
Issuer Any AM-flagged refinery station (Class 1 mining or Class 5 collection with AM controlling-faction tag)
Scope Single sector, single player
Duration 24 real-time hours per purchase, renewable at 80% of base cost
Daily fee 500 cr × richness_tier
Stack One active license per (player, sector) pair
Persistence Survives logout, expires on the timer

Schemaservices/gameserver/src/models/claim_license.py (table claim_licenses):

Column Type Constraints Notes
id UUID PK
player_id UUID FK Player.id, not null License holder
region_id UUID FK regions.id, nullable Compound sector identity (region + sector); nullable for region-less sectors, where the unique constraint distinguishes by sector_number alone
sector_number Integer not null Region-scoped sector
faction_code String(50) not null Issuing faction (always astral_mining_consortium at launch)
purchased_at DateTime not null UTC timestamp of purchase
expires_at DateTime not null purchased_at + 24h
cost_paid_cr Integer not null 500 × richness_tier
is_active Boolean (computed) expires_at > NOW()

UNIQUE on (player_id, region_id, sector_number) ensures one active license per (player, sector) at a time. Renewal: a fresh row is inserted on each renewal at 80% of base (400 cr × richness_tier); the prior row's expires_at is honoured for any overlap window.

License cost by richness tier

Richness tier Fee per 24h
1 (sparse) 500 cr
2 1,000 cr
3 (typical) 1,500 cr
4 2,000 cr
5 (deep core) 2,500 cr

Licenses are paid up-front; partial-day refunds are not granted on cancellation. The license row is created at any AM-flagged station via a single docked transaction; the player does not need to be in the licensed sector to purchase.

License effects

  • Licensed mining in an AM-claimed sector: harvest proceeds normally, no rep penalty, +1 AM rep per harvest (stacks with the base mining rep gain — see Faction reputation hooks).
  • Unlicensed mining in an AM-claimed sector: harvest proceeds, but −10 AM rep per extraction. Repeated unlicensed extraction trips the AM faction enforcement path (NPC patrol response in MILITARY_ZONE overlap clusters).
  • Mining in unclaimed asteroid sectors: no license required, no AM rep penalty. Frontier-zone unclaimed asteroids additionally grant a small Frontier Coalition rep boost — see hooks below.

Status: ✅ Shipped — the ClaimLicense model and the POST /mining/license purchase/renewal endpoint are live (mining_service.py:purchase_license, charging 500 cr × richness_tier on a new license and 80% of that on renewal). Licensed mining grants +2 AM rep, unlicensed extraction in AM space applies the −10 AM rep penalty, and unclaimed asteroids grant the base +1 AM tick — all wired in mining_service.py:harvest. The repeated-unlicensed AM enforcement (NPC patrol response) is 📐 Design-only.

Asteroid depletion

Each ASTEROID_FIELD sector carries a Sector.resources.depletion_pool — a counter that decrements per harvest and self-replenishes on a real-time timer. Depletion modulates yield band but never floors it to zero.

Depletion state Pool consumed Yield modifier Replenishment
Fresh 0% 1.0× (full band) n/a
Light < 5% per attempt 1.0× (full band) Auto-replenish in 24h real time after last harvest
Moderate 5–50% 0.75× Auto-replenish in 24h real time
Heavy 50–90% 0.5× Auto-replenish requires 7 real-time days
Exhausted > 90% Floor — minimum 1 ore per attempt regardless of band 7 real-time days to fresh

The hard floor is enforced server-side: if the rolled band would return zero (after multipliers and depletion), the harvest emits 1 ore and the depletion pool is unchanged for that attempt. This guarantees a non-zero feedback signal and prevents stuck "sector worthless" states.

Pool size scales with richness tier: tier 1 = 100 units, tier 5 = 500 units. The pool is decremented by the rolled ore yield; precious metals and quantum shards do not count against the pool.

Status: 🚧 Partial — the depletion_pool JSONB key, the tier-scaled pool size (tier × 100), the per-harvest decrement by rolled ore yield, the Fresh/Moderate/Heavy yield modifiers (1.0× / 0.75× / 0.5×), and the 1-ore hard floor are implemented in mining_service.py. The real-time auto-replenishment timer (24h / 7-day recovery) is 📐 Design-only — the pool decrements but does not yet refill on a schedule.

Faction reputation hooks

All AM deltas applied via services/gameserver/src/services/faction_service.py:apply_faction_rep_delta against FactionType.MINING, in the same transaction as the harvest, per the contract documented in ../gameplay/factions-and-teams.md#reputation-triggers.

Action Faction Delta Notes
Mine in any asteroid sector with Mining Laser equipped Astral Mining Consortium +1 / harvest Base AM rep tick — applies in all asteroid sectors regardless of claim status.
Mine in AM-claimed sector with valid license Astral Mining Consortium +1 / harvest Stacks with the base tick: licensed mining in AM-claimed space yields +2 AM rep / harvest.
Mine in AM-claimed sector without license Astral Mining Consortium −10 / extraction Per-attempt penalty; repeated incidents flag AM enforcement response.
Mine in Frontier-zone unclaimed asteroid Frontier Coalition +5 / harvest 📐 Design-only — small Frontier-loyalty signal; only fires in Frontier zone (Zone.type = FRONTIER) and only on unclaimed sectors. Awaits a Frontier Coalition faction row to apply the delta to.
Sell raw ore to AM-flagged refinery Astral Mining Consortium +2 / 5,000 cr of trade volume Already in ../gameplay/factions-and-teams.md#reputation-triggers; listed here for completeness.
Buy a ClaimLicense at any AM-flagged station Astral Mining Consortium +15 / purchase Single-shot reputation boost on license-fee payment (matches ../gameplay/factions-and-teams.md#reputation-triggers).

Reputation deltas are written inside the same transaction as the harvest commodity grant; a failed harvest does not move the rep counters.

Status: 🚧 Partial — all Astral Mining Consortium hooks (the +1 base tick, the +2 licensed total, the −10 unlicensed penalty, and the +15 license-purchase boost) are wired in mining_service.py. The Frontier Coalition +5 unclaimed-asteroid hook is 📐 Design-only — there is no Frontier Coalition faction row to apply it to yet.

PvP interaction

While Ship.status = MINING, the ship is stationary and visible in the sector for the duration of the harvest action. Any other player in the sector can attack a mining ship; the mining ship cannot maneuver or flee until the action resolves.

Interrupt behaviour

If a mining ship is attacked or otherwise interrupted (sector evacuation, warp gate collapse, etc.) before the harvest resolves:

  1. The harvest cancels — no commodities are awarded.
  2. 50% of the 5-turn cost is refunded (2 turns returned to Player.turns; the other 3 are sunk).
  3. The sector's depletion_pool is not decremented for the cancelled attempt.
  4. The Mining Laser is undamaged; the player can re-attempt as soon as the threat resolves.
  5. Ship.status resets to IN_SPACE (or IN_COMBAT if the interrupt was a successful PvP attack).

The 50% refund is the design lever that disincentivises grief-interrupting cheap harvests while still penalising the miner for poor situational awareness.

Status: 🚧 Partial — ShipStatus.MINING is in the canonical enum (per ../../DATA_MODELS/ships.md) and mining_service.py sets it on the ship during harvest. The harvest itself resolves atomically in a single transaction, so the interruptible in-progress window and the 50%-turn-refund-on-interrupt logic are 📐 Design-only — they await an asynchronous, multi-tick harvest model.

Player-facing affordances

  • Sector view in an ASTEROID_FIELD sector exposes a "Mine" button when a Mining Laser is fitted; greyed out (with reason text) when off-cooldown is not the issue but a precondition fails (no laser, docked, no cargo space, sector AM-claimed without license — the last surfaces a one-click "Buy License" CTA).
  • Yield preview shows the band for the current sector × laser level × richness tier before the player commits the 5 turns.
  • Depletion state is visible on the sector overlay: a four-step indicator (Fresh / Light / Moderate / Heavy) plus a real-time replenishment countdown when the sector is in Heavy.
  • ARIA market-intelligence overlay surfaces nearest AM-flagged refinery for ore offload, with current buy price.
  • License panel at AM-flagged stations lists the player's active and recently expired licenses with one-click renewal.
  • Notifications fire on harvest success (with yield), precious_metals rare drop, quantum_shards trace drop, license expiry warning (1h before), and depletion-state transitions on sectors the player has bookmarked.

Anti-cheat / safety

  • The harvest endpoint is server-authoritative and atomic; commodity grants and turn deductions occur in the same transaction.
  • Yield rolls use a cryptographically secure RNG, matching the standard set in ../galaxy/generation.md for Genesis device rolls.
  • The MINING status row takes with_for_update on the ship to serialise concurrent attempt requests from a single ship.
  • License-validity checks are read-locked against ClaimLicense rows; expiry timestamps are server-clock authoritative.
  • Per-sector depletion_pool decrements are atomic with the commodity grant; a failed grant rolls back the pool decrement.

Source map

Topic Path
Mining service services/gameserver/src/services/mining_service.py
Sector asteroid extensions services/gameserver/src/models/sector.py — the Sector.resources JSONB carries asteroid_richness, depletion_pool, and has_deep_asteroids (derived on demand by mining_service.py:_ensure_asteroid_richness)
Claim license model services/gameserver/src/models/claim_license.py
Mining Laser equipment services/gameserver/src/services/ship_upgrade_service.py:EQUIPMENT_DEFINITIONS["mining_laser"]
Mining Laser upgrade ladder services/gameserver/src/services/ship_upgrade_service.py:MINING_LASER_LADDER["mining_laser_level"]
MINING ship status services/gameserver/src/models/ship.py:ShipStatus.MINING
Faction rep hook services/gameserver/src/services/faction_service.py:apply_faction_rep_delta
AM-flagged refinery sale services/gameserver/src/services/trading_service.py
Mining API services/gameserver/src/api/routes/mining.py (registered in services/gameserver/src/api/api.py)