' based on code from https://www.youtube.com/watch?v=oJvJZNyW_rw

CLEAR ALL

' Side Scroller using Tilemaps

' Level storage
'    .    Sky
'    #    Solid block
'    G    Ground block
'    B    Brick block
'    ?    Question block
'    o    Coin
'    M    Mushroom
'    V    Flower
'    *    Star

nLevelWidth=64
nLevelHeight=16
sLevel$=""
sLevel$=sLevel$+"................................................................"
sLevel$=sLevel$+"................................................................"
sLevel$=sLevel$+".......ooooo.............................................MV*...."
sLevel$=sLevel$+"........ooo.............................................#####..."
sLevel$=sLevel$+".......................########................................."
sLevel$=sLevel$+".....BB?BBBB?BB.......###..............#.#......................"
sLevel$=sLevel$+"....................###................#.#......................"
sLevel$=sLevel$+"...................####........................................."
sLevel$=sLevel$+"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG.##############.....########"
sLevel$=sLevel$+"...................................#.#...............###........"
sLevel$=sLevel$+"........................############.#............###..........."
sLevel$=sLevel$+"........................#............#.........###.............."
sLevel$=sLevel$+"........................#.############......###................."
sLevel$=sLevel$+"........................#................###...................."
sLevel$=sLevel$+"........................#################......................."
sLevel$=sLevel$+"................................................................"

' Insert spaces to allow for smooth scrolling
FOR i=LEN(sLevel$)-nLevelWidth+1 TO 2 STEP -nLevelWidth
    sLevel$=STRINSERT$(sLevel$,i,"  ")
NEXT i
nLevelWidth=nLevelWidth+2
sLevel$=STRREPEAT$(" ",nLevelWidth)+" "+sLevel$+" "+STRREPEAT$(" ",nLevelWidth)
nLevelHeight=nLevelHeight+2

' Player properties
fPlayerPosX=2.0
fPlayerPosY=2.0
fPlayerVelX=0.0
fPlayerVelY=0.0
bPlayerOnGround=FALSE

' Camera properties
fCameraPosX=0.0
fCameraPosY=0.0

redraw=TRUE
iOldOffsetX=0
iOldOffsetY=0

' Load sprite assets
BITMAP 1 LOAD "SMB.png"
BITMAP 2 LOAD "Duck.png"

' Load audio assets
SOUND 1 LOAD "Jump.wav" VOLUME 25
SOUND 2 LOAD "Coin.wav" VOLUME 25

' Initialize screen
BACKGROUND RGB(0,128,255) ' Sky
TILESET 1 NEW 16,16,1 MAP 32 TO 0 MAP ".#GB?oMV*" TO 0
SCREEN TILEMAP 22,15,1 XYSIZE 4,4
DOUBLE BUFFER

' Initialize sprite
SPRITE 2 BIND BITMAP 2 SHOW XYSIZE 4,4 XYSTEP 4,4

' Is a gamepad connected?
gp=-1
FOR n=0 TO 3
    IF GAMEPAD.ISCONNECTED(n) THEN gp=n:EXIT FOR
NEXT n

' Utility Lambdas
FUNCTION GetLevelTile(x%,y%)
    SHARE nLevelWidth,nLevelHeight,sLevel$
    IF x%>=0 AND x%<nLevelWidth AND y%>=0 AND y%<nLevelHeight THEN
        GetLevelTile=ASC(sLevel$,y%*nLevelWidth+x%+1)
    ELSE
        GetLevelTile=32
    END IF
END FUNCTION

FUNCTION SetLevelTile(x%,y%,c)
    SHARE nLevelWidth,nLevelHeight,sLevel$
    IF x%>=0 AND x%<nLevelWidth AND y%>=0 AND y%<nLevelHeight THEN
        sLevel$=STRSUB$(sLevel$,y%*nLevelWidth+x%+1,CHR$(c))
    END IF
END FUNCTION

CLS
INK RGB(255,255,255)
PRINT "Side Scroller using Tilemaps"
PRINT
PRINT "Use the Left and Right arrow keys to move, Space to jump, Esc to quit."
IF gp<>-1 THEN
    PRINT
    PRINT "Gamepad detected."
END IF

tick:
fElapsedTime=ELAPSED.TIME/1000
IF fElapsedTime>0.1 THEN fElapsedTime=0.1

' Handle keyboard input
IF KEY.DOWN(VK_UP) THEN fPlayerVelY=-6.0
IF KEY.DOWN(VK_DOWN) THEN fPlayerVelY=6.0
IF KEY.DOWN(VK_LEFT) THEN
    t=-15.0:IF bPlayerOnGround THEN t=-25.0
    fPlayerVelX=fPlayerVelX+t*fElapsedTime
