From 24278297f02796edd6a66dacdf358a13f94c3272 Mon Sep 17 00:00:00 2001 From: josh Date: Fri, 24 Apr 2026 20:15:48 -0400 Subject: [PATCH] Add rack decommission pipeline stage Racks can now be marked for decommission from the DC view. The rack leaves production immediately (freeing slot and power), enters the pipeline as a timed decommission order, and is removed when complete. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/pages/InfrastructurePage.tsx | 23 ++++++++--- apps/web/src/store/index.ts | 41 +++++++++++++++++-- .../src/systems/infrastructureSystem.ts | 16 +++++++- packages/shared/src/types/infrastructure.ts | 2 +- 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/apps/web/src/pages/InfrastructurePage.tsx b/apps/web/src/pages/InfrastructurePage.tsx index 0991ed5..9e12756 100644 --- a/apps/web/src/pages/InfrastructurePage.tsx +++ b/apps/web/src/pages/InfrastructurePage.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Plus, Server, MapPin, Zap, HardDrive, Wrench, ChevronDown, ChevronUp, Thermometer, Shield } from 'lucide-react'; +import { Plus, Server, MapPin, Zap, HardDrive, Wrench, ChevronDown, ChevronUp, Thermometer, Shield, X } from 'lucide-react'; import { useGameStore } from '@/store'; import { formatMoney, formatNumber, formatPercent, @@ -16,6 +16,7 @@ const STAGE_LABELS: Record = { installation: 'Installation', testing: 'Testing', repair: 'Repair', + decommission: 'Decom', }; const STAGE_COLORS: Record = { @@ -25,6 +26,7 @@ const STAGE_COLORS: Record = { installation: 'bg-violet-500', testing: 'bg-amber-500', repair: 'bg-danger', + decommission: 'bg-surface-500', }; function PipelineKanban() { @@ -32,7 +34,7 @@ function PipelineKanban() { if (pipeline.length === 0) return null; - const stages: PipelineStage[] = ['ordered', 'manufacturing', 'receiving', 'installation', 'testing', 'repair']; + const stages: PipelineStage[] = ['ordered', 'manufacturing', 'receiving', 'installation', 'testing', 'repair', 'decommission']; const grouped = stages.map(stage => ({ stage, orders: pipeline.filter(o => o.stage === stage), @@ -41,7 +43,7 @@ function PipelineKanban() { return (

Rack Pipeline

-
+
{grouped.map(({ stage, orders }) => (
@@ -108,14 +110,16 @@ function DataCenterCard({ dcId }: { dcId: string }) { const era = useGameStore((s) => s.meta.currentEra); const completedResearch = useGameStore((s) => s.research.completedResearch); const orderRack = useGameStore((s) => s.orderRack); + const decommissionRack = useGameStore((s) => s.decommissionRack); const upgradeDataCenter = useGameStore((s) => s.upgradeDataCenter); const [expanded, setExpanded] = useState(true); const tierConfig = DC_TIER_CONFIGS[dc.tier]; const currentEraIdx = ERA_ORDER.indexOf(era); - const liveUsedSlots = dc.racks.length + pipelineForDc.length; + const activePipeline = pipelineForDc.filter(o => o.stage !== 'decommission'); + const liveUsedSlots = dc.racks.length + activePipeline.length; const liveUsedPower = dc.racks.reduce((s, r) => s + RACK_SKU_CONFIGS[r.skuId].powerDrawKW, 0) - + pipelineForDc.reduce((s, o) => s + RACK_SKU_CONFIGS[o.skuId].powerDrawKW, 0); + + activePipeline.reduce((s, o) => s + RACK_SKU_CONFIGS[o.skuId].powerDrawKW, 0); const availableSkus = Object.values(RACK_SKU_CONFIGS).filter(sku => { if (ERA_ORDER.indexOf(sku.era) > currentEraIdx) return false; @@ -180,11 +184,18 @@ function DataCenterCard({ dcId }: { dcId: string }) { {dc.racks.map(rack => { const sku = RACK_SKU_CONFIGS[rack.skuId]; return ( -
+
{sku.name}
{formatNumber(sku.flopsPerRack)} FLOPS
diff --git a/apps/web/src/store/index.ts b/apps/web/src/store/index.ts index 8fe33e1..2bed087 100644 --- a/apps/web/src/store/index.ts +++ b/apps/web/src/store/index.ts @@ -51,6 +51,7 @@ interface Actions { setTrainingAllocation: (ratio: number) => void; buildDataCenter: (name: string, location: LocationId, tier: DCTier) => void; orderRack: (dataCenterId: string, skuId: RackSkuId) => void; + decommissionRack: (dataCenterId: string, rackId: string) => void; upgradeDataCenter: (dataCenterId: string, upgrade: 'cooling' | 'redundancy') => void; startTraining: (job: Omit) => void; deployModel: (modelId: string) => void; @@ -196,10 +197,9 @@ export const useGameStore = create()( if (!dc || dc.status !== 'operational') return s; const tierConfig = DC_TIER_CONFIGS[dc.tier]; - const pipelineForDc = s.infrastructure.rackPipeline.filter(o => o.dataCenterId === dataCenterId).length; - const actualUsedSlots = dc.racks.length + pipelineForDc; - const pipelinePowerForDc = s.infrastructure.rackPipeline - .filter(o => o.dataCenterId === dataCenterId) + const activePipeline = s.infrastructure.rackPipeline.filter(o => o.dataCenterId === dataCenterId && o.stage !== 'decommission'); + const actualUsedSlots = dc.racks.length + activePipeline.length; + const pipelinePowerForDc = activePipeline .reduce((sum, o) => sum + RACK_SKU_CONFIGS[o.skuId].powerDrawKW, 0); const actualUsedPower = dc.racks.reduce((sum, r) => sum + RACK_SKU_CONFIGS[r.skuId].powerDrawKW, 0) + pipelinePowerForDc; if (actualUsedSlots >= tierConfig.rackSlots) return s; @@ -225,6 +225,39 @@ export const useGameStore = create()( }; }), + decommissionRack: (dataCenterId, rackId) => set((s) => { + const dc = s.infrastructure.dataCenters.find(d => d.id === dataCenterId); + if (!dc || dc.status !== 'operational') return s; + + const rack = dc.racks.find(r => r.id === rackId); + if (!rack) return s; + + const sku = RACK_SKU_CONFIGS[rack.skuId]; + const dataCenters = s.infrastructure.dataCenters.map(d => { + if (d.id !== dataCenterId) return d; + return { ...d, racks: d.racks.filter(r => r.id !== rackId) }; + }); + + const order = { + id: rackId, + skuId: rack.skuId, + dataCenterId, + stage: 'decommission' as const, + stageProgress: 0, + stageTotal: sku.pipelineTimeTicks.installation, + totalCost: 0, + repairCount: 0, + }; + + return { + infrastructure: { + ...s.infrastructure, + dataCenters, + rackPipeline: [...s.infrastructure.rackPipeline, order], + }, + }; + }), + upgradeDataCenter: (dataCenterId, upgrade) => set((s) => { const dc = s.infrastructure.dataCenters.find(d => d.id === dataCenterId); if (!dc || dc.status !== 'operational') return s; diff --git a/packages/game-engine/src/systems/infrastructureSystem.ts b/packages/game-engine/src/systems/infrastructureSystem.ts index f09cdad..f2d61bd 100644 --- a/packages/game-engine/src/systems/infrastructureSystem.ts +++ b/packages/game-engine/src/systems/infrastructureSystem.ts @@ -36,6 +36,7 @@ function stageTotal(stage: PipelineStage, order: RackOrder): number { case 'installation': return timings.installation; case 'testing': return timings.testing; case 'repair': return RACK_REPAIR_BASE_TICKS; + case 'decommission': return timings.installation; default: return 0; } } @@ -44,7 +45,8 @@ function stageSpeed(stage: PipelineStage, engEff: number, opsEff: number): numbe switch (stage) { case 'manufacturing': return 1 + engEff * 0.1; case 'installation': - case 'testing': return 1 + opsEff * 0.1; + case 'testing': + case 'decommission': return 1 + opsEff * 0.1; case 'repair': return 1 + opsEff * 0.05; default: return 1; } @@ -88,6 +90,16 @@ export function processInfrastructure(state: GameState): InfraTickResult { continue; } + if (order.stage === 'decommission') { + const sku = RACK_SKU_CONFIGS[order.skuId]; + notifications.push({ + title: 'Rack Decommissioned', + message: `${sku.name} rack has been fully decommissioned.`, + type: 'info', + }); + continue; + } + if (order.stage === 'repair') { const total = stageTotal('testing', order); rackPipeline.push({ @@ -235,7 +247,7 @@ export function processInfrastructure(state: GameState): InfraTickResult { usedPowerKW += sku.powerDrawKW; } - const pipelineRacksForDc = rackPipeline.filter(o => o.dataCenterId === dc.id).length; + const pipelineRacksForDc = rackPipeline.filter(o => o.dataCenterId === dc.id && o.stage !== 'decommission').length; const usedSlots = totalInDc + pipelineRacksForDc; const energyCostPerTick = (tierConfig.baseEnergyCostPerTick + usedPowerKW * BASE_ENERGY_COST_PER_FLOP) diff --git a/packages/shared/src/types/infrastructure.ts b/packages/shared/src/types/infrastructure.ts index 62fa074..ec272d3 100644 --- a/packages/shared/src/types/infrastructure.ts +++ b/packages/shared/src/types/infrastructure.ts @@ -46,7 +46,7 @@ export type RackSkuId = export type PipelineStage = | 'ordered' | 'manufacturing' | 'receiving' - | 'installation' | 'testing' | 'repair'; + | 'installation' | 'testing' | 'repair' | 'decommission'; export interface PipelineTimings { manufacturing: number;