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); }