const ixs = collect(12, ix => ix);
shipImages = ixs.map(pad2).map(nStr => loadImage("ship_00" + nStr + ".png"));
shipImagesGray = ixs.map(ix => ix + 12).map(pad2).map(nStr => loadImage("ship_00" + nStr + ".png"));
bulletImages = [0, 1, 2, 3].map(pad2).map(nStr => loadImage("bullet_00" + nStr + ".png"));
explosionImages = [1, 2, 3, 4, 5].map(n => loadImage("explosion" + n + ".png"));
health: loadImage("powerup_health.png"),
power: loadImage("powerup_power.png"),
shield: loadImage("powerup_shield.png")
weapons = createWeapons();
"water": loadImage("tile_water.png")
const imageKeys = generateImageKeys();
imageKeys.forEach(key => {
terrainImageMap[key] = loadImage(key + ".png");
createCanvas(windowWidth, windowHeight);
const ix = round(random(0, 11));
return createAndAddShip()
terrainImage = drawMapToImage();
image(terrainImage, 0, 0);
for (let ship of ships) {
for (let bullet of bullets) {
for (let powerup of powerups) {
for (let explosion of explosions) {
updateExplosion(explosion);
for (let powerup of powerups) {
for (let bullet of bullets) {
for (let ship of ships) {
for (let explosion of explosions) {
powerups = powerups.filter(p => !p.isDead);
ships = ships.filter(s => !s.isDead);
bullets = bullets.filter(b => !b.isDead);
explosions = explosions.filter(e => !e.isDead);
function drawMapToImage() {
const g = createGraphics(width, height);
function drawTerrain(g) {
const cellSize = 16 * tileScale;
const numCols = ceil(width / cellSize);
const numRows = ceil(height / cellSize);
for (let rowIx = 0; rowIx < numRows; rowIx++) {
for (let colIx = 0; colIx < numCols; colIx++) {
const n = g.noise(colIx * noiseScale, rowIx * noiseScale);
function getCellAt(cIx, rIx) {
function inGridBounds(x, y) {
return x >= 0 && y >= 0 && x < numCols && y < numRows;
function getNextCell(gridPos, xOff, yOff) {
const [x, y] = [gridPos.colIx + xOff, gridPos.rowIx + yOff];
if (inGridBounds(x, y)) {
if (rowIx === 16 && colIx === 1) {
if (type === "g" || type === "e") {
const upCell = getNextCell(gridPos, 0, -1);
const rightCell = getNextCell(gridPos, 1, 0);
const upRightCell = getNextCell(gridPos, 1, -1);
const upLeftCell = getNextCell(gridPos, -1, -1);
const downRightCell = getNextCell(gridPos, 1, 1);
const downLeftCell = getNextCell(gridPos, -1, 1);
const downCell = getNextCell(gridPos, 0, 1);
const leftCell = getNextCell(gridPos, -1, 0);
(upCell && rightCell && ![upCell.type, rightCell.type].includes(upRightCell.type)) ||
(upCell && leftCell && ![upCell.type, leftCell.type].includes(upLeftCell.type)) ||
(downCell && rightCell && ![downCell.type, rightCell.type].includes(downRightCell.type)) ||
(downCell && leftCell && ![downCell.type, leftCell.type].includes(downLeftCell.type))
const neighbours = [upCell, upRightCell, rightCell, downRightCell, downCell, downLeftCell, leftCell, upLeftCell].map(c => c?.type || type);
imageCode = [type, ...neighbours].join("");
const neighbours = [upCell, rightCell, downCell, leftCell].map(c => c?.type || type);
imageCode = [type, ...neighbours].join("");
if (imageCode === "ggggg" || imageCode === "eeeee") {
const abstractTileName = random() < 0.9 ? random(["xxxxx1", "xxxxx2"]) : random(["xxxxxtree1", "xxxxxtree2", "xxxxxtree3", "xxxxxhouse1", "xxxxxhouse2", "xxxxxflag"]);
imageCode = abstractTileName.replace(/x/g, imageCode[0]);
const x = colIx * cellSize;
const y = rowIx * cellSize;
let img = terrainImageMap[imageCode];
const replacementCode = (imageCode[0].repeat(5)) + "1";
img = terrainImageMap[replacementCode];
console.error("no img: ", {
g.textAlign(CENTER, CENTER);
function collect(num, callbackFn) {
for (let i = 0; i < num; i++) {
function createAndAddShip() {
const ix = round(random(0, 11));
const [img, imgGray] = [shipImages[ix], shipImagesGray[ix]];
const vel = p5.Vector.random2D().mult(random(1, 3));
const weapon = weapons[0]
pos: randomScreenPosition(),
angle: vel.heading() + PI / 2,
spawnAndMaybeRemoveOlder(ship, ships, 30);
function randomScreenPosition() {
return createVector(random(width), random(height));
function updateShip(ship) {
ship.vel.rotate(radians(map(noise(frameCount / 100), 0, 1, -3, 3)));
ship.angle = ship.vel.heading() + PI / 2
if (isFarFromScreen(ship.pos)) {
ship.pos = randomScreenPosition();
const angleOffsets = ship.weapon.numBullets === 3 ? [-PI / 10, 0, PI / 10] : [0];
angleOffsets.forEach(angleOffset => createAndAddBullet(ship, angleOffset));
ship.tookDamageRecently--;
ship.shield = max(0, ship.shield - 0.1)
function isOffscreen(pos) {
return (pos.x < 0 || pos.y < 0 || pos.x > width || pos.y > height);
function updateBullet(bullet) {
bullet.pos.add(bullet.vel);
if (isOffscreen(bullet.pos)) {
for (let ship of ships) {
if (bullet.owner === ship || ship.isDead || bullet.isDead) {
const collisionDistanceThreshold = 10 * ship.size + 3 * bullet.size;
if (p5.Vector.dist(bullet.pos, ship.pos) < collisionDistanceThreshold) {
bounceBulletOffShip(bullet, ship, collisionDistanceThreshold);
ship.shield = max(0, ship.shield - 30);
ship.tookDamageRecently = 6;
createAndAddExplosion(ship);
function bounceBulletOffShip(bullet, ship, collisionDistanceThreshold) {
const surfaceNormal = p5.Vector.sub(bullet.pos, ship.pos).normalize();
bullet.vel.reflect(surfaceNormal.copy());
bullet.angle = bullet.vel.heading() + PI / 2;
const offsetPos = surfaceNormal.setMag(collisionDistanceThreshold);
bullet.pos.set(p5.Vector.add(ship.pos, offsetPos))
function updatePowerup(powerup) {
powerup.pos.add(powerup.vel);
powerup.angle += powerup.rotation;
for (let ship of ships) {
if (p5.Vector.dist(powerup.pos, ship.pos) < 50) {
processReceivedPowerup(ship, powerup);
function updateExplosion(explosion) {
explosion.pos.add(explosion.vel);
explosion.angle += explosion.rotation;
if (frameCount % 10 === 0) {
if (explosion.animIx > explosion.images.length - 1) {
explosion.img = explosion.images[explosion.animIx];
function createAndAddBullet(ship, angleOffset) {
const newVel = ship.vel.copy().add(ship.vel.copy().setMag(4 * ship.size));
newVel.rotate(angleOffset);
angle: ship.vel.heading() + PI / 2 + angleOffset,
img: ship.weapon.bulletImage,
spawnAndMaybeRemoveOlder(bullet, bullets, 100);
function createAndAddPowerup() {
const [type, img] = random(Object.entries(powerupImageMap));
pos: randomScreenPosition(),
vel: p5.Vector.random2D().mult(random(0.1, 0.5)),
rotation: random(0.01, 0.02) * random([-1, 1]),
spawnAndMaybeRemoveOlder(powerup, powerups, 50);
function createAndAddExplosion(ship) {
vel: p5.Vector.random2D().mult(random(0.1, 0.5)),
rotation: random(0.01, 0.02) * random([-1, 1]),
spawnAndMaybeRemoveOlder(explosion, explosions, 50);
function spawnAndMaybeRemoveOlder(entity, list, maxInGame) {
if (list.length > maxInGame) {
function drawShip(ship) {
translate(ship.pos.x, ship.pos.y);
scale(ship.size, ship.size);
if (ship.tookDamageRecently > 0) {
image(ship.imgGray, 0, 0);
text(ship.health, 20, 20)
function drawShipShield(ship) {
const fillAlpha = map(sin(frameCount / 10), -1, 1, 20, 70, true);
circle(0, 0, ship.size * 10);
function drawEntity(ent) {
translate(ent.pos.x, ent.pos.y);
scale(ent.size, ent.size)
function collect(n, fn) {
for (let i = 0; i < n; i++) {
for (let i = 0; i < n; i++) {
return n > 9 ? n + "" : "0" + n
function processReceivedPowerup(ship, powerup) {
if (powerup.type === "power") {
ship.weapon = random(weapons);
if (powerup.type === "health") {
if (powerup.type === "shield") {
function createWeapons() {
bulletImage: bulletImages[0],
bulletImage: bulletImages[1],
bulletImage: bulletImages[2],
bulletImage: bulletImages[3],
function isFarFromScreen(pos) {
return pos.x < -margin || pos.x > width + margin || pos.y < -margin || pos.y > height + margin;
function generateImageKeys() {
function buildCode(n, charForZero, charForOne) {
for (let counter = 0; counter < 4; counter++) {
const chars = [charForZero, charForOne];
return result.map(val => chars[val]).join("")
for (let type of ["g", "e"]) {
for (let i = 1; i < 16; i++) {
if (i === 1 || i === 2 || i === 4 || i === 5 || i === 15 || i === 8 || i === 10) {
const code = type + buildCode(i, "w", type);
["g", "e"].forEach(landType => {
].forEach(abstractType => {
const concreteCode = abstractType.replace(/x/g, landType);
imageKeys.push(concreteCode);
["xxxxxtree1", "xxxxxtree2", "xxxxxtree3", "xxxxxhouse1",
"xxxxxhouse2", "xxxxxflag", "xxxxxlamppost"
].forEach(abstractCode => {
const code = abstractCode.replace(/[x]/g, landType);