refactor: improve captain data management and UI layout - Move captain configs to individual JSON files - Add dynamic captain indexing - Update UI to show missions above captain selector - Remove player labels for cleaner interface - Add captain selection filtering to prevent duplicate selections

This commit is contained in:
Your Name 2025-03-25 17:47:42 -04:00
parent c54e010801
commit f0e597a3d8
12 changed files with 258 additions and 158 deletions

View file

@ -4,9 +4,10 @@
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"build": "node scripts/generate-captains-index.js && astro build",
"preview": "astro preview",
"astro": "astro"
"astro": "astro",
"generate-captains": "node scripts/generate-captains-index.js"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.6",

View file

@ -0,0 +1,41 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const CAPTAINS_DIR = path.join(__dirname, '../src/assets/captains');
const INDEX_FILE = path.join(CAPTAINS_DIR, 'index.json');
function generateCaptainsIndex() {
try {
// Read all JSON files in the captains directory
const files = fs.readdirSync(CAPTAINS_DIR)
.filter(file => file.endsWith('.json') && file !== 'index.json');
// Parse each captain file to get their id and name
const captains = files.map(file => {
const content = fs.readFileSync(path.join(CAPTAINS_DIR, file), 'utf-8');
const captain = JSON.parse(content);
return {
id: captain.id,
name: captain.name
};
});
// Create the index object
const index = {
captains: captains
};
// Write the index file
fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 4));
console.log('Successfully generated captains index');
} catch (error) {
console.error('Error generating captains index:', error);
process.exit(1);
}
}
generateCaptainsIndex();

View file

@ -44,17 +44,5 @@
"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
}
]
}
}

View file

@ -0,0 +1,28 @@
{
"captains": [
{
"id": "burnham",
"name": "Burnham"
},
{
"id": "koloth",
"name": "Koloth"
},
{
"id": "picard",
"name": "Picard"
},
{
"id": "sela",
"name": "Sela"
},
{
"id": "shran",
"name": "Shran"
},
{
"id": "sisko",
"name": "Sisko"
}
]
}

View file

@ -44,17 +44,5 @@
"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
}
]
}
}

View file

@ -44,17 +44,5 @@
"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
}
]
}
}

View file

@ -44,17 +44,5 @@
"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
}
]
}
}

View file

