"use strict" class Clock { constructor() { this.currentFrametime = 0 this.currentFps = 0 this.frames = 0 this.timeSpan = 0 } update(deltaT) { if (typeof deltaT !== 'number') return; this.timeSpan += deltaT this.frames++ if (this.timeSpan >= 500) { this.currentFrametime = this.timeSpan / this.frames this.currentFps = 1000 / this.currentFrametime this.timeSpan = 0 this.frames = 0 } } } class UI { constructor(context, clock) { this.context = context this.context.font = "32px monospace" this.clock = clock } draw() { this.context.fillStyle = '#FFF' this.context.fillText(`frametime: ${this.clock.currentFrametime.toFixed(1)} ms`, 10, 32); this.context.fillText(`fps: ${Math.round(this.clock.currentFps)}`, 10, 64); } } class Vector2 { constructor(x, y) { this.x = x this.y = y } length() { return Math.sqrt((this.x * this.x) + (this.y * this.y)) } normalize() { const magnitude = this.length() if (magnitude === 0) return; this.x /= magnitude this.y /= magnitude } add(vector2) { this.x += vector2.x this.y += vector2.y } multiply(scalar) { this.x *= scalar this.y *= scalar } dot(vector2) { return vector2.x * this.x + vector2.y * this.y } } class Weapon { constructor(player, canvas) { this.player = player this.angle = 0 this.mousePosition = new Vector2(0, 0) this.rect = canvas.getBoundingClientRect() this.width = 10 this.height = 50 document.addEventListener("mousemove", (event) => { this.mousePosition.x = event.clientX - this.rect.left this.mousePosition.y = event.clientY - this.rect.top }) } get pivotX() { return this.player.position.x + (this.player.width / 2) } get pivotY() { return this.player.position.y + (this.player.height / 2) } update(deltaT, camera) { const worldMouseX = (this.mousePosition.x / camera.zoom) + camera.x const worldMouseY = (this.mousePosition.y / camera.zoom) + camera.y const dx = worldMouseX - this.pivotX const dy = worldMouseY - this.pivotY this.angle = Math.atan2(dy, dx) - Math.PI / 2 } draw(context) { context.save() context.translate(this.pivotX, this.pivotY) context.rotate(this.angle) context.fillStyle = '#F00' context.fillRect(-(this.width / 2), 0, this.width, this.height) context.restore() } } class Player { constructor(position) { this.position = position this.direction = new Vector2(0, 0) this.velocity = new Vector2(0, 0) this.speed = 500 this.width = 50 this.height = 50 this.keys = { w: false, a: false, s: false, d: false } document.addEventListener('keydown', (event) => { if (this.keys[event.key] !== undefined) { this.keys[event.key] = true } }) document.addEventListener('keyup', (event) => { if (this.keys[event.key] !== undefined) { this.keys[event.key] = false } }) } update(deltaT) { this.direction.x = 0 this.direction.y = 0 if (this.keys.w) this.direction.y -= 1 if (this.keys.a) this.direction.x -= 1 if (this.keys.s) this.direction.y += 1 if (this.keys.d) this.direction.x += 1 this.direction.normalize() this.velocity.x = this.direction.x * this.speed this.velocity.y = this.direction.y * this.speed this.velocity.multiply(deltaT) this.position.add(this.velocity) } draw(context) { context.fillStyle = '#00A' context.fillRect(this.position.x, this.position.y, this.width, this.height) } } class Camera { constructor(viewportWidth, viewportHeight) { this.x = 0 this.y = 0 this.zoom = 2.0 this.viewportWidth = viewportWidth this.viewportHeight = viewportHeight } follow(target) { this.x = target.x - (this.viewportWidth / 2) / this.zoom this.y = target.y - (this.viewportHeight / 2) / this.zoom } begin(context) { context.save() context.scale(this.zoom, this.zoom) context.translate(-this.x, -this.y) } end(context) { context.restore() } } class Game { constructor (canvas, context) { this._WIDTH = 1920 this._HEIGHT = 1080 this.isRunning = true this.lastTime = undefined this.deltaTime = undefined this.context = context this.canvas = canvas this.canvas.width = this._WIDTH this.canvas.height = this._HEIGHT this.clock = new Clock() this.UI = new UI(this.context, this.clock) this.player = new Player(new Vector2(200, 200)) this.weapon = new Weapon(this.player, this.canvas) this.camera = new Camera(this._WIDTH, this._HEIGHT) } update() { this.clock.update(this.deltaTime) const deltaTInSeconds = this.deltaTime / 1000 if (!isNaN(deltaTInSeconds)) { this.player.update(deltaTInSeconds) this.weapon.update(deltaTInSeconds, this.camera) } this.camera.follow(new Vector2(this.player.position.x + 25, this.player.position.y + 25)) } clear() { this.context.fillStyle = "#777" this.context.fillRect(0, 0, this._WIDTH, this._HEIGHT) } draw() { this.clear() this.camera.begin(this.context) this.context.fillStyle = '#BB0022' const c = 20 for (let x = 100; x < this._WIDTH - 100; x += c) { for (let y = 100; y < this._HEIGHT - 100; y += c) { this.context.fillRect(x, y, c/2, c/2) } } this.player.draw(this.context) this.weapon.draw(this.context) this.camera.end(this.context) this.UI.draw() } run(timeSpan) { if (this.lastTime === undefined) { this.lastTime = timeSpan } else { this.deltaTime = timeSpan - this.lastTime this.lastTime = timeSpan } this.update() this.draw() if (!this.isRunning) return; requestAnimationFrame((d) => this.run(d)) } } const canvas = document.querySelector("canvas") const context = canvas.getContext("2d") const game = new Game(canvas, context) game.run()