aboutsummaryrefslogtreecommitdiff
path: root/docs/api.org
blob: 3925bc277a4d49439068e8b3f7239b97e12d6bd3 (plain)
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
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
#+TITLE: Downstroke API Reference

This document describes the public API for the Downstroke game engine. All exported functions are organized by module.

* Engine (~downstroke-engine~)

#+begin_src scheme
(import downstroke-engine)
#+end_src

The engine module provides the top-level game lifecycle and state management.

** ~make-game~

#+begin_src scheme
(make-game #!key
  (title "Downstroke Game")
  (width 640)
  (height 480)
  (frame-delay 16)
  (input-config *default-input-config*)
  (preload #f)
  (create #f)
  (update #f)
  (render #f))
#+end_src

Creates and initializes a game object. All parameters are optional keywords.

| Parameter | Type | Default | Description |
|-----------+------+---------+-------------|
| ~title~ | string | "Downstroke Game" | Window title |
| ~width~ | integer | 640 | Game window width in pixels |
| ~height~ | integer | 480 | Game window height in pixels |
| ~frame-delay~ | integer | 16 | Delay between frames in milliseconds (30 FPS ≈ 33) |
| ~input-config~ | input-config | *default-input-config* | Keyboard/controller mappings |
| ~preload~ | procedure/false | #f | Hook: ~(lambda (game) ...)~ called once before create |
| ~create~ | procedure/false | #f | Hook: ~(lambda (game) ...)~ called once at startup |
| ~update~ | procedure/false | #f | Hook: ~(lambda (game dt) ...)~ called each frame |
| ~render~ | procedure/false | #f | Hook: ~(lambda (game) ...)~ called after render-scene! |

The game object is the central hub. Use it to store/retrieve assets, manage scenes, and access the current input state.

** ~game-run!~

#+begin_src scheme
(game-run! game)
#+end_src

Starts the main event loop. Initializes SDL2, opens the window, and runs the frame loop indefinitely until the user quits or the ~quit~ action is pressed. Never returns.

Lifecycle order within each frame:
1. Collect SDL2 events
2. Update input state
3. Call ~update:~ hook (or active state's ~update~)
4. Clear renderer
5. Call ~render-scene!~ (if scene is set)
6. Call ~render:~ hook (or active state's ~render~)
7. Present renderer
8. Apply frame delay

** ~game-camera~

#+begin_src scheme
(game-camera game)
#+end_src

Returns the current scene's camera struct. Only valid after ~create:~ runs. Returns a camera record with ~x~ and ~y~ fields for the top-left viewport corner in world coordinates.

** ~game-asset~

#+begin_src scheme
(game-asset game key)
#+end_src

Retrieves an asset from the game's registry by key. Returns ~#f~ if the key is not found.

** ~game-asset-set!~

#+begin_src scheme
(game-asset-set! game key value)
#+end_src

Stores an asset in the game's registry. Overwrites any existing value at the key.

** ~make-game-state~

#+begin_src scheme
(make-game-state #!key (create #f) (update #f) (render #f))
#+end_src

Creates a state record (plist) with optional lifecycle hooks. Used with ~game-add-state!~ and ~game-start-state!~ to build a state machine within the game.

| Parameter | Type | Default | Description |
|-----------+------+---------+-------------|
| ~create~ | procedure/false | #f | Called when entering this state |
| ~update~ | procedure/false | #f | Called each frame while active |
| ~render~ | procedure/false | #f | Called each frame after rendering (overlay) |

** ~game-add-state!~

#+begin_src scheme
(game-add-state! game name state)
#+end_src

Registers a named state (created with ~make-game-state~) in the game's state table. ~name~ must be a symbol.

** ~game-start-state!~

#+begin_src scheme
(game-start-state! game name)
#+end_src

Transitions to a named state, activating its lifecycle hooks. Calls the state's ~create:~ hook (if present) immediately.

** Example: Game with State Machine

#+begin_src scheme
(define my-game (make-game title: "My Game"))

(game-add-state! my-game 'playing
  (make-game-state
    create: (lambda (game) (print "Game started"))
    update: (lambda (game dt) (print "Updating..."))
    render: (lambda (game) (print "Rendering overlay"))))

(game-add-state! my-game 'paused
  (make-game-state
    create: (lambda (game) (print "Paused"))))

; Start the game in 'playing state
(game-start-state! my-game 'playing)

; Later, transition to paused
(game-start-state! my-game 'paused)
#+end_src

* World (~downstroke-world~)

#+begin_src scheme
(import downstroke-world)
#+end_src

The world module provides the scene (level) abstraction and camera management.

** ~make-scene~

Auto-generated by defstruct. Use keyword arguments:

#+begin_src scheme
(make-scene #!key
  (entities '())
  (tilemap #f)
  (camera #f)
  (tileset-texture #f))
#+end_src

Creates a scene record representing the current level state.

| Parameter | Type | Default | Description |
|-----------+------+---------+-------------|
| ~entities~ | list | ~'()~ | List of entity plists |
| ~tilemap~ | tilemap/false | #f | Tile grid and collisions |
| ~camera~ | camera/false | #f | Viewport position |
| ~tileset-texture~ | SDL2 texture/false | #f | Rendered tileset image |

** ~make-camera~

Auto-generated by defstruct. Use keyword arguments:

#+begin_src scheme
(make-camera #!key (x 0) (y 0))
#+end_src

Creates a camera record. ~x~ and ~y~ are the pixel coordinates of the viewport's top-left corner in world space.

** ~camera-x~, ~camera-y~

#+begin_src scheme
(camera-x camera)
(camera-y camera)
#+end_src

Accessors for camera position.

** ~camera-x-set!~, ~camera-y-set!~

#+begin_src scheme
(camera-x-set! camera x)
(camera-y-set! camera y)
#+end_src

Mutate camera position (in-place).

** ~camera-follow!~

#+begin_src scheme
(camera-follow! camera entity viewport-w viewport-h)
#+end_src

Centers the camera on an entity, clamping to stay within world bounds (never negative). ~viewport-w~ and ~viewport-h~ are the game window dimensions.

** ~scene-add-entity~

#+begin_src scheme
(scene-add-entity scene entity)
#+end_src

Appends an entity to the scene's entity list. Returns the modified scene.

** ~scene-update-entities~

#+begin_src scheme
(scene-update-entities scene proc1 proc2 ...)
#+end_src

Applies each procedure in sequence to all entities in the scene. Each procedure takes a single entity and returns a modified entity. The scene's entity list is updated once with the final result. Returns the modified scene.

Example:

#+begin_src scheme
(define (increment-x entity)
  (entity-set entity #:x (+ 1 (entity-ref entity #:x 0))))

(define (apply-gravity entity)
  (entity-set entity #:vy (+ 1 (entity-ref entity #:vy 0))))

(scene-update-entities scene increment-x apply-gravity)
; Each entity is passed through increment-x, then through apply-gravity
#+end_src

** ~scene-filter-entities~

#+begin_src scheme
(scene-filter-entities scene predicate)
#+end_src

Removes all entities that do not satisfy the predicate. Returns the modified scene.

Example:

#+begin_src scheme
; Remove all entities with type 'enemy
(scene-filter-entities scene
  (lambda (e) (not (eq? (entity-type e) 'enemy))))
#+end_src

** ~scene-find-tagged~

#+begin_src scheme
(scene-find-tagged scene tag)
#+end_src

Returns the first entity whose ~#:tags~ list (a list of symbols) contains the given tag, or ~#f~ if not found.

** ~scene-find-all-tagged~

#+begin_src scheme
(scene-find-all-tagged scene tag)
#+end_src

Returns a list of all entities whose ~#:tags~ list contains the given tag. Returns ~'()~ if none found.

** Accessor functions (auto-generated by defstruct)

- ~scene-entities~, ~scene-entities-set!~
- ~scene-tilemap~, ~scene-tilemap-set!~
- ~scene-camera~, ~scene-camera-set!~
- ~scene-tileset-texture~, ~scene-tileset-texture-set!~

* Entity (~downstroke-entity~)

#+begin_src scheme
(import downstroke-entity)
#+end_src

The entity module provides property list (plist) accessors for game objects. Entities are immutable plists, never modified in place.

** ~entity-ref~

#+begin_src scheme
(entity-ref entity key #!optional default)
#+end_src

Retrieves the value of a key from an entity plist. Returns ~default~ (or ~#f~) if the key is not found. If ~default~ is a procedure, it is called with no arguments to produce the default.

Example:

#+begin_src scheme
(entity-ref player #:x 0)          ; Get x, defaulting to 0
(entity-ref player #:tags '())     ; Get tags, defaulting to empty list
#+end_src

** ~entity-type~

#+begin_src scheme
(entity-type entity)
#+end_src

Shorthand for ~(entity-ref entity #:type #f)~. Returns the entity's ~#:type~ field or ~#f~.

** ~entity-set~

#+begin_src scheme
(entity-set entity key value)
#+end_src

Returns a new plist with the key/value updated. **Does not modify the original entity.** This is a functional (immutable) operation.

Example:

#+begin_src scheme
(define player (list #:x 100 #:y 200 #:type 'player))
(define moved (entity-set player #:x 150))
; player is still (list #:x 100 #:y 200 #:type 'player)
; moved is (list #:x 150 #:y 200 #:type 'player)
#+end_src

** ~entity-update~

#+begin_src scheme
(entity-update entity key proc #!optional default)
#+end_src

Functional update: applies ~proc~ to the current value of ~key~ and updates the entity with the result. Returns a new plist.

Example:

#+begin_src scheme
(entity-update player #:x (lambda (x) (+ x 10)))
; Equivalent to (entity-set player #:x (+ 10 (entity-ref player #:x 0)))
#+end_src

** Shared Entity Keys

All entities can have these keys. Not all are required:

| Key | Type | Description |
|-----|------|-------------|
| ~#:type~ | symbol | Entity type (e.g., 'player, 'enemy) |
| ~#:x~ | number | X position in pixels |
| ~#:y~ | number | Y position in pixels |
| ~#:width~ | number | Bounding box width in pixels |
| ~#:height~ | number | Bounding box height in pixels |
| ~#:vx~ | number | X velocity in pixels/frame |
| ~#:vy~ | number | Y velocity in pixels/frame |
| ~#:tile-id~ | integer | Sprite index in tileset (1-indexed) |
| ~#:tags~ | list | List of symbols for lookup (e.g., '(player)) |
| ~#:gravity?~ | boolean | Apply gravity to this entity? |
| ~#:on-ground?~ | boolean | Is entity touching a solid tile below? |
| ~#:facing~ | integer | 1 (right) or -1 (left) |
| ~#:solid?~ | boolean | Participate in AABB entity collisions? |
| ~#:anim-name~ | symbol | Current animation name |
| ~#:anim-frame~ | integer | Current frame index |
| ~#:anim-tick~ | integer | Ticks in current frame |

* Physics (~downstroke-physics~)

#+begin_src scheme
(import downstroke-physics)
#+end_src

The physics module implements the main collision and movement pipeline. The physics pipeline runs automatically before the user's ~update:~ hook.

** Physics Pipeline Order

The built-in physics runs in this order each frame:

1. ~apply-acceleration~ — consume ~#:ay~ into ~#:vy~
2. ~apply-gravity~ — add gravity to ~#:vy~
3. ~apply-velocity-x~ — move by ~#:vx~
4. ~resolve-tile-collisions-x~ — snap against horizontal tile collisions
5. ~apply-velocity-y~ — move by ~#:vy~
6. ~resolve-tile-collisions-y~ — snap against vertical tile collisions
7. ~detect-ground~ — set ~#:on-ground?~ if standing on a tile

(This separation ensures smooth sliding along walls.)

** ~apply-acceleration~

#+begin_src scheme
(apply-acceleration entity)
#+end_src

Consumes ~#:ay~ (one-shot acceleration) into ~#:vy~ and clears ~#:ay~ to 0. Only applies if ~#:gravity?~ is true.

** ~apply-gravity~

#+begin_src scheme
(apply-gravity entity)
#+end_src

Adds the gravity constant (1 pixel/frame²) to ~#:vy~. Only applies if ~#:gravity?~ is true.

** ~apply-velocity-x~

#+begin_src scheme
(apply-velocity-x entity)
#+end_src

Updates ~#:x~ by adding ~#:vx~. Returns a new entity.

** ~apply-velocity-y~

#+begin_src scheme
(apply-velocity-y entity)
#+end_src

Updates ~#:y~ by adding ~#:vy~. Returns a new entity.

** ~apply-velocity~

#+begin_src scheme
(apply-velocity entity)
#+end_src

Legacy function: updates both ~#:x~ and ~#:y~ by their respective velocities.

** ~resolve-tile-collisions-x~

#+begin_src scheme
(resolve-tile-collisions-x entity tilemap)
#+end_src

Detects and resolves collisions between the entity's bounding box and solid tiles along the X axis. Snaps the entity's ~#:x~ to the near/far tile edge and sets ~#:vx~ to 0. Returns a new entity.

** ~resolve-tile-collisions-y~

#+begin_src scheme
(resolve-tile-collisions-y entity tilemap)
#+end_src

Detects and resolves collisions between the entity's bounding box and solid tiles along the Y axis. Snaps the entity's ~#:y~ to the near/far tile edge and sets ~#:vy~ to 0. Returns a new entity.

** ~detect-ground~

#+begin_src scheme
(detect-ground entity tilemap)
#+end_src

Probes one pixel below the entity's feet to detect if it is standing on a solid tile. Sets ~#:on-ground?~ to true or false. Only applies if ~#:gravity?~ is true. Returns a new entity.

** ~apply-jump~

#+begin_src scheme
(apply-jump entity jump-pressed?)
#+end_src

If the jump button is pressed and the entity is on ground, sets ~#:ay~ to ~(- #:jump-force)~ (default 15 pixels/frame). On the next frame, ~apply-acceleration~ consumes this into ~#:vy~. Returns a new entity.

** ~resolve-entity-collisions~

#+begin_src scheme
(resolve-entity-collisions entities)
#+end_src

Detects and resolves AABB collisions between all pairs of entities with ~#:solid?~ true. Pushes overlapping entities apart along the axis of minimum penetration and sets their velocities in the push direction. Returns a new entity list.

** ~scene-resolve-collisions~

#+begin_src scheme
(scene-resolve-collisions scene)
#+end_src

Applies ~resolve-entity-collisions~ to the scene's entity list. Returns the modified scene.

** Physics Constants

- ~*gravity*~ = 1 (pixels per frame per frame)
- ~*jump-force*~ = 15 (vertical acceleration on jump)

* Input (~downstroke-input~)

#+begin_src scheme
(import downstroke-input)
#+end_src

The input module handles keyboard, joystick, and game controller events. It maintains the current and previous input state to support pressed/released detection.

** ~*default-input-config*~

The default input configuration record. Use as the ~input-config:~ parameter to ~make-game~, or create a custom one with ~make-input-config~.

| Action | Keyboard | Joystick | Controller |
|--------|----------|----------|------------|
| ~up~ | W or Up | Y-axis negative | DPad Up, Left-Y negative |
| ~down~ | S or Down | Y-axis positive | DPad Down, Left-Y positive |
| ~left~ | A or Left | X-axis negative | DPad Left, Left-X negative |
| ~right~ | D or Right | X-axis positive | DPad Right, Left-X positive |
| ~a~ | J or Z | Button 0 | A button |
| ~b~ | K or X | Button 1 | B button |
| ~start~ | Return | Button 7 | Start button |
| ~select~ | (unmapped) | Button 6 | Back button |
| ~quit~ | Escape | (unmapped) | (unmapped) |

** ~input-held?~

#+begin_src scheme
(input-held? state action)
#+end_src

Returns true if the action is currently held down. ~action~ is a symbol like ~'left~ or ~'a~.

** ~input-pressed?~

#+begin_src scheme
(input-pressed? state action)
#+end_src

Returns true if the action was pressed in this frame (held now but not in the previous frame).

** ~input-released?~

#+begin_src scheme
(input-released? state action)
#+end_src

Returns true if the action was released in this frame (held previously but not now).

** ~create-input-state~

#+begin_src scheme
(create-input-state config)
#+end_src

Initializes an input state record from a configuration. All actions start as unpressed.

** Example: Player Movement

#+begin_src scheme
(define (update-player game dt)
  (let ((input (game-input game))
        (player (car (scene-entities (game-scene game)))))
    (if (input-pressed? input 'a)
        (apply-jump player #t)
        player)))
#+end_src

* Renderer (~downstroke-renderer~)

#+begin_src scheme
(import downstroke-renderer)
#+end_src

The renderer module provides SDL2 drawing abstractions.

** ~render-scene!~

#+begin_src scheme
(render-scene! renderer scene)
#+end_src

Draws the entire scene: first the tilemap layers, then all entities. Called automatically by ~game-run!~ before the user's ~render:~ hook. Does nothing if the scene is ~#f~.

** ~entity-screen-coords~

#+begin_src scheme
(entity-screen-coords entity camera)
#+end_src

Returns a list ~(x y width height)~ of the entity's bounding box in screen (viewport) coordinates. Pure function, testable without SDL2.

Example:

#+begin_src scheme
(entity-screen-coords player (make-camera x: 100 y: 50))
; If player is at world (200, 100) with size (16, 16):
; Returns (100 50 16 16) -- offset by camera position
#+end_src

** ~entity-flip~

#+begin_src scheme
(entity-flip entity)
#+end_src

Returns an SDL2 flip list based on the entity's ~#:facing~ field. Returns ~'(horizontal)~ if facing is -1, ~'()~ otherwise.

** ~draw-ui-text~

#+begin_src scheme
(draw-ui-text renderer font text color x y)
#+end_src

Renders a single line of text to the screen at the given pixel coordinates. ~color~ is an SDL2 color struct. Positions are in screen (viewport) space, not world space. Does not cache; call once per frame for each text element.

* Assets (~downstroke-assets~)

#+begin_src scheme
(import downstroke-assets)
#+end_src

The assets module provides a simple registry for game resources.

** ~make-asset-registry~

#+begin_src scheme
(make-asset-registry)
#+end_src

Creates an empty asset registry (hash-table). Returned by ~make-game~ automatically.

** ~asset-set!~

#+begin_src scheme
(asset-set! registry key value)
#+end_src

Stores a value in the registry by key (any hashable value). Overwrites any existing value.

** ~asset-ref~

#+begin_src scheme
(asset-ref registry key)
#+end_src

Retrieves a value from the registry by key. Returns ~#f~ if not found.

** Example: Storing Fonts

#+begin_src scheme
(define (preload game)
  (let ((font (ttf:open-font "assets/font.ttf" 16)))
    (game-asset-set! game 'main-font font)))

(define (render-overlay game)
  (let ((font (game-asset game 'main-font)))
    (draw-ui-text (game-renderer game) font "Score: 100"
                  (sdl2:make-color 255 255 255) 10 10)))
#+end_src

* Sound (~downstroke-sound~)

#+begin_src scheme
(import downstroke-sound)
#+end_src

The sound module provides music and sound effect playback via SDL_mixer.

** ~init-audio!~

#+begin_src scheme
(init-audio!)
#+end_src

Initializes SDL_mixer with default settings (44100 Hz, stereo, 512 buffer). Call once in the ~preload:~ hook.

** ~load-sounds!~

#+begin_src scheme
(load-sounds! sound-alist)
#+end_src

Loads sound effects from an alist of ~(name . path)~ pairs, where ~name~ is a symbol and ~path~ is a file path. Stores them globally for playback.

Example:

#+begin_src scheme
(load-sounds! '((jump . "assets/jump.wav")
                (hit  . "assets/hit.wav")))
#+end_src

** ~play-sound~

#+begin_src scheme
(play-sound name)
#+end_src

Plays a loaded sound effect by name (symbol). Plays on the first available channel. No-op if the sound is not found.

** ~load-music!~

#+begin_src scheme
(load-music! path)
#+end_src

Loads background music from a file. Replaces any previously loaded music.

** ~play-music!~

#+begin_src scheme
(play-music! volume)
#+end_src

Plays the loaded music on an infinite loop. ~volume~ is a number from 0.0 to 1.0.

** ~stop-music!~

#+begin_src scheme
(stop-music!)
#+end_src

Stops the currently playing music immediately.

** ~set-music-volume!~

#+begin_src scheme
(set-music-volume! volume)
#+end_src

Changes the music volume while it is playing. ~volume~ is 0.0 to 1.0.

** ~cleanup-audio!~

#+begin_src scheme
(cleanup-audio!)
#+end_src

Releases all audio resources. Call at shutdown or in a cleanup hook.

* Animation (~downstroke-animation~)

#+begin_src scheme
(import downstroke-animation)
#+end_src

The animation module provides simple frame-based sprite animation.

** ~set-animation~

#+begin_src scheme
(set-animation entity name)
#+end_src

Switches the entity to a named animation, resetting frame and tick counters to 0. No-op if the animation is already active (prevents restarting mid-loop). Returns a new entity.

** ~animate-entity~

#+begin_src scheme
(animate-entity entity animations)
#+end_src

Advances the entity's animation frame based on elapsed ticks. ~animations~ is an alist of ~(name . animation-plist)~ pairs. Each animation plist has ~#:frames~ (list of tile IDs, 0-indexed) and ~#:duration~ (ticks per frame). Returns a new entity with updated ~#:anim-tick~, ~#:anim-frame~, and ~#:tile-id~.

Example:

#+begin_src scheme
(define player-animations
  '((idle #:frames (28) #:duration 10)
    (walk #:frames (27 28) #:duration 10)
    (jump #:frames (29) #:duration 1)))

(define player (list #:anim-name 'walk #:anim-frame 0 #:anim-tick 0))
(animate-entity player player-animations)
; After 10 ticks, frame advances to 1 (wraps to 0 after reaching length)
#+end_src

** ~frame->tile-id~

#+begin_src scheme
(frame->tile-id frames frame-idx)
#+end_src

Converts a frame index to a tile ID (1-indexed). Used internally by ~animate-entity~.

* Scene Loader (~downstroke-scene-loader~)

#+begin_src scheme
(import downstroke-scene-loader)
#+end_src

The scene-loader module provides utilities for loading Tiled maps and instantiating entities from prefabs.

** ~game-load-scene!~

#+begin_src scheme
(game-load-scene! game filename)
#+end_src

Loads a TMX (Tiled) map from a file and creates a scene. Steps:

1. Loads the tilemap from the file
2. Creates an SDL2 texture from the tileset image
3. Creates an empty scene with the tilemap and camera
4. Stores the tilemap in game assets under key ~'tilemap~
5. Sets the scene on the game
6. Returns the scene

Example:

#+begin_src scheme
(define (create-game game)
  (game-load-scene! game "assets/level1.tmx")
  ; Now add entities to the scene...
  )
#+end_src

** ~create-tileset-texture~

#+begin_src scheme
(create-tileset-texture renderer tilemap)
#+end_src

Creates an SDL2 texture from the tileset image embedded in a tilemap struct. Useful when you need the texture independently of ~game-load-scene!~.

** ~make-prefab-registry~

#+begin_src scheme
(make-prefab-registry name1 constructor1 name2 constructor2 ...)
#+end_src

Creates a hash-table mapping symbol names to entity constructor functions. Constructors have the signature ~(lambda (x y w h) entity)~.

Example:

#+begin_src scheme
(define (make-player x y w h)
  (list #:type 'player #:x x #:y y #:width w #:height h
        #:vx 0 #:vy 0 #:gravity? #t #:tile-id 29))

(define (make-enemy x y w h)
  (list #:type 'enemy #:x x #:y y #:width w #:height h
        #:vx -2 #:vy 0 #:gravity? #t #:tile-id 5))

(define prefabs
  (make-prefab-registry
    'player make-player
    'enemy make-enemy))
#+end_src

** ~instantiate-prefab~

#+begin_src scheme
(instantiate-prefab registry type x y w h)
#+end_src

Looks up a constructor by type in the registry and calls it with the given position and size. Returns the entity plist, or ~#f~ if the type is not registered.

** ~tilemap-objects->entities~

#+begin_src scheme
(tilemap-objects->entities tilemap instantiate-fn)
#+end_src

Converts the TMX object list (from Tiled) into entity plists. Each object's type (string from XML) is converted to a symbol and passed to ~instantiate-fn~. Filters out ~#f~ results (unregistered types).

~instantiate-fn~ should have the signature ~(lambda (type x y w h) entity-or-#f)~, typically the curried version of ~instantiate-prefab~:

Example:

#+begin_src scheme
(let* ((prefabs (make-prefab-registry 'player make-player 'enemy make-enemy))
       (inst (lambda (t x y w h) (instantiate-prefab prefabs t x y w h)))
       (entities (tilemap-objects->entities tilemap inst)))
  (scene-entities-set! scene entities))
#+end_src