Invasion (antes Defender) en NTC BASIC

Invasion se adaptó a partir de mi versión para ZX Spectrum Next de Defender antes de cambiar el título a Invasion. El lanzamiento original de Defender estaba bien, pensé: «hmm, un CPC ejecutando BASIC compilado, es lo que cabría esperar», pero… al volver a probar Invasion en el ZX Spectrum Next tras haber programado Cyborg Hellgate, noté un comportamiento extraño: cuantos más enemigos mataba, más rápido iba el juego… y el CPC heredó ese mismo fallo. Ahora la versión de CPC (y la de ZX Spectrum Next) va taaan fluida. ¿Es el mejor clon de Defender para CPC? No lo sé; sigo teniendo debilidad por Defend or Die y, si no se me colgara tanto de forma aleatoria, me quedaría con Defend or Die por pura autenticidad.

Eliminé el ensamblador en línea (inline assembly) para facilitar el seguimiento del código, pero el programa se programó originalmente al 100 % en NTC BASIC y luego se optimizó, lo que se redujo principalmente al movimiento de objetos, ya que el CPC no dispone de sprites por hardware. Todos los sprites también están compilados.

main.bas COMENTARIOS

' ============================================================================
' INVASION - Amstrad CPC Edition (modo 0, sprites por software)
' ============================================================================
' NTC BASIC. El juego de la edición de ZX Spectrum Next, tal cual, en un CPC: su mundo, sus landers,
' mutantes, humanos y radar, sus sonidos y jugabilidad:
'   - los 16 patrones de sprites (dos fotogramas para la nave, landers, mutantes y humanos, un disparo
'     enemigo, una explosión, el baiter) dibujados por el programa (Cpc.cls) a partir del arte de INVASION.SPR
'     convertido a modo 0 (CpcData.cls, mediante tools/cpcgfx.py): 8 x 16 píxeles de modo 0 cada uno, el
'     mismo tamaño en pantalla
'   - estrellas de fondo, con scroll más lento que el suelo, un paisaje de 1024 píxeles, el radar
'     mostrando dónde está la pantalla (la pantalla muestra 320 de los píxeles del mundo: un píxel de modo 0 son
'     dos del Next)
'   - sonidos para todo en el AY (Sfx.cls): láser, explosiones, abducciones, rescates, la
'     bomba inteligente, hiperespacio, el propulsor
'   - perder una nave no reinicia la oleada; un momento de invulnerabilidad después; la nave gira
'     suavemente; los humanos caminan, caen (desde muy alto mueren), son atrapados y depositados (500 cada uno);
'     el baiter aparece si una oleada se alarga; una nave extra y bomba inteligente cada 10.000; los
'     humanos y el planeta vuelven cada quinta oleada
' Dos pantallas (#C000 y #0000; el programa se compila desde #4000); en cada fotograma Cpc.Draw coloca los
' sprites, el suelo y las estrellas en la que no se muestra, Cpc.Flip la muestra.
' Un CPC procesa un fotograma de esto en 25-40 ms, y Cpc.Flip muestra uno cada segundo 50.º de   ' <--- MODIFICADO
' segundo, constante (el Next lo hacía cincuenta veces por segundo), así que cada fotograma aquí son dos del Next:   ' <--- MODIFICADO
' las velocidades se duplican y los tiempos en fotogramas se reducen a la mitad (el sonido se actualiza dos veces por fotograma): el   ' <--- MODIFICADO
' ritmo del Next.   ' <--- MODIFICADO


2 Me gusta

main.bas

Option Explicit
Option Turbo
Option Org &H4000                 ' la segunda pantalla es #0000-#3FFF (Cpc.cls); el programa desde #4000

' Constantes de Mundo y Pantalla (la vista: 320 x 192 píxeles del Next, 160 x 192 modo 0; y cuenta hacia arriba)
Const WORLD_W As Integer = 1024
Const WORLD_MASK As Integer = 1023
Const VIEW_W As Integer = 320
Const PLAY_TOP As Integer = 30
Const PLAY_BOT As Integer = 146

' Índices de patrones de sprites (CpcData.cls, de INVASION.SPR)
Const PAT_PLAYER As Integer = 0
Const PAT_LANDER As Integer = 1
Const PAT_MUTANT As Integer = 2
Const PAT_HUMAN  As Integer = 3
Const PAT_BOLT   As Integer = 4
Const PAT_FRAME2 As Integer = 5      ' + esto: el segundo fotograma de 0-3 (nave, lander, mutante, humano): patrones 5-8
Const PAT_SHOT   As Integer = 9
Const PAT_BOOM   As Integer = 10     ' 10-13
Const PAT_BAITER As Integer = 14     ' 14-15

' Asignación de ranuras de sprites (Cpc.Spr)
Const SLOT_PLAYER As Integer = 0
Const SLOT_LASER0 As Integer = 1
Const SLOT_HUMAN0 As Integer = 5
Const SLOT_ENEMY0 As Integer = 13
Const SLOT_SHOT0  As Integer = 23
Const SLOT_BOOM0  As Integer = 29

' Cantidades de entidades
Const MAX_LASERS As Integer = 4
Const MAX_HUMANS As Integer = 8
Const MAX_ENEMIES As Integer = 10
Const MAX_SHOTS As Integer = 6
Const MAX_BOOMS As Integer = 4

' Estados de los enemigos
Const ST_DEAD As Integer = 0
Const ST_WANDER As Integer = 1
Const ST_DESCEND As Integer = 2
Const ST_ABDUCT As Integer = 3
Const ST_MUTANT As Integer = 4
Const ST_BAITER As Integer = 5

Const H_DEAD As Integer = 0
Const H_GROUND As Integer = 1
Const H_GRABBED As Integer = 2
Const H_FALLING As Integer = 3
Const H_CARRIED As Integer = 4

' Lo que devuelven EnemyStep y HumanStep (Z80)
Const EV_GRABBED As Integer = 1      ' el lander ha atrapado a un humano
Const EV_TAKEN As Integer = 2        ' se ha llevado a uno por la parte superior: ahora es un mutante
Const EV_LASERED As Integer = 3      ' alcanzado por un láser: su número en los bits 4-5
Const EV_RAMMED As Integer = 4       ' chocó contra la nave
Const EV_SHOOT As Integer = 64       ' y dispara (es_x: su posición horizontal en la vista)
Const EV_SETDOWN As Integer = 1      ' se posa a un humano transportado
Const EV_CAUGHT As Integer = 2       ' se atrapa a un humano que caía
Const EV_FELL As Integer = 3         ' cayó desde demasiado alto
Const EV_LANDED As Integer = 4       ' cayó y aterrizó
Dim es_x As Integer

' Plumas (Cpc.cls: 0 negro, 1 blanco, 2 gris, 3 cian, 4 azul cielo, 5 azul, 6 amarillo, 7 naranja,
' 8 rojo, 9 rojo oscuro, 10 verde, 11 verde oscuro, 12 lima, 13 púrpura, 14 rosa, 15 amarillo pastel)
Const PEN_DIVIDER As Integer = 13
Const PEN_SCANNER As Integer = 10

' Estado del jugador
Dim px As Integer, py As Integer
Dim pvx As Integer, pvy As Integer
Dim pdir As Integer, psx As Integer, cam_x As Integer
Dim thrust As Integer, safeT As Integer, kb As Integer

Dim score As Long, nextBonus As Long
Dim lives As Integer
Dim bombs As Integer
Dim wave As Integer
Dim game_over As Boolean
Dim planet_dead As Boolean
Dim fire_cd As Integer
Dim bomb_cd As Integer
Dim hyper_cd As Integer
Dim flash_bg As Integer
Dim frames As Integer, waveT As Integer, baiterT As Integer, hudDirty As Integer
' de este fotograma: frames And 1, Mod 3, (\ 4) And 1, (\ 6) And 1 (una división cuesta 1.100 ciclos)
Dim f2 As Integer, f3 As Integer, f4 As Integer, f12 As Integer

' Láseres
Dim lx(MAX_LASERS) As Integer
Dim ly(MAX_LASERS) As Integer
Dim lvx(MAX_LASERS) As Integer
Dim lscr(MAX_LASERS) As Integer, lasersOn As Integer
Dim llife(MAX_LASERS) As Integer

' Humanoides
Dim hx(MAX_HUMANS) As Integer
Dim hy(MAX_HUMANS) As Integer
Dim hstate(MAX_HUMANS) As Integer
Dim hcarrier(MAX_HUMANS) As Integer
Dim hdir(MAX_HUMANS) As Integer
Dim hfall(MAX_HUMANS) As Integer, hvy(MAX_HUMANS) As Integer
Dim humans_alive As Integer

' Enemigos
Dim ex(MAX_ENEMIES) As Integer
Dim ey(MAX_ENEMIES) As Integer
Dim evx(MAX_ENEMIES) As Integer
Dim evy(MAX_ENEMIES) As Integer
Dim estate(MAX_ENEMIES) As Integer
Dim etarget(MAX_ENEMIES) As Integer
Dim enemies_alive As Integer
Dim spawn_timer As Integer
Dim enemies_to_spawn As Integer

' Proyectiles enemigos
Dim sx(MAX_SHOTS) As Integer
Dim sy(MAX_SHOTS) As Integer
Dim svx(MAX_SHOTS) As Integer
Dim svy(MAX_SHOTS) As Integer
Dim slife(MAX_SHOTS) As Integer

' Explosiones (mundo x, y, fotogramas restantes)
Dim bx(MAX_BOOMS) As Integer, by(MAX_BOOMS) As Integer, bt(MAX_BOOMS) As Integer

' El paisaje: una altura por cada 4 píxeles del mundo (256 de ellos)
Dim land(255) As Byte
Dim terDead As Boolean

' Las sentencias estilo CPC que utilizaba el original (Pen, At...)
Dim txtPen As Integer, atx As Integer, aty As Integer
Dim i As Integer, j As Integer, k As Integer, rs As Integer

' ============================================================================
' AUXILIARES DE SPRITES (las ranuras de sprites por hardware del Next, dibujadas por Cpc.Draw)
' ============================================================================

' Las ranuras de sprites son Cpc.Spr slot, x (0..319 a lo ancho de la vista), y (0..191, hacia arriba), pattern, mirror,
' shown y Cpc.Hide slot: llamadas directamente (una llamada a Sub intermedia cuesta 100 ciclos)

' ============================================================================
' AUXILIARES DE COORDENADAS Y TERRENO
' ============================================================================

' el paisaje, generado una sola vez: colinas y montañas que suben y bajan (no más de 4 píxeles
' por cada 4 horizontales, para que la línea no tenga cortes)
Sub MakeLand()
    Dim h As Integer, t As Integer
    rs = 12345 : h = 14 : t = 14
    For k = 0 To 255
        If k Mod 16 = 0 Then t = 8 + Rand(23)
        If h < t Then h = h + 1 + Rand(3)
        If h > t Then h = h - 1 - Rand(3)
        If h < 8 Then h = 8
        If h > 32 Then h = 32
        land(k) = h
    Next k
    ' los últimos se enlazan de vuelta con los primeros, cerrando el bucle en todo su recorrido
    For k = 248 To 255
        land(k) = land(247) + (land(0) - land(247)) * (k - 247) \ 9
    Next k
End Sub

' Rand(n): 0 a n - 1 a partir de rs, siempre idéntico con la misma semilla (el paisaje y las
' estrellas se generan a partir de él). Una secuencia de 16 bits: la original de 32 bits tomaba 10.000 ciclos por intento
Function Rand(ByVal n As Integer) As Integer
    rs = (rs * 2053 + 13849) And &H7FFF
    Rand = (rs \ 32) Mod n
