summaryrefslogtreecommitdiff
path: root/2026/games/asrael_io/source/src
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2026-08-17 20:14:16 -0400
committerLLLL Colonq <llll@colonq>2026-08-17 20:14:16 -0400
commitde99c5f7636a952025ea13de02bafd25bcb6bdec (patch)
treebe359424af4f6717103e3b667b6529ae5a4b4e6a /2026/games/asrael_io/source/src
parent4d4d1f24721f27fbf37e1070be6414de130ace61 (diff)
Fix broken
Diffstat (limited to '2026/games/asrael_io/source/src')
-rw-r--r--2026/games/asrael_io/source/src/anim.rs42
-rw-r--r--2026/games/asrael_io/source/src/bullet.rs118
-rw-r--r--2026/games/asrael_io/source/src/color.rs66
-rw-r--r--2026/games/asrael_io/source/src/enemy.rs174
-rw-r--r--2026/games/asrael_io/source/src/gfx.rs113
-rw-r--r--2026/games/asrael_io/source/src/main.rs642
-rw-r--r--2026/games/asrael_io/source/src/math.rs13
-rw-r--r--2026/games/asrael_io/source/src/player.rs96
-rw-r--r--2026/games/asrael_io/source/src/rng.rs35
-rw-r--r--2026/games/asrael_io/source/src/sfx.rs302
-rw-r--r--2026/games/asrael_io/source/src/sprite.rs89
-rw-r--r--2026/games/asrael_io/source/src/starfield.rs59
12 files changed, 0 insertions, 1749 deletions
diff --git a/2026/games/asrael_io/source/src/anim.rs b/2026/games/asrael_io/source/src/anim.rs
deleted file mode 100644
index 427e22e..0000000
--- a/2026/games/asrael_io/source/src/anim.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-use crate::color::Palette;
-use crate::sprite::Sprite;
-
-use std::rc::Rc;
-
-use glam::Vec2;
-
-const FRAME_TICKS: u32 = 6;
-
-pub struct Anim {
- center: Vec2,
- frames: Rc<Vec<Sprite>>,
- tick: u32,
-}
-
-impl Anim {
- pub fn new(frames: Rc<Vec<Sprite>>, center: Vec2) -> Self {
- Self {
- center,
- frames,
- tick: 0,
- }
- }
-
- pub fn done(&self) -> bool {
- self.tick >= FRAME_TICKS * self.frames.len() as u32
- }
-
- pub fn update(&mut self) {
- self.tick += 1;
- }
-
- pub fn draw(&self, frame: &mut [u32], palette: &Palette) {
- let i = (self.tick / FRAME_TICKS) as usize;
- let Some(sprite) = self.frames.get(i) else {
- return;
- };
-
- let pos = self.center - sprite.size.as_vec2() / 2.0;
- sprite.draw_at(frame, palette, pos.round().as_ivec2());
- }
-}
diff --git a/2026/games/asrael_io/source/src/bullet.rs b/2026/games/asrael_io/source/src/bullet.rs
deleted file mode 100644
index ac44f25..0000000
--- a/2026/games/asrael_io/source/src/bullet.rs
+++ /dev/null
@@ -1,118 +0,0 @@
-use crate::color::Palette;
-use crate::color::db32::{LIGHT_RED, LIME};
-use crate::math::aabb;
-use crate::sprite::Sprite;
-use crate::{GAME_H, GAME_W};
-
-use glam::Vec2;
-
-const ENEMY_BULLET_W: i32 = 2;
-const ENEMY_BULLET_H: i32 = 6;
-const PLAYER_BULLET_W: i32 = 3;
-const PLAYER_BULLET_H: i32 = 6;
-const ENEMY_SHOT_SPEED: f32 = 1.5;
-const PLAYER_SHOT_SPEED: f32 = 4.0;
-
-#[rustfmt::skip]
-const ENEMY_BULLET_PIXELS: [u8; (ENEMY_BULLET_W * ENEMY_BULLET_H) as usize] = [
- LIME, LIME,
- LIME, LIME,
- LIME, LIME,
- LIME, LIME,
- LIME, LIME,
- LIME, LIME,
-];
-
-#[rustfmt::skip]
-const PLAYER_BULLET_PIXELS: [u8; (PLAYER_BULLET_W * PLAYER_BULLET_H) as usize] = [
- 0, LIGHT_RED, 0,
- LIGHT_RED, LIGHT_RED, LIGHT_RED,
- 0, 0, 0,
- 0, LIGHT_RED, 0,
- 0, LIGHT_RED, 0,
- 0, LIGHT_RED, 0,
-];
-
-pub struct Bullet {
- angle: Option<f32>,
- pos: Vec2,
- vel: Vec2,
- sprite: Sprite,
-}
-
-impl Bullet {
- fn new(
- pos: Vec2,
- vel: Vec2,
- angle: Option<f32>,
- width: i32,
- height: i32,
- pixels: &[u8],
- ) -> Self {
- let sprite = Sprite::new(width, height, pixels.to_vec());
-
- Self {
- angle,
- pos,
- vel,
- sprite,
- }
- }
-
- pub fn aimed(from: Vec2, target: Vec2) -> Self {
- let dir = (target - from).normalize_or_zero();
- let vel = dir * ENEMY_SHOT_SPEED;
- let pos = from + Vec2::new(-(ENEMY_BULLET_W as f32) / 2.0, 0.0);
- let angle = (-dir.x).atan2(dir.y);
-
- Self::new(
- pos,
- vel,
- Some(angle),
- ENEMY_BULLET_W,
- ENEMY_BULLET_H,
- &ENEMY_BULLET_PIXELS,
- )
- }
-
- pub fn fired(muzzle_pos: Vec2) -> Self {
- let pos =
- muzzle_pos + Vec2::new(-(PLAYER_BULLET_W as f32) / 2.0, -(PLAYER_BULLET_H as f32));
-
- Self::new(
- pos,
- Vec2::new(0.0, -PLAYER_SHOT_SPEED),
- None,
- PLAYER_BULLET_W,
- PLAYER_BULLET_H,
- &PLAYER_BULLET_PIXELS,
- )
- }
-
- pub fn hits(&self, pos: Vec2, size: Vec2) -> bool {
- aabb(self.pos, self.sprite.size.as_vec2(), pos, size)
- }
-
- pub fn offscreen(&self) -> bool {
- self.pos.x + self.sprite.size.x as f32 <= 0.0
- || self.pos.x >= GAME_W as f32
- || self.pos.y + self.sprite.size.y as f32 <= 0.0
- || self.pos.y >= GAME_H as f32
- }
-
- pub fn update(&mut self) {
- self.pos += self.vel;
- }
-
- pub fn draw(&self, frame: &mut [u32], palette: &Palette, a: f32) {
- let p = self.pos - self.vel * (1.0 - a);
-
- match self.angle {
- Some(angle) => {
- let center = p + self.sprite.size.as_vec2() / 2.0;
- self.sprite.draw_rotated(frame, palette, center, angle, 0);
- }
- None => self.sprite.draw_at(frame, palette, p.round().as_ivec2()),
- }
- }
-}
diff --git a/2026/games/asrael_io/source/src/color.rs b/2026/games/asrael_io/source/src/color.rs
deleted file mode 100644
index 1db0786..0000000
--- a/2026/games/asrael_io/source/src/color.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-pub type Color = u32;
-
-#[allow(dead_code)]
-pub mod db32 {
- pub const BLACK: u8 = 0;
- pub const DARK_PURPLE: u8 = 1;
- pub const MAROON: u8 = 2;
- pub const DARK_BROWN: u8 = 3;
- pub const BROWN: u8 = 4;
- pub const ORANGE: u8 = 5;
- pub const TAN: u8 = 6;
- pub const PEACH: u8 = 7;
- pub const YELLOW: u8 = 8;
- pub const LIME: u8 = 9;
- pub const GREEN: u8 = 10;
- pub const SEA_GREEN: u8 = 11;
- pub const DARK_GREEN: u8 = 12;
- pub const OLIVE: u8 = 13;
- pub const CHARCOAL: u8 = 14;
- pub const NAVY: u8 = 15;
- pub const STEEL_BLUE: u8 = 16;
- pub const INDIGO: u8 = 17;
- pub const BLUE: u8 = 18;
- pub const CYAN: u8 = 19;
- pub const PALE_BLUE: u8 = 20;
- pub const WHITE: u8 = 21;
- pub const GRAY: u8 = 22;
- pub const STONE: u8 = 23;
- pub const DIM_GRAY: u8 = 24;
- pub const SLATE: u8 = 25;
- pub const PURPLE: u8 = 26;
- pub const RED: u8 = 27;
- pub const LIGHT_RED: u8 = 28;
- pub const PINK: u8 = 29;
- pub const MOSS: u8 = 30;
- pub const KHAKI: u8 = 31;
-}
-
-const fn from_rgb(r: u8, g: u8, b: u8) -> Color {
- (r as u32) << 16 | (g as u32) << 8 | b as u32
-}
-
-#[derive(Clone, Copy)]
-pub struct Palette([Color; 256]);
-
-impl Default for Palette {
- fn default() -> Self {
- Self([0; 256])
- }
-}
-
-impl Palette {
- pub fn at(&self, index: u8) -> Color {
- self.0[index as usize]
- }
-
- pub fn from_ase(src: &[aseprite::Color]) -> Self {
- let mut out = [0u32; 256];
-
- for (dst, s) in out.iter_mut().zip(src) {
- *dst = from_rgb(s.r, s.g, s.b);
- }
-
- Palette(out)
- }
-}
diff --git a/2026/games/asrael_io/source/src/enemy.rs b/2026/games/asrael_io/source/src/enemy.rs
deleted file mode 100644
index 46eabc0..0000000
--- a/2026/games/asrael_io/source/src/enemy.rs
+++ /dev/null
@@ -1,174 +0,0 @@
-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<Vec2> {
- 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);
- }
- }
-}
diff --git a/2026/games/asrael_io/source/src/gfx.rs b/2026/games/asrael_io/source/src/gfx.rs
deleted file mode 100644
index 4f515c1..0000000
--- a/2026/games/asrael_io/source/src/gfx.rs
+++ /dev/null
@@ -1,113 +0,0 @@
-use crate::color::Palette;
-use crate::{GAME_H, GAME_W};
-
-use embedded_graphics::Drawable;
-use embedded_graphics::Pixel;
-use embedded_graphics::draw_target::DrawTarget;
-use embedded_graphics::geometry::{OriginDimensions, Point, Size};
-use embedded_graphics::mono_font::{MonoFont, MonoTextStyle};
-use embedded_graphics::pixelcolor::{Rgb888, RgbColor};
-use embedded_graphics::text::{Baseline, Text};
-use glam::{IVec2, Vec2};
-
-pub struct Frame<'a>(pub &'a mut [u32]);
-
-impl OriginDimensions for Frame<'_> {
- fn size(&self) -> Size {
- Size::new(GAME_W, GAME_H)
- }
-}
-
-impl DrawTarget for Frame<'_> {
- type Color = Rgb888;
- type Error = core::convert::Infallible;
-
- fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
- where
- I: IntoIterator<Item = Pixel<Rgb888>>,
- {
- for Pixel(p, color) in pixels {
- if p.x < 0 || p.x >= GAME_W as i32 || p.y < 0 || p.y >= GAME_H as i32 {
- continue;
- }
-
- self.0[(p.y * GAME_W as i32 + p.x) as usize] =
- (color.r() as u32) << 16 | (color.g() as u32) << 8 | color.b() as u32;
- }
-
- Ok(())
- }
-}
-
-pub fn draw_text(frame: &mut [u32], font: &MonoFont, text: &str, pos: IVec2, color: u32) {
- let color = Rgb888::new((color >> 16) as u8, (color >> 8) as u8, color as u8);
- let style = MonoTextStyle::new(font, color);
-
- let _ = Text::with_baseline(text, Point::new(pos.x, pos.y), style, Baseline::Top)
- .draw(&mut Frame(frame));
-}
-
-pub fn blit(
- frame: &mut [u32],
- palette: &Palette,
- pixels: &[u8],
- origin: IVec2,
- width: i32,
- tint: u8,
-) {
- for (i, &px) in pixels.iter().enumerate() {
- if px == 0 {
- continue;
- }
-
- let sx = origin.x + i as i32 % width;
- let sy = origin.y + i as i32 / width;
- if sx < 0 || sx >= GAME_W as i32 || sy < 0 || sy >= GAME_H as i32 {
- continue;
- }
-
- let px = if tint != 0 { tint } else { px };
- frame[(sy * GAME_W as i32 + sx) as usize] = palette.at(px);
- }
-}
-
-pub fn blit_rotated(
- frame: &mut [u32],
- palette: &Palette,
- pixels: &[u8],
- center: Vec2,
- width: i32,
- angle: f32,
- tint: u8,
-) {
- let height = pixels.len() as i32 / width;
- let half = Vec2::new(width as f32, height as f32) / 2.0;
- let (sin, cos) = angle.sin_cos();
- let r = half.length().ceil() as i32;
-
- for dy in -r..=r {
- for dx in -r..=r {
- let x = dx as f32 + 0.5;
- let y = dy as f32 + 0.5;
- let sx = (cos * x + sin * y + half.x).floor() as i32;
- let sy = (-sin * x + cos * y + half.y).floor() as i32;
- if sx < 0 || sx >= width || sy < 0 || sy >= height {
- continue;
- }
-
- let px = pixels[(sy * width + sx) as usize];
- if px == 0 {
- continue;
- }
-
- let fx = center.x.round() as i32 + dx;
- let fy = center.y.round() as i32 + dy;
- if fx < 0 || fx >= GAME_W as i32 || fy < 0 || fy >= GAME_H as i32 {
- continue;
- }
-
- let px = if tint != 0 { tint } else { px };
- frame[(fy * GAME_W as i32 + fx) as usize] = palette.at(px);
- }
- }
-}
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<Anim>,
- cursor: Option<Vec2>,
- difficulty: f32,
- dive_timer: u32,
- done_timer: u32,
- enemies: Vec<Enemy>,
- enemy_bullets: Vec<Bullet>,
- explosion_frames: Rc<Vec<Sprite>>,
- frame: Vec<u32>,
- input: u32,
- interacted: bool,
- last: Option<Instant>,
- state: State,
- palette: Palette,
- player: Player,
- player_bullets: Vec<Bullet>,
- rng: Rng,
- sfx: Option<Sfx>,
- starfield: Starfield,
- surface: Option<Surface<Arc<Window>, Arc<Window>>>,
- tick: u32,
- volley_timer: u32,
- window: Option<Arc<Window>>,
-}
-
-#[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<Action> {
- 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());
- }
-}
diff --git a/2026/games/asrael_io/source/src/math.rs b/2026/games/asrael_io/source/src/math.rs
deleted file mode 100644
index 371f165..0000000
--- a/2026/games/asrael_io/source/src/math.rs
+++ /dev/null
@@ -1,13 +0,0 @@
-use glam::Vec2;
-
-pub fn aabb(a_pos: Vec2, a_size: Vec2, b_pos: Vec2, b_size: Vec2) -> bool {
- a_pos.x < b_pos.x + b_size.x
- && b_pos.x < a_pos.x + a_size.x
- && a_pos.y < b_pos.y + b_size.y
- && b_pos.y < a_pos.y + a_size.y
-}
-
-pub fn bezier(p: [Vec2; 4], t: f32) -> Vec2 {
- let u = 1.0 - t;
- u * u * u * p[0] + 3.0 * u * u * t * p[1] + 3.0 * u * t * t * p[2] + t * t * t * p[3]
-}
diff --git a/2026/games/asrael_io/source/src/player.rs b/2026/games/asrael_io/source/src/player.rs
deleted file mode 100644
index 637160b..0000000
--- a/2026/games/asrael_io/source/src/player.rs
+++ /dev/null
@@ -1,96 +0,0 @@
-use crate::color::Palette;
-use crate::color::db32::{ORANGE, YELLOW};
-use crate::gfx;
-use crate::sprite::Sprite;
-use crate::{GAME_H, GAME_W};
-
-use aseprite::AsepriteFile;
-use glam::{IVec2, Vec2};
-
-const FIRE_COOLDOWN: u32 = 12;
-const MOUSE_MAX: f32 = 2.0;
-
-#[derive(Default)]
-pub struct Player {
- cooldown: u32,
- pos: Vec2,
- step: Vec2,
- speed: f32,
- sprite: Sprite,
-}
-
-impl Player {
- pub fn new(sprites: &AsepriteFile, speed: f32) -> Self {
- let cooldown = 0;
- let pos = Vec2::new(GAME_W as f32 / 2.0, GAME_H as f32 / 2.0);
- let step = Vec2::ZERO;
- let sprite = Sprite::from_ase(sprites, "player");
-
- Self {
- cooldown,
- pos,
- step,
- speed,
- sprite,
- }
- }
-
- pub fn win_anim(&mut self, t: u32) {
- let before = self.pos;
-
- if t < 40 {
- self.pos.y += 0.3;
- } else {
- self.pos.y -= ((t - 40) as f32 * 0.25).min(8.0);
- }
-
- self.step = self.pos - before;
- }
-
- pub fn pos(&self) -> Vec2 {
- self.pos
- }
-
- pub fn size(&self) -> Vec2 {
- self.sprite.size.as_vec2()
- }
-
- pub fn try_fire(&mut self) -> Option<Vec2> {
- (self.cooldown == 0).then(|| {
- self.cooldown = FIRE_COOLDOWN;
- self.pos + Vec2::new(self.sprite.size.x as f32 / 2.0, 0.0)
- })
- }
-
- pub fn update(&mut self, direction: IVec2, mouse: Vec2) {
- let before = self.pos;
-
- self.cooldown = self.cooldown.saturating_sub(1);
-
- let dir = direction.as_vec2().normalize_or_zero();
- self.pos += dir * self.speed + mouse.clamp_length_max(MOUSE_MAX);
-
- let w = self.sprite.size.x as f32;
- let h = self.sprite.size.y as f32;
-
- self.pos.x = self.pos.x.clamp(0.0, GAME_W as f32 - w);
- self.pos.y = self.pos.y.clamp(0.0, GAME_H as f32 - h);
- self.step = self.pos - before;
- }
-
- pub fn draw(&self, frame: &mut [u32], palette: &Palette, a: f32, tick: u32) {
- let pos = (self.pos - self.step * (1.0 - a)).round().as_ivec2();
- self.sprite.draw_at(frame, palette, pos);
-
- if self.step.length_squared() > 0.01 {
- let flame = if (tick / 4).is_multiple_of(2) {
- ORANGE
- } else {
- YELLOW
- };
- let exhaust = [flame, flame];
- let nozzle = pos + IVec2::new(self.sprite.size.x / 2, self.sprite.size.y);
- gfx::blit(frame, palette, &exhaust, nozzle, 1, 0);
- }
- }
-}
diff --git a/2026/games/asrael_io/source/src/rng.rs b/2026/games/asrael_io/source/src/rng.rs
deleted file mode 100644
index 6e46de0..0000000
--- a/2026/games/asrael_io/source/src/rng.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-pub struct Rng(u32);
-
-impl Rng {
- pub fn new(seed: u32) -> Self {
- let mut rng = Self(seed.max(1));
- rng.next();
- rng
- }
-
- pub fn next(&mut self) -> u32 {
- self.0 ^= self.0 << 13;
- self.0 ^= self.0 >> 17;
- self.0 ^= self.0 << 5;
-
- self.0
- }
-
- pub fn range(&mut self, n: u32) -> u32 {
- self.next() % n
- }
-
- pub fn f32(&mut self) -> f32 {
- self.next() as f32 / u32::MAX as f32
- }
-
- pub fn chance(&mut self, p: f32) -> bool {
- self.f32() < p
- }
-}
-
-impl Default for Rng {
- fn default() -> Self {
- Self::new(u32::from_be_bytes(*b"CLNK"))
- }
-}
diff --git a/2026/games/asrael_io/source/src/sfx.rs b/2026/games/asrael_io/source/src/sfx.rs
deleted file mode 100644
index a1367df..0000000
--- a/2026/games/asrael_io/source/src/sfx.rs
+++ /dev/null
@@ -1,302 +0,0 @@
-use std::sync::mpsc::{Receiver, Sender, channel};
-
-use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
-use cpal::{Device, OutputCallbackInfo, Stream, SupportedStreamConfig};
-use resid::{ChipModel, PAL_CLOCK, SamplingMethod, Sid};
-
-const GATE: u8 = 0x01;
-const TRIANGLE: u8 = 0x10;
-const SAWTOOTH: u8 = 0x20;
-const PULSE: u8 = 0x40;
-const NOISE: u8 = 0x80;
-
-enum Sound {
- EnemyShoot,
- Explosion,
- Hit,
- Lose,
- Shoot,
- Win,
-}
-
-struct Pending {
- config: SupportedStreamConfig,
- device: Device,
- rx: Receiver<Sound>,
- sid: Sid,
-}
-
-pub struct Sfx {
- pending: Option<Pending>,
- stream: Option<Stream>,
- tx: Sender<Sound>,
-}
-
-impl Sfx {
- pub fn new() -> Self {
- let device = cpal::default_host()
- .default_output_device()
- .expect("no audio output device");
-
- let config = device
- .default_output_config()
- .expect("no default audio config");
-
- let mut sid = Sid::new(ChipModel::Mos6581);
- let (tx, rx) = channel::<Sound>();
-
- sid.set_sampling_parameters(SamplingMethod::ResampleFast, PAL_CLOCK, config.sample_rate());
- Self::set_volume(&mut sid, 0x0F);
- Self::set_envelope(&mut sid, 0, 0x06, 0x00);
- Self::set_envelope(&mut sid, 1, 0x06, 0x00);
- Self::set_envelope(&mut sid, 2, 0x08, 0xA9);
-
- Self {
- pending: Some(Pending {
- config,
- device,
- rx,
- sid,
- }),
- stream: None,
- tx,
- }
- }
-
- pub fn resume(&mut self) {
- if let Some(stream) = &self.stream {
- let _ = stream.play();
- return;
- }
-
- let Some(Pending {
- config,
- device,
- rx,
- mut sid,
- }) = self.pending.take()
- else {
- return;
- };
-
- let channels = config.channels() as usize;
- let sample_rate = config.sample_rate();
- let mut scratch: Vec<i16> = Vec::new();
-
- let mut enemy_shoot_hz: f32 = 0.0;
- let mut explosion_hz: f32 = 0.0;
- let mut hit_t: u32 = 0;
- let mut lose_hz: f32 = 0.0;
- let mut lose_t: u32 = 0;
- let mut lose_wob: f32 = 0.0;
- let mut shoot_hz: f32 = 0.0;
- let mut win_t: u32 = 0;
-
- let stream = device
- .build_output_stream(
- config.into(),
- move |data: &mut [f32], _: &OutputCallbackInfo| {
- while let Ok(sound) = rx.try_recv() {
- match sound {
- Sound::EnemyShoot => {
- enemy_shoot_hz = 520.0;
- Self::set_freq(&mut sid, 1, enemy_shoot_hz);
- Self::gate_on(&mut sid, 1, TRIANGLE);
- }
-
- Sound::Explosion => {
- hit_t = 0;
- Self::set_envelope(&mut sid, 2, 0x08, 0xA9);
-
- explosion_hz = 2500.0;
- Self::set_freq(&mut sid, 2, explosion_hz);
- Self::gate_on(&mut sid, 2, NOISE);
- }
-
- Sound::Hit => {
- Self::set_envelope(&mut sid, 2, 0x05, 0x00);
-
- hit_t = 45;
- Self::set_freq(&mut sid, 2, 2500.0);
- Self::gate_on(&mut sid, 2, NOISE);
- }
-
- Sound::Lose => {
- enemy_shoot_hz = 0.0;
- Self::set_envelope(&mut sid, 1, 0x00, 0xC6);
- Self::set_pulse_width(&mut sid, 1, 0x0800);
-
- lose_hz = 392.0;
- lose_t = 300;
- lose_wob = 0.0;
- Self::set_freq(&mut sid, 1, lose_hz);
- Self::gate_on(&mut sid, 1, PULSE);
- }
-
- Sound::Shoot => {
- shoot_hz = 1760.0;
- Self::set_freq(&mut sid, 0, shoot_hz);
- Self::gate_on(&mut sid, 0, SAWTOOTH);
- }
-
- Sound::Win => {
- shoot_hz = 0.0;
- Self::set_envelope(&mut sid, 0, 0x00, 0xA9);
- Self::set_pulse_width(&mut sid, 0, 0x0800);
-
- win_t = 300;
- Self::set_freq(&mut sid, 0, 987.77);
- Self::gate_on(&mut sid, 0, PULSE);
- }
- }
- }
-
- let frames = data.len() / channels;
- if scratch.len() < frames {
- scratch.resize(frames, 0);
- }
- let out = &mut scratch[..frames];
-
- for block in out.chunks_mut(64) {
- Self::sweep(&mut sid, &mut shoot_hz, 0.975, 220.0, 0, SAWTOOTH);
- Self::sweep(&mut sid, &mut enemy_shoot_hz, 0.97, 200.0, 1, TRIANGLE);
- Self::sweep(&mut sid, &mut explosion_hz, 0.985, 150.0, 2, NOISE);
-
- if hit_t > 0 {
- hit_t -= 1;
- if hit_t == 0 {
- Self::gate_off(&mut sid, 2, NOISE);
- }
- }
-
- if win_t > 0 {
- win_t -= 1;
-
- if win_t == 245 || win_t == 190 {
- let hz = if win_t == 245 { 1318.5 } else { 1661.2 };
- Self::set_freq(&mut sid, 0, hz);
- Self::gate_on(&mut sid, 0, PULSE);
- }
-
- if win_t == 0 {
- Self::gate_off(&mut sid, 0, PULSE);
- }
- }
-
- if lose_t > 0 {
- lose_t -= 1;
- lose_hz *= 0.995;
- lose_wob += 0.35;
-
- let hz = lose_hz * (1.0 + 0.05 * lose_wob.sin());
- Self::set_freq(&mut sid, 1, hz);
-
- if lose_t == 0 {
- Self::gate_off(&mut sid, 1, PULSE);
- }
- }
-
- Self::fill(&mut sid, block, sample_rate);
- }
-
- for (frame, &s) in data.chunks_mut(channels).zip(out.iter()) {
- frame.fill(s as f32 / 32768.0);
- }
- },
- move |err| log::error!("audio stream error: {err}"),
- None,
- )
- .expect("failed to build audio stream");
-
- stream.play().expect("failed to start audio stream");
- self.stream = Some(stream);
- }
-
- pub fn enemy_shoot(&self) {
- let _ = self.tx.send(Sound::EnemyShoot);
- }
-
- pub fn explode(&self) {
- let _ = self.tx.send(Sound::Explosion);
- }
-
- pub fn hit(&self) {
- let _ = self.tx.send(Sound::Hit);
- }
-
- pub fn lose(&self) {
- let _ = self.tx.send(Sound::Lose);
- }
-
- pub fn shoot(&self) {
- let _ = self.tx.send(Sound::Shoot);
- }
-
- pub fn win(&self) {
- let _ = self.tx.send(Sound::Win);
- }
-
- fn fill(sid: &mut Sid, out: &mut [i16], sample_rate: u32) {
- let budget = PAL_CLOCK / sample_rate + 1;
- let mut i = 0;
-
- while i < out.len() {
- let target = (out.len() - i) as u32;
- let (n, _) = sid.sample(target * budget, &mut out[i..], 1);
-
- if n == 0 {
- break;
- }
-
- i += n;
- }
- }
-
- fn gate_off(sid: &mut Sid, voice: u8, wave: u8) {
- sid.write(voice * 7 + 4, wave);
- }
-
- fn gate_on(sid: &mut Sid, voice: u8, wave: u8) {
- sid.write(voice * 7 + 4, wave);
- sid.write(voice * 7 + 4, wave | GATE);
- }
-
- fn set_envelope(sid: &mut Sid, voice: u8, attack_decay: u8, sustain_release: u8) {
- let base = voice * 7;
-
- sid.write(base + 5, attack_decay);
- sid.write(base + 6, sustain_release);
- }
-
- fn set_freq(sid: &mut Sid, voice: u8, hz: f32) {
- let base = voice * 7;
- let freq = (hz * 16_777_216.0 / PAL_CLOCK as f32) as u16;
-
- sid.write(base, freq as u8);
- sid.write(base + 1, (freq >> 8) as u8);
- }
-
- fn set_pulse_width(sid: &mut Sid, voice: u8, width: u16) {
- let base = voice * 7;
-
- sid.write(base + 2, width as u8);
- sid.write(base + 3, (width >> 8) as u8);
- }
-
- fn set_volume(sid: &mut Sid, volume: u8) {
- sid.write(0x18, volume);
- }
-
- fn sweep(sid: &mut Sid, hz: &mut f32, rate: f32, floor: f32, voice: u8, wave: u8) {
- if *hz > 0.0 {
- *hz *= rate;
-
- if *hz < floor {
- *hz = 0.0;
- Self::gate_off(sid, voice, wave);
- } else {
- Self::set_freq(sid, voice, *hz);
- }
- }
- }
-}
diff --git a/2026/games/asrael_io/source/src/sprite.rs b/2026/games/asrael_io/source/src/sprite.rs
deleted file mode 100644
index 2bfb104..0000000
--- a/2026/games/asrael_io/source/src/sprite.rs
+++ /dev/null
@@ -1,89 +0,0 @@
-use crate::color::Palette;
-use crate::gfx;
-
-use aseprite::{AsepriteFile, CelKind};
-use glam::{IVec2, Vec2};
-
-#[derive(Clone, Default)]
-pub struct Sprite {
- pixels: Vec<u8>,
- pub size: IVec2,
-}
-
-impl Sprite {
- pub fn new(width: i32, height: i32, pixels: Vec<u8>) -> Self {
- Self {
- pixels,
- size: IVec2::new(width, height),
- }
- }
-
- pub fn from_ase(file: &AsepriteFile, layer: &str) -> Self {
- let index = file
- .layers()
- .iter()
- .position(|l| l.name == layer)
- .unwrap_or_else(|| panic!("no layer named {layer}"));
- let cel = file.cel(file.layer_ref(index).unwrap(), 0).unwrap();
-
- let (CelKind::Raw { pixels, .. } | CelKind::Compressed { pixels, .. }) = &cel.kind else {
- panic!("layer {layer} has no pixel cel");
- };
-
- Self::new(
- pixels.width as i32,
- pixels.height as i32,
- pixels.data.clone(),
- )
- }
-
- pub fn frames_from_ase(file: &AsepriteFile, layer: &str) -> Vec<Self> {
- let index = file
- .layers()
- .iter()
- .position(|l| l.name == layer)
- .unwrap_or_else(|| panic!("no layer named {layer}"));
- let layer_ref = file.layer_ref(index).unwrap();
-
- (0..file.frames().len())
- .filter_map(|f| file.resolve_cel(layer_ref, f))
- .filter_map(|cel| match &cel.kind {
- CelKind::Raw { pixels, .. } | CelKind::Compressed { pixels, .. } => {
- Some(Self::new(
- pixels.width as i32,
- pixels.height as i32,
- pixels.data.clone(),
- ))
- }
- _ => None,
- })
- .collect()
- }
-
- pub fn draw_at(&self, frame: &mut [u32], palette: &Palette, pos: IVec2) {
- gfx::blit(frame, palette, &self.pixels, pos, self.size.x, 0);
- }
-
- pub fn draw_tinted(&self, frame: &mut [u32], palette: &Palette, pos: IVec2, tint: u8) {
- gfx::blit(frame, palette, &self.pixels, pos, self.size.x, tint);
- }
-
- pub fn draw_rotated(
- &self,
- frame: &mut [u32],
- palette: &Palette,
- center: Vec2,
- angle: f32,
- tint: u8,
- ) {
- gfx::blit_rotated(
- frame,
- palette,
- &self.pixels,
- center,
- self.size.x,
- angle,
- tint,
- );
- }
-}
diff --git a/2026/games/asrael_io/source/src/starfield.rs b/2026/games/asrael_io/source/src/starfield.rs
deleted file mode 100644
index ab7a199..0000000
--- a/2026/games/asrael_io/source/src/starfield.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-use crate::color::Palette;
-use crate::color::db32::{BLUE, CYAN, GRAY, LIGHT_RED, LIME, PALE_BLUE, WHITE, YELLOW};
-use crate::rng::Rng;
-use crate::{GAME_H, GAME_W};
-
-const STAR_COLORS: [u8; 8] = [WHITE, GRAY, PALE_BLUE, CYAN, BLUE, YELLOW, LIGHT_RED, LIME];
-
-struct Star {
- color: u8,
- speed: f32,
- x: i32,
- y: f32,
-}
-
-#[derive(Default)]
-pub struct Starfield {
- stars: Vec<Star>,
-}
-
-impl Starfield {
- pub fn new(count: usize, rng: &mut Rng) -> Self {
- let stars = (0..count)
- .map(|i| {
- let speed = match i % 3 {
- 0 => 0.5,
- 1 => 1.0,
- _ => 1.5,
- };
-
- Star {
- color: STAR_COLORS[rng.range(STAR_COLORS.len() as u32) as usize],
- speed,
- x: rng.range(GAME_W) as i32,
- y: rng.range(GAME_H) as f32,
- }
- })
- .collect();
-
- Self { stars }
- }
-
- pub fn update(&mut self) {
- for star in &mut self.stars {
- star.y += star.speed;
- if star.y >= GAME_H as f32 {
- star.y -= GAME_H as f32;
- }
- }
- }
-
- pub fn draw(&self, frame: &mut [u32], palette: &Palette, a: f32) {
- for star in &self.stars {
- let y = star.y - star.speed * (1.0 - a);
- let y = (y as i32).rem_euclid(GAME_H as i32);
-
- frame[(y * GAME_W as i32 + star.x) as usize] = palette.at(star.color);
- }
- }
-}