#[allow(unused)]
mod dense_terminal_double_buffer;
#[allow(unused)]
mod dense_terminal_single_buffer;
#[allow(unused)]
mod sparse_terminal_double_buffer;
use std::{collections::VecDeque, time::Duration};
use crossterm::style::Color;
use falling_tetromino_engine::{
Button, Coordinate, GameEndCause, Orientation, Phase, Stat, Tetromino, TileID,
};
use rand::RngExt;
use crate::{
fmt_helpers::{fmt_duration, fmt_hertz, fmt_lineclear_name, MAX_LEGEND_ENTRIES},
tui_settings::{
HardDropEffect, LineClearEffect, LineClearInlineEffect, LineClearParticleEffect,
LockEffect, Palette, QuickTileFromStr, TileTexture,
},
};
use super::*;
use dense_terminal_double_buffer::DenseTerminalDoubleBuffer as StandardTerminalBuffer;
#[derive(
PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct TermCell {
ch: char,
fg: Color,
}
impl TermCell {
const EMPTY: TermCell = TermCell {
ch: ' ',
fg: Color::Reset,
};
}
pub trait TerminalBuffer {
fn offset_and_area(&self) -> ((u16, u16), (u16, u16));
fn reset_with_offset_and_area(&mut self, offsets: (u16, u16), dimensions: (u16, u16));
fn write_char(&mut self, x: u16, y: u16, cell: TermCell);
fn write_tile(&mut self, x: u16, y: u16, tile: TileTexture, fg: Color);
fn write_str(&mut self, x: u16, y: u16, str: &str, fg: Color);
fn flush(&mut self, term: &mut impl Write) -> io::Result<()>;
}
#[derive(PartialEq, PartialOrd, Clone, Debug)]
pub struct HardDropEffectTile {
creation_time: InGameTime,
pos: Coordinate,
normalized_height: f32,
original_tile_id: TileID,
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
pub struct LockEffectTile {
creation_time: InGameTime,
pos: Coordinate,
original_tile_id: TileID,
}
#[derive(PartialEq, PartialOrd, Clone, Debug)]
pub struct LineClearEffectTile {
creation_time: InGameTime,
line_clear_duration: InGameTime,
origin: (usize, usize),
momentum: (f32, f32),
acceleration: (f32, f32),
tile_id: TileID,
}
#[derive(PartialEq, PartialOrd, Hash, Clone, Debug)]
pub struct LineClearEffectLine {
creation_time: InGameTime,
line_clear_duration: InGameTime,
y: usize,
line: [TileID; Game::WIDTH],
}
#[derive(PartialEq, PartialOrd, Clone, Debug, Default)]
pub struct StandardBufferedRenderer {
term_buf: StandardTerminalBuffer,
text_message_buf: VecDeque<(InGameTime, String)>,
hard_drop_effect_buf: Vec<(HardDropEffect, Vec<HardDropEffectTile>)>,
lock_effect_buf: Vec<(LockEffect, Vec<LockEffectTile>)>,
line_clear_inline_effect_buf: Vec<(LineClearInlineEffect, Vec<LineClearEffectLine>)>,
line_clear_particle_effect_buf: Vec<(LineClearParticleEffect, Vec<LineClearEffectTile>)>,
}
impl Renderer for StandardBufferedRenderer {
fn update_feed(
&mut self,
feed: impl IntoIterator<Item = (Notification, InGameTime)>,
settings: &Settings,
) {
for (notif, time) in feed {
match notif {
Notification::HardDrop {
height_dropped,
dropped_piece,
} => {
if height_dropped == 0
|| settings.hard_drop_effect().animation.is_empty()
|| settings.hard_drop_effect().duration.is_zero()
{
continue;
}
let mut hard_drop_effect_tiles = Vec::new();
let mut current_x = None;
for ((x, y), tile_id) in dropped_piece.tiles().into_iter().rev() {
if Some(x) == current_x {
continue;
}
current_x = Some(x);
for dy in 1..=height_dropped {
hard_drop_effect_tiles.push(HardDropEffectTile {
creation_time: time,
pos: (x, y + (dy as isize)),
normalized_height: (dy as f32) / (height_dropped as f32),
original_tile_id: tile_id,
});
}
}
self.hard_drop_effect_buf
.push((settings.hard_drop_effect().clone(), hard_drop_effect_tiles));
}
Notification::PieceLocked { piece } => {
if settings.lock_effect().animation.is_empty()
|| settings.lock_effect().duration.is_zero()
{
continue;
}
let mut lock_effect_tiles = Vec::new();
for (pos, tile_id) in piece.tiles() {
lock_effect_tiles.push(LockEffectTile {
creation_time: time,
pos,
original_tile_id: tile_id,
});
}
self.lock_effect_buf
.push((settings.lock_effect().clone(), lock_effect_tiles));
}
Notification::LinesClearing {
lines,
line_clear_duration,
} => match settings.line_clear_effect() {
LineClearEffect::Inline(line_clear_inline_effect) => {
let line_clear_effect_lines = lines
.into_iter()
.map(|(y, line)| LineClearEffectLine {
creation_time: time,
line_clear_duration,
y,
line,
})
.collect();
self.line_clear_inline_effect_buf
.push((line_clear_inline_effect.clone(), line_clear_effect_lines));
}
LineClearEffect::Particle(line_clear_particle_effect) => {
let mut line_clear_effect_particles = Vec::new();
for (y, line) in lines {
for (x, tile_id) in line.into_iter().enumerate() {
let (rand0, rand1) = (
rand::rng().random_range(-1.0..1.0),
rand::rng().random_range(-1.0..1.0),
);
let xpos = 2.0 * (x as f32) / (Game::WIDTH as f32) - 1.0;
let lcpe = line_clear_particle_effect;
let mmx = lcpe.momentum_base.0
+ lcpe.momentum_rand.0 * rand0
+ lcpe.momentum_xpos * xpos;
let mmy = lcpe.momentum_base.1 + lcpe.momentum_rand.1 * rand1;
line_clear_effect_particles.push(LineClearEffectTile {
creation_time: time,
line_clear_duration,
origin: (x, y),
momentum: (mmx, mmy),
acceleration: line_clear_particle_effect.acceleration,
tile_id,
});
}
}
self.line_clear_particle_effect_buf.push((
line_clear_particle_effect.clone(),
line_clear_effect_particles,
));
}
},
Notification::Accolade {
point_bonus,
lineclears,
combo,
is_spin,
is_perfect,
tetromino,
} => {
let mut tokens = Vec::new();
tokens.push(format!("+{point_bonus},"));
if is_perfect {
tokens.push("Perfect".to_owned());
}
tokens.push(fmt_lineclear_name(lineclears).to_string());
if is_spin {
tokens.push(format!("{tetromino:?}-spin"));
}
if combo > 1 {
tokens.push(format!("x{combo}"));
}
self.text_message_buf.push_front((time, tokens.join(" ")));
}
Notification::GameEnded { cause, is_win } => {
let game_end_msg = if is_win {
"Game Complete!".to_owned()
} else {
format!("{cause}...")
};
self.text_message_buf.push_front((time, game_end_msg));
}
Notification::Debug(debug_msg) => {
self.text_message_buf.push_front((time, debug_msg));
}
Notification::Custom(custom_msg) => {
self.text_message_buf.push_front((time, custom_msg));
}
}
}
}
fn reset_veffects_state(&mut self) {
self.text_message_buf.clear();
self.hard_drop_effect_buf.clear();
self.lock_effect_buf.clear();
self.line_clear_inline_effect_buf.clear();
self.line_clear_particle_effect_buf.clear();
}
fn reset_viewport_state_with_offset_and_area(
&mut self,
offsets: (u16, u16),
dimensions: (u16, u16),
) {
self.term_buf
.reset_with_offset_and_area(offsets, dimensions);
}
fn render<T: Write>(
&mut self,
term: &mut T,
game: &Game,
meta_data: &GameMetaData,
settings: &Settings,
temp_data: &TemporaryAppData,
keybinds_legend: &KeybindsLegend,
replay_extra: Option<(InGameTime, f64)>,
) -> io::Result<()> {
let (_offset, (w_viewport, h_viewport)) = self.term_buf.offset_and_area();
const W_PAD_LEFT: u16 = 1;
const W_ADD_ACTIVE_HUD: u16 = 15;
const W_HOLD: u16 = 7;
const W_FIELD: u16 = 2 * (Game::WIDTH as u16);
const W_BOARD: u16 = 1 + W_FIELD + 1;
const W_NEXT: u16 = 13;
const H_PAD_TOP: u16 = 1;
const H_FIELD: u16 = Game::LOCK_OUT_HEIGHT as u16;
const H_BOARD: u16 = 1 + H_FIELD + 1;
const H_PAD_BOT: u16 = 2;
let enough_space_for_hud =
w_viewport >= W_PAD_LEFT + W_ADD_ACTIVE_HUD + W_HOLD + W_BOARD + W_NEXT;
let hud_active =
enough_space_for_hud && (settings.graphics().show_main_hud || replay_extra.is_some());
let w_addhud = if hud_active { W_ADD_ACTIVE_HUD } else { 0 };
let w_float =
w_viewport.saturating_sub(W_PAD_LEFT + w_addhud + W_HOLD + W_BOARD + W_NEXT) / 2;
let h_float = h_viewport.saturating_sub(H_PAD_TOP + H_BOARD + H_PAD_BOT) / 2;
let tui_style = settings.tui_style();
let mino_textures = settings.mino_textures();
let ftch_col_or_rset = |tile_id: &TileID| {
settings
.palette()
.get(tile_id)
.copied()
.unwrap_or(Color::Reset)
};
let [c_fr_tl, c_fr_t, c_fr_tr, c_fr_r, c_fr_br, c_fr_b, c_fr_bl, c_fr_l] =
tui_style.frameglyphs;
let w_tmp1 = w_float + W_PAD_LEFT + w_addhud + W_HOLD;
let h_tmp1 = h_float + H_PAD_TOP;
#[rustfmt::skip] self.term_buf.write_char(w_tmp1, h_tmp1, TermCell { ch: c_fr_tl, fg: Color::Reset });
#[rustfmt::skip] self.term_buf.write_char(w_tmp1, h_tmp1 + 1 + H_FIELD, TermCell { ch: c_fr_bl, fg: Color::Reset });
for dx in 0..W_FIELD {
#[rustfmt::skip] self.term_buf.write_char(w_tmp1 + 1 + dx, h_tmp1, TermCell { ch: c_fr_t, fg: Color::Reset });
#[rustfmt::skip] self.term_buf.write_char(w_tmp1 + 1 + dx, h_tmp1 + 1 + H_FIELD, TermCell { ch: c_fr_b, fg: Color::Reset });
}
#[rustfmt::skip] self.term_buf.write_char(w_tmp1 + 1 + W_FIELD, h_tmp1, TermCell { ch: c_fr_tr, fg: Color::Reset });
#[rustfmt::skip] self.term_buf.write_char(w_tmp1 + 1 + W_FIELD, h_tmp1 + 1 + H_FIELD, TermCell { ch: c_fr_br, fg: Color::Reset });
for dy in 0..H_FIELD {
#[rustfmt::skip] self.term_buf.write_char(w_tmp1, h_tmp1 + 1 + dy, TermCell { ch: c_fr_l, fg: Color::Reset });
#[rustfmt::skip] self.term_buf.write_char(w_tmp1 + 1 + 2 * Game::WIDTH as u16, h_tmp1 + 1 + dy, TermCell { ch: c_fr_r, fg: Color::Reset });
}
if let Some((tet, is_swappable)) = game.state().piece_held {
let [c_h_tb, c_h_tl, c_h_l, c_h_bl] = tui_style.holdglyphs;
let w_tmp2 = w_float + W_PAD_LEFT + w_addhud;
let h_tmp2 = h_float + H_PAD_TOP;
#[rustfmt::skip] self.term_buf.write_char(w_tmp2, h_tmp2, TermCell { ch: c_h_tl, fg: Color::Reset });
#[rustfmt::skip] self.term_buf.write_char(w_tmp2, h_tmp2 + 2, TermCell { ch: c_h_bl, fg: Color::Reset });
for dx in 0..6 {
#[rustfmt::skip] self.term_buf.write_char(w_tmp2 + 1 + dx, h_tmp2, TermCell { ch: c_h_tb, fg: Color::Reset });
#[rustfmt::skip] self.term_buf.write_char(w_tmp2 + 1 + dx, h_tmp2 + 2, TermCell { ch: c_h_tb, fg: Color::Reset });
}
#[rustfmt::skip] self.term_buf.write_str(w_tmp2 + 2, h_tmp2, "hold",Color::Reset);
#[rustfmt::skip] self.term_buf.write_char(w_tmp2, h_tmp2 + 1, TermCell { ch: c_h_l, fg: Color::Reset });
let small_tet = &settings.small_tet_style().tets[tet as usize];
let w_extra_for_o = if tet == Tetromino::O { 1 } else { 0 };
let tile_id = if is_swappable {
tet.tile_id()
} else {
Palette::GRAY
};
let color = ftch_col_or_rset(&tile_id);
#[rustfmt::skip] self.term_buf.write_str(w_tmp2 + 2 + w_extra_for_o, h_tmp2 + 1, small_tet, color);
if !is_swappable {
#[rustfmt::skip] self.term_buf.write_char(w_tmp2 + 1, h_tmp2 + 1, TermCell { ch: 'x', fg: color });
}
}
let [c_n_tb, c_n_tr, c_n_r, c_n_jl, c_n_br, c_n_jd, c_n_ltb] = tui_style.nextglyphs;
let w_tmp6 = w_float + W_PAD_LEFT + w_addhud + W_HOLD + W_BOARD;
let h_tmp6 = h_float + H_PAD_TOP;
let mut next_tetrominos = game.state().piece_preview.iter().copied();
'render_preview: {
let draw_appended_normalsize_prev =
|term_buf: &mut StandardTerminalBuffer, y_offset: u16, next_tet: Tetromino| {
for dx in 0..12 {
#[rustfmt::skip] term_buf.write_char(w_tmp6 + dx, h_tmp6 + y_offset, TermCell { ch: c_n_ltb, fg: Color::Reset });
#[rustfmt::skip] term_buf.write_char(w_tmp6 + dx, h_tmp6 + y_offset + 3, TermCell { ch: c_n_tb, fg: Color::Reset });
}
#[rustfmt::skip] term_buf.write_char(w_tmp6 + 12, h_tmp6 + y_offset, TermCell { ch: c_n_jl, fg: Color::Reset });
#[rustfmt::skip] term_buf.write_char(w_tmp6 + 12, h_tmp6 + y_offset + 1, TermCell { ch: c_n_r, fg: Color::Reset });
#[rustfmt::skip] term_buf.write_char(w_tmp6 + 12, h_tmp6 + y_offset + 2, TermCell { ch: c_n_r, fg: Color::Reset });
#[rustfmt::skip] term_buf.write_char(w_tmp6 + 12, h_tmp6 + y_offset + 3, TermCell { ch: c_n_br, fg: Color::Reset });
let tile_texture = mino_textures.locked;
let color = ftch_col_or_rset(&next_tet.tile_id());
let w_extra_for_o = if next_tet == Tetromino::O { 2 } else { 0 };
for (dx, dy) in next_tet.minos(Orientation::N) {
#[rustfmt::skip] term_buf.write_tile(w_tmp6 + 2 + w_extra_for_o + 2 * (dx as u16), (h_tmp6 + y_offset + 2).saturating_sub(dy as u16), tile_texture, color);
}
};
let Some(first_next_tet) = next_tetrominos.next() else {
break 'render_preview;
};
draw_appended_normalsize_prev(&mut self.term_buf, 0, first_next_tet);
for dx in 0..12 {
#[rustfmt::skip] self.term_buf.write_char(w_tmp6 + dx, h_tmp6, TermCell { ch: c_n_tb, fg: Color::Reset });
}
#[rustfmt::skip] self.term_buf.write_char(w_tmp6 + 12, h_tmp6, TermCell { ch: c_n_tr, fg: Color::Reset });
#[rustfmt::skip] self.term_buf.write_str(w_tmp6 + 4, h_tmp6, "next",Color::Reset);
let mut idx = 1;
let mut y_offset = 3;
while y_offset + 3 < 20
&& settings
.graphics()
.normalsize_preview_limit
.is_none_or(|limit| idx < limit.get())
{
let Some(next_tet) = next_tetrominos.next() else {
break 'render_preview;
};
draw_appended_normalsize_prev(&mut self.term_buf, y_offset, next_tet);
idx += 1;
y_offset += 3;
}
let draw_appended_small_prev =
|term_buf: &mut StandardTerminalBuffer, y_offset: u16, next_tet: Tetromino| {
for dx in 0..8 {
#[rustfmt::skip] term_buf.write_char(w_tmp6 + dx, h_tmp6 + y_offset, TermCell { ch: c_n_ltb, fg: Color::Reset });
#[rustfmt::skip] term_buf.write_char(w_tmp6 + dx, h_tmp6 + y_offset + 2, TermCell { ch: c_n_tb, fg: Color::Reset });
}
#[rustfmt::skip] term_buf.write_char(w_tmp6 + 8, h_tmp6 + y_offset, TermCell { ch: c_n_jl, fg: Color::Reset });
#[rustfmt::skip] term_buf.write_char(w_tmp6 + 8, h_tmp6 + y_offset + 1, TermCell { ch: c_n_r, fg: Color::Reset });
#[rustfmt::skip] term_buf.write_char(w_tmp6 + 8, h_tmp6 + y_offset + 2, TermCell { ch: c_n_br, fg: Color::Reset });
let small_tet = &settings.small_tet_style().tets[next_tet as usize];
let color = ftch_col_or_rset(&next_tet.tile_id());
let w_extra_for_o = if next_tet == Tetromino::O { 1 } else { 0 };
#[rustfmt::skip] term_buf.write_str(w_tmp6 + 2 + w_extra_for_o, h_tmp6 + y_offset + 1, small_tet, color);
};
if y_offset + 2 < 20 {
let Some(next_tet) = next_tetrominos.next() else {
break 'render_preview;
};
draw_appended_small_prev(&mut self.term_buf, y_offset, next_tet);
#[rustfmt::skip] self.term_buf.write_char(w_tmp6 + 8, h_tmp6 + y_offset, TermCell { ch: c_n_jd, fg: Color::Reset });
y_offset += 2;
while y_offset + 2 < 20 {
let Some(next_tet) = next_tetrominos.next() else {
break 'render_preview;
};
draw_appended_small_prev(&mut self.term_buf, y_offset, next_tet);
y_offset += 2;
}
}
for (x_offset, next_tet) in next_tetrominos.enumerate() {
let mini_tet = settings.mini_tet_style().tets[next_tet as usize];
let color = ftch_col_or_rset(&next_tet.tile_id());
#[rustfmt::skip] self.term_buf.write_char(w_tmp6 + 10 + 2 * (x_offset as u16), h_tmp6 + y_offset.saturating_sub(1), TermCell { ch: mini_tet, fg: color });
}
}
if let Some([c_f2_l, c_f2_b0, c_f2_b1, c_f2_r]) = tui_style.frame2glyphs {
for dy in 0..H_FIELD + 1 {
#[rustfmt::skip] self.term_buf.write_char(w_tmp1.saturating_sub(1), h_tmp1 + 1 + dy, TermCell { ch: c_f2_l, fg: Color::Reset });
}
for dy in 0..H_FIELD + 1 {
#[rustfmt::skip] self.term_buf.write_char(w_tmp1 + W_BOARD, h_tmp1 + 1 + dy, TermCell { ch: c_f2_r, fg: Color::Reset });
}
for dx in 0..W_FIELD {
if dx.is_multiple_of(2) {
#[rustfmt::skip] self.term_buf.write_char(w_tmp1 + 1 + dx, h_tmp1 + 1 + H_FIELD + 1, TermCell { ch: c_f2_b0, fg: Color::Reset });
} else {
#[rustfmt::skip] self.term_buf.write_char(w_tmp1 + 1 + dx, h_tmp1 + 1 + H_FIELD + 1, TermCell { ch: c_f2_b1, fg: Color::Reset });
}
}
}
if hud_active {
let [c_m_tb] = tui_style.menuglyphs;
const W_TITLE_MARGIN: u16 = 2;
let w_tmp5 = w_float + W_PAD_LEFT;
const H_TITLE_OFFSET: u16 = 3;
let h_tmp5 = h_float + H_PAD_TOP + H_TITLE_OFFSET;
#[rustfmt::skip] self.term_buf.write_char(w_tmp5, h_tmp5 + 1, TermCell { ch: c_m_tb, fg: Color::Reset });
#[rustfmt::skip] self.term_buf.write_char(w_tmp5 + 1, h_tmp5 + 1, TermCell { ch: c_m_tb, fg: Color::Reset });
for (dx, opt_ch) in meta_data
.title
.chars()
.map(Some)
.chain([None, None].into_iter())
.take((w_addhud + W_HOLD).saturating_sub(W_TITLE_MARGIN) as usize)
.enumerate()
{
if let Some(ch) = opt_ch {
#[rustfmt::skip] self.term_buf.write_char(w_tmp5 + W_TITLE_MARGIN + (dx as u16), h_tmp5, TermCell { ch, fg: Color::Reset });
}
#[rustfmt::skip] self.term_buf.write_char(w_tmp5 + W_TITLE_MARGIN + (dx as u16), h_tmp5 + 1, TermCell { ch: c_m_tb, fg: Color::Reset });
}
let show_lockdelay = game
.state()
.fall_delay_lowerbound_hit_at_n_lineclears
.is_some()
&& !game.config.lock_delay_params.is_constant();
let stats = [
Some(("Time:", fmt_duration(game.state().time))),
Some(("Lines:", game.state().lineclears.to_string())),
Some(("Points:", game.state().points.to_string())),
Some(("Gravity:", fmt_hertz(game.state().fall_delay.as_hertz()))),
show_lockdelay.then(|| {
(
"Lock delay:",
format!(
"{}ms",
game.state().lock_delay.saturating_duration().as_millis()
),
)
}),
replay_extra.map(|(replay_len, _)| ("REPLAY", fmt_duration(replay_len))),
replay_extra
.map(|(_, replay_speed)| ("Replay speed:", format!("{replay_speed:.02}x"))),
];
for (dy, opt_stat) in stats.into_iter().enumerate() {
if let Some((str_statname, str_statval)) = opt_stat {
#[rustfmt::skip] self.term_buf.write_str(w_tmp5 + 1, h_tmp5 + 2 + (dy as u16), str_statname, Color::Reset);
let w_statname = str_statname.len() as u16;
#[rustfmt::skip] self.term_buf.write_str(w_tmp5 + 1 + w_statname + 1, h_tmp5 + 2 + (dy as u16), &str_statval, Color::Reset);
}
}
if settings.graphics().show_keybinds {
let w_tmp4 = w_float + W_PAD_LEFT;
let h_tmp4 = h_float + H_PAD_TOP + H_FIELD.saturating_sub(MAX_LEGEND_ENTRIES);
#[rustfmt::skip] self.term_buf.write_char(w_tmp4, h_tmp4, TermCell { ch: c_m_tb, fg: Color::Reset });
#[rustfmt::skip] self.term_buf.write_char(w_tmp4 + 1, h_tmp4, TermCell { ch: c_m_tb, fg: Color::Reset });
#[rustfmt::skip] self.term_buf.write_str(w_tmp4 + 1 + 1, h_tmp4, "basic keybinds", Color::Reset);
#[rustfmt::skip] self.term_buf.write_char(w_tmp4 + 1 + 1 + 14, h_tmp4, TermCell { ch: c_m_tb, fg: Color::Reset });
#[rustfmt::skip] self.term_buf.write_char(w_tmp4 + 1 + 1 + 14 + 1, h_tmp4, TermCell { ch: c_m_tb, fg: Color::Reset });
const W_KEYBINDS: usize = (W_ADD_ACTIVE_HUD + W_HOLD).saturating_sub(1) as usize;
let w_max_description = keybinds_legend
.iter()
.map(|s| s.1.chars().count())
.max()
.unwrap_or(0);
let w_budget_icons = W_KEYBINDS - w_max_description - 1;
let w_icons = keybinds_legend
.iter()
.map(|s| s.0.chars().count())
.max()
.unwrap_or(0)
.min(w_budget_icons);
for (dy, (icons, description)) in keybinds_legend.iter().enumerate() {
let icons = icons.chars().take(w_icons).collect::<String>();
let str = format!("{icons: >w_icons$} {description}");
#[rustfmt::skip] self.term_buf.write_str(w_tmp4 + 1, h_tmp4 + (dy as u16) + 1, &str, Color::Reset);
}
}
}
if let Some((end_condition_stat, _)) = game
.config
.game_limits
.iter()
.find(|(_stat, to_win)| *to_win)
{
let (str_statval, str_stattxt) = match end_condition_stat {
Stat::TimeElapsed(t) => (
t.saturating_sub(game.state().time).as_secs().to_string(),
"seconds remain",
),
Stat::PiecesLocked(p) => (
p.saturating_sub(game.state().pieces_locked.iter().sum::<u32>())
.to_string(),
"pieces remain",
),
Stat::LinesCleared(l) => (
l.saturating_sub(game.state().lineclears).to_string(),
"lines remain",
),
Stat::PointsScored(s) => (
s.saturating_sub(game.state().points).to_string(),
"points remain",
),
};
let w_tmp5 = w_float + W_PAD_LEFT + w_addhud + W_HOLD + W_BOARD + 2;
let h_tmp5 = h_float + H_PAD_TOP + H_FIELD;
#[rustfmt::skip] self.term_buf.write_str(w_tmp5, h_tmp5, &str_statval, Color::Reset);
let w_str_val = str_statval.len();
#[rustfmt::skip] self.term_buf.write_str(w_tmp5 + 1 + (w_str_val as u16), h_tmp5, str_stattxt, Color::Reset);
}
if settings.graphics().show_buttons || replay_extra.is_some() {
let w_tmp6 = w_float + W_PAD_LEFT + w_addhud + W_HOLD + W_BOARD + 2;
let h_tmp6 = h_float + H_PAD_TOP + H_FIELD + 1;
let elements = [
Ok('['),
Err(Button::MoveLeft),
Err(Button::DropSoft),
Err(Button::MoveRight),
Err(Button::RotateLeft),
Err(Button::Rotate180),
Err(Button::RotateRight),
Err(Button::DropHard),
Err(Button::HoldPiece),
Err(Button::TeleLeft),
Err(Button::TeleDown),
Err(Button::TeleRight),
Ok(']'),
];
for (dx, elem) in elements.into_iter().enumerate() {
let ch = elem.unwrap_or_else(|b| {
if game.state().active_buttons[b].is_some() {
settings.tui_style().buttonsglyphs[b]
} else {
' '
}
});
#[rustfmt::skip] self.term_buf.write_char(w_tmp6 + (dx as u16), h_tmp6, TermCell { ch, fg: Color::Reset });
}
}
const MESSAGE_EXPIRATION_TIME: Duration = Duration::from_secs(4);
{
let mut dy = 0;
self.text_message_buf.retain(|(creation_time, message)| {
let is_unexpired = game.state().time.saturating_sub(*creation_time) < MESSAGE_EXPIRATION_TIME;
if is_unexpired {
let w_msg = message.chars().count() as u16;
let x_msg = (w_float + w_addhud + W_HOLD + (W_BOARD / 2)).saturating_sub(w_msg / 2);
#[rustfmt::skip] self.term_buf.write_str(x_msg, h_float + H_PAD_TOP + H_BOARD + 1 + dy, message, Color::Reset);
dy += 1;
}
is_unexpired
});
}
let w_tmp3 = w_float + W_PAD_LEFT + w_addhud + W_HOLD + 1;
let h_tmp3 = h_float + H_PAD_TOP + H_FIELD;
if settings.graphics().show_grid {
for dy in 0..Game::LOCK_OUT_HEIGHT {
for dx in 0..Game::WIDTH {
let tile_texture = mino_textures.grid;
let color = Color::Reset;
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * dx as u16, h_tmp3.saturating_sub(dy as u16), tile_texture, color);
}
}
}
self.hard_drop_effect_buf.retain_mut(|(hard_drop_effect, hard_drop_effect_tiles)| {
let HardDropEffect { duration, animation, y_decay } = hard_drop_effect;
if duration.is_zero() || animation.is_empty() {
return false;
}
hard_drop_effect_tiles.retain(|hard_drop_effect_tile| {
let HardDropEffectTile { creation_time, pos: (dx, dy), normalized_height, original_tile_id } = *hard_drop_effect_tile;
let elapsed = game.state().time.saturating_sub(creation_time);
let timeshift = elapsed.as_secs_f32() / duration.as_secs_f32();
let factor = normalized_height * *y_decay + timeshift;
if factor > 1.0 {
return false
}
let (tile_texture, recolor) = animation[(factor * (animation.len() - 1) as f32).round() as usize];
let tile_id = recolor.unwrap_or(original_tile_id);
let color = ftch_col_or_rset(&tile_id);
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * (dx as u16), h_tmp3.saturating_sub(dy as u16), tile_texture, color);
true
});
!hard_drop_effect_tiles.is_empty()
});
if !temp_data.blindfold_enabled {
let mut y_highest_tile: isize = -1;
for (dy, line) in game
.state()
.board
.iter()
.take(Game::LOCK_OUT_HEIGHT + 1 + (H_PAD_TOP as usize))
.enumerate()
{
for (dx, tile) in line.iter().enumerate() {
if let Some(tile_id) = tile {
let tile_texture = mino_textures.locked;
let color = settings
.boardpalette()
.get(tile_id)
.copied()
.unwrap_or(Color::Reset);
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * (dx as u16), h_tmp3.saturating_sub(dy as u16), tile_texture, color);
y_highest_tile = dy as isize;
}
}
}
if settings.graphics().show_spawn && !game.has_ended() {
if let Some(next_tetromino) = game.state().piece_preview.front() {
let spawn_piece = next_tetromino.spawn_piece();
if spawn_piece.position.1 <= y_highest_tile + 4 {
for ((dx, dy), tile_id) in spawn_piece.tiles() {
if game.state().board[dy as usize][dx as usize].is_some() {
continue;
}
let tile_texture = mino_textures.shadow;
let color = ftch_col_or_rset(&tile_id);
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * (dx as u16), h_tmp3.saturating_sub(dy as u16), tile_texture, color);
}
}
}
}
}
match game.phase() {
Phase::Spawning { spawn_time: _ } => {}
Phase::PieceInPlay {
piece: player_piece,
autoshift_scheduled: _,
fall_or_lock_time: _,
lock_cap_time: _,
lowest_y: _,
} => {
if settings.graphics().show_shadow {
let shadow_piece = player_piece.teleported(&game.state().board, (0, -1));
for ((dx, dy), tile_id) in shadow_piece.tiles() {
let tile_texture = mino_textures.shadow;
let color = ftch_col_or_rset(&tile_id);
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * (dx as u16), h_tmp3.saturating_sub(dy as u16), tile_texture, color);
}
}
for ((dx, dy), tile_id) in player_piece.tiles() {
let tile_texture = mino_textures.play;
let color = ftch_col_or_rset(&tile_id);
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * (dx as u16), h_tmp3.saturating_sub(dy as u16), tile_texture, color);
}
}
Phase::LinesClearing {
clear_finish_time: _,
point_bonus: _,
} => {}
Phase::GameEnd { cause, is_win: _ } => {
match cause {
GameEndCause::LockOut { locking_piece } => {
for ((dx, dy), tile_id) in locking_piece.tiles() {
let tile_texture = mino_textures.crossed;
let color = ftch_col_or_rset(&tile_id);
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * (dx as u16), h_tmp3.saturating_sub(dy as u16), tile_texture, color);
}
}
GameEndCause::BlockOut { blocked_piece } => {
for ((dx, dy), tile_id) in blocked_piece.tiles() {
let (tile_texture, color) = if let Some(blocking_tile_id) =
game.state().board[dy as usize][dx as usize]
{
(mino_textures.crossed, ftch_col_or_rset(&blocking_tile_id))
} else {
(mino_textures.slashed, ftch_col_or_rset(&tile_id))
};
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * (dx as u16), h_tmp3.saturating_sub(dy as u16), tile_texture, color);
}
}
GameEndCause::TopOut { top_lines: _ } => {}
GameEndCause::Limit(_stat) => {}
GameEndCause::Forfeit { piece_in_play } => {
if let Some(forfeit_piece) = piece_in_play {
for ((dx, dy), tile_id) in forfeit_piece.tiles() {
let tile_texture = mino_textures.slashed;
let color = ftch_col_or_rset(&tile_id);
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * (dx as u16), h_tmp3.saturating_sub(dy as u16), tile_texture, color);
}
}
}
GameEndCause::Custom(_) => {}
}
}
}
if !game.has_ended() {
self.lock_effect_buf.retain_mut(|(lock_effect, lock_effect_tiles)| {
let LockEffect { duration, animation } = lock_effect;
if duration.is_zero() || animation.is_empty() {
return false;
}
lock_effect_tiles.retain(|lock_effect_tile| {
let LockEffectTile { creation_time, pos: (dx, dy), original_tile_id } = *lock_effect_tile;
let elapsed = game.state().time.saturating_sub(creation_time);
let timeshift = elapsed.as_secs_f32() / duration.as_secs_f32();
if timeshift > 1.0 {
return false
}
let (retexture, recolor) = animation[(timeshift * (animation.len() - 1) as f32).round() as usize];
let tile_texture = retexture.unwrap_or(mino_textures.locked);
let tile_id = recolor.unwrap_or(original_tile_id);
let color = settings
.boardpalette()
.get(&tile_id)
.copied()
.unwrap_or(Color::Reset);
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * (dx as u16), h_tmp3.saturating_sub(dy as u16), tile_texture, color);
true
});
!lock_effect_tiles.is_empty()
});
}
self.line_clear_inline_effect_buf.retain_mut(|(line_clear_inline_effect, line_clear_effect_lines)| {
let LineClearInlineEffect { anim_indices, anim_lastidx, color_animation } = line_clear_inline_effect;
line_clear_effect_lines.retain(|line_clear_effect_line| {
let LineClearEffectLine { creation_time, line_clear_duration, y: dy , line } = *line_clear_effect_line;
let elapsed = game.state().time.saturating_sub(creation_time);
let timeshift = elapsed.as_secs_f32() / line_clear_duration.as_secs_f32();
if timeshift > 1.0 {
return false
}
for (dx, original_tile_id) in line.iter().enumerate() {
let tile_texture = mino_textures.locked;
let tile_id = if !color_animation.is_empty() {
color_animation[(timeshift * (color_animation.len() - 1) as f32).round() as usize].unwrap_or(*original_tile_id)
} else {
*original_tile_id
};
let color = settings
.boardpalette()
.get(&tile_id)
.copied()
.unwrap_or(Color::Reset);
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * (dx as u16), h_tmp3.saturating_sub(dy as u16), tile_texture, color);
}
let threshold = timeshift * (*anim_lastidx as f32);
for (dx, anim_idx) in anim_indices.iter().enumerate() {
if (*anim_idx as f32) < threshold {
#[rustfmt::skip] self.term_buf.write_char(w_tmp3 + (dx as u16), h_tmp3.saturating_sub(dy as u16), TermCell { ch: ' ', fg: Color::Reset });
}
}
true
});
!line_clear_effect_lines.is_empty()
});
self.line_clear_particle_effect_buf.retain_mut(|(line_clear_particle_effect, line_clear_effect_tiles)| {
let LineClearParticleEffect { duration_override, animation, acceleration: _, momentum_base: _, momentum_rand: _, momentum_xpos: _ } = line_clear_particle_effect;
line_clear_effect_tiles.retain(|line_clear_effect_tile| {
let LineClearEffectTile { creation_time, line_clear_duration, origin: (dx, dy), momentum: (m_x, m_y), acceleration: (a_x, a_y), tile_id: original_tile_id } = *line_clear_effect_tile;
let lifetime = duration_override.unwrap_or(line_clear_duration);
if lifetime.is_zero() {
return false;
}
let elapsed = game.state().time.saturating_sub(creation_time);
let timeshift = elapsed.as_secs_f32() / lifetime.as_secs_f32();
if timeshift > 1.0 {
return false
}
if elapsed <= line_clear_duration {
let tile_texture = " ".tile();
let color = Color::Reset;
#[rustfmt::skip] self.term_buf.write_tile(w_tmp3 + 2 * (dx as u16), h_tmp3.saturating_sub(dy as u16), tile_texture, color);
}
let (retexture, recolor) = if animation.is_empty() { (None, None) } else { animation[(timeshift * (animation.len() - 1) as f32).round() as usize] };
let tile_texture = retexture.unwrap_or(mino_textures.locked);
let tile_id = recolor.unwrap_or(original_tile_id);
let color = settings
.boardpalette()
.get(&tile_id)
.copied()
.unwrap_or(Color::Reset);
let t = elapsed.as_secs_f32();
let x = (w_tmp3 + 2 * (dx as u16)) as f32 + m_x * t + a_x * t.powi(2) / 2.0;
let y = (h_tmp3.saturating_sub(dy as u16)) as f32 - m_y * t - a_y * t.powi(2) / 2.0;
#[rustfmt::skip] self.term_buf.write_tile(x.round() as u16, y.round() as u16, tile_texture, color);
true
});
!line_clear_effect_tiles.is_empty()
});
self.term_buf.flush(term)
}
}