IMPORT_RENDERING_MODULE()
// True when two rectangles overlap. Only uses its parameters.
PROCEDURE overlaps(ax, ay, aw, ah, bx, by, bw, bh) {
RETURN ax < bx + bw AND ax + aw > bx AND ay < by + bh AND ay + ah > by
}
// ================= Level data =================
// Each level is on one line: [ [rects], [lava], [coins], door, spawn ]
levels ← []
names ← ["First Steps", "Mind the Gap", "Lava Floor", "Hot Ground", "The Climb", "Stepping Stones", "Gauntlet", "The Summit" , "Lava River" , "Low Ceiling" , "Zig Zag Down" , "Pillars" , "Narrow Bridge" , "Spike Alley" , "Spiral" , "Lava Steps" , "Pit Parkour" , "Grand Finale"]
// 1: First Steps
APPEND(levels, [ [[0,360,400,40],[140,300,70,12]], [], [[175,275],[330,335]], [355,320], [30,330] ])
// 2: Mind the Gap
APPEND(levels, [ [[0,360,130,40],[210,360,190,40]], [], [[170,300],[300,335]], [355,320], [30,330] ])
// 3: Lava Floor
APPEND(levels, [ [[0,360,100,40],[150,320,60,12],[250,280,60,12],[340,240,60,12]], [[100,385,300,15]], [[180,295],[280,255]], [355,200], [30,330] ])
// 4: Hot Ground
APPEND(levels, [ [[0,360,400,40],[120,290,60,12],[220,230,60,12]], [[150,345,50,15],[260,345,50,15]], [[150,265],[250,205],[340,335]], [355,320], [30,330] ])
// 5: The Climb
APPEND(levels, [ [[0,360,120,40],[160,320,50,12],[60,265,50,12],[160,210,50,12],[260,160,50,12],[340,110,60,12]], [], [[185,295],[85,240],[185,185],[285,135]], [355,70], [30,330] ])
// 6: Stepping Stones
APPEND(levels, [ [[0,360,60,40],[340,360,60,40],[90,320,40,12],[170,280,40,12],[250,320,40,12]], [[60,365,280,35]], [[110,295],[190,225],[270,295]], [355,320], [20,330] ])
// 7: Gauntlet
APPEND(levels, [ [[0,360,400,40],[90,290,60,12],[200,240,60,12],[300,190,60,12]], [[120,345,40,15],[200,345,40,15],[280,345,40,15]], [[110,265],[220,215],[320,165],[370,335]], [355,320], [30,330] ])
// 8: The Summit
APPEND(levels, [ [[0,360,60,40],[100,320,40,12],[180,280,40,12],[100,230,40,12],[180,180,40,12],[260,130,40,12],[340,90,60,12]], [[60,380,340,20]], [[120,295],[200,255],[120,205],[200,155],[280,105]], [355,50], [20,330] ])
// 9: Lava River
APPEND(levels, [ [[0,360,100,40],[300,360,100,40],[130,330,50,12],[220,300,50,12]], [[100,380,200,20]], [[155,305],[245,275]], [355,320], [30,330] ])
// 10: Low Ceiling
APPEND(levels, [ [[0,360,400,40],[40,300,50,12],[100,270,200,20]], [[150,345,40,15],[230,345,40,15]], [[210,320],[200,245],[340,335]], [355,320], [10,330] ])
// 11: Zig Zag Down
APPEND(levels, [ [[0,100,80,12],[130,150,60,12],[230,200,60,12],[130,250,60,12],[30,300,60,12],[0,360,400,40]], [[150,345,40,15],[250,345,40,15]], [[155,125],[255,175],[155,225],[55,275],[340,335]], [355,320], [20,70] ])
// 12: Pillars
APPEND(levels, [ [[0,360,400,40],[100,300,30,60],[190,260,30,100],[280,300,30,60]], [[130,345,60,15],[220,345,60,15]], [[115,275],[205,235],[295,275],[360,335]], [355,320], [20,330] ])
// 13: Narrow Bridge
APPEND(levels, [ [[0,360,50,40],[110,320,40,12],[200,280,40,12],[290,240,40,12],[360,200,40,12]], [[50,385,350,15]], [[130,295],[220,255],[310,215]], [365,160], [10,330] ])
// 14: Spike Alley
APPEND(levels, [ [[0,360,400,40]], [[80,345,25,15],[140,345,25,15],[200,345,25,15],[260,345,25,15],[320,345,25,15]], [[92,300],[212,300],[332,300]], [355,320], [20,330] ])
// 15: Spiral
APPEND(levels, [ [[0,360,60,40],[100,320,50,12],[190,280,50,12],[280,240,50,12],[190,190,50,12],[100,150,50,12],[20,110,50,12]], [[60,385,340,15]], [[125,295],[215,255],[305,215],[215,165],[125,125]], [30,70], [10,330] ])
// 16: Lava Steps
APPEND(levels, [ [[0,360,40,40],[80,330,40,12],[140,300,40,12],[200,270,40,12],[260,300,40,12],[320,330,40,12],[370,360,30,40]], [[40,385,330,15]], [[100,305],[160,275],[220,245],[280,275],[340,305]], [370,320], [10,330] ])
// 17: Pit Parkour
APPEND(levels, [ [[0,360,120,40],[160,360,80,40],[280,360,120,40],[60,300,50,12],[170,300,60,12],[290,290,50,12]], [[120,385,40,15],[240,385,40,15]], [[85,275],[200,275],[315,265]], [355,320], [20,330] ])
// 18: Grand Finale
APPEND(levels, [ [[0,360,70,40],[110,320,40,12],[190,280,40,12],[270,240,40,12],[200,190,40,12],[120,150,40,12],[40,110,40,12],[130,70,40,12],[230,70,40,12],[330,110,70,12]], [[70,385,330,15]], [[130,295],[210,255],[290,215],[220,165],[140,125],[60,85],[150,45],[250,45]], [355,70], [10,330] ])
// ================= Tuning =================
accel ← 0.6
friction ← 0.5
maxSpeed ← 3.6
gravity ← 0.55
jumpPower ← 10.5
maxFall ← 12
cutMult ← 3
hangMult ← 0.5
hangSpeed ← 1.5
coyoteFrames ← 6
bufferFrames ← 6
// ================= State =================
// Everything used inside loops is created here first, because variables
// created inside a block are gone when the block ends.
w ← 18
h ← 28
x ← 0
y ← 0
vx ← 0
vy ← 0
facing ← 1
onGround ← false
coyote ← 0
jumpBuffer ← 0
prevJump ← false
deaths ← 0
levelNum ← 1
frame ← 0
done ← false
dead ← false
level ← []
rects ← []
lavas ← []
coins ← []
door ← []
spawn ← []
gotCoin ← []
r ← []
c ← []
nCoins ← 0
collected ← 0
atDoor ← false
left ← false
right ← false
jumpHeld ← false
gNow ← 0
i ← 1
shadowY ← 9999
dist ← 0
sw ← 0
p ← 0
bob ← 0
col ← "#453465"
doorOpen ← false
// ================= Main game: one pass per level =================
REPEAT UNTIL levelNum > LENGTH(levels) {
// ----- Load the level -----
level ← levels[levelNum]
rects ← level[1]
lavas ← level[2]
coins ← level[3]
door ← level[4]
spawn ← level[5]
nCoins ← LENGTH(coins)
gotCoin ← []
REPEAT nCoins TIMES {
APPEND(gotCoin, false)
}
x ← spawn[1]
y ← spawn[2]
vx ← 0
vy ← 0
onGround ← false
coyote ← 0
jumpBuffer ← 0
facing ← 1
frame ← 0
done ← false
REPEAT UNTIL done {
frame ← frame + 1
// ----- Input -----
left ← IS_KEY_DOWN("ArrowLeft") OR IS_KEY_DOWN("a")
right ← IS_KEY_DOWN("ArrowRight") OR IS_KEY_DOWN("d")
jumpHeld ← IS_KEY_DOWN("ArrowUp") OR IS_KEY_DOWN("w") OR IS_KEY_DOWN("Space")
// ----- Horizontal acceleration and friction -----
IF (left AND NOT right) {
vx ← vx - accel
facing ← 0 - 1
}
ELSE {
IF (right AND NOT left) {
vx ← vx + accel
facing ← 1
}
ELSE {
IF (vx > friction) {
vx ← vx - friction
}
ELSE {
IF (vx < 0 - friction) {
vx ← vx + friction
}
ELSE {
vx ← 0
}
}
}
}
IF (vx > maxSpeed) {
vx ← maxSpeed
}
IF (vx < 0 - maxSpeed) {
vx ← 0 - maxSpeed
}
// ----- Jump buffering and coyote time -----
IF (jumpHeld AND NOT prevJump) {
jumpBuffer ← bufferFrames
}
prevJump ← jumpHeld
IF (onGround) {
coyote ← coyoteFrames
}
ELSE {
IF (coyote > 0) {
coyote ← coyote - 1
}
}
IF (jumpBuffer > 0 AND coyote > 0) {
vy ← 0 - jumpPower
jumpBuffer ← 0
coyote ← 0
onGround ← false
}
IF (jumpBuffer > 0) {
jumpBuffer ← jumpBuffer - 1
}
// ----- Gravity: variable jump height and apex hang -----
gNow ← gravity
IF (vy < 0 AND NOT jumpHeld) {
gNow ← gravity * cutMult
}
IF (jumpHeld AND vy > 0 - hangSpeed AND vy < hangSpeed) {
gNow ← gravity * hangMult
}
vy ← vy + gNow
IF (vy > maxFall) {
vy ← maxFall
}
// ----- Move horizontally, then collide -----
x ← x + vx
IF (x < 0) {
x ← 0
vx ← 0
}
IF (x > 400 - w) {
x ← 400 - w
vx ← 0
}
i ← 1
REPEAT LENGTH(rects) TIMES {
r ← rects[i]
IF (overlaps(x, y, w, h, r[1], r[2], r[3], r[4])) {
IF (vx > 0) {
x ← r[1] - w
}
IF (vx < 0) {
x ← r[1] + r[3]
}
vx ← 0
}
i ← i + 1
}
// ----- Move vertically, then collide -----
y ← y + vy
onGround ← false
i ← 1
REPEAT LENGTH(rects) TIMES {
r ← rects[i]
IF (overlaps(x, y, w, h, r[1], r[2], r[3], r[4])) {
IF (vy > 0) {
y ← r[2] - h
onGround ← true
}
ELSE {
y ← r[2] + r[4]
}
vy ← 0
}
i ← i + 1
}
// ----- Death: pits and lava -----
dead ← false
IF (y > 450) {
dead ← true
}
i ← 1
REPEAT LENGTH(lavas) TIMES {
r ← lavas[i]
IF (overlaps(x, y, w, h, r[1] + 2, r[2] + 3, r[3] - 4, r[4] - 3)) {
dead ← true
}
i ← i + 1
}
IF (dead) {
x ← spawn[1]
y ← spawn[2]
vx ← 0
vy ← 0
coyote ← 0
jumpBuffer ← 0
deaths ← deaths + 1
}
// ----- Coins -----
collected ← 0
i ← 1
REPEAT nCoins TIMES {
c ← coins[i]
IF (NOT gotCoin[i] AND overlaps(x, y, w, h, c[1] - 8, c[2] - 8, 16, 16)) {
gotCoin[i] ← true
}
IF (gotCoin[i]) {
collected ← collected + 1
}
i ← i + 1
}
// ----- Door -----
doorOpen ← collected = nCoins
atDoor ← overlaps(x, y, w, h, door[1], door[2], 30, 40)
IF (atDoor AND doorOpen) {
done ← true
}
// ================= Drawing =================
CLEAR_SCREEN()
DRAW_RECT(0, 0, 400, 400, "#e0d4f5")
// Platforms, and find the surface below the player for the shadow
shadowY ← 9999
i ← 1
REPEAT LENGTH(rects) TIMES {
r ← rects[i]
DRAW_RECT(r[1], r[2], r[3], r[4], "#453465")
DRAW_RECT(r[1], r[2], r[3], 4, "#8a6bbf")
IF (x + w > r[1] AND x < r[1] + r[3]) {
IF (r[2] ≥ y + h AND r[2] < shadowY) {
shadowY ← r[2]
}
}
i ← i + 1
}
// Drop shadow: shrinks as the player rises
IF (NOT onGround AND shadowY < 9999) {
dist ← shadowY - (y + h)
sw ← w + 6 - dist / 10
IF (sw < 4) {
sw ← 4
}
DRAW_RECT(x + w / 2 - sw / 2, shadowY - 3, sw, 3, "#2e2247")
}
// Lava
i ← 1
REPEAT LENGTH(lavas) TIMES {
r ← lavas[i]
DRAW_RECT(r[1], r[2], r[3], r[4], "#d63f5c")
DRAW_RECT(r[1], r[2], r[3], 3, "#ff8fa3")
i ← i + 1
}
// Coins (they bob up and down)
p ← frame MOD 40
IF (p > 20) {
p ← 40 - p
}
bob ← p / 5
i ← 1
REPEAT nCoins TIMES {
IF (NOT gotCoin[i]) {
c ← coins[i]
DRAW_CIRCLE(c[1], c[2] - bob, 8, "#ffb84d")
DRAW_CIRCLE(c[1], c[2] - bob, 4, "#fff1c9")
}
i ← i + 1
}
// Door: dim when locked, glowing when open
IF (doorOpen) {
col ← "#ffe08a"
}
ELSE {
col ← "#7a6a96"
}
DRAW_RECT(door[1] - 3, door[2] - 3, 36, 43, "#453465")
DRAW_RECT(door[1], door[2], 30, 40, col)
IF (atDoor AND NOT doorOpen) {
DRAW_TEXT("Need all coins!", door[1] - 50, door[2] - 22, 14, "#453465")
}
// Player
DRAW_RECT(x, y, w, h, "#e0457b")
IF (facing > 0) {
DRAW_RECT(x + w - 8, y + 6, 6, 6, "#ffffff")
}
ELSE {
DRAW_RECT(x + 2, y + 6, 6, 6, "#ffffff")
}
// HUD: one icon per coin, level number, deaths
i ← 1
REPEAT nCoins TIMES {
IF (gotCoin[i]) {
col ← "#ffb84d"
}
ELSE {
col ← "#b9a6d9"
}
DRAW_CIRCLE(12 + i * 18, 14, 6, col)
i ← i + 1
}
DRAW_TEXT("Level", 270, 6, 14, "#453465")
DRAW_TEXT(levelNum, 318, 6, 14, "#453465")
DRAW_TEXT("Deaths", 270, 24, 14, "#453465")
DRAW_TEXT(deaths, 328, 24, 14, "#453465")
IF (frame < 120) {
DRAW_TEXT(names[levelNum], 150, 50, 18, "#453465")
}
CLOCK_TICK(60)
}
// ----- Level complete flash -----
REPEAT 45 TIMES {
DRAW_RECT(60, 160, 280, 70, "#453465")
DRAW_TEXT("Level complete!", 100, 180, 24, "#ffe08a")
CLOCK_TICK(60)
}
levelNum ← levelNum + 1
}
// ================= Win screen =================
CLEAR_SCREEN()
DRAW_RECT(0, 0, 400, 400, "#e0d4f5")
DRAW_TEXT("You beat every level!", 50, 150, 26, "#453465")
DRAW_TEXT("Total deaths", 110, 210, 20, "#453465")
DRAW_TEXT(deaths, 270, 210, 20, "#453465")
CLOCK_TICK(60)
Platformer
From simple things we do great things
csp