summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 00:50:30 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 00:50:30 +0800
commit8fad58bd62155da653251ab7457cd54ebba0b279 (patch)
treea425dc6bf963fe614aeb28cc80823d7c69b76904
parente9d3e224e802ec41b9f565f5e8ed7eec8470a310 (diff)
downloadliving-village-8fad58bd62155da653251ab7457cd54ebba0b279.tar.gz
test(desktop): P17 取证钩子(LV_AUTOPLAY_START_HOUR 纯函数起时)+ 证据与 docs 记录
-rw-r--r--docs/维护说明.md33
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs4
-rw-r--r--src/LivingVillage.Desktop/Game.fs15
-rw-r--r--src/LivingVillage.Desktop/M6Presentation.fs4
4 files changed, 55 insertions, 1 deletions
diff --git a/docs/维护说明.md b/docs/维护说明.md
index ad68e5b..024248f 100644
--- a/docs/维护说明.md
+++ b/docs/维护说明.md
@@ -173,6 +173,28 @@ dotnet src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll
- 门槛:clean rebuild 0 警告 0 错误;`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
一致、`performance_determinism=PASS`。
+## P17 批验:场景细节 / 人物动效 / 昼夜光照精修
+
+- ①街边中式细节物:`scripts/make-jiangnan-art.py` 世界图集由 25 格扩到 28 格(新增 slot 25 石灯笼 /
+ 26 水缸 / 27 摊贩摊位,全部烘焙进 `Assets/jiangnan-world.png`,尺寸 800x32→896x32),游戏侧只做
+ `tileSourceRectangle` 贴片、无程序几何。`VillageArt` 新增 `RenderPropKind.StoneLantern/WaterVat/
+ MarketStall` 与 `worldTileCount=28`;`sampleRenderPlan` 沿石路摆 4 件(石灯笼 4,14 / 16,14,水缸
+ 12,11,摊位 3,11)。图集脚本二次运行 byte-identical(sha256 一致)。
+- ②村民走路节奏差异化:`VillageArt` 新增 `NpcTrade`(Farmer/Peddler/Scholar,按 `NpcId mod 3` 纯函数
+ 分配)与 `WalkRhythm`(走路/待机窗口 + 相位)。农夫 4/24、货郎 3/20(更快)、书生 6/36(更慢);
+ `npcSpriteSpec` 改走各自节奏,`animationFrame/idleFrame/characterFrame` 保持农夫节奏即旧语义。
+- ③昼夜光照精修:`M6Presentation.lightingBlend` 过渡窗口参数化(清晨 4:00→6:30、黄昏 19:30→22:00)
+ 并把小时改分数(原为整数小时,过渡只有 3 档),新增纯函数 `transitionWarmth`(晨昏中点火暖色峰值
+ 1,正午/午夜 0)驱动 `backgroundAtWarmth/worldTintAtWarmth`,`M6RenderProfile` 增 `SkyWarmth`。
+- ④回归:更新/新增确定性测试(行走节奏、光照锚点与暖色、图集 28 格与 PNG 尺寸、证据起时纯函数);
+ Desktop 107 / Kernel 89 全绿(未触碰 Kernel/Sim.fs 数值路径)。
+- ⑤证据:`/tmp/opencode/lv-p17-art/`(`p17-day-0{1,2}.png`、`p17-dusk-0{1,2}.png`、`p17-night-0{1,2}.png`、
+ `p17-diff-before-dawn.png` vs `p17-diff-after-dawn.png`(同一 6:00 tick,P17 前/后各 1 帧)、`sample.mp4`
+ 13.1 s);`check-no-white-blocks.py` 对所选 6 帧 0 命中。起时用新增环境钩子 `LV_AUTOPLAY_START_HOUR`
+ (纯函数 `M6Presentation.startTickForHour`,桌面演示专用,不进 Kernel)。
+- 门槛:clean rebuild 0 警告 0 错误;`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
+ 一致、`performance_determinism=PASS`。
+
## 已验证命令
```bash
@@ -205,6 +227,15 @@ timeout 90 xvfb-run -a -s "-screen 0 1280x720x24" env \
LV_AUTOPLAY_MENU_SHOT=1 LV_RECORD_DIR=/tmp/opencode/lv-p16-art \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
+# P17 取证(街边细节/人物节奏/昼夜精修;LV_AUTOPLAY_START_HOUR 定起时:昼 12 / 黄昏 20.75 / 夜 0)
+for spec in "day 12.0" "dusk 20.75" "night 0.0"; do set -- $spec; \
+ timeout 100 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
+ LV_AUTOPLAY_START_HOUR=$2 LV_AUTOPLAY_RECORD=1 \
+ LV_RECORD_DIR=/tmp/opencode/lv-p17-art/$1 LV_RECORD_EVERY=2 \
+ dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done
+# 街边细节图集再生成(确定性,二次运行 byte-identical)
+python3 scripts/make-jiangnan-art.py
+
# P13 动画证据(确定性接触表 + 真实运行帧)
python3 scripts/make-animation-evidence.py # -> evidence/walk-idle-frames.png 等
# 真实运行帧(白天 / 夜晚各一次;record 目录与间隔可用环境变量覆盖)
@@ -235,6 +266,8 @@ python3 scripts/check-no-white-blocks.py evidence/night-run
- `/tmp/opencode/lv-p13/evidence/`(P13:`walk-idle-frames.png`、`water-3phase.png`、
`smoke-3phase.png`、`water-2frame.png`、`smoke-2frame.png`、`day-panorama.png`、
`night-lantern.png`、`day-night-compare.png`、`walk-real-run.png`、`day-run/`、`night-run/`)
+- `/tmp/opencode/lv-p17-art/`(P17:`p17-day-0{1,2}.png`、`p17-dusk-0{1,2}.png`、`p17-night-0{1,2}.png`、
+ `p17-diff-before-dawn.png`/`p17-diff-after-dawn.png`、`sample.mp4`;同一份拷入 `evidence/`)
- `/tmp/opencode/lv-p16-art/`(P16:`frame-day-0{1,2,3}.png`、`frame-night-0{1,2,3}.png`、
`frame-splash-01.png`、`frame-menu-0{1,2}.png`、`hud-bar-day.png`、`hud-bar-night.png`、
`sample.mp4`、`final_verification.txt`;沙箱不允许 `/tmp/lv-p16-art`,故落此路径)
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index 5a153b1..4d2c895 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -897,6 +897,10 @@ type PrototypeTests () =
Assert.AreEqual<float32>(0.0f, (atHour 13).LanternGlow)
Assert.AreEqual<float32>(1.0f, (atHour 0).LanternGlow)
+ // Evidence hook: fractional start hours map exactly onto sim ticks.
+ Assert.AreEqual<int64>(int64 20 * ticksPerHour + ticksPerHour * 3L / 4L, M6Presentation.startTickForHour 20.75)
+ Assert.AreEqual<int64>(6L * ticksPerHour, M6Presentation.startTickForHour 6.0)
+
[<TestMethod>]
member _.TitleEntranceProgressIsDeterministicAndCompleteAfterTwoSeconds () =
// 120 frames at the fixed 60Hz step = 2s entrance, pure function of frame count.
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 3f3a1d8..4760d4e 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -126,8 +126,21 @@ type LivingVillageGame() as this =
let mutable cjkAtlas = Unchecked.defaultof<Texture2D>
let mutable pixel = Unchecked.defaultof<Texture2D>
let renderPlan = VillageArt.sampleRenderPlan ()
+ // Evidence hook: pin the starting wall-clock hour (e.g. 20.75 for 黄昏), desktop-only.
+ let startHourOverride =
+ match Environment.GetEnvironmentVariable("LV_AUTOPLAY_START_HOUR") with
+ | null | "" -> None
+ | value ->
+ match Double.TryParse(value, Globalization.NumberStyles.Float, Globalization.CultureInfo.InvariantCulture) with
+ | true, hour -> Some hour
+ | _ -> None
let createInitialWorld () =
- WorldBootstrap.initialWorldForDaylight daylightAutoplay 42UL Sim.npcCount
+ let baseWorld = WorldBootstrap.initialWorldForDaylight daylightAutoplay 42UL Sim.npcCount
+ match startHourOverride with
+ | Some hour ->
+ let tick = M6Presentation.startTickForHour hour
+ { baseWorld with Tick = tick; Time = float tick * Sim.dtSeconds }
+ | None -> baseWorld
let mutable world = createInitialWorld ()
let mutable camera = Vector2.Zero
let mutable avatarFacing = VillageArt.SouthFacing
diff --git a/src/LivingVillage.Desktop/M6Presentation.fs b/src/LivingVillage.Desktop/M6Presentation.fs
index 11399c0..a64d494 100644
--- a/src/LivingVillage.Desktop/M6Presentation.fs
+++ b/src/LivingVillage.Desktop/M6Presentation.fs
@@ -100,6 +100,10 @@ module M6Presentation =
/// 倍速标签(语义别名,HUD 用)。
let speedLabel (control: SimulationControl) : string = clockLabel control
+ /// Deterministic start tick for a wall-clock hour (evidence harness): 20.75 -> 20:45.
+ let startTickForHour (hour: float) : int64 =
+ int64 (hour * float (Sim.ticksPerDay / 24L))
+
let private hourOf (tick: int64) : int =
int ((tick / (Sim.ticksPerDay / 24L)) % 24L)