главный экран
This commit is contained in:
+59
@@ -36,6 +36,65 @@
|
|||||||
font-size: 14px;
|
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 {
|
.main-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
+22
-1
@@ -1,16 +1,37 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import GameMap from './components/GameMap';
|
import GameMap from './components/GameMap';
|
||||||
|
import MainMenu from './components/MainMenu';
|
||||||
import Sidebar from './components/Sidebar';
|
import Sidebar from './components/Sidebar';
|
||||||
import Topbar from './components/Topbar';
|
import Topbar from './components/Topbar';
|
||||||
import { GameStore } from './game/GameStore';
|
import { GameStore } from './game/GameStore';
|
||||||
|
import { clearSave } from './game/persistence';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
|
type Screen = 'menu' | 'game';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [store] = useState(() => new GameStore());
|
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 (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Topbar store={store} />
|
<Topbar store={store} onExitToMenu={handleExitToMenu} />
|
||||||
<div className="main-row">
|
<div className="main-row">
|
||||||
<Sidebar store={store} />
|
<Sidebar store={store} />
|
||||||
<GameMap 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 {
|
interface Props {
|
||||||
store: GameStore;
|
store: GameStore;
|
||||||
|
onExitToMenu: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Topbar({ store }: Props) {
|
function Topbar({ store, onExitToMenu }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="topbar">
|
<div className="topbar">
|
||||||
<h1 className="topbar__title">Forest</h1>
|
<h1 className="topbar__title">Forest</h1>
|
||||||
@@ -27,6 +28,9 @@ function Topbar({ store }: Props) {
|
|||||||
<PixelSprite pixels={COIN_SPRITE} size={24} />
|
<PixelSprite pixels={COIN_SPRITE} size={24} />
|
||||||
<span>{store.inventory.coins.toFixed(1)}</span>
|
<span>{store.inventory.coins.toFixed(1)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" className="exit-menu-btn" onClick={onExitToMenu}>
|
||||||
|
Выйти в меню
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,3 +53,17 @@ export function writeSave(data: SaveData): void {
|
|||||||
// Ignore — e.g. private browsing mode or quota exceeded.
|
// 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