From c54e0108014c8b93a672d43f175d2fccbf9730cd Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 25 Mar 2025 16:50:46 -0400 Subject: [PATCH] Improve captain data handling and UI state management --- src/assets/captains/burnham/config.json | 60 ++++ src/assets/captains/koloth/config.json | 60 ++++ src/assets/captains/picard/config.json | 60 ++++ src/assets/captains/sela/config.json | 60 ++++ src/assets/captains/shran/config.json | 60 ++++ src/assets/captains/sisko/config.json | 60 ++++ src/pages/index.astro | 405 ++++++++++++++++-------- src/utils/captainData.ts | 133 ++++++++ src/utils/storage.ts | 83 +++++ src/utils/types.ts | 31 ++ src/utils/ui.ts | 337 ++++++++++++++++++++ tsconfig.json | 15 +- 12 files changed, 1221 insertions(+), 143 deletions(-) create mode 100644 src/assets/captains/burnham/config.json create mode 100644 src/assets/captains/koloth/config.json create mode 100644 src/assets/captains/picard/config.json create mode 100644 src/assets/captains/sela/config.json create mode 100644 src/assets/captains/shran/config.json create mode 100644 src/assets/captains/sisko/config.json create mode 100644 src/utils/captainData.ts create mode 100644 src/utils/storage.ts create mode 100644 src/utils/types.ts create mode 100644 src/utils/ui.ts diff --git a/src/assets/captains/burnham/config.json b/src/assets/captains/burnham/config.json new file mode 100644 index 0000000..2a9bfba --- /dev/null +++ b/src/assets/captains/burnham/config.json @@ -0,0 +1,60 @@ +{ + "id": "burnham", + "name": "Burnham", + "image": "captain.jpg", + "specialtyThresholds": { + "research": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "influence": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "military": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + } + }, + "basicMission": { + "name": "Time Crystal Recovery", + "description": "Retrieve a valuable time crystal from a dangerous location.", + "points": 3 + }, + "advancedMissions": [ + { + "name": "Red Angel Investigation", + "description": "Investigate the mysterious Red Angel phenomenon.", + "points": 5 + }, + { + "name": "Control Protocol Override", + "description": "Override the Control AI's protocols to save the galaxy.", + "points": 8 + } + ] +} \ No newline at end of file diff --git a/src/assets/captains/koloth/config.json b/src/assets/captains/koloth/config.json new file mode 100644 index 0000000..6eff0cb --- /dev/null +++ b/src/assets/captains/koloth/config.json @@ -0,0 +1,60 @@ +{ + "id": "koloth", + "name": "Koloth", + "image": "captain.jpg", + "specialtyThresholds": { + "research": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "influence": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "military": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + } + }, + "basicMission": { + "name": "Klingon Honor", + "description": "Uphold Klingon honor in a challenging diplomatic situation.", + "points": 3 + }, + "advancedMissions": [ + { + "name": "House Koloth", + "description": "Strengthen the position of House Koloth in the Klingon Empire.", + "points": 5 + }, + { + "name": "Klingon Empire Conquest", + "description": "Lead a successful military campaign to expand Klingon territory.", + "points": 8 + } + ] +} \ No newline at end of file diff --git a/src/assets/captains/picard/config.json b/src/assets/captains/picard/config.json new file mode 100644 index 0000000..b8480fb --- /dev/null +++ b/src/assets/captains/picard/config.json @@ -0,0 +1,60 @@ +{ + "id": "picard", + "name": "Picard", + "image": "captain.jpg", + "specialtyThresholds": { + "research": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "influence": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "military": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + } + }, + "basicMission": { + "name": "Diplomatic Mission", + "description": "Lead a diplomatic mission to establish peaceful relations with a new species.", + "points": 2 + }, + "advancedMissions": [ + { + "name": "Scientific Discovery", + "description": "Make a groundbreaking scientific discovery that advances Federation knowledge.", + "points": 3 + }, + { + "name": "First Contact Protocol", + "description": "Successfully execute first contact protocols with a technologically advanced species.", + "points": 4 + } + ] +} \ No newline at end of file diff --git a/src/assets/captains/sela/config.json b/src/assets/captains/sela/config.json new file mode 100644 index 0000000..193c0ea --- /dev/null +++ b/src/assets/captains/sela/config.json @@ -0,0 +1,60 @@ +{ + "id": "sela", + "name": "Sela", + "image": "captain.jpg", + "specialtyThresholds": { + "research": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "influence": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "military": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + } + }, + "basicMission": { + "name": "Romulan Infiltration", + "description": "Lead a covert operation to infiltrate a strategic Federation outpost.", + "points": 3 + }, + "advancedMissions": [ + { + "name": "Tal Shiar Operation", + "description": "Execute a complex Tal Shiar intelligence gathering mission.", + "points": 5 + }, + { + "name": "Romulan Empire Expansion", + "description": "Expand Romulan influence into a new sector of space.", + "points": 8 + } + ] +} \ No newline at end of file diff --git a/src/assets/captains/shran/config.json b/src/assets/captains/shran/config.json new file mode 100644 index 0000000..7972dc6 --- /dev/null +++ b/src/assets/captains/shran/config.json @@ -0,0 +1,60 @@ +{ + "id": "shran", + "name": "Shran", + "image": "captain.jpg", + "specialtyThresholds": { + "research": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "influence": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "military": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + } + }, + "basicMission": { + "name": "Andorian Defense", + "description": "Defend Andorian interests against external threats.", + "points": 3 + }, + "advancedMissions": [ + { + "name": "Imperial Guard Alliance", + "description": "Form a strategic alliance with the Imperial Guard.", + "points": 5 + }, + { + "name": "Andorian Empire Expansion", + "description": "Expand Andorian influence into new territories.", + "points": 8 + } + ] +} \ No newline at end of file diff --git a/src/assets/captains/sisko/config.json b/src/assets/captains/sisko/config.json new file mode 100644 index 0000000..1188afe --- /dev/null +++ b/src/assets/captains/sisko/config.json @@ -0,0 +1,60 @@ +{ + "id": "sisko", + "name": "Sisko", + "image": "captain.jpg", + "specialtyThresholds": { + "research": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "influence": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + }, + "military": { + "basic": [ + { "threshold": 4, "multiplier": "x2" }, + { "threshold": 8, "multiplier": "x3" }, + { "threshold": 12, "multiplier": "x4" } + ], + "advanced": [ + { "threshold": 3, "multiplier": "x2" }, + { "threshold": 6, "multiplier": "x3" }, + { "threshold": 9, "multiplier": "x4" } + ] + } + }, + "basicMission": { + "name": "Bajoran Temple", + "description": "Protect and maintain the Bajoran Temple on Deep Space Nine.", + "points": 3 + }, + "advancedMissions": [ + { + "name": "Dominion War", + "description": "Lead the Federation forces in a critical battle against the Dominion.", + "points": 5 + }, + { + "name": "Emissary of the Prophets", + "description": "Fulfill your role as the Emissary of the Prophets to save Bajor.", + "points": 8 + } + ] +} \ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index 9d450cd..f002056 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,8 +1,23 @@ --- import Layout from '../layouts/Layout.astro'; +import { UIService } from '../utils/ui'; // Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build // Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh. + +interface Captain { + id: string; + name: string; +} + +const captains: Captain[] = [ + { id: 'picard', name: 'Picard' }, + { id: 'sisko', name: 'Sisko' }, + { id: 'sela', name: 'Sela' }, + { id: 'burnham', name: 'Burnham' }, + { id: 'shran', name: 'Shran' }, + { id: 'koloth', name: 'Koloth' } +]; --- @@ -13,39 +28,119 @@ import Layout from '../layouts/Layout.astro';
- +
+
+ + +
+
+ + + Basic +
+
-
- -
- - 0 - + +
+

