From f1d0fa63e592f1209c2dea950d73c2df07a5da42 Mon Sep 17 00:00:00 2001 From: russofg Date: Sun, 16 Aug 2026 00:34:09 -0300 Subject: [PATCH 1/4] build(ts): turn on strict mode, drop dead code, and format the tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The typecheck only became meaningful once @types/react was installed, so strict had never actually been exercised. It passes with zero errors — the code was already well typed, nothing was enforcing it. noUnusedLocals surfaced 25 dead imports and variables, plus an effect in ClientPopup that returned a cleanup on only one path. hooks/useAIImage.ts is deleted outright: its last reference was a comment saying it had been removed. Prettier's first pass is folded into this commit rather than split out. It touches nearly every file, so isolating it would produce two large diffs over the same lines instead of one. --- .prettierignore | 4 + .prettierrc.json | 7 + App.tsx | 1285 +++++++++-------- components/AchievementPanel.tsx | 13 +- components/Button.tsx | 1 - components/ClientPopup.tsx | 102 +- components/ComboIndicator.tsx | 26 +- components/EarlyWarningPanel.tsx | 51 +- components/ErrorBoundary.tsx | 8 +- components/EventCard.tsx | 239 +-- components/FXCanvas.tsx | 47 +- components/FaderPanel.tsx | 205 ++- components/GameMenu.tsx | 470 +++--- components/GameSettingsPanel.tsx | 69 +- components/Minigames.tsx | 366 ++--- components/MissionPanel.tsx | 99 +- components/NarrativePopup.tsx | 58 +- components/ProgressBar.tsx | 48 +- components/Shop.tsx | 245 ++-- components/SocialFeed.tsx | 135 +- components/TerminalLog.tsx | 95 +- components/TutorialOverlay.tsx | 101 +- components/UpgradeShop.tsx | 40 +- components/Visualizer.tsx | 109 +- constants.ts | 160 +- docs/ART_BIBLE.md | 9 + hooks/gameLogic/bossMoments.ts | 85 +- hooks/gameLogic/career.ts | 23 +- hooks/gameLogic/director.ts | 118 +- hooks/gameLogic/economy.ts | 151 +- hooks/gameLogic/events.ts | 79 +- hooks/gameLogic/math.ts | 3 +- hooks/gameLogic/missions.ts | 100 +- hooks/gameLogic/sessionRules.ts | 3 +- hooks/gameLogic/upgrades.ts | 7 +- hooks/useAIEventGenerator.ts | 40 +- hooks/useAIImage.ts | 30 - hooks/useAchievementSystem.ts | 37 +- hooks/useClientAI.ts | 74 +- hooks/useComboSystem.ts | 39 +- hooks/useDynamicColors.ts | 10 +- hooks/useEarlyWarningSystem.ts | 73 +- hooks/useGameLogic.ts | 923 +++++++----- hooks/useNarrativeSystem.ts | 104 +- hooks/useSoundSynth.ts | 904 ++++++------ hooks/useUpgradeSystem.ts | 66 +- hooks/useUserSettings.ts | 13 +- index.css | 119 +- index.tsx | 2 +- tests/cinematic-fx-regressions.test.ts | 8 +- tests/error-boundary.test.tsx | 5 +- tests/fader-panel-a11y.test.tsx | 24 +- tests/game-content.test.ts | 42 +- tests/game-logic-regressions.test.ts | 483 +++++-- tests/game-session-flow.test.ts | 62 +- tests/mobile-ui-policy-regressions.test.ts | 2 +- tests/narrative-director-regressions.test.ts | 8 +- tests/playtest-automation-regressions.test.ts | 15 +- tests/scenario-unlocks.test.ts | 52 +- tests/sound-mix-regressions.test.ts | 20 +- tests/ui-accessibility-contract.test.tsx | 24 +- tests/visual-performance-regressions.test.ts | 8 +- tsconfig.json | 21 +- types.ts | 88 +- utils/aiRuntime.ts | 7 +- utils/artAssets.ts | 13 +- utils/artDirection.ts | 10 +- utils/cinematicFx.ts | 15 +- utils/mobileUiPolicy.ts | 16 +- utils/runtimeDiagnostics.ts | 10 +- utils/scenarioUnlocks.ts | 29 +- utils/visualPerformance.ts | 45 +- vite.config.ts | 22 +- 73 files changed, 4787 insertions(+), 3237 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.json delete mode 100644 hooks/useAIImage.ts diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..de102a9 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +dist +node_modules +public/assets +package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..da70510 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "semi": true, + "printWidth": 100, + "trailingComma": "none", + "arrowParens": "always" +} diff --git a/App.tsx b/App.tsx index 746f7be..388d4e0 100644 --- a/App.tsx +++ b/App.tsx @@ -1,6 +1,5 @@ - import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; -import { GameMode, GameState, SystemType, SystemState, GameEventOption } from './types'; +import { GameMode, GameState, SystemType, GameEventOption } from './types'; import { SCENARIOS, TUTORIAL_STEPS, PERMANENT_UPGRADES, WIN_CONDITIONS } from './constants'; import { useGameLogic } from './hooks/useGameLogic'; import { useClientAI } from './hooks/useClientAI'; @@ -35,15 +34,36 @@ import { computeMobileHudInsets } from './utils/mobileHudLayout'; import { useUserSettings, VISUAL_QUALITY_LABEL } from './hooks/useUserSettings'; import { useViewportLayout } from './hooks/useViewportLayout'; import { getVisualQualityProfile } from './utils/visualPerformance'; -import { getCinematicTransitionStyle, getThreatLevel, getThreatRailProfile } from './utils/cinematicFx'; +import { + getCinematicTransitionStyle, + getThreatLevel, + getThreatRailProfile +} from './utils/cinematicFx'; import { getEventImpactStyle } from './utils/impactFx'; import type { EventImpactStyle } from './utils/impactFx'; import { buildRuntimeDiagnosticsSnapshot } from './utils/runtimeDiagnostics'; import { getArtDirectionCssVariables, getScenarioArtDirectionProfile } from './utils/artDirection'; import { getFxTextureCssVariables, getSceneBackgroundAsset } from './utils/artAssets'; -import { getMenuToolbarClasses, getMobileOverlayVisibility, sortEventsByUrgency } from './utils/mobileUiPolicy'; +import { + getMenuToolbarClasses, + getMobileOverlayVisibility, + sortEventsByUrgency +} from './utils/mobileUiPolicy'; import { getHudCinematicClasses } from './utils/uiCinematics'; -import { Activity, DollarSign, Trophy, AlertOctagon, Users, ZapOff, Frown, Pause, RotateCcw, AlertTriangle, Settings, Home } from 'lucide-react'; +import { + Activity, + DollarSign, + Trophy, + AlertOctagon, + Users, + ZapOff, + Frown, + Pause, + RotateCcw, + AlertTriangle, + Settings, + Home +} from 'lucide-react'; interface ActiveCinematicTransition { id: number; @@ -70,11 +90,11 @@ interface ActiveCameraPunch { } const App: React.FC = () => { - const { - gameState, - stats, - systems, - activeEvents, + const { + gameState, + stats, + systems, + activeEvents, activeMinigame, careerData, inventory, @@ -88,12 +108,11 @@ const App: React.FC = () => { startGame, // Shop -> Game togglePause, quitGame, - resolveEvent, + resolveEvent, completeMinigame, setGameState, handleFaderChange, applyComboBonus, - setComboBonusCallback, setEventResolutionCallback, currentScenario, currentGameMode, @@ -102,7 +121,10 @@ const App: React.FC = () => { } = useGameLogic(); // REMOVED: useAIImage hook is no longer needed for 2D visualizer - const { clientMessage, clientMood, clearMessage } = useClientAI(stats, gameState === GameState.PLAYING && !tutorialActive); + const { clientMessage, clientMood, clearMessage } = useClientAI( + stats, + gameState === GameState.PLAYING && !tutorialActive + ); const { playClick, playError, @@ -122,19 +144,21 @@ const App: React.FC = () => { stopBackgroundLoop, updateBackgroundLoop } = useSoundSynth(); - + // Combo System - const { comboState, setBonusCallback } = useComboSystem({ - systems, - isPlaying: gameState === GameState.PLAYING && !tutorialActive + const { comboState, setBonusCallback } = useComboSystem({ + systems, + isPlaying: gameState === GameState.PLAYING && !tutorialActive }); - + const [selectedSystem, setSelectedSystem] = useState(SystemType.SOUND); const [screenShake, setScreenShake] = useState(false); const [explosionTrigger, setExplosionTrigger] = useState(false); const [selectedScenarioId, setSelectedScenarioId] = useState(SCENARIOS[0].id); const [selectedGameMode, setSelectedGameMode] = useState(GameMode.NORMAL); - const [logs, setLogs] = useState<{id: string, text: string, type: 'info'|'error'|'warning'|'success'}[]>([]); + const [logs, setLogs] = useState< + { id: string; text: string; type: 'info' | 'error' | 'warning' | 'success' }[] + >([]); const [showAchievements, setShowAchievements] = useState(false); const [showUpgrades, setShowUpgrades] = useState(false); const [showSettings, setShowSettings] = useState(false); @@ -162,7 +186,7 @@ const App: React.FC = () => { const [activeImpactFx, setActiveImpactFx] = useState(null); const [activeFreezePulse, setActiveFreezePulse] = useState(null); const [activeCameraPunch, setActiveCameraPunch] = useState(null); - + const prevEventIdsRef = useRef>(new Set()); const prevGameStateRef = useRef(gameState); const headerRef = useRef(null); @@ -206,9 +230,7 @@ const App: React.FC = () => { clearTimeout(transitionTimeoutRef.current); } transitionTimeoutRef.current = window.setTimeout(() => { - setActiveTransition(current => - current && current.id === transitionId ? null : current - ); + setActiveTransition((current) => (current && current.id === transitionId ? null : current)); transitionTimeoutRef.current = null; }, transitionStyle.durationMs); } @@ -242,7 +264,8 @@ const App: React.FC = () => { }, [isMobileLayout]); useEffect(() => { - if (!isMobileLayout || (gameState !== GameState.PLAYING && gameState !== GameState.PAUSED)) return; + if (!isMobileLayout || (gameState !== GameState.PLAYING && gameState !== GameState.PAUSED)) + return; updateMobileHudLayout(); const rafId = window.requestAnimationFrame(updateMobileHudLayout); @@ -272,8 +295,8 @@ const App: React.FC = () => { return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; - const addLog = (text: string, type: 'info'|'error'|'warning'|'success' = 'info') => { - setLogs(prev => [...prev.slice(-20), { id: Math.random().toString(), text, type }]); + const addLog = (text: string, type: 'info' | 'error' | 'warning' | 'success' = 'info') => { + setLogs((prev) => [...prev.slice(-20), { id: Math.random().toString(), text, type }]); }; // Narrative System (Fase 2) @@ -283,15 +306,18 @@ const App: React.FC = () => { isPlaying: gameState === GameState.PLAYING && !tutorialActive, currentScenario: currentScenario }); - + // Early Warning System (Fase 2) - const handlePreventEvent = useCallback((warningId: string) => { - addLog(`Evento prevenido: ${warningId}`, 'success'); - playSuccess(); - // Small bonus for preventing events - // This could be integrated with useGameLogic if needed - }, [playSuccess]); - + const handlePreventEvent = useCallback( + (warningId: string) => { + addLog(`Evento prevenido: ${warningId}`, 'success'); + playSuccess(); + // Small bonus for preventing events + // This could be integrated with useGameLogic if needed + }, + [playSuccess] + ); + const { activeWarnings } = useEarlyWarningSystem({ stats, systems, @@ -300,26 +326,30 @@ const App: React.FC = () => { }); // Achievement System (Fase 3) - const handleAchievementUnlocked = useCallback((achievementId: string, points: number) => { - const achievement = ACHIEVEMENTS.find(a => a.id === achievementId); - if (achievement && !careerData.unlockedAchievements.includes(achievementId)) { - const newCareer = { - ...careerData, - unlockedAchievements: [...careerData.unlockedAchievements, achievementId], - careerPoints: careerData.careerPoints + points - }; - saveCareer(newCareer); - addLog(`🎉 Logro desbloqueado: ${achievement.title} (+${points} pts)`, 'success'); - playSuccess(); - } - }, [careerData, saveCareer, playSuccess]); + const handleAchievementUnlocked = useCallback( + (achievementId: string, points: number) => { + const achievement = ACHIEVEMENTS.find((a) => a.id === achievementId); + if (achievement && !careerData.unlockedAchievements.includes(achievementId)) { + const newCareer = { + ...careerData, + unlockedAchievements: [...careerData.unlockedAchievements, achievementId], + careerPoints: careerData.careerPoints + points + }; + saveCareer(newCareer); + addLog(`🎉 Logro desbloqueado: ${achievement.title} (+${points} pts)`, 'success'); + playSuccess(); + } + }, + [careerData, saveCareer, playSuccess] + ); - const { sessionData, trackEventResolved, trackSpending } = useAchievementSystem({ + const { trackEventResolved, trackSpending } = useAchievementSystem({ stats, systems, activeEvents, // Keep session progress across pause; only stop tracking when leaving gameplay flow. - isPlaying: (gameState === GameState.PLAYING || gameState === GameState.PAUSED) && !tutorialActive, + isPlaying: + (gameState === GameState.PLAYING || gameState === GameState.PAUSED) && !tutorialActive, careerData, onAchievementUnlocked: handleAchievementUnlocked }); @@ -342,7 +372,7 @@ const App: React.FC = () => { clearTimeout(impactTimeoutRef.current); } impactTimeoutRef.current = window.setTimeout(() => { - setActiveImpactFx(current => (current && current.id === impactId ? null : current)); + setActiveImpactFx((current) => (current && current.id === impactId ? null : current)); impactTimeoutRef.current = null; }, impactStyle.durationMs); @@ -355,13 +385,15 @@ const App: React.FC = () => { clearTimeout(freezeTimeoutRef.current); } freezeTimeoutRef.current = window.setTimeout(() => { - setActiveFreezePulse(current => (current && current.id === impactId ? null : current)); + setActiveFreezePulse((current) => (current && current.id === impactId ? null : current)); freezeTimeoutRef.current = null; }, impactStyle.freezeMs); - const directionX = impactStyle.origin.xPercent < 35 ? 1 : impactStyle.origin.xPercent > 65 ? -1 : 0; - const directionY = impactStyle.origin.yPercent < 40 ? 1 : impactStyle.origin.yPercent > 65 ? -1 : 0; - const punchAmplitude = (success ? 2.4 : 4.2) + (severity * (success ? 0.9 : 1.35)); + const directionX = + impactStyle.origin.xPercent < 35 ? 1 : impactStyle.origin.xPercent > 65 ? -1 : 0; + const directionY = + impactStyle.origin.yPercent < 40 ? 1 : impactStyle.origin.yPercent > 65 ? -1 : 0; + const punchAmplitude = (success ? 2.4 : 4.2) + severity * (success ? 0.9 : 1.35); setActiveCameraPunch({ id: impactId, x: directionX * punchAmplitude, @@ -371,13 +403,16 @@ const App: React.FC = () => { if (punchTimeoutRef.current) { clearTimeout(punchTimeoutRef.current); } - punchTimeoutRef.current = window.setTimeout(() => { - setActiveCameraPunch(current => (current && current.id === impactId ? null : current)); - punchTimeoutRef.current = null; - }, success ? 160 : 210); + punchTimeoutRef.current = window.setTimeout( + () => { + setActiveCameraPunch((current) => (current && current.id === impactId ? null : current)); + punchTimeoutRef.current = null; + }, + success ? 160 : 210 + ); if (success) { - setExplosionTrigger(t => !t); + setExplosionTrigger((t) => !t); addLog(`${systemId}: ${eventTitle} RESUELTO`, 'success'); } else { addLog(`${systemId}: ${eventTitle} RESOLUCIÓN FALLIDA`, 'error'); @@ -386,38 +421,41 @@ const App: React.FC = () => { }, [setEventResolutionCallback, trackEventResolved, trackSpending, playEventResolutionSfx]); // Upgrade System (Fase 3) - const handleUpgradePurchased = useCallback((upgradeId: string, cost: number) => { - const upgrade = PERMANENT_UPGRADES.find(u => u.id === upgradeId); - const missingRequirements = (upgrade?.requires || []).filter( - requirementId => !careerData.unlockedUpgrades.includes(requirementId) - ); - - if (missingRequirements.length > 0) { - addLog('Esta mejora todavía requiere desbloqueos previos', 'warning'); - playError(); - return; - } + const handleUpgradePurchased = useCallback( + (upgradeId: string, cost: number) => { + const upgrade = PERMANENT_UPGRADES.find((u) => u.id === upgradeId); + const missingRequirements = (upgrade?.requires || []).filter( + (requirementId) => !careerData.unlockedUpgrades.includes(requirementId) + ); + + if (missingRequirements.length > 0) { + addLog('Esta mejora todavía requiere desbloqueos previos', 'warning'); + playError(); + return; + } - // Verificar que no esté ya desbloqueado y que tenga puntos suficientes - if (careerData.unlockedUpgrades.includes(upgradeId)) { - addLog('Esta mejora ya está desbloqueada', 'warning'); - return; // Ya está desbloqueado - } - if (careerData.careerPoints < cost) { - addLog('No tienes suficientes puntos de carrera', 'error'); - playError(); - return; // No tiene puntos suficientes - } + // Verificar que no esté ya desbloqueado y que tenga puntos suficientes + if (careerData.unlockedUpgrades.includes(upgradeId)) { + addLog('Esta mejora ya está desbloqueada', 'warning'); + return; // Ya está desbloqueado + } + if (careerData.careerPoints < cost) { + addLog('No tienes suficientes puntos de carrera', 'error'); + playError(); + return; // No tiene puntos suficientes + } - const newCareer = { - ...careerData, - unlockedUpgrades: [...careerData.unlockedUpgrades, upgradeId], - careerPoints: careerData.careerPoints - cost - }; - saveCareer(newCareer); - addLog(`✅ Mejora desbloqueada: ${upgrade?.name || upgradeId}`, 'success'); - playSuccess(); - }, [careerData, saveCareer, playSuccess, playError]); + const newCareer = { + ...careerData, + unlockedUpgrades: [...careerData.unlockedUpgrades, upgradeId], + careerPoints: careerData.careerPoints - cost + }; + saveCareer(newCareer); + addLog(`✅ Mejora desbloqueada: ${upgrade?.name || upgradeId}`, 'success'); + playSuccess(); + }, + [careerData, saveCareer, playSuccess, playError] + ); const { getAvailableUpgrades, purchaseUpgrade } = useUpgradeSystem({ careerData, @@ -435,58 +473,57 @@ const App: React.FC = () => { // Audio Loop Logic useEffect(() => { - if (gameState === GameState.PLAYING) { - startBackgroundLoop(); - } else { - stopBackgroundLoop(); - } - return () => { - stopBackgroundLoop(); - }; + if (gameState === GameState.PLAYING) { + startBackgroundLoop(); + } else { + stopBackgroundLoop(); + } + return () => { + stopBackgroundLoop(); + }; }, [gameState, startBackgroundLoop, stopBackgroundLoop]); // Audio Reactive Updates useEffect(() => { - if (gameState === GameState.PLAYING) { - updateBackgroundLoop( - systems[SystemType.SOUND].faderValue, - systems[SystemType.SOUND].status, - stats.stress, - systems[SystemType.STAGE].faderValue, - stats.publicInterest, - systems - ); - } + if (gameState === GameState.PLAYING) { + updateBackgroundLoop( + systems[SystemType.SOUND].faderValue, + systems[SystemType.SOUND].status, + stats.stress, + systems[SystemType.STAGE].faderValue, + stats.publicInterest, + systems + ); + } }, [gameState, systems, stats.stress, stats.publicInterest, updateBackgroundLoop]); - useEffect(() => { - const prevIds = prevEventIdsRef.current; - const newEvents = activeEvents.filter(event => !prevIds.has(event.id)); - - if (newEvents.length > 0) { - newEvents.forEach((newEvent) => { - const isEscalation = Boolean(newEvent.escalatedFrom); - addLog( - `${newEvent.systemId}: ${newEvent.title} ${isEscalation ? 'ESCALATED' : 'DETECTED'}`, - isEscalation || newEvent.severity === 3 ? 'error' : 'warning' - ); - - if (isEscalation) { - playEventEscalationSfx({ systemId: newEvent.systemId, severity: newEvent.severity }); - setScreenShake(true); - setTimeout(() => setScreenShake(false), 450); - } else if (newEvent.severity === 3) { - playAlarm(); - setScreenShake(true); - setTimeout(() => setScreenShake(false), 500); - } else { - playError(); - } - }); - } - - prevEventIdsRef.current = new Set(activeEvents.map(event => event.id)); + const prevIds = prevEventIdsRef.current; + const newEvents = activeEvents.filter((event) => !prevIds.has(event.id)); + + if (newEvents.length > 0) { + newEvents.forEach((newEvent) => { + const isEscalation = Boolean(newEvent.escalatedFrom); + addLog( + `${newEvent.systemId}: ${newEvent.title} ${isEscalation ? 'ESCALATED' : 'DETECTED'}`, + isEscalation || newEvent.severity === 3 ? 'error' : 'warning' + ); + + if (isEscalation) { + playEventEscalationSfx({ systemId: newEvent.systemId, severity: newEvent.severity }); + setScreenShake(true); + setTimeout(() => setScreenShake(false), 450); + } else if (newEvent.severity === 3) { + playAlarm(); + setScreenShake(true); + setTimeout(() => setScreenShake(false), 500); + } else { + playError(); + } + }); + } + + prevEventIdsRef.current = new Set(activeEvents.map((event) => event.id)); }, [activeEvents, playAlarm, playError, playEventEscalationSfx]); const criticalEventCount = useMemo( @@ -497,10 +534,7 @@ const App: React.FC = () => { () => activeEvents.reduce((acc, event) => acc + (event.severity === 2 ? 1 : 0), 0), [activeEvents] ); - const sortedActiveEvents = useMemo( - () => sortEventsByUrgency(activeEvents), - [activeEvents] - ); + const sortedActiveEvents = useMemo(() => sortEventsByUrgency(activeEvents), [activeEvents]); const mobilePrimaryEvent = sortedActiveEvents[0] || null; const mobileQueuedEvents = Math.max(0, sortedActiveEvents.length - 1); const mobileCenterHeight = useMemo(() => { @@ -526,7 +560,9 @@ const App: React.FC = () => { criticalEvents: criticalEventCount, warningEvents: warningEventCount, stress: stats.stress, - overlaysActive: Boolean(activeMission || activeNarrative || activeWarnings.length > 0 || clientMessage) + overlaysActive: Boolean( + activeMission || activeNarrative || activeWarnings.length > 0 || clientMessage + ) }); }, [ activeMission, @@ -540,31 +576,30 @@ const App: React.FC = () => { warningEventCount ]); - const handleStartSession = (scenarioId: string, crewId: string, gameMode: GameMode) => { - playClick(); - const resolvedScenarioId = initializeSession(scenarioId, crewId, gameMode); // Goes to Shop - setScenarioAudioProfile(resolvedScenarioId); - setGameModeAudioPreset(gameMode); - playScenarioTransitionSfx(resolvedScenarioId, 'LOAD'); - if (resolvedScenarioId !== scenarioId) { - setSelectedScenarioId(resolvedScenarioId); - addLog(`Escenario bloqueado. Redirigido a ${resolvedScenarioId}.`, 'warning'); - } - addLog(`Loading Scenario Config: ${resolvedScenarioId} (${gameMode})...`, 'info'); + playClick(); + const resolvedScenarioId = initializeSession(scenarioId, crewId, gameMode); // Goes to Shop + setScenarioAudioProfile(resolvedScenarioId); + setGameModeAudioPreset(gameMode); + playScenarioTransitionSfx(resolvedScenarioId, 'LOAD'); + if (resolvedScenarioId !== scenarioId) { + setSelectedScenarioId(resolvedScenarioId); + addLog(`Escenario bloqueado. Redirigido a ${resolvedScenarioId}.`, 'warning'); + } + addLog(`Loading Scenario Config: ${resolvedScenarioId} (${gameMode})...`, 'info'); }; - + const handleShopFinish = () => { - playStartGame(currentScenario.id); - startGame(); // Goes to Playing - setLogs([]); - addLog('SYSTEM INITIALIZED. SHOW STARTED.', 'success'); + playStartGame(currentScenario.id); + startGame(); // Goes to Playing + setLogs([]); + addLog('SYSTEM INITIALIZED. SHOW STARTED.', 'success'); }; const handleResolveEvent = (id: string, option: GameEventOption) => { - playClick(); - resolveEvent(id, option); - addLog(`User Action: ${option.label}`, 'info'); + playClick(); + resolveEvent(id, option); + addLog(`User Action: ${option.label}`, 'info'); }; const handleSystemSelect = (sys: SystemType) => { @@ -586,36 +621,51 @@ const App: React.FC = () => { restoreDefaultSettings(); addLog('Ajustes restablecidos a valores por defecto', 'warning'); }, [addLog, restoreDefaultSettings]); - + const getGameOverReason = () => { - if (gameState === GameState.VICTORY) return { - title: "MISIÓN CUMPLIDA", - desc: "Evento finalizado con éxito.", - icon: Trophy, color: "text-emerald-400" + if (gameState === GameState.VICTORY) + return { + title: 'MISIÓN CUMPLIDA', + desc: 'Evento finalizado con éxito.', + icon: Trophy, + color: 'text-emerald-400' }; - if (stats.stress >= 100) return { - title: "COLAPSO NERVIOSO", - desc: "El operador no pudo manejar la presión.", - icon: ZapOff, color: "text-red-500" + if (stats.stress >= 100) + return { + title: 'COLAPSO NERVIOSO', + desc: 'El operador no pudo manejar la presión.', + icon: ZapOff, + color: 'text-red-500' }; - if (stats.publicInterest <= 0) return { - title: "PÚBLICO HOSTIL", - desc: "Disturbios generalizados.", - icon: Users, color: "text-red-500" + if (stats.publicInterest <= 0) + return { + title: 'PÚBLICO HOSTIL', + desc: 'Disturbios generalizados.', + icon: Users, + color: 'text-red-500' }; - if (stats.clientSatisfaction <= 0) return { - title: "DESPEDIDO", - desc: "El cliente canceló el contrato.", - icon: Frown, color: "text-red-500" + if (stats.clientSatisfaction <= 0) + return { + title: 'DESPEDIDO', + desc: 'El cliente canceló el contrato.', + icon: Frown, + color: 'text-red-500' }; - if (stats.budget < WIN_CONDITIONS.minBudget) return { - title: "QUIEBRA", - desc: "Fondos insuficientes.", - icon: DollarSign, color: "text-red-500" + if (stats.budget < WIN_CONDITIONS.minBudget) + return { + title: 'QUIEBRA', + desc: 'Fondos insuficientes.', + icon: DollarSign, + color: 'text-red-500' }; - return { title: "GAME OVER", desc: "Sistema terminado.", icon: AlertOctagon, color: "text-red-500" }; + return { + title: 'GAME OVER', + desc: 'Sistema terminado.', + icon: AlertOctagon, + color: 'text-red-500' + }; }; - + // DAMAGE EFFECTS const videoCritical = systems[SystemType.VIDEO].status === 'CRITICAL'; const stageSmoke = systems[SystemType.STAGE].faderValue > 80; @@ -649,7 +699,10 @@ const App: React.FC = () => { const showMobileCombo = mobileOverlayVisibility.showCombo; const showMobileSocialFeed = mobileOverlayVisibility.showSocialFeed; const runtimeVisualProfile = useMemo(() => { - const nav = typeof navigator !== 'undefined' ? (navigator as Navigator & { deviceMemory?: number }) : null; + const nav = + typeof navigator !== 'undefined' + ? (navigator as Navigator & { deviceMemory?: number }) + : null; const deviceMemoryGb = nav && typeof nav.deviceMemory === 'number' ? nav.deviceMemory : 8; const hardwareConcurrency = nav?.hardwareConcurrency || 8; @@ -703,7 +756,8 @@ const App: React.FC = () => { // Fase 4: Paleta de colores dinámica const dynamicColors = useDynamicColors(stats, systems); - const artDirectionScenarioId = gameState === GameState.MENU ? selectedScenarioId : currentScenario.id; + const artDirectionScenarioId = + gameState === GameState.MENU ? selectedScenarioId : currentScenario.id; const artDirectionMode = gameState === GameState.MENU ? selectedGameMode : currentGameMode; const artDirectionProfile = useMemo( () => getScenarioArtDirectionProfile(artDirectionScenarioId), @@ -720,38 +774,36 @@ const App: React.FC = () => { }), [artDirectionMode, artDirectionScenarioId, gameState, reducedMotion, threatLevel] ); - const shellStyle = useMemo( - () => { - const sceneImage = getSceneBackgroundAsset(gameState, isMobileLayout); - const sceneImageOpacity = - gameState === GameState.MENU - ? 0.72 - : gameState === GameState.PLAYING || gameState === GameState.PAUSED - ? 0.44 - : 0.36; - - return { - ...artDirectionCssVariables, - ...getFxTextureCssVariables(), - ['--aaa-scene-image' as string]: `url('${sceneImage}')`, - ['--aaa-scene-image-opacity' as string]: `${sceneImageOpacity}`, - background: gameState === GameState.PLAYING + const shellStyle = useMemo(() => { + const sceneImage = getSceneBackgroundAsset(gameState, isMobileLayout); + const sceneImageOpacity = + gameState === GameState.MENU + ? 0.72 + : gameState === GameState.PLAYING || gameState === GameState.PAUSED + ? 0.44 + : 0.36; + + return { + ...artDirectionCssVariables, + ...getFxTextureCssVariables(), + ['--aaa-scene-image' as string]: `url('${sceneImage}')`, + ['--aaa-scene-image-opacity' as string]: `${sceneImageOpacity}`, + background: + gameState === GameState.PLAYING ? `linear-gradient(160deg, ${artDirectionProfile.shell.from} 0%, ${dynamicColors.bgColor} 52%, ${artDirectionProfile.shell.to} 100%)` : `linear-gradient(160deg, ${artDirectionProfile.shell.from}, ${artDirectionProfile.shell.mid} 56%, ${artDirectionProfile.shell.to})`, - filter: highContrastUi ? 'contrast(1.12) saturate(1.08)' : undefined - } as React.CSSProperties; - }, - [ - artDirectionCssVariables, - artDirectionProfile.shell.from, - artDirectionProfile.shell.mid, - artDirectionProfile.shell.to, - dynamicColors.bgColor, - gameState, - highContrastUi, - isMobileLayout - ] - ); + filter: highContrastUi ? 'contrast(1.12) saturate(1.08)' : undefined + } as React.CSSProperties; + }, [ + artDirectionCssVariables, + artDirectionProfile.shell.from, + artDirectionProfile.shell.mid, + artDirectionProfile.shell.to, + dynamicColors.bgColor, + gameState, + highContrastUi, + isMobileLayout + ]); const transitionTintClass: Record = { CYAN: 'from-cyan-400/35 via-sky-400/20 to-transparent', AMBER: 'from-amber-400/35 via-orange-400/20 to-transparent', @@ -766,20 +818,21 @@ const App: React.FC = () => { ? 'from-amber-400/75 via-orange-300/70 to-amber-400/75' : 'from-cyan-400/72 via-sky-300/65 to-cyan-400/72'; const impactOverlayToneClass = activeImpactFx?.success ? 'aaa-impact-success' : 'aaa-impact-fail'; - const freezeOverlayToneClass = activeFreezePulse?.tint === 'FAIL' ? 'aaa-freeze-fail' : 'aaa-freeze-success'; + const freezeOverlayToneClass = + activeFreezePulse?.tint === 'FAIL' ? 'aaa-freeze-fail' : 'aaa-freeze-success'; const menuToolbarClasses = useMemo( () => getMenuToolbarClasses(isMobileLayout, !isMobileLayout && isCompactViewport), [isCompactViewport, isMobileLayout] ); return ( -
{/* --- ATMOSPHERE LAYERS --- */}
@@ -789,35 +842,39 @@ const App: React.FC = () => {
{/* --- DAMAGE OVERLAYS --- */} - + {/* Video Failure Glitch */} {videoCritical && ( -
-
-
+
+
+
)} {/* Smoke Fog */} {stageSmoke && ( -
+
)} {(gameState === GameState.PLAYING || gameState === GameState.PAUSED) && (
)} @@ -825,9 +882,15 @@ const App: React.FC = () => { {activeTransition && (
-
+
{activeTransition.label} @@ -839,15 +902,17 @@ const App: React.FC = () => { {activeImpactFx && (
@@ -857,125 +922,145 @@ const App: React.FC = () => { {activeCameraPunch && (
)} {activeFreezePulse && (
)} {/* --- GAME UI --- */} {gameState === GameState.MENU && ( - <> - {}} - onRestart={() => {}} - onQuit={() => {}} - customStart={handleStartSession} - selectedScenarioId={selectedScenarioId} + <> + {}} + onRestart={() => {}} + onQuit={() => {}} + customStart={handleStartSession} + selectedScenarioId={selectedScenarioId} onSelectScenario={setSelectedScenarioId} selectedGameMode={selectedGameMode} onSelectGameMode={setSelectedGameMode} - scenarios={SCENARIOS} + scenarios={SCENARIOS} careerData={careerData} menuPanelOffsetClass={menuToolbarClasses.menuPanelOffset} isCompactLayout={isCompactViewport} isMobileLayout={isMobileLayout} reducedMotion={reducedMotion} - /> - {/* Fase 3: Botones de Logros y Mejoras */} -
- - - - -
- {showAchievements && ( - setShowAchievements(false)} - /> - )} - {showUpgrades && ( - { - const result = purchaseUpgrade(upgrade); - if (result.status === 'MISSING_REQUIREMENTS') { - const requiredNames = result.missingRequirements - .map(requirementId => PERMANENT_UPGRADES.find(item => item.id === requirementId)?.name || requirementId) - .join(', '); - addLog(`Mejora bloqueada. Requiere: ${requiredNames}`, 'warning'); - playError(); - } else if (result.status === 'INSUFFICIENT_POINTS') { - addLog(`No tienes suficientes puntos para ${upgrade.name}`, 'error'); - playError(); - } else if (result.status === 'ALREADY_UNLOCKED') { - addLog(`La mejora ${upgrade.name} ya está desbloqueada`, 'warning'); - } - }} - onClose={() => setShowUpgrades(false)} - /> - )} - + /> + {/* Fase 3: Botones de Logros y Mejoras */} +
+ + + + +
+ {showAchievements && ( + setShowAchievements(false)} + /> + )} + {showUpgrades && ( + { + const result = purchaseUpgrade(upgrade); + if (result.status === 'MISSING_REQUIREMENTS') { + const requiredNames = result.missingRequirements + .map( + (requirementId) => + PERMANENT_UPGRADES.find((item) => item.id === requirementId)?.name || + requirementId + ) + .join(', '); + addLog(`Mejora bloqueada. Requiere: ${requiredNames}`, 'warning'); + playError(); + } else if (result.status === 'INSUFFICIENT_POINTS') { + addLog(`No tienes suficientes puntos para ${upgrade.name}`, 'error'); + playError(); + } else if (result.status === 'ALREADY_UNLOCKED') { + addLog(`La mejora ${upgrade.name} ya está desbloqueada`, 'warning'); + } + }} + onClose={() => setShowUpgrades(false)} + /> + )} + )} {gameState === GameState.SHOP && ( - { playClick(); buyItem(item); }} - onStart={handleShopFinish} - /> + { + playClick(); + buyItem(item); + }} + onStart={handleShopFinish} + /> )} - + {gameState === GameState.PAUSED && ( { playClick(); togglePause(); }} + onResume={() => { + playClick(); + togglePause(); + }} onRestart={() => { playClick(); - const restartScenario = pendingStartData?.scenarioId || currentScenario.id || selectedScenarioId; + const restartScenario = + pendingStartData?.scenarioId || currentScenario.id || selectedScenarioId; const restartCrew = pendingStartData?.crewId || 'VETERAN'; const restartMode = pendingStartData?.gameMode || currentGameMode; handleStartSession(restartScenario, restartCrew, restartMode); }} - onQuit={() => { playClick(); quitGame(); }} + onQuit={() => { + playClick(); + quitGame(); + }} isCompactLayout={isCompactViewport} isMobileLayout={isMobileLayout} reducedMotion={reducedMotion} @@ -984,230 +1069,279 @@ const App: React.FC = () => { {(gameState === GameState.PLAYING || gameState === GameState.PAUSED) && ( <> - + + {/* Alarm Overlay */} +
80 ? 'opacity-100 animate-alarm' : 'opacity-0'}`} + style={{ boxShadow: 'inset 0 0 100px rgba(220, 38, 38, 0.4)' }} + >
+ + {!isMobileLayout && ( + <> + {/* MISSION PANEL */} + {activeMission && } + + {/* COMBO INDICATOR */} + + + {/* NARRATIVE POPUP (Fase 2) */} + {activeNarrative && ( + + )} + + {/* EARLY WARNING PANEL (Fase 2) */} + + + )} + + {isMobileLayout && ( +
+ {showMobileMission && activeMission && ( + + )} + {showMobilePrimaryEvent && mobilePrimaryEvent && ( +
+ {mobileQueuedEvents > 0 && ( +
+ + +{mobileQueuedEvents} EN COLA + +
+ )} + +
+ )} + {showMobileWarnings && ( + + )} + {showMobileNarrative && activeNarrative && ( + + )} + {showMobileClientPopup && ( + + )} + {showMobileCombo && } + {showMobileSocialFeed && } +
+ )} + + {/* TUTORIAL OVERLAY */} + {tutorialActive && TUTORIAL_STEPS[tutorialStepIndex] && ( + { + playClick(); + if (tutorialStepIndex === TUTORIAL_STEPS.length - 1) { + finishTutorial(); + } else { + advanceTutorial(); + } + }} /> - - {/* Alarm Overlay */} -
80 ? 'opacity-100 animate-alarm' : 'opacity-0'}`} - style={{ boxShadow: 'inset 0 0 100px rgba(220, 38, 38, 0.4)' }} - >
- - {!isMobileLayout && ( - <> - {/* MISSION PANEL */} - {activeMission && } - - {/* COMBO INDICATOR */} - - - {/* NARRATIVE POPUP (Fase 2) */} - {activeNarrative && ( - + )} + + {!isMobileLayout && ( + + )} + {!isMobileLayout && } + + {activeMinigame && ( + + )} + + {/* HEADER */} +
+
+ + {/* Clock LCD */} +
+
+ TIME + {stats.timeRemaining < 30 && ( + )} - - {/* EARLY WARNING PANEL (Fase 2) */} - - - )} - - {isMobileLayout && ( +
- {showMobileMission && activeMission && } - {showMobilePrimaryEvent && mobilePrimaryEvent && ( -
- {mobileQueuedEvents > 0 && ( -
- - +{mobileQueuedEvents} EN COLA - -
- )} - -
- )} - {showMobileWarnings && ( - - )} - {showMobileNarrative && activeNarrative && ( - - )} - {showMobileClientPopup && ( - - )} - {showMobileCombo && } - {showMobileSocialFeed && } + {formatTime(stats.timeRemaining)}
- )} - - {/* TUTORIAL OVERLAY */} - {tutorialActive && TUTORIAL_STEPS[tutorialStepIndex] && ( - { - playClick(); - if (tutorialStepIndex === TUTORIAL_STEPS.length - 1) { - finishTutorial(); - } else { - advanceTutorial(); - } - }} - /> - )} - - {!isMobileLayout && } - {!isMobileLayout && } - - {activeMinigame && ( - - )} - - {/* HEADER */} -
-
- - {/* Clock LCD */} -
-
- TIME - {stats.timeRemaining < 30 && } -
-
- {formatTime(stats.timeRemaining)} -
+
+ +
+ + + 80 ? dynamicColors.danger : dynamicColors.warning} + /> + +
+
+ Fondos
- -
- - - 80 ? dynamicColors.danger : dynamicColors.warning} /> - -
-
- Fondos -
-
- - {stats.budget.toLocaleString()} -
-
+
+ + {stats.budget.toLocaleString()}
+
+
- - - + + + +
+ +
+
+ {/* CENTER: 2D Visualizer */} +
+
- AUD {audioSpatialMode.slice(0, 4)} - - -
- -
-
- - {/* CENTER: 2D Visualizer */} -
-
- -
- -
- -
-
- - {/* RIGHT: Alerts */} - {!isMobileLayout && ( - - )} +
-
- +
+
+ + + {/* RIGHT: Alerts */} + {!isMobileLayout && ( + + )}
+ +
+ +
+
)} {showSettings && ( { playClick(); setShowSettings(false); }} + onClose={() => { + playClick(); + setShowSettings(false); + }} visualQualityMode={visualQualityMode} onVisualQualityChange={(mode) => { setVisualQualityMode(mode); @@ -1231,39 +1365,50 @@ const App: React.FC = () => { {/* GAME OVER OVERLAY */} {(gameState === GameState.GAME_OVER || gameState === GameState.VICTORY) && ( -
- {(() => { - const reason = getGameOverReason(); - const Icon = reason.icon; - return ( -
-
-
- -
-
-

- {reason.title} -

-

{reason.desc}

- - -
- ); - })()} -
+
+ {(() => { + const reason = getGameOverReason(); + const Icon = reason.icon; + return ( +
+
+
+ +
+
+

+ {reason.title} +

+

{reason.desc}

+ + +
+ ); + })()} +
)}
); diff --git a/components/AchievementPanel.tsx b/components/AchievementPanel.tsx index 2f5d939..ebc2e92 100644 --- a/components/AchievementPanel.tsx +++ b/components/AchievementPanel.tsx @@ -8,7 +8,11 @@ interface AchievementPanelProps { onClose: () => void; } -export const AchievementPanel: React.FC = ({ achievements, unlockedIds, onClose }) => { +export const AchievementPanel: React.FC = ({ + achievements, + unlockedIds, + onClose +}) => { const categoryColors = { PERFORMANCE: 'text-blue-400', ECONOMY: 'text-green-400', @@ -26,7 +30,10 @@ export const AchievementPanel: React.FC = ({ achievements className="bg-slate-900 border-2 border-slate-600 rounded-xl shadow-2xl p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto" >
-

+

@@ -41,7 +48,7 @@ export const AchievementPanel: React.FC = ({ achievements
- {achievements.map(achievement => { + {achievements.map((achievement) => { const isUnlocked = unlockedIds.includes(achievement.id); return (
= ({ message, mood, onClose, mobile = false }) => { +export const ClientPopup: React.FC = ({ + message, + mood, + onClose, + mobile = false +}) => { const [isVisible, setIsVisible] = useState(false); useEffect(() => { @@ -21,25 +26,34 @@ export const ClientPopup: React.FC = ({ message, mood, onClose }, 8000); return () => clearTimeout(timer); } + return undefined; }, [message, onClose]); if (!message && !isVisible) return null; const getStyles = () => { switch (mood) { - case 'ANGRY': return 'border-red-500 bg-red-950/90 shadow-red-900/50'; - case 'PANIC': return 'border-orange-500 bg-orange-950/90 shadow-orange-900/50'; - case 'HAPPY': return 'border-emerald-500 bg-emerald-950/90 shadow-emerald-900/50'; - default: return 'border-slate-500 bg-slate-900/90 shadow-slate-900/50'; + case 'ANGRY': + return 'border-red-500 bg-red-950/90 shadow-red-900/50'; + case 'PANIC': + return 'border-orange-500 bg-orange-950/90 shadow-orange-900/50'; + case 'HAPPY': + return 'border-emerald-500 bg-emerald-950/90 shadow-emerald-900/50'; + default: + return 'border-slate-500 bg-slate-900/90 shadow-slate-900/50'; } }; const getIconColor = () => { switch (mood) { - case 'ANGRY': return 'text-red-500 animate-pulse'; - case 'PANIC': return 'text-orange-500 animate-bounce'; - case 'HAPPY': return 'text-emerald-400'; - default: return 'text-slate-400'; + case 'ANGRY': + return 'text-red-500 animate-pulse'; + case 'PANIC': + return 'text-orange-500 animate-bounce'; + case 'HAPPY': + return 'text-emerald-400'; + default: + return 'text-slate-400'; } }; @@ -58,45 +72,53 @@ export const ClientPopup: React.FC = ({ message, mood, onClose aria-label="Mensaje del cliente" className={`border-l-4 p-4 rounded-r-lg shadow-2xl backdrop-blur-md relative overflow-hidden ${getStyles()}`} > - {/* Scanlines overlay specific to popup */} - +
- {/* Client Avatar Placeholder */} - + {/* Client Avatar Placeholder */} + + +
+
+ + {mood === 'ANGRY' + ? 'CLIENTE (FURIOSO)' + : mood === 'HAPPY' + ? 'CLIENTE (VIP)' + : 'INCOMING MSG'} + + +
-
-
- - {mood === 'ANGRY' ? 'CLIENTE (FURIOSO)' : mood === 'HAPPY' ? 'CLIENTE (VIP)' : 'INCOMING MSG'} - - -
- -
- "{message}" -
-
+
+ "{message}" +
+
{/* Decoration */}
- +
diff --git a/components/ComboIndicator.tsx b/components/ComboIndicator.tsx index 423bf1c..e69f83f 100644 --- a/components/ComboIndicator.tsx +++ b/components/ComboIndicator.tsx @@ -9,7 +9,7 @@ interface ComboIndicatorProps { export const ComboIndicator: React.FC = ({ comboState, mobile = false }) => { const { streakSeconds, multiplier, perfectRhythm } = comboState; - + if (multiplier <= 1.0 && !perfectRhythm) return null; const getMultiplierColor = () => { @@ -27,8 +27,12 @@ export const ComboIndicator: React.FC = ({ comboState, mobi }; return ( -
-
+
+
{perfectRhythm ? ( <> @@ -37,16 +41,16 @@ export const ComboIndicator: React.FC = ({ comboState, mobi
RITMO PERFECTO
-
- Sistemas sincronizados -
+
Sistemas sincronizados
) : ( <>
-
+
COMBO x{multiplier.toFixed(1)}
@@ -57,14 +61,14 @@ export const ComboIndicator: React.FC = ({ comboState, mobi )}
- + {/* Progress bar for next milestone */} {!perfectRhythm && (
-
diff --git a/components/EarlyWarningPanel.tsx b/components/EarlyWarningPanel.tsx index 89595e6..b4333c7 100644 --- a/components/EarlyWarningPanel.tsx +++ b/components/EarlyWarningPanel.tsx @@ -14,13 +14,17 @@ export const EarlyWarningPanel: React.FC = ({ maxItems }) => { if (warnings.length === 0) return null; - const visibleWarnings = typeof maxItems === 'number' ? warnings.slice(0, Math.max(1, maxItems)) : warnings; + const visibleWarnings = + typeof maxItems === 'number' ? warnings.slice(0, Math.max(1, maxItems)) : warnings; const getSeverityColor = (severity: 'LOW' | 'MEDIUM' | 'HIGH') => { switch (severity) { - case 'HIGH': return 'border-red-500 bg-red-950/80'; - case 'MEDIUM': return 'border-orange-500 bg-orange-950/80'; - case 'LOW': return 'border-yellow-500 bg-yellow-950/80'; + case 'HIGH': + return 'border-red-500 bg-red-950/80'; + case 'MEDIUM': + return 'border-orange-500 bg-orange-950/80'; + case 'LOW': + return 'border-yellow-500 bg-yellow-950/80'; } }; @@ -31,7 +35,7 @@ export const EarlyWarningPanel: React.FC = ({ aria-label="Advertencias tempranas" className={`${mobile ? 'relative w-full shrink-0 space-y-2 pointer-events-auto' : 'absolute top-20 left-2 right-2 md:left-8 md:right-auto z-[100] md:w-80 space-y-2 max-h-[calc(100vh-120px)] overflow-y-auto'}`} > - {visibleWarnings.map(warning => ( + {visibleWarnings.map((warning) => (
= ({
-