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
-
- - Austria: 0
- - England: 0
- - France: 0
- - Germany: 0
- - Italy: 0
- - Russia: 0
- - Turkey: 0
-
- `;
+ 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
+
+ - Austria: ${scCounts.AUSTRIA || 0}
+ - England: ${scCounts.ENGLAND || 0}
+ - France: ${scCounts.FRANCE || 0}
+ - Germany: ${scCounts.GERMANY || 0}
+ - Italy: ${scCounts.ITALY || 0}
+ - Russia: ${scCounts.RUSSIA || 0}
+ - Turkey: ${scCounts.TURKEY || 0}
+
+ `;
+
+ // 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}`);
}