commit 49591cbea39c9295f666df749ed65d4be2cb9125 Author: zenoamaro Date: Sun Jun 23 16:58:13 2019 +0200 Prototype diff --git a/font.ttf b/font.ttf new file mode 100644 index 0000000..3ddfed4 Binary files /dev/null and b/font.ttf differ diff --git a/gfx.js b/gfx.js new file mode 100644 index 0000000..655bb73 --- /dev/null +++ b/gfx.js @@ -0,0 +1,112 @@ +import {splitWords} from './utils.js'; +import {parable} from './math.js'; + +export function createCanvas(width, height) { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = width; canvas.height = height + return ctx; +} + +export function loadImage(src) { + return new Promise((resolve, reject) => { + const img = document.createElement('img'); + img.onload = (e) => resolve(img); + // @ts-ignore + img.onerror = (e) => reject(e.error); + img.src = src; + }); +} + +export function clipCanvas(ctx, fn) { + ctx.globalCompositeOperation = 'destination-out'; + fn(ctx); + ctx.globalCompositeOperation = 'source-over'; +} + +export function plot(ctx, x, y, color) { + ctx.fillStyle = color; + ctx.fillRect(x, y, 1, 1); +} + +export function drawLine(ctx, x1, y1, x2, y2, color) { + const steep = Math.abs(y2 - y1) > Math.abs(x2 - x1); + if (steep) [x1, y1, x2, y2] = [y1, x1, y2, x2]; + if (x1 > x2) [x1, y1, x2, y2] = [x2, y2, x1, y1]; + + const dX = (x2 - x1); + const dY = Math.abs(y2 - y1); + const ystep = (y1 < y2 ? 1 : -1); + + let err = (dX / 2); + let y = y1; + for (let x = x1; x <= x2; ++x) { + if (steep) plot(ctx, y, x, color); else plot(ctx, x, y, color); + err = err - dY; if (err < 0) { y += ystep; err += dX } + } +} + +export function drawRect(ctx, x, y, w, h, color) { + ctx.fillStyle = color; + ctx.fillRect(x, y, w, h); +} + +export function strokeRect(ctx, x, y, w, h, color) { + if (w === 1 || h === 1) return drawRect(x, y, w, h, color); + ctx.lineWidth = 1; + ctx.strokeStyle = color; + ctx.strokeRect(x+0.5, y+0.5, w-1, h-1); +} + +export function drawCircle(ctx, x, y, r, color) { + for (let cy = -r; cy <= r; cy++) { + for (let cx = -r; cx <= r; cx++) { + if (cx*cx + cy*cy <= r*r + r*0.85) { + plot(ctx, x+cx, y+cy, color); + } + } + } +} + +export function plotParable(ctx, x, y, a, v, g) { + for (let t=0; t<1000; t+=0.01) { + const [fx, fy] = parable(t, x, y, a, v, g); + plot(ctx, (fx), (fy), 'white'); + } +} + +/** @param align {CanvasTextAlign} */ +/** @param baseline {CanvasTextBaseline} */ +export function drawText(ctx, text, x, y, color, align='left', baseline='top') { + ctx.font = '5px text'; + ctx.textBaseline = baseline; + ctx.textAlign = align; + ctx.fillStyle = color; + ctx.strokeStyle = 'black'; + ctx.strokeText(text, x, y); + ctx.fillText(text, x, y); +} + +export function drawParagraph(ctx, text, x, y, w, h, color) { + let tx = 0; + let ty = 0; + // FIXME: Handle unbreakable words longer than width + const words = splitWords(text); + for (let word of words) { + const wordLength = word.length * 4; + if (word === '\n') { tx = 0; ty += 6; continue } + else if (tx + wordLength > w) { tx = 0; ty += 6 } + if (ty+6 > h) return; + for (let char of word) { + drawText(ctx, char, x+tx, y+ty, color); + tx += 4; + } + } +} + +export function loop(fn) { + (function looped() { + requestAnimationFrame(looped); + fn(); + })(); +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..b012f25 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + + + Scorched Earth + + + + + + diff --git a/input.js b/input.js new file mode 100644 index 0000000..db770e1 --- /dev/null +++ b/input.js @@ -0,0 +1,5 @@ +const input = {}; + +document.addEventListener('keydown', ({key}) => input[key] = true); +document.addEventListener('keyup', ({key}) => input[key] = false); +export function key(key) {return input[key]} diff --git a/main.js b/main.js new file mode 100644 index 0000000..c9aa5c1 --- /dev/null +++ b/main.js @@ -0,0 +1,112 @@ +import {key} from './input.js'; +import {createTerrain, clipTerrain, isTerrain} from './terrain.js'; +import {createCanvas, drawCircle, drawLine, plot, drawParagraph, loop, drawText} from './gfx.js'; +import {wrap, clamp, parable, deg2rad, vec} from './math.js'; +import {drawExplosion, WEAPONS} from './weapons.js'; + +const W = 384; const H = 240; const Z = 3; + +let state = 'aim'; +const players = []; +let currentPlayer = 0; +let projectile = null; + +// Init canvas +const fb = createCanvas(W, H); +fb.canvas.style.width = `${W * Z}px`; +fb.canvas.style.height = `${H * Z}px`; +document.body.appendChild(fb.canvas); + +// Init terrain +const terrain = createTerrain(W, H); + +// Init projectiles +const projectiles = createCanvas(W, H); + +// Init players +players.push({x:100, y:H-80, a:0, p:100, c:'red', weapon:WEAPONS[0]}); +players.push({x:220, y:H-80, a:0, p:100, c:'blue', weapon:WEAPONS[1]}); + +function update() { + if (state === 'aim') { + const player = players[currentPlayer]; + const {x, y, a, p} = player; + + if (key('ArrowLeft')) { + player.a = wrap(0, a -1, 180); + } else if (key('ArrowRight')) { + player.a = wrap(0, a +1, 180); + } else if (key('ArrowUp')) { + player.p = clamp(100, p +5, 1000); + } else if (key('ArrowDown')) { + player.p = clamp(100, p -5, 1000); + } else if (key(' ')) { + state = 'fire'; + fadeProjectiles(); + const [px, py] = vec(x, y, a+180, 10); + projectile = { + player: player, + weapon: player.weapon, + ox:px, oy:py, a, p, + x:0, y:0, t: 0, + }; + } + } + + else if (state === 'fire') { + for (let i=0; i<10; i++) { + const {weapon, ox, oy, a, p, t} = projectile; + const [x, y] = parable(t, ox, oy, deg2rad(180+a), p/10, 9.8); + projectile.t += 0.01; + projectile.x = Math.round(x); + projectile.y = Math.round(y); + + if (y > H || isTerrain(terrain, x, y)) { + clipTerrain(terrain, (ctx) => drawExplosion(ctx, projectile.x, projectile.y, weapon.xr)); + currentPlayer = wrap(0, currentPlayer+1, players.length); + projectile = null; + state = 'aim'; + return; + } + } + } +} + +function draw() { + fb.clearRect(0, 0, W, H); + fb.drawImage(terrain.canvas, 0, 0); + if (projectile) drawProjectile(); + fb.drawImage(projectiles.canvas, 0, 0); + for (let tank of players) drawPlayer(tank); +} + +function drawPlayer(player) { + const {x, y, a, c} = player; + const [px, py] = vec(x, y, a+180, 10); + drawLine(fb, x, y, Math.round(px), Math.round(py), c); + drawStatus(); +} + +function drawProjectile() { + const {x, y, player} = projectile; + plot(projectiles, x, y, player.c); +} + +function fadeProjectiles() { + const imageData = projectiles.getImageData(0, 0, W, H); + for (let i=0; i { + update(); + draw(); +}); diff --git a/math.js b/math.js new file mode 100644 index 0000000..57cf168 --- /dev/null +++ b/math.js @@ -0,0 +1,43 @@ + +export function coords(width, i) { + return [i%width, Math.floor(i/width)]; +} + +export function index(width, x, y) { + return y * width + x; +} + +export function random(min, max) { + return min + Math.random() * (max-min); +} + +export function randomInt(min, max) { + return Math.round(random(min, max)); +} + +export function sample(arr) { + return arr[randomInt(0, arr.length-1)]; +} + +export function clamp(min, x, max) { + return Math.max(min, Math.min(x, max)); +} + +export function wrap(min, x, max) { + return ((x-min + max-min) % (max-min)) + min; +} + +export function parable(t, x, y, a, v, g) { + const fx = x + t * v * Math.cos(a); + const fy = y + t * v * Math.sin(a) - 0.5 * -g * t*t; + return [fx, fy]; +} + + +export function vec(x, y, a, l) { + a = a * Math.PI / 180; + return [x+Math.cos(a)*l, y+Math.sin(a)*l]; +} + +export function deg2rad(a) { return a * Math.PI/180 } +export function rad2deg(a) { return a * 180/Math.PI } diff --git a/terrain.js b/terrain.js new file mode 100644 index 0000000..d03efc2 --- /dev/null +++ b/terrain.js @@ -0,0 +1,20 @@ +import {createCanvas, drawRect, clipCanvas} from './gfx.js'; + +export function createTerrain(width, height) { + const terrain = createCanvas(width, height); + generateTerrain(terrain, width, height); + return terrain; +} + +export function generateTerrain(ctx, width, height) { + drawRect(ctx, 0, height-80, width, 80, 'burlywood'); +} + +export function isTerrain(ctx, x, y) { + const imageData = ctx.getImageData(x, y, 1, 1); + return imageData.data[3] > 0; +} + +export function clipTerrain(ctx, fn) { + return clipCanvas(ctx, fn); +} diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..3a55566 --- /dev/null +++ b/utils.js @@ -0,0 +1,17 @@ +export function nextId() { + // @ts-ignore + const id = nextId.id || 0; + // @ts-ignore + nextId.id = id + 1; + return id; +} + +export function splitWords(text) { + let words = ['']; + for (let char of text) { + if ([' ', '\n'].includes(char)) words.push(char, ''); + else words[words.length-1] += char; + } + if (words[words.length-1] === '') words = words.slice(0, words.length-1); + return words; +} diff --git a/weapons.js b/weapons.js new file mode 100644 index 0000000..c4ccc99 --- /dev/null +++ b/weapons.js @@ -0,0 +1,10 @@ +import {drawCircle} from './gfx.js'; + +export const WEAPONS = [ + {id:'baby-missile', name:'Baby Missile', xr:5}, + {id:'missile', name:'Missile', xr:10}, +]; + +export function drawExplosion(ctx, x, y, r) { + drawCircle(ctx, x, y, r, 'white'); +}