; Raiders of the Lost Aisle -- ZX Spectrum 48K
; An original tribute reusing the dimetric ("isometric") projection
; technique documented for The Great Escape's engine:
;   screen X from the DIFFERENCE of the two ground axes
;   screen Y from their SUM, minus height
; Objects and characters are depth-sorted back-to-front each frame,
; same idea as the original engine's painter's-algorithm compositing.

    ORG 32768

; ---------------------------------------------------------------
; Constants
; ---------------------------------------------------------------
SCR         EQU 16384
ATTR        EQU 22528
ORIGIN_X    EQU 128
ORIGIN_Y    EQU 40

YELLOW_BRIGHT   EQU $70
YELLOW_DARK     EQU $30
WHITE_BRIGHT    EQU $78
WHITE_DARK      EQU $38
GREEN_BRIGHT    EQU $60
GREEN_DARK      EQU $20
MAGENTA_BRIGHT  EQU $58
MAGENTA_DARK    EQU $18
TIN_BRIGHT      EQU $F0
TIN_DARK        EQU $30
CYAN_BRIGHT     EQU $68
CYAN_DARK       EQU $28
RED_BRIGHT      EQU $50
RED_DARK        EQU $10
BLACK_ATTR      EQU $00

; ---------------------------------------------------------------
; Entry point -- title/play/end state machine, one HALT per frame
; ---------------------------------------------------------------
start:
    ; Interrupts are already IM 1 + enabled by the time BASIC hands
    ; off via RANDOMIZE USR, which is what HALT below relies on.
    ld a, 1
    out ($FE), a        ; blue border

    call clear_screen
    xor a
    ld (state), a
    call print_title_screen

main_loop:
    halt
    ld a, (state)
    cp 1
    jr z, frame_play
    or a
    jr z, frame_title
    call frame_end
    jr main_loop

frame_title:
    call key_s
    jr c, main_loop         ; not pressed
    call newgame
    jr main_loop

frame_play:
    ld hl, tick
    inc (hl)

    call render_room
    call read_input

    ld hl, guard_tick
    inc (hl)
    ld a, (guard_tick)
    cp 6
    jr c, skip_guard_move
    xor a
    ld (guard_tick), a
    call move_guard
skip_guard_move:
    call handle_capture

    ld a, (won_flag)
    or a
    jr z, main_loop
    ld a, 2
    ld (state), a
    jr main_loop

frame_end:
    call key_s
    jr c, main_loop
    call newgame
    jr main_loop

; key_s: carry clear if S ($FDFE bit1) is currently pressed
key_s:
    ld a, $FD
    in a, ($FE)
    rra
    rra
    ret

; newgame -- (re)initialise all state and switch into "playing"
newgame:
    call clear_screen
    ld a, 1
    out ($FE), a

    xor a
    ld (cur_room), a
    ld a, 1
    ld (player_x), a
    ld a, 5
    ld (player_y), a
    ld a, 2
    ld (guard_x), a
    ld a, 2
    ld (guard_y), a
    xor a
    ld (guard_wp), a
    ld (guard_mode), a
    ld (has_tin), a
    ld (guard_tick), a
    ld (won_flag), a
    ld (tick), a
    ld a, 60
    ld (invuln_timer), a
    ld a, 1
    ld (state), a

    call print_title
    ret

; ---------------------------------------------------------------
; clear_screen -- black paper/ink, blank pixels
; ---------------------------------------------------------------
clear_screen:
    ld hl, SCR
    ld de, SCR+1
    ld bc, 6144-1
    ld (hl), 0
    ldir
    ld hl, ATTR
    ld de, ATTR+1
    ld bc, 768-1
    ld (hl), 0
    ldir
    ret

