diff --git a/ai_animation/src/components/chatBubble.ts b/ai_animation/src/components/chatBubble.ts new file mode 100644 index 0000000..7fa0f10 --- /dev/null +++ b/ai_animation/src/components/chatBubble.ts @@ -0,0 +1,72 @@ +import { MessageSchemaType } from "../types/gameState"; +import { config } from "../config"; + + +export function createChatBubble(message: MessageSchemaType): HTMLElement { + + const messageElement = document.createElement('div'); + return messageElement +} + + +// New function to animate message words one at a time +/** + * Animates message text one word at a time + * @param message The full message text to animate + * @param contentSpanId The ID of the span element to animate within + * @param messagesContainer The container holding the messages + * @param onComplete Callback function to run when animation completes + */ +export function animateMessageWords(message: string, contentSpan: HTMLElement, messagesContainer: HTMLElement, onComplete: Function | null) { + if (!(typeof message === "string")) { + throw new Error("Message must be a string") + + } + const words = message.split(/\s+/); + if (!contentSpan) { + throw new Error("Couldn't find text bubble to fill") + } + + // Clear any existing content + contentSpan.textContent = ''; + let wordIndex = 0; + + // Function to add the next word + const addNextWord = () => { + if (wordIndex >= words.length) { + // All words added - message is complete + console.log(`Finished animating message with ${words.length} words in chat`); + // Call completion callback after all words are displayed + if (onComplete) { + setTimeout(() => { + onComplete(); + }, config.messageCompletionDelay || 100); + } + return; + } + + // Add space if not the first word + if (wordIndex > 0) { + contentSpan.textContent += ' '; + } + + // Add the next word + contentSpan.textContent += words[wordIndex]; + wordIndex++; + + // Calculate delay based on word length and playback speed + // Longer words get slightly longer display time + const wordLength = words[wordIndex - 1].length; + const delay = Math.max(config.messageWordDelay || 50, Math.min(200, (config.messageWordDelay || 50) * (wordLength / 4))); + + // Scroll to ensure newest content is visible + messagesContainer.scrollTop = messagesContainer.scrollHeight; + + // Schedule next word + setTimeout(addNextWord, delay); + }; + + // Start animation + addNextWord(); +} + diff --git a/ai_animation/src/components/momentModal.ts b/ai_animation/src/components/momentModal.ts index 7ade562..597918e 100644 --- a/ai_animation/src/components/momentModal.ts +++ b/ai_animation/src/components/momentModal.ts @@ -5,6 +5,7 @@ import { PowerENUM } from '../types/map'; import { Moment } from '../types/moments'; import { MessageSchemaType } from '../types/gameState'; import { ScheduledEvent } from '../events.ts' +import { animateMessageWords, createChatBubble } from './chatBubble.ts'; interface MomentDialogueOptions { moment: Moment; @@ -33,14 +34,26 @@ export function showMomentModal(options: MomentDialogueOptions): void { `This indicates a data quality issue - moments should only be created when there are actual conversations to display.` ); } + if (moment.powers_involved.length < 2) { + console.warn("Attempted to show moment with only one power involved") + onClose() + } + if (moment.raw_messages.length > config.convertsationModalMaxMessages) { + moment.raw_messages = moment.raw_messages.slice(moment.raw_messages.length - config.convertsationModalMaxMessages, moment.raw_messages.length) + } showConversationModalSequence(title, moment, onClose); } export function createMomentEvent(moment: Moment): ScheduledEvent { return new ScheduledEvent( `moment-${moment.phase}`, - () => showMomentModal({ moment })) + () => new Promise((resolve) => { + showMomentModal({ + moment, + onClose: () => resolve() + }); + })) } @@ -78,59 +91,56 @@ function showConversationModalSequence( dialogueOverlay.appendChild(dialogueContainer); document.body.appendChild(dialogueOverlay); - // Set up event listeners for close functionality - setupEventListeners(onClose); - dialogueOverlay!.style.opacity = '1' // Schedule messages to be displayed sequentially through event queue console.log(`Starting two-power conversation with ${moment.raw_messages.length} messages`); - scheduleMessageSequence(conversationArea, moment.raw_messages, power1, power2, onClose); + scheduleMessageSequence(conversationArea, moment.raw_messages, power1, power2).then(() => { + closeMomentModal(); + onClose(); + }); } /** - * Schedules all messages to be displayed sequentially through the event queue + * Schedules all messages to be displayed sequentially using promises */ -function scheduleMessageSequence( +async function scheduleMessageSequence( container: HTMLElement, messages: MessageSchemaType[], power1: string, power2: string, callbackOnClose?: () => void -): void { - let messageIndex = 0; +): Promise { + // Chain message animations using promises to ensure sequential display + for (let i = 0; i < messages.length; i++) { + const messageObj = messages[i]; - // Function to show the next message - const showNext = () => { - // All messages have been displayed - if (messageIndex >= messages.length) { - console.log(`All ${messages.length} conversation messages displayed, scheduling close in ${config.conversationFinalDelay}ms`); - // Schedule conversation close after all messages are shown - gameState.eventQueue.scheduleDelay(config.conversationFinalDelay, () => { - console.log('Closing two-power conversation and calling onClose callback'); - closeMomentModal(); - if (callbackOnClose) callbackOnClose(); - }, `close-conversation-after-messages-${Date.now()}`); - return; + // Create the message element + const messageElement = createMessageElement(messageObj, power1, power2); + container.appendChild(messageElement); + + // Animate message appearance + messageElement.style.opacity = '0'; + messageElement.style.transform = 'translateY(20px)'; + messageElement.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; + messageElement.style.opacity = '1'; + messageElement.style.transform = 'translateY(0)'; + + // Scroll to bottom + container.scrollTop = container.scrollHeight; + + // Wait for word-by-word animation to complete + const messageBubble = messageElement.querySelector('.message-bubble') as HTMLElement; + if (messageBubble) { + await new Promise((resolve) => { + animateMessageWords(messageObj.message, messageBubble, container, resolve); + }); } + } - // Get the next message - const message = messages[messageIndex]; - - // Function to call after message animation completes - const onMessageComplete = () => { - messageIndex++; - console.log(`Conversation message ${messageIndex} of ${messages.length} completed`); - // Schedule next message with proper delay - gameState.eventQueue.scheduleDelay(config.messageBetweenDelay, showNext, `show-next-conversation-message-${messageIndex}-${Date.now()}`); - }; - - // Display the message with word-by-word animation - displaySingleMessage(container, message, power1, power2, onMessageComplete); - }; - - // Start the message sequence with initial delay - gameState.eventQueue.scheduleDelay(config.conversationModalDelay, showNext, `start-conversation-sequence-${Date.now()}`); + if (callbackOnClose) { + callbackOnClose(); + } } /** @@ -210,7 +220,8 @@ function createDialogueOverlay(): HTMLElement { `; // Trigger fade in - gameState.eventQueue.scheduleDelay(10, () => overlay.style.opacity = '1', `fade-in-overlay-${Date.now()}`); + // Trigger fade in with a small timeout + setTimeout(() => overlay.style.opacity = '1', 10); return overlay; } @@ -551,55 +562,3 @@ function createMessageElement(message: MessageSchemaType, power1: string, power2 return messageDiv; } -/** - * Animates message text one word at a time (adapted from chatWindows.ts) - */ -function animateMessageWords( - message: string, - contentElement: HTMLElement, - container: HTMLElement, - onComplete: (() => void) | null -): void { - const words = message.split(/\s+/); - - // Clear any existing content - contentElement.textContent = ''; - let wordIndex = 0; - - // Function to add the next word - const addNextWord = () => { - if (wordIndex >= words.length) { - // All words added - message is complete - console.log(`Finished animating conversation message with ${words.length} words`); - - // Add a slight delay after the last word for readability - gameState.eventQueue.scheduleDelay(config.messageCompletionDelay, () => { - if (onComplete) { - onComplete(); // Call the completion callback - } - }, `conversation-message-complete-${Date.now()}`); - - return; - } - - // Add space if not the first word - if (wordIndex > 0) { - contentElement.textContent += ' '; - } - - // Add the next word - contentElement.textContent += words[wordIndex]; - wordIndex++; - - // Calculate delay based on word length and playback speed - const wordLength = words[wordIndex - 1].length; - const delay = Math.max(config.messageWordDelay, Math.min(200, config.messageWordDelay * (wordLength / 4))); - gameState.eventQueue.scheduleDelay(delay, addNextWord, `add-conversation-word-${wordIndex}-${Date.now()}`); - - // Scroll to ensure newest content is visible - container.scrollTop = container.scrollHeight; - }; - - // Start animation - addNextWord(); -} diff --git a/ai_animation/src/config.ts b/ai_animation/src/config.ts index 9894446..def83b9 100644 --- a/ai_animation/src/config.ts +++ b/ai_animation/src/config.ts @@ -117,6 +117,7 @@ export const config = { get conversationModalDelay(): number { return this.isInstantMode ? 50 : 500; // Initial delay before showing messages }, + convertsationModalMaxMessages: 20, /** * Phase and moment timing diff --git a/ai_animation/src/debug/eventQueueDisplay.ts b/ai_animation/src/debug/eventQueueDisplay.ts index aad4304..6b20265 100644 --- a/ai_animation/src/debug/eventQueueDisplay.ts +++ b/ai_animation/src/debug/eventQueueDisplay.ts @@ -58,7 +58,7 @@ class EventQueueDisplay { /* Wrapper for debug menu and event queue */ #debug-wrapper { position: fixed; - top: 20px; + top: 200px; right: 20px; width: 300px; display: flex; diff --git a/ai_animation/src/domElements/chatWindows.ts b/ai_animation/src/domElements/chatWindows.ts index 28ccb4f..00cc363 100644 --- a/ai_animation/src/domElements/chatWindows.ts +++ b/ai_animation/src/domElements/chatWindows.ts @@ -5,15 +5,13 @@ import { GamePhase, MessageSchemaType } from "../types/gameState"; import { getPowerDisplayName } from '../utils/powerNames'; import { PowerENUM } from '../types/map'; import { ScheduledEvent } from "../events"; +import { createChatBubble, animateMessageWords } from "../components/chatBubble"; //TODO: Sometimes the LLMs use lists, and they don't work in the chats. The just appear as bullets within a single line. -// -//TODO: We are getting a mixing of chats from different phases. In game 0, F1902M starts using chat before S1902M finishes let faceIconCache = {}; // Cache for generated face icons // Add a message counter to track sound effect frequency -let messageCounter = 0; type chatWindowMap = { [key in PowerENUM]: { element: HTMLHtmlElement, messagesContainer: HTMLHtmlElement, @@ -163,94 +161,21 @@ export function createMessageEvents(phase: GamePhase): ScheduledEvent[] { let messageEvents: ScheduledEvent[] = [] // Only show messages relevant to the current player (sent by them, to them, or global) - const relevantMessages = phase.messages.filter(msg => { - return ( - msg.sender === gameState.currentPower || - msg.recipient === gameState.currentPower || - msg.recipient === 'GLOBAL' - ); - }); - // Sort messages by time sent - relevantMessages.sort((a, b) => a.time_sent - b.time_sent); + const relevantMessages = fiterAndSortChatMessagesForPhase(phase) + for (let [idx, msg] of relevantMessages.entries()) { + messageEvents.push(new ScheduledEvent( + `message-${phase.name}-${msg.sender}`, + () => new Promise((resolve) => { + addMessageToChat(msg, !config.isInstantMode, () => resolve()); + animateHeadNod(msg, (idx % config.soundEffectFrequency === 0)); + }) + )) - for (let msg of relevantMessages) { - messageEvents.push(new ScheduledEvent(`message-${phase.name}-${msg.sender}`, () => addMessageToChat(msg, !config.isInstantMode))) } return messageEvents } - -// 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) - */ -function updateChatWindows(stepMessages = false, callback?: () => void) { - // Exit early if no messages - if (!gameState.currentPhase.messages || !gameState.currentPhase.messages.length) { - console.log("No messages to display for this phase"); - return; - } - - - - // 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 ${gameState.currentPhase.name}`); - } - - // Stepwise mode: show one message at a time, animating word-by-word - let index = 0; - - // Store the start time for debugging - const messageStartTime = Date.now(); - - // Function to process the next message - const showNext = () => { - // If we're not playing or user has manually advanced, stop message animation - if (!gameState.isPlaying && !config.isDebugMode) { - console.log("Playback stopped, halting message animations"); - return; - } - - // All messages have been displayed - if (index >= relevantMessages.length) { - if (config.isDebugMode) { - console.log(`All messages displayed in ${Date.now() - messageStartTime}ms`); - } - console.log("Messages complete, triggering next phase"); - if (callback) callback(); - return; - } - - // Get the next message - const msg = relevantMessages[index]; - - // Only log in debug mode to reduce console noise - if (config.isDebugMode) { - console.log(`Displaying message ${index + 1}/${relevantMessages.length}: ${msg.sender} to ${msg.recipient}`); - } - - // Function to call after message animation completes - const onMessageComplete = () => { - index++; // Only increment after animation completes - showNext() - }; - - // Add the message with word animation - addMessageToChat(msg, true, onMessageComplete); - - // Animate head and play sound for new messages (not just when not in debug mode) - messageCounter++; - animateHeadNod(msg, (messageCounter % config.soundEffectFrequency === 0)); - }; - - // Start the message sequence with initial delay - gameState.eventQueue.scheduleDelay(50, showNext, `start-message-sequence-${Date.now()}`); -} - // Modified to support word-by-word animation and callback function addMessageToChat(msg: MessageSchemaType, animateWords: boolean = false, onComplete: Function | null = null) { // Determine which chat window to use @@ -265,70 +190,52 @@ function addMessageToChat(msg: MessageSchemaType, animateWords: boolean = false, // Create a unique ID for this message to avoid duplication const msgId = `${msg.sender}-${msg.recipient}-${msg.time_sent}-${msg.message}`; - // Skip if we've already shown this message - if (chatWindows[targetPower].seenMessages.has(msgId)) { - return false; // Not a new message - } - - // Mark as seen - chatWindows[targetPower].seenMessages.add(msgId); const messagesContainer = chatWindows[targetPower].messagesContainer; - const messageElement = document.createElement('div'); + const chatBubble = createChatBubble(msg) // Style based on sender/recipient if (targetPower === 'GLOBAL') { // Global chat shows sender info const senderColor = msg.sender.toLowerCase(); - messageElement.className = 'chat-message message-incoming'; + chatBubble.className = 'chat-message message-incoming'; // Add the header with the sender name immediately const headerSpan = document.createElement('span'); headerSpan.style.fontWeight = 'bold'; headerSpan.className = `power-${senderColor}`; headerSpan.textContent = `${getPowerDisplayName(msg.sender as PowerENUM)}: `; - messageElement.appendChild(headerSpan); + chatBubble.appendChild(headerSpan); - // Create a span for the message content that will be filled word by word - const contentSpan = document.createElement('span'); - contentSpan.id = `msg-content-${msgId.replace(/[^a-zA-Z0-9]/g, '-')}`; - messageElement.appendChild(contentSpan); - // Add timestamp - const timeDiv = document.createElement('div'); - timeDiv.className = 'message-time'; - timeDiv.textContent = gameState.currentPhase.name; - messageElement.appendChild(timeDiv); } else { // Private chat - outgoing or incoming style const isOutgoing = msg.sender === gameState.currentPower; - messageElement.className = `chat-message ${isOutgoing ? 'message-outgoing' : 'message-incoming'}`; + chatBubble.className = `chat-message ${isOutgoing ? 'message-outgoing' : 'message-incoming'}`; - // Create content span - const contentSpan = document.createElement('span'); - contentSpan.id = `msg-content-${msgId.replace(/[^a-zA-Z0-9]/g, '-')}`; - messageElement.appendChild(contentSpan); - - // Add timestamp - const timeDiv = document.createElement('div'); - timeDiv.className = 'message-time'; - timeDiv.textContent = gameState.currentPhase.name; - messageElement.appendChild(timeDiv); } + const contentSpan = document.createElement('span'); + contentSpan.id = `msg-content-${msgId.replace(/[^a-zA-Z0-9]/g, '-')}`; + chatBubble.appendChild(contentSpan); + + // Add timestamp + const timeDiv = document.createElement('div'); + timeDiv.className = 'message-time'; + timeDiv.textContent = gameState.currentPhase.name; + chatBubble.appendChild(timeDiv); // Add to container - messagesContainer.appendChild(messageElement); + messagesContainer.appendChild(chatBubble); // Scroll to bottom messagesContainer.scrollTop = messagesContainer.scrollHeight; if (animateWords) { // Start word-by-word animation - const contentSpanId = `msg-content-${msgId.replace(/[^a-zA-Z0-9]/g, '-')}`; - animateMessageWords(msg.message, contentSpanId, targetPower, messagesContainer, onComplete); + animateMessageWords(msg.message, contentSpan, messagesContainer, onComplete); } else { // Show entire message at once - const contentSpan = messageElement.querySelector(`#msg-content-${msgId.replace(/[^a-zA-Z0-9]/g, '-')}`); + const contentSpan = chatBubble.querySelector(`#msg-content-${msgId.replace(/[^a-zA-Z0-9]/g, '-')}`); if (contentSpan) { contentSpan.textContent = msg.message; } @@ -342,59 +249,6 @@ function addMessageToChat(msg: MessageSchemaType, animateWords: boolean = false, return true; // This was a new message } -// New function to animate message words one at a time -/** - * Animates message text one word at a time - * @param message The full message text to animate - * @param contentSpanId The ID of the span element to animate within - * @param targetPower The power the message is displayed for - * @param messagesContainer The container holding the messages - * @param onComplete Callback function to run when animation completes - */ -function animateMessageWords(message: string, contentSpanId: string, targetPower: string, - messagesContainer: HTMLElement, onComplete: (() => void) | null) { - const words = message.split(/\s+/); - const contentSpan = document.getElementById(contentSpanId); - if (!contentSpan) { - // If span not found, still call onComplete to avoid breaking the game flow - if (onComplete) onComplete(); - return; - } - - // Clear any existing content - contentSpan.textContent = ''; - let wordIndex = 0; - - // Function to add the next word - const addNextWord = () => { - if (wordIndex >= words.length) { - // All words added - message is complete - console.log(`Finished animating message with ${words.length} words in ${targetPower} chat`); - return; - } - - // Add space if not the first word - if (wordIndex > 0) { - contentSpan.textContent += ' '; - } - - // Add the next word - contentSpan.textContent += words[wordIndex]; - wordIndex++; - - // Calculate delay based on word length and playback speed - // Longer words get slightly longer display time - const wordLength = words[wordIndex - 1].length; - // Use consistent word delay from config - - // Scroll to ensure newest content is visible - // Use requestAnimationFrame to batch DOM updates in streaming mode - addNextWord() - }; - - // Start animation - addNextWord(); -} // Modified to support conditional sound effects function animateHeadNod(msg, playSoundEffect = true) { @@ -671,4 +525,3 @@ function playRandomSoundEffect() { }); } } - diff --git a/ai_animation/src/events.ts b/ai_animation/src/events.ts index b9c15f2..760a967 100644 --- a/ai_animation/src/events.ts +++ b/ai_animation/src/events.ts @@ -4,30 +4,32 @@ import { config } from "./config"; import { debugMenuInstance } from "./debug/debugMenu"; -import { toggleEventQueueDisplayState } from "./debug/eventQueueDisplay"; -import { debugMenu } from "./domElements"; export class ScheduledEvent { id: string; - callback: () => void; + callback: () => void | Promise; resolved?: boolean; running!: boolean; error?: Error; // If the event caused an error, store it here. - constructor(id: string, callback: () => void, resolved?: boolean) { + promise?: Promise; + + constructor(id: string, callback: () => void | Promise, resolved?: boolean) { this.id = id this.callback = callback this.resolved = resolved ? true : resolved } + run = () => { this.running = true - try { - this.callback(); - } catch (e) { - this.error = e - } finally { - - this.resolved = true - } + // Store the promise so we can track it + this.promise = Promise.resolve(this.callback()) + .then(() => { + this.resolved = true; + }) + .catch((e) => { + this.error = e; + this.resolved = true; + }); } } @@ -35,13 +37,16 @@ export class EventQueue { private events: ScheduledEvent[] = []; private startTime: number = 0; private isRunning: boolean = false; + private currentEventPromise?: Promise; /** * Start the event queue with current time as reference */ start(): void { this.isRunning = true; - this.events[0].run() + if (this.events.length > 0) { + this.events[0].run() + } } /** @@ -58,6 +63,7 @@ export class EventQueue { reset(resetCallback?: () => void): void { this.events = []; this.isRunning = false; + this.currentEventPromise = undefined; if (resetCallback) { resetCallback(); } @@ -86,15 +92,26 @@ export class EventQueue { if (!this.isRunning) return; if (this.events.length < 1) return; - if (this.events[0].resolved) { - if (this.events[0].error) { - console.error(this.events[0].error) + const currentEvent = this.events[0]; + + // Start the event if not started + if (!currentEvent.running && !currentEvent.resolved) { + currentEvent.run(); + this.currentEventPromise = currentEvent.promise; + } + + // Check if current event is complete + if (currentEvent.resolved) { + if (currentEvent.error) { + console.error(currentEvent.error) } if (config.isDebugMode) { debugMenuInstance.updateTools() } this.events.shift() - this.events[0].run() + if (this.events.length > 0) { + this.events[0].run() + } } } diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index 113a446..9d6f171 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -1,6 +1,6 @@ import * as THREE from "three" import { type CoordinateData, CoordinateDataSchema, PowerENUM } from "./types/map" -import type { GameSchemaType, MessageSchemaType } from "./types/gameState"; +import type { GamePhase, GameSchemaType, MessageSchemaType } from "./types/gameState"; import { GameSchema } from "./types/gameState"; import { debugMenuInstance } from "./debug/debugMenu.ts" import { config } from "./config.ts" @@ -18,6 +18,7 @@ import { EventQueue, ScheduledEvent } from "./events.ts"; import { createAnimateUnitsEvent, createAnimationsForNextPhase } from "./units/animate.ts"; import { createUpdateNewsBannerEvent } from "./components/newsBanner.ts"; import { createMomentEvent } from "./components/momentModal.ts"; +import { updateMapOwnership } from "./map/state.ts"; //FIXME: This whole file is a mess. Need to organize and format @@ -112,6 +113,16 @@ class GameAudio { constructor() { this.players = [{ Name: "background_music", track: initializeBackgroundAudio() }] + } + getNarratorPlayer = (): Audio | null => { + let player = this.players.filter((player) => player.Name.includes("Narrator")) + if (player.length === 0) { + return null + } else { + return player[0].track + } + + } pause = (track_idx?: number | undefined) => { if (!track_idx) { @@ -196,7 +207,7 @@ class GameState { } _fillEventQueue = (gameData: GameSchemaType) => { - for (let phase of gameData.phases) { + for (let [phaseIdx, phase] of gameData.phases.entries()) { // Update Phase Display let updateUIEvent = createUpdateUIEvent(phase) this.eventQueue.schedule(updateUIEvent) @@ -216,13 +227,37 @@ class GameState { if (phaseMoment) { this.eventQueue.schedule(createMomentEvent(phaseMoment)) } - let animationEvents = createAnimateUnitsEvent(phase) - this.eventQueue.schedule(animationEvents) + if (!(phaseIdx === 0)) { + + let animationEvents = createAnimateUnitsEvent(phase, phaseIdx) + this.eventQueue.schedule(animationEvents) + } // Lastly, update the gamePhase id - this.eventQueue.schedule(new ScheduledEvent(`phaseidUpdate-${phase.name}`, () => { this.phaseIndex = gameData.phases.indexOf(phase) })) + this.eventQueue.schedule(this.createNextPhaseEvent(phase, phaseIdx)) } } + createNextPhaseEvent = (phase: GamePhase, phaseIdx: number): ScheduledEvent => { + return new ScheduledEvent( + `movePhase-${phase.name}`, + async () => { + while (true) { + let narrator = this.audio.getNarratorPlayer() + + let narratorFinished = (narrator === null) || narrator.ended + if (this.unitAnimations.length === 0 && narratorFinished) { + this.phaseIndex = phaseIdx + updateMapOwnership() + break; + } + + // Wait 500ms before checking again + await new Promise(resolve => setTimeout(resolve, 500)); + } + + }) + } + /** * Load game data from a JSON string and initialize the game state diff --git a/ai_animation/src/units/animate.ts b/ai_animation/src/units/animate.ts index c15a815..d7a1690 100644 --- a/ai_animation/src/units/animate.ts +++ b/ai_animation/src/units/animate.ts @@ -264,16 +264,17 @@ function createHoldAnimation(unitMesh: THREE.Group): Tween { return growTween; } -export function createAnimateUnitsEvent(phase: GamePhase): ScheduledEvent { - return new ScheduledEvent(`createAnimations-${phase.name}`, () => createAnimationsForNextPhase()) +export function createAnimateUnitsEvent(phase: GamePhase, phaseIdx: number): ScheduledEvent { + return new ScheduledEvent(`createAnimations-${phase.name}`, () => createAnimationsForNextPhase(phaseIdx)) } /** * Creates animations for unit movements based on orders from the previous phase * **/ -export function createAnimationsForNextPhase() { - let previousPhase = gameState.gameData?.phases[gameState.phaseIndex == 0 ? 0 : gameState.phaseIndex - 1] +export function createAnimationsForNextPhase(phaseIdx: number) { + if (phaseIdx === 0) { throw new Error("Cannot create animations for phase 0, must start on 1 or higher") } + let previousPhase = gameState.gameData?.phases[phaseIdx - 1] // const sequence = ["build", "disband", "hold", "move", "bounce", "retreat"] // Safety check - if no previous phase or no orders, return if (!previousPhase) { @@ -305,7 +306,7 @@ export function createAnimationsForNextPhase() { order.type = "bounce" } // If the result is void, that means the move was not valid? - if (result === "void") continue; + if (result === "void" || result === "no convoy") continue; let unitIndex = -1 unitIndex = getUnit(order, power);