HP formula stored on participant, loads in edit modal
hpFormula field persisted on participant doc (makeParticipant + builder). Add handler stores formula. Edit modal loads participant.hpFormula, reroll button sets maxHp field (no save until save). Edit submit persists formula. Formula only for monster/npc types.
This commit is contained in:
+71
-13
@@ -581,15 +581,26 @@ function EditParticipantModal({ participant, onClose, onSave }) {
|
||||
const [initiative, setInitiative] = useState(participant.initiative);
|
||||
const [currentHp, setCurrentHp] = useState(participant.currentHp);
|
||||
const [maxHp, setMaxHp] = useState(participant.maxHp);
|
||||
const [hpFormula, setHpFormula] = useState(participant.hpFormula || '');
|
||||
const [asNpc, setAsNpc] = useState(participant.type === 'npc');
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
let finalMaxHp = parseInt(maxHp, 10);
|
||||
// formula rolls only if maxHp empty. maxHp wins if both.
|
||||
if (isNaN(finalMaxHp) && hpFormula.trim()) {
|
||||
const rolled = shared.rollHpFormula(hpFormula);
|
||||
if (rolled) finalMaxHp = rolled;
|
||||
}
|
||||
if (isNaN(finalMaxHp)) finalMaxHp = participant.maxHp;
|
||||
let finalCurrentHp = parseInt(currentHp, 10);
|
||||
if (isNaN(finalCurrentHp)) finalCurrentHp = finalMaxHp;
|
||||
onSave({
|
||||
name: name.trim(),
|
||||
initiative: parseInt(initiative, 10),
|
||||
currentHp: parseInt(currentHp, 10),
|
||||
maxHp: parseInt(maxHp, 10),
|
||||
currentHp: finalCurrentHp,
|
||||
maxHp: finalMaxHp,
|
||||
hpFormula: (participant.type === 'monster' || participant.type === 'npc') ? (hpFormula.trim() || null) : null,
|
||||
type: participant.type === 'monster' || participant.type === 'npc' ? (asNpc ? 'npc' : 'monster') : participant.type,
|
||||
});
|
||||
};
|
||||
@@ -636,6 +647,32 @@ function EditParticipantModal({ participant, onClose, onSave }) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(participant.type === 'monster' || participant.type === 'npc') && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-stone-300 mb-1">HP Formula</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={hpFormula}
|
||||
onChange={(e) => setHpFormula(e.target.value)}
|
||||
placeholder="2d6+9"
|
||||
className="flex-1 px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!shared.rollHpFormula(hpFormula)}
|
||||
onClick={() => {
|
||||
const rolled = shared.rollHpFormula(hpFormula);
|
||||
if (rolled) setMaxHp(rolled);
|
||||
}}
|
||||
className="px-3 py-2 text-sm font-medium text-white bg-violet-700 hover:bg-violet-800 rounded-md transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
title="Roll formula, set Max HP"
|
||||
>
|
||||
<Dices size={16} className="inline" /> Reroll
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(participant.type === 'monster' || participant.type === 'npc') && (
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
@@ -972,7 +1009,8 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
const [participantType, setParticipantType] = useState('monster');
|
||||
const [selectedCharacterId, setSelectedCharacterId] = useState('');
|
||||
const [monsterInitMod, setMonsterInitMod] = useState(MONSTER_DEFAULT_INIT_MOD);
|
||||
const [maxHp, setMaxHp] = useState(DEFAULT_MAX_HP);
|
||||
const [maxHp, setMaxHp] = useState('');
|
||||
const [hpFormula, setHpFormula] = useState('');
|
||||
const [manualInitiative, setManualInitiative] = useState('');
|
||||
const [asNpc, setAsNpc] = useState(false);
|
||||
const [editingParticipant, setEditingParticipant] = useState(null);
|
||||
@@ -1002,11 +1040,11 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
if (selectedChar && selectedChar.defaultMaxHp) {
|
||||
setMaxHp(selectedChar.defaultMaxHp);
|
||||
} else {
|
||||
setMaxHp(DEFAULT_MAX_HP);
|
||||
setMaxHp('');
|
||||
}
|
||||
setAsNpc(false);
|
||||
} else if (participantType === 'monster') {
|
||||
setMaxHp(DEFAULT_MAX_HP);
|
||||
setMaxHp('');
|
||||
setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD);
|
||||
}
|
||||
}, [selectedCharacterId, participantType, campaignCharacters]);
|
||||
@@ -1039,6 +1077,11 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
} else {
|
||||
modifier = parseInt(monsterInitMod, 10) || 0;
|
||||
participantAsNpc = asNpc;
|
||||
// maxHp wins if both set; else roll formula
|
||||
if (!maxHp && hpFormula.trim()) {
|
||||
const rolled = shared.rollHpFormula(hpFormula);
|
||||
if (rolled) currentMaxHp = rolled;
|
||||
}
|
||||
}
|
||||
|
||||
const computedInitiative = manualInit ? finalInitiative : (initiativeRoll + modifier);
|
||||
@@ -1050,6 +1093,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
initiative: computedInitiative,
|
||||
maxHp: currentMaxHp,
|
||||
currentHp: currentMaxHp,
|
||||
hpFormula: participantType === 'monster' ? (hpFormula.trim() || null) : null,
|
||||
conditions: [],
|
||||
isActive: true,
|
||||
status: 'conscious',
|
||||
@@ -1071,7 +1115,8 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
setTimeout(() => setLastRollDetails(null), ROLL_DISPLAY_DURATION);
|
||||
|
||||
setParticipantName('');
|
||||
setMaxHp(DEFAULT_MAX_HP);
|
||||
setMaxHp('');
|
||||
setHpFormula('');
|
||||
setSelectedCharacterId('');
|
||||
setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD);
|
||||
setAsNpc(false);
|
||||
@@ -1332,7 +1377,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
e.preventDefault();
|
||||
handleAddParticipant();
|
||||
}}
|
||||
className="grid grid-cols-1 md:grid-cols-6 gap-x-4 gap-y-2 mb-4 p-3 bg-stone-800 rounded items-end"
|
||||
className="grid grid-cols-1 md:grid-cols-12 gap-x-4 gap-y-2 mb-4 p-3 bg-stone-800 rounded items-end"
|
||||
>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-stone-300">Type</label>
|
||||
@@ -1352,7 +1397,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
|
||||
{participantType === 'monster' ? (
|
||||
<>
|
||||
<div className="md:col-span-4">
|
||||
<div className="md:col-span-10">
|
||||
<label htmlFor="monsterName" className="block text-sm font-medium text-stone-300">
|
||||
Monster Name
|
||||
</label>
|
||||
@@ -1365,7 +1410,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<div className="md:col-span-3">
|
||||
<label htmlFor="monsterInitMod" className="block text-sm font-medium text-stone-300">
|
||||
Init Mod
|
||||
</label>
|
||||
@@ -1377,7 +1422,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<div className="md:col-span-3">
|
||||
<label htmlFor="manualInitiative" className="block text-sm font-medium text-stone-300">
|
||||
Initiative <span className="text-xs text-stone-400">(blank=roll)</span>
|
||||
</label>
|
||||
@@ -1390,7 +1435,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<div className="md:col-span-3">
|
||||
<label htmlFor="monsterMaxHp" className="block text-sm font-medium text-stone-300">
|
||||
Max HP
|
||||
</label>
|
||||
@@ -1402,7 +1447,20 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 flex items-center pt-5">
|
||||
<div className="md:col-span-3">
|
||||
<label htmlFor="monsterHpFormula" className="block text-sm font-medium text-stone-300">
|
||||
HP Formula (e.g. 2d6+9)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="monsterHpFormula"
|
||||
value={hpFormula}
|
||||
onChange={(e) => setHpFormula(e.target.value)}
|
||||
placeholder="2d6+9"
|
||||
className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-12 flex items-center pt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="asNpc"
|
||||
@@ -1457,7 +1515,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="md:col-span-6 flex justify-end mt-2">
|
||||
<div className="md:col-span-12 flex justify-end mt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={encounter.isStarted && !encounter.isPaused}
|
||||
|
||||
Reference in New Issue
Block a user