Planetary Production Tick¶
Purpose¶
Each tick (default 5 minutes), every owned, colonized planet runs a per-planet production cycle: colonists consume food, allocations produce fuel / organics / equipment / colonists, citadel-driven multipliers and habitability shape the rates, storage caps clamp the result, and overflow / starvation conditions feed back into morale and population. This is the per-planet step that makes empires economically alive — without it, a planet is just a parking spot.
Inputs¶
What triggers this:
- Scheduled tick (tick_production job, default 5-minute cadence).
- Manual admin trigger via POST /api/v1/admin/planets/{id}/tick.
- On-demand recompute when allocations change (no resource credit; only rate refresh).
State read:
- Planet.id, .owner_id, .specialization, .last_production.
- Allocations: fuel_allocation, organics_allocation, equipment_allocation (sum ≤ 100% of colonists).
- Resources on hand: fuel_ore, organics, equipment.
- Population: colonists, max_colonists.
- Buildings: factory_level, farm_level, mine_level, defense_level, research_level.
- Citadel state: citadel level, turret/shield/orbital-platform counts (see ../FEATURES/planets/citadels.md).
- Habitability: habitability_score (0–100).
- Production multipliers: production_efficiency (0.0–2.0), specialization bonuses.
- Siege state: under_siege flag.
Process¶
Tick loop¶
For each planet where owner_id IS NOT NULL and colonists > 0:
- Compute elapsed since
last_production. If 0, skip. - Compute production rates (per-day basis; see formulas).
- Scale rates by elapsed-time fraction (so a 5-minute tick yields 5/1440 of a daily rate).
- Compute colonist consumption (food = organics).
- Apply produced - consumed deltas, clamping to storage caps.
- Apply colonist growth (births - starvation).
- Update
last_production = now. - Emit
planet.production_tickevent with deltas.
The tick is idempotent against last_production — running twice in quick succession produces almost nothing the second time.
Production rate formula¶
Base rate is 10 units / colonist / day for each commodity, distributed by allocation:
fuel_rate = fuel_allocation * 10 * (1 + 0.10 * mine_level)
organics_rate = organics_allocation * 10 * (1 + 0.10 * farm_level)
equipment_rate= equipment_allocation* 10 * (1 + 0.10 * factory_level)
(fuel_allocation etc. is the count of colonists assigned, not a percentage. Sum of allocations cannot exceed colonists.)
Specialization bonus¶
If Planet.specialization is set, multiply the relevant rate:
| Specialization | Multipliers |
|---|---|
agricultural |
organics ×1.5, others ×0.85 |
industrial |
equipment ×1.5, organics ×0.85 |
mining |
fuel ×1.5, organics ×0.85 |
research |
All ×1.10, but +25% colonist demand |
military |
Defense buildings cheaper; production ×1.0 |
Habitability scaling¶
Colonist growth multiplier:
habitability_ratio = max(1, habitability_score) / 100
colonist_rate = colonists * 0.01 * habitability_ratio # 1% per day at 100 habitability
effective_max_colonists = max_colonists * habitability_ratio
Habitability ≤ 50 produces stagnation; ≤ 25 triggers slow population decline (target spec for very harsh worlds).
Citadel-driven multipliers¶
Citadel buildings provide passive bonuses:
| Citadel level | Production bonus |
|---|---|
| 0 | none |
| 1 | +5% all production |
| 2 | +10% |
| 3 | +15% |
| 4 | +20% |
| 5 | +25% |
Combined with specialization, the math is:
effective_rate = base_rate
* (1 + 0.10 * relevant_building_level)
* specialization_multiplier
* (1 + 0.05 * citadel_level)
* production_efficiency
production_efficiency is the catch-all admin-tunable multiplier (0.0–2.0).
Siege effects¶
If Planet.under_siege == true:
- All production rates × 0.75 (
SIEGE_PRODUCTION_PENALTY = 0.25). - Colonist growth = 0 (population stagnates).
- Resource theft: a fraction of generated commodities is intercepted by the besieger (📐 Design-only — currently penalty applies but no transfer).
Colonist consumption¶
Each colonist consumes 0.5 organics / day (food). At tick scale:
food_consumed = colonists * 0.5 * (elapsed_minutes / 1440)
If organics_on_hand < food_consumed:
- food_deficit = food_consumed - organics_on_hand
- organics_on_hand = 0
- Apply starvation: colonists -= ceil(food_deficit * 2) (each missing unit kills 2 colonists).
- Habitability score temporarily reduced (target spec).
Storage caps¶
Each commodity has a maximum derived from citadel level and storage building level (target spec):
cap_fuel = base_cap * (1 + storage_level * 0.5)
cap_organics = base_cap * (1 + storage_level * 0.5)
cap_equipment = base_cap * (1 + storage_level * 0.5)
If production would exceed cap:
- The excess is wasted (not stored, not transferred). Surface as overflow_warning event.
- 📐 Design-only — overflow could spill into the orbital station's market for a fire-sale price.
Population growth¶
births = colonists * 0.01 * habitability_ratio * (elapsed_minutes / 1440)
deaths = starvation_deaths + siege_deaths
new_colonists = clamp(
colonists + births - deaths,
0,
effective_max_colonists
)
If colonists == 0 after the tick: planet is uninhabited (allocations zero out, production halts; ownership remains).
Tick output¶
After all clamping and consumption:
deltas = {
"fuel_ore": produced_fuel,
"organics": produced_organics - food_consumed,
"equipment": produced_equipment,
"colonists": births - deaths
}
planet.fuel_ore = clamp(planet.fuel_ore + deltas.fuel_ore, 0, cap_fuel)
planet.organics = clamp(planet.organics + deltas.organics, 0, cap_organics)
planet.equipment = clamp(planet.equipment + deltas.equipment, 0, cap_equipment)
planet.colonists = clamp(planet.colonists + deltas.colonists, 0, effective_max_colonists)
planet.last_production = now
Commit; emit event.
Outputs / state changes¶
Per tick, per planet:
- Planet.fuel_ore, .organics, .equipment, .colonists updated.
- Planet.last_production updated to now.
- Planet.under_siege evaluated and possibly cleared (see ../FEATURES/planets/defense.md).
- Events:
- planet.production_tick — per-planet, includes deltas, rates, current resources.
- planet.starvation_warning — if food deficit occurred.
- planet.overflow_warning — if any cap was hit.
- planet.colonist_milestone — at population threshold (1k, 10k, 100k, max).
- Owner notification (websocket) if any warning event fires.
Invariants¶
colonists ≥ 0, bounded byeffective_max_colonists.fuel_ore,organics,equipment≥ 0, bounded by their respective caps.fuel_allocation + organics_allocation + equipment_allocation ≤ colonists.last_productionis monotonically non-decreasing.- Production rates are non-negative.
- Tick is idempotent given the same
last_production(no double-credit). - Siege flag toggles via siege resolution path only — production tick does not modify it.
- Starvation does not produce negative organics — overflow into deaths instead.
- Habitability ≤ 0 zeroes growth rate; production rates still apply.
- Citadel level ≤ 5 (defined cap); production multiplier capped at +25%.
Failure modes¶
| Mode | Target handling |
|---|---|
last_production corrupt / NULL |
Treat as now; first tick produces nothing; subsequent ticks normal. |
| Allocation sum exceeds colonists (e.g., colonist death) | Clamp allocations to current colonist count proportionally; preserve ratios. |
| Habitability ≤ 0 | Growth rate forced to 0; production still applies. |
| Specialization missing from bonus table | Default multipliers (1.0) — no bonus, no penalty. |
| Tick scheduler runs late (huge elapsed) | Cap elapsed at 24 hours per tick to prevent runaway growth. |
| Concurrent admin tick + scheduled tick | Row lock on planet; second waits, sees updated last_production, produces nothing. |
| Storage cap ≤ 0 due to misconfiguration | Treat as cap of max(1, base_cap); log warning. |
Planet has owner but colonists == 0 |
Skip — no production, no events. |
| Siege flag set but no besieger | Production penalty still applies until siege resolution clears flag. |
| Specialization changed mid-tick | Read fresh on tick; old rate reflects new specialization for that whole tick. |
Source map¶
| Concern | Path (target) |
|---|---|
| Tick service | services/gameserver/src/services/planet_production_service.py (target — currently inside planetary_service.py) |
| Production rate calculator | services/gameserver/src/services/planetary_service.py:_calculate_production_rates |
| Habitability effects | services/gameserver/src/services/planetary_service.py:get_habitability_effects |
| Specialization bonus table | same file (_calculate_specialization_bonuses) |
| Siege evaluation | services/gameserver/src/services/planetary_service.py:check_and_update_siege |
| Planet model | services/gameserver/src/models/planet.py |
| Scheduler entry | services/gameserver/src/scheduler/tick_planets.py (target — not yet split out) |
| Storage caps | services/gameserver/src/models/planet.py (max_colonists, target storage cap fields) |
Related¶
../FEATURES/planets/production.md— player-facing production controls.../FEATURES/planets/colonization.md— initial colonization that establishes a planet's first colonists.../FEATURES/planets/citadels.md— citadel buildings driving multipliers.../FEATURES/planets/defense.md— siege state.turn-regeneration.md— sibling tick that uses the same scheduler infrastructure.market-pricing.md— produced resources feed back into the market.