@ -1,6 +1,7 @@
---
import Layout from '../layouts/Layout.astro';
import { UIService } from '../utils/ui';
import { captainDataManager } from '../utils/captainData';
// 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.
@ -18,6 +19,9 @@ const captains: Captain[] = [
{ id: 'shran', name: 'Shran' },
{ id: 'koloth', name: 'Koloth' }
];
// Initialize UI service
const uiService = UIService.getInstance();
---
<Layout title="STCC - Token Counter">
@ -25,21 +29,26 @@ const captains: Captain[] = [
<h1 class="text-4xl font-bold text-center mb-8 text-blue-400">Star Trek Captain's Chair</h1>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Player 1 -->
<div class="bg-gray-800 rounded-lg p-6 shadow-lg border border-blue-500">
<div class="mb-4">
<!-- Player 1 Section -->
<div class="bg-gray-800 p-6 rounded-lg shadow-lg">
<div class="space-y-4">
<!-- Missions Section -->
<div id="player1-missions" class="hidden">
<h3 class="text-lg font-semibold text-gray-300 mb-2 text-center">Available Missions</h3>
<div id="player1-missions-list" class="space-y-2">
<!-- Missions will be populated here -->
</div>
</div>
<div>
<label for="player1-captain" class="block text-sm font-medium text-gray-300 mb-1">Select Captain</label>
<select id="player1-captain" class="w-full bg-gray-700 text-white rounded px-3 py-2">
<option value="">Choose a captain...</option>
</select>
</div>
<div class="flex items-center space-x-4">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-300 mb-2">Select Captain</label>
<select
id="player1-captain"
class="w-full bg-gray-700 text-white rounded px-4 py-2 border border-blue-400 focus:outline-none focus:border-blue-300"
>
<option value="">Choose a captain...</option>
</select>
</div>
<div class="flex items-center space-x-2">
<label class="text-sm font-medium text-gray-300">Mode:</label>
<label class="block text-sm font-medium text-gray-300 mb-2">Mode:</label>
<button
id="player1-mode-toggle"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
@ -135,32 +144,29 @@ const captains: Captain[] = [
</div>
</div>
</div>
<!-- Missions Section -->
<div id="player1-missions" class="hidden">
<h3 class="text-lg font-semibold text-gray-300 mb-2 text-center">Available Missions</h3>
<div id="player1-missions-list" class="space-y-2">
<!-- Missions will be populated here -->
</div>
</div>
</div>
</div>
<!-- Player 2 -->
<div class="bg-gray-800 rounded-lg p-6 shadow-lg border border-red-500">
<div class="mb-4">
<!-- Player 2 Section -->
<div class="bg-gray-800 p-6 rounded-lg shadow-lg">
<div class="space-y-4">
<!-- Missions Section -->
<div id="player2-missions" class="hidden">
<h3 class="text-lg font-semibold text-gray-300 mb-2 text-center">Available Missions</h3>
<div id="player2-missions-list" class="space-y-2">
<!-- Missions will be populated here -->
</div>
</div>
<div>
<label for="player2-captain" class="block text-sm font-medium text-gray-300 mb-1">Select Captain</label>
<select id="player2-captain" class="w-full bg-gray-700 text-white rounded px-3 py-2">
<option value="">Choose a captain...</option>
</select>
</div>
<div class="flex items-center space-x-4">
<div class="flex-1">
<label class="block text-sm font-medium text-gray-300 mb-2">Select Captain</label>
<select
id="player2-captain"
class="w-full bg-gray-700 text-white rounded px-4 py-2 border border-red-400 focus:outline-none focus:border-red-300"
>
<option value="">Choose a captain...</option>
</select>
</div>
<div class="flex items-center space-x-2">
<label class="text-sm font-medium text-gray-300">Mode:</label>
<label class="block text-sm font-medium text-gray-300 mb-2">Mode:</label>
<button
id="player2-mode-toggle"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
@ -256,14 +262,6 @@ const captains: Captain[] = [
</div>
</div>
</div>
<!-- Missions Section -->
<div id="player2-missions" class="hidden">
<h3 class="text-lg font-semibold text-gray-300 mb-2 text-center">Available Missions</h3>
<div id="player2-missions-list" class="space-y-2">
<!-- Missions will be populated here -->
</div>
</div>
</div>
</div>
</div>
@ -319,7 +317,7 @@ const captains: Captain[] = [
<script>
import { UIService } from '../utils/ui';
document.addEventListener('DOMContentLoaded', () => {
UIService.getInstance().initialize();
});

View file

@ -1,3 +1,5 @@
import type { Captain } from './types';
interface ThresholdTable {
basic: {
threshold: number;
@ -38,12 +40,13 @@ interface CaptainConfig {
[key: string]: ThresholdTable;
};
basicMission: Mission;
advancedMissions: Mission[];
advancedMissions?: Mission[];
}
class CaptainDataManager {
private static instance: CaptainDataManager;
private captainConfigs: Map<string, CaptainConfig> = new Map();
private availableCaptains: Captain[] = [];
private constructor() {}
@ -54,13 +57,35 @@ class CaptainDataManager {
return CaptainDataManager.instance;
}
async loadAvailableCaptains(): Promise<Captain[]> {
if (this.availableCaptains.length > 0) {
console.log('Returning cached captains:', this.availableCaptains);
return this.availableCaptains;
}
try {
console.log('Loading captains from index.json...');
const response = await fetch('/src/assets/captains/index.json');
if (!response.ok) {
throw new Error('Failed to load captains index');
}
const data = await response.json();
this.availableCaptains = data.captains;
console.log('Set available captains:', this.availableCaptains);
return this.availableCaptains;
} catch (error) {
console.error('Error loading captains:', error);
throw error;
}
}
async loadCaptainConfig(captainId: string): Promise<CaptainConfig> {
if (this.captainConfigs.has(captainId)) {
return this.captainConfigs.get(captainId)!;
}
try {
const response = await fetch(`/src/assets/captains/${captainId}/config.json`);
const response = await fetch(`/src/assets/captains/${captainId}.json`);
if (!response.ok) {
throw new Error(`Failed to load captain config for ${captainId}`);
}
@ -123,7 +148,7 @@ class CaptainDataManager {
// In advanced mode, include additional missions
if (mode === 'advanced') {
missions.push(...config.advancedMissions);
missions.push(...config.advancedMissions || []);
}
return missions;

View file

@ -46,36 +46,61 @@ export class UIService {
return element;
}
private updateCaptainOptions(playerNum: PlayerNumber): void {
const playerSelect = this.getElement<HTMLSelectElement>(`player${playerNum}-captain`);
if (!playerSelect) return;
private async updateCaptainOptions(playerNum: PlayerNumber): Promise<void> {
const select = this.getElement<HTMLSelectElement>(`player${playerNum}-captain`);
if (!select) return;
// Clear existing options except the first one
while (playerSelect.options.length > 1) {
playerSelect.remove(1);
}
try {
console.log('Loading available captains...');
const captains = await captainDataManager.loadAvailableCaptains();
console.log('Loaded captains:', captains);
const currentValue = select.value;
// 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' }
];
// Clear all existing options
select.innerHTML = '';
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);
// Get the other player's selected captain
const otherPlayerNum = playerNum === 1 ? 2 : 1;
const otherPlayerKey = `player${otherPlayerNum}` as keyof GameState;
const otherPlayerCaptain = this.gameState[otherPlayerKey].captain;
// Filter out the other player's selected captain
const availableCaptains = captains.filter(captain => captain.id !== otherPlayerCaptain);
// Add captain options
availableCaptains.forEach(captain => {
console.log('Adding captain option:', captain);
const option = document.createElement('option');
option.value = captain.id;
option.textContent = captain.name;
select.appendChild(option);
});
// If no captain is currently selected, select the first one
if (!currentValue && availableCaptains.length > 0) {
select.value = availableCaptains[0].id;
this.gameState[`player${playerNum}` as keyof GameState].captain = availableCaptains[0].id;
try {
await captainDataManager.loadCaptainConfig(availableCaptains[0].id);
} catch (error) {
console.error('Error loading captain config:', error);
}
} else if (currentValue && availableCaptains.some(c => c.id === currentValue)) {
// Restore the previously selected value if it exists and is still available
select.value = currentValue;
} else if (availableCaptains.length > 0) {
// If the current selection is no longer available, select the first available captain
select.value = availableCaptains[0].id;
this.gameState[`player${playerNum}` as keyof GameState].captain = availableCaptains[0].id;
try {
await captainDataManager.loadCaptainConfig(availableCaptains[0].id);
} catch (error) {
console.error('Error loading captain config:', error);
}
}
});
// Set the current value
const playerKey = `player${playerNum}` as keyof GameState;
playerSelect.value = this.gameState[playerKey].captain;
} catch (error) {
console.error('Error updating captain options:', error);
}
}
private calculateMultiplier(playerNum: PlayerNumber, specialty: 'research' | 'influence' | 'military'): string {
@ -123,11 +148,48 @@ export class UIService {
const toggle = this.getElement<HTMLButtonElement>(`player${playerNum}-mode-toggle`);
const label = this.getElement<HTMLSpanElement>(`player${playerNum}-mode-label`);
if (toggle) toggle.setAttribute('aria-checked', (player.mode === 'advanced').toString());
// Disable/enable mode toggle based on captain selection
const isCaptainSelected = player.captain !== '' && player.captain !== 'Choose a captain...';
if (toggle) {
toggle.disabled = !isCaptainSelected;
toggle.setAttribute('aria-checked', (player.mode === 'advanced').toString());
if (!isCaptainSelected) {
toggle.classList.add('opacity-50', 'cursor-not-allowed');
} else {
toggle.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
if (label) label.textContent = player.mode === 'advanced' ? 'Advanced' : 'Basic';
// Update missions display
const missionsContainer = this.getElement<HTMLDivElement>(`player${playerNum}-missions`);
const missionsList = this.getElement<HTMLDivElement>(`player${playerNum}-missions-list`);
if (missionsContainer && missionsList) {
missionsList.innerHTML = '';
if (isCaptainSelected) {
const missions = captainDataManager.getAvailableMissions(
player.captain,
{
research: player.research,
influence: player.influence,
military: player.military
},
player.mode
);
missions.forEach(mission => {
const li = document.createElement('li');
li.textContent = `${mission.name} (${mission.points} points)`;
missionsList.appendChild(li);
});
missionsContainer.classList.remove('hidden');
} else {
missionsContainer.classList.add('hidden');
}
}
// 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;
@ -137,6 +199,16 @@ export class UIService {
button.classList.remove('opacity-50', 'cursor-not-allowed');
}
});
// Reset counters if no captain is selected
if (!isCaptainSelected) {
player.latinum = 0;
player.dilithium = 0;
player.glory = 0;
player.research = 0;
player.influence = 0;
player.military = 0;
}
});
// Update resource counters
@ -160,16 +232,9 @@ export class UIService {
const multiplier = this.getElement<HTMLSpanElement>(`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;
}
if (multiplier) multiplier.textContent = this.calculateMultiplier(playerNum as PlayerNumber, specialty as 'research' | 'influence' | 'military');
});
});
// Update missions
this.updateMissions();
}
private updateMissions(): void {
@ -254,18 +319,13 @@ export class UIService {
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);
}
try {
await captainDataManager.loadCaptainConfig(target.value);
// Update player 2's options to remove the selected captain
await this.updateCaptainOptions(2);
} catch (error) {
console.error('Error loading captain config:', error);
}
this.updateUI();
StorageService.saveGameState(this.gameState);
@ -275,18 +335,13 @@ export class UIService {
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);
}
try {
await captainDataManager.loadCaptainConfig(target.value);
// Update player 1's options to remove the selected captain
await this.updateCaptainOptions(1);
} catch (error) {
console.error('Error loading captain config:', error);
}
this.updateUI();
StorageService.saveGameState(this.gameState);