Generic (non-5e) ruleset mode + UI polish + combat switch fix #7

Merged
robert merged 25 commits from feat/generic-ruleset into main 2026-07-08 22:54:04 -04:00
6 changed files with 172 additions and 64 deletions
Showing only changes of commit 863e8b3719 - Show all commits
+3 -2
View File
@@ -9,12 +9,13 @@ fullscreen and dont lock on main app dm view and the no-game-player view
also better vert tab layout - labelt friendly also better vert tab layout - labelt friendly
needs AC for players dude x needs AC for players dude
and quick entry hp and quick entry hp
hp do not carry from encounter to ecnounter!!! hp do not carry from encounter to ecnounter!!!
^feature to add back to campaign character after encounter?
maybe campaign toggle in charc section (choosing each end is DM overload will be missed forgotten done wront)
hp wont go over max and no temp hp support hp wont go over max and no temp hp support
+35
View File
@@ -0,0 +1,35 @@
// AC field on participant + builders.
const shared = require('@ttrpg/shared');
const { makeParticipant, buildMonsterParticipant, buildCharacterParticipant, rollHpFormula } = shared;
describe('AC field', () => {
test('makeParticipant accepts ac', () => {
const p = makeParticipant({ name: 'Orc', type: 'monster', initiative: 10, maxHp: 15, currentHp: 15, ac: 13 });
expect(p.ac).toBe(13);
});
test('makeParticipant ac defaults null', () => {
const p = makeParticipant({ name: 'Orc', type: 'monster', initiative: 10, maxHp: 15, currentHp: 15 });
expect(p.ac).toBeNull();
});
test('buildMonsterParticipant accepts ac', () => {
const { participant } = buildMonsterParticipant({ name: 'Goblin', maxHp: 7, initMod: 2, ac: 15 });
expect(participant.ac).toBe(15);
});
test('buildMonsterParticipant ac defaults null', () => {
const { participant } = buildMonsterParticipant({ name: 'Goblin', maxHp: 7 });
expect(participant.ac).toBeNull();
});
test('buildCharacterParticipant accepts character ac', () => {
const { participant } = buildCharacterParticipant({ id: 'c1', name: 'Fighter', defaultMaxHp: 30, defaultInitMod: 2, defaultAc: 18 });
expect(participant.ac).toBe(18);
});
test('buildCharacterParticipant ac defaults null when unset', () => {
const { participant } = buildCharacterParticipant({ id: 'c1', name: 'Fighter', defaultMaxHp: 30 });
expect(participant.ac).toBeNull();
});
});
+4 -1
View File
@@ -361,6 +361,7 @@ function makeParticipant(opts) {
deathSaveSuccesses: opts.deathSaveSuccesses || 0, deathSaveSuccesses: opts.deathSaveSuccesses || 0,
deathSaveFailures: opts.deathSaveFailures || 0, deathSaveFailures: opts.deathSaveFailures || 0,
hpFormula: opts.hpFormula || null, hpFormula: opts.hpFormula || null,
ac: opts.ac !== undefined ? opts.ac : null,
}; };
} }
@@ -377,12 +378,13 @@ function buildCharacterParticipant(character) {
initiative: finalInitiative, initiative: finalInitiative,
maxHp, maxHp,
currentHp: maxHp, currentHp: maxHp,
ac: character.defaultAc !== undefined ? character.defaultAc : null,
}), }),
roll: { roll: initiativeRoll, mod: modifier, total: finalInitiative }, roll: { roll: initiativeRoll, mod: modifier, total: finalInitiative },
}; };
} }
function buildMonsterParticipant({ name, maxHp, initMod, asNpc, hpFormula }) { function buildMonsterParticipant({ name, maxHp, initMod, asNpc, hpFormula, ac }) {
const initiativeRoll = rollD20(); const initiativeRoll = rollD20();
const modifier = initMod !== undefined ? initMod : MONSTER_DEFAULT_INIT_MOD; const modifier = initMod !== undefined ? initMod : MONSTER_DEFAULT_INIT_MOD;
const finalInitiative = initiativeRoll + modifier; const finalInitiative = initiativeRoll + modifier;
@@ -406,6 +408,7 @@ function buildMonsterParticipant({ name, maxHp, initMod, asNpc, hpFormula }) {
maxHp: hp, maxHp: hp,
currentHp: hp, currentHp: hp,
hpFormula: hpFormula || null, hpFormula: hpFormula || null,
ac: ac !== undefined ? ac : null,
}), }),
roll: { roll: initiativeRoll, mod: modifier, total: finalInitiative, hpRoll }, roll: { roll: initiativeRoll, mod: modifier, total: finalInitiative, hpRoll },
}; };
+101 -32
View File
@@ -583,6 +583,7 @@ function EditParticipantModal({ participant, onClose, onSave }) {
const [currentHp, setCurrentHp] = useState(participant.currentHp); const [currentHp, setCurrentHp] = useState(participant.currentHp);
const [maxHp, setMaxHp] = useState(participant.maxHp); const [maxHp, setMaxHp] = useState(participant.maxHp);
const [hpFormula, setHpFormula] = useState(participant.hpFormula || ''); const [hpFormula, setHpFormula] = useState(participant.hpFormula || '');
const [ac, setAc] = useState(participant.ac != null ? participant.ac : '');
const [asNpc, setAsNpc] = useState(participant.type === 'npc'); const [asNpc, setAsNpc] = useState(participant.type === 'npc');
const handleSubmit = (e) => { const handleSubmit = (e) => {
@@ -602,6 +603,7 @@ function EditParticipantModal({ participant, onClose, onSave }) {
currentHp: finalCurrentHp, currentHp: finalCurrentHp,
maxHp: finalMaxHp, maxHp: finalMaxHp,
hpFormula: (participant.type === 'monster' || participant.type === 'npc') ? (hpFormula.trim() || null) : null, hpFormula: (participant.type === 'monster' || participant.type === 'npc') ? (hpFormula.trim() || null) : null,
ac: ac !== '' ? parseInt(ac, 10) : null,
type: participant.type === 'monster' || participant.type === 'npc' ? (asNpc ? 'npc' : 'monster') : participant.type, type: participant.type === 'monster' || participant.type === 'npc' ? (asNpc ? 'npc' : 'monster') : participant.type,
}); });
}; };
@@ -618,7 +620,8 @@ function EditParticipantModal({ participant, onClose, onSave }) {
className="mt-1 block 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" className="mt-1 block 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>
<div> <div className="flex gap-4">
<div className="flex-1">
<label htmlFor="edit-initiative" className="block text-sm font-medium text-stone-300">Initiative</label> <label htmlFor="edit-initiative" className="block text-sm font-medium text-stone-300">Initiative</label>
<input <input
type="number" type="number"
@@ -628,6 +631,17 @@ function EditParticipantModal({ participant, onClose, onSave }) {
className="mt-1 block 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" className="mt-1 block 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>
<div className="flex-1">
<label className="block text-sm font-medium text-stone-300">AC</label>
<input
type="number"
value={ac}
onChange={(e) => setAc(e.target.value)}
placeholder="optional"
className="mt-1 block 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>
<div className="flex gap-4"> <div className="flex gap-4">
<div className="flex-1"> <div className="flex-1">
<label className="block text-sm font-medium text-stone-300">Current HP</label> <label className="block text-sm font-medium text-stone-300">Current HP</label>
@@ -656,7 +670,7 @@ function EditParticipantModal({ participant, onClose, onSave }) {
type="text" type="text"
value={hpFormula} value={hpFormula}
onChange={(e) => setHpFormula(e.target.value)} onChange={(e) => setHpFormula(e.target.value)}
placeholder="2d6+9" placeholder="e.g. 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" 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 <button
@@ -717,6 +731,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
const [characterName, setCharacterName] = useState(''); const [characterName, setCharacterName] = useState('');
const [defaultMaxHp, setDefaultMaxHp] = useState(DEFAULT_MAX_HP); const [defaultMaxHp, setDefaultMaxHp] = useState(DEFAULT_MAX_HP);
const [defaultInitMod, setDefaultInitMod] = useState(DEFAULT_INIT_MOD); const [defaultInitMod, setDefaultInitMod] = useState(DEFAULT_INIT_MOD);
const [defaultAc, setDefaultAc] = useState('');
const [editingCharacter, setEditingCharacter] = useState(null); const [editingCharacter, setEditingCharacter] = useState(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null); const [itemToDelete, setItemToDelete] = useState(null);
@@ -762,7 +777,8 @@ function CharacterManager({ campaignId, campaignCharacters }) {
id: generateId(), id: generateId(),
name: characterName.trim(), name: characterName.trim(),
defaultMaxHp: hp, defaultMaxHp: hp,
defaultInitMod: initMod defaultInitMod: initMod,
defaultAc: defaultAc !== '' ? parseInt(defaultAc, 10) : null
}; };
try { try {
@@ -772,12 +788,13 @@ function CharacterManager({ campaignId, campaignCharacters }) {
setCharacterName(''); setCharacterName('');
setDefaultMaxHp(DEFAULT_MAX_HP); setDefaultMaxHp(DEFAULT_MAX_HP);
setDefaultInitMod(DEFAULT_INIT_MOD); setDefaultInitMod(DEFAULT_INIT_MOD);
setDefaultAc('');
} catch (err) { } catch (err) {
console.error("Error adding character:", err); console.error("Error adding character:", err);
showToast("Failed to add character. Please try again."); } showToast("Failed to add character. Please try again."); }
}; };
const handleUpdateCharacter = async (characterId, newName, newDefaultMaxHp, newDefaultInitMod) => { const handleUpdateCharacter = async (characterId, newName, newDefaultMaxHp, newDefaultInitMod, newDefaultAc) => {
if (!db || !newName.trim() || !campaignId) return; if (!db || !newName.trim() || !campaignId) return;
const hp = parseInt(newDefaultMaxHp, 10); const hp = parseInt(newDefaultMaxHp, 10);
@@ -794,7 +811,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
const updatedCharacters = campaignCharacters.map(c => const updatedCharacters = campaignCharacters.map(c =>
c.id === characterId c.id === characterId
? { ...c, name: newName.trim(), defaultMaxHp: hp, defaultInitMod: initMod } ? { ...c, name: newName.trim(), defaultMaxHp: hp, defaultInitMod: initMod, defaultAc: newDefaultAc !== undefined && newDefaultAc !== '' ? parseInt(newDefaultAc, 10) : null }
: c : c
); );
@@ -843,8 +860,8 @@ function CharacterManager({ campaignId, campaignCharacters }) {
{isOpen && ( {isOpen && (
<> <>
<form onSubmit={(e) => { e.preventDefault(); handleAddCharacter(); }} className="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4 items-end"> <form onSubmit={(e) => { e.preventDefault(); handleAddCharacter(); }} className="grid grid-cols-2 sm:grid-cols-12 gap-2 mb-4 items-end">
<div className="sm:col-span-1"> <div className="col-span-2 sm:col-span-6">
<label htmlFor="characterName" className="block text-xs font-medium text-stone-400"> <label htmlFor="characterName" className="block text-xs font-medium text-stone-400">
Name Name
</label> </label>
@@ -857,19 +874,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
className="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" className="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>
<div className="w-full sm:w-auto"> <div className="sm:col-span-2">
<label htmlFor="defaultMaxHp" className="block text-xs font-medium text-stone-400">
Default HP
</label>
<input
type="number"
id="defaultMaxHp"
value={defaultMaxHp}
onChange={(e) => setDefaultMaxHp(e.target.value)}
className="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="w-full sm:w-auto">
<label htmlFor="defaultInitMod" className="block text-xs font-medium text-stone-400"> <label htmlFor="defaultInitMod" className="block text-xs font-medium text-stone-400">
Init Mod Init Mod
</label> </label>
@@ -881,9 +886,33 @@ function CharacterManager({ campaignId, campaignCharacters }) {
className="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" className="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>
<div className="sm:col-span-2">
<label htmlFor="defaultAc" className="block text-xs font-medium text-stone-400">
AC
</label>
<input
type="number"
id="defaultAc"
value={defaultAc}
onChange={(e) => setDefaultAc(e.target.value)}
className="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="sm:col-span-2">
<label htmlFor="defaultMaxHp" className="block text-xs font-medium text-stone-400">
HP
</label>
<input
type="number"
id="defaultMaxHp"
value={defaultMaxHp}
onChange={(e) => setDefaultMaxHp(e.target.value)}
className="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>
<button <button
type="submit" type="submit"
className="sm:col-span-3 sm:w-auto sm:justify-self-end px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors flex items-center justify-center" className="col-span-2 sm:col-span-12 sm:w-auto sm:justify-self-end px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors flex items-center justify-center"
> >
<PlusCircle size={18} className="mr-1" /> Add Character <PlusCircle size={18} className="mr-1" /> Add Character
</button> </button>
@@ -914,17 +943,23 @@ function CharacterManager({ campaignId, campaignCharacters }) {
character.id, character.id,
editingCharacter.name, editingCharacter.name,
editingCharacter.defaultMaxHp, editingCharacter.defaultMaxHp,
editingCharacter.defaultInitMod editingCharacter.defaultInitMod,
editingCharacter.defaultAc
); );
}} }}
className="flex-grow flex flex-wrap gap-2 items-center" className="flex-grow flex flex-wrap gap-2 items-center"
> >
<div className="flex flex-col flex-grow min-w-[100px]">
<label className="text-[10px] text-stone-400 mb-0.5">Name</label>
<input <input
type="text" type="text"
value={editingCharacter.name} value={editingCharacter.name}
onChange={(e) => setEditingCharacter({ ...editingCharacter, name: e.target.value })} onChange={(e) => setEditingCharacter({ ...editingCharacter, name: e.target.value })}
className="flex-grow min-w-[100px] px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white" className="w-full px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
/> />
</div>
<div className="flex flex-col">
<label className="text-[10px] text-stone-400 mb-0.5">HP</label>
<input <input
type="number" type="number"
value={editingCharacter.defaultMaxHp} value={editingCharacter.defaultMaxHp}
@@ -932,6 +967,9 @@ function CharacterManager({ campaignId, campaignCharacters }) {
className="w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white" className="w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
title="Default Max HP" title="Default Max HP"
/> />
</div>
<div className="flex flex-col">
<label className="text-[10px] text-stone-400 mb-0.5">Init</label>
<input <input
type="number" type="number"
value={editingCharacter.defaultInitMod} value={editingCharacter.defaultInitMod}
@@ -939,6 +977,17 @@ function CharacterManager({ campaignId, campaignCharacters }) {
className="w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white" className="w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
title="Default Init Mod" title="Default Init Mod"
/> />
</div>
<div className="flex flex-col">
<label className="text-[10px] text-stone-400 mb-0.5">AC</label>
<input
type="number"
value={editingCharacter.defaultAc ?? ''}
onChange={(e) => setEditingCharacter({ ...editingCharacter, defaultAc: e.target.value })}
className="w-16 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
title="AC"
/>
</div>
<button type="submit" className="p-1 text-green-400 hover:text-green-300"> <button type="submit" className="p-1 text-green-400 hover:text-green-300">
<Save size={18} /> <Save size={18} />
</button> </button>
@@ -955,7 +1004,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
<span className="text-stone-100"> <span className="text-stone-100">
{character.name}{' '} {character.name}{' '}
<span className="text-xs text-stone-400"> <span className="text-xs text-stone-400">
(HP: {character.defaultMaxHp || 'N/A'}, Init Mod: {formatInitMod(character.defaultInitMod)}) (HP: {character.defaultMaxHp || 'N/A'}, Init Mod: {formatInitMod(character.defaultInitMod)}{character.defaultAc != null ? `, AC: ${character.defaultAc}` : ''})
</span> </span>
</span> </span>
<div className="flex space-x-2"> <div className="flex space-x-2">
@@ -964,7 +1013,8 @@ function CharacterManager({ campaignId, campaignCharacters }) {
id: character.id, id: character.id,
name: character.name, name: character.name,
defaultMaxHp: character.defaultMaxHp || DEFAULT_MAX_HP, defaultMaxHp: character.defaultMaxHp || DEFAULT_MAX_HP,
defaultInitMod: character.defaultInitMod || DEFAULT_INIT_MOD defaultInitMod: character.defaultInitMod || DEFAULT_INIT_MOD,
defaultAc: character.defaultAc ?? ''
})} })}
className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-stone-700 hover:bg-stone-600" className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-stone-700 hover:bg-stone-600"
aria-label="Edit character" aria-label="Edit character"
@@ -1013,6 +1063,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
const [maxHp, setMaxHp] = useState(''); const [maxHp, setMaxHp] = useState('');
const [hpFormula, setHpFormula] = useState(''); const [hpFormula, setHpFormula] = useState('');
const [manualInitiative, setManualInitiative] = useState(''); const [manualInitiative, setManualInitiative] = useState('');
const [monsterAc, setMonsterAc] = useState('');
const [asNpc, setAsNpc] = useState(false); const [asNpc, setAsNpc] = useState(false);
const [editingParticipant, setEditingParticipant] = useState(null); const [editingParticipant, setEditingParticipant] = useState(null);
const [hpChangeValues, setHpChangeValues] = useState({}); const [hpChangeValues, setHpChangeValues] = useState({});
@@ -1095,6 +1146,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
maxHp: currentMaxHp, maxHp: currentMaxHp,
currentHp: currentMaxHp, currentHp: currentMaxHp,
hpFormula: participantType === 'monster' ? (hpFormula.trim() || null) : null, hpFormula: participantType === 'monster' ? (hpFormula.trim() || null) : null,
ac: participantType === 'monster' && monsterAc !== '' ? parseInt(monsterAc, 10) : (participantType === 'character' ? (campaignCharacters.find(c => c.id === selectedCharacterId)?.defaultAc ?? null) : null),
conditions: [], conditions: [],
isActive: true, isActive: true,
status: 'conscious', status: 'conscious',
@@ -1120,6 +1172,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
setHpFormula(''); setHpFormula('');
setSelectedCharacterId(''); setSelectedCharacterId('');
setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD); setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD);
setMonsterAc('');
setAsNpc(false); setAsNpc(false);
setManualInitiative(''); setManualInitiative('');
} catch (err) { } catch (err) {
@@ -1411,7 +1464,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" 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>
<div className="md:col-span-3"> <div className="md:col-span-2">
<label htmlFor="monsterInitMod" className="block text-sm font-medium text-stone-300"> <label htmlFor="monsterInitMod" className="block text-sm font-medium text-stone-300">
Init Mod Init Mod
</label> </label>
@@ -1423,7 +1476,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" 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>
<div className="md:col-span-3"> <div className="md:col-span-2">
<label htmlFor="manualInitiative" className="block text-sm font-medium text-stone-300"> <label htmlFor="manualInitiative" className="block text-sm font-medium text-stone-300">
Initiative <span className="text-xs text-stone-400">(blank=roll)</span> Initiative <span className="text-xs text-stone-400">(blank=roll)</span>
</label> </label>
@@ -1436,7 +1489,17 @@ 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" 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>
<div className="md:col-span-3"> <div className="md:col-span-2">
<label htmlFor="monsterAc" className="block text-sm font-medium text-stone-300">AC</label>
<input
type="number"
id="monsterAc"
value={monsterAc}
onChange={(e) => setMonsterAc(e.target.value)}
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">
<label htmlFor="monsterMaxHp" className="block text-sm font-medium text-stone-300"> <label htmlFor="monsterMaxHp" className="block text-sm font-medium text-stone-300">
Max HP Max HP
</label> </label>
@@ -1448,16 +1511,16 @@ 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" 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>
<div className="md:col-span-3"> <div className="md:col-span-4">
<label htmlFor="monsterHpFormula" className="block text-sm font-medium text-stone-300"> <label htmlFor="monsterHpFormula" className="block text-sm font-medium text-stone-300">
HP Formula (e.g. 2d6+9) HP Formula
</label> </label>
<input <input
type="text" type="text"
id="monsterHpFormula" id="monsterHpFormula"
value={hpFormula} value={hpFormula}
onChange={(e) => setHpFormula(e.target.value)} onChange={(e) => setHpFormula(e.target.value)}
placeholder="2d6+9" placeholder="e.g. 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" 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>
@@ -1579,6 +1642,12 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
<p className="font-semibold text-lg text-white"> <p className="font-semibold text-lg text-white">
{isZeroHp && <span className="mr-2"></span>} {isZeroHp && <span className="mr-2"></span>}
{p.name} <span className="text-xs">({participantDisplayType})</span> {p.name} <span className="text-xs">({participantDisplayType})</span>
{p.ac != null && (
<span className="ml-2 inline-flex items-baseline gap-1 px-2 py-0.5 bg-sky-900 border border-sky-500 rounded">
<span className="text-[10px] text-sky-300 font-semibold tracking-wide">AC</span>
<span className="text-sky-100 text-lg font-bold leading-none">{p.ac}</span>
</span>
)}
{isCurrentTurn && !encounter.isPaused && ( {isCurrentTurn && !encounter.isPaused && (
<span className="ml-2 px-2 py-0.5 bg-yellow-400 text-black text-xs font-bold rounded-full inline-flex items-center"> <span className="ml-2 px-2 py-0.5 bg-yellow-400 text-black text-xs font-bold rounded-full inline-flex items-center">
<Zap size={12} className="mr-1" /> CURRENT <Zap size={12} className="mr-1" /> CURRENT
@@ -1604,7 +1673,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
onFocus={(e) => e.target.select()} onFocus={(e) => e.target.select()}
onBlur={(e) => { if (e.target.value !== String(p.initiative)) handleInlineInitiative(p.id, e.target.value); }} onBlur={(e) => { if (e.target.value !== String(p.initiative)) handleInlineInitiative(p.id, e.target.value); }}
onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); }} onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); }}
className="w-10 px-1 py-0.5 bg-stone-800 border border-stone-700 rounded-md shadow-sm text-white text-sm focus:outline-none focus:ring-1 focus:ring-amber-600 focus:border-amber-600 disabled:opacity-50 disabled:cursor-not-allowed" className="w-8 px-1 py-0.5 bg-stone-800 border border-stone-700 rounded-md shadow-sm text-white text-sm text-center focus:outline-none focus:ring-1 focus:ring-amber-600 focus:border-amber-600 disabled:opacity-50 disabled:cursor-not-allowed"
aria-label={`Initiative for ${p.name}`} aria-label={`Initiative for ${p.name}`}
/> />
</span> </span>
+1 -1
View File
@@ -62,7 +62,7 @@ describe('Campaign -> Firebase', () => {
// CharacterManager form // CharacterManager form
fireEvent.change(screen.getByPlaceholderText('Character name'), { target: { value: 'Brog' } }); fireEvent.change(screen.getByPlaceholderText('Character name'), { target: { value: 'Brog' } });
fireEvent.change(screen.getByLabelText(/Default HP/i), { target: { value: '25' } }); fireEvent.change(screen.getByLabelText(/^HP$/i), { target: { value: '25' } });
fireEvent.change(screen.getByLabelText(/Init Mod/i), { target: { value: '3' } }); fireEvent.change(screen.getByLabelText(/Init Mod/i), { target: { value: '3' } });
fireEvent.click(screen.getByRole('button', { name: /Add Character/i })); fireEvent.click(screen.getByRole('button', { name: /Add Character/i }));
+1 -1
View File
@@ -20,7 +20,7 @@ async function addCharacterToEncounter(name = 'Hero', hp = 10) {
await selectCampaignByName(`DS-${name}`); await selectCampaignByName(`DS-${name}`);
fireEvent.change(screen.getByPlaceholderText('Character name'), { target: { value: name } }); fireEvent.change(screen.getByPlaceholderText('Character name'), { target: { value: name } });
fireEvent.change(screen.getByLabelText(/Default HP/i), { target: { value: String(hp) } }); fireEvent.change(screen.getByLabelText(/^HP$/i), { target: { value: String(hp) } });
fireEvent.click(screen.getByRole('button', { name: /Add Character/i })); fireEvent.click(screen.getByRole('button', { name: /Add Character/i }));
await waitFor(() => findCalls('updateDoc', '/campaigns/').find(c => c.data.players?.length === 1)); await waitFor(() => findCalls('updateDoc', '/campaigns/').find(c => c.data.players?.length === 1));
const charId = findCalls('updateDoc', '/campaigns/').find(c => c.data.players?.length === 1).data.players[0].id; const charId = findCalls('updateDoc', '/campaigns/').find(c => c.data.players?.length === 1).data.players[0].id;