Add Week 2 depth systems: research, events, competitors, talent, data

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>
This commit is contained in:
2026-04-24 17:30:24 -04:00
parent d1d3eb4bf2
commit 8c9555bc08
19 changed files with 3166 additions and 21 deletions
@@ -0,0 +1,76 @@
import type { Competitor } from '@ai-tycoon/shared';
/**
* Initial rival AI companies that compete with the player from the start.
* Names are fictional parodies -- any resemblance to real companies is purely satirical.
*/
export const INITIAL_RIVALS: Competitor[] = [
// ── Safety-first lab (Anthropic parody) ──────────────────────────────
{
id: 'competitor_prometheus',
name: 'Prometheus AI',
archetype: 'safety-first',
personality: {
aggression: 0.2,
safetyFocus: 0.95,
openSourceTendency: 0.3,
marketingFocus: 0.25,
researchFocus: 0.85,
riskTolerance: 0.15,
},
status: 'active',
estimatedCapability: 18,
estimatedRevenue: 50,
estimatedUsers: 1_200,
reputation: 70,
latestModelName: 'Aegis-1',
completedMilestones: [],
nextMilestoneAtTick: 300,
},
// ── Move-fast startup (xAI / Musk parody) ────────────────────────────
{
id: 'competitor_nexus',
name: 'Nexus Labs',
archetype: 'move-fast',
personality: {
aggression: 0.85,
safetyFocus: 0.15,
openSourceTendency: 0.4,
marketingFocus: 0.7,
researchFocus: 0.6,
riskTolerance: 0.9,
},
status: 'active',
estimatedCapability: 14,
estimatedRevenue: 30,
estimatedUsers: 3_500,
reputation: 45,
latestModelName: 'Blitz-0.9',
completedMilestones: [],
nextMilestoneAtTick: 300,
},
// ── Big-tech giant (Google parody) ────────────────────────────────────
{
id: 'competitor_titan',
name: 'Titan Computing',
archetype: 'big-tech',
personality: {
aggression: 0.5,
safetyFocus: 0.5,
openSourceTendency: 0.35,
marketingFocus: 0.55,
researchFocus: 0.65,
riskTolerance: 0.4,
},
status: 'active',
estimatedCapability: 22,
estimatedRevenue: 200,
estimatedUsers: 15_000,
reputation: 60,
latestModelName: 'Colossus 2.0',
completedMilestones: [],
nextMilestoneAtTick: 300,
},
];
File diff suppressed because it is too large Load Diff
+169
View File
@@ -0,0 +1,169 @@
import type { DepartmentId } from '@ai-tycoon/shared';
/**
* A recruitable key hire as it appears in the available pool.
* `hiredAtTick` is omitted because the hire hasn't been recruited yet.
*/
export interface KeyHireTemplate {
id: string;
name: string;
department: DepartmentId;
specialAbility: string;
description: string;
requiredEra: string;
effects: { type: string; value: number }[];
salary: number;
loyalty: number;
}
/**
* Master pool of key hires the player can recruit throughout the game.
* Salary is per-tick. Effect values are fractional multipliers (0.20 = +20%).
*/
export const KEY_HIRE_POOL: KeyHireTemplate[] = [
// ── Research ──────────────────────────────────────────────────────────
{
id: 'hire_elena_vasquez',
name: 'Dr. Elena Vasquez',
department: 'research',
specialAbility: 'Scaling Law Whisperer',
description:
'Former theoretical physicist who discovered three novel scaling laws. Her intuition for optimal compute allocation is uncanny.',
requiredEra: 'foundation',
effects: [{ type: 'research_speed', value: 0.2 }],
salary: 3,
loyalty: 0.7,
},
{
id: 'hire_raj_patel',
name: 'Dr. Raj Patel',
department: 'research',
specialAbility: 'Alignment Prodigy',
description:
'Published the seminal paper on constitutional training at age 24. Governments call him before passing AI regulation.',
requiredEra: 'growth',
effects: [
{ type: 'research_speed', value: 0.1 },
{ type: 'reputation', value: 0.15 },
],
salary: 4,
loyalty: 0.85,
},
// ── Engineering ───────────────────────────────────────────────────────
{
id: 'hire_marcus_chen',
name: 'Marcus Chen',
department: 'engineering',
specialAbility: 'Infrastructure Guru',
description:
'Built the internal orchestration layer at three hyperscalers. Can squeeze 40% more throughput out of any GPU cluster before lunch.',
requiredEra: 'foundation',
effects: [{ type: 'cost_reduction', value: 0.15 }],
salary: 3,
loyalty: 0.6,
},
{
id: 'hire_yuki_tanaka',
name: 'Yuki Tanaka',
department: 'engineering',
specialAbility: 'Latency Assassin',
description:
'Obsessed with shaving milliseconds. Once rewrote an entire inference stack on a red-eye flight and deployed it before landing.',
requiredEra: 'foundation',
effects: [
{ type: 'training_speed', value: 0.15 },
{ type: 'capability_boost', value: 0.05 },
],
salary: 2,
loyalty: 0.55,
},
{
id: 'hire_omar_hassan',
name: 'Omar Hassan',
department: 'engineering',
specialAbility: 'Compiler Whisperer',
description:
'Wrote a custom ML compiler that rival labs tried to acqui-hire him just to get access to. His kernel optimizations are legendary.',
requiredEra: 'growth',
effects: [
{ type: 'training_speed', value: 0.2 },
{ type: 'cost_reduction', value: 0.1 },
],
salary: 4,
loyalty: 0.65,
},
// ── Operations ────────────────────────────────────────────────────────
{
id: 'hire_diana_okafor',
name: 'Diana Okafor',
department: 'operations',
specialAbility: 'Talent Magnet',
description:
'Her recruiting network spans every top CS program on the planet. Candidates accept offers just because she asked.',
requiredEra: 'foundation',
effects: [{ type: 'hiring_speed', value: 0.25 }],
salary: 2,
loyalty: 0.75,
},
{
id: 'hire_liam_frost',
name: 'Liam Frost',
department: 'operations',
specialAbility: 'Supply Chain Sorcerer',
description:
'Secured GPU allocations during the Great Chip Shortage when everyone else was on a two-year waitlist. Knows every fab manager by first name.',
requiredEra: 'growth',
effects: [
{ type: 'cost_reduction', value: 0.1 },
{ type: 'capability_boost', value: 0.1 },
],
salary: 3,
loyalty: 0.7,
},
// ── Sales ─────────────────────────────────────────────────────────────
{
id: 'hire_sarah_kim',
name: 'Sarah Kim',
department: 'sales',
specialAbility: 'Enterprise Closer',
description:
'Closed the largest SaaS deal in history before turning 30. Fortune 500 CIOs have her on speed dial.',
requiredEra: 'foundation',
effects: [{ type: 'revenue_boost', value: 0.25 }],
salary: 3,
loyalty: 0.5,
},
{
id: 'hire_alex_reeves',
name: 'Alex Reeves',
department: 'sales',
specialAbility: 'Developer Evangelist',
description:
'Their conference talks go viral every time. Open-source communities worship the ground they commit on.',
requiredEra: 'foundation',
effects: [
{ type: 'reputation', value: 0.2 },
{ type: 'revenue_boost', value: 0.1 },
],
salary: 2,
loyalty: 0.6,
},
{
id: 'hire_isabella_marquez',
name: 'Isabella Marquez',
department: 'sales',
specialAbility: 'Government Liaison',
description:
'Former deputy tech advisor to three heads of state. Opens doors to public-sector contracts no one else can touch.',
requiredEra: 'growth',
effects: [
{ type: 'revenue_boost', value: 0.15 },
{ type: 'reputation', value: 0.1 },
],
salary: 5,
loyalty: 0.8,
},
];
+233
View File
@@ -0,0 +1,233 @@
import type { ResearchNode } from '@ai-tycoon/shared';
export const TECH_TREE: ResearchNode[] = [
// === COMPUTE / INFRASTRUCTURE ===
{
id: 'advanced-cooling',
name: 'Advanced Cooling',
description: 'Liquid cooling systems reduce energy costs by 25%.',
era: 'startup',
category: 'infrastructure',
prerequisites: [],
cost: { researchPoints: 0, compute: 5, ticks: 60 },
effects: [{ type: 'cost_reduction', target: 'energy', value: 0.25 }],
},
{
id: 'redundancy-protocols',
name: 'Redundancy Protocols',
description: 'Fault-tolerant architectures cut GPU failure rates in half.',
era: 'startup',
category: 'infrastructure',
prerequisites: [],
cost: { researchPoints: 0, compute: 5, ticks: 60 },
effects: [{ type: 'cost_reduction', target: 'failure_rate', value: 0.5 }],
},
{
id: 'advanced-gpu-arch',
name: 'Advanced GPU Architecture',
description: 'Unlocks procurement of NVIDIA A100 datacenter GPUs.',
era: 'startup',
category: 'infrastructure',
prerequisites: [],
cost: { researchPoints: 0, compute: 10, ticks: 90 },
effects: [{ type: 'unlock_gpu', target: 'a100', value: 1 }],
},
{
id: 'next-gen-gpu',
name: 'Next-Gen GPU Architecture',
description: 'Unlocks procurement of NVIDIA H100 GPUs.',
era: 'scaleup',
category: 'infrastructure',
prerequisites: ['advanced-gpu-arch'],
cost: { researchPoints: 2, compute: 40, ticks: 240 },
effects: [{ type: 'unlock_gpu', target: 'h100', value: 1 }],
},
{
id: 'frontier-compute',
name: 'Frontier Compute',
description: 'Unlocks procurement of NVIDIA B200 GPUs.',
era: 'bigtech',
category: 'infrastructure',
prerequisites: ['next-gen-gpu'],
cost: { researchPoints: 5, compute: 200, ticks: 480 },
effects: [{ type: 'unlock_gpu', target: 'b200', value: 1 }],
},
{
id: 'custom-silicon',
name: 'Custom Silicon Design',
description: 'Design and fabricate custom AI ASICs for maximum efficiency.',
era: 'agi',
category: 'infrastructure',
prerequisites: ['frontier-compute'],
cost: { researchPoints: 10, compute: 500, ticks: 900 },
effects: [{ type: 'unlock_gpu', target: 'custom', value: 1 }],
},
{
id: 'distributed-training',
name: 'Distributed Training',
description: 'Train models across multiple data centers simultaneously. +20% training speed.',
era: 'scaleup',
category: 'infrastructure',
prerequisites: ['advanced-gpu-arch'],
cost: { researchPoints: 2, compute: 30, ticks: 180 },
effects: [{ type: 'efficiency_boost', target: 'training_speed', value: 0.2 }],
},
// === EFFICIENCY ===
{
id: 'quantization',
name: 'Quantization Research',
description: 'INT8/INT4 inference reduces compute costs. +15% inference efficiency.',
era: 'startup',
category: 'efficiency',
prerequisites: [],
cost: { researchPoints: 0, compute: 8, ticks: 75 },
effects: [{ type: 'efficiency_boost', target: 'inference', value: 0.15 }],
},
{
id: 'distillation',
name: 'Knowledge Distillation',
description: 'Train smaller models that retain teacher quality. +10% model capability.',
era: 'scaleup',
category: 'efficiency',
prerequisites: ['quantization'],
cost: { researchPoints: 2, compute: 25, ticks: 180 },
effects: [{ type: 'capability_boost', target: 'all', value: 5 }],
},
{
id: 'inference-optimization',
name: 'Inference Optimization',
description: 'Optimized kernels and batching. +30% tokens per FLOP.',
era: 'scaleup',
category: 'efficiency',
prerequisites: ['quantization'],
cost: { researchPoints: 2, compute: 20, ticks: 150 },
effects: [{ type: 'efficiency_boost', target: 'tokens_per_flop', value: 0.3 }],
},
// === MODEL CAPABILITIES ===
{
id: 'transformer-v2',
name: 'Advanced Architectures',
description: 'Mixture-of-experts and improved attention. +10 base capability.',
era: 'startup',
category: 'generation',
prerequisites: [],
cost: { researchPoints: 0, compute: 10, ticks: 90 },
effects: [{ type: 'capability_boost', target: 'all', value: 10 }],
},
{
id: 'reasoning-enhancement',
name: 'Chain-of-Thought Training',
description: 'Enhanced reasoning through structured thinking. +15 reasoning.',
era: 'scaleup',
category: 'specialization',
branch: 'reasoning',
prerequisites: ['transformer-v2'],
cost: { researchPoints: 3, compute: 40, ticks: 240 },
effects: [{ type: 'capability_boost', target: 'reasoning', value: 15 }],
},
{
id: 'code-generation',
name: 'Code Generation',
description: 'Specialized code understanding and generation. +15 coding.',
era: 'scaleup',
category: 'specialization',
branch: 'coding',
prerequisites: ['transformer-v2'],
cost: { researchPoints: 3, compute: 35, ticks: 210 },
effects: [{ type: 'capability_boost', target: 'coding', value: 15 }],
},
{
id: 'creative-systems',
name: 'Creative Expression',
description: 'Enhanced creative writing and artistic understanding. +15 creative.',
era: 'scaleup',
category: 'specialization',
branch: 'creative',
prerequisites: ['transformer-v2'],
cost: { researchPoints: 3, compute: 30, ticks: 210 },
effects: [{ type: 'capability_boost', target: 'creative', value: 15 }],
},
{
id: 'multimodal-fusion',
name: 'Multi-Modal Fusion',
description: 'Vision-language integration for image understanding. +20 multimodal. Unlocks Image product.',
era: 'scaleup',
category: 'specialization',
branch: 'multimodal',
prerequisites: ['transformer-v2'],
cost: { researchPoints: 4, compute: 50, ticks: 300 },
effects: [
{ type: 'capability_boost', target: 'multimodal', value: 20 },
{ type: 'unlock_product_line', target: 'image', value: 1 },
],
},
{
id: 'agentic-architecture',
name: 'Agentic Architecture',
description: 'Tool use, planning, and autonomous execution. +20 agents. Unlocks Agents product.',
era: 'bigtech',
category: 'specialization',
branch: 'agents',
prerequisites: ['reasoning-enhancement', 'code-generation'],
cost: { researchPoints: 6, compute: 100, ticks: 480 },
effects: [
{ type: 'capability_boost', target: 'agents', value: 20 },
{ type: 'unlock_product_line', target: 'agents', value: 1 },
],
},
// === SAFETY ===
{
id: 'alignment-research',
name: 'Alignment Research',
description: 'RLHF and value alignment techniques. +10 safety, +5 reputation.',
era: 'startup',
category: 'safety',
prerequisites: [],
cost: { researchPoints: 0, compute: 8, ticks: 90 },
effects: [
{ type: 'safety_boost', target: 'models', value: 10 },
{ type: 'capability_boost', target: 'reputation', value: 5 },
],
},
{
id: 'interpretability',
name: 'Interpretability',
description: 'Understand model reasoning and detect failure modes. +10 safety, +5 reputation.',
era: 'scaleup',
category: 'safety',
prerequisites: ['alignment-research'],
cost: { researchPoints: 3, compute: 40, ticks: 240 },
effects: [
{ type: 'safety_boost', target: 'models', value: 10 },
{ type: 'capability_boost', target: 'reputation', value: 5 },
],
},
{
id: 'constitutional-ai',
name: 'Constitutional AI',
description: 'Self-supervised alignment at scale. +15 safety, +10 reputation.',
era: 'bigtech',
category: 'safety',
prerequisites: ['interpretability'],
cost: { researchPoints: 5, compute: 80, ticks: 420 },
effects: [
{ type: 'safety_boost', target: 'models', value: 15 },
{ type: 'capability_boost', target: 'reputation', value: 10 },
],
},
// === DATA ===
{
id: 'data-pipeline',
name: 'Data Pipeline Optimization',
description: 'Automated data cleaning and deduplication. +20% data quality.',
era: 'startup',
category: 'efficiency',
prerequisites: [],
cost: { researchPoints: 0, compute: 5, ticks: 60 },
effects: [{ type: 'efficiency_boost', target: 'data_quality', value: 0.2 }],
},
];
+6 -1
View File
@@ -1,3 +1,8 @@
export { GameEngine } from './engine';
export { processTick } from './tick';
export { processTick, setEventDefinitions } from './tick';
export type { TickNotification } from './tick';
export { getAvailableResearch, getResearchNode } from './systems/researchSystem';
export { TECH_TREE } from './data/techTree';
export { INITIAL_RIVALS } from './data/competitors';
export { KEY_HIRE_POOL } from './data/keyHires';
export { EVENT_DEFINITIONS } from './data/events';
@@ -0,0 +1,51 @@
import type { GameState, CompetitorState } from '@ai-tycoon/shared';
export function processCompetitors(state: GameState): CompetitorState {
const tick = state.meta.tickCount;
const rivals = state.competitors.rivals.map(rival => {
if (rival.status !== 'active') return rival;
if (tick < rival.nextMilestoneAtTick) return rival;
const { personality } = rival;
const capGrowth = (2 + personality.researchFocus * 5 + personality.riskTolerance * 3) *
(1 + tick * 0.00005);
const revenueGrowth = rival.estimatedRevenue * (0.02 + personality.marketingFocus * 0.03);
const userGrowth = rival.estimatedUsers * (0.01 + personality.marketingFocus * 0.02);
const newCapability = Math.min(95, rival.estimatedCapability + capGrowth);
const newRevenue = rival.estimatedRevenue + revenueGrowth + 50;
const newUsers = rival.estimatedUsers + userGrowth + 100;
const repChange = personality.safetyFocus > 0.6
? 1
: personality.riskTolerance > 0.7 ? -1 : 0;
const modelNames = [
'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',
'Nova', 'Quantum', 'Nexus', 'Apex', 'Zenith',
];
const modelIdx = Math.floor(newCapability / 10);
const latestModelName = `${rival.name.split(' ')[0]}-${modelNames[Math.min(modelIdx, modelNames.length - 1)]}`;
const milestoneInterval = 200 + Math.floor(Math.random() * 200);
return {
...rival,
estimatedCapability: newCapability,
estimatedRevenue: newRevenue,
estimatedUsers: Math.floor(newUsers),
reputation: Math.min(100, Math.max(0, rival.reputation + repChange)),
latestModelName,
nextMilestoneAtTick: tick + milestoneInterval,
};
});
const allCaps = [
...rivals.filter(r => r.status === 'active').map(r => r.estimatedCapability),
state.models.trainedModels.reduce((best, m) => Math.max(best, m.benchmarkScore), 0),
];
const industryBenchmark = allCaps.length > 0 ? Math.max(...allCaps) : 0;
return { rivals, industryBenchmark };
}
@@ -0,0 +1,17 @@
import type { GameState, DataState } from '@ai-tycoon/shared';
export function processData(state: GameState): DataState {
const subscribers = state.market.consumers.totalSubscribers;
const userDataRate = subscribers * 0.5;
const partnershipTokens = state.data.partnerships.reduce((sum, p) => sum + p.tokensPerTick, 0);
const newTokens = userDataRate + partnershipTokens;
const totalTrainingTokens = state.data.totalTrainingTokens + newTokens;
return {
...state.data,
userDataGenerationRate: userDataRate,
totalTrainingTokens,
};
}
@@ -0,0 +1,27 @@
import type { GameState, Era } from '@ai-tycoon/shared';
import { ERA_THRESHOLDS } from '@ai-tycoon/shared';
export function checkEraTransition(state: GameState): Era | null {
const current = state.meta.currentEra;
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
const currentIdx = eraOrder.indexOf(current);
const nextEra = eraOrder[currentIdx + 1];
if (!nextEra) return null;
const thresholds = ERA_THRESHOLDS[nextEra as keyof typeof ERA_THRESHOLDS];
if (!thresholds) return null;
const bestModel = state.models.trainedModels.reduce(
(best, m) => Math.max(best, m.benchmarkScore), 0,
);
if (
state.economy.totalRevenue >= thresholds.revenue &&
bestModel >= thresholds.capability &&
state.reputation.score >= thresholds.reputation
) {
return nextEra;
}
return null;
}
@@ -0,0 +1,117 @@
import type { GameState, EventState, ActiveEvent, EventDefinition, EventCondition } from '@ai-tycoon/shared';
export interface EventTickResult {
events: EventState;
newEvents: ActiveEvent[];
}
export function processEvents(
state: GameState,
definitions: EventDefinition[],
): EventTickResult {
const tick = state.meta.tickCount;
const events = { ...state.events };
const newEvents: ActiveEvent[] = [];
// Remove expired events (auto-choose default)
const stillActive: ActiveEvent[] = [];
for (const event of events.activeEvents) {
if (tick >= event.expiresAtTick) {
events.eventHistory = [
...events.eventHistory,
{
eventId: event.eventId,
instanceId: event.instanceId,
title: event.title,
category: event.category,
tick,
chosenOptionIndex: event.defaultChoiceIndex,
},
];
} else {
stillActive.push(event);
}
}
events.activeEvents = stillActive;
if (events.eventHistory.length > 50) {
events.eventHistory = events.eventHistory.slice(-50);
}
// Only try to fire a new event every 30 ticks, and max 1 active at a time
if (tick % 30 !== 0 || events.activeEvents.length > 0) {
return { events, newEvents };
}
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'];
const currentEraIdx = eraOrder.indexOf(state.meta.currentEra);
const eligible = definitions.filter(def => {
if (!def.eras.some(e => eraOrder.indexOf(e) <= currentEraIdx)) return false;
const occ = events.eventOccurrences[def.id] ?? 0;
if (occ >= def.maxOccurrences) return false;
const cooldownEnd = events.eventCooldowns[def.id] ?? 0;
if (tick < cooldownEnd) return false;
if (def.prerequisites.some(p => !state.research.completedResearch.includes(p))) return false;
if (!def.conditions.every(c => evaluateCondition(state, c))) return false;
return true;
});
if (eligible.length === 0) return { events, newEvents };
const totalWeight = eligible.reduce((s, d) => s + d.weight, 0);
let roll = Math.random() * totalWeight;
let chosen: EventDefinition | null = null;
for (const def of eligible) {
roll -= def.weight;
if (roll <= 0) { chosen = def; break; }
}
if (!chosen) return { events, newEvents };
// Only fire with 30% probability per check to space events out
if (Math.random() > 0.3) return { events, newEvents };
const activeEvent: ActiveEvent = {
eventId: chosen.id,
instanceId: crypto.randomUUID(),
triggeredAtTick: tick,
expiresAtTick: tick + chosen.expiryTicks,
title: chosen.title,
description: chosen.descriptionTemplate,
category: chosen.category,
choices: chosen.choices,
defaultChoiceIndex: chosen.defaultChoiceIndex,
};
events.activeEvents = [...events.activeEvents, activeEvent];
events.eventCooldowns = { ...events.eventCooldowns, [chosen.id]: tick + chosen.cooldownTicks };
events.eventOccurrences = {
...events.eventOccurrences,
[chosen.id]: (events.eventOccurrences[chosen.id] ?? 0) + 1,
};
newEvents.push(activeEvent);
return { events, newEvents };
}
function evaluateCondition(state: GameState, condition: EventCondition): boolean {
const value = getNestedValue(state, condition.field);
if (value === undefined) return false;
switch (condition.operator) {
case 'gt': return value > condition.value;
case 'lt': return value < condition.value;
case 'gte': return value >= condition.value;
case 'lte': return value <= condition.value;
case 'eq': return value === condition.value;
}
}
function getNestedValue(obj: object, path: string): number | undefined {
const parts = path.split('.');
let current: unknown = obj;
for (const part of parts) {
if (current == null || typeof current !== 'object') return undefined;
current = (current as Record<string, unknown>)[part];
}
return typeof current === 'number' ? current : undefined;
}
@@ -1,8 +1,14 @@
import type { GameState, ResearchState, ComputeState } from '@ai-tycoon/shared';
import { TECH_TREE } from '../data/techTree';
export function processResearch(state: GameState, compute: ComputeState): ResearchState {
export interface ResearchTickResult {
research: ResearchState;
researchCompleted: string | null;
}
export function processResearch(state: GameState, compute: ComputeState): ResearchTickResult {
const active = state.research.activeResearch;
if (!active) return state.research;
if (!active) return { research: state.research, researchCompleted: null };
const researcherBoost = state.talent.departments.research.headcount *
state.talent.departments.research.effectiveness;
@@ -12,18 +18,38 @@ export function processResearch(state: GameState, compute: ComputeState): Resear
if (newProgress >= active.totalTicks) {
return {
...state.research,
completedResearch: [...state.research.completedResearch, active.researchId],
activeResearch: null,
researchPoints: state.research.researchPoints + 1,
research: {
...state.research,
completedResearch: [...state.research.completedResearch, active.researchId],
activeResearch: null,
researchPoints: state.research.researchPoints + 1,
},
researchCompleted: active.researchId,
};
}
return {
...state.research,
activeResearch: {
...active,
progressTicks: newProgress,
research: {
...state.research,
activeResearch: { ...active, progressTicks: newProgress },
},
researchCompleted: null,
};
}
export function getAvailableResearch(state: GameState): typeof TECH_TREE {
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'];
const currentEraIdx = eraOrder.indexOf(state.meta.currentEra);
return TECH_TREE.filter(node => {
if (state.research.completedResearch.includes(node.id)) return false;
if (state.research.activeResearch?.researchId === node.id) return false;
if (eraOrder.indexOf(node.era) > currentEraIdx) return false;
if (node.prerequisites.some(p => !state.research.completedResearch.includes(p))) return false;
return true;
});
}
export function getResearchNode(id: string) {
return TECH_TREE.find(n => n.id === id);
}
+57 -9
View File
@@ -1,4 +1,4 @@
import type { GameState } from '@ai-tycoon/shared';
import type { GameState, EventDefinition } from '@ai-tycoon/shared';
import { processEconomy } from './systems/economySystem';
import { processInfrastructure } from './systems/infrastructureSystem';
import { processCompute } from './systems/computeSystem';
@@ -7,6 +7,10 @@ import { processModels } from './systems/modelSystem';
import { processMarket } from './systems/marketSystem';
import { processReputation } from './systems/reputationSystem';
import { processTalent } from './systems/talentSystem';
import { processEvents } from './systems/eventSystem';
import { processCompetitors } from './systems/competitorSystem';
import { processData } from './systems/dataSystem';
import { checkEraTransition } from './systems/eraSystem';
export interface TickResult {
state: Partial<GameState>;
@@ -19,6 +23,12 @@ export interface TickNotification {
type: 'info' | 'success' | 'warning' | 'danger';
}
let cachedEventDefs: EventDefinition[] | null = null;
export function setEventDefinitions(defs: EventDefinition[]) {
cachedEventDefs = defs;
}
export function processTick(state: GameState): Partial<GameState> {
const notifications: TickNotification[] = [];
@@ -46,27 +56,65 @@ export function processTick(state: GameState): Partial<GameState> {
const talent = processTalent(stateWithModels);
const stateWithTalent = { ...stateWithModels, talent };
const research = processResearch(stateWithTalent, compute);
const researchResult = processResearch(stateWithTalent, compute);
if (researchResult.researchCompleted) {
notifications.push({
title: 'Research Complete',
message: `${researchResult.researchCompleted} has been unlocked!`,
type: 'success',
});
}
const reputation = processReputation(stateWithTalent);
const economy = processEconomy(stateWithTalent, market, infrastructure);
const data = processData(stateWithTalent);
const competitors = processCompetitors(stateWithTalent);
const eventResult = cachedEventDefs
? processEvents(stateWithTalent, cachedEventDefs)
: { events: state.events, newEvents: [] };
for (const evt of eventResult.newEvents) {
notifications.push({
title: evt.title,
message: evt.description,
type: evt.category === 'regulatory' ? 'warning' : 'info',
});
}
const tickCount = state.meta.tickCount + 1;
let meta = {
...state.meta,
tickCount,
lastTickTimestamp: Date.now(),
totalPlayTime: state.meta.totalPlayTime + 1,
};
const newEra = checkEraTransition({ ...stateWithTalent, economy, reputation, research: researchResult.research });
if (newEra) {
meta = { ...meta, currentEra: newEra };
notifications.push({
title: 'Era Transition!',
message: `Your company has entered the ${newEra === 'scaleup' ? 'Scale-up' : newEra === 'bigtech' ? 'Big Tech' : 'AGI'} era!`,
type: 'success',
});
}
const result: Partial<GameState> = {
meta: {
...state.meta,
tickCount,
lastTickTimestamp: Date.now(),
totalPlayTime: state.meta.totalPlayTime + 1,
},
meta,
economy,
infrastructure,
compute,
research,
research: researchResult.research,
models: modelResult.modelsState,
market: market.marketState,
talent,
reputation,
data,
competitors,
events: eventResult.events,
};
(result as Record<string, unknown>)['_notifications'] = notifications;