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 { 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<void>((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<void> {
// 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<void>((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();
}

View file

@ -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

View file

@ -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;

View file

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

View file

@ -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<void>;
resolved?: boolean;
running!: boolean;
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.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<void>;
/**
* 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()
}
}
}

View file

@ -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

View file

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