diff options
Diffstat (limited to 'src/LivingVillage.Desktop/CharacterArt.fs')
| -rw-r--r-- | src/LivingVillage.Desktop/CharacterArt.fs | 242 |
1 files changed, 242 insertions, 0 deletions
diff --git a/src/LivingVillage.Desktop/CharacterArt.fs b/src/LivingVillage.Desktop/CharacterArt.fs new file mode 100644 index 0000000..9d02baf --- /dev/null +++ b/src/LivingVillage.Desktop/CharacterArt.fs @@ -0,0 +1,242 @@ +namespace LivingVillage.Desktop + +open Microsoft.Xna.Framework +open Microsoft.Xna.Framework.Graphics + +/// P45 角色动画帧:全部原创手绘点阵,用像素代码内嵌生成图集,不依赖任何外部素材。 +/// 布局与既有 `characterSourceRectangle` 完全一致:cell 32x48, +/// cellIndex = (variant*4 + direction)*6 + frame,共 5 变体 x 4 向 x 6 帧。 +/// direction 顺序 North/South/West/East(0..3),frame 顺序 走1/走2/走3/走4/待机1/待机2(0..5)。 +module CharacterArt = + + let sourceWidth = 16 + let sourceHeight = 24 + let pixelScale = 2 + let cellWidth = sourceWidth * pixelScale + let cellHeight = sourceHeight * pixelScale + let variantCount = 5 + let directionCount = 4 + let frameCount = 6 + let walkFrameCount = 4 + let idleFrameCount = 2 + let atlasWidth = cellWidth * variantCount * directionCount * frameCount + + // ---- 点阵字母:. 透明,其余为调色板索引 ---- + // H 头发 S 皮肤 E 眼睛 K 描边 T 草帽 R 外衣 r 外衣暗部 B 腰带 F 鞋 + + /// 头部(10 行)按朝向:North 后脑、South 正面、West 左侧脸、East 右侧脸(镜像)。 + let private headNorth : string list = + [ "................" + "....HHHHHHHH...." + "...HHHHHHHHHH..." + "...HHHHHHHHHH..." + "...HHHHHHHHHH..." + "...HHHHHHHHHH..." + "....HHHHHHHH...." + "....HHHHHHHH...." + ".....HHHHHH....." + "......KKKK......" ] + + let private headSouth : string list = + [ "................" + "....HHHHHHHH...." + "...HHHHHHHHHH..." + "...HSSSSSSSSH..." + "...HSSESSESSH..." + "...HSSSSSSSSH..." + "...HSSSSSSSSH..." + "....SSSSSSSS...." + ".....SSSSSS....." + "......KKKK......" ] + + let private headWest : string list = + [ "................" + "....HHHHHHHH...." + "...HHHHHHHHHH..." + "...HHHSSSSHHH..." + "...HHSESSSHHH..." + "...HHSSSSSHHH..." + "....HHSSSSHH...." + "....HSSSSSSH...." + ".....SSSSS......" + "......KKKK......" ] + + /// 躯干(8 行,行 10..17):走/待机的左右手臂高度不同,形成摆臂。 + let private torsoWalk0 : string list = + [ "...RRRRRRRRRS..." + "...RRRRRRRRRS..." + "...SRRRRRRRRS..." + "...SRRRRRRRRR..." + "...SRRRRRRRRR..." + "....RRRRRRRR...." + "....BBBBBBBB...." + "....RRRRRRRR...." ] + + let private torsoWalk1 : string list = + [ "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "....RRRRRRRR...." + "....BBBBBBBB...." + "....RRRRRRRR...." ] + + let private torsoWalk2 : string list = + [ "...SRRRRRRRRR..." + "...SRRRRRRRRR..." + "...SRRRRRRRRS..." + "...RRRRRRRRRS..." + "...RRRRRRRRRS..." + "....RRRRRRRR...." + "....BBBBBBBB...." + "....RRRRRRRR...." ] + + let private torsoWalk3 : string list = + [ "...SRRRRRRRRR..." + "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "...RRRRRRRRRS..." + "....RRRRRRRR...." + "....BBBBBBBB...." + "....RRRRRRRR...." ] + + let private torsoIdle1 : string list = + [ "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "....RRRRRRRR...." + "....BBBBBBBB...." + "....RRRRRRRR...." ] + + /// 待机第二帧:手臂收短、腰带整体上提 2 行,读作呼吸起伏。 + let private torsoIdle2 : string list = + [ "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "...SRRRRRRRRS..." + "....RRRRRRRR...." + "....BBBBBBBB...." + "....RRRRRRRR...." + "....RRRRRRRR...." + "....RRRRRRRR...." ] + + /// 腿(6 行,行 18..23):四帧行走跨步姿态各不相同,待机两帧双脚并拢。 + let private legsWalk0 : string list = + [ "....RRRRRRRR...." + "...RRRRRRRRRR..." + "..RRR......RRR.." + "..RRR......RRR.." + "..FFF......FFF.." + "..FFF......FFF.." ] + + let private legsWalk1 : string list = + [ "....RRRRRRRR...." + "....RRRRRRRR...." + ".....RRRRRR....." + ".....RR.RRR....." + ".....FF.RRR....." + "........FFF....." ] + + let private legsWalk2 : string list = + [ "....RRRRRRRR...." + "...RRRRRRRRR...." + "...RRR....RRR..." + "...RRR....RR...." + "...FFF....FF...." + "...FFF.........." ] + + let private legsWalk3 : string list = + [ "....RRRRRRRR...." + "....RRRRRRRR...." + ".....RRRRRR....." + ".....RRR.RR....." + ".....RRR.FF....." + ".....FFF........" ] + + let private legsIdle : string list = + [ "....RRRRRRRR...." + "....RRRRRRRR...." + ".....RRRRRR....." + ".....RRR.RR....." + ".....FFF.FF....." + ".....FFF.FF....." ] + + let private torsoByFrame : string list list = + [ torsoWalk0; torsoWalk1; torsoWalk2; torsoWalk3; torsoIdle1; torsoIdle2 ] + + let private legsByFrame : string list list = + [ legsWalk0; legsWalk1; legsWalk2; legsWalk3; legsIdle; legsIdle ] + + let private mirrorRow (row: string) : string = + System.String(row.ToCharArray() |> Array.rev) + + let private headByDirection : string list list = + [ headNorth; headSouth; headWest; headWest |> List.map mirrorRow ] + + /// 某个 (变体, 朝向, 帧) 的 24 行点阵字母;草帽变体在头部叠加草帽。 + let frameLetters (variantIndex: int) (directionIndex: int) (frameIndex: int) : string list = + let head = headByDirection.[((directionIndex % directionCount) + directionCount) % directionCount] + let torso = torsoByFrame.[((frameIndex % frameCount) + frameCount) % frameCount] + let legs = legsByFrame.[((frameIndex % frameCount) + frameCount) % frameCount] + let composed = head @ torso @ legs + if (variantIndex % variantCount + variantCount) % variantCount = 4 then + composed + |> List.mapi (fun row line -> + if row = 1 || row = 2 then + line |> Seq.map (fun c -> if c = '.' then '.' else 'T') |> System.String.Concat + elif row = 3 then + line.ToCharArray() + |> Array.mapi (fun column c -> if column >= 2 && column <= 13 then 'T' else c) + |> System.String + else line) + else composed + + /// 变体外衣主色 / 暗部 / 腰带;决定 5 个村民的观感差异。 + let variantColors (variantIndex: int) : Color * Color * Color = + match ((variantIndex % variantCount) + variantCount) % variantCount with + | 0 -> Color(66, 84, 148), Color(44, 58, 110), Color(214, 198, 150) // 靛蓝长衫 + | 1 -> Color(178, 132, 66), Color(128, 92, 44), Color(70, 60, 50) // 赭黄马甲 + | 2 -> Color(74, 150, 128), Color(48, 110, 92), Color(232, 214, 160) // 青碧束带 + | 3 -> Color(118, 124, 134), Color(84, 90, 100), Color(58, 62, 70) // 灰帽 + | _ -> Color(146, 96, 70), Color(108, 70, 52), Color(90, 74, 48) // 斗笠农夫 + + let private colorOf (variantIndex: int) (letter: char) : Color option = + let robe, robeShade, belt = variantColors variantIndex + match letter with + | 'H' -> Some(Color(44, 36, 34)) + | 'S' -> Some(Color(234, 194, 152)) + | 'E' -> Some(Color(24, 22, 28)) + | 'K' -> Some(Color(28, 30, 38)) + | 'T' -> Some(Color(216, 190, 120)) + | 'R' -> Some robe + | 'r' -> Some robeShade + | 'B' -> Some belt + | 'F' -> Some(Color(58, 42, 34)) + | _ -> None + + let private cellIndex (variantIndex: int) (directionIndex: int) (frameIndex: int) : int = + (variantIndex * directionCount + directionIndex) * frameCount + frameIndex + + /// 像素点阵 -> 直通 alpha 图集纹理。仅用 SetData,无外部素材。 + let buildAtlas (device: GraphicsDevice) : Texture2D = + let pixels = Array.zeroCreate<Color> (atlasWidth * cellHeight) + for variant in 0 .. variantCount - 1 do + for direction in 0 .. directionCount - 1 do + for frame in 0 .. frameCount - 1 do + let originX = cellIndex variant direction frame * cellWidth + let rows = frameLetters variant direction frame + for y in 0 .. sourceHeight - 1 do + let row = rows.[y] + for x in 0 .. sourceWidth - 1 do + match colorOf variant row.[x] with + | Some color -> + for sy in 0 .. pixelScale - 1 do + for sx in 0 .. pixelScale - 1 do + pixels.[(y * pixelScale + sy) * atlasWidth + originX + x * pixelScale + sx] <- color + | None -> () + let texture = new Texture2D(device, atlasWidth, cellHeight) + texture.SetData pixels + texture |
