Invasion (was Defender) in NTC BASIC

Invasion was backported from my ZX Spectrum Next version of Defender before changing the title to Invasion. The original release of Defender was ok, I thought hmm CPC running compiled BASIC - that is what I might expect, but… I found retesting Invasion on ZX Spectrum Next after having coded Cyborg Hellgate to be odd in behaviour - the more enemies I killed the faster the game got - and… the CPC inhereted that same bug. Now the CPC version (and the ZX Spectrum Next one) flies sooo well. Is it the best Defender clone on CPC? I don’t know - I still have a soft spot for Defend or Die and if it didn’t randomly crash a lot for me - I’d go with Defend or Die for authenticity.

I removed the inline assembly for easy of following the code, but the program was originally coded 100% in NTC BASIC then optimised - which mostly came down to the movement of objects because the CPC doesn’t have hardware sprites. All sprites are also compiled.

main.bas COMMENTS

' ============================================================================
' INVASION - Amstrad CPC Edition (mode 0, software sprites)
' ============================================================================
' NTC BASIC. The ZX Spectrum Next edition's game, as it is, on a CPC: its world, its landers,
' mutants, humans and scanner, its sounds and play:
'   - the 16 sprite patterns (two frames for the ship, landers, mutants and humans, an enemy
'     shot, an explosion, the baiter) drawn by the program (Cpc.cls) from INVASION.SPR's art
'     turned into mode 0 (CpcData.cls, by tools/cpcgfx.py): 8 x 16 mode 0 pixels each, the
'     same size on the screen
'   - stars behind, scrolling slower than the ground, a 1024 pixel landscape, the scanner
'     showing where the screen is (the screen shows 320 of the world's pixels: a mode 0 pixel is
'     two of the Next's)
'   - sounds for everything on the AY (Sfx.cls): laser, explosions, abductions, catches, the
'     smart bomb, hyperspace, the thrust
'   - losing a ship doesn't start the wave again; a moment's safety after; the ship turns round
'     smoothly; humans walk, fall (from high up they die), get caught and set down (500 each);
'     the baiter comes if a wave drags on; an extra ship and smart bomb every 10,000; the
'     humans and the planet come back every fifth wave
' Two screens (#C000 and #0000; the program is built from #4000); every frame Cpc.Draw puts the
' sprites, the ground and the stars on the one not shown, Cpc.Flip shows it.
' A CPC gets through a frame of this in 25-40 ms, and Cpc.Flip shows one every second 50th of a   ' <--- CHANGED
' second, steady (the Next did it fifty times a second), so each frame here is two of the Next's:   ' <--- CHANGED
' the speeds are doubled and the times in frames halved (the sound is updated twice a frame): the   ' <--- CHANGED
' Next's pace.   ' <--- CHANGED


2 Likes

main.bas

Option Explicit
Option Turbo
Option Org &H4000                 ' the second screen is #0000-#3FFF (Cpc.cls); the program from #4000

' World & Screen Constants (the view: 320 x 192 of the Next's pixels, 160 x 192 mode 0; y counts up)
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

' Sprite Pattern Indices (CpcData.cls, from 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      ' + this: the second frame of 0-3 (ship, lander, mutant, human): patterns 5-8
Const PAT_SHOT   As Integer = 9
Const PAT_BOOM   As Integer = 10     ' 10-13
Const PAT_BAITER As Integer = 14     ' 14-15

' Sprite Slot Allocations (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

' Entity Counts
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

' Enemy States
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

' What EnemyStep and HumanStep (Z80) come back with
Const EV_GRABBED As Integer = 1      ' the lander has taken hold of a human
Const EV_TAKEN As Integer = 2        ' it has taken one off the top: now a mutant
Const EV_LASERED As Integer = 3      ' hit by a laser: its number in bits 4-5
Const EV_RAMMED As Integer = 4       ' it hit the ship
Const EV_SHOOT As Integer = 64       ' and it fires (es_x: where it is across the view)
Const EV_SETDOWN As Integer = 1      ' a human carried is set down
Const EV_CAUGHT As Integer = 2       ' a human falling is caught
Const EV_FELL As Integer = 3         ' one fell too far
Const EV_LANDED As Integer = 4       ' one fell and landed
Dim es_x As Integer

