Compare commits
2 commits
bc50c63251
...
f0e597a3d8
| Author | SHA1 | Date | |
|---|---|---|---|
| f0e597a3d8 | |||
| c54e010801 |
15 changed files with 1334 additions and 156 deletions
|
|
@ -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",
|
||||
|
|
|
|||
41
scripts/generate-captains-index.js
Normal file
41
scripts/generate-captains-index.js
Normal 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();
|
||||
48
src/assets/captains/burnham.json
Normal file
48
src/assets/captains/burnham.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
28
src/assets/captains/index.json
Normal file
28
src/assets/captains/index.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
48
src/assets/captains/koloth.json
Normal file
48
src/assets/captains/koloth.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
48
src/assets/captains/picard.json
Normal file
48
src/assets/captains/picard.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
48
src/assets/captains/sela.json
Normal file
48
src/assets/captains/sela.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
60
src/assets/captains/shran.json
Normal file
60
src/assets/captains/shran.json
Normal file
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
60
src/assets/captains/sisko.json
Normal file
60
src/assets/captains/sisko.json
Normal file
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,8 +1,27 @@
|
|||
---
|
||||
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.
|
||||
|
||||
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' }
|
||||
];
|
||||
|
||||
// Initialize UI service
|
||||
const uiService = UIService.getInstance();
|
||||
---
|
||||
|
||||
<Layout title="STCC - Token Counter">
|
||||
|
|
@ -10,18 +29,48 @@ import Layout from '../layouts/Layout.astro';
|
|||
<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">
|
||||
<input
|
||||
type="text"
|
||||
id="player1-name"
|
||||
class="w-full bg-gray-700 text-white rounded px-4 py-2 border border-blue-400 focus:outline-none focus:border-blue-300"
|
||||
placeholder="Player 1 Name"
|
||||
/>
|
||||
<!-- 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">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"
|
||||
role="switch"
|
||||
aria-checked="false"
|
||||
aria-label="Toggle mode"
|
||||
>
|
||||
<span class="sr-only">Toggle mode</span>
|
||||
<span
|
||||
class="absolute left-1 inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||
></span>
|
||||
</button>
|
||||
<span id="player1-mode-label" class="text-sm text-gray-300">Basic</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Resources Section -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-300 mb-2 text-center">Resources</h3>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-yellow-400">Latinum</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
|
|
@ -32,37 +81,114 @@ import Layout from '../layouts/Layout.astro';
|
|||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-blue-400">Dilithium</label>
|
||||
<label class="text-pink-400">Dilithium</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="1" data-token="dilithium" data-action="decrease">-</button>
|
||||
<button class="token-btn bg-pink-500 hover:bg-pink-600" data-player="1" data-token="dilithium" data-action="decrease">-</button>
|
||||
<span id="player1-dilithium" class="text-2xl font-bold">0</span>
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="1" data-token="dilithium" data-action="increase">+</button>
|
||||
<button class="token-btn bg-pink-500 hover:bg-pink-600" data-player="1" data-token="dilithium" data-action="increase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-purple-400">Glory</label>
|
||||
<label class="text-blue-400">Glory</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="token-btn bg-purple-500 hover:bg-purple-600" data-player="1" data-token="glory" data-action="decrease">-</button>
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="1" data-token="glory" data-action="decrease">-</button>
|
||||
<span id="player1-glory" class="text-2xl font-bold">0</span>
|
||||
<button class="token-btn bg-purple-500 hover:bg-purple-600" data-player="1" data-token="glory" data-action="increase">+</button>
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="1" data-token="glory" data-action="increase">+</button>
|
||||
</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">
|
||||
<input
|
||||
type="text"
|
||||
id="player2-name"
|
||||
class="w-full bg-gray-700 text-white rounded px-4 py-2 border border-red-400 focus:outline-none focus:border-red-300"
|
||||
placeholder="Player 2 Name"
|
||||
/>
|
||||
<!-- Horizontal Divider -->
|
||||
<div class="w-full h-px bg-gray-700"></div>
|
||||
|
||||
<!-- Specialties Section -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-300 mb-2 text-center">Specialties</h3>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center w-32">
|
||||
<label class="text-blue-400">Research</label>
|
||||
<span id="player1-research-multiplier" class="text-sm text-gray-400 ml-2">x1</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="1" data-token="research" data-action="decrease">-</button>
|
||||
<span id="player1-research" class="text-2xl font-bold">0</span>
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="1" data-token="research" data-action="increase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center w-32">
|
||||
<label class="text-yellow-400">Influence</label>
|
||||
<span id="player1-influence-multiplier" class="text-sm text-gray-400 ml-2">x1</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="token-btn bg-yellow-500 hover:bg-yellow-600" data-player="1" data-token="influence" data-action="decrease">-</button>
|
||||
<span id="player1-influence" class="text-2xl font-bold">0</span>
|
||||
<button class="token-btn bg-yellow-500 hover:bg-yellow-600" data-player="1" data-token="influence" data-action="increase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center w-32">
|
||||
<label class="text-red-400">Military</label>
|
||||
<span id="player1-military-multiplier" class="text-sm text-gray-400 ml-2">x1</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="token-btn bg-red-500 hover:bg-red-600" data-player="1" data-token="military" data-action="decrease">-</button>
|
||||
<span id="player1-military" class="text-2xl font-bold">0</span>
|
||||
<button class="token-btn bg-red-500 hover:bg-red-600" data-player="1" data-token="military" data-action="increase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">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"
|
||||
role="switch"
|
||||
aria-checked="false"
|
||||
aria-label="Toggle mode"
|
||||
>
|
||||
<span class="sr-only">Toggle mode</span>
|
||||
<span
|
||||
class="absolute left-1 inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||
></span>
|
||||
</button>
|
||||
<span id="player2-mode-label" class="text-sm text-gray-300">Basic</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Resources Section -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-300 mb-2 text-center">Resources</h3>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-yellow-400">Latinum</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
|
|
@ -73,20 +199,67 @@ import Layout from '../layouts/Layout.astro';
|
|||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-blue-400">Dilithium</label>
|
||||
<label class="text-pink-400">Dilithium</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="2" data-token="dilithium" data-action="decrease">-</button>
|
||||
<button class="token-btn bg-pink-500 hover:bg-pink-600" data-player="2" data-token="dilithium" data-action="decrease">-</button>
|
||||
<span id="player2-dilithium" class="text-2xl font-bold">0</span>
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="2" data-token="dilithium" data-action="increase">+</button>
|
||||
<button class="token-btn bg-pink-500 hover:bg-pink-600" data-player="2" data-token="dilithium" data-action="increase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-purple-400">Glory</label>
|
||||
<label class="text-blue-400">Glory</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="token-btn bg-purple-500 hover:bg-purple-600" data-player="2" data-token="glory" data-action="decrease">-</button>
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="2" data-token="glory" data-action="decrease">-</button>
|
||||
<span id="player2-glory" class="text-2xl font-bold">0</span>
|
||||
<button class="token-btn bg-purple-500 hover:bg-purple-600" data-player="2" data-token="glory" data-action="increase">+</button>
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="2" data-token="glory" data-action="increase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Horizontal Divider -->
|
||||
<div class="w-full h-px bg-gray-700"></div>
|
||||
|
||||
<!-- Specialties Section -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-300 mb-2 text-center">Specialties</h3>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center w-32">
|
||||
<label class="text-blue-400">Research</label>
|
||||
<span id="player2-research-multiplier" class="text-sm text-gray-400 ml-2">x1</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="2" data-token="research" data-action="decrease">-</button>
|
||||
<span id="player2-research" class="text-2xl font-bold">0</span>
|
||||
<button class="token-btn bg-blue-500 hover:bg-blue-600" data-player="2" data-token="research" data-action="increase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center w-32">
|
||||
<label class="text-yellow-400">Influence</label>
|
||||
<span id="player2-influence-multiplier" class="text-sm text-gray-400 ml-2">x1</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="token-btn bg-yellow-500 hover:bg-yellow-600" data-player="2" data-token="influence" data-action="decrease">-</button>
|
||||
<span id="player2-influence" class="text-2xl font-bold">0</span>
|
||||
<button class="token-btn bg-yellow-500 hover:bg-yellow-600" data-player="2" data-token="influence" data-action="increase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center w-32">
|
||||
<label class="text-red-400">Military</label>
|
||||
<span id="player2-military-multiplier" class="text-sm text-gray-400 ml-2">x1</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button class="token-btn bg-red-500 hover:bg-red-600" data-player="2" data-token="military" data-action="decrease">-</button>
|
||||
<span id="player2-military" class="text-2xl font-bold">0</span>
|
||||
<button class="token-btn bg-red-500 hover:bg-red-600" data-player="2" data-token="military" data-action="increase">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -105,97 +278,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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Initialize state from localStorage or default values
|
||||
const initialState = {
|
||||
player1: {
|
||||
name: localStorage.getItem('player1Name') || 'Player 1',
|
||||
latinum: parseInt(localStorage.getItem('player1Latinum') || '0'),
|
||||
dilithium: parseInt(localStorage.getItem('player1Dilithium') || '0'),
|
||||
glory: parseInt(localStorage.getItem('player1Glory') || '0')
|
||||
},
|
||||
player2: {
|
||||
name: localStorage.getItem('player2Name') || 'Player 2',
|
||||
latinum: parseInt(localStorage.getItem('player2Latinum') || '0'),
|
||||
dilithium: parseInt(localStorage.getItem('player2Dilithium') || '0'),
|
||||
glory: parseInt(localStorage.getItem('player2Glory') || '0')
|
||||
}
|
||||
};
|
||||
import { UIService } from '../utils/ui';
|
||||
|
||||
// Update UI with initial state
|
||||
function updateUI() {
|
||||
// Update player names
|
||||
document.getElementById('player1-name').value = initialState.player1.name;
|
||||
document.getElementById('player2-name').value = initialState.player2.name;
|
||||
|
||||
// Update token counts
|
||||
document.getElementById('player1-latinum').textContent = initialState.player1.latinum;
|
||||
document.getElementById('player1-dilithium').textContent = initialState.player1.dilithium;
|
||||
document.getElementById('player1-glory').textContent = initialState.player1.glory;
|
||||
|
||||
document.getElementById('player2-latinum').textContent = initialState.player2.latinum;
|
||||
document.getElementById('player2-dilithium').textContent = initialState.player2.dilithium;
|
||||
document.getElementById('player2-glory').textContent = initialState.player2.glory;
|
||||
}
|
||||
|
||||
// Save state to localStorage
|
||||
function saveState() {
|
||||
localStorage.setItem('player1Name', initialState.player1.name);
|
||||
localStorage.setItem('player1Latinum', initialState.player1.latinum.toString());
|
||||
localStorage.setItem('player1Dilithium', initialState.player1.dilithium.toString());
|
||||
localStorage.setItem('player1Glory', initialState.player1.glory.toString());
|
||||
|
||||
localStorage.setItem('player2Name', initialState.player2.name);
|
||||
localStorage.setItem('player2Latinum', initialState.player2.latinum.toString());
|
||||
localStorage.setItem('player2Dilithium', initialState.player2.dilithium.toString());
|
||||
localStorage.setItem('player2Glory', initialState.player2.glory.toString());
|
||||
}
|
||||
|
||||
// Handle token button clicks
|
||||
document.querySelectorAll('.token-btn').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const player = button.dataset.player;
|
||||
const token = button.dataset.token;
|
||||
const action = button.dataset.action;
|
||||
|
||||
if (action === 'increase') {
|
||||
initialState[`player${player}`][token]++;
|
||||
} else {
|
||||
initialState[`player${player}`][token] = Math.max(0, initialState[`player${player}`][token] - 1);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
saveState();
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
UIService.getInstance().initialize();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle player name changes
|
||||
document.getElementById('player1-name').addEventListener('input', (e) => {
|
||||
initialState.player1.name = e.target.value;
|
||||
saveState();
|
||||
});
|
||||
|
||||
document.getElementById('player2-name').addEventListener('input', (e) => {
|
||||
initialState.player2.name = e.target.value;
|
||||
saveState();
|
||||
});
|
||||
|
||||
// Handle reset button
|
||||
document.getElementById('reset-btn').addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to reset all counters?')) {
|
||||
initialState.player1.latinum = 0;
|
||||
initialState.player1.dilithium = 0;
|
||||
initialState.player1.glory = 0;
|
||||
initialState.player2.latinum = 0;
|
||||
initialState.player2.dilithium = 0;
|
||||
initialState.player2.glory = 0;
|
||||
updateUI();
|
||||
saveState();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial UI update
|
||||
updateUI();
|
||||
</script>
|
||||
|
|
|
|||
158
src/utils/captainData.ts
Normal file
158
src/utils/captainData.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import type { Captain } from './types';
|
||||
|
||||
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.<string, ThresholdTable>} 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<string, CaptainConfig> = new Map();
|
||||
private availableCaptains: Captain[] = [];
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): CaptainDataManager {
|
||||
if (!CaptainDataManager.instance) {
|
||||
CaptainDataManager.instance = new 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}.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();
|
||||
83
src/utils/storage.ts
Normal file
83
src/utils/storage.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
31
src/utils/types.ts
Normal file
31
src/utils/types.ts
Normal file
|
|
@ -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;
|
||||
392
src/utils/ui.ts
Normal file
392
src/utils/ui.ts
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
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<T extends HTMLElement>(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 async updateCaptainOptions(playerNum: PlayerNumber): Promise<void> {
|
||||
const select = this.getElement<HTMLSelectElement>(`player${playerNum}-captain`);
|
||||
if (!select) return;
|
||||
|
||||
try {
|
||||
console.log('Loading available captains...');
|
||||
const captains = await captainDataManager.loadAvailableCaptains();
|
||||
console.log('Loaded captains:', captains);
|
||||
const currentValue = select.value;
|
||||
|
||||
// Clear all existing options
|
||||
select.innerHTML = '';
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating captain options:', error);
|
||||
}
|
||||
}
|
||||
|
||||
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<HTMLButtonElement>(`player${playerNum}-mode-toggle`);
|
||||
const label = this.getElement<HTMLSpanElement>(`player${playerNum}-mode-label`);
|
||||
|
||||
// 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 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');
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
['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<HTMLSpanElement>(`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<HTMLSpanElement>(`player${playerNum}-${specialty}`);
|
||||
const multiplier = this.getElement<HTMLSpanElement>(`player${playerNum}-${specialty}-multiplier`);
|
||||
|
||||
if (counter) counter.textContent = player[specialty as keyof typeof player].toString();
|
||||
if (multiplier) multiplier.textContent = this.calculateMultiplier(playerNum as PlayerNumber, specialty as 'research' | 'influence' | 'military');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private updateMissions(): void {
|
||||
[1, 2].forEach(playerNum => {
|
||||
const playerKey = `player${playerNum}` as keyof GameState;
|
||||
const player = this.gameState[playerKey];
|
||||
|
||||
const missionsContainer = this.getElement<HTMLDivElement>(`player${playerNum}-missions`);
|
||||
const missionsList = this.getElement<HTMLDivElement>(`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<HTMLSelectElement>('player1-captain');
|
||||
const player2CaptainSelect = this.getElement<HTMLSelectElement>('player2-captain');
|
||||
|
||||
if (player1CaptainSelect) {
|
||||
player1CaptainSelect.addEventListener('change', async (e: Event) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
this.gameState.player1.captain = target.value;
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
if (player2CaptainSelect) {
|
||||
player2CaptainSelect.addEventListener('change', async (e: Event) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
this.gameState.player2.captain = target.value;
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// Mode toggles
|
||||
[1, 2].forEach(playerNum => {
|
||||
const toggle = this.getElement<HTMLButtonElement>(`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<HTMLButtonElement>('reset-btn');
|
||||
if (resetButton) {
|
||||
resetButton.addEventListener('click', () => this.resetCounters());
|
||||
}
|
||||
|
||||
// Initial UI update
|
||||
this.updateUI();
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue