Mood through Constraint
The lighting engine is built; now compose with it. Give each room its own falloff — one byte — so the Hall reads as a broad lit chamber and the Vault as a near-black crypt lit only at the altar. Same two colours, same dither; restraint is the technique.
The keep is lit and furnished — but every room is lit the same. A grand hall and a burial crypt should not feel alike, and atmosphere is as much about restraint as addition. This unit stops adding features and starts composing with the ones we have. The whole palette is still two colours a cell and a ramp of dither; the art is in how we use the limit.
The lever is a single byte per room: how fast its light fades.
What you'll see by the end
The Vault, now: a single flame on the altar, a tight pocket of light around it, and beyond that darkness — the dither thickening to near-black across the floor, the walls swallowed. The thief is a faint shape down in the gloom, drawn toward the only light there is. Step back into the Hall and it's the opposite: a wide, even glow, an open and unthreatening room. Two rooms, one lighting engine, two completely different feelings.
One byte of mood
Back in Unit 9 every room halved the distance to get its shade (distance >> 1). The new idea is to make that shift a property of the room — its falloff:
room_falloff:
defb 2 ; Hall — distance >> 2: a broad, soft pool
defb 1 ; Gallery — distance >> 1: medium
defb 0 ; Vault — distance : a tight pool, deep dark
A bigger shift makes distance grow slowly, so the light reaches far — a generous, well-lit room. A shift of zero lets distance count cell-for-cell, so the light collapses to a tight ring and everything past it is black. shade_for_cell just reads the current room's falloff and shifts by it:
ld a, (current_room)
; ... index room_falloff ...
ld b, (hl) ; B = this room's falloff
ld a, e ; A = distance to the torch
.sf_shift:
dec b
jr z, .sf_clamp
srl a ; shift by falloff, then clamp
jr .sf_shift
That's the entire mechanism. The Hall is welcoming and the Vault is oppressive because of one number each.
Composing the dark
Mood isn't only the falloff — it's where the light is. The Hall's torch sits high in a wall, washing the room; the Vault has no wall torch at all. Its only flame is on the altar itself:
defb "#.............#T##.............#" ; a flame set in the altar
So in the Vault the light is born at the thing you're meant to look at, falls off at once (falloff 0), and leaves the rest to the dark. The eye has nowhere to go but the altar. That's composition: not "make it bright" or "make it dark", but put the one light where it means something and let the constraint do the rest. The Spectrum's two-colour cell, which looked like a limitation in Unit 1, is now the instrument.
Milestone — a mood per room
The shift that used to be fixed (distance >> 1 everywhere) becomes a per-room
falloff byte: shade_for_cell reads the current room's falloff and shifts the
distance by that many places. A big shift reaches far (the Hall, falloff 2); a shift
of zero collapses the light to a tight ring (the Vault, falloff 0, its only flame on
the altar). The whole composition is one number per room.
| 1 | 1 | ; Shadowkeep — Unit 11: Mood through Constraint | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 = Unit 10's end: lit, furnished rooms, all lit the same. | |
| 3 | + | ; step-01 gives each room its own falloff — the Hall broad, the Vault a tight crypt. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| 7 | 7 | WALL_ATTR equ %01001000 | |
| 8 | 8 | FLOOR_ATTR equ %00001000 | |
| 9 | 9 | TORCH_ATTR equ %01001110 | |
| 10 | - | STATUE_ATTR equ %01001111 ; BRIGHT white on blue — pale stone (solid) | |
| 11 | - | BANNER_ATTR equ %01001011 ; BRIGHT magenta on blue — a hanging (solid) | |
| 12 | - | RUBBLE_ATTR equ %00001000 ; dim, like floor — walkable broken stone | |
| 10 | + | STATUE_ATTR equ %01001111 | |
| 11 | + | BANNER_ATTR equ %01001011 | |
| 12 | + | RUBBLE_ATTR equ %00001000 | |
| 13 | 13 | MARK_ATTR equ %00001111 | |
| 14 | 14 | THIEF equ %01001010 | |
| 15 | 15 | WALL_BIT equ 6 | |
| ... | |||
| 144 | 144 | jr nz, .ft_row | |
| 145 | 145 | ret | |
| 146 | 146 | | |
| 147 | + | ; ---------------------------------------------------------------------------- | |
| 148 | + | ; shade_for_cell — row in B, column in C. Distance to the torch, shifted by | |
| 149 | + | ; THIS ROOM's falloff, clamped. Bigger falloff = light fades slower = wider, | |
| 150 | + | ; softer pool. Preserves B and C (the draw loop needs them). | |
| 151 | + | ; ---------------------------------------------------------------------------- | |
| 147 | 152 | shade_for_cell: | |
| 153 | + | push bc | |
| 148 | 154 | ld a, (torch_col) | |
| 149 | 155 | cp NO_TORCH | |
| 150 | 156 | jr z, .sf_dark | |
| ... | |||
| 165 | 171 | jr nc, .sf_max | |
| 166 | 172 | ld a, d | |
| 167 | 173 | .sf_max: | |
| 174 | + | ld e, a ; E = Chebyshev distance | |
| 175 | + | ld a, (current_room) | |
| 176 | + | ld c, a | |
| 177 | + | ld b, 0 | |
| 178 | + | ld hl, room_falloff | |
| 179 | + | add hl, bc | |
| 180 | + | ld b, (hl) ; B = this room's falloff | |
| 181 | + | ld a, e | |
| 182 | + | inc b | |
| 183 | + | .sf_shift: | |
| 184 | + | dec b | |
| 185 | + | jr z, .sf_clamp | |
| 168 | 186 | srl a | |
| 187 | + | jr .sf_shift | |
| 188 | + | .sf_clamp: | |
| 169 | 189 | cp MAX_SHADE + 1 | |
| 170 | 190 | jr c, .sf_done | |
| 171 | 191 | ld a, MAX_SHADE | |
| 172 | 192 | .sf_done: | |
| 193 | + | pop bc | |
| 173 | 194 | ret | |
| 174 | 195 | .sf_dark: | |
| 175 | 196 | ld a, MAX_SHADE | |
| 197 | + | pop bc | |
| 176 | 198 | ret | |
| 177 | 199 | | |
| 178 | 200 | draw_room: | |
| ... | |||
| 469 | 491 | add hl, de | |
| 470 | 492 | ret | |
| 471 | 493 | | |
| 472 | - | ; ---------------------------------------------------------------------------- | |
| 473 | - | ; Palette — now furnished. 'S' statue and 'B' banner are bright (solid); 'o' | |
| 474 | - | ; rubble is dim (walkable). '.' is still handled by the lighting path. | |
| 475 | - | ; ---------------------------------------------------------------------------- | |
| 476 | 494 | palette: | |
| 477 | 495 | defb '.' | |
| 478 | 496 | defw shade2_tile | |
| ... | |||
| 502 | 520 | defw shade2_tile | |
| 503 | 521 | defw shade3_tile | |
| 504 | 522 | defw shade4_tile | |
| 523 | + | | |
| 524 | + | ; One byte of mood per room: how slowly its light fades. | |
| 525 | + | room_falloff: | |
| 526 | + | defb 2 ; Hall — broad, soft pool | |
| 527 | + | defb 1 ; Gallery — medium | |
| 528 | + | defb 0 ; Vault — tight pool, deep dark | |
| 505 | 529 | | |
| 506 | 530 | rooms: | |
| 507 | 531 | defw room0_state | |
| ... | |||
| 511 | 535 | defw room2_state | |
| 512 | 536 | defb NO_EXIT, 1, NO_EXIT, NO_EXIT | |
| 513 | 537 | | |
| 514 | - | ; The Great Hall — torch above, banners flanking a statue, rubble at its feet. | |
| 538 | + | ; The Great Hall — broadly lit, a torch high in the wall. | |
| 515 | 539 | room0_template: | |
| 516 | 540 | defb "###############T################" | |
| 517 | 541 | defb "#..............................#" | |
| ... | |||
| 538 | 562 | defb "#..............................#" | |
| 539 | 563 | defb "################################" | |
| 540 | 564 | | |
| 541 | - | ; The Gallery — a banner on the west wall, a heap of rubble in the corner. | |
| 542 | 565 | room1_template: | |
| 543 | 566 | defb "###############.################" | |
| 544 | 567 | defb "#..............................#" | |
| ... | |||
| 565 | 588 | defb "#..............................#" | |
| 566 | 589 | defb "###############T################" | |
| 567 | 590 | | |
| 568 | - | ; The Vault — banners flanking the great altar. | |
| 591 | + | ; The Vault — a crypt. No wall torch; the only flame is on the altar itself, so | |
| 592 | + | ; with falloff 0 the light pools tight around it and the rest sinks to black. | |
| 569 | 593 | room2_template: | |
| 570 | - | defb "###############T################" | |
| 594 | + | defb "################################" | |
| 571 | 595 | defb "#..............................#" | |
| 572 | 596 | defb "#..............................#" | |
| 573 | 597 | defb "#..............................#" | |
| ... | |||
| 577 | 601 | defb "#..............................#" | |
| 578 | 602 | defb "#..............................#" | |
| 579 | 603 | defb "#..............................#" | |
| 604 | + | defb "#.............#T##.............#" | |
| 580 | 605 | defb "#.............####.............#" | |
| 581 | - | defb "B.............####.............#" | |
| 582 | - | defb "#.............####.............B" | |
| 606 | + | defb "#.............####.............#" | |
| 583 | 607 | defb "#.............####.............#" | |
| 584 | 608 | defb "#..............................#" | |
| 585 | 609 | defb "#..............................#" | |
| ... | |||
| 592 | 616 | defb "#..............................#" | |
| 593 | 617 | defb "###############.################" | |
| 594 | 618 | | |
| 595 | - | ; ---------------------------------------------------------------------------- | |
| 596 | - | ; Floor shade ramp. | |
| 597 | - | ; ---------------------------------------------------------------------------- | |
| 598 | 619 | shade0_tile: | |
| 599 | 620 | defb %00000000 | |
| 600 | 621 | defb %00100010 |
The complete program
; Shadowkeep — Unit 11: Mood through Constraint
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 gives each room its own falloff — the Hall broad, the Vault a tight crypt.
org 32768
WALL_ATTR equ %01001000
FLOOR_ATTR equ %00001000
TORCH_ATTR equ %01001110
STATUE_ATTR equ %01001111
BANNER_ATTR equ %01001011
RUBBLE_ATTR equ %00001000
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:
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. Distance to the torch, shifted by
; THIS ROOM's falloff, clamped. Bigger falloff = light fades slower = wider,
; softer pool. Preserves B and C (the draw loop needs them).
; ----------------------------------------------------------------------------
shade_for_cell:
push bc
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
ld a, c
ld hl, torch_col
sub (hl)
jr nc, .sf_cpos
neg
.sf_cpos:
cp d
jr nc, .sf_max
ld a, d
.sf_max:
ld e, a ; E = Chebyshev distance
ld a, (current_room)
ld c, a
ld b, 0
ld hl, room_falloff
add hl, bc
ld b, (hl) ; B = this room's falloff
ld a, e
inc b
.sf_shift:
dec b
jr z, .sf_clamp
srl a
jr .sf_shift
.sf_clamp:
cp MAX_SHADE + 1
jr c, .sf_done
ld a, MAX_SHADE
.sf_done:
pop bc
ret
.sf_dark:
ld a, MAX_SHADE
pop bc
ret
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
add a, a
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:
defb '.'
defw shade2_tile
defb FLOOR_ATTR
defb '#'
defw wall_tile
defb WALL_ATTR
defb 'T'
defw torch_tile
defb TORCH_ATTR
defb 'S'
defw statue_tile
defb STATUE_ATTR
defb 'B'
defw banner_tile
defb BANNER_ATTR
defb 'o'
defw rubble_tile
defb RUBBLE_ATTR
defb '+'
defw mark_tile
defb MARK_ATTR
shade_tiles:
defw shade0_tile
defw shade1_tile
defw shade2_tile
defw shade3_tile
defw shade4_tile
; One byte of mood per room: how slowly its light fades.
room_falloff:
defb 2 ; Hall — broad, soft pool
defb 1 ; Gallery — medium
defb 0 ; Vault — tight pool, deep dark
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 — broadly lit, a torch high in the wall.
room0_template:
defb "###############T################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "B..............S...............B"
defb "#.............ooo..............#"
defb "#..............................#"
defb "#........##..........##........#"
defb "#........##..........##........#"
defb "#..............................#"
defb "#..............................."
defb "#..............................#"
defb "#..............................#"
defb "#........##..........##........#"
defb "#........##..........##........#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "################################"
room1_template:
defb "###############.################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "###############.################"
defb "#..............................#"
defb "#..............................#"
defb "...............................#"
defb "#..............................#"
defb "#..............................#"
defb "B..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#........................ooo...#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "###############T################"
; The Vault — a crypt. No wall torch; the only flame is on the altar itself, so
; with falloff 0 the light pools tight around it and the rest sinks to black.
room2_template:
defb "################################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#.............#T##.............#"
defb "#.............####.............#"
defb "#.............####.............#"
defb "#.............####.............#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "###############.################"
shade0_tile:
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:
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:
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:
defb %00010000
defb %00111000
defb %00111000
defb %01111100
defb %01111100
defb %01111100
defb %00111000
defb %00010000
statue_tile:
defb %00111100
defb %01111110
defb %00111100
defb %00011000
defb %00011000
defb %00111100
defb %01111110
defb %01111110
banner_tile:
defb %01111110
defb %01111110
defb %01011010
defb %01011010
defb %01111110
defb %01111110
defb %00111100
defb %00011000
rubble_tile:
defb %00000000
defb %01100000
defb %01100000
defb %00000110
defb %00000110
defb %00011000
defb %00011000
defb %00000000
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
Walk the keep end to end and feel the rooms change under you — the broad light of the Hall, the medium Gallery, and the tight, fearful pocket of the Vault where the thief steps from black into a ring of altar-light:
Try this: re-light a room
Change a room's room_falloff and watch its character flip. Give the Hall a 0 and the welcoming chamber becomes a cramped, shadowed box. Give the Vault a 3 and the crypt floods with light and loses all its menace. You're not redrawing anything — you're tuning a feeling with a single digit.
Try this: a pitch-black room with one candle
Build a small room, no wall torches, a single T in a far corner, falloff 0. Almost the whole room is black; one corner glows. Drop a piece of treasure or a doorway just at the edge of the light, so the player has to step into the dark on faith to reach it. Darkness you have to enter is one of the oldest tricks in the genre.
Try this: contrast by neighbour
Put a bright room next to a dark one and a dark next to bright. Walking from the lit Hall into the black Vault feels darker than the Vault would on its own, because your eye arrives expecting light. Mood is relative — you compose the journey, not just the room.
When it's wrong, see why
- Every room looks the same.
shade_for_cellisn't readingroom_fallofffor the current room — check the index iscurrent_roomand the shift loop runs that many times. - A room is all black. Its falloff is low and its torch is far from the floor (or missing). With falloff 0 the light barely reaches; make sure there's a flame where you want the pool.
- A room is flatly bright. Falloff is too high for the room's size, so even the far corners clamp to the lightest shades. Lower it, or move the torch.
- The thief gets darker/lighter as he moves — flickering. Shade is baked per draw, not per frame; if it flickers, something is redrawing the room every frame. It should only redraw on entry.
- The pool is square, not round. Still the Chebyshev distance — fine and characterful. Sum the axes for a diamond, or their squares for a circle, if you want softer pools.
Before and after
You started with a keep where every room was lit alike and finished with three rooms that feel different — the Hall open and welcoming, the Vault a tight, fearful crypt — without adding a thing. The lighting engine is exactly Unit 9's; the change is one falloff byte per room and the choice of where each flame sits. That's composition: not more features but restraint, using the Spectrum's two-colour cell and a dither ramp — the same limit that looked like a wall in Unit 1 — as the instrument. The keep stopped being uniformly lit and started having moods.
What you've learnt
- Mood is a parameter. One byte of falloff per room turns the same lighting engine into welcoming, neutral, or fearful — composition, not new code.
- Place the light with intent. Where the flame sits, and how fast it fades, decides where the eye goes. In the Vault the light is the altar; everything else is permission to be dark.
- The constraint is the vocabulary. Two colours a cell and a dither ramp — the same limit that defined Unit 1 — is enough to make a room feel like somewhere. Restraint, not richness.
- Atmosphere is relative. Rooms read against their neighbours; you compose the walk through the keep, not just each screen.
What's next
Each room can now hold a mood — but they were composed one at a time. In Unit 12, "A Keep with Character," we pull back and treat the whole keep as one composition: a consistent hand across every room, so the three chambers read as parts of a single place with a single atmosphere, not three separate experiments. It's the unit that turns "rooms with lighting" into "a keep you believe in" — the close of the Mood and Light sub-arc.