aboutsummaryrefslogtreecommitdiff
path: root/physics.scm
blob: 627dbea78d8d42351d5f30bb80bda91118ca9af2 (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
(module downstroke-physics *
  (import scheme
      (chicken base)
      (chicken keyword)
      (only srfi-1 fold iota)
      defstruct
      downstroke-tilemap
      downstroke-entity
      downstroke-world
      simple-logger)

  ;; Gravity constant: pixels per frame per frame
  (define *gravity* 1)

  ;; Jump force: vertical acceleration applied on jump (one frame)
  (define *jump-force* 15)

  ;; Consume #:ay into #:vy and clear it (one-shot acceleration)
  (define (apply-acceleration entity)
    (if (not (entity-ref entity #:gravity? #f))
        entity
        (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 (apply-gravity entity)
    (if (entity-ref entity #:gravity? #f)
        (entity-set entity #:vy (+ (entity-ref entity #:vy) *gravity*))
        entity))

  ;; Update entity's x by its vx velocity
  (define (apply-velocity-x entity)
    "Update entity's x by its vx velocity."
    (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 (apply-velocity-y entity)
    "Update entity's y by its vy velocity."
    (let ((y (entity-ref entity #:y 0))
          (vy (entity-ref entity #:vy 0)))
      (entity-set entity #:y (+ y vy))))

  ;; Legacy function: update both x and y by velocities
  (define (apply-velocity entity)
    "Legacy function: update both x and y by velocities."
    (let ((x  (entity-ref entity #:x 0))
          (y  (entity-ref entity #:y 0))
          (vx (entity-ref entity #:vx 0))
          (vy (entity-ref entity #:vy 0)))
      (entity-set (entity-set entity #:x (+ x vx)) #:y (+ y vy))))

  ;; Build list of (col . row) pairs to check for collisions
  (define (build-cell-list col-start col-end row-start row-end)
    (let loop ((col col-start) (row row-start) (acc '()))
      (log-debug "Build-cell-list loop with: ~a" (list col row acc))
      (if (> col col-end)
          (if (>= row row-end)
              (reverse acc)
              (loop col-start (+ row 1) acc))
          (loop (+ col 1) row (cons (cons col row) acc)))))

  ;; Convert a pixel coordinate to a tile grid index
  (define (pixel->tile pixel tile-size)
    (inexact->exact (floor (/ pixel tile-size))))

  ;; Return all tile cells (col . row) overlapping the entity's bounding box
  (define (entity-tile-cells entity tilemap)
    (let ((x  (entity-ref entity #:x 0))
          (y  (entity-ref entity #:y 0))
          (w  (entity-ref entity #:width 0))
          (h  (entity-ref entity #:height 0))
          (tw (tilemap-tilewidth tilemap))
          (th (tilemap-tileheight tilemap)))
      (build-cell-list
        (pixel->tile x tw)
        (pixel->tile (- (+ x w) 1) tw)
        (pixel->tile y th)
        (pixel->tile (- (+ y h) 1) th))))

  ;; Snap position to the near or far edge of a tile after collision.
  ;; Moving forward (v>0): snap entity's leading edge to tile's near edge.
  ;; Moving backward (v<0): snap entity's trailing edge to tile's far edge.
  (define (tile-push-pos v coord tile-size entity-size)
    (if (> v 0)
        (- (* coord tile-size) entity-size)
        (* (+ coord 1) tile-size)))

  ;; Resolve collisions with tiles along a single axis.
  ;; push-fn: (v col row) -> new-pos
  ;; For v>0 (moving right/down): snap to the FIRST solid cell (shallowest penetration).
  ;; For v<0 (moving left/up):   snap to the LAST  solid cell (deepest penetration from above/left).
  (define (resolve-tile-collisions-axis entity tilemap vel-key pos-key push-fn)
    (let ((v (entity-ref entity vel-key 0)))
      (if (zero? v)
          entity
          (fold (lambda (cell acc)
                  (log-debug "resolve-~a: cell=~a acc=~a" vel-key cell acc)
                  (let* ((col     (car cell))
                         (row     (cdr cell))
                         (tile-id (tilemap-tile-at tilemap col row)))
                    (if (zero? tile-id)
                        acc
                        (if (and (> v 0) (zero? (entity-ref acc vel-key v)))
                            acc  ; v>0: first collision already resolved, don't overwrite
                            (entity-set (entity-set acc pos-key (push-fn v col row)) vel-key 0)))))
                entity
                (entity-tile-cells entity tilemap)))))

  ;; Resolve horizontal collisions with solid tiles
  (define (resolve-tile-collisions-x entity tilemap)
    (let ((w  (entity-ref entity #:width 0))
          (tw (tilemap-tilewidth tilemap)))
      (resolve-tile-collisions-axis entity tilemap #:vx #:x
        (lambda (v col row) (tile-push-pos v col tw w)))))

  ;; Resolve vertical collisions with solid tiles
  (define (resolve-tile-collisions-y entity tilemap)
    (let ((h  (entity-ref entity #:height 0))
          (th (tilemap-tileheight tilemap)))
      (resolve-tile-collisions-axis entity tilemap #:vy #:y
        (lambda (v col row) (tile-push-pos v row th h)))))

  ;; Detect if entity is standing on ground by probing 1px below feet
  (define (detect-ground entity tilemap)
    (if (not (entity-ref entity #:gravity? #f))
        entity
        (let* ((x       (entity-ref entity #:x 0))
               (w       (entity-ref entity #:width 0))
               (tw      (tilemap-tilewidth tilemap))
               (th      (tilemap-tileheight tilemap))
               (probe-y (+ (entity-ref entity #:y 0) (entity-ref entity #:height 0) 1))
               (row       (pixel->tile probe-y th))
               (col-left  (pixel->tile x tw))
               (col-right (pixel->tile (- (+ x w) 1) tw))
               (on-ground? (or (not (zero? (tilemap-tile-at tilemap col-left row)))
                               (not (zero? (tilemap-tile-at tilemap col-right row))))))
          (entity-set entity #:on-ground? on-ground?))))

  ;; Set vertical acceleration for jump (consumed next frame by apply-acceleration)
  (define (apply-jump entity jump-pressed?)
    "Set #:ay to jump force if jump pressed and entity is on ground."
    (if (and jump-pressed? (entity-ref entity #:on-ground? #f))
        (entity-set entity #:ay (- (entity-ref entity #:jump-force *jump-force*)))
        entity))

  ;; Replace element at idx in lst with val
  (define (list-set lst idx val)
    (let loop ((lst lst) (i 0) (acc '()))
      (if (null? lst)
          (reverse acc)
          (loop (cdr lst) (+ i 1)
                (cons (if (= i idx) val (car lst)) acc)))))

  ;; Generate all unique (i . j) index pairs where i < j
  (define (index-pairs n)
    (if (< n 2) '()
        (apply append
               (map (lambda (i)
                      (map (lambda (j) (cons i j))
                           (iota (- n i 1) (+ i 1))))
                    (iota (- n 1))))))

  (define (axis->dimension axis)
    (case axis
      ((#:x) #:width)
      ((#:y) #:height)))

  (define (axis->velocity axis)
    (case axis
      ((#:x) #:vx)
      ((#:y) #:vy)))

  ;; Push entity along one axis by half-overlap, setting velocity in push direction
  (define (push-entity entity pos-key vel-key pos overlap sign)
    (entity-set (entity-set entity pos-key (+ pos (* sign (/ overlap 2)))) vel-key sign))


  (define (entity-center-on-axis entity axis)
    (let ((dimension (axis->dimension axis)))
      (+ (entity-ref entity axis 0) (/ (entity-ref entity dimension 0) 2))))

  (define (aabb-overlap-on-axis axis a b)
    (let ((dimension (axis->dimension axis)))
      (- (/ (+ (entity-ref a dimension 0) (entity-ref b dimension 0)) 2)
         (abs (- (entity-center-on-axis b axis) (entity-center-on-axis a axis))))))

  (define (push-along-axis axis a b overlap)
    (let* ((a-center (entity-center-on-axis a axis))
           (b-center (entity-center-on-axis b axis))
           (delta (if (< a-center b-center) -1 1))
           (axis-velocity-key (axis->velocity axis)))
      (cons (push-entity a axis axis-velocity-key (entity-ref a axis 0) overlap delta)
            (push-entity b axis axis-velocity-key (entity-ref b axis 0) overlap (- delta)))))

  ;; Push two overlapping entities apart along the minimum penetration axis.
  ;; Returns (a2 . b2) with updated positions and velocities.
  (define (push-apart a b)
    (let* ((ovx (aabb-overlap-on-axis #:x a b))
           (ovy (aabb-overlap-on-axis #:y a b)))
      (if (<= ovx ovy)
          (push-along-axis #:x a b ovx)
          (push-along-axis #:y a b ovy))))

  ;; Check if two axis-aligned bounding boxes overlap.
  ;; Returns #t if they overlap, #f if they don't (including edge-touching).
  (define (aabb-overlap? x1 y1 w1 h1 x2 y2 w2 h2)
    (not (or (>= x1 (+ x2 w2))
             (>= x2 (+ x1 w1))
             (>= y1 (+ y2 h2))
             (>= y2 (+ y1 h1)))))

  ;; Resolve AABB collision between two solid entities.
  ;; Returns (a2 . b2) with positions/velocities adjusted, or #f if no collision.
  (define (resolve-pair a b)
    (and (entity-ref a #:solid? #f)
         (entity-ref b #:solid? #f)
         (aabb-overlap? (entity-ref a #:x 0) (entity-ref a #:y 0)
                        (entity-ref a #:width 0) (entity-ref a #:height 0)
                        (entity-ref b #:x 0) (entity-ref b #:y 0)
                        (entity-ref b #:width 0) (entity-ref b #:height 0))
         (push-apart a b)))

  ;; Detect and resolve AABB overlaps between all pairs of solid entities.
  ;; Returns a new entity list with collisions resolved.
  (define (resolve-entity-collisions entities)
    (fold (lambda (pair ents)
            (let* ((i (car pair)) (j (cdr pair))
                   (result (resolve-pair (list-ref ents i) (list-ref ents j))))
              (if result
                  (list-set (list-set ents i (car result)) j (cdr result))
                  ents)))
          entities
          (index-pairs (length entities))))

  ;; Wrapper for scene-resolve-collisions
  (define (scene-resolve-collisions scene)
    (scene-entities-set! scene
      (resolve-entity-collisions (scene-entities scene)))
    scene))