Compare commits

...

3 Commits

Author SHA1 Message Date
Brian Fertig a4931d491a feat: Implement opponent AI with CPU card playing logic and enhanced hand display
This commit adds full opponent functionality to the game including:
- Shuffling and dealing cards to both player and CPU decks
- Displaying opponent's hand face-down at top of screen
- Implementing CPU turn logic that automatically plays random cards
- Adding animations for CPU card placement on field with stats display
- Switching turns between player and CPU after card plays

The changes enable a complete two-player gameplay experience where the CPU automatically responds to player actions, creating a more engaging game flow.
2025-08-13 16:20:30 -06:00
Brian Fertig a07d64e9b1 Add player and opponent rows for card placement, implement hover effects with hand validation, and animate card movement to field with proper positioning. 2025-08-13 14:56:03 -06:00
Brian Fertig c6a1d928a6 Add card stat display (attack, shield, health) to player hand cards
This change adds visual indicators for attack, shield, and health values to each card displayed in the player's hand. The stats are shown in specific corners of each card:
- Attack value in upper left corner
- Shield value in lower left corner
- Health value in lower right corner

The stat text elements are properly positioned relative to the card sprite and maintain their positions during hover animations. Each card now stores references to its stat text elements for consistent positioning and updates.
2025-08-13 14:21:29 -06:00
1 changed files with 266 additions and 49 deletions

View File

@ -116,9 +116,14 @@ function create() {
}); });
title.setOrigin(0.5); title.setOrigin(0.5);
// Create a basic game area // Add player and opponent rows for cards in center of screen
const gameArea = this.add.rectangle(800, 450, 1400, 700, 0x3a7c2d); // Player row at bottom (closest to player)
gameArea.setOrigin(0.5); 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 // Add some instructions
const instructions = this.add.text(800, 850, 'Use mouse to play cards and build your deck', { const instructions = this.add.text(800, 850, 'Use mouse to play cards and build your deck', {
@ -144,15 +149,25 @@ function create() {
// Assign the first deck (deck1) to the player // Assign the first deck (deck1) to the player
this.gameState.playerDeck = [...window.deck1]; this.gameState.playerDeck = [...window.deck1];
// Shuffle the deck using Fisher-Yates algorithm (using Phaser's scope) // 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.playerDeck);
shuffleDeck(this.gameState.opponentDeck);
// Deal 3 cards to player's hand // Deal 3 cards to player's hand
this.gameState.playerHand = this.gameState.playerDeck.splice(0, 3); 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 // Display the player's hand at the bottom of the screen
displayPlayerHand.call(this); displayPlayerHand.call(this);
// Display the opponent's hand at the top of the screen (face down)
displayOpponentHand.call(this);
console.log('Game scene created successfully'); console.log('Game scene created successfully');
} }
@ -197,6 +212,32 @@ function displayPlayerHand() {
}); });
numberText.setOrigin(0.5); 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 // Store card data with sprite reference
card.sprite = cardSprite; card.sprite = cardSprite;
card.text = numberText; card.text = numberText;
@ -206,6 +247,44 @@ function displayPlayerHand() {
}); });
} }
/**
* 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 * Make a single card interactive with hover effects
*/ */
@ -219,6 +298,9 @@ function addInteractiveCard(card, xPosition, yPosition, width, height) {
// Add event listeners for hover effects // Add event listeners for hover effects
hitArea.on('pointerover', () => { hitArea.on('pointerover', () => {
// 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) // Raise the card up to full view (move it up by 30 pixels)
if (card.sprite) { if (card.sprite) {
card.sprite.setY(originalY - 30); card.sprite.setY(originalY - 30);
@ -226,20 +308,48 @@ function addInteractiveCard(card, xPosition, yPosition, width, height) {
if (card.text) { if (card.text) {
card.text.setY(originalY - 50); 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', () => { hitArea.on('pointerout', () => {
// 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 // Return the card to its original position
if (card.sprite && !card.isLocked) { if (card.sprite) {
card.sprite.setY(originalY); card.sprite.setY(originalY);
} }
if (card.text && !card.isLocked) { if (card.text) {
card.text.setY(originalY - 20); 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 // Add click event to lock the card in place
hitArea.on('pointerdown', () => { hitArea.on('pointerdown', () => {
// 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 // Lock the card in its current position
card.isLocked = true; card.isLocked = true;
@ -253,6 +363,7 @@ function addInteractiveCard(card, xPosition, yPosition, width, height) {
// Create Play and Cancel buttons above the card // Create Play and Cancel buttons above the card
createCardButtons.call(this, card, xPosition, originalY - 30); createCardButtons.call(this, card, xPosition, originalY - 30);
}
}); });
} }
@ -325,26 +436,41 @@ function createCardButtons(card, xPosition, yPosition) {
} }
/** /**
* Move a card to the player's field (center of screen) * Move a card to the player's field (player row on far right)
*/ */
function moveCardToField(card) { function moveCardToField(card) {
// For now, we'll just remove it from hand and add it to field // Remove it from hand and add it to field
// In a real implementation this would involve more complex logic
const index = this.gameState.playerHand.indexOf(card); const index = this.gameState.playerHand.indexOf(card);
if (index > -1) { if (index > -1) {
this.gameState.playerHand.splice(index, 1); this.gameState.playerHand.splice(index, 1);
this.gameState.playerField.push(card); this.gameState.playerField.push(card);
} }
// Remove the card from display // Calculate starting position for the animation (right side of screen)
if (card.sprite) { const startX = 1600 + 50; // Start just off-screen to the right
card.sprite.destroy(); const startY = 600; // Player field y-position (bottom row)
}
if (card.text) {
card.text.destroy();
}
// Re-display the player's hand to update positions of remaining cards // 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 // Clear existing hand display first by removing all sprites and texts
this.gameState.playerHand.forEach(handCard => { this.gameState.playerHand.forEach(handCard => {
if (handCard.sprite) { if (handCard.sprite) {
@ -357,6 +483,12 @@ function moveCardToField(card) {
// Redisplay the updated player's hand at the bottom of the screen // Redisplay the updated player's hand at the bottom of the screen
displayPlayerHand.call(this); 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
} }
/** /**
@ -410,6 +542,91 @@ function removeCardButtons(card) {
} }
} }
/**
* 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 * Initialize the game when the page loads
*/ */