summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/Game.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 03:13:42 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 03:13:42 +0800
commit2dd5ef0c5489f8fd656888f7d749a541101d5ad1 (patch)
tree4adeb67b100fc2bdc3b63d93633161f53b9c1331 /src/LivingVillage.Desktop/Game.fs
parentd06656aa968ccb2769ba7c90c97f57f2818bb828 (diff)
downloadliving-village-2dd5ef0c5489f8fd656888f7d749a541101d5ad1.tar.gz
feat(mapgen): 默认可玩世界接入生成器 + 河景细节(P20 第三步 B)
- Game:默认 256x192 生成器世界(核心可达区站位),LV_LEGACY_MAP=1 / LV_MAP_SCALE>1 / LV_MAP_TOUR=1 回退旧路径;StartNewGame 走同一站位。 - VillageArt.drawWorld 新增 pixel 与 mapGen option:Some 时按生成器瓦片绘地面 并叠加桥面/沿河民居/门,跳过 64x48 样例平面与道具;水波沿用 waterFrameTickAt。 - 新增 bridgeLanternTiles;夜间 LanternGlow 沿用既有预乘同心圆(无过曝白芯), 生成器世界用桥头灯笼。 - 新增 LV_RIVERSCAPE_TOUR 真走位巡游与 LV_RECORD_NAME 录屏前缀(证据用)。
Diffstat (limited to 'src/LivingVillage.Desktop/Game.fs')
-rw-r--r--src/LivingVillage.Desktop/Game.fs125
1 files changed, 103 insertions, 22 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index 5d2ba54..8eafd26 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -126,6 +126,39 @@ type LivingVillageGame() as this =
let mutable cjkAtlas = Unchecked.defaultof<Texture2D>
let mutable pixel = Unchecked.defaultof<Texture2D>
let renderPlan = VillageArt.sampleRenderPlan ()
+ let legacyMapMode = Environment.GetEnvironmentVariable("LV_LEGACY_MAP") = "1"
+ let mapScale =
+ match Environment.GetEnvironmentVariable("LV_MAP_SCALE") with
+ | null | "" -> 1
+ | v -> match Int32.TryParse v with (true, value) -> max 1 value | _ -> 1
+ let mapTourMode = Environment.GetEnvironmentVariable("LV_MAP_TOUR") = "1"
+ // P20 第三步:默认可玩世界走 256x192 生成器(核心可达区站位);LV_LEGACY_MAP=1 回退 64x48。
+ let riverscapeActive = not legacyMapMode && not mapTourMode && mapScale <= 1
+ let riverscapeMap : MapGen.Result option =
+ if riverscapeActive then
+ Sim.configureBounds 256 192
+ Some (MapGen.generateWithSize 256 192 42UL)
+ else
+ None
+ let riverscapeTourMode = Environment.GetEnvironmentVariable("LV_RIVERSCAPE_TOUR") = "1" && riverscapeActive
+ /// 巡游路点:第一座桥的整列(含两端外沿)+ 民居门 + 核心中心,用于桥面行走取证。
+ let riverscapeTourWaypoints () : (int * int) list =
+ match riverscapeMap with
+ | None -> []
+ | Some map ->
+ let bridgeStops =
+ map.Bridges
+ |> List.groupBy fst
+ |> List.sortBy fst
+ |> List.truncate 1
+ |> List.collect (fun (x, entries) ->
+ let ys = entries |> List.map snd |> List.sort
+ [ (x, List.min ys - 2) ] @ [ for y in ys -> (x, y) ] @ [ (x, List.max ys + 2) ])
+ let doors = map.Buildings |> List.map (fun building -> (building.DoorX, building.DoorY))
+ let core = ((map.Core.MinX + map.Core.MaxX) / 2, (map.Core.MinY + map.Core.MaxY) / 2)
+ (bridgeStops @ doors @ [ core ])
+ |> List.filter (fun (x, y) -> x >= 0 && x < map.Width && y >= 0 && y < map.Height)
+ |> List.distinct
// Evidence hook: pin the starting wall-clock hour (e.g. 20.75 for 黄昏), desktop-only.
let startHourOverride =
match Environment.GetEnvironmentVariable("LV_AUTOPLAY_START_HOUR") with
@@ -141,9 +174,20 @@ type LivingVillageGame() as this =
let withStartTick (world: World) : World =
let tick = evidenceStartTick ()
{ world with Tick = tick; Time = float tick * Sim.dtSeconds }
+ let initialWorldForMode (occupation: Occupation.State option) : World =
+ match riverscapeMap with
+ | Some map ->
+ let core = map.Core
+ WorldBootstrap.initialWorldWithPlacement
+ daylightAutoplay
+ 42UL
+ Sim.npcCount
+ ((core.MinX + core.MaxX) / 2, (core.MinY + core.MaxY) / 2)
+ map.Spawns
+ occupation
+ | None -> WorldBootstrap.initialWorldWithOccupation daylightAutoplay 42UL Sim.npcCount occupation
let createInitialWorld () =
- WorldBootstrap.initialWorldForDaylight daylightAutoplay 42UL Sim.npcCount
- |> withStartTick
+ initialWorldForMode None |> withStartTick
let mutable world = createInitialWorld ()
let mutable camera = Vector2.Zero
let mutable avatarFacing = VillageArt.SouthFacing
@@ -179,11 +223,10 @@ type LivingVillageGame() as this =
| null | "" -> 25
| v -> match Int32.TryParse v with (true, value) -> max 1 value | _ -> 25)
let mutable recordIndex = 0
- let mutable scale = int (
- match Environment.GetEnvironmentVariable("LV_MAP_SCALE") with
- | null | "" -> 1
- | v -> match Int32.TryParse v with (true, value) -> max 1 value | _ -> 1)
- let mapTourMode = Environment.GetEnvironmentVariable("LV_MAP_TOUR") = "1"
+ let recordName =
+ match Environment.GetEnvironmentVariable("LV_RECORD_NAME") with
+ | null | "" -> "rec-frame-"
+ | v -> v
let mutable tourWaypoints =
[ (1, 1); (255, 191); (509, 381); (256, 60); (256, 300); (80, 200); (430, 300) ]
let mutable tourIndex = 0
@@ -206,8 +249,8 @@ type LivingVillageGame() as this =
let mutable mapGenGenMs = 0.0
do
- if scale > 1 then
- Sim.configureBounds (64 * scale) (48 * scale)
+ if mapScale > 1 then
+ Sim.configureBounds (64 * mapScale) (48 * mapScale)
ProceduralMap.activate (uint64 4242)
if mapGenShotMode then
let stopwatch = System.Diagnostics.Stopwatch.StartNew()
@@ -229,6 +272,8 @@ type LivingVillageGame() as this =
[ (1, 1); (Sim.mapWidthTiles / 2, Sim.mapHeightTiles / 2); (Sim.mapWidthTiles - 3, Sim.mapHeightTiles - 3)
(Sim.mapWidthTiles / 2, 60); (Sim.mapWidthTiles / 2, Sim.mapHeightTiles - 60)
(80, 200); (430, 300) ]
+ if riverscapeTourMode then
+ tourWaypoints <- riverscapeTourWaypoints ()
graphics.PreferredBackBufferWidth <- 1280
graphics.PreferredBackBufferHeight <- 720
graphics.SynchronizeWithVerticalRetrace <- true
@@ -262,7 +307,7 @@ type LivingVillageGame() as this =
occupation
|> Option.map (fun kind -> WorldBootstrap.occupationStateFor 42UL kind startTick)
world <-
- WorldBootstrap.initialWorldWithOccupation daylightAutoplay 42UL Sim.npcCount state
+ initialWorldForMode state
|> withStartTick
this.ResetWorldView()
m5View <- { m5View with Task = state }
@@ -372,6 +417,31 @@ type LivingVillageGame() as this =
if menuShotFrame = MenuShotScript.downFrame then
this.DispatchMenuInput Down
+ /// P20 第三步取证:真走位巡游(Sim.step 驱动)沿桥/路/民居门移动,用于桥面行走帧与昼夜录制。
+ member private this.RunRiverscapeTour () =
+ let target = tourWaypoints.[tourIndex % tourWaypoints.Length]
+ let targetPos : Vec2 =
+ { X = float32 (fst target * Sim.tilePixels + Sim.tilePixels / 2)
+ Y = float32 (snd target * Sim.tilePixels + Sim.tilePixels / 2) }
+ let dx = targetPos.X - world.Avatar.Pos.X
+ let dy = targetPos.Y - world.Avatar.Pos.Y
+ let distance = sqrt (dx * dx + dy * dy)
+ if distance < 3.0f then
+ tourIndex <- tourIndex + 1
+ if tourIndex >= tourWaypoints.Length then
+ printfn "riverscape tour=complete frames=%d" recordIndex
+ this.Exit()
+ else
+ let inv = 1.0f / distance
+ let input = { Input = { MoveX = dx * inv; MoveY = dy * inv } }
+ let before = world.Avatar.Pos
+ world <- Sim.step input world
+ avatarFacing <- VillageArt.facingAfterMovement before world.Avatar.Pos avatarFacing
+ avatarMoving <- distance > 4.0f
+ m5View <- M5Interaction.refreshPrompt world m5View
+ this.CenterCamera()
+ tourFrame <- tourFrame + 1
+
member private this.RunMapTour () =
let (tileX, tileY) = tourWaypoints.[tourIndex % tourWaypoints.Length]
let position : Vec2 =
@@ -396,7 +466,9 @@ type LivingVillageGame() as this =
printfn $"map tour={tourIndex} at tile=({tileX},{tileY})"
member private this.UpdatePlaying (gameTime: GameTime) (kb: KeyboardState) (pressed: Keys -> bool) (pressedAny: Keys list -> bool) =
- if mapTourMode then
+ if riverscapeTourMode then
+ this.RunRiverscapeTour()
+ elif mapTourMode then
this.RunMapTour()
elif sampleMode then
this.RunSampleMode()
@@ -778,7 +850,7 @@ type LivingVillageGame() as this =
recordIndex <- recordIndex + 1
if recordIndex % recordEvery = 0 then
System.IO.Directory.CreateDirectory recordDirectory |> ignore
- this.SaveBackBuffer(sprintf "%s/rec-frame-%04d.png" recordDirectory recordIndex)
+ this.SaveBackBuffer(sprintf "%s/%s%04d.png" recordDirectory recordName recordIndex)
if menuShotMode then
this.CaptureMenuShot()
@@ -966,29 +1038,38 @@ type LivingVillageGame() as this =
VillageArt.drawWorld
spriteBatch
artTextures
+ pixel
camera
viewport.Width
viewport.Height
renderPlan
+ riverscapeMap
world.Tick
profile.WorldTint
// Warm lantern glows strengthen as night falls (pure function of tick phase).
// Concentric premultiplied circles fall off radially from each lantern tile, so
// the core stays a soft warm disc instead of saturating into a white block, and
- // no glow is painted where there is no lantern entity.
+ // no glow is painted where there is no lantern entity. 生成器世界用桥头灯笼。
if profile.LanternGlow > 0.3f then
let glow = profile.LanternGlow
let peakAlpha = int (140.0f * glow)
- for prop in renderPlan.Props do
- match prop.Kind with
- | VillageArt.RenderPropKind.RiverLantern ->
- let position = VillageArt.worldTile renderPlan prop.Position
- let centerX = position.X * Sim.tilePixels - int camera.X + Sim.tilePixels / 2
- let centerY = position.Y * Sim.tilePixels - int camera.Y + Sim.tilePixels / 2
- for (rect, color) in VillageArt.lanternGlowLayers centerX centerY 44 6 255 214 150 peakAlpha do
- spriteBatch.Draw(pixel, rect, color)
- | _ -> ()
+ let lanternTiles =
+ match riverscapeMap with
+ | Some map -> VillageArt.bridgeLanternTiles map
+ | None ->
+ renderPlan.Props
+ |> List.choose (fun prop ->
+ match prop.Kind with
+ | VillageArt.RenderPropKind.RiverLantern ->
+ let position = VillageArt.worldTile renderPlan prop.Position
+ Some(position.X, position.Y)
+ | _ -> None)
+ for (tileX, tileY) in lanternTiles do
+ let centerX = tileX * Sim.tilePixels - int camera.X + Sim.tilePixels / 2
+ let centerY = tileY * Sim.tilePixels - int camera.Y + Sim.tilePixels / 2
+ for (rect, color) in VillageArt.lanternGlowLayers centerX centerY 44 6 255 214 150 peakAlpha do
+ spriteBatch.Draw(pixel, rect, color)
VillageArt.drawGroundShadow spriteBatch pixel camera world.Avatar.Pos
let avatarSpec = VillageArt.avatarSpriteSpec avatarFacing avatarMoving world.Tick