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
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
|
.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_0 = HZ / 5
FALLING_VELOCITY_1 = HZ / 10
FALLING_VELOCITY_2 = HZ / 25
FALLING_VELOCITY_3 = HZ / 50
;; 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
;; Initialize the enemy arg, which is always 1 except for homing
;; attacks.
ldy #1
txa
cmp #3
bne @set_enemy_arg
iny
@set_enemy_arg:
sty Enemies::zp_enemy_arg
__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
;; The initial size of the pool is its whole capacity.
ldy #ENEMIES_POOL_CAPACITY
sty zp_enemies_pool_size
@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.
stx Globals::zp_tmp0
jsr generate_extra
ldx Globals::zp_tmp0
inx
sta zp_enemies_pool_base, x
;; Next enemy!
inx
dey
bne @enemies_init_loop
rts
.endproc
;; Generate a value for the 'extra' value depending on the current level
;; kind. The result is left in 'a'.
;;
;; NOTE: the 'x' register is touched, while the 'y' register is not.
.proc generate_extra
;; TODO: rest of the enemies.
lda Globals::zp_level_kind
beq @init_basic
cmp #1
beq @init_bounce_1
cmp #2
beq @init_erratic
cmp #3
beq @init_homing
cmp #5
beq @init_bounce_2
cmp #6
__fallthrough__ @init_basic
@init_basic:
jsr Prng::random_valid_y_coordinate
and #$0F
ora #$11
rts
@init_erratic:
jsr Prng::random_valid_y_coordinate
and #$01
rts
@init_homing:
jsr Prng::random_valid_y_coordinate
and #$01
ora #$10
rts
@init_bounce_1:
jsr Prng::random_valid_y_coordinate
and #$01
rts
@init_bounce_2:
jsr Prng::random_valid_y_coordinate
and #$01
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 defined as follows:
;;
;; |TTTT KK-D|; where:
;; |
;; |- D: downwards if 1; upwards if 0 (just like the 'diagonal' algorithm).
;; |- K: movement kind (see the constants FALLING_VELOCITY_*).
;; |- T: timer. Whenever it reaches zero, then a vertical movement is done.
;;
.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
tay
sec
sbc #$10
and #$F0
bne @update_extra_state
;; Move downwards and reset the 'extra' state depending on the enemy
;; kind.
inc Enemies::zp_enemies_pool_base + 1, x
;; Yes, doing an index on a pre-computed ROM table would've been faster,
;; but I need the 'x' register and I didn't feel like doing funny
;; dances when it's not so bad.
tya
and #$0C
beq @init_zero
cmp #$04
beq @init_one
cmp #$08
beq @init_two
lda #(FALLING_VELOCITY_3 << 4)
bne @update_extra_state
@init_zero:
lda #(FALLING_VELOCITY_0 << 4)
bne @update_extra_state
@init_one:
lda #(FALLING_VELOCITY_1 << 4)
bne @update_extra_state
@init_two:
lda #(FALLING_VELOCITY_2 << 4)
@update_extra_state:
;; Save the new timer into a temporary value, mask out the high byte
;; from the original value, and then merge the values.
sta Globals::zp_tmp0
tya
and #$0F
ora Globals::zp_tmp0
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. The 'extra' state is a boolean
;; which is set to 0 if moving upwards, and to 1 if moving downwards.
.proc bounce
;; First of all, we always move enemies horizontally, while being
;; mindful on the direction and the step depending on the enemy
;; type. This is just the same as the 'basic' algorithm.
lda Enemies::zp_enemies_pool_base, x
and #$80
beq @move_left
inc Enemies::zp_enemies_pool_base + 2, x
jmp @do_vertical
@move_left:
dec Enemies::zp_enemies_pool_base + 2, x
@do_vertical:
;; The vertical movement works the same way, but taking into account its
;; direction via the 'extra' state. Note that we mask it, which is not
;; needed for the main enemies which use this algorithm, but it is for
;; the 'erratic' algorithm which re-uses this one.
lda Enemies::zp_enemies_pool_base + 3, x
and #$01
beq @move_up
inc Enemies::zp_enemies_pool_base + 1, x
jmp @check_collision
@move_up:
dec Enemies::zp_enemies_pool_base + 1, x
;; Collision checking.
@check_collision:
;; Translate the Y axis into tile coordinates.
lda Enemies::zp_enemies_pool_base + 1, x
lsr
lsr
lsr
sta Globals::zp_arg0
;; Translate the X axis into tile coordinates. We will also save it into
;; 'Globals::zp_tmp0' as that will save us some trouble down the road.
lda Enemies::zp_enemies_pool_base + 2, x
lsr
lsr
lsr
sta Globals::zp_arg1
sta Globals::zp_tmp0
;; Does this upper left corner collide?
jsr Background::collides
bne @bounce_down
;; No. Increment the X coordinate to the right corner and ask again.
inc Globals::zp_arg1
inc Globals::zp_arg1
jsr Background::collides
beq @check_front_or_bottom
;; There was a (hopefully purely) upper collision!
@bounce_down:
;; The previous 'Background::collides' call has tampered with the 'x'
;; register. Load the proper value again.
ldx Enemies::zp_pool_index
;; Flip 'extra' boolean.
lda Enemies::zp_enemies_pool_base + 3, x
eor #1
sta Enemies::zp_enemies_pool_base + 3, x
;; Move downwards once, which cancels the movement set at the beginning
;; of the function.
inc Enemies::zp_enemies_pool_base + 1, x
rts
;; Now, depending on the level, the enemy might be the regular size or
;; shorter. If it's on the shorter end, then move to check the bottom
;; corners directly.
@check_front_or_bottom:
lda Globals::zp_level_kind
cmp #2
beq @prepare_check_front_collision
cmp #1
bne @check_bottom
@prepare_check_front_collision:
;; We are checking the "front" (center) of the enemy, which corresponds
;; to the regular sized enemy. This means to increase the Y tile
;; coordinate once.
inc Globals::zp_arg0
;; Is the enemy moving left or right? This is relevant because the X
;; tile coordinate is set to the right corner. If it's moving left, then
;; we need to decrement it twice to move it back to the left corner.
ldx Enemies::zp_pool_index
lda Enemies::zp_enemies_pool_base, x
and #$80
bne @check_front_collision
dec Globals::zp_arg1
dec Globals::zp_arg1
@check_front_collision:
;; Does it collide frontally?
jsr Background::collides
beq @check_bottom
;; Yes! Restore the 'x' after the 'Background::collides' and use it to
;; flip the direction where the enemy is headed.
ldx Enemies::zp_pool_index
lda Enemies::zp_enemies_pool_base, x
eor #$80
sta Enemies::zp_enemies_pool_base, x
;; And bounce already to the new direction to avoid the enemy getting
;; stucked or other weird situations.
and #$80
beq @bounce_left
inc Enemies::zp_enemies_pool_base + 2, x
rts
@bounce_left:
dec Enemies::zp_enemies_pool_base + 2, x
rts
;; Last but not least, let's see if the enemy collides on its bottom
;; corners.
@check_bottom:
;; Restore the X tile coordinate as the previous steps might have left
;; it in an unknown state.
lda Globals::zp_tmp0
sta Globals::zp_arg1
;; Increse the Y tile coordinate. Note that this is to be done
;; regardless to the enemy type, in contrast to what we did when we were
;; wondering about checking the front.
inc Globals::zp_arg0
;; And check for a collision on the bottom left corner.
jsr Background::collides
bne @bounce_up
;; Nope! Try again but with the bottom right corner.
inc Globals::zp_arg1
inc Globals::zp_arg1
jsr Background::collides
bne @bounce_up
rts
;; There was a (hopefully purely) bottom collision!
@bounce_up:
;; Restore the 'x' register from a previous 'Background::collides' call.
ldx Enemies::zp_pool_index
;; Flip the 'extra' boolean.
lda Enemies::zp_enemies_pool_base + 3, x
eor #1
sta Enemies::zp_enemies_pool_base + 3, x
;; Make it bounce up.
dec Enemies::zp_enemies_pool_base + 1, x
rts
.endproc
;; Erratic movement, which sometimes stops, sometimes moves horizontally,
;; and some other times it goes diagonally; all at random. The 'extra' state
;; is laid out as follows:
;;
;; |TTTT -AAD|; where:
;; |
;; |- D: downwards if 1; upwards if 0 (just like the 'diagonal' algorithm).
;; |- AA: current algorithm: 00/11: stop; 01: horizontal; 10: diagonal.
;; |- T: timer for algorithm. Whenever it reaches zero the algorithm is changed.
;;
.proc erratic
;; Check the timer.
lda Enemies::zp_enemies_pool_base + 3, x
and #$F0
bne @do
;; The 'extra' state has to change. In order to do this we prepare an
;; "or" mask that will be paired to the change of the algorithm in the
;; following code block. Not that this mask is shifted to the right, as
;; the end computation will finally shift it left once. This mask is
;; responsible for initializing the timer to 1, and the algorithm to 1
;; if we are coming from a "stop" phase (i.e. we want to guarantee that
;; the next state is not "stop" again).
lda Enemies::zp_enemies_pool_base + 3, x
ldx #$08
and #%00000110
bne @after_unpause
inx
@after_unpause:
stx Globals::zp_tmp0
;; Pick a random value and mask it to get the possible algorithms. If
;; the algorithm is "stop" and we were already coming from that phase,
;; the mask we prepared in the temporary value will take care of at
;; least going into another state.
jsr Prng::random_valid_y_coordinate
and #$03
ora Globals::zp_tmp0
asl
sta Globals::zp_tmp0
;; Restore the 'x' register from the previous
;; 'Prng::random_valid_y_coordinate' call.
ldx Enemies::zp_pool_index
;; The previous temporary value missed the D bit. Let's add it now and
;; store it.
lda Enemies::zp_enemies_pool_base + 3, x
and #$01
clc
adc Globals::zp_tmp0
sta Enemies::zp_enemies_pool_base + 3, x
rts
@do:
;; Blindly increase the timer as overflows will be covered when entering
;; this function.
lda Enemies::zp_enemies_pool_base + 3, x
clc
adc #$10
sta Enemies::zp_enemies_pool_base + 3, x
;; Now switch what to do depending on the algorithm.
and #%00000110
bne @next_algo_1
rts
@next_algo_1:
cmp #%00000110
bne @next_algo_2
rts
@next_algo_2:
and #%00000100
beq @horizontal
;; For the diagonal algorithm simply call the one we've got which
;; shouldn't mess with our 'extra' state from this one.
JAL bounce
@horizontal:
;; The 'y' register is used as a way to increment the X tile coordinates
;; during collision checking. Since we have to know the direction first
;; of all, we can take advantage of it and increment it whenever we are
;; moving right.
ldy #0
;; Plain old horizontal movement as it's done in other places.
lda Enemies::zp_enemies_pool_base, x
and #$80
beq @move_left
iny
iny
inc Enemies::zp_enemies_pool_base + 2, x
jmp @after_horizontal
@move_left:
dec Enemies::zp_enemies_pool_base + 2, x
@after_horizontal:
;; We store in a temporary value how much the X tile coordinates will
;; have to be increased in order to point to the right face.
sty Globals::zp_tmp0
;; After that has been done, check for collision.
;; Translate the Y axis into tile coordinates.
lda Enemies::zp_enemies_pool_base + 1, x
lsr
lsr
lsr
sta Globals::zp_arg0
;; Translate the X axis into tile coordinates, while adding the facing
;; value we computed earlier.
lda Enemies::zp_enemies_pool_base + 2, x
lsr
lsr
lsr
clc
adc Globals::zp_tmp0
sta Globals::zp_arg1
;; Top.
jsr Background::collides
bne @horizontal_collision
;; Center.
inc Globals::zp_arg0
jsr Background::collides
bne @horizontal_collision
;; Bottom.
inc Globals::zp_arg0
jsr Background::collides
bne @horizontal_collision
rts
@horizontal_collision:
;; Restore the 'x' register from a previous 'Background::collides' call.
ldx Enemies::zp_pool_index
;; Flip the direction bit.
lda Enemies::zp_enemies_pool_base, x
eor #$80
sta Enemies::zp_enemies_pool_base, x
;; And bounce already to the new direction to avoid the enemy getting
;; stucked or other weird situations.
and #$80
beq @bounce_left
inc Enemies::zp_enemies_pool_base + 2, x
rts
@bounce_left:
dec Enemies::zp_enemies_pool_base + 2, x
rts
.endproc
;; Track the player's current Y position and homes at it when the Y position
;; matches that of the player. The 'extra' state is laid out as follows:
;;
;; |TTTT ttSD|; where:
;; |
;; |- D: downwards if 1; upwards if 0 (just like the 'diagonal' algorithm).
;; |- S: state: 0: moving up/down; 01: homing.
;; |- tt: number of times TT has run out. When it reaches '11', then we
;; | change from the zero state to homing.
;; |- TT: timer for upwards/downwards movement.
;;
;; NOTE: whenever we transition to homing attach, then the 'extra' state
;; follows the one from 'basic'. Notice that bit 1 is untouched by the
;; 'basic' algorithm, which is used here to determine that we are in the
;; 'homing' state.
.proc homing
;; First of all, get the state of the enemy. If it's already on the
;; 'homing' state, then just jump-and-link to the 'basic'
;; algorithm. Otherwise we stay on this function.
;;
;; NOTE: this function needs to use the original 'extra' value a
;; lot. Save it on the 'y' register since it's never used
;; otherwise. Going forward notice all the 'tya', which simply mean "get
;; the original 'extra' value".
lda Enemies::zp_enemies_pool_base + 3, x
tay
and #$02
beq @zero_state
JAL basic
;; It's the first state of the enemy (i.e. just moving up and down).
@zero_state:
;; Has the timer run out? If not, just continue moving.
tya
and #$F0
bne @move
;; Yes! Grab the 'time' bits from the 'extra' state. If it's already
;; #%11, then we are done with the counting cycles and we can setup the
;; homing attack.
tya
and #$0C
cmp #$0C
beq @start_homing
cmp #$08
bne @increment_time
;; We are at the #%10 'kind', which means we need to flip the vertical
;; position.
tya
eor #$01
sta Enemies::zp_enemies_pool_base + 3, x
tay
@increment_time:
;; Increment the 'time' bits and continue moving.
tya
clc
adc #$04
sta Enemies::zp_enemies_pool_base + 3, x
tay
@move:
;; Moving is a matter of just increasing up/down depending on the 'down'
;; bit from the 'extra' state.
tya
and #$01
beq @go_down
inc Enemies::zp_enemies_pool_base + 1, x
jmp @increase_timer
@go_down:
dec Enemies::zp_enemies_pool_base + 1, x
@increase_timer:
tya
clc
adc #$10
sta Enemies::zp_enemies_pool_base + 3, x
rts
;; We are done going up and down. Now it's time to change the state of
;; this enemy, and home towards the player depending on its position.
@start_homing:
;; Ensure the 'state' bit is set.
tya
ora #$02
sta Enemies::zp_enemies_pool_base + 3, x
lda Enemies::zp_enemies_pool_base + 1, x
cmp Player::zp_screen_y
bcc @home_down
;; TODO: up
nop
@home_down:
;; TODO: subtract the same portion over and over. If overflow is set, then
;; we know we are passed it.
;; TODO: down
@end:
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 $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
;; Fighter jet 2
.byte $2A, $2B, $3A, $3B
.byte $4A, $4B, $5A, $5B
.byte $2B, $2A, $3B, $3A
.byte $4B, $4A, $5B, $5A
;; Weirdo
.byte $2E, $2F, $3E, $3F
.byte $4E, $4F, $5E, $5F
.byte $2F, $2E, $3F, $3E
.byte $4F, $4E, $5F, $5E
.endscope
|