summaryrefslogtreecommitdiff
path: root/crates/renderer/src/overlay/duel.rs
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2026-09-21 22:44:32 -0400
committerLLLL Colonq <llll@colonq>2026-09-21 22:44:32 -0400
commit44cbef39e79b48481d7a111954cd4367e3d6b9fe (patch)
treea6cf4c588f69b068d4cfcae0bdf878281786c796 /crates/renderer/src/overlay/duel.rs
parentab5106e978c8c3d5cd2f97f4da1fb9e4275d7f61 (diff)
Add duelingHEADmaster
Diffstat (limited to 'crates/renderer/src/overlay/duel.rs')
-rw-r--r--crates/renderer/src/overlay/duel.rs158
1 files changed, 158 insertions, 0 deletions
diff --git a/crates/renderer/src/overlay/duel.rs b/crates/renderer/src/overlay/duel.rs
new file mode 100644
index 0000000..4f90062
--- /dev/null
+++ b/crates/renderer/src/overlay/duel.rs
@@ -0,0 +1,158 @@
+use teleia::*;
+
+use crate::overlay;
+use crate::overlay::tcg;
+
+use std::{collections::VecDeque, io::Read};
+use byteorder::{LE, ReadBytesExt};
+
+const DUEL_DURATION: u64 = 5 * 60;
+const FLIP_START: u64 = 60;
+const FLIP_END: u64 = 90;
+
+fn read_length_prefixed(r: &mut impl Read) -> Erm<Vec<u8>> {
+ let len = r.read_u32::<LE>()?;
+ let mut bytes = vec![0; len as usize];
+ r.read_exact(&mut bytes)?;
+ Ok(bytes)
+}
+
+fn extract_meta(png: &[u8]) -> Erm<Option<tcg::Card>> {
+ let cursor = std::io::Cursor::new(png);
+ let decoder = png::Decoder::new(cursor);
+ let mut reader = decoder.read_info()?;
+ let mut buf = vec![0; reader.output_buffer_size().unwrap()];
+ while let Ok(f) = reader.next_frame(&mut buf) {}
+ reader.finish()?;
+ for tc in reader.info().uncompressed_latin1_text.iter() {
+ if tc.keyword == "lcolonqtcg" {
+ let (sig, meta) = if let Some(suffix) = tc.text.strip_prefix("+SIGNED+") {
+ suffix.split_once("+").unwrap_or(("", suffix))
+ } else {
+ ("", &*tc.text)
+ };
+ return Ok(Some(tcg::Card::parse(meta)?));
+ }
+ }
+ Ok(None)
+}
+
+fn render_card_texture(
+ ctx: &context::Context, st: &state::State,
+ ost: &overlay::State,
+ t: &texture::Texture,
+ pos: Mat4
+) {
+ st.bind_3d(ctx, &ost.assets.shader_tcg_screen);
+ ost.assets.shader_tcg_screen.set_i32(ctx, "texture_front", 0);
+ ost.assets.shader_tcg_screen.set_i32(ctx, "texture_back", 1);
+ t.bind(ctx);
+ ost.assets.texture_cardback.bind_index(ctx, 1);
+ ost.assets.shader_tcg_screen.set_position_3d(ctx, st, &pos);
+ st.mesh_square.render(ctx);
+}
+
+pub struct Duel {
+ duelist1: String,
+ duelist2: String,
+ png1: Vec<u8>,
+ png2: Vec<u8>,
+ meta1: Option<tcg::Card>,
+ meta2: Option<tcg::Card>,
+}
+
+pub struct Overlay {
+ current: Option<(Duel, u64)>,
+ queue: VecDeque<Duel>,
+ card1: texture::Texture,
+ card2: texture::Texture,
+}
+impl Overlay {
+ pub fn new(ctx: &context::Context) -> Self {
+ Self {
+ current: None,
+ queue: VecDeque::new(),
+ card1: texture::Texture::new_empty(ctx),
+ card2: texture::Texture::new_empty(ctx),
+ }
+ }
+}
+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<()> {
+ #[allow(clippy::single_match)]
+ match &*msg.event {
+ b"overlay tcg duel" => {
+ let mut reader = std::io::Cursor::new(&msg.data);
+ let duelist1 = String::from_utf8(read_length_prefixed(&mut reader)?)?;
+ let duelist2 = String::from_utf8(read_length_prefixed(&mut reader)?)?;
+ let png1 = read_length_prefixed(&mut reader)?;
+ let png2 = read_length_prefixed(&mut reader)?;
+ let meta1 = extract_meta(&png1)?;
+ let meta2 = extract_meta(&png2)?;
+ self.queue.push_back(Duel { duelist1, duelist2, png1, png2, meta1, meta2 });
+ },
+ _ => {},
+ }
+ Ok(())
+ }
+ fn update(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State) -> Erm<()> {
+ if let Some((_, start)) = &self.current {
+ let elapsed = st.tick - start;
+ if elapsed > DUEL_DURATION {
+ self.current = None;
+ }
+ } else if let Some(d) = self.queue.pop_front() {
+ self.card1.upload(ctx, &d.png1);
+ self.card2.upload(ctx, &d.png2);
+ self.current = Some((d, st.tick));
+ }
+ Ok(())
+ }
+ fn render(&mut self, ctx: &context::Context, st: &mut state::State, ost: &mut overlay::State) -> Erm<()> {
+ if let Some((d, start)) = &self.current {
+ let elapsed = st.tick - start;
+ let card_scale = glam::Vec3::new(0.7111, 1.0, 1.0);
+ let card_rotation = glam::Quat::from_rotation_y(
+ if elapsed < FLIP_START { 0.0 }
+ else if elapsed < FLIP_END {
+ utils::lerp(0.0, std::f32::consts::PI,
+ (elapsed - FLIP_START) as f32 / (FLIP_END - FLIP_START) as f32
+ )
+ }
+ else { std::f32::consts::PI }
+ );
+ render_card_texture(ctx, st, ost, &self.card1,
+ glam::Mat4::from_scale_rotation_translation(
+ card_scale,
+ card_rotation,
+ glam::Vec3::new(3.0, 1.9, -8.0)
+ )
+ );
+ render_card_texture(ctx, st, ost, &self.card2,
+ glam::Mat4::from_scale_rotation_translation(
+ card_scale,
+ card_rotation,
+ glam::Vec3::new(-3.0, 1.9, -8.0)
+ )
+ );
+ let mut names_pos = glam::Vec2::new(0.0, 80.0);
+ let names = format!("{} vs {}", d.duelist1, d.duelist2);
+ let scale = 8.0;
+ let width = names.len() as f32 * st.font_default.char_width as f32 * scale;
+ names_pos += glam::Vec2::new(
+ st.render_dims.x / 2.0 - width / 2.0,
+ 0.0,
+ );
+ st.font_default.render_text_at(ctx, st,
+ names_pos,
+ &names,
+ font::BitmapParams {
+ color: &[glam::Vec4::new(1.0, 1.0, 1.0, 1.0)],
+ scale: glam::Vec2::new(scale, scale),
+ offset: glam::Vec2::ZERO,
+ },
+ );
+ }
+ Ok(())
+ }
+}