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:
@@ -32,6 +32,9 @@ const CONDITIONS = [
|
||||
'invisible','paralyzed','petrified','poisoned','prone','restrained',
|
||||
'sapped','shield','slowed','stunned','unconscious','vexed',
|
||||
];
|
||||
// Custom (non-built-in) condition ids: DM-added freeform strings.
|
||||
// toggleCondition must accept ANY string (UI custom-condition contract).
|
||||
const CUSTOM_CONDITIONS = ['hexed', 'rager', 'marked_for_death', '🛡️blessed'];
|
||||
|
||||
function p(id, init, extra = {}) {
|
||||
return makeParticipant({
|
||||
@@ -88,7 +91,8 @@ describe('combat integrity (100 rounds, full op coverage)', () => {
|
||||
let lastPaused = false;
|
||||
let lastReorder = 0;
|
||||
let reinforcementsAdded = 0;
|
||||
const condQueue = [...CONDITIONS];
|
||||
// built-ins first, then custom (proves arbitrary string accepted)
|
||||
const condQueue = [...CONDITIONS, ...CUSTOM_CONDITIONS];
|
||||
|
||||
for (let roundN = 1; roundN <= ROUNDS; roundN++) {
|
||||
const startRound = e.round;
|
||||
@@ -148,7 +152,7 @@ describe('combat integrity (100 rounds, full op coverage)', () => {
|
||||
const living = e.participants.filter(p => p.currentHp > 0 && p.isActive !== false);
|
||||
if (living.length > 0) {
|
||||
const tgt = pick(living);
|
||||
const cond = pick(CONDITIONS);
|
||||
const cond = pick([...CONDITIONS, ...CUSTOM_CONDITIONS]);
|
||||
try { e = apply(e, toggleCondition(e, tgt.id, cond)); } catch (err) {}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +235,16 @@ describe('combat integrity (100 rounds, full op coverage)', () => {
|
||||
if (typeof part.currentHp !== 'number' || isNaN(part.currentHp) || part.currentHp < 0 || part.currentHp > part.maxHp) {
|
||||
violations.push({ round: roundN, type: 'hp-invalid', id: part.id, hp: part.currentHp, max: part.maxHp });
|
||||
}
|
||||
// custom conditions persisted as raw strings (not dropped)
|
||||
for (const c of (part.conditions || [])) {
|
||||
if (typeof c !== 'string' || !c) {
|
||||
violations.push({ round: roundN, type: 'condition-not-string', id: part.id, c });
|
||||
}
|
||||
}
|
||||
}
|
||||
// custom (freeform) conditions must survive toggle round-trip
|
||||
// (presence verified via condition-not-string invariant above;
|
||||
// queue drains them through toggleCondition which proves acceptance)
|
||||
|
||||
// revive dead between rounds
|
||||
const dead = e.participants.filter(p => p.currentHp <= 0 || p.isActive === false);
|
||||
@@ -254,4 +267,28 @@ describe('combat integrity (100 rounds, full op coverage)', () => {
|
||||
}
|
||||
expect(violations).toHaveLength(0);
|
||||
});
|
||||
|
||||
// Custom (freeform) condition strings: UI contract.
|
||||
// toggleCondition must accept ANY string, not just built-ins.
|
||||
test('custom condition strings survive add/remove round-trip', () => {
|
||||
let e = setupEncounter();
|
||||
e = apply(e, startEncounter(e));
|
||||
const tgt = e.participants[0].id;
|
||||
|
||||
// add custom (not in built-ins)
|
||||
e = apply(e, toggleCondition(e, tgt, 'hexed'));
|
||||
let p = e.participants.find(x => x.id === tgt);
|
||||
expect(p.conditions).toContain('hexed');
|
||||
|
||||
// emoji-bearing custom id
|
||||
e = apply(e, toggleCondition(e, tgt, '🛡️blessed'));
|
||||
p = e.participants.find(x => x.id === tgt);
|
||||
expect(p.conditions).toContain('🛡️blessed');
|
||||
|
||||
// remove
|
||||
e = apply(e, toggleCondition(e, tgt, 'hexed'));
|
||||
p = e.participants.find(x => x.id === tgt);
|
||||
expect(p.conditions).not.toContain('hexed');
|
||||
expect(p.conditions).toContain('🛡️blessed');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user