diff --git a/art/generated/icons/PROMPTS.md b/art/generated/icons/PROMPTS.md new file mode 100644 index 0000000..2612bec --- /dev/null +++ b/art/generated/icons/PROMPTS.md @@ -0,0 +1,66 @@ +# Interface icons (high-res) + +Generated on 2026-11-07 with `fal-ai/nano-banana-pro` (`resolution: 2K`, one image per sheet, +`aspect_ratio: 21:9` for the 5-column sheets, `16:9` for the spells). `batch.py` holds the +prompts: the shared text below followed by the numbered icon descriptions. + +```text +A sheet of game icons for a dark-fantasy dungeon strategy game, arranged in a grid of {cols} columns and {rows} rows, every icon centred in its own equal square cell and filling about 80 percent of it, all icons the same size and in the same style: hand-painted pixel art, chunky clear shapes, crisp edges, a dark near-black outline, rich but slightly muted colours, readable at small size, light from the upper left. The background is a completely flat pure magenta (#FF00FF) everywhere, also between the icons - no frames, no grid lines, no cell borders, no text, no labels, no numbers. The icons, in reading order (left to right, top to bottom): +``` + +## tools (5x2) + +- `I_PICK`: a pickaxe for digging +- `I_HAND`: a clawed green monster hand reaching to grab +- `I_SELL`: a small pile of gold coins with a curved arrow (selling) +- `I_FLAG`: a red rally flag on a pole +- `I_BOLT`: a yellow lightning bolt +- `I_HEAL`: a glowing green healing cross with sparkles +- `I_SUMMON`: a glowing violet summoning circle with a small imp silhouette +- `I_BRIDGE`: a small wooden plank bridge arch +- `I_WALL`: a block of grey stone wall with an upward arrow +- `I_STEEL`: a riveted dark steel wall plate + +## towers (5x2) + +- `I_ARROW`: a small wooden watchtower with an arrow +- `I_CANNON`: a squat stone tower with a black cannon +- `I_MAGIC`: a slender dark tower with a glowing purple orb on top +- `I_RUNE`: a dark stone obelisk with a glowing red rune and bronze bands +- `I_DRAWBRIDGE`: a raised wooden drawbridge with chains over water +- `I_RAM`: a battering ram: a tree trunk with an iron cap under a small wooden roof on wheels +- `I_CATAPULT`: a wooden catapult with a stone in its arm +- `I_ORDERS`: two crossed swords (attack order) +- `I_ORDERS+1`: a round shield (guard order) +- `I_FREEZE`: a glowing pale-blue snowflake (frost spell) + +## traps (5x2) + +- `I_TRAPS`: a floor trap with sharp metal spikes +- `I_TRAPS+1`: a floor vent puffing a green poison cloud +- `I_TRAPS+2`: a floor nozzle shooting a jet of fire +- `I_TRAPS+3`: a big round rolling boulder +- `I_TRAPS+4`: a brass alarm bell +- `I_TRAPS+5`: a glowing red rune mine on the floor +- `I_TRAPS+6`: a floor trap with a glowing blue frost crystal +- `I_TRAPS+7`: a wooden door with iron hinges +- `I_TRAPS+8`: a heavy iron door with rivets +- `I_TRAPS+9`: a magic door glowing with purple runes + +## spells (4x2) + +- `I_SPELLS`: a glowing all-seeing eye (seer's eye spell) +- `I_SPELLS+1`: falling rocks from a cracked ceiling (cave-in spell) +- `I_SPELLS+2`: a winged boot (haste spell) +- `I_SPELLS+3`: a fist of grey stone (stoneskin spell) +- `I_SPELLS+4`: gold coins raining down (gold rain spell) +- `I_SPELLS+5`: a fading translucent ghostly figure (invisibility spell) +- `I_SPELLS+6`: a green skull in a sickly cloud (plague spell) +- `I_SPELLS+7`: cracked shaking ground with rocks jumping (quake spell) + +## Processing + +`tools/cut_icons.py -raw.png --grid 2x5` (spells `--grid 2x4`) keys out only nearly +pure magenta (violet glows stay), crops every icon, centres it in a square and scales it to 64x64 +and 128x128 with hard alpha. The strips go to `assets/textures/icons/`; the game puts them into +the high-res atlases' icon cells (`HD_ICONS` in `src/gfx.rs`). diff --git a/art/generated/icons/batch.py b/art/generated/icons/batch.py new file mode 100644 index 0000000..e39b37e --- /dev/null +++ b/art/generated/icons/batch.py @@ -0,0 +1,29 @@ +# The interface icons of the high-res style (2026-11-07): four sheets, each a grid of icons. +# (sheet name, columns, rows, [(atlas cell constant, description)]) +STYLE = ("A sheet of game icons for a dark-fantasy dungeon strategy game, arranged in a grid of {cols} columns and {rows} rows, every icon " + "centred in its own equal square cell and filling about 80 percent of it, all icons the same size and in the same style: " + "hand-painted pixel art, chunky clear shapes, crisp edges, a dark near-black outline, rich but slightly muted colours, readable at small size, " + "light from the upper left. The background is a completely flat pure magenta (#FF00FF) everywhere, also between the icons - " + "no frames, no grid lines, no cell borders, no text, no labels, no numbers. The icons, in reading order (left to right, top to bottom): ") +SHEETS = [ + ("tools", 5, 2, [ + ("I_PICK", "a pickaxe for digging"), ("I_HAND", "a clawed green monster hand reaching to grab"), ("I_SELL", "a small pile of gold coins with a curved arrow (selling)"), + ("I_FLAG", "a red rally flag on a pole"), ("I_BOLT", "a yellow lightning bolt"), ("I_HEAL", "a glowing green healing cross with sparkles"), + ("I_SUMMON", "a glowing violet summoning circle with a small imp silhouette"), ("I_BRIDGE", "a small wooden plank bridge arch"), + ("I_WALL", "a block of grey stone wall with an upward arrow"), ("I_STEEL", "a riveted dark steel wall plate")]), + ("towers", 5, 2, [ + ("I_ARROW", "a small wooden watchtower with an arrow"), ("I_CANNON", "a squat stone tower with a black cannon"), ("I_MAGIC", "a slender dark tower with a glowing purple orb on top"), + ("I_RUNE", "a dark stone obelisk with a glowing red rune and bronze bands"), ("I_DRAWBRIDGE", "a raised wooden drawbridge with chains over water"), + ("I_RAM", "a battering ram: a tree trunk with an iron cap under a small wooden roof on wheels"), ("I_CATAPULT", "a wooden catapult with a stone in its arm"), + ("I_ORDERS", "two crossed swords (attack order)"), ("I_ORDERS+1", "a round shield (guard order)"), ("I_FREEZE", "a glowing pale-blue snowflake (frost spell)")]), + ("traps", 5, 2, [ + ("I_TRAPS", "a floor trap with sharp metal spikes"), ("I_TRAPS+1", "a floor vent puffing a green poison cloud"), ("I_TRAPS+2", "a floor nozzle shooting a jet of fire"), + ("I_TRAPS+3", "a big round rolling boulder"), ("I_TRAPS+4", "a brass alarm bell"), ("I_TRAPS+5", "a glowing red rune mine on the floor"), + ("I_TRAPS+6", "a floor trap with a glowing blue frost crystal"), ("I_TRAPS+7", "a wooden door with iron hinges"), ("I_TRAPS+8", "a heavy iron door with rivets"), + ("I_TRAPS+9", "a magic door glowing with purple runes")]), + ("spells", 4, 2, [ + ("I_SPELLS", "a glowing all-seeing eye (seer's eye spell)"), ("I_SPELLS+1", "falling rocks from a cracked ceiling (cave-in spell)"), + ("I_SPELLS+2", "a winged boot (haste spell)"), ("I_SPELLS+3", "a fist of grey stone (stoneskin spell)"), ("I_SPELLS+4", "gold coins raining down (gold rain spell)"), + ("I_SPELLS+5", "a fading translucent ghostly figure (invisibility spell)"), ("I_SPELLS+6", "a green skull in a sickly cloud (plague spell)"), + ("I_SPELLS+7", "cracked shaking ground with rocks jumping (quake spell)")]), +] diff --git a/art/generated/icons/spells-128.png b/art/generated/icons/spells-128.png new file mode 100644 index 0000000..88754ca Binary files /dev/null and b/art/generated/icons/spells-128.png differ diff --git a/art/generated/icons/spells-64.png b/art/generated/icons/spells-64.png new file mode 100644 index 0000000..ed79dde Binary files /dev/null and b/art/generated/icons/spells-64.png differ diff --git a/art/generated/icons/spells-preview.png b/art/generated/icons/spells-preview.png new file mode 100644 index 0000000..9bc1a10 Binary files /dev/null and b/art/generated/icons/spells-preview.png differ diff --git a/art/generated/icons/spells-raw.png b/art/generated/icons/spells-raw.png new file mode 100644 index 0000000..1db1de2 Binary files /dev/null and b/art/generated/icons/spells-raw.png differ diff --git a/art/generated/icons/tools-128.png b/art/generated/icons/tools-128.png new file mode 100644 index 0000000..bc55853 Binary files /dev/null and b/art/generated/icons/tools-128.png differ diff --git a/art/generated/icons/tools-64.png b/art/generated/icons/tools-64.png new file mode 100644 index 0000000..5cadc25 Binary files /dev/null and b/art/generated/icons/tools-64.png differ diff --git a/art/generated/icons/tools-preview.png b/art/generated/icons/tools-preview.png new file mode 100644 index 0000000..2a74b68 Binary files /dev/null and b/art/generated/icons/tools-preview.png differ diff --git a/art/generated/icons/tools-raw.png b/art/generated/icons/tools-raw.png new file mode 100644 index 0000000..06e70c7 Binary files /dev/null and b/art/generated/icons/tools-raw.png differ diff --git a/art/generated/icons/towers-128.png b/art/generated/icons/towers-128.png new file mode 100644 index 0000000..f25fa54 Binary files /dev/null and b/art/generated/icons/towers-128.png differ diff --git a/art/generated/icons/towers-64.png b/art/generated/icons/towers-64.png new file mode 100644 index 0000000..84857e4 Binary files /dev/null and b/art/generated/icons/towers-64.png differ diff --git a/art/generated/icons/towers-preview.png b/art/generated/icons/towers-preview.png new file mode 100644 index 0000000..846bde6 Binary files /dev/null and b/art/generated/icons/towers-preview.png differ diff --git a/art/generated/icons/towers-raw.png b/art/generated/icons/towers-raw.png new file mode 100644 index 0000000..9076c69 Binary files /dev/null and b/art/generated/icons/towers-raw.png differ diff --git a/art/generated/icons/traps-128.png b/art/generated/icons/traps-128.png new file mode 100644 index 0000000..1766896 Binary files /dev/null and b/art/generated/icons/traps-128.png differ diff --git a/art/generated/icons/traps-64.png b/art/generated/icons/traps-64.png new file mode 100644 index 0000000..36d96be Binary files /dev/null and b/art/generated/icons/traps-64.png differ diff --git a/art/generated/icons/traps-preview.png b/art/generated/icons/traps-preview.png new file mode 100644 index 0000000..c88171a Binary files /dev/null and b/art/generated/icons/traps-preview.png differ diff --git a/art/generated/icons/traps-raw.png b/art/generated/icons/traps-raw.png new file mode 100644 index 0000000..9f33ca8 Binary files /dev/null and b/art/generated/icons/traps-raw.png differ diff --git a/assets/textures/icons/spells-128.png b/assets/textures/icons/spells-128.png new file mode 100644 index 0000000..88754ca Binary files /dev/null and b/assets/textures/icons/spells-128.png differ diff --git a/assets/textures/icons/spells-64.png b/assets/textures/icons/spells-64.png new file mode 100644 index 0000000..ed79dde Binary files /dev/null and b/assets/textures/icons/spells-64.png differ diff --git a/assets/textures/icons/tools-128.png b/assets/textures/icons/tools-128.png new file mode 100644 index 0000000..bc55853 Binary files /dev/null and b/assets/textures/icons/tools-128.png differ diff --git a/assets/textures/icons/tools-64.png b/assets/textures/icons/tools-64.png new file mode 100644 index 0000000..5cadc25 Binary files /dev/null and b/assets/textures/icons/tools-64.png differ diff --git a/assets/textures/icons/towers-128.png b/assets/textures/icons/towers-128.png new file mode 100644 index 0000000..f25fa54 Binary files /dev/null and b/assets/textures/icons/towers-128.png differ diff --git a/assets/textures/icons/towers-64.png b/assets/textures/icons/towers-64.png new file mode 100644 index 0000000..84857e4 Binary files /dev/null and b/assets/textures/icons/towers-64.png differ diff --git a/assets/textures/icons/traps-128.png b/assets/textures/icons/traps-128.png new file mode 100644 index 0000000..1766896 Binary files /dev/null and b/assets/textures/icons/traps-128.png differ diff --git a/assets/textures/icons/traps-64.png b/assets/textures/icons/traps-64.png new file mode 100644 index 0000000..36d96be Binary files /dev/null and b/assets/textures/icons/traps-64.png differ diff --git a/src/gfx.rs b/src/gfx.rs index 4f22b04..c92bebd 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -414,6 +414,20 @@ const HD_DECALS: [(u32, &[u8], &[u8]); 8] = [ hd_obj!(D_SHARD, "shard"), ]; +// the interface icons of the high-res style (art/generated/icons/): strips of icons, 64x64 and +// 128x128, for these cells in this order +macro_rules! hd_icons { + ($name:literal, $cells:expr) => { + (include_bytes!(concat!("../assets/textures/icons/", $name, "-64.png")) as &[u8], include_bytes!(concat!("../assets/textures/icons/", $name, "-128.png")) as &[u8], $cells) + }; +} +const HD_ICONS: [(&[u8], &[u8], &[u32]); 4] = [ + hd_icons!("tools", &[I_PICK, I_HAND, I_SELL, I_FLAG, I_BOLT, I_HEAL, I_SUMMON, I_BRIDGE, I_WALL, I_STEEL]), + hd_icons!("towers", &[I_ARROW, I_CANNON, I_MAGIC, I_RUNE, I_DRAWBRIDGE, I_RAM, I_CATAPULT, I_ORDERS, I_ORDERS + 1, I_FREEZE]), + hd_icons!("traps", &[I_TRAPS, I_TRAPS + 1, I_TRAPS + 2, I_TRAPS + 3, I_TRAPS + 4, I_TRAPS + 5, I_TRAPS + 6, I_TRAPS + 7, I_TRAPS + 8, I_TRAPS + 9]), + hd_icons!("spells", &[I_SPELLS, I_SPELLS + 1, I_SPELLS + 2, I_SPELLS + 3, I_SPELLS + 4, I_SPELLS + 5, I_SPELLS + 6, I_SPELLS + 7]), +]; + // Everything else in the world (see art/generated/textures/all/): a kind's own cells get as many // variants as it has cells, tiles that fit together in any order (one seamless border, different // middles), and a tile picks one by its position. (first cell, cells, 64x64 tiles side by side, @@ -606,6 +620,17 @@ fn gen_hd_atlas(tile: u32) -> Image { } } } + for &(png, png2, cells) in HD_ICONS.iter() { + let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd icons"); + for (v, &id) in cells.iter().enumerate() { + let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile); + for y in 0..tile { + for x in 0..tile { + img.set_pixel(ox + x, oy + y, src.get_pixel(v as u32 * tile + x, y)); + } + } + } + } // the generated pictures take on the colours of the 16x16 cells they replace (mean and // contrast per channel, most of the way), so switching between the styles keeps the look let range = |a: u32, n: u32| (a..a + n).collect::>(); @@ -3269,6 +3294,13 @@ mod hd_atlas_tests { } } } + // the icons: a strip with one icon per cell + for &(png, png2, cells) in HD_ICONS.iter() { + for tile in [HD_TILE, HD_TILE2] { + let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).unwrap(); + assert_eq!((src.width as u32, src.height as u32), (cells.len() as u32 * tile, tile)); + } + } // the variants: as many tiles as the kind has cells, every cell filled for &(base, n, png, png2) in HD_VARS.iter() { for tile in [HD_TILE, HD_TILE2] { diff --git a/tools/cut_icons.py b/tools/cut_icons.py new file mode 100644 index 0000000..3daa175 --- /dev/null +++ b/tools/cut_icons.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Cut a generated sheet of icons (a grid on a flat magenta background) into transparent icons +for the high-res atlas: every cell keyed (only nearly pure magenta goes, so violet glows stay), +cropped to its icon, centred in a square with a small margin and scaled down without fringes, +the alpha made hard. + +Usage: + cut_icons.py raw.png out_prefix --grid ROWSxCOLS [--count N] [--margin 0.06] + +Writes out_prefix-64.png and out_prefix-128.png: the icons side by side in reading order, and +out_prefix-preview.png (on a grey background, 2x). Needs Pillow. +""" +import argparse + +from PIL import Image + + +def keyed(p): + r, g, b = p[:3] + return r > 170 and b > 170 and g < 90 and abs(r - b) < 70 + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("raw") + ap.add_argument("out") + ap.add_argument("--grid", required=True) + ap.add_argument("--count", type=int, default=0) + ap.add_argument("--margin", type=float, default=0.06) + a = ap.parse_args() + rows, cols = (int(v) for v in a.grid.split("x")) + n = a.count or rows * cols + + img = Image.open(a.raw).convert("RGBA") + px = img.load() + for y in range(img.height): + for x in range(img.width): + if keyed(px[x, y]): + px[x, y] = (0, 0, 0, 0) + icons = [] + for i in range(n): + r, c = divmod(i, cols) + cell = img.crop((c * img.width // cols, r * img.height // rows, (c + 1) * img.width // cols, (r + 1) * img.height // rows)) + box = cell.getchannel("A").point(lambda v: 255 if v > 0 else 0).getbbox() or (0, 0, cell.width, cell.height) + icon = cell.crop(box) + side = int(max(icon.size) * (1 + 2 * a.margin)) + sq = Image.new("RGBA", (side, side), (0, 0, 0, 0)) + sq.paste(icon, ((side - icon.width) // 2, (side - icon.height) // 2)) + icons.append(sq) + for size in (64, 128): + strip = Image.new("RGBA", (size * n, size), (0, 0, 0, 0)) + for k, sq in enumerate(icons): + small = sq.resize((size, size), Image.BOX) + sp = small.load() + for y in range(size): + for x in range(size): + r, g, b, al = sp[x, y] + if al < 110: + sp[x, y] = (0, 0, 0, 0) + else: + f = 255 / al + sp[x, y] = (min(255, int(r * f)), min(255, int(g * f)), min(255, int(b * f)), 255) + strip.paste(small, (k * size, 0)) + strip.save(f"{a.out}-{size}.png") + prev = Image.new("RGBA", (64 * n, 64), (80, 76, 84, 255)) + prev.alpha_composite(Image.open(f"{a.out}-64.png")) + prev.resize((prev.width * 2, prev.height * 2), Image.NEAREST).save(f"{a.out}-preview.png") + print(f"{a.out}: {n} icons") + + +if __name__ == "__main__": + main()