Cannon Fodder

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