Compare commits
2 Commits
1c8db78c43
...
b18bf683a9
| Author | SHA1 | Date | |
|---|---|---|---|
| b18bf683a9 | |||
| e1779292e5 |
20
TODO.md
20
TODO.md
@@ -1,9 +1,21 @@
|
||||
## Simple Tasks
|
||||
|
||||
- [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
|
||||
- [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
|
||||
|
||||
- [ ] **Logging**: Add some logging to the backend. The Nix service should log to /var/log/pirats.log. The current greatest need is diagnosing server crashes.
|
||||
- [ ] **Purge old finished/inactive games**: Add a periodic task (daily is good enough, but make the interval configurable through an env variable/the nix flake) to purge games that have finished more than two weeks ago, or have had no moves played within the last month. These time windows should also be configurable. Log all purges, including crew names/uuids and how much disk space they recovered.
|
||||
|
||||
## Polish
|
||||
|
||||
- [x] On the home page, rejoin a crew should be above muster a crew, and there should be a gap between the two boxes
|
||||
- [x] **Version number and change log** Start keeping track of version number. It's fine to just start with v1 and increment on every commit. In the hamburger menu, add an "About" item that pops up a modal with the current version number and a change log. The change log should only cover user-facing changes. Add a note to AGENTS.md about maintaining the change log.
|
||||
- [x] **Example Play formatting.** The Example Play section of the Rules page could use a bit more formatting for legibility. Make diagrams of the card state at each step rather than just a text listing.
|
||||
- [x] **Condense Obstacle card display**: Change how the cards in an Obstacle are laid out. Instead of being one big card and a bunch of little ones under it in a row, they should be a single column, with the cards overlapping so that you can just see the numbers and suits of the previous cards peeking out behind the latest card. This is more compact, puts the focus on the most relevant information, and is in line with the rulebook describing the card layout as a column. This is probably also how the cards in the example play section should be laid out.
|
||||
- [ ] **Event log layout**. The event log should still be collapsable into a corner button to give the player the option to declutter their screen. On screens big enough for three column layouts, since the event log has an internal scrolling area, make sure that the outer panel never extends past the bottom of the screen, so that the bottom of the log stays visible no matter what.
|
||||
- [ ] **Crew roster text illegible on dark mode**. The text on the crew roster bubbles is black-on-dark in dark mode. The roster items probably need some re-styling on both dark and light mode, because their background being barely different from the page background doesn't look great.
|
||||
- [ ] **Enforce End-Scene requirements** Unless dev mode is turned on, enforce that the scene cannot end unless every pi-rat has been challenged at least once or the obstacle list is empty. Leave enforcement of "had a chance to complete objective" to table talk.
|
||||
|
||||
## Words Words Words
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -167,6 +167,10 @@
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.checkbox-label:has(input:disabled) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.objectives-checklist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -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,8 +184,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div> <!-- end sheet-main-column -->
|
||||
|
||||
<div class="sheet-group margin-top">
|
||||
<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>
|
||||
@@ -204,6 +207,7 @@
|
||||
</label>
|
||||
</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,27 @@
|
||||
// - 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 = 4;
|
||||
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',
|
||||
changes: [
|
||||
'Completing a Crew Objective is now announced in the Event Log.',
|
||||
'Fixed a UI quirk where mousing over personal objectives incorrectly showed an interactive pointer for Pi-Rats who cannot change them.',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: 4,
|
||||
date: '2026-06-15',
|
||||
|
||||
@@ -305,26 +305,6 @@ def play_joker(db: Session, player_id: str, obstacle_id: str, card_code: str) ->
|
||||
|
||||
def toggle_objective(db: Session, game_id: str, target_id: str, obj_type: str, status: bool):
|
||||
"""Toggles Personal or Crew objectives and adjusts states/Ranks/Captaincy accordingly."""
|
||||
if obj_type.startswith("crew_"):
|
||||
game = get_game(db, game_id)
|
||||
if not game:
|
||||
return
|
||||
if obj_type == "crew_1":
|
||||
# Stealing a ship no longer auto-anoints a Captain — the crew has to
|
||||
# earn and choose one themselves (Crew Objective 2 + the Captaincy UI).
|
||||
game.completed_crew_1 = status
|
||||
db.add(game)
|
||||
db.commit()
|
||||
elif obj_type == "crew_2":
|
||||
game.completed_crew_2 = status
|
||||
db.add(game)
|
||||
db.commit()
|
||||
elif obj_type == "crew_3":
|
||||
game.completed_crew_3 = status
|
||||
db.add(game)
|
||||
db.commit()
|
||||
return
|
||||
|
||||
player = get_player(db, target_id)
|
||||
if not player:
|
||||
return
|
||||
@@ -332,6 +312,25 @@ def toggle_objective(db: Session, game_id: str, target_id: str, obj_type: str, s
|
||||
if not game:
|
||||
return
|
||||
|
||||
if obj_type.startswith("crew_"):
|
||||
if obj_type == "crew_1":
|
||||
# Stealing a ship no longer auto-anoints a Captain — the crew has to
|
||||
# earn and choose one themselves (Crew Objective 2 + the Captaincy UI).
|
||||
game.completed_crew_1 = status
|
||||
elif obj_type == "crew_2":
|
||||
game.completed_crew_2 = status
|
||||
elif obj_type == "crew_3":
|
||||
game.completed_crew_3 = status
|
||||
db.add(game)
|
||||
db.commit()
|
||||
|
||||
obj_name = obj_type.replace('_', ' ').title()
|
||||
status_text = "completed" if status else "unmarked"
|
||||
add_game_event(db, game_id, f"{player.name} marked objective '{obj_name}' as {status_text}.", kind="objective")
|
||||
return
|
||||
|
||||
|
||||
|
||||
rank_diff = 0
|
||||
if obj_type == "personal_1":
|
||||
if status and not player.completed_personal_1:
|
||||
|
||||
Reference in New Issue
Block a user