// Phaser is loaded from CDN, so we don't need to import it as a module /** * Main game configuration and initialization */ const config = { type: Phaser.AUTO, width: 1600, height: 900, parent: 'game-container', scene: { preload: preload, create: create, update: update }, scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, physics: { default: 'arcade', arcade: { gravity: { y: 0 }, debug: false } } }; /** * Game instance */ let game; /** * Preload function - load all assets before the game starts */ function preload() { console.log('Preloading game assets...'); // Create a simple loading bar const progressBar = this.add.graphics(); const progressBox = this.add.graphics(); progressBox.fillStyle(0x242424, 0.8); progressBox.fillRect(700, 430, 200, 50); const width = this.cameras.main.width; const height = this.cameras.main.height; const loadingText = this.make.text({ x: width / 2, y: height / 2 - 50, text: 'Loading...', style: { font: '20px monospace', fill: '#ffffff' } }); loadingText.setOrigin(0.5, 0.5); const percentText = this.make.text({ x: width / 2, y: height / 2 - 5, text: '0%', style: { font: '18px monospace', fill: '#ffffff' } }); percentText.setOrigin(0.5, 0.5); const assetText = this.make.text({ x: width / 2, y: height / 2 + 50, text: '', style: { font: '18px monospace', fill: '#ffffff' } }); assetText.setOrigin(0.5, 0.5); // Update loading progress this.load.on('progress', (value) => { percentText.setText(parseInt(value * 100) + '%'); progressBar.clear(); progressBar.fillStyle(0xffffff, 1); progressBar.fillRect(700, 430, 200 * value, 50); }); this.load.on('fileprogress', (file) => { assetText.setText('Loading asset: ' + file.key); }); this.load.on('complete', () => { progressBar.destroy(); progressBox.destroy(); loadingText.destroy(); percentText.destroy(); assetText.destroy(); }); } /** * Create function - setup the game world and initial state */ function create() { console.log('Creating game scene...'); // Set background color this.cameras.main.setBackgroundColor('#2d5a8c'); // Add a simple title text const title = this.add.text(800, 100, 'Deck Building Card Battle', { fontSize: '32px', fill: '#ffffff', align: 'center' }); title.setOrigin(0.5); // Add player and opponent rows for cards in center of screen // Player row at bottom (closest to player) const playerRow = this.add.rectangle(800, 600, 1400, 120, 0x3a7c2d); playerRow.setOrigin(0.5); // Opponent row at top (closest to opponent) const opponentRow = this.add.rectangle(800, 300, 1400, 120, 0x3a7c2d); opponentRow.setOrigin(0.5); // Add some instructions const instructions = this.add.text(800, 850, 'Use mouse to play cards and build your deck', { fontSize: '16px', fill: '#ffffff', align: 'center' }); instructions.setOrigin(0.5); // Initialize game state this.gameState = { playerDeck: [], opponentDeck: [], playerHand: [], opponentHand: [], playerField: [], opponentField: [], selectedCard: null, turnPhase: 'draw', // Possible phases: draw, player_play, cpu_play currentPlayer: 'player' }; // Assign the first deck (deck1) to the player this.gameState.playerDeck = [...window.deck1]; // Assign deck2 to the opponent this.gameState.opponentDeck = [...window.deck2]; // Shuffle both decks using Fisher-Yates algorithm (using Phaser's scope) shuffleDeck(this.gameState.playerDeck); shuffleDeck(this.gameState.opponentDeck); // Deal 3 cards to player's hand this.gameState.playerHand = this.gameState.playerDeck.splice(0, 3); // Deal 3 cards to opponent's hand this.gameState.opponentHand = this.gameState.opponentDeck.splice(0, 3); // Display the player's hand at the bottom of the screen displayPlayerHand.call(this); // Display the opponent's hand at the top of the screen (face down) displayOpponentHand.call(this); // Set initial turn phase to player play this.gameState.turnPhase = 'player_play'; console.log('Game scene created successfully'); } /** * Shuffle an array using Fisher-Yates algorithm */ function shuffleDeck(deck) { for (let i = deck.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [deck[i], deck[j]] = [deck[j], deck[i]]; } } /** * Display the player's hand at the bottom of the screen */ function displayPlayerHand() { const cardWidth = 100; const cardHeight = 140; const spacing = 20; // Calculate total width needed for all cards const totalWidth = (this.gameState.playerHand.length * cardWidth) + ((this.gameState.playerHand.length - 1) * spacing); // Starting x position to center the hand const startX = (1600 - totalWidth) / 2; // Position cards at the bottom of the screen const yPosition = 850; this.gameState.playerHand.forEach((card, index) => { const xPosition = startX + (index * (cardWidth + spacing)); // Create a simple card placeholder with number const cardSprite = this.add.rectangle(xPosition, yPosition, cardWidth, cardHeight, 0x8B4513); cardSprite.setOrigin(0.5); // Add the number to the card const numberText = this.add.text(xPosition, yPosition - 20, `${card.number}`, { fontSize: '24px', fill: '#ffffff' }); numberText.setOrigin(0.5); // Display attack in upper left corner const attackText = this.add.text(xPosition - 40, yPosition - 50, `A:${card.attack}`, { fontSize: '16px', fill: '#ffffff' }); attackText.setOrigin(0.5); // Display shield in lower left corner const shieldText = this.add.text(xPosition - 40, yPosition + 50, `S:${card.shield}`, { fontSize: '16px', fill: '#ffffff' }); shieldText.setOrigin(0.5); // Display health in lower right corner const healthText = this.add.text(xPosition + 40, yPosition + 50, `H:${card.health}`, { fontSize: '16px', fill: '#ffffff' }); healthText.setOrigin(0.5); // Store references to stat texts with sprite reference card.attackText = attackText; card.shieldText = shieldText; card.healthText = healthText; // Store card data with sprite reference card.sprite = cardSprite; card.text = numberText; // Add hover events for the card addInteractiveCard.call(this, card, xPosition, yPosition, cardWidth, cardHeight); }); } /** * Display the opponent's hand at the top of the screen (face down) */ function displayOpponentHand() { const cardWidth = 100; const cardHeight = 140; const spacing = 20; // Calculate total width needed for all cards const totalWidth = (this.gameState.opponentHand.length * cardWidth) + ((this.gameState.opponentHand.length - 1) * spacing); // Starting x position to center the hand const startX = (1600 - totalWidth) / 2; // Position cards at the top of the screen (opponent's row) const yPosition = 350; // Slightly below opponentRow rectangle (which is at y=300) this.gameState.opponentHand.forEach((card, index) => { const xPosition = startX + (index * (cardWidth + spacing)); // Create a simple card placeholder - face down (gray color) const cardSprite = this.add.rectangle(xPosition, yPosition, cardWidth, cardHeight, 0x808080); // Gray color for face-down cards cardSprite.setOrigin(0.5); // Add the number to the card (face down - show only number) const numberText = this.add.text(xPosition, yPosition - 20, `${card.number}`, { fontSize: '24px', fill: '#ffffff' }); numberText.setOrigin(0.5); // For face-down cards, we don't display attack/shield/health values // Store references to sprite with no stat texts for opponent's hand card.sprite = cardSprite; card.text = numberText; }); } /** * Make a single card interactive with hover effects and turn-based control */ function addInteractiveCard(card, xPosition, yPosition, width, height) { const originalY = yPosition; // Create a transparent hit area for the card that matches its dimensions const hitArea = this.add.rectangle(xPosition, yPosition + 70, width - 10, height - 10); hitArea.setOrigin(0.5); hitArea.setInteractive(); // Add event listeners for hover effects hitArea.on('pointerover', () => { // Only allow hover effects if it's the player's turn and phase if (this.gameState.currentPlayer !== 'player' || this.gameState.turnPhase !== 'player_play') { return; } // Only allow hover effects if the card is in hand (not already on field) const isInHand = this.gameState.playerHand.includes(card); if (isInHand && !card.isLocked) { // Raise the card up to full view (move it up by 30 pixels) if (card.sprite) { card.sprite.setY(originalY - 30); } if (card.text) { card.text.setY(originalY - 50); } // Move stat texts with the card if (card.attackText) { card.attackText.setY(originalY - 80); } if (card.shieldText) { card.shieldText.setY(originalY + 20); } if (card.healthText) { card.healthText.setY(originalY + 20); } } }); hitArea.on('pointerout', () => { // Only allow hover effects if it's the player's turn and phase if (this.gameState.currentPlayer !== 'player' || this.gameState.turnPhase !== 'player_play') { return; } // Only allow hover effects if the card is in hand (not already on field) const isInHand = this.gameState.playerHand.includes(card); if (isInHand && !card.isLocked) { // Return the card to its original position if (card.sprite) { card.sprite.setY(originalY); } if (card.text) { card.text.setY(originalY - 20); } // Return stat texts to original positions if (card.attackText) { card.attackText.setY(originalY - 50); } if (card.shieldText) { card.shieldText.setY(originalY + 50); } if (card.healthText) { card.healthText.setY(originalY + 50); } } }); // Add click event to lock the card in place hitArea.on('pointerdown', () => { // Only allow clicking if it's the player's turn and phase if (this.gameState.currentPlayer !== 'player' || this.gameState.turnPhase !== 'player_play') { return; } // Only allow clicking if the card is in hand (not already on field) const isInHand = this.gameState.playerHand.includes(card); if (isInHand) { // Lock the card in its current position card.isLocked = true; // Remove hover effects while locked if (card.sprite) { card.sprite.setY(originalY - 30); // Keep it raised up when locked } if (card.text) { card.text.setY(originalY - 50); } // Create Play and Cancel buttons above the card createCardButtons.call(this, card, xPosition, originalY - 30); } }); } /** * Update function - called every frame */ function update() { // Game logic updates go here } /** * Create Play and Cancel buttons for a locked card */ function createCardButtons(card, xPosition, yPosition) { const buttonWidth = 60; const buttonHeight = 30; const spacing = 10; // Position the buttons above the card (at yPosition - 40) const playButtonX = xPosition - buttonWidth - spacing / 2; // Left of card const cancelButtonX = xPosition + buttonWidth + spacing / 2; // Right of card const buttonY = yPosition - 40; // Create Play button const playButton = this.add.rectangle(playButtonX, buttonY, buttonWidth, buttonHeight, 0x00ff00); // Green color playButton.setOrigin(0.5); playButton.setInteractive(); // Add text to the Play button const playText = this.add.text(playButtonX, buttonY, 'Play', { fontSize: '14px', fill: '#ffffff' }); playText.setOrigin(0.5); // Create Cancel button const cancelButton = this.add.rectangle(cancelButtonX, buttonY, buttonWidth, buttonHeight, 0xff0000); // Red color cancelButton.setOrigin(0.5); cancelButton.setInteractive(); // Add text to the Cancel button const cancelText = this.add.text(cancelButtonX, buttonY, 'Cancel', { fontSize: '14px', fill: '#ffffff' }); cancelText.setOrigin(0.5); // Store references to buttons in the card object card.playButton = playButton; card.cancelButton = cancelButton; card.playButtonText = playText; card.cancelButtonText = cancelText; // Add click events for the buttons playButton.on('pointerdown', () => { // Move the card to the player's field (center of screen) moveCardToField.call(this, card); // Remove buttons and unlock the card removeCardButtons(card); }); cancelButton.on('pointerdown', () => { // Return card to original position in hand returnCardToHand.call(this, card); // Remove buttons and unlock the card removeCardButtons(card); }); } /** * Move a card to the player's field (player row on far right) */ function moveCardToField(card) { // Remove it from hand and add it to field const index = this.gameState.playerHand.indexOf(card); if (index > -1) { this.gameState.playerHand.splice(index, 1); this.gameState.playerField.push(card); } // Calculate starting position for the animation (right side of screen) const startX = 1600 + 50; // Start just off-screen to the right const startY = 600; // Player field y-position (bottom row) // Calculate target x position based on how many cards are already in the field // Cards stack from right to left with each card being 120px wide + 20px spacing const cardWidth = 100; const spacing = 20; const currentFieldCount = this.gameState.playerField.length - 1; // -1 because we just added the new card const targetX = 1600 - (cardWidth + spacing) * currentFieldCount - (cardWidth / 2); // Position from right edge // Animate the card moving from right to left towards its final position in player field this.tweens.add({ targets: [card.sprite, card.text, card.attackText, card.shieldText, card.healthText], x: targetX, y: startY, duration: 500, // 500ms animation time ease: 'Power2' }); // Set the current player to CPU after playing a card this.gameState.currentPlayer = 'cpu'; // Re-display the player's hand to update positions of remaining cards after animation completes setTimeout(() => { // Clear existing hand display first by removing all sprites and texts this.gameState.playerHand.forEach(handCard => { if (handCard.sprite) { handCard.sprite.destroy(); } if (handCard.text) { handCard.text.destroy(); } }); // Redisplay the updated player's hand at the bottom of the screen displayPlayerHand.call(this); }, 500); // Wait for animation to complete before updating hand // After a short delay, have CPU play a card automatically (if available) setTimeout(() => { cpuPlayCard.call(this); }, 1000); // Small delay before CPU plays card } /** * Return a locked card to its original position in the player's hand */ function returnCardToHand(card) { // Reset the locked state card.isLocked = false; // Remove buttons and text references from the card object if (card.playButton) { card.playButton.destroy(); card.playButtonText.destroy(); } if (card.cancelButton) { card.cancelButton.destroy(); card.cancelButtonText.destroy(); } // Reset positions to original hand position const yPosition = 850; // Original hand position at bottom of screen const originalY = yPosition; if (card.sprite) { card.sprite.setY(originalY); } if (card.text) { card.text.setY(originalY - 20); } } /** * Remove buttons from a locked card */ function removeCardButtons(card) { // Reset the locked state card.isLocked = false; // Destroy button elements if they exist if (card.playButton) { card.playButton.destroy(); card.playButtonText.destroy(); delete card.playButton; delete card.playButtonText; } if (card.cancelButton) { card.cancelButton.destroy(); card.cancelButtonText.destroy(); delete card.cancelButton; delete card.cancelButtonText; } } /** * CPU plays a random card from its hand */ function cpuPlayCard() { // Only play a card if it's the CPU's turn and they have cards in hand if (this.gameState.currentPlayer !== 'cpu' || this.gameState.opponentHand.length === 0) { return; } // Select a random card from opponent's hand const randomIndex = Math.floor(Math.random() * this.gameState.opponentHand.length); const card = this.gameState.opponentHand[randomIndex]; // Remove it from hand and add it to field this.gameState.opponentHand.splice(randomIndex, 1); this.gameState.opponentField.push(card); // Calculate starting position for the animation (left side of screen) const startX = -50; // Start just off-screen to the left const startY = 300; // Opponent field y-position (top row) // Calculate target x position based on how many cards are already in the field // Cards stack from left to right with each card being 120px wide + 20px spacing const cardWidth = 100; const spacing = 20; const currentFieldCount = this.gameState.opponentField.length - 1; // -1 because we just added the new card const targetX = (cardWidth + spacing) * currentFieldCount + (cardWidth / 2); // Position from left edge // For CPU cards in field, show full stats (not face-down) const cpuCardSprite = this.add.rectangle(targetX, startY, cardWidth, 140, 0x8B4513); // Brown color for card background cpuCardSprite.setOrigin(0.5); // Add the number to the CPU's played card (showing full stats) const numberText = this.add.text(targetX, startY - 20, `${card.number}`, { fontSize: '24px', fill: '#ffffff' }); numberText.setOrigin(0.5); // Display attack in upper left corner for CPU card const attackText = this.add.text(targetX - 40, startY - 50, `A:${card.attack}`, { fontSize: '16px', fill: '#ffffff' }); attackText.setOrigin(0.5); // Display shield in lower left corner for CPU card const shieldText = this.add.text(targetX - 40, startY + 50, `S:${card.shield}`, { fontSize: '16px', fill: '#ffffff' }); shieldText.setOrigin(0.5); // Display health in lower right corner for CPU card const healthText = this.add.text(targetX + 40, startY + 50, `H:${card.health}`, { fontSize: '16px', fill: '#ffffff' }); healthText.setOrigin(0.5); // Animate the card moving from left to right towards its final position in opponent field this.tweens.add({ targets: [cpuCardSprite, numberText, attackText, shieldText, healthText], x: targetX, y: startY, duration: 500, // 500ms animation time ease: 'Power2' }); // Update the card's sprite and text references to point to the new elements for CPU field display card.sprite = cpuCardSprite; card.text = numberText; card.attackText = attackText; card.shieldText = shieldText; card.healthText = healthText; // After a short delay, return to player's turn (or end game if needed) setTimeout(() => { this.gameState.currentPlayer = 'player'; // Redisplay the opponent's hand at the top of the screen - now only cards that remain in hand displayOpponentHand.call(this); }, 1000); // Wait for animation to complete before returning to player turn } /** * Initialize the game when the page loads */ document.addEventListener('DOMContentLoaded', () => { console.log('Initializing Phaser 3 card battle game...'); // Create the game instance game = new Phaser.Game(config); console.log('Phaser 3 card battle game initialized'); }); // Export for potential module usage (not needed when using CDN) // export { config, game };