diff options
Diffstat (limited to 'src/LivingVillage.Desktop')
| -rw-r--r-- | src/LivingVillage.Desktop/Assets/cc0-characters.png | bin | 4197 -> 5575 bytes | |||
| -rw-r--r-- | src/LivingVillage.Desktop/Assets/jiangnan-characters.png | bin | 3047 -> 5585 bytes | |||
| -rw-r--r-- | src/LivingVillage.Desktop/Assets/jiangnan-world.png | bin | 3045 -> 3394 bytes | |||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 89 |
4 files changed, 68 insertions, 21 deletions
diff --git a/src/LivingVillage.Desktop/Assets/cc0-characters.png b/src/LivingVillage.Desktop/Assets/cc0-characters.png Binary files differindex 7e553a6..afd1ed2 100644 --- a/src/LivingVillage.Desktop/Assets/cc0-characters.png +++ b/src/LivingVillage.Desktop/Assets/cc0-characters.png diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-characters.png b/src/LivingVillage.Desktop/Assets/jiangnan-characters.png Binary files differindex f6c8711..dd96e60 100644 --- a/src/LivingVillage.Desktop/Assets/jiangnan-characters.png +++ b/src/LivingVillage.Desktop/Assets/jiangnan-characters.png diff --git a/src/LivingVillage.Desktop/Assets/jiangnan-world.png b/src/LivingVillage.Desktop/Assets/jiangnan-world.png Binary files differindex 23e02cd..9138560 100644 --- a/src/LivingVillage.Desktop/Assets/jiangnan-world.png +++ b/src/LivingVillage.Desktop/Assets/jiangnan-world.png 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 |
