diff --git a/src/App.css b/src/App.css index e5fc43a..3299777 100644 --- a/src/App.css +++ b/src/App.css @@ -36,6 +36,65 @@ font-size: 14px; } +.exit-menu-btn { + padding: 8px 12px; + background: rgba(0, 0, 0, 0.25); + border: 3px solid var(--panel-border); + color: var(--text); + font-family: inherit; + font-size: 12px; + cursor: pointer; +} + +.exit-menu-btn:hover { + border-color: var(--accent); + color: var(--accent); +} + +.main-menu { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 40px; + background: var(--panel-bg); +} + +.main-menu__title { + font-size: 32px; + margin: 0; + color: var(--accent); + text-shadow: 4px 4px 0 var(--panel-border); +} + +.main-menu__buttons { + display: flex; + flex-direction: column; + gap: 16px; + width: 260px; +} + +.menu-btn { + padding: 16px; + background: rgba(0, 0, 0, 0.25); + border: 4px solid var(--panel-border); + color: var(--text); + font-family: inherit; + font-size: 14px; + cursor: pointer; +} + +.menu-btn:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} + +.menu-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + .main-row { display: flex; flex-direction: row; diff --git a/src/App.tsx b/src/App.tsx index 86cd591..f97ca5b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,37 @@ import { useState } from 'react'; import GameMap from './components/GameMap'; +import MainMenu from './components/MainMenu'; import Sidebar from './components/Sidebar'; import Topbar from './components/Topbar'; import { GameStore } from './game/GameStore'; +import { clearSave } from './game/persistence'; import './App.css'; +type Screen = 'menu' | 'game'; + function App() { const [store] = useState(() => new GameStore()); + // Always starts on the menu, regardless of whether a save exists — loading straight into a + // resumed game on every page (re)load isn't what was asked for. + const [screen, setScreen] = useState('menu'); + + function handleNewGame() { + clearSave(); + setScreen('game'); + } + + function handleExitToMenu() { + store.save(); + setScreen('menu'); + } + + if (screen === 'menu') { + return setScreen('game')} />; + } return (
- +
diff --git a/src/components/MainMenu.tsx b/src/components/MainMenu.tsx new file mode 100644 index 0000000..7c2066a --- /dev/null +++ b/src/components/MainMenu.tsx @@ -0,0 +1,29 @@ +import { useState } from 'react'; +import { hasSave } from '../game/persistence'; + +interface Props { + onNewGame: () => void; + onContinue: () => void; +} + +function MainMenu({ onNewGame, onContinue }: Props) { + // Computed once per mount — the menu remounts fresh every time the player returns to it (see + // App), so this naturally stays in sync with whether a save exists. + const [canContinue] = useState(hasSave); + + return ( +
+

Forest

+
+ + +
+
+ ); +} + +export default MainMenu; diff --git a/src/components/Topbar.tsx b/src/components/Topbar.tsx index 5d01c7b..b60ddab 100644 --- a/src/components/Topbar.tsx +++ b/src/components/Topbar.tsx @@ -5,9 +5,10 @@ import PixelSprite from './PixelSprite'; interface Props { store: GameStore; + onExitToMenu: () => void; } -function Topbar({ store }: Props) { +function Topbar({ store, onExitToMenu }: Props) { return (

Forest

@@ -27,6 +28,9 @@ function Topbar({ store }: Props) { {store.inventory.coins.toFixed(1)}
+
); } diff --git a/src/game/persistence.ts b/src/game/persistence.ts index c7551bd..68222f4 100644 --- a/src/game/persistence.ts +++ b/src/game/persistence.ts @@ -53,3 +53,17 @@ export function writeSave(data: SaveData): void { // Ignore — e.g. private browsing mode or quota exceeded. } } + +/** Deletes the saved game, if any — used when starting a brand new game. */ +export function clearSave(): void { + try { + localStorage.removeItem(STORAGE_KEY); + } catch { + // Ignore — e.g. private browsing mode. + } +} + +/** Whether a valid, resumable save currently exists (used to enable/disable "Continue" in the menu). */ +export function hasSave(): boolean { + return loadSave() !== null; +}