Compare commits

...

4 Commits

Author SHA1 Message Date
683c60fff9 Serve the rulebook as a static HTML page from FastAPI
Replace the SPA Rules route with a self-contained src/pirats/rules.html
served at GET /rules (registered before the SPA mount, included in
package data for the Nix build). Plain anchor links replace the
scrollIntoView workaround since there's no hash router to fight. The
corner Rules link and splash-page link now point at /rules, and the
Vite dev server proxies /rules to the backend like /api.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:35:04 -07:00
69e7d24e10 Add HTML rulebook page with always-available Rules link
New /#/rules route renders the full rulebook transcribed from
"Rats with Gats beta.pdf" (intro, character creation, gameplay,
obstacle charts, challenge resolution, example of play, and setting
info), with an in-page table of contents. A fixed corner link in
App.svelte opens it in a new tab from every page, and the splash
page's previously broken /rules link now points at the new route.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:30:00 -07:00
ed7900656d Replace copy-link alert() with inline button feedback
The lobby Copy buttons now flip to a transient "✓ Copied" label instead
of popping a modal alert, matching the admin page's copy buttons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:18:48 -07:00
bc867c95a1 Add player re-join links to admin page
Each player row now has Open/Copy actions for their dashboard link, so
the creator can recover access for players who lose their URL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:17:30 -07:00
8 changed files with 1384 additions and 13 deletions

20
TODO.md
View File

@@ -4,20 +4,20 @@
- [ ] **Allow Deep players to roll back game events.** This may require some updates to how game state is handled. Feel free to propose significant overhauls/refactors to enable this cleanly, I am not attached to the current architecture at all.
- [ ] **Dead Pi-Rat re-roll flow.** I suspect this is badly broken, as it was implemented in a rush by an older model. In particular, other players will need to contribute likes/hates and techniques, which I believe are not currently implemented. This flow should also be used for new players who join after initial character creation, and should take place as a between-scenes phase after Deep Upkeep.
## Missing Features
- [ ] **Smell-based temporary nicknames at game start.** When joining, players type an arbitrary lobby name; only re-rolled recruits get "Recruit {smell}". We should keep the lobby names, but be clear that these refer to the players, not the Pi-Rats, who should be identified by their smell until they earn a Name.
- [ ] **Tooltip reminders of card suit themes**. When you mouse over a card, there should be reminders that e.g. hearts are thematically social skills. Check the manual for the other suits.
- [ ] **Name string transfer on a refused Name Tax.** Mechanically the Objective (and Rank) transfer and the winner gets a "claim your name" prompt; this should be updated so that the stealer literally takes the other Pi-Rat's name, leaving them demoted to "Recruit {smell}".
- [ ] **First-scene framing requirement** ("must establish the Pi-Rats are on a ship they can steal; don't start far from water") is not surfaced anywhere in the scene-setup UI. We could add an interstitial phase that prompts the Deep to explain the scene before we move to the scene UI with the objectives.
- [ ] **Dead Pi-Rat re-roll flow.** I suspect this is badly broken, as it was implemented in a rush by an older model. In particular, other players will need to contribute likes/hates and techniques, which I believe are not currently implemented. This flow should also be used for new players who join after initial character creation, and should take place as a between-scenes phase after Deep Upkeep.
- [ ] **Event log visibility**. The event log should be visible in all phases, not just during a scene.
- [ ] **Admin page link**. The admin/creator player should always have a link to the admin page available to them.
- [x] **Incomplete admin page.**: The Admin page should include links for players to re-join the game.
- [x] **Less intrusive link copied notification.** When you copy a join link from the lobby page, it pops up an alert() to let you know the link was copied. I'd prefer something more subtle, and more importantly not modal.
- [x] **Rules endpoint** Make a nicely formatted HTML version of the rulebook. It should probably also be linked from a help menu somewhere that's always available, rather than only being surfaced on the splash page of the site (which joining players don't even see). Work from "Rats with Gats beta.pdf" rather than "RULEBOOK.md", which is a low fidelity AI summary of the former.
## Cosmetic
- [ ] **Refactor the CSS.** Make it easy to re-theme by keeping the definitions of primary/background/highlight/etc colors confined to a few specific styles. If this requires adopting some kind of CSS framework/preprocessing, so be it.
- [ ] **Re-styling**. The entire app could use a face lift, but in particular the splash page is ugly.
## Worth Double-Checking / Minor
- [ ] **Dev/prod server flows.** Investigate where assets are served from in both development and production workflows. Make sure we're not committing any compiled svelte code. In production, the app should be used through the Nix flake, which should handle the compile step when the flake is built.
- [ ] **Refactor the CSS.** Make it easy to re-theme by keeping the definitions of primary/background/highlight/etc colors confined to a few specific styles. If this requires adopting some kind of CSS framework/preprocessing, so be it.
- [ ] **More formal database migration procedures.**
- [ ] **Rules endpoint** Make a nicely formatted HTML version of the rulebook. It should probably also be linked from a help menu somewhere that's always available, rather than only being surfaced on the splash page of the site (which joining players don't even see)
- [ ] **Re-styling**. The entire app could use a face lift, but in particular the splash page is ugly.

View File

