🐐
This commit is contained in:
@@ -21,6 +21,19 @@
|
||||
let gameState = $state(null);
|
||||
let myId = $state('');
|
||||
|
||||
let viewingBoard = $state(false);
|
||||
let showDifficultyModal = $state(false);
|
||||
let selectedDifficulty = $state(5);
|
||||
|
||||
const difficultyLabel = $derived(
|
||||
selectedDifficulty <= 2 ? 'Throws the game' :
|
||||
selectedDifficulty === 3 ? 'Fully random' :
|
||||
selectedDifficulty <= 5 ? 'Beginner' :
|
||||
selectedDifficulty <= 7 ? 'Intermediate' :
|
||||
selectedDifficulty <= 9 ? 'Advanced' :
|
||||
'Expert'
|
||||
);
|
||||
|
||||
let selectedHandIndex = $state(null);
|
||||
let combatAnimating = $state(false);
|
||||
let lunging = $state(new Set());
|
||||
@@ -69,7 +82,7 @@
|
||||
method: 'POST'
|
||||
});
|
||||
if (!res.ok) {
|
||||
// Server rejected the claim — game may have ended another way
|
||||
// Server rejected the claim. Game may have ended another way
|
||||
const err = await res.json();
|
||||
console.warn('Timeout claim rejected:', err.detail);
|
||||
}
|
||||
@@ -78,8 +91,8 @@
|
||||
$effect(() => {
|
||||
if (!gameState || combatAnimating) return;
|
||||
displayedLife = {
|
||||
[gameState.you.user_id]: gameState.you.life,
|
||||
[gameState.opponent.user_id]: gameState.opponent.life,
|
||||
[gameState.you.user_id]: Math.max(0, gameState.you.life),
|
||||
[gameState.opponent.user_id]: Math.max(0, gameState.opponent.life),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -113,7 +126,7 @@
|
||||
});
|
||||
|
||||
function joinQueue() {
|
||||
if (!selectedDeckId || selectedDeck?.card_count < 20) return;
|
||||
if (!selectedDeckId || selectedDeck?.total_cost === 0 || selectedDeck?.total_cost > 50) return;
|
||||
error = '';
|
||||
phase = 'queuing';
|
||||
queueWs = new WebSocket(`${WS_URL}/ws/queue?deck_id=${selectedDeckId}`);
|
||||
@@ -168,7 +181,7 @@
|
||||
combatAnimating = true;
|
||||
|
||||
// The attacker is whoever was active when end_turn was called.
|
||||
// After end_turn resolves, active_player_id switches — so we look
|
||||
// After end_turn resolves, active_player_id switches. So we look
|
||||
// at who is NOT the current active player to find the attacker,
|
||||
// unless the game just ended (result is set), in which case
|
||||
// active_player_id hasn't switched yet.
|
||||
@@ -267,10 +280,11 @@
|
||||
}
|
||||
|
||||
async function joinSolo() {
|
||||
if (!selectedDeckId || selectedDeck?.card_count < 20) return;
|
||||
if (!selectedDeckId || selectedDeck?.total_cost === 0 || selectedDeck?.total_cost > 50) return;
|
||||
showDifficultyModal = false;
|
||||
error = '';
|
||||
phase = 'queuing';
|
||||
const res = await apiFetch(`${API_URL}/game/solo?deck_id=${selectedDeckId}&difficulty=5`, {
|
||||
const res = await apiFetch(`${API_URL}/game/solo?deck_id=${selectedDeckId}&difficulty=${selectedDifficulty}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -297,16 +311,16 @@
|
||||
<label class="deck-label" for="deck">Choose your deck</label>
|
||||
<select id="deck" bind:value={selectedDeckId}>
|
||||
{#each decks as deck}
|
||||
<option value={deck.id}>{deck.name} ({deck.card_count}/20)</option>
|
||||
<option value={deck.id}>{deck.name} ({deck.card_count} cards, cost {deck.total_cost})</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<p class="error">{selectedDeck && selectedDeck.card_count < 20 ? `Deck must have 20 cards (${selectedDeck.card_count}/20)` : ''}</p>
|
||||
<p class="error">{selectedDeck && (selectedDeck.total_cost === 0 || selectedDeck.total_cost > 50) ? `Deck cost must be between 1 and 50 (current: ${selectedDeck.total_cost})` : ''}</p>
|
||||
<div class="lobby-buttons">
|
||||
<button class="play-btn" onclick={joinQueue} disabled={!selectedDeckId || selectedDeck?.card_count < 20}>
|
||||
<button class="play-btn" onclick={joinQueue} disabled={!selectedDeckId || selectedDeck?.total_cost === 0 || selectedDeck?.total_cost > 50}>
|
||||
Find Opponent
|
||||
</button>
|
||||
<button class="play-btn solo-btn" onclick={joinSolo} disabled={!selectedDeckId || selectedDeck?.card_count < 20}>
|
||||
<button class="play-btn solo-btn" onclick={() => showDifficultyModal = true} disabled={!selectedDeckId || selectedDeck?.total_cost === 0 || selectedDeck?.total_cost > 50}>
|
||||
vs. Computer
|
||||
</button>
|
||||
</div>
|
||||
@@ -320,7 +334,7 @@
|
||||
<button class="cancel-btn" onclick={() => { queueWs?.close(); phase = 'idle'; }}>Cancel</button>
|
||||
</div>
|
||||
|
||||
{:else if phase === 'playing' && gameState}
|
||||
{:else if (phase === 'playing' || (phase === 'ended' && viewingBoard)) && gameState}
|
||||
<div class="game">
|
||||
|
||||
<div class="sidebar left-sidebar">
|
||||
@@ -374,7 +388,7 @@
|
||||
|
||||
<div class="divider">
|
||||
<span class="turn-indicator" class:my-turn={isMyTurn}>
|
||||
{isMyTurn ? 'Your turn' : `${opp.username}'s turn`}
|
||||
{phase === 'ended' ? 'Game Ended' : isMyTurn ? 'Your turn' : `${opp.username}'s turn`}
|
||||
</span>
|
||||
{#if secondsRemaining <= TIMER_WARNING}
|
||||
<span class="turn-timer" class:urgent={secondsRemaining <= 10}>
|
||||
@@ -420,7 +434,9 @@
|
||||
</div>
|
||||
|
||||
<div class="sidebar right-sidebar">
|
||||
{#if isMyTurn && !combatAnimating}
|
||||
{#if phase === 'ended'}
|
||||
<button class="end-turn-btn" onclick={() => viewingBoard = false}>Go Back</button>
|
||||
{:else if isMyTurn && !combatAnimating}
|
||||
<button class="end-turn-btn" onclick={endTurn}>End Turn</button>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -453,7 +469,36 @@
|
||||
{gameState.result.winner_id === myId ? 'Victory' : 'Defeat'}
|
||||
</h1>
|
||||
<p class="lobby-hint">{gameState.result.reason}</p>
|
||||
<button class="play-btn" onclick={() => { gameState = null; phase = 'idle'; }}>Go back</button>
|
||||
<div class="lobby-buttons">
|
||||
<button class="play-btn" onclick={() => viewingBoard = true}>View Board</button>
|
||||
<button class="play-btn" onclick={() => { gameState = null; phase = 'idle'; }}>Go Back</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showDifficultyModal}
|
||||
<div class="modal-backdrop" onclick={() => showDifficultyModal = false}>
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()}>
|
||||
<h2 class="modal-title">Choose Difficulty</h2>
|
||||
<div class="difficulty-display">
|
||||
<span class="difficulty-number">{selectedDifficulty}</span>
|
||||
<span class="difficulty-label">{difficultyLabel}</span>
|
||||
</div>
|
||||
<input
|
||||
class="difficulty-slider"
|
||||
type="range"
|
||||
min="1"
|
||||
max="10"
|
||||
bind:value={selectedDifficulty}
|
||||
/>
|
||||
<div class="difficulty-ticks">
|
||||
<span>1</span><span>10</span>
|
||||
</div>
|
||||
<div class="modal-buttons">
|
||||
<button class="cancel-btn" onclick={() => showDifficultyModal = false}>Cancel</button>
|
||||
<button class="play-btn" onclick={joinSolo}>Start Game</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1040,6 +1085,82 @@
|
||||
.hand-card.unaffordable { filter: grayscale(0.7) brightness(0.55); }
|
||||
.hand-card:disabled { cursor: default; }
|
||||
|
||||
/* ── Difficulty modal ── */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: #110d04;
|
||||
border: 1.5px solid #6b4c1e;
|
||||
border-radius: 10px;
|
||||
padding: 2rem 2.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #f0d080;
|
||||
margin: 0;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.difficulty-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.difficulty-number {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
color: #f0d080;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.difficulty-label {
|
||||
font-family: 'Crimson Text', serif;
|
||||
font-size: 16px;
|
||||
font-style: italic;
|
||||
color: rgba(240, 180, 80, 0.6);
|
||||
}
|
||||
|
||||
.difficulty-slider {
|
||||
width: 100%;
|
||||
accent-color: #c8861a;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.difficulty-ticks {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 10px;
|
||||
color: rgba(240, 180, 80, 0.4);
|
||||
margin-top: -0.75rem;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* ── Toast ── */
|
||||
.toast {
|
||||
position: fixed;
|
||||
|
||||
Reference in New Issue
Block a user