Make player character color and icon optional
Clicking an already-selected color or icon in the create/edit form now deselects it. PCs without a color use the default combatant styling; PCs without an icon show no icon. Domain, application, persistence, and display layers all updated to handle the optional fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,8 @@ export function createPlayerCharacterUseCase(
|
||||
name: string,
|
||||
ac: number,
|
||||
maxHp: number,
|
||||
color: string,
|
||||
icon: string,
|
||||
color: string | undefined,
|
||||
icon: string | undefined,
|
||||
): DomainEvent[] | DomainError {
|
||||
const characters = store.getAll();
|
||||
const result = createPlayerCharacter(
|
||||
|
||||
@@ -11,8 +11,8 @@ interface EditFields {
|
||||
readonly name?: string;
|
||||
readonly ac?: number;
|
||||
readonly maxHp?: number;
|
||||
readonly color?: string;
|
||||
readonly icon?: string;
|
||||
readonly color?: string | null;
|
||||
readonly icon?: string | null;
|
||||
}
|
||||
|
||||
export function editPlayerCharacterUseCase(
|
||||
|
||||
@@ -219,6 +219,49 @@ describe("createPlayerCharacter", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("allows undefined color", () => {
|
||||
const result = createPlayerCharacter(
|
||||
[],
|
||||
id,
|
||||
"Test",
|
||||
10,
|
||||
50,
|
||||
undefined,
|
||||
"sword",
|
||||
);
|
||||
if (isDomainError(result)) throw new Error(result.message);
|
||||
expect(result.characters[0].color).toBeUndefined();
|
||||
});
|
||||
|
||||
it("allows undefined icon", () => {
|
||||
const result = createPlayerCharacter(
|
||||
[],
|
||||
id,
|
||||
"Test",
|
||||
10,
|
||||
50,
|
||||
"blue",
|
||||
undefined,
|
||||
);
|
||||
if (isDomainError(result)) throw new Error(result.message);
|
||||
expect(result.characters[0].icon).toBeUndefined();
|
||||
});
|
||||
|
||||
it("allows both color and icon undefined", () => {
|
||||
const result = createPlayerCharacter(
|
||||
[],
|
||||
id,
|
||||
"Test",
|
||||
10,
|
||||
50,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
if (isDomainError(result)) throw new Error(result.message);
|
||||
expect(result.characters[0].color).toBeUndefined();
|
||||
expect(result.characters[0].icon).toBeUndefined();
|
||||
});
|
||||
|
||||
it("emits exactly one event on success", () => {
|
||||
const { events } = success([], "Test", 10, 50);
|
||||
expect(events).toHaveLength(1);
|
||||
|
||||
@@ -106,6 +106,22 @@ describe("editPlayerCharacter", () => {
|
||||
expect(result.events).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("clears color when set to null", () => {
|
||||
const result = editPlayerCharacter([makePC({ color: "green" })], id, {
|
||||
color: null,
|
||||
});
|
||||
if (isDomainError(result)) throw new Error(result.message);
|
||||
expect(result.characters[0].color).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears icon when set to null", () => {
|
||||
const result = editPlayerCharacter([makePC({ icon: "sword" })], id, {
|
||||
icon: null,
|
||||
});
|
||||
if (isDomainError(result)) throw new Error(result.message);
|
||||
expect(result.characters[0].icon).toBeUndefined();
|
||||
});
|
||||
|
||||
it("event includes old and new name", () => {
|
||||
const result = editPlayerCharacter([makePC()], id, { name: "Strider" });
|
||||
if (isDomainError(result)) throw new Error(result.message);
|
||||
|
||||
@@ -20,8 +20,8 @@ export function createPlayerCharacter(
|
||||
name: string,
|
||||
ac: number,
|
||||
maxHp: number,
|
||||
color: string,
|
||||
icon: string,
|
||||
color: string | undefined,
|
||||
icon: string | undefined,
|
||||
): CreatePlayerCharacterSuccess | DomainError {
|
||||
const trimmed = name.trim();
|
||||
|
||||
@@ -49,7 +49,7 @@ export function createPlayerCharacter(
|
||||
};
|
||||
}
|
||||
|
||||
if (!VALID_PLAYER_COLORS.has(color)) {
|
||||
if (color !== undefined && !VALID_PLAYER_COLORS.has(color)) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "invalid-color",
|
||||
@@ -57,7 +57,7 @@ export function createPlayerCharacter(
|
||||
};
|
||||
}
|
||||
|
||||
if (!VALID_PLAYER_ICONS.has(icon)) {
|
||||
if (icon !== undefined && !VALID_PLAYER_ICONS.has(icon)) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "invalid-icon",
|
||||
|
||||
@@ -18,8 +18,8 @@ interface EditFields {
|
||||
readonly name?: string;
|
||||
readonly ac?: number;
|
||||
readonly maxHp?: number;
|
||||
readonly color?: string;
|
||||
readonly icon?: string;
|
||||
readonly color?: string | null;
|
||||
readonly icon?: string | null;
|
||||
}
|
||||
|
||||
function validateFields(fields: EditFields): DomainError | null {
|
||||
@@ -50,14 +50,22 @@ function validateFields(fields: EditFields): DomainError | null {
|
||||
message: "Max HP must be a positive integer",
|
||||
};
|
||||
}
|
||||
if (fields.color !== undefined && !VALID_PLAYER_COLORS.has(fields.color)) {
|
||||
if (
|
||||
fields.color !== undefined &&
|
||||
fields.color !== null &&
|
||||
!VALID_PLAYER_COLORS.has(fields.color)
|
||||
) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "invalid-color",
|
||||
message: `Invalid color: ${fields.color}`,
|
||||
};
|
||||
}
|
||||
if (fields.icon !== undefined && !VALID_PLAYER_ICONS.has(fields.icon)) {
|
||||
if (
|
||||
fields.icon !== undefined &&
|
||||
fields.icon !== null &&
|
||||
!VALID_PLAYER_ICONS.has(fields.icon)
|
||||
) {
|
||||
return {
|
||||
kind: "domain-error",
|
||||
code: "invalid-icon",
|
||||
@@ -78,11 +86,11 @@ function applyFields(
|
||||
maxHp: fields.maxHp !== undefined ? fields.maxHp : existing.maxHp,
|
||||
color:
|
||||
fields.color !== undefined
|
||||
? (fields.color as PlayerCharacter["color"])
|
||||
? ((fields.color as PlayerCharacter["color"]) ?? undefined)
|
||||
: existing.color,
|
||||
icon:
|
||||
fields.icon !== undefined
|
||||
? (fields.icon as PlayerCharacter["icon"])
|
||||
? ((fields.icon as PlayerCharacter["icon"]) ?? undefined)
|
||||
: existing.icon,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ export interface PlayerCharacter {
|
||||
readonly name: string;
|
||||
readonly ac: number;
|
||||
readonly maxHp: number;
|
||||
readonly color: PlayerColor;
|
||||
readonly icon: PlayerIcon;
|
||||
readonly color?: PlayerColor;
|
||||
readonly icon?: PlayerIcon;
|
||||
}
|
||||
|
||||
export interface PlayerCharacterList {
|
||||
|
||||
Reference in New Issue
Block a user