End Function

Function GetTerrainY(ByVal wx As Integer) As Integer
    If planet_dead Then GetTerrainY = 6 Else GetTerrainY = land(Cpc.Shr(wx, 2) And 255)
End Function

' Cpc.ScrX(wx, cx): posición horizontal en la vista del punto wx del mundo respecto a cx, de -512 a 511.
' El radar: los píxeles del Next, 64 + ((wx - px + 512) And 1023) \ 8 a lo ancho del recuadro (la nave en el
' centro), 153 + y \ 7 hacia arriba desde la parte inferior: Cpc.RDot wx, px, y, pen dibuja un punto allí
Function WorldToRadarX(ByVal wx As Integer) As Integer
    WorldToRadarX = 64 + Cpc.Shr((wx - px + 512) And WORLD_MASK, 3)
End Function

Dim hudScore As Long, hudLives As Integer, hudBombs As Integer, hudWave As Integer   ' <--- NUEVO (lo que muestra el HUD: solo se imprime lo que cambia)
Sub UpdateHUD()
    If score <> hudScore Then                                  ' <--- NUEVO
    Pen 15
    At 1, 1
    PrintS Right$("000000" & Mid$(Str$(score), 2), 7)
    hudScore = score                                           ' <--- NUEVO
    End If                                                     ' <--- NUEVO
    If lives <> hudLives Then                                  ' <--- NUEVO
    Pen 14
    At 10, 1
    PrintS "SHIPS" & Str$(lives) & " "
    hudLives = lives                                           ' <--- NUEVO
    End If                                                     ' <--- NUEVO
    If bombs <> hudBombs Then                                  ' <--- NUEVO
    Pen 12
    At 19, 1
    PrintS "BOMBS" & Str$(bombs) & " "
    hudBombs = bombs                                           ' <--- NUEVO
    End If                                                     ' <--- NUEVO
    If wave <> hudWave Then                                    ' <--- NUEVO
    Pen 11
    At 27, 1
    PrintS "WAVE" & Str$(wave) & " "
    hudWave = wave                                             ' <--- NUEVO
    End If                                                     ' <--- NUEVO
    hudDirty = 0
End Sub

Sub DrawStaticFrame()
    ClsAll
    hudScore = -1 : hudLives = -1 : hudBombs = -1 : hudWave = -1   ' <--- NUEVO (la pantalla está limpia: se imprime todo el HUD)
    ' Separador del escáner (línea 43) y recuadro del radar (x 62..193, y 152..182 del Next hacia arriba desde
    ' el fondo: píxeles CPC 31..96, líneas 9..39)
    Cpc.HLine 0, 43, 160, PEN_DIVIDER
    Cpc.HLine 31, 9, 66, PEN_SCANNER
    Cpc.HLine 31, 39, 66, PEN_SCANNER
    For k = 10 To 38
        Cpc.HLine 31, k, 1, PEN_SCANNER : Cpc.HLine 96, k, 1, PEN_SCANNER
    Next k
    Stars
    Cpc.Ground 7
    UpdateHUD()
End Sub

' las estrellas: dispersas por el cielo (líneas 46-155), dibujadas por Cpc.Draw desplazándose a un cuarto de
' la velocidad del suelo
Sub Stars()
    Dim x As Integer, y As Integer
    rs = 777
    For k = 0 To 31
        x = Rand(320) : y = 46 + Rand(110)
        Cpc.Star k, x, y, Val(Mid$("2211", (k Mod 4) + 1, 1))
    Next k
End Sub

' ============================================================================
' GESTIÓN DE OLEADAS Y ENTIDADES
' ============================================================================

Sub InitWave()
    Cpc.HideAll
    DrawStaticFrame()

    px = 512 : py = 96 : pvx = 0 : pvy = 0 : pdir = 1 : psx = 110
    safeT = 25

    For i = 0 To MAX_LASERS - 1
        llife(i) = 0
    Next i
    For i = 0 To MAX_SHOTS - 1
        slife(i) = 0
    Next i
    For i = 0 To MAX_BOOMS - 1
        bt(i) = 0
    Next i

    ' los humanos: todos al principio, y de vuelta cada cinco oleadas
    If wave = 1 Or (wave - 1) Mod 5 = 0 Then
        planet_dead = False
        humans_alive = MAX_HUMANS
        For i = 0 To MAX_HUMANS - 1
            hx(i) = (i * 128 + 48) And WORLD_MASK
            hstate(i) = H_GROUND
        Next i
    End If
    For i = 0 To MAX_HUMANS - 1
        hcarrier(i) = -1
        If hstate(i) <> H_DEAD Then
            hstate(i) = H_GROUND
            hy(i) = GetTerrainY(hx(i)) + 6
        End If
        If Rand(2) Then hdir(i) = 1 Else hdir(i) = -1
    Next i

    For i = 0 To MAX_ENEMIES - 1
        estate(i) = ST_DEAD
    Next i

    enemies_to_spawn = 10 + wave * 2
    If enemies_to_spawn > 24 Then enemies_to_spawn = 24
    enemies_alive = 0
    spawn_timer = 5
    waveT = 0 : baiterT = 0

    For i = 0 To 4
        SpawnEnemy()
    Next i
    UpdateHUD()
    Message "WAVE" & Str$(wave), 30
End Sub

Sub SpawnEnemy()
    If enemies_to_spawn <= 0 Then Exit Sub
    For i = 0 To MAX_ENEMIES - 1
        If estate(i) = ST_DEAD Then
            ex(i) = (px + 250 + Rnd(500)) And WORLD_MASK
            ey(i) = 110 + Rnd(30)
            If Rnd(2) = 0 Then evx(i) = 1 Else evx(i) = -1
            evy(i) = -1
            If planet_dead Then estate(i) = ST_MUTANT Else estate(i) = ST_WANDER
            etarget(i) = -1
            enemies_alive = enemies_alive + 1
            enemies_to_spawn = enemies_to_spawn - 1
            Exit Sub
        End If
    Next i
End Sub

' el baiter: un platillo veloz que va a por ti cuando una oleada dura demasiado
Sub SpawnBaiter()
    For i = 0 To MAX_ENEMIES - 1
        If estate(i) = ST_DEAD Then
            ex(i) = (px + 300) And WORLD_MASK
            ey(i) = py
            estate(i) = ST_BAITER
            evx(i) = 0 : evy(i) = 0
            etarget(i) = -1
            Sfx.Play 1, 40, 2, 13, 25, 0
            Exit Sub
        End If
    Next i
End Sub

Sub FireLaser()
    For i = 0 To MAX_LASERS - 1
        If llife(i) = 0 Then
            lx(i) = (px + pdir * 12) And WORLD_MASK
            ly(i) = py - 1
            lvx(i) = pdir * 32 + pvx
            llife(i) = 7
            Sfx.Play 0, 24, 5, 12, 9, 0
            Exit Sub
        End If
    Next i
End Sub

Sub Boom(ByVal wx As Integer, ByVal wy As Integer)
    For k = 0 To MAX_BOOMS - 1
        If bt(k) = 0 Then
            bx(k) = wx : by(k) = wy : bt(k) = 8
            Exit Sub
        End If
    Next k
End Sub

Sub KillEnemy(ByVal idx As Integer)
    Dim h As Integer
    Cpc.Hide SLOT_ENEMY0 + idx
    Boom ex(idx), ey(idx)

    h = etarget(idx)
    If h >= 0 And h < MAX_HUMANS Then
        If hstate(h) = H_GRABBED And hcarrier(h) = idx Then
            hstate(h) = H_FALLING
            hcarrier(h) = -1
            hfall(h) = hy(h) : hvy(h) = 0
        End If
    End If

    If estate(idx) = ST_BAITER Then
        AddScore 200
    Else
        AddScore 150
        enemies_alive = enemies_alive - 1
    End If
    estate(idx) = ST_DEAD
    Sfx.Play 1, 10, 1, 14, 20, 1
End Sub

Sub AddScore(ByVal n As Integer)
    score = score + n
    If score >= nextBonus Then
        nextBonus = nextBonus + 10000
        lives = lives + 1
        If bombs < 9 Then bombs = bombs + 1
        Sfx.Play 2, 60, -1, 14, 40, 0
    End If
    hudDirty = 1
End Sub

Sub TriggerSmartBomb(ByVal cx As Integer)
    Dim sx_pos As Integer
    If bombs <= 0 Then Exit Sub
    bombs = bombs - 1
    bomb_cd = 12
    Sfx.Play 2, 4, 1, 15, 45, 1
    Border 7
    Cpc.Ink 0, 26                  ' el cielo en blanco
    flash_bg = 2

    For i = 0 To MAX_ENEMIES - 1
        If estate(i) <> ST_DEAD Then
            sx_pos = Cpc.ScrX(ex(i), cx)
            If sx_pos >= -16 And sx_pos <= VIEW_W + 16 Then KillEnemy(i)
        End If
    Next i
    For i = 0 To MAX_SHOTS - 1
        slife(i) = 0
    Next i
    hudDirty = 1
End Sub

' la nave es alcanzada: explota, los demás se apartan un poco, un instante de seguridad y adelante
Sub LoseLife()
    Dim f As Integer
    Sfx.Play 2, 6, 1, 15, 70, 1
    Sfx.Play 0, 150, 6, 14, 60, 0
    For i = 0 To MAX_HUMANS - 1
        If hstate(i) = H_CARRIED Then
            hstate(i) = H_FALLING : hfall(i) = hy(i) : hvy(i) = 0
        End If
    Next i
    For f = 0 To 29
        Cpc.Spr SLOT_PLAYER, psx, py, PAT_BOOM + (Cpc.Shr(f, 1) And 3), False, (f < 20)
        If (f And 4) = 0 Then Border 2 Else Border 0
        Vsync
    Next f
    Border 0
    lives = lives - 1
    If lives <= 0 Then
        Cpc.HideAll
        game_over = True
        Exit Sub
    End If
    ' los enemigos cercanos a la nave se alejan, los disparos desaparecen
    For i = 0 To MAX_ENEMIES - 1
        If estate(i) <> ST_DEAD Then
            If Abs(Cpc.ScrX(ex(i), px)) < 200 Then ex(i) = (ex(i) + 400) And WORLD_MASK
            If estate(i) = ST_BAITER Then
                estate(i) = ST_DEAD : Cpc.Hide SLOT_ENEMY0 + i
            End If
        End If
    Next i
    For i = 0 To MAX_SHOTS - 1
        slife(i) = 0
    Next i
    py = 96 : pvx = 0 : safeT = 50 : baiterT = 0
    hudDirty = 1
End Sub

' un mensaje en el centro del área de juego durante un tiempo
Dim msgT As Integer, msg As String
Sub Message(ByVal m As String, ByVal t As Integer)
    Pen 14
    At 17 - Cpc.Shr(Len(m), 1), 11                             ' <--- MODIFICADO (antes era Len(m) \ 2: una división)
    PrintS m
    msgT = t : msg = m
End Sub

' ============================================================================
' LAS PARTES DE UN FOTOGRAMA
' ============================================================================