' Pens (Cpc.cls: 0 black, 1 white, 2 grey, 3 cyan, 4 sky blue, 5 blue, 6 yellow, 7 orange,
' 8 red, 9 dark red, 10 green, 11 dark green, 12 lime, 13 purple, 14 pink, 15 pastel yellow)
Const PEN_DIVIDER As Integer = 13
Const PEN_SCANNER As Integer = 10

' Player State
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
' this frame's: frames And 1, Mod 3, (\ 4) And 1, (\ 6) And 1 (a division costs 1,100 cycles)
Dim f2 As Integer, f3 As Integer, f4 As Integer, f12 As Integer

' Lasers
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

' Humanoids
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

' Enemies
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

' Enemy Projectiles
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

' Explosions (world x, y, frames to go)
Dim bx(MAX_BOOMS) As Integer, by(MAX_BOOMS) As Integer, bt(MAX_BOOMS) As Integer

' The landscape: a height for each 4 pixels of the world (256 of them)
Dim land(255) As Byte
Dim terDead As Boolean

' The CPC-style statements the original used (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

' ============================================================================
' SPRITE HELPERS (the Next's hardware sprite slots, drawn by Cpc.Draw)
' ============================================================================

' The sprite slots are Cpc.Spr slot, x (0..319 across the view), y (0..191, up), pattern, mirror,
' shown and Cpc.Hide slot - called straight (a Sub in between costs 100 cycles a call)

' ============================================================================
' COORDINATE & TERRAIN HELPERS
' ============================================================================

' the landscape, made once: hills and mountains wandering up and down (no more than 4 pixels
' each 4 across, so the line has no gaps)
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
    ' the last few lead back to the first, so it joins all the way round
    For k = 248 To 255
        land(k) = land(247) + (land(0) - land(247)) * (k - 247) \ 9
    Next k
End Sub

