diff --git a/ai_animation/src/domElements/chatWindows.ts b/ai_animation/src/domElements/chatWindows.ts index 8882340..938a0f6 100644 --- a/ai_animation/src/domElements/chatWindows.ts +++ b/ai_animation/src/domElements/chatWindows.ts @@ -1,5 +1,5 @@ import * as THREE from "three"; -import { currentPower, gameState } from "../gameState"; +import { gameState } from "../gameState"; import { config } from "../config"; import { advanceToNextPhase } from "../phase"; @@ -12,6 +12,9 @@ let chatWindows = {}; // Store chat window elements by power export function createChatWindows() { // Clear existing chat windows const chatContainer = document.getElementById('chat-container'); + if (!chatContainer) { + throw new Error("Could not get element with ID 'chat-container'") + } chatContainer.innerHTML = ''; chatWindows = {}; diff --git a/ai_animation/src/logger.ts b/ai_animation/src/logger.ts index 9c266a7..78fea92 100644 --- a/ai_animation/src/logger.ts +++ b/ai_animation/src/logger.ts @@ -71,7 +71,7 @@ class Logger { if (centers) { // Count supply centers for each power - Object.entries(centers).forEach(([center, power]) => { + Object.entries(centers).forEach(([_, power]) => { if (power && typeof power === 'string' && power in counts) { counts[power as keyof typeof counts]++; } diff --git a/ai_animation/src/map/state.ts b/ai_animation/src/map/state.ts index 890f440..6d66b95 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 { ProvTypeENUM, PowerENUM } from "../types/map"; +import { MeshBasicMaterial } from "three"; export function updateSupplyCenterOwnership(centers) { @@ -98,8 +99,8 @@ export function updateMapOwnership() { let currentPhase = gameState.gameData?.phases[gameState.phaseIndex] if (currentPhase === undefined) { throw "Currentphase is undefined for index " + gameState.phaseIndex; - } + // Clear existing ownership to avoid stale data for (const key in gameState.boardState.provinces) { if (gameState.boardState.provinces[key].owner) { @@ -107,7 +108,7 @@ export function updateMapOwnership() { } } - for (let powerKey of Object.keys(currentPhase.state.influence)) { + for (let powerKey of Object.keys(currentPhase.state.influence) as Array) { for (let provKey of currentPhase.state.influence[powerKey]) { const province = gameState.boardState.provinces[provKey]; @@ -118,11 +119,11 @@ export function updateMapOwnership() { if (province.owner) { let powerColor = getPowerHexColor(province.owner); let powerColorHex = parseInt(powerColor.substring(1), 16); - province.mesh?.material.color.setHex(powerColorHex); + (province.mesh?.material as MeshBasicMaterial).color.setHex(powerColorHex); } else if (province.owner === undefined && province.mesh !== undefined) { let powerColor = getPowerHexColor(undefined); let powerColorHex = parseInt(powerColor.substring(1), 16); - province.mesh.material.color.setHex(powerColorHex) + (province.mesh.material as MeshBasicMaterial).color.setHex(powerColorHex) } } } diff --git a/ai_animation/src/map/utils.ts b/ai_animation/src/map/utils.ts index d0831eb..1c255a2 100644 --- a/ai_animation/src/map/utils.ts +++ b/ai_animation/src/map/utils.ts @@ -1,6 +1,6 @@ import { gameState } from "../gameState"; -export function getProvincePosition(loc) { +export function getProvincePosition(loc: string) { // Convert e.g. "Spa/sc" to "SPA_SC" if needed const normalized = loc.toUpperCase().replace('/', '_'); const base = normalized.split('_')[0]; diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index c68d293..01b0cab 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -1,7 +1,7 @@ import { gameState } from "./gameState"; import { logger } from "./logger"; import { updatePhaseDisplay } from "./domElements"; -import { createSupplyCenters, createUnitMesh, initUnits } from "./units/create"; +import { initUnits } from "./units/create"; import { updateSupplyCenterOwnership, updateLeaderboard, updateMapOwnership } from "./map/state"; import { updateChatWindows, addToNewsBanner } from "./domElements/chatWindows"; import { createAnimationsForNextPhase } from "./units/animate"; diff --git a/ai_animation/src/speech.ts b/ai_animation/src/speech.ts index 4db2cbe..9d67f18 100644 --- a/ai_animation/src/speech.ts +++ b/ai_animation/src/speech.ts @@ -9,7 +9,7 @@ try { if (import.meta.env.VITE_ELEVENLABS_API_KEY) { ELEVENLABS_API_KEY = String(import.meta.env.VITE_ELEVENLABS_API_KEY).trim(); // Simplified logging - } + } // Fallback to the direct env variable (for dev environments) else if (import.meta.env.ELEVENLABS_API_KEY) { ELEVENLABS_API_KEY = String(import.meta.env.ELEVENLABS_API_KEY).trim(); @@ -37,7 +37,7 @@ async function testElevenLabsKey() { console.warn("Cannot test API key - none provided"); return; } - + try { const response = await fetch('https://api.elevenlabs.io/v1/voices', { method: 'GET', @@ -45,11 +45,10 @@ async function testElevenLabsKey() { 'xi-api-key': ELEVENLABS_API_KEY } }); - + if (response.ok) { console.log("✅ ElevenLabs API key is valid and ready for TTS"); } else { - const errorText = await response.text().catch(() => 'No error details available'); console.error(`❌ ElevenLabs API key invalid: ${response.status}`); } } catch (error) { @@ -69,7 +68,7 @@ export async function speakSummary(summaryText: string): Promise { console.warn("No summary text provided to speakSummary function"); return; } - + // Check if the summary is in JSON format and extract the actual summary text let textToSpeak = summaryText; try { @@ -85,7 +84,7 @@ export async function speakSummary(summaryText: string): Promise { } catch (error) { console.warn("Failed to parse summary as JSON"); } - + if (!ELEVENLABS_API_KEY) { console.warn("No ElevenLabs API key found. Skipping TTS."); return; @@ -97,14 +96,14 @@ export async function speakSummary(summaryText: string): Promise { try { // Truncate text to first 100 characters for ElevenLabs const truncatedText = textToSpeak.substring(0, 100); - + // Hit ElevenLabs TTS endpoint with the truncated text const headers = { "xi-api-key": ELEVENLABS_API_KEY, "Content-Type": "application/json", "Accept": "audio/mpeg" }; - + const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}`, { method: "POST", headers: headers, diff --git a/ai_animation/src/types/gameState.ts b/ai_animation/src/types/gameState.ts index 47be81d..d87b19b 100644 --- a/ai_animation/src/types/gameState.ts +++ b/ai_animation/src/types/gameState.ts @@ -18,7 +18,7 @@ const PhaseSchema = z.object({ units: z.record(PowerENUMSchema, z.array(z.string())), centers: z.record(PowerENUMSchema, z.array(ProvinceENUMSchema)), homes: z.record(PowerENUMSchema, z.array(z.string())), - influence: z.record(PowerENUMSchema, z.array(z.string())), + influence: z.record(PowerENUMSchema, z.array(ProvinceENUMSchema)), }), year: z.number().optional(), summary: z.string().optional(), diff --git a/ai_animation/src/types/map.ts b/ai_animation/src/types/map.ts index df81c36..8976706 100644 --- a/ai_animation/src/types/map.ts +++ b/ai_animation/src/types/map.ts @@ -54,7 +54,7 @@ export const CoordinateDataSchema = z.object({ export type Province = z.infer; export type CoordinateData = z.infer; -enum ProvinceENUM { +export enum ProvinceENUM { ANK = "ANK", ARM = "ARM", CON = "CON", @@ -131,6 +131,10 @@ enum ProvinceENUM { AEG = "AEG", EAS = "EAS", BLA = "BLA", + NAO = "NAO", + MAO = "MAO", + TYS = "TYS" + } export const ProvinceENUMSchema = z.nativeEnum(ProvinceENUM) diff --git a/ai_animation/src/types/unitOrders.ts b/ai_animation/src/types/unitOrders.ts index 0ab2229..738a18b 100644 --- a/ai_animation/src/types/unitOrders.ts +++ b/ai_animation/src/types/unitOrders.ts @@ -58,7 +58,7 @@ export const OrderFromString = z.string().transform((orderStr) => { return { type: "retreat", unit: { type: unitType, origin }, - destination: tokens[-1], + destination: tokens.at(-1), raw: orderStr } } diff --git a/ai_animation/src/types/units.ts b/ai_animation/src/types/units.ts index b0c83ba..51bac50 100644 --- a/ai_animation/src/types/units.ts +++ b/ai_animation/src/types/units.ts @@ -4,7 +4,7 @@ import { PowerENUM } from "./map" export enum UnitTypeENUM { A = "A", - F = "Fleet" + F = "F" } export type UnitData = { diff --git a/ai_animation/src/units/animate.ts b/ai_animation/src/units/animate.ts index be26c94..d3004a9 100644 --- a/ai_animation/src/units/animate.ts +++ b/ai_animation/src/units/animate.ts @@ -6,7 +6,7 @@ import { gameState } from "../gameState"; import type { UnitOrder } from "../types/unitOrders"; import { logger } from "../logger"; import { config } from "../config"; // Assuming config is defined in a separate file -import { PowerENUM } from "../types/map"; +import { PowerENUM, ProvinceENUM, ProvTypeENUM } from "../types/map"; import { UnitTypeENUM } from "../types/units"; //FIXME: Move this to a file with all the constants @@ -42,12 +42,50 @@ function getUnit(unitOrder: UnitOrder, power: string) { return gameState.unitMeshes.indexOf(posUnits[0]); } -function createSpawnAnimation(newUnitMesh: THREE.Mesh): Tween { +/* Return a tween animation for the spawning of a unit. + * Intended to be invoked before the unit is added to the scene +*/ +function createSpawnAnimation(newUnitMesh: THREE.Group): Tween { // Start the unit really high, and lower it to the board. newUnitMesh.position.setY(1000) - return new Tween({ y: 1000 }).to({ y: 10 }, 1000).easing(Easing.Quadratic.Out).onUpdate((object) => { - newUnitMesh.position.setY(object.y) - }).start() + return new Tween({ y: 1000 }) + .to({ y: 10 }, 1000) + .easing(Easing.Quadratic.Out) + .onUpdate((object) => { + newUnitMesh.position.setY(object.y) + }).start() +} + +function createMoveAnimation(unitMesh: THREE.Group, orderDestination: ProvinceENUM): Tween { + let destinationVector = getProvincePosition(orderDestination); + if (!destinationVector) { + throw new Error("Unable to find the vector for province with name " + orderDestination) + } + let anim = new Tween(unitMesh.position) + .to({ + x: destinationVector.x, + y: 10, + z: destinationVector.z + }, config.animationDuration) + .easing(Easing.Quadratic.InOut) + .onUpdate(() => { + unitMesh.position.y = 10 + Math.sin(Date.now() * 0.05) * 2; + if (unitMesh.userData.type === 'F') { + unitMesh.rotation.z = Math.sin(Date.now() * 0.03) * 0.1; + unitMesh.rotation.x = Math.sin(Date.now() * 0.02) * 0.1; + } + }) + .onComplete(() => { + unitMesh.userData.province = orderDestination; + unitMesh.position.y = 10; + if (unitMesh.userData.type === 'F') { + unitMesh.rotation.z = 0; + unitMesh.rotation.x = 0; + } + }) + .start(); + gameState.unitAnimations.push(anim); + return anim } /** @@ -63,9 +101,12 @@ export function createAnimationsForNextPhase() { return; } for (const [power, orders] of Object.entries(previousPhase.orders)) { + if (orders === null) { + continue + } for (const order of orders) { // Check if unit bounced - let lastPhaseResultMatches = Object.entries(previousPhase.results).filter(([key, value]) => { + let lastPhaseResultMatches = Object.entries(previousPhase.results).filter(([key, _]) => { return key.split(" ")[1] == order.unit.origin }).map(val => { // in the form "A BER" (unitType origin) @@ -89,46 +130,13 @@ export function createAnimationsForNextPhase() { if (order.type != "build" && unitIndex < 0) throw new Error("Unable to find unit for order " + order.raw) switch (order.type) { case "move": - let destinationVector = getProvincePosition(order.destination); - if (!destinationVector) { - throw new Error("Unable to find the vector for province with name " + order.destination) - } + if (!order.destination) throw new Error("Move order with no destination, cannot complete move.") // Create a tween for smooth movement - let anim = new Tween(gameState.unitMeshes[unitIndex].position) - .to({ - x: destinationVector.x, - y: 10, - z: destinationVector.z - }, config.animationDuration) - .easing(Easing.Quadratic.InOut) - .onUpdate(() => { - gameState.unitMeshes[unitIndex].position.y = 10 + Math.sin(Date.now() * 0.05) * 2; - 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(() => { - gameState.unitMeshes[unitIndex].userData.province = order.destination; - if (config.isDebugMode) { - console.log(`Unit ${orderObj.power} ${gameState.unitMeshes[unitIndex].userData.type} moved: ${order.unit.origin} -> ${order.destination}`); - } - - 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.unitAnimations.push(anim); + createMoveAnimation(gameState.unitMeshes[unitIndex], order.destination as keyof typeof ProvinceENUM) break; case "disband": // TODO: Death animation - if (config.isDebugMode) { - console.log(`Disbanding unit ${orderObj.power} ${gameState.unitMeshes[unitIndex].userData.type} in ${gameState.unitMeshes[unitIndex].userData.province}`); - } gameState.scene.remove(gameState.unitMeshes[unitIndex]); gameState.unitMeshes.splice(unitIndex, 1); break; @@ -148,11 +156,19 @@ export function createAnimationsForNextPhase() { case "bounce": // TODO: implement bounce animation break; + case "hold": + //TODO: Hold animation, maybe a sheild or something? + break; + + case "retreat": + createMoveAnimation(gameState.unitMeshes[unitIndex], order.destination as keyof typeof ProvinceENUM) + break; + + case "support": + break + default: - if (config.isDebugMode) { - console.log(`Skipping order type: ${order.type} for ${orderObj.text}`); - } break; } } diff --git a/ai_animation/src/units/create.ts b/ai_animation/src/units/create.ts index 7287aa9..d1c4880 100644 --- a/ai_animation/src/units/create.ts +++ b/ai_animation/src/units/create.ts @@ -1,5 +1,5 @@ import * as THREE from "three"; -import { UnitData, UnitMesh } from "../types/units"; +import { UnitData, UnitTypeENUM } from "../types/units"; import { PowerENUM } from "../types/map"; import { gameState } from "../gameState"; import { getProvincePosition } from "../map/utils"; @@ -17,7 +17,7 @@ export function getPowerHexColor(power: PowerENUM | undefined): string { 'RUSSIA': '#cccccc', 'TURKEY': '#e0c846', }; - return powerColors[power.toUpperCase()] || defaultColor; // fallback to neutral + return powerColors[power.toUpperCase() as keyof typeof PowerENUM] || defaultColor; // fallback to neutral } function createArmy(color: string): THREE.Group { @@ -153,8 +153,8 @@ export function initUnits() { const match = unitStr.match(/^([AF])\s+(.+)$/); if (match) { let newUnit = createUnitMesh({ - power: power.toUpperCase(), - type: match[1], + power: PowerENUM[power.toUpperCase() as keyof typeof PowerENUM], + type: UnitTypeENUM[match[1] as keyof typeof UnitTypeENUM], province: match[2], }); gameState.scene.add(newUnit); diff --git a/ai_animation/tsconfig.json b/ai_animation/tsconfig.json index 41298c6..bb15751 100644 --- a/ai_animation/tsconfig.json +++ b/ai_animation/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2022", "useDefineForClassFields": true, "module": "ESNext", "lib": [