Sub Controls()
    kb = Cpc.Keys()
    ' Arriba: Q o Joy Arriba | Abajo: A o Joy Abajo
    If kb And 1 Then
        If pvy < 6 Then pvy = pvy + 2
    ElseIf kb And 2 Then
        If pvy > -6 Then pvy = pvy - 2
    Else
        pvy = 0
    End If

    ' Izquierda: O o Joy Izquierda | Derecha: P o Joy Derecha
    thrust = 0
    If kb And 4 Then
        pdir = -1 : thrust = 1
        If pvx > -20 Then pvx = pvx - 2
    ElseIf kb And 8 Then
        pdir = 1 : thrust = 1
        If pvx < 20 Then pvx = pvx + 2
    ElseIf f3 <> 0 Then
        If pvx > 0 Then pvx = pvx - 1
        If pvx < 0 Then pvx = pvx + 1
    End If
    If thrust And Sfx.Busy(2) = 0 Then Sfx.Play 2, 28, 0, 5, 3, 1

    px = (px + pvx) And WORLD_MASK
    py = py + pvy
    If py < PLAY_TOP Then py = PLAY_TOP
    If py > PLAY_BOT Then py = PLAY_BOT

    ' la nave se sitúa hacia un lado, dejando espacio por delante: al girar, se desliza hacia el otro
    If pdir > 0 And psx > 110 Then psx = psx - 8
    If pdir < 0 And psx < 210 Then psx = psx + 8
    cam_x = (px - psx) And WORLD_MASK

    ' Disparar láser: Espacio, Z o Disparo 1 de joystick
    If fire_cd > 0 Then fire_cd = fire_cd - 1
    If (kb And 16) And fire_cd = 0 Then
        FireLaser()
        fire_cd = 3
    End If

    ' Bomba inteligente: B, M o Disparo 2 de joystick
    If bomb_cd > 0 Then bomb_cd = bomb_cd - 1
    If (kb And 32) And bomb_cd = 0 Then TriggerSmartBomb(cam_x)

    ' Hiperespacio: H: en algún otro lugar, al azar
    If hyper_cd > 0 Then hyper_cd = hyper_cd - 1
    If (kb And 64) And hyper_cd = 0 Then
        px = Rnd(WORLD_W)
        py = 50 + Rnd(85)
        pvx = 0
        hyper_cd = 25 : safeT = 12
        Sfx.Play 0, 500, -24, 14, 20, 0
    End If
End Sub

Sub DrawPlayer()
    Dim f As Integer
    f = PAT_PLAYER
    If thrust And f2 Then f = PAT_PLAYER + PAT_FRAME2
    Cpc.Spr SLOT_PLAYER, psx, py, f, (pdir < 0), (safeT = 0 Or (safeT And 2) = 0)
    If safeT > 0 Then safeT = safeT - 1
    Cpc.RDot px, px, py + 7, 1
End Sub

' Láseres: los disparos avanzan y se dibujan (Z80); lscr y lasersOn para la detección de colisiones enemiga
Sub Lasers()
    ' LENGUAJE ENSAMBLADOR INTEGRADO ELIMINADO
End Sub

Sub Humans()
    ' cada humano es desplazado y dibujado por HumanStep (Z80); lo devuelto es lo que el juego debe
    ' gestionar al respecto
    Dim ev As Integer
    i = 0                                                      ' <--- NUEVO
    Do While i < MAX_HUMANS                                    ' <--- MODIFICADO (antes era For i = 0 To MAX_HUMANS - 1)
        ev = HumanStep(i)                                      ' <--- MODIFICADO (Z80: desde el humano i en adelante hasta el primero con algo pendiente; i = dicho humano)
        If ev Then                                             ' <--- NUEVO (nada que hacer: sin comprobaciones)
        If ev = EV_SETDOWN Then
            AddScore 500
            Sfx.Play 0, 90, -3, 14, 20, 0
        ElseIf ev = EV_CAUGHT Then
            AddScore 500
            Sfx.Play 0, 140, -6, 14, 14, 0
        ElseIf ev = EV_FELL Then
            ' caída demasiado alta
            Boom hx(i), hy(i)
            Sfx.Play 1, 100, 12, 13, 25, 0
            HumansGone
        ElseIf ev = EV_LANDED Then
            AddScore 250
        End If
        End If                                                 ' <--- NUEVO
        i = i + 1                                              ' <--- NUEVO
    Loop                                                       ' <--- MODIFICADO (antes era Next i)
End Sub

' sin humanos restantes: el planeta desaparece y todo lander se transforma en mutante
Sub HumansGone()
    If humans_alive <= 0 And Not planet_dead Then
        planet_dead = True
        Border 2
        flash_bg = 5
        Sfx.Play 2, 3, 1, 15, 60, 1
        For k = 0 To MAX_ENEMIES - 1
            If estate(k) = ST_WANDER Or estate(k) = ST_DESCEND Then estate(k) = ST_MUTANT
        Next k
        Message "THE PLANET IS GONE", 50
    End If
End Sub

Sub Enemies()
    Dim ev As Integer, l_idx As Integer

    If spawn_timer > 0 Then
        spawn_timer = spawn_timer - 1
    ElseIf enemies_to_spawn > 0 And enemies_alive < 7 Then
        SpawnEnemy()
        spawn_timer = 30
    End If
    ' el baiter: tras 40 segundos, luego cada 12
    waveT = waveT + 1
    If waveT > 1000 Then
        baiterT = baiterT + 1
        If baiterT >= 300 Or waveT = 1001 Then
            baiterT = 0
            SpawnBaiter()
        End If
    End If

    ' cada enemigo es desplazado, dibujado y comprobado por EnemyStep (Z80: antes eran 90 palabras clave por enemigo);
    ' lo devuelto es lo que el juego debe gestionar al respecto
    i = 0                                                      ' <--- NUEVO
    Do While i < MAX_ENEMIES                                   ' <--- MODIFICADO (antes era For i = 0 To MAX_ENEMIES - 1)
        ev = EnemyStep(i)                                      ' <--- MODIFICADO (Z80: desde el enemigo i en adelante hasta el primero con algo pendiente; i = dicho enemigo)
        If ev Then
            If (ev And 15) = EV_GRABBED Then
                Sfx.Play 1, 300, -16, 13, 30, 0
                Message "HUMANOID TAKEN!", 25
            ElseIf (ev And 15) = EV_TAKEN Then
                Sfx.Play 1, 60, 8, 15, 40, 0
                HumansGone
            ElseIf (ev And 15) = EV_LASERED Then
                l_idx = Cpc.Shr(ev, 4) And 3
                llife(l_idx) = 0
                Cpc.Hide SLOT_LASER0 + l_idx
                KillEnemy(i)
            ElseIf (ev And 15) = EV_RAMMED Then
                KillEnemy(i)
                LoseLife()
                Exit Sub
            End If
            If ev And EV_SHOOT Then EnemyShot i, es_x
        End If
        i = i + 1                                              ' <--- NUEVO
    Loop                                                       ' <--- MODIFICADO (antes era Next i)
End Sub

' EnemyStep(i): el fotograma del enemigo i (tal como lo tenía Enemies en BASIC: en el Main.bas de la edición
' ZX Spectrum Next, que es donde se puede consultar la lógica): se desplaza según su estado (merodeando, bajando por un
' humano, llevándose a uno hacia arriba, mutante, baiter), contenido dentro del área de juego, dibujado con su punto de radar
' (cp_showr), luego se comprueba contra los láseres y la nave, y si dispara. Devuelve
' EV_... (0 = nada pendiente), es_x = posición horizontal en la vista
Function EnemyStep(ByVal n As Integer) As Integer   ' <--- MODIFICADO (n: {i} más abajo es la i del juego)
    ' LENGUAJE ENSAMBLADOR INTEGRADO ELIMINADO
End Function

' HumanStep(i): el fotograma del humano i, tal como lo tenía Humans en BASIC: caminando por el suelo, llevado
' por la nave (depositado al tocar el suelo), cayendo (atrapado por la nave, o aterrizando; desde
' demasiada altura, muere); dibujado con su punto de radar. Devuelve EV_... (0 = nada pendiente)
Function HumanStep(ByVal n As Integer) As Integer   ' <--- MODIFICADO (n: {i} más abajo es la i del juego)
    ' LENGUAJE ENSAMBLADOR INTEGRADO ELIMINADO
End Function


' un disparo del enemigo n en x de pantalla esx, dirigido a la nave
Sub EnemyShot(ByVal n As Integer, ByVal esx As Integer)
    Dim dx As Integer, dy As Integer, m As Integer
    For k = 0 To MAX_SHOTS - 1
        If slife(k) = 0 Then
            dx = psx - esx : dy = py - ey(n)
            m = Abs(dx)
            If Abs(dy) > m Then m = Abs(dy)
            If m < 1 Then m = 1
            sx(k) = ex(n) * 4 : sy(k) = ey(n) * 4
            svx(k) = dx * 24 \ m : svy(k) = dy * 24 \ m
            slife(k) = 30
            Sfx.Play 2, 70, -3, 8, 6, 0
            Exit Sub
        End If
    Next k
End Sub

' Disparos: los disparos enemigos avanzan y se dibujan (Z80); uno que impacte a la nave acaba con ella
Sub Shots()
    If ShotsStep() Then LoseLife()
End Sub

' ShotsStep(): fotograma de los disparos: True cuando uno alcanza la nave (queda destruida)
Function ShotsStep() As Integer
    ' LENGUAJE ENSAMBLADOR INTEGRADO ELIMINADO
End Function

' Booms: fotogramas de las explosiones (Z80)
Sub Booms()
    ' LENGUAJE ENSAMBLADOR INTEGRADO ELIMINADO
End Sub

' marcas del escáner que indican la porción del mundo mostrada en pantalla (justo fuera del recuadro,
' arriba y abajo): Cpc.Draw las sitúa en m y 40 píxeles de radar más adelante
Sub ViewMarks()
    Dim m As Integer
    m = WorldToRadarX(cam_x)
    If m >= 62 And m + 41 <= 194 Then Cpc.Mark m Else Cpc.Mark -1
End Sub

' ============================================================================
' ENTRADA PRINCIPAL DEL PROGRAMA
' ============================================================================

Mode 0
CpcData.Data                      ' (las tablas: nunca se ejecuta, las mantiene en el programa)
Cpc.Init
Sfx.Init
Border 0
Pen 15
ClsAll
MakeLand
Heights

Do
    TitleScreen

    score = 0 : nextBonus = 10000
    lives = 3
    bombs = 3
    wave = 1
    game_over = False
    InitWave()

    Do While Not game_over
        Vsync
        frames = frames + 1
        f2 = frames And 1 : f3 = f3 + 1                       ' <--- MODIFICADO (antes era f3 = frames Mod 3: una división)
        If f3 = 3 Then f3 = 0                                  ' <--- NUEVO (frames Mod 3 como contador)
        f4 = Cpc.Shr(frames, 2) And 1 : f12 = Cpc.Div(frames, 6) And 1
        If flash_bg > 0 Then
            flash_bg = flash_bg - 1
            If flash_bg = 0 Then
                Border 0
                Cpc.Ink 0, 0
            End If
        End If
        If msgT > 0 Then
            msgT = msgT - 1
            If msgT = 0 Then
                At 17 - Cpc.Shr(Len(msg), 1), 11 : PrintS Space$(Len(msg))   ' <--- MODIFICADO (solo donde está el mensaje: antes era At 1, 11 : PrintS Space$(32), 64 letras dibujadas)
            ElseIf Cpc.Touched() Then
                Pen 14 : At 17 - Cpc.Shr(Len(msg), 1), 11 : PrintS msg   ' <--- MODIFICADO (un sprite se llevó parte del mensaje por delante: de nuevo; antes era Message msg, msgT: dos copias de la cadena)
            End If
        End If

        Controls
        DrawPlayer
        Lasers
        Humans
        Enemies
        If Not game_over Then Shots
        Booms
        Terrain cam_x
        ViewMarks
        If hudDirty Then UpdateHUD()

        ' oleada completada: bonificación por cada humano restante
        If enemies_alive <= 0 And enemies_to_spawn <= 0 And Not game_over Then
            WaveDone
        End If
    Loop

    ' Pantalla de fin de juego (Game Over)
    Cpc.HideAll
    ClsAll
    Cpc.Ground 0
    Pen 10
    At 8, 9
    PrintS "G A M E   O V E R"
    Pen 15
    At 8, 12
    PrintS "FINAL SCORE: " & Mid$(Str$(score), 2)
    Pen 14
    At 8, 15
    PrintS "REACHED WAVE: " & Mid$(Str$(wave), 2)
    For i = 1 To 75
        Vsync
    Next i