' Rand(n): 0 to n - 1 from rs, the same every time from the same seed (the landscape and the
' stars are made from it). A 16 bit sequence: the original's 32 bit one took 10,000 cycles a go
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): where world x wx is across the view from cx, -512 to 511.
' The radar: the Next's pixels, 64 + ((wx - px + 512) And 1023) \ 8 across the box (the ship in the
' middle), 153 + y \ 7 up from the bottom: Cpc.RDot wx, px, y, pen draws a dot there
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   ' <--- NEW (what the HUD shows: only what changes is printed)
Sub UpdateHUD()
    If score <> hudScore Then                                  ' <--- NEW
    Pen 15
    At 1, 1
    PrintS Right$("000000" & Mid$(Str$(score), 2), 7)
    hudScore = score                                           ' <--- NEW
    End If                                                     ' <--- NEW
    If lives <> hudLives Then                                  ' <--- NEW
    Pen 14
    At 10, 1
    PrintS "SHIPS" & Str$(lives) & " "
    hudLives = lives                                           ' <--- NEW
    End If                                                     ' <--- NEW
    If bombs <> hudBombs Then                                  ' <--- NEW
    Pen 12
    At 19, 1
    PrintS "BOMBS" & Str$(bombs) & " "
    hudBombs = bombs                                           ' <--- NEW
    End If                                                     ' <--- NEW
    If wave <> hudWave Then                                    ' <--- NEW
    Pen 11
    At 27, 1
    PrintS "WAVE" & Str$(wave) & " "
    hudWave = wave                                             ' <--- NEW
    End If                                                     ' <--- NEW
    hudDirty = 0
End Sub

Sub DrawStaticFrame()
    ClsAll
    hudScore = -1 : hudLives = -1 : hudBombs = -1 : hudWave = -1   ' <--- NEW (the screen is clear: all of the HUD printed)
    ' Scanner divider (line 43) and radar box (the Next's x 62..193, y 152..182 up from the
    ' bottom: CPC pixels 31..96, lines 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

' the stars: scattered over the sky (lines 46-155), drawn by Cpc.Draw scrolling at a quarter of
' the ground's speed
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

' ============================================================================
' WAVE & ENTITY MANAGEMENT
' ============================================================================

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

    ' the humans: all of them at the start, and back again every fifth wave
    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

' the baiter: a fast saucer that comes for you when a wave goes on too long
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                  ' the sky white
    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

' the ship's hit: it blows up, the others clear off a little, a moment's safety, and on
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
    ' the enemies near the ship are moved away, the shots gone
    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

' a message in the middle of the play area for a while
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                             ' <--- CHANGED (was Len(m) \ 2: a division)
    PrintS m
    msgT = t : msg = m
End Sub

' ============================================================================
' THE PARTS OF A FRAME
' ============================================================================

Sub Controls()
    kb = Cpc.Keys()
    ' Up: Q or Joy Up | Down: A or Joy Down
    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

    ' Left: O or Joy Left | Right: P or Joy Right
    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

    ' the ship sits to one side, leaving room ahead: turning round, it slides across
    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

    ' Fire Laser: Space, Z, or Joy Fire 1
    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

    ' Smart Bomb: B, M, or Joy Fire 2
    If bomb_cd > 0 Then bomb_cd = bomb_cd - 1
    If (kb And 32) And bomb_cd = 0 Then TriggerSmartBomb(cam_x)

    ' Hyperspace: H: somewhere else, at random
    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

' Lasers: the bolts flown on and drawn (Z80); lscr and lasersOn for the enemies' hit test
Sub Lasers()
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

Sub Humans()
    ' each human is moved and drawn by HumanStep (Z80); what comes back is what the game has to
    ' do about it
    Dim ev As Integer
    i = 0                                                      ' <--- NEW
    Do While i < MAX_HUMANS                                    ' <--- CHANGED (was For i = 0 To MAX_HUMANS - 1)
        ev = HumanStep(i)                                      ' <--- CHANGED (Z80: from human i on to the first with something to do; i = that one)
        If ev Then                                             ' <--- NEW (nothing to do: no tests)
        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
            ' too far to fall
            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                                                 ' <--- NEW
        i = i + 1                                              ' <--- NEW
    Loop                                                       ' <--- CHANGED (was Next i)
End Sub

' no humans left: the planet goes, and every lander turns mutant
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
    ' the baiter: after 40 seconds, then every 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

    ' each enemy is moved, drawn and checked by EnemyStep (Z80: it was 90 keywords an enemy);
    ' what comes back is what the game has to do about it
    i = 0                                                      ' <--- NEW
    Do While i < MAX_ENEMIES                                   ' <--- CHANGED (was For i = 0 To MAX_ENEMIES - 1)
        ev = EnemyStep(i)                                      ' <--- CHANGED (Z80: from enemy i on to the first with something to do; i = that one)
        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                                              ' <--- NEW
    Loop                                                       ' <--- CHANGED (was Next i)
End Sub

' EnemyStep(i): enemy i's frame - as Enemies had it in BASIC (the ZX Spectrum Next edition's
' Main.bas, which is where to read the logic): moved by its state (wandering, coming down for a
' human, taking one up, a mutant, the baiter), kept in the play area, drawn with its radar dot
' (cp_showr), then checked against the lasers and the ship, and whether it fires. Comes back
' with EV_... (0 = nothing to do), es_x = where it is across the view
Function EnemyStep(ByVal n As Integer) As Integer   ' <--- CHANGED (n: {i} below is the game's i)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' HumanStep(i): human i's frame, as Humans had it in BASIC: walking about on the ground, carried
' by the ship (set down when it meets the ground), falling (caught by the ship, or landing - from
' too high up, dead); drawn with its radar dot. Comes back with EV_... (0 = nothing to do)
Function HumanStep(ByVal n As Integer) As Integer   ' <--- CHANGED (n: {i} below is the game's i)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function


