fix: custom conditions apply on add; correct server WS env docs

Custom conditions (per-campaign freeform) previously only persisted to the
campaign palette — Add button did NOT apply them to the targeted participant,
and badges rendered via CONDITIONS (missing custom ids) so applied custom
conditions did not display. Now:

- addCustomCondition(label, participantId) persists to palette AND toggles
  onto the participant in one step. Enter + Add button both pass p.id.
- Admin badge lookup uses allConditions (built-ins + custom), not CONDITIONS.

Shared contract: toggleCondition accepts ANY string. Prove it in tests:
- turn.combat.test.js adds CUSTOM_CONDITIONS ('hexed','rager',
  'marked_for_death','🛡️blessed') to the seeded queue + random pool, asserts
  condition-not-string invariant, and a dedicated add/remove round-trip test.
- replay-combat.js mirrors the same CUSTOM_CONDITIONS through the live backend.

Server-mode docs/env: REACT_APP_BACKEND_WS (stale pre-rename) -> correct
REACT_APP_BACKEND_REALTIME_URL=ws://.../ws in README, env.example,
docker/Dockerfile (STORAGE=ws -> server). DEVELOPMENT.md + scripts/README.md
now point to ./scripts/dev-start.sh as the primary dev entrypoint.

133 shared tests green.
This commit is contained in:
david raistrick
2026-07-04 20:41:34 -04:00
parent 1ad4b660a4
commit 7bcf01dcf9
8 changed files with 116 additions and 30 deletions
+22 -13
View File
@@ -1100,19 +1100,28 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
}
};
// Persist new freeform condition to campaign doc. Persists across encounters.
const addCustomCondition = async (label) => {
// Add freeform condition: persist to campaign palette + apply to current participant.
const addCustomCondition = async (label, participantId = null) => {
const trimmed = (label || '').trim();
if (!trimmed || !campaignId) return;
// dedupe against built-ins + existing custom (case-insensitive)
setCustomConditionInput('');
const exists = allConditions.some(c => c.label.toLowerCase() === trimmed.toLowerCase());
if (exists) { setCustomConditionInput(''); return; }
const next = [...customConditions, trimmed];
try {
await storage.updateDoc(getPath.campaign(campaignId), { customConditions: next });
setCustomConditionInput('');
} catch (err) {
// fall through silently
if (!exists) {
try {
await storage.updateDoc(getPath.campaign(campaignId), { customConditions: [...customConditions, trimmed] });
} catch (err) {}
}
if (participantId) {
const cur = (encounter.participants.find(p => p.id === participantId)?.conditions) || [];
if (!cur.includes(trimmed)) {
try {
const { patch, log } = combatToggleCondition(encounter, participantId, trimmed);
await storage.updateDoc(encounterPath, patch);
logAction(log.message.replace(trimmed, trimmed), { encounterName: encounter.name }, {
encounterPath, updates: log.undo,
});
} catch (err) {}
}
}
};
@@ -1443,7 +1452,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
{(p.conditions || []).length > 0 && (
<div className="mt-1 flex flex-wrap gap-1">
{(p.conditions || []).map(cId => {
const cond = CONDITIONS.find(c => c.id === cId);
const cond = allConditions.find(c => c.id === cId);
return cond ? (
<span
key={cId}
@@ -1493,7 +1502,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
value={customConditionInput}
onChange={(e) => setCustomConditionInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') { e.preventDefault(); addCustomCondition(customConditionInput); }
if (e.key === 'Enter') { e.preventDefault(); addCustomCondition(customConditionInput, p.id); }
}}
placeholder="Add custom condition..."
className="flex-grow px-2 py-1 text-xs rounded bg-stone-800 border border-stone-600 text-stone-200 placeholder-stone-500 focus:outline-none focus:border-amber-500"
@@ -1501,7 +1510,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
/>
<button
type="button"
onClick={() => addCustomCondition(customConditionInput)}
onClick={() => addCustomCondition(customConditionInput, p.id)}
disabled={!customConditionInput.trim()}
className="px-2 py-1 rounded text-xs bg-amber-700 border border-amber-500 text-white hover:bg-amber-600 disabled:opacity-40 disabled:cursor-not-allowed"
>