/**
* Represents a collision manager that handles various collision checks and interactions within the game world.
*/
class CollisonManager {
world;
mouseX = 0;
mouseY = 0;
/**
* Creates a CollisonManager instance.
* @param {Object} world - The game world instance.
*/
constructor(world) {
this.world = world;
this.world.canvas.addEventListener(
"mousemove",
this.trackMousePosition.bind(this)
);
}
/**
* Tracks and stores the mouse position relative to the canvas.
* @param {MouseEvent} event - The mousemove event.
*/
trackMousePosition(event) {
const rect = this.world.canvas.getBoundingClientRect();
const scaleX = this.world.canvas.width / rect.width;
const scaleY = this.world.canvas.height / rect.height;
this.mouseX = (event.clientX - rect.left) * scaleX;
this.mouseY = (event.clientY - rect.top) * scaleY;
}
/**
* Checks collisions between the character and enemies.
* If the character jumps on an enemy, the enemy is hit.
* Otherwise, the character takes damage and is pushed back.
*/
checkCollision() {
this.world.level.enemies.forEach((enemy) => {
if (!enemy.enemyIsDead && this.world.character.isColliding(enemy)) {
if (this.shouldHitEnemy(enemy)) {
this.handleEnemyCollision(enemy);
} else {
this.handleCharacterCollision();
}
}
});
}
/**
* Determines if the enemy should be hit based on the character's position.
* @param {Object} enemy - The enemy object.
* @returns {boolean} True if the enemy should be hit, otherwise false.
*/
shouldHitEnemy(enemy) {
return (
this.world.character.y +
this.world.character.height -
this.world.character.offset.bottom <
enemy.y + enemy.offset.top + enemy.height / 3 && enemy.height < 400
);
}
/**
* Handles the collision when the enemy is hit.
* @param {Object} enemy - The enemy object.
*/
handleEnemyCollision(enemy) {
enemy.enemyIsDead = true;
enemy.hit();
this.world.audioManager.chickenHitSound.play();
}
/**
* Handles the collision when the character is hit.
* Applies a cooldown so that the character only takes damage once every 500ms.
*/
handleCharacterCollision() {
if (this.world.character.energy > 0) {
const now = Date.now();
if (
!this.world.character.lastHitTime ||
now - this.world.character.lastHitTime > 500
) {
this.world.audioManager.characterHitSound.play();
this.world.character.hit();
this.world.character.lastHitTime = now;
this.world.character.x -= 40;
this.world.statusBar.setPercentage(this.world.character.energy);
}
}
}
/**
* Checks for collisions between thrown bottles and enemies.
*
* This method iterates over all throwable objects (bottles) and, for each bottle,
* checks if it is colliding with any enemy in the current level. If a collision is
* detected and the enemy is not already dead:
* - The enemy is hit.
* - The "chicken hit" sound effect is played.
* - The bottle is marked as broken.
* - The throwing sound is stopped.
* Additionally, if the enemy's height equals 400, the end boss's status bar is updated
* to reflect the enemy's current energy.
*
* @function checkCollisionBottle
* @returns {void}
*/
checkCollisionBottle() {
this.world.throwableObjects.forEach((bottle) => {
this.world.level.enemies.forEach((enemy) => {
if (!enemy.enemyIsDead && bottle.isColliding(enemy)) {
enemy.hit();
this.world.audioManager.chickenHitSound.play();
bottle.bottleIsBroken = true;
this.world.audioManager.throwSound.stop();
if (enemy.height == 400) {
this.world.statusBarEndboss.setPercentage(enemy.energy);
}
}
});
});
}
/**
* Checks collisions with the endboss.
* Sets the endboss to attack mode if colliding, and checks if the endboss is dead.
*/
checkCollisionEndbos() {
let endboss = this.world.level.enemies.find(
(enemy) => enemy instanceof Endboss
);
if (endboss) {
if (this.world.character.isColliding(endboss)) {
endboss.isAttack = true;
} else {
endboss.isAttack = false;
}
if (endboss.energy <= 0) {
this.world.enbossIsDead = true;
}
if (this.world.character.x > 9590) {
endboss.itsMove = true;
}
}
}
/**
* Checks if thrown bottles are broken.
* Stops the throw sound, plays the bottle broken sound, and removes the broken bottle from the game.
*/
checkbottleIsBroken() {
this.world.throwableObjects.forEach((bottle) => {
if (bottle.bottleIsBroken) {
this.world.audioManager.throwSound.stop();
this.world.audioManager.bottleBrokenSound.play();
const index = this.world.throwableObjects.indexOf(bottle);
if (index !== -1) {
this.world.throwableObjects.splice(index, 1);
}
}
});
}
/**
* Collects bottles when the character collides with them.
* Increases the bottle count up to a maximum of 5.
*/
checkCollisionBottleCollectib() {
this.world.level.collectiblBottel.forEach((collectiblBottl) => {
if (
this.world.character.isColliding(collectiblBottl) &&
this.world.bottleCount < 5
) {
this.world.bottleCount++;
this.world.audioManager.bottleCollectSound.play();
this.world.statusBarBottle.setPercentage(this.world.bottleCount);
const index =
this.world.level.collectiblBottel.indexOf(collectiblBottl);
if (index !== -1) {
this.world.level.collectiblBottel.splice(index, 1);
}
}
});
}
/**
* Collects coins when the character collides with them.
* Increases the coin count up to a maximum of 5.
*/
checkCollisionCoinCollectib() {
this.world.level.collectiblCoin.forEach((collectiblCoin) => {
if (
this.world.character.isColliding(collectiblCoin) &&
this.world.CoinCount < 5
) {
this.world.CoinCount++;
this.world.audioManager.coinSound.play();
this.world.statusBarCoin.setPercentage(this.world.CoinCount);
const index = this.world.level.collectiblCoin.indexOf(collectiblCoin);
if (index !== -1) {
this.world.level.collectiblCoin.splice(index, 1);
}
}
});
}
/**
* Implements the purchase logic for the Salsa Store.
* If the character is colliding with the store and has enough coins,
* they can purchase a bottle (if below the maximum bottle count).
*/
checkCollisionSalsaStore() {
const now = Date.now();
if (this.world.lastPurchaseTime && now - this.world.lastPurchaseTime < 500)
return;
if (
this.world.keyboard.DOWN &&
this.world.CoinCount > 0 &&
this.world.bottleCount < 5 &&
this.world.character.isColliding(this.world.salsaStore)
) {
this.world.lastPurchaseTime = now;
this.world.bottleCount++;
this.world.statusBarBottle.setPercentage(this.world.bottleCount);
this.world.CoinCount--;
this.world.statusBarCoin.setPercentage(this.world.CoinCount);
this.world.audioManager.buySound.play();
}
}
/**
* Handles the bottle-throwing logic.
* Checks if a bottle can be thrown and prepares the throw if possible.
*/
checkThrowobjekt() {
if (this.world.canThrowBottle()) {
this.world.prepareThrow();
}
}
/**
* Checks if the mouse is hovering over any UI icons on the canvas and updates the cursor style accordingly.
*/
checkCollisionButtonToMouse() {
const icons = this.getRelevantIcons();
const isHovering = this.checkIconsHoverState(icons);
this.updateCursor(isHovering);
}
/**
* Returns an array of icons to be checked for hover state.
* @returns {Array<DrawableObject>} Icons that should respond to mouse hover.
*/
getRelevantIcons() {
return [
this.world.instructionIcon,
this.world.musicMuteIcon,
this.world.playGame,
this.world.SoundsMuteIcon,
this.world.imprint,
this.world.restartGameIcon,
];
}
/**
* Determines whether to skip checking a particular icon based on the current game state.
* @param {DrawableObject} icon - The icon to evaluate.
* @returns {boolean} True if the icon should be skipped, otherwise false.
*/
shouldSkipIcon(icon) {
if (this.world.startGame) {
return [this.world.imprint, this.world.instructionIcon].includes(icon);
}
const allowed = [
this.world.imprint,
this.world.instructionIcon,
this.world.SoundsMuteIcon,
this.world.playGame,
this.world.musicMuteIcon,
];
return !allowed.includes(icon);
}
/**
* Checks if an icon has valid x/y properties for collision detection.
* @param {DrawableObject} icon - The icon to check.
* @returns {boolean} True if the icon has valid position properties, otherwise false.
*/
hasValidPosition(icon) {
return typeof icon.x !== "undefined" && typeof icon.y !== "undefined";
}
/**
* Determines if the mouse coordinates are currently over the icon's bounds.
* @param {DrawableObject} icon - The icon to check.
* @returns {boolean} True if the mouse is over the icon, otherwise false.
*/
isMouseOverIcon(icon) {
return (
this.mouseX >= icon.x &&
this.mouseX <= icon.x + icon.width &&
this.mouseY >= icon.y &&
this.mouseY <= icon.y + icon.height
);
}
/**
* Iterates through icons to check if the mouse is hovering over any of them.
* Special handling is applied for the restartGameIcon, which only returns a hover state if:
* - this.world.character.energy <= 0
* - this.world.startGame is true
* - this.world.enbossIsDead is true
*
* @param {Array<DrawableObject>} icons - The array of icons to evaluate.
* @returns {boolean} True if the mouse is hovering over any icon, otherwise false.
*/
checkIconsHoverState(icons) {
for (const icon of icons) {
if (!icon || this.shouldSkipIcon(icon)) continue;
if (!this.hasValidPosition(icon)) continue;
if (icon === this.world.restartGameIcon) {
if (
!(
(this.world.character.energy <= 0 && this.world.startGame) ||
this.world.enbossIsDead
)
) {
continue;
}
}
if (this.isMouseOverIcon(icon)) return true;
}
return false;
}
/**
* Updates the canvas cursor style based on whether the mouse is hovering over a relevant icon.
* @param {boolean} isHovering - True if the mouse is hovering over an icon, otherwise false.
*/
updateCursor(isHovering) {
this.world.canvas.style.cursor = isHovering ? "pointer" : "default";
}
}