Compare commits

...

18 Commits

Author SHA1 Message Date
82bf4b6ced fix: Exclude target dir from CI cache to reduce size
Some checks failed
Gitea CI / Run Tests (push) Successful in 41m7s
Gitea CI / Build Linux (push) Failing after 10m19s
2025-12-21 01:00:31 +01:00
f21b74485e fix: Add libssl-dev to CI workflow to resolve openssl-sys build error
Some checks failed
Gitea CI / Build Linux (push) Has been cancelled
Gitea CI / Run Tests (push) Has been cancelled
2025-12-21 00:24:04 +01:00
e3b54f1a21 fix: Use correct package name libwayland-dev for CI
Some checks failed
Gitea CI / Run Tests (push) Failing after 10m8s
Gitea CI / Build Linux (push) Has been skipped
2025-12-21 00:08:11 +01:00
604b82216b build: Limit cargo test to pomomon-garden package
Some checks failed
Gitea CI / Run Tests (push) Failing after 6s
Gitea CI / Build Linux (push) Has been skipped
2025-12-21 00:07:25 +01:00
709e7bee2a fix: Run tests before build 2025-12-21 00:05:46 +01:00
5a5805d47f fix: Add libwayland-client-dev to CI dependencies 2025-12-21 00:04:13 +01:00
aaa2d582f0 fix: Run build job on all configured branches
Some checks failed
Gitea CI / Build Linux (push) Failing after 3m33s
Gitea CI / Run Tests (push) Failing after 5m49s
2025-12-20 23:44:48 +01:00
7d5d19df71 fix: Install nodejs for action runner compatibility
Some checks failed
Gitea CI / Build Linux (push) Has been skipped
Gitea CI / Run Tests (push) Failing after 56s
2025-12-20 23:43:30 +01:00
1f1b0abf21 build: Add gitea CI for linux builds and testing
Some checks failed
Gitea CI / Build Linux (push) Has been skipped
Gitea CI / Run Tests (push) Failing after 2m9s
2025-12-20 23:25:38 +01:00
demenik
337444b19d fix: Remove debug features from release build 2025-12-12 14:44:37 +01:00
Dominik Bernroider
93946696e3 Merge branch '47-achievement-menu-ui-implementation' into 'dev'
Achievement Menu UI

