commented code and cleaned formatting
This commit is contained in:
parent
8cb4fb7972
commit
3a57a1acab
|
@ -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;
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)];
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)){
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
import Layer from "../Layer";
|
||||
|
||||
export default class ObjectLayer extends Layer {}
|
|
@ -1,6 +0,0 @@
|
|||
import Layer from "../Layer";
|
||||
import Tilemap from "../../Nodes/Tilemap";
|
||||
|
||||
export default class TiledLayer extends Layer {
|
||||
private tilemap: Tilemap;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
@ -50,9 +80,17 @@ export default abstract class SceneGraph{
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,6 +19,9 @@ export default class AudioManager {
|
|||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the webAudio context
|
||||
*/
|
||||
private initAudio(): void {
|
||||
try {
|
||||
window.AudioContext = window.AudioContext;// || window.webkitAudioContext;
|
||||
|
@ -26,22 +32,28 @@ export default class AudioManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue
Block a user