Claimed floors back to fitted flagstones in the masters' colours with damage; high-res floor decals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-07 22:11:41 +01:00
1 parent 362f4e7670
commit ca8494aea0
51 files changed
+154 -39

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
# Floor decals (high-res)
Wear and tear the game lays over some plain floor tiles (`D_RUBBLE`, `D_CRACK`; `HD_DECALS` in
`src/gfx.rs`). Generated on 2026-11-07 with `fal-ai/nano-banana-pro` (`aspect_ratio: 1:1`,
`resolution: 1K`), the decal's sentence followed by the shared text:
```text
Seen straight from above (orthographic top-down, no perspective), flat even lighting, no cast shadows. Hand-painted pixel art style for a dark-fantasy dungeon game, crisp edges, readable at small size. The background is a completely flat pure magenta (#FF00FF) everywhere else - no floor, no stones, no texture behind it, no frame, no text.
```
## rubble
```text
An overlay decal for a dungeon floor: a loose scatter of small grey-brown pebbles, stone fragments and a little grit and dust, spread unevenly over the whole square image, denser in one corner, about thirty small pieces of different sizes, each with a dark outline.
```
## crack
```text
An overlay decal for a dungeon floor: one long dark jagged crack running diagonally across the whole square image from corner to corner, with a few short branching side cracks and two or three tiny chipped splinters beside it; only the dark crack lines themselves.
```
## Processing
`tools/make_decal.py` keys out the magenta and scales down with hard alpha; the crack is made
neutral grey (its lines took on the magenta):
```bash
python3 tools/make_decal.py rubble-raw.png rubble --size 128 # and --size 64
python3 tools/make_decal.py crack-raw.png crack --size 128 --gray
```
Copied to `assets/textures/decal-<name>.png` (64) and `decal-<name>-128.png`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 KiB

