feat(web): add Rules/Reference page with collapsible battle reference sections
Embeds Battle reference.md content as collapsible sections with markdown rendering, expand/collapse all controls, and styled tables and blockquotes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -476,3 +476,106 @@ button:disabled {
|
|||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rules Page */
|
||||||
|
.rules-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-sections {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-section {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-summary {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-summary::before {
|
||||||
|
content: '▸';
|
||||||
|
transition: transform 0.15s;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] > .rules-summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-content {
|
||||||
|
padding: 0 var(--spacing-md) var(--spacing-md);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-content h3 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-content ul,
|
||||||
|
.rules-content ol {
|
||||||
|
padding-left: var(--spacing-lg);
|
||||||
|
margin: var(--spacing-xs) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-content li {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: var(--spacing-sm) 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-content th,
|
||||||
|
.rules-content td {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-content th {
|
||||||
|
background: var(--color-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-content blockquote {
|
||||||
|
border-left: 3px solid var(--color-primary);
|
||||||
|
padding-left: var(--spacing-md);
|
||||||
|
margin: var(--spacing-sm) 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-content strong {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rules-content p {
|
||||||
|
margin: var(--spacing-xs) 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,57 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import Markdown from 'react-markdown';
|
||||||
import TopBar from '../components/layout/TopBar';
|
import TopBar from '../components/layout/TopBar';
|
||||||
import PageContainer from '../components/layout/PageContainer';
|
import PageContainer from '../components/layout/PageContainer';
|
||||||
|
import { battleReferenceSections } from '../rules/battle-reference';
|
||||||
|
|
||||||
export default function RulesPage() {
|
export default function RulesPage() {
|
||||||
|
const [openSections, setOpenSections] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
|
const toggle = (index: number) => {
|
||||||
|
setOpenSections((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(index)) {
|
||||||
|
next.delete(index);
|
||||||
|
} else {
|
||||||
|
next.add(index);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandAll = () => {
|
||||||
|
setOpenSections(new Set(battleReferenceSections.map((_, i) => i)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const collapseAll = () => {
|
||||||
|
setOpenSections(new Set());
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar title="Battle Reference" />
|
<TopBar title="Battle Reference" />
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<p style={{ color: 'var(--color-text-muted)' }}>Rules reference coming soon...</p>
|
<div className="rules-actions">
|
||||||
|
<button className="btn-secondary btn-sm" onClick={expandAll}>
|
||||||
|
Expand All
|
||||||
|
</button>
|
||||||
|
<button className="btn-secondary btn-sm" onClick={collapseAll}>
|
||||||
|
Collapse All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rules-sections">
|
||||||
|
{battleReferenceSections.map((section, i) => (
|
||||||
|
<details key={i} className="rules-section" open={openSections.has(i)}>
|
||||||
|
<summary className="rules-summary" onClick={(e) => { e.preventDefault(); toggle(i); }}>
|
||||||
|
{section.title}
|
||||||
|
</summary>
|
||||||
|
<div className="rules-content">
|
||||||
|
<Markdown>{section.content}</Markdown>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
262
web/src/rules/battle-reference.ts
Normal file
262
web/src/rules/battle-reference.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
// Battle reference content from Battle reference.md
|
||||||
|
// Sections are split by ## headings for collapsible rendering
|
||||||
|
|
||||||
|
export interface RulesSection {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawMarkdown = `## 1. Ship Health (Armor vs Hull)
|
||||||
|
|
||||||
|
* Ships have **Armor Points** and **Hull Points**.
|
||||||
|
* **Armor is lost first**, then Hull takes damage.
|
||||||
|
* **Hull is hard to repair in combat** (usually requires shipyard-level repair).
|
||||||
|
* **Armor can be recovered quickly** through actions.
|
||||||
|
|
||||||
|
## 2. Combat Round Structure
|
||||||
|
|
||||||
|
Ship combat happens in **two phases**:
|
||||||
|
|
||||||
|
### Phase 1 — Movement Phase
|
||||||
|
|
||||||
|
1. DM secretly plans enemy ship movement
|
||||||
|
2. Ally/revealed ships move openly
|
||||||
|
3. **Helmsman moves the ship as an action**
|
||||||
|
4. DM reveals remaining enemy movement
|
||||||
|
|
||||||
|
Movement ends → action phase begins.
|
||||||
|
|
||||||
|
### Phase 2 — Action Phase
|
||||||
|
|
||||||
|
All characters act in initiative order (group initiative is common).
|
||||||
|
|
||||||
|
Each PC turn includes:
|
||||||
|
|
||||||
|
* Move around the ship
|
||||||
|
* Bonus "card action" (if using decks)
|
||||||
|
* One standard action
|
||||||
|
|
||||||
|
Special ship actions include:
|
||||||
|
|
||||||
|
* **Fire a weapon**
|
||||||
|
* **Reload a weapon**
|
||||||
|
* **Recover armor**
|
||||||
|
* **Use subsystem**
|
||||||
|
* **Help another crew member**
|
||||||
|
* **Tend complications**
|
||||||
|
* **Insight check** (reveal enemy movement next round)
|
||||||
|
|
||||||
|
## 3. Ship Maneuverability (Movement Rules)
|
||||||
|
|
||||||
|
Ships have a **Maneuverability Class** defining turning + speed.
|
||||||
|
|
||||||
|
| Class | Turn Radius | Segment Size | Segment Count | Acceleration |
|
||||||
|
| ----- | ----------- | ------------ | ------------- | ------------ |
|
||||||
|
| S | Any | 1 | Varies | Instant |
|
||||||
|
| A | 120° | 1 | 8 | Instant |
|
||||||
|
| B | 60° | 2 | 3 | Half |
|
||||||
|
| C | 60° | 3 | 2 | Half |
|
||||||
|
| D | 60° | 4 | 1 | Half |
|
||||||
|
| F | 60° | 3 | 1 | Half (Poor) |
|
||||||
|
|
||||||
|
Acceleration:
|
||||||
|
|
||||||
|
* **Instant (S/A):** move any distance up to full speed
|
||||||
|
* **Half (B–F):** must step between stopped → half → full speed
|
||||||
|
|
||||||
|
Turning:
|
||||||
|
|
||||||
|
* 60° = one hex face rotation
|
||||||
|
* Ships must travel straight per segment before turning.
|
||||||
|
|
||||||
|
### Complex Maneuverability Option
|
||||||
|
|
||||||
|
Helmsman may reduce maneuverability by 1 step to change segment pattern (useful for obstacles).
|
||||||
|
|
||||||
|
## 4. Ship Weapons (Core Rules)
|
||||||
|
|
||||||
|
Firing ship weapons works like ranged attacks:
|
||||||
|
|
||||||
|
### Attack Roll
|
||||||
|
|
||||||
|
* Mundane weapons use **Dexterity**
|
||||||
|
* Cannons may allow **Dex or Intelligence**
|
||||||
|
* Spellcannons use **spell attack bonus**
|
||||||
|
* If you have **3+ gunner cards**, add proficiency bonus
|
||||||
|
|
||||||
|
### Damage
|
||||||
|
|
||||||
|
* Ship weapon damage is **Big damage**
|
||||||
|
* Damage is fixed — no ability modifier added
|
||||||
|
|
||||||
|
### Reload
|
||||||
|
|
||||||
|
* All ship weapons require an **action to reload**
|
||||||
|
* Cannot be bypassed by feats like Crossbow Expert
|
||||||
|
|
||||||
|
## 5. Firing Arcs & Hardpoints
|
||||||
|
|
||||||
|
Weapons can only fire within their arc.
|
||||||
|
|
||||||
|
Common arcs:
|
||||||
|
|
||||||
|
* **Port Broadside:** points 11 → 1
|
||||||
|
* **Starboard Broadside:** points 7 → 5
|
||||||
|
* **Chase Gun:** points 9 → 8
|
||||||
|
* **Fore Gun:** points 2 → 3
|
||||||
|
* **Forward Swivel:** points 12 → 6
|
||||||
|
* **Rear Swivel:** points 6 → 12
|
||||||
|
|
||||||
|
Edge of arc:
|
||||||
|
|
||||||
|
* Target gains **+2 cover bonus to AC**
|
||||||
|
|
||||||
|
## 6. Targeted Shots (Subsystem Attacks)
|
||||||
|
|
||||||
|
To target a subsystem:
|
||||||
|
|
||||||
|
* Must have **no other disadvantage**
|
||||||
|
* Must hit **Ship AC + subsystem modifier**
|
||||||
|
|
||||||
|
### Open Deck Targeting
|
||||||
|
|
||||||
|
On hit:
|
||||||
|
|
||||||
|
* All crew on deck make Dex save
|
||||||
|
* DC = 8 + weapon attack bonus
|
||||||
|
* Fail → full damage
|
||||||
|
* Success → no damage
|
||||||
|
|
||||||
|
### Subsystems (Weapons, Rudder, etc.)
|
||||||
|
|
||||||
|
* Damage goes to subsystem HP, not armor/hull
|
||||||
|
* At 0 HP → subsystem disabled
|
||||||
|
* If hit again while disabled:
|
||||||
|
* Ship makes Con save vs damage
|
||||||
|
* Fail → subsystem destroyed permanently
|
||||||
|
|
||||||
|
Helm:
|
||||||
|
|
||||||
|
* Nearly indestructible
|
||||||
|
* If exposed, can be targeted (default +8 AC modifier)
|
||||||
|
|
||||||
|
## 7. Armor Repair (In Combat)
|
||||||
|
|
||||||
|
A PC may spend an action to recover:
|
||||||
|
|
||||||
|
* Armor = proficiency bonus
|
||||||
|
* Boatswain-heavy crews recover more
|
||||||
|
|
||||||
|
Enemy ships may also have limited armor recovery actions.
|
||||||
|
|
||||||
|
## 8. Grappling & Boarding
|
||||||
|
|
||||||
|
Ships can grapple when adjacent and either:
|
||||||
|
|
||||||
|
* Facing same direction
|
||||||
|
* OR both stopped
|
||||||
|
|
||||||
|
To grapple:
|
||||||
|
|
||||||
|
* Captain plays a grapple card → auto success
|
||||||
|
* Otherwise discard cards equal to 3× target maneuver class
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
* F = 3 cards
|
||||||
|
* D = 6
|
||||||
|
* C = 9
|
||||||
|
* B = 12
|
||||||
|
* A = 15
|
||||||
|
* S = 18
|
||||||
|
|
||||||
|
While grappled:
|
||||||
|
|
||||||
|
* Ships move together
|
||||||
|
* Opposed spellcasting checks to resist movement
|
||||||
|
|
||||||
|
## 9. Saving Throws (Ship Effects)
|
||||||
|
|
||||||
|
* Strength save = helmsman spellcasting ability
|
||||||
|
* Dexterity save = helmsman Dex save
|
||||||
|
* +5 bonus if ship is A/S class
|
||||||
|
* −5 penalty if ship is D class
|
||||||
|
* Constitution save = ship's listed Con bonus
|
||||||
|
* Ship immune to Int/Wis/Cha saves
|
||||||
|
|
||||||
|
Size differences may shift DC by ±2 per category.
|
||||||
|
|
||||||
|
## 10. Complications (Fire, Maintenance, Rift…)
|
||||||
|
|
||||||
|
Complications are tracked as a **DC total**.
|
||||||
|
|
||||||
|
### Tending a complication (Action)
|
||||||
|
|
||||||
|
* Roll check → reduce DC by that amount
|
||||||
|
* If DC reaches 0 → complication ends
|
||||||
|
|
||||||
|
### Natural Growth (end of round)
|
||||||
|
|
||||||
|
DC increases by:
|
||||||
|
|
||||||
|
> (DC ÷ 10) + 1
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
* DC 53 → grows to 59
|
||||||
|
|
||||||
|
Thresholds trigger bad effects when DC gets too high.
|
||||||
|
|
||||||
|
## 11. Enemy Initiative Options
|
||||||
|
|
||||||
|
DM can choose enemy ship behavior:
|
||||||
|
|
||||||
|
* **Full:** move + fire freely (hardest)
|
||||||
|
* **Split:** helm + guns on separate initiative
|
||||||
|
* **Skilled:** move first, then fire
|
||||||
|
* **Unskilled:** fire first, then move (easiest)
|
||||||
|
|
||||||
|
## Battle Checklist (Quick Reference)
|
||||||
|
|
||||||
|
Each round:
|
||||||
|
|
||||||
|
1. **Movement Phase**
|
||||||
|
* Helmsman moves ship (respect segments + turning)
|
||||||
|
|
||||||
|
2. **Action Phase**
|
||||||
|
* Gunners fire/reload
|
||||||
|
* Boatswain repairs armor
|
||||||
|
* Crew handles complications
|
||||||
|
* Captain coordinates
|
||||||
|
|
||||||
|
3. **End of Round**
|
||||||
|
* Complications grow
|
||||||
|
* Disabled subsystems may worsen
|
||||||
|
* Prepare next movement`;
|
||||||
|
|
||||||
|
export function parseSections(markdown: string): RulesSection[] {
|
||||||
|
const sections: RulesSection[] = [];
|
||||||
|
const lines = markdown.split('\n');
|
||||||
|
let currentTitle = '';
|
||||||
|
let currentContent: string[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('## ')) {
|
||||||
|
if (currentTitle) {
|
||||||
|
sections.push({ title: currentTitle, content: currentContent.join('\n').trim() });
|
||||||
|
}
|
||||||
|
currentTitle = line.replace('## ', '');
|
||||||
|
currentContent = [];
|
||||||
|
} else {
|
||||||
|
currentContent.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTitle) {
|
||||||
|
sections.push({ title: currentTitle, content: currentContent.join('\n').trim() });
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const battleReferenceSections = parseSections(rawMarkdown);
|
||||||
Reference in New Issue
Block a user