Merge pull request #34 from Tylermarques/main

Lots of minor bugfixes
This commit is contained in:
AlxAI 2025-06-04 19:39:26 -07:00 committed by GitHub
commit 417e508300
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 212 additions and 478 deletions

View file

@ -0,0 +1,3 @@
**/*.zip
./node_modules/
./public/games/

View file

@ -0,0 +1 @@
./games/

View 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."

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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 => {

View file

@ -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();
}
}
});

View file

@ -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() {

View file

@ -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;
}

View file

@ -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",

View file

@ -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"]
}
};
});

View file

@ -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:

View file

@ -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 \