Adapt generator

This commit is contained in:
Blipz 2026-03-25 14:30:47 +01:00
parent 85f52e81a6
commit 6ced96796c
3 changed files with 53 additions and 481 deletions

20
package-lock.json generated
View file

@ -14,7 +14,8 @@
"armoria": "file:../armoria/dist",
"d3": "^7.9.0",
"delaunator": "^5.0.1",
"polylabel": "^2.0.1"
"polylabel": "^2.0.1",
"symlink-dir": "^9.0.0"
},
"devDependencies": {
"@biomejs/biome": "2.3.13",
@ -26,7 +27,6 @@
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
"playwright": "^1.57.0",
"symlink-dir": "^9.0.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18"
@ -51,14 +51,11 @@
"puppeteer-core": "24.11.2"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/adapter-vercel": "^6.3.3",
"@sveltejs/kit": "^2.22.5",
"@sveltejs/package": "^2.5.7",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^9.6.1",
"armoria-core": "file:../armoria-core",
"eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.10.1",
@ -75,6 +72,10 @@
"typescript-eslint": "^8.36.0",
"vite": "^5.0.3",
"workbox-precaching": "^7.3.0"
},
"peerDependencies": {
"@sveltejs/kit": "^2.22.5",
"svelte": "^4.2.7"
}
},
"node_modules/@biomejs/biome": {
@ -1571,7 +1572,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@zkochan/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-GBf4ua7ogWTr7fATnzk/JLowZDBnBJMm8RkMaC/KcvxZ9gxbMWix0/jImd815LmqKyIHZ7h7lADRddGMdGBuCA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
@ -1601,7 +1601,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz",
"integrity": "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-windows": "^1.0.0"
@ -2130,7 +2129,6 @@
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
@ -2160,7 +2158,6 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/iconv-lite": {
@ -2188,7 +2185,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -2198,7 +2194,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
@ -2396,7 +2391,6 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/rename-overwrite/-/rename-overwrite-6.0.3.tgz",
"integrity": "sha512-Daqe51STnrCUq/t4dbzCtfNBLElrqVpCtuWK0MuPrzUi6K/13E98y3E8/kzuMZt6IEmghMnF41J0AidrFqjZUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@zkochan/rimraf": "^3.0.2",
@ -2519,7 +2513,6 @@
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/symlink-dir/-/symlink-dir-9.0.0.tgz",
"integrity": "sha512-3i39G70eXo9POjx9RnYAgymfUO3AFrezABaJq0Sh+muMCR3Lx7B9KQR2uWMY+yc1QCwhEelVCGS6N+sR3aKCZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"better-path-resolve": "^1.0.0",
@ -2617,7 +2610,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"

View file

@ -34,7 +34,6 @@
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
"playwright": "^1.57.0",
"symlink-dir": "^9.0.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vitest": "^4.0.18"
@ -44,7 +43,8 @@
"armoria": "file:../armoria/dist",
"d3": "^7.9.0",
"delaunator": "^5.0.1",
"polylabel": "^2.0.1"
"polylabel": "^2.0.1",
"symlink-dir": "^9.0.0"
},
"engines": {
"node": ">=24.0.0"

View file

@ -1,12 +1,4 @@
import {
charges,
divisions,
lines,
ordinaries,
positions,
shields,
tinctures,
} from "armoria";
import { generateCOA, getTincture, shields, tinctures } from "armoria";
import { P, rw } from "../../utils";
import { typeMapping } from "./typeMapping";
@ -61,365 +53,60 @@ class EmblemGeneratorModule {
kinship = 0;
dominion = 0;
}
let usedPattern: string | null = null;
const usedTinctures: string[] = [];
const t1 = P(kinship as number)
? parent!.t1
: this.getTincture("field", usedTinctures, null);
if (t1.includes("-")) usedPattern = t1;
const coa: Emblem = { t1 };
const addCharge = P(usedPattern ? 0.5 : 0.93); // 80% for charge
const linedOrdinary =
(addCharge && P(0.3)) || P(0.5)
? parent?.ordinaries && P(kinship as number)
? parent.ordinaries[0].ordinary
: rw(ordinaries.lined)
: null;
const ordinary =
(!addCharge && P(0.65)) || P(0.3)
? linedOrdinary
? linedOrdinary
: rw(ordinaries.straight)
: null; // 36% for ordinary
const rareDivided = [
"chief",
"terrace",
"chevron",
"quarter",
"flaunches",
].includes(ordinary!);
const divisioned = (() => {
if (rareDivided) return P(0.03);
if (addCharge && ordinary) return P(0.03);
if (addCharge) return P(0.3);
if (ordinary) return P(0.7);
return P(0.995);
})();
const division = (() => {
if (divisioned) {
if (parent?.division && P((kinship as number) - 0.1))
return parent.division.division;
return rw(divisions.variants);
}
return null;
})();
if (division) {
const t = this.getTincture(
"division",
usedTinctures,
P(0.98) ? coa.t1 : null,
);
coa.division = { division, t };
if (divisions[division])
coa.division.line =
usedPattern || (ordinary && P(0.7))
? "straight"
: rw(divisions.data[division].line);
}
if (ordinary) {
coa.ordinaries = [
{ ordinary, t: this.getTincture("charge", usedTinctures, coa.t1) },
];
if (linedOrdinary)
coa.ordinaries[0].line =
usedPattern || (division && P(0.7)) ? "straight" : rw(lines.variants);
if (
division &&
!addCharge &&
!usedPattern &&
P(0.5) &&
ordinary !== "bordure" &&
ordinary !== "orle"
) {
if (P(0.8)) coa.ordinaries[0].divided = "counter";
// 40%
else if (P(0.6)) coa.ordinaries[0].divided = "field";
// 6%
else coa.ordinaries[0].divided = "division"; // 4%
}
}
if (addCharge) {
const charge = (() => {
return generateCOA(null, {
charge: () => {
if (parent?.charges && P((kinship as number) - 0.1))
return parent.charges[0].charge;
if (type && type !== "Generic" && P(0.3)) return rw(typeMapping[type]);
return this.selectCharge(
ordinary || divisioned ? charges.types : charges.single,
);
})();
const chargeDataEntry = charges.data[charge] || {};
},
division: () => {
if (parent?.division && P((kinship as number) - 0.1))
return parent.division.division;
},
ordinary: () => {
if (parent?.ordinaries && P(kinship as number))
return parent.ordinaries[0].ordinary;
},
tincture: () => {
if (P(kinship as number)) return parent!.t1;
},
finalize: (coa: Emblem, config: Record<string, any>) => {
// dominions have canton with parent coa
if (P(dominion as number) && parent?.charges) {
const invert = this.isSameType(parent.t1, coa.t1);
const t = invert
? getTincture(config, "division", config.usedTinctures, coa.t1)
: parent.t1;
const canton: EmblemOrdinary = { ordinary: "canton", t };
let p: string;
let t: string;
const ordinaryData = ordinaries.data[ordinary!];
const tOrdinary = coa.ordinaries ? coa.ordinaries[0].t : null;
if (ordinaryData?.positionsOn && P(0.8)) {
// place charge over ordinary (use tincture of field type)
p = rw(ordinaryData.positionsOn);
t =
!usedPattern && P(0.3)
? coa.t1
: this.getTincture("charge", [], tOrdinary);
} else if (ordinaryData?.positionsOff && P(0.95)) {
// place charge out of ordinary (use tincture of ordinary type)
p = rw(ordinaryData.positionsOff);
t =
!usedPattern && P(0.3)
? tOrdinary!
: this.getTincture("charge", usedTinctures, coa.t1);
} else if (divisions.data[division]?.positions) {
// place charge in fields made by division
p = rw(divisions.data[division].positions);
t = this.getTincture(
"charge",
tOrdinary ? usedTinctures.concat(tOrdinary) : usedTinctures,
coa.t1,
);
} else if (chargeDataEntry.positions) {
// place charge-suitable position
p = rw(chargeDataEntry.positions);
t = this.getTincture("charge", usedTinctures, coa.t1);
} else {
// place in standard position (use new tincture)
p = usedPattern
? "e"
: charges.conventional[charge]
? rw(positions.conventional)
: rw(positions.complex);
t = this.getTincture(
"charge",
usedTinctures.concat(tOrdinary!),
coa.t1,
);
}
if (
chargeDataEntry.natural &&
chargeDataEntry.natural !== t &&
chargeDataEntry.natural !== tOrdinary
)
t = chargeDataEntry.natural;
const item: EmblemCharge = { charge: charge, t, p };
const colors = chargeDataEntry.colors || 1;
if (colors > 1)
item.t2 = P(0.25)
? this.getTincture("charge", usedTinctures, coa.t1)
: t;
if (colors > 2 && item.t2)
item.t3 = P(0.5)
? this.getTincture("charge", usedTinctures, coa.t1)
: t;
coa.charges = [item];
if (p === "ABCDEFGHIJKL" && P(0.95)) {
// add central charge if charge is in bordure
coa.charges[0].charge = rw(charges.conventional);
const chargeNew = this.selectCharge(charges.single);
const tNew = this.getTincture("charge", usedTinctures, coa.t1);
coa.charges.push({ charge: chargeNew, t: tNew, p: "e" });
} else if (P(0.8) && charge === "inescutcheon") {
// add charge to inescutcheon
const chargeNew = this.selectCharge(charges.types);
const t2 = this.getTincture("charge", [], t);
coa.charges.push({ charge: chargeNew, t: t2, p, size: 0.5 });
} else if (division && !ordinary) {
const allowCounter =
!usedPattern &&
(!coa.division?.line || coa.division.line === "straight");
// dimidiation: second charge at division basic positions
if (
P(0.3) &&
["perPale", "perFess"].includes(division) &&
coa.division?.line === "straight"
) {
coa.charges[0].divided = "field";
if (P(0.95)) {
const p2 =
p === "e" || P(0.5)
? "e"
: rw(divisions.data[division].positions);
const chargeNew = this.selectCharge(charges.single);
const tNew = this.getTincture(
"charge",
usedTinctures,
coa.division!.t,
);
coa.charges.push({
charge: chargeNew,
t: tNew,
p: p2,
divided: "division",
});
if (coa.charges) {
for (let i = coa.charges.length - 1; i >= 0; i--) {
const charge = coa.charges[i];
if (charge.size === 1.5) charge.size = 1.4;
charge.p = charge.p.replaceAll(/[ajy]/g, "");
if (!charge.p) coa.charges.splice(i, 1);
}
}
} else if (allowCounter && P(0.4)) coa.charges[0].divided = "counter";
// counterchanged, 40%
else if (
["perPale", "perFess", "perBend", "perBendSinister"].includes(
division,
) &&
P(0.8)
) {
// place 2 charges in division standard positions
const [p1, p2] =
division === "perPale"
? ["p", "q"]
: division === "perFess"
? ["k", "n"]
: division === "perBend"
? ["l", "m"]
: ["j", "o"]; // perBendSinister
coa.charges[0].p = p1;
const chargeNew = this.selectCharge(charges.single);
const tNew = this.getTincture(
"charge",
usedTinctures,
coa.division!.t,
);
coa.charges.push({ charge: chargeNew, t: tNew, p: p2 });
} else if (["perCross", "perSaltire"].includes(division) && P(0.5)) {
// place 4 charges in division standard positions
const [p1, p2, p3, p4] =
division === "perCross"
? ["j", "l", "m", "o"]
: ["b", "d", "f", "h"];
coa.charges[0].p = p1;
let charge = parent.charges[0].charge;
if (charge === "inescutcheon" && parent.charges[1])
charge = parent.charges[1].charge;
const c2 = this.selectCharge(charges.single);
const t2 = this.getTincture("charge", [], coa.division!.t);
let t2 = invert ? parent.t1 : parent.charges[0].t;
if (this.isSameType(t, t2))
t2 = getTincture(config, "charge", config.usedTinctures, t);
const c3 = this.selectCharge(charges.single);
const t3 = this.getTincture("charge", [], coa.division!.t);
if (!coa.charges) coa.charges = [];
coa.charges.push({ charge, t: t2, p: "y", size: 0.5 });
const c4 = this.selectCharge(charges.single);
const t4 = this.getTincture("charge", [], coa.t1);
coa.charges.push(
{ charge: c2, t: t2, p: p2 },
{ charge: c3, t: t3, p: p3 },
{ charge: c4, t: t4, p: p4 },
);
} else if (allowCounter && p.length > 1)
coa.charges[0].divided = "counter"; // counterchanged, 40%
}
for (const c of coa.charges) {
this.defineChargeAttributes(ordinary, division, c);
}
}
// dominions have canton with parent coa
if (P(dominion as number) && parent?.charges) {
const invert = this.isSameType(parent.t1, coa.t1);
const t = invert
? this.getTincture("division", usedTinctures, coa.t1)
: parent.t1;
const canton: EmblemOrdinary = { ordinary: "canton", t };
if (coa.charges) {
for (let i = coa.charges.length - 1; i >= 0; i--) {
const charge = coa.charges[i];
if (charge.size === 1.5) charge.size = 1.4;
charge.p = charge.p.replaceAll(/[ajy]/g, "");
if (!charge.p) coa.charges.splice(i, 1);
if (coa.ordinaries) {
coa.ordinaries.push(canton);
} else {
coa.ordinaries = [canton];
}
}
}
let charge = parent.charges[0].charge;
if (charge === "inescutcheon" && parent.charges[1])
charge = parent.charges[1].charge;
let t2 = invert ? parent.t1 : parent.charges[0].t;
if (this.isSameType(t, t2))
t2 = this.getTincture("charge", usedTinctures, t);
if (!coa.charges) coa.charges = [];
coa.charges.push({ charge, t: t2, p: "y", size: 0.5 });
if (coa.ordinaries) {
coa.ordinaries.push(canton);
} else {
coa.ordinaries = [canton];
}
}
return coa;
}
private selectCharge(set?: Record<string, number>): string {
const type = set ? rw(set) : rw(charges.types);
return type === "inescutcheon"
? "inescutcheon"
: rw(charges[type as keyof typeof charges] as Record<string, number>);
}
// Select tincture: element type (field, division, charge), used field tinctures, field type to follow RoT
private getTincture(
element: "field" | "division" | "charge",
fields: string[] = [],
RoT: string | null,
): string {
const base = RoT ? (RoT.includes("-") ? RoT.split("-")[1] : RoT) : null;
let type = rw(tinctures[element]); // metals, colours, stains, patterns
if (RoT && type !== "patterns")
type = this.getType(base!) === "metals" ? "colours" : "metals"; // follow RoT
if (type === "metals" && fields.includes("or") && fields.includes("argent"))
type = "colours"; // exclude metals overuse
let tincture = rw(
tinctures[type as keyof typeof tinctures] as Record<string, number>,
);
while (tincture === base || fields.includes(tincture)) {
tincture = rw(
tinctures[type as keyof typeof tinctures] as Record<string, number>,
);
} // follow RoT
if (type !== "patterns" && element !== "charge") fields.push(tincture); // add field tincture
if (type === "patterns") {
tincture = this.definePattern(tincture, element, fields);
}
return tincture;
}
private defineChargeAttributes(
ordinary: string | null,
division: string | null,
c: EmblemCharge,
): void {
// define size
c.size = (c.size || 1) * this.getSize(c.p, ordinary, division);
// clean-up position
c.p = [...new Set(c.p)].join("");
// define orientation
if (P(0.02) && charges.data[c.charge]?.sinister) c.sinister = 1;
if (P(0.02) && charges.data[c.charge]?.reversed) c.reversed = 1;
}
private getType(t: string): string | undefined {
const tinc = t.includes("-") ? t.split("-")[1] : t;
if (Object.keys(tinctures.metals).includes(tinc)) return "metals";
if (Object.keys(tinctures.colours).includes(tinc)) return "colours";
if (Object.keys(tinctures.stains).includes(tinc)) return "stains";
return undefined;
},
});
}
private isSameType(t1: string, t2: string): boolean {
@ -433,113 +120,6 @@ class EmblemGeneratorModule {
return "pattern";
}
private definePattern(
pattern: string,
element: "field" | "division" | "charge",
usedTinctures: string[],
): string {
let t1: string | null = null;
let t2: string | null = null;
let size = "";
// Size selection - must use sequential P() calls to match original behavior
if (P(0.1)) size = "-small";
// biome-ignore lint/suspicious/noDuplicateElseIf: false positive
else if (P(0.1)) size = "-smaller";
else if (P(0.01)) size = "-big";
else if (P(0.005)) size = "-smallest";
// apply standard tinctures
if (P(0.5) && ["vair", "vairInPale", "vairEnPointe"].includes(pattern)) {
t1 = "azure";
t2 = "argent";
} else if (P(0.8) && pattern === "ermine") {
t1 = "argent";
t2 = "sable";
} else if (pattern === "pappellony") {
if (P(0.2)) {
t1 = "gules";
t2 = "or";
// biome-ignore lint/suspicious/noDuplicateElseIf: false positive
} else if (P(0.2)) {
t1 = "argent";
t2 = "sable";
// biome-ignore lint/suspicious/noDuplicateElseIf: false positive
} else if (P(0.2)) {
t1 = "azure";
t2 = "argent";
}
} else if (pattern === "masoned") {
if (P(0.3)) {
t1 = "gules";
t2 = "argent";
// biome-ignore lint/suspicious/noDuplicateElseIf: false positive
} else if (P(0.3)) {
t1 = "argent";
t2 = "sable";
} else if (P(0.1)) {
t1 = "or";
t2 = "sable";
}
} else if (pattern === "fretty") {
if (t2 === "sable" || P(0.35)) {
t1 = "argent";
t2 = "gules";
} else if (P(0.25)) {
t1 = "sable";
t2 = "or";
} else if (P(0.15)) {
t1 = "gules";
t2 = "argent";
}
} else if (pattern === "semy")
pattern = `${pattern}_of_${this.selectCharge(charges.semy)}`;
if (!t1 || !t2) {
const startWithMetal = P(0.7);
t1 = startWithMetal ? rw(tinctures.metals) : rw(tinctures.colours);
t2 = startWithMetal ? rw(tinctures.colours) : rw(tinctures.metals);
}
// division should not be the same tincture as base field
if (element === "division") {
if (usedTinctures.includes(t1)) t1 = this.replaceTincture(t1);
if (usedTinctures.includes(t2)) t2 = this.replaceTincture(t2);
}
usedTinctures.push(t1, t2);
return `${pattern}-${t1}-${t2}${size}`;
}
private replaceTincture(t: string): string {
const type = this.getType(t);
let n: string | null = null;
while (!n || n === t) {
n = rw(tinctures[type] as Record<string, number>);
}
return n;
}
private getSize(
p: string,
o: string | null = null,
d: string | null = null,
): number {
if (p === "e" && (o === "bordure" || o === "orle")) return 1.1;
if (p === "e") return 1.5;
if (p === "jln" || p === "jlh") return 0.7;
if (p === "abcpqh" || p === "ez" || p === "be") return 0.5;
if (["a", "b", "c", "d", "f", "g", "h", "i", "bh", "df"].includes(p))
return 0.5;
if (["j", "l", "m", "o", "jlmo"].includes(p) && d === "perCross")
return 0.6;
if (p.length > 10) return 0.18; // >10 (bordure)
if (p.length > 7) return 0.3; // 8, 9, 10
if (p.length > 4) return 0.4; // 5, 6, 7
if (p.length > 2) return 0.5; // 3, 4
return 0.7; // 1, 2
}
getShield(culture: number, state?: number): string {
const emblemShape = document.getElementById(
"emblemShape",