From f47b2e3f68e69238b731d6183e739805db20ae5b Mon Sep 17 00:00:00 2001 From: Joris Date: Sun, 14 Feb 2021 20:25:55 +0100 Subject: Control a ship that can fire missiles --- .gitignore | 1 + README.md | 9 +++++ bin/watch.sh | 8 +++++ public/icon.png | Bin 0 -> 1060 bytes public/index.html | 27 +++++++++++++++ public/main.css | 11 +++++++ shell.nix | 16 +++++++++ src/controls.ts | 38 ++++++++++++++++++++++ src/main.ts | 11 +++++++ src/model/vec2.ts | 15 +++++++++ src/screen.ts | 4 +++ src/util/number.ts | 3 ++ src/view/colors.ts | 4 +++ src/view/scene.ts | 35 ++++++++++++++++++++ src/view/ship.ts | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 13 ++++++++ 16 files changed, 289 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bin/watch.sh create mode 100644 public/icon.png create mode 100644 public/index.html create mode 100644 public/main.css create mode 100644 shell.nix create mode 100644 src/controls.ts create mode 100644 src/main.ts create mode 100644 src/model/vec2.ts create mode 100644 src/screen.ts create mode 100644 src/util/number.ts create mode 100644 src/view/colors.ts create mode 100644 src/view/scene.ts create mode 100644 src/view/ship.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b62f3b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/public/main.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f5b91f --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Getting started + +Run: + +```sh +nix-shell --run bin/watch.sh +``` + +Then, open your browser at `http://localhost:8000`. diff --git a/bin/watch.sh b/bin/watch.sh new file mode 100755 index 0000000..f38a2c8 --- /dev/null +++ b/bin/watch.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +python -m http.server --directory public 8000 & + +trap "fuser -k 8000/tcp" EXIT + +tsc --watch diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..3bc9ba2 Binary files /dev/null and b/public/icon.png differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..bcedaf9 --- /dev/null +++ b/public/index.html @@ -0,0 +1,27 @@ + + + + +Shoot + + + + + + + + diff --git a/public/main.css b/public/main.css new file mode 100644 index 0000000..2654c0e --- /dev/null +++ b/public/main.css @@ -0,0 +1,11 @@ +html { + height: 100%; +} + +body { + height: 100%; + margin: 0; + display: flex; + justify-content: center; + align-items: center; +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..ad4a34d --- /dev/null +++ b/shell.nix @@ -0,0 +1,16 @@ +with (import (builtins.fetchGit { + name = "nixpkgs-20.09"; + url = "git@github.com:nixos/nixpkgs.git"; + rev = "cd63096d6d887d689543a0b97743d28995bc9bc3"; + ref = "refs/tags/20.09"; +}){}); + +mkShell { + + buildInputs = [ + nodePackages.typescript + python3 + psmisc # fuser + ]; + +} diff --git a/src/controls.ts b/src/controls.ts new file mode 100644 index 0000000..b575b3f --- /dev/null +++ b/src/controls.ts @@ -0,0 +1,38 @@ +export interface Controls { + up: boolean, + right: boolean, + down: boolean, + left: boolean, + space: boolean, +} + +export let current = { + up: false, + right: false, + down: false, + left: false, + space: false, +} + +document.addEventListener('keydown', event => { + current = update(current, event.key, true) +}) + +document.addEventListener('keyup', event => { + current = update(current, event.key, false) +}) + +function update(current: Controls, key: string, isDown: boolean): Controls { + if (key === 'ArrowUp') + return { ...current, up: isDown } + else if (key === 'ArrowRight') + return { ...current, right: isDown } + else if (key === 'ArrowDown') + return { ...current, down: isDown } + else if (key === 'ArrowLeft') + return { ...current, left: isDown } + else if (key === ' ') + return { ...current, space: isDown } + else + return current +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..53bc487 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,11 @@ +import * as Scene from 'view/scene' + +let scene: Scene.State = Scene.init() + +function loop(timestamp: number) { + Scene.update(scene, timestamp) + Scene.view(scene) + window.requestAnimationFrame(loop) +} + +window.requestAnimationFrame(loop) diff --git a/src/model/vec2.ts b/src/model/vec2.ts new file mode 100644 index 0000000..4bec53d --- /dev/null +++ b/src/model/vec2.ts @@ -0,0 +1,15 @@ +export interface Vec2 { + x: number, + y: number, +} + +export function zero(): Vec2 { + return { + x: 0, + y: 0, + } +} + +export function equals(v1: Vec2, v2: Vec2): boolean { + return v1.x === v2.x && v1.y === v2.y +} diff --git a/src/screen.ts b/src/screen.ts new file mode 100644 index 0000000..930534e --- /dev/null +++ b/src/screen.ts @@ -0,0 +1,4 @@ +let canvas = document.querySelector('canvas') as HTMLCanvasElement + +export let width: number = canvas.width +export let height: number = canvas.height diff --git a/src/util/number.ts b/src/util/number.ts new file mode 100644 index 0000000..87f1e2e --- /dev/null +++ b/src/util/number.ts @@ -0,0 +1,3 @@ +export function clamp(x: number, min: number, max: number): number { + return Math.max(min, Math.min(max, x)) +} diff --git a/src/view/colors.ts b/src/view/colors.ts new file mode 100644 index 0000000..c663d25 --- /dev/null +++ b/src/view/colors.ts @@ -0,0 +1,4 @@ +export let colors = { + blue: "#333388", + red: "#CC3333", +} diff --git a/src/view/scene.ts b/src/view/scene.ts new file mode 100644 index 0000000..fe88c12 --- /dev/null +++ b/src/view/scene.ts @@ -0,0 +1,35 @@ +import * as Ship from 'view/ship' +import * as Colors from 'view/colors' +import * as Screen from 'screen' + +export interface State { + context: CanvasRenderingContext2D, + timestamp: number, + ship: Ship.State, +} + +export function init(): State { + let canvas = document.querySelector('canvas') as HTMLCanvasElement + let context = canvas.getContext("2d") as CanvasRenderingContext2D + + return { + context, + timestamp: 0, + ship: Ship.init(), + } +} + +export function update(state: State, timestamp: number) { + let delta = timestamp - state.timestamp + state.timestamp = timestamp + + Ship.update(state.ship, state.timestamp, delta) +} + +export function view(state: State) { + // Clear + state.context.fillStyle = Colors.colors.blue + state.context.fillRect(0, 0, Screen.width, Screen.height) + + Ship.view(state.context, state.ship) +} diff --git a/src/view/ship.ts b/src/view/ship.ts new file mode 100644 index 0000000..92590d3 --- /dev/null +++ b/src/view/ship.ts @@ -0,0 +1,94 @@ +import * as Controls from 'controls' +import * as Vec2 from 'model/vec2' +import * as Number from 'util/number' +import * as Screen from 'screen' +import * as Colors from 'view/colors' + +export const radius: number = 30 +export const fireDelay: number = 200 +export const missileWidth: number = 10 +export const missileHeight: number = 5 + +export interface State { + pos: Vec2.Vec2, + missiles: Array, + lastFired: number, +} + +export function init(): State { + return { + pos: { + x: Screen.width / 6, + y: Screen.height / 2, + }, + missiles: [], + lastFired: 0, + } +} + +export function update(state: State, timestamp: number, delta: number) { + move(state, delta) + updateMissiles(state, timestamp, delta) +} + +function move(state: State, delta: number) { + let dir = controlsDir(Controls.current) + + if (!Vec2.equals(dir, Vec2.zero())) { + let teta = Math.atan2(dir.y, dir.x) + state.pos.x += Math.cos(teta) * delta / 3 + state.pos.y += Math.sin(teta) * delta / 3 + } + + state.pos.x = Number.clamp(state.pos.x, radius, Screen.width - radius) + state.pos.y = Number.clamp(state.pos.y, radius, Screen.height - radius) +} + +function controlsDir(c: Controls.Controls): Vec2.Vec2 { + let dir = Vec2.zero() + + if (c.up && !c.down) + dir.y = -1 + else if (c.down && !c.up) + dir.y = 1 + + if (c.right && !c.left) + dir.x = 1 + else if (c.left && !c.right) + dir.x = -1 + + return dir +} + +function updateMissiles(state: State, timestamp: number, delta: number) { + if (Controls.current.space && state.lastFired + fireDelay < timestamp) { + state.missiles.push({x: state.pos.x + radius, y: state.pos.y}) + state.lastFired = timestamp + } + + state.missiles = state.missiles + .map(missile => ({ ...missile, x: missile.x + delta})) + .filter(missile => missile.x < Screen.width) +} + +export function view(context: CanvasRenderingContext2D, state: State) { + context.fillStyle = Colors.colors.red + context.beginPath() + context.moveTo(state.pos.x - radius, state.pos.y - radius) + context.lineTo(state.pos.x + radius, state.pos.y) + context.lineTo(state.pos.x - radius, state.pos.y + radius) + context.closePath() + context.fill() + + state.missiles.forEach(({x, y}) => { + context.fillStyle = Colors.colors.red + context.beginPath() + context.rect( + x - missileWidth / 2, + y - missileHeight / 2, + missileWidth, + missileHeight + ) + context.fill() + }) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3e7f32b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "amd", + "target": "es5", + "baseUrl": "src", + "outFile": "public/main.js", + "noImplicitAny": true, + "strictNullChecks": true, + "removeComments": true, + "preserveConstEnums": true + }, + "include": ["src/**/*"] +} -- cgit v1.2.3