summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/VillageArt.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/LivingVillage.Desktop/VillageArt.fs')
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs89
1 files changed, 68 insertions, 21 deletions
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index f72c0f3..abda888 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -88,10 +88,20 @@ module VillageArt =
| FrameThree
| FrameFour
+ /// Two-pose standing breath cycle used whenever a character is not moving.
+ type IdleFrame =
+ | IdleOne
+ | IdleTwo
+
+ /// A character either walks (four-pose cycle) or breathes in place (two-pose cycle).
+ type CharacterFrame =
+ | WalkFrame of AnimationFrame
+ | IdleFrame of IdleFrame
+
type NpcSpriteSpec =
{ Variant: NpcVisualVariant
Direction: FacingDirection
- Frame: AnimationFrame }
+ Frame: CharacterFrame }
type ArtTextures =
{ TileAtlas: Texture2D
@@ -121,6 +131,10 @@ module VillageArt =
| ReedSprite
| BenchSprite
| LanternSprite
+ | WaterSpriteC
+ | SmokeSpriteA
+ | SmokeSpriteB
+ | SmokeSpriteC
type private XnaColor = Microsoft.Xna.Framework.Color
type private XnaRectangle = Microsoft.Xna.Framework.Rectangle
@@ -272,9 +286,23 @@ module VillageArt =
| 2L -> FrameThree
| _ -> FrameFour
- /// Water surface ripple phase: flips every 32 simulation ticks, fully deterministic.
+ /// Two-pose standing breath cycle over a slower 48-tick window, fully deterministic.
+ let idleFrame (tick: int64) : IdleFrame =
+ match positiveModulo64 2L (tick / 24L) with
+ | 0L -> IdleOne
+ | _ -> IdleTwo
+
+ /// Picks the walk cycle while moving and the breath cycle while standing still.
+ let characterFrame (isMoving: bool) (tick: int64) : CharacterFrame =
+ if isMoving then WalkFrame(animationFrame tick) else IdleFrame(idleFrame tick)
+
+ /// Water surface ripple phase: three frames, one per 32 simulation ticks, fully deterministic.
let waterFrameTick (tick: int64) : int =
- int (positiveModulo64 2L (tick / 32L))
+ int (positiveModulo64 3L (tick / 32L))
+
+ /// Chimney smoke puff phase: three frames, one per 40 simulation ticks, fully deterministic.
+ let smokeFrameTick (tick: int64) : int =
+ int (positiveModulo64 3L (tick / 40L))
let directionFromVector (vector: Vec2) : FacingDirection =
if abs vector.X < 0.001f && abs vector.Y < 0.001f then
@@ -292,17 +320,18 @@ module VillageArt =
Y = nextPosition.Y - previousPosition.Y }
if abs movement.X < 0.001f && abs movement.Y < 0.001f then current else directionFromVector movement
- let npcSpriteSpec (npcId: NpcId) (direction: FacingDirection) (tick: int64) : NpcSpriteSpec =
+ let npcSpriteSpec (npcId: NpcId) (direction: FacingDirection) (isMoving: bool) (tick: int64) : NpcSpriteSpec =
{ Variant = npcVisualVariant npcId
Direction = direction
- Frame = animationFrame tick }
+ Frame = characterFrame isMoving tick }
+ /// Villagers breathe while standing at their target and walk while approaching it.
let npcSpriteSpecAtTarget (npcId: NpcId) (position: Vec2) (target: Vec2) (tick: int64) : NpcSpriteSpec =
- let direction =
- directionFromVector
- { X = target.X - position.X
- Y = target.Y - position.Y }
- npcSpriteSpec npcId direction tick
+ let movement : Vec2 =
+ { X = target.X - position.X
+ Y = target.Y - position.Y }
+ let isMoving = abs movement.X >= 0.5f || abs movement.Y >= 0.5f
+ npcSpriteSpec npcId (directionFromVector movement) isMoving tick
let characterSourceRectangle (spec: NpcSpriteSpec) : Microsoft.Xna.Framework.Rectangle =
let variantIndex =
@@ -320,28 +349,30 @@ module VillageArt =
| EastFacing -> 3
let frameIndex =
match spec.Frame with
- | FrameOne -> 0
- | FrameTwo -> 1
- | FrameThree -> 2
- | FrameFour -> 3
- let cellIndex = (variantIndex * 4 + directionIndex) * 4 + frameIndex
+ | WalkFrame FrameOne -> 0
+ | WalkFrame FrameTwo -> 1
+ | WalkFrame FrameThree -> 2
+ | WalkFrame FrameFour -> 3
+ | IdleFrame IdleOne -> 4
+ | IdleFrame IdleTwo -> 5
+ let cellIndex = (variantIndex * 4 + directionIndex) * 6 + frameIndex
Microsoft.Xna.Framework.Rectangle(cellIndex * 32, 0, 32, 48)
let private rgba (r: int) (g: int) (b: int) (a: int) : XnaColor = XnaColor(r, g, b, a)
- let avatarSpriteSpec (direction: FacingDirection) (tick: int64) : NpcSpriteSpec =
+ let avatarSpriteSpec (direction: FacingDirection) (isMoving: bool) (tick: int64) : NpcSpriteSpec =
{ Variant = StrawHat
Direction = direction
- Frame = animationFrame tick }
+ Frame = characterFrame isMoving tick }
let worldAtlasRelativePath = "Assets/jiangnan-world.png"
let characterAtlasRelativePath = "Assets/jiangnan-characters.png"
let interiorAtlasRelativePath = "Assets/jiangnan-interior.png"
- let worldTileCount = 21
+ let worldTileCount = 25
let worldAtlasWidth = Sim.tilePixels * worldTileCount
let worldAtlasHeight = Sim.tilePixels
- let characterAtlasWidth = 32 * 5 * 4 * 4
+ let characterAtlasWidth = 32 * 5 * 4 * 6
let characterAtlasHeight = 48
let interiorAtlasWidth = 392
let interiorAtlasHeight = 96
@@ -405,6 +436,10 @@ module VillageArt =
| ReedSprite -> 18
| BenchSprite -> 19
| LanternSprite -> 20
+ | WaterSpriteC -> 21
+ | SmokeSpriteA -> 22
+ | SmokeSpriteB -> 23
+ | SmokeSpriteC -> 24
XnaRectangle(index * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels)
let private tilePosition x y : TilePosition =
@@ -447,12 +482,17 @@ module VillageArt =
let draw position sprite =
drawWorldTile spriteBatch textures camera viewportWidth viewportHeight position sprite tint
let rippleFrame = waterFrameTick tick
+ let waterSprite =
+ match rippleFrame with
+ | 0 -> WaterSprite
+ | 1 -> WaterSpriteB
+ | _ -> WaterSpriteC
// Procedural terrain for the 512x384 large world: water/pebble/peat codes map
// onto the jiangnan atlas sprites with deterministic ripple continuity.
let drawTerrainTile (position: TilePosition) (code: int) =
match code with
- | 1 -> draw position (if rippleFrame = 0 then WaterSprite else WaterSpriteB)
+ | 1 -> draw position waterSprite
| 2 -> draw position StonePathSprite
| 3 -> draw position StonePathSprite
| _ -> draw position GrassSprite
@@ -482,7 +522,7 @@ module VillageArt =
for localPosition, kind in plan.Grounds do
let position = worldTile plan localPosition
match kind with
- | Water -> draw position (if rippleFrame = 0 then WaterSprite else WaterSpriteB)
+ | Water -> draw position waterSprite
| StonePath ->
match textures.Cc0TileAtlas with
| Some cc0 -> drawCc0Tile spriteBatch cc0 tint cc0PathRect (position.X * Sim.tilePixels - int camera.X) (position.Y * Sim.tilePixels - int camera.Y)
@@ -497,6 +537,13 @@ module VillageArt =
draw (offsetTile position -1 0) WallWindowSprite
draw (offsetTile position 0 0) DoorSprite
draw (offsetTile position 1 0) WallWoodSprite
+ // Chimney smoke drifting up from the roof, cycling through three puffs.
+ let smokeSprite =
+ match smokeFrameTick tick with
+ | 0 -> SmokeSpriteA
+ | 1 -> SmokeSpriteB
+ | _ -> SmokeSpriteC
+ draw (offsetTile position -2 -3) smokeSprite
let drawBridge localPosition =
let position = worldTile plan localPosition