Split Rust game src into modules; update AGENTS.md for dual toolchain
- Refactor apps/game/src/main.rs into modules: state.rs, camera.rs, ui/, gameplay/ - Add gameplay/galaxy_creation.rs scaffold (stubs only, no new logic) - Update root AGENTS.md: separate TS workspace vs Rust game commands - Update apps/AGENTS.md: docs/game/site use two toolchains, not one - Add apps/game/AGENTS.md: build commands, module layout, naming conventions, Plugin pattern
This commit is contained in:
51
AGENTS.md
51
AGENTS.md
@@ -6,37 +6,54 @@ Single-player open-world space exploration RPG. Windward Horizon in space with F
|
|||||||
|
|
||||||
| Path | Purpose |
|
| Path | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `apps/docs` | Living design docs, interactive demos, and vertical-slice prototype |
|
| `apps/docs` | Living design docs, interactive demos, and vertical-slice prototype (Vite + React) |
|
||||||
| `apps/game` | Playable game shell connected to SpacetimeDB |
|
| `apps/game` | Playable game client — **Rust + Bevy** (independent of the JS/TS workspace) |
|
||||||
| `apps/site` | Public landing page |
|
| `apps/site` | Public landing page (Vite + React) |
|
||||||
| `packages/ui` | Shared UI primitives and design tokens |
|
| `packages/ui` | Shared UI primitives and design tokens for the TS apps |
|
||||||
| `services/spacetimedb` | SpacetimeDB TypeScript backend module |
|
| `services/spacetimedb` | SpacetimeDB TypeScript backend module (authoritative game state) |
|
||||||
| `scripts/` | Dev tooling (dev.sh startup script) |
|
| `scripts/` | Dev tooling (dev.sh startup script) |
|
||||||
| `archive/` | Legacy pre-monorepo files kept for reference |
|
| `archive/` | Legacy pre-monorepo files kept for reference |
|
||||||
| `src/module_bindings/` | Empty; generated bindings go to `apps/game/src/module_bindings` |
|
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
- `package.json` — pnpm workspace scripts (`dev`, `build`, `check`, `generate:bindings`)
|
- `package.json` — pnpm workspace scripts (`dev`, `build`, `check`, `generate:bindings`)
|
||||||
- `pnpm-workspace.yaml` — workspace glob patterns
|
- `pnpm-workspace.yaml` — workspace glob patterns (covers TS apps only)
|
||||||
- `spacetime.json` — SpacetimeDB module path and binding generation config
|
- `spacetime.json` — SpacetimeDB module path and binding generation config
|
||||||
- `tsconfig.base.json` — Shared TypeScript config (ES2020, React JSX)
|
- `tsconfig.base.json` — Shared TypeScript config (ES2020, React JSX) — does not apply to `apps/game`
|
||||||
|
- `apps/game/Cargo.toml` — Rust manifest for the game binary
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
### JS/TS workspace (docs, site, packages/ui, services)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # Full stack: SpacetimeDB + game (port 5175)
|
pnpm dev # Full stack: SpacetimeDB + docs/site
|
||||||
pnpm dev:docs # Docs only (port 5173)
|
pnpm dev:docs # Docs only (port 5173)
|
||||||
pnpm dev:site # Site only (port 5174)
|
pnpm dev:site # Site only (port 5174)
|
||||||
pnpm dev:game # Game frontend only (requires running SpacetimeDB)
|
pnpm build # Build all TS packages
|
||||||
pnpm build # Build all apps
|
pnpm check # Typecheck all TS packages
|
||||||
pnpm check # Typecheck all packages
|
|
||||||
pnpm generate:bindings # Regenerate SpacetimeDB TypeScript bindings
|
pnpm generate:bindings # Regenerate SpacetimeDB TypeScript bindings
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conventions
|
### Rust game (`apps/game`)
|
||||||
|
|
||||||
- SpacetimeDB is the persistence layer from day 1 — no localStorage for game state
|
```bash
|
||||||
- All UI uses Tailwind CSS v4 with custom design tokens from `packages/ui`
|
cd apps/game
|
||||||
- 3D rendering uses React Three Fiber + Drei + Three.js
|
cargo run # Build + run the game binary
|
||||||
- Module bindings are auto-generated — never edit `module_bindings/` by hand
|
cargo check # Fast typecheck
|
||||||
|
cargo build # Build without running
|
||||||
|
cargo clippy # Lint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Notes
|
||||||
|
|
||||||
|
- **Two independent toolchains coexist**: the pnpm/TS workspace (docs, site, UI lib, SpacetimeDB backend) and the Cargo/Rust game client. They share no code.
|
||||||
|
- **SpacetimeDB** remains the authoritative persistence layer. The game client will connect to it via the SpacetimeDB Rust SDK when wired up (TS bindings generated by `pnpm generate:bindings` are consumed by the docs prototype, not the game).
|
||||||
|
- **Naming follows RFC 344**: see `apps/game/AGENTS.md` for Rust/Bevy conventions.
|
||||||
|
|
||||||
|
## Conventions (TS apps)
|
||||||
|
|
||||||
|
- SpacetimeDB is the persistence layer from day 1 — no localStorage for game state in the docs prototype
|
||||||
|
- TS apps use Tailwind CSS v4 with custom design tokens from `packages/ui`
|
||||||
|
- 3D rendering in `apps/docs` uses React Three Fiber + Drei + Three.js
|
||||||
|
- Module bindings are auto-generated — never edit generated bindings by hand
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
# apps/ — Application Packages
|
# apps/ — Application Packages
|
||||||
|
|
||||||
Three Vite + React apps, each independently buildable and dev-serveable.
|
Three independent applications. **Note**: they do not share a toolchain.
|
||||||
|
|
||||||
| App | Port | Package | Description |
|
| App | Toolchain | Port / Binary | Description |
|
||||||
|-----|------|---------|-------------|
|
|-----|-----------|---------------|-------------|
|
||||||
| `docs` | 5173 | `@void-nav/docs` | Living design docs, 16 doc pages, 14 interactive demos, game-slice prototype |
|
| `docs` | Vite + React + TS | 5173 | Living design docs, 16 doc pages, 14 interactive demos, game-slice prototype |
|
||||||
| `game` | 5175 | `@void-nav/game` | Playable single-player game client connected to SpacetimeDB backend |
|
| `game` | Rust + Bevy | `cargo run` | Single-player game client. Standalone — does not depend on the pnpm workspace |
|
||||||
| `site` | 5174 | `@void-nav/site` | Public landing page / marketing site |
|
| `site` | Vite + React + TS | 5174 | Public landing page / marketing site |
|
||||||
|
|
||||||
## Shared
|
## TS apps (`docs`, `site`)
|
||||||
|
|
||||||
- All use Tailwind CSS v4 and `@void-nav/ui` for shared primitives
|
- Vite 7, React 18, TypeScript 5.8
|
||||||
- All use Vite 7 as build tool
|
- Tailwind CSS v4 with `@void-nav/ui` shared primitives
|
||||||
- All use React 18 with TypeScript 5.8
|
- `docs` also uses Three.js + React Three Fiber for 3D scenes
|
||||||
- `docs` and `game` also use Three.js + React Three Fiber for 3D scenes
|
- Managed via pnpm workspace scripts (`pnpm dev:docs`, `pnpm dev:site`, etc.)
|
||||||
|
|
||||||
|
## Rust app (`game`)
|
||||||
|
|
||||||
|
- Bevy 0.16, Rust edition 2021
|
||||||
|
- Built and run via `cargo` (no Vite, no pnpm scripts)
|
||||||
|
- See [`apps/game/AGENTS.md`](./game/AGENTS.md) for module layout, naming conventions, and build commands
|
||||||
|
- Will connect to SpacetimeDB via the Rust SDK when persistence is wired up
|
||||||
|
|||||||
78
apps/game/AGENTS.md
Normal file
78
apps/game/AGENTS.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# apps/game — Bevy/Rust Game Client
|
||||||
|
|
||||||
|
Single-player space exploration RPG built with [Bevy 0.16](https://bevyengine.org).
|
||||||
|
Crate name: `void-nav`.
|
||||||
|
|
||||||
|
This app is **independent of the pnpm/TS workspace** — no Vite, no React, no Tailwind. It builds with plain `cargo` from this directory.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run # Build + launch the game
|
||||||
|
cargo check # Fast typecheck (no codegen)
|
||||||
|
cargo build # Build without running
|
||||||
|
cargo clippy # Lint
|
||||||
|
cargo test # Run unit tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.rs # App builder: plugins, state init, system registration
|
||||||
|
├── state.rs # AppState enum (MainMenu, GalaxyCreation, CharacterCreation, InGame, Options)
|
||||||
|
├── camera.rs # Camera spawn
|
||||||
|
├── ui/ # UI screens (menus, HUD, etc.)
|
||||||
|
│ ├── mod.rs
|
||||||
|
│ └── main_menu.rs
|
||||||
|
└── gameplay/ # Non-UI gameplay systems
|
||||||
|
├── mod.rs
|
||||||
|
└── galaxy_creation.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to add a file vs a folder
|
||||||
|
|
||||||
|
- **Default**: one file per feature (e.g. `main_menu.rs`, `galaxy_creation.rs`).
|
||||||
|
- **Promote to a folder** when the file exceeds ~300 lines, mixes UI + logic + data, or needs shared private helpers.
|
||||||
|
- A promoted folder should expose its public API via `mod.rs` and ideally bundle its systems into a Bevy `Plugin` (see pattern below).
|
||||||
|
|
||||||
|
### Plugin pattern (recommended once a folder exists)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/gameplay/galaxy_creation/mod.rs
|
||||||
|
pub struct GalaxyCreationPlugin;
|
||||||
|
|
||||||
|
impl Plugin for GalaxyCreationPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(OnEnter(AppState::GalaxyCreation), setup_galaxy_creation)
|
||||||
|
.add_systems(OnExit(AppState::GalaxyCreation), despawn_galaxy_creation)
|
||||||
|
.add_systems(Update, (
|
||||||
|
/* update systems */
|
||||||
|
).run_if(in_state(AppState::GalaxyCreation)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then `main.rs` collapses to `app.add_plugins((..., GalaxyCreationPlugin))`.
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
Follows [Rust RFC 344](https://rust-lang.github.io/api-guidelines/naming.html). Bevy layers these additional conventions:
|
||||||
|
|
||||||
|
| Item | Convention | Example |
|
||||||
|
|---|---|---|
|
||||||
|
| Files, modules, directories | `snake_case` | `galaxy_creation.rs`, `main_menu.rs` |
|
||||||
|
| Crate name | `snake_case` | `void-nav` |
|
||||||
|
| Structs, enums, enum variants | `UpperCamelCase` | `AppState`, `GalaxyCreation` |
|
||||||
|
| Components | `UpperCamelCase` (suffix optional) | `Player`, `MainMenuUi` |
|
||||||
|
| Resources | `UpperCamelCase` | `ClearColor`, `Time` |
|
||||||
|
| States | `UpperCamelCase` + `State` suffix | `AppState`, `GameState` |
|
||||||
|
| Plugins | `UpperCamelCase` + `Plugin` suffix | `GalaxyCreationPlugin` |
|
||||||
|
| Functions, systems, locals | `snake_case`, verb-first | `spawn_camera`, `setup_main_menu` |
|
||||||
|
| Constants, statics | `SCREAMING_SNAKE_CASE` | `MAX_HEALTH` |
|
||||||
|
|
||||||
|
## Architecture Notes
|
||||||
|
|
||||||
|
- **State machine driven**: each `AppState` variant has its own UI/systems wired via `OnEnter` / `OnExit` / `Update` (latter guarded by `in_state`).
|
||||||
|
- **Persistence**: SpacetimeDB will be the authoritative backend (via the Rust SDK, not the TS bindings). Not yet wired up.
|
||||||
|
- **No `module_bindings/` here**: TS bindings live in the TS workspace and are consumed by the docs prototype.
|
||||||
5
apps/game/src/camera.rs
Normal file
5
apps/game/src/camera.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub fn spawn_camera(mut commands: Commands) {
|
||||||
|
commands.spawn(Camera2d);
|
||||||
|
}
|
||||||
18
apps/game/src/gameplay/galaxy_creation.rs
Normal file
18
apps/game/src/gameplay/galaxy_creation.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
// ── Markers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct GalaxyCreationUi;
|
||||||
|
|
||||||
|
// ── Galaxy Creation Screen ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn setup_galaxy_creation(_commands: Commands) {
|
||||||
|
// TODO: spawn galaxy creation UI
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn despawn_galaxy_creation(mut commands: Commands, query: Query<Entity, With<GalaxyCreationUi>>) {
|
||||||
|
for entity in &query {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
1
apps/game/src/gameplay/mod.rs
Normal file
1
apps/game/src/gameplay/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod galaxy_creation;
|
||||||
@@ -1,176 +1,24 @@
|
|||||||
|
mod camera;
|
||||||
|
mod gameplay;
|
||||||
|
mod state;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use gameplay::galaxy_creation;
|
||||||
|
use state::AppState;
|
||||||
|
use ui::main_menu;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugins(DefaultPlugins)
|
.add_plugins(DefaultPlugins)
|
||||||
.insert_resource(ClearColor(Color::srgb(0.02, 0.02, 0.06)))
|
.insert_resource(ClearColor(Color::srgb(0.02, 0.02, 0.06)))
|
||||||
.init_state::<AppState>()
|
.init_state::<AppState>()
|
||||||
.add_systems(Startup, spawn_camera)
|
.add_systems(Startup, camera::spawn_camera)
|
||||||
.add_systems(OnEnter(AppState::MainMenu), setup_main_menu)
|
.add_systems(OnEnter(AppState::MainMenu), main_menu::setup_main_menu)
|
||||||
.add_systems(OnExit(AppState::MainMenu), despawn_main_menu)
|
.add_systems(OnExit(AppState::MainMenu), main_menu::despawn_main_menu)
|
||||||
.add_systems(Update, main_menu_buttons)
|
.add_systems(Update, main_menu::main_menu_buttons)
|
||||||
|
.add_systems(OnEnter(AppState::GalaxyCreation), galaxy_creation::setup_galaxy_creation)
|
||||||
|
.add_systems(OnExit(AppState::GalaxyCreation), galaxy_creation::despawn_galaxy_creation)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── States ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq, Debug, Hash, States, Default)]
|
|
||||||
enum AppState {
|
|
||||||
#[default]
|
|
||||||
MainMenu,
|
|
||||||
InGame,
|
|
||||||
Options,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Markers ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
struct MainMenuUi;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
enum MenuButton {
|
|
||||||
Start,
|
|
||||||
Options,
|
|
||||||
Exit,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Camera ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
fn spawn_camera(mut commands: Commands) {
|
|
||||||
commands.spawn(Camera2d);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Main Menu ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
fn setup_main_menu(mut commands: Commands) {
|
|
||||||
let button_style = Style {
|
|
||||||
width: Val::Px(280.0),
|
|
||||||
height: Val::Px(64.0),
|
|
||||||
justify_content: JustifyContent::Center,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
..default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let button_text_font = TextFont {
|
|
||||||
font_size: 28.0,
|
|
||||||
..default()
|
|
||||||
};
|
|
||||||
|
|
||||||
commands.spawn((
|
|
||||||
Node {
|
|
||||||
width: Val::Percent(100.0),
|
|
||||||
height: Val::Percent(100.0),
|
|
||||||
display: Display::Flex,
|
|
||||||
flex_direction: FlexDirection::Column,
|
|
||||||
justify_content: JustifyContent::Center,
|
|
||||||
align_items: AlignItems::Center,
|
|
||||||
row_gap: Val::Px(24.0),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
BackgroundColor(Color::srgb(0.02, 0.02, 0.06)),
|
|
||||||
MainMenuUi,
|
|
||||||
)).with_children(|parent| {
|
|
||||||
// Title
|
|
||||||
parent.spawn((
|
|
||||||
Text::new("VOID::NAV"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 72.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::srgb(0.7, 0.85, 1.0)),
|
|
||||||
Node {
|
|
||||||
margin: UiRect::bottom(Val::Px(48.0)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
// Subtitle
|
|
||||||
parent.spawn((
|
|
||||||
Text::new("A space exploration RPG"),
|
|
||||||
TextFont {
|
|
||||||
font_size: 18.0,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
TextColor(Color::srgb(0.4, 0.5, 0.6)),
|
|
||||||
Node {
|
|
||||||
margin: UiRect::bottom(Val::Px(32.0)),
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
|
|
||||||
// Start Game button
|
|
||||||
spawn_menu_button(
|
|
||||||
&mut parent.spawn_empty(),
|
|
||||||
"Start Game",
|
|
||||||
MenuButton::Start,
|
|
||||||
&button_style,
|
|
||||||
&button_text_font,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Options button
|
|
||||||
spawn_menu_button(
|
|
||||||
&mut parent.spawn_empty(),
|
|
||||||
"Options",
|
|
||||||
MenuButton::Options,
|
|
||||||
&button_style,
|
|
||||||
&button_text_font,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Exit button
|
|
||||||
spawn_menu_button(
|
|
||||||
&mut parent.spawn_empty(),
|
|
||||||
"Exit",
|
|
||||||
MenuButton::Exit,
|
|
||||||
&button_style,
|
|
||||||
&button_text_font,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_menu_button(
|
|
||||||
cmd: &mut EntityCommands,
|
|
||||||
label: &str,
|
|
||||||
marker: MenuButton,
|
|
||||||
style: &Style,
|
|
||||||
text_font: &TextFont,
|
|
||||||
) {
|
|
||||||
cmd.insert((
|
|
||||||
Button,
|
|
||||||
style.clone(),
|
|
||||||
BackgroundColor(Color::srgb(0.08, 0.1, 0.18)),
|
|
||||||
BorderColor(Color::srgb(0.3, 0.45, 0.7)),
|
|
||||||
BorderRadius::all(Val::Px(8.0)),
|
|
||||||
marker,
|
|
||||||
))
|
|
||||||
.with_children(|btn| {
|
|
||||||
btn.spawn((
|
|
||||||
Text::new(label),
|
|
||||||
text_font.clone(),
|
|
||||||
TextColor(Color::srgb(0.75, 0.85, 1.0)),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn despawn_main_menu(mut commands: Commands, query: Query<Entity, With<MainMenuUi>>) {
|
|
||||||
for entity in &query {
|
|
||||||
commands.entity(entity).despawn_recursive();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Button Interaction ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
fn main_menu_buttons(
|
|
||||||
mut next_state: ResMut<NextState<AppState>>,
|
|
||||||
mut exit: EventWriter<AppExit>,
|
|
||||||
interaction_query: Query<(&Interaction, &MenuButton), Changed<Interaction>>,
|
|
||||||
) {
|
|
||||||
for (interaction, button) in &interaction_query {
|
|
||||||
if *interaction == Interaction::Pressed {
|
|
||||||
match button {
|
|
||||||
MenuButton::Start => next_state.set(AppState::InGame),
|
|
||||||
MenuButton::Options => next_state.set(AppState::Options),
|
|
||||||
MenuButton::Exit => exit.send(AppExit::Success),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
11
apps/game/src/state.rs
Normal file
11
apps/game/src/state.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Eq, PartialEq, Debug, Hash, States, Default)]
|
||||||
|
pub enum AppState {
|
||||||
|
#[default]
|
||||||
|
MainMenu,
|
||||||
|
GalaxyCreation,
|
||||||
|
CharacterCreation,
|
||||||
|
InGame,
|
||||||
|
Options,
|
||||||
|
}
|
||||||
156
apps/game/src/ui/main_menu.rs
Normal file
156
apps/game/src/ui/main_menu.rs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
// ── Markers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct MainMenuUi;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub enum MenuButton {
|
||||||
|
ContinueGame, //Needed for later once save game functionality is implemented, should have a check to see if a save game exists and only show this button if it does
|
||||||
|
NewGame,
|
||||||
|
Options,
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main Menu ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn setup_main_menu(mut commands: Commands) {
|
||||||
|
let button_style = Node {
|
||||||
|
width: Val::Px(280.0),
|
||||||
|
height: Val::Px(64.0),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let button_text_font = TextFont {
|
||||||
|
font_size: 28.0,
|
||||||
|
..default()
|
||||||
|
};
|
||||||
|
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
width: Val::Percent(100.0),
|
||||||
|
height: Val::Percent(100.0),
|
||||||
|
display: Display::Flex,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
row_gap: Val::Px(24.0),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::srgb(0.02, 0.02, 0.06)),
|
||||||
|
MainMenuUi,
|
||||||
|
))
|
||||||
|
.with_children(|parent| {
|
||||||
|
// Title
|
||||||
|
parent.spawn((
|
||||||
|
Text::new("VOID::NAV"),
|
||||||
|
TextFont {
|
||||||
|
font_size: 72.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(Color::srgb(0.7, 0.85, 1.0)),
|
||||||
|
Node {
|
||||||
|
margin: UiRect::bottom(Val::Px(48.0)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Subtitle
|
||||||
|
parent.spawn((
|
||||||
|
Text::new("A space exploration RPG"),
|
||||||
|
TextFont {
|
||||||
|
font_size: 18.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(Color::srgb(0.4, 0.5, 0.6)),
|
||||||
|
Node {
|
||||||
|
margin: UiRect::bottom(Val::Px(32.0)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Start Game button
|
||||||
|
spawn_menu_button(
|
||||||
|
&mut parent.spawn_empty(),
|
||||||
|
"Start Game",
|
||||||
|
MenuButton::NewGame,
|
||||||
|
&button_style,
|
||||||
|
&button_text_font,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Options button
|
||||||
|
spawn_menu_button(
|
||||||
|
&mut parent.spawn_empty(),
|
||||||
|
"Options",
|
||||||
|
MenuButton::Options,
|
||||||
|
&button_style,
|
||||||
|
&button_text_font,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Exit button
|
||||||
|
spawn_menu_button(
|
||||||
|
&mut parent.spawn_empty(),
|
||||||
|
"Exit",
|
||||||
|
MenuButton::Exit,
|
||||||
|
&button_style,
|
||||||
|
&button_text_font,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_menu_button(
|
||||||
|
cmd: &mut EntityCommands,
|
||||||
|
label: &str,
|
||||||
|
marker: MenuButton,
|
||||||
|
style: &Node,
|
||||||
|
text_font: &TextFont,
|
||||||
|
) {
|
||||||
|
cmd.insert((
|
||||||
|
Button,
|
||||||
|
style.clone(),
|
||||||
|
BackgroundColor(Color::srgb(0.08, 0.1, 0.18)),
|
||||||
|
BorderColor(Color::srgb(0.3, 0.45, 0.7)),
|
||||||
|
BorderRadius::all(Val::Px(8.0)),
|
||||||
|
marker,
|
||||||
|
))
|
||||||
|
.with_children(|btn| {
|
||||||
|
btn.spawn((
|
||||||
|
Text::new(label),
|
||||||
|
text_font.clone(),
|
||||||
|
TextColor(Color::srgb(0.75, 0.85, 1.0)),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn despawn_main_menu(mut commands: Commands, query: Query<Entity, With<MainMenuUi>>) {
|
||||||
|
for entity in &query {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Button Interaction ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn main_menu_buttons(
|
||||||
|
mut next_state: ResMut<NextState<AppState>>,
|
||||||
|
mut exit: EventWriter<AppExit>,
|
||||||
|
interaction_query: Query<(&Interaction, &MenuButton), Changed<Interaction>>,
|
||||||
|
) {
|
||||||
|
for (interaction, button) in &interaction_query {
|
||||||
|
if *interaction == Interaction::Pressed {
|
||||||
|
match button {
|
||||||
|
MenuButton::ContinueGame => next_state.set(AppState::InGame), // Placeholder for now
|
||||||
|
MenuButton::NewGame => next_state.set(AppState::GalaxyCreation),
|
||||||
|
MenuButton::Options => next_state.set(AppState::Options),
|
||||||
|
MenuButton::Exit => {
|
||||||
|
exit.write(AppExit::Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
apps/game/src/ui/mod.rs
Normal file
1
apps/game/src/ui/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod main_menu;
|
||||||
Reference in New Issue
Block a user