/**
* Initializes various enemy groups and assigns their properties, animations, and behaviors.
* Each enemy group is configured with specific attributes such as size, movement type, sprite sheet,
* animations, and other properties like speed, friction, and scale. The function also sets their initial
* animations to 'stand'.
*
* This function creates multiple types of enemies with different behaviors and characteristics. The enemies
* include flying enemies (like `fly` and `bat`), ground-based enemies (like `leaf`, `cobra`, `imp`, and `ghoul`),
* as well as special enemies like `boss`, `witch`, and `frog`. Each enemy type is configured with a sprite sheet,
* animations (stand, move, attack, death, etc.), speed, friction, and other relevant properties. The function
* ensures each enemy is set to its idle 'stand' animation upon initialization.
*
* @function initializeEnemies
* @returns {void}
*
* @example
* //Enemy Properties explanation
*
* enemy = new enemies.Group(); (Subgroup of enemies Group)
* enemy.w = number; '(enemy width)'
* enemy.h = number; '(enemy height)'
* enemy.layer = number; '(Determines the layer of a Sprite in case of overlapping)'
* enemy.flying = false; '(attribute determining if an enemy can fly)'
* enemy.rotationLock = 'true'; '(Prevents sprite from spinning)'
* enemy.spriteSheet = 'img'; '(Sets animation spritesheet to group)'
* enemy.mass = 'number'; '(Sprites mass, is affected by gravity)'
* enemy.speed = 'number; '(Enemy speed, used in chasing)'
* enemy.friction = 'number; '(Sprite friction with other sprites)'
* enemy.anis.w= 'number; '(Widht of animation frame in spritesheet)'
* enemy.anis.h= 'number; '(Height of animation frame in spritesheet)'
* enemy.scale = 'number; '(Sprite size scalling)'
* enemy.anis.offset.x = 'number; '(x cord offset of the animation frame to the center of the sprite)'
* enemy.anis.offset.y = 'number; '(y cord offset of the animation frame to the center of the sprite)'
* enemy.addAnis({ '(Determines which part of the spritesheet is corresponds to each animation)'
* stand: { row: 0, frames: 4, frameDelay: 10 },
* move: { row: 1, frames: 8, frameDelay: 10 },
* attack: { row: 2, frames: 6, frameDelay: 13 },
* death: { row: 4, frames: 6, frameDelay: 10},
* dead: { row: 20, frames: 0}
* });
*
*/
function initializeEnemies(){
enemies = new Group();
fly = new enemies.Group();
fly.w = 8;
fly.h = 8;
fly.collider = 'none'
fly.flying = true;
fly.layer = 5;
fly.rotationLock = 'true';
fly.spriteSheet = flyImg;
fly.speed = 0.01;
fly.friction = 0;
fly.anis.w = 32;
fly.anis.h = 32;
fly.scale = 1.5;
fly.addAnis({
stand: { row: 0, frames: 4, frameDelay: 10 },
move: { row: 0, frames: 4, frameDelay: 10 },
attack: { row: 2, frames: 4, frameDelay: 13 },
death: { row: 3, frames: 6, frameDelay: 10},
dead: { row: 5, frames: 0}
});
for(f of fly) f.changeAni('stand');
leaf = new enemies.Group();
leaf.w = 5;
leaf.h = 5;
leaf.layer = 5;
leaf.flying = false;
leaf.rotationLock = 'true';
leaf.spriteSheet = leafImg;
leaf.mass = 100;
leaf.speed = 0.01;
leaf.friction = 0;
leaf.anis.w= 32;
leaf.anis.h= 32;
leaf.scale = 1.5;
leaf.anis.offset.x = 1;
leaf.anis.offset.y = -4.5;
leaf.addAnis({
stand: { row: 0, frames: 5, frameDelay: 10 },
move: { row: 1, frames: 5, frameDelay: 10 },
attack: { row: 5, frames: 6, frameDelay: 13 },
death: { row: 4, frames: 7, frameDelay: 10},
dead: { row: 20, frames: 0}
});
for(l of leaf) l.changeAni('stand');
bat = new enemies.Group();
bat.w = 5;
bat.h = 5;
bat.layer = 5;
bat.collider = 'none'
bat.flying = true;
bat.rotationLock = 'true';
bat.spriteSheet = batImg;
bat.speed = 0.01;
bat.friction = 0;
bat.anis.w= 16;
bat.anis.h= 16;
bat.scale = 2;
bat.anis.offset.x = 1;
bat.anis.offset.y = -4.5;
bat.addAnis({
stand: { row: 0, frames: 5, frameDelay: 10 },
move: { row: 1, frames: 5, frameDelay: 10 },
attack: { row: 5, frames: 6, frameDelay: 13 },
death: { row: 2, frames: 5, frameDelay: 10},
dead: { row: 20, frames: 0}
});
for(b of bat) b.changeAni('stand');
cobra = new enemies.Group();
cobra.w = 9;
cobra.h = 7;
cobra.layer = 5;
cobra.flying = false;
cobra.rotationLock = 'true';
cobra.spriteSheet = cobraImg;
cobra.mass = 100;
cobra.speed = 0.01;
cobra.friction = 0;
cobra.anis.w= 32;
cobra.anis.h= 16;
cobra.scale = 2;
cobra.anis.offset.x = 2;
cobra.anis.offset.y = 0;
cobra.addAnis({
stand: { row: 0, frames: 8, frameDelay: 10 },
move: { row: 1, frames: 8, frameDelay: 10 },
attack: { row: 3, frames: 8, frameDelay: 13 },
death: { row: 2, frames: 8, frameDelay: 10},
dead: { row: 20, frames: 0}
});
for(c of cobra) c.changeAni('stand');
ghoul = new enemies.Group();
ghoul.w = 6;
ghoul.h = 10;
ghoul.layer = 5;
ghoul.flying = false;
ghoul.rotationLock = 'true';
ghoul.spriteSheet = ghoulImg;
ghoul.mass = 100;
ghoul.speed = 0.015;
ghoul.friction = 0;
ghoul.anis.w= 32;
ghoul.anis.h= 32;
ghoul.scale = 1.7;
ghoul.anis.offset.x = 0;
ghoul.anis.offset.y = -2.4;
ghoul.addAnis({
stand: { row: 0, frames: 4, frameDelay: 10 },
move: { row: 1, frames: 8, frameDelay: 10 },
attack: { row: 2, frames: 6, frameDelay: 13 },
death: { row: 4, frames: 6, frameDelay: 10},
dead: { row: 20, frames: 0}
});
for(g of ghoul) g.changeAni('stand');
imp = new enemies.Group();
imp.w = 6;
imp.h = 6;
imp.layer = 5;
imp.flying = false;
imp.rotationLock = 'true';
imp.spriteSheet = impImg;
imp.mass = 100;
imp.speed = 0.01;
imp.friction = 0;
imp.anis.w= 32;
imp.anis.h= 32;
imp.scale = 2;
imp.anis.offset.x = 0;
imp.anis.offset.y = -4.5;
imp.addAnis({
stand: { row: 0, frames: 7, frameDelay: 10 },
move: { row: 1, frames: 8, frameDelay: 10 },
attack: { row: 2, frames: 6, frameDelay: 13 },
death: { row: 4, frames: 6, frameDelay: 10},
dead: { row: 20, frames: 0}
});
for(i of imp) i.changeAni('stand');
boss = new Group();
boss.w = 15;
boss.h = 20;
boss.rotationLock = 'false';
boss.spriteSheet = bossImg;
boss.friction = 0;
boss.speed = 0.01;
boss.health = 4;
boss.anis.w = 64;
boss.anis.h = 64;
boss.scale = 2;
boss.anis.offset.x = 0;
boss.anis.offset.y = -5.5;
boss.addAnis({
stand: { row: 0, frames: 3, frameDelay: 10 },
move: { row: 1, frames: 5, frameDelay: 10 },
damage: { row: 6, frames: 4, frameDelay: 10 },
bounceU: {row: 9, frames: 5, frameDelay: 10},
bounceD: {row: 10, frames: 6, frameDelay: 10},
death: { row: 7, frames: 10, frameDelay: 10},
dead: { row: 4, frames: 1}
})
for(b of boss) b.changeAni('stand');
witch = new enemies.Group();
witch.w = 6;
witch.h = 10;
witch.scale = 2;
witch.layer = 5;
witch.mirror.x = true;
witch.rotationLock = 'true';
witch.spriteSheet = witchImg;
witch.anis.offset.x = 0;
witch.anis.offset.y = -2;
witch.anis.frameDelay = 6;
witch.friction = 0;
witch.anis.w=32;
witch.anis.h=32;
witch.addAnis({
stand: { row: 0, frames: 4, frameDelay: 10},
run: { row: 1, frames: 8 },
hex: { row: 2, frames: 8, frameDelay: 10},
death: { row: 4, frames: 10, frameDelay: 10},
dead: { row: 20, frames: 0, frameDelay: 10},
fly: { row: 5, frames: 4, frameDelay: 10},
hit: { row: 3, frames: 4, frameDelay: 10}
});
for (w of witch) w.changeAni('stand');
frog = new enemies.Group();
frog.w = 5;
frog.h = 5;
frog.scale = 2;
frog.layer = 5;
frog.mirror.x = true;
frog.rotationLock = true;
frog.friction = 0;
frog.spriteSheet = frogImg;
frog.anis.offset.x = 0;
frog.anis.offset.y = -5;
frog.anis.frameDelay = 10;
frog.anis.w=32;
frog.anis.h=32;
frog.addAnis({
stand: { row: 0, frames: 4 },
move: { row: 1, frames: 6, frameDelay:5 },
jump: { row: 2, frames: 5, frameDelay: 10 },
death: { row: 0, frames: 1, frameDelay: 10},
dead: { row: 20, frames: 0, frameDelay: 10},
});
for(f of frog) f.changeAni('stand');
}
/**
* Enemy Y coordinate at begging of chase
* @type {number}
* @see {@link enemyProximity} for use
*/
let enemyCurrentY
/**
* Constantly checks if an enemy is near the player and updates enemy behavior accordingly.
* If the enemy is within a specified proximity of the player, it will start chasing the player.
* It also handles enemy orientation, movement, and interactions with environmental elements like spikes and platforms.
*
* This function iterates over all the enemies in the map and checks if any of them are near the player.
* The proximity is determined by a distance check (100 units in both the X and Y axes).
* If an enemy is within this range, the following actions occur:
* - Enemy orientation is adjusted based on the player's position.
* - If the enemy is not already chasing, it begins chasing the player, and its Y-coordinate is tracked.
* - If the enemy's Y-coordinate changes significantly (i.e., falls to a lower level), it applies a downward force.
* - Flying enemies can move freely toward the player, while walking enemies are limited to moving along the X-axis.
* - Enemies also interact with environmental hazards (e.g., spikes) by dying upon overlap.
*
* @function enemyProximity
* @returns {void}
*
*
*
*/
function enemyProximity() {
for(e of enemies){
if(e.overlaps(spikes)) killEnemy(e); //enemies die on spikes
if ((abs(lizard.x - e.x))<100 && (abs(lizard.y - e.y))<100){ //Checks distance
if((lizard.x - e.x)<0){ //fixes enemy orientation
e.mirror.x = true;
e.dir = 1;
}else{
e.mirror.x = false;
e.dir = -1;
}
if(!chasing){ //At the moment of chasing grabs enemy Y and changes to movement animation
enemyCurrentY = e.y
e.changeAni('move');
chasing = true;
}
if(((e.y - enemyCurrentY) > 0.5 || (e.y - enemyCurrentY) < -0.5)&& !e.flying ) { //Updates Y cord in case enemy falls to lower level
enemyCurrentY = e.y;
e.applyForceScaled(0, 50) //Pushes enemy down
} else e.mass = 0 //Pushes enemy down
if(e.flying){ //Flying enemies move freely
e.moveTowards(lizard, e.groups[2].speed)
}else{ //Walking enemies move only on the X axis
e.moveTowards(lizard.x - (20*e.dir),enemyCurrentY, e.groups[2].speed)
}
}
}
}
/**
* Initializes the boss character and its associated attack area.
*
* This function creates an instance of the boss (Goblin King) at the boss spawner location
* and sets up its attack area. The boss attack area is an invisible sprite that overlaps
* with both the boss and the player (lizard) to detect attacks. The function configures the
* boss's visual properties, its attack area, and the animation states for the attack area.
*
* @function initializeBoss
* @returns {void} Does not return a value.
*
*
*/
function initializeBoss(){
for(sp of bossSpawner){
goblinKing = new boss.Sprite(sp.position.x, sp.position.y)
goblinKing.mirror.x = true;
bossAttackArea = new Sprite(goblinKing.x-50,goblinKing.y,75,4);
bossAttackArea.scale = 2;
bossAttackArea.overlaps(goblinKing);
bossAttackArea.overlaps(lizard);
bossAttackArea.visible = false;
bossAttackArea.canDmg = false;
bossAttackArea.spriteSheet = bossAttackAreaImg;
bossAttackArea.anis.w = 64;
bossAttackArea.anis.h = 64;
bossAttackArea.anis.offset.x = -5;
bossAttackArea.addAnis({
active: {row: 0, frames: 12, frameDelay: 5},
inactive: {row:10, frames:0}
})
bossAttackArea.changeAni('inactive')
}
}
/**
* Sprite spawned when the boss is defeated
* representing the defeated boss body
* @type {Sprite}
*/
let bossBody;
/**
* Indicator of the boss dying
* @type {boolean}
*/
let deathTrigger = false;
/**
* Handles the behavior and logic of the boss character during gameplay.
*
* This asynchronous function manages the boss's actions and state transitions. If the boss's health is greater than zero,
* it performs its attacks. When the boss's health reaches zero, the function triggers its death sequence, including removing
* its attack area, transitioning its animations, and handling its removal from the game.
*
*
* @async
* @function bossAI
* @returns {Promise<void>} Resolves when the boss's death sequence is complete.
*
*
*/
async function bossAI(){
if(goblinKing.health>0){
bossAttack();
}else if(!deathTrigger){
deathTrigger = true;
bossAttackArea.remove();
bossBody = new boss.Sprite(goblinKing.x, goblinKing.y)
bossBody.overlaps(goblinKing);
bossBody.overlaps(lizard);
goblinKing.remove();
await bossBody.changeAni(['death','dead']);
}
}
/**
* Timer ID for managing boss attack intervals.
* @type {number}
* @see {@link bossAttack}
*/
let bossAttackAreaTimer;
/**
* Flag indicating when the boss can attack
* @type {boolean}
* @see {@link bossAttack}
*/
let canBossAttack = true;
/**
* Stores the frame of the time of a boss attack
* Used to calculate when the boss can attack again
* @type {number}
* @see {@link bossAttack}
*/
let bossPrevFrame;
/**
* Executes the boss's attack behavior, including animation transitions and damage logic.
*
* - **Attack Triggering**:
* - The attack can only occur if `canBossAttack` is `true`.
* - Sets a cooldown for the boss's attack by toggling `canBossAttack` and storing the frame count in `bossPrevFrame`.
* - **Animation Sequence**:
* - Plays an upward and downward bounce animation for the boss (`bounceU` and `bounceD`).
* - Activates the attack area by making it visible, enabling damage, and switching to the 'active' animation state.
* - After the attack, transitions the boss back to its 'stand' animation state.
* - **Damage Logic**:
* - If the attack area overlaps with the player (`lizard`), the `damage` function is triggered.
* - **Attack Reset**:
* - After a specific number of frames, the attack area is deactivated by hiding it, disabling damage, and changing its animation state to 'inactive'.
* - Once the cooldown period is reached (350 frames), the boss is ready to attack again by setting `canBossAttack` to `true`.
*
*
* @async
* @function bossAttack
* @returns {Promise<void>} Resolves when the attack sequence is complete.
*
*/
async function bossAttack(){
if(canBossAttack){
canBossAttack = false
bossPrevFrame = frameCount;
await goblinKing.changeAni(['bounceU','bounceD']);
bossAttackArea.canDmg = true;
bossAttackArea.visible = true;
bossAttackArea.changeAni('active');
await goblinKing.changeAni('stand');
if(abs(bossPrevFrame-frameCount)>80){
bossAttackArea.visible = false;
bossAttackArea.canDmg = false;
bossAttackArea.changeAni('inactive')
}
if (bossAttackArea.overlapping(lizard)) damage();
}
if(abs(bossPrevFrame-frameCount)>350){
canBossAttack = true
}
}
/**
* Handles applying damage to the boss when certain conditions are met.
*
* Checks if the boss can damage the player and then if the player is within range. If so, it reduces the boss's health,
* plays a sound effect, and transitions the boss through its damage animation sequence.
*
* @async
* @function damageBoss
* @param {Sprite} area - The sprite or representing the bosses attack area.
* @returns {Promise<void>} Resolves after the boss's damage animation sequence is complete.
*
*/
async function damageBoss(area){
if(!bossAttackArea.canDmg){
if(abs(goblinKing.x - area.x) < 30 && abs(goblinKing.y - area.y) < 30){
canDamage = false;
defeatSound.play();
await goblinKing.changeAni(['damage','stand'])
goblinKing.health--
canDamage = true;
}
}
}