diff --git a/art/generated/textures/PROMPTS.md b/art/generated/textures/PROMPTS.md index 4bfbc43..f1eb84e 100644 --- a/art/generated/textures/PROMPTS.md +++ b/art/generated/textures/PROMPTS.md @@ -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. ``` -## claimed: T_CLAIM (claimed floor) +## claimed -```text -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. -``` +Replaced by the masters' floors with five slabs each, see `floors/PROMPTS.md`. ## earth: T_EARTH (earth blocks) diff --git a/art/generated/textures/floors/PROMPTS.md b/art/generated/textures/floors/PROMPTS.md new file mode 100644 index 0000000..32191dc --- /dev/null +++ b/art/generated/textures/floors/PROMPTS.md @@ -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). +`-64.png` / `-128.png` are copied to `assets/textures/floor-.png` / +`floor--128.png`; `-128-preview.png` shows a floor laid out at random. diff --git a/art/generated/textures/floors/mossgrim-128-preview.png b/art/generated/textures/floors/mossgrim-128-preview.png new file mode 100644 index 0000000..0a40e33 Binary files /dev/null and b/art/generated/textures/floors/mossgrim-128-preview.png differ diff --git a/art/generated/textures/floors/mossgrim-128.png b/art/generated/textures/floors/mossgrim-128.png new file mode 100644 index 0000000..267b897 Binary files /dev/null and b/art/generated/textures/floors/mossgrim-128.png differ diff --git a/art/generated/textures/floors/mossgrim-64-preview.png b/art/generated/textures/floors/mossgrim-64-preview.png new file mode 100644 index 0000000..b64f0d6 Binary files /dev/null and b/art/generated/textures/floors/mossgrim-64-preview.png differ diff --git a/art/generated/textures/floors/mossgrim-64.png b/art/generated/textures/floors/mossgrim-64.png new file mode 100644 index 0000000..0e3d409 Binary files /dev/null and b/art/generated/textures/floors/mossgrim-64.png differ diff --git a/art/generated/textures/floors/mossgrim-raw.png b/art/generated/textures/floors/mossgrim-raw.png new file mode 100644 index 0000000..930a302 Binary files /dev/null and b/art/generated/textures/floors/mossgrim-raw.png differ diff --git a/art/generated/textures/floors/nightwell-128-preview.png b/art/generated/textures/floors/nightwell-128-preview.png new file mode 100644 index 0000000..7590c61 Binary files /dev/null and b/art/generated/textures/floors/nightwell-128-preview.png differ diff --git a/art/generated/textures/floors/nightwell-128.png b/art/generated/textures/floors/nightwell-128.png new file mode 100644 index 0000000..28a4ab1 Binary files /dev/null and b/art/generated/textures/floors/nightwell-128.png differ diff --git a/art/generated/textures/floors/nightwell-64-preview.png b/art/generated/textures/floors/nightwell-64-preview.png new file mode 100644 index 0000000..6262875 Binary files /dev/null and b/art/generated/textures/floors/nightwell-64-preview.png differ diff --git a/art/generated/textures/floors/nightwell-64.png b/art/generated/textures/floors/nightwell-64.png new file mode 100644 index 0000000..0239d51 Binary files /dev/null and b/art/generated/textures/floors/nightwell-64.png differ diff --git a/art/generated/textures/floors/nightwell-raw.png b/art/generated/textures/floors/nightwell-raw.png new file mode 100644 index 0000000..3edbe9c Binary files /dev/null and b/art/generated/textures/floors/nightwell-raw.png differ diff --git a/art/generated/textures/floors/player-128-preview.png b/art/generated/textures/floors/player-128-preview.png new file mode 100644 index 0000000..24e77e1 Binary files /dev/null and b/art/generated/textures/floors/player-128-preview.png differ diff --git a/art/generated/textures/floors/player-128.png b/art/generated/textures/floors/player-128.png new file mode 100644 index 0000000..a9212da Binary files /dev/null and b/art/generated/textures/floors/player-128.png differ diff --git a/art/generated/textures/floors/player-64-preview.png b/art/generated/textures/floors/player-64-preview.png new file mode 100644 index 0000000..964622e Binary files /dev/null and b/art/generated/textures/floors/player-64-preview.png differ diff --git a/art/generated/textures/floors/player-64.png b/art/generated/textures/floors/player-64.png new file mode 100644 index 0000000..599bf5e Binary files /dev/null and b/art/generated/textures/floors/player-64.png differ diff --git a/art/generated/textures/floors/player-raw.png b/art/generated/textures/floors/player-raw.png new file mode 100644 index 0000000..7fb2b90 Binary files /dev/null and b/art/generated/textures/floors/player-raw.png differ diff --git a/art/generated/textures/floors/saffrax-128-preview.png b/art/generated/textures/floors/saffrax-128-preview.png new file mode 100644 index 0000000..16e62ca Binary files /dev/null and b/art/generated/textures/floors/saffrax-128-preview.png differ diff --git a/art/generated/textures/floors/saffrax-128.png b/art/generated/textures/floors/saffrax-128.png new file mode 100644 index 0000000..0b10232 Binary files /dev/null and b/art/generated/textures/floors/saffrax-128.png differ diff --git a/art/generated/textures/floors/saffrax-64-preview.png b/art/generated/textures/floors/saffrax-64-preview.png new file mode 100644 index 0000000..7e86794 Binary files /dev/null and b/art/generated/textures/floors/saffrax-64-preview.png differ diff --git a/art/generated/textures/floors/saffrax-64.png b/art/generated/textures/floors/saffrax-64.png new file mode 100644 index 0000000..cde0a01 Binary files /dev/null and b/art/generated/textures/floors/saffrax-64.png differ diff --git a/art/generated/textures/floors/saffrax-raw.png b/art/generated/textures/floors/saffrax-raw.png new file mode 100644 index 0000000..96a445c Binary files /dev/null and b/art/generated/textures/floors/saffrax-raw.png differ diff --git a/assets/textures/claimed-256.png b/assets/textures/claimed-256.png deleted file mode 100644 index bef1c06..0000000 Binary files a/assets/textures/claimed-256.png and /dev/null differ diff --git a/assets/textures/claimed.png b/assets/textures/claimed.png deleted file mode 100644 index 8a2cd2e..0000000 Binary files a/assets/textures/claimed.png and /dev/null differ diff --git a/assets/textures/floor-mossgrim-128.png b/assets/textures/floor-mossgrim-128.png new file mode 100644 index 0000000..267b897 Binary files /dev/null and b/assets/textures/floor-mossgrim-128.png differ diff --git a/assets/textures/floor-mossgrim.png b/assets/textures/floor-mossgrim.png new file mode 100644 index 0000000..0e3d409 Binary files /dev/null and b/assets/textures/floor-mossgrim.png differ diff --git a/assets/textures/floor-nightwell-128.png b/assets/textures/floor-nightwell-128.png new file mode 100644 index 0000000..28a4ab1 Binary files /dev/null and b/assets/textures/floor-nightwell-128.png differ diff --git a/assets/textures/floor-nightwell.png b/assets/textures/floor-nightwell.png new file mode 100644 index 0000000..0239d51 Binary files /dev/null and b/assets/textures/floor-nightwell.png differ diff --git a/assets/textures/floor-player-128.png b/assets/textures/floor-player-128.png new file mode 100644 index 0000000..a9212da Binary files /dev/null and b/assets/textures/floor-player-128.png differ diff --git a/assets/textures/floor-player.png b/assets/textures/floor-player.png new file mode 100644 index 0000000..599bf5e Binary files /dev/null and b/assets/textures/floor-player.png differ diff --git a/assets/textures/floor-saffrax-128.png b/assets/textures/floor-saffrax-128.png new file mode 100644 index 0000000..0b10232 Binary files /dev/null and b/assets/textures/floor-saffrax-128.png differ diff --git a/assets/textures/floor-saffrax.png b/assets/textures/floor-saffrax.png new file mode 100644 index 0000000..cde0a01 Binary files /dev/null and b/assets/textures/floor-saffrax.png differ diff --git a/src/gfx.rs b/src/gfx.rs index 8e25559..f74c352 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -6,7 +6,7 @@ use macroquad::prelude::*; pub const CELL: f32 = 16.0; 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) 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_TILE2: u32 = 128; // (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_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_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")), @@ -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")), ]; +// 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 -// 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 { + 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::(); + 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)) { Some(&(base, 4, _, _)) => base + (x & 1) as u32 + (z & 1) as u32 * 2, _ => 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 } @@ -2730,7 +2771,30 @@ mod hd_atlas_tests { 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 assert_eq!(hd_tile(T_FLOOR + 3, 0, 0), T_FLOOR); assert_eq!(hd_tile(T_FLOOR, 1, 1), T_FLOOR + 3); diff --git a/tools/make_floor_tiles.py b/tools/make_floor_tiles.py new file mode 100644 index 0000000..0089e15 --- /dev/null +++ b/tools/make_floor_tiles.py @@ -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-.png: the picked tiles side by side, and out_prefix--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()