High-res claimed floors for all four masters: five slabs each with own engravings and damage

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-07 21:41:07 +01:00
1 parent 50f8af15c7
commit aab466cfbe
34 files changed
+202 -8

No files matched your search

+2 -4
View File
@@ -22,11 +22,9 @@ Seamless tileable square texture, viewed straight from above (orthographic top-d
A uniform all-over pattern of rough grey-brown cobblestones of mixed sizes set in packed dirt, evenly spread from edge to edge with no frame, no border and no centre motif, like a cropped piece of a large dungeon floor. A uniform all-over pattern of rough grey-brown cobblestones of mixed sizes set in packed dirt, evenly spread from edge to edge with no frame, no border and no centre motif, like a cropped piece of a large dungeon floor.
``` ```
## claimed: T_CLAIM (claimed floor) ## claimed
```text Replaced by the masters' floors with five slabs each, see `floors/PROMPTS.md`.
The floor of an evil dungeon master's lair: fitted dark crimson-purple flagstones with thin black grout lines, some stones carry a small faint carved demonic rune, worn and polished.
```
## earth: T_EARTH (earth blocks) ## earth: T_EARTH (earth blocks)
+53
View File
@@ -0,0 +1,53 @@
# The masters' claimed floors (high-res)
Generated on 2026-11-07 with fal.ai: `fal-ai/nano-banana-pro`, `aspect_ratio: 3:2`,
`resolution: 2K`, one image per master. Each sheet has six slabs in a 3x2 grid on magenta; five
are used (`HD_FLOORS` in `src/gfx.rs`, with weights so plain slabs come more often).
Prompt template (`{floor}` per master):
```text
A texture sheet of six square floor tiles for a dark-fantasy dungeon strategy game, arranged in a grid of 3 columns and 2 rows, the tiles separated by thin flat pure magenta (#FF00FF) gaps, magenta also around the outside. Every tile is seen straight from above (orthographic top-down, no perspective), flat even lighting, no shadows, and fills its square completely. Every tile is one square floor slab made of a few fitted flagstones, with a dark grout line along all four edges, so the tiles fit together in any arrangement. All six tiles use the same stone, colours and style: {floor} The six tiles differ: tile 1 intact and clean; tile 2 with a large carved engraving in the middle; tile 3 with fine hairline cracks; tile 4 with a chipped corner and a few missing stone splinters; tile 5 with a broken flagstone, cracked through, a piece sunk and loose rubble; tile 6 with small engravings and worn edges. Style: hand-painted pixel art, chunky clear shapes, crisp edges, readable at small size. No text, no labels, no numbers.
```
## player
```text
polished flagstones of medium crimson-red stone with a purple tint (clearly brighter than black), the engravings are glowing-edged demonic runes, horned skulls and pentagram-like sigils of an evil dungeon master.
```
## mossgrim
```text
damp mossy green-grey flagstones; the engravings show mushrooms, spores and coiling mould tendrils, green fungus and moss grow out of the grout lines and cracks, a mouldy cellar master's floor.
```
## nightwell
```text
elegant deep-blue marble flagstones with fine silver veins, the engravings are refined lilies, crescent moons and elegant curling filigree inlaid with silver, the floor of a cold, cultivated and vain lady of the night.
```
## saffrax
```text
warm ochre-yellow sandstone flagstones with brass inlays, the engravings are alchemical sigils, suns and sulphur symbols, a few scorch marks, the floor of a greedy alchemist master of sulphur and gold.
```
## Processing
`tools/make_floor_tiles.py` crops the slabs, fills magenta inside them (chipped corners, holes)
with a dark void, brightens if asked, scales and reduces the colours:
```bash
T=tools/make_floor_tiles.py
python3 $T player-raw.png player --grid 2x3 --pick 1,2,3,5,6 --size 128 --bright 1.45 # and --size 64
python3 $T mossgrim-raw.png mossgrim --grid 2x3 --pick 1,2,3,4,5 --size 128
python3 $T nightwell-raw.png nightwell --grid 2x3 --pick 1,2,3,4,5 --size 128 --bright 1.15
python3 $T saffrax-raw.png saffrax --grid 2x3 --pick 1,2,3,4,5 --size 128 --bright 0.82
```
The player's sheet came out too dark (brightened by 1.45); its fourth slab repeats the big skull
with a chipped corner and is left out. Saffrax' sandstone was too glaring (darkened by 0.82).
`<name>-64.png` / `<name>-128.png` are copied to `assets/textures/floor-<name>.png` /
`floor-<name>-128.png`; `<name>-128-preview.png` shows a floor laid out at random.
Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+68 -4
View File
@@ -6,7 +6,7 @@ use macroquad::prelude::*;
pub const CELL: f32 = 16.0; pub const CELL: f32 = 16.0;
pub const ATLAS_COLS: u32 = 16; pub const ATLAS_COLS: u32 = 16;
pub const ATLAS_ROWS: u32 = 14; pub const ATLAS_ROWS: u32 = 16; // rows 14-15: cells only the high-res atlases fill (see HD_FLOORS)
// atlas indices (4 variants each) // atlas indices (4 variants each)
pub const T_ROCK: u32 = 0; pub const T_ROCK: u32 = 0;
@@ -338,9 +338,8 @@ fn earth_px(x: i32, y: i32, v: u32) -> Color {
pub const HD_TILE: u32 = 64; pub const HD_TILE: u32 = 64;
pub const HD_TILE2: u32 = 128; pub const HD_TILE2: u32 = 128;
// (first cell, variants, 64x64 level, 128x128 level) // (first cell, variants, 64x64 level, 128x128 level)
const HD_TILES: [(u32, u32, &[u8], &[u8]); 10] = [ const HD_TILES: [(u32, u32, &[u8], &[u8]); 9] = [
(T_FLOOR, 4, include_bytes!("../assets/textures/floor.png"), include_bytes!("../assets/textures/floor-256.png")), (T_FLOOR, 4, include_bytes!("../assets/textures/floor.png"), include_bytes!("../assets/textures/floor-256.png")),
(T_CLAIM, 4, include_bytes!("../assets/textures/claimed.png"), include_bytes!("../assets/textures/claimed-256.png")),
(T_EARTH, 4, include_bytes!("../assets/textures/earth.png"), include_bytes!("../assets/textures/earth-256.png")), (T_EARTH, 4, include_bytes!("../assets/textures/earth.png"), include_bytes!("../assets/textures/earth-256.png")),
(T_ROCK, 4, include_bytes!("../assets/textures/rock.png"), include_bytes!("../assets/textures/rock-256.png")), (T_ROCK, 4, include_bytes!("../assets/textures/rock.png"), include_bytes!("../assets/textures/rock-256.png")),
(T_GOLD, 4, include_bytes!("../assets/textures/gold.png"), include_bytes!("../assets/textures/gold-256.png")), (T_GOLD, 4, include_bytes!("../assets/textures/gold.png"), include_bytes!("../assets/textures/gold-256.png")),
@@ -351,9 +350,39 @@ const HD_TILES: [(u32, u32, &[u8], &[u8]); 10] = [
(T_DIAMOND, 2, include_bytes!("../assets/textures/diamond.png"), include_bytes!("../assets/textures/diamond-128.png")), (T_DIAMOND, 2, include_bytes!("../assets/textures/diamond.png"), include_bytes!("../assets/textures/diamond-128.png")),
]; ];
// The masters' claimed floors in the high-res style: five single slabs each (with a grout line
// along the edges, so they fit in any order), in the master's colour with engravings of their
// own and some of them cracked, chipped or broken. They go into cells of their own (from
// T_HDFLOOR on, five per master); a tile picks one by its position, weighted (plain slabs more
// often than the big engravings and the broken ones).
// (the claimed floor's cells, the first own cell, 64x64 slabs side by side, 128x128 ones, weights)
pub const T_HDFLOOR: u32 = 224;
const HD_FLOORS: [(u32, u32, &[u8], &[u8], [u32; 5]); 4] = [
// the player: crimson stone, demonic runes and horned skulls (plain, big engraving, cracks, broken, small engravings)
(T_CLAIM, T_HDFLOOR, include_bytes!("../assets/textures/floor-player.png"), include_bytes!("../assets/textures/floor-player-128.png"), [4, 1, 3, 1, 2]),
// Mossgrim: mossy green slabs, mushrooms and mould (plain, big engraving, cracks, chipped, broken)
(T_RIVAL1, T_HDFLOOR + 5, include_bytes!("../assets/textures/floor-mossgrim.png"), include_bytes!("../assets/textures/floor-mossgrim-128.png"), [4, 1, 3, 2, 1]),
// Lady Nightwell: blue marble, silver lilies and moons
(T_RIVAL2, T_HDFLOOR + 10, include_bytes!("../assets/textures/floor-nightwell.png"), include_bytes!("../assets/textures/floor-nightwell-128.png"), [4, 1, 3, 2, 1]),
// Saffrax: ochre sandstone, brass and alchemical sigils
(T_RIVAL3, T_HDFLOOR + 15, include_bytes!("../assets/textures/floor-saffrax.png"), include_bytes!("../assets/textures/floor-saffrax-128.png"), [4, 1, 3, 2, 1]),
];
// the cell a tile at (x, z) shows in the high-res style: generated textures with four variants // the cell a tile at (x, z) shows in the high-res style: generated textures with four variants
// are laid out by the position (see HD_TILES), everything else stays // are laid out by the position (see HD_TILES), the masters' floors pick one of their five slabs
// (HD_FLOORS), everything else stays
pub fn hd_tile(id: u32, x: i32, z: i32) -> u32 { pub fn hd_tile(id: u32, x: i32, z: i32) -> u32 {
if let Some(&(_, own, _, _, w)) = HD_FLOORS.iter().find(|f| (f.0..f.0 + 4).contains(&id)) {
let h = (x as u32).wrapping_mul(73_856_093) ^ (z as u32).wrapping_mul(19_349_663);
let h = (h ^ (h >> 13)).wrapping_mul(0x5bd1_e995);
let mut r = (h >> 8) % w.iter().sum::<u32>();
for (k, &wk) in w.iter().enumerate() {
if r < wk {
return own + k as u32;
}
r -= wk;
}
}
match HD_TILES.iter().find(|&&(base, n, _, _)| (base..base + n).contains(&id)) { match HD_TILES.iter().find(|&&(base, n, _, _)| (base..base + n).contains(&id)) {
Some(&(base, 4, _, _)) => base + (x & 1) as u32 + (z & 1) as u32 * 2, Some(&(base, 4, _, _)) => base + (x & 1) as u32 + (z & 1) as u32 * 2,
_ => id, _ => id,
@@ -384,6 +413,18 @@ fn gen_hd_atlas(tile: u32) -> Image {
} }
} }
} }
for &(_, own, png, png2, _) in HD_FLOORS.iter() {
let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd floor");
for v in 0..5 {
let id = own + v;
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 * tile + x, y));
}
}
}
}
img img
} }
@@ -2730,7 +2771,30 @@ mod hd_atlas_tests {
assert_eq!(px(&hd, ox + 7, oy + 40), px(&src, sx + 7, sy + 40)); assert_eq!(px(&hd, ox + 7, oy + 40), px(&src, sx + 7, sy + 40));
} }
} }
// the masters' floors: five slabs each in their own cells (inside the atlas)
for &(_, own, png, png2, _) in HD_FLOORS.iter() {
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), (5 * tile, tile));
assert!(own + 5 <= ATLAS_COLS * ATLAS_ROWS);
for v in 0..5 {
let id = own + v;
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
assert_eq!(px(&hd, ox + 9, oy + 30), px(&src, v * tile + 9, 30));
}
} }
}
// every slab of a floor shows up, the plain one most often
let mut n = [0u32; 5];
for z in 0..60 {
for x in 0..60 {
let id = hd_tile(T_CLAIM + (x as u32 % 4), x, z);
assert!((T_HDFLOOR..T_HDFLOOR + 5).contains(&id));
n[(id - T_HDFLOOR) as usize] += 1;
}
}
assert!(n.iter().all(|&k| k > 100), "{n:?}");
assert!(n[0] > n[1] * 2, "{n:?}");
assert!((T_HDFLOOR + 15..T_HDFLOOR + 20).contains(&hd_tile(T_RIVAL3 + 2, 3, 7)));
// four variants by position, two variants keep theirs, others stay // four variants by position, two variants keep theirs, others stay
assert_eq!(hd_tile(T_FLOOR + 3, 0, 0), T_FLOOR); assert_eq!(hd_tile(T_FLOOR + 3, 0, 0), T_FLOOR);
assert_eq!(hd_tile(T_FLOOR, 1, 1), T_FLOOR + 3); assert_eq!(hd_tile(T_FLOOR, 1, 1), T_FLOOR + 3);
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Cut a generated sheet of floor tiles (a grid on a magenta background) into single tiles for
the high-res atlas: every tile cropped to its square, magenta inside it (chipped corners, holes)
filled with a dark void, optionally brightened, scaled to the tile size and reduced to one
palette shared by all tiles of the sheet.
Usage:
make_floor_tiles.py raw.png out_prefix --grid 2x3 --pick 1,2,3,5,6 [--size 128]
[--colors 48] [--bright 1.0]
--pick which cells to keep, numbered row by row from 1
--bright factor on the colours (above 1: lighter)
Writes out_prefix-<size>.png: the picked tiles side by side, and out_prefix-<size>-preview.png
(a 6x4 floor of them laid out at random, scaled up 2x). Needs Pillow.
"""
import argparse
import random
from PIL import Image, ImageEnhance
VOID = (22, 14, 18)
def magenta(p):
r, g, b = p[:3]
return r > g + 60 and b > g + 60
def main():
ap = argparse.ArgumentParser()
ap.add_argument("raw")
ap.add_argument("out")
ap.add_argument("--grid", default="2x3")
ap.add_argument("--pick", default="1,2,3,4,5")
ap.add_argument("--size", type=int, default=128)
ap.add_argument("--colors", type=int, default=48)
ap.add_argument("--bright", type=float, default=1.0)
a = ap.parse_args()
rows, cols = (int(v) for v in a.grid.split("x"))
pick = [int(v) - 1 for v in a.pick.split(",")]
img = Image.open(a.raw).convert("RGB")
w, h = img.size
px = img.load()
tiles = []
for i in pick:
r, c = divmod(i, cols)
x0, x1, y0, y1 = c * w // cols, (c + 1) * w // cols, r * h // rows, (r + 1) * h // rows
# the tile's square: the columns and rows that are mostly not magenta
xs = [x for x in range(x0, x1) if sum(not magenta(px[x, y]) for y in range(y0, y1, 4)) > (y1 - y0) / 8]
ys = [y for y in range(y0, y1) if sum(not magenta(px[x, y]) for x in range(x0, x1, 4)) > (x1 - x0) / 8]
tile = img.crop((min(xs), min(ys), max(xs) + 1, max(ys) + 1))
tp = tile.load()
for y in range(tile.height):
for x in range(tile.width):
if magenta(tp[x, y]):
tp[x, y] = VOID
if a.bright != 1.0:
tile = ImageEnhance.Brightness(tile).enhance(a.bright)
tiles.append(tile.resize((a.size, a.size), Image.LANCZOS))
strip = Image.new("RGB", (a.size * len(tiles), a.size))
for k, t in enumerate(tiles):
strip.paste(t, (k * a.size, 0))
strip = strip.quantize(colors=a.colors, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE).convert("RGB")
strip.save(f"{a.out}-{a.size}.png")
rnd = random.Random(7)
prev = Image.new("RGB", (a.size * 6, a.size * 4))
for k in range(24):
v = rnd.randrange(len(tiles))
prev.paste(strip.crop((v * a.size, 0, (v + 1) * a.size, a.size)), ((k % 6) * a.size, (k // 6) * a.size))
prev.resize((prev.width * 2, prev.height * 2), Image.NEAREST).save(f"{a.out}-{a.size}-preview.png")
print(f"{a.out}-{a.size}.png: {len(tiles)} tiles")
if __name__ == "__main__":
main()