import React, { CSSProperties } from "react"; import { Checkbox } from "../../checkbox"; import { AttachedCreatureSymbol, CreatureSymbol, NestedCreatureSymbol, } from "../../creature-symbol"; import { RememberedDetails } from "../../remembered-details"; import { AttachmentPointType, ATTACHMENT_POINT_TYPES } from "../../specs"; import { SvgVocabulary } from "../../svg-vocabulary"; import { capitalize, range } from "../../util"; import { VocabularyWidget } from "../../vocabulary-widget"; type SymbolWithIndices = CreatureSymbol & { indices: number[] }; function getAvailableIndices( symbols: SymbolWithIndices[], numIndices: number ): number[] { const available = new Set(range(numIndices)); for (let s of symbols) { for (let i of s.indices) { available.delete(i); } } return Array.from(available); } function getImmutableIndices( symbols: SymbolWithIndices[], symbol: SymbolWithIndices ): Set { const immutableIndices = new Set(); for (let s of symbols) { if (s !== symbol) { for (let idx of s.indices) { // This index is taken up by another attachment. immutableIndices.add(idx); } } } if (symbol.indices.length === 1) { // This attachment is only for one index, don't let it be unselected. immutableIndices.add(symbol.indices[0]); } return immutableIndices; } type IndicesWidgetProps = { label: string; numIndices: number; immutableIndices: Set; symbol: T; onChange: (symbol: T) => void; }; function IndicesWidget({ symbol, onChange, label, numIndices, immutableIndices, }: IndicesWidgetProps): JSX.Element { const allIndices = range(numIndices); const toggleIndex = (i: number) => { const indices = symbol.indices.slice(); const idx = indices.indexOf(i); if (idx === -1) { indices.push(i); } else { indices.splice(idx, 1); } onChange({ ...symbol, indices }); }; return ( <>
{label}
{allIndices.map((i) => { return ( toggleIndex(i)} disabled={immutableIndices.has(i)} value={symbol.indices.includes(i)} /> ); })}
); } class ArrayManipulator { constructor(readonly items: T[]) {} strictIndexOf(item: T): number { const index = this.items.indexOf(item); if (index === -1) { throw new Error(`Assertion failure, unable to find item`); } return index; } withItemRemoved(item: T): T[] { const items = this.items.slice(); items.splice(this.strictIndexOf(item), 1); return items; } withItemUpdated(originalItem: T, updatedItem: T): T[] { const items = this.items.slice(); items[this.strictIndexOf(originalItem)] = updatedItem; return items; } withItemAdded(item: T): T[] { return [...this.items, item]; } } function NestingEditor({ creature, onChange, idPrefix, }: CreatureEditorProps): JSX.Element | null { const specs = creature.data.specs || {}; const nests = new ArrayManipulator(creature.nests); const handleChangedNests = (nests: NestedCreatureSymbol[]) => onChange({ ...creature, nests, }); const deleteNested = (nested: NestedCreatureSymbol) => handleChangedNests(nests.withItemRemoved(nested)); const updateNested = ( orig: NestedCreatureSymbol, updated: NestedCreatureSymbol ) => handleChangedNests(nests.withItemUpdated(orig, updated)); const addNested = (indices: number[]) => handleChangedNests( nests.withItemAdded({ indices, data: SvgVocabulary.items[0], invertColors: false, attachments: [], nests: [], }) ); const points = specs.nesting || []; const symbolHasNesting = points.length > 0; const creatureDefinesNesting = creature.nests.length > 0; if (!symbolHasNesting && !creatureDefinesNesting) { return null; } const style: CSSProperties = {}; let title = `Symbol defines nesting and cluster provides at least one`; if (!symbolHasNesting) { style.textDecoration = "line-through"; title = `Cluster defines nesting but symbol doesn't define any`; // Honestly, this is just going to confuse people, so leave it out // for now. return null; } if (!creatureDefinesNesting) { style.color = "gray"; title = `Symbol defines nesting but cluster doesn't provide any`; } const availableIndices = getAvailableIndices(creature.nests, points.length); return (
Nesting
{creature.nests.map((nest, i) => { const atIdPrefix = `${idPrefix}_nest_${i}_`; const immutableIndices = getImmutableIndices(creature.nests, nest); return (
); })} {availableIndices.length > 0 && ( )}
); } function AttachmentEditor({ creature, onChange, idPrefix, }: CreatureEditorProps): JSX.Element { const specs = creature.data.specs || {}; const attachments = new ArrayManipulator(creature.attachments); const handleChangedAttachments = (attachments: AttachedCreatureSymbol[]) => onChange({ ...creature, attachments }); const deleteAttachment = (attachment: AttachedCreatureSymbol) => handleChangedAttachments(attachments.withItemRemoved(attachment)); const updateAttachment = ( originalAttachment: AttachedCreatureSymbol, updatedAttachment: AttachedCreatureSymbol ) => handleChangedAttachments( attachments.withItemUpdated(originalAttachment, updatedAttachment) ); const addAttachment = (attachTo: AttachmentPointType, indices: number[]) => handleChangedAttachments( attachments.withItemAdded({ attachTo, indices, data: SvgVocabulary.items[0], invertColors: false, attachments: [], nests: [], }) ); return ( <> {" "} {ATTACHMENT_POINT_TYPES.map((type) => { if (type === "anchor") return null; const points = specs[type] || []; const symbolHasAttachments = points.length > 0; const creatureAttachments = creature.attachments.filter( (at) => at.attachTo === type ); const creatureDefinesAttachments = creatureAttachments.length > 0; if (!symbolHasAttachments && !creatureDefinesAttachments) { return null; } const style: CSSProperties = {}; let title = `Symbol defines ${type}(s) and cluster provides at least one`; if (!symbolHasAttachments) { style.textDecoration = "line-through"; title = `Cluster defines ${type}(s) but symbol doesn't define any`; // Honestly, this is just going to confuse people, so leave it out // for now. return; } if (!creatureDefinesAttachments) { style.color = "gray"; title = `Symbol defines ${type}(s) but cluster doesn't provide any`; } const availableIndices = getAvailableIndices( creatureAttachments, points.length ); const typeCap = capitalize(type); return (
{typeCap} attachments
{creatureAttachments.map((attach, i) => { const atIdPrefix = `${idPrefix}_${type}_${i}_`; const immutableIndices = getImmutableIndices( creatureAttachments, attach ); return (
); })} {availableIndices.length > 0 && ( )}
); })} ); } type CreatureEditorProps = { creature: T; onChange: (symbol: T) => void; idPrefix: string; }; function CreaturePartEditor({ creature, onChange, idPrefix, }: CreatureEditorProps): JSX.Element { return ( <>
onChange({ ...creature, data })} choices={SvgVocabulary} />
onChange({ ...creature, invertColors })} /> ); } export const CreatureEditorWidget: React.FC<{ creature: CreatureSymbol; onChange: (symbol: CreatureSymbol) => void; }> = ({ creature, onChange }) => { return (
Edit cluster…
); };