Loop

Sub WaveDone()
    Dim b As Integer, f As Integer
    For i = 0 To MAX_ENEMIES - 1
        If estate(i) = ST_BAITER Then estate(i) = ST_DEAD
    Next i
    For i = 0 To MAX_BOOMS - 1
        bt(i) = 0
    Next i
    Cpc.HideAll                ' (el texto va donde estaban los sprites)
    b = humans_alive * 100 * wave
    If b > 5000 Then b = 5000
    Pen 14
    At 9, 10 : PrintS "ATTACK WAVE" & Str$(wave)
    At 11, 11 : PrintS "COMPLETED"
    Pen 15
    At 9, 13 : PrintS "BONUS" & Str$(b)
    For f = 0 To 5
        Sfx.Play f Mod 3, 120 - f * 12, 0, 13, 12, 0
        For i = 1 To 5
            Vsync
        Next i
    Next f
    AddScore b
    For i = 1 To 30
        Vsync
    Next i
    wave = wave + 1
    If bombs < 5 Then bombs = bombs + 1
    InitWave()
End Sub

Sub TitleScreen()
    Dim t As Integer, a As Integer
    Cpc.HideAll
    ClsAll
    Stars
    Cpc.Ground 2
    Cpc.Big 16, 6, "INVASION"   ' <--- MODIFICADO (antes era DEFENDER)
    Pen 11
    At 7, 6
    PrintS "AMSTRAD CPC EDITION"
    Pen 15
    At 9, 9 : PrintS "LANDER      150"
    At 9, 11 : PrintS "MUTANT      150"
    At 9, 13 : PrintS "BAITER      200"
    At 9, 15 : PrintS "HUMANOID    500"
    Pen 12
    At 3, 17 : PrintS "Q / A or UP/DN : ALTITUDE"
    At 3, 18 : PrintS "O / P or LT/RT : THRUST"
    At 3, 19 : PrintS "SPACE / FIRE 1 : LASER"
    At 3, 20 : PrintS "B / M / FIRE 2 : SMART BOMB"
    At 3, 21 : PrintS "H              : HYPERSPACE"
    Pen 11                                         ' <--- NUEVO
    At 1, 22 : PrintS "(C) 2026 CYBORG UNICORN PTY LTD"   ' <--- NUEVO
    t = 0
    Do
        Vsync
        t = t + 1
        a = Cpc.Shr(t, 2) And 1
        Cpc.Spr 1, 70, 195 - 9 * 8, PAT_LANDER + a * PAT_FRAME2, False, True
        Cpc.Spr 2, 70, 195 - 11 * 8, PAT_MUTANT + a * PAT_FRAME2, False, True
        Cpc.Spr 3, 70, 195 - 13 * 8, PAT_BAITER + a, False, True
        Cpc.Spr 4, 70, 195 - 15 * 8, PAT_HUMAN + a * PAT_FRAME2, False, True
        ' la nave vuela a lo largo de la parte inferior
        Cpc.Spr 0, (t * 4) Mod 360 - 20, 6, PAT_PLAYER + a * PAT_FRAME2, False, True
        Pen 10
        At 7, 23
        If Cpc.Div(t, 12) And 1 Then PrintS "PRESS SPACE OR FIRE" Else PrintS "                   "
        k = Rnd(10)
    Loop Until Cpc.Keys() And 16
    Do
        Vsync
    Loop While Cpc.Keys() And 16
    Cpc.HideAll
End Sub

' ============================================================================
' SENTENCIAS AL ESTILO CPC DEL ORIGINAL
' ============================================================================

' Pen n: color para el texto siguiente (los números de pluma originales como plumas de Cpc.cls)
Sub Pen(ByVal n As Integer)
    Select Case n
        Case 14: txtPen = 6
        Case 11: txtPen = 3
        Case 10: txtPen = 8
        Case 12: txtPen = 10
        Case 13: txtPen = 14
        Case Else: txtPen = 1
    End Select
End Sub

' Border n: colores de borde originales (0 negro, 2 rojo, 7 blanco)
Sub Border(ByVal n As Integer)
    Select Case n
        Case 2: Cpc.Ink 16, 6
        Case 7: Cpc.Ink 16, 26
        Case Else: Cpc.Ink 16, 0
    End Select
End Sub

' At x, y: dónde se coloca el texto (1, 1 = esquina superior izquierda de la imagen de 32 x 24 del Next: columna 4 de
' las 40 del CPC, fila 0 de sus 25)
Sub At(ByVal x As Integer, ByVal y As Integer)
    atx = x + 3 : aty = y - 1
End Sub

Sub PrintS(ByVal t As String)
    Cpc.Text atx, aty, t, txtPen
    atx = atx + Len(t)
End Sub

Sub ClsAll()
    Cpc.Cls
End Sub

' Vsync: el fotograma dibujado fuera de pantalla, mostrado durante el refresco vertical; el sonido sigue activo
Sub Vsync()
    Cpc.Draw cam_x
    Cpc.Flip
    Sfx.Update
    Sfx.Update                     ' (dos veces: un fotograma aquí equivale a dos del Next)
End Sub

' El terreno: Cpc.Draw lo dibuja a partir de las alturas (Heights); solo la destrucción del planeta las altera
Sub Terrain(ByVal cx As Integer)
    If terDead <> planet_dead Then Heights : terDead = planet_dead
End Sub

' la línea de suelo para cada x del mundo (de arriba a abajo), con pendiente entre los pasos de 4 píxeles de
' GetTerrainY
Sub Heights()
    Dim wx As Integer, h0 As Integer, h1 As Integer
    For wx = 0 To WORLD_MASK Step 2
        h0 = GetTerrainY(wx) : h1 = GetTerrainY((wx + 4) And WORLD_MASK)
        Cpc.Height wx, 191 - (h0 + (h1 - h0) * (wx And 3) \ 4)
    Next wx
    terDead = planet_dead
End Sub
1 me gusta

cpc.cls

' Cpc - Pantalla de INVASION en el Amstrad CPC: modo 0 (160 x 200, 16 tintas), dos pantallas (#C000
' y #0000-#3FFF: el programa se compila a partir de #4000), sprites por software que sustituyen a los
' de hardware del Next, y la aritmética rápida que el bucle de juego necesita.
' (c) 2026 Cyborg Unicorn Pty Ltd
'
' En cada fotograma el juego define sus sprites (Spr: una ranura, su posición, su patrón; al igual que las
' ranuras del Next, permanecen hasta que cambian; Hide), los puntos del radar (RDot, Dot), las marcas de encuadre (Mark) y
' cualquier texto (Text: directo a ambas pantallas); luego Draw dibuja el fotograma en la pantalla no
' visible: el suelo (solo cuando la vista se mueve: los bytes donde estuvo la última vez se rellenan de negro, y luego uno   ' <--- CHANGED
' o dos píxeles por columna), las estrellas (las viejas se apagan, las nuevas se encienden), las líneas que un sprite ha   ' <--- CHANGED
' tapado de negro, y después los sprites (código compilado). Flip la muestra en el segundo retorno de cuadro (flyback) desde   ' <--- CHANGED
' el último volteo: 25 fotogramas por segundo, estables, sin importar lo que el fotograma tarde por debajo de 40 ms (cp_tick cuenta   ' <--- CHANGED
' el tiempo), y prepara la otra pantalla: los sprites de hace dos fotogramas se cubren de negro, su   ' <--- CHANGED
' radar se limpia. No se guarda nada debajo de un sprite: todo lo que uno puede cubrir se dibuja de nuevo,   ' <--- CHANGED
' excepto los textos (Touched indica cuándo se alcanzó la fila de mensajes y qué letras).   ' <--- CHANGED
' Las coordenadas de los sprites son las del Next (x de 0-319 a lo ancho de la vista, y hacia arriba desde la base como en
' el Plot original); un patrón de 16 x 16 pasa a ser de 8 x 16 píxeles en modo 0 (CpcData.cls).
' Textos: 40 columnas de 4 píxeles (la tipografía estrecha), 25 filas de 8 líneas. Las llamadas a píxeles (Dot, HLine,
' Big) usan píxeles de modo 0, 0-159 en horizontal, líneas 0-199 en vertical.
' Todo está en Z80 (#asm); el bloque de Draw contiene las tablas y las rutinas que los demás llaman, por lo
' que un programa debe usar Draw (todos lo hacen).
' Interrupciones: IM 2 (una tabla de 257 bytes: cualquier valor del bus sirve), activas solo mientras el Z80 de Cpc, Sfx y el juego se ejecuta en la   ' <--- NEW
' pila real; el hilo de BASIC (SP es el hilo) se ejecuta con ellas desactivadas, como antes.   ' <--- NEW
' Memoria más allá del programa (no en el archivo): tablas de Text, tablas de líneas, listas del suelo y de   ' <--- NEW
' las estrellas (desde ntc_free hacia arriba, hasta aprox. #AD00: la RAM del firmware no se usa una vez que ha iniciado).   ' <--- NEW
Option Explicit

Public Const Slots = 33

