summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Kernel/Occupation.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Kernel/Occupation.fs')
-rw-r--r--src/LivingVillage.Kernel/Occupation.fs74
1 files changed, 65 insertions, 9 deletions
diff --git a/src/LivingVillage.Kernel/Occupation.fs b/src/LivingVillage.Kernel/Occupation.fs
index cb9e25b..58b05ec 100644
--- a/src/LivingVillage.Kernel/Occupation.fs
+++ b/src/LivingVillage.Kernel/Occupation.fs
@@ -169,28 +169,72 @@ module Occupation =
state <- next
out
+ // ---- P66 任务目标具体化(design §2.4-3)----
+ // 候选集合由调用方注入(世界侧可达 NPC / 瓦片),Kernel 不感知地图/世界。
+ // 定点走 splitmix 同源的另一段流;空候选 → None(保持通配,不臆造)。
+ type TaskTargets =
+ { Npcs: Sim.NpcId list
+ Tiles: (int * int) list }
+
+ let private targetNpcSalt = 0x9E3779B97F4A7C15UL
+ let private targetTileSalt = 0xD1B54A32D192ED03UL
+
+ let private pickTargetIndex (salt: uint64) (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (count: int) : int =
+ int (taskHash (seed ^^^ salt) occupationSeed dayIndex % uint64 count)
+
+ /// 按 hash 从候选 NPC 内定点(先按 id 升序消除输入顺序影响);空集 → None(通配)。
+ let targetNpcOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (candidates: Sim.NpcId list) : Sim.NpcId option =
+ match candidates |> List.sortBy (fun (Sim.NpcId id) -> id) with
+ | [] -> None
+ | sorted -> Some sorted.[pickTargetIndex targetNpcSalt seed occupationSeed dayIndex sorted.Length]
+
+ /// 按 hash 从候选瓦片内定点(先按 (x,y) 升序消除输入顺序影响);空集 → None(通配)。
+ let targetTileOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (candidates: (int * int) list) : (int * int) option =
+ match candidates |> List.sort with
+ | [] -> None
+ | sorted -> Some sorted.[pickTargetIndex targetTileSalt seed occupationSeed dayIndex sorted.Length]
+
/// 当日任务生成:池内确定性抽 1 条;空池 → None。
- /// TargetNpc/TargetTile 首期留 None(通配),OfferedTick/DueTick 覆盖整日。
- let dailyTaskOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (kind: Kind) : DailyTask option =
+ /// 仅对既有 `satisfiesCompletion` 会读取目标的模板定点:帮工 → TargetNpc(指定村民),
+ /// 翻土 → TargetTile(自家地块);其余模板目标保持 None(不新增完成语义)。
+ /// 候选集合为空时目标为 None,落到既有通配行为。
+ let dailyTaskOfWith (targets: TaskTargets) (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (kind: Kind) : DailyTask option =
match taskPoolOf kind with
| [] -> None
| pool ->
let index = int (taskHash seed occupationSeed dayIndex % uint64 pool.Length)
+ let template = pool.[index]
+ let targetNpc =
+ match template with
+ | HelpWork -> targetNpcOf seed occupationSeed dayIndex targets.Npcs
+ | _ -> None
+ let targetTile =
+ match template with
+ | TillSoil -> targetTileOf seed occupationSeed dayIndex targets.Tiles
+ | _ -> None
let offered = dayIndex * Sim.ticksPerDay
Some
- { TemplateId = pool.[index]
- TargetNpc = None
- TargetTile = None
+ { TemplateId = template
+ TargetNpc = targetNpc
+ TargetTile = targetTile
OfferedTick = offered
DueTick = offered + Sim.ticksPerDay - 1L
State = Offered }
+ /// 历史口径:无候选集合 → 目标恒 None(通配)。既有调用点/测试逐字节不变。
+ let dailyTaskOf (seed: uint64) (occupationSeed: uint64) (dayIndex: int64) (kind: Kind) : DailyTask option =
+ dailyTaskOfWith { Npcs = []; Tiles = [] } seed occupationSeed dayIndex kind
+
/// 当日任务刷新:已持任务的 OfferedTick 仍落在 nowTick 所属日时原样保留(含
/// Done/Failed 终态,保证当日面板与存档稳定);跨日才按当日重新生成 Offered。
- let refreshToday (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (today: DailyTask option) : DailyTask option =
+ let refreshTodayWith (targets: TaskTargets) (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (today: DailyTask option) : DailyTask option =
match today with
| Some task when dayIndexOf task.OfferedTick = dayIndexOf nowTick -> Some task
- | _ -> dailyTaskOf seed occupationSeed (dayIndexOf nowTick) kind
+ | _ -> dailyTaskOfWith targets seed occupationSeed (dayIndexOf nowTick) kind
+
+ /// 历史口径:无候选集合刷新(目标恒 None)。
+ let refreshToday (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (today: DailyTask option) : DailyTask option =
+ refreshTodayWith { Npcs = []; Tiles = [] } seed occupationSeed kind nowTick today
/// 挂在玩家侧的职业状态,随存档 v3 保存(design §2)。
/// Today = 当日任务(§3);TaskToken 保留为 v3 前缀占位扩展位(恒 0,暂未启用)。
@@ -295,8 +339,16 @@ module Occupation =
/// 跨日刷新职业状态(P55):重算当日任务,并按「上一日是否已完成」保留/清零连续天数。
/// 同日刷新原样保留 Streak(含 Done 终态),避免每帧刷新把 streak 冲掉。
- let refreshState (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (state: State) : State =
- let today = refreshToday seed occupationSeed kind nowTick state.Today
+ /// P66:`targets` 提供候选集合时,跨日重算的当日任务按候选定点;空候选 → 通配(历史口径)。
+ let refreshStateWith
+ (targets: TaskTargets)
+ (seed: uint64)
+ (occupationSeed: uint64)
+ (kind: Kind)
+ (nowTick: int64)
+ (state: State)
+ : State =
+ let today = refreshTodayWith targets seed occupationSeed kind nowTick state.Today
let sameDay =
match state.Today with
| Some task -> dayIndexOf task.OfferedTick = dayIndexOf nowTick
@@ -310,6 +362,10 @@ module Occupation =
| None -> false
{ state with Today = today; Streak = (if continuing then state.Streak else 0) }
+ /// 历史口径:无候选集合刷新(目标恒 None,逐字节不变)。
+ let refreshState (seed: uint64) (occupationSeed: uint64) (kind: Kind) (nowTick: int64) (state: State) : State =
+ refreshStateWith { Npcs = []; Tiles = [] } seed occupationSeed kind nowTick state
+
// ---- 报价偏置(design §2/§3):Kernel 纯函数旁路,不改 Sim.fs 数值口径 ----
// 4 位定点整数基点(1/10000)表达偏置,避免浮点漂移;结果钳在 [0.95, 1.05]。
// 货郎:买卖双向有利(买压价/卖抬价 ±3.00%);书生:解读行情小幅加成 ±1.00%;