@axiapps/forge-render 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@axiapps/forge-render",
3
+ "version": "0.1.0",
4
+ "description": "Framework-free AxiForge build/comp card renderers + scoped CSS, shared with AxiVale and AxiBridge",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "files": ["src"],
8
+ "publishConfig": { "access": "public" },
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./forge-render.css": "./src/forge-render.css"
12
+ },
13
+ "scripts": {
14
+ "test": "cd ../.. && npx jest tests/forge-render --maxWorkers=2"
15
+ },
16
+ "license": "MIT",
17
+ "keywords": [
18
+ "gw2",
19
+ "guild-wars-2",
20
+ "build-card",
21
+ "axiforge",
22
+ "axivale"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/darkharasho/axiforge",
27
+ "directory": "packages/forge-render"
28
+ },
29
+ "dependencies": {
30
+ "@axiapps/gw2-data": "^0.1.2",
31
+ "gw2-class-icons": "^0.3.0"
32
+ }
33
+ }
@@ -0,0 +1,81 @@
1
+ // Shared build display helpers — extracted from comp-detail.js for reuse.
2
+
3
+ import { getProfessionSvg, getProfessionSvgColored } from "./profession-icons.js";
4
+
5
+ export function getEliteSpecName(build) {
6
+ if (!build.specializations) return null;
7
+ for (const s of build.specializations) {
8
+ if (s.elite && s.name) return s.name;
9
+ }
10
+ return null;
11
+ }
12
+
13
+ export function getSpecIcon(build) {
14
+ const eliteSpec = getEliteSpecName(build);
15
+ const name = eliteSpec || build.profession;
16
+ if (!name) return "";
17
+ return getProfessionSvg(name) || "";
18
+ }
19
+
20
+ export function getSpecIconColored(build, color) {
21
+ const eliteSpec = getEliteSpecName(build);
22
+ const name = eliteSpec || build.profession;
23
+ if (!name) return "";
24
+ return getProfessionSvgColored(name, color) || "";
25
+ }
26
+
27
+ export function profClass(profession) {
28
+ const slug = String(profession || "")
29
+ .toLowerCase()
30
+ .replace(/[^a-z-]/g, "");
31
+ if (!slug) return "";
32
+ return `lib-prof--${slug}`;
33
+ }
34
+
35
+ export function getDisplayName(build) {
36
+ const elite = getEliteSpecName(build);
37
+ return build.title || elite || build.profession || "Untitled";
38
+ }
39
+
40
+ export function resolveStatPackage(build) {
41
+ const pkg = build.equipment?.statPackage || "";
42
+ if (pkg && !/^\d+$/.test(pkg)) return pkg;
43
+
44
+ const slots = build.equipment?.slots;
45
+ if (slots && typeof slots === "object") {
46
+ const counts = {};
47
+ for (const v of Object.values(slots)) {
48
+ if (v && typeof v === "string") counts[v] = (counts[v] || 0) + 1;
49
+ }
50
+ let best = "";
51
+ let bestCount = 0;
52
+ for (const [label, count] of Object.entries(counts)) {
53
+ if (count > bestCount) { best = label; bestCount = count; }
54
+ }
55
+ if (best) return best;
56
+ }
57
+
58
+ return "";
59
+ }
60
+
61
+ export function getRuneName(build, upgradeCatalog) {
62
+ const runes = build.equipment?.runes;
63
+ if (!runes || typeof runes !== "object") return "";
64
+ const counts = {};
65
+ for (const v of Object.values(runes)) {
66
+ if (v) counts[String(v)] = (counts[String(v)] || 0) + 1;
67
+ }
68
+ let bestId = "";
69
+ let bestCount = 0;
70
+ for (const [id, count] of Object.entries(counts)) {
71
+ if (count > bestCount) { bestId = id; bestCount = count; }
72
+ }
73
+ if (!bestId) return "";
74
+
75
+ const runeDef = upgradeCatalog?.runeById?.get(Number(bestId));
76
+ if (runeDef?.name) {
77
+ return runeDef.name.replace(/^(?:Superior|Major|Minor) Rune of (?:the )?/i, "");
78
+ }
79
+
80
+ return /^\d+$/.test(bestId) ? "" : bestId;
81
+ }
@@ -0,0 +1,63 @@
1
+ // Comp card — party lines + mini build cards for a squad composition.
2
+ // comp: AxiForge comp record ({ title, partyLines, buildColors, tags }).
3
+ // buildsById: plain object of build records keyed by id.
4
+ // catalog: upgradeCatalog with runeById/relicByName Maps, or null.
5
+ import { escapeHtml } from "./escape.js";
6
+ import { profClass, getDisplayName, getSpecIcon, getSpecIconColored } from "./build-helpers.js";
7
+ import { renderMiniBuildCard, renderMissingMiniBuildCard } from "./mini-build-card.js";
8
+
9
+ function renderSlot(build, color) {
10
+ if (!build) return `<div class="comp-slot comp-slot--empty"></div>`;
11
+ const icon = color && color !== "normal" ? getSpecIconColored(build, color) : getSpecIcon(build);
12
+ const colorAttr = color && color !== "normal" ? ` data-slot-color="${color}"` : "";
13
+ return `
14
+ <div class="comp-slot comp-slot--filled ${profClass(build.profession)}"${colorAttr}
15
+ title="${escapeHtml(getDisplayName(build))}">
16
+ <span class="comp-slot__icon">${icon || escapeHtml((build.profession || "?")[0])}</span>
17
+ </div>`;
18
+ }
19
+
20
+ function renderPartyLines(comp, buildsById) {
21
+ const colors = comp.buildColors || {};
22
+ return (comp.partyLines || []).map((line, idx) => {
23
+ const slots = line.slots || [];
24
+ const capacity = line.capacity || 5;
25
+ const boxes = slots.map((id) => renderSlot(buildsById[id], colors[id] || "normal"));
26
+ for (let i = slots.length; i < capacity; i++) {
27
+ boxes.push(`<div class="comp-slot comp-slot--empty"></div>`);
28
+ }
29
+ return `
30
+ <div class="comp-line">
31
+ <span class="comp-line__label">P${idx + 1}</span>
32
+ <div class="comp-line__slots">${boxes.join("")}</div>
33
+ <span class="comp-line__count">${slots.length} / ${capacity}</span>
34
+ </div>`;
35
+ }).join("");
36
+ }
37
+
38
+ export function renderCompCard(comp, buildsById = {}, catalog = null) {
39
+ const colors = comp.buildColors || {};
40
+ const referenced = [...new Set((comp.partyLines || []).flatMap((l) => l.slots || []))];
41
+ const pool = referenced
42
+ .map((id) =>
43
+ buildsById[id]
44
+ ? renderMiniBuildCard(buildsById[id], catalog, {
45
+ showActions: false,
46
+ slotColor: colors[id] || null,
47
+ })
48
+ : renderMissingMiniBuildCard(id)
49
+ )
50
+ .join("");
51
+ const tags = (comp.tags || [])
52
+ .map((t) => `<span class="comp-card__tag">${escapeHtml(t)}</span>`)
53
+ .join("");
54
+ return `
55
+ <div class="comp-card">
56
+ <div class="comp-card__head">
57
+ <span class="comp-card__name">${escapeHtml(comp.title || "Untitled comp")}</span>
58
+ ${tags}
59
+ </div>
60
+ <div class="comp-card__lines">${renderPartyLines(comp, buildsById)}</div>
61
+ <div class="comp-card__pool">${pool}</div>
62
+ </div>`;
63
+ }
package/src/escape.js ADDED
@@ -0,0 +1,10 @@
1
+ // HTML escaping for string-template renderers. DOM-free so the package
2
+ // works in Node tests and any bundler.
3
+ export function escapeHtml(s) {
4
+ return String(s ?? "")
5
+ .replace(/&/g, "&amp;")
6
+ .replace(/</g, "&lt;")
7
+ .replace(/>/g, "&gt;")
8
+ .replace(/"/g, "&quot;")
9
+ .replace(/'/g, "&#39;");
10
+ }