; clear_play_area -- blanks pixels+attributes for character rows 2-14
; (covers both rooms' floor/object extents). Called on every room
; transition so the previous room's larger footprint can't leave
; stray pixels behind the new, smaller one.
clear_play_area:
    ld d, 2
cpa_rowloop:
    ld a, d
    cp 15
    jr nc, cpa_attr
    push de
    ld e, 0
    call calc_addr_top
    ld b, 8
cpa_scanloop:
    push bc
    push hl
    ld bc, 32
cpa_bytefill:
    ; write the constant directly rather than through A -- A gets
    ; reused below for the BC==0 test, so writing (hl),a here would
    ; silently start writing the countdown value instead of 0 after
    ; the first byte.
    ld (hl), 0
    inc hl
    dec bc
    ld a, b
    or c
    jr nz, cpa_bytefill
    pop hl
    ld bc, 256
    add hl, bc
    pop bc
    djnz cpa_scanloop
    pop de
    inc d
    jr cpa_rowloop
cpa_attr:
    ld d, 2
cpa_attrloop:
    ld a, d
    cp 15
    jr nc, cpa_attr_done
    ld e, 0
    call attr_addr
    ld b, 32
    xor a
cpa_attrfill:
    ld (hl), a
    inc hl
    djnz cpa_attrfill
    ld a, d
    inc a
    ld d, a
    jr cpa_attrloop
cpa_attr_done:
    ret

; ---------------------------------------------------------------
; read_input -- Q/A move X axis, O/P move Y axis
; ---------------------------------------------------------------
read_input:
    ld a, (tick)
    and 3
    ret nz                  ; move on exactly 1 frame in 4

    ld a, (won_flag)
    or a
    ret nz

    ; Q: row $FBFE bit0  -> gx -= 1
    ld a, $FB
    in a, ($FE)
    rra
    jr c, chk_a
    ld a, -1
    ld c, 0
    call try_move
    jr input_done
chk_a:
    ; A: row $FDFE bit0 -> gx += 1
    ld a, $FD
    in a, ($FE)
    rra
    jr c, chk_o
    ld a, 1
    ld c, 0
    call try_move
    jr input_done
chk_o:
    ; O: row $DFFE bit1 -> gy -= 1
    ld a, $DF
    in a, ($FE)
    rra
    rra
    jr c, chk_p
    ld a, 0
    ld c, -1
    call try_move
    jr input_done
chk_p:
    ; P: row $DFFE bit0 -> gy += 1
    ld a, $DF
    in a, ($FE)
    rra
    jr c, input_done
    ld a, 0
    ld c, 1
    call try_move
input_done:
    ret

; ---------------------------------------------------------------
; try_move -- A=dx, C=dy (each -1/0/1). Moves player if legal,
; handles doors, tin pickup, checkout win.
; ---------------------------------------------------------------
try_move:
    ld b, a          ; b = dx
    ld a, (player_x)
    add a, b
    ld d, a          ; d = nx
    ld a, (player_y)
    add a, c
    ld e, a          ; e = ny

    ld a, (cur_room)
    or a
    jr nz, tm_aisle13

tm_aisle7:
    ; bounds
    ld a, d
    cp 8
    ret nc
    ld a, e
    cp 8
    ret nc
    ; blocked cells
    call blocked_aisle7
    ret c            ; carry set = blocked

    ; door cell (4,7) -> aisle13 entry (2,1)
    ld a, d
    cp 4
    jr nz, tm7_commit
    ld a, e
    cp 7
    jr nz, tm7_commit
    ld a, 1
    ld (cur_room), a
    ld a, 2
    ld (player_x), a
    ld a, 1
    ld (player_y), a
    call clear_play_area
    jr tm_set_cooldown

tm7_commit:
    ld a, d
    ld (player_x), a
    ld a, e
    ld (player_y), a

    ; checkout win: (0,0) with tin
    ld a, d
    or a
    jr nz, tm_set_cooldown
    ld a, e
    or a
    jr nz, tm_set_cooldown
    ld a, (has_tin)
    or a
    jr z, tm_set_cooldown
    ld a, 1
    ld (won_flag), a
    call print_win
    jr tm_set_cooldown

tm_aisle13:
    ld a, d
    cp 6
    ret nc
    ld a, e
    cp 6
    ret nc
    call blocked_aisle13
    ret c

    ; door cell (2,0) -> aisle7 entry (4,6)
    ld a, d
    cp 2
    jr nz, tm13_commit
    ld a, e
    or a
    jr nz, tm13_commit
    xor a
    ld (cur_room), a
    ld a, 4
    ld (player_x), a
    ld a, 6
    ld (player_y), a
    ; the guard may have converged right next to the door while the
    ; player was away in aisle 13 (it moves during the away trip too)
    ; -- a real window here matters, not just a token few frames,
    ; since the player only outruns the guard (4 vs 6 frames/tile)
    ; over sustained distance, not a snap escape from point-blank.
    ld a, 100
    ld (invuln_timer), a
    call clear_play_area
    jr tm_set_cooldown

tm13_commit:
    ld a, d
    ld (player_x), a
    ld a, e
    ld (player_y), a

    ; tin pickup: (2,2)
    ld a, (has_tin)
    or a
    jr nz, tm_set_cooldown
    ld a, d
    cp 2
    jr nz, tm_set_cooldown
    ld a, e
    cp 2
    jr nz, tm_set_cooldown
    ld a, 1
    ld (has_tin), a

tm_set_cooldown:
    ret

; carry set if (d,e) blocked by a shelf/trolley in aisle 7
blocked_aisle7:
    ld a, d
    cp 3
    jr c, ba7_b
    cp 5
    jr nc, ba7_b
    ld a, e
    cp 1
    jr z, ba7_hit
    cp 4
    jr z, ba7_hit
ba7_b:
    ld a, d
    cp 1
    jr nz, ba7_clear
    ld a, e
    cp 6
    jr z, ba7_hit
ba7_clear:
    or a
    ret
ba7_hit:
    scf
    ret

; carry set if (d,e) blocked by a shelf in aisle 13
blocked_aisle13:
    ld a, d
    cp 1
    jr z, ba13_col
    cp 4
    jr z, ba13_col
    or a
    ret
ba13_col:
    ld a, e
    cp 1
    ret c
    cp 4
    ret nc
    scf
    ret

; ---------------------------------------------------------------
; handle_capture -- guard touches player
; ---------------------------------------------------------------
handle_capture:
    ld a, (cur_room)
    or a
    ret nz            ; guard only present in aisle 7

    ld a, (invuln_timer)
    or a
    jr z, hc_active

    ; Still in the grace period. If the guard has camped right on
    ; the player's own tile during it (it can converge here while
    ; the player is protected), don't let the timer lapse into an
    ; ambush the instant it hits zero -- hold at 1 until the guard
    ; actually moves off, then let the countdown (and the "CAUGHT!"
    ; banner) finish normally.
    call same_tile
    jr nz, hc_tick
    ld a, 1
    ld (invuln_timer), a
    ret
