-
Notifications
You must be signed in to change notification settings - Fork 4
/
SpecBong.asm
1270 lines (1191 loc) · 55 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)
; TODO plan to final release:
; - upon jump "drop" invisible sprite "egg" to calculate collisions against balls
; - track the hits of "egg" (only once per ball) and score it: first +100, next: 3x (300, 900, 2700, ...)
; - display "egg" bonus to player with delayed decay (needs probably two of these, but only single "egg" itself)
; - add logic: total score, total lives, bonus-timer
; - reset level, end of level, death of player, game over
; 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
; 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,0 ; DEBUG - switch to 3.5MHz for fun
; also to show how powerful the new HW features are, or in other way,
; how little you can do in 3.5MHz and how optimized the classic games
; must be to achieve anything more complex
; 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 SNOWBALLS_CNT snowballs and one player - the in-memory copy of sprite attributes
; init them at some debug positions, they just fly around mindlessly
ld ix,SprSnowballs ; IX = address of first snowball sprite
ld b,SNOWBALLS_CNT-1 ; define all of them except last one
ld hl,0 ; HL will generate X positions
ld e,32 ; E will generate Y positions
ld d,$80 + 52 ; visible sprite + snowball pattern (52, second is 53)
InitBallsLoop:
; set current ball data
ld (ix+S_SPRITE_4B_ATTR.x),l
ld (ix+S_SPRITE_4B_ATTR.y),e
ld (ix+S_SPRITE_4B_ATTR.mrx8),h ; clear pal offset, mirrors, rotate, set x8
ld (ix+S_SPRITE_4B_ATTR.vpat),d
; adjust initial position and pattern for next ball
add hl,13 ; 13*32 = 416: will produce X coordinates 0..511 range only
ld a,e
add a,5
ld e,a ; 5*32 = 160 pixel spread vertically
ld a,d
xor 1 ; alternate snowball patterns between 52/53
ld d,a
; advance IX to point to next snowball
push de
ld de,S_SPRITE_4B_ATTR
add ix,de
pop de
djnz InitBallsLoop
; init the last snowball for testing collisions code (will just stand near bottom)
ld (ix+S_SPRITE_4B_ATTR.x),100
ld (ix+S_SPRITE_4B_ATTR.y),192
ld (ix+S_SPRITE_4B_ATTR.mrx8),0
ld (ix+S_SPRITE_4B_ATTR.vpat),d
; init game state for new game
call GameStateInit_NewGame
; main loop of the game
GameLoop:
; 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 pattern-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
IFDEF DISPLAY_PERFORMANCE_DEBUG_BORDER
; magenda border: to measure the AI code performance
ld a,3
out (ULA_P_FE),a
ENDIF
; adjust sprite attributes in memory pointlessly (in debug way) just to see some movement
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
; cyan border: to measure the collisions code performance
ld a,5
out (ULA_P_FE),a
ENDIF
call SnowballvsPlayerCollision
; DEBUG add some random value to score to verify it works (every 63 frames)
ld a,(TotalFrames)
and $3F
call z,DebugRandomScoreAdd
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
DebugRandomScoreAdd: ; DEBUG
call Rand16
ld a,l
xor h ; 0..255 to add to score (score is displayed as *100, so it's +0..+25,500)
call AddScore
call DecreaseBonus
ret
;-------------------------------------------------------------------------------------
; Part 10 - UI drawing routines - using transparent ULA layer above everything
GameStateInit_NewGame:
; 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:
; currently there's nothing special about new level
; |
; fallthrough to GameStateInit_NewLife
; |
; v
GameStateInit_NewLife:
; 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
dec a ; A = 255
ld (Player1SafeLandingY),a
;TODO snowball AI reset + sprites reset
ret
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
; "score" label attr
ld hl,MEM_ZX_ATTRIB_5800 + 0*32 + 24
ld (hl),P_WHITE|WHITE|A_BRIGHT
ld c,8
call .FillAttribs
; score value attr
ld hl,MEM_ZX_ATTRIB_5800 + 1*32 + 24
ld (hl),P_WHITE|YELLOW|A_BRIGHT
ld c,8
call .FillAttribs
; "bonus" label attr
ld hl,MEM_ZX_ATTRIB_5800 + 2*32 + 24
ld (hl),P_WHITE|WHITE|A_BRIGHT
ld c,8
call .FillAttribs
; bonus value attr
ld hl,MEM_ZX_ATTRIB_5800 + 3*32 + 24
ld (hl),P_WHITE|CYAN|A_BRIGHT
ld c,8
call .FillAttribs
; "lives" label attr
ld hl,MEM_ZX_ATTRIB_5800 + 4*32 + 24
ld (hl),P_WHITE|WHITE|A_BRIGHT
ld c,8
call .FillAttribs
; print the static labels
ld de,MEM_ZX_SCREEN_4000 + 0*32 + 24
ld hl,ScoreLabelTxt
ld b,8
call PrintStringHlAtDe
ld de,MEM_ZX_SCREEN_4000 + 1*32 + 24
ld hl,Player1Score
ld b,8
call PrintStringHlAtDe
ld de,MEM_ZX_SCREEN_4000 + 2*32 + 24
ld hl,BonusLabelTxt
ld b,8
call PrintStringHlAtDe
ld de,MEM_ZX_SCREEN_4000 + 3*32 + 28
ld hl,LevelBonus
ld b,4
call PrintStringHlAtDe
ld de,MEM_ZX_SCREEN_4000 + 4*32 + 24
ld hl,LivesLabelTxt
ld b,8
call PrintStringHlAtDe
ret
.FillAttribs:
ld d,h
ld e,l
inc de
dec bc
ldir
ret
RefreshUi:
; refresh the score
ld de,MEM_ZX_SCREEN_4000 + 1*32 + 24
ld hl,Player1Score
ld b,8
call PrintStringHlAtDe
; refresh the bonus score
ld de,MEM_ZX_SCREEN_4000 + 3*32 + 28
ld hl,LevelBonus
ld b,4
call PrintStringHlAtDe
; refresh the lives UI (it's shown with sprites :) )
ld ix,SprLivesUi
ld b,0
ld a,(Player1Lives)
ld c,a ; amount of lives to show (others to hide)
ld hl,32+24*8+2
.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+5*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
inc b
cp 5 ; show at most 6 lives sprites
jr c,.livesUiSetSpriteLoop
ret
AddScore:
; In: A = score to add (0..255, score is automatically *100)
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
;-------------------------------------------------------------------------------------
; the collision detection player vs snowball (only player vs balls)
SnowballvsPlayerCollision:
; read player position into registers
ld ix,SprPlayer
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
; init IY to point to specialFX dynamic part of sprites displaying collision effect
ld iy,SprCollisionFx
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+5)*(6+5) ; check against radius 6+5px, if less -> collision
; 6px is snowball radius, 5px 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
ld e,(ix+S_SPRITE_4B_ATTR.x) ; copy the data from snowball sprite
ld a,(ix+S_SPRITE_4B_ATTR.y)
add a,2 ; +2 down
ld d,(ix+S_SPRITE_4B_ATTR.mrx8)
ld (iy+S_SPRITE_4B_ATTR.x),e
ld (iy+S_SPRITE_4B_ATTR.y),a
ld (iy+S_SPRITE_4B_ATTR.mrx8),d
ld (iy+S_SPRITE_4B_ATTR.vpat),$80 + 61 ; pattern 61 + visible
ld de,S_SPRITE_4B_ATTR
add iy,de ; advance collisionFx sprite ptr
.skipCollisionCheck:
; next snowball, do them all
ld de,S_SPRITE_4B_ATTR
add ix,de
djnz .snowballLoop
; clear the old collisionFx sprites from previous frame
ld a,(CollisionFxCount)
sub c
jr c,.noFxToRemove
jr z,.noFxToRemove
ld b,a ; fx sprites to make invisible
.removeFxLoop:
ld (iy+S_SPRITE_4B_ATTR.vpat),d ; DE = 4 -> D=0
add iy,de
djnz .removeFxLoop
.noFxToRemove:
ld a,c
ld (CollisionFxCount),a ; remember new amount of collision FX sprites
; also modify players palette offset by count of collisions (for fun)
swapnib
and $F0
ld c,a
ld ix,SprPlayer
ld a,(ix+S_SPRITE_4B_ATTR.mrx8)
and $0F
or c
ld (ix+S_SPRITE_4B_ATTR.mrx8),a
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 (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
ld a,50
ld (Player1ControlsCoolDown),a
ret
.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, B = user controls
bit JOY_BIT_FIRE,b
jr z,.notJumpPressed
; start a new jump sequence
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
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
ld c,LaddersCount+1
ld iy,LaddersData-S_LADDER_DATA
.LadderCheckLoop:
dec c
ret z ; no ladded is aligned enough, keep standing
ld de,S_LADDER_DATA
add iy,de ; advance pointer into ladder data
; check Y coordinates of ladder (top/bottom can be entered with correct key)
ld e,(iy+S_LADDER_DATA.y)
ld d,(iy+S_LADDER_DATA.t)
ld a,e
cp (ix+S_SPRITE_4B_ATTR.y)
jr nz,.notAtTopOfLadder
; if at top ladder position, check if control down is pressed and check X-coordinate
bit JOY_BIT_DOWN,b
jr z,.LadderCheckLoop ; not pressing "down", ignore it
jr .checkLadderXCoord
.notAtTopOfLadder:
add a,d
cp (ix+S_SPRITE_4B_ATTR.y)
jr nz,.LadderCheckLoop ; not at bottom position of ladder, ignore it
bit JOY_BIT_UP,b
jr z,.LadderCheckLoop ; not pressing "up", ignore it
.checkLadderXCoord:
ld h,(iy+S_LADDER_DATA.x)
ld a,h
sub l
add a,2 ; -2..+2 -> 0..+4
cp 5
jr nc,.LadderCheckLoop ; this ladder is more than +-2px away -> ignore it
; grab the ladder
ld l,h ; refresh also local copy of posX in L
ld (ix+S_SPRITE_4B_ATTR.x),l ; align with ladder on X-axis
ld (Player1LadderData),de ; current top+tall info
; following is regular ladder AI, after it was grabbed previously
.isGrabbingLadder:
; modify posY by controls
ld a,(TotalFrames)
and 1 ; only 2 of 4 frames the inputs are read
ld a,(ix+S_SPRITE_4B_ATTR.y)
jr z,.notClimbing ; but do the rest of ladder handler to set pattern/etc
bit JOY_BIT_DOWN,b
jr z,.notDescending
inc a
.notDescending:
bit JOY_BIT_UP,b
jr z,.notClimbing
dec a
.notClimbing:
; sanitize the posY
sub e
adc a,0 ; A=0..tall+1 where he is on the ladder (-1 fixed by ADC)
jr z,.atTopOrBottom
dec a
cp d ; Fc=(A-1) - tall (will be "no-carry" only when posY==tall+1)
adc a,0 ; clamps A to 0..tall range only
cp d
jr z,.atTopOrBottom
; somewhere in the middle of ladder, convert 1..tall-1 position into sprite pattern and posY
add a,e
ld (ix+S_SPRITE_4B_ATTR.y),a
sub e
cp LadderPatternDataSZ
jr c,.getClimbPatternFromTable
; beyond pre-defined table, so wrap around the last 8 patterns
and LadderPatternWrapMask
add a,LadderPatternWrapOfs
.getClimbPatternFromTable:
ld de,LadderPatternData
add de,a
ld a,(de)
ld (ix+S_SPRITE_4B_ATTR.vpat),a
ret
.atTopOrBottom:
add a,e
ld (ix+S_SPRITE_4B_ATTR.y),a
ld (ix+S_SPRITE_4B_ATTR.vpat),$80+10 ; standing pattern
ld a,b
and (1<<JOY_BIT_LEFT)|(1<<JOY_BIT_RIGHT)
ret z ; no left/right controls, stay on ladder standing
; player wants to leave the ladder, clear the current ladder data
xor a
ld (Player1LadderData+1),a ; clear the "tall" value to zero
; follow with regular running handler, that will resolve pattern/etc (L is up to date)
; |
; v
.noUpOrDownPressed:
; L = posX, B = user controls
bit JOY_BIT_LEFT,b
jr z,.notGoingLeft
; move left
set 3,(ix+S_SPRITE_4B_ATTR.mrx8) ; set MirroX flag (to face left)
ld a,(TotalFrames)
and 3
jr z,.animateRunPattern ; one frame out of 4 don't move but animate (0.75px speed)
ld a,-1
jr .updateXPosAplusL
.notGoingLeft:
bit JOY_BIT_RIGHT,b
jr z,.notGoingRight
; move right
res 3,(ix+S_SPRITE_4B_ATTR.mrx8) ; reset MirroX flag (to face right)
ld a,(TotalFrames)
and 3
jr z,.animateRunPattern ; one frame out of 4 don't move but animate (0.75px speed)
ld a,+1
jr .updateXPosAplusL
.animateRunPattern:
; animate pattern 0..3 frames (in case he holds left or right) (if not, it will be zeroed)
ld a,(ix+S_SPRITE_4B_ATTR.vpat)
inc a
and %1'0'000011 ; force pattern to stay 0..3 (+visible flag)
ld (ix+S_SPRITE_4B_ATTR.vpat),a
ret
.notGoingRight:
; not going anywhere
ld (ix+S_SPRITE_4B_ATTR.vpat),$80 ; reset to frame 0
ret
.updateXPosAplusL:
add a,l ; add A and L to get final posX
cp 32
ret c ; way too left, don't update
cp 32+192-16+1
ret nc ; way too right, don't update
ld (ix+S_SPRITE_4B_ATTR.x),a ; valid X: 32..32+192-16
ret
;-------------------------------------------------------------------------------------
; "AI" subroutines - snowballs "AI"
SnowballsAI:
ld ix,SprSnowballs
ld de,S_SPRITE_4B_ATTR
ld b,SNOWBALLS_CNT-1 ; move all of them except last
.loop:
bit 7,(ix+S_SPRITE_4B_ATTR.vpat)
jr z,.doNextSnowball ; if the sprite is not visible, don't process it
; check the Y coordinate against platform's collisions
call GetPlatformPosUnder
sub 16 ; snowball wants platform at +16 (bottom of ball is bottom of sprite)
cp (ix+S_SPRITE_4B_ATTR.y)
jr z,.isInOrAtPlatform ; at plaform
jr c,.isInOrAtPlatform ; in platform (will reset Y to be *at*)
ld l,a ; keep the platformY for compare
; falling, keep the previous direction (extracted from mirroX flag into C=0/1)
ld c,0
bit 3,(ix+S_SPRITE_4B_ATTR.mrx8) ; mirroX bit
jr z,.fallingRight
inc c ; falling left
.fallingRight
ld a,(ix+S_SPRITE_4B_ATTR.y)
inc a ; Y += 1
cp l ; did it land at platform now?
adc a,0 ; if not, do another Y += 1 (falling at +2 speed)
; continue with "in/at platform" code, but keeps direction, sets new fall Y
.isInOrAtPlatform:
ld (ix+S_SPRITE_4B_ATTR.y),a
; check if the ball is not under the screen, make it invisible then (+simpler AI)
rl (ix+S_SPRITE_4B_ATTR.vpat) ; throw away visibility flag
cp 192+32 ; Fc=1 if (Y < 192+32)
rr (ix+S_SPRITE_4B_ATTR.vpat) ; set new visibility from carry
; if at platform, choose direction by platform extras
rr c ; extras bit 0 to carry
sbc a,a ; 0 = right, $FF = left
; HL = current X coordinate (9 bit)
ld l,(ix+S_SPRITE_4B_ATTR.x)
ld h,(ix+S_SPRITE_4B_ATTR.mrx8)
ld c,0 ; mirrorX flag = 0
sli a ; Right: A=+1 Fc=0 || Left: A=-1 Fc=1
; do: HL += signed(A) (the "add hl,a" is "unsigned", so extra jump+adjust needed)
jr nc,.moveRight
dec h
ld c,$08 ; mirrorX flag = 1
.moveRight:
add hl,a
; put H and C together to work as palette_offset/mirror/rotate bits with X8 bit
ld a,h
and 1 ; keep only "x8" bit
or c ; add desired mirrorX bit
; store the new X coordinate and mirror/rotation flags
ld (ix+S_SPRITE_4B_ATTR.x),l
ld (ix+S_SPRITE_4B_ATTR.mrx8),a
; alternate pattern between 52 and 53 - every 8th frame (in 50Hz: 6.25FPS=160ms)
; the 8th frame check is against B (counter), so not all sprites update at same frame
ld a,(TotalFrames)
xor b
and 7
jr nz,.doNextSnowball
ld a,(ix+S_SPRITE_4B_ATTR.vpat)
xor 1
ld (ix+S_SPRITE_4B_ATTR.vpat),a
.doNextSnowball:
add ix,de ; next snowball
djnz .loop ; do all of them
ret
;-------------------------------------------------------------------------------------
; utility subroutines
PrintStringHlAtDe:
; In: HL = string address, DE = ULA VRAM address, B = length of string
; modifies: HL,DE,B
push hl
ld l,(hl)
; do HL = MEM_ROM_CHARS_3C00 + char_value*8
add hl,hl ; regular ASCII should fit into L only
ld h,(high MEM_ROM_CHARS_3C00)/4
.2 add hl,hl ; remaining 4x to get final address of font data
.8 ldws ; 8x copy font data to ULA VRAM