Light and Shadow
The keep is flatly lit — every cell the same cold blue. Hang a torch and let the dithering you met in Unit 2 become lighting: pick each floor cell's shade by its distance from the flame, so a pool of light fades into the dark.
The keep is built, but it's lit like an office — every cell the same even blue, no dark corners, no sense of nightfall. Atmosphere is the whole point of Shadowkeep, and atmosphere starts with light. This unit hangs a torch in each room and lets it throw a pool of light into the gloom — and the tool that does it is one you already have.
Back in Unit 2 you learned that dither density is shade: sparse pixels read light, dense read dark. Lighting is choosing that density by distance from a flame. Close to the torch, sparse and bright; far away, dense and black. Nothing new but the choosing.
What you'll see by the end
A torch burns in the top wall, and the floor beneath it is lit — nearly all blue, the stone catching the flame. Step away and the dither thickens: half-and-half slate, then darker, then the far corners almost black. The thief stands down in the shadow, the lit pool above him. Same keep as last unit; an entirely different place to be in.
A torch is a glyph
A torch is just another tile in the palette — T, a yellow flame in its sconce — placed in the room map like any wall:
defb "###############T################" ; the Hall's top wall
It's drawn bright, so wall_at already treats it as solid stone — a fixture on the wall, not something you walk through. When a room is drawn, we first find its torch:
find_torch:
; ... scan the room map for 'T', record (torch_col, torch_row) ...
; ... or NO_TORCH if the room has none ...
Distance becomes shade
For each floor cell, how lit it is depends on how far it sits from that flame. We measure the distance — the larger of the row gap and the column gap, which gives a neat square-ish pool — halve it, and clamp it to give a shade from 0 (right by the flame) to 4 (deep dark):
shade_for_cell: ; row in B, column in C -> shade 0..4 in A
; ... |row - torch_row| and |col - torch_col| ...
; ... A = the larger of the two (the distance) ...
srl a ; distance / 2 — a gentle falloff
cp MAX_SHADE + 1
jr c, .sf_done
ld a, MAX_SHADE ; clamp to the darkest
.sf_done:
ret
And the five shades are five floor tiles, the same blue and black at thickening densities — shade0 nearly all blue, shade2 the half-and-half slate from Unit 2, shade4 nearly all black:
shade_tiles:
defw shade0_tile ; lit
defw shade1_tile
defw shade2_tile ; the old floor
defw shade3_tile
defw shade4_tile ; deep shadow
Lighting as the room draws
draw_room now does the choosing as it paints. A floor cell is shaded by distance; a wall, torch or chalk mark is drawn flat as before:
.room_col:
ld a, (hl) ; the glyph here
cp '.'
jr nz, .not_floor
call shade_for_cell ; floor: pick a shade by distance
; ... point tile_ptr at shade_tiles[shade] ...
call draw_tile
jr .cell_done
.not_floor:
call lookup_tile ; wall / torch / chalk: flat
call draw_tile
The light is baked — worked out once, when the room is drawn. The torch doesn't move, so the pool doesn't either; it's the still light a sconce casts. (A flickering, carried flame comes later.) And it costs nothing as the thief walks: his save-and-restore already preserves whatever's beneath him, lit shade and all, so he moves through the gradient and leaves it untouched.
Milestone — light the keep
find_torch scans each room's map for a T; shade_for_cell turns a floor cell's
distance from that flame into a shade 0–4; and draw_room now picks a floor tile
from a five-step density ramp as it paints, walls and torches still drawn flat. The
light is baked once, as the room draws — and the thief's save/restore carries each
shade beneath him for free.
| 1 | 1 | ; Shadowkeep — Unit 9: Light and Shadow | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 = Unit 8's end: three rooms, flatly lit. | |
| 3 | + | ; step-01 hangs a torch in each room and shades the floor by distance from the flame. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| 7 | 7 | WALL_ATTR equ %01001000 | |
| 8 | - | FLOOR_ATTR equ %00001000 | |
| 8 | + | FLOOR_ATTR equ %00001000 ; the shade ramp all share this (PAPER 1, INK 0); only the pattern differs | |
| 9 | + | TORCH_ATTR equ %01001110 ; BRIGHT, PAPER 1 (blue), INK 6 (yellow) — a lit sconce (solid) | |
| 9 | 10 | MARK_ATTR equ %00001111 | |
| 10 | 11 | THIEF equ %01001010 | |
| 11 | 12 | WALL_BIT equ 6 | |
| ... | |||
| 13 | 14 | START_COL equ 15 | |
| 14 | 15 | START_ROW equ 11 | |
| 15 | 16 | NO_EXIT equ $FF | |
| 17 | + | NO_TORCH equ $FF | |
| 18 | + | MAX_SHADE equ 4 | |
| 16 | 19 | | |
| 17 | 20 | KEYS_OP equ $DFFE | |
| 18 | 21 | KEYS_Q equ $FBFE | |
| ... | |||
| 104 | 107 | add hl, hl | |
| 105 | 108 | ld de, rooms | |
| 106 | 109 | add hl, de | |
| 110 | + | ret | |
| 111 | + | | |
| 112 | + | ; ---------------------------------------------------------------------------- | |
| 113 | + | ; find_torch — scan the current room's map for 'T', remember where it is (or | |
| 114 | + | ; NO_TORCH if the room is unlit). | |
| 115 | + | ; ---------------------------------------------------------------------------- | |
| 116 | + | find_torch: | |
| 117 | + | ld a, NO_TORCH | |
| 118 | + | ld (torch_col), a | |
| 119 | + | ld (torch_row), a | |
| 120 | + | call room_entry_addr | |
| 121 | + | ld a, (hl) | |
| 122 | + | inc hl | |
| 123 | + | ld h, (hl) | |
| 124 | + | ld l, a | |
| 125 | + | ld b, 0 | |
| 126 | + | .ft_row: | |
| 127 | + | ld c, 0 | |
| 128 | + | .ft_col: | |
| 129 | + | ld a, (hl) | |
| 130 | + | cp 'T' | |
| 131 | + | jr nz, .ft_skip | |
| 132 | + | ld a, c | |
| 133 | + | ld (torch_col), a | |
| 134 | + | ld a, b | |
| 135 | + | ld (torch_row), a | |
| 136 | + | .ft_skip: | |
| 137 | + | inc hl | |
| 138 | + | inc c | |
| 139 | + | ld a, c | |
| 140 | + | cp 32 | |
| 141 | + | jr nz, .ft_col | |
| 142 | + | inc b | |
| 143 | + | ld a, b | |
| 144 | + | cp 24 | |
| 145 | + | jr nz, .ft_row | |
| 146 | + | ret | |
| 147 | + | | |
| 148 | + | ; ---------------------------------------------------------------------------- | |
| 149 | + | ; shade_for_cell — row in B, column in C. Returns a shade 0..4 in A: the | |
| 150 | + | ; Chebyshev distance to the torch, halved and clamped. Near the flame = 0 | |
| 151 | + | ; (lightest); far away = 4 (darkest). No torch = darkest everywhere. | |
| 152 | + | ; ---------------------------------------------------------------------------- | |
| 153 | + | shade_for_cell: | |
| 154 | + | ld a, (torch_col) | |
| 155 | + | cp NO_TORCH | |
| 156 | + | jr z, .sf_dark | |
| 157 | + | ld a, b | |
| 158 | + | ld hl, torch_row | |
| 159 | + | sub (hl) | |
| 160 | + | jr nc, .sf_rpos | |
| 161 | + | neg | |
| 162 | + | .sf_rpos: | |
| 163 | + | ld d, a ; |row - torch_row| | |
| 164 | + | ld a, c | |
| 165 | + | ld hl, torch_col | |
| 166 | + | sub (hl) | |
| 167 | + | jr nc, .sf_cpos | |
| 168 | + | neg | |
| 169 | + | .sf_cpos: | |
| 170 | + | cp d ; A = |dc|; max(|dc|, |dr|) | |
| 171 | + | jr nc, .sf_max | |
| 172 | + | ld a, d | |
| 173 | + | .sf_max: | |
| 174 | + | srl a ; distance / 2 | |
| 175 | + | cp MAX_SHADE + 1 | |
| 176 | + | jr c, .sf_done | |
| 177 | + | ld a, MAX_SHADE | |
| 178 | + | .sf_done: | |
| 179 | + | ret | |
| 180 | + | .sf_dark: | |
| 181 | + | ld a, MAX_SHADE | |
| 107 | 182 | ret | |
| 108 | 183 | | |
| 184 | + | ; ---------------------------------------------------------------------------- | |
| 185 | + | ; draw_room — now lights as it draws. A floor cell ('.') is shaded by distance | |
| 186 | + | ; to the torch; everything else (wall, torch, chalk) is drawn flat. | |
| 187 | + | ; ---------------------------------------------------------------------------- | |
| 109 | 188 | draw_room: | |
| 189 | + | call find_torch | |
| 110 | 190 | call room_entry_addr | |
| 111 | 191 | ld a, (hl) | |
| 112 | 192 | inc hl | |
| ... | |||
| 119 | 199 | .room_col: | |
| 120 | 200 | ld hl, (map_ptr) | |
| 121 | 201 | ld a, (hl) | |
| 202 | + | cp '.' | |
| 203 | + | jr nz, .not_floor | |
| 204 | + | | |
| 205 | + | call shade_for_cell ; A = shade 0..4 | |
| 206 | + | add a, a ; index the pointer table | |
| 207 | + | ld e, a | |
| 208 | + | ld d, 0 | |
| 209 | + | ld hl, shade_tiles | |
| 210 | + | add hl, de | |
| 211 | + | ld e, (hl) | |
| 212 | + | inc hl | |
| 213 | + | ld d, (hl) | |
| 214 | + | ld (tile_ptr), de | |
| 215 | + | ld a, FLOOR_ATTR | |
| 216 | + | ld (tile_attr), a | |
| 217 | + | call draw_tile | |
| 218 | + | jr .cell_done | |
| 219 | + | .not_floor: | |
| 122 | 220 | call lookup_tile | |
| 123 | 221 | call draw_tile | |
| 222 | + | .cell_done: | |
| 124 | 223 | ld hl, (map_ptr) | |
| 125 | 224 | inc hl | |
| 126 | 225 | ld (map_ptr), hl | |
| ... | |||
| 381 | 480 | add hl, de | |
| 382 | 481 | ret | |
| 383 | 482 | | |
| 483 | + | ; ---------------------------------------------------------------------------- | |
| 484 | + | ; Palette — '.' is handled by the lighting path, so its entry here is only a | |
| 485 | + | ; fallback; '#', 'T' and '+' are looked up as normal. | |
| 486 | + | ; ---------------------------------------------------------------------------- | |
| 384 | 487 | palette: | |
| 385 | 488 | defb '.' | |
| 386 | - | defw floor_tile | |
| 489 | + | defw shade2_tile | |
| 387 | 490 | defb FLOOR_ATTR | |
| 388 | 491 | defb '#' | |
| 389 | 492 | defw wall_tile | |
| 390 | 493 | defb WALL_ATTR | |
| 494 | + | defb 'T' | |
| 495 | + | defw torch_tile | |
| 496 | + | defb TORCH_ATTR | |
| 391 | 497 | defb '+' | |
| 392 | 498 | defw mark_tile | |
| 393 | 499 | defb MARK_ATTR | |
| 394 | 500 | | |
| 395 | - | ; ---------------------------------------------------------------------------- | |
| 396 | - | ; The keep: three rooms. Hall -east-> Gallery -north-> Vault, and back. | |
| 397 | - | ; entry: map ptr, North, South, East, West | |
| 398 | - | ; ---------------------------------------------------------------------------- | |
| 501 | + | ; The shade ramp: lightest (lit) to darkest (deep shadow), all blue/black dither. | |
| 502 | + | shade_tiles: | |
| 503 | + | defw shade0_tile | |
| 504 | + | defw shade1_tile | |
| 505 | + | defw shade2_tile | |
| 506 | + | defw shade3_tile | |
| 507 | + | defw shade4_tile | |
| 508 | + | | |
| 399 | 509 | rooms: | |
| 400 | 510 | defw room0_state | |
| 401 | - | defb NO_EXIT, NO_EXIT, 1, NO_EXIT ; Hall: east -> Gallery | |
| 511 | + | defb NO_EXIT, NO_EXIT, 1, NO_EXIT | |
| 402 | 512 | defw room1_state | |
| 403 | - | defb 2, NO_EXIT, NO_EXIT, 0 ; Gallery: north -> Vault, west -> Hall | |
| 513 | + | defb 2, NO_EXIT, NO_EXIT, 0 | |
| 404 | 514 | defw room2_state | |
| 405 | - | defb NO_EXIT, 1, NO_EXIT, NO_EXIT ; Vault: south -> Gallery | |
| 515 | + | defb NO_EXIT, 1, NO_EXIT, NO_EXIT | |
| 406 | 516 | | |
| 407 | - | ; The Great Hall — four pillars, east door at row 11. | |
| 517 | + | ; The Great Hall — a torch ('T') set in the top wall, column 15. | |
| 408 | 518 | room0_template: | |
| 409 | - | defb "################################" | |
| 519 | + | defb "###############T################" | |
| 410 | 520 | defb "#..............................#" | |
| 411 | 521 | defb "#..............................#" | |
| 412 | 522 | defb "#..............................#" | |
| ... | |||
| 431 | 541 | defb "#..............................#" | |
| 432 | 542 | defb "################################" | |
| 433 | 543 | | |
| 434 | - | ; The Gallery — a dividing wall with one gap (column 15). West door (row 11) | |
| 435 | - | ; back to the Hall; north door (column 15) up to the Vault. | |
| 544 | + | ; The Gallery — a torch low on the south wall. | |
| 436 | 545 | room1_template: | |
| 437 | 546 | defb "###############.################" | |
| 438 | 547 | defb "#..............................#" | |
| ... | |||
| 457 | 566 | defb "#..............................#" | |
| 458 | 567 | defb "#..............................#" | |
| 459 | 568 | defb "#..............................#" | |
| 460 | - | defb "################################" | |
| 569 | + | defb "###############T################" | |
| 461 | 570 | | |
| 462 | - | ; The Vault — a great altar of stone in the middle, south door (column 15) | |
| 463 | - | ; down to the Gallery. | |
| 571 | + | ; The Vault — a torch in the top wall above the altar. | |
| 464 | 572 | room2_template: | |
| 465 | - | defb "################################" | |
| 573 | + | defb "###############T################" | |
| 466 | 574 | defb "#..............................#" | |
| 467 | 575 | defb "#..............................#" | |
| 468 | 576 | defb "#..............................#" | |
| ... | |||
| 487 | 595 | defb "#..............................#" | |
| 488 | 596 | defb "###############.################" | |
| 489 | 597 | | |
| 490 | - | floor_tile: | |
| 598 | + | ; ---------------------------------------------------------------------------- | |
| 599 | + | ; The five shades of floor — same blue/black, denser and darker each step. | |
| 600 | + | ; ---------------------------------------------------------------------------- | |
| 601 | + | shade0_tile: ; lit — nearly all blue | |
| 602 | + | defb %00000000 | |
| 603 | + | defb %00100010 | |
| 604 | + | defb %00000000 | |
| 605 | + | defb %00000000 | |
| 606 | + | defb %00000000 | |
| 607 | + | defb %10001000 | |
| 608 | + | defb %00000000 | |
| 609 | + | defb %00000000 | |
| 610 | + | shade1_tile: | |
| 611 | + | defb %00100010 | |
| 612 | + | defb %00000000 | |
| 613 | + | defb %10001000 | |
| 614 | + | defb %00000000 | |
| 615 | + | defb %00100010 | |
| 616 | + | defb %00000000 | |
| 617 | + | defb %10001000 | |
| 618 | + | defb %00000000 | |
| 619 | + | shade2_tile: ; the half-and-half slate from Unit 2 | |
| 620 | + | defb %10101010 | |
| 621 | + | defb %01010101 | |
| 622 | + | defb %10101010 | |
| 623 | + | defb %01010101 | |
| 491 | 624 | defb %10101010 | |
| 492 | 625 | defb %01010101 | |
| 493 | 626 | defb %10101010 | |
| 494 | 627 | defb %01010101 | |
| 628 | + | shade3_tile: | |
| 495 | 629 | defb %10101010 | |
| 630 | + | defb %11111111 | |
| 496 | 631 | defb %01010101 | |
| 632 | + | defb %11111111 | |
| 497 | 633 | defb %10101010 | |
| 634 | + | defb %11111111 | |
| 498 | 635 | defb %01010101 | |
| 636 | + | defb %11111111 | |
| 637 | + | shade4_tile: ; deep shadow — nearly all black | |
| 638 | + | defb %11111111 | |
| 639 | + | defb %11101110 | |
| 640 | + | defb %11111111 | |
| 641 | + | defb %10111011 | |
| 642 | + | defb %11111111 | |
| 643 | + | defb %11101110 | |
| 644 | + | defb %11111111 | |
| 645 | + | defb %10111011 | |
| 499 | 646 | | |
| 500 | 647 | wall_tile: | |
| 501 | 648 | defb %00010001 | |
| ... | |||
| 506 | 653 | defb %00000000 | |
| 507 | 654 | defb %01000100 | |
| 508 | 655 | defb %00000000 | |
| 656 | + | | |
| 657 | + | torch_tile: ; a flame in its sconce | |
| 658 | + | defb %00010000 | |
| 659 | + | defb %00111000 | |
| 660 | + | defb %00111000 | |
| 661 | + | defb %01111100 | |
| 662 | + | defb %01111100 | |
| 663 | + | defb %01111100 | |
| 664 | + | defb %00111000 | |
| 665 | + | defb %00010000 | |
| 509 | 666 | | |
| 510 | 667 | mark_tile: | |
| 511 | 668 | defb %00000000 | |
| ... | |||
| 529 | 686 | | |
| 530 | 687 | current_room: | |
| 531 | 688 | defb 0 | |
| 689 | + | torch_col: | |
| 690 | + | defb NO_TORCH | |
| 691 | + | torch_row: | |
| 692 | + | defb NO_TORCH | |
| 532 | 693 | thief_col: | |
| 533 | 694 | defb START_COL | |
| 534 | 695 | thief_row: |
The complete program
; Shadowkeep — Unit 9: Light and Shadow
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 hangs a torch in each room and shades the floor by distance from the flame.
org 32768
WALL_ATTR equ %01001000
FLOOR_ATTR equ %00001000 ; the shade ramp all share this (PAPER 1, INK 0); only the pattern differs
TORCH_ATTR equ %01001110 ; BRIGHT, PAPER 1 (blue), INK 6 (yellow) — a lit sconce (solid)
MARK_ATTR equ %00001111
THIEF equ %01001010
WALL_BIT equ 6
START_COL equ 15
START_ROW equ 11
NO_EXIT equ $FF
NO_TORCH equ $FF
MAX_SHADE equ 4
KEYS_OP equ $DFFE
KEYS_Q equ $FBFE
KEYS_A equ $FDFE
KEYS_SPACE equ $7FFE
start:
ld a, 0
out ($FE), a
ld hl, room0_template
ld de, room0_state
ld bc, 768
ldir
ld hl, room1_template
ld de, room1_state
ld bc, 768
ldir
ld hl, room2_template
ld de, room2_state
ld bc, 768
ldir
xor a
ld (current_room), a
ld a, START_COL
ld (thief_col), a
ld a, START_ROW
ld (thief_row), a
call draw_room
call save_under
call draw_thief
im 1
ei
.loop:
halt
call player_step
call mark_step
jr .loop
mark_step:
ld bc, KEYS_SPACE
in a, (c)
bit 0, a
ret nz
call cell_state_addr
ld (hl), '+'
ld hl, mark_tile
ld de, under_thief
ld bc, 8
ldir
ld a, MARK_ATTR
ld (under_thief + 8), a
ret
cell_state_addr:
call room_entry_addr
ld a, (hl)
inc hl
ld h, (hl)
ld l, a
push hl
ld a, (thief_row)
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
add hl, hl
add hl, hl
ld a, (thief_col)
ld e, a
ld d, 0
add hl, de
pop de
add hl, de
ret
room_entry_addr:
ld a, (current_room)
ld l, a
ld h, 0
ld d, h
ld e, l
add hl, hl
add hl, de
add hl, hl
ld de, rooms
add hl, de
ret
; ----------------------------------------------------------------------------
; find_torch — scan the current room's map for 'T', remember where it is (or
; NO_TORCH if the room is unlit).
; ----------------------------------------------------------------------------
find_torch:
ld a, NO_TORCH
ld (torch_col), a
ld (torch_row), a
call room_entry_addr
ld a, (hl)
inc hl
ld h, (hl)
ld l, a
ld b, 0
.ft_row:
ld c, 0
.ft_col:
ld a, (hl)
cp 'T'
jr nz, .ft_skip
ld a, c
ld (torch_col), a
ld a, b
ld (torch_row), a
.ft_skip:
inc hl
inc c
ld a, c
cp 32
jr nz, .ft_col
inc b
ld a, b
cp 24
jr nz, .ft_row
ret
; ----------------------------------------------------------------------------
; shade_for_cell — row in B, column in C. Returns a shade 0..4 in A: the
; Chebyshev distance to the torch, halved and clamped. Near the flame = 0
; (lightest); far away = 4 (darkest). No torch = darkest everywhere.
; ----------------------------------------------------------------------------
shade_for_cell:
ld a, (torch_col)
cp NO_TORCH
jr z, .sf_dark
ld a, b
ld hl, torch_row
sub (hl)
jr nc, .sf_rpos
neg
.sf_rpos:
ld d, a ; |row - torch_row|
ld a, c
ld hl, torch_col
sub (hl)
jr nc, .sf_cpos
neg
.sf_cpos:
cp d ; A = |dc|; max(|dc|, |dr|)
jr nc, .sf_max
ld a, d
.sf_max:
srl a ; distance / 2
cp MAX_SHADE + 1
jr c, .sf_done
ld a, MAX_SHADE
.sf_done:
ret
.sf_dark:
ld a, MAX_SHADE
ret
; ----------------------------------------------------------------------------
; draw_room — now lights as it draws. A floor cell ('.') is shaded by distance
; to the torch; everything else (wall, torch, chalk) is drawn flat.
; ----------------------------------------------------------------------------
draw_room:
call find_torch
call room_entry_addr
ld a, (hl)
inc hl
ld h, (hl)
ld l, a
ld (map_ptr), hl
ld b, 0
.room_row:
ld c, 0
.room_col:
ld hl, (map_ptr)
ld a, (hl)
cp '.'
jr nz, .not_floor
call shade_for_cell ; A = shade 0..4
add a, a ; index the pointer table
ld e, a
ld d, 0
ld hl, shade_tiles
add hl, de
ld e, (hl)
inc hl
ld d, (hl)
ld (tile_ptr), de
ld a, FLOOR_ATTR
ld (tile_attr), a
call draw_tile
jr .cell_done
.not_floor:
call lookup_tile
call draw_tile
.cell_done:
ld hl, (map_ptr)
inc hl
ld (map_ptr), hl
inc c
ld a, c
cp 32
jr nz, .room_col
inc b
ld a, b
cp 24
jr nz, .room_row
ret
lookup_tile:
ld hl, palette
.scan:
cp (hl)
jr z, .found
inc hl
inc hl
inc hl
inc hl
jr .scan
.found:
inc hl
ld e, (hl)
inc hl
ld d, (hl)
ld (tile_ptr), de
inc hl
ld a, (hl)
ld (tile_attr), a
ret
draw_tile:
push bc
call attr_addr_cr
ld a, (tile_attr)
ld (hl), a
call scr_addr_cr
ld de, (tile_ptr)
ld b, 8
.tile_row:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .tile_row
pop bc
ret
player_step:
ld a, (thief_col)
ld (tcol), a
ld a, (thief_row)
ld (trow), a
ld bc, KEYS_OP
in a, (c)
bit 1, a
jr z, .left
bit 0, a
jr z, .right
ld bc, KEYS_Q
in a, (c)
bit 0, a
jr z, .up
ld bc, KEYS_A
in a, (c)
bit 0, a
jr z, .down
ret
.left:
ld hl, tcol
dec (hl)
jr .move
.right:
ld hl, tcol
inc (hl)
jr .move
.up:
ld hl, trow
dec (hl)
jr .move
.down:
ld hl, trow
inc (hl)
.move:
ld a, (trow)
ld b, a
ld a, (tcol)
ld c, a
call wall_at
ret nz
call restore_under
ld a, (tcol)
ld (thief_col), a
ld a, (trow)
ld (thief_row), a
call save_under
call draw_thief
call check_exit
ret
wall_at:
call attr_addr_cr
bit WALL_BIT, (hl)
ret
check_exit:
ld a, (thief_col)
or a
jr z, .west
cp 31
jr z, .east
ld a, (thief_row)
or a
jr z, .north
cp 23
jr z, .south
ret
.east:
call room_entry_addr
ld de, 4
add hl, de
ld a, (hl)
cp NO_EXIT
ret z
ld (current_room), a
ld a, 1
ld (thief_col), a
jr .enter
.west:
call room_entry_addr
ld de, 5
add hl, de
ld a, (hl)
cp NO_EXIT
ret z
ld (current_room), a
ld a, 30
ld (thief_col), a
jr .enter
.north:
call room_entry_addr
inc hl
inc hl
ld a, (hl)
cp NO_EXIT
ret z
ld (current_room), a
ld a, 22
ld (thief_row), a
jr .enter
.south:
call room_entry_addr
inc hl
inc hl
inc hl
ld a, (hl)
cp NO_EXIT
ret z
ld (current_room), a
ld a, 1
ld (thief_row), a
.enter:
call draw_room
call save_under
call draw_thief
ret
pos_bc:
ld a, (thief_row)
ld b, a
ld a, (thief_col)
ld c, a
ret
save_under:
call pos_bc
call scr_addr_cr
ld de, under_thief
ld b, 8
.save_row:
ld a, (hl)
ld (de), a
inc de
inc h
djnz .save_row
call pos_bc
call attr_addr_cr
ld a, (hl)
ld (under_thief + 8), a
ret
restore_under:
call pos_bc
call scr_addr_cr
ld de, under_thief
ld b, 8
.restore_row:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .restore_row
call pos_bc
call attr_addr_cr
ld a, (under_thief + 8)
ld (hl), a
ret
draw_thief:
call pos_bc
call attr_addr_cr
ld (hl), THIEF
call pos_bc
call scr_addr_cr
ld de, thief
ld b, 8
.thief_row:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .thief_row
ret
scr_addr_cr:
ld a, b
and %00011000
or %01000000
ld h, a
ld a, b
and %00000111
rrca
rrca
rrca
or c
ld l, a
ret
attr_addr_cr:
ld a, b
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
add hl, hl
add hl, hl
ld de, $5800
add hl, de
ld a, c
ld e, a
ld d, 0
add hl, de
ret
; ----------------------------------------------------------------------------
; Palette — '.' is handled by the lighting path, so its entry here is only a
; fallback; '#', 'T' and '+' are looked up as normal.
; ----------------------------------------------------------------------------
palette:
defb '.'
defw shade2_tile
defb FLOOR_ATTR
defb '#'
defw wall_tile
defb WALL_ATTR
defb 'T'
defw torch_tile
defb TORCH_ATTR
defb '+'
defw mark_tile
defb MARK_ATTR
; The shade ramp: lightest (lit) to darkest (deep shadow), all blue/black dither.
shade_tiles:
defw shade0_tile
defw shade1_tile
defw shade2_tile
defw shade3_tile
defw shade4_tile
rooms:
defw room0_state
defb NO_EXIT, NO_EXIT, 1, NO_EXIT
defw room1_state
defb 2, NO_EXIT, NO_EXIT, 0
defw room2_state
defb NO_EXIT, 1, NO_EXIT, NO_EXIT
; The Great Hall — a torch ('T') set in the top wall, column 15.
room0_template:
defb "###############T################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#........##..........##........#"
defb "#........##..........##........#"
defb "#..............................#"
defb "#..............................."
defb "#..............................#"
defb "#..............................#"
defb "#........##..........##........#"
defb "#........##..........##........#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "################################"
; The Gallery — a torch low on the south wall.
room1_template:
defb "###############.################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "###############.################"
defb "#..............................#"
defb "#..............................#"
defb "...............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "###############T################"
; The Vault — a torch in the top wall above the altar.
room2_template:
defb "###############T################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#.............####.............#"
defb "#.............####.............#"
defb "#.............####.............#"
defb "#.............####.............#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "###############.################"
; ----------------------------------------------------------------------------
; The five shades of floor — same blue/black, denser and darker each step.
; ----------------------------------------------------------------------------
shade0_tile: ; lit — nearly all blue
defb %00000000
defb %00100010
defb %00000000
defb %00000000
defb %00000000
defb %10001000
defb %00000000
defb %00000000
shade1_tile:
defb %00100010
defb %00000000
defb %10001000
defb %00000000
defb %00100010
defb %00000000
defb %10001000
defb %00000000
shade2_tile: ; the half-and-half slate from Unit 2
defb %10101010
defb %01010101
defb %10101010
defb %01010101
defb %10101010
defb %01010101
defb %10101010
defb %01010101
shade3_tile:
defb %10101010
defb %11111111
defb %01010101
defb %11111111
defb %10101010
defb %11111111
defb %01010101
defb %11111111
shade4_tile: ; deep shadow — nearly all black
defb %11111111
defb %11101110
defb %11111111
defb %10111011
defb %11111111
defb %11101110
defb %11111111
defb %10111011
wall_tile:
defb %00010001
defb %00000000
defb %01000100
defb %00000000
defb %00010001
defb %00000000
defb %01000100
defb %00000000
torch_tile: ; a flame in its sconce
defb %00010000
defb %00111000
defb %00111000
defb %01111100
defb %01111100
defb %01111100
defb %00111000
defb %00010000
mark_tile:
defb %00000000
defb %00011000
defb %00011000
defb %01111110
defb %01111110
defb %00011000
defb %00011000
defb %00000000
thief:
defb %00011000
defb %00111100
defb %01111110
defb %01111110
defb %01111110
defb %01111110
defb %00111100
defb %00100100
current_room:
defb 0
torch_col:
defb NO_TORCH
torch_row:
defb NO_TORCH
thief_col:
defb START_COL
thief_row:
defb START_ROW
tcol:
defb 0
trow:
defb 0
map_ptr:
defw 0
tile_ptr:
defw 0
tile_attr:
defb 0
under_thief:
defb 0, 0, 0, 0, 0, 0, 0, 0, 0
room0_state:
defs 768
room1_state:
defs 768
room2_state:
defs 768
end start
Stand in the lit pool under the Hall's torch, then walk straight down into the dark — the floor thickens to shadow around you, and the gradient closes behind you untouched:
Try this: move the flame
Shift the T in a room map to a side wall, or into a corner. The pool moves with it — the lit area is wherever the torch is, computed fresh. Light is data now: you place a flame by typing a letter, and the room lights itself around it.
Try this: a brighter, wider torch
The falloff is srl a — distance halved. Change it: srl a twice (distance / 4) makes a huge soft pool that barely reaches dark; remove the srl entirely (distance itself) and the light collapses to a tight, harsh ring. One shift instruction is the difference between a candle and a bonfire.
Try this: light the walls too
Only floor is shaded here; the walls stay uniformly lit. Extend the idea: give # a shade ramp of its own and shade walls by distance to the torch as well. The far walls sink into the dark with the floor, and the pool becomes a true bubble of light in a black room — a big step toward Knight Lore's gloom.
When it's wrong, see why
- The whole floor is darkest. No torch was found — check the room map contains a
T, and thatfind_torchruns before the draw.NO_TORCHmeans everything clamps to shade 4. - The pool is the wrong shape or off-centre.
shade_for_cellmixed up rows and columns, or isn't taking the larger of the two gaps. Distance ismax(|dr|, |dc|). - Floor cells are solid / the thief is trapped. A shade tile was given a bright attribute. All five shades share
FLOOR_ATTR(dim); only the pattern changes, so they stay walkable. - The torch is walkable / draws as floor.
Tmust be in the palette with a bright attribute, and the floor branch only catches.— anything else falls through to the palette. - The lit area is a hard-edged block. That's the square (Chebyshev) distance showing its corners. It's fine, and true to the machine; for a rounder pool, add the gaps instead of taking the larger (a diamond) or sum their squares (a circle, more maths).
Before and after
You started with a keep lit like an office — every cell the same flat blue — and finished with rooms that have a pool of light and a dark to step into. Nothing new drew the change: it's the dither-is-shade idea from Unit 2, with the density now chosen by distance from a flame instead of fixed. A torch is a glyph you type into the map; the light bakes itself as the room paints; and the thief carries each shade beneath him without a line of extra code. The keep stopped being evenly lit and started having somewhere to hide — the first real stroke of atmosphere.
What you've learnt
- Light is dither density chosen by distance. The shading technique from Unit 2, driven by how far a cell is from a flame — no new drawing, only a new choice.
- A light source is data. A glyph in the map; place it, move it, and the room re-lights around it.
- Bake what doesn't change. Static light is computed once as the room draws, costing nothing per frame — and the hero's save/restore carries the shade for free.
- Falloff is a feel knob. How fast light fades — one shift — sets whether the keep feels candle-lit or floodlit.
What's next
The keep has light and dark now, but its rooms are bare — stone, pillars, and not much else. A place you believe in has things in it: an altar, a sconce, a scatter of rubble, a well. In Unit 10, "Furnishings," we add decorative objects — scenery the thief walks past, not into — that turn a lit room into a room with a story. Atmosphere is light and the things the light falls on.