| Server IP : 167.235.67.158 / Your IP : 216.73.216.95 Web Server : Apache System : Linux ubuntu-8gb-nbg1-1 6.8.0-111-generic #111-Ubuntu SMP PREEMPT_DYNAMIC Sat Apr 11 23:16:02 UTC 2026 x86_64 User : upstairsbar.co.uk ( 982) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/bc.the-word.com/bowling/ |
Upload File : |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bowling Game</title>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&display=swap" rel="stylesheet" />
<style>
html,
body {
height: 100%;
overflow: hidden;
position: fixed;
width: 100%;
color: #fff;
margin: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
background: #000;
font-family: "Fredoka", sans-serif;
font-weight: 400;
touch-action: none;
}
canvas {
position: relative;
width: 100%;
height: 100%;
max-width: 100vw;
max-height: 100vh;
background: #000;
overflow: hidden;
aspect-ratio: 5 / 8;
width: min(100vw, calc(100vh * 5 / 8));
height: min(100vh, calc(100vw * 8 / 5));
}
#preloader {
position: absolute;
top: 0;
left: 50%;
width: min(100vw, calc(100vh * 5 / 8));
height: min(100vh, calc(100vw * 8 / 5));
z-index: 10;
transform: translateX(-50%);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: opacity 0.5s ease-out;
}
#preloader.fade-out {
opacity: 0;
pointer-events: none;
}
.logo {
font-size: 48px;
font-weight: 600;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
margin-bottom: 40px;
animation: logoGlow 2s ease-in-out infinite alternate;
}
@keyframes logoGlow {
from {
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
to {
text-shadow: 2px 2px 20px rgba(255, 255, 255, 0.5), 2px 2px 4px rgba(0, 0, 0, 0.5);
}
}
.loading-bar {
width: 200px;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
overflow: hidden;
margin-bottom: 20px;
}
.loading-progress {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #4CAF50, #8BC34A);
border-radius: 2px;
transition: width 0.3s ease;
}
.loading-text {
color: rgba(255, 255, 255, 0.8);
font-size: 16px;
font-weight: 400;
}
#homeScreen {
position: absolute;
top: 0;
left: 50%;
width: min(100vw, calc(100vh * 5 / 8));
height: min(100vh, calc(100vw * 8 / 5));
z-index: 10;
transform: translateX(-50%);
background: linear-gradient(135deg, #4CAF50 0%, #2E7D32 100%);
transition: opacity 0.5s ease-out;
}
#homeScreen.fade-out {
opacity: 0;
pointer-events: none;
}
.screen-content {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.game-title {
font-size: 42px;
font-weight: 600;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
margin-bottom: 20px;
animation: titleBounce 1.5s ease-in-out infinite alternate;
}
.game-description {
font-size: 18px;
color: rgba(255, 255, 255, 0.9);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
cursor: pointer;
padding: 10px 20px;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 25px;
transition: all 0.3s ease;
}
.game-description:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.8);
}
@keyframes titleBounce {
from {
transform: translateY(0px);
}
to {
transform: translateY(-5px);
}
}
</style>
</head>
<body>
<div id="preloader">
<div class="logo">A New Game</div>
<div class="loading-bar">
<div class="loading-progress" id="loadingProgress"></div>
</div>
<div class="loading-text" id="loadingText">Loading...</div>
</div>
<div id="homeScreen" style="display: none;">
<div class="screen-content">
<div class="game-title">🌿 Wall-nut Bowling</div>
<div class="game-description">Click to Start</div>
</div>
</div>
<script type="module">
import * as Phaser from "https://cdn.jsdelivr.net/npm/phaser@3.90.0/dist/phaser.esm.min.js";
class Preloader extends Phaser.Scene {
constructor() {
super({ key: "Preloader" });
}
preload() {
const progressBar = document.getElementById('loadingProgress');
const progressText = document.getElementById('loadingText');
this.load.on('progress', (value) => {
const percentage = Math.floor(value * 100);
progressBar.style.width = percentage + '%';
progressText.textContent = `Loading... ${percentage}%`;
});
this.load.on('complete', () => {
progressText.textContent = 'Ready!';
setTimeout(() => {
const preloader = document.getElementById('preloader');
preloader.classList.add('fade-out');
setTimeout(() => {
preloader.style.display = 'none';
const homeScreen = document.getElementById('homeScreen');
homeScreen.style.display = 'block';
console.log('[HOME SCREEN] Home screen now visible - click to start game');
}, 500);
}, 500);
});
// Example asset loading - you can add your bowling assets here
// this.load.image('ball', 'assets/bowling-ball.png');
// this.load.image('pin', 'assets/bowling-pin.png');
// this.load.image('lane', 'assets/bowling-lane.png');
// Load background image
this.load.image('background', 'background.jpg');
// Load scroll image for falling cards
this.load.image('scroll', 'scroll.png');
// Load shield image for armored enemies
this.load.image('shield', 'shield.png');
// Load character sprite sheets with atlas
this.load.atlas('summoner', 'wizards-15.png', 'frames.json');
this.load.atlas('armored', 'wizards-283.png', 'frames.json');
this.load.atlas('basic', 'wizards-989.png', 'frames.json');
this.load.atlas('summoned', 'wizards-965.png', 'frames.json');
this.load.atlas('defender', 'wizards-242.png', 'frames.json');
this.load.atlas('jeff', 'wizards-366.png', 'frames.json');
// For now, create placeholder graphics
this.load.on('complete', () => {
// Complete callback already handled above
});
// Simulate loading time
this.load.image('placeholder', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==');
}
}
// Layer Depth Constants
const LAYERS = {
BACKGROUND: 0,
TILES: 10,
GRAPHICS: 20,
ENEMIES: 30,
PROJECTILES: 40,
CARDS: 45,
GUI: 50
};
// Base Enemy Class - Holds core mechanics
class Enemy extends Phaser.GameObjects.Container {
constructor(scene, x, y) {
super(scene, x, y);
this.scene = scene;
this.maxHealth = this.getMaxHealth();
this.health = this.maxHealth;
this.hitRadius = this.getHitRadius();
this.movementSpeed = this.getMovementSpeed();
this.isPaused = false;
this.pauseTimer = 0;
this.createVisuals();
this.setDepth(LAYERS.ENEMIES);
scene.add.existing(this);
}
// Abstract methods - to be overridden by subclasses
getMaxHealth() { return 1; }
getHitRadius() { return 20; }
getMovementSpeed() { return 1; }
createVisuals() { }
onMove(deltaTime) { }
onSummon() { }
// Core mechanics - shared by all enemies
takeDamage(damage = 1) {
this.health -= damage;
this.onDamageTaken(damage);
// Pause movement for 1 second when taking damage
this.isPaused = true;
this.pauseTimer = 1000; // 1000ms = 1 second
if (this.health <= 0) {
this.onDestroy();
return true;
}
return false;
}
onDamageTaken(damage) {
// Flash red when taking damage
this.flashRed();
// Override in subclasses for specific damage reactions
}
onDestroy() {
console.log(`[ENEMY] ${this.constructor.name} destroyed at position (${Math.round(this.x)}, ${Math.round(this.y)})`);
// Flash red and fade out on death
this.flashRed(() => {
this.fadeOut(() => {
console.log(`[ENEMY] ${this.constructor.name} fade complete, returning to pool`);
this.scene.returnEnemyToPool(this);
});
});
// Override in subclasses for specific death effects
}
flashRed(callback) {
// Flash red effect for damage
// Apply tint to all contained objects
this.each((child) => {
if (child.setTint) {
child.setTint(0xff0000);
}
});
this.scene.tweens.add({
targets: this,
duration: 200,
onComplete: () => {
// Clear tint from all contained objects
this.each((child) => {
if (child.clearTint) {
child.clearTint();
}
});
if (callback) callback();
}
});
}
fadeOut(callback) {
// Fade out effect for death
this.scene.tweens.add({
targets: this,
alpha: 0,
duration: 500,
ease: 'Power2.easeOut',
onComplete: () => {
this.setAlpha(1); // Reset for reuse
if (callback) callback();
}
});
}
update(deltaTime) {
// Handle pause timer
if (this.isPaused) {
this.pauseTimer -= deltaTime;
if (this.pauseTimer <= 0) {
this.isPaused = false;
this.pauseTimer = 0;
}
return; // Don't move while paused
}
this.onMove(deltaTime);
}
reset() {
this.health = this.maxHealth;
this.setAlpha(1);
this.setVisible(true);
this.setActive(true);
this.isPaused = false;
this.pauseTimer = 0;
this.createVisuals();
}
}
// Basic Enemy - Red triangle with 1 health
class BasicEnemy extends Enemy {
constructor(scene, x, y) {
super(scene, x, y);
this.isPaused = false;
this.pauseTimer = 0;
}
getMaxHealth() { return 1; }
getHitRadius() { return 15; }
getMovementSpeed() { return 1; }
createVisuals() {
this.removeAll(true);
// Create sprite for basic enemy
this.sprite = this.scene.add.sprite(0, 0, 'basic', '0');
this.sprite.setScale(1.75); // Double the enemy size
this.add(this.sprite);
// Play idle animation initially
this.sprite.play('basic-idle');
}
onMove(deltaTime) {
// Basic movement - move down slowly
const movement = this.movementSpeed * deltaTime * 0.02;
this.y += movement;
// Play walking animation if moving
if (this.sprite && !this.sprite.anims.isPlaying || this.sprite.anims.currentAnim.key !== 'basic-walk-down') {
this.sprite.play('basic-walk-down');
}
// Check if enemy has reached the bottom boundary
const bottomBoundary = this.scene.GRID_START_Y + (this.scene.GRID_ROWS * this.scene.TILE_SIZE);
if (this.y > bottomBoundary) {
// Stop at boundary for now
this.y = bottomBoundary;
}
}
}
// SummonedEnemy - Created by summoners, similar to basic but with different appearance
class SummonedEnemy extends BasicEnemy {
constructor(scene, x, y) {
super(scene, x, y);
}
createVisuals() {
this.removeAll(true);
// Create sprite for summoned enemy
this.sprite = this.scene.add.sprite(0, 0, 'summoned', '0');
this.sprite.setScale(1.75); // Double the enemy size
this.add(this.sprite);
// Play idle animation initially
this.sprite.play('summoned-idle');
// Add small glow to indicate it was summoned
const summonGlow = this.scene.add.circle(0, 0, 20, 0xffff44, 0.2);
this.add(summonGlow);
this.sendToBack(summonGlow);
// Fade out the glow over time
this.scene.tweens.add({
targets: summonGlow,
alpha: 0,
duration: 1000,
onComplete: () => {
summonGlow.destroy();
}
});
}
onMove(deltaTime) {
// Same movement as basic enemy
const movement = this.movementSpeed * deltaTime * 0.02;
this.y += movement;
// Play walking animation if moving
if (this.sprite && (!this.sprite.anims.isPlaying || this.sprite.anims.currentAnim.key !== 'summoned-walk-down')) {
this.sprite.play('summoned-walk-down');
}
// Check if enemy has reached the bottom boundary
const bottomBoundary = this.scene.GRID_START_Y + (this.scene.GRID_ROWS * this.scene.TILE_SIZE);
if (this.y > bottomBoundary) {
// Stop at boundary for now
this.y = bottomBoundary;
}
}
}
// Armored Enemy - Gray triangle with red triangle on top (2 health)
class ArmoredEnemy extends Enemy {
constructor(scene, x, y) {
super(scene, x, y);
}
getMaxHealth() { return 2; }
getHitRadius() { return 18; }
getMovementSpeed() { return 0.8; }
createVisuals() {
this.removeAll(true);
// Create sprite for armored enemy
this.sprite = this.scene.add.sprite(0, 0, 'armored', '0');
this.sprite.setScale(1.75); // Double the enemy size
this.add(this.sprite);
// Play idle animation initially
this.sprite.play('armored-idle');
// Add visual indicator for armor health
if (this.health === 2) {
// Full armor - add shield image on right side, 30px below character bottom
const shield = this.scene.add.image(8, 15, 'shield');
shield.setScale(0.75); // Adjust size as needed
this.add(shield);
}
}
onDamageTaken(damage) {
// Recreate visuals when armor is lost
this.createVisuals();
}
onMove(deltaTime) {
// Slower movement due to armor
const movement = this.movementSpeed * deltaTime * 0.02;
this.y += movement;
// Play walking animation if moving
if (this.sprite && !this.sprite.anims.isPlaying || this.sprite.anims.currentAnim.key !== 'armored-walk-down') {
this.sprite.play('armored-walk-down');
}
// Check if enemy has reached the bottom boundary
const bottomBoundary = this.scene.GRID_START_Y + (this.scene.GRID_ROWS * this.scene.TILE_SIZE);
if (this.y > bottomBoundary) {
// Stop at boundary for now
this.y = bottomBoundary;
}
}
}
// Summoner Enemy - Purple circle that spawns other enemies
class SummonerEnemy extends Enemy {
constructor(scene, x, y) {
super(scene, x, y);
this.summonCooldown = 0;
this.maxSummonCooldown = 5000; // Will be randomized on reset
this.summoning = false;
this.summonPauseTimer = 0;
this.summonPhase = 'none'; // 'pre', 'summoning', 'post', 'none'
this.glowEffect = null;
this.currentGridRow = 0;
this.currentGridCol = 0;
this.summonsUsed = 0;
this.maxSummons = 1;
}
getMaxHealth() { return 1; }
getHitRadius() { return 20; }
getMovementSpeed() { return 0.5; }
createVisuals() {
this.removeAll(true);
// Create sprite for summoner enemy
this.sprite = this.scene.add.sprite(0, 0, 'summoner', '0');
this.sprite.setScale(1.75); // Double the enemy size
this.add(this.sprite);
// Play idle animation initially
this.sprite.play('summoner-idle');
// Central glowing orb that gently pulses
this.centralOrb = this.scene.add.circle(-12, 15, 7, 0xffff44, 0.7);
this.centralOrb.setStrokeStyle(2, 0xffff00, 0.9);
this.add(this.centralOrb);
// Gentle pulsing animation for the central orb
this.scene.tweens.add({
targets: this.centralOrb,
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.9,
duration: 1000,
yoyo: true,
repeat: -1,
ease: 'Sine.easeInOut'
});
// Add summoning sphere effect if summoning
if (this.summonPhase === 'summoning') {
this.summoningSphere = this.scene.add.circle(0, 0, 5, 0xffffff, 0.8);
this.summoningSphere.setStrokeStyle(3, 0xffff00, 1.0);
this.add(this.summoningSphere);
// Expanding sphere of light animation
this.scene.tweens.add({
targets: this.summoningSphere,
scaleX: 10,
scaleY: 10,
alpha: 0,
duration: 800,
ease: 'Power2.easeOut',
onComplete: () => {
if (this.summoningSphere) {
this.summoningSphere.destroy();
this.centralOrb.destroy();
this.summoningSphere = null;
this.centralOrb = null;
}
}
});
}
}
onMove(deltaTime) {
// Handle summoning phases
if (this.summonPhase !== 'none') {
this.summonPauseTimer -= deltaTime;
// Play idle animation when paused for summoning
if (this.sprite && (!this.sprite.anims.isPlaying || this.sprite.anims.currentAnim.key !== 'summoner-idle')) {
this.sprite.play('summoner-idle');
}
if (this.summonPhase === 'pre' && this.summonPauseTimer <= 0) {
// Start summoning phase
this.summonPhase = 'summoning';
this.createVisuals(); // Add glow effect
this.performSummon();
this.summonPauseTimer = 500; // 0.5 second post-summon pause
} else if (this.summonPhase === 'summoning' && this.summonPauseTimer <= 0) {
// End summoning phase
this.summonPhase = 'none';
this.createVisuals(); // Remove glow effect
this.summonCooldown = this.maxSummonCooldown;
}
// Don't move while summoning
return;
}
// Normal movement + summoning logic
const movement = this.movementSpeed * deltaTime * 0.02;
this.y += movement;
// Play walking animation when moving
if (this.sprite && (!this.sprite.anims.isPlaying || this.sprite.anims.currentAnim.key !== 'summoner-walk-down')) {
this.sprite.play('summoner-walk-down');
}
// Check if enemy has reached the bottom boundary
const bottomBoundary = this.scene.GRID_START_Y + (this.scene.GRID_ROWS * this.scene.TILE_SIZE);
if (this.y > bottomBoundary) {
// Stop at boundary for now
this.y = bottomBoundary;
}
// Update current grid position for summoning
this.updateGridPosition();
this.summonCooldown -= deltaTime;
if (this.summonCooldown <= 0) {
this.startSummon();
}
}
updateGridPosition() {
// Use the scene's getTile helper function for consistency
const tileInfo = this.scene.getTile(this.x, this.y);
this.currentGridCol = tileInfo.col;
this.currentGridRow = tileInfo.row;
}
isOnTileCenter() {
// Check if the summoner is close enough to the center of a tile
const gridPos = this.scene.grid[this.currentGridRow][this.currentGridCol];
const distanceToCenter = Phaser.Math.Distance.Between(
this.x, this.y,
gridPos.centerX, gridPos.centerY
);
// Allow a tolerance of 5 pixels from perfect center
return distanceToCenter <= 5;
}
startSummon() {
// Check summon limit
if (this.summonsUsed >= this.maxSummons) {
console.log(`Summoner has used all ${this.maxSummons} summons - no more summoning`);
this.summonCooldown = Number.MAX_SAFE_INTEGER; // No more summoning
return;
}
// Only summon in first 4 rows
if (this.currentGridRow >= 4) {
this.summonCooldown = this.maxSummonCooldown;
return;
}
// Only summon when centered on a tile
if (!this.isOnTileCenter()) {
console.log(`Summoner not centered on tile - delaying summon`);
this.summonCooldown = 500; // Check again in 0.5 seconds
return;
}
const summonChance = Math.random();
console.log(`Summon chance: ${summonChance}`);
console.log(`Current grid row: ${this.currentGridRow}`);
if (this.currentGridRow < 3 && summonChance > 0.25) {
console.log(`dont summon`);
this.summonCooldown = this.maxSummonCooldown;
return;
}
console.log(`Summoner at (${this.currentGridCol}, ${this.currentGridRow}) starting summon sequence! (${this.summonsUsed + 1}/${this.maxSummons})`);
this.summonPhase = 'pre';
this.summonPauseTimer = 500; // 0.5 second pre-summon pause
}
performSummon() {
// Increment summons used counter
this.summonsUsed++;
// Spawn BasicEnemies in adjacent tiles (up, down, left, right)
const spawnOffsets = [
{ col: 0, row: -1 }, // up
{ col: 0, row: 1 }, // down
{ col: -1, row: 0 }, // left
{ col: 1, row: 0 } // right
];
let spawnedCount = 0;
spawnOffsets.forEach((offset, index) => {
const targetCol = this.currentGridCol + offset.col;
const targetRow = this.currentGridRow + offset.row;
const direction = ['up', 'down', 'left', 'right'][index];
console.log(`Attempting to spawn ${direction} at (${targetCol}, ${targetRow}) from summoner at (${this.currentGridCol}, ${this.currentGridRow})`);
// Check bounds
if (targetCol >= 0 && targetCol < this.scene.GRID_COLS &&
targetRow >= 0 && targetRow < this.scene.GRID_ROWS) {
const gridPos = this.scene.grid[targetRow][targetCol];
//if (!gridPos.occupied) {
// Spawn a summoned enemy with fade-in and pause
const newEnemy = this.scene.getEnemyFromPool('summoned');
if (newEnemy) {
newEnemy.setPosition(gridPos.centerX, gridPos.centerY);
newEnemy.setAlpha(0); // Start invisible
newEnemy.isPaused = true;
newEnemy.pauseTimer = 500; // 0.5 second pause
gridPos.occupied = true;
spawnedCount++;
// Fade in animation
this.scene.tweens.add({
targets: newEnemy,
alpha: 1,
duration: 300,
ease: 'Power2.easeOut'
});
console.log(`✅ Spawned SummonedEnemy ${direction} at (${targetCol}, ${targetRow})`);
} else {
console.log(`❌ Failed to get enemy from pool for spawn at (${targetCol}, ${targetRow})`);
}
/*} else {
console.log(`❌ Cannot spawn ${direction} at (${targetCol}, ${targetRow}) - tile occupied`);
}*/
} else {
console.log(`❌ Cannot spawn ${direction} at (${targetCol}, ${targetRow}) - out of bounds`);
}
});
console.log(`Summoner completed summon ${this.summonsUsed}/${this.maxSummons} - spawned ${spawnedCount} enemies`);
}
reset() {
super.reset();
// Reset summon counter
this.summonsUsed = 0;
// Randomize summon cooldown based on current row (first 4 tiles)
this.updateGridPosition();
const baseTime = 3000; // 3 seconds base
const rowMultiplier = Math.max(1, this.currentGridRow + 1); // Row 0 = 1x, Row 1 = 2x, etc.
const randomFactor = 0.5 + Math.random(); // 0.5x to 1.5x
this.maxSummonCooldown = baseTime * rowMultiplier * randomFactor;
this.summonCooldown = this.maxSummonCooldown;
this.summonPhase = 'none';
this.summonPauseTimer = 0;
console.log(`Summoner cooldown set to ${Math.round(this.maxSummonCooldown)}ms for row ${this.currentGridRow} (0/${this.maxSummons} summons used)`);
}
}
// Base Attack Class - Handles drag and drop behavior
class Attack extends Phaser.GameObjects.Container {
constructor(scene, x, y, attackType) {
super(scene, x, y);
this.scene = scene;
this.attackType = attackType;
this.isDragging = false;
this.originalX = x;
this.originalY = y;
this.targetColumn = -1;
this.hitPositions = []; // Track positions where this attack has already caused damage
this.createVisuals();
this.setupInteractivity();
this.setDepth(LAYERS.GUI);
scene.add.existing(this);
}
// Abstract methods - to be overridden by subclasses
createVisuals() { }
getAttackName() { return 'Unknown'; }
executeAttack(column) { }
setupInteractivity() {
this.setSize(60, 60);
this.setInteractive({ draggable: true });
this.on('dragstart', (pointer) => {
this.isDragging = true;
this.setDepth(LAYERS.GUI + 10);
console.log(`Started dragging ${this.getAttackName()}`);
});
this.on('drag', (pointer, dragX, dragY) => {
this.setPosition(dragX, dragY);
this.checkColumnHighlight(dragX, dragY);
});
this.on('dragend', (pointer) => {
this.isDragging = false;
this.setDepth(LAYERS.GUI);
if (this.targetColumn >= 0) {
console.log(`Dropping ${this.getAttackName()} on column ${this.targetColumn}`);
// Jeff plays attack animation
if (this.scene.jeff) {
this.scene.jeff.playAttackAnimation();
}
this.executeAttack(this.targetColumn);
this.resetPosition();
} else {
console.log(`${this.getAttackName()} not dropped on valid column`);
this.resetPosition();
}
this.scene.clearColumnHighlight();
this.targetColumn = -1;
});
}
checkColumnHighlight(x, y) {
// Check if dragging over the grid area
if (y >= this.scene.GRID_START_Y && y <= this.scene.GRID_START_Y + (this.scene.GRID_ROWS * this.scene.TILE_SIZE)) {
if (x >= this.scene.GRID_START_X && x <= this.scene.GRID_START_X + (this.scene.GRID_COLS * this.scene.TILE_SIZE)) {
// Calculate which column we're over
const column = Math.floor((x - this.scene.GRID_START_X) / this.scene.TILE_SIZE);
if (column >= 0 && column < this.scene.GRID_COLS) {
if (this.targetColumn !== column) {
this.targetColumn = column;
this.scene.highlightColumn(column);
}
return;
}
}
}
// Not over valid grid area
if (this.targetColumn >= 0) {
this.scene.clearColumnHighlight();
this.targetColumn = -1;
}
}
resetPosition() {
this.setPosition(this.originalX, this.originalY);
}
}
// Bullet Attack - Flows down column destroying all enemies
class BulletAttack extends Attack {
constructor(scene, x, y) {
super(scene, x, y, 'bullet');
}
getAttackName() { return 'Bullet'; }
createVisuals() {
this.removeAll(true);
// Background circle
const bg = this.scene.add.circle(0, 0, 25, 0x444444);
bg.setStrokeStyle(3, 0x000000);
this.add(bg);
// Bullet visual - vertical line with arrow
const line = this.scene.add.rectangle(0, 0, 4, 30, 0x00ff00);
this.add(line);
const arrowHead = this.scene.add.polygon(0, -18, [0, -8, -6, 4, 6, 4], 0x00ff00);
this.add(arrowHead);
}
executeAttack(column) {
console.log(`Executing bullet attack on column ${column}`);
// Get bullet from pool
const bullet = this.scene.getProjectileFromPool('bullet');
const startX = this.scene.GRID_START_X + (column * this.scene.TILE_SIZE) + (this.scene.TILE_SIZE / 2);
const startY = this.scene.GRID_START_Y + (this.scene.GRID_ROWS * this.scene.TILE_SIZE);
bullet.setPosition(startX, startY);
// Store bullet properties
bullet.column = column;
bullet.hitEnemies = []; // Reset hit enemies list
bullet.hitPositions = []; // Reset hit positions list
// Move bullet up the column
const bulletTween = this.scene.tweens.add({
targets: bullet,
y: this.scene.GRID_START_Y - 50,
duration: 2000,
ease: 'Linear',
onComplete: () => {
this.scene.returnProjectileToPool(bullet);
}
});
bullet.moveTween = bulletTween;
// Add to active projectiles list
this.scene.activeProjectiles = this.scene.activeProjectiles || [];
this.scene.activeProjectiles.push(bullet);
console.log(`Created bullet projectile`);
}
}
// Standard Attack - Bowling ball that bounces diagonally
class StandardAttack extends Attack {
constructor(scene, x, y) {
super(scene, x, y, 'standard');
}
getAttackName() { return 'Standard'; }
createVisuals() {
this.removeAll(true);
// Background circle
const bg = this.scene.add.circle(0, 0, 25, 0x444444);
bg.setStrokeStyle(3, 0x000000);
this.add(bg);
// Bowling ball visual
const ball = this.scene.add.circle(0, 0, 15, 0x4444ff);
ball.setStrokeStyle(2, 0x0000aa);
this.add(ball);
// Bounce arrows
const arrow1 = this.scene.add.polygon(-8, -8, [0, -6, -4, 2, 4, 2], 0xffaa00);
arrow1.setRotation(-Math.PI / 4);
this.add(arrow1);
const arrow2 = this.scene.add.polygon(8, -8, [0, -6, -4, 2, 4, 2], 0xffaa00);
arrow2.setRotation(Math.PI / 4);
this.add(arrow2);
}
executeAttack(column) {
console.log(`Executing standard bowling attack on column ${column}`);
// Get bowling ball from pool
const ball = this.scene.getProjectileFromPool('bowling');
const startX = this.scene.GRID_START_X + (column * this.scene.TILE_SIZE) + (this.scene.TILE_SIZE / 2);
const startY = this.scene.GRID_START_Y + (this.scene.GRID_ROWS * this.scene.TILE_SIZE);
ball.setPosition(startX, startY);
// Reset ball properties
ball.direction = 0; // Start straight up
ball.damage = 1;
ball.hitEnemies = []; // Reset hit enemies list
ball.hitPositions = []; // Reset hit positions list
// Add to active projectiles list
this.scene.activeProjectiles = this.scene.activeProjectiles || [];
this.scene.activeProjectiles.push(ball);
// Start movement
this.scene.moveBowlingBall(ball, column);
}
}
// Bomb Attack - Explodes on contact with radius damage
class BombAttack extends Attack {
constructor(scene, x, y) {
super(scene, x, y, 'bomb');
}
getAttackName() { return 'Bomb'; }
createVisuals() {
this.removeAll(true);
// Background circle
const bg = this.scene.add.circle(0, 0, 25, 0x444444);
bg.setStrokeStyle(3, 0x000000);
this.add(bg);
// Bomb visual
const bomb = this.scene.add.circle(0, 2, 12, 0xff4444);
bomb.setStrokeStyle(2, 0xaa0000);
this.add(bomb);
// Fuse
const fuse = this.scene.add.rectangle(-2, -8, 2, 8, 0x8B4513);
this.add(fuse);
// Spark
const spark = this.scene.add.circle(-2, -12, 2, 0xffff00);
this.add(spark);
// Explosion radius indicator
const radiusCircle = this.scene.add.circle(0, 0, 20, 0xff4444, 0.2);
radiusCircle.setStrokeStyle(1, 0xff4444, 0.5);
this.add(radiusCircle);
}
executeAttack(column) {
console.log(`Executing bomb attack on column ${column}`);
// Get bomb from pool
const bomb = this.scene.getProjectileFromPool('bomb');
const startX = this.scene.GRID_START_X + (column * this.scene.TILE_SIZE) + (this.scene.TILE_SIZE / 2);
const startY = this.scene.GRID_START_Y + (this.scene.GRID_ROWS * this.scene.TILE_SIZE);
bomb.setPosition(startX, startY);
// Reset bomb properties
bomb.hasExploded = false;
// Move bomb up the column
const bombTween = this.scene.tweens.add({
targets: bomb,
y: this.scene.GRID_START_Y - 50,
duration: 2500,
ease: 'Linear',
onComplete: () => {
if (bomb.active && !bomb.hasExploded) {
this.scene.returnProjectileToPool(bomb);
}
}
});
// Store tween reference for stopping on collision
bomb.moveTween = bombTween;
// Add to active projectiles list
this.scene.activeProjectiles = this.scene.activeProjectiles || [];
this.scene.activeProjectiles.push(bomb);
}
}
// Jeff - The player character
class Jeff extends Phaser.GameObjects.Container {
constructor(scene, x, y) {
super(scene, x, y);
this.scene = scene;
// Create sprite facing upward
this.sprite = this.scene.add.sprite(0, 0, 'jeff', '8'); // Frame 8 for upward facing
this.sprite.setScale(1.75); // Same scale as enemies
this.add(this.sprite);
// Start with idle animation
this.sprite.play('jeff-idle');
this.setDepth(LAYERS.ENEMIES + 1); // Above enemies
scene.add.existing(this);
this.targetColumn = 2; // Start in center column (0-indexed)
}
// Teleport to target column position
moveToColumn(column) {
if (column === this.targetColumn) return;
this.targetColumn = column;
const targetX = this.scene.GRID_START_X + (column * this.scene.TILE_SIZE) + (this.scene.TILE_SIZE / 2);
// Instant teleport
this.setX(targetX);
}
// Play attack animation
playAttackAnimation() {
this.sprite.play('jeff-attack');
// Return to idle after a brief moment
this.scene.time.delayedCall(200, () => {
this.sprite.play('jeff-idle');
});
}
}
class Game extends Phaser.Scene {
constructor() {
super({ key: "Game" });
this.GRID_COLS = 5;
this.GRID_ROWS = 8;
this.GRID_START_X = 68;
this.GRID_START_Y = 50;
this.TILE_SIZE = 72;//(window.game.config.width - (this.GRID_START_X * 2)) / this.GRID_COLS;
}
createEnemyAnimations() {
// Create animations for each enemy type
const enemyTypes = ['basic', 'armored', 'summoner', 'summoned'];
enemyTypes.forEach(type => {
// Idle animation (standing still, facing forward) - frame 0
this.anims.create({
key: `${type}-idle`,
frames: this.anims.generateFrameNames(type, {
start: 0,
end: 0
}),
frameRate: 1,
repeat: 0
});
// Walking down animation - frames 0-3 (first row of sprite sheet)
this.anims.create({
key: `${type}-walk-down`,
frames: this.anims.generateFrameNames(type, {
start: 0,
end: 3
}),
frameRate: 8,
repeat: -1
});
});
// Create Jeff animations (upward-facing)
// Jeff idle (facing up) - frame 8 (up direction)
this.anims.create({
key: 'jeff-idle',
frames: this.anims.generateFrameNames('jeff', {
start: 8,
end: 8
}),
frameRate: 1,
repeat: 0
});
// Jeff attack animation (walking up first frame) - frame 9
this.anims.create({
key: 'jeff-attack',
frames: this.anims.generateFrameNames('jeff', {
start: 9,
end: 9
}),
frameRate: 1,
repeat: 0
});
console.log('Enemy and Jeff animations created');
}
create() {
this.gameStarted = false;
// Create background image
const background = this.add.image(0, 0, 'background');
background.setOrigin(0, 0);
background.setDepth(LAYERS.BACKGROUND);
background.setDisplaySize(500, 1600);
// Create enemy animations
this.createEnemyAnimations();
this.initializePhysicsWorld();
this.createEnemyPools();
this.createProjectilePools();
this.createGrid();
this.createJeff();
this.createFPSCounter();
this.createAttacks();
this.createFallingCardSystem();
this.setupInput();
// Debug timer for periodic enemy position output
this.debugTimer = 0;
// Initialize wave system instead of test enemies
this.initializeWaveSystem();
// Auto-start game after a delay if home screen was already dismissed
this.time.delayedCall(3000, () => {
if (!this.gameStarted) {
console.log('[GAME STATE] Auto-starting game after 3 second delay');
this.startGame();
}
});
}
initializePhysicsWorld() {
// Initialize projectiles array
this.activeProjectiles = [];
}
createEnemyPools() {
this.enemyPools = {
basic: [],
armored: [],
summoner: [],
summoned: []
};
this.activeEnemies = [];
// Pre-create enemies for pooling - increased pool size
const poolSize = 20;
// Create BasicEnemy pool
for (let i = 0; i < poolSize; i++) {
const enemy = new BasicEnemy(this, 0, 0);
enemy.setVisible(false);
enemy.setActive(false);
this.enemyPools.basic.push(enemy);
}
// Create ArmoredEnemy pool
for (let i = 0; i < poolSize; i++) {
const enemy = new ArmoredEnemy(this, 0, 0);
enemy.setVisible(false);
enemy.setActive(false);
this.enemyPools.armored.push(enemy);
}
// Create SummonerEnemy pool
for (let i = 0; i < poolSize; i++) {
const enemy = new SummonerEnemy(this, 0, 0);
enemy.setVisible(false);
enemy.setActive(false);
this.enemyPools.summoner.push(enemy);
}
// Create SummonedEnemy pool
for (let i = 0; i < poolSize * 2; i++) { // Larger pool since summoners create many
const enemy = new SummonedEnemy(this, 0, 0);
enemy.setVisible(false);
enemy.setActive(false);
this.enemyPools.summoned.push(enemy);
}
}
createProjectilePools() {
this.projectilePools = {
bullet: [],
bowling: [],
bomb: []
};
// Pre-create projectiles for pooling
const poolSize = 20;
// Create bullet pool
for (let i = 0; i < poolSize; i++) {
const bullet = this.createBulletProjectile(0, 0);
bullet.setVisible(false);
bullet.setActive(false);
this.projectilePools.bullet.push(bullet);
}
// Create bowling ball pool
for (let i = 0; i < poolSize; i++) {
const ball = this.createBowlingProjectile(0, 0);
ball.setVisible(false);
ball.setActive(false);
this.projectilePools.bowling.push(ball);
}
// Create bomb pool
for (let i = 0; i < poolSize; i++) {
const bomb = this.createBombProjectile(0, 0);
bomb.setVisible(false);
bomb.setActive(false);
this.projectilePools.bomb.push(bomb);
}
}
createBulletProjectile(x, y) {
const bullet = this.add.container(x, y);
bullet.setDepth(LAYERS.PROJECTILES);
// Bullet visual
const line = this.add.rectangle(0, 0, 3, 20, 0x00ff00);
const arrow = this.add.polygon(0, -12, [0, -6, -4, 2, 4, 2], 0x00ff00);
bullet.add([line, arrow]);
// Store bullet properties
bullet.attackType = 'bullet';
bullet.damage = 999;
bullet.hitEnemies = []; // Track which enemies this bullet has already hit
bullet.hitPositions = []; // Track tile positions where damage has been dealt
return bullet;
}
createBowlingProjectile(x, y) {
const ball = this.add.container(x, y);
ball.setDepth(LAYERS.PROJECTILES);
// Ball visual
const ballCircle = this.add.circle(0, 0, 12, 0x4444ff);
ballCircle.setStrokeStyle(2, 0x0000aa);
ball.add(ballCircle);
// Store ball properties
ball.attackType = 'bowling';
ball.direction = 0; // 0 = straight down, -45 = left diagonal, 45 = right diagonal
ball.damage = 1;
ball.hitEnemies = []; // Track which enemies this ball has already hit
ball.hitPositions = []; // Track tile positions where damage has been dealt
return ball;
}
createBombProjectile(x, y) {
const bomb = this.add.container(x, y);
bomb.setDepth(LAYERS.PROJECTILES);
// Bomb visual
const bombCircle = this.add.circle(0, 2, 10, 0xff4444);
bombCircle.setStrokeStyle(2, 0xaa0000);
const fuse = this.add.rectangle(-2, -6, 2, 6, 0x8B4513);
const spark = this.add.circle(-2, -9, 2, 0xffff00);
bomb.add([bombCircle, fuse, spark]);
// Store bomb properties
bomb.attackType = 'bomb';
bomb.hasExploded = false;
return bomb;
}
getProjectileFromPool(type) {
const pool = this.projectilePools[type];
if (pool.length > 0) {
const projectile = pool.pop();
projectile.setVisible(true);
projectile.setActive(true);
return projectile;
}
// Create new projectile if pool is empty
switch (type) {
case 'bullet': return this.createBulletProjectile(0, 0);
case 'bowling': return this.createBowlingProjectile(0, 0);
case 'bomb': return this.createBombProjectile(0, 0);
default: return this.createBulletProjectile(0, 0);
}
}
returnProjectileToPool(projectile) {
projectile.setVisible(false);
projectile.setActive(false);
projectile.setPosition(0, 0);
// Reset projectile properties
if (projectile.attackType === 'bowling') {
projectile.direction = 0;
projectile.hitEnemies = []; // Reset hit enemies list
projectile.hitPositions = []; // Reset hit positions list
} else if (projectile.attackType === 'bullet') {
projectile.hitEnemies = []; // Reset hit enemies list
projectile.hitPositions = []; // Reset hit positions list
} else if (projectile.attackType === 'bomb') {
projectile.hasExploded = false;
}
// Remove from active projectiles
const index = this.activeProjectiles.indexOf(projectile);
if (index > -1) {
this.activeProjectiles.splice(index, 1);
}
// Return to appropriate pool
this.projectilePools[projectile.attackType].push(projectile);
}
getEnemyFromPool(type) {
const pool = this.enemyPools[type];
// Find an available enemy in the pool
let availableEnemy = null;
for (let i = 0; i < pool.length; i++) {
const enemy = pool[i];
if (!enemy.active && !enemy.visible) {
availableEnemy = enemy;
// Remove from pool
pool.splice(i, 1);
break;
}
}
// If no available enemy found, create a new one
if (!availableEnemy) {
console.log(`[POOL] No available ${type} enemy in pool, creating new one`);
switch (type) {
case 'basic': availableEnemy = new BasicEnemy(this, 0, 0); break;
case 'armored': availableEnemy = new ArmoredEnemy(this, 0, 0); break;
case 'summoner': availableEnemy = new SummonerEnemy(this, 0, 0); break;
default: availableEnemy = new BasicEnemy(this, 0, 0); break;
}
}
// Reset and activate the enemy
availableEnemy.reset();
availableEnemy.setVisible(true);
availableEnemy.setActive(true);
this.activeEnemies.push(availableEnemy);
return availableEnemy;
}
returnEnemyToPool(enemy) {
// Make sure enemy is properly reset
enemy.setVisible(false);
enemy.setActive(false);
enemy.setPosition(-1000, -1000); // Move far off screen
enemy.setAlpha(1); // Reset alpha
// Clear any ongoing tweens on this enemy
this.tweens.killTweensOf(enemy);
// Remove from active enemies
const index = this.activeEnemies.indexOf(enemy);
if (index > -1) {
this.activeEnemies.splice(index, 1);
}
// Decrement wave enemy counter
if (this.enemiesAliveInWave > 0) {
this.enemiesAliveInWave--;
console.log(`[WAVE] Enemy defeated. Enemies remaining in wave: ${this.enemiesAliveInWave}`);
// Check if wave is complete
if (this.enemiesAliveInWave <= 0 && this.enemiesSpawnedInWave > 0) {
console.log(`[WAVE] All enemies defeated! Completing wave...`);
this.onWaveComplete();
}
} else {
console.log(`[WAVE WARNING] Enemy died but enemiesAliveInWave was already ${this.enemiesAliveInWave}`);
}
// Return to appropriate pool - check SummonedEnemy before BasicEnemy since it extends it
if (enemy instanceof SummonedEnemy) {
this.enemyPools.summoned.push(enemy);
} else if (enemy instanceof BasicEnemy) {
this.enemyPools.basic.push(enemy);
} else if (enemy instanceof ArmoredEnemy) {
this.enemyPools.armored.push(enemy);
} else if (enemy instanceof SummonerEnemy) {
this.enemyPools.summoner.push(enemy);
}
}
createJeff() {
// Position Jeff 40px below the bottom of the lanes, in the center column (col 2)
const jeffX = this.GRID_START_X + (2 * this.TILE_SIZE) + (this.TILE_SIZE / 2); // Center column
const jeffY = this.GRID_START_Y + (this.GRID_ROWS * this.TILE_SIZE) + 40; // 40px below lanes
this.jeff = new Jeff(this, jeffX, jeffY);
console.log(`Jeff created at position (${jeffX}, ${jeffY})`);
}
createGrid() {
this.grid = [];
this.gridGraphics = this.add.graphics();
this.columnHighlight = null; // For column highlighting during drag
for (let row = 0; row < this.GRID_ROWS; row++) {
this.grid[row] = [];
for (let col = 0; col < this.GRID_COLS; col++) {
const x = this.GRID_START_X + col * this.TILE_SIZE;
const y = this.GRID_START_Y + row * this.TILE_SIZE;
// Varying shades of green for grass tiles
const greenShades = [0x4CAF50, 0x66BB6A, 0x43A047, 0x388E3C, 0x2E7D32];
const grassColor = greenShades[Phaser.Math.Between(0, greenShades.length - 1)];
const tile = this.add.rectangle(x + this.TILE_SIZE / 2, y + this.TILE_SIZE / 2,
this.TILE_SIZE, this.TILE_SIZE, grassColor);
tile.setDepth(LAYERS.TILES);
tile.setAlpha(0);
this.grid[row][col] = {
tile: tile,
x: x,
y: y,
centerX: x + this.TILE_SIZE / 2,
centerY: y + this.TILE_SIZE / 2,
occupied: false
};
}
}
// Create walls on left and right sides
this.createWalls();
}
createWalls() {
// Wall width and positions
const wallWidth = 10;
const wallHeight = this.GRID_ROWS * this.TILE_SIZE;
const wallY = this.GRID_START_Y + wallHeight / 2;
// Left wall
this.leftWall = this.add.rectangle(
this.GRID_START_X - wallWidth / 2,
wallY,
wallWidth,
wallHeight,
0xFF0000
);
this.leftWall.setDepth(LAYERS.GRAPHICS + 2);
// Right wall
this.rightWall = this.add.rectangle(
this.GRID_START_X + (this.GRID_COLS * this.TILE_SIZE) + wallWidth / 2,
wallY,
wallWidth,
wallHeight,
0xFF0000
);
this.rightWall.setDepth(LAYERS.GRAPHICS + 2);
this.leftWall.setAlpha(0);
this.rightWall.setAlpha(0);
console.log('Created left and right walls with red visualization');
}
createFPSCounter() {
this.fpsText = this.add.text(10, 10, 'FPS: 0', {
fontSize: '16px',
fontFamily: 'Arial',
color: '#ffffff',
backgroundColor: 'rgba(0,0,0,0.5)',
padding: { x: 8, y: 4 }
});
this.fpsText.setDepth(LAYERS.GUI);
}
// Helper function to convert world coordinates to tile coordinates
getTile(x, y) {
const col = Math.floor((x - this.GRID_START_X) / this.TILE_SIZE);
const row = Math.floor((y - this.GRID_START_Y) / this.TILE_SIZE);
// Clamp to valid grid bounds
const clampedCol = Math.max(0, Math.min(this.GRID_COLS - 1, col));
const clampedRow = Math.max(0, Math.min(this.GRID_ROWS - 1, row));
return {
col: clampedCol,
row: clampedRow,
// Also include raw unclamped values for debugging
rawCol: col,
rawRow: row
};
}
spawnEnemyAtColumn(type, column, row = 0) {
// Validate column is within bounds
if (column < 0 || column >= this.GRID_COLS) {
console.warn(`Invalid column ${column}, must be between 0 and ${this.GRID_COLS - 1}`);
return null;
}
// Validate row is within bounds
if (row < 0 || row >= this.GRID_ROWS) {
console.warn(`Invalid row ${row}, must be between 0 and ${this.GRID_ROWS - 1}`);
return null;
}
const enemy = this.getEnemyFromPool(type);
const gridPos = this.grid[row][column];
// Center enemy on the tile's x-axis, place at tile's center y
enemy.setPosition(gridPos.centerX, gridPos.centerY);
gridPos.occupied = true;
return enemy;
}
createFallingCardSystem() {
// Initialize falling card system
this.fallingCards = [];
this.cardSpawnTimer = 0;
this.cardSpawnMinTime = 2000; // 5 seconds minimum
this.cardSpawnMaxTime = 4000; // 10 seconds maximum
this.nextCardSpawnTime = Phaser.Math.Between(this.cardSpawnMinTime, this.cardSpawnMaxTime);
console.log('Falling card system initialized');
}
spawnFallingCard() {
// Randomly select an attack type
const attackTypes = ['bullet', 'standard', 'bomb'];
const randomType = attackTypes[Phaser.Math.Between(0, attackTypes.length - 1)];
// Random spawn position within visible area (10% to 50% of screen height)
const spawnX = Phaser.Math.Between(this.GRID_START_X + 30, this.GRID_START_X + (this.GRID_COLS * this.TILE_SIZE) - 30);
const minY = this.scale.height * 0.1; // 10% from top (e.g., 80px for 800px height)
const maxY = this.scale.height * 0.5; // 50% from top (e.g., 400px for 800px height)
const spawnY = Phaser.Math.Between(Math.floor(minY), Math.floor(maxY));
console.log(`[CARD SPAWN] Creating ${randomType} card at position (${spawnX}, ${spawnY}) - Screen height: ${this.scale.height}, Range: ${Math.floor(minY)}-${Math.floor(maxY)}`);
// Create card container
const card = this.add.container(spawnX, spawnY);
card.setDepth(LAYERS.CARDS);
// Use scroll image as background (47x40px)
const cardBg = this.add.image(0, 0, 'scroll');
card.add(cardBg);
// Add white background for icon visibility
const iconBg = this.add.circle(0, 0, 15, 0xFFFFFF);
card.add(iconBg);
// Add attack icon based on type
switch (randomType) {
case 'bullet':
// Bullet visual
const bulletLine = this.add.rectangle(0, 0, 3, 25, 0x00ff00);
const bulletArrow = this.add.polygon(0, -14, [0, -6, -5, 3, 5, 3], 0x00ff00);
card.add([bulletLine, bulletArrow]);
break;
case 'standard':
// Bowling ball visual
const ballCircle = this.add.circle(0, 0, 12, 0x4444ff);
ballCircle.setStrokeStyle(2, 0x0000aa);
card.add(ballCircle);
break;
case 'bomb':
// Bomb visual
const bombCircle = this.add.circle(0, 2, 10, 0xff4444);
bombCircle.setStrokeStyle(2, 0xaa0000);
const fuse = this.add.rectangle(-2, -6, 2, 6, 0x8B4513);
const spark = this.add.circle(-2, -9, 2, 0xffff00);
card.add([bombCircle, fuse, spark]);
break;
}
// Store card properties
card.attackType = randomType;
card.isDragging = false;
card.fallSpeed = Phaser.Math.Between(50, 100); // pixels per second
// Make card interactive for dragging (using scroll dimensions)
card.setSize(47, 40);
card.setInteractive({ draggable: true });
// Setup drag handlers
card.on('dragstart', (pointer) => {
card.isDragging = true;
card.setDepth(LAYERS.GUI + 10);
console.log(`Started dragging falling ${card.attackType} card`);
});
card.on('drag', (pointer, dragX, dragY) => {
card.setPosition(dragX, dragY);
this.checkCardColumnHighlight(dragX, dragY, card);
});
card.on('dragend', (pointer) => {
card.isDragging = false;
if (card.targetColumn >= 0) {
console.log(`Dropping ${card.attackType} card on column ${card.targetColumn}`);
this.executeCardAttack(card.attackType, card.targetColumn);
// Remove card from falling cards list
const index = this.fallingCards.indexOf(card);
if (index > -1) {
this.fallingCards.splice(index, 1);
}
card.destroy();
} else {
// Return to falling if not dropped on valid column
card.isDragging = false;
card.setDepth(LAYERS.CARDS);
}
this.clearColumnHighlight();
card.targetColumn = -1;
});
this.fallingCards.push(card);
console.log(`[CARD SPAWN] Successfully spawned ${randomType} card. Total falling cards: ${this.fallingCards.length}`);
}
checkCardColumnHighlight(x, y, card) {
// Check if dragging over the grid area
if (y >= this.GRID_START_Y && y <= this.GRID_START_Y + (this.GRID_ROWS * this.TILE_SIZE)) {
if (x >= this.GRID_START_X && x <= this.GRID_START_X + (this.GRID_COLS * this.TILE_SIZE)) {
// Calculate which column we're over
const column = Math.floor((x - this.GRID_START_X) / this.TILE_SIZE);
if (column >= 0 && column < this.GRID_COLS) {
if (card.targetColumn !== column) {
card.targetColumn = column;
this.highlightColumn(column);
}
return;
}
}
}
// Not over valid grid area
if (card.targetColumn >= 0) {
this.clearColumnHighlight();
card.targetColumn = -1;
}
}
executeCardAttack(attackType, column) {
// Jeff plays attack animation
if (this.jeff) {
this.jeff.playAttackAnimation();
}
// Execute the attack based on type
switch (attackType) {
case 'bullet':
this.executeBulletAttack(column);
break;
case 'standard':
this.executeStandardAttack(column);
break;
case 'bomb':
this.executeBombAttack(column);
break;
}
}
executeBulletAttack(column) {
console.log(`Executing bullet attack on column ${column}`);
// Get bullet from pool
const bullet = this.getProjectileFromPool('bullet');
const startX = this.GRID_START_X + (column * this.TILE_SIZE) + (this.TILE_SIZE / 2);
const startY = this.GRID_START_Y + (this.GRID_ROWS * this.TILE_SIZE);
bullet.setPosition(startX, startY);
// Store bullet properties
bullet.column = column;
bullet.hitEnemies = [];
bullet.hitPositions = [];
// Move bullet up the column
const bulletTween = this.tweens.add({
targets: bullet,
y: this.GRID_START_Y - 50,
duration: 2000,
ease: 'Linear',
onComplete: () => {
this.returnProjectileToPool(bullet);
}
});
bullet.moveTween = bulletTween;
this.activeProjectiles.push(bullet);
}
executeStandardAttack(column) {
console.log(`Executing standard bowling attack on column ${column}`);
// Get bowling ball from pool
const ball = this.getProjectileFromPool('bowling');
const startX = this.GRID_START_X + (column * this.TILE_SIZE) + (this.TILE_SIZE / 2);
const startY = this.GRID_START_Y + (this.GRID_ROWS * this.TILE_SIZE);
ball.setPosition(startX, startY);
// Reset ball properties
ball.direction = 0;
ball.damage = 1;
ball.hitEnemies = [];
ball.hitPositions = [];
this.activeProjectiles.push(ball);
this.moveBowlingBall(ball, column);
}
executeBombAttack(column) {
console.log(`Executing bomb attack on column ${column}`);
// Get bomb from pool
const bomb = this.getProjectileFromPool('bomb');
const startX = this.GRID_START_X + (column * this.TILE_SIZE) + (this.TILE_SIZE / 2);
const startY = this.GRID_START_Y + (this.GRID_ROWS * this.TILE_SIZE);
bomb.setPosition(startX, startY);
// Reset bomb properties
bomb.hasExploded = false;
// Move bomb up the column
const bombTween = this.tweens.add({
targets: bomb,
y: this.GRID_START_Y - 50,
duration: 2500,
ease: 'Linear',
onComplete: () => {
if (bomb.active && !bomb.hasExploded) {
this.returnProjectileToPool(bomb);
}
}
});
bomb.moveTween = bombTween;
this.activeProjectiles.push(bomb);
}
initializeWaveSystem() {
// Wave configuration
this.currentWave = 0;
this.waveTimer = 0;
this.enemiesSpawnedInWave = 0;
this.enemiesAliveInWave = 0;
this.waveActive = false;
this.waveComplete = false;
this.timeBetweenWaves = 1500; // 1.5 seconds between waves
this.nextWaveTime = 500; // Start first wave after 0.5 seconds
console.log(`[WAVE DEBUG] Wave system initialized. enemiesAliveInWave: ${this.enemiesAliveInWave}`);
// Pre-seeded wave configurations with randomized columns
this.waveConfigs = [
// Wave 1: Introduction (3 basic enemies)
{
enemies: [
{ type: 'armored', delay: 0 },
{ type: 'basic', delay: 1000 },
{ type: 'summoner', delay: 2000 }
],
message: 'Wave 1: Getting Started'
},
// Wave 2: Mixed basics (5 enemies)
{
enemies: [
{ type: 'basic', delay: 0 },
{ type: 'basic', delay: 500 },
{ type: 'basic', delay: 1000 },
{ type: 'basic', delay: 1500 },
{ type: 'basic', delay: 2000 }
],
message: 'Wave 2: Warming Up'
},
// Wave 3: Introduce armored (3 basic + 2 armored)
{
enemies: [
{ type: 'armored', delay: 0 },
{ type: 'basic', delay: 500 },
{ type: 'basic', delay: 1000 },
{ type: 'armored', delay: 1500 },
{ type: 'basic', delay: 2000 }
],
message: 'Wave 3: Armored Enemies!'
},
// Wave 4: Summoner introduction (1 summoner + basics)
{
enemies: [
{ type: 'summoner', delay: 0 },
{ type: 'basic', delay: 1000 },
{ type: 'basic', delay: 1500 },
{ type: 'basic', delay: 2500 }
],
message: 'Wave 4: Summoners Arrive!'
},
// Wave 5: Mixed challenge (faster spawn)
{
enemies: [
{ type: 'basic', delay: 0 },
{ type: 'armored', delay: 300 },
{ type: 'basic', delay: 600 },
{ type: 'basic', delay: 900 },
{ type: 'armored', delay: 1200 },
{ type: 'basic', delay: 1500 }
],
message: 'Wave 5: Speed Round!'
},
// Wave 6: Double summoners
{
enemies: [
{ type: 'summoner', delay: 0 },
{ type: 'summoner', delay: 500 },
{ type: 'armored', delay: 1500 },
{ type: 'armored', delay: 2000 }
],
message: 'Wave 6: Double Trouble!'
},
// Wave 7: Chaos mode
{
enemies: [
{ type: 'basic', delay: 0 },
{ type: 'basic', delay: 200 },
{ type: 'basic', delay: 400 },
{ type: 'basic', delay: 600 },
{ type: 'basic', delay: 800 },
{ type: 'basic', delay: 1000 },
{ type: 'armored', delay: 1500 },
{ type: 'summoner', delay: 2000 }
],
message: 'Wave 7: Full Assault!'
},
// Wave 8+: Generate increasingly difficult waves
];
console.log('Wave system initialized');
}
startNextWave() {
if (this.waveActive) return;
this.waveActive = true;
this.waveComplete = false;
this.enemiesSpawnedInWave = 0;
this.enemiesAliveInWave = 0;
console.log(`[WAVE DEBUG] Starting wave with counters reset. Active: ${this.waveActive}, Complete: ${this.waveComplete}`);
let waveConfig;
if (this.currentWave < this.waveConfigs.length) {
waveConfig = this.waveConfigs[this.currentWave];
} else {
// Generate random wave for waves beyond pre-configured ones
waveConfig = this.generateRandomWave(this.currentWave);
}
console.log(`[WAVE] Starting ${waveConfig.message} (Wave ${this.currentWave + 1}/${this.waveConfigs.length})`);
// Show wave announcement
this.showWaveAnnouncement(waveConfig.message);
// Spawn enemies according to wave configuration
waveConfig.enemies.forEach(enemyConfig => {
this.time.delayedCall(enemyConfig.delay, () => {
// Randomize column if not specified
const column = enemyConfig.column !== undefined ? enemyConfig.column : Phaser.Math.Between(0, this.GRID_COLS - 1);
const enemy = this.spawnEnemyAtColumn(enemyConfig.type, column, 0);
if (enemy) {
this.enemiesSpawnedInWave++;
this.enemiesAliveInWave++;
console.log(`[WAVE DEBUG] Spawned ${enemyConfig.type} enemy at column ${column}. Total spawned: ${this.enemiesSpawnedInWave}, Alive: ${this.enemiesAliveInWave}`);
}
// Check if all enemies have been spawned
if (this.enemiesSpawnedInWave >= waveConfig.enemies.length) {
console.log(`[WAVE] All enemies spawned for wave ${this.currentWave + 1}. Enemies alive: ${this.enemiesAliveInWave}`);
}
});
});
}
onWaveComplete() {
console.log(`[WAVE] Wave ${this.currentWave + 1} complete!`);
this.waveActive = false;
this.waveComplete = true;
this.currentWave++;
this.nextWaveTime = this.timeBetweenWaves;
}
showWaveAnnouncement(message) {
// Create wave announcement text
const centerX = this.scale.width / 2;
const centerY = this.scale.height / 2;
const waveText = this.add.text(centerX, centerY, message, {
fontSize: '36px',
fontFamily: 'Fredoka',
fontWeight: '600',
color: '#ffffff',
stroke: '#000000',
strokeThickness: 6,
align: 'center'
});
waveText.setOrigin(0.5, 0.5);
waveText.setDepth(LAYERS.GUI + 20);
waveText.setScale(0);
waveText.setAlpha(0);
// Animate in
this.tweens.add({
targets: waveText,
scaleX: 1,
scaleY: 1,
alpha: 1,
duration: 500,
ease: 'Back.easeOut',
onComplete: () => {
// Hold for a moment then fade out
this.tweens.add({
targets: waveText,
alpha: 0,
scaleX: 1.2,
scaleY: 1.2,
duration: 1000,
delay: 1500,
ease: 'Power2.easeIn',
onComplete: () => {
waveText.destroy();
}
});
}
});
}
generateRandomWave(waveNumber) {
// Generate increasingly difficult waves
const baseEnemyCount = 5 + Math.floor(waveNumber / 2);
const enemyCount = Math.min(baseEnemyCount, 12); // Cap at 12 enemies
const enemies = [];
for (let i = 0; i < enemyCount; i++) {
// Increase chance of harder enemies as waves progress
const rand = Math.random();
let type = 'basic';
if (waveNumber > 10 && rand > 0.7) {
type = 'summoner';
} else if (waveNumber > 5 && rand > 0.5) {
type = 'armored';
}
enemies.push({
type: type,
column: Phaser.Math.Between(0, this.GRID_COLS - 1),
delay: i * Math.max(200, 1000 - (waveNumber * 50)) // Faster spawns in later waves
});
}
return {
enemies: enemies,
message: `Wave ${waveNumber + 1}: Endless Mode!`
};
}
createAttacks() {
// Create the three attack types at the bottom of the screen (hidden for now)
const bottomY = this.scale.height - 50;
const attackSpacing = 80;
const startX = (this.scale.width - (attackSpacing * 2)) / 2; // Center the three attacks
this.attacks = [
new BulletAttack(this, startX, bottomY),
new StandardAttack(this, startX + attackSpacing, bottomY),
new BombAttack(this, startX + (attackSpacing * 2), bottomY)
];
// Hide the attacks initially
this.attacks.forEach(attack => {
attack.setVisible(false);
});
console.log(`Created ${this.attacks.length} attack types at bottom of screen (hidden)`);
}
highlightColumn(column) {
this.clearColumnHighlight();
// Create column highlight
this.columnHighlight = this.add.graphics();
this.columnHighlight.setDepth(LAYERS.GRAPHICS + 1);
const columnX = this.GRID_START_X + (column * this.TILE_SIZE);
const columnWidth = this.TILE_SIZE;
const columnHeight = this.GRID_ROWS * this.TILE_SIZE + 15; // Extended by 15px at bottom
// Draw glowing column highlight
this.columnHighlight.fillStyle(0xffff00, 0.3);
this.columnHighlight.fillRect(columnX, this.GRID_START_Y, columnWidth, columnHeight);
this.columnHighlight.lineStyle(3, 0xffff00, 0.8);
this.columnHighlight.strokeRect(columnX, this.GRID_START_Y, columnWidth, columnHeight);
// Add pulsing animation
this.tweens.add({
targets: this.columnHighlight,
alpha: 0.5,
duration: 500,
yoyo: true,
repeat: -1
});
// Move Jeff to the highlighted column
if (this.jeff) {
this.jeff.moveToColumn(column);
}
}
clearColumnHighlight() {
if (this.columnHighlight) {
this.columnHighlight.destroy();
this.columnHighlight = null;
}
}
// Bowling ball movement with bouncing
moveBowlingBall(ball, currentColumn) {
const moveDistance = 30;
let targetX = ball.x;
let targetY = ball.y - moveDistance;
// Calculate movement based on direction
if (ball.direction === -45) {
// Moving left-up diagonal
targetX = ball.x - (moveDistance * 0.7);
targetY = ball.y - (moveDistance * 0.7);
} else if (ball.direction === 45) {
// Moving right-up diagonal
targetX = ball.x + (moveDistance * 0.7);
targetY = ball.y - (moveDistance * 0.7);
}
// Check wall collisions for bouncing
const leftBoundary = this.GRID_START_X;
const rightBoundary = this.GRID_START_X + (this.GRID_COLS * this.TILE_SIZE);
// Check if ball will hit a wall
if (targetX <= leftBoundary) {
// Hit left wall - bounce to the right
targetX = leftBoundary;
ball.direction = 45; // Change to right diagonal
console.log('Bowling ball hit left wall - bouncing right');
} else if (targetX >= rightBoundary) {
// Hit right wall - bounce to the left
targetX = rightBoundary;
ball.direction = -45; // Change to left diagonal
console.log('Bowling ball hit right wall - bouncing left');
}
// Check top boundary - if ball goes above game area, return to pool
if (targetY < this.GRID_START_Y - 50) {
this.returnProjectileToPool(ball);
return;
}
this.tweens.add({
targets: ball,
x: targetX,
y: targetY,
duration: 150,
onComplete: () => {
if (ball.active) {
// Continue movement - collisions handled in main collision system
const newColumn = Math.floor((ball.x - this.GRID_START_X) / this.TILE_SIZE);
this.moveBowlingBall(ball, newColumn);
}
}
});
}
// Bomb explosion with area damage
explodeBomb(bomb, centerEnemy) {
console.log(`Bomb exploding at enemy position: (${centerEnemy.x}, ${centerEnemy.y})`);
// Calculate which grid tile the enemy is in
const tileInfo = this.getTile(centerEnemy.x, centerEnemy.y);
const enemyGridCol = tileInfo.col;
const enemyGridRow = tileInfo.row;
const gridCenterX = this.GRID_START_X + (enemyGridCol * this.TILE_SIZE) + (this.TILE_SIZE / 2);
const gridCenterY = this.GRID_START_Y + (enemyGridRow * this.TILE_SIZE) + (this.TILE_SIZE / 2);
// Check if enemy is mostly on one spot (close to tile center) vs between spots
const distanceFromCenter = Phaser.Math.Distance.Between(centerEnemy.x, centerEnemy.y, gridCenterX, gridCenterY);
const isOnSingleSpot = distanceFromCenter <= (this.TILE_SIZE * 0.3); // 30% of tile size tolerance
let explosionTiles = [];
if (isOnSingleSpot) {
// Enemy is mostly on one spot - 3x3 explosion (9 tiles) around the enemy's position
console.log(`Bomb: Enemy on single spot - 3x3 explosion at (${enemyGridCol}, ${enemyGridRow})`);
for (let row = -1; row <= 1; row++) {
for (let col = -1; col <= 1; col++) {
const tileRow = enemyGridRow + row;
const tileCol = enemyGridCol + col;
if (tileRow >= 0 && tileRow < this.GRID_ROWS && tileCol >= 0 && tileCol < this.GRID_COLS) {
explosionTiles.push({ row: tileRow, col: tileCol });
}
}
}
} else {
// Enemy is between spots - determine the two main spots they're between
// Check if they're between rows or columns
const xOffset = (centerEnemy.x - gridCenterX) / this.TILE_SIZE;
const yOffset = (centerEnemy.y - gridCenterY) / this.TILE_SIZE;
if (Math.abs(yOffset) > Math.abs(xOffset)) {
// Enemy is primarily between rows (vertically between tiles)
const secondRow = yOffset < 0 ? enemyGridRow - 1 : enemyGridRow + 1;
console.log(`Bomb: Enemy between rows ${enemyGridRow} and ${secondRow} - cross explosion`);
// Add the 6 tiles: 2 center tiles + 2 left + 2 right
const crossTiles = [
{ row: enemyGridRow, col: enemyGridCol }, // Main tile
{ row: secondRow, col: enemyGridCol }, // Adjacent tile
{ row: enemyGridRow, col: enemyGridCol - 1 }, // Left of main
{ row: enemyGridRow, col: enemyGridCol + 1 }, // Right of main
{ row: secondRow, col: enemyGridCol - 1 }, // Left of adjacent
{ row: secondRow, col: enemyGridCol + 1 } // Right of adjacent
];
crossTiles.forEach(tile => {
if (tile.row >= 0 && tile.row < this.GRID_ROWS && tile.col >= 0 && tile.col < this.GRID_COLS) {
explosionTiles.push(tile);
}
});
} else {
// Enemy is primarily between columns (horizontally between tiles)
const secondCol = xOffset < 0 ? enemyGridCol - 1 : enemyGridCol + 1;
console.log(`Bomb: Enemy between cols ${enemyGridCol} and ${secondCol} - cross explosion`);
// Add the 6 tiles: 2 center tiles + 2 above + 2 below
const crossTiles = [
{ row: enemyGridRow, col: enemyGridCol }, // Main tile
{ row: enemyGridRow, col: secondCol }, // Adjacent tile
{ row: enemyGridRow - 1, col: enemyGridCol }, // Above main
{ row: enemyGridRow + 1, col: enemyGridCol }, // Below main
{ row: enemyGridRow - 1, col: secondCol }, // Above adjacent
{ row: enemyGridRow + 1, col: secondCol } // Below adjacent
];
crossTiles.forEach(tile => {
if (tile.row >= 0 && tile.row < this.GRID_ROWS && tile.col >= 0 && tile.col < this.GRID_COLS) {
explosionTiles.push(tile);
}
});
}
}
// Create explosion effects and damage enemies
explosionTiles.forEach((tile, index) => {
const tileX = this.GRID_START_X + (tile.col * this.TILE_SIZE) + (this.TILE_SIZE / 2);
const tileY = this.GRID_START_Y + (tile.row * this.TILE_SIZE) + (this.TILE_SIZE / 2);
// Create smoke effect with slight delay for visual effect
this.time.delayedCall(index * 50, () => {
this.createSmokeEffect(tileX, tileY);
});
// Damage all enemies in this specific tile area (using distance-based detection)
this.activeEnemies.forEach(enemy => {
// Check if enemy is within this explosion tile using distance from tile center
const distanceFromTileCenter = Phaser.Math.Distance.Between(enemy.x, enemy.y, tileX, tileY);
const maxTileRadius = this.TILE_SIZE * 0.7; // 70% of tile size to account for tile boundaries
if (distanceFromTileCenter <= maxTileRadius) {
console.log(`Bomb: Killing enemy ${enemy.constructor.name} at tile (${tile.col}, ${tile.row}) - distance: ${Math.round(distanceFromTileCenter)}px`);
enemy.takeDamage(999); // Bomb kills instantly
}
});
});
console.log(`Bomb exploded in ${explosionTiles.length} tiles (${isOnSingleSpot ? '3x3' : '6-tile cross'} pattern)`);
}
// Create smoke effect for bomb explosions
createSmokeEffect(x, y) {
const smoke = this.add.circle(x, y, 15, 0x666666, 0.7);
smoke.setDepth(LAYERS.PROJECTILES + 1);
this.tweens.add({
targets: smoke,
scaleX: 2,
scaleY: 2,
alpha: 0,
duration: 800,
ease: 'Power2.easeOut',
onComplete: () => {
smoke.destroy();
}
});
}
setupInput() {
this.input.on('pointerdown', (pointer) => {
// Game input will be handled here later
});
}
update(time, delta) {
// Update FPS counter
this.fpsText.setText('FPS: ' + Math.round(this.game.loop.actualFps));
// Periodic debug output for enemy positions (every 1 second)
this.debugTimer += delta;
if (this.debugTimer >= 1000) {
this.debugTimer = 0;
this.logEnemyPositions();
}
// Update all active enemies (they should move even before game officially starts)
this.activeEnemies.forEach(enemy => {
enemy.update(delta);
});
// Update falling card system
if (this.gameStarted) {
// Update card spawn timer
this.cardSpawnTimer += delta;
if (this.cardSpawnTimer >= this.nextCardSpawnTime) {
console.log(`[CARD SYSTEM] Timer reached! Spawning new card...`);
this.spawnFallingCard();
this.cardSpawnTimer = 0;
this.nextCardSpawnTime = Phaser.Math.Between(this.cardSpawnMinTime, this.cardSpawnMaxTime);
console.log(`[CARD SYSTEM] Next card spawn in ${Math.round(this.nextCardSpawnTime / 1000)} seconds`);
}
// Update falling cards
this.fallingCards = this.fallingCards.filter(card => {
if (!card.isDragging && card.active) {
// Make card fall
card.y += (card.fallSpeed * delta) / 1000;
// Remove card if it falls off screen
if (card.y > this.scale.height + 50) {
card.destroy();
return false;
}
}
return card.active;
});
// Update wave system
if (!this.waveActive && this.waveComplete) {
this.nextWaveTime -= delta;
if (this.nextWaveTime <= 0) {
console.log(`[WAVE] Timer reached! Starting next wave...`);
this.startNextWave();
}
} else if (!this.waveActive && !this.waveComplete && this.currentWave === 0) {
// First wave timer
this.nextWaveTime -= delta;
if (this.nextWaveTime <= 0) {
console.log(`[WAVE] Starting first wave...`);
this.startNextWave();
}
}
} else {
// Log once per second that game hasn't started
if (this.debugTimer === 0) {
console.log('[GAME STATE] Waiting for game to start. Click home screen to begin.');
}
}
// Check simple distance-based collisions
this.checkCollisions();
}
logEnemyPositions() {
console.log('=== ENEMY POSITION DEBUG ===');
this.activeEnemies.forEach((enemy, index) => {
const tileInfo = this.getTile(enemy.x, enemy.y);
const expectedTileCenter = this.grid[tileInfo.row] && this.grid[tileInfo.row][tileInfo.col]
? this.grid[tileInfo.row][tileInfo.col]
: { centerX: 'N/A', centerY: 'N/A' };
const enemyType = enemy.constructor.name;
console.log(`Enemy ${index + 1} (${enemyType}):
Position: (${Math.round(enemy.x)}, ${Math.round(enemy.y)})
Calculated tile: (${tileInfo.col}, ${tileInfo.row}) ${tileInfo.rawCol !== tileInfo.col || tileInfo.rawRow !== tileInfo.row ? `[raw: (${tileInfo.rawCol}, ${tileInfo.rawRow})]` : ''}
Expected tile center: (${Math.round(expectedTileCenter.centerX || 0)}, ${Math.round(expectedTileCenter.centerY || 0)})
Distance to center: ${expectedTileCenter.centerX ? Math.round(Phaser.Math.Distance.Between(enemy.x, enemy.y, expectedTileCenter.centerX, expectedTileCenter.centerY)) : 'N/A'}px`);
});
console.log(`TILE_SIZE: ${Math.round(this.TILE_SIZE)}px | GRID_START: (${this.GRID_START_X}, ${this.GRID_START_Y})`);
console.log('=========================');
}
checkCollisions() {
// Clean up destroyed projectiles first
this.activeProjectiles = this.activeProjectiles.filter(p => p.active && p.visible);
// Check each projectile against each enemy using distance-based collision detection
this.activeProjectiles.forEach(projectile => {
this.activeEnemies.forEach(enemy => {
const distance = Phaser.Math.Distance.Between(projectile.x, projectile.y, enemy.x, enemy.y);
const collisionThreshold = 25; // Collision threshold
if (distance < collisionThreshold) {
this.handleCollision(projectile, enemy);
}
});
});
}
handleCollision(projectile, enemy) {
console.log(`${projectile.attackType} hit ${enemy.constructor.name}`);
// Calculate enemy's tile position for position-based collision tracking
const tileInfo = this.getTile(enemy.x, enemy.y);
const tileKey = `${tileInfo.col},${tileInfo.row}`;
switch (projectile.attackType) {
case 'bullet':
// Check if this enemy has already been hit by this projectile
if (projectile.hitEnemies.includes(enemy)) {
return; // Skip collision if already hit
}
console.log(`Bullet hit ${enemy.constructor.name} for ${projectile.damage} damage`);
enemy.takeDamage(projectile.damage);
// Add enemy to hit list
projectile.hitEnemies.push(enemy);
// Note: Bullet continues through enemies (doesn't get destroyed)
break;
case 'bowling':
// For StandardAttack (bowling), use position-based collision prevention
if (projectile.hitPositions.includes(tileKey)) {
return; // Skip collision if we've already hit this position
}
console.log(`Bowling ball hit ${enemy.constructor.name} at position ${tileKey} for ${projectile.damage} damage`);
enemy.takeDamage(projectile.damage);
// Add position to hit list to prevent multiple hits at same location
projectile.hitPositions.push(tileKey);
// Implement bouncing trajectory
if (projectile.direction === 0) {
// First hit - randomly choose -45 or 45 degrees
projectile.direction = Math.random() < 0.5 ? -45 : 45;
console.log(`Bowling ball first hit - changing direction to ${projectile.direction} degrees`);
} else {
// Subsequent hits - invert the direction
projectile.direction = projectile.direction === 45 ? -45 : 45;
console.log(`Bowling ball bounce - inverting direction to ${projectile.direction} degrees`);
}
break;
case 'bomb':
console.log(`Bomb hit ${enemy.constructor.name} - exploding!`);
if (!projectile.hasExploded) {
projectile.hasExploded = true;
// Stop the bomb's movement tween
if (projectile.moveTween) {
projectile.moveTween.stop();
}
// Kill the enemy that was hit directly
enemy.takeDamage(999);
// Explode and damage nearby enemies
this.explodeBomb(projectile, enemy);
// Return bomb to pool
this.returnProjectileToPool(projectile);
}
break;
}
}
startGame() {
this.gameStarted = true;
console.log('[GAME STATE] Game started! Initializing systems...');
console.log(`[WAVE] First wave will start in ${this.nextWaveTime / 1000} seconds`);
console.log(`[CARD SYSTEM] First card will spawn in ${Math.round(this.nextCardSpawnTime / 1000)} seconds`);
}
}
const config = {
type: Phaser.AUTO,
width: 500,
height: 800,
scene: [Preloader, Game],
fps: {
target: 60,
forceSetTimeOut: true,
},
backgroundColor: '#87CEEB'
};
window.game = new Phaser.Game(config);
// Home screen click handler
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('click', (e) => {
const homeScreen = document.getElementById('homeScreen');
if (homeScreen && homeScreen.style.display !== 'none' && !homeScreen.classList.contains('fade-out')) {
homeScreen.classList.add('fade-out');
setTimeout(() => {
homeScreen.style.display = 'none';
const gameScene = window.game.scene.getScene('Game');
if (!gameScene.scene.isActive()) {
window.game.scene.start('Game');
} else {
gameScene.startGame();
}
}, 500);
}
});
});
</script>
</body>
</html>