import type { DomainEvent } from "./events.js"; import type { PlayerCharacter, PlayerCharacterId, } from "./player-character-types.js"; import { VALID_PLAYER_COLORS, VALID_PLAYER_ICONS, } from "./player-character-types.js"; import type { DomainError } from "./types.js"; export interface EditPlayerCharacterSuccess { readonly characters: readonly PlayerCharacter[]; readonly events: DomainEvent[]; } interface EditFields { readonly name?: string; readonly ac?: number; readonly maxHp?: number; readonly color?: string; readonly icon?: string; } function validateFields(fields: EditFields): DomainError | null { if (fields.name !== undefined && fields.name.trim() === "") { return { kind: "domain-error", code: "invalid-name", message: "Player character name must not be empty", }; } if ( fields.ac !== undefined && (!Number.isInteger(fields.ac) || fields.ac < 0) ) { return { kind: "domain-error", code: "invalid-ac", message: "AC must be a non-negative integer", }; } if ( fields.maxHp !== undefined && (!Number.isInteger(fields.maxHp) || fields.maxHp < 1) ) { return { kind: "domain-error", code: "invalid-max-hp", message: "Max HP must be a positive integer", }; } if (fields.color !== undefined && !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)) { return { kind: "domain-error", code: "invalid-icon", message: `Invalid icon: ${fields.icon}`, }; } return null; } function applyFields( existing: PlayerCharacter, fields: EditFields, ): PlayerCharacter { return { id: existing.id, name: fields.name !== undefined ? fields.name.trim() : existing.name, ac: fields.ac !== undefined ? fields.ac : existing.ac, maxHp: fields.maxHp !== undefined ? fields.maxHp : existing.maxHp, color: fields.color !== undefined ? (fields.color as PlayerCharacter["color"]) : existing.color, icon: fields.icon !== undefined ? (fields.icon as PlayerCharacter["icon"]) : existing.icon, }; } export function editPlayerCharacter( characters: readonly PlayerCharacter[], id: PlayerCharacterId, fields: EditFields, ): EditPlayerCharacterSuccess | DomainError { const index = characters.findIndex((c) => c.id === id); if (index === -1) { return { kind: "domain-error", code: "player-character-not-found", message: `Player character not found: ${id}`, }; } const validationError = validateFields(fields); if (validationError) return validationError; const existing = characters[index]; const updated = applyFields(existing, fields); if ( updated.name === existing.name && updated.ac === existing.ac && updated.maxHp === existing.maxHp && updated.color === existing.color && updated.icon === existing.icon ) { return { kind: "domain-error", code: "no-changes", message: "No fields changed", }; } const newList = characters.map((c, i) => (i === index ? updated : c)); return { characters: newList, events: [ { type: "PlayerCharacterUpdated", playerCharacterId: id, oldName: existing.name, newName: updated.name, }, ], }; }