diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ae8df8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ + +*.js.map +*.js \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f147058 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "firefox", + "request": "launch", + "name": "Launch Firefox against localhost", + "url": "file:///${workspaceFolder}/build/index.html", + "webRoot": "${workspaceFolder}" + } + ] + } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..22f5739 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "typescript", + "tsconfig": "tsconfig.json", + "problemMatcher": [ + "$tsc" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "label": "tsc: build - tsconfig.json" + } + ] +} \ No newline at end of file diff --git a/build/img/balloon_red.png b/build/img/balloon_red.png new file mode 100644 index 0000000..0968b6a Binary files /dev/null and b/build/img/balloon_red.png differ diff --git a/build/img/player.png b/build/img/player.png new file mode 100644 index 0000000..f2f78de Binary files /dev/null and b/build/img/player.png differ diff --git a/build/index.html b/build/index.html new file mode 100644 index 0000000..95ca99c --- /dev/null +++ b/build/index.html @@ -0,0 +1,17 @@ + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/init.ts b/init.ts new file mode 100644 index 0000000..5c23028 --- /dev/null +++ b/init.ts @@ -0,0 +1,36 @@ +class Game { + + public state: State; + public renderer: Renderer; + + private lastUpdate: number; + + constructor() { + Input.init(); + this.state = new State(); + this.renderer = new Renderer(document.getElementById("canvas_main") as HTMLCanvasElement); + this.lastUpdate = NaN; + } + + mainLoop(time: DOMHighResTimeStamp) { + let deltaTime = (time - this.lastUpdate) / 1000; + this.lastUpdate = time; + + if (!isNaN(deltaTime)) { + this.state.update(deltaTime); + this.renderer.update(this.state); + Input.update(); + } + window.requestAnimationFrame(this.mainLoop.bind(this)); + } + + +} + +window.onload = () => { + + let game = new Game(); + + window.requestAnimationFrame(game.mainLoop.bind(game)); +} + diff --git a/input.ts b/input.ts new file mode 100644 index 0000000..63e2f47 --- /dev/null +++ b/input.ts @@ -0,0 +1,68 @@ +let Input = { + downKeys: {}, + pressedKeys: {}, + releasedKeys: {}, + + + // Mouse status + mouse: { + x:0, + y:0, + down: false, + click: false, + onscreen:false + }, + + setMouseState(event) { + this.mouse.x = event.clientX; + this.mouse.y = event.clientY; + this.mouse.down = (event.buttons & 1); + this.mouse.click = (event.buttons & 1); + }, + + // register the event listeners + init(){ + let me = this; + document.addEventListener('keydown', function(event) { + if (!me.downKeys[event.code]) { + me.pressedKeys[event.code] = true; + } + me.downKeys[event.code] = true; + console.log("Keydown: " + event.code); + }); + document.addEventListener('keyup', function(event) { + me.downKeys[event.code] = false; + me.releasedKeys[event.code] = true; + }); + + + document.onmousemove = this.setMouseState.bind(this); + document.onmousedown = this.setMouseState.bind(this); + document.onmouseup = this.setMouseState.bind(this); + document.onmouseleave = function() { me.mouseOnScreen = false; }; + document.onmouseenter = function() { me.mouseOnScreen = true; }; + }, + + // returns true if the key is currently held down + keyDown(key: string) { + return this.downKeys[key] == true; + }, + + // returns true if the key was pressed this update + keyPressed(key: string) { + return this.pressedKeys[key] == true; + }, + + // returns true if the key was released this update + keyReleased(key: string) { + return this.releasedKeys[key] == true; + }, + + // called to signal that a new update tick begins + update() { + this.pressedKeys = {}; + this.releasedKeys = {}; + this.mouse.click = false; + } + +} \ No newline at end of file diff --git a/player.ts b/player.ts new file mode 100644 index 0000000..b3cb85b --- /dev/null +++ b/player.ts @@ -0,0 +1,53 @@ +class Player implements Entity { + pos: Point; + sprite: CanvasImageSource; + renderPivot: Point = {x:32, y:64}; + + private isGrounded = false; + + private velocity: Point = {x: 0, y:0}; + + constructor() { + this.pos = { x: 0, y: -500}; + let image = new Image(); + Resources.setImage(this, ImageResource.PLAYER); + } + + update(deltaTime: number, state: State) : void { + // apply gravity + this.velocity.y += 1000 * deltaTime; + + if(Input.mouse.click) { + this.explosion({x: 0, y: 10}); + } + + // apply velocity + this.pos.x += this.velocity.x * deltaTime; + this.pos.y += this.velocity.y * deltaTime; + + // ground collision checking + if (this.pos.y > 0) { + this.pos.y = 0; + this.velocity = {x: 0, y: 0} + this.isGrounded = true; + } + + + } + + explosion(point: Point) { + console.log("Explosion"); + let distx = this.pos.x - point.x; + let disty = this.pos.y - point.y; + let dist = Math.sqrt(distx * distx + disty * disty); + + let force = 10000 / ( dist * dist + 100 ) + let normalized: Point = { x: distx / dist, y: disty / dist }; + + // it would be more physically correct to add the explosion force, but + // stopping the player actually feels more right + this.velocity.x = normalized.x * force; + this.velocity.y = normalized.y * force; + + } +} \ No newline at end of file diff --git a/renderer.ts b/renderer.ts new file mode 100644 index 0000000..e252ae1 --- /dev/null +++ b/renderer.ts @@ -0,0 +1,34 @@ +// the rendering system +class Renderer { + + static readonly COLOR_SKY: string = "#66ddff"; + static readonly COLOR_GROUND: string = "#88ddbb"; + + private canvas: HTMLCanvasElement; + private ctx: CanvasRenderingContext2D; + + + constructor(canvas: HTMLCanvasElement) { + this.canvas = canvas; + this.ctx = canvas.getContext("2d"); + } + + + update(state: State) { + // background + this.ctx.fillStyle = Renderer.COLOR_SKY; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height) + // ground + let offset: Point = { x: this.canvas.width / 2 - state.cameraFocus.x, y: this.canvas.height / 2 - state.cameraFocus.y }; + this.ctx.fillStyle = Renderer.COLOR_GROUND; + this.ctx.fillRect(0, offset.y, this.canvas.width, this.canvas.height); + + // objects + state.entities.forEach(r => { + if (r.sprite) { + this.ctx.drawImage(r.sprite, r.pos.x + offset.x - r.renderPivot.x, r.pos.y + offset.y - r.renderPivot.y ); + } + }) + + } +} \ No newline at end of file diff --git a/resources.ts b/resources.ts new file mode 100644 index 0000000..564248c --- /dev/null +++ b/resources.ts @@ -0,0 +1,19 @@ +// handler for resources +enum ImageResource { + PLAYER = "img/player.png" +} + +class Resources { + + + public static setImage(entity: Entity, res: ImageResource) : void { + let image = new Image(); + image.src = res; + image.onload = () => { + entity.sprite = image; + } + image.onerror = () => { + console.error("Could not load image " + res); + } + } +} \ No newline at end of file diff --git a/state.ts b/state.ts new file mode 100644 index 0000000..7d0a4b5 --- /dev/null +++ b/state.ts @@ -0,0 +1,23 @@ +// the main game-state class +class State { + + public cameraFocus: Point ; + public entities: Entity[]; + + private player: Player; + + + constructor() { + this.cameraFocus = {x:0, y: -200}; + this.entities = Array(); + + this.player = new Player(); + this.entities.push(this.player); + + } + + update(deltaTime: number) { + this.entities.forEach(e => e.update(deltaTime, this)); + } + +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..34687c8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "CommonJS", + "sourceMap": true, + "outDir": "build/js", + + } + } \ No newline at end of file diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..235d598 --- /dev/null +++ b/types.ts @@ -0,0 +1,13 @@ +// some general utility types + +interface Point { + x: number + y: number +} + +interface Entity { + pos: Point; + sprite: CanvasImageSource; + renderPivot: Point; + update(deltaTime: number, state:State) : void; +} \ No newline at end of file