+21 -27
View File
@@ -1,47 +1,41 @@
# The masters' claimed floors (high-res) # The masters' claimed floors (high-res)
Generated on 2026-11-07 with fal.ai. The same grey, worn-smooth castle flagstones for every Generated on 2026-11-07 with fal.ai, close to the first claimed texture (`../claimed-raw.png`,
master; every tile has a polished inlay in the master's colour with a light engraving of their `../claimed-128.png`): fitted flagstones of different sizes with faint carved symbols, in the
own, so ownership shows on every tile (`HD_FLOORS` in `src/gfx.rs`). master's colour, now with cracked, chipped and broken stones (`HD_SETS` in `src/gfx.rs`).
Earlier versions: `v1/` (regular coloured slabs with thick grout, too even), `v2/` (roughly hewn Earlier versions: `v1/` (regular coloured slabs with thick grout), `v2/` (roughly hewn stones
stones fully in the master's colour, too glaring). fully in the master's colour, too glaring), `v3/` (grey stones with a coloured inlay).
1. The player's sheet: `fal-ai/nano-banana-pro`, `aspect_ratio: 3:2`, `resolution: 2K`; six tiles 1. The player: `fal-ai/nano-banana-pro/edit` on `../claimed-raw.png` (`aspect_ratio: 1:1`,
in a 3x2 grid on magenta, the prompt below with the player's inlay and engraving. `resolution: 1K`) adds the damage:
2. The other masters: `fal-ai/nano-banana-pro/edit` with the player's sheet as the reference, so
the grey stones stay exactly the same and only the inlays change.
Prompt for the player's sheet (`{inlay}`, `{engraving}` below):
```text ```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, no vignette, and fills its square completely. Every tile is a piece of the floor of an old medieval castle dungeon: irregular, roughly hewn flagstones of neutral medium-grey stone (muted, desaturated, not glaring) of different sizes and shapes, laid with uneven dark-grey mortar joints - not a regular grid. Centuries of feet have worn the stones smooth: soft sheen, rounded worn edges, shallow dips. The joints along the four edges of every tile are as thin and irregular as the ones inside, so the tiles fit together in any arrangement without a visible grid. In the middle of every tile one polished inlay is set into the floor: a small rounded diamond-shaped inset of {inlay}, about a quarter of the tile wide, framed by a thin dark rim, with a light shallow engraving in it: {engraving}. The six tiles differ only in the grey stones around the inlay: tile 1 plain worn stones; tile 2 plain stones of other sizes; tile 3 with hairline cracks in the grey stones; tile 4 with a chipped grey stone corner and missing splinters; tile 5 with a broken grey flagstone beside the inlay, cracked through, a piece sunk and loose rubble; tile 6 with a few small scratches. Style: hand-painted pixel art, crisp edges, readable at small size. No text, no labels, no numbers. Keep this seamless tileable floor texture exactly as it is - the same fitted dark crimson flagstones of different sizes, the same thin dark joints, the faint carved runes, the same top-down view, colours and style - and only add some wear and damage spread evenly over the whole image: about eight stones with hairline cracks, four stones with chipped corners, and two broken stones cracked through with a few small loose splinters. Keep the texture seamless across all four edges. No text.
``` ```
Edit prompt for the others: 2. The others: `fal-ai/nano-banana-pro/edit` on the player's result, so the stones stay the same:
```text ```text
Keep this image exactly the same - the same grey stones, mortar joints, cracks, damage, layout, magenta gaps and pixel art style - and change only the six small diamond-shaped inlays in the middle of the tiles: make them {inlay}, with a light shallow engraving of {engraving} in every inlay instead of the skull and runes. Do not change anything else. Keep this seamless tileable floor texture exactly the same - the same flagstone layout, joints, cracks, chipped and broken stones, top-down view and style - and change only the colour of the stones to {colour}, and the faint carved symbols on some stones to {symbols}. Keep it seamless across all four edges. No text.
``` ```
| master | inlay | engraving | | master | colour | symbols |
|---|---|---| |---|---|---|
| player | deep crimson-red polished stone | a small horned demon skull and demonic rune marks | | player | dark crimson (as the reference) | faint carved demonic runes (as the reference) |
| mossgrim | mossy green polished stone | a small mushroom with spores and a coiling mould tendril | | mossgrim | a dark mossy green with a little moss in some joints | faint carved mushrooms and spirals of mould |
| nightwell | deep blue polished stone with a silver rim | a small lily under a crescent moon | | nightwell | a dark cold midnight blue | faint carved lilies and crescent moons |
| saffrax | golden-yellow polished stone with a brass rim | a small alchemical sun sigil | | saffrax | a dark ochre brown-yellow | faint carved alchemical sun and sulphur sigils |
## Processing ## Processing
Tiles 1, 3, 4, 5, 6 are used (plain, cracked, chipped, broken, scratched; tile 2 nearly repeats Five tiles that fit in any order (one shared seamless border, different middles), brightened
tile 1), weighted 4:2:2:1:3 in the game. `tools/make_floor_tiles.py` crops the tiles, fills (the reference was too dark):
magenta inside them with a dark void, cuts a thin edge off, evens out the light and scales;
the colours are kept (a palette would let the large grey areas swallow the inlays' colours):
```bash ```bash
python3 tools/make_floor_tiles.py <name>-raw.png <name> --grid 2x3 --pick 1,3,4,5,6 --size 128 --colors 0 --inset 0.03 --flatten python3 tools/make_tile.py <name>-raw.png <name> --size 256 --colors 48 --band 28 --variants 5 --bright 1.6 # 128x128
# the same with --size 64 python3 tools/make_tile.py <name>-raw.png <name> --size 128 --colors 32 --band 14 --variants 5 --bright 1.6 # 64x64
``` ```
`<name>-64.png` / `<name>-128.png` are copied to `assets/textures/floor-<name>.png` / `<name>-v64.png` / `<name>-v128.png` are copied to `assets/textures/floor-<name>.png` /
`floor-<name>-128.png`; `<name>-128-preview.png` shows a floor laid out at random. `floor-<name>-128.png`.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 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.

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 27 KiB

+36 -10
View File
@@ -347,19 +347,27 @@ const HD_TILES: [(u32, u32, &[u8], &[u8]); 2] = [
// Sets of five tiles that fit together in any order, so a floor or a wall shows no pattern: // Sets of five tiles that fit together in any order, so a floor or a wall shows no pattern:
// the ground kinds (all five tiles share one seamless border, their middles differ; see // the ground kinds (all five tiles share one seamless border, their middles differ; see
// tools/make_tile.py --variants) and the masters' claimed floors (the same grey, worn-smooth // tools/make_tile.py --variants), the masters' claimed floors as well: fitted flagstones of
// castle flagstones for everybody, every tile with a polished inlay in its master's colour and // different sizes, some cracked, chipped or broken, in the master's colour with faint carved
// an engraving of their own: the player a demon skull, Mossgrim a mushroom, Lady Nightwell a lily // symbols of their own (the player crimson with demonic runes, Mossgrim mossy green with
// under the moon, Saffrax an alchemical sun; plain, cracked, chipped, broken, scratched). They go // mushrooms and mould, Lady Nightwell midnight blue with lilies and moons, Saffrax ochre with
// alchemical sigils). They go
// into cells of their own from T_HDSET on, five per set; a tile picks one by its position, // into cells of their own from T_HDSET on, five per set; a tile picks one by its position,
// weighted. (the kind's cells: first and count, the first own cell, 64x64 tiles side by side, // weighted. (the kind's cells: first and count, the first own cell, 64x64 tiles side by side,
// 128x128 ones, weights) // 128x128 ones, weights)
pub const T_HDSET: u32 = 224; pub const T_HDSET: u32 = 224;
// the floor decals (wear and tear laid over plain floors) in the high-res style: transparent
// pictures for their cells (cell, 64x64, 128x128)
const HD_DECALS: [(u32, &[u8], &[u8]); 2] = [
(D_RUBBLE, include_bytes!("../assets/textures/decal-rubble.png"), include_bytes!("../assets/textures/decal-rubble-128.png")),
(D_CRACK, include_bytes!("../assets/textures/decal-crack.png"), include_bytes!("../assets/textures/decal-crack-128.png")),
];
const HD_SETS: [(u32, u32, u32, &[u8], &[u8], [u32; 5]); 11] = [ const HD_SETS: [(u32, u32, u32, &[u8], &[u8], [u32; 5]); 11] = [
(T_CLAIM, 4, T_HDSET, include_bytes!("../assets/textures/floor-player.png"), include_bytes!("../assets/textures/floor-player-128.png"), [4, 2, 2, 1, 3]), (T_CLAIM, 4, T_HDSET, include_bytes!("../assets/textures/floor-player.png"), include_bytes!("../assets/textures/floor-player-128.png"), [1; 5]),
(T_RIVAL1, 4, T_HDSET + 5, include_bytes!("../assets/textures/floor-mossgrim.png"), include_bytes!("../assets/textures/floor-mossgrim-128.png"), [4, 2, 2, 1, 3]), (T_RIVAL1, 4, T_HDSET + 5, include_bytes!("../assets/textures/floor-mossgrim.png"), include_bytes!("../assets/textures/floor-mossgrim-128.png"), [1; 5]),
(T_RIVAL2, 4, T_HDSET + 10, include_bytes!("../assets/textures/floor-nightwell.png"), include_bytes!("../assets/textures/floor-nightwell-128.png"), [4, 2, 2, 1, 3]), (T_RIVAL2, 4, T_HDSET + 10, include_bytes!("../assets/textures/floor-nightwell.png"), include_bytes!("../assets/textures/floor-nightwell-128.png"), [1; 5]),
(T_RIVAL3, 4, T_HDSET + 15, include_bytes!("../assets/textures/floor-saffrax.png"), include_bytes!("../assets/textures/floor-saffrax-128.png"), [4, 2, 2, 1, 3]), (T_RIVAL3, 4, T_HDSET + 15, include_bytes!("../assets/textures/floor-saffrax.png"), include_bytes!("../assets/textures/floor-saffrax-128.png"), [1; 5]),
(T_FLOOR, 4, T_HDSET + 20, include_bytes!("../assets/textures/floor-v64.png"), include_bytes!("../assets/textures/floor-v128.png"), [1; 5]), (T_FLOOR, 4, T_HDSET + 20, include_bytes!("../assets/textures/floor-v64.png"), include_bytes!("../assets/textures/floor-v128.png"), [1; 5]),
(T_EARTH, 4, T_HDSET + 25, include_bytes!("../assets/textures/earth-v64.png"), include_bytes!("../assets/textures/earth-v128.png"), [1; 5]), (T_EARTH, 4, T_HDSET + 25, include_bytes!("../assets/textures/earth-v64.png"), include_bytes!("../assets/textures/earth-v128.png"), [1; 5]),
(T_ROCK, 4, T_HDSET + 30, include_bytes!("../assets/textures/rock-v64.png"), include_bytes!("../assets/textures/rock-v128.png"), [1; 5]), (T_ROCK, 4, T_HDSET + 30, include_bytes!("../assets/textures/rock-v64.png"), include_bytes!("../assets/textures/rock-v128.png"), [1; 5]),
@@ -425,6 +433,15 @@ fn gen_hd_atlas(tile: u32) -> Image {
} }
} }
} }
for &(id, png, png2) in HD_DECALS.iter() {
let src = Image::from_file_with_format(if tile == HD_TILE2 { png2 } else { png }, Some(ImageFormat::Png)).expect("hd decal");
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(x, y));
}
}
}
img img
} }
@@ -2783,7 +2800,16 @@ mod hd_atlas_tests {
} }
} }
} }
// every slab of a floor shows up, the plain one most often // the decals: transparent around their pictures
for &(id, _, _) in HD_DECALS.iter() {
for tile in [HD_TILE, HD_TILE2] {
let hd = gen_hd_atlas(tile);
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
let clear = (0..tile * tile).filter(|i| hd.get_pixel(ox + i % tile, oy + i / tile).a < 0.5).count() as u32;
assert!(clear > tile * tile / 3 && clear < tile * tile, "decal {id} at {tile}: {clear} clear texels");
}
}
// every tile of a set shows up
let mut n = [0u32; 5]; let mut n = [0u32; 5];
for z in 0..60 { for z in 0..60 {
for x in 0..60 { for x in 0..60 {
@@ -2793,7 +2819,7 @@ mod hd_atlas_tests {
} }
} }
assert!(n.iter().all(|&k| k > 100), "{n:?}"); assert!(n.iter().all(|&k| k > 100), "{n:?}");
assert!(n[0] > n[3] * 2, "{n:?}");
assert!((T_HDSET + 15..T_HDSET + 20).contains(&hd_tile(T_RIVAL3 + 2, 3, 7))); assert!((T_HDSET + 15..T_HDSET + 20).contains(&hd_tile(T_RIVAL3 + 2, 3, 7)));
assert!((T_HDSET + 50..T_HDSET + 55).contains(&hd_tile(T_DIAMOND + 1, 8, 2))); assert!((T_HDSET + 50..T_HDSET + 55).contains(&hd_tile(T_DIAMOND + 1, 8, 2)));
assert!((T_HDSET + 25..T_HDSET + 30).contains(&hd_tile(T_EARTH + 3, 5, 5))); assert!((T_HDSET + 25..T_HDSET + 30).contains(&hd_tile(T_EARTH + 3, 5, 5)));
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Turn a generated decal (a picture on a flat magenta background) into a transparent tile for
the high-res atlas: magenta keyed out, scaled down without magenta fringes (premultiplied), the
alpha made hard (pixels are either there or not, like the rest of the pixel art).
Usage:
make_decal.py raw.png out_prefix --size 128 [--gray]
--gray neutral grey (a dark crack whose lines took on the magenta of the background)
Writes out_prefix-<size>.png and out_prefix-<size>-preview.png (on a grey floor, scaled up 3x).
Needs Pillow.
"""
import argparse
from PIL import Image
def main():
ap = argparse.ArgumentParser()
ap.add_argument("raw")
ap.add_argument("out")
ap.add_argument("--size", type=int, default=128)
ap.add_argument("--gray", action="store_true")
a = ap.parse_args()
img = Image.open(a.raw).convert("RGBA")
px = img.load()
for y in range(img.height):
for x in range(img.width):
r, g, b, _ = px[x, y]
if r > g + 40 and b > g + 40:
px[x, y] = (0, 0, 0, 0)
# premultiplied scaling: transparent pixels add nothing to the colour
small = img.resize((a.size, a.size), Image.BOX)
sp = small.load()
for y in range(a.size):
for x in range(a.size):
r, g, b, al = sp[x, y]
if al < 110:
sp[x, y] = (0, 0, 0, 0)
else:
k = 255 / al
c = (min(255, int(r * k)), min(255, int(g * k)), min(255, int(b * k)))
if a.gray:
v = min(c) * 0.9
c = (int(v), int(v), int(v * 1.05))
sp[x, y] = c + (255,)
# Pillow's BOX filter works on straight alpha; transparent pixels were black, so colours near
# the edges come out darker, which suits the dark outlines
small.save(f"{a.out}-{a.size}.png")
prev = Image.new("RGBA", (a.size, a.size), (90, 86, 84, 255))
prev.alpha_composite(small)
prev.resize((a.size * 3, a.size * 3), Image.NEAREST).save(f"{a.out}-{a.size}-preview.png")
print(f"{a.out}-{a.size}.png")
if __name__ == "__main__":
main()
+5 -2
View File
@@ -2,7 +2,7 @@
"""Turn a generated texture into a seamless pixel-art tile sheet for the high-res atlas. """Turn a generated texture into a seamless pixel-art tile sheet for the high-res atlas.
Usage: Usage:
make_tile.py raw.png out_prefix [--size 128] [--colors 32] [--band 14] [--variants N] make_tile.py raw.png out_prefix [--size 128] [--colors 32] [--band 14] [--variants N] [--bright F]
The picture is scaled down to size x size (default 128: a 2x2 block of 64x64 tiles; the game The picture is scaled down to size x size (default 128: a 2x2 block of 64x64 tiles; the game
lays the four quarters out by the tiles' positions, so the floor repeats only every two tiles), lays the four quarters out by the tiles' positions, so the floor repeats only every two tiles),
@@ -20,7 +20,7 @@ out_prefix-v<tile>-preview.png (a floor of them laid out at random). Needs Pillo
import argparse import argparse
import random import random
from PIL import Image, ImageChops from PIL import Image, ImageChops, ImageEnhance
def seamless(img, band): def seamless(img, band):
@@ -75,12 +75,15 @@ def main():
ap.add_argument("--colors", type=int, default=32) ap.add_argument("--colors", type=int, default=32)
ap.add_argument("--band", type=int, default=14) ap.add_argument("--band", type=int, default=14)
ap.add_argument("--variants", type=int, default=0) ap.add_argument("--variants", type=int, default=0)
ap.add_argument("--bright", type=float, default=1.0)
a = ap.parse_args() a = ap.parse_args()
img = Image.open(a.raw).convert("RGB") img = Image.open(a.raw).convert("RGB")
s = min(img.size) s = min(img.size)
img = img.crop(((img.width - s) // 2, (img.height - s) // 2, (img.width + s) // 2, (img.height + s) // 2)) img = img.crop(((img.width - s) // 2, (img.height - s) // 2, (img.width + s) // 2, (img.height + s) // 2))
img = img.resize((a.size, a.size), Image.LANCZOS) img = img.resize((a.size, a.size), Image.LANCZOS)
if a.bright != 1.0:
img = ImageEnhance.Brightness(img).enhance(a.bright)
img = seamless(img, a.band) img = seamless(img, a.band)
if a.variants: if a.variants:
return variants(img, a) return variants(img, a)