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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user