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
+147
View File
@@ -0,0 +1,147 @@
import { FlaskConical, Lock, Check, Play } from 'lucide-react';
import { useGameStore } from '@/store';
import { formatDuration, formatPercent, formatNumber } from '@ai-tycoon/shared';
import { TECH_TREE, getAvailableResearch } from '@ai-tycoon/game-engine';
import type { ResearchNode } from '@ai-tycoon/shared';
const CATEGORY_COLORS: Record<string, string> = {
generation: 'border-purple-500/50 bg-purple-500/10',
efficiency: 'border-blue-500/50 bg-blue-500/10',
safety: 'border-green-500/50 bg-green-500/10',
specialization: 'border-orange-500/50 bg-orange-500/10',
infrastructure: 'border-cyan-500/50 bg-cyan-500/10',
};
const CATEGORY_LABELS: Record<string, string> = {
generation: 'Model Architecture',
efficiency: 'Efficiency',
safety: 'Safety & Alignment',
specialization: 'Specialization',
infrastructure: 'Infrastructure',
};
export function ResearchPage() {
const completedResearch = useGameStore((s) => s.research.completedResearch);
const activeResearch = useGameStore((s) => s.research.activeResearch);
const researchPoints = useGameStore((s) => s.research.researchPoints);
const startResearch = useGameStore((s) => s.startResearch);
const era = useGameStore((s) => s.meta.currentEra);
const state = useGameStore.getState();
const available = getAvailableResearch(state);
const availableIds = new Set(available.map(n => n.id));
const handleStart = (node: ResearchNode) => {
if (activeResearch) return;
startResearch({
researchId: node.id,
progressTicks: 0,
totalTicks: node.cost.ticks,
allocatedResearchers: state.talent.departments.research.headcount,
allocatedCompute: node.cost.compute,
});
};
const categories = [...new Set(TECH_TREE.map(n => n.category))];
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">Research & Development</h2>
<div className="flex items-center gap-4 text-sm">
<div className="text-surface-400">
Completed: <span className="text-surface-100 font-mono">{completedResearch.length}/{TECH_TREE.length}</span>
</div>
<div className="text-surface-400">
Research Points: <span className="text-accent-light font-mono">{researchPoints}</span>
</div>
</div>
</div>
{activeResearch && (
<div className="bg-surface-900 border border-accent/30 rounded-xl p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<FlaskConical size={16} className="text-accent" />
<span className="font-medium">
{TECH_TREE.find(n => n.id === activeResearch.researchId)?.name ?? activeResearch.researchId}
</span>
</div>
<span className="text-sm text-surface-400">
{formatPercent(activeResearch.progressTicks / activeResearch.totalTicks)} complete
</span>
</div>
<div className="h-2 bg-surface-800 rounded-full overflow-hidden">
<div
className="h-full bg-accent rounded-full transition-all duration-300"
style={{ width: `${(activeResearch.progressTicks / activeResearch.totalTicks) * 100}%` }}
/>
</div>
<div className="text-xs text-surface-500 mt-1">
ETA: {formatDuration(Math.ceil(activeResearch.totalTicks - activeResearch.progressTicks))}
</div>
</div>
)}
{categories.map(category => {
const nodes = TECH_TREE.filter(n => n.category === category);
return (
<div key={category}>
<h3 className="text-sm font-semibold text-surface-400 uppercase tracking-wider mb-3">
{CATEGORY_LABELS[category] ?? category}
</h3>
<div className="grid grid-cols-2 xl:grid-cols-3 gap-3">
{nodes.map(node => {
const isCompleted = completedResearch.includes(node.id);
const isActive = activeResearch?.researchId === node.id;
const isAvailable = availableIds.has(node.id);
const isLocked = !isCompleted && !isActive && !isAvailable;
return (
<div
key={node.id}
className={`rounded-xl border p-4 transition-all ${
isCompleted ? 'border-success/50 bg-success/5 opacity-70' :
isActive ? 'border-accent/50 bg-accent/5' :
isAvailable ? `${CATEGORY_COLORS[category]} hover:brightness-110` :
'border-surface-700 bg-surface-900 opacity-50'
}`}
>
<div className="flex items-start justify-between mb-2">
<h4 className="font-medium text-sm">{node.name}</h4>
{isCompleted && <Check size={16} className="text-success flex-shrink-0" />}
{isLocked && <Lock size={14} className="text-surface-500 flex-shrink-0" />}
</div>
<p className="text-xs text-surface-400 mb-3">{node.description}</p>
<div className="flex items-center justify-between">
<div className="text-xs text-surface-500">
{formatDuration(node.cost.ticks)} · {formatNumber(node.cost.compute)} compute
{node.cost.researchPoints > 0 && ` · ${node.cost.researchPoints} RP`}
</div>
{isAvailable && !activeResearch && (
<button
onClick={() => handleStart(node)}
className="flex items-center gap-1 bg-accent hover:bg-accent-dark text-white rounded px-2 py-1 text-xs"
>
<Play size={12} />
Start
</button>
)}
</div>
{node.prerequisites.length > 0 && isLocked && (
<div className="text-xs text-surface-600 mt-2">
Requires: {node.prerequisites.map(p =>
TECH_TREE.find(n => n.id === p)?.name ?? p
).join(', ')}
</div>
)}
</div>
);
})}
</div>
</div>
);
})}
</div>
);
}