@@ -15,4 +15,26 @@
<main>
<Router {routes} />
<a class="rules-corner-link" href="/rules" target="_blank" rel="noopener" title="Open the rulebook in a new tab">📖 Rules</a>
</main>
<style>
.rules-corner-link {
position: fixed;
top: 12px;
left: 16px;
z-index: 1001;
padding: 6px 14px;
border-radius: 16px;
background: rgba(10, 15, 25, 0.9);
border: 1px solid var(--glass-border-focus);
color: var(--neon-cyan);
font-size: 0.85rem;
font-weight: 700;
text-decoration: none;
font-family: var(--font-heading);
}
.rules-corner-link:hover {
background: rgba(0, 242, 254, 0.1);
}
</style>

View File

@@ -4,6 +4,15 @@
export let state;
let starting = false;
let copiedLink = null;
let copiedTimer = null;
function copyLink(which, value) {
navigator.clipboard.writeText(value);
copiedLink = which;
clearTimeout(copiedTimer);
copiedTimer = setTimeout(() => { copiedLink = null; }, 2000);
}
async function startGame() {
starting = true;
@@ -39,7 +48,9 @@
<h4>Share Join Link:</h4>
<div class="copy-container">
<input type="text" readonly value="{window.location.origin}/#/game/{state.game.id}/join" class="copy-input" id="join-link-copy">
<button on:click={() => { navigator.clipboard.writeText(document.getElementById('join-link-copy').value); alert('Join link copied!'); }} class="btn btn-secondary btn-small">Copy</button>
<button on:click={() => copyLink('join', document.getElementById('join-link-copy').value)} class="btn btn-secondary btn-small">
{copiedLink === 'join' ? '✓ Copied' : 'Copy'}
</button>
</div>
</div>
@@ -49,7 +60,9 @@
<p class="info-text">Save this admin link! You can use it to recover character sheets if another player loses their connection or link.</p>
<div class="copy-container">
<input type="text" readonly value="{window.location.origin}/#/game/{state.game.id}/admin" class="copy-input admin-input" id="admin-link-copy">
<button on:click={() => { navigator.clipboard.writeText(document.getElementById('admin-link-copy').value); alert('Admin link copied!'); }} class="btn btn-gold btn-small">Copy Admin Link</button>
<button on:click={() => copyLink('admin', document.getElementById('admin-link-copy').value)} class="btn btn-gold btn-small">
{copiedLink === 'admin' ? '✓ Copied' : 'Copy Admin Link'}
</button>
</div>
<div class="mt-4">
<a href="/#/game/{state.game.id}/admin" class="text-sm text-silver underline hover:text-white">Access Admin Panel directly</a>

View File

@@ -8,6 +8,19 @@
let data = null;
let error = '';
let copiedPlayerId = null;
let copiedTimer = null;
function rejoinLink(playerId) {
return `${window.location.origin}/#/game/${gameId}/player/${playerId}`;
}
function copyRejoinLink(playerId) {
navigator.clipboard.writeText(rejoinLink(playerId));
copiedPlayerId = playerId;
clearTimeout(copiedTimer);
copiedTimer = setTimeout(() => { copiedPlayerId = null; }, 2000);
}
onMount(() => {
// Try to load admin_key from local storage
@@ -58,6 +71,7 @@
<th class="p-3">Role</th>
<th class="p-3">Status</th>
<th class="p-3">Rank</th>
<th class="p-3">Re-join Link</th>
</tr>
</thead>
<tbody>
@@ -78,6 +92,14 @@
{/if}
</td>
<td class="p-3">{p.rank}</td>
<td class="p-3">
<div class="link-copy-action">
<a href={rejoinLink(p.id)} class="btn btn-secondary btn-small">Open</a>
<button on:click={() => copyRejoinLink(p.id)} class="btn btn-secondary btn-small">
{copiedPlayerId === p.id ? '✓ Copied' : 'Copy'}
</button>
</div>
</td>
</tr>
{/each}
</tbody>

View File

@@ -9,6 +9,10 @@ export default defineConfig({
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
},
'/rules': {
target: 'http://127.0.0.1:8000',
changeOrigin: true,
}
}
}

View File

@@ -27,7 +27,7 @@ pirats = "pirats.main:main"
where = ["src"]
[tool.setuptools.package-data]
pirats = ["static/**/*"]
pirats = ["static/**/*", "rules.html"]
[tool.pytest.ini_options]
pythonpath = ["src"]

View File

@@ -1,6 +1,7 @@
from contextlib import asynccontextmanager
from typing import Optional
from fastapi import FastAPI, Form, Depends, HTTPException, APIRouter
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from sqlmodel import Session
import uvicorn
@@ -105,6 +106,11 @@ api.include_router(admin_router)
# Mount API at /api
app.include_router(api, prefix="/api")
# Static rulebook page (registered before the SPA mount so it takes precedence)
@app.get("/rules", include_in_schema=False)
def rules_page():
return FileResponse(BASE_DIR / "rules.html", media_type="text/html")
# Mount SPA
static_dir = BASE_DIR / "static"
if os.path.exists(static_dir):

1304
src/pirats/rules.html Normal file

File diff suppressed because it is too large Load Diff