WIP: Queue works properly, fills from beginning to end of game and plays/pauses correctly

Just need to fix the issues with the momentModal not displaying
correctly.
This commit is contained in:
Tyler Marques 2025-08-06 20:56:50 -07:00
parent 73d195304a
commit 79a2eceef4
No known key found for this signature in database
GPG key ID: CB99EDCF41D3016F
8 changed files with 230 additions and 292 deletions

View file

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

View file

@ -5,6 +5,7 @@ import { PowerENUM } from '../types/map';
import { Moment } from '../types/moments'; import { Moment } from '../types/moments';
import { MessageSchemaType } from '../types/gameState'; import { MessageSchemaType } from '../types/gameState';
import { ScheduledEvent } from '../events.ts' import { ScheduledEvent } from '../events.ts'
import { animateMessageWords, createChatBubble } from './chatBubble.ts';
interface MomentDialogueOptions { interface MomentDialogueOptions {
moment: Moment; 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.` `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); showConversationModalSequence(title, moment, onClose);
} }
export function createMomentEvent(moment: Moment): ScheduledEvent { export function createMomentEvent(moment: Moment): ScheduledEvent {
return new ScheduledEvent( return new ScheduledEvent(
`moment-${moment.phase}`, `moment-${moment.phase}`,
() => showMomentModal({ moment })) () => new Promise<void>((resolve) => {
showMomentModal({
moment,
onClose: () => resolve()
});
}))
} }
@ -78,59 +91,56 @@ function showConversationModalSequence(
dialogueOverlay.appendChild(dialogueContainer); dialogueOverlay.appendChild(dialogueContainer);
document.body.appendChild(dialogueOverlay); document.body.appendChild(dialogueOverlay);
// Set up event listeners for close functionality
setupEventListeners(onClose);
dialogueOverlay!.style.opacity = '1' dialogueOverlay!.style.opacity = '1'
// Schedule messages to be displayed sequentially through event queue // Schedule messages to be displayed sequentially through event queue
console.log(`Starting two-power conversation with ${moment.raw_messages.length} messages`); 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, container: HTMLElement,
messages: MessageSchemaType[], messages: MessageSchemaType[],
power1: string, power1: string,
power2: string, power2: string,
callbackOnClose?: () => void callbackOnClose?: () => void
): void { ): Promise<void> {
let messageIndex = 0; // 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 // Create the message element
const showNext = () => { const messageElement = createMessageElement(messageObj, power1, power2);
// All messages have been displayed container.appendChild(messageElement);
if (messageIndex >= messages.length) {
console.log(`All ${messages.length} conversation messages displayed, scheduling close in ${config.conversationFinalDelay}ms`); // Animate message appearance
// Schedule conversation close after all messages are shown messageElement.style.opacity = '0';
gameState.eventQueue.scheduleDelay(config.conversationFinalDelay, () => { messageElement.style.transform = 'translateY(20px)';
console.log('Closing two-power conversation and calling onClose callback'); messageElement.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
closeMomentModal(); messageElement.style.opacity = '1';
if (callbackOnClose) callbackOnClose(); messageElement.style.transform = 'translateY(0)';
}, `close-conversation-after-messages-${Date.now()}`);
return; // 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<void>((resolve) => {
animateMessageWords(messageObj.message, messageBubble, container, resolve);
});
} }
}
// Get the next message if (callbackOnClose) {
const message = messages[messageIndex]; callbackOnClose();
}
// 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()}`);
} }
/** /**
@ -210,7 +220,8 @@ function createDialogueOverlay(): HTMLElement {
`; `;
// Trigger fade in // 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; return overlay;
} }
@ -551,55 +562,3 @@ function createMessageElement(message: MessageSchemaType, power1: string, power2
return messageDiv; 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();
}

View file

@ -117,6 +117,7 @@ export const config = {
get conversationModalDelay(): number { get conversationModalDelay(): number {
return this.isInstantMode ? 50 : 500; // Initial delay before showing messages return this.isInstantMode ? 50 : 500; // Initial delay before showing messages
}, },
convertsationModalMaxMessages: 20,
/** /**
* Phase and moment timing * Phase and moment timing

View file

@ -58,7 +58,7 @@ class EventQueueDisplay {
/* Wrapper for debug menu and event queue */ /* Wrapper for debug menu and event queue */
#debug-wrapper { #debug-wrapper {
position: fixed; position: fixed;
top: 20px; top: 200px;
right: 20px; right: 20px;
width: 300px; width: 300px;
display: flex; display: flex;

