Adding a GameId to allow moving through a list of games.

This commit is contained in:
Tyler Marques 2025-05-27 12:52:56 -07:00
parent 9314a411f9
commit 9a43be9b9c
No known key found for this signature in database
GPG key ID: CB99EDCF41D3016F
7 changed files with 163 additions and 7 deletions

View file

@ -25,3 +25,5 @@ dist-ssr
# AI things
.claude/
./public/games/

View file

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

View file

@ -27,6 +27,7 @@
</select>
<span id="phase-display">No game loaded</span>
</div>
<span id="game-id-display">Game: --</span>
</div>
<div id="map-view" class="map-view"></div>
<input type="file" id="file-input" accept=".json">

View file

@ -0,0 +1,64 @@
#!/bin/bash
# Script to show conversation between two powers in a specific phase
# Usage: ./show_conversation.sh <power1> <power2> <phase> [game_file]
if [ $# -lt 3 ]; then
echo "Usage: $0 <power1> <power2> <phase> [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

View file

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

View file

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

View file

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