See merge request softwaregrundprojekt/2025-2026/einzelprojekt/tutorium-moritz/bernroider-dominik/bernroider-dominik!46
2025-12-11 19:30:20 +00:00
demenik
526a4657ae fix: Make save game delete button not rounded 2025-12-11 20:24:29 +01:00
demenik
01067d4770 fix: Change order of buttons in load save game menu 2025-12-11 20:22:31 +01:00
demenik
2857f59a85 feat: Show achievement progress in savegame load menu (#47) 2025-12-11 20:21:46 +01:00
demenik
52dd300ca0 feat: Add achievements button to HUD settings (#47) 2025-12-11 20:21:02 +01:00
demenik
8c9a27f0df feat: Implement achievement menu UI (#47) 2025-12-11 20:20:30 +01:00
demenik
d58b23c1b1 feat: Add title and description methods to AchievementId (#47) 2025-12-11 20:19:00 +01:00
demenik
e8af0add0b feat: Add achievement sprite (#47) 2025-12-11 19:52:53 +01:00
14 changed files with 279 additions and 43 deletions

67
.gitea/workflows/ci.yaml Normal file
View File

@@ -0,0 +1,67 @@
name: Gitea CI
on:
push:
branches:
- 'main'
- 'dev'
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
container:
image: rust:slim
steps:
- name: Install System Dependencies
run: |
apt-get update -yq
apt-get install -yq git g++ pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 libwayland-dev nodejs libssl-dev
- name: Checkout
uses: actions/checkout@v4
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Run Tests
run: cargo test --verbose --workspace --package pomomon-garden
build:
name: Build Linux
needs: test
runs-on: ubuntu-latest
container:
image: rust:slim
steps:
- name: Install System Dependencies
run: |
apt-get update -yq
apt-get install -yq git g++ pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 libwayland-dev nodejs libssl-dev
- name: Checkout
uses: actions/checkout@v4
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Build Release
run: cargo build --release --target x86_64-unknown-linux-gnu
- name: Get Short SHA
id: slug
run: echo "sha8=$(echo ${GITHUB_SHA} | cut -c1-8)" >> $GITHUB_OUTPUT
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: pomomon-garden-linux-x86_64-${{ steps.slug.outputs.sha8 }}
path: target/x86_64-unknown-linux-gnu/release/pomomon-garden
retention-days: 14

BIN
assets/achievement.aseprite Normal file

Binary file not shown.

View File

@@ -19,21 +19,41 @@ pub enum AchievementId {
}
impl AchievementId {
/// Label to be displayed ingame
pub fn label(&self) -> String {
/// Title to be displayed ingame
pub fn title(&self) -> String {
match self {
AchievementId::FirstSteps => "Erste Schritte: Verdiene eine Beere.",
AchievementId::MasterFarmer => "Meisterbauer: Verdiene 100 Beeren.",
AchievementId::BerryTycoon => "Beeren-Tycoon: Verdiene 1.000 Beeren.",
AchievementId::GettingStarted => "Aller Anfang: Schließe deine erste Fokus-Phase ab.",
AchievementId::FocusMaster => "Fokus-Meister: Schließe 10 Fokus-Phasen ab.",
AchievementId::ZenMaster => "Zen-Meister: Schließe 50 Fokus-Phasen ab.",
AchievementId::Negligent => "Nachlässig: Lasse eine Pflanze verdorren.",
AchievementId::CompostKing => "Kompost-König: Lasse 10 Pflanzen verdorren.",
AchievementId::FirstSteps => "Erste Schritte",
AchievementId::MasterFarmer => "Meisterbauer",
AchievementId::BerryTycoon => "Beeren-Tycoon",
AchievementId::GettingStarted => "Aller Anfang",
AchievementId::FocusMaster => "Fokus-Meister",
AchievementId::ZenMaster => "Zen-Meister",
AchievementId::Negligent => "Nachlässig",
AchievementId::CompostKing => "Kompost-König",
}
.into()
}
/// Description to be displayed ingame
pub fn description(&self) -> String {
match self {
AchievementId::FirstSteps => "Verdiene eine Beere.",
AchievementId::MasterFarmer => "Verdiene 100 Beeren.",
AchievementId::BerryTycoon => "Verdiene 1.000 Beeren.",
AchievementId::GettingStarted => "Schließe deine erste Fokus-Phase ab.",
AchievementId::FocusMaster => "Schließe 10 Fokus-Phasen ab.",
AchievementId::ZenMaster => "Schließe 50 Fokus-Phasen ab.",
AchievementId::Negligent => "Lasse eine Pflanze verdorren.",
AchievementId::CompostKing => "Lasse 10 Pflanzen verdorren.",
}
.into()
}
/// Label to be displayed ingame (Title: Description)
pub fn label(&self) -> String {
format!("{}: {}", self.title(), self.description())
}
/// Checks if an achievement's conditions are met
pub fn conditions_met(&self, tracker: &SessionTracker) -> bool {
match self {

View File

@@ -5,6 +5,7 @@ use components::{AchievementId, AchievementProgress};
use strum::IntoEnumIterator;
pub mod components;
pub mod ui;
pub struct AchievementPlugin;

View File

@@ -0,0 +1,90 @@
use crate::features::achievement::components::{AchievementId, AchievementProgress};
use crate::features::ui::ui::popups::spawn_popup;
use crate::prelude::*;
use strum::IntoEnumIterator;
#[derive(Component)]
pub enum AchievementRootMarker {
Menu,
}
pub fn open_achievements_menu(commands: &mut Commands, progress: &AchievementProgress) {
spawn_popup(
commands,
AchievementRootMarker::Menu,
"Erfolge",
Node {
width: px(700),
height: px(500),
..default()
},
|parent| {
// Scrollable Content
parent
.spawn((Node {
width: percent(100),
height: percent(100),
flex_direction: FlexDirection::Column,
overflow: Overflow::scroll_y(),
padding: UiRect::all(px(10)),
row_gap: px(10),
..default()
},))
.with_children(|list| {
for id in AchievementId::iter() {
let unlocked = progress.is_unlocked(&id);
let color = if unlocked {
Color::WHITE
} else {
Color::srgb(0.5, 0.5, 0.5)
};
let bg_color = if unlocked {
Color::srgb(0.2, 0.3, 0.2) // Dark Greenish for unlocked
} else {
Color::srgb(0.1, 0.1, 0.1) // Dark Grey for locked
};
list.spawn((
Node {
width: percent(100),
padding: UiRect::all(px(10)),
flex_direction: FlexDirection::Column,
row_gap: px(5),
border: UiRect::all(px(2)),
..default()
},
BackgroundColor(bg_color),
BorderColor::all(if unlocked {
Color::srgb(0.4, 0.8, 0.4)
} else {
Color::BLACK
}),
BorderRadius::all(px(5)),
))
.with_children(|item| {
// Title
item.spawn(text(id.title(), 20.0, color));
// Description
item.spawn(text(id.description(), 16.0, color));
// Status Text
let status = if unlocked {
"Freigeschaltet"
} else {
"Gesperrt"
};
item.spawn(text(
status,
12.0,
if unlocked {
Color::srgb(0.6, 1.0, 0.6)
} else {
Color::srgb(0.7, 0.3, 0.3)
},
));
});
}
});
},
);
}

View File

@@ -21,6 +21,7 @@ pub enum ButtonType {
SettingsOpen,
SettingsExit,
SettingsSave,
SettingsAchievements,
SettingsTimerChange {
input: SettingsTimerInput,
amount: i32,

View File

@@ -1,3 +1,4 @@
use crate::features::achievement::{components::AchievementProgress, ui::open_achievements_menu};
use crate::features::phase::components::TimerSettings;
use crate::features::savegame::messages::SavegameDumpMessage;
use crate::features::{inventory, shop};
@@ -151,6 +152,8 @@ fn buttons(
mut next_state: ResMut<NextState<AppState>>,
mut timer_settings: ResMut<TimerSettings>,
keys: Res<ButtonInput<KeyCode>>,
achievement_progress: Res<AchievementProgress>,
root_query: Query<(Entity, &RootMarker)>,
) {
let shift_multiplier = if keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]) {
10
@@ -171,6 +174,14 @@ fn buttons(
ButtonType::SettingsSave => {
savegame_messages.write(SavegameDumpMessage);
}
ButtonType::SettingsAchievements => {
open_achievements_menu(&mut commands, &achievement_progress);
for (entity, root) in root_query.iter() {
if let RootMarker::Settings = root {
commands.entity(entity).despawn();
}
}
}
ButtonType::SettingsTimerChange { input, amount } => match input {
SettingsTimerInput::Minutes(timer_type) => {
timer_settings.change(timer_type, 60 * amount * shift_multiplier)

View File

@@ -27,6 +27,12 @@ pub fn open_settings(commands: &mut Commands) {
ButtonVariant::Secondary,
Node::from_padding(UiRect::all(px(10))),
|color| text("Spiel speichern", 24.0, color)
),
button(
ButtonType::SettingsAchievements,
ButtonVariant::Secondary,
Node::from_padding(UiRect::all(px(10))),
|color| text("Erfolge", 24.0, color)
),(
Node {
justify_content: JustifyContent::Center,

View File

@@ -42,7 +42,10 @@ impl Plugin for InputPlugin {
app.add_systems(Update, next_phase.run_if(in_state(AppState::GameScreen)));
app.add_systems(Update, shop_keybind.run_if(in_state(AppState::GameScreen)));
app.add_systems(Update, inventory_keybind.run_if(in_state(AppState::GameScreen)));
app.add_systems(
Update,
inventory_keybind.run_if(in_state(AppState::GameScreen)),
);
app.add_message::<ClosePopupMessage>();
app.add_systems(Update, popup_keybind);
@@ -181,6 +184,7 @@ fn interact_click(
}
}
/// Handles debug interactions (shift + left click).
#[cfg(debug_assertions)]
fn debug_click(
mouse_btn: Res<ButtonInput<MouseButton>>,
keys: Res<ButtonInput<KeyCode>>,

View File

@@ -1,3 +1,4 @@
#[cfg(debug_assertions)]
use crate::features::phase::components::SessionTracker;
use crate::{features::inventory::ui::open_inventory, prelude::*};
use components::*;

View File

@@ -1,3 +1,4 @@
use crate::features::achievement::components::AchievementProgress;
use crate::prelude::*;
use std::fs;
use std::path::PathBuf;
@@ -7,18 +8,20 @@ use std::path::PathBuf;
pub struct SavegamePath(pub PathBuf);
/// Metadata about a savegame.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct SavegameInfo {
pub path: SavegamePath,
pub index: u32,
pub total_berries: u32,
pub completed_focus: u32,
pub achievement_progress: AchievementProgress,
}
/// Helper for partial JSON deserialization.
#[derive(Deserialize)]
struct PartialSaveData {
session_tracker: PartialSessionTracker,
achievement_progress: AchievementProgress,
}
/// Helper for partial JSON deserialization of session stats.
@@ -87,6 +90,7 @@ impl SavegamePath {
index,
total_berries: data.session_tracker.total_berries_earned,
completed_focus: data.session_tracker.completed_focus_phases,
achievement_progress: data.achievement_progress,
});
}
@@ -113,4 +117,5 @@ pub enum RootMarker {
pub enum ButtonType {
SavegameLoad { savegame_path: SavegamePath },
SavegameDelete { savegame_path: SavegamePath },
Achievements { savegame: SavegameInfo },
}

View File

@@ -1,8 +1,9 @@
use super::super::components::{ButtonType, RootMarker};
use crate::features::achievement::ui::open_achievements_menu;
use crate::{features::savegame::messages::SavegameLoadMessage, prelude::*};
/// Spawns the "Load Game" popup.
pub fn spawn_load_popup(commands: &mut Commands) {
pub fn spawn_load_popup(commands: &mut Commands, asset_server: &AssetServer) {
spawn_popup(
commands,
RootMarker::PopupSavegameLoad,
@@ -67,7 +68,26 @@ pub fn spawn_load_popup(commands: &mut Commands) {
),
]
),
pill_button(
button(
ButtonType::Achievements {
savegame: savegame.clone()
},
ButtonVariant::Primary,
Node {
width: px(40),
height: px(40),
..default()
},
|_| (
ImageNode::default(),
AseSlice {
aseprite: asset_server
.load("achievement.aseprite"),
name: "Achievement".into()
}
)
),
button(
ButtonType::SavegameDelete {
savegame_path: savegame.path.clone()
},
@@ -111,6 +131,9 @@ pub fn load_popup_handler(
println!("Error while deleting savegame: {:?}", e);
}
}
ButtonType::Achievements { savegame } => {
open_achievements_menu(&mut commands, &savegame.achievement_progress);
}
};
for (entity, root) in root_query.iter() {

View File

@@ -80,12 +80,13 @@ fn menu(
mut commands: Commands,
mut next_state: ResMut<NextState<AppState>>,
mut interaction_query: Query<(&Interaction, &ButtonType), (Changed<Interaction>, With<Button>)>,
asset_server: Res<AssetServer>,
) {
for (interaction, button_type) in &mut interaction_query {
match *interaction {
Interaction::Pressed => match button_type {
ButtonType::LoadGame => {
spawn_load_popup(&mut commands);
spawn_load_popup(&mut commands, &asset_server);
}
ButtonType::NewGame => {
commands.insert_resource(SavegamePath::next());

View File

@@ -1,15 +1,19 @@
use bevy_dev_tools::fps_overlay::*;
use pomomon_garden::prelude::*;
fn main() {
let config = GameConfig::read_config().unwrap_or(GameConfig::default());
App::new()
.add_plugins((
DefaultPlugins.set(ImagePlugin::default_nearest()),
AsepriteUltraPlugin,
))
.add_plugins((FpsOverlayPlugin {
let mut app = App::new();
app.add_plugins((
DefaultPlugins.set(ImagePlugin::default_nearest()),
AsepriteUltraPlugin,
));
#[cfg(debug_assertions)]
{
use bevy_dev_tools::fps_overlay::*;
app.add_plugins(FpsOverlayPlugin {
config: FpsOverlayConfig {
refresh_interval: core::time::Duration::from_millis(100),
enabled: true,
@@ -20,27 +24,29 @@ fn main() {
},
..default()
},
},))
.add_plugins((
features::CorePlugin,
features::StartScreenPlugin,
features::GameScreenPlugin,
features::GridPlugin,
features::PomPlugin,
features::InputPlugin,
features::PhasePlugin,
features::HudPlugin,
features::SavegamePlugin,
features::UiPlugin,
features::InventoryPlugin,
features::ShopPlugin,
features::WonderEventPlugin,
features::NotificationPlugin,
features::AchievementPlugin,
))
.insert_resource(config)
.add_systems(Startup, overwrite_default_font)
.run();
});
}
app.add_plugins((
features::CorePlugin,
features::StartScreenPlugin,
features::GameScreenPlugin,
features::GridPlugin,
features::PomPlugin,
features::InputPlugin,
features::PhasePlugin,
features::HudPlugin,
features::SavegamePlugin,
features::UiPlugin,
features::InventoryPlugin,
features::ShopPlugin,
features::WonderEventPlugin,
features::NotificationPlugin,
features::AchievementPlugin,
));
app.insert_resource(config);
app.add_systems(Startup, overwrite_default_font);
app.run();
}
fn overwrite_default_font(mut fonts: ResMut<Assets<Font>>) {