¡Gracias por vuestras respuestas, al final di con el origen de este bug!
En resumen, mi función NextRandom utiliza el registro B como variable de trabajo sin guardarlo, mientras que SpawnMeteors (e InitStars) lo usan en paralelo como contador del bucle djnz. Cada llamada a NextRandom dentro del bucle sobrescribía este contador con un valor pseudoaleatorio, lo que hacía que el número de iteraciones se descontrolara y corrompiera la memoria mucho más allá de las tablas previstas.
Solución: push bc / pop bc alrededor del cuerpo de NextRandom.
Todavía me quedan algunos bugs incomprensibles:
Las colisiones (disparo↔alien, nave↔meteoro) siguen sin funcionar.
Sigue sin haber Música DMA (nada de sonido).
Y por último, el peor de todos: la imagen aparece recortada en la esquina superior izquierda. Mi juego solo se muestra en el tercio izquierdo de la pantalla, pero si muevo la nave hacia la derecha vuelve a aparecer a la izquierda y una vez más, pero se detiene en el medio al segundo intento.
Adjunto el código comentado con la ayuda de Claude IA.
Si a alguien se le ocurre algo, gracias de nuevo.
enlace a un vídeo del juego
; ================================================================
; INVADER GX4000 - RASM cartridge build wrapper
; ================================================================
BUILDCPR “build\INVADERGX.CPR”
; Banque 0 : le jeu complet (code + assets), inchange.
BANK 0
ORG #0000
INCLUDE “invader.asm”
; — CORRECTIF ---------------------------------------------------
; Le firmware GX4000/CPC+ mappe TOUJOURS la banque 3 de la cartouche
; en ROM haute (&C000-&FFFF) au demarrage, quelle que soit la taille
; reelle du programme. Une cartouche d’une seule banque (comme
; c’etait le cas ici) ne fournit pas les banques 1 a 3 : selon
; l’emulateur/le materiel, la cartouche peut alors ne pas etre
; reconnue comme demarrable, meme si le code de la banque 0 est
; parfaitement valide (c’est ce qu’on observait : le code s’executait
; bien lors des tests, mais la cartouche entiere n’etait jamais
; consideree comme bootable). On complete donc a 4 banques minimum
; (64 Ko), comme le fait toute cartouche CPC+ qui fonctionne
; (l’exemple Bomb Jack GX en comporte 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 - version complete source
; 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’
; — CORRECTIF CRITIQUE -------------------------------------------
; Reserve l’adresse &0038 (vecteur standard d’interruption du CPC,
; cible du RST 38h) avec un gestionnaire minimal sûr, comme le fait
; toute cartouche CPC+ qui fonctionne (verifie par comparaison
; binaire avec Bomb Jack GX, qui fait exactement pareil). Sans ca,
; si une interruption survient avant meme que “di” ait pu s’executer
; (le firmware peut laisser les interruptions actives juste avant de
; sauter dans la cartouche), le CPU saute en plein milieu d’une
; instruction de notre code → plantage immediat, ecran noir des le
; premier instant.
; ---------------------------------------------------------------------
ds #0038-$,0
IrqVector:
ei
reti
Boot:
di
ld sp,STACK_TOP
call UnlockASIC
call InitSprites
call InitVideo
call InitPalette
call InitGame
call InitMusic
; — CORRECTIF (voir rapport) ------------------------------------
; “ei” etait active ici sans jamais poser “im 1” et sans le moindre
; gestionnaire d’interruption (aucun “reti” dans tout le source).
; Le Gate Array genere pourtant une IRQ materielle 300 fois/seconde,
; quoi qu’il arrive : en IM 0 (mode par defaut), cela revient a un
; “rst &38” force qui saute EN PLEIN MILIEU du code de FrameLoop et
; empile une adresse de retour jamais depilee (fuite de pile a
; chaque IRQ) → plantage quasi immediat, ecran noir.
; Ce moteur pilote tout par attente active du VSync (WaitVSync) et
; n’a besoin d’aucune interruption CPU : on ne les active donc pas.
; Si des interruptions redeviennent necessaires plus tard, il faudra
; ajouter “im 1” AVANT “ei” et fournir un vrai gestionnaire a &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, ROM HAUTE DESACTIVEE (#7f88). Necessaire pour
; que les lectures CPU de l’ecran (&C000+) renvoient la vraie RAM et
; non la ROM (le CRTC, lui, lit toujours la RAM directement pour
; l’affichage - mais DrawStars fait un lire-modifier-ecrire sur
; l’ecran a CHAQUE trame : avec la ROM haute active, cette lecture
; renvoyait du contenu ROM au lieu du vrai pixel, source probable
; d’une partie du bruit observe). Le “Aucun signal” vu precedemment
; avec cette valeur etait du a l’absence d’init CRTC dans ce test
; isole precis, pas a #7f88 lui-meme (confirme par un nouveau test
; cible, CRTC complet + #7f88 : stable). Merci a roudoudou (Discord
; Praline / CPCWiki) d’avoir repere ce point.
ld bc,#7f88
out (c),c
; CRTC : programme la table complete R0-R9 + R12/R13. Auparavant seuls
; R12/R13 (adresse ecran) etaient ecrits ; R0-R9 (qui definissent la
; synchro elle-meme) restaient sur leurs valeurs par defaut au boot
; direct sur cartouche → defilement/roulement d’image observe.
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 : RETIRE. Ces ecritures (&6801-&6804) causaient
; une distorsion/bruit generalise sur tout l’ecran sur materiel reel
; (confirme par test isole). Cette fonctionnalite n’etait de toute
; facon jamais activee (juste “preparee” sans le bit d’activation),
; donc aucune perte fonctionnelle a la retirer.
; seed a deterministic star field
call InitStars
ret
; ----------------------------------------------------------------
; Table CRTC standard Amstrad (50 Hz) + R12/R13 pour ecran a &C000.
; Format : paires (registre, valeur).
; ----------------------------------------------------------------
InitCRTC:
ld hl,CRTCTable
ld b,CRTCTableLen
.crtc_loop:
push bc
ld bc,#bc00
ld a,(hl)
out (c),a ; selectionne le registre
inc hl
ld bc,#bd00
ld a,(hl)
out (c),a ; ecrit la valeur
inc hl
pop bc
djnz .crtc_loop
ret
CRTCTable:
; TEST : on ne touche plus R0/R2/R3/R4/R5/R7 (total horizontal/
; vertical, position de synchro), a l’instar de Bomb Jack GX (verifie
; par desassemblage : il ne les ecrit jamais, laissant le firmware
; gerer ces valeurs). On garde R1 (notre bitmap suppose 80 octets/
; ligne = 40 caracteres) et R6 (nombre de lignes affichees).
db 1,#28 ; R1 horizontal affiche (40 car., nécessaire pour notre bitmap)
db 6,#19 ; R6 vertical affiche (25 lignes car.)
db 8,0 ; R8 entrelacement/skew : aucun
db 9,7 ; R9 hauteur de caractere - 1 (8 lignes)
db 12,#30 ; R12 adresse ecran (poids fort) → &C000
db 13,0 ; R13 adresse ecran (poids faible)
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 : coupe totalement l’affichage (CRTC R6=0) pendant tout
; l’upload, plutot que de compter sur le masquage individuel des
; sprites (Mag=0) qui n’empeche apparemment pas un conflit de bus
; ecriture/balayage sur materiel reel (confirme par un contributeur
; du forum CPCWiki : “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 : plus aucune ligne affichee
; masque les 16 sprites (Mag=0 + Y hors ecran) - conserve par
; prudence en plus de la coupure d’affichage
ld hl,#6002
ld de,8
ld b,16
.hide:
ld a,255
ld (hl),a ; Y (poids faible) hors ecran
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
; restaure l’affichage (R6 = 25 lignes de caracteres, valeur normale)
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 pour l’appelant (SpawnMeteors/InitStars
ld a,(RandomSeed) ; utilisent B comme compteur de boucle “djnz” : sans
ld b,a ; cette sauvegarde, cet appel l’ecrasait avec une
add a,a ; valeur pseudo-aleatoire au lieu de le laisser se
xor b ; decrementer normalement, faisant partir la boucle
rrca ; pour un nombre d’iterations imprevisible et
xor #1d ; corrompant la memoire au-dela des tableaux prevus.
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 hors ecran : un sprite a mag=0 reste
; visible a sa derniere position sinon.
.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 hors ecran, meme raison que .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:
; efface les 8 premieres lignes video (bandeau HUD), une par une
; avec la bonne adresse entrelacee
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
; ligne separatrice juste sous le bandeau (ligne video 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
; ----------------------------------------------------------------
; Adressage ecran du CPC (Mode 0/1/2) : la memoire n’est PAS lineaire.
; Une ligne video (0-199) se decompose en :
; - une “ligne de caracteres” (0-24) = ligne div 8
; - une “sous-ligne” (0-7) = ligne mod 8, qui saute par blocs de 2048o
; adresse = SCREEN_BASE + (ligne div 8)*80 + (ligne mod 8)*2048
; Entree : A = ligne video (0-199). Sortie : HL = adresse de debut de
; cette ligne (colonne 0).
; ----------------------------------------------------------------
GetLineAddr:
push bc
push de
ld c,a
and 7
ld l,a
ld h,0
add hl,hl ; index * 2 (mot de 16 bits)
ld de,SubScan2048Table
add hl,de
ld e,(hl)
inc hl
ld d,(hl) ; de = sous-ligne * 2048
ld a,c
srl a
srl a
srl a ; a = ligne de caracteres (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 = ligne de caracteres * 80
add hl,de ; hl += sous-ligne * 2048
ld de,SCREEN_BASE
add hl,de ; hl += base ecran
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 = ligne video (24..182)
call GetLineAddr ; hl = debut de cette ligne
ld a,(ix+0) ; StarX
srl a ; /2 : 2 pixels par octet en 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:
; petit tir vertical visible (avant : “ds 256,0” = totalement 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