aboutsummaryrefslogtreecommitdiff
path: root/src/enemies.s
blob: 081ea50f6723ab8e61e7f8ae35f0197b7e21adce (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
.segment "CODE"

;; Assuming that the 'x' register indexes an enemy on its pool, increment the
;; register as many times as to point to the next one. Bound checking is not
;; performed, it's up to the caller to implement that.
.macro NEXT_ENEMY_INDEX_X
    inx
    inx
    inx
    inx
.endmacro

.scope Enemies
    ;; Maximum amount of enemies allowed on screen at the same time.
    ENEMIES_POOL_CAPACITY = 3

    ;; The capacity of the enemies pool in bytes.
    ENEMIES_POOL_CAPACITY_BYTES = ENEMIES_POOL_CAPACITY * 4

    ;; Initial X coordinates for enemies depending on if they appear on the
    ;; left/right edge of the screen.
    ENEMIES_INITIAL_X       = $F0
    ENEMIES_INITIAL_X_RIGHT = $10

    ;; Base address for the pool of enemies used on this game. The pool has
    ;; #ENEMIES_POOL_CAPACITY capacity of enemy objects where each one is 4
    ;; bytes long:
    ;;  1. State: which can have two formats:
    ;;     - $FF: the enemy is not active.
    ;;     - |DIxx|xxxx|: where D is the direction bit (1: right; 0: left); and
    ;;                    the rest of bits count the number of moves from this
    ;;                    enemy. This is used to account for the inner movement
    ;;                    from an enemy sprite and, in fact, is initialized at
    ;;                    random. This counter is split in two phases depending
    ;;                    on the value of I. If I=0, then the enemy is at its
    ;;                    first inner movement state; and if I=1, then the enemy
    ;;                    is at the other inner movement state. Last but not
    ;;                    least, if D=1 and I=1, then the counter never reaches
    ;;                    the limit, as that would make the value $FF (inactive).
    ;;  2. Y coordinate.
    ;;  3. X coordinate.
    ;;  4. 'extra' state: depends on the enemy type.
    zp_enemies_pool_base = $60  ; asan:reserve ENEMIES_POOL_CAPACITY_BYTES

    ;; The current size of active enemies. That is, one thing is the capacity of
    ;; the pool, and another is what's the number of enemies on screen.
    zp_enemies_pool_size = $D0

    ;; Base index of the enemy tiles in 'tiles' to be used. Whether to use one
    ;; row or the other for a given enemy is to be decided by its current state.
    zp_enemy_tiles = $D1

    ;; Pointer to the function that handles movement for the current enemy
    ;; type. Using a function pointer is a bit tricky on the humble 6502's
    ;; architecture, as you need to do indirect jumps with possible optimisation
    ;; tricks along the way. But there are really too many different enemy
    ;; algorithms that a plain if-else + jsr code flow would be too expensive
    ;; and harder to read.
    zp_enemy_movement_fn = $D2  ; asan:reserve $02

    ;; Preserves the index on 'zp_enemies_pool_base' for a given enemy inside of
    ;; the movement handler. Check the documentation on movement handlers.
    zp_pool_index = $D4

    ;; An extra argument that enemies can have depending on their type. This is
    ;; useful for different waves with the same algorithm but different speeds.
    zp_enemy_arg = $D5

    ;; Values for the counter of enemies that fall.
    FALLING_VELOCITY      = HZ / 10
    FALLING_VELOCITY_FAST = FALLING_VELOCITY / 2

    ;; Initializes all the enemies for the current level. That is, it prepares
    ;; all the movement handlers, the enemy tiles to be used, and initializes
    ;; the pool of objects for it.
    .proc init
        lda Globals::zp_level_kind
        tax

        ;; Pick the right index for this type.
        asl
        asl
        asl
        asl
        sta zp_enemy_tiles

        ;; And set the movement function for this type.
        lda movement_lo, x
        sta zp_enemy_movement_fn
        lda movement_hi, x
        sta zp_enemy_movement_fn + 1

        txa
        beq @init_zero

        ;; TODO: rest of the enemies. For now this is only true for the 'basic' movement.
        lda #2
        sta Enemies::zp_enemy_arg
        lda #FALLING_VELOCITY_FAST
        bne @set
    @init_zero:
        lda #1
        sta Enemies::zp_enemy_arg
        lda #FALLING_VELOCITY

    @set:
        ;; The 'init_pool' wants an argument which is the 'extra' state to be
        ;; set up for all enemies of the pool.
        sta Globals::zp_arg0

        __fallthrough__ init_pool
    .endproc

    ;; Initializes the enemy pool for this game. It requires an argument to be
    ;; passed in 'Globals::zp_arg0' which contains the 'extra' state to be
    ;; passed to all enemies of the pool.
    .proc init_pool
        ldx #0

        ldy #ENEMIES_POOL_CAPACITY
    @enemies_init_loop:
        ;; The state is set at random.
        stx Globals::zp_tmp0
        jsr Prng::random_valid_y_coordinate
        ldx Globals::zp_tmp0
        sta zp_enemies_pool_base, x
        sta Globals::zp_tmp1

        ;; The Y coordinate is also set at random within the bounds of the
        ;; playable screen.
        jsr Prng::random_valid_y_coordinate
        ldx Globals::zp_tmp0
        inx
        sta zp_enemies_pool_base, x

        ;; The initial X position is based on whether it's facing left or right.
        inx
        bit Globals::zp_tmp1
        bmi @facing_right
        lda #ENEMIES_INITIAL_X
        bne @set_x_position
    @facing_right:
        lda #ENEMIES_INITIAL_X_RIGHT
    @set_x_position:
        sta zp_enemies_pool_base, x

        ;; And set the 'extra' state as passed down by the 'init' function.
        inx
        lda Globals::zp_arg0
        sta zp_enemies_pool_base, x

        ;; Next enemy!
        inx
        dey
        bne @enemies_init_loop

        ;; The initial size of the pool is its whole capacity.
        lda #ENEMIES_POOL_CAPACITY
        sta zp_enemies_pool_size

        rts
    .endproc

    ;; Update the state and movement of all active enemies.
    ;;
    ;; NOTE: this function does not do collision checking with bullets as
    ;; 'Bullets::update' already accounts for it and we assume that it ran
    ;; before this one.
    .proc update
        ldx #252

        ;; The loop index will be moved out of the 'y' register since movement
        ;; handlers might need to use it.
        ldy zp_enemies_pool_size
        sty Globals::zp_idx

        ;; In the (unlikely) case that there are no enemies left, just skip
        ;; 'update' altogether.
        bne @loop
        rts

    @loop:
        ;; Move the 'x' register to the current enemy for this iteration.
        NEXT_ENEMY_INDEX_X

        ;; Is the current enemy marked as invalid? If so just skip it. Note that
        ;; we don't even go to the '@next' down below, as that would decrease
        ;; the loop counter and this loop only cares about active
        ;; enemies. Having an enemy in the middle of the pool invalid is totally
        ;; valid as it could have died before assigning a new one.
        lda zp_enemies_pool_base, x
        cmp #$FF
        beq @loop

        ;; If its movement state is already at the maximum, reset it, otherwise
        ;; increase it by 1. Note that we compare with $7E instead of $7F
        ;; because the latter would be equal to $FF if we accounted for the
        ;; direction bit and it could be confused with the "invalid"
        ;; state. Hence, the second phase of the inner movement has one frame
        ;; less of time than the other, but whatever. We also 'and' it with $7E
        ;; to avoid the direction bit to affect the comparison.
        sta Globals::zp_tmp0
        and #$7E
        cmp #$7E
        beq @reset
        inc zp_enemies_pool_base, x
        bne @move
    @reset:
        lda Globals::zp_tmp0
        and #$80
        sta zp_enemies_pool_base, x

    @move:
        ;; Store the index to the current enemy.
        stx Enemies::zp_pool_index

        ;; Jump to the movement handler for the current enemy. As to why this
        ;; needs to be in a function pointer, refer to
        ;; 'zp_enemy_movement_fn'. Note that this could've been done in other
        ;; ways. Here we fake a 'jsr' by pushing the address to return into the
        ;; stack (-1 to account for the 'rts' behavior of adding +1 to the PC),
        ;; and then calling the function pointed by 'zp_enemy_movement_fn'. Then
        ;; this function can act as usual and perform an 'rts' at the end.
        ;;
        ;; Since the return address is always the same, maybe the movement
        ;; handler could've done a 'jmp <fixed address>', but that would mean to
        ;; know the exact address for '@return_from_movement_handler', and that
        ;; would mean to move everything out of .proc and .scope. That would be
        ;; my way to go if performance was paramount at this point, as it would
        ;; save: (2 x lda's: 4 cycles; 2 x pha's: 6 cycles; 1 x rts: 6 cycles) =
        ;; 16 cycles - indirect jump from handler (5 cycles). Hence 11 cycles of
        ;; performance gain per iteration. We are not at the point of requiring
        ;; these cycles for now and, given the luxury, I take readability first.
        ;;
        ;; Another approach would be to introduce a "trampoline" function, but
        ;; that would be the same as here plus an extra 'jsr' to the trampoline
        ;; (and an extra cycle considering that the 'rts' at the trampoline is
        ;; slower than an indirect 'jmp'). Another approach would've been the
        ;; "rts trick", but I feel that it's only useful at the tail of a
        ;; function, and this whole ordeal is happening inside of a loop, so we
        ;; don't want to break it just yet.
        lda #.hibyte(@return_from_movement_handler - 1)
        pha
        lda #.lobyte(@return_from_movement_handler - 1)
        pha
        jmp (zp_enemy_movement_fn)

    @return_from_movement_handler:
        ;; Restore the value from the 'x' register.
        ldx Enemies::zp_pool_index

        ;; TODO: collision with player

    @next:
        ;; Any more enemies left?
        dec Globals::zp_idx
        bne @loop

        rts
    .endproc

    ;; Allocate an enemy indexed by 'x' from the `zp_enemies_pool_base` buffer,
    ;; and set it to OAM-reserved space indexed via 'y'.
    ;;
    ;; The 'y' register will be updated by increasing its value by 16,
    ;; indicating the amount of bytes allocated in OAM space.
    ;;
    ;; The 'x' register will be changed, so make sure to back it up if you care
    ;; about its value before calling this function.
    ;;
    ;; The 'Globals::zp_tmp0', 'Globals::zp_tmp1' and 'Globals::zp_tmp2' memory
    ;; regions are also tampered by this function.
    ;;
    ;; NOTE: this function assumes that the enemy is in a valid state. That's up
    ;; to the caller to check on this before calling this function.
    .proc allocate_x_y
        ;; Save the 'y' index, as it's faster to do funny address arithmetics
        ;; and add 16 in the end than constantly 'iny' every time in the right
        ;; order.
        sty Globals::zp_tmp0

        ;; Y coordinates for each sprite of the enemy.
        lda Enemies::zp_enemies_pool_base + 1, x
        sta OAM::m_sprites, y                       ; top left
        sta OAM::m_sprites + 4, y                   ; top right
        clc
        adc #8
        sta OAM::m_sprites + 8, y                   ; bottom left
        sta OAM::m_sprites + 12, y                  ; bottom right

        ;; The next thing to account is where the enemy is facing. This will
        ;; change the tile set to be picked (e.g. 1st/2nd vs 3rd/4th rows of
        ;; tile IDs definitions); but it also changes whether the enemy needs to
        ;; be horizontally mirrored by the PPU or not. For the logic we make use
        ;; of temporary memory regions that will help us along the way, and we
        ;; start like this.
        lda Enemies::zp_enemies_pool_base, x
        sta Globals::zp_tmp2
        stx Globals::zp_tmp1
        ldx zp_enemy_tiles

        ;; Check on the direction bit from the enemy's state. If facing right,
        ;; then the 'x' register will be increased by 8 (pointing then to the
        ;; 3rd/4th rows of the enemy tiles ID definitions), and 'a' will have
        ;; the value for the third byte of the sprite (i.e. whether to mirror or
        ;; not the sprite at the PPU level).
        bit Globals::zp_tmp2
        bmi @face_right
        lda #0
        beq @set_state
    @face_right:
        txa
        clc
        adc #8
        tax
        lda #%01000000
    @set_state:
        sta OAM::m_sprites + 2, y                   ; top left
        sta OAM::m_sprites + 6, y                   ; top right
        sta OAM::m_sprites + 10, y                  ; bottom left
        sta OAM::m_sprites + 14, y                  ; bottom right

        ;; If the counter for the enemy's state is already at its second phase,
        ;; increase 'x' by 4 to reflect that it needs to pick the "other" state
        ;; from the tiles ID definitions. Then load all four bytes of tile IDs
        ;; and store them appropiately.
        lda Globals::zp_tmp2
        and #$40
        beq @set_facing
        txa
        clc
        adc #4
        tax
    @set_facing:
        lda tiles, x
        sta OAM::m_sprites + 1, y                   ; top left
        lda tiles + 1, x
        sta OAM::m_sprites + 5, y                   ; top right
        lda tiles + 2, x
        sta OAM::m_sprites + 9, y                   ; bottom left
        lda tiles + 3, x
        sta OAM::m_sprites + 13, y                  ; bottom right

        ;; The Y-coordinate for each sprite.
        ldx Globals::zp_tmp1
        lda Enemies::zp_enemies_pool_base + 2, x    ; top left
        sta OAM::m_sprites + 3, y
        sta OAM::m_sprites + 11, y                  ; bottom left
        clc
        adc #8
        sta OAM::m_sprites + 7, y                   ; top right
        sta OAM::m_sprites + 15, y                  ; bottom right

        ;; And update the 'y' register to notify 16 bytes were stored.
        lda Globals::zp_tmp0
        clc
        adc #16
        tay

        rts
    .endproc

    ;; The enemy has been set to dust, remove it.
    .proc bite_the_dust
        dec Enemies::zp_enemies_pool_size

        ;; TODO: this assumes we are coming from within Enemies always. What
        ;; about impacting bullets?
        ldx Enemies::zp_pool_index

        ;; TODO: cloud animation and all that.
        lda #$FF
        sta Enemies::zp_enemies_pool_base, x

        rts
    .endproc

    ;;;
    ;; Movement handlers.
    ;;
    ;; Each enemy type has a function assigned to it as to how to move. These
    ;; functions are stored in the 'movement_lo' and 'movement_hi' ROM addresses
    ;; and they are used via the 'zp_enemy_movement_fn' function
    ;; pointer. Movement handlers are free to use any register and any memory
    ;; location, as that's handled by the caller.
    ;;
    ;; Collision only needs to be checked with platforms, as each handler might
    ;; have a different take on that scenario. Collision with bullets are
    ;; handled in the Bullets scope, and with the player is handled by the
    ;; caller.
    ;;
    ;; All handlers receive 'Enemies::zp_pool_index' which contain the index to the
    ;; 'Enemy::zp_enemies_pool_base' array of the current enemy. This argument
    ;; is expected to be _immutable_; if you want to abuse the 'x' register, you
    ;; are free to do so. For other arguments handlers are expected to abuse on
    ;; the 'extra' state that is available for each enemy.

    ;; Basic falling movement. Straight horizontal movement with a slight
    ;; downward angle. Enemy should explode on platform/ground contact. The
    ;; 'extra' state is used as a counter for the falling velocity (i.e. enemy
    ;; falls 1 pixel per counter exhaustion).
    .proc basic
        ;; First of all, we always move enemies horizontally, while being
        ;; mindful on the direction and the step depending on the enemy type.
        lda Enemies::zp_enemies_pool_base, x
        and #$80
        beq @move_left
        lda Enemies::zp_enemies_pool_base + 2, x
        clc
        adc Enemies::zp_enemy_arg
        sta Enemies::zp_enemies_pool_base + 2, x
        jmp @do_counter
    @move_left:
        lda Enemies::zp_enemies_pool_base + 2, x
        sec
        sbc Enemies::zp_enemy_arg
        sta Enemies::zp_enemies_pool_base + 2, x

        ;; Decrement the counter from the 'extra' state. If it reaches zero,
        ;; then we should do some downward movement. Otherwise we just go to
        ;; collision checking.
    @do_counter:
        lda Enemies::zp_enemies_pool_base + 3, x
        sec
        sbc #1
        bne @update_extra_state

        ;; Move downwards and reset the 'extra' state depending on the enemy
        ;; kind.
    @downward:
        inc Enemies::zp_enemies_pool_base + 1, x

        lda Globals::zp_level_kind
        beq @init_zero
        lda #FALLING_VELOCITY_FAST
        bne @update_extra_state
    @init_zero:
        lda #FALLING_VELOCITY

    @update_extra_state:
        sta Enemies::zp_enemies_pool_base + 3, x

        ;; Check collisions with the background.

        ;; Remember that background checks are done in tile coordinates, not
        ;; screen ones. So we have to do the translation to it (3 x
        ;; 'lsr'). After that, for the X coordinate, depending if the enemy is
        ;; facing left/right, we have to increment this coordinate (i.e. twice
        ;; if facing right as an enemy of this type is always 2x2 sprites).
        lda Enemies::zp_enemies_pool_base + 2, x
        lsr
        lsr
        lsr
        tay
        lda Enemies::zp_enemies_pool_base, x
        and #$80
        beq @after_x
        iny
        iny
    @after_x:
        sty Globals::zp_arg1

        ;; Translate the Y coordinate into tile ones.
        lda Enemies::zp_enemies_pool_base + 1, x
        lsr
        lsr
        lsr
        sta Globals::zp_arg0

        ;; Perform a collision check with the upper boundary.
        jsr Background::collides
        beq @check_down
        JAL bite_the_dust

    @check_down:
        ;; If that failed, then increment the vertical tile coordinate twice to
        ;; get the bottom boundary and check again.
        inc Globals::zp_arg0
        inc Globals::zp_arg0
        jsr Background::collides
        beq @end
        JAL bite_the_dust

    @end:
        rts
    .endproc

    ;; Diagonal bouncing at a 45 degree angle. TODO: explain 'extra'.
    .proc bounce
        ;; TODO

        rts
    .endproc

    ;; Erratic diagonal bouncing like 'bounce', meaning that vertically the go
    ;; up and down at random, not in a predictable manner.
    .proc erratic
        ;; TODO

        rts
    .endproc

    ;; Track the player's current Y position and homes at it when the Y position
    ;; matches that of the player.
    .proc homing
        ;; TODO

        rts
    .endproc

    ;; Simply chases the player. TODO: explain 'extra'.
    .proc chase
        ;; TODO

        rts
    .endproc

    ;; Function pointers to movement handlers.
movement_lo:
    .byte <basic, <bounce, <erratic, <homing
    .byte <chase, <bounce, <basic, <chase
movement_hi:
    .byte >basic, >bounce, >erratic, >homing
    .byte >chase, >bounce, >basic, >chase

    ;;;
    ;; Definitions for all the enemy types.
    ;;
    ;; An enemy type is defined by four bytes, containing the tile IDs for
    ;; it. Some enemies only span 2 tiles, and because of this they have $FF as
    ;; filler bytes.
    ;;
    ;; Moreover, each enemy has two states in order to show some inner
    ;; movement. This is why each enemy has an extra row of tile IDs, which
    ;; contain the "other" state.
    ;;
    ;; Finally, enemies can face right or left, which usually would be handled
    ;; in code, but it's much cheaper to abuse our mostly empty ROM-space with
    ;; extra definitions than being careful on the order in the allocation
    ;; loop.
    ;;
    ;; Thus, an enemy takes a whoping amount of 32 bytes. The first four bytes
    ;; are the actualy tile IDs for the enemy. The second row of four bytes is
    ;; its "other" shape in order to show inner movement. And the last two rows
    ;; are simply mirrors of the first two whenever the enemy is facing right
    ;; instead of left.
tiles:
    ;; Asteroid
    .byte $26, $27, $36, $37
    .byte $46, $47, $56, $57
    .byte $27, $26, $37, $36
    .byte $47, $46, $57, $56

    ;; Furry thingie
    .byte $28, $29, $38, $39
    .byte $48, $49, $58, $59
    .byte $29, $28, $39, $38
    .byte $49, $48, $59, $58

    ;; Bubble
    .byte $24, $25, $34, $35
    .byte $44, $45, $54, $55
    .byte $25, $24, $35, $34
    .byte $45, $44, $55, $54

    ;; Fighter jet 1
    .byte $2A, $2B, $3A, $3B
    .byte $4A, $4B, $5A, $5B
    .byte $2B, $2A, $3B, $3A
    .byte $4B, $4A, $5B, $5A

    ;; Fighter jet 2
    .byte $31, $32, $FF, $FF
    .byte $60, $61, $FF, $FF
    .byte $32, $31, $FF, $FF
    .byte $61, $60, $FF, $FF

    ;; UFO
    .byte $40, $41, $FF, $FF
    .byte $50, $51, $FF, $FF
    .byte $41, $40, $FF, $FF
    .byte $51, $50, $FF, $FF

    ;; Cross
    .byte $2C, $2D, $3C, $3D
    .byte $4C, $4D, $5C, $5D
    .byte $2D, $2C, $3D, $3C
    .byte $4D, $4C, $5D, $5C

    ;; Weirdo
    .byte $2E, $2F, $3E, $3F
    .byte $4E, $4F, $5E, $5F
    .byte $2F, $2E, $3F, $3E
    .byte $4F, $4E, $5F, $5E
.endscope