Skip to content

Instantly share code, notes, and snippets.

@thquinn
Created July 15, 2018 16:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thquinn/82943cb33c28cad87d8aca04dd70fe38 to your computer and use it in GitHub Desktop.
Save thquinn/82943cb33c28cad87d8aca04dd70fe38 to your computer and use it in GitHub Desktop.
Cooperative asteroids for the Math Square exhibit at MoMath
/* MoMath Math Square Behavior
*
* Title: Asterangles
* Description: cooperative asteroids, form an equilateral triangle with an
* asteroid and another player to fire
* Framework: P5
* Author: Tom Quinn <thquinn.github.io>
* Created: 2018-07
* Status: works
*/
const SIZE = 576;
Math.dist2 = function(x1, y1, x2, y2) {
return Math.pow(Math.min(Math.abs(x1 - x2), SIZE - Math.abs(x1 - x2)), 2) +
Math.pow(Math.min(Math.abs(y1 - y2), SIZE - Math.abs(y1 - y2)), 2);
}
Math.dist = function(x1, y1, x2, y2) {
return Math.sqrt(Math.dist2(x1, y1, x2, y2));
}
Math.mod = function(n, m) {
return ((n%m)+m)%m;
};
Math.randFloat = function (min, max) {
return Math.random() * (max - min) + min;
};
function detectCollision(x1, y1, x2, y2, rad1, rad2) {
let totalRad = rad1 + rad2;
if (Math.abs(x2 - x1) > totalRad) {
return false;
}
if (Math.abs(y2 - y1) > totalRad) {
return false;
}
return Math.dist2(x1, y1, x2, y2) < totalRad * totalRad;
}
function HSVtoRGB(h, s, v) {
var r, g, b, i, f, p, q, t;
if (arguments.length === 1) {
s = h.s, v = h.v, h = h.h;
}
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
import P5Behavior from 'p5beh';
const pb = new P5Behavior();
const ASTEROID_COUNT = 6;
const ASTEROID_HEALTH_CONSTANT = 25;
const ASTEROID_HEALTH_MULTIPLIER = 1.25;
const ASTEROID_REGEN_TIME = 450;
const ASTEROID_REGEN_RATE = 1/40.0;
const ASTEROID_MIN_RADIUS = 15;
const ASTEROID_SPAWN_ACCEL = 1.1;
const ASTEROID_SPEED = .3;
const ASTEROID_SEGMENTS = 12;
const ASTEROID_VARIANCE = .25;
const TRIANGLE_MIN_RANGE = 40;
const TRIANGLE_MAX_RANGE = 200;
const TRIANGLE_MAX_VARIANCE = 1.33; // longest side can be 133% of shortest side
const TRIANGLE_MIN_DAMAGE = 20 / 60.0;
const TRIANGLE_MAX_DAMAGE = 40 / 60.0;
const TRIANGLE_INDICATOR_MULTIPLIER = 1.5;
const PLAYER_RADIUS = 15;
const PLAYER_TIMEOUT = 180;
var frame = 0;
var stars = [];
class Star {
constructor(cx, cy, rgb, speed, scale, width) {
this.cx = cx;
this.cy = cy;
this.rgb = rgb;
this.speed = speed;
this.scale = scale;
this.width = width;
this.theta = Math.randFloat(0, 2 * Math.PI);
this.percent = .05;
}
updateAndDraw(p) {
this.percent *= this.speed;
let length = 5 + 60 * this.percent;
p.strokeWeight(this.width);
let alpha = this.percent < 1 ? Math.min(this.percent * 5, 1) : Math.max(0, 2 - this.percent);
p.stroke('rgba(' + this.rgb[0] + ', ' + this.rgb[1] + ', ' + this.rgb[2] + ', ' + alpha + ')');
let cx = this.cx + Math.cos(this.theta) * SIZE / 2 * this.percent * this.scale;
let cy = this.cy + Math.sin(this.theta) * SIZE / 2 * this.percent * this.scale;
let dx = cx + length * Math.cos(this.theta) * this.scale;
let dy = cy + length * Math.sin(this.theta) * this.scale;
p.line(cx, cy, dx, dy);
p.noStroke();
}
}
class Asteroid {
constructor(x, y, radius) {
let health = ASTEROID_HEALTH_CONSTANT + radius * ASTEROID_HEALTH_MULTIPLIER;
this.health = [health, health];
this.regenTimer = 0;
this.x = x;
this.y = y;
this.theta = Math.randFloat(0, 2 * Math.PI);
this.dx = Math.cos(this.theta) * ASTEROID_SPEED;
this.dy = Math.sin(this.theta) * ASTEROID_SPEED;
this.dTheta = 0.025;
this.radius = radius;
this.segmentAngleOffsets = [];
this.segmentRadiusOffsets = [];
for (let i = 0; i < ASTEROID_SEGMENTS; i++) {
this.segmentAngleOffsets.push(Math.randFloat(-ASTEROID_VARIANCE, ASTEROID_VARIANCE);
this.segmentRadiusOffsets.push(Math.randFloat(-ASTEROID_VARIANCE, ASTEROID_VARIANCE);
}
}
draw(p, offX, offY) {
p.beginShape();
for (let i = 0; i < ASTEROID_SEGMENTS; i++) {
let angle = this.theta + (2 * Math.PI / ASTEROID_SEGMENTS) * (i + this.segmentAngleOffsets[i]);
let radius = this.radius * (1 + this.segmentRadiusOffsets[i]);
let x = this.x + Math.cos(angle) * radius + offX;
let y = this.y + Math.sin(angle) * radius + offY;
p.vertex(x, y);
}
p.endShape(p.CLOSE);
}
dist(x, y) {
return Math.hypot(Math.min(Math.abs(this.x - x), SIZE - Math.abs(this.x - x)), Math.min(Math.abs(this.y - y), SIZE - Math.abs(this.y - y)));
}
dist2(a) {
return Math.dist2(this.x, this.y, a.x, a.y);
}
collide(p, other) {
let distanceVect = p.createVector(other.x - this.x, other.y - this.y);
let distanceVectMag = distanceVect.mag();
let theta = distanceVect.heading();
let minDistance = this.radius + other.radius;
if (distanceVectMag > minDistance) {
return;
}
let sine = Math.sin(theta);
let cosine = Math.cos(theta);
let bTemp = [p.createVector(0, 0), p.createVector(0, 0)];
bTemp[1].x = cosine * distanceVect.x + sine * distanceVect.y;
bTemp[1].y = cosine * distanceVect.y - sine * distanceVect.x;
let vTemp = [p.createVector(0, 0), p.createVector(0, 0)];
vTemp[0].x = cosine * this.dx + sine * this.dy;
vTemp[0].y = cosine * this.dy - sine * this.dx;
vTemp[1].x = cosine * other.dx + sine * other.dy;
vTemp[1].y = cosine * other.dy - sine * other.dx;
let vFinal = [p.createVector(0, 0), p.createVector(0, 0)];
let m = this.radius, otherM = other.radius;
vFinal[0].x = ((m - otherM) * vTemp[0].x + 2 * otherM * vTemp[1].x) / (m + otherM);
vFinal[0].y = vTemp[0].y;
vFinal[1].x = ((otherM - m) * vTemp[1].x + 2 * m * vTemp[0].x) / (m + otherM);
vFinal[1].y = vTemp[1].y;
bTemp[0].x += vFinal[0].x;
bTemp[1].x += vFinal[1].x;
let bFinal = [p.createVector(0, 0), p.createVector(0, 0)];
bFinal[0].x = cosine * bTemp[0].x - sine * bTemp[0].y;
bFinal[0].y = cosine * bTemp[0].y + sine * bTemp[0].x;
bFinal[1].x = cosine * bTemp[1].x - sine * bTemp[1].y;
bFinal[1].y = cosine * bTemp[1].y + sine * bTemp[1].x;
other.x = this.x + bFinal[1].x;
other.y = this.y + bFinal[1].y;
this.x += bFinal[0].x;
this.y += bFinal[0].y;
this.dx = cosine * vFinal[0].x - sine * vFinal[0].y;
this.dy = cosine * vFinal[0].y + sine * vFinal[0].x;
other.dx = cosine * vFinal[1].x - sine * vFinal[1].y;
other.dy = cosine * vFinal[1].y + sine * vFinal[1].x;
let distanceCorrection = (minDistance-distanceVectMag)/10;
this.x -= distanceCorrection * Math.cos(theta);
this.y -= distanceCorrection * Math.sin(theta);
other.x += distanceCorrection * Math.cos(theta);
other.y += distanceCorrection * Math.sin(theta);
}
}
var playerTimeouts = new Map();
pb.preload = function (p) {
}
pb.setup = function (p) {
this.textSize(32);
this.textAlign(this.LEFT, this.TOP);
};
var playerToPlayerDistances = new Map();
var playerToAsteroidDistances = new Map();
var triangles;
var deadAsteroidIndices = new Set();
function update(floor, p) {
frame++;
// Update and draw stars.
if (frame % 4 == 0) {
stars.push(new Star(SIZE / 2, SIZE / 2, [50, 50, 50], 1.05, 1, 4));
}
for (let i = stars.length - 1; i >= 0; i--) {
stars[i].updateAndDraw(p);
if (stars[i].percent >= 2) {
stars.splice(i, 1);
}
}
// Update asteroids.
for (let asteroid of asteroids) {
asteroid.regenTimer++;
if (asteroid.regenTimer > ASTEROID_REGEN_TIME) {
asteroid.health[0] = Math.min(asteroid.health[0] + ASTEROID_REGEN_RATE, asteroid.health[1]);
}
asteroid.x = Math.mod(asteroid.x + asteroid.dx, SIZE);
asteroid.y = Math.mod(asteroid.y + asteroid.dy, SIZE);
asteroid.theta += asteroid.dTheta;
}
for (let i = 0; i < asteroids.length; i++) {
for (let j = i + 1; j < asteroids.length; j++) {
let totalRadius = asteroids[i].radius + asteroids[j].radius;
if (asteroids[i].dist2(asteroids[j]) > totalRadius * totalRadius) {
continue;
}
asteroids[i].collide(p, asteroids[j]);
}
}
// Update players and find all distances.
playerToPlayerDistances.clear();
playerToAsteroidDistances.clear();
triangles = [];
deadAsteroidIndices.clear();
for (let u of floor.users) {
if (playerTimeouts.has(u.id)) {
playerTimeouts.set(u.id, playerTimeouts.get(u.id) - 1);
if (playerTimeouts.get(u.id) == 0) {
playerTimeouts.delete(u.id);
}
}
for (let i = 0; i < asteroids.length; i++) {
let asteroid = asteroids[i];
let distance = Math.hypot(u.x - asteroid.x, u.y - asteroid.y);
if (distance < PLAYER_RADIUS + asteroid.radius) {
playerTimeouts.set(u.id, PLAYER_TIMEOUT);
break;
}
if (distance > TRIANGLE_MAX_RANGE * TRIANGLE_INDICATOR_MULTIPLIER) {
continue;
}
if (!playerToAsteroidDistances.has(u.id)) {
playerToAsteroidDistances.set(u.id, new Map());
}
playerToAsteroidDistances.get(u.id).set(i, distance);
playerToAsteroidDistances.set(key, distance);
}
if (!playerTimeouts.has(u.id)) {
for (let u2 of floor.users) {
if (u2.id >= u.id) {
continue;
}
if (playerTimeouts.has(u2.id)) {
continue;
}
let key = u.id + ',' + u2.id;
let distance = Math.hypot(u.x - u2.x, u.y - u2.y);
if (distance > TRIANGLE_MAX_RANGE * TRIANGLE_INDICATOR_MULTIPLIER) {
continue;
}
playerToPlayerDistances.set(key, distance);
}
}
}
// Find all triangles.
// TODO: Why can't I iterate through this map?!
for (var k of Array.from(playerToPlayerDistances.keys())) {
let v = playerToPlayerDistances.get(k);
if (v > TRIANGLE_MAX_RANGE) {
continue;
}
let ids = k.split(',').map(Number);
if (!playerToAsteroidDistances.has(ids[0]) || !playerToAsteroidDistances.has(ids[1])) {
continue;
}
for (let key of Array.from(playerToAsteroidDistances.get(ids[0]).keys())) {
let distance = playerToAsteroidDistances.get(ids[0]).get(key);
if (distance <= TRIANGLE_MAX_RANGE &&
playerToAsteroidDistances.get(ids[1]).has(key) &&
playerToAsteroidDistances.get(ids[1]).get(key) <= TRIANGLE_MAX_RANGE) {
let distance2 = playerToAsteroidDistances.get(ids[1]).get(key);
let triangle = [v, distance, distance2].sort((a, b) => a - b);
let variance = triangle[2] / triangle[0];
if (variance <= TRIANGLE_MAX_VARIANCE) {
// Damage the asteroid.
let averageRange = (v + distance + distance2) / 3;
let rangePercent = (averageRange - TRIANGLE_MIN_RANGE) / (TRIANGLE_MAX_RANGE - TRIANGLE_MIN_RANGE);
rangePercent = Math.max(0, Math.min(rangePercent, 1));
let variancePercent = (variance - 1) / (1 - TRIANGLE_MAX_VARIANCE);
let damagePercent = (2 - variancePercent - rangePercent);
let damage = TRIANGLE_MIN_DAMAGE + damagePercent * (TRIANGLE_MAX_DAMAGE - TRIANGLE_MIN_DAMAGE);
let asteroid = asteroids[key];
asteroid.health[0] -= damage;
asteroid.regenTimer = 0;
if (asteroid.health[0] <= 0) {
deadAsteroidIndices.add(key);
}
// Add to draw calls.
let u1 = floor.usersByID[ids[0]];
let u2 = floor.usersByID[ids[1]];
triangles.push([[u1.x, u1.y], [u2.x, u2.y], [asteroid.x, asteroid.y], damagePercent]);
}
}
}
}
for (let i of Array.from(deadAsteroidIndices).sort((a, b) => b - a)) {
let asteroid = asteroids[i];
for (let i = 0; i < asteroid.radius; i++) {
let rgb = HSVtoRGB(Math.randFloat(.075, .125), 1, 1);
rgb = [rgb.r, rgb.g, rgb.b];
stars.push(new Star(asteroid.x, asteroid.y, rgb, Math.randFloat(1.05, 1.1), .25, 2));
}
asteroids.splice(i, 1);
if (asteroid.radius < ASTEROID_MIN_RADIUS) {
if (asteroids.length == 0) {
setup();
}
break;
}
let theta = Math.randFloat(0, Math.PI);
let x1 = asteroid.x + Math.cos(theta) * asteroid.radius * .5;
let y1 = asteroid.y + Math.sin(theta) * asteroid.radius * .5;
let x2 = asteroid.x + Math.cos(theta + Math.PI) * asteroid.radius * .5;
let y2 = asteroid.y + Math.sin(theta + Math.PI) * asteroid.radius * .5;
// TODO: inherit dx and dy
let a1 = new Asteroid(x1, y1, asteroid.radius / 2);
let a2 = new Asteroid(x2, y2, asteroid.radius / 2);
a1.dx = asteroid.dx * ASTEROID_SPAWN_ACCEL;
a1.dy = asteroid.dy * ASTEROID_SPAWN_ACCEL;
a2.dx = asteroid.dx * ASTEROID_SPAWN_ACCEL;
a2.dy = asteroid.dy * ASTEROID_SPAWN_ACCEL;
asteroids.push(a1, a2);
}
}
pb.draw = function (floor, p) {
this.clear();
update(floor, p);
for (let u of floor.users) {
if (playerTimeouts.has(u.id) {
this.fill(255, 0, 0);
} else {
this.fill(210, 210, 255);
}
this.ellipse(u.x, u.y, PLAYER_RADIUS * 2);
}
this.noFill();
// Draw asteroids.
for (let asteroid of asteroids) {
this.strokeWeight(2);
this.stroke('rgba(255, 255, 255, 0.25)');
this.ellipse(asteroid.x, asteroid.y, 8);
this.strokeWeight(4);
let gb = (asteroid.health[0] / asteroid.health[1]) * 255;
this.stroke(255, gb, gb);
asteroid.draw(p, 0, 0);
let margin = asteroid.radius * (1 + ASTEROID_VARIANCE);
if (asteroid.x < margin) {
asteroid.draw(p, SIZE, 0);
} else if ((SIZE - asteroid.x) < margin) {
asteroid.draw(p, -SIZE, 0);
}
if (asteroid.y < margin) {
asteroid.draw(p, 0, SIZE);
} else if ((SIZE - asteroid.y) < margin) {
asteroid.draw(p, 0, -SIZE);
}
}
this.noStroke();
// Draw triangles.
let alphaFlash = frame % 6;
if (alphaFlash > 3) {
alphaFlash = 6 - alphaFlash;
}
alphaFlash = (alphaFlash - 5) / 50;
this.noStroke();
for (let triangle of triangles) {
let alpha = .1 + .15 * triangle[3];
if (triangle[3] == 1) {
alpha += .1;
}
alpha += alphaFlash;
this.fill('rgba(255, 220, 0, ' + alpha + ')');
this.beginShape();
this.vertex(triangle[0][0], triangle[0][1]);
this.vertex(triangle[1][0], triangle[1][1]);
this.vertex(triangle[2][0], triangle[2][1]);
this.endShape(p.CLOSE);
}
};
var asteroids;
function setup() {
asteroids = [];
let attempts = 0;
while (asteroids.length < ASTEROID_COUNT && attempts < 1000) {
attempts++;
let x = Math.randFloat(0, SIZE);
let y = Math.randFloat(0, SIZE);
let radius = Math.randFloat(50, 60);
let good = true;
for (let asteroid of asteroids) {
let totalRadius = radius + asteroid.radius;
if (asteroid.dist(x, y) < totalRadius) {
good = false;
break;
}
}
if (!good) {
continue;
}
asteroids.push(new Asteroid(x, y, radius));
}
playerTimeouts.clear();
}
setup();
export const behavior = {
title: 'Asterangles',
init: pb.init.bind(pb),
frameRate: 'animate',
render: pb.render.bind(pb),
numGhosts: 0
};
export default behavior
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment