diff --git a/assets/maps/test_level.json b/assets/maps/test_level.json index 6a0431fbe2..a0218c4917 100644 --- a/assets/maps/test_level.json +++ b/assets/maps/test_level.json @@ -809,6 +809,30 @@ "kind": "object_layer", "has_collision": false, "objects": [ + { + "id": "fish_school", + "kind": "environment", + "position": { + "x": 312.14642, + "y": 615.9121 + } + }, + { + "id": "fish_school", + "kind": "environment", + "position": { + "x": 466.00787, + "y": 497.32727 + } + }, + { + "id": "fish_school", + "kind": "environment", + "position": { + "x": 622.8744, + "y": 246.50894 + } + }, { "id": "crab", "kind": "environment", @@ -853,8 +877,8 @@ "id": "trident", "kind": "item", "position": { - "x": 786.3906, - "y": 677.3542 + "x": 702.3754, + "y": 287.63873 } }, { @@ -945,38 +969,6 @@ "y": 518.0 } }, - { - "id": "jellyfish", - "kind": "item", - "position": { - "x": 696.2978, - "y": 647.9329 - } - }, - { - "id": "galleon", - "kind": "item", - "position": { - "x": 500.0, - "y": 650.0 - } - }, - { - "id": "volcano", - "kind": "item", - "position": { - "x": 350.0, - "y": 650.0 - } - }, - { - "id": "shark_rain", - "kind": "item", - "position": { - "x": 300.0, - "y": 50.0 - } - }, { "id": "grenades", "kind": "item", @@ -1001,22 +993,6 @@ "y": 388.0 } }, - { - "id": "shoes", - "kind": "item", - "position": { - "x": 770.0, - "y": 147.0 - } - }, - { - "id": "curse", - "kind": "item", - "position": { - "x": 772.0, - "y": 266.0 - } - }, { "id": "cannon", "kind": "item", @@ -1049,22 +1025,6 @@ "y": 552.0 } }, - { - "id": "life_ring", - "kind": "item", - "position": { - "x": 435.33334, - "y": 547.3333 - } - }, - { - "id": "flippers", - "kind": "item", - "position": { - "x": 500.0, - "y": 650.0 - } - }, { "id": "kick_bomb", "kind": "item", @@ -1802,10 +1762,10 @@ "58": [ "jumpthrough" ], - "56": [ + "60": [ "jumpthrough" ], - "60": [ + "56": [ "jumpthrough" ] } diff --git a/assets/textures.json b/assets/textures.json index 7829b80ca0..d4493e961e 100644 --- a/assets/textures.json +++ b/assets/textures.json @@ -429,11 +429,101 @@ }, { "id": "crab", - "path": "textures/items/Crab(15x8).png", + "path": "textures/items/Crab(17x12).png", "type": "spritesheet", "sprite_size": { - "x": 15, - "y": 8 + "x": 17, + "y": 12 + } + }, + { + "id": "blue_tang", + "path": "textures/items/BlueTang(19x9).png", + "type": "spritesheet", + "sprite_size": { + "x": 19, + "y": 9 + } + }, + { + "id": "royal_gramma", + "path": "textures/items/RoyalGramma(25x11).png", + "type": "spritesheet", + "sprite_size": { + "x": 25, + "y": 11 + } + }, + { + "id": "arabian_angelfish", + "path": "textures/items/ArabianAngelfish(19x12).png", + "type": "spritesheet", + "sprite_size": { + "x": 19, + "y": 12 + } + }, + { + "id": "blue_green_chromis", + "path": "textures/items/BlueGreenChromis(22x11).png", + "type": "spritesheet", + "sprite_size": { + "x": 22, + "y": 11 + } + }, + { + "id": "banded_butterfly_fish", + "path": "textures/items/BandedButterflyFish(19x11).png", + "type": "spritesheet", + "sprite_size": { + "x": 19, + "y": 11 + } + }, + { + "id": "small_fish1", + "path": "textures/items/SmallFish1(13x9).png", + "type": "spritesheet", + "sprite_size": { + "x": 13, + "y": 9 + } + }, + { + "id": "small_fish1", + "path": "textures/items/SmallFish1(13x9).png", + "type": "spritesheet", + "sprite_size": { + "x": 13, + "y": 9 + } + }, + { + "id": "small_fish2", + "path": "textures/items/SmallFish3(13x9).png", + "type": "spritesheet", + "sprite_size": { + "x": 13, + "y": 9 + } + }, + { + "id": "small_fish3", + "path": "textures/items/SmallFish2(13x9).png", + "type": "spritesheet", + "sprite_size": { + "x": 13, + "y": 9 + } + }, + { + "id": "fish_school_icon", + "path": "textures/items/FishSchoolIcon(64x64).png", + "type": "spritesheet", + "sprite_size": { + "x": 64, + "y": 64 } }, { diff --git a/assets/textures/items/ArabianAngelfish(19x12).png b/assets/textures/items/ArabianAngelfish(19x12).png new file mode 100644 index 0000000000..73f541293a Binary files /dev/null and b/assets/textures/items/ArabianAngelfish(19x12).png differ diff --git a/assets/textures/items/BandedButterflyFish(19x11).png b/assets/textures/items/BandedButterflyFish(19x11).png new file mode 100644 index 0000000000..99bf51f273 Binary files /dev/null and b/assets/textures/items/BandedButterflyFish(19x11).png differ diff --git a/assets/textures/items/BlueGreenChromis(22x11).png b/assets/textures/items/BlueGreenChromis(22x11).png new file mode 100644 index 0000000000..579764a9f9 Binary files /dev/null and b/assets/textures/items/BlueGreenChromis(22x11).png differ diff --git a/assets/textures/items/BlueTang(19x9).png b/assets/textures/items/BlueTang(19x9).png new file mode 100644 index 0000000000..2c8243c561 Binary files /dev/null and b/assets/textures/items/BlueTang(19x9).png differ diff --git a/assets/textures/items/Crab(15x8).png b/assets/textures/items/Crab(15x8).png deleted file mode 100644 index 9972446f12..0000000000 Binary files a/assets/textures/items/Crab(15x8).png and /dev/null differ diff --git a/assets/textures/items/Crab(17x12).png b/assets/textures/items/Crab(17x12).png new file mode 100644 index 0000000000..718e067b09 Binary files /dev/null and b/assets/textures/items/Crab(17x12).png differ diff --git a/assets/textures/items/FishSchoolIcon(64x64).png b/assets/textures/items/FishSchoolIcon(64x64).png new file mode 100644 index 0000000000..862ea7954d Binary files /dev/null and b/assets/textures/items/FishSchoolIcon(64x64).png differ diff --git a/assets/textures/items/RoyalGramma(25x11).png b/assets/textures/items/RoyalGramma(25x11).png new file mode 100644 index 0000000000..afaa57ecc3 Binary files /dev/null and b/assets/textures/items/RoyalGramma(25x11).png differ diff --git a/assets/textures/items/SmallFish1(13x9).png b/assets/textures/items/SmallFish1(13x9).png new file mode 100644 index 0000000000..b5d7183e64 Binary files /dev/null and b/assets/textures/items/SmallFish1(13x9).png differ diff --git a/assets/textures/items/SmallFish2(13x9).png b/assets/textures/items/SmallFish2(13x9).png new file mode 100644 index 0000000000..0030b73900 Binary files /dev/null and b/assets/textures/items/SmallFish2(13x9).png differ diff --git a/assets/textures/items/SmallFish3(13x9).png b/assets/textures/items/SmallFish3(13x9).png new file mode 100644 index 0000000000..981f074d6f Binary files /dev/null and b/assets/textures/items/SmallFish3(13x9).png differ diff --git a/src/editor/gui/windows/create_object.rs b/src/editor/gui/windows/create_object.rs index 5a5c1ccb95..92320eb02d 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", "crab"], + MapObjectKind::Environment => vec!["sproinger", "crab", "fish_school"], MapObjectKind::Decoration => resources .decoration .keys() diff --git a/src/editor/mod.rs b/src/editor/mod.rs index f1bac22bb7..291730597f 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -1,7 +1,11 @@ use std::any::TypeId; use std::path::Path; -use crate::{exit_to_main_menu, map::CRAB_TEXTURE_ID, quit_to_desktop, Resources}; +use crate::{ + exit_to_main_menu, + map::{CRAB_TEXTURE_ID, FISH_SCHOOL_ICON_TEXTURE_ID}, + quit_to_desktop, Resources, +}; mod camera; @@ -1450,9 +1454,15 @@ impl Node for Editor { } } MapObjectKind::Environment => match object.id.as_str() { - "sproinger" => { + "sproinger" | "crab" | "fish_school" => { + let texture_id = match object.id.as_str() { + "sproinger" => "sproinger", + "crab" => CRAB_TEXTURE_ID, + "fish_school" => FISH_SCHOOL_ICON_TEXTURE_ID, + _ => unreachable!(), + }; let texture_res = - resources.textures.get("sproinger").unwrap(); + resources.textures.get(texture_id).unwrap(); let frame_size = texture_res.meta.frame_size.unwrap_or_else(|| { @@ -1477,17 +1487,6 @@ impl Node for Editor { }, ); } - "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()); } @@ -1624,6 +1623,10 @@ fn get_object_size(object: &MapObject) -> Vec2 { let texture_res = resources.textures.get(CRAB_TEXTURE_ID).unwrap(); res = texture_res.meta.frame_size; } + "fish_school" => { + let texture_res = resources.textures.get(FISH_SCHOOL_ICON_TEXTURE_ID).unwrap(); + res = texture_res.meta.frame_size; + } _ => label = Some("INVALID OBJECT ID".to_string()), }, } diff --git a/src/game/mod.rs b/src/game/mod.rs index 8f971a020c..6a18bc569b 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -35,7 +35,8 @@ 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_crab, spawn_decoration, spawn_sproinger, update_crabs, + debug_draw_fish_schools, fixed_update_sproingers, spawn_crab, spawn_decoration, + spawn_fish_school, spawn_sproinger, update_crabs, update_fish_schools, }; use crate::network::{ fixed_update_network_client, fixed_update_network_host, update_network_client, @@ -121,6 +122,7 @@ impl Game { .add_system(update_player_inventory) .add_system(update_player_passive_effects) .add_system(update_player_events) + .add_system(update_fish_schools) .add_system(update_crabs); fixed_updates_builder @@ -151,6 +153,7 @@ impl Game { .with_thread_local(debug_draw_physics_bodies) .with_thread_local(debug_draw_rigid_bodies) .with_thread_local(debug_draw_active_effects) + .with_thread_local(debug_draw_fish_schools) .build(); let res = Game { @@ -270,6 +273,10 @@ pub fn spawn_map_objects(world: &mut World, map: &Map) -> Result> { let crab = spawn_crab(world, map_object.position)?; objects.push(crab); } + "fish_school" => { + let fish_school = spawn_fish_school(world, map_object.position)?; + objects.push(fish_school); + } _ => { #[cfg(debug_assertions)] println!("WARNING: Invalid environment item id '{}'", &map_object.id) diff --git a/src/map/crab.rs b/src/map/crab.rs index b26d13607a..4f9abf20f9 100644 --- a/src/map/crab.rs +++ b/src/map/crab.rs @@ -8,8 +8,8 @@ use macroquad::{ }; use crate::{ - player::Player, utils::timer::Timer, CollisionWorld, Drawable, PhysicsBody, PhysicsBodyParams, - Resources, + player::Player, utils::timer::Timer, Animation, CollisionWorld, Drawable, PhysicsBody, + PhysicsBodyParams, Resources, }; pub const CRAB_TEXTURE_ID: &str = "crab"; @@ -69,6 +69,15 @@ pub fn spawn_crab(world: &mut World, spawn_position: Vec2) -> Result { size.y as i32, ); + let crab_animations = &[Animation { + id: "idle".to_string(), + row: 0, + frames: 2, + fps: 2, + tweens: Default::default(), + is_looping: true, + }]; + Ok(world.spawn(( Crab { spawn_position, @@ -76,7 +85,12 @@ pub fn spawn_crab(world: &mut World, spawn_position: Vec2) -> Result { state_timer: Timer::new(1.0), }, Transform::from(spawn_position), - Drawable::new_sprite(DRAW_ORDER, CRAB_TEXTURE_ID, Default::default()), + Drawable::new_animated_sprite( + DRAW_ORDER, + CRAB_TEXTURE_ID, + crab_animations, + Default::default(), + ), PhysicsBody::new( actor, None, @@ -169,15 +183,24 @@ pub fn update_crabs(world: &mut World) { } // Apply any component modifications for the current state + let sprite = drawable.get_animated_sprite_mut().unwrap(); match &crab.state { - CrabState::Paused => body.velocity.x = 0.0, + CrabState::Paused => { + sprite.is_playing = true; + + body.velocity.x = 0.0; + } CrabState::Walking { left } => { - drawable.get_sprite_mut().unwrap().is_flipped_x = *left; + sprite.is_flipped_x = *left; + sprite.is_playing = false; + let direction = if *left { -1.0 } else { 1.0 }; let speed = direction * WALK_SPEED; body.velocity.x = speed; } CrabState::Running { scared_of } => { + sprite.is_playing = true; + let scary_pos = world.get::(*scared_of).unwrap().position; let direction = (pos.x - scary_pos.x).signum(); diff --git a/src/map/fish_school.rs b/src/map/fish_school.rs new file mode 100644 index 0000000000..17256f48c0 --- /dev/null +++ b/src/map/fish_school.rs @@ -0,0 +1,370 @@ +use core::Transform; + +use fishsticks::error::Result; +use hecs::{Entity, World}; +use macroquad::{ + prelude::{collections::storage, vec2, Color, Rect, Vec2}, + rand, +}; + +use crate::{ + player::Player, + utils::{ease::Ease, timer::Timer}, + AnimatedSpriteParams, Animation, Drawable, PhysicsBody, Resources, RigidBody, +}; + +/// The texture of the fish school icon ( used in the editor to represent a school of fish ) +pub const FISH_SCHOOL_ICON_TEXTURE_ID: &str = "fish_school_icon"; + +/// List of fish textures +const FISH_TEXTURE_IDS: &[&str] = &[ + "blue_tang", + "royal_gramma", + "arabian_angelfish", + "blue_green_chromis", + "banded_butterfly_fish", + // "small_fish1", + // "small_fish2", + // "small_fish3", +]; +/// The default and most-likely to ocurr number of fish in a school +const FISH_COUNT_BASE: u32 = 3; +/// The ammount greater or less than the base number of fish that may spawn +const FISH_COUNT_RANGE: u32 = 2; +/// The distance from the spawn point on each axis that the individual fish in the school will be +/// initially spawned within +const FISH_SPAWN_RANGE: f32 = 64.0; +/// The distance that the fish wish to stay within the center of their school +const TARGET_SCHOOL_SIZE: f32 = 100.0; + +/// Minimum draw order +const DRAW_ORDER_MIN: u32 = 0; +/// Maximum draw order +const DRAW_ORDER_MAX: u32 = 100; + +/// The color to debug draw school bounds +const GROUPED_SCHOOL_BOUNDS_DEBUG_DRAW_COLOR: Color = Color { + r: 0.0, + g: 1.0, + b: 0.0, + a: 1.0, +}; +/// The color to debug draw school bounds when the school is not grouped +const UNGROUPED_SCHOOL_BOUNDS_DEBUG_DRAW_COLOR: Color = Color { + r: 1.0, + g: 0.0, + b: 1.0, + a: 1.0, +}; + +pub struct FishSchool { + pub spawn_pos: Vec2, + pub fish_entities: Vec, +} + +pub struct Fish { + state: FishState, + state_timer: Timer, +} + +#[derive(Debug)] +pub enum FishState { + /// Moving to a location + Moving { from: Vec2, to: Vec2 }, +} + +pub fn spawn_fish_school(world: &mut World, spawn_position: Vec2) -> Result { + let resources = storage::get::(); + let fish_school_icon_sprite = resources.textures.get(FISH_SCHOOL_ICON_TEXTURE_ID).unwrap(); + let fish_school_icon_sprite_size = fish_school_icon_sprite.meta.frame_size.unwrap(); + + let rand_bool = || rand::gen_range(0u8, 2) == 0; + + let mut fish_count = FISH_COUNT_BASE as i32; + if rand_bool() { + let sign = if rand_bool() { -1 } else { 1 }; + for _ in 0..FISH_COUNT_RANGE { + if rand_bool() { + fish_count = fish_count.saturating_add(sign); + } + } + } + + let mut fish_school = FishSchool { + spawn_pos: spawn_position + fish_school_icon_sprite_size / 2.0, + fish_entities: Vec::with_capacity(fish_count as usize), + }; + let fish_school_entity = world.reserve_entity(); + + let fish_spawn_min = spawn_position; + let fish_spawn_max = spawn_position + Vec2::splat(FISH_SPAWN_RANGE); + + for _ in 0..fish_count { + let spawn_point = vec2( + rand::gen_range(fish_spawn_min.x, fish_spawn_max.x), + rand::gen_range(fish_spawn_min.y, fish_spawn_max.y), + ); + + let texture_index = rand::gen_range(0, FISH_TEXTURE_IDS.len()); + let texture_id = FISH_TEXTURE_IDS[texture_index]; + + let fish_entity = world.spawn(( + Fish { + state: FishState::Moving { + from: spawn_point, + to: spawn_point, + }, + state_timer: Timer::new(rand::gen_range(0.2, 1.0)), + }, + Transform::from(spawn_point), + Drawable::new_animated_sprite( + rand::gen_range(DRAW_ORDER_MIN, DRAW_ORDER_MAX + 1), + texture_id, + &[Animation { + id: "default".to_string(), + row: 0, + frames: 4, + fps: 3, + tweens: Default::default(), + is_looping: true, + }], + AnimatedSpriteParams { + is_flipped_x: rand_bool(), + ..Default::default() + }, + ), + )); + + fish_school.fish_entities.push(fish_entity); + } + + world.insert_one(fish_school_entity, fish_school).unwrap(); + + Ok(fish_school_entity) +} + +pub fn update_fish_schools(world: &mut World) { + let mut schools = Vec::new(); + + for (_, school) in world.query::<&FishSchool>().iter() { + let school: &FishSchool = school; + + let school_info = if let Some(info) = get_school_info(world, school) { + info + } else { + continue; + }; + + schools.push(school_info); + } + + for school in &schools { + let school: &SchoolInfo = school; + + let mut collider_rects = world + .query::<(&Transform, &PhysicsBody)>() + .with::() + .iter() + .map(|(_, (transform, body))| body.as_rect(transform.position)) + .collect::>(); + + collider_rects.extend( + world + .query::<(&Transform, &RigidBody)>() + .iter() + .map(|(_, (transform, body))| body.as_rect(transform.position)), + ); + + for fish_entity in &school.fish_entities { + let (mut fish, drawable, transform) = world + .query_one_mut::<(&mut Fish, &mut Drawable, &mut Transform)>(*fish_entity) + .unwrap(); + let sprite = drawable.get_animated_sprite_mut().unwrap(); + let pos: &mut Vec2 = &mut transform.position; + let padding = 20.0; + let rect = Rect::new( + pos.x - padding / 2.0, + pos.y - padding / 2.0, + sprite.texture.width() + padding, + sprite.texture.height() + padding, + ); + + let mut collides_with = None; + for body in &collider_rects { + if rect.overlaps(body) { + collides_with = Some(body); + break; + } + } + + let rand_bool = || rand::gen_range(0u8, 2) > 0; + let rand_delay = |min, max| Timer::new(rand::gen_range(min, max)); + + let pick_next_move = || { + if !school.is_grouped { + let target_point = pos.lerp(school.center, rand::gen_range(0.1, 0.4)); + + ( + FishState::Moving { + from: *pos, + to: target_point, + }, + rand_delay(0.2, 0.7), + ) + } else if rand_bool() { + let target_point = vec2( + pos.x + rand::gen_range(-20.0, 20.0), + pos.y + rand::gen_range(-20.0, 20.0), + ); + ( + FishState::Moving { + from: *pos, + to: target_point, + }, + rand_delay(0.5, 1.5), + ) + } else { + let target_point = pos.lerp(school.spawn_pos, rand::gen_range(0.10, 0.25)); + ( + FishState::Moving { + from: *pos, + to: target_point, + }, + rand_delay(0.5, 1.5), + ) + } + }; + + fish.state_timer.tick_frame_time(); + + if let Some(collision_rect) = collides_with { + let collision_center = collision_rect.point() + collision_rect.size() / 2.0; + let diff = *pos - collision_center; + fish.state = FishState::Moving { + from: *pos, + to: *pos + diff.normalize() * rand::gen_range(30.0, 60.0), + }; + fish.state_timer = rand_delay(0.2, 0.6); + + // We tick the timer an extra time here to make sure that the fish gets moving + // immediately without waiting for an extra frame, because if we keep colliding we + // may just keep re-setting the timer and the fish get's stuck until it stops + // colliding. + fish.state_timer.tick_frame_time(); + } else if fish.state_timer.has_finished() { + let (state, timer) = pick_next_move(); + fish.state = state; + fish.state_timer = timer; + } + + match &fish.state { + FishState::Moving { from, to } => { + let lerp_progress = Ease { + ease_in: false, + ease_out: true, + function: crate::utils::ease::EaseFunction::Sinusoidial, + progress: fish.state_timer.progress(), + } + .output(); + + sprite.is_flipped_x = from.x > to.x; + sprite.current_frame = + (sprite.animations[0].frames as f32 * lerp_progress).floor() as u32; + + *pos = from.lerp(*to, lerp_progress); + } + } + } + } +} + +pub fn debug_draw_fish_schools(world: &mut World) { + for (_, school) in world.query::<&FishSchool>().iter() { + let school: &FishSchool = school; + let school = if let Some(info) = get_school_info(world, school) { + info + } else { + continue; + }; + + macroquad::shapes::draw_rectangle_lines( + school.bounds_rect.x, + school.bounds_rect.y, + school.bounds_rect.w, + school.bounds_rect.h, + 1.0, + if school.is_grouped { + GROUPED_SCHOOL_BOUNDS_DEBUG_DRAW_COLOR + } else { + UNGROUPED_SCHOOL_BOUNDS_DEBUG_DRAW_COLOR + }, + ); + + macroquad::shapes::draw_circle_lines( + school.center.x, + school.center.y, + 2.0, + 1.0, + if school.is_grouped { + GROUPED_SCHOOL_BOUNDS_DEBUG_DRAW_COLOR + } else { + UNGROUPED_SCHOOL_BOUNDS_DEBUG_DRAW_COLOR + }, + ); + } +} + +#[derive(Clone)] +struct SchoolInfo { + spawn_pos: Vec2, + center: Vec2, + bounds_rect: Rect, + is_grouped: bool, + fish_entities: Vec, +} + +fn get_school_info(world: &World, school: &FishSchool) -> Option { + let resources = storage::get::(); + let fish_sprite = resources.textures.get(FISH_TEXTURE_IDS[0]).unwrap(); + let fish_size = fish_sprite.meta.frame_size.unwrap(); + + let fish_transforms = school + .fish_entities + .iter() + .map(|&x| world.get::(x).unwrap()) + .collect::>(); + + if fish_transforms.is_empty() { + return None; + } + + let mut school_bounds_min = fish_transforms[0].position; + let mut school_bounds_max = fish_transforms[0].position; + + for transform in &fish_transforms { + let pos: &Vec2 = &transform.position; + + school_bounds_min.x = school_bounds_min.x.min(pos.x); + school_bounds_min.y = school_bounds_min.y.min(pos.y); + school_bounds_max.x = school_bounds_max.x.max(pos.x + fish_size.x); + school_bounds_max.y = school_bounds_max.y.max(pos.y + fish_size.y); + } + let bounds_rect = Rect::new( + school_bounds_min.x, + school_bounds_min.y, + school_bounds_max.x - school_bounds_min.x, + school_bounds_max.y - school_bounds_min.y, + ); + + let size = school_bounds_max - school_bounds_min; + let center = school_bounds_min + size / 2.0; + let is_grouped = size.x.max(size.y) < TARGET_SCHOOL_SIZE; + + Some(SchoolInfo { + spawn_pos: school.spawn_pos, + center, + bounds_rect, + is_grouped, + fish_entities: school.fish_entities.clone(), + }) +} diff --git a/src/map/mod.rs b/src/map/mod.rs index 648da58fc9..687d20339c 100644 --- a/src/map/mod.rs +++ b/src/map/mod.rs @@ -6,10 +6,12 @@ use serde::{Deserialize, Serialize}; mod crab; mod decoration; +mod fish_school; mod sproinger; pub use crab::*; pub use decoration::*; +pub use fish_school::*; pub use sproinger::*; use core::math::URect; diff --git a/src/utils/ease.rs b/src/utils/ease.rs new file mode 100644 index 0000000000..6506cbf876 --- /dev/null +++ b/src/utils/ease.rs @@ -0,0 +1,68 @@ +use std::f32::consts::PI; + +/// Simple easing calculator +pub struct Ease { + pub ease_in: bool, + pub ease_out: bool, + pub function: EaseFunction, + pub progress: f32, +} + +pub enum EaseFunction { + Quadratic, + Cubic, + Sinusoidial, +} + +impl Default for Ease { + fn default() -> Self { + Self { + ease_in: true, + ease_out: true, + function: EaseFunction::Quadratic, + progress: 0.0, + } + } +} + +impl Ease { + pub fn output(&self) -> f32 { + let mut k = self.progress; + + // Reference for easing functions: + // https://echarts.apache.org/examples/en/editor.html?c=line-easing&lang=ts + // + // TODO: Add tests to make sure easings are correct + match (&self.function, self.ease_in, self.ease_out) { + (EaseFunction::Quadratic, true, true) => { + k *= 2.0; + if k < 1.0 { + 0.5 * k * k + } else { + k -= 1.0; + -0.5 * (k * (k - 2.0) - 1.0) + } + } + (EaseFunction::Quadratic, true, false) => k * k, + (EaseFunction::Quadratic, false, true) => k * (2.0 - k), + (EaseFunction::Cubic, true, true) => { + k *= 2.0; + if k < 1.0 { + 0.5 * k * k * k + } else { + k -= 2.0; + 0.5 * (k * k * k + 2.0) + } + } + (EaseFunction::Cubic, true, false) => k * k * k, + (EaseFunction::Cubic, false, true) => { + k -= 1.0; + k * k * k + 1.0 + } + (EaseFunction::Sinusoidial, true, true) => 0.5 * (1.0 - f32::cos(PI * k)), + (EaseFunction::Sinusoidial, true, false) => 1.0 - f32::cos((k * PI) / 2.0), + (EaseFunction::Sinusoidial, false, true) => f32::sin((k * PI) / 2.0), + (_, false, false) => k, + } + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 1227065ce2..e5224f4c09 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1 +1,2 @@ +pub mod ease; pub mod timer; diff --git a/src/utils/timer.rs b/src/utils/timer.rs index 6f0928984a..a1c4977f74 100644 --- a/src/utils/timer.rs +++ b/src/utils/timer.rs @@ -24,6 +24,11 @@ impl Timer { self.elapsed } + /// Return the percentage completion of the timer as a number between 0 and 1 + pub fn progress(&self) -> f32 { + self.elapsed / self.duration + } + /// Get whether or not the timer has finished pub fn has_finished(&self) -> bool { self.elapsed > self.duration