Fix persistent damage tag ordering and differentiate condition icons
- Render persistent damage tags before the "+" button, not after - Use insertion order for conditions on the row instead of definition order - Differentiate Undetected condition (EyeClosed/slate) from Invisible (Ghost/violet) - Use purple for void persistent damage to distinguish from violet conditions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -618,14 +618,17 @@ export function CombatantRow({
|
|||||||
onRemove={(conditionId) => toggleCondition(id, conditionId)}
|
onRemove={(conditionId) => toggleCondition(id, conditionId)}
|
||||||
onDecrement={(conditionId) => decrementCondition(id, conditionId)}
|
onDecrement={(conditionId) => decrementCondition(id, conditionId)}
|
||||||
onOpenPicker={() => setPickerOpen((prev) => !prev)}
|
onOpenPicker={() => setPickerOpen((prev) => !prev)}
|
||||||
/>
|
>
|
||||||
</div>
|
|
||||||
{isPf2e && (
|
{isPf2e && (
|
||||||
<PersistentDamageTags
|
<PersistentDamageTags
|
||||||
entries={combatant.persistentDamage}
|
entries={combatant.persistentDamage}
|
||||||
onRemove={(damageType) => removePersistentDamage(id, damageType)}
|
onRemove={(damageType) =>
|
||||||
|
removePersistentDamage(id, damageType)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</ConditionTags>
|
||||||
|
</div>
|
||||||
{!!pickerOpen && (
|
{!!pickerOpen && (
|
||||||
<ConditionPicker
|
<ConditionPicker
|
||||||
anchorRef={conditionAnchorRef}
|
anchorRef={conditionAnchorRef}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
EarOff,
|
EarOff,
|
||||||
Eclipse,
|
Eclipse,
|
||||||
Eye,
|
Eye,
|
||||||
|
EyeClosed,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Flame,
|
Flame,
|
||||||
FlaskConical,
|
FlaskConical,
|
||||||
@@ -57,6 +58,7 @@ export const CONDITION_ICON_MAP: Record<string, LucideIcon> = {
|
|||||||
EarOff,
|
EarOff,
|
||||||
Eclipse,
|
Eclipse,
|
||||||
Eye,
|
Eye,
|
||||||
|
EyeClosed,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Flame,
|
Flame,
|
||||||
FlaskConical,
|
FlaskConical,
|
||||||
@@ -92,6 +94,7 @@ export const CONDITION_COLOR_CLASSES: Record<string, string> = {
|
|||||||
pink: "text-pink-400",
|
pink: "text-pink-400",
|
||||||
amber: "text-amber-400",
|
amber: "text-amber-400",
|
||||||
orange: "text-orange-400",
|
orange: "text-orange-400",
|
||||||
|
purple: "text-purple-400",
|
||||||
gray: "text-gray-400",
|
gray: "text-gray-400",
|
||||||
violet: "text-violet-400",
|
violet: "text-violet-400",
|
||||||
yellow: "text-yellow-400",
|
yellow: "text-yellow-400",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getConditionDescription,
|
getConditionDescription,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
||||||
import { cn } from "../lib/utils.js";
|
import { cn } from "../lib/utils.js";
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +19,7 @@ interface ConditionTagsProps {
|
|||||||
onRemove: (conditionId: ConditionId) => void;
|
onRemove: (conditionId: ConditionId) => void;
|
||||||
onDecrement: (conditionId: ConditionId) => void;
|
onDecrement: (conditionId: ConditionId) => void;
|
||||||
onOpenPicker: () => void;
|
onOpenPicker: () => void;
|
||||||
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConditionTags({
|
export function ConditionTags({
|
||||||
@@ -25,6 +27,7 @@ export function ConditionTags({
|
|||||||
onRemove,
|
onRemove,
|
||||||
onDecrement,
|
onDecrement,
|
||||||
onOpenPicker,
|
onOpenPicker,
|
||||||
|
children,
|
||||||
}: Readonly<ConditionTagsProps>) {
|
}: Readonly<ConditionTagsProps>) {
|
||||||
const { edition } = useRulesEditionContext();
|
const { edition } = useRulesEditionContext();
|
||||||
return (
|
return (
|
||||||
@@ -69,6 +72,7 @@ export function ConditionTags({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{children}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
title="Add condition"
|
title="Add condition"
|
||||||
|
|||||||
@@ -60,13 +60,13 @@ describe("toggleCondition", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maintains definition order when adding conditions", () => {
|
it("appends new conditions to the end (insertion order)", () => {
|
||||||
const e = enc([makeCombatant("A", [{ id: "poisoned" }])]);
|
const e = enc([makeCombatant("A", [{ id: "poisoned" }])]);
|
||||||
const { encounter } = success(e, "A", "blinded");
|
const { encounter } = success(e, "A", "blinded");
|
||||||
|
|
||||||
expect(encounter.combatants[0].conditions).toEqual([
|
expect(encounter.combatants[0].conditions).toEqual([
|
||||||
{ id: "blinded" },
|
|
||||||
{ id: "poisoned" },
|
{ id: "poisoned" },
|
||||||
|
{ id: "blinded" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,15 +109,16 @@ describe("toggleCondition", () => {
|
|||||||
expect(encounter.combatants[0].conditions).toBeUndefined();
|
expect(encounter.combatants[0].conditions).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves order across all conditions", () => {
|
it("preserves insertion order across all conditions", () => {
|
||||||
const order = CONDITION_DEFINITIONS.map((d) => d.id);
|
const order = CONDITION_DEFINITIONS.map((d) => d.id);
|
||||||
// Add in reverse order
|
// Add in reverse order — result should be reverse order (insertion order)
|
||||||
|
const reversed = [...order].reverse();
|
||||||
let e = enc([makeCombatant("A")]);
|
let e = enc([makeCombatant("A")]);
|
||||||
for (const cond of [...order].reverse()) {
|
for (const cond of reversed) {
|
||||||
const result = success(e, "A", cond);
|
const result = success(e, "A", cond);
|
||||||
e = result.encounter;
|
e = result.encounter;
|
||||||
}
|
}
|
||||||
expect(e.combatants[0].conditions).toEqual(order.map((id) => ({ id })));
|
expect(e.combatants[0].conditions).toEqual(reversed.map((id) => ({ id })));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -500,8 +500,8 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
|
|||||||
description5e: "",
|
description5e: "",
|
||||||
descriptionPf2e:
|
descriptionPf2e:
|
||||||
"Location unknown. Must pick a square to target; DC 11 flat check. Attacker is off-guard against your attacks.",
|
"Location unknown. Must pick a square to target; DC 11 flat check. Attacker is off-guard against your attacks.",
|
||||||
iconName: "Ghost",
|
iconName: "EyeClosed",
|
||||||
color: "violet",
|
color: "slate",
|
||||||
systems: ["pf2e"],
|
systems: ["pf2e"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const PERSISTENT_DAMAGE_DEFINITIONS: readonly PersistentDamageDefinition[
|
|||||||
color: "pink",
|
color: "pink",
|
||||||
},
|
},
|
||||||
{ type: "force", label: "Force", iconName: "Orbit", color: "indigo" },
|
{ type: "force", label: "Force", iconName: "Orbit", color: "indigo" },
|
||||||
{ type: "void", label: "Void", iconName: "Eclipse", color: "slate" },
|
{ type: "void", label: "Void", iconName: "Eclipse", color: "purple" },
|
||||||
{ type: "spirit", label: "Spirit", iconName: "Wind", color: "neutral" },
|
{ type: "spirit", label: "Spirit", iconName: "Wind", color: "neutral" },
|
||||||
{
|
{
|
||||||
type: "vitality",
|
type: "vitality",
|
||||||
|
|||||||
@@ -14,12 +14,6 @@ export interface ToggleConditionSuccess {
|
|||||||
readonly events: DomainEvent[];
|
readonly events: DomainEvent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortByDefinitionOrder(entries: ConditionEntry[]): ConditionEntry[] {
|
|
||||||
const order = CONDITION_DEFINITIONS.map((d) => d.id);
|
|
||||||
entries.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateConditionId(conditionId: ConditionId): DomainError | null {
|
function validateConditionId(conditionId: ConditionId): DomainError | null {
|
||||||
if (!VALID_CONDITION_IDS.has(conditionId)) {
|
if (!VALID_CONDITION_IDS.has(conditionId)) {
|
||||||
return {
|
return {
|
||||||
@@ -67,8 +61,7 @@ export function toggleCondition(
|
|||||||
newConditions = filtered.length > 0 ? filtered : undefined;
|
newConditions = filtered.length > 0 ? filtered : undefined;
|
||||||
event = { type: "ConditionRemoved", combatantId, condition: conditionId };
|
event = { type: "ConditionRemoved", combatantId, condition: conditionId };
|
||||||
} else {
|
} else {
|
||||||
const added = sortByDefinitionOrder([...current, { id: conditionId }]);
|
newConditions = [...current, { id: conditionId }];
|
||||||
newConditions = added;
|
|
||||||
event = { type: "ConditionAdded", combatantId, condition: conditionId };
|
event = { type: "ConditionAdded", combatantId, condition: conditionId };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,10 +118,7 @@ export function setConditionValue(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const added = sortByDefinitionOrder([
|
const added = [...current, { id: conditionId, value: clampedValue }];
|
||||||
...current,
|
|
||||||
{ id: conditionId, value: clampedValue },
|
|
||||||
]);
|
|
||||||
return {
|
return {
|
||||||
encounter: applyConditions(encounter, combatantId, added),
|
encounter: applyConditions(encounter, combatantId, added),
|
||||||
events: [
|
events: [
|
||||||
|
|||||||
Reference in New Issue
Block a user