From 32a62cfcb925225359a5c90761b795f2f737ebd2 Mon Sep 17 00:00:00 2001 From: LLLL Colonq Date: Tue, 18 Aug 2026 00:08:28 -0400 Subject: Re-populate games --- 2026/games/asrael_io/source/src/enemy.rs | 174 +++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 2026/games/asrael_io/source/src/enemy.rs (limited to '2026/games/asrael_io/source/src/enemy.rs') diff --git a/2026/games/asrael_io/source/src/enemy.rs b/2026/games/asrael_io/source/src/enemy.rs new file mode 100644 index 0000000..46eabc0 --- /dev/null +++ b/2026/games/asrael_io/source/src/enemy.rs @@ -0,0 +1,174 @@ +use crate::color::Palette; +use crate::color::db32::{LIGHT_RED, PINK, PURPLE}; +use crate::gfx; +use crate::math; +use crate::rng::Rng; +use crate::sprite::Sprite; +use crate::{GAME_H, GAME_W}; + +use std::f32::consts::TAU; + +use aseprite::AsepriteFile; +use glam::Vec2; + +const ROT_STEPS: f32 = 16.0; + +#[derive(Default)] +enum State { + Dive { + path: [Vec2; 4], + fired: u32, + shots: u32, + t: f32, + }, + #[default] + Formation, +} + +pub struct Enemy { + base: Vec2, + flash: u32, + hp: u32, + pos: Vec2, + step: Vec2, + sprite: Sprite, + state: State, +} + +impl Enemy { + pub fn new(sprites: &AsepriteFile, layer: &str, base: Vec2, hp: u32) -> Self { + let pos = base; + let step = Vec2::ZERO; + let sprite = Sprite::from_ase(sprites, layer); + let state = State::default(); + + Self { + base, + flash: 0, + hp, + pos, + step, + sprite, + state, + } + } + + pub fn damage(&mut self) -> bool { + self.hp = self.hp.saturating_sub(1); + self.flash = 12; + self.hp == 0 + } + + pub fn center(&self) -> Vec2 { + self.pos + self.size() / 2.0 + } + + pub fn pos(&self) -> Vec2 { + self.pos + } + + pub fn size(&self) -> Vec2 { + self.sprite.size.as_vec2() + } + + pub fn start_dive(&mut self, player_x: f32, shots: u32, rng: &mut Rng) { + if matches!(self.state, State::Dive { .. }) { + return; + } + + let side = if self.pos.x < 70.0 { + 1.0 + } else if self.pos.x > GAME_W as f32 - 70.0 { + -1.0 + } else if rng.chance(0.5) { + 1.0 + } else { + -1.0 + }; + + let min = Vec2::new(4.0, 4.0); + let max = Vec2::new(GAME_W as f32 - 20.0, GAME_H as f32 - 20.0); + let swing = (self.pos + Vec2::new(side * 70.0, -40.0)).clamp(min, max); + let plunge = Vec2::new(player_x, GAME_H as f32 - 20.0).clamp(min, max); + + let path = [self.pos, swing, plunge, self.base]; + + self.state = State::Dive { + path, + fired: 0, + shots, + t: 0.0, + }; + } + + pub fn update(&mut self, sway: f32, player: Vec2) -> Option { + let before = self.pos; + let mut shot = None; + + self.flash = self.flash.saturating_sub(1); + + match &mut self.state { + State::Formation => { + let target = self.base.x + sway; + self.pos.x += (target - self.pos.x) * 0.2; + } + + State::Dive { + path, + fired, + shots, + t, + } => { + *t += 1.0 / 240.0; + + if *t >= 1.0 { + self.pos = self.base; + self.state = State::Formation; + } else { + self.pos = math::bezier(*path, *t); + + let center = self.pos + self.sprite.size.as_vec2() / 2.0; + let facing = self.step.normalize_or_zero(); + let to_player = (player - center).normalize_or_zero(); + + let next = 0.25 + 0.5 * *fired as f32 / *shots as f32; + if *fired < *shots && *t > next && facing.dot(to_player) > 0.8 { + *fired += 1; + shot = Some(center); + } + } + } + } + + self.step = self.pos - before; + shot + } + + pub fn draw(&self, frame: &mut [u32], palette: &Palette, a: f32, tick: u32) { + let pos = self.pos - self.step * (1.0 - a); + let tint = if self.flash > 0 && (tick / 2).is_multiple_of(2) { + LIGHT_RED + } else { + 0 + }; + + if matches!(self.state, State::Dive { .. }) && self.step.length_squared() > 0.001 { + let dir = self.step.normalize(); + let angle = (-dir.x).atan2(dir.y); + let angle = (angle * ROT_STEPS / TAU).round() * (TAU / ROT_STEPS); + let center = pos + self.sprite.size.as_vec2() / 2.0; + self.sprite + .draw_rotated(frame, palette, center, angle, tint); + + let half = self.sprite.size.y as f32 / 2.0; + for i in 0..3 { + let tail = center - dir * (half + 1.0 + i as f32 * 2.0); + let c = [PURPLE, PINK][(tick / 4 + i) as usize % 2]; + gfx::blit(frame, palette, &[c], tail.round().as_ivec2(), 1, 0); + } + } else { + self.sprite + .draw_tinted(frame, palette, pos.round().as_ivec2(), tint); + } + } +} -- cgit v1.3.1