From 0d2d8cd897e94bd94e0b3ae7c6ce9aa580a44a0a Mon Sep 17 00:00:00 2001 From: LLLL Colonq Date: Wed, 26 Aug 2026 23:47:59 -0400 Subject: Add a_tension_span --- 2026/games/asrael_io/source/src/main.rs | 642 -------------------------------- 1 file changed, 642 deletions(-) delete mode 100644 2026/games/asrael_io/source/src/main.rs (limited to '2026/games/asrael_io/source/src/main.rs') diff --git a/2026/games/asrael_io/source/src/main.rs b/2026/games/asrael_io/source/src/main.rs deleted file mode 100644 index 7e57f8b..0000000 --- a/2026/games/asrael_io/source/src/main.rs +++ /dev/null @@ -1,642 +0,0 @@ -mod anim; -mod bullet; -mod color; -mod enemy; -mod gfx; -mod math; -mod player; -mod rng; -mod sfx; -mod sprite; -mod starfield; - -use anim::Anim; -use bullet::Bullet; -use color::Palette; -use color::db32::{BLACK, CYAN, LIGHT_RED, LIME, RED, WHITE, YELLOW}; -use enemy::Enemy; -use math::aabb; -use player::Player; -use rng::Rng; -use sfx::Sfx; -use sprite::Sprite; -use starfield::Starfield; - -use std::f32::consts::TAU; -use std::num::NonZeroU32; -use std::rc::Rc; -use std::sync::Arc; -use std::time::Duration; - -use aseprite::AsepriteFile; -use embedded_graphics::mono_font::ascii::{FONT_6X10, FONT_10X20}; -use glam::{IVec2, Vec2}; -use softbuffer::{Context, Surface}; -use web_time::Instant; -use winit::application::ApplicationHandler; -use winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent}; -use winit::event_loop::{ActiveEventLoop, EventLoop}; -use winit::keyboard::{KeyCode, PhysicalKey}; -use winit::window::{Window, WindowId}; - -const MAX_FRAME: Duration = Duration::from_millis(100); -const TICK: Duration = Duration::from_nanos(16_666_667); - -const PLAYER_MOVES: [(Action, IVec2); 4] = [ - (Action::Up, IVec2::new(0, -1)), - (Action::Down, IVec2::new(0, 1)), - (Action::Left, IVec2::new(-1, 0)), - (Action::Right, IVec2::new(1, 0)), -]; - -pub(crate) const GAME_W: u32 = 240; -pub(crate) const GAME_H: u32 = 160; - -const SPRITES: &[u8] = include_bytes!("../assets/sprites.aseprite"); - -const DONE_HOLD: u32 = 120; - -const WIN_CYCLE: [u8; 4] = [LIME, WHITE, CYAN, YELLOW]; -const OVER_CYCLE: [u8; 2] = [RED, LIGHT_RED]; - -#[derive(Default)] -struct Cj2k26 { - accumulator: Duration, - anims: Vec, - cursor: Option, - difficulty: f32, - dive_timer: u32, - done_timer: u32, - enemies: Vec, - enemy_bullets: Vec, - explosion_frames: Rc>, - frame: Vec, - input: u32, - interacted: bool, - last: Option, - state: State, - palette: Palette, - player: Player, - player_bullets: Vec, - rng: Rng, - sfx: Option, - starfield: Starfield, - surface: Option, Arc>>, - tick: u32, - volley_timer: u32, - window: Option>, -} - -#[derive(Clone, Copy)] -enum Action { - Up, - Down, - Left, - Right, - Fire, -} - -#[derive(Default)] -enum State { - #[default] - Waiting, - Playing, - Win, - GameOver, -} - -#[cfg(target_arch = "wasm32")] -mod jam { - use wasm_bindgen::prelude::wasm_bindgen; - - #[wasm_bindgen] - extern "C" { - #[wasm_bindgen(js_name = jamShouldStart)] - pub fn should_start() -> bool; - - #[wasm_bindgen(js_name = jamGetDifficulty)] - pub fn difficulty() -> f32; - - #[wasm_bindgen(js_name = jamStarted)] - pub fn started(verb: &str); - - #[wasm_bindgen(js_name = jamDone)] - pub fn done(win: bool); - } -} - -#[cfg(not(target_arch = "wasm32"))] -mod jam { - pub fn should_start() -> bool { - true - } - - pub fn difficulty() -> f32 { - 0.0 - } - - pub fn started(_verb: &str) {} - - pub fn done(_win: bool) {} -} - -impl Cj2k26 { - fn has_action(&self, action: Action) -> bool { - self.input & (1 << action as u32) != 0 - } - - fn unlock_audio(&mut self) { - if let Some(sfx) = &mut self.sfx { - sfx.resume(); - } - } - - fn update(&mut self) { - self.tick = self.tick.wrapping_add(1); - - if matches!(self.state, State::Waiting) && jam::should_start() { - jam::started("Shoot!"); - self.difficulty = jam::difficulty().min(7.0); - self.state = State::Playing; - } - - if self.input != 0 { - self.interacted = true; - } - - let playing = matches!(self.state, State::Playing); - - self.starfield.update(); - - if playing { - let player_dir = PLAYER_MOVES - .iter() - .filter(|(a, _)| self.has_action(*a)) - .fold(IVec2::ZERO, |acc, (_, v)| acc + *v); - - if player_dir != IVec2::ZERO { - self.cursor = None; - } - - let mut mouse = Vec2::ZERO; - if let Some(cursor) = self.cursor { - let target = cursor - self.player.size() / 2.0; - mouse = target - self.player.pos(); - } - - self.player.update(player_dir, mouse); - } - - let player_center = self.player.pos() + self.player.size() / 2.0; - - if playing && self.interacted && !self.enemies.is_empty() { - let dive_after = (90.0 - self.difficulty * 6.0).max(45.0) as u32; - self.dive_timer += 1; - if self.dive_timer >= dive_after { - self.dive_timer = 0; - - let i = self.rng.range(self.enemies.len() as u32) as usize; - let shots = (2.0 + self.difficulty / 4.0).min(5.0) as u32; - self.enemies[i].start_dive(player_center.x, shots, &mut self.rng); - } - - let volley_after = (70.0 - self.difficulty * 4.0).max(30.0) as u32; - self.volley_timer += 1; - if self.volley_timer >= volley_after { - self.volley_timer = 0; - - let volley = (2.0 + self.difficulty / 3.0).min(5.0) as u32; - for _ in 0..volley { - let i = self.rng.range(self.enemies.len() as u32) as usize; - let enemy = &self.enemies[i]; - let muzzle = enemy.center() + Vec2::new(0.0, enemy.size().y / 2.0); - let spread = Vec2::new((self.rng.f32() - 0.5) * 60.0, 0.0); - - self.enemy_bullets - .push(Bullet::aimed(muzzle, player_center + spread)); - } - - if let Some(sfx) = &self.sfx { - sfx.enemy_shoot(); - } - } - } - - let seconds = self.tick as f32 / 60.0; - let sway = 12.0 * (seconds * TAU / 3.0).sin(); - for enemy in &mut self.enemies { - if let Some(muzzle_pos) = enemy.update(sway, player_center) - && playing - { - self.enemy_bullets - .push(Bullet::aimed(muzzle_pos, self.player.pos())); - if let Some(sfx) = &self.sfx { - sfx.enemy_shoot(); - } - } - } - - if playing - && self.has_action(Action::Fire) - && let Some(muzzle_pos) = self.player.try_fire() - { - if let Some(sfx) = &self.sfx { - sfx.shoot(); - } - - self.player_bullets.push(Bullet::fired(muzzle_pos)); - } - - for bullet in self - .player_bullets - .iter_mut() - .chain(&mut self.enemy_bullets) - { - bullet.update(); - } - - self.player_bullets.retain(|b| !b.offscreen()); - self.enemy_bullets.retain(|b| !b.offscreen()); - - self.player_bullets.retain(|b| { - match self.enemies.iter().position(|e| b.hits(e.pos(), e.size())) { - Some(i) => { - if self.enemies[i].damage() { - let enemy = self.enemies.swap_remove(i); - self.anims - .push(Anim::new(self.explosion_frames.clone(), enemy.center())); - if let Some(sfx) = &self.sfx { - sfx.explode(); - } - } else if let Some(sfx) = &self.sfx { - sfx.hit(); - } - false - } - None => true, - } - }); - - if playing { - let p_pos = self.player.pos(); - let p_size = self.player.size(); - - let mut player_hit = false; - self.enemy_bullets.retain(|b| { - !b.hits(p_pos, p_size) || { - player_hit = true; - false - } - }); - - player_hit |= self - .enemies - .iter() - .any(|e| aabb(p_pos, p_size, e.pos(), e.size())); - - if player_hit { - self.state = State::GameOver; - self.done_timer = DONE_HOLD; - self.anims.push(Anim::new( - self.explosion_frames.clone(), - p_pos + p_size / 2.0, - )); - if let Some(sfx) = &self.sfx { - sfx.explode(); - sfx.lose(); - } - } - } - - if matches!(self.state, State::Playing) && self.enemies.is_empty() { - self.state = State::Win; - self.done_timer = DONE_HOLD; - if let Some(sfx) = &self.sfx { - sfx.win(); - } - } - - if matches!(self.state, State::Win) { - self.player - .win_anim(DONE_HOLD.saturating_sub(self.done_timer)); - } - - if self.done_timer > 0 { - self.done_timer -= 1; - if self.done_timer == 0 { - jam::done(matches!(self.state, State::Win)); - } - } - - for anim in &mut self.anims { - anim.update(); - } - self.anims.retain(|a| !a.done()); - } - - fn draw(&mut self, a: f32) { - self.frame.fill(self.palette.at(BLACK)); - - self.starfield.draw(&mut self.frame, &self.palette, a); - - if !matches!(self.state, State::GameOver) { - self.player - .draw(&mut self.frame, &self.palette, a, self.tick); - } - - for enemy in &self.enemies { - enemy.draw(&mut self.frame, &self.palette, a, self.tick); - } - - for bullet in self.player_bullets.iter().chain(&self.enemy_bullets) { - bullet.draw(&mut self.frame, &self.palette, a); - } - - for anim in &self.anims { - anim.draw(&mut self.frame, &self.palette); - } - - let cycle = (self.tick / 8) as usize; - let banner = match self.state { - State::Win => Some(("YOU WIN", WIN_CYCLE[cycle % WIN_CYCLE.len()])), - State::GameOver => Some(("GAME OVER", OVER_CYCLE[cycle % OVER_CYCLE.len()])), - _ => None, - }; - - if let Some((text, color)) = banner { - let w = text.len() as i32 * FONT_10X20.character_size.width as i32; - let pos = IVec2::new((GAME_W as i32 - w) / 2, (GAME_H as i32 - 20) / 2); - gfx::draw_text( - &mut self.frame, - &FONT_10X20, - text, - pos, - self.palette.at(color), - ); - } - - if !self.interacted && matches!(self.state, State::Waiting | State::Playing) { - let lines = ["Mouse or WASD to Move", "Left Click or Space to Fire"]; - for (i, hint) in lines.iter().enumerate() { - let w = hint.len() as i32 * FONT_6X10.character_size.width as i32; - let y = GAME_H as i32 / 2 + 20 + i as i32 * 12; - let pos = IVec2::new((GAME_W as i32 - w) / 2, y); - gfx::draw_text( - &mut self.frame, - &FONT_6X10, - hint, - pos, - self.palette.at(WHITE), - ); - } - } - - self.present(); - } - - fn present(&mut self) { - let Some(window) = self.window.as_ref() else { - return; - }; - let Some(surface) = self.surface.as_mut() else { - return; - }; - - let size = window.inner_size(); - let (w, h) = (size.width as usize, size.height as usize); - let scale = (w / GAME_W as usize).min(h / GAME_H as usize); - if scale == 0 { - return; - } - - let Ok(mut buffer) = surface.buffer_mut() else { - return; - }; - if buffer.len() != w * h { - return; - } - let out_w = GAME_W as usize * scale; - let x0 = w.saturating_sub(out_w) / 2; - let y0 = h.saturating_sub(GAME_H as usize * scale) / 2; - - buffer.fill(0); - - let mut row = vec![0u32; out_w]; - for gy in 0..GAME_H as usize { - let src = &self.frame[gy * GAME_W as usize..(gy + 1) * GAME_W as usize]; - for (i, &c) in src.iter().enumerate() { - row[i * scale..(i + 1) * scale].fill(c); - } - - for sy in 0..scale { - let start = (y0 + gy * scale + sy) * w + x0; - buffer[start..start + out_w].copy_from_slice(&row); - } - } - - let _ = buffer.present(); - } -} - -impl ApplicationHandler for Cj2k26 { - fn resumed(&mut self, event_loop: &ActiveEventLoop) { - let attributes = Window::default_attributes().with_title("cj2k26"); - - #[cfg(not(target_arch = "wasm32"))] - let attributes = { - use winit::dpi::LogicalSize; - - let size = LogicalSize::new(GAME_W * 4, GAME_H * 4); - attributes.with_inner_size(size).with_min_inner_size(size) - }; - - #[cfg(target_arch = "wasm32")] - let attributes = { - use winit::platform::web::WindowAttributesExtWebSys; - attributes.with_append(true) - }; - - let window = Arc::new( - event_loop - .create_window(attributes) - .expect("failed to create window"), - ); - window.set_cursor_visible(false); - - #[cfg(not(target_arch = "wasm32"))] - { - use winit::window::CursorGrabMode; - let _ = window - .set_cursor_grab(CursorGrabMode::Confined) - .or_else(|_| window.set_cursor_grab(CursorGrabMode::Locked)); - } - - let context = Context::new(window.clone()).expect("failed to create softbuffer context"); - let mut surface = Surface::new(&context, window.clone()).expect("failed to create surface"); - - let inner = window.inner_size(); - if let (Some(w), Some(h)) = (NonZeroU32::new(inner.width), NonZeroU32::new(inner.height)) { - let _ = surface.resize(w, h); - } - - self.frame = vec![0; (GAME_W * GAME_H) as usize]; - self.surface = Some(surface); - - let sprites = AsepriteFile::from_reader(SPRITES).expect("failed to read sprites aseprite!"); - - self.enemies = (0..8) - .map(|i| { - let hp = if i % 3 == 0 { 2 } else { 1 }; - Enemy::new( - &sprites, - "enemy", - Vec2::new(24.0 + i as f32 * 24.0, 24.0), - hp, - ) - }) - .collect(); - - self.explosion_frames = Rc::new(Sprite::frames_from_ase(&sprites, "explosion")); - self.palette = Palette::from_ase(sprites.palette()); - self.player = Player::new(&sprites, 2.0); - self.rng = Rng::default(); - self.sfx = Some(Sfx::new()); - self.starfield = Starfield::new(60, &mut self.rng); - self.window = Some(window.clone()); - - window.request_redraw(); - } - - fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { - match event { - WindowEvent::CloseRequested => event_loop.exit(), - - WindowEvent::Resized(size) => { - if let Some(surface) = self.surface.as_mut() - && let (Some(w), Some(h)) = - (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) - { - let _ = surface.resize(w, h); - } - } - - WindowEvent::RedrawRequested => { - let now = Instant::now(); - let dt = self - .last - .replace(now) - .map(|prev| (now - prev).min(MAX_FRAME)) - .unwrap_or_default(); - self.accumulator += dt; - - while self.accumulator >= TICK { - self.update(); - self.accumulator -= TICK; - } - - let alpha = self.accumulator.as_secs_f32() / TICK.as_secs_f32(); - self.draw(alpha); - - #[cfg(not(target_arch = "wasm32"))] - std::thread::sleep(TICK.saturating_sub(now.elapsed())); - - if let Some(window) = self.window.as_ref() { - window.request_redraw(); - } - } - - WindowEvent::KeyboardInput { - event: - KeyEvent { - physical_key: PhysicalKey::Code(code), - state, - .. - }, - .. - } => { - self.unlock_audio(); - - if let Some(action) = bind_key(code) { - let bit = 1 << action as u32; - - match state { - ElementState::Pressed => self.input |= bit, - ElementState::Released => self.input &= !bit, - } - } - } - - WindowEvent::CursorMoved { position, .. } => { - if let Some(window) = self.window.as_ref() { - let size = window.inner_size(); - let (w, h) = (size.width as i32, size.height as i32); - let scale = (w / GAME_W as i32).min(h / GAME_H as i32); - - if scale > 0 { - let x0 = (w - GAME_W as i32 * scale) / 2; - let y0 = (h - GAME_H as i32 * scale) / 2; - let gx = (position.x as i32 - x0) as f32 / scale as f32; - let gy = (position.y as i32 - y0) as f32 / scale as f32; - - self.cursor = Some(Vec2::new(gx, gy)); - if matches!(self.state, State::Playing) { - self.interacted = true; - } - } - } - } - - WindowEvent::MouseInput { - state, - button: MouseButton::Left, - .. - } => { - self.unlock_audio(); - - let bit = 1 << Action::Fire as u32; - match state { - ElementState::Pressed => self.input |= bit, - ElementState::Released => self.input &= !bit, - } - } - - WindowEvent::MouseInput { .. } | WindowEvent::KeyboardInput { .. } => { - self.unlock_audio(); - } - - _ => {} - } - } -} - -fn bind_key(code: KeyCode) -> Option { - match code { - KeyCode::KeyW | KeyCode::ArrowUp => Some(Action::Up), - KeyCode::KeyS | KeyCode::ArrowDown => Some(Action::Down), - KeyCode::KeyA | KeyCode::ArrowLeft => Some(Action::Left), - KeyCode::KeyD | KeyCode::ArrowRight => Some(Action::Right), - KeyCode::Space => Some(Action::Fire), - - _ => None, - } -} - -fn main() { - let event_loop = EventLoop::new().expect("failed to create event loop"); - - #[cfg(not(target_arch = "wasm32"))] - { - env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); - event_loop - .run_app(&mut Cj2k26::default()) - .expect("failed to run app"); - } - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::EventLoopExtWebSys; - - console_error_panic_hook::set_once(); - event_loop.spawn_app(Cj2k26::default()); - } -} -- cgit v1.3.1