diff --git a/src/DataTypes/Collection.ts b/src/DataTypes/Collection.ts index f6a1fa0..749a24b 100644 --- a/src/DataTypes/Collection.ts +++ b/src/DataTypes/Collection.ts @@ -1,3 +1,17 @@ + +// TODO - Is there already a way to do this in js/ts? +/** + * An interface for all iterable data custom data structures + */ export default interface Collection { + /** + * Iterates through all of the items in this data structure. + * @param func + */ forEach(func: Function): void; + + /** + * Clears the contents of the data structure + */ + clear(): void; } \ No newline at end of file diff --git a/src/DataTypes/Map.ts b/src/DataTypes/Map.ts index 29c11f1..eab373a 100644 --- a/src/DataTypes/Map.ts +++ b/src/DataTypes/Map.ts @@ -1,5 +1,8 @@ import Collection from "./Collection"; +/** + * Associates strings with elements of type T + */ export default class Map implements Collection { private map: Record; @@ -7,31 +10,52 @@ export default class Map implements Collection { this.map = {}; } + /** + * Adds a value T stored at a key. + * @param key + * @param value + */ add(key: string, value: T): void { this.map[key] = value; } + /** + * Get the value associated with a key. + * @param key + */ get(key: string): T { return this.map[key]; } + /** + * Sets the value stored at key to the new specified value + * @param key + * @param value + */ set(key: string, value: T): void { this.add(key, value); } + /** + * Returns true if there is a value stored at the specified key, false otherwise. + * @param key + */ has(key: string): boolean { return this.map[key] !== undefined; } + /** + * Returns an array of all of the keys in this map. + */ keys(): Array { return Object.keys(this.map); } - forEach(func: Function): void { + forEach(func: (key: string) => void): void { Object.keys(this.map).forEach(key => func(key)); } clear(): void { - this.forEach((key: string) => delete this.map[key]); + this.forEach(key => delete this.map[key]); } } \ No newline at end of file diff --git a/src/DataTypes/Queue.ts b/src/DataTypes/Queue.ts index 3539508..90b054d 100644 --- a/src/DataTypes/Queue.ts +++ b/src/DataTypes/Queue.ts @@ -1,6 +1,9 @@ import Collection from "./Collection"; -export default class Queue implements Collection{ +/** + * A FIFO queue with elements of type T + */ +export default class Queue implements Collection { private readonly MAX_ELEMENTS: number; private q: Array; private head: number; @@ -15,6 +18,10 @@ export default class Queue implements Collection{ this.size = 0; } + /** + * Adds an item to the back of the queue + * @param item + */ enqueue(item: T): void{ if((this.tail + 1) % this.MAX_ELEMENTS === this.head){ throw "Queue full - cannot add element" @@ -25,6 +32,9 @@ export default class Queue implements Collection{ this.tail = (this.tail + 1) % this.MAX_ELEMENTS; } + /** + * Retrieves an item from the front of the queue + */ dequeue(): T { if(this.head === this.tail){ throw "Queue empty - cannot remove element" @@ -33,11 +43,16 @@ export default class Queue implements Collection{ this.size -= 1; let item = this.q[this.head]; + // Now delete the item + delete this.q[this.head]; this.head = (this.head + 1) % this.MAX_ELEMENTS; return item; } + /** + * Returns the item at the front of the queue, but does not return it + */ peekNext(): T { if(this.head === this.tail){ throw "Queue empty - cannot get element" @@ -48,24 +63,30 @@ export default class Queue implements Collection{ return item; } + /** + * Returns true if the queue has items in it, false otherwise + */ hasItems(): boolean { return this.head !== this.tail; } + /** + * Returns the number of elements in the queue. + */ getSize(): number { return this.size; } - // TODO: This should actually delete the items in the queue instead of leaving them here clear(): void { + this.forEach((item, index) => delete this.q[index]); this.size = 0; this.head = this.tail; } - forEach(func: Function): void { + forEach(func: (item: T, index?: number) => void): void { let i = this.head; while(i !== this.tail){ - func(this.q[i]); + func(this.q[i], i); i = (i + 1) % this.MAX_ELEMENTS; } } diff --git a/src/DataTypes/Stack.ts b/src/DataTypes/Stack.ts index 2ec5aed..338cdee 100644 --- a/src/DataTypes/Stack.ts +++ b/src/DataTypes/Stack.ts @@ -1,6 +1,9 @@ import Collection from "./Collection"; -export default class Stack implements Collection{ +/** + * A LIFO stack with items of type T + */ +export default class Stack implements Collection { readonly MAX_ELEMENTS: number; private stack: Array; private head: number; @@ -13,7 +16,7 @@ export default class Stack implements Collection{ /** * Adds an item to the top of the stack - * @param {*} item The new item to add to the stack + * @param item The new item to add to the stack */ push(item: T): void { if(this.head + 1 === this.MAX_ELEMENTS){ @@ -44,10 +47,8 @@ export default class Stack implements Collection{ return this.stack[this.head]; } - /** - * Removes all elements from the stack - */ - clear(): void{ + clear(): void { + this.forEach((item, index) => delete this.stack[index]); this.head = -1; } @@ -58,7 +59,7 @@ export default class Stack implements Collection{ return this.head + 1; } - forEach(func: Function): void{ + forEach(func: (item: T, index?: number) => void): void{ let i = 0; while(i <= this.head){ func(this.stack[i]); diff --git a/src/DataTypes/Tilesets/TiledData.ts b/src/DataTypes/Tilesets/TiledData.ts index 7314954..6cbea50 100644 --- a/src/DataTypes/Tilesets/TiledData.ts +++ b/src/DataTypes/Tilesets/TiledData.ts @@ -1,3 +1,6 @@ +/** + * a representation of Tiled's tilemap data + */ export class TiledTilemapData { height: number; width: number; @@ -8,12 +11,18 @@ export class TiledTilemapData { tilesets: Array; } +/** + * A representation of a custom layer property in a Tiled tilemap + */ export class TiledLayerProperty { name: string; type: string; value: any; } +/** + * A representation of a tileset in a Tiled tilemap + */ export class TiledTilesetData { columns: number; tilewidth: number; @@ -28,6 +37,9 @@ export class TiledTilesetData { image: string; } +/** + * A representation of a layer in a Tiled tilemap + */ export class TiledLayerData { data: number[]; x: number; diff --git a/src/DataTypes/Tilesets/Tileset.ts b/src/DataTypes/Tilesets/Tileset.ts index 01635d6..b7c7f10 100644 --- a/src/DataTypes/Tilesets/Tileset.ts +++ b/src/DataTypes/Tilesets/Tileset.ts @@ -17,9 +17,14 @@ export default class Tileset { // TODO: Change this to be more general and work with other tileset formats constructor(tilesetData: TiledTilesetData){ + // Defer handling of the data to a helper class this.initFromTiledData(tilesetData); } + /** + * Initialize the tileset from the data from a Tiled json file + * @param tiledData The parsed object from a Tiled json file + */ initFromTiledData(tiledData: TiledTilesetData): void { this.numRows = tiledData.tilecount/tiledData.columns; this.numCols = tiledData.columns; @@ -58,23 +63,32 @@ export default class Tileset { return this.numCols; } - // TODO: This should probably be a thing that is tracked in the resource loader, not here - isReady(): boolean { - return this.image !== null; - } - hasTile(tileIndex: number): boolean { return tileIndex >= this.startIndex && tileIndex <= this.endIndex; } + /** + * Render a singular tile with index tileIndex from the tileset located at position dataIndex + * @param ctx The rendering context + * @param tileIndex The value of the tile to render + * @param dataIndex The index of the tile in the data array + * @param worldSize The size of the world + * @param origin The viewport origin in the current layer + * @param scale The scale of the tilemap + */ renderTile(ctx: CanvasRenderingContext2D, tileIndex: number, dataIndex: number, worldSize: Vec2, origin: Vec2, scale: Vec2): void { + // Get the true index let index = tileIndex - this.startIndex; let row = Math.floor(index / this.numCols); let col = index % this.numCols; let width = this.tileSize.x; let height = this.tileSize.y; + + // Calculate the position to start a crop in the tileset image let left = col * width; let top = row * height; + + // Calculate the position in the world to render the tile let x = (dataIndex % worldSize.x) * width * scale.x; let y = Math.floor(dataIndex / worldSize.x) * height * scale.y; ctx.drawImage(this.image, left, top, width, height, x - origin.x, y - origin.y, width * scale.x, height * scale.y); diff --git a/src/DataTypes/Vec2.ts b/src/DataTypes/Vec2.ts index aecfc6d..d3e27c3 100644 --- a/src/DataTypes/Vec2.ts +++ b/src/DataTypes/Vec2.ts @@ -1,21 +1,51 @@ +/** + * A two-dimensional vector (x, y) + */ export default class Vec2 { - public x: number; - public y: number; + // Store x and y in an array + private vec: Float32Array; constructor(x: number = 0, y: number = 0) { - this.x = x; - this.y = y; + this.vec = new Float32Array(2); + this.vec[0] = x; + this.vec[1] = y; } + // Expose x and y with getters and setters + get x() { + return this.vec[0]; + } + + set x(x: number) { + this.vec[0] = x; + } + + get y() { + return this.vec[1]; + } + + set y(y: number) { + this.vec[1] = y; + } + + /** + * The squared magnitude of the vector + */ magSq(): number { return this.x*this.x + this.y*this.y; } + /** + * The magnitude of the vector + */ mag(): number { return Math.sqrt(this.magSq()); } + /** + * Returns this vector as a unit vector - Equivalent to dividing x and y by the magnitude + */ normalize(): Vec2 { if(this.x === 0 && this.y === 0) return this; let mag = this.mag(); @@ -24,16 +54,29 @@ export default class Vec2 { return this; } + /** + * Sets the vector's x and y based on the angle provided. Goes counter clockwise. + * @param angle The angle in radians + */ setToAngle(angle: number): Vec2 { this.x = Math.cos(angle); this.y = Math.sin(angle); return this; } + /** + * Keeps the vector's direction, but sets its magnitude to be the provided magnitude + * @param magnitude + */ scaleTo(magnitude: number): Vec2 { return this.normalize().scale(magnitude); } + /** + * Scales x and y by the number provided, or if two number are provided, scales them individually. + * @param factor + * @param yFactor + */ scale(factor: number, yFactor: number = null): Vec2 { if(yFactor !== null){ this.x *= factor; @@ -45,6 +88,10 @@ export default class Vec2 { return this; } + /** + * Rotates the vector counter-clockwise by the angle amount specified + * @param angle The angle to rotate by in radians + */ rotateCCW(angle: number): Vec2 { let cs = Math.cos(angle); let sn = Math.sin(angle); @@ -55,38 +102,65 @@ export default class Vec2 { return this; } + /** + * Sets the vectors coordinates to be the ones provided + * @param x + * @param y + */ set(x: number, y: number): Vec2 { this.x = x; this.y = y; return this; } + /** + * Adds this vector the another vector + * @param other + */ add(other: Vec2): Vec2 { this.x += other.x; this.y += other.y; return this; } + /** + * Subtracts another vector from this vector + * @param other + */ sub(other: Vec2): Vec2 { this.x -= other.x; this.y -= other.y; return this; } + /** + * Multiplies this vector with another vector element-wise + * @param other + */ mult(other: Vec2): Vec2 { this.x *= other.x; this.y *= other.y; return this; } + /** + * Returns a string representation of this vector rounded to 1 decimal point + */ toString(): string { return this.toFixed(); } + /** + * Returns a string representation of this vector rounded to the specified number of decimal points + * @param numDecimalPoints + */ toFixed(numDecimalPoints: number = 1): string { return "(" + this.x.toFixed(numDecimalPoints) + ", " + this.y.toFixed(numDecimalPoints) + ")"; } + /** + * Returns a new vector with the same coordinates as this one. + */ clone(): Vec2 { return new Vec2(this.x, this.y); } diff --git a/src/DataTypes/Vec4.ts b/src/DataTypes/Vec4.ts index fdcfc6d..6f0e150 100644 --- a/src/DataTypes/Vec4.ts +++ b/src/DataTypes/Vec4.ts @@ -2,18 +2,49 @@ import Vec2 from "./Vec2"; export default class Vec4{ - public x : number; - public y : number; - public z : number; - public w : number; + public vec: Float32Array; constructor(x : number = 0, y : number = 0, z : number = 0, w : number = 0) { - this.x = x; - this.y = y; - this.z = z; - this.w = w; + this.vec = new Float32Array(4); + this.vec[0] = x; + this.vec[1] = y; + this.vec[2] = z; + this.vec[3] = w; } + // Expose x and y with getters and setters + get x() { + return this.vec[0]; + } + + set x(x: number) { + this.vec[0] = x; + } + + get y() { + return this.vec[1]; + } + + set y(y: number) { + this.vec[1] = y; + } + + get z() { + return this.vec[2]; + } + + set z(x: number) { + this.vec[2] = x; + } + + get w() { + return this.vec[3]; + } + + set w(y: number) { + this.vec[3] = y; + } + split() : [Vec2, Vec2] { return [new Vec2(this.x, this.y), new Vec2(this.z, this.w)]; } diff --git a/src/Debug/Debug.ts b/src/Debug/Debug.ts index a36a51e..f986e38 100644 --- a/src/Debug/Debug.ts +++ b/src/Debug/Debug.ts @@ -2,6 +2,7 @@ import Map from "../DataTypes/Map"; export default class Debug { + // A map of log messages to display on the screen private static logMessages: Map = new Map(); static log(id: string, message: string): void { diff --git a/src/Events/EventQueue.ts b/src/Events/EventQueue.ts index 1fd967d..103b991 100644 --- a/src/Events/EventQueue.ts +++ b/src/Events/EventQueue.ts @@ -27,6 +27,11 @@ export default class EventQueue { this.q.enqueue(event); } + /** + * Associates a receiver with a type of event. Every time this event appears in the future, it will be given to the receiver (and any others watching that type) + * @param receiver + * @param type + */ subscribe(receiver: Receiver, type: string | Array): void { if(type instanceof Array){ // If it is an array, subscribe to all event types @@ -38,6 +43,7 @@ export default class EventQueue { } } + // Associate the receiver and the type private addListener(receiver: Receiver, type: string): void { if(this.receivers.has(type)){ this.receivers.get(type).push(receiver); @@ -48,14 +54,17 @@ export default class EventQueue { update(deltaT: number): void{ while(this.q.hasItems()){ + // Retrieve each event let event = this.q.dequeue(); - + + // If a receiver has this event type, send it the event if(this.receivers.has(event.type)){ for(let receiver of this.receivers.get(event.type)){ receiver.receive(event); } } - + + // If a receiver is subscribed to all events, send it the event if(this.receivers.has("all")){ for(let receiver of this.receivers.get("all")){ receiver.receive(event); diff --git a/src/Events/GameEvent.ts b/src/Events/GameEvent.ts index 2ee3d64..79959bd 100644 --- a/src/Events/GameEvent.ts +++ b/src/Events/GameEvent.ts @@ -1,11 +1,15 @@ import Map from "../DataTypes/Map" -export default class GameEvent{ +/** + * A representation of an in-game event + */ +export default class GameEvent { public type: string; public data: Map; public time: number; - constructor(type: string, data: Map | Record = null){ + constructor(type: string, data: Map | Record = null) { + // Parse the game event data if (data === null) { this.data = new Map(); } else if (!(data instanceof Map)){ diff --git a/src/Events/Receiver.ts b/src/Events/Receiver.ts index 5e71061..47ed8ce 100644 --- a/src/Events/Receiver.ts +++ b/src/Events/Receiver.ts @@ -1,6 +1,9 @@ import Queue from "../DataTypes/Queue"; import GameEvent from "./GameEvent"; +/** + * Receives subscribed events from the EventQueue + */ export default class Receiver{ readonly MAX_SIZE: number; private q: Queue; @@ -10,22 +13,37 @@ export default class Receiver{ this.q = new Queue(this.MAX_SIZE); } + /** + * Adds an event to the queue of this reciever + */ receive(event: GameEvent): void { this.q.enqueue(event); } + /** + * Retrieves the next event from the receiver's queue + */ getNextEvent(): GameEvent { return this.q.dequeue(); } + /** + * Looks at the next event in the receiver's queue + */ peekNextEvent(): GameEvent { return this.q.peekNext() } + /** + * Returns true if the receiver has any events in its queue + */ hasNextEvent(): boolean { return this.q.hasItems(); } + /** + * Ignore all events this frame + */ ignoreEvents(): void { this.q.clear(); } diff --git a/src/Input/InputHandler.ts b/src/Input/InputHandler.ts index 0064b81..8f5fe31 100644 --- a/src/Input/InputHandler.ts +++ b/src/Input/InputHandler.ts @@ -2,6 +2,9 @@ import EventQueue from "../Events/EventQueue"; import Vec2 from "../DataTypes/Vec2"; import GameEvent from "../Events/GameEvent"; +/** + * Handles communication with the web browser to receive asynchronous events and send them to the event queue + */ export default class InputHandler{ private eventQueue: EventQueue; diff --git a/src/Input/InputReceiver.ts b/src/Input/InputReceiver.ts index c6ab8b8..4ffac5c 100644 --- a/src/Input/InputReceiver.ts +++ b/src/Input/InputReceiver.ts @@ -4,6 +4,9 @@ import Vec2 from "../DataTypes/Vec2"; import EventQueue from "../Events/EventQueue"; import Viewport from "../SceneGraph/Viewport"; +/** + * Receives input events from the event queue and allows for easy access of information about input + */ export default class InputReceiver{ private static instance: InputReceiver = null; @@ -27,6 +30,7 @@ export default class InputReceiver{ this.mousePressPosition = new Vec2(0, 0); this.eventQueue = EventQueue.getInstance(); + // Subscribe to all input events this.eventQueue.subscribe(this.receiver, ["mouse_down", "mouse_up", "mouse_move", "key_down", "key_up", "canvas_blur"]); } @@ -44,6 +48,8 @@ export default class InputReceiver{ while(this.receiver.hasNextEvent()){ let event = this.receiver.getNextEvent(); + + // Handle each event type if(event.type === "mouse_down"){ this.mouseJustPressed = true; this.mousePressed = true; @@ -77,7 +83,7 @@ export default class InputReceiver{ } } - clearKeyPresses(): void { + private clearKeyPresses(): void { this.keyJustPressed.forEach((key: string) => this.keyJustPressed.set(key, false)); this.keyPressed.forEach((key: string) => this.keyPressed.set(key, false)); } diff --git a/src/Loop/GameLoop.ts b/src/Loop/GameLoop.ts index 2c1a6a5..28ecc22 100644 --- a/src/Loop/GameLoop.ts +++ b/src/Loop/GameLoop.ts @@ -29,11 +29,14 @@ export default class GameLoop{ private running: boolean; private frameDelta: number; + // Game canvas and its width and height readonly GAME_CANVAS: HTMLCanvasElement; readonly WIDTH: number; readonly HEIGHT: number; private viewport: Viewport; - private ctx: CanvasRenderingContext2D; + private ctx: CanvasRenderingContext2D; + + // All of the necessary subsystems that need to run here private eventQueue: EventQueue; private inputHandler: InputHandler; private inputReceiver: InputReceiver; @@ -54,15 +57,20 @@ export default class GameLoop{ this.started = false; this.running = false; + // Get the game canvas and give it a background color this.GAME_CANVAS = document.getElementById("game-canvas") as HTMLCanvasElement; this.GAME_CANVAS.style.setProperty("background-color", "whitesmoke"); + // Give the canvas a size and get the rendering context this.WIDTH = 800; this.HEIGHT = 500; this.ctx = this.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT); + + // Size the viewport to the game canvas this.viewport = new Viewport(); this.viewport.setSize(this.WIDTH, this.HEIGHT); + // Initialize all necessary game subsystems this.eventQueue = EventQueue.getInstance(); this.inputHandler = new InputHandler(this.GAME_CANVAS); this.inputReceiver = InputReceiver.getInstance(); @@ -77,10 +85,18 @@ export default class GameLoop{ canvas.width = width; canvas.height = height; let ctx = canvas.getContext("2d"); + + // For crisp pixel art ctx.imageSmoothingEnabled = false; + return ctx; } + // TODO - This currently also changes the rendering framerate + /** + * Changes the maximum allowed physics framerate of the game + * @param initMax + */ setMaxFPS(initMax: number): void { this.maxFPS = initMax; this.simulationTimestep = Math.floor(1000/this.maxFPS); @@ -90,6 +106,10 @@ export default class GameLoop{ return this.sceneManager; } + /** + * Updates the frame count and sum of time for the framerate of the game + * @param timestep + */ private updateFrameCount(timestep: number): void { this.frame += 1; this.numFramesInSum += 1; @@ -103,6 +123,9 @@ export default class GameLoop{ Debug.log("fps", "FPS: " + this.fps.toFixed(1)); } + /** + * Starts up the game loop and calls the first requestAnimationFrame + */ start(): void { if(!this.started){ this.started = true; @@ -111,6 +134,10 @@ export default class GameLoop{ } } + /** + * The first game frame - initializes the first frame time and begins the render + * @param timestamp + */ startFrame = (timestamp: number): void => { this.running = true; @@ -121,6 +148,10 @@ export default class GameLoop{ window.requestAnimationFrame(this.doFrame); } + /** + * The main loop of the game. Updates and renders every frame + * @param timestamp + */ doFrame = (timestamp: number): void => { // Request animation frame to prepare for another update or render window.requestAnimationFrame(this.doFrame); @@ -148,14 +179,30 @@ export default class GameLoop{ this.render(); } + /** + * Updates all necessary subsystems of the game. Defers scene updates to the sceneManager + * @param deltaT + */ update(deltaT: number): void { + // Handle all events that happened since the start of the last loop this.eventQueue.update(deltaT); + + // Update the input data structures so game objects can see the input this.inputReceiver.update(deltaT); + + // Update the recording of the game this.recorder.update(deltaT); + + // Update all scenes this.sceneManager.update(deltaT); + + // Load or unload any resources if needed this.resourceManager.update(deltaT); } + /** + * Clears the canvas and defers scene rendering to the sceneManager. Renders the debug + */ render(): void { this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT); this.sceneManager.render(this.ctx); diff --git a/src/Nodes/CanvasNode.ts b/src/Nodes/CanvasNode.ts index da52122..4905d13 100644 --- a/src/Nodes/CanvasNode.ts +++ b/src/Nodes/CanvasNode.ts @@ -1,7 +1,9 @@ import GameNode from "./GameNode"; import Vec2 from "../DataTypes/Vec2"; -import Layer from "../Scene/Layer"; +/** + * The representation of an object in the game world that can be drawn to the screen + */ export default abstract class CanvasNode extends GameNode{ protected size: Vec2; @@ -22,6 +24,11 @@ export default abstract class CanvasNode extends GameNode{ } } + /** + * Returns true if the point (x, y) is inside of this canvas object + * @param x + * @param y + */ contains(x: number, y: number): boolean { if(this.position.x < x && this.position.x + this.size.x > x){ if(this.position.y < y && this.position.y + this.size.y > y){ diff --git a/src/Nodes/GameNode.ts b/src/Nodes/GameNode.ts index 1e32abe..c062406 100644 --- a/src/Nodes/GameNode.ts +++ b/src/Nodes/GameNode.ts @@ -7,6 +7,9 @@ import GameEvent from "../Events/GameEvent"; import Scene from "../Scene/Scene"; import Layer from "../Scene/Layer"; +/** + * The representation of an object in the game world + */ export default abstract class GameNode{ private eventQueue: EventQueue; protected input: InputReceiver; @@ -49,17 +52,26 @@ export default abstract class GameNode{ } } + /** + * Subscribe this object's receiver to the specified event type + * @param eventType + */ subscribe(eventType: string): void { this.eventQueue.subscribe(this.receiver, eventType); } + /** + * Emit and event of type eventType with the data packet data + * @param eventType + * @param data + */ emit(eventType: string, data: Map | Record = null): void { let event = new GameEvent(eventType, data); this.eventQueue.addEvent(event); } // TODO - This doesn't seem ideal. Is there a better way to do this? - getViewportOriginWithParallax(){ + protected getViewportOriginWithParallax(): Vec2 { return this.scene.getViewport().getPosition().clone().mult(this.layer.getParallax()); } diff --git a/src/Nodes/Graphic.ts b/src/Nodes/Graphic.ts index 40f1089..5182ef1 100644 --- a/src/Nodes/Graphic.ts +++ b/src/Nodes/Graphic.ts @@ -1,9 +1,12 @@ import CanvasNode from "./CanvasNode"; import Color from "../Utils/Color"; +/** + * The representation of a game object that doesn't rely on any resources to render - it is drawn to the screen by the canvas + */ export default abstract class Graphic extends CanvasNode { - color: Color; + protected color: Color; setColor(color: Color){ this.color = color; diff --git a/src/Nodes/Sprites/Sprite.ts b/src/Nodes/Sprites/Sprite.ts index 02cc481..44b9f92 100644 --- a/src/Nodes/Sprites/Sprite.ts +++ b/src/Nodes/Sprites/Sprite.ts @@ -2,6 +2,9 @@ import CanvasNode from "../CanvasNode"; import ResourceManager from "../../ResourceManager/ResourceManager"; import Vec2 from "../../DataTypes/Vec2"; +/** + * The representation of a sprite - an in-game image + */ export default class Sprite extends CanvasNode { private imageId: string; private scale: Vec2; @@ -14,10 +17,17 @@ export default class Sprite extends CanvasNode { this.scale = new Vec2(1, 1); } + /** + * Returns the scale of the sprite + */ getScale(): Vec2 { return this.scale; } + /** + * Sets the scale of the sprite to the value provided + * @param scale + */ setScale(scale: Vec2): void { this.scale = scale; } diff --git a/src/Nodes/Tilemap.ts b/src/Nodes/Tilemap.ts index fa10074..6df50d1 100644 --- a/src/Nodes/Tilemap.ts +++ b/src/Nodes/Tilemap.ts @@ -4,9 +4,10 @@ import Tileset from "../DataTypes/Tilesets/Tileset"; import { TiledTilemapData, TiledLayerData } from "../DataTypes/Tilesets/TiledData" /** - * Represents one layer of tiles + * The representation of a tilemap - this can consist of a combination of tilesets in one layer */ export default abstract class Tilemap extends GameNode { + // A tileset represents the tiles within one specific image loaded from a file protected tilesets: Array; protected worldSize: Vec2; protected tileSize: Vec2; @@ -21,6 +22,8 @@ export default abstract class Tilemap extends GameNode { this.tilesets = new Array(); this.worldSize = new Vec2(0, 0); this.tileSize = new Vec2(0, 0); + + // Defer parsing of the data to child classes - this allows for isometric vs. orthographic tilemaps and handling of Tiled data or other data this.parseTilemapData(tilemapData, layer); this.scale = new Vec2(4, 4); } diff --git a/src/Nodes/Tilemaps/OrthogonalTilemap.ts b/src/Nodes/Tilemaps/OrthogonalTilemap.ts index c2dfdb2..44e07ba 100644 --- a/src/Nodes/Tilemaps/OrthogonalTilemap.ts +++ b/src/Nodes/Tilemaps/OrthogonalTilemap.ts @@ -3,9 +3,16 @@ import Vec2 from "../../DataTypes/Vec2"; import { TiledTilemapData, TiledLayerData } from "../../DataTypes/Tilesets/TiledData"; import Tileset from "../../DataTypes/Tilesets/Tileset"; - +/** + * The representation of an orthogonal tilemap - i.e. a top down or platformer tilemap + */ export default class OrthogonalTilemap extends Tilemap { + /** + * Parses the tilemap data loaded from the json file. DOES NOT process images automatically - the ResourceManager class does this while loading tilemaps + * @param tilemapData + * @param layer + */ protected parseTilemapData(tilemapData: TiledTilemapData, layer: TiledLayerData): void { this.worldSize.set(tilemapData.width, tilemapData.height); this.tileSize.set(tilemapData.tilewidth, tilemapData.tileheight); @@ -23,6 +30,10 @@ export default class OrthogonalTilemap extends Tilemap { tilemapData.tilesets.forEach(tilesetData => this.tilesets.push(new Tileset(tilesetData))); } + /** + * Get the value of the tile at the coordinates in the vector worldCoords + * @param worldCoords + */ getTileAt(worldCoords: Vec2): number { let localCoords = this.getColRowAt(worldCoords); if(localCoords.x < 0 || localCoords.x >= this.worldSize.x || localCoords.y < 0 || localCoords.y >= this.worldSize.y){ @@ -33,6 +44,11 @@ export default class OrthogonalTilemap extends Tilemap { return this.data[localCoords.y * this.worldSize.x + localCoords.x] } + /** + * Returns true if the tile at the specified row and column of the tilemap is collidable + * @param indexOrCol + * @param row + */ isTileCollidable(indexOrCol: number, row?: number): boolean { let index = 0; if(row){ @@ -53,6 +69,10 @@ export default class OrthogonalTilemap extends Tilemap { return this.data[index] !== 0 && this.collidable; } + /** + * Takes in world coordinates and returns the row and column of the tile at that position + * @param worldCoords + */ // TODO: Should this throw an error if someone tries to access an out of bounds value? getColRowAt(worldCoords: Vec2): Vec2 { let col = Math.floor(worldCoords.x / this.tileSize.x / this.scale.x); diff --git a/src/Nodes/UIElement.ts b/src/Nodes/UIElement.ts index d8f59b4..d46eefb 100644 --- a/src/Nodes/UIElement.ts +++ b/src/Nodes/UIElement.ts @@ -2,6 +2,9 @@ import CanvasNode from "./CanvasNode"; import Color from "../Utils/Color"; import Vec2 from "../DataTypes/Vec2"; +/** + * The representation of a UIElement - the parent class of things like buttons + */ export default class UIElement extends CanvasNode{ // Style attributes protected textColor: Color; @@ -68,6 +71,7 @@ export default class UIElement extends CanvasNode{ } update(deltaT: number): void { + // See of this object was just clicked if(this.input.isMouseJustPressed()){ let clickPos = this.input.getMousePressPosition(); if(this.contains(clickPos.x, clickPos.y)){ @@ -83,12 +87,14 @@ export default class UIElement extends CanvasNode{ } } + // If the mouse wasn't just pressed, then we definitely weren't clicked if(!this.input.isMousePressed()){ if(this.isClicked){ this.isClicked = false; } } + // Check if the mouse is hovering over this element let mousePos = this.input.getMousePosition(); if(mousePos && this.contains(mousePos.x, mousePos.y)){ this.isEntered = true; @@ -117,6 +123,10 @@ export default class UIElement extends CanvasNode{ } } + /** + * Calculate the offset of the text - this is useful for rendering text with different alignments + * + */ protected calculateOffset(ctx: CanvasRenderingContext2D): Vec2 { let textWidth = ctx.measureText(this.text).width; @@ -143,19 +153,29 @@ export default class UIElement extends CanvasNode{ return offset; } + /** + * Overridable method for calculating background color - useful for elements that want to be colored on different after certain events + */ protected calculateBackgroundColor(): string { return this.backgroundColor.toStringRGBA(); } + /** + * Overridable method for calculating border color - useful for elements that want to be colored on different after certain events + */ protected calculateBorderColor(): string { return this.borderColor.toStringRGBA(); } + /** + * Overridable method for calculating text color - useful for elements that want to be colored on different after certain events + */ protected calculateTextColor(): string { return this.textColor.toStringRGBA(); } render(ctx: CanvasRenderingContext2D): void { + // Grab the global alpha so we can adjust it for this render let previousAlpha = ctx.globalAlpha; ctx.globalAlpha = this.getLayer().getAlpha(); @@ -164,6 +184,7 @@ export default class UIElement extends CanvasNode{ ctx.font = this.fontSize + "px " + this.font; let offset = this.calculateOffset(ctx); + // Stroke and fill a rounded rect and give it text ctx.fillStyle = this.calculateBackgroundColor(); ctx.fillRoundedRect(this.position.x - origin.x, this.position.y - origin.y, this.size.x, this.size.y, this.borderRadius); diff --git a/src/Nodes/UIElements/Button.ts b/src/Nodes/UIElements/Button.ts index 8cfa985..e56b0d5 100644 --- a/src/Nodes/UIElements/Button.ts +++ b/src/Nodes/UIElements/Button.ts @@ -12,6 +12,7 @@ export default class Button extends UIElement{ } protected calculateBackgroundColor(): string { + // Change the background color if clicked or hovered if(this.isEntered && !this.isClicked){ return this.backgroundColor.lighten().toStringRGBA(); } else if(this.isClicked){ diff --git a/src/Physics/PhysicsManager.ts b/src/Physics/PhysicsManager.ts index 0b932fc..68a8ba0 100644 --- a/src/Physics/PhysicsManager.ts +++ b/src/Physics/PhysicsManager.ts @@ -18,51 +18,85 @@ export default class PhysicsManager { this.movements = new Array(); } + /** + * Adds a PhysicsNode to the manager to be handled in case of collisions + * @param node + */ add(node: PhysicsNode): void { this.physicsNodes.push(node); } + /** + * Adds a tilemap node to the manager to be handled for collisions + * @param tilemap + */ addTilemap(tilemap: Tilemap): void { this.tilemaps.push(tilemap); } + /** + * Adds a movement to this frame. All movements are handled at the end of the frame + * @param node + * @param velocity + */ addMovement(node: PhysicsNode, velocity: Vec2): void { this.movements.push(new MovementData(node, velocity)); } + /** + * Handles a collision between a physics node and a tilemap + * @param node + * @param tilemap + * @param velocity + */ private collideWithTilemap(node: PhysicsNode, tilemap: Tilemap, velocity: Vec2): void { if(tilemap instanceof OrthogonalTilemap){ this.collideWithOrthogonalTilemap(node, tilemap, velocity); } } + /** + * Specifically handles a collision for orthogonal tilemaps + * @param node + * @param tilemap + * @param velocity + */ private collideWithOrthogonalTilemap(node: PhysicsNode, tilemap: OrthogonalTilemap, velocity: Vec2): void { + // Get the starting position of the moving node let startPos = node.getPosition(); + + // Get the end position of the moving node let endPos = startPos.clone().add(velocity); let size = node.getCollider().getSize(); + + // Get the min and max x and y coordinates of the moving node let min = new Vec2(Math.min(startPos.x, endPos.x), Math.min(startPos.y, endPos.y)); let max = new Vec2(Math.max(startPos.x + size.x, endPos.x + size.x), Math.max(startPos.y + size.y, endPos.y + size.y)); + // Convert the min/max x/y to the min and max row/col in the tilemap array let minIndex = tilemap.getColRowAt(min); let maxIndex = tilemap.getColRowAt(max); + // Create an empty set of tilemap collisions (We'll handle all of them at the end) let tilemapCollisions = new Array(); let tileSize = tilemap.getTileSize(); Debug.log("tilemapCollision", ""); + // Loop over all possible tiles for(let col = minIndex.x; col <= maxIndex.x; col++){ for(let row = minIndex.y; row <= maxIndex.y; row++){ if(tilemap.isTileCollidable(col, row)){ Debug.log("tilemapCollision", "Colliding with Tile"); - // Tile position + // Get the position of this tile let tilePos = new Vec2(col * tileSize.x, row * tileSize.y); - // Calculate collision area + // Calculate collision area between the node and the tile let dx = Math.min(startPos.x, tilePos.x) - Math.max(startPos.x + size.x, tilePos.x + size.x); let dy = Math.min(startPos.y, tilePos.y) - Math.max(startPos.y + size.y, tilePos.y + size.y); + // If we overlap, how much do we overlap by? let overlap = 0; if(dx * dy > 0){ overlap = dx * dy; @@ -73,32 +107,35 @@ export default class PhysicsManager { } } - // Now that we have all collisions, sort by collision area + // Now that we have all collisions, sort by collision area highest to lowest tilemapCollisions = tilemapCollisions.sort((a, b) => a.overlapArea - b.overlapArea); - // Resolve the collisions + // Resolve the collisions in order of collision area (i.e. "closest" tiles are collided with first, so we can slide along a surface of tiles) tilemapCollisions.forEach(collision => { let [firstContact, _, collidingX, collidingY] = this.getTimeOfAABBCollision(startPos, size, velocity, collision.position, tileSize, new Vec2(0, 0)); // Handle collision if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ if(collidingX && collidingY){ - // If we're already intersecting, freak out I guess? + // If we're already intersecting, freak out I guess? Probably should handle this in some way for if nodes get spawned inside of tiles } else { - // let contactTime = Math.min(firstContact.x, firstContact.y); - // velocity.scale(contactTime); + // Get the amount to scale x and y based on their initial collision times let xScale = MathUtils.clamp(firstContact.x, 0, 1); let yScale = MathUtils.clamp(firstContact.y, 0, 1); - // Handle special case of stickiness on corner to corner collisions + // Handle special case of stickiness on perfect corner to corner collisions if(xScale === yScale){ xScale = 1; } + // If we are scaling y, we're on the ground, so tell the node it's grounded + // TODO - This is a bug, check to make sure our velocity is going downwards + // Maybe feed in a downward direction to check to be sure if(yScale !== 1){ node.setGrounded(true); } + // Scale the velocity of the node velocity.scale(xScale, yScale); } } @@ -264,6 +301,7 @@ export default class PhysicsManager { // Helper classes for internal data // TODO: Move these to data +// When an object moves, store it's data as MovementData so all movements can be processed at the same time at the end of the frame class MovementData { node: PhysicsNode; velocity: Vec2; @@ -273,6 +311,7 @@ class MovementData { } } +// Collision data objects for tilemaps class TileCollisionData { position: Vec2; overlapArea: number; diff --git a/src/Physics/PhysicsNode.ts b/src/Physics/PhysicsNode.ts index f7c78ac..748c2df 100644 --- a/src/Physics/PhysicsNode.ts +++ b/src/Physics/PhysicsNode.ts @@ -3,6 +3,10 @@ import GameNode from "../Nodes/GameNode"; import PhysicsManager from "./PhysicsManager"; import Vec2 from "../DataTypes/Vec2"; +/** + * The representation of a physic-affected object in the game world. Sprites and other game nodes can be associated with + * a physics node to move them around as well. + */ export default abstract class PhysicsNode extends GameNode { protected collider: Collider = null; @@ -42,11 +46,19 @@ export default abstract class PhysicsNode extends GameNode { return this.moving; } + /** + * Register a movement to the physics manager that can be handled at the end of the frame + * @param velocity + */ protected move(velocity: Vec2): void { this.moving = true; this.manager.addMovement(this, velocity); } + /** + * Called by the physics manager to finish the movement and actually move the physics object and its children + * @param velocity + */ finishMove(velocity: Vec2): void { this.position.add(velocity); this.collider.getPosition().add(velocity); diff --git a/src/ResourceManager/ResourceManager.ts b/src/ResourceManager/ResourceManager.ts index 52668e4..530ce2d 100644 --- a/src/ResourceManager/ResourceManager.ts +++ b/src/ResourceManager/ResourceManager.ts @@ -6,30 +6,72 @@ import StringUtils from "../Utils/StringUtils"; import AudioManager from "../Sound/AudioManager"; export default class ResourceManager { + // Instance for the singleton class private static instance: ResourceManager; + // Booleans to keep track of whether or not the ResourceManager is currently loading something private loading: boolean; private justLoaded: boolean; + // Functions to do something when loading progresses or is completed such as render a loading screen public onLoadProgress: Function; public onLoadComplete: Function; + + /** + * Number to keep track of how many images need to be loaded + */ private imagesLoaded: number; + /** + * Number to keep track of how many images are loaded + */ private imagesToLoad: number; + /** + * The queue of images we must load + */ private imageLoadingQueue: Queue<{key: string, path: string}>; + /** + * A map of the images that are currently loaded and (presumably) being used by the scene + */ private images: Map; + /** + * Number to keep track of how many tilemaps need to be loaded + */ private tilemapsLoaded: number; + /** + * Number to keep track of how many tilemaps are loaded + */ private tilemapsToLoad: number; + /** + * The queue of tilemaps we must load + */ private tilemapLoadingQueue: Queue<{key: string, path: string}>; + /** + * A map of the tilemaps that are currently loaded and (presumably) being used by the scene + */ private tilemaps: Map; + /** + * Number to keep track of how many sounds need to be loaded + */ private audioLoaded: number; + /** + * Number to keep track of how many sounds are loaded + */ private audioToLoad: number; + /** + * The queue of sounds we must load + */ private audioLoadingQueue: Queue<{key: string, path: string}>; + /** + * A map of the sounds that are currently loaded and (presumably) being used by the scene + */ private audioBuffers: Map; - // The number of different types of things to load + /** + * The total number of "types" of things that need to be loaded (i.e. images and tilemaps) + */ private typesToLoad: number; private constructor(){ @@ -52,6 +94,9 @@ export default class ResourceManager { this.audioBuffers = new Map(); }; + /** + * Returns the current instance of this class or a new instance if none exist + */ static getInstance(): ResourceManager { if(!this.instance){ this.instance = new ResourceManager(); @@ -60,11 +105,20 @@ export default class ResourceManager { return this.instance; } + /** + * Loads an image from file + * @param key The key to associate the loaded image with + * @param path The path to the image to load + */ public image(key: string, path: string): void { this.imageLoadingQueue.enqueue({key: key, path: path}); } - public getImage(key: string): HTMLImageElement{ + /** + * Retrieves a loaded image + * @param key The key of the loaded image + */ + public getImage(key: string): HTMLImageElement { return this.images.get(key); } @@ -72,23 +126,45 @@ export default class ResourceManager { } + /** + * Load an audio file + * @param key + * @param path + */ public audio(key: string, path: string): void { this.audioLoadingQueue.enqueue({key: key, path: path}); } + /** + * Retrieves a loaded audio file + * @param key + */ public getAudio(key: string): AudioBuffer { return this.audioBuffers.get(key); } + /** + * Load a tilemap from a json file. Automatically loads related images + * @param key + * @param path + */ public tilemap(key: string, path: string): void { this.tilemapLoadingQueue.enqueue({key: key, path: path}); } + /** + * Retreives a loaded tilemap + * @param key + */ public getTilemap(key: string): TiledTilemapData { return this.tilemaps.get(key); } // TODO - Should everything be loaded in order, one file at a time? + /** + * Loads all resources currently in the queue + * @param callback + */ loadResourcesFromQueue(callback: Function): void { this.typesToLoad = 3; @@ -108,6 +184,9 @@ export default class ResourceManager { } + /** + * Deletes references to all resources in the resource manager + */ unloadAllResources(): void { this.loading = false; this.justLoaded = false; @@ -125,7 +204,11 @@ export default class ResourceManager { this.audioBuffers.clear(); } - private loadTilemapsFromQueue(onFinishLoading: Function){ + /** + * Loads all tilemaps currently in the tilemap loading queue + * @param onFinishLoading + */ + private loadTilemapsFromQueue(onFinishLoading: Function): void { this.tilemapsToLoad = this.tilemapLoadingQueue.getSize(); this.tilemapsLoaded = 0; @@ -135,6 +218,12 @@ export default class ResourceManager { } } + /** + * Loads a singular tilemap + * @param key + * @param pathToTilemapJSON + * @param callbackIfLast + */ private loadTilemap(key: string, pathToTilemapJSON: string, callbackIfLast: Function): void { this.loadTextFile(pathToTilemapJSON, (fileText: string) => { let tilemapObject = JSON.parse(fileText); @@ -154,7 +243,11 @@ export default class ResourceManager { }); } - private finishLoadingTilemap(callback: Function){ + /** + * Finish loading a tilemap. Calls the callback function if this is the last tilemap being loaded + * @param callback + */ + private finishLoadingTilemap(callback: Function): void { this.tilemapsLoaded += 1; if(this.tilemapsLoaded === this.tilemapsToLoad){ @@ -163,6 +256,10 @@ export default class ResourceManager { } } + /** + * Loads all images currently in the tilemap loading queue + * @param onFinishLoading + */ private loadImagesFromQueue(onFinishLoading: Function): void { this.imagesToLoad = this.imageLoadingQueue.getSize(); this.imagesLoaded = 0; @@ -173,7 +270,12 @@ export default class ResourceManager { } } - // TODO: When you switch to WebGL, make sure to make this private and make a "loadTexture" function + /** + * Loads a singular image + * @param key + * @param path + * @param callbackIfLast + */ public loadImage(key: string, path: string, callbackIfLast: Function): void { var image = new Image(); @@ -188,6 +290,10 @@ export default class ResourceManager { image.src = path; } + /** + * Finish loading an image. If this is the last image, it calls the callback function + * @param callback + */ private finishLoadingImage(callback: Function): void { this.imagesLoaded += 1; @@ -197,6 +303,10 @@ export default class ResourceManager { } } + /** + * Loads all audio currently in the tilemap loading queue + * @param onFinishLoading + */ private loadAudioFromQueue(onFinishLoading: Function){ this.audioToLoad = this.audioLoadingQueue.getSize(); this.audioLoaded = 0; @@ -207,6 +317,12 @@ export default class ResourceManager { } } + /** + * Load a singular audio file + * @param key + * @param path + * @param callbackIfLast + */ private loadAudio(key: string, path: string, callbackIfLast: Function): void { let audioCtx = AudioManager.getInstance().getAudioContext(); @@ -228,6 +344,10 @@ export default class ResourceManager { request.send(); } + /** + * Finish loading an audio file. Calls the callback functon if this is the last audio sample being loaded. + * @param callback + */ private finishLoadingAudio(callback: Function): void { this.audioLoaded += 1; diff --git a/src/Scene/Factories/AudioFactory.ts b/src/Scene/Factories/AudioFactory.ts index a14226b..ff39e9b 100644 --- a/src/Scene/Factories/AudioFactory.ts +++ b/src/Scene/Factories/AudioFactory.ts @@ -14,7 +14,11 @@ export default class AudioFactory { this.audioManager = AudioManager.getInstance(); } - addAudio = (key: string, ...args: any): Audio => { + /** + * Returns an audio element created using the previously loaded audio file specified by the key. + * @param key The key of the loaded audio file + */ + addAudio = (key: string): Audio => { let audio = new Audio(key); return audio; } diff --git a/src/Scene/Factories/CanvasNodeFactory.ts b/src/Scene/Factories/CanvasNodeFactory.ts index bc5c8db..649c3f3 100644 --- a/src/Scene/Factories/CanvasNodeFactory.ts +++ b/src/Scene/Factories/CanvasNodeFactory.ts @@ -14,6 +14,12 @@ export default class CanvasNodeFactory { this.sceneGraph = sceneGraph; } + /** + * Adds an instance of a UIElement to the current scene - i.e. any class that extends UIElement + * @param constr The constructor of the UIElement to be created + * @param layer The layer to add the UIElement to + * @param args Any additional arguments to feed to the constructor + */ addUIElement = (constr: new (...a: any) => T, layer: Layer, ...args: any): T => { let instance = new constr(...args); @@ -27,8 +33,13 @@ export default class CanvasNodeFactory { return instance; } - addSprite = (imageId: string, layer: Layer, ...args: any): Sprite => { - let instance = new Sprite(imageId); + /** + * Adds a sprite to the current scene + * @param key The key of the image the sprite will represent + * @param layer The layer on which to add the sprite + */ + addSprite = (key: string, layer: Layer): Sprite => { + let instance = new Sprite(key); // Add instance to scene instance.setScene(this.scene); @@ -40,6 +51,12 @@ export default class CanvasNodeFactory { return instance; } + /** + * Adds a new graphic element to the current Scene + * @param constr The constructor of the graphic element to add + * @param layer The layer on which to add the graphic + * @param args Any additional arguments to send to the graphic constructor + */ addGraphic = (constr: new (...a: any) => T, layer: Layer, ...args: any): T => { let instance = new constr(...args); diff --git a/src/Scene/Factories/FactoryManager.ts b/src/Scene/Factories/FactoryManager.ts index 39dea85..7aa2228 100644 --- a/src/Scene/Factories/FactoryManager.ts +++ b/src/Scene/Factories/FactoryManager.ts @@ -9,6 +9,7 @@ import Tilemap from "../../Nodes/Tilemap"; export default class FactoryManager { + // Constructors are called here to allow assignment of their functions to functions in this class private canvasNodeFactory: CanvasNodeFactory = new CanvasNodeFactory(); private physicsNodeFactory: PhysicsNodeFactory = new PhysicsNodeFactory(); private tilemapFactory: TilemapFactory = new TilemapFactory(); @@ -21,6 +22,7 @@ export default class FactoryManager { this.audioFactory.init(scene); } + // Expose all of the factories through the factory manager uiElement = this.canvasNodeFactory.addUIElement; sprite = this.canvasNodeFactory.addSprite; graphic = this.canvasNodeFactory.addGraphic; diff --git a/src/Scene/Factories/PhysicsNodeFactory.ts b/src/Scene/Factories/PhysicsNodeFactory.ts index 8ac9826..e15b7e2 100644 --- a/src/Scene/Factories/PhysicsNodeFactory.ts +++ b/src/Scene/Factories/PhysicsNodeFactory.ts @@ -13,6 +13,12 @@ export default class PhysicsNodeFactory { } // TODO: Currently this doesn't care about layers + /** + * Adds a new PhysicsNode to the scene on the specified Layer + * @param constr The constructor of the PhysicsNode to be added to the scene + * @param layer The layer on which to add the PhysicsNode + * @param args Any additional arguments to send to the PhysicsNode constructor + */ add = (constr: new (...a: any) => T, layer: Layer, ...args: any): T => { let instance = new constr(...args); instance.setScene(this.scene); diff --git a/src/Scene/Factories/TilemapFactory.ts b/src/Scene/Factories/TilemapFactory.ts index 6392755..def6df2 100644 --- a/src/Scene/Factories/TilemapFactory.ts +++ b/src/Scene/Factories/TilemapFactory.ts @@ -16,6 +16,12 @@ export default class TilemapFactory { this.resourceManager = ResourceManager.getInstance(); } + /** + * Adds a tilemap to the scene + * @param key The key of the loaded tilemap to load + * @param constr The constructor of the desired tilemap + * @param args Additional arguments to send to the tilemap constructor + */ add = (key: string, constr: new (...a: any) => T, ...args: any): Array => { // Get Tilemap Data let tilemapData = this.resourceManager.getTilemap(key); diff --git a/src/Scene/Layer.ts b/src/Scene/Layer.ts index 9ef9b4f..ddb8bcf 100644 --- a/src/Scene/Layer.ts +++ b/src/Scene/Layer.ts @@ -3,6 +3,9 @@ import Scene from "./Scene"; import MathUtils from "../Utils/MathUtils"; import GameNode from "../Nodes/GameNode"; +/** + * A layer in the scene. Has its own alpha value and parallax. + */ export default class Layer { protected scene: Scene; protected parallax: Vec2; @@ -66,6 +69,4 @@ export default class Layer { this.items.push(node); node.setLayer(this); } - - render(ctx: CanvasRenderingContext2D): void {} } \ No newline at end of file diff --git a/src/Scene/Layers/ObjectLayer.ts b/src/Scene/Layers/ObjectLayer.ts deleted file mode 100644 index 2185a80..0000000 --- a/src/Scene/Layers/ObjectLayer.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Layer from "../Layer"; - -export default class ObjectLayer extends Layer {} \ No newline at end of file diff --git a/src/Scene/Layers/TiledLayer.ts b/src/Scene/Layers/TiledLayer.ts deleted file mode 100644 index f536854..0000000 --- a/src/Scene/Layers/TiledLayer.ts +++ /dev/null @@ -1,6 +0,0 @@ -import Layer from "../Layer"; -import Tilemap from "../../Nodes/Tilemap"; - -export default class TiledLayer extends Layer { - private tilemap: Tilemap; -} \ No newline at end of file diff --git a/src/Scene/Layers/UILayer.ts b/src/Scene/Layers/UILayer.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/Scene/Scene.ts b/src/Scene/Scene.ts index e3737f4..fcfca28 100644 --- a/src/Scene/Scene.ts +++ b/src/Scene/Scene.ts @@ -20,10 +20,21 @@ export default class Scene{ protected sceneManager: SceneManager; protected tilemaps: Array; + + /** + * The scene graph of the Scene - can be exchanged with other SceneGraphs for more variation + */ protected sceneGraph: SceneGraph; protected physicsManager: PhysicsManager; + /** + * An interface that allows the adding of different nodes to the scene + */ public add: FactoryManager; + + /** + * An interface that allows the loading of different files for use in the scene + */ public load: ResourceManager; constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop){ @@ -39,19 +50,38 @@ export default class Scene{ this.sceneGraph = new SceneGraphArray(this.viewport, this); this.physicsManager = new PhysicsManager(); - // Factories for this scene + this.add = new FactoryManager(this, this.sceneGraph, this.physicsManager, this.tilemaps); + + this.load = ResourceManager.getInstance(); } + /** + * A function that gets called when a new scene is created. Load all files you wish to access in the scene here. + */ loadScene(): void {} + /** + * A function that gets called on scene destruction. Specify which files you no longer need for garbage collection. + */ unloadScene(): void {} + /** + * Called strictly after loadScene() is called. Create any game objects you wish to use in the scene here. + */ startScene(): void {} + /** + * Called every frame of the game. This is where you can dynamically do things like add in new enemies + * @param delta + */ updateScene(delta: number): void {} + /** + * Updates all scene elements + * @param deltaT + */ update(deltaT: number): void { this.updateScene(deltaT); @@ -72,6 +102,10 @@ export default class Scene{ this.viewport.update(deltaT); } + /** + * Render all CanvasNodes and Tilemaps in the Scene + * @param ctx + */ render(ctx: CanvasRenderingContext2D): void { // For webGL, pass a visible set to the renderer // We need to keep track of the order of things. @@ -94,12 +128,18 @@ export default class Scene{ return this.running; } + /** + * Adds a new layer to the scene and returns it + */ addLayer(): Layer { let layer = new Layer(this); this.layers.push(layer); return layer; } + /** + * Returns the viewport associated with this scene + */ getViewport(): Viewport { return this.viewport; } diff --git a/src/Scene/SceneManager.ts b/src/Scene/SceneManager.ts index 556f6c2..aa3847b 100644 --- a/src/Scene/SceneManager.ts +++ b/src/Scene/SceneManager.ts @@ -3,7 +3,7 @@ import ResourceManager from "../ResourceManager/ResourceManager"; import Viewport from "../SceneGraph/Viewport"; import GameLoop from "../Loop/GameLoop"; -export default class SceneManager{ +export default class SceneManager { private currentScene: Scene; private viewport: Viewport; @@ -16,6 +16,10 @@ export default class SceneManager{ this.game = game; } + /** + * Add a scene as the main scene + * @param constr The constructor of the scene to add + */ public addScene(constr: new (...args: any) => T): void { let scene = new constr(this.viewport, this, this.game); this.currentScene = scene; @@ -30,6 +34,10 @@ export default class SceneManager{ }); } + /** + * Change from the current scene to this new scene + * @param constr The constructor of the scene to change to + */ public changeScene(constr: new (...args: any) => T): void { // unload current scene this.currentScene.unloadScene(); diff --git a/src/SceneGraph/SceneGraph.ts b/src/SceneGraph/SceneGraph.ts index dcc84bd..06a8238 100644 --- a/src/SceneGraph/SceneGraph.ts +++ b/src/SceneGraph/SceneGraph.ts @@ -4,7 +4,10 @@ import Map from "../DataTypes/Map"; import Vec2 from "../DataTypes/Vec2"; import Scene from "../Scene/Scene"; -export default abstract class SceneGraph{ +/** + * An abstract interface of a SceneGraph. Exposes methods for use by other code, but leaves the implementation up to the subclasses. + */ +export default abstract class SceneGraph { protected viewport: Viewport; protected nodeMap: Map; protected idCounter: number; @@ -17,6 +20,10 @@ export default abstract class SceneGraph{ this.idCounter = 0; } + /** + * Add a node to the SceneGraph + * @param node The CanvasNode to add to the SceneGraph + */ addNode(node: CanvasNode): number { this.nodeMap.add(this.idCounter.toString(), node); this.addNodeSpecific(node, this.idCounter.toString()); @@ -24,8 +31,17 @@ export default abstract class SceneGraph{ return this.idCounter - 1; }; + /** + * An overridable method to add a CanvasNode to the specific data structure of the SceneGraph + * @param node The node to add to the data structure + * @param id The id of the CanvasNode + */ protected abstract addNodeSpecific(node: CanvasNode, id: string): void; + /** + * Removes a node from the SceneGraph + * @param node The node to remove + */ removeNode(node: CanvasNode): void { // Find and remove node in O(n) // TODO: Can this be better? @@ -36,12 +52,26 @@ export default abstract class SceneGraph{ } }; + /** + * The specific implementation of removing a node + * @param node The node to remove + * @param id The id of the node to remove + */ protected abstract removeNodeSpecific(node: CanvasNode, id: string): void; - getNode(id: string): CanvasNode{ + /** + * Get a specific node using its id + * @param id The id of the CanvasNode to retrieve + */ + getNode(id: string): CanvasNode { return this.nodeMap.get(id); - }; + } + /** + * Returns the node at specific coordinates + * @param vecOrX + * @param y + */ getNodeAt(vecOrX: Vec2 | number, y: number = null): CanvasNode { if(vecOrX instanceof Vec2){ return this.getNodeAtCoords(vecOrX.x, vecOrX.y); @@ -49,10 +79,18 @@ export default abstract class SceneGraph{ return this.getNodeAtCoords(vecOrX, y); } } - + + /** + * The specific implementation of getting a node at certain coordinates + * @param x + * @param y + */ protected abstract getNodeAtCoords(x: number, y: number): CanvasNode; - + abstract update(deltaT: number): void; + /** + * Gets the visible set of CanvasNodes based on the viewport + */ abstract getVisibleSet(): Array; } \ No newline at end of file diff --git a/src/SceneGraph/Viewport.ts b/src/SceneGraph/Viewport.ts index e404a09..4a12fc5 100644 --- a/src/SceneGraph/Viewport.ts +++ b/src/SceneGraph/Viewport.ts @@ -4,7 +4,7 @@ import GameNode from "../Nodes/GameNode"; import CanvasNode from "../Nodes/CanvasNode"; import MathUtils from "../Utils/MathUtils"; -export default class Viewport{ +export default class Viewport { private position: Vec2; private size: Vec2; private bounds: Vec4; @@ -16,10 +16,18 @@ export default class Viewport{ this.bounds = new Vec4(0, 0, 0, 0); } + /** + * Returns the position of the viewport as a Vec2 + */ getPosition(): Vec2 { return this.position; } + /** + * Set the position of the viewport + * @param vecOrX + * @param y + */ setPosition(vecOrX: Vec2 | number, y: number = null): void { if(vecOrX instanceof Vec2){ this.position.set(vecOrX.x, vecOrX.y); @@ -28,10 +36,18 @@ export default class Viewport{ } } + /** + * Returns the size of the viewport as a Vec2 + */ getSize(): Vec2{ return this.size; } + /** + * Sets the size of the viewport + * @param vecOrX + * @param y + */ setSize(vecOrX: Vec2 | number, y: number = null): void { if(vecOrX instanceof Vec2){ this.size.set(vecOrX.x, vecOrX.y); @@ -40,6 +56,10 @@ export default class Viewport{ } } + /** + * Returns true if the CanvasNode is inside of the viewport + * @param node + */ includes(node: CanvasNode): boolean { let nodePos = node.getPosition(); let nodeSize = node.getSize(); @@ -56,17 +76,30 @@ export default class Viewport{ } // TODO: Put some error handling on this for trying to make the bounds too small for the viewport - // TODO: This should probably be done automatically, or should consider the aspect ratio or something + // TODO: This should probably be done automatically, or should consider the aspect ratio or something + /** + * Sets the bounds of the viewport + * @param lowerX + * @param lowerY + * @param upperX + * @param upperY + */ setBounds(lowerX: number, lowerY: number, upperX: number, upperY: number): void { this.bounds = new Vec4(lowerX, lowerY, upperX, upperY); } + /** + * Make the viewport follow the specified GameNode + * @param node The GameNode to follow + */ follow(node: GameNode): void { this.following = node; } update(deltaT: number): void { + // If viewport is following an object if(this.following){ + // Set this position either to the object or to its bounds this.position.x = this.following.getPosition().x - this.size.x/2; this.position.y = this.following.getPosition().y - this.size.y/2; let [min, max] = this.bounds.split(); diff --git a/src/Sound/Audio.ts b/src/Sound/Audio.ts index 34a7730..9e3cb2b 100644 --- a/src/Sound/Audio.ts +++ b/src/Sound/Audio.ts @@ -8,7 +8,11 @@ export default class Audio { this.key = key; } - play(loop?: boolean){ + /** + * Play the sound this audio represents + * @param loop A boolean for whether or not to loop the sound + */ + play(loop?: boolean): void { this.sound = AudioManager.getInstance().createSound(this.key); if(loop){ @@ -18,7 +22,10 @@ export default class Audio { this.sound.start(); } - stop(){ + /** + * Stop the sound this audio represents + */ + stop(): void { if(this.sound){ this.sound.stop(); } diff --git a/src/Sound/AudioManager.ts b/src/Sound/AudioManager.ts index 0fe51ff..0bd110a 100644 --- a/src/Sound/AudioManager.ts +++ b/src/Sound/AudioManager.ts @@ -9,6 +9,9 @@ export default class AudioManager { this.initAudio(); } + /** + * Get the instance of the AudioManager class or create a new one if none exists + */ public static getInstance(): AudioManager { if(!this.instance){ this.instance = new AudioManager(); @@ -16,7 +19,10 @@ export default class AudioManager { return this.instance; } - private initAudio(): void { + /** + * Initializes the webAudio context + */ + private initAudio(): void { try { window.AudioContext = window.AudioContext;// || window.webkitAudioContext; this.audioCtx = new AudioContext(); @@ -24,24 +30,30 @@ export default class AudioManager { } catch(e) { console.log('Web Audio API is not supported in this browser'); } - } + } + /** + * Returns the current audio context + */ public getAudioContext(): AudioContext { return this.audioCtx; } + /** + * Creates a new sound from the key of a loaded audio file + * @param key The key of the loaded audio file to create a new sound for + */ createSound(key: string): AudioBufferSourceNode { // Get audio buffer let buffer = ResourceManager.getInstance().getAudio(key); - // creates a sound source + // Create a sound source var source = this.audioCtx.createBufferSource(); - // tell the source which sound to play + // Tell the source which sound to play source.buffer = buffer; - // connect the source to the context's destination - // i.e. the speakers + // Connect the source to the context's destination source.connect(this.audioCtx.destination); return source; diff --git a/src/Utils/Color.ts b/src/Utils/Color.ts index 2b4aa56..0c59cf7 100644 --- a/src/Utils/Color.ts +++ b/src/Utils/Color.ts @@ -1,7 +1,7 @@ import MathUtils from "./MathUtils"; // TODO: This should be moved to the datatypes folder -export default class Color{ +export default class Color { public r: number; public g: number; public b: number; @@ -14,22 +14,37 @@ export default class Color{ this.a = a; } + /** + * Returns a new color slightly lighter than the current color + */ lighten(): Color { return new Color(MathUtils.clamp(this.r + 40, 0, 255), MathUtils.clamp(this.g + 40, 0, 255), MathUtils.clamp(this.b + 40, 0, 255), this.a); } + /** + * Returns a new color slightly darker than the current color + */ darken(): Color { return new Color(MathUtils.clamp(this.r - 40, 0, 255), MathUtils.clamp(this.g - 40, 0, 255), MathUtils.clamp(this.b - 40, 0, 255), this.a); } + /** + * Returns the color as a string of the form #RRGGBB + */ toString(): string { return "#" + MathUtils.toHex(this.r, 2) + MathUtils.toHex(this.g, 2) + MathUtils.toHex(this.b, 2); } + /** + * Returns the color as a string of the form rgb(r, g, b) + */ toStringRGB(): string { return "rgb(" + this.r.toString() + ", " + this.g.toString() + ", " + this.b.toString() + ")"; } + /** + * Returns the color as a string of the form rgba(r, g, b, a) + */ toStringRGBA(): string { if(this.a === null){ return this.toStringRGB(); diff --git a/src/Utils/MathUtils.ts b/src/Utils/MathUtils.ts index 2a1575a..551293c 100644 --- a/src/Utils/MathUtils.ts +++ b/src/Utils/MathUtils.ts @@ -1,10 +1,21 @@ -export default class MathUtils{ +export default class MathUtils { + /** + * Clamps the value x to the range [min, max], rounding up or down if needed + * @param x The value to be clamped + * @param min The min of the range + * @param max The max of the range + */ static clamp(x: number, min: number, max: number): number { if(x < min) return min; if(x > max) return max; return x; } + /** + * Returns the number as a hexadecimal + * @param num The number to convert to hex + * @param minLength The length of the returned hex string (adds zero padding if needed) + */ static toHex(num: number, minLength: number = null): string { let factor = 1; while(factor*16 < num){ @@ -27,6 +38,10 @@ export default class MathUtils{ return hexStr; } + /** + * Converts the number to hexadecimal + * @param num The number to convert to hexadecimal + */ static toHexDigit(num: number): string { if(num < 10){ return "" + num; diff --git a/src/Utils/RandUtils.ts b/src/Utils/RandUtils.ts index d0e59b6..d5642c7 100644 --- a/src/Utils/RandUtils.ts +++ b/src/Utils/RandUtils.ts @@ -1,15 +1,28 @@ import MathUtils from "./MathUtils"; import Color from "./Color"; -export default class RandUtils{ +export default class RandUtils { + /** + * Generates a random integer in the specified range + * @param min The min of the range (inclusive) + * @param max The max of the range (exclusive) + */ static randInt(min: number, max: number): number { return Math.floor(Math.random()*(max - min) + min); } + /** + * Generates a random hexadecimal number in the specified range + * @param min The min of the range (inclusive) + * @param max The max of the range (exclusive) + */ static randHex(min: number, max: number): string { return MathUtils.toHex(RandUtils.randInt(min, max)); } + /** + * Generates a random color + */ static randColor(): Color { let r = RandUtils.randInt(0, 256); let g = RandUtils.randInt(0, 256); diff --git a/src/Utils/StringUtils.ts b/src/Utils/StringUtils.ts index 6c172b5..1d5da49 100644 --- a/src/Utils/StringUtils.ts +++ b/src/Utils/StringUtils.ts @@ -1,4 +1,8 @@ export default class StringUtils { + /** + * Extracts the path from a filepath that includes the file + * @param filePath the filepath to extract the path form + */ static getPathFromFilePath(filePath: string): string { let splitPath = filePath.split("/"); splitPath.pop();