mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-19 12:58:09 +00:00
FIX: Map jump bug, leaderboard misname
Reintroducing the fix for the bug in tween.js (issue 677) that causes the map to jump at the end of the tween. Leaderboard name was used twice in the code base, once for the modal at the beginning of games, and also for the rotating display in the bottom left. I've removed the modal at the beggining of the game as its data is stale and not updated yet. I've renamed the bottom left to 'rotatingDisplay' and the bottom right to 'leaderboard'.
This commit is contained in:
parent
d52753569b
commit
b8a8f5a6f2
13 changed files with 213 additions and 264 deletions
|
|
@ -15,7 +15,6 @@
|
|||
<div class="top-controls">
|
||||
<div>
|
||||
<button id="load-btn">Load Game</button>
|
||||
<button id="standings-btn">📊 Leaderboard</button>
|
||||
<button id="prev-btn" disabled>← Prev</button>
|
||||
<button id="next-btn" disabled>Next →</button>
|
||||
<button id="play-btn" disabled>▶ Play</button>
|
||||
|
|
@ -30,9 +29,8 @@
|
|||
</div>
|
||||
<div id="map-view" class="map-view"></div>
|
||||
<input type="file" id="file-input" accept=".json">
|
||||
<div id="info-panel"></div>
|
||||
<!-- New leaderboard element -->
|
||||
<div id="leaderboard"></div>
|
||||
<div id="rotating-display"></div>
|
||||
<!-- Chat windows container -->
|
||||
<div id="chat-container"></div>
|
||||
<!-- Add this after the info-panel div -->
|
||||
|
|
|
|||
|
|
@ -10,28 +10,27 @@ let isAudioInitialized = false;
|
|||
* Only loads in streaming mode to avoid unnecessary downloads
|
||||
*/
|
||||
export function initializeBackgroundAudio(): void {
|
||||
const isStreamingMode = import.meta.env.VITE_STREAMING_MODE === 'True' || import.meta.env.VITE_STREAMING_MODE === 'true';
|
||||
|
||||
if (!isStreamingMode || isAudioInitialized) {
|
||||
return;
|
||||
|
||||
if (isAudioInitialized) {
|
||||
throw new Error("Attempted to init audio twice.")
|
||||
}
|
||||
|
||||
isAudioInitialized = true;
|
||||
|
||||
|
||||
// Create audio element
|
||||
backgroundAudio = new Audio();
|
||||
backgroundAudio.loop = true;
|
||||
backgroundAudio.volume = 0.4; // 40% volume as requested
|
||||
|
||||
|
||||
// For now, we'll use a placeholder - you should download and convert the wave file
|
||||
// to a smaller MP3 format (aim for < 10MB) and place it in public/sounds/
|
||||
backgroundAudio.src = './sounds/background_ambience.mp3';
|
||||
|
||||
|
||||
// Handle audio loading
|
||||
backgroundAudio.addEventListener('canplaythrough', () => {
|
||||
console.log('Background audio loaded and ready to play');
|
||||
});
|
||||
|
||||
|
||||
backgroundAudio.addEventListener('error', (e) => {
|
||||
console.error('Failed to load background audio:', e);
|
||||
});
|
||||
|
|
@ -66,4 +65,4 @@ export function setBackgroundAudioVolume(volume: number): void {
|
|||
if (backgroundAudio) {
|
||||
backgroundAudio.volume = Math.max(0, Math.min(1, volume));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
77
ai_animation/src/components/leaderboard.ts
Normal file
77
ai_animation/src/components/leaderboard.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { gameState } from "../gameState";
|
||||
import { getPowerDisplayName } from "../utils/powerNames";
|
||||
import { PowerENUM } from "../types/map";
|
||||
|
||||
|
||||
let containerElement = document.getElementById("leaderboard")
|
||||
|
||||
export function initLeaderBoard() {
|
||||
if (!containerElement) {
|
||||
console.error(`Container element with ID "${containerId}" not found`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Updated function to update leaderboard with useful information and smooth transitions
|
||||
export function updateLeaderboard() {
|
||||
const totalPhases = gameState.gameData?.phases?.length || 0;
|
||||
const currentPhaseNumber = gameState.phaseIndex + 1;
|
||||
const phaseName = gameState.gameData?.phases?.[gameState.phaseIndex]?.name || 'Unknown';
|
||||
|
||||
// Add fade-out transition
|
||||
containerElement.style.transition = 'opacity 0.3s ease-out';
|
||||
containerElement.style.opacity = '0';
|
||||
|
||||
// Update content after fade-out
|
||||
setTimeout(() => {
|
||||
// Get supply center counts for the current phase
|
||||
const scCounts = getSupplyCenterCounts();
|
||||
|
||||
containerElement.innerHTML = `
|
||||
<div><strong>Power:</strong> <span class="power-${gameState.currentPower.toLowerCase()}">${getPowerDisplayName(gameState.currentPower)}</span></div>
|
||||
<div><strong>Current Phase:</strong> ${phaseName}</div>
|
||||
<hr/>
|
||||
<h4>Supply Center Counts</h4>
|
||||
<ul style="list-style:none;padding-left:0;margin:0;">
|
||||
<li><span class="power-austria">${getPowerDisplayName(PowerENUM.AUSTRIA)}:</span> ${scCounts.AUSTRIA || 0}</li>
|
||||
<li><span class="power-england">${getPowerDisplayName(PowerENUM.ENGLAND)}:</span> ${scCounts.ENGLAND || 0}</li>
|
||||
<li><span class="power-france">${getPowerDisplayName(PowerENUM.FRANCE)}:</span> ${scCounts.FRANCE || 0}</li>
|
||||
<li><span class="power-germany">${getPowerDisplayName(PowerENUM.GERMANY)}:</span> ${scCounts.GERMANY || 0}</li>
|
||||
<li><span class="power-italy">${getPowerDisplayName(PowerENUM.ITALY)}:</span> ${scCounts.ITALY || 0}</li>
|
||||
<li><span class="power-russia">${getPowerDisplayName(PowerENUM.RUSSIA)}:</span> ${scCounts.RUSSIA || 0}</li>
|
||||
<li><span class="power-turkey">${getPowerDisplayName(PowerENUM.TURKEY)}:</span> ${scCounts.TURKEY || 0}</li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
// Fade back in
|
||||
containerElement.style.opacity = '1';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Helper function to count supply centers for each power
|
||||
function getSupplyCenterCounts() {
|
||||
const counts = {
|
||||
AUSTRIA: 0,
|
||||
ENGLAND: 0,
|
||||
FRANCE: 0,
|
||||
GERMANY: 0,
|
||||
ITALY: 0,
|
||||
RUSSIA: 0,
|
||||
TURKEY: 0
|
||||
};
|
||||
|
||||
// Get current phase's supply center data
|
||||
const centers = gameState.gameData?.phases?.[gameState.phaseIndex]?.state?.centers;
|
||||
|
||||
if (centers) {
|
||||
// Count supply centers for each power
|
||||
Object.entries(centers).forEach(([power, provinces]) => {
|
||||
if (power && Array.isArray(provinces)) {
|
||||
counts[power as keyof typeof counts] = provinces.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ let isInitialized = false;
|
|||
* Initialize the rotating display
|
||||
* @param containerId The ID of the container element
|
||||
*/
|
||||
export function initRotatingDisplay(containerId: string): void {
|
||||
export function initRotatingDisplay(containerId: string = 'rotating-display'): void {
|
||||
containerElement = document.getElementById(containerId);
|
||||
|
||||
if (!containerElement) {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
export const config = {
|
||||
// Default speed in milliseconds for animations and transitions
|
||||
playbackSpeed: 500,
|
||||
|
||||
|
||||
// Streaming mode specific timing
|
||||
get streamingPlaybackSpeed(): number {
|
||||
const isStreamingMode = import.meta.env.VITE_STREAMING_MODE === 'True' || import.meta.env.VITE_STREAMING_MODE === 'true';
|
||||
const isStreamingMode = import.meta.env.MODE === 'production'
|
||||
return isStreamingMode ? 1000 : this.playbackSpeed; // Slower for streaming
|
||||
},
|
||||
|
||||
|
|
@ -74,13 +74,13 @@ export const config = {
|
|||
unitBobFrequency: 0.8,
|
||||
fleetRollFrequency: 0.5,
|
||||
fleetPitchFrequency: 0.3,
|
||||
|
||||
|
||||
// Supply center pulse frequency (Hz)
|
||||
supplyPulseFrequency: 1.0,
|
||||
|
||||
|
||||
// Province highlight flash frequency (Hz)
|
||||
provinceFlashFrequency: 2.0,
|
||||
|
||||
|
||||
// Maximum frame delta time (seconds) to prevent animation jumps
|
||||
maxDeltaTime: 0.1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,9 +71,8 @@ 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 rotatingDisplay = document.getElementById('rotating-display');
|
||||
if (null === rotatingDisplay) throw new Error("Element with ID 'rotating-display' not found");
|
||||
// Debug menu elements
|
||||
export const debugMenu = document.getElementById('debug-menu');
|
||||
if (null === debugMenu) throw new Error("Element with ID 'debug-menu' not found");
|
||||
|
|
|
|||
|
|
@ -201,20 +201,20 @@ export function updateChatWindows(phase: any, stepMessages = false) {
|
|||
console.log(`All messages displayed in ${Date.now() - messageStartTime}ms`);
|
||||
}
|
||||
gameState.messagesPlaying = false;
|
||||
|
||||
|
||||
// Trigger unit animations now that messages are done
|
||||
// This imports a circular dependency, so we use a dynamic import
|
||||
import('../units/animate').then(({ createAnimationsForNextPhase }) => {
|
||||
const phaseIndex = gameState.phaseIndex;
|
||||
const isFirstPhase = phaseIndex === 0;
|
||||
const previousPhase = !isFirstPhase && phaseIndex > 0 ? gameState.gameData.phases[phaseIndex - 1] : null;
|
||||
|
||||
|
||||
if (!isFirstPhase && previousPhase) {
|
||||
console.log("Messages complete, starting unit animations");
|
||||
createAnimationsForNextPhase();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -231,10 +231,7 @@ export function updateChatWindows(phase: any, stepMessages = false) {
|
|||
index++; // Only increment after animation completes
|
||||
|
||||
// Schedule next message with proper delay
|
||||
// In streaming mode, add extra delay to prevent message overlap
|
||||
const isStreamingMode = import.meta.env.VITE_STREAMING_MODE === 'True' || import.meta.env.VITE_STREAMING_MODE === 'true';
|
||||
const messageDelay = isStreamingMode ? config.effectivePlaybackSpeed : config.effectivePlaybackSpeed / 2;
|
||||
setTimeout(showNext, messageDelay);
|
||||
setTimeout(showNext, config.effectivePlaybackSpeed);
|
||||
};
|
||||
|
||||
// Add the message with word animation
|
||||
|
|
@ -398,14 +395,13 @@ function animateMessageWords(message: string, contentSpanId: string, targetPower
|
|||
// Longer words get slightly longer display time
|
||||
const wordLength = words[wordIndex - 1].length;
|
||||
// In streaming mode, use a more consistent delay to prevent overlap
|
||||
const isStreamingMode = import.meta.env.VITE_STREAMING_MODE === 'True' || import.meta.env.VITE_STREAMING_MODE === 'true';
|
||||
const baseDelay = isStreamingMode ? 150 : config.effectivePlaybackSpeed / 10;
|
||||
const baseDelay = config.effectivePlaybackSpeed
|
||||
const delay = Math.max(50, Math.min(200, baseDelay * (wordLength / 4)));
|
||||
setTimeout(addNextWord, delay);
|
||||
|
||||
// Scroll to ensure newest content is visible
|
||||
// Use requestAnimationFrame to batch DOM updates in streaming mode
|
||||
const isStreamingModeForScroll = import.meta.env.VITE_STREAMING_MODE === 'True' || import.meta.env.VITE_STREAMING_MODE === 'true';
|
||||
const isStreamingModeForScroll = import.meta.env.MODE === 'production' || import.meta.env.VITE_STREAMING_MODE === 'true';
|
||||
if (isStreamingModeForScroll) {
|
||||
requestAnimationFrame(() => {
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
|
|
@ -672,17 +668,17 @@ function playRandomSoundEffect() {
|
|||
// Create an <audio> and play
|
||||
const audio = new Audio(`./sounds/${chosen}`);
|
||||
audio.volume = 0.5; // Set volume to 50% to avoid being too loud
|
||||
|
||||
if (config.isDebugMode || config.isTestingMode) {
|
||||
console.debug("Not playing sounds in debug or testing mode");
|
||||
|
||||
if (config.isDebugMode || config.isTestingMode) {
|
||||
console.debug("Not playing sounds in debug or testing mode");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log(`Attempting to play sound: ${chosen}`);
|
||||
|
||||
|
||||
// Try to play the audio
|
||||
const playPromise = audio.play();
|
||||
|
||||
|
||||
if (playPromise !== undefined) {
|
||||
playPromise
|
||||
.then(() => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { StandingsData, StandingsEntry, SortBy, SortDirection, SortOptions } from '../types/standings';
|
||||
import { gameState } from '../gameState';
|
||||
import { logger } from '../logger';
|
||||
import { standingsBtn } from '../domElements';
|
||||
//import { standingsBtn } from '../domElements';
|
||||
import { getPowerDisplayName } from '../utils/powerNames';
|
||||
import { PowerENUM } from '../types/map';
|
||||
|
||||
|
|
@ -25,22 +25,22 @@ export function initStandingsBoard(): void {
|
|||
if (!document.getElementById('standings-board-container')) {
|
||||
createStandingsBoardElements();
|
||||
}
|
||||
|
||||
|
||||
// Get references to the created elements
|
||||
standingsBoardContainer = document.getElementById('standings-board-container');
|
||||
standingsTable = document.getElementById('standings-table');
|
||||
closeButton = document.getElementById('standings-close-btn');
|
||||
|
||||
|
||||
// Add event listeners
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', hideStandingsBoard);
|
||||
}
|
||||
|
||||
|
||||
// Add click handler for the standings button
|
||||
if (standingsBtn) {
|
||||
standingsBtn.addEventListener('click', toggleStandingsBoard);
|
||||
}
|
||||
|
||||
|
||||
// Load standings data
|
||||
loadStandingsData();
|
||||
}
|
||||
|
|
@ -52,34 +52,34 @@ function createStandingsBoardElements(): void {
|
|||
const container = document.createElement('div');
|
||||
container.id = 'standings-board-container';
|
||||
container.className = 'standings-board-container';
|
||||
|
||||
|
||||
// Create header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'standings-header';
|
||||
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.textContent = 'AI Diplomacy Leaderboard';
|
||||
header.appendChild(title);
|
||||
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.id = 'standings-close-btn';
|
||||
closeBtn.textContent = '×';
|
||||
closeBtn.title = 'Close Leaderboard';
|
||||
header.appendChild(closeBtn);
|
||||
|
||||
|
||||
container.appendChild(header);
|
||||
|
||||
|
||||
// Create table container
|
||||
const tableContainer = document.createElement('div');
|
||||
tableContainer.className = 'standings-table-container';
|
||||
|
||||
|
||||
const table = document.createElement('table');
|
||||
table.id = 'standings-table';
|
||||
table.className = 'standings-table';
|
||||
tableContainer.appendChild(table);
|
||||
|
||||
|
||||
container.appendChild(tableContainer);
|
||||
|
||||
|
||||
// Create legend/info section
|
||||
const legend = document.createElement('div');
|
||||
legend.className = 'standings-legend';
|
||||
|
|
@ -87,7 +87,7 @@ function createStandingsBoardElements(): void {
|
|||
<p>Numbers indicate wins per Power-Model combination. Click column headers to sort.</p>
|
||||
`;
|
||||
container.appendChild(legend);
|
||||
|
||||
|
||||
// Add to document
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
|
@ -119,33 +119,33 @@ function loadStandingsData(): void {
|
|||
function parseCSV(csvText: string): StandingsData {
|
||||
const lines = csvText.split('\n').filter(line => line.trim().length > 0);
|
||||
const headers = lines[0].split(',').map(h => h.trim());
|
||||
|
||||
|
||||
// First column is 'Model', rest are power names
|
||||
const powers = headers.slice(1);
|
||||
|
||||
|
||||
// Process each data row
|
||||
const entries: StandingsEntry[] = [];
|
||||
const models: string[] = [];
|
||||
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const values = lines[i].split(',').map(v => v.trim());
|
||||
const model = values[0];
|
||||
models.push(model);
|
||||
|
||||
|
||||
// Create wins record
|
||||
const wins: Record<string, number> = {};
|
||||
let totalWins = 0;
|
||||
|
||||
|
||||
for (let j = 1; j < values.length; j++) {
|
||||
const power = powers[j - 1];
|
||||
const winCount = parseInt(values[j]) || 0;
|
||||
wins[power] = winCount;
|
||||
totalWins += winCount;
|
||||
}
|
||||
|
||||
|
||||
entries.push({ model, wins, totalWins });
|
||||
}
|
||||
|
||||
|
||||
return { models, powers, entries };
|
||||
}
|
||||
|
||||
|
|
@ -154,21 +154,21 @@ function parseCSV(csvText: string): StandingsData {
|
|||
*/
|
||||
function renderStandingsTable(): void {
|
||||
if (!standingsTable || !standingsData) return;
|
||||
|
||||
|
||||
// Clear existing content
|
||||
standingsTable.innerHTML = '';
|
||||
|
||||
|
||||
// Create header row
|
||||
const thead = document.createElement('thead');
|
||||
const headerRow = document.createElement('tr');
|
||||
|
||||
|
||||
// Model column header
|
||||
const modelHeader = document.createElement('th');
|
||||
modelHeader.textContent = 'Model';
|
||||
modelHeader.className = 'model-header';
|
||||
modelHeader.addEventListener('click', () => sortTable(SortBy.MODEL));
|
||||
headerRow.appendChild(modelHeader);
|
||||
|
||||
|
||||
// Power column headers
|
||||
standingsData.powers.forEach(power => {
|
||||
const th = document.createElement('th');
|
||||
|
|
@ -177,23 +177,23 @@ function renderStandingsTable(): void {
|
|||
th.addEventListener('click', () => sortTable(`power_${power}`));
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
|
||||
|
||||
// Total column header
|
||||
const totalHeader = document.createElement('th');
|
||||
totalHeader.textContent = 'Total';
|
||||
totalHeader.className = 'total-header';
|
||||
totalHeader.addEventListener('click', () => sortTable(SortBy.TOTAL_WINS));
|
||||
headerRow.appendChild(totalHeader);
|
||||
|
||||
|
||||
thead.appendChild(headerRow);
|
||||
standingsTable.appendChild(thead);
|
||||
|
||||
|
||||
// Sort entries based on current sort options
|
||||
const sortedEntries = [...standingsData.entries].sort((a, b) => {
|
||||
if (currentSort.by === SortBy.MODEL) {
|
||||
const comparison = a.model.localeCompare(b.model);
|
||||
return currentSort.direction === SortDirection.ASC ? comparison : -comparison;
|
||||
}
|
||||
}
|
||||
else if (currentSort.by === SortBy.TOTAL_WINS) {
|
||||
const comparison = a.totalWins - b.totalWins;
|
||||
return currentSort.direction === SortDirection.ASC ? comparison : -comparison;
|
||||
|
|
@ -205,19 +205,19 @@ function renderStandingsTable(): void {
|
|||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
|
||||
// Create table body
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
|
||||
sortedEntries.forEach(entry => {
|
||||
const row = document.createElement('tr');
|
||||
|
||||
|
||||
// Model cell
|
||||
const modelCell = document.createElement('td');
|
||||
modelCell.textContent = entry.model;
|
||||
modelCell.className = 'model-cell';
|
||||
row.appendChild(modelCell);
|
||||
|
||||
|
||||
// Power cells
|
||||
standingsData.powers.forEach(power => {
|
||||
const td = document.createElement('td');
|
||||
|
|
@ -229,18 +229,18 @@ function renderStandingsTable(): void {
|
|||
if (wins >= 5) td.classList.add('top-wins');
|
||||
row.appendChild(td);
|
||||
});
|
||||
|
||||
|
||||
// Total cell
|
||||
const totalCell = document.createElement('td');
|
||||
totalCell.textContent = entry.totalWins.toString();
|
||||
totalCell.className = 'total-cell';
|
||||
row.appendChild(totalCell);
|
||||
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
|
||||
standingsTable.appendChild(tbody);
|
||||
|
||||
|
||||
// Update sort indicators
|
||||
updateSortIndicators();
|
||||
}
|
||||
|
|
@ -250,24 +250,24 @@ function renderStandingsTable(): void {
|
|||
*/
|
||||
function updateSortIndicators(): void {
|
||||
if (!standingsTable) return;
|
||||
|
||||
|
||||
// Remove all sort indicators
|
||||
const allHeaders = standingsTable.querySelectorAll('th');
|
||||
allHeaders.forEach(header => {
|
||||
header.classList.remove('sort-asc', 'sort-desc');
|
||||
});
|
||||
|
||||
|
||||
// Add indicator to current sort column
|
||||
let headerSelector = '.model-header';
|
||||
|
||||
|
||||
if (currentSort.by === SortBy.TOTAL_WINS) {
|
||||
headerSelector = '.total-header';
|
||||
}
|
||||
}
|
||||
else if (currentSort.by.startsWith('power_')) {
|
||||
const power = currentSort.by.replace('power_', '');
|
||||
headerSelector = `.power-${power.toLowerCase()}`;
|
||||
}
|
||||
|
||||
|
||||
const header = standingsTable.querySelector(headerSelector);
|
||||
if (header) {
|
||||
header.classList.add(
|
||||
|
|
@ -282,9 +282,9 @@ function updateSortIndicators(): void {
|
|||
function sortTable(sortBy: SortBy | string): void {
|
||||
if (currentSort.by === sortBy) {
|
||||
// Toggle direction if already sorting by this column
|
||||
currentSort.direction =
|
||||
currentSort.direction === SortDirection.ASC
|
||||
? SortDirection.DESC
|
||||
currentSort.direction =
|
||||
currentSort.direction === SortDirection.ASC
|
||||
? SortDirection.DESC
|
||||
: SortDirection.ASC;
|
||||
} else {
|
||||
// Set new sort column with default direction
|
||||
|
|
@ -293,7 +293,7 @@ function sortTable(sortBy: SortBy | string): void {
|
|||
direction: sortBy === SortBy.MODEL ? SortDirection.ASC : SortDirection.DESC
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
renderStandingsTable();
|
||||
}
|
||||
|
||||
|
|
@ -338,4 +338,4 @@ export function updateStandingsBoardVisibility(): void {
|
|||
} else {
|
||||
hideStandingsBoard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import { logger } from "./logger";
|
|||
import { OrbitControls } from "three/examples/jsm/Addons.js";
|
||||
import { displayInitialPhase, togglePlayback } from "./phase";
|
||||
import { Tween, Group as TweenGroup } from "@tweenjs/tween.js";
|
||||
import { hideStandingsBoard, } from "./domElements/standingsBoard";
|
||||
import { MomentsDataSchema, Moment, NormalizedMomentsData } from "./types/moments";
|
||||
import { updateLeaderboard } from "./components/leaderboard";
|
||||
|
||||
//FIXME: This whole file is a mess. Need to organize and format
|
||||
|
||||
|
|
@ -26,28 +26,28 @@ function getRandomPower(gameData?: GameSchemaType): PowerENUM {
|
|||
const allPowers = Object.values(PowerENUM).filter(power =>
|
||||
power !== PowerENUM.GLOBAL && power !== PowerENUM.EUROPE
|
||||
);
|
||||
|
||||
|
||||
// If no game data provided, return any random power
|
||||
if (!gameData || !gameData.phases || gameData.phases.length === 0) {
|
||||
const idx = Math.floor(Math.random() * allPowers.length);
|
||||
return allPowers[idx];
|
||||
}
|
||||
|
||||
|
||||
// Get the last phase to check supply centers
|
||||
const lastPhase = gameData.phases[gameData.phases.length - 1];
|
||||
|
||||
|
||||
// Filter powers that have more than 2 supply centers
|
||||
const eligiblePowers = allPowers.filter(power => {
|
||||
const centers = lastPhase.state?.centers?.[power];
|
||||
return centers && centers.length > 2;
|
||||
});
|
||||
|
||||
|
||||
// If no powers have more than 2 centers, fall back to any power
|
||||
if (eligiblePowers.length === 0) {
|
||||
const idx = Math.floor(Math.random() * allPowers.length);
|
||||
return allPowers[idx];
|
||||
}
|
||||
|
||||
|
||||
const idx = Math.floor(Math.random() * eligiblePowers.length);
|
||||
return eligiblePowers[idx];
|
||||
}
|
||||
|
|
@ -339,13 +339,12 @@ class GameState {
|
|||
})
|
||||
.then(() => {
|
||||
console.log(`Game file with id ${gameId} loaded and parsed successfully`);
|
||||
// Explicitly hide standings board after loading game
|
||||
hideStandingsBoard();
|
||||
// Update rotating display and relationship popup with game data
|
||||
if (this.gameData) {
|
||||
updateRotatingDisplay(this.gameData, this.phaseIndex, this.currentPower);
|
||||
this.gameId = gameId
|
||||
updateGameIdDisplay();
|
||||
updateLeaderboard();
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
|
|
@ -376,9 +375,7 @@ class GameState {
|
|||
if (mapView === null) {
|
||||
throw Error("Cannot find mapView element, unable to continue.")
|
||||
}
|
||||
|
||||
const isStreamingMode = import.meta.env.VITE_STREAMING_MODE === 'True' || import.meta.env.VITE_STREAMING_MODE === 'true';
|
||||
|
||||
|
||||
this.scene.background = new THREE.Color(0x87CEEB);
|
||||
|
||||
// Camera
|
||||
|
|
@ -389,28 +386,17 @@ class GameState {
|
|||
3000
|
||||
);
|
||||
this.camera.position.set(0, 800, 900); // MODIFIED: Increased z-value to account for map shift
|
||||
|
||||
|
||||
// Renderer with streaming optimizations
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
antialias: !isStreamingMode, // Disable antialiasing in streaming mode
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
powerPreference: "high-performance",
|
||||
preserveDrawingBuffer: isStreamingMode // Required for streaming
|
||||
});
|
||||
this.renderer.setSize(mapView.clientWidth, mapView.clientHeight);
|
||||
|
||||
// Force lower pixel ratio for streaming to reduce GPU load
|
||||
if (isStreamingMode) {
|
||||
this.renderer.setPixelRatio(1);
|
||||
} else {
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
}
|
||||
|
||||
|
||||
mapView.appendChild(this.renderer.domElement);
|
||||
|
||||
// Controls with streaming optimizations
|
||||
this.camControls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.camControls.enableDamping = !isStreamingMode; // Disable damping for immediate response
|
||||
this.camControls.dampingFactor = 0.05;
|
||||
this.camControls.screenSpacePanning = true;
|
||||
this.camControls.minDistance = 100;
|
||||
this.camControls.maxDistance = 2000;
|
||||
|
|
|
|||
|
|
@ -20,67 +20,5 @@ class Logger {
|
|||
console.log(msg);
|
||||
}
|
||||
|
||||
// Updated function to update info panel with useful information and smooth transitions
|
||||
updateInfoPanel = () => {
|
||||
const totalPhases = gameState.gameData?.phases?.length || 0;
|
||||
const currentPhaseNumber = gameState.phaseIndex + 1;
|
||||
const phaseName = gameState.gameData?.phases?.[gameState.phaseIndex]?.name || 'Unknown';
|
||||
|
||||
// Add fade-out transition
|
||||
this.infoPanel.style.transition = 'opacity 0.3s ease-out';
|
||||
this.infoPanel.style.opacity = '0';
|
||||
|
||||
// Update content after fade-out
|
||||
setTimeout(() => {
|
||||
// Get supply center counts for the current phase
|
||||
const scCounts = this.getSupplyCenterCounts();
|
||||
|
||||
this.infoPanel.innerHTML = `
|
||||
<div><strong>Power:</strong> <span class="power-${gameState.currentPower.toLowerCase()}">${getPowerDisplayName(gameState.currentPower)}</span></div>
|
||||
<div><strong>Current Phase:</strong> ${phaseName}</div>
|
||||
<hr/>
|
||||
<h4>Supply Center Counts</h4>
|
||||
<ul style="list-style:none;padding-left:0;margin:0;">
|
||||
<li><span class="power-austria">${getPowerDisplayName(PowerENUM.AUSTRIA)}:</span> ${scCounts.AUSTRIA || 0}</li>
|
||||
<li><span class="power-england">${getPowerDisplayName(PowerENUM.ENGLAND)}:</span> ${scCounts.ENGLAND || 0}</li>
|
||||
<li><span class="power-france">${getPowerDisplayName(PowerENUM.FRANCE)}:</span> ${scCounts.FRANCE || 0}</li>
|
||||
<li><span class="power-germany">${getPowerDisplayName(PowerENUM.GERMANY)}:</span> ${scCounts.GERMANY || 0}</li>
|
||||
<li><span class="power-italy">${getPowerDisplayName(PowerENUM.ITALY)}:</span> ${scCounts.ITALY || 0}</li>
|
||||
<li><span class="power-russia">${getPowerDisplayName(PowerENUM.RUSSIA)}:</span> ${scCounts.RUSSIA || 0}</li>
|
||||
<li><span class="power-turkey">${getPowerDisplayName(PowerENUM.TURKEY)}:</span> ${scCounts.TURKEY || 0}</li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
// Fade back in
|
||||
this.infoPanel.style.opacity = '1';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Helper function to count supply centers for each power
|
||||
getSupplyCenterCounts = () => {
|
||||
const counts = {
|
||||
AUSTRIA: 0,
|
||||
ENGLAND: 0,
|
||||
FRANCE: 0,
|
||||
GERMANY: 0,
|
||||
ITALY: 0,
|
||||
RUSSIA: 0,
|
||||
TURKEY: 0
|
||||
};
|
||||
|
||||
// Get current phase's supply center data
|
||||
const centers = gameState.gameData?.phases?.[gameState.phaseIndex]?.state?.centers;
|
||||
|
||||
if (centers) {
|
||||
// Count supply centers for each power
|
||||
Object.entries(centers).forEach(([power, provinces]) => {
|
||||
if (power && Array.isArray(provinces)) {
|
||||
counts[power as keyof typeof counts] = provinces.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
}
|
||||
export const logger = new Logger()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { gameState } from "./gameState";
|
|||
import { logger } from "./logger";
|
||||
import { loadBtn, prevBtn, nextBtn, speedSelector, fileInput, playBtn, mapView, loadGameBtnFunction } from "./domElements";
|
||||
import { updateChatWindows } from "./domElements/chatWindows";
|
||||
import { initStandingsBoard, hideStandingsBoard, showStandingsBoard } from "./domElements/standingsBoard";
|
||||
import { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase, nextPhase, previousPhase, togglePlayback } from "./phase";
|
||||
import { config } from "./config";
|
||||
import { Tween, Group, Easing } from "@tweenjs/tween.js";
|
||||
|
|
@ -13,84 +12,50 @@ import { initRotatingDisplay, updateRotatingDisplay } from "./components/rotatin
|
|||
import { closeTwoPowerConversation, showTwoPowerConversation } from "./components/twoPowerConversation";
|
||||
import { PowerENUM } from "./types/map";
|
||||
import { debugMenuInstance } from "./debug/debugMenu";
|
||||
import { sineWave } from "./utils/timing";
|
||||
import { initializeBackgroundAudio, startBackgroundAudio } from "./backgroundAudio";
|
||||
import { initLeaderBoard, updateLeaderboard } from "./components/leaderboard";
|
||||
|
||||
//TODO: Create a function that finds a suitable unit location within a given polygon, for placing units better
|
||||
// Currently the location for label, unit, and SC are all the same manually picked location
|
||||
|
||||
const isStreamingMode = import.meta.env.VITE_STREAMING_MODE === 'True' || import.meta.env.VITE_STREAMING_MODE === 'true'
|
||||
const isStreamingMode = import.meta.env.VITE_STREAMING_MODE
|
||||
|
||||
// --- INITIALIZE SCENE ---
|
||||
function initScene() {
|
||||
gameState.createThreeScene()
|
||||
|
||||
// Initialize background audio for streaming mode
|
||||
initializeBackgroundAudio();
|
||||
|
||||
// Enable audio on first user interaction (to comply with browser autoplay policies)
|
||||
let audioEnabled = false;
|
||||
const enableAudio = () => {
|
||||
if (!audioEnabled) {
|
||||
console.log('User interaction detected, audio enabled');
|
||||
audioEnabled = true;
|
||||
// Create and play a silent audio to unlock audio context
|
||||
const silentAudio = new Audio();
|
||||
silentAudio.volume = 0;
|
||||
silentAudio.play().catch(() => {});
|
||||
|
||||
// Start background audio in streaming mode
|
||||
if (isStreamingMode) {
|
||||
startBackgroundAudio();
|
||||
}
|
||||
|
||||
// Remove the listener after first interaction
|
||||
document.removeEventListener('click', enableAudio);
|
||||
document.removeEventListener('keydown', enableAudio);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', enableAudio);
|
||||
document.addEventListener('keydown', enableAudio);
|
||||
|
||||
initializeBackgroundAudio()
|
||||
|
||||
|
||||
// Initialize standings board
|
||||
initStandingsBoard();
|
||||
// TODO: Re-add standinds board when it has an actual use, and not stale data
|
||||
//
|
||||
//initStandingsBoard();
|
||||
|
||||
|
||||
// Load coordinate data, then build the map
|
||||
gameState.loadBoardState().then(() => {
|
||||
initMap(gameState.scene).then(() => {
|
||||
// Update info panel with initial power information
|
||||
logger.updateInfoPanel();
|
||||
updateLeaderboard();
|
||||
|
||||
// Initialize rotating display
|
||||
initRotatingDisplay('leaderboard');
|
||||
|
||||
// Only show standings board at startup if no game is loaded
|
||||
if (!gameState.gameData || !gameState.gameData.phases || gameState.gameData.phases.length === 0) {
|
||||
showStandingsBoard();
|
||||
}
|
||||
initRotatingDisplay();
|
||||
|
||||
gameState.cameraPanAnim = createCameraPan()
|
||||
|
||||
// Load default game file if in debug mode
|
||||
if (config.isDebugMode || isStreamingMode) {
|
||||
gameState.loadGameFile(0);
|
||||
gameState.loadGameFile(0);
|
||||
|
||||
// Initialize info panel
|
||||
logger.updateInfoPanel();
|
||||
}
|
||||
|
||||
// Initialize debug menu if in debug mode
|
||||
if (config.isDebugMode) {
|
||||
debugMenuInstance.show();
|
||||
}
|
||||
if (isStreamingMode) {
|
||||
startBackgroundAudio()
|
||||
setTimeout(() => {
|
||||
togglePlayback();
|
||||
// Try to start background audio when auto-starting
|
||||
startBackgroundAudio();
|
||||
}, 10000) // Increased delay to 10 seconds for Chrome to fully load in Docker
|
||||
togglePlayback()
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}).catch(err => {
|
||||
|
|
@ -102,8 +67,7 @@ function initScene() {
|
|||
window.addEventListener('resize', onWindowResize);
|
||||
|
||||
// Kick off animation loop
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
function createCameraPan(): Group {
|
||||
|
|
@ -145,24 +109,14 @@ function createCameraPan(): Group {
|
|||
* Main animation loop that runs continuously
|
||||
* Handles camera movement, animations, and game state transitions
|
||||
*/
|
||||
let lastTime = 0;
|
||||
function animate(currentTime: number = 0) {
|
||||
// Calculate delta time in seconds
|
||||
let deltaTime = lastTime ? (currentTime - lastTime) / 1000 : 0;
|
||||
lastTime = currentTime;
|
||||
|
||||
// Clamp delta time to prevent animation jumps when tab loses focus
|
||||
deltaTime = Math.min(deltaTime, config.animation.maxDeltaTime);
|
||||
|
||||
// Update global timing in gameState
|
||||
gameState.deltaTime = deltaTime;
|
||||
gameState.globalTime = currentTime / 1000; // Store in seconds
|
||||
function animate() {
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
if (gameState.isPlaying) {
|
||||
// Update the camera angle with delta time
|
||||
// Pass currentTime to update() to fix the Tween.js bug properly
|
||||
gameState.cameraPanAnim.update(currentTime);
|
||||
// Update the camera angle
|
||||
// FIXME: This has to call the update functino twice inorder to avoid a bug in Tween.js, see here https://github.com/tweenjs/tween.js/issues/677
|
||||
gameState.cameraPanAnim.update();
|
||||
gameState.cameraPanAnim.update();
|
||||
|
||||
} else {
|
||||
// Manual camera controls when not in playback mode
|
||||
|
|
@ -180,8 +134,8 @@ function animate(currentTime: number = 0) {
|
|||
console.log("All unit animations have completed");
|
||||
}
|
||||
|
||||
// Call update on each active animation with current time
|
||||
gameState.unitAnimations.forEach((anim) => anim.update(currentTime))
|
||||
// Call update on each active animation
|
||||
gameState.unitAnimations.forEach((anim) => anim.update())
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -191,29 +145,33 @@ function animate(currentTime: number = 0) {
|
|||
console.log(`Scheduling next phase in ${config.effectivePlaybackSpeed}ms`);
|
||||
gameState.nextPhaseScheduled = true;
|
||||
gameState.playbackTimer = setTimeout(() => {
|
||||
advanceToNextPhase()
|
||||
try {
|
||||
advanceToNextPhase()
|
||||
} catch {
|
||||
// FIXME: This is a dumb patch for us not being able to find the unit we expect to find.
|
||||
// We should instead bee figuring out why units aren't where we expect them to be when the engine has said that is a valid move
|
||||
nextPhase()
|
||||
gameState.nextPhaseScheduled;
|
||||
}
|
||||
}, config.effectivePlaybackSpeed);
|
||||
}
|
||||
// Update any pulsing or wave animations on supply centers or units
|
||||
// In streaming mode, reduce animation frequency
|
||||
const isStreamingMode = import.meta.env.VITE_STREAMING_MODE === 'True' || import.meta.env.VITE_STREAMING_MODE === 'true';
|
||||
const frameSkip = isStreamingMode ? 2 : 1; // Skip every other frame in streaming
|
||||
|
||||
if (gameState.scene.userData.animatedObjects && (Math.floor(currentTime / 16.67) % frameSkip === 0)) {
|
||||
if (gameState.scene.userData.animatedObjects) {
|
||||
gameState.scene.userData.animatedObjects.forEach(obj => {
|
||||
if (obj.userData.pulseAnimation) {
|
||||
const anim = obj.userData.pulseAnimation;
|
||||
// Use delta time for consistent animation speed regardless of frame rate
|
||||
anim.time += anim.speed * deltaTime * frameSkip; // Compensate for skipped frames
|
||||
anim.time += anim.speed;
|
||||
if (obj.userData.glowMesh) {
|
||||
const pulseValue = sineWave(config.animation.supplyPulseFrequency, anim.time, anim.intensity, 0.5);
|
||||
const pulseValue = Math.sin(anim.time) * anim.intensity + 0.5;
|
||||
obj.userData.glowMesh.material.opacity = 0.2 + (pulseValue * 0.3);
|
||||
const scale = 1 + (pulseValue * 0.1);
|
||||
obj.userData.glowMesh.scale.set(scale, scale, scale);
|
||||
obj.userData.glowMesh.scale.set(
|
||||
1 + (pulseValue * 0.1),
|
||||
1 + (pulseValue * 0.1),
|
||||
1 + (pulseValue * 0.1)
|
||||
);
|
||||
}
|
||||
// Subtle bobbing up/down - reduce in streaming mode
|
||||
const bobAmount = isStreamingMode ? 0.25 : 0.5;
|
||||
obj.position.y = 2 + sineWave(config.animation.supplyPulseFrequency, anim.time, bobAmount);
|
||||
// Subtle bobbing up/down
|
||||
obj.position.y = 2 + Math.sin(anim.time) * 0.5;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -244,7 +202,6 @@ fileInput.addEventListener('change', e => {
|
|||
if (file) {
|
||||
loadGameBtnFunction(file);
|
||||
// Explicitly hide standings board after loading game
|
||||
hideStandingsBoard();
|
||||
// Update rotating display and relationship popup with game data
|
||||
if (gameState.gameData) {
|
||||
updateRotatingDisplay(gameState.gameData, gameState.phaseIndex, gameState.currentPower);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { gameState } from "./gameState";
|
|||
import { logger } from "./logger";
|
||||
import { updatePhaseDisplay, playBtn, prevBtn, nextBtn } from "./domElements";
|
||||
import { initUnits } from "./units/create";
|
||||
import { updateSupplyCenterOwnership, updateLeaderboard, updateMapOwnership as _updateMapOwnership, updateMapOwnership } from "./map/state";
|
||||
import { updateSupplyCenterOwnership, updateMapOwnership as _updateMapOwnership, updateMapOwnership } from "./map/state";
|
||||
import { updateChatWindows, addToNewsBanner } from "./domElements/chatWindows";
|
||||
import { createAnimationsForNextPhase } from "./units/animate";
|
||||
import { speakSummary } from "./speech";
|
||||
|
|
@ -11,6 +11,7 @@ import { debugMenuInstance } from "./debug/debugMenu";
|
|||
import { showTwoPowerConversation, closeTwoPowerConversation } from "./components/twoPowerConversation";
|
||||
import { closeVictoryModal, showVictoryModal } from "./components/victoryModal";
|
||||
import { notifyPhaseChange } from "./webhooks/phaseNotifier";
|
||||
import { updateLeaderboard } from "./components/leaderboard";
|
||||
import { updateRotatingDisplay } from "./components/rotatingDisplay";
|
||||
|
||||
const MOMENT_THRESHOLD = 8.0
|
||||
|
|
@ -199,7 +200,7 @@ export function displayPhase(skipMessages = false) {
|
|||
|
||||
|
||||
// Update UI elements with smooth transitions
|
||||
updateLeaderboard(currentPhase);
|
||||
updateRotatingDisplay(gameState.gameData, gameState.phaseIndex, gameState.currentPower);
|
||||
_updateMapOwnership();
|
||||
|
||||
// Add phase info to news banner if not already there
|
||||
|
|
@ -210,8 +211,8 @@ export function displayPhase(skipMessages = false) {
|
|||
const phaseInfo = `Phase: ${currentPhase.name}\nSCs: ${currentPhase.state?.centers ? JSON.stringify(currentPhase.state.centers) : 'None'}\nUnits: ${currentPhase.state?.units ? JSON.stringify(currentPhase.state.units) : 'None'}`;
|
||||
console.log(phaseInfo); // Use console.log instead of logger.log
|
||||
|
||||
// Update info panel with power information
|
||||
logger.updateInfoPanel();
|
||||
// Update leaderboard with power information
|
||||
updateLeaderboard();
|
||||
|
||||
// Show messages with animation or immediately based on skipMessages flag
|
||||
if (!skipMessages) {
|
||||
|
|
@ -288,9 +289,7 @@ export function advanceToNextPhase() {
|
|||
console.log(`Processing phase transition for ${currentPhase.name}`);
|
||||
}
|
||||
|
||||
// In streaming mode, add extra delay before speech to ensure phase is fully displayed
|
||||
const isStreamingMode = import.meta.env.VITE_STREAMING_MODE === 'True' || import.meta.env.VITE_STREAMING_MODE === 'true';
|
||||
const speechDelay = isStreamingMode ? 2000 : 0; // 2 second delay in streaming mode
|
||||
const speechDelay = 2000
|
||||
|
||||
// First show summary if available
|
||||
if (currentPhase.summary && currentPhase.summary.trim() !== '') {
|
||||
|
|
|
|||
|
|
@ -99,10 +99,10 @@
|
|||
}
|
||||
|
||||
/* -----------------
|
||||
Info Panel
|
||||
Leaderboard
|
||||
(lower-right)
|
||||
----------------- */
|
||||
#info-panel {
|
||||
#leaderboard {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
Leaderboard
|
||||
(lower-left)
|
||||
----------------- */
|
||||
#leaderboard {
|
||||
#rotating-display {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 10px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue