mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-19 12:58:09 +00:00
Retreats now work as expected
This commit is contained in:
parent
34371a40af
commit
cae33dd5ba
13 changed files with 92 additions and 69 deletions
|
|
@ -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 = {};
|
||||
|
||||
|
|
|
|||
|
|
@ -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]++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { PowerENUM } from "./map"
|
|||
|
||||
export enum UnitTypeENUM {
|
||||
A = "A",
|
||||
F = "Fleet"
|
||||
F = "F"
|
||||
}
|
||||
|
||||
export type UnitData = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue