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>
No files matched your search
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 4.1 MiB |
|
After Width: | Height: | Size: 397 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 4.4 MiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -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);
|
||||||
|
|||||||
@@ -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()
|
||||||