summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 10:38:53 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 10:38:53 +0800
commit328760853c15c44d1de915d6c88e0dfd7ddc5af6 (patch)
treeaa1e570378cfe92b0c4e9f125141a43d0ba89663 /src/LivingVillage.Desktop
parent46073814a67598c1a4122d1cb5abaaeb38c94e6d (diff)
downloadliving-village-328760853c15c44d1de915d6c88e0dfd7ddc5af6.tar.gz
p72: 角色行走四向动画帧提升(6 走 + 2 待机)
CharacterArt 运行时点阵图集从 4 走 + 2 待机扩为 6 走(新增左右低过渡步 4/5,由 1/2 镜像)+ 2 待机,格数 *8、图集 5120x48;VillageArt 帧循环 %6、characterSourceRectangle *8、characterAtlasWidth 5120。同步 P45/Prototype 帧断言、新增 P72 帧表完整性回归、脚本与素材重新生成、维护说明动画语义。白天正午村庄行走实跑证据落 docs/evidence/p72-*。Desktop 343/343、Kernel 120/120、digest 与 p70 一致。
Diffstat (limited to 'src/LivingVillage.Desktop')
-rw-r--r--src/LivingVillage.Desktop/Assets/jiangnan-characters.pngbin5585 -> 6911 bytes
-rw-r--r--src/LivingVillage.Desktop/CharacterArt.fs31
-rw-r--r--src/LivingVillage.Desktop/Game.fs11
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs24
4 files changed, 46 insertions, 20 deletions
diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-characters.png b/src/LivingVillage.Desktop/Assets/jiangnan-characters.png
index dd96e60..8698b39 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/CharacterArt.fs b/src/LivingVillage.Desktop/CharacterArt.fs
index 9d02baf..bfe86ad 100644
--- a/src/LivingVillage.Desktop/CharacterArt.fs
+++ b/src/LivingVillage.Desktop/CharacterArt.fs
@@ -5,8 +5,9 @@ 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)。
+/// cellIndex = (variant*4 + direction)*8 + frame,共 5 变体 x 4 向 x 8 帧。
+/// direction 顺序 North/South/West/East(0..3),
+/// frame 顺序 走1..走6/待机1/待机2(0..7);P72 将行走由 4 帧提升为 6 帧(含左右过渡步)。
module CharacterArt =
let sourceWidth = 16
@@ -16,8 +17,8 @@ module CharacterArt =
let cellHeight = sourceHeight * pixelScale
let variantCount = 5
let directionCount = 4
- let frameCount = 6
- let walkFrameCount = 4
+ let frameCount = 8
+ let walkFrameCount = 6
let idleFrameCount = 2
let atlasWidth = cellWidth * variantCount * directionCount * frameCount
@@ -102,6 +103,13 @@ module CharacterArt =
"....BBBBBBBB...."
"....RRRRRRRR...." ]
+ let private mirrorRow (row: string) : string =
+ System.String(row.ToCharArray() |> Array.rev)
+
+ /// P72 过渡步:走 5/6 为走 2/3 的左右镜像,形成「左-右」对称的六帧循环。
+ let private torsoWalk4 : string list = torsoWalk1 |> List.map mirrorRow
+ let private torsoWalk5 : string list = torsoWalk2 |> List.map mirrorRow
+
let private torsoIdle1 : string list =
[ "...SRRRRRRRRS..."
"...SRRRRRRRRS..."
@@ -123,7 +131,8 @@ module CharacterArt =
"....RRRRRRRR...."
"....RRRRRRRR...." ]
- /// 腿(6 行,行 18..23):四帧行走跨步姿态各不相同,待机两帧双脚并拢。
+ /// 腿(6 行,行 18..23):六帧行走跨步姿态各不相同,待机两帧双脚并拢。
+ /// 走 5/6 为走 2/3 的左右镜像(P72 过渡步),使左右跨步对称。
let private legsWalk0 : string list =
[ "....RRRRRRRR...."
"...RRRRRRRRRR..."
@@ -154,7 +163,10 @@ module CharacterArt =
".....RRRRRR....."
".....RRR.RR....."
".....RRR.FF....."
- ".....FFF........" ]
+ ".......FFFF....." ]
+
+ let private legsWalk4 : string list = legsWalk1 |> List.map mirrorRow
+ let private legsWalk5 : string list = legsWalk2 |> List.map mirrorRow
let private legsIdle : string list =
[ "....RRRRRRRR...."
@@ -165,13 +177,10 @@ module CharacterArt =
".....FFF.FF....." ]
let private torsoByFrame : string list list =
- [ torsoWalk0; torsoWalk1; torsoWalk2; torsoWalk3; torsoIdle1; torsoIdle2 ]
+ [ torsoWalk0; torsoWalk1; torsoWalk2; torsoWalk3; torsoWalk4; torsoWalk5; 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)
+ [ legsWalk0; legsWalk1; legsWalk2; legsWalk3; legsWalk4; legsWalk5; legsIdle; legsIdle ]
let private headByDirection : string list list =
[ headNorth; headSouth; headWest; headWest |> List.map mirrorRow ]
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index ff1dfe0..6252117 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -99,6 +99,9 @@ type LivingVillageGame() as this =
// P36 证据钩子:复用 sample 脚本的走位/互动序列,但按状态抓 4 帧关键帧
// (靠近→提示出现→对话层打开且世界交互被隔离→关闭离开提示消失)后退出。
let p36ShotMode = Environment.GetEnvironmentVariable("LV_P36_SHOT") = "1"
+ // P72 evidence hook: with sample recording on, log each saved frame's per-NPC walk
+ // frame so the PNGs can be correlated with the deterministic 6-frame cycle.
+ let p72Log = Environment.GetEnvironmentVariable("LV_P72_LOG") = "1"
let sampleMode = Environment.GetEnvironmentVariable("LV_AUTOPLAY_SAMPLE") = "1" || p36ShotMode
let menuShotMode = Environment.GetEnvironmentVariable("LV_AUTOPLAY_MENU_SHOT") = "1"
let daylightAutoplay = Environment.GetEnvironmentVariable("LV_AUTOPLAY_DAYLIGHT") = "1"
@@ -2812,7 +2815,13 @@ type LivingVillageGame() as this =
recordIndex <- recordIndex + 1
if recordIndex % recordEvery = 0 then
System.IO.Directory.CreateDirectory recordDirectory |> ignore
- this.SaveBackBuffer(sprintf "%s/%s%04d.png" recordDirectory recordName recordIndex)
+ let name = sprintf "%s/%s%04d.png" recordDirectory recordName recordIndex
+ this.SaveBackBuffer(name)
+ if p72Log then
+ printfn "p72-frame %s tick=%d" name world.Tick
+ for npc in world.Npcs do
+ let spec = VillageArt.npcSpriteSpecAtTarget npc.Id npc.Pos npc.Mind.Target world.Tick
+ printfn " npc=%A pos=(%.1f,%.1f) frame=%A moving=%b" npc.Id npc.Pos.X npc.Pos.Y spec.Frame (npc.Mind.Target <> npc.Pos)
if menuShotMode then
this.CaptureMenuShot()
if p42ShotMode then
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index 76abc4a..db594d2 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -108,13 +108,16 @@ module VillageArt =
| FrameTwo
| FrameThree
| FrameFour
+ | FrameFive
+ | FrameSix
/// Two-pose standing breath cycle used whenever a character is not moving.
type IdleFrame =
| IdleOne
| IdleTwo
- /// A character either walks (four-pose cycle) or breathes in place (two-pose cycle).
+ /// A character either walks (six-pose cycle, incl. left/right transition steps) or
+ /// breathes in place (two-pose cycle).
type CharacterFrame =
| WalkFrame of AnimationFrame
| IdleFrame of IdleFrame
@@ -363,13 +366,16 @@ module VillageArt =
| Peddler -> { WalkWindow = 3L; IdleWindow = 20L; PhaseOffset = 1L }
| Scholar -> { WalkWindow = 6L; IdleWindow = 36L; PhaseOffset = 2L }
- /// Four-pose walk cycle at a rhythm's window (one pose per WalkWindow sim ticks).
+ /// Six-pose walk cycle at a rhythm's window (one pose per WalkWindow sim ticks).
+ /// P72: four strides became six by adding left/right transition steps.
let animationFrameFor (rhythm: WalkRhythm) (tick: int64) : AnimationFrame =
- match positiveModulo64 4L (tick / rhythm.WalkWindow + rhythm.PhaseOffset) with
+ match positiveModulo64 6L (tick / rhythm.WalkWindow + rhythm.PhaseOffset) with
| 0L -> FrameOne
| 1L -> FrameTwo
| 2L -> FrameThree
- | _ -> FrameFour
+ | 3L -> FrameFour
+ | 4L -> FrameFive
+ | _ -> FrameSix
/// Two-pose standing breath cycle at a rhythm's IdleWindow, fully deterministic.
let idleFrameFor (rhythm: WalkRhythm) (tick: int64) : IdleFrame =
@@ -454,9 +460,11 @@ module VillageArt =
| WalkFrame FrameTwo -> 1
| WalkFrame FrameThree -> 2
| WalkFrame FrameFour -> 3
- | IdleFrame IdleOne -> 4
- | IdleFrame IdleTwo -> 5
- let cellIndex = (variantIndex * 4 + directionIndex) * 6 + frameIndex
+ | WalkFrame FrameFive -> 4
+ | WalkFrame FrameSix -> 5
+ | IdleFrame IdleOne -> 6
+ | IdleFrame IdleTwo -> 7
+ let cellIndex = (variantIndex * 4 + directionIndex) * 8 + frameIndex
Microsoft.Xna.Framework.Rectangle(cellIndex * 32, 0, 32, 48)
let private rgba (r: int) (g: int) (b: int) (a: int) : XnaColor = XnaColor(r, g, b, a)
@@ -566,7 +574,7 @@ module VillageArt =
let worldTileCount = 32
let worldAtlasWidth = Sim.tilePixels * worldTileCount
let worldAtlasHeight = Sim.tilePixels
- let characterAtlasWidth = 32 * 5 * 4 * 6
+ let characterAtlasWidth = 32 * 5 * 4 * 8
let characterAtlasHeight = 48
let interiorAtlasWidth = 392
let interiorAtlasHeight = 96