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