commit d940db331a39db4cca9d05f70d9c0d219700ca2e Author: sfj Date: Thu Aug 14 16:40:37 2025 +0200 init diff --git a/assets/bullet1.png b/assets/bullet1.png new file mode 100644 index 0000000..d9cc8c2 Binary files /dev/null and b/assets/bullet1.png differ diff --git a/assets/bullet2.png b/assets/bullet2.png new file mode 100644 index 0000000..529a39c Binary files /dev/null and b/assets/bullet2.png differ diff --git a/assets/enemy1.png b/assets/enemy1.png new file mode 100644 index 0000000..66e1a00 Binary files /dev/null and b/assets/enemy1.png differ diff --git a/assets/enemy2.png b/assets/enemy2.png new file mode 100644 index 0000000..420f705 Binary files /dev/null and b/assets/enemy2.png differ diff --git a/assets/explosion1.png b/assets/explosion1.png new file mode 100644 index 0000000..def5b4a Binary files /dev/null and b/assets/explosion1.png differ diff --git a/assets/explosion2.png b/assets/explosion2.png new file mode 100644 index 0000000..3d196a7 Binary files /dev/null and b/assets/explosion2.png differ diff --git a/assets/explosion3.png b/assets/explosion3.png new file mode 100644 index 0000000..6708801 Binary files /dev/null and b/assets/explosion3.png differ diff --git a/assets/explosion4.png b/assets/explosion4.png new file mode 100644 index 0000000..65f5970 Binary files /dev/null and b/assets/explosion4.png differ diff --git a/assets/player.png b/assets/player.png new file mode 100644 index 0000000..df35cba Binary files /dev/null and b/assets/player.png differ diff --git a/game.js b/game.js new file mode 100644 index 0000000..d9bf300 --- /dev/null +++ b/game.js @@ -0,0 +1,143 @@ +import * as lib from "./lib.js" + +const playerSprite = await lib.texture.loadImage("assets/player.png"); +const bulletSprite = await lib.texture.loadImage("assets/bullet1.png"); +const enemySprite = await lib.texture.loadImage("assets/enemy1.png"); + +const explosionSprites = await Promise.all( + [1, 2, 3, 4] + .map((id) => lib.texture.loadImage(`assets/explosion${id}.png`)) + .map((texture) => texture.then(texture => { + texture.width = 64; + texture.height = 64; + texture.multiplyPixelColor(1, 1, 0) + return texture; + })), +); + +playerSprite.width = 64; +playerSprite.height = 64; +playerSprite.multiplyPixelColor(1, 0.5, 0); + +bulletSprite.width = 32; +bulletSprite.height = 32; +bulletSprite.rotate(Math.PI); + +enemySprite.width = 64; +enemySprite.height = 64; +enemySprite.multiplyPixelColor(0.2, 0.2, 1) + +const playerSpeed = 300; +const bulletSpeed = 400; + +let playerX = 0; + +let bullets = []; +const bulletCoolDown = 0.5; +let bulletCooldownTimer = bulletCoolDown; + +let enemies = [ + { x: 10, y: 0 }, + { x: 110, y: 0 }, + { x: 210, y: 0 }, + { x: 310, y: 0 }, + { x: 410, y: 0 }, + { x: 10, y: 100 }, + { x: 110, y: 100 }, + { x: 210, y: 100 }, + { x: 310, y: 100 }, + { x: 410, y: 100 }, +]; + +let explosions = []; +const explosionDuration = 0.5; + +function loop() { + + const goLeft = lib.isPressed("ArrowLeft"); + const goRight = lib.isPressed("ArrowRight"); + if (goLeft && !goRight) { + playerX -= playerSpeed * lib.frameDeltaT; + if (playerX < 0) { + playerX = 0; + } + } else if (goRight && !goLeft) { + playerX += playerSpeed * lib.frameDeltaT; + if (playerX >= lib.canvas.width - playerSprite.width) { + playerX = lib.canvas.width - playerSprite.width; + } + } + + for (const bullet of bullets) { + bullet.y -= bulletSpeed * lib.frameDeltaT + } + bulletCooldownTimer += lib.frameDeltaT; + + let deadEnemies = [] + let deadBullets = [] + for (let enemyIdx = 0; enemyIdx < enemies.length; ++enemyIdx) { + + const enemy = enemies[enemyIdx]; + + for (let bulletIdx = 0; bulletIdx < bullets.length; ++bulletIdx) { + const bullet = bullets[bulletIdx]; + + if (bullet.x < enemy.x + 64 && bullet.x + 32 >= enemy.x + && bullet.y < enemy.y + 64 && bullet.y + 32 >= enemy.y) { + deadBullets.push(bulletIdx); + deadEnemies.push(enemyIdx); + explosions.push({ x: bullet.x, y: bullet.y, time: 0 }); + } + + } + } + for (const i of deadEnemies.toReversed()) { + enemies.splice(i, 1) + } + for (const i of deadBullets.toReversed()) { + bullets.splice(i, 1) + } + + let deadExplosions = [] + for (let i = 0; i < explosions.length; ++i) { + const explosion = explosions[i]; + explosion.time += lib.frameDeltaT; + if (explosion.time > explosionDuration) { + deadExplosions.push(i); + } + } + for (const i of deadExplosions.toReversed()) { + explosions.splice(i, 1) + } + + const cx = lib.canvas; + + cx.clear("black"); + + cx.putTexture(playerSprite, playerX, 300); + + for (const bullet of bullets) { + cx.putTexture(bulletSprite, bullet.x, bullet.y); + } + + for (const enemy of enemies) { + cx.putTexture(enemySprite, enemy.x, enemy.y); + } + + for (const explosion of explosions) { + const idx = Math.floor(explosionSprites.length * (explosion.time / explosionDuration)); + const texture = explosionSprites[idx]; + cx.putTexture(texture, explosion.x - 32, explosion.y - 32); + } +} + +lib.onPress(" ", () => { + if (bulletCooldownTimer < bulletCoolDown) + return; + bullets.push({ x: playerX + 16, y: 300 }); + bulletCooldownTimer = 0; +}) + +lib.startGame(loop); + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..ed7e1e2 --- /dev/null +++ b/index.html @@ -0,0 +1,32 @@ + + + + + + My Game + + + + + + + + + \ No newline at end of file diff --git a/lib.js b/lib.js new file mode 100644 index 0000000..e1a9e6f --- /dev/null +++ b/lib.js @@ -0,0 +1,68 @@ + +export * as canvas from "./lib/canvas.js" +export * as texture from "./lib/texture.js" + +export let frameDeltaT = 0; + +/** + * Start a game. + * + * @param {() => void} initFunction Run once at start + * @param {() => void} loopFunction Run every frame + */ +export function startGame(loopFunction) { + let before = Date.now(); + setInterval(() => { + const now = Date.now(); + frameDeltaT = (now - before) / 1000; + before = now; + + loopFunction(); + }, 16); +} + +const keysPressed = new Set(); +const keyPressHandlers = new Map(); +const keyReleaseHandlers = new Map(); + +document.body.addEventListener("keydown", (ev) => { + keysPressed.add(ev.key); + keyPressHandlers.get(ev.key)?.(); +}); + +document.body.addEventListener("keyup", (ev) => { + keysPressed.delete(ev.key); + keyReleaseHandlers.get(ev.key)?.(); +}); + +/** + * If a key is currently being pressed. + * @param {string} key + * @returns {boolean} + */ +export function isPressed(key) { + return keysPressed.has(key); +} + +/** + * When a key is pressed (key down). + * @param {string} key + * @param {() => void} handlerFunction + */ +export function onPress(key, handlerFunction) { + keyPressHandlers.set(key, handlerFunction); +} + +/** + * When a key is released (key up). + * @param {string} key + * @param {() => void} handlerFunction + */ +export function onRelease(key, handlerFunction) { + keyReleaseHandlers.set(key, handlerFunction); +} + + + + + diff --git a/lib/canvas.js b/lib/canvas.js new file mode 100644 index 0000000..a25fb95 --- /dev/null +++ b/lib/canvas.js @@ -0,0 +1,41 @@ +import { Texture } from "./texture.js"; + +/** @type {HTMLCanvasElement} */ +const htmlCanvas = document.querySelector("#game"); +const ctx = htmlCanvas.getContext("2d"); +ctx.imageSmoothingEnabled = false; + +export const width = htmlCanvas.width; +export const height = htmlCanvas.height; + +/** + * Clear canvas with a color. + * @param {string} color + */ +export function clear(color) { + ctx.fillStyle = color; + ctx.fillRect(0, 0, width, height); +} + +/** + * Draw a filled rectangle. + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + * @param {string} color + */ +export function fillRect(x, y, width, height, color) { + ctx.fillStyle = color; + ctx.fillRect(x, y, width, height); +} + +/** + * + * @param {Texture} texture + * @param {number} x + * @param {number} y + */ +export function putTexture(texture, x, y) { + texture.draw(ctx, x, y); +} diff --git a/lib/texture.js b/lib/texture.js new file mode 100644 index 0000000..a7dd03c --- /dev/null +++ b/lib/texture.js @@ -0,0 +1,102 @@ + +export class Texture { + /** + * + * @param {HTMLCanvasElement} canvas + */ + constructor(canvas) { + this.canvas = canvas; + } + + /** + * + * @param {HTMLImageElement} imageElement + * @returns {Texture} + */ + static fromImage(imageElement) { + const canvas = new OffscreenCanvas(imageElement.width, imageElement.height); + const ctx = canvas.getContext("2d"); + ctx.drawImage(imageElement, 0, 0); + return new Texture(canvas); + } + + get width() { + return this.canvas.width; + } + get height() { + return this.canvas.height; + } + + set width(value) { + const newCanvas = new OffscreenCanvas(value, this.canvas.height); + const ctx = newCanvas.getContext("2d"); + ctx.imageSmoothingEnabled = false; + ctx.drawImage(this.canvas, 0, 0, value, this.canvas.height); + this.canvas = newCanvas; + } + set height(value) { + const newCanvas = new OffscreenCanvas(this.canvas.width, value); + const ctx = newCanvas.getContext("2d"); + ctx.imageSmoothingEnabled = false; + ctx.drawImage(this.canvas, 0, 0, this.canvas.width, value); + this.canvas = newCanvas; + } + + /** + * + * @param {CanvasRenderingContext2D} ctx + * @param {number} x + * @param {number} y + */ + draw(ctx, x, y) { + ctx.drawImage(this.canvas, x, y); + } + + /** + * + * @param {number} red + * @param {number} green + * @param {number} blue + */ + multiplyPixelColor(red, green, blue) { + const ctx = this.canvas.getContext("2d"); + const data = ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); + for (let y = 0; y < data.height; ++y) { + for (let x = 0; x < data.width; ++x) { + const idx = y * (data.width * 4) + x * 4; + data.data[idx] *= red; + data.data[idx + 1] *= green; + data.data[idx + 2] *= blue; + } + } + ctx.putImageData(data, 0, 0); + } + + rotate(angle) { + const { width, height } = this; + const newCanvas = new OffscreenCanvas(width, height); + const newCtx = newCanvas.getContext("2d"); + newCtx.imageSmoothingEnabled = false; + newCtx.save(); + newCtx.translate(width / 2, height / 2) + newCtx.rotate(angle) + newCtx.drawImage(this.canvas, -width / 2 , -height / 2, width, height); + newCtx.restore(); + this.canvas = newCanvas; + } +} + +/** + * + * @param {string} path + * @returns {Promise} + */ +export function loadImage(path) { + const image = document.createElement("img"); + image.src = path; + return new Promise((resolve) => { + image.onload = () => { + resolve(Texture.fromImage(image)); + } + }); +}