1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
|
namespace LivingVillage.Desktop
open Microsoft.Xna.Framework.Graphics
open LivingVillage.Kernel
open LivingVillage.Kernel.Sim
open LivingVillage.Desktop.VillagePresentation
module VillageArt =
type PropKind =
| WhiteWallDarkTileHouse
| StoneBridge
| Bamboo
| VegetableGarden
type TileKind =
| RiverWater
| StonePaving
type ArtProp =
{ Kind: PropKind
Position: TilePosition }
type ArtTile =
{ Kind: TileKind
Position: TilePosition }
type JiangnanArtPlan =
{ Props: ArtProp list
Tiles: ArtTile list }
type GroundKind =
| Water
| StonePath
type RenderPropKind =
| House
| Bridge
| BambooGrove
| VegetablePatch
type RenderProp =
{ Kind: RenderPropKind
Position: TilePosition }
type InteriorElementKind =
| Window
| Table
| Bed
| Screen
type InteriorElement =
{ Kind: InteriorElementKind
Position: TilePosition }
type InteriorPlan =
{ Elements: InteriorElement list }
type JiangnanRenderPlan =
{ Origin: TilePosition
Grounds: (TilePosition * GroundKind) list
Props: RenderProp list
Interior: InteriorPlan }
type NpcVisualVariant =
| IndigoRobe
| OchreVest
| JadeSash
type FacingDirection =
| NorthFacing
| SouthFacing
| WestFacing
| EastFacing
type AnimationFrame =
| FrameOne
| FrameTwo
type NpcSpriteSpec =
{ Variant: NpcVisualVariant
Direction: FacingDirection
Frame: AnimationFrame }
type ArtTextures =
{ TileAtlas: Texture2D
CharacterAtlas: Texture2D }
type private TileSprite =
| GrassSprite
| WaterSprite
| StonePathSprite
| RoofSprite
| WallSprite
| BridgeSprite
| BambooSprite
| GardenSprite
type private XnaColor = Microsoft.Xna.Framework.Color
type private XnaRectangle = Microsoft.Xna.Framework.Rectangle
type private XnaVector2 = Microsoft.Xna.Framework.Vector2
let private position x y : TilePosition =
{ X = x
Y = y }
let private prop kind x y : ArtProp =
{ Kind = kind
Position = position x y }
let private tile kind x y : ArtTile =
{ Kind = kind
Position = position x y }
let samplePlan () : JiangnanArtPlan =
{ Props =
[ prop WhiteWallDarkTileHouse 6 4
prop StoneBridge 10 4
prop Bamboo 3 2
prop VegetableGarden 8 7 ]
Tiles =
[ tile RiverWater 10 2
tile RiverWater 10 3
tile StonePaving 4 5
tile StonePaving 5 5
tile StonePaving 6 5 ] }
let sampleRenderPlan () : JiangnanRenderPlan =
let ground kind x y = position x y, kind
let renderProp (kind: RenderPropKind) x y : RenderProp =
{ Kind = kind
Position = position x y }
let interiorElement kind x y : InteriorElement =
{ Kind = kind
Position = position x y }
{ Origin = position 25 18
Grounds =
[ ground Water 10 2
ground Water 10 3
ground Water 10 4
ground Water 10 5
ground Water 10 6
ground Water 10 7
ground Water 11 4
ground Water 11 5
ground StonePath 3 5
ground StonePath 4 5
ground StonePath 5 5
ground StonePath 6 5
ground StonePath 7 5
ground StonePath 8 5
ground StonePath 9 5
ground StonePath 10 5
ground StonePath 11 5
ground StonePath 6 6
ground StonePath 6 7 ]
Props =
[ renderProp House 6 4
renderProp Bridge 10 5
renderProp BambooGrove 3 2
renderProp VegetablePatch 8 7 ]
Interior =
{ Elements =
[ interiorElement Window 2 2
interiorElement Table 4 4
interiorElement Bed 7 3
interiorElement Screen 7 6 ] } }
let worldTile (plan: JiangnanRenderPlan) (localPosition: TilePosition) : TilePosition =
{ X = plan.Origin.X + localPosition.X
Y = plan.Origin.Y + localPosition.Y }
let private positiveModulo divisor value =
let remainder = value % divisor
if remainder < 0 then remainder + divisor else remainder
let npcVisualVariant (NpcId npcId) : NpcVisualVariant =
match positiveModulo 3 npcId with
| 0 -> IndigoRobe
| 1 -> OchreVest
| _ -> JadeSash
let private positiveModulo64 divisor value =
let remainder = value % divisor
if remainder < 0L then remainder + divisor else remainder
let animationFrame (tick: int64) : AnimationFrame =
if positiveModulo64 8L tick < 4L then FrameOne else FrameTwo
let directionFromVector (vector: Vec2) : FacingDirection =
if abs vector.X < 0.001f && abs vector.Y < 0.001f then
SouthFacing
elif abs vector.X >= abs vector.Y then
if vector.X >= 0.0f then EastFacing else WestFacing
elif vector.Y >= 0.0f then
SouthFacing
else
NorthFacing
let facingAfterMovement (previousPosition: Vec2) (nextPosition: Vec2) (current: FacingDirection) : FacingDirection =
let movement : Vec2 =
{ X = nextPosition.X - previousPosition.X
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 =
{ Variant = npcVisualVariant npcId
Direction = direction
Frame = animationFrame tick }
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 characterSourceRectangle (spec: NpcSpriteSpec) : Microsoft.Xna.Framework.Rectangle =
let variantIndex =
match spec.Variant with
| IndigoRobe -> 0
| OchreVest -> 1
| JadeSash -> 2
let directionIndex =
match spec.Direction with
| NorthFacing -> 0
| SouthFacing -> 1
| WestFacing -> 2
| EastFacing -> 3
let frameIndex =
match spec.Frame with
| FrameOne -> 0
| FrameTwo -> 1
let cellIndex = (variantIndex * 4 + directionIndex) * 2 + frameIndex
Microsoft.Xna.Framework.Rectangle(cellIndex * 32, 0, 32, 48)
let private setPixel (pixels: XnaColor[]) (width: int) (x: int) (y: int) (color: XnaColor) =
let height = pixels.Length / width
if x >= 0 && x < width && y >= 0 && y < height then
pixels.[y * width + x] <- color
let private fillRect (pixels: XnaColor[]) (width: int) (x: int) (y: int) (rectWidth: int) (rectHeight: int) (color: XnaColor) =
for row in y .. y + rectHeight - 1 do
for column in x .. x + rectWidth - 1 do
setPixel pixels width column row color
let private rgba (r: int) (g: int) (b: int) (a: int) : XnaColor = XnaColor(r, g, b, a)
let private buildTileAtlas (device: GraphicsDevice) : Texture2D =
let atlasWidth = Sim.tilePixels * 8
let atlasHeight = Sim.tilePixels
let pixels = Array.create (atlasWidth * atlasHeight) (rgba 0 0 0 0)
let fillSlot slot color = fillRect pixels atlasWidth (slot * Sim.tilePixels) 0 Sim.tilePixels Sim.tilePixels color
let setSlotPixel slot x y color = setPixel pixels atlasWidth (slot * Sim.tilePixels + x) y color
let grass = rgba 66 112 70 255
let grassLight = rgba 92 139 82 255
let water = rgba 54 116 145 255
let waterLight = rgba 105 174 184 255
let path = rgba 170 153 123 255
let pathLight = rgba 201 183 145 255
let roof = rgba 45 49 57 255
let roofLight = rgba 76 80 85 255
let wall = rgba 225 216 195 255
let wallShadow = rgba 166 147 119 255
let wood = rgba 119 76 47 255
let woodLight = rgba 164 111 61 255
let bamboo = rgba 51 101 61 255
let bambooLight = rgba 107 153 81 255
let soil = rgba 122 78 49 255
let sprout = rgba 86 139 68 255
fillSlot 0 grass
for y in 0 .. Sim.tilePixels - 1 do
for x in 0 .. Sim.tilePixels - 1 do
if (x * 13 + y * 7) % 29 = 0 then
setSlotPixel 0 x y grassLight
fillSlot 1 water
for y in [ 5; 16; 27 ] do
for x in 3 .. Sim.tilePixels - 4 do
if (x + y) % 5 <> 0 then
setSlotPixel 1 x y waterLight
for x in [ 8; 23 ] do
fillRect pixels atlasWidth (x + Sim.tilePixels) 2 2 26 (rgba 44 93 124 255)
fillSlot 2 path
for y in [ 0; 31 ] do
for x in 0 .. Sim.tilePixels - 1 do
setSlotPixel 2 x y pathLight
for x in [ 7; 24 ] do
for y in 4 .. 27 do
if y % 3 <> 0 then
setSlotPixel 2 x y pathLight
fillSlot 3 roof
for y in [ 5; 13; 21; 29 ] do
for x in 0 .. Sim.tilePixels - 1 do
if (x + y) % 6 < 5 then
setSlotPixel 3 x y roofLight
fillSlot 4 wall
fillRect pixels atlasWidth (4 * Sim.tilePixels) 0 2 Sim.tilePixels wallShadow
fillRect pixels atlasWidth (4 * Sim.tilePixels + 15) 0 2 Sim.tilePixels wallShadow
fillRect pixels atlasWidth (4 * Sim.tilePixels + 30) 0 2 Sim.tilePixels wallShadow
fillRect pixels atlasWidth (4 * Sim.tilePixels) 27 Sim.tilePixels 3 wallShadow
fillSlot 5 wood
for y in [ 6; 15; 24 ] do
fillRect pixels atlasWidth (5 * Sim.tilePixels) y Sim.tilePixels 2 woodLight
fillSlot 6 (rgba 0 0 0 0)
for stemX in [ 6; 16; 26 ] do
fillRect pixels atlasWidth (6 * Sim.tilePixels + stemX) 4 3 25 bamboo
for leafY in [ 8; 17; 25 ] do
fillRect pixels atlasWidth (6 * Sim.tilePixels + stemX - 4) leafY 5 2 bambooLight
fillRect pixels atlasWidth (6 * Sim.tilePixels + stemX + 2) (leafY + 2) 5 2 bambooLight
fillSlot 7 (rgba 0 0 0 0)
for row in [ 6; 15; 24 ] do
fillRect pixels atlasWidth (7 * Sim.tilePixels + 2) row 28 3 soil
for sproutX in [ 6; 15; 24 ] do
fillRect pixels atlasWidth (7 * Sim.tilePixels + sproutX) (row - 3) 2 3 sprout
let texture = new Texture2D(device, atlasWidth, atlasHeight, false, SurfaceFormat.Color)
texture.SetData pixels
texture
let private buildCharacterAtlas (device: GraphicsDevice) : Texture2D =
let cellWidth = 32
let cellHeight = 48
let columns = 3 * 4 * 2
let atlasWidth = columns * cellWidth
let pixels = Array.create (atlasWidth * cellHeight) (rgba 0 0 0 0)
let variants = [| IndigoRobe; OchreVest; JadeSash |]
let directions = [| NorthFacing; SouthFacing; WestFacing; EastFacing |]
let frames = [| FrameOne; FrameTwo |]
for variantIndex in 0 .. variants.Length - 1 do
for directionIndex in 0 .. directions.Length - 1 do
for frameIndex in 0 .. frames.Length - 1 do
let variant = variants.[variantIndex]
let direction = directions.[directionIndex]
let frame = frames.[frameIndex]
let spec = { Variant = variant; Direction = direction; Frame = frame }
let source = characterSourceRectangle spec
let offsetX = source.X
let robe, sash =
match variant with
| IndigoRobe -> rgba 46 57 108 255, rgba 104 145 183 255
| OchreVest -> rgba 141 91 45 255, rgba 205 163 76 255
| JadeSash -> rgba 46 113 91 255, rgba 121 185 139 255
let skin = rgba 232 180 139 255
let hair = rgba 42 36 37 255
let shadow = rgba 23 31 34 120
let legShift = if frame = FrameOne then 0 else 2
fillRect pixels atlasWidth (offsetX + 7) 42 18 3 shadow
fillRect pixels atlasWidth (offsetX + 8 + legShift) 35 6 10 robe
fillRect pixels atlasWidth (offsetX + 18 - legShift) 35 6 10 robe
fillRect pixels atlasWidth (offsetX + 7) 22 18 16 robe
fillRect pixels atlasWidth (offsetX + 9) 32 14 4 sash
fillRect pixels atlasWidth (offsetX + 10) 12 12 10 skin
fillRect pixels atlasWidth (offsetX + 7) 8 18 4 hair
fillRect pixels atlasWidth (offsetX + 5) 7 22 3 sash
match direction with
| NorthFacing ->
fillRect pixels atlasWidth (offsetX + 9) 13 14 8 hair
| SouthFacing ->
fillRect pixels atlasWidth (offsetX + 12) 17 2 2 hair
fillRect pixels atlasWidth (offsetX + 18) 17 2 2 hair
| WestFacing ->
fillRect pixels atlasWidth (offsetX + 9) 16 3 5 hair
| EastFacing ->
fillRect pixels atlasWidth (offsetX + 20) 16 3 5 hair
let texture = new Texture2D(device, atlasWidth, cellHeight, false, SurfaceFormat.Color)
texture.SetData pixels
texture
let buildTextures (device: GraphicsDevice) : ArtTextures =
{ TileAtlas = buildTileAtlas device
CharacterAtlas = buildCharacterAtlas device }
let private tileSourceRectangle sprite : XnaRectangle =
let index =
match sprite with
| GrassSprite -> 0
| WaterSprite -> 1
| StonePathSprite -> 2
| RoofSprite -> 3
| WallSprite -> 4
| BridgeSprite -> 5
| BambooSprite -> 6
| GardenSprite -> 7
XnaRectangle(index * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels)
let private tilePosition x y : TilePosition =
{ X = x
Y = y }
let private isVisible (camera: XnaVector2) (viewportWidth: int) (viewportHeight: int) (position: TilePosition) =
let screenX = position.X * Sim.tilePixels - int camera.X
let screenY = position.Y * Sim.tilePixels - int camera.Y
screenX < viewportWidth
&& screenX + Sim.tilePixels > 0
&& screenY < viewportHeight
&& screenY + Sim.tilePixels > 0
let private drawTile (spriteBatch: SpriteBatch) (atlas: Texture2D) (camera: XnaVector2) (position: TilePosition) sprite tint =
let destination =
XnaRectangle(
position.X * Sim.tilePixels - int camera.X,
position.Y * Sim.tilePixels - int camera.Y,
Sim.tilePixels,
Sim.tilePixels)
spriteBatch.Draw(atlas, destination, tileSourceRectangle sprite, tint)
let private drawWorldTile (spriteBatch: SpriteBatch) (textures: ArtTextures) (camera: XnaVector2) (viewportWidth: int) (viewportHeight: int) position sprite tint =
if isVisible camera viewportWidth viewportHeight position then
drawTile spriteBatch textures.TileAtlas camera position sprite tint
let private offsetTile (position: TilePosition) dx dy : TilePosition =
tilePosition (position.X + dx) (position.Y + dy)
let drawWorld
(spriteBatch: SpriteBatch)
(textures: ArtTextures)
(camera: XnaVector2)
(viewportWidth: int)
(viewportHeight: int)
(plan: JiangnanRenderPlan)
(tint: XnaColor) =
let draw position sprite =
drawWorldTile spriteBatch textures camera viewportWidth viewportHeight position sprite tint
let tx0 = max 0 (int camera.X / Sim.tilePixels)
let ty0 = max 0 (int camera.Y / Sim.tilePixels)
let tx1 = min (Sim.mapWidthTiles - 1) ((int camera.X + viewportWidth) / Sim.tilePixels + 1)
let ty1 = min (Sim.mapHeightTiles - 1) ((int camera.Y + viewportHeight) / Sim.tilePixels + 1)
for y in ty0 .. ty1 do
for x in tx0 .. tx1 do
draw (tilePosition x y) GrassSprite
for localPosition, kind in plan.Grounds do
let position = worldTile plan localPosition
let sprite =
match kind with
| Water -> WaterSprite
| StonePath -> StonePathSprite
draw position sprite
let drawHouse localPosition =
let position = worldTile plan localPosition
for dx in -1 .. 1 do
draw (offsetTile position dx 0) RoofSprite
draw (offsetTile position dx 1) WallSprite
draw (offsetTile position 0 1) BridgeSprite
let drawBridge localPosition =
let position = worldTile plan localPosition
draw position BridgeSprite
draw (offsetTile position 1 0) BridgeSprite
let drawBamboo localPosition =
let position = worldTile plan localPosition
draw position BambooSprite
draw (offsetTile position 0 1) BambooSprite
draw (offsetTile position 1 0) BambooSprite
let drawGarden localPosition =
let position = worldTile plan localPosition
draw position GardenSprite
draw (offsetTile position 1 0) GardenSprite
for prop in plan.Props do
match prop.Kind with
| House -> drawHouse prop.Position
| Bridge -> drawBridge prop.Position
| BambooGrove -> drawBamboo prop.Position
| VegetablePatch -> drawGarden prop.Position
let drawCharacter
(spriteBatch: SpriteBatch)
(textures: ArtTextures)
(camera: XnaVector2)
(position: Vec2)
(spec: NpcSpriteSpec)
(tint: XnaColor) =
let destination =
XnaRectangle(
int position.X - int camera.X - 16,
int position.Y - int camera.Y - 48,
32,
48)
spriteBatch.Draw(textures.CharacterAtlas, destination, characterSourceRectangle spec, tint)
let drawInterior (spriteBatch: SpriteBatch) (pixel: Texture2D) (viewportWidth: int) (viewportHeight: int) (plan: InteriorPlan) =
let roomX = 40
let roomY = 40
let roomWidth = max 320 (viewportWidth - 80)
let roomHeight = max 240 (viewportHeight - 80)
spriteBatch.Draw(pixel, XnaRectangle(0, 0, viewportWidth, viewportHeight), rgba 18 25 42 255)
spriteBatch.Draw(pixel, XnaRectangle(roomX, roomY, roomWidth, roomHeight), rgba 188 163 126 255)
spriteBatch.Draw(pixel, XnaRectangle(roomX, roomY, roomWidth, 18), rgba 65 57 59 255)
spriteBatch.Draw(pixel, XnaRectangle(roomX, roomY + roomHeight - 18, roomWidth, 18), rgba 99 70 50 255)
for element in plan.Elements do
let x = roomX + element.Position.X * Sim.tilePixels
let y = roomY + element.Position.Y * Sim.tilePixels
match element.Kind with
| Window ->
spriteBatch.Draw(pixel, XnaRectangle(x, y, 48, 30), rgba 86 143 170 255)
spriteBatch.Draw(pixel, XnaRectangle(x + 22, y, 4, 30), rgba 225 216 195 255)
| Table ->
spriteBatch.Draw(pixel, XnaRectangle(x, y, 64, 32), rgba 119 76 47 255)
spriteBatch.Draw(pixel, XnaRectangle(x + 8, y + 28, 8, 20), rgba 83 52 39 255)
spriteBatch.Draw(pixel, XnaRectangle(x + 48, y + 28, 8, 20), rgba 83 52 39 255)
| Bed ->
spriteBatch.Draw(pixel, XnaRectangle(x, y, 48, 80), rgba 114 62 67 255)
spriteBatch.Draw(pixel, XnaRectangle(x + 4, y + 4, 40, 24), rgba 224 211 185 255)
| Screen ->
spriteBatch.Draw(pixel, XnaRectangle(x, y, 16, 80), rgba 61 45 49 255)
spriteBatch.Draw(pixel, XnaRectangle(x + 12, y + 4, 8, 72), rgba 142 102 72 255)
|