summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/Game.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 20:08:02 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 20:08:02 +0800
commite55edb080153943f396f8b324b9412c540707b24 (patch)
tree5181ccba94d773ab697539bc479cee220ea3a0fd /src/LivingVillage.Desktop/Game.fs
parent8d339b4fb8e4f5d7710b99afb1e4b54341a8a72b (diff)
downloadliving-village-e55edb080153943f396f8b324b9412c540707b24.tar.gz
p46: 菜单/启动环境动效 — 逐格涟漪(复用waterFrameTickAt)+灯笼呼吸+屋脊上双视差云影, 全纯函数可复现
Diffstat (limited to 'src/LivingVillage.Desktop/Game.fs')
-rw-r--r--src/LivingVillage.Desktop/Game.fs89
1 files changed, 81 insertions, 8 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 3e39b09..578be72 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -321,6 +321,16 @@ type LivingVillageGame() as this =
let mutable p45Step = 0
let mutable p45PendingName = ""
let mutable p45Pending = false
+ // P46 证据钩子:LV_P46_SHOT=1 时拍 splash 涟漪中帧 + 云影两帧 + 菜单灯笼呼吸两相位,
+ // 全程走正常启动/菜单流程,仅按帧表落盘。
+ let p46ShotMode = Environment.GetEnvironmentVariable("LV_P46_SHOT") = "1"
+ let mutable p46PendingName = ""
+ let mutable p46Pending = false
+ let mutable p46SplashRippleDone = false
+ let mutable p46CloudEarlyDone = false
+ let mutable p46CloudLateDone = false
+ let mutable p46MenuADone = false
+ let mutable p46MenuBDone = false
let mutable autoplayFrames = 0
let mutable flowStep = 0
let mutable flowHold = 0
@@ -954,6 +964,45 @@ type LivingVillageGame() as this =
(sprintf "%A" spec.Frame)
p45Step <- p45Step + 1
+ /// P46 证据:Draw 末尾落盘。splash 阶段按帧阈值拍涟漪/云影,菜单阶段拍灯笼两相位。
+ member private this.CaptureP46Shot () =
+ let save (name: string) =
+ System.IO.Directory.CreateDirectory recordDirectory |> ignore
+ this.SaveBackBuffer(sprintf "%s/%s.png" recordDirectory name)
+ printfn "p46-shot=%s splashFrame=%d menuEntranceFrame=%d" name splashFrame menuEntranceFrame
+ if splashActive then
+ if not p46SplashRippleDone && splashFrame >= P46ShotScript.splashRippleFrame then
+ p46SplashRippleDone <- true
+ save P46ShotScript.splashRippleName
+ elif not p46CloudEarlyDone && splashFrame >= P46ShotScript.cloudEarlyFrame then
+ p46CloudEarlyDone <- true
+ save P46ShotScript.cloudEarlyName
+ elif not p46CloudLateDone && splashFrame >= P46ShotScript.cloudLateFrame then
+ p46CloudLateDone <- true
+ save P46ShotScript.cloudLateName
+ elif p46Pending then
+ p46Pending <- false
+ if p46PendingName = P46ShotScript.menuPeakName then p46MenuADone <- true
+ else p46MenuBDone <- true
+ save p46PendingName
+
+ /// P46 证据:菜单阶段按帧表把 menuEntranceFrame 钉到灯笼呼吸的波峰/波谷,拍两相位。
+ member private this.PrepareP46Shot () =
+ if p46MenuADone && p46MenuBDone then
+ printfn "p46-shot=done"
+ this.Exit()
+ elif not p46MenuADone then
+ // 每帧钉住目标帧,抵消 Update 末尾的自增,保证 Draw 看到精确相位。
+ menuEntranceFrame <- P46ShotScript.menuPeakFrame
+ if not p46Pending then
+ p46PendingName <- P46ShotScript.menuPeakName
+ p46Pending <- true
+ elif not p46MenuBDone then
+ menuEntranceFrame <- P46ShotScript.menuTroughFrame
+ if not p46Pending then
+ p46PendingName <- P46ShotScript.menuTroughName
+ p46Pending <- true
+
/// P20 第三步取证:真走位巡游(Sim.step 驱动)沿桥/路/民居门移动,用于桥面行走帧与昼夜录制。
member private this.RunRiverscapeTour () =
if not riverscapeTourWatchStarted then
@@ -1268,6 +1317,17 @@ type LivingVillageGame() as this =
let pressedAny (keys: Keys list) = keys |> List.exists pressed
if splashActive then
splashFrame <- splashFrame + 1L
+ if p46ShotMode && splashFrame >= 10L then
+ // P46 取证:等画面绘制稳定后,把 splashFrame 钉到下一个未取证的目标帧,
+ // 令 Draw 看到精确帧号(前若干帧 backbuffer 未铺满,故需 warm-up 门槛)。
+ let target =
+ if not p46SplashRippleDone then Some P46ShotScript.splashRippleFrame
+ elif not p46CloudEarlyDone then Some P46ShotScript.cloudEarlyFrame
+ elif not p46CloudLateDone then Some P46ShotScript.cloudLateFrame
+ else None
+ match target with
+ | Some t -> splashFrame <- t
+ | None -> ()
let skip = LaunchScreen.skipRequested (pressed Keys.Escape) (pressed Keys.Space)
if skip || LaunchScreen.isComplete splashFrame then
splashActive <- false
@@ -1322,6 +1382,9 @@ type LivingVillageGame() as this =
else this.DispatchMenuInput Confirm
elif menu.Page = Playing then
this.PrepareP45Shot()
+ elif p46ShotMode then
+ if menu.Page = MainMenu then
+ this.PrepareP46Shot()
elif menu.Page = Playing then
this.UpdatePlaying gameTime kb pressed pressedAny
elif menuShotMode then
@@ -1731,6 +1794,8 @@ type LivingVillageGame() as this =
this.CaptureP44Shot()
if p45ShotMode && menu.Page = Playing then
this.CaptureP45Shot()
+ if p46ShotMode then
+ this.CaptureP46Shot()
/// 启动画面:分层天色 + 屋脊/水面剪影 + 标题淡入上浮 + 跳过提示,全部为帧计数纯函数。
member private this.DrawSplash() =
@@ -1751,6 +1816,14 @@ type LivingVillageGame() as this =
let y1 = if index = bandCount - 1 then height else y0 + bandHeight
spriteBatch.Draw(pixel, Rectangle(0, y0, width, y1 - y0), Color(band.R, band.G, band.B))) layout.SkyBands
+ // ③ 屋脊上方的双层视差云影:纯水平位移(随帧缓慢漂移),低 alpha 预乘柔光,
+ // 只用椭圆行落成,绝不留大黑块/实心色块。
+ for layer in 0 .. MenuAmbience.cloudLayerCount - 1 do
+ let cloudColor = VillageArt.premultiply 214 228 236 (MenuAmbience.cloudLayerAlpha layer)
+ for (cx, cy, rx, ry) in MenuAmbience.cloudPuffs layer splashFrame width do
+ for (runX, runY, runW) in VillageArt.ellipseRuns cx cy rx ry do
+ spriteBatch.Draw(pixel, Rectangle(runX, runY, runW, 1), cloudColor)
+
// 屋脊剪影随暗幕退场而上浮就位。
let drop = int (28.0f * veil)
for x in 0 .. Sim.tilePixels .. width - 1 do
@@ -1765,22 +1838,22 @@ type LivingVillageGame() as this =
spriteBatch.Draw(artTextures.TileAtlas, Rectangle(x, row, Sim.tilePixels, Sim.tilePixels), VillageArt.atlasTileRectangle TitleScreen.waterSlot, waterColor)
for x in 0 .. Sim.tilePixels .. width - 1 do
let column = x / Sim.tilePixels
- let brightness = TitleScreen.waterRippleBrightness splashFrame column rowIndex
+ let brightness = MenuAmbience.waterColumnBrightness splashFrame column rowIndex
let crestColor =
Color(
min 255 (int (150.0f * brightness)),
min 255 (int (170.0f * brightness)),
min 255 (int (178.0f * brightness)),
waterAlpha)
- let lift = TitleScreen.waterRippleLift splashFrame column rowIndex
+ let lift = MenuAmbience.waterColumnLift splashFrame column rowIndex
spriteBatch.Draw(artTextures.TileAtlas, Rectangle(x, row + lift, Sim.tilePixels, Sim.tilePixels), VillageArt.atlasTileRectangle TitleScreen.waterSlot, crestColor)
- if TitleScreen.waterRippleCrest splashFrame column rowIndex then
+ if MenuAmbience.waterColumnCrest splashFrame column rowIndex then
spriteBatch.Draw(pixel, Rectangle(x, row + lift, Sim.tilePixels, 3), Color(225, 240, 245, waterAlpha))
// P42 屋檐灯笼呼吸光晕:复用 P14 预乘 alpha 径向路径,随帧与灯序号呼吸。
let lanternColor, _ = TitleScreen.lanternColors
for (lanternIndex, (lx, ly)) in List.indexed layout.Lanterns do
- let breath = TitleScreen.lanternBreathAlpha splashFrame lanternIndex
+ let breath = MenuAmbience.lanternBreathAlpha splashFrame lanternIndex
for (rect, color) in VillageArt.lanternGlowLayers lx ly 28 6 252 216 148 breath do
spriteBatch.Draw(pixel, rect, color)
spriteBatch.Draw(pixel, Rectangle(lx - 3, ly - 5, 8, 10), Color(lanternColor.R, lanternColor.G, lanternColor.B))
@@ -1875,22 +1948,22 @@ type LivingVillageGame() as this =
spriteBatch.Draw(artTextures.TileAtlas, Rectangle(x, row, Sim.tilePixels, Sim.tilePixels), VillageArt.atlasTileRectangle TitleScreen.waterSlot, waterColor)
for x in 0 .. Sim.tilePixels .. width - 1 do
let column = x / Sim.tilePixels
- let brightness = TitleScreen.waterRippleBrightness menuEntranceFrame column rowIndex
+ let brightness = MenuAmbience.waterColumnBrightness menuEntranceFrame column rowIndex
let crestColor =
Color(
min 255 (int (150.0f * brightness)),
min 255 (int (170.0f * brightness)),
min 255 (int (178.0f * brightness)),
waterAlpha)
- let lift = TitleScreen.waterRippleLift menuEntranceFrame column rowIndex
+ let lift = MenuAmbience.waterColumnLift menuEntranceFrame column rowIndex
spriteBatch.Draw(artTextures.TileAtlas, Rectangle(x, row + lift, Sim.tilePixels, Sim.tilePixels), VillageArt.atlasTileRectangle TitleScreen.waterSlot, crestColor)
- if TitleScreen.waterRippleCrest menuEntranceFrame column rowIndex then
+ if MenuAmbience.waterColumnCrest menuEntranceFrame column rowIndex then
spriteBatch.Draw(pixel, Rectangle(x, row + lift, Sim.tilePixels, 3), Color(225, 240, 245, waterAlpha))
// P42 bank lanterns breathe through the shared P14 premultiplied-alpha radial path.
let lanternColor, _ = TitleScreen.lanternColors
for (lanternIndex, (lx, ly)) in List.indexed layout.Lanterns do
- let breath = TitleScreen.lanternBreathAlpha menuEntranceFrame lanternIndex
+ let breath = MenuAmbience.lanternBreathAlpha menuEntranceFrame lanternIndex
for (rect, color) in VillageArt.lanternGlowLayers lx ly 28 6 252 216 148 breath do
spriteBatch.Draw(pixel, rect, color)
spriteBatch.Draw(pixel, Rectangle(lx - 3, ly - 5, 8, 10), Color(lanternColor.R, lanternColor.G, lanternColor.B))