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>
146 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|