feat: menu system, basic state handling, more code splitting/cleanup

This commit is contained in:
YK 2024-07-14 06:07:16 +03:00
parent 391ef7fd8b
commit b8dde775f1
7 changed files with 315 additions and 135 deletions

View File

@ -7,7 +7,7 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.86" 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" itertools = "0.13.0"
log = { version = "*", features = ["max_level_debug", "release_max_level_warn"] } log = { version = "*", features = ["max_level_debug", "release_max_level_warn"] }
rand = "0.8.5" rand = "0.8.5"

23
src/common.rs Normal file
View 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
View 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) {
}

View File

@ -1,4 +1,7 @@
mod setup; mod setup;
mod game;
mod menu;
mod common;
mod prelude; mod prelude;
use prelude::*; use prelude::*;
@ -7,76 +10,25 @@ use prelude::*;
impl Plugin for SqPlugin { impl Plugin for SqPlugin {
fn build (&self, app: &mut App) { 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 // add things to your app here
app app
.insert_resource(Score(0))
.insert_resource(ClearColor(BG_COLOR)) .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 () { fn main () {
App::new() App::new()
@ -87,8 +39,5 @@ fn main () {
}), }),
..Default::default() ..Default::default()
}), SqPlugin)) }), SqPlugin))
.add_systems(Startup, setup::setup)
.add_systems(Update, update_scoreboard)
.add_systems(Update, render_field)
.run(); .run();
} }

107
src/menu.rs Normal file
View 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);
},
_ => ()
}
}
}
}

View File

@ -12,10 +12,28 @@ pub struct ScoreboardUi;
pub struct Field (pub Vec<u32>); 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 BG_COLOR: Color = Color::srgb(0.949, 0.937, 0.769);
pub const SCOREBOARD_FONT_SIZE: f32 = 44.0; pub const SCOREBOARD_FONT_SIZE: f32 = 44.0;
pub const TEXT_COLOR: Color = Color::BLACK; pub const TEXT_COLOR: Color = Color::BLACK;
pub const SCORE_COLOR: Color = Color::srgb(0.0, 0.05, 0.66); 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 const SCOREBOARD_TEXT_PADDING: Val = Val::Px(22.0);
pub struct SqPlugin; pub struct SqPlugin;

View File

@ -6,75 +6,7 @@ pub fn setup (
mut _materials: ResMut<Assets<ColorMaterial>>, mut _materials: ResMut<Assets<ColorMaterial>>,
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
) { ) {
let font = asset_server.load("proxima.ttf");
commands.spawn(Camera2dBundle::default()); commands.spawn(Camera2dBundle::default());
let _: Handle<Font> = asset_server.load("proxima.ttf");
commands.spawn(( let _: Handle<Font> = asset_server.load("proxima_thin.ttf");
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)));
}
} }