From c421468def5f57e082299eb35c997128adae2eb8 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Tue, 11 Mar 2025 10:13:26 -0400 Subject: [PATCH 1/9] Rebasing on new_bar branch, Adding some typing, renaming map files --- .../{standard_coords.json => coords.json} | 0 .../maps/standard/{standard.svg => map.svg} | 0 .../{standard_styles.json => styles.json} | 0 ai_animation/package-lock.json | 12 ++++- ai_animation/package.json | 6 +-- ai_animation/src/gameState.ts | 33 ++++++++++++ ai_animation/src/main.js | 43 +++++++-------- ai_animation/src/types/gameState.ts | 28 ++++++++++ ai_animation/src/types/map.ts | 52 +++++++++++++------ ai_animation/src/units/animate.ts | 0 ai_animation/src/units/create.ts | 0 11 files changed, 130 insertions(+), 44 deletions(-) rename ai_animation/assets/maps/standard/{standard_coords.json => coords.json} (100%) rename ai_animation/assets/maps/standard/{standard.svg => map.svg} (100%) rename ai_animation/assets/maps/standard/{standard_styles.json => styles.json} (100%) create mode 100644 ai_animation/src/gameState.ts create mode 100644 ai_animation/src/types/gameState.ts create mode 100644 ai_animation/src/units/animate.ts create mode 100644 ai_animation/src/units/create.ts diff --git a/ai_animation/assets/maps/standard/standard_coords.json b/ai_animation/assets/maps/standard/coords.json similarity index 100% rename from ai_animation/assets/maps/standard/standard_coords.json rename to ai_animation/assets/maps/standard/coords.json diff --git a/ai_animation/assets/maps/standard/standard.svg b/ai_animation/assets/maps/standard/map.svg similarity index 100% rename from ai_animation/assets/maps/standard/standard.svg rename to ai_animation/assets/maps/standard/map.svg diff --git a/ai_animation/assets/maps/standard/standard_styles.json b/ai_animation/assets/maps/standard/styles.json similarity index 100% rename from ai_animation/assets/maps/standard/standard_styles.json rename to ai_animation/assets/maps/standard/styles.json diff --git a/ai_animation/package-lock.json b/ai_animation/package-lock.json index eb2a585..e77d9fa 100644 --- a/ai_animation/package-lock.json +++ b/ai_animation/package-lock.json @@ -8,7 +8,8 @@ "name": "ai_animation", "version": "0.0.0", "dependencies": { - "three": "^0.174.0" + "three": "^0.174.0", + "zod": "^3.24.2" }, "devDependencies": { "typescript": "~5.7.2", @@ -964,6 +965,15 @@ "optional": true } } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/ai_animation/package.json b/ai_animation/package.json index 620c979..2b9f079 100644 --- a/ai_animation/package.json +++ b/ai_animation/package.json @@ -13,7 +13,7 @@ "vite": "^6.2.0" }, "dependencies": { - "three": "^0.174.0" - }, - "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228" + "three": "^0.174.0", + "zod": "^3.24.2" + } } diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts new file mode 100644 index 0000000..e6cc41f --- /dev/null +++ b/ai_animation/src/gameState.ts @@ -0,0 +1,33 @@ +import { type Province, type CoordinateData, CoordinateDataSchema } from "./types/map" + +enum AvailableMaps { + STANDARD = "standard" +} + + +export default class { + boardState: CoordinateData | null + + constructor(boardName: AvailableMaps) { + this.boardState = null + this._loadMapData(boardName) + } + + + _loadMapData = (boardName: AvailableMaps) => { + + fetch(`./assets/maps/${boardName}/coords.json`) + .then(response => { + if (!response.ok) { + throw new Error(`Failed to load coordinates: ${response.status}`); + } + this.boardState = CoordinateDataSchema.parse(response.json()) + }) + .catch(error => { + console.error(error); + throw error + }); + + } + +} diff --git a/ai_animation/src/main.js b/ai_animation/src/main.js index 22a9c69..398f676 100644 --- a/ai_animation/src/main.js +++ b/ai_animation/src/main.js @@ -351,18 +351,11 @@ function onWindowResize() { // --- LOAD COORDINATE DATA --- function loadCoordinateData() { return new Promise((resolve, reject) => { - fetch('./assets/maps/standard/standard_coords.json') + fetch('./assets/maps/standard/coords.json') .then(response => { if (!response.ok) { // Try an alternate path if desired - return fetch('../assets/maps/standard_coords.json'); - } - return response; - }) - .then(response => { - if (!response.ok) { - // Another fallback path - return fetch('/diplomacy/animation/assets/maps/standard_coords.json'); + throw new Error("Something went wrong when fetching the coords.json") } return response; }) @@ -386,9 +379,9 @@ function loadCoordinateData() { // --- CREATE THE FALLBACK MAP AS A PLANE --- function drawMap() { const loader = new SVGLoader(); - loader.load('assets/maps/standard/standard.svg', + loader.load('assets/maps/standard/map.svg', function (data) { - fetch('assets/maps/standard/standard_styles.json') + fetch('assets/maps/standard/styles.json') .then(resp => resp.json()) .then(map_styles => { const paths = data.paths; @@ -902,20 +895,6 @@ function displayPhaseWithAnimation(index) { phaseDisplay.textContent = `Era: ${currentPhase.name || 'Unknown Era'} (${index + 1}/${gameData.phases.length})`; // Rebuild supply centers, remove old units - 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) - // First show messages, THEN animate units after if (currentPhase.messages && currentPhase.messages.length) { @@ -925,6 +904,20 @@ function displayPhaseWithAnimation(index) { // 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) + // No messages, animate units immediately animateUnitsForPhase(currentPhase, previousPhase); } diff --git a/ai_animation/src/types/gameState.ts b/ai_animation/src/types/gameState.ts new file mode 100644 index 0000000..1e3d3b7 --- /dev/null +++ b/ai_animation/src/types/gameState.ts @@ -0,0 +1,28 @@ +import { z } from 'zod'; + +const UnitSchema = z.object({ + type: z.enum(["A", "F"]), + power: z.string(), + location: z.string(), +}); + +const OrderSchema = z.object({ + text: z.string(), + power: z.string(), + region: z.string(), +}); + +const PhaseSchema = z.object({ + name: z.string(), + year: z.number(), + season: z.enum(["SPRING", "FALL", "WINTER"]), + type: z.enum(["MOVEMENT", "ADJUSTMENT"]), + units: z.array(UnitSchema), + orders: z.array(OrderSchema), +}); + +export const GameSchema = z.object({ + map_name: z.string(), + game_id: z.string(), + phases: z.array(PhaseSchema), +}); diff --git a/ai_animation/src/types/map.ts b/ai_animation/src/types/map.ts index 1b0c070..e38d44c 100644 --- a/ai_animation/src/types/map.ts +++ b/ai_animation/src/types/map.ts @@ -1,22 +1,44 @@ +import { z } from "zod"; -enum ProvTypeEnum { +export enum ProvTypeENUM { WATER = "Water", COAST = "Coast", LAND = "Land", - -} -type Province = { - label: { - x: number - y: number - } - type: ProvTypeEnum - unit?: { - x: number - y: number - } } -type CoordinateData = { - provinces: Province[] +export enum PowerENUM { + FRANCE = "France", + TURKEY = "Turkey", + AUSTRIA = "Austria", + GERMANY = "Germany", + ITALY = "Italy", + RUSSIA = "Russia", } + +export const ProvTypeSchema = z.nativeEnum(ProvTypeENUM); +export const PowerSchema = z.nativeEnum(PowerENUM); + +export const LabelSchema = z.object({ + x: z.number(), + y: z.number(), +}); + +export const UnitSchema = z.object({ + x: z.number(), + y: z.number(), +}); + +export const ProvinceSchema = z.object({ + label: LabelSchema, + type: ProvTypeSchema, + unit: UnitSchema.optional(), + owner: PowerSchema.optional(), +}); + +export const CoordinateDataSchema = z.object({ + provinces: z.array(ProvinceSchema), +}); + +export type Province = z.infer; +export type CoordinateData = z.infer; + diff --git a/ai_animation/src/units/animate.ts b/ai_animation/src/units/animate.ts new file mode 100644 index 0000000..e69de29 diff --git a/ai_animation/src/units/create.ts b/ai_animation/src/units/create.ts new file mode 100644 index 0000000..e69de29 From 245091ada9e2c40f31e22d04659e11c080db4f11 Mon Sep 17 00:00:00 2001 From: Tyler Marques Date: Tue, 11 Mar 2025 11:06:51 -0400 Subject: [PATCH 2/9] Moved to typescript for main file. Added logging util, more types --- ai_animation/index.html | 2 +- ai_animation/package-lock.json | 51 ++++++++++ ai_animation/package.json | 1 + ai_animation/src/logger.ts | 18 ++++ ai_animation/src/{main.js => main.ts} | 141 ++++++++++++++++---------- ai_animation/src/types/units.ts | 13 +++ 6 files changed, 174 insertions(+), 52 deletions(-) create mode 100644 ai_animation/src/logger.ts rename ai_animation/src/{main.js => main.ts} (95%) create mode 100644 ai_animation/src/types/units.ts diff --git a/ai_animation/index.html b/ai_animation/index.html index 8bb433b..31c3cb1 100644 --- a/ai_animation/index.html +++ b/ai_animation/index.html @@ -9,7 +9,7 @@
- +
diff --git a/ai_animation/package-lock.json b/ai_animation/package-lock.json index e77d9fa..39ee503 100644 --- a/ai_animation/package-lock.json +++ b/ai_animation/package-lock.json @@ -8,6 +8,7 @@ "name": "ai_animation", "version": "0.0.0", "dependencies": { + "@types/three": "^0.174.0", "three": "^0.174.0", "zod": "^3.24.2" }, @@ -707,6 +708,12 @@ "win32" ] }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -714,6 +721,38 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/stats.js": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", + "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.174.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.174.0.tgz", + "integrity": "sha512-De/+vZnfg2aVWNiuy1Ldu+n2ydgw1osinmiZTAn0necE++eOfsygL8JpZgFjR2uHmAPo89MkxBj3JJ+2BMe+Uw==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.21.tgz", + "integrity": "sha512-geZIAtLzjGmgY2JUi6VxXdCrTb99A7yP49lxLr2Nm/uIK0PkkxcEi4OGhoGDO4pxCf3JwGz2GiJL2Ej4K2bKaA==", + "license": "MIT" + }, + "node_modules/@webgpu/types": { + "version": "0.1.55", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.55.tgz", + "integrity": "sha512-p97I8XEC1h04esklFqyIH+UhFrUcj8/1/vBWgc6lAK4jMJc+KbhUy8D4dquHYztFj6pHLqGcp/P1xvBBF4r3DA==", + "license": "BSD-3-Clause" + }, "node_modules/esbuild": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", @@ -755,6 +794,12 @@ "@esbuild/win32-x64": "0.25.0" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -770,6 +815,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", diff --git a/ai_animation/package.json b/ai_animation/package.json index 2b9f079..b611fbd 100644 --- a/ai_animation/package.json +++ b/ai_animation/package.json @@ -13,6 +13,7 @@ "vite": "^6.2.0" }, "dependencies": { + "@types/three": "^0.174.0", "three": "^0.174.0", "zod": "^3.24.2" } diff --git a/ai_animation/src/logger.ts b/ai_animation/src/logger.ts new file mode 100644 index 0000000..4006bf4 --- /dev/null +++ b/ai_animation/src/logger.ts @@ -0,0 +1,18 @@ + +export default class Logger { + get infoPanel() { + let _panel = document.getElementById('info-panel'); + if (_panel === null) { + throw new Error("Unable to find the element with id 'info-panel'") + } + return _panel + } + log = (msg: string) => { + if (typeof msg !== "string") { + throw new Error(`Logger messages must be strings, you passed a ${typeof msg}`) + } + this.infoPanel.textContent = msg; + + console.log(msg) + } +} diff --git a/ai_animation/src/main.js b/ai_animation/src/main.ts similarity index 95% rename from ai_animation/src/main.js rename to ai_animation/src/main.ts index 398f676..4f244e6 100644 --- a/ai_animation/src/main.js +++ b/ai_animation/src/main.ts @@ -2,9 +2,11 @@ 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 { addMapMouseEvents } from "./map/mouseMovement" import { createLabel } from "./map/labels" import "./style.css" +import { UnitMesh } from "./types/units"; +import { PowerENUM } from "./types/map"; +import Logger from "./logger"; // --- NEW: ElevenLabs TTS helper function --- const ELEVENLABS_API_KEY = import.meta.env.VITE_ELEVENLABS_API_KEY || ""; @@ -55,7 +57,7 @@ async function speakSummary(summaryText) { // Convert response into a playable blob const audioBlob = await response.blob(); const audioUrl = URL.createObjectURL(audioBlob); - + // Play the audio, pause until finished return new Promise((resolve, reject) => { const audio = new Audio(audioUrl); @@ -95,7 +97,7 @@ let scene, camera, renderer, controls; let gameData = null; let currentPhaseIndex = 0; let coordinateData = null; -let unitMeshes = []; // To store references for units + supply center 3D objects +let unitMeshes: UnitMesh[] = []; // To store references for units + supply center 3D objects let mapPlane = null; // The fallback map plane let isPlaying = false; // Track playback state let playbackSpeed = 500; // Default speed in ms @@ -105,6 +107,7 @@ let unitAnimations = []; // Track ongoing unit animations let territoryTransitions = []; // Track territory color transitions let chatWindows = {}; // Store chat window elements by power let currentPower = getRandomPower(); // Now randomly selected +let logger = new Logger() // >>> ADDED: Camera pan and message playback variables let cameraPanTime = 0; // Timer that drives the camera panning @@ -196,14 +199,19 @@ function initScene() { drawMap(); // Create the map plane from the start // target the center of the map controls.target = new THREE.Vector3(800, 0, 800) + // Load default game file if in debug mode + if (isDebugMode) { + loadDefaultGameFile(); + } + }) .catch(err => { console.error("Error loading coordinates:", err); + logger.log(`Error loading coords: ${err.message}`) }); // Handle resizing window.addEventListener('resize', onWindowResize); - addMapMouseEvents(mapView) // Kick off animation loop animate(); @@ -348,6 +356,31 @@ function onWindowResize() { renderer.setSize(mapView.clientWidth, mapView.clientHeight); } +// Load a default game if we're running debug +function loadDefaultGameFile() { + console.log("Loading default game file for debug mode..."); + + // Path to the default game file + const defaultGameFilePath = './assets/default_game.json'; + + fetch(defaultGameFilePath) + .then(response => { + if (!response.ok) { + throw new Error(`Failed to load default game file: ${response.status}`); + } + return response.text(); + }) + .then(data => { + // Create a mock file object to pass to loadGame + const file = new File([data], "default_game.json", { type: "application/json" }); + loadGame(file); + console.log("Default game file loaded successfully"); + }) + .catch(error => { + console.error("Error loading default game file:", error); + logger.log(`Error loading default game: ${error.message}`) + }); +} // --- LOAD COORDINATE DATA --- function loadCoordinateData() { return new Promise((resolve, reject) => { @@ -367,6 +400,7 @@ function loadCoordinateData() { }) .then(data => { coordinateData = data; + logger.log('Coordinate data loaded!') resolve(coordinateData); }) .catch(error => { @@ -692,6 +726,7 @@ function loadGame(file) { reader.onload = e => { try { gameData = JSON.parse(e.target.result); + logger.log(`Game data loaded: ${gameData.phases?.length || 0} phases found.`) currentPhaseIndex = 0; if (gameData.phases?.length) { prevBtn.disabled = false; @@ -705,16 +740,15 @@ function loadGame(file) { // Display initial phase but WITHOUT messages displayInitialPhase(currentPhaseIndex); } - + // Add: Update info panel updateInfoPanel(); } catch (err) { - console.error("Error parsing JSON:", err); - infoPanel.textContent = "Error parsing JSON: " + err.message; + logger.log("Error parsing JSON: " + err.message) } }; reader.onerror = () => { - infoPanel.textContent = "Error reading file."; + logger.log("Error reading file.") }; reader.readAsText(file); } @@ -722,7 +756,7 @@ function loadGame(file) { // New function to display initial state without messages function displayInitialPhase(index) { if (!gameData || !gameData.phases || index < 0 || index >= gameData.phases.length) { - infoPanel.textContent = "Invalid phase index."; + logger.log("Invalid phase index.") return; } @@ -761,10 +795,15 @@ function displayInitialPhase(index) { updateMapOwnership(phase) // DON'T show messages yet - skip updateChatWindows call + if (isDebugMode) { + logger.log(JSON.stringify(phase.state.units)) + } else { + logger.log(`Phase: ${phase.name}\nSCs: ${phase.state?.centers ? JSON.stringify(phase.state.centers) : 'None'}\nUnits: ${phase.state?.units ? JSON.stringify(phase.state.units) : 'None'}`) + } // Add: Update info panel updateInfoPanel(); - + drawMap() } @@ -865,7 +904,7 @@ async function advanceToNextPhase() { 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); } @@ -884,7 +923,7 @@ async function advanceToNextPhase() { function displayPhaseWithAnimation(index) { if (!gameData || !gameData.phases || index < 0 || index >= gameData.phases.length) { - infoPanel.textContent = "Invalid phase index."; + logger.log("Invalid phase index.") return; } @@ -918,16 +957,14 @@ function displayPhaseWithAnimation(index) { updateLeaderboard(currentPhase); updateMapOwnership(currentPhase) - // No messages, animate units immediately animateUnitsForPhase(currentPhase, previousPhase); } - + let msg = `Phase: ${currentPhase.name}\nSCs: ${JSON.stringify(currentPhase.state.centers)} \nUnits: ${currentPhase.state?.units ? JSON.stringify(currentPhase.state.units) : 'None'} ` // Panel - // Remove: infoPanel.textContent = `Phase: ${currentPhase.name}\nSCs: ${currentPhase.state?.centers ? JSON.stringify(currentPhase.state.centers) : 'None'}\nUnits: ${currentPhase.state?.units ? JSON.stringify(currentPhase.state.units) : 'None'}`; - + // Add: Update info panel updateInfoPanel(); - + drawMap() } @@ -964,7 +1001,7 @@ function animateUnitsForPhase(currentPhase, previousPhase) { unitArr.forEach(unitStr => { const match = unitStr.match(/^([AF])\s+(.+)$/); if (match) { - const key = `${power}-${match[1]}-${match[2]}`; + const key = `${power} -${match[1]} -${match[2]} `; previousUnitPositions[key] = getProvincePosition(match[2]); } }); @@ -980,7 +1017,7 @@ function animateUnitsForPhase(currentPhase, previousPhase) { if (!match) return; const unitType = match[1]; const location = match[2]; - const key = `${power}-${unitType}-${location}`; + const key = `${power} -${unitType} -${location} `; const unitMesh = createUnitMesh({ power: power.toUpperCase(), type: unitType, @@ -995,7 +1032,7 @@ function animateUnitsForPhase(currentPhase, previousPhase) { let startPos; let matchFound = false; for (const prevKey in previousUnitPositions) { - if (prevKey.startsWith(`${power}-${unitType}`)) { + if (prevKey.startsWith(`${power} -${unitType} `)) { startPos = previousUnitPositions[prevKey]; matchFound = true; delete previousUnitPositions[prevKey]; @@ -1003,6 +1040,8 @@ function animateUnitsForPhase(currentPhase, previousPhase) { } } if (!matchFound) { + // TODO: Add a spawn animation? + // // New spawn startPos = { x: currentPos.x, y: -20, z: currentPos.z }; } @@ -1137,7 +1176,7 @@ function colorShift(hex, percent) { 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')}`; + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')} `; } // --- CHAT WINDOW FUNCTIONS --- @@ -1167,7 +1206,7 @@ function createChatWindow(power, isGlobal = false) { const chatContainer = document.getElementById('chat-container'); const chatWindow = document.createElement('div'); chatWindow.className = 'chat-window'; - chatWindow.id = `chat-${power}`; + chatWindow.id = `chat - ${power} `; chatWindow.style.position = 'relative'; // Add relative positioning for absolute child positioning // Create a slimmer header with appropriate styling @@ -1188,7 +1227,7 @@ function createChatWindow(power, isGlobal = false) { titleElement.style.color = '#ffffff'; titleElement.textContent = 'GLOBAL'; } else { - titleElement.className = `power-${power.toLowerCase()}`; + titleElement.className = `power - ${power.toLowerCase()} `; titleElement.textContent = power; } titleElement.style.fontWeight = 'bold'; // Make text more prominent @@ -1208,7 +1247,7 @@ function createChatWindow(power, isGlobal = false) { faceHolder.style.boxShadow = '0 2px 5px rgba(0,0,0,0.5)'; faceHolder.style.border = '2px solid #fff'; faceHolder.style.zIndex = '10'; // Ensure it's above other elements - faceHolder.id = `face-${power}`; + faceHolder.id = `face - ${power} `; // Generate the face icon and add it to the chat window (not header) generateFaceIcon(power).then(dataURL => { @@ -1216,9 +1255,9 @@ function createChatWindow(power, isGlobal = false) { img.src = dataURL; img.style.width = '100%'; img.style.height = '100%'; - img.id = `face-img-${power}`; // Add ID for animation targeting + img.id = `face - img - ${power} `; // Add ID for animation targeting - img.id = `face-img-${power}`; // Add ID for animation targeting + img.id = `face - img - ${power} `; // Add ID for animation targeting // Add subtle idle animation setInterval(() => { @@ -1237,7 +1276,7 @@ function createChatWindow(power, isGlobal = false) { // Create messages container const messagesContainer = document.createElement('div'); messagesContainer.className = 'chat-messages'; - messagesContainer.id = `messages-${power}`; + messagesContainer.id = `messages - ${power} `; messagesContainer.style.paddingTop = '8px'; // Add padding to prevent content being hidden under face // Add toggle functionality @@ -1304,16 +1343,16 @@ function updateChatWindows(phase, stepMessages = false) { } return; } - + const msg = relevantMessages[index]; index++; // Increment index before adding message so word animation knows the correct next message - + const isNew = addMessageToChat(msg, phase.name, true, showNext); // Pass showNext as callback - + if (isNew && !isDebugMode) { // Increment message counter messageCounter++; - + // Only animate head and play sound for every third message animateHeadNod(msg, (messageCounter % 3 === 0)); } else if (isDebugMode) { @@ -1341,7 +1380,7 @@ function addMessageToChat(msg, phaseName, animateWords = false, onComplete = nul if (!chatWindows[targetPower]) return false; // Create a unique ID for this message to avoid duplication - const msgId = `${msg.sender}-${msg.recipient}-${msg.time_sent}-${msg.message}`; + const msgId = `${msg.sender} -${msg.recipient} -${msg.time_sent} -${msg.message} `; // Skip if we've already shown this message if (chatWindows[targetPower].seenMessages.has(msgId)) { @@ -1359,19 +1398,19 @@ function addMessageToChat(msg, phaseName, animateWords = false, onComplete = nul // Global chat shows sender info const senderColor = msg.sender.toLowerCase(); messageElement.className = 'chat-message message-incoming'; - + // Add the header with the sender name immediately const headerSpan = document.createElement('span'); headerSpan.style.fontWeight = 'bold'; headerSpan.className = `power-${senderColor}`; headerSpan.textContent = `${msg.sender}: `; messageElement.appendChild(headerSpan); - + // Create a span for the message content that will be filled word by word const contentSpan = document.createElement('span'); contentSpan.id = `msg-content-${msgId.replace(/[^a-zA-Z0-9]/g, '-')}`; messageElement.appendChild(contentSpan); - + // Add timestamp const timeDiv = document.createElement('div'); timeDiv.className = 'message-time'; @@ -1381,12 +1420,12 @@ function addMessageToChat(msg, phaseName, animateWords = false, onComplete = nul // Private chat - outgoing or incoming style const isOutgoing = msg.sender === currentPower; messageElement.className = `chat-message ${isOutgoing ? 'message-outgoing' : 'message-incoming'}`; - + // Create content span const contentSpan = document.createElement('span'); contentSpan.id = `msg-content-${msgId.replace(/[^a-zA-Z0-9]/g, '-')}`; messageElement.appendChild(contentSpan); - + // Add timestamp const timeDiv = document.createElement('div'); timeDiv.className = 'message-time'; @@ -1399,7 +1438,7 @@ function addMessageToChat(msg, phaseName, animateWords = false, onComplete = nul // Scroll to bottom messagesContainer.scrollTop = messagesContainer.scrollHeight; - + if (animateWords) { // Start word-by-word animation const contentSpanId = `msg-content-${msgId.replace(/[^a-zA-Z0-9]/g, '-')}`; @@ -1410,7 +1449,7 @@ function addMessageToChat(msg, phaseName, animateWords = false, onComplete = nul if (contentSpan) { contentSpan.textContent = msg.message; } - + // If there's a completion callback, call it immediately for non-animated messages if (onComplete) { onComplete(); @@ -1429,43 +1468,43 @@ function animateMessageWords(message, contentSpanId, targetPower, messagesContai if (onComplete) onComplete(); return; } - + // Clear any existing content contentSpan.textContent = ''; let wordIndex = 0; - + // Function to add the next word const addNextWord = () => { if (wordIndex >= words.length) { // All words added - keep messagesPlaying true until next message starts - + // Add a slight delay after the last word for readability setTimeout(() => { if (onComplete) { onComplete(); // Call the completion callback } }, Math.min(playbackSpeed / 3, 150)); - + return; } - + // Add space if not the first word if (wordIndex > 0) { contentSpan.textContent += ' '; } - + // Add the next word contentSpan.textContent += words[wordIndex]; 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, playbackSpeed / 10 * (words[wordIndex - 1].length / 4))); setTimeout(addNextWord, delay); - + // Scroll to ensure newest content is visible messagesContainer.scrollTop = messagesContainer.scrollHeight; }; - + // Start animation addNextWord(); } @@ -1484,7 +1523,7 @@ function animateHeadNod(msg, playSoundEffect = true) { if (!chatWindow) return; // Find the face image and animate it - const img = chatWindow.querySelector(`#face-img-${targetPower}`); + const img = chatWindow.querySelector(`#face - img - ${targetPower} `); if (!img) return; img.dataset.animating = 'true'; @@ -1720,7 +1759,7 @@ function playRandomSoundEffect() { const chosen = soundEffects[Math.floor(Math.random() * soundEffects.length)]; // Create an