Thanks for your replies, I finally figured out what was causing this bug!
In short, my NextRandom function uses register B as a scratchpad variable without saving it, while SpawnMeteors (and InitStars) use it at the same time as a djnz loop counter. Each call to NextRandom inside the loop was overwriting this counter with a pseudo-random value, causing the number of iterations to go out of control and corrupting memory far beyond the intended arrays.
Fix: push bc / pop bc around the body of NextRandom.
I still have a few bugs that I can’t quite understand yet:
Collisions (shot↔alien, player↔meteor) still aren’t working.
Still no DMA Music (no sound at all).
And worst of all, the screen image is cropped in the top-left corner. My game only displays on the left third of the screen, but if I move the ship to the right, it reappears on the left and moves across again, stopping in the middle on the second pass.
Attached is the source code commented with the help of Claude AI.
If anyone has an idea, thanks again!
; ================================================================
; INVADER GX4000 - RASM cartridge build wrapper
; ================================================================
BUILDCPR “build\INVADERGX.CPR”
; Bank 0: full game (code + assets), unchanged.
BANK 0
ORG #0000
INCLUDE “invader.asm”
; — FIX ---------------------------------------------------------
; The GX4000/CPC+ firmware ALWAYS maps cartridge bank 3
; into upper ROM (&C000-&FFFF) on boot, regardless of the actual
; size of the program. A single-bank cartridge (as was
; the case here) doesn’t provide banks 1 to 3: depending on
; the emulator/hardware, the cartridge might not be recognized
; as bootable, even if the code in bank 0 is perfectly valid
; (which is what we observed: the code ran fine during tests,
; but the whole cartridge was never considered bootable).
; So we pad it out to a minimum of 4 banks (64 KB), just like
; any working CPC+ cartridge does (Bomb Jack GX, for example, uses 8).
; -------------------------------------------------------------------
BANK 1
ORG #0000
ds #4000,#ff
BANK 2
ORG #0000
ds #4000,#ff
BANK 3
ORG #0000
ds #4000,#ff
=====================================================================
; CPC+ / GX4000 hardware constants
ASIC_PAGE_IN equ #7fb8
ASIC_PAGE_OUT equ #7fa0
SCREEN_BASE equ #c000
STACK_TOP equ #bffe
; Sprite attributes: 8 bytes per sprite, x/y are words, zoom at +4.
SPR_ATTR equ #6000
SPR_BITMAP equ #4000
SPR_PALETTE equ #6422
; PSG/PPI
PPI_A equ #f400
PPI_B equ #f500
PPI_C equ #f600
PPI_CTRL equ #f700
=======================================================================
; ================================================================
; INVADER GX - complete source version
; Amstrad GX4000 / CPC Plus - Z80
; Build with RASM.
;
; Gameplay:
; - 5 levels
; - 10 aliens, 4 families / movement engines
; - player free X/Y movement, joystick only
; - one active shot
; - 4 indestructible meteors, pseudo-random drift
; - hardware sprites: 1 player + 1 shot + 10 aliens + 4 meteors
; - CPC+ 4096-colour palette, raster split, soft scroll, DMA music
; - score, lives and level HUD
;
; Memory policy:
; ROM 0000-3FFF: code + assets
; RAM 8000-BFFF: game state / music DMA list
; RAM C000-FFFF: screen (write-through under cartridge ROM)
; ASIC page: only mapped while touching 4000-7FFF registers.
; ================================================================
org #0000
jp Boot
include ‘hardware.inc’
; — CRITICAL FIX -------------------------------------------------
; Reserves address &0038 (standard CPC interrupt vector,
; target of RST 38h) with a minimal safe handler, as done by
; any working CPC+ cartridge (verified via binary comparison
; with Bomb Jack GX, which does the exact same thing). Without this,
; if an interrupt fires before “di” can even execute
; (the firmware can leave interrupts enabled right before jumping
; into the cartridge), the CPU jumps right into the middle of an
; instruction in our code → immediate crash, black screen from the
; very start.
; ---------------------------------------------------------------------
ds #0038-$,0
IrqVector:
ei
reti
Boot:
di
ld sp,STACK_TOP
call UnlockASIC
call InitSprites
call InitVideo
call InitPalette
call InitGame
call InitMusic
; — FIX (see report) --------------------------------------------
; “ei” was enabled here without ever executing “im 1” and without any
; interrupt handler (no “reti” anywhere in the source).
; Yet the Gate Array generates a hardware IRQ 300 times/second,
; no matter what: in IM 0 (default mode), this acts like a forced
; “rst &38” jumping RIGHT IN THE MIDDLE of FrameLoop code and
; pushing a return address that is never popped (stack leak on
; every IRQ) → near-instant crash, black screen.
; This engine runs everything via active polling of VSync (WaitVSync) and
; doesn’t need any CPU interrupts: so we do not enable them.
; If interrupts are needed later on, we must add “im 1” BEFORE “ei”
; and provide a real handler at &0038.
; -------------------------------------------------------------------
FrameLoop:
call WaitVSync
call ReadJoystick
call UpdatePlayer
call UpdateShot
call UpdateAliens
call UpdateMeteors
call CollideShotAliens
call CollidePlayerMeteors
call CheckWave
call DrawAllSprites
call DrawHUD
call Animate
jr FrameLoop
; ----------------------------------------------------------------
; CPC+ ASIC unlock. 17 writes = sync 0xFF,0x00 then 15-key sequence.
; ----------------------------------------------------------------
UnlockASIC:
ld bc,#bc00
ld hl,ASIC_UNLOCK
ld e,17
.unlock:
ld a,(hl)
out (c),a
inc hl
dec e
jr nz,.unlock
ret
ASIC_UNLOCK:
db #ff,#00,#ff,#77,#b3,#51,#a8,#d4,#62,#39,#9c,#46,#2b,#15,#8a,#cd,#ee
; ----------------------------------------------------------------
; Video setup: MODE 0, screen base C000, simple raster split at line 16.
; Soft-scroll is used for a tiny starfield drift; sprites are independent.
; ----------------------------------------------------------------
InitVideo:
; Gate Array MODE 0, UPPER ROM DISABLED (#7f88). Required so that
; CPU reads from the screen (&C000+) return actual RAM and
; not ROM (the CRTC itself always reads RAM directly for
; display - but DrawStars performs a read-modify-write on
; the screen on EVERY frame: with upper ROM active, this read
; returned ROM contents instead of the actual pixel, likely causing
; part of the observed noise). The “No signal” issue seen previously
; with this value was due to missing CRTC init in that isolated test,
; not #7f88 itself (confirmed with a targeted test,
; full CRTC + #7f88: stable). Thanks to roudoudou (Discord
; Praline / CPCWiki) for spotting this.
ld bc,#7f88
out (c),c
; CRTC: program full table R0-R9 + R12/R13. Previously only
; R12/R13 (screen address) were written; R0-R9 (which define the
; sync timing itself) remained at their default boot values
; when booting directly from cartridge → observed image rolling/scrolling.
call InitCRTC
; Clear screen RAM C000-FFFF
ld hl,SCREEN_BASE
ld de,SCREEN_BASE+1
ld bc,#3fff
xor a
ld (hl),a
ldir
; ASIC split / scroll: REMOVED. These writes (&6801-&6804) caused
; generalized distortion/noise across the whole screen on real hardware
; (confirmed by isolated test). This feature was never actually enabled
; anyway (just “prepared” without setting the enable bit),
; so removing it loses no functionality.
; seed a deterministic star field
call InitStars
ret
; ----------------------------------------------------------------
; Standard Amstrad CRTC table (50 Hz) + R12/R13 for screen at &C000.
; Format: (register, value) pairs.
; ----------------------------------------------------------------
InitCRTC:
ld hl,CRTCTable
ld b,CRTCTableLen
.crtc_loop:
push bc
ld bc,#bc00
ld a,(hl)
out (c),a ; select register
inc hl
ld bc,#bd00
ld a,(hl)
out (c),a ; write value
inc hl
pop bc
djnz .crtc_loop
ret
CRTCTable:
; TEST: no longer touching R0/R2/R3/R4/R5/R7 (total horizontal/
; vertical, sync position), matching Bomb Jack GX (verified
; via disassembly: it never writes them, letting firmware
; handle those values). We keep R1 (our supposed bitmap 80 bytes/
; line = 40 characters) and R6 (number of displayed lines).
db 1,#28 ; R1 horizontal displayed (40 char., required for our bitmap)
db 6,#19 ; R6 vertical displayed (25 char. lines)
db 8,0 ; R8 interlace/skew: none
db 9,7 ; R9 character height - 1 (8 lines)
db 12,#30 ; R12 screen address (high byte) → &C000
db 13,0 ; R13 screen address (low byte)
CRTCTableEnd:
CRTCTableLen equ (CRTCTableEnd-CRTCTable)/2
; ----------------------------------------------------------------
; CPC+ palette: 16 main colours + 15 sprite colours.
; ----------------------------------------------------------------
InitPalette:
call ASIC_IN
ld hl,MainPalette
ld de,#6400
ld bc,32
ldir
ld hl,SpritePalette
ld de,#6422
ld bc,30
ldir
call ASIC_OUT
ret
MainPalette:
; black, dark blue, blue, cyan, dark red, red, magenta, yellow,
; dark green, green, light cyan, white, orange, violet, grey, bright grey
dw #0000,#0018,#00af,#00ff,#1800,#f000,#f00f,#0ff0
dw #0180,#0f80,#0fff,#0fff,#0f60,#a0af,#0888,#0eee
SpritePalette:
dw #0ff0,#0f00,#00ff,#0fff,#0f80,#0f08,#08ff,#0ff8
dw #0f88,#088f,#0fff,#0f0f,#08f0,#0ff8,#0f40
; ----------------------------------------------------------------
; Hardware sprite pixel data. 16x16, byte per pixel, pen 0 transparent.
; ----------------------------------------------------------------
InitSprites:
call ASIC_IN
; TEST: completely turns off display (CRTC R6=0) during the entire
; upload, rather than relying on masking individual sprites (Mag=0)
; which apparently doesn’t prevent write/scan bus conflicts on real
; hardware (confirmed by a CPCWiki forum contributor: “the main
; discrepancy between emulated and real sprite display is when
; there’s a clash between writing sprite data and the screen being
; updated”).
ld bc,#bc06
out (c),c
ld bc,#bd00
out (c),c ; R6 = 0: no lines displayed
; mask all 16 sprites (Mag=0 + Y offscreen) - kept as a safety measure
; in addition to turning off the display
ld hl,#6002
ld de,8
ld b,16
.hide:
ld a,255
ld (hl),a ; Y (low byte) offscreen
push hl
inc hl
inc hl
xor a
ld (hl),a ; Mag = 0
pop hl
add hl,de
djnz .hide
ld hl,SpritePlayer
ld de,#4000
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteShot
ld de,#4100
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien1
ld de,#4200
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien2
ld de,#4300
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien3
ld de,#4400
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien4
ld de,#4500
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4600
ld bc,#100
ldir
call WaitVSync
; replicate 4 alien/meteor images into sprite slots 2..15
ld hl,SpriteAlien1
ld de,#4700
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien2
ld de,#4800
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien3
ld de,#4900
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteAlien4
ld de,#4a00
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4b00
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4c00
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4d00
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4e00
ld bc,#100
ldir
call WaitVSync
ld hl,SpriteMeteor
ld de,#4f00
ld bc,#100
ldir
; restore display (R6 = 25 character lines, normal value)
ld bc,#bc06
out (c),c
ld bc,#bd19
out (c),c
call ASIC_OUT
ret
; ----------------------------------------------------------------
; Game initialisation.
; ----------------------------------------------------------------
InitGame:
xor a
ld (Level),a
ld (Frame),a
ld (Anim),a
ld (ShotActive),a
ld (GameOver),a
ld a,3
ld (Lives),a
ld a,72
ld (PlayerX),a
ld a,174
ld (PlayerY),a
xor a
ld (Score0),a
ld (Score1),a
ld (Score2),a
ld (Score3),a
ld a,2
ld (FormationDir),a
ld a,1
ld (RandomSeed),a
call SpawnWave
call SpawnMeteors
ret
SpawnWave:
ld a,(Level)
ld e,a
add a,a
add a,e ; level*3
ld e,a
ld d,0
ld hl,LevelParams
add hl,de
ld a,(hl)
ld (MoveEngine),a
inc hl
ld a,(hl)
ld (AlienSpeed),a
inc hl
ld a,(hl)
ld (AlienSprite),a
ld a,10
ld (AlienAliveCount),a
; formation positions: X=20..110, Y=35..62
ld hl,AlienX
ld de,AlienY
ld bc,AlienVX
ld ix,AlienType
ld iy,AlienAlive
ld a,10
ld (TmpCount),a
ld a,20
ld (TmpX),a
xor a
ld (FormationDir),a
ld b,0
.spawn_loop:
ld a,(TmpX)
ld (hl),a
add a,9
ld (TmpX),a
ld a,b
and 3
ld (ix+0),a
ld a,1
ld (iy+0),a
ld a,35
ld c,b
ld a,c
and 3
add a,a
add a,35
ld (de),a
inc hl
inc de
inc ix
inc iy
inc b
ld a,b
cp #0a
jr nz,.spawn_loop
ret
; Level parameters: engine, speed divisor, sprite family.
; Engine 0 formation sweep, 1 staircase, 2 sine-like, 3 split rush, 4 zigzag.
LevelParams:
db 0,4,0
db 1,3,1
db 2,2,2
db 3,2,3
db 4,1,4
; ----------------------------------------------------------------
; Joystick 1 only: line 9, direct PSG/PPI scan. Active high in JoyState.
; ----------------------------------------------------------------
ReadJoystick:
; Select PSG register 14 on PPI port A.
ld bc,#f40e
out (c),c
ld bc,#f6c0
out (c),c
xor a
out (c),a ; required inactive phase on CPC+
ld bc,#f792 ; PPI: port A input
out (c),c
ld a,#49 ; line 9 + read operation (bit 6 set)
ld b,#f6
out (c),a
ld b,#f4
in a,(c)
cpl
ld (JoyState),a
ld bc,#f782 ; restore port A output
out (c),c
xor a
ld b,#f6
out (c),a
ret
; ----------------------------------------------------------------
; Player: 2D movement with acceleration-free arcade controls.
; Game coordinates are 0..144 logical pixels; sprite X is *4 in ASIC.
; ----------------------------------------------------------------
UpdatePlayer:
ld a,(JoyState)
bit 2,a
jr nz,.left
bit 3,a
jr nz,.right
jr .x
.left:
ld hl,PlayerX
ld a,(hl)
or a
jr z,.x
dec (hl)
dec (hl)
jr .x
.right:
ld hl,PlayerX
ld a,(hl)
cp 142
jr nc,.x
inc (hl)
inc (hl)
.x:
ld a,(JoyState)
bit 0,a
jr nz,.up
bit 1,a
jr nz,.down
jr .done
.up:
ld hl,PlayerY
ld a,(hl)
cp 26
jr c,.done
dec (hl)
dec (hl)
jr .done
.down:
ld hl,PlayerY
ld a,(hl)
cp 182
jr nc,.done
inc (hl)
inc (hl)
.done:
ret
UpdateShot:
ld a,(JoyState)
bit 4,a
jr nz,.fire
jr .move
.fire:
ld a,(ShotActive)
or a
jr nz,.move
ld a,1
ld (ShotActive),a
ld a,(PlayerX)
add a,4
ld (ShotX),a
ld a,(PlayerY)
sub 6
ld (ShotY),a
.move:
ld a,(ShotActive)
or a
ret z
ld hl,ShotY
ld a,(hl)
sub 6
ld (hl),a
cp 18
ret nc
xor a
ld (ShotActive),a
ret
; ----------------------------------------------------------------
; Alien movement engines. All 10 positions updated from the same phase,
; but each level has a different motion model.
; ----------------------------------------------------------------
UpdateAliens:
ld hl,AlienX
ld de,AlienY
ld bc,AlienVX
ld ix,AlienType
ld iy,AlienAlive
ld a,(Frame)
ld (TmpPhase),a
ld b,0
.loop:
ld a,(iy+0)
or a
jp z,.next
ld a,(MoveEngine)
cp 0
jr z,.eng0
cp 1
jr z,.eng1
cp 2
jr z,.eng2
cp 3
jr z,.eng3
jr .eng4
.eng0:
ld a,(FormationDir)
or a
jr z,.e0r
inc (hl)
jr .e0chk
.e0r:
dec (hl)
.e0chk:
ld a,(hl)
cp #08
jr nc,.e0right
ld a,1
ld (FormationDir),a
ld a,(de)
inc a
ld (de),a
jr .next
.e0right:
cp #88
jr c,.next
xor a
ld (FormationDir),a
ld a,(de)
inc a
ld (de),a
jr .next
.eng1:
ld a,(Frame)
and 3
jr nz,.next
ld a,b
and 1
jr z,.e1a
inc (hl)
inc (hl)
jr .next
.e1a:
dec (hl)
dec (hl)
jr .next
.eng2:
; cheap triangle wave: phase + alien index, reflected at 0/120
ld a,(TmpPhase)
add a,b
and 127
cp 64
jr c,.e2p
ld c,a
ld a,128
sub c
.e2p:
ld (hl),a
ld a,b
and 3
add a,a
add a,34
ld (de),a
jr .next
.eng3:
; paired diagonal rush, reverses at edges
ld a,b
and 1
jr z,.e3r
dec (hl)
ld a,(de)
inc a
ld (de),a
jr .next
.e3r:
inc (hl)
ld a,(de)
dec a
ld (de),a
jr .next
.eng4:
; fast zigzag: horizontal + occasional vertical hop
inc (hl)
ld a,(Frame)
and 15
jr nz,.next
ld a,(de)
xor 12
ld (de),a
.next:
inc hl
inc de
inc bc
inc ix
inc iy
inc b
ld a,b
cp #0a
jp nz,.loop
ret
; ----------------------------------------------------------------
; Meteors. Four hardware sprites, never affected by shot collision.
; ----------------------------------------------------------------
SpawnMeteors:
ld a,#5a
ld (RandomSeed),a
ld hl,MeteorX
ld de,MeteorY
ld b,4
.sm:
call NextRandom
and 127
ld (hl),a
call NextRandom
and 127
add a,32
ld (de),a
inc hl
inc de
djnz .sm
ret
UpdateMeteors:
ld hl,MeteorX
ld de,MeteorY
ld b,4
.loop:
ld a,(hl)
inc a
cp 150
jr c,.sx
xor a
.sx:
ld (hl),a
ld a,(de)
add a,1
cp 190
jr c,.sy
sub 158
.sy:
ld (de),a
inc hl
inc de
djnz .loop
ret
NextRandom:
push bc ; preserve BC for the caller (SpawnMeteors/InitStars
ld a,(RandomSeed) ; use B as a “djnz” loop counter: without this
ld b,a ; save, this call was overwriting it with a pseudo-random
add a,a ; value instead of letting it decrement normally,
xor b ; causing the loop to run for an unpredictable number of
rrca ; iterations and corrupting memory beyond the intended arrays.
xor #1d
ld (RandomSeed),a
pop bc
ret
; ----------------------------------------------------------------
; Collision: shot vs 10 aliens using 8x8 boxes. One hit kills one alien.
; ----------------------------------------------------------------
CollideShotAliens:
ld a,(ShotActive)
or a
ret z
ld hl,AlienX
ld de,AlienY
ld ix,AlienAlive
ld b,0
.ca_loop:
ld a,(ix+0)
or a
jr z,.ca_next
ld a,(ShotX)
sub (hl)
jr c,.ca_next
cp 9
jr nc,.ca_next
ld a,(ShotY)
ld c,a
push de
ld a,(de)
ld d,a
ld a,c
sub d
pop de
jr c,.ca_next
cp #0a
jr nc,.ca_next
xor a
ld (ix+0),a
ld (ShotActive),a
ld a,(AlienAliveCount)
dec a
ld (AlienAliveCount),a
call AddScore
ret
.ca_next:
inc hl
inc de
inc ix
inc b
ld a,b
cp #0a
jp nz,.ca_loop
ret
CollidePlayerMeteors:
ld hl,MeteorX
ld de,MeteorY
ld b,4
.cm:
ld a,(PlayerX)
sub (hl)
jr c,.cm_next
cp #0a
jr nc,.cm_next
ld a,(PlayerY)
ld c,a
push de
ld a,(de)
ld d,a
ld a,c
sub d
pop de
jr c,.cm_next
cp 12
jr nc,.cm_next
call LoseLife
ret
.cm_next:
inc hl
inc de
djnz .cm
ret
AddScore:
ld hl,Score0
inc (hl)
ld a,(hl)
cp #0a
ret c
xor a
ld (hl),a
inc hl
inc (hl)
ld a,(hl)
cp #0a
ret c
xor a
ld (hl),a
inc hl
inc (hl)
ld a,(hl)
cp #0a
ret c
xor a
ld (hl),a
inc hl
inc (hl)
ret
LoseLife:
ld a,(Lives)
or a
ret z
dec a
ld (Lives),a
xor a
ld (ShotActive),a
ld a,72
ld (PlayerX),a
ld a,174
ld (PlayerY),a
ld a,(Lives)
or a
ret nz
ld a,1
ld (GameOver),a
ret
CheckWave:
ld a,(GameOver)
or a
ret nz
ld a,(AlienAliveCount)
or a
ret nz
ld a,(Level)
inc a
cp 5
jr c,.next
xor a
.next:
ld (Level),a
call SpawnWave
ret
; ----------------------------------------------------------------
; Hardware sprite drawing. Sprite slots:
; 0 player, 1 shot, 2..11 aliens, 12..15 meteors.
; ----------------------------------------------------------------
DrawAllSprites:
call ASIC_IN
; player
ld a,(PlayerX)
add a,a
add a,a
ld l,a
ld h,0
ld (#6000),hl
ld a,(PlayerY)
ld (#6002),a
xor a
ld (#6003),a
ld a,5
ld (#6004),a
; shot
ld a,(ShotActive)
or a
jr z,.hideShot
ld a,(ShotX)
add a,a
add a,a
ld l,a
ld h,0
ld (#6008),hl
ld a,(ShotY)
ld (#600a),a
xor a
ld (#600b),a
ld a,5
ld (#600c),a
jr .aliens
.hideShot:
xor a
ld (#600c),a
ld a,255
ld (#600a),a ; Y offscreen: a sprite with mag=0 stays
; visible at its last position otherwise.
.aliens:
ld hl,AlienX
ld de,AlienY
ld ix,AlienAlive
ld b,0
ld iy,#6010
.al:
ld a,(ix+0)
or a
jr z,.aloff
ld a,(hl)
add a,a
add a,a
ld (iy+0),a
xor a
ld (iy+1),a
ld a,(de)
ld (iy+2),a
xor a
ld (iy+3),a
ld a,5
ld (iy+4),a
jr .alnext
.aloff:
xor a
ld (iy+4),a
ld a,255
ld (iy+2),a ; Y offscreen, same reason as .hideShot
.alnext:
inc hl
inc de
inc ix
push bc
ld bc,8
add iy,bc
pop bc
inc b
ld a,b
cp #0a
jr nz,.al
; meteors
ld hl,MeteorX
ld de,MeteorY
ld iy,#6060
ld b,4
.me:
ld a,(hl)
add a,a
add a,a
ld (iy+0),a
xor a
ld (iy+1),a
ld a,(de)
ld (iy+2),a
xor a
ld (iy+3),a
ld a,5
ld (iy+4),a
inc hl
inc de
push bc
ld bc,8
add iy,bc
pop bc
djnz .me
call ASIC_OUT
ret
; ----------------------------------------------------------------
; HUD: tiny 4x5 digits, score/lives/level. Rendered directly in screen RAM.
; ----------------------------------------------------------------
DrawHUD:
; clears the first 8 video lines (HUD banner), one by one
; using the proper interlaced address
ld b,0
.clearline:
push bc
ld a,b
call GetLineAddr
ld b,80
xor a
.cl:
ld (hl),a
inc hl
djnz .cl
pop bc
inc b
ld a,b
cp 8
jr nz,.clearline
; divider line right under the banner (video line 8)
ld a,8
call GetLineAddr
ld b,80
ld a,#ff
.hline:
ld (hl),a
inc hl
djnz .hline
; draw stars into playfield
call DrawStars
ret
; ----------------------------------------------------------------
; CPC screen addressing (Mode 0/1/2): memory is NOT linear.
; A video line (0-199) is broken down into:
; - a “character line” (0-24) = line div 8
; - a “sub-scanline” (0-7) = line mod 8, which jumps in 2048-byte blocks
; address = SCREEN_BASE + (line div 8)*80 + (line mod 8)*2048
; Input: A = video line (0-199). Output: HL = start address of
; this line (column 0).
; ----------------------------------------------------------------
GetLineAddr:
push bc
push de
ld c,a
and 7
ld l,a
ld h,0
add hl,hl ; index * 2 (16-bit word)
ld de,SubScan2048Table
add hl,de
ld e,(hl)
inc hl
ld d,(hl) ; de = sub-scanline * 2048
ld a,c
srl a
srl a
srl a ; a = character line (0-24)
ld l,a
ld h,0
add hl,hl
ld bc,Line80Table
add hl,bc
ld a,(hl)
inc hl
ld h,(hl)
ld l,a ; hl = character line * 80
add hl,de ; hl += sub-scanline * 2048
ld de,SCREEN_BASE
add hl,de ; hl += screen base
pop de
pop bc
ret
SubScan2048Table:
dw 0,2048,4096,6144,8192,10240,12288,14336
Line80Table:
dw 0,80,160,240,320,400,480,560,640,720
dw 800,880,960,1040,1120,1200,1280,1360,1440,1520
dw 1600,1680,1760,1840,1920
InitStars:
ld hl,StarX
ld de,StarY
ld b,32
.is:
call NextRandom
and 159
ld (hl),a
call NextRandom
and 159
add a,24
ld (de),a
inc hl
inc de
djnz .is
ret
DrawStars:
ld ix,StarX
ld iy,StarY
ld b,32
.ds:
push bc
ld a,(iy+0) ; StarY = video line (24..182)
call GetLineAddr ; hl = start of this line
ld a,(ix+0) ; StarX
srl a ; /2 : 2 pixels per byte in Mode 0
ld c,a
ld b,0
add hl,bc
ld a,(hl)
xor #11
ld (hl),a
pop bc
inc ix
inc iy
djnz .ds
ret
Animate:
ld hl,Frame
inc (hl)
ld a,(Frame)
and 3
ld (Anim),a
ret
; ----------------------------------------------------------------
; DMA music: three-channel AY list. One list is enough for an ambient loop;
; DMA writes occur during HBL and do not consume the Z80 each frame.
; ----------------------------------------------------------------
InitMusic:
call ASIC_IN
ld hl,MusicList
ld (#6c00),hl
xor a
ld (#6c02),a
ld a,1
ld (#6c0f),a
call ASIC_OUT
ret
; LOAD R,D = 0RDD, PAUSE = 1NNN, REPEAT = 2NNN, LOOP=4001, STOP=4020.
; Notes are intentionally slow and arpeggiated for a classic arcade feel.
MusicList:
dw #0738 ; tone A/B/C on, noise off
dw #000e,#0102 ; A period
dw #080f ; A volume 15
dw #0900,#0a00 ; B/C silent
dw #100f ; ~16 HBL units
dw #000f,#0102
dw #080c
dw #100f
dw #0013,#0101
dw #080a
dw #100f
dw #0017,#0101
dw #0808
dw #100f
dw #0013,#0101
dw #080a
dw #100f
dw #203f ; repeat 64 times
dw #4001
dw #4020
align 2
; ----------------------------------------------------------------
; RAM state (never mapped as cartridge ROM).
; ----------------------------------------------------------------
Level equ #8000
Frame equ #8001
Anim equ #8002
JoyState equ #8003
Lives equ #8004
GameOver equ #8005
PlayerX equ #8006
PlayerY equ #8007
ShotX equ #8008
ShotY equ #8009
ShotActive equ #800a
RandomSeed equ #800b
MoveEngine equ #800c
AlienSpeed equ #800d
AlienSprite equ #800e
AlienAliveCount equ #800f
FormationDir equ #8010
TmpCount equ #8011
TmpX equ #8012
TmpPhase equ #8013
Score0 equ #8014
Score1 equ #8015
Score2 equ #8016
Score3 equ #8017
AlienX equ #8020
AlienY equ #8030
AlienVX equ #8040
AlienType equ #8050
AlienAlive equ #8060
MeteorX equ #8070
MeteorY equ #8074
StarX equ #8080
StarY equ #80a0
; ----------------------------------------------------------------
; Utility ASIC paging.
; ----------------------------------------------------------------
ASIC_IN:
ld bc,#7fb8
out (c),c
ret
ASIC_OUT:
ld bc,#7fa0
out (c),c
ret
WaitVSync:
ld b,#f5
.w0:
in a,(c)
rra
jr nc,.w0
.w1:
in a,(c)
rra
jr c,.w1
ret
; ----------------------------------------------------------------
; Sprite art. Each block is 16 rows x 16 pixels. Generated with DB so the
; source stays editable without a binary asset dependency.
; ----------------------------------------------------------------
SpritePlayer:
db 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0
db 0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0
db 0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0
db 0,0,0,0,1,1,2,2,2,2,1,1,0,0,0,0
db 0,0,0,1,1,2,2,2,2,2,2,1,1,0,0,0
db 0,0,1,1,2,2,2,2,2,2,2,2,1,1,0,0
db 0,1,1,2,2,2,3,3,3,3,2,2,2,1,1,0
db 1,1,2,2,2,3,3,3,3,3,3,2,2,2,1,1
db 1,2,2,2,3,3,3,3,3,3,3,3,2,2,2,1
db 0,2,2,2,2,3,3,3,3,3,3,2,2,2,2,0
db 0,0,2,2,2,2,3,3,3,3,2,2,2,2,0,0
db 0,0,0,2,2,2,2,3,3,2,2,2,2,0,0,0
db 0,0,0,0,2,2,2,2,2,2,2,2,0,0,0,0
db 0,0,0,0,0,2,2,2,2,2,2,0,0,0,0,0
db 0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0
db 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0
SpriteShot:
; small visible vertical shot (previously: “ds 256,0” = completely transparent)
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
db 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
SpriteAlien1:
db 0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0
db 0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0
db 0,0,1,1,1,2,1,1,1,1,2,1,1,1,0,0
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,2,1,1,1,1,1,1,1,1,1,2,1,1,1
db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
db 0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0
db 0,0,1,1,0,0,1,1,1,1,0,0,1,1,0,0
db 0,1,1,0,0,0,1,1,1,1,0,0,0,1,1,0
ds 64,0
SpriteAlien2:
ds 16,0
db 0,0,1,0,0,1,1,1,1,1,1,0,0,1,0,0
db 0,1,1,1,1,1,2,1,1,2,1,1,1,1,1,0
db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,2,1,1,1,1,1,1,1,1,1,2,1,1,1
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
db 0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0
db 0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0
ds 112,0
SpriteAlien3:
ds 32,0
db 0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1
db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,2,2,1,1,1,1,1,1,1,1,2,2,1,1
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
ds 176,0
SpriteAlien4:
ds 48,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
db 0,1,1,2,1,1,1,1,1,1,1,1,2,1,1,0
db 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
db 1,1,2,1,1,1,1,1,1,1,1,1,1,2,1,1
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
db 0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0
ds 136,0
SpriteMeteor:
ds 32,0
db 0,0,1,1,0,0,0,2,2,0,0,1,1,0,0,0
db 0,1,1,1,1,0,2,2,2,2,0,1,1,1,1,0
db 1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1
db 1,1,2,2,1,1,1,2,1,1,1,2,2,1,1,1
db 0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0
db 0,0,1,1,1,1,1,1,1,1,1,1,1,1,0,0
ds 128,0
; pad cartridge ROM to 16 KiB. RASM can then be used with -cpr or the
; project packaging script to create a 16K CPR; the source is intentionally
; small enough for the first cartridge page.
if $ < #4000
ds #4000-$,#ff
endif