Through the Doorway
Unit 5's room travel was a teleport to the centre. Make it a step: leave by one edge and arrive at the opposite edge of the next room, at the same height, mid-stride — so the flick reads as walking through a doorway.
Last unit the thief could travel the keep, but the crossing cheated: walk through any door and you appeared in the middle of the next room. It worked, but it didn't feel like a doorway — it felt like a blink. This unit fixes that, and the fix is the beating heart of the flick-screen adventure.
The rule: leave by one edge, arrive at the opposite edge — at the same height. Walk off the east edge at row 11 and you step in at the west edge, still row 11, mid-stride. Your position across the seam is preserved; only the side flips. That one change turns a teleport into a step.
What you'll see by the end
Walk east out of the pillared hall and you don't land in the middle of the next room — you arrive at its west doorway, at the same height you left, as if you'd walked straight through the wall. Turn around and walk back west, and you emerge at the east doorway of the hall, again at the same height. The two rooms feel joined at the threshold now, not wired together by teleport.
Opposite edge, same height
In Unit 5, travelling always set the thief to the centre. Now each edge sets a different entry point — the opposite edge — and leaves the cross-axis (his height for an east/west door, his column for a north/south door) untouched:
.east: ; left by the east edge -> enter at the west
; ... follow the east link into current_room ...
ld a, 1 ; west side, one cell in
ld (thief_col), a ; (thief_row is left as it was)
jr .enter
.west: ; left by the west edge -> enter at the east
; ... follow the west link ...
ld a, 30 ; east side, one cell in
ld (thief_col), a
jr .enter
North and south mirror this on the other axis: leave by the top, arrive near the bottom at the same column; leave by the bottom, arrive near the top. We only set the axis we crossed; the other carries over, and that's what makes it read as a continuous step.
One cell in, not on the edge
Notice the entry column is 1, not 0 — one cell inside the west wall, not on it. That matters. The west edge is column 0; if we dropped him exactly there, check_exit would see him on an edge the next frame and bounce him straight back out. Landing one cell in puts him safely on the floor just past the doorway, free to walk on. The same reason gives 30 (not 31) for the east, 22 for the bottom, 1 for the top.
Doors have to line up
For this to land him on floor and not face-first into stone, the doorways must agree: room 0's east door is at row 11, so room 1's west door is at row 11 too. Leave east at row 11, arrive west at row 11 — straight onto the doorway floor of the next room. Lining up connected doors is now part of designing the keep; a door that leads to a blank stretch of the next room's wall would strand the thief against it. (Our two rooms agree, so the step is clean both ways.)
Milestone — make the crossing a step
check_exit keeps Unit 5's link-following, but instead of dropping the thief in the centre it sets only the axis he crossed — to the opposite edge, one cell in — and leaves the other axis alone. Leave east at row 11, arrive west at row 11; his height carries across the seam.
| 1 | 1 | ; Shadowkeep — Unit 6: Through the Doorway | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 = Unit 5's end: travel works, but lands the thief in the room's centre. | |
| 3 | + | ; step-01 makes the crossing a step — arrive at the opposite edge, same height. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 42 | 42 | call player_step | |
| 43 | 43 | jr .loop | |
| 44 | 44 | | |
| 45 | - | ; ---------------------------------------------------------------------------- | |
| 46 | - | ; room_entry_addr — HL = rooms + current_room * 6 (entry = map ptr + 4 links). | |
| 47 | - | ; ---------------------------------------------------------------------------- | |
| 48 | 45 | room_entry_addr: | |
| 49 | 46 | ld a, (current_room) | |
| 50 | 47 | ld l, a | |
| 51 | 48 | ld h, 0 | |
| 52 | 49 | ld d, h | |
| 53 | - | ld e, l ; DE = room number | |
| 54 | - | add hl, hl ; *2 | |
| 55 | - | add hl, de ; *3 | |
| 56 | - | add hl, hl ; *6 | |
| 50 | + | ld e, l | |
| 51 | + | add hl, hl | |
| 52 | + | add hl, de | |
| 53 | + | add hl, hl | |
| 57 | 54 | ld de, rooms | |
| 58 | 55 | add hl, de | |
| 59 | 56 | ret | |
| 60 | 57 | | |
| 61 | - | ; ---------------------------------------------------------------------------- | |
| 62 | - | ; draw_room — draw the current room's map. Same walk as Unit 4, but the map | |
| 63 | - | ; pointer comes from the room table now, not a fixed label. | |
| 64 | - | ; ---------------------------------------------------------------------------- | |
| 65 | 58 | draw_room: | |
| 66 | 59 | call room_entry_addr | |
| 67 | - | ld a, (hl) ; map pointer, low | |
| 60 | + | ld a, (hl) | |
| 68 | 61 | inc hl | |
| 69 | - | ld h, (hl) ; map pointer, high | |
| 62 | + | ld h, (hl) | |
| 70 | 63 | ld l, a | |
| 71 | 64 | ld (map_ptr), hl | |
| 72 | 65 | ld b, 0 | |
| ... | |||
| 128 | 121 | pop bc | |
| 129 | 122 | ret | |
| 130 | 123 | | |
| 131 | - | ; ---------------------------------------------------------------------------- | |
| 132 | - | ; player_step — move as before, then check whether he stepped onto a doorway. | |
| 133 | - | ; ---------------------------------------------------------------------------- | |
| 134 | 124 | player_step: | |
| 135 | 125 | ld a, (thief_col) | |
| 136 | 126 | ld (tcol), a | |
| ... | |||
| 191 | 181 | ret | |
| 192 | 182 | | |
| 193 | 183 | ; ---------------------------------------------------------------------------- | |
| 194 | - | ; check_exit — if the thief is standing on an edge, follow that edge's link in | |
| 195 | - | ; the room table and travel there. | |
| 184 | + | ; check_exit — on an edge, follow that edge's link AND set the entry position | |
| 185 | + | ; to the opposite edge at the same height: a step through the doorway, not a | |
| 186 | + | ; jump to the middle. He arrives one cell inside the edge so he doesn't sit on | |
| 187 | + | ; the doorway and bounce straight back. | |
| 196 | 188 | ; ---------------------------------------------------------------------------- | |
| 197 | 189 | check_exit: | |
| 198 | 190 | ld a, (thief_col) | |
| 199 | 191 | or a | |
| 200 | - | jr z, .west ; column 0 | |
| 192 | + | jr z, .west | |
| 201 | 193 | cp 31 | |
| 202 | - | jr z, .east ; column 31 | |
| 194 | + | jr z, .east | |
| 203 | 195 | ld a, (thief_row) | |
| 204 | 196 | or a | |
| 205 | - | jr z, .north ; row 0 | |
| 197 | + | jr z, .north | |
| 206 | 198 | cp 23 | |
| 207 | - | jr z, .south ; row 23 | |
| 208 | - | ret ; not on an edge | |
| 209 | - | .north: | |
| 199 | + | jr z, .south | |
| 200 | + | ret | |
| 201 | + | .east: ; left by the east edge -> enter at the west | |
| 210 | 202 | call room_entry_addr | |
| 211 | - | inc hl | |
| 212 | - | inc hl ; +2 = North link | |
| 203 | + | ld de, 4 | |
| 204 | + | add hl, de | |
| 213 | 205 | ld a, (hl) | |
| 214 | - | jr .travel | |
| 215 | - | .south: | |
| 206 | + | cp NO_EXIT | |
| 207 | + | ret z | |
| 208 | + | ld (current_room), a | |
| 209 | + | ld a, 1 ; west side, one cell in; row unchanged | |
| 210 | + | ld (thief_col), a | |
| 211 | + | jr .enter | |
| 212 | + | .west: ; left by the west edge -> enter at the east | |
| 216 | 213 | call room_entry_addr | |
| 217 | - | inc hl | |
| 218 | - | inc hl | |
| 219 | - | inc hl ; +3 = South link | |
| 214 | + | ld de, 5 | |
| 215 | + | add hl, de | |
| 220 | 216 | ld a, (hl) | |
| 221 | - | jr .travel | |
| 222 | - | .east: | |
| 217 | + | cp NO_EXIT | |
| 218 | + | ret z | |
| 219 | + | ld (current_room), a | |
| 220 | + | ld a, 30 ; east side, one cell in; row unchanged | |
| 221 | + | ld (thief_col), a | |
| 222 | + | jr .enter | |
| 223 | + | .north: ; left by the north edge -> enter at the south | |
| 223 | 224 | call room_entry_addr | |
| 224 | - | ld de, 4 ; +4 = East link | |
| 225 | - | add hl, de | |
| 225 | + | inc hl | |
| 226 | + | inc hl | |
| 226 | 227 | ld a, (hl) | |
| 227 | - | jr .travel | |
| 228 | - | .west: | |
| 228 | + | cp NO_EXIT | |
| 229 | + | ret z | |
| 230 | + | ld (current_room), a | |
| 231 | + | ld a, 22 ; bottom, one cell in; column unchanged | |
| 232 | + | ld (thief_row), a | |
| 233 | + | jr .enter | |
| 234 | + | .south: ; left by the south edge -> enter at the north | |
| 229 | 235 | call room_entry_addr | |
| 230 | - | ld de, 5 ; +5 = West link | |
| 231 | - | add hl, de | |
| 236 | + | inc hl | |
| 237 | + | inc hl | |
| 238 | + | inc hl | |
| 232 | 239 | ld a, (hl) | |
| 233 | - | .travel: | |
| 234 | 240 | cp NO_EXIT | |
| 235 | - | ret z ; a doorway to nowhere — stay | |
| 241 | + | ret z | |
| 236 | 242 | ld (current_room), a | |
| 237 | - | call draw_room | |
| 238 | - | ld a, START_COL ; crude: drop him in the centre (Unit 6 fixes this) | |
| 239 | - | ld (thief_col), a | |
| 240 | - | ld a, START_ROW | |
| 243 | + | ld a, 1 ; top, one cell in; column unchanged | |
| 241 | 244 | ld (thief_row), a | |
| 245 | + | .enter: | |
| 246 | + | call draw_room | |
| 242 | 247 | call save_under | |
| 243 | 248 | call draw_thief | |
| 244 | 249 | ret | |
| ... | |||
| 335 | 340 | ret | |
| 336 | 341 | | |
| 337 | 342 | ; ---------------------------------------------------------------------------- | |
| 338 | - | ; Palette. | |
| 343 | + | ; Palette and the room graph (unchanged from Unit 5 — doors aligned at row 11). | |
| 339 | 344 | ; ---------------------------------------------------------------------------- | |
| 340 | 345 | palette: | |
| 341 | 346 | defb '.' | |
| ... | |||
| 345 | 350 | defw wall_tile | |
| 346 | 351 | defb WALL_ATTR | |
| 347 | 352 | | |
| 348 | - | ; ---------------------------------------------------------------------------- | |
| 349 | - | ; The room graph. Each entry: map pointer, then North, South, East, West links | |
| 350 | - | ; ($FF = no door). Two rooms, joined east-west: room 0's east door leads to | |
| 351 | - | ; room 1, room 1's west door leads back. | |
| 352 | - | ; ---------------------------------------------------------------------------- | |
| 353 | 353 | rooms: | |
| 354 | 354 | defw room0_data | |
| 355 | 355 | defb NO_EXIT, NO_EXIT, 1, NO_EXIT | |
| 356 | 356 | defw room1_data | |
| 357 | 357 | defb NO_EXIT, NO_EXIT, NO_EXIT, 0 | |
| 358 | 358 | | |
| 359 | - | ; Room 0 — the pillared hall, with a doorway in the east wall (row 11). | |
| 360 | 359 | room0_data: | |
| 361 | 360 | defb "################################" | |
| 362 | 361 | defb "#..............................#" | |
| ... | |||
| 383 | 382 | defb "#..............................#" | |
| 384 | 383 | defb "################################" | |
| 385 | 384 | | |
| 386 | - | ; Room 1 — a plainer chamber with a block near the top, doorway in the west | |
| 387 | - | ; wall (row 11) leading back to room 0. | |
| 388 | 385 | room1_data: | |
| 389 | 386 | defb "################################" | |
| 390 | 387 | defb "#..............................#" | |
| ... | |||
| 411 | 408 | defb "#..............................#" | |
| 412 | 409 | defb "################################" | |
| 413 | 410 | | |
| 414 | - | ; ---------------------------------------------------------------------------- | |
| 415 | - | ; Tiles and the thief. | |
| 416 | - | ; ---------------------------------------------------------------------------- | |
| 417 | 411 | floor_tile: | |
| 418 | 412 | defb %10101010 | |
| 419 | 413 | defb %01010101 | |
| ... | |||
| 444 | 438 | defb %00111100 | |
| 445 | 439 | defb %00100100 | |
| 446 | 440 | | |
| 447 | - | ; ---------------------------------------------------------------------------- | |
| 448 | - | ; Variables. | |
| 449 | - | ; ---------------------------------------------------------------------------- | |
| 450 | 441 | current_room: | |
| 451 | 442 | defb 0 | |
| 452 | 443 | thief_col: |
The complete program
; Shadowkeep — Unit 6: Through the Doorway
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 makes the crossing a step — arrive at the opposite edge, same height.
org 32768
WALL_ATTR equ %01001000
FLOOR_ATTR equ %00001000
THIEF equ %01001010
WALL_BIT equ 6
START_COL equ 15
START_ROW equ 11
NO_EXIT equ $FF
KEYS_OP equ $DFFE
KEYS_Q equ $FBFE
KEYS_A equ $FDFE
; ----------------------------------------------------------------------------
; SETUP.
; ----------------------------------------------------------------------------
start:
ld a, 0
out ($FE), a
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
jr .loop
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
draw_room:
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)
call lookup_tile
call draw_tile
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 — on an edge, follow that edge's link AND set the entry position
; to the opposite edge at the same height: a step through the doorway, not a
; jump to the middle. He arrives one cell inside the edge so he doesn't sit on
; the doorway and bounce straight back.
; ----------------------------------------------------------------------------
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: ; left by the east edge -> enter at the west
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 ; west side, one cell in; row unchanged
ld (thief_col), a
jr .enter
.west: ; left by the west edge -> enter at the east
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 ; east side, one cell in; row unchanged
ld (thief_col), a
jr .enter
.north: ; left by the north edge -> enter at the south
call room_entry_addr
inc hl
inc hl
ld a, (hl)
cp NO_EXIT
ret z
ld (current_room), a
ld a, 22 ; bottom, one cell in; column unchanged
ld (thief_row), a
jr .enter
.south: ; left by the south edge -> enter at the north
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 ; top, one cell in; column unchanged
ld (thief_row), a
.enter:
call draw_room
call save_under
call draw_thief
ret
; ----------------------------------------------------------------------------
; Save / restore / draw the thief — unchanged.
; ----------------------------------------------------------------------------
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 and the room graph (unchanged from Unit 5 — doors aligned at row 11).
; ----------------------------------------------------------------------------
palette:
defb '.'
defw floor_tile
defb FLOOR_ATTR
defb '#'
defw wall_tile
defb WALL_ATTR
rooms:
defw room0_data
defb NO_EXIT, NO_EXIT, 1, NO_EXIT
defw room1_data
defb NO_EXIT, NO_EXIT, NO_EXIT, 0
room0_data:
defb "################################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#........##..........##........#"
defb "#........##..........##........#"
defb "#..............................#"
defb "#..............................."
defb "#..............................#"
defb "#..............................#"
defb "#........##..........##........#"
defb "#........##..........##........#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "################################"
room1_data:
defb "################################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............##..............#"
defb "#..............##..............#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "...............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "################################"
floor_tile:
defb %10101010
defb %01010101
defb %10101010
defb %01010101
defb %10101010
defb %01010101
defb %10101010
defb %01010101
wall_tile:
defb %00010001
defb %00000000
defb %01000100
defb %00000000
defb %00010001
defb %00000000
defb %01000100
defb %00000000
thief:
defb %00011000
defb %00111100
defb %01111110
defb %01111110
defb %01111110
defb %01111110
defb %00111100
defb %00100100
current_room:
defb 0
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
end start
Walk east through the hall's right-hand door and you arrive at the left edge of the next room, level with where you left — then walk on. It feels like one place now:
Try this: misalign a door
Move room 1's west doorway up a few rows (shift the . in its west wall) but leave room 0's east door at row 11. Walk east. You'll arrive at row 11 of room 1 — against its west wall, because the door's elsewhere. A vivid lesson in why connected doors must share a row (or column). Put it back, or move both to match.
Try this: keep his exact row
Right now his row carries across unchanged — try breaking it on purpose: in .east, also force thief_row to the centre. Walk through at the top of the door, and watch him jump to mid-height as he crosses. Ugly — and proof of how much the preserved cross-axis was doing for the feel. Undo it.
Try this: a corridor of rooms
Add a third room east of room 1 (room 1 gains an east door and link; room 2 gets a west door and link back). Now you can walk a straight line of three chambers, the screen flicking each time you cross a threshold, your height carried the whole way. A keep is just enough rooms with enough agreeing doors.
When it's wrong, see why
- He bounces straight back through the door. The entry cell is on the edge (column 0 or 31, row 0 or 23). Use one cell in — 1, 30, 22, 1 — so he doesn't re-trigger
check_exiton arrival. - He arrives stuck against a wall. The connected doors don't share a row (for east/west) or column (for north/south). Line them up.
- He jumps to a different height when crossing. An entry branch is writing the cross-axis too. East/west should set only
thief_col; north/south onlythief_row. - He enters at the wrong side. Check the mapping: leave east → enter west (small column); leave west → enter east (large column). It's the opposite edge, not the same one.
- The screen tears for a moment on crossing. That's the full-room repaint — the genre flick. It settles at once in play.
Before and after
You started with travel that worked but felt like a blink, and finished with a crossing that reads as a walk — leave one edge, arrive at the opposite one at the same height, one cell in so the exit check doesn't fire again. It is a handful of bytes: which axis to set, which edge to set it to, and the discipline of landing just inside. But it's the difference between a wired-up pair of screens and a keep that feels like a continuous place — and a new level-design rule, that connected doors must agree, came with it.
What you've learnt
- A doorway is a preserved coordinate. Crossing an edge flips the side you're on but keeps your position along the wall — that continuity is what makes a flick feel like a step.
- Land one cell inside the edge. On the edge itself, the exit check fires again and bounces you. Just inside, you're free to walk.
- Connected doors must agree. East-to-west doors share a row; north-to-south doors share a column. Alignment is a level-design rule now.
- Small rules, big feel. A couple of bytes of entry coordinate turn a teleport into a walk — most of "game feel" is details exactly this size.
What's next
The thief steps cleanly between rooms, but each room is drawn fresh every time he enters — the keep has no memory. In Unit 7, "The Hero Remembers," we make the keep a persistent place: it tracks where the thief is across the whole world, so the keep holds its state as he moves through it — the groundwork for rooms that stay as he left them, and for a keep big enough to get lost in. From a pair of rooms that flick, toward a world that remembers.