diff --git a/ai_animation/src/domElements/chatWindows.ts b/ai_animation/src/domElements/chatWindows.ts index 2f4711e..da6ae2c 100644 --- a/ai_animation/src/domElements/chatWindows.ts +++ b/ai_animation/src/domElements/chatWindows.ts @@ -213,23 +213,42 @@ export function updateChatWindows(phase: any, stepMessages = false) { const prevIndex = gameState.phaseIndex > 0 ? gameState.phaseIndex - 1 : null; const previousPhase = prevIndex !== null ? gameState.gameData.phases[prevIndex] : null; - // Show animations for current phase's orders - if (previousPhase) { - if (config.isDebugMode) { - console.log(`Animating orders from ${previousPhase.name} to ${currentPhase.name}`); - } - createTweenAnimations(currentPhase, previousPhase); - } - - // After animations complete, advance to next phase - gameState.playbackTimer = setTimeout(() => { - if (gameState.isPlaying) { + // Only schedule next phase if not already scheduled + if (!gameState.nextPhaseScheduled) { + gameState.nextPhaseScheduled = true; + + // Show animations for current phase's orders + if (previousPhase) { if (config.isDebugMode) { - console.log(`Animations complete, advancing from ${currentPhase.name}`); + console.log(`Animating orders from ${previousPhase.name} to ${currentPhase.name}`); } - advanceToNextPhase(); + createTweenAnimations(currentPhase, previousPhase); + + // After animations complete, advance to next phase with longer delay + gameState.playbackTimer = setTimeout(() => { + if (gameState.isPlaying) { + if (config.isDebugMode) { + console.log(`Animations complete, advancing from ${currentPhase.name}`); + } + advanceToNextPhase(); + } + }, config.playbackSpeed + config.animationDuration); // Wait for both summary and animations + } else { + // For first phase, use shorter delay since there are no animations + if (config.isDebugMode) { + console.log(`First phase ${currentPhase.name} - no animations to wait for`); + } + + gameState.playbackTimer = setTimeout(() => { + if (gameState.isPlaying) { + if (config.isDebugMode) { + console.log(`Advancing from first phase ${currentPhase.name}`); + } + advanceToNextPhase(); + } + }, config.playbackSpeed); // Only wait for summary, no animation delay } - }, config.playbackSpeed + config.animationDuration); // Wait for both summary and animations + } } } return; @@ -692,16 +711,27 @@ export function addToNewsBanner(newText: string): void { return; } - console.log(`Adding to news banner: "${newText}"`); - - // If the banner only has the default text or is empty, replace it - if ( - bannerEl.textContent.trim() === 'Diplomatic actions unfolding...' || - bannerEl.textContent.trim() === '' - ) { - bannerEl.textContent = newText; - } else { - // Otherwise append with a separator - bannerEl.textContent += ' | ' + newText; + if (config.isDebugMode) { + console.log(`Adding to news banner: "${newText}"`); } + + // Add a fade-out transition + bannerEl.style.transition = 'opacity 0.3s ease-out'; + bannerEl.style.opacity = '0'; + + setTimeout(() => { + // If the banner only has the default text or is empty, replace it + if ( + bannerEl.textContent?.trim() === 'Diplomatic actions unfolding...' || + bannerEl.textContent?.trim() === '' + ) { + bannerEl.textContent = newText; + } else { + // Otherwise append with a separator + bannerEl.textContent += ' | ' + newText; + } + + // Fade back in + bannerEl.style.opacity = '1'; + }, 300); } diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index 5bc71ad..569ddf6 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -43,6 +43,7 @@ class GameState { isPlaying: boolean isSpeaking: boolean isAnimating: boolean + nextPhaseScheduled: boolean // Flag to prevent multiple phase transitions being scheduled //Scene for three.js scene: THREE.Scene @@ -59,8 +60,6 @@ class GameState { // playbackTimer: number - animationAttempted: boolean - constructor(boardName: AvailableMaps) { this.phaseIndex = 0 this.gameData = null @@ -70,7 +69,7 @@ class GameState { this.isPlaying = false this.isAnimating = false this.messagesPlaying = false - this.animationAttempted = false + this.nextPhaseScheduled = false this.scene = new THREE.Scene() this.unitMeshes = [] diff --git a/ai_animation/src/logger.ts b/ai_animation/src/logger.ts index 2b6fce2..0c06810 100644 --- a/ai_animation/src/logger.ts +++ b/ai_animation/src/logger.ts @@ -17,27 +17,67 @@ class Logger { console.log(msg) } - // New function to update info panel with useful information + // Updated function to update info panel with useful information and smooth transitions updateInfoPanel = () => { const totalPhases = gameState.gameData?.phases?.length || 0; - const currentPhaseNumber = currentPhaseIndex + 1; - const phaseName = gameState.gameData?.phases?.[currentPhaseIndex]?.name || 'Unknown'; - - this.infoPanel.innerHTML = ` -
Power: ${currentPower}
-
Current Phase: ${phaseName} (${currentPhaseNumber}/${totalPhases})
-
-

All-Time Leaderboard

- - `; + const currentPhaseNumber = gameState.phaseIndex + 1; + const phaseName = gameState.gameData?.phases?.[gameState.phaseIndex]?.name || 'Unknown'; + + // Add fade-out transition + this.infoPanel.style.transition = 'opacity 0.3s ease-out'; + this.infoPanel.style.opacity = '0'; + + // Update content after fade-out + setTimeout(() => { + // Get supply center counts for the current phase + const scCounts = this.getSupplyCenterCounts(); + + this.infoPanel.innerHTML = ` +
Power: ${currentPower}
+
Current Phase: ${phaseName} (${currentPhaseNumber}/${totalPhases})
+
+

Supply Center Counts

+ + `; + + // Fade back in + this.infoPanel.style.opacity = '1'; + }, 300); + } + + // Helper function to count supply centers for each power + getSupplyCenterCounts = () => { + const counts = { + AUSTRIA: 0, + ENGLAND: 0, + FRANCE: 0, + GERMANY: 0, + ITALY: 0, + RUSSIA: 0, + TURKEY: 0 + }; + + // Get current phase's supply center data + const centers = gameState.gameData?.phases?.[gameState.phaseIndex]?.state?.centers; + + if (centers) { + // Count supply centers for each power + Object.entries(centers).forEach(([center, power]) => { + if (power && typeof power === 'string' && power in counts) { + counts[power as keyof typeof counts]++; + } + }); + } + + return counts; } } export const logger = new Logger() diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index cebd150..d2071df 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -79,23 +79,24 @@ function animate() { // If messages are done playing but we haven't started unit animations yet if (!gameState.messagesPlaying && !gameState.isSpeaking && - gameState.unitAnimations.length === 0 && gameState.isPlaying && - !gameState.animationAttempted) { + gameState.unitAnimations.length === 0 && gameState.isPlaying) { if (gameState.gameData && gameState.gameData.phases) { - // Log that we're transitioning to animations - console.log("Messages complete, starting unit animations"); - - // Mark that we've attempted animation for this phase - gameState.animationAttempted = true; - + // Get previous phase index const prevIndex = gameState.phaseIndex > 0 ? gameState.phaseIndex - 1 : null; - // Create animations for unit movements based on orders - createTweenAnimations( - gameState.gameData.phases[gameState.phaseIndex], - prevIndex !== null ? gameState.gameData.phases[prevIndex] : null - ); + // Only attempt animations if we have a previous phase and we're not in the first phase + // Note: We're removing the scheduling logic from here since it's handled in chatWindows.ts + if (prevIndex !== null && !gameState.nextPhaseScheduled) { + // Log that we're transitioning to animations + console.log("Messages complete, starting unit animations"); + + // Create animations for unit movements based on orders + createTweenAnimations( + gameState.gameData.phases[gameState.phaseIndex], + gameState.gameData.phases[prevIndex] + ); + } } } } else { diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index 75f6041..0d6b688 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -1,4 +1,3 @@ -import * as THREE from "three"; import { gameState } from "./gameState"; import { logger } from "./logger"; import { phaseDisplay } from "./domElements"; @@ -9,110 +8,121 @@ import { updateChatWindows, addToNewsBanner } from "./domElements/chatWindows"; import { createTweenAnimations } from "./units/animate"; import { speakSummary } from "./speech"; import { config } from "./config"; -import { getProvincePosition } from "./map/utils"; -// New function to display initial state without messages -export function displayInitialPhase() { - let index = 0 - if (!gameState.gameData || !gameState.gameData.phases || index < 0 || index >= gameState.gameData.phases.length) { - logger.log("Invalid phase index.") +/** + * Unified function to display a phase with proper transitions + * Handles both initial display and animated transitions between phases + * @param index The index of the phase to display + * @param skipMessages Whether to skip message animations (used for initial load) + */ +export function displayPhase(index, skipMessages = false) { + if (!gameState.gameData || !gameState.gameData.phases || + index < 0 || index >= gameState.gameData.phases.length) { + logger.log("Invalid phase index."); return; } - // Clear any existing units - const supplyCenters = gameState.unitMeshes.filter(m => m.userData && m.userData.isSupplyCenter); - const oldUnits = gameState.unitMeshes.filter(m => m.userData && !m.userData.isSupplyCenter); - oldUnits.forEach(m => gameState.scene.remove(m)); - gameState.unitMeshes = supplyCenters; - - const phase = gameState.gameData.phases[index]; - phaseDisplay.textContent = `Era: ${phase.name || 'Unknown Era'} (${index + 1}/${gameState.gameData.phases.length})`; - - // Show supply centers - let newSCs = createSupplyCenters(); - newSCs.forEach((sc) => gameState.scene.add(sc)) - if (phase.state?.centers) { - updateSupplyCenterOwnership(phase.state.centers); - } - - // Show units - if (phase.state?.units) { - for (const [power, unitArr] of Object.entries(phase.state.units)) { - unitArr.forEach(unitStr => { - const match = unitStr.match(/^([AF])\s+(.+)$/); - if (match) { - let newUnit = createUnitMesh({ - power: power.toUpperCase(), - type: match[1], - province: match[2], - }); - gameState.scene.add(newUnit) - gameState.unitMeshes.push(newUnit) - } - }); - } - } - - updateLeaderboard(phase); - updateMapOwnership(phase) - - logger.log(`Phase: ${phase.name}\nSCs: ${phase.state?.centers ? JSON.stringify(phase.state.centers) : 'None'}\nUnits: ${phase.state?.units ? JSON.stringify(phase.state.units) : 'None'}`) - - // Add: Update info panel - logger.updateInfoPanel(); - -} - -export function displayPhaseWithAnimation(index) { - if (!gameState.gameData || !gameState.gameData.phases || index < 0 || index >= gameState.gameData.phases.length) { - logger.log("Invalid phase index.") - return; - } - - // Reset animation attempted flag for the new phase - gameState.animationAttempted = false; - // Handle the special case for the first phase (index 0) const isFirstPhase = index === 0; const currentPhase = gameState.gameData.phases[index]; // Only get previous phase if not the first phase - const prevIndex = isFirstPhase ? null : (index > 0 ? index - 1 : gameState.gameData.phases.length - 1); - const previousPhase = isFirstPhase ? null : gameState.gameData.phases[prevIndex]; + const prevIndex = isFirstPhase ? null : (index > 0 ? index - 1 : null); + const previousPhase = prevIndex !== null ? gameState.gameData.phases[prevIndex] : null; - phaseDisplay.textContent = `Era: ${currentPhase.name || 'Unknown Era'} (${index + 1}/${gameState.gameData.phases.length})`; + // Update phase display with smooth transition + if (phaseDisplay) { + // Add fade-out effect + phaseDisplay.style.transition = 'opacity 0.3s ease-out'; + phaseDisplay.style.opacity = '0'; + + // Update text after fade-out + setTimeout(() => { + phaseDisplay.textContent = `Era: ${currentPhase.name || 'Unknown Era'} (${index + 1}/${gameState.gameData.phases.length})`; + // Fade back in + phaseDisplay.style.opacity = '1'; + }, 300); + } - // Rebuild supply centers, remove old units + // Clear existing units except supply centers + const supplyCenters = gameState.unitMeshes.filter(m => m.userData && m.userData.isSupplyCenter); + const oldUnits = gameState.unitMeshes.filter(m => m.userData && !m.userData.isSupplyCenter); + oldUnits.forEach(m => gameState.scene.remove(m)); + gameState.unitMeshes = supplyCenters; - // First show messages, THEN animate units after - // First show messages with stepwise animation - updateChatWindows(currentPhase, true); - - // Ownership + // Update supply centers if (currentPhase.state?.centers) { updateSupplyCenterOwnership(currentPhase.state.centers); } - // Update leaderboard - updateLeaderboard(currentPhase); - updateMapOwnership(currentPhase) + // Add units for the current phase + if (currentPhase.state?.units) { + for (const [power, unitArr] of Object.entries(currentPhase.state.units)) { + unitArr.forEach(unitStr => { + const match = unitStr.match(/^([AF])\s+(.+)$/); + if (match) { + try { + let newUnit = createUnitMesh({ + power: power.toUpperCase(), + type: match[1], + province: match[2], + }); + gameState.scene.add(newUnit); + gameState.unitMeshes.push(newUnit); + } catch (error) { + logger.log(`Error creating unit: ${error.message}`); + } + } + }); + } + } - // Only animate if not the first phase - if (!isFirstPhase) { - createTweenAnimations(currentPhase, previousPhase); + // Update UI elements with smooth transitions + updateLeaderboard(currentPhase); + updateMapOwnership(currentPhase); + + // Add phase info to news banner if not already there + const phaseBannerText = `Phase: ${currentPhase.name}`; + addToNewsBanner(phaseBannerText); + + // Update info panel with current phase details + const phaseInfo = `Phase: ${currentPhase.name}\nSCs: ${currentPhase.state?.centers ? JSON.stringify(currentPhase.state.centers) : 'None'}\nUnits: ${currentPhase.state?.units ? JSON.stringify(currentPhase.state.units) : 'None'}`; + logger.log(phaseInfo); + logger.updateInfoPanel(); + + // Show messages with animation or immediately based on skipMessages flag + if (!skipMessages) { + updateChatWindows(currentPhase, true); } else { - logger.log("First phase - no previous phase to animate from"); - // Since we're not animating, mark messages as done gameState.messagesPlaying = false; } - - let msg = `Phase: ${currentPhase.name}\nSCs: ${JSON.stringify(currentPhase.state.centers)} \nUnits: ${currentPhase.state?.units ? JSON.stringify(currentPhase.state.units) : 'None'} ` - // Panel - // Add: Update info panel - logger.updateInfoPanel(); + // Only animate if not the first phase and animations are requested + if (!isFirstPhase && !skipMessages) { + if (previousPhase) { + createTweenAnimations(currentPhase, previousPhase); + } + } else { + logger.log("No animations for this phase transition"); + gameState.messagesPlaying = false; + } } +/** + * Display the initial phase without animations + * Used when first loading a game + */ +export function displayInitialPhase() { + displayPhase(0, true); +} + +/** + * Display a phase with animations + * Used during normal gameplay + */ +export function displayPhaseWithAnimation(index) { + displayPhase(index, false); +} /** * Advances to the next phase in the game sequence @@ -124,6 +134,9 @@ export function advanceToNextPhase() { return; } + // Reset the nextPhaseScheduled flag to allow scheduling the next phase + gameState.nextPhaseScheduled = false; + // Get current phase const currentPhase = gameState.gameData.phases[gameState.phaseIndex]; @@ -131,9 +144,6 @@ export function advanceToNextPhase() { console.log(`Processing phase transition for ${currentPhase.name}`); } - // Reset animation attempted flag for the next phase - gameState.animationAttempted = false; - // First show summary if available if (currentPhase.summary && currentPhase.summary.trim() !== '') { // Update the news banner with full summary @@ -164,18 +174,25 @@ function moveToNextPhase() { // Clear any existing animations to prevent overlap if (gameState.playbackTimer) { clearTimeout(gameState.playbackTimer); + gameState.playbackTimer = 0; } + + // Clear any existing animations gameState.unitAnimations = []; + + // Reset animation state + gameState.isAnimating = false; + gameState.messagesPlaying = false; // Advance the phase index - if (gameState.phaseIndex >= gameState.gameData.phases.length - 1) { + if (gameState.gameData && gameState.phaseIndex >= gameState.gameData.phases.length - 1) { gameState.phaseIndex = 0; logger.log("Reached end of game, looping back to start"); } else { gameState.phaseIndex++; } - if (config.isDebugMode) { + if (config.isDebugMode && gameState.gameData) { console.log(`Moving to phase ${gameState.gameData.phases[gameState.phaseIndex].name}`); }