diff --git a/ai_animation/assets/maps/standard/styles.json b/ai_animation/assets/maps/standard/styles.json index 1089625..e886848 100644 --- a/ai_animation/assets/maps/standard/styles.json +++ b/ai_animation/assets/maps/standard/styles.json @@ -5,9 +5,6 @@ "impassable": { "fill": "#353433" }, - "background": { - "fill": "#FFFFFF" - }, "labeltext": { "font-size": "12px", "fill": "black", @@ -36,31 +33,6 @@ "fill-opacity": "0.0", "opacity": "0.0" }, - "provinceRed": { - "fill": "url(#patternRed)", - "stroke": "black", - "stroke-width": "2" - }, - "provinceBrown": { - "fill": "url(#patternBrown)", - "stroke": "black", - "stroke-width": "2" - }, - "provinceGreen": { - "fill": "url(#patternGreen)", - "stroke": "black", - "stroke-width": "2" - }, - "provinceBlack": { - "fill": "url(#patternBlack)", - "stroke": "black", - "stroke-width": "2" - }, - "provinceBlue": { - "fill": "url(#patternBlue)", - "stroke": "black", - "stroke-width": "2" - }, "nopower": { "fill": "#ddd2af", "stroke": "black", @@ -72,131 +44,5 @@ "stroke": "black", "stroke-linejoin": "round", "stroke-width": "2" - }, - "britain": { - "fill": "royalblue", - "stroke": "black", - "stroke-width": "2" - }, - "egypt": { - "fill": "#808000", - "stroke": "black", - "stroke-width": "2" - }, - "france": { - "fill": "#00FFFF", - "stroke": "black", - "stroke-width": "2" - }, - "germany": { - "fill": "darkgrey", - "stroke": "black", - "stroke-width": "2" - }, - "italy": { - "fill": "#80FF80", - "stroke": "black", - "stroke-width": "2" - }, - "poland": { - "fill": "#FF0000", - "stroke": "black", - "stroke-width": "2" - }, - "russia": { - "fill": "#008000", - "stroke": "black", - "stroke-width": "2" - }, - "spain": { - "fill": "#FF8080", - "stroke": "black", - "stroke-width": "2" - }, - "turkey": { - "fill": "#FFFF00", - "stroke": "black", - "stroke-width": "2" - }, - "ukraine": { - "fill": "#FF00FF", - "stroke": "black", - "stroke-width": "2" - }, - "unitbritain": { - "fill": "deepskyblue", - "stroke": "black", - "fill-opacity": "0.90" - }, - "unitegypt": { - "fill": "#808000", - "stroke": "black", - "fill-opacity": "0.90" - }, - "unitfrance": { - "fill": "#00FFFF", - "stroke": "black", - "fill-opacity": "0.90" - }, - "unitgermany": { - "fill": "darkgrey", - "stroke": "black", - "fill-opacity": "0.90" - }, - "unititaly": { - "fill": "#80FF80", - "stroke": "black", - "fill-opacity": "0.90" - }, - "unitpoland": { - "fill": "#FF0000", - "stroke": "black", - "fill-opacity": "0.90" - }, - "unitrussia": { - "fill": "#008000", - "stroke": "black", - "fill-opacity": "0.90" - }, - "unitspain": { - "fill": "#FF8080", - "stroke": "black", - "fill-opacity": "0.90" - }, - "unitturkey": { - "fill": "#FFFF00", - "stroke": "black", - "fill-opacity": "0.90" - }, - "unitukraine": { - "fill": "#FF00FF", - "stroke": "black", - "fill-opacity": "0.90" - }, - "supportorder": { - "stroke-width": "2", - "fill": "none", - "stroke-dasharray": "5,5" - }, - "convoyorder": { - "stroke-dasharray": "15,5", - "stroke-width": "2", - "fill": "none" - }, - "shadowdash": { - "stroke-width": "4", - "fill": "none", - "stroke": "black", - "opacity": "0.45" - }, - "varwidthorder": { - "fill": "none" - }, - "varwidthshadow": { - "fill": "none", - "stroke": "black" - }, - "style1": { - "fill": "darkGray" } } diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index 77729da..e7b5fd1 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -16,13 +16,7 @@ export function loadCoordinateData() { // Try an alternate path if desired throw new Error("Something went wrong when fetching the coords.json") } - return response; - }) - .then(response => { - if (!response.ok) { - throw new Error(`Failed to load coordinates: ${response.status}`); - } - return response.json(); + return response.json() }) .then(data => { coordinateData = data; diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index 5e5e6c5..cea738d 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -1,14 +1,13 @@ import * as THREE from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; -import { FontLoader } from 'three/addons/loaders/FontLoader.js'; -import { SVGLoader } from 'three/addons/loaders/SVGLoader.js'; -import { createLabel } from "./map/labels" import "./style.css" import { UnitMesh } from "./types/units"; import { CoordinateData, PowerENUM } from "./types/map"; import { createUnitMesh, getPowerHexColor } from "./units/create"; +import { initMap } from "./map/create"; import { getProvincePosition } from "./map/utils"; -import { createAnimationsForPhaseTransition } from "./units/animate"; +import { createAnimationsForPhaseTransition, processUnitAnimation } from "./units/animate"; +import type { UnitAnimation } from "./units/animate"; import { loadCoordinateData, coordinateData } from "./gameState"; import Logger from "./logger"; import { GamePhase } from "./types/gameState"; @@ -105,7 +104,7 @@ let unitMeshes: UnitMesh[] = []; // To store references for units + supply cente let isPlaying = false; // Track playback state let playbackSpeed = 500; // Default speed in ms let playbackTimer = null; // Timer reference for playback -let unitAnimations = []; // Track ongoing unit animations +let unitAnimations: UnitAnimation[] = []; // Track ongoing unit animations let chatWindows = {}; // Store chat window elements by power let currentPower = getRandomPower(); // Now randomly selected let logger = new Logger() @@ -195,10 +194,11 @@ function initScene() { // Load coordinate data, then build the fallback map loadCoordinateData() - .then(() => { - drawMap(); // Create the map plane from the start + .then((coordinateData) => { + initMap(scene, coordinateData) + // Create the map plane from the start // target the center of the map - controls.target = new THREE.Vector3(800, 0, 800) + // Set the camera's target to the center of the map // Load default game file if in debug mode if (isDebugMode) { loadDefaultGameFile(); @@ -223,7 +223,6 @@ function initScene() { function animate() { requestAnimationFrame(animate); - const currentTime = Date.now(); if (isPlaying) { // Pan camera slowly in playback mode @@ -252,54 +251,17 @@ function animate() { } // Process unit movement animations - if (unitAnimations.length > 0) { - unitAnimations.forEach((anim, index) => { - // Calculate progress (0 to 1) - const elapsed = currentTime - anim.startTime; - const progress = Math.min(1, elapsed / anim.duration); - - // Apply movement - if (progress < 1) { - // Apply easing for more natural movement - ease in and out - const easedProgress = easeInOutCubic(progress); - - // Update position - anim.object.position.x = anim.startPos.x + (anim.endPos.x - anim.startPos.x) * easedProgress; - anim.object.position.z = anim.startPos.z + (anim.endPos.z - anim.startPos.z) * easedProgress; - - // Subtle bobbing up and down during movement - anim.object.position.y = 10 + Math.sin(progress * Math.PI * 2) * 5; - - // For fleets (ships), add a gentle rocking motion - if (anim.object.userData.type === 'F') { - anim.object.rotation.z = Math.sin(progress * Math.PI * 3) * 0.05; - anim.object.rotation.x = Math.sin(progress * Math.PI * 2) * 0.05; - } - } else { - // Animation complete, remove from active animations - unitAnimations.splice(index, 1); - - // Set final position - anim.object.position.x = anim.endPos.x; - anim.object.position.z = anim.endPos.z; - anim.object.position.y = 10; // Reset height - - // Reset rotation for ships - if (anim.object.userData.type === 'F') { - anim.object.rotation.z = 0; - anim.object.rotation.x = 0; - } - - // >>> MODIFIED: Check if messages are still playing before advancing - if (unitAnimations.length === 0 && isPlaying && !messagesPlaying && !isSpeaking) { - // Schedule next phase after a pause delay - playbackTimer = setTimeout(() => { - // Call the async function without awaiting it here - advanceToNextPhase(); - }, playbackSpeed); - } - } + if (unitAnimations && unitAnimations.length > 0) { + unitAnimations.forEach((anim: UnitAnimation, index) => { + processUnitAnimation(anim) + // Animation complete, remove from active animations + unitAnimations.splice(index, 1); }); + // >>> MODIFIED: Check if messages are still playing before advancing + if (unitAnimations.length === 0 && isPlaying && !messagesPlaying) { + // Schedule next phase after a pause delay + playbackTimer = setTimeout(() => advanceToNextPhase(), playbackSpeed); + } } // Update any pulsing or wave animations on supply centers or units @@ -327,10 +289,6 @@ function animate() { renderer.render(scene, camera); } -// Easing function for smooth animations -function easeInOutCubic(t) { - return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; -} // --- RESIZE HANDLER --- function onWindowResize() { @@ -365,109 +323,6 @@ function loadDefaultGameFile() { }); } -// --- CREATE THE FALLBACK MAP AS A PLANE --- -function drawMap() { - const loader = new SVGLoader(); - loader.load('assets/maps/standard/map.svg', - function (data) { - fetch('assets/maps/standard/styles.json') - .then(resp => resp.json()) - .then(map_styles => { - const paths = data.paths; - const group = new THREE.Group(); - const textGroup = new THREE.Group(); - - for (let i = 0; i < paths.length; i++) { - let fillColor = "" - const path = paths[i]; - // The "standard" map has keys like _mos, so remove that then send them to caps - let provinceKey = path.userData.node.id.substring(1).toUpperCase(); - - - if (map_styles[path.userData.node.classList[0]] === undefined) { - // If there is no style in the map_styles, skip drawing the shape - continue - } else if (provinceKey && coordinateData.provinces[provinceKey] && coordinateData.provinces[provinceKey].owner) { - fillColor = getPowerHexColor(coordinateData.provinces[provinceKey].owner) - } - else { - fillColor = map_styles[path.userData.node.classList[0]].fill; - } - - const material = new THREE.MeshBasicMaterial({ - color: fillColor, - side: THREE.DoubleSide, - depthWrite: false - }); - - const shapes = SVGLoader.createShapes(path); - - for (let j = 0; j < shapes.length; j++) { - - const shape = shapes[j]; - const geometry = new THREE.ShapeGeometry(shape); - const mesh = new THREE.Mesh(geometry, material); - - mesh.rotation.x = Math.PI / 2; - if (provinceKey && coordinateData.provinces[provinceKey] && coordinateData.provinces[provinceKey].owner) { - coordinateData.provinces[provinceKey].mesh = mesh - } - group.add(mesh); - - - // Create an edges geometry from the shape geometry. - const edges = new THREE.EdgesGeometry(geometry); - // Create a line material with black color for the border. - const lineMaterial = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 2 }); - // Create the line segments object to display the border. - const line = new THREE.LineSegments(edges, lineMaterial); - // Add the border as a child of the mesh. - mesh.add(line); - } - } - - // Load all the labels for each map position - const fontLoader = new FontLoader(); - fontLoader.load('assets/fonts/helvetiker_regular.typeface.json', function (font) { - for (const [key, value] of Object.entries(coordinateData.provinces)) { - - textGroup.add(createLabel(font, key, value)) - } - }) - // This rotates the SVG the "correct" way round, and scales it down - group.scale.set(1, -1, 1) - textGroup.rotation.x = Math.PI / 2; - textGroup.scale.set(1, -1, 1) - - // After adding all meshes to the group, update its matrix: - group.updateMatrixWorld(true); - textGroup.updateMatrixWorld(true); - - // Compute the bounding box of the group: - const box = new THREE.Box3().setFromObject(group); - const center = new THREE.Vector3(); - box.getCenter(center); - - - scene.add(group); - scene.add(textGroup); - - // Set the camera's target to the center of the map - controls.target = center - camera.position.set(center.x, 1400, 1100) - }) - .catch(error => { - console.error('Error loading map styles:', error); - }); - }, - // Progress function - undefined, - function (error) { console.log(error) }) - - return -} - - // --- 3D SUPPLY CENTERS --- function displaySupplyCenters() { if (!coordinateData || !coordinateData.provinces) return; @@ -711,7 +566,6 @@ function displayInitialPhase(index) { // Add: Update info panel updateInfoPanel(); - drawMap() } // --- LEADERBOARD FUNCTION --- @@ -843,36 +697,26 @@ function displayPhaseWithAnimation(index) { // Rebuild supply centers, remove old units // First show messages, THEN animate units after - if (currentPhase.messages && currentPhase.messages.length) { - // First show messages with stepwise animation - updateChatWindows(currentPhase, true); + // First show messages with stepwise animation + updateChatWindows(currentPhase, true); - // We'll animate units only after messages are done - // This happens in the animation loop when messagesPlaying becomes false - } else { - const supplyCenters = unitMeshes.filter(m => m.userData && m.userData.isSupplyCenter); - const oldUnits = unitMeshes.filter(m => m.userData && !m.userData.isSupplyCenter); - oldUnits.forEach(m => scene.remove(m)); - unitMeshes = supplyCenters; - // Ownership - if (currentPhase.state?.centers) { - updateSupplyCenterOwnership(currentPhase.state.centers); - } - - // Update leaderboard - updateLeaderboard(currentPhase); - updateMapOwnership(currentPhase) - - unitAnimations = createAnimationsForPhaseTransition(unitMeshes, currentPhase, previousPhase); + // Ownership + if (currentPhase.state?.centers) { + updateSupplyCenterOwnership(currentPhase.state.centers); } + + // Update leaderboard + updateLeaderboard(currentPhase); + updateMapOwnership(currentPhase) + + unitAnimations = createAnimationsForPhaseTransition(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 updateInfoPanel(); - drawMap() } function updateMapOwnership(currentPhase: GamePhase) { @@ -894,11 +738,7 @@ function updateMapOwnership(currentPhase: GamePhase) { for (const [key, value] of Object.entries(coordinateData.provinces)) { let power = coordinateData.provinces[key].owner let powerColor: string - if (!power) { - powerColor = '#000000' - } else { - powerColor = getPowerHexColor(coordinateData.provinces[key].owner) - } + powerColor = getPowerHexColor(coordinateData.provinces[key].owner) let powerColorHex = parseInt(powerColor.substring(1), 16); coordinateData.provinces[key].mesh?.material.color.setHex(powerColorHex) diff --git a/ai_animation/src/map/create.ts b/ai_animation/src/map/create.ts new file mode 100644 index 0000000..5693072 --- /dev/null +++ b/ai_animation/src/map/create.ts @@ -0,0 +1,105 @@ +import * as THREE from "three"; +import { FontLoader } from 'three/addons/loaders/FontLoader.js'; +import { SVGLoader } from 'three/addons/loaders/SVGLoader.js'; +import { createLabel } from "./labels" +import { coordinateData } from "../gameState"; +import { getPowerHexColor } from "../units/create"; + +export function initMap(scene,) { + const loader = new SVGLoader(); + loader.load('assets/maps/standard/map.svg', + function (data) { + fetch('assets/maps/standard/styles.json') + .then(resp => resp.json()) + .then(map_styles => { + const paths = data.paths; + const group = new THREE.Group(); + const textGroup = new THREE.Group(); + let fillColor; + + for (let i = 0; i < paths.length; i++) { + fillColor = "#ddd2af"; + const path = paths[i]; + // The "standard" map has keys like _mos, so remove that then send them to caps + let provinceKey = path.userData.node.id.substring(1).toUpperCase(); + + + if (map_styles[path.userData.node.classList[0]] === undefined) { + // If there is no style in the map_styles, skip drawing the shape + // This is a bandaid for using an svg with extraneous shapes + continue + } else if (provinceKey && coordinateData.provinces[provinceKey]) { + fillColor = getPowerHexColor(coordinateData.provinces[provinceKey].owner) + } + else { + fillColor = map_styles[path.userData.node.classList[0]].fill; + } + + const material = new THREE.MeshBasicMaterial({ + color: fillColor, + side: THREE.DoubleSide, + depthWrite: false + }); + + const shapes = SVGLoader.createShapes(path); + + for (let j = 0; j < shapes.length; j++) { + + const shape = shapes[j]; + const geometry = new THREE.ShapeGeometry(shape); + const mesh = new THREE.Mesh(geometry, material); + + mesh.rotation.x = Math.PI / 2; + if (provinceKey && coordinateData.provinces[provinceKey]) { + coordinateData.provinces[provinceKey].mesh = mesh + } + + + // Create an edges geometry from the shape geometry. + const edges = new THREE.EdgesGeometry(geometry); + // Create a line material with black color for the border. + const lineMaterial = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 2 }); + // Create the line segments object to display the border. + const line = new THREE.LineSegments(edges, lineMaterial); + // Add the border as a child of the mesh. + mesh.add(line); + group.add(mesh); + } + } + + // Load all the labels for each map position + const fontLoader = new FontLoader(); + fontLoader.load('assets/fonts/helvetiker_regular.typeface.json', function (font) { + for (const [key, value] of Object.entries(coordinateData.provinces)) { + + textGroup.add(createLabel(font, key, value)) + } + }) + // This rotates the SVG the "correct" way round, and scales it down + group.scale.set(1, -1, 1) + textGroup.rotation.x = Math.PI / 2; + textGroup.scale.set(1, -1, 1) + + // After adding all meshes to the group, update its matrix: + group.updateMatrixWorld(true); + textGroup.updateMatrixWorld(true); + + // Compute the bounding box of the group: + const box = new THREE.Box3().setFromObject(group); + const center = new THREE.Vector3(); + box.getCenter(center); + + + scene.add(group); + scene.add(textGroup); + + }) + .catch(error => { + console.error('Error loading map styles:', error); + }); + }, + // Progress function + undefined, + function (error) { console.log(error) }) + +} diff --git a/ai_animation/src/map/utils.ts b/ai_animation/src/map/utils.ts index 34d91ac..2ab89b9 100644 --- a/ai_animation/src/map/utils.ts +++ b/ai_animation/src/map/utils.ts @@ -9,6 +9,7 @@ function hashStringToPosition(str) { const z = ((hash >> 8) % 800) - 400; return { x, y: 0, z }; } + //TODO: Make coordinateData come from gameState export function getProvincePosition(coordinateData, loc) { // Convert e.g. "Spa/sc" to "SPA_SC" if needed diff --git a/ai_animation/src/units/animate.ts b/ai_animation/src/units/animate.ts index 9f5b6f8..d3b8f65 100644 --- a/ai_animation/src/units/animate.ts +++ b/ai_animation/src/units/animate.ts @@ -11,12 +11,12 @@ enum AnimationTypeENUM { MOVE, DELETE, } -type unitAnimation = { +export type UnitAnimation = { animationType: AnimationTypeENUM } -export function createAnimationsForPhaseTransition(unitMeshes: UnitMesh[], currentPhase: GamePhase, previousPhase: GamePhase | null): unitAnimation[] { - let unitAnimations: unitAnimation[] = [] +export function createAnimationsForPhaseTransition(unitMeshes: UnitMesh[], currentPhase: GamePhase, previousPhase: GamePhase | null): UnitAnimation[] { + let unitAnimations: UnitAnimation[] = [] // Prepare unit position maps const previousUnitPositions = {}; if (previousPhase.state?.units) { @@ -82,3 +82,46 @@ export function createAnimationsForPhaseTransition(unitMeshes: UnitMesh[], curre } return unitAnimations } +// Easing function for smooth animations +function easeInOutCubic(t) { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; +} + +export function processUnitAnimation(anim: UnitAnimation) { + + const currentTime = Date.now(); + const elapsed = currentTime - anim.startTime; + // Calculate progress (0 to 1) + const progress = Math.min(1, elapsed / anim.duration); + + // Apply movement + if (progress < 1) { + // Apply easing for more natural movement - ease in and out + const easedProgress = easeInOutCubic(progress); + + // Update position + anim.object.position.x = anim.startPos.x + (anim.endPos.x - anim.startPos.x) * easedProgress; + anim.object.position.z = anim.startPos.z + (anim.endPos.z - anim.startPos.z) * easedProgress; + + // Subtle bobbing up and down during movement + anim.object.position.y = 10 + Math.sin(progress * Math.PI * 2) * 5; + + // For fleets (ships), add a gentle rocking motion + if (anim.object.userData.type === 'F') { + anim.object.rotation.z = Math.sin(progress * Math.PI * 3) * 0.05; + anim.object.rotation.x = Math.sin(progress * Math.PI * 2) * 0.05; + } + } else { + + // Set final position + anim.object.position.x = anim.endPos.x; + anim.object.position.z = anim.endPos.z; + anim.object.position.y = 10; // Reset height + + // Reset rotation for ships + if (anim.object.userData.type === 'F') { + anim.object.rotation.z = 0; + anim.object.rotation.x = 0; + } + } +} diff --git a/ai_animation/src/units/create.ts b/ai_animation/src/units/create.ts index 6280afd..e3e4bd0 100644 --- a/ai_animation/src/units/create.ts +++ b/ai_animation/src/units/create.ts @@ -4,8 +4,8 @@ import { PowerENUM } from "../types/map"; // Get color for a power export function getPowerHexColor(power: PowerENUM) { - if (!power) throw new Error("Cannot pass undefined to getPowerHexColor") - let powerString = power.toUpperCase() + let defaultColor = '#ddd2af' + if (power === undefined) return defaultColor const powerColors = { 'AUSTRIA': '#c40000', 'ENGLAND': '#00008B', @@ -15,7 +15,7 @@ export function getPowerHexColor(power: PowerENUM) { 'RUSSIA': '#cccccc', 'TURKEY': '#e0c846', }; - return powerColors[powerString] || '#b19b69'; // fallback to neutral + return powerColors[power] || defaultColor; // fallback to neutral } function createArmy(color: string): THREE.Group {