8c9555bc08
Tech tree with 21 research nodes across 5 categories (infrastructure, efficiency, generation, specialization, safety). Research page with category-grouped cards, progress tracking, prerequisite gating. Event engine with 34 events across industry/regulatory/PR/internal/market categories, weighted random firing, cooldowns, expiry, and choice modal with consequence preview. Events auto-expire with default choice. Competitor system with 3 rival AI labs (Prometheus AI, Nexus Labs, Titan Computing), personality-driven milestone progression, and comparison UI. Talent page with department hiring, headcount management, and key hire recruitment from a pool of 10 named characters with special abilities. Data marketplace with 8 purchasable datasets, user data flywheel from subscribers, and data system processing in tick loop. Era transition system checks revenue/capability/reputation thresholds. All new systems integrated into tick processor with notifications. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
332 lines
10 KiB
TypeScript
332 lines
10 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
import type {
|
|
GameState, Era, GameSpeed, GameSettings,
|
|
EconomyState, InfrastructureState, ComputeState,
|
|
ResearchState, ModelsState, MarketState,
|
|
CompetitorState, TalentState, DataState,
|
|
ReputationState, EventState, AchievementState,
|
|
DataCenter, GpuType, GpuInventory, TrainingJob,
|
|
ActiveResearch, EventConsequence, OwnedDataset,
|
|
} from '@ai-tycoon/shared';
|
|
import {
|
|
INITIAL_SETTINGS, SAVE_VERSION,
|
|
INITIAL_ECONOMY, INITIAL_INFRASTRUCTURE, INITIAL_COMPUTE,
|
|
INITIAL_RESEARCH, INITIAL_MODELS, INITIAL_MARKET,
|
|
INITIAL_COMPETITORS, INITIAL_TALENT, INITIAL_DATA,
|
|
INITIAL_REPUTATION, INITIAL_EVENTS, INITIAL_ACHIEVEMENTS,
|
|
GPU_CONFIGS,
|
|
} from '@ai-tycoon/shared';
|
|
import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
|
|
|
|
export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models'
|
|
| 'market' | 'talent' | 'data' | 'competitors' | 'finance' | 'settings';
|
|
|
|
interface UIState {
|
|
activePage: ActivePage;
|
|
notifications: GameNotification[];
|
|
}
|
|
|
|
export interface GameNotification {
|
|
id: string;
|
|
title: string;
|
|
message: string;
|
|
type: 'info' | 'success' | 'warning' | 'danger';
|
|
tick: number;
|
|
read: boolean;
|
|
}
|
|
|
|
interface Actions {
|
|
setActivePage: (page: ActivePage) => void;
|
|
addNotification: (n: Omit<GameNotification, 'id' | 'read'>) => void;
|
|
dismissNotification: (id: string) => void;
|
|
startNewGame: (companyName: string) => void;
|
|
setGameSpeed: (speed: GameSpeed) => void;
|
|
togglePause: () => void;
|
|
setTrainingAllocation: (ratio: number) => void;
|
|
buyGpu: (dataCenterId: string, gpuType: GpuType, count: number) => void;
|
|
buildDataCenter: (name: string, location: DataCenter['location']) => void;
|
|
startTraining: (job: Omit<TrainingJob, 'progressTicks'>) => void;
|
|
deployModel: (modelId: string) => void;
|
|
setProductPricing: (productLineId: string, field: string, value: number) => void;
|
|
toggleProductLine: (productLineId: string) => void;
|
|
startResearch: (research: ActiveResearch) => void;
|
|
resolveEvent: (instanceId: string, choiceIndex: number) => void;
|
|
hireDepartment: (departmentId: string, count: number) => void;
|
|
purchaseDataset: (dataset: OwnedDataset, cost: number) => void;
|
|
updateState: (partial: Partial<GameState>) => void;
|
|
}
|
|
|
|
type Store = GameState & UIState & Actions;
|
|
|
|
const initialGameState: GameState = {
|
|
meta: {
|
|
saveVersion: SAVE_VERSION,
|
|
companyName: '',
|
|
currentEra: 'startup',
|
|
tickCount: 0,
|
|
lastTickTimestamp: Date.now(),
|
|
gameSpeed: 1,
|
|
isPaused: true,
|
|
createdAt: Date.now(),
|
|
totalPlayTime: 0,
|
|
settings: INITIAL_SETTINGS,
|
|
},
|
|
economy: INITIAL_ECONOMY,
|
|
infrastructure: INITIAL_INFRASTRUCTURE,
|
|
compute: INITIAL_COMPUTE,
|
|
research: INITIAL_RESEARCH,
|
|
models: INITIAL_MODELS,
|
|
market: INITIAL_MARKET,
|
|
competitors: INITIAL_COMPETITORS,
|
|
talent: INITIAL_TALENT,
|
|
data: INITIAL_DATA,
|
|
reputation: INITIAL_REPUTATION,
|
|
events: INITIAL_EVENTS,
|
|
achievements: INITIAL_ACHIEVEMENTS,
|
|
};
|
|
|
|
export const useGameStore = create<Store>()(
|
|
persist(
|
|
(set, get) => ({
|
|
...initialGameState,
|
|
activePage: 'dashboard' as ActivePage,
|
|
notifications: [],
|
|
|
|
setActivePage: (page) => set({ activePage: page }),
|
|
|
|
addNotification: (n) => set((s) => ({
|
|
notifications: [
|
|
{ ...n, id: crypto.randomUUID(), read: false },
|
|
...s.notifications.slice(0, 49),
|
|
],
|
|
})),
|
|
|
|
dismissNotification: (id) => set((s) => ({
|
|
notifications: s.notifications.map(n =>
|
|
n.id === id ? { ...n, read: true } : n,
|
|
),
|
|
})),
|
|
|
|
startNewGame: (companyName) => set({
|
|
...initialGameState,
|
|
meta: {
|
|
...initialGameState.meta,
|
|
companyName,
|
|
isPaused: false,
|
|
createdAt: Date.now(),
|
|
lastTickTimestamp: Date.now(),
|
|
},
|
|
competitors: {
|
|
rivals: INITIAL_RIVALS,
|
|
industryBenchmark: 0,
|
|
},
|
|
activePage: 'dashboard',
|
|
notifications: [],
|
|
}),
|
|
|
|
setGameSpeed: (speed) => set((s) => ({
|
|
meta: { ...s.meta, gameSpeed: speed },
|
|
})),
|
|
|
|
togglePause: () => set((s) => ({
|
|
meta: { ...s.meta, isPaused: !s.meta.isPaused },
|
|
})),
|
|
|
|
setTrainingAllocation: (ratio) => set((s) => ({
|
|
compute: { ...s.compute, trainingAllocation: ratio, inferenceAllocation: 1 - ratio },
|
|
})),
|
|
|
|
buyGpu: (dataCenterId, gpuType, count) => set((s) => {
|
|
const price = s.infrastructure.gpuMarketPrices[gpuType] * count;
|
|
if (s.economy.money < price) return s;
|
|
|
|
const dataCenters = s.infrastructure.dataCenters.map(dc => {
|
|
if (dc.id !== dataCenterId) return dc;
|
|
const existingIdx = dc.gpus.findIndex(g => g.type === gpuType);
|
|
let gpus: GpuInventory[];
|
|
if (existingIdx >= 0) {
|
|
gpus = dc.gpus.map((g, i) =>
|
|
i === existingIdx
|
|
? { ...g, count: g.count + count, healthyCount: g.healthyCount + count }
|
|
: g,
|
|
);
|
|
} else {
|
|
gpus = [...dc.gpus, { type: gpuType, count, healthyCount: count, failedCount: 0 }];
|
|
}
|
|
return { ...dc, gpus };
|
|
});
|
|
|
|
return {
|
|
economy: { ...s.economy, money: s.economy.money - price },
|
|
infrastructure: { ...s.infrastructure, dataCenters },
|
|
};
|
|
}),
|
|
|
|
buildDataCenter: (name, location) => set((s) => {
|
|
const buildCost = 10_000;
|
|
if (s.economy.money < buildCost) return s;
|
|
|
|
const dc: DataCenter = {
|
|
id: crypto.randomUUID(),
|
|
name,
|
|
location,
|
|
gpus: [],
|
|
maxCapacity: 100,
|
|
coolingLevel: 0.5,
|
|
redundancyLevel: 0.3,
|
|
currentUptime: 1,
|
|
energyCostPerTick: 0,
|
|
maintenanceCostPerTick: 0,
|
|
};
|
|
|
|
return {
|
|
economy: { ...s.economy, money: s.economy.money - buildCost },
|
|
infrastructure: {
|
|
...s.infrastructure,
|
|
dataCenters: [...s.infrastructure.dataCenters, dc],
|
|
},
|
|
};
|
|
}),
|
|
|
|
startTraining: (job) => set((s) => ({
|
|
models: {
|
|
...s.models,
|
|
activeTraining: { ...job, progressTicks: 0 },
|
|
},
|
|
})),
|
|
|
|
deployModel: (modelId) => set((s) => ({
|
|
models: {
|
|
...s.models,
|
|
trainedModels: s.models.trainedModels.map(m =>
|
|
m.id === modelId ? { ...m, isDeployed: true } : m,
|
|
),
|
|
productLines: s.models.productLines.map(pl => ({
|
|
...pl, modelId, isActive: true,
|
|
})),
|
|
},
|
|
})),
|
|
|
|
setProductPricing: (productLineId, field, value) => set((s) => ({
|
|
models: {
|
|
...s.models,
|
|
productLines: s.models.productLines.map(pl =>
|
|
pl.id === productLineId
|
|
? { ...pl, pricing: { ...pl.pricing, [field]: value } }
|
|
: pl,
|
|
),
|
|
},
|
|
})),
|
|
|
|
toggleProductLine: (productLineId) => set((s) => ({
|
|
models: {
|
|
...s.models,
|
|
productLines: s.models.productLines.map(pl =>
|
|
pl.id === productLineId ? { ...pl, isActive: !pl.isActive } : pl,
|
|
),
|
|
},
|
|
})),
|
|
|
|
startResearch: (research) => set((s) => {
|
|
if (s.research.activeResearch) return s;
|
|
return {
|
|
research: { ...s.research, activeResearch: research },
|
|
};
|
|
}),
|
|
|
|
resolveEvent: (instanceId, choiceIndex) => set((s) => {
|
|
const event = s.events.activeEvents.find(e => e.instanceId === instanceId);
|
|
if (!event) return s;
|
|
|
|
const choice = event.choices[choiceIndex];
|
|
if (!choice) return s;
|
|
|
|
let money = s.economy.money;
|
|
let reputation = { ...s.reputation };
|
|
const consequences = choice.consequences;
|
|
|
|
for (const c of consequences) {
|
|
switch (c.type) {
|
|
case 'money': money += c.value; break;
|
|
case 'reputation': reputation = { ...reputation, score: Math.min(100, Math.max(0, reputation.score + c.value)), publicPerception: Math.min(100, Math.max(0, reputation.publicPerception + c.value)) }; break;
|
|
}
|
|
}
|
|
|
|
return {
|
|
economy: { ...s.economy, money: Math.max(0, money) },
|
|
reputation,
|
|
events: {
|
|
...s.events,
|
|
activeEvents: s.events.activeEvents.filter(e => e.instanceId !== instanceId),
|
|
eventHistory: [
|
|
...s.events.eventHistory,
|
|
{
|
|
eventId: event.eventId,
|
|
instanceId,
|
|
title: event.title,
|
|
category: event.category,
|
|
tick: s.meta.tickCount,
|
|
chosenOptionIndex: choiceIndex,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}),
|
|
|
|
hireDepartment: (departmentId, count) => set((s) => {
|
|
const costPerHire = 2000;
|
|
const totalCost = costPerHire * count;
|
|
if (s.economy.money < totalCost) return s;
|
|
|
|
return {
|
|
economy: { ...s.economy, money: s.economy.money - totalCost },
|
|
talent: {
|
|
...s.talent,
|
|
departments: {
|
|
...s.talent.departments,
|
|
[departmentId]: {
|
|
...s.talent.departments[departmentId as keyof typeof s.talent.departments],
|
|
headcount: s.talent.departments[departmentId as keyof typeof s.talent.departments].headcount + count,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}),
|
|
|
|
purchaseDataset: (dataset, cost) => set((s) => {
|
|
if (s.economy.money < cost) return s;
|
|
return {
|
|
economy: { ...s.economy, money: s.economy.money - cost },
|
|
data: {
|
|
...s.data,
|
|
ownedDatasets: [...s.data.ownedDatasets, dataset],
|
|
totalTrainingTokens: s.data.totalTrainingTokens + dataset.sizeTokens,
|
|
},
|
|
};
|
|
}),
|
|
|
|
updateState: (partial) => set((s) => {
|
|
const newState: Partial<Store> = {};
|
|
for (const key of Object.keys(partial) as (keyof GameState)[]) {
|
|
const value = partial[key];
|
|
const current = s[key];
|
|
if (typeof value === 'object' && value !== null && !Array.isArray(value) && typeof current === 'object' && current !== null) {
|
|
(newState as Record<string, unknown>)[key] = { ...current, ...value };
|
|
} else {
|
|
(newState as Record<string, unknown>)[key] = value;
|
|
}
|
|
}
|
|
return newState;
|
|
}),
|
|
}),
|
|
{
|
|
name: 'ai-tycoon-save',
|
|
partialize: (state) => {
|
|
const { activePage, notifications, ...rest } = state;
|
|
return rest;
|
|
},
|
|
},
|
|
),
|
|
);
|