use crate::app::{Screen, Sim, layout};
use bevy::asset::uuid_handle;
use bevy::prelude::*;
use bevy::text::FontCx;
use parlance::Script;
pub(super) fn setup_camera(mut commands: Commands) {
commands.spawn(Camera2d);
}
pub const ITALIC_FONT: Handle<Font> = uuid_handle!("9b451db2-8b49-4ad5-9794-ac1ce2ac943e");
pub const JP_FONT: Handle<Font> = uuid_handle!("6c2f0e33-3d4b-4d2e-9a58-6f2b2c9d4d71");
pub const DISPLAY_FONT: Handle<Font> = uuid_handle!("6cf5591f-337a-4b2e-bb6a-697aece4ab2b");
const JP_FAMILY: &str = "Noto Sans Mono CJK JP";
const JP_SCRIPTS: [Script; 3] = [
Script::from_bytes(*b"Hira"),
Script::from_bytes(*b"Kana"),
Script::from_bytes(*b"Hani"),
];
pub(super) fn install_ui_font(mut fonts: ResMut<Assets<Font>>) {
let upright = include_bytes!("../../assets/fonts/DejaVuSansMono.ttf");
if fonts
.insert(
&Handle::<Font>::default(),
Font::from_bytes(upright.to_vec()),
)
.is_err()
{
warn!("UI font failed to install; accented glyphs will be missing");
}
let slanted = include_bytes!("../../assets/fonts/DejaVuSansMono-Oblique.ttf");
if fonts
.insert(&ITALIC_FONT, Font::from_bytes(slanted.to_vec()))
.is_err()
{
warn!("italic UI font failed to install; notices will read upright");
}
let display = include_bytes!("../../assets/fonts/Nunito-ExtraBold.ttf");
if fonts
.insert(&DISPLAY_FONT, Font::from_bytes(display.to_vec()))
.is_err()
{
warn!("display font failed to install; headings will read in the UI face");
}
let kanji = include_bytes!("../../assets/fonts/NotoSansMonoCJKjp-Subset.otf");
if fonts
.insert(&JP_FONT, Font::from_bytes(kanji.to_vec()))
.is_err()
{
warn!("Japanese font failed to install; Japanese will draw blank");
}
}
pub(super) fn teach_the_kanji_fallback(
mut fonts: ResMut<FontCx>,
mut lines: Query<&mut TextFont>,
mut taught: Local<bool>,
) {
if *taught {
return;
}
let Some(family) = fonts.collection.family_id(JP_FAMILY) else {
return;
};
for script in JP_SCRIPTS {
fonts
.collection
.append_fallbacks(script, std::iter::once(family));
}
for mut line in &mut lines {
line.set_changed();
}
*taught = true;
}
pub(super) fn fit_camera(
sim: Res<Sim>,
screen: Res<State<Screen>>,
ui_scale: Res<UiScale>,
windows: Query<&Window>,
mut cameras: Query<(&mut Projection, &mut Transform), With<Camera2d>>,
) {
let Ok(window) = windows.single() else {
return;
};
let menu = crate::app::postcard_screen(*screen.get());
let chrome = screen.get().chrome();
let ui = ui_scale.0;
let (chrome_w, chrome_top, chrome_bottom) =
(chrome.width * ui, chrome.top * ui, chrome.bottom * ui);
let chrome_h = chrome_top + chrome_bottom;
let board_w = f32::from(sim.0.width()) * layout::TILE;
let board_h = f32::from(sim.0.height()) * layout::TILE;
let fit_w = (window.width() - chrome_w).max(layout::TILE);
let fit_h = (window.height() - chrome_h).max(layout::TILE);
let scale = if menu {
1.0 / ui.max(f32::EPSILON)
} else {
board_scale(
Vec2::new(board_w, board_h),
Vec2::new(fit_w, fit_h),
window.scale_factor(),
)
};
let offset = Vec2::new(
0.0,
if menu {
0.0
} else {
(chrome_top - chrome_bottom) / 2.0
},
);
for (mut projection, mut transform) in &mut cameras {
if let Projection::Orthographic(ortho) = &mut *projection
&& (ortho.scale - scale).abs() > 0.001
{
ortho.scale = scale;
}
let target = Vec2::new(-offset.x, offset.y) * scale;
if transform.translation.truncate().distance(target) > 0.5 {
transform.translation.x = target.x;
transform.translation.y = target.y;
}
}
}
fn board_scale(board: Vec2, fit: Vec2, scale_factor: f32) -> f32 {
let fitted = |board: Vec2| {
let rim = Vec2::splat(2.0 * crate::app::board_render::RIM);
let grid = board / fit;
let framed = (board + rim) / (fit + rim);
grid.max_element()
.max(framed.max_element())
.max(scale_factor / layout::MAX_ZOOM)
};
let small = fitted(layout::SMALLEST_FIT).min(1.0 / layout::SMALL_BOARD_ZOOM);
fitted(board).max(small)
}
pub(super) fn shake_camera(
time: Res<Time>,
settings: Res<crate::app::settings::GameSettings>,
mut trauma: ResMut<crate::app::effects::Trauma>,
mut cameras: Query<(&Projection, &mut Transform), With<Camera2d>>,
) {
let offset = trauma.offset(time.delta_secs(), time.elapsed_secs());
if offset == Vec2::ZERO || settings.reduced_motion || crate::app::dev::screenshotting() {
return;
}
for (projection, mut transform) in &mut cameras {
let scale = match projection {
Projection::Orthographic(ortho) => ortho.scale,
Projection::Perspective(_) | Projection::Custom(_) => 1.0,
};
transform.translation.x += offset.x * scale;
transform.translation.y += offset.y * scale;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::board_render::RIM;
fn old_scale(board: Vec2, fit: Vec2) -> f32 {
(board / fit).max_element().max(0.8)
}
#[test]
fn a_720p_window_fits_as_it_did() {
let fit = Vec2::new(1280.0 - 490.0, 720.0 - 137.0);
let xl = Vec2::new(20.0, 13.0) * layout::TILE;
assert!(old_scale(xl, fit) > 1.0, "the XL beach is zoomed out");
assert_eq!(board_scale(xl, fit, 1.0), old_scale(xl, fit));
let classic = Vec2::new(12.0, 9.0) * layout::TILE;
let (new, old) = (board_scale(classic, fit, 1.0), old_scale(classic, fit));
assert!((new / old - 1.0).abs() < 0.01, "{new} against {old}");
}
#[test]
fn a_small_board_keeps_its_old_zoom_in_a_small_window() {
let fit = Vec2::new(1280.0 - 490.0, 720.0 - 137.0);
let first = Vec2::new(5.0, 3.0) * layout::TILE;
assert_eq!(board_scale(first, fit, 1.0), old_scale(first, fit));
}
#[test]
fn a_small_board_grows_no_further_than_the_classic_one() {
let fit = Vec2::new(1920.0 - 490.0, 1152.0 - 137.0);
let classic = board_scale(Vec2::new(12.0, 9.0) * layout::TILE, fit, 1.0);
for tiles in [
Vec2::new(5.0, 3.0),
Vec2::new(8.0, 6.0),
Vec2::new(12.0, 5.0),
] {
let scale = board_scale(tiles * layout::TILE, fit, 1.0);
assert_eq!(scale, classic, "{tiles}");
}
let long = board_scale(Vec2::new(20.0, 5.0) * layout::TILE, fit, 1.0);
assert!(long > classic, "a 20-wide board zooms out further: {long}");
}
#[test]
fn a_tile_stops_at_its_sprite() {
let board = Vec2::new(12.0, 9.0) * layout::TILE;
let fit = Vec2::new(6000.0, 4000.0);
for scale_factor in [1.0, 1.5, 2.0] {
let scale = board_scale(board, fit, scale_factor);
let physical = layout::TILE / scale * scale_factor;
assert!(
(physical - layout::SPRITE_PX).abs() < 0.01,
"{physical} px at {scale_factor}x"
);
}
}
#[test]
fn a_zoomed_board_keeps_its_rim_in_the_room_it_had() {
let board = Vec2::new(12.0, 9.0) * layout::TILE;
let fit = Vec2::new(1920.0 - 490.0, 1152.0 - 137.0);
let scale = board_scale(board, fit, 1.0);
assert!(scale < 0.8, "a 1080p-class window zooms past the old cap");
let drawn = (board + Vec2::splat(2.0 * RIM)) / scale;
let room = fit + Vec2::splat(2.0 * RIM);
assert!(
drawn.x <= room.x + 0.01 && drawn.y <= room.y + 0.01,
"{drawn} in {room}"
);
}
}