COTR
An infinite descent game.
Can you get past the second win screen? Or get a win without taking damage (and thus without the "But at what cost?" message)?
A bit of lore: Initially, I thought of the circle as somehow transcending beyond the mortal plane by collecting coins. Now this doesn't make any sense: transcendence does not come from riches, and depending on who you ask, it even comes from a lack of riches! Plus, the only "transcendence" achieved is moving faster, while being just as vulnerable to the "mortal" world—not very transcedent at all.
Yet, two things have remained of that initial ideation: the name, COTR, as a portmanteau of "coin" and "transcend", and the circle glowing brighter as more "coins", for the lack of better word, are collected.
The map generation algorithm was inspired by the BBC Article on Entombed's maze generation.
let playerX = 200
let keyCooldown = 0
let gridOffset = 0
let grid = []
let lost = true
let crashless = true
let main = true
let hp = 1
let won = 0
let coins = 0
let speedUp = 1
let mood = 0
let moodChangeTime = 0
let invincibleTime = 0
function setup() {
createCanvas(416, 400);
playerX = width / 2
}
function draw() {
background(220);
fill(0, 0, 0)
rectMode(CENTER)
noStroke()
if (!lost) {
moveGrid()
movePlayer()
}
drawGrid()
drawPlayer()
drawScore()
if (lost) {
restartScreen()
}
}
function drawScore() {
if (coins > 0) {
noStroke()
fill(255, 255, 255)
let filled = coins / 512
rect(width, 0, filled * -width * 2, 17)
if (filled > 1) {
rect(0, 0, 17, (filled - 1) * height * 2)
}
if (filled > 2) {
rect(0, height, (filled - 2) * width * 2, 17)
}
if (filled > 3) {
rect(width, height, 17, (filled - 3) * height * 2)
}
fill(0)
textFont('monospace')
textSize(10)
textAlign(RIGHT, TOP)
text(coins, width, 0)
if (keyIsDown(17)) {
text(speedUp.toFixed(2), width, 12)
}
textAlign(LEFT, TOP)
text(moods[mood], 0, 0)
}
fill(255, 0, 0)
rect(0, 0, max(1 - hp, 0) * -width * 2, 3)
}
function restartScreen() {
fill(255, 0, 0)
stroke(255, 255, 255)
strokeWeight(5)
strokeJoin(ROUND)
textFont('monospace')
textSize(40)
textAlign(CENTER, CENTER)
if (main) {
text("$ COTR", 200, 150)
textSize(35)
}
text("> START", 200, 200)
// gridOffset += speedUp * 0.8
if (mouseIsPressed || keyIsDown(32)) {
mouseIsPressed = false
grid = []
for (let i = 0; i < height / 32 + 9; i ++) {
grid[i] = []
}
grid[grid.length - 9] = ['?']
grid[grid.length - 8] = ['X']
grid[grid.length - 6] = ['.']
grid[grid.length - 5] = ['*']
grid[grid.length - 3] = ['!']
main = false
lost = false
crashless = true
hp = 1
won = 0
coins = 0
speedUp = 2
mood = 0
}
}
function moveGrid() {
if (gridOffset <= -16) {
gridOffset += 32
grid.shift()
grid.push(createGridRow())
speedUp = speedUp * 0.94
}
let speed = (1 + speedUp + coins/200) + 1 * keyIsDown(DOWN_ARROW)
gridOffset -= min(deltaTime * 0.06 * speed, 32)
}
function drawGrid() {
for (let i = 0; i < grid.length; i ++) {
let rowOffset = (width - (grid[i].length - 1) * 32) / 2
let squareY = i * 32 + gridOffset
for (let j = 0; j < grid[i].length; j ++) {
let squareX = j * 32 + rowOffset
if (grid[i][j] == 'X') {
fill(0, 0, 0)
noStroke()
rect(squareX, squareY, 32)
if (i == 1 && abs(playerX - squareX) < 24 && invincibleTime < 0) {
crashless = false
//hp -= 0.2 * deltaTime * 0.06
//playerX += (playerX < squareX ? -1 : 1) * deltaTime * 0.06 * 3
hp -= (32 - abs(playerX - squareX)) / 40
playerX += (playerX < squareX ? -1 : 1) * (32 - abs(playerX - squareX))
}
}
if (grid[i][j] == '*') {
fill(255, 255, 0)
stroke(200 - abs(gridOffset) / 2)
strokeWeight(5)
circle(squareX, squareY, 16)
if (squareY > -8 && squareY < 40 && abs(playerX - squareX) < 32) {
coins ++
speedUp += 0.7
grid[i][j] = ' '
}
}
if (grid[i][j] == '?' || grid[i][j] == '.' || grid[i][j] == '!' || grid[i][j] == '$' || grid[i][j] == '&') {
fill(255, 0, 0)
stroke(255, 255, 255)
strokeWeight(3)
textFont('monospace')
textSize(20)
textAlign(CENTER, CENTER)
if (grid[i][j] == '?') {
text("> Arrow keys to move", squareX, squareY)
} else if (grid[i][j] == '.') {
text("> Collect coins to go faster", squareX, squareY)
} else if (grid[i][j] == '!') {
text("> Good luck", squareX, squareY)
} else if (grid[i][j] == '$') {
text("> You win!", squareX, squareY)
} else if (grid[i][j] == '&') {
text("> But at what cost?", squareX, squareY)
}
}
}
}
}
function movePlayer() {
if (hp < 0) {
lost = true
} else {
hp = min(hp + 0.005 * deltaTime * 0.06, 1)
}
if (keyIsDown(LEFT_ARROW)) {
playerX -= deltaTime * 0.06 * 5
}
if (keyIsDown(RIGHT_ARROW)) {
playerX += deltaTime * 0.06 * 5
}
if (playerX < 16) {
playerX = 16
}
if (playerX > width - 16) {
playerX = width - 16
}
}
function drawPlayer() {
if (lost) {
fill(255, 0, 0)
} else {
fill(coins, coins, coins - 255)
}
if (invincibleTime > 0) {
stroke(invincibleTime / 2, invincibleTime, invincibleTime)
} else {
noStroke()
}
if (hp >= 1 || lost || frameCount % 3 < hp * 3) {
circle(playerX, 24, 30)
}
}
let moods = [' ', 'care', 'coins', 'tunnels', 'farlands', 'farlands']
let moodProbabilities = [ // x[last mood I][new mood I] => probability
eq([70, 3, 3, 0, 0, 0]),
eq([5, 60, 3, 5, 0, 0]),
eq([5, 0, 70, 0, 0, 0]),
eq([6, 0, 0, 80, 2, 0]),
eq([0, 0, 0, 0, 0, 1]),
eq([0, 0, 2, 0, 0, 80]),
]
let objects = [' ', 'X', '*']
let objectProbabilities = [ // x[mood I][type last I][type new I] => probability
[ // ' '
eq([80, 5, 9]),
eq([80, 1, 9]),
eq([80, 5, 9]),
],
[ // 'care'
eq([80, 10, 5]),
eq([80, 10, 5]),
eq([80, 10, 5]),
],
[ // coins
eq([80, 2, 20]),
eq([80, 0, 20]),
eq([80, 0, 10]),
],
[ // tunnels
eq([80, 2, 9]),
eq([80, 80, 0]),
eq([80, 0, 9]),
],
[ // farlands (1)
eq([20, 1, 0]),
eq([1, 1, 0]),
eq([1, 0, 0]),
],
[ // farlands (2)
eq([1, 0, 1]),
eq([0, 1, 0]),
eq([3, 0, 1]),
],
]
function eq(weights) {
let sum = weights.reduce((a,b) => a + b)
return weights.map(x => x / sum)
}
function pickProbability(probabilities) {
let r = random()
return max(probabilities.findIndex(x => (r -= x) <= 0), 0)
}
function createGridRow() {
invincibleTime --
moodChangeTime --
if (coins > 32 && moodChangeTime < 0) {
let oldMood = mood
mood = pickProbability(moodProbabilities[mood])
if (mood != oldMood) {
moodChangeTime = 30 / random(1, 10)
}
}
let row = []
if (coins - floor(won / 100) * 512 > 512) {
won ++
if (won % 400 == 1) {
return ['$']
} else if (won % 400 == 2 && !crashless) {
return ['&']
} else {
for (let j = 0; j < width / 32; j ++) {
row[j] = '*'
}
return row
}
}
for (let j = 0; j < width / 32; j ++) {
let lastType = max(objects.indexOf(grid[grid.length - 1][j]), 0)
let newType = pickProbability(objectProbabilities[mood][lastType])
row[j] = objects[newType]
}
return row
}(Originally seen at https://editor.p5js.org/bojidar-bg/sketches/zPCPYQkjM)
Browse more articles?
← MIDI Visualizer Experiments tagged p5 (16/85) Triangle space partitioning →
← Rhythm 1 Experiments tagged game (2/11) Circle lockpick →
← MIDI Visualizer Experiments on this site (16/85) Triangle space partitioning →