use crate::palette::{Palette, HP_BAR_GREEN_PALETTE, HP_BAR_RED_PALETTE, HP_BAR_YELLOW_PALETTE};
use crate::text_renderer::{write_tiles_at, ScreenTileBuffer};
use crate::textbox::TILE_SPACE;
pub const TILE_HP_LABEL: u8 = 0x71; pub const TILE_HP_BAR_LEFT: u8 = 0x62; pub const TILE_HP_EMPTY: u8 = 0x63; pub const TILE_HP_FULL: u8 = 0x6B; pub const TILE_HP_END_CAP_BATTLE: u8 = 0x6D; pub const TILE_HP_END_CAP_ENEMY: u8 = 0x6C; pub const TILE_HP_PARTIAL_BASE: u8 = 0x63;
pub const TILE_HUD_CONNECTOR: u8 = 0x73; pub const TILE_HUD_HORIZONTAL: u8 = 0x76; pub const TILE_HUD_PLAYER_CORNER: u8 = 0x77; pub const TILE_HUD_PLAYER_TRIANGLE: u8 = 0x6F; pub const TILE_HUD_ENEMY_CORNER: u8 = 0x74; pub const TILE_HUD_ENEMY_TRIANGLE: u8 = 0x78;
pub const TILE_BALL_NORMAL: u8 = 0x31;
pub const TILE_BALL_STATUS: u8 = 0x32;
pub const TILE_BALL_FAINTED: u8 = 0x33;
pub const TILE_BALL_EMPTY: u8 = 0x34;
pub const TILE_BALL_RETREATED: u8 = 0x4C;
pub const BATTLE_HP_BAR_TILES: u32 = 6;
pub const BATTLE_HP_BAR_PIXELS: u32 = BATTLE_HP_BAR_TILES * 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HpBarColor {
Green = 0,
Yellow = 1,
Red = 2,
}
impl HpBarColor {
pub fn from_pixels(pixels: u32) -> Self {
if pixels >= 27 {
Self::Green
} else if pixels >= 10 {
Self::Yellow
} else {
Self::Red
}
}
pub fn from_hp(current_hp: u16, max_hp: u16) -> Self {
Self::from_pixels(calc_hp_bar_pixels(current_hp, max_hp))
}
pub fn palette(self) -> &'static Palette {
match self {
Self::Green => &HP_BAR_GREEN_PALETTE,
Self::Yellow => &HP_BAR_YELLOW_PALETTE,
Self::Red => &HP_BAR_RED_PALETTE,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusCondition {
Sleep,
Poison,
Burn,
Freeze,
Paralysis,
}
impl StatusCondition {
pub fn tiles(self) -> [u8; 3] {
match self {
Self::Sleep => [
0x92, 0x8B, 0x8F, ],
Self::Poison => [
0x8F, 0x92, 0x8D, ],
Self::Burn => [
0x81, 0x91, 0x8D, ],
Self::Freeze => [
0x85, 0x91, 0x99, ],
Self::Paralysis => [
0x8F, 0x80, 0x91, ],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BallStatus {
Normal,
StatusAilment,
Fainted,
Empty,
}
impl BallStatus {
pub fn tile(self) -> u8 {
match self {
Self::Normal => TILE_BALL_NORMAL,
Self::StatusAilment => TILE_BALL_STATUS,
Self::Fainted => TILE_BALL_FAINTED,
Self::Empty => TILE_BALL_EMPTY,
}
}
}
pub fn calc_hp_bar_pixels(current_hp: u16, max_hp: u16) -> u32 {
if max_hp == 0 || current_hp == 0 {
return 0;
}
let pixels = (current_hp as u32) * BATTLE_HP_BAR_PIXELS / (max_hp as u32);
pixels.max(1).min(BATTLE_HP_BAR_PIXELS)
}
pub fn draw_hp_bar(
buf: &mut ScreenTileBuffer,
x: u32,
y: u32,
current_hp: u16,
max_hp: u16,
end_cap_tile: u8,
show_hp_numbers: bool,
) -> HpBarColor {
let pixels = calc_hp_bar_pixels(current_hp, max_hp);
let color = HpBarColor::from_pixels(pixels);
buf.set(x, y, TILE_HP_LABEL);
buf.set(x + 1, y, TILE_HP_BAR_LEFT);
let mut remaining = pixels;
for i in 0..BATTLE_HP_BAR_TILES {
let tile = if remaining >= 8 {
remaining -= 8;
TILE_HP_FULL
} else if remaining > 0 {
let partial = remaining;
remaining = 0;
TILE_HP_PARTIAL_BASE + partial as u8 } else {
TILE_HP_EMPTY
};
buf.set(x + 2 + i, y, tile);
}
buf.set(x + 2 + BATTLE_HP_BAR_TILES, y, end_cap_tile);
if show_hp_numbers {
draw_hp_numbers(buf, x + 1, y + 1, current_hp, max_hp);
}
color
}
fn draw_hp_numbers(buf: &mut ScreenTileBuffer, x: u32, y: u32, current: u16, max: u16) {
let cur_str = format!("{:>3}", current.min(999));
let max_str = format!("{:>3}", max.min(999));
let combined = format!("{}/{}", cur_str, max_str);
let tiles: Vec<u8> = combined
.chars()
.filter_map(|c| match c {
'0'..='9' => Some(0xF6 + (c as u8 - b'0')),
'/' => Some(0xF3), ' ' => Some(TILE_SPACE),
_ => None,
})
.collect();
write_tiles_at(buf, x, y, &tiles);
}
pub struct PlayerHud;
impl PlayerHud {
pub const CLEAR_X: u32 = 9;
pub const CLEAR_Y: u32 = 7;
pub const CLEAR_W: u32 = 11;
pub const CLEAR_H: u32 = 5;
pub const NAME_X: u32 = 10;
pub const NAME_Y: u32 = 7;
pub const LEVEL_X: u32 = 14;
pub const LEVEL_Y: u32 = 8;
pub const HP_BAR_X: u32 = 10;
pub const HP_BAR_Y: u32 = 9;
pub fn clear(buf: &mut ScreenTileBuffer) {
for row in 0..Self::CLEAR_H {
for col in 0..Self::CLEAR_W {
buf.set(Self::CLEAR_X + col, Self::CLEAR_Y + row, TILE_SPACE);
}
}
}
pub fn draw_border(buf: &mut ScreenTileBuffer) {
buf.set(18, 9, TILE_HUD_CONNECTOR);
buf.set(18, 10, TILE_HUD_CONNECTOR);
buf.set(18, 11, TILE_HUD_PLAYER_CORNER);
for i in 0..8 {
buf.set(17 - i, 11, TILE_HUD_HORIZONTAL);
}
buf.set(9, 11, TILE_HUD_PLAYER_TRIANGLE);
}
pub fn draw(
buf: &mut ScreenTileBuffer,
name_tiles: &[u8],
level: u8,
status_tiles: Option<&[u8]>,
current_hp: u16,
max_hp: u16,
) -> HpBarColor {
Self::clear(buf);
Self::draw_border(buf);
let name_offset = center_name_offset(name_tiles.len());
write_tiles_at(buf, Self::NAME_X + name_offset, Self::NAME_Y, name_tiles);
if let Some(status) = status_tiles {
write_tiles_at(buf, Self::LEVEL_X + 1, Self::LEVEL_Y, status);
} else {
draw_level(buf, Self::LEVEL_X, Self::LEVEL_Y, level);
}
draw_hp_bar(
buf,
Self::HP_BAR_X,
Self::HP_BAR_Y,
current_hp,
max_hp,
TILE_HP_END_CAP_BATTLE,
true,
)
}
}
fn center_name_offset(name_len: usize) -> u32 {
match name_len {
0..=2 => 2,
3..=4 => 1,
_ => 0,
}
}
pub struct EnemyHud;
impl EnemyHud {
pub const CLEAR_X: u32 = 0;
pub const CLEAR_Y: u32 = 0;
pub const CLEAR_W: u32 = 12;
pub const CLEAR_H: u32 = 4;
pub const NAME_X: u32 = 1;
pub const NAME_Y: u32 = 0;
pub const LEVEL_X: u32 = 4;
pub const LEVEL_Y: u32 = 1;
pub const HP_BAR_X: u32 = 2;
pub const HP_BAR_Y: u32 = 2;
pub fn clear(buf: &mut ScreenTileBuffer) {
for row in 0..Self::CLEAR_H {
for col in 0..Self::CLEAR_W {
buf.set(Self::CLEAR_X + col, Self::CLEAR_Y + row, TILE_SPACE);
}
}
}
pub fn draw_border(buf: &mut ScreenTileBuffer) {
buf.set(1, 2, TILE_HUD_CONNECTOR);
buf.set(1, 3, TILE_HUD_ENEMY_CORNER);
for i in 0..8 {
buf.set(2 + i, 3, TILE_HUD_HORIZONTAL);
}
buf.set(10, 3, TILE_HUD_ENEMY_TRIANGLE);
}
pub fn draw(
buf: &mut ScreenTileBuffer,
name_tiles: &[u8],
level: u8,
status_tiles: Option<&[u8]>,
current_hp: u16,
max_hp: u16,
) -> HpBarColor {
Self::clear(buf);
Self::draw_border(buf);
let name_offset = center_name_offset(name_tiles.len());
write_tiles_at(buf, Self::NAME_X + name_offset, Self::NAME_Y, name_tiles);
if let Some(status) = status_tiles {
write_tiles_at(buf, Self::LEVEL_X + 1, Self::LEVEL_Y, status);
} else {
draw_level(buf, Self::LEVEL_X, Self::LEVEL_Y, level);
}
draw_hp_bar(
buf,
Self::HP_BAR_X,
Self::HP_BAR_Y,
current_hp,
max_hp,
TILE_HP_END_CAP_ENEMY,
false,
)
}
}
pub fn draw_level(buf: &mut ScreenTileBuffer, x: u32, y: u32, level: u8) {
buf.set(x, y, 0x6E); if level >= 100 {
let h = level / 100;
let t = (level % 100) / 10;
let o = level % 10;
buf.set(x + 1, y, 0xF6 + h);
buf.set(x + 2, y, 0xF6 + t);
buf.set(x + 3, y, 0xF6 + o);
} else if level >= 10 {
let t = level / 10;
let o = level % 10;
buf.set(x + 1, y, 0xF6 + t);
buf.set(x + 2, y, 0xF6 + o);
} else {
buf.set(x + 1, y, 0xF6 + level);
}
}
pub struct BallIndicators;
impl BallIndicators {
pub const PLAYER_BASE_X: u32 = 10;
pub const PLAYER_BASE_Y: u32 = 11;
pub const ENEMY_BASE_X: u32 = 7;
pub const ENEMY_BASE_Y: u32 = 4;
pub fn draw_player(buf: &mut ScreenTileBuffer, party_status: &[BallStatus]) {
for i in 0..6 {
let status = party_status
.get(i)
.copied()
.unwrap_or(BallStatus::Empty);
let x = Self::PLAYER_BASE_X + i as u32;
if x < buf.width_tiles {
buf.set(x, Self::PLAYER_BASE_Y, status.tile());
}
}
}
pub fn draw_enemy(buf: &mut ScreenTileBuffer, party_status: &[BallStatus]) {
for i in 0..6 {
let status = party_status
.get(i)
.copied()
.unwrap_or(BallStatus::Empty);
let x = Self::ENEMY_BASE_X.wrapping_sub(i as u32);
if x < buf.width_tiles {
buf.set(x, Self::ENEMY_BASE_Y, status.tile());
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetreatStage {
Full, Medium, Small, Ball, }
impl RetreatStage {
pub fn layout(self) -> (u32, u32, u32, u32) {
match self {
Self::Full => (1, 5, 7, 7),
Self::Medium => (3, 7, 5, 5),
Self::Small => (4, 9, 3, 3),
Self::Ball => (5, 11, 1, 1),
}
}
pub fn next(self) -> Option<Self> {
match self {
Self::Full => Some(Self::Medium),
Self::Medium => Some(Self::Small),
Self::Small => Some(Self::Ball),
Self::Ball => None,
}
}
}
pub fn draw_retreat_stage(buf: &mut ScreenTileBuffer, stage: RetreatStage) {
for row in 0..7 {
for col in 0..7 {
buf.set(1 + col, 5 + row, TILE_SPACE);
}
}
if stage == RetreatStage::Ball {
buf.set(5, 11, TILE_BALL_RETREATED);
}
}
pub fn scale_sprite_2x(src_pixels: &[u8], src_w: usize, src_h: usize) -> Vec<u8> {
let dst_w = src_w * 2;
let dst_h = src_h * 2;
let mut dst = vec![0u8; dst_w * dst_h];
for sy in 0..src_h {
for sx in 0..src_w {
let color = src_pixels[sy * src_w + sx];
let dx = sx * 2;
let dy = sy * 2;
dst[dy * dst_w + dx] = color;
dst[dy * dst_w + dx + 1] = color;
dst[(dy + 1) * dst_w + dx] = color;
dst[(dy + 1) * dst_w + dx + 1] = color;
}
}
dst
}
pub struct BattleSpritePositions;
impl BattleSpritePositions {
pub const PLAYER_SPRITE_X: u32 = 1;
pub const PLAYER_SPRITE_Y: u32 = 5;
pub const PLAYER_SPRITE_SIZE: u32 = 7;
pub const ENEMY_SPRITE_X: u32 = 12;
pub const ENEMY_SPRITE_Y: u32 = 0;
pub const ENEMY_SPRITE_SIZE: u32 = 7;
pub const SEND_OUT_X: u32 = 4;
pub const SEND_OUT_Y: u32 = 11;
}
pub struct TrainerPicScroll {
pub columns_remaining: u32,
pub screen_tiles_x: u32,
}
impl TrainerPicScroll {
pub const TOTAL_COLUMNS: u32 = 7;
pub const START_ROW: u32 = 0;
pub const PIC_HEIGHT: u32 = 7;
pub fn new(screen_tiles_x: u32) -> Self {
Self {
columns_remaining: Self::TOTAL_COLUMNS,
screen_tiles_x,
}
}
pub fn is_scrolling(&self) -> bool {
self.columns_remaining > 0
}
pub fn step(&mut self) -> u32 {
if self.columns_remaining > 0 {
self.columns_remaining -= 1;
}
Self::TOTAL_COLUMNS - self.columns_remaining
}
pub fn visible_start_x(&self) -> u32 {
self.screen_tiles_x - (Self::TOTAL_COLUMNS - self.columns_remaining)
}
pub fn draw_visible(&self, buf: &mut ScreenTileBuffer, pic_tiles: &[u8]) {
let visible_cols = Self::TOTAL_COLUMNS - self.columns_remaining;
if visible_cols == 0 {
return;
}
let start_x = self.visible_start_x();
for row in 0..Self::PIC_HEIGHT {
for col in 0..visible_cols {
let tile_idx = (row * Self::TOTAL_COLUMNS + col) as usize;
if tile_idx < pic_tiles.len() {
let screen_x = start_x + col;
let screen_y = Self::START_ROW + row;
if screen_x < buf.width_tiles && screen_y < buf.height_tiles {
buf.set(screen_x, screen_y, pic_tiles[tile_idx]);
}
}
}
}
}
}
impl Default for TrainerPicScroll {
fn default() -> Self {
Self::new(20)
}
}
pub struct BattleScene {
pub show_player_hud: bool,
pub show_enemy_hud: bool,
pub player_party: [BallStatus; 6],
pub enemy_party: [BallStatus; 6],
pub retreat_stage: Option<RetreatStage>,
pub trainer_scroll: Option<TrainerPicScroll>,
}
impl BattleScene {
pub fn new() -> Self {
Self {
show_player_hud: false,
show_enemy_hud: false,
player_party: [BallStatus::Empty; 6],
enemy_party: [BallStatus::Empty; 6],
retreat_stage: None,
trainer_scroll: None,
}
}
pub fn draw(
&self,
buf: &mut ScreenTileBuffer,
player_name: &[u8],
player_level: u8,
player_status: Option<&[u8]>,
player_hp: u16,
player_max_hp: u16,
enemy_name: &[u8],
enemy_level: u8,
enemy_status: Option<&[u8]>,
enemy_hp: u16,
enemy_max_hp: u16,
) {
if self.show_player_hud {
PlayerHud::draw(
buf,
player_name,
player_level,
player_status,
player_hp,
player_max_hp,
);
BallIndicators::draw_player(buf, &self.player_party);
}
if self.show_enemy_hud {
EnemyHud::draw(
buf,
enemy_name,
enemy_level,
enemy_status,
enemy_hp,
enemy_max_hp,
);
BallIndicators::draw_enemy(buf, &self.enemy_party);
}
if let Some(stage) = self.retreat_stage {
draw_retreat_stage(buf, stage);
}
}
}
impl Default for BattleScene {
fn default() -> Self {
Self::new()
}
}