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:
2026-04-24 17:56:40 -04:00
parent 8ea6c771a1
commit 8a8b49d934
20 changed files with 907 additions and 75 deletions
+34 -2
View File
@@ -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>
);
})}