Merge branch 'dev' into 'main'
Merge Sprint 3 Closes #52 See merge request softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik!28
This commit is contained in:
32
.gitlab-ci.yml
Normal file
32
.gitlab-ci.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
image: "rust:latest"
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
- test
|
||||||
|
cache:
|
||||||
|
key: "$CI_COMMIT_REF_SLUG"
|
||||||
|
paths:
|
||||||
|
- .cargo/
|
||||||
|
- target/
|
||||||
|
.only_on_main_rule: &only_on_main_rule
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_REF_SLUG == "main"
|
||||||
|
.install_linux_dependencies: &install_linux_dependencies
|
||||||
|
before_script:
|
||||||
|
- apt-get update -yq
|
||||||
|
- apt-get install -yq g++ pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0
|
||||||
|
linux_build:
|
||||||
|
stage: build
|
||||||
|
!!merge <<: *only_on_main_rule
|
||||||
|
!!merge <<: *install_linux_dependencies
|
||||||
|
script:
|
||||||
|
- cargo build --release --target x86_64-unknown-linux-gnu
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- target/x86_64-unknown-linux-gnu/release/pomomon-garden
|
||||||
|
name: "pomomon-garden-linux-x86_64-$CI_COMMIT_SHORT_SHA"
|
||||||
|
expire_in: "2 weeks"
|
||||||
|
run_tests:
|
||||||
|
stage: test
|
||||||
|
!!merge <<: *install_linux_dependencies
|
||||||
|
script:
|
||||||
|
- cargo test --verbose
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3663,6 +3663,7 @@ dependencies = [
|
|||||||
"directories",
|
"directories",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -20,3 +20,6 @@ bevy_dev_tools = "0.17.2"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
directories = "6.0"
|
directories = "6.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
uuid = "1.18.1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Pomomon Garden 
|
# Pomomon Garden 
|
||||||
|
|
||||||
Pomomon Garden is a Pomodoro game created during ["Sopra" (Softwareprojekt)](https://www.uni-ulm.de/in/sgi/lehre/softwaojekt-sopra-1/) WiSe 25/26 at the University of Ulm.
|
Pomomon Garden is a Pomodoro game created during ["Sopra" (Softwareprojekt)](https://www.uni-ulm.de/in/sgi/lehre/softwareprojekt-sopra-1/) WiSe 25/26 at the University of Ulm.
|
||||||
It uses the [ECS](https://en.wikipedia.org/wiki/Entity_component_system)-based Rust game engine called [Bevy](https://bevy.org/).
|
It uses the [ECS](https://en.wikipedia.org/wiki/Entity_component_system)-based Rust game engine called [Bevy](https://bevy.org/).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -40,10 +40,14 @@ cargo run
|
|||||||
### Hidden binds (Only available in the debug build)
|
### Hidden binds (Only available in the debug build)
|
||||||
|
|
||||||
- `Shift + Enter`: Duration of the current phase is set to 3 seconds.
|
- `Shift + Enter`: Duration of the current phase is set to 3 seconds.
|
||||||
- `Left Mouse Button` on Tile: Rotate tile state.
|
- `Shift + Left Mouse Button` on Tile: Toggle tile state.
|
||||||
|
- `Shift + Arrow Up`: Add one berry to your inventory
|
||||||
|
- `Shift + Arrow Down`: Remove one berry from your inventory
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
This project is released under the terms of the [MIT License](LICENSE).
|
This project is released under the terms of the [MIT License](LICENSE).
|
||||||
|
|
||||||
|
The font used is called Jersey 10. It is protected under the [SIL OPEN FONT LICENSE Version 1.1](assets/fonts/Jersey10.LICENSE).
|
||||||
|
|||||||
BIN
assets/berry.aseprite
Normal file
BIN
assets/berry.aseprite
Normal file
Binary file not shown.
@@ -1,5 +1,30 @@
|
|||||||
{
|
{
|
||||||
"grid_width": 12,
|
"grid_width": 12,
|
||||||
"grid_height": 4,
|
"grid_height": 4,
|
||||||
"pom_speed": 1.5
|
"pom_speed": 1.5,
|
||||||
|
"shovel_base_price": 10,
|
||||||
|
"shovel_rate": 0.5,
|
||||||
|
"berry_seeds": [
|
||||||
|
{
|
||||||
|
"name": "Normale Samen",
|
||||||
|
"cost": 1,
|
||||||
|
"grants": 2,
|
||||||
|
"slice": "Seed1",
|
||||||
|
"growth_stages": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Super-Samen",
|
||||||
|
"cost": 3,
|
||||||
|
"grants": 9,
|
||||||
|
"slice": "Seed2",
|
||||||
|
"growth_stages": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Zauber-Samen",
|
||||||
|
"cost": 5,
|
||||||
|
"grants": 20,
|
||||||
|
"slice": "Seed3",
|
||||||
|
"growth_stages": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
BIN
assets/crop.aseprite
Normal file
BIN
assets/crop.aseprite
Normal file
Binary file not shown.
BIN
assets/fonts/Jersey10-Regular.ttf
Normal file
BIN
assets/fonts/Jersey10-Regular.ttf
Normal file
Binary file not shown.
93
assets/fonts/Jersey10.LICENSE
Normal file
93
assets/fonts/Jersey10.LICENSE
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2023 The Soft Type Project Authors (https://github.com/scfried/soft-type-jersey)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
BIN
assets/seed.aseprite
Normal file
BIN
assets/seed.aseprite
Normal file
Binary file not shown.
@@ -7,6 +7,18 @@ pub struct GameConfig {
|
|||||||
pub grid_width: u32,
|
pub grid_width: u32,
|
||||||
pub grid_height: u32,
|
pub grid_height: u32,
|
||||||
pub pom_speed: f32,
|
pub pom_speed: f32,
|
||||||
|
pub shovel_base_price: u32,
|
||||||
|
pub shovel_rate: f32,
|
||||||
|
pub berry_seeds: Vec<BerrySeedConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct BerrySeedConfig {
|
||||||
|
pub name: String,
|
||||||
|
pub cost: u32,
|
||||||
|
pub grants: u32,
|
||||||
|
pub slice: String,
|
||||||
|
pub growth_stages: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GameConfig {
|
impl Default for GameConfig {
|
||||||
@@ -15,13 +27,42 @@ impl Default for GameConfig {
|
|||||||
grid_width: 12,
|
grid_width: 12,
|
||||||
grid_height: 4,
|
grid_height: 4,
|
||||||
pom_speed: 1.5,
|
pom_speed: 1.5,
|
||||||
|
shovel_base_price: 10,
|
||||||
|
shovel_rate: 0.2,
|
||||||
|
berry_seeds: vec![
|
||||||
|
BerrySeedConfig {
|
||||||
|
name: "Normale Samen".to_string(),
|
||||||
|
cost: 1,
|
||||||
|
grants: 2,
|
||||||
|
slice: "Seed1".to_string(),
|
||||||
|
growth_stages: 2,
|
||||||
|
},
|
||||||
|
BerrySeedConfig {
|
||||||
|
name: "Super-Samen".to_string(),
|
||||||
|
cost: 3,
|
||||||
|
grants: 9,
|
||||||
|
slice: "Seed2".to_string(),
|
||||||
|
growth_stages: 4,
|
||||||
|
},
|
||||||
|
BerrySeedConfig {
|
||||||
|
name: "Zauber-Samen".to_string(),
|
||||||
|
cost: 5,
|
||||||
|
grants: 20,
|
||||||
|
slice: "Seed3".to_string(),
|
||||||
|
growth_stages: 6,
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameConfig {
|
impl GameConfig {
|
||||||
pub fn read_config() -> Option<Self> {
|
pub fn read_config() -> Option<Self> {
|
||||||
let file = File::open("assets/config.json").ok()?;
|
Self::read_from_path(std::path::Path::new("assets/config.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_from_path(path: &std::path::Path) -> Option<Self> {
|
||||||
|
let file = File::open(path).ok()?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
serde_json::from_reader(reader).ok()
|
serde_json::from_reader(reader).ok()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,32 @@ pub struct Tile {
|
|||||||
pub y: u32,
|
pub y: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component, Default, Serialize, Deserialize, Clone, Copy, Debug)]
|
#[derive(Component)]
|
||||||
|
pub struct CropVisual;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct WaterVisual;
|
||||||
|
|
||||||
|
#[derive(Component, Default, Serialize, Deserialize, Clone, Debug)]
|
||||||
pub enum TileState {
|
pub enum TileState {
|
||||||
#[default]
|
#[default]
|
||||||
Unclaimed,
|
Unclaimed,
|
||||||
Empty,
|
Empty,
|
||||||
Occupied,
|
Occupied {
|
||||||
|
seed: ItemType,
|
||||||
|
watered: bool,
|
||||||
|
growth_stage: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
withered: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
dry_counter: u8,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TileState {
|
impl TileState {
|
||||||
pub fn is_blocking(&self) -> bool {
|
pub fn is_blocking(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
TileState::Occupied => true,
|
TileState::Occupied { .. } => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use components::{CropVisual, WaterVisual};
|
||||||
|
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
@@ -12,10 +13,7 @@ impl Plugin for GridPlugin {
|
|||||||
app.add_systems(OnEnter(AppState::GameScreen), setup);
|
app.add_systems(OnEnter(AppState::GameScreen), setup);
|
||||||
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
||||||
|
|
||||||
app.add_systems(
|
app.add_systems(Update, update_tiles.run_if(in_state(AppState::GameScreen)));
|
||||||
Update,
|
|
||||||
update_tile_colors.run_if(in_state(AppState::GameScreen)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +43,30 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<Gam
|
|||||||
grid_height,
|
grid_height,
|
||||||
)),
|
)),
|
||||||
))
|
))
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn((
|
||||||
|
CropVisual,
|
||||||
|
AseSlice {
|
||||||
|
name: "Crop".into(),
|
||||||
|
aseprite: asset_server.load("crop.aseprite"),
|
||||||
|
},
|
||||||
|
Sprite::default(),
|
||||||
|
Transform::from_translation(Vec3::new(0.0, 0.0, 1.0)),
|
||||||
|
Visibility::Hidden,
|
||||||
|
ZIndex(1),
|
||||||
|
));
|
||||||
|
parent.spawn((
|
||||||
|
WaterVisual,
|
||||||
|
AseSlice {
|
||||||
|
name: "Water".into(),
|
||||||
|
aseprite: asset_server.load("crop.aseprite"),
|
||||||
|
},
|
||||||
|
Sprite::default(),
|
||||||
|
Transform::from_translation(Vec3::new(0.0, 0.0, 2.0)),
|
||||||
|
Visibility::Hidden,
|
||||||
|
ZIndex(2),
|
||||||
|
));
|
||||||
|
})
|
||||||
.id();
|
.id();
|
||||||
column.push(tile_entity);
|
column.push(tile_entity);
|
||||||
}
|
}
|
||||||
@@ -65,22 +87,73 @@ fn cleanup(mut commands: Commands, tile_query: Query<Entity, With<Tile>>) {
|
|||||||
commands.remove_resource::<Grid>();
|
commands.remove_resource::<Grid>();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_tile_colors(
|
fn update_tiles(
|
||||||
mut query: Query<(&TileState, &mut AseSlice)>,
|
mut query: Query<(&TileState, &mut AseSlice, &Children), (With<Tile>, Without<CropVisual>)>,
|
||||||
|
mut crop_query: Query<
|
||||||
|
(&mut Visibility, &mut Transform, &mut AseSlice),
|
||||||
|
(With<CropVisual>, Without<WaterVisual>, Without<Tile>),
|
||||||
|
>,
|
||||||
|
mut water_query: Query<
|
||||||
|
(&mut Visibility, &mut Transform),
|
||||||
|
(With<WaterVisual>, Without<CropVisual>),
|
||||||
|
>,
|
||||||
asset_server: Res<AssetServer>,
|
asset_server: Res<AssetServer>,
|
||||||
|
game_config: Res<GameConfig>,
|
||||||
) {
|
) {
|
||||||
for (state, mut slice) in &mut query {
|
for (state, mut slice, children) in &mut query {
|
||||||
slice.name = match state {
|
slice.name = match state {
|
||||||
TileState::Unclaimed => "Unclaimed",
|
TileState::Unclaimed => "Unclaimed",
|
||||||
TileState::Empty => "Empty",
|
TileState::Empty => "Empty",
|
||||||
TileState::Occupied => "Occupied",
|
TileState::Occupied { .. } => "Occupied",
|
||||||
}
|
}
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
slice.aseprite = match state {
|
slice.aseprite = match state {
|
||||||
TileState::Unclaimed => asset_server.load("tiles/tile-unclaimed.aseprite"),
|
TileState::Unclaimed => asset_server.load("tiles/tile-unclaimed.aseprite"),
|
||||||
TileState::Empty => asset_server.load("tiles/tile-empty.aseprite"),
|
TileState::Empty => asset_server.load("tiles/tile-empty.aseprite"),
|
||||||
TileState::Occupied => asset_server.load("tiles/tile-occupied.aseprite"),
|
TileState::Occupied { .. } => asset_server.load("tiles/tile-occupied.aseprite"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let scale: Vec3 = match state {
|
||||||
|
TileState::Occupied {
|
||||||
|
seed, growth_stage, ..
|
||||||
|
} => {
|
||||||
|
let max_stages = seed
|
||||||
|
.get_seed_config(&game_config)
|
||||||
|
.map(|config| config.growth_stages)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
if max_stages > 0 {
|
||||||
|
let progress = (*growth_stage as f32 / max_stages as f32).min(1.0);
|
||||||
|
Vec3::splat(0.3 + (progress * 0.7))
|
||||||
|
} else {
|
||||||
|
Vec3::ONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Vec3::ONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
for child in children.iter() {
|
||||||
|
if let Ok((mut visibility, mut transform, mut sprite)) = crop_query.get_mut(child) {
|
||||||
|
*visibility = match state {
|
||||||
|
TileState::Occupied { .. } => Visibility::Visible,
|
||||||
|
_ => Visibility::Hidden,
|
||||||
|
};
|
||||||
|
transform.scale = scale;
|
||||||
|
|
||||||
|
if let TileState::Occupied { withered: true, .. } = state {
|
||||||
|
sprite.name = "Wither".into();
|
||||||
|
} else {
|
||||||
|
sprite.name = "Crop".into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok((mut visibility, mut transform)) = water_query.get_mut(child) {
|
||||||
|
*visibility = match state {
|
||||||
|
TileState::Occupied { watered: true, .. } => Visibility::Visible,
|
||||||
|
_ => Visibility::Hidden,
|
||||||
|
};
|
||||||
|
transform.scale = scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use super::errors::GridError;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub fn grid_start_x(grid_width: u32) -> f32 {
|
pub fn grid_start_x(grid_width: u32) -> f32 {
|
||||||
@@ -8,24 +9,25 @@ pub fn grid_start_y(grid_height: u32) -> f32 {
|
|||||||
-(grid_height as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0
|
-(grid_height as f32 * TILE_SIZE) / 2.0 + TILE_SIZE / 2.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn world_to_grid_coords(world_pos: Vec3, grid_width: u32, grid_height: u32) -> (u32, u32) {
|
pub fn world_to_grid_coords(
|
||||||
|
world_pos: Vec3,
|
||||||
|
grid_width: u32,
|
||||||
|
grid_height: u32,
|
||||||
|
) -> Result<(u32, u32), GridError> {
|
||||||
let start_x = grid_start_x(grid_width);
|
let start_x = grid_start_x(grid_width);
|
||||||
let start_y = grid_start_y(grid_height);
|
let start_y = grid_start_y(grid_height);
|
||||||
|
|
||||||
let x = ((world_pos.x - start_x + TILE_SIZE / 2.0) / TILE_SIZE).floor();
|
let x = ((world_pos.x - start_x + TILE_SIZE / 2.0) / TILE_SIZE).floor();
|
||||||
let y = ((world_pos.y - start_y + TILE_SIZE / 2.0) / TILE_SIZE).floor();
|
let y = ((world_pos.y - start_y + TILE_SIZE / 2.0) / TILE_SIZE).floor();
|
||||||
|
|
||||||
let mut x_u32 = x as u32;
|
if x >= grid_width as f32 || y >= grid_height as f32 || x < 0.0 || y < 0.0 {
|
||||||
let mut y_u32 = y as u32;
|
return Err(GridError::OutOfBounds {
|
||||||
|
x: x as i32,
|
||||||
if x_u32 >= grid_width {
|
y: x as i32,
|
||||||
x_u32 = grid_width - 1;
|
});
|
||||||
}
|
|
||||||
if y_u32 >= grid_height {
|
|
||||||
y_u32 = grid_height - 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(x_u32, y_u32)
|
Ok((x as u32, y as u32))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn grid_to_world_coords(
|
pub fn grid_to_world_coords(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::features::inventory;
|
|
||||||
use crate::features::phase::components::TimerSettings;
|
use crate::features::phase::components::TimerSettings;
|
||||||
use crate::features::savegame::messages::SavegameDumpMessage;
|
use crate::features::savegame::messages::SavegameDumpMessage;
|
||||||
|
use crate::features::{inventory, shop};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use components::*;
|
use components::*;
|
||||||
use ui::*;
|
use ui::*;
|
||||||
@@ -41,6 +41,15 @@ fn setup(mut commands: Commands) {
|
|||||||
children![
|
children![
|
||||||
text_with_component(TextType::Phase, "...", 16.0, Color::WHITE),
|
text_with_component(TextType::Phase, "...", 16.0, Color::WHITE),
|
||||||
text_with_component(TextType::Timer, "...", 16.0, Color::WHITE),
|
text_with_component(TextType::Timer, "...", 16.0, Color::WHITE),
|
||||||
|
button(
|
||||||
|
shop::components::ButtonType::ShopOpen,
|
||||||
|
ButtonVariant::Secondary,
|
||||||
|
Node {
|
||||||
|
padding: UiRect::all(px(10)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
|color| text("Shop [P]", 16.0, color)
|
||||||
|
),
|
||||||
button(
|
button(
|
||||||
inventory::components::ButtonType::InventoryOpen,
|
inventory::components::ButtonType::InventoryOpen,
|
||||||
ButtonVariant::Secondary,
|
ButtonVariant::Secondary,
|
||||||
@@ -48,8 +57,7 @@ fn setup(mut commands: Commands) {
|
|||||||
padding: UiRect::all(px(10)),
|
padding: UiRect::all(px(10)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"Inventar",
|
|color| text("Inventar", 16.0, color)
|
||||||
16.0
|
|
||||||
),
|
),
|
||||||
button(
|
button(
|
||||||
ButtonType::SettingsOpen,
|
ButtonType::SettingsOpen,
|
||||||
@@ -58,8 +66,7 @@ fn setup(mut commands: Commands) {
|
|||||||
padding: UiRect::all(px(10)),
|
padding: UiRect::all(px(10)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"Einstellungen",
|
|color| text("Einstellungen", 16.0, color)
|
||||||
16.0
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub fn open_settings(commands: &mut Commands) {
|
|||||||
},
|
},
|
||||||
ZIndex(1),
|
ZIndex(1),
|
||||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
||||||
|
GlobalTransform::default(),
|
||||||
))
|
))
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
parent
|
parent
|
||||||
@@ -42,8 +43,7 @@ pub fn open_settings(commands: &mut Commands) {
|
|||||||
height: px(40),
|
height: px(40),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"X",
|
|color| text("X", 24.0, color)
|
||||||
24.0
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
@@ -58,8 +58,7 @@ pub fn open_settings(commands: &mut Commands) {
|
|||||||
padding: UiRect::all(px(10)),
|
padding: UiRect::all(px(10)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"Spiel verlassen",
|
|color| text("Spiel verlassen", 24.0, color)
|
||||||
24.0,
|
|
||||||
));
|
));
|
||||||
|
|
||||||
parent.spawn(button(
|
parent.spawn(button(
|
||||||
@@ -69,8 +68,7 @@ pub fn open_settings(commands: &mut Commands) {
|
|||||||
padding: UiRect::all(px(10)),
|
padding: UiRect::all(px(10)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"Spiel speichern",
|
|color| text("Spiel speichern", 24.0, color)
|
||||||
24.0,
|
|
||||||
));
|
));
|
||||||
|
|
||||||
parent.spawn((
|
parent.spawn((
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ fn timer_settings_part(input: SettingsTimerInput, amount: u32) -> impl Bundle {
|
|||||||
width: percent(100),
|
width: percent(100),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"+",
|
|color| text("+", 12.0, color)
|
||||||
12.0
|
|
||||||
),
|
),
|
||||||
text_with_component(input.clone(), "--", 24.0, Color::WHITE),
|
text_with_component(input.clone(), "--", 24.0, Color::WHITE),
|
||||||
button(
|
button(
|
||||||
@@ -43,8 +42,7 @@ fn timer_settings_part(input: SettingsTimerInput, amount: u32) -> impl Bundle {
|
|||||||
width: percent(100),
|
width: percent(100),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"-",
|
|color| text("-", 12.0, color)
|
||||||
12.0
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
use crate::features::{
|
use crate::features::{
|
||||||
|
input::utils::mouse_to_grid,
|
||||||
phase::messages::{NextPhaseMessage, PhaseTimerPauseMessage},
|
phase::messages::{NextPhaseMessage, PhaseTimerPauseMessage},
|
||||||
pom::messages::InvalidMoveMessage,
|
pom::messages::InvalidMoveMessage,
|
||||||
|
shop::ui::open_shop,
|
||||||
};
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use bevy::input::mouse::MouseButton;
|
use bevy::input::mouse::MouseButton;
|
||||||
use bevy::window::PrimaryWindow;
|
use bevy::window::PrimaryWindow;
|
||||||
|
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
pub struct InputPlugin;
|
pub struct InputPlugin;
|
||||||
|
|
||||||
impl Plugin for InputPlugin {
|
impl Plugin for InputPlugin {
|
||||||
@@ -15,11 +19,15 @@ impl Plugin for InputPlugin {
|
|||||||
app.add_systems(Update, move_click.run_if(in_state(AppState::GameScreen)));
|
app.add_systems(Update, move_click.run_if(in_state(AppState::GameScreen)));
|
||||||
|
|
||||||
app.add_message::<InteractStartMessage>();
|
app.add_message::<InteractStartMessage>();
|
||||||
|
app.add_message::<TileClickMessage>();
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
interact_click.run_if(in_state(AppState::GameScreen)),
|
interact_click.run_if(in_state(AppState::GameScreen)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
app.add_systems(Update, debug_click.run_if(in_state(AppState::GameScreen)));
|
||||||
|
|
||||||
app.add_message::<PhaseTimerPauseMessage>();
|
app.add_message::<PhaseTimerPauseMessage>();
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
@@ -28,6 +36,8 @@ impl Plugin for InputPlugin {
|
|||||||
|
|
||||||
app.add_message::<NextPhaseMessage>();
|
app.add_message::<NextPhaseMessage>();
|
||||||
app.add_systems(Update, next_phase.run_if(in_state(AppState::GameScreen)));
|
app.add_systems(Update, next_phase.run_if(in_state(AppState::GameScreen)));
|
||||||
|
|
||||||
|
app.add_systems(Update, shop_keybind.run_if(in_state(AppState::GameScreen)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +48,7 @@ fn move_click(
|
|||||||
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||||
config: Res<GameConfig>,
|
config: Res<GameConfig>,
|
||||||
phase: Res<CurrentPhase>,
|
phase: Res<CurrentPhase>,
|
||||||
|
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
|
||||||
) {
|
) {
|
||||||
match phase.0 {
|
match phase.0 {
|
||||||
Phase::Focus { .. } => return,
|
Phase::Focus { .. } => return,
|
||||||
@@ -45,15 +56,9 @@ fn move_click(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if mouse_btn.just_pressed(MouseButton::Right) {
|
if mouse_btn.just_pressed(MouseButton::Right) {
|
||||||
let (cam, cam_transform) = *camera;
|
let Some((x, y)) = mouse_to_grid(window, camera, config, ui_query) else {
|
||||||
|
|
||||||
let Some(cursor_pos) = window.cursor_position() else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Ok(world_pos) = cam.viewport_to_world(cam_transform, cursor_pos) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let (x, y) = world_to_grid_coords(world_pos.origin, config.grid_width, config.grid_height);
|
|
||||||
|
|
||||||
println!("Move Click: ({}, {})", x, y);
|
println!("Move Click: ({}, {})", x, y);
|
||||||
move_messages.write(MoveMessage { x, y });
|
move_messages.write(MoveMessage { x, y });
|
||||||
@@ -61,14 +66,41 @@ fn move_click(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn interact_click(
|
fn interact_click(
|
||||||
mut interact_messages: MessageWriter<InteractStartMessage>,
|
mut tile_click_messages: MessageWriter<TileClickMessage>,
|
||||||
mouse_btn: Res<ButtonInput<MouseButton>>,
|
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
window: Single<&Window, With<PrimaryWindow>>,
|
window: Single<&Window, With<PrimaryWindow>>,
|
||||||
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||||
config: Res<GameConfig>,
|
config: Res<GameConfig>,
|
||||||
phase: Res<CurrentPhase>,
|
phase: Res<CurrentPhase>,
|
||||||
// for debug
|
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
|
||||||
grid: ResMut<Grid>,
|
) {
|
||||||
|
match phase.0 {
|
||||||
|
Phase::Focus { .. } => return,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mouse_btn.just_pressed(MouseButton::Left) {
|
||||||
|
if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some((x, y)) = mouse_to_grid(window, camera, config, ui_query) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
tile_click_messages.write(TileClickMessage { x, y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_click(
|
||||||
|
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
window: Single<&Window, With<PrimaryWindow>>,
|
||||||
|
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||||
|
config: Res<GameConfig>,
|
||||||
|
phase: Res<CurrentPhase>,
|
||||||
|
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
|
||||||
|
grid: Res<Grid>,
|
||||||
tile_query: Query<&mut TileState>,
|
tile_query: Query<&mut TileState>,
|
||||||
) {
|
) {
|
||||||
match phase.0 {
|
match phase.0 {
|
||||||
@@ -77,31 +109,32 @@ fn interact_click(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if mouse_btn.just_pressed(MouseButton::Left) {
|
if mouse_btn.just_pressed(MouseButton::Left) {
|
||||||
let (cam, cam_transform) = *camera;
|
if !keys.pressed(KeyCode::ShiftLeft) && !keys.pressed(KeyCode::ShiftRight) {
|
||||||
|
return;
|
||||||
let Some(cursor_pos) = window.cursor_position() else {
|
}
|
||||||
|
let Some((x, y)) = mouse_to_grid(window, camera, config, ui_query) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Ok(world_pos) = cam.viewport_to_world(cam_transform, cursor_pos) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let (x, y) = world_to_grid_coords(world_pos.origin, config.grid_width, config.grid_height);
|
|
||||||
|
|
||||||
println!("Interact Click: ({}, {})", x, y);
|
println!("Debug Toggle Click: ({}, {})", x, y);
|
||||||
interact_messages.write(InteractStartMessage { x, y });
|
|
||||||
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
grid.map_tile_state(
|
grid.map_tile_state(
|
||||||
(x, y),
|
(x, y),
|
||||||
|state| match state {
|
|state| match state {
|
||||||
TileState::Unclaimed => TileState::Empty,
|
TileState::Unclaimed => TileState::Empty,
|
||||||
TileState::Empty => TileState::Occupied,
|
TileState::Empty => TileState::Occupied {
|
||||||
TileState::Occupied => TileState::Unclaimed,
|
seed: ItemType::BerrySeed {
|
||||||
|
name: "Debug".into(),
|
||||||
|
},
|
||||||
|
watered: false,
|
||||||
|
growth_stage: 0,
|
||||||
|
withered: false,
|
||||||
|
dry_counter: 0,
|
||||||
|
},
|
||||||
|
TileState::Occupied { .. } => TileState::Unclaimed,
|
||||||
},
|
},
|
||||||
tile_query,
|
tile_query,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap_or_else(|_| ());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,3 +152,14 @@ fn next_phase(mut messages: MessageWriter<NextPhaseMessage>, keys: Res<ButtonInp
|
|||||||
messages.write(NextPhaseMessage);
|
messages.write(NextPhaseMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shop_keybind(
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
game_config: Res<GameConfig>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
) {
|
||||||
|
if keys.just_pressed(KeyCode::KeyP) {
|
||||||
|
open_shop(&mut commands, &game_config, &asset_server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
28
src/features/input/utils.rs
Normal file
28
src/features/input/utils.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
use bevy::window::PrimaryWindow;
|
||||||
|
|
||||||
|
pub fn mouse_to_grid(
|
||||||
|
window: Single<&Window, With<PrimaryWindow>>,
|
||||||
|
camera: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||||
|
config: Res<GameConfig>,
|
||||||
|
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
|
||||||
|
) -> Option<(u32, u32)> {
|
||||||
|
let (cam, cam_transform) = *camera;
|
||||||
|
|
||||||
|
let Some(cursor_pos) = window.cursor_position() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if ui_blocks(window, cursor_pos, ui_query) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let Ok(world_pos) = cam.viewport_to_world(cam_transform, cursor_pos) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Ok(grid_pos) =
|
||||||
|
world_to_grid_coords(world_pos.origin, config.grid_width, config.grid_height)
|
||||||
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(grid_pos)
|
||||||
|
}
|
||||||
@@ -1,30 +1,104 @@
|
|||||||
|
use crate::features::config::components::BerrySeedConfig;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum ItemType {
|
pub enum ItemType {
|
||||||
Berry,
|
Berry,
|
||||||
|
BerrySeed { name: String },
|
||||||
|
Shovel,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ItemType {
|
impl ItemType {
|
||||||
pub fn singular(&self) -> String {
|
pub fn singular(&self, game_config: &GameConfig) -> String {
|
||||||
match self {
|
match self {
|
||||||
ItemType::Berry => "Beere",
|
ItemType::Berry => "Beere".into(),
|
||||||
|
ItemType::Shovel => "Schaufel".into(),
|
||||||
|
ItemType::BerrySeed { name } => {
|
||||||
|
let seed_config = game_config.berry_seeds.iter().find(|s| s.name == *name);
|
||||||
|
seed_config
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.unwrap_or_else(|| format!("Unbekannter Samen ({})", name))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn plural(&self) -> String {
|
pub fn plural(&self, game_config: &GameConfig) -> String {
|
||||||
match self {
|
match self {
|
||||||
ItemType::Berry => "Beeren",
|
ItemType::Berry => "Beeren".into(),
|
||||||
|
ItemType::Shovel => "Schaufeln".into(),
|
||||||
|
ItemType::BerrySeed { name } => {
|
||||||
|
let seed_config = game_config.berry_seeds.iter().find(|s| s.name == *name);
|
||||||
|
seed_config
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.unwrap_or_else(|| format!("Unbekannte Samen ({})", name))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn description(&self) -> String {
|
pub fn description(&self, game_config: &GameConfig) -> String {
|
||||||
match self {
|
match self {
|
||||||
ItemType::Berry => "Von Pflanzen erntbar. Kann im Shop zum Einkaufen benutzt werden.",
|
ItemType::Berry => {
|
||||||
|
"Von Pflanzen erntbar. Kann im Shop zum Einkaufen benutzt werden.".into()
|
||||||
|
}
|
||||||
|
ItemType::Shovel => "Im Shop kaufbar. Schaltet ein neues Feld im Garten frei. Preis steigt bei jedem Kauf!".into(),
|
||||||
|
ItemType::BerrySeed { name } => {
|
||||||
|
let seed_config = game_config.berry_seeds.iter().find(|s| s.name == *name);
|
||||||
|
if let Some(s) = seed_config {
|
||||||
|
format!(
|
||||||
|
"Im Shop kaufbar. Kann eingepflanzt werden. Nach {} Fokus-Phasen ausgewachsen. Erhalte beim Ernten {} {}.",
|
||||||
|
s.growth_stages,
|
||||||
|
s.grants,
|
||||||
|
match s.grants {
|
||||||
|
1 => ItemType::Berry.singular(game_config),
|
||||||
|
_ => ItemType::Berry.plural(game_config),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("Unbekannter Samen ({})", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_seed_config<'a>(&self, game_config: &'a GameConfig) -> Option<&'a BerrySeedConfig> {
|
||||||
|
match self {
|
||||||
|
ItemType::Berry | ItemType::Shovel => None,
|
||||||
|
ItemType::BerrySeed { name } => {
|
||||||
|
game_config.berry_seeds.iter().find(|s| s.name == *name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sprite(
|
||||||
|
&self,
|
||||||
|
asset_server: &Res<AssetServer>,
|
||||||
|
game_config: &GameConfig,
|
||||||
|
) -> AseSlice {
|
||||||
|
match self {
|
||||||
|
ItemType::Berry => AseSlice {
|
||||||
|
name: "Berry".into(),
|
||||||
|
aseprite: asset_server.load("berry.aseprite"),
|
||||||
|
},
|
||||||
|
ItemType::Shovel => AseSlice {
|
||||||
|
name: "Berry".into(),
|
||||||
|
aseprite: asset_server.load("berry.aseprite"),
|
||||||
|
},
|
||||||
|
ItemType::BerrySeed { name } => {
|
||||||
|
let seed_config = game_config.berry_seeds.iter().find(|s| s.name == *name);
|
||||||
|
if let Some(s) = seed_config {
|
||||||
|
AseSlice {
|
||||||
|
name: s.slice.clone(),
|
||||||
|
aseprite: asset_server.load("seed.aseprite"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback for unknown seed
|
||||||
|
AseSlice {
|
||||||
|
name: "Seed1".into(),
|
||||||
|
aseprite: asset_server.load("seed.aseprite"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +113,84 @@ pub struct Inventory {
|
|||||||
pub items: Vec<Entity>,
|
pub items: Vec<Entity>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Inventory {
|
||||||
|
pub fn has_item(&self, items_query: Query<&ItemStack>) -> bool {
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.map(|entity| items_query.get(*entity).ok())
|
||||||
|
.find(|option| option.is_some())
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_item_stack(
|
||||||
|
&mut self,
|
||||||
|
commands: &mut Commands,
|
||||||
|
items_query: &mut Query<&mut ItemStack>,
|
||||||
|
item_type_to_update: ItemType,
|
||||||
|
amount_delta: i32,
|
||||||
|
) -> bool {
|
||||||
|
let mut target_entity_index: Option<usize> = None;
|
||||||
|
let mut current_stack_amount: u32 = 0;
|
||||||
|
let mut entity_id_to_update: Option<Entity> = None;
|
||||||
|
|
||||||
|
// Try to find an existing stack of the item
|
||||||
|
for (i, &entity) in self.items.iter().enumerate() {
|
||||||
|
if let Ok(stack) = items_query.get(entity) {
|
||||||
|
if stack.item_type == item_type_to_update {
|
||||||
|
target_entity_index = Some(i);
|
||||||
|
current_stack_amount = stack.amount;
|
||||||
|
entity_id_to_update = Some(entity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match amount_delta {
|
||||||
|
val if val > 0 => {
|
||||||
|
// Add items
|
||||||
|
let add_amount = amount_delta as u32;
|
||||||
|
if let Some(entity) = entity_id_to_update {
|
||||||
|
if let Ok(mut stack) = items_query.get_mut(entity) {
|
||||||
|
stack.amount += add_amount;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Item not found, create a new stack
|
||||||
|
let new_item_stack = ItemStack {
|
||||||
|
item_type: item_type_to_update,
|
||||||
|
amount: add_amount,
|
||||||
|
};
|
||||||
|
let id = commands.spawn(new_item_stack).id();
|
||||||
|
self.items.push(id);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
val if val < 0 => {
|
||||||
|
// Remove items
|
||||||
|
let remove_amount = amount_delta.abs() as u32;
|
||||||
|
|
||||||
|
let Some(entity) = entity_id_to_update else {
|
||||||
|
return false; // Item not found for removal
|
||||||
|
};
|
||||||
|
if current_stack_amount < remove_amount {
|
||||||
|
return false; // Not enough items
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(mut stack) = items_query.get_mut(entity) {
|
||||||
|
stack.amount -= remove_amount;
|
||||||
|
if stack.amount == 0 {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
if let Some(index) = target_entity_index {
|
||||||
|
self.items.remove(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub enum RootMarker {
|
pub enum RootMarker {
|
||||||
Inventory,
|
Inventory,
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ impl Plugin for InventoryPlugin {
|
|||||||
app.init_resource::<Inventory>();
|
app.init_resource::<Inventory>();
|
||||||
|
|
||||||
app.add_systems(Update, buttons.run_if(in_state(AppState::GameScreen)));
|
app.add_systems(Update, buttons.run_if(in_state(AppState::GameScreen)));
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
app.add_systems(Update, debug_modify_berries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,12 +22,14 @@ fn buttons(
|
|||||||
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||||
itemstack_query: Query<&ItemStack>,
|
itemstack_query: Query<&ItemStack>,
|
||||||
root_query: Query<(Entity, &RootMarker)>,
|
root_query: Query<(Entity, &RootMarker)>,
|
||||||
|
game_config: Res<GameConfig>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
) {
|
) {
|
||||||
for (interaction, button_type) in &mut interaction_query {
|
for (interaction, button_type) in &mut interaction_query {
|
||||||
match *interaction {
|
match *interaction {
|
||||||
Interaction::Pressed => match button_type {
|
Interaction::Pressed => match button_type {
|
||||||
ButtonType::InventoryOpen => {
|
ButtonType::InventoryOpen => {
|
||||||
open_inventory(&mut commands, itemstack_query);
|
open_inventory(&mut commands, itemstack_query, &game_config, &asset_server);
|
||||||
}
|
}
|
||||||
ButtonType::InventoryClose => {
|
ButtonType::InventoryClose => {
|
||||||
for (entity, root) in root_query.iter() {
|
for (entity, root) in root_query.iter() {
|
||||||
@@ -38,3 +43,21 @@ fn buttons(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
fn debug_modify_berries(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
mut items: Query<&mut ItemStack>,
|
||||||
|
keys: Res<ButtonInput<KeyCode>>,
|
||||||
|
) {
|
||||||
|
if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
|
||||||
|
if keys.just_pressed(KeyCode::ArrowUp) {
|
||||||
|
println!("Adding 1 berry using debug bind");
|
||||||
|
inventory.update_item_stack(&mut commands, &mut items, ItemType::Berry, 1);
|
||||||
|
} else if keys.just_pressed(KeyCode::ArrowDown) {
|
||||||
|
println!("Removing 1 berry using debug bind");
|
||||||
|
inventory.update_item_stack(&mut commands, &mut items, ItemType::Berry, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
use super::super::components::{ButtonType, RootMarker};
|
use super::super::components::{ButtonType, RootMarker};
|
||||||
|
use crate::prelude::GameConfig;
|
||||||
use crate::{features::inventory::ui::list_itemstack, prelude::*};
|
use crate::{features::inventory::ui::list_itemstack, prelude::*};
|
||||||
|
|
||||||
pub fn open_inventory(commands: &mut Commands, items: Query<&ItemStack>) {
|
pub fn open_inventory(
|
||||||
|
commands: &mut Commands,
|
||||||
|
items: Query<&ItemStack>,
|
||||||
|
game_config: &Res<GameConfig>,
|
||||||
|
asset_server: &Res<AssetServer>,
|
||||||
|
) {
|
||||||
commands
|
commands
|
||||||
.spawn((
|
.spawn((
|
||||||
RootMarker::Inventory,
|
RootMarker::Inventory,
|
||||||
@@ -13,6 +19,7 @@ pub fn open_inventory(commands: &mut Commands, items: Query<&ItemStack>) {
|
|||||||
},
|
},
|
||||||
ZIndex(1),
|
ZIndex(1),
|
||||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
||||||
|
GlobalTransform::default(),
|
||||||
))
|
))
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
parent
|
parent
|
||||||
@@ -42,8 +49,7 @@ pub fn open_inventory(commands: &mut Commands, items: Query<&ItemStack>) {
|
|||||||
height: px(40),
|
height: px(40),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"X",
|
|color| text("X", 24.0, color)
|
||||||
24.0
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
@@ -56,7 +62,7 @@ pub fn open_inventory(commands: &mut Commands, items: Query<&ItemStack>) {
|
|||||||
})
|
})
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
for itemstack in items.iter() {
|
for itemstack in items.iter() {
|
||||||
parent.spawn(list_itemstack(itemstack));
|
parent.spawn(list_itemstack(itemstack, game_config, asset_server));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,33 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub fn list_itemstack(itemstack: &ItemStack) -> impl Bundle {
|
pub fn list_itemstack(
|
||||||
|
itemstack: &ItemStack,
|
||||||
|
game_config: &GameConfig,
|
||||||
|
asset_server: &Res<AssetServer>,
|
||||||
|
) -> impl Bundle {
|
||||||
let name = match itemstack.amount {
|
let name = match itemstack.amount {
|
||||||
1 => itemstack.item_type.singular(),
|
1 => itemstack.item_type.singular(game_config),
|
||||||
_ => itemstack.item_type.plural(),
|
_ => itemstack.item_type.plural(game_config),
|
||||||
};
|
};
|
||||||
|
|
||||||
(
|
(
|
||||||
Node {
|
Node {
|
||||||
|
width: percent(100),
|
||||||
padding: UiRect::all(px(4)),
|
padding: UiRect::all(px(4)),
|
||||||
..Node::hstack(px(8))
|
..Node::hstack(px(8))
|
||||||
},
|
},
|
||||||
BackgroundColor(ButtonVariant::Secondary.normal_background()),
|
|
||||||
BorderRadius::all(px(10)),
|
BorderRadius::all(px(10)),
|
||||||
children![
|
children![
|
||||||
(
|
(
|
||||||
// Placeholder for icon
|
|
||||||
Node {
|
Node {
|
||||||
height: percent(100),
|
height: percent(100),
|
||||||
aspect_ratio: Some(1.0),
|
aspect_ratio: Some(1.0),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
BackgroundColor(ButtonVariant::Secondary.hover_background()),
|
BackgroundColor(ButtonVariant::Secondary.hover_background()),
|
||||||
BorderRadius::all(px(10))
|
BorderRadius::all(px(10)),
|
||||||
|
itemstack.item_type.get_sprite(asset_server, game_config),
|
||||||
|
ImageNode::default()
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Node {
|
Node {
|
||||||
@@ -36,7 +41,11 @@ pub fn list_itemstack(itemstack: &ItemStack) -> impl Bundle {
|
|||||||
14.0,
|
14.0,
|
||||||
Color::WHITE
|
Color::WHITE
|
||||||
),
|
),
|
||||||
text(itemstack.item_type.description(), 10.0, Color::WHITE)
|
text(
|
||||||
|
itemstack.item_type.description(game_config),
|
||||||
|
10.0,
|
||||||
|
Color::WHITE
|
||||||
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod inventory;
|
|||||||
pub mod phase;
|
pub mod phase;
|
||||||
pub mod pom;
|
pub mod pom;
|
||||||
pub mod savegame;
|
pub mod savegame;
|
||||||
|
pub mod shop;
|
||||||
pub mod start_screen;
|
pub mod start_screen;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
@@ -20,5 +21,6 @@ pub use inventory::InventoryPlugin;
|
|||||||
pub use phase::PhasePlugin;
|
pub use phase::PhasePlugin;
|
||||||
pub use pom::PomPlugin;
|
pub use pom::PomPlugin;
|
||||||
pub use savegame::SavegamePlugin;
|
pub use savegame::SavegamePlugin;
|
||||||
|
pub use shop::ShopPlugin;
|
||||||
pub use start_screen::StartScreenPlugin;
|
pub use start_screen::StartScreenPlugin;
|
||||||
pub use ui::UiPlugin;
|
pub use ui::UiPlugin;
|
||||||
|
|||||||
@@ -119,43 +119,94 @@ fn handle_pause(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_continue(
|
pub fn next_phase(
|
||||||
mut messages: MessageReader<NextPhaseMessage>,
|
current_phase: &mut CurrentPhase,
|
||||||
mut phase_res: ResMut<CurrentPhase>,
|
session_tracker: &mut SessionTracker,
|
||||||
mut session_tracker: ResMut<SessionTracker>,
|
settings: &TimerSettings,
|
||||||
settings: Res<TimerSettings>,
|
|
||||||
) {
|
) {
|
||||||
for _ in messages.read() {
|
if let Phase::Finished { completed_phase } = ¤t_phase.0 {
|
||||||
let phase = &mut phase_res.0;
|
|
||||||
|
|
||||||
if let Phase::Finished { completed_phase } = phase {
|
|
||||||
match **completed_phase {
|
match **completed_phase {
|
||||||
Phase::Focus { .. } => {
|
Phase::Focus { .. } => {
|
||||||
session_tracker.completed_focus_phases += 1;
|
session_tracker.completed_focus_phases += 1;
|
||||||
|
|
||||||
// TODO: add berry grant logic
|
|
||||||
|
|
||||||
let is_long_break = session_tracker.completed_focus_phases > 0
|
let is_long_break = session_tracker.completed_focus_phases > 0
|
||||||
&& session_tracker.completed_focus_phases % settings.long_break_interval
|
&& session_tracker.completed_focus_phases % settings.long_break_interval == 0;
|
||||||
== 0;
|
|
||||||
|
|
||||||
if is_long_break {
|
if is_long_break {
|
||||||
*phase = Phase::Break {
|
current_phase.0 = Phase::Break {
|
||||||
duration: settings.long_break_duration as f32,
|
duration: settings.long_break_duration as f32,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
*phase = Phase::Break {
|
current_phase.0 = Phase::Break {
|
||||||
duration: settings.short_break_duration as f32,
|
duration: settings.short_break_duration as f32,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Phase::Break { .. } => {
|
Phase::Break { .. } => {
|
||||||
*phase = Phase::Focus {
|
current_phase.0 = Phase::Focus {
|
||||||
duration: 25.0 * 60.0,
|
duration: settings.focus_duration as f32,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_continue(
|
||||||
|
mut messages: MessageReader<NextPhaseMessage>,
|
||||||
|
mut phase_res: ResMut<CurrentPhase>,
|
||||||
|
mut session_tracker: ResMut<SessionTracker>,
|
||||||
|
settings: Res<TimerSettings>,
|
||||||
|
mut tile_query: Query<&mut TileState>,
|
||||||
|
game_config: Res<GameConfig>,
|
||||||
|
) {
|
||||||
|
for _ in messages.read() {
|
||||||
|
let entering_break = if let Phase::Finished { completed_phase } = &phase_res.0 {
|
||||||
|
matches!(**completed_phase, Phase::Focus { .. })
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
next_phase(&mut phase_res, &mut session_tracker, &settings);
|
||||||
|
|
||||||
|
if entering_break {
|
||||||
|
println!("Growing crops and resetting watered state.");
|
||||||
|
for mut state in tile_query.iter_mut() {
|
||||||
|
if let TileState::Occupied {
|
||||||
|
seed,
|
||||||
|
watered,
|
||||||
|
growth_stage,
|
||||||
|
withered,
|
||||||
|
dry_counter,
|
||||||
|
} = &*state
|
||||||
|
{
|
||||||
|
let mut new_stage = *growth_stage;
|
||||||
|
let mut new_withered = *withered;
|
||||||
|
let mut new_dry_counter = *dry_counter;
|
||||||
|
|
||||||
|
if *watered {
|
||||||
|
new_dry_counter = 0;
|
||||||
|
if let Some(config) = seed.get_seed_config(&game_config) {
|
||||||
|
if new_stage < config.growth_stages && !new_withered {
|
||||||
|
new_stage += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
new_dry_counter += 1;
|
||||||
|
if new_dry_counter >= 2 {
|
||||||
|
new_withered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*state = TileState::Occupied {
|
||||||
|
seed: seed.clone(),
|
||||||
|
watered: false,
|
||||||
|
growth_stage: new_stage,
|
||||||
|
withered: new_withered,
|
||||||
|
dry_counter: new_dry_counter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
182
src/features/pom/actions.rs
Normal file
182
src/features/pom/actions.rs
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum InteractionAction {
|
||||||
|
Plant(ItemType),
|
||||||
|
Water,
|
||||||
|
Harvest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InteractionAction {
|
||||||
|
pub fn get_name(&self, game_config: &GameConfig) -> String {
|
||||||
|
match self {
|
||||||
|
InteractionAction::Plant(item) => format!("Pflanze {}", item.singular(game_config)),
|
||||||
|
InteractionAction::Water => "Gießen".into(),
|
||||||
|
InteractionAction::Harvest => "Ernten".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sprite(
|
||||||
|
&self,
|
||||||
|
asset_server: &Res<AssetServer>,
|
||||||
|
game_config: &GameConfig,
|
||||||
|
) -> Option<AseSlice> {
|
||||||
|
match self {
|
||||||
|
InteractionAction::Plant(item) => Some(item.get_sprite(asset_server, game_config)),
|
||||||
|
InteractionAction::Water => None,
|
||||||
|
InteractionAction::Harvest => Some(ItemType::Berry.get_sprite(asset_server, game_config)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_options(
|
||||||
|
tile_state: &TileState,
|
||||||
|
inventory: &Inventory,
|
||||||
|
item_query: Query<&ItemStack>,
|
||||||
|
game_config: &GameConfig,
|
||||||
|
) -> Vec<InteractionAction> {
|
||||||
|
let mut options: Vec<InteractionAction> = vec![];
|
||||||
|
|
||||||
|
match tile_state {
|
||||||
|
TileState::Occupied {
|
||||||
|
watered,
|
||||||
|
withered,
|
||||||
|
seed,
|
||||||
|
growth_stage,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if *withered {
|
||||||
|
options.push(InteractionAction::Harvest);
|
||||||
|
} else if let ItemType::BerrySeed { name } = seed {
|
||||||
|
if let Some(config) = game_config.berry_seeds.iter().find(|s| s.name == *name) {
|
||||||
|
if *growth_stage >= config.growth_stages {
|
||||||
|
options.push(InteractionAction::Harvest);
|
||||||
|
} else if !*watered {
|
||||||
|
options.push(InteractionAction::Water);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TileState::Empty => {
|
||||||
|
for &entity in &inventory.items {
|
||||||
|
let Ok(stack) = item_query.get(entity) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if stack.amount <= 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match &stack.item_type {
|
||||||
|
ItemType::BerrySeed { .. } => {
|
||||||
|
options.push(InteractionAction::Plant(stack.item_type.clone()));
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
options
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(
|
||||||
|
&self,
|
||||||
|
pos: (u32, u32),
|
||||||
|
grid: &Grid,
|
||||||
|
tile_query: &mut Query<&mut TileState>,
|
||||||
|
inventory: &mut Inventory,
|
||||||
|
item_stack_query: &mut Query<&mut ItemStack>,
|
||||||
|
commands: &mut Commands,
|
||||||
|
game_config: &GameConfig,
|
||||||
|
) {
|
||||||
|
let Ok(tile_entity) = grid.get_tile(pos) else {
|
||||||
|
println!("Error during interaction: Couldn't get tile_entity");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(mut tile_state) = tile_query.get_mut(tile_entity) else {
|
||||||
|
println!("Error during interaction: Couldn't get mut tile_state");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match self {
|
||||||
|
InteractionAction::Plant(seed_type) => {
|
||||||
|
if let TileState::Empty = *tile_state {
|
||||||
|
if inventory.update_item_stack(
|
||||||
|
commands,
|
||||||
|
item_stack_query,
|
||||||
|
seed_type.clone(),
|
||||||
|
-1,
|
||||||
|
) {
|
||||||
|
println!("Planting {:?}", seed_type);
|
||||||
|
*tile_state = TileState::Occupied {
|
||||||
|
seed: seed_type.clone(),
|
||||||
|
watered: false,
|
||||||
|
growth_stage: 0,
|
||||||
|
withered: false,
|
||||||
|
dry_counter: 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
println!("No {:?} in inventory!", seed_type);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Tile is not empty, cannot plant.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InteractionAction::Water => {
|
||||||
|
if let TileState::Occupied {
|
||||||
|
seed,
|
||||||
|
growth_stage,
|
||||||
|
withered,
|
||||||
|
..
|
||||||
|
} = &*tile_state
|
||||||
|
{
|
||||||
|
println!("Watering {:?}", seed);
|
||||||
|
*tile_state = TileState::Occupied {
|
||||||
|
seed: seed.clone(),
|
||||||
|
watered: true,
|
||||||
|
growth_stage: *growth_stage,
|
||||||
|
withered: *withered,
|
||||||
|
dry_counter: 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
println!("Tile is not occupied, cannot water.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InteractionAction::Harvest => {
|
||||||
|
if let TileState::Occupied {
|
||||||
|
seed,
|
||||||
|
withered,
|
||||||
|
growth_stage,
|
||||||
|
..
|
||||||
|
} = &*tile_state
|
||||||
|
{
|
||||||
|
let mut can_harvest = *withered;
|
||||||
|
|
||||||
|
if !can_harvest {
|
||||||
|
if let ItemType::BerrySeed { name } = seed {
|
||||||
|
if let Some(config) =
|
||||||
|
game_config.berry_seeds.iter().find(|s| s.name == *name)
|
||||||
|
{
|
||||||
|
if *growth_stage >= config.growth_stages {
|
||||||
|
can_harvest = true;
|
||||||
|
inventory.update_item_stack(
|
||||||
|
commands,
|
||||||
|
item_stack_query,
|
||||||
|
ItemType::Berry,
|
||||||
|
config.grants as i32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if can_harvest {
|
||||||
|
*tile_state = TileState::Empty;
|
||||||
|
} else {
|
||||||
|
println!("Cannot harvest: not withered and not fully grown.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::features::pom::actions::InteractionAction;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
@@ -36,3 +37,9 @@ impl MovingState {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct InteractionTarget {
|
||||||
|
pub target: Option<(u32, u32)>,
|
||||||
|
pub action: Option<InteractionAction>,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::features::pom::actions::InteractionAction;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
@@ -15,4 +16,11 @@ pub struct InvalidMoveMessage {
|
|||||||
pub struct InteractStartMessage {
|
pub struct InteractStartMessage {
|
||||||
pub x: u32,
|
pub x: u32,
|
||||||
pub y: u32,
|
pub y: u32,
|
||||||
|
pub action: InteractionAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
pub struct TileClickMessage {
|
||||||
|
pub x: u32,
|
||||||
|
pub y: u32,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,77 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use components::*;
|
use components::*;
|
||||||
use messages::InvalidMoveMessage;
|
use messages::{InteractStartMessage, InvalidMoveMessage};
|
||||||
|
use std::collections::VecDeque;
|
||||||
use utils::find_path;
|
use utils::find_path;
|
||||||
|
use utils::manhattan_distance;
|
||||||
|
|
||||||
|
pub mod actions;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
pub mod ui;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
pub struct PomPlugin;
|
pub struct PomPlugin;
|
||||||
|
|
||||||
impl Plugin for PomPlugin {
|
impl Plugin for PomPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_plugins(ui::PomUiPlugin);
|
||||||
app.add_systems(OnEnter(AppState::GameScreen), setup);
|
app.add_systems(OnEnter(AppState::GameScreen), setup);
|
||||||
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
app.add_systems(OnExit(AppState::GameScreen), cleanup);
|
||||||
|
|
||||||
app.add_systems(
|
app.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(handle_move, move_pom, update_pom).run_if(in_state(AppState::GameScreen)),
|
(
|
||||||
|
handle_move,
|
||||||
|
handle_interact,
|
||||||
|
move_pom,
|
||||||
|
update_pom,
|
||||||
|
perform_interaction,
|
||||||
|
draw_path,
|
||||||
|
)
|
||||||
|
.run_if(in_state(AppState::GameScreen)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_path(
|
||||||
|
mut gizmos: Gizmos,
|
||||||
|
query: Query<(&Transform, &PathQueue), With<Pom>>,
|
||||||
|
config: Res<GameConfig>,
|
||||||
|
) {
|
||||||
|
let Ok((transform, path_queue)) = query.single() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if path_queue.steps.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line_z = 0.5;
|
||||||
|
let mut current_pos = transform.translation;
|
||||||
|
current_pos.z = line_z;
|
||||||
|
|
||||||
|
for step in &path_queue.steps {
|
||||||
|
let next_pos = grid_to_world_coords(
|
||||||
|
step.0,
|
||||||
|
step.1,
|
||||||
|
Some(line_z),
|
||||||
|
config.grid_width,
|
||||||
|
config.grid_height,
|
||||||
|
);
|
||||||
|
|
||||||
|
gizmos.line(current_pos, next_pos, Color::srgba(1.0, 1.0, 1.0, 0.3));
|
||||||
|
current_pos = next_pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<GameConfig>) {
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>, config: Res<GameConfig>) {
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Pom,
|
Pom,
|
||||||
GridPosition { x: 0, y: 0 },
|
GridPosition { x: 0, y: 0 },
|
||||||
PathQueue::default(),
|
PathQueue::default(),
|
||||||
MovingState::default(),
|
MovingState::default(),
|
||||||
|
InteractionTarget::default(),
|
||||||
AseAnimation {
|
AseAnimation {
|
||||||
aseprite: asset_server.load("pom/pom-sleep.aseprite"),
|
aseprite: asset_server.load("pom/pom-sleep.aseprite"),
|
||||||
animation: Animation::tag("sleep-sit-start").with_repeat(AnimationRepeat::Loop),
|
animation: Animation::tag("sleep-sit-start").with_repeat(AnimationRepeat::Loop),
|
||||||
@@ -53,10 +98,14 @@ fn handle_move(
|
|||||||
mut invalid_move_messages: MessageWriter<InvalidMoveMessage>,
|
mut invalid_move_messages: MessageWriter<InvalidMoveMessage>,
|
||||||
grid: Res<Grid>,
|
grid: Res<Grid>,
|
||||||
tile_query: Query<&TileState>,
|
tile_query: Query<&TileState>,
|
||||||
mut pom_query: Query<(&GridPosition, &mut PathQueue)>,
|
mut pom_query: Query<(&GridPosition, &mut PathQueue, &mut InteractionTarget)>,
|
||||||
) {
|
) {
|
||||||
for message in move_messages.read() {
|
for message in move_messages.read() {
|
||||||
for (grid_pos, mut path_queue) in pom_query.iter_mut() {
|
for (grid_pos, mut path_queue, mut interaction_target) in pom_query.iter_mut() {
|
||||||
|
// Clear any pending interaction when moving manually
|
||||||
|
interaction_target.target = None;
|
||||||
|
interaction_target.action = None;
|
||||||
|
|
||||||
let grid_start = (grid_pos.x, grid_pos.y);
|
let grid_start = (grid_pos.x, grid_pos.y);
|
||||||
let start = path_queue.steps.front().unwrap_or(&grid_start);
|
let start = path_queue.steps.front().unwrap_or(&grid_start);
|
||||||
let end = (message.x, message.y);
|
let end = (message.x, message.y);
|
||||||
@@ -78,6 +127,104 @@ fn handle_move(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_interact(
|
||||||
|
mut interact_messages: MessageReader<InteractStartMessage>,
|
||||||
|
mut pom_query: Query<(&GridPosition, &mut PathQueue, &mut InteractionTarget)>,
|
||||||
|
grid: Res<Grid>,
|
||||||
|
tile_query: Query<&TileState>,
|
||||||
|
) {
|
||||||
|
for message in interact_messages.read() {
|
||||||
|
for (grid_pos, mut path_queue, mut interaction_target) in pom_query.iter_mut() {
|
||||||
|
let target_pos = (message.x, message.y);
|
||||||
|
let current_pos = (grid_pos.x, grid_pos.y);
|
||||||
|
|
||||||
|
// If we are already adjacent to the target, just set the target and clear path
|
||||||
|
if manhattan_distance(current_pos.0, current_pos.1, target_pos.0, target_pos.1) == 1 {
|
||||||
|
path_queue.steps.clear();
|
||||||
|
interaction_target.target = Some(target_pos);
|
||||||
|
interaction_target.action = Some(message.action.clone());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a path to an adjacent tile
|
||||||
|
let neighbors = [
|
||||||
|
(target_pos.0 as i32 + 1, target_pos.1 as i32),
|
||||||
|
(target_pos.0 as i32 - 1, target_pos.1 as i32),
|
||||||
|
(target_pos.0 as i32, target_pos.1 as i32 + 1),
|
||||||
|
(target_pos.0 as i32, target_pos.1 as i32 - 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut best_path: Option<VecDeque<(u32, u32)>> = None;
|
||||||
|
|
||||||
|
for (nx, ny) in neighbors {
|
||||||
|
if nx < 0 || ny < 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let neighbor_pos = (nx as u32, ny as u32);
|
||||||
|
|
||||||
|
if let Some(path) = find_path(current_pos, neighbor_pos, &grid, &tile_query) {
|
||||||
|
// Pick the shortest path
|
||||||
|
if best_path.as_ref().map_or(true, |p| path.len() < p.len()) {
|
||||||
|
best_path = Some(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(path) = best_path {
|
||||||
|
path_queue.steps = path;
|
||||||
|
interaction_target.target = Some(target_pos);
|
||||||
|
interaction_target.action = Some(message.action.clone());
|
||||||
|
} else {
|
||||||
|
println!("Cannot reach interaction target at {:?}", target_pos);
|
||||||
|
// Don't set target if unreachable
|
||||||
|
interaction_target.target = None;
|
||||||
|
interaction_target.action = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform_interaction(
|
||||||
|
mut pom_query: Query<(&GridPosition, &mut InteractionTarget, &PathQueue)>,
|
||||||
|
grid: Res<Grid>,
|
||||||
|
mut tile_query: Query<&mut TileState>,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
|
mut commands: Commands,
|
||||||
|
config: Res<GameConfig>,
|
||||||
|
) {
|
||||||
|
for (pos, mut target_component, path_queue) in pom_query.iter_mut() {
|
||||||
|
if let Some(target) = target_component.target {
|
||||||
|
// Wait until movement stops
|
||||||
|
if !path_queue.steps.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if manhattan_distance(pos.x, pos.y, target.0, target.1) == 1 {
|
||||||
|
println!(
|
||||||
|
"Performing interaction on tile ({}, {})",
|
||||||
|
target.0, target.1
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(action) = &target_component.action {
|
||||||
|
action.execute(
|
||||||
|
target,
|
||||||
|
&grid,
|
||||||
|
&mut tile_query,
|
||||||
|
&mut inventory,
|
||||||
|
&mut item_stack_query,
|
||||||
|
&mut commands,
|
||||||
|
&config,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target_component.target = None;
|
||||||
|
target_component.action = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn move_pom(
|
fn move_pom(
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
mut query: Query<(
|
mut query: Query<(
|
||||||
|
|||||||
149
src/features/pom/ui/context_menu.rs
Normal file
149
src/features/pom/ui/context_menu.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
use crate::features::pom::actions::InteractionAction;
|
||||||
|
use crate::features::ui::utils::ui_blocks;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use bevy::window::PrimaryWindow;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub enum RootMarker {
|
||||||
|
ContextMenu,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub enum ButtonType {
|
||||||
|
Interact {
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
action: InteractionAction,
|
||||||
|
},
|
||||||
|
Cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_context_menu(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut tile_click_messages: MessageReader<TileClickMessage>,
|
||||||
|
root_query: Query<Entity, With<RootMarker>>,
|
||||||
|
camera_query: Single<(&Camera, &GlobalTransform), With<Camera2d>>,
|
||||||
|
config: Res<GameConfig>,
|
||||||
|
grid: Res<Grid>,
|
||||||
|
tile_query: Query<&TileState>,
|
||||||
|
inventory: Res<Inventory>,
|
||||||
|
item_query: Query<&ItemStack>,
|
||||||
|
game_config: Res<GameConfig>,
|
||||||
|
) {
|
||||||
|
for message in tile_click_messages.read() {
|
||||||
|
// Despawn existing menu
|
||||||
|
for entity in root_query.iter() {
|
||||||
|
commands.entity(entity).try_despawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
let world_pos = grid_to_world_coords(
|
||||||
|
message.x,
|
||||||
|
message.y,
|
||||||
|
Some(0.0),
|
||||||
|
config.grid_width,
|
||||||
|
config.grid_height,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (camera, camera_transform) = *camera_query;
|
||||||
|
|
||||||
|
if let Ok(screen_pos) = camera.world_to_viewport(camera_transform, world_pos) {
|
||||||
|
let Ok(tile_entity) = grid.get_tile((message.x, message.y)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(tile_state) = tile_query.get(tile_entity) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let options =
|
||||||
|
InteractionAction::list_options(tile_state, &inventory, item_query, &game_config);
|
||||||
|
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
left: px(screen_pos.x),
|
||||||
|
top: px(screen_pos.y),
|
||||||
|
padding: UiRect::all(px(5.0)),
|
||||||
|
..Node::vstack(px(5.0))
|
||||||
|
},
|
||||||
|
ZIndex(100),
|
||||||
|
BackgroundColor(Color::srgb(0.1, 0.1, 0.1)),
|
||||||
|
BorderRadius::all(px(5)),
|
||||||
|
RootMarker::ContextMenu,
|
||||||
|
GlobalTransform::default(),
|
||||||
|
))
|
||||||
|
.with_children(|parent| {
|
||||||
|
for option in options {
|
||||||
|
parent.spawn(button(
|
||||||
|
ButtonType::Interact {
|
||||||
|
x: message.x,
|
||||||
|
y: message.y,
|
||||||
|
action: option.clone(),
|
||||||
|
},
|
||||||
|
ButtonVariant::Primary,
|
||||||
|
Node {
|
||||||
|
padding: UiRect::all(px(5)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
|c| text(option.clone().get_name(&game_config), 20.0, c), // TODO: add sprite
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.spawn(button(
|
||||||
|
ButtonType::Cancel,
|
||||||
|
ButtonVariant::Destructive,
|
||||||
|
Node {
|
||||||
|
padding: UiRect::all(px(5)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
|c| text("Abbrechen", 20.0, c),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn click_outside_context_menu(
|
||||||
|
mut commands: Commands,
|
||||||
|
mouse_btn: Res<ButtonInput<MouseButton>>,
|
||||||
|
window: Single<&Window, With<PrimaryWindow>>,
|
||||||
|
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
|
||||||
|
root_query: Query<Entity, With<RootMarker>>,
|
||||||
|
) {
|
||||||
|
if mouse_btn.just_pressed(MouseButton::Left) {
|
||||||
|
let Some(cursor_pos) = window.cursor_position() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !ui_blocks(window, cursor_pos, ui_query) {
|
||||||
|
for entity in root_query.iter() {
|
||||||
|
commands.entity(entity).try_despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buttons(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut button_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||||
|
mut interact_messages: MessageWriter<InteractStartMessage>,
|
||||||
|
root_query: Query<Entity, With<RootMarker>>,
|
||||||
|
) {
|
||||||
|
for (interaction, button_type) in button_query.iter_mut() {
|
||||||
|
if *interaction == Interaction::Pressed {
|
||||||
|
match button_type {
|
||||||
|
ButtonType::Interact { x, y, action } => {
|
||||||
|
interact_messages.write(InteractStartMessage {
|
||||||
|
x: *x,
|
||||||
|
y: *y,
|
||||||
|
action: action.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ButtonType::Cancel => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
for entity in root_query.iter() {
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/features/pom/ui/mod.rs
Normal file
22
src/features/pom/ui/mod.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
pub mod context_menu;
|
||||||
|
|
||||||
|
pub struct PomUiPlugin;
|
||||||
|
|
||||||
|
impl Plugin for PomUiPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
context_menu::spawn_context_menu.run_if(in_state(AppState::GameScreen)),
|
||||||
|
);
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
context_menu::click_outside_context_menu.run_if(in_state(AppState::GameScreen)),
|
||||||
|
);
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
context_menu::buttons.run_if(in_state(AppState::GameScreen)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ fn dump_savegame(
|
|||||||
for y in 0..grid.height {
|
for y in 0..grid.height {
|
||||||
if let Ok(entity) = grid.get_tile((x, y)) {
|
if let Ok(entity) = grid.get_tile((x, y)) {
|
||||||
if let Ok(state) = tile_query.get(entity) {
|
if let Ok(state) = tile_query.get(entity) {
|
||||||
col.push(*state);
|
col.push(state.clone());
|
||||||
} else {
|
} else {
|
||||||
col.push(TileState::Unclaimed);
|
col.push(TileState::Unclaimed);
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ fn load_savegame(
|
|||||||
if x < grid.width && y < grid.height {
|
if x < grid.width && y < grid.height {
|
||||||
if let Ok(entity) = grid.get_tile((x, y)) {
|
if let Ok(entity) = grid.get_tile((x, y)) {
|
||||||
if let Ok(mut state) = tile_query.get_mut(entity) {
|
if let Ok(mut state) = tile_query.get_mut(entity) {
|
||||||
*state = save_data.tiles[x as usize][y as usize];
|
*state = save_data.tiles[x as usize][y as usize].clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub fn spawn_load_popup(commands: &mut Commands) {
|
|||||||
},
|
},
|
||||||
ZIndex(1),
|
ZIndex(1),
|
||||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
||||||
|
GlobalTransform::default(),
|
||||||
))
|
))
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
parent
|
parent
|
||||||
@@ -46,8 +47,7 @@ pub fn spawn_load_popup(commands: &mut Commands) {
|
|||||||
height: px(40),
|
height: px(40),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"X",
|
|color| text("X", 24.0, color)
|
||||||
24.0
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
@@ -63,24 +63,22 @@ pub fn spawn_load_popup(commands: &mut Commands) {
|
|||||||
})
|
})
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
for savegame in SavegamePath::list() {
|
for savegame in SavegamePath::list() {
|
||||||
parent.spawn((
|
parent.spawn(
|
||||||
Button,
|
button(
|
||||||
ButtonType::SavegameLoad {
|
ButtonType::SavegameLoad { savegame_path: savegame.path.clone() },
|
||||||
savegame_path: savegame.path.clone(),
|
|
||||||
},
|
|
||||||
ButtonVariant::Secondary,
|
ButtonVariant::Secondary,
|
||||||
Node {
|
Node {
|
||||||
width: percent(100),
|
width: percent(100),
|
||||||
height: px(80),
|
padding: UiRect::all(px(10)),
|
||||||
flex_direction: FlexDirection::Row,
|
|
||||||
column_gap: px(10.0),
|
|
||||||
padding: UiRect::horizontal(px(10.0)),
|
|
||||||
..Node::center()
|
..Node::center()
|
||||||
},
|
},
|
||||||
BackgroundColor(ButtonVariant::Secondary.normal_background()),
|
|color| (
|
||||||
BorderRadius::all(px(10)),
|
Node {
|
||||||
children![
|
width: percent(100),
|
||||||
(
|
align_items: AlignItems::Center,
|
||||||
|
..Node::hstack(px(10))
|
||||||
|
},
|
||||||
|
children![(
|
||||||
Node {
|
Node {
|
||||||
width: percent(100),
|
width: percent(100),
|
||||||
height: percent(100),
|
height: percent(100),
|
||||||
@@ -92,7 +90,7 @@ pub fn spawn_load_popup(commands: &mut Commands) {
|
|||||||
text(
|
text(
|
||||||
format!("Spielstand {}", savegame.index + 1),
|
format!("Spielstand {}", savegame.index + 1),
|
||||||
24.0,
|
24.0,
|
||||||
Color::WHITE
|
color
|
||||||
),
|
),
|
||||||
text(
|
text(
|
||||||
format!(
|
format!(
|
||||||
@@ -115,11 +113,11 @@ pub fn spawn_load_popup(commands: &mut Commands) {
|
|||||||
height: px(40),
|
height: px(40),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"X",
|
|color| text("X", 24.0, color)
|
||||||
24.0
|
)]
|
||||||
|
)
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
74
src/features/shop/components.rs
Normal file
74
src/features/shop/components.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub enum RootMarker {
|
||||||
|
Shop,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub enum ButtonType {
|
||||||
|
ShopOpen,
|
||||||
|
ShopClose,
|
||||||
|
ShopBuyItem(ShopOffer),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ShopOffer {
|
||||||
|
pub item: ItemStack,
|
||||||
|
pub cost: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShopOffer {
|
||||||
|
pub fn list_all(game_config: &GameConfig, tile_count: u32) -> Vec<ShopOffer> {
|
||||||
|
let mut offers = Vec::new();
|
||||||
|
|
||||||
|
for seed in &game_config.berry_seeds {
|
||||||
|
offers.push(ShopOffer {
|
||||||
|
item: ItemStack {
|
||||||
|
item_type: ItemType::BerrySeed {
|
||||||
|
name: seed.name.clone(),
|
||||||
|
},
|
||||||
|
amount: 1,
|
||||||
|
},
|
||||||
|
cost: seed.cost,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut shovel_cost = game_config.shovel_base_price as f32;
|
||||||
|
for _ in 0..=tile_count {
|
||||||
|
shovel_cost = shovel_cost + (game_config.shovel_rate * shovel_cost);
|
||||||
|
shovel_cost = shovel_cost.ceil();
|
||||||
|
}
|
||||||
|
|
||||||
|
offers.push(ShopOffer {
|
||||||
|
item: ItemStack {
|
||||||
|
item_type: ItemType::Shovel,
|
||||||
|
amount: 1,
|
||||||
|
},
|
||||||
|
cost: shovel_cost as u32,
|
||||||
|
});
|
||||||
|
|
||||||
|
offers
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buy(
|
||||||
|
&self,
|
||||||
|
inventory: &mut Inventory,
|
||||||
|
commands: &mut Commands,
|
||||||
|
items: &mut Query<&mut ItemStack>,
|
||||||
|
) -> bool {
|
||||||
|
// Try to remove cost (berries)
|
||||||
|
if inventory.update_item_stack(commands, items, ItemType::Berry, -(self.cost as i32)) {
|
||||||
|
inventory.update_item_stack(
|
||||||
|
commands,
|
||||||
|
items,
|
||||||
|
self.item.item_type.clone(),
|
||||||
|
self.item.amount as i32,
|
||||||
|
);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false // Not enough berries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
54
src/features/shop/mod.rs
Normal file
54
src/features/shop/mod.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
use components::*;
|
||||||
|
use ui::open_shop;
|
||||||
|
|
||||||
|
pub mod components;
|
||||||
|
pub mod ui;
|
||||||
|
|
||||||
|
pub struct ShopPlugin;
|
||||||
|
|
||||||
|
impl Plugin for ShopPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Update, buttons.run_if(in_state(AppState::GameScreen)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buttons(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
|
||||||
|
root_query: Query<(Entity, &RootMarker)>,
|
||||||
|
game_config: Res<GameConfig>,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
mut items: Query<&mut ItemStack>,
|
||||||
|
) {
|
||||||
|
for (interaction, button_type) in &mut interaction_query {
|
||||||
|
match *interaction {
|
||||||
|
Interaction::Pressed => match button_type {
|
||||||
|
ButtonType::ShopOpen => {
|
||||||
|
open_shop(&mut commands, &game_config, &asset_server);
|
||||||
|
}
|
||||||
|
ButtonType::ShopClose => {
|
||||||
|
for (entity, root) in root_query.iter() {
|
||||||
|
match *root {
|
||||||
|
RootMarker::Shop => commands.entity(entity).despawn(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ButtonType::ShopBuyItem(offer) => {
|
||||||
|
if offer.buy(&mut inventory, &mut commands, &mut items) {
|
||||||
|
// Item bought, exit the menu
|
||||||
|
for (entity, root) in root_query.iter() {
|
||||||
|
match *root {
|
||||||
|
RootMarker::Shop => commands.entity(entity).despawn(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Error (e.g. not enough berries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/features/shop/ui/mod.rs
Normal file
5
src/features/shop/ui/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod offer;
|
||||||
|
pub mod shop;
|
||||||
|
|
||||||
|
pub use offer::shop_offer;
|
||||||
|
pub use shop::open_shop;
|
||||||
47
src/features/shop/ui/offer.rs
Normal file
47
src/features/shop/ui/offer.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use super::super::components::*;
|
||||||
|
use crate::{features::inventory::ui::item::list_itemstack, prelude::*};
|
||||||
|
|
||||||
|
pub fn shop_offer(
|
||||||
|
offer: &ShopOffer,
|
||||||
|
game_config: &GameConfig,
|
||||||
|
asset_server: &Res<AssetServer>,
|
||||||
|
) -> impl Bundle {
|
||||||
|
button(
|
||||||
|
ButtonType::ShopBuyItem(offer.clone()),
|
||||||
|
ButtonVariant::Secondary,
|
||||||
|
Node::default(),
|
||||||
|
|_| {
|
||||||
|
(
|
||||||
|
Node {
|
||||||
|
width: percent(100),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..Node::hstack(px(10))
|
||||||
|
},
|
||||||
|
children![
|
||||||
|
list_itemstack(&offer.item, game_config, asset_server),
|
||||||
|
shop_price(offer.cost, asset_server, game_config)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shop_price(
|
||||||
|
price: u32,
|
||||||
|
asset_server: &Res<AssetServer>,
|
||||||
|
game_config: &GameConfig,
|
||||||
|
) -> impl Bundle {
|
||||||
|
(
|
||||||
|
Node {
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..Node::hstack(px(0))
|
||||||
|
},
|
||||||
|
children![
|
||||||
|
text(price.to_string(), 12.0, Color::WHITE),
|
||||||
|
(
|
||||||
|
ImageNode::default(),
|
||||||
|
ItemType::Berry.get_sprite(asset_server, game_config)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
64
src/features/shop/ui/shop.rs
Normal file
64
src/features/shop/ui/shop.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use super::super::components::*;
|
||||||
|
use crate::{features::shop::ui::shop_offer, prelude::*};
|
||||||
|
|
||||||
|
pub fn open_shop(
|
||||||
|
commands: &mut Commands,
|
||||||
|
game_config: &GameConfig,
|
||||||
|
asset_server: &Res<AssetServer>,
|
||||||
|
) {
|
||||||
|
// TODO: calculate tile_count
|
||||||
|
let offers = ShopOffer::list_all(game_config, 0);
|
||||||
|
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
RootMarker::Shop,
|
||||||
|
Node {
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
width: percent(100),
|
||||||
|
height: percent(100),
|
||||||
|
..Node::center()
|
||||||
|
},
|
||||||
|
ZIndex(1),
|
||||||
|
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.8)),
|
||||||
|
GlobalTransform::default(),
|
||||||
|
))
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent
|
||||||
|
.spawn((
|
||||||
|
Node {
|
||||||
|
width: px(700),
|
||||||
|
padding: UiRect::all(px(20.0)),
|
||||||
|
..Node::vstack(px(20))
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::srgb(0.2, 0.2, 0.2)),
|
||||||
|
BorderRadius::all(px(10.0)),
|
||||||
|
))
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn((
|
||||||
|
Node {
|
||||||
|
justify_content: JustifyContent::SpaceBetween,
|
||||||
|
..Node::hstack(px(20))
|
||||||
|
},
|
||||||
|
children![
|
||||||
|
text("Shop", 40.0, Color::WHITE),
|
||||||
|
pill_button(
|
||||||
|
ButtonType::ShopClose,
|
||||||
|
ButtonVariant::Destructive,
|
||||||
|
Node {
|
||||||
|
width: px(40),
|
||||||
|
height: px(40),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
|color| text("X", 24.0, color)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
parent.spawn(Node::vstack(px(10))).with_children(|parent| {
|
||||||
|
for offer in offers {
|
||||||
|
parent.spawn(shop_offer(&offer, game_config, asset_server));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -34,8 +34,7 @@ fn setup(mut commands: Commands) {
|
|||||||
padding: UiRect::all(px(10)),
|
padding: UiRect::all(px(10)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"Spiel laden",
|
|color| text("Spiel laden", 33.0, color)
|
||||||
33.0
|
|
||||||
),
|
),
|
||||||
button(
|
button(
|
||||||
ButtonType::NewGame,
|
ButtonType::NewGame,
|
||||||
@@ -45,8 +44,7 @@ fn setup(mut commands: Commands) {
|
|||||||
padding: UiRect::all(px(10)),
|
padding: UiRect::all(px(10)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"Neues Spiel",
|
|color| text("Neues Spiel", 33.0, color)
|
||||||
33.0,
|
|
||||||
),
|
),
|
||||||
button(
|
button(
|
||||||
ButtonType::Settings,
|
ButtonType::Settings,
|
||||||
@@ -56,8 +54,7 @@ fn setup(mut commands: Commands) {
|
|||||||
padding: UiRect::all(px(10)),
|
padding: UiRect::all(px(10)),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
"Einstellungen",
|
|color| text("Einstellungen", 33.0, color)
|
||||||
33.0
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use bevy::{input::mouse::*, picking::hover::HoverMap};
|
|||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
pub struct UiPlugin;
|
pub struct UiPlugin;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
pub fn button(
|
pub fn button<C, R>(
|
||||||
button_type: impl Component,
|
button_type: impl Component,
|
||||||
variant: ButtonVariant,
|
variant: ButtonVariant,
|
||||||
mut node: Node,
|
mut node: Node,
|
||||||
title: impl Into<String>,
|
child: C,
|
||||||
font_size: f32,
|
) -> impl Bundle
|
||||||
) -> impl Bundle {
|
where
|
||||||
|
C: FnOnce(Color) -> R,
|
||||||
|
R: Bundle,
|
||||||
|
{
|
||||||
node.justify_content = JustifyContent::Center;
|
node.justify_content = JustifyContent::Center;
|
||||||
node.align_items = AlignItems::Center;
|
node.align_items = AlignItems::Center;
|
||||||
|
|
||||||
@@ -17,17 +20,20 @@ pub fn button(
|
|||||||
node,
|
node,
|
||||||
BackgroundColor(variant.normal_background()),
|
BackgroundColor(variant.normal_background()),
|
||||||
BorderRadius::all(px(10)),
|
BorderRadius::all(px(10)),
|
||||||
children![text(title, font_size, variant.text_color())],
|
children![child(variant.text_color())],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pill_button(
|
pub fn pill_button<C, R>(
|
||||||
button_type: impl Component,
|
button_type: impl Component,
|
||||||
variant: ButtonVariant,
|
variant: ButtonVariant,
|
||||||
mut node: Node,
|
mut node: Node,
|
||||||
title: impl Into<String>,
|
child: C,
|
||||||
font_size: f32,
|
) -> impl Bundle
|
||||||
) -> impl Bundle {
|
where
|
||||||
|
C: FnOnce(Color) -> R,
|
||||||
|
R: Bundle,
|
||||||
|
{
|
||||||
node.justify_content = JustifyContent::Center;
|
node.justify_content = JustifyContent::Center;
|
||||||
node.align_items = AlignItems::Center;
|
node.align_items = AlignItems::Center;
|
||||||
|
|
||||||
@@ -38,7 +44,7 @@ pub fn pill_button(
|
|||||||
node,
|
node,
|
||||||
BackgroundColor(variant.normal_background()),
|
BackgroundColor(variant.normal_background()),
|
||||||
BorderRadius::MAX,
|
BorderRadius::MAX,
|
||||||
children![text(title, font_size, variant.text_color())],
|
children![child(variant.text_color())],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
24
src/features/ui/utils.rs
Normal file
24
src/features/ui/utils.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
use bevy::window::PrimaryWindow;
|
||||||
|
|
||||||
|
pub fn ui_blocks(
|
||||||
|
window: Single<&Window, With<PrimaryWindow>>,
|
||||||
|
cursor_pos: Vec2,
|
||||||
|
ui_query: Query<(&ComputedNode, &GlobalTransform), With<Node>>,
|
||||||
|
) -> bool {
|
||||||
|
let ui_point = Vec2::new(
|
||||||
|
cursor_pos.x - window.width() / 2.0,
|
||||||
|
(window.height() / 2.0) - cursor_pos.y,
|
||||||
|
);
|
||||||
|
|
||||||
|
ui_query.iter().any(|(node, global_transform)| {
|
||||||
|
let size = node.size();
|
||||||
|
let center = global_transform.translation().truncate();
|
||||||
|
let half_size = size / 2.0;
|
||||||
|
|
||||||
|
let min = center - half_size;
|
||||||
|
let max = center + half_size;
|
||||||
|
|
||||||
|
ui_point.x >= min.x && ui_point.x <= max.x && ui_point.y >= min.y && ui_point.y <= max.y
|
||||||
|
})
|
||||||
|
}
|
||||||
10
src/main.rs
10
src/main.rs
@@ -33,7 +33,17 @@ fn main() {
|
|||||||
features::SavegamePlugin,
|
features::SavegamePlugin,
|
||||||
features::UiPlugin,
|
features::UiPlugin,
|
||||||
features::InventoryPlugin,
|
features::InventoryPlugin,
|
||||||
|
features::ShopPlugin,
|
||||||
))
|
))
|
||||||
.insert_resource(config)
|
.insert_resource(config)
|
||||||
|
.add_systems(Startup, overwrite_default_font)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn overwrite_default_font(mut fonts: ResMut<Assets<Font>>) {
|
||||||
|
let custom_font_bytes = include_bytes!("../assets/fonts/Jersey10-Regular.ttf");
|
||||||
|
let custom_font =
|
||||||
|
Font::try_from_bytes(custom_font_bytes.to_vec()).expect("Failed to parse custom font");
|
||||||
|
let default_font_id = Handle::<Font>::default().id();
|
||||||
|
let _ = fonts.insert(default_font_id, custom_font);
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ pub use crate::features::{
|
|||||||
phase::components::{CurrentPhase, Phase},
|
phase::components::{CurrentPhase, Phase},
|
||||||
pom::{
|
pom::{
|
||||||
components::{GridPosition, MovingState, Pom},
|
components::{GridPosition, MovingState, Pom},
|
||||||
messages::{InteractStartMessage, MoveMessage},
|
messages::{InteractStartMessage, MoveMessage, TileClickMessage},
|
||||||
},
|
},
|
||||||
savegame::components::SavegamePath,
|
savegame::components::SavegamePath,
|
||||||
ui::{components::ButtonVariant, consts::*, ui::*},
|
ui::{components::ButtonVariant, consts::*, ui::*, utils::*},
|
||||||
};
|
};
|
||||||
pub use crate::utils::path::get_internal_path;
|
pub use crate::utils::path::get_internal_path;
|
||||||
pub use bevy::prelude::*;
|
pub use bevy::prelude::*;
|
||||||
|
|||||||
69
tests/common/mod.rs
Normal file
69
tests/common/mod.rs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use pomomon_garden::features::config::components::{BerrySeedConfig, GameConfig};
|
||||||
|
use pomomon_garden::features::grid::components::{Grid, Tile, TileState};
|
||||||
|
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
||||||
|
|
||||||
|
pub fn setup_app(
|
||||||
|
grid_width: u32,
|
||||||
|
grid_height: u32,
|
||||||
|
initial_tile_states: &[(u32, u32, TileState)],
|
||||||
|
initial_inventory: Vec<(ItemType, u32)>,
|
||||||
|
seed_configs: Option<Vec<BerrySeedConfig>>,
|
||||||
|
) -> App {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins);
|
||||||
|
app.add_plugins(AssetPlugin::default());
|
||||||
|
|
||||||
|
// Grid Setup
|
||||||
|
let mut grid_tiles = Vec::with_capacity(grid_width as usize);
|
||||||
|
for x in 0..grid_width {
|
||||||
|
let mut column = Vec::with_capacity(grid_height as usize);
|
||||||
|
for y in 0..grid_height {
|
||||||
|
let entity = app
|
||||||
|
.world_mut()
|
||||||
|
.spawn((Tile { x, y }, TileState::Unclaimed))
|
||||||
|
.id();
|
||||||
|
column.push(entity);
|
||||||
|
}
|
||||||
|
grid_tiles.push(column);
|
||||||
|
}
|
||||||
|
app.insert_resource(Grid {
|
||||||
|
width: grid_width,
|
||||||
|
height: grid_height,
|
||||||
|
tiles: grid_tiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
for &(x, y, ref state) in initial_tile_states {
|
||||||
|
if let Ok(entity) = app.world().resource::<Grid>().get_tile((x, y)) {
|
||||||
|
*app.world_mut().get_mut::<TileState>(entity).unwrap() = state.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inventory Setup
|
||||||
|
let mut inventory_items = Vec::new();
|
||||||
|
for (item_type, amount) in initial_inventory {
|
||||||
|
let id = app.world_mut().spawn(ItemStack { item_type, amount }).id();
|
||||||
|
inventory_items.push(id);
|
||||||
|
}
|
||||||
|
app.insert_resource(Inventory {
|
||||||
|
items: inventory_items,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Game Config
|
||||||
|
let seeds = seed_configs.unwrap_or_else(|| {
|
||||||
|
vec![BerrySeedConfig {
|
||||||
|
name: "TestSeed".to_string(),
|
||||||
|
cost: 1,
|
||||||
|
grants: 1,
|
||||||
|
slice: "seed.aseprite".to_string(),
|
||||||
|
growth_stages: 2,
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
app.insert_resource(GameConfig {
|
||||||
|
berry_seeds: seeds,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
app
|
||||||
|
}
|
||||||
57
tests/config.rs
Normal file
57
tests/config.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use pomomon_garden::features::config::components::GameConfig;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Write;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// Helper function to create a temporary file with content
|
||||||
|
fn create_temp_file(content: &str) -> (std::path::PathBuf, String) {
|
||||||
|
let filename = format!("test_config_{}.json", Uuid::new_v4());
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let filepath = temp_dir.join(&filename);
|
||||||
|
|
||||||
|
let mut file = fs::File::create(&filepath).expect("Could not create temp file");
|
||||||
|
file.write_all(content.as_bytes())
|
||||||
|
.expect("Could not write to temp file");
|
||||||
|
|
||||||
|
(filepath, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_valid_config() {
|
||||||
|
let (filepath, _filename) = create_temp_file(
|
||||||
|
r#"{
|
||||||
|
"grid_width": 10,
|
||||||
|
"grid_height": 5,
|
||||||
|
"pom_speed": 2.0,
|
||||||
|
"shovel_base_price": 10,
|
||||||
|
"shovel_rate": 0.2,
|
||||||
|
"berry_seeds": []
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let config = GameConfig::read_from_path(&filepath).expect("Failed to read valid config");
|
||||||
|
assert_eq!(config.grid_width, 10);
|
||||||
|
assert_eq!(config.grid_height, 5);
|
||||||
|
assert_eq!(config.pom_speed, 2.0);
|
||||||
|
|
||||||
|
fs::remove_file(filepath).expect("Failed to delete temp file");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_invalid_config() {
|
||||||
|
let (filepath, _filename) = create_temp_file(r#"this is not valid json"#);
|
||||||
|
|
||||||
|
let config = GameConfig::read_from_path(&filepath);
|
||||||
|
assert!(config.is_none(), "Expected invalid config to return None");
|
||||||
|
|
||||||
|
fs::remove_file(filepath).expect("Failed to delete temp file");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_missing_config() {
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let filepath = temp_dir.join("non_existent_config.json");
|
||||||
|
|
||||||
|
let config = GameConfig::read_from_path(&filepath);
|
||||||
|
assert!(config.is_none(), "Expected missing config to return None");
|
||||||
|
}
|
||||||
167
tests/growth.rs
Normal file
167
tests/growth.rs
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
use bevy::ecs::system::RunSystemOnce;
|
||||||
|
use pomomon_garden::features::config::components::{BerrySeedConfig, GameConfig};
|
||||||
|
use pomomon_garden::features::grid::components::{Grid, Tile, TileState};
|
||||||
|
use pomomon_garden::features::inventory::components::ItemType;
|
||||||
|
use pomomon_garden::features::phase::components::{
|
||||||
|
CurrentPhase, Phase, SessionTracker, TimerSettings,
|
||||||
|
};
|
||||||
|
use pomomon_garden::features::phase::handle_continue;
|
||||||
|
use pomomon_garden::features::phase::messages::NextPhaseMessage;
|
||||||
|
use pomomon_garden::prelude::*;
|
||||||
|
|
||||||
|
fn setup_growth_app(
|
||||||
|
grid_width: u32,
|
||||||
|
grid_height: u32,
|
||||||
|
initial_tile_states: &[(u32, u32, TileState)],
|
||||||
|
) -> App {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins);
|
||||||
|
app.add_plugins(AssetPlugin::default());
|
||||||
|
|
||||||
|
app.init_resource::<TimerSettings>();
|
||||||
|
app.init_resource::<SessionTracker>();
|
||||||
|
// Initialize phase as Finished(Focus) to trigger growth on continue
|
||||||
|
app.insert_resource(CurrentPhase(Phase::Finished {
|
||||||
|
completed_phase: Box::new(Phase::Focus { duration: 25.0 }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// GameConfig with known seeds
|
||||||
|
app.insert_resource(GameConfig {
|
||||||
|
berry_seeds: vec![BerrySeedConfig {
|
||||||
|
name: "FastSeed".into(),
|
||||||
|
cost: 1,
|
||||||
|
grants: 1,
|
||||||
|
slice: "".into(),
|
||||||
|
growth_stages: 2,
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Grid Setup
|
||||||
|
let mut grid_tiles = Vec::with_capacity(grid_width as usize);
|
||||||
|
for x in 0..grid_width {
|
||||||
|
let mut column = Vec::with_capacity(grid_height as usize);
|
||||||
|
for y in 0..grid_height {
|
||||||
|
let entity = app
|
||||||
|
.world_mut()
|
||||||
|
.spawn((Tile { x, y }, TileState::Unclaimed))
|
||||||
|
.id();
|
||||||
|
column.push(entity);
|
||||||
|
}
|
||||||
|
grid_tiles.push(column);
|
||||||
|
}
|
||||||
|
app.insert_resource(Grid {
|
||||||
|
width: grid_width,
|
||||||
|
height: grid_height,
|
||||||
|
tiles: grid_tiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
for &(x, y, ref state) in initial_tile_states {
|
||||||
|
if let Ok(entity) = app.world().resource::<Grid>().get_tile((x, y)) {
|
||||||
|
*app.world_mut().get_mut::<TileState>(entity).unwrap() = state.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.add_message::<NextPhaseMessage>();
|
||||||
|
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_crop_growth_logic() {
|
||||||
|
let seed_type = ItemType::BerrySeed {
|
||||||
|
name: "FastSeed".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Scenario:
|
||||||
|
// (0,0): Watered, Stage 0 -> Should grow to 1, become unwatered
|
||||||
|
// (1,0): Unwatered, Stage 0 -> Should NOT grow, stay unwatered
|
||||||
|
// (2,0): Watered, Stage 2 (Max) -> Should NOT grow, become unwatered
|
||||||
|
|
||||||
|
let initial_states = vec![
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
TileState::Occupied {
|
||||||
|
seed: seed_type.clone(),
|
||||||
|
watered: true,
|
||||||
|
growth_stage: 0,
|
||||||
|
withered: false,
|
||||||
|
dry_counter: 0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
TileState::Occupied {
|
||||||
|
seed: seed_type.clone(),
|
||||||
|
watered: false,
|
||||||
|
growth_stage: 0,
|
||||||
|
withered: false,
|
||||||
|
dry_counter: 0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
TileState::Occupied {
|
||||||
|
seed: seed_type.clone(),
|
||||||
|
watered: true,
|
||||||
|
growth_stage: 2, // Max
|
||||||
|
withered: false,
|
||||||
|
dry_counter: 0,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut app = setup_growth_app(3, 1, &initial_states);
|
||||||
|
|
||||||
|
app.world_mut().write_message(NextPhaseMessage);
|
||||||
|
let _ = app.world_mut().run_system_once(handle_continue);
|
||||||
|
let grid = app.world().resource::<Grid>();
|
||||||
|
|
||||||
|
// Check (0,0)
|
||||||
|
let t1 = grid.get_tile((0, 0)).unwrap();
|
||||||
|
let s1 = app.world().entity(t1).get::<TileState>().unwrap();
|
||||||
|
match s1 {
|
||||||
|
TileState::Occupied {
|
||||||
|
watered,
|
||||||
|
growth_stage,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(*growth_stage, 1, "(0,0) should grow to stage 1");
|
||||||
|
assert_eq!(*watered, false, "(0,0) should be unwatered");
|
||||||
|
}
|
||||||
|
_ => panic!("Invalid state at (0,0)"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check (1,0)
|
||||||
|
let t2 = grid.get_tile((1, 0)).unwrap();
|
||||||
|
let s2 = app.world().entity(t2).get::<TileState>().unwrap();
|
||||||
|
match s2 {
|
||||||
|
TileState::Occupied {
|
||||||
|
watered,
|
||||||
|
growth_stage,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(*growth_stage, 0, "(1,0) should stay at stage 0");
|
||||||
|
assert_eq!(*watered, false, "(1,0) should be unwatered");
|
||||||
|
}
|
||||||
|
_ => panic!("Invalid state at (1,0)"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check (2,0)
|
||||||
|
let t3 = grid.get_tile((2, 0)).unwrap();
|
||||||
|
let s3 = app.world().entity(t3).get::<TileState>().unwrap();
|
||||||
|
match s3 {
|
||||||
|
TileState::Occupied {
|
||||||
|
watered,
|
||||||
|
growth_stage,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(*growth_stage, 2, "(2,0) should stay at stage 2 (max)");
|
||||||
|
assert_eq!(*watered, false, "(2,0) should be unwatered");
|
||||||
|
}
|
||||||
|
_ => panic!("Invalid state at (2,0)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
197
tests/harvest.rs
Normal file
197
tests/harvest.rs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
use bevy::ecs::system::RunSystemOnce;
|
||||||
|
use pomomon_garden::features::config::components::{BerrySeedConfig, GameConfig};
|
||||||
|
use pomomon_garden::features::grid::components::{Grid, TileState};
|
||||||
|
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
||||||
|
use pomomon_garden::features::pom::actions::InteractionAction;
|
||||||
|
use pomomon_garden::prelude::*;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::setup_app;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_harvest_fully_grown() {
|
||||||
|
let seed_type = ItemType::BerrySeed {
|
||||||
|
name: "TestSeed".into(),
|
||||||
|
};
|
||||||
|
let initial_states = vec![(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
TileState::Occupied {
|
||||||
|
seed: seed_type.clone(),
|
||||||
|
watered: false,
|
||||||
|
growth_stage: 2, // Max
|
||||||
|
withered: false,
|
||||||
|
dry_counter: 0,
|
||||||
|
},
|
||||||
|
)];
|
||||||
|
|
||||||
|
let seed_config = BerrySeedConfig {
|
||||||
|
name: "TestSeed".into(),
|
||||||
|
cost: 1,
|
||||||
|
grants: 5,
|
||||||
|
slice: "".into(),
|
||||||
|
growth_stages: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut app = setup_app(1, 1, &initial_states, vec![], Some(vec![seed_config]));
|
||||||
|
|
||||||
|
let _ = app.world_mut().run_system_once(
|
||||||
|
|mut commands: Commands,
|
||||||
|
grid: Res<Grid>,
|
||||||
|
mut tile_query: Query<&mut TileState>,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
|
config: Res<GameConfig>| {
|
||||||
|
InteractionAction::Harvest.execute(
|
||||||
|
(0, 0),
|
||||||
|
&grid,
|
||||||
|
&mut tile_query,
|
||||||
|
&mut inventory,
|
||||||
|
&mut item_stack_query,
|
||||||
|
&mut commands,
|
||||||
|
&config,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check Tile State -> Empty
|
||||||
|
// Wait for commands to apply
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
let grid = app.world().resource::<Grid>();
|
||||||
|
let tile_entity = grid.get_tile((0, 0)).unwrap();
|
||||||
|
let tile_state = app.world().entity(tile_entity).get::<TileState>().unwrap();
|
||||||
|
assert!(matches!(tile_state, TileState::Empty));
|
||||||
|
|
||||||
|
// Check Inventory -> 5 Berries
|
||||||
|
let inventory = app.world().resource::<Inventory>();
|
||||||
|
assert_eq!(inventory.items.len(), 1);
|
||||||
|
let stack_entity = inventory.items[0];
|
||||||
|
let stack = app.world().entity(stack_entity).get::<ItemStack>().unwrap();
|
||||||
|
assert_eq!(stack.item_type, ItemType::Berry);
|
||||||
|
assert_eq!(stack.amount, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_harvest_withered() {
|
||||||
|
let seed_type = ItemType::BerrySeed {
|
||||||
|
name: "TestSeed".into(),
|
||||||
|
};
|
||||||
|
let initial_states = vec![(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
TileState::Occupied {
|
||||||
|
seed: seed_type.clone(),
|
||||||
|
watered: false,
|
||||||
|
growth_stage: 1,
|
||||||
|
withered: true, // Withered
|
||||||
|
dry_counter: 0,
|
||||||
|
},
|
||||||
|
)];
|
||||||
|
|
||||||
|
let seed_config = BerrySeedConfig {
|
||||||
|
name: "TestSeed".into(),
|
||||||
|
cost: 1,
|
||||||
|
grants: 5,
|
||||||
|
slice: "".into(),
|
||||||
|
growth_stages: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut app = setup_app(1, 1, &initial_states, vec![], Some(vec![seed_config]));
|
||||||
|
|
||||||
|
let _ = app.world_mut().run_system_once(
|
||||||
|
|mut commands: Commands,
|
||||||
|
grid: Res<Grid>,
|
||||||
|
mut tile_query: Query<&mut TileState>,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
|
config: Res<GameConfig>| {
|
||||||
|
InteractionAction::Harvest.execute(
|
||||||
|
(0, 0),
|
||||||
|
&grid,
|
||||||
|
&mut tile_query,
|
||||||
|
&mut inventory,
|
||||||
|
&mut item_stack_query,
|
||||||
|
&mut commands,
|
||||||
|
&config,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
// Check Tile State -> Empty
|
||||||
|
let grid = app.world().resource::<Grid>();
|
||||||
|
let tile_entity = grid.get_tile((0, 0)).unwrap();
|
||||||
|
let tile_state = app.world().entity(tile_entity).get::<TileState>().unwrap();
|
||||||
|
assert!(matches!(tile_state, TileState::Empty));
|
||||||
|
|
||||||
|
// Check Inventory -> Empty (no berries for withered)
|
||||||
|
let inventory = app.world().resource::<Inventory>();
|
||||||
|
assert!(inventory.items.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cannot_harvest_growing() {
|
||||||
|
let seed_type = ItemType::BerrySeed {
|
||||||
|
name: "TestSeed".into(),
|
||||||
|
};
|
||||||
|
let initial_states = vec![(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
TileState::Occupied {
|
||||||
|
seed: seed_type.clone(),
|
||||||
|
watered: false,
|
||||||
|
growth_stage: 1, // Growing (Max is 2)
|
||||||
|
withered: false,
|
||||||
|
dry_counter: 0,
|
||||||
|
},
|
||||||
|
)];
|
||||||
|
|
||||||
|
let seed_config = BerrySeedConfig {
|
||||||
|
name: "TestSeed".into(),
|
||||||
|
cost: 1,
|
||||||
|
grants: 5,
|
||||||
|
slice: "".into(),
|
||||||
|
growth_stages: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut app = setup_app(1, 1, &initial_states, vec![], Some(vec![seed_config]));
|
||||||
|
|
||||||
|
let _ = app.world_mut().run_system_once(
|
||||||
|
|mut commands: Commands,
|
||||||
|
grid: Res<Grid>,
|
||||||
|
mut tile_query: Query<&mut TileState>,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
|
config: Res<GameConfig>| {
|
||||||
|
InteractionAction::Harvest.execute(
|
||||||
|
(0, 0),
|
||||||
|
&grid,
|
||||||
|
&mut tile_query,
|
||||||
|
&mut inventory,
|
||||||
|
&mut item_stack_query,
|
||||||
|
&mut commands,
|
||||||
|
&config,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
// Check Tile State -> Still Occupied
|
||||||
|
let grid = app.world().resource::<Grid>();
|
||||||
|
let tile_entity = grid.get_tile((0, 0)).unwrap();
|
||||||
|
let tile_state = app.world().entity(tile_entity).get::<TileState>().unwrap();
|
||||||
|
match tile_state {
|
||||||
|
TileState::Occupied { growth_stage, .. } => {
|
||||||
|
assert_eq!(*growth_stage, 1);
|
||||||
|
}
|
||||||
|
_ => panic!("Should still be occupied"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Inventory -> Empty
|
||||||
|
let inventory = app.world().resource::<Inventory>();
|
||||||
|
assert!(inventory.items.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
175
tests/pathfinding.rs
Normal file
175
tests/pathfinding.rs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
use bevy::ecs::system::RunSystemOnce;
|
||||||
|
use pomomon_garden::features::grid::components::{Grid, Tile, TileState};
|
||||||
|
use pomomon_garden::features::pom::utils::find_path;
|
||||||
|
use pomomon_garden::prelude::*;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
// Helper to set up a Bevy App for pathfinding tests
|
||||||
|
fn setup_pathfinding_app(
|
||||||
|
grid_width: u32,
|
||||||
|
grid_height: u32,
|
||||||
|
initial_tile_states: &[(u32, u32, TileState)], // (x, y, state)
|
||||||
|
) -> App {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins);
|
||||||
|
|
||||||
|
let mut grid_tiles = Vec::with_capacity(grid_width as usize);
|
||||||
|
for x in 0..grid_width {
|
||||||
|
let mut column = Vec::with_capacity(grid_height as usize);
|
||||||
|
for y in 0..grid_height {
|
||||||
|
let entity = app
|
||||||
|
.world_mut()
|
||||||
|
.spawn((Tile { x, y }, TileState::Unclaimed))
|
||||||
|
.id();
|
||||||
|
column.push(entity);
|
||||||
|
}
|
||||||
|
grid_tiles.push(column);
|
||||||
|
}
|
||||||
|
app.world_mut().insert_resource(Grid {
|
||||||
|
width: grid_width,
|
||||||
|
height: grid_height,
|
||||||
|
tiles: grid_tiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
for &(x, y, ref state) in initial_tile_states {
|
||||||
|
if let Ok(entity) = app.world().resource::<Grid>().get_tile((x, y)) {
|
||||||
|
*app.world_mut().get_mut::<TileState>(entity).unwrap() = state.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
// Struct to hold start and end positions as a Resource
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct PathParams {
|
||||||
|
start: UVec2,
|
||||||
|
end: UVec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test system to run find_path and store result
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct PathResult(Option<VecDeque<(u32, u32)>>);
|
||||||
|
|
||||||
|
fn pathfinding_system(
|
||||||
|
grid: Res<Grid>,
|
||||||
|
tile_query: Query<&TileState>,
|
||||||
|
mut path_result: ResMut<PathResult>,
|
||||||
|
path_params: Res<PathParams>, // Input: (start, end)
|
||||||
|
) {
|
||||||
|
path_result.0 = find_path(
|
||||||
|
path_params.start.into(),
|
||||||
|
path_params.end.into(),
|
||||||
|
&grid,
|
||||||
|
&tile_query,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_path_simple() {
|
||||||
|
let mut app = setup_pathfinding_app(5, 5, &[]); // Empty 5x5 grid
|
||||||
|
|
||||||
|
app.world_mut().insert_resource(PathResult(None));
|
||||||
|
app.world_mut().insert_resource(PathParams {
|
||||||
|
start: UVec2::new(0, 0),
|
||||||
|
end: UVec2::new(4, 4),
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = app.world_mut().run_system_once(pathfinding_system);
|
||||||
|
|
||||||
|
let path_result = app.world().resource::<PathResult>();
|
||||||
|
assert!(path_result.0.is_some());
|
||||||
|
let path = path_result.0.as_ref().unwrap();
|
||||||
|
// A* with Manhattan distance will find a shortest path.
|
||||||
|
// Length for (0,0) to (4,4) in an empty grid is 8 steps + 1 start = 9 nodes
|
||||||
|
assert_eq!(path.len(), 9, "Path length incorrect for simple path");
|
||||||
|
assert_eq!(*path.front().unwrap(), (0, 0), "Path should start at (0,0)");
|
||||||
|
assert_eq!(*path.back().unwrap(), (4, 4), "Path should end at (4,4)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_path_around_obstacle() {
|
||||||
|
let obstacle: TileState = TileState::Occupied {
|
||||||
|
seed: ItemType::BerrySeed {
|
||||||
|
name: "Test".into(),
|
||||||
|
},
|
||||||
|
watered: false,
|
||||||
|
growth_stage: 0,
|
||||||
|
withered: false,
|
||||||
|
dry_counter: 0,
|
||||||
|
};
|
||||||
|
let obstacles = vec![
|
||||||
|
(2, 2, obstacle.clone()),
|
||||||
|
(2, 3, obstacle.clone()),
|
||||||
|
(2, 4, obstacle.clone()),
|
||||||
|
];
|
||||||
|
let mut app = setup_pathfinding_app(5, 5, &obstacles);
|
||||||
|
|
||||||
|
let _ = app.world_mut().insert_resource(PathResult(None));
|
||||||
|
let _ = app.world_mut().insert_resource(PathParams {
|
||||||
|
start: UVec2::new(0, 0),
|
||||||
|
end: UVec2::new(4, 4),
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = app.world_mut().run_system_once(pathfinding_system);
|
||||||
|
|
||||||
|
let path_result = app.world().resource::<PathResult>();
|
||||||
|
|
||||||
|
if path_result.0.is_none() {
|
||||||
|
panic!("Should find a path around obstacles, but found none.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = path_result.0.as_ref().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(*path.front().unwrap(), (0, 0));
|
||||||
|
assert_eq!(*path.back().unwrap(), (4, 4));
|
||||||
|
|
||||||
|
// Assert that no obstacle tile is in the path
|
||||||
|
for (ox, oy, _) in &obstacles {
|
||||||
|
assert!(
|
||||||
|
!path.contains(&(*ox, *oy)),
|
||||||
|
"Path should not contain obstacle {:?}",
|
||||||
|
(ox, oy)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shortest path around these specific obstacles has a length of 9 nodes (8 steps)
|
||||||
|
// For example: (0,0) -> (1,0) -> (2,0) -> (3,0) -> (4,0) -> (4,1) -> (4,2) -> (4,3) -> (4,4)
|
||||||
|
assert_eq!(
|
||||||
|
path.len(),
|
||||||
|
9,
|
||||||
|
"Path length incorrect for path around obstacles."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_path_no_path() {
|
||||||
|
let obstacle: TileState = TileState::Occupied {
|
||||||
|
seed: ItemType::BerrySeed {
|
||||||
|
name: "Test".into(),
|
||||||
|
},
|
||||||
|
watered: false,
|
||||||
|
growth_stage: 0,
|
||||||
|
withered: false,
|
||||||
|
dry_counter: 0,
|
||||||
|
};
|
||||||
|
let obstacles = vec![
|
||||||
|
(2, 0, obstacle.clone()),
|
||||||
|
(2, 1, obstacle.clone()),
|
||||||
|
(2, 2, obstacle.clone()),
|
||||||
|
(2, 3, obstacle.clone()),
|
||||||
|
(2, 4, obstacle.clone()),
|
||||||
|
];
|
||||||
|
let mut app = setup_pathfinding_app(5, 5, &obstacles);
|
||||||
|
|
||||||
|
app.world_mut().insert_resource(PathResult(None));
|
||||||
|
app.world_mut().insert_resource(PathParams {
|
||||||
|
start: UVec2::new(0, 0),
|
||||||
|
end: UVec2::new(4, 4),
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = app.world_mut().run_system_once(pathfinding_system);
|
||||||
|
|
||||||
|
let path_result = app.world().resource::<PathResult>();
|
||||||
|
assert!(path_result.0.is_none(), "Expected no path when blocked");
|
||||||
|
}
|
||||||
128
tests/planting.rs
Normal file
128
tests/planting.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use bevy::ecs::system::RunSystemOnce;
|
||||||
|
use pomomon_garden::features::config::components::GameConfig;
|
||||||
|
use pomomon_garden::features::grid::components::{Grid, TileState};
|
||||||
|
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
||||||
|
use pomomon_garden::features::pom::actions::InteractionAction;
|
||||||
|
use pomomon_garden::prelude::*;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::setup_app;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plant_seed_interaction() {
|
||||||
|
let seed_type = ItemType::BerrySeed {
|
||||||
|
name: "TestSeed".into(),
|
||||||
|
};
|
||||||
|
let mut app = setup_app(
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
&[(1, 1, TileState::Empty)],
|
||||||
|
vec![(seed_type.clone(), 1)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = app.world_mut().run_system_once(
|
||||||
|
move |grid: Res<Grid>,
|
||||||
|
mut tile_query: Query<&mut TileState>,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
|
mut commands: Commands,
|
||||||
|
game_config: Res<GameConfig>| {
|
||||||
|
let action = InteractionAction::Plant(seed_type.clone());
|
||||||
|
action.execute(
|
||||||
|
(1, 1),
|
||||||
|
&grid,
|
||||||
|
&mut tile_query,
|
||||||
|
&mut inventory,
|
||||||
|
&mut item_stack_query,
|
||||||
|
&mut commands,
|
||||||
|
&game_config,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply commands (despawns etc.)
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
// Assert Tile State
|
||||||
|
let grid = app.world().resource::<Grid>();
|
||||||
|
let tile_entity = grid.get_tile((1, 1)).unwrap();
|
||||||
|
let tile_state = app.world().entity(tile_entity).get::<TileState>().unwrap();
|
||||||
|
|
||||||
|
if let TileState::Occupied { seed, .. } = tile_state {
|
||||||
|
assert_eq!(
|
||||||
|
*seed,
|
||||||
|
ItemType::BerrySeed {
|
||||||
|
name: "TestSeed".into()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Tile should be Occupied with seed, found {:?}", tile_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert Inventory Empty
|
||||||
|
let inventory = app.world().resource::<Inventory>();
|
||||||
|
// Item stack entity should be despawned or amount 0
|
||||||
|
if !inventory.items.is_empty() {
|
||||||
|
// If the entity still exists, check amount
|
||||||
|
if let Some(entity) = inventory.items.first() {
|
||||||
|
if let Some(stack) = app.world().entity(*entity).get::<ItemStack>() {
|
||||||
|
assert_eq!(stack.amount, 0, "Item amount should be 0");
|
||||||
|
} else {
|
||||||
|
panic!(
|
||||||
|
"Inventory items list should be empty or point to valid 0 amount entities. Found phantom entity."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert!(inventory.items.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plant_seed_no_inventory() {
|
||||||
|
let seed_type = ItemType::BerrySeed {
|
||||||
|
name: "TestSeed".into(),
|
||||||
|
};
|
||||||
|
let mut app = setup_app(
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
&[(1, 1, TileState::Empty)],
|
||||||
|
vec![], // Empty inventory
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = app.world_mut().run_system_once(
|
||||||
|
move |grid: Res<Grid>,
|
||||||
|
mut tile_query: Query<&mut TileState>,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
|
mut commands: Commands,
|
||||||
|
game_config: Res<GameConfig>| {
|
||||||
|
let action = InteractionAction::Plant(seed_type.clone());
|
||||||
|
action.execute(
|
||||||
|
(1, 1),
|
||||||
|
&grid,
|
||||||
|
&mut tile_query,
|
||||||
|
&mut inventory,
|
||||||
|
&mut item_stack_query,
|
||||||
|
&mut commands,
|
||||||
|
&game_config,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
// Assert Tile State Unchanged
|
||||||
|
let grid = app.world().resource::<Grid>();
|
||||||
|
let tile_entity = grid.get_tile((1, 1)).unwrap();
|
||||||
|
let tile_state = app.world().entity(tile_entity).get::<TileState>().unwrap();
|
||||||
|
|
||||||
|
if let TileState::Empty = tile_state {
|
||||||
|
// Correct
|
||||||
|
} else {
|
||||||
|
panic!("Tile should remain Empty, found {:?}", tile_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
110
tests/session.rs
Normal file
110
tests/session.rs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
use pomomon_garden::features::phase::components::{
|
||||||
|
CurrentPhase, Phase, SessionTracker, TimerSettings,
|
||||||
|
};
|
||||||
|
use pomomon_garden::features::phase::next_phase;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_session_tracker_focus_to_short_break() {
|
||||||
|
let mut current_phase = CurrentPhase(Phase::Finished {
|
||||||
|
completed_phase: Box::new(Phase::Focus {
|
||||||
|
duration: 25.0 * 60.0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
let timer_settings = TimerSettings::default();
|
||||||
|
let mut session_tracker = SessionTracker::default();
|
||||||
|
|
||||||
|
next_phase(&mut current_phase, &mut session_tracker, &timer_settings);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
session_tracker.completed_focus_phases, 1,
|
||||||
|
"Completed focus phases should be 1"
|
||||||
|
);
|
||||||
|
if let Phase::Break { duration } = current_phase.0 {
|
||||||
|
assert_eq!(
|
||||||
|
duration, timer_settings.short_break_duration as f32,
|
||||||
|
"Should transition to short break"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Expected a Break phase, got {:?}", current_phase.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_session_tracker_focus_to_long_break() {
|
||||||
|
let mut current_phase = CurrentPhase(Phase::Finished {
|
||||||
|
completed_phase: Box::new(Phase::Focus {
|
||||||
|
duration: 25.0 * 60.0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
let timer_settings = TimerSettings::default();
|
||||||
|
let mut session_tracker = SessionTracker {
|
||||||
|
completed_focus_phases: timer_settings.long_break_interval - 1,
|
||||||
|
}; // To trigger long break on next phase
|
||||||
|
|
||||||
|
next_phase(&mut current_phase, &mut session_tracker, &timer_settings);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
session_tracker.completed_focus_phases, timer_settings.long_break_interval,
|
||||||
|
"Completed focus phases should reach long break interval"
|
||||||
|
);
|
||||||
|
if let Phase::Break { duration } = current_phase.0 {
|
||||||
|
assert_eq!(
|
||||||
|
duration, timer_settings.long_break_duration as f32,
|
||||||
|
"Should transition to long break"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Expected a Break phase, got {:?}", current_phase.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_session_tracker_break_to_focus() {
|
||||||
|
let mut current_phase = CurrentPhase(Phase::Finished {
|
||||||
|
completed_phase: Box::new(Phase::Break {
|
||||||
|
duration: 5.0 * 60.0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
let mut session_tracker = SessionTracker {
|
||||||
|
completed_focus_phases: 1,
|
||||||
|
}; // Arbitrary value, should not change
|
||||||
|
let timer_settings = TimerSettings::default();
|
||||||
|
|
||||||
|
next_phase(&mut current_phase, &mut session_tracker, &timer_settings);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
session_tracker.completed_focus_phases, 1,
|
||||||
|
"Completed focus phases should not change"
|
||||||
|
);
|
||||||
|
if let Phase::Focus { duration } = current_phase.0 {
|
||||||
|
assert_eq!(
|
||||||
|
duration, timer_settings.focus_duration as f32,
|
||||||
|
"Should transition to Focus phase"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Expected a Focus phase, got {:?}", current_phase.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_session_tracker_not_finished_phase_no_change() {
|
||||||
|
// Test that nothing changes if the phase is not `Finished`
|
||||||
|
let mut current_phase = CurrentPhase(Phase::Focus { duration: 100.0 });
|
||||||
|
let mut session_tracker = SessionTracker {
|
||||||
|
completed_focus_phases: 0,
|
||||||
|
};
|
||||||
|
let timer_settings = TimerSettings::default();
|
||||||
|
|
||||||
|
let initial_phase = current_phase.0.clone();
|
||||||
|
let initial_completed_focus = session_tracker.completed_focus_phases;
|
||||||
|
|
||||||
|
next_phase(&mut current_phase, &mut session_tracker, &timer_settings);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
current_phase.0, initial_phase,
|
||||||
|
"Phase should not change if not Finished"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
session_tracker.completed_focus_phases, initial_completed_focus,
|
||||||
|
"Session tracker should not change if phase not Finished"
|
||||||
|
);
|
||||||
|
}
|
||||||
106
tests/watering.rs
Normal file
106
tests/watering.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
use bevy::ecs::system::RunSystemOnce;
|
||||||
|
use pomomon_garden::features::config::components::GameConfig;
|
||||||
|
use pomomon_garden::features::grid::components::{Grid, TileState};
|
||||||
|
use pomomon_garden::features::inventory::components::{Inventory, ItemStack, ItemType};
|
||||||
|
use pomomon_garden::features::pom::actions::InteractionAction;
|
||||||
|
use pomomon_garden::prelude::*;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
use common::setup_app;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_water_crop() {
|
||||||
|
let seed_type = ItemType::BerrySeed {
|
||||||
|
name: "TestSeed".into(),
|
||||||
|
};
|
||||||
|
let mut app = setup_app(
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
&[(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
TileState::Occupied {
|
||||||
|
seed: seed_type.clone(),
|
||||||
|
watered: false,
|
||||||
|
growth_stage: 0,
|
||||||
|
withered: false,
|
||||||
|
dry_counter: 0,
|
||||||
|
},
|
||||||
|
)],
|
||||||
|
vec![],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify Water option is available
|
||||||
|
let _ = app.world_mut().run_system_once(
|
||||||
|
move |grid: Res<Grid>,
|
||||||
|
tile_query: Query<&TileState>,
|
||||||
|
inventory: Res<Inventory>,
|
||||||
|
item_query: Query<&ItemStack>,
|
||||||
|
game_config: Res<GameConfig>| {
|
||||||
|
let tile_entity = grid.get_tile((1, 1)).unwrap();
|
||||||
|
let tile_state = tile_query.get(tile_entity).unwrap();
|
||||||
|
let options =
|
||||||
|
InteractionAction::list_options(tile_state, &inventory, item_query, &game_config);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
options.contains(&InteractionAction::Water),
|
||||||
|
"Water option should be available"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute Water
|
||||||
|
let _ = app.world_mut().run_system_once(
|
||||||
|
move |grid: Res<Grid>,
|
||||||
|
mut tile_query: Query<&mut TileState>,
|
||||||
|
mut inventory: ResMut<Inventory>,
|
||||||
|
mut item_stack_query: Query<&mut ItemStack>,
|
||||||
|
mut commands: Commands,
|
||||||
|
game_config: Res<GameConfig>| {
|
||||||
|
let action = InteractionAction::Water;
|
||||||
|
action.execute(
|
||||||
|
(1, 1),
|
||||||
|
&grid,
|
||||||
|
&mut tile_query,
|
||||||
|
&mut inventory,
|
||||||
|
&mut item_stack_query,
|
||||||
|
&mut commands,
|
||||||
|
&game_config,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.update();
|
||||||
|
|
||||||
|
// Assert Tile State Watered
|
||||||
|
let grid = app.world().resource::<Grid>();
|
||||||
|
let tile_entity = grid.get_tile((1, 1)).unwrap();
|
||||||
|
let tile_state = app.world().entity(tile_entity).get::<TileState>().unwrap();
|
||||||
|
|
||||||
|
if let TileState::Occupied { watered, .. } = tile_state {
|
||||||
|
assert!(watered, "Tile should be watered");
|
||||||
|
} else {
|
||||||
|
panic!("Tile should be Occupied, found {:?}", tile_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Water option is NOT available
|
||||||
|
let _ = app.world_mut().run_system_once(
|
||||||
|
move |grid: Res<Grid>,
|
||||||
|
tile_query: Query<&TileState>,
|
||||||
|
inventory: Res<Inventory>,
|
||||||
|
item_query: Query<&ItemStack>,
|
||||||
|
game_config: Res<GameConfig>| {
|
||||||
|
let tile_entity = grid.get_tile((1, 1)).unwrap();
|
||||||
|
let tile_state = tile_query.get(tile_entity).unwrap();
|
||||||
|
let options =
|
||||||
|
InteractionAction::list_options(tile_state, &inventory, item_query, &game_config);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!options.contains(&InteractionAction::Water),
|
||||||
|
"Water option should NOT be available"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
162
tests/withering.rs
Normal file
162
tests/withering.rs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
use bevy::ecs::system::RunSystemOnce;
|
||||||
|
use pomomon_garden::features::config::components::{BerrySeedConfig, GameConfig};
|
||||||
|
use pomomon_garden::features::grid::components::{Grid, Tile, TileState};
|
||||||
|
use pomomon_garden::features::inventory::components::ItemType;
|
||||||
|
use pomomon_garden::features::phase::components::{
|
||||||
|
CurrentPhase, Phase, SessionTracker, TimerSettings,
|
||||||
|
};
|
||||||
|
use pomomon_garden::features::phase::handle_continue;
|
||||||
|
use pomomon_garden::features::phase::messages::NextPhaseMessage;
|
||||||
|
use pomomon_garden::prelude::*;
|
||||||
|
|
||||||
|
fn setup_withering_app(
|
||||||
|
grid_width: u32,
|
||||||
|
grid_height: u32,
|
||||||
|
initial_tile_states: &[(u32, u32, TileState)],
|
||||||
|
) -> App {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins);
|
||||||
|
app.add_plugins(AssetPlugin::default());
|
||||||
|
|
||||||
|
app.init_resource::<TimerSettings>();
|
||||||
|
app.init_resource::<SessionTracker>();
|
||||||
|
app.insert_resource(CurrentPhase(Phase::Finished {
|
||||||
|
completed_phase: Box::new(Phase::Focus { duration: 25.0 }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.insert_resource(GameConfig {
|
||||||
|
berry_seeds: vec![BerrySeedConfig {
|
||||||
|
name: "TestSeed".into(),
|
||||||
|
cost: 1,
|
||||||
|
grants: 1,
|
||||||
|
slice: "".into(),
|
||||||
|
growth_stages: 5,
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut grid_tiles = Vec::with_capacity(grid_width as usize);
|
||||||
|
for x in 0..grid_width {
|
||||||
|
let mut column = Vec::with_capacity(grid_height as usize);
|
||||||
|
for y in 0..grid_height {
|
||||||
|
let entity = app
|
||||||
|
.world_mut()
|
||||||
|
.spawn((Tile { x, y }, TileState::Unclaimed))
|
||||||
|
.id();
|
||||||
|
column.push(entity);
|
||||||
|
}
|
||||||
|
grid_tiles.push(column);
|
||||||
|
}
|
||||||
|
app.insert_resource(Grid {
|
||||||
|
width: grid_width,
|
||||||
|
height: grid_height,
|
||||||
|
tiles: grid_tiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
for &(x, y, ref state) in initial_tile_states {
|
||||||
|
if let Ok(entity) = app.world().resource::<Grid>().get_tile((x, y)) {
|
||||||
|
*app.world_mut().get_mut::<TileState>(entity).unwrap() = state.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.add_message::<NextPhaseMessage>();
|
||||||
|
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_withering_progression() {
|
||||||
|
let seed_type = ItemType::BerrySeed {
|
||||||
|
name: "TestSeed".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start with a fresh plant
|
||||||
|
let initial_states = vec![(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
TileState::Occupied {
|
||||||
|
seed: seed_type.clone(),
|
||||||
|
watered: false,
|
||||||
|
growth_stage: 0,
|
||||||
|
withered: false,
|
||||||
|
dry_counter: 0,
|
||||||
|
},
|
||||||
|
)];
|
||||||
|
|
||||||
|
let mut app = setup_withering_app(1, 1, &initial_states);
|
||||||
|
|
||||||
|
// --- Phase 1: Not watered ---
|
||||||
|
app.world_mut().write_message(NextPhaseMessage);
|
||||||
|
let _ = app.world_mut().run_system_once(handle_continue);
|
||||||
|
|
||||||
|
let t = app.world().resource::<Grid>().get_tile((0, 0)).unwrap();
|
||||||
|
let s = app.world().entity(t).get::<TileState>().unwrap();
|
||||||
|
match s {
|
||||||
|
TileState::Occupied {
|
||||||
|
withered,
|
||||||
|
dry_counter,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(
|
||||||
|
*dry_counter, 1,
|
||||||
|
"Dry counter should be 1 after 1st dry phase"
|
||||||
|
);
|
||||||
|
assert_eq!(*withered, false, "Should not be withered yet");
|
||||||
|
}
|
||||||
|
_ => panic!("Invalid state"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Phase 2: Not watered -> Wither ---
|
||||||
|
// Reset phase to trigger update again
|
||||||
|
app.insert_resource(CurrentPhase(Phase::Finished {
|
||||||
|
completed_phase: Box::new(Phase::Focus { duration: 25.0 }),
|
||||||
|
}));
|
||||||
|
app.world_mut().write_message(NextPhaseMessage);
|
||||||
|
let _ = app.world_mut().run_system_once(handle_continue);
|
||||||
|
|
||||||
|
let s = app.world().entity(t).get::<TileState>().unwrap();
|
||||||
|
match s {
|
||||||
|
TileState::Occupied {
|
||||||
|
withered,
|
||||||
|
dry_counter,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(*dry_counter, 2, "Dry counter should be 2");
|
||||||
|
assert_eq!(*withered, true, "Should be withered now");
|
||||||
|
}
|
||||||
|
_ => panic!("Invalid state"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Phase 3: Watered -> Withered persists ---
|
||||||
|
// Manually water it (simulate user action)
|
||||||
|
if let Some(mut s_mut) = app.world_mut().get_mut::<TileState>(t) {
|
||||||
|
match *s_mut {
|
||||||
|
TileState::Occupied {
|
||||||
|
ref mut watered, ..
|
||||||
|
} => *watered = true,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run phase
|
||||||
|
app.insert_resource(CurrentPhase(Phase::Finished {
|
||||||
|
completed_phase: Box::new(Phase::Focus { duration: 25.0 }),
|
||||||
|
}));
|
||||||
|
app.world_mut().write_message(NextPhaseMessage);
|
||||||
|
let _ = app.world_mut().run_system_once(handle_continue);
|
||||||
|
|
||||||
|
let s = app.world().entity(t).get::<TileState>().unwrap();
|
||||||
|
match s {
|
||||||
|
TileState::Occupied {
|
||||||
|
growth_stage,
|
||||||
|
withered,
|
||||||
|
dry_counter,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(*dry_counter, 0, "Watering should reset dry counter");
|
||||||
|
assert_eq!(*withered, true, "Should still be withered");
|
||||||
|
assert_eq!(*growth_stage, 0, "Should not grow if withered");
|
||||||
|
}
|
||||||
|
_ => panic!("Invalid state"),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user