Switch to the Dinowyde font with DINOBYTE for missing glyphs
Unicode glyph lookup instead of CP437 codes, rope and double-strand frames with clean title ends, progress bars in eighth blocks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d99166a746
commit
d76e74b4b0
11 files changed
+223
-63
No files matched your search
+8
-2
@@ -1,9 +1,15 @@
|
|||||||
# Credits
|
# Credits
|
||||||
|
|
||||||
## Font: DINOBYTE bitmap font
|
## Font: Dinowyde bitmap font
|
||||||
|
- Author: **mby** – https://mby.itch.io/
|
||||||
|
- License: **Creative Commons Attribution 4.0 International (CC BY 4.0)** – https://creativecommons.org/licenses/by/4.0/
|
||||||
|
- File: `assets/dinowyde_1x.png` (unmodified, work-in-progress version of the font). 6x8 px glyphs ordered by Unicode: Latin with all European accents, Greek, Cyrillic and many symbols. In the game it is scaled by integer factors and tinted; characters the sheet does not draw yet (arrows, card suits, smileys, notes, ...) come from DINOBYTE (below).
|
||||||
|
- `assets/dinowyde_wip.png` is the font's overview sheet (which Unicode blocks are where).
|
||||||
|
|
||||||
|
## Font: DINOBYTE bitmap font (for the characters Dinowyde does not have yet)
|
||||||
- Author: **mby** – https://mby.itch.io/dinobyte
|
- Author: **mby** – https://mby.itch.io/dinobyte
|
||||||
- License: **Creative Commons Attribution 4.0 International (CC BY 4.0)** – https://creativecommons.org/licenses/by/4.0/
|
- License: **Creative Commons Attribution 4.0 International (CC BY 4.0)** – https://creativecommons.org/licenses/by/4.0/
|
||||||
- File: `assets/dbyte_1x.png` (unmodified). In the game it is scaled by integer factors and tinted. The swapped glyphs ö/ò are corrected in the character mapping, not in the file.
|
- File: `assets/dbyte_1x.png` (unmodified, CP437). The swapped glyphs ö/ò are corrected in the character mapping, not in the file.
|
||||||
|
|
||||||
## Made with Claude Code
|
## Made with Claude Code
|
||||||
The game was developed with the help of [Claude Code](https://claude.com/claude-code), Anthropic's AI coding assistant. The small pixel-art mark next to the note on the title and login screens is a rendition of the Claude Code logo. Claude, Claude Code and Anthropic are trademarks of Anthropic; the mark only credits the tool and is not covered by the MIT licence of this project.
|
The game was developed with the help of [Claude Code](https://claude.com/claude-code), Anthropic's AI coding assistant. The small pixel-art mark next to the note on the title and login screens is a rendition of the Claude Code logo. Claude, Claude Code and Anthropic are trademarks of Anthropic; the mark only credits the tool and is not covered by the MIT licence of this project.
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ An original dungeon management game (genre: dungeon building & defence) that run
|
|||||||
## Technology
|
## Technology
|
||||||
- Rust + **macroquad** (WebGL), target `wasm32-unknown-unknown`
|
- Rust + **macroquad** (WebGL), target `wasm32-unknown-unknown`
|
||||||
- User interface drawn entirely **in the canvas**
|
- User interface drawn entirely **in the canvas**
|
||||||
- Font: **DINOBYTE** bitmap font (CP437, 6×8 px, CC BY 4.0, see CREDITS.md) for all texts, integer scaled
|
- Font: **Dinowyde** bitmap font (Unicode: Latin, Greek, Cyrillic, symbols; 6×8 px, CC BY 4.0, see CREDITS.md) for all texts, integer scaled; missing glyphs (arrows, suits, ...) from DINOBYTE; frames: rope (single) and double strand, bars in eighth blocks
|
||||||
- All UI windows use **CP437 frames** (single lines for panels, double lines for dialogs); **all bars are CP437 block characters** (█ ▌ ░)
|
- All UI windows use **box-drawing frames** of the font (single lines for panels, double lines for dialogs); **all bars are block characters** (█ ▌ ░)
|
||||||
- Text only in **fixed integer sizes**: 1×, 2× and 4× the 6×8 glyph size, times an integer **interface factor** (automatic from the device resolution, or 1×–4× in the settings)
|
- Text only in **fixed integer sizes**: 1×, 2× and 4× the 6×8 glyph size, times an integer **interface factor** (automatic from the device resolution, or 1×–4× in the settings)
|
||||||
- The interface is drawn in **device pixels** (high DPI) and laid out on the **character grid**, so every font pixel has exactly the same size
|
- The interface is drawn in **device pixels** (high DPI) and laid out on the **character grid**, so every font pixel has exactly the same size
|
||||||
- **3D resolution** setting: the world can be rendered at 1/1 down to 1/16 resolution and scaled up by exactly that factor (pixel look); the setting refers to a window 1080 device pixels high and scales with the window size, so the 3D pixels keep their size relative to the window; the interface stays sharp
|
- **3D resolution** setting: the world can be rendered at 1/1 down to 1/16 resolution and scaled up by exactly that factor (pixel look); the setting refers to a window 1080 device pixels high and scales with the window size, so the 3D pixels keep their size relative to the window; the interface stays sharp
|
||||||
|
|||||||
+2
-1
@@ -131,7 +131,8 @@ number by one (a new month starts again at `00001`) and prints the old and the n
|
|||||||
|
|
||||||
1. Copy `lang/en.lang` to `lang/<code>.lang` and translate the texts (placeholders `{0}`, `{1}` stay as they are).
|
1. Copy `lang/en.lang` to `lang/<code>.lang` and translate the texts (placeholders `{0}`, `{1}` stay as they are).
|
||||||
2. Add the file to `SOURCES` in `src/i18n.rs`.
|
2. Add the file to `SOURCES` in `src/i18n.rs`.
|
||||||
3. Note: the DINOBYTE font only covers the CP437 character set.
|
3. The Dinowyde font covers Latin (all European accents), Greek and Cyrillic; the test
|
||||||
|
`every_text_of_the_game_has_its_glyphs` in `src/gfx.rs` lists characters it cannot draw (add the new file there).
|
||||||
|
|
||||||
## Refreshing the generated files
|
## Refreshing the generated files
|
||||||
|
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ marks on the title screen only credit the tools.
|
|||||||
DungeonCraft is released under the **[MIT License](LICENSE)** - © 2026 Richard Zink. It covers the code, the generated
|
DungeonCraft is released under the **[MIT License](LICENSE)** - © 2026 Richard Zink. It covers the code, the generated
|
||||||
music in `assets/music/` and the procedural graphics.
|
music in `assets/music/` and the procedural graphics.
|
||||||
|
|
||||||
The **DINOBYTE** font (`assets/dbyte_1x.png`) by [mby](https://mby.itch.io/dinobyte) is licensed separately under
|
The **Dinowyde** and **DINOBYTE** fonts (`assets/dinowyde_1x.png`, `assets/dbyte_1x.png`) by [mby](https://mby.itch.io/) are licensed separately under
|
||||||
**CC BY 4.0**. Details in [CREDITS.md](CREDITS.md).
|
**CC BY 4.0**. Details in [CREDITS.md](CREDITS.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
File renamed without changes.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
+1
-1
@@ -153,7 +153,7 @@ help.s2 = 2. Baue Quartier, Pilzgarten und Schatzkammer.
|
|||||||
help.s3 = 3. Trainiere deine Kreaturen, zahle ihren Lohn pünktlich.
|
help.s3 = 3. Trainiere deine Kreaturen, zahle ihren Lohn pünktlich.
|
||||||
help.s4 = 4. Wehre die Heldenwellen ab. Sie passen sich deiner Armee an.
|
help.s4 = 4. Wehre die Heldenwellen ab. Sie passen sich deiner Armee an.
|
||||||
help.s5 = 5. Setze die Angriffsflagge nahe ans Tor und greife an!
|
help.s5 = 5. Setze die Angriffsflagge nahe ans Tor und greife an!
|
||||||
help.font = Schrift: DINOBYTE von mby (CC BY 4.0)
|
help.font = Schriften: Dinowyde und DINOBYTE von mby (CC BY 4.0)
|
||||||
help.start = Klick zum Starten
|
help.start = Klick zum Starten
|
||||||
|
|
||||||
# ---- Spielende
|
# ---- Spielende
|
||||||
|
|||||||
+1
-1
@@ -154,7 +154,7 @@ help.s2 = 2. Build a lair, a mushroom garden and a treasury.
|
|||||||
help.s3 = 3. Train your creatures and pay their wages on time.
|
help.s3 = 3. Train your creatures and pay their wages on time.
|
||||||
help.s4 = 4. Fend off the hero waves. They adapt to your army.
|
help.s4 = 4. Fend off the hero waves. They adapt to your army.
|
||||||
help.s5 = 5. Place the attack flag near the gate and attack!
|
help.s5 = 5. Place the attack flag near the gate and attack!
|
||||||
help.font = Font: DINOBYTE by mby (CC BY 4.0)
|
help.font = Fonts: Dinowyde and DINOBYTE by mby (CC BY 4.0)
|
||||||
help.start = Click to start
|
help.start = Click to start
|
||||||
|
|
||||||
# ---- end screen
|
# ---- end screen
|
||||||
|
|||||||
+189
-37
@@ -85,53 +85,136 @@ pub struct Gfx {
|
|||||||
pub atlas: Texture2D,
|
pub atlas: Texture2D,
|
||||||
pub sprites: Texture2D,
|
pub sprites: Texture2D,
|
||||||
pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*)
|
pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*)
|
||||||
pub font: Texture2D, // DINOBYTE, CP437, 16x16 glyphs of 6x8 px
|
pub font: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE
|
||||||
|
pub font_map: FontMap, // which glyphs the two fonts hold
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ bitmap font (CP437)
|
// ------------------------------------------------------------------ bitmap font (Dinowyde)
|
||||||
|
//
|
||||||
|
// assets/dinowyde_1x.png: 6x8 px glyphs, ordered by Unicode in five columns of 16 glyphs
|
||||||
|
// (96 px) and 48 rows each:
|
||||||
|
// column 0: U+0000-02FF (Basic Latin, Latin-1, Latin Extended A/B, IPA, spacing modifiers)
|
||||||
|
// column 1: U+0310-05FF (combining marks from U+0310, Greek, Cyrillic, Armenian, Hebrew;
|
||||||
|
// U+0300-030F have no slot)
|
||||||
|
// column 2: U+0600-06FF (Arabic) in rows 0-15, U+2000-21FF (punctuation ... arrows) in rows 16-47
|
||||||
|
// column 3: U+2200-24FF (mathematical operators, technical, ...)
|
||||||
|
// column 4: U+2500-27FF (box drawing, blocks, geometric shapes, ...)
|
||||||
|
|
||||||
pub const GW: f32 = 6.0; // glyph width
|
pub const GW: f32 = 6.0; // glyph width
|
||||||
pub const GH: f32 = 8.0; // glyph height
|
pub const GH: f32 = 8.0; // glyph height
|
||||||
|
const FONT_COLS: usize = 80; // glyphs per row of the sheet
|
||||||
|
const FONT_ROWS: usize = 48;
|
||||||
|
|
||||||
|
// place of a character in the sheet (glyph index row * FONT_COLS + column), if the sheet has a slot for it
|
||||||
|
pub fn font_slot(ch: char) -> Option<usize> {
|
||||||
|
let u = ch as usize;
|
||||||
|
let (block, off) = match u {
|
||||||
|
0x0000..=0x02FF => (0, u),
|
||||||
|
0x0310..=0x05FF => (1, u - 0x0310),
|
||||||
|
0x0600..=0x06FF => (2, u - 0x0600),
|
||||||
|
0x2000..=0x21FF => (2, 0x100 + u - 0x2000),
|
||||||
|
0x2200..=0x24FF => (3, u - 0x2200),
|
||||||
|
0x2500..=0x27FF => (4, u - 0x2500),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some((off / 16) * FONT_COLS + block * 16 + off % 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Characters Dinowyde does not draw yet (arrows, card suits, smileys, notes, ...) come from the
|
||||||
|
// DINOBYTE font (assets/dbyte_1x.png, CP437 order, 16x16 glyphs), which sits right of the
|
||||||
|
// Dinowyde sheet in the font texture.
|
||||||
|
const DBYTE_X: usize = FONT_COLS * GW as usize; // x of the DINOBYTE sheet in the font texture
|
||||||
const CP437_LOW: &str = " ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼";
|
const CP437_LOW: &str = " ☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼";
|
||||||
const CP437_HIGH: &str = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}";
|
const CP437_HIGH: &str = "ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}";
|
||||||
|
|
||||||
pub fn cp437(ch: char) -> u8 {
|
// the CP437 code of a character (DINOBYTE), if it has one
|
||||||
|
fn cp437(ch: char) -> Option<u8> {
|
||||||
// DINOBYTE has ö (0x94) and ò (0x95) swapped
|
// DINOBYTE has ö (0x94) and ò (0x95) swapped
|
||||||
match ch {
|
match ch {
|
||||||
'ö' => return 0x95,
|
'ö' => return Some(0x95),
|
||||||
'ò' => return 0x94,
|
'ò' => return Some(0x94),
|
||||||
|
'⌂' => return Some(127),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
let u = ch as u32;
|
let u = ch as u32;
|
||||||
if (32..127).contains(&u) {
|
if (32..127).contains(&u) {
|
||||||
return u as u8;
|
return Some(u as u8);
|
||||||
}
|
}
|
||||||
if ch == '⌂' {
|
if let Some(i) = CP437_LOW.chars().position(|c| c == ch).filter(|&i| i > 0) {
|
||||||
return 127;
|
return Some(i as u8);
|
||||||
}
|
}
|
||||||
if let Some(i) = CP437_LOW.chars().position(|c| c == ch) {
|
CP437_HIGH.chars().position(|c| c == ch).map(|i| 128 + i as u8)
|
||||||
if i > 0 {
|
}
|
||||||
return i as u8;
|
|
||||||
|
// stand-ins for characters neither font draws: similar glyphs
|
||||||
|
fn font_substitute(ch: char) -> Option<char> {
|
||||||
|
Some(match ch {
|
||||||
|
'✓' | '✔' => '√',
|
||||||
|
'ẞ' => 'ß',
|
||||||
|
'–' | '—' => '-',
|
||||||
|
'„' | '“' | '”' => '"',
|
||||||
|
'‚' | '‘' | '’' => '\'',
|
||||||
|
'…' => '.',
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// which glyphs the two sheets hold
|
||||||
|
pub struct FontMap {
|
||||||
|
has: Vec<bool>, // Dinowyde slots with a glyph
|
||||||
|
dbyte: [bool; 256], // DINOBYTE codes with a glyph
|
||||||
|
}
|
||||||
|
|
||||||
|
fn glyph_drawn(data: &[[u8; 4]], w: usize, gx: usize, gy: usize) -> bool {
|
||||||
|
(0..GH as usize).any(|y| (0..GW as usize).any(|x| data.get((gy + y) * w + gx + x).map_or(false, |p| p[3] > 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// the font texture: Dinowyde, and DINOBYTE right of it; white glyphs on a transparent
|
||||||
|
// background (for tinting)
|
||||||
|
pub fn load_font() -> (Image, FontMap) {
|
||||||
|
let wyde = Image::from_file_with_format(include_bytes!("../assets/dinowyde_1x.png"), Some(ImageFormat::Png)).expect("font");
|
||||||
|
let byte = Image::from_file_with_format(include_bytes!("../assets/dbyte_1x.png"), Some(ImageFormat::Png)).expect("font");
|
||||||
|
let (w, h) = (DBYTE_X + byte.width as usize, (wyde.height as usize).max(byte.height as usize));
|
||||||
|
let mut img = Image::gen_image_color(w as u16, h as u16, Color::new(0.0, 0.0, 0.0, 0.0));
|
||||||
|
{
|
||||||
|
let out = img.get_image_data_mut();
|
||||||
|
let ww = wyde.width as usize;
|
||||||
|
for (i, p) in wyde.get_image_data().iter().enumerate() {
|
||||||
|
if p[3] > 128 {
|
||||||
|
out[(i / ww) * w + i % ww] = [255, 255, 255, 255];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(i) = CP437_HIGH.chars().position(|c| c == ch) {
|
let bw = byte.width as usize;
|
||||||
return 128 + i as u8;
|
for (i, p) in byte.get_image_data().iter().enumerate() {
|
||||||
|
if p[3] > 128 && p[0] > 128 {
|
||||||
|
out[(i / bw) * w + DBYTE_X + i % bw] = [255, 255, 255, 255];
|
||||||
}
|
}
|
||||||
match ch {
|
|
||||||
'ẞ' => 0xE1,
|
|
||||||
'–' | '—' => b'-',
|
|
||||||
'„' | '“' | '”' => b'"',
|
|
||||||
'‚' | '‘' | '’' => b'\'',
|
|
||||||
'…' => b'.',
|
|
||||||
'×' => b'x',
|
|
||||||
_ => b'?',
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
let data = img.get_image_data();
|
||||||
|
let has = (0..FONT_COLS * FONT_ROWS).map(|g| glyph_drawn(data, w, (g % FONT_COLS) * GW as usize, (g / FONT_COLS) * GH as usize)).collect();
|
||||||
|
let mut dbyte = [false; 256];
|
||||||
|
for (c, d) in dbyte.iter_mut().enumerate() {
|
||||||
|
*d = glyph_drawn(data, w, DBYTE_X + (c % 16) * GW as usize, (c / 16) * GH as usize);
|
||||||
|
}
|
||||||
|
(img, FontMap { has, dbyte })
|
||||||
|
}
|
||||||
|
|
||||||
|
// where in the font texture a character's glyph is (top-left pixel): Dinowyde, else
|
||||||
|
// DINOBYTE, else a stand-in, else '?'
|
||||||
|
pub fn font_src(map: &FontMap, ch: char) -> (f32, f32) {
|
||||||
|
let find = |c: char| -> Option<(f32, f32)> {
|
||||||
|
if let Some(g) = font_slot(c).filter(|&g| map.has.get(g).copied().unwrap_or(false)) {
|
||||||
|
return Some(((g % FONT_COLS) as f32 * GW, (g / FONT_COLS) as f32 * GH));
|
||||||
|
}
|
||||||
|
cp437(c).filter(|&k| map.dbyte[k as usize]).map(|k| ((DBYTE_X + (k as usize % 16) * GW as usize) as f32, (k / 16) as f32 * GH))
|
||||||
|
};
|
||||||
|
find(ch).or_else(|| font_substitute(ch).and_then(find)).or_else(|| find('?')).unwrap_or((0.0, 0.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
// frame glyphs: [top-left, top-right, bottom-left, bottom-right, horizontal, vertical]
|
// frame glyphs: [top-left, top-right, bottom-left, bottom-right, horizontal, vertical]
|
||||||
pub const BOX_SINGLE: [u8; 6] = [0xDA, 0xBF, 0xC0, 0xD9, 0xC4, 0xB3];
|
pub const BOX_SINGLE: [char; 6] = ['┌', '┐', '└', '┘', '─', '│'];
|
||||||
pub const BOX_DOUBLE: [u8; 6] = [0xC9, 0xBB, 0xC8, 0xBC, 0xCD, 0xBA];
|
pub const BOX_DOUBLE: [char; 6] = ['╔', '╗', '╚', '╝', '═', '║'];
|
||||||
|
|
||||||
fn hash(x: i32, y: i32, s: u32) -> u32 {
|
fn hash(x: i32, y: i32, s: u32) -> u32 {
|
||||||
let mut h = (x as u32).wrapping_mul(374761393) ^ (y as u32).wrapping_mul(668265263) ^ s.wrapping_mul(2246822519);
|
let mut h = (x as u32).wrapping_mul(374761393) ^ (y as u32).wrapping_mul(668265263) ^ s.wrapping_mul(2246822519);
|
||||||
@@ -1772,17 +1855,13 @@ impl Gfx {
|
|||||||
atlas.set_filter(FilterMode::Nearest);
|
atlas.set_filter(FilterMode::Nearest);
|
||||||
let sprites = Texture2D::from_image(&gen_sprites());
|
let sprites = Texture2D::from_image(&gen_sprites());
|
||||||
sprites.set_filter(FilterMode::Nearest);
|
sprites.set_filter(FilterMode::Nearest);
|
||||||
// load the font; transparent background, white glyphs (for tinting)
|
// the fonts (Dinowyde, DINOBYTE for what it lacks)
|
||||||
let mut img = Image::from_file_with_format(include_bytes!("../assets/dbyte_1x.png"), Some(ImageFormat::Png)).expect("Schrift");
|
let (img, font_map) = load_font();
|
||||||
for px in img.get_image_data_mut().iter_mut() {
|
|
||||||
let on = px[3] > 128 && px[0] > 128;
|
|
||||||
*px = if on { [255, 255, 255, 255] } else { [0, 0, 0, 0] };
|
|
||||||
}
|
|
||||||
let font = Texture2D::from_image(&img);
|
let font = Texture2D::from_image(&img);
|
||||||
font.set_filter(FilterMode::Nearest);
|
font.set_filter(FilterMode::Nearest);
|
||||||
let cursors = Texture2D::from_image(&gen_cursors());
|
let cursors = Texture2D::from_image(&gen_cursors());
|
||||||
cursors.set_filter(FilterMode::Nearest);
|
cursors.set_filter(FilterMode::Nearest);
|
||||||
Gfx { atlas, sprites, cursors, font }
|
Gfx { atlas, sprites, cursors, font, font_map }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cell(id: u32) -> Rect {
|
pub fn cell(id: u32) -> Rect {
|
||||||
@@ -1881,9 +1960,10 @@ impl Gfx {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw a single CP437 glyph (x, y = top-left corner)
|
// draw a single character (x, y = top-left corner)
|
||||||
pub fn glyph(&self, code: u8, x: f32, y: f32, scale: f32, color: Color) {
|
pub fn glyph(&self, ch: char, x: f32, y: f32, scale: f32, color: Color) {
|
||||||
let src = Rect::new((code % 16) as f32 * GW, (code / 16) as f32 * GH, GW, GH);
|
let (sx, sy) = font_src(&self.font_map, ch);
|
||||||
|
let src = Rect::new(sx, sy, GW, GH);
|
||||||
draw_texture_ex(
|
draw_texture_ex(
|
||||||
&self.font,
|
&self.font,
|
||||||
x.floor(),
|
x.floor(),
|
||||||
@@ -1898,7 +1978,7 @@ impl Gfx {
|
|||||||
let mut cx = x;
|
let mut cx = x;
|
||||||
for ch in s.chars() {
|
for ch in s.chars() {
|
||||||
if ch != ' ' {
|
if ch != ' ' {
|
||||||
self.glyph(cp437(ch), cx, y, scale, color);
|
self.glyph(ch, cx, y, scale, color);
|
||||||
}
|
}
|
||||||
cx += GW * scale;
|
cx += GW * scale;
|
||||||
}
|
}
|
||||||
@@ -1971,7 +2051,7 @@ impl Gfx {
|
|||||||
self.text(s, x, y, scale, color);
|
self.text(s, x, y, scale, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// window with a CP437 frame; size in characters (cols x rows incl. frame)
|
// window with a box-drawing frame; size in characters (cols x rows incl. frame)
|
||||||
pub fn window(&self, x: f32, y: f32, cols: i32, rows: i32, scale: f32, double: bool, frame: Color, bg: Color) {
|
pub fn window(&self, x: f32, y: f32, cols: i32, rows: i32, scale: f32, double: bool, frame: Color, bg: Color) {
|
||||||
let cw = GW * scale;
|
let cw = GW * scale;
|
||||||
let ch = GH * scale;
|
let ch = GH * scale;
|
||||||
@@ -2003,11 +2083,23 @@ impl Gfx {
|
|||||||
if !title.is_empty() {
|
if !title.is_empty() {
|
||||||
let cw = GW * scale;
|
let cw = GW * scale;
|
||||||
let t = format!(" {} ", title);
|
let t = format!(" {} ", title);
|
||||||
let tx = x + 2.0 * cw;
|
let n = t.chars().count() as i32;
|
||||||
draw_rectangle(tx, y, self.measure(&t, scale), GH * scale, bg);
|
// the frame line ends cleanly on both sides of the title (rope ends, or the double
|
||||||
|
// strand's caps), if there is room for them
|
||||||
|
let caps = n + 5 <= cols;
|
||||||
|
let tx = x + if caps { 1.0 } else { 2.0 } * cw;
|
||||||
|
let w = cw * (n + if caps { 2 } else { 0 }) as f32;
|
||||||
|
draw_rectangle(tx, y, w, GH * scale, bg);
|
||||||
|
if caps {
|
||||||
|
let (l, r) = if double { ('╡', '╞') } else { ('╴', '╶') };
|
||||||
|
self.glyph(l, tx, y, scale, frame);
|
||||||
|
self.text(&t, tx + cw, y, scale, tcol);
|
||||||
|
self.glyph(r, tx + cw * (n + 1) as f32, y, scale, frame);
|
||||||
|
} else {
|
||||||
self.text(&t, tx, y, scale, tcol);
|
self.text(&t, tx, y, scale, tcol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -2057,3 +2149,63 @@ mod doc_images {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod font_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// the glyph of a character as rows of '#' and '.'
|
||||||
|
fn bitmap(img: &Image, ch: char) -> Vec<String> {
|
||||||
|
let g = font_slot(ch).expect("slot");
|
||||||
|
let (gx, gy) = ((g % FONT_COLS) * 6, (g / FONT_COLS) * 8);
|
||||||
|
let w = img.width as usize;
|
||||||
|
let d = img.get_image_data();
|
||||||
|
(0..8).map(|y| (0..6).map(|x| if d[(gy + y) * w + gx + x][3] > 0 { '#' } else { '.' }).collect()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dinowyde_slots_match_the_sheet() {
|
||||||
|
let (img, _) = load_font();
|
||||||
|
assert_eq!(bitmap(&img, 'A'), ["......", ".###..", "#...#.", "#...#.", "#####.", "#...#.", "#...#.", "......"]);
|
||||||
|
assert_eq!(bitmap(&img, 'Ж'), ["......", "#.#.#.", "#.#.#.", ".###..", "#.#.#.", "#.#.#.", "#.#.#.", "......"]);
|
||||||
|
assert_eq!(bitmap(&img, 'Ο')[2..4], [".###..", "#...#."], "Greek capital omicron");
|
||||||
|
assert!(bitmap(&img, '▀')[0..4].iter().all(|r| r == "######"), "upper half block");
|
||||||
|
assert_eq!(bitmap(&img, '–')[4], ".###..", "en dash");
|
||||||
|
assert_eq!(bitmap(&img, '—')[4], "#####.", "em dash");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_glyphs_come_from_dinobyte() {
|
||||||
|
let (_, map) = load_font();
|
||||||
|
let from_dbyte = |ch: char| font_src(&map, ch).0 >= DBYTE_X as f32;
|
||||||
|
assert!(!from_dbyte('A') && !from_dbyte('ö') && !from_dbyte('Ж') && !from_dbyte('─'), "Dinowyde first");
|
||||||
|
for ch in ['↑', '↓', '→', '←', '☺', '♥', '♪', '☼'] {
|
||||||
|
assert!(from_dbyte(ch), "{} from DINOBYTE", ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_text_of_the_game_has_its_glyphs() {
|
||||||
|
let (_, map) = load_font();
|
||||||
|
let question = font_src(&map, '?');
|
||||||
|
let mut missing = std::collections::BTreeSet::new();
|
||||||
|
for file in [include_str!("../lang/en.lang"), include_str!("../lang/de.lang")] {
|
||||||
|
for ch in file.chars().filter(|c| !c.is_control() && *c != ' ') {
|
||||||
|
if ch != '?' && font_src(&map, ch) == question {
|
||||||
|
missing.insert(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// interface symbols used in the code
|
||||||
|
for ch in ['█', '░', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '▼', '▲', '☻', '≡', '☼', '●', '○', '✓', '—', '·', '↑', '↓', '→', '←', '♥', '♪', '┌', '┐', '└', '┘', '─', '│', '╴', '╶', '╔', '╗', '╚', '╝', '═', '║', '╡', '╞'] {
|
||||||
|
if font_src(&map, ch) == question {
|
||||||
|
missing.insert(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(missing.is_empty(), "no glyph for {:?}", missing);
|
||||||
|
// Cyrillic and Greek are there for later languages
|
||||||
|
for ch in "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяёΑΒΓΔαβγδ".chars() {
|
||||||
|
assert_ne!(font_src(&map, ch), question, "{}", ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
// Menus: title screen, pause menu, savegame list (server) and the settings dialog
|
// Menus: title screen, pause menu, savegame list (server) and the settings dialog
|
||||||
// (tabs, values, key bindings).
|
// (tabs, values, key bindings).
|
||||||
// Drawn as CP437 windows on top of the (paused) game world.
|
// Drawn as text-mode windows on top of the (paused) game world.
|
||||||
|
|
||||||
use crate::campaign::{missions, Progress};
|
use crate::campaign::{missions, Progress};
|
||||||
use crate::campaign_doc::CampaignDoc;
|
use crate::campaign_doc::CampaignDoc;
|
||||||
|
|||||||
+3
-3
@@ -1536,7 +1536,7 @@ impl R3d {
|
|||||||
ascii_bar(gfx, x, head.y - GH * small, cols, f, c, small);
|
ascii_bar(gfx, x, head.y - GH * small, cols, f, c, small);
|
||||||
}
|
}
|
||||||
if e.guard {
|
if e.guard {
|
||||||
gfx.glyph(0x0F, head.x - (GW * small * 0.5).floor(), head.y - 2.0 * GH * small, small, Color::from_rgba(255, 220, 90, 255));
|
gfx.glyph('☼', head.x - (GW * small * 0.5).floor(), head.y - 2.0 * GH * small, small, Color::from_rgba(255, 220, 90, 255));
|
||||||
}
|
}
|
||||||
// rank badge left of the health bar
|
// rank badge left of the health bar
|
||||||
if e.level > 1 && px_per_tile >= 20.0 * F() as f32 {
|
if e.level > 1 && px_per_tile >= 20.0 * F() as f32 {
|
||||||
@@ -1547,11 +1547,11 @@ impl R3d {
|
|||||||
if matches!(e.job, Job::Sleep(_)) && !e.moving {
|
if matches!(e.job, Job::Sleep(_)) && !e.moving {
|
||||||
let k = (t * 1.5 + e.anim) % 1.0;
|
let k = (t * 1.5 + e.anim) % 1.0;
|
||||||
let rise = (k * 14.0 * F() as f32).floor();
|
let rise = (k * 14.0 * F() as f32).floor();
|
||||||
gfx.glyph(b'z', head.x + cw * 2.0, head.y - GH * S() - rise, S(), Color::new(0.8, 0.85, 1.0, 1.0 - k));
|
gfx.glyph('z', head.x + cw * 2.0, head.y - GH * S() - rise, S(), Color::new(0.8, 0.85, 1.0, 1.0 - k));
|
||||||
}
|
}
|
||||||
if e.carry > 0 {
|
if e.carry > 0 {
|
||||||
let p = proj(vec3(e.disp_x, 0.35, e.disp_y));
|
let p = proj(vec3(e.disp_x, 0.35, e.disp_y));
|
||||||
gfx.glyph(0x0F, p.x + cw * 2.0, p.y - GH * small, small, Color::from_rgba(250, 210, 60, 255));
|
gfx.glyph('☼', p.x + cw * 2.0, p.y - GH * small, small, Color::from_rgba(250, 210, 60, 255));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for tw in &g.towers {
|
for tw in &g.towers {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// In-canvas user interface: CP437 windows with the DINOBYTE bitmap font.
|
// In-canvas user interface: text-mode windows with the Dinowyde bitmap font.
|
||||||
// All texts come from the language files (see i18n.rs).
|
// All texts come from the language files (see i18n.rs).
|
||||||
|
|
||||||
use crate::game::*;
|
use crate::game::*;
|
||||||
@@ -114,21 +114,22 @@ pub fn begin_ui() {
|
|||||||
set_camera(&Camera2D { target: vec2(w * 0.5, h * 0.5), zoom: vec2(2.0 / w, 2.0 / h), ..Default::default() });
|
set_camera(&Camera2D { target: vec2(w * 0.5, h * 0.5), zoom: vec2(2.0 / w, 2.0 / h), ..Default::default() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// bar made of CP437 block characters: full blocks, a half block, shaded rest
|
// bar made of block characters: full blocks, an eighth block for the rest, shaded background
|
||||||
|
const EIGHTHS: [char; 7] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉'];
|
||||||
|
|
||||||
pub fn ascii_bar(gfx: &Gfx, x: f32, y: f32, cols: usize, frac: f32, col: Color, scale: f32) {
|
pub fn ascii_bar(gfx: &Gfx, x: f32, y: f32, cols: usize, frac: f32, col: Color, scale: f32) {
|
||||||
let f = frac.clamp(0.0, 1.0) * cols as f32;
|
let eighths = (frac.clamp(0.0, 1.0) * cols as f32 * 8.0).round() as usize;
|
||||||
let full = f.floor() as usize;
|
let (full, part) = (eighths / 8, eighths % 8);
|
||||||
let half = f - full as f32 >= 0.5;
|
|
||||||
let dim = Color::new(col.r * 0.35, col.g * 0.35, col.b * 0.35, col.a);
|
let dim = Color::new(col.r * 0.35, col.g * 0.35, col.b * 0.35, col.a);
|
||||||
for k in 0..cols {
|
for k in 0..cols {
|
||||||
let px = x + k as f32 * GW * scale;
|
let px = x + k as f32 * GW * scale;
|
||||||
if k < full {
|
if k < full {
|
||||||
gfx.glyph(0xDB, px, y, scale, col);
|
gfx.glyph('█', px, y, scale, col);
|
||||||
} else if k == full && half {
|
|
||||||
gfx.glyph(0xB0, px, y, scale, dim);
|
|
||||||
gfx.glyph(0xDD, px, y, scale, col);
|
|
||||||
} else {
|
} else {
|
||||||
gfx.glyph(0xB0, px, y, scale, dim);
|
gfx.glyph('░', px, y, scale, dim);
|
||||||
|
if k == full && part > 0 {
|
||||||
|
gfx.glyph(EIGHTHS[part - 1], px, y, scale, col);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -729,11 +730,11 @@ impl Ui {
|
|||||||
let locked = !tools.iter().any(|&t| g.tool_allowed(t));
|
let locked = !tools.iter().any(|&t| g.tool_allowed(t));
|
||||||
let r = tool_button(bx, by, bw, ti.icon, key, &label, has, self.open_cat == Some(c) || hovered, locked);
|
let r = tool_button(bx, by, bw, ti.icon, key, &label, has, self.open_cat == Some(c) || hovered, locked);
|
||||||
if has {
|
if has {
|
||||||
gfx.glyph(0x1F, bx + (bw - CW()) * 0.5, tb_y, S(), if pulse { WHITE } else { FRAME_HI });
|
gfx.glyph('▼', bx + (bw - CW()) * 0.5, tb_y, S(), if pulse { WHITE } else { FRAME_HI });
|
||||||
}
|
}
|
||||||
if tools.len() > 1 {
|
if tools.len() > 1 {
|
||||||
// small arrow: this button opens a row
|
// small arrow: this button opens a row
|
||||||
gfx.glyph(0x1E, r.x + r.w - GW * SMALL() - 2.0 * px, r.y + 2.0 * px, SMALL(), if self.open_cat == Some(c) { FRAME_HI } else { DIM });
|
gfx.glyph('▲', r.x + r.w - GW * SMALL() - 2.0 * px, r.y + 2.0 * px, SMALL(), if self.open_cat == Some(c) { FRAME_HI } else { DIM });
|
||||||
}
|
}
|
||||||
self.touch_btns.push((r, TBtn::Cat(c)));
|
self.touch_btns.push((r, TBtn::Cat(c)));
|
||||||
}
|
}
|
||||||
@@ -743,7 +744,7 @@ impl Ui {
|
|||||||
let hovered = !touch_ui() && r.contains(vec2(mx, my));
|
let hovered = !touch_ui() && r.contains(vec2(mx, my));
|
||||||
draw_rectangle(r.x, r.y, r.w, r.h, if hovered || self.show_overview { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
draw_rectangle(r.x, r.y, r.w, r.h, if hovered || self.show_overview { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
||||||
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, if self.show_overview { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) });
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, if self.show_overview { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) });
|
||||||
gfx.glyph(0x02, bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI);
|
gfx.glyph('☻', bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI);
|
||||||
let ol = tr("cat.overview");
|
let ol = tr("cat.overview");
|
||||||
let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize;
|
let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize;
|
||||||
let ol: String = ol.chars().take(maxc).collect();
|
let ol: String = ol.chars().take(maxc).collect();
|
||||||
@@ -755,7 +756,7 @@ impl Ui {
|
|||||||
let hovered = !touch_ui() && r.contains(vec2(mx, my));
|
let hovered = !touch_ui() && r.contains(vec2(mx, my));
|
||||||
draw_rectangle(r.x, r.y, r.w, r.h, if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
draw_rectangle(r.x, r.y, r.w, r.h, if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
||||||
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, Color::new(0.35, 0.28, 0.2, 1.0));
|
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, Color::new(0.35, 0.28, 0.2, 1.0));
|
||||||
gfx.glyph(0xF0, bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI);
|
gfx.glyph('≡', bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI);
|
||||||
let ml = tr("cat.menu");
|
let ml = tr("cat.menu");
|
||||||
let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize;
|
let maxc = ((bw - 2.0 * px) / (GW * SMALL())).floor() as usize;
|
||||||
let ml: String = ml.chars().take(maxc).collect();
|
let ml: String = ml.chars().take(maxc).collect();
|
||||||
|
|||||||
Reference in new issue
Block a user