WIP: Working on fixing moving from game to game

This commit is contained in:
Tyler Marques 2025-06-09 09:39:47 -07:00
parent fd00131af2
commit 6f9c050741
No known key found for this signature in database
GPG key ID: CB99EDCF41D3016F
8 changed files with 49 additions and 358 deletions

View file

@ -1,293 +0,0 @@
import { gameState } from '../gameState';
interface EndScreenModalOptions {
totalGamesPlayed: number;
onRestart?: () => void;
}
let endScreenModalOverlay: HTMLElement | null = null;
/**
* Shows an end screen modal when all games have been completed
* @param options Configuration for the end screen modal
*/
export function showEndScreenModal(options: EndScreenModalOptions): void {
const { totalGamesPlayed, onRestart } = options;
// Close any existing modal
closeEndScreenModal();
// Create overlay
endScreenModalOverlay = createEndScreenOverlay();
// Create modal container
const modalContainer = createEndScreenContainer(totalGamesPlayed);
// Add close button if not in playing mode
if (!gameState.isPlaying) {
const closeButton = createCloseButton();
modalContainer.appendChild(closeButton);
}
// Add to overlay
endScreenModalOverlay.appendChild(modalContainer);
document.body.appendChild(endScreenModalOverlay);
// Set up event listeners
setupEventListeners(onRestart);
// Auto-restart after 5 seconds
setTimeout(() => {
closeEndScreenModal();
onRestart?.();
}, 5000);
}
/**
* Closes the end screen modal
*/
export function closeEndScreenModal(): void {
if (endScreenModalOverlay) {
endScreenModalOverlay.classList.add('fade-out');
setTimeout(() => {
if (endScreenModalOverlay?.parentNode) {
endScreenModalOverlay.parentNode.removeChild(endScreenModalOverlay);
}
endScreenModalOverlay = null;
}, 300);
}
}
/**
* Creates the main overlay element
*/
function createEndScreenOverlay(): HTMLElement {
const overlay = document.createElement('div');
overlay.className = 'end-screen-modal-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
opacity: 0;
transition: opacity 0.5s ease;
`;
// Add fade-out class styles
const style = document.createElement('style');
style.textContent = `
.end-screen-modal-overlay.fade-out {
opacity: 0 !important;
transition: opacity 0.3s ease;
}
`;
document.head.appendChild(style);
// Trigger fade in
setTimeout(() => overlay.style.opacity = '1', 10);
return overlay;
}
/**
* Creates the main end screen container
*/
function createEndScreenContainer(totalGamesPlayed: number): HTMLElement {
const container = document.createElement('div');
container.className = 'end-screen-modal-container';
container.style.cssText = `
background: radial-gradient(ellipse at center, #2a1f1f 0%, #1a1015 100%);
border: 5px solid #8b7355;
border-radius: 12px;
box-shadow: 0 0 40px rgba(255, 215, 0, 0.6);
width: 90%;
max-width: 600px;
position: relative;
padding: 40px;
text-align: center;
font-family: "Book Antiqua", Palatino, serif;
color: #f0e6d2;
animation: endScreenGlow 3s ease-in-out infinite alternate;
`;
// Add glow animation
const style = document.createElement('style');
style.textContent = `
@keyframes endScreenGlow {
0% { box-shadow: 0 0 40px rgba(255, 215, 0, 0.6); }
100% { box-shadow: 0 0 60px rgba(255, 215, 0, 0.8); }
}
`;
document.head.appendChild(style);
// Crown/completion icon
const crownElement = document.createElement('div');
crownElement.textContent = '👑';
crownElement.style.cssText = `
font-size: 64px;
margin-bottom: 20px;
animation: float 3s ease-in-out infinite;
`;
// Add float animation
const floatStyle = document.createElement('style');
floatStyle.textContent = `
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
`;
document.head.appendChild(floatStyle);
// Title
const titleElement = document.createElement('h1');
titleElement.textContent = 'SERIES COMPLETE!';
titleElement.style.cssText = `
margin: 0 0 20px 0;
color: #ffd700;
font-size: 42px;
font-weight: bold;
text-shadow: 3px 3px 6px rgba(0,0,0,0.7);
letter-spacing: 2px;
`;
// Completion message
const messageElement = document.createElement('h2');
messageElement.textContent = `All ${totalGamesPlayed} games completed!`;
messageElement.style.cssText = `
margin: 0 0 30px 0;
color: #f0e6d2;
font-size: 24px;
font-weight: normal;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
`;
// Restart countdown
const countdownElement = document.createElement('div');
countdownElement.style.cssText = `
background: linear-gradient(90deg, #4a4a4a 0%, #2a2a2a 100%);
color: #ffd700;
padding: 15px 25px;
border-radius: 25px;
display: inline-block;
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
border: 2px solid #8b7355;
box-shadow: 0 4px 8px rgba(0,0,0,0.4);
`;
// Animate countdown
let countdown = 5;
countdownElement.textContent = `Restarting series in ${countdown} seconds...`;
const countdownInterval = setInterval(() => {
countdown--;
if (countdown > 0) {
countdownElement.textContent = `Restarting series in ${countdown} seconds...`;
} else {
countdownElement.textContent = 'Restarting now...';
clearInterval(countdownInterval);
}
}, 1000);
// Thank you message
const thankYouElement = document.createElement('div');
thankYouElement.textContent = 'Thank you for watching the AI Diplomacy series!';
thankYouElement.style.cssText = `
color: #c4b998;
font-size: 16px;
font-style: italic;
margin-top: 20px;
`;
// Assemble container
container.appendChild(crownElement);
container.appendChild(titleElement);
container.appendChild(messageElement);
container.appendChild(countdownElement);
container.appendChild(thankYouElement);
return container;
}
/**
* Creates a close button
*/
function createCloseButton(): HTMLElement {
const button = document.createElement('button');
button.textContent = '×';
button.className = 'end-screen-close-button';
button.style.cssText = `
position: absolute;
top: 15px;
right: 20px;
background: none;
border: none;
font-size: 40px;
color: #8b7355;
cursor: pointer;
padding: 0;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
`;
button.addEventListener('mouseenter', () => {
button.style.color = '#ffd700';
button.style.transform = 'scale(1.1)';
button.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
});
button.addEventListener('mouseleave', () => {
button.style.color = '#8b7355';
button.style.transform = 'scale(1)';
button.style.backgroundColor = 'transparent';
});
return button;
}
/**
* Sets up event listeners for the end screen modal
*/
function setupEventListeners(onRestart?: () => void): void {
if (!endScreenModalOverlay) return;
const closeButton = endScreenModalOverlay.querySelector('.end-screen-close-button');
const handleClose = () => {
closeEndScreenModal();
onRestart?.();
};
// Close button click (only if not in playing mode)
if (!gameState.isPlaying) {
closeButton?.addEventListener('click', handleClose);
// Escape key
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
handleClose();
document.removeEventListener('keydown', handleKeydown);
}
};
document.addEventListener('keydown', handleKeydown);
// Click outside to close
endScreenModalOverlay.addEventListener('click', (e) => {
if (e.target === endScreenModalOverlay) {
handleClose();
}
});
}
}

View file

@ -79,9 +79,9 @@ function rotateToNextDisplay(): void {
// Determine next display type
switch (currentDisplayType) {
// case DisplayType.SC_HISTORY_CHART:
// currentDisplayType = DisplayType.RELATIONSHIP_HISTORY_CHART;
// break;
case DisplayType.SC_HISTORY_CHART:
currentDisplayType = DisplayType.RELATIONSHIP_HISTORY_CHART;
break;
case DisplayType.RELATIONSHIP_HISTORY_CHART:
currentDisplayType = DisplayType.SC_HISTORY_CHART;
break;
@ -155,6 +155,9 @@ function renderSCHistoryChartView(
container: HTMLElement,
gameData: GameSchemaType
): void {
// TODO: This likely needs to not be a custom rendered svg. There are plenty of charting libraries to use to do this instead.
//
// Create header
const header = document.createElement('div');
header.innerHTML = `<strong>Supply Center History</strong>`;

View file

@ -40,13 +40,6 @@ export function showVictoryModal(options: VictoryModalOptions): void {
// Set up event listeners
setupEventListeners(onClose);
// Auto-dismiss in playing mode after delay
if (gameState.isPlaying) {
setTimeout(() => {
closeVictoryModal();
onClose?.();
}, 5000); // 5 second display in playing mode
}
}
/**
@ -55,12 +48,10 @@ export function showVictoryModal(options: VictoryModalOptions): void {
export function closeVictoryModal(): void {
if (victoryModalOverlay) {
victoryModalOverlay.classList.add('fade-out');
setTimeout(() => {
if (victoryModalOverlay?.parentNode) {
victoryModalOverlay.parentNode.removeChild(victoryModalOverlay);
}
victoryModalOverlay = null;
}, 300);
if (victoryModalOverlay?.parentNode) {
victoryModalOverlay.parentNode.removeChild(victoryModalOverlay);
}
victoryModalOverlay = null;
}
}
@ -249,7 +240,7 @@ function createStandingsElement(finalStandings: Array<{ power: string; centers:
const standingItem = document.createElement('div');
const medal = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : `${index + 1}.`;
const displayName = getPowerDisplayName(entry.power as PowerENUM);
standingItem.innerHTML = `
<span style="font-size: 18px; margin-right: 8px;">${medal}</span>
<span class="power-${entry.power.toLowerCase()}" style="font-weight: bold;">${displayName}</span>
@ -346,4 +337,4 @@ function setupEventListeners(onClose?: () => void): void {
}
});
}
}
}

