use std::{collections::VecDeque, time::Duration};
use crate::{
core_game_engine::{
BOARD_WIDTH, Button, Coordinate, ExtDuration, GameEndCause, GameExt, Orientation,
PLAYABLE_BOARD_HEIGHT, Phase, Stat, Tetromino, TileType,
},
terminal_buffers::DenseDoubleBuffer,
};
use rand::RngExt;
use crate::{
fmt_helpers::{fmt_duration, fmt_hertz, fmt_lineclear_name},
settings::{
HardDropEffect, LineClearEffect, LineClearInlineEffect, LineClearParticleEffect,
LockEffect, MaybeOverride::Override, TileTexture,
},
terminal_buffers::{TermCell, TerminalBuffer},
};
use super::*;
#[derive(PartialEq, PartialOrd, Clone, Debug)]
pub struct HardDropEffectTile {
creation_time: InGameTime,
pos: Coordinate,
normalized_height: f32,
original_tile_type: TileType,
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
pub struct LockEffectTile {
creation_time: InGameTime,
pos: Coordinate,
original_tile_type: TileType,
}
#[derive(PartialEq, PartialOrd, Clone, Debug)]
pub struct LineClearEffectTile {
creation_time: InGameTime,
line_clear_duration: InGameTime,
origin: (usize, usize),
momentum: (f32, f32),
acceleration: (f32, f32),
original_tile_type: TileType,
}
#[derive(PartialEq, PartialOrd, Hash, Clone, Debug)]
pub struct LineClearEffectLine {
creation_time: InGameTime,
line_clear_duration: InGameTime,
y: usize,
line: [TileType; BOARD_WIDTH],
}
type MainBufRendererTermBuf = DenseDoubleBuffer;
#[derive(PartialEq, Clone, Debug, Default)]
pub struct MainBufRenderer {
term_buf: MainBufRendererTermBuf,
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 GameRenderer for MainBufRenderer {
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) in dropped_piece.coords().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_type: dropped_piece.tetromino.into(),
});
}
}
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 in piece.coords() {
lock_effect_tiles.push(LockEffectTile {
creation_time: time,
pos,
original_tile_type: piece.tetromino.into(),
});
}
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) / (BOARD_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,
original_tile_type: 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::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(
&mut self,
offsets: (u16, u16),
dimensions: (u16, u16),
ambience: TermCell,
) {
self.term_buf.reset_with_ambience(ambience);
self.term_buf
.reset_with_offset_and_area(offsets, dimensions);
}
fn render<W: Write>(
&mut self,
term: &mut W,
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_PLAYFIELD: u16 = 2 * (BOARD_WIDTH as u16);
const W_BOARD: u16 = 1 + W_PLAYFIELD + 1;
const W_NEXT: u16 = 13;
const H_PAD_TOP: u16 = 1;
const H_PLAYFIELD: u16 = PLAYABLE_BOARD_HEIGHT as u16;
const H_BOARD: u16 = 1 + H_PLAYFIELD + 1;
const H_PAD_BOT: u16 = 2;
const RENDERED_FIELD_HEIGHT: usize = PLAYABLE_BOARD_HEIGHT + 1 + (H_PAD_TOP as usize);
let hud_active = settings.graphics().show_main_hud;
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_symbols = settings.tui_symbols();
let tui_colors = settings.tui_coloring();
let fg_tui = tui_colors.fg_tui;
let fg_accent = tui_colors.fg_accent;
let fg_widgetframe = tui_colors.fg_widgetframe;
let fg_boardframe = tui_colors.fg_boardframe;
let fg_grid = tui_colors.fg_grid;
let bg_tui = tui_colors.bg_tui;
let bg_widget = tui_colors.bg_widget;
let bg_boardframe = tui_colors.bg_boardframe;
let bg_board = tui_colors.bg_board;
if hud_active {
let [c_m_tb] = tui_symbols.headingline;
const W_TITLE_MARGIN: u16 = 2;
let w_tmp_hudtl = w_float + W_PAD_LEFT; const H_TITLE_OFFSET: u16 = 3;
let h_tmp_hudtl = h_float + H_PAD_TOP + H_TITLE_OFFSET;
#[rustfmt::skip] self.term_buf.write_char(w_tmp_hudtl, h_tmp_hudtl + 1, c_m_tb, fg_accent, Some(bg_tui));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_hudtl + 1, h_tmp_hudtl + 1, c_m_tb, fg_accent, Some(bg_tui));
for (dx, opt_ch) in meta_data
.title
.chars()
.map(Some)
.chain([None, None])
.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_tmp_hudtl + W_TITLE_MARGIN + (dx as u16), h_tmp_hudtl, ch, fg_tui, Some(bg_tui));
}
#[rustfmt::skip] self.term_buf.write_char(w_tmp_hudtl + W_TITLE_MARGIN + (dx as u16), h_tmp_hudtl + 1, c_m_tb, fg_accent, Some(bg_tui));
}
let mut stats: Vec<Option<(&str, String)>> = vec![];
if meta_data.show_stats.contains(ShowStatsHud::TIME) {
stats.push(Some(("Time: ", fmt_duration(game.state().time))));
}
if meta_data.show_stats.contains(ShowStatsHud::LINES) {
stats.push(Some(("Lines: ", game.state().lineclears.to_string())));
}
if meta_data.show_stats.contains(ShowStatsHud::POINTS) {
stats.push(Some(("Points: ", game.state().points.to_string())));
}
if meta_data.show_stats.contains(ShowStatsHud::PIECES) {
stats.push(Some((
"Pieces: ",
game.state().pieces_locked.iter().sum::<u32>().to_string(),
)));
}
if meta_data.show_stats.contains(ShowStatsHud::PIECES_COUNTS) {
let mut tet_infos = game
.state()
.pieces_locked
.iter()
.zip(Tetromino::VARIANTS)
.map(|(n, t)| {
format!(
"{n}{}",
settings.mini_tetromino_symbols().tets[t as usize].to_ascii_lowercase()
)
});
let pieces_l1 = tet_infos.by_ref().take(4).collect::<Vec<_>>().join(" ");
let pieces_l2 = tet_infos.collect::<Vec<_>>().join(" ");
stats.push(Some((" ", pieces_l1)));
stats.push(Some((" ", pieces_l2)));
}
if meta_data.show_stats.contains(ShowStatsHud::GRAVITY) {
stats.push(Some((
"Gravity: ",
fmt_hertz(game.state().fall_delay.as_hertz()),
)));
}
if meta_data.show_stats.contains(ShowStatsHud::LOCKDELAY) {
stats.push(Some((
"Lock delay: ",
if let ExtDuration::Finite(lock_delay) = game.state().lock_delay {
format!("{}ms", lock_delay.as_millis())
} else {
"inf".to_owned()
},
)));
}
for modifier in &game.modifiers {
for (s1, s2) in modifier.values() {
stats.push(Some(("", format!("{s1}: {s2}"))));
}
}
if let Some((replay_len, replay_speed)) = replay_extra {
stats.push(None);
stats.push(Some(("REPLAY ", fmt_duration(replay_len))));
stats.push(Some(("", {
let (partial_glyphs, full_glyph) = &tui_symbols.progressbar;
let w_progressbar = (W_ADD_ACTIVE_HUD + W_HOLD).saturating_sub(3);
let progress = (game.state().time.as_secs_f32() / replay_len.as_secs_f32())
.clamp(0.0, 1.0);
let pip_granularity = if partial_glyphs.is_empty() {
1
} else {
partial_glyphs.len()
};
let pips_available = (w_progressbar as f32) * (pip_granularity as f32);
let pips_to_fill = (progress * pips_available).round() as usize;
let mut progress_bar = String::new();
progress_bar.push_str(
&full_glyph
.to_string()
.repeat(pips_to_fill / pip_granularity),
);
if !pips_to_fill.is_multiple_of(pip_granularity) {
progress_bar.push(partial_glyphs[pips_to_fill % pip_granularity]);
}
progress_bar.push_str(
&" ".repeat(w_progressbar as usize - progress_bar.chars().count()),
);
progress_bar.push(']');
progress_bar
})));
stats.push(Some(("Replay speed: ", format!("{replay_speed:.02}x"))));
}
let h_stats = stats.len();
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_tmp_hudtl + 1, h_tmp_hudtl + 2 + (dy as u16), str_statname, fg_tui, Some(bg_tui));
let w_statname = str_statname.len() as u16;
#[rustfmt::skip] self.term_buf.write_str(w_tmp_hudtl + 1 + w_statname, h_tmp_hudtl + 2 + (dy as u16), &str_statval, fg_tui, Some(bg_tui));
}
}
if settings.graphics().show_keybinds {
let w_tmp_ktl = w_float + W_PAD_LEFT; let h_tmp_ktl = (h_tmp_hudtl + 2 + (h_stats as u16) + 1)
.max(h_float + H_PAD_TOP + H_PLAYFIELD.saturating_sub(MAX_LEGEND_ENTRIES));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_ktl, h_tmp_ktl, c_m_tb, fg_accent, Some(bg_tui));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_ktl + 1, h_tmp_ktl, c_m_tb, fg_accent, Some(bg_tui));
#[rustfmt::skip] self.term_buf.write_str(w_tmp_ktl + 1 + 1, h_tmp_ktl, "basic keybinds", fg_tui, Some(bg_tui));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_ktl + 1 + 1 + 14, h_tmp_ktl, c_m_tb, fg_accent, Some(bg_tui));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_ktl + 1 + 1 + 14 + 1, h_tmp_ktl, c_m_tb, fg_accent, Some(bg_tui));
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_tmp_ktl + 1, h_tmp_ktl + (dy as u16) + 1, &str, fg_tui, Some(bg_tui));
}
}
}
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_tmp_wtl = w_float + W_PAD_LEFT + w_addhud + W_HOLD + W_BOARD + 2; let h_tmp_wtl = h_float + H_PAD_TOP + H_PLAYFIELD;
#[rustfmt::skip] self.term_buf.write_str(w_tmp_wtl, h_tmp_wtl, &str_statval, fg_tui, Some(bg_tui));
let w_str_val = str_statval.len();
#[rustfmt::skip] self.term_buf.write_str(w_tmp_wtl + 1 + (w_str_val as u16), h_tmp_wtl, str_stattxt, fg_tui, Some(bg_tui));
}
if settings.graphics().show_buttons || replay_extra.is_some() {
let w_tmp_btntl = w_float + W_PAD_LEFT + w_addhud + W_HOLD + W_BOARD + 2; let h_tmp_btntl = h_float + H_PAD_TOP + H_PLAYFIELD + 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_symbols().buttons[b]
} else {
' '
}
});
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btntl + (dx as u16), h_tmp_btntl, ch, fg_tui, Some(bg_tui));
}
}
const MESSAGE_EXPIRATION_TIME: Duration = Duration::from_secs(4);
{
let w_aesthetic_pad = if h_viewport > h_float + H_PAD_TOP + H_BOARD + 1 {
1
} else {
0
};
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_wrapping(x_msg, h_float + H_PAD_TOP + H_BOARD + w_aesthetic_pad + dy, message, fg_tui, Some(bg_tui));
dy += 1;
}
is_unexpired
});
}
let tile_symbols = settings.tile_symbols();
let fetchcols =
|tile_type: TileType| settings.tile_coloring().tile_col(tile_type, game.level());
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_symbols.boardframe;
let w_tmp_btl = w_float + W_PAD_LEFT + w_addhud + W_HOLD; let h_tmp_btl = h_float + H_PAD_TOP;
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl, h_tmp_btl, c_fr_tl, fg_boardframe, Some(bg_boardframe));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl, h_tmp_btl + 1 + H_PLAYFIELD, c_fr_bl, fg_boardframe, Some(bg_boardframe));
for dx in 0..W_PLAYFIELD {
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl + 1 + dx, h_tmp_btl, c_fr_t, fg_boardframe, Some(bg_boardframe));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl + 1 + dx, h_tmp_btl + 1 + H_PLAYFIELD, c_fr_b, fg_boardframe, Some(bg_boardframe));
}
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl + 1 + W_PLAYFIELD, h_tmp_btl, c_fr_tr, fg_boardframe, Some(bg_boardframe));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl + 1 + W_PLAYFIELD, h_tmp_btl + 1 + H_PLAYFIELD, c_fr_br, fg_boardframe, Some(bg_boardframe));
for dy in 0..H_PLAYFIELD {
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl, h_tmp_btl + 1 + dy, c_fr_l, fg_boardframe, Some(bg_boardframe));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl + 1 + 2 * BOARD_WIDTH as u16, h_tmp_btl + 1 + dy, c_fr_r, fg_boardframe, Some(bg_boardframe));
}
if let Some((tet, is_swappable)) = game.state().tetromino_held {
let [c_h_tb, c_h_tl, c_h_l, c_h_bl] = tui_symbols.holdframe;
let w_tmp_htl = w_float + W_PAD_LEFT + w_addhud; let h_tmp_htl = h_float + H_PAD_TOP;
#[rustfmt::skip] self.term_buf.write_char(w_tmp_htl, h_tmp_htl, c_h_tl, fg_widgetframe, Some(bg_widget));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_htl, h_tmp_htl + 2, c_h_bl, fg_widgetframe, Some(bg_widget));
for dx in 0..6 {
#[rustfmt::skip] self.term_buf.write_char(w_tmp_htl + 1 + dx, h_tmp_htl, c_h_tb, fg_widgetframe, Some(bg_widget));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_htl + 1 + dx, h_tmp_htl + 1, ' ', fg_tui, Some(bg_widget));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_htl + 1 + dx, h_tmp_htl + 2, c_h_tb, fg_widgetframe, Some(bg_widget));
}
#[rustfmt::skip] self.term_buf.write_str(w_tmp_htl + 2, h_tmp_htl, "hold", fg_widgetframe, Some(bg_widget));
#[rustfmt::skip] self.term_buf.write_char(w_tmp_htl, h_tmp_htl + 1, c_h_l, fg_widgetframe, Some(bg_widget));
let small_tet = &settings.small_tetromino_symbols().tets[tet as usize];
let w_extra_for_o = if tet == Tetromino::O { 1 } else { 0 };
let tile_type = if is_swappable {
tet.into()
} else {
TileType::Generic
};
let fg_holdtet = settings
.tile_coloring()
.simplified_tile_col(tile_type, game.level());
#[rustfmt::skip] self.term_buf.write_str(w_tmp_htl + 2 + w_extra_for_o, h_tmp_htl + 1, small_tet, fg_holdtet, Some(bg_widget));
if !is_swappable {
#[rustfmt::skip] self.term_buf.write_char(w_tmp_htl + 1, h_tmp_htl + 1, 'x', fg_holdtet, Some(bg_widget));
}
}
let [c_n_tb, c_n_tr, c_n_r, c_n_jl, c_n_br, c_n_jd, c_n_ltb] = tui_symbols.nextframe;
let w_tmp_ntl = w_float + W_PAD_LEFT + w_addhud + W_HOLD + W_BOARD; let h_tmp_ntl = h_float + H_PAD_TOP;
let mut next_tetrominos = game.state().tetromino_preview.iter().copied();
'render_preview: {
let draw_appended_normalsize_prev =
|term_buf: &mut MainBufRendererTermBuf, y_offset: u16, next_tet: Tetromino| {
for dx in 0..12 {
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + dx, h_tmp_ntl + y_offset, c_n_ltb, fg_widgetframe, Some(bg_widget));
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + dx, h_tmp_ntl + y_offset + 1, ' ', fg_tui, Some(bg_widget));
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + dx, h_tmp_ntl + y_offset + 2, ' ', fg_tui, Some(bg_widget));
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + dx, h_tmp_ntl + y_offset + 3, c_n_tb, fg_widgetframe, Some(bg_widget));
}
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + 12, h_tmp_ntl + y_offset, c_n_jl, fg_widgetframe, Some(bg_widget));
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + 12, h_tmp_ntl + y_offset + 1, c_n_r, fg_widgetframe, Some(bg_widget));
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + 12, h_tmp_ntl + y_offset + 2, c_n_r, fg_widgetframe, Some(bg_widget));
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + 12, h_tmp_ntl + y_offset + 3, c_n_br, fg_widgetframe, Some(bg_widget));
let tile_texture = tile_symbols.player(next_tet);
let (fg_nexttet, bg_nexttet) = fetchcols(next_tet.into());
let bg_nexttet = bg_nexttet.unwrap_or(bg_widget);
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_tmp_ntl + 2 + w_extra_for_o + 2 * (dx as u16), (h_tmp_ntl + y_offset + 2).saturating_sub(dy as u16), tile_texture, fg_nexttet, Some(bg_nexttet));
}
};
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_tmp_ntl + dx, h_tmp_ntl, c_n_tb, fg_widgetframe, Some(bg_widget));
}
#[rustfmt::skip] self.term_buf.write_char(w_tmp_ntl + 12, h_tmp_ntl, c_n_tr, fg_widgetframe, Some(bg_widget));
#[rustfmt::skip] self.term_buf.write_str(w_tmp_ntl + 4, h_tmp_ntl, "next", fg_widgetframe, Some(bg_widget));
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 MainBufRendererTermBuf, y_offset: u16, next_tet: Tetromino| {
for dx in 0..8 {
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + dx, h_tmp_ntl + y_offset, c_n_ltb, fg_widgetframe, Some(bg_widget));
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + dx, h_tmp_ntl + y_offset + 1, ' ', fg_tui, Some(bg_widget));
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + dx, h_tmp_ntl + y_offset + 2, c_n_tb, fg_widgetframe, Some(bg_widget));
}
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + 8, h_tmp_ntl + y_offset, c_n_jl, fg_widgetframe, Some(bg_widget));
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + 8, h_tmp_ntl + y_offset + 1, c_n_r, fg_widgetframe, Some(bg_widget));
#[rustfmt::skip] term_buf.write_char(w_tmp_ntl + 8, h_tmp_ntl + y_offset + 2, c_n_br, fg_widgetframe, Some(bg_widget));
let small_tet = &settings.small_tetromino_symbols().tets[next_tet as usize];
let fg_smallnexttet = settings
.tile_coloring()
.simplified_tile_col(next_tet.into(), game.level());
let w_extra_for_o = if next_tet == Tetromino::O { 1 } else { 0 };
#[rustfmt::skip] term_buf.write_str(w_tmp_ntl + 2 + w_extra_for_o, h_tmp_ntl + y_offset + 1, small_tet, fg_smallnexttet, Some(bg_widget));
};
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_tmp_ntl + 8, h_tmp_ntl + y_offset, c_n_jd, fg_widgetframe, Some(bg_widget));
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_tetromino_symbols().tets[next_tet as usize];
let fg_mininexttet = settings
.tile_coloring()
.simplified_tile_col(next_tet.into(), game.level());
#[rustfmt::skip] self.term_buf.write_char(w_tmp_ntl + 10 + 2 * (x_offset as u16), h_tmp_ntl + y_offset.saturating_sub(1), mini_tet, fg_mininexttet, None);
}
}
if let Some([c_f2_l, c_f2_b0, c_f2_b1, c_f2_r]) = tui_symbols.boardframe2 {
for dy in 0..H_PLAYFIELD + 1 {
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl.saturating_sub(1), h_tmp_btl + 1 + dy, c_f2_l, fg_boardframe, Some(bg_widget));
}
for dy in 0..H_PLAYFIELD + 1 {
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl + W_BOARD, h_tmp_btl + 1 + dy, c_f2_r, fg_boardframe, Some(bg_widget));
}
for dx in 0..W_PLAYFIELD {
if dx.is_multiple_of(2) {
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl + 1 + dx, h_tmp_btl + 1 + H_PLAYFIELD + 1, c_f2_b0, fg_boardframe, Some(bg_widget));
} else {
#[rustfmt::skip] self.term_buf.write_char(w_tmp_btl + 1 + dx, h_tmp_btl + 1 + H_PLAYFIELD + 1, c_f2_b1, fg_boardframe, Some(bg_widget));
}
}
}
let w_tmp_ftl = w_float + W_PAD_LEFT + w_addhud + W_HOLD + 1; let h_tmp_ftl = h_float + H_PAD_TOP + H_PLAYFIELD;
for dy in 0..PLAYABLE_BOARD_HEIGHT {
for dx in 0..BOARD_WIDTH {
let tile = if settings.graphics().show_grid {
tile_symbols.grid
} else {
TileTexture::SPACE
};
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * dx as u16, h_tmp_ftl.saturating_sub(dy as u16), tile, fg_grid, Some(bg_board));
}
}
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_type } = *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 fg_harddrop = if let Override(color_id) = recolor {
settings.tile_coloring().lookup_col_id(color_id, game.level())
} else {
settings.tile_coloring().simplified_tile_col(original_tile_type, game.level())
};
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), tile_texture, fg_harddrop, Some(bg_board));
true
});
!hard_drop_effect_tiles.is_empty()
});
if !temp_data.blindfold_game {
let mut y_highest_tile: isize = -1;
for (dy, (line, _is_frozen )) in game
.state()
.board
.iter()
.take(RENDERED_FIELD_HEIGHT)
.enumerate()
{
for (dx, tile) in line.iter().enumerate() {
if let Some(tile_type) = *tile {
let tile_texture = if settings.graphics().uniform_locked_tiles {
tile_symbols.locked(TileType::Generic)
} else {
tile_symbols.locked(tile_type)
};
let (fg_tile, opt_bg_tile) = if settings.graphics().uniform_locked_tiles {
settings.tile_coloring().uniform_tile(game.level())
} else {
settings.tile_coloring().tile_col(tile_type, game.level())
};
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), tile_texture, fg_tile, opt_bg_tile);
y_highest_tile = dy as isize;
}
}
}
if settings.graphics().show_spawn && !game.has_ended() {
if let Some(upcoming_tet) = game.state().tetromino_preview.front().copied() {
let spawn_piece = upcoming_tet.spawn_piece();
if spawn_piece.position.1 <= y_highest_tile + 4 {
for (dx, dy) in spawn_piece.coords() {
if game
.state()
.board
.get(dy as usize)
.is_some_and(|(line, _is_frozen)| line[dx as usize].is_some())
{
continue;
}
let tile_texture = tile_symbols.shadow;
let fg_shadow = settings
.tile_coloring()
.simplified_tile_col(upcoming_tet.into(), game.level());
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), tile_texture, fg_shadow, None);
}
}
}
}
}
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) in shadow_piece.coords() {
let tile_texture = tile_symbols.shadow;
let fg_shadow = settings
.tile_coloring()
.simplified_tile_col(shadow_piece.tetromino.into(), game.level());
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), tile_texture, fg_shadow, Some(bg_board));
}
}
for (dx, dy) in player_piece.coords() {
let tile_texture = tile_symbols.player(player_piece.tetromino);
let (fg_tile, opt_bg_tile) = settings
.tile_coloring()
.tile_col(player_piece.tetromino.into(), game.level());
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), tile_texture, fg_tile, opt_bg_tile);
}
if settings.graphics().show_lockdelay {
if !player_piece.is_airborne(&game.state().board) {
let elapsed = fall_or_lock_time
.saturating_sub(game.state().time)
.as_secs_f64();
let provided_lock_delay = game.state().lock_delay.as_secs_ennf64();
if !provided_lock_delay.is_zero()
&& !provided_lock_delay.is_infinite()
&& elapsed < provided_lock_delay.get()
{
let str = &tui_symbols.timer[((tui_symbols.timer.len() as f64)
* elapsed
/ provided_lock_delay.get()
- 1.0)
.ceil()
as usize];
#[rustfmt::skip] self.term_buf.write_str((w_tmp_ftl + 2 * (player_piece.position.0 as u16)).saturating_sub(1), h_tmp_ftl.saturating_sub(player_piece.position.1 as u16).saturating_add(1), str, fg_tui, Some(bg_board));
}
}
}
}
Phase::ClearingLines {
clear_finish_time: _,
point_bonus: _,
} => {}
Phase::GameEnd { cause, is_win: _ } => {
match cause {
GameEndCause::LockOut { locking_piece } => {
for (dx, dy) in locking_piece.coords() {
let tile_texture = tile_symbols.crossed;
let fg_crossed = settings
.tile_coloring()
.simplified_tile_col(locking_piece.tetromino.into(), game.level());
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), tile_texture, fg_crossed, None);
}
}
GameEndCause::BlockOut { blocked_piece } => {
for (dx, dy) in blocked_piece.coords() {
let (tile_texture, fg_blockd) = if let Some(blocking_tile_type) = game
.state()
.board
.get(dy as usize)
.and_then(|(line, _is_frozen)| line[dx as usize])
{
(
tile_symbols.crossed,
settings
.tile_coloring()
.simplified_tile_col(blocking_tile_type, game.level()),
)
} else {
(
tile_symbols.hatched,
settings.tile_coloring().simplified_tile_col(
blocked_piece.tetromino.into(),
game.level(),
),
)
};
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), tile_texture, fg_blockd, None);
}
}
GameEndCause::BufferOut => {
}
GameEndCause::Limit(_stat) => {}
GameEndCause::Forfeit { piece_in_play } => {
if let Some(forfeit_piece) = piece_in_play {
for (dx, dy) in forfeit_piece.coords() {
let tile_texture = tile_symbols.hatched;
let fg_forfeit = settings.tile_coloring().simplified_tile_col(
forfeit_piece.tetromino.into(),
game.level(),
);
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), tile_texture, fg_forfeit, None);
}
}
}
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_type } = *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 = if let Override(textur) = retexture {
textur
} else if settings.graphics().uniform_locked_tiles {
tile_symbols.locked(TileType::Generic)
} else {
tile_symbols.locked(original_tile_type)
};
let fg_lockeff = if let Override(color_id) = recolor {
settings.tile_coloring().lookup_col_id(color_id, game.level())
} else if settings.graphics().uniform_locked_tiles {
settings.tile_coloring().uniform_tile(game.level()).0
} else {
settings.tile_coloring().simplified_tile_col(original_tile_type, game.level())
};
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), tile_texture, fg_lockeff, None);
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_type) in line.iter().copied().enumerate() {
let tile_texture = if settings.graphics().uniform_locked_tiles {
tile_symbols.locked(TileType::Generic)
} else {
tile_symbols.locked(original_tile_type)
};
let (fg_lineclear, opt_bg_lineclear) = if !color_animation.is_empty() && let Override(color_id) = color_animation[(timeshift * (color_animation.len() - 1) as f32).round() as usize] {
(settings.tile_coloring().lookup_col_id(color_id, game.level()), Some(bg_board))
} else if settings.graphics().uniform_locked_tiles {
settings.tile_coloring().uniform_tile(game.level())
} else {
settings.tile_coloring().tile_col(original_tile_type, game.level())
};
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), tile_texture, fg_lineclear, opt_bg_lineclear);
}
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_tmp_ftl + (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), ' ', fg_grid, Some(bg_board));
}
}
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), original_tile_type } = *line_clear_effect_tile;
let lifetime = duration_override.unwrap_or(line_clear_duration);
if lifetime.is_zero() || animation.is_empty() {
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 = if settings.graphics().show_grid {
tile_symbols.grid
} else {
TileTexture::SPACE
};
#[rustfmt::skip] self.term_buf.write_tile(w_tmp_ftl + 2 * (dx as u16), h_tmp_ftl.saturating_sub(dy as u16), tile, fg_grid, Some(bg_board));
}
let (retexture, recolor) = animation[(timeshift * (animation.len() - 1) as f32).round() as usize];
let tile_texture = if let Override(textur) = retexture {
textur
} else if settings.graphics().uniform_locked_tiles {
tile_symbols.locked(TileType::Generic)
} else {
tile_symbols.locked(original_tile_type)
};
let (fg_lineclear, opt_bg_lineclear) = if let Override(color_id) = recolor {
(settings.tile_coloring().lookup_col_id(color_id, game.level()), None)
} else if settings.graphics().uniform_locked_tiles {
settings.tile_coloring().uniform_tile(game.level())
} else {
settings.tile_coloring().tile_col(original_tile_type, game.level())
};
let t = elapsed.as_secs_f32();
let x = (w_tmp_ftl + 2 * (dx as u16)) as f32 + m_x * t + a_x * t.powi(2) / 2.0;
let y = (h_tmp_ftl.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, fg_lineclear, opt_bg_lineclear);
true
});
!line_clear_effect_tiles.is_empty()
});
self.term_buf.flush(term)
}
}