namespace LivingVillage.Desktop open LivingVillage.Kernel open LivingVillage.Kernel.Sim module WorldBootstrap = let private tileCenter (x: int) (y: int) : Vec2 = { X = float32 (x * Sim.tilePixels + Sim.tilePixels / 2) Y = float32 (y * Sim.tilePixels + Sim.tilePixels / 2) } let private npcSpawnTiles : (int * int) array = [| for y in [ 14; 18; 28; 32; 36; 40 ] do for x in [ 24; 28; 32; 36; 40 ] do yield x, y |] let private avatarSpawnPosition = tileCenter 31 25 let private daylightStartTick = 6L * Sim.ticksPerDay / 24L /// 新档职业状态:按世界 seed 与职业种子生成 nowTick 所属日的 Offered 任务(design §3)。 let occupationStateFor (seed: uint64) (kind: Occupation.Kind) (nowTick: int64) : Occupation.State = let baseState = Occupation.stateOf kind { baseState with Today = Occupation.dailyTaskOf seed (Occupation.occupationSeedOf kind) (Occupation.dayIndexOf nowTick) kind } /// 跨日刷新当日任务;同日(含 Done/Failed 终态)原样保留。 /// P55:经 Kernel `refreshState`,跨日时按上一日是否已完成保留/清零连续天数。 let refreshOccupationToday (seed: uint64) (state: Occupation.State) (nowTick: int64) : Occupation.State = Occupation.refreshState seed (Occupation.occupationSeedOf state.Profile.Kind) state.Profile.Kind nowTick state /// 开局身份年鉴(design §3):带职业建世界后写入一条 `StoryAnnal`「以…为生」, /// 形成个人传记线。Game 与测试共用同一口径。 let appendIdentityAnnal (state: Occupation.State) (world: World) : World = Sim.appendAnnal { Tick = world.Tick Kind = Sim.StoryAnnal Summary = Occupation.Story.identitySummaryOf state.Profile.Kind } world /// 以显式站位建世界:avatarTile 为玩家出生瓦片,npcTiles 为 NPC 出生瓦片序列。 /// 64x48 旧档与世界走 `avatarSpawnPosition` + `npcSpawnTiles` 的等价调用,字节口径不变。 let initialWorldWithPlacement (startAtDaylight: bool) (seed: uint64) (count: int) (avatarTile: int * int) (npcTiles: (int * int) list) (occupation: Occupation.State option) : World = if count < 0 || count > npcTiles.Length then invalidArg (nameof count) "Desktop bootstrap supports at most 30 NPCs." let avatarPosition = tileCenter (fst avatarTile) (snd avatarTile) let startTick = if startAtDaylight then daylightStartTick else 0L let baseWorld = Sim.initialWorldN seed count let avatar = let baseAvatar = { baseWorld.Avatar with Pos = avatarPosition Mind = { baseWorld.Avatar.Mind with Target = avatarPosition } } match occupation with | None -> baseAvatar | Some state -> let needs = baseAvatar.Mind.Needs { baseAvatar with Mind = { baseAvatar.Mind with Needs = { needs with Money = float32 state.Profile.InitialMoney Energy = float32 state.Profile.InitialEnergy } } } let npcs = baseWorld.Npcs |> Array.mapi (fun index npc -> let x, y = List.item index npcTiles { npc with Pos = tileCenter x y }) { baseWorld with Tick = startTick Time = float startTick * Sim.dtSeconds Avatar = avatar Npcs = npcs } let initialWorldWithOccupation (startAtDaylight: bool) (seed: uint64) (count: int) (occupation: Occupation.State option) : World = initialWorldWithPlacement startAtDaylight seed count (31, 25) (List.ofArray npcSpawnTiles) occupation /// P20 第三步:默认可玩世界。生成器的核心可达区作为站位,30 个 spawn 全部落核心可达域。 let initialWorldInMap (startAtDaylight: bool) (seed: uint64) (count: int) (map: MapGen.Result) : World = let avatarTile = ((map.Core.MinX + map.Core.MaxX) / 2, (map.Core.MinY + map.Core.MaxY) / 2) initialWorldWithPlacement startAtDaylight seed count avatarTile map.Spawns None let initialWorldForDaylight (startAtDaylight: bool) (seed: uint64) (count: int) : World = initialWorldWithOccupation startAtDaylight seed count None let initialWorld (seed: uint64) (count: int) : World = initialWorldWithOccupation false seed count None