import { createCanvas } from "@common/display/canvas"; import { gameLoop } from "@common/game"; import { bresenhamCircleGen } from "@common/navigation/bresenham"; import Physics from "@common/physics"; import { mapNumber } from "@common/utils"; interface Ball { body: number; damage: number; health: number; } interface Peg { body: number; health: number; } interface State extends Record { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D; bottom: number; balls: Set; pegs: Set; pegHealth: number; ballHealth: number; ballDamage: number; } let lastTick = performance.now(); function update() { const dt = (performance.now() - lastTick) / 1000; if (dt < 1) { Physics.update(dt); } lastTick = performance.now(); setTimeout(update, 5); } const killBody = (state: State, body: Peg | Ball) => { state.balls.delete(body as Ball); state.pegs.delete(body as Peg); Physics.deleteBody(body.body); } const onCollision = (state: State, b1: number, b2: number) => { const ball = state.balls.values().find(b => b.body === b1 || b.body === b2); const peg = state.pegs.values().find(b => b.body === b1 || b.body === b2); if (ball && (b1 === state.bottom || b2 === state.bottom)) { killBody(state, ball); } else if (peg && ball) { peg.health -= ball.damage; ball.health -= 1; if (peg.health <= 0) { killBody(state, peg); } if (ball.health <= 0) { killBody(state, ball); } } } const generatePegs = (state: State) => { const { canvas } = state; state.pegs.clear(); const gap = 45; const vgap = gap * Math.cos(Math.PI / 6); let offset = 0; for (let y = canvas.height / 3; y <= canvas.height - gap; y += vgap) { for (let i = offset; i <= canvas.width / 2; i += gap) { const x1 = canvas.width / 2 + i; state.pegs.add({ body: Physics.newCircle(x1, y, 3, Number.POSITIVE_INFINITY), health: state.pegHealth, }); const x2 = canvas.width / 2 - i; if (Math.abs(x1 - x2) < gap) continue; state.pegs.add({ body: Physics.newCircle(x2, y, 3, Number.POSITIVE_INFINITY), health: state.pegHealth, }); } offset = gap / 2 - offset; } } const setup = (): State => { const canvas = createCanvas(640, 480); const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('Failed to get canvas context'); } const state: State = { canvas, ctx, bottom: 0, pegs: new Set(), balls: new Set(), pegHealth: 3, ballHealth: 10, ballDamage: 1, }; ctx.imageSmoothingEnabled = false; canvas.addEventListener('contextmenu', e => { e.stopPropagation(); e.preventDefault(); }); canvas.addEventListener('mouseup', () => { const body = Physics.newCircle(canvas.width / 2 + Math.random() - 0.5, 50, 10, 1); const health = state.ballHealth; const damage = state.ballDamage; const ball: Ball = { body, health, damage, } state.balls.add(ball); return false; }); generatePegs(state); state.bottom = Physics.newPlane(0, canvas.height, 0, -1); Physics.newPlane(0, 0, 1, 0); Physics.newPlane(0, 0, 0, 1); Physics.newPlane(canvas.width, 0, -1, 0); Physics.setCollisionCallback((a, b) => onCollision(state, a, b)); update(); return state; } const frame = (dt: number, state: State) => { Physics.addGlobalForce(0, 100); Physics.update(dt); const { ctx, canvas } = state; ctx.fillStyle = `#111`; ctx.fillRect(0, 0, canvas.width, canvas.height); for (const peg of state.pegs) { const { x, y, radius } = Physics.getBody(peg.body); if (!radius) continue; ctx.fillStyle = `hsl(0, 0%, ${mapNumber(peg.health, 0, state.pegHealth, 0, 100)}%)`; fillCircle(ctx, x, y, radius); } for (const ball of state.balls) { const { id, x, y, radius } = Physics.getBody(ball.body); if (!radius) continue; ctx.fillStyle = `hsl(${id % 360}, 100%, 50%)`; fillCircle(ctx, x, y, radius); } } function fillCircle(ctx: CanvasRenderingContext2D, xc: number, yc: number, r: number) { // because default circle fill is antialiased for (const { x, y } of bresenhamCircleGen(xc, yc, r, { fill: true })) { ctx.fillRect(x, y, 1, 1); } } export default gameLoop(setup, frame);