Resources

+
+
+ +
+ + 0 + +
+
+ +
+ +
+ + 0 + +
+
+ +
+ +
+ + 0 + +
+
- -
- -
- - 0 - + + +
+ + +
+

Specialties

+
+
+
+ + x1 +
+
+ + 0 + +
+
+ +
+
+ + x1 +
+
+ + 0 + +
+
+ +
+
+ + x1 +
+
+ + 0 + +
+
- -
- -
- - 0 - + + +
@@ -54,39 +149,119 @@ import Layout from '../layouts/Layout.astro';
- +
+
+ + +
+
+ + + Basic +
+
-
- -
- - 0 - + +
+

Resources

+
+
+ +
+ + 0 + +
+
+ +
+ +
+ + 0 + +
+
+ +
+ +
+ + 0 + +
+
- -
- -
- - 0 - + + +
+ + +
+

Specialties

+
+
+
+ + x1 +
+
+ + 0 + +
+
+ +
+
+ + x1 +
+
+ + 0 + +
+
+ +
+
+ + x1 +
+
+ + 0 + +
+
- -
- -
- - 0 - + + +
@@ -105,97 +280,47 @@ import Layout from '../layouts/Layout.astro'; .token-btn { @apply text-white font-bold py-2 px-4 rounded transition-colors duration-200; } + + /* Toggle switch styles */ + button[role="switch"] { + @apply bg-gray-600; + } + + button[role="switch"][aria-checked="true"] { + @apply bg-blue-500; + } + + button[role="switch"][aria-checked="true"] span { + @apply translate-x-5; + } + + /* Player 2 specific toggle styles */ + #player2-mode-toggle[aria-checked="true"] { + @apply bg-red-500; + } + + /* Mission card styles */ + .mission-card { + @apply bg-gray-700 rounded-lg p-4 border border-gray-600; + } + + .mission-card h4 { + @apply text-lg font-semibold text-gray-200 mb-2; + } + + .mission-card p { + @apply text-gray-400 text-sm mb-2; + } + + .mission-points { + @apply text-sm text-green-400 font-semibold; + } diff --git a/src/utils/captainData.ts b/src/utils/captainData.ts new file mode 100644 index 0000000..84234cd --- /dev/null +++ b/src/utils/captainData.ts @@ -0,0 +1,133 @@ +interface ThresholdTable { + basic: { + threshold: number; + multiplier: string; + }[]; + advanced: { + threshold: number; + multiplier: string; + }[]; +} + +/** + * @typedef {Object} Mission + * @property {string} name - The name of the mission + * @property {string} description - A brief description of the mission + * @property {number} points - The reward points for completing the mission + */ +interface Mission { + name: string; + description: string; + points: number; +} + +/** + * @typedef {Object} CaptainConfig + * @property {string} id - Unique identifier for the captain + * @property {string} name - Display name of the captain + * @property {string} image - Filename of the captain's image + * @property {Object.} specialtyThresholds - Threshold tables for each specialty + * @property {Mission} basicMission - The single mission available in basic mode + * @property {Mission[]} advancedMissions - Array of missions available in advanced mode + */ +interface CaptainConfig { + id: string; + name: string; + image: string; + specialtyThresholds: { + [key: string]: ThresholdTable; + }; + basicMission: Mission; + advancedMissions: Mission[]; +} + +class CaptainDataManager { + private static instance: CaptainDataManager; + private captainConfigs: Map = new Map(); + + private constructor() {} + + static getInstance(): CaptainDataManager { + if (!CaptainDataManager.instance) { + CaptainDataManager.instance = new CaptainDataManager(); + } + return CaptainDataManager.instance; + } + + async loadCaptainConfig(captainId: string): Promise { + if (this.captainConfigs.has(captainId)) { + return this.captainConfigs.get(captainId)!; + } + + try { + const response = await fetch(`/src/assets/captains/${captainId}/config.json`); + if (!response.ok) { + throw new Error(`Failed to load captain config for ${captainId}`); + } + const config: CaptainConfig = await response.json(); + this.captainConfigs.set(captainId, config); + return config; + } catch (error) { + console.error(`Error loading captain config for ${captainId}:`, error); + throw error; + } + } + + getCaptainImagePath(captainId: string): string { + return `/src/assets/captains/${captainId}/captain.jpg`; + } + + getSpecialtyThreshold(captainId: string, specialty: 'research' | 'influence' | 'military', mode: 'basic' | 'advanced', value: number): string { + const config = this.captainConfigs.get(captainId); + if (!config) { + throw new Error(`Captain config not found for ${captainId}`); + } + + const thresholds = config.specialtyThresholds[specialty][mode]; + console.log(`Calculating multiplier for ${captainId} ${specialty} in ${mode} mode:`, { + currentValue: value, + thresholds: thresholds + }); + + // Find the highest threshold that the current value meets or exceeds + for (let i = thresholds.length - 1; i >= 0; i--) { + if (value >= thresholds[i].threshold) { + console.log(`Found threshold ${thresholds[i].threshold} with multiplier ${thresholds[i].multiplier}`); + return thresholds[i].multiplier; + } + } + console.log('No thresholds met, returning default multiplier x1'); + return 'x1'; + } + + /** + * Get available missions for a captain based on their current stats + * @param {string} captainId - The ID of the captain + * @param {Object} stats - Current stats of the player + * @param {number} stats.research - Current research value + * @param {number} stats.influence - Current influence value + * @param {number} stats.military - Current military value + * @param {string} mode - The current game mode ('basic' or 'advanced') + * @returns {Mission[]} Array of available missions + */ + getAvailableMissions(captainId: string, stats: { research: number; influence: number; military: number }, mode: 'basic' | 'advanced' = 'basic'): Mission[] { + const config = this.captainConfigs.get(captainId); + if (!config) { + throw new Error(`Captain config not found for ${captainId}`); + } + + const missions: Mission[] = []; + + // Always include the basic mission + missions.push(config.basicMission); + + // In advanced mode, include additional missions + if (mode === 'advanced') { + missions.push(...config.advancedMissions); + } + + return missions; + } +} + +export const captainDataManager = CaptainDataManager.getInstance(); \ No newline at end of file diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..66e9a45 --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,83 @@ +import type { GameState, PlayerNumber } from './types'; + +export class StorageService { + private static readonly STORAGE_PREFIX = 'stcc_'; + + public static saveGameState(state: GameState): void { + // Save player 1 state + localStorage.setItem(`${this.STORAGE_PREFIX}player1_captain`, state.player1.captain); + localStorage.setItem(`${this.STORAGE_PREFIX}player1_mode`, state.player1.mode); + localStorage.setItem(`${this.STORAGE_PREFIX}player1_latinum`, state.player1.latinum.toString()); + localStorage.setItem(`${this.STORAGE_PREFIX}player1_dilithium`, state.player1.dilithium.toString()); + localStorage.setItem(`${this.STORAGE_PREFIX}player1_glory`, state.player1.glory.toString()); + localStorage.setItem(`${this.STORAGE_PREFIX}player1_research`, state.player1.research.toString()); + localStorage.setItem(`${this.STORAGE_PREFIX}player1_influence`, state.player1.influence.toString()); + localStorage.setItem(`${this.STORAGE_PREFIX}player1_military`, state.player1.military.toString()); + + // Save player 2 state + localStorage.setItem(`${this.STORAGE_PREFIX}player2_captain`, state.player2.captain); + localStorage.setItem(`${this.STORAGE_PREFIX}player2_mode`, state.player2.mode); + localStorage.setItem(`${this.STORAGE_PREFIX}player2_latinum`, state.player2.latinum.toString()); + localStorage.setItem(`${this.STORAGE_PREFIX}player2_dilithium`, state.player2.dilithium.toString()); + localStorage.setItem(`${this.STORAGE_PREFIX}player2_glory`, state.player2.glory.toString()); + localStorage.setItem(`${this.STORAGE_PREFIX}player2_research`, state.player2.research.toString()); + localStorage.setItem(`${this.STORAGE_PREFIX}player2_influence`, state.player2.influence.toString()); + localStorage.setItem(`${this.STORAGE_PREFIX}player2_military`, state.player2.military.toString()); + } + + public static loadGameState(): GameState { + const state: GameState = { + player1: { + captain: '', + mode: 'basic', + latinum: 0, + dilithium: 0, + glory: 0, + research: 0, + influence: 0, + military: 0 + }, + player2: { + captain: '', + mode: 'basic', + latinum: 0, + dilithium: 0, + glory: 0, + research: 0, + influence: 0, + military: 0 + } + }; + + // Load player 1 state + state.player1.captain = localStorage.getItem(`${this.STORAGE_PREFIX}player1_captain`) || ''; + state.player1.mode = (localStorage.getItem(`${this.STORAGE_PREFIX}player1_mode`) as 'basic' | 'advanced') || 'basic'; + state.player1.latinum = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player1_latinum`) || '0', 10); + state.player1.dilithium = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player1_dilithium`) || '0', 10); + state.player1.glory = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player1_glory`) || '0', 10); + state.player1.research = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player1_research`) || '0', 10); + state.player1.influence = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player1_influence`) || '0', 10); + state.player1.military = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player1_military`) || '0', 10); + + // Load player 2 state + state.player2.captain = localStorage.getItem(`${this.STORAGE_PREFIX}player2_captain`) || ''; + state.player2.mode = (localStorage.getItem(`${this.STORAGE_PREFIX}player2_mode`) as 'basic' | 'advanced') || 'basic'; + state.player2.latinum = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player2_latinum`) || '0', 10); + state.player2.dilithium = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player2_dilithium`) || '0', 10); + state.player2.glory = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player2_glory`) || '0', 10); + state.player2.research = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player2_research`) || '0', 10); + state.player2.influence = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player2_influence`) || '0', 10); + state.player2.military = parseInt(localStorage.getItem(`${this.STORAGE_PREFIX}player2_military`) || '0', 10); + + return state; + } + + public static clearGameState(): void { + // Clear all state entries + Object.keys(localStorage).forEach(key => { + if (key.startsWith(this.STORAGE_PREFIX)) { + localStorage.removeItem(key); + } + }); + } +} \ No newline at end of file diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..ff5cc34 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,31 @@ +export interface Captain { + id: string; + name: string; +} + +export interface GameState { + player1: PlayerState; + player2: PlayerState; +} + +export interface PlayerState { + captain: string; + mode: 'basic' | 'advanced'; + latinum: number; + dilithium: number; + glory: number; + research: number; + influence: number; + military: number; +} + +export const TOKEN_TYPES = { + RESOURCES: ['latinum', 'dilithium', 'glory'] as const, + SPECIALTIES: ['research', 'influence', 'military'] as const +} as const; + +export type ResourceType = typeof TOKEN_TYPES.RESOURCES[number]; +export type SpecialtyType = typeof TOKEN_TYPES.SPECIALTIES[number]; +export type TokenType = ResourceType | SpecialtyType; + +export type PlayerNumber = 1 | 2; \ No newline at end of file diff --git a/src/utils/ui.ts b/src/utils/ui.ts new file mode 100644 index 0000000..fcba64a --- /dev/null +++ b/src/utils/ui.ts @@ -0,0 +1,337 @@ +import type { GameState, PlayerNumber, TokenType } from './types'; +import { captainDataManager } from './captainData'; +import { StorageService } from './storage'; + +export class UIService { + private static instance: UIService; + private gameState: GameState; + + private constructor() { + this.gameState = { + player1: { + captain: '', + mode: 'basic', + latinum: 0, + dilithium: 0, + glory: 0, + research: 0, + influence: 0, + military: 0 + }, + player2: { + captain: '', + mode: 'basic', + latinum: 0, + dilithium: 0, + glory: 0, + research: 0, + influence: 0, + military: 0 + } + }; + } + + static getInstance(): UIService { + if (!UIService.instance) { + UIService.instance = new UIService(); + } + return UIService.instance; + } + + private getElement(id: string): T | null { + const element = document.getElementById(id) as T | null; + if (!element) { + console.error(`Element with id '${id}' not found`); + } + return element; + } + + private updateCaptainOptions(playerNum: PlayerNumber): void { + const playerSelect = this.getElement(`player${playerNum}-captain`); + if (!playerSelect) return; + + // Clear existing options except the first one + while (playerSelect.options.length > 1) { + playerSelect.remove(1); + } + + // Add available captains + const captains = [ + { id: 'picard', name: 'Picard' }, + { id: 'sisko', name: 'Sisko' }, + { id: 'sela', name: 'Sela' }, + { id: 'burnham', name: 'Burnham' }, + { id: 'shran', name: 'Shran' }, + { id: 'koloth', name: 'Koloth' } + ]; + + captains.forEach(captain => { + const otherPlayer = playerNum === 1 ? 'player2' : 'player1'; + if (captain.id !== this.gameState[otherPlayer].captain) { + const option = new Option(captain.name, captain.id); + playerSelect.add(option); + } + }); + + // Set the current value + const playerKey = `player${playerNum}` as keyof GameState; + playerSelect.value = this.gameState[playerKey].captain; + } + + private calculateMultiplier(playerNum: PlayerNumber, specialty: 'research' | 'influence' | 'military'): string { + const playerKey = `player${playerNum}` as keyof GameState; + const player = this.gameState[playerKey]; + + console.log('Calculating multiplier:', { + playerNum, + specialty, + captain: player.captain, + mode: player.mode, + value: player[specialty] + }); + + if (!player.captain) { + console.log('No captain selected, returning x1'); + return 'x1'; + } + + try { + const multiplier = captainDataManager.getSpecialtyThreshold( + player.captain, + specialty, + player.mode, + player[specialty] + ); + console.log('Calculated multiplier:', multiplier); + return multiplier; + } catch (error) { + console.error(`Error calculating multiplier: ${error}`); + return 'x1'; + } + } + + private updateUI(): void { + // Update player selections + this.updateCaptainOptions(1); + this.updateCaptainOptions(2); + + // Update mode toggles and counter buttons + [1, 2].forEach(playerNum => { + const playerKey = `player${playerNum}` as keyof GameState; + const player = this.gameState[playerKey]; + + const toggle = this.getElement(`player${playerNum}-mode-toggle`); + const label = this.getElement(`player${playerNum}-mode-label`); + + if (toggle) toggle.setAttribute('aria-checked', (player.mode === 'advanced').toString()); + if (label) label.textContent = player.mode === 'advanced' ? 'Advanced' : 'Basic'; + + // Disable/enable counter buttons based on captain selection + const isCaptainSelected = !!player.captain; + const playerButtons = document.querySelectorAll(`.token-btn[data-player="${playerNum}"]`); + playerButtons.forEach(button => { + (button as HTMLButtonElement).disabled = !isCaptainSelected; + if (!isCaptainSelected) { + button.classList.add('opacity-50', 'cursor-not-allowed'); + } else { + button.classList.remove('opacity-50', 'cursor-not-allowed'); + } + }); + }); + + // Update resource counters + ['latinum', 'dilithium', 'glory'].forEach(resource => { + [1, 2].forEach(playerNum => { + const playerKey = `player${playerNum}` as keyof GameState; + const player = this.gameState[playerKey]; + + const counter = this.getElement(`player${playerNum}-${resource}`); + if (counter) counter.textContent = player[resource as keyof typeof player].toString(); + }); + }); + + // Update specialty counters and multipliers + ['research', 'influence', 'military'].forEach(specialty => { + [1, 2].forEach(playerNum => { + const playerKey = `player${playerNum}` as keyof GameState; + const player = this.gameState[playerKey]; + + const counter = this.getElement(`player${playerNum}-${specialty}`); + const multiplier = this.getElement(`player${playerNum}-${specialty}-multiplier`); + + if (counter) counter.textContent = player[specialty as keyof typeof player].toString(); + if (multiplier) { + const multiplierValue = this.calculateMultiplier(playerNum as PlayerNumber, specialty as 'research' | 'influence' | 'military'); + console.log(`Updating multiplier for player ${playerNum} ${specialty}:`, multiplierValue); + multiplier.textContent = multiplierValue; + } + }); + }); + + // Update missions + this.updateMissions(); + } + + private updateMissions(): void { + [1, 2].forEach(playerNum => { + const playerKey = `player${playerNum}` as keyof GameState; + const player = this.gameState[playerKey]; + + const missionsContainer = this.getElement(`player${playerNum}-missions`); + const missionsList = this.getElement(`player${playerNum}-missions-list`); + + if (!missionsContainer || !missionsList) return; + + // Clear existing missions + missionsList.innerHTML = ''; + + // Get available missions + const missions = captainDataManager.getAvailableMissions( + player.captain, + { + research: player.research, + influence: player.influence, + military: player.military + }, + player.mode + ); + + // Add missions to the list + missions.forEach(mission => { + const li = document.createElement('li'); + li.textContent = `${mission.name} (${mission.points} points)`; + missionsList.appendChild(li); + }); + }); + } + + private handleTokenChange(playerNum: PlayerNumber, token: TokenType, action: 'increment' | 'decrement'): void { + const playerKey = `player${playerNum}` as keyof GameState; + const currentValue = this.gameState[playerKey][token]; + + console.log('Handling token change:', { + playerNum, + token, + action, + currentValue + }); + + if (action === 'increment') { + this.gameState[playerKey][token] = Math.min(15, currentValue + 1); + } else { + this.gameState[playerKey][token] = Math.max(0, currentValue - 1); + } + + console.log('New value:', this.gameState[playerKey][token]); + + this.updateUI(); + StorageService.saveGameState(this.gameState); + } + + private resetCounters(): void { + if (confirm('Are you sure you want to reset all counters?')) { + ['1', '2'].forEach(playerNum => { + const playerKey = `player${playerNum}` as keyof GameState; + const tokens: TokenType[] = ['latinum', 'dilithium', 'glory', 'research', 'influence', 'military']; + tokens.forEach(token => { + this.gameState[playerKey][token] = 0; + }); + }); + this.updateUI(); + StorageService.saveGameState(this.gameState); + } + } + + public initialize(): void { + // Load saved state + const savedState = StorageService.loadGameState(); + this.gameState = savedState; + + // Captain selection + const player1CaptainSelect = this.getElement('player1-captain'); + const player2CaptainSelect = this.getElement('player2-captain'); + + if (player1CaptainSelect) { + player1CaptainSelect.addEventListener('change', async (e: Event) => { + const target = e.target as HTMLSelectElement; + if (target.value === player2CaptainSelect?.value && target.value !== '') { + alert('Cannot select the same captain for both players'); + target.value = ''; + return; + } + this.gameState.player1.captain = target.value; + if (target.value) { + try { + await captainDataManager.loadCaptainConfig(target.value); + } catch (error) { + console.error('Error loading captain config:', error); + } + } + this.updateUI(); + StorageService.saveGameState(this.gameState); + }); + } + + if (player2CaptainSelect) { + player2CaptainSelect.addEventListener('change', async (e: Event) => { + const target = e.target as HTMLSelectElement; + if (target.value === player1CaptainSelect?.value && target.value !== '') { + alert('Cannot select the same captain for both players'); + target.value = ''; + return; + } + this.gameState.player2.captain = target.value; + if (target.value) { + try { + await captainDataManager.loadCaptainConfig(target.value); + } catch (error) { + console.error('Error loading captain config:', error); + } + } + this.updateUI(); + StorageService.saveGameState(this.gameState); + }); + } + + // Mode toggles + [1, 2].forEach(playerNum => { + const toggle = this.getElement(`player${playerNum}-mode-toggle`); + if (toggle) { + toggle.addEventListener('click', () => { + const playerKey = `player${playerNum}` as keyof GameState; + this.gameState[playerKey].mode = this.gameState[playerKey].mode === 'basic' ? 'advanced' : 'basic'; + this.updateUI(); + StorageService.saveGameState(this.gameState); + }); + } + }); + + // Token button handlers + document.querySelectorAll('.token-btn').forEach(button => { + const element = button as HTMLElement; + const player = element.dataset.player; + const type = element.dataset.token; + const action = element.dataset.action; + + if (player && type && action) { + element.addEventListener('click', () => { + const playerNum = parseInt(player, 10) as PlayerNumber; + const tokenType = type as TokenType; + const actionType = action === 'increase' ? 'increment' : 'decrement'; + + console.log('Token button clicked:', { playerNum, tokenType, actionType }); + this.handleTokenChange(playerNum, tokenType, actionType); + }); + } + }); + + // Reset button + const resetButton = this.getElement('reset-btn'); + if (resetButton) { + resetButton.addEventListener('click', () => this.resetCounters()); + } + + // Initial UI update + this.updateUI(); + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8bf91d3..ff4a676 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,14 @@ { - "extends": "astro/tsconfigs/strict", - "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist"] + "compilerOptions": { + "target": "ES2017", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2017", "DOM"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] }