diff --git a/assets/img/character/stunned.json b/assets/img/character/stunned.json new file mode 100644 index 0000000..5d29dfc --- /dev/null +++ b/assets/img/character/stunned.json @@ -0,0 +1,50 @@ +{ "frames": { + "walk1 0.png": { + "frame": { "x": 0, "y": 0, "w": 12, "h": 22 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 12, "h": 22 }, + "sourceSize": { "w": 12, "h": 22 }, + "duration": 200 + }, + "walk1 1.png": { + "frame": { "x": 12, "y": 0, "w": 12, "h": 22 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 12, "h": 22 }, + "sourceSize": { "w": 12, "h": 22 }, + "duration": 200 + }, + "walk1 2.png": { + "frame": { "x": 24, "y": 0, "w": 12, "h": 22 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 12, "h": 22 }, + "sourceSize": { "w": 12, "h": 22 }, + "duration": 200 + }, + "walk1 3.png": { + "frame": { "x": 36, "y": 0, "w": 12, "h": 22 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 12, "h": 22 }, + "sourceSize": { "w": 12, "h": 22 }, + "duration": 200 + } + }, + "meta": { + "app": "http://www.aseprite.org/", + "version": "1.2.27-x64", + "image": "stunned.png", + "format": "RGBA8888", + "size": { "w": 48, "h": 22 }, + "scale": "1", + "frameTags": [ + ], + "layers": [ + { "name": "Layer", "opacity": 255, "blendMode": "normal" } + ], + "slices": [ + ] + } +} diff --git a/assets/img/character/stunned.png b/assets/img/character/stunned.png new file mode 100644 index 0000000..986e8f0 Binary files /dev/null and b/assets/img/character/stunned.png differ diff --git a/assets/img/map/darkness.png b/assets/img/map/darkness.png new file mode 100644 index 0000000..33450f0 Binary files /dev/null and b/assets/img/map/darkness.png differ diff --git a/assets/worlds/mainworld.json b/assets/worlds/mainworld.json index 40d085e..add0aa3 100644 --- a/assets/worlds/mainworld.json +++ b/assets/worlds/mainworld.json @@ -1,7 +1,7 @@ { "end_position": { - "x": 10000.0, - "y": 10000.0 + "x": 350.0, + "y": 2100.0 }, "player_spawn": { "x": 220.0, @@ -9,5 +9,25 @@ }, "fish": [ {"x":49,"y":801},{"x":570,"y":594},{"x":761,"y":186},{"x":760,"y":940},{"x":241,"y":32},{"x":501,"y":18},{"x":487,"y":37},{"x":802,"y":849},{"x":864,"y":43},{"x":544,"y":886},{"x":987,"y":710},{"x":949,"y":404},{"x":694,"y":32},{"x":364,"y":899},{"x":26,"y":849},{"x":253,"y":627},{"x":39,"y":547},{"x":307,"y":730},{"x":133,"y":967},{"x":861,"y":76},{"x":199,"y":229},{"x":617,"y":532},{"x":391,"y":388},{"x":491,"y":816},{"x":539,"y":243},{"x":222,"y":288},{"x":81,"y":784},{"x":432,"y":830},{"x":741,"y":737},{"x":426,"y":480},{"x":591,"y":437},{"x":903,"y":380},{"x":653,"y":349},{"x":684,"y":235},{"x":797,"y":438},{"x":546,"y":615},{"x":497,"y":523},{"x":406,"y":468},{"x":173,"y":183},{"x":641,"y":187},{"x":517,"y":294},{"x":527,"y":650},{"x":962,"y":237},{"x":25,"y":868},{"x":16,"y":369},{"x":434,"y":712},{"x":632,"y":315},{"x":172,"y":421},{"x":450,"y":53},{"x":731,"y":220},{"x":532,"y":467},{"x":816,"y":497},{"x":948,"y":539},{"x":467,"y":829},{"x":533,"y":809},{"x":146,"y":989},{"x":850,"y":245},{"x":989,"y":214},{"x":203,"y":354},{"x":466,"y":611},{"x":382,"y":376},{"x":111,"y":148},{"x":411,"y":77},{"x":124,"y":418},{"x":154,"y":611},{"x":56,"y":732},{"x":800,"y":488},{"x":851,"y":668},{"x":240,"y":220},{"x":1000,"y":62},{"x":95,"y":784},{"x":700,"y":428},{"x":735,"y":517},{"x":259,"y":843},{"x":647,"y":268},{"x":668,"y":823},{"x":198,"y":241},{"x":243,"y":422},{"x":838,"y":433},{"x":642,"y":97},{"x":563,"y":974},{"x":386,"y":548},{"x":646,"y":482},{"x":691,"y":794},{"x":167,"y":485},{"x":978,"y":622},{"x":845,"y":431},{"x":529,"y":719},{"x":963,"y":145},{"x":6,"y":412},{"x":381,"y":830},{"x":918,"y":118},{"x":915,"y":27},{"x":618,"y":262},{"x":250,"y":635},{"x":100,"y":500},{"x":442,"y":321},{"x":769,"y":767},{"x":714,"y":204},{"x":506,"y":872},{"x":575,"y":178},{"x":256,"y":411},{"x":921,"y":617},{"x":971,"y":214},{"x":726,"y":702},{"x":103,"y":450},{"x":501,"y":134},{"x":265,"y":993},{"x":31,"y":63},{"x":502,"y":448},{"x":46,"y":457},{"x":809,"y":184},{"x":763,"y":962},{"x":632,"y":873},{"x":916,"y":761},{"x":710,"y":720},{"x":873,"y":222},{"x":256,"y":861},{"x":246,"y":482},{"x":390,"y":812},{"x":28,"y":247},{"x":516,"y":523},{"x":869,"y":43},{"x":680,"y":740},{"x":406,"y":65},{"x":657,"y":196},{"x":692,"y":635},{"x":97,"y":993},{"x":616,"y":490},{"x":515,"y":955},{"x":412,"y":502},{"x":743,"y":565},{"x":16,"y":499},{"x":324,"y":582},{"x":871,"y":62},{"x":128,"y":476},{"x":716,"y":525},{"x":627,"y":2},{"x":730,"y":913},{"x":704,"y":522},{"x":242,"y":934},{"x":172,"y":277},{"x":651,"y":948},{"x":349,"y":263},{"x":731,"y":967},{"x":382,"y":762},{"x":217,"y":15},{"x":49,"y":25},{"x":583,"y":110},{"x":700,"y":620},{"x":230,"y":537},{"x":285,"y":978},{"x":4,"y":791},{"x":939,"y":866},{"x":371,"y":342},{"x":759,"y":870},{"x":892,"y":103},{"x":57,"y":129},{"x":233,"y":383},{"x":171,"y":472},{"x":173,"y":842},{"x":516,"y":464},{"x":407,"y":458},{"x":963,"y":231},{"x":526,"y":253},{"x":815,"y":857},{"x":175,"y":909},{"x":993,"y":255},{"x":129,"y":390},{"x":76,"y":997},{"x":833,"y":174},{"x":501,"y":396},{"x":897,"y":218},{"x":876,"y":601},{"x":41,"y":165},{"x":993,"y":473},{"x":606,"y":308},{"x":831,"y":382},{"x":517,"y":828},{"x":984,"y":26},{"x":286,"y":712},{"x":422,"y":311},{"x":448,"y":103},{"x":260,"y":229},{"x":5,"y":738},{"x":283,"y":346},{"x":744,"y":463},{"x":634,"y":719},{"x":446,"y":977},{"x":220,"y":89},{"x":745,"y":866},{"x":851,"y":860},{"x":369,"y":940},{"x":828,"y":577},{"x":350,"y":337},{"x":334,"y":378},{"x":203,"y":248},{"x":665,"y":788},{"x":334,"y":927},{"x":307,"y":764},{"x":500,"y":763},{"x":613,"y":843},{"x":384,"y":253},{"x":956,"y":569},{"x":846,"y":137},{"x":105,"y":728},{"x":686,"y":226},{"x":657,"y":52},{"x":592,"y":433},{"x":997,"y":820},{"x":746,"y":389},{"x":405,"y":448},{"x":973,"y":19},{"x":538,"y":518},{"x":790,"y":275},{"x":633,"y":738},{"x":128,"y":484},{"x":603,"y":371},{"x":932,"y":21},{"x":582,"y":445},{"x":438,"y":793},{"x":963,"y":69},{"x":158,"y":263},{"x":988,"y":297},{"x":249,"y":227},{"x":245,"y":466},{"x":131,"y":495},{"x":620,"y":266},{"x":505,"y":384},{"x":813,"y":647},{"x":113,"y":66},{"x":757,"y":10},{"x":2,"y":707},{"x":540,"y":140},{"x":562,"y":691},{"x":484,"y":433},{"x":859,"y":455},{"x":248,"y":117},{"x":36,"y":432},{"x":798,"y":754},{"x":611,"y":291},{"x":664,"y":770},{"x":299,"y":788},{"x":433,"y":920},{"x":540,"y":739},{"x":201,"y":829},{"x":972,"y":362},{"x":811,"y":120},{"x":941,"y":670},{"x":186,"y":448},{"x":549,"y":611},{"x":206,"y":387},{"x":973,"y":437},{"x":700,"y":709},{"x":472,"y":243},{"x":971,"y":518},{"x":184,"y":540},{"x":271,"y":257},{"x":290,"y":895},{"x":546,"y":7},{"x":256,"y":542},{"x":418,"y":553},{"x":816,"y":875},{"x":908,"y":547},{"x":315,"y":354},{"x":266,"y":471},{"x":242,"y":88},{"x":785,"y":52},{"x":497,"y":47},{"x":466,"y":279},{"x":750,"y":690},{"x":329,"y":296},{"x":545,"y":715},{"x":508,"y":562},{"x":993,"y":467},{"x":703,"y":733},{"x":824,"y":11},{"x":419,"y":337},{"x":393,"y":229},{"x":898,"y":261},{"x":264,"y":708},{"x":711,"y":768},{"x":568,"y":409},{"x":473,"y":342},{"x":329,"y":53},{"x":95,"y":815},{"x":783,"y":977},{"x":48,"y":551},{"x":635,"y":931},{"x":653,"y":86},{"x":9,"y":153},{"x":955,"y":660},{"x":480,"y":716},{"x":936,"y":622},{"x":607,"y":221},{"x":423,"y":545},{"x":507,"y":668},{"x":676,"y":957},{"x":253,"y":515},{"x":327,"y":495},{"x":965,"y":808},{"x":2,"y":807},{"x":276,"y":199},{"x":584,"y":75},{"x":770,"y":51},{"x":667,"y":717},{"x":944,"y":913},{"x":982,"y":977},{"x":618,"y":482},{"x":372,"y":545},{"x":507,"y":518},{"x":604,"y":492},{"x":772,"y":730},{"x":350,"y":141},{"x":783,"y":437},{"x":282,"y":714},{"x":269,"y":691},{"x":991,"y":386},{"x":234,"y":196},{"x":908,"y":635},{"x":785,"y":340},{"x":125,"y":712},{"x":466,"y":210},{"x":280,"y":185},{"x":995,"y":466},{"x":589,"y":258},{"x":700,"y":120},{"x":855,"y":323},{"x":690,"y":355},{"x":755,"y":353},{"x":378,"y":970},{"x":865,"y":270},{"x":220,"y":62},{"x":685,"y":848},{"x":670,"y":907},{"x":710,"y":671},{"x":209,"y":68},{"x":642,"y":470},{"x":104,"y":642},{"x":631,"y":328},{"x":898,"y":424},{"x":909,"y":427},{"x":189,"y":141},{"x":259,"y":993},{"x":332,"y":791},{"x":842,"y":778},{"x":63,"y":390},{"x":146,"y":895},{"x":230,"y":274},{"x":316,"y":447},{"x":603,"y":59},{"x":377,"y":841},{"x":602,"y":119},{"x":728,"y":557},{"x":395,"y":514},{"x":379,"y":754},{"x":822,"y":840},{"x":860,"y":478},{"x":695,"y":360},{"x":156,"y":784},{"x":241,"y":353},{"x":195,"y":199},{"x":284,"y":110},{"x":484,"y":966},{"x":889,"y":370},{"x":246,"y":684},{"x":710,"y":345},{"x":382,"y":635},{"x":447,"y":948},{"x":741,"y":274},{"x":224,"y":883},{"x":99,"y":37},{"x":472,"y":803},{"x":141,"y":397},{"x":371,"y":602},{"x":7,"y":482},{"x":184,"y":990},{"x":555,"y":313},{"x":573,"y":886},{"x":167,"y":365},{"x":810,"y":721},{"x":958,"y":767},{"x":891,"y":561},{"x":314,"y":987},{"x":156,"y":95},{"x":349,"y":542},{"x":775,"y":35},{"x":121,"y":655},{"x":311,"y":242},{"x":534,"y":135},{"x":71,"y":134},{"x":367,"y":896},{"x":447,"y":524},{"x":120,"y":421},{"x":878,"y":398},{"x":469,"y":822},{"x":483,"y":966},{"x":240,"y":880},{"x":759,"y":980},{"x":531,"y":759},{"x":395,"y":118},{"x":354,"y":360},{"x":173,"y":924},{"x":550,"y":958},{"x":888,"y":379},{"x":244,"y":448},{"x":999,"y":554},{"x":941,"y":455},{"x":798,"y":916},{"x":134,"y":123},{"x":90,"y":440},{"x":923,"y":263},{"x":405,"y":595},{"x":194,"y":387},{"x":370,"y":697},{"x":943,"y":888},{"x":607,"y":336},{"x":168,"y":105},{"x":874,"y":66},{"x":675,"y":50},{"x":601,"y":242},{"x":925,"y":728},{"x":643,"y":609},{"x":769,"y":713},{"x":410,"y":913},{"x":153,"y":776},{"x":775,"y":949},{"x":184,"y":93},{"x":624,"y":632},{"x":899,"y":804},{"x":909,"y":327},{"x":371,"y":510},{"x":663,"y":415},{"x":337,"y":542},{"x":248,"y":104},{"x":925,"y":450},{"x":310,"y":925},{"x":4,"y":550},{"x":559,"y":652},{"x":671,"y":296},{"x":414,"y":60},{"x":972,"y":505},{"x":221,"y":147},{"x":318,"y":592},{"x":861,"y":656},{"x":258,"y":675},{"x":565,"y":390},{"x":703,"y":236},{"x":227,"y":76},{"x":989,"y":252},{"x":924,"y":419},{"x":983,"y":971},{"x":795,"y":244},{"x":256,"y":498},{"x":517,"y":674},{"x":89,"y":197},{"x":366,"y":234},{"x":41,"y":952},{"x":487,"y":981},{"x":939,"y":922},{"x":384,"y":315},{"x":958,"y":57},{"x":499,"y":152},{"x":716,"y":167},{"x":167,"y":301},{"x":781,"y":964},{"x":101,"y":215},{"x":605,"y":396},{"x":31,"y":973},{"x":128,"y":831},{"x":685,"y":701},{"x":150,"y":507},{"x":663,"y":77},{"x":792,"y":561},{"x":398,"y":281},{"x":168,"y":936},{"x":8,"y":266},{"x":19,"y":723},{"x":377,"y":975},{"x":68,"y":114},{"x":191,"y":784},{"x":94,"y":222},{"x":986,"y":578},{"x":474,"y":160},{"x":936,"y":945},{"x":603,"y":778},{"x":105,"y":845},{"x":955,"y":583},{"x":832,"y":905},{"x":264,"y":132},{"x":219,"y":747},{"x":515,"y":562},{"x":178,"y":198},{"x":999,"y":1},{"x":470,"y":345},{"x":450,"y":490},{"x":967,"y":306},{"x":257,"y":360},{"x":632,"y":26},{"x":916,"y":382},{"x":631,"y":194},{"x":492,"y":235},{"x":479,"y":373},{"x":887,"y":154},{"x":65,"y":181},{"x":956,"y":879},{"x":567,"y":578},{"x":718,"y":617},{"x":464,"y":243},{"x":545,"y":410},{"x":923,"y":340},{"x":978,"y":716},{"x":277,"y":261},{"x":462,"y":600},{"x":687,"y":507} + ], + "jellyfish": [ + { + "position": { + "x": 300, + "y": 100 + } + } + ], + "octopus": [ + { + "position_a" : { + "x": 300, + "y": 150 + }, + "position_b" : { + "x": 500, + "y": 100 + } + } ] } \ No newline at end of file diff --git a/src/entities/enemy/base.rs b/src/entities/enemy/base.rs new file mode 100644 index 0000000..ddddc49 --- /dev/null +++ b/src/entities/enemy/base.rs @@ -0,0 +1,15 @@ +use raylib::prelude::*; + +use crate::{player::Player, resources::GlobalResources}; + +pub trait EnemyBase { + fn render( + &mut self, + context_2d: &mut RaylibMode2D, + player: &mut Player, + resources: &mut GlobalResources, + dt: f64, + ); + fn handle_logic(&mut self, player: &mut Player, dt: f64); + fn handle_getting_attacked(&mut self, stun_duration: f64, current_time: f64); +} diff --git a/src/entities/enemy/jellyfish.rs b/src/entities/enemy/jellyfish.rs new file mode 100644 index 0000000..7537fdc --- /dev/null +++ b/src/entities/enemy/jellyfish.rs @@ -0,0 +1,93 @@ +use super::base::EnemyBase; +use crate::{ + lib::utils::calculate_linear_slide, pallette::TRANSLUCENT_RED_64, player::Player, + resources::GlobalResources, +}; +use raylib::prelude::*; +use serde::{Deserialize, Serialize}; + +const JELLYFISH_STUN_DURATION: f64 = 0.75; +const JELLYFISH_STUN_REACH: f32 = 20.0; + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct JellyFish { + pub position: Vector2, + + #[serde(skip)] + pub stunned_timer: f64, + #[serde(skip)] + pub max_stunned_time: f64, + + #[serde(skip)] + pub do_stun_player: bool, +} + +impl JellyFish {} + +impl EnemyBase for JellyFish { + fn render( + &mut self, + context_2d: &mut raylib::prelude::RaylibMode2D, + player: &mut Player, + resources: &mut GlobalResources, + dt: f64, + ) { + let is_jelly_stunned = self.stunned_timer > 0.0; + + // Simple sine position + let v_trans = if is_jelly_stunned { + 0.0 + } else { + context_2d.get_time().sin() + }; + let trans_pose = Vector2 { + x: self.position.x, + y: self.position.y + (2.0 * v_trans as f32), + }; + + // Render the stun ring + if self.max_stunned_time > 0.0 && self.stunned_timer > 0.0 { + let stun_ring_alpha = + calculate_linear_slide(self.stunned_timer / self.max_stunned_time); + context_2d.draw_circle_v( + trans_pose, + JELLYFISH_STUN_REACH, + TRANSLUCENT_RED_64.fade(0.55 * stun_ring_alpha as f32), + ); + self.stunned_timer -= dt; + } + + // Render the jellyfish + resources + .jellyfish_animation_regular + .draw(context_2d, trans_pose, 0.0); + + // Only do stun loop if not stunned + if !is_jelly_stunned { + resources + .jellyfish_animation_attack + .draw(context_2d, trans_pose, 0.0); + } + + // Check if the jelly is in stun mode + self.do_stun_player = (resources + .jellyfish_animation_attack + .get_current_frame_id(context_2d) + == 13) + && !is_jelly_stunned; + } + + fn handle_logic(&mut self, player: &mut Player, dt: f64) { + // Handle stunning the player + if self.do_stun_player { + if self.position.distance_to(player.position).abs() <= JELLYFISH_STUN_REACH { + player.set_stun_seconds(JELLYFISH_STUN_DURATION); + } + } + } + + fn handle_getting_attacked(&mut self, stun_duration: f64, current_time: f64) { + self.stunned_timer = stun_duration; + self.max_stunned_time = stun_duration; + } +} diff --git a/src/entities/enemy/mod.rs b/src/entities/enemy/mod.rs new file mode 100644 index 0000000..1189f2b --- /dev/null +++ b/src/entities/enemy/mod.rs @@ -0,0 +1,3 @@ +pub mod base; +pub mod jellyfish; +pub mod octopus; \ No newline at end of file diff --git a/src/entities/enemy/octopus.rs b/src/entities/enemy/octopus.rs new file mode 100644 index 0000000..895d8c8 --- /dev/null +++ b/src/entities/enemy/octopus.rs @@ -0,0 +1,131 @@ +use crate::{lib::utils::calculate_linear_slide, pallette::{TRANSLUCENT_RED_64, TRANSLUCENT_WHITE_128, TRANSLUCENT_WHITE_64}, player::Player}; + +use super::base::EnemyBase; +use rand::{prelude::ThreadRng, Rng}; +use raylib::prelude::*; +use serde::{Deserialize, Serialize}; + +const OCTOPUS_SUCK_AIR_DELAY: f64 = 3.5; +const OCTOPUS_SUCK_AIR_RANGE: f32 = 70.0; +const OCTOPUS_SUCK_AIR_DURATION: f64 = 1.0; +const OCTOPUS_SUCK_AIR_AMOUNT: f32 = 0.1; +// const RNG: ThreadRng = rand::thread_rng(); + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +struct OctopusAirBubble { + position: Vector2, + speed: f32, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct Octopus { + pub position_a: Vector2, + pub position_b: Vector2, + + #[serde(skip)] + pub current_position: Vector2, + + #[serde(skip)] + pub stunned_timer: f64, + #[serde(skip)] + pub max_stunned_time: f64, + + #[serde(skip)] + pub suck_air_time_remaining: f64, + #[serde(skip)] + suck_air_bubbles: Vec, + #[serde(skip)] + has_taken_air_from_player: bool, +} + +impl Octopus {} + +impl EnemyBase for Octopus { + fn render( + &mut self, + context_2d: &mut raylib::prelude::RaylibMode2D, + player: &mut Player, + resources: &mut crate::resources::GlobalResources, + dt: f64, + ) { + let is_octopus_stunned = self.stunned_timer > 0.0; + + // Simple sine position + let h_trans = (context_2d.get_time() / 8.0).sin().abs() as f32; + + // Modify the current pose + let dist_a_to_b = self.position_b - self.position_a; + self.current_position = (dist_a_to_b * h_trans) + self.position_a; + + // Render the stun ring + if self.max_stunned_time > 0.0 && self.stunned_timer > 0.0 { + let stun_ring_alpha = + calculate_linear_slide(self.stunned_timer / self.max_stunned_time); + context_2d.draw_circle_v( + self.current_position, + 20.0, + TRANSLUCENT_RED_64.fade(0.55 * stun_ring_alpha as f32), + ); + self.stunned_timer -= dt; + } + + // Every once in a while, start sucking air + if (context_2d.get_time() % OCTOPUS_SUCK_AIR_DELAY) < 0.1 + && self.suck_air_time_remaining == 0.0 && !is_octopus_stunned + { + self.suck_air_time_remaining = OCTOPUS_SUCK_AIR_DURATION; + self.has_taken_air_from_player = false; + + // Spawn a few air bubbles if the player is in range + if player.position.distance_to(self.current_position).abs() <= OCTOPUS_SUCK_AIR_RANGE { + for _ in 0..5 { + self.suck_air_bubbles.push(OctopusAirBubble { + position: player.position, + speed: rand::thread_rng().gen_range(0.8..1.3), + }); + } + } + } + + // Handle sucking air bubble animation + if self.suck_air_time_remaining > 0.0 { + // Render and update all bubbles + for bubble in self.suck_air_bubbles.iter_mut() { + // Get the direction from the bubble to the octopus + let direction = (self.current_position - bubble.position).normalized(); + + // Render the bubble + context_2d.draw_circle_v(bubble.position, 2.0, TRANSLUCENT_WHITE_128); + + // Move the bubble + bubble.position += direction * bubble.speed; + } + + // Reduce time + self.suck_air_time_remaining = (self.suck_air_time_remaining - dt).max(0.0); + } else { + self.suck_air_bubbles.clear(); + } + + // TODO: TMP + context_2d.draw_circle_v(self.current_position, 10.0, Color::RED); + } + + fn handle_logic(&mut self, player: &mut crate::player::Player, dt: f64) { + if self.suck_air_time_remaining > 0.0 && !self.has_taken_air_from_player { + if player.position.distance_to(self.current_position).abs() <= OCTOPUS_SUCK_AIR_RANGE { + // Take air from the player + println!("Stealing"); + player.breath_percent -= OCTOPUS_SUCK_AIR_AMOUNT; + + // Set the flag + self.has_taken_air_from_player = true; + } + } + } + + fn handle_getting_attacked(&mut self, stun_duration: f64, current_time: f64) { + self.stunned_timer = stun_duration; + self.max_stunned_time = stun_duration; + } +} diff --git a/src/entities/mod.rs b/src/entities/mod.rs index e948307..5bf6e50 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1 +1,2 @@ -pub mod fish; \ No newline at end of file +pub mod fish; +pub mod enemy; \ No newline at end of file diff --git a/src/gamecore.rs b/src/gamecore.rs index d8216b7..19b9592 100644 --- a/src/gamecore.rs +++ b/src/gamecore.rs @@ -6,7 +6,11 @@ use raylib::{ camera::Camera2D, math::Vector2, prelude::RaylibDrawHandle, RaylibHandle, RaylibThread, }; -use crate::{items::ShopItems, player::Player, resources::GlobalResources, world::World}; +use crate::{ + player::{Player, PlayerInventory}, + resources::GlobalResources, + world::World, +}; use failure::Error; use log::debug; @@ -31,10 +35,10 @@ impl fmt::Display for GameState { #[derive(Debug, Serialize, Deserialize, Default)] pub struct GameProgress { - coins: u32, - max_depth: f32, - fastest_time: Option, - inventory: Vec, + pub coins: u32, + pub max_depth: f32, + pub fastest_time: Option, + pub inventory: PlayerInventory, } impl GameProgress { diff --git a/src/items.rs b/src/items.rs index b49d49b..547477e 100644 --- a/src/items.rs +++ b/src/items.rs @@ -1,10 +1,56 @@ -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -#[serde(tag = "t", content = "c")] -pub enum ShopItems { - StunGun(u8), - AirBag, - Flashlight(u8), - Flippers(u8) -} \ No newline at end of file +pub struct StunGun { + pub range: f32, + pub duration: f64, +} + +impl StunGun { + pub fn lvl1() -> Self { + Self { + range: 30.0, + duration: 0.75, + } + } + pub fn lvl2() -> Self { + Self { + range: 60.0, + duration: 1.25, + } + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct AirBag; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct Flashlight { + pub radius: f32 +} + +impl Flashlight { + pub fn lvl1() -> Self { + Self { + radius: 0.25 + } + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct Flippers { + pub speed_increase: f32, +} + +impl Flippers { + pub fn lvl1() -> Self { + Self { + speed_increase: 1.2 + } + } + pub fn lvl2() -> Self { + Self { + speed_increase: 1.5 + } + } +} diff --git a/src/lib/utils/mod.rs b/src/lib/utils/mod.rs index 9d6dd04..baba8af 100644 --- a/src/lib/utils/mod.rs +++ b/src/lib/utils/mod.rs @@ -1,2 +1,12 @@ pub mod profiler; -pub mod triangles; \ No newline at end of file +pub mod triangles; + +pub fn calculate_linear_slide(playthrough_percent: f64) -> f64 { + if playthrough_percent < 0.25 { + return playthrough_percent / 0.25; + } else if playthrough_percent > 0.75 { + return 1.0 - ((playthrough_percent - 0.75) / 0.25); + } else { + return 1.0; + } +} \ No newline at end of file diff --git a/src/lib/utils/profiler.rs b/src/lib/utils/profiler.rs index c813885..e8af3ea 100644 --- a/src/lib/utils/profiler.rs +++ b/src/lib/utils/profiler.rs @@ -1,3 +1,4 @@ +use raylib::math::Vector2; use serde_json::json; use serialstudio::{ data::{DataGroup, DataSet, TelemetryFrame}, @@ -21,7 +22,8 @@ pub struct ProfilerData { // Player pub player_coins: u32, pub player_boost_percent: f32, - pub player_breath_percent: f32 + pub player_breath_percent: f32, + pub player_pose: Vector2 } /// The development profiler @@ -147,6 +149,20 @@ impl GameProfiler { unit: Some("%".to_string()), w_type: None, }, + DataSet { + title: Some("X".to_string()), + value: json!(self.data.player_pose.x), + graph: Some(false), + unit: Some("pixels".to_string()), + w_type: None, + }, + DataSet { + title: Some("Y".to_string()), + value: json!(self.data.player_pose.y), + graph: Some(false), + unit: Some("pixels".to_string()), + w_type: None, + }, ], }, ], diff --git a/src/lib/wrappers/animation.rs b/src/lib/wrappers/animation.rs index 053cf2b..163b70b 100644 --- a/src/lib/wrappers/animation.rs +++ b/src/lib/wrappers/animation.rs @@ -2,7 +2,7 @@ use raylib::{core::color::Color, math::{Rectangle, Vector2}, prelude::{RaylibDra /// A wrapper around an animation spritesheet pub struct FrameAnimationWrapper { - sprite_sheet: Texture2D, + pub sprite_sheet: Texture2D, size: Vector2, frame_count: u32, frames_per_second: u8, diff --git a/src/logic/ingame/hud.rs b/src/logic/ingame/hud.rs index 6a70ff9..e7e508d 100644 --- a/src/logic/ingame/hud.rs +++ b/src/logic/ingame/hud.rs @@ -8,13 +8,7 @@ pub fn render_hud( window_center: Vector2, ) { // Get the relevant data - let dist_from_player_to_end = game_core - .player - .position - .distance_to(game_core.world.end_position); - let dist_from_start_to_end = Vector2::zero().distance_to(game_core.world.end_position); - let progress = ((dist_from_start_to_end - dist_from_player_to_end) / dist_from_start_to_end) - .clamp(0.0, 1.0); + let progress = game_core.player.calculate_depth_percent(&game_core.world); // Determine the progress slider position let slider_bound_height = 20.0; diff --git a/src/logic/ingame/mod.rs b/src/logic/ingame/mod.rs index 18ab910..3f2bce6 100644 --- a/src/logic/ingame/mod.rs +++ b/src/logic/ingame/mod.rs @@ -4,9 +4,10 @@ mod playerlogic; use raylib::prelude::*; use crate::{ + entities::enemy::base::EnemyBase, gamecore::{GameCore, GameState}, lib::wrappers::audio::player::AudioPlayer, - pallette::{SKY, WATER}, + pallette::{SKY, WATER, WATER_DARK}, }; use super::screen::Screen; @@ -31,6 +32,7 @@ impl InGameScreen { &mut self, context_2d: &mut RaylibMode2D, game_core: &mut GameCore, + dt: f64, ) { // Build source bounds let source_bounds = Rectangle { @@ -47,7 +49,21 @@ impl InGameScreen { }; // Clear the background - context_2d.draw_rectangle_rec(world_bounds, WATER); + context_2d.draw_rectangle_gradient_v( + world_bounds.x as i32, + world_bounds.y as i32, + world_bounds.width as i32, + world_bounds.height as i32, + WATER, + WATER_DARK, + ); + + // Render fish + let fish_clone = game_core.world.fish.clone(); + for fish in game_core.world.fish.iter_mut() { + fish.update_position(&mut game_core.player, dt, &fish_clone); + fish.render(context_2d); + } // Render the world texture context_2d.draw_texture_rec( @@ -68,13 +84,63 @@ impl InGameScreen { ) { // Render every collider for collider in game_core.world.colliders.iter() { - context_2d.draw_rectangle_lines_ex( - collider, - 1, - Color::RED, - ); + context_2d.draw_rectangle_lines_ex(collider, 1, Color::RED); } } + + fn render_darkness(&mut self, draw_handle: &mut RaylibDrawHandle, game_core: &mut GameCore) { + // Calculate the min view radius based on the current flashlight + let mut min_radius = 0.0; + if game_core.player.inventory.flashlight.is_some() { + min_radius = game_core + .player + .inventory + .flashlight + .as_ref() + .unwrap() + .radius; + } + + // Get the window center + let win_height = draw_handle.get_screen_height(); + let win_width = draw_handle.get_screen_width(); + + // Calculate the occusion radius based on depth + let radius = (1.0 + - (game_core.player.calculate_depth_percent(&game_core.world) * 1.3).clamp(0.0, 1.0)) + .max(min_radius); + + // Determine width and height scales + // This is clamped to make the rendering logic below easier by removing the need to overdraw + let width_scale = (5.0 * radius).max(0.5); + let height_scale = (5.0 * radius).max(0.5); + + // Get the base sizes of everything + let texture_width = game_core.resources.darkness_overlay.width as f32; + let texture_height = game_core.resources.darkness_overlay.height as f32; + let texture_width_scaled = texture_width * width_scale; + let texture_height_scaled = texture_height * height_scale; + + // Render the overlay + draw_handle.draw_texture_pro( + &game_core.resources.darkness_overlay, + Rectangle { + x: 0.0, + y: 0.0, + width: texture_width, + height: texture_height, + }, + Rectangle { + x: (win_width as f32 - texture_width_scaled) / 2.0, + y: (win_height as f32 - texture_height_scaled) / 2.0, + width: texture_width_scaled, + height: texture_height_scaled, + }, + Vector2 { x: 0.0, y: 0.0 }, + 0.0, + Color::WHITE, + ); + } } impl Screen for InGameScreen { @@ -113,22 +179,40 @@ impl Screen for InGameScreen { let mut context_2d = draw_handle.begin_mode2D(game_core.master_camera); // Render the world - self.render_world(&mut context_2d, game_core); - if game_core.show_simple_debug_info{ + self.render_world(&mut context_2d, game_core, dt); + if game_core.show_simple_debug_info { self.render_colliders(&mut context_2d, game_core); } // Render entities - let fish_clone = game_core.world.fish.clone(); - for fish in game_core.world.fish.iter_mut() { - fish.update_position(&mut game_core.player, dt, &fish_clone); - fish.render(&mut context_2d); + for jellyfish in game_core.world.jellyfish.iter_mut() { + jellyfish.handle_logic(&mut game_core.player, dt); + jellyfish.render( + &mut context_2d, + &mut game_core.player, + &mut game_core.resources, + dt, + ); + } + for octopus in game_core.world.octopus.iter_mut() { + octopus.handle_logic(&mut game_core.player, dt); + octopus.render( + &mut context_2d, + &mut game_core.player, + &mut game_core.resources, + dt, + ); } // Render Player - playerlogic::render_player(&mut context_2d, game_core); + game_core + .player + .render(&mut context_2d, &mut game_core.resources, dt); } + // Render the darkness layer + self.render_darkness(draw_handle, game_core); + // Render the hud hud::render_hud(draw_handle, game_core, window_center); diff --git a/src/logic/ingame/playerlogic.rs b/src/logic/ingame/playerlogic.rs index 6446ec5..a506daa 100644 --- a/src/logic/ingame/playerlogic.rs +++ b/src/logic/ingame/playerlogic.rs @@ -5,13 +5,13 @@ use crate::{ pallette::{TRANSLUCENT_WHITE_128, TRANSLUCENT_WHITE_64, TRANSLUCENT_WHITE_96}, }; -const NORMAL_PLAYER_SPEED: i32 = 3; +const NORMAL_PLAYER_SPEED: i32 = 1; const BOOST_PLAYER_SPEED: i32 = NORMAL_PLAYER_SPEED * 2; const CAMERA_FOLLOW_SPEED: f32 = 0.7; const TURN_SPEED: f32 = 0.15; const BOOST_DECREASE_PER_SECOND: f32 = 0.65; const BOOST_REGEN_PER_SECOND: f32 = 0.25; -const BREATH_DECREASE_PER_SECOND: f32 = 0.01; +const BREATH_DECREASE_PER_SECOND: f32 = 0.02; pub fn update_player_movement( draw_handle: &mut RaylibDrawHandle, @@ -65,7 +65,9 @@ pub fn update_player_movement( } // set angle - game_core.player.direction = Vector2::new(f32::cos(player_angle), f32::sin(player_angle)); + if !game_core.player.is_stunned() { + game_core.player.direction = Vector2::new(f32::cos(player_angle), f32::sin(player_angle)); + } // In the case the player is in "null", just jump the camera to them if game_core.player.position == Vector2::zero() { @@ -76,8 +78,12 @@ pub fn update_player_movement( let user_request_boost = draw_handle.is_mouse_button_down(MouseButton::MOUSE_LEFT_BUTTON); let user_request_action = draw_handle.is_mouse_button_pressed(MouseButton::MOUSE_RIGHT_BUTTON); + if user_request_action { + game_core.player.begin_attack(&mut game_core.world, draw_handle.get_time()); + } + // Move the player in their direction - let speed_multiplier; + let mut speed_multiplier; if user_request_boost && game_core.player.boost_percent >= 0.0 { // Set the speed multiplier speed_multiplier = BOOST_PLAYER_SPEED as f32; @@ -122,29 +128,58 @@ pub fn update_player_movement( } } + // Handle flippers doing a speed increase + if game_core.player.inventory.flippers.is_some() { + speed_multiplier = speed_multiplier + * game_core + .player + .inventory + .flippers + .as_ref() + .unwrap() + .speed_increase; + } + // Update the player's breath game_core.player.breath_percent = (game_core.player.breath_percent - BREATH_DECREASE_PER_SECOND * dt as f32).clamp(0.0, 1.0); // Only do this if the mouse is far enough away - let player_real_movement = game_core.player.direction * speed_multiplier; - if raw_movement_direction.distance_to(Vector2::zero()) > game_core.player.size.y / 2.0 { - game_core.player.is_moving = true; - game_core.player.position += player_real_movement; + let player_stunned = game_core.player.stun_timer > 0.0; + let mut player_real_movement = game_core.player.direction * speed_multiplier; + if raw_movement_direction.distance_to(Vector2::zero()) > game_core.player.size.y / 2.0 + && !game_core.player.is_stunned() + { + if game_core.player.is_moving { + // move in x + game_core.player.position.x += player_real_movement.x; - // Check for any collisions - for collider in game_core.world.colliders.iter() { - if game_core.player.collides_with_rec(collider) { - game_core.player.is_moving = false; - break; + // Check for any collisions + for collider in game_core.world.colliders.iter() { + if game_core.player.collides_with_rec(collider) { + game_core.player.position.x -= player_real_movement.x; + player_real_movement.x = 0.0; + break; + } + } + + // move in y + game_core.player.position.y += player_real_movement.y; + + // Check for any collisions + for collider in game_core.world.colliders.iter() { + if game_core.player.collides_with_rec(collider) { + game_core.player.position.y -= player_real_movement.y; + player_real_movement.y = 0.0; + break; + } } } + } - if !game_core.player.is_moving { - game_core.player.position -= player_real_movement; - } - } else { - game_core.player.is_moving = false; + // Handle updating the stun timer + if player_stunned { + game_core.player.stun_timer -= dt; } // Move the camera to follow the player @@ -168,61 +203,3 @@ pub fn update_player_movement( // game_core.master_camera.target.y = -100.0; // } } - -pub fn render_player(context_2d: &mut RaylibMode2D, game_core: &mut GameCore) { - // Get the player - let player = &game_core.player; - - // Convert the player direction to a rotation - let player_rotation = Vector2::zero().angle_to(player.direction); - - // Render the player's boost ring - // This functions both as a breath meter, and as a boost meter - let boost_ring_max_radius = player.size.x + 5.0; - context_2d.draw_circle( - player.position.x as i32, - player.position.y as i32, - boost_ring_max_radius * player.boost_percent, - TRANSLUCENT_WHITE_64, - ); - context_2d.draw_ring( - Vector2 { - x: player.position.x as i32 as f32, - y: player.position.y as i32 as f32, - }, - boost_ring_max_radius, - boost_ring_max_radius + 1.0, - 0, - (360.0 * player.breath_percent) as i32, - 0, - TRANSLUCENT_WHITE_96, - ); - - // Render the player based on what is happening - if player.is_boost_charging { - game_core.resources.player_animation_boost_charge.draw( - context_2d, - player.position, - player_rotation.to_degrees() - 90.0, - ); - } else if player.is_boosting { - game_core.resources.player_animation_boost.draw( - context_2d, - player.position, - player_rotation.to_degrees() - 90.0, - ); - } else if player.is_moving { - game_core.resources.player_animation_regular.draw( - context_2d, - player.position, - player_rotation.to_degrees() - 90.0, - ); - } else { - game_core.resources.player_animation_regular.draw_frame( - context_2d, - player.position, - player_rotation.to_degrees() - 90.0, - 0, - ); - } -} diff --git a/src/logic/loadingscreen.rs b/src/logic/loadingscreen.rs index 184322b..06056c1 100644 --- a/src/logic/loadingscreen.rs +++ b/src/logic/loadingscreen.rs @@ -1,9 +1,6 @@ use raylib::prelude::*; -use crate::{ - gamecore::{GameCore, GameState}, - lib::wrappers::audio::player::AudioPlayer, -}; +use crate::{gamecore::{GameCore, GameState}, lib::{utils::calculate_linear_slide, wrappers::audio::player::AudioPlayer}}; use super::screen::Screen; @@ -32,14 +29,7 @@ impl LoadingScreen { fn get_logo_mask(&self, playthrough_percent: f64) -> Color { // Determine the alpha - let alpha; - if playthrough_percent < 0.25 { - alpha = playthrough_percent / 0.25 - } else if playthrough_percent > 0.75 { - alpha = 1.0 - ((playthrough_percent - 0.75) / 0.25); - } else { - alpha = 1.0; - } + let alpha = calculate_linear_slide(playthrough_percent); // Build a color mask Color { diff --git a/src/logic/mainmenu.rs b/src/logic/mainmenu.rs index e3fc664..e03d098 100644 --- a/src/logic/mainmenu.rs +++ b/src/logic/mainmenu.rs @@ -1,9 +1,6 @@ use raylib::prelude::*; -use crate::{ - gamecore::{GameCore, GameState}, - lib::wrappers::audio::player::AudioPlayer, -}; +use crate::{gamecore::{GameCore, GameState}, lib::wrappers::audio::player::AudioPlayer, pallette::WATER_DARK}; use super::screen::Screen; @@ -34,40 +31,48 @@ impl Screen for MainMenuScreen { draw_handle.draw_text( "ONE BREATH", (win_height / 2) - 80, - win_width / 4, - 40, + win_width / 8, + 80, Color::BLACK, ); + // Get mouse position data + let mouse_position = draw_handle.get_mouse_position(); + let hovering_play_button = mouse_position.y > (win_width as f32 / 4.0) + && mouse_position.y < (win_width as f32 / 4.0) + 60.0; + let hovering_quit_button = mouse_position.y > (win_width as f32 / 4.0) + 100.0 + && mouse_position.y < (win_width as f32 / 4.0) + 160.0; + // Play and quit draw_handle.draw_text( "Play", - (win_height / 2) - 80, - (win_width / 4) + 100, - 20, - Color::BLACK, + (win_height / 2) + 120, + (win_width / 4), + 60, + match hovering_play_button { + true => Color::GREEN, + false => Color::BLACK, + }, ); draw_handle.draw_text( "Quit", - (win_height / 2) - 80, - (win_width / 4) + 140, - 20, - Color::BLACK, + (win_height / 2) + 130, + (win_width / 4) + 100, + 60, + match hovering_quit_button { + true => Color::GREEN, + false => Color::BLACK, + }, ); // Handle button presses - let mouse_position = draw_handle.get_mouse_position(); let mouse_clicked = draw_handle.is_mouse_button_pressed(MouseButton::MOUSE_LEFT_BUTTON); // Check clicks if mouse_clicked { - if mouse_position.y > (win_width as f32 / 4.0) + 100.0 - && mouse_position.y < (win_width as f32 / 4.0) + 120.0 - { + if hovering_play_button { return Some(GameState::InGame); - } else if mouse_position.y > (win_width as f32 / 4.0) + 140.0 - && mouse_position.y < (win_width as f32 / 4.0) + 180.0 - { + } else if hovering_quit_button { return Some(GameState::GameQuit); } } diff --git a/src/main.rs b/src/main.rs index 75a8769..aa48d4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -134,6 +134,7 @@ fn main() { profiler.data.player_coins = game_core.player.coins; profiler.data.player_boost_percent = game_core.player.boost_percent; profiler.data.player_breath_percent = game_core.player.breath_percent; + profiler.data.player_pose = game_core.player.position; // Send telemetry data profiler.update(); diff --git a/src/pallette.rs b/src/pallette.rs index 5d675ca..65f2243 100644 --- a/src/pallette.rs +++ b/src/pallette.rs @@ -33,4 +33,18 @@ pub const WATER: Color = Color { g: 66, b: 143, a: 255 +}; + +pub const WATER_DARK: Color = Color { + r: 8, + g: 24, + b: 54, + a: 255 +}; + +pub const TRANSLUCENT_RED_64: Color = Color { + r: 230, + g: 41, + b: 55, + a: 64, }; \ No newline at end of file diff --git a/src/player.rs b/src/player.rs index 01c5343..e7f367e 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,18 +1,51 @@ -use raylib::math::{Rectangle, Vector2}; +use crate::{ + entities::enemy::base::EnemyBase, + gamecore::{GameCore, GameProgress}, + items::{AirBag, Flashlight, Flippers, StunGun}, + lib::utils::calculate_linear_slide, + pallette::{TRANSLUCENT_WHITE_64, TRANSLUCENT_WHITE_96}, + resources::GlobalResources, + world::World, +}; +use raylib::prelude::*; +use serde::{Deserialize, Serialize}; -use crate::lib::utils::triangles::rotate_vector; +const AOE_RING_MAX_RADIUS: f32 = 60.0; +const STUN_ATTACK_TIME: f64 = 0.75; + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct PlayerInventory { + pub stun_gun: Option, + pub air_bag: Option, + pub flashlight: Option, + pub flippers: Option, +} + +impl PlayerInventory { + pub fn new() -> Self { + Self { + stun_gun: Some(StunGun::lvl1()), //TMP + flashlight: Some(Flashlight::lvl1()), //TMP + ..Default::default() + } + } +} #[derive(Debug, Default)] pub struct Player { pub position: Vector2, pub direction: Vector2, pub size: Vector2, + pub radius: f32, pub coins: u32, pub boost_percent: f32, pub breath_percent: f32, pub is_moving: bool, pub is_boosting: bool, pub is_boost_charging: bool, + pub inventory: PlayerInventory, + pub stun_timer: f64, + pub attacking_timer: f64, } impl Player { @@ -21,7 +54,10 @@ impl Player { boost_percent: 1.0, size: Vector2 { x: 11.0, y: 21.0 }, breath_percent: 1.0, + is_moving: true, + radius: 4.5, position: spawn.clone(), + inventory: PlayerInventory::new(), ..Default::default() } } @@ -54,6 +90,150 @@ impl Player { // || rectangle.check_collision_point_rec(top_right_corner) // || rectangle.check_collision_point_rec(bottom_left_corner); - return rectangle.check_collision_circle_rec(self.position, (self.size.y * 0.5) / 2.0); + return rectangle.check_collision_circle_rec(self.position, self.radius); + } + + /// Stun the player + pub fn set_stun_seconds(&mut self, seconds: f64) { + self.stun_timer = seconds; + } + + /// Try to attack with the stun gun + pub fn begin_attack(&mut self, world: &mut World, current_time: f64) { + if self.inventory.stun_gun.is_some() && !self.is_stunned() { + self.attacking_timer = self.inventory.stun_gun.as_ref().unwrap().duration; + + // Stun everything in reach + let stun_reach = self.inventory.stun_gun.as_ref().unwrap().range; + + for jellyfish in world.jellyfish.iter_mut() { + if jellyfish.position.distance_to(self.position).abs() <= stun_reach { + jellyfish.handle_getting_attacked(self.attacking_timer, current_time); + } + } + for octopus in world.octopus.iter_mut() { + if octopus.current_position.distance_to(self.position).abs() <= stun_reach { + octopus.handle_getting_attacked(self.attacking_timer, current_time); + } + } + } + } + + pub fn is_stun_gun_active(&self) -> bool { + return self.attacking_timer != 0.0 && self.inventory.stun_gun.is_some(); + } + + pub fn is_stunned(&self) -> bool { + return self.stun_timer > 0.0; + } + + /// Calculate how far the player is + pub fn calculate_depth_percent(&self, world: &World) -> f32 { + let dist_from_player_to_end = self.position.distance_to(world.end_position); + let dist_from_start_to_end = Vector2::zero().distance_to(world.end_position); + return ((dist_from_start_to_end - dist_from_player_to_end) / dist_from_start_to_end) + .clamp(0.0, 1.0); + } + + /// Create GameProgress from the current life + pub fn create_statistics(&self, game_core: &GameCore, current_time: f64) -> GameProgress { + GameProgress { + coins: self.coins, + inventory: self.inventory.clone(), + max_depth: self.calculate_depth_percent(&game_core.world), + fastest_time: Some(current_time - game_core.last_state_change_time), + } + } + + /// Render the player + pub fn render( + &mut self, + context_2d: &mut RaylibMode2D, + resources: &mut GlobalResources, + dt: f64, + ) { + // Convert the player direction to a rotation + let player_rotation = Vector2::zero().angle_to(self.direction); + + // Render the player's boost ring + // This functions both as a breath meter, and as a boost meter + let boost_ring_max_radius = self.size.x + 5.0; + context_2d.draw_circle( + self.position.x as i32, + self.position.y as i32, + boost_ring_max_radius * self.boost_percent, + TRANSLUCENT_WHITE_64, + ); + context_2d.draw_ring( + Vector2 { + x: self.position.x as i32 as f32, + y: self.position.y as i32 as f32, + }, + boost_ring_max_radius, + boost_ring_max_radius + 1.0, + 0, + (360.0 * self.breath_percent) as i32, + 0, + TRANSLUCENT_WHITE_96, + ); + + // Calculate AOE ring + if self.is_stun_gun_active() { + let animation_progression = + self.attacking_timer / self.inventory.stun_gun.as_ref().unwrap().duration; + let aoe_ring = calculate_linear_slide(animation_progression) as f32; + self.attacking_timer = (self.attacking_timer - dt).max(0.0); + + // Render attack AOE + if animation_progression >= 0.5 { + context_2d.draw_circle_lines( + self.position.x as i32, + self.position.y as i32, + self.inventory.stun_gun.as_ref().unwrap().range * aoe_ring, + TRANSLUCENT_WHITE_64.fade(aoe_ring), + ); + } else { + context_2d.draw_circle_lines( + self.position.x as i32, + self.position.y as i32, + self.inventory.stun_gun.as_ref().unwrap().range, + TRANSLUCENT_WHITE_64.fade(aoe_ring), + ); + } + } + + // Render the player based on what is happening + if self.is_stunned() { + resources.player_animation_stunned.draw( + context_2d, + self.position, + player_rotation.to_degrees() - 90.0, + ); + } else if self.is_boost_charging { + resources.player_animation_boost_charge.draw( + context_2d, + self.position, + player_rotation.to_degrees() - 90.0, + ); + } else if self.is_boosting { + resources.player_animation_boost.draw( + context_2d, + self.position, + player_rotation.to_degrees() - 90.0, + ); + } else if self.is_moving { + resources.player_animation_regular.draw( + context_2d, + self.position, + player_rotation.to_degrees() - 90.0, + ); + } else { + resources.player_animation_regular.draw_frame( + context_2d, + self.position, + player_rotation.to_degrees() - 90.0, + 0, + ); + } } } diff --git a/src/resources.rs b/src/resources.rs index b88c5a4..03cd78c 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -16,9 +16,17 @@ pub struct GlobalResources { pub player_animation_regular: FrameAnimationWrapper, pub player_animation_boost_charge: FrameAnimationWrapper, pub player_animation_boost: FrameAnimationWrapper, + pub player_animation_stunned: FrameAnimationWrapper, // Cave - pub cave_mid_layer: Texture2D + pub cave_mid_layer: Texture2D, + + // Enemies + pub jellyfish_animation_regular: FrameAnimationWrapper, + pub jellyfish_animation_attack: FrameAnimationWrapper, + + // Darkness layer + pub darkness_overlay: Texture2D } impl GlobalResources { @@ -59,10 +67,41 @@ impl GlobalResources { 21, 30, ), + player_animation_stunned: FrameAnimationWrapper::new( + raylib.load_texture_from_image( + &thread, + &Image::load_image("./assets/img/character/stunned.png")?, + )?, + Vector2 { x: 12.0, y: 22.0 }, + 4, + 100 / 8, + ), cave_mid_layer: raylib.load_texture_from_image( &thread, &Image::load_image("./assets/img/map/cave.png")?, )?, + jellyfish_animation_regular: FrameAnimationWrapper::new( + raylib.load_texture_from_image( + &thread, + &Image::load_image("./assets/img/enemies/jelly.png")?, + )?, + Vector2 { x: 10.0, y: 10.0 }, + 6, + 4, + ), + jellyfish_animation_attack: FrameAnimationWrapper::new( + raylib.load_texture_from_image( + &thread, + &Image::load_image("./assets/img/enemies/jellyAttack.png")?, + )?, + Vector2 { x: 20.0, y: 20.0 }, + 15, + 4, + ), + darkness_overlay: raylib.load_texture_from_image( + &thread, + &Image::load_image("./assets/img/map/darkness.png")?, + )?, }) } } diff --git a/src/world.rs b/src/world.rs index 1f344fe..b4237c4 100644 --- a/src/world.rs +++ b/src/world.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use std::io::Read; use failure::Error; -use crate::entities::fish::FishEntity; +use crate::entities::{enemy::{jellyfish::JellyFish, octopus::Octopus}, fish::FishEntity}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct World { @@ -19,7 +19,11 @@ pub struct World { pub fish: Vec, #[serde(skip)] - pub colliders: Vec + pub colliders: Vec, + + pub jellyfish: Vec, + pub octopus: Vec, + } impl World {