diff --git a/lib/pages/creature-page/core.tsx b/lib/pages/creature-page/core.tsx index 9fe90b6..facb0d9 100644 --- a/lib/pages/creature-page/core.tsx +++ b/lib/pages/creature-page/core.tsx @@ -34,6 +34,7 @@ import { Checkbox } from "../../checkbox"; import { CompositionContextWidget, createSvgCompositionContext, + SvgCompositionContext, } from "../../svg-composition-context"; import { Page } from "../../page"; import { RandomizerWidget } from "../../randomizer-widget"; @@ -206,8 +207,8 @@ const MAX_COMPLEXITY_LEVEL = COMPLEXITY_LEVEL_GENERATORS.length - 1; const INITIAL_COMPLEXITY_LEVEL = 2; -function getDownloadBasename(randomSeed: number) { - return `mystic-symbolic-creature-${randomSeed}`; +function getDownloadBasename(rootSymbolName: string) { + return `mystic-symbolic-creature-${rootSymbolName}`; } function creatureHasSymbol( @@ -244,55 +245,56 @@ function repeatUntilSymbolIsIncluded( return createCreature(rng); } -export const CREATURE_DESIGN_DEFAULTS = { - randomSeed: 0, - randomlyInvert: true, - complexity: INITIAL_COMPLEXITY_LEVEL, - alwaysIncludeSymbol: EMPTY_SVG_SYMBOL_DATA, - compCtx: createSvgCompositionContext(), +export type CreatureDesign = { + compCtx: SvgCompositionContext; + creature: CreatureSymbol; }; -export type CreatureDesign = typeof CREATURE_DESIGN_DEFAULTS; +export const CREATURE_DESIGN_DEFAULTS: CreatureDesign = { + compCtx: createSvgCompositionContext(), + creature: { + data: ROOT_SYMBOLS[0], + invertColors: false, + attachments: [], + nests: [], + }, +}; export const CreaturePageWithDefaults: React.FC< ComponentWithShareableStateProps > = ({ defaults, onChange }) => { const svgRef = useRef(null); - const [randomSeed, setRandomSeed] = useState(defaults.randomSeed); - const [randomlyInvert, setRandomlyInvert] = useState(defaults.randomlyInvert); + const [randomlyInvert, setRandomlyInvert] = useState(true); const [compCtx, setCompCtx] = useState(defaults.compCtx); - const [complexity, setComplexity] = useState(defaults.complexity); + const [complexity, setComplexity] = useState(INITIAL_COMPLEXITY_LEVEL); + const [creature, setCreature] = useState(defaults.creature); const defaultCtx = useContext(CreatureContext); - const newRandomSeed = () => setRandomSeed(Date.now()); - const ctx: CreatureContextType = noFillIfShowingSpecs({ - ...defaultCtx, - ...compCtx, - }); - const [alwaysInclude, setAlwaysInclude] = useState( - defaults.alwaysIncludeSymbol - ); - const creature = useMemo( - () => + const newRandomCreature = () => { + setCreature( repeatUntilSymbolIsIncluded( alwaysInclude, - new Random(randomSeed), + new Random(Date.now()), (rng) => COMPLEXITY_LEVEL_GENERATORS[complexity]({ rng, randomlyInvert, }) - ), - [alwaysInclude, complexity, randomSeed, randomlyInvert] + ) + ); + }; + const ctx: CreatureContextType = noFillIfShowingSpecs({ + ...defaultCtx, + ...compCtx, + }); + const [alwaysInclude, setAlwaysInclude] = useState( + EMPTY_SVG_SYMBOL_DATA ); const design: CreatureDesign = useMemo( () => ({ - randomSeed, - randomlyInvert, - complexity, - alwaysIncludeSymbol: alwaysInclude, + creature, compCtx, }), - [randomSeed, randomlyInvert, complexity, alwaysInclude, compCtx] + [creature, compCtx] ); useDebouncedEffect( @@ -313,7 +315,7 @@ export const CreaturePageWithDefaults: React.FC< value={complexity} onChange={(value) => { setComplexity(value); - newRandomSeed(); + newRandomCreature(); }} /> @@ -326,7 +328,7 @@ export const CreaturePageWithDefaults: React.FC< setCompCtx({ ...compCtx, ...colors })} - onSymbolsChange={newRandomSeed} + onSymbolsChange={newRandomCreature} >
diff --git a/lib/pages/creature-page/creature-design.avsc.json b/lib/pages/creature-page/creature-design.avsc.json index 8d6f1e8..c82c01a 100644 --- a/lib/pages/creature-page/creature-design.avsc.json +++ b/lib/pages/creature-page/creature-design.avsc.json @@ -2,10 +2,6 @@ "type": "record", "name": "AvroCreatureDesign", "fields": [ - { "name": "randomSeed", "type": "long" }, - { "name": "randomlyInvert", "type": "boolean" }, - { "name": "complexity", "type": "int" }, - { "name": "alwaysIncludeSymbol", "type": "string" }, { "name": "compCtx", "type": { @@ -19,6 +15,46 @@ { "name": "disableGradients", "type": "boolean", "default": true } ] } + }, + { + "name": "creature", + "type": { + "name": "AvroCreatureSymbol", + "type": "record", + "fields": [ + { "name": "symbol", "type": "string" }, + { "name": "invertColors", "type": "boolean" }, + { + "name": "attachments", + "type": { + "type": "array", + "items": { + "name": "AvroAttachedCreatureSymbol", + "type": "record", + "fields": [ + { "name": "base", "type": "AvroCreatureSymbol" }, + { "name": "attachTo", "type": "int" }, + { "name": "indices", "type": "bytes" } + ] + } + } + }, + { + "name": "nests", + "type": { + "type": "array", + "items": { + "name": "AvroNestedCreatureSymbol", + "type": "record", + "fields": [ + { "name": "base", "type": "AvroCreatureSymbol" }, + { "name": "indices", "type": "bytes" } + ] + } + } + } + ] + } } ] } diff --git a/lib/pages/creature-page/serialization.ts b/lib/pages/creature-page/serialization.ts index c17e892..49414e8 100644 --- a/lib/pages/creature-page/serialization.ts +++ b/lib/pages/creature-page/serialization.ts @@ -1,29 +1,107 @@ import * as avro from "avro-js"; import { CreatureDesign } from "./core"; -import { AvroCreatureDesign } from "./creature-design.avsc"; +import { + AvroAttachedCreatureSymbol, + AvroCreatureDesign, + AvroCreatureSymbol, + AvroNestedCreatureSymbol, +} from "./creature-design.avsc"; import { fromBase64, toBase64 } from "../../base64"; import CreatureAvsc from "./creature-design.avsc.json"; -import { SvgVocabularyWithBlank } from "../../svg-vocabulary"; import { Packer, SvgCompositionContextPacker } from "../../serialization"; +import { + AttachedCreatureSymbol, + CreatureSymbol, + NestedCreatureSymbol, +} from "../../creature-symbol"; +import { SvgVocabulary } from "../../svg-vocabulary"; +import { ATTACHMENT_POINT_TYPES } from "../../specs"; -const LATEST_VERSION = "v1"; +const LATEST_VERSION = "v2"; const avroCreatureDesign = avro.parse(CreatureAvsc); -const DesignConfigPacker: Packer = { +const ATTACHMENT_POINT_MAPPING = new Map( + ATTACHMENT_POINT_TYPES.map((name, i) => { + return [name, i]; + }) +); + +const NestedCreatureSymbolPacker: Packer< + NestedCreatureSymbol, + AvroNestedCreatureSymbol +> = { + pack: (value) => { + return { + base: CreatureSymbolPacker.pack(value), + indices: Buffer.from(value.indices), + }; + }, + unpack: (value) => { + return { + ...CreatureSymbolPacker.unpack(value.base), + indices: Array.from(value.indices), + }; + }, +}; + +const AttachedCreatureSymbolPacker: Packer< + AttachedCreatureSymbol, + AvroAttachedCreatureSymbol +> = { + pack: (value) => { + const attachTo = ATTACHMENT_POINT_MAPPING.get(value.attachTo); + if (attachTo === undefined) { + throw new Error(`Invalid attachment type "${value.attachTo}"`); + } + return { + base: CreatureSymbolPacker.pack(value), + attachTo, + indices: Buffer.from(value.indices), + }; + }, + unpack: (value) => { + const attachTo = ATTACHMENT_POINT_TYPES[value.attachTo]; + if (attachTo === undefined) { + throw new Error(`Invalid attachment type "${value.attachTo}"`); + } + return { + ...CreatureSymbolPacker.unpack(value.base), + attachTo, + indices: Array.from(value.indices), + }; + }, +}; + +const CreatureSymbolPacker: Packer = { pack: (value) => { return { ...value, - alwaysIncludeSymbol: value.alwaysIncludeSymbol.name, - compCtx: SvgCompositionContextPacker.pack(value.compCtx), + symbol: value.data.name, + attachments: value.attachments.map(AttachedCreatureSymbolPacker.pack), + nests: value.nests.map(NestedCreatureSymbolPacker.pack), }; }, unpack: (value) => { return { ...value, - alwaysIncludeSymbol: SvgVocabularyWithBlank.get( - value.alwaysIncludeSymbol - ), + data: SvgVocabulary.get(value.symbol), + attachments: value.attachments.map(AttachedCreatureSymbolPacker.unpack), + nests: value.nests.map(NestedCreatureSymbolPacker.unpack), + }; + }, +}; + +const DesignConfigPacker: Packer = { + pack: (value) => { + return { + creature: CreatureSymbolPacker.pack(value.creature), + compCtx: SvgCompositionContextPacker.pack(value.compCtx), + }; + }, + unpack: (value) => { + return { + creature: CreatureSymbolPacker.unpack(value.creature), compCtx: SvgCompositionContextPacker.unpack(value.compCtx), }; }, @@ -35,7 +113,10 @@ export function serializeCreatureDesign(value: CreatureDesign): string { } export function deserializeCreatureDesign(value: string): CreatureDesign { - const [_version, serialized] = value.split(".", 2); + const [version, serialized] = value.split(".", 2); + if (version === "v1") { + throw new Error(`Sorry, we no longer support loading v1 creatures!`); + } const buf = fromBase64(serialized); return DesignConfigPacker.unpack(avroCreatureDesign.fromBuffer(buf)); }