From 49591cbea39c9295f666df749ed65d4be2cb9125 Mon Sep 17 00:00:00 2001 From: zenoamaro Date: Sun, 23 Jun 2019 16:58:13 +0200 Subject: [PATCH] Prototype --- font.ttf | Bin 0 -> 11872 bytes gfx.js | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 19 +++++++++ input.js | 5 +++ main.js | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++ math.js | 43 ++++++++++++++++++++ terrain.js | 20 ++++++++++ utils.js | 17 ++++++++ weapons.js | 10 +++++ 9 files changed, 338 insertions(+) create mode 100644 font.ttf create mode 100644 gfx.js create mode 100644 index.html create mode 100644 input.js create mode 100644 main.js create mode 100644 math.js create mode 100644 terrain.js create mode 100644 utils.js create mode 100644 weapons.js diff --git a/font.ttf b/font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3ddfed4b4762374cf8e03138eb7c79ee6d0a262b GIT binary patch literal 11872 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 GivA0r4^3zQ literal 0 HcmV?d00001 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'); +}