diff --git a/ai_animation/src/config.ts b/ai_animation/src/config.ts new file mode 100644 index 0000000..f102d6f --- /dev/null +++ b/ai_animation/src/config.ts @@ -0,0 +1,4 @@ +export const config = { + playbackSpeed: 500, // Default speed in ms + isDebugMode: true +} diff --git a/ai_animation/src/domElements/chatWindows.ts b/ai_animation/src/domElements/chatWindows.ts index fe9d331..b6ce9c8 100644 --- a/ai_animation/src/domElements/chatWindows.ts +++ b/ai_animation/src/domElements/chatWindows.ts @@ -1,5 +1,6 @@ import * as THREE from "three"; -import { currentPower } from "../gameState"; +import { currentPower, gameState } from "../gameState"; +import { config } from "../config"; let faceIconCache = {}; // Cache for generated face icons @@ -128,9 +129,9 @@ function createChatWindow(power, isGlobal = false) { } // Modified to accumulate messages instead of resetting and only animate for new messages -function updateChatWindows(phase, stepMessages = false) { +export function updateChatWindows(phase, stepMessages = false) { if (!phase.messages || !phase.messages.length) { - messagesPlaying = false; + gameState.messagesPlaying = false; return; } @@ -153,19 +154,19 @@ function updateChatWindows(phase, stepMessages = false) { animateHeadNod(msg, (messageCounter % 3 === 0)); } }); - messagesPlaying = false; + gameState.messagesPlaying = false; } else { // Stepwise - messagesPlaying = true; + gameState.messagesPlaying = true; let index = 0; // Define the showNext function that will be called after each message animation completes const showNext = () => { if (index >= relevantMessages.length) { - messagesPlaying = false; - if (unitAnimations.length === 0 && isPlaying && !isSpeaking) { + gameState.messagesPlaying = false; + if (gameState.isAnimating && gameState.isPlaying && !gameState.isSpeaking) { // Call the async function without awaiting it here - playbackTimer = setTimeout(() => advanceToNextPhase(), playbackSpeed); + gameState.playbackTimer = setTimeout(() => advanceToNextPhase(), config.playbackSpeed); } return; } @@ -175,13 +176,13 @@ function updateChatWindows(phase, stepMessages = false) { const isNew = addMessageToChat(msg, phase.name, true, showNext); // Pass showNext as callback - if (isNew && !isDebugMode) { + if (isNew && !config.isDebugMode) { // Increment message counter messageCounter++; // Only animate head and play sound for every third message animateHeadNod(msg, (messageCounter % 3 === 0)); - } else if (isDebugMode) { + } else if (config.isDebugMode) { // In debug mode, immediately call showNext to skip waiting for animation showNext(); } else { @@ -309,7 +310,7 @@ function animateMessageWords(message, contentSpanId, targetPower, messagesContai if (onComplete) { onComplete(); // Call the completion callback } - }, Math.min(playbackSpeed / 3, 150)); + }, Math.min(config.playbackSpeed / 3, 150)); return; } @@ -324,7 +325,7 @@ function animateMessageWords(message, contentSpanId, targetPower, messagesContai wordIndex++; // Schedule the next word with a delay based on word length and playback speed - const delay = Math.max(30, Math.min(120, playbackSpeed / 10 * (words[wordIndex - 1].length / 4))); + const delay = Math.max(30, Math.min(120, config.playbackSpeed / 10 * (words[wordIndex - 1].length / 4))); setTimeout(addNextWord, delay); // Scroll to ensure newest content is visible diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index 565a0c7..9679c45 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -7,6 +7,7 @@ import { createChatWindows } from "./domElements/chatWindows"; import { logger } from "./logger"; import { OrbitControls } from "three/examples/jsm/Addons.js"; import { displayInitialPhase } from "./phase"; +import * as TWEEN from "@tweenjs/tween.js"; //FIXME: This whole file is a mess. Need to organize and format // @@ -27,13 +28,16 @@ export const currentPower = getRandomPower(); class GameState { - boardState: CoordinateData | null + boardState: CoordinateData gameData: GameSchemaType | null phaseIndex: number boardName: string - isSpeaking: boolean + + // state locks messagesPlaying: boolean isPlaying: boolean + isSpeaking: boolean + isAnimating: boolean //Scene for three.js scene: THREE.Scene @@ -43,19 +47,26 @@ class GameState { camera: THREE.PerspectiveCamera renderer: THREE.WebGLRenderer - //Unit Meshes unitMeshes: THREE.Group[] + // Animations needed for this turn + unitAnimations: TWEEN.Tween[] + + // + playbackTimer: number constructor(boardName: AvailableMaps) { this.phaseIndex = 0 - this.boardState = null this.gameData = null this.boardName = boardName + // State locks this.isSpeaking = false this.isPlaying = false + this.isAnimating = false this.messagesPlaying = false + this.scene = new THREE.Scene() this.unitMeshes = [] + this.unitAnimations = [] } loadGameData = (gameDataString: string): Promise => { diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index 195ba77..84a8172 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -1,25 +1,22 @@ import * as THREE from "three"; import "./style.css" -import { UnitMesh } from "./types/units"; import { initMap } from "./map/create"; -import { createTweenAnimations, proccessUnitAnimationWithTween } from "./units/animate"; -import type { UnitAnimation } from "./units/animate"; +import { createTweenAnimations } from "./units/animate"; +import * as TWEEN from "@tweenjs/tween.js"; import { gameState } from "./gameState"; import { logger } from "./logger"; -import { loadBtn, prevBtn, nextBtn, speedSelector, fileInput, playBtn, mapView, loadGameBtnFunction, phaseDisplay } from "./domElements"; -import { updateLeaderboard, updateMapOwnership, updateSupplyCenterOwnership } from "./map/state"; +import { loadBtn, prevBtn, nextBtn, speedSelector, fileInput, playBtn, mapView, loadGameBtnFunction } from "./domElements"; +import { updateChatWindows } from "./domElements/chatWindows"; +import { displayPhaseWithAnimation } from "./phase"; +import { config } from "./config"; //TODO: Create a function that finds a suitable unit location within a given polygon, for placing units better // Currently the location for label, unit, and SC are all the same manually picked location //const isDebugMode = process.env.NODE_ENV === 'development' || localStorage.getItem('debug') === 'true'; -const isDebugMode = true; +const isDebugMode = config.isDebugMode; // --- CORE VARIABLES --- -let unitMeshes: UnitMesh[] = []; // To store references for units + supply center 3D objects -let playbackSpeed = 500; // Default speed in ms -let playbackTimer = null; // Timer reference for playback -let unitAnimations: UnitAnimation[] = []; // Track ongoing unit animations let cameraPanTime = 0; // Timer that drives the camera panning const cameraPanSpeed = 0.0005; // Smaller = slower @@ -36,9 +33,7 @@ function initScene() { dirLight.position.set(300, 400, 300); gameState.scene.add(dirLight); - - - // Load coordinate data, then build the fallback map + // Load coordinate data, then build the map gameState.loadBoardState().then(() => { initMap(gameState.scene).then(() => { // Load default game file if in debug mode @@ -65,7 +60,6 @@ function initScene() { function animate() { requestAnimationFrame(animate); - if (gameState.isPlaying) { // Pan camera slowly in playback mode cameraPanTime += cameraPanSpeed; @@ -78,11 +72,10 @@ function animate() { ); // If messages are done playing but we haven't started unit animations yet - if (!gameState.messagesPlaying && !gameState.isSpeaking && unitAnimations.length === 0 && gameState.isPlaying) { + if (!gameState.messagesPlaying && !gameState.isSpeaking && gameState.unitAnimations.length === 0 && gameState.isPlaying) { if (gameState.gameData && gameState.gameData.phases) { const prevIndex = gameState.phaseIndex > 0 ? gameState.phaseIndex - 1 : gameState.gameData.phases.length - 1; - unitAnimations = createTweenAnimations( - unitMeshes, + createTweenAnimations( gameState.gameData.phases[gameState.phaseIndex], gameState.gameData.phases[prevIndex] ); @@ -92,21 +85,18 @@ function animate() { gameState.camControls.update(); } - // Process unit movement animations - if (unitAnimations && unitAnimations.length > 0) { + // Process unit movement animations using TWEEN.js update - unitAnimations.forEach((anim: UnitAnimation, index) => { - let isFinished = proccessUnitAnimationWithTween(anim) - // Animation complete, remove from active animations - if (isFinished) { - unitAnimations.splice(index, 1); - } + // Check if all animations are complete + if (gameState.unitAnimations.length > 0) { + // Filter out completed animations + gameState.unitAnimations = gameState.unitAnimations.filter(anim => anim.isPlaying()); + gameState.unitAnimations.forEach((anim) => anim.update()) - }); - // >>> MODIFIED: Check if messages are still playing before advancing - if (unitAnimations.length === 0 && gameState.isPlaying && !gameState.messagesPlaying) { + // If all animations are complete and we're in playback mode + if (gameState.unitAnimations.length === 0 && gameState.isPlaying && !gameState.messagesPlaying) { // Schedule next phase after a pause delay - playbackTimer = setTimeout(() => advanceToNextPhase(), playbackSpeed); + gameState.playbackTimer = setTimeout(() => advanceToNextPhase(), config.playbackSpeed); } } @@ -173,11 +163,11 @@ function togglePlayback() { if (!gameState.gameData || gameState.gameData.phases.length <= 1) return; // NEW: If we're speaking, don't allow toggling playback - if (isSpeaking) return; + if (gameState.isSpeaking) return; - isPlaying = !isPlaying; + gameState.isPlaying = !gameState.isPlaying; - if (isPlaying) { + if (gameState.isPlaying) { playBtn.textContent = "⏸ Pause"; prevBtn.disabled = true; nextBtn.disabled = true; @@ -193,79 +183,17 @@ function togglePlayback() { } } else { playBtn.textContent = "▶ Play"; - if (playbackTimer) { - clearTimeout(playbackTimer); - playbackTimer = null; + if (gameState.playbackTimer) { + clearTimeout(gameState.playbackTimer); + gameState.playbackTimer = null; } - unitAnimations = []; - messagesPlaying = false; + gameState.messagesPlaying = false; prevBtn.disabled = false; nextBtn.disabled = false; } } -// --- MODIFIED: Update news banner before TTS --- -async function advanceToNextPhase() { - // Only show a summary if we have at least started the first phase - // and only if the just-ended phase has a "summary" property. - if (gameState.gameData && gameState.gameData.phases && gameState.phaseIndex >= 0) { - const justEndedPhase = gameState.gameData.phases[gameState.phaseIndex]; - if (justEndedPhase.summary && justEndedPhase.summary.trim() !== '') { - // UPDATED: First update the news banner with full summary - addToNewsBanner(`(${justEndedPhase.name}) ${justEndedPhase.summary}`); - // Then speak the summary (will be truncated internally) - await speakSummary(justEndedPhase.summary); - } - } - - // If we've reached the end, loop back to the beginning - if (gameState.phaseIndex >= gameState.gameData.phases.length - 1) { - gameState.phaseIndex = 0; - } else { - gameState.phaseIndex++; - } - - // Display the new phase with animation - displayPhaseWithAnimation(gameState.phaseIndex); -} - -function displayPhaseWithAnimation(index) { - if (!gameState.gameData || !gameState.gameData.phases || index < 0 || index >= gameState.gameData.phases.length) { - logger.log("Invalid phase index.") - return; - } - - const prevIndex = index > 0 ? index - 1 : gameState.gameData.phases.length - 1; - const currentPhase = gameState.gameData.phases[index]; - const previousPhase = gameState.gameData.phases[prevIndex]; - - phaseDisplay.textContent = `Era: ${currentPhase.name || 'Unknown Era'} (${index + 1}/${gameState.gameData.phases.length})`; - - // Rebuild supply centers, remove old units - - // First show messages, THEN animate units after - // First show messages with stepwise animation - updateChatWindows(currentPhase, true); - - - // Ownership - if (currentPhase.state?.centers) { - updateSupplyCenterOwnership(currentPhase.state.centers); - } - - // Update leaderboard - updateLeaderboard(currentPhase); - updateMapOwnership(currentPhase) - - unitAnimations = createTweenAnimations(unitMeshes, currentPhase, previousPhase); - let msg = `Phase: ${currentPhase.name}\nSCs: ${JSON.stringify(currentPhase.state.centers)} \nUnits: ${currentPhase.state?.units ? JSON.stringify(currentPhase.state.units) : 'None'} ` - // Panel - - // Add: Update info panel - logger.updateInfoPanel(); - -} // --- EVENT HANDLERS --- loadBtn.addEventListener('click', () => fileInput.click()); @@ -292,40 +220,16 @@ nextBtn.addEventListener('click', () => { playBtn.addEventListener('click', togglePlayback); speedSelector.addEventListener('change', e => { - playbackSpeed = parseInt(e.target.value); + config.playbackSpeed = parseInt(e.target.value); // If we're currently playing, restart the timer with the new speed - if (gameState.isPlaying && playbackTimer) { - clearTimeout(playbackTimer); - playbackTimer = setTimeout(() => advanceToNextPhase(), playbackSpeed); + if (gameState.isPlaying && gameState.playbackTimer) { + clearTimeout(gameState.playbackTimer); + gameState.playbackTimer = setTimeout(() => advanceToNextPhase(), config.playbackSpeed); } }); // --- BOOTSTRAP ON PAGE LOAD --- window.addEventListener('load', initScene); -// Utility functions for color manipulation -function lightenColor(hex, percent) { - return colorShift(hex, percent); -} - -function darkenColor(hex, percent) { - return colorShift(hex, -percent); -} - -function colorShift(hex, percent) { - // Convert hex to RGB - let r = parseInt(hex.substr(1, 2), 16); - let g = parseInt(hex.substr(3, 2), 16); - let b = parseInt(hex.substr(5, 2), 16); - - // Shift color by percentage - r = Math.min(255, Math.max(0, r + Math.floor(r * percent / 100))); - g = Math.min(255, Math.max(0, g + Math.floor(g * percent / 100))); - b = Math.min(255, Math.max(0, b + Math.floor(b * percent / 100))); - - // Convert back to hex - return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')} `; -} - diff --git a/ai_animation/src/map/create.ts b/ai_animation/src/map/create.ts index abc3a07..aabbe4b 100644 --- a/ai_animation/src/map/create.ts +++ b/ai_animation/src/map/create.ts @@ -89,6 +89,8 @@ export function initMap(scene): Promise { const box = new THREE.Box3().setFromObject(group); const center = new THREE.Vector3(); box.getCenter(center); + gameState.camera.position.set(center.x, center.y + 1100, 1600) + gameState.camControls.target = center scene.add(group); diff --git a/ai_animation/src/map/state.ts b/ai_animation/src/map/state.ts index 4f74ee8..f8e74a1 100644 --- a/ai_animation/src/map/state.ts +++ b/ai_animation/src/map/state.ts @@ -2,6 +2,7 @@ import { getPowerHexColor } from "../units/create"; import { gameState } from "../gameState"; import { leaderboard } from "../domElements"; import type { GamePhase } from "../types/gameState"; +import { ProvTypeENUM } from "../types/map"; export function updateSupplyCenterOwnership(centers) { @@ -112,7 +113,7 @@ export function updateMapOwnership(currentPhase: GamePhase) { } for (const [key, value] of Object.entries(gameState.boardState.provinces)) { // Update the color of the provinces if needed - if (gameState.boardState.provinces[key].owner) { + if (gameState.boardState.provinces[key].owner && gameState.boardState?.provinces[key].type != ProvTypeENUM.WATER) { let powerColor = getPowerHexColor(gameState.boardState.provinces[key].owner) let powerColorHex = parseInt(powerColor.substring(1), 16); gameState.boardState.provinces[key].mesh?.material.color.setHex(powerColorHex) diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index a7c0bce..c2ef461 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -4,6 +4,8 @@ import { phaseDisplay } from "./domElements"; import { createSupplyCenters } from "./units/create"; import { createUnitMesh } from "./units/create"; import { updateSupplyCenterOwnership, updateLeaderboard, updateMapOwnership } from "./map/state"; +import { updateChatWindows } from "./domElements/chatWindows"; +import { createTweenAnimations } from "./units/animate"; // New function to display initial state without messages export function displayInitialPhase() { @@ -56,3 +58,65 @@ export function displayInitialPhase() { logger.updateInfoPanel(); } + +export function displayPhaseWithAnimation(index) { + if (!gameState.gameData || !gameState.gameData.phases || index < 0 || index >= gameState.gameData.phases.length) { + logger.log("Invalid phase index.") + return; + } + + const prevIndex = index > 0 ? index - 1 : gameState.gameData.phases.length - 1; + const currentPhase = gameState.gameData.phases[index]; + const previousPhase = gameState.gameData.phases[prevIndex]; + + phaseDisplay.textContent = `Era: ${currentPhase.name || 'Unknown Era'} (${index + 1}/${gameState.gameData.phases.length})`; + + // Rebuild supply centers, remove old units + + // First show messages, THEN animate units after + // First show messages with stepwise animation + updateChatWindows(currentPhase, true); + + + // Ownership + if (currentPhase.state?.centers) { + updateSupplyCenterOwnership(currentPhase.state.centers); + } + + // Update leaderboard + updateLeaderboard(currentPhase); + updateMapOwnership(currentPhase) + + createTweenAnimations(currentPhase, previousPhase); + let msg = `Phase: ${currentPhase.name}\nSCs: ${JSON.stringify(currentPhase.state.centers)} \nUnits: ${currentPhase.state?.units ? JSON.stringify(currentPhase.state.units) : 'None'} ` + // Panel + + // Add: Update info panel + logger.updateInfoPanel(); + +} + +async function advanceToNextPhase() { + // Only show a summary if we have at least started the first phase + // and only if the just-ended phase has a "summary" property. + if (gameState.gameData && gameState.gameData.phases && gameState.phaseIndex >= 0) { + const justEndedPhase = gameState.gameData.phases[gameState.phaseIndex]; + if (justEndedPhase.summary && justEndedPhase.summary.trim() !== '') { + // UPDATED: First update the news banner with full summary + addToNewsBanner(`(${justEndedPhase.name}) ${justEndedPhase.summary}`); + + // Then speak the summary (will be truncated internally) + await speakSummary(justEndedPhase.summary); + } + } + + // If we've reached the end, loop back to the beginning + if (gameState.phaseIndex >= gameState.gameData.phases.length - 1) { + gameState.phaseIndex = 0; + } else { + gameState.phaseIndex++; + } + + // Display the new phase with animation + displayPhaseWithAnimation(gameState.phaseIndex); +} diff --git a/ai_animation/src/types/unitOrders.ts b/ai_animation/src/types/unitOrders.ts index 936f5f9..00ea495 100644 --- a/ai_animation/src/types/unitOrders.ts +++ b/ai_animation/src/types/unitOrders.ts @@ -79,3 +79,4 @@ export const OrderFromString = z.string().transform((orderStr) => { throw new Error(`Order format not recognized: ${orderStr}`); } }); +export type UnitOrder = z.infer diff --git a/ai_animation/src/units/animate.ts b/ai_animation/src/units/animate.ts index ad65031..c19d3f0 100644 --- a/ai_animation/src/units/animate.ts +++ b/ai_animation/src/units/animate.ts @@ -3,8 +3,9 @@ import type { GamePhase } from "../types/gameState"; import { createUnitMesh } from "./create"; import { UnitMesh } from "../types/units"; import { getProvincePosition } from "../map/utils"; -import { Tween } from "@tweenjs/tween.js"; +import * as TWEEN from "@tweenjs/tween.js"; import { gameState } from "../gameState"; +import type { UnitOrder } from "../types/unitOrders"; //FIXME: Move this to a file with all the constants let animationDuration = 1500; // Duration of unit movement animation in ms @@ -22,79 +23,71 @@ export type UnitAnimation = { animationType?: AnimationTypeENUM } +function getUnit(unitOrder: UnitOrder) { + let posUnits = gameState.unitMeshes.filter((unit) => { + return (unit.userData.province === unitOrder.unit.origin && unit.userData.type === unitOrder.unit.type) + }) + // TODO: Need to do something here if we get multiple results + return gameState.unitMeshes.indexOf(posUnits[0]) + +} -export function createTweenAnimations(unitMeshes: UnitMesh[], currentPhase: GamePhase, previousPhase: GamePhase | null): Tween[] { +export function createTweenAnimations(currentPhase: GamePhase, previousPhase: GamePhase | null) { + for (const [power, orders] of Object.entries(previousPhase.orders)) { + for (const order of orders) { + let unitIndex = getUnit(order); + if (unitIndex === -1) continue; // Skip if unit not found + switch (order.type) { + case "move": + let destinationVector = getProvincePosition(gameState.boardState, order.destination); + if (!destinationVector) continue; // Skip if destination not found - let unitAnimations: Tween[] = [] + // Create a tween for smooth movement + let anim = new TWEEN.Tween(gameState.unitMeshes[unitIndex].position) + .to({ + x: destinationVector.x, + y: 10, // Keep consistent height + z: destinationVector.z + }, 1500) + .easing(TWEEN.Easing.Quadratic.InOut) // Add easing for smoother motion + .onUpdate(() => { + // Add a slight bobbing effect during movement + gameState.unitMeshes[unitIndex].position.y = 10 + Math.sin(Date.now() * 0.05) * 2; - for (const [power, orders] of Object.entries(currentPhase.orders)) { - for (const order in parseOrders(orders)) { + // For fleets, add a gentle rocking motion + if (gameState.unitMeshes[unitIndex].userData.type === 'F') { + gameState.unitMeshes[unitIndex].rotation.z = Math.sin(Date.now() * 0.03) * 0.1; + gameState.unitMeshes[unitIndex].rotation.x = Math.sin(Date.now() * 0.02) * 0.1; + } + }) + .onComplete(() => { + // Update the unit's province data when animation completes - } + // Reset height and rotation + gameState.unitMeshes[unitIndex].position.y = 10; + if (gameState.unitMeshes[unitIndex].userData.type === 'F') { + gameState.unitMeshes[unitIndex].rotation.z = 0; + gameState.unitMeshes[unitIndex].rotation.x = 0; + } + }) + .start(); - } + gameState.unitMeshes[unitIndex].userData.province = order.destination; + gameState.unitAnimations.push(anim); + break; - - // Prepare unit position maps - const previousUnitPositions = {}; - if (previousPhase.state?.units) { - for (const [power, unitArr] of Object.entries(previousPhase.state.units)) { - unitArr.forEach(unitStr => { - const match = unitStr.match(/^([AF])\s+(.+)$/); - if (match) { - const key = `${power}-${match[1]}-${match[2]}`; - previousUnitPositions[key] = getProvincePosition(gameState.boardState, match[2]); - } - }); + case "disband": + gameState.scene.remove(gameState.unitMeshes[unitIndex]); + // Remove from unitMeshes array + gameState.unitMeshes.splice(unitIndex, 1); + break; + case "bounce": + break; + } } } - - // Animate new units from old positions (or spawn from below) - if (currentPhase.state?.units) { - for (const [power, unitArr] of Object.entries(currentPhase.state.units)) { - unitArr.forEach(unitStr => { - // Check if is fleet or army order - const armyOrFleetOrder = unitStr.match(/^([AF])\s+(.+)$/); - if (!armyOrFleetOrder) return; - const unitType = armyOrFleetOrder[1]; - const location = armyOrFleetOrder[2]; - - // Current final - const currentPos = getProvincePosition(gameState.boardState, location); - - let startPos; - let matchFound = false; - for (const prevKey in previousUnitPositions) { - if (prevKey.startsWith(`${power}-${unitType}`)) { - startPos = previousUnitPositions[prevKey]; - matchFound = true; - delete previousUnitPositions[prevKey]; - break; - } - } - if (!matchFound) { - // TODO: Add a spawn animation? - // - // New spawn - startPos = { x: currentPos.x, y: -20, z: currentPos.z }; - } - - const unitMesh = createUnitMesh({ - power: power, - province: location, - type: unitType, - }); - unitMesh.position.set(startPos.x, 10, startPos.z); - let tween = new Tween(unitMesh.position).to(currentPos, 1500).start() - - // Animate - unitAnimations.push(tween); - }); - } - } - return unitAnimations } export function createAnimationsForPhaseTransition(unitMeshes: UnitMesh[], currentPhase: GamePhase, previousPhase: GamePhase | null): UnitAnimation[] { let unitAnimations: UnitAnimation[] = [] diff --git a/ai_animation/src/units/create.ts b/ai_animation/src/units/create.ts index 763b336..2606b75 100644 --- a/ai_animation/src/units/create.ts +++ b/ai_animation/src/units/create.ts @@ -123,7 +123,7 @@ export function createSupplyCenters(): THREE.Group[] { } return supplyCenterMeshes } -export function createUnitMesh(unitData: UnitData): UnitMesh { +export function createUnitMesh(unitData: UnitData): THREE.Group { const color = getPowerHexColor(unitData.power); let group: THREE.Group | null; @@ -140,7 +140,7 @@ export function createUnitMesh(unitData: UnitData): UnitMesh { group.userData = { power: unitData.power, type: unitData.type, - location: unitData.province + province: unitData.province }; return group;