From c1c868d9b6ee61002c7ccb33fbc6a15c5b090e6a Mon Sep 17 00:00:00 2001 From: Gene Pasquet Date: Sun, 12 Apr 2026 15:57:45 +0100 Subject: Enrich entities pipelining to provide scene context to processors --- TODO.org | 4 +++- animation.scm | 48 ++++++++++++++++++++++++------------------ demo/sandbox.scm | 2 +- demo/shmup.scm | 2 +- engine.scm | 25 ++++++++++++---------- entity.scm | 4 ++-- physics.scm | 14 ++++++------ prefabs.scm | 10 ++++----- tests/animation-test.scm | 55 ++++++++++++++++++++++++++++++++---------------- tests/entity-test.scm | 12 +++++------ tests/input-test.scm | 17 +++++++++------ tests/world-test.scm | 8 +++---- tween.scm | 2 +- world.scm | 8 ++++--- 14 files changed, 123 insertions(+), 88 deletions(-) mode change 120000 => 100644 TODO.org diff --git a/TODO.org b/TODO.org deleted file mode 120000 index d2964da..0000000 --- a/TODO.org +++ /dev/null @@ -1 +0,0 @@ -/home/gene/Documents/Perso/Projects/downstroke.org \ No newline at end of file diff --git a/TODO.org b/TODO.org new file mode 100644 index 0000000..edaad3c --- /dev/null +++ b/TODO.org @@ -0,0 +1,3 @@ +* Downstroke TODO + +** TODO change exports so that imports follow the chicken convention: =(import (downstroke engine))= diff --git a/animation.scm b/animation.scm index 468f7f0..c8f4497 100644 --- a/animation.scm +++ b/animation.scm @@ -1,17 +1,17 @@ (module downstroke-animation * - (import scheme - (chicken base) - (chicken keyword) - (only srfi-1 filter) - downstroke-entity - downstroke-world) +(import scheme + (chicken base) + (chicken keyword) + (only srfi-1 filter) + downstroke-entity + downstroke-world) - ;; ---- Animation data accessors ---- +;; ---- Animation data accessors ---- - (define (animation-frames anim) - (get-keyword #:frames anim)) - (define (animation-duration anim) - (get-keyword #:duration anim)) +(define (animation-frames anim) + (get-keyword #:frames anim)) +(define (animation-duration anim) + (get-keyword #:duration anim)) (define (frame-by-idx frames frame-idx) (list-ref frames (modulo frame-idx (length frames)))) @@ -19,9 +19,9 @@ ;; The tile ID is 1-indexed. (define (frame->tile-id frames frame-idx) (let ((frame-def (frame-by-idx frames frame-idx))) - (+ 1 (if (list? frame-def) - (car frame-def) - frame-def)))) + (if (list? frame-def) + (car frame-def) + frame-def))) (define (frame->duration frames frame-idx) (let ((frame-def (frame-by-idx frames frame-idx))) @@ -42,7 +42,7 @@ (define (animation-by-name animations name) - (let ((matching-anims (filter (lambda (anim) (eq? (get-keyword #:name anim) 'walk)) animations))) + (let ((matching-anims (filter (lambda (anim) (eq? (get-keyword #:name anim) name)) animations))) (if matching-anims (car matching-anims) #f))) @@ -68,10 +68,16 @@ (list (cons #:anim-tick tick) (cons #:tile-id (frame->tile-id frames frame))))))) - (define (animate-entity entity animations) - (let* ((anim-name (entity-ref entity #:anim-name #f)) - (anim (and anim-name (animation-by-name animations anim-name)))) - (if anim - (advance-animation entity anim) - entity))) +(define (animate-entity entity animations) + (let* ((anim-name (entity-ref entity #:anim-name #f)) + (anim (and anim-name (animation-by-name animations anim-name)))) + (if anim + (advance-animation entity anim) + entity))) + +(define-pipeline (apply-animation animation) (scene entity dt) + guard: (entity-ref entity #:animations #f) + (let ((animations (entity-ref entity #:animations #f))) + (animate-entity entity animations))) + ) ;; End of animation module diff --git a/demo/sandbox.scm b/demo/sandbox.scm index 09c31fb..e23584f 100644 --- a/demo/sandbox.scm +++ b/demo/sandbox.scm @@ -140,7 +140,7 @@ (let ((scene (game-scene game))) (game-scene-set! game (scene-map-entities scene - (lambda (e) + (lambda (scene_ e) (if (eq? (entity-type e) 'demo-bot) (update-demo-bot e dt) e)))))))) diff --git a/demo/shmup.scm b/demo/shmup.scm index f4897ae..315069c 100644 --- a/demo/shmup.scm +++ b/demo/shmup.scm @@ -149,7 +149,7 @@ (game-scene-set! game (chain (update-scene scene entities: all) (scene-map-entities _ - (lambda (e) (if (eq? (entity-type e) 'player) e (move-projectile e)))) + (lambda (scene_ e) (if (eq? (entity-type e) 'player) e (move-projectile e)))) (scene-remove-dead _) (scene-filter-entities _ (lambda (e) (or (eq? (entity-type e) 'player) (in-bounds? e)))))))))) diff --git a/engine.scm b/engine.scm index 95d33b9..faa0909 100644 --- a/engine.scm +++ b/engine.scm @@ -7,6 +7,7 @@ (prefix sdl2-ttf "ttf:") (prefix sdl2-image "img:") (srfi 69) + (only srfi-197 chain) defstruct downstroke-world downstroke-input @@ -49,17 +50,19 @@ (define (default-engine-update game dt) (let ((scene (game-scene game))) (when scene - (let* ((scene (scene-map-entities scene (cut step-tweens <> scene dt))) - (scene (scene-map-entities scene (cut apply-acceleration <> scene dt))) - (scene (scene-map-entities scene (cut apply-gravity <> scene dt))) - (scene (scene-map-entities scene (cut apply-velocity-x <> scene dt))) - (scene (scene-map-entities scene (cut resolve-tile-collisions-x <> scene dt))) - (scene (scene-map-entities scene (cut apply-velocity-y <> scene dt))) - (scene (scene-map-entities scene (cut resolve-tile-collisions-y <> scene dt))) - (scene (scene-map-entities scene (cut detect-on-solid <> scene dt))) - (scene (scene-transform-entities scene resolve-entity-collisions)) - (scene (scene-transform-entities scene sync-groups))) - (game-scene-set! game scene))))) + (game-scene-set! + game + (chain scene + (scene-map-entities _ (cut step-tweens <> <> dt)) + (scene-map-entities _ (cut apply-acceleration <> <> dt)) + (scene-map-entities _ (cut apply-gravity <> <> dt)) + (scene-map-entities _ (cut apply-velocity-x <> <> dt)) + (scene-map-entities _ (cut resolve-tile-collisions-x <> <> dt)) + (scene-map-entities _ (cut apply-velocity-y <> <> dt)) + (scene-map-entities _ (cut resolve-tile-collisions-y <> <> dt)) + (scene-map-entities _ (cut detect-on-solid <> <> dt)) + (scene-transform-entities _ resolve-entity-collisions) + (scene-transform-entities _ sync-groups)))))) (define (make-game #!key (title "Downstroke Game") diff --git a/entity.scm b/entity.scm index 16d28c4..a655169 100644 --- a/entity.scm +++ b/entity.scm @@ -29,7 +29,7 @@ (define (entity-set-many entity pairs) (fold (lambda (pair working-ent) - (entity-set working-ent (car pair) (cdr pair))) + (entity-set working-ent (car pair) (if (list? (cdr pair)) (cadr pair) (cdr pair)))) entity pairs)) @@ -59,7 +59,7 @@ (name (car name-skip)) (skip (cadr name-skip)) (formals (caddr form)) - (f1 (car formals)) + (f1 (cadr formals)) (rest (cdddr form)) (has-guard? (and (pair? rest) (pair? (cdr rest)) (eq? (car rest) guard:))) diff --git a/physics.scm b/physics.scm index d418539..b95fc86 100644 --- a/physics.scm +++ b/physics.scm @@ -32,25 +32,25 @@ ;; for #:skip-pipelines symbol names). ;; Consume #:ay into #:vy and clear it (one-shot acceleration) - (define-pipeline (apply-acceleration acceleration) (entity scene dt) + (define-pipeline (apply-acceleration acceleration) (scene entity dt) guard: (entity-ref entity #:gravity? #f) (let ((ay (entity-ref entity #:ay 0)) (vy (entity-ref entity #:vy 0))) (entity-set (entity-set entity #:vy (+ vy ay)) #:ay 0))) ;; Apply gravity to an entity if it has gravity enabled - (define-pipeline (apply-gravity gravity) (entity scene dt) + (define-pipeline (apply-gravity gravity) (scene entity dt) guard: (entity-ref entity #:gravity? #f) (entity-set entity #:vy (+ (entity-ref entity #:vy) *gravity*))) ;; Update entity's x by its vx velocity - (define-pipeline (apply-velocity-x velocity-x) (entity scene dt) + (define-pipeline (apply-velocity-x velocity-x) (scene entity dt) (let ((x (entity-ref entity #:x 0)) (vx (entity-ref entity #:vx 0))) (entity-set entity #:x (+ x vx)))) ;; Update entity's y by its vy velocity - (define-pipeline (apply-velocity-y velocity-y) (entity scene dt) + (define-pipeline (apply-velocity-y velocity-y) (scene entity dt) (let ((y (entity-ref entity #:y 0)) (vy (entity-ref entity #:vy 0))) (entity-set entity #:y (+ y vy)))) @@ -128,7 +128,7 @@ (entity-tile-cells entity tilemap))))) ;; Resolve horizontal collisions with solid tiles - (define-pipeline (resolve-tile-collisions-x tile-collisions-x) (entity scene dt) + (define-pipeline (resolve-tile-collisions-x tile-collisions-x) (scene entity dt) guard: (scene-tilemap scene) (let* ((tilemap (scene-tilemap scene)) (w (entity-ref entity #:width 0)) @@ -137,7 +137,7 @@ (lambda (v col row) (tile-push-pos v col tw w))))) ;; Resolve vertical collisions with solid tiles - (define-pipeline (resolve-tile-collisions-y tile-collisions-y) (entity scene dt) + (define-pipeline (resolve-tile-collisions-y tile-collisions-y) (scene entity dt) guard: (scene-tilemap scene) (let* ((tilemap (scene-tilemap scene)) (h (entity-ref entity #:height 0)) @@ -179,7 +179,7 @@ (or (not (zero? (tilemap-tile-at tilemap col-left row))) (not (zero? (tilemap-tile-at tilemap col-right row)))))) - (define-pipeline (detect-on-solid on-solid) (entity scene dt) + (define-pipeline (detect-on-solid on-solid) (scene entity dt) guard: (entity-ref entity #:gravity? #f) (let* ((tilemap (scene-tilemap scene)) (on-tile? (and tilemap (tile-ground-below? entity tilemap))) diff --git a/prefabs.scm b/prefabs.scm index 5ae1255..819a382 100644 --- a/prefabs.scm +++ b/prefabs.scm @@ -14,7 +14,7 @@ (define (engine-mixins) '((physics-body #:vx 0 #:vy 0 #:ay 0 #:gravity? #t #:solid? #t #:on-ground? #f) (has-facing #:facing 1) - (animated #:anim-name idle #:anim-frame 0 #:anim-tick 0 #:tile-id 0))) + (animated #:anim-name idle #:anim-frame 0 #:anim-tick 0 #:tile-id 0 #:animations #t))) ;; Compose a prefab entry with mixin table ;; Returns (name . merged-plist) @@ -79,11 +79,11 @@ (define (load-prefabs file engine-mixin-table user-hooks) (let* ((data (with-input-from-file file read)) - (mixin-section (cdr (assq 'mixins data))) + (mixin-section (if (assq 'mixins data) (cdr (assq 'mixins data)) '())) (prefab-section (cdr (assq 'prefabs data))) (group-section (cond ((assq 'group-prefabs data) => cdr) (else '()))) ;; user mixins first → user wins on assq lookup (overrides engine mixin by name) - (user-mixin-table (map (lambda (m) (cons (car m) (cdr m))) mixin-section)) + (user-mixin-table (if (null? mixin-section) '() (map (lambda (m) (cons (car m) (cdr m))) mixin-section))) (merged-mixin-table (append user-mixin-table engine-mixin-table)) ;; user-hooks first → user wins on assq lookup (overrides engine hooks by name) (hook-table (append user-hooks *engine-hooks*)) @@ -109,9 +109,7 @@ (let ((entry (assq type (prefab-registry-prefabs registry)))) (if (not entry) #f - ;; instance fields prepended → highest priority - (let* ((base (append (make-entity x y w h) - (cdr entry))) + (let* ((base (append (cdr entry) (make-entity x y w h))) (hook-val (entity-ref base #:on-instantiate #f)) (handler (cond diff --git a/tests/animation-test.scm b/tests/animation-test.scm index aaaba41..9a71dec 100644 --- a/tests/animation-test.scm +++ b/tests/animation-test.scm @@ -9,13 +9,13 @@ (test-group "frame->tile-id" (test-group "tile IDs only" - (test-equal "first frame, frames (0)" 1 (frame->tile-id '(0) 0)) - (test-equal "wraps around" 1 (frame->tile-id '(0 1) 2)) - (test-equal "frame 1 of (27 28)" 29 (frame->tile-id '(27 28) 1))) + (test-equal "first frame, frames (0)" 0 (frame->tile-id '(0) 0)) + (test-equal "wraps around" 0 (frame->tile-id '(0 1) 2)) + (test-equal "frame 1 of (27 28)" 28 (frame->tile-id '(27 28) 1))) (test-group "tile IDs and durations" - (test-equal "first frame, frames (0)" 1 (frame->tile-id '((0 10)) 0)) - (test-equal "wraps around" 1 (frame->tile-id '((0 10) (1 10)) 2)) - (test-equal "frame 1 of (27 28)" 29 (frame->tile-id '((27 10) (28 10)) 1)))) + (test-equal "first frame, frames (0)" 0 (frame->tile-id '((0 10)) 0)) + (test-equal "wraps around" 0 (frame->tile-id '((0 10) (1 10)) 2)) + (test-equal "frame 1 of (27 28)" 28 (frame->tile-id '((27 10) (28 10)) 1)))) (test-group "frame->duration" (test-equal "first frame, frames (0)" 100 (frame->duration '((0 100)) 0)) @@ -31,17 +31,36 @@ (test-equal "resets tick" 0 (entity-ref switched #:anim-tick))))) (test-group "animate-entity" - (let* ((anims '((#:name walk #:frames (0 1) #:duration 4))) - (entity (list #:type 'player #:anim-name 'walk #:anim-frame 0 #:anim-tick 0)) - (stepped (animate-entity entity anims))) - (test-equal "increments tick" 1 (entity-ref stepped #:anim-tick)) - (test-equal "sets tile-id on first tick" 1 (entity-ref stepped #:tile-id))) - (let* ((anims '((#:name walk #:frames (0 1) #:duration 2))) - (entity (list #:type 'player #:anim-name 'walk #:anim-frame 0 #:anim-tick 1)) - (advanced (animate-entity entity anims))) - (test-equal "advances frame when tick reaches duration" 1 (entity-ref advanced #:anim-frame)) - (test-equal "resets tick on frame advance" 0 (entity-ref advanced #:anim-tick))) - (let* ((entity (list #:type 'player))) - (test-equal "unchanged entity without anim-name" entity (animate-entity entity '())))) + (test-group "Single frames" + (let* ((anims '((#:name walk #:frames (2 3) #:duration 4))) + (entity (list #:type 'player #:anim-name 'walk #:anim-frame 0 #:anim-tick 0)) + (stepped (animate-entity entity anims))) + (test-equal "increments tick" 1 (entity-ref stepped #:anim-tick)) + (test-equal "sets tile-id on first tick" 2 (entity-ref stepped #:tile-id))) + (let* ((anims '((#:name walk #:frames (0 1) #:duration 2))) + (entity (list #:type 'player #:anim-name 'walk #:anim-frame 0 #:anim-tick 1)) + (advanced (animate-entity entity anims))) + (test-equal "advances frame when tick reaches duration" 1 (entity-ref advanced #:anim-frame)) + (test-equal "resets tick on frame advance" 0 (entity-ref advanced #:anim-tick)))) + (test-group "Frames with duration" + (let* ((anims '((#:name walk #:frames ((0 10) (1 20)) #:duration 4))) + (entity (list #:type 'player #:anim-name 'walk #:anim-frame 0 #:anim-tick 9)) + (stepped (animate-entity entity anims))) + (test-equal "ticks resets on frame switch" 0 (entity-ref stepped #:anim-tick)) + (test-equal "sets tile-id on 10th tick" 1 (entity-ref stepped #:tile-id)) + (test-equal "sets duration to frame duration" 20 (entity-ref stepped #:duration)))) + (test-group "Empty" + (let* ((entity (list #:type 'player))) + (test-equal "unchanged entity without anim-name" entity (animate-entity entity '()))))) + +(test-group "animation pipeline" + (test-group "animated entity" + (let* ((anims '((#:name walk #:frames (2 3) #:duration 4))) + (entity (list #:type 'player #:anim-name 'walk #:anim-frame 0 #:anim-tick 0 #:animations anims)) + (stepped-entity (apply-animation entity #f 10))) + (test-equal "Updated animated entity" 1 (entity-ref stepped-entity #:anim-tick))) + (let* ((entity (list #:type 'static)) + (stepped-entity (apply-animation entity #f 10))) + (test-equal "unchanged static entity" #f (entity-ref stepped-entity #:anim-tick))))) (test-end "animation") diff --git a/tests/entity-test.scm b/tests/entity-test.scm index 9c7607c..3b83dff 100644 --- a/tests/entity-test.scm +++ b/tests/entity-test.scm @@ -82,12 +82,12 @@ (test-group "entity-set-many" (test-group "Set multiple entities with lists" (let ((e (entity-set-many '(#:x 10 #:y 20) '((#:x 15) (#:y 25))))) - (test-equal "value x updated" (entity-ref e #:x) 15) - (test-equal "value y updated" (entity-ref e #:y) 25))) + (test-equal "value x updated" 15 (entity-ref e #:x)) + (test-equal "value y updated" 25 (entity-ref e #:y)))) (test-group "Set multiple entities with cons" (let ((e (entity-set-many '(#:x 10 #:y 20) (list (cons #:x 15) (cons #:y 25))))) - (test-equal "value x updated" (entity-ref e #:x) 15) - (test-equal "value y updated" (entity-ref e #:y) 25)))) + (test-equal "value x updated" 15 (entity-ref e #:x)) + (test-equal "value y updated" 25 (entity-ref e #:y))))) ;; Test: entity-update applies transformations (test-group "entity-update" @@ -118,7 +118,7 @@ (test-assert "not member" (not (entity-skips-pipeline? '(#:skip-pipelines (gravity)) 'velocity-x)))) -(define-pipeline (fixture-pipeline fixture-skip) (ent) +(define-pipeline (fixture-pipeline fixture-skip) (scene_ ent) (entity-set ent #:x 42)) (test-group "define-pipeline" @@ -127,7 +127,7 @@ (let ((e '(#:type t #:x 0 #:skip-pipelines (fixture-skip)))) (test-equal "skipped" 0 (entity-ref (fixture-pipeline e) #:x)))) -(define-pipeline (guarded-pipeline guarded-skip) (ent) +(define-pipeline (guarded-pipeline guarded-skip) (scene_ ent) guard: (entity-ref ent #:active? #f) (entity-set ent #:x 99)) diff --git a/tests/input-test.scm b/tests/input-test.scm index 44af6e8..0d1e4b5 100644 --- a/tests/input-test.scm +++ b/tests/input-test.scm @@ -117,6 +117,11 @@ #f (input-pressed? state3 'up))))) +(define (make-physics-entity) + (entity-set-many (make-entity 0 0 16 16) + '((#:vx 0) (#:vy 0) + (#:input-map ((left . (-2 . 0)) (right . (2 . 0))))))) + ;; Test: apply-input-to-entity applies input to entity (test-group "apply-input-to-entity" (test-group "no input-map: entity unchanged" @@ -125,35 +130,35 @@ (test-equal "entity returned as-is" e out))) (test-group "no actions held: velocity is zero" - (let* ((e (make-entity 0 0 16 16)) + (let* ((e (make-physics-entity)) (out (apply-input-to-entity e (lambda (a) #f)))) (test-equal "vx is 0" 0 (entity-ref out #:vx)) (test-equal "vy is 0" 0 (entity-ref out #:vy)))) (test-group "right held: vx=2 vy=0" - (let* ((e (make-entity 0 0 16 16)) + (let* ((e (make-physics-entity)) (out (apply-input-to-entity e (lambda (a) (eq? a 'right))))) (test-equal "vx is 2" 2 (entity-ref out #:vx)) (test-equal "vy is 0" 0 (entity-ref out #:vy)))) (test-group "right+down held: vx=2 vy unchanged" - (let* ((e (make-entity 0 0 16 16)) + (let* ((e (make-physics-entity)) (out (apply-input-to-entity e (lambda (a) (memv a '(right down)))))) (test-equal "vx is 2" 2 (entity-ref out #:vx)) (test-equal "vy is unchanged (input handler does not set vy)" 0 (entity-ref out #:vy)))) (test-group "right held: facing set to 1" - (let* ((e (make-entity 0 0 16 16)) + (let* ((e (make-physics-entity)) (out (apply-input-to-entity e (lambda (a) (eq? a 'right))))) (test-equal "facing is 1" 1 (entity-ref out #:facing 0)))) (test-group "left held: facing set to -1" - (let* ((e (make-entity 0 0 16 16)) + (let* ((e (make-physics-entity)) (out (apply-input-to-entity e (lambda (a) (eq? a 'left))))) (test-equal "facing is -1" -1 (entity-ref out #:facing 0)))) (test-group "no key held: facing retains previous value" - (let* ((e (entity-set (make-entity 0 0 16 16) #:facing 1)) + (let* ((e (entity-set (make-physics-entity) #:facing 1)) (out (apply-input-to-entity e (lambda (a) #f)))) (test-equal "facing stays 1 when vx=0" 1 (entity-ref out #:facing 0))))) diff --git a/tests/world-test.scm b/tests/world-test.scm index 557a121..bfbb336 100644 --- a/tests/world-test.scm +++ b/tests/world-test.scm @@ -110,7 +110,7 @@ ;; Test: scene with entities and tilemap (test-group "scene-with-data" - (let* ((player (make-entity 100 100 16 16)) + (let* ((player (entity-set (make-entity 100 100 16 16) #:type 'player)) (enemy '(#:type enemy #:x 200 #:y 200)) (tilemap "mock-tilemap") (scene (make-scene entities: (list player enemy) @@ -159,7 +159,7 @@ (let* ((e1 '(#:type player #:x 100 #:y 100)) (e2 '(#:type enemy #:x 200 #:y 200)) (scene (make-scene entities: (list e1 e2) tilemap: #f camera-target: #f)) - (move-right (lambda (entity) + (move-right (lambda (scene entity) (let ((x (entity-ref entity #:x)) (y (entity-ref entity #:y)) (type (entity-ref entity #:type))) @@ -184,7 +184,7 @@ (let* ((e1 '(#:type player #:x 100)) (e2 '(#:type enemy #:x 200)) (scene (make-scene entities: (list e1 e2) tilemap: #f camera-target: #f)) - (scene2 (scene-map-entities scene (lambda (e) e)))) + (scene2 (scene-map-entities scene (lambda (scene e) e)))) (test-equal "entity count unchanged" 2 (length (scene-entities scene2))) (test-equal "first entity unchanged" @@ -200,7 +200,7 @@ (test-equal "entity added" 1 (length (scene-entities scene))) (let ((scene (scene-map-entities scene - (lambda (e) + (lambda (scene e) (let ((x (entity-ref e #:x)) (y (entity-ref e #:y)) (type (entity-type e))) diff --git a/tween.scm b/tween.scm index 64ed05e..2dd0a61 100644 --- a/tween.scm +++ b/tween.scm @@ -196,7 +196,7 @@ ;; per-entity pipeline, e.g. (step-tweens entity dt). Removes #:tween ;; when the tween finishes. - (define-pipeline (step-tweens tweens) (entity scene dt) + (define-pipeline (step-tweens tweens) (scene entity dt) guard: (entity-ref entity #:tween #f) (let ((tw (entity-ref entity #:tween))) (receive (tw2 ent2) (tween-step tw entity dt) diff --git a/world.scm b/world.scm index d09b9c9..0726eea 100644 --- a/world.scm +++ b/world.scm @@ -49,9 +49,11 @@ (define (scene-map-entities scene . procs) "Apply each proc in sequence to the scene's entities; returns a new scene." (update-scene scene - entities: (fold (lambda (proc es) (map proc es)) - (scene-entities scene) - procs))) + entities: (fold + (lambda (proc es) + (map (cut proc scene <>) es)) + (scene-entities scene) + procs))) (define (scene-filter-entities scene pred) "Keep only entities satisfying pred; returns a new scene." -- cgit v1.2.3