use std::collections::HashMap; use rand::Rng; use teleia::*; use crate::overlay; use enum_map::{enum_map, Enum, EnumMap}; use strum::{EnumIter, IntoEnumIterator}; const MAX_SCORE: f32 = 12800.0; const WIDTH: i64 = 250; const HEIGHT: i64 = 400; #[derive(Debug, Clone, Copy, PartialEq, Eq, Enum, EnumIter)] enum Rank {SSS, SS, S, A, B, C, D, None} impl Rank { fn threshold(self) -> f32 { match self { Self::SSS => 2800.0, Self::SS => 2100.0, Self::S => 1500.0, Self::A => 1000.0, Self::B => 600.0, Self::C => 300.0, Self::D => 100.0, Self::None => 0.0, } } fn multiplier(self) -> f32 { match self { Self::SSS => 16.0, Self::SS => 6.0, Self::S => 5.0, Self::A => 4.0, Self::B => 3.0, Self::C => 2.0, Self::D => 1.5, Self::None => 1.0, } } } pub struct Overlay { font: font::Bitmap, fb: framebuffer::Framebuffer, rank_textures: EnumMap, shake: u64, score: f32, tags: HashMap, } impl Overlay { pub fn new(ctx: &context::Context) -> Self { let fb = framebuffer::Framebuffer::new(ctx, &glam::Vec2::new(WIDTH as f32, HEIGHT as f32), &glam::Vec2::new(0.0, 0.0)); Self { fb, font: font::Bitmap::from_image(ctx, 6, 12, 96, 72, include_bytes!("../assets/fonts/terminus.png")), rank_textures: enum_map! { Rank::SSS => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/sss.webp")), Rank::SS => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/ss.webp")), Rank::S => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/s.webp")), Rank::A => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/a.webp")), Rank::B => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/b.webp")), Rank::C => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/c.webp")), Rank::D => texture::Texture::new(ctx, include_bytes!("../assets/textures/combo/d.webp")), Rank::None => texture::Texture::new(ctx, include_bytes!("../assets/textures/test.png")), }, shake: 0, score: 0.0, tags: HashMap::new(), } } fn rank(&self) -> (Rank, f32) { let mut prev = MAX_SCORE; for r in Rank::iter() { let thresh = r.threshold(); if self.score > thresh { return (r, (self.score - thresh) / (prev - thresh)); } prev = thresh; } return (Rank::None, 0.0); } fn tag_multiplier(&self) -> f32 { return 1.0 + self.tags.len() as f32; } fn shake_offset(&self, r: Rank) -> f32 { rand::thread_rng().gen_range(0..10) as f32 / 100.0 * r.multiplier() } } impl overlay::Overlay for Overlay { fn handle_binary(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State, msg: &net::fig::BinaryMessage) -> Erm<()> { match &*msg.event { b"overlay combo message" => { let (oldrank, _) = self.rank(); self.score += 267.0 * self.tag_multiplier(); let (newrank, _) = self.rank(); if oldrank != newrank { self.shake = 20; } if let Ok(ts) = String::from_utf8(msg.data.clone()) { for t in ts.split(",") { if t.len() > 0 { self.tags.entry(t.to_owned()).and_modify(|x| *x += 1).or_insert(1); } } } }, _ => {}, } Ok(()) } fn update(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State) -> Erm<()> { let (rank, _) = self.rank(); self.score -= 2.0 * rank.multiplier(); if self.score < 0.0 { self.score = 0.0; self.tags.clear(); } if self.score > MAX_SCORE { self.score = MAX_SCORE; } if self.shake > 0 { self.shake -= 1; } Ok(()) } fn render(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State) -> Erm<()> { let (rank, meter) = self.rank(); if rank == Rank::None { return Ok(()); } st.bind_framebuffer(ctx, &self.fb); ctx.clear_color(glam::Vec4::new(0.0, 0.0, 0.0, 0.8)); ctx.clear(); // rank name st.bind_2d(ctx, &ost.assets.shader_flat); ost.assets.shader_flat.set_position_2d( ctx, st, &glam::Vec2::new(-4.0, 4.0), &glam::Vec2::new(WIDTH as f32, WIDTH as f32 / 3.0), ); self.rank_textures[rank].bind(ctx); st.mesh_square.render(ctx); // meter st.bind_2d(ctx, &ost.assets.shader_color); ost.assets.shader_color.set_position_2d( ctx, st, &glam::Vec2::new(0.0, 100.0), &glam::Vec2::new(WIDTH as f32, 16.0), ); ost.assets.shader_color.set_vec4(ctx, "color", &glam::Vec4::new(0.0, 0.0, 0.0, 1.0)); st.mesh_square.render(ctx); ost.assets.shader_color.set_position_2d( ctx, st, &glam::Vec2::new(0.0, 100.0), &glam::Vec2::new(WIDTH as f32 * meter, 16.0), ); ost.assets.shader_color.set_vec4(ctx, "color", &glam::Vec4::new(1.0, 1.0, 1.0, 1.0)); st.mesh_square.render(ctx); // tags let mut tagy = 124.0; for (t, count) in self.tags.iter() { self.font.render_text_parameterized( ctx, st, &glam::Vec2::new(4.0, tagy), &format!("+ {t} (x{count})"), font::BitmapParams { color: &[glam::Vec3::new(1.0, 1.0, 1.0)], scale: glam::Vec2::new(2.0, 2.0), }, ); tagy += 24.0; } // multiplier self.font.render_text_parameterized( ctx, st, &glam::Vec2::new(4.0, HEIGHT as f32 - 50.0), &format!("MULT:"), font::BitmapParams { color: &[glam::Vec3::new(1.0, 1.0, 1.0)], scale: glam::Vec2::new(4.0, 4.0), }, ); self.font.render_text_parameterized( ctx, st, &glam::Vec2::new(128.0, HEIGHT as f32 - 50.0), &format!("{}x", self.tag_multiplier()), font::BitmapParams { color: &[glam::Vec3::new(1.0, 0.7, 0.0)], scale: glam::Vec2::new(4.0, 4.0), }, ); // actually draw box to screen st.bind_render_framebuffer(ctx); ctx.clear_depth(); st.bind_3d(ctx, &ost.assets.shader_flat_noflip); let shake_offset = if self.shake > 0 { glam::Vec3::new(self.shake_offset(rank), 0.0, self.shake_offset(rank)) } else { glam::Vec3::ZERO }; ost.assets.shader_flat_noflip.set_position_3d( ctx, st, &glam::Mat4::from_scale_rotation_translation( glam::Vec3::new(WIDTH as f32 / HEIGHT as f32, 1.0, 1.0), glam::Quat::from_rotation_y(std::f32::consts::PI + 0.7), glam::Vec3::new(-5.5, 1.0, -8.0) + shake_offset, ), ); self.fb.bind_texture(ctx); st.mesh_square.render(ctx); Ok(()) } }