feat: ship templates, crew fields, and read/edit dashboard

- Add crew_req, hardpoints, cargo fields (DB migration + server + types)
- Add 10 ship templates from Appendix A (Caravel, Galleon, Sloop, etc.)
- Enhanced CreateShipModal with template picker that auto-fills all stats
- Dashboard defaults to compact read-only view; Edit button toggles to
  editable steppers/dropdowns
- New CrewSection showing crew required, hardpoints, and cargo
- Two-column form layout in create modal for better use of space
- Fix .gitignore data/ pattern to only match root-level data directory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bas van Rossem
2026-02-19 17:16:44 +01:00
parent cbda07d793
commit 642f1f70e8
12 changed files with 524 additions and 112 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
node_modules/ node_modules/
dist/ dist/
data/ /data/
*.sqlite *.sqlite
*.sqlite-journal *.sqlite-journal
*.sqlite-wal *.sqlite-wal

View File

@@ -0,0 +1,3 @@
ALTER TABLE ships ADD COLUMN crew_req INTEGER NOT NULL DEFAULT 0;
ALTER TABLE ships ADD COLUMN hardpoints INTEGER NOT NULL DEFAULT 0;
ALTER TABLE ships ADD COLUMN cargo INTEGER NOT NULL DEFAULT 0;

View File