hc_tick:
    ; same_tile clobbers A -- reload invuln_timer rather than trust
    ; A still holds it from before the call.
    ld a, (invuln_timer)
    dec a
    ld (invuln_timer), a
    ret nz
    jp clear_message      ; grace period just expired -- clear "CAUGHT!"

hc_active:
    call same_tile
    ret nz

    call print_caught

    ld a, 1
    ld (player_x), a
    ld a, 5
    ld (player_y), a
    ld a, 2
    ld (guard_x), a
    ld a, 2
    ld (guard_y), a
    xor a
    ld (guard_wp), a
    ld (guard_mode), a
    ld (has_tin), a
    ld a, 75
    ld (invuln_timer), a
    ret

; same_tile: Z set if guard_x==player_x and guard_y==player_y
same_tile:
    ld a, (guard_x)
    ld b, a
    ld a, (player_x)
    cp b
    ret nz
    ld a, (guard_y)
    ld b, a
    ld a, (player_y)
    cp b
    ret

; ---------------------------------------------------------------
; move_guard -- patrol waypoints, or chase if player is close
; ---------------------------------------------------------------
GUARD_WP_X:
DEFB 2, 2, 5, 5
GUARD_WP_Y:
DEFB 2, 5, 5, 2

move_guard:
    ld a, (cur_room)
    or a
    ret nz

    ; distance check (Manhattan) to decide patrol/chase
    ld a, (guard_x)
    ld b, a
    ld a, (player_x)
    sub b
    call abs8
    ld c, a           ; c = |dx|
    ld a, (guard_y)
    ld b, a
    ld a, (player_y)
    sub b
    call abs8
    add a, c          ; a = |dx|+|dy|

    ld b, a
    ld a, (guard_mode)
    or a
    jr nz, mg_chasing

    ld a, b
    cp 4
    jr nc, mg_patrol
    ld a, 1
    ld (guard_mode), a
    jr mg_chase

