Switch to Svelte
This commit is contained in:
503
frontend/src/components/ScenePhase.svelte
Normal file
503
frontend/src/components/ScenePhase.svelte
Normal file
@@ -0,0 +1,503 @@
|
||||
<script>
|
||||
import { apiRequest } from '../lib/api';
|
||||
|
||||
export let state;
|
||||
|
||||
let playingCard = false;
|
||||
let error = '';
|
||||
|
||||
// Helpers
|
||||
$: hand = state.player.hand_cards ? JSON.parse(state.player.hand_cards) : [];
|
||||
$: isCaptain = state.players.reduce((max, p) => p.rank > max.rank ? p : max, state.players[0])?.id === state.player.id;
|
||||
|
||||
async function playCard(obstacleId, cardCode) {
|
||||
playingCard = true;
|
||||
error = '';
|
||||
try {
|
||||
const res = await apiRequest(`/game/${state.game.id}/player/${state.player.id}/play-card`, 'POST', {
|
||||
obstacle_id: obstacleId,
|
||||
card_code: cardCode
|
||||
});
|
||||
// Removed alert
|
||||
|
||||
} catch(e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
playingCard = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function playJoker(obstacleId, cardCode) {
|
||||
playingCard = true;
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/play-joker`, 'POST', {
|
||||
obstacle_id: obstacleId,
|
||||
card_code: cardCode
|
||||
});
|
||||
} catch(e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
playingCard = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function endScene() {
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/scene/end`, 'POST');
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearObstacle(obstacleId) {
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/scene/obstacle/${obstacleId}/clear`, 'POST');
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleObjective(targetPlayerId, type) {
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${targetPlayerId}/objective/toggle`, 'POST', {
|
||||
type: type
|
||||
});
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
let showEventLog = false;
|
||||
|
||||
let newNameInput = '';
|
||||
async function submitNewName() {
|
||||
if (!newNameInput.trim()) return;
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/set-name`, 'POST', {
|
||||
new_name: newNameInput.trim()
|
||||
});
|
||||
newNameInput = '';
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing card helpers
|
||||
function getCardValue(code) {
|
||||
if (!code) return 0;
|
||||
if (code === 'JOKER') return 0;
|
||||
let rank = code.slice(0, -1);
|
||||
if (['J', 'Q', 'K'].includes(rank)) return 10;
|
||||
if (rank === 'A') return 1;
|
||||
return parseInt(rank);
|
||||
}
|
||||
|
||||
function getCardDisplay(code) {
|
||||
if (!code) return "";
|
||||
if (code === 'JOKER') return "🃏 JOKER";
|
||||
let rank = code.slice(0, -1);
|
||||
let suit = code.slice(-1);
|
||||
let suitEmoji = { 'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️' }[suit] || suit;
|
||||
return `${rank}${suitEmoji}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="scene-view-layout" id="scene-layout-container" data-game-id={state.game.id} data-player-id={state.player.id}>
|
||||
<!-- LEFT COLUMN: The Shared Table -->
|
||||
<div class="table-column">
|
||||
|
||||
<!-- Game Deck and Obstacles Roster -->
|
||||
<div class="card glass-panel obstacle-list-card">
|
||||
<div class="card-header">
|
||||
<h3>🌊 The Obstacle List</h3>
|
||||
<span class="deck-counter">🎴 Deck: {state.game.deck_cards ? JSON.parse(state.game.deck_cards).length : 0} cards left</span>
|
||||
</div>
|
||||
|
||||
<div class="obstacles-container" id="scene-obstacles-container">
|
||||
<!-- TOP STATUS BAR: Scene Info and Captain -->
|
||||
<div class="scene-status-banner glass-panel" style="margin-bottom: 1.5rem; padding: 1rem; background: rgba(7, 11, 18, 0.6); border: 1px solid rgba(212, 175, 55, 0.2); border-radius: 8px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
|
||||
<!-- Captain Badge -->
|
||||
<div>
|
||||
{#if isCaptain}
|
||||
<span class="captain-badge" style="background: rgba(212, 175, 55, 0.15); border: 1px solid var(--gold); color: var(--gold); padding: 0.4rem 1rem; border-radius: 20px; font-size: 1rem; font-weight: 700; font-family: var(--font-heading); display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||
🏴☠️ Captain: {state.players.find(p => p.id === state.player.id)?.name} (Rank {state.players.find(p => p.id === state.player.id)?.rank})
|
||||
</span>
|
||||
{:else}
|
||||
<span class="captain-badge" style="background: rgba(255, 255, 255, 0.05); border: 1px dashed rgba(255, 255, 255, 0.2); color: var(--text-muted); padding: 0.4rem 1rem; border-radius: 20px; font-size: 1rem; display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||
🏴☠️ Captain: {state.players.reduce((max, p) => p.rank > max.rank ? p : max, state.players[0])?.name || 'None'}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Active Scene Roster Mini-list -->
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
|
||||
<span style="font-size: 0.85rem; color: var(--text-muted); font-family: var(--font-heading); margin-right: 0.25rem;">Active Roster:</span>
|
||||
{#each state.players as p}
|
||||
{#if p.role === 'deep'}
|
||||
<span class="player-chip deep-chip" style="font-size: 0.8rem; padding: 0.25rem 0.6rem; border-radius: 12px; margin: 0; display: inline-flex; align-items: center; gap: 0.25rem;">
|
||||
🌊 {p.name} (Deep)
|
||||
</span>
|
||||
{:else if p.role === 'pirat'}
|
||||
<span class="player-chip pirat-chip" style="font-size: 0.8rem; padding: 0.25rem 0.6rem; border-radius: 12px; margin: 0; display: inline-flex; align-items: center; gap: 0.25rem; {p.is_ghost ? 'border-color: #7890a8; color: #a0b0c0; background: rgba(120, 150, 180, 0.1);' : ''} {p.is_dead ? 'border-color: var(--red-suit); color: var(--red-suit); background: rgba(255, 42, 95, 0.1);' : ''} {p.id === state.players.reduce((max, p2) => p2.rank > max.rank ? p2 : max, state.players[0])?.id && !p.is_ghost && !p.is_dead ? 'border-color: var(--gold); color: var(--gold); background: rgba(212, 175, 55, 0.1);' : ''}">
|
||||
{#if p.is_ghost}👻{:else if p.is_dead}💀{:else}🐀{/if} {p.name} (Rank {p.rank}){p.id === state.players.reduce((max, p2) => p2.rank > max.rank ? p2 : max, state.players[0])?.id && !p.is_ghost && !p.is_dead ? ' ⭐' : ''}
|
||||
</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-danger">{error}</div>
|
||||
{/if}
|
||||
|
||||
{#each state.obstacles as obs}
|
||||
{@const column_cards = JSON.parse(obs.played_cards || '[]')}
|
||||
{@const active_card_code = column_cards.length > 0 ? column_cards[column_cards.length - 1].card : obs.original_card}
|
||||
{@const success_count = column_cards.filter(c => c.success).length}
|
||||
{@const is_completed = success_count >= state.players.filter(p => p.role !== 'deep').length}
|
||||
<div class="obstacle-item suit-{obs.suit.toLowerCase()} {is_completed ? 'completed' : ''}"
|
||||
data-obstacle-id={obs.id}
|
||||
on:dragover={(e) => { if (!is_completed) e.preventDefault(); }}
|
||||
on:dragenter={(e) => { if (!is_completed) e.currentTarget.classList.add("drag-over"); }}
|
||||
on:dragleave={(e) => { if (!is_completed) e.currentTarget.classList.remove("drag-over"); }}
|
||||
on:drop={(e) => {
|
||||
if (is_completed) return;
|
||||
e.preventDefault();
|
||||
e.currentTarget.classList.remove("drag-over");
|
||||
const cardCode = e.dataTransfer.getData('text/plain');
|
||||
if (cardCode) {
|
||||
if (cardCode === 'JOKER') playJoker(obs.id, cardCode);
|
||||
else playCard(obs.id, cardCode);
|
||||
}
|
||||
}}>
|
||||
<div class="obstacle-card-wrapper" style="display: flex; justify-content: center; align-items: center;">
|
||||
<div class="card-medium suit-{active_card_code.slice(-1).toLowerCase()} {active_card_code === 'JOKER' ? 'joker-card' : ''}" title="Active Card: {getCardDisplay(active_card_code)}">
|
||||
<div class="card-corner top-left">
|
||||
<span class="val">{active_card_code === 'JOKER' ? '🃏' : active_card_code.slice(0, -1)}</span>
|
||||
<span class="suit">{active_card_code === 'JOKER' ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[active_card_code.slice(-1)]}</span>
|
||||
</div>
|
||||
|
||||
<div class="card-center">
|
||||
{#if active_card_code === 'JOKER'}
|
||||
🃏
|
||||
{:else}
|
||||
<span class="giant-symbol" style="font-size: 1.5rem;">{{'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[active_card_code.slice(-1)]}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="card-corner bottom-right">
|
||||
<span class="val">{active_card_code === 'JOKER' ? '🃏' : active_card_code.slice(0, -1)}</span>
|
||||
<span class="suit">{active_card_code === 'JOKER' ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[active_card_code.slice(-1)]}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="obstacle-details">
|
||||
<h4>{obs.title}</h4>
|
||||
<p class="desc">{obs.description}</p>
|
||||
</div>
|
||||
|
||||
<!-- Value Display -->
|
||||
<div class="obstacle-value-display text-center">
|
||||
<span class="val-label">Current Difficulty</span>
|
||||
<span class="val-number">
|
||||
{#if ['J', 'Q', 'K'].includes(obs.original_card.slice(0, -1))}
|
||||
Rank ({state.player.rank})
|
||||
{:else}
|
||||
{obs.current_value}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Successes Tracker -->
|
||||
<div class="obstacle-successes-display text-center">
|
||||
<span class="val-label">Successes</span>
|
||||
<span class="val-number" style="font-size: 1.5rem;">
|
||||
{success_count} / {state.players.filter(p => p.role !== 'deep').length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Played Cards Column -->
|
||||
<div class="played-column">
|
||||
<h5>Card History (Column)</h5>
|
||||
<div class="column-cards-flex">
|
||||
<div class="card-mini base-card">
|
||||
<span class="val">{obs.original_card.slice(0, -1)}</span>
|
||||
<span class="suit">{{'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[obs.original_card.slice(-1)]}</span>
|
||||
</div>
|
||||
{#each column_cards as pc}
|
||||
<div class="card-mini played-card rotated {pc.success ? 'success' : 'failure'}"
|
||||
title="{pc.player_name} played {getCardDisplay(pc.card)}">
|
||||
<span class="val">{pc.card === 'JOKER' ? '🃏' : pc.card.slice(0, -1)}</span>
|
||||
<span class="suit">{pc.card === 'JOKER' ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[pc.card.slice(-1)]}</span>
|
||||
<span class="owner">{pc.player_name.slice(0,4)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if is_completed && state.player.role === 'deep'}
|
||||
<div class="text-center" style="margin-top: 1rem;">
|
||||
<button class="btn btn-success" on:click={() => clearObstacle(obs.id)} style="width: 100%; border: 1px solid rgba(255,255,255,0.2);">Clear Obstacle</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="empty-text">No active obstacles. Deep players can end the scene!</p>
|
||||
{/each}
|
||||
|
||||
<!-- Event Log Removed from inline layout -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN: Private Hand & Character Sheet -->
|
||||
<div class="private-column">
|
||||
<!-- DEEP CONTROLS: End Scene & Objectives -->
|
||||
{#if state.player.role === "deep"}
|
||||
<div class="card glass-panel deep-panel-card" style="max-height: 80vh; overflow-y: auto;">
|
||||
<h3 class="deep-text text-center">🌊 Deep Control Panel</h3>
|
||||
|
||||
<div class="sheet-group margin-top">
|
||||
<h4 style="color: var(--gold);">Crew Objectives</h4>
|
||||
<div class="objectives-checklist">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={state.game.completed_crew_1} on:change={() => toggleObjective(state.player.id, 'crew_1')}>
|
||||
<span>Steal a Ship</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={state.game.completed_crew_2} on:change={() => toggleObjective(state.player.id, 'crew_2')}>
|
||||
<span>Choose a Captain</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={state.game.completed_crew_3} on:change={() => toggleObjective(state.player.id, 'crew_3')}>
|
||||
<span>Commit Piracy</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sheet-group margin-top">
|
||||
<h4 style="color: var(--gold);">Pi-Rat Personal Objectives</h4>
|
||||
<p class="info-text">You control completion. Only mark in sequence (1 -> 2 -> 3).</p>
|
||||
{#each state.players.filter(p => p.role === 'pirat') as p}
|
||||
<div style="background: rgba(0,0,0,0.2); padding: 0.5rem; border-radius: 4px; margin-bottom: 0.5rem;">
|
||||
<strong>{p.name}</strong>
|
||||
<div class="objectives-checklist" style="margin-top: 0.25rem;">
|
||||
<label class="checkbox-label"><input type="checkbox" checked={p.completed_personal_1} on:change={() => toggleObjective(p.id, 'personal_1')}> <span>1. Gat</span></label>
|
||||
<label class="checkbox-label"><input type="checkbox" checked={p.completed_personal_2} on:change={() => toggleObjective(p.id, 'personal_2')}> <span>2. Name</span></label>
|
||||
<label class="checkbox-label"><input type="checkbox" checked={p.completed_personal_3} on:change={() => toggleObjective(p.id, 'personal_3')}> <span>3. Die/Retire</span></label>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- End Scene Button -->
|
||||
<div class="scene-upkeep-controls text-center" style="margin-top: 1rem; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1rem;">
|
||||
<p class="info-text">Conclude the scene once players have made attempts or the obstacle list is depleted.</p>
|
||||
<button on:click={endScene} class="btn btn-danger btn-large btn-full glow-effect">
|
||||
End Scene & Proceed to Ranking
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Player Hand Card -->
|
||||
<div class="card glass-panel hand-card">
|
||||
<h3>🃏 Your Hand</h3>
|
||||
<p class="section-desc">Keep your cards secret! {#if state.player.role !== "deep"}Drag a card onto an active obstacle to play it.{/if}</p>
|
||||
|
||||
<div class="hand-flex">
|
||||
{#each hand as card}
|
||||
<div class="card-large suit-{card.slice(-1).toLowerCase()} {card === 'JOKER' ? 'joker-card' : ''}"
|
||||
draggable={state.player.role !== "deep"}
|
||||
on:dragstart={(e) => {
|
||||
e.dataTransfer.setData('text/plain', card);
|
||||
e.currentTarget.classList.add("dragging");
|
||||
}}
|
||||
on:dragend={(e) => {
|
||||
e.currentTarget.classList.remove("dragging");
|
||||
}}>
|
||||
<div class="card-corner top-left">
|
||||
<span class="val">{card === 'JOKER' ? '🃏' : card.slice(0, -1)}</span>
|
||||
<span class="suit">{card === 'JOKER' ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
|
||||
</div>
|
||||
|
||||
<div class="card-center">
|
||||
{#if card === 'JOKER'}
|
||||
🃏
|
||||
{:else}
|
||||
<span class="giant-symbol">{{'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="card-tech-overlay">
|
||||
{#if card.slice(0, -1) === "J"}
|
||||
<div class="tech-tag" title="Automatic success when played">J: "{state.player.tech_jack}"</div>
|
||||
{:else if card.slice(0, -1) === "Q"}
|
||||
<div class="tech-tag" title="Automatic success when played">Q: "{state.player.tech_queen}"</div>
|
||||
{:else if card.slice(0, -1) === "K"}
|
||||
<div class="tech-tag" title="Automatic success when played">K: "{state.player.tech_king}"</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="card-corner bottom-right">
|
||||
<span class="val">{card === 'JOKER' ? '🃏' : card.slice(0, -1)}</span>
|
||||
<span class="suit">{card === 'JOKER' ? '' : {'C': '♣️', 'S': '♠️', 'H': '♥️', 'D': '♦️'}[card.slice(-1)]}</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="empty-text text-center">Your hand is empty! (You draw cards when playing cards that match the suit color of the obstacle.)</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player Character Sheet Card -->
|
||||
{#if state.player.role !== "deep"}
|
||||
<div class="card glass-panel sheet-card">
|
||||
<h3>🐀 {state.player.name}'s Character Sheet</h3>
|
||||
|
||||
<div class="character-sheet-details">
|
||||
{#if state.player.is_dead}
|
||||
<div class="sheet-group prompt-box" style="background: rgba(255, 42, 95, 0.1); border: 1px dashed var(--red-suit); padding: 1rem; border-radius: 8px; margin-bottom: 1rem; text-align: center;">
|
||||
<h4 style="color: var(--red-suit); margin-top: 0; font-family: var(--font-heading); font-size: 1.1rem; font-weight: 700;">💀 You have Died / Retired!</h4>
|
||||
<p class="info-text" style="margin-bottom: 0; font-size: 0.9rem;">Your story arc is complete. You will choose to become a Ghost or roll a new character during the upkeep phase between scenes.</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if state.player.is_ghost}
|
||||
<div class="sheet-group prompt-box" style="background: rgba(120, 150, 180, 0.1); border: 1px dashed #7890a8; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; text-align: center;">
|
||||
<h4 style="color: #a0b0c0; margin-top: 0; font-family: var(--font-heading); font-size: 1.1rem; font-weight: 700;">👻 Playing as a Ghost</h4>
|
||||
<p class="info-text" style="margin-bottom: 0; font-size: 0.9rem;">You are in the Ghost World (grayscale view). You cannot interact well with the living but you can still help them resolve challenges!</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if state.player.needs_name}
|
||||
<div class="sheet-group prompt-box" style="background: rgba(212, 175, 55, 0.1); border: 1px dashed var(--gold); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
|
||||
<h4 style="color: var(--gold); margin-top: 0;">🏴☠️ You Earned a Name!</h4>
|
||||
<p class="info-text" style="margin-bottom: 0.5rem;">You completed your second objective and ranked up! What is your new Pirate Name?</p>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<input type="text" bind:value={newNameInput} class="form-control" placeholder="Enter new name...">
|
||||
<button class="btn btn-gold" on:click={submitNewName} disabled={!newNameInput.trim()}>Claim Name</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="sheet-group">
|
||||
<h4>Description:</h4>
|
||||
<p><strong>Look:</strong> {state.player.avatar_look}</p>
|
||||
<p><strong>Smell:</strong> {state.player.avatar_smell}</p>
|
||||
<p><strong>First Words:</strong> "{state.player.first_words}"</p>
|
||||
</div>
|
||||
|
||||
<div class="sheet-group margin-top">
|
||||
<h4>Assigned Secret Techniques (J/Q/K):</h4>
|
||||
<ul class="techniques-list-sheet">
|
||||
<li><strong>Jack (J):</strong> "{state.player.tech_jack}"</li>
|
||||
<li><strong>Queen (Q):</strong> "{state.player.tech_queen}"</li>
|
||||
<li><strong>King (K):</strong> "{state.player.tech_king}"</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sheet-group margin-top">
|
||||
<h4 style="color: var(--gold);">Crew Objectives</h4>
|
||||
<div class="objectives-checklist">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={state.game.completed_crew_1} disabled>
|
||||
<span>Steal a Ship</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={state.game.completed_crew_2} disabled>
|
||||
<span>Choose a Captain</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={state.game.completed_crew_3} disabled>
|
||||
<span>Commit Piracy</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sheet-group margin-top">
|
||||
<h4 style="color: var(--gold);">Personal Objectives</h4>
|
||||
<div class="objectives-checklist">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={state.player.completed_personal_1} disabled>
|
||||
<span>1. Gat</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={state.player.completed_personal_2} disabled>
|
||||
<span>2. Name</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={state.player.completed_personal_3} disabled>
|
||||
<span>3. Die/Retire</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Event Log -->
|
||||
<div class="floating-event-log {showEventLog ? 'open' : ''}">
|
||||
<button class="toggle-log-btn" on:click={() => showEventLog = !showEventLog}>
|
||||
{showEventLog ? '⬇️ Hide Log' : '📜 Event Log'}
|
||||
</button>
|
||||
|
||||
{#if showEventLog}
|
||||
<div class="log-content font-mono text-sm space-y-2 p-2">
|
||||
{#each state.events as event}
|
||||
<div class="p-2 border-l-2 border-gray-600 bg-black text-gray-400" style="border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 0.25rem;">
|
||||
{event.message}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-2 text-gray-500">No events yet.</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.floating-event-log {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 350px;
|
||||
background: rgba(10, 15, 25, 0.95);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 60vh;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.toggle-log-btn {
|
||||
background: var(--dark-bg, #1a1a2e);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
border-radius: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.toggle-log-btn:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
.log-content {
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
.floating-event-log:not(.open) {
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user