summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/VillageArt.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 01:41:05 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 01:41:05 +0800
commitfc77481196aaea817eec991bdcf4dc22bb1d245c (patch)
tree95df2d0bff79a742c7f3637fbec680f2e6945316 /src/LivingVillage.Desktop/VillageArt.fs
parentb6b95988e525a68ea7426ecc9690b18a1b3bbfe8 (diff)
downloadliving-village-fc77481196aaea817eec991bdcf4dc22bb1d245c.tar.gz
feat(desktop): 江南美术改为真实 PNG 图集
- scripts/make-jiangnan-art.py 生成三张原创透明 PNG:世界 256x32、角色 1024x48、内景 392x96 - VillageArt.fs 移除进程内像素烘焙,改为 BaseDirectory 加载图集并校验尺寸 - 内景渲染改用图集(地板/墙/窗/桌/床/屏/毯/柜),新增玩家专属 StrawHat 变体 - 内景新增地毯与柜子装饰,角色按 FeetPosition 定位 - fsproj 随包复制三张 PNG;m6c-package.sh 校验三张 PNG 进入交付包
Diffstat (limited to 'src/LivingVillage.Desktop/VillageArt.fs')
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs305
1 files changed, 142 insertions, 163 deletions
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)