Bestiary portraits in the high-res style: a painted pixel-art bust for every creature

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 00:05:41 +01:00
1 parent a24fc51605
commit 8d2362fc11
41 files changed
+308 -1

No files matched your search

+1
View File
@@ -28,6 +28,7 @@ mod menu;
mod netplay;
mod personality;
mod platform;
mod portraits;
mod orders;
mod particles;
mod props;
+25 -1
View File
@@ -174,7 +174,19 @@ impl Menu {
_ => level,
};
match entry {
Entry::Unit(kind, _) => draw_sprite(gfx, kind, show_level, x + k, y + k, pic),
Entry::Unit(kind, _) => {
// the high-res style shows the creature's portrait filling the box
let box_size = pic + 2.0 * k;
match crate::portraits::rect(kind).filter(|_| crate::gfx::high_res_ui()) {
Some(src) => {
let m = 2.0;
draw_texture_ex(&crate::portraits::atlas(), x + m, y + m, WHITE, DrawTextureParams { source: Some(src), dest_size: Some(vec2(box_size - 2.0 * m, box_size - 2.0 * m)), ..Default::default() });
draw_rectangle_lines(x + m, y + m, box_size - 2.0 * m, box_size - 2.0 * m, 2.0, Color::new(0.0, 0.0, 0.0, 0.8));
draw_rectangle_lines(x, y, box_size, box_size, 2.0, FRAME);
}
None => draw_sprite(gfx, kind, show_level, x + k, y + k, pic),
}
}
Entry::Rank(l) => draw_sprite(gfx, EK::Ogre, l, x + k, y + k, pic),
Entry::Building(i) => {
let icon = tool_info(TOOLS[i]).icon;
@@ -321,3 +333,15 @@ fn draw_sprite(gfx: &Gfx, kind: EK, level: u32, x: f32, y: f32, size: f32) {
let src = Gfx::sprite_cell(kind, false, level);
draw_texture_ex(&gfx.sprites, x, y, WHITE, DrawTextureParams { source: Some(src), dest_size: Some(vec2(size, size)), ..Default::default() });
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_bestiary_unit_has_a_portrait() {
for &k in CREATURES.iter().chain(HUMANS.iter()) {
assert!(crate::portraits::rect(k).is_some(), "no portrait for {:?}", k);
}
}
}
+57
View File
@@ -0,0 +1,57 @@
// Bestiary portraits for the high-res style: one 128x128 pixel-art bust per creature kind in one
// atlas (8 columns), generated from the creatures' sprites (see art/generated/portraits/).
use crate::game::EK;
use macroquad::prelude::*;
const PNG: &[u8] = include_bytes!("../assets/portraits/portraits.png");
pub const CELL: f32 = 128.0;
const COLS: usize = 8;
// the atlas order, the same as assets/portraits/order.txt
const ORDER: [EK; 31] = [
EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Knight, EK::Archer, EK::Priest,
EK::Dwarf, EK::Paladin, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Bat, EK::Gargoyle, EK::Owl,
EK::Wyrm, EK::Cogling, EK::Revenant, EK::Giant, EK::Fairy, EK::BattleMage, EK::Ram, EK::Catapult,
EK::CaveBear, EK::Beetle, EK::Trader, EK::Thief, EK::Monk, EK::Ranger, EK::Inquisitor,
];
thread_local! {
static ATLAS: Texture2D = {
let t = Texture2D::from_file_with_format(PNG, Some(ImageFormat::Png));
t.set_filter(FilterMode::Nearest);
t
};
}
// the atlas texture (loaded on first use)
pub fn atlas() -> Texture2D {
ATLAS.with(|t| t.clone())
}
// the source rect of a kind's portrait in the atlas, None for kinds without one
pub fn rect(kind: EK) -> Option<Rect> {
let i = ORDER.iter().position(|&k| k == kind)?;
Some(Rect::new((i % COLS) as f32 * CELL, (i / COLS) as f32 * CELL, CELL, CELL))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn atlas_decodes_with_room_for_every_portrait() {
let img = Image::from_file_with_format(PNG, Some(ImageFormat::Png)).expect("atlas decodes");
for &k in ORDER.iter() {
let r = rect(k).unwrap();
assert!(r.x + r.w <= img.width as f32 && r.y + r.h <= img.height as f32, "{:?} outside the atlas", k);
}
}
#[test]
fn order_matches_list() {
let list: Vec<&str> = include_str!("../assets/portraits/order.txt").lines().collect();
let ours: Vec<String> = ORDER.iter().map(|k| format!("{:?}", k)).collect();
assert_eq!(list, ours);
}
}