High-res interface icons: tools, towers, traps, doors, spells, orders and siege

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-07 23:41:08 +01:00
1 parent 37e5250c52
commit d951c16902
28 files changed
+199

No files matched your search

+66
View File
@@ -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 <sheet>-raw.png <sheet> --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`).
+29
View File
@@ -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)")]),
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+32
View File
@@ -414,6 +414,20 @@ const HD_DECALS: [(u32, &[u8], &[u8]); 8] = [
hd_obj!(D_SHARD, "shard"), 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 // 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 // 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, // 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 // 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 // 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::<Vec<u32>>(); let range = |a: u32, n: u32| (a..a + n).collect::<Vec<u32>>();
@@ -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 // the variants: as many tiles as the kind has cells, every cell filled
for &(base, n, png, png2) in HD_VARS.iter() { for &(base, n, png, png2) in HD_VARS.iter() {
for tile in [HD_TILE, HD_TILE2] { for tile in [HD_TILE, HD_TILE2] {
+72
View File
@@ -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()