diff --git a/ai_animation/.dockerignore b/ai_animation/.dockerignore
new file mode 100644
index 0000000..f767f2a
--- /dev/null
+++ b/ai_animation/.dockerignore
@@ -0,0 +1,3 @@
+**/*.zip
+./node_modules/
+./public/games/
diff --git a/ai_animation/public/.dockerignore b/ai_animation/public/.dockerignore
new file mode 100644
index 0000000..33bb22a
--- /dev/null
+++ b/ai_animation/public/.dockerignore
@@ -0,0 +1 @@
+./games/
diff --git a/ai_animation/reorganize_games.sh b/ai_animation/reorganize_games.sh
new file mode 100755
index 0000000..0a3b741
--- /dev/null
+++ b/ai_animation/reorganize_games.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+# Navigate to the games directory
+cd "public/games" || exit 1
+
+# Initialize counter
+counter=0
+
+# Process each directory (excluding files)
+for dir in */; do
+ # Remove trailing slash from directory name
+ dir_name="${dir%/}"
+
+ # Skip if it's not a directory or if it's a numeric directory (already processed)
+ if [[ ! -d "$dir_name" ]] || [[ "$dir_name" =~ ^[0-9]+$ ]]; then
+ continue
+ fi
+
+ echo "Processing: $dir_name"
+
+ # Create empty file with same name as directory in parent directory
+ touch "../$dir_name"
+
+ # Check if lmvsgame.json exists and rename it to game.json
+ if [[ -f "$dir_name/lmvsgame.json" ]]; then
+ mv "$dir_name/lmvsgame.json" "$dir_name/game.json"
+ echo " Renamed lmvsgame.json to game.json"
+ fi
+
+ # Rename directory to integer
+ mv "$dir_name" "$counter"
+ echo " Renamed directory to: $counter"
+
+ # Increment counter
+ ((counter++))
+done
+
+echo "Reorganization complete. Processed $counter directories."
\ No newline at end of file
diff --git a/ai_animation/src/domElements/chatWindows.ts b/ai_animation/src/domElements/chatWindows.ts
index ade89f3..0f4a0a6 100644
--- a/ai_animation/src/domElements/chatWindows.ts
+++ b/ai_animation/src/domElements/chatWindows.ts
@@ -178,65 +178,6 @@ export function updateChatWindows(phase: any, stepMessages = false) {
}
});
gameState.messagesPlaying = false;
-
- // If instant chat is enabled during stepwise mode, immediately proceed to next phase logic
- if (stepMessages && config.isInstantMode) {
- // Trigger the same logic as the end of stepwise message display
- if (gameState.isPlaying && !gameState.isSpeaking) {
- if (gameState.gameData && gameState.gameData.phases) {
- const currentPhase = gameState.gameData.phases[gameState.phaseIndex];
-
- if (config.isDebugMode) {
- console.log(`Instant chat enabled - processing end of phase ${currentPhase.name}`);
- }
-
- // Show summary first if available
- if (currentPhase.summary?.trim()) {
- addToNewsBanner(`(${currentPhase.name}) ${currentPhase.summary}`);
- }
-
- // Get previous phase for animations
- const prevIndex = gameState.phaseIndex > 0 ? gameState.phaseIndex - 1 : null;
- const previousPhase = prevIndex !== null ? gameState.gameData.phases[prevIndex] : null;
-
- // 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(`Animating orders from ${previousPhase.name} to ${currentPhase.name}`);
- }
-
- // 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
- }
- }
- }
- }
- }
} else {
// Stepwise mode: show one message at a time, animating word-by-word
gameState.messagesPlaying = true;
@@ -259,64 +200,7 @@ export function updateChatWindows(phase: any, stepMessages = false) {
if (config.isDebugMode) {
console.log(`All messages displayed in ${Date.now() - messageStartTime}ms`);
}
-
gameState.messagesPlaying = false;
-
- // Only proceed if we're in playback mode and not speaking
- if (gameState.isPlaying && !gameState.isSpeaking) {
- if (gameState.gameData && gameState.gameData.phases) {
- const currentPhase = gameState.gameData.phases[gameState.phaseIndex];
-
- if (config.isDebugMode) {
- console.log(`Processing end of phase ${currentPhase.name}`);
- }
-
- // Show summary first if available
- if (currentPhase.summary?.trim()) {
- addToNewsBanner(`(${currentPhase.name}) ${currentPhase.summary}`);
- }
-
- // Get previous phase for animations
- const prevIndex = gameState.phaseIndex > 0 ? gameState.phaseIndex - 1 : null;
- const previousPhase = prevIndex !== null ? gameState.gameData.phases[prevIndex] : null;
-
- // 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(`Animating orders from ${previousPhase.name} to ${currentPhase.name}`);
- }
-
- // 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.effectivePlaybackSpeed + config.effectiveAnimationDuration); // 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.effectivePlaybackSpeed); // Only wait for summary, no animation delay
- }
- }
- }
- }
return;
}
diff --git a/ai_animation/src/domElements/relationshipPopup.ts b/ai_animation/src/domElements/relationshipPopup.ts
deleted file mode 100644
index b23f977..0000000
--- a/ai_animation/src/domElements/relationshipPopup.ts
+++ /dev/null
@@ -1,244 +0,0 @@
-import { relationshipsBtn } from '../domElements';
-import { gameState } from '../gameState';
-import { PowerENUM } from '../types/map';
-import { GameSchemaType } from '../types/gameState';
-import { renderRelationshipHistoryChartView, DisplayType } from '../components/rotatingDisplay';
-import { getPowerDisplayName } from '../utils/powerNames';
-
-// DOM element references
-let relationshipPopupContainer: HTMLElement | null = null;
-let relationshipContent: HTMLElement | null = null;
-let closeButton: HTMLElement | null = null;
-
-/**
- * Initialize the relationship popup by creating DOM elements and attaching event handlers
- */
-export function initRelationshipPopup(): void {
- // Create the container if it doesn't exist
- if (!document.getElementById('relationship-popup-container')) {
- createRelationshipPopupElements();
- }
-
- // Get references to the created elements
- relationshipPopupContainer = document.getElementById('relationship-popup-container');
- relationshipContent = document.getElementById('relationship-content');
- closeButton = document.getElementById('relationship-close-btn');
-
- // Add event listeners
- if (closeButton) {
- closeButton.addEventListener('click', hideRelationshipPopup);
- }
-
- // Add click handler for the relationships button
- if (relationshipsBtn) {
- relationshipsBtn.addEventListener('click', toggleRelationshipPopup);
- }
-}
-
-/**
- * Create all DOM elements needed for the relationship popup
- */
-function createRelationshipPopupElements(): void {
- const container = document.createElement('div');
- container.id = 'relationship-popup-container';
- container.className = 'relationship-popup-container';
-
- // Create header
- const header = document.createElement('div');
- header.className = 'relationship-header';
-
- const title = document.createElement('h2');
- title.textContent = 'Diplomatic Relations';
- header.appendChild(title);
-
- const closeBtn = document.createElement('button');
- closeBtn.id = 'relationship-close-btn';
- closeBtn.textContent = '×';
- closeBtn.title = 'Close Relationships Chart';
- header.appendChild(closeBtn);
-
- container.appendChild(header);
-
- // Create content container
- const content = document.createElement('div');
- content.id = 'relationship-content';
- content.className = 'relationship-content';
- container.appendChild(content);
-
- // Add to document
- document.body.appendChild(container);
-}
-
-/**
- * Toggle the visibility of the relationship popup
- */
-export function toggleRelationshipPopup(): void {
- if (relationshipPopupContainer) {
- if (relationshipPopupContainer.classList.contains('visible')) {
- hideRelationshipPopup();
- } else {
- showRelationshipPopup();
- }
- }
-}
-
-/**
- * Show the relationship popup
- */
-export function showRelationshipPopup(): void {
- if (relationshipPopupContainer && relationshipContent) {
- relationshipPopupContainer.classList.add('visible');
-
- // Only render if we have game data
- if (gameState.gameData) {
- renderRelationshipChart();
- } else {
- relationshipContent.innerHTML = '
No game data loaded. Please load a game to view relationships.
';
- }
- }
-}
-
-/**
- * Hide the relationship popup
- */
-export function hideRelationshipPopup(): void {
- if (relationshipPopupContainer) {
- relationshipPopupContainer.classList.remove('visible');
- }
-}
-
-/**
- * Render the relationship chart in the popup
- */
-function renderRelationshipChart(): void {
- if (!relationshipContent || !gameState.gameData) return;
-
- // Clear current content
- relationshipContent.innerHTML = '';
-
- // Get a list of powers that have relationship data
- const powersWithRelationships = new Set();
-
- // Check all phases for relationships
- if (gameState.gameData && gameState.gameData.phases) {
- // Debug what relationship data we have
-
- let hasRelationshipData = false;
- for (const phase of gameState.gameData.phases) {
- if (phase.agent_relationships) {
- hasRelationshipData = true;
- // Add powers that have relationship data defined
- Object.keys(phase.agent_relationships).forEach(power => {
- powersWithRelationships.add(power);
- });
- }
- }
-
- if (!hasRelationshipData) {
- console.log("No relationship data found in any phase");
- }
- }
-
- // Create a container for each power's relationship chart
- for (const power of Object.values(PowerENUM)) {
- // Skip any non-string values
- if (typeof power !== 'string') continue;
-
- // Check if this power has relationship data
- if (powersWithRelationships.has(power)) {
- const powerContainer = document.createElement('div');
- powerContainer.className = `power-relationship-container power-${power.toLowerCase()}`;
-
- const powerHeader = document.createElement('h3');
- powerHeader.className = `power-${power.toLowerCase()}`;
- powerHeader.textContent = getPowerDisplayName(power as PowerENUM);
- powerContainer.appendChild(powerHeader);
-
- const chartContainer = document.createElement('div');
- chartContainer.className = 'relationship-chart-container';
-
- // Use the existing chart rendering function
- renderRelationshipHistoryChartView(
- chartContainer,
- gameState.gameData,
- gameState.phaseIndex,
- power as PowerENUM
- );
-
- powerContainer.appendChild(chartContainer);
- relationshipContent.appendChild(powerContainer);
- }
- }
-
- // If no powers have relationship data, create message to say so
- if (powersWithRelationships.size === 0) {
- console.log("No relationship data found in game");
-
- // Create sample relationship data for all powers in the game
- const allPowers = new Set();
-
- // Find all powers from units and centers
- if (gameState.gameData && gameState.gameData.phases && gameState.gameData.phases.length > 0) {
- const currentPhase = gameState.gameData.phases[gameState.phaseIndex];
-
- if (currentPhase.state?.units) {
- Object.keys(currentPhase.state.units).forEach(power => allPowers.add(power));
- }
-
- if (currentPhase.state?.centers) {
- Object.keys(currentPhase.state.centers).forEach(power => allPowers.add(power));
- }
-
- // Only proceed if we found some powers
- if (allPowers.size > 0) {
- console.log(`Found ${allPowers.size} powers in game, creating sample relationships`);
-
- // For each power, create a container and chart
- for (const power of allPowers) {
- const powerContainer = document.createElement('div');
- powerContainer.className = `power-relationship-container power-${power.toLowerCase()}`;
-
- const powerHeader = document.createElement('h3');
- powerHeader.className = `power-${power.toLowerCase()}`;
- powerHeader.textContent = getPowerDisplayName(power as PowerENUM);
- powerContainer.appendChild(powerHeader);
-
- const chartContainer = document.createElement('div');
- chartContainer.className = 'relationship-chart-container';
-
- // Create a message about sample data
- const sampleMessage = document.createElement('div');
- sampleMessage.className = 'sample-data-message';
- sampleMessage.innerHTML = `Note: No relationship data found for ${power}.
- This chart will display when relationship data is available.`;
-
- chartContainer.appendChild(sampleMessage);
- powerContainer.appendChild(chartContainer);
- relationshipContent.appendChild(powerContainer);
- }
- } else {
- // If we couldn't find any powers, show the no data message
- const noDataMsg = document.createElement('div');
- noDataMsg.className = 'no-data-message';
- noDataMsg.textContent = 'No relationship data available in this game file.';
- relationshipContent.appendChild(noDataMsg);
- }
- } else {
- // If no phases, show the no data message
- const noDataMsg = document.createElement('div');
- noDataMsg.className = 'no-data-message';
- noDataMsg.textContent = 'No relationship data available in this game file.';
- relationshipContent.appendChild(noDataMsg);
- }
- }
-}
-
-/**
- * Update the relationship popup when game data changes
- */
-export function updateRelationshipPopup(): void {
- if (relationshipPopupContainer &&
- relationshipPopupContainer.classList.contains('visible')) {
- renderRelationshipChart();
- }
-}
diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts
index 791418e..cac5d54 100644
--- a/ai_animation/src/gameState.ts
+++ b/ai_animation/src/gameState.ts
@@ -7,10 +7,10 @@ import { prevBtn, nextBtn, playBtn, speedSelector, mapView, updateGameIdDisplay
import { createChatWindows } from "./domElements/chatWindows";
import { logger } from "./logger";
import { OrbitControls } from "three/examples/jsm/Addons.js";
-import { displayInitialPhase } from "./phase";
+import { displayInitialPhase, togglePlayback } from "./phase";
import { Tween, Group as TweenGroup } from "@tweenjs/tween.js";
import { hideStandingsBoard, } from "./domElements/standingsBoard";
-import { MomentsDataSchema, MomentsDataSchemaType, Moment, NormalizedMomentsData } from "./types/moments";
+import { MomentsDataSchema, Moment, NormalizedMomentsData } from "./types/moments";
//FIXME: This whole file is a mess. Need to organize and format
@@ -20,13 +20,36 @@ enum AvailableMaps {
/**
* Return a random power from the PowerENUM for the player to control
+ * Only returns powers that have more than 2 supply centers in the last phase
*/
-function getRandomPower(): PowerENUM {
- const values = Object.values(PowerENUM).filter(power =>
+function getRandomPower(gameData?: GameSchemaType): PowerENUM {
+ const allPowers = Object.values(PowerENUM).filter(power =>
power !== PowerENUM.GLOBAL && power !== PowerENUM.EUROPE
);
- const idx = Math.floor(Math.random() * values.length);
- return values[idx];
+
+ // If no game data provided, return any random power
+ if (!gameData || !gameData.phases || gameData.phases.length === 0) {
+ const idx = Math.floor(Math.random() * allPowers.length);
+ return allPowers[idx];
+ }
+
+ // Get the last phase to check supply centers
+ const lastPhase = gameData.phases[gameData.phases.length - 1];
+
+ // Filter powers that have more than 2 supply centers
+ const eligiblePowers = allPowers.filter(power => {
+ const centers = lastPhase.state?.centers?.[power];
+ return centers && centers.length > 2;
+ });
+
+ // If no powers have more than 2 centers, fall back to any power
+ if (eligiblePowers.length === 0) {
+ const idx = Math.floor(Math.random() * allPowers.length);
+ return allPowers[idx];
+ }
+
+ const idx = Math.floor(Math.random() * eligiblePowers.length);
+ return eligiblePowers[idx];
}
function loadFileFromServer(filePath: string): Promise {
@@ -34,23 +57,21 @@ function loadFileFromServer(filePath: string): Promise {
fetch(filePath)
.then(response => {
if (!response.ok) {
- alert(`Couldn't load file, received reponse code ${response.status}`)
- throw new Error(`Failed to load file: ${response.status}`);
+ reject(`Failed to load file: ${response.status}`);
}
// FIXME: This occurs because the server seems to resolve any URL to the homepage. This is the case for Vite's Dev Server.
// Check content type to avoid HTML errors
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('text/html')) {
- alert(`Unable to load file ${filePath}, was presented HTML, contentType ${contentType}`)
- throw new Error('Received HTML instead of JSON. Check the file path.');
+ reject('Received HTML instead of JSON. Check the file path.');
}
return response.text();
})
.then(data => {
// Check for HTML content as a fallback
if (data.trim().startsWith(' {
- //
+ let gameId = this.gameId + 1
+ let contPlaying = false
+ if (this.isPlaying) {
+ contPlaying = true
+ }
+ this.loadGameFile(gameId).then(() => {
- this.gameId += 1
+ if (contPlaying) {
+ togglePlayback(true)
+ }
+ }).catch(() => {
+ console.warn("caught error trying to advance game. Setting gameId to 0 and restarting...")
+ this.loadGameFile(0)
+ if (contPlaying) {
+ togglePlayback(true)
+ }
+ })
- // Try to load the next game, if it fails, show end screen forever
}
/*
* Given a gameId, load that game's state into the GameState Object
*/
- loadGameFile = (gameId: number) => {
+ loadGameFile = (gameId: number): Promise => {
if (gameId === null || gameId < 0) {
throw Error(`Attempted to load game with invalid ID ${gameId}`)
@@ -292,25 +326,29 @@ class GameState {
// Path to the default game file
const gameFilePath = `./games/${gameId}/game.json`;
- loadFileFromServer(gameFilePath).then((data) => {
- this.gameId = gameId
+ return new Promise((resolve, reject) => {
+ loadFileFromServer(gameFilePath).then((data) => {
- return this.loadGameData(data);
- })
- .then(() => {
- console.log("Default game file loaded and parsed successfully");
- // Explicitly hide standings board after loading game
- hideStandingsBoard();
- // Update rotating display and relationship popup with game data
- if (this.gameData) {
- updateRotatingDisplay(this.gameData, this.phaseIndex, this.currentPower);
- updateGameIdDisplay();
- }
+ return this.loadGameData(data);
})
- .catch(error => {
- // Use console.error instead of logger.log to avoid updating the info panel
- console.error(`Error loading game ${gameFilePath}: ${error.message}`);
- });
+ .then(() => {
+ console.log(`Game file with id ${gameId} loaded and parsed successfully`);
+ // Explicitly hide standings board after loading game
+ hideStandingsBoard();
+ // Update rotating display and relationship popup with game data
+ if (this.gameData) {
+ updateRotatingDisplay(this.gameData, this.phaseIndex, this.currentPower);
+ this.gameId = gameId
+ updateGameIdDisplay();
+ resolve()
+ }
+ })
+ .catch(error => {
+ // Use console.error instead of logger.log to avoid updating the info panel
+ console.error(`Error loading game ${gameFilePath}: ${error}`);
+ reject()
+ });
+ })
}
checkPhaseHasMoment = (phaseName: string): Moment | null => {
diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts
index 9eebe84..fa917a8 100644
--- a/ai_animation/src/main.ts
+++ b/ai_animation/src/main.ts
@@ -6,7 +6,7 @@ import { logger } from "./logger";
import { loadBtn, prevBtn, nextBtn, speedSelector, fileInput, playBtn, mapView, loadGameBtnFunction } from "./domElements";
import { updateChatWindows } from "./domElements/chatWindows";
import { initStandingsBoard, hideStandingsBoard, showStandingsBoard } from "./domElements/standingsBoard";
-import { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase, nextPhase, previousPhase } from "./phase";
+import { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase, nextPhase, previousPhase, togglePlayback } from "./phase";
import { config } from "./config";
import { Tween, Group, Easing } from "@tweenjs/tween.js";
import { initRotatingDisplay, updateRotatingDisplay } from "./components/rotatingDisplay";
@@ -141,14 +141,24 @@ function animate() {
// Call update on each active animation
gameState.unitAnimations.forEach((anim) => anim.update())
- // If all animations are complete and we're in playback mode
- if (gameState.unitAnimations.length === 0 && gameState.isPlaying && !gameState.messagesPlaying && !gameState.isSpeaking) {
- // Schedule next phase after a pause delay
- console.log(`Scheduling next phase in ${config.effectivePlaybackSpeed}ms`);
- gameState.playbackTimer = setTimeout(() => advanceToNextPhase(), config.effectivePlaybackSpeed);
- }
}
+ // If all animations are complete and we're in playback mode
+ if (gameState.unitAnimations.length === 0 && gameState.isPlaying && !gameState.messagesPlaying && !gameState.isSpeaking && !gameState.nextPhaseScheduled) {
+ // Schedule next phase after a pause delay
+ console.log(`Scheduling next phase in ${config.effectivePlaybackSpeed}ms`);
+ gameState.nextPhaseScheduled = true;
+ gameState.playbackTimer = setTimeout(() => {
+ try {
+ advanceToNextPhase()
+ } catch {
+ // FIXME: This is a dumb patch for us not being able to find the unit we expect to find.
+ // We should instead bee figuring out why units aren't where we expect them to be when the engine has said that is a valid move
+ nextPhase()
+ gameState.nextPhaseScheduled;
+ }
+ }, config.effectivePlaybackSpeed);
+ }
// Update any pulsing or wave animations on supply centers or units
if (gameState.scene.userData.animatedObjects) {
gameState.scene.userData.animatedObjects.forEach(obj => {
@@ -186,58 +196,6 @@ function onWindowResize() {
-// --- PLAYBACK CONTROLS ---
-function togglePlayback() {
- // If the game doesn't have any data, or there are no phases, return;
- if (!gameState.gameData || gameState.gameData.phases.length <= 0) {
- alert("This game file appears to be broken. Please reload the page and load a different game.")
- throw Error("Bad gameState, exiting.")
- };
-
- // TODO: Likely not how we want to handle the speaking section of this.
- // Should be able to pause the other elements while we're speaking
- if (gameState.isSpeaking) return;
-
- gameState.isPlaying = !gameState.isPlaying;
-
- if (gameState.isPlaying) {
- playBtn.textContent = "⏸ Pause";
- prevBtn.disabled = true;
- nextBtn.disabled = true;
- logger.log("Starting playback...");
-
- if (gameState.cameraPanAnim) gameState.cameraPanAnim.getAll()[1].start()
- // Hide standings board when playback starts
- hideStandingsBoard();
-
- // Update rotating display
- if (gameState.gameData) {
- updateRotatingDisplay(gameState.gameData, gameState.phaseIndex, gameState.currentPower);
- }
-
- // First, show the messages of the current phase if it's the initial playback
- const phase = gameState.gameData.phases[gameState.phaseIndex];
- if (phase.messages && phase.messages.length) {
- // Show messages with stepwise animation
- logger.log(`Playing ${phase.messages.length} messages from phase ${gameState.phaseIndex + 1}/${gameState.gameData.phases.length}`);
- updateChatWindows(phase, true);
- } else {
- // No messages, go straight to unit animations
- logger.log("No messages for this phase, proceeding to animations");
- displayPhaseWithAnimation();
- }
- } else {
- if (gameState.cameraPanAnim) gameState.cameraPanAnim.getAll()[0].pause();
- playBtn.textContent = "▶ Play";
- if (gameState.playbackTimer) {
- clearTimeout(gameState.playbackTimer);
- gameState.playbackTimer = null;
- }
- gameState.messagesPlaying = false;
- prevBtn.disabled = false;
- nextBtn.disabled = false;
- }
-}
@@ -252,7 +210,6 @@ fileInput.addEventListener('change', e => {
// Update rotating display and relationship popup with game data
if (gameState.gameData) {
updateRotatingDisplay(gameState.gameData, gameState.phaseIndex, gameState.currentPower);
- updateRelationshipPopup();
}
}
});
diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts
index bbcbaa3..2ad090e 100644
--- a/ai_animation/src/phase.ts
+++ b/ai_animation/src/phase.ts
@@ -1,6 +1,6 @@
import { gameState } from "./gameState";
import { logger } from "./logger";
-import { updatePhaseDisplay } from "./domElements";
+import { updatePhaseDisplay, playBtn, prevBtn, nextBtn } from "./domElements";
import { initUnits } from "./units/create";
import { updateSupplyCenterOwnership, updateLeaderboard, updateMapOwnership as _updateMapOwnership, updateMapOwnership } from "./map/state";
import { updateChatWindows, addToNewsBanner } from "./domElements/chatWindows";
@@ -11,6 +11,7 @@ import { debugMenuInstance } from "./debug/debugMenu";
import { showTwoPowerConversation, closeTwoPowerConversation } from "./components/twoPowerConversation";
import { closeVictoryModal, showVictoryModal } from "./components/victoryModal";
import { notifyPhaseChange } from "./webhooks/phaseNotifier";
+import { updateRotatingDisplay } from "./components/rotatingDisplay";
const MOMENT_THRESHOLD = 8.0
// If we're in debug mode or instant mode, show it quick, otherwise show it for 30 seconds
@@ -60,6 +61,7 @@ export function _setPhase(phaseIndex: number) {
} else {
displayPhase()
}
+ gameState.nextPhaseScheduled = false;
}
// Finally, update the gameState with the current phaseIndex
@@ -69,6 +71,59 @@ export function _setPhase(phaseIndex: number) {
notifyPhaseChange(oldPhaseIndex, phaseIndex);
}
+// --- PLAYBACK CONTROLS ---
+export function togglePlayback(explicitSet: boolean) {
+ // If the game doesn't have any data, or there are no phases, return;
+ if (!gameState.gameData || gameState.gameData.phases.length <= 0) {
+ alert("This game file appears to be broken. Please reload the page and load a different game.")
+ throw Error("Bad gameState, exiting.")
+ };
+
+ // TODO: Likely not how we want to handle the speaking section of this.
+ // Should be able to pause the other elements while we're speaking
+ if (gameState.isSpeaking) return;
+
+ gameState.isPlaying = !gameState.isPlaying;
+ if (typeof explicitSet === "boolean") {
+ gameState.isPlaying = explicitSet
+ }
+
+ if (gameState.isPlaying) {
+ playBtn.textContent = "⏸ Pause";
+ prevBtn.disabled = true;
+ nextBtn.disabled = true;
+ logger.log("Starting playback...");
+
+ if (gameState.cameraPanAnim) gameState.cameraPanAnim.getAll()[1].start()
+
+ // Update rotating display
+ if (gameState.gameData) {
+ updateRotatingDisplay(gameState.gameData, gameState.phaseIndex, gameState.currentPower);
+ }
+
+ // First, show the messages of the current phase if it's the initial playback
+ const phase = gameState.gameData.phases[gameState.phaseIndex];
+ if (phase.messages && phase.messages.length) {
+ // Show messages with stepwise animation
+ logger.log(`Playing ${phase.messages.length} messages from phase ${gameState.phaseIndex + 1}/${gameState.gameData.phases.length}`);
+ updateChatWindows(phase, true);
+ } else {
+ // No messages, go straight to unit animations
+ logger.log("No messages for this phase, proceeding to animations");
+ displayPhaseWithAnimation();
+ }
+ } else {
+ if (gameState.cameraPanAnim) gameState.cameraPanAnim.getAll()[0].pause();
+ playBtn.textContent = "▶ Play";
+ if (gameState.playbackTimer) {
+ clearTimeout(gameState.playbackTimer);
+ gameState.playbackTimer = null;
+ }
+ gameState.messagesPlaying = false;
+ prevBtn.disabled = false;
+ nextBtn.disabled = false;
+ }
+}
export function nextPhase() {
@@ -146,7 +201,7 @@ export function displayPhase(skipMessages = false) {
_updateMapOwnership();
// Add phase info to news banner if not already there
- const phaseBannerText = `Phase: ${currentPhase.name}`;
+ const phaseBannerText = `Phase: ${currentPhase.name}: ${currentPhase.summary}`;
addToNewsBanner(phaseBannerText);
// Log phase details to console only, don't update info panel with this
@@ -207,8 +262,6 @@ export function advanceToNextPhase() {
logger.log("Cannot advance phase: invalid game state");
return;
}
- // Reset the nextPhaseScheduled flag to allow scheduling the next phase
- gameState.nextPhaseScheduled = false;
// Get current phase
const currentPhase = gameState.gameData.phases[gameState.phaseIndex];
@@ -224,10 +277,6 @@ export function advanceToNextPhase() {
// First show summary if available
if (currentPhase.summary && currentPhase.summary.trim() !== '') {
- // Update the news banner with full summary
- addToNewsBanner(`(${currentPhase.name}) ${currentPhase.summary}`);
- console.log("Added summary to news banner, preparing to call speakSummary");
-
// Speak the summary and advance after
if (!gameState.isSpeaking) {
speakSummary(currentPhase.summary)
@@ -242,6 +291,8 @@ export function advanceToNextPhase() {
if (gameState.isPlaying) {
nextPhase();
}
+ }).finally(() => {
+
});
} else {
console.error("Attempted to start speaking when already speaking...")
@@ -251,6 +302,9 @@ export function advanceToNextPhase() {
// No summary to speak, advance immediately
nextPhase();
}
+
+ // Reset the nextPhaseScheduled flag to allow scheduling the next phase
+ gameState.nextPhaseScheduled = false;
}
function displayFinalPhase() {
diff --git a/ai_animation/src/utils/powerNames.ts b/ai_animation/src/utils/powerNames.ts
index 4cfdc81..7e9b7e2 100644
--- a/ai_animation/src/utils/powerNames.ts
+++ b/ai_animation/src/utils/powerNames.ts
@@ -25,7 +25,7 @@ export function getPowerDisplayName(power: PowerENUM | string): string {
// Remove the extra long parts of some of the model names e.g. openroute-meta/llama-4-maverick -> llama-4-maverick
let slashIdx = modelName?.indexOf("/")
if (slashIdx >= 0) {
- return modelName.slice(0, slashIdx)
+ return modelName.slice(slashIdx + 1, modelName.length)
}
return modelName;
}
diff --git a/ai_animation/tsconfig.json b/ai_animation/tsconfig.json
index bb15751..3bba997 100644
--- a/ai_animation/tsconfig.json
+++ b/ai_animation/tsconfig.json
@@ -9,7 +9,7 @@
"DOM.Iterable"
],
// Turn the below off later, just for now we're not full TS yet
- //"noCheck": true,
+ "noCheck": true,
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
diff --git a/ai_animation/vite.config.js b/ai_animation/vite.config.js
index 32355d9..dc2ada2 100644
--- a/ai_animation/vite.config.js
+++ b/ai_animation/vite.config.js
@@ -4,10 +4,10 @@ import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
// Load environment variables
const env = loadEnv(mode, process.cwd(), '');
-
+
console.log('Environment mode:', mode);
console.log('Environment variables loaded:', Object.keys(env).filter(key => key.startsWith('VITE_')));
-
+
return {
// Define environment variables that should be available in the client
define: {
@@ -19,10 +19,10 @@ export default defineConfig(({ mode }) => {
},
// Server configuration
"preview": {
- "allowedHosts": ["diplomacy"]
+ "allowedHosts": ["diplomacy", "archlinux"]
},
"dev": {
- "allowedHosts": ["diplomacy"]
+ "allowedHosts": ["diplomacy", "archlinux"]
}
};
});
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 933c8f8..1342c25 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -8,12 +8,16 @@ services:
- DISPLAY=:99
ports:
- "9222:9222"
+ ipc: host
+ shm_size: "1gb"
diplomacy:
build: ai_animation
ports:
- "4173:4173"
- "5173:5173"
+ volumes:
+ - ./ai_animation/public/games/:/app/dist/games
diplomacy-dev:
build: ai_animation
ports:
diff --git a/twitch-streamer/entrypoint.sh b/twitch-streamer/entrypoint.sh
index 1fa2fcc..93a3e75 100755
--- a/twitch-streamer/entrypoint.sh
+++ b/twitch-streamer/entrypoint.sh
@@ -20,15 +20,15 @@ sleep 2
mkdir -p /home/chrome
# Launch Chrome in the background, pointing at your site.
-# --app=... to open it as a single-window "app"
-# --no-sandbox / --disable-gpu often needed in Docker
-# --use-fake-device-for-media-stream / etc. if you need to simulate mic/cam
+# --disable-background-timer-throttling & related flags to prevent fps throttling in headless/Xvfb
DISPLAY=$DISPLAY google-chrome \
--remote-debugging-port=9222 \
--disable-gpu \
- --disable-dev-shm-usage \
- --no-first-run \
--disable-infobars \
+ --no-first-run \
+ --disable-background-timer-throttling \
+ --disable-renderer-backgrounding \
+ --disable-backgrounding-occluded-windows \
--user-data-dir=/home/chrome/chrome-data \
--window-size=1920,1080 --window-position=0,0 \
--kiosk \
@@ -37,13 +37,12 @@ DISPLAY=$DISPLAY google-chrome \
sleep 5 # let the page load or animations start
# Start streaming with FFmpeg.
-# - For video: x11grab from DISPLAY
-# - For audio: pulse from the "default" device
-# Adjust your bitrate, resolution, frame rate, etc. as desired.
+# - For video: x11grab at 30fps
+# - For audio: pulse from the default device
exec ffmpeg -y \
- -f x11grab -thread_queue_size 512 -r 30 -s 1920x1080 -i $DISPLAY \
+ -f x11grab -video_size 1920x1080 -framerate 30 -thread_queue_size 512 -i $DISPLAY \
-f pulse -thread_queue_size 512 -i default \
- -c:v libx264 -preset veryfast -b:v 6000k -maxrate 6000k -bufsize 12000k \
+ -c:v libx264 -preset ultrafast -b:v 6000k -maxrate 6000k -bufsize 12000k \
-pix_fmt yuv420p \
-c:a aac -b:a 160k \
-vsync 1 -async 1 \