' a shot from enemy n at screen x esx, aimed at the ship
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

' Shots: the enemies' shots flown on and drawn (Z80); one that hits the ship ends it
Sub Shots()
    If ShotsStep() Then LoseLife()
End Sub

' ShotsStep(): the shots' frame: True when one hit the ship (it is gone)
Function ShotsStep() As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Booms: the explosions' frames (Z80)
Sub Booms()
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' the scanner's marks showing the part of the world on the screen (just outside the box, above
' and below it): Cpc.Draw puts them at m and 40 radar pixels on
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

' ============================================================================
' MAIN PROGRAM ENTRY
' ============================================================================

Mode 0
CpcData.Data                      ' (the tables: never runs, keeps them in the program)
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                       ' <--- CHANGED (was f3 = frames Mod 3: a division)
        If f3 = 3 Then f3 = 0                                  ' <--- NEW (frames Mod 3 as a counter)
        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))   ' <--- CHANGED (only where the message is: was At 1, 11 : PrintS Space$(32), 64 letters drawn)
            ElseIf Cpc.Touched() Then
                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)
            End If
        End If

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

        ' wave complete: a bonus for each human left
        If enemies_alive <= 0 And enemies_to_spawn <= 0 And Not game_over Then
            WaveDone
        End If
    Loop

    ' Game Over Screen
    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                ' (the words go where the sprites were)
    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"   ' <--- CHANGED (was 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                                         ' <--- NEW
    At 1, 22 : PrintS "(C) 2026 CYBORG UNICORN PTY LTD"   ' <--- NEW
    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
        ' the ship flies across the bottom
        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

' ============================================================================
' THE ORIGINAL'S CPC-STYLE STATEMENTS
' ============================================================================

' Pen n: the colour for the words that follow (the original's pen numbers, as Cpc.cls pens)
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: the original's border colours (0 black, 2 red, 7 white)
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: where the words go (1, 1 = the top left of the Next's 32 x 24 picture: column 4 of
' the CPC's 40, row 0 of its 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: the frame drawn on the screen not shown, shown at the frame flyback; the sound kept going
Sub Vsync()
    Cpc.Draw cam_x
    Cpc.Flip
    Sfx.Update
    Sfx.Update                     ' (twice: a frame here is two of the Next's)
End Sub

' The terrain: Cpc.Draw draws it from the heights (Heights); only the planet's death changes them
Sub Terrain(ByVal cx As Integer)
    If terDead <> planet_dead Then Heights : terDead = planet_dead
End Sub

' the ground's line for every x of the world (top down), sloping between the 4-pixel steps of
' 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 Like

cpc.cls

