diff --git a/ai_animation/src/components/endScreenModal.ts b/ai_animation/src/components/endScreenModal.ts deleted file mode 100644 index 7f03568..0000000 --- a/ai_animation/src/components/endScreenModal.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { gameState } from '../gameState'; - -interface EndScreenModalOptions { - totalGamesPlayed: number; - onRestart?: () => void; -} - -let endScreenModalOverlay: HTMLElement | null = null; - -/** - * Shows an end screen modal when all games have been completed - * @param options Configuration for the end screen modal - */ -export function showEndScreenModal(options: EndScreenModalOptions): void { - const { totalGamesPlayed, onRestart } = options; - - // Close any existing modal - closeEndScreenModal(); - - // Create overlay - endScreenModalOverlay = createEndScreenOverlay(); - - // Create modal container - const modalContainer = createEndScreenContainer(totalGamesPlayed); - - // Add close button if not in playing mode - if (!gameState.isPlaying) { - const closeButton = createCloseButton(); - modalContainer.appendChild(closeButton); - } - - // Add to overlay - endScreenModalOverlay.appendChild(modalContainer); - document.body.appendChild(endScreenModalOverlay); - - // Set up event listeners - setupEventListeners(onRestart); - - // Auto-restart after 5 seconds - setTimeout(() => { - closeEndScreenModal(); - onRestart?.(); - }, 5000); -} - -/** - * Closes the end screen modal - */ -export function closeEndScreenModal(): void { - if (endScreenModalOverlay) { - endScreenModalOverlay.classList.add('fade-out'); - setTimeout(() => { - if (endScreenModalOverlay?.parentNode) { - endScreenModalOverlay.parentNode.removeChild(endScreenModalOverlay); - } - endScreenModalOverlay = null; - }, 300); - } -} - -/** - * Creates the main overlay element - */ -function createEndScreenOverlay(): HTMLElement { - const overlay = document.createElement('div'); - overlay.className = 'end-screen-modal-overlay'; - overlay.style.cssText = ` - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.9); - display: flex; - justify-content: center; - align-items: center; - z-index: 2000; - opacity: 0; - transition: opacity 0.5s ease; - `; - - // Add fade-out class styles - const style = document.createElement('style'); - style.textContent = ` - .end-screen-modal-overlay.fade-out { - opacity: 0 !important; - transition: opacity 0.3s ease; - } - `; - document.head.appendChild(style); - - // Trigger fade in - setTimeout(() => overlay.style.opacity = '1', 10); - - return overlay; -} - -/** - * Creates the main end screen container - */ -function createEndScreenContainer(totalGamesPlayed: number): HTMLElement { - const container = document.createElement('div'); - container.className = 'end-screen-modal-container'; - container.style.cssText = ` - background: radial-gradient(ellipse at center, #2a1f1f 0%, #1a1015 100%); - border: 5px solid #8b7355; - border-radius: 12px; - box-shadow: 0 0 40px rgba(255, 215, 0, 0.6); - width: 90%; - max-width: 600px; - position: relative; - padding: 40px; - text-align: center; - font-family: "Book Antiqua", Palatino, serif; - color: #f0e6d2; - animation: endScreenGlow 3s ease-in-out infinite alternate; - `; - - // Add glow animation - const style = document.createElement('style'); - style.textContent = ` - @keyframes endScreenGlow { - 0% { box-shadow: 0 0 40px rgba(255, 215, 0, 0.6); } - 100% { box-shadow: 0 0 60px rgba(255, 215, 0, 0.8); } - } - `; - document.head.appendChild(style); - - // Crown/completion icon - const crownElement = document.createElement('div'); - crownElement.textContent = '👑'; - crownElement.style.cssText = ` - font-size: 64px; - margin-bottom: 20px; - animation: float 3s ease-in-out infinite; - `; - - // Add float animation - const floatStyle = document.createElement('style'); - floatStyle.textContent = ` - @keyframes float { - 0%, 100% { transform: translateY(0px); } - 50% { transform: translateY(-10px); } - } - `; - document.head.appendChild(floatStyle); - - // Title - const titleElement = document.createElement('h1'); - titleElement.textContent = 'SERIES COMPLETE!'; - titleElement.style.cssText = ` - margin: 0 0 20px 0; - color: #ffd700; - font-size: 42px; - font-weight: bold; - text-shadow: 3px 3px 6px rgba(0,0,0,0.7); - letter-spacing: 2px; - `; - - // Completion message - const messageElement = document.createElement('h2'); - messageElement.textContent = `All ${totalGamesPlayed} games completed!`; - messageElement.style.cssText = ` - margin: 0 0 30px 0; - color: #f0e6d2; - font-size: 24px; - font-weight: normal; - text-shadow: 2px 2px 4px rgba(0,0,0,0.5); - `; - - // Restart countdown - const countdownElement = document.createElement('div'); - countdownElement.style.cssText = ` - background: linear-gradient(90deg, #4a4a4a 0%, #2a2a2a 100%); - color: #ffd700; - padding: 15px 25px; - border-radius: 25px; - display: inline-block; - font-size: 18px; - font-weight: bold; - margin-bottom: 20px; - border: 2px solid #8b7355; - box-shadow: 0 4px 8px rgba(0,0,0,0.4); - `; - - // Animate countdown - let countdown = 5; - countdownElement.textContent = `Restarting series in ${countdown} seconds...`; - - const countdownInterval = setInterval(() => { - countdown--; - if (countdown > 0) { - countdownElement.textContent = `Restarting series in ${countdown} seconds...`; - } else { - countdownElement.textContent = 'Restarting now...'; - clearInterval(countdownInterval); - } - }, 1000); - - // Thank you message - const thankYouElement = document.createElement('div'); - thankYouElement.textContent = 'Thank you for watching the AI Diplomacy series!'; - thankYouElement.style.cssText = ` - color: #c4b998; - font-size: 16px; - font-style: italic; - margin-top: 20px; - `; - - // Assemble container - container.appendChild(crownElement); - container.appendChild(titleElement); - container.appendChild(messageElement); - container.appendChild(countdownElement); - container.appendChild(thankYouElement); - - return container; -} - -/** - * Creates a close button - */ -function createCloseButton(): HTMLElement { - const button = document.createElement('button'); - button.textContent = '×'; - button.className = 'end-screen-close-button'; - button.style.cssText = ` - position: absolute; - top: 15px; - right: 20px; - background: none; - border: none; - font-size: 40px; - color: #8b7355; - cursor: pointer; - padding: 0; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - transition: all 0.2s ease; - `; - - button.addEventListener('mouseenter', () => { - button.style.color = '#ffd700'; - button.style.transform = 'scale(1.1)'; - button.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; - }); - - button.addEventListener('mouseleave', () => { - button.style.color = '#8b7355'; - button.style.transform = 'scale(1)'; - button.style.backgroundColor = 'transparent'; - }); - - return button; -} - -/** - * Sets up event listeners for the end screen modal - */ -function setupEventListeners(onRestart?: () => void): void { - if (!endScreenModalOverlay) return; - - const closeButton = endScreenModalOverlay.querySelector('.end-screen-close-button'); - const handleClose = () => { - closeEndScreenModal(); - onRestart?.(); - }; - - // Close button click (only if not in playing mode) - if (!gameState.isPlaying) { - closeButton?.addEventListener('click', handleClose); - - // Escape key - const handleKeydown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - handleClose(); - document.removeEventListener('keydown', handleKeydown); - } - }; - document.addEventListener('keydown', handleKeydown); - - // Click outside to close - endScreenModalOverlay.addEventListener('click', (e) => { - if (e.target === endScreenModalOverlay) { - handleClose(); - } - }); - } -} \ No newline at end of file diff --git a/ai_animation/src/components/rotatingDisplay.ts b/ai_animation/src/components/rotatingDisplay.ts index 8325072..66b7d0b 100644 --- a/ai_animation/src/components/rotatingDisplay.ts +++ b/ai_animation/src/components/rotatingDisplay.ts @@ -79,9 +79,9 @@ function rotateToNextDisplay(): void { // Determine next display type switch (currentDisplayType) { - // case DisplayType.SC_HISTORY_CHART: - // currentDisplayType = DisplayType.RELATIONSHIP_HISTORY_CHART; - // break; + case DisplayType.SC_HISTORY_CHART: + currentDisplayType = DisplayType.RELATIONSHIP_HISTORY_CHART; + break; case DisplayType.RELATIONSHIP_HISTORY_CHART: currentDisplayType = DisplayType.SC_HISTORY_CHART; break; @@ -155,6 +155,9 @@ function renderSCHistoryChartView( container: HTMLElement, gameData: GameSchemaType ): void { + + // TODO: This likely needs to not be a custom rendered svg. There are plenty of charting libraries to use to do this instead. + // // Create header const header = document.createElement('div'); header.innerHTML = `Supply Center History`; diff --git a/ai_animation/src/components/victoryModal.ts b/ai_animation/src/components/victoryModal.ts index e438f5a..828a795 100644 --- a/ai_animation/src/components/victoryModal.ts +++ b/ai_animation/src/components/victoryModal.ts @@ -40,13 +40,6 @@ export function showVictoryModal(options: VictoryModalOptions): void { // Set up event listeners setupEventListeners(onClose); - // Auto-dismiss in playing mode after delay - if (gameState.isPlaying) { - setTimeout(() => { - closeVictoryModal(); - onClose?.(); - }, 5000); // 5 second display in playing mode - } } /** @@ -55,12 +48,10 @@ export function showVictoryModal(options: VictoryModalOptions): void { export function closeVictoryModal(): void { if (victoryModalOverlay) { victoryModalOverlay.classList.add('fade-out'); - setTimeout(() => { - if (victoryModalOverlay?.parentNode) { - victoryModalOverlay.parentNode.removeChild(victoryModalOverlay); - } - victoryModalOverlay = null; - }, 300); + if (victoryModalOverlay?.parentNode) { + victoryModalOverlay.parentNode.removeChild(victoryModalOverlay); + } + victoryModalOverlay = null; } } @@ -249,7 +240,7 @@ function createStandingsElement(finalStandings: Array<{ power: string; centers: const standingItem = document.createElement('div'); const medal = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : `${index + 1}.`; const displayName = getPowerDisplayName(entry.power as PowerENUM); - + standingItem.innerHTML = ` ${medal} ${displayName} @@ -346,4 +337,4 @@ function setupEventListeners(onClose?: () => void): void { } }); } -} \ No newline at end of file +} diff --git a/ai_animation/src/config.ts b/ai_animation/src/config.ts index a0fb831..3f19f25 100644 --- a/ai_animation/src/config.ts +++ b/ai_animation/src/config.ts @@ -4,7 +4,7 @@ export const config = { // Default speed in milliseconds for animations and transitions playbackSpeed: 500, - + victoryModalDisplayMs: 10000, // Streaming mode specific timing get streamingPlaybackSpeed(): number { const isStreamingMode = import.meta.env.MODE === 'production' diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index d79884f..37abf5a 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -2,6 +2,8 @@ import * as THREE from "three" import { updateRotatingDisplay } from "./components/rotatingDisplay"; import { type CoordinateData, CoordinateDataSchema, PowerENUM } from "./types/map" import type { GameSchemaType } from "./types/gameState"; +import { debugMenuInstance } from "./debug/debugMenu.ts" +import { config } from "./config.ts" import { GameSchema } from "./types/gameState"; import { prevBtn, nextBtn, playBtn, speedSelector, mapView, updateGameIdDisplay } from "./domElements"; import { createChatWindows } from "./domElements/chatWindows"; @@ -11,6 +13,7 @@ import { displayInitialPhase, togglePlayback } from "./phase"; import { Tween, Group as TweenGroup } from "@tweenjs/tween.js"; import { MomentsDataSchema, Moment, NormalizedMomentsData } from "./types/moments"; import { updateLeaderboard } from "./components/leaderboard"; +import { closeVictoryModal } from "./components/victoryModal.ts"; //FIXME: This whole file is a mess. Need to organize and format @@ -123,7 +126,7 @@ class GameState { this.phaseIndex = 0 this.boardName = boardName this.currentPower = null; - this.gameId = 12 + this.gameId = 16 this.momentsData = null; // Initialize as null, will be loaded later // State locks @@ -226,6 +229,7 @@ class GameState { // Update game ID display updateGameIdDisplay(); + }) resolve() } else { @@ -300,16 +304,19 @@ class GameState { /* * Loads the next game in the order, reseting the board and gameState */ - loadNextGame = () => { + loadNextGame = (setPlayback: boolean = false) => { + let gameId = this.gameId + 1 let contPlaying = false - if (this.isPlaying) { + if (setPlayback || this.isPlaying) { contPlaying = true } this.loadGameFile(gameId).then(() => { - + gameState.gameId = gameId if (contPlaying) { - togglePlayback(true) + setTimeout(() => { + togglePlayback(true) + }, config.victoryModalDisplayMs) } }).catch(() => { console.warn("caught error trying to advance game. Setting gameId to 0 and restarting...") @@ -317,7 +324,7 @@ class GameState { if (contPlaying) { togglePlayback(true) } - }) + }).finally(closeVictoryModal) } @@ -348,6 +355,9 @@ class GameState { this.gameId = gameId updateGameIdDisplay(); updateLeaderboard(); + if (config.isDebugMode) { + debugMenuInstance.updateTools() + } resolve() } }) diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index 7964051..a6b3795 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -2,18 +2,15 @@ import * as THREE from "three"; import "./style.css" import { initMap } from "./map/create"; import { gameState } from "./gameState"; -import { logger } from "./logger"; import { loadBtn, prevBtn, nextBtn, speedSelector, fileInput, playBtn, mapView, loadGameBtnFunction } from "./domElements"; -import { updateChatWindows } from "./domElements/chatWindows"; -import { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase, nextPhase, previousPhase, togglePlayback, _setPhase } from "./phase"; import { config } from "./config"; import { Tween, Group, Easing } from "@tweenjs/tween.js"; -import { initRotatingDisplay, updateRotatingDisplay } from "./components/rotatingDisplay"; -import { closeTwoPowerConversation, showTwoPowerConversation } from "./components/twoPowerConversation"; -import { PowerENUM } from "./types/map"; +import { initRotatingDisplay, } from "./components/rotatingDisplay"; import { debugMenuInstance } from "./debug/debugMenu"; import { initializeBackgroundAudio, startBackgroundAudio } from "./backgroundAudio"; import { updateLeaderboard } from "./components/leaderboard"; +import { _setPhase } from "./phase"; +import { togglePlayback } from "./phase"; //TODO: Create a function that finds a suitable unit location within a given polygon, for placing units better // Currently the location for label, unit, and SC are all the same manually picked location @@ -161,7 +158,6 @@ function animate() { // 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); } diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index a5062fd..44cc2a7 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -20,7 +20,14 @@ const MOMENT_DISPLAY_TIMEOUT_MS = config.isDebugMode || config.isInstantMode ? 1 // FIXME: Going to previous phases is borked. Units do not animate properly, map doesn't update. export function _setPhase(phaseIndex: number) { - console.log(`[Phase] _setPhase called with index: ${phaseIndex}`); + if (phaseIndex < 0) { + throw new Error(`Provided invalid phaseIndex, cannot setPhase to ${phaseIndex} - game has ${gameState.gameData.phases.length} phases`) + } + if (phaseIndex >= gameState.gameData.phases.length - 1) { + gameState.phaseIndex = gameState.gameData.phases.length - 1 + displayFinalPhase() + return + } // Store the old phase index at the very beginning const oldPhaseIndex = gameState.phaseIndex; @@ -29,10 +36,9 @@ export function _setPhase(phaseIndex: number) { debugMenuInstance.updateTools() } const gameLength = gameState.gameData.phases.length + + // Validate that the phaseIndex is within the bounds of the game length. - if (phaseIndex >= gameLength || phaseIndex < 0) { - throw new Error(`Provided invalid phaseIndex, cannot setPhase to ${phaseIndex} - game has ${gameState.gameData.phases.length} phases`) - } if (phaseIndex - gameState.phaseIndex != 1) { // We're moving more than one Phase forward, or any number of phases backward, to do so clear the board and reInit the units on the correct phase gameState.unitAnimations = []; @@ -56,12 +62,7 @@ export function _setPhase(phaseIndex: number) { console.log(`Moving to phase ${gameState.gameData.phases[gameState.phaseIndex].name}`); } - if (phaseIndex === gameLength - 1) { - displayFinalPhase() - } else { - displayPhase() - } - gameState.nextPhaseScheduled = false; + displayPhase() } // Finally, update the gameState with the current phaseIndex @@ -235,6 +236,8 @@ export function displayPhase(skipMessages = false) { logger.log("No animations for this phase transition"); gameState.messagesPlaying = false; } + gameState.nextPhaseScheduled = false; + } /** @@ -242,7 +245,6 @@ export function displayPhase(skipMessages = false) { * Used when first loading a game */ export function displayInitialPhase() { - gameState.phaseIndex = 0; initUnits(0); displayPhase(true); } @@ -316,8 +318,6 @@ export function advanceToNextPhase() { nextPhase(); } - // Reset the nextPhaseScheduled flag to allow scheduling the next phase - gameState.nextPhaseScheduled = false; } function displayFinalPhase() { @@ -360,27 +360,11 @@ function displayFinalPhase() { winner, maxCenters, finalStandings, - onClose: () => { - // Only proceed to next game if in playing mode - if (gameState.isPlaying) { - gameState.loadNextGame(); - } - } }); - //setTimeout(closeVictoryModal, 10000) + setTimeout(() => { + gameState.loadNextGame(true) + }, config.victoryModalDisplayMs) - // Log the victory - logger.log(`Victory! ${winner} wins the game with ${maxCenters} supply centers.`); - - // Display final standings in console - console.log("Final Standings:"); - finalStandings.forEach((entry, index) => { - const medal = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : " "; - console.log(`${medal} ${entry.power}: ${entry.centers} centers`); - }); - - // Show victory in info panel - logger.updateInfoPanel(`🏆 ${winner} VICTORIOUS! 🏆\n\nFinal Score: ${maxCenters} supply centers\n\nCheck console for full standings.`); } else { logger.log("Could not determine game winner"); } diff --git a/ai_animation/tsconfig.json b/ai_animation/tsconfig.json index 3bba997..9a53325 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": false, "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler",