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 {
/**
* 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;
}

View File

@ -1,5 +1,8 @@
import Collection from "./Collection";
/**
* Associates strings with elements of type T
*/
export default class Map<T> implements Collection {
private map: Record<string, T>;
@ -7,31 +10,52 @@ export default class Map<T> 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<string> {
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]);
}
}

View File

@ -1,6 +1,9 @@
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 q: Array<T>;
private head: number;
@ -15,6 +18,10 @@ export default class Queue<T> 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<T> 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<T> 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<T> 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;
}
}

View File

@ -1,6 +1,9 @@
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;
private stack: Array<T>;
private head: number;
@ -13,7 +16,7 @@ export default class Stack<T> 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<T> 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<T> 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]);

View File

@ -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<TiledTilesetData>;
}
/**
* 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;

View File

@ -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);

View File

@ -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);
}

View File

@ -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)];
}

View File

@ -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<string> = new Map();
static log(id: string, message: string): void {

View File

@ -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<string>): 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);

View File

@ -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<any>;
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) {
this.data = new Map<any>();
} else if (!(data instanceof Map)){

View File

@ -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<GameEvent>;
@ -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();
}

View File

@ -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;

View File

@ -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));
}

View File

@ -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);

View File

@ -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){

View File

@ -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<any> | Record<string, any> = 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());
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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<Tileset>;
protected worldSize: Vec2;
protected tileSize: Vec2;
@ -21,6 +22,8 @@ export default abstract class Tilemap extends GameNode {
this.tilesets = new Array<Tileset>();
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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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){

View File

@ -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<TileCollisionData>();
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;

View File

@ -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);

View File

@ -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<HTMLImageElement>;
/**
* 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<TiledTilemapData>;
/**
* 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<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 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 = <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;
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;

View File

@ -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;
}

View File

@ -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 = <T extends UIElement>(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 = <T extends Graphic>(constr: new (...a: any) => T, layer: Layer, ...args: any): T => {
let instance = new constr(...args);

View File

@ -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;

View File

@ -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 = <T extends PhysicsNode>(constr: new (...a: any) => T, layer: Layer, ...args: any): T => {
let instance = new constr(...args);
instance.setScene(this.scene);

View File

@ -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 = <T extends Tilemap>(key: string, constr: new (...a: any) => T, ...args: any): Array<Tilemap> => {
// Get Tilemap Data
let tilemapData = this.resourceManager.getTilemap(key);

View File

@ -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 {}
}

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 tilemaps: Array<Tilemap>;
/**
* 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;
}

View File

@ -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<T extends Scene>(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<T extends Scene>(constr: new (...args: any) => T): void {
// unload current scene
this.currentScene.unloadScene();

View File

@ -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<CanvasNode>;
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<CanvasNode>;
}

View File

@ -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();

View File

@ -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();
}

View File

@ -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;

View File

@ -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();

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 {
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;

View File

@ -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);

View File

@ -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();