-
Notifications
You must be signed in to change notification settings - Fork 0
/
cursor.z80
660 lines (592 loc) · 25.5 KB
/
cursor.z80
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
; TODO diagonals (should be supported)
keyrepeat_ctr: defb 0 ; counts how long a key was held down for (before repeating)
keyrepeat_dirs: defb 0 ; what keys were held down since last time
fire_pressed: defb 0 ; whether fire was held down since last time
key_delay: equ 8
key_repeat: equ 6
cursor_moved: defb 0 ; 0 if cursor did not move since last frane; nonzero if it did
old_cursor_hl: defw 0 ; handy way to keep track of where the cursor was (screen memory) last frame
controller_flags_xoffs: defb 0 ; x-offset
controller_flags_yoffs: defb 0 ; y-offset
init_cursor_controller:
xor a
ld (keyrepeat_ctr), a
ld (keyrepeat_dirs), a
ld (fire_pressed), a
ld (cursor_moved), a
ld (old_cursor_hl), a
ld (old_cursor_hl+1), a
ld (controller_flags_xoffs), a
ld (controller_flags_yoffs), a
ld a, (controller_flags_xy_pos)
ret
erase_old_cursor:
; TODO optimize and be smarter?
ld a, (cursor_moved)
and a
ret z
ld hl, (old_cursor_hl)
xor a ; because palette black is 0 ; TODO BACKGROUND IMAGES
call box_at_hl
ret
draw_cursor:
; is the cursor on a tile with fire still held?
; if so, we follow the tile
ld a, (controller_flags_tile_captured)
and a
jr z, @tile_not_captured
ld a, (controller_flags_xy_pos)
ld l, a
ld h, (tiles-1)/256
; is tile falling
ld a, (hl)
bit IS_FALLING_BIT, a
jr nz, @captured_falling_tile ; tile is falling so y-offset matches tile
bit IS_SLIDER_BIT, a
jr nz, @captured_sliding_tile ; tile is sliding so y-offset or x-offset matches tile (according to slider direction)
jp @tile_not_captured
@captured_sliding_tile:
; is it vertical or horizontal?
; if vertical, use same logic as falling tile (handy)
ld hl, slider_info
bit SLIDER_AXIS_BIT, (hl)
jr z, @captured_vertical_slider
@captured_horizontal_slider:
and 15
rra
ld l, a
ld a, (controller_flags_xy_pos)
ld c, a ; backup
call grid_xy_in_C_and_A_to_screen_de
ld a, e
add l
ld l, a
ld h, d
jp @+_draw_cursor
@captured_vertical_slider:
@captured_falling_tile:
; get the offset for top-left into hl
and 15
ld h, a
ld l, 0
srl h
rr l
ld a, (controller_flags_xy_pos)
ld c, a ; backup
call grid_xy_in_C_and_A_to_screen_de
ld a, e
add l
ld l, a
ld a, d
add h
ld h, a
jp @+_draw_cursor
@tile_not_captured:
ld a, (controller_flags_xy_pos)
ld c, a ; backup
call grid_xy_in_C_and_A_to_screen_de
; cursor draw expects screen location in hl not de
ex de, hl
; TODO only draw the cursor if the flashing counter tells us to
; i.e. redraw the title where the cursor would be, the cursor is flashing
; what does that mean? well, if fire is held then we draw the cursor
; for four frames and then hide it for four frames, etc. we have a ctr for it.
;ld a, (controller_flags_flashing_ctr)
;and 4
;ret nz ; four frames with bit cleared => draw cursor. four frames with bit set => don't draw cursor
; however we don't really want to have to draw the whole tile under the cursor, just the bit
; where the cursor is.
; so, cheat for now. draw the cursor always, but in one of two colours.
; red usually, but white/red if flashing.
@_draw_cursor:
ld a, (controller_flags_flashing_ctr)
and 4
jr nz, @white
@red:
ld a, palette_index_red * 17
ld (old_cursor_hl), hl
call box_at_hl
ret
@white:
ld a, palette_index_white * 17
ld (old_cursor_hl), hl
call box_at_hl
ret
update_cursor_position:
; update the data structures associated with the current cursor position and state
;
; initially assume cursor did not move
xor a
ld (cursor_moved), a
; controller can be periodically disabled during game logic
; so check that first
ld a, (controller_flags_disabled)
and a
ret nz
@check_fire:
; If fire is not held, then cursor moves up/down/left/right,
; subject to boundaries (i.e. cannot pass through tile==2)
;
; If fire is held, and the cell contains a tile, then you can only
; move left or right if the tile can move left or right
; NB CANNOT MOVE UP OR DOWN, WITH FIRE HELD!
;
; If fire is held, and the cell does not contain a tile, then cursor
; moves just as if fire is not held
ld a, (controller_flags_fire)
and a
jp nz, @+check_dirs_with_fire_pressed
; fire was not pressed
; reset the flashing cursor counter
; reset the 'captured' flag
; reset the 'fire was held down' flag
xor a
ld (controller_flags_tile_captured), a
ld (controller_flags_flashing_ctr), a
ld (fire_pressed), a
@movements_no_tile:
; either fire was not pressed (as fall through from above)
; or fire was pressed but we're not over a block that can move
@check_directions:
ld a, (controller_flags_dirs)
; if NOTHING to do, do nothing
and a
jr nz, @some_directions_are_pressed
xor a
ld (keyrepeat_dirs), a
ld (keyrepeat_ctr), a
ret
@some_directions_are_pressed:
; if the same directions were held down from before,
; do nothing (unless keyrepeat tells us to do something)
ld b, a ; backup
ld a, (keyrepeat_dirs)
cp b
jr z, @same_dirs_still_held
ld a, key_delay
ld (keyrepeat_ctr), a
ld a, b
ld (keyrepeat_dirs), a
jr @check_up
@same_dirs_still_held:
ld a, (keyrepeat_ctr)
dec a
ld (keyrepeat_ctr), a
ret nz
ld a, key_repeat
ld (keyrepeat_ctr), a
ld a, b
@check_up:
; was UP pressed?
bit controller_flags_dirs_up_bit,a
jr z, @+check_down ; no, up was not pressed
; yes, up was pressed
; todo: also check left/right for diagonal movement
; get new position
; (first get current position)
ld a, (controller_flags_xy_pos)
sub 16 ; go up a row
; see if the tile in the new position is
; penetrable...
ld l, a
ld h, tiles/256
ld a, (hl)
cp IMPENETRABLE_TILE
ret z
; all good, we can move there
call trigger_sfx_cursor
ld a, l
ld (controller_flags_xy_pos), a
; mark the OLD location as dirty
; (old location was the row below this one)
add a, 16
call push_dirty_tile
ld a, 1 ; TODO optimize, a is already nonzero
ld (cursor_moved), a
ret
@check_down:
; was DOWN pressed?
bit controller_flags_dirs_down_bit,a
jr z, @+check_left ; no, down was not pressed
; yes, down was pressed
; todo: also check left/right for diagonal movement
; get new position
; (first get current position)
ld a, (controller_flags_xy_pos)
add a, 16
; see if the tile in the new position is
; penetrable...
ld l, a
ld h, tiles/256
ld a, (hl)
cp IMPENETRABLE_TILE
ret z
; all good, we can move there
call trigger_sfx_cursor
ld a, l
ld (controller_flags_xy_pos), a
; mark the OLD location as dirty
; (old location was the row above this one)
sub 16
call push_dirty_tile
ld a, 1 ; TODO optimize, a is already nonzero
ld (cursor_moved), a
ret
@check_left:
; was LEFT pressed
bit controller_flags_dirs_left_bit,a
jr z, @+check_right ; no
; yes, left was pressed
; get new position
; (first get current position)
ld a, (controller_flags_xy_pos)
dec a
; see if the tile in the new position is
; penetrable...
ld l, a
ld h, tiles/256
ld a, (hl)
cp IMPENETRABLE_TILE
ret z
; all good, we can move there
call trigger_sfx_cursor
ld a, l
ld (controller_flags_xy_pos), a
; mark the OLD location as dirty
; (old location was the column right of this one)
inc a
call push_dirty_tile
ld a, 1 ; TODO optimize, a is already nonzero
ld (cursor_moved), a
ret
@check_right:
; was RIGHT pressed?
bit controller_flags_dirs_right_bit,a
; ; jr z, @+done ; no ; TODO OPTIMIZE AS RET Z
ret z
; yes, right was pressed
; get new position
; (first get current position)
ld a, (controller_flags_xy_pos)
inc a
; see if the tile in the new position is
; penetrable...
ld l, a ; e = y*12 + x
ld h, tiles/256
ld a, (hl)
cp IMPENETRABLE_TILE
ret z
; all good, we can move there
call trigger_sfx_cursor
ld a, l
ld (controller_flags_xy_pos), a
; mark the OLD location as dirty
; (old location was the column left of this one)
dec a
call push_dirty_tile
ld a, 1 ; TODO optimize, a is already nonzero
ld (cursor_moved), a
ret
@check_dirs_with_fire_pressed:
; first some admin:
; FIRE is held down - so increment frame counter for flashing
; cursor if the flag for 'is captured' is already set
; (implying it is "still" held down)
ld a, (controller_flags_tile_captured)
and a
jr z, @+skip_capture
ld a, (controller_flags_flashing_ctr)
inc a
@skip_capture:
; (flashing_ctr is either incremented, or zero. neat.)
ld (controller_flags_flashing_ctr), a
; also, trigger a sound effect just for pressing fire
ld a, (fire_pressed)
and a
jr nz, @+skip_sfx
inc a
ld (fire_pressed), a
call trigger_sfx_shove
@skip_sfx:
; Now for the movement (i.e. push left / push right)
; There are a lot of cases to consider here
; TODO optimise. We should be able to make determination here pretty
; quickly, and right now there's quite a lot of back-and-forth
; 1. determine if there is a (movable) tile at current position
; (first get current position)
ld a, (controller_flags_xy_pos)
ld l, a
ld h, tiles/256
ld a, (hl)
; is it moveable tile (i.e. >= NUM_NON_TILES)
cp NUM_NON_TILES
; No? ok, then just move like there's no tile (i.e. ignore fact FIRE is held)
jp c, @movements_no_tile
; Yes? Ok, it's a moveable tile, great, let's keep checking other conditions.
; 2. is it already falling maybe? can't move a falling tile
dec h ; (point to tiles_flags)
ld a, (hl)
bit IS_FALLING_BIT, a
; if it's falling, don't permit any movements at all
; TODO check gameplay, is this correct? feels correct but that's just my guess.
ret nz
; 3. similarly, it might not be falling yet (but the tile under
; it could be mid falling, so effectively we're in 'mid air') - same rule
; applies - you can't move it if it's in mid air
; TODO optimise, now we're pushing and popping HL, ugh.
; TODO does this condition apply for something that has landed 'onto' where
; the slider is (or will be)?
push hl
ld a, l
add 16
ld l, a
ld a, (hl)
pop hl
bit IS_FALLING_BIT, a
; if it's 'effectively in mid air' then again ignore the FIRE and just
; move cursor as if there's no tile being controlled
jp nz, @movements_no_tile
; ok fire was pressed AND we have a moveable tile under us
; now the excitement begins!!
; Mark as captured (we reset this when FIRE is let go)
ld a, (controller_flags_tile_captured)
and a
jr nz, @we_already_captured_earlier
ld a, 1
ld (controller_flags_tile_captured), a
; also - essentially this means the cursor moved because it may have jumped in position
; now, to meet the tile that was in this location
ld (cursor_moved), a
ld a, 4 ; seed with 4 which is when we first change cursor colour (so that the colour change is immediate on this frame)
ld (controller_flags_flashing_ctr), a
@we_already_captured_earlier:
; are we also pressing left or right?
ld a, (controller_flags_dirs)
and %00001111
; if not, do nothing else
jr nz, @+some_directions_are_pressed
xor a
ld (keyrepeat_dirs), a
ld (keyrepeat_ctr), a
ret
@some_directions_are_pressed:
; if the same directions were held down from before,
; do nothing (unless keyrepeat tells us to do something)
ld b, a ; backup
ld a, (keyrepeat_dirs)
cp b
jr z, @+same_dirs_still_held
ld a, key_delay
ld (keyrepeat_ctr), a
ld a, b
ld (keyrepeat_dirs), a
jp @check_push_left
@same_dirs_still_held:
ld a, (keyrepeat_ctr)
dec a
ld (keyrepeat_ctr), a
ret nz
ld a, key_repeat
ld (keyrepeat_ctr), a
ld a, b
@check_push_left:
; This implements the shove_left action.
; was LEFT pressed
bit controller_flags_dirs_left_bit, a
jp z, @+check_push_right ; no
; yes, left was pressed (with fire)
call trigger_sfx_cursor
; get new position
; with 'old' position in D and 'new' position in E
ld d, l
ld e, l
dec e ; dec = 'left'
call can_I_push_here
ret nz
; all good, we can move there
; copy old tile to new place
jp shove_block_h_d_e
@check_push_right:
; was RIGHT pressed?
bit controller_flags_dirs_right_bit,a
ret z ; no, RIGHT was not pressed, and we already checked LEFT, so we're done
; yes, right was pressed (with fire)
call trigger_sfx_cursor
; get new position (left = dec l, right = inc l)
ld d, l
ld e, l
inc e ; inc = 'right'
call can_I_push_here
ret nz
; all good, we can move there
; copy old tile to new place
jp shove_block_h_d_e
lock_controller:
ld a, 1
ld (controller_flags_disabled), a
ret
unlock_controller:
xor a
ld (controller_flags_disabled), a
ret
@check_adjust_slider_for_moved_tile_in_stack:
; SLIDER - if this tile WAS in the slider stack, then we also need to
; clear the 'is_slider' bit from the old position, and reduce the height of
; the slider stack (to equal the remaining height of all tiles STRICTLY BELOW
; this one), and clear the 'is_slider bit from all the tiles strictly above us
; in the slider stack.
dec h ; point to flags
ld a, (hl)
and IS_SLIDER_MASK
ret z
; clear the slider flag (in fact clear all flags, that old position is now empty)
; clear the flags above us too, and call 'check falling tile' on those
push hl ; let's remember where we started
@next:
xor a
ld (hl), a
ld a, l
sub 16
ld l, a
ld a, (hl)
and IS_SLIDER_MASK
jr nz, @-next
@finished_clearing_flags_for_slider_stack:
; now work out new height of the slider stack
; get y-coord of slider and subtract y-coord of this tile
pop hl
ld a, l
and 240
rrca
rrca
rrca
rrca ; get y-coord only into a
ld b, a
ld a, (slider_xy)
and 240
rrca
rrca
rrca
rrca ; get y-coord only into a
sub b
dec a ; subtract one because we have removed a tile from the stack
ld (slider_stack_height), a
ret
can_I_push_here:
; on entry, D is the grid YX of the source
; E is the grid YX of the destination
;
; return: Z FLAG set if it's ok to move a tile into destination
; Z FLAG clear (i.e. NZ condition) if you cannot
;
; TODO more edge cases here. what happens when the tile being shoved is part
; of the stack on top of a vertical slider? it seems to do the following:
; if the slider is grid-aligned then this just works (look left or right and
; see a space), but if the slider is part-way then the tile will move to the
; nearest grid-aligned y-coordinate after being shoved. So in some cases that
; means the tile y-coordinate will be 'rounded up' and jump up, or 'rounded down'
; and jump down. In either case, the tile might find itself immediately on
; solid ground, or it might start to fall, etc.
; ....
ld h, tiles/256 ; TODO optimize out if that's what it is already (per caller)
ld l, e
ld a, (hl)
and a ; check if it is zero. you can only push a tile into zero
ret nz
; yet another edge case - even if this space is zero now, if there's something
; ABOVE it, that is partially obstructing and falling INTO this space already, we can't move into it now.
ld a, l
sub 16
ld l, a
dec h ; (point to tiles_flags)
ld a, (hl)
; is it falling or slider? if so, it might be already partially obstructing the space, we can't move into that space
bit IS_FALLING_BIT, a
jr z, @+_not_falling
; falling block above the dest square. is it encroaching?
and 15 ; get the y-coordinate-offset of the falling block
ret ; if it's nonzero, then falling block is not grid-aligned (it's poking downwards into
; where we want to move our tile)
; if it's zero, then we're good
@_not_falling:
; block above the dest is not a faller.
; if sliders are vertical, we need to check if it's a slider
; (if sliders are horiz, we need a different check)
ld a, (slider_info)
and a
ret z ; no slider, nothing else to check
; get current type
bit SLIDER_AXIS_BIT, a
jr nz, @_horiz_slider_checks
; vert slider type
; ok so see if the block above the dest is a slider
ld a, (hl)
bit IS_SLIDER_BIT, a
ret z ; not a slider above us, so we're done - this is SUCCESS - you can move here!
; we also have to check the slider y offset. if it's zero, then we're actually
; fine to move into this space, because the slider is at a grid-aligned coordinate and
; there's enough space to sneak in under it.
and 15 ; get the y-coordinate-offset of the slider
ret ; if it's nonzero, then slider is not grid-aligned (it's poking downwards into
; where we want to move our tile)
; if it's zero, then we're good
@_horiz_slider_checks:
; TODO
sub a ; set Z flag
ret
shove_block_h_d_e:
; move the tile info pointed to by YX in D to the new YX in E
;
push de
ld h, tiles/256
ld l, d
ld a, (hl)
ld l, e
ld (hl), a
ld l, d
ld (hl), 0
dec h
; we're now pointing at the 'old' flags.
; This contains the Y-offset if tile was sliding/falling
; so blank out this old location (whatever it was)
; before clearing the flags
; TODO NEED TO HANDLE HORIZ SLIDER HERE i.e. THAT MIGHT NEED THE 18x16 CASE
; TODO JUST NEED A GENERIC "BLANK THIS OLD THING" ROUTINE ACTUALLY, THAT FIGURES OUT WHAT TO DO
; (SHOULDN'T NEED TO BE PART OF CURSOR.Z80)
push hl ; TODO optimize out the push/pop here
ld c, l
ld a, (hl)
ld b, a
call grid_xy_in_C_flags_in_b_to_screen_de
call defer_fillrect_16x16_black_at_de
pop hl
inc h ; point back to tiles
push hl
call @check_adjust_slider_for_moved_tile_in_stack
pop hl
; finally, clear old flags
; (we can't do that before the above call, because it needs to check if the flag was set)
dec h
ld (hl), 0
inc h
; and mark this as our new cursor position
pop de
ld a, e
ld (controller_flags_xy_pos), a
; mark the new location as dirty
call push_dirty_tile
ld a, 1 ; TODO optimize, a is already nonzero
ld (cursor_moved), a
; add a check if the newly-shoved tile is now maybe falling
ld l, e
call queue_check_falling_tile_l
; add a check to see if the one ABOVE us in the OLD location is now falling
ld a, d
sub 16
ld l, a
ld e, l ; need this again in a sec
call queue_check_falling_tile_l ; FIX ME, INEFFICIENT, WE SUB 16 ABOVE AND THIS CALL JUST ADDS IT BACK AGAIN
; mark the old location as dirty
ld a, e ; from backup
call push_dirty_tile
ret