Simple tasks: Tooltips and UI Layout
Completed tasks: 'PvP challenge tooltips', 'Secret technique tooltips', and 'Character sheet layout' from TODO.md
This commit is contained in:
6
TODO.md
6
TODO.md
@@ -2,9 +2,9 @@
|
||||
|
||||
- [x] **Event log items**. Completing a crew objective should go in the event log
|
||||
- [x] **Personal objectives for pi-rats** Mousing over a personal objective as a pi-rat should not make the cursor change, as it's a non-interactive UI element for them
|
||||
- [ ] **PvP challenge tooltips** Update the PvP challenge card selection dropdown tooltips so that they describe the general class of obstacles for the suit rather than the specific challenge for that card. This also goes for the temporary obstacles created by PvP challenges.
|
||||
- [ ] **Secret technique tooltips** The tooltip for a played secret technique should include the full secret technique description, since it gets truncated in the main display.
|
||||
- [ ] **Character sheet layout** Personal objectives should be on a second column to the right, on screen layouts wide enough to accommodate it
|
||||
- [x] **PvP challenge tooltips** Update the PvP challenge card selection dropdown tooltips so that they describe the general class of obstacles for the suit rather than the specific challenge for that card. This also goes for the temporary obstacles created by PvP challenges.
|
||||
- [x] **Secret technique tooltips** The tooltip for a played secret technique should include the full secret technique description, since it gets truncated in the main display.
|
||||
- [x] **Character sheet layout** Personal objectives should be on a second column to the right, on screen layouts wide enough to accommodate it
|
||||
|
||||
## DevOps/Maintenance
|
||||
|
||||
|
||||
@@ -123,6 +123,24 @@
|
||||
/* --- Character sheet layout --- */
|
||||
.character-sheet-details {
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.character-sheet-details {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.sheet-main-column {
|
||||
flex: 2;
|
||||
min-width: 0;
|
||||
}
|
||||
.sheet-side-column {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.techniques-list-sheet {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
{#if techName}
|
||||
<div class="card-tech-overlay">
|
||||
<div class="tech-tag" use:tooltip={"Automatic success when played"}>{cardValue(card)}: "{techName}"</div>
|
||||
<div class="tech-tag" use:tooltip={`Automatic success when played\n\n"${techName}"`}>{cardValue(card)}: "{techName}"</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card-corner bottom-right">
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
{/if}
|
||||
{#if ch.challenge_type === 'pvp'}
|
||||
<p class="info-text" style="margin: 0.5rem 0 0 0;">
|
||||
Temporary Obstacle: <strong use:tooltip={{ html: cardTooltipHtml(ch.temp_card, $obstacleTable) }}>{getCardDisplay(ch.temp_card)}</strong> — {playerName(ch.acting_player_id)} must answer it!
|
||||
Temporary Obstacle: <strong use:tooltip={{ html: cardTooltipHtml(ch.temp_card, $obstacleTable, true) }}>{getCardDisplay(ch.temp_card)}</strong> — {playerName(ch.acting_player_id)} must answer it!
|
||||
</p>
|
||||
{:else}
|
||||
<div class="challenge-obstacles">
|
||||
@@ -173,7 +173,7 @@
|
||||
<p style="margin: 0 0 0.5rem 0;"><strong>Defend yourself!</strong> Pick a card:</p>
|
||||
<div class="challenge-actions">
|
||||
{#each hand.filter(c => !isJoker(c)) as card}
|
||||
<button class="btn btn-secondary" use:tooltip={{ html: cardTooltipHtml(card, $obstacleTable) }} on:click={() => pvpDefend(ch.id, card)}>{getCardDisplay(card)}</button>
|
||||
<button class="btn btn-secondary" use:tooltip={{ html: cardTooltipHtml(card, $obstacleTable, true) }} on:click={() => pvpDefend(ch.id, card)}>{getCardDisplay(card)}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { apiRequest } from '../../lib/api';
|
||||
import { getCardDisplay, isJoker, displayName, crewLabel, captainName, cardObstacleInfo, obstacleTable } from '../../lib/cards';
|
||||
import { getCardDisplay, isJoker, displayName, crewLabel, captainName, cardObstacleInfo, pvpObstacleInfo, obstacleTable } from '../../lib/cards';
|
||||
|
||||
export let state;
|
||||
export let target; // the player whose sheet is being viewed
|
||||
@@ -26,7 +26,7 @@
|
||||
&& viewer.role === 'pirat' && !viewer.is_dead && !viewer.needs_reroll
|
||||
&& target.role === 'pirat' && !target.is_dead;
|
||||
// What the chosen duel card becomes as a temporary Obstacle for the defender
|
||||
$: pvpCardObstacle = cardObstacleInfo(pvpCard, $obstacleTable);
|
||||
$: pvpCardObstacle = pvpObstacleInfo(pvpCard);
|
||||
|
||||
function getPlayerName(id) {
|
||||
if (!id) return 'a crewmate';
|
||||
@@ -86,6 +86,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="character-sheet-details">
|
||||
<div class="sheet-main-column">
|
||||
{#if error}
|
||||
<div class="alert alert-danger">{error}</div>
|
||||
{/if}
|
||||
@@ -159,7 +160,7 @@
|
||||
<select class="select-field" bind:value={pvpCard} style="width: 100%; margin-bottom: 0.5rem;">
|
||||
<option value="">Throw which card?</option>
|
||||
{#each hand.filter(c => !isJoker(c)) as card}
|
||||
<option value={card} title={cardObstacleInfo(card, $obstacleTable) ? `${cardObstacleInfo(card, $obstacleTable).title} — ${cardObstacleInfo(card, $obstacleTable).description}` : ''}>{getCardDisplay(card)}</option>
|
||||
<option value={card} title={pvpObstacleInfo(card) ? `${pvpObstacleInfo(card).title} — ${pvpObstacleInfo(card).description}` : ''}>{getCardDisplay(card)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if pvpCardObstacle}
|
||||
@@ -183,27 +184,30 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div> <!-- end sheet-main-column -->
|
||||
|
||||
<div class="sheet-group margin-top">
|
||||
<h4>Personal Objectives</h4>
|
||||
{#if canManageObjectives}
|
||||
<p class="info-text" style="font-size: 0.8rem;">As the Deep, tick these off in sequence (1 → 2 → 3).</p>
|
||||
{/if}
|
||||
<div class="objectives-checklist">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={target.completed_personal_1} disabled={!canManageObjectives} on:change={() => toggleObjective('personal_1')}>
|
||||
<span>1. Gat</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={target.completed_personal_2} disabled={!canManageObjectives} on:change={() => toggleObjective('personal_2')}>
|
||||
<span>2. Name</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={target.completed_personal_3} disabled={!canManageObjectives} on:change={() => toggleObjective('personal_3')}>
|
||||
<span>3. Die/Retire</span>
|
||||
</label>
|
||||
<div class="sheet-side-column">
|
||||
<div class="sheet-group">
|
||||
<h4>Personal Objectives</h4>
|
||||
{#if canManageObjectives}
|
||||
<p class="info-text" style="font-size: 0.8rem;">As the Deep, tick these off in sequence (1 → 2 → 3).</p>
|
||||
{/if}
|
||||
<div class="objectives-checklist">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={target.completed_personal_1} disabled={!canManageObjectives} on:change={() => toggleObjective('personal_1')}>
|
||||
<span>1. Gat</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={target.completed_personal_2} disabled={!canManageObjectives} on:change={() => toggleObjective('personal_2')}>
|
||||
<span>2. Name</span>
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" checked={target.completed_personal_3} disabled={!canManageObjectives} on:change={() => toggleObjective('personal_3')}>
|
||||
<span>3. Die/Retire</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- end sheet-side-column -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ function esc(s) {
|
||||
// it means when it surfaces as an Obstacle. Built only from static rulebook
|
||||
// data (SUIT_THEMES + the obstacle table), so the HTML is trusted. Feed the
|
||||
// result to the `tooltip` action as `{ html: cardTooltipHtml(...) }`.
|
||||
export function cardTooltipHtml(code, table = OBSTACLE_TABLE) {
|
||||
export function cardTooltipHtml(code, table = OBSTACLE_TABLE, isPvp = false) {
|
||||
if (!code) return '';
|
||||
if (isJoker(code)) {
|
||||
return '<div class="tt-head"><span class="tt-card tt-joker">🃏 Joker</span></div>'
|
||||
@@ -83,7 +83,14 @@ export function cardTooltipHtml(code, table = OBSTACLE_TABLE) {
|
||||
let html = `<div class="tt-head"><span class="tt-card ${colorClass}">${esc(val)}${SUIT_EMOJI[suit] || esc(suit)}</span>`
|
||||
+ `<span class="tt-color tt-color-${isRed ? 'red' : 'black'}">${isRed ? 'Red' : 'Black'}</span></div>`;
|
||||
html += `<div class="tt-theme">${esc(SUIT_THEMES[suit] || '')}</div>`;
|
||||
if (FACE_VALUES.includes(val)) {
|
||||
|
||||
if (isPvp) {
|
||||
const themeStr = SUIT_THEMES[suit] || '';
|
||||
const splitIdx = themeStr.indexOf(': ');
|
||||
const title = splitIdx !== -1 ? themeStr.substring(0, splitIdx) : themeStr;
|
||||
const desc = splitIdx !== -1 ? themeStr.substring(splitIdx + 2) : '';
|
||||
html += `<div class="tt-row"><span class="tt-label">PvP Obstacle</span><b>${esc(title)}</b> — ${esc(desc)}</div>`;
|
||||
} else if (FACE_VALUES.includes(val)) {
|
||||
html += '<div class="tt-row"><span class="tt-label">Played by a Pi-Rat</span>Triggers a Secret Technique — automatic success.</div>';
|
||||
html += "<div class=\"tt-row\"><span class=\"tt-label\">As an Obstacle</span>Its value equals the challenged Pi-Rat's Rank.</div>";
|
||||
} else {
|
||||
@@ -95,6 +102,19 @@ export function cardTooltipHtml(code, table = OBSTACLE_TABLE) {
|
||||
return html;
|
||||
}
|
||||
|
||||
export function pvpObstacleInfo(code) {
|
||||
if (!code || isJoker(code)) return null;
|
||||
const suit = cardSuit(code);
|
||||
const themeStr = SUIT_THEMES[suit];
|
||||
if (!themeStr) return null;
|
||||
const splitIdx = themeStr.indexOf(': ');
|
||||
if (splitIdx === -1) return { title: themeStr, description: '' };
|
||||
return {
|
||||
title: themeStr.substring(0, splitIdx),
|
||||
description: themeStr.substring(splitIdx + 2)
|
||||
};
|
||||
}
|
||||
|
||||
export function isJoker(code) {
|
||||
return !!code && code.startsWith('Joker');
|
||||
}
|
||||
|
||||
@@ -6,10 +6,19 @@
|
||||
// - Add a CHANGELOG entry only when a commit changes something players can
|
||||
// see. Skip refactors, tests, and tooling. Keep wording player-facing.
|
||||
|
||||
export const VERSION = 5;
|
||||
export const VERSION = 6;
|
||||
|
||||
// Newest first. Each entry: { version, date: 'YYYY-MM-DD', changes: [string, ...] }.
|
||||
export const CHANGELOG = [
|
||||
{
|
||||
version: 6,
|
||||
date: '2026-06-15',
|
||||
changes: [
|
||||
'PvP challenge cards now display the general suit theme instead of specific obstacles in tooltips.',
|
||||
'Secret Technique tooltips now show the full technique description when played.',
|
||||
'Personal Objectives on the Character Sheet are now positioned in a side column on wider screens.'
|
||||
],
|
||||
},
|
||||
{
|
||||
version: 5,
|
||||
date: '2026-06-15',
|
||||
|
||||
Reference in New Issue
Block a user