diff --git a/ai_animation/src/debug/debugMenu.ts b/ai_animation/src/debug/debugMenu.ts index 11e7d13..5a1c445 100644 --- a/ai_animation/src/debug/debugMenu.ts +++ b/ai_animation/src/debug/debugMenu.ts @@ -7,6 +7,7 @@ import { updateNextMomentDisplay, initNextMomentTool } from "./nextMoment"; import { initDebugProvinceHighlighting } from "./provinceHighlight"; import { initInstantChatTool } from "./instantChat"; import { initSpeechToggleTool } from "./speechToggle"; +import { initShowRandomMomentTool, updateMomentStatus } from "./showRandomMoment"; export class DebugMenu { private toggleBtn: HTMLButtonElement; @@ -176,11 +177,13 @@ export class DebugMenu { initSpeechToggleTool(this); initInstantChatTool(this); initNextMomentTool(this); + initShowRandomMomentTool(this); initDebugProvinceHighlighting() } public updateTools(): void { - updateNextMomentDisplay() + updateNextMomentDisplay(); + updateMomentStatus(); } } diff --git a/ai_animation/src/debug/showRandomMoment.ts b/ai_animation/src/debug/showRandomMoment.ts new file mode 100644 index 0000000..eb62b0a --- /dev/null +++ b/ai_animation/src/debug/showRandomMoment.ts @@ -0,0 +1,218 @@ +/** + * Show Random Moment Debug Tool + * Triggers showTwoPowerConversation for a random moment involving at least 2 powers with conversation + */ + +import { gameState } from '../gameState'; +import { showTwoPowerConversation } from '../components/twoPowerConversation'; +import { Moment } from '../types/moments'; +import { _setPhase } from '../phase'; +import { config } from '../config'; + +/** + * Initializes the show random moment debug tool + * @param debugMenu - Debug menu instance to add the tool to + */ +export function initShowRandomMomentTool(debugMenu: any) { + debugMenu.addDebugTool( + 'Show Random Moment', + ` +
+
Status: --
+
Available Moments: --
+ + +
+ `, + 'Future Tools' + ); + + // Initialize the display + updateMomentStatus(); + + // Add button functionality + const showBtn = document.getElementById('debug-show-random-moment'); + const refreshBtn = document.getElementById('debug-refresh-moment-list'); + + if (showBtn) { + showBtn.addEventListener('click', showRandomMoment); + } + + if (refreshBtn) { + refreshBtn.addEventListener('click', updateMomentStatus); + } +} + +/** + * Updates the moment status display in the debug menu + */ +export function updateMomentStatus() { + const statusElement = document.getElementById('debug-moment-status'); + const countElement = document.getElementById('debug-moment-count'); + + if (!statusElement || !countElement) return; + + if (!gameState.gameData || !gameState.momentsData) { + statusElement.textContent = 'No game/moments loaded'; + countElement.textContent = '0'; + return; + } + + const eligibleMoments = getEligibleMoments(); + statusElement.textContent = 'Ready'; + countElement.textContent = eligibleMoments.length.toString(); +} + +/** + * Gets all moments that involve at least 2 powers and have conversations between them + * @returns Array of moments eligible for conversation display + */ +function getEligibleMoments(): Moment[] { + if (!gameState.momentsData?.moments || !gameState.gameData?.phases) { + return []; + } + + return gameState.momentsData.moments.filter(moment => { + // Must involve at least 2 powers + if (!moment.powers_involved || moment.powers_involved.length < 2) { + return false; + } + + // Find the phase that matches this moment + const phase = gameState.gameData.phases.find(p => p.name === moment.phase); + if (!phase?.messages) { + return false; + } + + // Check if there are messages between any pair of the involved powers + for (let i = 0; i < moment.powers_involved.length; i++) { + for (let j = i + 1; j < moment.powers_involved.length; j++) { + const power1 = moment.powers_involved[i].toUpperCase(); + const power2 = moment.powers_involved[j].toUpperCase(); + + const hasConversation = phase.messages.some(msg => { + const sender = msg.sender?.toUpperCase(); + const recipient = msg.recipient?.toUpperCase(); + + return (sender === power1 && recipient === power2) || + (sender === power2 && recipient === power1); + }); + + if (hasConversation) { + return true; + } + } + } + + return false; + }); +} + +/** + * Finds the best power pair for a given moment based on message count + * @param moment - The moment to analyze + * @returns Object with power1, power2, and message count, or null if no conversations found + */ +function findBestPowerPairForMoment(moment: Moment): { power1: string; power2: string; messageCount: number } | null { + const phase = gameState.gameData.phases.find(p => p.name === moment.phase); + if (!phase?.messages) { + return null; + } + + let bestPair: { power1: string; power2: string; messageCount: number } | null = null; + + // Check all pairs of involved powers + for (let i = 0; i < moment.powers_involved.length; i++) { + for (let j = i + 1; j < moment.powers_involved.length; j++) { + const power1 = moment.powers_involved[i].toUpperCase(); + const power2 = moment.powers_involved[j].toUpperCase(); + + const messageCount = phase.messages.filter(msg => { + const sender = msg.sender?.toUpperCase(); + const recipient = msg.recipient?.toUpperCase(); + + return (sender === power1 && recipient === power2) || + (sender === power2 && recipient === power1); + }).length; + + if (messageCount > 0 && (!bestPair || messageCount > bestPair.messageCount)) { + bestPair = { power1, power2, messageCount }; + } + } + } + + return bestPair; +} + +/** + * Shows a random moment from eligible moments + */ +function showRandomMoment() { + const eligibleMoments = getEligibleMoments(); + + if (eligibleMoments.length === 0) { + console.warn('No eligible moments found for conversation display'); + const statusElement = document.getElementById('debug-moment-status'); + if (statusElement) { + statusElement.textContent = 'No eligible moments'; + statusElement.style.color = '#ff6b6b'; + setTimeout(() => { + statusElement.style.color = ''; + updateMomentStatus(); + }, 2000); + } + return; + } + + // Pick a random moment + const randomMoment = eligibleMoments[Math.floor(Math.random() * eligibleMoments.length)]; + + // Find the best power pair for this moment + const powerPair = findBestPowerPairForMoment(randomMoment); + + if (!powerPair) { + console.warn('No valid power pair found for selected moment'); + return; + } + + console.log(`Showing random moment: ${randomMoment.category} in ${randomMoment.phase}`); + console.log(`Powers: ${powerPair.power1} & ${powerPair.power2} (${powerPair.messageCount} messages)`); + console.log(`Interest Score: ${randomMoment.interest_score}/10`); + + // Find the phase index for this moment + const phaseIndex = gameState.gameData.phases.findIndex(p => p.name === randomMoment.phase); + + if (phaseIndex === -1) { + const errorMsg = `CRITICAL ERROR: Phase ${randomMoment.phase} from moment data not found in game data! This indicates a serious data integrity issue.`; + console.error(errorMsg); + + if (config.isDebugMode) { + alert(errorMsg + '\n\nAvailable phases: ' + gameState.gameData.phases.map(p => p.name).join(', ')); + } + + throw new Error(errorMsg); + } + + // Set the board to the correct phase + console.log(`Setting board to phase ${phaseIndex} (${randomMoment.phase})`); + _setPhase(phaseIndex); + + // Show the moment using the two-power conversation display + showTwoPowerConversation({ + power1: powerPair.power1, + power2: powerPair.power2, + moment: randomMoment, + title: `Random Moment: ${randomMoment.category}`, + onClose: () => { + console.log('Random moment display closed'); + updateMomentStatus(); + } + }); + + // Update status to show what was triggered + const statusElement = document.getElementById('debug-moment-status'); + if (statusElement) { + statusElement.textContent = `Showing: ${randomMoment.category}`; + statusElement.style.color = '#4dabf7'; + } +} \ No newline at end of file diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index 2b249b7..8721bf9 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -2,7 +2,7 @@ import { gameState } from "./gameState"; import { logger } from "./logger"; import { updatePhaseDisplay } from "./domElements"; import { initUnits } from "./units/create"; -import { updateSupplyCenterOwnership, updateLeaderboard, updateMapOwnership as _updateMapOwnership } from "./map/state"; +import { updateSupplyCenterOwnership, updateLeaderboard, updateMapOwnership as _updateMapOwnership, updateMapOwnership } from "./map/state"; import { updateChatWindows, addToNewsBanner } from "./domElements/chatWindows"; import { createAnimationsForNextPhase } from "./units/animate"; import { speakSummary } from "./speech"; @@ -16,7 +16,7 @@ const MOMENT_THRESHOLD = 8.0 const MOMENT_DISPLAY_TIMEOUT_MS = config.isDebugMode ? 5000 : 30000 // FIXME: Going to previous phases is borked. Units do not animate properly, map doesn't update. -function _setPhase(phaseIndex: number) { +export function _setPhase(phaseIndex: number) { if (config.isDebugMode) { debugMenuInstance.updateTools() } @@ -29,7 +29,9 @@ function _setPhase(phaseIndex: number) { // 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 = []; initUnits(phaseIndex) - displayPhase() + gameState.phaseIndex = phaseIndex + updateMapOwnership() + updatePhaseDisplay() } else { // Clear any existing animations to prevent overlap if (gameState.playbackTimer) { @@ -46,16 +48,16 @@ function _setPhase(phaseIndex: number) { if (config.isDebugMode && gameState.gameData) { console.log(`Moving to phase ${gameState.gameData.phases[gameState.phaseIndex].name}`); } + + if (phaseIndex === gameLength - 1) { + displayFinalPhase() + } else { + displayPhase() + } } // Finally, update the gameState with the current phaseIndex gameState.phaseIndex = phaseIndex - // If we're at the end of the game, don't attempt to animate. - if (phaseIndex === gameLength - 1) { - displayFinalPhase() - } else { - displayPhase() - } }