2026-09-28 17:20:17 -05:00
|
|
|
/* ISLE SLINGERS — renderer.js
|
|
|
|
|
* Canvas 2D renderer: procedural art, camera, particles, screen shake.
|
|
|
|
|
* Consumes state.events (visual effects) and reports them via onEvent for audio.
|
|
|
|
|
*/
|
|
|
|
|
(function (root, factory) {
|
|
|
|
|
var api = factory();
|
|
|
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
|
|
|
else root.ISLE_RENDERER = api;
|
|
|
|
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
var VIEW_W = 1280, VIEW_H = 720;
|
|
|
|
|
|
|
|
|
|
function mulberry32(a) {
|
|
|
|
|
return function () {
|
|
|
|
|
a |= 0; a = a + 0x6D2B79F5 | 0;
|
|
|
|
|
var t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
|
|
|
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
|
|
|
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Renderer(canvas, opts) {
|
|
|
|
|
this.canvas = canvas;
|
|
|
|
|
this.ctx = canvas.getContext('2d');
|
|
|
|
|
this.onEvent = (opts && opts.onEvent) || function () {};
|
|
|
|
|
this.particles = [];
|
|
|
|
|
this.rng = mulberry32(1234);
|
|
|
|
|
this.fit();
|
|
|
|
|
this._onResize = this.fit.bind(this);
|
|
|
|
|
if (typeof window !== 'undefined') window.addEventListener('resize', this._onResize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Renderer.prototype.fit = function () {
|
|
|
|
|
var dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
|
|
|
|
|
this.dpr = dpr;
|
|
|
|
|
this.scale = 1;
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
var s = Math.min(window.innerWidth / VIEW_W, window.innerHeight / VIEW_H);
|
|
|
|
|
this.scale = s;
|
|
|
|
|
this.canvas.style.width = (VIEW_W * s) + 'px';
|
|
|
|
|
this.canvas.style.height = (VIEW_H * s) + 'px';
|
|
|
|
|
}
|
|
|
|
|
this.canvas.width = VIEW_W * dpr;
|
|
|
|
|
this.canvas.height = VIEW_H * dpr;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Renderer.prototype.destroy = function () {
|
|
|
|
|
if (typeof window !== 'undefined') window.removeEventListener('resize', this._onResize);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ------------------------------------------------------------ particles
|
|
|
|
|
|
|
|
|
|
Renderer.prototype._spawn = function (n, fn) {
|
|
|
|
|
for (var i = 0; i < n; i++) {
|
|
|
|
|
var p = fn(this.rng);
|
|
|
|
|
if (p) this.particles.push(p);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Renderer.prototype.handleEvents = function (events) {
|
|
|
|
|
var self = this;
|
|
|
|
|
for (var i = 0; i < events.length; i++) {
|
|
|
|
|
var e = events[i];
|
|
|
|
|
switch (e.type) {
|
|
|
|
|
case 'hit':
|
|
|
|
|
case 'block_impact':
|
|
|
|
|
this._spawn(8, function (r) {
|
|
|
|
|
var ang = r() * Math.PI * 2, sp = 60 + r() * 220;
|
|
|
|
|
return { kind: 'spark', x: e.x, y: e.y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp - 80,
|
|
|
|
|
life: 0.5, maxLife: 0.5, size: 2 + r() * 3, color: '#ffe9b0' };
|
|
|
|
|
});
|
|
|
|
|
this._spawn(6, function (r) {
|
|
|
|
|
var matColor = e.mat === 'wood' ? '#b98a4e' : e.mat === 'ice' ? '#bfe8ff' : '#9aa0a8';
|
|
|
|
|
var ang2 = r() * Math.PI * 2, sp2 = 40 + r() * 160;
|
|
|
|
|
return { kind: 'debris', x: e.x, y: e.y, vx: Math.cos(ang2) * sp2, vy: Math.sin(ang2) * sp2 - 120,
|
|
|
|
|
life: 1.2, maxLife: 1.2, size: 3 + r() * 5, rot: r() * 6.3, vr: (r() - 0.5) * 8, color: matColor };
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'break': {
|
|
|
|
|
var mat = e.mat;
|
|
|
|
|
var big = e.big ? 26 : 14;
|
|
|
|
|
var color = mat === 'wood' ? '#b98a4e' : mat === 'ice' ? '#bfe8ff' : '#9aa0a8';
|
|
|
|
|
this._spawn(big, function (r) {
|
|
|
|
|
var ang = r() * Math.PI * 2, sp = 80 + r() * 300;
|
|
|
|
|
return { kind: 'debris', x: e.x, y: e.y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp - 160,
|
|
|
|
|
life: 1.5, maxLife: 1.5, size: 3 + r() * 6, rot: r() * 6.3, vr: (r() - 0.5) * 10, color: color };
|
|
|
|
|
});
|
|
|
|
|
this._spawn(6, function (r) {
|
|
|
|
|
var ang = r() * Math.PI * 2, sp = 30 + r() * 90;
|
|
|
|
|
return { kind: 'dust', x: e.x, y: e.y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp * 0.4 - 30,
|
|
|
|
|
life: 0.7, maxLife: 0.7, size: 6 + r() * 8, color: 'rgba(200,190,170,0.5)' };
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 'crack':
|
|
|
|
|
this._spawn(3, function (r) {
|
|
|
|
|
var ang = r() * Math.PI * 2, sp = 20 + r() * 60;
|
|
|
|
|
return { kind: 'debris', x: e.x, y: e.y, vx: Math.cos(ang) * sp, vy: -40 - r() * 60,
|
|
|
|
|
life: 0.8, maxLife: 0.8, size: 2 + r() * 3, rot: r() * 6.3, vr: 4, color: '#c9b391' };
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'pop':
|
|
|
|
|
this._spawn(14, function (r) {
|
|
|
|
|
var ang = r() * Math.PI * 2, sp = 60 + r() * 200;
|
|
|
|
|
return { kind: 'dust', x: e.x, y: e.y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp - 60,
|
|
|
|
|
life: 0.8, maxLife: 0.8, size: 5 + r() * 7, color: e.kind === 'king' ? 'rgba(240,200,90,0.8)' : 'rgba(126,196,96,0.8)' };
|
|
|
|
|
});
|
|
|
|
|
this._spawn(8, function (r) {
|
|
|
|
|
var ang = r() * Math.PI * 2, sp = 90 + r() * 240;
|
|
|
|
|
return { kind: 'spark', x: e.x, y: e.y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp,
|
|
|
|
|
life: 0.4, maxLife: 0.4, size: 2 + r() * 2, color: '#fff2c4' };
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'bird_ground':
|
|
|
|
|
this._spawn(7, function (r) {
|
|
|
|
|
var ang = -Math.PI / 2 + (r() - 0.5) * 1.6, sp = 40 + r() * 120;
|
|
|
|
|
return { kind: 'dust', x: e.x, y: e.y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp,
|
|
|
|
|
life: 0.6, maxLife: 0.6, size: 5 + r() * 6, color: 'rgba(190,170,130,0.55)' };
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'crush':
|
|
|
|
|
this._spawn(6, function (r) {
|
|
|
|
|
var ang = r() * Math.PI, sp = 30 + r() * 100;
|
|
|
|
|
return { kind: 'dust', x: e.x, y: e.y, vx: Math.cos(ang) * sp * (r() > 0.5 ? 1 : -1), vy: -30 - r() * 80,
|
|
|
|
|
life: 0.5, maxLife: 0.5, size: 4 + r() * 5, color: 'rgba(180,180,170,0.5)' };
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'ability_fury':
|
|
|
|
|
this._spawn(10, function (r) {
|
|
|
|
|
return { kind: 'spark', x: e.x - r() * 60, y: e.y + (r() - 0.5) * 40, vx: -300 - r() * 200, vy: (r() - 0.5) * 120,
|
|
|
|
|
life: 0.35, maxLife: 0.35, size: 2 + r() * 2, color: '#ffd9a8' };
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'ability_dive':
|
|
|
|
|
this._spawn(10, function (r) {
|
|
|
|
|
return { kind: 'spark', x: e.x + (r() - 0.5) * 50, y: e.y - r() * 40, vx: (r() - 0.5) * 100, vy: -200 - r() * 150,
|
|
|
|
|
life: 0.35, maxLife: 0.35, size: 2 + r() * 2, color: '#fff6c8' };
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'ability_quake':
|
|
|
|
|
this._spawn(24, function (r) {
|
|
|
|
|
var ang = r() * Math.PI * 2, sp = 120 + r() * 260;
|
|
|
|
|
return { kind: 'dust', x: e.x + Math.cos(ang) * 30, y: Math.max(e.y, 0) , vx: Math.cos(ang) * sp, vy: -Math.abs(Math.sin(ang)) * sp * 0.7 - 40,
|
|
|
|
|
life: 0.9, maxLife: 0.9, size: 5 + r() * 7, color: 'rgba(196,178,140,0.6)' };
|
|
|
|
|
});
|
|
|
|
|
this.ring = { x: e.x, y: e.y, r: 20, max: e.r, life: 0.5, maxLife: 0.5 };
|
|
|
|
|
break;
|
|
|
|
|
case 'bird_gone':
|
|
|
|
|
this._spawn(10, function (r) {
|
|
|
|
|
var ang = r() * Math.PI * 2, sp = 20 + r() * 60;
|
|
|
|
|
return { kind: 'feather', x: e.x, y: e.y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp - 40,
|
|
|
|
|
life: 1.4, maxLife: 1.4, size: 4 + r() * 4, rot: r() * 6.3, vr: (r() - 0.5) * 6, color: 'rgba(235,235,240,0.9)' };
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case 'confetti_burst':
|
|
|
|
|
self.confetti = [];
|
|
|
|
|
self._spawn(80, function (r) {
|
|
|
|
|
var ang = -Math.PI / 2 + (r() - 0.5) * 1.8, sp = 200 + r() * 420;
|
|
|
|
|
var cols = ['#ffd23e', '#ff5d5d', '#5dc8ff', '#7de07d', '#e08cff'];
|
|
|
|
|
return { kind: 'confetti', x: e.x, y: e.y, vx: Math.cos(ang) * sp, vy: Math.sin(ang) * sp,
|
|
|
|
|
life: 2.4, maxLife: 2.4, size: 4 + r() * 5, rot: r() * 6.3, vr: (r() - 0.5) * 12, color: cols[(r() * cols.length) | 0] };
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
self.onEvent(e);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ------------------------------------------------------------ drawing
|
|
|
|
|
|
|
|
|
|
var BIRD_COLORS = {
|
|
|
|
|
wren: { body: '#e2452e', belly: '#f7d9b0', beak: '#f5a623' },
|
|
|
|
|
zippy: { body: '#f2c51d', belly: '#fff3c4', beak: '#e07f12' },
|
|
|
|
|
boulder: { body: '#31507f', belly: '#9db8d9', beak: '#d98e2b' }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Renderer.prototype.drawBird = function (ctx, b, t) {
|
|
|
|
|
var c = BIRD_COLORS[b.kind];
|
|
|
|
|
var r = b.r;
|
|
|
|
|
ctx.save();
|
|
|
|
|
ctx.translate(b.x, b.y);
|
|
|
|
|
var tilt = 0;
|
|
|
|
|
if (b.state === 'flying') tilt = Math.atan2(b.vy, Math.max(40, b.vx)) * 0.35;
|
|
|
|
|
ctx.rotate(tilt);
|
|
|
|
|
// tail
|
|
|
|
|
ctx.fillStyle = c.body;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(-r * 0.8, -r * 0.2); ctx.lineTo(-r * 1.5, -r * 0.55); ctx.lineTo(-r * 1.45, 0.1 * r);
|
|
|
|
|
ctx.lineTo(-r * 1.5, r * 0.35); ctx.lineTo(-r * 0.8, r * 0.25); ctx.closePath(); ctx.fill();
|
|
|
|
|
// body
|
|
|
|
|
ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
// belly
|
|
|
|
|
ctx.fillStyle = c.belly;
|
|
|
|
|
ctx.beginPath(); ctx.ellipse(r * 0.1, r * 0.42, r * 0.62, r * 0.42, 0, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
// wing
|
|
|
|
|
ctx.fillStyle = 'rgba(0,0,0,0.18)';
|
|
|
|
|
ctx.beginPath(); ctx.ellipse(-r * 0.35, r * 0.05, r * 0.5, r * 0.3, -0.5, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
// eyes
|
|
|
|
|
ctx.fillStyle = '#fff';
|
|
|
|
|
ctx.beginPath(); ctx.arc(r * 0.38, -r * 0.28, r * 0.3, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.beginPath(); ctx.arc(r * 0.72, -r * 0.24, r * 0.26, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.fillStyle = '#222';
|
|
|
|
|
ctx.beginPath(); ctx.arc(r * 0.46, -r * 0.26, r * 0.13, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.beginPath(); ctx.arc(r * 0.78, -r * 0.22, r * 0.12, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
// beak
|
|
|
|
|
ctx.fillStyle = c.beak;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(r * 0.85, -r * 0.05); ctx.lineTo(r * 1.45, r * 0.08); ctx.lineTo(r * 0.8, r * 0.22);
|
|
|
|
|
ctx.closePath(); ctx.fill();
|
|
|
|
|
// angry brow
|
|
|
|
|
ctx.strokeStyle = '#3a2a20';
|
|
|
|
|
ctx.lineWidth = Math.max(2, r * 0.14);
|
|
|
|
|
ctx.beginPath(); ctx.moveTo(r * 0.12, -r * 0.62); ctx.lineTo(r * 0.9, -r * 0.42); ctx.stroke();
|
|
|
|
|
// ability-ready flash
|
|
|
|
|
if (b.state === 'flying' && !b.abilityUsed && b.age > 0.35) {
|
|
|
|
|
ctx.strokeStyle = 'rgba(255,255,190,' + (0.5 + 0.5 * Math.sin(t * 10)).toFixed(2) + ')';
|
|
|
|
|
ctx.lineWidth = 2.5;
|
|
|
|
|
ctx.beginPath(); ctx.arc(0, 0, r + 7, 0, Math.PI * 2); ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
ctx.restore();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Renderer.prototype.drawMog = function (ctx, e, t) {
|
|
|
|
|
var r = e.r;
|
|
|
|
|
var body = e.kind === 'grum' ? '#5d9e4a' : e.kind === 'king' ? '#4f8f3e' : '#7ec06a';
|
|
|
|
|
ctx.save();
|
|
|
|
|
ctx.translate(e.x, e.y);
|
|
|
|
|
if (e.dead) {
|
|
|
|
|
ctx.globalAlpha = Math.max(0, 1 - e.deathAge * 2.2);
|
|
|
|
|
ctx.scale(1 + e.deathAge * 0.6, Math.max(0.1, 1 - e.deathAge * 1.4));
|
|
|
|
|
}
|
|
|
|
|
// body
|
|
|
|
|
ctx.fillStyle = body;
|
|
|
|
|
ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
// snout
|
|
|
|
|
ctx.fillStyle = e.kind === 'king' ? '#d8c07a' : '#cfe8b8';
|
|
|
|
|
ctx.beginPath(); ctx.ellipse(0, r * 0.3, r * 0.55, r * 0.4, 0, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.fillStyle = 'rgba(0,0,0,0.35)';
|
|
|
|
|
ctx.beginPath(); ctx.arc(-r * 0.2, r * 0.28, r * 0.09, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.beginPath(); ctx.arc(r * 0.2, r * 0.28, r * 0.09, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
// eyes
|
|
|
|
|
if (e.dead) {
|
|
|
|
|
ctx.strokeStyle = '#233';
|
|
|
|
|
ctx.lineWidth = 2.5;
|
|
|
|
|
[-1, 1].forEach(function (s) {
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(s * r * 0.28 - 5, -r * 0.35 - 5); ctx.lineTo(s * r * 0.28 + 5, -r * 0.35 + 5);
|
|
|
|
|
ctx.moveTo(s * r * 0.28 + 5, -r * 0.35 - 5); ctx.lineTo(s * r * 0.28 - 5, -r * 0.35 + 5);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
ctx.fillStyle = '#fff';
|
|
|
|
|
ctx.beginPath(); ctx.arc(-r * 0.3, -r * 0.3, r * 0.24, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.beginPath(); ctx.arc(r * 0.3, -r * 0.3, r * 0.24, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.fillStyle = '#223';
|
|
|
|
|
var blink = Math.sin(t * 1.7 + e.id * 2.1) > 0.985 ? 0.15 : 1;
|
|
|
|
|
ctx.beginPath(); ctx.ellipse(-r * 0.28, -r * 0.28, r * 0.1, r * 0.1 * blink, 0, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.beginPath(); ctx.ellipse(r * 0.32, -r * 0.28, r * 0.1, r * 0.1 * blink, 0, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
}
|
|
|
|
|
// ears
|
|
|
|
|
ctx.fillStyle = body;
|
|
|
|
|
ctx.beginPath(); ctx.arc(-r * 0.55, -r * 0.75, r * 0.22, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.beginPath(); ctx.arc(r * 0.55, -r * 0.75, r * 0.22, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
if (e.kind === 'king') {
|
|
|
|
|
ctx.fillStyle = '#f2c230';
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(-r * 0.5, -r * 0.82);
|
|
|
|
|
ctx.lineTo(-r * 0.35, -r * 1.25); ctx.lineTo(-r * 0.15, -r * 0.95);
|
|
|
|
|
ctx.lineTo(0, -r * 1.3); ctx.lineTo(r * 0.15, -r * 0.95);
|
|
|
|
|
ctx.lineTo(r * 0.35, -r * 1.25); ctx.lineTo(r * 0.5, -r * 0.82);
|
|
|
|
|
ctx.closePath(); ctx.fill();
|
|
|
|
|
} else if (e.kind === 'grum') {
|
|
|
|
|
ctx.fillStyle = '#6b4a2a';
|
|
|
|
|
ctx.fillRect(-r * 0.55, -r * 0.95, r * 1.1, r * 0.22);
|
|
|
|
|
}
|
|
|
|
|
ctx.restore();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Renderer.prototype.drawBlock = function (ctx, bl) {
|
|
|
|
|
if (bl.hp <= 0) return;
|
|
|
|
|
var x = bl.x - bl.w / 2, y = bl.y - bl.h / 2;
|
Physics upgrade + repo modularization (RC-375D84)
Delivered by Hermies Qwen, gated and deployed by Relay.
- Board tip/topple: full continuous rotation when a board's center of
mass passes its support edge (Q1: smooth pivot swing, no discrete
snap). Falling boards deal impact damage and can kill enemies in
their fall path.
- Falling-object impact damage: blocks and birds can now kill enemies
from vertical drops and side plows (velocity-aware crush gate),
chain momentum transfers correctly through stacks (mass-weighted
contact), repeated impacts finish tougher enemies.
- Landed-bird physics: a bird resting on a board remains a real
physical body under continuous gravity (Q2: no lifetime timer). If
its platform is destroyed it falls, damages what it lands on
(chain reaction), and can kill enemies.
- Repo modularized: folders = modules, subfolders = submodules,
architecture.md + MODULES.md at root, an agents.md card in every
module and submodule (17 total).
- Test suite grown from 27 to 47 (20 new RED-watched tests for the
above behaviors), all green. Golden differential against the
original core confirmed the only behavioral changes are the
intended ones.
Live at http://10.244.136.121:8093/, independently re-verified by
Relay (full suite re-run, module structure check, headless Chromium
smoke) before this push.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:21:18 -05:00
|
|
|
// S-3: a tipped board draws rotated about its centre (COM). Supported
|
|
|
|
|
// stacks keep ang=0, so the transform is a no-op and output is identical.
|
|
|
|
|
var rotated = !!bl.ang;
|
|
|
|
|
if (rotated) { ctx.save(); ctx.translate(bl.x, bl.y); ctx.rotate(bl.ang); ctx.translate(-bl.x, -bl.y); }
|
2026-09-28 17:20:17 -05:00
|
|
|
var styles = {
|
|
|
|
|
wood: { fill: '#b98a4e', edge: '#8a5f2e', line: 'rgba(90,60,25,0.5)' },
|
|
|
|
|
ice: { fill: 'rgba(168,220,250,0.82)', edge: 'rgba(120,180,220,0.9)', line: 'rgba(255,255,255,0.55)' },
|
|
|
|
|
stone: { fill: '#9aa0a8', edge: '#6d7278', line: 'rgba(70,75,82,0.5)' }
|
|
|
|
|
};
|
|
|
|
|
var s = styles[bl.mat];
|
|
|
|
|
ctx.fillStyle = s.fill;
|
|
|
|
|
ctx.fillRect(x, y, bl.w, bl.h);
|
|
|
|
|
ctx.strokeStyle = s.edge;
|
|
|
|
|
ctx.lineWidth = 3;
|
|
|
|
|
ctx.strokeRect(x + 1.5, y + 1.5, bl.w - 3, bl.h - 3);
|
|
|
|
|
// plank lines / grain
|
|
|
|
|
ctx.strokeStyle = s.line;
|
|
|
|
|
ctx.lineWidth = 1.5;
|
|
|
|
|
if (bl.mat === 'wood') {
|
|
|
|
|
if (bl.h > bl.w) {
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(x + bl.w * 0.3, y + 4); ctx.lineTo(x + bl.w * 0.35, y + bl.h - 4);
|
|
|
|
|
ctx.moveTo(x + bl.w * 0.7, y + 4); ctx.lineTo(x + bl.w * 0.62, y + bl.h - 4);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
} else {
|
|
|
|
|
for (var px = x + bl.w / 4; px < x + bl.w - 8; px += bl.w / 4) {
|
|
|
|
|
ctx.beginPath(); ctx.moveTo(px, y + 4); ctx.lineTo(px, y + bl.h - 4); ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (bl.mat === 'stone') {
|
|
|
|
|
var rnd = mulberry32(bl.id * 97 + 13);
|
|
|
|
|
ctx.fillStyle = 'rgba(70,75,82,0.35)';
|
|
|
|
|
for (var i = 0; i < 6; i++) {
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.arc(x + rnd() * bl.w, y + rnd() * bl.h, 1.5 + rnd() * 2, 0, Math.PI * 2);
|
|
|
|
|
ctx.fill();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(x + bl.w * 0.15, y + 6); ctx.lineTo(x + bl.w * 0.4, y + bl.h * 0.4);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
// cracks by damage
|
|
|
|
|
var dmg = 1 - bl.hp / bl.maxHp;
|
|
|
|
|
if (dmg > 0.15) {
|
|
|
|
|
var rnd2 = mulberry32(bl.id * 31 + 7);
|
|
|
|
|
ctx.strokeStyle = 'rgba(30,25,20,0.55)';
|
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
|
var nCracks = dmg > 0.6 ? 4 : dmg > 0.35 ? 2 : 1;
|
|
|
|
|
for (var c = 0; c < nCracks; c++) {
|
|
|
|
|
var cx0 = x + rnd2() * bl.w, cy0 = y;
|
|
|
|
|
ctx.beginPath(); ctx.moveTo(cx0, cy0);
|
|
|
|
|
var segs = 3;
|
|
|
|
|
for (var sgm = 1; sgm <= segs; sgm++) {
|
|
|
|
|
cx0 += (rnd2() - 0.5) * bl.w * 0.4;
|
|
|
|
|
ctx.lineTo(cx0, cy0 + (bl.h / segs) * sgm);
|
|
|
|
|
}
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
}
|
Physics upgrade + repo modularization (RC-375D84)
Delivered by Hermies Qwen, gated and deployed by Relay.
- Board tip/topple: full continuous rotation when a board's center of
mass passes its support edge (Q1: smooth pivot swing, no discrete
snap). Falling boards deal impact damage and can kill enemies in
their fall path.
- Falling-object impact damage: blocks and birds can now kill enemies
from vertical drops and side plows (velocity-aware crush gate),
chain momentum transfers correctly through stacks (mass-weighted
contact), repeated impacts finish tougher enemies.
- Landed-bird physics: a bird resting on a board remains a real
physical body under continuous gravity (Q2: no lifetime timer). If
its platform is destroyed it falls, damages what it lands on
(chain reaction), and can kill enemies.
- Repo modularized: folders = modules, subfolders = submodules,
architecture.md + MODULES.md at root, an agents.md card in every
module and submodule (17 total).
- Test suite grown from 27 to 47 (20 new RED-watched tests for the
above behaviors), all green. Golden differential against the
original core confirmed the only behavioral changes are the
intended ones.
Live at http://10.244.136.121:8093/, independently re-verified by
Relay (full suite re-run, module structure check, headless Chromium
smoke) before this push.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:21:18 -05:00
|
|
|
if (rotated) ctx.restore();
|
2026-09-28 17:20:17 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Renderer.prototype.drawSling = function (ctx, sling, bird, drag) {
|
|
|
|
|
var sx = sling.x, sy = sling.y;
|
|
|
|
|
ctx.save();
|
|
|
|
|
ctx.strokeStyle = '#6d4a26';
|
|
|
|
|
ctx.lineCap = 'round';
|
|
|
|
|
ctx.lineWidth = 14;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(sx, sy + 130);
|
|
|
|
|
ctx.lineTo(sx, sy - 18);
|
|
|
|
|
ctx.moveTo(sx, sy - 18);
|
|
|
|
|
ctx.lineTo(sx - 26, sy - 62);
|
|
|
|
|
ctx.moveTo(sx, sy - 18);
|
|
|
|
|
ctx.lineTo(sx + 26, sy - 62);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
ctx.strokeStyle = '#8a5f2e';
|
|
|
|
|
ctx.lineWidth = 5;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(sx, sy + 130); ctx.lineTo(sx, sy - 10);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
// rubber bands: taut toward the bird ONLY while it is being aimed.
|
|
|
|
|
// The moment it launches the sling must let go — a band that keeps
|
|
|
|
|
// tracking the flying bird reads as "the sling never released it"
|
|
|
|
|
// (bug #14). In flight (or with no bird) the bands hang relaxed.
|
|
|
|
|
if (bird && bird.state === 'aiming') {
|
|
|
|
|
var bx = drag ? drag.x : bird.x;
|
|
|
|
|
var by = drag ? drag.y : bird.y;
|
|
|
|
|
ctx.strokeStyle = '#4a3018';
|
|
|
|
|
ctx.lineWidth = 7;
|
|
|
|
|
ctx.beginPath(); ctx.moveTo(sx - 26, sy - 62); ctx.lineTo(bx - 6, by); ctx.stroke();
|
|
|
|
|
ctx.lineWidth = 9;
|
|
|
|
|
ctx.beginPath(); ctx.moveTo(sx + 26, sy - 62); ctx.lineTo(bx + 6, by); ctx.stroke();
|
|
|
|
|
} else {
|
|
|
|
|
ctx.strokeStyle = '#4a3018';
|
|
|
|
|
ctx.lineWidth = 7;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(sx - 26, sy - 62);
|
|
|
|
|
ctx.quadraticCurveTo(sx, sy - 30, sx + 26, sy - 62);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
ctx.restore();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Renderer.prototype.drawPalm = function (ctx, x, ground, s) {
|
|
|
|
|
ctx.save();
|
|
|
|
|
ctx.translate(x, ground);
|
|
|
|
|
ctx.strokeStyle = '#7a5a33';
|
|
|
|
|
ctx.lineWidth = 14 * s;
|
|
|
|
|
ctx.lineCap = 'round';
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(0, 0);
|
|
|
|
|
ctx.quadraticCurveTo(10 * s, -70 * s, 26 * s, -118 * s);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
ctx.strokeStyle = '#3f9d4f';
|
|
|
|
|
ctx.lineWidth = 9 * s;
|
|
|
|
|
for (var i = 0; i < 6; i++) {
|
|
|
|
|
var ang = -Math.PI / 2 + (i - 2.5) * 0.5;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(26 * s, -118 * s);
|
|
|
|
|
ctx.quadraticCurveTo(26 * s + Math.cos(ang) * 45 * s, -118 * s + Math.sin(ang) * 30 * s - 12 * s,
|
|
|
|
|
26 * s + Math.cos(ang) * 72 * s, -118 * s + Math.sin(ang) * 48 * s + 14 * s);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
ctx.fillStyle = '#8a5a2a';
|
|
|
|
|
ctx.beginPath(); ctx.arc(26 * s, -112 * s, 7 * s, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.restore();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Renderer.prototype.draw = function (state, view) {
|
|
|
|
|
var ctx = this.ctx;
|
|
|
|
|
var t = state.time;
|
|
|
|
|
var cam = state.camera;
|
|
|
|
|
var dpr = this.dpr;
|
|
|
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
|
|
|
// sky
|
|
|
|
|
var sky = ctx.createLinearGradient(0, 0, 0, VIEW_H);
|
|
|
|
|
sky.addColorStop(0, '#7ec8f0');
|
|
|
|
|
sky.addColorStop(0.55, '#b8e4f7');
|
|
|
|
|
sky.addColorStop(1, '#e8f7ff');
|
|
|
|
|
ctx.fillStyle = sky;
|
|
|
|
|
ctx.fillRect(0, 0, VIEW_W, VIEW_H);
|
|
|
|
|
// sun
|
|
|
|
|
ctx.fillStyle = 'rgba(255,240,180,0.9)';
|
|
|
|
|
ctx.beginPath(); ctx.arc(VIEW_W - 140, 110, 46, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
// world transform: camera pan + shake
|
|
|
|
|
var shX = 0, shY = 0;
|
|
|
|
|
if (cam.shake > 0) {
|
|
|
|
|
shX = (this.rng() - 0.5) * cam.shake * 2;
|
|
|
|
|
shY = (this.rng() - 0.5) * cam.shake * 2;
|
|
|
|
|
}
|
|
|
|
|
ctx.save();
|
|
|
|
|
// world transform: camera pan + zoom + shake (shake applied in screen
|
|
|
|
|
// space so its magnitude does not scale with the zoom)
|
|
|
|
|
var zoom = (cam && cam.zoom) || 1;
|
|
|
|
|
ctx.translate(VIEW_W / 2 + shX, VIEW_H / 2 + shY);
|
|
|
|
|
ctx.scale(zoom, zoom);
|
|
|
|
|
ctx.translate(-cam.x, -cam.y);
|
|
|
|
|
var G = state.world.ground;
|
|
|
|
|
|
|
|
|
|
// distant island silhouette (parallax-lite via fixed offset)
|
|
|
|
|
ctx.fillStyle = 'rgba(122,178,140,0.35)';
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.ellipse(1200, G + 30, 900, 90, 0, Math.PI, 0);
|
|
|
|
|
ctx.fill();
|
|
|
|
|
|
|
|
|
|
// island ground
|
|
|
|
|
ctx.fillStyle = '#c9a36a';
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(-400, G);
|
|
|
|
|
ctx.quadraticCurveTo(1200, G - 90, 2800, G);
|
|
|
|
|
ctx.lineTo(2800, G + 400); ctx.lineTo(-400, G + 400);
|
|
|
|
|
ctx.closePath(); ctx.fill();
|
|
|
|
|
// grass
|
|
|
|
|
ctx.fillStyle = '#67b34d';
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(-400, G);
|
|
|
|
|
ctx.quadraticCurveTo(1200, G - 90, 2800, G);
|
|
|
|
|
ctx.quadraticCurveTo(1200, G - 44, -400, G);
|
|
|
|
|
ctx.closePath(); ctx.fill();
|
|
|
|
|
|
|
|
|
|
// decor
|
|
|
|
|
if (state.level.decor) {
|
|
|
|
|
var d = state.level.decor;
|
|
|
|
|
for (var p = 0; p < (d.palms || []).length; p++) this.drawPalm(ctx, d.palms[p], G - 40, 1);
|
|
|
|
|
for (var f = 0; f < (d.flowers || []).length; f++) {
|
|
|
|
|
var fx = d.flowers[f];
|
|
|
|
|
ctx.fillStyle = '#e2557b';
|
|
|
|
|
ctx.beginPath(); ctx.arc(fx, G - 48, 5, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.strokeStyle = '#3f9d4f'; ctx.lineWidth = 3;
|
|
|
|
|
ctx.beginPath(); ctx.moveTo(fx, G - 44); ctx.lineTo(fx, G - 30); ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// queued birds at the beach
|
|
|
|
|
for (var qb = 0; qb < state.birds.length; qb++) {
|
|
|
|
|
var qb2 = state.birds[qb];
|
|
|
|
|
if (qb2.state === 'queued') {
|
|
|
|
|
var bx = state.level.sling.x - 130 - (state.birds.length - 1 - qb) * 56;
|
|
|
|
|
this.drawBird(ctx, { x: bx, y: G - 30 - qb2.r * 0.4, r: qb2.r, kind: qb2.kind, state: 'queued', vx: 0, vy: 0, age: t }, t);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// slingshot back arm + bird + front arm (bird sits between the bands)
|
|
|
|
|
var ab = (view && view.activeBird) || null;
|
|
|
|
|
var drag = (view && view.drag) || null;
|
|
|
|
|
this.drawSling(ctx, state.level.sling, ab, drag);
|
|
|
|
|
if (ab && (ab.state === 'aiming' || ab.state === 'flying')) {
|
|
|
|
|
var bb = { x: drag && ab.state === 'aiming' ? drag.x : ab.x, y: drag && ab.state === 'aiming' ? drag.y : ab.y,
|
|
|
|
|
r: ab.r, kind: ab.kind, state: ab.state, vx: ab.vx, vy: ab.vy, age: ab.age,
|
|
|
|
|
abilityUsed: ab.abilityUsed };
|
|
|
|
|
this.drawBird(ctx, bb, t);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// trajectory preview
|
|
|
|
|
if (view && view.traj) {
|
|
|
|
|
for (var tp = 0; tp < view.traj.length; tp++) {
|
|
|
|
|
var pt = view.traj[tp];
|
|
|
|
|
ctx.fillStyle = 'rgba(255,255,255,' + (0.75 - tp * 0.02).toFixed(2) + ')';
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.arc(pt.x, pt.y, Math.max(2, 6 - tp * 0.12), 0, Math.PI * 2);
|
|
|
|
|
ctx.fill();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// blocks
|
|
|
|
|
for (var bl = 0; bl < state.blocks.length; bl++) this.drawBlock(ctx, state.blocks[bl]);
|
|
|
|
|
// enemies
|
|
|
|
|
for (var en = 0; en < state.enemies.length; en++) this.drawMog(ctx, state.enemies[en], t);
|
|
|
|
|
|
Physics upgrade + repo modularization (RC-375D84)
Delivered by Hermies Qwen, gated and deployed by Relay.
- Board tip/topple: full continuous rotation when a board's center of
mass passes its support edge (Q1: smooth pivot swing, no discrete
snap). Falling boards deal impact damage and can kill enemies in
their fall path.
- Falling-object impact damage: blocks and birds can now kill enemies
from vertical drops and side plows (velocity-aware crush gate),
chain momentum transfers correctly through stacks (mass-weighted
contact), repeated impacts finish tougher enemies.
- Landed-bird physics: a bird resting on a board remains a real
physical body under continuous gravity (Q2: no lifetime timer). If
its platform is destroyed it falls, damages what it lands on
(chain reaction), and can kill enemies.
- Repo modularized: folders = modules, subfolders = submodules,
architecture.md + MODULES.md at root, an agents.md card in every
module and submodule (17 total).
- Test suite grown from 27 to 47 (20 new RED-watched tests for the
above behaviors), all green. Golden differential against the
original core confirmed the only behavioral changes are the
intended ones.
Live at http://10.244.136.121:8093/, independently re-verified by
Relay (full suite re-run, module structure check, headless Chromium
smoke) before this push.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:21:18 -05:00
|
|
|
// spent birds lie on the ground; S-5: LANDED birds are live bodies too --
|
|
|
|
|
// they stay in the world (perched, falling, rolling) until level end.
|
2026-09-28 17:20:17 -05:00
|
|
|
for (var sp = 0; sp < state.birds.length; sp++) {
|
|
|
|
|
var sb = state.birds[sp];
|
|
|
|
|
if (sb.state === 'spent' && !sb.gone) this.drawBird(ctx, sb, t);
|
Physics upgrade + repo modularization (RC-375D84)
Delivered by Hermies Qwen, gated and deployed by Relay.
- Board tip/topple: full continuous rotation when a board's center of
mass passes its support edge (Q1: smooth pivot swing, no discrete
snap). Falling boards deal impact damage and can kill enemies in
their fall path.
- Falling-object impact damage: blocks and birds can now kill enemies
from vertical drops and side plows (velocity-aware crush gate),
chain momentum transfers correctly through stacks (mass-weighted
contact), repeated impacts finish tougher enemies.
- Landed-bird physics: a bird resting on a board remains a real
physical body under continuous gravity (Q2: no lifetime timer). If
its platform is destroyed it falls, damages what it lands on
(chain reaction), and can kill enemies.
- Repo modularized: folders = modules, subfolders = submodules,
architecture.md + MODULES.md at root, an agents.md card in every
module and submodule (17 total).
- Test suite grown from 27 to 47 (20 new RED-watched tests for the
above behaviors), all green. Golden differential against the
original core confirmed the only behavioral changes are the
intended ones.
Live at http://10.244.136.121:8093/, independently re-verified by
Relay (full suite re-run, module structure check, headless Chromium
smoke) before this push.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 08:21:18 -05:00
|
|
|
else if (sb.state === 'landed') this.drawBird(ctx, sb, t);
|
2026-09-28 17:20:17 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// score popups
|
|
|
|
|
ctx.textAlign = 'center';
|
|
|
|
|
for (var pp = 0; pp < state.scorePopups.length; pp++) {
|
|
|
|
|
var pop = state.scorePopups[pp];
|
|
|
|
|
ctx.globalAlpha = Math.max(0, 1 - pop.age / 1.4);
|
|
|
|
|
ctx.fillStyle = '#fff';
|
|
|
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
|
|
|
|
|
ctx.lineWidth = 4;
|
|
|
|
|
ctx.font = 'bold 30px system-ui, sans-serif';
|
|
|
|
|
ctx.strokeText(pop.text, pop.x, pop.y);
|
|
|
|
|
ctx.fillText(pop.text, pop.x, pop.y);
|
|
|
|
|
ctx.globalAlpha = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// quake ring
|
|
|
|
|
if (this.ring) {
|
|
|
|
|
var rg = this.ring;
|
|
|
|
|
rg.life -= (1 / 60);
|
|
|
|
|
var k = 1 - rg.life / rg.maxLife;
|
|
|
|
|
ctx.strokeStyle = 'rgba(220,200,160,' + (0.7 * (1 - k)).toFixed(2) + ')';
|
|
|
|
|
ctx.lineWidth = 6;
|
|
|
|
|
ctx.beginPath(); ctx.arc(rg.x, rg.y, 20 + k * (rg.max - 20), 0, Math.PI * 2); ctx.stroke();
|
|
|
|
|
if (rg.life <= 0) this.ring = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// particles
|
|
|
|
|
var dtP = 1 / 60;
|
|
|
|
|
for (var pi = this.particles.length - 1; pi >= 0; pi--) {
|
|
|
|
|
var pt2 = this.particles[pi];
|
|
|
|
|
pt2.life -= dtP;
|
|
|
|
|
if (pt2.life <= 0) { this.particles.splice(pi, 1); continue; }
|
|
|
|
|
pt2.vy += (pt2.kind === 'dust' ? 160 : 900) * dtP;
|
|
|
|
|
pt2.x += pt2.vx * dtP;
|
|
|
|
|
pt2.y += pt2.vy * dtP;
|
|
|
|
|
var a = Math.max(0, pt2.life / pt2.maxLife);
|
|
|
|
|
if (pt2.kind === 'debris' || pt2.kind === 'feather' || pt2.kind === 'confetti') {
|
|
|
|
|
pt2.rot += (pt2.vr || 0) * dtP;
|
|
|
|
|
ctx.save();
|
|
|
|
|
ctx.translate(pt2.x, pt2.y);
|
|
|
|
|
ctx.rotate(pt2.rot);
|
|
|
|
|
ctx.globalAlpha = a;
|
|
|
|
|
ctx.fillStyle = pt2.color;
|
|
|
|
|
if (pt2.kind === 'feather') {
|
|
|
|
|
ctx.beginPath(); ctx.ellipse(0, 0, pt2.size * 1.4, pt2.size * 0.6, 0, 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
} else {
|
|
|
|
|
ctx.fillRect(-pt2.size / 2, -pt2.size / 2, pt2.size, pt2.size * (pt2.kind === 'debris' ? 0.7 : 1));
|
|
|
|
|
}
|
|
|
|
|
ctx.restore();
|
|
|
|
|
ctx.globalAlpha = 1;
|
|
|
|
|
} else {
|
|
|
|
|
ctx.globalAlpha = a;
|
|
|
|
|
ctx.fillStyle = pt2.color;
|
|
|
|
|
ctx.beginPath(); ctx.arc(pt2.x, pt2.y, pt2.size * (pt2.kind === 'dust' ? (1.6 - a * 0.6) : a), 0, Math.PI * 2); ctx.fill();
|
|
|
|
|
ctx.globalAlpha = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// confetti (screen-anchored burst from win)
|
|
|
|
|
if (this.confetti) {
|
|
|
|
|
for (var ci = this.confetti.length - 1; ci >= 0; ci--) {
|
|
|
|
|
var c2 = this.confetti[ci];
|
|
|
|
|
c2.life -= dtP;
|
|
|
|
|
if (c2.life <= 0) { this.confetti.splice(ci, 1); continue; }
|
|
|
|
|
c2.vy += 700 * dtP;
|
|
|
|
|
c2.x += c2.vx * dtP;
|
|
|
|
|
c2.y += c2.vy * dtP;
|
|
|
|
|
c2.rot += (c2.vr || 0) * dtP;
|
|
|
|
|
var ca = Math.max(0, Math.min(1, c2.life / 0.6));
|
|
|
|
|
ctx.save();
|
|
|
|
|
ctx.translate(c2.x, c2.y);
|
|
|
|
|
ctx.rotate(c2.rot);
|
|
|
|
|
ctx.globalAlpha = ca;
|
|
|
|
|
ctx.fillStyle = c2.color;
|
|
|
|
|
ctx.fillRect(-c2.size / 2, -c2.size / 2, c2.size, c2.size * 0.6);
|
|
|
|
|
ctx.restore();
|
|
|
|
|
ctx.globalAlpha = 1;
|
|
|
|
|
}
|
|
|
|
|
if (this.confetti.length === 0) this.confetti = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.restore();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return { Renderer: Renderer, VIEW_W: VIEW_W, VIEW_H: VIEW_H };
|
|
|
|
|
});
|