END IF
IF KEY.DOWN(VK_RIGHT) THEN
    t=15.0:IF bPlayerOnGround THEN t=25.0
    fPlayerVelX=fPlayerVelX+t*fElapsedTime
END IF

IF KEY.PRESSED(VK_ESC) THEN END
IF KEY.PRESSED(VK_SPACE) THEN
    IF fPlayerVelY=0 THEN
        SOUND 1 PLAY
        fPlayerVelY=-12.0
    END IF
END IF

' Handle gamepad input
IF gp<>-1 THEN
    dz=0.25
    x=GAMEPAD.THUMBSTICK(gp,1)
    y=0
    b=GAMEPAD.DPAD(gp)
    IF b=1 THEN y=1
    IF b=2 THEN y=-1
    IF y<-dz OR y>dz THEN fPlayerVelY=-6.0*y
    IF x<-dz OR x>dz THEN
        t=15.0*x:IF bPlayerOnGround THEN t=25.0*x
        fPlayerVelX=fPlayerVelX+t*fElapsedTime
    END IF

    IF GAMEPAD.BUTTONS(gp)=2 THEN
        IF bPlayerOnGround AND fPlayerVelY=0 THEN
            SOUND 1 PLAY
            fPlayerVelY=-12.0
        END IF
    END IF
END IF

' Gravity
fPlayerVelY=fPlayerVelY+20*fElapsedTime

' Drag
IF bPlayerOnGround THEN
    fPlayerVelX=fPlayerVelX+(-3.0*fPlayerVelX*fElapsedTime)
    IF ABS(fPlayerVelX)<0.01 THEN
        fPlayerVelX=0.0
    END IF
END IF

' Clamp velocities
fPlayerVelX=CLAMP(fPlayerVelX,-10.0,10.0)
fPlayerVelY=CLAMP(fPlayerVelY,-10.0,10.0)

' Calculate potential new position
fNewPlayerPosX=fPlayerPosX+fPlayerVelX*fElapsedTime:fNewPlayerPosX=CLAMP(fNewPlayerPosX,1,nLevelWidth-2)
fNewPlayerPosY=fPlayerPosY+fPlayerVelY*fElapsedTime:fNewPlayerPosY=CLAMP(fNewPlayerPosY,1,nLevelHeight-2)

' Check for pickups!
IF GetLevelTile(fNewPlayerPosX+0.0,fNewPlayerPosY+0.0)=ASC("o") THEN
    SOUND 2 PLAY
    SetLevelTile(fNewPlayerPosX+0.0,fNewPlayerPosY+0.0,ASC("."))
    PUT TILE fNewPlayerPosX-INT(fOffsetX)+0.0,fNewPlayerPosY-INT(fOffsetY)+0.0,0
END IF

IF GetLevelTile(fNewPlayerPosX+0.0,fNewPlayerPosY+1.0)=ASC("o") THEN
    SOUND 2 PLAY
    SetLevelTile(fNewPlayerPosX+0.0,fNewPlayerPosY+1.0,ASC("."))
    PUT TILE fNewPlayerPosX-INT(fOffsetX)+0.0,fNewPlayerPosY-INT(fOffsetY)+1.0,0
END IF

IF GetLevelTile(fNewPlayerPosX+1.0,fNewPlayerPosY+0.0)=ASC("o") THEN
    SOUND 2 PLAY
    SetLevelTile(fNewPlayerPosX+1.0,fNewPlayerPosY+0.0,ASC("."))
    PUT TILE fNewPlayerPosX-INT(fOffsetX)+1.0,fNewPlayerPosY-INT(fOffsetY)+0.0,0
END IF

IF GetLevelTile(fNewPlayerPosX+1.0,fNewPlayerPosY+1.0)=ASC("o") THEN
    SOUND 2 PLAY
    SetLevelTile(fNewPlayerPosX+1.0,fNewPlayerPosY+1.0,ASC("."))
    PUT TILE fNewPlayerPosX-INT(fOffsetX)+1.0,fNewPlayerPosY-INT(fOffsetY)+1.0,0
END IF

' Check for collision
IF fPlayerVelX<0 THEN ' Moving left
    IF GetLevelTile(fNewPlayerPosX+0.0,fPlayerPosY+0.0)<>ASC(".") OR GetLevelTile(fNewPlayerPosX+0.0,fPlayerPosY+0.9)<>ASC(".") THEN
        fNewPlayerPosX=INT(fNewPlayerPosX)+1
        fPlayerVelX=0
    END IF
ELSE IF fPlayerVelX>0 THEN ' Moving right
    IF GetLevelTile(fNewPlayerPosX+1.0,fPlayerPosY+0.0)<>ASC(".") OR GetLevelTile(fNewPlayerPosX+1.0,fPlayerPosY+0.9)<>ASC(".") THEN
        fNewPlayerPosX=INT(fNewPlayerPosX)
        fPlayerVelX=0
    END IF
