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;
|
||||
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 PageContainer from '../components/layout/PageContainer';
|
||||
import { battleReferenceSections } from '../rules/battle-reference';
|
||||
|
||||
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 (
|
||||
<>
|
||||
<TopBar title="Battle Reference" />
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
||||
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