summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 11:15:49 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 11:15:49 +0800
commit7f0c40dc89672d218f10ba3f81a0ee235da30969 (patch)
treeefa2e3884c4d1443854c17248bad8ddc027feb2f
parent326575888b9c8edf0ded1dcf1c01f5c78d3eca84 (diff)
downloadliving-village-7f0c40dc89672d218f10ba3f81a0ee235da30969.tar.gz
feat(desktop): 村民五套服饰帽饰区分
人物图集扩至五套变体(1280x48):靛蓝袍配布巾、褐衫配发髻马甲、翠衫配发簪红巾、灰袍配瓜皮帽,主角专属斗笠独立第五套;村民按 id 取模四种服饰轮转,永不与主角同装,四向两帧动画保持。
-rw-r--r--scripts/make-jiangnan-art.py40
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs18
-rw-r--r--src/LivingVillage.Desktop/Assets/jiangnan-characters.pngbin1892 -> 2539 bytes
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs11
4 files changed, 57 insertions, 12 deletions
diff --git a/scripts/make-jiangnan-art.py b/scripts/make-jiangnan-art.py
index 616928e..cdae4e9 100644
--- a/scripts/make-jiangnan-art.py
+++ b/scripts/make-jiangnan-art.py
@@ -3,7 +3,7 @@
Generates three RGBA PNGs into src/LivingVillage.Desktop/Assets:
jiangnan-world.png 672x32 (21 ground/prop tiles, 32px each)
- jiangnan-characters.png 1024x48 (4 variants x 4 directions x 2 frames, 32x48 cells)
+ jiangnan-characters.png 1280x48 (5 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.
@@ -434,10 +434,11 @@ def build_world_atlas():
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},
+ {"robe": (46, 57, 108, 255), "light": (104, 145, 183, 255), "dark": (32, 41, 80, 255), "extra": "kerchief"},
+ {"robe": (141, 91, 45, 255), "light": (205, 163, 76, 255), "dark": (104, 66, 32, 255), "extra": "bun_vest"},
+ {"robe": (46, 113, 91, 255), "light": (121, 185, 139, 255), "dark": (32, 84, 68, 255), "extra": "hairpin_scarf"},
+ {"robe": (96, 102, 112, 255), "light": (150, 156, 164, 255), "dark": (62, 66, 74, 255), "extra": "round_cap"},
+ {"robe": (122, 96, 58, 255), "light": (176, 141, 84, 255), "dark": (92, 71, 42, 255), "extra": "straw_hat"},
]
DIRECTIONS = ["north", "south", "west", "east"]
@@ -447,6 +448,7 @@ def draw_character_cell(c, ox, variant, direction, frame):
robe = variant["robe"]
light = variant["light"]
dark = variant["dark"]
+ extra = variant.get("extra")
hat = (196, 158, 74, 255)
hat_light = (228, 197, 112, 255)
hat_dark = (168, 132, 58, 255)
@@ -497,17 +499,39 @@ def draw_character_cell(c, ox, variant, direction, frame):
c.rect(ox + 10, 12, 4, 6, HAIR)
c.rect(ox + 19, 15, 2, 2, EYE)
- if variant["hat"]:
+ if extra == "straw_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)
+ elif extra == "kerchief":
+ c.rect(ox + 8, 7, 16, 4, (40, 52, 96, 255))
+ c.rect(ox + 8, 7, 16, 1, (68, 84, 138, 255))
+ c.rect(ox + 24, 10, 3, 3, (40, 52, 96, 255))
+ c.rect(ox + 24, 12, 2, 4, (68, 84, 138, 255))
+ elif extra == "bun_vest":
+ c.rect(ox + 14, 4, 4, 4, HAIR)
+ c.rect(ox + 15, 3, 2, 1, HAIR)
+ c.rect(ox + 10, 23, 12, 9, dark)
+ c.rect(ox + 10, 23, 12, 1, light)
+ c.rect(ox + 15, 24, 2, 8, light)
+ elif extra == "hairpin_scarf":
+ c.rect(ox + 21, 9, 5, 1, hat)
+ c.rect(ox + 24, 8, 2, 1, hat_light)
+ c.rect(ox + 9, 21, 14, 2, (150, 62, 58, 255))
+ c.rect(ox + 9, 21, 14, 1, (188, 92, 84, 255))
+ c.rect(ox + 22, 22, 3, 6, (150, 62, 58, 255))
+ elif extra == "round_cap":
+ c.rect(ox + 9, 5, 14, 5, (48, 52, 66, 255))
+ c.rect(ox + 10, 4, 12, 2, (48, 52, 66, 255))
+ c.rect(ox + 10, 9, 12, 1, (70, 74, 90, 255))
+ c.rect(ox + 15, 3, 2, 2, hat)
c.outline(ox, 0, 32, 48)
def build_character_atlas():
- c = Canvas(1024, 48)
+ c = Canvas(1280, 48)
cell = 0
for variant in VARIANTS:
for direction in DIRECTIONS:
@@ -650,7 +674,7 @@ def main():
ASSET_DIR.mkdir(parents=True, exist_ok=True)
targets = [
("jiangnan-world.png", build_world_atlas(), 672, 32),
- ("jiangnan-characters.png", build_character_atlas(), 1024, 48),
+ ("jiangnan-characters.png", build_character_atlas(), 1280, 48),
("jiangnan-interior.png", build_interior_atlas(), 392, 96),
]
for name, canvas, w, h in targets:
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index 4d698ad..3e15272 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -223,6 +223,24 @@ type PrototypeTests () =
Assert.AreEqual<AnimationFrame>(FrameOne, (VillageArt.animationFrame 8L))
[<TestMethod>]
+ member _.VillagersUseFourDistinctVariantsDistinctFromTheAvatar () =
+ let npcVariants = [ 0 .. 63 ] |> List.map (fun id -> npcVisualVariant (NpcId id)) |> Set.ofList
+
+ Assert.AreEqual<int>(4, npcVariants.Count)
+ Assert.IsFalse(npcVariants.Contains StrawHat)
+ Assert.AreEqual<NpcVisualVariant>(StrawHat, (avatarSpriteSpec SouthFacing 0L).Variant)
+
+ let variantXes =
+ npcVariants
+ |> Set.toList
+ |> List.map (fun variant ->
+ let rect = characterSourceRectangle { Variant = variant; Direction = SouthFacing; Frame = FrameOne }
+ rect.X)
+ |> Set.ofList
+ Assert.AreEqual<int>(4, variantXes.Count)
+ Assert.AreEqual<int>(1280, VillageArt.characterAtlasWidth)
+
+ [<TestMethod>]
member _.ChineseCopyProvidesFormalSceneTimeMenuAndStatusLabels () =
let labels =
[ sceneLabel VillageScene
diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-characters.png b/src/LivingVillage.Desktop/Assets/jiangnan-characters.png
index ffe6f66..cbbe570 100644
--- a/src/LivingVillage.Desktop/Assets/jiangnan-characters.png
+++ b/src/LivingVillage.Desktop/Assets/jiangnan-characters.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index 151c73a..3b948e5 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -72,6 +72,7 @@ module VillageArt =
| IndigoRobe
| OchreVest
| JadeSash
+ | GreyCap
| StrawHat
type FacingDirection =
@@ -239,10 +240,11 @@ module VillageArt =
if remainder < 0 then remainder + divisor else remainder
let npcVisualVariant (NpcId npcId) : NpcVisualVariant =
- match positiveModulo 3 npcId with
+ match positiveModulo 4 npcId with
| 0 -> IndigoRobe
| 1 -> OchreVest
- | _ -> JadeSash
+ | 2 -> JadeSash
+ | _ -> GreyCap
let private positiveModulo64 divisor value =
let remainder = value % divisor
@@ -289,7 +291,8 @@ module VillageArt =
| IndigoRobe -> 0
| OchreVest -> 1
| JadeSash -> 2
- | StrawHat -> 3
+ | GreyCap -> 3
+ | StrawHat -> 4
let directionIndex =
match spec.Direction with
| NorthFacing -> 0
@@ -317,7 +320,7 @@ module VillageArt =
let worldTileCount = 21
let worldAtlasWidth = Sim.tilePixels * worldTileCount
let worldAtlasHeight = Sim.tilePixels
- let characterAtlasWidth = 32 * 4 * 4 * 2
+ let characterAtlasWidth = 32 * 5 * 4 * 2
let characterAtlasHeight = 48
let interiorAtlasWidth = 392
let interiorAtlasHeight = 96