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.fs79
1 files changed, 68 insertions, 11 deletions
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index 4845408..11a6d71 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -45,6 +45,9 @@ module VillageArt =
| ReedCluster
| StoneBench
| RiverLantern
+ | StoneLantern
+ | WaterVat
+ | MarketStall
type RenderProp =
{ Kind: RenderPropKind
@@ -76,6 +79,19 @@ module VillageArt =
| GreyCap
| StrawHat
+ /// Presentation-only occupational archetype. Kernel NPCs carry no occupation, so the
+ /// visible trade is a pure function of NpcId; three walking rhythms coexist on screen.
+ type NpcTrade =
+ | Farmer
+ | Peddler
+ | Scholar
+
+ /// Per-trade cadence in simulation ticks plus a phase offset, all deterministic.
+ type WalkRhythm =
+ { WalkWindow: int64
+ IdleWindow: int64
+ PhaseOffset: int64 }
+
type FacingDirection =
| NorthFacing
| SouthFacing
@@ -135,6 +151,9 @@ module VillageArt =
| SmokeSpriteA
| SmokeSpriteB
| SmokeSpriteC
+ | StoneLanternSprite
+ | WaterVatSprite
+ | StallSprite
type private XnaColor = Microsoft.Xna.Framework.Color
type private XnaRectangle = Microsoft.Xna.Framework.Rectangle
@@ -228,7 +247,11 @@ module VillageArt =
renderProp ReedCluster 15 13
renderProp StoneBench 9 11
renderProp RiverLantern 5 11
- renderProp RiverLantern 7 14 ]
+ renderProp RiverLantern 7 14
+ renderProp StoneLantern 4 14
+ renderProp StoneLantern 16 14
+ renderProp WaterVat 12 11
+ renderProp MarketStall 3 11 ]
Interior =
{ Elements =
[ interiorElement Window 2 2
@@ -260,7 +283,10 @@ module VillageArt =
| FlowerBush
| ReedCluster
| StoneBench
- | RiverLantern -> [ position x y ])
+ | RiverLantern
+ | StoneLantern
+ | WaterVat
+ | MarketStall -> [ position x y ])
|> many
let private positiveModulo divisor value =
@@ -278,23 +304,48 @@ module VillageArt =
let remainder = value % divisor
if remainder < 0L then remainder + divisor else remainder
- /// Four-pose walk cycle over a 16-tick window (one pose per 4 sim ticks).
- let animationFrame (tick: int64) : AnimationFrame =
- match positiveModulo64 4L (tick / 4L) with
+ /// Trade cycle: NPCs repeat Farmer -> Peddler -> Scholar by id, so all three rhythms
+ /// are present without touching kernel state.
+ let npcTrade (NpcId npcId) : NpcTrade =
+ match positiveModulo 3 npcId with
+ | 0 -> Farmer
+ | 1 -> Peddler
+ | _ -> Scholar
+
+ /// Farmer ambles at the original cadence; Peddler shuffles faster; Scholar strolls slower.
+ let rhythmFor (trade: NpcTrade) : WalkRhythm =
+ match trade with
+ | Farmer -> { WalkWindow = 4L; IdleWindow = 24L; PhaseOffset = 0L }
+ | Peddler -> { WalkWindow = 3L; IdleWindow = 20L; PhaseOffset = 1L }
+ | Scholar -> { WalkWindow = 6L; IdleWindow = 36L; PhaseOffset = 2L }
+
+ /// Four-pose walk cycle at a rhythm's window (one pose per WalkWindow sim ticks).
+ let animationFrameFor (rhythm: WalkRhythm) (tick: int64) : AnimationFrame =
+ match positiveModulo64 4L (tick / rhythm.WalkWindow + rhythm.PhaseOffset) with
| 0L -> FrameOne
| 1L -> FrameTwo
| 2L -> FrameThree
| _ -> FrameFour
- /// Two-pose standing breath cycle over a slower 48-tick window, fully deterministic.
- let idleFrame (tick: int64) : IdleFrame =
- match positiveModulo64 2L (tick / 24L) with
+ /// Two-pose standing breath cycle at a rhythm's IdleWindow, fully deterministic.
+ let idleFrameFor (rhythm: WalkRhythm) (tick: int64) : IdleFrame =
+ match positiveModulo64 2L (tick / rhythm.IdleWindow + rhythm.PhaseOffset) with
| 0L -> IdleOne
| _ -> IdleTwo
/// Picks the walk cycle while moving and the breath cycle while standing still.
+ let characterFrameFor (rhythm: WalkRhythm) (isMoving: bool) (tick: int64) : CharacterFrame =
+ if isMoving then WalkFrame(animationFrameFor rhythm tick) else IdleFrame(idleFrameFor rhythm tick)
+
+ /// Farmer cadence is the base cadence (window 4/24, phase 0), preserving older semantics.
+ let animationFrame (tick: int64) : AnimationFrame =
+ animationFrameFor (rhythmFor Farmer) tick
+
+ let idleFrame (tick: int64) : IdleFrame =
+ idleFrameFor (rhythmFor Farmer) tick
+
let characterFrame (isMoving: bool) (tick: int64) : CharacterFrame =
- if isMoving then WalkFrame(animationFrame tick) else IdleFrame(idleFrame tick)
+ characterFrameFor (rhythmFor Farmer) isMoving tick
/// Water surface ripple phase: three frames, one per 32 simulation ticks, fully deterministic.
let waterFrameTick (tick: int64) : int =
@@ -329,7 +380,7 @@ module VillageArt =
let npcSpriteSpec (npcId: NpcId) (direction: FacingDirection) (isMoving: bool) (tick: int64) : NpcSpriteSpec =
{ Variant = npcVisualVariant npcId
Direction = direction
- Frame = characterFrame isMoving tick }
+ Frame = characterFrameFor (rhythmFor (npcTrade npcId)) 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 =
@@ -445,7 +496,7 @@ module VillageArt =
let characterAtlasRelativePath = "Assets/jiangnan-characters.png"
let interiorAtlasRelativePath = "Assets/jiangnan-interior.png"
- let worldTileCount = 25
+ let worldTileCount = 28
let worldAtlasWidth = Sim.tilePixels * worldTileCount
let worldAtlasHeight = Sim.tilePixels
let characterAtlasWidth = 32 * 5 * 4 * 6
@@ -516,6 +567,9 @@ module VillageArt =
| SmokeSpriteA -> 22
| SmokeSpriteB -> 23
| SmokeSpriteC -> 24
+ | StoneLanternSprite -> 25
+ | WaterVatSprite -> 26
+ | StallSprite -> 27
XnaRectangle(index * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels)
let private tilePosition x y : TilePosition =
@@ -653,6 +707,9 @@ module VillageArt =
| ReedCluster -> drawSingleTile prop.Position ReedSprite
| StoneBench -> drawSingleTile prop.Position BenchSprite
| RiverLantern -> drawSingleTile prop.Position LanternSprite
+ | StoneLantern -> drawSingleTile prop.Position StoneLanternSprite
+ | WaterVat -> drawSingleTile prop.Position WaterVatSprite
+ | MarketStall -> drawSingleTile prop.Position StallSprite
// Chimney smoke is a late pass so it always reads above the roof, the bamboo and
// any other prop sharing its tile. The puff sits one row above the ridge centre