2021-02-27 13:35:34 +00:00
|
|
|
import React, { useContext, useRef, useState } from "react";
|
2021-03-26 22:07:01 +00:00
|
|
|
import { getSvgSymbol, SvgVocabulary } from "../svg-vocabulary";
|
2021-02-27 13:50:06 +00:00
|
|
|
import { createSvgSymbolContext, SvgSymbolData } from "../svg-symbol";
|
2021-03-18 23:32:05 +00:00
|
|
|
import {
|
|
|
|
AttachmentPointType,
|
|
|
|
ATTACHMENT_POINT_TYPES,
|
|
|
|
iterAttachmentPoints,
|
|
|
|
} from "../specs";
|
2021-02-16 22:42:19 +00:00
|
|
|
import { Random } from "../random";
|
2021-02-17 13:07:04 +00:00
|
|
|
import { SymbolContextWidget } from "../symbol-context-widget";
|
2021-02-23 02:50:14 +00:00
|
|
|
import { range } from "../util";
|
2021-02-27 13:50:06 +00:00
|
|
|
|
2021-02-27 13:35:34 +00:00
|
|
|
import { AutoSizingSvg } from "../auto-sizing-svg";
|
2021-02-27 13:43:31 +00:00
|
|
|
import { exportSvg } from "../export-svg";
|
2021-02-27 23:55:14 +00:00
|
|
|
import {
|
|
|
|
createCreatureSymbolFactory,
|
|
|
|
extractCreatureSymbolFromElement,
|
|
|
|
} from "../creature-symbol-factory";
|
2021-02-27 13:50:06 +00:00
|
|
|
import {
|
|
|
|
CreatureContext,
|
|
|
|
CreatureContextType,
|
|
|
|
CreatureSymbol,
|
2021-02-27 23:55:14 +00:00
|
|
|
NestedCreatureSymbol,
|
2021-02-27 13:50:06 +00:00
|
|
|
} from "../creature-symbol";
|
2021-02-27 18:28:44 +00:00
|
|
|
import { HoverDebugHelper } from "../hover-debug-helper";
|
2021-02-15 14:56:02 +00:00
|
|
|
|
2021-02-26 00:40:18 +00:00
|
|
|
const DEFAULT_BG_COLOR = "#858585";
|
|
|
|
|
2021-02-27 15:02:07 +00:00
|
|
|
/** Symbols that can be the "root" (i.e., main body) of a creature. */
|
|
|
|
const ROOT_SYMBOLS = SvgVocabulary.filter(
|
|
|
|
(data) => data.meta?.always_be_nested !== true
|
|
|
|
);
|
|
|
|
|
2021-03-18 23:32:05 +00:00
|
|
|
type AttachmentSymbolMap = {
|
|
|
|
[key in AttachmentPointType]: SvgSymbolData[];
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Symbols that can be attached to the main body of a creature,
|
|
|
|
* at a particular attachment point.
|
|
|
|
*/
|
|
|
|
const ATTACHMENT_SYMBOLS: AttachmentSymbolMap = (() => {
|
|
|
|
const result = {} as AttachmentSymbolMap;
|
|
|
|
|
|
|
|
for (let type of ATTACHMENT_POINT_TYPES) {
|
|
|
|
result[type] = SvgVocabulary.filter((data) => {
|
|
|
|
const { meta } = data;
|
|
|
|
|
|
|
|
// If we have no metadata whatsoever, it can attach anywhere.
|
|
|
|
if (!meta) return true;
|
|
|
|
|
|
|
|
if (meta.always_be_nested === true) {
|
|
|
|
// This symbol should *only* ever be nested, so return false.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we have no "attach_to", it can attach anywhere.
|
|
|
|
if (!meta.attach_to) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only attach to points listed in "attach_to".
|
|
|
|
return meta.attach_to.includes(type);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
})();
|
2021-02-27 15:02:07 +00:00
|
|
|
|
|
|
|
/** Symbols that can be nested within any part of a creature. */
|
|
|
|
const NESTED_SYMBOLS = SvgVocabulary.filter(
|
2021-03-01 00:10:53 +00:00
|
|
|
// Since we don't currently support recursive nesting, ignore anything that
|
|
|
|
// wants nested children.
|
|
|
|
(data) =>
|
|
|
|
data.meta?.always_nest !== true && data.meta?.never_be_nested !== true
|
2021-02-27 15:02:07 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a parent symbol, return an array of random children to be nested within
|
|
|
|
* it.
|
|
|
|
*
|
|
|
|
* Can return an empty array e.g. if the parent symbol doesn't have
|
|
|
|
* any nesting areas.
|
|
|
|
*/
|
2021-02-27 23:55:14 +00:00
|
|
|
function getNestingChildren(
|
|
|
|
parent: SvgSymbolData,
|
2021-03-01 00:49:41 +00:00
|
|
|
rng: Random,
|
|
|
|
preferNesting?: boolean
|
2021-02-27 23:55:14 +00:00
|
|
|
): NestedCreatureSymbol[] {
|
2021-02-27 15:02:07 +00:00
|
|
|
const { meta, specs } = parent;
|
2021-03-01 00:49:41 +00:00
|
|
|
if ((meta?.always_nest || preferNesting) && specs?.nesting) {
|
2021-02-27 15:02:07 +00:00
|
|
|
const indices = range(specs.nesting.length);
|
|
|
|
const child = rng.choice(NESTED_SYMBOLS);
|
|
|
|
return [
|
2021-02-27 23:55:14 +00:00
|
|
|
{
|
|
|
|
data: child,
|
|
|
|
attachments: [],
|
|
|
|
nests: [],
|
|
|
|
indices,
|
2021-03-06 00:38:25 +00:00
|
|
|
invertColors: meta?.invert_nested ?? false,
|
2021-02-27 23:55:14 +00:00
|
|
|
},
|
2021-02-27 15:02:07 +00:00
|
|
|
];
|
|
|
|
}
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2021-02-27 14:22:07 +00:00
|
|
|
/**
|
|
|
|
* Randomly creates a symbol with the given number of
|
|
|
|
* types of attachments. The symbol itself, and where the
|
|
|
|
* attachments are attached, are chosen randomly.
|
|
|
|
*/
|
|
|
|
function getSymbolWithAttachments(
|
|
|
|
numAttachmentKinds: number,
|
2021-03-07 01:35:58 +00:00
|
|
|
{ rng, randomlyInvert: randomlyInvertSymbols }: CreatureGeneratorOptions
|
2021-02-27 23:55:14 +00:00
|
|
|
): CreatureSymbol {
|
2021-02-27 15:02:07 +00:00
|
|
|
const root = rng.choice(ROOT_SYMBOLS);
|
2021-03-07 01:35:58 +00:00
|
|
|
const randomlyInvertRng = rng.clone();
|
|
|
|
const shouldInvert = () =>
|
|
|
|
randomlyInvertSymbols ? randomlyInvertRng.bool() : false;
|
2021-02-27 23:55:14 +00:00
|
|
|
const result: CreatureSymbol = {
|
|
|
|
data: root,
|
|
|
|
attachments: [],
|
2021-03-01 00:49:41 +00:00
|
|
|
nests: getNestingChildren(root, rng, true),
|
2021-03-07 01:35:58 +00:00
|
|
|
invertColors: shouldInvert(),
|
2021-02-27 23:55:14 +00:00
|
|
|
};
|
2021-02-27 14:22:07 +00:00
|
|
|
if (root.specs) {
|
|
|
|
const attachmentKinds = rng.uniqueChoices(
|
|
|
|
Array.from(iterAttachmentPoints(root.specs))
|
|
|
|
.filter((point) => point.type !== "anchor")
|
|
|
|
.map((point) => point.type),
|
|
|
|
numAttachmentKinds
|
|
|
|
);
|
|
|
|
for (let kind of attachmentKinds) {
|
2021-03-18 23:32:05 +00:00
|
|
|
const attachment = rng.choice(ATTACHMENT_SYMBOLS[kind]);
|
2021-02-27 14:22:07 +00:00
|
|
|
const indices = range(root.specs[kind]?.length ?? 0);
|
2021-02-27 23:55:14 +00:00
|
|
|
result.attachments.push({
|
|
|
|
data: attachment,
|
|
|
|
attachTo: kind,
|
|
|
|
indices,
|
|
|
|
attachments: [],
|
|
|
|
nests: getNestingChildren(attachment, rng),
|
2021-03-07 01:35:58 +00:00
|
|
|
invertColors: shouldInvert(),
|
2021-02-27 23:55:14 +00:00
|
|
|
});
|
2021-02-27 14:22:07 +00:00
|
|
|
}
|
|
|
|
}
|
2021-02-27 23:55:14 +00:00
|
|
|
return result;
|
2021-02-27 14:22:07 +00:00
|
|
|
}
|
|
|
|
|
2021-03-26 22:07:01 +00:00
|
|
|
const symbol = createCreatureSymbolFactory(getSvgSymbol);
|
2021-02-15 22:19:07 +00:00
|
|
|
|
2021-02-27 23:55:14 +00:00
|
|
|
const Eye = symbol("eye");
|
2021-02-15 22:19:07 +00:00
|
|
|
|
2021-02-27 23:55:14 +00:00
|
|
|
const Hand = symbol("hand");
|
2021-02-15 22:19:07 +00:00
|
|
|
|
2021-02-27 23:55:14 +00:00
|
|
|
const Arm = symbol("arm");
|
2021-02-15 22:19:07 +00:00
|
|
|
|
2021-02-27 23:55:14 +00:00
|
|
|
const Antler = symbol("antler");
|
2021-02-16 01:12:14 +00:00
|
|
|
|
2021-02-27 23:55:14 +00:00
|
|
|
const Crown = symbol("crown");
|
2021-02-16 01:12:14 +00:00
|
|
|
|
2021-02-27 23:55:14 +00:00
|
|
|
const Wing = symbol("wing");
|
2021-02-16 01:20:41 +00:00
|
|
|
|
2021-02-27 23:55:14 +00:00
|
|
|
const MuscleArm = symbol("muscle_arm");
|
2021-02-16 01:20:41 +00:00
|
|
|
|
2021-02-27 23:55:14 +00:00
|
|
|
const Leg = symbol("leg");
|
2021-02-16 03:51:35 +00:00
|
|
|
|
2021-02-27 23:55:14 +00:00
|
|
|
const Tail = symbol("tail");
|
2021-02-16 01:20:41 +00:00
|
|
|
|
2021-02-27 23:55:14 +00:00
|
|
|
const Lightning = symbol("lightning");
|
2021-02-26 02:57:10 +00:00
|
|
|
|
2021-02-16 22:11:41 +00:00
|
|
|
const EYE_CREATURE = (
|
|
|
|
<Eye>
|
2021-02-26 02:57:10 +00:00
|
|
|
<Lightning nestInside />
|
2021-02-16 22:11:41 +00:00
|
|
|
<Arm attachTo="arm" left>
|
|
|
|
<Wing attachTo="arm" left right />
|
|
|
|
</Arm>
|
|
|
|
<Arm attachTo="arm" right>
|
|
|
|
<MuscleArm attachTo="arm" left right />
|
|
|
|
</Arm>
|
|
|
|
<Antler attachTo="horn" left right />
|
|
|
|
<Crown attachTo="crown">
|
|
|
|
<Hand attachTo="horn" left right>
|
|
|
|
<Arm attachTo="arm" left />
|
|
|
|
</Hand>
|
|
|
|
</Crown>
|
|
|
|
<Leg attachTo="leg" left right />
|
2021-03-06 00:38:25 +00:00
|
|
|
<Tail attachTo="tail" invert />
|
2021-02-16 22:11:41 +00:00
|
|
|
</Eye>
|
|
|
|
);
|
|
|
|
|
2021-02-27 23:55:14 +00:00
|
|
|
const EYE_CREATURE_SYMBOL = extractCreatureSymbolFromElement(EYE_CREATURE);
|
|
|
|
|
2021-02-27 14:21:11 +00:00
|
|
|
/**
|
|
|
|
* Randomly replace all the parts of the given creature. Note that this
|
|
|
|
* might end up logging some console messages about not being able to find
|
|
|
|
* attachment/nesting indices, because it doesn't really check to make
|
|
|
|
* sure the final creature structure is fully valid.
|
|
|
|
*/
|
2021-02-27 23:55:14 +00:00
|
|
|
function randomlyReplaceParts<T extends CreatureSymbol>(
|
|
|
|
rng: Random,
|
|
|
|
creature: T
|
|
|
|
): T {
|
|
|
|
const result: T = {
|
|
|
|
...creature,
|
2021-02-16 22:42:19 +00:00
|
|
|
data: rng.choice(SvgVocabulary),
|
2021-02-27 23:55:14 +00:00
|
|
|
attachments: creature.attachments.map((a) => randomlyReplaceParts(rng, a)),
|
|
|
|
nests: creature.nests.map((n) => randomlyReplaceParts(rng, n)),
|
|
|
|
};
|
|
|
|
return result;
|
2021-02-16 22:42:19 +00:00
|
|
|
}
|
|
|
|
|
2021-03-07 01:35:58 +00:00
|
|
|
type CreatureGeneratorOptions = {
|
|
|
|
rng: Random;
|
|
|
|
randomlyInvert: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
type CreatureGenerator = (options: CreatureGeneratorOptions) => CreatureSymbol;
|
2021-02-23 02:50:14 +00:00
|
|
|
|
2021-02-27 14:21:11 +00:00
|
|
|
/**
|
|
|
|
* Each index of this array represents the algorithm we use to
|
|
|
|
* randomly construct a creature with a particular "complexity level".
|
|
|
|
*
|
|
|
|
* For instance, the algorithm used to create a creature with
|
|
|
|
* complexity level 0 will be the first item of this array.
|
|
|
|
*/
|
2021-02-23 02:50:14 +00:00
|
|
|
const COMPLEXITY_LEVEL_GENERATORS: CreatureGenerator[] = [
|
|
|
|
...range(5).map((i) => getSymbolWithAttachments.bind(null, i)),
|
2021-03-07 01:35:58 +00:00
|
|
|
({ rng }) => randomlyReplaceParts(rng, EYE_CREATURE_SYMBOL),
|
2021-02-23 02:50:14 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
const MAX_COMPLEXITY_LEVEL = COMPLEXITY_LEVEL_GENERATORS.length - 1;
|
|
|
|
|
2021-03-23 00:54:01 +00:00
|
|
|
const INITIAL_COMPLEXITY_LEVEL = 2;
|
|
|
|
|
2021-02-17 14:14:06 +00:00
|
|
|
function getDownloadFilename(randomSeed: number | null) {
|
|
|
|
let downloadBasename = "mystic-symbolic-creature";
|
|
|
|
|
|
|
|
if (randomSeed !== null) {
|
|
|
|
downloadBasename += `-${randomSeed}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return `${downloadBasename}.svg`;
|
|
|
|
}
|
2021-02-17 01:54:05 +00:00
|
|
|
|
|
|
|
export const CreaturePage: React.FC<{}> = () => {
|
2021-02-17 14:14:06 +00:00
|
|
|
const svgRef = useRef<SVGSVGElement>(null);
|
2021-03-23 00:54:01 +00:00
|
|
|
const qs = new URLSearchParams(window.location.search);
|
|
|
|
const showEyeCreature = qs.get("eye") === "on";
|
2021-02-26 00:40:18 +00:00
|
|
|
const [bgColor, setBgColor] = useState(DEFAULT_BG_COLOR);
|
2021-03-23 00:54:01 +00:00
|
|
|
const [randomSeed, setRandomSeed] = useState<number | null>(
|
|
|
|
showEyeCreature ? null : Date.now()
|
|
|
|
);
|
|
|
|
const [randomlyInvert, setRandomlyInvert] = useState(true);
|
2021-02-17 13:07:04 +00:00
|
|
|
const [symbolCtx, setSymbolCtx] = useState(createSvgSymbolContext());
|
2021-03-23 00:54:01 +00:00
|
|
|
const [complexity, setComplexity] = useState(
|
|
|
|
showEyeCreature ? MAX_COMPLEXITY_LEVEL : INITIAL_COMPLEXITY_LEVEL
|
|
|
|
);
|
2021-02-16 03:36:18 +00:00
|
|
|
const defaultCtx = useContext(CreatureContext);
|
2021-02-23 02:50:14 +00:00
|
|
|
const newRandomSeed = () => setRandomSeed(Date.now());
|
2021-02-16 03:36:18 +00:00
|
|
|
const ctx: CreatureContextType = {
|
|
|
|
...defaultCtx,
|
2021-02-17 13:07:04 +00:00
|
|
|
...symbolCtx,
|
|
|
|
fill: symbolCtx.showSpecs ? "none" : symbolCtx.fill,
|
2021-02-16 03:36:18 +00:00
|
|
|
};
|
2021-02-16 22:42:19 +00:00
|
|
|
const creature =
|
|
|
|
randomSeed === null
|
2021-02-27 23:55:14 +00:00
|
|
|
? EYE_CREATURE_SYMBOL
|
2021-03-07 01:35:58 +00:00
|
|
|
: COMPLEXITY_LEVEL_GENERATORS[complexity]({
|
|
|
|
rng: new Random(randomSeed),
|
|
|
|
randomlyInvert,
|
|
|
|
});
|
2021-02-17 14:14:06 +00:00
|
|
|
const handleSvgExport = () =>
|
|
|
|
exportSvg(getDownloadFilename(randomSeed), svgRef);
|
2021-03-07 01:35:58 +00:00
|
|
|
const isBonkers = complexity === MAX_COMPLEXITY_LEVEL;
|
2021-02-16 03:36:18 +00:00
|
|
|
|
2021-02-15 13:34:22 +00:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<h1>Creature!</h1>
|
2021-02-20 16:38:04 +00:00
|
|
|
<SymbolContextWidget ctx={symbolCtx} onChange={setSymbolCtx}>
|
|
|
|
<label htmlFor="bgColor">Background: </label>
|
|
|
|
<input
|
|
|
|
type="color"
|
|
|
|
value={bgColor}
|
|
|
|
onChange={(e) => setBgColor(e.target.value)}
|
|
|
|
/>{" "}
|
|
|
|
</SymbolContextWidget>
|
2021-02-17 01:47:12 +00:00
|
|
|
<p>
|
2021-02-23 02:50:14 +00:00
|
|
|
<label htmlFor="complexity">Random creature complexity: </label>
|
|
|
|
<input
|
|
|
|
type="range"
|
|
|
|
min={0}
|
|
|
|
max={MAX_COMPLEXITY_LEVEL}
|
|
|
|
step={1}
|
|
|
|
value={complexity}
|
|
|
|
onChange={(e) => {
|
|
|
|
setComplexity(parseInt(e.target.value));
|
|
|
|
newRandomSeed();
|
|
|
|
}}
|
|
|
|
/>{" "}
|
2021-03-07 01:35:58 +00:00
|
|
|
{isBonkers ? "bonkers" : complexity}
|
2021-02-23 02:50:14 +00:00
|
|
|
</p>
|
2021-03-07 01:35:58 +00:00
|
|
|
{!isBonkers && (
|
|
|
|
<p>
|
|
|
|
<label>
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
checked={randomlyInvert}
|
|
|
|
onChange={(e) => setRandomlyInvert(e.target.checked)}
|
|
|
|
/>
|
|
|
|
Randomly invert symbols
|
|
|
|
</label>
|
|
|
|
</p>
|
|
|
|
)}
|
2021-02-23 02:50:14 +00:00
|
|
|
<p>
|
|
|
|
<button accessKey="r" onClick={newRandomSeed}>
|
2021-02-20 16:07:57 +00:00
|
|
|
<u>R</u>andomize!
|
|
|
|
</button>{" "}
|
2021-02-17 14:14:06 +00:00
|
|
|
<button onClick={() => window.location.reload()}>Reset</button>{" "}
|
|
|
|
<button onClick={handleSvgExport}>Export SVG</button>
|
2021-02-17 01:47:12 +00:00
|
|
|
</p>
|
2021-02-16 03:36:18 +00:00
|
|
|
<CreatureContext.Provider value={ctx}>
|
2021-02-27 18:28:44 +00:00
|
|
|
<HoverDebugHelper>
|
|
|
|
<AutoSizingSvg padding={20} ref={svgRef} bgColor={bgColor}>
|
2021-02-27 23:55:14 +00:00
|
|
|
<g transform="scale(0.5 0.5)">
|
|
|
|
<CreatureSymbol {...creature} />
|
|
|
|
</g>
|
2021-02-27 18:28:44 +00:00
|
|
|
</AutoSizingSvg>
|
|
|
|
</HoverDebugHelper>
|
2021-02-16 03:36:18 +00:00
|
|
|
</CreatureContext.Provider>
|
2021-02-15 13:34:22 +00:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|