mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-19 12:58:09 +00:00
commit
417e508300
13 changed files with 212 additions and 478 deletions
3
ai_animation/.dockerignore
Normal file
3
ai_animation/.dockerignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
**/*.zip
|
||||
./node_modules/
|
||||
./public/games/
|
||||
1
ai_animation/public/.dockerignore
Normal file
1
ai_animation/public/.dockerignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
./games/
|
||||
38
ai_animation/reorganize_games.sh
Executable file
38
ai_animation/reorganize_games.sh
Executable file
|
|
@ -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."
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = '<div class="no-data-message">No game data loaded. Please load a game to view relationships.</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string>();
|
||||
|
||||
// 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<string>();
|
||||
|
||||
// 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 = `<strong>Note:</strong> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> {
|
||||
|
|
@ -34,23 +57,21 @@ function loadFileFromServer(filePath: string): Promise<string> {
|
|||
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('<!DOCTYPE') || data.trim().startsWith('<html')) {
|
||||
throw new Error('Received HTML instead of JSON. Check the file path.');
|
||||
reject('Received HTML instead of JSON. Check the file path.');
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
|
|
@ -157,8 +178,8 @@ class GameState {
|
|||
playBtn.disabled = false;
|
||||
speedSelector.disabled = false;
|
||||
|
||||
// Set the poewr if the game specifies it, else random.
|
||||
this.currentPower = this.gameData.power !== undefined ? this.gameData.power : getRandomPower();
|
||||
// Set the power if the game specifies it, else random.
|
||||
this.currentPower = this.gameData.power !== undefined ? this.gameData.power : getRandomPower(this.gameData);
|
||||
|
||||
|
||||
const momentsFilePath = `./games/${this.gameId}/moments.json`;
|
||||
|
|
@ -273,18 +294,31 @@ class GameState {
|
|||
* Loads the next game in the order, reseting the board and gameState
|
||||
*/
|
||||
loadNextGame = () => {
|
||||
//
|
||||
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<void> => {
|
||||
|
||||
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 => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue