Retreats now work as expected

This commit is contained in:
Tyler Marques 2025-03-26 17:05:30 -04:00
parent 34371a40af
commit cae33dd5ba
No known key found for this signature in database
GPG key ID: 7672EFD79378341C
13 changed files with 92 additions and 69 deletions

View file

@ -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 = {};

View file

@ -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]++;
}

View file

@ -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<keyof typeof PowerENUM>) {
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)
}
}
}

View file

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

View file

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

View file

@ -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<void> {
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<void> {
} 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<void> {
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,

View file

@ -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(),

View file

@ -54,7 +54,7 @@ export const CoordinateDataSchema = z.object({
export type Province = z.infer<typeof ProvinceSchema>;
export type CoordinateData = z.infer<typeof CoordinateDataSchema>;
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)

View file

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

View file

@ -4,7 +4,7 @@ import { PowerENUM } from "./map"
export enum UnitTypeENUM {
A = "A",
F = "Fleet"
F = "F"
}
export type UnitData = {

View file

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

View file

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

View file

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2020",
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": [