Redesign infrastructure to hypercluster scale with 4-level hierarchy
CI / build-and-push (push) Successful in 43s
CI / build-and-push (push) Successful in 43s
Replace flat DataCenter/Rack model with Cluster > Campus > Data Center > Racks hierarchy. Individual rack entities eliminated in favor of statistical batch simulation using deployment cohorts. Adds tiered network topology (ToR/agg/core) with proportional outage model, DC retrofitting, bulk operations, and drill-down UI navigation with breadcrumbs. First cluster and campus are free to preserve early game flow. Rebalances starting economy ($600K), funding rounds, and cohort scaling for hypercluster-scale gameplay. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+351
-79
@@ -6,8 +6,10 @@ import type {
|
||||
ResearchState, ModelsState, MarketState,
|
||||
CompetitorState, TalentState, DataState,
|
||||
ReputationState, AchievementState,
|
||||
DataCenter, DCTier, RackSkuId, TrainingJob,
|
||||
Cluster, Campus, DataCenter, DCTier, RackSkuId, TrainingJob,
|
||||
ActiveResearch, OwnedDataset, LocationId,
|
||||
DeploymentCohort, PipelineStage,
|
||||
NetworkHealthState,
|
||||
} from '@ai-tycoon/shared';
|
||||
import type { FundingRoundType, OverloadPolicy, TuningPreset, ModelTuning } from '@ai-tycoon/shared';
|
||||
import {
|
||||
@@ -18,8 +20,12 @@ import {
|
||||
INITIAL_REPUTATION, INITIAL_ACHIEVEMENTS,
|
||||
DC_TIER_CONFIGS, RACK_SKU_CONFIGS,
|
||||
PIPELINE_ORDER_BASE_TICKS, DC_UPGRADE_COST_FRACTION, DC_UPGRADE_INCREMENT,
|
||||
CLUSTER_COST_CONFIG, CAMPUS_TIER_COSTS, FIRST_CAMPUS_BUILD_TICKS,
|
||||
COHORT_SCALE_FACTOR,
|
||||
FUNDING_ROUNDS,
|
||||
OPEN_SOURCE_REPUTATION_BOOST,
|
||||
LOCATION_CONFIGS,
|
||||
networkSlotsRequired, maxComputeRacks,
|
||||
uuid,
|
||||
} from '@ai-tycoon/shared';
|
||||
import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
|
||||
@@ -27,9 +33,19 @@ import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
|
||||
export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models'
|
||||
| 'market' | 'talent' | 'data' | 'competitors' | 'finance' | 'achievements' | 'leaderboard' | 'settings';
|
||||
|
||||
export type InfraNavLevel = 'clusters' | 'cluster' | 'campus' | 'datacenter';
|
||||
|
||||
export interface InfraNav {
|
||||
level: InfraNavLevel;
|
||||
clusterId?: string;
|
||||
campusId?: string;
|
||||
datacenterId?: string;
|
||||
}
|
||||
|
||||
interface UIState {
|
||||
activePage: ActivePage;
|
||||
notifications: GameNotification[];
|
||||
infraNav: InfraNav;
|
||||
}
|
||||
|
||||
export interface GameNotification {
|
||||
@@ -41,8 +57,13 @@ export interface GameNotification {
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
function emptyNetworkHealth(): NetworkHealthState {
|
||||
return { tier1Required: 0, tier1Healthy: 0, tier2Required: 0, tier2Healthy: 0, tier3Required: 0, tier3Healthy: 0, racksDisconnected: 0 };
|
||||
}
|
||||
|
||||
interface Actions {
|
||||
setActivePage: (page: ActivePage) => void;
|
||||
setInfraNav: (nav: InfraNav) => void;
|
||||
addNotification: (n: Omit<GameNotification, 'id' | 'read'>) => void;
|
||||
dismissNotification: (id: string) => void;
|
||||
markAllNotificationsRead: () => void;
|
||||
@@ -50,9 +71,14 @@ interface Actions {
|
||||
setGameSpeed: (speed: GameSpeed) => void;
|
||||
togglePause: () => void;
|
||||
setTrainingAllocation: (ratio: number) => void;
|
||||
buildDataCenter: (name: string, location: LocationId, tier: DCTier) => void;
|
||||
orderRack: (dataCenterId: string, skuId: RackSkuId) => void;
|
||||
decommissionRack: (dataCenterId: string, rackId: string) => void;
|
||||
buildCluster: (name: string, locationId: LocationId) => void;
|
||||
buildCampus: (name: string, clusterId: string, dcTier: DCTier) => void;
|
||||
buildDataCenter: (name: string, campusId: string) => void;
|
||||
deployRacks: (dataCenterId: string, skuId: RackSkuId, quantity: number) => void;
|
||||
fillDCToCapacity: (dataCenterId: string, skuId: RackSkuId) => void;
|
||||
addDCsToCampus: (campusId: string, count: number) => void;
|
||||
retrofitDC: (dataCenterId: string, newSkuId: RackSkuId) => void;
|
||||
cancelRetrofit: (dataCenterId: string) => void;
|
||||
upgradeDataCenter: (dataCenterId: string, upgrade: 'cooling' | 'redundancy') => void;
|
||||
startTraining: (job: Omit<TrainingJob, 'progressTicks'>) => void;
|
||||
deployModel: (modelId: string) => void;
|
||||
@@ -97,15 +123,73 @@ const initialGameState: GameState = {
|
||||
achievements: INITIAL_ACHIEVEMENTS,
|
||||
};
|
||||
|
||||
// --- Helper: find entities in the hierarchy ---
|
||||
|
||||
function findCluster(infra: InfrastructureState, clusterId: string): Cluster | undefined {
|
||||
return infra.clusters.find(c => c.id === clusterId);
|
||||
}
|
||||
|
||||
function findCampusInCluster(cluster: Cluster, campusId: string): Campus | undefined {
|
||||
return cluster.campuses.find(c => c.id === campusId);
|
||||
}
|
||||
|
||||
function findCampus(infra: InfrastructureState, campusId: string): { cluster: Cluster; campus: Campus } | undefined {
|
||||
for (const cluster of infra.clusters) {
|
||||
const campus = cluster.campuses.find(c => c.id === campusId);
|
||||
if (campus) return { cluster, campus };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findDC(infra: InfrastructureState, dcId: string): { cluster: Cluster; campus: Campus; dc: DataCenter } | undefined {
|
||||
for (const cluster of infra.clusters) {
|
||||
for (const campus of cluster.campuses) {
|
||||
const dc = campus.dataCenters.find(d => d.id === dcId);
|
||||
if (dc) return { cluster, campus, dc };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function updateDCInInfra(infra: InfrastructureState, dcId: string, updater: (dc: DataCenter) => DataCenter): InfrastructureState {
|
||||
return {
|
||||
...infra,
|
||||
clusters: infra.clusters.map(cluster => ({
|
||||
...cluster,
|
||||
campuses: cluster.campuses.map(campus => ({
|
||||
...campus,
|
||||
dataCenters: campus.dataCenters.map(dc =>
|
||||
dc.id === dcId ? updater(dc) : dc,
|
||||
),
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function updateCampusInInfra(infra: InfrastructureState, campusId: string, updater: (campus: Campus) => Campus): InfrastructureState {
|
||||
return {
|
||||
...infra,
|
||||
clusters: infra.clusters.map(cluster => ({
|
||||
...cluster,
|
||||
campuses: cluster.campuses.map(campus =>
|
||||
campus.id === campusId ? updater(campus) : campus,
|
||||
),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export const useGameStore = create<Store>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...initialGameState,
|
||||
activePage: 'dashboard' as ActivePage,
|
||||
notifications: [],
|
||||
infraNav: { level: 'clusters' } as InfraNav,
|
||||
|
||||
setActivePage: (page) => set({ activePage: page }),
|
||||
|
||||
setInfraNav: (nav) => set({ infraNav: nav }),
|
||||
|
||||
addNotification: (n) => set((s) => ({
|
||||
notifications: [
|
||||
{ ...n, id: uuid(), read: false },
|
||||
@@ -138,6 +222,7 @@ export const useGameStore = create<Store>()(
|
||||
},
|
||||
activePage: 'dashboard',
|
||||
notifications: [],
|
||||
infraNav: { level: 'clusters' },
|
||||
}),
|
||||
|
||||
setGameSpeed: (speed) => set((s) => ({
|
||||
@@ -152,121 +237,310 @@ export const useGameStore = create<Store>()(
|
||||
compute: { ...s.compute, trainingAllocation: ratio, inferenceAllocation: 1 - ratio },
|
||||
})),
|
||||
|
||||
buildDataCenter: (name, location, tier) => set((s) => {
|
||||
const tierConfig = DC_TIER_CONFIGS[tier];
|
||||
if (s.economy.money < tierConfig.baseCost) return s;
|
||||
// --- Infrastructure: Cluster ---
|
||||
|
||||
buildCluster: (name, locationId) => set((s) => {
|
||||
const loc = LOCATION_CONFIGS[locationId];
|
||||
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
|
||||
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(loc.availableAt)) return s;
|
||||
|
||||
const existingInRegion = s.infrastructure.clusters.find(c => c.locationId === locationId);
|
||||
if (existingInRegion) return s;
|
||||
|
||||
const isFirst = s.infrastructure.clusters.length === 0;
|
||||
const cost = isFirst ? 0 : CLUSTER_COST_CONFIG.baseCost;
|
||||
if (s.economy.money < cost) return s;
|
||||
|
||||
const cluster: Cluster = {
|
||||
id: uuid(),
|
||||
name,
|
||||
locationId,
|
||||
campuses: [],
|
||||
status: isFirst ? 'operational' : 'constructing',
|
||||
constructionProgress: isFirst ? 0 : 0,
|
||||
constructionTotal: isFirst ? 0 : CLUSTER_COST_CONFIG.buildTimeTicks,
|
||||
};
|
||||
|
||||
return {
|
||||
economy: { ...s.economy, money: s.economy.money - cost },
|
||||
infrastructure: {
|
||||
...s.infrastructure,
|
||||
clusters: [...s.infrastructure.clusters, cluster],
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
// --- Infrastructure: Campus ---
|
||||
|
||||
buildCampus: (name, clusterId, dcTier) => set((s) => {
|
||||
const cluster = findCluster(s.infrastructure, clusterId);
|
||||
if (!cluster || cluster.status !== 'operational') return s;
|
||||
|
||||
const tierConfig = DC_TIER_CONFIGS[dcTier];
|
||||
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
|
||||
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(tierConfig.requiredEra)) return s;
|
||||
if (tierConfig.requiredResearch && !s.research.completedResearch.includes(tierConfig.requiredResearch)) return s;
|
||||
|
||||
const isFirstDC = s.infrastructure.dataCenters.length === 0;
|
||||
const campusCost = CAMPUS_TIER_COSTS[dcTier];
|
||||
const isFirstCampus = s.infrastructure.clusters.every(c => c.campuses.length === 0);
|
||||
const cost = isFirstCampus ? 0 : campusCost.baseCost;
|
||||
if (s.economy.money < cost) return s;
|
||||
|
||||
const buildTime = isFirstCampus ? FIRST_CAMPUS_BUILD_TICKS : campusCost.buildTimeTicks;
|
||||
|
||||
const campus: Campus = {
|
||||
id: uuid(),
|
||||
name,
|
||||
clusterId,
|
||||
dcTier,
|
||||
dataCenters: [],
|
||||
status: 'constructing',
|
||||
constructionProgress: 0,
|
||||
constructionTotal: buildTime,
|
||||
};
|
||||
|
||||
return {
|
||||
economy: { ...s.economy, money: s.economy.money - cost },
|
||||
infrastructure: {
|
||||
...s.infrastructure,
|
||||
clusters: s.infrastructure.clusters.map(c =>
|
||||
c.id === clusterId
|
||||
? { ...c, campuses: [...c.campuses, campus] }
|
||||
: c,
|
||||
),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
// --- Infrastructure: Data Center ---
|
||||
|
||||
buildDataCenter: (name, campusId) => set((s) => {
|
||||
const found = findCampus(s.infrastructure, campusId);
|
||||
if (!found || found.campus.status !== 'operational') return s;
|
||||
|
||||
const tierConfig = DC_TIER_CONFIGS[found.campus.dcTier];
|
||||
if (s.economy.money < tierConfig.baseCost) return s;
|
||||
|
||||
const isFirstDC = s.infrastructure.clusters.every(cl =>
|
||||
cl.campuses.every(ca => ca.dataCenters.length === 0),
|
||||
);
|
||||
const buildTime = isFirstDC ? tierConfig.firstBuildTimeTicks : tierConfig.buildTimeTicks;
|
||||
|
||||
const dc: DataCenter = {
|
||||
id: uuid(),
|
||||
name,
|
||||
location,
|
||||
tier,
|
||||
campusId,
|
||||
tier: found.campus.dcTier,
|
||||
status: 'constructing',
|
||||
constructionProgress: 0,
|
||||
constructionTotal: buildTime,
|
||||
racks: [],
|
||||
rackSkuId: null,
|
||||
computeRacksOnline: 0,
|
||||
computeRacksFailed: 0,
|
||||
networkHealth: emptyNetworkHealth(),
|
||||
deploymentCohorts: [],
|
||||
retrofitState: null,
|
||||
coolingLevel: 0,
|
||||
redundancyLevel: 0,
|
||||
currentUptime: 1,
|
||||
energyCostPerTick: 0,
|
||||
maintenanceCostPerTick: 0,
|
||||
effectiveComputeRacks: 0,
|
||||
usedSlots: 0,
|
||||
usedPowerKW: 0,
|
||||
energyCostPerTick: 0,
|
||||
maintenanceCostPerTick: 0,
|
||||
currentUptime: 1,
|
||||
};
|
||||
|
||||
return {
|
||||
economy: { ...s.economy, money: s.economy.money - tierConfig.baseCost },
|
||||
infrastructure: {
|
||||
...s.infrastructure,
|
||||
dataCenters: [...s.infrastructure.dataCenters, dc],
|
||||
},
|
||||
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
|
||||
...campus,
|
||||
dataCenters: [...campus.dataCenters, dc],
|
||||
})),
|
||||
};
|
||||
}),
|
||||
|
||||
orderRack: (dataCenterId, skuId) => set((s) => {
|
||||
const sku = RACK_SKU_CONFIGS[skuId];
|
||||
if (s.economy.money < sku.baseCost) return s;
|
||||
// --- Infrastructure: Deploy Racks ---
|
||||
|
||||
deployRacks: (dataCenterId, skuId, quantity) => set((s) => {
|
||||
if (quantity <= 0) return s;
|
||||
|
||||
const found = findDC(s.infrastructure, dataCenterId);
|
||||
if (!found || found.dc.status !== 'operational') return s;
|
||||
|
||||
const dc = found.dc;
|
||||
if (dc.rackSkuId !== null && dc.rackSkuId !== skuId) return s;
|
||||
|
||||
const sku = RACK_SKU_CONFIGS[skuId];
|
||||
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
|
||||
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
|
||||
if (sku.requiredResearch && !s.research.completedResearch.includes(sku.requiredResearch)) return s;
|
||||
|
||||
const dc = s.infrastructure.dataCenters.find(d => d.id === dataCenterId);
|
||||
if (!dc || dc.status !== 'operational') return s;
|
||||
|
||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||
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;
|
||||
if (actualUsedPower + sku.powerDrawKW > tierConfig.powerBudgetKW) return s;
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots);
|
||||
const existingCompute = dc.computeRacksOnline + dc.computeRacksFailed
|
||||
+ dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
|
||||
const available = maxCompute - existingCompute;
|
||||
const actualQty = Math.min(quantity, available);
|
||||
if (actualQty <= 0) return s;
|
||||
|
||||
const order = {
|
||||
const totalNetSlots = networkSlotsRequired(existingCompute + actualQty);
|
||||
const totalSlotsNeeded = existingCompute + actualQty + totalNetSlots;
|
||||
if (totalSlotsNeeded > tierConfig.rackSlots) return s;
|
||||
|
||||
const powerNeeded = (existingCompute + actualQty) * sku.powerDrawKW;
|
||||
if (powerNeeded > tierConfig.powerBudgetKW) return s;
|
||||
|
||||
const totalCost = sku.baseCost * actualQty;
|
||||
if (s.economy.money < totalCost) return s;
|
||||
|
||||
const baseTicks = PIPELINE_ORDER_BASE_TICKS;
|
||||
const scaledTicks = Math.ceil(baseTicks * (1 + COHORT_SCALE_FACTOR * actualQty));
|
||||
|
||||
const cohort: DeploymentCohort = {
|
||||
id: uuid(),
|
||||
count: actualQty,
|
||||
skuId,
|
||||
dataCenterId,
|
||||
stage: 'ordered' as const,
|
||||
stage: 'ordered',
|
||||
stageProgress: 0,
|
||||
stageTotal: PIPELINE_ORDER_BASE_TICKS,
|
||||
totalCost: sku.baseCost,
|
||||
stageTotal: scaledTicks,
|
||||
repairCount: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
economy: { ...s.economy, money: s.economy.money - sku.baseCost },
|
||||
infrastructure: {
|
||||
...s.infrastructure,
|
||||
rackPipeline: [...s.infrastructure.rackPipeline, order],
|
||||
},
|
||||
economy: { ...s.economy, money: s.economy.money - totalCost },
|
||||
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
|
||||
...d,
|
||||
rackSkuId: skuId,
|
||||
deploymentCohorts: [...d.deploymentCohorts, cohort],
|
||||
})),
|
||||
};
|
||||
}),
|
||||
|
||||
decommissionRack: (dataCenterId, rackId) => set((s) => {
|
||||
const dc = s.infrastructure.dataCenters.find(d => d.id === dataCenterId);
|
||||
if (!dc || dc.status !== 'operational') return s;
|
||||
fillDCToCapacity: (dataCenterId, skuId) => {
|
||||
const s = get();
|
||||
const found = findDC(s.infrastructure, dataCenterId);
|
||||
if (!found || found.dc.status !== 'operational') return;
|
||||
|
||||
const rack = dc.racks.find(r => r.id === rackId);
|
||||
if (!rack) return s;
|
||||
const dc = found.dc;
|
||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots);
|
||||
const existingCompute = dc.computeRacksOnline + dc.computeRacksFailed
|
||||
+ dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
|
||||
const available = maxCompute - existingCompute;
|
||||
if (available <= 0) return;
|
||||
|
||||
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 sku = RACK_SKU_CONFIGS[skuId];
|
||||
const affordableQty = Math.floor(s.economy.money / sku.baseCost);
|
||||
const powerLimit = Math.floor((tierConfig.powerBudgetKW - existingCompute * sku.powerDrawKW) / sku.powerDrawKW);
|
||||
const qty = Math.min(available, affordableQty, powerLimit);
|
||||
if (qty <= 0) return;
|
||||
|
||||
const order = {
|
||||
id: rackId,
|
||||
skuId: rack.skuId,
|
||||
dataCenterId,
|
||||
stage: 'decommission' as const,
|
||||
stageProgress: 0,
|
||||
stageTotal: sku.pipelineTimeTicks.installation,
|
||||
totalCost: 0,
|
||||
repairCount: 0,
|
||||
};
|
||||
get().deployRacks(dataCenterId, skuId, qty);
|
||||
},
|
||||
|
||||
addDCsToCampus: (campusId, count) => set((s) => {
|
||||
if (count <= 0) return s;
|
||||
|
||||
const found = findCampus(s.infrastructure, campusId);
|
||||
if (!found || found.campus.status !== 'operational') return s;
|
||||
|
||||
const tierConfig = DC_TIER_CONFIGS[found.campus.dcTier];
|
||||
const totalCost = tierConfig.baseCost * count;
|
||||
if (s.economy.money < totalCost) return s;
|
||||
|
||||
const newDCs: DataCenter[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
newDCs.push({
|
||||
id: uuid(),
|
||||
name: `${found.campus.name}-DC-${found.campus.dataCenters.length + i + 1}`,
|
||||
campusId,
|
||||
tier: found.campus.dcTier,
|
||||
status: 'constructing',
|
||||
constructionProgress: 0,
|
||||
constructionTotal: tierConfig.buildTimeTicks,
|
||||
rackSkuId: null,
|
||||
computeRacksOnline: 0,
|
||||
computeRacksFailed: 0,
|
||||
networkHealth: emptyNetworkHealth(),
|
||||
deploymentCohorts: [],
|
||||
retrofitState: null,
|
||||
coolingLevel: 0,
|
||||
redundancyLevel: 0,
|
||||
effectiveComputeRacks: 0,
|
||||
usedSlots: 0,
|
||||
usedPowerKW: 0,
|
||||
energyCostPerTick: 0,
|
||||
maintenanceCostPerTick: 0,
|
||||
currentUptime: 1,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
infrastructure: {
|
||||
...s.infrastructure,
|
||||
dataCenters,
|
||||
rackPipeline: [...s.infrastructure.rackPipeline, order],
|
||||
},
|
||||
economy: { ...s.economy, money: s.economy.money - totalCost },
|
||||
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
|
||||
...campus,
|
||||
dataCenters: [...campus.dataCenters, ...newDCs],
|
||||
})),
|
||||
};
|
||||
}),
|
||||
|
||||
// --- Infrastructure: Retrofit ---
|
||||
|
||||
retrofitDC: (dataCenterId, newSkuId) => set((s) => {
|
||||
const found = findDC(s.infrastructure, dataCenterId);
|
||||
if (!found || found.dc.status !== 'operational') return s;
|
||||
|
||||
const dc = found.dc;
|
||||
if (!dc.rackSkuId || dc.rackSkuId === newSkuId) return s;
|
||||
|
||||
const sku = RACK_SKU_CONFIGS[newSkuId];
|
||||
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
|
||||
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
|
||||
if (sku.requiredResearch && !s.research.completedResearch.includes(sku.requiredResearch)) return s;
|
||||
|
||||
const totalRacksToRetrofit = dc.computeRacksOnline + dc.computeRacksFailed;
|
||||
if (totalRacksToRetrofit <= 0) return s;
|
||||
|
||||
const oldSku = RACK_SKU_CONFIGS[dc.rackSkuId];
|
||||
const decommTicks = Math.ceil(oldSku.pipelineTimeTicks.installation * (1 + COHORT_SCALE_FACTOR * totalRacksToRetrofit));
|
||||
|
||||
return {
|
||||
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
|
||||
...d,
|
||||
status: 'retrofitting' as const,
|
||||
deploymentCohorts: [],
|
||||
retrofitState: {
|
||||
fromSkuId: d.rackSkuId!,
|
||||
toSkuId: newSkuId,
|
||||
phase: 'decommissioning' as const,
|
||||
progress: 0,
|
||||
total: decommTicks,
|
||||
racksRemaining: totalRacksToRetrofit,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}),
|
||||
|
||||
cancelRetrofit: (dataCenterId) => set((s) => {
|
||||
const found = findDC(s.infrastructure, dataCenterId);
|
||||
if (!found || found.dc.status !== 'retrofitting') return s;
|
||||
|
||||
return {
|
||||
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
|
||||
...d,
|
||||
status: 'operational' as const,
|
||||
retrofitState: null,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
|
||||
// --- Infrastructure: Upgrades ---
|
||||
|
||||
upgradeDataCenter: (dataCenterId, upgrade) => set((s) => {
|
||||
const dc = s.infrastructure.dataCenters.find(d => d.id === dataCenterId);
|
||||
if (!dc || dc.status !== 'operational') return s;
|
||||
const found = findDC(s.infrastructure, dataCenterId);
|
||||
if (!found || found.dc.status !== 'operational') return s;
|
||||
|
||||
const dc = found.dc;
|
||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||
const cost = tierConfig.baseCost * DC_UPGRADE_COST_FRACTION;
|
||||
if (s.economy.money < cost) return s;
|
||||
@@ -274,21 +548,18 @@ export const useGameStore = create<Store>()(
|
||||
const currentLevel = upgrade === 'cooling' ? dc.coolingLevel : dc.redundancyLevel;
|
||||
if (currentLevel >= 1.0) return s;
|
||||
|
||||
const dataCenters = s.infrastructure.dataCenters.map(d => {
|
||||
if (d.id !== dataCenterId) return d;
|
||||
return {
|
||||
return {
|
||||
economy: { ...s.economy, money: s.economy.money - cost },
|
||||
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
|
||||
...d,
|
||||
[upgrade === 'cooling' ? 'coolingLevel' : 'redundancyLevel']:
|
||||
Math.min(1.0, currentLevel + DC_UPGRADE_INCREMENT),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
economy: { ...s.economy, money: s.economy.money - cost },
|
||||
infrastructure: { ...s.infrastructure, dataCenters },
|
||||
})),
|
||||
};
|
||||
}),
|
||||
|
||||
// --- Non-infrastructure actions (unchanged) ---
|
||||
|
||||
startTraining: (job) => set((s) => ({
|
||||
models: {
|
||||
...s.models,
|
||||
@@ -463,7 +734,7 @@ export const useGameStore = create<Store>()(
|
||||
name: 'ai-tycoon-save',
|
||||
version: SAVE_VERSION,
|
||||
partialize: (state) => {
|
||||
const { activePage, notifications, ...rest } = state;
|
||||
const { activePage, notifications, infraNav, ...rest } = state;
|
||||
return rest;
|
||||
},
|
||||
migrate: (_persisted, version) => {
|
||||
@@ -474,11 +745,12 @@ export const useGameStore = create<Store>()(
|
||||
notifications: [{
|
||||
id: uuid(),
|
||||
title: 'Save Reset',
|
||||
message: 'Your save was reset due to a major infrastructure overhaul. Enjoy the new rack-based system!',
|
||||
message: 'Your save was reset due to a major infrastructure redesign — Hypercluster scale! Build clusters, campuses, and data centers.',
|
||||
type: 'info' as const,
|
||||
tick: 0,
|
||||
read: false,
|
||||
}],
|
||||
infraNav: { level: 'clusters' },
|
||||
} as unknown as Store;
|
||||
}
|
||||
return _persisted as Store;
|
||||
|
||||
Reference in New Issue
Block a user