commented code and cleaned formatting

This commit is contained in:
Joe Weaver 2020-09-13 20:57:28 -04:00
parent 8cb4fb7972
commit 3a57a1acab
45 changed files with 827 additions and 88 deletions

View File

@ -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 { export default interface Collection {
/**
* Iterates through all of the items in this data structure.
* @param func
*/
forEach(func: Function): void; forEach(func: Function): void;
/**
* Clears the contents of the data structure
*/
clear(): void;
} }

View File

@ -1,5 +1,8 @@
import Collection from "./Collection"; import Collection from "./Collection";
/**
* Associates strings with elements of type T
*/
export default class Map<T> implements Collection { export default class Map<T> implements Collection {
private map: Record<string, T>; private map: Record<string, T>;
@ -7,31 +10,52 @@ export default class Map<T> implements Collection {
this.map = {}; this.map = {};
} }
/**
* Adds a value T stored at a key.
* @param key
* @param value
*/
add(key: string, value: T): void { add(key: string, value: T): void {
this.map[key] = value; this.map[key] = value;
} }
/**
* Get the value associated with a key.
* @param key
*/
get(key: string): T { get(key: string): T {
return this.map[key]; 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 { set(key: string, value: T): void {
this.add(key, value); this.add(key, value);
} }
/**
* Returns true if there is a value stored at the specified key, false otherwise.
* @param key
*/
has(key: string): boolean { has(key: string): boolean {
return this.map[key] !== undefined; return this.map[key] !== undefined;
} }
/**
* Returns an array of all of the keys in this map.
*/
keys(): Array<string> { keys(): Array<string> {
return Object.keys(this.map); return Object.keys(this.map);
} }
forEach(func: Function): void { forEach(func: (key: string) => void): void {
Object.keys(this.map).forEach(key => func(key)); Object.keys(this.map).forEach(key => func(key));
} }
clear(): void { clear(): void {
this.forEach((key: string) => delete this.map[key]); this.forEach(key => delete this.map[key]);
} }
} }

View File

@ -1,6 +1,9 @@
import Collection from "./Collection"; import Collection from "./Collection";
export default class Queue<T> implements Collection{ /**
* A FIFO queue with elements of type T
*/
export default class Queue<T> implements Collection {
private readonly MAX_ELEMENTS: number; private readonly MAX_ELEMENTS: number;
private q: Array<T>; private q: Array<T>;
private head: number; private head: number;
@ -15,6 +18,10 @@ export default class Queue<T> implements Collection{
this.size = 0; this.size = 0;
} }
/**
* Adds an item to the back of the queue
* @param item
*/
enqueue(item: T): void{ enqueue(item: T): void{
if((this.tail + 1) % this.MAX_ELEMENTS === this.head){ if((this.tail + 1) % this.MAX_ELEMENTS === this.head){
throw "Queue full - cannot add element" throw "Queue full - cannot add element"
@ -25,6 +32,9 @@ export default class Queue<T> implements Collection{
this.tail = (this.tail + 1) % this.MAX_ELEMENTS; this.tail = (this.tail + 1) % this.MAX_ELEMENTS;
} }
/**
* Retrieves an item from the front of the queue
*/
dequeue(): T { dequeue(): T {
if(this.head === this.tail){ if(this.head === this.tail){
throw "Queue empty - cannot remove element" throw "Queue empty - cannot remove element"
@ -33,11 +43,16 @@ export default class Queue<T> implements Collection{
this.size -= 1; this.size -= 1;
let item = this.q[this.head]; let item = this.q[this.head];
// Now delete the item
delete this.q[this.head];
this.head = (this.head + 1) % this.MAX_ELEMENTS; this.head = (this.head + 1) % this.MAX_ELEMENTS;
return item; return item;
} }
/**
* Returns the item at the front of the queue, but does not return it
*/
peekNext(): T { peekNext(): T {
if(this.head === this.tail){ if(this.head === this.tail){
throw "Queue empty - cannot get element" throw "Queue empty - cannot get element"
@ -48,24 +63,30 @@ export default class Queue<T> implements Collection{
return item; return item;
} }
/**
* Returns true if the queue has items in it, false otherwise
*/
hasItems(): boolean { hasItems(): boolean {
return this.head !== this.tail; return this.head !== this.tail;
} }
/**
* Returns the number of elements in the queue.
*/
getSize(): number { getSize(): number {
return this.size; return this.size;
} }
// TODO: This should actually delete the items in the queue instead of leaving them here
clear(): void { clear(): void {
this.forEach((item, index) => delete this.q[index]);
this.size = 0; this.size = 0;
this.head = this.tail; this.head = this.tail;
} }
forEach(func: Function): void { forEach(func: (item: T, index?: number) => void): void {
let i = this.head; let i = this.head;
while(i !== this.tail){ while(i !== this.tail){
func(this.q[i]); func(this.q[i], i);
i = (i + 1) % this.MAX_ELEMENTS; i = (i + 1) % this.MAX_ELEMENTS;
} }
} }

View File

@ -1,6 +1,9 @@
import Collection from "./Collection"; import Collection from "./Collection";
export default class Stack<T> implements Collection{ /**
* A LIFO stack with items of type T
*/
export default class Stack<T> implements Collection {
readonly MAX_ELEMENTS: number; readonly MAX_ELEMENTS: number;
private stack: Array<T>; private stack: Array<T>;
private head: number; private head: number;
@ -13,7 +16,7 @@ export default class Stack<T> implements Collection{
/** /**
* Adds an item to the top of the stack * 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 { push(item: T): void {
if(this.head + 1 === this.MAX_ELEMENTS){ if(this.head + 1 === this.MAX_ELEMENTS){
@ -44,10 +47,8 @@ export default class Stack<T> implements Collection{
return this.stack[this.head]; return this.stack[this.head];
} }
/** clear(): void {
* Removes all elements from the stack this.forEach((item, index) => delete this.stack[index]);
*/
clear(): void{
this.head = -1; this.head = -1;
} }
@ -58,7 +59,7 @@ export default class Stack<T> implements Collection{
return this.head + 1; return this.head + 1;
} }
forEach(func: Function): void{ forEach(func: (item: T, index?: number) => void): void{
let i = 0; let i = 0;
while(i <= this.head){ while(i <= this.head){
func(this.stack[i]); func(this.stack[i]);

View File

@ -1,3 +1,6 @@
/**
* a representation of Tiled's tilemap data
*/
export class TiledTilemapData { export class TiledTilemapData {
height: number; height: number;
width: number; width: number;
@ -8,12 +11,18 @@ export class TiledTilemapData {
tilesets: Array<TiledTilesetData>; tilesets: Array<TiledTilesetData>;
} }
/**
* A representation of a custom layer property in a Tiled tilemap
*/
export class TiledLayerProperty { export class TiledLayerProperty {
name: string; name: string;
type: string; type: string;
value: any; value: any;
} }
/**
* A representation of a tileset in a Tiled tilemap
*/
export class TiledTilesetData { export class TiledTilesetData {
columns: number; columns: number;
tilewidth: number; tilewidth: number;
@ -28,6 +37,9 @@ export class TiledTilesetData {
image: string; image: string;
} }
/**
* A representation of a layer in a Tiled tilemap
*/
export class TiledLayerData { export class TiledLayerData {
data: number[]; data: number[];
x: number; x: number;

View File

@ -17,9 +17,14 @@ export default class Tileset {
// TODO: Change this to be more general and work with other tileset formats // TODO: Change this to be more general and work with other tileset formats
constructor(tilesetData: TiledTilesetData){ constructor(tilesetData: TiledTilesetData){
// Defer handling of the data to a helper class
this.initFromTiledData(tilesetData); 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 { initFromTiledData(tiledData: TiledTilesetData): void {
this.numRows = tiledData.tilecount/tiledData.columns; this.numRows = tiledData.tilecount/tiledData.columns;
this.numCols = tiledData.columns; this.numCols = tiledData.columns;
@ -58,23 +63,32 @@ export default class Tileset {
return this.numCols; 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 { hasTile(tileIndex: number): boolean {
return tileIndex >= this.startIndex && tileIndex <= this.endIndex; 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 { renderTile(ctx: CanvasRenderingContext2D, tileIndex: number, dataIndex: number, worldSize: Vec2, origin: Vec2, scale: Vec2): void {
// Get the true index
let index = tileIndex - this.startIndex; let index = tileIndex - this.startIndex;
let row = Math.floor(index / this.numCols); let row = Math.floor(index / this.numCols);
let col = index % this.numCols; let col = index % this.numCols;
let width = this.tileSize.x; let width = this.tileSize.x;
let height = this.tileSize.y; let height = this.tileSize.y;
// Calculate the position to start a crop in the tileset image
let left = col * width; let left = col * width;
let top = row * height; let top = row * height;
// Calculate the position in the world to render the tile
let x = (dataIndex % worldSize.x) * width * scale.x; let x = (dataIndex % worldSize.x) * width * scale.x;
let y = Math.floor(dataIndex / worldSize.x) * height * scale.y; 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); ctx.drawImage(this.image, left, top, width, height, x - origin.x, y - origin.y, width * scale.x, height * scale.y);

View File

@ -1,21 +1,51 @@
/**
* A two-dimensional vector (x, y)
*/
export default class Vec2 { export default class Vec2 {
public x: number; // Store x and y in an array
public y: number; private vec: Float32Array;
constructor(x: number = 0, y: number = 0) { constructor(x: number = 0, y: number = 0) {
this.x = x; this.vec = new Float32Array(2);
this.y = y; 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 { magSq(): number {
return this.x*this.x + this.y*this.y; return this.x*this.x + this.y*this.y;
} }
/**
* The magnitude of the vector
*/
mag(): number { mag(): number {
return Math.sqrt(this.magSq()); return Math.sqrt(this.magSq());
} }
/**
* Returns this vector as a unit vector - Equivalent to dividing x and y by the magnitude
*/
normalize(): Vec2 { normalize(): Vec2 {
if(this.x === 0 && this.y === 0) return this; if(this.x === 0 && this.y === 0) return this;
let mag = this.mag(); let mag = this.mag();
@ -24,16 +54,29 @@ export default class Vec2 {
return this; 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 { setToAngle(angle: number): Vec2 {
this.x = Math.cos(angle); this.x = Math.cos(angle);
this.y = Math.sin(angle); this.y = Math.sin(angle);
return this; return this;
} }
/**
* Keeps the vector's direction, but sets its magnitude to be the provided magnitude
* @param magnitude
*/
scaleTo(magnitude: number): Vec2 { scaleTo(magnitude: number): Vec2 {
return this.normalize().scale(magnitude); 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 { scale(factor: number, yFactor: number = null): Vec2 {
if(yFactor !== null){ if(yFactor !== null){
this.x *= factor; this.x *= factor;
@ -45,6 +88,10 @@ export default class Vec2 {
return this; 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 { rotateCCW(angle: number): Vec2 {
let cs = Math.cos(angle); let cs = Math.cos(angle);
let sn = Math.sin(angle); let sn = Math.sin(angle);
@ -55,38 +102,65 @@ export default class Vec2 {
return this; return this;
} }
/**
* Sets the vectors coordinates to be the ones provided
* @param x
* @param y
*/
set(x: number, y: number): Vec2 { set(x: number, y: number): Vec2 {
this.x = x; this.x = x;
this.y = y; this.y = y;
return this; return this;
} }
/**
* Adds this vector the another vector
* @param other
*/
add(other: Vec2): Vec2 { add(other: Vec2): Vec2 {
this.x += other.x; this.x += other.x;
this.y += other.y; this.y += other.y;
return this; return this;
} }
/**
* Subtracts another vector from this vector
* @param other
*/
sub(other: Vec2): Vec2 { sub(other: Vec2): Vec2 {
this.x -= other.x; this.x -= other.x;
this.y -= other.y; this.y -= other.y;
return this; return this;
} }
/**
* Multiplies this vector with another vector element-wise
* @param other
*/
mult(other: Vec2): Vec2 { mult(other: Vec2): Vec2 {
this.x *= other.x; this.x *= other.x;
this.y *= other.y; this.y *= other.y;
return this; return this;
} }
/**
* Returns a string representation of this vector rounded to 1 decimal point
*/
toString(): string { toString(): string {
return this.toFixed(); 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 { toFixed(numDecimalPoints: number = 1): string {
return "(" + this.x.toFixed(numDecimalPoints) + ", " + this.y.toFixed(numDecimalPoints) + ")"; return "(" + this.x.toFixed(numDecimalPoints) + ", " + this.y.toFixed(numDecimalPoints) + ")";
} }
/**
* Returns a new vector with the same coordinates as this one.
*/
clone(): Vec2 { clone(): Vec2 {
return new Vec2(this.x, this.y); return new Vec2(this.x, this.y);
} }

View File

@ -2,18 +2,49 @@ import Vec2 from "./Vec2";
export default class Vec4{ export default class Vec4{
public x : number; public vec: Float32Array;
public y : number;
public z : number;
public w : number;
constructor(x : number = 0, y : number = 0, z : number = 0, w : number = 0) { constructor(x : number = 0, y : number = 0, z : number = 0, w : number = 0) {
this.x = x; this.vec = new Float32Array(4);
this.y = y; this.vec[0] = x;
this.z = z; this.vec[1] = y;
this.w = w; 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] { split() : [Vec2, Vec2] {
return [new Vec2(this.x, this.y), new Vec2(this.z, this.w)]; return [new Vec2(this.x, this.y), new Vec2(this.z, this.w)];
} }

View File

@ -2,6 +2,7 @@ import Map from "../DataTypes/Map";
export default class Debug { export default class Debug {
// A map of log messages to display on the screen
private static logMessages: Map<string> = new Map(); private static logMessages: Map<string> = new Map();
static log(id: string, message: string): void { static log(id: string, message: string): void {

View File

@ -27,6 +27,11 @@ export default class EventQueue {
this.q.enqueue(event); 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<string>): void { subscribe(receiver: Receiver, type: string | Array<string>): void {
if(type instanceof Array){ if(type instanceof Array){
// If it is an array, subscribe to all event types // 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 { private addListener(receiver: Receiver, type: string): void {
if(this.receivers.has(type)){ if(this.receivers.has(type)){
this.receivers.get(type).push(receiver); this.receivers.get(type).push(receiver);
@ -48,14 +54,17 @@ export default class EventQueue {
update(deltaT: number): void{ update(deltaT: number): void{
while(this.q.hasItems()){ while(this.q.hasItems()){
// Retrieve each event
let event = this.q.dequeue(); let event = this.q.dequeue();
// If a receiver has this event type, send it the event
if(this.receivers.has(event.type)){ if(this.receivers.has(event.type)){
for(let receiver of this.receivers.get(event.type)){ for(let receiver of this.receivers.get(event.type)){
receiver.receive(event); receiver.receive(event);
} }
} }
// If a receiver is subscribed to all events, send it the event
if(this.receivers.has("all")){ if(this.receivers.has("all")){
for(let receiver of this.receivers.get("all")){ for(let receiver of this.receivers.get("all")){
receiver.receive(event); receiver.receive(event);

View File

@ -1,11 +1,15 @@
import Map from "../DataTypes/Map" import Map from "../DataTypes/Map"
export default class GameEvent{ /**
* A representation of an in-game event
*/
export default class GameEvent {
public type: string; public type: string;
public data: Map<any>; public data: Map<any>;
public time: number; public time: number;
constructor(type: string, data: Map<any> | Record<string, any> = null){ constructor(type: string, data: Map<any> | Record<string, any> = null) {
// Parse the game event data
if (data === null) { if (data === null) {
this.data = new Map<any>(); this.data = new Map<any>();
} else if (!(data instanceof Map)){ } else if (!(data instanceof Map)){

View File

@ -1,6 +1,9 @@
import Queue from "../DataTypes/Queue"; import Queue from "../DataTypes/Queue";
import GameEvent from "./GameEvent"; import GameEvent from "./GameEvent";
/**
* Receives subscribed events from the EventQueue
*/
export default class Receiver{ export default class Receiver{
readonly MAX_SIZE: number; readonly MAX_SIZE: number;
private q: Queue<GameEvent>; private q: Queue<GameEvent>;
@ -10,22 +13,37 @@ export default class Receiver{
this.q = new Queue(this.MAX_SIZE); this.q = new Queue(this.MAX_SIZE);
} }
/**
* Adds an event to the queue of this reciever
*/
receive(event: GameEvent): void { receive(event: GameEvent): void {
this.q.enqueue(event); this.q.enqueue(event);
} }
/**
* Retrieves the next event from the receiver's queue
*/
getNextEvent(): GameEvent { getNextEvent(): GameEvent {
return this.q.dequeue(); return this.q.dequeue();
} }
/**
* Looks at the next event in the receiver's queue
*/
peekNextEvent(): GameEvent { peekNextEvent(): GameEvent {
return this.q.peekNext() return this.q.peekNext()
} }
/**
* Returns true if the receiver has any events in its queue
*/
hasNextEvent(): boolean { hasNextEvent(): boolean {
return this.q.hasItems(); return this.q.hasItems();
} }
/**
* Ignore all events this frame
*/
ignoreEvents(): void { ignoreEvents(): void {
this.q.clear(); this.q.clear();
} }

View File

@ -2,6 +2,9 @@ import EventQueue from "../Events/EventQueue";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
import GameEvent from "../Events/GameEvent"; 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{ export default class InputHandler{
private eventQueue: EventQueue; private eventQueue: EventQueue;

View File

@ -4,6 +4,9 @@ import Vec2 from "../DataTypes/Vec2";
import EventQueue from "../Events/EventQueue"; import EventQueue from "../Events/EventQueue";
import Viewport from "../SceneGraph/Viewport"; 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{ export default class InputReceiver{
private static instance: InputReceiver = null; private static instance: InputReceiver = null;
@ -27,6 +30,7 @@ export default class InputReceiver{
this.mousePressPosition = new Vec2(0, 0); this.mousePressPosition = new Vec2(0, 0);
this.eventQueue = EventQueue.getInstance(); 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"]); 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()){ while(this.receiver.hasNextEvent()){
let event = this.receiver.getNextEvent(); let event = this.receiver.getNextEvent();
// Handle each event type
if(event.type === "mouse_down"){ if(event.type === "mouse_down"){
this.mouseJustPressed = true; this.mouseJustPressed = true;
this.mousePressed = 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.keyJustPressed.forEach((key: string) => this.keyJustPressed.set(key, false));
this.keyPressed.forEach((key: string) => this.keyPressed.set(key, false)); this.keyPressed.forEach((key: string) => this.keyPressed.set(key, false));
} }

View File

@ -29,11 +29,14 @@ export default class GameLoop{
private running: boolean; private running: boolean;
private frameDelta: number; private frameDelta: number;
// Game canvas and its width and height
readonly GAME_CANVAS: HTMLCanvasElement; readonly GAME_CANVAS: HTMLCanvasElement;
readonly WIDTH: number; readonly WIDTH: number;
readonly HEIGHT: number; readonly HEIGHT: number;
private viewport: Viewport; private viewport: Viewport;
private ctx: CanvasRenderingContext2D; private ctx: CanvasRenderingContext2D;
// All of the necessary subsystems that need to run here
private eventQueue: EventQueue; private eventQueue: EventQueue;
private inputHandler: InputHandler; private inputHandler: InputHandler;
private inputReceiver: InputReceiver; private inputReceiver: InputReceiver;
@ -54,15 +57,20 @@ export default class GameLoop{
this.started = false; this.started = false;
this.running = 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 = document.getElementById("game-canvas") as HTMLCanvasElement;
this.GAME_CANVAS.style.setProperty("background-color", "whitesmoke"); this.GAME_CANVAS.style.setProperty("background-color", "whitesmoke");
// Give the canvas a size and get the rendering context
this.WIDTH = 800; this.WIDTH = 800;
this.HEIGHT = 500; this.HEIGHT = 500;
this.ctx = this.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT); this.ctx = this.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT);
// Size the viewport to the game canvas
this.viewport = new Viewport(); this.viewport = new Viewport();
this.viewport.setSize(this.WIDTH, this.HEIGHT); this.viewport.setSize(this.WIDTH, this.HEIGHT);
// Initialize all necessary game subsystems
this.eventQueue = EventQueue.getInstance(); this.eventQueue = EventQueue.getInstance();
this.inputHandler = new InputHandler(this.GAME_CANVAS); this.inputHandler = new InputHandler(this.GAME_CANVAS);
this.inputReceiver = InputReceiver.getInstance(); this.inputReceiver = InputReceiver.getInstance();
@ -77,10 +85,18 @@ export default class GameLoop{
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
let ctx = canvas.getContext("2d"); let ctx = canvas.getContext("2d");
// For crisp pixel art
ctx.imageSmoothingEnabled = false; ctx.imageSmoothingEnabled = false;
return ctx; 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 { setMaxFPS(initMax: number): void {
this.maxFPS = initMax; this.maxFPS = initMax;
this.simulationTimestep = Math.floor(1000/this.maxFPS); this.simulationTimestep = Math.floor(1000/this.maxFPS);
@ -90,6 +106,10 @@ export default class GameLoop{
return this.sceneManager; return this.sceneManager;
} }
/**
* Updates the frame count and sum of time for the framerate of the game
* @param timestep
*/
private updateFrameCount(timestep: number): void { private updateFrameCount(timestep: number): void {
this.frame += 1; this.frame += 1;
this.numFramesInSum += 1; this.numFramesInSum += 1;
@ -103,6 +123,9 @@ export default class GameLoop{
Debug.log("fps", "FPS: " + this.fps.toFixed(1)); Debug.log("fps", "FPS: " + this.fps.toFixed(1));
} }
/**
* Starts up the game loop and calls the first requestAnimationFrame
*/
start(): void { start(): void {
if(!this.started){ if(!this.started){
this.started = true; 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 => { startFrame = (timestamp: number): void => {
this.running = true; this.running = true;
@ -121,6 +148,10 @@ export default class GameLoop{
window.requestAnimationFrame(this.doFrame); window.requestAnimationFrame(this.doFrame);
} }
/**
* The main loop of the game. Updates and renders every frame
* @param timestamp
*/
doFrame = (timestamp: number): void => { doFrame = (timestamp: number): void => {
// Request animation frame to prepare for another update or render // Request animation frame to prepare for another update or render
window.requestAnimationFrame(this.doFrame); window.requestAnimationFrame(this.doFrame);
@ -148,14 +179,30 @@ export default class GameLoop{
this.render(); this.render();
} }
/**
* Updates all necessary subsystems of the game. Defers scene updates to the sceneManager
* @param deltaT
*/
update(deltaT: number): void { update(deltaT: number): void {
// Handle all events that happened since the start of the last loop
this.eventQueue.update(deltaT); this.eventQueue.update(deltaT);
// Update the input data structures so game objects can see the input
this.inputReceiver.update(deltaT); this.inputReceiver.update(deltaT);
// Update the recording of the game
this.recorder.update(deltaT); this.recorder.update(deltaT);
// Update all scenes
this.sceneManager.update(deltaT); this.sceneManager.update(deltaT);
// Load or unload any resources if needed
this.resourceManager.update(deltaT); this.resourceManager.update(deltaT);
} }
/**
* Clears the canvas and defers scene rendering to the sceneManager. Renders the debug
*/
render(): void { render(): void {
this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT); this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT);
this.sceneManager.render(this.ctx); this.sceneManager.render(this.ctx);

View File

@ -1,7 +1,9 @@
import GameNode from "./GameNode"; import GameNode from "./GameNode";
import Vec2 from "../DataTypes/Vec2"; 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{ export default abstract class CanvasNode extends GameNode{
protected size: Vec2; 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 { contains(x: number, y: number): boolean {
if(this.position.x < x && this.position.x + this.size.x > x){ if(this.position.x < x && this.position.x + this.size.x > x){
if(this.position.y < y && this.position.y + this.size.y > y){ if(this.position.y < y && this.position.y + this.size.y > y){

View File

@ -7,6 +7,9 @@ import GameEvent from "../Events/GameEvent";
import Scene from "../Scene/Scene"; import Scene from "../Scene/Scene";
import Layer from "../Scene/Layer"; import Layer from "../Scene/Layer";
/**
* The representation of an object in the game world
*/
export default abstract class GameNode{ export default abstract class GameNode{
private eventQueue: EventQueue; private eventQueue: EventQueue;
protected input: InputReceiver; 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 { subscribe(eventType: string): void {
this.eventQueue.subscribe(this.receiver, eventType); 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<any> | Record<string, any> = null): void { emit(eventType: string, data: Map<any> | Record<string, any> = null): void {
let event = new GameEvent(eventType, data); let event = new GameEvent(eventType, data);
this.eventQueue.addEvent(event); this.eventQueue.addEvent(event);
} }
// TODO - This doesn't seem ideal. Is there a better way to do this? // 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()); return this.scene.getViewport().getPosition().clone().mult(this.layer.getParallax());
} }

View File

@ -1,9 +1,12 @@
import CanvasNode from "./CanvasNode"; import CanvasNode from "./CanvasNode";
import Color from "../Utils/Color"; 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 { export default abstract class Graphic extends CanvasNode {
color: Color; protected color: Color;
setColor(color: Color){ setColor(color: Color){
this.color = color; this.color = color;

View File

@ -2,6 +2,9 @@ import CanvasNode from "../CanvasNode";
import ResourceManager from "../../ResourceManager/ResourceManager"; import ResourceManager from "../../ResourceManager/ResourceManager";
import Vec2 from "../../DataTypes/Vec2"; import Vec2 from "../../DataTypes/Vec2";
/**
* The representation of a sprite - an in-game image
*/
export default class Sprite extends CanvasNode { export default class Sprite extends CanvasNode {
private imageId: string; private imageId: string;
private scale: Vec2; private scale: Vec2;
@ -14,10 +17,17 @@ export default class Sprite extends CanvasNode {
this.scale = new Vec2(1, 1); this.scale = new Vec2(1, 1);
} }
/**
* Returns the scale of the sprite
*/
getScale(): Vec2 { getScale(): Vec2 {
return this.scale; return this.scale;
} }
/**
* Sets the scale of the sprite to the value provided
* @param scale
*/
setScale(scale: Vec2): void { setScale(scale: Vec2): void {
this.scale = scale; this.scale = scale;
} }

View File

@ -4,9 +4,10 @@ import Tileset from "../DataTypes/Tilesets/Tileset";
import { TiledTilemapData, TiledLayerData } from "../DataTypes/Tilesets/TiledData" 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 { export default abstract class Tilemap extends GameNode {
// A tileset represents the tiles within one specific image loaded from a file
protected tilesets: Array<Tileset>; protected tilesets: Array<Tileset>;
protected worldSize: Vec2; protected worldSize: Vec2;
protected tileSize: Vec2; protected tileSize: Vec2;
@ -21,6 +22,8 @@ export default abstract class Tilemap extends GameNode {
this.tilesets = new Array<Tileset>(); this.tilesets = new Array<Tileset>();
this.worldSize = new Vec2(0, 0); this.worldSize = new Vec2(0, 0);
this.tileSize = 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.parseTilemapData(tilemapData, layer);
this.scale = new Vec2(4, 4); this.scale = new Vec2(4, 4);
} }

View File

@ -3,9 +3,16 @@ import Vec2 from "../../DataTypes/Vec2";
import { TiledTilemapData, TiledLayerData } from "../../DataTypes/Tilesets/TiledData"; import { TiledTilemapData, TiledLayerData } from "../../DataTypes/Tilesets/TiledData";
import Tileset from "../../DataTypes/Tilesets/Tileset"; 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 { 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 { protected parseTilemapData(tilemapData: TiledTilemapData, layer: TiledLayerData): void {
this.worldSize.set(tilemapData.width, tilemapData.height); this.worldSize.set(tilemapData.width, tilemapData.height);
this.tileSize.set(tilemapData.tilewidth, tilemapData.tileheight); 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))); 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 { getTileAt(worldCoords: Vec2): number {
let localCoords = this.getColRowAt(worldCoords); let localCoords = this.getColRowAt(worldCoords);
if(localCoords.x < 0 || localCoords.x >= this.worldSize.x || localCoords.y < 0 || localCoords.y >= this.worldSize.y){ 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] 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 { isTileCollidable(indexOrCol: number, row?: number): boolean {
let index = 0; let index = 0;
if(row){ if(row){
@ -53,6 +69,10 @@ export default class OrthogonalTilemap extends Tilemap {
return this.data[index] !== 0 && this.collidable; 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? // TODO: Should this throw an error if someone tries to access an out of bounds value?
getColRowAt(worldCoords: Vec2): Vec2 { getColRowAt(worldCoords: Vec2): Vec2 {
let col = Math.floor(worldCoords.x / this.tileSize.x / this.scale.x); let col = Math.floor(worldCoords.x / this.tileSize.x / this.scale.x);

View File

@ -2,6 +2,9 @@ import CanvasNode from "./CanvasNode";
import Color from "../Utils/Color"; import Color from "../Utils/Color";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
/**
* The representation of a UIElement - the parent class of things like buttons
*/
export default class UIElement extends CanvasNode{ export default class UIElement extends CanvasNode{
// Style attributes // Style attributes
protected textColor: Color; protected textColor: Color;
@ -68,6 +71,7 @@ export default class UIElement extends CanvasNode{
} }
update(deltaT: number): void { update(deltaT: number): void {
// See of this object was just clicked
if(this.input.isMouseJustPressed()){ if(this.input.isMouseJustPressed()){
let clickPos = this.input.getMousePressPosition(); let clickPos = this.input.getMousePressPosition();
if(this.contains(clickPos.x, clickPos.y)){ 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.input.isMousePressed()){
if(this.isClicked){ if(this.isClicked){
this.isClicked = false; this.isClicked = false;
} }
} }
// Check if the mouse is hovering over this element
let mousePos = this.input.getMousePosition(); let mousePos = this.input.getMousePosition();
if(mousePos && this.contains(mousePos.x, mousePos.y)){ if(mousePos && this.contains(mousePos.x, mousePos.y)){
this.isEntered = true; 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 { protected calculateOffset(ctx: CanvasRenderingContext2D): Vec2 {
let textWidth = ctx.measureText(this.text).width; let textWidth = ctx.measureText(this.text).width;
@ -143,19 +153,29 @@ export default class UIElement extends CanvasNode{
return offset; return offset;
} }
/**
* Overridable method for calculating background color - useful for elements that want to be colored on different after certain events
*/
protected calculateBackgroundColor(): string { protected calculateBackgroundColor(): string {
return this.backgroundColor.toStringRGBA(); 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 { protected calculateBorderColor(): string {
return this.borderColor.toStringRGBA(); 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 { protected calculateTextColor(): string {
return this.textColor.toStringRGBA(); return this.textColor.toStringRGBA();
} }
render(ctx: CanvasRenderingContext2D): void { render(ctx: CanvasRenderingContext2D): void {
// Grab the global alpha so we can adjust it for this render
let previousAlpha = ctx.globalAlpha; let previousAlpha = ctx.globalAlpha;
ctx.globalAlpha = this.getLayer().getAlpha(); ctx.globalAlpha = this.getLayer().getAlpha();
@ -164,6 +184,7 @@ export default class UIElement extends CanvasNode{
ctx.font = this.fontSize + "px " + this.font; ctx.font = this.fontSize + "px " + this.font;
let offset = this.calculateOffset(ctx); let offset = this.calculateOffset(ctx);
// Stroke and fill a rounded rect and give it text
ctx.fillStyle = this.calculateBackgroundColor(); ctx.fillStyle = this.calculateBackgroundColor();
ctx.fillRoundedRect(this.position.x - origin.x, this.position.y - origin.y, this.size.x, this.size.y, this.borderRadius); ctx.fillRoundedRect(this.position.x - origin.x, this.position.y - origin.y, this.size.x, this.size.y, this.borderRadius);

View File

@ -12,6 +12,7 @@ export default class Button extends UIElement{
} }
protected calculateBackgroundColor(): string { protected calculateBackgroundColor(): string {
// Change the background color if clicked or hovered
if(this.isEntered && !this.isClicked){ if(this.isEntered && !this.isClicked){
return this.backgroundColor.lighten().toStringRGBA(); return this.backgroundColor.lighten().toStringRGBA();
} else if(this.isClicked){ } else if(this.isClicked){

View File

@ -18,51 +18,85 @@ export default class PhysicsManager {
this.movements = new Array(); this.movements = new Array();
} }
/**
* Adds a PhysicsNode to the manager to be handled in case of collisions
* @param node
*/
add(node: PhysicsNode): void { add(node: PhysicsNode): void {
this.physicsNodes.push(node); this.physicsNodes.push(node);
} }
/**
* Adds a tilemap node to the manager to be handled for collisions
* @param tilemap
*/
addTilemap(tilemap: Tilemap): void { addTilemap(tilemap: Tilemap): void {
this.tilemaps.push(tilemap); 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 { addMovement(node: PhysicsNode, velocity: Vec2): void {
this.movements.push(new MovementData(node, velocity)); 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 { private collideWithTilemap(node: PhysicsNode, tilemap: Tilemap, velocity: Vec2): void {
if(tilemap instanceof OrthogonalTilemap){ if(tilemap instanceof OrthogonalTilemap){
this.collideWithOrthogonalTilemap(node, tilemap, velocity); 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 { private collideWithOrthogonalTilemap(node: PhysicsNode, tilemap: OrthogonalTilemap, velocity: Vec2): void {
// Get the starting position of the moving node
let startPos = node.getPosition(); let startPos = node.getPosition();
// Get the end position of the moving node
let endPos = startPos.clone().add(velocity); let endPos = startPos.clone().add(velocity);
let size = node.getCollider().getSize(); 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 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)); 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 minIndex = tilemap.getColRowAt(min);
let maxIndex = tilemap.getColRowAt(max); 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<TileCollisionData>(); let tilemapCollisions = new Array<TileCollisionData>();
let tileSize = tilemap.getTileSize(); let tileSize = tilemap.getTileSize();
Debug.log("tilemapCollision", ""); Debug.log("tilemapCollision", "");
// Loop over all possible tiles // Loop over all possible tiles
for(let col = minIndex.x; col <= maxIndex.x; col++){ for(let col = minIndex.x; col <= maxIndex.x; col++){
for(let row = minIndex.y; row <= maxIndex.y; row++){ for(let row = minIndex.y; row <= maxIndex.y; row++){
if(tilemap.isTileCollidable(col, row)){ if(tilemap.isTileCollidable(col, row)){
Debug.log("tilemapCollision", "Colliding with Tile"); Debug.log("tilemapCollision", "Colliding with Tile");
// Tile position // Get the position of this tile
let tilePos = new Vec2(col * tileSize.x, row * tileSize.y); 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 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); 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; let overlap = 0;
if(dx * dy > 0){ if(dx * dy > 0){
overlap = dx * dy; 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); 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 => { tilemapCollisions.forEach(collision => {
let [firstContact, _, collidingX, collidingY] = this.getTimeOfAABBCollision(startPos, size, velocity, collision.position, tileSize, new Vec2(0, 0)); let [firstContact, _, collidingX, collidingY] = this.getTimeOfAABBCollision(startPos, size, velocity, collision.position, tileSize, new Vec2(0, 0));
// Handle collision // Handle collision
if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){ if( (firstContact.x < 1 || collidingX) && (firstContact.y < 1 || collidingY)){
if(collidingX && 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 { } else {
// let contactTime = Math.min(firstContact.x, firstContact.y); // Get the amount to scale x and y based on their initial collision times
// velocity.scale(contactTime);
let xScale = MathUtils.clamp(firstContact.x, 0, 1); let xScale = MathUtils.clamp(firstContact.x, 0, 1);
let yScale = MathUtils.clamp(firstContact.y, 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){ if(xScale === yScale){
xScale = 1; 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){ if(yScale !== 1){
node.setGrounded(true); node.setGrounded(true);
} }
// Scale the velocity of the node
velocity.scale(xScale, yScale); velocity.scale(xScale, yScale);
} }
} }
@ -264,6 +301,7 @@ export default class PhysicsManager {
// Helper classes for internal data // Helper classes for internal data
// TODO: Move these to 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 { class MovementData {
node: PhysicsNode; node: PhysicsNode;
velocity: Vec2; velocity: Vec2;
@ -273,6 +311,7 @@ class MovementData {
} }
} }
// Collision data objects for tilemaps
class TileCollisionData { class TileCollisionData {
position: Vec2; position: Vec2;
overlapArea: number; overlapArea: number;

View File

@ -3,6 +3,10 @@ import GameNode from "../Nodes/GameNode";
import PhysicsManager from "./PhysicsManager"; import PhysicsManager from "./PhysicsManager";
import Vec2 from "../DataTypes/Vec2"; 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 { export default abstract class PhysicsNode extends GameNode {
protected collider: Collider = null; protected collider: Collider = null;
@ -42,11 +46,19 @@ export default abstract class PhysicsNode extends GameNode {
return this.moving; 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 { protected move(velocity: Vec2): void {
this.moving = true; this.moving = true;
this.manager.addMovement(this, velocity); 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 { finishMove(velocity: Vec2): void {
this.position.add(velocity); this.position.add(velocity);
this.collider.getPosition().add(velocity); this.collider.getPosition().add(velocity);

View File

@ -6,30 +6,72 @@ import StringUtils from "../Utils/StringUtils";
import AudioManager from "../Sound/AudioManager"; import AudioManager from "../Sound/AudioManager";
export default class ResourceManager { export default class ResourceManager {
// Instance for the singleton class
private static instance: ResourceManager; private static instance: ResourceManager;
// Booleans to keep track of whether or not the ResourceManager is currently loading something
private loading: boolean; private loading: boolean;
private justLoaded: boolean; private justLoaded: boolean;
// Functions to do something when loading progresses or is completed such as render a loading screen
public onLoadProgress: Function; public onLoadProgress: Function;
public onLoadComplete: Function; public onLoadComplete: Function;
/**
* Number to keep track of how many images need to be loaded
*/
private imagesLoaded: number; private imagesLoaded: number;
/**
* Number to keep track of how many images are loaded
*/
private imagesToLoad: number; private imagesToLoad: number;
/**
* The queue of images we must load
*/
private imageLoadingQueue: Queue<{key: string, path: string}>; 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<HTMLImageElement>; private images: Map<HTMLImageElement>;
/**
* Number to keep track of how many tilemaps need to be loaded
*/
private tilemapsLoaded: number; private tilemapsLoaded: number;
/**
* Number to keep track of how many tilemaps are loaded
*/
private tilemapsToLoad: number; private tilemapsToLoad: number;
/**
* The queue of tilemaps we must load
*/
private tilemapLoadingQueue: Queue<{key: string, path: string}>; 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<TiledTilemapData>; private tilemaps: Map<TiledTilemapData>;
/**
* Number to keep track of how many sounds need to be loaded
*/
private audioLoaded: number; private audioLoaded: number;
/**
* Number to keep track of how many sounds are loaded
*/
private audioToLoad: number; private audioToLoad: number;
/**
* The queue of sounds we must load
*/
private audioLoadingQueue: Queue<{key: string, path: string}>; 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<AudioBuffer>; private audioBuffers: Map<AudioBuffer>;
// 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 typesToLoad: number;
private constructor(){ private constructor(){
@ -52,6 +94,9 @@ export default class ResourceManager {
this.audioBuffers = new Map(); this.audioBuffers = new Map();
}; };
/**
* Returns the current instance of this class or a new instance if none exist
*/
static getInstance(): ResourceManager { static getInstance(): ResourceManager {
if(!this.instance){ if(!this.instance){
this.instance = new ResourceManager(); this.instance = new ResourceManager();
@ -60,11 +105,20 @@ export default class ResourceManager {
return this.instance; 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 { public image(key: string, path: string): void {
this.imageLoadingQueue.enqueue({key: key, path: path}); 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); 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 { public audio(key: string, path: string): void {
this.audioLoadingQueue.enqueue({key: key, path: path}); this.audioLoadingQueue.enqueue({key: key, path: path});
} }
/**
* Retrieves a loaded audio file
* @param key
*/
public getAudio(key: string): AudioBuffer { public getAudio(key: string): AudioBuffer {
return this.audioBuffers.get(key); 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 { public tilemap(key: string, path: string): void {
this.tilemapLoadingQueue.enqueue({key: key, path: path}); this.tilemapLoadingQueue.enqueue({key: key, path: path});
} }
/**
* Retreives a loaded tilemap
* @param key
*/
public getTilemap(key: string): TiledTilemapData { public getTilemap(key: string): TiledTilemapData {
return this.tilemaps.get(key); return this.tilemaps.get(key);
} }
// TODO - Should everything be loaded in order, one file at a time? // 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 { loadResourcesFromQueue(callback: Function): void {
this.typesToLoad = 3; this.typesToLoad = 3;
@ -108,6 +184,9 @@ export default class ResourceManager {
} }
/**
* Deletes references to all resources in the resource manager
*/
unloadAllResources(): void { unloadAllResources(): void {
this.loading = false; this.loading = false;
this.justLoaded = false; this.justLoaded = false;
@ -125,7 +204,11 @@ export default class ResourceManager {
this.audioBuffers.clear(); 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.tilemapsToLoad = this.tilemapLoadingQueue.getSize();
this.tilemapsLoaded = 0; 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 { private loadTilemap(key: string, pathToTilemapJSON: string, callbackIfLast: Function): void {
this.loadTextFile(pathToTilemapJSON, (fileText: string) => { this.loadTextFile(pathToTilemapJSON, (fileText: string) => {
let tilemapObject = <TiledTilemapData>JSON.parse(fileText); let tilemapObject = <TiledTilemapData>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; this.tilemapsLoaded += 1;
if(this.tilemapsLoaded === this.tilemapsToLoad){ 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 { private loadImagesFromQueue(onFinishLoading: Function): void {
this.imagesToLoad = this.imageLoadingQueue.getSize(); this.imagesToLoad = this.imageLoadingQueue.getSize();
this.imagesLoaded = 0; 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 { public loadImage(key: string, path: string, callbackIfLast: Function): void {
var image = new Image(); var image = new Image();
@ -188,6 +290,10 @@ export default class ResourceManager {
image.src = path; 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 { private finishLoadingImage(callback: Function): void {
this.imagesLoaded += 1; 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){ private loadAudioFromQueue(onFinishLoading: Function){
this.audioToLoad = this.audioLoadingQueue.getSize(); this.audioToLoad = this.audioLoadingQueue.getSize();
this.audioLoaded = 0; 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 { private loadAudio(key: string, path: string, callbackIfLast: Function): void {
let audioCtx = AudioManager.getInstance().getAudioContext(); let audioCtx = AudioManager.getInstance().getAudioContext();
@ -228,6 +344,10 @@ export default class ResourceManager {
request.send(); 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 { private finishLoadingAudio(callback: Function): void {
this.audioLoaded += 1; this.audioLoaded += 1;

View File

@ -14,7 +14,11 @@ export default class AudioFactory {
this.audioManager = AudioManager.getInstance(); 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); let audio = new Audio(key);
return audio; return audio;
} }

View File

@ -14,6 +14,12 @@ export default class CanvasNodeFactory {
this.sceneGraph = sceneGraph; 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 = <T extends UIElement>(constr: new (...a: any) => T, layer: Layer, ...args: any): T => { addUIElement = <T extends UIElement>(constr: new (...a: any) => T, layer: Layer, ...args: any): T => {
let instance = new constr(...args); let instance = new constr(...args);
@ -27,8 +33,13 @@ export default class CanvasNodeFactory {
return instance; 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 // Add instance to scene
instance.setScene(this.scene); instance.setScene(this.scene);
@ -40,6 +51,12 @@ export default class CanvasNodeFactory {
return instance; 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 = <T extends Graphic>(constr: new (...a: any) => T, layer: Layer, ...args: any): T => { addGraphic = <T extends Graphic>(constr: new (...a: any) => T, layer: Layer, ...args: any): T => {
let instance = new constr(...args); let instance = new constr(...args);

View File

@ -9,6 +9,7 @@ import Tilemap from "../../Nodes/Tilemap";
export default class FactoryManager { 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 canvasNodeFactory: CanvasNodeFactory = new CanvasNodeFactory();
private physicsNodeFactory: PhysicsNodeFactory = new PhysicsNodeFactory(); private physicsNodeFactory: PhysicsNodeFactory = new PhysicsNodeFactory();
private tilemapFactory: TilemapFactory = new TilemapFactory(); private tilemapFactory: TilemapFactory = new TilemapFactory();
@ -21,6 +22,7 @@ export default class FactoryManager {
this.audioFactory.init(scene); this.audioFactory.init(scene);
} }
// Expose all of the factories through the factory manager
uiElement = this.canvasNodeFactory.addUIElement; uiElement = this.canvasNodeFactory.addUIElement;
sprite = this.canvasNodeFactory.addSprite; sprite = this.canvasNodeFactory.addSprite;
graphic = this.canvasNodeFactory.addGraphic; graphic = this.canvasNodeFactory.addGraphic;

View File

@ -13,6 +13,12 @@ export default class PhysicsNodeFactory {
} }
// TODO: Currently this doesn't care about layers // 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 = <T extends PhysicsNode>(constr: new (...a: any) => T, layer: Layer, ...args: any): T => { add = <T extends PhysicsNode>(constr: new (...a: any) => T, layer: Layer, ...args: any): T => {
let instance = new constr(...args); let instance = new constr(...args);
instance.setScene(this.scene); instance.setScene(this.scene);

View File

@ -16,6 +16,12 @@ export default class TilemapFactory {
this.resourceManager = ResourceManager.getInstance(); 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 = <T extends Tilemap>(key: string, constr: new (...a: any) => T, ...args: any): Array<Tilemap> => { add = <T extends Tilemap>(key: string, constr: new (...a: any) => T, ...args: any): Array<Tilemap> => {
// Get Tilemap Data // Get Tilemap Data
let tilemapData = this.resourceManager.getTilemap(key); let tilemapData = this.resourceManager.getTilemap(key);

View File

@ -3,6 +3,9 @@ import Scene from "./Scene";
import MathUtils from "../Utils/MathUtils"; import MathUtils from "../Utils/MathUtils";
import GameNode from "../Nodes/GameNode"; import GameNode from "../Nodes/GameNode";
/**
* A layer in the scene. Has its own alpha value and parallax.
*/
export default class Layer { export default class Layer {
protected scene: Scene; protected scene: Scene;
protected parallax: Vec2; protected parallax: Vec2;
@ -66,6 +69,4 @@ export default class Layer {
this.items.push(node); this.items.push(node);
node.setLayer(this); node.setLayer(this);
} }
render(ctx: CanvasRenderingContext2D): void {}
} }

View File

@ -1,3 +0,0 @@
import Layer from "../Layer";
export default class ObjectLayer extends Layer {}

View File

@ -1,6 +0,0 @@
import Layer from "../Layer";
import Tilemap from "../../Nodes/Tilemap";
export default class TiledLayer extends Layer {
private tilemap: Tilemap;
}

View File

@ -20,10 +20,21 @@ export default class Scene{
protected sceneManager: SceneManager; protected sceneManager: SceneManager;
protected tilemaps: Array<Tilemap>; protected tilemaps: Array<Tilemap>;
/**
* The scene graph of the Scene - can be exchanged with other SceneGraphs for more variation
*/
protected sceneGraph: SceneGraph; protected sceneGraph: SceneGraph;
protected physicsManager: PhysicsManager; protected physicsManager: PhysicsManager;
/**
* An interface that allows the adding of different nodes to the scene
*/
public add: FactoryManager; public add: FactoryManager;
/**
* An interface that allows the loading of different files for use in the scene
*/
public load: ResourceManager; public load: ResourceManager;
constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop){ constructor(viewport: Viewport, sceneManager: SceneManager, game: GameLoop){
@ -39,19 +50,38 @@ export default class Scene{
this.sceneGraph = new SceneGraphArray(this.viewport, this); this.sceneGraph = new SceneGraphArray(this.viewport, this);
this.physicsManager = new PhysicsManager(); this.physicsManager = new PhysicsManager();
// Factories for this scene
this.add = new FactoryManager(this, this.sceneGraph, this.physicsManager, this.tilemaps); this.add = new FactoryManager(this, this.sceneGraph, this.physicsManager, this.tilemaps);
this.load = ResourceManager.getInstance(); 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 {} loadScene(): void {}
/**
* A function that gets called on scene destruction. Specify which files you no longer need for garbage collection.
*/
unloadScene(): void {} unloadScene(): void {}
/**
* Called strictly after loadScene() is called. Create any game objects you wish to use in the scene here.
*/
startScene(): void {} 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 {} updateScene(delta: number): void {}
/**
* Updates all scene elements
* @param deltaT
*/
update(deltaT: number): void { update(deltaT: number): void {
this.updateScene(deltaT); this.updateScene(deltaT);
@ -72,6 +102,10 @@ export default class Scene{
this.viewport.update(deltaT); this.viewport.update(deltaT);
} }
/**
* Render all CanvasNodes and Tilemaps in the Scene
* @param ctx
*/
render(ctx: CanvasRenderingContext2D): void { render(ctx: CanvasRenderingContext2D): void {
// For webGL, pass a visible set to the renderer // For webGL, pass a visible set to the renderer
// We need to keep track of the order of things. // We need to keep track of the order of things.
@ -94,12 +128,18 @@ export default class Scene{
return this.running; return this.running;
} }
/**
* Adds a new layer to the scene and returns it
*/
addLayer(): Layer { addLayer(): Layer {
let layer = new Layer(this); let layer = new Layer(this);
this.layers.push(layer); this.layers.push(layer);
return layer; return layer;
} }
/**
* Returns the viewport associated with this scene
*/
getViewport(): Viewport { getViewport(): Viewport {
return this.viewport; return this.viewport;
} }

View File

@ -3,7 +3,7 @@ import ResourceManager from "../ResourceManager/ResourceManager";
import Viewport from "../SceneGraph/Viewport"; import Viewport from "../SceneGraph/Viewport";
import GameLoop from "../Loop/GameLoop"; import GameLoop from "../Loop/GameLoop";
export default class SceneManager{ export default class SceneManager {
private currentScene: Scene; private currentScene: Scene;
private viewport: Viewport; private viewport: Viewport;
@ -16,6 +16,10 @@ export default class SceneManager{
this.game = game; this.game = game;
} }
/**
* Add a scene as the main scene
* @param constr The constructor of the scene to add
*/
public addScene<T extends Scene>(constr: new (...args: any) => T): void { public addScene<T extends Scene>(constr: new (...args: any) => T): void {
let scene = new constr(this.viewport, this, this.game); let scene = new constr(this.viewport, this, this.game);
this.currentScene = scene; 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<T extends Scene>(constr: new (...args: any) => T): void { public changeScene<T extends Scene>(constr: new (...args: any) => T): void {
// unload current scene // unload current scene
this.currentScene.unloadScene(); this.currentScene.unloadScene();

View File

@ -4,7 +4,10 @@ import Map from "../DataTypes/Map";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
import Scene from "../Scene/Scene"; 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 viewport: Viewport;
protected nodeMap: Map<CanvasNode>; protected nodeMap: Map<CanvasNode>;
protected idCounter: number; protected idCounter: number;
@ -17,6 +20,10 @@ export default abstract class SceneGraph{
this.idCounter = 0; this.idCounter = 0;
} }
/**
* Add a node to the SceneGraph
* @param node The CanvasNode to add to the SceneGraph
*/
addNode(node: CanvasNode): number { addNode(node: CanvasNode): number {
this.nodeMap.add(this.idCounter.toString(), node); this.nodeMap.add(this.idCounter.toString(), node);
this.addNodeSpecific(node, this.idCounter.toString()); this.addNodeSpecific(node, this.idCounter.toString());
@ -24,8 +31,17 @@ export default abstract class SceneGraph{
return this.idCounter - 1; 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; protected abstract addNodeSpecific(node: CanvasNode, id: string): void;
/**
* Removes a node from the SceneGraph
* @param node The node to remove
*/
removeNode(node: CanvasNode): void { removeNode(node: CanvasNode): void {
// Find and remove node in O(n) // Find and remove node in O(n)
// TODO: Can this be better? // 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; 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); return this.nodeMap.get(id);
}; }
/**
* Returns the node at specific coordinates
* @param vecOrX
* @param y
*/
getNodeAt(vecOrX: Vec2 | number, y: number = null): CanvasNode { getNodeAt(vecOrX: Vec2 | number, y: number = null): CanvasNode {
if(vecOrX instanceof Vec2){ if(vecOrX instanceof Vec2){
return this.getNodeAtCoords(vecOrX.x, vecOrX.y); return this.getNodeAtCoords(vecOrX.x, vecOrX.y);
@ -49,10 +79,18 @@ export default abstract class SceneGraph{
return this.getNodeAtCoords(vecOrX, y); 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; protected abstract getNodeAtCoords(x: number, y: number): CanvasNode;
abstract update(deltaT: number): void; abstract update(deltaT: number): void;
/**
* Gets the visible set of CanvasNodes based on the viewport
*/
abstract getVisibleSet(): Array<CanvasNode>; abstract getVisibleSet(): Array<CanvasNode>;
} }

View File

@ -4,7 +4,7 @@ import GameNode from "../Nodes/GameNode";
import CanvasNode from "../Nodes/CanvasNode"; import CanvasNode from "../Nodes/CanvasNode";
import MathUtils from "../Utils/MathUtils"; import MathUtils from "../Utils/MathUtils";
export default class Viewport{ export default class Viewport {
private position: Vec2; private position: Vec2;
private size: Vec2; private size: Vec2;
private bounds: Vec4; private bounds: Vec4;
@ -16,10 +16,18 @@ export default class Viewport{
this.bounds = new Vec4(0, 0, 0, 0); this.bounds = new Vec4(0, 0, 0, 0);
} }
/**
* Returns the position of the viewport as a Vec2
*/
getPosition(): Vec2 { getPosition(): Vec2 {
return this.position; return this.position;
} }
/**
* Set the position of the viewport
* @param vecOrX
* @param y
*/
setPosition(vecOrX: Vec2 | number, y: number = null): void { setPosition(vecOrX: Vec2 | number, y: number = null): void {
if(vecOrX instanceof Vec2){ if(vecOrX instanceof Vec2){
this.position.set(vecOrX.x, vecOrX.y); 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{ getSize(): Vec2{
return this.size; return this.size;
} }
/**
* Sets the size of the viewport
* @param vecOrX
* @param y
*/
setSize(vecOrX: Vec2 | number, y: number = null): void { setSize(vecOrX: Vec2 | number, y: number = null): void {
if(vecOrX instanceof Vec2){ if(vecOrX instanceof Vec2){
this.size.set(vecOrX.x, vecOrX.y); 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 { includes(node: CanvasNode): boolean {
let nodePos = node.getPosition(); let nodePos = node.getPosition();
let nodeSize = node.getSize(); 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: 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 { setBounds(lowerX: number, lowerY: number, upperX: number, upperY: number): void {
this.bounds = new Vec4(lowerX, lowerY, upperX, upperY); 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 { follow(node: GameNode): void {
this.following = node; this.following = node;
} }
update(deltaT: number): void { update(deltaT: number): void {
// If viewport is following an object
if(this.following){ 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.x = this.following.getPosition().x - this.size.x/2;
this.position.y = this.following.getPosition().y - this.size.y/2; this.position.y = this.following.getPosition().y - this.size.y/2;
let [min, max] = this.bounds.split(); let [min, max] = this.bounds.split();

View File

@ -8,7 +8,11 @@ export default class Audio {
this.key = key; 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); this.sound = AudioManager.getInstance().createSound(this.key);
if(loop){ if(loop){
@ -18,7 +22,10 @@ export default class Audio {
this.sound.start(); this.sound.start();
} }
stop(){ /**
* Stop the sound this audio represents
*/
stop(): void {
if(this.sound){ if(this.sound){
this.sound.stop(); this.sound.stop();
} }

View File

@ -9,6 +9,9 @@ export default class AudioManager {
this.initAudio(); this.initAudio();
} }
/**
* Get the instance of the AudioManager class or create a new one if none exists
*/
public static getInstance(): AudioManager { public static getInstance(): AudioManager {
if(!this.instance){ if(!this.instance){
this.instance = new AudioManager(); this.instance = new AudioManager();
@ -16,7 +19,10 @@ export default class AudioManager {
return this.instance; return this.instance;
} }
private initAudio(): void { /**
* Initializes the webAudio context
*/
private initAudio(): void {
try { try {
window.AudioContext = window.AudioContext;// || window.webkitAudioContext; window.AudioContext = window.AudioContext;// || window.webkitAudioContext;
this.audioCtx = new AudioContext(); this.audioCtx = new AudioContext();
@ -24,24 +30,30 @@ export default class AudioManager {
} catch(e) { } catch(e) {
console.log('Web Audio API is not supported in this browser'); console.log('Web Audio API is not supported in this browser');
} }
} }
/**
* Returns the current audio context
*/
public getAudioContext(): AudioContext { public getAudioContext(): AudioContext {
return this.audioCtx; 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 { createSound(key: string): AudioBufferSourceNode {
// Get audio buffer // Get audio buffer
let buffer = ResourceManager.getInstance().getAudio(key); let buffer = ResourceManager.getInstance().getAudio(key);
// creates a sound source // Create a sound source
var source = this.audioCtx.createBufferSource(); var source = this.audioCtx.createBufferSource();
// tell the source which sound to play // Tell the source which sound to play
source.buffer = buffer; source.buffer = buffer;
// connect the source to the context's destination // Connect the source to the context's destination
// i.e. the speakers
source.connect(this.audioCtx.destination); source.connect(this.audioCtx.destination);
return source; return source;

View File

@ -1,7 +1,7 @@
import MathUtils from "./MathUtils"; import MathUtils from "./MathUtils";
// TODO: This should be moved to the datatypes folder // TODO: This should be moved to the datatypes folder
export default class Color{ export default class Color {
public r: number; public r: number;
public g: number; public g: number;
public b: number; public b: number;
@ -14,22 +14,37 @@ export default class Color{
this.a = a; this.a = a;
} }
/**
* Returns a new color slightly lighter than the current color
*/
lighten(): 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); 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 { 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); 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 { toString(): string {
return "#" + MathUtils.toHex(this.r, 2) + MathUtils.toHex(this.g, 2) + MathUtils.toHex(this.b, 2); 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 { toStringRGB(): string {
return "rgb(" + this.r.toString() + ", " + this.g.toString() + ", " + this.b.toString() + ")"; 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 { toStringRGBA(): string {
if(this.a === null){ if(this.a === null){
return this.toStringRGB(); return this.toStringRGB();

View File

@ -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 { static clamp(x: number, min: number, max: number): number {
if(x < min) return min; if(x < min) return min;
if(x > max) return max; if(x > max) return max;
return x; 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 { static toHex(num: number, minLength: number = null): string {
let factor = 1; let factor = 1;
while(factor*16 < num){ while(factor*16 < num){
@ -27,6 +38,10 @@ export default class MathUtils{
return hexStr; return hexStr;
} }
/**
* Converts the number to hexadecimal
* @param num The number to convert to hexadecimal
*/
static toHexDigit(num: number): string { static toHexDigit(num: number): string {
if(num < 10){ if(num < 10){
return "" + num; return "" + num;

View File

@ -1,15 +1,28 @@
import MathUtils from "./MathUtils"; import MathUtils from "./MathUtils";
import Color from "./Color"; 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 { static randInt(min: number, max: number): number {
return Math.floor(Math.random()*(max - min) + min); 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 { static randHex(min: number, max: number): string {
return MathUtils.toHex(RandUtils.randInt(min, max)); return MathUtils.toHex(RandUtils.randInt(min, max));
} }
/**
* Generates a random color
*/
static randColor(): Color { static randColor(): Color {
let r = RandUtils.randInt(0, 256); let r = RandUtils.randInt(0, 256);
let g = RandUtils.randInt(0, 256); let g = RandUtils.randInt(0, 256);

View File

@ -1,4 +1,8 @@
export default class StringUtils { 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 { static getPathFromFilePath(filePath: string): string {
let splitPath = filePath.split("/"); let splitPath = filePath.split("/");
splitPath.pop(); splitPath.pop();