' Cpc - INVASION's screen on the Amstrad CPC: mode 0 (160 x 200, 16 pens), two screens (#C000
' and #0000-#3FFF: the program is built from #4000), software sprites standing in for the
' Next's hardware ones, and the fast arithmetic the game loop needs.
' (c) 2026 Cyborg Unicorn Pty Ltd
'
' Each frame the game sets its sprites (Spr: a slot, its place, its pattern - as the Next's
' slots, they stay until changed; Hide), the radar's dots (RDot, Dot) and view marks (Mark), and
' any words (Text: straight on to both screens), then Draw draws the frame on the screen not
' shown: the ground (only when the view moved: the bytes it was in last time blacked out, then a   ' <--- CHANGED
' pixel or two a column), the stars (the old ones off, the new ones on), the lines a sprite has   ' <--- CHANGED
' blacked out, then the sprites (compiled code). Flip shows it at the second frame flyback since   ' <--- CHANGED
' the last flip - 25 frames a second, steady, whatever a frame took under 40 ms (cp_tick counts   ' <--- CHANGED
' the time) - and gets the other screen ready: the sprites of two frames ago blacked out, its   ' <--- CHANGED
' radar cleared. Nothing is kept from under a sprite: everything one can cover is drawn again,   ' <--- CHANGED
' except words (Touched says when the message row was hit, and which letters).   ' <--- CHANGED
' The sprites' coordinates are the Next's (x 0-319 across the view, y up from the bottom as the
' original's Plot); a 16 x 16 pattern becomes 8 x 16 mode 0 pixels (CpcData.cls).
' Words: 40 columns of 4 pixels (the narrow font), 25 rows of 8 lines. Pixel calls (Dot, HLine,
' Big) take mode 0 pixels, 0-159 across, lines 0-199 down.
' All of it is Z80 (#asm); Draw's block holds the tables and the routines the others call, so
' a program must use Draw (they all do).
' Interrupts: IM 2 (a 257 byte table: any bus value works), on only while Cpc's, Sfx's and the game's Z80 runs on the   ' <--- NEW
' real stack; the BASIC thread (SP is the thread) runs with them off, as before.   ' <--- NEW
' Memory past the program (not in the file): Text's tables, the line tables, the ground's and the   ' <--- NEW
' stars' lists (ntc_free up, to about #AD00: the firmware's RAM is not used once it has started).   ' <--- NEW
Option Explicit

Public Const Slots = 33

' Init: the 16 inks and the border, the line tables, the screen at #C000 shown (the other is
' #0000-#3FFF: the program is built from #4000), nothing drawn yet. After Mode 0
Public Sub Init()
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Ink pen, ink: a pen's (0-15, 16 = the border) CPC ink (0-26)
Public Sub Ink(ByVal pen As Integer, ByVal ink As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Spr slot, x, y, pattern, mirror, shown: slot 0-32 shows pattern (0-15) with its middle at x
' (0-319 across the view), y (up from the bottom, 0-191), mirrored if mirror; or hidden
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: slot hidden
Public Sub Hide(ByVal slot As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' HideAll: every slot hidden
Public Sub HideAll()
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Text col, row, s, pen: s from column col (0-39), row (0-24) in pen (black behind it), on both
' screens. Letters 32-95 (small letters shown as capitals). A sprite over the words takes them
' with it when it goes (Flip blacks out where it was): Touched says when that has happened on
' the message row
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 once when a sprite has been blacked out from the message row (lines 80-87)
' on either screen since the last call: the message wants printing again (and the Text that
' prints it then draws only the letters that were blacked out, on each screen)   ' <--- CHANGED
Public Function Touched() As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Dot x, y, pen: a radar dot (pixel x 0-159, line y 0-199) on the screen being drawn on (the
' radar's inside is cleared at each 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: the radar dot for world x wx (the ship at px is in the middle of the
' box), y (up from the bottom): at pixel 32 + ((wx - px + 512) And 1023) \ 16, line 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): an entity of the world at wx, y (up from the
' bottom) in one go: its sprite in slot (shown where it is across the view from cx when that is
' -8 to 327, hidden otherwise), its radar dot in pen (none when pen is -1), and where it is
' across the view given back (-512 to 511). One keyword for what took ten. (Z80 in the game
' fills cp_shb and calls cp_showr: the same thing, no keyword)
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): where world x wx is across a view from cx: -512 to 511
Public Function ScrX(ByVal wx As Integer, ByVal cx As Integer) As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Mark x: the view marks at radar x (the Next's: 62-153), on the screen being drawn on: a byte
' (two white pixels) at x / 4 and 10 bytes on, lines 7 and 41 (taken off at the next Flip, as
' the dots are). -1 = none
Public Sub Mark(ByVal x As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Star k, x, y, pen: star k (0-31) at x (0-319 in the sky), line y, in pen. (Kept as x, the
' line's offset in a screen and the pen's left pixel bits: nothing to look up when it is drawn)
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: the ground's line (0-199) at world x wx (0-1023, even: the odd ones aren't kept)
Public Sub Height(ByVal wx As Integer, ByVal y As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' GroundAt(wx): the ground's height (up from the bottom) at world x wx, from the height table
' (Height), in 100 cycles - the game's GetTerrainY took 500
Public Function GroundAt(ByVal wx As Integer) As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Ground on: what Draw draws besides the sprites: 1 the ground, 2 the stars, 4 the radar (its
' inside cleared at each Flip, its lines drawn again). Added: 7 in play, 2 on the title, 0 the
' game over screen
Public Sub Ground(ByVal on As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Cls: both screens black, nothing remembered on either
Public Sub Cls()
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' HLine x, y, w, pen: a line across from pixel x, line y, w pixels, on both screens
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 in the CPC's own 8 x 8 letters, two pixels across and three lines down for each
' of theirs (16 x 24), shaded pastel yellow to red with a dark red shadow, from pixel x (even),
' line y, on both screens
Public Sub Big(ByVal x As Integer, ByVal y As Integer, ByVal s As String)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Flip: wait for the second frame flyback since the last flip (the next one when that is already
' past), show the screen Draw drew on, draw on the other from now   ' <--- CHANGED
Public Sub Flip()
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Shr(x, n): x (0-65535, as unsigned) shifted right n bits: x \ 2, 4, 8 ... without the division
' (which takes about 1,100 cycles; this takes 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 for x 0-65535 (unsigned), d 1-255: about 400 cycles (the division's 1,100),
' 250 for x under 256
Public Function Div(ByVal x As Integer, ByVal d As Integer) As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Rnd(n): a random number 0 to n - 1 (n 1-255), in about 350 cycles (NTC's Rnd takes 1,200)
Public Function Rnd(ByVal n As Integer) As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Keys(): the keys held: 1 up (Q, joystick up), 2 down (A), 4 left (O), 8 right (P), 16 fire
' (space, Z, fire 1), 32 smart bomb (B, M, fire 2), 64 hyperspace (H)
Public Function Keys() As Integer
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Function

' Draw cx: the frame on the screen being drawn on (the one not shown: Flip took its old
' sprites off and cleared its radar), the view from world x cx: the ground and the stars
' (Ground), the sprites. Then Flip
Public Sub Draw(ByVal cx As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

sfx.cls

' Sfx - sound effects on the CPC's AY chip, through the PPI (port A #F4xx carries the data,
' port C #F6xx the AY's control bits). Three channels, each playing one effect at a time:
' a tone or noise whose pitch slides and whose volume fades to nothing over its length.
' (c) 2026 Cyborg Unicorn Pty Ltd
' Call Update once a 50th of a second; here Vsync calls it twice a frame, and the AY is written on
' the second of each two (the first one's writes were overwritten straight away)   ' <--- CHANGED
'   Sfx.Play 0, 200, 8, 15, 12, 0     ' channel A: tone period 200, rising pitch... 12 frames
Option Explicit

Private per(2) As Integer         ' the tone period now (12 bits; noise: 0-31)
Private slide(2) As Integer       ' added to it each frame
Private vol(2) As Integer         ' the volume it starts at (0-15)
Private total(2) As Integer       ' its length in frames
Private left(2) As Integer        ' frames to go (0 = silent)
Private nse(2) As Integer         ' 1 = noise, 0 = tone

Private Sub W(ByVal r As Integer, ByVal v As Integer)
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Init: every channel quiet
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: start an effect on channel 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 while channel ch plays
Public Function Busy(ByVal ch As Integer) As Integer
  Busy = (left(ch) > 0)
End Function

' Update: once a frame - the pitches slide, the volumes fade, the AY written (Z80: it is called
' twice a frame)
Public Sub Update()
    ' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

' Quiet: every channel stopped
Public Sub Quiet()
  Dim i As Integer
  For i = 0 To 2
    left(i) = 0
    W 8 + i, 0
  Next
End Sub

and finally cpcdata.cls

' CpcData - INVASION's sprites and narrow font for the Amstrad CPC, mode 0 (made by
' tools/cpcgfx.py from INVASION.SPR: don't edit). The sprites are compiled: each of the 16
' patterns, plain and mirrored (only the ship, the humans and the bolt have a mirrored one:
' the others' entries point at the plain), as Z80 code that draws it at HL (its top left
' byte) - a whole byte written, a half one masked in, a clear one skipped - one line to the
' next by #800 (cp_nextw past a row's eighth: Cpc.cls). cd_spr is a table of 32 entries (pattern * 2 + mirror): xoff,
' yoff, w (bytes), h, the code's address. Sprites are drawn at even pixels (a byte boundary).
' cd_font: 64 characters (32-95) of 8 rows, a 4 pixel row in bits 7-4. cd_inks: the 16 pens'
' CPC ink numbers.
' (c) 2026 Cyborg Unicorn Pty Ltd
Option Explicit

' Data: never called - it holds the tables and the sprite code (global labels, used by Cpc.cls)
Public Sub Data()
' INLINE ASSEMBLY LANGUAGE REMOVED
End Sub

What does that above code assemble to? Here is a snippet.

; ============================================================
; 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

As you can see the program looks like data - it is and it isn’t. SP is repurposed as the PC - what it means is the program can jump directly from one keyword to the next without any call and parameter passing overheads. This is why on both the ZX Spectrum Next and Amstrad CPC can make such fast hardware pushing games (especially on the ZX Spectrum next) from BASIC because BASIC suits this construct. C? as a syntactic sugar yes, but how it typically works under the hood - no. NTC BASIC lets you program in JavaScript (ES5+ subset) or PHP (subset) if you don’t like Visual BASIC 5/6 style. This works by translating the JavaScript or PHP into NTC BASIC as a preprocessor before compiling - which means predictable behaviour and one set of compile bugs. All variables are static without exception - why this matters? You lose recursion - but you gain stability - there is no heap. Can you crate a heap? of course you can, but better you create functions to deal with your memory how you wish.

CyborgOS (the Z80 hypervisor) is also coded in NTC BASIC (with quite a fair bit of inline Assembly). All the code is in the repo for CyborgOS for the CPC, ZX Spectrum Next and MSX builds.

Classes exist in NTC BASIC to contain related functions in a syntactic sugar way, but also to ensure they are contained to stay within a bank. This allows control of the memory map for large programs up to 4megabytes! or as per CyborgOS - 20 instances of CP/M programs running at once with still more than half the RAM free on 4mb machines. That extra RAM could be more instances but… they are used for drivers, for ‘the heap’ contained within a Linked List class, ramdiscs etc - you can create and drop ramdiscs on the fly. Each CP/M instance is unaware of the other instances - and they don’t need to be CP/M either - they can very well be a POSIX shell which I will consider. Classes can be interrupt driven and triggered in their own banks - when an application wants a lot of data, it requests for it, and the kernel which can be at C0 or C2 (proven but still using C0 for now) supplies it in bulk like a virtual DMA (It is DMA from the application perspective only).

For unexpanded 464s? You can get another 12kb or so by using the NTC BASIC ROMs which contain the keywords - but really? Go buy a memory expansion - 1mb does your computer wonders! 4mb is all you will ever need.

Is NTC BASIC as fast as SDCC? No - and in reality neither is SDCC faster - it depends on what you are doing - NTC BASIC is not a very optimising compiler - only some basic optimisations are made. But it gives so much that SDCC doesn’t - like full use of overlays as well as pages (the subtle difference is overlays replace eachother for computers without much RAM - or CP/M instances which don’t want to use newer techniques) whereas pages remain in banks of memory.

1 Like