diff --git a/assets/sprites/missing.aseprite b/assets/sprites/missing.aseprite new file mode 100644 index 0000000..132fc30 Binary files /dev/null and b/assets/sprites/missing.aseprite differ diff --git a/assets/sprites/missing.png b/assets/sprites/missing.png new file mode 100644 index 0000000..55b9d11 Binary files /dev/null and b/assets/sprites/missing.png differ diff --git a/src/game.rs b/src/game.rs index a36d8ef..c9fec69 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,84 +1,28 @@ -use crate::{debug, game_setup}; -use bevy::{prelude::*, sprite::Anchor}; +use crate::{debug, game_setup, util}; +use bevy::prelude::*; use bevy_rapier2d::prelude::*; +mod item; +mod prefab; +mod systems; +mod work; + pub fn init(app: &mut App) { let app = app.add_plugins(( game_setup::GameSetupPlugin, RapierPhysicsPlugin::::default(), + util::UtilPlugin, debug::DebugPlugin, )); - app.add_systems(Startup, setup_2d) - .add_systems(Update, demo_2d); -} - -fn setup_2d(mut commands: Commands, assets: Res) { - commands.spawn(( - Name::from("Demo 2D camera"), - Camera2d, - Transform::from_xyz(0.0, 0.0, 10.0), - )); - - commands.spawn(( - Name::from("Glorb"), - Transform::from_xyz(-200.0, 0.0, 0.0), - Sprite { - image: assets.load("sprites/glorb.png"), - anchor: Anchor::BottomCenter, - ..default() - }, - )); - - commands.spawn(( - Name::from("Box"), - Transform::from_xyz(-200.0, -150.0, 0.0), - Sprite { - image: assets.load("sprites/box.png"), - anchor: Anchor::Custom(Vec2 { x: 0.0, y: -0.375 }), - ..default() - }, - )); - - commands.spawn(( - Name::from("Wood"), - Sprite { - image: assets.load("sprites/wood.png"), - ..default() - }, - )); - - commands.spawn(( - Name::from("Tree"), - Transform::from_xyz(200.0, 0.0, 0.0), - Sprite { - image: assets.load("sprites/tree.png"), - anchor: Anchor::Custom(Vec2 { x: 0.0, y: -0.375 }), - ..default() - }, - )); -} - -fn demo_2d( - mut camera_query: Query<(&mut Transform, &mut OrthographicProjection)>, - mut mouse_events: EventReader, - mut scroll_events: EventReader, - mouse_input: Res>, -) { - let raw_mouse_motion: Vec2 = mouse_events.read().map(|e| e.delta).sum(); - let raw_scroll_motion: f32 = scroll_events - .read() - .map(|event| match event.unit { - bevy::input::mouse::MouseScrollUnit::Line => event.y * -0.1, - bevy::input::mouse::MouseScrollUnit::Pixel => event.y * -0.05, - }) - .sum(); - - for (mut transform, mut projection) in camera_query.iter_mut() { - projection.scale += raw_scroll_motion * projection.scale; - let mouse_motion = raw_mouse_motion * projection.scale * Vec2::new(-1.0, 1.0); - if mouse_input.pressed(MouseButton::Middle) { - transform.translation += mouse_motion.extend(0.0); - } - } + app.register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .add_systems(Startup, systems::setup_2d) + .add_systems(Update, (systems::demo_2d, systems::work_select)) + .add_systems(PostUpdate, item::update_item_sprite); } diff --git a/src/game/item.rs b/src/game/item.rs new file mode 100644 index 0000000..96ac0a4 --- /dev/null +++ b/src/game/item.rs @@ -0,0 +1,141 @@ +use bevy::prelude::*; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Component, Reflect)] +#[reflect(Component)] +#[require(Sprite)] +pub enum Item { + Wood, +} + +#[derive(Copy, Clone, Debug, Reflect)] +pub struct ItemStack { + pub item: Item, + pub count: u32, +} + +impl Default for ItemStack { + fn default() -> Self { + Self { + item: Item::Wood, + count: 1, + } + } +} + +#[derive(Clone, Debug, Default, Component, Reflect)] +#[reflect(Component)] +pub struct ItemSource(pub ItemStack); + +#[derive(Clone, Debug, Default, Component, Reflect)] +#[reflect(Component)] +pub struct Inventory { + pub capacity: Option, + pub items: Vec, +} + +pub enum InsertResult { + /// Combine to an existing stack. Contains (`index`, `new_count`) + Combine(usize, u32), + /// No existing stackable stacks, push a new one. + Push(ItemStack), +} + +pub enum RemoveResult { + /// The stack will get depleted, so it can be removed. Contains the `index` of empty stack. + Empty(usize), + /// The stack will get only partially depleted. Contains (`index`, `new_count`) + Partial(usize, u32), +} + +impl Inventory { + pub fn with_capacity(capacity: usize) -> Self { + Self { + capacity: Some(capacity), + ..default() + } + } + + pub fn is_empty(&self) -> bool { + self.items.iter().all(|stack| stack.count == 0) + } + + pub fn try_insert(&self, item_stack: &ItemStack) -> Option { + match self + .items + .iter() + .enumerate() + .find(|(_, stack)| stack.item == item_stack.item) + { + Some((index, stack)) => { + Some(InsertResult::Combine(index, stack.count + item_stack.count)) + } + None => { + if self + .capacity + .map_or(true, |capacity| self.items.len() < capacity) + { + Some(InsertResult::Push(*item_stack)) + } else { + None + } + } + } + } + + pub fn try_remove(&self, item_stack: &ItemStack) -> Option { + match self + .items + .iter() + .enumerate() + .find(|(_, stack)| stack.item == item_stack.item) + { + Some((index, stack)) => match stack.count.checked_sub(item_stack.count) { + Some(new_count) => { + if new_count == 0 { + Some(RemoveResult::Empty(index)) + } else { + Some(RemoveResult::Partial(index, new_count)) + } + } + // Not enough items + None => None, + }, + // No matching items found + None => None, + } + } + + pub fn try_transfer( + &self, + to: &Self, + item_stack: &ItemStack, + ) -> Option<(RemoveResult, InsertResult)> { + match (self.try_remove(item_stack), to.try_insert(item_stack)) { + (Some(remove_result), Some(insert_result)) => Some((remove_result, insert_result)), + _ => None, + } + } + + pub fn sanitize(&mut self) { + if let Some(capacity) = self.capacity { + self.items.truncate(capacity); + } + self.items.retain(|stack| stack.count > 0); + } +} + +#[derive(Clone, Debug, Default, Component, Reflect)] +#[reflect(Component)] +#[require(Inventory)] +pub struct Stockpile; + +pub fn update_item_sprite( + mut query: Query<(&mut Sprite, &Item), Changed>, + assets: Res, +) { + for (mut sprite, item) in query.iter_mut() { + sprite.image = assets.load(match item { + Item::Wood => "sprites/wood.png", + }); + } +} diff --git a/src/game/prefab.rs b/src/game/prefab.rs new file mode 100644 index 0000000..9561abe --- /dev/null +++ b/src/game/prefab.rs @@ -0,0 +1,51 @@ +use bevy::{prelude::*, sprite::Anchor}; +use bevy_rapier2d::prelude::*; +use item::{Inventory, Item, ItemSource, ItemStack, Stockpile}; +use work::{WorkDuration, Worker}; + +use super::*; +use crate::util::SpriteLoader; + +#[derive(Clone, Debug, Default, Component, Reflect)] +#[reflect(Component)] +#[require( + Name(|| Name::from("Glorb")), + Sprite(|| Sprite { + anchor: Anchor::BottomCenter, + ..default() + }), + SpriteLoader(|| SpriteLoader::from("sprites/glorb.png")), + Worker, + Inventory(|| Inventory::with_capacity(1)), + Velocity, + RigidBody(|| RigidBody::KinematicVelocityBased), +)] +pub struct Glorb; + +#[derive(Clone, Debug, Default, Component, Reflect)] +#[reflect(Component)] +#[require( + Name(|| Name::from("Tree")), + Sprite(|| Sprite { + anchor: Anchor::Custom(Vec2 { x: 0.0, y: -0.375 }), + ..default() + }), + SpriteLoader(|| SpriteLoader::from("sprites/tree.png")), + ItemSource(|| ItemSource(ItemStack { item: Item::Wood, count: 1 })), + WorkDuration(|| WorkDuration(5.0)) +)] +pub struct Tree; + +#[derive(Clone, Debug, Default, Component, Reflect)] +#[reflect(Component)] +#[require( + Name(|| Name::from("Chest")), + Sprite(|| Sprite { + anchor: Anchor::Custom(Vec2 { x: 0.0, y: -0.375 }), + ..default() + }), + SpriteLoader(|| SpriteLoader::from("sprites/box.png")), + Inventory(|| Inventory::with_capacity(25)), + Stockpile, +)] +pub struct Chest; diff --git a/src/game/systems.rs b/src/game/systems.rs new file mode 100644 index 0000000..116a60f --- /dev/null +++ b/src/game/systems.rs @@ -0,0 +1,5 @@ +mod demo; +mod worker; + +pub use demo::*; +pub use worker::*; diff --git a/src/game/systems/demo.rs b/src/game/systems/demo.rs new file mode 100644 index 0000000..a288725 --- /dev/null +++ b/src/game/systems/demo.rs @@ -0,0 +1,45 @@ +use bevy::prelude::*; + +use crate::game::{item::Item, prefab}; + +pub fn setup_2d(mut commands: Commands) { + commands.spawn(( + Name::from("Demo 2D camera"), + Camera2d, + Transform::from_xyz(0.0, 0.0, 10.0), + )); + + commands.spawn((Transform::from_xyz(-200.0, 0.0, 0.0), prefab::Glorb)); + commands.spawn((Transform::from_xyz(-200.0, 100.0, 0.0), prefab::Glorb)); + commands.spawn((Transform::from_xyz(-200.0, -100.0, 0.0), prefab::Glorb)); + + commands.spawn((Transform::from_xyz(200.0, 0.0, 0.0), prefab::Tree)); + + commands.spawn((Name::from("Wood"), Item::Wood)); + + commands.spawn((Transform::from_xyz(-200.0, -150.0, 0.0), prefab::Chest)); +} + +pub fn demo_2d( + mut camera_query: Query<(&mut Transform, &mut OrthographicProjection)>, + mut mouse_events: EventReader, + mut scroll_events: EventReader, + mouse_input: Res>, +) { + let raw_mouse_motion: Vec2 = mouse_events.read().map(|e| e.delta).sum(); + let raw_scroll_motion: f32 = scroll_events + .read() + .map(|event| match event.unit { + bevy::input::mouse::MouseScrollUnit::Line => event.y * -0.1, + bevy::input::mouse::MouseScrollUnit::Pixel => event.y * -0.05, + }) + .sum(); + + for (mut transform, mut projection) in camera_query.iter_mut() { + projection.scale += raw_scroll_motion * projection.scale; + let mouse_motion = raw_mouse_motion * projection.scale * Vec2::new(-1.0, 1.0); + if mouse_input.pressed(MouseButton::Middle) { + transform.translation += mouse_motion.extend(0.0); + } + } +} diff --git a/src/game/systems/worker.rs b/src/game/systems/worker.rs new file mode 100644 index 0000000..abf267e --- /dev/null +++ b/src/game/systems/worker.rs @@ -0,0 +1,72 @@ +use bevy::prelude::*; + +use crate::game::{ + item::{Inventory, ItemSource, Stockpile}, + work::{Task, WorkType, Worker}, +}; + +pub fn work_select( + mut worker_query: Query<(&mut Worker, &Inventory, &GlobalTransform)>, + store_query: Query<(Entity, &Inventory, &GlobalTransform), With>, + gather_query: Query<(Entity, &ItemSource, &GlobalTransform)>, +) { + for (mut worker, worker_inventory, worker_transform) in worker_query.iter_mut() { + // Skip if worker already has a job + if worker.0.is_some() { + continue; + } + + let mut task_by_distance: Option<(Task, f32)> = None; + + for (stockpile_entity, stockpile_inventory, stockpile_transform) in store_query.iter() { + for worker_stack in worker_inventory.items.iter() { + if let Some((_, _)) = + worker_inventory.try_transfer(stockpile_inventory, worker_stack) + { + let stockpile_dist_squared = worker_transform + .translation() + .distance_squared(stockpile_transform.translation()); + if task_by_distance.map_or(true, |(_, closest_task_dist)| { + stockpile_dist_squared < closest_task_dist + }) { + task_by_distance = Some(( + Task { + target: stockpile_entity, + work_type: WorkType::Store(*worker_stack), + progress: 0.0, + }, + stockpile_dist_squared, + )) + } + } + } + } + + if let Some((task, _)) = task_by_distance { + worker.0 = Some(task); + continue; + } + + for (item_source_entity, item_source, item_source_transform) in gather_query.iter() { + if worker_inventory.try_insert(&item_source.0).is_some() { + let source_dist_squared = worker_transform + .translation() + .distance_squared(item_source_transform.translation()); + if task_by_distance.map_or(true, |(_, closest_task_dist)| { + source_dist_squared < closest_task_dist + }) { + task_by_distance = Some(( + Task { + target: item_source_entity, + work_type: WorkType::Gather, + progress: 0.0, + }, + source_dist_squared, + )) + } + } + } + + worker.0 = task_by_distance.map(|(task, _)| task); + } +} diff --git a/src/game/work.rs b/src/game/work.rs new file mode 100644 index 0000000..473a483 --- /dev/null +++ b/src/game/work.rs @@ -0,0 +1,47 @@ +use bevy::prelude::*; + +use crate::util::Seconds; + +use super::item::ItemStack; + +/// Total time it takes to finish a task +#[derive(Copy, Clone, Debug, Default, Component, Reflect)] +#[reflect(Component)] +pub struct WorkDuration(pub Seconds); + +impl WorkDuration { + /// Returns the `Some`(`duration_secs`) if the value is above `0.0`, and `None` otherwise + pub fn safe_duration(&self) -> Option { + if self.0 > 0.0 { + Some(self.0) + } else { + None + } + } +} + +#[derive(Copy, Clone, Debug, Reflect)] +pub enum WorkType { + Gather, + Store(ItemStack), +} + +#[derive(Copy, Clone, Debug, Reflect)] +pub struct Task { + pub progress: Seconds, + pub work_type: WorkType, + pub target: Entity, +} + +impl Task { + pub fn add_progress(&mut self, work_duration: &WorkDuration, progress: Seconds) -> u32 { + match work_duration.safe_duration() { + Some(duration) => (self.progress + progress).div_euclid(duration).max(0.0) as u32, + None => 1, + } + } +} + +#[derive(Copy, Clone, Debug, Default, Component, Reflect)] +#[reflect(Component)] +pub struct Worker(pub Option); diff --git a/src/game_setup.rs b/src/game_setup.rs index 78456b6..a756319 100644 --- a/src/game_setup.rs +++ b/src/game_setup.rs @@ -4,18 +4,19 @@ pub struct GameSetupPlugin; impl Plugin for GameSetupPlugin { fn build(&self, app: &mut App) { - app.insert_resource(ClearColor(Color::BLACK)).add_plugins( - DefaultPlugins - .set(WindowPlugin { - primary_window: Some(Window { - resolution: WindowResolution::new(512.0 * 2.0, 320.0 * 2.0), - title: "Glorbs ".to_string(), // NOTE: Replace this - resizable: false, + app.insert_resource(ClearColor(Color::linear_rgb(0.070, 0.094, 0.071))) + .add_plugins( + DefaultPlugins + .set(WindowPlugin { + primary_window: Some(Window { + resolution: WindowResolution::new(512.0 * 2.0, 320.0 * 2.0), + title: "Glorbs ".to_string(), // NOTE: Replace this + resizable: false, + ..default() + }), ..default() - }), - ..default() - }) - .set(ImagePlugin::default_nearest()), - ); + }) + .set(ImagePlugin::default_nearest()), + ); } } diff --git a/src/main.rs b/src/main.rs index 5b08ab9..4c159db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use bevy::prelude::*; -use util::create_app_graphs; pub mod debug; pub mod game; @@ -9,6 +8,6 @@ pub mod util; fn main() { let mut app = App::new(); game::init(&mut app); - create_app_graphs(&mut app); + // util::create_app_graphs(&mut app); app.run(); } diff --git a/src/util.rs b/src/util.rs index 8c0bc35..a4b872a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -4,14 +4,18 @@ use core::{fmt, ops}; use std::{fs, path::Path}; mod basis; +mod plugin; mod transform_f64; +mod types; mod vector2; mod vector2_i32; mod vector3; mod vector3_i32; pub use basis::*; +pub use plugin::*; pub use transform_f64::*; +pub use types::*; pub use vector2::*; pub use vector2_i32::*; pub use vector3::*; diff --git a/src/util/plugin.rs b/src/util/plugin.rs new file mode 100644 index 0000000..8a6520e --- /dev/null +++ b/src/util/plugin.rs @@ -0,0 +1,33 @@ +use bevy::prelude::*; + +pub struct UtilPlugin; + +impl Plugin for UtilPlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .add_systems(PostUpdate, load_sprite); + } +} + +/// Automatically sets the `Sprite` components `image` to given asset path upon being added. +/// +/// Can be used to set default sprite in required components without having access to AssetServer. +#[derive(Clone, Debug, Default, Component, Reflect)] +#[reflect(Component)] +#[require(Sprite)] +pub struct SpriteLoader(pub String); + +impl SpriteLoader { + pub fn from(path: impl Into) -> Self { + Self(path.into()) + } +} + +pub fn load_sprite( + mut sprite_query: Query<(&mut Sprite, &SpriteLoader), Added>, + assets: Res, +) { + for (mut sprite, loader) in sprite_query.iter_mut() { + sprite.image = assets.load(&loader.0); + } +} diff --git a/src/util/types.rs b/src/util/types.rs new file mode 100644 index 0000000..9b673c4 --- /dev/null +++ b/src/util/types.rs @@ -0,0 +1 @@ +pub type Seconds = f32;