diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 03:13:42 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 03:13:42 +0800 |
| commit | 2dd5ef0c5489f8fd656888f7d749a541101d5ad1 (patch) | |
| tree | 4adeb67b100fc2bdc3b63d93633161f53b9c1331 | |
| parent | d06656aa968ccb2769ba7c90c97f57f2818bb828 (diff) | |
| download | living-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 录屏前缀(证据用)。
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 125 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 108 |
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 |
