Okay, the uncompressed, huge image data started bugging me, so let's compress it. I fetched Einar Saukas' excellent zx0 (github) decompressor and the windows compressor, compressed the image, included the decompressor source, replaced the raw data with the compressed one.
keyselect_scr: INCBIN "keyselect.scr.zx0" INCLUDE "dzx0_standard.asm"
..and replaced the image copy ldir with a call to the decompressor:
ld de, SCREEN ld hl, keyselect_scr call dzx0_standard
Result: 3868 bytes, or 5329 bytes saved.
I also desaturated the image so it will compress a bit better. Whether the resulting image looks better or worse is a matter of taste.
We need those minitiles for the minimap, but before doing that, I wanted a few new tiles. We know that the falling rock will be its own tile, so it might as well have a separate image too. When the rock hits the player the player goes splat, let's have a tile for that. I also want a wandering monster type, so four tiles go to the purple people eater, and finally let's have water and water source. Like so:
Again, the tiles are laid out top down to make them easier to parse.
With all the tiles done, I shrunk them with point sampling down to 8x8 and did some fixing by hand. The mini-tiles don't need to look too great, as they are just for preview.
The artwork done, I again converted these to binary with using my "png2bin" tool from my speccy github tool, and designed the colors straight in the source file.
tiles: BLOCK 32,0 ; empty tile INCBIN "tiles.dat" ; 0x00 0x00 = black 0x04 0x20 = green ; 0x01 0x08 = blue 0x05 0x28 = cyan ; 0x02 0x10 = red 0x06 0x30 = yellow ; 0x03 0x18 = magenta 0x07 0x38 = white ; 0x40 = bright 0x80 = blink tilecolors: db 0x00, 0x00, 0x00, 0x00 ; 0 empty db 0x05, 0x05, 0x04, 0x04 ; 1 player db 0x10, 0x10, 0x10, 0x10 ; 2 ground db 0x57, 0x57, 0x57, 0x57 ; 3 bricks db 0x44, 0x44, 0x44, 0x44 ; 4 goo db 0x45, 0x05, 0x05, 0x45 ; 5 gem db 0x07, 0x07, 0x07, 0x07 ; 6 stone db 0x04, 0x04, 0x04, 0x04 ; 7 exit (closed) db 0x44, 0x44, 0x44, 0x44 ; 8 exit (open) db 0x07, 0x07, 0x07, 0x07 ; 9 falling stone db 0x07, 0x07, 0x02, 0x02 ; 10 splat stone db 0x03, 0x03, 0x03, 0x03 ; 11 people eater up db 0x03, 0x03, 0x03, 0x03 ; 12 people eater right db 0x03, 0x03, 0x03, 0x03 ; 13 people eater down db 0x03, 0x03, 0x03, 0x03 ; 14 people eater left db 0x0d, 0x0d, 0x0d, 0x0d ; 15 water db 0x07, 0x07, 0x0d, 0x0d ; 16 water source minitiles: BLOCK 8,0 ; empty tile INCBIN "minitiles.dat" minitilecolors: db 0x00 ; 0 empty db 0x05 ; 1 player db 0x10 ; 2 ground db 0x57 ; 3 bricks db 0x44 ; 4 goo db 0x45 ; 5 gem db 0x07 ; 6 stone db 0x04 ; 7 exit (closed) db 0x44 ; 8 exit (open) db 0x07 ; 9 falling stone db 0x07 ; 10 splat stone db 0x03 ; 11 people eater up db 0x03 ; 12 people eater right db 0x03 ; 13 people eater down db 0x03 ; 14 people eater left db 0x0d ; 15 water db 0x07 ; 16 water source
Since the minitile colors are always the first ones from the tile colors, I could have saved a few bytes by just keep using those, but maybe it's cleaner this way.
Drawminitile got a few small changes to use the new minitile data; the changes are so trivial that I don't think it makes much sense to copy all of it here.
; One tile is 8 bytes add hl, hl ; *2 add hl, hl ; *4 add hl, hl ; *8 ld de, minitiles add hl, de ld de, hl ; hl now is pointing at the start of tile x
Since one tile is 8 bytes, *8 is now enough.
; Instead of looping, we'll plot each pixel separately.. DUP 7 ld a, (de) ; Read pixels from data ld (hl), a ; Write to screen inc de ; Increment de and hl.. inc h EDUP ld a, (de) ; Read pixels from data ld (hl), a ; Write to screen
One INC DE is gone from here, since we don't need to skip data anymore.
pop hl ; tile index ld de, minitilecolors add hl, de ; hl now points at tile color
Again, simplification: *4 is gone.
Since we're going to have different kinds of physics interactions than just falling, we'll need to scan the whole map, which means we need to check that we're not falling through the bottom of the map.
The changes to scan the whole map are trivial:
physofs: db 0, 176
jr nz, physics_notlooped ld hl, 176 physics_notlooped:
Stopping items from falling below the map requires a new check at the start of physics_drop.
physics_drop: ld a, ixl cp 16*11+map jr nc, physicsdone
Since IX points at the map data and is not just the tile index, we also need to take the map address into account. Which is bad. After all, the map address is 16 bit. Let's move the map data to a 256 byte offset so we don't need to deal with this - after this change the bottom byte of the address and tile index is the same thing. (This is one of those things that would be really complicated in C, but trivial in assembly).
We'll remove the +map from above, and move the map definition to be the very last thing in the file, with ORG to say what address it should start from.
ORG $fd00 map: BLOCK 16*12,0
Careful now. The interrupt jump table is at 0xfe00 - 0xff01 and the table points at 0xfdfd, meaning we should really leave the memory area 0xfdfd-0xff01 alone. Our map will now be at 0xfd00-0xfdc0, so it's fine. There's also plenty of loose bytes in that region, but unless we're really hurting for space, we should keep everything under 0xfd00 now.
While we're dealing with things like this, let's stop player from pushing rocks outside the screen. The movestone handling gets a new check at the start.
movestone: ld a, l and a, 0xf jr z, movedone ; left border; can't be pushed cp 15 jr z, movedone ; right border; can't be pushed
Since the player can't be between the border and a rock that's already at a border, the pushing is impossible.
All that out of the way, we can start adding more physics stuff. Let's start with the falling rock.
Making the player go splat requires a few changes. We need to change the stone's state when it's moving, add handler for falling rock, and add a flag that says the player's inputs are no longer desired.
cp 0 jr nz, physicsdone ld a, (ix) cp 6 ; stone jr nz, notstone ld a, 9 ; falling stone notstone: or 0x80
In the physics_drop after we see that an item is about to fall downwards we need to check if it's a stone or not, and if it is, we replace it with a falling stone. Running the game after this change is a bit boring as the falling stone doesn't do anything. The sprite changes, though.
cp 5 jr z, physics_drop cp 6 jr z, physics_drop cp 9 jr z, physics_stone
In the physics handler we'll check for the moving stones and jump to a new handler, physics_stone.
physics_stone: ld a, ixl cp 16*11 jr nc, stone_stop ld a, (ix+16) cp 0 jr z, stone_fall stone_stop: ld (ix), 0x86 ; inert stone jp physicsdone stone_fall: ld (ix), 0x80 ld (ix+16), 0x89 jp physicsdone
The handler is pretty simple; we check if we're at the bottom and go back to inert stone if we can't, then check if there's empty space below us and if there is, move there, otherwise go inert. Testing this we see the sprite change from inert to moving back to inert like we wanted.
One thing to note is that we're using JP to physicsdone here, because our function has grown so large that JR can't reach it anymore. The assembler will complain in these cases, and the fix is trivial. I'll be doing a lot of these changes as the program grows, and won't be mentioning about it every time.
Next we'll deal with the game state. After adding a single byte variable playerdone, we need to clear it at the start of levelselect or the player won't be able to keep playing:
levelselect: ld a, 0 ld (movekey), a ld (playerdone), a
To stop player from moving (but not from hitting space), the moveplayer function's start changes to:
moveplayer: ld a, (movekey) bit 4, a jp nz, levelselect ld a, (playerdone) cp 1 ret z
What's left is changing the moving stone's behaviour to accept moving on top of the player.
cp 0 jr z, stone_fall cp 1 jr z, stone_splat
Simply check if the falling stone's target is 1 (player)...
stone_splat: ld (ix), 0x80 ld (ix+16), 0x8a ; splat ld a, 1 ld (playerdone), a jp physicsdone
...move the stone there (as the splat sprite), mark player as done, and that's it. The player has been splatted.
This chapter's version of the source as well as the new assets and source of the decompressor is available here.
Size is at 4385 bytes, which tells me that sjasmplus does not include zero bytes in the raw output (as the size should be closer to 32k if it was). The good news there is that we can keep using the .raw file for size estimates; the bad side is that we can't be sure if the zeroed data is actually zeroed. Doesn't matter in our case, but something to keep in mind. Since our optimized size at the start was 3868 bytes, we increased by 517 bytes, mostly from the new tile graphics.
Any comments etc. can be emailed to me.