Polish Week 1: tooltips, save import, game balance tuning
Add reusable Tooltip component and rich tooltips on all TopBar KPIs (cash breakdown, compute utilization, reputation context). Add save import button to Settings page. Fix game balance: reduce GPU maintenance 100x, increase organic API demand 200x, accelerate subscription revenue timescale, boost early subscriber seeding, use sqrt scaling for model compute factor, simplify deploy to activate all product lines at once. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { useState, useRef, type ReactNode } from 'react';
|
||||
|
||||
interface TooltipProps {
|
||||
content: ReactNode;
|
||||
children: ReactNode;
|
||||
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
export function Tooltip({ content, children, position = 'bottom' }: TooltipProps) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
const show = () => {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(() => setVisible(true), 300);
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
clearTimeout(timeoutRef.current);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const positionClasses = {
|
||||
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
||||
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
|
||||
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
|
||||
right: 'left-full top-1/2 -translate-y-1/2 ml-2',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative inline-flex" onMouseEnter={show} onMouseLeave={hide}>
|
||||
{children}
|
||||
{visible && (
|
||||
<div className={`absolute z-50 ${positionClasses[position]} pointer-events-none`}>
|
||||
<div className="bg-surface-800 border border-surface-600 rounded-lg px-3 py-2 text-xs text-surface-200 shadow-xl whitespace-nowrap max-w-xs">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import { Pause, Play, Bell, Zap } from 'lucide-react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { Pause, Play, Bell } from 'lucide-react';
|
||||
import { useGameStore } from '@/store';
|
||||
import { formatMoney, formatNumber, formatDuration } from '@ai-tycoon/shared';
|
||||
import { formatMoney, formatNumber, formatDuration, formatPercent } from '@ai-tycoon/shared';
|
||||
import type { GameSpeed } from '@ai-tycoon/shared';
|
||||
import { Tooltip } from '@/components/common/Tooltip';
|
||||
|
||||
const SPEEDS: GameSpeed[] = [1, 2, 5];
|
||||
|
||||
export function TopBar() {
|
||||
const money = useGameStore((s) => s.economy.money);
|
||||
const revenuePerTick = useGameStore((s) => s.economy.revenuePerTick);
|
||||
const expensesPerTick = useGameStore((s) => s.economy.expensesPerTick);
|
||||
const reputation = useGameStore((s) => s.reputation.score);
|
||||
const totalFlops = useGameStore((s) => s.infrastructure.totalFlops);
|
||||
const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization);
|
||||
const isPaused = useGameStore((s) => s.meta.isPaused);
|
||||
const gameSpeed = useGameStore((s) => s.meta.gameSpeed);
|
||||
const tickCount = useGameStore((s) => s.meta.tickCount);
|
||||
@@ -18,14 +22,44 @@ export function TopBar() {
|
||||
const notifications = useGameStore((s) => s.notifications);
|
||||
const unreadCount = notifications.filter(n => !n.read).length;
|
||||
|
||||
const netIncome = revenuePerTick - expensesPerTick;
|
||||
|
||||
return (
|
||||
<header className="h-14 bg-surface-900 border-b border-surface-700 flex items-center justify-between px-4">
|
||||
<div className="flex items-center gap-6">
|
||||
<KPI label="Cash" value={formatMoney(money)} trend={revenuePerTick > 0 ? 'up' : 'neutral'} />
|
||||
<KPI label="Revenue/s" value={formatMoney(revenuePerTick)} />
|
||||
<KPI label="Compute" value={`${formatNumber(totalFlops)} FLOPS`} />
|
||||
<KPI label="Reputation" value={`${reputation}/100`} />
|
||||
<KPI label="Time" value={formatDuration(tickCount)} />
|
||||
<KPI
|
||||
label="Cash"
|
||||
value={formatMoney(money)}
|
||||
trend={netIncome > 0 ? 'up' : netIncome < 0 ? 'down' : 'neutral'}
|
||||
tooltip={<div className="space-y-1">
|
||||
<div>Revenue: <span className="text-success">{formatMoney(revenuePerTick)}/s</span></div>
|
||||
<div>Expenses: <span className="text-danger">{formatMoney(expensesPerTick)}/s</span></div>
|
||||
<div>Net: <span className={netIncome >= 0 ? 'text-success' : 'text-danger'}>{formatMoney(netIncome)}/s</span></div>
|
||||
</div>}
|
||||
/>
|
||||
<KPI
|
||||
label="Revenue/s"
|
||||
value={formatMoney(revenuePerTick)}
|
||||
tooltip="Total revenue per second from API calls and subscriptions"
|
||||
/>
|
||||
<KPI
|
||||
label="Compute"
|
||||
value={`${formatNumber(totalFlops)} FLOPS`}
|
||||
tooltip={<div className="space-y-1">
|
||||
<div>Total compute from all healthy GPUs</div>
|
||||
<div>Utilization: {formatPercent(inferenceUtil)}</div>
|
||||
</div>}
|
||||
/>
|
||||
<KPI
|
||||
label="Reputation"
|
||||
value={`${reputation}/100`}
|
||||
tooltip="Affects talent acquisition, user trust, and investor confidence"
|
||||
/>
|
||||
<KPI
|
||||
label="Time"
|
||||
value={formatDuration(tickCount)}
|
||||
tooltip={`${formatNumber(tickCount)} ticks elapsed`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -65,9 +99,14 @@ export function TopBar() {
|
||||
);
|
||||
}
|
||||
|
||||
function KPI({ label, value, trend }: { label: string; value: string; trend?: 'up' | 'down' | 'neutral' }) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
function KPI({ label, value, trend, tooltip }: {
|
||||
label: string;
|
||||
value: string;
|
||||
trend?: 'up' | 'down' | 'neutral';
|
||||
tooltip?: ReactNode;
|
||||
}) {
|
||||
const content = (
|
||||
<div className="flex flex-col cursor-default">
|
||||
<span className="text-xs text-surface-400">{label}</span>
|
||||
<span className={`text-sm font-mono font-semibold ${
|
||||
trend === 'up' ? 'text-success' : trend === 'down' ? 'text-danger' : 'text-surface-100'
|
||||
@@ -76,4 +115,9 @@ function KPI({ label, value, trend }: { label: string; value: string; trend?: 'u
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return <Tooltip content={tooltip}>{content}</Tooltip>;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user