mg_chasing:
    ld a, b
    cp 5
    jr c, mg_chase
    xor a
    ld (guard_mode), a
    jr mg_patrol

mg_chase:
    ld a, (player_x)
    ld b, a
    ld a, (guard_x)
    cp b
    jr z, mg_chase_y
    jr c, mg_gx_inc
    dec a
    ld (guard_x), a
    jr mg_chase_y
mg_gx_inc:
    inc a
    ld (guard_x), a
mg_chase_y:
    ld a, (player_y)
    ld b, a
    ld a, (guard_y)
    cp b
    ret z
    jr c, mg_gy_inc
    dec a
    ld (guard_y), a
    ret
mg_gy_inc:
    inc a
    ld (guard_y), a
    ret

mg_patrol:
    ld a, (guard_wp)
    ld hl, GUARD_WP_X
    ld d, 0
    ld e, a
    add hl, de
    ld b, (hl)         ; target x
    ld hl, GUARD_WP_Y
    add hl, de
    ld c, (hl)         ; target y

    ld a, (guard_x)
    cp b
    jr z, mgp_y
    jr c, mgp_x_inc
    dec a
    ld (guard_x), a
    jr mgp_done
mgp_x_inc:
    inc a
    ld (guard_x), a
    jr mgp_done
mgp_y:
    ld a, (guard_y)
    cp c
    jr z, mgp_reached
    jr c, mgp_y_inc
    dec a
    ld (guard_y), a
    jr mgp_done
mgp_y_inc:
    inc a
    ld (guard_y), a
    jr mgp_done
mgp_reached:
    ld a, (guard_wp)
    inc a
    cp 4
    jr c, mgp_store
    xor a
mgp_store:
    ld (guard_wp), a
mgp_done:
    ret

; abs8: A = |A| (8-bit twos-complement). Uses only C/Z (no sign/parity
; flag test), since our small grid deltas never approach -128.
abs8:
    cp 128
    ret c            ; A < 128 unsigned => already non-negative, done
    cpl
    inc a            ; two's-complement negate: A = -A
    ret

; ---------------------------------------------------------------
; render_room
; ---------------------------------------------------------------
render_room:
    ; floor pad
    ld a, (cur_room)
    or a
    jr nz, rr_floor13
    ld b, 8            ; rows 5..12
    ld c, 9             ; cols 9..23 (15 wide)
    ld hl, floor_row5
    call fill_floor_block
    jr rr_objs
rr_floor13:
    ld b, 6
    ld c, 12
    ld hl, floor_row5b
    call fill_floor_block

rr_objs:
    xor a
    ld (list_count), a

    ld a, (cur_room)
    or a
    jp nz, rr_aisle13

