главный экран
This commit is contained in:
+59
@@ -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;
|
||||
|
||||
+22
-1
@@ -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<Screen>('menu');
|
||||
|
||||
function handleNewGame() {
|
||||
clearSave();
|
||||
setScreen('game');
|
||||
}
|
||||
|
||||
function handleExitToMenu() {
|
||||
store.save();
|
||||
setScreen('menu');
|
||||
}
|
||||
|
||||
if (screen === 'menu') {
|
||||
return <MainMenu onNewGame={handleNewGame} onContinue={() => setScreen('game')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Topbar store={store} />
|
||||
<Topbar store={store} onExitToMenu={handleExitToMenu} />
|
||||
<div className="main-row">
|
||||
<Sidebar store={store} />
|
||||
<GameMap store={store} />
|
||||
|
||||
@@ -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 (
|
||||
<div className="main-menu">
|
||||
<h1 className="main-menu__title">Forest</h1>
|
||||
<div className="main-menu__buttons">
|
||||
<button type="button" className="menu-btn" onClick={onNewGame}>
|
||||
Новая игра
|
||||
</button>
|
||||
<button type="button" className="menu-btn" disabled={!canContinue} onClick={onContinue}>
|
||||
Продолжить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MainMenu;
|
||||
@@ -5,9 +5,10 @@ import PixelSprite from './PixelSprite';
|
||||
|
||||
interface Props {
|
||||
store: GameStore;
|
||||
onExitToMenu: () => void;
|
||||
}
|
||||
|
||||
function Topbar({ store }: Props) {
|
||||
function Topbar({ store, onExitToMenu }: Props) {
|
||||
return (
|
||||
<div className="topbar">
|
||||
<h1 className="topbar__title">Forest</h1>
|
||||
@@ -27,6 +28,9 @@ function Topbar({ store }: Props) {
|
||||
<PixelSprite pixels={COIN_SPRITE} size={24} />
|
||||
<span>{store.inventory.coins.toFixed(1)}</span>
|
||||
</div>
|
||||
<button type="button" className="exit-menu-btn" onClick={onExitToMenu}>
|
||||
Выйти в меню
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user