//Set Variables
/**
* Current game state
* @type {string} */
let gameState = "menu";
/**
* World gravity value
* @type {number} */
let mapGravity = 10;
/**
* @typedef {Object} Level
* @property {string} platforms // The tile layout of the level
* @property {string} map // The map of the level
* @property {number} level //Number of the level
*/
/**
* Array of game levels
* @type {Array.<Level>}
* @see {@link preloadLevels} for usage
* @see https://p5play.org/learn/tiles.html?page=0 for Tile documentation
* */
let levels = [];
/**
* Current game map
* @type {string} */
let currentMap = 'forest'
/**
* Current game level
* @type {number} */
let currentLevel = 0;
//Map Tiles
/**
* Group of all the tiles of the current level
* Utilizes p5play's 'Tiles' class for handling tile generation.
* @type {Tiles}
* @see https://p5play.org/learn/tiles.html?page=0 for Tile examples
* @see https://p5play.org/docs/Tiles.html For Tile class source code
* @see {@link changeLevel} for utilization
*/
let tileGroup;
/**
* Parent group for all the Sprite groups that build the level tiles
* Utilizes p5play's 'Group' class for handling collections of sprites.
* @see https://p5play.org/learn/group.html for Group examples
* @see https://p5play.org/docs/Group.html for Group class source code
* @see {@link setEnviroment} for utilization
* @type {Group}
*/
let myTiles;
/**
* A subset of `myTiles` containing only walkable tiles.
* Represents areas the player can move on.
* @type {Group}
* @see {@link myTiles} For the parent group of all tiles.
* @see {@link setEnviroment} for initialization
*/
let walkableTiles;
/**
* A subset of `myTiles` containing the spawn and level end point Groups.
* Only one Sprite of each groupe is used per level to declare its starting and end point
* @type {Group}
* @see {@link myTiles} For the parent group of all tiles.
* @see {@link setEnviroment} for initialization
*/
let spawnPoint, endPoint;
/**
* A helper group used to remove all unwanted Sprites from previous level
* @type {Group}
* @see {@link changeLevel} for utilization
*/
let allSpritesGroup;
/**
*Checks to see if player is in a movement sequence (prevents sequence cancellation)
* @type {boolean}
*/
let inSequence = false;
/**
* Sprites attached to the player, used as "sensors"
* @type {Sprite}
* @see {@link spawnLizard} for initialization
*/
let groundSensor,leftSensor,rightSensor, cameraSensor;
/**
* Sprites attached to the top of the player, used as "sensor"
* @type {Sprite}
* @see {@link spawnLizard} for initialization
* @deprecated to be removed
*/
let topSensor;
/**
* Keeps track of score (number of coins collected)
* @type {number}
* @see {@link keepScore} for usage
*/
let score = 0;
//Enviroment
/**
* Children Sprite Groups of my myTiles Group
* Each Group has multuple instances(Sprites) on each level
* @type {Group}
* @see {@link myTiles}
*/
let coins, ground, groundL, groundR, invBlock, topBlock, underGround, platform, spikes;
/**
* Animation spritesheet for the collecatble coins
* Different animations are used in each level
* @type {q5.Image}
*/
let coinsImg;
/**
* Used to indicate player direction
* Takes values of 1 or -1
* @type {number}
* @see {@link gameControlls}
*/
let direction = 0;
/**
* Timer ID for managing attack intervals.
* @type {number}
* @see {@link attack}
*/
let attackTimer;
/**
* Flag indicating when a character can attack
* @type {boolean}
* @see {@link attack}
*/
let canAttack = true;
/**
* Indicates the time between player attacks
* @type {number}
* @see {@link attack}
*/
let attackSpeed = 300//ms
/**
* Timer ID for managing damge taking intervals.
* @type {number}
* @see {@link damage}
*/
let damageTimer;
/**
* Flag indicating when a character can get damaged
* @type {boolean}
* @see {@link damage}
*/
let canDamage = true;
/**
* Stores the frame of the time of an attack
* Used to calculate when the players sprite opacity returns to normal
* @type {number}
* @see {@link damage}
* @see {@link runGame}
*/
let prevFrame = 0
/**
* Flag indicating when the player's getting chased by an enemy(prevens enemy animation overlap)
* @see {@link killEnemy}
*
*/
let chasing = false;
//Sprites and Assets
/**
* Sprite groups for the player and the enemies
* @type {Group}
* @see {@link initializeEnemies}
* @see {@link spawnLizard}
*/
let witch, lizard, frog, fly, leaf, bat, cobra, ghoul, imp, goblinKing;
/**
* Animation spritesheets caontaining the animaations for the player and enemies
* @type {q5.Image}
* @see {@link preload}
* @see https://q5js.org/learn/#loadImage for loadImage documantation
*/
let heroImg, partnerImg, witchImg, lizardImg, portalImg, hexImg, frogImg, cloudImg, flyImg, leafImg, batImg, cobraImg, ghoulImg, impImg, bossImg, bossAttackAreaImg;
/**
* Parent group for all enemy groups
* Used to acces all enemies from one place
* @type {Group}
* @see {@link initializeEnemies}
*/
let enemies;
/**
* A subset of 'myTiles' used to determin the spawn coordinates for each enemy type
* @type {Group}
* @see {@link myTiles} For the parent group of all tiles.
* @see {@link setEnviroment} for initialization
*/
let enemySpawn1, enemySpawn2
/**
* An object containing enemy groups for different types of enemies.
* Every map has its own two unique enemies
* @type {{e1: Group | undefined, e2: Group | undefined}}
* @see {@link changeLevel}
*/
let enemyGroup = {e1: undefined, e2: undefined}
/**
* Conatins the single tile image sheets for each map
* @type {q5.Image}
*/
let forestTiles, mountainTiles, castleTiles;
/**
* Sprite Group specifically for the boss
* Even if theres one boss, using a Group helps with spawning in the boss
* @type {Group}
* @see {@link initializeEnemies}
*/
let boss;
/**
* Sprite representing the area that the bosses attack takes
* Used to detect if the bosses attack hits the player
* @type {Sprite}
* @see {@link initializeBoss}
*/
let bossAttackArea;
//Sounds
/**
* Sound files for background music and sound effects
* @type {q5.Sound}
* @see {@link preload}
* @see https://q5js.org/learn/#loadSound for loadSound documantation
*/
let forestMusic, mountainMusic, entranceMusic, castleMusic, coinSound, damageSound, defeatSound, bossMusic, menuMusic, introMusic, outroMusic;
/**
* Sprite Group used for the UI (Health bar)
* @type {Group}
* @see {@link setUI}
*/
let ui;
/**
* Sprite belonging to ui group
* Represents individual hearts(player hit points)
* @type {Sprite}
* @see {@link setUI}
*/
let heart;
/**
* Image sprite sheet for the ui's hearts
* @type {q5.Image}
*/
let heartImg
/**
* Images for the main menu and Controlls page
* @type {q5.Image}
* @see {@link menu}
*/
let menuImg, controllsImg;
/**
* @typedef backgroundLayer
* @property {string} file // The file path to the background image.
* @property {q5.Image | undefined} img // The q5.Image object for the layer, initially undefined.
* @property {number} x // The x-coordinate of the background layer.
* @property {number} speed // The scrolling speed of the background layer.
*/
/**
* An array representing layers of the forest background for a parallax effect.
* Each layer contains the file path to the image, an image object (assigned later),
* its x-coordinate, and its scrolling speed.
*
* @type {Array<{backgroundLayer}>}
*/
let forestBackground = [
{
file: 'assets/Backgrounds/forest/1.png',
img: undefined,
x: 0,
speed: 0
},
{
file: 'assets/Backgrounds/forest/2.png',
img: undefined,
x: 0,
speed: 0.2
},
{
file: 'assets/Backgrounds/forest/3.png',
img: undefined,
x: 0,
speed: 0.4
},
{
file: 'assets/Backgrounds/forest/4.png',
img: undefined,
x: 0,
speed: 0.6
}
];
/**
* An array representing layers of the mountain background for a parallax effect.
* Each layer contains the file path to the image, an image object (assigned later),
* its x-coordinate, and its scrolling speed.
*
* @type {Array<{backgroundLayer}>}
*/
let mountainBackground = [
{
file: 'assets/Backgrounds/mountain/sky.png',
img: undefined,
x: 0,
speed: 0
},
{
file: 'assets/Backgrounds/mountain/city.png',
img: undefined,
x: 0,
speed: 0.2
},
{
file: 'assets/Backgrounds/mountain/mountain1.png',
img: undefined,
x: 0,
speed: 0.4
},
{
file: 'assets/Backgrounds/mountain/mountain2.png',
img: undefined,
x: 0,
speed: 0.6
},
{
file: 'assets/Backgrounds/mountain/mountain3.png',
img: undefined,
x: 0,
speed: 0.7
}
];
/**
* An array representing layers of the entance background for a parallax effect.
* Each layer contains the file path to the image, an image object (assigned later),
* its x-coordinate, and its scrolling speed.
*
* @type {Array<{backgroundLayer}>}
*/
let entranceBackground = [
{
file: 'assets/Backgrounds/entrance/1.png',
img: undefined,
x: 0,
speed: 0
},
{
file: 'assets/Backgrounds/entrance/2.png',
img: undefined,
x: 0,
speed: 0
},
{
file: 'assets/Backgrounds/entrance/3.png',
img: undefined,
x: 0,
speed: 0
},
{
file: 'assets/Backgrounds/entrance/4.png',
img: undefined,
x: 0,
speed: 0
},
{
file: 'assets/Backgrounds/entrance/5.png',
img: undefined,
x: 0,
speed: 0
}
];
/**
* An array representing layers of the castle background for a parallax effect.
* Each layer contains the file path to the image, an image object (assigned later),
* its x-coordinate, and its scrolling speed.
*
* @type {Array<{backgroundLayer}>}
*/
let castleBackground = [
{
file: 'assets/Backgrounds/castle/wall.png',
img: undefined,
x: 0,
speed: 0
}
];
/**
* An array representing layers of the boss room background for a parallax effect.
* Each layer contains the file path to the image, an image object (assigned later),
* its x-coordinate, and its scrolling speed.
*
* @type {Array<{backgroundLayer}>}
*/
let bossBackground = [
{
file: 'assets/Backgrounds/boss/sky-full.png',
img: undefined,
x: 0,
speed: 0
},
{
file: 'assets/Backgrounds/boss/mountain1-full.png',
img: undefined,
x: 0,
speed: 0.2
},
{
file: 'assets/Backgrounds/boss/mountain2-full.png',
img: undefined,
x: 0,
speed: 0.4
},
{
file: 'assets/Backgrounds/boss/foreground.png',
img: undefined,
x: 0,
speed: 0
}
];
/**
* @typedef storyScene
* @property {string} file // The file path to the background image.
* @property {q5.Image | undefined} img // The q5.Image object for the layer, initially undefined.
*/
/**
* An array representing the intro story scenes.
* Each layer contains the file path to the image and an image object (assigned later),
*
* @type {Array<{storyScene}>}
*/
let introScenes = [
{
file: 'assets/controlls.jpg',
img: undefined,
},
{
file: 'assets/intro/1.jpg',
img: undefined,
},
{
file: 'assets/intro/2.jpg',
img: undefined,
},
{
file: 'assets/intro/3.jpg',
img: undefined,
},
{
file: 'assets/intro/4.jpg',
img: undefined,
},
]
/**
* An array representing the outro story scenes.
* Each layer contains the file path to the image and an image object (assigned later),
*
* @type {Array<{storyScene}>}
*/
let outroScenes = [
{
file: 'assets/outro/1.jpg',
img: undefined,
},
{
file: 'assets/outro/2.jpg',
img: undefined,
},
{
file: 'assets/outro/3.jpg',
img: undefined,
},
{
file: 'assets/outro/4.jpg',
img: undefined,
},
{
file: 'assets/outro/5.jpg',
img: undefined,
},
]
/**
* Represents the current story scene shown in the intro and outro of the game
* @type {number}
* @see {@link intro}
* @see {@link endGame}
*/
let currentScene = 0;
/**
* Q5 function that pre-loads assets before these are needed in the setup and update/draw function
* @see https://q5js.org/learn/#preload for documentation
*/
function preload() {
for (let b of forestBackground){
b.img = loadImage(`${b.file}`)
}
for (let b of mountainBackground){
b.img = loadImage(`${b.file}`)
}
for (let b of entranceBackground){
b.img = loadImage(`${b.file}`)
}
castleBackground[0].img = loadImage(castleBackground[0].file)
for (let b of bossBackground){
b.img = loadImage(`${b.file}`)
}
for (let b of introScenes){
b.img = loadImage(`${b.file}`)
}
for (let b of outroScenes){
b.img = loadImage(`${b.file}`)
}
witchImg = loadImage('assets/boss/witch.png');
lizardImg = loadImage('assets/player/lizard.png');
frogImg = loadImage('assets/boss/frog.png');
forestTiles = loadImage('assets/Enviroment/forestTiles2.png');
mountainTiles = loadImage('assets/Enviroment/mountainTiles.png');
castleTiles = loadImage('assets/Enviroment/castleTiles.png');
coinsImg = loadImage('assets/Enviroment/coin.png');
flyImg = loadImage('assets/enemies/forest/fly.png');
leafImg = loadImage('assets/enemies/forest/leaf.png');
batImg = loadImage('assets/enemies/mountain/bat.png');
cobraImg = loadImage('assets/enemies/mountain/cobra.png');
ghoulImg = loadImage('assets/enemies/castle/ghoul.png');
impImg = loadImage('assets/enemies/castle/imp.png');
bossImg = loadImage('assets/boss/goblin.png');
bossAttackAreaImg = loadImage('assets/boss/bossAttack.png');
forestMusic = loadSound('assets/sound/forest.ogg');
mountainMusic = loadSound('assets/sound/mountain.ogg');
entranceMusic = loadSound('assets/sound/entrance.ogg');
castleMusic = loadSound('assets/sound/castle.ogg');
bossMusic = loadSound('assets/sound/boss-music.ogg');
coinSound = loadSound('assets/sound/coin.ogg');
damageSound = loadSound('assets/sound/damage.ogg');
damageSound.setVolume(.5)
defeatSound = loadSound('assets/sound/enemy-defeat.ogg');
defeatSound.setVolume(1);
menuMusic = loadSound('assets/sound/menu.ogg');
menuMusic.setVolume(.3);
introMusic = loadSound('assets/sound/intro-music.ogg');
introMusic.setVolume(.3);
outroMusic = loadSound('assets/sound/outro-music.ogg');
outroMusic.setVolume(.3);
heartImg = loadImage('assets/ui/heart.png');
menuImg = loadImage('assets/menu.jpg');
controllsImg = loadImage('assets/controlls.jpg');
initializeEnemies();
}
/**
* Q5 function that runs one time when the program starts
* Sets up the game environment, including canvas, sprites, levels, and UI elements.
* Initializes global variables, loads resources, and prepares the game for play.
*
* @function setup
* @global
* @returns {void} Does not return a value.
*
* @see {@link canvasSetup} For the canvas initialization details.
* @see {@link preloadLevels} For preloading all the level maps.
* @see {@link changeLevel} For setting up the environment groups/tiles.
* @see {@link spawnLizard} For creating the lizard(player) sprite function.
* @see {@link setUI} For initializing the user interface.
* @see {@link spawner} for determining the spanw point coordinates
* @see https://q5js.org/learn/#setup for documantaion
* @see https://p5play.org/docs/Sprite.html#pixelPerfect for pixelPerfect documanation
*/
function setup() {
allSpritesGroup = new Group();
world.gravity.y = mapGravity;
allSprites.pixelPerfect = true; //The sprite will be drawn at integer coordinates, while retaining the precise position of its collider.
canvasSetup() // sets up the canvas
preloadLevels(); //load all level maps
changeLevel(); // change level function, here it loads firt level
spawnLizard(spawner().x,spawner().y); //spawns player at spawner tile coordinates
lizard.overlaps(coins); //Player can overlap coins, has to be declared here after player and enviroment initialization
setUI(); //displays UI
}
/**
* Q5 function that runs 60 times per second by default.
* Acts as game state machine
*
* @function update
* @global
* @returns {void} Does not return a value.
* @see {@link menu} for the starting menu
* @see {@link intro} for the intro story
* @see {@link runGame} for main game functionality
* @see {@link endGame} for ending story
* @see https://developer.mozilla.org/en-US/docs/Web/API/Location/reload for location.reload documantation
* */
function update() {
clear();
if(gameState=='menu') menu();
if(gameState=='intro') intro();
if(gameState=='runGame') runGame();
if(gameState=='endGame') endGame();
if(gameState=='replay') location.reload();
}
/**
* Draws the UI after turning of the camera, making it static
* @function drawFrame
* @returns {void}
* @see https://p5play.org/learn/camera.html?page=2 for camera.off explanation
*/
function drawFrame() {
camera.off();
ui.draw();
}
/**
* Sets up the UI - Player Health
* Creates heart sprites, same ammount as the players maxHealth property
* @function setInput
* @global
* @return {void}
*/
function setUI(){
ui = new Group();
ui.isPerm = true; //Used to except UI from sprite resseting at changeLevel()
ui.overlaps(allSprites);
ui.layer = 100;
for (let i = 0; i < lizard.maxHealth; i++) {
heart = new ui.Sprite(30 + i * 40, 25, 19, 18, 'n');
heart.spriteSheet = heartImg;
heart.addAnis({
full: { row: 0, frames: 1, frameSize: [19,18]},
empty: { row: 1, frames: 1, frameSize: [19,18]}
});
heart.changeAni('full')
}
}
/**
* Displays main menu.
* The first level is loaded in setup() but its turned invisible in the main menu
* @function menu
* @global
* @returns {void}
*/
function menu(){
mouse.visible = false;
menuMusic.play();
walkableTiles.visible = false; //
enemies.visible = false; //
lizard.visible = false; //Turns loaded first level invisible
ui.visible = false; //
coins.visible = false; //
background(menuImg)
textAlign(CENTER, MIDDLE);
fill('white');
textSize(30);
text('Press "Space" to start',canvas.hw, canvas.hh+150)
if(kb.presses('space')) gameState = 'intro';
}
/**
* Displays the intro story scenes
* Starts the game when the scenes end
* @function intro
* @global
* @returns {void}
*/
function intro(){
menuMusic.pause();
introMusic.play();
mouse.visible = true;
if(currentScene<introScenes.length){
background(introScenes[currentScene].img);
if(mouse.pressed()) currentScene++;
} else{
currentScene = 0;
gameState = 'runGame';
}
}
/**
* Displays the outro story scenes
* Has option to restart the game at last scene
* @function endGame
* @global
* @returns {void}
*/
function endGame(){
allSprites.remove();
bossMusic.pause();
outroMusic.play();
mouse.visible = true;
if(currentScene<outroScenes.length){
background(outroScenes[currentScene].img);
if(mouse.pressed()) ++currentScene;
} else gameState = 'replay';
}
/**
* Executes the main game loop, managing player interactions, camera controls, level transitions,
* enemy behavior, and game state logic. This function is called continuously to keep the game running.
*
* @function runGame
* @global
* @returns {void} Does not return a value.
*
* @see {@link displayBackground} For background rendering logic.
* @see {@link gameControlls} For managing player input.
* @see {@link cameraControll} For controlling the camera.
* @see {@link enemyProximity} For enemy chase logic
* @see {@link bossAI} For handling boss-specific behavior.
* @see {@link endLevel} For transitioning to the next level.
* @see {@link damage} For applying damage to the player.
*/
function runGame(){
mouse.visible = false;
introMusic.pause();
walkableTiles.visible = true; //
enemies.visible = true; //
lizard.visible = true; // Makes first level visible again
ui.visible = true; //
coins.visible = true; //
//Background
displayBackground();
//Player Controlls
gameControlls (lizard);
//Camera Controlls
if(currentMap!='bossRoom'){
cameraControll(lizard, tileGroup, 4);
enemyProximity();
}else {
camera.x =spawner().x + 34;
camera.y =spawner().y - 48;
//Boss Fight Logic
lizard.overlaps(goblinKing)
bossAI();
if(lizard.overlaps(finaleTrigger)&&deathTrigger) endLevel();
//Final room logic
if(currentLevel==7) {
witch.mirror.x = true;
frog.mirror.x = true;
if(lizard.overlaps(frog)) gameState = 'endGame'
}
}
//Die on spikes
if(groundSensor.overlaps(spikes)) {
death();
damageSound.play();
}
//Change to next level
if(lizard.overlaps(endPoint)) endLevel();
//Collect coins and keep Score
keepScore();
//Background Music
backgroundMusic(volume = .2);
//check if player gets damaged
if(enemies.overlapping(lizard)&&canDamage) damage();
//Resets player opacity after 100 frames pass from the time player gets damaged
if (frameCount-prevFrame > 100){
lizard.opacity = 1
prevFrame = frameCount
}
//Debug Mode
gameDebug(showSprites = true);
}
/**
* Manages character controls, including movement, jumping and attacking.
* Handles input from the keyboard and controller, and updates the character's state and animation accordingly.
*
* @function gameControlls
* @param {Sprite} character - The character sprite whose controls are being managed.
* @returns {void} Does not return a value.
*
* @see {@link stuckCheck} For handling scenarios where the character is stuck.
* @see {@link changeState} For updating the character's state.
* @see {@link isOnGround} For checking if the character is on the ground.
* @see {@link attack} For handling attack actions.
*/
function gameControlls(character){
//----------Controls----------\\
if((character.currentState != character.states.ATTACK) && !inSequence){
stuckCheck();
if (kb.pressing('left')|| contro.leftStick.x < -0.25) {
character.mirror.x = true;
character.changeAni('run');
//If the character is stucks, dont let him move towards stuck area
//Prevents player sprite from shaking
if(character.currentState == character.states.STUCK){
character.vel.x = 0;
if(rightSensor.overlapping(walkableTiles)){
character.vel.x = -playerSpeed;
changeState('WALK')
}
}else{
character.vel.x = -playerSpeed;
changeState('WALK')
direction = -1;
}
}
else if ((kb.pressing('right')|| contro.leftStick.x > 0.25)){
character.mirror.x = false;
character.changeAni('run');
//If the character is stucks, dont let him move towards stuck area
//Prevents player sprite from shaking
if(character.currentState == character.states.STUCK){
character.vel.x = 0;
if(leftSensor.overlapping(walkableTiles)){
character.vel.x = playerSpeed;
changeState('WALK');
}
}else{
character.vel.x = playerSpeed;
changeState('WALK');
direction = 1;
}
}
else if (kb.released('right')||kb.released('left')){
character.changeAni('stand');
character.vel.x = 0;
changeState('IDLE')
}
if (isOnGround()){
if(kb.presses('space')||kb.presses('up')){
world.gravity.y = 15;
character.vel.y = -3.5;
world.gravity.y = 10;
}
}
}
if(kb.presses('e')){
if(canAttack) attack(character);
}
// if(kb.presses('q')){
// block();
// }
// if(kb.released('q')){
// releaseBlock();
// }
}
/**
* Resets player back to spawning point
* @function resetPlayer
* @param {boolean} resetCamera Option to reset the camera | Avoid weird camera movements during level change
* @param {boolean} resetHealth Option to reseet player health | Doesnt reset health on level change
* @returns {void}
* @see {@link spawner} For spawning coordinates
*/
function resetPlayer(resetCamera, resetHealth){
if(resetHealth){
lizard.health = lizard.maxHealth;
for (h of ui) h.changeAni('full')
}
lizard.speed = 0;
lizard.rotationSpeed = 0;
lizard.rotation = 0;
lizard.x = spawner().x;
lizard.y = spawner().y;
//Reset camera sensor if needed
if (resetCamera) {
cameraSensor.x = lizard.x;
cameraSensor.y = lizard.y;
}
}
//Debug function for testting
function gameDebug(showSprites){
if (kb.pressing('`')){
if(showSprites) allSprites.visible = true; //shows all hidden sprites
allSprites.debug = true; //shows all colliders
textSize(60)
fill('white')
text(round(frameRate()), 50, 100); //displays frames per second
}
// else if (kb.released('`')){
// allSprites.debug = false;
// textSize(0)
// }
}
/**
* Executes an attack action for the given character. Creates an invisible attack area in front of the character,
* plays the attack animation, and applies damage to enemies or the boss depending on the current level.
*
* This function uses GlueJoint to attach the attack area sprite to the player
*
* This function uses asynchronous operations to manage attack timing and resets the ability to attack after a cooldown period.
* Using await the attack animation is completed before damage calculations take place
*
* @async
* @function attack
* @param {Sprite} character - The character performing the attack.
* @returns {Promise<void>} Resolves when the attack sequence is complete.
*
* @see {@link changeState} For updating the character's state.
* @see {@link attackAreaProximity} For handling proximity-based attack effects on regular enemies.
* @see {@link damageBoss} For applying damage to the boss in level 6.
* @see https://p5play.org/docs/GlueJoint.html for GlueJoint documentation
* @see https://p5play.org/learn/animation.html?page=6 for using async/await for animation sequencing
*/
async function attack(character) {
canAttack = false;
character.vel.x = 0;
changeState('ATTACK')
let attackArea = new Sprite((character.x+(8)*direction), (character.y), 25, 20) //creates an invisible sprite in front of player
attackArea.visible = false;
attackArea.mass = 0.0;
character.overlaps(attackArea);
attackArea.overlaps(allSprites);
let area = new GlueJoint(character, attackArea); //Connects attack area to player
area.visible = false;
await character.changeAni('slash'); //plays attack animation
character.changeAni('stand'); //
changeState('IDLE')
if(!(currentLevel == 6)){ //level 6 = boss fight room
await attackAreaProximity(attackArea);
}else{
await damageBoss(attackArea);
}
attackArea.remove(); //removes attack sprite after attack ends
attackTimer = setInterval(()=>{
canAttack = true
console.log('attack')
clearInterval(attackTimer);
attackTimer = undefined;
}, attackSpeed)
}
/**
* Determines and returns the spawn coordinates for the character.
* Calculates the position based on the spawnPoint Sprite/Tile and adjusts the x and y coordinates for placement.
*
* @function spawner
* @returns {{x: number, y: number}} An object containing the x and y coordinates for the spawn point.
*
* @see {@link spawnPoint} For the reference to the spawn point from which the coordinates are derived.
*/
function spawner(){
return {x: spawnPoint[0].position.x+24, y: spawnPoint[0].position.y-5};
}
/**
* Detects if the charater is on a Sprite/Tile that allows jumping
* Prevents multiple jumps and jumping while touching the sides of walls
* @function isOnGround
* @returns {boolean}
* @see {@link groundSensor}
*/
function isOnGround() {
return groundSensor.overlapping(ground)||
groundSensor.overlapping(platform)||
groundSensor.overlapping(cornerR)||
groundSensor.overlapping(cornerL)||
groundSensor.overlapping(invBlock)
}
/**
* Plays dying animation and resets the player
* @async
* @function death
* @returns {void}
* @see {@link resetPlayer} for player reset
* @see {@link inSequence} for its usage/logic
*/
async function death() {
inSequence = true;
lizard.opacity = 1;
lizard.vel.x = 0;
await lizard.changeAni(['death','dead']);
resetPlayer(resetCamera =false, resetHealth = true);
lizard.changeAni('stand')
inSequence = false;
}
/**
* Checks if player goes through a coin
* The coin gets removed and the score updates
* Collecting coins also heals the player
* @function keepScore
* @returns {void}
*/
function keepScore() {
for (let c of coins){
if (lizard.overlaps(c)){
coinSound.play();
c.remove();
score++;
if(lizard.health<lizard.maxHealth){
ui[lizard.health].changeAni('full');
lizard.health++;
}
}
}
}
/**
* Checks if the attacking area overlaps an enemy
* checks if enemy x cord is close enought to the attack area sprite x cord
* Kills enemies hit
* @function attackAreaProximity
* @param {Sprite} area The attacking area created during an attack
* @returns {void}
* @see {@link attack} for attacking area creation
* @see {@link killEnemy} for killing enemy logic
*/
function attackAreaProximity(area) {
for (let e of enemies){
if(abs(e.x - area.x) < 18 && abs(e.y - area.y) < 25){
killEnemy(e)
}
}
}
/**
* Cripples attacked enemy, plays its death animation and removes it
* @function killEnemy
* @param {Sprite} e The specific enemy that gets hit by the attack
* @returns {void}
* @see {@link attackAreaProximity} for attack detection
*/
async function killEnemy(e) {
canDamage = false;
defeatSound.play();
await e.changeAni(['death','dead'])
chasing = false;
e.vel.x = 0;
e.vel.y = 0;
e.speed = 0;
e.remove();
canDamage = true;
}
/**
* Plays background music according to level
* Pauses previous level music and plays the current one
* @function backgroundMusic
* @param {number} volume The volume at which the background music is played
* @returns {void}
* @see {@link currentMap}
*/
function backgroundMusic(volume){
switch(currentMap){
case 'forest':
forestMusic.play();
forestMusic.setVolume(volume);
break;
case 'mountain':
forestMusic.pause();
mountainMusic.play();
mountainMusic.setVolume(volume);
break;
case 'entrance':
mountainMusic.pause();
entranceMusic.play();
entranceMusic.setVolume(volume);
break;
case 'castle':
entranceMusic.pause();
castleMusic.play();
castleMusic.setVolume(volume);
break;
case 'bossRoom':
castleMusic.pause();
bossMusic.play();
bossMusic.setVolume(volume);
break;
}
}
/**
* Handles reaching the end of current level
* The player moves to the right and the level changes
* @function endLevel
* @returns {void}
* @see {@link changeLevel}
*/
async function endLevel() {
inSequence = true;
await lizard.changeAni('run');
await lizard.move(160, 'right', 1);
changeLevel();
lizard.changeAni('stand');
inSequence = false;
}
/**
* Applies damage to the player. Reduces health, plays a damage sound,
* shakes the character, and updates the UI to reflect the change in health.
* The function also manages the cooldown for when the character can take damage again.
*
* @function damage
* @returns {void}
*
* @see {@link shake} For handling the player shake effect when the character takes damage.
* @see {@link death} For triggering the death sequence when health reaches zero.
*/
function damage() {
canDamage = false;
lizard.opacity = 0.4;
damageSound.play();
shake(lizard);
ui[lizard.health-1].changeAni('empty');
lizard.health--;
if(lizard.health==0) death();
damageTimer = setInterval(()=>{
canDamage = true
clearInterval(damageTimer);
damageTimer = undefined;
}, 2000)
}
/**
* Moves entity left and right
* @async
* @function shake
* @param {Sprite} entity Sprite that the shake is applied to
* @returns {void}
*/
async function shake(entity){
await entity.move(15, 'left', 1);
await entity.move(5, 'right', 1);
}
/**
* Spawns enemies at predefined spawn points. This function creates enemy sprites for the specified enemies
* and places them at the positions defined in `enemySpawn1` and `enemySpawn2`.
*
* @function spawnEnemies
* @param {Object} enemy1 - Spawner tile for the first enemy type
* @param {Object} enemy2 - Spawner tile for the first enemy type
* @returns {void}
*
* @see {@link enemySpawn1} For the first set of spawn points where enemy1 is placed.
* @see {@link enemySpawn2} For the second set of spawn points where enemy2 is placed.
*/
function spawnEnemies(enemy1, enemy2){
for(e1 of enemySpawn1){
e = new enemy1.Sprite(e1.position.x, e1.position.y)
}
for(e2 of enemySpawn2){
e = new enemy2.Sprite(e2.position.x, e2.position.y)
}
}