END IF

bPlayerOnGround=FALSE
IF fPlayerVelY<0 THEN ' Moving up
    IF GetLevelTile(fNewPlayerPosX+0.0,fNewPlayerPosY)<>ASC(".") OR GetLevelTile(fNewPlayerPosX+0.9,fNewPlayerPosY)<>ASC(".") THEN
        fNewPlayerPosY=INT(fNewPlayerPosY)+1
        fPlayerVelY=0
    END IF
ELSE IF fPlayerVelY>0 THEN ' Moving down
    IF GetLevelTile(fNewPlayerPosX+0.0,fNewPlayerPosY+1.0)<>ASC(".") OR GetLevelTile(fNewPlayerPosX+0.9,fNewPlayerPosY+1.0)<>ASC(".") THEN
        fNewPlayerPosY=INT(fNewPlayerPosY)
        fPlayerVelY=0
        bPlayerOnGround=TRUE ' Player has a solid surface underfoot
    END IF
END IF

' Apply new position
fPlayerPosX=fNewPlayerPosX
fPlayerPosY=fNewPlayerPosY

' Link camera to player position
fCameraPosX=fPlayerPosX
fCameraPosY=fPlayerPosY

' Draw Level
nTileWidth=16
nTileHeight=16
nVisibleTilesX=22
nVisibleTilesY=15

' Calculate top-leftmost visible tile
fOffsetX=fCameraPosX-nVisibleTilesX/2.0
fOffsetY=fCameraPosY-nVisibleTilesY/2.0

' Clamp camera to game boundaries
IF fOffsetX<0 THEN fOffsetX=0
IF fOffsetY<0 THEN fOffsetY=0
IF fOffsetX>nLevelWidth-nVisibleTilesX THEN fOffsetX=nLevelWidth-nVisibleTilesX
IF fOffsetY>nLevelHeight-nVisibleTilesY THEN fOffsetY=nLevelHeight-nVisibleTilesY

' Get offsets for smooth movement
fTileOffsetX=(fOffsetX-INT(fOffsetX))*nTileWidth
fTileOffsetY=(fOffsetY-INT(fOffsetY))*nTileHeight

' Draw visible tile map
IF redraw THEN
    UpdateScreen(0,0,nVisibleTilesX-1,nVisibleTilesY-1)
    redraw=FALSE
    iOldOffsetX=INT(fOffsetX)
    iOldOffsetY=INT(fOffsetY)
END IF

' Scroll the tile map
IF INT(fOffsetY)>iOldOffsetY THEN
    count=INT(fOffsetY)-iOldOffsetY
    SCROLL TILEMAP UP count
    UpdateScreen(0,nVisibleTilesY-count-1,nVisibleTilesX-1,nVisibleTilesY-1)
    iOldOffsetY=INT(fOffsetY)
ELSE IF INT(fOffsetY)<iOldOffsetY THEN
    count=iOldOffsetY-INT(fOffsetY)
    SCROLL TILEMAP DOWN count
    UpdateScreen(0,0,nVisibleTilesX-1,count-1)
    iOldOffsetY=INT(fOffsetY)
END IF

IF INT(fOffsetX)>iOldOffsetX THEN
    count=INT(fOffsetX)-iOldOffsetX
    SCROLL TILEMAP LEFT count
    UpdateScreen(nVisibleTilesX-count-1,0,nVisibleTilesX-1,nVisibleTilesY-1)
    iOldOffsetX=INT(fOffsetX)
ELSE IF INT(fOffsetX)<iOldOffsetX THEN
    count=iOldOffsetX-INT(fOffsetX)
    SCROLL TILEMAP RIGHT count
    UpdateScreen(0,0,count-1,nVisibleTilesY-1)
    iOldOffsetX=INT(fOffsetX)
END IF

' Swap buffers
SHIFT TILEMAP fTileOffsetX,fTileOffsetY-4
SWAP BUFFERS

' Move player
SPRITE 2 MOVE (fPlayerPosX-fOffsetX)*nTileWidth-8,(fPlayerPosY-fOffsetY)*nTileHeight-8

' Print stats
LOCATE 2,TEXT.ROWS-1
INK RGB(255,255,255)
HIGHLIGHT RGB(0,0,0)
PRINT " " USING$("###.#", fPlayerPosX) ", " USING$("###.#", fPlayerPosY) " "
HIGHLIGHT 0

PAUSE:GOTO tick

FUNCTION UpdateScreen(x1,y1,x2,y2)
    SHARE fOffsetX,fOffsetY
    FOR x=x1 TO x2
        FOR y=y1 TO y2
            sTileID=GetLevelTile(x+fOffsetX,y+fOffsetY)
            PUT TILE x,y,TCHAR(sTileID)
        NEXT y
    NEXT x
END FUNCTION
