-
Notifications
You must be signed in to change notification settings - Fork 4
/
SpecBong.asm
1607 lines (1517 loc) · 67.7 KB
/
SpecBong.asm
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
;-------------------------------
; SpecBong - tutorial-like project to load Layer2 image and move sprites
; © Peter Helcmanovsky, John McGibbitts 2020, license: https://opensource.org/licenses/MIT
;
; to build this ASM file we use https://github.com/z00m128/sjasmplus command:
; sjasmplus --fullpath --nologo --lst --lstlab --msg=war SpecBong.asm
; (this will also produce the listing file, so we can review the machine code generated
; and addresses assigned to various symbols)
;
; to convert BMP to upside-down TGA I use ImageMagick "convert" command:
; convert SpecBong.bmp -flip tga:SpecBong.tga
; (the upside down uncompressed 8bpp TGA has the advantage that it can be just binary
; included as L2 pixel data, from correct offset, no need of any further conversion)
; adjusting sjasmplus syntax to my taste (a bit more strict than default) + enable Z80N
OPT --syntax=abfw --zxnext
OPT --zxnext=cspect ;DEBUG enable break/exit fake instructions of CSpect (remove for real board)
; include symbolic names for "magic numbers" like NextRegisters and I/O ports
INCLUDE "constants.i.asm"
JOY_BIT_RIGHT EQU 0
JOY_BIT_LEFT EQU 1
JOY_BIT_DOWN EQU 2
JOY_BIT_UP EQU 3
JOY_BIT_FIRE EQU 4
; DEFINE DISPLAY_PERFORMANCE_DEBUG_BORDER ; enable the color stripes in border
MAIN_BORDER_COLOR EQU 1
STRUCT S_SPRITE_4B_ATTR ; helper structure to work with 4B sprites attributes
x BYTE 0 ; X0:7
y BYTE 0 ; Y0:7
mrx8 BYTE 0 ; PPPP Mx My Rt X8 (pal offset, mirrors, rotation, X8)
vpat BYTE 0 ; V 0 NNNNNN (visible, 5B type=off, pattern number 0..63)
ENDS
STRUCT S_LADDER_DATA
x BYTE 0 ; centre of ladder -8 (left edge of player sprite when aligned)
y BYTE 0 ; position of top platform -16 (Ypos for player to stand at top)
t BYTE 0 ; y+t = bottom platform -16 (Ypos for player to stand at)
ENDS
STRUCT S_UI_STRING_DATA
length BYTE 0 ; length of text
vram WORD MEM_ZX_SCREEN_4000 ; address into pixel VRAM to print
txt WORD 0 ; address of text
vramA WORD MEM_ZX_ATTRIB_5800 ; address into attribute VRAM to set
attr BYTE 0 ; attribute to set
ENDS
; selecting "Next" as virtual device in assembler, which allows me to set up all banks
; of Next (0..223 8kiB pages = 1.75MiB of memory) and page-in virtual memory
; with SLOT/PAGE/MMU directives
DEVICE ZXSPECTRUMNEXT
; the default mapping of memory is 16k banks: 7, 5, 2, 0 (8k pages: 14,15,10,11,4,5,0,1)
; ^ it's the default mapping of assembler at assembling time, at runtime the NEXLOAD
; will set the default mapping the same way, but first 16k is ROM, not bank 7.
; $8000..BFFF is here Bank 2 (pages 4 and 5) -> we will put **all** code here
ORG $8000
start:
; break at start when running in CSpect with "-brk" option (`DD 01` is "break" in CSpect)
; break : nop : nop ; but `DD 01` on Z80 is `ld bc,nn`, so adding 2x nop after = `ld bc,0`
; disable interrupts, we will avoid using them to keep code simpler to understand
di
; setup bottom part of random seed by R
ld a,r
ld (Rand16.s),a
nextreg TURBO_CONTROL_NR_07,2 ; switch to 14MHz as final speed (it's more than enough)
; but makes it somewhat easier on the emulator than max 28MHz mode
; make the Layer 2 visible and reset some registers (should be reset by NEXLOAD, but to be safe)
nextreg DISPLAY_CONTROL_NR_69,$80 ; Layer 2 visible, ULA bank 5, Timex mode 0
nextreg SPRITE_CONTROL_NR_15,%000'100'01 ; LoRes off, layer priority USL, sprites visible
nextreg LAYER2_RAM_BANK_NR_12,9 ; visible Layer 2 starts at bank 9
nextreg LAYER2_CONTROL_NR_70,0 ; 256x192x8 Layer 2 mode, L2 palette offset +0
nextreg LAYER2_XOFFSET_NR_16,0 ; Layer 2 X,Y offset = [0,0]
nextreg LAYER2_XOFFSET_MSB_NR_71,0 ; including the new NextReg 0x71 for cores 3.0.6+
nextreg LAYER2_YOFFSET_NR_17,0
; set all three clip windows (Sprites, Layer2, ULA) explicitly just to be sure
; helps with bug in CSpect which draws sprites "over-border" even when it is OFF
nextreg CLIP_WINDOW_CONTROL_NR_1C,$03 ; reset write index to all three clip windows
nextreg CLIP_LAYER2_NR_18,0
nextreg CLIP_LAYER2_NR_18,255
nextreg CLIP_LAYER2_NR_18,0
nextreg CLIP_LAYER2_NR_18,191
nextreg CLIP_SPRITE_NR_19,0
nextreg CLIP_SPRITE_NR_19,255
nextreg CLIP_SPRITE_NR_19,0
nextreg CLIP_SPRITE_NR_19,191
nextreg CLIP_ULA_LORES_NR_1A,0
nextreg CLIP_ULA_LORES_NR_1A,255
nextreg CLIP_ULA_LORES_NR_1A,0
nextreg CLIP_ULA_LORES_NR_1A,191
call InitUi ; will setup everything important about ULA screen, CLS + labels, etc.
; setup Layer 2 palette - map palette data to $E000 region, to process them
nextreg MMU7_E000_NR_57,$$BackGroundPalette ; map the memory with palette
nextreg PALETTE_CONTROL_NR_43,%0'001'0'0'0'0 ; write to Layer 2 palette, select first palettes
nextreg PALETTE_INDEX_NR_40,0 ; color index
ld b,0 ; 256 colors (loop counter)
ld hl,BackGroundPalette ; address of first byte of 256x 24 bit color def.
; calculate 9bit color from 24bit value for every color
; -> will produce pair of bytes -> write that to nextreg $44
SetPaletteLoop:
; TGA palette data are three bytes per color, [B,G,R] order in memory
; so palette data are: BBBbbbbb GGGggggg RRRrrrrr
; (B/G/R = 3 bits for Next, b/g/r = 5bits too fine for Next, thrown away)
; first byte to calculate: RRR'GGG'BB
ld a,(hl) ; Blue
inc hl
rlca
rlca
ld c,a ; preserve blue third bit in C.b7 ($80)
and %000'000'11 ; two blue bits at their position
ld e,a ; preserve blue bits in E
ld a,(hl) ; Green
inc hl
rrca
rrca
rrca
and %000'111'00
ld d,a ; preserve green bits in D
ld a,(hl) ; Red
inc hl
and %111'000'00 ; top three red bits
or d ; add green bits
or e ; add blue bits
nextreg PALETTE_VALUE_9BIT_NR_44,a ; RRR'GGG'BB
; second byte is: p000'000B (priority will be 0 in this app)
xor a
rl c ; move top bit from C to bottom bit in A (Blue third bit)
rla
nextreg PALETTE_VALUE_9BIT_NR_44,a ; p000'000B p=0 in this image always
djnz SetPaletteLoop
; the image pixel data are already in the correct banks 9,10,11 - loaded by NEX loader
; nothing to do with the pixel data - we are done
; SpecBong sprite gfx does use the default palette: color[i] = convert8bitColorTo9bit(i);
; which is set by the NEX loader in the first sprite palette
; nothing to do here in the code with sprite palette
; upload the sprite gfx patterns to patterns memory (from regular memory - loaded by NEX loader)
; preconfigure the Next for uploading patterns from slot 0
ld bc,SPRITE_STATUS_SLOT_SELECT_P_303B
xor a
out (c),a ; select slot 0 for patterns (selects also index 0 for attributes)
; we will map full 16kiB to memory region $C000..$FFFF (to pages 25,26 with sprite pixels)
nextreg MMU6_C000_NR_56,$$SpritePixelData ; C000..DFFF <- 8k page 25
nextreg MMU7_E000_NR_57,$$SpritePixelData+1 ; E000..FFFF <- 8k page 26
ld hl,SpritePixelData ; HL = $C000 (beginning of the sprite pixels)
ld bc,SPRITE_PATTERN_P_5B ; sprite pattern-upload I/O port, B=0 (inner loop counter)
ld a,64 ; 64 patterns (outer loop counter), each pattern is 256 bytes long
UploadSpritePatternsLoop:
; upload 256 bytes of pattern data (otir increments HL and decrements B until zero)
otir ; B=0 ahead, so otir will repeat 256x ("dec b" wraps 0 to 255)
dec a
jr nz,UploadSpritePatternsLoop ; do 64 patterns
; setup high part of random seed by R
ld a,r
ld (Rand16.s+1),a
; init game state for new game
call GameStateInit_NewGame
; main loop of the game
GameLoop:
call GameLoop_BaseThings
IFDEF DISPLAY_PERFORMANCE_DEBUG_BORDER
; magenda border: to measure the AI code performance
ld a,3
out (ULA_P_FE),a
ENDIF
; move the snowballs in the level, and occasionally spawn a new one
call SnowballsAI
IFDEF DISPLAY_PERFORMANCE_DEBUG_BORDER
; green border: to measure the player AI code performance
ld a,4
out (ULA_P_FE),a
ENDIF
call ReadInputDevices
call Player1MoveByControls
IFDEF DISPLAY_PERFORMANCE_DEBUG_BORDER
; black border: to measure the jump bonus refresh code performance
ld a,0
out (ULA_P_FE),a
ENDIF
call JumpBonusLogic
IFDEF DISPLAY_PERFORMANCE_DEBUG_BORDER
; cyan border: to measure the collisions code performance
ld a,5
out (ULA_P_FE),a
ENDIF
call SnowballvsPlayerCollision
call EndOfLevelLogic
IF 0 ; DEBUG wait for fire key after frame
.waitForFire: call ReadInputDevices : ld a,(Player1Controls) : bit JOY_BIT_FIRE,a : jr z,.waitForFire
.waitForRelease: call ReadInputDevices : ld a,(Player1Controls) : bit JOY_BIT_FIRE,a : jr nz,.waitForRelease
ENDIF
; do the GameLoop infinitely
jr GameLoop
GameLoop_BaseThings:
; wait for scanline 192, so the update of sprites happens outside of visible area
; this will also force the GameLoop to tick at "per frame" speed 50 or 60 FPS
call WaitForScanlineUnderUla
IFDEF DISPLAY_PERFORMANCE_DEBUG_BORDER
; red border: to measure the sprite upload time by tallness of the border stripe
ld a,2
out (ULA_P_FE),a
ENDIF
; upload sprite data from memory array to the actual HW sprite engine
; reset sprite index for upload
ld bc,SPRITE_STATUS_SLOT_SELECT_P_303B
xor a
out (c),a ; select slot 0 for sprite attributes
ld hl,Sprites
ld bc,SPRITE_ATTRIBUTE_P_57 ; B = 0 (repeat 256x), C = sprite attribute-upload I/O port
; out 512 bytes in total (whole sprites buffer)
otir
otir
IFDEF DISPLAY_PERFORMANCE_DEBUG_BORDER
; yellow border: to measure the UI code performance
ld a,6
out (ULA_P_FE),a
ENDIF
call RefreshUi ; draws score, lives, jump-over-ball bonus scores, etc
ret
;-------------------------------------------------------------------------------------
; Part 10 - UI drawing routines - using transparent ULA layer above everything
GameStateInit_NewGame:
; reset level number to "00" and minimal difficulty
; (will be raised in GameStateInit_NewLevel from 0 to 1 before new game starts)
ld hl,"00"
ld (LevelNumberTxt),hl
ld hl,150
ld (LevelDifficulty),hl
; reset score to zero
ld hl,Player1Score
ld de,Player1Score+1
ld (hl),'0'
ld bc,7
ldir
; reset lives
ld a,3
ld (Player1Lives),a
; |
; fallthrough to GameStateInit_NewLevel
; |
; v
GameStateInit_NewLevel:
; increment the difficulty
ld hl,(LevelDifficulty)
add hl,25
ld (LevelDifficulty),hl
; increment the level number
ld hl,LevelNumberTxt+2
ld a,1
ld bc,2<<8 ; B = 2, C = 0
call AddScore.updateDigitsLoop
; |
; fallthrough to GameStateInit_NewLife
; |
; v
GameStateInit_NewLife:
ld a,$08
ld (CurrentDifficulty+1),a ; first snowball to happen quite early
; reset bonus counter to 5000 (the last two are always zeroes, no need to re-init)
ld hl,"05" ; L = '5', H = '0' vs little-endian way of storing 16bit value
ld (LevelBonus),hl
; reset Player position, and movement internals like ladder/jumping stuff
ld ix,SprPlayer
ld (ix+S_SPRITE_4B_ATTR.x),32+16 ; near left of paper area
ld (ix+S_SPRITE_4B_ATTR.y),206 ; near bottom of paper area
ld (ix+S_SPRITE_4B_ATTR.mrx8),0 ; clear pal offset, mirrors, rotate, x8
ld (ix+S_SPRITE_4B_ATTR.vpat),$80 + 0 ; start with pattern 0
xor a
ld (Player1Controls),a
ld (Player1ControlsCoolDown),a
ld (Player1LadderData+1),a ; just "tall" to zero is enough
ld (Player1JumpIdx),a
ld (Player1JumpDir),a
ld (JumpBonusDetection.x),a ; switch off JumpBonus detector
ld (EmitBallCoolDown1),a ; reset snowball emitter cooldowns
ld (EmitBallCoolDown2),a
dec a ; A = 255
ld (Player1SafeLandingY),a
; init SNOWBALLS_CNT snowballs - the in-memory copy of sprite attributes
ld ix,SprSnowballs ; IX = address of first snowball sprite
ld de,S_SPRITE_4B_ATTR
ld bc,SNOWBALLS_CNT<<8 ; B = SNOWBALLS_CNT (counter), C = 0
ld a,52 ; invisible sprite + snowball pattern (52 or 53)
InitBallsLoop:
; set current ball data
ld (ix+S_SPRITE_4B_ATTR.x),c
ld (ix+S_SPRITE_4B_ATTR.y),c
ld (ix+S_SPRITE_4B_ATTR.mrx8),c
ld (ix+S_SPRITE_4B_ATTR.vpat),a
xor 1 ; alternate snowball patterns between 52/53
; advance IX to point to next snowball
add ix,de
djnz InitBallsLoop
; refresh all UI strings at "level" level and exit
jr RefreshUi_Level
InitUi:
; set ULA palette (to have background transparent) and do classic "CLS"
nextreg PALETTE_CONTROL_NR_43,%0'000'0'0'0'0 ; Classic ULA + custom palette
nextreg PALETTE_INDEX_NR_40,16+7 ; paper 7
nextreg PALETTE_VALUE_NR_41,$E3
nextreg PALETTE_INDEX_NR_40,16+8+7 ; paper 7 + bright 1
nextreg PALETTE_VALUE_NR_41,$E3
nextreg GLOBAL_TRANSPARENCY_NR_14,$E3
nextreg TRANSPARENCY_FALLBACK_COL_NR_4A,%000'111'11 ; bright cyan as debug (shouldn't be seen)
; do the "CLS"
ld hl,MEM_ZX_SCREEN_4000
ld de,MEM_ZX_SCREEN_4000+1
ld bc,MEM_ZX_ATTRIB_5800-MEM_ZX_SCREEN_4000
ld (hl),l
ldir
ld (hl),P_WHITE|BLACK ; set all attributes to white paper + black ink
ld bc,32*24-1
ldir
; set attributes of some areas of screeen
ld ix,UiTextsData
jp PrintStrings
RefreshUi_Level:
; refresh the level, score, bonus score + exit
ld ix,UiTextsData_Level
jr RefreshUi.customIx
RefreshUi:
; refresh the score and bonus score
ld ix,UiTextsData_Frame
.customIx:
call PrintStrings
; refresh the lives UI (it's shown with sprites :) )
ld ix,SprLivesUi
ld bc,(Player1Lives) ; C = amount of lives to show (others to hide)
ld b,6 ; max amount of sprites to show
inc c
ld hl,32+24*8+4
.livesUiSetSpriteLoop:
ld (ix+S_SPRITE_4B_ATTR.mrx8),h ; no mirror/rotate flags
ld (ix+S_SPRITE_4B_ATTR.x),l
ld (ix+S_SPRITE_4B_ATTR.y),32+7*8+1
ld e,32*2 ; will become pattern number 32
ld a,b
cp c
rr e ; index < lives -> top bit (visible/hidden flag)
ld (ix+S_SPRITE_4B_ATTR.vpat),e
ld de,S_SPRITE_4B_ATTR
add ix,de
add hl,10
djnz .livesUiSetSpriteLoop
ret
AddScore:
; In: A = score to add (0..255, score is automatically *100)
; Modifies: BC, HL, AF
ld bc,(Player1Score+3) ; remember ten-thousands digit
push bc
call .implementation
pop bc
ld a,(Player1Score+3) ; new ten-thousands digit
cp c
ret z ; no change in ten thousands
cp '5'
jr z,.addBonusLifeAtEvery50k
cp '0'
ret nz
.addBonusLifeAtEvery50k:
ld a,(Player1Lives)
inc a
ld (Player1Lives),a
ret
.implementation:
ld bc,(100<<8) | $FF ; B = 100, C = -1
call .extractDigit
push bc
ld bc,(10<<8) | $FF ; B = 10, C = -1
call .extractDigit
; C = tens, A = ones (C on stack = hundreds)
; add the digits to the string in memory representing score
ld hl,Player1Score+5 ; start at third digit from right ("00" is fixed)
call .addDigit
ld a,c
pop bc
ld b,5
.updateDigitsLoop:
dec hl
call .addDigit
ld a,c
ld c,0
djnz .updateDigitsLoop
ret
.addDigit:
; A = current digit amount 0..10, C = next digit amount 0..9 (!)
add a,(hl)
cp '0'+10
ld (hl),a ; digit updated, check if carry has to happen
ret c ; '0'..'9' = ok, done
sub 10 ; beyond '9' -> fix char and increment next digit
ld (hl),a
inc c
ret
.extractDigit:
inc c
sub b
jr nc,.extractDigit
add a,b
ret
DecreaseBonus:
; decrement hundreds digit
ld hl,(LevelBonus) ; L = first digit char, H = second digit char
dec h
ld a,'0'-1
cp h
jr c,.writeNewValue
; hundreds digit was '0' before, wrap around or refuse to decrement when " 000"
dec l
cp l
ret nc ; value was already " 000", can decrement more, ignore
inc a
cp l
jr nz,.keepFirstDigit
ld l,' ' ; exchange first '0' with space
.keepFirstDigit:
ld h,'9' ; L is still valid 0..9, fix the hundreds digit to "9"
.writeNewValue:
ld (LevelBonus),hl
ret
EndOfLevelLogic:
ld a,(SprPlayer.y)
cp 51-16+1 ; top platform Y coordinate, compare with player posY
ret nc ; not there yet
; custom frame loop to add bonus to score
.FrameLoop:
call GameLoop_BaseThings
; abuse the jump bonus mechanics to display the end-level animation
IFDEF DISPLAY_PERFORMANCE_DEBUG_BORDER
; black border: to measure the jump bonus refresh code performance
ld a,0
out (ULA_P_FE),a
ENDIF
; switch detector off
ld hl,0
ld (JumpBonusDetection.x),hl
call JumpBonusLogic
; transfer level bonus to score
ld a,(TotalFrames)
rrca
jr nc,.FrameLoop ; only every second frame
ld hl,(LevelBonus)
ld de,"0 "
or a
sbc hl,de
jr z,.levelBonusIsDone
call DecreaseBonus
ld a,1
call AddScore
; add new star for remaining level bonus
ld a,(TotalFrames)
rrca
jr nc,.FrameLoop ; only every fourth frame
; fake detector position on Santa's bag, to emit star there
and 7
add a,7+32
ld (JumpBonusDetection.y),a
add a,48-7
ld (JumpBonusDetection.x),a
ld a,(TotalFrames)
rrca
rrca
and 3
inc a
ld (JumpBonusScore),a ; will change color of stars
; reuse the JumpBonus emitter to create the bonus star
ld iy,SprJumpStars
call JumpBonusCollisionHandler.addNewStar
jr .FrameLoop
.levelBonusIsDone:
ld b,50
.freezeScreenLoop:
push bc
call GameLoop_BaseThings
; switch detector off
ld hl,0
ld (JumpBonusDetection.x),hl
call JumpBonusLogic ; will animate the remaining bonus stars
pop bc
djnz .freezeScreenLoop
; init new level
call GameStateInit_NewLevel
ret ; return back to main GameLoop
PlayerLosesLife:
; do the animation - part 1, spin pattern 5
ld a,$80+5
ld (SprPlayer.vpat),a
ld b,25
.animateLoopPart1:
push bc
call GameLoop_BaseThings
ld a,(TotalFrames)
and 3
jr nz,.keepAnimation
; change mirrors/rotate (+1) to make chaos
ld a,(SprPlayer.mrx8)
add a,2
and $0F
ld (SprPlayer.mrx8),a
.keepAnimation:
pop bc
djnz .animateLoopPart1
; decrease lives counter
ld a,(Player1Lives)
dec a
ld (Player1Lives),a
; do the animation - part 2 (just static pattern 11)
ld a,$80+11
ld (SprPlayer.vpat),a
xor a
ld (SprPlayer.mrx8),a
ld b,30
.animateLoopPart2:
push bc
call GameLoop_BaseThings
pop bc
djnz .animateLoopPart2
; check if this is game over
ld a,(Player1Lives)
or a
jp nz,GameStateInit_NewLife ; reset state (new life), return back to GameLoop
; show game over and wait for fire, then reset state with new game
ld hl,GameOverTxt
ld de,MEM_ZX_SCREEN_4000+$800+3*32+7
ld c,9
.GameOverAnimLoop:
ld b,1 ; print only single character every 10 frames
call PrintStringHlAtDe
ld b,10
.GameOverAnimLoop2:
push bc
push hl
push de
call GameLoop_BaseThings
pop de
pop hl
pop bc
djnz .GameOverAnimLoop2
dec c
jr nz,.GameOverAnimLoop
; wait for fire on keyboard
.GameOverWaitLoop:
call GameLoop_BaseThings
call ReadInputDevices
ld a,(Player1Controls)
bit JOY_BIT_FIRE,a
jr z,.GameOverWaitLoop
; erase the game over text on screen
ld hl,GameOverTxtErase
ld de,MEM_ZX_SCREEN_4000+$800+3*32+7
ld b,9
call PrintStringHlAtDe
; wait for no-input
.GameOverReleaseLoop:
call GameLoop_BaseThings
call ReadInputDevices
ld a,(Player1Controls)
or a
jr nz,.GameOverReleaseLoop
jp GameStateInit_NewGame ; reinit all and return to main loop
;-------------------------------------------------------------------------------------
; Part 11 - jump-bonus-over-snowball scoring (logic + detection by collisions code)
JumpBonusLogic:
; refresh the bonus effect sprites
ld iy,SprJumpStars
ld de,S_SPRITE_4B_ATTR
ld bc,(TotalFrames) ; C = total Frames
ld b,JUMP_STAR_CNT
.refreshStarLoop:
bit 7,(iy+S_SPRITE_4B_ATTR.vpat)
jr z,.skipThisOne ; this one is invisible (unused)
; every second frame (for this particular star) fly up by 1px
ld a,c
xor b
rra
jr nc,.skipThisOne
dec (iy+S_SPRITE_4B_ATTR.y) ; fly up
; every fourth frame (for this particular star) modify mirror/rotate flags
rra
jr nc,.skipThisOne
ld a,(iy+S_SPRITE_4B_ATTR.mrx8)
add a,2
ld (iy+S_SPRITE_4B_ATTR.mrx8),a
; when mirror/rotate flags get back to zero, make it invisible (it's like countdown timer)
and $0E
jr nz,.skipThisOne ; still visible
ld (iy+S_SPRITE_4B_ATTR.vpat),a ; visible = OFF
.skipThisOne:
add iy,de
djnz .refreshStarLoop
; check if the jump-bonus-detection is in play (X == 0 -> it's off currently)
ld a,(JumpBonusDetection.x)
or a
ret z
; calculate the collisions against the fake "sprite" used as bonus detector
ld ix,JumpBonusDetection
; this will spawn new bonus effect sprites and add score
ld hl,JumpBonusCollisionHandler
ld iy,SprJumpStars ; pointer to the star-sprites memory for handler
jr SnowballvsSpriteCollision ; do the collisions and return
JumpBonusCollisionHandler:
; In: IX = colliding ball, B = SNOWBALLS_CNT-index_of_ball, C = total_collisions_counter
; Must preserve: BC, IX, HL, can modify: AF, DE
; check if this is new ball -> each ball gives bonus only once
ld d,high JumpBonusHitBy
ld e,b ; B = 1..SNOWBALLS_CNT (inclusive, never zero)
ld a,(de) ; DE = address into flags field
or a
ret nz ; this ball was already scored, ignore it
inc a
ld (de),a ; flag it for future test
; new ball, add the bonus score for evasion manuever
ld a,(JumpBonusScore)
add a,a ; bonus*=2
jr nz,.bonusDidDouble
inc a ; bonus=1 for first ball
.bonusDidDouble:
; this means the total bonus is: 100 for one ball, 300 for two, 700 for three, 1500 for four, ...
; (doing +100, +200, +400, +800, +1600, for each new collision...)
ld (JumpBonusScore),a
push bc
push hl
call AddScore ; add bonus to the score
pop hl
pop bc
; add new star effect sprite
.addNewStar:
; find first empty star-sprite
ld de,S_SPRITE_4B_ATTR
jr .findFirstEmptyLoopEntry
.findFirstEmptyLoop:
add iy,de
.findFirstEmptyLoopEntry:
; check if the new IY points still within the star sprites, or already at balls
ld a,JUMP_STAR_CNT*S_SPRITE_4B_ATTR-1
cp iyl
ret c ; then just return without effect sprite (score was added)
; is it invisible?
bit 7,(iy+S_SPRITE_4B_ATTR.vpat)
jr nz,.findFirstEmptyLoop
ld (iy+S_SPRITE_4B_ATTR.vpat),$80+33 ; star pattern + make it visible
ld de,(JumpBonusDetection) ; extract [x,y] of detector into DE
ld (iy+S_SPRITE_4B_ATTR.x),e ; will become star position
ld (iy+S_SPRITE_4B_ATTR.y),d
; set palette offset based on the score bonus (just for fun)
ld a,(JumpBonusScore)
neg
inc a
swapnib
and $F0 ; reset mirror/rotate and x8=0
ld (iy+S_SPRITE_4B_ATTR.mrx8),a
ret
;-------------------------------------------------------------------------------------
; the collision detection player vs snowball (only player vs balls)
SnowballvsPlayerCollision:
; player's collision handler address
ld hl,PlayerVsBallCollisionHandler
; IX to point to the player sprite (position for collision)
ld ix,SprPlayer
; call the collision checks
call SnowballvsSpriteCollision
; clear the old collisionFx sprites from previous frame
ld a,c
ld (CollisionFxCount),a ; remember new amount of collision FX sprites
or a
ret z
; there is some collision with snowball, "just die, can't ya?"
jp PlayerLosesLife
PlayerVsBallCollisionHandler:
; nothing to do here, the player will die at the end
ret
;-------------------------------------------------------------------------------------
; the collision detection of S_SPRITE_4B_ATTR vs snowballs
; with custom collision handler (the caller must override the sub-routine address)
SnowballvsSpriteCollision:
; In:
; IX = S_SPRITE_4B_ATTR instance to check balls against
; HL = collision-handler sub-routine
; Modifies:
; AF, BC, DE, HL, IX (IY is preserved) + what collision handler does
; --- collision handler ABI ---
; In: IX = colliding ball, B = SNOWBALLS_CNT-index_of_ball, C = total_collisions_counter
; Must preserve: BC, IX, HL, can modify: AF, DE
ld (.ch),hl
; read sprite position into registers
ld l,(ix+S_SPRITE_4B_ATTR.x)
ld h,(ix+S_SPRITE_4B_ATTR.mrx8)
; "normalize" X coordinate to have coordinate system 0,0 .. 255,191 (PAPER area)
; and to have coordinates of centre of player sprite (+7,+7)
; It's more code (worse performance), but data will fit into 8bit => less registers
add hl,-32+7 ; X normalized, it fits 8bit now (H will be reused)
ld a,(ix+S_SPRITE_4B_ATTR.y)
add a,-32+8
ld h,a ; Y normalized, HL = [x,y] of player for tests
ld ix,SprSnowballs
ld bc,SNOWBALLS_CNT<<8 ; B = snowballs count, C = 0 (collisions counter)
.snowballLoop:
; the collision detection will use circle formula (x*x+y*y=r*r), but we will first reject
; the fine-calculation by box-check, player must be +-15px (cetre vs centre) near ball
; to do the fine centres distance test (16*16=256 => overflow in the simplified MUL logic)
bit 7,(ix+S_SPRITE_4B_ATTR.vpat)
jr z,.skipCollisionCheck ; ball is invisible, skip the test
; read and normalize snowball pos X
ld e,(ix+S_SPRITE_4B_ATTR.x)
ld d,(ix+S_SPRITE_4B_ATTR.mrx8)
add de,-32+7 ; DE = normalized X (only E will be used later)
rr d ; check x8
jr c,.skipCollisionCheck ; ignore balls outside of 0..255 positions (half of ball visible at most)
ld a,(ix+S_SPRITE_4B_ATTR.y)
add a,-32+7+3 ; snowball sprites is only in bottom 11px of 16x16 -> +3 shift
jr nc,.skipCollisionCheck ; this ball is too high in the border (just partially visible), ignore it
sub h ; A = dY = ball.Y - player.Y
; reject deltas which are too big
ld d,a
add a,15
cp 31
jr nc,.skipCollisionCheck ; deltaY is more than +-15 pixels, ignore it
ld a,e
sub l ; A = dX = ball.X - player.X
; check also X delta for -16..+15 range
add a,15
cp 31
jr nc,.skipCollisionCheck
sub 15
; both deltas are -16..+15, use the dX*dX + dY*dY to check the distance between sprites
; the 2D distance will in this case work quite well, because snowballs are like circle
; So no need to do pixel-masks collision
ld e,d
mul de ; E = dY * dY (low 8 bits are correct for negative dY)
ld d,a
ld a,e
ld e,d
mul de ; E = dX * dX
add a,e
jr c,.skipCollisionCheck ; dY*dY + dX*dX is 256+ -> no collision
cp (6+4)*(6+4) ; check against radius 6+4px, if less -> collision
; 6px is snowball radius, 4px is the player radius, being a bit benevolent (a lot)
jr nc,.skipCollisionCheck
; collision detected, create new effectFx sprite at the snowbal possition
inc c ; collision counter
.ch=$+1:call PlayerVsBallCollisionHandler
.skipCollisionCheck:
; next snowball, do them all
ld de,S_SPRITE_4B_ATTR
add ix,de
djnz .snowballLoop
ret
;-------------------------------------------------------------------------------------
; platforms collisions
; These don't check the image pixels, but instead there are few columns accross
; the screen, and for each column there can be 8 platforms defined. These data are
; hand-tuned for the background image. Each column is 16px wide, so there are 16 columns
; per PAPER area. But the background is actually only 192x192 (12 columns), and I will
; define +1 column extra on each side in case some sprite is partially out of screen.
; Single column data is 16 bytes: 8x[height of platform, extras] where extras will be
; 8bit flags for things like ladders/etc.
GetPlatformPosUnder:
; In: IX = sprite pointer (centre x-coordinate is used for collision, i.e. +8)
; Out: A = platform coordinate (in sprite coordinates 0..255), C = extras byte
; for X coordinate outside of range, or very low Y the [A:255, C:0] is always returned
push hl
call .implementation
; returns through here only when outside of range or no platform found
ld a,255
ld c,0
pop hl
ret
.implementation:
bit 0,(ix+S_SPRITE_4B_ATTR.mrx8)
ret nz ; 256..511 is invalid X range (no column data)
ld a,(ix+S_SPRITE_4B_ATTR.x)
sub 16-8 ; -16 to skip 16px, +8 to test centre of sprite (not left edge)
ret c ; 0..7 is invalid X range (no column data)
; each column is 8 platforms x2 bytes = 16 bytes -> the X coordinate top 4 bits
; are indentical to address of particular column! (no need to multiply/divide)
cp low PlatformsCollisionDataEnd
ret nc ; 224 <= (X-16) -> invalid X range (224 = 14*16) - 14 columns are defined
and -16 ; clear the bottom four bits of X -> becomes low-byte of address
ld l,a
ld h,high PlatformsCollisionData ; HL = address of column data
ld a,(ix+S_SPRITE_4B_ATTR.y) ; raw sprite Y (top edge)
cp 255-13
ret nc ; already too low to even check (after +13 only 13..254 are valid for check)
add a,13 ; the base-line coordinate, the sprite can be 2px deep into platform to "hit" it
; now we are ready to compare against the data in column table
jr .columnDataLoopEntry
.columnDataLoop:
inc l
inc l
.columnDataLoopEntry:
cp (hl)
jr nc,.columnDataLoop ; platformY <= spriteY_base_line -> will not catch this one
; this platform is below baseline, report it as hit
ld a,(hl)
inc l
ld c,(hl)
pop hl ; discard return address from .implementation
pop hl ; restore HL
ret ; return directly to caller with results in A and C
;-------------------------------------------------------------------------------------
; "AI" subroutines - player movements
Player1MoveByControls:
; update "cooldown" of controls if there's some delay needed
ld a,(Player1ControlsCoolDown)
sub 1 ; SUB to update also carry flag
adc a,0 ; clamp the value to 0 to not go -1
ld (Player1ControlsCoolDown),a
ret nz ; don't do anything with player during "cooldown"
ld ix,SprPlayer
; calculate nearest platform at x-3 and x+3
ld l,(ix+S_SPRITE_4B_ATTR.x)
ld a,l
sub 3 ; not caring about edge cases, only works for expected X values
ld (ix+S_SPRITE_4B_ATTR.x),a ; posX-3
call GetPlatformPosUnder
sub 16 ; player wants platform at +16 from player.Y
ld h,a
ld a,l
add a,3
ld (ix+S_SPRITE_4B_ATTR.x),a
call GetPlatformPosUnder
sub 16 ; player wants platform at +16 from player.Y
ld (ix+S_SPRITE_4B_ATTR.x),l ; restore posX
cp h
jr nc,.keepHigherPlatform
ld h,a
.keepHigherPlatform:
; H = -16 + min(PlatformY[-3], PlatformY[+3]), C = extras of right platform, L = posX
ld a,(Player1JumpIdx)
or a
jr z,.notInTableJump
.doTheTableJumpFall:
; table jump/fall .. keep doing it until landing (no controls accepted)
ld e,a
ld d,high PlayerJumpYOfs ; address of current DeltaY
cp 255
adc a,0 ; increment it until it will reach 255, then keep 255
ld (Player1JumpIdx),a
; adjust posX by jump direction (3 of 4 frames)
ld a,(TotalFrames)
and 3
jr z,.skipJumpPosXupdate
ld a,(Player1JumpDir)
call .updateXPosAplusL
.skipJumpPosXupdate:
; adjust posY by jump/fall table
ld a,(de) ; deltaY for current jumpIdx
add a,(ix+S_SPRITE_4B_ATTR.y)
cp h ; compare with platform Y
jr z,.didLand
jr nc,.didLand
; still falling
ld (ix+S_SPRITE_4B_ATTR.y),a
ret
.didLand:
ld (ix+S_SPRITE_4B_ATTR.y),h ; land *at* platform precisely
xor a
ld (Player1JumpIdx),a ; next frame do regular AI (no more jump table)
ld (JumpBonusDetection.x),a ; switch off jump-bonus detector OFF
ld (ix+S_SPRITE_4B_ATTR.vpat),$80+4 ; landing sprite
ld a,4 ; keep landing sprite for 4 frames
ld (Player1ControlsCoolDown),a
; check if landing was too hard
ld a,(Player1SafeLandingY)
cp h
ret nc
; lands too hard, "die" - just disable controls for 1s for now
jp PlayerLosesLife
.notInTableJump:
ld a,(Player1Controls)
ld b,a ; keep control bits around in B for controls checks
; H = -16 + min(PlatformY[-3], PlatformY[+3]), C = extras of right platform, L = posX, B = controls
; check if already hangs on ladder
ld de,(Player1LadderData) ; current ladder top+tall info (E=top,D=tall)
ld a,d
or a
jp nz,.isGrabbingLadder
; if landing pattern, turn it into normal pattern
ld a,(ix+S_SPRITE_4B_ATTR.vpat)
cp $80+4
jr c,.runningSprite
; landing pattern (or unhandled unknown state :) )
ld (ix+S_SPRITE_4B_ATTR.vpat),$80 ; reset sprite to 0 and continue with "running"
.runningSprite:
; if regular pattern, do regular movement handling
; check if stands at platform +-1px (and align with platform)
ld a,(ix+S_SPRITE_4B_ATTR.y)
sub h
inc a ; -1/0/+1 -> 0/+1/+2
cp 3
jr c,.almostAtPlatform
; too much above platform, turn it into freefall (table jump)
xor a
ld (Player1JumpDir),a
ld a,low PlayerFallYOfs
jr .doTheTableJumpFall ; and do the first tick of fall this frame already
.almostAtPlatform:
ld (ix+S_SPRITE_4B_ATTR.y),h ; place him precisely at platform
; refresh safe landing Y
ld a,18
add a,h
ld (Player1SafeLandingY),a
; C = extras of right platform, L = posX, H = posY, B = user controls
bit JOY_BIT_FIRE,b
jr z,.notJumpPressed
; start a new jump sequence
; clear the jump-bonus flag field, and initialize the detector to count bonus
push bc
push hl
ld hl,JumpBonusScore
ld de,JumpBonusScore+1
ld bc,SNOWBALLS_CNT ; clears JumpBonusScore and JumpBonusHitBy field
ld (hl),b ; B = 0
ldir
pop hl
pop bc
; now start the jump-sequence itself
ld a,$80+3 ; jump pattern + visible
ld (ix+S_SPRITE_4B_ATTR.vpat),a
xor a
bit JOY_BIT_UP,b ; up/down prevents the side jump
jr nz,.noRightJump
bit JOY_BIT_DOWN,b
jr nz,.noRightJump
bit JOY_BIT_LEFT,b
jr z,.noLeftJump
dec a
.noLeftJump:
bit JOY_BIT_RIGHT,b
jr z,.noRightJump
inc a
.noRightJump:
ld (Player1JumpDir),a
; set detector position to current player position advancing it +-4px jump-dir ahead
add a,a
add a,a
add a,l
ld (JumpBonusDetection.x),a
ld a,h
ld (JumpBonusDetection.y),a
; start the jump-table movement
ld a,low PlayerJumpYOfs
jp .doTheTableJumpFall ; and do the first tick of jump this frame already
.notJumpPressed:
; C = extras of right platform, L = posX, B = user controls
; check if up/down is pressed during regular running -> may enter ladder or stand
ld a,b
and (1<<JOY_BIT_UP)|(1<<JOY_BIT_DOWN)
jp z,.noUpOrDownPressed
; ladder mechanics - grab the near ladder if possible
bit 1,c ; check platform "extras" flag if ladder is near
ret z ; no ladder near, just keep standing
; check if some ladder can be grabbed by precise positions and controls pressed