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,185 @@
|
||||
import { useState } from 'react';
|
||||
import { Users, Plus, Star, Briefcase } from 'lucide-react';
|
||||
import { useGameStore } from '@/store';
|
||||
import { formatMoney } from '@ai-tycoon/shared';
|
||||
import { KEY_HIRE_POOL } from '@ai-tycoon/game-engine';
|
||||
import type { DepartmentId } from '@ai-tycoon/shared';
|
||||
|
||||
const DEPT_LABELS: Record<string, string> = {
|
||||
research: 'Research',
|
||||
engineering: 'Engineering',
|
||||
operations: 'Operations',
|
||||
sales: 'Sales',
|
||||
};
|
||||
|
||||
const DEPT_DESCRIPTIONS: Record<string, string> = {
|
||||
research: 'Improves R&D speed and model quality',
|
||||
engineering: 'Faster training and better infrastructure',
|
||||
operations: 'Lower costs and higher uptime',
|
||||
sales: 'More enterprise contracts and revenue',
|
||||
};
|
||||
|
||||
export function TalentPage() {
|
||||
const departments = useGameStore((s) => s.talent.departments);
|
||||
const keyHires = useGameStore((s) => s.talent.keyHires);
|
||||
const totalSalary = useGameStore((s) => s.talent.totalSalaryPerTick);
|
||||
const money = useGameStore((s) => s.economy.money);
|
||||
const era = useGameStore((s) => s.meta.currentEra);
|
||||
const hireDepartment = useGameStore((s) => s.hireDepartment);
|
||||
|
||||
const [showKeyHires, setShowKeyHires] = useState(false);
|
||||
|
||||
const hiringCost = 2000;
|
||||
const canHire = money >= hiringCost;
|
||||
|
||||
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'];
|
||||
const currentEraIdx = eraOrder.indexOf(era);
|
||||
const availableKeyHires = KEY_HIRE_POOL.filter(h => {
|
||||
const hireEraIdx = eraOrder.indexOf(h.requiredEra);
|
||||
if (hireEraIdx > currentEraIdx) return false;
|
||||
return !keyHires.some(kh => kh.id === h.id);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">Talent</h2>
|
||||
<div className="text-sm text-surface-400">
|
||||
Total Salary: <span className="text-danger font-mono">{formatMoney(totalSalary)}/s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{Object.entries(departments).map(([id, dept]) => (
|
||||
<div key={id} className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="font-semibold">{DEPT_LABELS[id]}</h3>
|
||||
<p className="text-xs text-surface-400">{DEPT_DESCRIPTIONS[id]}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold font-mono">{dept.headcount}</div>
|
||||
<div className="text-xs text-surface-500">employees</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mb-3">
|
||||
<StatBar label="Effectiveness" value={dept.effectiveness} />
|
||||
<StatBar label="Morale" value={dept.morale} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-surface-400">
|
||||
Budget: {formatMoney(dept.budget)}/mo
|
||||
</span>
|
||||
<button
|
||||
onClick={() => hireDepartment(id, 1)}
|
||||
disabled={!canHire}
|
||||
className="flex items-center gap-1 bg-surface-800 hover:bg-surface-700 border border-surface-600 rounded px-3 py-1.5 text-xs disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Plus size={12} />
|
||||
Hire ({formatMoney(hiringCost)})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-semibold flex items-center gap-2">
|
||||
<Star size={16} className="text-yellow-400" />
|
||||
Key Hires
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowKeyHires(!showKeyHires)}
|
||||
className="text-xs text-accent hover:text-accent-light"
|
||||
>
|
||||
{showKeyHires ? 'Hide Available' : `Show Available (${availableKeyHires.length})`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{keyHires.length > 0 && (
|
||||
<div className="space-y-2 mb-4">
|
||||
{keyHires.map(hire => (
|
||||
<div key={hire.id} className="flex items-center justify-between bg-surface-800 rounded-lg p-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">{hire.name}</div>
|
||||
<div className="text-xs text-surface-400">
|
||||
{DEPT_LABELS[hire.department]} · {hire.specialAbility}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-surface-400">
|
||||
{formatMoney(hire.salary)}/s
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showKeyHires && (
|
||||
<div className="space-y-2">
|
||||
{availableKeyHires.length > 0 ? availableKeyHires.map(hire => (
|
||||
<div key={hire.id} className="flex items-center justify-between bg-surface-800/50 border border-surface-700 rounded-lg p-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">{hire.name}</div>
|
||||
<div className="text-xs text-surface-400">{hire.description}</div>
|
||||
<div className="text-xs text-surface-500">
|
||||
{DEPT_LABELS[hire.department]} · {formatMoney(hire.salary)}/s
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const store = useGameStore.getState();
|
||||
const keyHireObj = {
|
||||
id: hire.id,
|
||||
name: hire.name,
|
||||
department: hire.department as DepartmentId,
|
||||
specialAbility: hire.description,
|
||||
effects: hire.effects,
|
||||
salary: hire.salary,
|
||||
hiredAtTick: store.meta.tickCount,
|
||||
loyalty: hire.loyalty,
|
||||
};
|
||||
store.updateState({
|
||||
talent: {
|
||||
...store.talent,
|
||||
keyHires: [...store.talent.keyHires, keyHireObj],
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={money < 5000}
|
||||
className="flex items-center gap-1 bg-accent hover:bg-accent-dark text-white rounded px-3 py-1.5 text-xs disabled:opacity-40"
|
||||
>
|
||||
<Briefcase size={12} />
|
||||
Recruit ($5K)
|
||||
</button>
|
||||
</div>
|
||||
)) : (
|
||||
<p className="text-sm text-surface-500 text-center py-2">No key hires available in current era</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{keyHires.length === 0 && !showKeyHires && (
|
||||
<p className="text-sm text-surface-500">No key hires recruited yet.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatBar({ label, value }: { label: string; value: number }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-surface-500 w-24">{label}</span>
|
||||
<div className="flex-1 h-1.5 bg-surface-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${value > 0.7 ? 'bg-success' : value > 0.4 ? 'bg-warning' : 'bg-danger'}`}
|
||||
style={{ width: `${value * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-surface-400 font-mono w-8 text-right">{Math.round(value * 100)}%</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user