rr_aisle7:
    ld a, 3
    ld (p_gx),a
    ld a,1
    ld (p_gy),a
    ld a,2
    ld (p_gh),a
    ld a, 2
    ld (p_w),a
    ld a,1
    ld (p_d),a
    ld a, YELLOW_BRIGHT
    ld (p_cb),a
    ld a, YELLOW_DARK
    ld (p_cd),a
    call add_entry

    ld a, 3
    ld (p_gx),a
    ld a,4
    ld (p_gy),a
    ld a,2
    ld (p_gh),a
    ld a, 2
    ld (p_w),a
    ld a,1
    ld (p_d),a
    ld a, YELLOW_BRIGHT
    ld (p_cb),a
    ld a, YELLOW_DARK
    ld (p_cd),a
    call add_entry

    ld a, 1
    ld (p_gx),a
    ld a,6
    ld (p_gy),a
    ld a,1
    ld (p_gh),a
    ld a, 1
    ld (p_w),a
    ld a,1
    ld (p_d),a
    ld a, WHITE_BRIGHT
    ld (p_cb),a
    ld a, WHITE_DARK
    ld (p_cd),a
    call add_entry

    ld a, 0
    ld (p_gx),a
    ld a,0
    ld (p_gy),a
    ld a,1
    ld (p_gh),a
    ld a, 2
    ld (p_w),a
    ld a,1
    ld (p_d),a
    ld a, GREEN_BRIGHT
    ld (p_cb),a
    ld a, GREEN_DARK
    ld (p_cd),a
    call add_entry

    ; guard
    ld a, (guard_x)
    ld (p_gx),a
    ld a, (guard_y)
    ld (p_gy),a
    ld a, 1
    ld (p_gh),a
    ld a, 1
    ld (p_w),a
    ld a, 1
    ld (p_d),a
    ld a, RED_BRIGHT
    ld (p_cb),a
    ld a, RED_DARK
    ld (p_cd),a
    call add_entry
    jr rr_player

rr_aisle13:
    ld a, 1
    ld (p_gx),a
    ld a,1
    ld (p_gy),a
    ld a,2
    ld (p_gh),a
    ld a, 1
    ld (p_w),a
    ld a,3
    ld (p_d),a
    ld a, MAGENTA_BRIGHT
    ld (p_cb),a
    ld a, MAGENTA_DARK
    ld (p_cd),a
    call add_entry

    ld a, 4
    ld (p_gx),a
    ld a,1
    ld (p_gy),a
    ld a,2
    ld (p_gh),a
    ld a, 1
    ld (p_w),a
    ld a,3
    ld (p_d),a
    ld a, MAGENTA_BRIGHT
    ld (p_cb),a
    ld a, MAGENTA_DARK
    ld (p_cd),a
    call add_entry

    ld a, (has_tin)
    or a
    jr nz, rr_player
    ld a, 2
    ld (p_gx),a
    ld a,2
    ld (p_gy),a
    ld a,1
    ld (p_gh),a
    ld a, 1
    ld (p_w),a
    ld a,1
    ld (p_d),a
    ld a, TIN_BRIGHT
    ld (p_cb),a
    ld a, TIN_DARK
    ld (p_cd),a
    call add_entry

rr_player:
    ld a, (player_x)
    ld (p_gx),a
    ld a, (player_y)
    ld (p_gy),a
    ld a, 1
    ld (p_gh),a
    ld a, 1
    ld (p_w),a
    ld a, 1
    ld (p_d),a
    ld a, CYAN_BRIGHT
    ld (p_cb),a
    ld a, CYAN_DARK
    ld (p_cd),a
    call add_entry

    call sort_list
    call draw_list
    ret

; fill_floor_block: B=row count, C=start col, HL=start row(as a byte via label trick not used)
; simplified: B=rows, C=cols, D=start row, E=start col -- but we only pass B,C above.
; use fixed start rows via separate labels instead:
floor_row5:
DEFB 5
floor_row5b:
DEFB 5

fill_floor_block:
    ld d, (hl)         ; start row
    ld e, c            ; start col
ffb_row:
    push bc
    push de
    call attr_addr     ; hl = addr for (d,e)
    ld b, 15
ffb_col:
    ld a, (cur_room)
    or a
    jr nz, ffb_col13
    ld (hl), $08        ; paper=blue, ink=black (aisle7 floor)
    jr ffb_col_next
ffb_col13:
    ld (hl), $18        ; paper=magenta, ink=black (eerie aisle13 floor)
ffb_col_next:
    inc hl
    djnz ffb_col
    pop de
    inc d
    pop bc
    dec b
    jr nz, ffb_row
    ret

