feat: menu system, basic state handling, more code splitting/cleanup
This commit is contained in:
parent
391ef7fd8b
commit
b8dde775f1
@ -7,7 +7,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
bevy = { version = "0.14.0", features = ["dynamic_linking"] }
|
||||
bevy = { version = "0.14.0", features = ["dynamic_linking", "subpixel_glyph_atlas"] }
|
||||
itertools = "0.13.0"
|
||||
log = { version = "*", features = ["max_level_debug", "release_max_level_warn"] }
|
||||
rand = "0.8.5"
|
||||
|
||||
23
src/common.rs
Normal file
23
src/common.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
pub fn button_system (
|
||||
mut interaction_query: Query<
|
||||
(&Interaction, &mut BackgroundColor),
|
||||
(Changed<Interaction>, With<Button>),
|
||||
>,
|
||||
) {
|
||||
for (interaction, mut color) in &mut interaction_query {
|
||||
*color = BackgroundColor(match *interaction {
|
||||
Interaction::Pressed => BUTTON_BG_COLOR_HOVER,
|
||||
Interaction::Hovered => BUTTON_BG_COLOR_HOVER,
|
||||
Interaction::None => BUTTON_BG_COLOR,
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pub fn despawn_screen <T: Component> (to_despawn: Query<Entity, With<T>>, mut commands: Commands) {
|
||||
for entity in &to_despawn {
|
||||
commands.entity(entity).despawn_recursive();
|
||||
}
|
||||
}
|
||||
151
src/game.rs
Normal file
151
src/game.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
fn spawn_square_at (commands: &mut Commands, level: u32, position: usize, asset_server: AssetServer) {
|
||||
let font = asset_server.load("proxima_thin.ttf");
|
||||
let Some(&(x, y)) = sq_origins.get(position) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let text = 2i32.pow(level);
|
||||
let label = Text2dBundle {
|
||||
text: Text::from_section(text.to_string(), TextStyle {
|
||||
font,
|
||||
font_size: 44.,
|
||||
color: Color::WHITE,
|
||||
}),
|
||||
transform: Transform {
|
||||
translation: Vec3::new(x, y, 0.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
commands.spawn(SpriteBundle {
|
||||
sprite: Sprite {
|
||||
color: sq_colors[level as usize % sq_colors.len()],
|
||||
..Default::default()
|
||||
},
|
||||
transform: Transform {
|
||||
translation: Vec3::new(x, y, 0.0),
|
||||
scale: Vec3::new(148.0, 148.0, 1.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
commands.spawn(label);
|
||||
}
|
||||
|
||||
pub fn update_scoreboard (score: Res<Score>, mut query: Query<&mut Text, With<ScoreboardUi>>) {
|
||||
let mut text = query.single_mut();
|
||||
text.sections[1].value = score.to_string();
|
||||
}
|
||||
|
||||
pub fn render_field (
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
field: Res<Field>,
|
||||
) {
|
||||
for (idx, &val) in field.iter().enumerate() {
|
||||
if val > 0 {
|
||||
spawn_square_at(&mut commands, val, idx, asset_server.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enter (
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut field: ResMut<Field>,
|
||||
) {
|
||||
let thread = &mut thread_rng();
|
||||
|
||||
let mut start = vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1];
|
||||
|
||||
start.shuffle(thread);
|
||||
*field = Field(start);
|
||||
|
||||
ui(commands, asset_server)
|
||||
|
||||
}
|
||||
|
||||
pub fn ui (
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
let font = asset_server.load("proxima.ttf");
|
||||
|
||||
|
||||
commands.spawn((
|
||||
ScoreboardUi,
|
||||
TextBundle::from_sections([
|
||||
TextSection::new(
|
||||
"score: ",
|
||||
TextStyle {
|
||||
font: font.clone(),
|
||||
font_size: SCOREBOARD_FONT_SIZE,
|
||||
color: TEXT_COLOR,
|
||||
},
|
||||
),
|
||||
TextSection::from_style(TextStyle {
|
||||
font,
|
||||
font_size: SCOREBOARD_FONT_SIZE,
|
||||
color: SCORE_COLOR,
|
||||
}),
|
||||
])
|
||||
.with_style(Style {
|
||||
position_type: PositionType::Absolute,
|
||||
top: SCOREBOARD_TEXT_PADDING,
|
||||
left: SCOREBOARD_TEXT_PADDING,
|
||||
..default()
|
||||
}),
|
||||
));
|
||||
|
||||
draw_board(&mut commands);
|
||||
}
|
||||
|
||||
|
||||
fn draw_board (commands: &mut Commands) {
|
||||
let borders = [-305., 305.];
|
||||
let lines = [-150., 0., 150.];
|
||||
|
||||
let border_width = 10.;
|
||||
let line_width = 1.;
|
||||
let length = 620.;
|
||||
|
||||
let border_color = Color::BLACK;
|
||||
let line_color = Color::srgba(0., 0., 0., 0.667);
|
||||
let bg_color = Color::srgba(0.2, 0.01, 0.5, 0.632);
|
||||
|
||||
// bg
|
||||
commands.spawn(SpriteBundle {
|
||||
sprite: Sprite { color: bg_color, ..Default::default() },
|
||||
transform: Transform {
|
||||
scale: Vec3::new(length, length, 1.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let line = |color: Color, translation: Vec3, scale: Vec3| SpriteBundle {
|
||||
sprite: Sprite { color, ..Default::default() },
|
||||
transform: Transform { translation, scale, ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// @TODO variable grid size
|
||||
for border in 0..2 {
|
||||
commands.spawn(line(border_color, Vec3::new(borders[border], 0.0, 0.0), Vec3::new(border_width, length, 1.0)));
|
||||
commands.spawn(line(border_color, Vec3::new(0.0, borders[border], 0.0), Vec3::new(length, border_width, 1.0)));
|
||||
}
|
||||
|
||||
for mesh in 0..3 {
|
||||
commands.spawn(line(line_color, Vec3::new(lines[mesh], 0.0, 0.0), Vec3::new(line_width, length, 1.0)));
|
||||
commands.spawn(line(line_color, Vec3::new(0.0, lines[mesh], 0.0), Vec3::new(length, line_width, 1.0)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn run (mut commands: Commands) {
|
||||
|
||||
}
|
||||
77
src/main.rs
77
src/main.rs
@ -1,4 +1,7 @@
|
||||
mod setup;
|
||||
mod game;
|
||||
mod menu;
|
||||
mod common;
|
||||
mod prelude;
|
||||
|
||||
use prelude::*;
|
||||
@ -7,76 +10,25 @@ use prelude::*;
|
||||
impl Plugin for SqPlugin {
|
||||
fn build (&self, app: &mut App) {
|
||||
|
||||
let thread = &mut thread_rng();
|
||||
|
||||
let mut start = vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1];
|
||||
|
||||
start.shuffle(thread);
|
||||
|
||||
// add things to your app here
|
||||
app
|
||||
.insert_resource(Score(0))
|
||||
.insert_resource(ClearColor(BG_COLOR))
|
||||
.insert_resource(Field(start))
|
||||
|
||||
.insert_state(State4096::Menu)
|
||||
.insert_state(InGameState::Static)
|
||||
.insert_resource(Score(0))
|
||||
.insert_resource(Field(vec![]))
|
||||
.add_systems(Startup, setup::setup)
|
||||
.add_systems(Update, (game::render_field, game::update_scoreboard, game::run).run_if(in_state(State4096::Game)))
|
||||
.add_systems(Update, (menu::actions, common::button_system).run_if(in_state(State4096::Menu)))
|
||||
.add_systems(OnEnter(State4096::Menu), menu::enter)
|
||||
.add_systems(OnExit(State4096::Menu), common::despawn_screen::<menu::Menu>)
|
||||
.add_systems(OnEnter(State4096::Game), game::enter)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn spawn_square_at (commands: &mut Commands, level: u32, position: usize, asset_server: AssetServer) {
|
||||
let font = asset_server.load("proxima_thin.ttf");
|
||||
let Some(&(x, y)) = sq_origins.get(position) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let text = 2i32.pow(level);
|
||||
let label = Text2dBundle {
|
||||
text: Text::from_section(text.to_string(), TextStyle {
|
||||
font,
|
||||
font_size: 44.,
|
||||
color: Color::WHITE,
|
||||
}),
|
||||
transform: Transform {
|
||||
translation: Vec3::new(x, y, 0.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
commands.spawn(SpriteBundle {
|
||||
sprite: Sprite {
|
||||
color: sq_colors[level as usize % sq_colors.len()],
|
||||
..Default::default()
|
||||
},
|
||||
transform: Transform {
|
||||
translation: Vec3::new(x, y, 0.0),
|
||||
scale: Vec3::new(148.0, 148.0, 1.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
commands.spawn(label);
|
||||
}
|
||||
|
||||
fn update_scoreboard (score: Res<Score>, mut query: Query<&mut Text, With<ScoreboardUi>>) {
|
||||
let mut text = query.single_mut();
|
||||
text.sections[1].value = score.to_string();
|
||||
}
|
||||
|
||||
fn render_field (
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
field: Res<Field>,
|
||||
) {
|
||||
for (idx, &val) in field.iter().enumerate() {
|
||||
if val > 0 {
|
||||
spawn_square_at(&mut commands, val, idx, asset_server.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn main () {
|
||||
App::new()
|
||||
@ -87,8 +39,5 @@ fn main () {
|
||||
}),
|
||||
..Default::default()
|
||||
}), SqPlugin))
|
||||
.add_systems(Startup, setup::setup)
|
||||
.add_systems(Update, update_scoreboard)
|
||||
.add_systems(Update, render_field)
|
||||
.run();
|
||||
}
|
||||
|
||||
107
src/menu.rs
Normal file
107
src/menu.rs
Normal file
@ -0,0 +1,107 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Menu;
|
||||
|
||||
|
||||
#[derive(Component)]
|
||||
pub enum MenuAction {
|
||||
New,
|
||||
Continue,
|
||||
Leaderboards,
|
||||
Settings,
|
||||
Quit,
|
||||
}
|
||||
|
||||
|
||||
pub fn enter (mut commands: Commands, asr: Res<AssetServer>) {
|
||||
let f1 = asr.load("proxima.ttf");
|
||||
let f2 = asr.load("proxima_thin.ttf");
|
||||
|
||||
|
||||
commands.spawn((NodeBundle {
|
||||
style: Style {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
align_items: AlignItems::Center,
|
||||
justify_content: JustifyContent::FlexStart,
|
||||
flex_direction: FlexDirection::Column,
|
||||
..Default::default()
|
||||
},
|
||||
border_color: BorderColor(Color::srgb(0.749, 0.737, 0.569)),
|
||||
..Default::default()
|
||||
}, Menu)).with_children(|p| {
|
||||
p.spawn(TextBundle {
|
||||
text: Text::from_section("4096 Game", TextStyle {
|
||||
font_size: 72.0, color: Color::BLACK, font: f1.clone()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
p.spawn(TextBundle {
|
||||
text: Text::from_section("totally not a clone of some game that was popular 10 years ago", TextStyle {
|
||||
font_size: 20.0, color: Color::BLACK, font: f2.clone()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
create_button(p, &asr, Some(UiRect::top(Val::Px(120.0))), "New Game", MenuAction::New);
|
||||
create_button(p, &asr, None, "Continue", MenuAction::Continue);
|
||||
create_button(p, &asr, None, "Leaderboards", MenuAction::Leaderboards);
|
||||
create_button(p, &asr, None, "Settings", MenuAction::Settings);
|
||||
create_button(p, &asr, None, "Quit to OS", MenuAction::Quit);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
pub fn create_button (parent: &mut ChildBuilder, asset_server: &Res<AssetServer>, extra_margin: Option<UiRect>, text: impl Into<String>, action: MenuAction) {
|
||||
let font = asset_server.load("proxima_thin.ttf");
|
||||
parent.spawn((ButtonBundle {
|
||||
style: Style {
|
||||
margin: if let Some(extra_margin) = extra_margin { extra_margin } else { Default::default() },
|
||||
width: Val::Px(350.0),
|
||||
height: Val::Px(65.0),
|
||||
border: UiRect::all(Val::Px(5.0)),
|
||||
// horizontally center child text
|
||||
justify_content: JustifyContent::Center,
|
||||
// vertically center child text
|
||||
align_items: AlignItems::Center,
|
||||
..default()
|
||||
|
||||
},
|
||||
// border_radius: BorderRadius::all(Val::Px(12.)),
|
||||
// image: UiImage::default().with_color(BUTTON_BG_COLOR),
|
||||
// background_color: BackgroundColor(BUTTON_BG_COLOR),
|
||||
..Default::default()
|
||||
}, action)).with_children(|p| {
|
||||
p.spawn(TextBundle {
|
||||
text: Text::from_section(text, TextStyle {
|
||||
font_size: 40.0, color: Color::WHITE, font
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
pub fn actions (
|
||||
interaction_query: Query<
|
||||
(&Interaction, &MenuAction),
|
||||
(Changed<Interaction>, With<Button>),
|
||||
>,
|
||||
mut app_exit_events: EventWriter<AppExit>,
|
||||
mut game_state: ResMut<NextState<State4096>>,
|
||||
) {
|
||||
for (interaction, menu_action) in &interaction_query {
|
||||
if *interaction == Interaction::Pressed {
|
||||
match menu_action {
|
||||
MenuAction::Quit => {
|
||||
app_exit_events.send(AppExit::Success);
|
||||
},
|
||||
MenuAction::New => {
|
||||
game_state.set(State4096::Game);
|
||||
},
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,10 +12,28 @@ pub struct ScoreboardUi;
|
||||
pub struct Field (pub Vec<u32>);
|
||||
|
||||
|
||||
#[derive(States, Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub enum State4096 {
|
||||
#[default]
|
||||
Menu,
|
||||
Game,
|
||||
GameOver,
|
||||
}
|
||||
|
||||
#[derive(States, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum InGameState {
|
||||
Static,
|
||||
Transtion (Vec<u32>), // target state,
|
||||
}
|
||||
|
||||
|
||||
pub const BG_COLOR: Color = Color::srgb(0.949, 0.937, 0.769);
|
||||
pub const SCOREBOARD_FONT_SIZE: f32 = 44.0;
|
||||
pub const TEXT_COLOR: Color = Color::BLACK;
|
||||
pub const SCORE_COLOR: Color = Color::srgb(0.0, 0.05, 0.66);
|
||||
pub const BUTTON_BG_COLOR: Color = Color::srgb(0.5, 0.5, 0.99);
|
||||
pub const BUTTON_BG_COLOR_HOVER: Color = Color::srgb(0.9, 0.2, 0.89);
|
||||
|
||||
pub const SCOREBOARD_TEXT_PADDING: Val = Val::Px(22.0);
|
||||
|
||||
pub struct SqPlugin;
|
||||
|
||||
72
src/setup.rs
72
src/setup.rs
@ -6,75 +6,7 @@ pub fn setup (
|
||||
mut _materials: ResMut<Assets<ColorMaterial>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
let font = asset_server.load("proxima.ttf");
|
||||
|
||||
commands.spawn(Camera2dBundle::default());
|
||||
|
||||
commands.spawn((
|
||||
ScoreboardUi,
|
||||
TextBundle::from_sections([
|
||||
TextSection::new(
|
||||
"score: ",
|
||||
TextStyle {
|
||||
font: font.clone(),
|
||||
font_size: SCOREBOARD_FONT_SIZE,
|
||||
color: TEXT_COLOR,
|
||||
},
|
||||
),
|
||||
TextSection::from_style(TextStyle {
|
||||
font,
|
||||
font_size: SCOREBOARD_FONT_SIZE,
|
||||
color: SCORE_COLOR,
|
||||
}),
|
||||
])
|
||||
.with_style(Style {
|
||||
position_type: PositionType::Absolute,
|
||||
top: SCOREBOARD_TEXT_PADDING,
|
||||
left: SCOREBOARD_TEXT_PADDING,
|
||||
..default()
|
||||
}),
|
||||
));
|
||||
|
||||
draw_board(&mut commands);
|
||||
}
|
||||
|
||||
|
||||
fn draw_board (commands: &mut Commands) {
|
||||
let borders = [-305., 305.];
|
||||
let lines = [-150., 0., 150.];
|
||||
|
||||
let border_width = 10.;
|
||||
let line_width = 1.;
|
||||
let length = 620.;
|
||||
|
||||
let border_color = Color::BLACK;
|
||||
let line_color = Color::srgba(0., 0., 0., 0.667);
|
||||
let bg_color = Color::srgba(0.2, 0.01, 0.5, 0.632);
|
||||
|
||||
// bg
|
||||
commands.spawn(SpriteBundle {
|
||||
sprite: Sprite { color: bg_color, ..Default::default() },
|
||||
transform: Transform {
|
||||
scale: Vec3::new(length, length, 1.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let line = |color: Color, translation: Vec3, scale: Vec3| SpriteBundle {
|
||||
sprite: Sprite { color, ..Default::default() },
|
||||
transform: Transform { translation, scale, ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// @TODO variable grid size
|
||||
for border in 0..2 {
|
||||
commands.spawn(line(border_color, Vec3::new(borders[border], 0.0, 0.0), Vec3::new(border_width, length, 1.0)));
|
||||
commands.spawn(line(border_color, Vec3::new(0.0, borders[border], 0.0), Vec3::new(length, border_width, 1.0)));
|
||||
}
|
||||
|
||||
for mesh in 0..3 {
|
||||
commands.spawn(line(line_color, Vec3::new(lines[mesh], 0.0, 0.0), Vec3::new(line_width, length, 1.0)));
|
||||
commands.spawn(line(line_color, Vec3::new(0.0, lines[mesh], 0.0), Vec3::new(length, line_width, 1.0)));
|
||||
}
|
||||
let _: Handle<Font> = asset_server.load("proxima.ttf");
|
||||
let _: Handle<Font> = asset_server.load("proxima_thin.ttf");
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user