summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop')
-rw-r--r--src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.pngbin63292 -> 80906 bytes
-rw-r--r--src/LivingVillage.Desktop/Assets/cjk-glyphs.txt69
-rw-r--r--src/LivingVillage.Desktop/ChineseText.fs36
-rw-r--r--src/LivingVillage.Desktop/CjkGlyphAtlas.fs2
-rw-r--r--src/LivingVillage.Desktop/Game.fs91
-rw-r--r--src/LivingVillage.Desktop/Interaction.fs42
6 files changed, 230 insertions, 10 deletions
diff --git a/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png b/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png
index af83cd7..25d317b 100644
--- a/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png
+++ b/src/LivingVillage.Desktop/Assets/cjk-glyph-atlas.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt b/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt
index 701200b..1b9bf67 100644
--- a/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt
+++ b/src/LivingVillage.Desktop/Assets/cjk-glyphs.txt
@@ -1,13 +1,18 @@
×
一
+上
不
+与
业
中
+为
主
乐
乡
书
买
+了
+争
事
于
互
@@ -15,36 +20,53 @@
人
今
从
+以
+件
价
任
+众
+会
传
+位
体
作
使
保
+信
倍
停
+先
入
关
内
+册
+再
农
准
+几
出
+分
+切
到
前
+剧
力
功
务
动
助
+匀
单
卖
南
原
+去
友
取
受
+只
可
+各
同
和
回
@@ -52,8 +74,10 @@
圃
土
在
+坏
墙
备
+多
夜
大
天
@@ -61,23 +85,33 @@
失
好
始
+子
存
完
+定
家
察
对
+将
+小
+尚
就
层
居
屋
工
+差
+己
已
币
帮
+常
+平
年
庄
库
应
+庙
度
开
式
@@ -91,15 +125,19 @@
怒
总
恼
+情
+愁
意
戏
成
或
+户
打
找
报
拒
择
+指
挑
捕
据
@@ -108,6 +146,7 @@
播
操
收
+改
效
敌
数
@@ -115,11 +154,13 @@
方
无
日
+旧
时
明
昏
易
是
+晓
晚
晨
景
@@ -127,9 +168,12 @@
更
最
有
+未
村
+束
板
林
+果
标
格
档
@@ -142,23 +186,31 @@
江
没
法
+淡
清
渔
游
+热
+照
物
状
理
瓦
生
用
+田
留
白
+的
目
+看
+真
石
确
社
离
移
+稳
空
竹
笑
@@ -166,6 +218,10 @@
精
系
紧
+纭
+纷
+线
+结
给
绝
继
@@ -173,9 +229,11 @@
续
置
翻
+耕
聊
职
能
+自
范
获
菜
@@ -189,6 +247,8 @@
讲
论
设
+访
+试
话
询
说
@@ -198,8 +258,11 @@
谈
败
货
+贩
资
+走
足
+路
跳
转
过
@@ -213,17 +276,23 @@
选
速
道
+邻
郎
+都
+里
量
金
鉴
钱
门
闭
+问
+闯
闲
间
闻
附
+雨
零
需
面
diff --git a/src/LivingVillage.Desktop/ChineseText.fs b/src/LivingVillage.Desktop/ChineseText.fs
index 52e124d..9789ae7 100644
--- a/src/LivingVillage.Desktop/ChineseText.fs
+++ b/src/LivingVillage.Desktop/ChineseText.fs
@@ -217,7 +217,41 @@ module ChineseText =
"收货"
"转卖"
"观察记事"
- "论理" ]
+ "论理"
+ "剧情线"
+ "雨水与年成"
+ "水位与收成"
+ "新货的路子"
+ "庙前的争论"
+ "未开始"
+ "已接任务"
+ "走访中"
+ "已收束"
+ "耕田"
+ "打渔"
+ "贩货"
+ "读书"
+ "以耕田为生"
+ "以打渔为生"
+ "以贩货为生"
+ "以读书为生"
+ "接了件事:雨水与年成,得去村里多问问"
+ "接了件事:水位与收成,得去村里多问问"
+ "接了件事:新货的路子,得去村里多问问"
+ "接了件事:庙前的争论,得去村里多问问"
+ "走访了几户人家,改日再问"
+ "雨水与年成有了结果:乡邻都说雨水匀,今年差不了"
+ "雨水与年成有了结果:雨水尚可,年成将就,照旧耕作"
+ "雨水与年成有了结果:问不出准信,年成只能看天"
+ "水位与收成有了结果:乡邻说得真切,水位稳了,收成坏不了"
+ "水位与收成有了结果:众说平常,水位尚可,收成将就"
+ "水位与收成有了结果:问了几家都淡淡的,水情还得自己看"
+ "新货的路子有了结果:乡邻热心指路,新货不愁卖"
+ "新货的路子有了结果:说法平常,先小量试卖"
+ "新货的路子有了结果:各家都淡淡的,路子还得自己闯"
+ "庙前的争论有了结果:各说各理,事有分晓"
+ "庙前的争论有了结果:众说纷纭,暂无定论"
+ "庙前的争论有了结果:无人理会,先记在册上" ]
let layoutText (maxWidth: int) (supported: Set<char>) (text: string) : TextLayout =
let width = max 1 maxWidth
diff --git a/src/LivingVillage.Desktop/CjkGlyphAtlas.fs b/src/LivingVillage.Desktop/CjkGlyphAtlas.fs
index b6e6947..01d6a59 100644
--- a/src/LivingVillage.Desktop/CjkGlyphAtlas.fs
+++ b/src/LivingVillage.Desktop/CjkGlyphAtlas.fs
@@ -11,7 +11,7 @@ module CjkGlyphAtlas =
// Keep this table sorted and stable so the generated PNG has a reproducible layout.
let private characterTable =
- "×一不业中主乐乡书买事于互交人今从价任传体作使保倍停入关内农准出到前力功务动助单卖南原友取受可同和回围圃土在墙备夜大天夫失好始存完家察对就层居屋工已币帮年庄库应度开式当录待得心必态怒总恼意戏成或打找报拒择挑捕据接提播操收效敌数新方无日时明昏易是晚晨景暂更最有村板林标格档桥歉段民水求江没法清渔游物状理瓦生用留白目石确社离移空竹笑粮精系紧给绝继绪续置翻聊职能范获菜衅行观认讨议记讲论设话询说请读谅谈败货资足跳转过近返还进迫退送选速道郎量金鉴钱门闭闲间闻附零需面项须食饥饿黄黛,:"
+ "×一上不与业中为主乐乡书买了争事于互交人今从以件价任众会传位体作使保信倍停先入关内册再农准几出分切到前剧力功务动助匀单卖南原去友取受只可各同和回围圃土在坏墙备多夜大天夫失好始子存完定家察对将小尚就层居屋工差己已币帮常平年庄库应庙度开式当录待得心必态怒总恼情愁意戏成或户打找报拒择指挑捕据接提播操收改效敌数新方无日旧时明昏易是晓晚晨景暂更最有未村束板林果标格档桥歉段民水求江没法淡清渔游热照物状理瓦生用田留白的目看真石确社离移稳空竹笑粮精系紧纭纷线结给绝继绪续置翻耕聊职能自范获菜衅行观认讨议记讲论设访试话询说请读谅谈败货贩资走足路跳转过近返还进迫退送选速道邻郎都里量金鉴钱门闭问闯闲间闻附雨零需面项须食饥饿黄黛,:"
let characters = characterTable.ToCharArray()
let rowCount = (characters.Length + columns - 1) / columns
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 3a9a004..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
@@ -383,6 +400,16 @@ type LivingVillageGame() as this =
world <-
initialWorldForMode state
|> withStartTick
+ // §3 身份条目:带职业开局即写入年鉴,形成个人传记线
+ match state with
+ | Some occupationState ->
+ world <-
+ Sim.appendAnnal
+ { Tick = world.Tick
+ Kind = StoryAnnal
+ Summary = Occupation.Story.identitySummaryOf occupationState.Profile.Kind }
+ world
+ | None -> ()
this.ResetWorldView()
m5View <- { m5View with Task = state }
menu <- menu |> MenuState.setCurrentWorld true |> MenuState.enterGame
@@ -740,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))
@@ -752,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
@@ -1117,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() =
@@ -1264,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)
diff --git a/src/LivingVillage.Desktop/Interaction.fs b/src/LivingVillage.Desktop/Interaction.fs
index dd372ef..9aceff6 100644
--- a/src/LivingVillage.Desktop/Interaction.fs
+++ b/src/LivingVillage.Desktop/Interaction.fs
@@ -49,7 +49,9 @@ type M5View =
HomeMode: HomeMode
Prompt: string option
PromptTargetPos: Vec2 option
- Task: Occupation.State option }
+ Task: Occupation.State option
+ /// 世界种子(与 Game.fs 的开局种子同源),供剧情线开线日哈希使用。
+ Seed: uint64 }
module InteractionResolver =
@@ -211,7 +213,8 @@ module M5Interaction =
HomeMode = Outside
Prompt = None
PromptTargetPos = None
- Task = None }
+ Task = None
+ Seed = 42UL }
let worldInputAllowed (view: M5View) : bool =
view.Panel = WorldPanel
@@ -299,17 +302,37 @@ module M5Interaction =
match List.tryItem index menu.Options with
| None -> world, { view with Status = "invalid dialogue option" }
| Some intent ->
- match Sim.chooseDialogue menu.Target intent world with
+ // P-d:回应经职业偏差纯函数;无职业走既有 identity 路径(逐字节不变)。
+ let respond =
+ match view.Task with
+ | Some task -> Occupation.biasResponse (Some task.Profile.Kind)
+ | None -> Sim.responseFor
+ match Sim.chooseDialogueWith respond menu.Target intent world with
| DialogueSucceeded(outcome, next) ->
- next,
+ // P-d 剧情线:每次玩家对话至多推进一段,摘要写年鉴(StoryAnnal)。
+ let storyWorld, storyTask =
+ match view.Task with
+ | Some task ->
+ let nextState, summary = Occupation.Story.onDialogue view.Seed task next
+ let withAnnal =
+ match summary with
+ | Some text ->
+ Sim.appendAnnal
+ { Tick = next.Tick; Kind = StoryAnnal; Summary = text }
+ next
+ | None -> next
+ withAnnal, Some nextState
+ | None -> next, None
+ storyWorld,
{ view with
Panel = WorldPanel
Menu = None
- Chronicle = Sim.annalText next
+ Chronicle = Sim.annalText storyWorld
Prompt = None
PromptTargetPos = None
+ Task = storyTask
Status = sprintf "dialogue intent=%A response=%A" outcome.Intent outcome.Response
- StatusTick = Some next.Tick }
+ StatusTick = Some storyWorld.Tick }
| DialogueRejected(failure, next) ->
next,
{ view with
@@ -536,6 +559,7 @@ module M5Interaction =
quantity
unitPrice
| _ -> "交易记录"
+ | StoryAnnal -> entry.Summary
let private chronicleLines (world: World) : string list =
world.Annals
@@ -608,4 +632,8 @@ module M5Interaction =
| None -> [ "今日暂无任务" ]
| Some task ->
[ sprintf "%s:%s" (Occupation.taskNameOf task.TemplateId) (taskStateText task.State) ]
- Occupation.nameOf occupationState.Profile.Kind :: taskLines @ [ status ]
+ let storyLine =
+ sprintf "剧情线:%s,%s"
+ (Occupation.Story.lineNameOf occupationState.Profile.Kind)
+ (Occupation.Story.stageNameOf occupationState.StoryStage)
+ Occupation.nameOf occupationState.Profile.Kind :: taskLines @ [ storyLine ] @ [ status ]