; ---------------------------------------------------------------
; Object list: 8 bytes/entry: depth,gx,gy,gh,w,d,cb,cd
; ---------------------------------------------------------------
p_gx:
DEFB 0
p_gy:
DEFB 0
p_gh:
DEFB 0
p_w:
DEFB 0
p_d:
DEFB 0
p_cb:
DEFB 0
p_cd:
DEFB 0

list_count:
DEFB 0
obj_list:
DEFS 8*8

add_entry:
    ld a, (list_count)
    cp 8
    ret nc
    ld hl, obj_list
    ld d, 0
    ld e, a
    add hl, hl
    add hl, hl
    add hl, hl         ; *8
    ex de, hl
    ld hl, obj_list
    add hl, de
    ex de, hl          ; de = obj_list, hl = dest... simplify below
    ; hl currently = obj_list + idx*8 is WRONG after swap; recompute cleanly:
    ld a, (list_count)
    ld l, a
    ld h, 0
    add hl, hl
    add hl, hl
    add hl, hl
    ld de, obj_list
    add hl, de          ; hl = obj_list + idx*8

    ld a, (p_gx)
    ld b, a
    ld a, (p_gy)
    add a, b
    ld (hl), a          ; depth = gx+gy
    inc hl
    ld a, (p_gx)
    ld (hl), a
    inc hl
    ld a, (p_gy)
    ld (hl), a
    inc hl
    ld a, (p_gh)
    ld (hl), a
    inc hl
    ld a, (p_w)
    ld (hl), a
    inc hl
    ld a, (p_d)
    ld (hl), a
    inc hl
    ld a, (p_cb)
    ld (hl), a
    inc hl
    ld a, (p_cd)
    ld (hl), a

    ld a, (list_count)
    inc a
    ld (list_count), a
    ret

; insertion sort by depth (ascending), 8-byte records.
; For i = 1..n-1: lift record[i] into a scratch buffer, shift every
; preceding record with a larger depth one slot to the right, then
; drop the scratch buffer into the gap. Uses LDIR block copies
; instead of hand-rolled byte swaps, to keep this unambiguous.
sort_list:
    ld a, (list_count)
    cp 2
    ret c
    ld (sl_n), a
    ld a, 1
    ld (sl_i), a
sl_outer:
    ld a, (sl_i)
    ld hl, sl_n
    cp (hl)
    ret nc              ; i >= n: done

    ld a, (sl_i)
    call rec_addr        ; hl = &record[i]
    ld de, sl_temp
    ld bc, 8
    ldir                  ; sl_temp = record[i]

    ld a, (sl_i)
    ld (sl_j), a

sl_inner:
    ld a, (sl_j)
    or a
    jr z, sl_insert        ; j == 0: nowhere further to shift
    dec a
    call rec_addr            ; hl = &record[j-1]
    ld a, (hl)                 ; depth of record[j-1]
    ld b, a
    ld a, (sl_temp)              ; depth of the lifted record
    cp b
    jr nc, sl_insert               ; temp.depth >= record[j-1].depth: stop

    ld a, (sl_j)
    call rec_addr                    ; hl = &record[j]
    ex de, hl
    ld a, (sl_j)
    dec a
    call rec_addr                      ; hl = &record[j-1]
    ld bc, 8
    ldir                                 ; record[j] = record[j-1]

    ld a, (sl_j)
    dec a
    ld (sl_j), a
    jr sl_inner

sl_insert:
    ld a, (sl_j)
    call rec_addr
    ex de, hl
    ld hl, sl_temp
    ld bc, 8
    ldir                                    ; record[j] = sl_temp

    ld a, (sl_i)
    inc a
    ld (sl_i), a
    jr sl_outer

sl_n:    DEFB 0
sl_i:    DEFB 0
sl_j:    DEFB 0
sl_temp: DEFS 8

; rec_addr: A=index -> HL = obj_list + A*8
rec_addr:
    ld l, a
    ld h, 0
    add hl, hl
    add hl, hl
    add hl, hl
    ld de, obj_list
    add hl, de
    ret

