Files
AIHostingTycoon/apps/web/src/pages/TalentPage.tsx
T
josh c1cc70eeb9
Balance Check / balance-simulation (pull_request) Successful in 38s
Balance Check / multi-run-balance (pull_request) Successful in 13m44s
Rename AI Tycoon to Token Empire across entire codebase
Full rebrand: UI display text, package scope (@ai-tycoon/* -> @token-empire/*),
localStorage keys, Docker/CI image paths, database names, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 21:04:07 -04:00

189 lines
7.6 KiB
TypeScript

import { useState } from 'react';
import { Users, Plus, Star, Briefcase } from 'lucide-react';
import { useGameStore } from '@/store';
import { formatMoney } from '@token-empire/shared';
import { KEY_HIRE_POOL } from '@token-empire/game-engine';
import type { DepartmentId } from '@token-empire/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>
<div className="text-right">
<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>
{!canHire && <div className="text-[10px] text-warning mt-0.5">Insufficient funds</div>}
</div>
</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>
);
}