Multi-player game.
Save an animated GIF of your victory!
Log in to post a comment.
// LL 2021
Canvas.setpenopacity(-1);
const player1_angle = 45; // min=0 max=90 step=0.01
const player1_force = 0.5; // min=0 max=1 step=0.01
const player2_angle = 45; // min=0 max=90 step=0.01
const player2_force = 0.5; // min=0 max=1 step=0.01
const iterations = 20; // min=0 max=200 step=1
var iterations_t = iterations;
const level = 1; // min=1 max=100 step=1
const style = 1; /// min=0 max=1 step=1 (Polygons (fast),Polygons (slow))
const numOctaves = 10; /// min=1 max=20 step=1
const height = 100; // min=1 max=300 step=1
const tt = new Turtle();
var polygons = null;
var draw_queue = new DrawQueue();
var rng = new RNG(level);
var noise = new SimplexNoise(rng.nextFloat() * 2345056334);
const canvas_size = 99;
const floor = 2;
var water_level = 50;
const steps = 2000;
const sea_width = 0.3;
const player_dist = 0.55;
const player_width = 0.1;
console.clear();
function walk(i, t) {
if (i == 0) {
iterations_t = Math.floor(iterations * t);
rng = new RNG(level);
noise = new SimplexNoise(rng.nextFloat() * 2345056334);
polygons = new Polygons();
traces = [];
explosions = [];
cannons = []
cannons.push(new Cannon(-1, player1_angle/360, player1_force));
cannons.push(new Cannon( 1, -0.5 - player2_angle/360, player2_force));
water_level = Math.min(water_level, Math.min(cannons[0].oy-2, cannons[1].oy-2));
cannon_balls = [];
cannons.forEach(c => cannon_balls.push(c.createBall()));
for (var it=0; it<iterations_t; it++) {
cannon_balls.forEach(b => b.update());
checkCollisions();
explosions.forEach(e => e.update());
}
queueMountain();
cannon_balls.forEach(b => b.draw());
explosions.forEach(e => e.draw());
queueTraces();
cannons.forEach(c => c.draw());
queueWater(t);
queueStars(t);
}
draw_queue.drawNext();
return draw_queue.length() > 0;
}
function queueMountain() {
const points = [];
points.push([canvas_size, canvas_size]);
points.push([-canvas_size, canvas_size]);
for (var x=-canvas_size; x<=canvas_size; x+=canvas_size*2/steps) {
var xx = getX(x);
var y = getHeightFBM(xx);
y = digSea(x, y);
y = digCrater(x, y);
y = Math.max(y, floor + (Math.sin(x)+1) * 0.5);
points.push([x, canvas_size - floor - y]);
}
draw_queue.add(points, -Math.PI/4, 1);
}
function queueWater(t) {
const points = [];
points.push([canvas_size, canvas_size]);
points.push([-canvas_size, canvas_size]);
const start1 = rng.nextFloat() * canvas_size + t * Math.PI * 2;// + iterations_t * 0.01;
const start2 = rng.nextFloat() * canvas_size - t * Math.PI * 2;// - iterations_t * 0.01;
for (var x=-canvas_size; x<=canvas_size; x+=canvas_size*2/steps) {
var y = water_level + Math.sin(start1+x*0.25) + Math.sin(start2+x*0.5);
points.push([x, canvas_size - y]);
}
draw_queue.add(points, Math.PI/4, 0.3);
}
function queueStars(t) {
const r = [0.125, 0.5];
for (var star=0; star<30; star++) {
var points = [];
const start = rng.nextFloat() + t;
const cr = (rng.nextFloat() + t) % 1;
var cx = -canvas_size + ((cr*0.1+rng.nextFloat()) % 1) * canvas_size * 2;
var cy = rng.nextFloat() * canvas_size*0.8 + 2*r[1] - canvas_size;
if (cr > 0.5) continue;
for (var a=0, index=0; a<1; a+=0.1, index=(index+1)&1) {
var x = cx + Math.cos((start + a) * Math.PI*2) * r[index];
var y = cy + Math.sin((start + a) * Math.PI*2) * r[index];
points.push([x, y]);
}
draw_queue.add(points);
}
}
function queueTraces() {
traces.forEach(t => {
var points = [];
points.push([t[0], canvas_size-t[1]]);
points.push([t[2], canvas_size-t[3]]);
draw_queue.add(points);
});
}
function getHeightFBM(x) {
const H = 0.8;
const fbm_ = 0.5 + 0.2 * fbm(x/canvas_size/2*0.6, H);
return fbm_ * height;
}
function getX(x) {
var xx = Math.abs(x/canvas_size);
if (xx < player_dist) return x;
if (xx > player_dist+player_width) return x;
return Math.sign(x) * (player_dist + player_width*0.5) * canvas_size;
}
function digSea(x, y) {
var xx = Math.abs(x/canvas_size);
if (xx < sea_width) return y - (height*0.6) * (1-Math.pow(xx / sea_width, 5));
return y;
}
function digCrater(x, y) {
explosions.forEach(e => {
const mult = 0.8;
if ((x >= e.x - e.radius*mult) && (x <= e.x + e.radius*mult)) {
const a = (x - e.x) / e.radius*mult;
y = Math.min(y, e.y - e.radius*mult * Math.cos(a));
}
});
return y;
}
function fbm(x, H)
{
var t = 0;
for (var i=0; i<numOctaves; i++)
{
const f = Math.pow(2.0, i);
const a = Math.pow(f, -H);
t += a * noise.noise2D([f * x, 10]);
}
return t;
}
function checkCollisions() {
if (cannon_balls[0].exploded) return;
if (cannon_balls[1].exploded) return;
const x0 = cannon_balls[0].x;
const y0 = cannon_balls[0].y;
const r = cannon_balls[0].radius;
const x1 = cannon_balls[1].x;
const y1 = cannon_balls[1].y;
if (Math.hypot(x1-x0, y1-y0) < r*2) {
cannon_balls[0].exploded = true;
cannon_balls[1].exploded = true;
explosionAt((x0+x1) / 2, (y0+y1) / 2);
}
}
function addTrace(x1, y1, x2, y2) {
const dash = 0.7
x2 = x1 + (x2-x1) * dash;
y2 = y1 + (y2-y1) * dash;
traces.push([x1, y1, x2, y2]);
}
function explosionAt(x, y) {
explosions.push(new Explosion(x, y));
}
class Explosion {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = 2;
this.max_radius = 10;
}
update() {
if (this.radius > this.max_radius) return;
this.radius *= 1.1;
}
draw() {
const points = [];
for (var a=0, index=0; a<1; a+=0.05, index=(index+1)&1) {
var x = this.x + Math.cos(a * Math.PI*2) * this.radius * Math.max(index, 0.3);
var y = this.y + Math.sin(a * Math.PI*2) * this.radius * Math.max(index, 0.3);
points.push([x, canvas_size - y]);
}
draw_queue.add(points, Math.PI/2, 2);
}
}
class CannonBall {
constructor(x, y, angle, force) {
this.radius = 2;
this.x = x;
this.y = y;
this.px = x - force * 10 * Math.cos(angle * Math.PI*2);
this.py = y - force * 10 * Math.sin(angle * Math.PI*2);
this.exploded = false;
}
update() {
if (this.exploded) return;
var vx = this.x - this.px, vy = this.y - this.py;
this.px = this.x; this.py = this.y;
const gravity = 0.1;
const friction = 0.97;
vy -= gravity;
vx *= friction;
vy *= friction;
this.x += vx; this.y += vy;
addTrace(this.px, this.py, this.x, this.y);
const th = digSea(this.x, getHeightFBM(getX(this.x)));
if (this.y - this.radius < th) {
explosionAt(this.x, this.y);
this.exploded = true;
} else this.exploded = this.y < water_level;
}
draw() {
if (this.exploded) return;
var cx = this.x;
var cy = this.y;
var br = this.radius;
const points = [];
for (var a=0; a<1; a+=0.1) {
var x = cx + Math.cos(a * Math.PI*2) * br;
var y = cy + Math.sin(a * Math.PI*2) * br;
points.push([x, canvas_size - y]);
}
draw_queue.add(points, this.dir * Math.PI/4, 2);
}
}
class Cannon {
constructor(dir, angle, force) {
this.dir = dir;
this.angle = angle;
this.force = force;
this.wheel_radius = 5;
this.barrel_length = 12;
this.ox = dir * (player_dist + player_width * 0.5) * canvas_size;
var xx = getX(this.ox);
this.oy = getHeightFBM(xx);
}
createBall() {
const br = this.barrel_length;
const bx = this.ox + br * Math.cos(this.angle * Math.PI*2);
const by = this.oy + this.wheel_radius + br * Math.sin(this.angle * Math.PI*2);
return new CannonBall(bx, by, this.angle, this.force);
}
draw() {
var cx = this.ox;
var cy = this.oy;
var wr = this.wheel_radius;
// Wheel
{
const points = [];
for (var a=0; a<1; a+=0.05) {
var x = cx + Math.cos(a * Math.PI*2) * wr;
var y = cy + Math.sin(a * Math.PI*2) * wr + wr;
points.push([x, canvas_size - y]);
}
draw_queue.add(points, this.dir * Math.PI/4, 2);
}
// Barrel
{
var r = [this.barrel_length, this.barrel_length/6];
const points = [];
for (var a=0.02, index=0; a<0.5; a+=0.4, index++) {
var x = cx + Math.cos((this.angle + a) * Math.PI*2) * r[index];
var y = cy + Math.sin((this.angle + a) * Math.PI*2) * r[index] + wr;
points.push([x, canvas_size - y]);
var x = cx + Math.cos((this.angle - a) * Math.PI*2) * r[index];
var y = cy + Math.sin((this.angle - a) * Math.PI*2) * r[index] + wr;
points.unshift([x, canvas_size - y]);
}
draw_queue.add(points, this.dir * this.angle, 1);
}
}
}
function DrawQueue(){
class DQ {
constructor() {
this.list = [];
}
add(points, hatching_angle=NaN, hatching_spacing=NaN) {
this.list.push([points, hatching_angle, hatching_spacing]);
}
drawNext() {
if (this.list.length > 0) {
if (style == 0) {
const points = this.list.shift()[0];
tt.jump(points[points.length-1]);
points.forEach(p=>tt.goto(p));
} else {
const hatching_angle = this.list[0][1];
const hatching_spacing = this.list[0][2];
const points = this.list.shift()[0];
const p1 = polygons.create();
p1.addPoints(...points);
if (!isNaN(hatching_angle) && !isNaN(hatching_spacing)) p1.addHatching(hatching_angle, hatching_spacing);
p1.addOutline();
polygons.draw(tt, p1, true);
}
}
}
length() { return this.list.length; }
}
return new DQ();
}
// Random with seed
function RNG(_seed) {
class RNGc {
constructor(_seed) {
this.m = 0x80000000; this.a = 1103515245; this.c = 12345; /* LCG using GCC's constants */
this.state = _seed ? _seed : Math.floor(Math.random() * (this.m - 1));
}
nextFloat() { // returns in range [0,1]
this.state = (this.a * this.state + this.c) % this.m;
return this.state / (this.m - 1);
}
}
return new RNGc(_seed);
}
////////////////////////////////////////////////////////////////
// Polygon Clipping utility code - Created by Reinder Nijhoff 2019
// https://turtletoy.net/turtle/a5befa1f8d
////////////////////////////////////////////////////////////////
function Polygons() {
const polygonList = [];
const Polygon = class {
constructor() {
this.cp = []; // clip path: array of [x,y] pairs
this.dp = []; // 2d lines [x0,y0],[x1,y1] to draw
this.aabb = []; // AABB bounding box
}
addPoints(...points) {
// add point to clip path and update bounding box
let xmin = 1e5, xmax = -1e5, ymin = 1e5, ymax = -1e5;
(this.cp = [...this.cp, ...points]).forEach( p => {
xmin = Math.min(xmin, p[0]), xmax = Math.max(xmax, p[0]);
ymin = Math.min(ymin, p[1]), ymax = Math.max(ymax, p[1]);
});
this.aabb = [(xmin+xmax)/2, (ymin+ymax)/2, (xmax-xmin)/2, (ymax-ymin)/2];
}
addSegments(...points) {
// add segments (each a pair of points)
points.forEach(p => this.dp.push(p));
}
addOutline() {
for (let i = 0, l = this.cp.length; i < l; i++) {
this.dp.push(this.cp[i], this.cp[(i + 1) % l]);
}
}
draw(t) {
for (let i = 0, l = this.dp.length; i < l; i+=2) {
t.jump(this.dp[i]), t.goto(this.dp[i + 1]);
}
}
addHatching(a, d) {
const tp = new Polygon();
tp.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);
const dx = Math.sin(a) * d, dy = Math.cos(a) * d;
const cx = Math.sin(a) * 200, cy = Math.cos(a) * 200;
for (let i = 0.5; i < 150 / d; i++) {
tp.dp.push([dx * i + cy, dy * i - cx], [dx * i - cy, dy * i + cx]);
tp.dp.push([-dx * i + cy, -dy * i - cx], [-dx * i - cy, -dy * i + cx]);
}
tp.boolean(this, false);
this.dp = [...this.dp, ...tp.dp];
}
inside(p) {
let int = 0; // find number of i ntersection points from p to far away
for (let i = 0, l = this.cp.length; i < l; i++) {
if (this.segment_intersect(p, [0.1, -1000], this.cp[i], this.cp[(i + 1) % l])) {
int++;
}
}
return int & 1; // if even your outside
}
boolean(p, diff = true) {
// bouding box optimization by ge1doot.
if (Math.abs(this.aabb[0] - p.aabb[0]) - (p.aabb[2] + this.aabb[2]) >= 0 &&
Math.abs(this.aabb[1] - p.aabb[1]) - (p.aabb[3] + this.aabb[3]) >= 0) return this.dp.length > 0;
// polygon diff algorithm (narrow phase)
const ndp = [];
for (let i = 0, l = this.dp.length; i < l; i+=2) {
const ls0 = this.dp[i];
const ls1 = this.dp[i + 1];
// find all intersections with clip path
const int = [];
for (let j = 0, cl = p.cp.length; j < cl; j++) {
const pint = this.segment_intersect(ls0, ls1, p.cp[j], p.cp[(j + 1) % cl]);
if (pint !== false) {
int.push(pint);
}
}
if (int.length === 0) {
// 0 intersections, inside or outside?
if (diff === !p.inside(ls0)) {
ndp.push(ls0, ls1);
}
} else {
int.push(ls0, ls1);
// order intersection points on line ls.p1 to ls.p2
const cmpx = ls1[0] - ls0[0];
const cmpy = ls1[1] - ls0[1];
int.sort( (a,b) => (a[0] - ls0[0]) * cmpx + (a[1] - ls0[1]) * cmpy -
(b[0] - ls0[0]) * cmpx - (b[1] - ls0[1]) * cmpy);
for (let j = 0; j < int.length - 1; j++) {
if ((int[j][0] - int[j+1][0])**2 + (int[j][1] - int[j+1][1])**2 >= 0.001) {
if (diff === !p.inside([(int[j][0]+int[j+1][0])/2,(int[j][1]+int[j+1][1])/2])) {
ndp.push(int[j], int[j+1]);
}
}
}
}
}
return (this.dp = ndp).length > 0;
}
//port of http://paulbourke.net/geometry/pointlineplane/Helpers.cs
segment_intersect(l1p1, l1p2, l2p1, l2p2) {
const d = (l2p2[1] - l2p1[1]) * (l1p2[0] - l1p1[0]) - (l2p2[0] - l2p1[0]) * (l1p2[1] - l1p1[1]);
if (d === 0) return false;
const n_a = (l2p2[0] - l2p1[0]) * (l1p1[1] - l2p1[1]) - (l2p2[1] - l2p1[1]) * (l1p1[0] - l2p1[0]);
const n_b = (l1p2[0] - l1p1[0]) * (l1p1[1] - l2p1[1]) - (l1p2[1] - l1p1[1]) * (l1p1[0] - l2p1[0]);
const ua = n_a / d;
const ub = n_b / d;
if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
return [l1p1[0] + ua * (l1p2[0] - l1p1[0]), l1p1[1] + ua * (l1p2[1] - l1p1[1])];
}
return false;
}
};
return {
list: () => polygonList,
create: () => new Polygon(),
draw: (turtle, p, addToVisList=true) => {
for (let j = 0; j < polygonList.length && p.boolean(polygonList[j]); j++);
p.draw(turtle);
if (addToVisList) polygonList.push(p);
}
};
}
////////////////////////////////////////////////////////////////
// Simplex Noise utility code. Created by Reinder Nijhoff 2020
// https://turtletoy.net/turtle/6e4e06d42e
// Based on: http://webstaff.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf
////////////////////////////////////////////////////////////////
function SimplexNoise(seed = 1) {
const grad = [ [1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0],
[1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1],
[0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1] ];
const perm = new Uint8Array(512);
const F2 = (Math.sqrt(3) - 1) / 2, F3 = 1/3;
const G2 = (3 - Math.sqrt(3)) / 6, G3 = 1/6;
const dot2 = (a, b) => a[0] * b[0] + a[1] * b[1];
const sub2 = (a, b) => [a[0] - b[0], a[1] - b[1]];
const dot3 = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const sub3 = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
class SimplexNoise {
constructor(seed = 1) {
for (let i = 0; i < 512; i++) {
perm[i] = i & 255;
}
for (let i = 0; i < 255; i++) {
const r = (seed = this.hash(i+seed)) % (256 - i) + i;
const swp = perm[i];
perm[i + 256] = perm[i] = perm[r];
perm[r + 256] = perm[r] = swp;
}
}
noise2D(p) {
const s = dot2(p, [F2, F2]);
const c = [Math.floor(p[0] + s), Math.floor(p[1] + s)];
const i = c[0] & 255, j = c[1] & 255;
const t = dot2(c, [G2, G2]);
const p0 = sub2(p, sub2(c, [t, t]));
const o = p0[0] > p0[1] ? [1, 0] : [0, 1];
const p1 = sub2(sub2(p0, o), [-G2, -G2]);
const p2 = sub2(p0, [1-2*G2, 1-2*G2]);
let n = Math.max(0, 0.5-dot2(p0, p0))**4 * dot2(grad[perm[i+perm[j]] % 12], p0);
n += Math.max(0, 0.5-dot2(p1, p1))**4 * dot2(grad[perm[i+o[0]+perm[j+o[1]]] % 12], p1);
n += Math.max(0, 0.5-dot2(p2, p2))**4 * dot2(grad[perm[i+1+perm[j+1]] % 12], p2);
return 70 * n;
}
noise3D(p) {
const s = dot3(p, [F3, F3, F3]);
const c = [Math.floor(p[0] + s), Math.floor(p[1] + s), Math.floor(p[2] + s)];
const i = c[0] & 255, j = c[1] & 255, k = c[2] & 255;
const t = dot3(c, [G3, G3, G3]);
const p0 = sub3(p, sub3(c, [t, t, t]));
const [o0, o1] = p0[0] >= p0[1] ? p0[1] >= p0[2] ? [ [1, 0, 0], [1, 1, 0] ]
: p0[0] >= p0[2] ? [ [1, 0, 0], [1, 0, 1] ]
: [ [0, 0, 1], [1, 0, 1] ]
: p0[1] < p0[2] ? [ [0, 0, 1], [0, 1, 1] ]
: p0[0] < p0[2] ? [ [0, 1, 0], [0, 1, 1] ]
: [ [0, 1, 0], [1, 1, 0] ];
const p1 = sub3(sub3(p0, o0), [-G3, -G3, -G3]);
const p2 = sub3(sub3(p0, o1), [-2*G3, -2*G3, -2*G3]);
const p3 = sub3(p0, [1-3*G3, 1-3*G3, 1-3*G3]);
let n = Math.max(0, 0.6-dot3(p0, p0))**4 * dot3(grad[perm[i+perm[j+perm[k]]] % 12], p0);
n += Math.max(0, 0.6-dot3(p1, p1))**4 * dot3(grad[perm[i+o0[0]+perm[j+o0[1]+perm[k+o0[2]]]] % 12], p1);
n += Math.max(0, 0.6-dot3(p2, p2))**4 * dot3(grad[perm[i+o1[0]+perm[j+o1[1]+perm[k+o1[2]]]] % 12], p2);
n += Math.max(0, 0.6-dot3(p3, p3))**4 * dot3(grad[perm[i+1+perm[j+1+perm[k+1]]] % 12], p3);
return 32 * n;
}
hash(i) {
i = 1103515245 * ((i >> 1) ^ i);
const h32 = 1103515245 * (i ^ (i>>3));
return h32 ^ (h32 >> 16);
}
}
return new SimplexNoise(seed);
}