draw_list:
    ld a, (list_count)
    or a
    ret z
    ld b, a
    xor a
    ld (draw_idx), a
dl_loop:
    push bc
    ld a, (draw_idx)
    call rec_addr
    inc hl              ; skip depth byte
    ld a, (hl)
    ld (p_gx), a
    inc hl
    ld a, (hl)
    ld (p_gy), a
    inc hl
    ld a, (hl)
    ld (p_gh), a
    inc hl
    ld a, (hl)
    ld (p_w), a
    inc hl
    ld a, (hl)
    ld (p_d), a
    inc hl
    ld a, (hl)
    ld (p_cb), a
    inc hl
    ld a, (hl)
    ld (p_cd), a
    call draw_box
    ld a, (draw_idx)
    inc a
    ld (draw_idx), a
    pop bc
    djnz dl_loop
    ret
draw_idx:
DEFB 0

; ---------------------------------------------------------------
; draw_box -- draws p_gx/p_gy/p_gh/p_w/p_d as base+top rectangles
; ---------------------------------------------------------------
draw_box:
    ; sx = ORIGIN_X + (gy-gx)*8
    ld a, (p_gy)
    ld b, a
    ld a, (p_gx)
    ld c, a
    ld a, b
    sub c               ; a = gy-gx (signed)
    add a, a
    add a, a
    add a, a            ; *8
    add a, ORIGIN_X
    ld d, a             ; d = sx

    ; sy = ORIGIN_Y + (gx+gy)*4  (ground level -- height is applied
    ; separately below, only for the "top" rectangle)
    ld a, b
    add a, c            ; gy+gx
    add a, a
    add a, a            ; *4
    add a, ORIGIN_Y
    ld e, a             ; e = sy (ground row, unadjusted for height)

    ; row = e>>3, col = d>>3
    ld a, e
    rrca
    rrca
    rrca
    and $1F
    ld (box_row), a
    ld a, d
    rrca
    rrca
    rrca
    and $1F
    ld (box_col), a

    ; base rectangle at (row,col) with p_cd
    ld a, (box_row)
    ld d, a
    ld a, (box_col)
    ld e, a
    ld a, (p_d)
    ld b, a
    ld a, (p_w)
    ld c, a
    ld a, (p_cd)
    call fill_rect

    ; top rectangle at (row-gh, col) with p_cb
    ld a, (box_row)
    ld hl, p_gh
    sub (hl)
    ld d, a
    ld a, (box_col)
    ld e, a
    ld a, (p_d)
    ld b, a
    ld a, (p_w)
    ld c, a
    ld a, (p_cb)
    call fill_rect
    ret

box_row:
DEFB 0
box_col:
DEFB 0

; fill_rect: D=row,E=col,B=rows,C=cols,A=color
fill_rect:
    push af
    ld a, b
    ld (fr_rows), a
    ld a, c
    ld (fr_cols), a
    pop af
    ld (fr_color), a
fr_row_loop:
    push de
    push bc
    call attr_addr
    ld a, (fr_cols)
    ld b, a
    ld a, (fr_color)
fr_col_loop:
    ld (hl), a
    inc hl
    djnz fr_col_loop
    pop bc
    pop de
    inc d
    ld a, (fr_rows)
    dec a
    ld (fr_rows), a
    jr nz, fr_row_loop
    ret
fr_color:
DEFB 0
fr_rows:
DEFB 0
fr_cols:
DEFB 0

; attr_addr: D=row(0-23), E=col(0-31) -> HL = ATTR + row*32+col
attr_addr:
    ld a, d
    add a, a
    add a, a
    add a, a
    add a, a
    add a, a            ; row*32
    add a, e
    ld l, a
    ld a, d
    rrca
    rrca
    rrca
    and $07
    add a, ATTR/256
    ld h, a
    ret

; ---------------------------------------------------------------
; Text printing -- uses the bundled 8x8 font (fontdata.inc, chars
; 32-90) rather than the ROM's character set at $3D00. Direct screen
; writes, independent of BASIC channel state -- and independent of
; there being a real ROM image in memory at all.
; ---------------------------------------------------------------

