Add Week 3 polish and late-game features
VC funding system (seed through IPO with requirements gating), 15 achievements with engine checker, model tuning presets and unlockable sliders, overload policy controls, open-source mechanic with reputation boost, enhanced Recharts analytics (subscriber/reputation/revenue vs expenses charts), M&A acquisition system, sidebar NEW badges on era transitions, tutorial hints, and wired-up settings toggles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { useState } from 'react';
|
||||
import { X, Lightbulb } from 'lucide-react';
|
||||
|
||||
const DISMISSED_KEY = 'ai-tycoon-dismissed-hints';
|
||||
|
||||
function getDismissed(): Set<string> {
|
||||
try {
|
||||
return new Set(JSON.parse(localStorage.getItem(DISMISSED_KEY) || '[]'));
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function dismiss(id: string) {
|
||||
const d = getDismissed();
|
||||
d.add(id);
|
||||
localStorage.setItem(DISMISSED_KEY, JSON.stringify([...d]));
|
||||
}
|
||||
|
||||
export function TutorialHint({ id, children }: { id: string; children: React.ReactNode }) {
|
||||
const [visible, setVisible] = useState(!getDismissed().has(id));
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-accent/10 border border-accent/30 rounded-lg p-3 flex items-start gap-3 mb-4">
|
||||
<Lightbulb size={16} className="text-accent-light mt-0.5 shrink-0" />
|
||||
<div className="text-sm text-surface-200 flex-1">{children}</div>
|
||||
<button
|
||||
onClick={() => { dismiss(id); setVisible(false); }}
|
||||
className="text-surface-400 hover:text-surface-200"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { FinancePage } from '@/pages/FinancePage';
|
||||
import { TalentPage } from '@/pages/TalentPage';
|
||||
import { DataPage } from '@/pages/DataPage';
|
||||
import { CompetitorsPage } from '@/pages/CompetitorsPage';
|
||||
import { AchievementsPage } from '@/pages/AchievementsPage';
|
||||
|
||||
export function MainLayout() {
|
||||
const activePage = useGameStore((s) => s.activePage);
|
||||
@@ -43,6 +44,7 @@ function PageRouter({ page }: { page: string }) {
|
||||
case 'talent': return <TalentPage />;
|
||||
case 'data': return <DataPage />;
|
||||
case 'competitors': return <CompetitorsPage />;
|
||||
case 'achievements': return <AchievementsPage />;
|
||||
case 'settings': return <SettingsPage />;
|
||||
default: return <PlaceholderPage name={page} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
LayoutDashboard, Server, FlaskConical, Brain,
|
||||
TrendingUp, Users, Database, Swords, DollarSign, Settings,
|
||||
TrendingUp, Users, Database, Swords, DollarSign, Settings, Trophy,
|
||||
} from 'lucide-react';
|
||||
import { useGameStore, type ActivePage } from '@/store';
|
||||
|
||||
@@ -14,6 +15,7 @@ const NAV_ITEMS: { page: ActivePage; label: string; icon: typeof LayoutDashboard
|
||||
{ page: 'talent', label: 'Talent', icon: Users, era: 'scaleup' },
|
||||
{ page: 'data', label: 'Data', icon: Database, era: 'scaleup' },
|
||||
{ page: 'competitors', label: 'Competitors', icon: Swords, era: 'scaleup' },
|
||||
{ page: 'achievements', label: 'Achievements', icon: Trophy },
|
||||
{ page: 'settings', label: 'Settings', icon: Settings },
|
||||
];
|
||||
|
||||
@@ -26,6 +28,32 @@ export function Sidebar() {
|
||||
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'];
|
||||
const currentEraIdx = eraOrder.indexOf(era);
|
||||
|
||||
const seenEraRef = useRef(era);
|
||||
const [newPages, setNewPages] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (era !== seenEraRef.current) {
|
||||
const oldIdx = eraOrder.indexOf(seenEraRef.current);
|
||||
const newIdx = eraOrder.indexOf(era);
|
||||
if (newIdx > oldIdx) {
|
||||
const newlyVisible = NAV_ITEMS
|
||||
.filter(item => item.era && eraOrder.indexOf(item.era) > oldIdx && eraOrder.indexOf(item.era) <= newIdx)
|
||||
.map(item => item.page);
|
||||
setNewPages(prev => new Set([...prev, ...newlyVisible]));
|
||||
}
|
||||
seenEraRef.current = era;
|
||||
}
|
||||
}, [era]);
|
||||
|
||||
const handleNavClick = (page: ActivePage) => {
|
||||
setActivePage(page);
|
||||
setNewPages(prev => {
|
||||
const next = new Set(prev);
|
||||
next.delete(page);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="w-56 bg-surface-900 border-r border-surface-700 flex flex-col h-screen">
|
||||
<div className="p-4 border-b border-surface-700">
|
||||
@@ -40,10 +68,11 @@ export function Sidebar() {
|
||||
if (requiredEra && eraOrder.indexOf(requiredEra) > currentEraIdx) return null;
|
||||
|
||||
const isActive = activePage === page;
|
||||
const isNew = newPages.has(page);
|
||||
return (
|
||||
<button
|
||||
key={page}
|
||||
onClick={() => setActivePage(page)}
|
||||
onClick={() => handleNavClick(page)}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
|
||||
isActive
|
||||
? 'bg-accent/10 text-accent-light border-r-2 border-accent'
|
||||
@@ -52,6 +81,9 @@ export function Sidebar() {
|
||||
>
|
||||
<Icon size={18} />
|
||||
{label}
|
||||
{isNew && (
|
||||
<span className="ml-auto text-[10px] font-bold bg-accent text-white px-1.5 py-0.5 rounded">NEW</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user