summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/assets-and-licenses.md3
-rwxr-xr-xscripts/m6c-package.sh4
-rw-r--r--scripts/make-jiangnan-art.py460
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs13
-rw-r--r--src/LivingVillage.Desktop/Assets/jiangnan-characters.pngbin0 -> 1892 bytes
-rw-r--r--src/LivingVillage.Desktop/Assets/jiangnan-interior.pngbin0 -> 1613 bytes
-rw-r--r--src/LivingVillage.Desktop/Assets/jiangnan-world.pngbin0 -> 1098 bytes
-rw-r--r--src/LivingVillage.Desktop/Game.fs9
-rw-r--r--src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj3
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs305
10 files changed, 628 insertions, 169 deletions
diff --git a/docs/assets-and-licenses.md b/docs/assets-and-licenses.md
index f2dc1bc..10bd69e 100644
--- a/docs/assets-and-licenses.md
+++ b/docs/assets-and-licenses.md
@@ -2,7 +2,8 @@
## 仓库和交付包中的实际资产
-- **原创代码生成图形与 procedural data**:`src/LivingVillage.Desktop/Game.fs` 中的 `PixelText`、色块和 `Atlas` 绘制逻辑,以及 `VillageArt.fs` 和 `ChineseText.fs` 中的江南场景数据、NPC 视觉元数据和中文排版策略,均由仓库代码在运行时或纯函数中生成,不主张外部素材来源。
+- **原创代码生成图形与 procedural data**:`src/LivingVillage.Desktop/Game.fs` 中的 `PixelText`、色块和 `Atlas` 绘制逻辑,以及 `VillageArt.fs` 和 `ChineseText.fs` 中的图集采样坐标、NPC 视觉元数据和中文排版策略,均由仓库代码在运行时或纯函数中生成,不主张外部素材来源。
+- **江南美术原创图集**:`src/LivingVillage.Desktop/Assets/jiangnan-world.png`(256x32)、`jiangnan-characters.png`(1024x48)与 `jiangnan-interior.png`(392x96)是仓库实际分发的透明 PNG 图集,全部由 `scripts/make-jiangnan-art.py` 以原创调色板和逐像素几何生成,不使用任何外部素材;三者随 Linux/macOS 发布包复制到 `Assets/` 目录。`VillageArt.fs` 运行时只加载并采样这些图集,不再在进程内烘焙像素。
- **Noto Sans CJK 派生字形图集**:`src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png` 是仓库实际分发的 `384x288` 透明 PNG,使用 `src/LivingVillage.Desktop/Assets/cjk-glyphs.txt` 中的 188 个字符、16 列 24px 单元生成;两者会复制到 Linux/macOS 发布包的 `Assets/` 目录。原始字体文件不随仓库或包分发。
- 来源:`/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc`,字体族包含 Noto Sans CJK SC。
- 许可:SIL Open Font License 1.1;本地版权文件 SHA-256:`849f4ea9c214fa4ac3593b770c699f387534b11ce671264c1b10d85bdcb5997b`。
diff --git a/scripts/m6c-package.sh b/scripts/m6c-package.sh
index 89ae6a4..b620427 100755
--- a/scripts/m6c-package.sh
+++ b/scripts/m6c-package.sh
@@ -46,9 +46,9 @@ else
fi
fi
-for asset in Assets/cjk-glyph-atlas.png Assets/cjk-glyphs.txt; do
+for asset in Assets/cjk-glyph-atlas.png Assets/cjk-glyphs.txt Assets/jiangnan-world.png Assets/jiangnan-characters.png Assets/jiangnan-interior.png; do
if [[ ! -s "$APP_DIR/$asset" ]]; then
- printf 'Required CJK asset is missing from the Linux package: %s\n' "$APP_DIR/$asset" >&2
+ printf 'Required asset is missing from the Linux package: %s\n' "$APP_DIR/$asset" >&2
exit 1
fi
done
diff --git a/scripts/make-jiangnan-art.py b/scripts/make-jiangnan-art.py
new file mode 100644
index 0000000..478e732
--- /dev/null
+++ b/scripts/make-jiangnan-art.py
@@ -0,0 +1,460 @@
+#!/usr/bin/env python3
+"""Deterministic pixel-art atlases for LivingVillage.Desktop.
+
+Generates three RGBA PNGs into src/LivingVillage.Desktop/Assets:
+ jiangnan-world.png 256x32 (8 ground/prop tiles, 32px each)
+ jiangnan-characters.png 1024x48 (4 variants x 4 directions x 2 frames, 32x48 cells)
+ jiangnan-interior.png 392x96 (variable-size bottom-aligned interior cells)
+
+Cell geometry is mirrored in VillageArt.fs; keep both in sync.
+"""
+
+import struct
+import zlib
+from pathlib import Path
+
+ASSET_DIR = Path(__file__).resolve().parents[1] / "src" / "LivingVillage.Desktop" / "Assets"
+
+OUTLINE = (23, 31, 34, 255)
+SKIN = (232, 180, 139, 255)
+HAIR = (42, 36, 37, 255)
+SHOE = (40, 30, 28, 255)
+EYE = (43, 32, 30, 255)
+
+
+class Canvas:
+ def __init__(self, width, height):
+ self.width = width
+ self.height = height
+ self.pixels = [[(0, 0, 0, 0)] * width for _ in range(height)]
+
+ def set(self, x, y, color):
+ if 0 <= x < self.width and 0 <= y < self.height:
+ self.pixels[y][x] = color
+
+ def get(self, x, y):
+ return self.pixels[y][x]
+
+ def rect(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 outline(self, x0, y0, w, h, color=OUTLINE):
+ opaque = {
+ (x, y)
+ for y in range(y0, y0 + h)
+ for x in range(x0, x0 + w)
+ if self.get(x, y)[3] == 255
+ }
+ for (x, y) in opaque:
+ for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):
+ nx, ny = x + dx, y + dy
+ if x0 <= nx < x0 + w and y0 <= ny < y0 + h and self.get(nx, ny)[3] == 0:
+ self.set(nx, ny, color)
+
+
+def write_png(path, canvas):
+ raw = bytearray()
+ for y in range(canvas.height):
+ raw.append(0)
+ for x in range(canvas.width):
+ raw.extend(canvas.pixels[y][x])
+
+ def chunk(tag, payload):
+ return (
+ struct.pack(">I", len(payload))
+ + tag
+ + payload
+ + struct.pack(">I", zlib.crc32(tag + payload) & 0xFFFFFFFF)
+ )
+
+ header = struct.pack(">IIBBBBB", canvas.width, canvas.height, 8, 6, 0, 0, 0)
+ data = (
+ b"\x89PNG\r\n\x1a\n"
+ + chunk(b"IHDR", header)
+ + chunk(b"IDAT", zlib.compress(bytes(raw), 9))
+ + chunk(b"IEND", b"")
+ )
+ path.write_bytes(data)
+
+
+def verify_png(path, expected_w, expected_h):
+ data = path.read_bytes()
+ assert data[:8] == b"\x89PNG\r\n\x1a\n", path
+ width, height = struct.unpack(">II", data[16:24])
+ assert (width, height) == (expected_w, expected_h), (path, width, height)
+ idat = b""
+ offset = 8
+ while offset < len(data):
+ (length,) = struct.unpack(">I", data[offset : offset + 4])
+ tag = data[offset + 4 : offset + 8]
+ if tag == b"IDAT":
+ idat += data[offset + 8 : offset + 8 + length]
+ offset += 12 + length
+ raw = zlib.decompress(idat)
+ stride = expected_w * 4 + 1
+ assert len(raw) == stride * expected_h, (path, len(raw))
+ print(f"ok {path.name} {width}x{height} bytes={len(data)}")
+
+
+def build_world_atlas():
+ c = Canvas(256, 32)
+ tile = 32
+
+ def slot(index):
+ return index * tile
+
+ grass = (66, 112, 70, 255)
+ grass_light = (92, 139, 82, 255)
+ grass_dark = (54, 92, 60, 255)
+ water = (54, 116, 145, 255)
+ water_light = (105, 174, 184, 255)
+ water_deep = (44, 93, 124, 255)
+ path = (170, 153, 123, 255)
+ path_light = (201, 183, 145, 255)
+ path_grout = (140, 124, 96, 255)
+ roof = (45, 49, 57, 255)
+ roof_light = (76, 80, 85, 255)
+ roof_dark = (30, 33, 38, 255)
+ wall = (225, 216, 195, 255)
+ wall_light = (238, 232, 214, 255)
+ wall_shadow = (166, 147, 119, 255)
+ wall_seam = (196, 180, 152, 255)
+ wood = (119, 76, 47, 255)
+ wood_light = (164, 111, 61, 255)
+ wood_dark = (83, 52, 39, 255)
+ bamboo = (51, 101, 61, 255)
+ bamboo_light = (107, 153, 81, 255)
+ bamboo_node = (38, 76, 46, 255)
+ soil = (122, 78, 49, 255)
+ soil_light = (146, 96, 60, 255)
+ sprout = (86, 139, 68, 255)
+ sprout_light = (107, 171, 85, 255)
+
+ x0 = slot(0)
+ c.rect(x0, 0, 32, 32, grass)
+ for y in range(32):
+ for x in range(32):
+ if (x * 13 + y * 7) % 29 == 0:
+ c.set(x0 + x, y, grass_light)
+ elif (x * 7 + y * 11) % 43 == 0:
+ c.set(x0 + x, y, grass_dark)
+ for bx, by in ((5, 7), (13, 19), (22, 11), (28, 25), (9, 27), (18, 3)):
+ c.set(x0 + bx, by, grass_light)
+ c.set(x0 + bx, by + 1, grass_light)
+
+ x0 = slot(1)
+ c.rect(x0, 0, 32, 32, water)
+ c.rect(x0 + 8, 2, 2, 26, water_deep)
+ c.rect(x0 + 23, 2, 2, 26, water_deep)
+ for y in (5, 16, 27):
+ for x in range(3, 29):
+ if (x + y) % 5 != 0:
+ c.set(x0 + x, y, water_light)
+ for sx, sy in ((12, 9), (26, 20), (6, 22)):
+ c.set(x0 + sx, sy, (214, 231, 238, 255))
+
+ x0 = slot(2)
+ c.rect(x0, 0, 32, 32, path)
+ c.rect(x0, 0, 32, 1, path_light)
+ c.rect(x0, 31, 32, 1, path_light)
+ for y in (10, 21):
+ c.rect(x0, y, 32, 1, path_grout)
+ for x in (11, 22):
+ c.rect(x0 + x, 0, 1, 10, path_grout)
+ for x in (5, 17, 28):
+ c.rect(x0 + x, 11, 1, 10, path_grout)
+ for x in (12, 24):
+ c.rect(x0 + x, 22, 1, 10, path_grout)
+ for y in range(1, 31):
+ for x in range(1, 31):
+ if (x * 5 + y * 3) % 23 == 0 and c.get(x0 + x, y) == path:
+ c.set(x0 + x, y, path_light)
+
+ x0 = slot(3)
+ c.rect(x0, 0, 32, 32, roof)
+ for band in range(4):
+ y = band * 8
+ c.rect(x0, y, 32, 1, roof_light)
+ for x in range((band % 2) * 4, 32, 8):
+ c.rect(x0 + x, y + 1, 1, 7, roof_dark)
+ c.rect(x0, 31, 32, 1, roof_dark)
+
+ x0 = slot(4)
+ c.rect(x0, 0, 32, 32, wall)
+ c.rect(x0, 0, 32, 1, wall_light)
+ c.rect(x0 + 15, 0, 2, 32, wall_seam)
+ c.rect(x0, 9, 32, 1, wall_seam)
+ c.rect(x0, 18, 32, 1, wall_seam)
+ c.rect(x0, 27, 32, 5, wall_shadow)
+ c.rect(x0, 31, 32, 1, (120, 102, 80, 255))
+
+ x0 = slot(5)
+ c.rect(x0, 0, 32, 32, wood)
+ for y in (0, 8, 16, 24):
+ c.rect(x0, y, 32, 1, wood_light)
+ for y in (7, 15, 23, 31):
+ c.rect(x0, y, 32, 1, wood_dark)
+ for y in (4, 12, 20, 28):
+ c.set(x0 + 4, y, (61, 45, 49, 255))
+ c.set(x0 + 27, y, (61, 45, 49, 255))
+
+ x0 = slot(6)
+ for stem in (5, 15, 25):
+ c.rect(x0 + stem, 2, 3, 30, bamboo)
+ c.set(x0 + stem, 2, bamboo_light)
+ c.set(x0 + stem, 3, bamboo_light)
+ c.set(x0 + stem, 4, bamboo_light)
+ for y in (8, 16, 24):
+ c.rect(x0 + stem, y, 3, 1, bamboo_node)
+ for i, ly in enumerate((6, 14, 22)):
+ if i % 2 == 0:
+ c.rect(x0 + stem - 4, ly, 5, 2, bamboo_light)
+ c.rect(x0 + stem + 3, ly + 3, 4, 2, bamboo)
+ else:
+ c.rect(x0 + stem + 3, ly, 5, 2, bamboo_light)
+ c.rect(x0 + stem - 4, ly + 3, 4, 2, bamboo)
+ c.outline(x0, 0, 32, 32)
+
+ x0 = slot(7)
+ for row_y in (5, 14, 23):
+ c.rect(x0 + 2, row_y, 28, 4, soil)
+ c.rect(x0 + 2, row_y, 28, 1, soil_light)
+ for sx in (6, 15, 24):
+ c.rect(x0 + sx, row_y - 3, 2, 3, sprout)
+ c.set(x0 + sx, row_y - 3, sprout_light)
+ c.set(x0 + sx + 1, row_y - 3, sprout_light)
+ c.outline(x0, 0, 32, 32)
+
+ return c
+
+
+VARIANTS = [
+ {"robe": (46, 57, 108, 255), "light": (104, 145, 183, 255), "dark": (32, 41, 80, 255), "hat": False},
+ {"robe": (141, 91, 45, 255), "light": (205, 163, 76, 255), "dark": (104, 66, 32, 255), "hat": False},
+ {"robe": (46, 113, 91, 255), "light": (121, 185, 139, 255), "dark": (32, 84, 68, 255), "hat": False},
+ {"robe": (122, 96, 58, 255), "light": (176, 141, 84, 255), "dark": (92, 71, 42, 255), "hat": True},
+]
+
+DIRECTIONS = ["north", "south", "west", "east"]
+
+
+def draw_character_cell(c, ox, variant, direction, frame):
+ robe = variant["robe"]
+ light = variant["light"]
+ dark = variant["dark"]
+ hat = (196, 158, 74, 255)
+ hat_light = (228, 197, 112, 255)
+ hat_dark = (168, 132, 58, 255)
+
+ c.rect(ox + 8, 44, 16, 1, (23, 31, 34, 110))
+ c.rect(ox + 10, 45, 12, 1, (23, 31, 34, 90))
+
+ if frame == 0:
+ c.rect(ox + 10, 36, 4, 7, dark)
+ c.rect(ox + 9, 42, 6, 3, SHOE)
+ c.rect(ox + 18, 36, 4, 5, dark)
+ c.rect(ox + 18, 39, 6, 3, SHOE)
+ else:
+ c.rect(ox + 18, 36, 4, 7, dark)
+ c.rect(ox + 18, 42, 6, 3, SHOE)
+ c.rect(ox + 10, 36, 4, 5, dark)
+ c.rect(ox + 10, 39, 6, 3, SHOE)
+
+ c.rect(ox + 7, 22, 18, 14, robe)
+ c.rect(ox + 23, 22, 2, 14, dark)
+ c.rect(ox + 7, 31, 18, 3, light)
+ c.rect(ox + 13, 22, 6, 2, light)
+ c.rect(ox + 15, 24, 2, 2, light)
+
+ c.rect(ox + 5, 24, 2, 9, robe)
+ c.rect(ox + 25, 24, 2, 9, robe)
+ if direction != "north":
+ c.rect(ox + 5, 32, 2, 2, light)
+ c.rect(ox + 25, 32, 2, 2, light)
+ c.rect(ox + 5, 34, 2, 3, SKIN)
+ c.rect(ox + 25, 34, 2, 3, SKIN)
+
+ c.rect(ox + 10, 12, 12, 10, SKIN)
+ c.rect(ox + 9, 8, 14, 4, HAIR)
+ c.rect(ox + 9, 12, 2, 3, HAIR)
+ c.rect(ox + 21, 12, 2, 3, HAIR)
+
+ if direction == "north":
+ c.rect(ox + 10, 12, 12, 5, HAIR)
+ elif direction == "south":
+ c.rect(ox + 12, 15, 2, 2, EYE)
+ c.rect(ox + 18, 15, 2, 2, EYE)
+ c.rect(ox + 15, 19, 2, 1, (196, 120, 104, 255))
+ elif direction == "west":
+ c.rect(ox + 18, 12, 4, 6, HAIR)
+ c.rect(ox + 11, 15, 2, 2, EYE)
+ elif direction == "east":
+ c.rect(ox + 10, 12, 4, 6, HAIR)
+ c.rect(ox + 19, 15, 2, 2, EYE)
+
+ if variant["hat"]:
+ c.rect(ox + 11, 3, 10, 7, hat)
+ c.rect(ox + 12, 4, 8, 1, hat_light)
+ c.rect(ox + 6, 9, 20, 2, hat)
+ c.rect(ox + 7, 11, 18, 1, hat_dark)
+
+ c.outline(ox, 0, 32, 48)
+
+
+def build_character_atlas():
+ c = Canvas(1024, 48)
+ cell = 0
+ for variant in VARIANTS:
+ for direction in DIRECTIONS:
+ for frame in (0, 1):
+ draw_character_cell(c, cell * 32, variant, direction, frame)
+ cell += 1
+ return c
+
+
+def build_interior_atlas():
+ c = Canvas(392, 96)
+
+ floor = (172, 138, 96, 255)
+ floor_line = (146, 113, 74, 255)
+ floor_light = (188, 155, 110, 255)
+ wall = (225, 216, 195, 255)
+ wall_light = (238, 232, 214, 255)
+ wall_seam = (196, 180, 152, 255)
+ wood = (119, 76, 47, 255)
+ wood_light = (164, 111, 61, 255)
+ wood_dark = (83, 52, 39, 255)
+ frame_wood = (94, 66, 40, 255)
+ ink = (61, 45, 49, 255)
+ brass = (196, 158, 74, 255)
+ sky = (86, 143, 170, 255)
+ cloud = (214, 231, 238, 255)
+
+ x0, y0 = 0, 64
+ c.rect(x0, y0, 32, 32, floor)
+ for y in (7, 15, 23, 31):
+ c.rect(x0, y0 + y, 32, 1, floor_line)
+ c.rect(x0 + 20, y0, 1, 7, floor_line)
+ for x in (6, 26):
+ c.rect(x0 + x, y0 + 8, 1, 7, floor_line)
+ c.rect(x0 + 13, y0 + 16, 1, 7, floor_line)
+ for x in (3, 22):
+ c.rect(x0 + x, y0 + 24, 1, 7, floor_line)
+ for y in range(32):
+ for x in range(32):
+ if (x * 11 + y * 5) % 37 == 0:
+ c.set(x0 + x, y0 + y, floor_light)
+
+ x0, y0 = 32, 64
+ c.rect(x0, y0, 32, 32, wall)
+ c.rect(x0, y0, 32, 1, wall_light)
+ c.rect(x0 + 15, y0, 2, 32, wall_seam)
+ c.rect(x0, y0 + 28, 32, 4, floor_line)
+ c.rect(x0, y0 + 31, 32, 1, wood_dark)
+
+ x0, y0 = 64, 56
+ c.rect(x0, y0, 48, 40, frame_wood)
+ c.rect(x0 + 3, y0 + 3, 42, 28, sky)
+ c.rect(x0 + 6, y0 + 7, 10, 3, cloud)
+ c.rect(x0 + 20, y0 + 5, 12, 3, cloud)
+ c.rect(x0 + 14, y0 + 3, 3, 28, frame_wood)
+ c.rect(x0 + 30, y0 + 3, 3, 28, frame_wood)
+ c.rect(x0 + 3, y0 + 14, 42, 3, frame_wood)
+ c.rect(x0, y0 + 33, 48, 7, wood)
+ c.rect(x0, y0 + 38, 48, 2, wood_dark)
+ c.outline(x0, y0, 48, 40)
+
+ x0, y0 = 112, 48
+ c.rect(x0 + 6, y0 + 16, 8, 28, frame_wood)
+ c.rect(x0 + 50, y0 + 16, 8, 28, frame_wood)
+ c.rect(x0 + 12, y0 + 16, 2, 28, wood_dark)
+ c.rect(x0 + 56, y0 + 16, 2, 28, wood_dark)
+ c.rect(x0, y0 + 4, 64, 14, wood)
+ c.rect(x0, y0 + 4, 64, 3, wood_light)
+ c.rect(x0, y0 + 16, 64, 2, wood_dark)
+ c.rect(x0 + 12, y0 + 10, 6, 5, wall)
+ c.rect(x0 + 13, y0 + 11, 4, 2, sky)
+ c.rect(x0 + 40, y0 + 7, 10, 8, ink)
+ c.rect(x0 + 43, y0 + 5, 4, 2, ink)
+ c.rect(x0 + 50, y0 + 9, 3, 3, ink)
+ c.outline(x0, y0, 64, 48)
+
+ x0, y0 = 176, 0
+ c.rect(x0, y0, 48, 96, frame_wood)
+ c.rect(x0, y0, 5, 96, wood_dark)
+ c.rect(x0 + 43, y0, 5, 96, wood_dark)
+ c.rect(x0 + 5, y0 + 4, 38, 88, (224, 211, 185, 255))
+ c.rect(x0 + 9, y0 + 8, 30, 14, (238, 232, 214, 255))
+ c.rect(x0 + 9, y0 + 22, 30, 2, wall_seam)
+ quilt = (114, 62, 67, 255)
+ quilt_line = (94, 48, 52, 255)
+ c.rect(x0 + 5, y0 + 28, 38, 64, quilt)
+ c.rect(x0 + 5, y0 + 28, 38, 2, quilt_line)
+ for y in (38, 48, 58, 68, 78):
+ c.rect(x0 + 5, y0 + y, 38, 2, quilt_line)
+ c.rect(x0, y0 + 88, 48, 6, wood_dark)
+ c.rect(x0, y0 + 92, 48, 2, ink)
+ c.outline(x0, y0, 48, 96)
+
+ x0, y0 = 224, 0
+ c.rect(x0, y0, 32, 96, ink)
+ c.rect(x0 + 4, y0 + 4, 24, 88, (142, 102, 72, 255))
+ c.rect(x0 + 6, y0 + 6, 20, 84, (163, 124, 86, 255))
+ c.rect(x0 + 8, y0 + 60, 16, 4, frame_wood)
+ c.rect(x0 + 10, y0 + 56, 12, 4, frame_wood)
+ c.rect(x0 + 12, y0 + 52, 8, 4, frame_wood)
+ c.rect(x0 + 8, y0 + 68, 16, 2, (188, 163, 126, 255))
+ c.outline(x0, y0, 32, 96)
+
+ x0, y0 = 256, 32
+ rug = (150, 62, 58, 255)
+ rug_inner = (171, 84, 72, 255)
+ rug_dark = (134, 52, 50, 255)
+ c.rect(x0, y0, 96, 64, rug)
+ c.rect(x0, y0, 96, 5, brass)
+ c.rect(x0, y0 + 59, 96, 5, brass)
+ c.rect(x0, y0, 5, 64, brass)
+ c.rect(x0 + 91, y0, 5, 64, brass)
+ c.rect(x0 + 9, y0 + 9, 78, 46, rug_inner)
+ for dy in range(-9, 10):
+ w = (9 - abs(dy)) * 2
+ c.rect(x0 + 48 - w // 2, y0 + 32 + dy, w, 1, brass)
+ for cx, cy in ((12, 12), (82, 12), (12, 50), (82, 50)):
+ c.rect(x0 + cx, y0 + cy, 2, 2, brass)
+ for y in range(9, 55):
+ for x in range(9, 87):
+ if (x * 7 + y * 13) % 41 == 0:
+ c.set(x0 + x, y0 + y, rug_dark)
+
+ x0, y0 = 352, 60
+ c.rect(x0, y0 + 8, 40, 28, wood)
+ c.rect(x0, y0, 40, 12, frame_wood)
+ c.rect(x0, y0, 40, 2, wood_light)
+ c.rect(x0, y0 + 11, 40, 2, ink)
+ c.rect(x0 + 17, y0 + 9, 6, 8, brass)
+ c.rect(x0 + 19, y0 + 12, 2, 3, ink)
+ for bx, by in ((0, 8), (37, 8), (0, 31), (37, 31)):
+ c.rect(x0 + bx, y0 + by, 3, 3, wood_light)
+ c.rect(x0, y0 + 33, 40, 3, ink)
+ c.outline(x0, y0, 40, 36)
+
+ return c
+
+
+def main():
+ ASSET_DIR.mkdir(parents=True, exist_ok=True)
+ targets = [
+ ("jiangnan-world.png", build_world_atlas(), 256, 32),
+ ("jiangnan-characters.png", build_character_atlas(), 1024, 48),
+ ("jiangnan-interior.png", build_interior_atlas(), 392, 96),
+ ]
+ for name, canvas, w, h in targets:
+ path = ASSET_DIR / name
+ write_png(path, canvas)
+ verify_png(path, w, h)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index 842ab4a..c5b127f 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -416,3 +416,16 @@ type PrototypeTests () =
Assert.IsTrue(manifestLines |> Array.forall (fun line -> line.Length = 1), "The CJK manifest must contain exactly one character per line.")
CollectionAssert.AreEqual(characters, manifestLines |> Array.map (fun line -> line.[0]))
Assert.IsTrue(File.Exists(runtimeAssetPath ()), sprintf "Missing atlas PNG: %s" (runtimeAssetPath ()))
+
+ [<TestMethod>]
+ member _.JiangnanArtAssetsArePackagedAsRealPngFiles () =
+ let requiredAssets =
+ [ "Assets/jiangnan-world.png"
+ "Assets/jiangnan-characters.png"
+ "Assets/jiangnan-interior.png" ]
+
+ let missing =
+ requiredAssets
+ |> List.filter (fun relativePath -> not (File.Exists(Path.Combine(AppContext.BaseDirectory, relativePath))))
+
+ Assert.AreEqual<string list>([], missing, sprintf "Missing Jiangnan PNG assets: %s" (String.concat ", " missing))
diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-characters.png b/src/LivingVillage.Desktop/Assets/jiangnan-characters.png
new file mode 100644
index 0000000..ffe6f66
--- /dev/null
+++ b/src/LivingVillage.Desktop/Assets/jiangnan-characters.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-interior.png b/src/LivingVillage.Desktop/Assets/jiangnan-interior.png
new file mode 100644
index 0000000..d23f6b7
--- /dev/null
+++ b/src/LivingVillage.Desktop/Assets/jiangnan-interior.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-world.png b/src/LivingVillage.Desktop/Assets/jiangnan-world.png
new file mode 100644
index 0000000..75dc906
--- /dev/null
+++ b/src/LivingVillage.Desktop/Assets/jiangnan-world.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index dd04e26..514928f 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -387,7 +387,7 @@ type LivingVillageGame() as this =
override _.LoadContent() =
spriteBatch <- new SpriteBatch(this.GraphicsDevice)
- artTextures <- VillageArt.buildTextures this.GraphicsDevice
+ artTextures <- VillageArt.loadTextures this.GraphicsDevice
cjkAtlas <- CjkGlyphAtlas.load this.GraphicsDevice
pixel <- new Texture2D(this.GraphicsDevice, 1, 1)
pixel.SetData([| Color.White |])
@@ -395,6 +395,7 @@ type LivingVillageGame() as this =
override _.UnloadContent() =
if not (isNull artTextures.TileAtlas) then artTextures.TileAtlas.Dispose()
if not (isNull artTextures.CharacterAtlas) then artTextures.CharacterAtlas.Dispose()
+ if not (isNull artTextures.InteriorAtlas) then artTextures.InteriorAtlas.Dispose()
if not (isNull cjkAtlas) then cjkAtlas.Dispose()
if not (isNull pixel) then pixel.Dispose()
if not (isNull spriteBatch) then spriteBatch.Dispose()
@@ -527,7 +528,9 @@ type LivingVillageGame() as this =
spriteBatch.Begin()
match m5View.HomeMode with
| Inside _ ->
- VillageArt.drawInterior spriteBatch pixel viewport.Width viewport.Height renderPlan.Interior
+ VillageArt.drawInterior spriteBatch pixel artTextures viewport.Width viewport.Height renderPlan.Interior Color.White
+ let interiorAvatarSpec = VillageArt.avatarSpriteSpec avatarFacing world.Tick
+ VillageArt.drawInteriorCharacter spriteBatch artTextures viewport.Width viewport.Height world.Avatar.Pos interiorAvatarSpec Color.White
| Outside ->
VillageArt.drawWorld
spriteBatch
@@ -538,7 +541,7 @@ type LivingVillageGame() as this =
renderPlan
profile.WorldTint
- let avatarSpec = VillageArt.npcSpriteSpec (NpcId 0) avatarFacing world.Tick
+ let avatarSpec = VillageArt.avatarSpriteSpec avatarFacing world.Tick
VillageArt.drawCharacter spriteBatch artTextures camera world.Avatar.Pos avatarSpec profile.WorldTint
for npc in world.Npcs do
diff --git a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
index 4a1b483..b6b619f 100644
--- a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
+++ b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj
@@ -28,6 +28,9 @@
<ItemGroup>
<None Include="Assets\cjk-glyph-atlas.png" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<None Include="Assets\cjk-glyphs.txt" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
+ <None Include="Assets\jiangnan-world.png" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
+ <None Include="Assets\jiangnan-characters.png" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
+ <None Include="Assets\jiangnan-interior.png" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Project>
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index e46d85c..03cd22e 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -66,6 +66,7 @@ module VillageArt =
| IndigoRobe
| OchreVest
| JadeSash
+ | StrawHat
type FacingDirection =
| NorthFacing
@@ -84,7 +85,8 @@ module VillageArt =
type ArtTextures =
{ TileAtlas: Texture2D
- CharacterAtlas: Texture2D }
+ CharacterAtlas: Texture2D
+ InteriorAtlas: Texture2D }
type private TileSprite =
| GrassSprite
@@ -222,6 +224,7 @@ module VillageArt =
| IndigoRobe -> 0
| OchreVest -> 1
| JadeSash -> 2
+ | StrawHat -> 3
let directionIndex =
match spec.Direction with
| NorthFacing -> 0
@@ -235,151 +238,41 @@ module VillageArt =
let cellIndex = (variantIndex * 4 + directionIndex) * 2 + frameIndex
Microsoft.Xna.Framework.Rectangle(cellIndex * 32, 0, 32, 48)
- let private setPixel (pixels: XnaColor[]) (width: int) (x: int) (y: int) (color: XnaColor) =
- let height = pixels.Length / width
- if x >= 0 && x < width && y >= 0 && y < height then
- pixels.[y * width + x] <- color
+ let private rgba (r: int) (g: int) (b: int) (a: int) : XnaColor = XnaColor(r, g, b, a)
- let private fillRect (pixels: XnaColor[]) (width: int) (x: int) (y: int) (rectWidth: int) (rectHeight: int) (color: XnaColor) =
- for row in y .. y + rectHeight - 1 do
- for column in x .. x + rectWidth - 1 do
- setPixel pixels width column row color
+ let avatarSpriteSpec (direction: FacingDirection) (tick: int64) : NpcSpriteSpec =
+ { Variant = StrawHat
+ Direction = direction
+ Frame = animationFrame tick }
- let private rgba (r: int) (g: int) (b: int) (a: int) : XnaColor = XnaColor(r, g, b, a)
+ let worldAtlasRelativePath = "Assets/jiangnan-world.png"
+ let characterAtlasRelativePath = "Assets/jiangnan-characters.png"
+ let interiorAtlasRelativePath = "Assets/jiangnan-interior.png"
+
+ let worldAtlasWidth = Sim.tilePixels * 8
+ let worldAtlasHeight = Sim.tilePixels
+ let characterAtlasWidth = 32 * 4 * 4 * 2
+ let characterAtlasHeight = 48
+ let interiorAtlasWidth = 392
+ let interiorAtlasHeight = 96
+
+ let private loadTexture (device: GraphicsDevice) (relativePath: string) (expectedWidth: int) (expectedHeight: int) : Texture2D =
+ let fullPath = System.IO.Path.Combine(System.AppContext.BaseDirectory, relativePath)
+ if not (System.IO.File.Exists fullPath) then
+ raise (System.IO.FileNotFoundException(sprintf "Jiangnan art atlas is missing: %s" fullPath, fullPath))
+
+ use stream = System.IO.File.OpenRead fullPath
+ let texture = Texture2D.FromStream(device, stream)
+ if texture.Width <> expectedWidth || texture.Height <> expectedHeight then
+ texture.Dispose()
+ invalidOp (sprintf "Jiangnan art atlas %s has dimensions %dx%d; expected %dx%d." fullPath texture.Width texture.Height expectedWidth expectedHeight)
+ else
+ texture
- let private buildTileAtlas (device: GraphicsDevice) : Texture2D =
- let atlasWidth = Sim.tilePixels * 8
- let atlasHeight = Sim.tilePixels
- let pixels = Array.create (atlasWidth * atlasHeight) (rgba 0 0 0 0)
- let fillSlot slot color = fillRect pixels atlasWidth (slot * Sim.tilePixels) 0 Sim.tilePixels Sim.tilePixels color
- let setSlotPixel slot x y color = setPixel pixels atlasWidth (slot * Sim.tilePixels + x) y color
- let grass = rgba 66 112 70 255
- let grassLight = rgba 92 139 82 255
- let water = rgba 54 116 145 255
- let waterLight = rgba 105 174 184 255
- let path = rgba 170 153 123 255
- let pathLight = rgba 201 183 145 255
- let roof = rgba 45 49 57 255
- let roofLight = rgba 76 80 85 255
- let wall = rgba 225 216 195 255
- let wallShadow = rgba 166 147 119 255
- let wood = rgba 119 76 47 255
- let woodLight = rgba 164 111 61 255
- let bamboo = rgba 51 101 61 255
- let bambooLight = rgba 107 153 81 255
- let soil = rgba 122 78 49 255
- let sprout = rgba 86 139 68 255
-
- fillSlot 0 grass
- for y in 0 .. Sim.tilePixels - 1 do
- for x in 0 .. Sim.tilePixels - 1 do
- if (x * 13 + y * 7) % 29 = 0 then
- setSlotPixel 0 x y grassLight
-
- fillSlot 1 water
- for y in [ 5; 16; 27 ] do
- for x in 3 .. Sim.tilePixels - 4 do
- if (x + y) % 5 <> 0 then
- setSlotPixel 1 x y waterLight
- for x in [ 8; 23 ] do
- fillRect pixels atlasWidth (x + Sim.tilePixels) 2 2 26 (rgba 44 93 124 255)
-
- fillSlot 2 path
- for y in [ 0; 31 ] do
- for x in 0 .. Sim.tilePixels - 1 do
- setSlotPixel 2 x y pathLight
- for x in [ 7; 24 ] do
- for y in 4 .. 27 do
- if y % 3 <> 0 then
- setSlotPixel 2 x y pathLight
-
- fillSlot 3 roof
- for y in [ 5; 13; 21; 29 ] do
- for x in 0 .. Sim.tilePixels - 1 do
- if (x + y) % 6 < 5 then
- setSlotPixel 3 x y roofLight
-
- fillSlot 4 wall
- fillRect pixels atlasWidth (4 * Sim.tilePixels) 0 2 Sim.tilePixels wallShadow
- fillRect pixels atlasWidth (4 * Sim.tilePixels + 15) 0 2 Sim.tilePixels wallShadow
- fillRect pixels atlasWidth (4 * Sim.tilePixels + 30) 0 2 Sim.tilePixels wallShadow
- fillRect pixels atlasWidth (4 * Sim.tilePixels) 27 Sim.tilePixels 3 wallShadow
-
- fillSlot 5 wood
- for y in [ 6; 15; 24 ] do
- fillRect pixels atlasWidth (5 * Sim.tilePixels) y Sim.tilePixels 2 woodLight
-
- fillSlot 6 (rgba 0 0 0 0)
- for stemX in [ 6; 16; 26 ] do
- fillRect pixels atlasWidth (6 * Sim.tilePixels + stemX) 4 3 25 bamboo
- for leafY in [ 8; 17; 25 ] do
- fillRect pixels atlasWidth (6 * Sim.tilePixels + stemX - 4) leafY 5 2 bambooLight
- fillRect pixels atlasWidth (6 * Sim.tilePixels + stemX + 2) (leafY + 2) 5 2 bambooLight
-
- fillSlot 7 (rgba 0 0 0 0)
- for row in [ 6; 15; 24 ] do
- fillRect pixels atlasWidth (7 * Sim.tilePixels + 2) row 28 3 soil
- for sproutX in [ 6; 15; 24 ] do
- fillRect pixels atlasWidth (7 * Sim.tilePixels + sproutX) (row - 3) 2 3 sprout
-
- let texture = new Texture2D(device, atlasWidth, atlasHeight, false, SurfaceFormat.Color)
- texture.SetData pixels
- texture
-
- let private buildCharacterAtlas (device: GraphicsDevice) : Texture2D =
- let cellWidth = 32
- let cellHeight = 48
- let columns = 3 * 4 * 2
- let atlasWidth = columns * cellWidth
- let pixels = Array.create (atlasWidth * cellHeight) (rgba 0 0 0 0)
- let variants = [| IndigoRobe; OchreVest; JadeSash |]
- let directions = [| NorthFacing; SouthFacing; WestFacing; EastFacing |]
- let frames = [| FrameOne; FrameTwo |]
-
- for variantIndex in 0 .. variants.Length - 1 do
- for directionIndex in 0 .. directions.Length - 1 do
- for frameIndex in 0 .. frames.Length - 1 do
- let variant = variants.[variantIndex]
- let direction = directions.[directionIndex]
- let frame = frames.[frameIndex]
- let spec = { Variant = variant; Direction = direction; Frame = frame }
- let source = characterSourceRectangle spec
- let offsetX = source.X
- let robe, sash =
- match variant with
- | IndigoRobe -> rgba 46 57 108 255, rgba 104 145 183 255
- | OchreVest -> rgba 141 91 45 255, rgba 205 163 76 255
- | JadeSash -> rgba 46 113 91 255, rgba 121 185 139 255
- let skin = rgba 232 180 139 255
- let hair = rgba 42 36 37 255
- let shadow = rgba 23 31 34 120
- let legShift = if frame = FrameOne then 0 else 2
- fillRect pixels atlasWidth (offsetX + 7) 42 18 3 shadow
- fillRect pixels atlasWidth (offsetX + 8 + legShift) 35 6 10 robe
- fillRect pixels atlasWidth (offsetX + 18 - legShift) 35 6 10 robe
- fillRect pixels atlasWidth (offsetX + 7) 22 18 16 robe
- fillRect pixels atlasWidth (offsetX + 9) 32 14 4 sash
- fillRect pixels atlasWidth (offsetX + 10) 12 12 10 skin
- fillRect pixels atlasWidth (offsetX + 7) 8 18 4 hair
- fillRect pixels atlasWidth (offsetX + 5) 7 22 3 sash
- match direction with
- | NorthFacing ->
- fillRect pixels atlasWidth (offsetX + 9) 13 14 8 hair
- | SouthFacing ->
- fillRect pixels atlasWidth (offsetX + 12) 17 2 2 hair
- fillRect pixels atlasWidth (offsetX + 18) 17 2 2 hair
- | WestFacing ->
- fillRect pixels atlasWidth (offsetX + 9) 16 3 5 hair
- | EastFacing ->
- fillRect pixels atlasWidth (offsetX + 20) 16 3 5 hair
-
- let texture = new Texture2D(device, atlasWidth, cellHeight, false, SurfaceFormat.Color)
- texture.SetData pixels
- texture
-
- let buildTextures (device: GraphicsDevice) : ArtTextures =
- { TileAtlas = buildTileAtlas device
- CharacterAtlas = buildCharacterAtlas device }
+ let loadTextures (device: GraphicsDevice) : ArtTextures =
+ { TileAtlas = loadTexture device worldAtlasRelativePath worldAtlasWidth worldAtlasHeight
+ CharacterAtlas = loadTexture device characterAtlasRelativePath characterAtlasWidth characterAtlasHeight
+ InteriorAtlas = loadTexture device interiorAtlasRelativePath interiorAtlasWidth interiorAtlasHeight }
let private tileSourceRectangle sprite : XnaRectangle =
let index =
@@ -494,30 +387,116 @@ module VillageArt =
48)
spriteBatch.Draw(textures.CharacterAtlas, destination, characterSourceRectangle spec, tint)
- let drawInterior (spriteBatch: SpriteBatch) (pixel: Texture2D) (viewportWidth: int) (viewportHeight: int) (plan: InteriorPlan) =
- let roomX = 40
- let roomY = 40
- let roomWidth = max 320 (viewportWidth - 80)
- let roomHeight = max 240 (viewportHeight - 80)
+ type InteriorLayout =
+ { RoomX: int
+ RoomY: int
+ RoomWidth: int
+ RoomHeight: int }
+
+ let private interiorWallBandHeight = 64
+
+ let private floorCellSource = XnaRectangle(0, 64, Sim.tilePixels, Sim.tilePixels)
+ let private wallCellSource = XnaRectangle(32, 64, Sim.tilePixels, Sim.tilePixels)
+ let private rugSource = XnaRectangle(256, 32, 96, 64)
+ let private chestSource = XnaRectangle(352, 60, 40, 36)
+
+ let interiorSourceRectangle (kind: InteriorElementKind) : Microsoft.Xna.Framework.Rectangle =
+ match kind with
+ | Window -> XnaRectangle(64, 56, 48, 40)
+ | Table -> XnaRectangle(112, 48, 64, 48)
+ | Bed -> XnaRectangle(176, 0, 48, 96)
+ | Screen -> XnaRectangle(224, 0, 32, 96)
+
+ let interiorLayout (viewportWidth: int) (viewportHeight: int) : InteriorLayout =
+ { RoomX = 40
+ RoomY = 40
+ RoomWidth = max 320 (viewportWidth - 80)
+ RoomHeight = max 240 (viewportHeight - 80) }
+
+ let interiorColumns (layout: InteriorLayout) : int =
+ max 1 (layout.RoomWidth / Sim.tilePixels)
+
+ let interiorRows (layout: InteriorLayout) : int =
+ max 1 ((layout.RoomHeight - interiorWallBandHeight) / Sim.tilePixels)
+
+ let interiorFloorY (layout: InteriorLayout) : int =
+ layout.RoomY + interiorWallBandHeight
+
+ let private clampInt (minimum: int) (maximum: int) (value: int) : int =
+ if value < minimum then minimum
+ elif value > maximum then maximum
+ else value
+
+ let interiorCharacterScreenPosition (layout: InteriorLayout) (worldPosition: Vec2) : PixelPosition =
+ let sceneTile = sceneTileFromPixel { PixelPosition.X = worldPosition.X; Y = worldPosition.Y }
+ let tileX = clampInt 1 (interiorColumns layout - 2) sceneTile.X
+ let tileY = clampInt 1 (interiorRows layout - 2) sceneTile.Y
+ let floorY = interiorFloorY layout
+ { PixelPosition.X = float32 (layout.RoomX + tileX * Sim.tilePixels + Sim.tilePixels / 2)
+ Y = float32 (floorY + (tileY + 1) * Sim.tilePixels) }
+
+ let drawInterior
+ (spriteBatch: SpriteBatch)
+ (pixel: Texture2D)
+ (textures: ArtTextures)
+ (viewportWidth: int)
+ (viewportHeight: int)
+ (plan: InteriorPlan)
+ (tint: XnaColor) =
+ let layout = interiorLayout viewportWidth viewportHeight
+ let floorY = interiorFloorY layout
+ let columns = interiorColumns layout
+ let rows = interiorRows layout
+ let partialRows = (layout.RoomHeight - interiorWallBandHeight) % Sim.tilePixels
+ let totalRows = 2 + rows + (if partialRows > 0 then 1 else 0)
+
spriteBatch.Draw(pixel, XnaRectangle(0, 0, viewportWidth, viewportHeight), rgba 18 25 42 255)
- spriteBatch.Draw(pixel, XnaRectangle(roomX, roomY, roomWidth, roomHeight), rgba 188 163 126 255)
- spriteBatch.Draw(pixel, XnaRectangle(roomX, roomY, roomWidth, 18), rgba 65 57 59 255)
- spriteBatch.Draw(pixel, XnaRectangle(roomX, roomY + roomHeight - 18, roomWidth, 18), rgba 99 70 50 255)
- for element in plan.Elements do
- let x = roomX + element.Position.X * Sim.tilePixels
- let y = roomY + element.Position.Y * Sim.tilePixels
+ let drawCell (source: XnaRectangle) (screenX: int) (screenY: int) =
+ let width = min Sim.tilePixels (layout.RoomX + layout.RoomWidth - screenX)
+ let height = min Sim.tilePixels (layout.RoomY + layout.RoomHeight - screenY)
+ if width > 0 && height > 0 then
+ spriteBatch.Draw(textures.InteriorAtlas, XnaRectangle(screenX, screenY, width, height), source, tint)
+
+ for row in 0 .. totalRows - 1 do
+ for column in 0 .. columns - 1 do
+ let screenX = layout.RoomX + column * Sim.tilePixels
+ let screenY = layout.RoomY + row * Sim.tilePixels
+ let source = if row < 2 then wallCellSource else floorCellSource
+ drawCell source screenX screenY
+
+ let rugX = layout.RoomX + (layout.RoomWidth - 96) / 2
+ let rugY = floorY + 96
+ spriteBatch.Draw(textures.InteriorAtlas, XnaRectangle(rugX, rugY, 96, 64), rugSource, tint)
+
+ let elementDestination (element: InteriorElement) : XnaRectangle =
+ let baseY = floorY + (element.Position.Y + 1) * Sim.tilePixels
match element.Kind with
| Window ->
- spriteBatch.Draw(pixel, XnaRectangle(x, y, 48, 30), rgba 86 143 170 255)
- spriteBatch.Draw(pixel, XnaRectangle(x + 22, y, 4, 30), rgba 225 216 195 255)
+ XnaRectangle(layout.RoomX + element.Position.X * Sim.tilePixels - 8, layout.RoomY + 24, 48, 40)
| Table ->
- spriteBatch.Draw(pixel, XnaRectangle(x, y, 64, 32), rgba 119 76 47 255)
- spriteBatch.Draw(pixel, XnaRectangle(x + 8, y + 28, 8, 20), rgba 83 52 39 255)
- spriteBatch.Draw(pixel, XnaRectangle(x + 48, y + 28, 8, 20), rgba 83 52 39 255)
+ XnaRectangle(layout.RoomX + element.Position.X * Sim.tilePixels - 16, baseY - 48, 64, 48)
| Bed ->
- spriteBatch.Draw(pixel, XnaRectangle(x, y, 48, 80), rgba 114 62 67 255)
- spriteBatch.Draw(pixel, XnaRectangle(x + 4, y + 4, 40, 24), rgba 224 211 185 255)
+ XnaRectangle(layout.RoomX + element.Position.X * Sim.tilePixels - 8, baseY - 96, 48, 96)
| Screen ->
- spriteBatch.Draw(pixel, XnaRectangle(x, y, 16, 80), rgba 61 45 49 255)
- spriteBatch.Draw(pixel, XnaRectangle(x + 12, y + 4, 8, 72), rgba 142 102 72 255)
+ XnaRectangle(layout.RoomX + element.Position.X * Sim.tilePixels, baseY - 96, 32, 96)
+
+ for element in plan.Elements |> List.sortBy (fun element -> element.Position.Y, element.Position.X) do
+ spriteBatch.Draw(textures.InteriorAtlas, elementDestination element, interiorSourceRectangle element.Kind, tint)
+
+ let chestX = layout.RoomX + layout.RoomWidth - 44
+ let chestY = layout.RoomY + layout.RoomHeight - 36
+ spriteBatch.Draw(textures.InteriorAtlas, XnaRectangle(chestX, chestY, 40, 36), chestSource, tint)
+
+ let drawInteriorCharacter
+ (spriteBatch: SpriteBatch)
+ (textures: ArtTextures)
+ (viewportWidth: int)
+ (viewportHeight: int)
+ (position: Vec2)
+ (spec: NpcSpriteSpec)
+ (tint: XnaColor) =
+ let layout = interiorLayout viewportWidth viewportHeight
+ let screenPosition = interiorCharacterScreenPosition layout position
+ let destination = XnaRectangle(int screenPosition.X - 16, int screenPosition.Y - 48, 32, 48)
+ spriteBatch.Draw(textures.CharacterAtlas, destination, characterSourceRectangle spec, tint)