Let players dismiss completed duel feedback
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] Add an online indicator that displays if a player is connected
|
- [x] Add an online indicator that displays if a player is connected
|
||||||
- [x] PvP challenges should linger in the UI after completion so that players can see the result
|
- [x] PvP challenges should linger in the UI after completion so that players can see the result, then dismiss it for themselves
|
||||||
- [x] Add hat/gun icons next to player names on the player list to indicate if they're the captain and/or have a gat
|
- [x] Add hat/gun icons next to player names on the player list to indicate if they're the captain and/or have a gat
|
||||||
- [x] Replace red/green borders of successful/failed cards with checks and crosses in the upper corner of the card.
|
- [x] Replace red/green borders of successful/failed cards with checks and crosses in the upper corner of the card.
|
||||||
- [ ] Add a note taking area to store game state between sessions
|
- [ ] Add a note taking area to store game state between sessions
|
||||||
|
|||||||
@@ -11,12 +11,31 @@
|
|||||||
// The inline Event Log can be toggled from a fixed corner button to declutter.
|
// The inline Event Log can be toggled from a fixed corner button to declutter.
|
||||||
let logOpen = true;
|
let logOpen = true;
|
||||||
|
|
||||||
|
let dismissedDuelIds = [];
|
||||||
|
$: dismissalKey = `dismissed-duels:${state.game.id}:${state.player.id}:${state.game.current_scene_number}`;
|
||||||
|
$: dismissedDuelIds = loadDismissals(dismissalKey);
|
||||||
|
|
||||||
|
function loadDismissals(key) {
|
||||||
|
try {
|
||||||
|
const ids = JSON.parse(localStorage.getItem(key) || '[]');
|
||||||
|
return Array.isArray(ids) ? ids : [];
|
||||||
|
} catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissDuel(id) {
|
||||||
|
dismissedDuelIds = [...dismissedDuelIds, id];
|
||||||
|
try { localStorage.setItem(dismissalKey, JSON.stringify(dismissedDuelIds)); } catch { /* Still dismiss when storage is unavailable. */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
$: completedDuels = (state.challenges || []).filter(c =>
|
||||||
|
c.challenge_type === 'pvp' && ['succeeded', 'failed'].includes(c.status) && !dismissedDuelIds.includes(c.id)
|
||||||
|
);
|
||||||
$: hand = state.player.hand_cards ? JSON.parse(state.player.hand_cards) : [];
|
$: hand = state.player.hand_cards ? JSON.parse(state.player.hand_cards) : [];
|
||||||
$: playerTechs = { J: state.player.tech_jack, Q: state.player.tech_queen, K: state.player.tech_king };
|
$: playerTechs = { J: state.player.tech_jack, Q: state.player.tech_queen, K: state.player.tech_king };
|
||||||
$: isDeep = state.player.role === 'deep';
|
$: isDeep = state.player.role === 'deep';
|
||||||
$: openChallenges = (state.challenges || []).filter(c => c.status === 'open');
|
$: openChallenges = (state.challenges || []).filter(c => c.status === 'open');
|
||||||
// Keep duel results visible to the whole table for the rest of the scene.
|
// Keep results visible until this player dismisses them.
|
||||||
$: showChallengeArea = isDeep || openChallenges.length > 0 || (state.challenges || []).some(c => c.challenge_type === 'pvp' && ['succeeded', 'failed'].includes(c.status));
|
$: showChallengeArea = isDeep || openChallenges.length > 0 || completedDuels.length > 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="scene-view-layout" class:log-collapsed={!logOpen} id="scene-layout-container" data-game-id={state.game.id} data-player-id={state.player.id}>
|
<div class="scene-view-layout" class:log-collapsed={!logOpen} id="scene-layout-container" data-game-id={state.game.id} data-player-id={state.player.id}>
|
||||||
@@ -50,7 +69,7 @@
|
|||||||
|
|
||||||
{#if showChallengeArea}
|
{#if showChallengeArea}
|
||||||
<div class="card glass-panel challenge-area-card">
|
<div class="card glass-panel challenge-area-card">
|
||||||
<ChallengePanel {state} />
|
<ChallengePanel {state} {completedDuels} on:dismissDuel={(event) => dismissDuel(event.detail)} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { apiRequest } from '../../lib/api';
|
import { apiRequest } from '../../lib/api';
|
||||||
import { getCardDisplay, isJoker, displayName, playerName as lookupName, cardTooltipHtml, obstacleTable } from '../../lib/cards';
|
import { getCardDisplay, isJoker, displayName, playerName as lookupName, cardTooltipHtml, obstacleTable } from '../../lib/cards';
|
||||||
import { tooltip } from '../../lib/tooltip';
|
import { tooltip } from '../../lib/tooltip';
|
||||||
import ObstacleItem from './ObstacleItem.svelte';
|
import ObstacleItem from './ObstacleItem.svelte';
|
||||||
|
|
||||||
export let state;
|
export let state;
|
||||||
|
export let completedDuels = [];
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let error = '';
|
let error = '';
|
||||||
let taxTargetId = '';
|
let taxTargetId = '';
|
||||||
@@ -17,7 +20,6 @@
|
|||||||
|
|
||||||
$: hand = state.player.hand_cards ? JSON.parse(state.player.hand_cards) : [];
|
$: hand = state.player.hand_cards ? JSON.parse(state.player.hand_cards) : [];
|
||||||
$: openChallenges = (state.challenges || []).filter(c => c.status === 'open');
|
$: openChallenges = (state.challenges || []).filter(c => c.status === 'open');
|
||||||
$: completedDuels = (state.challenges || []).filter(c => c.challenge_type === 'pvp' && ['succeeded', 'failed'].includes(c.status));
|
|
||||||
$: challengedObstacleIds = new Set(openChallenges.flatMap(c => JSON.parse(c.obstacle_ids || '[]')));
|
$: challengedObstacleIds = new Set(openChallenges.flatMap(c => JSON.parse(c.obstacle_ids || '[]')));
|
||||||
$: myTaxRequest = openChallenges.find(c => c.tax_state === 'requested' && c.tax_target_id === state.player.id) || null;
|
$: myTaxRequest = openChallenges.find(c => c.tax_state === 'requested' && c.tax_target_id === state.player.id) || null;
|
||||||
$: myPvpDefense = openChallenges.find(c => c.challenge_type === 'pvp' && c.acting_player_id === state.player.id) || null;
|
$: myPvpDefense = openChallenges.find(c => c.challenge_type === 'pvp' && c.acting_player_id === state.player.id) || null;
|
||||||
@@ -185,10 +187,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<!-- Results remain available until the next scene, including after a reload. -->
|
<!-- Each player clears their own result feedback after reading it. -->
|
||||||
{#each completedDuels as ch (ch.id)}
|
{#each completedDuels as ch (ch.id)}
|
||||||
<div class="challenge-item duel-result">
|
<div class="challenge-item duel-result">
|
||||||
|
<div class="challenge-head">
|
||||||
<h4>⚔️ Duel: {playerName(ch.challenger_player_id)} vs {playerName(ch.target_player_id)}</h4>
|
<h4>⚔️ Duel: {playerName(ch.challenger_player_id)} vs {playerName(ch.target_player_id)}</h4>
|
||||||
|
<button class="btn btn-secondary btn-small" title="Dismiss this result for you" on:click={() => dispatch('dismissDuel', ch.id)}>Dismiss</button>
|
||||||
|
</div>
|
||||||
<p class="duel-outcome">
|
<p class="duel-outcome">
|
||||||
{ch.status === 'succeeded' ? '✓' : '✕'}
|
{ch.status === 'succeeded' ? '✓' : '✕'}
|
||||||
<strong>{playerName(ch.acting_player_id)} {ch.status === 'succeeded' ? 'won the defense' : 'lost the defense'}.</strong>
|
<strong>{playerName(ch.acting_player_id)} {ch.status === 'succeeded' ? 'won the defense' : 'lost the defense'}.</strong>
|
||||||
|
|||||||
@@ -6,10 +6,11 @@
|
|||||||
// - Add a CHANGELOG entry only when a commit changes something players can
|
// - Add a CHANGELOG entry only when a commit changes something players can
|
||||||
// see. Skip refactors, tests, and tooling. Keep wording player-facing.
|
// see. Skip refactors, tests, and tooling. Keep wording player-facing.
|
||||||
|
|
||||||
export const VERSION = 40;
|
export const VERSION = 41;
|
||||||
|
|
||||||
// Newest first. Each entry: { version, date: 'YYYY-MM-DD', changes: [string, ...] }.
|
// Newest first. Each entry: { version, date: 'YYYY-MM-DD', changes: [string, ...] }.
|
||||||
export const CHANGELOG = [
|
export const CHANGELOG = [
|
||||||
|
{ version: 41, date: '2026-09-04', changes: ['Dismiss a completed duel once you have read the result. Other players can still read it, and dismissed results stay hidden after a reload.'] },
|
||||||
{ version: 40, date: '2026-09-04', changes: ['Played cards show a check or cross in the upper corner instead of a green or red outline, so results remain clear in stacked cards.'] },
|
{ version: 40, date: '2026-09-04', changes: ['Played cards show a check or cross in the upper corner instead of a green or red outline, so results remain clear in stacked cards.'] },
|
||||||
{ version: 39, date: '2026-09-04', changes: ['Completed duels stay visible for the rest of the scene, showing the winner, both cards, and how the defense resolved.'] },
|
{ version: 39, date: '2026-09-04', changes: ['Completed duels stay visible for the rest of the scene, showing the winner, both cards, and how the defense resolved.'] },
|
||||||
{ version: 38, date: '2026-09-04', changes: ['The crew roster shows who is online, with hat and gun icons for the Captain and Pi-Rats who have a Gat.'] },
|
{ version: 38, date: '2026-09-04', changes: ['The crew roster shows who is online, with hat and gun icons for the Captain and Pi-Rats who have a Gat.'] },
|
||||||
|
|||||||
Reference in New Issue
Block a user