; print_string: HL=text (0-terminated), B=char row, C=char col
print_string:
    ld a, (hl)
    or a
    ret z
    push hl
    push bc
    call print_char
    pop bc
    pop hl
    inc hl
    inc c
    jr print_string

; print_char: A=char, B=row, C=col
print_char:
    push af
    ld d, b
    ld e, c
    call calc_addr_top
    ex de, hl
    pop af
    sub 32
    ld l, a
    ld h, 0
    add hl, hl
    add hl, hl
    add hl, hl
    ld bc, font
    add hl, bc
    ex de, hl           ; de = font glyph ptr, hl = screen addr
    ld b, 8
pc_loop:
    ld a, (de)
    ld (hl), a
    inc de
    inc h                ; next scanline = +0x100 (same effect, cheaper)
    djnz pc_loop
    ret

    include "fontdata.inc"

; calc_addr_top: D=row(0-23), E=col(0-31) -> HL = screen addr, scanline0
calc_addr_top:
    ld a, d
    and 7
    add a, a
    add a, a
    add a, a
    add a, a
    add a, a            ; (row&7)*32
    add a, e
    ld l, a
    ld a, d
    rrca
    rrca
    rrca
    and 3
    add a, a
    add a, a
    add a, a
    add a, $40
    ld h, a
    ret

; set_row_attr: D=row, A=color -- fills all 32 columns of that
; character row with the given attribute (text is otherwise
; invisible: default attribute is black ink on black paper)
set_row_attr:
    push af
    ld e, 0
    call attr_addr
    pop af
    ld b, 32
sra_loop:
    ld (hl), a
    inc hl
    djnz sra_loop
    ret

print_title_screen:
    ld d, 8
    ld a, $46
    call set_row_attr
    ld hl, msg_title
    ld b, 8
    ld c, 6
    call print_string

    ld d, 11
    ld a, $47
    call set_row_attr
    ld hl, msg_sub
    ld b, 11
    ld c, 3
    call print_string

    ld d, 14
    ld a, $45
    call set_row_attr
    ld hl, msg_controls
    ld b, 14
    ld c, 3
    call print_string

    ld d, 17
    ld a, $46
    call set_row_attr
    ld hl, msg_pressS
    ld b, 17
    ld c, 8
    call print_string
    ret

print_title:
    ld d, 0
    ld a, $47
    call set_row_attr
    ld hl, msg_title
    ld b, 0
    ld c, 6
    call print_string
    ret

print_caught:
    ld d, 22
    ld a, $42
    call set_row_attr
    ld hl, msg_caught
    ld b, 22
    ld c, 3
    call print_string
    ret

print_win:
    ld d, 22
    ld a, $46
    call set_row_attr
    ld hl, msg_win
    ld b, 22
    ld c, 2
    call print_string
    ret

clear_message:
    ld d, 22
    xor a
    call set_row_attr
    ld hl, msg_blank
    ld b, 22
    ld c, 0
    call print_string
    ret

msg_title:
DEFB "RAIDERS OF THE LOST AISLE", 0
msg_sub:
DEFB "A BARGAIN-HUNT ADVENTURE", 0
msg_controls:
DEFB "Q A O P MOVE, DODGE THE GUARD", 0
msg_pressS:
DEFB "PRESS S TO START", 0
msg_caught:
DEFB "CAUGHT! BACK TO START", 0
msg_win:
DEFB "PAID IN FULL - YOU WIN!", 0
msg_blank:
DEFB "                                        ", 0

; ---------------------------------------------------------------
; Game state
; ---------------------------------------------------------------
cur_room:
DEFB 0
player_x:
DEFB 0
player_y:
DEFB 0
guard_x:
DEFB 0
guard_y:
DEFB 0
guard_wp:
DEFB 0
guard_mode:
DEFB 0
has_tin:
DEFB 0
guard_tick:
DEFB 0
won_flag:
DEFB 0
invuln_timer:
DEFB 0
state:
DEFB 0
tick:
DEFB 0

    END start
