From fd429fffb9328515beb120da2807faf5305a6e0c Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 28 Sep 2026 08:42:59 +0800 Subject: feat(art): P34 素材扩展切片(水面/屋顶/墙体/栏杆) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cc0-tiles.png 由 4 格扩到 12 格:0-3 仍 Kenney 地面,4-11 为自绘构件 (4 水A/5 水B/6 屋脊/7 屋檐/8 木构墙/9 木格窗/10 栏杆/11 门) - 自绘 2 相位水面 cc0WaterTileIndexAt=(tick/32+x+2y)%2,单帧覆盖双相位; 河道与水田/水网共用;江南 fallback 不变 - drawWorld 接入:cc0 模式下民居屋顶/墙/窗/门、桥栏杆、水面走真素材,其余道具仍江南 - AssetPackTests 更新图集尺寸(12 格)+整数缩放,新增构件索引与双水相位用例 - docs/素材来源.md 记录自绘清单;同步 assets-and-licenses 与维护说明 P34 小节 - docs/evidence/p34-*.png 昼/夜/2x 特写/水波/对照(check-no-white-blocks 0 命中) --- docs/assets-and-licenses.md | 2 +- docs/evidence/p34-baseline-day.png | Bin 0 -> 58773 bytes docs/evidence/p34-realassets-closeup-2x.png | Bin 0 -> 20899 bytes docs/evidence/p34-realassets-day.png | Bin 0 -> 47319 bytes docs/evidence/p34-realassets-night.png | Bin 0 -> 57561 bytes docs/evidence/p34-water-2x.png | Bin 0 -> 8014 bytes ...264\240\346\235\220\346\235\245\346\272\220.md" | 45 +++++-- ...273\264\346\212\244\350\257\264\346\230\216.md" | 36 ++++++ scripts/make-cc0-art.py | 141 +++++++++++++++++++-- src/LivingVillage.Desktop.Tests/AssetPackTests.fs | 32 ++++- src/LivingVillage.Desktop/Assets/cc0-tiles.png | Bin 388 -> 816 bytes src/LivingVillage.Desktop/VillageArt.fs | 95 ++++++++++++-- 12 files changed, 310 insertions(+), 41 deletions(-) create mode 100644 docs/evidence/p34-baseline-day.png create mode 100644 docs/evidence/p34-realassets-closeup-2x.png create mode 100644 docs/evidence/p34-realassets-day.png create mode 100644 docs/evidence/p34-realassets-night.png create mode 100644 docs/evidence/p34-water-2x.png diff --git a/docs/assets-and-licenses.md b/docs/assets-and-licenses.md index 1d691a7..5b13f49 100644 --- a/docs/assets-and-licenses.md +++ b/docs/assets-and-licenses.md @@ -8,7 +8,7 @@ - 来源:`/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc`,字体族包含 Noto Sans CJK SC。 - 许可:SIL Open Font License 1.1;本地版权文件 SHA-256:`849f4ea9c214fa4ac3593b770c699f387534b11ce671264c1b10d85bdcb5997b`。 - 参考:。 -- **第三方 CC0 真实像素素材图集(P33 起)**:`src/LivingVillage.Desktop/Assets/cc0-tiles.png`(128x32,地块)与 `cc0-characters.png`(3840x48,角色)由 `scripts/make-cc0-art.py` 从两个 CC0 素材包确定性再生成(整数 2x NEAREST): +- **第三方 CC0 真实像素素材图集(P33 起)**:`src/LivingVillage.Desktop/Assets/cc0-tiles.png`(384x32:前 4 格地块来自 Kenney,后 8 格水面/屋顶/墙体/栏杆/门为 P34 仓库自绘原创)与 `cc0-characters.png`(3840x48,角色)由 `scripts/make-cc0-art.py` 从两个 CC0 素材包确定性再生成(整数 2x NEAREST): - **Kenney Tiny Farm 1.0**:来源 ,许可 CC0 1.0(包内 `License.txt`),zip SHA-256 `a06f75f3…decc`。 - **Puny Characters(Shade)**:来源 ,许可 CC0 1.0(发布页),zip SHA-256 `971d7b07…320e`。 - 原始 zip 记录于 `third_party/`;用途/构建/开关详见 `docs/素材来源.md`。运行时仅在 `LV_ASSET_PACK=cc0` 时启用,默认仍走程序绘制江南图集。 diff --git a/docs/evidence/p34-baseline-day.png b/docs/evidence/p34-baseline-day.png new file mode 100644 index 0000000..7e3057b Binary files /dev/null and b/docs/evidence/p34-baseline-day.png differ diff --git a/docs/evidence/p34-realassets-closeup-2x.png b/docs/evidence/p34-realassets-closeup-2x.png new file mode 100644 index 0000000..b9cde48 Binary files /dev/null and b/docs/evidence/p34-realassets-closeup-2x.png differ diff --git a/docs/evidence/p34-realassets-day.png b/docs/evidence/p34-realassets-day.png new file mode 100644 index 0000000..e447502 Binary files /dev/null and b/docs/evidence/p34-realassets-day.png differ diff --git a/docs/evidence/p34-realassets-night.png b/docs/evidence/p34-realassets-night.png new file mode 100644 index 0000000..72a4083 Binary files /dev/null and b/docs/evidence/p34-realassets-night.png differ diff --git a/docs/evidence/p34-water-2x.png b/docs/evidence/p34-water-2x.png new file mode 100644 index 0000000..1599a85 Binary files /dev/null and b/docs/evidence/p34-water-2x.png differ diff --git "a/docs/\347\264\240\346\235\220\346\235\245\346\272\220.md" "b/docs/\347\264\240\346\235\220\346\235\245\346\272\220.md" index dd193de..31494e9 100644 --- "a/docs/\347\264\240\346\235\220\346\235\245\346\272\220.md" +++ "b/docs/\347\264\240\346\235\220\346\235\245\346\272\220.md" @@ -1,8 +1,8 @@ # 第三方像素素材来源与许可 -P33 开始把「程序几何绘制」的样例村庄逐步替换为**真实像素素材**(统一像素风格)。 -本单只做第一条竖直切片:样例村庄的一类地面(草地/石板)与一类角色(村民/主角)接入 -CC0 素材,其余仍走原程序绘制江南图集作为 fallback。 +P33 开始把「程序几何绘制」的样例村庄逐步替换为**真实像素素材**(统一像素风格), +P34 扩展到水面 / 屋顶 / 墙体 / 栏杆等构件。仍是竖直切片,不做全村,其余走原程序绘制 +江南图集作为 fallback。 - **开关**:`LV_ASSET_PACK=cc0` 时启用真实素材包;未设置或取其它值一律回退到程序绘制 图集(`Assets/jiangnan-*.png`)。图集选择入口见 `src/LivingVillage.Desktop/VillageArt.fs` @@ -11,11 +11,12 @@ CC0 素材,其余仍走原程序绘制江南图集作为 fallback。 ```bash python3 scripts/make-cc0-art.py - # -> src/LivingVillage.Desktop/Assets/cc0-tiles.png 128x32 (4 个 32x32 地面格) + # -> src/LivingVillage.Desktop/Assets/cc0-tiles.png 384x32 (12 个 32x32 格) # -> src/LivingVillage.Desktop/Assets/cc0-characters.png 3840x48 (5 变体 x 4 向 x 6 帧, 32x48 格) ``` - 源素材 16x16 / 16x24,**整数 2x NEAREST** 放大到 32x32 / 32x48;缩放断言见 + `cc0-tiles.png` 前 4 格来自 Kenney(源 16x16,**整数 2x NEAREST** 到 32x32),后 8 格为 + P34 自绘(详见第 3 节);角色图为 Puny 源 16x24 整数 2x 到 32x48。缩放断言见 `src/LivingVillage.Desktop.Tests/AssetPackTests.fs`。 ## 1. Kenney Tiny Farm(地块) @@ -28,8 +29,9 @@ CC0 素材,其余仍走原程序绘制江南图集作为 fallback。 - SHA-256:`a06f75f312c27eff15a2288475612e6f6699411be7259d408323cd15a790decc`(191128 bytes) - 获取日期:2026-09-21 - 用法:取 `Tilemap/tilemap_packed.png`(192x176,12x11 格,**无间距、16px pitch**;带间距的 - `tilemap.png` 为 203x186/17px)。本单取 (9,8)/(10,8) 两档草 + (4,8)/(4,9) 石板, - 整数 2x 生成 `Assets/cc0-tiles.png`。 + `tilemap.png` 为 203x186/17px)。取 (9,8)/(10,8) 两档草 + (4,8)/(4,9) 石板,整数 2x 生成 + `Assets/cc0-tiles.png` 的 0-3 格。**Tiny Farm 是农场包,无水面 / 屋顶 / 墙体 / 栏杆素材** + (已逐格核查:无蓝色水面格),故这些构件自绘,见第 3 节。 ## 2. Puny Characters(角色) @@ -44,10 +46,31 @@ CC0 素材,其余仍走原程序绘制江南图集作为 fallback。 32x48 格。包只提供南向姿态,北向复用南向、东/西为镜像(限制沿用 `scripts/make-cc0-art.py` 注释)。生成 `Assets/cc0-characters.png`。 +## 3. 自绘补绘构件(P34,原创,无第三方来源) + +Kenney Tiny Farm 无合适的瓦屋顶 / 白墙 / 木格窗 / 栏杆 / 水波 / 门素材,按任务要求用仓库 +自绘 32px 像素图补入 `cc0-tiles.png` 的 4-11 格(**原创,无外部来源,无需第三方许可**), +统一江南调色板:青灰瓦、白灰墙、暖木构件、碧水。逐格: + +| 索引 | 内容 | 说明 | +| --- | --- | --- | +| 4 | 水面 A | 自绘 2 相位水波之一 | +| 5 | 水面 B | 相位相对 A 位移 4px(两相位差 256px) | +| 6 | 屋顶正脊 | 青灰瓦脊 + 亮脊线 | +| 7 | 屋顶檐口 | 青灰瓦 + 檐口高光/滴水 | +| 8 | 墙体(木构) | 白灰墙 + 上下暖木梁 | +| 9 | 木格窗 | 白灰墙 + 中心木格窗 | +| 10 | 栏杆 | 木栏杆(用于桥面) | +| 11 | 门 | 暖木双扇门 | + +- 自绘代码位于 `scripts/make-cc0-art.py`(`draw_water`/`draw_roof_*`/`draw_wall`/`draw_railing`/`draw_door`), + 确定性、可复现,无外部素材。 +- 水网/水田(生成器 `code=4`)与样例河道共用同一自绘 2 相位水面。 + ## 说明 -- 两者原始 zip 均随仓库记录于 `third_party/`(`PROVENANCE.md` 为总来源记录),仓库分发的 - 只是上述再生成图集,不声称拥有未包含素材。 +- Kenney/Puny 原始 zip 均随仓库记录于 `third_party/`(`PROVENANCE.md` 为总来源记录),仓库 + 分发的只是上述再生成图集,不声称拥有未包含素材;自绘构件为原创。 - 版权/许可复核:见 `third_party/` 内原始文件与发布页;重跑 `make-cc0-art.py` 前后对 - `third_party/*.zip` 重新 `sha256sum` 校验(本单校验一致)。 -- 「中式元素补绘」为后续切片;本单只接入基础地块与一类角色,保证管线跑通。 + `third_party/*.zip` 重新 `sha256sum` 校验。 +- 范围仍为切片:动画全套 / 全村 / 更多中式元素补绘留待后续。 diff --git "a/docs/\347\273\264\346\212\244\350\257\264\346\230\216.md" "b/docs/\347\273\264\346\212\244\350\257\264\346\230\216.md" index 96dcecd..c095914 100644 --- "a/docs/\347\273\264\346\212\244\350\257\264\346\230\216.md" +++ "b/docs/\347\273\264\346\212\244\350\257\264\346\230\216.md" @@ -1073,3 +1073,39 @@ late pass),故「灯笼光晕 + 水面」以生成器夜帧为准,「炊 ``` - 范围:本单只接入基础地块与一类角色;动画全套 / 全村 / 中式元素补绘留待后续切片。 + +## P34 素材扩展切片(水面 / 屋顶 / 墙体 / 栏杆) + +在 P33 竖直切片之上,把样例村庄更多表面/构件从程序几何切到真像素素材,延续 `LV_ASSET_PACK=cc0` +开关与程序绘制 fallback。来源/许可/自绘清单见 `docs/素材来源.md`。 + +- 图集扩展:`Assets/cc0-tiles.png` 由 128x32(4 格)扩到 **384x32(12 格)**。0-3 格仍为 + Kenney Tiny Farm 地面(整数 2x);4-11 格为**自绘**构件(Kenney 农场包无水面/屋顶/墙/栏杆): + `4 water A / 5 water B / 6 roof ridge / 7 roof eave / 8 wall wood / 9 wall window / 10 railing / 11 door`。 +- 构建:`python3 scripts/make-cc0-art.py`(确定性,二次运行 byte-identical)。 +- 水面:自绘 **2 相位**,`cc0WaterTileIndexAt tick x y = (tick/32 + x + 2y) mod 2`,单帧内沿河 + 同时出现两相位;河道与水田/水网(生成器 `code=4`)共用。江南 fallback 仍是 3 相位。 +- 屋顶 / 墙体 / 门:样例民居在 cc0 模式下用 6/7/8/9/11 格;桥面用 10 格栏杆;其余道具仍走 + 江南图集。 +- 断言:`src/LivingVillage.Desktop.Tests/AssetPackTests.fs` 更新图集尺寸(12 格)与整数缩放, + 并新增「构件索引都在图集内 + 单帧覆盖两水相位」用例。 +- 证据(`docs/evidence/`,`git add -f`;`check-no-white-blocks.py` 5 帧 0 命中): + - `p34-realassets-day.png` / `p34-realassets-night.png`:样例村庄真实素材昼 / 夜整帧 1280x720。 + - `p34-realassets-closeup-2x.png`:民居屋顶/白墙/桥栏杆/水面 2x NEAREST(青灰瓦 (52,58,72)、 + 白墙 (234,228,212)、木栏杆 (112,74,46)、水面 (54,110,170) 均在场)。 + - `p34-water-2x.png`:河面 2x 特写(同帧可见两相位波纹)。 + - `p34-baseline-day.png`:同场景程序绘制 fallback 对照。 +- 复现: + + ```bash + DLL=src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll + for h in 12.0 23.5; do + tag=day; [ "$h" = 23.5 ] && tag=night + timeout 20 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \ + LV_AUTOPLAY_RECORD=1 LV_AUTOPLAY_START_HOUR=$h LV_RECORD_EVERY=5 \ + LV_RECORD_NAME=p34-$tag- LV_ASSET_PACK=cc0 \ + LV_RECORD_DIR=/tmp/opencode/lv-p34-evidence/$tag dotnet "$DLL" + done + ``` + +- 范围:仍为切片,不做全村;动画全套与更多中式元素补绘留待后续。 diff --git a/scripts/make-cc0-art.py b/scripts/make-cc0-art.py index b26cba1..44bd013 100644 --- a/scripts/make-cc0-art.py +++ b/scripts/make-cc0-art.py @@ -1,12 +1,18 @@ #!/usr/bin/env python3 """CC0 asset pack integration (third_party -> game atlases). -Reads the two provenanced CC0 packs in third_party/ (Kenney Tiny Farm tiles, -Puny Characters) and emits Deterministic CC0 Atlases for the prototype: +Reads the provenanced CC0 packs in third_party/ (Kenney Tiny Farm tiles, Puny +Characters) and emits deterministic atlases for the prototype: - src/LivingVillage.Desktop/Assets/cc0-tiles.png 128x32 (4 ground tiles, 32px each) + src/LivingVillage.Desktop/Assets/cc0-tiles.png 384x32 (12 tiles, 32px each) src/LivingVillage.Desktop/Assets/cc0-characters.png 3840x48 (5 variants x 4 directions x 6 frames, 32x48 cells) +Ground tiles (indices 0-3) come from Kenney Tiny Farm via integer 2x NEAREST of +the 16px source. Kenney Tiny Farm ships no water / roof / wall / railing / door +art (it is a farm pack), so indices 4-11 are *self-drawn* 32px pixel art in a +consistent Jiangnan palette (white plaster, dark slate tile, warm wood). Each +tile's provenance is recorded in docs/素材来源.md. + Mapping strategy (16 -> 32): integer 2x nearest-neighbour upscale of every source sprite; ground tiles fill the 32x32 cell exactly; Puny sprites keep their 16x24-ish content band (2x -> 32x48) anchored to the cell bottom. @@ -40,6 +46,19 @@ class Canvas: def get(self, x, y): return self.pixels[y][x] + def fill(self, x, y, w, h, color): + for yy in range(y, y + h): + for xx in range(x, x + w): + self.set(xx, yy, color) + + def vline(self, x, y, h, color): + for yy in range(y, y + h): + self.set(x, yy, color) + + def hline(self, x, y, w, color): + for xx in range(x, x + w): + self.set(xx, y, color) + def blit(self, img, ox, oy, scale=2): px = img.load() for y in range(img.height): @@ -95,16 +114,102 @@ def verify_png(path, expected_w, expected_h): print(f"ok {path.name} {width}x{height} bytes={len(data)}") -def build_cc0_tile_atlas(): - """Four 32x32 ground tiles from Kenney Tiny Farm, upscaled 2x. +# --- self-drawn Jiangnan structure palette (indices 4-11) --------------------- +WATER_DEEP = (36, 84, 138, 255) +WATER_MID = (54, 110, 170, 255) +WATER_HI = (104, 158, 210, 255) +SLATE = (52, 58, 72, 255) +SLATE_HI = (96, 104, 122, 255) +SLATE_DARK = (33, 37, 47, 255) +PLASTER = (234, 228, 212, 255) +PLASTER_SH = (204, 197, 181, 255) +WOOD = (112, 74, 46, 255) +WOOD_DARK = (74, 47, 28, 255) +LATTICE = (52, 34, 22, 255) +CLEAR = (0, 0, 0, 0) + + +def draw_water(c, ox, phase): + """One 32x32 water tile; `phase` in {0,1} shifts the wave crests by 4px.""" + c.fill(ox, 0, 32, 32, WATER_MID) + shift = phase * 4 + for yy in (6, 10, 20, 26): + for x in range(32): + if ((x + shift) // 4) % 2 == 0: + c.set(ox + x, yy, WATER_HI) + for yy in (4, 8, 18, 24): + for x in range(32): + if ((x + shift + 2) // 4) % 2 == 0: + c.set(ox + x, yy, WATER_DEEP) + + +def draw_roof_ridge(c, ox): + c.fill(ox, 0, 32, 32, SLATE) + c.fill(ox, 6, 32, 4, SLATE_HI) + for x in range(0, 32, 4): + c.vline(ox + x, 0, 6, SLATE_DARK) + c.vline(ox + x + 2, 10, 22, SLATE_DARK) + c.fill(ox, 0, 32, 2, SLATE_DARK) + + +def draw_roof_eave(c, ox): + c.fill(ox, 0, 32, 32, SLATE) + for x in range(0, 32, 4): + c.vline(ox + x, 0, 22, SLATE_DARK) + c.fill(ox, 22, 32, 3, SLATE_HI) + c.fill(ox, 25, 32, 3, SLATE_DARK) + for x in range(0, 32, 4): + c.set(ox + x + 3, 28, SLATE_HI) + + +def draw_wall(c, ox, window): + c.fill(ox, 0, 32, 32, PLASTER) + c.fill(ox, 0, 32, 4, WOOD) + c.fill(ox, 28, 32, 4, WOOD) + c.vline(ox, 0, 32, WOOD) + c.vline(ox + 31, 0, 32, WOOD) + if window: + c.fill(ox + 6, 6, 20, 20, WOOD_DARK) + c.fill(ox + 8, 8, 16, 16, LATTICE) + for i in range(8, 24, 4): + c.vline(ox + i, 8, 16, WOOD_DARK) + c.hline(ox + 8, i, 16, WOOD_DARK) + else: + c.fill(ox + 6, 12, 20, 2, PLASTER_SH) + c.fill(ox + 6, 18, 20, 2, PLASTER_SH) + + +def draw_railing(c, ox): + c.fill(ox, 0, 32, 32, CLEAR) + c.fill(ox, 6, 32, 3, WOOD) + c.fill(ox, 16, 32, 3, WOOD) + c.fill(ox, 2, 32, 2, WOOD_DARK) + for px in (2, 15, 28): + c.fill(ox + px, 2, 3, 26, WOOD) + c.set(ox + px + 1, 28, WOOD_DARK) + + +def draw_door(c, ox): + c.fill(ox, 0, 32, 32, CLEAR) + c.fill(ox + 4, 2, 24, 30, WOOD_DARK) + c.fill(ox + 7, 5, 18, 27, WOOD) + c.fill(ox + 14, 5, 4, 27, WOOD_DARK) + c.fill(ox + 20, 17, 3, 3, (222, 190, 120, 255)) + - Source is the *packed* sheet (tilemap_packed.png) which has NO spacing between - tiles: 192x176 = 12x11 tiles at a 16px pitch (the spaced tilemap.png is 203x186 - at 17px pitch; mixing the two misaligns every non-zero cell). Tile selection is - fixed by grid coordinates: two full grass shades (9,8)/(10,8) for the checker - ground, then two grey stone slabs (4,8)/(4,9) for the paved path. +def build_cc0_tile_atlas(): + """Twelve 32x32 tiles: 4 Kenney ground + 8 self-drawn structure tiles. + + Ground source is the *packed* sheet (tilemap_packed.png) which has NO spacing + between tiles: 192x176 = 12x11 tiles at a 16px pitch (the spaced tilemap.png + is 203x186 at 17px pitch; mixing the two misaligns every non-zero cell). Grid + coordinates: two full grass shades (9,8)/(10,8) then two grey stone slabs + (4,8)/(4,9). Indices 4-11 are self-drawn (Kenney Tiny Farm has no water, roof, + wall, railing or door tiles), layout fixed by the P34 atlas contract: + 4 water A / 5 water B / 6 roof ridge / 7 roof eave + 8 wall wood / 9 wall window / 10 railing / 11 door """ - c = Canvas(128, 32) + c = Canvas(384, 32) packed = Image.open(THIRD / "kenney_tiny-farm" / "Tilemap" / "tilemap_packed.png").convert("RGBA") def tile(grid_x, grid_y): @@ -112,8 +217,16 @@ def build_cc0_tile_atlas(): choices = [(9, 8), (10, 8), (4, 8), (4, 9)] for index, (gx, gy) in enumerate(choices): - t = tile(gx, gy) - c.blit(t, index * 32, 0, scale=2) + c.blit(tile(gx, gy), index * 32, 0, scale=2) + + draw_water(c, 4 * 32, 0) + draw_water(c, 5 * 32, 1) + draw_roof_ridge(c, 6 * 32) + draw_roof_eave(c, 7 * 32) + draw_wall(c, 8 * 32, window=False) + draw_wall(c, 9 * 32, window=True) + draw_railing(c, 10 * 32) + draw_door(c, 11 * 32) return c @@ -168,7 +281,7 @@ def build_cc0_character_atlas(): def main(): ASSETS.mkdir(parents=True, exist_ok=True) targets = [ - ("cc0-tiles.png", build_cc0_tile_atlas(), 128, 32), + ("cc0-tiles.png", build_cc0_tile_atlas(), 384, 32), ("cc0-characters.png", build_cc0_character_atlas(), 3840, 48), ] for name, canvas, w, h in targets: diff --git a/src/LivingVillage.Desktop.Tests/AssetPackTests.fs b/src/LivingVillage.Desktop.Tests/AssetPackTests.fs index fc235d0..46513f9 100644 --- a/src/LivingVillage.Desktop.Tests/AssetPackTests.fs +++ b/src/LivingVillage.Desktop.Tests/AssetPackTests.fs @@ -36,10 +36,40 @@ type AssetPackTests () = Assert.AreEqual(cc0TileAtlasHeight, height) Assert.AreEqual(0, width % Sim.tilePixels) Assert.AreEqual(0, height % Sim.tilePixels) - Assert.AreEqual(4, width / Sim.tilePixels) + Assert.AreEqual(cc0TileCount, width / Sim.tilePixels) + Assert.AreEqual(cc0TileAtlasWidth, width) Assert.AreEqual(2, Sim.tilePixels / cc0TileSourcePixels) Assert.AreEqual(0, Sim.tilePixels % cc0TileSourcePixels) + [] + member _.Cc0StructureTileIndicesFitTheAtlasAndWaterHasTwoPhases () = + // Every P34 self-drawn structure cell must sit inside the 12-tile atlas band. + let indices = + [ cc0TileIndexWaterA + cc0TileIndexWaterB + cc0TileIndexRoofRidge + cc0TileIndexRoofEave + cc0TileIndexWallWood + cc0TileIndexWallWindow + cc0TileIndexRailing + cc0TileIndexDoor ] + for index in indices do + Assert.IsTrue(index >= 0 && index < cc0TileCount, sprintf "cc0 tile index %d out of atlas" index) + let width, _ = pngDimensions (assetPath cc0TileAtlasRelativePath) + Assert.IsTrue(width >= (List.max indices + 1) * Sim.tilePixels, "atlas must cover every structure index") + // Two-phase water: phase depends only on tick/tile, always in {A, B}, and both + // phases appear across a row of tiles in one frame. + Assert.AreEqual(2, cc0WaterFrameCount) + let phases = + [ for tileX in 0 .. 15 -> cc0WaterTileIndexAt 0L tileX 0 ] + |> Set.ofList + Assert.IsTrue((phases = Set.ofList [ cc0TileIndexWaterA; cc0TileIndexWaterB ]), + "one frame must show both water phases across the river") + for tileX in 0 .. 15 do + for tileY in 0 .. 15 do + let index = cc0WaterTileIndexAt 12345L tileX tileY + Assert.IsTrue(index = cc0TileIndexWaterA || index = cc0TileIndexWaterB) + [] member _.Cc0CharacterAtlasIsIntegerMultipleOfTheCellGrid () = let width, height = pngDimensions (assetPath cc0CharacterAtlasRelativePath) diff --git a/src/LivingVillage.Desktop/Assets/cc0-tiles.png b/src/LivingVillage.Desktop/Assets/cc0-tiles.png index 204eba2..afa46a4 100644 Binary files a/src/LivingVillage.Desktop/Assets/cc0-tiles.png and b/src/LivingVillage.Desktop/Assets/cc0-tiles.png differ diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs index 9a034aa..80eed07 100644 --- a/src/LivingVillage.Desktop/VillageArt.fs +++ b/src/LivingVillage.Desktop/VillageArt.fs @@ -553,7 +553,20 @@ module VillageArt = let cc0TileAtlasRelativePath = "Assets/cc0-tiles.png" // Source ground tiles are 16x16, upscaled exactly 2x to one Sim.tilePixels cell. let cc0TileSourcePixels = 16 - let cc0TileAtlasWidth = Sim.tilePixels * 4 + // P34 structure slice: indices into cc0-tiles.png. Kenney Tiny Farm ships no + // water / roof / wall / railing / door art, so indices 4-11 are self-drawn + // (provenance in docs/素材来源.md), laid out by this fixed contract. + let cc0TileCount = 12 + let cc0TileIndexWaterA = 4 + let cc0TileIndexWaterB = 5 + let cc0TileIndexRoofRidge = 6 + let cc0TileIndexRoofEave = 7 + let cc0TileIndexWallWood = 8 + let cc0TileIndexWallWindow = 9 + let cc0TileIndexRailing = 10 + let cc0TileIndexDoor = 11 + let cc0WaterFrameCount = 2 + let cc0TileAtlasWidth = Sim.tilePixels * cc0TileCount let cc0TileAtlasHeight = Sim.tilePixels let cc0CharacterAtlasRelativePath = "Assets/cc0-characters.png" // Source Puny band is 16x24, upscaled exactly 2x to a 32x48 cell; atlas packs @@ -568,6 +581,16 @@ module VillageArt = let cc0CharacterAtlasWidth = cc0CharacterCellWidth * cc0CharacterVariantCount * cc0CharacterDirectionCount * cc0CharacterFrameCount + /// Deterministic 2-phase water per tile: `(tick/32 + x + 2y) mod 2`, so one frame + /// shows both ripple phases across the river. Mirrors the 3-phase Jiangnan + /// `waterFrameTickAt`, but only two self-drawn CC0 frames exist. + let cc0WaterTileIndexAt (tick: int64) (tileX: int) (tileY: int) : int = + let phase = int ((tick / 32L + int64 (tileX + 2 * tileY)) % 2L) + if phase = 0 then cc0TileIndexWaterA else cc0TileIndexWaterB + + let cc0TileRect (index: int) : XnaRectangle = + XnaRectangle(index * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels) + /// Ground atlas selection: `Some cc0-tiles.png` when the pack is on, else `None` /// so the renderer keeps its procedural Jiangnan ground as the fallback. let selectedGroundAtlasRelativePath () : string option = @@ -792,6 +815,20 @@ module VillageArt = let draw position sprite = drawWorldTile spriteBatch textures camera viewportWidth viewportHeight position sprite tint + // P34: with the CC0 pack on, individual sample structures draw from the + // real/self-drawn cc0-tiles atlas; otherwise every draw falls back to jiangnan. + let drawCc0At (position: TilePosition) (index: int) = + match textures.Cc0TileAtlas with + | Some cc0 -> + drawCc0Tile + spriteBatch + cc0 + tint + (cc0TileRect index) + (position.X * Sim.tilePixels - int camera.X) + (position.Y * Sim.tilePixels - int camera.Y) + | None -> () + // Each water tile picks its phase from its own world coordinates, so a single // frame shows the three ripple phases across the river instead of one global frame. let waterSpriteAt (position: TilePosition) = @@ -800,15 +837,41 @@ module VillageArt = | 1 -> WaterSpriteB | _ -> WaterSpriteC + // P34 water: the self-drawn CC0 atlas only has two frames, so it picks the + // 2-phase index; the jiangnan atlas keeps its 3-phase ripple. + let drawWater (position: TilePosition) = + match textures.Cc0TileAtlas with + | Some _ -> drawCc0At position (cc0WaterTileIndexAt tick position.X position.Y) + | None -> draw position (waterSpriteAt position) + + // P34 structures: real/self-drawn roof, wall, window and door cells replace + // the jiangnan house tiles only when the CC0 pack is on. + let houseSpriteAt (position: TilePosition) (sprite: TileSprite) = + let cc0Index = + match textures.Cc0TileAtlas with + | Some _ -> + match sprite with + | RoofRidgeSprite -> Some cc0TileIndexRoofRidge + | RoofEaveSprite -> Some cc0TileIndexRoofEave + | WallWoodSprite -> Some cc0TileIndexWallWood + | WallWindowSprite -> Some cc0TileIndexWallWindow + | DoorSprite -> Some cc0TileIndexDoor + | _ -> None + | None -> None + match cc0Index with + | Some index -> drawCc0At position index + | None -> draw position sprite + // Procedural terrain for the 512x384 large world: water/pebble/peat codes map // onto the jiangnan atlas sprites with deterministic ripple continuity. // 生成器世界:1 水 / 2 石板路 / 3 泥炭 / 0 草地。泥炭按草地渲染,路走石板贴图。 let drawTerrainTile (position: TilePosition) (code: int) = match code with - | 1 -> draw position (waterSpriteAt position) + | 1 -> drawWater position | 2 -> draw position StonePathSprite - // P30 乡村空区:水田(4)走静态水面素材、菜畦(5)走菜地素材;均为成熟 tile。 - | 4 -> draw position WaterSprite + // P30 乡村空区:水田(4)走水波动画、菜畦(5)走菜地素材;均为成熟 tile。 + // P34:水田/水网在 CC0 包下同样走自绘 2 相位水面。 + | 4 -> drawWater position | 5 -> draw position GardenSprite | _ -> draw position GrassSprite @@ -851,7 +914,7 @@ module VillageArt = for localPosition, kind in planGrounds do let position = worldTile plan localPosition match kind with - | Water -> draw position (waterSpriteAt position) + | Water -> drawWater position | StonePath -> match textures.Cc0TileAtlas with | Some cc0 -> drawCc0Tile spriteBatch cc0 tint cc0PathRect (position.X * Sim.tilePixels - int camera.X) (position.Y * Sim.tilePixels - int camera.Y) @@ -860,18 +923,22 @@ module VillageArt = let drawHouse doorTile = let position = worldTile plan doorTile for dx in -2 .. 1 do - draw (offsetTile position dx -2) RoofRidgeSprite - draw (offsetTile position dx -1) RoofEaveSprite - draw (offsetTile position -2 0) WallWoodSprite - draw (offsetTile position -1 0) WallWindowSprite - draw (offsetTile position 0 0) DoorSprite - draw (offsetTile position 1 0) WallWoodSprite + houseSpriteAt (offsetTile position dx -2) RoofRidgeSprite + houseSpriteAt (offsetTile position dx -1) RoofEaveSprite + houseSpriteAt (offsetTile position -2 0) WallWoodSprite + houseSpriteAt (offsetTile position -1 0) WallWindowSprite + houseSpriteAt (offsetTile position 0 0) DoorSprite + houseSpriteAt (offsetTile position 1 0) WallWoodSprite let drawBridge localPosition = let position = worldTile plan localPosition - draw position BridgeSprite - draw (offsetTile position 0 1) BridgeSprite - draw (offsetTile position 0 2) BridgeSprite + let bridgeTile p = + match textures.Cc0TileAtlas with + | Some _ -> drawCc0At p cc0TileIndexRailing + | None -> draw p BridgeSprite + bridgeTile position + bridgeTile (offsetTile position 0 1) + bridgeTile (offsetTile position 0 2) let drawBamboo localPosition = let position = worldTile plan localPosition -- cgit v1.2.3