Files
spelljammer-ships/web/src/components/weapons/AddWeaponModal.tsx
Bas van Rossem 4de0b1cb2a feat(web): weapon templates for Add Weapon modal
Add 19 weapon templates from Appendix A (9 mundane + 10 spellcannons)
with pre-filled damage, range, and notes. Template picker in Add Weapon
modal is grouped by type (Mundane / Spellcannons). Selecting a template
auto-fills all fields; "Custom" option for manual entry. Also adds
notes field and two-column form layout to the modal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:26:04 +01:00

146 lines
4.4 KiB
TypeScript

import { useState } from 'react';
import Modal from '../ui/Modal';
import { weaponTemplates } from '../../data/weapon-templates';
import type { WeaponTemplate } from '../../data/weapon-templates';
interface Props {
open: boolean;
onClose: () => void;
onCreate: (weapon: Record<string, unknown>) => void;
}
interface FormState {
name: string;
type: string;
damage: string;
attackMod: string;
range: string;
ammoMax: string;
notes: string;
}
function defaults(): FormState {
return { name: '', type: '', damage: '', attackMod: '', range: '', ammoMax: '', notes: '' };
}
function fromTemplate(t: WeaponTemplate): FormState {
return {
name: t.name,
type: t.type,
damage: t.damage,
attackMod: '',
range: t.range,
ammoMax: '',
notes: t.notes ?? '',
};
}
export default function AddWeaponModal({ open, onClose, onCreate }: Props) {
const [templateIdx, setTemplateIdx] = useState<string>('');
const [form, setForm] = useState<FormState>(defaults());
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(weaponTemplates[parseInt(val)]));
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!form.name.trim()) return;
const aMax = parseInt(form.ammoMax) || null;
onCreate({
name: form.name.trim(),
type: form.type || null,
damage: form.damage.trim() || null,
attack_mod: parseInt(form.attackMod) || null,
range: form.range.trim() || null,
ammo_max: aMax,
ammo_current: aMax,
status: 'ok',
notes: form.notes.trim() || null,
});
setForm(defaults());
setTemplateIdx('');
onClose();
};
// Group templates by type for the dropdown
const mundane = weaponTemplates.map((t, i) => ({ t, i })).filter(({ t }) => t.type === 'mundane');
const spellcannons = weaponTemplates.map((t, i) => ({ t, i })).filter(({ t }) => t.type === 'spellcannon');
return (
<Modal open={open} onClose={onClose} title="Add Weapon">
<form onSubmit={handleSubmit} className="modal-form">
<label className="form-label">
Template
<select value={templateIdx} onChange={handleTemplateChange}>
<option value="">Custom</option>
<optgroup label="Mundane">
{mundane.map(({ t, i }) => (
<option key={t.name} value={i}>{t.name}</option>
))}
</optgroup>
<optgroup label="Spellcannons">
{spellcannons.map(({ t, i }) => (
<option key={t.name} value={i}>{t.name}</option>
))}
</optgroup>
</select>
</label>
<label className="form-label">
Name *
<input type="text" value={form.name} onChange={set('name')} placeholder="e.g. Ballista" autoFocus required />
</label>
<div className="form-row">
<label className="form-label">
Damage
<input type="text" value={form.damage} onChange={set('damage')} placeholder="e.g. 3d10" />
</label>
<label className="form-label">
Range
<input type="text" value={form.range} onChange={set('range')} placeholder="e.g. 250/800" />
</label>
</div>
<div className="form-row">
<label className="form-label">
Attack Modifier
<input type="number" value={form.attackMod} onChange={set('attackMod')} />
</label>
<label className="form-label">
Ammo Max
<input type="number" value={form.ammoMax} onChange={set('ammoMax')} min="0" placeholder="∞" />
</label>
</div>
<label className="form-label">
Notes
<input type="text" value={form.notes} onChange={set('notes')} placeholder="e.g. Dex or Int." />
</label>
<div className="modal-actions">
<button type="button" className="btn-secondary" onClick={onClose}>
Cancel
</button>
<button type="submit" className="btn-primary" disabled={!form.name.trim()}>
Add Weapon
</button>
</div>
</form>
</Modal>
);
}