diff --git a/assets/maps/test_level.json b/assets/maps/test_level.json index 77e60eb5a2..6a0431fbe2 100644 --- a/assets/maps/test_level.json +++ b/assets/maps/test_level.json @@ -784,7 +784,24 @@ "id": "decorations", "kind": "object_layer", "has_collision": false, - "objects": [], + "objects": [ + { + "id": "anemones", + "kind": "decoration", + "position": { + "x": 313.01, + "y": 269.10092 + } + }, + { + "id": "seaweed", + "kind": "decoration", + "position": { + "x": 637.80585, + "y": 269.37476 + } + } + ], "is_visible": true }, { @@ -792,6 +809,46 @@ "kind": "object_layer", "has_collision": false, "objects": [ + { + "id": "crab", + "kind": "environment", + "position": { + "x": 486.8546, + "y": 178.04547 + } + }, + { + "id": "crab", + "kind": "environment", + "position": { + "x": 640.52356, + "y": 432.3415 + } + }, + { + "id": "crab", + "kind": "environment", + "position": { + "x": 414.69, + "y": 305.67166 + } + }, + { + "id": "crab", + "kind": "environment", + "position": { + "x": 610.0421, + "y": 302.41638 + } + }, + { + "id": "crab", + "kind": "environment", + "position": { + "x": 530.3602, + "y": 677.60126 + } + }, { "id": "trident", "kind": "item", @@ -804,8 +861,8 @@ "id": "crown_hat", "kind": "item", "position": { - "x": 524.5, - "y": 408.0 + "x": 519.0186, + "y": 405.68817 } }, { @@ -892,8 +949,8 @@ "id": "jellyfish", "kind": "item", "position": { - "x": 700.0, - "y": 650.0 + "x": 696.2978, + "y": 647.9329 } }, { @@ -1742,13 +1799,13 @@ false ], "tile_attributes": { - "56": [ + "58": [ "jumpthrough" ], - "60": [ + "56": [ "jumpthrough" ], - "58": [ + "60": [ "jumpthrough" ] } diff --git a/assets/textures.json b/assets/textures.json index 045db1e186..7829b80ca0 100644 --- a/assets/textures.json +++ b/assets/textures.json @@ -427,6 +427,15 @@ "y": 20 } }, + { + "id": "crab", + "path": "textures/items/Crab(15x8).png", + "type": "spritesheet", + "sprite_size": { + "x": 15, + "y": 8 + } + }, { "id": "sword", "path": "textures/items/Sword(65x93).png", diff --git a/assets/textures/items/Crab(15x8).png b/assets/textures/items/Crab(15x8).png new file mode 100644 index 0000000000..9972446f12 Binary files /dev/null and b/assets/textures/items/Crab(15x8).png differ diff --git a/src/editor/gui/windows/create_object.rs b/src/editor/gui/windows/create_object.rs index f6a8c35be5..5a5c1ccb95 100644 --- a/src/editor/gui/windows/create_object.rs +++ b/src/editor/gui/windows/create_object.rs @@ -132,7 +132,7 @@ impl Window for CreateObjectWindow { .keys() .map(|k| k.as_str()) .collect::>(), - MapObjectKind::Environment => vec!["sproinger"], + MapObjectKind::Environment => vec!["sproinger", "crab"], MapObjectKind::Decoration => resources .decoration .keys() diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 421b8bc6c9..f1bac22bb7 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,7 +1,7 @@ use std::any::TypeId; use std::path::Path; -use crate::{exit_to_main_menu, quit_to_desktop, Resources}; +use crate::{exit_to_main_menu, map::CRAB_TEXTURE_ID, quit_to_desktop, Resources}; mod camera; @@ -1449,8 +1449,8 @@ impl Node for Editor { label = Some("INVALID OBJECT ID".to_string()); } } - MapObjectKind::Environment => { - if &object.id == "sproinger" { + MapObjectKind::Environment => match object.id.as_str() { + "sproinger" => { let texture_res = resources.textures.get("sproinger").unwrap(); @@ -1476,10 +1476,22 @@ impl Node for Editor { ..Default::default() }, ); - } else { + } + "crab" => { + let texture_res = + resources.textures.get(CRAB_TEXTURE_ID).unwrap(); + + draw_texture( + texture_res.texture, + object_position.x, + object_position.y, + color::WHITE, + ); + } + _ => { label = Some("INVALID OBJECT ID".to_string()); } - } + }, } let size = get_object_size(object); @@ -1603,14 +1615,17 @@ fn get_object_size(object: &MapObject) -> Vec2 { label = Some("INVALID OBJECT ID".to_string()) } } - MapObjectKind::Environment => { - if &object.id == "sproinger" { + MapObjectKind::Environment => match object.id.as_str() { + "sproinger" => { let texture_res = resources.textures.get("sproinger").unwrap(); res = texture_res.meta.frame_size; - } else { - label = Some("INVALID OBJECT ID".to_string()) } - } + "crab" => { + let texture_res = resources.textures.get(CRAB_TEXTURE_ID).unwrap(); + res = texture_res.meta.frame_size; + } + _ => label = Some("INVALID OBJECT ID".to_string()), + }, } if let Some(label) = &label { diff --git a/src/editor/tools/placement.rs b/src/editor/tools/placement.rs index 843ea90afa..c397527dd8 100644 --- a/src/editor/tools/placement.rs +++ b/src/editor/tools/placement.rs @@ -86,7 +86,11 @@ impl EditorTool for TilePlacementTool { for x in 0..3 { if let Some(layer) = &ctx.selected_layer { let is_some = map - .get_tile(layer, coords.x + x - 1, coords.y + y - 1) + .get_tile( + layer, + (coords.x + x).saturating_sub(1), + (coords.y + y).saturating_sub(1), + ) .is_some(); surrounding_tiles.push(is_some); } diff --git a/src/game/mod.rs b/src/game/mod.rs index 5d0956d4a0..8f971a020c 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -34,7 +34,9 @@ use crate::effects::active::debug_draw_active_effects; use crate::effects::active::projectiles::fixed_update_projectiles; use crate::effects::active::triggered::fixed_update_triggered_effects; use crate::items::spawn_item; -use crate::map::{fixed_update_sproingers, spawn_decoration, spawn_sproinger}; +use crate::map::{ + fixed_update_sproingers, spawn_crab, spawn_decoration, spawn_sproinger, update_crabs, +}; use crate::network::{ fixed_update_network_client, fixed_update_network_host, update_network_client, update_network_host, @@ -118,7 +120,8 @@ impl Game { .add_system(update_player_states) .add_system(update_player_inventory) .add_system(update_player_passive_effects) - .add_system(update_player_events); + .add_system(update_player_events) + .add_system(update_crabs); fixed_updates_builder .add_system(fixed_update_physics_bodies) @@ -258,15 +261,20 @@ pub fn spawn_map_objects(world: &mut World, map: &Map) -> Result> { println!("WARNING: Invalid item id '{}'", &map_object.id) } } - MapObjectKind::Environment => { - if map_object.id == "sproinger" { + MapObjectKind::Environment => match map_object.id.as_str() { + "sproinger" => { let sproinger = spawn_sproinger(world, map_object.position)?; objects.push(sproinger); - } else { + } + "crab" => { + let crab = spawn_crab(world, map_object.position)?; + objects.push(crab); + } + _ => { #[cfg(debug_assertions)] println!("WARNING: Invalid environment item id '{}'", &map_object.id) } - } + }, } } } diff --git a/src/main.rs b/src/main.rs index c5669aa1b5..3d0687d7d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ pub mod particles; pub mod physics; pub mod player; pub mod resources; +pub mod utils; pub mod drawables; diff --git a/src/map/crab.rs b/src/map/crab.rs new file mode 100644 index 0000000000..b26d13607a --- /dev/null +++ b/src/map/crab.rs @@ -0,0 +1,189 @@ +use core::Transform; + +use fishsticks::error::Result; +use hecs::{Entity, With, World}; +use macroquad::{ + prelude::{collections::storage, Vec2}, + rand, +}; + +use crate::{ + player::Player, utils::timer::Timer, CollisionWorld, Drawable, PhysicsBody, PhysicsBodyParams, + Resources, +}; + +pub const CRAB_TEXTURE_ID: &str = "crab"; + +const DRAW_ORDER: u32 = 2; +/// This horizontal distance away from it's spawn point that a crab is comfortable being +const COMFORTABLE_SPAWN_DISTANCE: f32 = 25.0; +/// The distance away from "scary stuff" that the crab wants to be +const COMFORTABLE_SCARY_DISTANCE: f32 = 100.0; +/// The y difference in position to still consider being as on the same level as the crab +const SAME_LEVEL_THRESHOLD: f32 = 50.0; + +const WALK_SPEED: f32 = 0.5; +const RUN_SPEED: f32 = 2.5; + +pub struct Crab { + pub spawn_position: Vec2, + pub state: CrabState, + pub state_timer: Timer, +} + +#[derive(Debug)] +pub enum CrabState { + /// Standing still for a time + Paused, + /// Walking in a direction for a time + Walking { + /// Are we walking left? If false, we are walking right. + left: bool, + }, + /// We are running from something we're scared of + Running { + /// What scared the little crabbie? 🦀 + scared_of: Entity, + }, +} + +impl CrabState { + fn is_moving(&self) -> bool { + matches!(self, CrabState::Walking { .. } | CrabState::Running { .. }) + } +} + +impl Default for CrabState { + fn default() -> Self { + CrabState::Paused + } +} + +pub fn spawn_crab(world: &mut World, spawn_position: Vec2) -> Result { + let resources = storage::get::(); + let texture_res = resources.textures.get(CRAB_TEXTURE_ID).unwrap(); + let size = texture_res.meta.size; + let actor = storage::get_mut::().add_actor( + spawn_position, + size.x as i32, + size.y as i32, + ); + + Ok(world.spawn(( + Crab { + spawn_position, + state: CrabState::default(), + state_timer: Timer::new(1.0), + }, + Transform::from(spawn_position), + Drawable::new_sprite(DRAW_ORDER, CRAB_TEXTURE_ID, Default::default()), + PhysicsBody::new( + actor, + None, + PhysicsBodyParams { + size, + can_rotate: false, + gravity: 0.5, + ..Default::default() + }, + ), + ))) +} + +pub fn update_crabs(world: &mut World) { + for (_, (crab, drawable, transform, body)) in world + .query::<(&mut Crab, &mut Drawable, &Transform, &mut PhysicsBody)>() + .iter() + { + let transform: &Transform = transform; + let drawable: &mut Drawable = drawable; + let crab: &mut Crab = crab; + let body: &mut PhysicsBody = body; + + let pos = transform.position; + + let rand_bool = |true_bias: u8| rand::gen_range(0u8, 2 + true_bias) > 0; + let rand_delay = |min, max| Timer::new(rand::gen_range(min, max)); + + let next_scary_thing = || { + for (scary_entity, transform) in world.query::>().iter() { + let scary_pos = transform.position; + + if (pos - scary_pos).length() < COMFORTABLE_SCARY_DISTANCE // If scary thing is too close + && (pos.y - scary_pos.y).abs() < SAME_LEVEL_THRESHOLD + // and we're on the same level + { + return Some(scary_entity); + } + } + + None + }; + + let pick_next_move = || { + let x_diff = pos.x - crab.spawn_position.x; + + let pause_bias = if crab.state.is_moving() { 2 } else { 0 }; + if rand_bool(pause_bias) { + (CrabState::Paused, rand_delay(0.2, 0.5)) + } else { + let left = if (x_diff.abs() > COMFORTABLE_SPAWN_DISTANCE) && rand_bool(2) { + x_diff > 0.0 + } else { + rand_bool(0) + }; + (CrabState::Walking { left }, rand_delay(0.05, 0.75)) + } + }; + + crab.state_timer.tick_frame_time(); + + // Perform any state transitions + if crab.state_timer.has_finished() { + if let Some(scared_of) = next_scary_thing() { + crab.state = CrabState::Running { scared_of }; + crab.state_timer = rand_delay(0.3, 0.7); + } else { + match &crab.state { + CrabState::Paused | CrabState::Walking { .. } => { + let (state, timer) = pick_next_move(); + crab.state = state; + crab.state_timer = timer; + } + CrabState::Running { scared_of } => { + let scary_pos = world.get::(*scared_of).unwrap().position; + + if (pos - scary_pos).length() > COMFORTABLE_SCARY_DISTANCE { + if let Some(scared_of) = next_scary_thing() { + crab.state = CrabState::Running { scared_of }; + crab.state_timer = rand_delay(0.3, 0.7); + } else { + let (state, timer) = pick_next_move(); + crab.state = state; + crab.state_timer = timer; + } + } + } + } + } + } + + // Apply any component modifications for the current state + match &crab.state { + CrabState::Paused => body.velocity.x = 0.0, + CrabState::Walking { left } => { + drawable.get_sprite_mut().unwrap().is_flipped_x = *left; + let direction = if *left { -1.0 } else { 1.0 }; + let speed = direction * WALK_SPEED; + body.velocity.x = speed; + } + CrabState::Running { scared_of } => { + let scary_pos = world.get::(*scared_of).unwrap().position; + + let direction = (pos.x - scary_pos.x).signum(); + let speed = direction * RUN_SPEED; + body.velocity.x = speed; + } + } + } +} diff --git a/src/map/mod.rs b/src/map/mod.rs index a755080a4e..648da58fc9 100644 --- a/src/map/mod.rs +++ b/src/map/mod.rs @@ -4,9 +4,11 @@ use macroquad::{color, experimental::collections::storage, prelude::*}; use serde::{Deserialize, Serialize}; +mod crab; mod decoration; mod sproinger; +pub use crab::*; pub use decoration::*; pub use sproinger::*; diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000000..1227065ce2 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod timer; diff --git a/src/utils/timer.rs b/src/utils/timer.rs new file mode 100644 index 0000000000..6f0928984a --- /dev/null +++ b/src/utils/timer.rs @@ -0,0 +1,69 @@ +/// A simple timer utility +#[derive(Debug)] +pub struct Timer { + duration: f32, + elapsed: f32, +} + +impl Timer { + /// Create a new timer with the provider duration + pub fn new(duration: f32) -> Self { + Timer { + duration, + elapsed: f32::default(), + } + } + + /// Get the duration of the timer + pub fn duration(&self) -> f32 { + self.duration + } + + /// Get the time that has been elapsed + pub fn elapsed(&self) -> f32 { + self.elapsed + } + + /// Get whether or not the timer has finished + pub fn has_finished(&self) -> bool { + self.elapsed > self.duration + } + + /// Reset the time elapsed + pub fn reset(&mut self) { + self.elapsed = 0.0; + } + + /// Advanced the elapsed time by the macroquad frame time + pub fn tick_frame_time(&mut self) { + self.elapsed += macroquad::time::get_frame_time(); + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_timer() { + let mut t = Timer::new(3.0); + + assert_eq!(t.duration(), 3.0); + + t.elapsed += 0.5; + assert_eq!(t.elapsed(), 0.5); + assert_eq!(t.has_finished(), false); + + t.elapsed += 2.0; + assert_eq!(t.elapsed(), 2.5); + assert_eq!(t.has_finished(), false); + + t.elapsed += 1.0; + assert_eq!(t.elapsed(), 3.5); + assert_eq!(t.has_finished(), true); + + t.reset(); + assert_eq!(t.duration(), 3.0); + assert_eq!(t.elapsed(), 0.0); + } +}