summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/Game.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop/Game.fs')
-rw-r--r--src/LivingVillage.Desktop/Game.fs81
1 files changed, 80 insertions, 1 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index ea5272b..c68cf6a 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -256,6 +256,23 @@ type LivingVillageGame() as this =
let mutable splashFrame = 0L
let mutable menuEntranceFrame = 0L
let mutable menuShotFrame = 0L
+ // P31 证据钩子:LV_STORY_SHOT=1 时以指定职业开局、经真实对话管线推进剧情线后
+ // 打开任务面板截图,拍完即退出。LV_STORY_OCCUPATION=farmer|fisher|peddler|scholar,
+ // LV_STORY_STAGE=0..3(默认 2),输出到 LV_RECORD_DIR/p31-story-<职业>-stage<N>.png。
+ let storyShotMode = Environment.GetEnvironmentVariable("LV_STORY_SHOT") = "1"
+ let storyShotKind =
+ match Environment.GetEnvironmentVariable("LV_STORY_OCCUPATION") with
+ | "fisher" -> Occupation.Fisher
+ | "peddler" -> Occupation.Peddler
+ | "scholar" -> Occupation.Scholar
+ | _ -> Occupation.Farmer
+ let storyShotStage =
+ match Environment.GetEnvironmentVariable("LV_STORY_STAGE") with
+ | null | "" -> 2
+ | v -> match Int32.TryParse v with (true, value) -> max 0 (min 3 value) | _ -> 2
+ let mutable storyShotPrepared = false
+ let mutable storyShotFrame = 0L
+ let mutable storyShotSaved = false
let mutable autoplayFrames = 0
let mutable flowStep = 0
let mutable flowHold = 0
@@ -750,6 +767,44 @@ type LivingVillageGame() as this =
if not (isNull pixel) then pixel.Dispose()
if not (isNull spriteBatch) then spriteBatch.Dispose()
+ /// P31 证据:以真实开局函数建世界,用真实对话管线(bias + onDialogue)推进剧情线,
+ /// 再打开任务面板——与玩家操作走同一套 Kernel 纯函数,仅脚本化输入。
+ member private this.RunStoryShotSetup () =
+ let seed = 42UL
+ let kind = storyShotKind
+ let stage = storyShotStage
+ let lineStartDay = Occupation.Story.startDayOf seed kind + int64 (max 0 (stage - 1))
+ let startTick = lineStartDay * Sim.ticksPerDay + 12L * Sim.ticksPerDay / 24L
+ let state = WorldBootstrap.occupationStateFor seed kind startTick
+ let world0 = WorldBootstrap.initialWorldWithOccupation false seed Sim.npcCount (Some state)
+ let world1 = { world0 with Tick = startTick; Time = float startTick * Sim.dtSeconds }
+ let world2 =
+ Sim.appendAnnal
+ { Tick = startTick; Kind = StoryAnnal; Summary = Occupation.Story.identitySummaryOf kind }
+ world1
+ let mutable w = world2
+ let mutable task = state
+ for i in 0 .. stage - 1 do
+ let npc = w.Npcs.[i % w.Npcs.Length]
+ let wNear = { w with Avatar = { w.Avatar with Pos = npc.Pos } }
+ let respond = Occupation.biasResponse (Some kind)
+ match Sim.chooseDialogueWith respond npc.Id SmallTalk wNear with
+ | DialogueSucceeded (_, next) ->
+ let nextState, summary = Occupation.Story.onDialogue seed task next
+ w <-
+ match summary with
+ | Some text -> Sim.appendAnnal { Tick = next.Tick; Kind = StoryAnnal; Summary = text } next
+ | None -> next
+ task <- nextState
+ | DialogueRejected (failure, _) -> printfn "story-shot dialogue rejected: %A" failure
+ world <- w
+ m5View <- { M5Interaction.initial with Task = Some task; Seed = seed }
+ let _, view = M5Interaction.apply ShowTaskPanel world m5View
+ m5View <- view
+ menu <- menu |> MenuState.setCurrentWorld true |> MenuState.enterGame
+ printfn "story-shot=prepared occupation=%s stage=%d tick=%d annals=%d"
+ (Occupation.saveToken kind) stage world.Tick world.Annals.Length
+
override this.Update(gameTime: GameTime) =
let kb = Keyboard.GetState()
let pressed (key: Keys) = kb.IsKeyDown(key) && not (prevKb.IsKeyDown(key))
@@ -762,7 +817,10 @@ type LivingVillageGame() as this =
menuEntranceFrame <- 0L
printfn (if skip then "splash=skip frame=%d" else "splash=done frame=%d") splashFrame
else
- if autoplayFlow then
+ if storyShotMode && not storyShotPrepared then
+ storyShotPrepared <- true
+ this.RunStoryShotSetup()
+ elif autoplayFlow then
this.RunAutoplayFlow()
elif menu.Page = Playing then
this.UpdatePlaying gameTime kb pressed pressedAny
@@ -1127,6 +1185,18 @@ type LivingVillageGame() as this =
this.SaveBackBuffer(sprintf "%s/%s%04d.png" recordDirectory recordName recordIndex)
if menuShotMode then
this.CaptureMenuShot()
+ if storyShotMode && storyShotPrepared then
+ storyShotFrame <- storyShotFrame + 1L
+ if not storyShotSaved && storyShotFrame = 20L then
+ storyShotSaved <- true
+ System.IO.Directory.CreateDirectory recordDirectory |> ignore
+ this.SaveBackBuffer(
+ sprintf "%s/p31-story-%s-stage%d.png"
+ recordDirectory
+ (Occupation.saveToken storyShotKind)
+ storyShotStage)
+ printfn "story-shot=saved occupation=%s stage=%d" (Occupation.saveToken storyShotKind) storyShotStage
+ this.Exit()
/// 启动画面:分层天色 + 屋脊/水面剪影 + 标题淡入上浮 + 跳过提示,全部为帧计数纯函数。
member private this.DrawSplash() =
@@ -1274,6 +1344,15 @@ type LivingVillageGame() as this =
MenuState.occupationOptions
|> List.iteri (fun index occupation ->
drawSelection index (MenuState.occupationLabel occupation) (contentY + index * lineHeight))
+ // P-d:选中职业时提示其轻剧情线(仅最小展示,复用既有选择页机制)。
+ (match MenuState.selectedOccupation menu with
+ | Some kind ->
+ drawText
+ innerX
+ (contentY + MenuState.occupationOptions.Length * lineHeight)
+ mutedColor
+ (sprintf "剧情线:%s" (Occupation.Story.lineNameOf kind))
+ | None -> ())
drawText innerX (panelY + panelHeight - 40) mutedColor "UP DOWN 选择 ENTER 确认 ESC 返回"
| Settings ->
drawText innerX headingY mutedColor (ChineseText.menuHeading SettingsHeading)