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>
No files matched your search
@@ -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`.
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 902 B |
|
After Width: | Height: | Size: 864 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 836 KiB |
@@ -1,47 +1,41 @@
|
||||
# The masters' claimed floors (high-res)
|
||||
|
||||
Generated on 2026-11-07 with fal.ai. The same grey, worn-smooth castle flagstones for every
|
||||
master; every tile has a polished inlay in the master's colour with a light engraving of their
|
||||
own, so ownership shows on every tile (`HD_FLOORS` in `src/gfx.rs`).
|
||||
Generated on 2026-11-07 with fal.ai, close to the first claimed texture (`../claimed-raw.png`,
|
||||
`../claimed-128.png`): fitted flagstones of different sizes with faint carved symbols, in the
|
||||
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
|
||||
stones fully in the master's colour, too glaring).
|
||||
Earlier versions: `v1/` (regular coloured slabs with thick grout), `v2/` (roughly hewn stones
|
||||
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
|
||||
in a 3x2 grid on magenta, the prompt below with the player's inlay and engraving.
|
||||
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):
|
||||
1. The player: `fal-ai/nano-banana-pro/edit` on `../claimed-raw.png` (`aspect_ratio: 1:1`,
|
||||
`resolution: 1K`) adds the damage:
|
||||
|
||||
```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
|
||||
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 |
|
||||
| mossgrim | mossy green polished stone | a small mushroom with spores and a coiling mould tendril |
|
||||
| nightwell | deep blue polished stone with a silver rim | a small lily under a crescent moon |
|
||||
| saffrax | golden-yellow polished stone with a brass rim | a small alchemical sun sigil |
|
||||
| player | dark crimson (as the reference) | faint carved demonic runes (as the reference) |
|
||||
| mossgrim | a dark mossy green with a little moss in some joints | faint carved mushrooms and spirals of mould |
|
||||
| nightwell | a dark cold midnight blue | faint carved lilies and crescent moons |
|
||||
| saffrax | a dark ochre brown-yellow | faint carved alchemical sun and sulphur sigils |
|
||||
|
||||
## Processing
|
||||
|
||||
Tiles 1, 3, 4, 5, 6 are used (plain, cracked, chipped, broken, scratched; tile 2 nearly repeats
|
||||
tile 1), weighted 4:2:2:1:3 in the game. `tools/make_floor_tiles.py` crops the tiles, fills
|
||||
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):
|
||||
Five tiles that fit in any order (one shared seamless border, different middles), brightened
|
||||
(the reference was too dark):
|
||||
|
||||
```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
|
||||
# the same with --size 64
|
||||
python3 tools/make_tile.py <name>-raw.png <name> --size 256 --colors 48 --band 28 --variants 5 --bright 1.6 # 128x128
|
||||
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` /
|
||||
`floor-<name>-128.png`; `<name>-128-preview.png` shows a floor laid out at random.
|
||||
`<name>-v64.png` / `<name>-v128.png` are copied to `assets/textures/floor-<name>.png` /
|
||||
`floor-<name>-128.png`.
|
||||
|
Before Width: | Height: | Size: 4.7 MiB After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 388 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 4.7 MiB After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 356 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 4.6 MiB After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 375 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 4.7 MiB After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 4.7 MiB |
|
After Width: | Height: | Size: 4.7 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 4.7 MiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 902 B |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 27 KiB |
@@ -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:
|
||||
// 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
|
||||
// castle flagstones for everybody, every tile with a polished inlay in its master's colour and
|
||||
// an engraving of their own: the player a demon skull, Mossgrim a mushroom, Lady Nightwell a lily
|
||||
// under the moon, Saffrax an alchemical sun; plain, cracked, chipped, broken, scratched). They go
|
||||
// tools/make_tile.py --variants), the masters' claimed floors as well: fitted flagstones of
|
||||
// different sizes, some cracked, chipped or broken, in the master's colour with faint carved
|
||||
// symbols of their own (the player crimson with demonic runes, Mossgrim mossy green with
|
||||
// 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,
|
||||
// weighted. (the kind's cells: first and count, the first own cell, 64x64 tiles side by side,
|
||||
// 128x128 ones, weights)
|
||||
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] = [
|
||||
(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_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_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_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_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"), [1; 5]),
|
||||
(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"), [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_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
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
for z 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[0] > n[3] * 2, "{n:?}");
|
||||
|
||||
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 + 25..T_HDSET + 30).contains(&hd_tile(T_EARTH + 3, 5, 5)));
|
||||
|
||||
@@ -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()
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Turn a generated texture into a seamless pixel-art tile sheet for the high-res atlas.
|
||||
|
||||
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
|
||||
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 random
|
||||
|
||||
from PIL import Image, ImageChops
|
||||
from PIL import Image, ImageChops, ImageEnhance
|
||||
|
||||
|
||||
def seamless(img, band):
|
||||
@@ -75,12 +75,15 @@ def main():
|
||||
ap.add_argument("--colors", type=int, default=32)
|
||||
ap.add_argument("--band", type=int, default=14)
|
||||
ap.add_argument("--variants", type=int, default=0)
|
||||
ap.add_argument("--bright", type=float, default=1.0)
|
||||
a = ap.parse_args()
|
||||
|
||||
img = Image.open(a.raw).convert("RGB")
|
||||
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.resize((a.size, a.size), Image.LANCZOS)
|
||||
if a.bright != 1.0:
|
||||
img = ImageEnhance.Brightness(img).enhance(a.bright)
|
||||
img = seamless(img, a.band)
|
||||
if a.variants:
|
||||
return variants(img, a)
|
||||
|
||||