' Init: las 16 tintas y el borde, las tablas de líneas, la pantalla en #C000 visible (la otra es
' #0000-#3FFF: el programa se compila a partir de #4000), nada dibujado aún. Después de Mode 0
Public Sub Init()
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Ink pen, ink: tinta CPC (0-26) para una pluma (0-15, 16 = el borde)
Public Sub Ink(ByVal pen As Integer, ByVal ink As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Spr slot, x, y, pattern, mirror, shown: la ranura 0-32 muestra el patrón (0-15) con su centro en x
' (0-319 a lo ancho de la vista), y (hacia arriba desde la base, 0-191), reflejado si mirror; u oculto
Public Sub Spr(ByVal slot As Integer, ByVal x As Integer, ByVal y As Integer, ByVal pat As Integer, ByVal mirror As Integer, ByVal shown As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Hide slot: oculta la ranura
Public Sub Hide(ByVal slot As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' HideAll: oculta todas las ranuras
Public Sub HideAll()
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Text col, row, s, pen: s desde la columna col (0-39), fila (0-24) con la pluma pen (fondo negro), en ambas
' pantallas. Letras 32-95 (las minúsculas se muestran como mayúsculas). Un sprite sobre los textos se los
' lleva consigo al irse (Flip rellena de negro donde estaba): Touched indica si eso ha ocurrido en
' la fila de mensajes
Public Sub Text(ByVal col As Integer, ByVal row As Integer, ByVal s As String, ByVal pen As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Touched(): True una vez cuando un sprite ha sido borrado en negro de la fila de mensajes (líneas 80-87)
' en cualquiera de las pantallas desde la última llamada: el mensaje necesita reimprimirse (y el Text que
' lo imprime dibuja entonces solo las letras que fueron cubiertas de negro, en cada pantalla)   ' <--- CHANGED
Public Function Touched() As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Dot x, y, pen: un punto de radar (píxel x 0-159, línea y 0-199) en la pantalla sobre la que se dibuja (el
' interior del radar se limpia en cada Flip)
Public Sub Dot(ByVal x As Integer, ByVal y As Integer, ByVal pen As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' RDot wx, px, y, pen: el punto de radar para x del mundo wx (la nave en px está en el centro de
' la caja), y (hacia arriba desde la base): en el píxel 32 + ((wx - px + 512) And 1023) \ 16, línea 38 - y \ 7
Public Sub RDot(ByVal wx As Integer, ByVal px As Integer, ByVal y As Integer, ByVal pen As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Show(slot, wx, y, pattern, mirror, cx, px, pen): una entidad del mundo en wx, y (hacia arriba desde la
' base) de una sola vez: su sprite en la ranura (visible según su posición a lo ancho de la vista desde cx cuando esta es
' de -8 a 327, oculto de lo contrario), su punto de radar en pen (ninguno si pen es -1), y devuelve su posición
' a lo ancho de la vista (-512 a 511). Una sola palabra clave para lo que tomaba diez. (Z80 en el juego
' llena cp_shb y llama a cp_showr: lo mismo, sin palabra clave)
Public Function Show(ByVal slot As Integer, ByVal wx As Integer, ByVal y As Integer, ByVal pat As Integer, ByVal mirror As Integer, ByVal cx As Integer, ByVal px As Integer, ByVal pen As Integer) As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' ScrX(wx, cx): posición del mundo x wx a lo ancho de una vista desde cx: -512 a 511
Public Function ScrX(ByVal wx As Integer, ByVal cx As Integer) As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Mark x: las marcas de encuadre en el radar en x (las del Next: 62-153), en la pantalla sobre la que se dibuja: un byte
' (dos píxeles blancos) en x / 4 y 10 bytes más adelante, líneas 7 y 41 (se eliminan en el siguiente Flip, como
' ocurre con los puntos). -1 = ninguna
Public Sub Mark(ByVal x As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Star k, x, y, pen: estrella k (0-31) en x (0-319 en el cielo), línea y, con tinta pen. (Guardado como x, el
' desplazamiento de la línea en una pantalla y los bits de píxel izquierdos de la tinta: nada que buscar cuando se dibuja)
Public Sub Star(ByVal k As Integer, ByVal x As Integer, ByVal y As Integer, ByVal pen As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Height wx, y: la línea del suelo (0-199) en x del mundo wx (0-1023, pares: los impares no se conservan)
Public Sub Height(ByVal wx As Integer, ByVal y As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' GroundAt(wx): la altura del suelo (hacia arriba desde la base) en x del mundo wx, a partir de la tabla de alturas
' (Height), en 100 ciclos; el GetTerrainY del juego tomaba 500
Public Function GroundAt(ByVal wx As Integer) As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Ground on: lo que Draw dibuja además de los sprites: 1 el suelo, 2 las estrellas, 4 el radar (su
' interior se limpia en cada Flip, sus líneas se redibujan). Sumados: 7 durante la partida, 2 en el título, 0 en la
' pantalla de fin de partida
Public Sub Ground(ByVal on As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Cls: ambas pantallas a negro, nada se recuerda en ninguna
Public Sub Cls()
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' HLine x, y, w, pen: una línea horizontal desde el píxel x, línea y, de w píxeles de ancho, en ambas pantallas
Public Sub HLine(ByVal x As Integer, ByVal y As Integer, ByVal w As Integer, ByVal pen As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Big x, y, s: s en los caracteres de 8 x 8 propios del CPC, a razón de dos píxeles de ancho y tres líneas hacia abajo por cada
' uno de ellos (16 x 24), sombreado de amarillo pastel a rojo con una sombra roja oscura, desde el píxel x (par),
' línea y, en ambas pantallas
Public Sub Big(ByVal x As Integer, ByVal y As Integer, ByVal s As String)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Flip: espera al segundo retorno de cuadro (flyback) desde el último volteo (el siguiente si ese ya ha
' pasado), muestra la pantalla sobre la que dibujó Draw y dibuja en la otra a partir de ahora   ' <--- CHANGED
Public Sub Flip()
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Shr(x, n): x (0-65535, como sin signo) desplazado a la derecha n bits: x \ 2, 4, 8... sin la división
' (que tarda unos 1.100 ciclos; esto tarda 100)
Public Function Shr(ByVal x As Integer, ByVal n As Integer) As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Div(x, d): x \ d para x de 0-65535 (sin signo), d 1-255: unos 400 ciclos (la división tarda 1.100),
' 250 para x menor que 256
Public Function Div(ByVal x As Integer, ByVal d As Integer) As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Rnd(n): un número aleatorio de 0 a n - 1 (n 1-255), en unos 350 ciclos (el Rnd de NTC tarda 1.200)
Public Function Rnd(ByVal n As Integer) As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Keys(): las teclas presionadas: 1 arriba (Q, joystick arriba), 2 abajo (A), 4 izquierda (O), 8 derecha (P), 16 disparo
' (espacio, Z, disparo 1), 32 bomba inteligente (B, M, disparo 2), 64 hiperespacio (H)
Public Function Keys() As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Draw cx: el fotograma en la pantalla sobre la que se dibuja (la que no está visible: Flip eliminó sus sprites
' anteriores y limpió su radar), la vista desde x del mundo cx: el suelo y las estrellas
' (Ground), los sprites. Luego Flip
Public Sub Draw(ByVal cx As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

sfx.cls

' Sfx - efectos de sonido en el chip AY del CPC, a través del PPI (el puerto A #F4xx lleva los datos,
' el puerto C #F6xx los bits de control del AY). Tres canales, cada uno reproduciendo un efecto a la vez:
' un tono o ruido cuyo tono (pitch) se desliza y cuyo volumen se desvanece a cero a lo largo de su duración.
' (c) 2026 Cyborg Unicorn Pty Ltd
' Llama a Update una vez cada 50.º de segundo; aquí Vsync lo llama dos veces por fotograma, y el AY se escribe en
' el segundo de cada dos (las escrituras del primero se sobrescribían de inmediato)   ' <--- MODIFICADO
'   Sfx.Play 0, 200, 8, 15, 12, 0     ' canal A: período de tono 200, tono ascendente... 12 fotogramas
Option Explicit

Private per(2) As Integer         ' el período del tono actual (12 bits; ruido: 0-31)
Private slide(2) As Integer       ' añadido a este en cada fotograma
Private vol(2) As Integer         ' el volumen en el que inicia (0-15)
Private total(2) As Integer       ' su duración en fotogramas
Private left(2) As Integer        ' fotogramas restantes (0 = silencio)
Private nse(2) As Integer         ' 1 = ruido, 0 = tono

Private Sub W(ByVal r As Integer, ByVal v As Integer)
    ' LENGUAJE ENSAMBLADOR INTEGRADO (INLINE ASSEMBLY) ELIMINADO
End Sub

' Init: todos los canales en silencio
Public Sub Init()
  Dim i As Integer
  For i = 0 To 13
    W i, 0
  Next
  W 7, &H3F
  For i = 0 To 2
    left(i) = 0
  Next
End Sub

' Play ch, period, slidePerFrame, volume, frames, noise: inicia un efecto en el canal ch (0-2)
Public Sub Play(ByVal ch As Integer, ByVal period As Integer, ByVal sl As Integer, ByVal volume As Integer, ByVal frames As Integer, ByVal noise As Integer)
  If ch < 0 Or ch > 2 Then Exit Sub
  per(ch) = period : slide(ch) = sl : vol(ch) = volume : total(ch) = frames : left(ch) = frames : nse(ch) = noise
End Sub

' Busy(ch): True mientras el canal ch esté reproduciendo
Public Function Busy(ByVal ch As Integer) As Integer
  Busy = (left(ch) > 0)
End Function

' Update: una vez por fotograma - los tonos se deslizan, los volúmenes se desvanecen, el AY se escribe (Z80: se llama
' dos veces por fotograma)
Public Sub Update()
    ' LENGUAJE ENSAMBLADOR INTEGRADO (INLINE ASSEMBLY) ELIMINADO
End Sub

' Quiet: todos los canales detenidos
Public Sub Quiet()
  Dim i As Integer
  For i = 0 To 2
    left(i) = 0
    W 8 + i, 0
  Next
End Sub

y finalmente cpcdata.cls

' CpcData - sprites y fuente estrecha de INVASION para el Amstrad CPC, modo 0 (generado por
' tools/cpcgfx.py a partir de INVASION.SPR: no editar). Los sprites están compilados: cada uno de los 16
' patrones, normal y reflejado (solo la nave, los humanos y el rayo tienen versión reflejada:
' las entradas de los demás apuntan al normal), como código Z80 que lo dibuja en HL (su byte superior
' izquierdo) - un byte completo escrito, medio enmascarado, uno vacío omitido - de una línea a la
' siguiente mediante #800 (cp_nextw más allá del octavo de una fila: Cpc.cls). cd_spr es una tabla de 32 entradas (patrón * 2 + reflejo): xoff,
' yoff, w (bytes), h, la dirección del código. Los sprites se dibujan en píxeles pares (en un límite de byte).
' cd_font: 64 caracteres (32-95) de 8 filas, una fila de 4 píxeles en los bits 7-4. cd_inks: los números de tinta
' CPC de las 16 plumas.
' (c) 2026 Cyborg Unicorn Pty Ltd
Option Explicit

' Data: nunca se llama - contiene las tablas y el código de los sprites (etiquetas globales, utilizadas por Cpc.cls)
Public Sub Data()
' LENGUAJE ENSAMBLADOR INTEGRADO (INLINE ASSEMBLY) ELIMINADO
End Sub

¿A qué se ensambla el código anterior? Aquí hay un fragmento.

; ============================================================
; NTC program - generated by NTC BASIC (VB style BASIC to NTC)
; platform: cpc (Amstrad CPC - direct hardware, no firmware), starts in turbo mode
; ============================================================

			DEVICE AMSTRADCPC6128		; for SAVEAMSDOS at the end
			org #4000

NTC_TERM	equ 0		; Option Terminal: 0 none, 1 VT52, 2 ANSI (CP/M)
NTC_STRMAX	equ 40		; the longest string (library buffers, temps)
NTC_MEMBANKS	equ 64		; Option Memory: the most 64K banks the Memory class looks for
NTC_ROMENABLE	equ 0		; Option RomEnable (cpcfw, cpcmin): the start code starts the ROMs
NTC_ROMLIB	equ 0		; 1 = the library is the keyword ROM (Library: ROM; ntcRomAsm builds it)
NTC_WINHOLE	equ 0		; 1 = the program is built round an Option Reserve range over the window #4000-#7FFF
NTC_BUFSIZE	equ 2048		; the library's buffer (ntc_buf: FileSystem, Memory.Copy): Option Buffer
NTC_CPMGFX	equ 0		; 1 = CP/M: Graphics draws on an Amstrad CPC's screen (2.2: #C000, Plus: block 1 from common memory)

ntc_start:
			di
			ld (ntc_csp), sp
			ld sp, main_thread
			ret

; ============================================================
; Main Thread
; ============================================================

main_thread:
			dw kw_bssinit, ntc_bss, ntc_bss_size		; the arrays: do they fit, clear them

			; Main.bas 1975: Mode 0
			dw kw_model, 0

			; Main.bas 1976: CpcData.Data                      ' (the tables: never runs, keeps them in the program)
			dw fn_cpcdata_data

			; Main.bas 1977: Cpc.Init
			dw fn_cpc_init

			; Main.bas 1978: Sfx.Init
			dw p_sfx_init_c

			; Main.bas 1979: Border 0
			dw p_border_c, lit_0

			; Main.bas 1980: Pen 15
			dw p_pen_c, lit_15

			; Main.bas 1981: ClsAll
			dw p_clsall_c

			; Main.bas 1982: MakeLand
			dw p_makeland_c

			; Main.bas 1983: Heights
			dw p_heights_c

			; Main.bas 1985: Do
do1_t:

			; Main.bas 1986: TitleScreen
			dw p_titlescreen_c

			; Main.bas 1988: score = 0 : nextBonus = 10000
			dw kw_l_mov, llit_0, var_score
			dw kw_l_mov, llit_10000, var_nextbonus

			; Main.bas 1989: lives = 3
			dw kw_ltov, 3, var_lives

			; Main.bas 1990: bombs = 3
			dw kw_ltov, 3, var_bombs

			; Main.bas 1991: wave = 1
			dw kw_ltov, 1, var_wave

			; Main.bas 1992: game_over = False
			dw kw_ztov, var_game_over

			; Main.bas 1993: InitWave()
			dw p_initwave_c

			; Main.bas 1995: Do While Not game_over
do2_t:
			dw kw_notv, var_game_over, tmp0
			dw kw_goto_ifzero, tmp0, do2_e

			; Main.bas 1996: Vsync
			dw p_vsync_c

			; Main.bas 1997: frames = frames + 1
			dw kw_inc, var_frames

			; Main.bas 1998: f2 = frames And 1 : f3 = f3 + 1                       ' <--- CHANGED (was f3 = frames Mod 3: a division)
			dw kw_andvl, var_frames, 1, var_f2
			dw kw_inc, var_f3

			; Main.bas 1999: If f3 = 3 Then f3 = 0                                  ' <--- NEW (frames Mod 3 as a counter)
			dw kw_cpeqvl, var_f3, 3
			dw kw_goto_iffalse, if3_1
			dw kw_ztov, var_f3
if3_1:

			; Main.bas 2000: f4 = Cpc.Shr(frames, 2) And 1 : f12 = Cpc.Div(frames, 6) And 1
			dw fn_cpc_shr, var_frames, lit_2, var_cpc_shr_shr
			dw kw_vtov, var_cpc_shr_shr, tmp0
			dw kw_andvl, tmp0, 1, var_f4
			dw fn_cpc_div, var_frames, lit_6, var_cpc_div_div
			dw kw_vtov, var_cpc_div_div, tmp0
			dw kw_andvl, tmp0, 1, var_f12

			; Main.bas 2001: If flash_bg > 0 Then
			dw kw_scpgtvl, var_flash_bg, 0
			dw kw_goto_iffalse, if4_1

			; Main.bas 2002: flash_bg = flash_bg - 1
			dw kw_dec, var_flash_bg

			; Main.bas 2003: If flash_bg = 0 Then
			dw kw_cpeqvl, var_flash_bg, 0
			dw kw_goto_iffalse, if5_1

			; Main.bas 2004: Border 0
			dw p_border_c, lit_0

			; Main.bas 2005: Cpc.Ink 0, 0
			dw fn_cpc_ink, lit_0, lit_0

			; Main.bas 2006: End If
if5_1:

			; Main.bas 2007: End If
if4_1:

			; Main.bas 2008: If msgT > 0 Then
			dw kw_scpgtvl, var_msgt, 0
			dw kw_goto_iffalse, if6_1

			; Main.bas 2009: msgT = msgT - 1
			dw kw_dec, var_msgt

			; Main.bas 2010: If msgT = 0 Then
			dw kw_cpeqvl, var_msgt, 0
			dw kw_goto_iffalse, if7_1

			; Main.bas 2011: At 17 - Cpc.Shr(Len(msg), 1), 11 : PrintS Space$(Len(msg))   ' <--- CHANGED (only where the message is: was At 1, 11 : PrintS Space$(32), 64 letters drawn)
			dw kw_strlen, var_msg, tmp1
			dw fn_cpc_shr, tmp1, lit_1, var_cpc_shr_shr
			dw kw_vtov, var_cpc_shr_shr, tmp1
			dw kw_sublv, 17, tmp1, tmp0
			dw p_at_c, tmp0, lit_11
			dw kw_strlen, var_msg, tmp0
			dw kw_strspace, stmp0, tmp0
			dw p_prints_c, stmp0

			; Main.bas 2012: ElseIf Cpc.Touched() Then
			dw if7_e_go
if7_1:
			dw fn_cpc_touched, var_cpc_touched_touched
			dw kw_goto_ifzero, var_cpc_touched_touched, if7_2

			; Main.bas 2013: Pen 14 : At 17 - Cpc.Shr(Len(msg), 1), 11 : PrintS msg   ' <--- CHANGED (a sprite took some of it with it: again; was Message msg, msgT: two copies of the string)
			dw p_pen_c, lit_14
			dw kw_strlen, var_msg, tmp1
			dw fn_cpc_shr, tmp1, lit_1, var_cpc_shr_shr
			dw kw_vtov, var_cpc_shr_shr, tmp1
			dw kw_sublv, 17, tmp1, tmp0
			dw p_at_c, tmp0, lit_11
			dw p_prints_c, var_msg

			; Main.bas 2014: End If
if7_2:
if7_e:

			; Main.bas 2015: End If
if6_1:

			; Main.bas 2017: Controls
			dw p_controls_c

			; Main.bas 2018: DrawPlayer
			dw p_drawplayer_c

			; Main.bas 2019: Lasers
			dw fn_lasers

			; Main.bas 2020: Humans
			dw p_humans_c

			; Main.bas 2021: Enemies
			dw p_enemies_c

			; Main.bas 2022: If Not game_over Then Shots
			dw kw_notv, var_game_over, tmp0
			dw kw_goto_ifzero, tmp0, if8_1
			dw p_shots_c
if8_1:

			; Main.bas 2023: Booms
			dw fn_booms

			; Main.bas 2024: Terrain cam_x
			dw p_terrain_c, var_cam_x

			; Main.bas 2025: ViewMarks
			dw p_viewmarks_c

			; Main.bas 2026: If hudDirty Then UpdateHUD()
			dw kw_goto_ifzero, var_huddirty, if9_1
			dw p_updatehud_c
if9_1:

			; Main.bas 2029: If enemies_alive <= 0 And enemies_to_spawn <= 0 And Not game_over Then
			dw kw_scplevl, var_enemies_alive, 0
			dw kw_ftov, tmp0
			dw kw_scplevl, var_enemies_to_spawn, 0
			dw kw_ftov, tmp1
			dw kw_andvv, tmp0, tmp1, tmp0
			dw kw_notv, var_game_over, tmp1
			dw kw_andvv, tmp0, tmp1, tmp0
			dw kw_goto_ifzero, tmp0, do2_t

			; Main.bas 2030: WaveDone
			dw p_wavedone_c

			; Main.bas 2031: End If
if10_1:

			; Main.bas 2032: Loop
			dw do2_t_go
do2_e:

			; Main.bas 2035: Cpc.HideAll
			dw fn_cpc_hideall

			; Main.bas 2036: ClsAll
			dw p_clsall_c

			; Main.bas 2037: Cpc.Ground 0
			dw fn_cpc_ground, lit_0

			; Main.bas 2038: Pen 10
			dw p_pen_c, lit_10

			; Main.bas 2039: At 8, 9
			dw p_at_c, lit_8, lit_9

			; Main.bas 2040: PrintS "G A M E   O V E R"
			dw p_prints_c, str0

			; Main.bas 2041: Pen 15
			dw p_pen_c, lit_15

			; Main.bas 2042: At 8, 12
			dw p_at_c, lit_8, lit_12

			; Main.bas 2043: PrintS "FINAL SCORE: " & Mid$(Str$(score), 2)
			dw kw_l_str, stmp1, var_score
			dw kw_strmid, stmp1, stmp1, lit_2, lit_255
			dw kw_strcpy, stmp0, str1
			dw kw_strcat, stmp0, stmp1
			dw p_prints_c, stmp0

			; Main.bas 2044: Pen 14
			dw p_pen_c, lit_14

			; Main.bas 2045: At 8, 15
			dw p_at_c, lit_8, lit_15

			; Main.bas 2046: PrintS "REACHED WAVE: " & Mid$(Str$(wave), 2)
			dw kw_strstr, stmp1, var_wave
			dw kw_strmid, stmp1, stmp1, lit_2, lit_255
			dw kw_strcpy, stmp0, str2
			dw kw_strcat, stmp0, stmp1
			dw p_prints_c, stmp0

			; Main.bas 2047: For i = 1 To 75
			dw kw_ltov, 1, var_i
for11_t:

			; Main.bas 2048: Vsync
			dw p_vsync_c

			; Main.bas 2049: Next i
			dw kw_next1l, var_i, 75, for11_t
for11_e:

			; Main.bas 2050: Loop
			dw do1_t_go
do1_e:
			dw kw_stop

; ============================================================
; sub makeland (turbo mode)
; ============================================================

p_makeland:

			; Main.bas 172: Dim h As Integer, t As Integer

			; Main.bas 173: rs = 12345 : h = 14 : t = 14
			dw kw_ltov, 12345, var_rs
			dw kw_ltov, 14, var_makeland_h
			dw kw_ltov, 14, var_makeland_t

			; Main.bas 174: For k = 0 To 255
			dw kw_ztov, var_k
for12_t:

			; Main.bas 175: If k Mod 16 = 0 Then t = 8 + Rand(23)
			dw kw_modvl, var_k, 16, tmp0
			dw kw_cpeqvl, tmp0, 0
			dw kw_goto_iffalse, if13_1
			dw p_rand_c, lit_23
			dw kw_vtov, var_rand_rand, tmp1
			dw kw_addl, tmp1, 8, var_makeland_t
if13_1:

			; Main.bas 176: If h < t Then h = h + 1 + Rand(3)
			dw kw_scpltvv, var_makeland_h, var_makeland_t
			dw kw_goto_iffalse, if14_1
			dw kw_addl, var_makeland_h, 1, tmp0
			dw p_rand_c, lit_3
			dw kw_vtov, var_rand_rand, tmp1
			dw kw_add, tmp0, tmp1, var_makeland_h
if14_1:

			; Main.bas 177: If h > t Then h = h - 1 - Rand(3)
			dw kw_scpgtvv, var_makeland_h, var_makeland_t
			dw kw_goto_iffalse, if15_1
			dw kw_addl, var_makeland_h, -1, tmp0
			dw p_rand_c, lit_3
			dw kw_vtov, var_rand_rand, tmp1
			dw kw_subvv, tmp0, tmp1, var_makeland_h
if15_1:

			; Main.bas 178: If h < 8 Then h = 8
			dw kw_scpltvl, var_makeland_h, 8
			dw kw_goto_iffalse, if16_1
			dw kw_ltov, 8, var_makeland_h
if16_1:

			; Main.bas 179: If h > 32 Then h = 32
			dw kw_scpgtvl, var_makeland_h, 32
			dw kw_goto_iffalse, if17_1
			dw kw_ltov, 32, var_makeland_h
if17_1:

			; Main.bas 180: land(k) = h
			dw kw_aset8, arr_land, var_k, var_makeland_h

			; Main.bas 181: Next k
			dw kw_next1l, var_k, 255, for12_t
for12_e:

			; Main.bas 183: For k = 248 To 255
			dw kw_ltov, 248, var_k
for18_t:

			; Main.bas 184: land(k) = land(247) + (land(0) - land(247)) * (k - 247) \ 9
			dw kw_lptrtov8, arr_land+247, tmp1
			dw kw_lptrtov8, arr_land, tmp2
			dw kw_lptrtov8, arr_land+247, tmp3
			dw kw_subvv, tmp2, tmp3, tmp2
			dw kw_addl, var_k, -247, tmp3
			dw kw_mulvv, tmp2, tmp3, tmp2
			dw kw_divvl, tmp2, 9, tmp2
			dw kw_add, tmp1, tmp2, tmp1
			dw kw_aset8, arr_land, var_k, tmp1

			; Main.bas 185: Next k
			dw kw_next1l, var_k, 255, for18_t
for18_e:
			dw p_makeland_r

; ============================================================
; function rand (turbo mode)
; ============================================================

p_rand:

			; Main.bas 191: rs = (rs * 2053 + 13849) And &H7FFF
			dw kw_mulvl, var_rs, 2053, tmp1
			dw kw_addl, tmp1, 13849, tmp1
			dw kw_andvl, tmp1, 32767, var_rs

			; Main.bas 192: Rand = (rs \ 32) Mod n
			dw kw_divvl, var_rs, 32, tmp1
			dw kw_modvv, tmp1, var_rand_n, var_rand_rand
			dw p_rand_r

; ============================================================
; function getterrainy (turbo mode)
; ============================================================

p_getterrainy:

			; Main.bas 196: If planet_dead Then GetTerrainY = 6 Else GetTerrainY = land(Cpc.Shr(wx, 2) And 255)
			dw kw_goto_ifzero, var_planet_dead, if19_1
			dw kw_ltov, 6, var_getterrainy_getterrainy
			dw if19_e_go
if19_1:
			dw fn_cpc_shr, var_getterrainy_wx, lit_2, var_cpc_shr_shr
			dw kw_vtov, var_cpc_shr_shr, tmp1
			dw kw_andvl, tmp1, 255, tmp1
			dw kw_aget8, arr_land, tmp1, var_getterrainy_getterrainy
if19_e:
			dw p_getterrainy_r

; ============================================================
; function worldtoradarx (turbo mode)
; ============================================================

p_worldtoradarx:

			; Main.bas 203: WorldToRadarX = 64 + Cpc.Shr((wx - px + 512) And WORLD_MASK, 3)
			dw kw_subvv, var_worldtoradarx_wx, var_px, tmp1
			dw kw_addl, tmp1, 512, tmp1
			dw kw_andvl, tmp1, 1023, tmp1
			dw fn_cpc_shr, tmp1, lit_3, var_cpc_shr_shr
			dw kw_vtov, var_cpc_shr_shr, tmp1
			dw kw_addl, tmp1, 64, var_worldtoradarx_worldtoradarx
			dw p_worldtoradarx_r

; ============================================================
; sub updatehud (turbo mode)
; ============================================================

p_updatehud:

			; Main.bas 208: If score <> hudScore Then                                  ' <--- NEW
			dw kw_l_cpne, var_score, var_hudscore
			dw kw_goto_iffalse, if20_1

			; Main.bas 209: Pen 15
			dw p_pen_c, lit_15

			; Main.bas 210: At 1, 1
			dw p_at_c, lit_1, lit_1

			; Main.bas 211: PrintS Right$("000000" & Mid$(Str$(score), 2), 7)
			dw kw_l_str, stmp1, var_score
			dw kw_strmid, stmp1, stmp1, lit_2, lit_255
			dw kw_strcpy, stmp0, str3
			dw kw_strcat, stmp0, stmp1
			dw kw_strright, stmp0, stmp0, lit_7
			dw p_prints_c, stmp0

			; Main.bas 212: hudScore = score                                           ' <--- NEW
			dw kw_l_mov, var_score, var_hudscore

			; Main.bas 213: End If                                                     ' <--- NEW
if20_1:

			; Main.bas 214: If lives <> hudLives Then                                  ' <--- NEW
			dw kw_cpnevv, var_lives, var_hudlives
			dw kw_goto_iffalse, if21_1

			; Main.bas 215: Pen 14
			dw p_pen_c, lit_14

			; Main.bas 216: At 10, 1
			dw p_at_c, lit_10, lit_1

			; Main.bas 217: PrintS "SHIPS" & Str$(lives) & " "
			dw kw_strstr, stmp1, var_lives
			dw kw_strcpy, stmp0, str4
			dw kw_strcat, stmp0, stmp1
			dw kw_strcat, stmp0, str5
			dw p_prints_c, stmp0

			; Main.bas 218: hudLives = lives                                           ' <--- NEW
			dw kw_vtov, var_lives, var_hudlives

			; Main.bas 219: End If                                                     ' <--- NEW
if21_1:

			; Main.bas 220: If bombs <> hudBombs Then                                  ' <--- NEW
			dw kw_cpnevv, var_bombs, var_hudbombs
			dw kw_goto_iffalse, if22_1

			; Main.bas 221: Pen 12
			dw p_pen_c, lit_12

			; Main.bas 222: At 19, 1
			dw p_at_c, lit_19, lit_1

			; Main.bas 223: PrintS "BOMBS" & Str$(bombs) & " "
			dw kw_strstr, stmp1, var_bombs
			dw kw_strcpy, stmp0, str6
			dw kw_strcat, stmp0, stmp1
			dw kw_strcat, stmp0, str5
			dw p_prints_c, stmp0

			; Main.bas 224: hudBombs = bombs                                           ' <--- NEW
			dw kw_vtov, var_bombs, var_hudbombs

			; Main.bas 225: End If                                                     ' <--- NEW
if22_1:

			; Main.bas 226: If wave <> hudWave Then                                    ' <--- NEW
			dw kw_cpnevv, var_wave, var_hudwave
			dw kw_goto_iffalse, if23_1

			; Main.bas 227: Pen 11
			dw p_pen_c, lit_11

			; Main.bas 228: At 27, 1
			dw p_at_c, lit_27, lit_1

			; Main.bas 229: PrintS "WAVE" & Str$(wave) & " "
			dw kw_strstr, stmp1, var_wave
			dw kw_strcpy, stmp0, str7
			dw kw_strcat, stmp0, stmp1
			dw kw_strcat, stmp0, str5
			dw p_prints_c, stmp0

			; Main.bas 230: hudWave = wave                                             ' <--- NEW
			dw kw_vtov, var_wave, var_hudwave

			; Main.bas 231: End If                                                     ' <--- NEW
if23_1:

			; Main.bas 232: hudDirty = 0
			dw kw_ztov, var_huddirty
			dw p_updatehud_r

; ============================================================
; sub drawstaticframe (turbo mode)
; ============================================================

p_drawstaticframe:

			; Main.bas 236: ClsAll
			dw p_clsall_c

			; Main.bas 237: hudScore = -1 : hudLives = -1 : hudBombs = -1 : hudWave = -1   ' <--- NEW (the screen is clear: all of the HUD printed)
			dw kw_l_mov, llit_m1, var_hudscore
			dw kw_ltov, -1, var_hudlives
			dw kw_ltov, -1, var_hudbombs
			dw kw_ltov, -1, var_hudwave

			; Main.bas 240: Cpc.HLine 0, 43, 160, PEN_DIVIDER
			dw fn_cpc_hline, lit_0, lit_43, lit_160, lit_13

			; Main.bas 241: Cpc.HLine 31, 9, 66, PEN_SCANNER
			dw fn_cpc_hline, lit_31, lit_9, lit_66, lit_10

			; Main.bas 242: Cpc.HLine 31, 39, 66, PEN_SCANNER
			dw fn_cpc_hline, lit_31, lit_39, lit_66, lit_10

			; Main.bas 243: For k = 10 To 38
			dw kw_ltov, 10, var_k
for24_t:

			; Main.bas 244: Cpc.HLine 31, k, 1, PEN_SCANNER : Cpc.HLine 96, k, 1, PEN_SCANNER
			dw fn_cpc_hline, lit_31, var_k, lit_1, lit_10
			dw fn_cpc_hline, lit_96, var_k, lit_1, lit_10

			; Main.bas 245: Next k
			dw kw_next1l, var_k, 38, for24_t
for24_e:

			; Main.bas 246: Stars
			dw p_stars_c

			; Main.bas 247: Cpc.Ground 7
			dw fn_cpc_ground, lit_7

			; Main.bas 248: UpdateHUD()
			dw p_updatehud_c
			dw p_drawstaticframe_r

; ============================================================
; sub stars (turbo mode)
; ============================================================

p_stars:

			; Main.bas 254: Dim x As Integer, y As Integer

			; Main.bas 255: rs = 777
			dw kw_ltov, 777, var_rs

			; Main.bas 256: For k = 0 To 31
			dw kw_ztov, var_k
for25_t:

			; Main.bas 257: x = Rand(320) : y = 46 + Rand(110)
			dw p_rand_c, lit_320
			dw kw_vtov, var_rand_rand, var_stars_x
			dw p_rand_c, lit_110
			dw kw_vtov, var_rand_rand, tmp1
			dw kw_addl, tmp1, 46, var_stars_y

			; Main.bas 258: Cpc.Star k, x, y, Val(Mid$("2211", (k Mod 4) + 1, 1))
			dw kw_modvl, var_k, 4, tmp4
			dw kw_inc, tmp4
			dw kw_strmid, stmp3, str8, tmp4, lit_1
			dw kw_strval, stmp3, tmp3
			dw fn_cpc_star, var_k, var_stars_x, var_stars_y, tmp3

			; Main.bas 259: Next k
			dw kw_next1l, var_k, 31, for25_t
for25_e:
			dw p_stars_r

; ============================================================
; sub initwave (turbo mode)
; ============================================================

p_initwave:

			; Main.bas 267: Cpc.HideAll
			dw fn_cpc_hideall

			; Main.bas 268: DrawStaticFrame()
			dw p_drawstaticframe_c

			; Main.bas 270: px = 512 : py = 96 : pvx = 0 : pvy = 0 : pdir = 1 : psx = 110
			dw kw_ltov, 512, var_px
			dw kw_ltov, 96, var_py
			dw kw_ztov, var_pvx
			dw kw_ztov, var_pvy
			dw kw_ltov, 1, var_pdir
			dw kw_ltov, 110, var_psx

			; Main.bas 271: safeT = 25
			dw kw_ltov, 25, var_safet

			; Main.bas 273: For i = 0 To MAX_LASERS - 1
			dw kw_ztov, var_i
for26_t:

			; Main.bas 274: llife(i) = 0
			dw kw_asetl, arr_llife, var_i, 0

			; Main.bas 275: Next i
			dw kw_next1l, var_i, 3, for26_t
for26_e:

			; Main.bas 276: For i = 0 To MAX_SHOTS - 1
			dw kw_ztov, var_i
for27_t:

			; Main.bas 277: slife(i) = 0
			dw kw_asetl, arr_slife, var_i, 0

			; Main.bas 278: Next i
			dw kw_next1l, var_i, 5, for27_t
for27_e:

			; Main.bas 279: For i = 0 To MAX_BOOMS - 1
			dw kw_ztov, var_i
for28_t:

			; Main.bas 280: bt(i) = 0
			dw kw_asetl, arr_bt, var_i, 0

			; Main.bas 281: Next i
			dw kw_next1l, var_i, 3, for28_t
for28_e:

			; Main.bas 284: If wave = 1 Or (wave - 1) Mod 5 = 0 Then
			dw kw_cpeqvl, var_wave, 1
			dw kw_ftov, tmp0
			dw kw_addl, var_wave, -1, tmp1
			dw kw_modvl, tmp1, 5, tmp1
			dw kw_cpeqvl, tmp1, 0
			dw kw_ftov, tmp1
			dw kw_orvv, tmp0, tmp1, tmp0
			dw kw_goto_ifzero, tmp0, if29_1

			; Main.bas 285: planet_dead = False
			dw kw_ztov, var_planet_dead

			; Main.bas 286: humans_alive = MAX_HUMANS
			dw kw_ltov, 8, var_humans_alive

			; Main.bas 287: For i = 0 To MAX_HUMANS - 1
			dw kw_ztov, var_i
for30_t:

			; Main.bas 288: hx(i) = (i * 128 + 48) And WORLD_MASK
			dw kw_mulvl, var_i, 128, tmp1
			dw kw_addl, tmp1, 48, tmp1
			dw kw_andvl, tmp1, 1023, tmp1
			dw kw_aset, arr_hx, var_i, tmp1

			; Main.bas 289: hstate(i) = H_GROUND
			dw kw_asetl, arr_hstate, var_i, 1

			; Main.bas 290: Next i
			dw kw_next1l, var_i, 7, for30_t
for30_e:

			; Main.bas 291: End If
if29_1:

			; Main.bas 292: For i = 0 To MAX_HUMANS - 1
			dw kw_ztov, var_i
for31_t:

			; Main.bas 293: hcarrier(i) = -1
			dw kw_asetl, arr_hcarrier, var_i, -1

			; Main.bas 294: If hstate(i) <> H_DEAD Then
			dw kw_aget, arr_hstate, var_i, tmp0
			dw kw_cpnevl, tmp0, 0
			dw kw_goto_iffalse, if32_1

			; Main.bas 295: hstate(i) = H_GROUND
			dw kw_asetl, arr_hstate, var_i, 1

			; Main.bas 296: hy(i) = GetTerrainY(hx(i)) + 6
			dw kw_aget, arr_hx, var_i, tmp1
			dw p_getterrainy_c, tmp1
			dw kw_vtov, var_getterrainy_getterrainy, tmp1
			dw kw_addl, tmp1, 6, tmp1
			dw kw_aset, arr_hy, var_i, tmp1

			; Main.bas 297: End If
if32_1:

			; Main.bas 298: If Rand(2) Then hdir(i) = 1 Else hdir(i) = -1
			dw p_rand_c, lit_2
			dw kw_goto_ifzero, var_rand_rand, if33_1
			dw kw_asetl, arr_hdir, var_i, 1
			dw if33_e_go
if33_1:
			dw kw_asetl, arr_hdir, var_i, -1
if33_e:

			; Main.bas 299: Next i
			dw kw_next1l, var_i, 7, for31_t
for31_e:

			; Main.bas 301: For i = 0 To MAX_ENEMIES - 1
			dw kw_ztov, var_i
for34_t:

			; Main.bas 302: estate(i) = ST_DEAD
			dw kw_asetl, arr_estate, var_i, 0

			; Main.bas 303: Next i
			dw kw_next1l, var_i, 9, for34_t
for34_e:

			; Main.bas 305: enemies_to_spawn = 10 + wave * 2
			dw kw_2x, var_wave, tmp1
			dw kw_addl, tmp1, 10, var_enemies_to_spawn

			; Main.bas 306: If enemies_to_spawn > 24 Then enemies_to_spawn = 24
			dw kw_scpgtvl, var_enemies_to_spawn, 24
			dw kw_goto_iffalse, if35_1
			dw kw_ltov, 24, var_enemies_to_spawn
if35_1:

			; Main.bas 307: enemies_alive = 0
			dw kw_ztov, var_enemies_alive

			; Main.bas 308: spawn_timer = 5
			dw kw_ltov, 5, var_spawn_timer

			; Main.bas 309: waveT = 0 : baiterT = 0
			dw kw_ztov, var_wavet
			dw kw_ztov, var_baitert

			; Main.bas 311: For i = 0 To 4
			dw kw_ztov, var_i
for36_t:

			; Main.bas 312: SpawnEnemy()
			dw p_spawnenemy_c

			; Main.bas 313: Next i
			dw kw_next1l, var_i, 4, for36_t
for36_e:

			; Main.bas 314: UpdateHUD()
			dw p_updatehud_c

			; Main.bas 315: Message "WAVE" & Str$(wave), 30
			dw kw_strstr, stmp1, var_wave
			dw kw_strcpy, stmp0, str7
			dw kw_strcat, stmp0, stmp1
			dw p_message_c, stmp0, lit_30
			dw p_initwave_r

; ============================================================
; sub spawnenemy (turbo mode)
; ============================================================

p_spawnenemy:

			; Main.bas 319: If enemies_to_spawn <= 0 Then Exit Sub
			dw kw_scplevl, var_enemies_to_spawn, 0
			dw kw_goto_iffalse, if37_1
			dw p_spawnenemy_r
if37_1:

			; Main.bas 320: For i = 0 To MAX_ENEMIES - 1
			dw kw_ztov, var_i
for38_t:

			; Main.bas 321: If estate(i) = ST_DEAD Then
			dw kw_aget, arr_estate, var_i, tmp0
			dw kw_cpeqvl, tmp0, 0
			dw kw_goto_iffalse, if39_1

			; Main.bas 322: ex(i) = (px + 250 + Rnd(500)) And WORLD_MASK
			dw kw_addl, var_px, 250, tmp1
			dw kw_rndl, 500, tmp2
			dw kw_add, tmp1, tmp2, tmp1
			dw kw_andvl, tmp1, 1023, tmp1
			dw kw_aset, arr_ex, var_i, tmp1

			; Main.bas 323: ey(i) = 110 + Rnd(30)
			dw kw_rndl, 30, tmp2
			dw kw_addl, tmp2, 110, tmp1
			dw kw_aset, arr_ey, var_i, tmp1

			; Main.bas 324: If Rnd(2) = 0 Then evx(i) = 1 Else evx(i) = -1
			dw kw_rndl, 2, tmp0
			dw kw_cpeqvl, tmp0, 0
			dw kw_goto_iffalse, if40_1
			dw kw_asetl, arr_evx, var_i, 1
			dw if40_e_go
if40_1:
			dw kw_asetl, arr_evx, var_i, -1
if40_e:

			; Main.bas 325: evy(i) = -1
			dw kw_asetl, arr_evy, var_i, -1

			; Main.bas 326: If planet_dead Then estate(i) = ST_MUTANT Else estate(i) = ST_WANDER
			dw kw_goto_ifzero, var_planet_dead, if41_1
			dw kw_asetl, arr_estate, var_i, 4
			dw if41_e_go
if41_1:
			dw kw_asetl, arr_estate, var_i, 1
if41_e:

			; Main.bas 327: etarget(i) = -1
			dw kw_asetl, arr_etarget, var_i, -1

			; Main.bas 328: enemies_alive = enemies_alive + 1
			dw kw_inc, var_enemies_alive

			; Main.bas 329: enemies_to_spawn = enemies_to_spawn - 1
			dw kw_dec, var_enemies_to_spawn

			; Main.bas 330: Exit Sub
			dw p_spawnenemy_r

			; Main.bas 331: End If
if39_1:

			; Main.bas 332: Next i
			dw kw_next1l, var_i, 9, for38_t
for38_e:
			dw p_spawnenemy_r

; ============================================================
; sub spawnbaiter (turbo mode)
; ============================================================

p_spawnbaiter:

			; Main.bas 337: For i = 0 To MAX_ENEMIES - 1
			dw kw_ztov, var_i
for42_t:

			; Main.bas 338: If estate(i) = ST_DEAD Then
			dw kw_aget, arr_estate, var_i, tmp0
			dw kw_cpeqvl, tmp0, 0
			dw kw_goto_iffalse, if43_1

			; Main.bas 339: ex(i) = (px + 300) And WORLD_MASK
			dw kw_addl, var_px, 300, tmp1
			dw kw_andvl, tmp1, 1023, tmp1
			dw kw_aset, arr_ex, var_i, tmp1

			; Main.bas 340: ey(i) = py
			dw kw_aset, arr_ey, var_i, var_py

			; Main.bas 341: estate(i) = ST_BAITER
			dw kw_asetl, arr_estate, var_i, 5

			; Main.bas 342: evx(i) = 0 : evy(i) = 0
			dw kw_asetl, arr_evx, var_i, 0
			dw kw_asetl, arr_evy, var_i, 0

			; Main.bas 343: etarget(i) = -1
			dw kw_asetl, arr_etarget, var_i, -1

			; Main.bas 344: Sfx.Play 1, 40, 2, 13, 25, 0
			dw p_sfx_play_c, lit_1, lit_40, lit_2, lit_13, lit_25, lit_0

			; Main.bas 345: Exit Sub
			dw p_spawnbaiter_r

			; Main.bas 346: End If
if43_1:

			; Main.bas 347: Next i
			dw kw_next1l, var_i, 9, for42_t
for42_e:
			dw p_spawnbaiter_r

Como puedes ver, el programa parece datos: lo es y no lo es. El SP se reutiliza como PC, lo que significa que el programa puede saltar directamente de una palabra clave a la siguiente sin la sobrecarga de llamadas ni de paso de parámetros. Por esto, tanto en el ZX Spectrum Next como en el Amstrad CPC se pueden crear juegos tan rápidos que exprimen el hardware (especialmente en el ZX Spectrum Next) desde BASIC, porque a BASIC le sienta como un guante esta estructura. ¿C? Como azúcar sintáctico sí, pero por cómo funciona normalmente bajo el capó: no. NTC BASIC te permite programar en JavaScript (un subconjunto de ES5+) o PHP (subconjunto) si no te gusta el estilo de Visual BASIC 5/6. Esto funciona traduciendo el JavaScript o PHP a NTC BASIC mediante un preprocesador antes de compilar, lo que se traduce en un comportamiento predecible y una sola tanda de errores de compilación. Todas las variables son estáticas sin excepción; ¿por qué importa esto? Pierdes la recursión, pero ganas estabilidad: no hay heap (montículo). ¿Puedes crear un heap? Claro que puedes, pero es mejor que crees funciones para gestionar tu memoria como tú prefieras.

CyborgOS (el hipervisor para Z80) también está programado en NTC BASIC (con una buena cantidad de ensamblador inline). Todo el código está en el repositorio de CyborgOS para las versiones de CPC, ZX Spectrum Next y MSX.

En NTC BASIC existen las clases para agrupar funciones relacionadas a modo de azúcar sintáctico, pero también para asegurar que queden confinadas dentro de un banco. Esto permite controlar el mapa de memoria para programas grandes de ¡hasta 4 megabytes!, o como en el caso de CyborgOS: 20 instancias de programas CP/M ejecutándose a la vez con todavía más de la mitad de la RAM libre en máquinas de 4 MB. Esa RAM adicional podría dedicarse a más instancias, pero… se utiliza para controladores, para «el heap» contenido dentro de una clase Linked List, discos RAM, etc.; puedes crear y destruir discos RAM sobre la marcha. Cada instancia de CP/M no tiene constancia de las demás instancias, y tampoco tienen por qué ser de CP/M: bien podrían ser una shell de POSIX, algo que consideraré. Las clases pueden estar dirigidas por interrupciones y activarse en sus propios bancos; cuando una aplicación quiere una gran cantidad de datos, la solicita y el kernel, que puede estar en C0 o C2 (probado, pero de momento sigo usando C0), se la suministra en bloque como un DMA virtual (es DMA solo desde la perspectiva de la aplicación).

¿Para los 464 sin expandir? Puedes conseguir unos 12 KB extra usando las ROMs de NTC BASIC que contienen las palabras clave, pero ¿en serio? Cómprate una expansión de memoria: ¡1 MB hace maravillas en tu ordenador! 4 MB es todo lo que llegarás a necesitar.

¿Es NTC BASIC tan rápido como SDCC? No; y en realidad SDCC tampoco es más rápido: depende de lo que estés haciendo. NTC BASIC no es un compilador especialmente optimizador; solo se aplican algunas optimizaciones básicas. Pero ofrece muchísimo que SDCC no tiene, como el uso completo de superposiciones (overlays) y páginas (la sutil diferencia es que los overlays se reemplazan entre sí para ordenadores sin mucha RAM, o para instancias de CP/M que no quieran recurrir a técnicas más modernas), mientras que las páginas permanecen en bancos de memoria.

1 me gusta