init repo
17
hw1/README.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# CSE380_HW1
|
||||
---
|
||||
## GAME concentration_multi(temp)
|
||||
### prepare state
|
||||
all lights are off
|
||||
single click anyone to set the number of players
|
||||
double click to start the game
|
||||
### play state
|
||||
at each player's tern, single click to select a single blink
|
||||
nothing happened -- earn 1 pt
|
||||
turn white for a second -- earn no pt
|
||||
### end state
|
||||
all lights turn white to validate
|
||||
then all blink will turn to the color of the player who chose that blink
|
||||
double click to reset to prepare state
|
||||
## GAME 2
|
||||
## GAME 3
|
224
hw1/concentration_multi.cpp
Normal file
|
@ -0,0 +1,224 @@
|
|||
enum signalStates { PREPARE, INERT, GO, RESOLVE, CHECK, END };
|
||||
byte signalState = PREPARE;
|
||||
enum player {PLAYER1, PLAYER2, PLAYER3, PLAYER4, PLAYER5, PLAYER6};//these modes will simply be different colors
|
||||
byte player = PLAYER1;//the default mode when the game begins
|
||||
byte player_num = 0;
|
||||
byte chosen_player = 6;
|
||||
Timer warn_timer;
|
||||
Timer check_timer;
|
||||
void loop() {
|
||||
switch (signalState) {
|
||||
case PREPARE:
|
||||
prepareLoop();
|
||||
break;
|
||||
case INERT:
|
||||
case GO:
|
||||
case RESOLVE:
|
||||
playLoop();
|
||||
break;
|
||||
case CHECK:
|
||||
checkLoop();
|
||||
break;
|
||||
case END:
|
||||
endLoop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void prepareLoop() {
|
||||
FOREACH_FACE(f) {
|
||||
if (getSignalState(getLastValueReceivedOnFace(f)) == INERT) {
|
||||
signalState = INERT;
|
||||
player = getPlayer(getLastValueReceivedOnFace(f));
|
||||
player_num = player + 1;
|
||||
byte sendData = (signalState << 3) + (player);
|
||||
setValueSentOnAllFaces(sendData);
|
||||
}
|
||||
else {
|
||||
if (player_num == 0) {
|
||||
setColor(OFF);
|
||||
}
|
||||
else {
|
||||
for ( byte i = 0; i < player_num; i ++) {
|
||||
setColorOnFace(getColor(i+1), i);
|
||||
}
|
||||
}
|
||||
if (buttonSingleClicked()) {
|
||||
player_num = (player_num + 1) % 7;
|
||||
}
|
||||
if (buttonDoubleClicked() && player_num != 0) {
|
||||
player = player_num - 1;
|
||||
signalState = INERT;
|
||||
byte sendData = (signalState << 3) + (player);
|
||||
setValueSentOnAllFaces(sendData);
|
||||
}
|
||||
else {
|
||||
setValueSentOnAllFaces(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void playLoop() {
|
||||
switch (signalState) {
|
||||
case INERT:
|
||||
inertLoop();
|
||||
break;
|
||||
case GO:
|
||||
goLoop();
|
||||
break;
|
||||
case RESOLVE:
|
||||
resolveLoop();
|
||||
break;
|
||||
}
|
||||
displaySignalState();
|
||||
byte sendData = (signalState << 3) + (player);
|
||||
setValueSentOnAllFaces(sendData);
|
||||
}
|
||||
|
||||
void inertLoop() {
|
||||
//set myself to GO
|
||||
if (buttonSingleClicked()) {
|
||||
if (chosen_player == 6) {
|
||||
chosen_player = player;
|
||||
}
|
||||
else {
|
||||
warn_timer.set(1000);
|
||||
}
|
||||
signalState = GO;
|
||||
player = (player + 1) % (player_num);//adds one to game mode, but 3+1 becomes 0
|
||||
}
|
||||
//listen for neighbors in GO
|
||||
FOREACH_FACE(f) {
|
||||
if (!isValueReceivedOnFaceExpired(f)) { //a neighbor!
|
||||
if (getSignalState(getLastValueReceivedOnFace(f)) == GO) { //a neighbor saying GO!
|
||||
signalState = GO;
|
||||
player = getPlayer(getLastValueReceivedOnFace(f));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
void goLoop() {
|
||||
signalState = RESOLVE; //I default to this at the start of the loop. Only if I see a problem does this not happen
|
||||
//look for neighbors who have not heard the GO news
|
||||
FOREACH_FACE(f) {
|
||||
if (!isValueReceivedOnFaceExpired(f)) { //a neighbor!
|
||||
if (getSignalState(getLastValueReceivedOnFace(f)) == INERT) {//This neighbor doesn't know it's GO time. Stay in GO
|
||||
signalState = GO;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void resolveLoop() {
|
||||
if (chosen_player == 6) {
|
||||
signalState = INERT; //I default to this at the start of the loop. Only if I see a problem does this not happen
|
||||
}
|
||||
else {
|
||||
check_timer.set(2000);
|
||||
signalState = CHECK;
|
||||
}
|
||||
//look for neighbors who have not moved to RESOLVE
|
||||
FOREACH_FACE(f) {
|
||||
if (!isValueReceivedOnFaceExpired(f)) { //a neighbor!
|
||||
if (getSignalState(getLastValueReceivedOnFace(f)) == GO) {//This neighbor isn't in RESOLVE. Stay in RESOLVE
|
||||
signalState = RESOLVE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void checkLoop() {
|
||||
if (check_timer.isExpired()) {
|
||||
setColor(OFF);
|
||||
signalState = END;
|
||||
}
|
||||
else {
|
||||
setColor(WHITE);
|
||||
FOREACH_FACE(f) {
|
||||
if (!isValueReceivedOnFaceExpired(f)) { //a neighbor!
|
||||
if (getSignalState(getLastValueReceivedOnFace(f)) == INERT) {//This neighbor isn't in RESOLVE. Stay in RESOLVE
|
||||
signalState = INERT;
|
||||
}
|
||||
}
|
||||
}
|
||||
setValueSentOnAllFaces(6 << 3);
|
||||
}
|
||||
}
|
||||
|
||||
void endLoop() {
|
||||
setColor(getColor(getRealPlayer(chosen_player)))
|
||||
FOREACH_FACE(f) {
|
||||
if (!isValueReceivedOnFaceExpired(f)) { //a neighbor!
|
||||
if (getSignalState(getLastValueReceivedOnFace(f)) == PREPARE) {//This neighbor isn't in RESOLVE. Stay in RESOLVE
|
||||
signalState = PREPARE;
|
||||
player = PLAYER1;
|
||||
player_num = 0;
|
||||
chosen_player = 6;
|
||||
setColor(OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (buttonDoubleClicked()) {
|
||||
signalState = PREPARE;
|
||||
player = PLAYER1;
|
||||
player_num = 0;
|
||||
chosen_player = 6;
|
||||
setValueSentOnAllFaces(0);
|
||||
}
|
||||
}
|
||||
|
||||
void displaySignalState() {
|
||||
if (warn_timer.isExpired()) {
|
||||
switch (signalState) {
|
||||
case INERT:
|
||||
setColor(getColor(getRealPlayer(player)));
|
||||
break;
|
||||
case GO:
|
||||
case RESOLVE:
|
||||
setColor(WHITE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
setColor(WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
Color getColor(data) {
|
||||
switch (data) {
|
||||
case 0:
|
||||
return (makeColorRGB(0,0,0));
|
||||
break;
|
||||
case 1:
|
||||
return (makeColorRGB(255,0,0));
|
||||
break;
|
||||
case 2:
|
||||
return (makeColorRGB(0,255,0));
|
||||
break;
|
||||
case 3:
|
||||
return (makeColorRGB(0,0,255));
|
||||
break;
|
||||
case 4:
|
||||
return (makeColorRGB(255,0,255));
|
||||
break;
|
||||
case 5:
|
||||
return (makeColorRGB(255,255,0));
|
||||
break;
|
||||
case 6:
|
||||
return (makeColorRGB(0,255,255));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
byte getRealPlayer(byte data) {
|
||||
return (data + 1) % player_num + 1;
|
||||
}
|
||||
|
||||
byte getPlayer(byte data) {
|
||||
return (data & 7);//returns bits E and F
|
||||
}
|
||||
|
||||
byte getSignalState(byte data) {
|
||||
return ((data >> 3) & 7);//returns bits C and D
|
||||
}
|
18
hw3/.gitignore
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Exclude node modules
|
||||
node_modules
|
||||
|
||||
# Exclude the compiled project
|
||||
dist/*
|
||||
|
||||
# Include the demo_assets folder
|
||||
!dist/demo_assets/
|
||||
|
||||
# Include the built-in asset folder
|
||||
!dist/builtin/
|
||||
|
||||
# Include the hw1 assets
|
||||
!dist/hw3_assets/
|
||||
|
||||
### IF YOU ARE MAKING A PROJECT, YOU MAY WANT TO UNCOMMENT THIS LINE ###
|
||||
# !dist/assets/
|
||||
|
77
hw3/dist/builtin/shaders/label.fshader
vendored
Normal file
|
@ -0,0 +1,77 @@
|
|||
precision mediump float;
|
||||
|
||||
uniform vec4 u_BackgroundColor;
|
||||
uniform vec4 u_BorderColor;
|
||||
uniform float u_BorderWidth;
|
||||
uniform float u_BorderRadius;
|
||||
uniform vec2 u_MaxSize;
|
||||
|
||||
varying vec4 v_Position;
|
||||
|
||||
void main(){
|
||||
vec2 adj_MaxSize = u_MaxSize - u_BorderWidth;
|
||||
vec2 rad_MaxSize = u_MaxSize - u_BorderRadius;
|
||||
vec2 rad2_MaxSize = u_MaxSize - 2.0*u_BorderRadius;
|
||||
|
||||
bool inX = (v_Position.x < adj_MaxSize.x) && (v_Position.x > -adj_MaxSize.x);
|
||||
bool inY = (v_Position.y < adj_MaxSize.y) && (v_Position.y > -adj_MaxSize.y);
|
||||
|
||||
bool inRadiusRangeX = (v_Position.x < rad_MaxSize.x) && (v_Position.x > -rad_MaxSize.x);
|
||||
bool inRadiusRangeY = (v_Position.y < rad_MaxSize.y) && (v_Position.y > -rad_MaxSize.y);
|
||||
|
||||
bool inRadius2RangeX = (v_Position.x < rad2_MaxSize.x) && (v_Position.x > -rad2_MaxSize.x);
|
||||
bool inRadius2RangeY = (v_Position.y < rad2_MaxSize.y) && (v_Position.y > -rad2_MaxSize.y);
|
||||
|
||||
if(inX && inY){
|
||||
// Inside bounds, draw background color
|
||||
gl_FragColor = u_BackgroundColor;
|
||||
} else {
|
||||
// In boundary, draw border color
|
||||
gl_FragColor = u_BorderColor;
|
||||
}
|
||||
|
||||
// This isn't working well right now
|
||||
/*
|
||||
if(inRadius2RangeX || inRadius2RangeY){
|
||||
// Draw normally
|
||||
if(inX && inY){
|
||||
// Inside bounds, draw background color
|
||||
gl_FragColor = u_BackgroundColor;
|
||||
} else {
|
||||
// In boundary, draw border color
|
||||
gl_FragColor = u_BorderColor;
|
||||
}
|
||||
} else if(inRadiusRangeX || inRadiusRangeY){
|
||||
// Draw a rounded boundary for the inner part
|
||||
float x = v_Position.x - sign(v_Position.x)*rad2_MaxSize.x;
|
||||
float y = v_Position.y - sign(v_Position.y)*rad2_MaxSize.y;
|
||||
|
||||
float radSq = x*x + y*y;
|
||||
float bRadSq = u_BorderRadius*u_BorderRadius;
|
||||
|
||||
if(radSq > bRadSq){
|
||||
// Outside of radius - draw as transparent
|
||||
gl_FragColor = u_BorderColor;
|
||||
} else {
|
||||
gl_FragColor = u_BackgroundColor;
|
||||
}
|
||||
} else {
|
||||
// Both coordinates are in the circular section
|
||||
float x = v_Position.x - sign(v_Position.x)*rad_MaxSize.x;
|
||||
float y = v_Position.y - sign(v_Position.y)*rad_MaxSize.y;
|
||||
|
||||
float radSq = x*x + y*y;
|
||||
float bRadSq = u_BorderRadius*u_BorderRadius;
|
||||
|
||||
if(radSq > bRadSq){
|
||||
// Outside of radius - draw as transparent
|
||||
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
|
||||
} else if(sqrt(bRadSq) - sqrt(radSq) < u_BorderWidth) {
|
||||
// In border
|
||||
gl_FragColor = u_BorderColor;
|
||||
} else {
|
||||
gl_FragColor = u_BackgroundColor;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
12
hw3/dist/builtin/shaders/label.vshader
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
attribute vec4 a_Position;
|
||||
|
||||
uniform mat4 u_Transform;
|
||||
|
||||
varying vec4 v_Position;
|
||||
|
||||
void main(){
|
||||
gl_Position = u_Transform * a_Position;
|
||||
|
||||
// Pass position to the fragment shader
|
||||
v_Position = a_Position;
|
||||
}
|
7
hw3/dist/builtin/shaders/point.fshader
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
precision mediump float;
|
||||
|
||||
uniform vec4 u_Color;
|
||||
|
||||
void main(){
|
||||
gl_FragColor = u_Color;
|
||||
}
|
8
hw3/dist/builtin/shaders/point.vshader
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
attribute vec4 a_Position;
|
||||
|
||||
uniform float u_PointSize;
|
||||
|
||||
void main(){
|
||||
gl_Position = a_Position;
|
||||
gl_PointSize = u_PointSize;
|
||||
}
|
7
hw3/dist/builtin/shaders/rect.fshader
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
precision mediump float;
|
||||
|
||||
uniform vec4 u_Color;
|
||||
|
||||
void main(){
|
||||
gl_FragColor = u_Color;
|
||||
}
|
7
hw3/dist/builtin/shaders/rect.vshader
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
attribute vec4 a_Position;
|
||||
|
||||
uniform mat4 u_Transform;
|
||||
|
||||
void main(){
|
||||
gl_Position = u_Transform * a_Position;
|
||||
}
|
9
hw3/dist/builtin/shaders/sprite.fshader
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
precision mediump float;
|
||||
|
||||
uniform sampler2D u_Sampler;
|
||||
|
||||
varying vec2 v_TexCoord;
|
||||
|
||||
void main(){
|
||||
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
|
||||
}
|
13
hw3/dist/builtin/shaders/sprite.vshader
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
attribute vec4 a_Position;
|
||||
attribute vec2 a_TexCoord;
|
||||
|
||||
uniform mat4 u_Transform;
|
||||
uniform vec2 u_texShift;
|
||||
uniform vec2 u_texScale;
|
||||
|
||||
varying vec2 v_TexCoord;
|
||||
|
||||
void main(){
|
||||
gl_Position = u_Transform * a_Position;
|
||||
v_TexCoord = a_TexCoord*u_texScale + u_texShift;
|
||||
}
|
BIN
hw3/dist/demo_assets/images/platformer_background.png
vendored
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
hw3/dist/demo_assets/images/wolfie2d_text.png
vendored
Normal file
After Width: | Height: | Size: 701 B |
BIN
hw3/dist/demo_assets/sounds/jump.wav
vendored
Normal file
BIN
hw3/dist/demo_assets/sounds/title.mp3
vendored
Normal file
27
hw3/dist/demo_assets/spritesheets/platformer/player.json
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "PlatformerPlayer",
|
||||
"spriteSheetImage": "player.png",
|
||||
"spriteWidth": 16,
|
||||
"spriteHeight": 16,
|
||||
"columns": 5,
|
||||
"rows": 1,
|
||||
"durationType": "time",
|
||||
"animations": [
|
||||
{
|
||||
"name": "IDLE",
|
||||
"frames": [ {"index": 0, "duration": 1} ]
|
||||
},
|
||||
{
|
||||
"name": "WALK",
|
||||
"frames": [ {"index": 0, "duration": 16}, {"index": 1, "duration": 16}, {"index": 2, "duration": 16}, {"index": 3, "duration": 16} ]
|
||||
},
|
||||
{
|
||||
"name": "JUMP",
|
||||
"frames":[ {"index": 4, "duration": 32}]
|
||||
},
|
||||
{
|
||||
"name": "FALL",
|
||||
"frames":[ {"index": 4, "duration": 32}]
|
||||
}
|
||||
]
|
||||
}
|
BIN
hw3/dist/demo_assets/spritesheets/platformer/player.png
vendored
Normal file
After Width: | Height: | Size: 268 B |
453
hw3/dist/demo_assets/tilemaps/platformer/platformer.json
vendored
Normal file
|
@ -0,0 +1,453 @@
|
|||
{ "compressionlevel":-1,
|
||||
"editorsettings":
|
||||
{
|
||||
"export":
|
||||
{
|
||||
"format":"json",
|
||||
"target":"platformer.json"
|
||||
}
|
||||
},
|
||||
"height":20,
|
||||
"infinite":false,
|
||||
"layers":[
|
||||
{
|
||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 44, 44, 44, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 0, 44, 44, 44, 44, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 44, 45, 0, 43, 0, 44, 44, 44, 44, 44, 44, 0, 44, 44, 44, 44, 44, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 36, 0, 36, 36, 36, 36, 36, 36, 36, 36, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 44, 45, 0, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 54, 55, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 44, 45, 0, 43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 44, 62, 63, 44, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 43, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"height":20,
|
||||
"id":2,
|
||||
"name":"Background",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"Collidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"Depth",
|
||||
"type":"int",
|
||||
"value":0
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":64,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 20, 20, 20, 8, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 11, 12, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 10, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 12, 12, 9, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 16, 12, 12, 12, 12, 10, 12, 12, 12, 12, 12, 12, 9, 12, 12, 12, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 7, 20, 20, 20, 20, 20, 20, 8, 12, 12, 12, 12, 10, 12, 12, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 9, 16, 12, 12, 13, 0, 0, 0, 0, 0, 0, 11, 12, 12, 12, 12, 12, 12, 9, 12, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 19, 20, 20, 20, 20, 13, 0, 0, 0, 0, 0, 0, 11, 20, 20, 20, 20, 20, 20, 20, 8, 12, 9, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 4, 4, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 12, 12, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 2, 2, 2, 2, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 10, 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 0, 0, 0, 0, 0, 0, 11, 12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 2, 2, 2, 2, 13, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 12, 13, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 11, 10, 12, 12, 12, 12, 12, 12, 10, 15, 4, 4, 4, 4, 4, 4, 4, 4],
|
||||
"height":20,
|
||||
"id":1,
|
||||
"name":"Main",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"Collidable",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
},
|
||||
{
|
||||
"name":"Depth",
|
||||
"type":"int",
|
||||
"value":1
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":64,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"draworder":"topdown",
|
||||
"id":4,
|
||||
"name":"Coins",
|
||||
"objects":[
|
||||
{
|
||||
"gid":25,
|
||||
"height":16,
|
||||
"id":2,
|
||||
"name":"",
|
||||
"properties":[
|
||||
{
|
||||
"name":"Group",
|
||||
"type":"string",
|
||||
"value":"Coins"
|
||||
},
|
||||
{
|
||||
"name":"HasPhysics",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
},
|
||||
{
|
||||
"name":"IsCollidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"IsTrigger",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":256,
|
||||
"y":272
|
||||
},
|
||||
{
|
||||
"gid":25,
|
||||
"height":16,
|
||||
"id":3,
|
||||
"name":"",
|
||||
"properties":[
|
||||
{
|
||||
"name":"Group",
|
||||
"type":"string",
|
||||
"value":"Coins"
|
||||
},
|
||||
{
|
||||
"name":"HasPhysics",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
},
|
||||
{
|
||||
"name":"IsCollidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"IsTrigger",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":272,
|
||||
"y":272
|
||||
},
|
||||
{
|
||||
"gid":25,
|
||||
"height":16,
|
||||
"id":4,
|
||||
"name":"",
|
||||
"properties":[
|
||||
{
|
||||
"name":"Group",
|
||||
"type":"string",
|
||||
"value":"Coins"
|
||||
},
|
||||
{
|
||||
"name":"HasPhysics",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
},
|
||||
{
|
||||
"name":"IsCollidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"IsTrigger",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":368,
|
||||
"y":288
|
||||
},
|
||||
{
|
||||
"gid":25,
|
||||
"height":16,
|
||||
"id":5,
|
||||
"name":"",
|
||||
"properties":[
|
||||
{
|
||||
"name":"Group",
|
||||
"type":"string",
|
||||
"value":"Coins"
|
||||
},
|
||||
{
|
||||
"name":"HasPhysics",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
},
|
||||
{
|
||||
"name":"IsCollidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"IsTrigger",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":384,
|
||||
"y":288
|
||||
},
|
||||
{
|
||||
"gid":25,
|
||||
"height":16,
|
||||
"id":6,
|
||||
"name":"",
|
||||
"properties":[
|
||||
{
|
||||
"name":"Group",
|
||||
"type":"string",
|
||||
"value":"Coins"
|
||||
},
|
||||
{
|
||||
"name":"HasPhysics",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
},
|
||||
{
|
||||
"name":"IsCollidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"IsTrigger",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":400,
|
||||
"y":288
|
||||
},
|
||||
{
|
||||
"gid":25,
|
||||
"height":16,
|
||||
"id":7,
|
||||
"name":"",
|
||||
"properties":[
|
||||
{
|
||||
"name":"Group",
|
||||
"type":"string",
|
||||
"value":"Coins"
|
||||
},
|
||||
{
|
||||
"name":"HasPhysics",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
},
|
||||
{
|
||||
"name":"IsCollidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"IsTrigger",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":688,
|
||||
"y":272
|
||||
},
|
||||
{
|
||||
"gid":25,
|
||||
"height":16,
|
||||
"id":8,
|
||||
"name":"",
|
||||
"properties":[
|
||||
{
|
||||
"name":"Group",
|
||||
"type":"string",
|
||||
"value":"Coins"
|
||||
},
|
||||
{
|
||||
"name":"HasPhysics",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
},
|
||||
{
|
||||
"name":"IsCollidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"IsTrigger",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":688,
|
||||
"y":288
|
||||
},
|
||||
{
|
||||
"gid":25,
|
||||
"height":16,
|
||||
"id":9,
|
||||
"name":"",
|
||||
"properties":[
|
||||
{
|
||||
"name":"Group",
|
||||
"type":"string",
|
||||
"value":"Coins"
|
||||
},
|
||||
{
|
||||
"name":"HasPhysics",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
},
|
||||
{
|
||||
"name":"IsCollidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"IsTrigger",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":688,
|
||||
"y":304
|
||||
},
|
||||
{
|
||||
"gid":25,
|
||||
"height":16,
|
||||
"id":10,
|
||||
"name":"",
|
||||
"properties":[
|
||||
{
|
||||
"name":"Group",
|
||||
"type":"string",
|
||||
"value":"Coins"
|
||||
},
|
||||
{
|
||||
"name":"HasPhysics",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
},
|
||||
{
|
||||
"name":"IsCollidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"IsTrigger",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":784,
|
||||
"y":256
|
||||
},
|
||||
{
|
||||
"gid":25,
|
||||
"height":16,
|
||||
"id":11,
|
||||
"name":"",
|
||||
"properties":[
|
||||
{
|
||||
"name":"Group",
|
||||
"type":"string",
|
||||
"value":"Coins"
|
||||
},
|
||||
{
|
||||
"name":"HasPhysics",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
},
|
||||
{
|
||||
"name":"IsCollidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"IsTrigger",
|
||||
"type":"bool",
|
||||
"value":true
|
||||
}],
|
||||
"rotation":0,
|
||||
"type":"",
|
||||
"visible":true,
|
||||
"width":16,
|
||||
"x":832,
|
||||
"y":256
|
||||
}],
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"Depth",
|
||||
"type":"int",
|
||||
"value":1
|
||||
}],
|
||||
"type":"objectgroup",
|
||||
"visible":true,
|
||||
"x":0,
|
||||
"y":0
|
||||
},
|
||||
{
|
||||
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"height":20,
|
||||
"id":3,
|
||||
"name":"Foreground",
|
||||
"opacity":1,
|
||||
"properties":[
|
||||
{
|
||||
"name":"Collidable",
|
||||
"type":"bool",
|
||||
"value":false
|
||||
},
|
||||
{
|
||||
"name":"Depth",
|
||||
"type":"int",
|
||||
"value":2
|
||||
}],
|
||||
"type":"tilelayer",
|
||||
"visible":true,
|
||||
"width":64,
|
||||
"x":0,
|
||||
"y":0
|
||||
}],
|
||||
"nextlayerid":5,
|
||||
"nextobjectid":14,
|
||||
"orientation":"orthogonal",
|
||||
"renderorder":"right-down",
|
||||
"tiledversion":"1.3.4",
|
||||
"tileheight":16,
|
||||
"tilesets":[
|
||||
{
|
||||
"columns":8,
|
||||
"firstgid":1,
|
||||
"image":"platformer.png",
|
||||
"imageheight":128,
|
||||
"imagewidth":128,
|
||||
"margin":0,
|
||||
"name":"platformer_tileset",
|
||||
"spacing":0,
|
||||
"tilecount":64,
|
||||
"tileheight":16,
|
||||
"tilewidth":16
|
||||
}],
|
||||
"tilewidth":16,
|
||||
"type":"map",
|
||||
"version":1.2,
|
||||
"width":64
|
||||
}
|
BIN
hw3/dist/demo_assets/tilemaps/platformer/platformer.png
vendored
Normal file
After Width: | Height: | Size: 1.7 KiB |
25
hw3/dist/hw3_assets/shaders/gradient_circle.fshader
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
precision mediump float;
|
||||
|
||||
varying vec4 v_Position;
|
||||
|
||||
uniform vec4 circle_Color;
|
||||
|
||||
void main(){
|
||||
// Default alpha is 0
|
||||
float alpha = 0.0;
|
||||
|
||||
// Radius is 0.5, since the diameter of our quad is 1
|
||||
float radius = 0.5;
|
||||
|
||||
// Get the distance squared of from (0, 0)
|
||||
float dist_sq = v_Position.x*v_Position.x + v_Position.y*v_Position.y;
|
||||
|
||||
if(dist_sq < radius*radius){
|
||||
// Multiply by 4, since distance squared is at most 0.25
|
||||
alpha = 4.0*dist_sq;
|
||||
}
|
||||
|
||||
// Use the alpha value in our color
|
||||
gl_FragColor = vec4(circle_Color);
|
||||
gl_FragColor.a = alpha;
|
||||
}
|
11
hw3/dist/hw3_assets/shaders/gradient_circle.vshader
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
attribute vec4 a_Position;
|
||||
|
||||
uniform mat4 u_Transform;
|
||||
|
||||
varying vec4 v_Position;
|
||||
|
||||
void main(){
|
||||
gl_Position = u_Transform * a_Position;
|
||||
|
||||
v_Position = a_Position;
|
||||
}
|
33
hw3/dist/hw3_assets/shaders/linear_gradient_circle.fshader
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
precision mediump float;
|
||||
|
||||
varying vec4 v_Position;
|
||||
|
||||
uniform vec4 circle_Color;
|
||||
uniform vec4 default_Color;
|
||||
|
||||
// HOMEWORK 3
|
||||
/*
|
||||
The fragment shader is where pixel colors are decided.
|
||||
You'll have to modify this code to make the circle vary between 2 colors.
|
||||
Currently this will render the exact same thing as the gradient_circle shaders
|
||||
*/
|
||||
void main(){
|
||||
// Default alpha is 0
|
||||
float alpha = 0.0;
|
||||
|
||||
// Radius is 0.5, since the diameter of our quad is 1
|
||||
float radius = 0.5;
|
||||
float ratio = (1.0-(v_Position.x+0.25+v_Position.y+0.25));
|
||||
|
||||
// Get the distance squared of from (0, 0)
|
||||
float dist_sq = v_Position.x*v_Position.x + v_Position.y*v_Position.y;
|
||||
|
||||
if(dist_sq < radius*radius){
|
||||
// Multiply by 4, since distance squared is at most 0.25
|
||||
alpha = 1.0;
|
||||
}
|
||||
|
||||
// Use the alpha value in our color
|
||||
gl_FragColor = vec4(default_Color[0]*ratio+circle_Color[0]*(1.0-ratio), default_Color[1]*ratio+circle_Color[1]*(1.0-ratio), default_Color[2]*ratio+circle_Color[2]*(1.0-ratio), default_Color[3]*ratio+circle_Color[3]*(1.0-ratio));
|
||||
gl_FragColor.a = alpha;
|
||||
}
|
11
hw3/dist/hw3_assets/shaders/linear_gradient_circle.vshader
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
attribute vec4 a_Position;
|
||||
|
||||
uniform mat4 u_Transform;
|
||||
|
||||
varying vec4 v_Position;
|
||||
|
||||
void main(){
|
||||
gl_Position = u_Transform * a_Position;
|
||||
|
||||
v_Position = a_Position;
|
||||
}
|
BIN
hw3/dist/hw3_assets/sprites/road.jpg
vendored
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
hw3/dist/hw3_assets/sprites/stone.png
vendored
Normal file
After Width: | Height: | Size: 4.6 KiB |
139
hw3/dist/hw3_assets/spritesheets/car.json
vendored
Normal file
|
@ -0,0 +1,139 @@
|
|||
{
|
||||
"name": "cars",
|
||||
"spriteSheetImage": "cars.png",
|
||||
"spriteWidth": 256,
|
||||
"spriteHeight": 256,
|
||||
"leftBuffer": 0,
|
||||
"rightBuffer": 0,
|
||||
"topBuffer": 0,
|
||||
"bottomBuffer": 0,
|
||||
"columns": 3,
|
||||
"rows": 4,
|
||||
"animations": [
|
||||
{
|
||||
"name": "driving",
|
||||
"repeat": true,
|
||||
"frames": [
|
||||
{
|
||||
"index": 0,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"duration": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "firing",
|
||||
"repeat": true,
|
||||
"frames": [
|
||||
{
|
||||
"index": 0,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"duration": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "damage",
|
||||
"repeat": true,
|
||||
"frames": [
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
}, {
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
}, {
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dying",
|
||||
"repeat": false,
|
||||
"next": "dead",
|
||||
"frames": [
|
||||
{
|
||||
"index": 6,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 7,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 8,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 9,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 10,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 11,
|
||||
"duration": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dead",
|
||||
"repeat": true,
|
||||
"frames": [
|
||||
{
|
||||
"index": 11,
|
||||
"duration": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
139
hw3/dist/hw3_assets/spritesheets/cars.json
vendored
Normal file
|
@ -0,0 +1,139 @@
|
|||
{
|
||||
"name": "cars",
|
||||
"spriteSheetImage": "cars.png",
|
||||
"spriteWidth": 256,
|
||||
"spriteHeight": 256,
|
||||
"leftBuffer": 0,
|
||||
"rightBuffer": 0,
|
||||
"topBuffer": 0,
|
||||
"bottomBuffer": 0,
|
||||
"columns": 3,
|
||||
"rows": 4,
|
||||
"animations": [
|
||||
{
|
||||
"name": "driving",
|
||||
"repeat": true,
|
||||
"frames": [
|
||||
{
|
||||
"index": 0,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"duration": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "firing",
|
||||
"repeat": true,
|
||||
"frames": [
|
||||
{
|
||||
"index": 0,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"duration": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "damage",
|
||||
"repeat": true,
|
||||
"frames": [
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
}, {
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"duration": 6
|
||||
}, {
|
||||
"index": 5,
|
||||
"duration": 6
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dying",
|
||||
"repeat": false,
|
||||
"next": "dead",
|
||||
"frames": [
|
||||
{
|
||||
"index": 6,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 7,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 8,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 9,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 10,
|
||||
"duration": 10
|
||||
},
|
||||
{
|
||||
"index": 11,
|
||||
"duration": 10
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dead",
|
||||
"repeat": true,
|
||||
"frames": [
|
||||
{
|
||||
"index": 11,
|
||||
"duration": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
BIN
hw3/dist/hw3_assets/spritesheets/cars.png
vendored
Normal file
After Width: | Height: | Size: 33 KiB |
34
hw3/gulpfile.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
var gulp = require('gulp');
|
||||
var browserify = require('browserify');
|
||||
var source = require('vinyl-source-stream');
|
||||
var watchify = require('watchify');
|
||||
var tsify = require('tsify');
|
||||
var fancy_log = require('fancy-log');
|
||||
var paths = {
|
||||
pages: ['src/*.html']
|
||||
};
|
||||
|
||||
var watchedBrowserify = watchify(browserify({
|
||||
basedir: '.',
|
||||
debug: true,
|
||||
entries: ['src/main.ts'],
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
}).plugin(tsify));
|
||||
|
||||
gulp.task('copy-html', function () {
|
||||
return gulp.src(paths.pages)
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
function bundle() {
|
||||
return watchedBrowserify
|
||||
.bundle()
|
||||
.on('error', fancy_log)
|
||||
.pipe(source('bundle.js'))
|
||||
.pipe(gulp.dest('dist'));
|
||||
}
|
||||
|
||||
gulp.task('default', gulp.series(gulp.parallel('copy-html'), bundle));
|
||||
watchedBrowserify.on('update', bundle);
|
||||
watchedBrowserify.on('log', fancy_log);
|
9236
hw3/package-lock.json
generated
Normal file
21
hw3/package.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "wolfie2d",
|
||||
"version": "1.0.0",
|
||||
"description": "A game engine written in TypeScript",
|
||||
"main": "./dist/main.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Joe Weaver",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"browserify": "^16.5.1",
|
||||
"fancy-log": "^1.3.3",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-typescript": "^6.0.0-alpha.1",
|
||||
"tsify": "^5.0.0",
|
||||
"typescript": "^3.9.7",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"watchify": "^3.11.1"
|
||||
}
|
||||
}
|
6
hw3/readme.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Game Engine
|
||||
## How to transpile and run
|
||||
|
||||
Start gulp by just running `gulp` in the console. Start the code by running `dist/main.js` with Web Server for Chrome or a similar product. Anytime you save, gulp should recompile the code automatically.
|
||||
|
||||
Setup follows [this helpful guide from TypeScript] (https://www.typescriptlang.org/docs/handbook/gulp.html) (Up through Watchify).
|
65
hw3/src/Wolfie2D/AI/AIManager.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import Actor from "../DataTypes/Interfaces/Actor";
|
||||
import Updateable from "../DataTypes/Interfaces/Updateable";
|
||||
import AI from "../DataTypes/Interfaces/AI";
|
||||
import Map from "../DataTypes/Map";
|
||||
|
||||
/**
|
||||
* A manager class for all of the AI in a scene.
|
||||
* Keeps a list of registered actors and handles AI generation for actors.
|
||||
*/
|
||||
export default class AIManager implements Updateable {
|
||||
/** The array of registered actors */
|
||||
actors: Array<Actor>;
|
||||
/** Maps AI names to their constructors */
|
||||
registeredAI: Map<AIConstructor>;
|
||||
|
||||
constructor(){
|
||||
this.actors = new Array();
|
||||
this.registeredAI = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an actor with the AIManager
|
||||
* @param actor The actor to register
|
||||
*/
|
||||
registerActor(actor: Actor): void {
|
||||
this.actors.push(actor);
|
||||
}
|
||||
|
||||
removeActor(actor: Actor): void {
|
||||
let index = this.actors.indexOf(actor);
|
||||
|
||||
if(index !== -1){
|
||||
this.actors.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an AI with the AIManager for use later on
|
||||
* @param name The name of the AI to register
|
||||
* @param constr The constructor for the AI
|
||||
*/
|
||||
registerAI(name: string, constr: new <T extends AI>() => T ): void {
|
||||
this.registeredAI.add(name, constr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an AI instance from its name
|
||||
* @param name The name of the AI to add
|
||||
* @returns A new AI instance
|
||||
*/
|
||||
generateAI(name: string): AI {
|
||||
if(this.registeredAI.has(name)){
|
||||
return new (this.registeredAI.get(name))();
|
||||
} else {
|
||||
throw `Cannot create AI with name ${name}, no AI with that name is registered`;
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
// Run the ai for every active actor
|
||||
this.actors.forEach(actor => { if(actor.aiActive) actor.ai.update(deltaT) });
|
||||
}
|
||||
}
|
||||
|
||||
type AIConstructor = new <T extends AI>() => T;
|
21
hw3/src/Wolfie2D/AI/ControllerAI.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import AI from "../DataTypes/Interfaces/AI";
|
||||
import GameEvent from "../Events/GameEvent";
|
||||
import GameNode from "../Nodes/GameNode";
|
||||
|
||||
export default abstract class ControllerAI implements AI {
|
||||
/** The owner of this controller */
|
||||
owner: GameNode;
|
||||
|
||||
/** Removes the instance of the owner of this AI */
|
||||
destroy(): void {
|
||||
delete this.owner;
|
||||
}
|
||||
|
||||
abstract initializeAI(owner: GameNode, options: Record<string, any>): void;
|
||||
|
||||
abstract activate(options: Record<string, any>): void;
|
||||
|
||||
abstract handleEvent(event: GameEvent): void;
|
||||
|
||||
abstract update(deltaT: number): void;
|
||||
}
|
24
hw3/src/Wolfie2D/AI/StateMachineAI.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import AI from "../DataTypes/Interfaces/AI";
|
||||
import StateMachine from "../DataTypes/State/StateMachine";
|
||||
import GameNode from "../Nodes/GameNode";
|
||||
|
||||
/**
|
||||
* A version of a @reference[StateMachine] that is configured to work as an AI controller for a @reference[GameNode]
|
||||
*/
|
||||
export default class StateMachineAI extends StateMachine implements AI {
|
||||
/** The GameNode that uses this StateMachine for its AI */
|
||||
protected owner: GameNode;
|
||||
|
||||
// @implemented
|
||||
initializeAI(owner: GameNode, config: Record<string, any>): void {}
|
||||
|
||||
// @implemented
|
||||
destroy(){
|
||||
// Get rid of our reference to the owner
|
||||
delete this.owner;
|
||||
this.receiver.destroy();
|
||||
}
|
||||
|
||||
// @implemented
|
||||
activate(options: Record<string, any>): void {}
|
||||
}
|
17
hw3/src/Wolfie2D/DataTypes/Collection.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
// @ignorePage
|
||||
|
||||
/**
|
||||
* 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 The function to evaluate of every item in the collection
|
||||
*/
|
||||
forEach(func: Function): void;
|
||||
|
||||
/**
|
||||
* Clears the contents of the data structure
|
||||
*/
|
||||
clear(): void;
|
||||
}
|
8
hw3/src/Wolfie2D/DataTypes/Functions/NullFunc.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
// @ignorePage
|
||||
|
||||
/**
|
||||
* A placeholder function for No Operation. Does nothing
|
||||
*/
|
||||
const NullFunc = () => {};
|
||||
|
||||
export default NullFunc;
|
22
hw3/src/Wolfie2D/DataTypes/Graphs/EdgeNode.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* A linked-list for the edges in a @reference[Graph].
|
||||
*/
|
||||
export default class EdgeNode {
|
||||
/** The node in the Graph this edge connects to */
|
||||
y: number;
|
||||
/** The weight of this EdgeNode */
|
||||
weight: number;
|
||||
/** The next EdgeNode in the linked-list */
|
||||
next: EdgeNode;
|
||||
|
||||
/**
|
||||
* Creates a new EdgeNode
|
||||
* @param index The index of the node this edge connects to
|
||||
* @param weight The weight of this edge
|
||||
*/
|
||||
constructor(index: number, weight?: number){
|
||||
this.y = index;
|
||||
this.next = null;
|
||||
this.weight = weight ? weight : 1;
|
||||
}
|
||||
}
|
145
hw3/src/Wolfie2D/DataTypes/Graphs/Graph.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
import EdgeNode from "./EdgeNode";
|
||||
|
||||
export const MAX_V = 100;
|
||||
|
||||
/**
|
||||
* An implementation of a graph data structure using edge lists. Inspired by The Algorithm Design Manual.
|
||||
*/
|
||||
export default class Graph {
|
||||
/** An array of edges at the node specified by the index */
|
||||
edges: Array<EdgeNode>;
|
||||
/** An array representing the degree of the node specified by the index */
|
||||
degree: Array<number>;
|
||||
/** The number of vertices in the graph */
|
||||
numVertices: number;
|
||||
/** The number of edges in the graph */
|
||||
numEdges: number;
|
||||
/** Whether or not the graph is directed */
|
||||
directed: boolean;
|
||||
/** Whether or not the graph is weighted */
|
||||
weighted: boolean;
|
||||
|
||||
/**
|
||||
* Constructs a new graph
|
||||
* @param directed Whether or not this graph is directed
|
||||
*/
|
||||
constructor(directed: boolean = false){
|
||||
this.directed = directed;
|
||||
this.weighted = false;
|
||||
|
||||
this.numVertices = 0;
|
||||
this.numEdges = 0;
|
||||
|
||||
this.edges = new Array(MAX_V);
|
||||
this.degree = new Array(MAX_V);
|
||||
}
|
||||
|
||||
/** Adds a node to this graph and returns the index of it
|
||||
* @returns The index of the new node
|
||||
*/
|
||||
addNode(): number {
|
||||
this.numVertices++;
|
||||
return this.numVertices;
|
||||
}
|
||||
|
||||
/** Adds an edge between node x and y, with an optional weight
|
||||
* @param x The index of the start of the edge
|
||||
* @param y The index of the end of the edge
|
||||
* @param weight The optional weight of the new edge
|
||||
*/
|
||||
addEdge(x: number, y: number, weight?: number): void {
|
||||
let edge = new EdgeNode(y, weight);
|
||||
|
||||
|
||||
|
||||
if(this.edges[x]){
|
||||
edge.next = this.edges[x];
|
||||
}
|
||||
|
||||
this.edges[x] = edge;
|
||||
|
||||
if(!this.directed){
|
||||
edge = new EdgeNode(x, weight);
|
||||
|
||||
if(this.edges[y]){
|
||||
edge.next = this.edges[y];
|
||||
}
|
||||
|
||||
this.edges[y] = edge;
|
||||
}
|
||||
|
||||
this.numEdges += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not an edge exists between two nodes.
|
||||
* This check is directional if this is a directed graph.
|
||||
* @param x The first node
|
||||
* @param y The second node
|
||||
* @returns true if an edge exists, false otherwise
|
||||
*/
|
||||
edgeExists(x: number, y: number): boolean {
|
||||
let edge = this.edges[x];
|
||||
|
||||
while(edge !== null){
|
||||
if(edge.y === y){
|
||||
return true;
|
||||
}
|
||||
edge = edge.next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the edge list associated with node x
|
||||
* @param x The index of the node
|
||||
* @returns The head of a linked-list of edges
|
||||
*/
|
||||
getEdges(x: number): EdgeNode {
|
||||
return this.edges[x];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the degree associated with node x
|
||||
* @param x The index of the node
|
||||
*/
|
||||
getDegree(x: number): number {
|
||||
return this.degree[x];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the specifed node into a string
|
||||
* @param index The index of the node to convert to a string
|
||||
* @returns The string representation of the node: "Node x"
|
||||
*/
|
||||
protected nodeToString(index: number): string {
|
||||
return "Node " + index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the Graph into a string format
|
||||
* @returns The graph as a string
|
||||
*/
|
||||
toString(): string {
|
||||
let retval = "";
|
||||
|
||||
for(let i = 0; i < this.numVertices; i++){
|
||||
let edge = this.edges[i];
|
||||
let edgeStr = "";
|
||||
while(edge !== null){
|
||||
edgeStr += edge.y.toString();
|
||||
if(this.weighted){
|
||||
edgeStr += " (" + edge.weight + ")";
|
||||
}
|
||||
if(edge.next !== null){
|
||||
edgeStr += ", ";
|
||||
}
|
||||
|
||||
edge = edge.next;
|
||||
}
|
||||
|
||||
retval += this.nodeToString(i) + ": " + edgeStr + "\n";
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
}
|
94
hw3/src/Wolfie2D/DataTypes/Graphs/PositionGraph.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import Graph, { MAX_V } from "./Graph";
|
||||
import Vec2 from "../Vec2";
|
||||
import DebugRenderable from "../Interfaces/DebugRenderable";
|
||||
|
||||
/**
|
||||
* An extension of Graph that has nodes with positions in 2D space.
|
||||
* This is a weighted graph (though not inherently directd)
|
||||
*/
|
||||
export default class PositionGraph extends Graph implements DebugRenderable {
|
||||
/** An array of the positions of the nodes in this graph */
|
||||
positions: Array<Vec2>;
|
||||
|
||||
/**
|
||||
* Createes a new PositionGraph
|
||||
* @param directed Whether or not this graph is directed
|
||||
*/
|
||||
constructor(directed: boolean = false){
|
||||
super(directed);
|
||||
this.positions = new Array(MAX_V);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a positioned node to this graph
|
||||
* @param position The position of the node to add
|
||||
* @returns The index of the added node
|
||||
*/
|
||||
addPositionedNode(position: Vec2): number {
|
||||
this.positions[this.numVertices] = position;
|
||||
return this.addNode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the position of a node.
|
||||
* Automatically adjusts the weights of the graph tied to this node.
|
||||
* As such, be warned that this function has an O(n + m) running time, and use it sparingly.
|
||||
* @param index The index of the node
|
||||
* @param position The new position of the node
|
||||
*/
|
||||
setNodePosition(index: number, position: Vec2): void {
|
||||
this.positions[index] = position;
|
||||
|
||||
// Recalculate all weights associated with this index
|
||||
for(let i = 0; i < this.numEdges; i++){
|
||||
|
||||
let edge = this.edges[i];
|
||||
|
||||
while(edge !== null){
|
||||
// If this node is on either side of the edge, recalculate weight
|
||||
if(i === index || edge.y === index){
|
||||
edge.weight = this.positions[i].distanceTo(this.positions[edge.y]);
|
||||
}
|
||||
|
||||
edge = edge.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the position of a node
|
||||
* @param index The index of the node
|
||||
* @returns The position of the node
|
||||
*/
|
||||
getNodePosition(index: number): Vec2 {
|
||||
return this.positions[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an edge to this graph between node x and y.
|
||||
* Automatically calculates the weight of the edge as the distance between the nodes.
|
||||
* @param x The beginning of the edge
|
||||
* @param y The end of the edge
|
||||
*/
|
||||
addEdge(x: number, y: number): void {
|
||||
if(!this.positions[x] || !this.positions[y]){
|
||||
throw "Can't add edge to un-positioned node!";
|
||||
}
|
||||
|
||||
// Weight is the distance between the nodes
|
||||
let weight = this.positions[x].distanceTo(this.positions[y]);
|
||||
|
||||
super.addEdge(x, y, weight);
|
||||
}
|
||||
|
||||
// @override
|
||||
protected nodeToString(index: number): string {
|
||||
return "Node " + index + " - " + this.positions[index].toString();
|
||||
}
|
||||
|
||||
debugRender = (): void => {
|
||||
// for(let point of this.positions){
|
||||
// ctx.fillRect((point.x - origin.x - 4)*zoom, (point.y - origin.y - 4)*zoom, 8, 8);
|
||||
// }
|
||||
}
|
||||
}
|
20
hw3/src/Wolfie2D/DataTypes/Interfaces/AI.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import GameEvent from "../../Events/GameEvent";
|
||||
import GameNode from "../../Nodes/GameNode";
|
||||
import Updateable from "./Updateable";
|
||||
|
||||
/**
|
||||
* Defines a controller for a bot or a human. Must be able to update
|
||||
*/
|
||||
export default interface AI extends Updateable {
|
||||
/** Initializes the AI with the actor and any additional config */
|
||||
initializeAI(owner: GameNode, options: Record<string, any>): void;
|
||||
|
||||
/** Clears references from to the owner */
|
||||
destroy(): void;
|
||||
|
||||
/** Activates this AI from a stopped state and allows variables to be passed in */
|
||||
activate(options: Record<string, any>): void;
|
||||
|
||||
/** Handles events from the Actor */
|
||||
handleEvent(event: GameEvent): void;
|
||||
}
|
40
hw3/src/Wolfie2D/DataTypes/Interfaces/Actor.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import NavigationPath from "../../Pathfinding/NavigationPath";
|
||||
import AI from "./AI";
|
||||
|
||||
/**
|
||||
* A game object that has an AI and can perform its own actions every update cycle
|
||||
*/
|
||||
export default interface Actor {
|
||||
/** The AI of the actor */
|
||||
ai: AI;
|
||||
|
||||
/** The activity status of the actor */
|
||||
aiActive: boolean;
|
||||
|
||||
/** The path that navigation will follow */
|
||||
path: NavigationPath;
|
||||
|
||||
/** A flag representing whether or not the actor is currently pathfinding */
|
||||
pathfinding: boolean;
|
||||
|
||||
/**
|
||||
* Adds an AI to this Actor.
|
||||
* @param ai The name of the AI, or the actual AI, to add to the Actor.
|
||||
* @param options The options to give to the AI for initialization.
|
||||
*/
|
||||
addAI<T extends AI>(ai: string | (new () => T), options: Record<string, any>): void;
|
||||
|
||||
/**
|
||||
* Sets the AI to start/stop for this Actor.
|
||||
* @param active The new active status of the AI.
|
||||
* @param options An object that allows options to be pased to the activated AI
|
||||
*/
|
||||
setAIActive(active: boolean, options: Record<string, any>): void;
|
||||
|
||||
/**
|
||||
* Moves this GameNode along a path
|
||||
* @param speed The speed to move with
|
||||
* @param path The path we're moving along
|
||||
*/
|
||||
moveOnPath(speed: number, path: NavigationPath): void;
|
||||
}
|
7
hw3/src/Wolfie2D/DataTypes/Interfaces/DebugRenderable.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Represents a game object that can be rendered in Debug mode
|
||||
*/
|
||||
export default interface DebugRenderable {
|
||||
/** Renders the debugging information for this object. */
|
||||
debugRender(): void;
|
||||
}
|
12
hw3/src/Wolfie2D/DataTypes/Interfaces/Navigable.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import Vec2 from "../Vec2";
|
||||
import NavigationPath from "../../Pathfinding/NavigationPath";
|
||||
|
||||
/** Represents an entity that can be navigated on. */
|
||||
export default interface Navigable {
|
||||
/**
|
||||
* Gets a new navigation path based on this Navigable object.
|
||||
* @param fromPosition The position to start navigation from.
|
||||
* @param toPosition The position to navigate to.
|
||||
*/
|
||||
getNavigationPath(fromPosition: Vec2, toPosition: Vec2): NavigationPath;
|
||||
}
|
120
hw3/src/Wolfie2D/DataTypes/Interfaces/Physical.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
import Shape from "../Shapes/Shape";
|
||||
import AABB from "../Shapes/AABB";
|
||||
import Vec2 from "../Vec2";
|
||||
import Map from "../Map";
|
||||
|
||||
/**
|
||||
* Describes an object that can opt into physics.
|
||||
*/
|
||||
export default interface Physical {
|
||||
/** A flag for whether or not this object has initialized game physics. */
|
||||
hasPhysics: boolean;
|
||||
|
||||
/** Represents whether the object is moving or not. */
|
||||
moving: boolean;
|
||||
|
||||
/** Represent whether the object is frozen from moving or not. */
|
||||
frozen: boolean;
|
||||
|
||||
/** Represents whether the object is on the ground or not. */
|
||||
onGround: boolean;
|
||||
|
||||
/** Reprsents whether the object is on the wall or not. */
|
||||
onWall: boolean;
|
||||
|
||||
/** Reprsents whether the object is on the ceiling or not. */
|
||||
onCeiling: boolean;
|
||||
|
||||
/** Represnts whether this object has active physics or not. */
|
||||
active: boolean;
|
||||
|
||||
/** The shape of the collider for this physics object. */
|
||||
collisionShape: Shape;
|
||||
|
||||
/** The offset of the collision shape from the center of the node */
|
||||
colliderOffset: Vec2;
|
||||
|
||||
/** Represents whether this object can move or not. */
|
||||
isStatic: boolean;
|
||||
|
||||
/** Represents whether this object is collidable (solid) or not. */
|
||||
isCollidable: boolean;
|
||||
|
||||
/** Represnts whether this object is a trigger or not. */
|
||||
isTrigger: boolean;
|
||||
|
||||
/** The trigger mask for this node */
|
||||
triggerMask: number;
|
||||
|
||||
/** Events to trigger for collision enters. */
|
||||
triggerEnters: Array<string>;
|
||||
|
||||
/** Events to trigger for collision exits */
|
||||
triggerExits: Array<string>;
|
||||
|
||||
/** A vector that allows velocity to be passed to the physics engine */
|
||||
_velocity: Vec2;
|
||||
|
||||
/** The rectangle swept by the movement of this object, if dynamic */
|
||||
sweptRect: AABB;
|
||||
|
||||
/** A boolean representing whether or not the node just collided with the tilemap */
|
||||
collidedWithTilemap: boolean;
|
||||
|
||||
/** The physics group this node belongs to */
|
||||
group: number;
|
||||
|
||||
isPlayer: boolean;
|
||||
|
||||
isColliding: boolean;
|
||||
|
||||
/*---------- FUNCTIONS ----------*/
|
||||
|
||||
/**
|
||||
* Tells the physics engine to handle a move by this object.
|
||||
* @param velocity The velocity with which to move the object.
|
||||
*/
|
||||
move(velocity: Vec2): void;
|
||||
|
||||
/**
|
||||
* The move actually done by the physics engine after collision checks are done.
|
||||
* @param velocity The velocity with which the object will move.
|
||||
*/
|
||||
finishMove(): void;
|
||||
|
||||
/**
|
||||
* Adds physics to this object
|
||||
* @param collisionShape The shape of this collider for this object
|
||||
* @param isCollidable Whether this object will be able to collide with other objects
|
||||
* @param isStatic Whether this object will be static or not
|
||||
*/
|
||||
addPhysics(collisionShape?: Shape, colliderOffset?: Vec2, isCollidable?: boolean, isStatic?: boolean): void;
|
||||
|
||||
/** Removes this object from the physics system */
|
||||
removePhysics(): void;
|
||||
|
||||
/** Prevents this object from participating in all collisions and triggers. It can still move. */
|
||||
disablePhysics(): void;
|
||||
|
||||
/** Enables this object to participate in collisions and triggers. This is only necessary if disablePhysics was called */
|
||||
enablePhysics(): void;
|
||||
|
||||
/**
|
||||
* Sets this object to be a trigger for a specific group
|
||||
* @param group The name of the group that activates the trigger
|
||||
* @param onEnter The name of the event to send when this trigger is activated
|
||||
* @param onExit The name of the event to send when this trigger stops being activated
|
||||
*/
|
||||
setTrigger(group: string, onEnter: string, onExit: string): void;
|
||||
|
||||
/**
|
||||
* Sets the physics group of this node
|
||||
* @param group The name of the group
|
||||
*/
|
||||
setGroup(group: string): void;
|
||||
|
||||
/**
|
||||
* If used before "move()", it will tell you the velocity of the node after its last movement
|
||||
*/
|
||||
getLastVelocity(): Vec2;
|
||||
}
|
10
hw3/src/Wolfie2D/DataTypes/Interfaces/Positioned.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import Vec2 from "../Vec2";
|
||||
|
||||
/** An object that has a position */
|
||||
export default interface Positioned {
|
||||
/** The center of this object. */
|
||||
position: Vec2;
|
||||
|
||||
/** The center of this object relative to the viewport. */
|
||||
readonly relativePosition: Vec2;
|
||||
}
|
21
hw3/src/Wolfie2D/DataTypes/Interfaces/Region.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Vec2 from "../Vec2";
|
||||
import AABB from "../Shapes/AABB";
|
||||
|
||||
/** An object that is a region, with a size, scale, and boundary. */
|
||||
export default interface Region {
|
||||
/** The size of this object. */
|
||||
size: Vec2;
|
||||
|
||||
/** The scale of this object. */
|
||||
scale: Vec2;
|
||||
|
||||
/** The size of the object taking into account the zoom and scale */
|
||||
readonly sizeWithZoom: Vec2;
|
||||
|
||||
/** The bounding box of this object. */
|
||||
boundary: AABB;
|
||||
}
|
||||
|
||||
export function isRegion(arg: any): boolean {
|
||||
return arg && arg.size && arg.scale && arg.boundary;
|
||||
}
|
7
hw3/src/Wolfie2D/DataTypes/Interfaces/Unique.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Represents an object with a unique id
|
||||
*/
|
||||
export default interface Unique {
|
||||
/** The unique id of this object. */
|
||||
id: number;
|
||||
}
|
8
hw3/src/Wolfie2D/DataTypes/Interfaces/Updateable.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/** Represents a game object that can be updated */
|
||||
export default interface Updateable {
|
||||
/**
|
||||
* Updates this object.
|
||||
* @param deltaT The timestep of the update.
|
||||
*/
|
||||
update(deltaT: number): void;
|
||||
}
|
63
hw3/src/Wolfie2D/DataTypes/List.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import Collection from "./Collection";
|
||||
|
||||
/**
|
||||
* A doubly linked list
|
||||
*/
|
||||
export default class List<T> implements Collection {
|
||||
|
||||
private head: ListItem<T>;
|
||||
private tail: ListItem<T>;
|
||||
private _size: number;
|
||||
|
||||
constructor(){
|
||||
this.head = null;
|
||||
this.tail = null;
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
add(value: T){
|
||||
if(this._size === 0){
|
||||
// There were no items in the list previously, so head and tail are the same
|
||||
this.head = new ListItem(value, null, null);
|
||||
this.tail = this.head;
|
||||
} else {
|
||||
this.tail.next = new ListItem(value, this.tail, null);
|
||||
this.tail = this.tail.next;
|
||||
}
|
||||
|
||||
// Increment the size
|
||||
this._size += 1;
|
||||
}
|
||||
|
||||
forEach(func: Function): void {
|
||||
let p = this.head;
|
||||
|
||||
while(p !== null){
|
||||
func(p.value);
|
||||
p = p.next;
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.head = null
|
||||
this.tail = null
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ListItem<T> {
|
||||
value: T;
|
||||
next: ListItem<T>;
|
||||
prev: ListItem<T>;
|
||||
|
||||
constructor(value: T, next: ListItem<T>, prev: ListItem<T>){
|
||||
this.value = value;
|
||||
this.next = next;
|
||||
this.prev = prev;
|
||||
}
|
||||
}
|
87
hw3/src/Wolfie2D/DataTypes/Map.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import Collection from "./Collection";
|
||||
|
||||
/**
|
||||
* Associates strings with elements of type T
|
||||
*/
|
||||
export default class Map<T> implements Collection {
|
||||
private map: Record<string, T>;
|
||||
|
||||
/** Creates a new map */
|
||||
constructor(){
|
||||
this.map = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a value T stored at a key.
|
||||
* @param key The key of the item to be stored
|
||||
* @param value The item to be stored
|
||||
*/
|
||||
add(key: string, value: T): void {
|
||||
this.map[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value associated with a key.
|
||||
* @param key The key of the item
|
||||
* @returns The item at the key or undefined
|
||||
*/
|
||||
get(key: string): T {
|
||||
return this.map[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* An alias of add. Sets the value stored at key to the new specified value
|
||||
* @param key The key of the item to be stored
|
||||
* @param value The item to be stored
|
||||
*/
|
||||
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 The key to check
|
||||
* @returns A boolean representing whether or not there is an item at the given key.
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
return this.map[key] !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all of the keys in this map.
|
||||
* @returns An array containing all keys in the map.
|
||||
*/
|
||||
keys(): Array<string> {
|
||||
return Object.keys(this.map);
|
||||
}
|
||||
|
||||
// @implemented
|
||||
forEach(func: (key: string) => void): void {
|
||||
Object.keys(this.map).forEach(key => func(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an item associated with a key
|
||||
* @param key The key at which to delete an item
|
||||
*/
|
||||
delete(key: string): void {
|
||||
delete this.map[key];
|
||||
}
|
||||
|
||||
// @implemented
|
||||
clear(): void {
|
||||
this.forEach(key => delete this.map[key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this map to a string representation.
|
||||
* @returns The string representation of this map.
|
||||
*/
|
||||
toString(): string {
|
||||
let str = "";
|
||||
|
||||
this.forEach((key) => str += key + " -> " + this.get(key).toString() + "\n");
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
167
hw3/src/Wolfie2D/DataTypes/Mat4x4.ts
Normal file
|
@ -0,0 +1,167 @@
|
|||
import Vec2 from "./Vec2";
|
||||
|
||||
/** A 4x4 matrix0 */
|
||||
export default class Mat4x4 {
|
||||
private mat: Float32Array;
|
||||
|
||||
constructor(){
|
||||
this.mat = new Float32Array([
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0
|
||||
]);
|
||||
}
|
||||
|
||||
// Static members
|
||||
static get IDENTITY(): Mat4x4 {
|
||||
return new Mat4x4().identity();
|
||||
}
|
||||
|
||||
static get ZERO(): Mat4x4 {
|
||||
return new Mat4x4().zero();
|
||||
}
|
||||
|
||||
// Accessors
|
||||
set _00(x: number) {
|
||||
this.mat[0] = x;
|
||||
}
|
||||
|
||||
set(col: number, row: number, value: number): Mat4x4 {
|
||||
if(col < 0 || col > 3 || row < 0 || row > 3){
|
||||
throw `Error - index (${col}, ${row}) is out of bounds for Mat4x4`
|
||||
}
|
||||
this.mat[row*4 + col] = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
get(col: number, row: number): number {
|
||||
return this.mat[row*4 + col];
|
||||
}
|
||||
|
||||
setAll(...items: Array<number>): Mat4x4 {
|
||||
this.mat.set(items);
|
||||
return this;
|
||||
}
|
||||
|
||||
identity(): Mat4x4 {
|
||||
return this.setAll(
|
||||
1, 0, 0, 0,
|
||||
0, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1
|
||||
)
|
||||
}
|
||||
|
||||
zero(): Mat4x4 {
|
||||
return this.setAll(
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this Mat4x4 a rotation matrix of the specified number of radians ccw
|
||||
* @param zRadians The number of radians to rotate
|
||||
* @returns this Mat4x4
|
||||
*/
|
||||
rotate(zRadians: number): Mat4x4 {
|
||||
return this.setAll(
|
||||
Math.cos(zRadians), -Math.sin(zRadians), 0, 0,
|
||||
Math.sin(zRadians), Math.cos(zRadians), 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns this Mat4x4 into a translation matrix of the specified translation
|
||||
* @param translation The translation in x and y
|
||||
* @returns this Mat4x4
|
||||
*/
|
||||
translate(translation: Vec2 | Float32Array): Mat4x4 {
|
||||
// If translation is a vec, get its array
|
||||
if(translation instanceof Vec2){
|
||||
translation = translation.toArray();
|
||||
}
|
||||
|
||||
return this.setAll(
|
||||
1, 0, 0, translation[0],
|
||||
0, 1, 0, translation[1],
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1
|
||||
);
|
||||
}
|
||||
|
||||
scale(scale: Vec2 | Float32Array | number): Mat4x4 {
|
||||
// Make sure scale is a float32Array
|
||||
if(scale instanceof Vec2){
|
||||
scale = scale.toArray();
|
||||
} else if(!(scale instanceof Float32Array)){
|
||||
scale = new Float32Array([scale, scale]);
|
||||
}
|
||||
|
||||
return this.setAll(
|
||||
scale[0], 0, 0, 0,
|
||||
0, scale[1], 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Mat4x4 that represents the right side multiplication THIS x OTHER
|
||||
* @param other The other Mat4x4 to multiply by
|
||||
* @returns a new Mat4x4 containing the product of these two Mat4x4s
|
||||
*/
|
||||
mult(other: Mat4x4, out?: Mat4x4): Mat4x4 {
|
||||
let temp = new Float32Array(16);
|
||||
|
||||
for(let i = 0; i < 4; i++){
|
||||
for(let j = 0; j < 4; j++){
|
||||
let value = 0;
|
||||
for(let k = 0; k < 4; k++){
|
||||
value += this.get(k, i) * other.get(j, k);
|
||||
}
|
||||
temp[j*4 + i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if(out !== undefined){
|
||||
return out.setAll(...temp);
|
||||
} else {
|
||||
return new Mat4x4().setAll(...temp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies all given matricies in order. e.g. MULT(A, B, C) -> A*B*C
|
||||
* @param mats A list of Mat4x4s to multiply in order
|
||||
* @returns A new Mat4x4 holding the result of the operation
|
||||
*/
|
||||
static MULT(...mats: Array<Mat4x4>): Mat4x4 {
|
||||
// Create a new array
|
||||
let temp = Mat4x4.IDENTITY;
|
||||
|
||||
// Multiply by every array in order, in place
|
||||
for(let i = 0; i < mats.length; i++){
|
||||
temp.mult(mats[i], temp);
|
||||
}
|
||||
|
||||
return temp;
|
||||
}
|
||||
|
||||
toArray(): Float32Array {
|
||||
return this.mat;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `|${this.mat[0].toFixed(2)}, ${this.mat[1].toFixed(2)}, ${this.mat[2].toFixed(2)}, ${this.mat[3].toFixed(2)}|\n` +
|
||||
`|${this.mat[4].toFixed(2)}, ${this.mat[5].toFixed(2)}, ${this.mat[6].toFixed(2)}, ${this.mat[7].toFixed(2)}|\n` +
|
||||
`|${this.mat[8].toFixed(2)}, ${this.mat[9].toFixed(2)}, ${this.mat[10].toFixed(2)}, ${this.mat[11].toFixed(2)}|\n` +
|
||||
`|${this.mat[12].toFixed(2)}, ${this.mat[13].toFixed(2)}, ${this.mat[14].toFixed(2)}, ${this.mat[15].toFixed(2)}|`;
|
||||
}
|
||||
}
|
40
hw3/src/Wolfie2D/DataTypes/Physics/AreaCollision.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import Physical from "../Interfaces/Physical";
|
||||
import AABB from "../Shapes/AABB";
|
||||
import Vec2 from "../Vec2";
|
||||
import Hit from "./Hit";
|
||||
|
||||
/**
|
||||
* A class that contains the area of overlap of two colliding objects to allow for sorting by the physics system.
|
||||
*/
|
||||
export default class AreaCollision {
|
||||
/** The area of the overlap for the colliding objects */
|
||||
area: number;
|
||||
|
||||
/** The AABB of the other collider in this collision */
|
||||
collider: AABB;
|
||||
|
||||
/** Type of the collision */
|
||||
type: string;
|
||||
|
||||
/** Ther other object in the collision */
|
||||
other: Physical;
|
||||
|
||||
/** The tile, if this was a tilemap collision */
|
||||
tile: Vec2;
|
||||
|
||||
/** The physics hit for this object */
|
||||
hit: Hit;
|
||||
|
||||
/**
|
||||
* Creates a new AreaCollision object
|
||||
* @param area The area of the collision
|
||||
* @param collider The other collider
|
||||
*/
|
||||
constructor(area: number, collider: AABB, other: Physical, type: string, tile: Vec2){
|
||||
this.area = area;
|
||||
this.collider = collider;
|
||||
this.other = other;
|
||||
this.type = type;
|
||||
this.tile = tile;
|
||||
}
|
||||
}
|
18
hw3/src/Wolfie2D/DataTypes/Physics/Hit.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import Vec2 from "../Vec2";
|
||||
|
||||
/**
|
||||
* An object representing the data collected from a physics hit between two geometric objects.
|
||||
* Inspired by the helpful collision documentation @link(here)(https://noonat.github.io/intersect/).
|
||||
*/
|
||||
export default class Hit {
|
||||
/** The time of the collision. Only numbers 0 through 1 happen in this frame. */
|
||||
time: number;
|
||||
/** The near times of the collision */
|
||||
nearTimes: Vec2 = Vec2.ZERO;
|
||||
/** The position of the collision */
|
||||
pos: Vec2 = Vec2.ZERO;
|
||||
/** The overlap distance of the hit */
|
||||
delta: Vec2 = Vec2.ZERO;
|
||||
/** The normal vector of the hit */
|
||||
normal: Vec2 = Vec2.ZERO;
|
||||
}
|
156
hw3/src/Wolfie2D/DataTypes/QuadTree.ts
Normal file
|
@ -0,0 +1,156 @@
|
|||
import Vec2 from "./Vec2";
|
||||
import Collection from "./Collection";
|
||||
import AABB from "./Shapes/AABB"
|
||||
import Positioned from "./Interfaces/Positioned";
|
||||
|
||||
// TODO - Make max depth
|
||||
|
||||
// @ignorePage
|
||||
|
||||
/**
|
||||
* Primarily used to organize the scene graph
|
||||
*/
|
||||
export default class QuadTree<T extends Positioned> implements Collection {
|
||||
/**
|
||||
* The center of this quadtree
|
||||
*/
|
||||
protected boundary: AABB;
|
||||
|
||||
/**
|
||||
* The number of elements this quadtree root can hold before splitting
|
||||
*/
|
||||
protected capacity: number;
|
||||
|
||||
/**
|
||||
* The maximum height of the quadtree from this root
|
||||
*/
|
||||
protected maxDepth: number;
|
||||
|
||||
/**
|
||||
* Represents whether the quadtree is a root or a leaf
|
||||
*/
|
||||
protected divided: boolean;
|
||||
|
||||
/**
|
||||
* The array of the items in this quadtree
|
||||
*/
|
||||
protected items: Array<T>;
|
||||
|
||||
// The child quadtrees of this one
|
||||
protected nw: QuadTree<T>;
|
||||
protected ne: QuadTree<T>;
|
||||
protected sw: QuadTree<T>;
|
||||
protected se: QuadTree<T>;
|
||||
|
||||
constructor(center: Vec2, size: Vec2, maxDepth?: number, capacity?: number){
|
||||
this.boundary = new AABB(center, size);
|
||||
this.maxDepth = maxDepth !== undefined ? maxDepth : 10
|
||||
this.capacity = capacity ? capacity : 10;
|
||||
|
||||
// If we're at the bottom of the tree, don't set a max size
|
||||
if(this.maxDepth === 0){
|
||||
this.capacity = Infinity;
|
||||
}
|
||||
|
||||
this.divided = false;
|
||||
this.items = new Array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new item into this quadtree. Defers to children if this quadtree is divided
|
||||
* or divides the quadtree if capacity is exceeded with this add.
|
||||
* @param item The item to add to the quadtree
|
||||
*/
|
||||
insert(item: T){
|
||||
// If the item is inside of the bounds of this quadtree
|
||||
if(this.boundary.containsPointSoft(item.position)){
|
||||
if(this.divided){
|
||||
// Defer to the children
|
||||
this.deferInsert(item);
|
||||
} else if (this.items.length < this.capacity){
|
||||
// Add to this items list
|
||||
this.items.push(item);
|
||||
} else {
|
||||
// We aren't divided, but are at capacity - divide
|
||||
this.subdivide();
|
||||
this.deferInsert(item);
|
||||
this.divided = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides this quadtree up into 4 smaller ones - called through insert.
|
||||
*/
|
||||
protected subdivide(): void {
|
||||
let x = this.boundary.x;
|
||||
let y = this.boundary.y;
|
||||
let hw = this.boundary.hw;
|
||||
let hh = this.boundary.hh;
|
||||
|
||||
this.nw = new QuadTree(new Vec2(x-hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1);
|
||||
this.ne = new QuadTree(new Vec2(x+hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1)
|
||||
this.sw = new QuadTree(new Vec2(x-hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1)
|
||||
this.se = new QuadTree(new Vec2(x+hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1)
|
||||
|
||||
this.distributeItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Distributes the items of this quadtree into its children.
|
||||
*/
|
||||
protected distributeItems(): void {
|
||||
this.items.forEach(item => this.deferInsert(item));
|
||||
|
||||
// Delete the items from this array
|
||||
this.items.forEach((item, index) => delete this.items[index]);
|
||||
this.items.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defers this insertion to the children of this quadtree
|
||||
* @param item
|
||||
*/
|
||||
protected deferInsert(item: T): void {
|
||||
this.nw.insert(item);
|
||||
this.ne.insert(item);
|
||||
this.sw.insert(item);
|
||||
this.se.insert(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the quadtree for demo purposes.
|
||||
* @param ctx
|
||||
*/
|
||||
public render_demo(ctx: CanvasRenderingContext2D): void {
|
||||
ctx.strokeStyle = "#FF0000";
|
||||
ctx.strokeRect(this.boundary.x - this.boundary.hw, this.boundary.y - this.boundary.hh, 2*this.boundary.hw, 2*this.boundary.hh);
|
||||
|
||||
if(this.divided){
|
||||
this.nw.render_demo(ctx);
|
||||
this.ne.render_demo(ctx);
|
||||
this.sw.render_demo(ctx);
|
||||
this.se.render_demo(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
forEach(func: Function): void {
|
||||
// If divided, send the call down
|
||||
if(this.divided){
|
||||
this.nw.forEach(func);
|
||||
this.ne.forEach(func);
|
||||
this.sw.forEach(func);
|
||||
this.se.forEach(func);
|
||||
} else {
|
||||
// Otherwise, iterate over items
|
||||
for(let i = 0; i < this.items.length; i++){
|
||||
func(this.items[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
}
|
130
hw3/src/Wolfie2D/DataTypes/Queue.ts
Normal file
|
@ -0,0 +1,130 @@
|
|||
import Collection from "./Collection";
|
||||
|
||||
/**
|
||||
* A FIFO queue with elements of type T
|
||||
*/
|
||||
export default class Queue<T> implements Collection {
|
||||
/** The maximum number of elements in the Queue */
|
||||
private readonly MAX_ELEMENTS: number;
|
||||
|
||||
/** The internal representation of the queue */
|
||||
private q: Array<T>;
|
||||
|
||||
/** The head of the queue */
|
||||
private head: number;
|
||||
|
||||
/** The tail of the queue */
|
||||
private tail: number;
|
||||
|
||||
/** The current number of items in the queue */
|
||||
private size: number;
|
||||
|
||||
/**
|
||||
* Constructs a new queue
|
||||
* @param maxElements The maximum size of the stack
|
||||
*/
|
||||
constructor(maxElements: number = 100){
|
||||
this.MAX_ELEMENTS = maxElements;
|
||||
this.q = new Array(this.MAX_ELEMENTS);
|
||||
this.head = 0;
|
||||
this.tail = 0;
|
||||
this.size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to the back of the queue
|
||||
* @param item The item to add to the back of the queue
|
||||
*/
|
||||
enqueue(item: T): void{
|
||||
if((this.tail + 1) % this.MAX_ELEMENTS === this.head){
|
||||
throw new Error("Queue full - cannot add element");
|
||||
}
|
||||
|
||||
this.size += 1;
|
||||
this.q[this.tail] = item;
|
||||
this.tail = (this.tail + 1) % this.MAX_ELEMENTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an item from the front of the queue
|
||||
* @returns The item at the front of the queue
|
||||
*/
|
||||
dequeue(): T {
|
||||
if(this.head === this.tail){
|
||||
throw new Error("Queue empty - cannot remove element");
|
||||
}
|
||||
|
||||
|
||||
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 remove it
|
||||
* @returns The item at the front of the queue
|
||||
*/
|
||||
peekNext(): T {
|
||||
if(this.head === this.tail){
|
||||
throw "Queue empty - cannot get element"
|
||||
}
|
||||
|
||||
let item = this.q[this.head];
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the queue has items in it, false otherwise
|
||||
* @returns A boolean representing whether or not this queue has items
|
||||
*/
|
||||
hasItems(): boolean {
|
||||
return this.head !== this.tail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of elements in the queue.
|
||||
* @returns The size of the queue
|
||||
*/
|
||||
getSize(): number {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
// @implemented
|
||||
clear(): void {
|
||||
this.forEach((item, index) => delete this.q[index]);
|
||||
this.size = 0;
|
||||
this.head = this.tail;
|
||||
}
|
||||
|
||||
// @implemented
|
||||
forEach(func: (item: T, index?: number) => void): void {
|
||||
let i = this.head;
|
||||
while(i !== this.tail){
|
||||
func(this.q[i], i);
|
||||
i = (i + 1) % this.MAX_ELEMENTS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this queue into a string format
|
||||
* @returns A string representing this queue
|
||||
*/
|
||||
toString(): string {
|
||||
let retval = "";
|
||||
|
||||
this.forEach( (item, index) => {
|
||||
let str = item.toString()
|
||||
if(index !== 0){
|
||||
str += " -> "
|
||||
}
|
||||
retval = str + retval;
|
||||
});
|
||||
|
||||
return "Top -> " + retval;
|
||||
}
|
||||
}
|
271
hw3/src/Wolfie2D/DataTypes/RegionQuadTree.ts
Normal file
|
@ -0,0 +1,271 @@
|
|||
import Vec2 from "./Vec2";
|
||||
import Collection from "./Collection";
|
||||
import AABB from "./Shapes/AABB";
|
||||
import Region from "./Interfaces/Region";
|
||||
import Unique from "./Interfaces/Unique";
|
||||
import Map from "./Map";
|
||||
|
||||
/**
|
||||
* A quadtree data structure implemented to work with regions rather than points.
|
||||
* Elements in this quadtree have a position and an area, and thus can span multiple
|
||||
* quadtree branches.
|
||||
*/
|
||||
export default class QuadTree<T extends Region & Unique> implements Collection {
|
||||
/** The center of this quadtree */
|
||||
protected boundary: AABB;
|
||||
|
||||
/** The number of elements this quadtree root can hold before splitting */
|
||||
protected capacity: number;
|
||||
|
||||
/** The maximum height of the quadtree from this root */
|
||||
protected maxDepth: number;
|
||||
|
||||
/** Represents whether the quadtree is a root or a leaf */
|
||||
protected divided: boolean;
|
||||
|
||||
/** The array of the items in this quadtree */
|
||||
protected items: Array<T>;
|
||||
|
||||
// The child quadtrees of this one
|
||||
/** The top left child */
|
||||
protected nw: QuadTree<T>;
|
||||
/** The top right child */
|
||||
protected ne: QuadTree<T>;
|
||||
/** The bottom left child */
|
||||
protected sw: QuadTree<T>;
|
||||
/** The bottom right child */
|
||||
protected se: QuadTree<T>;
|
||||
|
||||
constructor(center: Vec2, size: Vec2, maxDepth?: number, capacity?: number){
|
||||
this.boundary = new AABB(center, size);
|
||||
this.maxDepth = maxDepth !== undefined ? maxDepth : 10
|
||||
this.capacity = capacity ? capacity : 10;
|
||||
|
||||
// If we're at the bottom of the tree, don't set a max size
|
||||
if(this.maxDepth === 1){
|
||||
this.capacity = Infinity;
|
||||
}
|
||||
|
||||
this.divided = false;
|
||||
this.items = new Array();
|
||||
|
||||
// Create all of the children for this quadtree if there are any
|
||||
if(this.maxDepth > 1){
|
||||
let x = this.boundary.x;
|
||||
let y = this.boundary.y;
|
||||
let hw = this.boundary.hw;
|
||||
let hh = this.boundary.hh;
|
||||
|
||||
this.nw = new QuadTree(new Vec2(x-hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity);
|
||||
this.ne = new QuadTree(new Vec2(x+hw/2, y-hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity);
|
||||
this.sw = new QuadTree(new Vec2(x-hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity);
|
||||
this.se = new QuadTree(new Vec2(x+hw/2, y+hh/2), new Vec2(hw/2, hh/2), this.maxDepth - 1, this.capacity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new item into this quadtree. Defers to children if this quadtree is divided
|
||||
* or divides the quadtree if capacity is exceeded with this add.
|
||||
* @param item The item to add to the quadtree
|
||||
*/
|
||||
insert(item: T): void {
|
||||
// If the item is inside of the bounds of this quadtree
|
||||
if(this.boundary.overlaps(item.boundary)){
|
||||
if(this.divided){
|
||||
// Defer to the children
|
||||
this.deferInsert(item);
|
||||
} else if (this.items.length < this.capacity){
|
||||
// Add to this items list
|
||||
this.items.push(item);
|
||||
} else {
|
||||
// We aren't divided, but are at capacity - divide
|
||||
this.subdivide();
|
||||
this.deferInsert(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all items at this point.
|
||||
* @param point The point to query at
|
||||
* @returns A list of all elements in the quadtree that contain the specified point
|
||||
*/
|
||||
queryPoint(point: Vec2): Array<T> {
|
||||
// A matrix to keep track of our results
|
||||
let results = new Array<T>();
|
||||
|
||||
// A map to keep track of the items we've already found
|
||||
let uniqueMap = new Map<T>();
|
||||
|
||||
// Query and return
|
||||
this._queryPoint(point, results, uniqueMap);
|
||||
return results;
|
||||
}
|
||||
|
||||
// @ignoreFunction
|
||||
/**
|
||||
* A recursive function called by queryPoint
|
||||
* @param point The point being queried
|
||||
* @param results The results matrix
|
||||
* @param uniqueMap A map that stores the unique ids of the results so we know what was already found
|
||||
*/
|
||||
protected _queryPoint(point: Vec2, results: Array<T>, uniqueMap: Map<T>): void {
|
||||
// Does this quadtree even contain the point?
|
||||
if(!this.boundary.containsPointSoft(point)) return;
|
||||
|
||||
// If the matrix is divided, ask its children for results
|
||||
if(this.divided){
|
||||
this.nw._queryPoint(point, results, uniqueMap);
|
||||
this.ne._queryPoint(point, results, uniqueMap);
|
||||
this.sw._queryPoint(point, results, uniqueMap);
|
||||
this.se._queryPoint(point, results, uniqueMap);
|
||||
} else {
|
||||
// Otherwise, return a set of the items
|
||||
for(let item of this.items){
|
||||
let id = item.id.toString();
|
||||
// If the item hasn't been found yet and it contains the point
|
||||
if(!uniqueMap.has(id) && item.boundary.containsPoint(point)){
|
||||
// Add it to our found points
|
||||
uniqueMap.add(id, item);
|
||||
results.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all items in this region
|
||||
* @param boundary The region to check
|
||||
* @param inclusionCheck Allows for additional inclusion checks to further refine searches
|
||||
* @returns A list of all elements in the specified region
|
||||
*/
|
||||
queryRegion(boundary: AABB): Array<T> {
|
||||
// A matrix to keep track of our results
|
||||
let results = new Array<T>();
|
||||
|
||||
// A map to keep track of the items we've already found
|
||||
let uniqueMap = new Array<boolean>();
|
||||
|
||||
// Query and return
|
||||
this._queryRegion(boundary, results, uniqueMap);
|
||||
return results;
|
||||
}
|
||||
|
||||
// @ignoreFunction
|
||||
/**
|
||||
* A recursive function called by queryPoint
|
||||
* @param point The point being queried
|
||||
* @param results The results matrix
|
||||
* @param uniqueMap A map that stores the unique ids of the results so we know what was already found
|
||||
*/
|
||||
protected _queryRegion(boundary: AABB, results: Array<T>, uniqueMap: Array<boolean>): void {
|
||||
// Does this quadtree even contain the point?
|
||||
if(!this.boundary.overlaps(boundary)) return;
|
||||
|
||||
// If the matrix is divided, ask its children for results
|
||||
if(this.divided){
|
||||
this.nw._queryRegion(boundary, results, uniqueMap);
|
||||
this.ne._queryRegion(boundary, results, uniqueMap);
|
||||
this.sw._queryRegion(boundary, results, uniqueMap);
|
||||
this.se._queryRegion(boundary, results, uniqueMap);
|
||||
} else {
|
||||
// Otherwise, return a set of the items
|
||||
for(let item of this.items){
|
||||
// TODO - This is REALLY slow for some reason when we check for unique keys
|
||||
|
||||
// let id = item.getId().toString();
|
||||
// // If the item hasn't been found yet and it contains the point
|
||||
// if(!uniqueMap.has(id) && item.getBoundary().overlaps(boundary)){
|
||||
// // Add it to our found points
|
||||
// uniqueMap.add(id, item);
|
||||
// results.push(item);
|
||||
// }
|
||||
|
||||
// Maybe this is better? Just use a boolean array with no string nonsense?
|
||||
if(item.id >= uniqueMap.length || !uniqueMap[item.id]){
|
||||
if(item.boundary.overlaps(boundary)){
|
||||
results.push(item);
|
||||
uniqueMap[item.id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides this quadtree up into 4 smaller ones - called through insert.
|
||||
*/
|
||||
protected subdivide(): void {
|
||||
this.divided = true;
|
||||
this.distributeItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Distributes the items of this quadtree into its children.
|
||||
*/
|
||||
protected distributeItems(): void {
|
||||
this.items.forEach(item => this.deferInsert(item));
|
||||
|
||||
// Delete the items from this array
|
||||
this.items.forEach((item, index) => delete this.items[index]);
|
||||
this.items.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defers this insertion to the children of this quadtree
|
||||
* @param item The item to insert
|
||||
*/
|
||||
protected deferInsert(item: T): void {
|
||||
this.nw.insert(item);
|
||||
this.ne.insert(item);
|
||||
this.sw.insert(item);
|
||||
this.se.insert(item);
|
||||
}
|
||||
|
||||
public render_demo(ctx: CanvasRenderingContext2D, origin: Vec2, zoom: number): void {
|
||||
ctx.strokeStyle = "#0000FF";
|
||||
ctx.strokeRect((this.boundary.x - this.boundary.hw - origin.x)*zoom, (this.boundary.y - this.boundary.hh - origin.y)*zoom, 2*this.boundary.hw*zoom, 2*this.boundary.hh*zoom);
|
||||
|
||||
if(this.divided){
|
||||
this.nw.render_demo(ctx, origin, zoom);
|
||||
this.ne.render_demo(ctx, origin, zoom);
|
||||
this.sw.render_demo(ctx, origin, zoom);
|
||||
this.se.render_demo(ctx, origin, zoom);
|
||||
}
|
||||
}
|
||||
|
||||
// @implemented
|
||||
forEach(func: Function): void {
|
||||
// If divided, send the call down
|
||||
if(this.divided){
|
||||
this.nw.forEach(func);
|
||||
this.ne.forEach(func);
|
||||
this.sw.forEach(func);
|
||||
this.se.forEach(func);
|
||||
} else {
|
||||
// Otherwise, iterate over items
|
||||
for(let i = 0; i < this.items.length; i++){
|
||||
func(this.items[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @implemented
|
||||
clear(): void {
|
||||
if(this.nw){
|
||||
this.nw.clear();
|
||||
this.ne.clear();
|
||||
this.sw.clear();
|
||||
this.se.clear();
|
||||
}
|
||||
|
||||
for(let item in this.items){
|
||||
delete this.items[item];
|
||||
}
|
||||
|
||||
this.items.length = 0;
|
||||
|
||||
this.divided = false;
|
||||
}
|
||||
|
||||
}
|
5
hw3/src/Wolfie2D/DataTypes/Rendering/WebGLGameTexture.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default class WebGLGameTexture {
|
||||
webGLTextureId: number;
|
||||
webGLTexture: WebGLTexture;
|
||||
imageKey: string;
|
||||
}
|
29
hw3/src/Wolfie2D/DataTypes/Rendering/WebGLProgramType.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/** A container for info about a webGL shader program */
|
||||
export default class WebGLProgramType {
|
||||
/** A webGL program */
|
||||
program: WebGLProgram;
|
||||
|
||||
/** A vertex shader */
|
||||
vertexShader: WebGLShader;
|
||||
|
||||
/** A fragment shader */
|
||||
fragmentShader: WebGLShader;
|
||||
|
||||
/**
|
||||
* Deletes this shader program
|
||||
*/
|
||||
delete(gl: WebGLRenderingContext): void {
|
||||
// Clean up all aspects of this program
|
||||
if(this.program){
|
||||
gl.deleteProgram(this.program);
|
||||
}
|
||||
|
||||
if(this.vertexShader){
|
||||
gl.deleteShader(this.vertexShader);
|
||||
}
|
||||
|
||||
if(this.fragmentShader){
|
||||
gl.deleteShader(this.fragmentShader);
|
||||
}
|
||||
}
|
||||
}
|
339
hw3/src/Wolfie2D/DataTypes/Shapes/AABB.ts
Normal file
|
@ -0,0 +1,339 @@
|
|||
import Shape from "./Shape";
|
||||
import Vec2 from "../Vec2";
|
||||
import MathUtils from "../../Utils/MathUtils";
|
||||
import Circle from "./Circle";
|
||||
import Hit from "../Physics/Hit";
|
||||
|
||||
/**
|
||||
* An Axis-Aligned Bounding Box. In other words, a rectangle that is always aligned to the x-y grid.
|
||||
* Inspired by the helpful collision documentation @link(here)(https://noonat.github.io/intersect/).
|
||||
*/
|
||||
export default class AABB extends Shape {
|
||||
center: Vec2;
|
||||
halfSize: Vec2;
|
||||
|
||||
/**
|
||||
* Creates a new AABB
|
||||
* @param center The center of the AABB
|
||||
* @param halfSize The half size of the AABB - The distance from the center to an edge in x and y
|
||||
*/
|
||||
constructor(center?: Vec2, halfSize?: Vec2){
|
||||
super();
|
||||
this.center = center ? center : new Vec2(0, 0);
|
||||
this.halfSize = halfSize ? halfSize : new Vec2(0, 0);
|
||||
}
|
||||
|
||||
/** Returns a point representing the top left corner of the AABB */
|
||||
get topLeft(): Vec2 {
|
||||
return new Vec2(this.left, this.top)
|
||||
}
|
||||
|
||||
/** Returns a point representing the top right corner of the AABB */
|
||||
get topRight(): Vec2 {
|
||||
return new Vec2(this.right, this.top)
|
||||
}
|
||||
|
||||
/** Returns a point representing the bottom left corner of the AABB */
|
||||
get bottomLeft(): Vec2 {
|
||||
return new Vec2(this.left, this.bottom)
|
||||
}
|
||||
|
||||
/** Returns a point representing the bottom right corner of the AABB */
|
||||
get bottomRight(): Vec2 {
|
||||
return new Vec2(this.right, this.bottom)
|
||||
}
|
||||
|
||||
// @override
|
||||
getBoundingRect(): AABB {
|
||||
return this.clone();
|
||||
}
|
||||
|
||||
// @override
|
||||
getBoundingCircle(): Circle {
|
||||
let r = Math.max(this.hw, this.hh)
|
||||
return new Circle(this.center.clone(), r);
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
getHalfSize(): Vec2 {
|
||||
return this.halfSize;
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
setHalfSize(halfSize: Vec2): void {
|
||||
this.halfSize = halfSize;
|
||||
}
|
||||
|
||||
// TODO - move these all to the Shape class
|
||||
/**
|
||||
* A simple boolean check of whether this AABB contains a point
|
||||
* @param point The point to check
|
||||
* @returns A boolean representing whether this AABB contains the specified point
|
||||
*/
|
||||
containsPoint(point: Vec2): boolean {
|
||||
return point.x >= this.x - this.hw && point.x <= this.x + this.hw
|
||||
&& point.y >= this.y - this.hh && point.y <= this.y + this.hh
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple boolean check of whether this AABB contains a point
|
||||
* @param point The point to check
|
||||
* @returns A boolean representing whether this AABB contains the specified point
|
||||
*/
|
||||
intersectPoint(point: Vec2): boolean {
|
||||
let dx = point.x - this.x;
|
||||
let px = this.hw - Math.abs(dx);
|
||||
|
||||
if(px <= 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
let dy = point.y - this.y;
|
||||
let py = this.hh - Math.abs(dy);
|
||||
|
||||
if(py <= 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A boolean check of whether this AABB contains a point with soft left and top boundaries.
|
||||
* In other words, if the top left is (0, 0), the point (0, 0) is not in the AABB
|
||||
* @param point The point to check
|
||||
* @returns A boolean representing whether this AABB contains the specified point
|
||||
*/
|
||||
containsPointSoft(point: Vec2): boolean {
|
||||
return point.x > this.x - this.hw && point.x <= this.x + this.hw
|
||||
&& point.y > this.y - this.hh && point.y <= this.y + this.hh
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the data from the intersection of this AABB with a line segment from a point in a direction
|
||||
* @param point The point that the line segment starts from
|
||||
* @param delta The direction and distance of the segment
|
||||
* @param padding Pads the AABB to make it wider for the intersection test
|
||||
* @returns The Hit object representing the intersection, or null if there was no intersection
|
||||
*/
|
||||
intersectSegment(point: Vec2, delta: Vec2, padding?: Vec2): Hit {
|
||||
let paddingX = padding ? padding.x : 0;
|
||||
let paddingY = padding ? padding.y : 0;
|
||||
|
||||
let scaleX = 1/delta.x;
|
||||
let scaleY = 1/delta.y;
|
||||
|
||||
let signX = MathUtils.sign(scaleX);
|
||||
let signY = MathUtils.sign(scaleY);
|
||||
|
||||
let tnearx = scaleX*(this.x - signX*(this.hw + paddingX) - point.x);
|
||||
let tneary = scaleY*(this.y - signY*(this.hh + paddingY) - point.y);
|
||||
let tfarx = scaleX*(this.x + signX*(this.hw + paddingX) - point.x);
|
||||
let tfary = scaleY*(this.y + signY*(this.hh + paddingY) - point.y);
|
||||
|
||||
if(tnearx > tfary || tneary > tfarx){
|
||||
// We aren't colliding - we clear one axis before intersecting another
|
||||
return null;
|
||||
}
|
||||
|
||||
let tnear = Math.max(tnearx, tneary);
|
||||
|
||||
// Double check for NaNs
|
||||
if(tnearx !== tnearx){
|
||||
tnear = tneary;
|
||||
} else if (tneary !== tneary){
|
||||
tnear = tnearx;
|
||||
}
|
||||
|
||||
let tfar = Math.min(tfarx, tfary);
|
||||
|
||||
if(tnear === -Infinity){
|
||||
return null;
|
||||
}
|
||||
|
||||
if(tnear >= 1 || tfar <= 0){
|
||||
return null;
|
||||
}
|
||||
|
||||
// We are colliding
|
||||
let hit = new Hit();
|
||||
hit.time = MathUtils.clamp01(tnear);
|
||||
hit.nearTimes.x = tnearx;
|
||||
hit.nearTimes.y = tneary;
|
||||
|
||||
if(tnearx > tneary){
|
||||
// We hit on the left or right size
|
||||
hit.normal.x = -signX;
|
||||
hit.normal.y = 0;
|
||||
} else if(Math.abs(tnearx - tneary) < 0.0001){
|
||||
// We hit on the corner
|
||||
hit.normal.x = -signX;
|
||||
hit.normal.y = -signY;
|
||||
hit.normal.normalize();
|
||||
} else {
|
||||
// We hit on the top or bottom
|
||||
hit.normal.x = 0;
|
||||
hit.normal.y = -signY;
|
||||
}
|
||||
|
||||
hit.delta.x = (1.0 - hit.time) * -delta.x;
|
||||
hit.delta.y = (1.0 - hit.time) * -delta.y;
|
||||
hit.pos.x = point.x + delta.x * hit.time;
|
||||
hit.pos.y = point.y + delta.y * hit.time;
|
||||
|
||||
return hit;
|
||||
}
|
||||
|
||||
// @override
|
||||
overlaps(other: Shape): boolean {
|
||||
if(other instanceof AABB){
|
||||
return this.overlapsAABB(other);
|
||||
}
|
||||
throw "Overlap not defined between these shapes."
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple boolean check of whether this AABB overlaps another
|
||||
* @param other The other AABB to check against
|
||||
* @returns True if this AABB overlaps the other, false otherwise
|
||||
*/
|
||||
protected overlapsAABB(other: AABB): boolean {
|
||||
let dx = other.x - this.x;
|
||||
let px = this.hw + other.hw - Math.abs(dx);
|
||||
|
||||
if(px <= 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
let dy = other.y - this.y;
|
||||
let py = this.hh + other.hh - Math.abs(dy);
|
||||
|
||||
if(py <= 0){
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether these AABBs are JUST touching - not overlapping.
|
||||
* Vec2.x is -1 if the other is to the left, 1 if to the right.
|
||||
* Likewise, Vec2.y is -1 if the other is on top, 1 if on bottom.
|
||||
* @param other The other AABB to check
|
||||
* @returns The collision sides stored in a Vec2 if the AABBs are touching, null otherwise
|
||||
*/
|
||||
touchesAABB(other: AABB): Vec2 {
|
||||
let dx = other.x - this.x;
|
||||
let px = this.hw + other.hw - Math.abs(dx);
|
||||
|
||||
let dy = other.y - this.y;
|
||||
let py = this.hh + other.hh - Math.abs(dy);
|
||||
|
||||
// If one axis is just touching and the other is overlapping, true
|
||||
if((px === 0 && py >= 0) || (py === 0 && px >= 0)){
|
||||
let ret = new Vec2();
|
||||
|
||||
if(px === 0){
|
||||
ret.x = other.x < this.x ? -1 : 1;
|
||||
}
|
||||
|
||||
if(py === 0){
|
||||
ret.y = other.y < this.y ? -1 : 1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether these AABBs are JUST touching - not overlapping.
|
||||
* Also, if they are only touching corners, they are considered not touching.
|
||||
* Vec2.x is -1 if the other is to the left, 1 if to the right.
|
||||
* Likewise, Vec2.y is -1 if the other is on top, 1 if on bottom.
|
||||
* @param other The other AABB to check
|
||||
* @returns The side of the touch, stored as a Vec2, or null if there is no touch
|
||||
*/
|
||||
touchesAABBWithoutCorners(other: AABB): Vec2 {
|
||||
let dx = other.x - this.x;
|
||||
let px = this.hw + other.hw - Math.abs(dx);
|
||||
|
||||
let dy = other.y - this.y;
|
||||
let py = this.hh + other.hh - Math.abs(dy);
|
||||
|
||||
// If one axis is touching, and the other is strictly overlapping
|
||||
if((px === 0 && py > 0) || (py === 0 && px > 0)){
|
||||
let ret = new Vec2();
|
||||
|
||||
if(px === 0){
|
||||
ret.x = other.x < this.x ? -1 : 1;
|
||||
} else {
|
||||
ret.y = other.y < this.y ? -1 : 1;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the area of the overlap between this AABB and another
|
||||
* @param other The other AABB
|
||||
* @returns The area of the overlap between the AABBs
|
||||
*/
|
||||
overlapArea(other: AABB): number {
|
||||
let leftx = Math.max(this.x - this.hw, other.x - other.hw);
|
||||
let rightx = Math.min(this.x + this.hw, other.x + other.hw);
|
||||
let dx = rightx - leftx;
|
||||
|
||||
let lefty = Math.max(this.y - this.hh, other.y - other.hh);
|
||||
let righty = Math.min(this.y + this.hh, other.y + other.hh);
|
||||
let dy = righty - lefty;
|
||||
|
||||
if(dx < 0 || dy < 0) return 0;
|
||||
|
||||
return dx*dy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves and resizes this rect from its current position to the position specified
|
||||
* @param velocity The movement of the rect from its position
|
||||
* @param fromPosition A position specified to be the starting point of sweeping
|
||||
* @param halfSize The halfSize of the sweeping rect
|
||||
*/
|
||||
sweep(velocity: Vec2, fromPosition?: Vec2, halfSize?: Vec2): void {
|
||||
if(!fromPosition){
|
||||
fromPosition = this.center;
|
||||
}
|
||||
|
||||
if(!halfSize){
|
||||
halfSize = this.halfSize;
|
||||
}
|
||||
|
||||
let centerX = fromPosition.x + velocity.x/2;
|
||||
let centerY = fromPosition.y + velocity.y/2;
|
||||
|
||||
let minX = Math.min(fromPosition.x - halfSize.x, fromPosition.x + velocity.x - halfSize.x);
|
||||
let minY = Math.min(fromPosition.y - halfSize.y, fromPosition.y + velocity.y - halfSize.y);
|
||||
|
||||
this.center.set(centerX, centerY);
|
||||
this.halfSize.set(centerX - minX, centerY - minY);
|
||||
}
|
||||
|
||||
// @override
|
||||
clone(): AABB {
|
||||
return new AABB(this.center.clone(), this.halfSize.clone());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this AABB to a string format
|
||||
* @returns (center: (x, y), halfSize: (x, y))
|
||||
*/
|
||||
toString(): string {
|
||||
return "(center: " + this.center.toString() + ", half-size: " + this.halfSize.toString() + ")"
|
||||
}
|
||||
}
|
76
hw3/src/Wolfie2D/DataTypes/Shapes/Circle.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import Vec2 from "../Vec2";
|
||||
import AABB from "./AABB";
|
||||
import Shape from "./Shape";
|
||||
|
||||
/**
|
||||
* A Circle
|
||||
*/
|
||||
export default class Circle extends Shape {
|
||||
private _center: Vec2;
|
||||
radius: number;
|
||||
|
||||
/**
|
||||
* Creates a new Circle
|
||||
* @param center The center of the circle
|
||||
* @param radius The radius of the circle
|
||||
*/
|
||||
constructor(center: Vec2, radius: number) {
|
||||
super();
|
||||
this._center = center ? center : new Vec2(0, 0);
|
||||
this.radius = radius ? radius : 0;
|
||||
}
|
||||
|
||||
get center(): Vec2 {
|
||||
return this._center;
|
||||
}
|
||||
|
||||
set center(center: Vec2) {
|
||||
this._center = center;
|
||||
}
|
||||
|
||||
get halfSize(): Vec2 {
|
||||
return new Vec2(this.radius, this.radius);
|
||||
}
|
||||
|
||||
get r(): number {
|
||||
return this.radius;
|
||||
}
|
||||
|
||||
set r(radius: number) {
|
||||
this.radius = radius;
|
||||
}
|
||||
|
||||
// @override
|
||||
/**
|
||||
* A simple boolean check of whether this AABB contains a point
|
||||
* @param point The point to check
|
||||
* @returns A boolean representing whether this AABB contains the specified point
|
||||
*/
|
||||
containsPoint(point: Vec2): boolean {
|
||||
return this.center.distanceSqTo(point) <= this.radius*this.radius;
|
||||
}
|
||||
|
||||
// @override
|
||||
getBoundingRect(): AABB {
|
||||
return new AABB(this._center.clone(), new Vec2(this.radius, this.radius));
|
||||
}
|
||||
|
||||
// @override
|
||||
getBoundingCircle(): Circle {
|
||||
return this.clone();
|
||||
}
|
||||
|
||||
// @override
|
||||
overlaps(other: Shape): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
// @override
|
||||
clone(): Circle {
|
||||
return new Circle(this._center.clone(), this.radius);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return "(center: " + this.center.toString() + ", radius: " + this.radius + ")";
|
||||
}
|
||||
}
|
169
hw3/src/Wolfie2D/DataTypes/Shapes/Shape.ts
Normal file
|
@ -0,0 +1,169 @@
|
|||
import Vec2 from "../Vec2";
|
||||
import AABB from "./AABB";
|
||||
import Circle from "./Circle";
|
||||
|
||||
/**
|
||||
* An abstract Shape class that acts as an interface for better interactions with subclasses.
|
||||
*/
|
||||
export default abstract class Shape {
|
||||
abstract get center(): Vec2;
|
||||
|
||||
abstract set center(center: Vec2);
|
||||
|
||||
abstract get halfSize(): Vec2;
|
||||
|
||||
get x(): number {
|
||||
return this.center.x;
|
||||
}
|
||||
|
||||
get y(): number {
|
||||
return this.center.y;
|
||||
}
|
||||
|
||||
get hw(): number {
|
||||
return this.halfSize.x;
|
||||
}
|
||||
|
||||
get hh(): number {
|
||||
return this.halfSize.y;
|
||||
}
|
||||
|
||||
get top(): number {
|
||||
return this.y - this.hh;
|
||||
}
|
||||
|
||||
get bottom(): number {
|
||||
return this.y + this.hh;
|
||||
}
|
||||
|
||||
get left(): number {
|
||||
return this.x - this.hw;
|
||||
}
|
||||
|
||||
get right(): number {
|
||||
return this.x + this.hw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a bounding rectangle for this shape. Warning - may be the same as this Shape.
|
||||
* For instance, the bounding circle of an AABB is itself. Use clone() if you need a new shape.
|
||||
* @returns An AABB that bounds this shape
|
||||
*/
|
||||
abstract getBoundingRect(): AABB;
|
||||
|
||||
/**
|
||||
* Gets a bounding circle for this shape. Warning - may be the same as this Shape.
|
||||
* For instance, the bounding circle of a Circle is itself. Use clone() if you need a new shape.
|
||||
* @returns A Circle that bounds this shape
|
||||
*/
|
||||
abstract getBoundingCircle(): Circle;
|
||||
|
||||
/**
|
||||
* Returns a copy of this Shape
|
||||
* @returns A new copy of this shape
|
||||
*/
|
||||
abstract clone(): Shape;
|
||||
|
||||
/**
|
||||
* Checks if this shape overlaps another
|
||||
* @param other The other shape to check against
|
||||
* @returns a boolean that represents whether this Shape overlaps the other one
|
||||
*/
|
||||
abstract overlaps(other: Shape): boolean;
|
||||
|
||||
/**
|
||||
* A simple boolean check of whether this Shape contains a point
|
||||
* @param point The point to check
|
||||
* @returns A boolean representing whether this Shape contains the specified point
|
||||
*/
|
||||
abstract containsPoint(point: Vec2): boolean;
|
||||
|
||||
static getTimeOfCollision(A: Shape, velA: Vec2, B: Shape, velB: Vec2): [Vec2, Vec2, boolean, boolean] {
|
||||
if(A instanceof AABB && B instanceof AABB){
|
||||
return Shape.getTimeOfCollision_AABB_AABB(A, velA, B, velB);
|
||||
}
|
||||
}
|
||||
|
||||
private static getTimeOfCollision_AABB_AABB(A: AABB, velA: Vec2, B: Shape, velB: Vec2): [Vec2, Vec2, boolean, boolean] {
|
||||
let posSmaller = A.center;
|
||||
let posLarger = B.center;
|
||||
|
||||
let sizeSmaller = A.halfSize;
|
||||
let sizeLarger = B.halfSize;
|
||||
|
||||
let firstContact = new Vec2(0, 0);
|
||||
let lastContact = new Vec2(0, 0);
|
||||
|
||||
let collidingX = false;
|
||||
let collidingY = false;
|
||||
|
||||
// Sort by position
|
||||
if(posLarger.x < posSmaller.x){
|
||||
// Swap, because smaller is further right than larger
|
||||
let temp: Vec2;
|
||||
temp = sizeSmaller;
|
||||
sizeSmaller = sizeLarger;
|
||||
sizeLarger = temp;
|
||||
|
||||
temp = posSmaller;
|
||||
posSmaller = posLarger;
|
||||
posLarger = temp;
|
||||
|
||||
temp = velA;
|
||||
velA = velB;
|
||||
velB = temp;
|
||||
}
|
||||
|
||||
// A is left, B is right
|
||||
firstContact.x = Infinity;
|
||||
lastContact.x = Infinity;
|
||||
|
||||
if (posLarger.x - sizeLarger.x >= posSmaller.x + sizeSmaller.x){
|
||||
// If we aren't currently colliding
|
||||
let relVel = velA.x - velB.x;
|
||||
|
||||
if(relVel > 0){
|
||||
// If they are moving towards each other
|
||||
firstContact.x = ((posLarger.x - sizeLarger.x) - (posSmaller.x + sizeSmaller.x))/(relVel);
|
||||
lastContact.x = ((posLarger.x + sizeLarger.x) - (posSmaller.x - sizeSmaller.x))/(relVel);
|
||||
}
|
||||
} else {
|
||||
collidingX = true;
|
||||
}
|
||||
|
||||
if(posLarger.y < posSmaller.y){
|
||||
// Swap, because smaller is further up than larger
|
||||
let temp: Vec2;
|
||||
temp = sizeSmaller;
|
||||
sizeSmaller = sizeLarger;
|
||||
sizeLarger = temp;
|
||||
|
||||
temp = posSmaller;
|
||||
posSmaller = posLarger;
|
||||
posLarger = temp;
|
||||
|
||||
temp = velA;
|
||||
velA = velB;
|
||||
velB = temp;
|
||||
}
|
||||
|
||||
// A is top, B is bottom
|
||||
firstContact.y = Infinity;
|
||||
lastContact.y = Infinity;
|
||||
|
||||
if (posLarger.y - sizeLarger.y >= posSmaller.y + sizeSmaller.y){
|
||||
// If we aren't currently colliding
|
||||
let relVel = velA.y - velB.y;
|
||||
|
||||
if(relVel > 0){
|
||||
// If they are moving towards each other
|
||||
firstContact.y = ((posLarger.y - sizeLarger.y) - (posSmaller.y + sizeSmaller.y))/(relVel);
|
||||
lastContact.y = ((posLarger.y + sizeLarger.y) - (posSmaller.y - sizeSmaller.y))/(relVel);
|
||||
}
|
||||
} else {
|
||||
collidingY = true;
|
||||
}
|
||||
|
||||
return [firstContact, lastContact, collidingX, collidingY];
|
||||
}
|
||||
}
|
22
hw3/src/Wolfie2D/DataTypes/Spritesheet.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { AnimationData } from "../Rendering/Animations/AnimationTypes";
|
||||
|
||||
/** A class representing data contained in a spritesheet.
|
||||
* Spritesheets are the images associated with sprites, and contain images indexed in a grid, which
|
||||
* correspond to animations.
|
||||
*/
|
||||
export default class Spritesheet {
|
||||
/** The name of the spritesheet */
|
||||
name: string;
|
||||
/** The image key of the spritesheet */
|
||||
spriteSheetImage: string;
|
||||
/** The width of the sprite */
|
||||
spriteWidth: number;
|
||||
/** The height of the sprite */
|
||||
spriteHeight: number;
|
||||
/** The number of columns in the spritesheet */
|
||||
columns: number;
|
||||
/** The number of rows in the spritesheet */
|
||||
rows: number;
|
||||
/** An array of the animations associated with this spritesheet */
|
||||
animations: Array<AnimationData>;
|
||||
}
|
108
hw3/src/Wolfie2D/DataTypes/Stack.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
import Collection from "./Collection";
|
||||
|
||||
/**
|
||||
* A LIFO stack with items of type T
|
||||
*/
|
||||
export default class Stack<T> implements Collection {
|
||||
/** The maximum number of elements in the Stack */
|
||||
private MAX_ELEMENTS: number;
|
||||
|
||||
/** The internal representation of the stack */
|
||||
private stack: Array<T>;
|
||||
|
||||
/** The head of the stack */
|
||||
private head: number;
|
||||
|
||||
/**
|
||||
* Constructs a new stack
|
||||
* @param maxElements The maximum size of the stack
|
||||
*/
|
||||
constructor(maxElements: number = 100){
|
||||
this.MAX_ELEMENTS = maxElements;
|
||||
this.stack = new Array<T>(this.MAX_ELEMENTS);
|
||||
this.head = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to the top of the stack
|
||||
* @param item The new item to add to the stack
|
||||
*/
|
||||
push(item: T): void {
|
||||
if(this.head + 1 === this.MAX_ELEMENTS){
|
||||
throw "Stack full - cannot add element";
|
||||
}
|
||||
this.head += 1;
|
||||
this.stack[this.head] = item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the top of the stack
|
||||
* @returns The item at the top of the stack
|
||||
*/
|
||||
pop(): T {
|
||||
if(this.head === -1){
|
||||
throw "Stack empty - cannot remove element";
|
||||
}
|
||||
this.head -= 1;
|
||||
return this.stack[this.head + 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element currently at the top of the stack
|
||||
* @returns The item at the top of the stack
|
||||
*/
|
||||
peek(): T {
|
||||
if(this.head === -1){
|
||||
throw "Stack empty - cannot get element";
|
||||
}
|
||||
return this.stack[this.head];
|
||||
}
|
||||
|
||||
/** Returns true if this stack is empty
|
||||
* @returns A boolean that represents whether or not the stack is empty
|
||||
*/
|
||||
isEmpty(): boolean {
|
||||
return this.head === -1;
|
||||
}
|
||||
|
||||
// @implemented
|
||||
clear(): void {
|
||||
this.forEach((item, index) => delete this.stack[index]);
|
||||
this.head = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of items currently in the stack
|
||||
* @returns The number of items in the stack
|
||||
*/
|
||||
size(): number {
|
||||
return this.head + 1;
|
||||
}
|
||||
|
||||
// @implemented
|
||||
forEach(func: (item: T, index?: number) => void): void{
|
||||
let i = 0;
|
||||
while(i <= this.head){
|
||||
func(this.stack[i], i);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this stack into a string format
|
||||
* @returns A string representing this stack
|
||||
*/
|
||||
toString(): string {
|
||||
let retval = "";
|
||||
|
||||
this.forEach( (item, index) => {
|
||||
let str = item.toString()
|
||||
if(index !== 0){
|
||||
str += " -> "
|
||||
}
|
||||
retval = str + retval;
|
||||
});
|
||||
|
||||
return "Top -> " + retval;
|
||||
}
|
||||
}
|
54
hw3/src/Wolfie2D/DataTypes/State/State.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import Emitter from "../../Events/Emitter";
|
||||
import GameEvent from "../../Events/GameEvent";
|
||||
import Updateable from "../Interfaces/Updateable";
|
||||
import StateMachine from "./StateMachine";
|
||||
|
||||
/**
|
||||
* An abstract implementation of a state for a @reference[StateMachine].
|
||||
* This class should be extended to allow for custom state behaviors.
|
||||
*/
|
||||
export default abstract class State implements Updateable {
|
||||
/** The StateMachine that uses this State */
|
||||
protected parent: StateMachine;
|
||||
|
||||
/** An event emitter */
|
||||
protected emitter: Emitter;
|
||||
|
||||
/**
|
||||
* Constructs a new State
|
||||
* @param parent The parent StateMachine of this state
|
||||
*/
|
||||
constructor(parent: StateMachine) {
|
||||
this.parent = parent;
|
||||
this.emitter = new Emitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* A method that is called when this state is entered. Use this to initialize any variables before updates occur.
|
||||
* @param options Information to pass to this state
|
||||
*/
|
||||
abstract onEnter(options: Record<string, any>): void;
|
||||
|
||||
/**
|
||||
* A lifecycle method that handles an input event, such as taking damage.
|
||||
* @param event The GameEvent to process
|
||||
*/
|
||||
abstract handleInput(event: GameEvent): void;
|
||||
|
||||
// @implemented
|
||||
abstract update(deltaT: number): void;
|
||||
|
||||
/**
|
||||
* Tells the state machine that this state has ended, and makes it transition to the new state specified
|
||||
* @param stateName The name of the state to transition to
|
||||
*/
|
||||
protected finished(stateName: string): void {
|
||||
this.parent.changeState(stateName);
|
||||
}
|
||||
|
||||
/**
|
||||
* A lifecycle method is called when the state is ending.
|
||||
* @returns info to pass to the next state
|
||||
*/
|
||||
abstract onExit(): Record<string, any>;
|
||||
}
|
137
hw3/src/Wolfie2D/DataTypes/State/StateMachine.ts
Normal file
|
@ -0,0 +1,137 @@
|
|||
import Stack from "../Stack";
|
||||
import State from "./State";
|
||||
import Map from "../Map";
|
||||
import GameEvent from "../../Events/GameEvent";
|
||||
import Receiver from "../../Events/Receiver";
|
||||
import Emitter from "../../Events/Emitter";
|
||||
import Updateable from "../Interfaces/Updateable";
|
||||
|
||||
/**
|
||||
* An implementation of a Push Down Automata State machine. States can also be hierarchical
|
||||
* for more flexibility, as described in @link(Game Programming Patterns)(https://gameprogrammingpatterns.com/state.html).
|
||||
*/
|
||||
export default class StateMachine implements Updateable {
|
||||
/** A stack of the current states */
|
||||
protected stack: Stack<State>;
|
||||
/** A mape of state keys to actual state instances */
|
||||
protected stateMap: Map<State>;
|
||||
/** The current state */
|
||||
protected currentState: State;
|
||||
/** An event receiver */
|
||||
protected receiver: Receiver;
|
||||
/** An event emitter */
|
||||
protected emitter: Emitter;
|
||||
/** A boolean representing whether or not this StateMachine is currently active */
|
||||
protected active: boolean;
|
||||
/** A boolean representing whether or not this StateMachine should emit an event on state change */
|
||||
protected emitEventOnStateChange: boolean;
|
||||
/** The name of the event to be emitted on state change */
|
||||
protected stateChangeEventName: string;
|
||||
|
||||
/**
|
||||
* Creates a new StateMachine
|
||||
*/
|
||||
constructor(){
|
||||
this.stack = new Stack();
|
||||
this.stateMap = new Map();
|
||||
this.receiver = new Receiver();
|
||||
this.emitter = new Emitter();
|
||||
this.emitEventOnStateChange = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the activity state of this state machine
|
||||
* @param flag True if you want to set this machine running, false otherwise
|
||||
*/
|
||||
setActive(flag: boolean): void {
|
||||
this.active = flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes this state machine emit an event any time its state changes
|
||||
* @param stateChangeEventName The name of the event to emit
|
||||
*/
|
||||
setEmitEventOnStateChange(stateChangeEventName: string): void {
|
||||
this.emitEventOnStateChange = true;
|
||||
this.stateChangeEventName = stateChangeEventName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops this state machine from emitting events on state change.
|
||||
*/
|
||||
cancelEmitEventOnStateChange(): void {
|
||||
this.emitEventOnStateChange = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this state machine with an initial state and sets it running
|
||||
* @param initialState The name of initial state of the state machine
|
||||
*/
|
||||
initialize(initialState: string, options: Record<string, any> = {}): void {
|
||||
this.stack.push(this.stateMap.get(initialState));
|
||||
this.currentState = this.stack.peek();
|
||||
this.currentState.onEnter(options);
|
||||
this.setActive(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a state to this state machine
|
||||
* @param stateName The name of the state to add
|
||||
* @param state The state to add
|
||||
*/
|
||||
addState(stateName: string, state: State): void {
|
||||
this.stateMap.add(stateName, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the state of this state machine to the provided string
|
||||
* @param state The string name of the state to change to
|
||||
*/
|
||||
changeState(state: string): void {
|
||||
// Exit the current state
|
||||
let options = this.currentState.onExit();
|
||||
|
||||
// Make sure the correct state is at the top of the stack
|
||||
if(state === "previous"){
|
||||
// Pop the current state off the stack
|
||||
this.stack.pop();
|
||||
} else {
|
||||
// Retrieve the new state from the statemap and put it at the top of the stack
|
||||
this.stack.pop();
|
||||
this.stack.push(this.stateMap.get(state));
|
||||
}
|
||||
|
||||
// Retreive the new state from the stack
|
||||
this.currentState = this.stack.peek();
|
||||
|
||||
// Emit an event if turned on
|
||||
if(this.emitEventOnStateChange){
|
||||
this.emitter.fireEvent(this.stateChangeEventName, {state: this.currentState});
|
||||
}
|
||||
|
||||
// Enter the new state
|
||||
this.currentState.onEnter(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles input. This happens at the very beginning of this state machine's update cycle.
|
||||
* @param event The game event to process
|
||||
*/
|
||||
handleEvent(event: GameEvent): void {
|
||||
if(this.active){
|
||||
this.currentState.handleInput(event);
|
||||
}
|
||||
}
|
||||
|
||||
// @implemented
|
||||
update(deltaT: number): void {
|
||||
// Distribute events
|
||||
while(this.receiver.hasNextEvent()){
|
||||
let event = this.receiver.getNextEvent();
|
||||
this.handleEvent(event);
|
||||
}
|
||||
|
||||
// Delegate the update to the current state
|
||||
this.currentState.update(deltaT);
|
||||
}
|
||||
}
|
78
hw3/src/Wolfie2D/DataTypes/Tilesets/TiledData.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
// @ignorePage
|
||||
/**
|
||||
* a representation of Tiled's tilemap data
|
||||
*/
|
||||
export class TiledTilemapData {
|
||||
height: number;
|
||||
width: number;
|
||||
tileheight: number;
|
||||
tilewidth: number;
|
||||
orientation: string;
|
||||
layers: Array<TiledLayerData>;
|
||||
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;
|
||||
tileheight: number;
|
||||
tilecount: number;
|
||||
firstgid: number;
|
||||
imageheight: number;
|
||||
imagewidth: number;
|
||||
margin: number;
|
||||
spacing: number;
|
||||
name: string;
|
||||
image: string;
|
||||
tiles: Array<TiledCollectionTile>
|
||||
}
|
||||
|
||||
/**
|
||||
* A representation of a layer in a Tiled tilemap
|
||||
*/
|
||||
export class TiledLayerData {
|
||||
data: number[];
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
name: string;
|
||||
opacity: number;
|
||||
visible: boolean;
|
||||
properties: TiledLayerProperty[];
|
||||
type: string;
|
||||
objects: Array<TiledObject>;
|
||||
}
|
||||
|
||||
export class TiledObject {
|
||||
gid: number;
|
||||
height: number;
|
||||
width: number;
|
||||
id: number;
|
||||
name: string;;
|
||||
properties: Array<TiledLayerProperty>;
|
||||
rotation: number;
|
||||
type: string;
|
||||
visible: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export class TiledCollectionTile {
|
||||
id: number;
|
||||
image: string;
|
||||
imageheight: number;
|
||||
imagewidth: number;
|
||||
}
|
146
hw3/src/Wolfie2D/DataTypes/Tilesets/Tileset.ts
Normal file
|
@ -0,0 +1,146 @@
|
|||
import ResourceManager from "../../ResourceManager/ResourceManager";
|
||||
import Vec2 from "../Vec2";
|
||||
import { TiledTilesetData } from "./TiledData";
|
||||
|
||||
/**
|
||||
* The data representation of a Tileset for the game engine. This represents one image,
|
||||
* with a startIndex if required (as it is with Tiled using two images in one tilset).
|
||||
*/
|
||||
export default class Tileset {
|
||||
/** The key of the image used by this tileset */
|
||||
protected imageKey: string;
|
||||
/** The size of the tileset image */
|
||||
protected imageSize: Vec2;
|
||||
/** The index of 0th image of this tileset */
|
||||
protected startIndex: number;
|
||||
/** The index of the last image of this tilset */
|
||||
protected endIndex: number;
|
||||
/** The size of the tiles in this tileset */
|
||||
protected tileSize: Vec2;
|
||||
/** The number of rows in this tileset */
|
||||
protected numRows: number;
|
||||
/** The number of columns in this tileset */
|
||||
protected numCols: number;
|
||||
|
||||
// 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;
|
||||
this.startIndex = tiledData.firstgid;
|
||||
this.endIndex = this.startIndex + tiledData.tilecount - 1;
|
||||
this.tileSize = new Vec2(tiledData.tilewidth, tiledData.tilewidth);
|
||||
this.imageKey = tiledData.image;
|
||||
this.imageSize = new Vec2(tiledData.imagewidth, tiledData.imageheight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the image key associated with this tilemap
|
||||
* @returns The image key of this tilemap
|
||||
*/
|
||||
getImageKey(): string {
|
||||
return this.imageKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Vec2 containing the left and top offset from the image origin for this tile.
|
||||
* @param tileIndex The index of the tile from startIndex to endIndex of this tileset
|
||||
* @returns A Vec2 containing the offset for the specified tile.
|
||||
*/
|
||||
getImageOffsetForTile(tileIndex: number): Vec2 {
|
||||
// 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;
|
||||
|
||||
return new Vec2(left, top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the start index
|
||||
* @returns The start index
|
||||
*/
|
||||
getStartIndex(): number {
|
||||
return this.startIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tile set
|
||||
* @returns A Vec2 containing the tile size
|
||||
*/
|
||||
getTileSize(): Vec2 {
|
||||
return this.tileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of rows in the tileset
|
||||
* @returns The number of rows
|
||||
*/
|
||||
getNumRows(): number {
|
||||
return this.numRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of columns in the tilset
|
||||
* @returns The number of columns
|
||||
*/
|
||||
getNumCols(): number {
|
||||
return this.numCols;
|
||||
}
|
||||
|
||||
getTileCount(): number {
|
||||
return this.endIndex - this.startIndex + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether or not this tilset contains the specified tile index. This is used for rendering.
|
||||
* @param tileIndex The index of the tile to check
|
||||
* @returns A boolean representing whether or not this tilset uses the specified index
|
||||
*/
|
||||
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, maxCols: number, origin: Vec2, scale: Vec2, zoom: number): void {
|
||||
let image = ResourceManager.getInstance().getImage(this.imageKey);
|
||||
|
||||
// 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 = Math.floor((dataIndex % maxCols) * width * scale.x);
|
||||
let y = Math.floor(Math.floor(dataIndex / maxCols) * height * scale.y);
|
||||
ctx.drawImage(image, left, top, width, height, Math.floor((x - origin.x)*zoom), Math.floor((y - origin.y)*zoom), Math.ceil(width * scale.x * zoom), Math.ceil(height * scale.y * zoom));
|
||||
}
|
||||
}
|
432
hw3/src/Wolfie2D/DataTypes/Vec2.ts
Normal file
|
@ -0,0 +1,432 @@
|
|||
import MathUtils from "../Utils/MathUtils";
|
||||
|
||||
/**
|
||||
* A two-dimensional vector (x, y)
|
||||
*/
|
||||
export default class Vec2 {
|
||||
|
||||
// Store x and y in an array
|
||||
/** The array that stores the actual vector values x and y */
|
||||
private vec: Float32Array;
|
||||
|
||||
/**
|
||||
* When this vector changes its value, do something
|
||||
*/
|
||||
private onChange: Function = () => {};
|
||||
|
||||
/**
|
||||
* Creates a new Vec2
|
||||
* @param x The x value of the vector
|
||||
* @param y The y value of the vector
|
||||
*/
|
||||
constructor(x: number = 0, y: number = 0) {
|
||||
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;
|
||||
|
||||
if(this.onChange){
|
||||
this.onChange();
|
||||
}
|
||||
}
|
||||
|
||||
get y() {
|
||||
return this.vec[1];
|
||||
}
|
||||
|
||||
set y(y: number) {
|
||||
this.vec[1] = y;
|
||||
|
||||
if(this.onChange){
|
||||
this.onChange();
|
||||
}
|
||||
}
|
||||
|
||||
static get ZERO() {
|
||||
return new Vec2(0, 0);
|
||||
}
|
||||
|
||||
static readonly ZERO_STATIC = new Vec2(0, 0);
|
||||
|
||||
static get INF() {
|
||||
return new Vec2(Infinity, Infinity);
|
||||
}
|
||||
|
||||
static get UP() {
|
||||
return new Vec2(0, -1);
|
||||
}
|
||||
|
||||
static get DOWN() {
|
||||
return new Vec2(0, 1);
|
||||
}
|
||||
|
||||
static get LEFT() {
|
||||
return new Vec2(-1, 0);
|
||||
}
|
||||
|
||||
static get RIGHT() {
|
||||
return new Vec2(1, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The squared magnitude of the vector. This tends to be faster, so use it in situations where taking the
|
||||
* square root doesn't matter, like for comparing distances.
|
||||
* @returns The squared magnitude of the vector
|
||||
*/
|
||||
magSq(): number {
|
||||
return this.x*this.x + this.y*this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* The magnitude of the vector.
|
||||
* @returns The magnitude of the vector.
|
||||
*/
|
||||
mag(): number {
|
||||
return Math.sqrt(this.magSq());
|
||||
}
|
||||
|
||||
/**
|
||||
* Divdes x and y by the magnitude to obtain the unit vector in the direction of this vector.
|
||||
* @returns This vector as a unit vector.
|
||||
*/
|
||||
normalize(): Vec2 {
|
||||
if(this.x === 0 && this.y === 0) return this;
|
||||
let mag = this.mag();
|
||||
this.x /= mag;
|
||||
this.y /= mag;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Works like normalize(), but returns a new Vec2
|
||||
* @returns A new vector that is the unit vector for this one
|
||||
*/
|
||||
normalized(): Vec2 {
|
||||
if(this.isZero()){
|
||||
return this;
|
||||
}
|
||||
|
||||
let mag = this.mag();
|
||||
return new Vec2(this.x/mag, this.y/mag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the x and y elements of this vector to zero.
|
||||
* @returns This vector, with x and y set to zero.
|
||||
*/
|
||||
zero(): Vec2 {
|
||||
return this.set(0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vector's x and y based on the angle provided. Goes counter clockwise.
|
||||
* @param angle The angle in radians
|
||||
* @param radius The magnitude of the vector at the specified angle
|
||||
* @returns This vector.
|
||||
*/
|
||||
setToAngle(angle: number, radius: number = 1): Vec2 {
|
||||
this.x = MathUtils.floorToPlace(Math.cos(angle)*radius, 5);
|
||||
this.y = MathUtils.floorToPlace(-Math.sin(angle)*radius, 5);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a vector that point from this vector to another one
|
||||
* @param other The vector to point to
|
||||
* @returns A new Vec2 that points from this vector to the one provided
|
||||
*/
|
||||
vecTo(other: Vec2): Vec2 {
|
||||
return new Vec2(other.x - this.x, other.y - this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a vector containing the direction from this vector to another
|
||||
* @param other The vector to point to
|
||||
* @returns A new Vec2 that points from this vector to the one provided. This new Vec2 will be a unit vector.
|
||||
*/
|
||||
dirTo(other: Vec2): Vec2 {
|
||||
return this.vecTo(other).normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the vector's direction, but sets its magnitude to be the provided magnitude
|
||||
* @param magnitude The magnitude the vector should be
|
||||
* @returns This vector with its magnitude set to the new 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 The scaling factor for the vector, or for only the x-component if yFactor is provided
|
||||
* @param yFactor The scaling factor for the y-component of the vector
|
||||
* @returns This vector after scaling
|
||||
*/
|
||||
scale(factor: number, yFactor: number = null): Vec2 {
|
||||
if(yFactor !== null){
|
||||
this.x *= factor;
|
||||
this.y *= yFactor;
|
||||
return this;
|
||||
}
|
||||
this.x *= factor;
|
||||
this.y *= factor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a scaled version of this vector without modifying it.
|
||||
* @param factor The scaling factor for the vector, or for only the x-component if yFactor is provided
|
||||
* @param yFactor The scaling factor for the y-component of the vector
|
||||
* @returns A new vector that has the values of this vector after scaling
|
||||
*/
|
||||
scaled(factor: number, yFactor: number = null): Vec2 {
|
||||
return this.clone().scale(factor, yFactor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the vector counter-clockwise by the angle amount specified
|
||||
* @param angle The angle to rotate by in radians
|
||||
* @returns This vector after rotation.
|
||||
*/
|
||||
rotateCCW(angle: number): Vec2 {
|
||||
let cs = Math.cos(angle);
|
||||
let sn = Math.sin(angle);
|
||||
let tempX = this.x*cs - this.y*sn;
|
||||
let tempY = this.x*sn + this.y*cs;
|
||||
this.x = tempX;
|
||||
this.y = tempY;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vectors coordinates to be the ones provided
|
||||
* @param x The new x value for this vector
|
||||
* @param y The new y value for this vector
|
||||
* @returns This vector
|
||||
*/
|
||||
set(x: number, y: number): Vec2 {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the values of the other Vec2 into this one.
|
||||
* @param other The Vec2 to copy
|
||||
* @returns This vector with its values set to the vector provided
|
||||
*/
|
||||
copy(other: Vec2): Vec2 {
|
||||
return this.set(other.x, other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds this vector the another vector
|
||||
* @param other The Vec2 to add to this one
|
||||
* @returns This vector after adding the one provided
|
||||
*/
|
||||
add(other: Vec2): Vec2 {
|
||||
this.x += other.x;
|
||||
this.y += other.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the fields of this vector. Both are incremented with a, if only a is provided.
|
||||
* @param a The first number to increment by
|
||||
* @param b The second number to increment by
|
||||
* @returnss This vector after incrementing
|
||||
*/
|
||||
inc(a: number, b?: number): Vec2 {
|
||||
if(b === undefined){
|
||||
this.x += a;
|
||||
this.y += a;
|
||||
} else {
|
||||
this.x += a;
|
||||
this.y += b;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts another vector from this vector
|
||||
* @param other The Vec2 to subtract from this one
|
||||
* @returns This vector after subtracting the one provided
|
||||
*/
|
||||
sub(other: Vec2): Vec2 {
|
||||
this.x -= other.x;
|
||||
this.y -= other.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies this vector with another vector element-wise. In other words, this.x *= other.x and this.y *= other.y
|
||||
* @param other The Vec2 to multiply this one by
|
||||
* @returns This vector after multiplying its components by this one
|
||||
*/
|
||||
mult(other: Vec2): Vec2 {
|
||||
this.x *= other.x;
|
||||
this.y *= other.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Divides this vector with another vector element-wise. In other words, this.x /= other.x and this.y /= other.y
|
||||
* @param other The vector to divide this one by
|
||||
* @returns This vector after division
|
||||
*/
|
||||
div(other: Vec2): Vec2 {
|
||||
if(other.x === 0 || other.y === 0) throw "Divide by zero error";
|
||||
this.x /= other.x;
|
||||
this.y /= other.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does an element wise remainder operation on this vector. this.x %= other.x and this.y %= other.y
|
||||
* @param other The other vector
|
||||
* @returns this vector
|
||||
*/
|
||||
remainder(other: Vec2): Vec2 {
|
||||
this.x = this.x % other.x;
|
||||
this.y = this.y % other.y;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the squared distance between this vector and another vector
|
||||
* @param other The vector to compute distance squared to
|
||||
* @returns The squared distance between this vector and the one provided
|
||||
*/
|
||||
distanceSqTo(other: Vec2): number {
|
||||
return (this.x - other.x)*(this.x - other.x) + (this.y - other.y)*(this.y - other.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the distance between this vector and another vector
|
||||
* @param other The vector to compute distance to
|
||||
* @returns The distance between this vector and the one provided
|
||||
*/
|
||||
distanceTo(other: Vec2): number {
|
||||
return Math.sqrt(this.distanceSqTo(other));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dot product of this vector and another
|
||||
* @param other The vector to compute the dot product with
|
||||
* @returns The dot product of this vector and the one provided.
|
||||
*/
|
||||
dot(other: Vec2): number {
|
||||
return this.x*other.x + this.y*other.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the angle counter-clockwise in radians from this vector to another vector
|
||||
* @param other The vector to compute the angle to
|
||||
* @returns The angle, rotating CCW, from this vector to the other vector
|
||||
*/
|
||||
angleToCCW(other: Vec2): number {
|
||||
let dot = this.dot(other);
|
||||
let det = this.x*other.y - this.y*other.x;
|
||||
let angle = -Math.atan2(det, dot);
|
||||
|
||||
if(angle < 0){
|
||||
angle += 2*Math.PI;
|
||||
}
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this vector rounded to 1 decimal point
|
||||
* @returns This vector as a string
|
||||
*/
|
||||
toString(): string {
|
||||
return this.toFixed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this vector rounded to the specified number of decimal points
|
||||
* @param numDecimalPoints The number of decimal points to create a string to
|
||||
* @returns This vector as a string
|
||||
*/
|
||||
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.
|
||||
* @returns A new Vec2 with the same values as this one
|
||||
*/
|
||||
clone(): Vec2 {
|
||||
return new Vec2(this.x, this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this vector and other have the EXACT same x and y (not assured to be safe for floats)
|
||||
* @param other The vector to check against
|
||||
* @returns A boolean representing the equality of the two vectors
|
||||
*/
|
||||
strictEquals(other: Vec2): boolean {
|
||||
return this.x === other.x && this.y === other.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this vector and other have the same x and y
|
||||
* @param other The vector to check against
|
||||
* @returns A boolean representing the equality of the two vectors
|
||||
*/
|
||||
equals(other: Vec2): boolean {
|
||||
let xEq = Math.abs(this.x - other.x) < 0.0000001;
|
||||
let yEq = Math.abs(this.y - other.y) < 0.0000001;
|
||||
|
||||
return xEq && yEq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this vector is the zero vector exactly (not assured to be safe for floats).
|
||||
* @returns A boolean representing the equality of this vector and the zero vector
|
||||
*/
|
||||
strictIsZero(): boolean {
|
||||
return this.x === 0 && this.y === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this x and y for this vector are both zero.
|
||||
* @returns A boolean representing the equality of this vector and the zero vector
|
||||
*/
|
||||
isZero(): boolean {
|
||||
return Math.abs(this.x) < 0.0000001 && Math.abs(this.y) < 0.0000001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the function that is called whenever this vector is changed.
|
||||
* @param f The function to be called
|
||||
*/
|
||||
setOnChange(f: Function): void {
|
||||
this.onChange = f;
|
||||
}
|
||||
|
||||
toArray(): Float32Array {
|
||||
return this.vec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs linear interpolation between two vectors
|
||||
* @param a The first vector
|
||||
* @param b The second vector
|
||||
* @param t The time of the lerp, with 0 being vector A, and 1 being vector B
|
||||
* @returns A new Vec2 representing the lerp between vector a and b.
|
||||
*/
|
||||
static lerp(a: Vec2, b: Vec2, t: number): Vec2 {
|
||||
return new Vec2(MathUtils.lerp(a.x, b.x, t), MathUtils.lerp(a.y, b.y, t));
|
||||
}
|
||||
}
|
197
hw3/src/Wolfie2D/Debug/Debug.ts
Normal file
|
@ -0,0 +1,197 @@
|
|||
import Map from "../DataTypes/Map";
|
||||
import Vec2 from "../DataTypes/Vec2";
|
||||
import GameNode from "../Nodes/GameNode";
|
||||
import Color from "../Utils/Color";
|
||||
|
||||
/**
|
||||
* A util class for rendering Debug messages to the canvas.
|
||||
*/
|
||||
export default class Debug {
|
||||
|
||||
/** A map of log messages to display on the screen */
|
||||
private static logMessages: Map<string> = new Map();
|
||||
|
||||
/** An array of game nodes to render debug info for */
|
||||
private static nodes: Array<GameNode>;
|
||||
|
||||
/** The rendering context for any debug messages */
|
||||
private static debugRenderingContext: CanvasRenderingContext2D;
|
||||
|
||||
/** The size of the debug canvas */
|
||||
private static debugCanvasSize: Vec2;
|
||||
|
||||
/** The rendering color for text */
|
||||
private static defaultTextColor: Color = Color.WHITE;
|
||||
|
||||
/**
|
||||
* Add a message to display on the debug screen
|
||||
* @param id A unique ID for this message
|
||||
* @param messages The messages to print to the debug screen
|
||||
*/
|
||||
static log(id: string, ...messages: any): void {
|
||||
// let message = "";
|
||||
// for(let i = 0; i < messages.length; i++){
|
||||
// message += messages[i].toString();
|
||||
// }
|
||||
// Join all messages with spaces
|
||||
let message = messages.map((m: any) => m.toString()).join(" ");
|
||||
this.logMessages.add(id, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a a key from the log and stops it from keeping up space on the screen
|
||||
* @param id The id of the log item to clear
|
||||
*/
|
||||
static clearLogItem(id: string): void {
|
||||
this.logMessages.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the list of nodes to render with the debugger
|
||||
* @param nodes The new list of nodes
|
||||
*/
|
||||
static setNodes(nodes: Array<GameNode>): void {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a box at the specified position
|
||||
* @param center The center of the box
|
||||
* @param halfSize The dimensions of the box
|
||||
* @param filled A boolean for whether or not the box is filled
|
||||
* @param color The color of the box to draw
|
||||
*/
|
||||
static drawBox(center: Vec2, halfSize: Vec2, filled: boolean, color: Color): void {
|
||||
let alpha = this.debugRenderingContext.globalAlpha;
|
||||
this.debugRenderingContext.globalAlpha = color.a;
|
||||
|
||||
if(filled){
|
||||
this.debugRenderingContext.fillStyle = color.toString();
|
||||
this.debugRenderingContext.fillRect(center.x - halfSize.x, center.y - halfSize.y, halfSize.x*2, halfSize.y*2);
|
||||
} else {
|
||||
let lineWidth = 2;
|
||||
this.debugRenderingContext.lineWidth = lineWidth;
|
||||
this.debugRenderingContext.strokeStyle = color.toString();
|
||||
this.debugRenderingContext.strokeRect(center.x - halfSize.x, center.y - halfSize.y, halfSize.x*2, halfSize.y*2);
|
||||
}
|
||||
|
||||
this.debugRenderingContext.globalAlpha = alpha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a circle at the specified position
|
||||
* @param center The center of the circle
|
||||
* @param radius The dimensions of the box
|
||||
* @param filled A boolean for whether or not the circle is filled
|
||||
* @param color The color of the circle
|
||||
*/
|
||||
static drawCircle(center: Vec2, radius: number, filled: boolean, color: Color): void {
|
||||
let alpha = this.debugRenderingContext.globalAlpha;
|
||||
this.debugRenderingContext.globalAlpha = color.a;
|
||||
|
||||
if(filled){
|
||||
this.debugRenderingContext.fillStyle = color.toString();
|
||||
this.debugRenderingContext.beginPath();
|
||||
this.debugRenderingContext.arc(center.x, center.y, radius, 0, 2 * Math.PI);
|
||||
this.debugRenderingContext.closePath();
|
||||
this.debugRenderingContext.fill();
|
||||
} else {
|
||||
let lineWidth = 2;
|
||||
this.debugRenderingContext.lineWidth = lineWidth;
|
||||
this.debugRenderingContext.strokeStyle = color.toString();
|
||||
this.debugRenderingContext.beginPath();
|
||||
this.debugRenderingContext.arc(center.x, center.y, radius, 0, 2 * Math.PI);
|
||||
this.debugRenderingContext.closePath();
|
||||
this.debugRenderingContext.stroke();
|
||||
}
|
||||
|
||||
this.debugRenderingContext.globalAlpha = alpha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a ray at the specified position
|
||||
* @param from The starting position of the ray
|
||||
* @param to The ending position of the ray
|
||||
* @param color The color of the ray
|
||||
*/
|
||||
static drawRay(from: Vec2, to: Vec2, color: Color): void {
|
||||
this.debugRenderingContext.lineWidth = 2;
|
||||
this.debugRenderingContext.strokeStyle = color.toString();
|
||||
|
||||
this.debugRenderingContext.beginPath();
|
||||
this.debugRenderingContext.moveTo(from.x, from.y);
|
||||
this.debugRenderingContext.lineTo(to.x, to.y);
|
||||
this.debugRenderingContext.closePath();
|
||||
this.debugRenderingContext.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a point at the specified position
|
||||
* @param pos The position of the point
|
||||
* @param color The color of the point
|
||||
*/
|
||||
static drawPoint(pos: Vec2, color: Color): void {
|
||||
let pointSize = 6;
|
||||
this.debugRenderingContext.fillStyle = color.toString();
|
||||
this.debugRenderingContext.fillRect(pos.x - pointSize/2, pos.y - pointSize/2, pointSize, pointSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default rendering color for text for the debugger
|
||||
* @param color The color to render the text
|
||||
*/
|
||||
static setDefaultTextColor(color: Color): void {
|
||||
this.defaultTextColor = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs any necessary setup operations on the Debug canvas
|
||||
* @param canvas The debug canvas
|
||||
* @param width The desired width of the canvas
|
||||
* @param height The desired height of the canvas
|
||||
* @returns The rendering context extracted from the canvas
|
||||
*/
|
||||
static initializeDebugCanvas(canvas: HTMLCanvasElement, width: number, height: number): CanvasRenderingContext2D {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
this.debugCanvasSize = new Vec2(width, height);
|
||||
|
||||
this.debugRenderingContext = canvas.getContext("2d");
|
||||
|
||||
return this.debugRenderingContext;
|
||||
}
|
||||
|
||||
/** Clears the debug canvas */
|
||||
static clearCanvas(): void {
|
||||
this.debugRenderingContext.clearRect(0, 0, this.debugCanvasSize.x, this.debugCanvasSize.y);
|
||||
}
|
||||
|
||||
/** Renders the text and nodes sent to the Debug system */
|
||||
static render(): void {
|
||||
this.renderText();
|
||||
this.renderNodes();
|
||||
}
|
||||
|
||||
/** Renders the text sent to the Debug canvas */
|
||||
static renderText(): void {
|
||||
let y = 20;
|
||||
this.debugRenderingContext.font = "20px Arial";
|
||||
this.debugRenderingContext.fillStyle = this.defaultTextColor.toString();
|
||||
|
||||
// Draw all of the text
|
||||
this.logMessages.forEach((key: string) => {
|
||||
this.debugRenderingContext.fillText(this.logMessages.get(key), 10, y)
|
||||
y += 30;
|
||||
});
|
||||
}
|
||||
|
||||
/** Renders the nodes registered with the debug canvas */
|
||||
static renderNodes(): void {
|
||||
if(this.nodes){
|
||||
this.nodes.forEach(node => {
|
||||
node.debugRender();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
244
hw3/src/Wolfie2D/Debug/Stats.ts
Normal file
|
@ -0,0 +1,244 @@
|
|||
import Color from "../Utils/Color";
|
||||
|
||||
// @ignorePage
|
||||
export default class Stats extends Object {
|
||||
// The fps of the game.
|
||||
private static prevfps: Array<number>;
|
||||
private static readonly NUM_POINTS: number = 60;
|
||||
private static ctx: CanvasRenderingContext2D;
|
||||
private static CANVAS_WIDTH: number = 300;
|
||||
private static CANVAS_HEIGHT: number = 300;
|
||||
private static statsDiv: HTMLDivElement;
|
||||
private static graphChoices: HTMLSelectElement;
|
||||
|
||||
// Quadtree stats
|
||||
private static prevClearTimes: Array<number>;
|
||||
private static SGClearTimes: Array<number>;
|
||||
private static avgSGClearTime: number;
|
||||
|
||||
private static prevFillTimes: Array<number>;
|
||||
private static SGFillTimes: Array<number>;
|
||||
private static avgSGFillTime: number;
|
||||
|
||||
private static prevUpdateTimes: Array<number>;
|
||||
private static SGUpdateTimes: Array<number>;
|
||||
private static avgSGUpdateTime: number;
|
||||
|
||||
private static prevQueryTimes: Array<number>;
|
||||
private static SGQueryTimes: Array<number>;
|
||||
private static avgSGQueryTime: number;
|
||||
|
||||
static initStats(): void {
|
||||
let canvas = <HTMLCanvasElement>document.getElementById("stats-canvas");
|
||||
canvas.width = this.CANVAS_WIDTH;
|
||||
canvas.height = this.CANVAS_HEIGHT;
|
||||
this.ctx = canvas.getContext("2d");
|
||||
|
||||
this.statsDiv = <HTMLDivElement>document.getElementById("stats-display");
|
||||
|
||||
this.prevfps = new Array();
|
||||
|
||||
this.prevClearTimes = new Array();
|
||||
this.SGClearTimes = new Array();
|
||||
this.avgSGClearTime = 0;
|
||||
|
||||
this.prevFillTimes = new Array();
|
||||
this.SGFillTimes = new Array();
|
||||
this.avgSGFillTime = 0;
|
||||
|
||||
this.prevUpdateTimes = new Array();
|
||||
this.SGUpdateTimes = new Array();
|
||||
this.avgSGUpdateTime = 0;
|
||||
|
||||
this.prevQueryTimes = new Array();
|
||||
this.SGQueryTimes = new Array();
|
||||
this.avgSGQueryTime = 0;
|
||||
|
||||
let clearTime = document.createElement("span");
|
||||
clearTime.setAttribute("id", "sgclear");
|
||||
let fillTime = document.createElement("span");
|
||||
fillTime.setAttribute("id", "sgfill");
|
||||
let updateTime = document.createElement("span");
|
||||
updateTime.setAttribute("id", "sgupdate");
|
||||
let queryTime = document.createElement("span");
|
||||
queryTime.setAttribute("id", "sgquery");
|
||||
let br1 = document.createElement("br");
|
||||
let br2 = document.createElement("br");
|
||||
let br3 = document.createElement("br");
|
||||
|
||||
this.statsDiv.append(clearTime, br1, fillTime, br2, updateTime, br3, queryTime);
|
||||
|
||||
this.graphChoices = <HTMLSelectElement>document.getElementById("chart-option");
|
||||
let option1 = document.createElement("option");
|
||||
option1.value = "prevfps";
|
||||
option1.label = "FPS";
|
||||
let option2 = document.createElement("option");
|
||||
option2.value = "prevClearTimes";
|
||||
option2.label = "Clear Time";
|
||||
let option3 = document.createElement("option");
|
||||
option3.value = "prevFillTimes";
|
||||
option3.label = "Fill time";
|
||||
let option4 = document.createElement("option");
|
||||
option4.value = "prevUpdateTimes";
|
||||
option4.label = "Update time";
|
||||
let option5 = document.createElement("option");
|
||||
option5.value = "prevQueryTimes";
|
||||
option5.label = "Query Time";
|
||||
let optionAll = document.createElement("option");
|
||||
optionAll.value = "all";
|
||||
optionAll.label = "All";
|
||||
this.graphChoices.append(option1, option2, option3, option4, option5, optionAll);
|
||||
}
|
||||
|
||||
static updateFPS(fps: number): void {
|
||||
this.prevfps.push(fps);
|
||||
if(this.prevfps.length > Stats.NUM_POINTS){
|
||||
this.prevfps.shift();
|
||||
}
|
||||
|
||||
if(this.SGClearTimes.length > 0){
|
||||
this.prevClearTimes.push(this.avgSGClearTime);
|
||||
if(this.prevClearTimes.length > this.NUM_POINTS){
|
||||
this.prevClearTimes.shift();
|
||||
}
|
||||
}
|
||||
if(this.SGFillTimes.length > 0){
|
||||
this.prevFillTimes.push(this.avgSGFillTime);
|
||||
if(this.prevFillTimes.length > this.NUM_POINTS){
|
||||
this.prevFillTimes.shift();
|
||||
}
|
||||
}
|
||||
if(this.SGUpdateTimes.length > 0){
|
||||
this.prevUpdateTimes.push(this.avgSGUpdateTime);
|
||||
if(this.prevUpdateTimes.length > this.NUM_POINTS){
|
||||
this.prevUpdateTimes.shift();
|
||||
}
|
||||
}
|
||||
if(this.SGQueryTimes.length > 0){
|
||||
this.prevQueryTimes.push(this.avgSGQueryTime);
|
||||
if(this.prevQueryTimes.length > this.NUM_POINTS){
|
||||
this.prevQueryTimes.shift();
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSGStats();
|
||||
}
|
||||
|
||||
static log(key: string, data: any): void {
|
||||
if(key === "sgclear"){
|
||||
this.SGClearTimes.push(data);
|
||||
if(this.SGClearTimes.length > 100){
|
||||
this.SGClearTimes.shift();
|
||||
}
|
||||
} else if(key === "sgfill"){
|
||||
this.SGFillTimes.push(data);
|
||||
if(this.SGFillTimes.length > 100){
|
||||
this.SGFillTimes.shift();
|
||||
}
|
||||
} else if(key === "sgupdate"){
|
||||
this.SGUpdateTimes.push(data);
|
||||
if(this.SGUpdateTimes.length > 100){
|
||||
this.SGUpdateTimes.shift();
|
||||
}
|
||||
} else if(key === "sgquery"){
|
||||
this.SGQueryTimes.push(data);
|
||||
if(this.SGQueryTimes.length > 1000){
|
||||
this.SGQueryTimes.shift();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static render(): void {
|
||||
// Display stats
|
||||
this.drawCharts();
|
||||
}
|
||||
|
||||
static drawCharts(){
|
||||
this.ctx.clearRect(0, 0, this.CANVAS_WIDTH, this.CANVAS_HEIGHT);
|
||||
|
||||
let paramString = this.graphChoices.value;
|
||||
|
||||
if(paramString === "prevfps" || paramString === "all"){
|
||||
let param = this.prevfps;
|
||||
let color = Color.BLUE.toString();
|
||||
this.drawChart(param, color);
|
||||
}
|
||||
if(paramString === "prevClearTimes" || paramString === "all"){
|
||||
let param = this.prevClearTimes;
|
||||
let color = Color.RED.toString();
|
||||
this.drawChart(param, color);
|
||||
}
|
||||
if(paramString === "prevFillTimes" || paramString === "all"){
|
||||
let param = this.prevFillTimes;
|
||||
let color = Color.GREEN.toString();
|
||||
this.drawChart(param, color);
|
||||
}
|
||||
if(paramString === "prevUpdateTimes" || paramString === "all"){
|
||||
let param = this.prevUpdateTimes;
|
||||
let color = Color.CYAN.toString();
|
||||
this.drawChart(param, color);
|
||||
}
|
||||
if(paramString === "prevQueryTimes" || paramString === "all"){
|
||||
let param = this.prevQueryTimes;
|
||||
let color = Color.ORANGE.toString();
|
||||
this.drawChart(param, color);
|
||||
}
|
||||
}
|
||||
|
||||
static drawChart(param: Array<number>, color: string){
|
||||
this.ctx.strokeStyle = Color.BLACK.toString();
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(10, 10);
|
||||
this.ctx.lineTo(10, this.CANVAS_HEIGHT - 10);
|
||||
this.ctx.closePath();
|
||||
this.ctx.stroke();
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(10, this.CANVAS_HEIGHT - 10);
|
||||
this.ctx.lineTo(this.CANVAS_WIDTH - 10, this.CANVAS_HEIGHT - 10);
|
||||
this.ctx.closePath();
|
||||
this.ctx.stroke();
|
||||
|
||||
let max = Math.max(...param);
|
||||
let prevX = 10;
|
||||
let prevY = this.CANVAS_HEIGHT - 10 - param[0]/max*(this.CANVAS_HEIGHT-20);
|
||||
this.ctx.strokeStyle = color;
|
||||
|
||||
for(let i = 1; i < param.length; i++){
|
||||
let fps = param[i];
|
||||
let x = 10 + i*(this.CANVAS_WIDTH - 20)/this.NUM_POINTS;
|
||||
let y = this.CANVAS_HEIGHT - 10 - fps/max*(this.CANVAS_HEIGHT-20)
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(prevX, prevY);
|
||||
this.ctx.lineTo(x, y);
|
||||
this.ctx.closePath();
|
||||
this.ctx.stroke();
|
||||
|
||||
prevX = x;
|
||||
prevY = y;
|
||||
}
|
||||
}
|
||||
|
||||
static updateSGStats(){
|
||||
if(this.SGClearTimes.length > 0){
|
||||
this.avgSGClearTime = this.SGClearTimes.reduce((acc, val) => acc + val)/this.SGClearTimes.length;
|
||||
}
|
||||
|
||||
if(this.SGFillTimes.length > 0){
|
||||
this.avgSGFillTime = this.SGFillTimes.reduce((acc, val) => acc + val)/this.SGFillTimes.length;
|
||||
}
|
||||
|
||||
if(this.SGUpdateTimes.length > 0){
|
||||
this.avgSGUpdateTime = this.SGUpdateTimes.reduce((acc, val) => acc + val)/this.SGUpdateTimes.length;
|
||||
}
|
||||
|
||||
if(this.SGQueryTimes.length > 0){
|
||||
this.avgSGQueryTime = this.SGQueryTimes.reduce((acc, val) => acc + val)/this.SGQueryTimes.length;
|
||||
}
|
||||
|
||||
document.getElementById("sgclear").innerHTML = "Avg SG clear time: " + this.avgSGClearTime;
|
||||
document.getElementById("sgfill").innerHTML = "Avg SG fill time: " + this.avgSGFillTime;
|
||||
document.getElementById("sgupdate").innerHTML = "Avg SG update time: " + this.avgSGUpdateTime;
|
||||
document.getElementById("sgquery").innerHTML = "Avg SG query time: " + this.avgSGQueryTime;
|
||||
}
|
||||
}
|
26
hw3/src/Wolfie2D/Events/Emitter.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import Map from "../DataTypes/Map";
|
||||
import EventQueue from "./EventQueue";
|
||||
import GameEvent from "./GameEvent";
|
||||
|
||||
/**
|
||||
* An event emitter object other systems can use to hook into the EventQueue.
|
||||
* Provides an easy interface for firing off events.
|
||||
*/
|
||||
export default class Emitter {
|
||||
/** A reference to the EventQueue */
|
||||
private eventQueue: EventQueue;
|
||||
|
||||
/** Creates a new Emitter */
|
||||
constructor(){
|
||||
this.eventQueue = EventQueue.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit and event of type eventType with the data packet data
|
||||
* @param eventType The name of the event to fire off
|
||||
* @param data A @reference[Map] or record containing any data about the event
|
||||
*/
|
||||
fireEvent(eventType: string, data: Map<any> | Record<string, any> = null): void {
|
||||
this.eventQueue.addEvent(new GameEvent(eventType, data));
|
||||
}
|
||||
}
|
125
hw3/src/Wolfie2D/Events/EventQueue.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
import Queue from "../DataTypes/Queue";
|
||||
import Map from "../DataTypes/Map";
|
||||
import GameEvent from "./GameEvent";
|
||||
import Receiver from "./Receiver";
|
||||
import { GameEventType } from "./GameEventType";
|
||||
|
||||
/**
|
||||
* The main event system of the game engine.
|
||||
* Events are sent to the EventQueue, which handles distribution to any systems that are listening for those events.
|
||||
* This allows for handling of input without having classes directly hook into javascript event handles,
|
||||
* and allows otherwise separate classes to communicate with each other cleanly, such as a Player object
|
||||
* requesting a sound be played by the audio system.
|
||||
*
|
||||
* The distribution of @reference[GameEvent]s happens as follows:
|
||||
*
|
||||
* Events are recieved throughout a frame and are queued up by the EventQueue.
|
||||
* At the beginning of the next frame, events are sent out to any receivers that are hooked into the event type.
|
||||
* @reference[Receiver]s are then free to process events as they see fit.
|
||||
*
|
||||
* Overall, the EventQueue can be considered as something similar to an email server,
|
||||
* and the @reference[Receiver]s can be considered as the client inboxes.
|
||||
*
|
||||
* See @link(Game Programming Patterns)(https://gameprogrammingpatterns.com/event-queue.html) for more discussion on EventQueues
|
||||
*/
|
||||
export default class EventQueue {
|
||||
private static instance: EventQueue = null;
|
||||
|
||||
/** The maximum number of events visible */
|
||||
private readonly MAX_SIZE: number;
|
||||
|
||||
/** The actual queue of events */
|
||||
private q: Queue<GameEvent>;
|
||||
|
||||
/** The map of receivers registered for an event name */
|
||||
private receivers: Map<Array<Receiver>>;
|
||||
|
||||
private constructor(){
|
||||
this.MAX_SIZE = 100;
|
||||
this.q = new Queue<GameEvent>(this.MAX_SIZE);
|
||||
this.receivers = new Map<Array<Receiver>>();
|
||||
}
|
||||
|
||||
/** Retrieves the instance of the Singleton EventQueue */
|
||||
static getInstance(): EventQueue {
|
||||
if(this.instance === null){
|
||||
this.instance = new EventQueue();
|
||||
}
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/** Adds an event to the EventQueue.
|
||||
* This is exposed to the rest of the game engine through the @reference[Emitter] class */
|
||||
addEvent(event: GameEvent): void {
|
||||
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).
|
||||
* This is exposed to the rest of the game engine through the @reference[Receiver] class
|
||||
* @param receiver The event receiver
|
||||
* @param type The type or types of events to subscribe to
|
||||
*/
|
||||
subscribe(receiver: Receiver, type: string | Array<string>): void {
|
||||
if(type instanceof Array){
|
||||
// If it is an array, subscribe to all event types
|
||||
for(let t of type){
|
||||
this.addListener(receiver, t);
|
||||
}
|
||||
} else {
|
||||
this.addListener(receiver, type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes the specified receiver from all events, or from whatever events are provided
|
||||
* @param receiver The receiver to unsubscribe
|
||||
* @param keys The events to unsubscribe from. If none are provided, unsubscribe from all
|
||||
*/
|
||||
unsubscribe(receiver: Receiver, ...events: Array<string>): void {
|
||||
this.receivers.forEach(eventName => {
|
||||
// If keys were provided, only continue if this key is one of them
|
||||
if(events.length > 0 && events.indexOf(eventName) === -1) return;
|
||||
|
||||
// Find the index of our receiver for this key
|
||||
let index = this.receivers.get(eventName).indexOf(receiver);
|
||||
|
||||
// If an index was found, remove the receiver
|
||||
if(index !== -1){
|
||||
this.receivers.get(eventName).splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Associate the receiver and the type
|
||||
private addListener(receiver: Receiver, type: string): void {
|
||||
if(this.receivers.has(type)){
|
||||
this.receivers.get(type).push(receiver);
|
||||
} else {
|
||||
this.receivers.add(type, [receiver]);
|
||||
}
|
||||
}
|
||||
|
||||
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(GameEventType.ALL)){
|
||||
for(let receiver of this.receivers.get(GameEventType.ALL)){
|
||||
receiver.receive(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
hw3/src/Wolfie2D/Events/GameEvent.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import Map from "../DataTypes/Map"
|
||||
|
||||
/**
|
||||
* A representation of an in-game event that is passed through the @reference[EventQueue]
|
||||
*/
|
||||
export default class GameEvent {
|
||||
/** The type of the event */
|
||||
public type: string;
|
||||
/** The data contained by the event */
|
||||
public data: Map<any>;
|
||||
/** The time of the event in ms */
|
||||
public time: number;
|
||||
|
||||
/**
|
||||
* Creates a new GameEvent.
|
||||
* This is handled implicitly through the @reference[Emitter] class
|
||||
* @param type The type of the GameEvent
|
||||
* @param data The data contained by the GameEvent
|
||||
*/
|
||||
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)){
|
||||
// data is a raw object, unpack
|
||||
this.data = new Map<any>();
|
||||
for(let key in data){
|
||||
this.data.add(key, data[key]);
|
||||
}
|
||||
} else {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
this.type = type;
|
||||
this.time = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the type of the GameEvent
|
||||
* @param type The type to check
|
||||
* @returns True if the GameEvent is the specified type, false otherwise.
|
||||
*/
|
||||
isType(type: string): boolean {
|
||||
return this.type === type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this GameEvent as a string
|
||||
* @returns The string representation of the GameEvent
|
||||
*/
|
||||
toString(): string {
|
||||
return this.type + ": @" + this.time;
|
||||
}
|
||||
}
|
91
hw3/src/Wolfie2D/Events/GameEventType.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
// @ignorePage
|
||||
|
||||
export enum GameEventType {
|
||||
/**
|
||||
* Mouse Down event. Has data: {position: Vec2 - Mouse Position}
|
||||
*/
|
||||
MOUSE_DOWN = "mouse_down",
|
||||
/**
|
||||
* Mouse Up event. Has data: {position: Vec2 - Mouse Position}
|
||||
*/
|
||||
MOUSE_UP = "mouse_up",
|
||||
/**
|
||||
* Mouse Move event. Has data: {position: Vec2 - Mouse Position}
|
||||
*/
|
||||
MOUSE_MOVE = "mouse_move",
|
||||
|
||||
/**
|
||||
* Key Down event. Has data: {key: string - The key that is down}
|
||||
*/
|
||||
KEY_DOWN = "key_down",
|
||||
|
||||
/**
|
||||
* Key Up event. Has data: {key: string - The key that is up}
|
||||
*/
|
||||
KEY_UP = "key_up",
|
||||
|
||||
/**
|
||||
* Canvas Blur event. Has data: {}
|
||||
*/
|
||||
CANVAS_BLUR = "canvas_blur",
|
||||
|
||||
/**
|
||||
* Mouse wheel up event. Has data: {}
|
||||
*/
|
||||
WHEEL_UP = "wheel_up",
|
||||
|
||||
/**
|
||||
* Mouse wheel down event. Has data: {}
|
||||
*/
|
||||
WHEEL_DOWN = "wheel_down",
|
||||
|
||||
/**
|
||||
* Start Recording event. Has data: {}
|
||||
*/
|
||||
START_RECORDING = "start_recording",
|
||||
|
||||
/**
|
||||
* Stop Recording event. Has data: {}
|
||||
*/
|
||||
STOP_RECORDING = "stop_recording",
|
||||
|
||||
/**
|
||||
* Play Recording event. Has data: {}
|
||||
*/
|
||||
PLAY_RECORDING = "play_recording",
|
||||
|
||||
/**
|
||||
* Play Sound event. Has data: {key: string, loop: boolean, holdReference: boolean }
|
||||
*/
|
||||
PLAY_SOUND = "play_sound",
|
||||
|
||||
/**
|
||||
* Play Sound event. Has data: {key: string}
|
||||
*/
|
||||
STOP_SOUND = "stop_sound",
|
||||
|
||||
/**
|
||||
* Play Sound event. Has data: {key: string, loop: boolean, holdReference: boolean, channel: AudioChannelType }
|
||||
*/
|
||||
PLAY_SFX = "play_sfx",
|
||||
|
||||
/**
|
||||
* Play Sound event. Has data: {key: string, loop: boolean, holdReference: boolean }
|
||||
*/
|
||||
PLAY_MUSIC = "play_music",
|
||||
|
||||
/**
|
||||
* Mute audio channel event. Has data: {channel: AudioChannelType}
|
||||
*/
|
||||
MUTE_CHANNEL = "mute_channel",
|
||||
|
||||
/**
|
||||
* Unmute audio channel event. Has data: {channel: AudioChannelType}
|
||||
*/
|
||||
UNMUTE_CHANNEL = "unmute_channel",
|
||||
|
||||
/**
|
||||
* Encompasses all event types. Used for receivers only.
|
||||
*/
|
||||
ALL = "all",
|
||||
}
|
77
hw3/src/Wolfie2D/Events/Receiver.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import Queue from "../DataTypes/Queue";
|
||||
import EventQueue from "./EventQueue";
|
||||
import GameEvent from "./GameEvent";
|
||||
|
||||
/**
|
||||
* Receives subscribed events from the EventQueue.
|
||||
*/
|
||||
export default class Receiver {
|
||||
/** The maximum number of events this Receiver can hold at one time */
|
||||
readonly MAX_SIZE: number;
|
||||
|
||||
/** The inbox of the Receiver */
|
||||
private q: Queue<GameEvent>;
|
||||
|
||||
/** Creates a new Receiver */
|
||||
constructor(){
|
||||
this.MAX_SIZE = 100;
|
||||
this.q = new Queue(this.MAX_SIZE);
|
||||
}
|
||||
|
||||
destroy(){
|
||||
EventQueue.getInstance().unsubscribe(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds these types of events to this receiver's queue every update.
|
||||
* @param eventTypes The types of events this receiver will be subscribed to
|
||||
*/
|
||||
subscribe(eventTypes: string | Array<string>): void {
|
||||
EventQueue.getInstance().subscribe(this, eventTypes);
|
||||
this.q.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event to the queue of this reciever. This is used by the @reference[EventQueue] to distribute events
|
||||
* @param event The event to receive
|
||||
*/
|
||||
receive(event: GameEvent): void {
|
||||
try{
|
||||
this.q.enqueue(event);
|
||||
} catch(e){
|
||||
console.warn("Receiver overflow for event " + event.toString());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the next event from the receiver's queue
|
||||
* @returns The next GameEvent
|
||||
*/
|
||||
getNextEvent(): GameEvent {
|
||||
return this.q.dequeue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks at the next event in the receiver's queue, but doesn't remove it from the queue
|
||||
* @returns The next GameEvent
|
||||
*/
|
||||
peekNextEvent(): GameEvent {
|
||||
return this.q.peekNext()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the receiver has any events in its queue
|
||||
* @returns True if the receiver has another event, false otherwise
|
||||
*/
|
||||
hasNextEvent(): boolean {
|
||||
return this.q.hasItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore all events this frame
|
||||
*/
|
||||
ignoreEvents(): void {
|
||||
this.q.clear();
|
||||
}
|
||||
}
|
322
hw3/src/Wolfie2D/Input/Input.ts
Normal file
|
@ -0,0 +1,322 @@
|
|||
import Receiver from "../Events/Receiver";
|
||||
import Map from "../DataTypes/Map";
|
||||
import Vec2 from "../DataTypes/Vec2";
|
||||
import EventQueue from "../Events/EventQueue";
|
||||
import Viewport from "../SceneGraph/Viewport";
|
||||
import GameEvent from "../Events/GameEvent";
|
||||
import { GameEventType } from "../Events/GameEventType";
|
||||
|
||||
/**
|
||||
* Receives input events from the @reference[EventQueue] and allows for easy access of information about input by other systems
|
||||
*/
|
||||
export default class Input {
|
||||
private static mousePressed: boolean;
|
||||
private static mouseJustPressed: boolean;
|
||||
|
||||
private static keyJustPressed: Map<boolean>;
|
||||
private static keyPressed: Map<boolean>;
|
||||
|
||||
private static mousePosition: Vec2;
|
||||
private static mousePressPosition: Vec2;
|
||||
|
||||
private static scrollDirection: number;
|
||||
private static justScrolled: boolean;
|
||||
|
||||
private static eventQueue: EventQueue;
|
||||
private static receiver: Receiver;
|
||||
private static viewport: Viewport;
|
||||
|
||||
private static keyMap: Map<Array<string>>;
|
||||
|
||||
private static keysDisabled: boolean;
|
||||
private static mouseDisabled: boolean;
|
||||
|
||||
/**
|
||||
* Initializes the Input object
|
||||
* @param viewport A reference to the viewport of the game
|
||||
*/
|
||||
static initialize(viewport: Viewport, keyMap: Array<Record<string, any>>){
|
||||
Input.viewport = viewport;
|
||||
Input.mousePressed = false;
|
||||
Input.mouseJustPressed = false;
|
||||
Input.receiver = new Receiver();
|
||||
Input.keyJustPressed = new Map<boolean>();
|
||||
Input.keyPressed = new Map<boolean>();
|
||||
Input.mousePosition = new Vec2(0, 0);
|
||||
Input.mousePressPosition = new Vec2(0, 0);
|
||||
Input.scrollDirection = 0;
|
||||
Input.justScrolled = false;
|
||||
Input.keysDisabled = false;
|
||||
Input.mouseDisabled = false;
|
||||
|
||||
// Initialize the keymap
|
||||
Input.keyMap = new Map();
|
||||
|
||||
// Add all keys to the keymap
|
||||
for(let entry in keyMap){
|
||||
let name = keyMap[entry].name;
|
||||
let keys = keyMap[entry].keys;
|
||||
Input.keyMap.add(name, keys);
|
||||
}
|
||||
|
||||
Input.eventQueue = EventQueue.getInstance();
|
||||
// Subscribe to all input events
|
||||
Input.eventQueue.subscribe(Input.receiver, [GameEventType.MOUSE_DOWN, GameEventType.MOUSE_UP, GameEventType.MOUSE_MOVE,
|
||||
GameEventType.KEY_DOWN, GameEventType.KEY_UP, GameEventType.CANVAS_BLUR, GameEventType.WHEEL_UP, GameEventType.WHEEL_DOWN]);
|
||||
}
|
||||
|
||||
static update(deltaT: number): void {
|
||||
// Reset the justPressed values to false
|
||||
Input.mouseJustPressed = false;
|
||||
Input.keyJustPressed.forEach((key: string) => Input.keyJustPressed.set(key, false));
|
||||
Input.justScrolled = false;
|
||||
Input.scrollDirection = 0;
|
||||
|
||||
while(Input.receiver.hasNextEvent()){
|
||||
let event = Input.receiver.getNextEvent();
|
||||
|
||||
// Handle each event type
|
||||
if(event.type === GameEventType.MOUSE_DOWN){
|
||||
Input.mouseJustPressed = true;
|
||||
Input.mousePressed = true;
|
||||
Input.mousePressPosition = event.data.get("position");
|
||||
}
|
||||
|
||||
if(event.type === GameEventType.MOUSE_UP){
|
||||
Input.mousePressed = false;
|
||||
}
|
||||
|
||||
if(event.type === GameEventType.MOUSE_MOVE){
|
||||
Input.mousePosition = event.data.get("position");
|
||||
}
|
||||
|
||||
if(event.type === GameEventType.KEY_DOWN){
|
||||
let key = event.data.get("key");
|
||||
// Handle space bar
|
||||
if(key === " "){
|
||||
key = "space";
|
||||
}
|
||||
if(!Input.keyPressed.get(key)){
|
||||
Input.keyJustPressed.set(key, true);
|
||||
Input.keyPressed.set(key, true);
|
||||
}
|
||||
}
|
||||
|
||||
if(event.type === GameEventType.KEY_UP){
|
||||
let key = event.data.get("key");
|
||||
// Handle space bar
|
||||
if(key === " "){
|
||||
key = "space";
|
||||
}
|
||||
Input.keyPressed.set(key, false);
|
||||
}
|
||||
|
||||
if(event.type === GameEventType.CANVAS_BLUR){
|
||||
Input.clearKeyPresses()
|
||||
}
|
||||
|
||||
if(event.type === GameEventType.WHEEL_UP){
|
||||
Input.scrollDirection = -1;
|
||||
Input.justScrolled = true;
|
||||
} else if(event.type === GameEventType.WHEEL_DOWN){
|
||||
Input.scrollDirection = 1;
|
||||
Input.justScrolled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static clearKeyPresses(): void {
|
||||
Input.keyJustPressed.forEach((key: string) => Input.keyJustPressed.set(key, false));
|
||||
Input.keyPressed.forEach((key: string) => Input.keyPressed.set(key, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not a key was newly pressed Input frame.
|
||||
* If the key is still pressed from last frame and wasn't re-pressed, Input will return false.
|
||||
* @param key The key
|
||||
* @returns True if the key was just pressed, false otherwise
|
||||
*/
|
||||
static isKeyJustPressed(key: string): boolean {
|
||||
if(Input.keysDisabled) return false;
|
||||
|
||||
if(Input.keyJustPressed.has(key)){
|
||||
return Input.keyJustPressed.get(key)
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all of the keys that are newly pressed Input frame.
|
||||
* If a key is still pressed from last frame and wasn't re-pressed, it will not be in Input list.
|
||||
* @returns An array of all of the newly pressed keys.
|
||||
*/
|
||||
static getKeysJustPressed(): Array<string> {
|
||||
if(Input.keysDisabled) return [];
|
||||
|
||||
let keys = Array<string>();
|
||||
Input.keyJustPressed.forEach(key => {
|
||||
if(Input.keyJustPressed.get(key)){
|
||||
keys.push(key);
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not a key is being pressed.
|
||||
* @param key The key
|
||||
* @returns True if the key is currently pressed, false otherwise
|
||||
*/
|
||||
static isKeyPressed(key: string): boolean {
|
||||
if(Input.keysDisabled) return false;
|
||||
|
||||
if(Input.keyPressed.has(key)){
|
||||
return Input.keyPressed.get(key)
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the binding of an input name to keys
|
||||
* @param inputName The name of the input
|
||||
* @param keys The corresponding keys
|
||||
*/
|
||||
static changeKeyBinding(inputName: string, keys: Array<string>): void {
|
||||
Input.keyMap.set(inputName, keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all key bindings
|
||||
*/
|
||||
static clearAllKeyBindings(): void {
|
||||
Input.keyMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not an input was just pressed this frame
|
||||
* @param inputName The name of the input
|
||||
* @returns True if the input was just pressed, false otherwise
|
||||
*/
|
||||
static isJustPressed(inputName: string): boolean {
|
||||
if(Input.keysDisabled) return false;
|
||||
|
||||
if(Input.keyMap.has(inputName)){
|
||||
const keys = Input.keyMap.get(inputName);
|
||||
let justPressed = false;
|
||||
|
||||
for(let key of keys){
|
||||
justPressed = justPressed || Input.isKeyJustPressed(key);
|
||||
}
|
||||
|
||||
return justPressed;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not an input is currently pressed
|
||||
* @param inputName The name of the input
|
||||
* @returns True if the input is pressed, false otherwise
|
||||
*/
|
||||
static isPressed(inputName: string): boolean {
|
||||
if(Input.keysDisabled) return false;
|
||||
|
||||
if(Input.keyMap.has(inputName)){
|
||||
const keys = Input.keyMap.get(inputName);
|
||||
let pressed = false;
|
||||
|
||||
for(let key of keys){
|
||||
pressed = pressed || Input.isKeyPressed(key);
|
||||
}
|
||||
|
||||
return pressed;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the mouse was newly pressed Input frame
|
||||
* @returns True if the mouse was just pressed, false otherwise
|
||||
*/
|
||||
static isMouseJustPressed(): boolean {
|
||||
return Input.mouseJustPressed && !Input.mouseDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the mouse is currently pressed
|
||||
* @returns True if the mouse is currently pressed, false otherwise
|
||||
*/
|
||||
static isMousePressed(): boolean {
|
||||
return Input.mousePressed && !Input.mouseDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the user scrolled or not
|
||||
* @returns True if the user just scrolled Input frame, false otherwise
|
||||
*/
|
||||
static didJustScroll(): boolean {
|
||||
return Input.justScrolled && !Input.mouseDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the direction of the scroll
|
||||
* @returns -1 if the user scrolled up, 1 if they scrolled down
|
||||
*/
|
||||
static getScrollDirection(): number {
|
||||
return Input.scrollDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the position of the player's mouse
|
||||
* @returns The mouse position stored as a Vec2
|
||||
*/
|
||||
static getMousePosition(): Vec2 {
|
||||
return Input.mousePosition.scaled(1/this.viewport.getZoomLevel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the position of the player's mouse in the game world,
|
||||
* taking into consideration the scrolling of the viewport
|
||||
* @returns The mouse position stored as a Vec2
|
||||
*/
|
||||
static getGlobalMousePosition(): Vec2 {
|
||||
return Input.mousePosition.clone().scale(1/this.viewport.getZoomLevel()).add(Input.viewport.getOrigin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the position of the last mouse press
|
||||
* @returns The mouse position stored as a Vec2
|
||||
*/
|
||||
static getMousePressPosition(): Vec2 {
|
||||
return Input.mousePressPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the position of the last mouse press in the game world,
|
||||
* taking into consideration the scrolling of the viewport
|
||||
* @returns The mouse position stored as a Vec2
|
||||
*/
|
||||
static getGlobalMousePressPosition(): Vec2 {
|
||||
return Input.mousePressPosition.clone().add(Input.viewport.getOrigin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables all keypress and mouse click inputs
|
||||
*/
|
||||
static disableInput(): void {
|
||||
Input.keysDisabled = true;
|
||||
Input.mouseDisabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables all keypress and mouse click inputs
|
||||
*/
|
||||
static enableInput(): void {
|
||||
Input.keysDisabled = false;
|
||||
Input.mouseDisabled = false;
|
||||
}
|
||||
}
|
93
hw3/src/Wolfie2D/Input/InputHandler.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
import EventQueue from "../Events/EventQueue";
|
||||
import Vec2 from "../DataTypes/Vec2";
|
||||
import GameEvent from "../Events/GameEvent";
|
||||
import { GameEventType } from "../Events/GameEventType";
|
||||
|
||||
/**
|
||||
* Handles communication with the web browser to receive asynchronous events and send them to the @reference[EventQueue]
|
||||
*/
|
||||
export default class InputHandler {
|
||||
private eventQueue: EventQueue;
|
||||
|
||||
/**
|
||||
* Creates a new InputHandler
|
||||
* @param canvas The game canvas
|
||||
*/
|
||||
constructor(canvas: HTMLCanvasElement){
|
||||
this.eventQueue = EventQueue.getInstance();
|
||||
|
||||
canvas.onmousedown = (event) => this.handleMouseDown(event, canvas);
|
||||
canvas.onmouseup = (event) => this.handleMouseUp(event, canvas);
|
||||
canvas.oncontextmenu = this.handleContextMenu;
|
||||
canvas.onmousemove = (event) => this.handleMouseMove(event, canvas);
|
||||
document.onkeydown = this.handleKeyDown;
|
||||
document.onkeyup = this.handleKeyUp;
|
||||
document.onblur = this.handleBlur;
|
||||
document.oncontextmenu = this.handleBlur;
|
||||
document.onwheel = this.handleWheel;
|
||||
}
|
||||
|
||||
private handleMouseDown = (event: MouseEvent, canvas: HTMLCanvasElement): void => {
|
||||
let pos = this.getMousePosition(event, canvas);
|
||||
let gameEvent = new GameEvent(GameEventType.MOUSE_DOWN, {position: pos});
|
||||
this.eventQueue.addEvent(gameEvent);
|
||||
}
|
||||
|
||||
private handleMouseUp = (event: MouseEvent, canvas: HTMLCanvasElement): void => {
|
||||
let pos = this.getMousePosition(event, canvas);
|
||||
let gameEvent = new GameEvent(GameEventType.MOUSE_UP, {position: pos});
|
||||
this.eventQueue.addEvent(gameEvent);
|
||||
}
|
||||
|
||||
private handleMouseMove = (event: MouseEvent, canvas: HTMLCanvasElement): void => {
|
||||
let pos = this.getMousePosition(event, canvas);
|
||||
let gameEvent = new GameEvent(GameEventType.MOUSE_MOVE, {position: pos});
|
||||
this.eventQueue.addEvent(gameEvent);
|
||||
}
|
||||
|
||||
private handleKeyDown = (event: KeyboardEvent): void => {
|
||||
let key = this.getKey(event);
|
||||
let gameEvent = new GameEvent(GameEventType.KEY_DOWN, {key: key});
|
||||
this.eventQueue.addEvent(gameEvent);
|
||||
}
|
||||
|
||||
private handleKeyUp = (event: KeyboardEvent): void => {
|
||||
let key = this.getKey(event);
|
||||
let gameEvent = new GameEvent(GameEventType.KEY_UP, {key: key});
|
||||
this.eventQueue.addEvent(gameEvent);
|
||||
}
|
||||
|
||||
private handleBlur = (event: Event): void => {
|
||||
let gameEvent = new GameEvent(GameEventType.CANVAS_BLUR, {});
|
||||
this.eventQueue.addEvent(gameEvent);
|
||||
}
|
||||
|
||||
private handleContextMenu = (event: Event): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
private handleWheel = (event: WheelEvent): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
let gameEvent: GameEvent;
|
||||
if(event.deltaY < 0){
|
||||
gameEvent = new GameEvent(GameEventType.WHEEL_UP, {});
|
||||
} else {
|
||||
gameEvent = new GameEvent(GameEventType.WHEEL_DOWN, {});
|
||||
}
|
||||
this.eventQueue.addEvent(gameEvent);
|
||||
}
|
||||
|
||||
private getKey(keyEvent: KeyboardEvent){
|
||||
return keyEvent.key.toLowerCase();
|
||||
}
|
||||
|
||||
private getMousePosition(mouseEvent: MouseEvent, canvas: HTMLCanvasElement): Vec2 {
|
||||
let rect = canvas.getBoundingClientRect();
|
||||
let x = mouseEvent.clientX - rect.left;
|
||||
let y = mouseEvent.clientY - rect.top;
|
||||
return new Vec2(x, y);
|
||||
}
|
||||
}
|
47
hw3/src/Wolfie2D/Loop/EnvironmentInitializer.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import {} from "../../index"; // This import allows us to modify the CanvasRenderingContext2D to add extra functionality
|
||||
// @ignorePage
|
||||
|
||||
/**
|
||||
* Sets up the environment of the game engine
|
||||
*/
|
||||
export default class EnvironmentInitializer {
|
||||
static setup(){
|
||||
CanvasRenderingContext2D.prototype.roundedRect = function(x: number, y: number, w: number, h: number, r: number): void {
|
||||
// Clamp the radius between 0 and the min of the width or height
|
||||
if(r < 0) r = 0;
|
||||
if(r > Math.min(w, h)) r = Math.min(w, h);
|
||||
|
||||
// Draw the rounded rect
|
||||
this.beginPath();
|
||||
|
||||
// Top
|
||||
this.moveTo(x + r, y);
|
||||
this.lineTo(x + w - r, y);
|
||||
this.arcTo(x + w, y, x + w, y + r, r);
|
||||
|
||||
// Right
|
||||
this.lineTo(x + w, y + h - r);
|
||||
this.arcTo(x + w, y + h, x + w - r, y + h, r);
|
||||
|
||||
// Bottom
|
||||
this.lineTo(x + r, y + h);
|
||||
this.arcTo(x, y + h, x, y + h - r, r);
|
||||
|
||||
// Left
|
||||
this.lineTo(x, y + r);
|
||||
this.arcTo(x, y, x + r, y, r)
|
||||
|
||||
this.closePath();
|
||||
}
|
||||
|
||||
CanvasRenderingContext2D.prototype.strokeRoundedRect = function(x, y, w, h, r){
|
||||
this.roundedRect(x, y, w, h, r);
|
||||
this.stroke();
|
||||
}
|
||||
|
||||
CanvasRenderingContext2D.prototype.fillRoundedRect = function(x, y, w, h, r){
|
||||
this.roundedRect(x, y, w, h, r);
|
||||
this.fill();
|
||||
}
|
||||
}
|
||||
}
|
235
hw3/src/Wolfie2D/Loop/FixedUpdateGameLoop.ts
Normal file
|
@ -0,0 +1,235 @@
|
|||
import GameLoop from "./GameLoop";
|
||||
import Debug from "../Debug/Debug";
|
||||
import Stats from "../Debug/Stats";
|
||||
|
||||
/**
|
||||
* A game loop with a fixed update time and a variable render time.
|
||||
* Every frame, the game updates until all time since the last frame has been processed.
|
||||
* If too much time has passed, such as if the last update was too slow,
|
||||
* or if the browser was put into the background, the loop will panic and discard time.
|
||||
* A render happens at the end of every frame. This happens as fast as possible unless specified.
|
||||
* A loop of this type allows for deterministic behavior - No matter what the frame rate is, the update should behave the same,
|
||||
* as it is occuring in a fixed interval.
|
||||
*/
|
||||
export default class FixedUpdateGameLoop extends GameLoop {
|
||||
|
||||
/** The max allowed update fps.*/
|
||||
private maxUpdateFPS: number;
|
||||
|
||||
/** The timestep for each update. This is the deltaT passed to update calls. */
|
||||
private updateTimestep: number;
|
||||
|
||||
/** The amount of time we are yet to simulate. */
|
||||
private frameDelta: number;
|
||||
|
||||
/** The time when the last frame was drawn. */
|
||||
private lastFrameTime: number;
|
||||
|
||||
/** The minimum time we want to wait between game frames. */
|
||||
private minFrameDelay: number;
|
||||
|
||||
/** The current frame of the game. */
|
||||
private frame: number;
|
||||
|
||||
/** The actual fps of the game. */
|
||||
private fps: number;
|
||||
|
||||
/** The time between fps measurement updates. */
|
||||
private fpsUpdateInterval: number;
|
||||
|
||||
/** The time of the last fps update. */
|
||||
private lastFpsUpdate: number;
|
||||
|
||||
/** The number of frames since the last fps update was done. */
|
||||
private framesSinceLastFpsUpdate: number;
|
||||
|
||||
/** The status of whether or not the game loop has started. */
|
||||
private started: boolean;
|
||||
|
||||
/** The status of whether or not the game loop is paused */
|
||||
private paused: boolean;
|
||||
|
||||
/** The status of whether or not the game loop is currently running. */
|
||||
private running: boolean;
|
||||
|
||||
/** The number of update steps this iteration of the game loop. */
|
||||
private numUpdateSteps: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.maxUpdateFPS = 60;
|
||||
this.updateTimestep = Math.floor(1000/this.maxUpdateFPS);
|
||||
this.frameDelta = 0;
|
||||
this.lastFrameTime = 0;
|
||||
this.minFrameDelay = 0;
|
||||
this.frame = 0;
|
||||
this.fps = this.maxUpdateFPS; // Initialize the fps to the max allowed fps
|
||||
this.fpsUpdateInterval = 1000;
|
||||
this.lastFpsUpdate = 0;
|
||||
this.framesSinceLastFpsUpdate = 0;
|
||||
this.started = false;
|
||||
this.paused = false;
|
||||
this.running = false;
|
||||
this.numUpdateSteps = 0;
|
||||
}
|
||||
|
||||
getFPS(): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the frame count and sum of time for the framerate of the game
|
||||
* @param timestep The current time in ms
|
||||
*/
|
||||
protected updateFPS(timestamp: number): void {
|
||||
this.fps = 0.9 * this.framesSinceLastFpsUpdate * 1000 / (timestamp - this.lastFpsUpdate) +(1 - 0.9) * this.fps;
|
||||
this.lastFpsUpdate = timestamp;
|
||||
this.framesSinceLastFpsUpdate = 0;
|
||||
|
||||
Debug.log("fps", "FPS: " + this.fps.toFixed(1));
|
||||
Stats.updateFPS(this.fps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the maximum allowed physics framerate of the game
|
||||
* @param initMax The max framerate
|
||||
*/
|
||||
setMaxUpdateFPS(initMax: number): void {
|
||||
this.maxUpdateFPS = initMax;
|
||||
this.updateTimestep = Math.floor(1000/this.maxUpdateFPS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum rendering framerate
|
||||
* @param maxFPS The max framerate
|
||||
*/
|
||||
setMaxFPS(maxFPS: number): void {
|
||||
this.minFrameDelay = 1000/maxFPS;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called when the game loop panics, i.e. it tries to process too much time in an entire frame.
|
||||
* This will reset the amount of time back to zero.
|
||||
* @returns The amount of time we are discarding from processing.
|
||||
*/
|
||||
resetFrameDelta() : number {
|
||||
let oldFrameDelta = this.frameDelta;
|
||||
this.frameDelta = 0;
|
||||
return oldFrameDelta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts up the game loop and calls the first requestAnimationFrame
|
||||
*/
|
||||
start(): void {
|
||||
if(!this.started){
|
||||
this.started = true;
|
||||
|
||||
window.requestAnimationFrame((timestamp) => this.doFirstFrame(timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this.paused = true;
|
||||
}
|
||||
|
||||
resume(): void {
|
||||
this.paused = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first game frame - initializes the first frame time and begins the render
|
||||
* @param timestamp The current time in ms
|
||||
*/
|
||||
protected doFirstFrame(timestamp: number): void {
|
||||
this.running = true;
|
||||
|
||||
this._doRender();
|
||||
|
||||
this.lastFrameTime = timestamp;
|
||||
this.lastFpsUpdate = timestamp;
|
||||
this.framesSinceLastFpsUpdate = 0;
|
||||
|
||||
window.requestAnimationFrame((t) => this.doFrame(t));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles any processing that needs to be done at the start of the frame
|
||||
* @param timestamp The time of the frame in ms
|
||||
*/
|
||||
protected startFrame(timestamp: number): void {
|
||||
// Update the amount of time we need our update to process
|
||||
this.frameDelta += timestamp - this.lastFrameTime;
|
||||
|
||||
// Set the new time of the last frame
|
||||
this.lastFrameTime = timestamp;
|
||||
|
||||
// Update the estimate of the framerate
|
||||
if(timestamp > this.lastFpsUpdate + this.fpsUpdateInterval){
|
||||
this.updateFPS(timestamp);
|
||||
}
|
||||
|
||||
// Increment the number of frames
|
||||
this.frame++;
|
||||
this.framesSinceLastFpsUpdate++;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main loop of the game. Updates until the current time is reached. Renders once
|
||||
* @param timestamp The current time in ms
|
||||
*/
|
||||
protected doFrame = (timestamp: number): void => {
|
||||
// If a pause was executed, stop doing the loop.
|
||||
if(this.paused){
|
||||
return;
|
||||
}
|
||||
|
||||
// Request animation frame to prepare for another update or render
|
||||
window.requestAnimationFrame((t) => this.doFrame(t));
|
||||
|
||||
// If we are trying to render too soon, do nothing.
|
||||
if(timestamp < this.lastFrameTime + this.minFrameDelay){
|
||||
return;
|
||||
}
|
||||
|
||||
// A frame is actually happening
|
||||
this.startFrame(timestamp);
|
||||
|
||||
// Update while there is still time to make up. If we do too many update steps, panic and exit the loop.
|
||||
this.numUpdateSteps = 0;
|
||||
let panic = false;
|
||||
|
||||
while(this.frameDelta >= this.updateTimestep){
|
||||
// Do an update
|
||||
this._doUpdate(this.updateTimestep/1000);
|
||||
|
||||
// Remove the update step time from the time we have to process
|
||||
this.frameDelta -= this.updateTimestep;
|
||||
|
||||
// Increment steps and check if we've done too many
|
||||
this.numUpdateSteps++;
|
||||
if(this.numUpdateSteps > 100){
|
||||
panic = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Updates are done, render
|
||||
this._doRender();
|
||||
|
||||
// Wrap up the frame
|
||||
this.finishFrame(panic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps up the frame and handles the panic state if there is one
|
||||
* @param panic Whether or not the loop panicked
|
||||
*/
|
||||
protected finishFrame(panic: boolean): void {
|
||||
if(panic) {
|
||||
var discardedTime = Math.round(this.resetFrameDelta());
|
||||
console.warn('Main loop panicked, probably because the browser tab was put in the background. Discarding ' + discardedTime + 'ms');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
214
hw3/src/Wolfie2D/Loop/Game.ts
Normal file
|
@ -0,0 +1,214 @@
|
|||
import EventQueue from "../Events/EventQueue";
|
||||
import Input from "../Input/Input";
|
||||
import InputHandler from "../Input/InputHandler";
|
||||
import Recorder from "../Playback/Recorder";
|
||||
import Debug from "../Debug/Debug";
|
||||
import ResourceManager from "../ResourceManager/ResourceManager";
|
||||
import Viewport from "../SceneGraph/Viewport";
|
||||
import SceneManager from "../Scene/SceneManager";
|
||||
import AudioManager from "../Sound/AudioManager";
|
||||
import Stats from "../Debug/Stats";
|
||||
import RenderingManager from "../Rendering/RenderingManager";
|
||||
import CanvasRenderer from "../Rendering/CanvasRenderer";
|
||||
import Color from "../Utils/Color";
|
||||
import GameOptions from "./GameOptions";
|
||||
import GameLoop from "./GameLoop";
|
||||
import FixedUpdateGameLoop from "./FixedUpdateGameLoop";
|
||||
import EnvironmentInitializer from "./EnvironmentInitializer";
|
||||
import Vec2 from "../DataTypes/Vec2";
|
||||
import RegistryManager from "../Registry/RegistryManager";
|
||||
import WebGLRenderer from "../Rendering/WebGLRenderer";
|
||||
import Scene from "../Scene/Scene";
|
||||
|
||||
/**
|
||||
* The main loop of the game engine.
|
||||
* Handles the update order, and initializes all subsystems.
|
||||
* The Game manages the update cycle, and requests animation frames to render to the browser.
|
||||
*/
|
||||
export default class Game {
|
||||
gameOptions: GameOptions;
|
||||
private showDebug: boolean;
|
||||
private showStats: boolean;
|
||||
|
||||
// The game loop
|
||||
private loop: GameLoop;
|
||||
|
||||
// Game canvas and its width and height
|
||||
readonly GAME_CANVAS: HTMLCanvasElement;
|
||||
readonly DEBUG_CANVAS: HTMLCanvasElement;
|
||||
readonly WIDTH: number;
|
||||
readonly HEIGHT: number;
|
||||
private viewport: Viewport;
|
||||
private ctx: CanvasRenderingContext2D | WebGLRenderingContext;
|
||||
private clearColor: Color;
|
||||
|
||||
// All of the necessary subsystems that need to run here
|
||||
private eventQueue: EventQueue;
|
||||
private inputHandler: InputHandler;
|
||||
private recorder: Recorder;
|
||||
private resourceManager: ResourceManager;
|
||||
private sceneManager: SceneManager;
|
||||
private audioManager: AudioManager;
|
||||
private renderingManager: RenderingManager;
|
||||
|
||||
/**
|
||||
* Creates a new Game
|
||||
* @param options The options for Game initialization
|
||||
*/
|
||||
constructor(options?: Record<string, any>){
|
||||
// Before anything else, build the environment
|
||||
EnvironmentInitializer.setup();
|
||||
|
||||
// Typecast the config object to a GameConfig object
|
||||
this.gameOptions = GameOptions.parse(options);
|
||||
|
||||
this.showDebug = this.gameOptions.showDebug;
|
||||
this.showStats = this.gameOptions.showStats;
|
||||
|
||||
// Create an instance of a game loop
|
||||
this.loop = new FixedUpdateGameLoop();
|
||||
|
||||
// Get the game canvas and give it a background color
|
||||
this.GAME_CANVAS = <HTMLCanvasElement>document.getElementById("game-canvas");
|
||||
this.DEBUG_CANVAS = <HTMLCanvasElement>document.getElementById("debug-canvas");
|
||||
|
||||
// Give the canvas a size and get the rendering context
|
||||
this.WIDTH = this.gameOptions.canvasSize.x;
|
||||
this.HEIGHT = this.gameOptions.canvasSize.y;
|
||||
|
||||
// This step MUST happen before the resource manager does anything
|
||||
if(this.gameOptions.useWebGL){
|
||||
this.renderingManager = new WebGLRenderer();
|
||||
} else {
|
||||
this.renderingManager = new CanvasRenderer();
|
||||
}
|
||||
this.initializeGameWindow();
|
||||
this.ctx = this.renderingManager.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT);
|
||||
this.clearColor = new Color(this.gameOptions.clearColor.r, this.gameOptions.clearColor.g, this.gameOptions.clearColor.b);
|
||||
|
||||
// Initialize debugging and stats
|
||||
Debug.initializeDebugCanvas(this.DEBUG_CANVAS, this.WIDTH, this.HEIGHT);
|
||||
Stats.initStats();
|
||||
|
||||
if(this.gameOptions.showStats) {
|
||||
// Find the stats output and make it no longer hidden
|
||||
document.getElementById("stats").hidden = false;
|
||||
}
|
||||
|
||||
// Size the viewport to the game canvas
|
||||
const canvasSize = new Vec2(this.WIDTH, this.HEIGHT);
|
||||
this.viewport = new Viewport(canvasSize, this.gameOptions.zoomLevel);
|
||||
|
||||
// Initialize all necessary game subsystems
|
||||
this.eventQueue = EventQueue.getInstance();
|
||||
this.inputHandler = new InputHandler(this.GAME_CANVAS);
|
||||
Input.initialize(this.viewport, this.gameOptions.inputs);
|
||||
this.recorder = new Recorder();
|
||||
this.resourceManager = ResourceManager.getInstance();
|
||||
this.sceneManager = new SceneManager(this.viewport, this.renderingManager);
|
||||
this.audioManager = AudioManager.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the game window that holds the canvases
|
||||
*/
|
||||
private initializeGameWindow(): void {
|
||||
const gameWindow = document.getElementById("game-window");
|
||||
|
||||
// Set the height of the game window
|
||||
gameWindow.style.width = this.WIDTH + "px";
|
||||
gameWindow.style.height = this.HEIGHT + "px";
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreives the SceneManager from the Game
|
||||
* @returns The SceneManager
|
||||
*/
|
||||
getSceneManager(): SceneManager {
|
||||
return this.sceneManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the game
|
||||
*/
|
||||
start(InitialScene: new (...args: any) => Scene, options: Record<string, any>): void {
|
||||
// Set the update function of the loop
|
||||
this.loop.doUpdate = (deltaT: number) => this.update(deltaT);
|
||||
|
||||
// Set the render function of the loop
|
||||
this.loop.doRender = () => this.render();
|
||||
|
||||
// Preload registry items
|
||||
RegistryManager.preload();
|
||||
|
||||
// Load the items with the resource manager
|
||||
this.resourceManager.loadResourcesFromQueue(() => {
|
||||
// When we're done loading, start the loop
|
||||
console.log("Finished Preload - loading first scene");
|
||||
this.sceneManager.changeToScene(InitialScene, {}, options);
|
||||
this.loop.start();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all necessary subsystems of the game. Defers scene updates to the sceneManager
|
||||
* @param deltaT The time sine the last update
|
||||
*/
|
||||
update(deltaT: number): void {
|
||||
try{
|
||||
// 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
|
||||
Input.update(deltaT);
|
||||
|
||||
// Update the recording of the game
|
||||
this.recorder.update(deltaT);
|
||||
|
||||
// Update all scenes
|
||||
this.sceneManager.update(deltaT);
|
||||
|
||||
// Update all sounds
|
||||
this.audioManager.update(deltaT);
|
||||
|
||||
// Load or unload any resources if needed
|
||||
this.resourceManager.update(deltaT);
|
||||
} catch(e){
|
||||
this.loop.pause();
|
||||
console.warn("Uncaught Error in Update - Crashing gracefully");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the canvas and defers scene rendering to the sceneManager. Renders the debug canvas
|
||||
*/
|
||||
render(): void {
|
||||
try{
|
||||
// Clear the canvases
|
||||
Debug.clearCanvas();
|
||||
|
||||
this.renderingManager.clear(this.clearColor);
|
||||
|
||||
this.sceneManager.render();
|
||||
|
||||
// Hacky debug mode
|
||||
if(Input.isKeyJustPressed("g")){
|
||||
this.showDebug = !this.showDebug;
|
||||
}
|
||||
|
||||
// Debug render
|
||||
if(this.showDebug){
|
||||
Debug.render();
|
||||
}
|
||||
|
||||
if(this.showStats){
|
||||
Stats.render();
|
||||
}
|
||||
} catch(e){
|
||||
this.loop.pause();
|
||||
console.warn("Uncaught Error in Render - Crashing gracefully");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
69
hw3/src/Wolfie2D/Loop/GameLoop.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import NullFunc from "../DataTypes/Functions/NullFunc";
|
||||
|
||||
/**
|
||||
* The main game loop of the game. Keeps track of fps and handles scheduling of updates and rendering.
|
||||
* This class is left abstract, so that a subclass can handle exactly how the loop is scheduled.
|
||||
* For an example of different types of game loop scheduling, check out @link(Game Programming Patterns)(https://gameprogrammingpatterns.com/game-loop.html)
|
||||
*/
|
||||
export default abstract class GameLoop {
|
||||
|
||||
/** The function to call when an update occurs */
|
||||
protected _doUpdate: Function = NullFunc;
|
||||
|
||||
set doUpdate(update: Function){
|
||||
this._doUpdate = update;
|
||||
}
|
||||
|
||||
/** The function to call when a render occurs */
|
||||
protected _doRender: Function = NullFunc;
|
||||
|
||||
|
||||
set doRender(render: Function){
|
||||
this._doRender = render;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current FPS of the game
|
||||
*/
|
||||
abstract getFPS(): number;
|
||||
|
||||
/**
|
||||
* Starts up the game loop
|
||||
*/
|
||||
abstract start(): void;
|
||||
|
||||
/**
|
||||
* Pauses the game loop, usually for an error condition.
|
||||
*/
|
||||
abstract pause(): void;
|
||||
|
||||
/**
|
||||
* Resumes the game loop.
|
||||
*/
|
||||
abstract resume(): void;
|
||||
|
||||
/**
|
||||
* Runs the first frame of the game. No update occurs here, only a render.
|
||||
* This is needed to initialize delta time values
|
||||
* @param timestamp The timestamp of the frame. This is received from the browser
|
||||
*/
|
||||
protected abstract doFirstFrame(timestamp: number): void;
|
||||
|
||||
/**
|
||||
* Run before any updates or the render of a frame.
|
||||
* @param timestamp The timestamp of the frame. This is received from the browser
|
||||
*/
|
||||
protected abstract startFrame(timestamp: number): void;
|
||||
|
||||
/**
|
||||
* The core of the frame, where any necessary updates occur, and where a render happens
|
||||
* @param timestamp The timestamp of the frame. This is received from the browser
|
||||
*/
|
||||
protected abstract doFrame(timestamp: number): void;
|
||||
|
||||
/**
|
||||
* Wraps up the frame
|
||||
* @param panic Whether or not the update cycle panicked. This happens when too many updates try to happen in a single frame
|
||||
*/
|
||||
protected abstract finishFrame(panic: boolean): void;
|
||||
}
|
44
hw3/src/Wolfie2D/Loop/GameOptions.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
// @ignorePage
|
||||
|
||||
/** The options for initializing the @reference[GameLoop] */
|
||||
export default class GameOptions {
|
||||
/** The size of the viewport */
|
||||
canvasSize: {x: number, y: number};
|
||||
|
||||
/* The default level of zoom */
|
||||
zoomLevel: number;
|
||||
|
||||
/** The color to clear the canvas to each frame */
|
||||
clearColor: {r: number, g: number, b: number}
|
||||
|
||||
/* A list of input bindings */
|
||||
inputs: Array<{name: string, keys: Array<string>}>;
|
||||
|
||||
/* Whether or not the debug rendering should occur */
|
||||
showDebug: boolean;
|
||||
|
||||
/* Whether or not the stats rendering should occur */
|
||||
showStats: boolean;
|
||||
|
||||
/* Whether or not to use webGL */
|
||||
useWebGL: boolean;
|
||||
|
||||
/**
|
||||
* Parses the data in the raw options object
|
||||
* @param options The game options as a Record
|
||||
* @returns A version of the options converted to a GameOptions object
|
||||
*/
|
||||
static parse(options: Record<string, any>): GameOptions {
|
||||
let gOpt = new GameOptions();
|
||||
|
||||
gOpt.canvasSize = options.canvasSize ? options.canvasSize : {x: 800, y: 600};
|
||||
gOpt.zoomLevel = options.zoomLevel ? options.zoomLevel : 1;
|
||||
gOpt.clearColor = options.clearColor ? options.clearColor : {r: 255, g: 255, b: 255};
|
||||
gOpt.inputs = options.inputs ? options.inputs : [];
|
||||
gOpt.showDebug = !!options.showDebug;
|
||||
gOpt.showStats = !!options.showStats;
|
||||
gOpt.useWebGL = !!options.useWebGL;
|
||||
|
||||
return gOpt;
|
||||
}
|
||||
}
|
137
hw3/src/Wolfie2D/Nodes/CanvasNode.ts
Normal file
|
@ -0,0 +1,137 @@
|
|||
import GameNode from "./GameNode";
|
||||
import Vec2 from "../DataTypes/Vec2";
|
||||
import Region from "../DataTypes/Interfaces/Region";
|
||||
import AABB from "../DataTypes/Shapes/AABB";
|
||||
import Debug from "../Debug/Debug";
|
||||
import Color from "../Utils/Color";
|
||||
|
||||
/**
|
||||
* The representation of an object in the game world that can be drawn to the screen
|
||||
*/
|
||||
export default abstract class CanvasNode extends GameNode implements Region {
|
||||
private _size: Vec2;
|
||||
private _scale: Vec2;
|
||||
private _boundary: AABB;
|
||||
private _hasCustomShader: boolean;
|
||||
private _customShaderKey: string;
|
||||
private _alpha: number;
|
||||
|
||||
/** A flag for whether or not the CanvasNode is visible */
|
||||
visible: boolean = true;
|
||||
|
||||
constructor(){
|
||||
super();
|
||||
this._size = new Vec2(0, 0);
|
||||
this._size.setOnChange(() => this.sizeChanged());
|
||||
this._scale = new Vec2(1, 1);
|
||||
this._scale.setOnChange(() => this.scaleChanged());
|
||||
this._boundary = new AABB();
|
||||
this.updateBoundary();
|
||||
|
||||
this._hasCustomShader = false;
|
||||
}
|
||||
|
||||
get alpha(): number {
|
||||
return this._alpha;
|
||||
}
|
||||
|
||||
set alpha(a: number) {
|
||||
this._alpha = a;
|
||||
}
|
||||
|
||||
get size(): Vec2 {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
set size(size: Vec2){
|
||||
this._size = size;
|
||||
// Enter as a lambda to bind "this"
|
||||
this._size.setOnChange(() => this.sizeChanged());
|
||||
this.sizeChanged();
|
||||
}
|
||||
|
||||
get scale(): Vec2 {
|
||||
return this._scale;
|
||||
}
|
||||
|
||||
set scale(scale: Vec2){
|
||||
this._scale = scale;
|
||||
// Enter as a lambda to bind "this"
|
||||
this._scale.setOnChange(() => this.scaleChanged());
|
||||
this.scaleChanged();
|
||||
}
|
||||
|
||||
set scaleX(value: number) {
|
||||
this.scale.x = value;
|
||||
}
|
||||
|
||||
set scaleY(value: number) {
|
||||
this.scale.y = value;
|
||||
}
|
||||
|
||||
get hasCustomShader(): boolean {
|
||||
return this._hasCustomShader;
|
||||
}
|
||||
|
||||
get customShaderKey(): string {
|
||||
return this._customShaderKey;
|
||||
}
|
||||
|
||||
// @override
|
||||
protected positionChanged(): void {
|
||||
super.positionChanged();
|
||||
this.updateBoundary();
|
||||
}
|
||||
|
||||
/** Called if the size vector is changed or replaced. */
|
||||
protected sizeChanged(): void {
|
||||
this.updateBoundary();
|
||||
}
|
||||
|
||||
/** Called if the scale vector is changed or replaced */
|
||||
protected scaleChanged(): void {
|
||||
this.updateBoundary();
|
||||
}
|
||||
|
||||
// @docIgnore
|
||||
/** Called if the position, size, or scale of the CanvasNode is changed. Updates the boundary. */
|
||||
private updateBoundary(): void {
|
||||
this._boundary.center.set(this.position.x, this.position.y);
|
||||
this._boundary.halfSize.set(this.size.x*this.scale.x/2, this.size.y*this.scale.y/2);
|
||||
}
|
||||
|
||||
get boundary(): AABB {
|
||||
return this._boundary;
|
||||
}
|
||||
|
||||
get sizeWithZoom(): Vec2 {
|
||||
let zoom = this.scene.getViewScale();
|
||||
|
||||
return this.boundary.halfSize.clone().scaled(zoom, zoom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom shader to this CanvasNode
|
||||
* @param key The registry key of the ShaderType
|
||||
*/
|
||||
useCustomShader(key: string): void {
|
||||
this._hasCustomShader = true;
|
||||
this._customShaderKey = key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the point (x, y) is inside of this canvas object
|
||||
* @param x The x position of the point
|
||||
* @param y The y position of the point
|
||||
* @returns A flag representing whether or not this node contains the point.
|
||||
*/
|
||||
contains(x: number, y: number): boolean {
|
||||
return this._boundary.containsPoint(new Vec2(x, y));
|
||||
}
|
||||
|
||||
// @implemented
|
||||
debugRender(): void {
|
||||
Debug.drawBox(this.relativePosition, this.sizeWithZoom, false, Color.BLUE);
|
||||
super.debugRender();
|
||||
}
|
||||
}
|
478
hw3/src/Wolfie2D/Nodes/GameNode.ts
Normal file
|
@ -0,0 +1,478 @@
|
|||
import Vec2 from "../DataTypes/Vec2";
|
||||
import Receiver from "../Events/Receiver";
|
||||
import Emitter from "../Events/Emitter";
|
||||
import Scene from "../Scene/Scene";
|
||||
import Layer from "../Scene/Layer";
|
||||
import AI from "../DataTypes/Interfaces/AI";
|
||||
import Physical from "../DataTypes/Interfaces/Physical";
|
||||
import Positioned from "../DataTypes/Interfaces/Positioned";
|
||||
import { isRegion } from "../DataTypes/Interfaces/Region";
|
||||
import Unique from "../DataTypes/Interfaces/Unique";
|
||||
import Updateable from "../DataTypes/Interfaces/Updateable";
|
||||
import DebugRenderable from "../DataTypes/Interfaces/DebugRenderable";
|
||||
import Actor from "../DataTypes/Interfaces/Actor";
|
||||
import Shape from "../DataTypes/Shapes/Shape";
|
||||
import AABB from "../DataTypes/Shapes/AABB";
|
||||
import NavigationPath from "../Pathfinding/NavigationPath";
|
||||
import TweenController from "../Rendering/Animations/TweenController";
|
||||
import Debug from "../Debug/Debug";
|
||||
import Color from "../Utils/Color";
|
||||
import Circle from "../DataTypes/Shapes/Circle";
|
||||
|
||||
/**
|
||||
* The representation of an object in the game world.
|
||||
* To construct GameNodes, see the @reference[Scene] documentation.
|
||||
*/
|
||||
export default abstract class GameNode implements Positioned, Unique, Updateable, Physical, Actor, DebugRenderable {
|
||||
/*---------- POSITIONED ----------*/
|
||||
private _position: Vec2;
|
||||
|
||||
/*---------- UNIQUE ----------*/
|
||||
private _id: number;
|
||||
|
||||
/*---------- PHYSICAL ----------*/
|
||||
hasPhysics: boolean = false;
|
||||
moving: boolean = false;
|
||||
frozen: boolean = false;
|
||||
onGround: boolean = false;
|
||||
onWall: boolean = false;
|
||||
onCeiling: boolean = false;
|
||||
active: boolean = false;
|
||||
collisionShape: Shape;
|
||||
colliderOffset: Vec2;
|
||||
isStatic: boolean;
|
||||
isCollidable: boolean;
|
||||
isTrigger: boolean;
|
||||
triggerMask: number;
|
||||
triggerEnters: Array<string>;
|
||||
triggerExits: Array<string>;
|
||||
_velocity: Vec2;
|
||||
sweptRect: AABB;
|
||||
collidedWithTilemap: boolean;
|
||||
group: number;
|
||||
isPlayer: boolean;
|
||||
isColliding: boolean = false;
|
||||
|
||||
/*---------- ACTOR ----------*/
|
||||
_ai: AI;
|
||||
aiActive: boolean;
|
||||
path: NavigationPath;
|
||||
pathfinding: boolean = false;
|
||||
|
||||
/*---------- GENERAL ----------*/
|
||||
/** An event receiver. */
|
||||
protected receiver: Receiver;
|
||||
/** An event emitter. */
|
||||
protected emitter: Emitter;
|
||||
/** A reference to the scene this GameNode is a part of. */
|
||||
protected scene: Scene;
|
||||
/** The visual layer this GameNode resides in. */
|
||||
protected layer: Layer;
|
||||
/** A utility that allows the use of tweens on this GameNode */
|
||||
tweens: TweenController;
|
||||
/** A tweenable property for rotation. Does not affect the bounding box of this GameNode - Only rendering. */
|
||||
rotation: number;
|
||||
/** The opacity value of this GameNode */
|
||||
abstract set alpha(a: number);
|
||||
|
||||
abstract get alpha(): number;
|
||||
|
||||
// Constructor docs are ignored, as the user should NOT create new GameNodes with a raw constructor
|
||||
constructor(){
|
||||
this._position = new Vec2(0, 0);
|
||||
this._position.setOnChange(() => this.positionChanged());
|
||||
this.receiver = new Receiver();
|
||||
this.emitter = new Emitter();
|
||||
this.tweens = new TweenController(this);
|
||||
this.rotation = 0;
|
||||
}
|
||||
|
||||
destroy(){
|
||||
this.tweens.destroy();
|
||||
this.receiver.destroy();
|
||||
|
||||
if(this.hasPhysics){
|
||||
this.removePhysics();
|
||||
}
|
||||
|
||||
if(this._ai){
|
||||
this._ai.destroy();
|
||||
delete this._ai;
|
||||
this.scene.getAIManager().removeActor(this);
|
||||
}
|
||||
|
||||
this.scene.remove(this);
|
||||
|
||||
this.layer.removeNode(this);
|
||||
}
|
||||
|
||||
/*---------- POSITIONED ----------*/
|
||||
get position(): Vec2 {
|
||||
return this._position;
|
||||
}
|
||||
|
||||
set position(pos: Vec2) {
|
||||
this._position = pos;
|
||||
this._position.setOnChange(() => this.positionChanged());
|
||||
this.positionChanged();
|
||||
}
|
||||
|
||||
get relativePosition(): Vec2 {
|
||||
return this.inRelativeCoordinates(this.position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a point to coordinates relative to the zoom and origin of this node
|
||||
* @param point The point to conver
|
||||
* @returns A new Vec2 representing the point in relative coordinates
|
||||
*/
|
||||
inRelativeCoordinates(point: Vec2): Vec2 {
|
||||
let origin = this.scene.getViewTranslation(this);
|
||||
let zoom = this.scene.getViewScale();
|
||||
return point.clone().sub(origin).scale(zoom);
|
||||
}
|
||||
|
||||
/*---------- UNIQUE ----------*/
|
||||
get id(): number {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
set id(id: number) {
|
||||
// id can only be set once
|
||||
if(this._id === undefined){
|
||||
this._id = id;
|
||||
} else {
|
||||
throw "Attempted to assign id to object that already has id."
|
||||
}
|
||||
}
|
||||
|
||||
/*---------- PHYSICAL ----------*/
|
||||
// @implemented
|
||||
/**
|
||||
* @param velocity The velocity with which to move the object.
|
||||
*/
|
||||
move(velocity: Vec2): void {
|
||||
if(this.frozen) return;
|
||||
this.moving = true;
|
||||
this._velocity = velocity;
|
||||
};
|
||||
|
||||
moveOnPath(speed: number, path: NavigationPath): void {
|
||||
if(this.frozen) return;
|
||||
this.path = path;
|
||||
let dir = path.getMoveDirection(this);
|
||||
this.moving = true;
|
||||
this.pathfinding = true;
|
||||
this._velocity = dir.scale(speed);
|
||||
}
|
||||
|
||||
// @implemented
|
||||
/**
|
||||
* @param velocity The velocity with which the object will move.
|
||||
*/
|
||||
finishMove(): void {
|
||||
this.moving = false;
|
||||
this.position.add(this._velocity);
|
||||
if(this.pathfinding){
|
||||
this.path.handlePathProgress(this);
|
||||
this.path = null;
|
||||
this.pathfinding = false;
|
||||
}
|
||||
}
|
||||
|
||||
// @implemented
|
||||
/**
|
||||
* @param collisionShape The collider for this object. If this has a region (implements Region),
|
||||
* it will be used when no collision shape is specified (or if collision shape is null).
|
||||
* @param isCollidable Whether this is collidable or not. True by default.
|
||||
* @param isStatic Whether this is static or not. False by default
|
||||
*/
|
||||
addPhysics(collisionShape?: Shape, colliderOffset?: Vec2, isCollidable: boolean = true, isStatic: boolean = false): void {
|
||||
// Initialize the physics variables
|
||||
this.hasPhysics = true;
|
||||
this.moving = false;
|
||||
this.onGround = false;
|
||||
this.onWall = false;
|
||||
this.onCeiling = false;
|
||||
this.active = true;
|
||||
this.isCollidable = isCollidable;
|
||||
this.isStatic = isStatic;
|
||||
this.isTrigger = false;
|
||||
this.triggerMask = 0;
|
||||
this.triggerEnters = new Array(32);
|
||||
this.triggerExits = new Array(32);
|
||||
this._velocity = Vec2.ZERO;
|
||||
this.sweptRect = new AABB();
|
||||
this.collidedWithTilemap = false;
|
||||
this.group = -1; // The default group, collides with everything
|
||||
|
||||
// Set the collision shape if provided, or simply use the the region if there is one.
|
||||
if(collisionShape){
|
||||
this.collisionShape = collisionShape;
|
||||
this.collisionShape.center = this.position;
|
||||
} else if (isRegion(this)) {
|
||||
// If the gamenode has a region and no other is specified, use that
|
||||
this.collisionShape = (<any>this).boundary.clone();
|
||||
} else {
|
||||
throw "No collision shape specified for physics object."
|
||||
}
|
||||
|
||||
// If we were provided with a collider offset, set it. Otherwise there is no offset, so use the zero vector
|
||||
if(colliderOffset){
|
||||
this.colliderOffset = colliderOffset;
|
||||
} else {
|
||||
this.colliderOffset = Vec2.ZERO;
|
||||
}
|
||||
|
||||
// Initialize the swept rect
|
||||
this.sweptRect = this.collisionShape.getBoundingRect();
|
||||
|
||||
// Register the object with physics
|
||||
this.scene.getPhysicsManager().registerObject(this);
|
||||
}
|
||||
|
||||
/** Removes this object from the physics system */
|
||||
removePhysics(): void {
|
||||
// Remove this from the physics manager
|
||||
this.scene.getPhysicsManager().deregisterObject(this);
|
||||
|
||||
// Nullify all physics fields
|
||||
this.hasPhysics = false;
|
||||
this.moving = false;
|
||||
this.onGround = false;
|
||||
this.onWall = false;
|
||||
this.onCeiling = false;
|
||||
this.active = false;
|
||||
this.isCollidable = false;
|
||||
this.isStatic = false;
|
||||
this.isTrigger = false;
|
||||
this.triggerMask = 0;
|
||||
this.triggerEnters = null;
|
||||
this.triggerExits = null;
|
||||
this._velocity = Vec2.ZERO;
|
||||
this.sweptRect = null;
|
||||
this.collidedWithTilemap = false;
|
||||
this.group = -1;
|
||||
this.collisionShape = null;
|
||||
this.colliderOffset = Vec2.ZERO;
|
||||
this.sweptRect = null;
|
||||
}
|
||||
|
||||
/** Disables physics movement for this node */
|
||||
freeze(): void {
|
||||
this.frozen = true;
|
||||
}
|
||||
|
||||
/** Reenables physics movement for this node */
|
||||
unfreeze(): void {
|
||||
this.frozen = false;
|
||||
}
|
||||
|
||||
/** Prevents this object from participating in all collisions and triggers. It can still move. */
|
||||
disablePhysics(): void {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
/** Enables this object to participate in collisions and triggers. This is only necessary if disablePhysics was called */
|
||||
enablePhysics(): void {
|
||||
this.active = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the collider for this GameNode
|
||||
* @param collider The new collider to use
|
||||
*/
|
||||
setCollisionShape(collider: Shape): void {
|
||||
this.collisionShape = collider;
|
||||
this.collisionShape.center.copy(this.position);
|
||||
}
|
||||
|
||||
// @implemented
|
||||
/**
|
||||
* Sets this object to be a trigger for a specific group
|
||||
* @param group The name of the group that activates the trigger
|
||||
* @param onEnter The name of the event to send when this trigger is activated
|
||||
* @param onExit The name of the event to send when this trigger stops being activated
|
||||
*/
|
||||
setTrigger(group: string, onEnter: string, onExit: string): void {
|
||||
// Make this object a trigger
|
||||
this.isTrigger = true;
|
||||
|
||||
// Get the number of the physics layer
|
||||
let layerNumber = this.scene.getPhysicsManager().getGroupNumber(group);
|
||||
|
||||
if(layerNumber === 0){
|
||||
console.warn(`Trigger for GameNode ${this.id} not set - group "${group}" was not recognized by the physics manager.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add this to the trigger mask
|
||||
this.triggerMask |= layerNumber;
|
||||
|
||||
// Layer numbers are bits, so get which bit it is
|
||||
let index = Math.log2(layerNumber);
|
||||
|
||||
// Set the event names
|
||||
this.triggerEnters[index] = onEnter;
|
||||
this.triggerExits[index] = onExit;
|
||||
};
|
||||
|
||||
// @implemented
|
||||
/**
|
||||
* @param group The physics group this node should belong to
|
||||
*/
|
||||
setGroup(group: string): void {
|
||||
this.scene.getPhysicsManager().setGroup(this, group);
|
||||
}
|
||||
|
||||
// @implemened
|
||||
getLastVelocity(): Vec2 {
|
||||
return this._velocity;
|
||||
}
|
||||
|
||||
/*---------- ACTOR ----------*/
|
||||
get ai(): AI {
|
||||
return this._ai;
|
||||
}
|
||||
|
||||
set ai(ai: AI) {
|
||||
if(!this._ai){
|
||||
// If we haven't been previously had an ai, register us with the ai manager
|
||||
this.scene.getAIManager().registerActor(this);
|
||||
}
|
||||
|
||||
this._ai = ai;
|
||||
this.aiActive = true;
|
||||
}
|
||||
|
||||
// @implemented
|
||||
addAI<T extends AI>(ai: string | (new () => T), options?: Record<string, any>): void {
|
||||
if(!this._ai){
|
||||
this.scene.getAIManager().registerActor(this);
|
||||
}
|
||||
|
||||
if(typeof ai === "string"){
|
||||
this._ai = this.scene.getAIManager().generateAI(ai);
|
||||
} else {
|
||||
this._ai = new ai();
|
||||
}
|
||||
|
||||
this._ai.initializeAI(this, options);
|
||||
|
||||
this.aiActive = true;
|
||||
}
|
||||
|
||||
// @implemented
|
||||
setAIActive(active: boolean, options: Record<string, any>): void {
|
||||
this.aiActive = active;
|
||||
if(this.aiActive){
|
||||
this.ai.activate(options);
|
||||
}
|
||||
}
|
||||
|
||||
/*---------- TWEENABLE PROPERTIES ----------*/
|
||||
set positionX(value: number) {
|
||||
this.position.x = value;
|
||||
}
|
||||
|
||||
set positionY(value: number) {
|
||||
this.position.y = value;
|
||||
}
|
||||
|
||||
abstract set scaleX(value: number);
|
||||
|
||||
abstract set scaleY(value: number);
|
||||
|
||||
/*---------- GAME NODE ----------*/
|
||||
/**
|
||||
* Sets the scene for this object.
|
||||
* @param scene The scene this object belongs to.
|
||||
*/
|
||||
setScene(scene: Scene): void {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the scene this object is in.
|
||||
* @returns The scene this object belongs to
|
||||
*/
|
||||
getScene(): Scene {
|
||||
return this.scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layer of this object.
|
||||
* @param layer The layer this object will be on.
|
||||
*/
|
||||
setLayer(layer: Layer): void {
|
||||
this.layer = layer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the layer this object is on.
|
||||
* @returns This layer this object is on.
|
||||
*/
|
||||
getLayer(): Layer {
|
||||
return this.layer;
|
||||
}
|
||||
|
||||
/** Called if the position vector is modified or replaced */
|
||||
protected positionChanged(): void {
|
||||
if(this.collisionShape){
|
||||
if(this.colliderOffset){
|
||||
this.collisionShape.center = this.position.clone().add(this.colliderOffset);
|
||||
} else {
|
||||
this.collisionShape.center = this.position.clone();
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates this GameNode
|
||||
* @param deltaT The timestep of the update.
|
||||
*/
|
||||
update(deltaT: number): void {
|
||||
// Defer event handling to AI.
|
||||
while(this.receiver.hasNextEvent()){
|
||||
this._ai.handleEvent(this.receiver.getNextEvent());
|
||||
}
|
||||
}
|
||||
|
||||
// @implemented
|
||||
debugRender(): void {
|
||||
// Draw the position of this GameNode
|
||||
Debug.drawPoint(this.relativePosition, Color.BLUE);
|
||||
|
||||
// If velocity is not zero, draw a vector for it
|
||||
if(this._velocity && !this._velocity.isZero()){
|
||||
Debug.drawRay(this.relativePosition, this._velocity.clone().scaleTo(20).add(this.relativePosition), Color.BLUE);
|
||||
}
|
||||
|
||||
// If this has a collider, draw it
|
||||
if(this.collisionShape){
|
||||
let color = this.isColliding ? Color.RED : Color.GREEN;
|
||||
|
||||
if(this.isTrigger){
|
||||
color = Color.MAGENTA;
|
||||
}
|
||||
|
||||
color.a = 0.2;
|
||||
|
||||
if(this.collisionShape instanceof AABB){
|
||||
Debug.drawBox(this.inRelativeCoordinates(this.collisionShape.center), this.collisionShape.halfSize.scaled(this.scene.getViewScale()), true, color);
|
||||
} else if(this.collisionShape instanceof Circle){
|
||||
Debug.drawCircle(this.inRelativeCoordinates(this.collisionShape.center), this.collisionShape.hw*this.scene.getViewScale(), true, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export enum TweenableProperties{
|
||||
posX = "positionX",
|
||||
posY = "positionY",
|
||||
scaleX = "scaleX",
|
||||
scaleY = "scaleY",
|
||||
rotation = "rotation",
|
||||
alpha = "alpha"
|
||||
}
|
32
hw3/src/Wolfie2D/Nodes/Graphic.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
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 {
|
||||
/** The color of the Graphic */
|
||||
color: Color;
|
||||
|
||||
constructor(){
|
||||
super();
|
||||
this.color = Color.RED;
|
||||
}
|
||||
|
||||
get alpha(): number {
|
||||
return this.color.a;
|
||||
}
|
||||
|
||||
set alpha(a: number) {
|
||||
this.color.a = a;
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
/**
|
||||
* Sets the color of the Graphic. DEPRECATED
|
||||
* @param color The new color of the Graphic.
|
||||
*/
|
||||
setColor(color: Color){
|
||||
this.color = color;
|
||||
}
|
||||
}
|
5
hw3/src/Wolfie2D/Nodes/Graphics/GraphicTypes.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export enum GraphicType {
|
||||
POINT = "POINT",
|
||||
RECT = "RECT",
|
||||
LINE = "LINE",
|
||||
}
|
33
hw3/src/Wolfie2D/Nodes/Graphics/Line.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import Vec2 from "../../DataTypes/Vec2";
|
||||
import Graphic from "../Graphic";
|
||||
|
||||
export default class Line extends Graphic {
|
||||
protected _end: Vec2;
|
||||
thickness: number;
|
||||
|
||||
constructor(start: Vec2, end: Vec2){
|
||||
super();
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.thickness = 2;
|
||||
|
||||
// Does this really have a meaning for lines?
|
||||
this.size.set(5, 5);
|
||||
}
|
||||
|
||||
set start(pos: Vec2){
|
||||
this.position = pos;
|
||||
}
|
||||
|
||||
get start(): Vec2 {
|
||||
return this.position;
|
||||
}
|
||||
|
||||
set end(pos: Vec2){
|
||||
this._end = pos;
|
||||
}
|
||||
|
||||
get end(): Vec2 {
|
||||
return this._end;
|
||||
}
|
||||
}
|
12
hw3/src/Wolfie2D/Nodes/Graphics/Point.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import Graphic from "../Graphic";
|
||||
import Vec2 from "../../DataTypes/Vec2";
|
||||
|
||||
/** A basic point to be drawn on the screen. */
|
||||
export default class Point extends Graphic {
|
||||
|
||||
constructor(position: Vec2){
|
||||
super();
|
||||
this.position = position;
|
||||
this.size.set(5, 5);
|
||||
}
|
||||
}
|
46
hw3/src/Wolfie2D/Nodes/Graphics/Rect.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import Graphic from "../Graphic";
|
||||
import Vec2 from "../../DataTypes/Vec2";
|
||||
import Color from "../../Utils/Color";
|
||||
|
||||
/** A basic rectangle to be drawn on the screen. */
|
||||
export default class Rect extends Graphic {
|
||||
|
||||
/** The border color of the Rect */
|
||||
borderColor: Color;
|
||||
|
||||
/** The width of the border */
|
||||
borderWidth: number;
|
||||
|
||||
constructor(position: Vec2, size: Vec2){
|
||||
super();
|
||||
this.position = position;
|
||||
this.size = size;
|
||||
this.borderColor = Color.TRANSPARENT;
|
||||
this.borderWidth = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the border color of this rectangle
|
||||
* @param color The border color
|
||||
*/
|
||||
setBorderColor(color: Color): void {
|
||||
this.borderColor = color;
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
getBorderColor(): Color {
|
||||
return this.borderColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the border width of this rectangle
|
||||
* @param width The width of the rectangle in pixels
|
||||
*/
|
||||
setBorderWidth(width: number){
|
||||
this.borderWidth = width;
|
||||
}
|
||||
|
||||
getBorderWidth(): number {
|
||||
return this.borderWidth;
|
||||
}
|
||||
}
|
49
hw3/src/Wolfie2D/Nodes/Sprites/AnimatedSprite.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import Sprite from "./Sprite";
|
||||
import AnimationManager from "../../Rendering/Animations/AnimationManager";
|
||||
import Spritesheet from "../../DataTypes/Spritesheet";
|
||||
import Vec2 from "../../DataTypes/Vec2";
|
||||
|
||||
/** An sprite with specified animation frames. */
|
||||
export default class AnimatedSprite extends Sprite {
|
||||
/** The number of columns in this sprite sheet */
|
||||
protected numCols: number;
|
||||
|
||||
get cols(): number {
|
||||
return this.numCols;
|
||||
}
|
||||
|
||||
/** The number of rows in this sprite sheet */
|
||||
protected numRows: number;
|
||||
|
||||
get rows(): number {
|
||||
return this.numRows;
|
||||
}
|
||||
|
||||
/** The animationManager for this sprite */
|
||||
animation: AnimationManager;
|
||||
|
||||
constructor(spritesheet: Spritesheet){
|
||||
super(spritesheet.name);
|
||||
this.numCols = spritesheet.columns;
|
||||
this.numRows = spritesheet.rows;
|
||||
|
||||
// Set the size of the sprite to the sprite size specified by the spritesheet
|
||||
this.size.set(spritesheet.spriteWidth, spritesheet.spriteHeight);
|
||||
|
||||
this.animation = new AnimationManager(this);
|
||||
|
||||
// Add the animations to the animated sprite
|
||||
for(let animation of spritesheet.animations){
|
||||
this.animation.add(animation.name, animation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the image offset for the current index of animation
|
||||
* @param index The index we're at in the animation
|
||||
* @returns A Vec2 containing the image offset
|
||||
*/
|
||||
getAnimationOffset(index: number): Vec2 {
|
||||
return new Vec2((index % this.numCols) * this.size.x, Math.floor(index / this.numCols) * this.size.y);
|
||||
}
|
||||
}
|
35
hw3/src/Wolfie2D/Nodes/Sprites/Sprite.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
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 {
|
||||
/** The id of the image from the resourceManager */
|
||||
imageId: string;
|
||||
/** The offset of the sprite in an atlas image */
|
||||
imageOffset: Vec2;
|
||||
/** Whether or not the x-axis should be inverted on render */
|
||||
invertX: boolean;
|
||||
/** Whether or not the y-axis should be inverted on render */
|
||||
invertY: boolean;
|
||||
|
||||
constructor(imageId: string){
|
||||
super();
|
||||
this.imageId = imageId;
|
||||
let image = ResourceManager.getInstance().getImage(this.imageId);
|
||||
this.size = new Vec2(image.width, image.height);
|
||||
this.imageOffset = Vec2.ZERO;
|
||||
this.invertX = false;
|
||||
this.invertY = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the offset of the sprite from (0, 0) in the image's coordinates
|
||||
* @param offset The offset of the sprite from (0, 0) in image coordinates
|
||||
*/
|
||||
setImageOffset(offset: Vec2): void {
|
||||
this.imageOffset = offset;
|
||||
}
|
||||
}
|
119
hw3/src/Wolfie2D/Nodes/Tilemap.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import Vec2 from "../DataTypes/Vec2";
|
||||
import Tileset from "../DataTypes/Tilesets/Tileset";
|
||||
import { TiledTilemapData, TiledLayerData } from "../DataTypes/Tilesets/TiledData"
|
||||
import CanvasNode from "./CanvasNode";
|
||||
import PhysicsManager from "../Physics/PhysicsManager";
|
||||
|
||||
/**
|
||||
* The representation of a tilemap - this can consist of a combination of tilesets in one layer
|
||||
*/
|
||||
export default abstract class Tilemap extends CanvasNode {
|
||||
/** An array of the tilesets that this tilemap uses */
|
||||
protected tilesets: Array<Tileset>;
|
||||
|
||||
/** The size of a tile in this tilemap */
|
||||
protected tileSize: Vec2;
|
||||
|
||||
/** An array of tile data */
|
||||
protected data: Array<number>;
|
||||
|
||||
/** An array of tile collision data */
|
||||
protected collisionMap: Array<boolean>;
|
||||
|
||||
/** The name of the tilemap */
|
||||
name: string;
|
||||
|
||||
// TODO: Make this no longer be specific to Tiled
|
||||
constructor(tilemapData: TiledTilemapData, layer: TiledLayerData, tilesets: Array<Tileset>, scale: Vec2) {
|
||||
super();
|
||||
this.tilesets = tilesets;
|
||||
this.tileSize = new Vec2(0, 0);
|
||||
this.name = layer.name;
|
||||
|
||||
let tilecount = 0;
|
||||
for(let tileset of tilesets){
|
||||
tilecount += tileset.getTileCount() + 1;
|
||||
}
|
||||
|
||||
this.collisionMap = new Array(tilecount);
|
||||
for(let i = 0; i < this.collisionMap.length; i++){
|
||||
this.collisionMap[i] = false;
|
||||
}
|
||||
|
||||
// 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.set(scale.x, scale.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of the tilesets associated with this tilemap
|
||||
* @returns An array of all of the tilesets assocaited with this tilemap.
|
||||
*/
|
||||
getTilesets(): Tileset[] {
|
||||
return this.tilesets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of tiles in this tilemap as they appear in the game world after scaling
|
||||
* @returns A vector containing the size of tiles in this tilemap as they appear in the game world after scaling.
|
||||
*/
|
||||
getTileSize(): Vec2 {
|
||||
return this.tileSize.scaled(this.scale.x, this.scale.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tile size taking zoom into account
|
||||
* @returns The tile size with zoom
|
||||
*/
|
||||
getTileSizeWithZoom(): Vec2 {
|
||||
let zoom = this.scene.getViewScale();
|
||||
|
||||
return this.getTileSize().scale(zoom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds this tilemap to the physics system
|
||||
*/
|
||||
addPhysics(): void {
|
||||
this.hasPhysics = true;
|
||||
this.active = true;
|
||||
this.group = -1;
|
||||
this.scene.getPhysicsManager().registerTilemap(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the tile at the specified position
|
||||
* @param worldCoords The position in world coordinates
|
||||
* @returns A number that represents the data value of the tile at the specified world position.
|
||||
*/
|
||||
abstract getTileAtWorldPosition(worldCoords: Vec2): number;
|
||||
|
||||
/**
|
||||
* Returns the world position of the top left corner of the tile at the specified index
|
||||
* @param index The index of the tile in the tileData array
|
||||
* @returns The world position of the tile at the specified index
|
||||
*/
|
||||
abstract getTileWorldPosition(index: number): Vec2;
|
||||
|
||||
/**
|
||||
* Returns the value of the tile at the specified index
|
||||
* @param index The index of the tile in the tileData array
|
||||
* @returns The value of the tile in the tileData array
|
||||
*/
|
||||
abstract getTile(index: number): number;
|
||||
|
||||
/**
|
||||
* Sets the tile at the specified index
|
||||
* @param index The index of the tile
|
||||
* @param type The new data value of the tile
|
||||
*/
|
||||
abstract setTile(index: number, type: number): void;
|
||||
|
||||
// TODO: This shouldn't use tiled data specifically - it should be more general
|
||||
/**
|
||||
* Sets up the tileset using the data loaded from file
|
||||
* @param tilemapData The tilemap data from file
|
||||
* @param layer The layer data from file
|
||||
*/
|
||||
protected abstract parseTilemapData(tilemapData: TiledTilemapData, layer: TiledLayerData): void;
|
||||
}
|
190
hw3/src/Wolfie2D/Nodes/Tilemaps/OrthogonalTilemap.ts
Normal file
|
@ -0,0 +1,190 @@
|
|||
import Tilemap from "../Tilemap";
|
||||
import Vec2 from "../../DataTypes/Vec2";
|
||||
import { TiledTilemapData, TiledLayerData } from "../../DataTypes/Tilesets/TiledData";
|
||||
import Debug from "../../Debug/Debug";
|
||||
import Color from "../../Utils/Color";
|
||||
|
||||
/**
|
||||
* The representation of an orthogonal tilemap - i.e. a top down or platformer tilemap
|
||||
*/
|
||||
export default class OrthogonalTilemap extends Tilemap {
|
||||
/** The number of columns in the tilemap */
|
||||
protected numCols: number;
|
||||
/** The number of rows in the tilemap */
|
||||
protected numRows: number;
|
||||
|
||||
// @override
|
||||
protected parseTilemapData(tilemapData: TiledTilemapData, layer: TiledLayerData): void {
|
||||
// The size of the tilemap in local space
|
||||
this.numCols = tilemapData.width;
|
||||
this.numRows = tilemapData.height;
|
||||
|
||||
// The size of tiles
|
||||
this.tileSize.set(tilemapData.tilewidth, tilemapData.tileheight);
|
||||
|
||||
// The size of the tilemap on the canvas
|
||||
this.size.set(this.numCols * this.tileSize.x, this.numRows * this.tileSize.y);
|
||||
this.position.copy(this.size.scaled(0.5));
|
||||
this.data = layer.data;
|
||||
this.visible = layer.visible;
|
||||
|
||||
// Whether the tilemap is collidable or not
|
||||
this.isCollidable = false;
|
||||
if(layer.properties){
|
||||
for(let item of layer.properties){
|
||||
if(item.name === "Collidable"){
|
||||
this.isCollidable = item.value;
|
||||
|
||||
// Set all tiles besides "empty: 0" to be collidable
|
||||
for(let i = 1; i < this.collisionMap.length; i++){
|
||||
this.collisionMap[i] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dimensions of the tilemap
|
||||
* @returns A Vec2 containing the number of columns and the number of rows in the tilemap.
|
||||
*/
|
||||
getDimensions(): Vec2 {
|
||||
return new Vec2(this.numCols, this.numRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data value of the tile at the specified world position
|
||||
* @param worldCoords The coordinates in world space
|
||||
* @returns The data value of the tile
|
||||
*/
|
||||
getTileAtWorldPosition(worldCoords: Vec2): number {
|
||||
let localCoords = this.getColRowAt(worldCoords);
|
||||
return this.getTileAtRowCol(localCoords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tile at the specified row and column
|
||||
* @param rowCol The coordinates in tilemap space
|
||||
* @returns The data value of the tile
|
||||
*/
|
||||
getTileAtRowCol(rowCol: Vec2): number {
|
||||
if(rowCol.x < 0 || rowCol.x >= this.numCols || rowCol.y < 0 || rowCol.y >= this.numRows){
|
||||
return -1;
|
||||
}
|
||||
|
||||
return this.data[rowCol.y * this.numCols + rowCol.x];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the world position of the tile at the specified index
|
||||
* @param index The index of the tile
|
||||
* @returns A Vec2 containing the world position of the tile
|
||||
*/
|
||||
getTileWorldPosition(index: number): Vec2 {
|
||||
// Get the local position
|
||||
let col = index % this.numCols;
|
||||
let row = Math.floor(index / this.numCols);
|
||||
|
||||
// Get the world position
|
||||
let x = col * this.tileSize.x;
|
||||
let y = row * this.tileSize.y;
|
||||
|
||||
return new Vec2(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the data value of the tile at the specified index
|
||||
* @param index The index of the tile
|
||||
* @returns The data value of the tile
|
||||
*/
|
||||
getTile(index: number): number {
|
||||
return this.data[index];
|
||||
}
|
||||
|
||||
// @override
|
||||
setTile(index: number, type: number): void {
|
||||
this.data[index] = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tile at the specified row and column
|
||||
* @param rowCol The position of the tile in tilemap space
|
||||
* @param type The new data value of the tile
|
||||
*/
|
||||
setTileAtRowCol(rowCol: Vec2, type: number): void {
|
||||
let index = rowCol.y * this.numCols + rowCol.x;
|
||||
this.setTile(index, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the tile at the specified row and column of the tilemap is collidable
|
||||
* @param indexOrCol The index of the tile or the column it is in
|
||||
* @param row The row the tile is in
|
||||
* @returns A flag representing whether or not the tile is collidable.
|
||||
*/
|
||||
isTileCollidable(indexOrCol: number, row?: number): boolean {
|
||||
// The value of the tile
|
||||
let tile = 0;
|
||||
|
||||
if(row){
|
||||
// We have a column and a row
|
||||
tile = this.getTileAtRowCol(new Vec2(indexOrCol, row));
|
||||
|
||||
if(tile < 0){
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if(indexOrCol < 0 || indexOrCol >= this.data.length){
|
||||
// Tiles that don't exist aren't collidable
|
||||
return false;
|
||||
}
|
||||
// We have an index
|
||||
tile = this.getTile(indexOrCol);
|
||||
}
|
||||
|
||||
return this.collisionMap[tile];
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in world coordinates and returns the row and column of the tile at that position
|
||||
* @param worldCoords The coordinates of the potential tile in world space
|
||||
* @returns A Vec2 containing the coordinates of the potential tile in tilemap space
|
||||
*/
|
||||
getColRowAt(worldCoords: Vec2): Vec2 {
|
||||
let col = Math.floor(worldCoords.x / this.tileSize.x / this.scale.x);
|
||||
let row = Math.floor(worldCoords.y / this.tileSize.y / this.scale.y);
|
||||
|
||||
return new Vec2(col, row);
|
||||
}
|
||||
|
||||
// @override
|
||||
update(deltaT: number): void {}
|
||||
|
||||
// @override
|
||||
debugRender(){
|
||||
// Half of the tile size
|
||||
let zoomedHalfTileSize = this.getTileSizeWithZoom().scaled(0.5);
|
||||
let halfTileSize = this.getTileSize().scaled(0.5);
|
||||
|
||||
// The center of the top left tile
|
||||
let topLeft = this.position.clone().sub(this.size.scaled(0.5));
|
||||
|
||||
// A vec to store the center
|
||||
let center = Vec2.ZERO;
|
||||
|
||||
for(let col = 0; col < this.numCols; col++){
|
||||
// Calculate the x-position
|
||||
center.x = topLeft.x + col*2*halfTileSize.x + halfTileSize.x;
|
||||
|
||||
for(let row = 0; row < this.numRows; row++){
|
||||
if(this.isCollidable && this.isTileCollidable(col, row)){
|
||||
// Calculate the y-position
|
||||
center.y = topLeft.y + row*2*halfTileSize.y + halfTileSize.y;
|
||||
|
||||
// Draw a box for this tile
|
||||
Debug.drawBox(this.inRelativeCoordinates(center), zoomedHalfTileSize, false, Color.BLUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
149
hw3/src/Wolfie2D/Nodes/UIElement.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
import CanvasNode from "./CanvasNode";
|
||||
import Color from "../Utils/Color";
|
||||
import Vec2 from "../DataTypes/Vec2";
|
||||
import Input from "../Input/Input";
|
||||
|
||||
/**
|
||||
* The representation of a UIElement - the parent class of things like buttons
|
||||
*/
|
||||
export default abstract class UIElement extends CanvasNode {
|
||||
// Style attributes - TODO - abstract this into a style object/interface
|
||||
/** The backgound color */
|
||||
backgroundColor: Color;
|
||||
/** The border color */
|
||||
borderColor: Color;
|
||||
/** The border radius */
|
||||
borderRadius: number;
|
||||
/** The border width */
|
||||
borderWidth: number;
|
||||
/** The padding */
|
||||
padding: Vec2;
|
||||
|
||||
// EventAttributes
|
||||
/** The reaction of this UIElement on a click */
|
||||
onClick: Function;
|
||||
/** The event propagated on click */
|
||||
onClickEventId: string;
|
||||
/** The reaction to the release of a click */
|
||||
onRelease: Function;
|
||||
/** The event propagated on the release of a click */
|
||||
onReleaseEventId: string;
|
||||
/** The reaction when a mouse enters this UIElement */
|
||||
onEnter: Function;
|
||||
/** The event propagated when a mouse enters this UIElement */
|
||||
onEnterEventId: string;
|
||||
/** The reaction when a mouse leaves this UIElement */
|
||||
onLeave: Function;
|
||||
/** The event propogated when a mouse leaves this UIElement */
|
||||
onLeaveEventId: string;
|
||||
|
||||
/** Whether or not this UIElement is currently clicked on */
|
||||
protected isClicked: boolean;
|
||||
/** Whether or not this UIElement is currently hovered over */
|
||||
protected isEntered: boolean;
|
||||
|
||||
constructor(position: Vec2){
|
||||
super();
|
||||
this.position = position;
|
||||
|
||||
this.backgroundColor = new Color(0, 0, 0, 0);
|
||||
this.borderColor = new Color(0, 0, 0, 0);
|
||||
this.borderRadius = 5;
|
||||
this.borderWidth = 1;
|
||||
this.padding = Vec2.ZERO;
|
||||
|
||||
this.onClick = null;
|
||||
this.onClickEventId = null;
|
||||
this.onRelease = null;
|
||||
this.onReleaseEventId = null;
|
||||
|
||||
this.onEnter = null;
|
||||
this.onEnterEventId = null;
|
||||
this.onLeave = null;
|
||||
this.onLeaveEventId = null;
|
||||
|
||||
this.isClicked = false;
|
||||
this.isEntered = false;
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
setBackgroundColor(color: Color): void {
|
||||
this.backgroundColor = color;
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
setPadding(padding: Vec2): void {
|
||||
this.padding.copy(padding);
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
super.update(deltaT);
|
||||
|
||||
// See of this object was just clicked
|
||||
if(Input.isMouseJustPressed()){
|
||||
let clickPos = Input.getMousePressPosition();
|
||||
if(this.contains(clickPos.x, clickPos.y) && this.visible && !this.layer.isHidden()){
|
||||
this.isClicked = true;
|
||||
|
||||
if(this.onClick !== null){
|
||||
this.onClick();
|
||||
}
|
||||
if(this.onClickEventId !== null){
|
||||
let data = {};
|
||||
this.emitter.fireEvent(this.onClickEventId, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the mouse wasn't just pressed, then we definitely weren't clicked
|
||||
if(!Input.isMousePressed()){
|
||||
if(this.isClicked){
|
||||
this.isClicked = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the mouse is hovering over this element
|
||||
let mousePos = Input.getMousePosition();
|
||||
if(mousePos && this.contains(mousePos.x, mousePos.y)){
|
||||
this.isEntered = true;
|
||||
|
||||
if(this.onEnter !== null){
|
||||
this.onEnter();
|
||||
}
|
||||
if(this.onEnterEventId !== null){
|
||||
let data = {};
|
||||
this.emitter.fireEvent(this.onEnterEventId, data);
|
||||
}
|
||||
|
||||
} else if(this.isEntered) {
|
||||
this.isEntered = false;
|
||||
|
||||
if(this.onLeave !== null){
|
||||
this.onLeave();
|
||||
}
|
||||
if(this.onLeaveEventId !== null){
|
||||
let data = {};
|
||||
this.emitter.fireEvent(this.onLeaveEventId, data);
|
||||
}
|
||||
} else if(this.isClicked) {
|
||||
// If mouse is dragged off of element while down, it is not clicked anymore
|
||||
this.isClicked = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridable method for calculating background color - useful for elements that want to be colored on different after certain events
|
||||
* @returns The background color of the UIElement
|
||||
*/
|
||||
calculateBackgroundColor(): Color {
|
||||
return this.backgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridable method for calculating border color - useful for elements that want to be colored on different after certain events
|
||||
* @returns The border color of the UIElement
|
||||
*/
|
||||
calculateBorderColor(): Color {
|
||||
return this.borderColor;
|
||||
}
|
||||
}
|
27
hw3/src/Wolfie2D/Nodes/UIElements/Button.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import Label from "./Label";
|
||||
import Color from "../../Utils/Color";
|
||||
import Vec2 from "../../DataTypes/Vec2";
|
||||
|
||||
/** A clickable button UIElement */
|
||||
export default class Button extends Label {
|
||||
|
||||
constructor(position: Vec2, text: string){
|
||||
super(position, text);
|
||||
|
||||
this.backgroundColor = new Color(150, 75, 203);
|
||||
this.borderColor = new Color(41, 46, 30);
|
||||
this.textColor = new Color(255, 255, 255);
|
||||
}
|
||||
|
||||
// @override
|
||||
calculateBackgroundColor(): Color {
|
||||
// Change the background color if clicked or hovered
|
||||
if(this.isEntered && !this.isClicked){
|
||||
return this.backgroundColor.lighten();
|
||||
} else if(this.isClicked){
|
||||
return this.backgroundColor.darken();
|
||||
} else {
|
||||
return this.backgroundColor;
|
||||
}
|
||||
}
|
||||
}
|
152
hw3/src/Wolfie2D/Nodes/UIElements/Label.ts
Normal file
|
@ -0,0 +1,152 @@
|
|||
import Vec2 from "../../DataTypes/Vec2";
|
||||
import Color from "../../Utils/Color";
|
||||
import UIElement from "../UIElement";
|
||||
|
||||
/** A basic text-containing label */
|
||||
export default class Label extends UIElement{
|
||||
/** The color of the text of this UIElement */
|
||||
textColor: Color;
|
||||
/** The value of the text of this UIElement */
|
||||
text: string;
|
||||
/** The name of the font */
|
||||
font: string;
|
||||
/** The size of the font */
|
||||
fontSize: number;
|
||||
/** The horizontal alignment of the text within the label */
|
||||
protected hAlign: string;
|
||||
/** The vertical alignment of text within the label */
|
||||
protected vAlign: string;
|
||||
|
||||
/** A flag for if the width of the text has been measured on the canvas for auto width assignment */
|
||||
protected sizeAssigned: boolean;
|
||||
|
||||
constructor(position: Vec2, text: string){
|
||||
super(position);
|
||||
this.text = text;
|
||||
this.textColor = new Color(0, 0, 0, 1);
|
||||
this.font = "Arial";
|
||||
this.fontSize = 30;
|
||||
this.hAlign = "center";
|
||||
this.vAlign = "center";
|
||||
|
||||
this.sizeAssigned = false;
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
setText(text: string): void {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
setTextColor(color: Color): void {
|
||||
this.textColor = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string containg the font details for rendering
|
||||
* @returns A string containing the font details
|
||||
*/
|
||||
getFontString(): string {
|
||||
return this.fontSize + "px " + this.font;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridable method for calculating text color - useful for elements that want to be colored on different after certain events
|
||||
* @returns a string containg the text color
|
||||
*/
|
||||
calculateTextColor(): string {
|
||||
return this.textColor.toStringRGBA();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the canvas to calculate the width of the text
|
||||
* @param ctx The rendering context
|
||||
* @returns A number representing the rendered text width
|
||||
*/
|
||||
protected calculateTextWidth(ctx: CanvasRenderingContext2D): number {
|
||||
ctx.font = this.fontSize + "px " + this.font;
|
||||
return ctx.measureText(this.text).width;
|
||||
}
|
||||
|
||||
setHAlign(align: string): void {
|
||||
this.hAlign = align;
|
||||
}
|
||||
|
||||
setVAlign(align: string): void {
|
||||
this.vAlign = align;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the offset of the text - this is used for rendering text with different alignments
|
||||
* @param ctx The rendering context
|
||||
* @returns The offset of the text in a Vec2
|
||||
*/
|
||||
calculateTextOffset(ctx: CanvasRenderingContext2D): Vec2 {
|
||||
let textWidth = this.calculateTextWidth(ctx);
|
||||
|
||||
let offset = new Vec2(0, 0);
|
||||
|
||||
let hDiff = this.size.x - textWidth;
|
||||
if(this.hAlign === HAlign.CENTER){
|
||||
offset.x = hDiff/2;
|
||||
} else if (this.hAlign === HAlign.RIGHT){
|
||||
offset.x = hDiff;
|
||||
}
|
||||
|
||||
if(this.vAlign === VAlign.TOP){
|
||||
ctx.textBaseline = "top";
|
||||
offset.y = 0;
|
||||
} else if (this.vAlign === VAlign.BOTTOM){
|
||||
ctx.textBaseline = "bottom";
|
||||
offset.y = this.size.y;
|
||||
} else {
|
||||
ctx.textBaseline = "middle";
|
||||
offset.y = this.size.y/2;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
protected sizeChanged(): void {
|
||||
super.sizeChanged();
|
||||
this.sizeAssigned = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically sizes the element to the text within it
|
||||
* @param ctx The rendering context
|
||||
*/
|
||||
protected autoSize(ctx: CanvasRenderingContext2D): void {
|
||||
let width = this.calculateTextWidth(ctx);
|
||||
let height = this.fontSize;
|
||||
this.size.set(width + this.padding.x*2, height + this.padding.y*2);
|
||||
this.sizeAssigned = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initially assigns a size to the UIElement if none is provided
|
||||
* @param ctx The rendering context
|
||||
*/
|
||||
handleInitialSizing(ctx: CanvasRenderingContext2D): void {
|
||||
if(!this.sizeAssigned){
|
||||
this.autoSize(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/** On the next render, size this element to it's current text using its current font size */
|
||||
sizeToText(): void {
|
||||
this.sizeAssigned = false;
|
||||
}
|
||||
}
|
||||
|
||||
export enum VAlign {
|
||||
TOP = "top",
|
||||
CENTER = "center",
|
||||
BOTTOM = "bottom"
|
||||
}
|
||||
|
||||
export enum HAlign {
|
||||
LEFT = "left",
|
||||
CENTER = "center",
|
||||
RIGHT = "right"
|
||||
}
|
64
hw3/src/Wolfie2D/Nodes/UIElements/Slider.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import Vec2 from "../../DataTypes/Vec2";
|
||||
import Input from "../../Input/Input";
|
||||
import Color from "../../Utils/Color";
|
||||
import MathUtils from "../../Utils/MathUtils";
|
||||
import UIElement from "../UIElement";
|
||||
|
||||
/** A slider UIElement */
|
||||
export default class Slider extends UIElement {
|
||||
/** The value of the slider from [0, 1] */
|
||||
protected value: number;
|
||||
/** The color of the slider nib */
|
||||
public nibColor: Color;
|
||||
/** The size of the nib */
|
||||
public nibSize: Vec2;
|
||||
/** The color of the slider track */
|
||||
public sliderColor: Color;
|
||||
/** The reaction of this UIElement to a value change */
|
||||
public onValueChange: Function;
|
||||
/** The event propagated by this UIElement when value changes */
|
||||
public onValueChangeEventId: string;
|
||||
|
||||
constructor(position: Vec2, initValue: number){
|
||||
super(position);
|
||||
|
||||
this.value = initValue;
|
||||
this.nibColor = Color.RED;
|
||||
this.sliderColor = Color.BLACK;
|
||||
this.backgroundColor = Color.TRANSPARENT;
|
||||
this.borderColor = Color.TRANSPARENT;
|
||||
this.nibSize = new Vec2(10, 20);
|
||||
|
||||
// Set a default size
|
||||
this.size.set(200, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value of the slider
|
||||
* @returns The value of the slider
|
||||
*/
|
||||
getValue(): number {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/** A method called in response to the value changing */
|
||||
protected valueChanged(): void {
|
||||
if(this.onValueChange){
|
||||
this.onValueChange(this.value);
|
||||
}
|
||||
|
||||
if(this.onValueChangeEventId){
|
||||
this.emitter.fireEvent(this.onValueChangeEventId, {target: this, value: this.value});
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
super.update(deltaT);
|
||||
|
||||
if(this.isClicked){
|
||||
let val = MathUtils.invLerp(this.position.x - this.size.x/2, this.position.x + this.size.x/2, Input.getMousePosition().x);
|
||||
this.value = MathUtils.clamp01(val);
|
||||
this.valueChanged();
|
||||
}
|
||||
}
|
||||
}
|
64
hw3/src/Wolfie2D/Nodes/UIElements/TextInput.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import Vec2 from "../../DataTypes/Vec2";
|
||||
import Color from "../../Utils/Color";
|
||||
import Label from "./Label";
|
||||
import Input from "../../Input/Input";
|
||||
|
||||
/** A text input UIElement */
|
||||
export default class TextInput extends Label {
|
||||
/** A flag the represents whether the user can type in this TextInput */
|
||||
focused: boolean;
|
||||
/** The position of the cursor in this TextInput */
|
||||
cursorCounter: number;
|
||||
|
||||
constructor(position: Vec2){
|
||||
super(position, "");
|
||||
|
||||
this.focused = false;
|
||||
this.cursorCounter = 0;
|
||||
|
||||
// Give a default size to the x only
|
||||
this.size.set(200, this.fontSize);
|
||||
this.hAlign = "left";
|
||||
|
||||
this.borderColor = Color.BLACK;
|
||||
this.backgroundColor = Color.WHITE;
|
||||
}
|
||||
|
||||
update(deltaT: number): void {
|
||||
super.update(deltaT);
|
||||
|
||||
if(Input.isMouseJustPressed()){
|
||||
let clickPos = Input.getMousePressPosition();
|
||||
if(this.contains(clickPos.x, clickPos.y)){
|
||||
this.focused = true;
|
||||
this.cursorCounter = 30;
|
||||
} else {
|
||||
this.focused = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(this.focused){
|
||||
let keys = Input.getKeysJustPressed();
|
||||
let nums = "1234567890";
|
||||
let specialChars = "`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?";
|
||||
let letters = "qwertyuiopasdfghjklzxcvbnm";
|
||||
let mask = nums + specialChars + letters;
|
||||
keys = keys.filter(key => mask.includes(key));
|
||||
let shiftPressed = Input.isKeyPressed("shift");
|
||||
let backspacePressed = Input.isKeyJustPressed("backspace");
|
||||
let spacePressed = Input.isKeyJustPressed("space");
|
||||
|
||||
if(backspacePressed){
|
||||
this.text = this.text.substring(0, this.text.length - 1);
|
||||
} else if(spacePressed){
|
||||
this.text += " ";
|
||||
} else if(keys.length > 0) {
|
||||
if(shiftPressed){
|
||||
this.text += keys[0].toUpperCase();
|
||||
} else {
|
||||
this.text += keys[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
6
hw3/src/Wolfie2D/Nodes/UIElements/UIElementTypes.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export enum UIElementType {
|
||||
BUTTON = "BUTTON",
|
||||
LABEL = "LABEL",
|
||||
SLIDER = "SLIDER",
|
||||
TEXT_INPUT = "TEXTINPUT"
|
||||
}
|