Documentatie¶
Tiles¶
Er zijn 4 types van normale tegels en 2 van speciale tegels en elke type heeft bijbehorende plaatje. De normale tegels zijn juwelen die je moet drie of meer in de rij zetten om ze laten te verwijderen. Terwijl de speciale tegels zijn in de vorm van hout of steen. Je kunt ze niet laten bewegen, maar ze hebben wel duurzaamheid die daalt als een tegel ernaast is vernietigd. Als die duurzaamheid 0 bereikt, wordt de speciale tegel ook vernietigd. (Er moest ook origineel een speciale tegel zijn die als je die aanraakt alle tegels op dezelfde rij of kolom verwijdert, maar die kond ik niet realiseren)
class Tile {
//Dit zijn de coördinaten van een tile
#x;
#y;
//Dit zijn de coördinaten van de tilegrid waarin de tegel geplaatst wordt
#gridx;
#gridy;
//Dit is de grootte van de tegel
#size;
//De tegels hebben een bepaalde image afhankelijk van de type
_image;
#visible;
_type;
#shift;
//Sommige tegels hebben durability (duurzaamheid) die daalt als de tegel ernaast is vernietigd
#durability
//Hier door gebruik van get en set worden de parameters van dit class gelezen en veranderd
get x() {
return this.#x;
}
set x(value) {
this.#x = value;
}
get y() {
return this.#y;
}
set y(value) {
this.#y = value;
}
get type() {
return this._type;
}
set type(value) {
this._type = value;
}
get image() {
return this._image;
}
set image(value) {
this._image = value;
}
get shift() {
return this.#shift;
}
set shift(value) {
this.#shift = value;
}
get position() {
let pos = createVector(this.#x, this.#y).mult(this.#size);
return pos.add(this.#gridx, this.#gridy);
}
get visible() {
return this.#visible;
}
set visible(value) {
this.#visible = value;
}
get durability() {
return this.#durability;
}
set durability(value) {
this.#durability = value;
}
//Hier worden de tegels getekend
draw(time) {
let coord = this.getTileCoordinate(0, time * this.#shift);
if (this.#visible && this._type > 0) {
fill(128, 0, 0);
stroke(80, 0, 0);
strokeWeight(3);
rect(coord.x, coord.y, this.#size, this.#size);
image(this._image, coord.x, coord.y, this.#size, this.#size);
}
}
//De functie die coördinaten berekent van waar de tegel meot getekend worden.
getTileCoordinate(shiftx, shifty) {
let tilex = this.position.x + shiftx * this.#size;
let tiley = this.position.y + shifty * this.#size;
return { x: tilex, y: tiley }
}
constructor(type, size, x, y, gridx, gridy) {
this._type = type;
this.#size = size;
this.#x = x;
this.#y = y;
this.#gridx = gridx;
this.#gridy = gridy;
this.#shift = 0;
this.#visible = true;
if(type == 6){
this.#durability = 3;
}
else if (type == 8){
this.#durability = 5;
}
/*Normale tegels hebben oneindige duurzaamheid, waardor ze kunnien niet vernietigd worden
tenzij ze in de cluster zijn.*/
else{
this.#durability = Infinity;
}
}
}
Grid¶
In de TileGrid class worden meeste functies uitgevoerd. Daar worden de tegels gemaakt en gezet, moves worden opgeslagen, en in geval er blijven geen meer moves over wordt de tile grid opnieuw gemaakt.
class TileGrid {
//Hier zijn de coördinaten van de tileGrid
#x;
#y;
//Hier is de parameter voor de grootte van de tegels
#tileSize;
#tiles;
//Hier zijn de breedte en de hoogte van de canvas.
#width;
#height;
//Hier is de nummer van de huidige level
#level;
//Hier zijn de arrays met types die gebruikt worden voor een specefieke level
#typelist = [1, 2, 3, 4];
#typelist2 = [1, 2, 3, 4, 6, 6];
#typelist3 = [1, 2, 3, 4, 8, 8];
#typelist4 = [1, 2, 3, 4, 6, 8];
//Het staat de coördinaten voor de tile die nu is geselecteerd
selectedTile = {
selected: false,
column: 0,
row: 0
}
//In deze lijst worden later clusters gezet
#clusters = [];
//In deze lijst staan alle beschikbare moves
#moves = [];
//Hier staan de coördinaten voor de huidige twee tegels die geswapt worden
#currentmove = {
x1: 0,
y1: 0,
x2: 0,
y2: 0
}
constructor(x, y, width, height, tileSize, level) {
this.#x = x;
this.#y = y;
this.#tileSize = tileSize;
this.#width = width;
this.#height = height;
this.#level = level;
this.#generateTileGrid();
}
#generateTileGrid() {
let done = false;
//tiles is een 2D array, dat betekent dat het is een array met andere arrays daarbinnen.
this.#tiles = new Array();
//Hier wordt de tile grid gegenereerd and de tegels worden in de 2D #tiles array geplaatst.
while (done == false) {
for (let x = 0; x < this.#width; x++) {
for (let y = 0; y < this.#height; y++) {
if (!this.#tiles[x]) {
this.#tiles[x] = new Array();
}
if (this.#level == 1) {
this.#tiles[x][y] = new NormalTile(random(this.#typelist), this.#tileSize, x, y, this.#x, this.#y);
}
else if (this.#level == 2) {
this.#tiles[x][y] = new NormalTile(random(this.#typelist2), this.#tileSize, x, y, this.#x, this.#y);
}
else if (this.#level == 3) {
this.#tiles[x][y] = new NormalTile(random(this.#typelist3), this.#tileSize, x, y, this.#x, this.#y);
}
else if (this.#level == 4) {
this.#tiles[x][y] = new NormalTile(random(this.#typelist4), this.#tileSize, x, y, this.#x, this.#y);
}
}
}
this.#resolveClusters();
this.#findMoves();
if (this.#moves.length > 0) {
done = true;
}
}
}
//Hier wordt de tile grid opnieuw gemaakt in het geval als er blijven geen meer moves over.
#changeTileGrid() {
let done = false;
while (done == false) {
for (let x = 0; x < this.#width; x++) {
for (let y = 0; y < this.#height; y++) {
if (this.#level == 1) {
this.#tiles[x][y].type = random(this.#typelist);
}
else if (this.#level == 2) {
this.#tiles[x][y].type = random(this.#typelist2);
}
else if (this.#level == 3) {
this.#tiles[x][y].type = random(this.#typelist3);
}
else if (this.#level == 4) {
this.#tiles[x][y].type = random(this.#typelist4);
}
this.#tiles[x][y].updateImage();
}
}
this.#resolveClusters();
this.#findMoves();
if (this.#moves.length > 0) {
done = true;
}
}
}
//Hier wordt gezocht naar beschikbare moves
#findMoves() {
this.#moves = [];
for (let y = 0; y < this.#height; y++) {
for (let x = 0; x < this.#width - 1; x++) {
if (this.#tiles[x][y].type < 6 && this.#tiles[x + 1][y].type < 6) {
this.#swap(x, y, x + 1, y);
this.#findClusters();
this.#swap(x, y, x + 1, y);
if (this.#clusters.length > 0) {
this.#moves.push({ column1: x, row1: y, column2: x + 1, row2: y })
}
}
}
}
for (let x = 0; x < this.#width; x++) {
for (let y = 0; y < this.#height - 1; y++) {
if (this.#tiles[x][y].type < 6 && this.#tiles[x][y + 1].type < 6) {
this.#swap(x, y, x, y + 1);
this.#findClusters();
this.#swap(x, y, x, y + 1);
if (this.#clusters.length > 0) {
this.#moves.push({ column1: x, row1: y, column2: x, row2: y + 1 })
}
}
}
}
this.#clusters = [];
}
//Dit functie geeft de tegel terug afhankelijk van de positie
getTileAtPosition(position) {
const gridXPosition = Math.floor((position.x - this.#x) / this.#tileSize);
const gridYPosition = Math.floor((position.y - this.#y) / this.#tileSize);
return this.getTileAtGridIndex(gridXPosition, gridYPosition);
}
//Dit functie geeft de tegel terug afhankelijk van de grid index
getTileAtGridIndex(x, y) {
if (x < 0 || x >= this.#width || y < 0 || y >= this.#height) {
throw new Error("index outside of bounds of grid!");
}
return this.#tiles[x][y];
}
}
Clusters¶
Wanneer drie of meer tegels in een rij staan, een cluster wordt gevormd. Deze clusters worden eerst in de lijst gezet, daarna uit de lijst gehaald en uit de tile grid verwijderd.
class TileGrid {
//In deze lijst worden later clusters gezet
#clusters = [];
//Dit functie zorgt voor vinden en verwijderen van clusters
#resolveClusters() {
this.#findClusters();
while (this.#clusters.length > 0) {
this.#removeClusters();
this.#shiftTiles();
this.#findClusters();
}
}
//Hier wordt gekeken naar de aanwezigheid van de clusters
#findClusters() {
this.#clusters = [];
//Zoekt horizontale clusters
for (let y = 0; y < this.#height; y++) {
let matchlength = 1;
for (let x = 0; x < this.#width; x++) {
let checkcluster = false;
if (x == this.#width - 1) {
checkcluster = true;
}
else {
if (this.#tiles[x][y].type == this.#tiles[x + 1][y].type &&
this.#tiles[x][y].type != -1 && this.#tiles[x][y].type < 6) {
matchlength += 1;
}
else {
checkcluster = true;
}
}
if (checkcluster == true) {
if (matchlength >= 3) {
let cluster = {
column: x + 1 - matchlength,
row: y,
length: matchlength,
horizontal: true
}
this.#clusters.push(cluster);
}
matchlength = 1;
}
}
}
//Zoekt verticale clusters
for (let x = 0; x < this.#width; x++) {
let matchlength = 1;
for (let y = 0; y < this.#height; y++) {
let checkcluster = false;
if (y == this.#height - 1) {
checkcluster = true;
}
else {
if (this.#tiles[x][y].type == this.#tiles[x][y + 1].type &&
this.#tiles[x][y].type != -1 && this.#tiles[x][y].type < 6) {
matchlength += 1;
}
else {
checkcluster = true;
}
}
if (checkcluster == true) {
if (matchlength >= 3) {
let cluster = {
column: x,
row: y + 1 - matchlength,
length: matchlength,
horizontal: false
}
this.#clusters.push(cluster);
}
matchlength = 1;
}
}
}
}
//Dit functie zorgt voor de wisseling van de tiles
#swap(x1, y1, x2, y2) {
let typeswap = {
type: this.#tiles[x1][y1].type,
durability: this.#tiles[x1][y1].durability,
image: this.#tiles[x1][y1].image
};
this.#tiles[x1][y1].type = this.#tiles[x2][y2].type;
this.#tiles[x1][y1].durability = this.#tiles[x2][y2].durability;
this.#tiles[x1][y1].image = this.#tiles[x2][y2].image;
this.#tiles[x2][y2].type = typeswap.type;
this.#tiles[x2][y2].durability = typeswap.durability;
this.#tiles[x2][y2].image = typeswap.image;
}
//Hier wordt gezocht naar beschikbare moves
#findMoves() {
this.#moves = [];
for (let y = 0; y < this.#height; y++) {
for (let x = 0; x < this.#width - 1; x++) {
if (this.#tiles[x][y].type < 6 && this.#tiles[x + 1][y].type < 6) {
this.#swap(x, y, x + 1, y);
this.#findClusters();
this.#swap(x, y, x + 1, y);
if (this.#clusters.length > 0) {
this.#moves.push({ column1: x, row1: y, column2: x + 1, row2: y })
}
}
}
}
for (let x = 0; x < this.#width; x++) {
for (let y = 0; y < this.#height - 1; y++) {
if (this.#tiles[x][y].type < 6 && this.#tiles[x][y + 1].type < 6) {
this.#swap(x, y, x, y + 1);
this.#findClusters();
this.#swap(x, y, x, y + 1);
if (this.#clusters.length > 0) {
this.#moves.push({ column1: x, row1: y, column2: x, row2: y + 1 })
}
}
}
}
this.#clusters = [];
}
//Hier woordt door elke cluster over elke cluster
#loopClusters() {
for (let i = 0; i < this.#clusters.length; i++) {
let cluster = this.#clusters[i];
let coffset = 0;
let roffset = 0;
for (let j = 0; j < cluster.length; j++) {
let tile = this.#tiles[cluster.column + coffset][cluster.row + roffset];
tile.type = -1;
//Hier worden de houten en steenrots tegels verwijderd
for (let x = tile.x - 1; x <= tile.x + 1; x++) {
if (x < 0) {
continue;
}
else if (x >= this.#width) {
break;
}
for (let y = tile.y - 1; y <= tile.y + 1; y++) {
if (y < 0) {
continue;
}
else if (y >= this.#height) {
break;
}
let tile2 = this.getTileAtGridIndex(x, y);
if (tile2 != null && tile2.type >= 6) {
tile2.durability -= 1;
if (tile2.durability <= 0) {
tile2.type = -1;
}
}
}
}
if (cluster.horizontal) {
coffset++;
}
else {
roffset++;
}
}
}
}
//Hiet worden de clusters weggehaald
#removeClusters() {
this.#loopClusters();
for (let x = 0; x < this.#width; x++) {
let shift = 0;
for (let y = this.#height - 1; y >= 0; y--) {
if (this.#tiles[x][y].type == -1) {
shift++;
this.#tiles[x][y].shift = 0;
}
else {
this.#tiles[x][y].shift = shift;
}
}
}
}
//Hier worden de nieuwe tegels aangemaakt
#shiftTiles() {
for (let x = 0; x < this.#width; x++) {
for (let y = this.#height - 1; y >= 0; y--) {
if (this.#tiles[x][y].type == -1) {
if (this.score == 0) {
if(this.#level == 1){
this.#tiles[x][y].type = random(this.#typelist);
}
else if(this.#level == 2){
this.#tiles[x][y].type = random(this.#typelist2);
}
}
//Hier worden nieuwe tegels gemaakt
else {
this.#tiles[x][y].type = random(this.#typelist);
}
this.#tiles[x][y].updateImage();
}
else {
let shift = this.#tiles[x][y].shift;
if (shift > 0) {
this.#swap(x, y, x, y + shift);
}
}
this.#tiles[x][y].shift = 0;
}
}
}
}
Swaps¶
Origineel, wanneer ik heb de code geschreven, de swaps werkten niet, en ik wist niet waarom, waardoor we liepen vast tijdens de eerste sprint. Later heb ik het probleem gevonden. Voor sommige reden ik dacht dat als ik de type van de tegels verander, de image zal samen met hun veranderen, maar het werkte zo niet, waardoor ik moest een functie maken die handmatig de image van de tegel zal veranderen. De andere probleem was dat de touch controls werkten niet, waardoor ik moest een aparte functie schrijven die afhankelijk van of de vinger of de muis is gebruikt op eigen manier de input van de speler leest.
class TileGrid {
//Hier wordt gecontroleerd of de bepaalde twee tegels kunnen met elkaar omgewisseld worden
canSwap(x1, y1, x2, y2) {
if ((Math.abs(x1 - x2) == 1 && y1 == y2) ||
(Math.abs(y1 - y2) == 1 && x1 == x2)) {
return true;
}
return false;
}
//Hier worden de coördinaten van de huidige move opgeslagen
touchSwap(c1, r1, c2, r2) {
this.#currentmove = { x1: c1, y1: r1, x2: c2, y2: r2 };
this.selectedTile.selected = false;
this.#animationstate = 2;
this.#animationtime = 0;
this.#gamestate = this.#gamestates.resolve;
}
}
function touchStarted(event) {
//Hier zijn functies die uitgevoerd worden aan het begin van de aanraking
if (drag == false) {
const tile = tileGrid.getTileAtPosition(getPosition(event));
if (tile != null && tile.type < 6) {
let swapped = false;
if (tileGrid.selectedTile.selected == true) {
if (tile.x == tileGrid.selectedTile.column &&
tile.y == tileGrid.selectedTile.row) {
tileGrid.selectedTile.selected = false;
drag = true;
return;
}
else if (tileGrid.canSwap(tile.x, tile.y, tileGrid.selectedTile.column, tileGrid.selectedTile.row)) {
tileGrid.touchSwap(tile.x, tile.y, tileGrid.selectedTile.column, tileGrid.selectedTile.row);
movesleft -= 1;
swapped = true;
}
}
if (swapped == false) {
tileGrid.selectedTile.column = tile.x;
tileGrid.selectedTile.row = tile.y;
tileGrid.selectedTile.selected = true;
}
}
else {
tileGrid.selectedTile.selected = false;
}
drag = true;
}
}
//Deze functie wordt uitgevoerd als de speler zijn vinger beweegd
function touchMoved(event) {
if (drag == true && tileGrid.selectedTile.selected == true) {
const tile = tileGrid.getTileAtPosition(getPosition(event));
if (tile != null && tile.type < 6) {
if (tileGrid.canSwap(tile.x, tile.y, tileGrid.selectedTile.column, tileGrid.selectedTile.row)) {
tileGrid.touchSwap(tile.x, tile.y, tileGrid.selectedTile.column, tileGrid.selectedTile.row);
movesleft -= 1;
}
}
}
}
//Deze functie wordt uitgevoerd als de speler zijn vinger loslaat
function touchEnded(event) {
drag = false;
}
//Hier wordt de positie van de vinger of muis getraceerd
function getPosition(event) {
let rect = canvas.getBoundingClientRect();
//Afhankelijk van of de vinger of muis is gebruikt, wordtde positie op unieke manier getraceerd
if (event.type == 'touchstart' || event.type == 'touchmove') {
let evt = (typeof event.originalEvent === 'undefined') ? event : event.originalEvent;
let touch = evt.touches[0] || evt.changedTouches[0]
return createVector(touch.pageX - rect.left, touch.pageY - rect.top);
}
else if (event.type == 'mousedown' || event.type == 'mousemove') {
return createVector(event.x - rect.left, event.y - rect.top);
}
}
Animaties¶
En natuurlijk alles moet goed eruitzien, dus we hebben functies gemaakt om alles te animeren. Ook deze functies zorgen ervoor dat als de clusters verwijderd worden, de score en de aanvalskracht van de speler stijgt.
class TileGrid {
//Hier staat de score van de speler.
score = 0;
//Hier staat de spelers aanvalskracht
playerdamage = 0;
#gamestates = {
init: 0,
ready: 1,
resolve: 2
}
#gamestate = this.#gamestates.init;
//Deze parameters zijn voor animaties bedoeld
#animationstate = 0;
#animationtime = 0;
#animationtimetotal = 180;
//Hier worden de animaties geregeld voor bewegen van de tegels
#renderTiles(time) {
let tile1 = this.#tiles[this.#currentmove.x1][this.#currentmove.y1];
let tile2 = this.#tiles[this.#currentmove.x2][this.#currentmove.y2];
if (this.#gamestate == this.#gamestates.resolve && (this.#animationstate == 2 || this.#animationstate == 3)) {
let shiftx = tile2.x - tile1.x;
let shifty = tile2.y - tile1.y;
let coord1shift = tile1.getTileCoordinate(time * shiftx, time * shifty);
let image1 = tile1.image;
let coord2shift = tile2.getTileCoordinate(time * -shiftx, time * -shifty);
let image2 = tile2.image;
tile1.visible = false;
tile2.visible = false;
fill(128, 0, 0);
stroke(25);
strokeWeight(3);
if (this.#animationstate == 2) {
rect(coord1shift.x, coord1shift.y, this.#tileSize, this.#tileSize);
image(image1, coord1shift.x, coord1shift.y, this.#tileSize, this.#tileSize);
rect(coord2shift.x, coord2shift.y, this.#tileSize, this.#tileSize);
image(image2, coord2shift.x, coord2shift.y, this.#tileSize, this.#tileSize);
}
//Als de wisseling van tegels geen clusters vormt, worden de tegels teruggewisseld
else {
rect(coord2shift.x, coord2shift.y, this.#tileSize, this.#tileSize);
image(image2, coord2shift.x, coord2shift.y, this.#tileSize, this.#tileSize);
rect(coord1shift.x, coord1shift.y, this.#tileSize, this.#tileSize);
image(image1, coord1shift.x, coord1shift.y, this.#tileSize, this.#tileSize);
}
}
else {
tile1.visible = true;
tile2.visible = true;
}
}
//Hier worden de resultaten van de player input gehandeld
update(deltaTime) {
if (this.#gamestate == this.#gamestates.ready && this.#moves.length <= 0) {
this.#changeTileGrid();
}
else if (this.#gamestate == this.#gamestates.resolve) {
this.#animationtime += deltaTime;
if (this.#animationstate == 0) {
if (this.#animationtime > this.#animationtimetotal) {
this.#findClusters();
if (this.#clusters.length > 0) {
for (let i = 0; i < this.#clusters.length; i++) {
this.score += 100 * (this.#clusters[i].length - 2);
this.playerdamage += 10 * (this.#clusters[i].length - 2);
}
this.#removeClusters()
this.#animationstate = 1;
}
else {
this.#gamestate = this.#gamestates.ready;
}
this.#animationtime = 0;
}
}
//Animationstate voor bewegen van de tegels na verwijdering van clusters
else if (this.#animationstate == 1) {
if (this.#animationtime > this.#animationtimetotal) {
this.#shiftTiles();
this.#animationstate = 0;
this.#animationtime = 0;
this.#findClusters();
if (this.#clusters.length <= 0) {
this.#gamestate = this.#gamestates.ready
}
}
}
//Animationstate voor wisseling van tegels
else if (this.#animationstate == 2) {
if (this.#animationtime > this.#animationtimetotal) {
this.#swap(this.#currentmove.x1, this.#currentmove.y1, this.#currentmove.x2, this.#currentmove.y2);
this.#findClusters();
if (this.#clusters.length > 0) {
this.#animationstate = 0;
this.#animationtime = 0;
this.#gamestate = this.#gamestates.resolve;
}
else {
this.#animationstate = 3;
this.#animationtime = 0;
}
this.#findMoves();
this.#findClusters();
}
}
//Animationstate voor terugwisseling van tegels
else if (this.#animationstate == 3) {
if (this.#animationtime > this.#animationtimetotal) {
this.#swap(this.#currentmove.x1, this.#currentmove.y1, this.#currentmove.x2, this.#currentmove.y2);
this.#gamestate = this.#gamestates.ready;
}
}
this.#findMoves();
this.#findClusters();
}
}
//Hier wordt de tile grid getekend
draw() {
fill(0);
textSize(75);
noStroke();
text(this.score, this.#x, this.#y - this.#tileSize / 3);
fill(165, 42, 42);
stroke(25);
strokeWeight(5);
rect(this.#x, this.#y, this.#width * this.#tileSize, this.#height * this.#tileSize);
for (let x = 0; x < this.#width; x++) {
for (let y = 0; y < this.#height; y++) {
this.#tiles[x][y].draw(this.#animationtime / this.#animationtimetotal);
}
}
this.update(deltaTime);
this.#renderTiles(this.#animationtime / this.#animationtimetotal);
}
//Det functie controleert of de tile grid stilstaat
get act() {
if (this.#gamestate == this.#gamestates.ready) {
return true;
}
else {
return false;
}
}
}
Bronnen¶
Rembound. (2020, 17 februari). How to make a match-3 game with HTML5 canvas. Rembound. https://rembound.com/articles/how-to-make-a-match3-game-with-html5-canvas
Rembound. (z.d.). Match-3-Game-HTML5/match3-example.js at Master · Rembound/Match-3-Game-HTML5. GitHub. https://github.com/rembound/Match-3-Game-HTML5/blob/master/match3-example.js
Touch events. (z.d.). https://www.w3schools.com/jsref/obj_touchevent.asp
Mouse events. (z.d.). https://www.w3schools.com/jsref/obj_mouseevent.asp
Touch - Web APIs | MDN. (2023, 20 november). MDN Web Docs. https://developer.mozilla.org/en-US/docs/Web/API/Touch
Determine touch position on tablets with JavaScript. (z.d.). Stack Overflow. https://stackoverflow.com/questions/41993176/determine-touch-position-on-tablets-with-javascript/61732450#61732450
Personages¶
Sinds onze game is deels een rpg waar je moet monsters verslaan, we hebben die ook toegevoegd. We hebben ook sub-classes gemaakt voor de held en voor de vijanden. Elk personage heeft eigen hp (aantal health points), aanvalskracht (of, in andere woorden, damage) en sprites.
class Character {
//De coördianten van waar de personage moet getekend worden
#x
#y
#role
//De huidige en maximale hp van de personage
#hp
#maxhp
_damage
_spritesheet
#size
#color
//Hier door gebruik van get en set worden de parameters van dit class gelezen en veranderd
get x() {
return this.#x;
}
get y() {
return this.#y;
}
get hp() {
return this.#hp
}
set hp(value) {
this.#hp = value;
}
get damage() {
return this._damage;
}
set damage(value) {
this._damage = value;
}
get position() {
return createVector(this.#x, this.#y).mult(this.#size);
}
constructor(x, y, role, hp, size, color) {
this.#x = x;
this.#y = y;
this.#role = role;
this.#hp = hp;
this.#maxhp = hp;
this.#size = size;
this.#color = color
this.#animationstate = 1;
}
}
class Hero extends Character {
//Hier wordt de held aangemaakt
constructor(x, y, size) {
super(x, y, "pc", 500, size, color(0, 0, 255));
this._damage = 0;
this._spritesheet = {
idle: gameManager.getImage("knight_idle"),
attack: gameManager.getImage("knight_attack"),
hurt: gameManager.getImage("knight_hurt"),
dead: gameManager.getImage("knight_dead")
}
}
}
class Enemy extends Character {
#moves
#mindmg
#maxdmg
get moves() {
return this.#moves
}
//Hier wordt de willekeurige aanvalskracht gemaakt tussen minimale en maximale waarde
#getRndInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
updateDamage() {
this._damage = this.#getRndInteger(this.#mindmg, this.#maxdmg);
}
constructor(type, x, y) {
if (type == "skeleton") {
super(x, y, "npc", 400, 300, color(255, 0, 0));
this.#moves = 3;
this.#mindmg = 50;
this.#maxdmg = 75;
this._spritesheet = {
idle: gameManager.getImage("skeleton_idle"),
attack: gameManager.getImage("skeleton_attack"),
hurt: gameManager.getImage("skeleton_hurt"),
dead: gameManager.getImage("skeleton_dead")
}
}
else if (type == "slime") {
super(x, y, "npc", 250, 150, color(255, 0, 0));
this.#moves = 3;
this.#mindmg = 25;
this.#maxdmg = 50;
this._spritesheet = {
idle: gameManager.getImage("slime_idle"),
attack: gameManager.getImage("slime_attack"),
hurt: gameManager.getImage("slime_hurt"),
dead: gameManager.getImage("slime_dead")
}
}
}
}
Animaties¶
En natuurlijk, de personages moeten bewegen, dus we hebben spritesheets uit de internet gedownload en die geanimeerd.
class Character {
//De coördianten van waar de personage moet getekend worden
#x
#y
#role
//De huidige en maximale hp van de personage
#hp
#maxhp
_damage
_spritesheet
#size
#color
//Deze parameters zijn voor animaties bedoeld
#animationstate = 0;
#animationtime = 0;
#animationtimetotal = 900;
//De twee functies bewgen zorgen voor het tekenen van de personages
draw(sprite) {
let frame;
let time = Math.floor(this.#animationtime / (this.#animationtimetotal / Math.round(sprite.width / 128)));
const distance = 128;
let spritex;
const spritesize = this.#size * 1.5;
if (this.#role == "pc") {
frame = distance * time;
spritex = this.#x;
}
else if (this.#role == "npc") {
frame = sprite.width - distance * (1 + time);
spritex = this.#x - (spritesize - this.#size) / 2;
}
//Hier wordt de healthbar getekend
fill(255, 0, 0);
stroke(0);
strokeWeight(5);
rect(this.#x, this.#y, this.#size, 30, 10);
if (this.#hp > 0) {
fill(0, 255, 0);
stroke(0);
strokeWeight(3);
rect(this.#x, this.#y, this.#size * (this.#hp / this.#maxhp), 30, 10);
}
//Bij aanval wordt de aanvalskracht weergegeven
if (this.#animationstate == 2) {
fill(this.#color);
textSize(100);
textFont(gameManager.getFont("medieval"));
text(this._damage, this.#x + this.#size / 3, this.#y - this.#size);
}
image(sprite, spritex, this.#y - spritesize, spritesize, spritesize, frame, 0, sprite.height, sprite.height);
//Hier wordt de animatie opnieuw gespeelt
if ((frame >= sprite.width && this.#role == "pc") || (frame < 0 && this.#role == "npc")) {
if (this.#animationstate > 0 && this.#animationstate < 4) {
this.#animationstate = 1;
frame = 0;
this.#animationtime = 0;
}
else if (this.#animationstate == 4) {
this.#animationstate = 0;
}
}
}
//Hier wordt bepaald welke animatie moet getekend worden
update(deltaTime) {
if (this.#animationstate > 0) {
this.#animationtime += deltaTime;
if (this.#animationstate == 1) {
this.draw(this._spritesheet.idle);
}
else if (this.#animationstate == 2) {
this.draw(this._spritesheet.attack);
}
else if (this.#animationstate == 3) {
this.draw(this._spritesheet.hurt);
}
else if (this.#animationstate == 4) {
this.draw(this._spritesheet.dead);
}
}
}
constructor(x, y, role, hp, size, color) {
this.#x = x;
this.#y = y;
this.#role = role;
this.#hp = hp;
this.#maxhp = hp;
this.#size = size;
this.#color = color
this.#animationstate = 1;
}
}
Bronnen¶
Ram, P. (2019, 29 augustus). How to build a simple sprite animation in JavaScript. Medium. https://medium.com/dailyjs/how-to-build-a-simple-sprite-animation-in-javascript-b764644244aa
Aanval¶
De damage van de held stijgt wanneer de tegels zijn verwijderd. Hoe meer tegels zijn verwijderd per move, hoe hoger de damage van de held. Terwijl de vijand heeft vaste minimale en maximale damage en de damage die de vijand uitvoert tijdens zijn beurt is een willekeurige getal tussen minimum en maximum.
class Character {
//De functies bewegen worden uitgevoerd afhakelijk van of de personage valt aan, geraakt wordt, of doodgaat.
attack(enemy) {
if (this.#hp > 0) {
this.#animationstate = 2;
this.#animationtime = 0;
enemy.hp -= this._damage;
enemy.hurt();
}
}
hurt() {
this.#animationstate = 3;
this.#animationtime = 0;
}
death() {
this.#animationstate = 4;
}
}
//Dit is de functie voor gevecht
function fight() {
timer();
hero.update(deltaTime);
for (let enemy of enemies) {
enemy.update(deltaTime);
if (enemy.hp <= 0){
if(enemy.act != 0){
enemy.death();
}
else{
enemies = enemies.filter(checkHP);
}
}
}
if (tileGrid.act == true) {
if (tileGrid.playerdamage > 0) {
hero.damage = tileGrid.playerdamage;
hero.attack(enemies[0]);
tileGrid.playerdamage = 0;
}
else if (hero.act != 2 && movesleft <= 0) {
for (let enemy of enemies) {
enemy.updateDamage();
enemy.attack(hero);
tileGrid.score -= 100;
movesleft = enemies[0].moves;
}
}
}
}
//Hier wordt gecontroleerd op hoeveel hp blijft er over
function checkHP(character){
return character.hp > 0;
}
HTML Elementen¶
Net zoals voige blok, ik heb een div gemaakt waarin ik kan de canvas en andere HTML-elementen plaatsen (zodat HTML-elementen worden ten opzichte van de canvas gepositioneerd).
<body>
<main>
<!--In dit div wordt later canvas en andere html-elementen geplaatst die met javascript worden gecreërd-->
<div id="canvas-wrap"></div>
</main>
</body>
html {
height: 100%;
}
body {
margin: 0;
padding: 0;
background-image: url('assets/images/windowbg.jpg');
background-repeat: no-repeat;
background-size: 100% 100%;
height: 100%;
justify-content: center;
align-items: center;
}
main {
min-height: 100%;
}
canvas {
padding: 0;
display: block;
position: absolute;
margin: auto;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
/*Hier zijn styles voor een div waarin canvas en html-elementen worden geplaatst die met javascript gemaakt worden.*/
#canvas-wrap {
padding: 0;
margin: auto;
width: 800px;
height: 1400px;
/*Hier wordt een rand voor de canvas gemaakt.*/
border: 10px;
border-radius: 5px;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
button {
margin: auto;
position: relative;
background: rgba(255, 255, 255, 0.2);
color: white;
text-align: center;
border-color: white;
border-style: solid;
border-width: thin;
cursor: pointer;
font-family: 'MedievalSharp', cursive;
right: 0;
top: 700px;
bottom: 0;
}
Bronnen¶
Reference | P5.js. (z.d.-m). https://p5js.org/reference/#/p5/createElement
Placing a div within a canvas. (z.d.). Stack Overflow. https://stackoverflow.com/questions/5763911/placing-a-div-within-a-canvas
Winnen en verliezen¶
Als de held alle hp verliest, gaat de held dood en heeft de speler verloren. Als de vijand alle hp verliest, gaat die dood en wordt die uit de lijst met de vijanden verwijderd. Als er blijven geen meer vijanden over, de speler wint. Daarna ziet de speler een scherm met zijn score en de scoreboard. Ook zijn er twee knoppen om de level opnieuw te spelen of om terug naar de menu gaan.
//Dit is de functie voor gevecht
function fight() {
if (hero.hp <= 0) {
if (hero.act != 0) {
hero.death();
}
else {
gameManager.saveResults(minutes + ':' + seconds, moves, "lose", tileGrid.score);
defeat = true;
}
}
else if (enemies.length == 0) {
gameManager.saveResults(minutes + ':' + seconds, moves, "win", tileGrid.score);
victory = true;
}
}
//Hier wordt gecontroleerd op hoeveel hp blijft er over
function checkHP(character) {
return character.hp > 0;
}
//Dit functie laat de resultaat zien van de level
function gameResult() {
gameManager.UpdateScore(tileGrid.score, tileGrid.levelnr, minutes + ':' + seconds);
textSize(72);
textFont(font);
noStroke();
if (victory == true) {
fill(0, 0, 255);
text("Victory", width / 3, 200);
}
if (defeat == true) {
fill(255, 0, 0);
text("Game Over", width / 4, 200);
}
//Hier wordt de score weergegeven
textSize(48);
text("Your score: " + tileGrid.score, width / 4, 350);
//Hier worden de knoppen aangemaakt om de level te verlaten
let retryButton = document.createElement("button");
retryButton.setAttribute("type", "button");
retryButton.innerHTML = "Try Again";
retryButton.setAttribute("onclick", "exitLevel(true)");
retryButton.setAttribute("class", "result");
let returnButton = document.createElement("button");
returnButton.setAttribute("type", "button");
returnButton.innerHTML = "Exit to the menu";
returnButton.setAttribute("onclick", "exitLevel(false)");
returnButton.setAttribute("class", "result");
returnButton.style.left = "150px";
if (resultappend == true) {
LeaderBoard();
canvaswrap.appendChild(retryButton);
canvaswrap.appendChild(returnButton);
resultappend = false;
}
}
//Dit is de functie om de level te verlaten en terug naar de menu gaan
function exitLevel(retry) {
removeHTMLElements("result");
removeHTMLElements("scoreboard");
victory = false;
defeat = false;
done = false;
seconds = 0;
minutes = 0;
moves = 0;
if (retry == true) {
generateLevel(tileGrid.levelnr);
}
else {
levelappend = true;
}
}
//Dit functie is om html-elemeten te verwijderen.
function removeHTMLElements(classname) {
let elements = document.getElementsByClassName(classname);
const amount = elements.length;
for (let i = 0; i < amount; i++) {
elements[0].remove();
}
}
.result{
padding: 5px;
display: inline-block;
border-radius: 5px;
font-size: 50px;
left: 100px;
top: 650px;
}
Startscherm¶
Aan het begin van het spel, ziet de speler een startscherm. Als de speler nieuw is, wordt er gevraagd om zijn naam in te voeren. Dan wordt een unieke code voor de speler gemaakt en opgeslagen op de computer (meer daarover in de “Database” deel);
function StartScreen() {
//Hier wordt de titel getekend
textSize(96);
fill(255, 215, 0);
textFont(font);
text('Jewel', width / 3, 200);
text('Quest', width / 3, 300);
//Hier wordt de functie uitgevoerd om een nieuwe speler te maken
if (gameManager.newplayer == true) {
EnterName();
}
//Hier wordt een knop aangemaakt om het spel te beginnen
else {
let playButton = document.createElement("button");
playButton.setAttribute("type", "button");
playButton.innerHTML = "Play";
playButton.setAttribute("onclick", "enterTheGame()");
playButton.setAttribute("class", "startbutton");
if (loginappend == true) {
playButton.style.fontSize = "96px";
canvaswrap.appendChild(playButton);
loginappend = false;
}
}
}
function EnterName() {
textSize(56);
textFont(font);
fill(255);
text('Please enter your name', width / 10, 650);
let nameInput = document.createElement("input");
nameInput.setAttribute("type", "text");
nameInput.setAttribute("id", "userName");
nameInput.setAttribute("placeholder", "Username");
nameInput.setAttribute("oninput", "turnButtonOn(submitButton)");
nameInput.setAttribute("autocomplete", "off");
let submitButton = document.createElement("button");
submitButton.setAttribute("type", "button");
submitButton.setAttribute("disabled", "true")
submitButton.innerHTML = "Confirm";
submitButton.setAttribute("onclick", "enterTheGame()");
submitButton.setAttribute("class", "startbutton");
submitButton.setAttribute("id", "submitButton");
if (loginappend == true) {
canvaswrap.appendChild(nameInput);
canvaswrap.appendChild(submitButton);
loginappend = false;
}
}
//Dit is een functie om een knop aan te zetten als een naam (die moet ten minste 3 letters lang zijn) was ingevuld.
function turnButtonOn(button) {
failure = false;
let userName = document.getElementById("userName").value;
if (userName.length >= 3) {
button.disabled = false;
}
else if (userName.length < 3) {
button.disabled = true;
}
}
//Hier wordt een nieuwe speler aangemaakt (als dat nodig is) en html-elementen verwijderen
function enterTheGame() {
if (gameManager.newplayer == true) {
gameManager.savePlayer();
}
let inputbar = document.getElementById("userName");
if (inputbar != null) {
inputbar.remove();
}
removeHTMLElements("startbutton");
start = false;
}
input {
padding: 5px;
display: inline-block;
margin: auto;
margin-bottom: 100px;
position: relative;
display: block;
width: auto;
border: 2px solid;
border-radius: 10px;
background: rgba(0, 0, 0, 0);
top: 750px;
bottom: 0;
left: 0;
right: 0;
color: white;
justify-content: center;
font-size: 50px;
}
.startbutton{
padding: 5px;
display: block;
border-radius: 5px;
font-size: 64px;
}
Meerdere levels¶
Nadat de speler heeft de game gestart, ziet die een scherm met vijf knoppen voor elk level. Bij het aanklikken van de knop wordt de bijbehorende level aangemaakt. Nadat je een level voltooit, wordt de volgende level ontgrendeld.
function LevelSelect() {
textSize(96);
fill(215, 0, 0);
textFont(font);
text('Select level', 150, 250);
let levelProgress = localStorage.getItem("levelProgress");
for (let i = 1; i < 6; i++) {
let levelbutton = document.createElement("button");
levelbutton.setAttribute("type", "button");
levelbutton.setAttribute("class", "levelbutton");
levelbutton.innerHTML = i;
if (i > levelProgress) {
levelbutton.setAttribute("disabled", "true");
}
levelbutton.setAttribute("onclick", "generateLevel(this.innerHTML)");
if (i >= 4) {
levelbutton.style.top = "750px";
levelbutton.style.left = "200px";
}
if (levelappend == true) {
canvaswrap.appendChild(levelbutton);
if (i == 5) {
levelappend = false;
}
}
}
}
//Dit is een functie om een nieuwe level aan te maken
function generateLevel(levelnr) {
removeHTMLElements("levelbutton");
if (levelnr > 0) {
if (!done) {
tileGrid = new TileGrid(gridx, gridy, tileWidth, tileHeight, tileSize, levelnr);
if (levelnr == 1) {
hero = new Hero(50, chary, 300);
let enemy = new Enemy("skeleton", width - 400, chary);
enemies.push(enemy);
movesleft = enemy.moves;
}
else if (levelnr == 2) {
hero = new Hero(50, chary, 250);
for (let i = 0; i < 2; i++) {
let enemy = new Enemy("slime", width - 400 + 200 * i, chary);
enemies.push(enemy);
}
movesleft = enemies[0].moves;
}
else if (levelnr == 3) {
hero = new Hero(50, chary, 300);
let enemy = new Enemy("werewolf", width - 400, chary);
enemies.push(enemy);
movesleft = enemy.moves;
}
else if (levelnr == 4) {
hero = new Hero(50, chary, 250);
for (let i = 0; i < 2; i++) {
let enemy = new Enemy("slime", width - 400 + 200 * i, chary);
enemies.push(enemy);
}
movesleft = enemies[0].moves;
}
else if (levelnr == 5) {
hero = new Hero(50, chary, 300);
let enemy = new Enemy("wizard", width - 400, chary);
enemies.push(enemy);
movesleft = enemy.moves;
}
//Hier wordt een nieuwe speelsessie in de database aangemaakt
gameManager.createSession(levelnr);
done = true;
}
}
}
/*Hier staan styles die aan de levelknoppen toegepast moeten worden.*/
.levelbutton {
display: inline-block;
margin-right: 100px;
font-size: 120px;
border-radius: 20px;
left: 75px;
height: 150px;
width: 150px;
}
Timer¶
Net als vorige blok, ik heb een timer toegevoegd om te tijd erbij te halen.
function timer() {
seconds = constrain(seconds % 60, 0, 60);
// Hiet wordt een text geschreven die laat de tijd zien.
fill(255, 215, 0);
stroke(128, 0, 0);
strokeWeight(5);
rect(gridx + (tileWidth * tileSize) * 2 / 3, gridy - 100, tileWidth * tileSize / 3, 75, 20);
textSize(64);
fill(75, 0, 0);
textFont(font);
stroke(0);
strokeWeight(1);
// Hier worden frames in seconden omgezet
if (frameCount % fr == 0) {
seconds += 1;
}
//Bij 60 seconden stijgt het aantal minuten.
if (seconds == 60) {
minutes += 1;
}
let time;
//Afhankelijk van aantal minuten en seconden zal de tijd anders eruitzien
if (seconds <= 9) {
if (minutes <= 9) {
time = '0' + minutes + ':0' + seconds;
}
else if (minutes > 9) {
time = minutes + ':0' + seconds;
}
}
else if (seconds > 9) {
if (minutes <= 9) {
time = '0' + minutes + ':' + seconds;
}
else if (minutes > 9) {
time = minutes + ':' + seconds;
}
}
text(time, gridx + (tileWidth * tileSize) * 2 / 3, gridy - tileSize / 3);
}
Bronnen¶
Reference | P5.js. (z.d.-h). https://p5js.org/reference/#/p5/frameCount
Remainder (%) - JavaScript | MDN. (2023, 25 augustus). https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Remainder
Actergrond en fonts¶
Fonts en images worden in de AssetManager geladen. Voor de achtergrond hebben we een grot gekozen voor de bovenste deel van de canvas, een stenen achtergrond voor de onderste deel van de canvas, en een middeleeuwse rol voor de rest van het scherm. En voor de font heb ik een middeleeuwse font uit de internet gedownload.
class AssetManager {
static #instance;
#images;
#fonts;
constructor() {
this.#images = new Map();
this.#fonts = new Map();
this.#loadImages();
this.#loadFonts();
window.assetManager = this;
}
#loadImages() {
this.#images.set("diamond", loadImage("assets/images/diamond.svg"));
this.#images.set("emerald", loadImage("assets/images/emerald.png"));
this.#images.set("ruby", loadImage("assets/images/ruby.png"));
this.#images.set("sapphire", loadImage("assets/images/sapphire.png"));
this.#images.set("obsidian", loadImage("assets/images/obsidian.png"));
this.#images.set("wood", loadImage("assets/images/wood.png"));
this.#images.set("stone", loadImage("assets/images/stone.png"));
this.#images.set("lair", loadImage("assets/images/lair.jpg"));
this.#images.set("background", loadImage("assets/images/rockybg.jpg"));
this.#images.set("knight_idle", loadImage("assets/sprites/Knight/Idle.png"));
this.#images.set("knight_attack", loadImage("assets/sprites/Knight/Attack.png"));
this.#images.set("knight_hurt", loadImage("assets/sprites/Knight/Hurt.png"));
this.#images.set("knight_dead", loadImage("assets/sprites/Knight/Dead.png"));
this.#images.set("skeleton_idle", loadImage("assets/sprites/Skeleton/Idle.png"));
this.#images.set("skeleton_attack", loadImage("assets/sprites/Skeleton/Attack.png"));
this.#images.set("skeleton_hurt", loadImage("assets/sprites/Skeleton/Hurt.png"));
this.#images.set("skeleton_dead", loadImage("assets/sprites/Skeleton/Dead.png"));
this.#images.set("slime_idle", loadImage("assets/sprites/Slime/Idle.png"));
this.#images.set("slime_attack", loadImage("assets/sprites/Slime/Attack.png"));
this.#images.set("slime_hurt", loadImage("assets/sprites/Slime/Hurt.png"));
this.#images.set("slime_dead", loadImage("assets/sprites/Slime/Dead.png"));
this.#images.set("wizard_idle", loadImage("assets/sprites/Wizard/Idle.png"));
this.#images.set("wizard_attack", loadImage("assets/sprites/Wizard/Attack.png"));
this.#images.set("wizard_hurt", loadImage("assets/sprites/Wizard/Hurt.png"));
this.#images.set("wizard_dead", loadImage("assets/sprites/Wizard/Dead.png"));
this.#images.set("werewolf_idle", loadImage("assets/sprites/Werewolf/Idle.png"));
this.#images.set("werewolf_attack", loadImage("assets/sprites/Werewolf/Attack.png"));
this.#images.set("werewolf_hurt", loadImage("assets/sprites/Werewolf/Hurt.png"));
this.#images.set("werewolf_dead", loadImage("assets/sprites/Werewolf/Dead.png"));
}
#loadFonts(){
this.#fonts.set("medieval", loadFont("assets/fonts/Medieval.ttf"));
}
getImage(assetname) {
try {
return this.#images.get(assetname);
} catch (exc) {
throw new Error("file does not exist!");
}
}
getFont(assetname){
try {
return this.#fonts.get(assetname);
} catch (exc) {
throw new Error("file does not exist!");
}
}
}
class GameManager {
#analyticsTrackerManager;
#assetManager;
constructor() {
this.#analyticsTrackerManager = new AnalyticsTrackerManager();
this.#assetManager = new AssetManager();
window.gameManager = this;
}
getImage(assetname) {
return this.#assetManager.getImage(assetname);
}
getFont(assetname){
return this.#assetManager.getFont(assetname);
}
}
function preload() {
new GameManager();
//Hier worden de images en fonts geladen
lair = gameManager.getImage("lair");
bgimage = gameManager.getImage("background");
font = gameManager.getFont("medieval");
canvaswrap = document.getElementById("canvas-wrap");
}
function draw() {
image(bgimage, 0, width * 2 / 3, width, width * 3 / 2);
//Hier wordt de achergrond getekend en functies uitgevoerd
image(lair, 0, 0, width, width * 2 / 3);
noStroke();
if (start == true) {
StartScreen();
}
else if (done == false && start == false) {
LevelSelect();
}
else if (victory == false && defeat == false && done == true) {
fight()
tileGrid.draw();
}
else if (victory == true || defeat == true) {
gameResult();
}
}
Bronnen¶
MedievalSharp - Google Fonts. (z.d.). Google Fonts. https://fonts.google.com/specimen/MedievalSharp
Reference | P5.js. (z.d.-k). https://p5js.org/reference/#/p5/textFont
Reference | P5.js. (z.d.-i). https://p5js.org/reference/#/p5/image
Database¶
Nadat ik heb tabellen bij de database gemaakt, heb ik voor elk tabel eigen php-bestand gemaakt om gegevens eerst in de database op te slaan, en daarna die uit de database te halen (bij sommigen alleen om op te slaan).
User.php¶
Hier wordt de speler in de database gemaakt en uit de gatabase gehaald
// Handle request type logic
switch ($requestMethod) {
case 'PUT':
// nothing yet
break;
case 'POST':
// README: https://lornajane.net/posts/2008/accessing-incoming-put-data-from-php
$params = file_get_contents('php://input'); // Access the request PUT data
$paramsArray = json_decode($params, true);
createPlayer($paramsArray);
break;
case 'GET':
findPlayer();
break;
default:
handleError('Invalid HTTP request method');
break;
}
function findPlayer()
{
if (isset($_GET['code'])) {
$playerUniqueCode = $_GET["code"];
$dbReturnID = 0;
$dbAanmaakDatum = 0;
$dbUsername = NULL;
// Search for player in the database based on GET param playerName
try {
$dbConnect = new DatabaseConnection();
$playerUniqueCode = mysqli_real_escape_string($dbConnect->getConnection(), $playerUniqueCode);
$statement = "
SELECT
id, aanmaakDatumTijd, Username
FROM
Blok2_Speler
WHERE
uniqueCode = '" . $playerUniqueCode . "';
";
// Execute query-statement on the database
$result = $dbConnect->executeQuery($statement);
if (!$result) {
$errorMsg = $dbConnect->getConnection()->error;
handleError('' . $errorMsg);
} else {
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
$dbReturnID = $row['id'];
$dbAanmaakDatum = $row['aanmaakDatumTijd'];
$dbUsername = $row['Username'];
// Return JSON structured data with requested/needed information about the existing player
echo '{"responseType":"ok", "aanmaakDatumTijd":"' . $dbAanmaakDatum . '", "id":' . $dbReturnID . ',
"Username":"' . $dbUsername . '"}';
} else {
// No records found, return error notifying the user about this.
handleNotFound('user with uniqueCode ' . $playerUniqueCode);
}
}
// Free the result set
$result->free();
} catch (Exception $e) {
$dbReturnID = -1;
$errorMsg = 'Failed to query the database. Err: ' . $e->getMessage();
handleError('' . $errorMsg);
}
} else {
handleError('No unqiue player code received via GET, or use POST with param createPlayer : true to create a new player.');
}
}
function createPlayer($paramsPostArray)
{
$errorMsg = NULL;
$bCreatePlayer = NULL;
$bUsername = NULL;
if (isset($paramsPostArray["createPlayer"], $paramsPostArray["userName"])) {
$uniqueDBCode = -1;
$dbReturnId = -1;
$bCreatePlayer = $paramsPostArray["createPlayer"];
$bUsername = $paramsPostArray["userName"];
if ($bCreatePlayer == true) {
// Add a new player to the database and return player data (row id, uniqueCode, aanmaakDatum)
try {
$dbConnect = new DatabaseConnection();
// Generate a unique token of 23 characters with a 'player_' prefix (30 characters)
$uniqueDBCode = uniqid('player_', true);
$statement = "
INSERT INTO Blok2_Speler
(uniqueCode, aanmaakDatumTijd, Username)
VALUES
('" . $uniqueDBCode . "', now(), '" . $bUsername . "');
";
// Execute query-statement on the Database
$bSucces = $dbConnect->executeQuery($statement);
if (!$bSucces) {
$errorMsg = $dbConnect->getConnection()->error;
handleError('Insert failed: ' . $errorMsg);
} else {
$dbReturnId = $dbConnect->getConnection()->insert_id;
// Return JSON structured data with requested/needed information about the new player
echo '{"responseType":"ok", "uniqueCode": "' . $uniqueDBCode . '"}';
}
} catch (Exception $e) {
$dbReturnId = -1;
$errorMsg = 'Failed to query the database. Err: ' . $e->getMessage();
handleError('' . $errorMsg);
}
}
} else {
handleError('No createPlayer POST param received');
}
}
Session.php¶
Dit is om een nieuwe sessie aan te makken.
// Handle request type logic
switch ($requestMethod) {
case 'PUT':
// nothing yet
break;
case 'POST':
// README: https://lornajane.net/posts/2008/accessing-incoming-put-data-from-php
$params = file_get_contents('php://input'); // Access the request PUT data
$paramsArray = json_decode($params, true);
createSession($paramsArray);
break;
case 'GET':
handleError('Invalid HTTP request method');
break;
default:
handleError('Invalid HTTP request method');
break;
}
function createSession($paramsPostArray)
{
$errorMsg = NULL;
$SpelerUniqueCode = NULL;
$bLevelID = NULL;
if (isset($paramsPostArray["PlayerCode"], $paramsPostArray["Levelnumber"])) {
$dbReturnId = -1;
$SpelerUniqueCode = $paramsPostArray["PlayerCode"];
$bLevelID = $paramsPostArray["Levelnumber"];
// Add a new player to the database and return player data (row id, uniqueCode, aanmaakDatum)
try {
$dbConnect = new DatabaseConnection();
$statement = "
INSERT INTO Blok2_Speelsessie
(Blok2_Speler_uniqueCode, Level_id, startDatumTijd)
VALUES
('" . $SpelerUniqueCode . "', '" . $bLevelID . "', now());
";
// Execute query-statement on the Database
$bSucces = $dbConnect->executeQuery($statement);
if (!$bSucces) {
$errorMsg = $dbConnect->getConnection()->error;
handleError('Insert failed: ' . $errorMsg);
} else {
$dbReturnId = $dbConnect->getConnection()->insert_id;
// Return JSON structured data with requested/needed information about the new player
echo '{"responseType":"ok", "id": "' . $dbReturnId . '"}';
}
} catch (Exception $e) {
$dbReturnId = -1;
$errorMsg = 'Failed to query the database. Err: ' . $e->getMessage();
handleError('' . $errorMsg);
}
} else {
handleError('No createPlayer POST param received');
}
}
Measurepoint.php¶
Dit is om de meetpunten op te slagen
// Handle request type logic
switch ($requestMethod) {
case 'PUT':
// nothing yet
break;
case 'POST':
// README: https://lornajane.net/posts/2008/accessing-incoming-put-data-from-php
$params = file_get_contents('php://input'); // Access the request PUT data
$paramsArray = json_decode($params, true);
addMeasurePoint($paramsArray);
break;
case 'GET':
handleError('Invalid HTTP request method');
break;
default:
handleError('Invalid HTTP request method');
break;
}
function addMeasurePoint($paramsPostArray)
{
$errorMsg = NULL;
$bTime = NULL;
$bMoves = NULL;
$bResult = NULL;
$bScore = NULL;
$bSpeelsessieId = NULL;
if (isset($paramsPostArray["gameTime"], $paramsPostArray["moves"], $paramsPostArray["result"], $paramsPostArray["score"],
$paramsPostArray["SpeelsessieId"])) {
$bTime = $paramsPostArray["gameTime"];
$bMoves = $paramsPostArray["moves"];
$bResult = $paramsPostArray["result"];
$bScore = $paramsPostArray["score"];
$bSpeelsessieId = $paramsPostArray["SpeelsessieId"];
// Add a new player to the database and return player data (row id, uniqueCode, aanmaakDatum)
try {
$dbConnect = new DatabaseConnection();
$statement = "
INSERT INTO Blok2_Meetpunt
(inGameTime, moves, result, score, Speelsessie_id)
VALUES
('" . $bTime . "','" . $bMoves . "','" . $bResult . "','" . $bScore . "', '" . $bSpeelsessieId . "');
";
// Execute query-statement on the Database
$bSucces = $dbConnect->executeQuery($statement);
if (!$bSucces) {
$errorMsg = $dbConnect->getConnection()->error;
handleError('Insert failed: ' . $errorMsg);
} else {
// Return JSON structured data with requested/needed information about the new player
echo '{"responseType":"ok", "Speelsessie_id": "' . $bSpeelsessieId . '"}';
}
} catch (Exception $e) {
$errorMsg = 'Failed to query the database. Err: ' . $e->getMessage();
handleError('' . $errorMsg);
}
} else {
handleError('No createPlayer POST param received');
}
}
Tile.php¶
Hier worden de type en de coördinaten opgeslagen van de tiles die de speler liet bewegen.
// Handle request type logic
switch ($requestMethod) {
case 'PUT':
// nothing yet
break;
case 'POST':
// README: https://lornajane.net/posts/2008/accessing-incoming-put-data-from-php
$params = file_get_contents('php://input'); // Access the request PUT data
$paramsArray = json_decode($params, true);
addTile($paramsArray);
break;
case 'GET':
handleError('Invalid HTTP request method');
break;
default:
handleError('Invalid HTTP request method');
break;
}
function addTile($paramsPostArray)
{
$errorMsg = NULL;
$bcoordinates = NULL;
$bSpeelsessieId = NULL;
$btype = NULL;
if (isset($paramsPostArray["coordinates"], $paramsPostArray["SessieID"], $paramsPostArray["type"])) {
$dbReturnId = -1;
$bcoordinates = $paramsPostArray["coordinates"];
$bSpeelsessieId = $paramsPostArray["SessieID"];
$btype = $paramsPostArray["type"];
// Add a new player to the database and return player data (row id, uniqueCode, aanmaakDatum)
try {
$dbConnect = new DatabaseConnection();
$statement = "
INSERT INTO Blok2_Tile
(coördinaten, Speelsessie_id, type)
VALUES
('" . $bcoordinates . "', '" . $bSpeelsessieId . "', '" . $btype . "');
";
// Execute query-statement on the Database
$bSucces = $dbConnect->executeQuery($statement);
if (!$bSucces) {
$errorMsg = $dbConnect->getConnection()->error;
handleError('Insert failed: ' . $errorMsg);
} else {
$dbReturnId = $dbConnect->getConnection()->insert_id;
// Return JSON structured data with requested/needed information about the new player
echo '{"responseType":"ok", "coordinates": "' . $bcoordinates . '"}';
}
} catch (Exception $e) {
$dbReturnId = -1;
$errorMsg = 'Failed to query the database. Err: ' . $e->getMessage();
handleError('' . $errorMsg);
}
} else {
handleError('No createPlayer POST param received');
}
}
Scoreboard.php¶
Bij loadScoreboard() worden meerdere rijen teruggegeven.
// Handle request type logic
switch ($requestMethod) {
case 'PUT':
// nothing yet
break;
case 'POST':
// README: https://lornajane.net/posts/2008/accessing-incoming-put-data-from-php
$params = file_get_contents('php://input'); // Access the request PUT data
$paramsArray = json_decode($params, true);
postScore($paramsArray);
break;
case 'GET':
loadScoreboard();
break;
default:
handleError('Invalid HTTP request method');
break;
}
function loadScoreboard()
{
if (isset($_GET['Levelnumber'])) {
$LevelID = $_GET["Levelnumber"];
$dbReturnName = NULL;
$dbHighscore = 0;
// Search for player in the database based on GET param playerName
try {
$dbConnect = new DatabaseConnection();
$LevelID = mysqli_real_escape_string($dbConnect->getConnection(), $LevelID);
$statement = "
SELECT
Speler_Username, Highscore
FROM
Blok2_Scoreboard
WHERE
Level_id = '" . $LevelID . "'
ORDER BY
Highscore DESC, Time ASC;
";
// Execute query-statement on the database
$result = $dbConnect->executeQuery($statement);
if (!$result) {
$errorMsg = $dbConnect->getConnection()->error;
handleError('' . $errorMsg);
} else {
if ($result->num_rows > 0) {
$rows = $result->fetch_all(MYSQLI_ASSOC);
$array = array();
foreach ($rows as $row) {
$dbReturnName = $row['Speler_Username'];
$dbHighscore = $row['Highscore'];
// Return JSON structured data with requested/needed information about the existing player
array_push($array, json_decode('{"responseType":"ok", "UserName":"' . $dbReturnName . '", "Highscore":' . $dbHighscore . '}'));
}
echo(json_encode($array));
} else {
// No records found, return error notifying the user about this.
handleNotFound('scores with level_id ' . $LevelID);
}
}
// Free the result set
$result->free();
} catch (Exception $e) {
$dbReturnID = -1;
$errorMsg = 'Failed to query the database. Err: ' . $e->getMessage();
handleError('' . $errorMsg);
}
} else {
handleError('No unqiue player code received via GET, or use POST with param createPlayer : true to create a new player.');
}
}
function postScore($paramsPostArray)
{
$errorMsg = NULL;
$bHighScore = NULL;
$bLevelID = NULL;
$bSpelerUsername = NULL;
$bTime = NULL;
if (
isset($paramsPostArray["highscore"], $paramsPostArray["Levelnumber"],
$paramsPostArray["SpelerUserName"], $paramsPostArray["gameTime"])
) {
$dbReturnId = -1;
$bHighScore = $paramsPostArray["highscore"];
$bLevelID = $paramsPostArray["Levelnumber"];
$bSpelerUsername = $paramsPostArray["SpelerUserName"];
$bTime = $paramsPostArray["gameTime"];
// Add a new player to the database and return player data (row id, uniqueCode, aanmaakDatum)
try {
$dbConnect = new DatabaseConnection();
$statement = "
INSERT INTO Blok2_Scoreboard
(Highscore, Level_id, Speler_Username, Time)
VALUES
('" . $bHighScore . "', '" . $bLevelID . "', '" . $bSpelerUsername . "', '" . $bTime . "');
";
// Execute query-statement on the Database
$bSucces = $dbConnect->executeQuery($statement);
if (!$bSucces) {
$errorMsg = $dbConnect->getConnection()->error;
handleError('Insert failed: ' . $errorMsg);
} else {
$dbReturnId = $dbConnect->getConnection()->insert_id;
// Return JSON structured data with requested/needed information about the new player
echo '{"responseType":"ok", "id": "' . $dbReturnId . '",
"Speler_Username": "' . $bSpelerUsername . '", "Highscore": "' . $bHighScore . '"}';
}
} catch (Exception $e) {
$dbReturnId = -1;
$errorMsg = 'Failed to query the database. Err: ' . $e->getMessage();
handleError('' . $errorMsg);
}
} else {
handleError('No createPlayer POST param received');
}
}
Scoreboard2.php¶
// Handle request type logic
switch ($requestMethod) {
case 'PUT':
// nothing yet
break;
case 'POST':
// README: https://lornajane.net/posts/2008/accessing-incoming-put-data-from-php
$params = file_get_contents('php://input'); // Access the request PUT data
$paramsArray = json_decode($params, true);
updateScore($paramsArray);
break;
case 'GET':
getScore();
break;
default:
handleError('Invalid HTTP request method');
break;
}
function getScore()
{
if (isset($_GET['Levelnumber'], $_GET['PlayerName'])) {
$LevelID = $_GET["Levelnumber"];
$dbPlayerName = $_GET["PlayerName"];
$dbHighscore = 0;
// Search for player in the database based on GET param playerName
try {
$dbConnect = new DatabaseConnection();
$LevelID = mysqli_real_escape_string($dbConnect->getConnection(), $LevelID);
$dbPlayerName = mysqli_real_escape_string($dbConnect->getConnection(), $dbPlayerName);
$statement = "
SELECT
Highscore
FROM
Blok2_Scoreboard
WHERE
Level_id = '" . $LevelID . "'
AND
Speler_Username = '" . $dbPlayerName . "'
";
// Execute query-statement on the database
$result = $dbConnect->executeQuery($statement);
if (!$result) {
$errorMsg = $dbConnect->getConnection()->error;
handleError('' . $errorMsg);
} else {
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
$dbHighscore = $row['Highscore'];
// Return JSON structured data with requested/needed information about the existing player
echo '{"responseType":"ok", "Highscore":' . $dbHighscore . '}';
} else {
// No records found, return error notifying the user about this.
handleNotFound('score with Username ' . $dbPlayerName);
}
}
// Free the result set
$result->free();
} catch (Exception $e) {
$errorMsg = 'Failed to query the database. Err: ' . $e->getMessage();
handleError('' . $errorMsg);
}
} else {
handleError('No unqiue player code received via GET, or use POST with param createPlayer : true to create a new player.');
}
}
function updateScore($paramsPostArray)
{
$errorMsg = NULL;
$bHighScore = NULL;
$bLevelID = NULL;
$bSpelerUsername = NULL;
$bTime = NULL;
if (isset($paramsPostArray["highscore"], $paramsPostArray["Levelnumber"],
$paramsPostArray["SpelerUserName"], $paramsPostArray["gameTime"])) {
$bHighScore = $paramsPostArray["highscore"];
$bLevelID = $paramsPostArray["Levelnumber"];
$bSpelerUsername = $paramsPostArray["SpelerUserName"];
$bTime = $paramsPostArray["gameTime"];
// Add a new player to the database and return player data (row id, uniqueCode, aanmaakDatum)
try {
$dbConnect = new DatabaseConnection();
$statement = "
UPDATE Blok2_Scoreboard
SET Highscore = '" . $bHighScore . "', Time = '" . $bTime . "'
WHERE
Speler_Username = '" . $bSpelerUsername . "'
AND
Level_id = '" . $bLevelID . "';
";
// Execute query-statement on the Database
$bSucces = $dbConnect->executeQuery($statement);
if (!$bSucces) {
$errorMsg = $dbConnect->getConnection()->error;
handleError('Insert failed: ' . $errorMsg);
} else {
// Return JSON structured data with requested/needed information about the new player
echo '{"responseType":"ok", "Highscore": "' . $bHighScore . '"}';
}
} catch (Exception $e) {
$dbReturnId = -1;
$errorMsg = 'Failed to query the database. Err: ' . $e->getMessage();
handleError('' . $errorMsg);
}
} else {
handleError('No createPlayer POST param received');
}
}
Javascript¶
class AnalyticsTrackerManager {
playerID;
newplayer = false;
#newscore = true;
#playerName
#sessionID
#oldscore
constructor() {
//get a unique id and store it in the localstorage.
//retrieve from localStorage if available, otherwise create a new one.
this.#initialize();
}
async #initialize() {
const playerGUID = localStorage.getItem("playerGUID");
const levelProgress = localStorage.getItem("levelProgress");
if (playerGUID === null) {
this.newplayer = true;
} else {
this.#retrievePlayerData(playerGUID);
}
if (levelProgress === null){
localStorage.setItem("levelProgress", 1);
}
}
//Dit functie haalt de data van de speler op uit de database
#retrievePlayerData(uniqueCode) {
let url = `https://oege.ie.hva.nl/~akimovm/blok2/analytics/user.php?code=${uniqueCode}`;
httpGet(url, 'json', (responce) => {
this.playerID = responce.id;
this.#playerName = responce.Username
this.newplayer = false;
})
}
//Dit functie slaat de data van de speler op in de database
storePlayerData() {
// create a p5 httppost connection
let username = document.getElementById("userName").value;
const postData = {
"createPlayer": true,
"userName": username
};
const url = `https://oege.ie.hva.nl/~akimovm/blok2/analytics/user.php`;
httpPost(url, 'json', postData, (result) => {
const playerUniqueCode = result.uniqueCode;
localStorage.setItem("playerGUID", playerUniqueCode);
localStorage.setItem("levelProgress", 1);
this.#retrievePlayerData(playerUniqueCode);
}, function (error) { throw new Error(error); });
}
//Dit functie maakt de nieuwe speelsessie aan
createSession(levelid) {
//track a custom event. Make sure to include the playerGUID.
const playerGUID = localStorage.getItem("playerGUID");
const postData = {
"PlayerCode": playerGUID,
"Levelnumber": levelid
}
const url = `https://oege.ie.hva.nl/~akimovm/blok2/analytics/session.php`;
httpPost(url, 'json', postData, (result) => {
this.#sessionID = result.id;
}, function (error) { throw new Error(error); })
}
//Dit functie slaat de meetpunten van de huidige speelsessie op
saveResults(time, moves, result, score){
const postData = {
"gameTime": time,
"moves": moves,
"result": result,
"score": score,
"SpeelsessieId": this.#sessionID
}
const url = `https://oege.ie.hva.nl/~akimovm/blok2/analytics/measurepoint.php`;
httpPost(url, 'json', postData, (result) => {
console.log(result);
}, function (error) { throw new Error(error); });
}
//Dit functie slaat de coördinaten en de type van de tiles op
saveTile(x, y, type){
const postData = {
"coordinates": "(" + x + "," + y + ")",
"SessieID": this.#sessionID,
"type": type
}
const url = `https://oege.ie.hva.nl/~akimovm/blok2/analytics/tile.php`;
httpPost(url, 'json', postData, (result) => {
console.log(result);
}, function (error) { throw new Error(error); });
}
//Dit functie slaat de highscore op
#saveHighscore(score, levelnr, time){
const postData = {
"highscore": score,
"Levelnumber": levelnr,
"SpelerUserName": this.#playerName,
"gameTime": time
}
const url = `https://oege.ie.hva.nl/~akimovm/blok2/analytics/scoreboard.php`;
httpPost(url, 'json', postData, (result) => {
console.log(result);
localStorage.setItem("levelProgress", levelnr + 1);
}, function (error) { throw new Error(error); });
}
//Dit functie haalt de highscore uit de database
#loadScore(levelnr){
let url = `https://oege.ie.hva.nl/~akimovm/blok2/analytics/scoreboard2.php?Levelnumber=${levelnr}&PlayerName=${this.#playerName}`;
httpGet(url, 'json', (responce) => {
this.#oldscore = responce.Highscore;
})
}
//Dit functie vergelijkt de oude en de nieuwe score en update de highscore als het nodig is
UpdateScore(score, levelnr, time){
this.#loadScore(levelnr);
if(this.#oldscore === null && this.#newscore == true){
this.#saveHighscore(score, levelnr, time);
this.#newscore = false;
}
else if(this.#oldscore != null && score > this.#oldscore){
const postData = {
"highscore": score,
"Levelnumber": levelnr,
"SpelerUserName": this.#playerName,
"gameTime": time
}
const url = `https://oege.ie.hva.nl/~akimovm/blok2/analytics/scoreboard2.php`;
httpPost(url, 'json', postData, (result) => {
console.log(result);
}, function (error) { throw new Error(error); });
}
}
}
class GameManager {
#analyticsTrackerManager;
#assetManager;
constructor() {
this.#analyticsTrackerManager = new AnalyticsTrackerManager();
this.#assetManager = new AssetManager();
window.gameManager = this;
}
//Hier worden de functies van de analyticsTrackerManager uitgevoerd
savePlayer(){
this.#analyticsTrackerManager.storePlayerData();
}
createSession(levelnr){
this.#analyticsTrackerManager.createSession(levelnr);
}
saveResults(time, moves, result, score){
this.#analyticsTrackerManager.saveResults(time, moves, result, score);
}
saveTile(x, y, type){
this.#analyticsTrackerManager.saveTile(x, y, type);
}
UpdateScore(score, levelnr, time){
this.#analyticsTrackerManager.UpdateScore(score, levelnr, time);
}
get newplayer(){
return this.#analyticsTrackerManager.newplayer;
}
get playerID(){
return this.#analyticsTrackerManager.playerID;
}
get data(){
return this.#analyticsTrackerManager.data;
}
}
Bronnen¶
Window localStorage property. (z.d.-b). https://www.w3schools.com/jsref/prop_win_localstorage.asp
PHP Mysqli Fetch_all() function. (z.d.). https://www.w3schools.com/php/func_mysqli_fetch_all.asp
PHP: Mysqli_result::Fetch_all - Manual. (z.d.). https://www.php.net/manual/en/mysqli-result.fetch-all.php
LeaderBoard¶
Alle highscores van de spelers worden in de Scoreboard opgeslagen en laten zien. Elk level heeft een eigen scoreboard.
function LeaderBoard() {
let scoreboard = document.createElement("ol");
scoreboard.setAttribute("type", "1");
scoreboard.setAttribute("class", "scoreboard");
//Hier wordt het aan html file gekoppeld.
if (resultappend == true) {
canvaswrap.appendChild(scoreboard);
}
let data;
//Hier wordt een lijst voor scores gemaakt
let url = `https://oege.ie.hva.nl/~akimovm/blok2/analytics/scoreboard.php?Levelnumber=${tileGrid.levelnr}`;
httpGet(url, 'json', function (responce) {
//Hier worden de scores en de bijbehorende namen naar de lijst toegevoegd.
for (let i = 0; i < responce.length; i++) {
data = responce[i];
let boardEntry = document.createElement("li");
boardEntry.innerHTML = data.UserName + ": " + data.Highscore;
scoreboard.appendChild(boardEntry);
}
})
}
/*Dit is voor de lijst met scores*/
ol {
width: 600px;
height: 600px;
border: 5px solid white;
border-radius: 20px;
overflow: auto;
margin: auto;
position: relative;
top: 600px;
left: 0;
}
/*Dit is voor wat binnen de lijst staat*/
li {
color: white;
margin-top: 10px;
margin-bottom: 10px;
margin-left: 25px;
font-family: 'MedievalSharp', cursive;
font-size: 48px;
}