summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop')
-rw-r--r--src/LivingVillage.Desktop/Game.fs125
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs108
2 files changed, 170 insertions, 63 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
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index 19c9aa6..3ad5b19 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -600,13 +600,48 @@ module VillageArt =
let private offsetTile (position: TilePosition) dx dy : TilePosition =
tilePosition (position.X + dx) (position.Y + dy)
+ let private fillRect (spriteBatch: SpriteBatch) (pixel: Texture2D) (color: XnaColor) (x: int) (y: int) (w: int) (h: int) =
+ spriteBatch.Draw(pixel, XnaRectangle(x, y, max 1 w, max 1 h), color)
+
+ /// 生成器世界里的桥面与沿河民居(白墙黑瓦色块 + 门);坐标由调用方按相机换算。
+ let private drawMapStructures
+ (spriteBatch: SpriteBatch)
+ (textures: ArtTextures)
+ (pixel: Texture2D)
+ (tileX: int -> int)
+ (tileY: int -> int)
+ (cell: int)
+ (map: MapGen.Result) =
+ let bridgeSource = tileSourceRectangle BridgeSprite
+ for (x, y) in map.Bridges do
+ spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY y, cell, cell), bridgeSource, XnaColor.White)
+ for building in map.Buildings do
+ for row in building.Top .. building.Top + building.Height - 1 do
+ // 白墙朝向道路一行(门所在行),其余为黑瓦屋顶,两侧河岸朝向一致。
+ let color =
+ if row = building.DoorY then XnaColor(236, 232, 220)
+ else XnaColor(48, 52, 60)
+ fillRect spriteBatch pixel color (tileX building.Left) (tileY row) (building.Width * cell) (cell)
+ fillRect spriteBatch pixel (XnaColor(96, 60, 42)) (tileX building.DoorX) (tileY building.DoorY) cell cell
+
+ /// 桥头灯笼瓦片:每座桥每列的最上/最下一格(桥两端),用于挂灯笼与夜间暖光。
+ let bridgeLanternTiles (map: MapGen.Result) : (int * int) list =
+ map.Bridges
+ |> List.groupBy fst
+ |> List.collect (fun (x, entries) ->
+ let ys = entries |> List.map snd
+ [ (x, List.min ys); (x, List.max ys) ])
+ |> List.distinct
+
let drawWorld
(spriteBatch: SpriteBatch)
(textures: ArtTextures)
+ (pixel: Texture2D)
(camera: XnaVector2)
(viewportWidth: int)
(viewportHeight: int)
(plan: JiangnanRenderPlan)
+ (mapGen: MapGen.Result option)
(tick: int64)
(tint: XnaColor) =
let draw position sprite =
@@ -622,26 +657,30 @@ module VillageArt =
// Procedural terrain for the 512x384 large world: water/pebble/peat codes map
// onto the jiangnan atlas sprites with deterministic ripple continuity.
+ // 生成器世界:1 水 / 2 石板路 / 3 泥炭 / 0 草地。泥炭按草地渲染,路走石板贴图。
let drawTerrainTile (position: TilePosition) (code: int) =
match code with
| 1 -> draw position (waterSpriteAt position)
| 2 -> draw position StonePathSprite
- | 3 -> draw position StonePathSprite
| _ -> draw position GrassSprite
let drawBaseTile (position: TilePosition) =
- match ProceduralMap.terrainOption, ProceduralMap.bigWorldActive with
- | (Some terrain, true) ->
- let tx, ty = position.X, position.Y
- let code = terrain.Tiles.[ty * ProceduralMap.width + tx]
- drawTerrainTile position code
- | _ ->
+ match mapGen with
+ | Some map ->
+ drawTerrainTile position map.Tiles.[position.Y * map.Width + position.X]
+ | None ->
+ match ProceduralMap.terrainOption, ProceduralMap.bigWorldActive with
+ | (Some terrain, true) ->
+ let tx, ty = position.X, position.Y
+ let code = terrain.Tiles.[ty * ProceduralMap.width + tx]
+ drawTerrainTile position code
+ | _ ->
+ match textures.Cc0TileAtlas with
+ | Some cc0 -> drawCc0Tile spriteBatch cc0 tint (cc0GroundRect position) (position.X * Sim.tilePixels - int camera.X) (position.Y * Sim.tilePixels - int camera.Y)
+ | None -> draw position GrassSprite
match textures.Cc0TileAtlas with
| Some cc0 -> drawCc0Tile spriteBatch cc0 tint (cc0GroundRect position) (position.X * Sim.tilePixels - int camera.X) (position.Y * Sim.tilePixels - int camera.Y)
| None -> draw position GrassSprite
- match textures.Cc0TileAtlas with
- | Some cc0 -> drawCc0Tile spriteBatch cc0 tint (cc0GroundRect position) (position.X * Sim.tilePixels - int camera.X) (position.Y * Sim.tilePixels - int camera.Y)
- | None -> draw position GrassSprite
let tx0, ty0, tx1, ty1 =
MapGen.visibleTileRange Sim.mapWidthTiles Sim.mapHeightTiles (int camera.X) (int camera.Y) viewportWidth viewportHeight
@@ -649,7 +688,19 @@ module VillageArt =
for x in tx0 .. tx1 do
drawBaseTile (tilePosition x y)
- for localPosition, kind in plan.Grounds do
+ // 生成器世界:叠加桥面、沿河民居、桥头灯笼;不再绘制 64x48 样例平面与道具。
+ match mapGen with
+ | Some map ->
+ let mapTileX x = x * Sim.tilePixels - int camera.X
+ let mapTileY y = y * Sim.tilePixels - int camera.Y
+ drawMapStructures spriteBatch textures pixel mapTileX mapTileY Sim.tilePixels map
+ for (lanternX, lanternY) in bridgeLanternTiles map do
+ draw (tilePosition lanternX lanternY) LanternSprite
+ | None -> ()
+
+ // 回退(64x48 样例世界)或 LV_MAP_SCALE 大世界时仍绘制样例地面/道具。
+ let planGrounds = match mapGen with Some _ -> [] | None -> plan.Grounds
+ for localPosition, kind in planGrounds do
let position = worldTile plan localPosition
match kind with
| Water -> draw position (waterSpriteAt position)
@@ -693,7 +744,8 @@ module VillageArt =
let drawSingleTile localPosition sprite =
draw (worldTile plan localPosition) sprite
- for prop in plan.Props do
+ let planProps = match mapGen with Some _ -> [] | None -> plan.Props
+ for prop in planProps do
match prop.Kind with
| House -> drawHouse prop.Position
| Bridge -> drawBridge prop.Position
@@ -717,37 +769,13 @@ module VillageArt =
| 0 -> SmokeSpriteA
| 1 -> SmokeSpriteB
| _ -> SmokeSpriteC
- for prop in plan.Props do
+ for prop in planProps do
match prop.Kind with
| House -> draw (offsetTile (worldTile plan prop.Position) 0 -3) smokeSprite
| _ -> ()
/// P20 证据用:把整张 MapGen 地图等比缩放到视口内绘制,返回绘制的瓦片数。
/// 仅用于 `LV_MAPGEN_SHOT` 整图取证,不参与正常游戏循环。
- let private fillRect (spriteBatch: SpriteBatch) (pixel: Texture2D) (color: XnaColor) (x: int) (y: int) (w: int) (h: int) =
- spriteBatch.Draw(pixel, XnaRectangle(x, y, max 1 w, max 1 h), color)
-
- /// 整图证据里叠加桥面与沿河民居(白墙黑瓦色块 + 门)。
- let private drawMapStructures
- (spriteBatch: SpriteBatch)
- (textures: ArtTextures)
- (pixel: Texture2D)
- (tileX: int -> int)
- (tileY: int -> int)
- (cell: int)
- (map: MapGen.Result) =
- let bridgeSource = tileSourceRectangle BridgeSprite
- for (x, y) in map.Bridges do
- spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY y, cell, cell), bridgeSource, XnaColor.White)
- for building in map.Buildings do
- for row in building.Top .. building.Top + building.Height - 1 do
- // 白墙朝向道路一行(门所在行),其余为黑瓦屋顶,两侧河岸朝向一致。
- let color =
- if row = building.DoorY then XnaColor(236, 232, 220)
- else XnaColor(48, 52, 60)
- fillRect spriteBatch pixel color (tileX building.Left) (tileY row) (building.Width * cell) (cell)
- fillRect spriteBatch pixel (XnaColor(96, 60, 42)) (tileX building.DoorX) (tileY building.DoorY) cell cell
-
let drawMapFitted
(spriteBatch: SpriteBatch)
(textures: ArtTextures)
@@ -767,8 +795,7 @@ module VillageArt =
let spriteOf code =
match code with
| 1 -> WaterSprite
- | 2
- | 3 -> StonePathSprite
+ | 2 -> StonePathSprite
| _ -> GrassSprite
for y in 0 .. map.Height - 1 do
for x in 0 .. map.Width - 1 do
@@ -799,8 +826,7 @@ module VillageArt =
| 0 -> WaterSprite
| 1 -> WaterSpriteB
| _ -> WaterSpriteC
- | 2
- | 3 -> StonePathSprite
+ | 2 -> StonePathSprite
| _ -> GrassSprite
let mutable drawn = 0
for y in y0 .. y1 do