diff --git a/ai_animation/src/domElements/chatWindows.ts b/ai_animation/src/domElements/chatWindows.ts index 7f12cbc..ca9a767 100644 --- a/ai_animation/src/domElements/chatWindows.ts +++ b/ai_animation/src/domElements/chatWindows.ts @@ -2,6 +2,7 @@ import * as THREE from "three"; import { gameState } from "../gameState"; import { config } from "../config"; import { advanceToNextPhase } from "../phase"; +import { GamePhase, Message } from "../types/gameState"; import { getPowerDisplayName, getAllPowerDisplayNames } from '../utils/powerNames'; import { PowerENUM } from '../types/map'; @@ -13,7 +14,16 @@ let faceIconCache = {}; // Cache for generated face icons // Add a message counter to track sound effect frequency let messageCounter = 0; -let chatWindows = {}; // Store chat window elements by power +type chatWindowMap = { [key in PowerENUM]: { + element: HTMLHtmlElement, + messagesContainer: HTMLHtmlElement, + isGlobal: boolean, + seenMessages: Set +} } + + +let chatWindows: chatWindowMap + // --- CHAT WINDOW FUNCTIONS --- export function createChatWindows() { // Clear existing chat windows @@ -136,22 +146,48 @@ function createChatWindow(power, isGlobal = false) { }; } +function fiterAndSortChatMessagesForPhase(phase: GamePhase): Message[] { + + let relevantMessages = phase.messages.filter(msg => { + return ( + msg.sender === gameState.currentPower || + msg.recipient === gameState.currentPower || + msg.recipient === 'GLOBAL' + ); + }); + relevantMessages.sort((a, b) => a.time_sent - b.time_sent); + return relevantMessages +} +/** + * updates the gameState.phaseChatMessages array to the messages for this phase. + * + */ +export function initChatMessagesForPhase() { + gameState.phaseChatMessages = [] + gameState.phaseChatMessages = fiterAndSortChatMessagesForPhase(gameState.gameData.phases[gameState.phaseIndex]) +} + +function playChatMessage(messageIndex) { + addMessageToChat(gameState.currentPhase) + +} // Modified to accumulate messages instead of resetting and only animate for new messages /** * Updates chat windows with messages for the current phase * @param phase The current game phase containing messages * @param stepMessages Whether to animate messages one-by-word (true) or show all at once (false) */ -export function updateChatWindows(phase: any, stepMessages = false) { +export function updateChatWindows(stepMessages = false) { + gameState.messagesPlaying = true // Exit early if no messages - if (!phase.messages || !phase.messages.length) { + if (!gameState.currentPhase.messages || !gameState.currentPhase.messages.length) { console.log("No messages to display for this phase"); gameState.messagesPlaying = false; return; } // Only show messages relevant to the current player (sent by them, to them, or global) - const relevantMessages = phase.messages.filter(msg => { + const relevantMessages = gameState.currentPhase.messages.filter(msg => { return ( msg.sender === gameState.currentPower || msg.recipient === gameState.currentPower || @@ -164,13 +200,13 @@ export function updateChatWindows(phase: any, stepMessages = false) { // Log message count but only in debug mode to reduce noise if (config.isDebugMode) { - console.log(`Found ${relevantMessages.length} messages for player ${gameState.currentPower} in phase ${phase.name}`); + console.log(`Found ${relevantMessages.length} messages for player ${gameState.currentPower} in phase ${gameState.currentPhase.name}`); } if (!stepMessages || config.isInstantMode) { // Normal mode or instant chat mode: show all messages at once relevantMessages.forEach(msg => { - const isNew = addMessageToChat(msg, phase.name); + const isNew = addMessageToChat(msg); if (isNew) { // Increment message counter and play sound on every third message messageCounter++; @@ -235,7 +271,7 @@ export function updateChatWindows(phase: any, stepMessages = false) { }; // Add the message with word animation - const isNew = addMessageToChat(msg, phase.name, true, onMessageComplete); + const isNew = addMessageToChat(msg, true, onMessageComplete); // Handle non-new messages if (!isNew) { @@ -253,7 +289,7 @@ export function updateChatWindows(phase: any, stepMessages = false) { } // Modified to support word-by-word animation and callback -function addMessageToChat(msg, phaseName, animateWords = false, onComplete = null) { +function addMessageToChat(msg: Message, animateWords: boolean = false, onComplete: Function | null = null) { // Determine which chat window to use let targetPower; if (msg.recipient === 'GLOBAL') { @@ -298,7 +334,7 @@ function addMessageToChat(msg, phaseName, animateWords = false, onComplete = nul // Add timestamp const timeDiv = document.createElement('div'); timeDiv.className = 'message-time'; - timeDiv.textContent = phaseName; + timeDiv.textContent = gameState.currentPhase.name; messageElement.appendChild(timeDiv); } else { // Private chat - outgoing or incoming style @@ -313,7 +349,7 @@ function addMessageToChat(msg, phaseName, animateWords = false, onComplete = nul // Add timestamp const timeDiv = document.createElement('div'); timeDiv.className = 'message-time'; - timeDiv.textContent = phaseName; + timeDiv.textContent = gameState.currentPhase.name; messageElement.appendChild(timeDiv); } diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index e78a740..0d7c1ce 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -1,9 +1,9 @@ import * as THREE from "three" import { type CoordinateData, CoordinateDataSchema, PowerENUM } from "./types/map" -import type { GameSchemaType } from "./types/gameState"; +import type { GameSchemaType, Message } from "./types/gameState"; import { debugMenuInstance } from "./debug/debugMenu.ts" import { config } from "./config.ts" -import { GameSchema } from "./types/gameState"; +import { GameSchema, type MessageSchema } from "./types/gameState"; import { prevBtn, nextBtn, playBtn, speedSelector, mapView, updateGameIdDisplay } from "./domElements"; import { createChatWindows } from "./domElements/chatWindows"; import { logger } from "./logger"; @@ -92,6 +92,7 @@ class GameState { // state locks messagesPlaying: boolean + phaseChatMessages: Message[] isPlaying: boolean isSpeaking: boolean isAnimating: boolean @@ -125,6 +126,7 @@ class GameState { this.phaseIndex = 0 this.boardName = boardName this.gameId = 16 + this.phaseChatMessages = [] // State locks this.isSpeaking = false @@ -420,6 +422,9 @@ class GameState { dirLight.position.set(300, 400, 300); this.scene.add(dirLight); } + get currentPhase() { + return this.gameData.phases[this.phaseIndex] + } } diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index e8ad303..7b1d31e 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -9,7 +9,7 @@ import { initRotatingDisplay, } from "./components/rotatingDisplay"; import { debugMenuInstance } from "./debug/debugMenu"; import { initializeBackgroundAudio, startBackgroundAudio } from "./backgroundAudio"; import { updateLeaderboard } from "./components/leaderboard"; -import { _setPhase, advanceToNextPhase, nextPhase, previousPhase } from "./phase"; +import { _setPhase, advanceToNextPhase, displayInitialPhase, nextPhase, previousPhase } from "./phase"; import { togglePlayback } from "./phase"; //TODO: Create a function that finds a suitable unit location within a given polygon, for placing units better @@ -126,6 +126,21 @@ function animate() { gameState.cameraPanAnim.update(); gameState.cameraPanAnim.update(); + // If all animations are complete + if (gameState.unitAnimations.length === 0 && !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() + } + }, config.effectivePlaybackSpeed); + } } else { // Manual camera controls when not in playback mode gameState.camControls.update(); @@ -147,21 +162,6 @@ function animate() { } - // 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() - } - }, config.effectivePlaybackSpeed); - } // Update any pulsing or wave animations on supply centers or units if (gameState.scene.userData.animatedObjects) { gameState.scene.userData.animatedObjects.forEach(obj => { diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index e5c296b..90e0708 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -35,7 +35,6 @@ export function _setPhase(phaseIndex: number) { if (config.isDebugMode) { debugMenuInstance.updateTools() } - const gameLength = gameState.gameData.phases.length // Validate that the phaseIndex is within the bounds of the game length. @@ -73,6 +72,12 @@ export function _setPhase(phaseIndex: number) { } // --- PLAYBACK CONTROLS --- +/** + * Updates the gameState.isPlaying variable, toggling it from current position. Updates UI Elements to indicate current state. + * + * @param explicitSet - If you need to set the state to playing or not, use this with the bool of what you want the state to be. + * + */ export function togglePlayback(explicitSet: boolean | undefined = undefined) { // If the game doesn't have any data, or there are no phases, return; if (!gameState.gameData || gameState.gameData.phases.length <= 0) { @@ -98,15 +103,14 @@ export function togglePlayback(explicitSet: boolean | undefined = undefined) { if (gameState.cameraPanAnim) gameState.cameraPanAnim.getAll()[1].start() // 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) { + if (gameState.currentPhase.messages && gameState.currentPhase.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); + logger.log(`Playing ${gameState.currentPhase.messages.length} messages from phase ${gameState.phaseIndex + 1}/${gameState.gameData.phases.length}`); + gameState.nextPhaseScheduled = true + displayPhase() } 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(); @@ -211,7 +215,7 @@ export function displayPhase(skipMessages = false) { // Show messages with animation or immediately based on skipMessages flag if (!skipMessages) { - updateChatWindows(currentPhase, true); + updateChatWindows(true); } else { gameState.messagesPlaying = false; } @@ -234,7 +238,6 @@ export function displayPhase(skipMessages = false) { } } else { logger.log("No animations for this phase transition"); - gameState.messagesPlaying = false; } gameState.nextPhaseScheduled = false; diff --git a/ai_animation/src/types/gameState.ts b/ai_animation/src/types/gameState.ts index e1e6cc3..c18cd62 100644 --- a/ai_animation/src/types/gameState.ts +++ b/ai_animation/src/types/gameState.ts @@ -12,11 +12,6 @@ const RelationshipStatusSchema = z.enum([ "Ally" ]); -const GameState = z.record(ProvinceENUMSchema, z.object({ - power: PowerENUMSchema, - //unit: z.object({}).optional() -})) - const MessageSchema = z.object({ sender: PowerENUMSchema, recipient: PowerENUMSchema, @@ -73,3 +68,4 @@ export const GameSchema = z.object({ export type GamePhase = z.infer; export type GameSchemaType = z.infer; +export type Message = z.infer