diff --git a/.forgejo/workflows/rust.yml b/.forgejo/workflows/rust.yml index 84e04f9..01a4dfd 100644 --- a/.forgejo/workflows/rust.yml +++ b/.forgejo/workflows/rust.yml @@ -59,10 +59,11 @@ jobs: target key: "${{runner.os}} Rust ${{steps.rust-toolchain.outputs.cachekey}}" - run: mkdir .ci-destdir - - run: cargo install --path . --locked --root .ci-destdir + - run: cargo install --path . --locked --root . - run: find .ci-destdir - uses: forgejo/upload-artifact@v3 with: name: Powercreep - path: - .ci-destdir/* + path: | + bin + assets diff --git a/assets/entities/human.png b/assets/entities/human.png new file mode 100644 index 0000000..cd10a96 Binary files /dev/null and b/assets/entities/human.png differ diff --git a/assets/entities/human_captured.png b/assets/entities/human_captured.png new file mode 100644 index 0000000..505bfc8 Binary files /dev/null and b/assets/entities/human_captured.png differ diff --git a/assets/entities/human_overworld.png b/assets/entities/human_overworld.png new file mode 100644 index 0000000..43609bf Binary files /dev/null and b/assets/entities/human_overworld.png differ diff --git a/build.rs b/build.rs index 15a5cd1..edecaa2 100644 --- a/build.rs +++ b/build.rs @@ -122,16 +122,16 @@ impl AssetsWriter { writeln!(file, "{indent}impl Assets {{")?; writeln!( file, - "{indent}\tpub fn load(c: &mut comfy::EngineContext<'_>) {{" + "{indent}\tpub fn load(_ctx: &mut comfy::EngineContext<'_>) {{" )?; for asset_const_name in root.assets.values() { - writeln!(file, "{indent}\t\tc.load_texture_from_bytes({asset_const_name:?}, {asset_const_name});")?; + writeln!(file, "{indent}\t\t_ctx.load_texture_from_bytes({asset_const_name:?}, {asset_const_name});")?; } for asset_const_name in root.sound_assets.values() { writeln!(file, "{indent}\t\tcomfy::load_sound_from_bytes({asset_const_name:?}, {asset_const_name}, Default::default());")?; } for group_name in root.groups.keys() { - writeln!(file, "{indent}\t\t{group_name}::Assets::load(c);")?; + writeln!(file, "{indent}\t\t{group_name}::Assets::load(_ctx);")?; } writeln!(file, "{indent}\t}}")?; writeln!(file, "{indent}}}")?; diff --git a/src/activities/house/furniture.rs b/src/activities/house/furniture.rs index 0377185..916eecc 100644 --- a/src/activities/house/furniture.rs +++ b/src/activities/house/furniture.rs @@ -1,5 +1,5 @@ use comfy::{error, texture_id, EngineContext, HashSet, Lazy, Mutex, TextureHandle}; -use std::{fmt::Debug, fs, io, sync::Arc}; +use std::{fmt::Debug, fs, io, path::PathBuf, sync::Arc}; static ASSETS_LOADED: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(HashSet::new()))); @@ -39,10 +39,13 @@ impl FurnitureAsset { if loaded.contains(&path) { return Some(texture_id(&path)); } - let bytes = match fs::read(format!( - "{}/assets/furniture/{path}", - env!("CARGO_MANIFEST_DIR") - )) { + + let mut asset_path = PathBuf::from(format!("assets/furniture/{path}")); + if !asset_path.exists() { + asset_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(asset_path); + } + + let bytes = match fs::read(asset_path) { Ok(bytes) => bytes, Err(err) if err.kind() == io::ErrorKind::NotFound => return None, Err(err) => { diff --git a/src/activities/house/mod.rs b/src/activities/house/mod.rs index ef5f25f..edcd05a 100644 --- a/src/activities/house/mod.rs +++ b/src/activities/house/mod.rs @@ -3,7 +3,7 @@ mod grid; mod player; mod room; -use comfy::{delta, random_i32, vec2, EngineContext, RandomRange}; +use comfy::{delta, main_camera_mut, random_i32, vec2, EngineContext, RandomRange as _}; use grid::Grid; use indexmap::IndexSet; use log::error; @@ -25,6 +25,11 @@ pub struct HouseState { player: Player, human_layer: bool, //Human, magnetic, electric exit_time: f32, + + /// The energy level remaining in the house. Should decrease by itself, and much + /// faster when inhabited by the ghost. + pub charge: f32, + pub max_charge: f32 } impl HouseState { @@ -48,6 +53,7 @@ impl HouseState { } let player = Player::new(rooms.first().unwrap()); + let max_charge = f32::gen_range(2_000.0, 5_000.0); HouseState { current_room_id: 0, room_count, @@ -57,6 +63,9 @@ impl HouseState { player, human_layer: false, exit_time: 0.0, + // TODO this should be lower depending on the time elapsed + charge: max_charge, + max_charge } } } @@ -82,6 +91,10 @@ pub fn draw(state: &crate::State, _ctx: &comfy::EngineContext<'_>) { } pub fn update(state: &mut crate::State, ctx: &mut comfy::EngineContext<'_>) { + let mut camera = main_camera_mut(); + camera.center = vec2(0.0, 0.0); + drop(camera); + let house = state.house_mut(ctx); let current_room = house.rooms.get(house.current_room_id).unwrap(); house.player.update(¤t_room); diff --git a/src/activities/house/room.rs b/src/activities/house/room.rs index ff5d308..fd14e5f 100644 --- a/src/activities/house/room.rs +++ b/src/activities/house/room.rs @@ -110,6 +110,37 @@ impl Room { let random_idx = usize::gen_range(0, empty_spots.len()); empty_spots.swap_remove_index(random_idx) } + + fn random_empty_spot_size( + empty_spots: &mut IndexSet, + size: u8 + ) -> Option { + let mut empty_spots_size = IndexSet::::new(); + + for &index in empty_spots.iter() { + let mut is_valid = true; + + for offset in 0 .. size { + if !empty_spots.contains(&(index + offset)) { + is_valid = false; + break; + } + } + if is_valid { + empty_spots_size.insert(index); + } + } + + if empty_spots_size.is_empty() { + return None; + } + let random_idx = usize::gen_range(0, empty_spots_size.len()); + for offset in (0 .. size).rev() { + empty_spots.swap_remove_index(random_idx + offset as usize); + } + Some(random_idx as u8) + } + fn random_appliance(empty_spots: &mut Vec) -> Option { if empty_spots.is_empty() { return None; @@ -276,6 +307,42 @@ impl Room { } }, + RoomType::LivingRoom => { + let has_couch = match u8::gen_range(0, 2) { + 0 => false, + 1 => true, + _ => unreachable!() + }; + if has_couch { + if let Some(pos) = random_empty_spot_size(&mut empty_spots, 3) { + furnitures.push(Tile { + pos: vec2(pos as f32, 0.0), + size: vec2(3.0, 1.0), + f: Furniture::new("bedroom", "couch", ctx), + z: 0 + }); + } + } + + if let Some(pos) = random_empty_spot(&mut empty_spots) { + furnitures.push(Tile { + pos: vec2(pos as f32, 0.0), + size: vec2(1.0, 2.0), + f: Furniture::new("bedroom", "bookshelf", ctx), + z: 0 + }); + } + + if let Some(pos) = random_empty_spot(&mut empty_spots) { + furnitures.push(Tile { + pos: vec2(pos as f32, 0.0), + size: vec2(0.5, 0.9), + f: Furniture::new("bedroom", "mini_ac", ctx), + z: 0 + }); + } + }, + _ => {} } diff --git a/src/activities/overworld/mod.rs b/src/activities/overworld/mod.rs index 50c354f..0068780 100644 --- a/src/activities/overworld/mod.rs +++ b/src/activities/overworld/mod.rs @@ -1,14 +1,28 @@ -use crate::{activities::Activity, game::ZLayer, State}; +use crate::{ + activities::Activity, + game::{ZLayer, GHOST_DISCHARGE_RATE, GHOST_DISCHARGE_RATE_MOVEMENT}, + State +}; use comfy::{ - draw_circle, draw_rect_outline, draw_sprite, error, info, is_key_down, - main_camera_mut, EngineContext, IVec2, KeyCode, Vec2, RED, WHITE + draw_rect_outline, draw_sprite, error, info, is_key_down, main_camera_mut, + texture_id, vec2, EngineContext, IVec2, KeyCode, Vec2, RED, WHITE }; use std::time::Instant; use worldgen::MovementCost; pub mod worldgen; -pub fn draw(state: &crate::State, _engine: &comfy::EngineContext<'_>) { +pub fn setup(_state: &State, ctx: &EngineContext<'_>) { + ctx.load_texture_from_bytes( + "human_overworld", + include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/entities/human_overworld.png" + )) + ); +} + +pub fn draw(state: &State, _ctx: &EngineContext<'_>) { for (coords, tile) in state.overworld.iter_tiles() { for (i, texture) in tile.textures().iter().rev().enumerate() { let i = i as i32; @@ -24,10 +38,19 @@ pub fn draw(state: &crate::State, _engine: &comfy::EngineContext<'_>) { } } } - draw_circle(state.ghost.overworld_pos, 0.5, RED, ZLayer::Ghost.into()); + + let mut ghost_pos = state.ghost.overworld_pos; + ghost_pos.y += 0.5; + draw_sprite( + texture_id("human_overworld"), + ghost_pos, + WHITE, + ZLayer::Ghost.into(), + vec2(1.0, 1.25) + ); } -fn update_move_player(state: &mut State, ctx: &mut EngineContext<'_>) { +fn update_move_player(state: &mut State, _ctx: &mut EngineContext<'_>) { let now = Instant::now(); // Are there any pending position updates? If so, we ignore all user input and execute @@ -174,6 +197,15 @@ pub fn update(state: &mut State, ctx: &mut EngineContext<'_>) { } } + // energie lost + { + let ghost = &mut state.ghost; + ghost.charge -= GHOST_DISCHARGE_RATE * ctx.delta; + if ghost.overworld_movement_pending != Vec2::ZERO { + ghost.charge -= GHOST_DISCHARGE_RATE_MOVEMENT * ctx.delta; + } + } + // generate more chunks if needed { let half_viewport = (camera.world_viewport() * 0.5 + 3.0).as_ivec2(); diff --git a/src/game.rs b/src/game.rs index 23b36ca..5674bcd 100644 --- a/src/game.rs +++ b/src/game.rs @@ -4,7 +4,10 @@ use crate::{ State }; use comfy::{EngineContext, Vec2}; -use std::{ops::Sub, time::Instant}; +use std::{ + ops::{Add, Sub}, + time::Instant +}; #[derive(Debug)] pub struct Ghost { @@ -71,7 +74,8 @@ pub enum ZLayer { ElectricLayer = -2, MapMax = -1, Human = 0, - Ghost = 1 + Ghost = 1, + UI = 100 } impl From for i32 { @@ -89,9 +93,18 @@ impl Sub for ZLayer { } } +impl Add for ZLayer { + type Output = i32; + + fn add(self, other: i32) -> Self::Output { + i32::from(self) + other + } +} + pub fn setup(state: &mut State, ctx: &mut EngineContext<'_>) { Assets::load(ctx); + overworld::setup(state, ctx); //house::setup(state, ctx); ctx.load_texture_from_bytes( @@ -103,12 +116,54 @@ pub fn setup(state: &mut State, ctx: &mut EngineContext<'_>) { ); } -pub fn update(state: &mut State, engine: &mut EngineContext<'_>) { - state.score += engine.delta * 10.0; +/// The amount of energy a ghost consumes idle. +pub const GHOST_DISCHARGE_RATE: f32 = 70.0; +/// The amount of energy additionally consumed by a moving ghost. +pub const GHOST_DISCHARGE_RATE_MOVEMENT: f32 = 70.0; +/// The amount of energy a house consumes idle. +pub const HOUSE_DISCHARGE_RATE: f32 = 30.0; +/// The amount of energy a ghost can charge when inside a house. +pub const GHOST_CHARGE_RATE: f32 = 200.0; + +pub fn update(state: &mut State, ctx: &mut EngineContext<'_>) { + // Update the score. It's based on time. + state.score += ctx.delta * 10.0; + + // Update the currently active activity. match state.activity { - Activity::House(_) => house::update(state, engine), - Activity::Overworld => overworld::update(state, engine) + Activity::House(_) => house::update(state, ctx), + Activity::Overworld => overworld::update(state, ctx) } + + // We update the charge of houses here - the charge will always decrease, even if the + // house is not the current activity. + for (house_pos, house) in &mut state.houses { + house.charge -= ctx.delta * HOUSE_DISCHARGE_RATE; + + match state.activity { + Activity::House(pos) if *house_pos == pos => { + // The ghost is currently inside the house. Increase its discarge rate. + house.charge -= ctx.delta * GHOST_DISCHARGE_RATE; + if house.charge < 0.0 { + state.ghost.charge += house.charge; + house.charge = 0.0; + } + + // And possibly also charge the ghost when inside a house. + if state.ghost.charge < state.ghost.max_charge { + let charge_transfer = (ctx.delta * GHOST_CHARGE_RATE) + .min(state.ghost.max_charge - state.ghost.charge) + .min(house.charge); + state.ghost.charge += charge_transfer; + house.charge -= charge_transfer; + } + }, + _ => {} + } + } + + // Make sure the ghost's charge never drops below 0. + state.ghost.charge = state.ghost.charge.max(0.0); } pub fn draw(state: &State, engine: &EngineContext<'_>) { diff --git a/src/ui.rs b/src/ui.rs index 9d434b2..4a22853 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,42 +1,66 @@ -use crate::State; +use crate::{game::ZLayer, State}; use comfy::{ draw_rect, draw_rect_outline, egui, screen_height, screen_to_world, screen_width, - EngineContext, Vec2, BLUE, RED, WHITE + vec2, Color, EngineContext, Vec2, BLACK, BLUE, PURPLE, RED, WHITE }; use egui::widget_text::RichText; -pub fn draw_batterie(state: &State, _engine: &EngineContext<'_>) { - // seperate fill state into smaller section for better readability - let section_count = 5; - let mut start_positon = screen_to_world(Vec2::new(screen_width(), screen_height())); +// seperate fill state into smaller section for better readability +const BATTERY_SECTION_COUNT: u8 = 5; +const BATTERY_SECTION_WIDTH: f32 = 1.0; +const BATTERY_SECTION_HEIGHT: f32 = 0.5; + +fn draw_battery(mut start_position: Vec2, charge: f32, max_charge: f32, color: Color) { // section size in world codinates - let section_size = Vec2::new(0.5, 0.25); - start_positon.x -= 0.5 * section_size.x + 0.5 * section_size.y; - start_positon.y += 0.5 * section_size.y + 0.5 * section_size.y; + let section_size = vec2(BATTERY_SECTION_WIDTH, BATTERY_SECTION_HEIGHT); + start_position.x -= 0.5 * section_size.x + 0.5 * section_size.y; + start_position.y += 0.5 * section_size.y + 0.5 * section_size.y; // draw fill level { - let ghost = &state.ghost; - let percent = ghost.charge / ghost.max_charge; + let percent = charge / max_charge; let mut size = section_size; - size.y = section_size.y * section_count as f32 * percent; - let mut position = start_positon; + size.y = section_size.y * BATTERY_SECTION_COUNT as f32; + let mut position = start_position; position.y += 0.5 * -section_size.y + 0.5 * size.y; - draw_rect(position, size, BLUE, 100); + draw_rect(position, size, BLACK, ZLayer::UI.into()); + size.y *= percent; + let mut position = start_position; + position.y += 0.5 * -section_size.y + 0.5 * size.y; + draw_rect(position, size, BLUE, ZLayer::UI + 1); } // draw sections - for i in 0 .. section_count { - let mut position = start_positon; + for i in 0 .. BATTERY_SECTION_COUNT { + let mut position = start_position; position.y += i as f32 * section_size.y; - draw_rect_outline(position, section_size, 0.1, RED, 100); + draw_rect_outline(position, section_size, 0.1, color, ZLayer::UI + 2); } } -pub fn draw_highscore(state: &State, _engine: &EngineContext<'_>) { +pub fn draw_ghost_battery(state: &State) { + let start_position = screen_to_world(Vec2::new(screen_width(), screen_height())); + draw_battery( + start_position, + state.ghost.charge, + state.ghost.max_charge, + RED + ); +} + +pub fn draw_house_battery(state: &State) { + let Some(house) = state.house() else { + return; + }; + let mut start_position = screen_to_world(Vec2::new(screen_width(), screen_height())); + start_position.x -= 2.0 * BATTERY_SECTION_WIDTH; + draw_battery(start_position, house.charge, house.max_charge, PURPLE) +} + +pub fn draw_highscore(state: &State) { egui::Area::new("score") .anchor(egui::Align2::RIGHT_TOP, egui::vec2(0.0, 0.0)) .show(egui(), |ui| { ui.label( - RichText::new(format!("{:.0}", state.score)) + RichText::new(format!("{:.0} ", state.score)) .color(WHITE) .monospace() .size(16.0) @@ -45,7 +69,8 @@ pub fn draw_highscore(state: &State, _engine: &EngineContext<'_>) { }); } -pub fn draw(state: &State, engine: &EngineContext<'_>) { - draw_batterie(state, engine); - draw_highscore(state, engine); +pub fn draw(state: &State, _ctx: &EngineContext<'_>) { + draw_ghost_battery(state); + draw_house_battery(state); + draw_highscore(state); }