summaryrefslogtreecommitdiff
path: root/src
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
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')
-rw-r--r--src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj1
-rw-r--r--src/LivingVillage.Desktop.Tests/P45CharacterAnimTests.fs18
-rw-r--r--src/LivingVillage.Desktop.Tests/P72CharacterWalkTests.fs40
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs36
-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
8 files changed, 119 insertions, 42 deletions
diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
index a736a83..432cd03 100644
--- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
+++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
@@ -43,6 +43,7 @@
<Compile Include="P68MapSnapshotTests.fs" />
<Compile Include="P69CopyAuditTests.fs" />
<Compile Include="P71ApiDocsTests.fs" />
+ <Compile Include="P72CharacterWalkTests.fs" />
<Compile Include="SampleTests.fs" />
</ItemGroup>
diff --git a/src/LivingVillage.Desktop.Tests/P45CharacterAnimTests.fs b/src/LivingVillage.Desktop.Tests/P45CharacterAnimTests.fs
index eba03aa..b9241ad 100644
--- a/src/LivingVillage.Desktop.Tests/P45CharacterAnimTests.fs
+++ b/src/LivingVillage.Desktop.Tests/P45CharacterAnimTests.fs
@@ -5,7 +5,7 @@ open LivingVillage.Kernel
open LivingVillage.Kernel.Sim
open LivingVillage.Desktop
-/// P45 角色动画帧升级回归:四向 x 6 帧原创点阵、帧序列确定性、行走推进、停止回 idle。
+/// P45 角色动画帧升级回归:四向 x 8 帧(6 走 + 2 待机)原创点阵、帧序列确定性、行走推进、停止回 idle。
[<TestClass>]
type P45CharacterAnimTests () =
@@ -76,16 +76,18 @@ type P45CharacterAnimTests () =
[ VillageArt.NorthFacing; VillageArt.SouthFacing; VillageArt.WestFacing; VillageArt.EastFacing ]
|> List.map (fun d -> (rect VillageArt.IndigoRobe d (VillageArt.WalkFrame VillageArt.FrameOne)).X)
assertEqualMsg 4 (xs |> List.distinct |> List.length) "四向必须落在不同源格"
- // 同向 6 帧源格逐一不同且按序排列。
+ // 同向 8 帧源格逐一不同且按序排列。
let frameXs =
[ VillageArt.WalkFrame VillageArt.FrameOne
VillageArt.WalkFrame VillageArt.FrameTwo
VillageArt.WalkFrame VillageArt.FrameThree
VillageArt.WalkFrame VillageArt.FrameFour
+ VillageArt.WalkFrame VillageArt.FrameFive
+ VillageArt.WalkFrame VillageArt.FrameSix
VillageArt.IdleFrame VillageArt.IdleOne
VillageArt.IdleFrame VillageArt.IdleTwo ]
|> List.map (fun f -> (rect VillageArt.IndigoRobe VillageArt.SouthFacing f).X)
- assertEqualMsg 6 (frameXs |> List.distinct |> List.length) "同向 6 帧源格须互不同"
+ assertEqualMsg 8 (frameXs |> List.distinct |> List.length) "同向 8 帧源格须互不同"
assertEqualMsg frameXs (List.sort frameXs) "帧源格须按帧序递增"
[<TestMethod>]
@@ -95,18 +97,22 @@ type P45CharacterAnimTests () =
assertEqualMsg (VillageArt.WalkFrame VillageArt.FrameTwo) (VillageArt.characterFrame true 4L) "走 4"
assertEqualMsg (VillageArt.WalkFrame VillageArt.FrameThree) (VillageArt.characterFrame true 8L) "走 8"
assertEqualMsg (VillageArt.WalkFrame VillageArt.FrameFour) (VillageArt.characterFrame true 12L) "走 12"
- assertEqualMsg (VillageArt.WalkFrame VillageArt.FrameOne) (VillageArt.characterFrame true 16L) "走 16 循环"
+ assertEqualMsg (VillageArt.WalkFrame VillageArt.FrameFive) (VillageArt.characterFrame true 16L) "走 16"
+ assertEqualMsg (VillageArt.WalkFrame VillageArt.FrameSix) (VillageArt.characterFrame true 20L) "走 20"
+ assertEqualMsg (VillageArt.WalkFrame VillageArt.FrameOne) (VillageArt.characterFrame true 24L) "走 24 循环"
// 停止:无论 tick,回 idle 两帧呼吸循环。
assertEqualMsg (VillageArt.IdleFrame VillageArt.IdleOne) (VillageArt.characterFrame false 0L) "停 0"
assertEqualMsg (VillageArt.IdleFrame VillageArt.IdleTwo) (VillageArt.characterFrame false 24L) "停 24"
assertEqualMsg (VillageArt.IdleFrame VillageArt.IdleOne) (VillageArt.characterFrame false 9999L) "停任意 tick 仍在 idle"
// 玩家行走规格:方向 + 是否移动 + tick 决定帧,四向共享同一帧序(方向只换源格)。
let framesFor direction =
- [ 0L; 4L; 8L; 12L ] |> List.map (fun t -> (VillageArt.avatarSpriteSpec direction true t).Frame)
+ [ 0L; 4L; 8L; 12L; 16L; 20L ] |> List.map (fun t -> (VillageArt.avatarSpriteSpec direction true t).Frame)
let north = framesFor VillageArt.NorthFacing
assertEqualMsg [ VillageArt.WalkFrame VillageArt.FrameOne
VillageArt.WalkFrame VillageArt.FrameTwo
VillageArt.WalkFrame VillageArt.FrameThree
- VillageArt.WalkFrame VillageArt.FrameFour ] north "方向->帧序确定"
+ VillageArt.WalkFrame VillageArt.FrameFour
+ VillageArt.WalkFrame VillageArt.FrameFive
+ VillageArt.WalkFrame VillageArt.FrameSix ] north "方向->帧序确定"
for direction in [ VillageArt.SouthFacing; VillageArt.WestFacing; VillageArt.EastFacing ] do
assertEqualMsg north (framesFor direction) "四向帧序一致"
diff --git a/src/LivingVillage.Desktop.Tests/P72CharacterWalkTests.fs b/src/LivingVillage.Desktop.Tests/P72CharacterWalkTests.fs
new file mode 100644
index 0000000..781d22c
--- /dev/null
+++ b/src/LivingVillage.Desktop.Tests/P72CharacterWalkTests.fs
@@ -0,0 +1,40 @@
+namespace LivingVillage.Desktop.Tests
+
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open LivingVillage.Desktop
+
+/// P72 回归:角色行走帧表完整性——四向各 8 格(6 走 + 2 待机),
+/// 六张行走帧两两可见区分(含左右过渡步),且与待机帧不同。
+[<TestClass>]
+type P72CharacterWalkTests () =
+
+ let letters variant direction frame = CharacterArt.frameLetters variant direction frame
+
+ let cellDiff (a: string list) (b: string list) : int =
+ List.zip a b
+ |> List.sumBy (fun (ra, rb) -> Seq.zip ra rb |> Seq.filter (fun (x, y) -> x <> y) |> Seq.length)
+
+ [<TestMethod>]
+ member _.WalkFrameTableHasSixDistinctPosesPerDirection () =
+ Assert.AreEqual<int>(8, CharacterArt.frameCount)
+ Assert.AreEqual<int>(6, CharacterArt.walkFrameCount)
+ Assert.AreEqual<int>(2, CharacterArt.idleFrameCount)
+ for variant in 0 .. CharacterArt.variantCount - 1 do
+ for direction in 0 .. CharacterArt.directionCount - 1 do
+ let walk = [ 0 .. CharacterArt.walkFrameCount - 1 ] |> List.map (letters variant direction)
+ // 6 张行走帧两两必须有可见差异(不允许重复姿态)。
+ for i in 0 .. walk.Length - 1 do
+ for j in i + 1 .. walk.Length - 1 do
+ Assert.IsTrue(
+ cellDiff walk.[i] walk.[j] >= 4,
+ sprintf "v%d d%d 走帧 %d 与 %d 姿态重复" variant direction i j)
+ // 行走与待机必须不同。
+ let idle = letters variant direction CharacterArt.walkFrameCount
+ Assert.IsTrue(cellDiff walk.[0] idle >= 4, "行走帧与待机帧相同")
+
+ [<TestMethod>]
+ member _.AtlasStridePlacesSixWalkThenTwoIdleCells () =
+ Assert.AreEqual<int>(5120, CharacterArt.atlasWidth)
+ Assert.AreEqual<int>(5120, VillageArt.characterAtlasWidth)
+ let stride = VillageArt.characterAtlasWidth / 32 / (5 * 4)
+ Assert.AreEqual<int>(8, stride)
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index 61fda7d..68e5b93 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -326,7 +326,7 @@ type PrototypeTests () =
Assert.IsTrue(first.PanelY + first.PanelHeight <= 720)
[<TestMethod>]
- member _.NpcVariantsAreStableAndAnimationCoversFourDirectionsAndFourFrames () =
+ member _.NpcVariantsAreStableAndAnimationCoversFourDirectionsAndSixFrames () =
let sameNpc = npcVisualVariant (NpcId 7)
let repeatedNpc = npcVisualVariant (NpcId 7)
let otherNpc = npcVisualVariant (NpcId 8)
@@ -345,7 +345,9 @@ type PrototypeTests () =
Assert.AreEqual<AnimationFrame>(FrameTwo, (VillageArt.animationFrame 4L))
Assert.AreEqual<AnimationFrame>(FrameThree, (VillageArt.animationFrame 8L))
Assert.AreEqual<AnimationFrame>(FrameFour, (VillageArt.animationFrame 12L))
- Assert.AreEqual<AnimationFrame>(FrameOne, (VillageArt.animationFrame 16L))
+ Assert.AreEqual<AnimationFrame>(FrameFive, (VillageArt.animationFrame 16L))
+ Assert.AreEqual<AnimationFrame>(FrameSix, (VillageArt.animationFrame 20L))
+ Assert.AreEqual<AnimationFrame>(FrameOne, (VillageArt.animationFrame 24L))
[<TestMethod>]
member _.VillagersUseFourDistinctVariantsDistinctFromTheAvatar () =
@@ -363,28 +365,27 @@ type PrototypeTests () =
rect.X)
|> Set.ofList
Assert.AreEqual<int>(4, variantXes.Count)
- Assert.AreEqual<int>(3840, VillageArt.characterAtlasWidth)
+ Assert.AreEqual<int>(5120, VillageArt.characterAtlasWidth)
[<TestMethod>]
- member _.WalkAnimationCoversFourFramesDeterministically () =
- // 16-tick cycle, new frame every 4 sim ticks, no wall-clock dependency.
+ member _.WalkAnimationCoversSixFramesDeterministically () =
+ // 24-tick cycle, new frame every 4 sim ticks, no wall-clock dependency.
let frames = [ 0L; 4L; 8L; 12L; 16L; 20L ] |> List.map VillageArt.animationFrame
- Assert.AreEqual<AnimationFrame list>([ FrameOne; FrameTwo; FrameThree; FrameFour; FrameOne; FrameTwo ], frames)
+ Assert.AreEqual<AnimationFrame list>([ FrameOne; FrameTwo; FrameThree; FrameFour; FrameFive; FrameSix ], frames)
let cycleFrames =
- [ 0L; 4L; 8L; 12L ]
+ [ 0L; 4L; 8L; 12L; 16L; 20L ]
|> List.map VillageArt.animationFrame
|> List.distinct
- Assert.AreEqual<AnimationFrame list>([ FrameOne; FrameTwo; FrameThree; FrameFour ], cycleFrames)
+ Assert.AreEqual<AnimationFrame list>([ FrameOne; FrameTwo; FrameThree; FrameFour; FrameFive; FrameSix ], cycleFrames)
// Every frame maps to a distinct atlas cell column across the five variants.
- // Every frame maps to a distinct atlas cell column across the five variants.
let perFrameX =
- [ FrameOne; FrameTwo; FrameThree; FrameFour ]
+ [ FrameOne; FrameTwo; FrameThree; FrameFour; FrameFive; FrameSix ]
|> List.map (fun frame ->
let rect = characterSourceRectangle { Variant = IndigoRobe; Direction = NorthFacing; Frame = WalkFrame frame }
rect.X / 32)
- Assert.AreEqual<int list>([ 0; 1; 2; 3 ], perFrameX)
+ Assert.AreEqual<int list>([ 0; 1; 2; 3; 4; 5 ], perFrameX)
// Idle breath poses occupy the two cells after the walk cycle for each (variant, direction).
let idleXes =
@@ -392,7 +393,7 @@ type PrototypeTests () =
|> List.map (fun frame ->
let rect = characterSourceRectangle { Variant = IndigoRobe; Direction = NorthFacing; Frame = IdleFrame frame }
rect.X / 32)
- Assert.AreEqual<int list>([ 4; 5 ], idleXes)
+ Assert.AreEqual<int list>([ 6; 7 ], idleXes)
[<TestMethod>]
member _.ChineseCopyProvidesFormalSceneTimeMenuAndStatusLabels () =
@@ -533,6 +534,9 @@ type PrototypeTests () =
Assert.AreEqual<CharacterFrame>(IdleFrame IdleTwo, VillageArt.characterFrame false 24L)
Assert.AreEqual<CharacterFrame>(WalkFrame FrameOne, VillageArt.characterFrame true 0L)
Assert.AreEqual<CharacterFrame>(WalkFrame FrameTwo, VillageArt.characterFrame true 4L)
+ Assert.AreEqual<CharacterFrame>(WalkFrame FrameFive, VillageArt.characterFrame true 16L)
+ Assert.AreEqual<CharacterFrame>(WalkFrame FrameSix, VillageArt.characterFrame true 20L)
+ Assert.AreEqual<CharacterFrame>(WalkFrame FrameOne, VillageArt.characterFrame true 24L)
Assert.AreEqual<CharacterFrame>(VillageArt.characterFrame true 12L, VillageArt.characterFrame true 12L)
[<TestMethod>]
@@ -550,18 +554,18 @@ type PrototypeTests () =
Assert.IsTrue(peddler.WalkWindow < farmer.WalkWindow && farmer.WalkWindow < scholar.WalkWindow)
Assert.IsTrue(peddler.IdleWindow < farmer.IdleWindow && farmer.IdleWindow < scholar.IdleWindow)
- // Same tick, three trade-specific frames (at 12 ticks: Farmer IV, Peddler II, Scholar I).
+ // Same tick, three trade-specific frames (at 12 ticks: Farmer IV, Peddler VI, Scholar V).
let frameAt trade tick = VillageArt.animationFrameFor (VillageArt.rhythmFor trade) tick
let frames =
[ VillageArt.Farmer; VillageArt.Peddler; VillageArt.Scholar ]
|> List.map (fun trade -> frameAt trade 12L)
- Assert.AreEqual<AnimationFrame list>([ FrameFour; FrameTwo; FrameOne ], frames)
+ Assert.AreEqual<AnimationFrame list>([ FrameFour; FrameSix; FrameFive ], frames)
Assert.AreEqual<VillageArt.WalkRhythm>(VillageArt.rhythmFor VillageArt.Scholar, VillageArt.rhythmFor VillageArt.Scholar)
// npcSpriteSpec now follows the NPC's own trade cadence.
Assert.AreEqual<CharacterFrame>(WalkFrame FrameFour, (VillageArt.npcSpriteSpec (NpcId 0) VillageArt.SouthFacing true 12L).Frame)
- Assert.AreEqual<CharacterFrame>(WalkFrame FrameTwo, (VillageArt.npcSpriteSpec (NpcId 1) VillageArt.SouthFacing true 12L).Frame)
- Assert.AreEqual<CharacterFrame>(WalkFrame FrameOne, (VillageArt.npcSpriteSpec (NpcId 2) VillageArt.SouthFacing true 12L).Frame)
+ Assert.AreEqual<CharacterFrame>(WalkFrame FrameSix, (VillageArt.npcSpriteSpec (NpcId 1) VillageArt.SouthFacing true 12L).Frame)
+ Assert.AreEqual<CharacterFrame>(WalkFrame FrameFive, (VillageArt.npcSpriteSpec (NpcId 2) VillageArt.SouthFacing true 12L).Frame)
[<TestMethod>]
member _.ChimneySmokeFramesAreDeterministicAndThreePhase () =
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