diff --git a/ai_animation/.gitignore b/ai_animation/.gitignore index 789ec75..f62b80a 100644 --- a/ai_animation/.gitignore +++ b/ai_animation/.gitignore @@ -25,3 +25,5 @@ dist-ssr # AI things .claude/ + +./public/games/ diff --git a/ai_animation/README.md b/ai_animation/README.md index acdc328..7c78a6d 100644 --- a/ai_animation/README.md +++ b/ai_animation/README.md @@ -20,33 +20,43 @@ The turn animation system is built around several key components that work toget The turn advancement follows a carefully orchestrated sequence: #### 1. Playback Initiation + When the user clicks Play, `togglePlayback()` is triggered, which: + - Sets `gameState.isPlaying = true` - Hides the standings board - Starts the camera pan animation - Begins message display for the current phase #### 2. Message Animation Phase + If the current phase has messages: + - `updateChatWindows()` displays messages word-by-word - Each message appears with typing animation - `gameState.messagesPlaying` tracks this state #### 3. Unit Animation Phase + Once messages complete (or if there are no messages): + - `displayPhaseWithAnimation()` is called - `createAnimationsForNextPhase()` analyzes the previous phase's orders - Movement tweens are created for each unit based on order results - Animations are added to `gameState.unitAnimations` array #### 4. Animation Monitoring + The main `animate()` loop continuously: + - Updates all active unit animations - Filters out completed animations - Detects when `gameState.unitAnimations.length === 0` #### 5. Phase Transition + When all animations complete: + - `advanceToNextPhase()` is scheduled with a configurable delay - If the phase has a summary, text-to-speech is triggered - After speech completes, `moveToNextPhase()` increments the phase index @@ -124,4 +134,7 @@ This architecture ensures smooth, coordinated animations while maintaining clear ## Game Data -Game data is loaded from JSON files in the `public/games/` directory. The expected format includes phases with messages, orders, and state information for each turn of the Diplomacy game. \ No newline at end of file +Game data is loaded from JSON files in the `public/games/` directory. The expected format includes phases with messages, orders, and state information for each turn of the Diplomacy game. + +--TODO: Create something to combine the game data into a simpler place, Diary is in csv and I need that to display the thoughts of the LLM during the betrayal scenes + diff --git a/ai_animation/index.html b/ai_animation/index.html index 3f22307..ef84066 100644 --- a/ai_animation/index.html +++ b/ai_animation/index.html @@ -27,6 +27,7 @@ No game loaded + Game: --
diff --git a/ai_animation/show_conversation.sh b/ai_animation/show_conversation.sh new file mode 100755 index 0000000..f0632ad --- /dev/null +++ b/ai_animation/show_conversation.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Script to show conversation between two powers in a specific phase +# Usage: ./show_conversation.sh [game_file] + +if [ $# -lt 3 ]; then + echo "Usage: $0 [game_file]" + echo "Example: $0 FRANCE ENGLAND S1901M" + echo "Example: $0 FRANCE ENGLAND S1901M public/games/0/game.json" + exit 1 +fi + +POWER1="$1" +POWER2="$2" +PHASE="$3" +GAME_FILE="${4:-public/default_game_formatted.json}" + +# Check if game file exists +if [ ! -f "$GAME_FILE" ]; then + echo "Error: Game file '$GAME_FILE' not found" + exit 1 +fi + +# Check if jq is installed +if ! command -v jq &>/dev/null; then + echo "Error: jq is required but not installed" + exit 1 +fi + +echo "=== Conversation between $POWER1 and $POWER2 in phase $PHASE ===" +echo "Game file: $GAME_FILE" +echo "" + +# Extract messages between the two powers in the specified phase, sorted by time +jq -r --arg power1 "$POWER1" --arg power2 "$POWER2" --arg phase "$PHASE" ' + .phases[] | + select(.name == $phase) | + .messages[] | + select( + (.sender == $power1 and .recipient == $power2) or + (.sender == $power2 and .recipient == $power1) + ) | + . as $msg | + "\($msg.time_sent) \($msg.sender) -> \($msg.recipient): \($msg.message)\n" +' "$GAME_FILE" | sort -n #| sed 's/^[0-9]* //' + +# Check if any messages were found +if [ ${PIPESTATUS[0]} -eq 0 ]; then + message_count=$(jq --arg power1 "$POWER1" --arg power2 "$POWER2" --arg phase "$PHASE" ' + .phases[] | + select(.name == $phase) | + .messages[] | + select( + (.sender == $power1 and .recipient == $power2) or + (.sender == $power2 and .recipient == $power1) + ) + ' "$GAME_FILE" | jq -s 'length') + + echo "" + echo "Found $message_count messages in phase $PHASE between $POWER1 and $POWER2" +else + echo "No messages found between $POWER1 and $POWER2 in phase $PHASE" +fi + diff --git a/ai_animation/src/domElements.ts b/ai_animation/src/domElements.ts index e3b4445..630a935 100644 --- a/ai_animation/src/domElements.ts +++ b/ai_animation/src/domElements.ts @@ -15,6 +15,21 @@ export function updatePhaseDisplay() { phaseDisplay.style.opacity = '1'; }, 300); } + +export function updateGameIdDisplay() { + if (!gameIdDisplay) return; + + // Add fade-out effect + gameIdDisplay.style.transition = 'opacity 0.3s ease-out'; + gameIdDisplay.style.opacity = '0'; + + // Update text after fade-out + setTimeout(() => { + gameIdDisplay.textContent = `Game: ${gameState.gameId}`; + // Fade back in + gameIdDisplay.style.opacity = '1'; + }, 300); +} // --- LOADING & DISPLAYING GAME PHASES --- export function loadGameBtnFunction(file) { const reader = new FileReader(); @@ -27,16 +42,41 @@ export function loadGameBtnFunction(file) { reader.readAsText(file); } export const loadBtn = document.getElementById('load-btn'); +if (null === loadBtn) throw new Error("Element with ID 'load-btn' not found"); + export const fileInput = document.getElementById('file-input'); +if (null === fileInput) throw new Error("Element with ID 'file-input' not found"); + export const prevBtn = document.getElementById('prev-btn'); +if (null === prevBtn) throw new Error("Element with ID 'prev-btn' not found"); + export const nextBtn = document.getElementById('next-btn'); +if (null === nextBtn) throw new Error("Element with ID 'next-btn' not found"); + export const playBtn = document.getElementById('play-btn'); +if (null === playBtn) throw new Error("Element with ID 'play-btn' not found"); + export const speedSelector = document.getElementById('speed-selector'); +if (null === speedSelector) throw new Error("Element with ID 'speed-selector' not found"); + export const phaseDisplay = document.getElementById('phase-display'); +if (null === phaseDisplay) throw new Error("Element with ID 'phase-display' not found"); + +export const gameIdDisplay = document.getElementById('game-id-display'); +if (null === gameIdDisplay) throw new Error("Element with ID 'game-id-display' not found"); + export const mapView = document.getElementById('map-view'); +if (null === mapView) throw new Error("Element with ID 'map-view' not found"); + export const leaderboard = document.getElementById('leaderboard'); +if (null === leaderboard) throw new Error("Element with ID 'leaderboard' not found"); + export const standingsBtn = document.getElementById('standings-btn'); +if (null === standingsBtn) throw new Error("Element with ID 'standings-btn' not found"); + export const relationshipsBtn = document.getElementById('relationships-btn'); +if (null === relationshipsBtn) throw new Error("Element with ID 'relationships-btn' not found"); + // Add roundRect polyfill for browsers that don't support it if (!CanvasRenderingContext2D.prototype.roundRect) { diff --git a/ai_animation/src/gameState.ts b/ai_animation/src/gameState.ts index aa99944..26f17ea 100644 --- a/ai_animation/src/gameState.ts +++ b/ai_animation/src/gameState.ts @@ -4,16 +4,16 @@ import { updateRelationshipPopup } from "./domElements/relationshipPopup"; import { type CoordinateData, CoordinateDataSchema, PowerENUM } from "./types/map" import type { GameSchemaType } from "./types/gameState"; import { GameSchema } from "./types/gameState"; -import { prevBtn, nextBtn, playBtn, speedSelector, mapView } from "./domElements"; +import { prevBtn, nextBtn, playBtn, speedSelector, mapView, updateGameIdDisplay } from "./domElements"; import { createChatWindows } from "./domElements/chatWindows"; import { logger } from "./logger"; import { OrbitControls } from "three/examples/jsm/Addons.js"; import { displayInitialPhase } from "./phase"; import { Tween, Group as TweenGroup } from "@tweenjs/tween.js"; import { hideStandingsBoard, } from "./domElements/standingsBoard"; -import { MomentsDataSchema, MomentsMetadataSchema } from "./types/moments"; +import { MomentsDataSchema, MomentsDataSchemaType } from "./types/moments"; -//FIXME: This whole file is a mess. Need to organize and format +//FIXME: This whole file is a mess. Need to organkze and format enum AvailableMaps { STANDARD = "standard" @@ -34,7 +34,7 @@ class GameState { boardState: CoordinateData gameId: number gameData: GameSchemaType - momentsData: MomentSchemaType + momentsData: MomentsDataSchemaType phaseIndex: number boardName: string currentPower: PowerENUM @@ -133,6 +133,9 @@ class GameState { // Display the initial phase displayInitialPhase() + // Update game ID display + updateGameIdDisplay(); + this.loadMomentsFile() resolve() } else { @@ -204,6 +207,18 @@ class GameState { return false; } + /* + * Loads the next game in the order, reseting the board and gameState + */ + loadNextGame = () => { + // + + this.gameId += 1 + + // Try to load the next game, if it fails, show end screen forever + + } + /* * Given a gameId, load that game's state into the GameState Object */ @@ -251,6 +266,7 @@ class GameState { if (this.gameData) { updateRotatingDisplay(this.gameData, this.phaseIndex, this.currentPower); updateRelationshipPopup(); + updateGameIdDisplay(); } }) .catch(error => { @@ -304,7 +320,7 @@ class GameState { }) } - initScene = () => { + createThreeScene = () => { if (mapView === null) { throw Error("Cannot find mapView element, unable to continue.") } @@ -332,6 +348,14 @@ class GameState { this.camControls.maxDistance = 2000; this.camControls.maxPolarAngle = Math.PI / 2; // Limit so you don't flip under the map this.camControls.target.set(0, 0, 100); // ADDED: Set control target to new map center + + + // Lighting (keep it simple) + this.scene.add(new THREE.AmbientLight(0xffffff, 0.6)); + + const dirLight = new THREE.DirectionalLight(0xffffff, 0.6); + dirLight.position.set(300, 400, 300); + this.scene.add(dirLight); } } diff --git a/ai_animation/src/style.css b/ai_animation/src/style.css index 16726b4..88140de 100644 --- a/ai_animation/src/style.css +++ b/ai_animation/src/style.css @@ -28,7 +28,7 @@ background: linear-gradient(90deg, #5a3e2b 0%, #382519 100%); color: #f0e6d2; display: flex; - justify-content: space-between; + justify-content: flex-start; align-items: center; border-bottom: 3px solid #2e1c10; } @@ -46,6 +46,18 @@ font-size: 1rem; } + #game-id-display { + font-weight: bold; + margin-left: auto; + padding: 4px 8px; + border: 2px solid #2e1c10; + border-radius: 6px; + background-color: rgba(0, 0, 0, 0.3); + color: #ffd; + font-family: "Book Antiqua", Palatino, serif; + font-size: 1rem; + } + /* Buttons (Load Game, Next, Prev, etc.) */ button { padding: 8px 16px;