c1cc70eeb9
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>
189 lines
7.6 KiB
TypeScript
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>
|
|
);
|
|
}
|