@@ -15,6 +15,9 @@ export interface Ship {
maneuver_class: string | null; maneuver_class: string | null;
size_category: string | null; size_category: string | null;
notes: string | null; notes: string | null;
crew_req: number;
hardpoints: number;
cargo: number;
updated_at: number; updated_at: number;
} }
@@ -56,8 +59,8 @@ export function createShip(input: CreateShipInput): Ship {
const now = Date.now(); const now = Date.now();
db.prepare(` db.prepare(`
INSERT INTO ships (id, name, hull_current, hull_max, armor_current, armor_max, ac, con_save, speed, maneuver_class, size_category, notes, updated_at) INSERT INTO ships (id, name, hull_current, hull_max, armor_current, armor_max, ac, con_save, speed, maneuver_class, size_category, notes, crew_req, hardpoints, cargo, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run( `).run(
id, id,
input.name, input.name,
@@ -71,6 +74,9 @@ export function createShip(input: CreateShipInput): Ship {
input.maneuver_class, input.maneuver_class,
input.size_category, input.size_category,
input.notes, input.notes,
input.crew_req,
input.hardpoints,
input.cargo,
now, now,
); );

View File

@@ -14,6 +14,9 @@ export const createShipSchema = z.object({
maneuver_class: z.enum(maneuverClasses).nullable().default(null), maneuver_class: z.enum(maneuverClasses).nullable().default(null),
size_category: z.string().max(50).nullable().default(null), size_category: z.string().max(50).nullable().default(null),
notes: z.string().nullable().default(null), notes: z.string().nullable().default(null),
crew_req: z.number().int().min(0).default(0),
hardpoints: z.number().int().min(0).default(0),
cargo: z.number().int().min(0).default(0),
}); });
export const updateShipSchema = z export const updateShipSchema = z
@@ -29,6 +32,9 @@ export const updateShipSchema = z
maneuver_class: z.enum(maneuverClasses).nullable(), maneuver_class: z.enum(maneuverClasses).nullable(),
size_category: z.string().max(50).nullable(), size_category: z.string().max(50).nullable(),
notes: z.string().nullable(), notes: z.string().nullable(),
crew_req: z.number().int().min(0),
hardpoints: z.number().int().min(0),
cargo: z.number().int().min(0),
}) })
.partial() .partial()
.refine( .refine(

View File

@@ -0,0 +1,50 @@
import NumericStepper from '../ui/NumericStepper';
import type { Ship } from '../../types/ship';
interface Props {
ship: Ship;
onUpdate: (patch: Partial<Ship>) => void;
editing: boolean;
}
export default function CrewSection({ ship, onUpdate, editing }: Props) {
return (
<section className="dashboard-section">
<h3 className="section-title">Crew &amp; Cargo</h3>
{editing ? (
<div className="stat-grid">
<NumericStepper
label="Crew Required"
value={ship.crew_req}
onChange={(v) => onUpdate({ crew_req: v })}
/>
<NumericStepper
label="Hardpoints"
value={ship.hardpoints}
onChange={(v) => onUpdate({ hardpoints: v })}
/>
<NumericStepper
label="Cargo (tons)"
value={ship.cargo}
onChange={(v) => onUpdate({ cargo: v })}
/>
</div>
) : (
<div className="stat-grid-readonly">
<div className="stat-ro">
<span className="stat-ro-label">Crew</span>
<span className="stat-ro-value">{ship.crew_req}</span>
</div>
<div className="stat-ro">
<span className="stat-ro-label">Hardpoints</span>
<span className="stat-ro-value">{ship.hardpoints}</span>
</div>
<div className="stat-ro">
<span className="stat-ro-label">Cargo</span>
<span className="stat-ro-value">{ship.cargo}t</span>
</div>
</div>
)}
</section>
);
}

View File

@@ -8,31 +8,49 @@ const SIZE_CATEGORIES = ['Tiny', 'Small', 'Medium', 'Large', 'Huge', 'Gargantuan
interface Props { interface Props {
ship: Ship; ship: Ship;
onUpdate: (patch: Partial<Ship>) => void; onUpdate: (patch: Partial<Ship>) => void;
editing: boolean;
} }
export default function MobilitySection({ ship, onUpdate }: Props) { export default function MobilitySection({ ship, onUpdate, editing }: Props) {
return ( return (
<section className="dashboard-section"> <section className="dashboard-section">
<h3 className="section-title">Mobility</h3> <h3 className="section-title">Mobility</h3>
<div className="stat-grid"> {editing ? (
<NumericStepper <div className="stat-grid">
label="Speed" <NumericStepper
value={ship.speed ?? 0} label="Speed"
onChange={(v) => onUpdate({ speed: v })} value={ship.speed ?? 0}
/> onChange={(v) => onUpdate({ speed: v })}
<EnumDropdown />
label="Maneuver Class" <EnumDropdown
value={ship.maneuver_class} label="Maneuver Class"
options={MANEUVER_CLASSES} value={ship.maneuver_class}
onChange={(v) => onUpdate({ maneuver_class: v })} options={MANEUVER_CLASSES}
/> onChange={(v) => onUpdate({ maneuver_class: v })}
<EnumDropdown />
label="Size Category" <EnumDropdown
value={ship.size_category} label="Size Category"
options={SIZE_CATEGORIES} value={ship.size_category}
onChange={(v) => onUpdate({ size_category: v })} options={SIZE_CATEGORIES}
/> onChange={(v) => onUpdate({ size_category: v })}
</div> />
</div>
) : (
<div className="stat-grid-readonly">
<div className="stat-ro">
<span className="stat-ro-label">Speed</span>
<span className="stat-ro-value">{ship.speed ?? 0}ft</span>
</div>
<div className="stat-ro">
<span className="stat-ro-label">Class</span>
<span className="stat-ro-value">{ship.maneuver_class ?? '—'}</span>
</div>
<div className="stat-ro">
<span className="stat-ro-label">Size</span>
<span className="stat-ro-value">{ship.size_category ?? '—'}</span>
</div>
</div>
)}
</section> </section>
); );
} }

View File

@@ -4,48 +4,70 @@ import type { Ship } from '../../types/ship';
interface Props { interface Props {
ship: Ship; ship: Ship;
onUpdate: (patch: Partial<Ship>) => void; onUpdate: (patch: Partial<Ship>) => void;
editing: boolean;
} }
export default function VitalsSection({ ship, onUpdate }: Props) { export default function VitalsSection({ ship, onUpdate, editing }: Props) {
return ( return (
<section className="dashboard-section"> <section className="dashboard-section">
<h3 className="section-title">Vitals</h3> <h3 className="section-title">Vitals</h3>
<div className="stat-grid"> {editing ? (
<NumericStepper <div className="stat-grid">
label="Hull" <NumericStepper
value={ship.hull_current} label="Hull"
max={ship.hull_max} value={ship.hull_current}
onChange={(v) => onUpdate({ hull_current: v })} max={ship.hull_max}
/> onChange={(v) => onUpdate({ hull_current: v })}
<NumericStepper />
label="Hull Max" <NumericStepper
value={ship.hull_max} label="Hull Max"
min={ship.hull_current} value={ship.hull_max}
onChange={(v) => onUpdate({ hull_max: v })} min={ship.hull_current}
/> onChange={(v) => onUpdate({ hull_max: v })}
<NumericStepper />
label="Armor" <NumericStepper
value={ship.armor_current} label="Armor"
max={ship.armor_max} value={ship.armor_current}
onChange={(v) => onUpdate({ armor_current: v })} max={ship.armor_max}
/> onChange={(v) => onUpdate({ armor_current: v })}
<NumericStepper />
label="Armor Max" <NumericStepper
value={ship.armor_max} label="Armor Max"
min={ship.armor_current} value={ship.armor_max}
onChange={(v) => onUpdate({ armor_max: v })} min={ship.armor_current}
/> onChange={(v) => onUpdate({ armor_max: v })}
<NumericStepper />
label="AC" <NumericStepper
value={ship.ac} label="AC"
onChange={(v) => onUpdate({ ac: v })} value={ship.ac}
/> onChange={(v) => onUpdate({ ac: v })}
<NumericStepper />
label="Con Save" <NumericStepper
value={ship.con_save ?? 0} label="Con Save"
onChange={(v) => onUpdate({ con_save: v })} value={ship.con_save ?? 0}
/> onChange={(v) => onUpdate({ con_save: v })}
</div> />
</div>
) : (
<div className="stat-grid-readonly">
<div className="stat-ro">
<span className="stat-ro-label">Hull</span>
<span className="stat-ro-value">{ship.hull_current}/{ship.hull_max}</span>
</div>
<div className="stat-ro">
<span className="stat-ro-label">AC</span>
<span className="stat-ro-value">{ship.ac}</span>
</div>
<div className="stat-ro">
<span className="stat-ro-label">Armor</span>
<span className="stat-ro-value">{ship.armor_current}/{ship.armor_max}</span>
</div>
<div className="stat-ro">
<span className="stat-ro-label">Con</span>
<span className="stat-ro-value">{ship.con_save != null ? `+${ship.con_save}` : '—'}</span>
</div>
</div>
)}
</section> </section>
); );
} }

View File

@@ -1,40 +1,108 @@
import { useState } from 'react'; import { useState } from 'react';
import Modal from '../ui/Modal'; import Modal from '../ui/Modal';
import { useShipsList } from '../../store/use-ships-list'; import { useShipsList } from '../../store/use-ships-list';
import { shipTemplates } from '../../data/ship-templates';
import type { ShipTemplate } from '../../data/ship-templates';
interface Props { interface Props {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
} }
const MANEUVER_CLASSES = ['S', 'A', 'B', 'C', 'D', 'E', 'F'];
const SIZE_CATEGORIES = ['Tiny', 'Small', 'Medium', 'Large', 'Huge', 'Gargantuan'];
function defaults(): FormState {
return {
name: '',
hullMax: '0',
armorMax: '0',
ac: '10',
conSave: '0',
speed: '300',
maneuverClass: '',
sizeCategory: '',
crewReq: '0',
hardpoints: '0',
cargo: '0',
};
}
function fromTemplate(t: ShipTemplate): FormState {
return {
name: t.name,
hullMax: String(t.hull_max),
armorMax: String(t.armor_max),
ac: String(t.ac),
conSave: String(t.con_save),
speed: String(t.speed),
maneuverClass: t.maneuver_class,
sizeCategory: t.size_category,
crewReq: String(t.crew_req),
hardpoints: String(t.hardpoints),
cargo: String(t.cargo),
};
}
interface FormState {
name: string;
hullMax: string;
armorMax: string;
ac: string;
conSave: string;
speed: string;
maneuverClass: string;
sizeCategory: string;
crewReq: string;
hardpoints: string;
cargo: string;
}
export default function CreateShipModal({ open, onClose }: Props) { export default function CreateShipModal({ open, onClose }: Props) {
const [name, setName] = useState(''); const [templateIdx, setTemplateIdx] = useState<string>('');
const [hullMax, setHullMax] = useState('0'); const [form, setForm] = useState<FormState>(defaults());
const [armorMax, setArmorMax] = useState('0');
const [ac, setAc] = useState('10');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const createShip = useShipsList((s) => s.createShip); const createShip = useShipsList((s) => s.createShip);
const set = (field: keyof FormState) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setForm((f) => ({ ...f, [field]: e.target.value }));
};
const handleTemplateChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const val = e.target.value;
setTemplateIdx(val);
if (val === '') {
setForm(defaults());
} else {
setForm(fromTemplate(shipTemplates[parseInt(val)]));
}
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!name.trim()) return; if (!form.name.trim()) return;
setSubmitting(true); setSubmitting(true);
try { try {
const hMax = parseInt(hullMax) || 0; const hMax = parseInt(form.hullMax) || 0;
const aMax = parseInt(armorMax) || 0; const aMax = parseInt(form.armorMax) || 0;
await createShip({ await createShip({
name: name.trim(), name: form.name.trim(),
hull_max: hMax, hull_max: hMax,
hull_current: hMax, hull_current: hMax,
armor_max: aMax, armor_max: aMax,
armor_current: aMax, armor_current: aMax,
ac: parseInt(ac) || 10, ac: parseInt(form.ac) || 10,
con_save: parseInt(form.conSave) || 0,
speed: parseInt(form.speed) || 0,
maneuver_class: form.maneuverClass || null,
size_category: form.sizeCategory || null,
crew_req: parseInt(form.crewReq) || 0,
hardpoints: parseInt(form.hardpoints) || 0,
cargo: parseInt(form.cargo) || 0,
}); });
setName(''); setForm(defaults());
setHullMax('0'); setTemplateIdx('');
setArmorMax('0');
setAc('10');
onClose(); onClose();
} catch { } catch {
// Error handled by store // Error handled by store
@@ -46,49 +114,91 @@ export default function CreateShipModal({ open, onClose }: Props) {
return ( return (
<Modal open={open} onClose={onClose} title="Create Ship"> <Modal open={open} onClose={onClose} title="Create Ship">
<form onSubmit={handleSubmit} className="modal-form"> <form onSubmit={handleSubmit} className="modal-form">
<label className="form-label">
Template
<select value={templateIdx} onChange={handleTemplateChange}>
<option value="">Custom</option>
{shipTemplates.map((t, i) => (
<option key={t.name} value={i}>{t.name}</option>
))}
</select>
</label>
<label className="form-label"> <label className="form-label">
Ship Name * Ship Name *
<input <input type="text" value={form.name} onChange={set('name')} placeholder="e.g. Astral Clipper" autoFocus required />
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Astral Clipper"
autoFocus
required
/>
</label>
<label className="form-label">
Hull Max
<input
type="number"
value={hullMax}
onChange={(e) => setHullMax(e.target.value)}
min="0"
/>
</label>
<label className="form-label">
Armor Max
<input
type="number"
value={armorMax}
onChange={(e) => setArmorMax(e.target.value)}
min="0"
/>
</label>
<label className="form-label">
AC
<input
type="number"
value={ac}
onChange={(e) => setAc(e.target.value)}
min="0"
/>
</label> </label>
<div className="form-row">
<label className="form-label">
Hull Max
<input type="number" value={form.hullMax} onChange={set('hullMax')} min="0" />
</label>
<label className="form-label">
Armor Max
<input type="number" value={form.armorMax} onChange={set('armorMax')} min="0" />
</label>
</div>
<div className="form-row">
<label className="form-label">
AC
<input type="number" value={form.ac} onChange={set('ac')} min="0" />
</label>
<label className="form-label">
Con Save
<input type="number" value={form.conSave} onChange={set('conSave')} />
</label>
</div>
<div className="form-row">
<label className="form-label">
Speed (ft)
<input type="number" value={form.speed} onChange={set('speed')} min="0" step="50" />
</label>
<label className="form-label">
Maneuver Class
<select value={form.maneuverClass} onChange={set('maneuverClass')}>
<option value=""></option>
{MANEUVER_CLASSES.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
</label>
</div>
<div className="form-row">
<label className="form-label">
Size
<select value={form.sizeCategory} onChange={set('sizeCategory')}>
<option value=""></option>
{SIZE_CATEGORIES.map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
</label>
<label className="form-label">
Crew Required
<input type="number" value={form.crewReq} onChange={set('crewReq')} min="0" />
</label>
</div>
<div className="form-row">
<label className="form-label">
Hardpoints
<input type="number" value={form.hardpoints} onChange={set('hardpoints')} min="0" />
</label>
<label className="form-label">
Cargo (tons)
<input type="number" value={form.cargo} onChange={set('cargo')} min="0" />
</label>
</div>
<div className="modal-actions"> <div className="modal-actions">
<button type="button" className="btn-secondary" onClick={onClose}> <button type="button" className="btn-secondary" onClick={onClose}>
Cancel Cancel
</button> </button>
<button type="submit" className="btn-primary" disabled={submitting || !name.trim()}> <button type="submit" className="btn-primary" disabled={submitting || !form.name.trim()}>
{submitting ? 'Creating...' : 'Create Ship'} {submitting ? 'Creating...' : 'Create Ship'}
</button> </button>
</div> </div>

View File

@@ -0,0 +1,146 @@
export interface ShipTemplate {
name: string;
hull_max: number;
armor_max: number;
ac: number;
con_save: number;
speed: number;
maneuver_class: string;
size_category: string;
crew_req: number;
hardpoints: number;
cargo: number;
}
export const shipTemplates: ShipTemplate[] = [
{
name: 'Caravel',
hull_max: 50,
armor_max: 20,
ac: 13,
con_save: 3,
speed: 300,
maneuver_class: 'B',
size_category: 'Small',
crew_req: 8,
hardpoints: 4,
cargo: 15,
},
{
name: 'Galleon',
hull_max: 75,
armor_max: 35,
ac: 13,
con_save: 4,
speed: 300,
maneuver_class: 'C',
size_category: 'Medium',
crew_req: 20,
hardpoints: 12,
cargo: 40,
},
{
name: 'Sloop',
hull_max: 40,
armor_max: 16,
ac: 13,
con_save: 3,
speed: 300,
maneuver_class: 'B',
size_category: 'Small',
crew_req: 8,
hardpoints: 3,
cargo: 10,
},
{
name: 'Frigate',
hull_max: 120,
armor_max: 40,
ac: 15,
con_save: 7,
speed: 300,
maneuver_class: 'B',
size_category: 'Medium',
crew_req: 20,
hardpoints: 12,
cargo: 100,
},
{
name: 'Hammership',
hull_max: 150,
armor_max: 40,
ac: 15,
con_save: 7,
speed: 300,
maneuver_class: 'C',
size_category: 'Medium',
crew_req: 24,
hardpoints: 16,
cargo: 150,
},
{
name: 'Squid Ship',
hull_max: 120,
armor_max: 35,
ac: 14,
con_save: 7,
speed: 300,
maneuver_class: 'C',
size_category: 'Medium',
crew_req: 14,
hardpoints: 7,
cargo: 60,
},
{
name: 'Man-O-War',
hull_max: 75,
armor_max: 25,
ac: 15,
con_save: 6,
speed: 300,
maneuver_class: 'C',
size_category: 'Medium',
crew_req: 10,
hardpoints: 6,
cargo: 30,
},
{
name: 'Bombard',
hull_max: 60,
armor_max: 35,
ac: 14,
con_save: 4,
speed: 300,
maneuver_class: 'C',
size_category: 'Small',
crew_req: 10,
hardpoints: 3,
cargo: 15,
},
{
name: 'Shrike Ship',
hull_max: 40,
armor_max: 20,
ac: 15,
con_save: 3,
speed: 400,
maneuver_class: 'A',
size_category: 'Small',
crew_req: 5,
hardpoints: 3,
cargo: 5,
},
{
name: 'Whaleship',
hull_max: 225,
armor_max: 50,
ac: 15,
con_save: 4,
speed: 200,
maneuver_class: 'D',
size_category: 'Large',
crew_req: 12,
hardpoints: 12,
cargo: 250,
},
];

View File

@@ -234,6 +234,12 @@ a:hover {
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-sm);
}
/* Ship List */ /* Ship List */
.ship-list-header { .ship-list-header {
margin-bottom: var(--spacing-md); margin-bottom: var(--spacing-md);
@@ -315,12 +321,42 @@ button:disabled {
padding-bottom: var(--spacing-xs); padding-bottom: var(--spacing-xs);
} }
.dashboard-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: var(--spacing-md);
}
.stat-grid { .stat-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: var(--spacing-sm); gap: var(--spacing-sm);
} }
.stat-grid-readonly {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-xs) var(--spacing-md);
}
.stat-ro {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: var(--spacing-xs) 0;
}
.stat-ro-label {
font-size: 0.85rem;
color: var(--color-text-muted);
}
.stat-ro-value {
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
}
/* Stepper */ /* Stepper */
.stepper { .stepper {
display: flex; display: flex;

View File

@@ -1,9 +1,10 @@
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
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 VitalsSection from '../components/dashboard/VitalsSection'; import VitalsSection from '../components/dashboard/VitalsSection';
import MobilitySection from '../components/dashboard/MobilitySection'; import MobilitySection from '../components/dashboard/MobilitySection';
import CrewSection from '../components/dashboard/CrewSection';
import WeaponsSection from '../components/dashboard/WeaponsSection'; import WeaponsSection from '../components/dashboard/WeaponsSection';
import NotesSection from '../components/dashboard/NotesSection'; import NotesSection from '../components/dashboard/NotesSection';
import { useShipStore } from '../store/use-ship-store'; import { useShipStore } from '../store/use-ship-store';
@@ -11,6 +12,7 @@ import { useShipStore } from '../store/use-ship-store';
export default function ShipDashboardPage() { export default function ShipDashboardPage() {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const { ship, weapons, loading, joinShip, leaveShip, updateShip, createWeapon, updateWeapon, deleteWeapon } = useShipStore(); const { ship, weapons, loading, joinShip, leaveShip, updateShip, createWeapon, updateWeapon, deleteWeapon } = useShipStore();
const [editing, setEditing] = useState(false);
useEffect(() => { useEffect(() => {
if (id) joinShip(id); if (id) joinShip(id);
@@ -32,8 +34,18 @@ export default function ShipDashboardPage() {
<> <>
<TopBar title={ship.name} /> <TopBar title={ship.name} />
<PageContainer> <PageContainer>
<VitalsSection ship={ship} onUpdate={updateShip} /> <div className="dashboard-toolbar">
<MobilitySection ship={ship} onUpdate={updateShip} /> <button
type="button"
className={editing ? 'btn-primary btn-sm' : 'btn-secondary btn-sm'}
onClick={() => setEditing(!editing)}
>
{editing ? 'Done' : 'Edit'}
</button>
</div>
<VitalsSection ship={ship} onUpdate={updateShip} editing={editing} />
<MobilitySection ship={ship} onUpdate={updateShip} editing={editing} />
<CrewSection ship={ship} onUpdate={updateShip} editing={editing} />
<WeaponsSection <WeaponsSection
weapons={weapons} weapons={weapons}
onCreateWeapon={createWeapon} onCreateWeapon={createWeapon}

View File

@@ -11,6 +11,9 @@ export interface Ship {
maneuver_class: string | null; maneuver_class: string | null;
size_category: string | null; size_category: string | null;
notes: string | null; notes: string | null;
crew_req: number;
hardpoints: number;
cargo: number;
updated_at: number; updated_at: number;
} }