Updating phase state control to allow for reinit of units at any phase.

I wanted to be able to cause the turns to either progress, or to set the
units in their specific spots. This way if I skip ahead a bunch of turns
for one reason or another, I don't have to worry about the units on the
  board being all messed up.
This commit is contained in:
Tyler Marques 2025-05-27 13:35:19 -07:00
parent 9a43be9b9c
commit 3642e391bc
No known key found for this signature in database
GPG key ID: CB99EDCF41D3016F
7 changed files with 101 additions and 56 deletions

View file

@ -26,4 +26,4 @@ dist-ssr
# AI things # AI things
.claude/ .claude/
./public/games/ public/games/

View file

@ -13,7 +13,7 @@ import { Tween, Group as TweenGroup } from "@tweenjs/tween.js";
import { hideStandingsBoard, } from "./domElements/standingsBoard"; import { hideStandingsBoard, } from "./domElements/standingsBoard";
import { MomentsDataSchema, MomentsDataSchemaType } from "./types/moments"; import { MomentsDataSchema, MomentsDataSchemaType } from "./types/moments";
//FIXME: This whole file is a mess. Need to organkze and format //FIXME: This whole file is a mess. Need to organize and format
enum AvailableMaps { enum AvailableMaps {
STANDARD = "standard" STANDARD = "standard"

View file

@ -7,7 +7,7 @@ import { loadBtn, prevBtn, nextBtn, speedSelector, fileInput, playBtn, mapView,
import { updateChatWindows } from "./domElements/chatWindows"; import { updateChatWindows } from "./domElements/chatWindows";
import { initStandingsBoard, hideStandingsBoard, showStandingsBoard } from "./domElements/standingsBoard"; import { initStandingsBoard, hideStandingsBoard, showStandingsBoard } from "./domElements/standingsBoard";
import { initRelationshipPopup, hideRelationshipPopup, updateRelationshipPopup } from "./domElements/relationshipPopup"; import { initRelationshipPopup, hideRelationshipPopup, updateRelationshipPopup } from "./domElements/relationshipPopup";
import { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase } from "./phase"; import { displayPhaseWithAnimation, advanceToNextPhase, resetToPhase, nextPhase, previousPhase } from "./phase";
import { config } from "./config"; import { config } from "./config";
import { Tween, Group, Easing } from "@tweenjs/tween.js"; import { Tween, Group, Easing } from "@tweenjs/tween.js";
import { initRotatingDisplay, updateRotatingDisplay } from "./components/rotatingDisplay"; import { initRotatingDisplay, updateRotatingDisplay } from "./components/rotatingDisplay";
@ -23,15 +23,8 @@ let prevPos
// --- INITIALIZE SCENE --- // --- INITIALIZE SCENE ---
function initScene() { function initScene() {
gameState.initScene() gameState.createThreeScene()
// Lighting (keep it simple)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
gameState.scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(300, 400, 300);
gameState.scene.add(dirLight);
// Initialize standings board // Initialize standings board
initStandingsBoard(); initStandingsBoard();
@ -267,12 +260,10 @@ fileInput.addEventListener('change', e => {
}); });
prevBtn.addEventListener('click', () => { prevBtn.addEventListener('click', () => {
if (gameState.phaseIndex > 0) { previousPhase()
resetToPhase(gameState.phaseIndex - 1)
}
}); });
nextBtn.addEventListener('click', () => { nextBtn.addEventListener('click', () => {
advanceToNextPhase() nextPhase()
}); });
playBtn.addEventListener('click', togglePlayback); playBtn.addEventListener('click', togglePlayback);

View file

@ -11,6 +11,42 @@ import { config } from "./config";
function _setPhase(phaseIndex: number) {
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 (Math.abs(phaseIndex - gameState.phaseIndex) > 1) {
// We're moving more than one Phase, to do so clear the board and reInit the units on the correct phase
gameState.unitAnimations = [];
initUnits(phaseIndex)
updateMapOwnership()
} else {
}
// Finally, update the gameState with the current phaseIndex
gameState.phaseIndex = phaseIndex
// If we're at the end of the game, don't attempt to animate.
if (phaseIndex === gameLength - 1) {
} else {
displayPhase()
}
}
export function nextPhase() {
_setPhase(gameState.phaseIndex + 1)
}
export function previousPhase() {
_setPhase(gameState.phaseIndex - 1)
}
/** /**
* Unified function to display a phase with proper transitions * Unified function to display a phase with proper transitions
* Handles both initial display and animated transitions between phases * Handles both initial display and animated transitions between phases
@ -86,8 +122,8 @@ export function displayPhase(skipMessages = false) {
* Used when first loading a game * Used when first loading a game
*/ */
export function displayInitialPhase() { export function displayInitialPhase() {
initUnits();
gameState.phaseIndex = 0; gameState.phaseIndex = 0;
initUnits(0);
displayPhase(true); displayPhase(true);
} }
@ -99,17 +135,6 @@ export function displayPhaseWithAnimation() {
displayPhase(false); displayPhase(false);
} }
// Explicityly sets the phase to a given index,
// Removes and recreates all units.
export function resetToPhase(index: number) {
gameState.phaseIndex = index
gameState.unitAnimations = [];
gameState.unitMeshes.map(unitMesh => gameState.scene.remove(unitMesh))
updateMapOwnership()
initUnits()
}
/** /**
* Advances to the next phase in the game sequence * Advances to the next phase in the game sequence
@ -178,7 +203,7 @@ function displayFinalPhase() {
// Get the final phase to determine the winner // Get the final phase to determine the winner
const finalPhase = gameState.gameData.phases[gameState.gameData.phases.length - 1]; const finalPhase = gameState.gameData.phases[gameState.gameData.phases.length - 1];
if (!finalPhase.state?.centers) { if (!finalPhase.state?.centers) {
logger.log("No supply center data available to determine winner"); logger.log("No supply center data available to determine winner");
return; return;
@ -187,7 +212,7 @@ function displayFinalPhase() {
// Find the power with the most supply centers // Find the power with the most supply centers
let winner = ''; let winner = '';
let maxCenters = 0; let maxCenters = 0;
for (const [power, centers] of Object.entries(finalPhase.state.centers)) { for (const [power, centers] of Object.entries(finalPhase.state.centers)) {
const centerCount = Array.isArray(centers) ? centers.length : 0; const centerCount = Array.isArray(centers) ? centers.length : 0;
if (centerCount > maxCenters) { if (centerCount > maxCenters) {
@ -199,13 +224,13 @@ function displayFinalPhase() {
// Display victory message // Display victory message
if (winner && maxCenters > 0) { if (winner && maxCenters > 0) {
const victoryMessage = `🏆 GAME OVER - ${winner} WINS with ${maxCenters} supply centers! 🏆`; const victoryMessage = `🏆 GAME OVER - ${winner} WINS with ${maxCenters} supply centers! 🏆`;
// Add victory message to news banner with dramatic styling // Add victory message to news banner with dramatic styling
addToNewsBanner(victoryMessage); addToNewsBanner(victoryMessage);
// Log the victory // Log the victory
logger.log(`Victory! ${winner} wins the game with ${maxCenters} supply centers.`); logger.log(`Victory! ${winner} wins the game with ${maxCenters} supply centers.`);
// Display final standings in console // Display final standings in console
const standings = Object.entries(finalPhase.state.centers) const standings = Object.entries(finalPhase.state.centers)
.map(([power, centers]) => ({ .map(([power, centers]) => ({
@ -213,7 +238,7 @@ function displayFinalPhase() {
centers: Array.isArray(centers) ? centers.length : 0 centers: Array.isArray(centers) ? centers.length : 0
})) }))
.sort((a, b) => b.centers - a.centers); .sort((a, b) => b.centers - a.centers);
console.log("Final Standings:"); console.log("Final Standings:");
standings.forEach((entry, index) => { standings.forEach((entry, index) => {
const medal = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : " "; const medal = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : " ";

View file

@ -21,7 +21,27 @@ const PhaseSchema = z.object({
messages: z.array(z.any()), messages: z.array(z.any()),
name: z.string(), name: z.string(),
orders: z.record(PowerENUMSchema, z.array(OrderFromString).nullable()), orders: z.record(PowerENUMSchema, z.array(OrderFromString).nullable()),
results: z.record(z.string(), z.array(z.any())), results: z.record(z.string(), z.array(z.any())).transform((originalResults) => {
// Transform results from {"A BUD": [results]} to {A: {"BUD": [results]}, F: {"BUD": [results]}}
const transformed: { A: Record<string, any[]>, F: Record<string, any[]> } = { A: {}, F: {} };
for (const [key, value] of Object.entries(originalResults)) {
const tokens = key.split(' ');
if (tokens.length >= 2) {
const unitType = tokens[0]; // "A" or "F"
const province = tokens[1].split('/')[0]; // Remove coast specification if present
if (unitType === 'A' || unitType === 'F') {
if (!transformed[unitType as 'A' | 'F']) {
transformed[unitType as 'A' | 'F'] = {};
}
transformed[unitType as 'A' | 'F'][province] = value;
}
}
}
return transformed;
}),
state: z.object({ state: z.object({
units: z.record(PowerENUMSchema, z.array(z.string())), units: z.record(PowerENUMSchema, z.array(z.string())),
centers: z.record(PowerENUMSchema, z.array(ProvinceENUMSchema)), centers: z.record(PowerENUMSchema, z.array(ProvinceENUMSchema)),

View file

@ -94,25 +94,26 @@ export function createAnimationsForNextPhase() {
} }
for (const order of orders) { for (const order of orders) {
// Check if unit bounced // Check if unit bounced
let lastPhaseResultMatches = Object.entries(previousPhase.results).filter(([key, _]) => { // With new format: {A: {"BUD": [results]}, F: {"BUD": [results]}}
return key.split(" ")[1] == order.unit.origin const unitType = order.unit.type;
}).map(val => { const unitOrigin = order.unit.origin;
// in the form "A BER" (unitType origin)
let orderSplit = val[0].split(" ") let result = undefined;
return { origin: orderSplit[1], unitType: orderSplit[0], result: val[1][0] } if (previousPhase.results && previousPhase.results[unitType] && previousPhase.results[unitType][unitOrigin]) {
}) const resultArray = previousPhase.results[unitType][unitOrigin];
// This should always exist. If we don't have a match here, that means something went wrong with our order parsing result = resultArray.length > 0 ? resultArray[0] : null;
if (!lastPhaseResultMatches) {
throw new Error("No result present in current phase for previous phase order. Cannot continue")
} }
if (lastPhaseResultMatches.length > 1) {
throw new Error("Multiple matching results from last phase. Should only ever be 1.") if (result === undefined) {
throw new Error(`No result present in current phase for previous phase order: ${unitType} ${unitOrigin}. Cannot continue`);
} }
if (lastPhaseResultMatches[0].result === "bounce") {
if (result === "bounce") {
order.type = "bounce" order.type = "bounce"
} }
// If the result is void, that means the move was not valid? // If the result is void, that means the move was not valid?
if (lastPhaseResultMatches[0].result === "void") continue; if (result === "void") continue;
let unitIndex = -1 let unitIndex = -1
switch (order.type) { switch (order.type) {
@ -163,10 +164,6 @@ export function createAnimationsForNextPhase() {
case "support": case "support":
break break
default:
break;
} }
} }
} }

View file

@ -144,11 +144,17 @@ export function createUnitMesh(unitData: UnitData): THREE.Group {
return group; return group;
} }
function _removeUnitsFromBoard() {
// Creates the units for the current gameState.phaseIndex. gameState.unitMeshes.map((mesh) => gameState.scene.remove(mesh))
export function initUnits() { }
createSupplyCenters()
for (const [power, unitArr] of Object.entries(gameState.gameData.phases[gameState.phaseIndex].state.units)) { /*
* Given a phaseIndex, Add the units for that phase to the board, in the province specified in the game.json file.
*/
function _addUnitsToBoard(phaseIndex: number) {
_removeUnitsFromBoard()
for (const [power, unitArr] of Object.entries(gameState.gameData.phases[phaseIndex].state.units)) {
unitArr.forEach(unitStr => { unitArr.forEach(unitStr => {
const match = unitStr.match(/^([AF])\s+(.+)$/); const match = unitStr.match(/^([AF])\s+(.+)$/);
if (match) { if (match) {
@ -163,3 +169,9 @@ export function initUnits() {
}); });
} }
} }
// Creates the units for the current gameState.phaseIndex.
export function initUnits(phaseIndex: number) {
if (phaseIndex === undefined) throw new Error("Cannot pass undefined phaseIndex");
createSupplyCenters()
_addUnitsToBoard(phaseIndex)
}