View file

@ -5,15 +5,13 @@ import { GamePhase, MessageSchemaType } from "../types/gameState";
import { getPowerDisplayName } from '../utils/powerNames'; import { getPowerDisplayName } from '../utils/powerNames';
import { PowerENUM } from '../types/map'; import { PowerENUM } from '../types/map';
import { ScheduledEvent } from "../events"; 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: 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 let faceIconCache = {}; // Cache for generated face icons
// Add a message counter to track sound effect frequency // Add a message counter to track sound effect frequency
let messageCounter = 0;
type chatWindowMap = { [key in PowerENUM]: { type chatWindowMap = { [key in PowerENUM]: {
element: HTMLHtmlElement, element: HTMLHtmlElement,
messagesContainer: HTMLHtmlElement, messagesContainer: HTMLHtmlElement,
@ -163,94 +161,21 @@ export function createMessageEvents(phase: GamePhase): ScheduledEvent[] {
let messageEvents: ScheduledEvent[] = [] let messageEvents: ScheduledEvent[] = []
// Only show messages relevant to the current player (sent by them, to them, or global) // Only show messages relevant to the current player (sent by them, to them, or global)
const relevantMessages = phase.messages.filter(msg => { const relevantMessages = fiterAndSortChatMessagesForPhase(phase)
return ( for (let [idx, msg] of relevantMessages.entries()) {
msg.sender === gameState.currentPower || messageEvents.push(new ScheduledEvent(
msg.recipient === gameState.currentPower || `message-${phase.name}-${msg.sender}`,
msg.recipient === 'GLOBAL' () => new Promise<void>((resolve) => {
); addMessageToChat(msg, !config.isInstantMode, () => resolve());
}); animateHeadNod(msg, (idx % config.soundEffectFrequency === 0));
// Sort messages by time sent })
relevantMessages.sort((a, b) => a.time_sent - b.time_sent); ))
for (let msg of relevantMessages) {
messageEvents.push(new ScheduledEvent(`message-${phase.name}-${msg.sender}`, () => addMessageToChat(msg, !config.isInstantMode)))
} }
return messageEvents 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 // Modified to support word-by-word animation and callback
function addMessageToChat(msg: MessageSchemaType, animateWords: boolean = false, onComplete: Function | null = null) { function addMessageToChat(msg: MessageSchemaType, animateWords: boolean = false, onComplete: Function | null = null) {
// Determine which chat window to use // 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 // Create a unique ID for this message to avoid duplication
const msgId = `${msg.sender}-${msg.recipient}-${msg.time_sent}-${msg.message}`; 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 messagesContainer = chatWindows[targetPower].messagesContainer;
const messageElement = document.createElement('div'); const chatBubble = createChatBubble(msg)
// Style based on sender/recipient // Style based on sender/recipient
if (targetPower === 'GLOBAL') { if (targetPower === 'GLOBAL') {
// Global chat shows sender info // Global chat shows sender info
const senderColor = msg.sender.toLowerCase(); 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 // Add the header with the sender name immediately
const headerSpan = document.createElement('span'); const headerSpan = document.createElement('span');
headerSpan.style.fontWeight = 'bold'; headerSpan.style.fontWeight = 'bold';
headerSpan.className = `power-${senderColor}`; headerSpan.className = `power-${senderColor}`;
headerSpan.textContent = `${getPowerDisplayName(msg.sender as PowerENUM)}: `; 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 { } else {
// Private chat - outgoing or incoming style // Private chat - outgoing or incoming style
const isOutgoing = msg.sender === gameState.currentPower; 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 // Add to container
messagesContainer.appendChild(messageElement); messagesContainer.appendChild(chatBubble);
// Scroll to bottom // Scroll to bottom
messagesContainer.scrollTop = messagesContainer.scrollHeight; messagesContainer.scrollTop = messagesContainer.scrollHeight;
if (animateWords) { if (animateWords) {
// Start word-by-word animation // Start word-by-word animation
const contentSpanId = `msg-content-${msgId.replace(/[^a-zA-Z0-9]/g, '-')}`; animateMessageWords(msg.message, contentSpan, messagesContainer, onComplete);
animateMessageWords(msg.message, contentSpanId, targetPower, messagesContainer, onComplete);
} else { } else {
// Show entire message at once // 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) { if (contentSpan) {
contentSpan.textContent = msg.message; contentSpan.textContent = msg.message;
} }
@ -342,59 +249,6 @@ function addMessageToChat(msg: MessageSchemaType, animateWords: boolean = false,
return true; // This was a new message 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 // Modified to support conditional sound effects
function animateHeadNod(msg, playSoundEffect = true) { function animateHeadNod(msg, playSoundEffect = true) {
@ -671,4 +525,3 @@ function playRandomSoundEffect() {
}); });
} }
} }

View file

@ -4,30 +4,32 @@
import { config } from "./config"; import { config } from "./config";
import { debugMenuInstance } from "./debug/debugMenu"; import { debugMenuInstance } from "./debug/debugMenu";
import { toggleEventQueueDisplayState } from "./debug/eventQueueDisplay";
import { debugMenu } from "./domElements";
export class ScheduledEvent { export class ScheduledEvent {
id: string; id: string;
callback: () => void; callback: () => void | Promise<void>;
resolved?: boolean; resolved?: boolean;
running!: boolean; running!: boolean;
error?: Error; // If the event caused an error, store it here. error?: Error; // If the event caused an error, store it here.
constructor(id: string, callback: () => void, resolved?: boolean) { promise?: Promise<void>;
constructor(id: string, callback: () => void | Promise<void>, resolved?: boolean) {
this.id = id this.id = id
this.callback = callback this.callback = callback
this.resolved = resolved ? true : resolved this.resolved = resolved ? true : resolved
} }
run = () => { run = () => {
this.running = true this.running = true
try { // Store the promise so we can track it
this.callback(); this.promise = Promise.resolve(this.callback())
} catch (e) { .then(() => {
this.error = e this.resolved = true;
} finally { })
.catch((e) => {
this.resolved = true this.error = e;
} this.resolved = true;
});
} }
} }
@ -35,13 +37,16 @@ export class EventQueue {
private events: ScheduledEvent[] = []; private events: ScheduledEvent[] = [];
private startTime: number = 0; private startTime: number = 0;
private isRunning: boolean = false; private isRunning: boolean = false;
private currentEventPromise?: Promise<void>;
/** /**
* Start the event queue with current time as reference * Start the event queue with current time as reference
*/ */
start(): void { start(): void {
this.isRunning = true; 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 { reset(resetCallback?: () => void): void {
this.events = []; this.events = [];
this.isRunning = false; this.isRunning = false;
this.currentEventPromise = undefined;
if (resetCallback) { if (resetCallback) {
resetCallback(); resetCallback();
} }
@ -86,15 +92,26 @@ export class EventQueue {
if (!this.isRunning) return; if (!this.isRunning) return;
if (this.events.length < 1) return; if (this.events.length < 1) return;
if (this.events[0].resolved) { const currentEvent = this.events[0];
if (this.events[0].error) {
console.error(this.events[0].error) // 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) { if (config.isDebugMode) {
debugMenuInstance.updateTools() debugMenuInstance.updateTools()
} }
this.events.shift() this.events.shift()
this.events[0].run() if (this.events.length > 0) {
this.events[0].run()
}
} }
} }

View file

@ -1,6 +1,6 @@
import * as THREE from "three" import * as THREE from "three"
import { type CoordinateData, CoordinateDataSchema, PowerENUM } from "./types/map" 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 { GameSchema } from "./types/gameState";
import { debugMenuInstance } from "./debug/debugMenu.ts" import { debugMenuInstance } from "./debug/debugMenu.ts"
import { config } from "./config.ts" import { config } from "./config.ts"
@ -18,6 +18,7 @@ import { EventQueue, ScheduledEvent } from "./events.ts";
import { createAnimateUnitsEvent, createAnimationsForNextPhase } from "./units/animate.ts"; import { createAnimateUnitsEvent, createAnimationsForNextPhase } from "./units/animate.ts";
import { createUpdateNewsBannerEvent } from "./components/newsBanner.ts"; import { createUpdateNewsBannerEvent } from "./components/newsBanner.ts";
import { createMomentEvent } from "./components/momentModal.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 //FIXME: This whole file is a mess. Need to organize and format
@ -112,6 +113,16 @@ class GameAudio {
constructor() { constructor() {
this.players = [{ Name: "background_music", track: initializeBackgroundAudio() }] 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) => { pause = (track_idx?: number | undefined) => {
if (!track_idx) { if (!track_idx) {
@ -196,7 +207,7 @@ class GameState {
} }
_fillEventQueue = (gameData: GameSchemaType) => { _fillEventQueue = (gameData: GameSchemaType) => {
for (let phase of gameData.phases) { for (let [phaseIdx, phase] of gameData.phases.entries()) {
// Update Phase Display // Update Phase Display
let updateUIEvent = createUpdateUIEvent(phase) let updateUIEvent = createUpdateUIEvent(phase)
this.eventQueue.schedule(updateUIEvent) this.eventQueue.schedule(updateUIEvent)
@ -216,13 +227,37 @@ class GameState {
if (phaseMoment) { if (phaseMoment) {
this.eventQueue.schedule(createMomentEvent(phaseMoment)) this.eventQueue.schedule(createMomentEvent(phaseMoment))
} }
let animationEvents = createAnimateUnitsEvent(phase) if (!(phaseIdx === 0)) {
this.eventQueue.schedule(animationEvents)
let animationEvents = createAnimateUnitsEvent(phase, phaseIdx)
this.eventQueue.schedule(animationEvents)
}
// Lastly, update the gamePhase id // 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 * Load game data from a JSON string and initialize the game state

View file

@ -264,16 +264,17 @@ function createHoldAnimation(unitMesh: THREE.Group): Tween {
return growTween; return growTween;
} }
export function createAnimateUnitsEvent(phase: GamePhase): ScheduledEvent { export function createAnimateUnitsEvent(phase: GamePhase, phaseIdx: number): ScheduledEvent {
return new ScheduledEvent(`createAnimations-${phase.name}`, () => createAnimationsForNextPhase()) return new ScheduledEvent(`createAnimations-${phase.name}`, () => createAnimationsForNextPhase(phaseIdx))
} }
/** /**
* Creates animations for unit movements based on orders from the previous phase * Creates animations for unit movements based on orders from the previous phase
* *
**/ **/
export function createAnimationsForNextPhase() { export function createAnimationsForNextPhase(phaseIdx: number) {
let previousPhase = gameState.gameData?.phases[gameState.phaseIndex == 0 ? 0 : gameState.phaseIndex - 1] 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"] // const sequence = ["build", "disband", "hold", "move", "bounce", "retreat"]
// Safety check - if no previous phase or no orders, return // Safety check - if no previous phase or no orders, return
if (!previousPhase) { if (!previousPhase) {
@ -305,7 +306,7 @@ export function createAnimationsForNextPhase() {
order.type = "bounce" order.type = "bounce"
} }
// If the result is void, that means the move was not valid? // 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 let unitIndex = -1
unitIndex = getUnit(order, power); unitIndex = getUnit(order, power);