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
+134
View File
@@ -0,0 +1,134 @@
import { Swords, TrendingUp, Shield, Users, Brain } from 'lucide-react';
import { useGameStore } from '@/store';
import { formatMoney, formatNumber } from '@ai-tycoon/shared';
const ARCHETYPE_LABELS: Record<string, string> = {
'safety-first': 'Safety-First Lab',
'move-fast': 'Move-Fast Startup',
'big-tech': 'Big Tech Giant',
'open-source': 'Open Source Maximalist',
'stealth-startup': 'Stealth Startup',
};
const ARCHETYPE_COLORS: Record<string, string> = {
'safety-first': 'text-green-400',
'move-fast': 'text-red-400',
'big-tech': 'text-blue-400',
'open-source': 'text-orange-400',
'stealth-startup': 'text-purple-400',
};
export function CompetitorsPage() {
const rivals = useGameStore((s) => s.competitors.rivals);
const industryBenchmark = useGameStore((s) => s.competitors.industryBenchmark);
const playerBest = useGameStore((s) =>
s.models.trainedModels.reduce((best, m) => Math.max(best, m.benchmarkScore), 0),
);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">Competitors</h2>
<div className="text-sm text-surface-400">
Industry Benchmark: <span className="text-surface-100 font-mono">{industryBenchmark.toFixed(1)}</span>
</div>
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<h3 className="text-sm font-medium text-surface-400 mb-3">Your Position</h3>
<div className="flex items-center gap-6">
<div>
<div className="text-xs text-surface-500">Best Model</div>
<div className="text-lg font-mono font-bold">{playerBest.toFixed(1)}/100</div>
</div>
<div className="flex-1">
<div className="h-3 bg-surface-800 rounded-full overflow-hidden relative">
<div
className="absolute h-full bg-accent rounded-full transition-all"
style={{ width: `${playerBest}%` }}
/>
{rivals.filter(r => r.status === 'active').map(rival => (
<div
key={rival.id}
className="absolute top-0 h-full w-0.5 bg-danger"
style={{ left: `${rival.estimatedCapability}%` }}
title={`${rival.name}: ${rival.estimatedCapability.toFixed(1)}`}
/>
))}
</div>
</div>
</div>
</div>
<div className="space-y-4">
{rivals.map(rival => (
<div
key={rival.id}
className={`bg-surface-900 border rounded-xl p-4 ${
rival.status === 'active' ? 'border-surface-700' : 'border-surface-800 opacity-50'
}`}
>
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="font-semibold text-lg">{rival.name}</h3>
<span className={`text-xs ${ARCHETYPE_COLORS[rival.archetype]}`}>
{ARCHETYPE_LABELS[rival.archetype]}
</span>
</div>
<span className={`text-xs px-2 py-1 rounded-full ${
rival.status === 'active' ? 'bg-success/20 text-success' :
rival.status === 'acquired' ? 'bg-blue-500/20 text-blue-400' :
'bg-surface-700 text-surface-400'
}`}>
{rival.status}
</span>
</div>
<div className="grid grid-cols-4 gap-4">
<Stat icon={Brain} label="Capability" value={rival.estimatedCapability.toFixed(1)} sub={rival.latestModelName} />
<Stat icon={TrendingUp} label="Est. Revenue" value={formatMoney(rival.estimatedRevenue)} sub="/tick" />
<Stat icon={Users} label="Est. Users" value={formatNumber(rival.estimatedUsers)} />
<Stat icon={Shield} label="Reputation" value={`${rival.reputation}/100`} />
</div>
<div className="mt-3 grid grid-cols-6 gap-1">
{Object.entries(rival.personality).map(([key, val]) => (
<div key={key} className="text-center">
<div className="h-1 bg-surface-800 rounded-full overflow-hidden mb-1">
<div className="h-full bg-accent rounded-full" style={{ width: `${(val as number) * 100}%` }} />
</div>
<div className="text-[10px] text-surface-500 capitalize">{key.replace(/([A-Z])/g, ' $1').trim()}</div>
</div>
))}
</div>
</div>
))}
</div>
{rivals.length === 0 && (
<div className="bg-surface-900 border border-surface-700 rounded-xl p-8 text-center text-surface-500">
<Swords size={48} className="mx-auto mb-4 opacity-50" />
<p>No competitors detected yet.</p>
</div>
)}
</div>
);
}
function Stat({ icon: Icon, label, value, sub }: {
icon: typeof Brain;
label: string;
value: string;
sub?: string;
}) {
return (
<div className="bg-surface-800 rounded-lg p-2">
<div className="flex items-center gap-1 mb-1">
<Icon size={12} className="text-surface-400" />
<span className="text-xs text-surface-400">{label}</span>
</div>
<div className="text-sm font-mono font-semibold">{value}</div>
{sub && <div className="text-[10px] text-surface-500">{sub}</div>}
</div>
);
}