summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 05:42:36 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 05:42:36 +0800
commit4971a2ada6db9a37807bf46a29dbea3d9edbf06a (patch)
tree2fa44306526fe26d0d6c9b88c63bf357844508f5 /src/LivingVillage.Desktop
parent1354745258f08fae0eea2c599b59fbbfb3254dbd (diff)
downloadliving-village-4971a2ada6db9a37807bf46a29dbea3d9edbf06a.tar.gz
p64: A3 样板三处视觉雕琢(俯瞰上半补种/桥栏杆与护岸石/内景家具)+ 证据
Diffstat (limited to 'src/LivingVillage.Desktop')
-rw-r--r--src/LivingVillage.Desktop/Assets/jiangnan-interior.pngbin1613 -> 1906 bytes
-rw-r--r--src/LivingVillage.Desktop/Assets/jiangnan-world.pngbin4492 -> 4512 bytes
-rw-r--r--src/LivingVillage.Desktop/Game.fs98
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs65
4 files changed, 160 insertions, 3 deletions
diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-interior.png b/src/LivingVillage.Desktop/Assets/jiangnan-interior.png
index d23f6b7..027d036 100644
--- a/src/LivingVillage.Desktop/Assets/jiangnan-interior.png
+++ b/src/LivingVillage.Desktop/Assets/jiangnan-interior.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-world.png b/src/LivingVillage.Desktop/Assets/jiangnan-world.png
index b67dd01..90d21e3 100644
--- a/src/LivingVillage.Desktop/Assets/jiangnan-world.png
+++ b/src/LivingVillage.Desktop/Assets/jiangnan-world.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs
index e5f2c11..6b52225 100644
--- a/src/LivingVillage.Desktop/Game.fs
+++ b/src/LivingVillage.Desktop/Game.fs
@@ -331,6 +331,12 @@ type LivingVillageGame() as this =
let mutable p62PendingName = ""
let mutable p62Pending = false
let mutable p62MoveFrames = 0
+ // P64 证据钩子(A3 成熟度):俯瞰/桥近景/内景三帧,内景走真实 MoveTo + Interact。
+ let p64ShotMode = Environment.GetEnvironmentVariable("LV_P64_SHOT") = "1"
+ let mutable p64Step = 0
+ let mutable p64PendingName = ""
+ let mutable p64Pending = false
+ let mutable p64MoveFrames = 0
// P45 证据钩子:LV_P45_SHOT=1 时拍角色 idle / 行走朝下 / 行走朝右 三帧(真实游戏截图)。
let p45ShotMode = Environment.GetEnvironmentVariable("LV_P45_SHOT") = "1"
let mutable p45Step = 0
@@ -1070,6 +1076,88 @@ type LivingVillageGame() as this =
p62PendingName world.Tick world.Avatar.Pos.X world.Avatar.Pos.Y camera.X camera.Y m5View.HomeMode
p62Step <- p62Step + 1
+ /// P64 证据(A3 成熟度):样板区俯瞰 / 桥近景 / 内景三帧。
+ /// 内景沿用 P62 的真实 MoveTo + Interact 进门路径;桥为既有位置,只增强可读性。
+ member private this.PrepareP64Shot () =
+ let tileCenter (t: TilePosition) : Vec2 =
+ { X = float32 (t.X * Sim.tilePixels + Sim.tilePixels / 2)
+ Y = float32 (t.Y * Sim.tilePixels + Sim.tilePixels / 2) }
+ let sceneCenter localX localY =
+ tileCenter (worldTileForSceneTile { X = localX; Y = localY })
+ let home = VillagePresentation.scene.Homes |> List.head
+ let doorCenter = sceneCenter home.Door.X home.Door.Y
+ let setHour (hour: float) =
+ let tick = M6Presentation.resolveStartTick (Some hour) false
+ world <- { world with Tick = tick; Time = float tick * Sim.dtSeconds }
+ if not p64Pending then
+ match p64Step with
+ | 0 ->
+ setHour 12.0
+ // 俯瞰:整村居中,上半部补种与桥、民居同框。
+ world <- { world with Avatar = { world.Avatar with Pos = sceneCenter 6 3 } }
+ this.CenterCamera()
+ let plan = VillageArt.sampleRenderPlan ()
+ let upper =
+ plan.Props
+ |> List.filter (fun prop -> prop.Kind <> VillageArt.House && prop.Position.Y >= 0 && prop.Position.Y <= 6)
+ let upperKinds = upper |> List.map (fun prop -> prop.Kind) |> List.distinct |> List.length
+ let bridgeProps = plan.Props |> List.filter (fun prop -> prop.Kind = VillageArt.Bridge) |> List.length
+ let abutments = VillageArt.sampleBridgeAbutmentTiles plan |> List.length
+ printfn
+ "p64-plan upper_band_props=%d upper_band_kinds=%d bridge_props=%d bridge_foot_tiles=3 bridge_abutments=%d interior_elements=%d"
+ upper.Length
+ upperKinds
+ bridgeProps
+ abutments
+ plan.Interior.Elements.Length
+ p64PendingName <- "p64-overview"
+ p64Pending <- true
+ | 1 ->
+ setHour 12.0
+ // 桥近景:角色立于桥北岸,相机居中,桥面栏杆/护岸石入画。
+ world <- { world with Avatar = { world.Avatar with Pos = sceneCenter 6 10 } }
+ this.CenterCamera()
+ p64PendingName <- "p64-bridge"
+ p64Pending <- true
+ | 2 ->
+ // 真实走入:沿真实 Sim.step 向门口移动,抵达后真实 Interact 进门。
+ let dx = doorCenter.X - world.Avatar.Pos.X
+ let dy = doorCenter.Y - world.Avatar.Pos.Y
+ if dx * dx + dy * dy <= 12.0f * 12.0f then
+ let nextWorld, nextView = M5Interaction.apply M5Command.Interact world m5View
+ world <- nextWorld
+ m5View <- nextView
+ match m5View.HomeMode with
+ | Inside _ ->
+ p64PendingName <- "p64-interior"
+ p64Pending <- true
+ | Outside ->
+ p64MoveFrames <- p64MoveFrames + 1
+ else
+ let length = sqrt (dx * dx + dy * dy)
+ let input =
+ if length > 0.001f then { MoveX = dx / length; MoveY = dy / length }
+ else { MoveX = 0.0f; MoveY = 0.0f }
+ world <- Sim.step { Input = input } world
+ p64MoveFrames <- p64MoveFrames + 1
+ if p64MoveFrames > 900 then
+ printfn "p64-shot=move-timeout frames=%d home=%A" p64MoveFrames m5View.HomeMode
+ this.Exit()
+ | _ ->
+ printfn "p64-shot=done"
+ this.Exit()
+
+ /// P64 证据:落盘并打印帧名/相机/内景模式,供 analyze 核验。
+ member private this.CaptureP64Shot () =
+ if p64Pending then
+ p64Pending <- false
+ System.IO.Directory.CreateDirectory recordDirectory |> ignore
+ this.SaveBackBuffer(sprintf "%s/%s.png" recordDirectory p64PendingName)
+ printfn
+ "p64-shot=%s tick=%d avatar=(%.0f,%.0f) camera=(%.0f,%.0f) home=%A"
+ p64PendingName world.Tick world.Avatar.Pos.X world.Avatar.Pos.Y camera.X camera.Y m5View.HomeMode
+ p64Step <- p64Step + 1
+
member private this.PrepareP45Shot () =
let baseTick = M6Presentation.resolveStartTick (Some 12.0) false
let setTick (tick: int64) =
@@ -2067,6 +2155,14 @@ type LivingVillageGame() as this =
else this.DispatchMenuInput Confirm
elif menu.Page = Playing then
this.PrepareP62Shot()
+ elif p64ShotMode then
+ if menu.Page = MainMenu then
+ this.DispatchMenuInput Confirm
+ elif menu.Page = OccupationSelect then
+ if menu.Selected <> 0 then this.DispatchMenuInput Down
+ else this.DispatchMenuInput Confirm
+ elif menu.Page = Playing then
+ this.PrepareP64Shot()
elif p45ShotMode then
if menu.Page = MainMenu then
this.DispatchMenuInput Confirm
@@ -2541,6 +2637,8 @@ type LivingVillageGame() as this =
this.CaptureP61Shot()
if p62ShotMode && menu.Page = Playing then
this.CaptureP62Shot()
+ if p64ShotMode && menu.Page = Playing then
+ this.CaptureP64Shot()
if p45ShotMode && menu.Page = Playing then
this.CaptureP45Shot()
if p46ShotMode then
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index 3dbb8d4..031b577 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -58,6 +58,11 @@ module VillageArt =
| Table
| Bed
| Screen
+ // P64 内景下半部家具补件(椅/凳/床尾凳/壶)。
+ | Chair
+ | Stool
+ | BedFoot
+ | Teapot
type InteriorElement =
{ Kind: InteriorElementKind
@@ -210,7 +215,10 @@ module VillageArt =
{ Origin = position 25 18
Grounds =
- [ ground StonePath 3 5
+ // P64 北侧田埂小径:衔接上方补种的菜畦/竹丛,填充样板区上半部空旷。
+ [ for x in 0 .. 19 do
+ ground StonePath x 0
+ ground StonePath 3 5
ground StonePath 4 5
ground StonePath 5 5
ground StonePath 6 5
@@ -256,13 +264,30 @@ module VillageArt =
renderProp StoneLantern 4 14
renderProp StoneLantern 16 14
renderProp WaterVat 12 11
- renderProp MarketStall 3 11 ]
+ renderProp MarketStall 3 11
+ // P64 样板区上半部(local y 0..6)中密度补种:菜畦/竹丛/灌木/花丛,
+ // 全部复用既有 RenderPropKind 与低饱和配色,非几何占位。
+ renderProp VegetablePatch 1 1
+ renderProp VegetablePatch 10 2
+ renderProp VegetablePatch 14 1
+ renderProp VegetablePatch 16 4
+ renderProp BambooGrove 8 3
+ renderProp BambooGrove 18 2
+ renderProp ShrubPatch 0 5
+ renderProp ShrubPatch 12 5
+ renderProp FlowerBush 2 3
+ renderProp FlowerBush 6 1 ]
Interior =
{ Elements =
[ interiorElement Window 2 2
interiorElement Table 4 4
interiorElement Bed 7 3
- interiorElement Screen 7 6 ] } }
+ interiorElement Screen 7 6
+ // P64 内景下半部家具补件:椅/凳/床尾凳/柜旁壶。
+ interiorElement BedFoot 5 4
+ interiorElement Chair 2 5
+ interiorElement Stool 6 5
+ interiorElement Teapot 4 5 ] } }
let worldTile (plan: JiangnanRenderPlan) (localPosition: TilePosition) : TilePosition =
{ X = plan.Origin.X + localPosition.X
@@ -294,6 +319,19 @@ module VillageArt =
| MarketStall -> [ position x y ])
|> many
+ /// P64 河埠/护岸石偏移(相对桥首格):桥两侧水线各铺一块,强化桥与河岸的衔接。
+ /// 只做视觉叠加,不写入 footprint、不改瓦片/通行。
+ let bridgeAbutmentOffsets : (int * int) list =
+ [ (-1, 1); (1, 1); (-1, 2); (1, 2) ]
+
+ /// 样板桥两侧护岸石的世界坐标(纯函数,供绘制与回归共用)。
+ let sampleBridgeAbutmentTiles (plan: JiangnanRenderPlan) : TilePosition list =
+ plan.Props
+ |> List.filter (fun prop -> prop.Kind = Bridge)
+ |> List.collect (fun prop ->
+ bridgeAbutmentOffsets
+ |> List.map (fun (dx, dy) -> worldTile plan { X = prop.Position.X + dx; Y = prop.Position.Y + dy }))
+
let private positiveModulo divisor value =
let remainder = value % divisor
if remainder < 0 then remainder + divisor else remainder
@@ -1175,6 +1213,13 @@ module VillageArt =
match textures.Cc0TileAtlas with
| Some _ -> drawCc0At p cc0TileIndexRailing
| None -> draw p BridgeSprite
+ // P64 河埠/护岸石:仅江南图集下于桥两侧水线补石,增强桥的可读性;
+ // cc0 模式保持原样,不改位置/通行。
+ match textures.Cc0TileAtlas with
+ | Some _ -> ()
+ | None ->
+ for (dx, dy) in bridgeAbutmentOffsets do
+ draw (offsetTile position dx dy) RevetmentStoneSprite
bridgeTile position
bridgeTile (offsetTile position 0 1)
bridgeTile (offsetTile position 0 2)
@@ -1368,6 +1413,11 @@ module VillageArt =
| Table -> XnaRectangle(112, 48, 64, 48)
| Bed -> XnaRectangle(176, 0, 48, 96)
| Screen -> XnaRectangle(224, 0, 32, 96)
+ // P64 补件:坐标与 scripts/make-jiangnan-art.py 的绘制区间一一对应。
+ | Chair -> XnaRectangle(4, 8, 28, 44)
+ | Stool -> XnaRectangle(36, 14, 24, 22)
+ | BedFoot -> XnaRectangle(262, 4, 56, 18)
+ | Teapot -> XnaRectangle(330, 6, 20, 16)
let interiorLayout (viewportWidth: int) (viewportHeight: int) : InteriorLayout =
{ RoomX = 40
@@ -1442,6 +1492,15 @@ module VillageArt =
XnaRectangle(layout.RoomX + element.Position.X * Sim.tilePixels - 8, baseY - 96, 48, 96)
| Screen ->
XnaRectangle(layout.RoomX + element.Position.X * Sim.tilePixels, baseY - 96, 32, 96)
+ // P64 补件按其像素体量落位(低矮家具贴近地面线)。
+ | Chair ->
+ XnaRectangle(layout.RoomX + element.Position.X * Sim.tilePixels - 8, baseY - 44, 28, 44)
+ | Stool ->
+ XnaRectangle(layout.RoomX + element.Position.X * Sim.tilePixels - 8, baseY - 22, 24, 22)
+ | BedFoot ->
+ XnaRectangle(layout.RoomX + element.Position.X * Sim.tilePixels - 16, baseY - 14, 56, 18)
+ | Teapot ->
+ XnaRectangle(layout.RoomX + element.Position.X * Sim.tilePixels - 8, baseY - 16, 20, 16)
for element in plan.Elements |> List.sortBy (fun element -> element.Position.Y, element.Position.X) do
spriteBatch.Draw(textures.InteriorAtlas, elementDestination element, interiorSourceRectangle element.Kind, tint)