Created
July 15, 2018 16:38
-
-
Save thquinn/82943cb33c28cad87d8aca04dd70fe38 to your computer and use it in GitHub Desktop.
Cooperative asteroids for the Math Square exhibit at MoMath
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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