View file

@ -4,7 +4,7 @@
export const config = {
// Default speed in milliseconds for animations and transitions
playbackSpeed: 500,
victoryModalDisplayMs: 10000,
// Streaming mode specific timing
get streamingPlaybackSpeed(): number {
const isStreamingMode = import.meta.env.MODE === 'production'

View file

@ -2,6 +2,8 @@ import * as THREE from "three"
import { updateRotatingDisplay } from "./components/rotatingDisplay";
import { type CoordinateData, CoordinateDataSchema, PowerENUM } from "./types/map"
import type { GameSchemaType } from "./types/gameState";
import { debugMenuInstance } from "./debug/debugMenu.ts"
import { config } from "./config.ts"
import { GameSchema } from "./types/gameState";
import { prevBtn, nextBtn, playBtn, speedSelector, mapView, updateGameIdDisplay } from "./domElements";
import { createChatWindows } from "./domElements/chatWindows";
@ -11,6 +13,7 @@ import { displayInitialPhase, togglePlayback } from "./phase";
import { Tween, Group as TweenGroup } from "@tweenjs/tween.js";
import { MomentsDataSchema, Moment, NormalizedMomentsData } from "./types/moments";
import { updateLeaderboard } from "./components/leaderboard";
import { closeVictoryModal } from "./components/victoryModal.ts";
//FIXME: This whole file is a mess. Need to organize and format
@ -123,7 +126,7 @@ class GameState {
this.phaseIndex = 0
this.boardName = boardName
this.currentPower = null;
this.gameId = 12
this.gameId = 16
this.momentsData = null; // Initialize as null, will be loaded later
// State locks
@ -226,6 +229,7 @@ class GameState {
// Update game ID display
updateGameIdDisplay();
})
resolve()
} else {
@ -300,16 +304,19 @@ class GameState {
/*
* Loads the next game in the order, reseting the board and gameState
*/
loadNextGame = () => {
loadNextGame = (setPlayback: boolean = false) => {
let gameId = this.gameId + 1
let contPlaying = false
if (this.isPlaying) {
if (setPlayback || this.isPlaying) {
contPlaying = true
}
this.loadGameFile(gameId).then(() => {
gameState.gameId = gameId
if (contPlaying) {
togglePlayback(true)
setTimeout(() => {
togglePlayback(true)
}, config.victoryModalDisplayMs)
}
}).catch(() => {
console.warn("caught error trying to advance game. Setting gameId to 0 and restarting...")
@ -317,7 +324,7 @@ class GameState {
if (contPlaying) {
togglePlayback(true)
}
})
}).finally(closeVictoryModal)
}
@ -348,6 +355,9 @@ class GameState {
this.gameId = gameId
updateGameIdDisplay();
updateLeaderboard();
if (config.isDebugMode) {
debugMenuInstance.updateTools()
}
resolve()
}
})

View file

@ -2,18 +2,15 @@ import * as THREE from "three";
import "./style.css"
import { initMap } from "./map/create";
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 { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase, nextPhase, previousPhase, togglePlayback, _setPhase } from "./phase";
import { config } from "./config";
import { Tween, Group, Easing } from "@tweenjs/tween.js";
import { initRotatingDisplay, updateRotatingDisplay } from "./components/rotatingDisplay";
import { closeTwoPowerConversation, showTwoPowerConversation } from "./components/twoPowerConversation";
import { PowerENUM } from "./types/map";
import { initRotatingDisplay, } from "./components/rotatingDisplay";
import { debugMenuInstance } from "./debug/debugMenu";
import { initializeBackgroundAudio, startBackgroundAudio } from "./backgroundAudio";
import { updateLeaderboard } from "./components/leaderboard";
import { _setPhase } from "./phase";
import { togglePlayback } from "./phase";
//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
@ -161,7 +158,6 @@ function animate() {
// 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);
}

View file

@ -20,7 +20,14 @@ const MOMENT_DISPLAY_TIMEOUT_MS = config.isDebugMode || config.isInstantMode ? 1
// FIXME: Going to previous phases is borked. Units do not animate properly, map doesn't update.
export function _setPhase(phaseIndex: number) {
console.log(`[Phase] _setPhase called with index: ${phaseIndex}`);
if (phaseIndex < 0) {
throw new Error(`Provided invalid phaseIndex, cannot setPhase to ${phaseIndex} - game has ${gameState.gameData.phases.length} phases`)
}
if (phaseIndex >= gameState.gameData.phases.length - 1) {
gameState.phaseIndex = gameState.gameData.phases.length - 1
displayFinalPhase()
return
}
// Store the old phase index at the very beginning
const oldPhaseIndex = gameState.phaseIndex;
@ -29,10 +36,9 @@ export function _setPhase(phaseIndex: number) {
debugMenuInstance.updateTools()
}
const gameLength = gameState.gameData.phases.length
// Validate that the phaseIndex is within the bounds of the game length.
if (phaseIndex >= gameLength || phaseIndex < 0) {
throw new Error(`Provided invalid phaseIndex, cannot setPhase to ${phaseIndex} - game has ${gameState.gameData.phases.length} phases`)
}
if (phaseIndex - gameState.phaseIndex != 1) {
// We're moving more than one Phase forward, or any number of phases backward, to do so clear the board and reInit the units on the correct phase
gameState.unitAnimations = [];
@ -56,12 +62,7 @@ export function _setPhase(phaseIndex: number) {
console.log(`Moving to phase ${gameState.gameData.phases[gameState.phaseIndex].name}`);
}
if (phaseIndex === gameLength - 1) {
displayFinalPhase()
} else {
displayPhase()
}
gameState.nextPhaseScheduled = false;
displayPhase()
}
// Finally, update the gameState with the current phaseIndex
@ -235,6 +236,8 @@ export function displayPhase(skipMessages = false) {
logger.log("No animations for this phase transition");
gameState.messagesPlaying = false;
}
gameState.nextPhaseScheduled = false;
}
/**
@ -242,7 +245,6 @@ export function displayPhase(skipMessages = false) {
* Used when first loading a game
*/
export function displayInitialPhase() {
gameState.phaseIndex = 0;
initUnits(0);
displayPhase(true);
}
@ -316,8 +318,6 @@ export function advanceToNextPhase() {
nextPhase();
}
// Reset the nextPhaseScheduled flag to allow scheduling the next phase
gameState.nextPhaseScheduled = false;
}
function displayFinalPhase() {
@ -360,27 +360,11 @@ function displayFinalPhase() {
winner,
maxCenters,
finalStandings,
onClose: () => {
// Only proceed to next game if in playing mode
if (gameState.isPlaying) {
gameState.loadNextGame();
}
}
});
//setTimeout(closeVictoryModal, 10000)
setTimeout(() => {
gameState.loadNextGame(true)
}, config.victoryModalDisplayMs)
// Log the victory
logger.log(`Victory! ${winner} wins the game with ${maxCenters} supply centers.`);
// Display final standings in console
console.log("Final Standings:");
finalStandings.forEach((entry, index) => {
const medal = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : " ";
console.log(`${medal} ${entry.power}: ${entry.centers} centers`);
});
// Show victory in info panel
logger.updateInfoPanel(`🏆 ${winner} VICTORIOUS! 🏆\n\nFinal Score: ${maxCenters} supply centers\n\nCheck console for full standings.`);
} else {
logger.log("Could not determine game winner");
}

View file

@ -9,7 +9,7 @@
"DOM.Iterable"
],
// Turn the below off later, just for now we're not full TS yet
"noCheck": true,
"noCheck": false,
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",