Muse owns muse-shaped files now

This commit is contained in:
Endeavorance 2025-03-17 17:20:30 -04:00
parent 3e2ab6ec73
commit bf444ccb1a
23 changed files with 1288 additions and 201 deletions

View file

@ -0,0 +1,41 @@
import type { Ability } from "@proscenium/playbill";
import { html, renderMarkdown } from "#lib";
export async function AbilityCard(ability: Ability): Promise<string> {
const renderedMarkdown = await renderMarkdown(ability.description);
const costs: string[] = [];
if (ability.ap > 0) {
costs.push(`<li class="ability-cost-ap">${ability.ap} AP</li>`);
}
if (ability.hp > 0) {
costs.push(`<li class="ability-cost-hp">${ability.hp} HP</li>`);
}
if (ability.ep > 0) {
costs.push(`<li class="ability-cost-ep">${ability.ep} EP</li>`);
}
const costList =
costs.length > 0
? `<ul class="ability-costs">${costs.join("\n")}</ul>`
: "";
const classList = `ability ability-${ability.type}`;
return html`
<div class="${classList}" data-component-id="${ability.id}">
<div class="ability-header">
<h4>${ability.name}</h4>
<span class="ability-type">${ability.type}</span>
<span class="ability-cost-xp">${ability.xp} XP</span>
${costList}
</div>
<div class="ability-content">
${renderedMarkdown}
</div>
</div>
`;
}

View file

@ -0,0 +1,39 @@
import { html } from "#lib";
interface SectionProps {
type: string;
title: string;
content: string;
preInfo?: string;
info?: string;
componentId?: string;
leftCornerTag?: string;
rightCornerTag?: string;
}
export async function Section({
type,
title,
content,
preInfo = "",
info = "",
componentId,
leftCornerTag = "",
rightCornerTag = "",
}: SectionProps): Promise<string> {
const dataTags = componentId ? `data-component-id="${componentId}"` : "";
return html`
<section class="section ${type}" ${dataTags}>
<div class="section-header ${type}-header">
${preInfo}
<h2>${title}</h2>
${info}
</div>
<div class="section-tag section-tag--left">${leftCornerTag}</div>
<div class="section-tag section-tag--right">${rightCornerTag}</div>
<div class="section-content ${type}-content">
${content}
</div>
</section>
`;
}

View file

@ -0,0 +1,25 @@
import { html } from "#lib";
import { Section } from "./base/section";
export async function GlossaryTable(
glossary: Record<string, string[]>,
): Promise<string> {
const terms = Object.entries(glossary).map(([term, defs]) => {
return html`<dt>${term}</dt>${defs.map((d) => html`<dd>${d}</dd>`).join("\n")}`;
});
const content = html`
<div class="glossary-content">
<dl>
${terms.join("\n")}
</dl>
</div>
`;
return Section({
type: "glossary",
componentId: "glossary",
title: "Glossary",
content,
});
}

View file

@ -0,0 +1,12 @@
import type { Playbill } from "@proscenium/playbill";
import { html } from "#lib";
export async function PageHeader(playbill: Playbill): Promise<string> {
return html`
<header>
<h1 class="header-title">${playbill.name}</h1>
<p class="header-byline">A Playbill by ${playbill.author}</p>
<p><small class="header-info">Version ${playbill.version}</small></p>
</header>
`;
}

View file

@ -0,0 +1,51 @@
import type { Item } from "@proscenium/playbill";
import { capitalize } from "lodash-es";
import { html, renderMarkdown } from "#lib";
import { Section } from "./base/section";
export async function ItemCard(item: Item): Promise<string> {
const renderedMarkdown = await renderMarkdown(item.description);
let itemTypeDescriptor = "";
switch (item.type) {
case "wielded":
itemTypeDescriptor = `Wielded (${item.hands} ${item.hands === 1 ? "Hand" : "Hands"})`;
break;
case "worn":
itemTypeDescriptor = `Worn (${item.slot})`;
break;
case "trinket":
itemTypeDescriptor = "Trinket";
break;
}
const affinity =
item.affinity !== "none"
? html`<p class="item-affinity ${item.affinity}">${item.affinity} Affinity</p>`
: "";
const damageType = item.damage?.type === "arc" ? "Arcane" : "Physical";
const damageAmount = item.damage?.amount ?? 0;
const damageRating =
damageAmount > 0
? html`<p class="item-damage ${damageType}">${damageAmount} ${damageType} Damage</p>`
: "";
const info = html`
<div class="item-info">
${affinity}
${damageRating}
</div>
`;
return Section({
type: "item",
componentId: item.id,
title: item.name,
content: renderedMarkdown,
info,
rightCornerTag: capitalize(item.rarity),
leftCornerTag: itemTypeDescriptor,
});
}

View file

@ -0,0 +1,38 @@
import type { Method, Playbill } from "@proscenium/playbill";
import { html, renderMarkdown } from "#lib";
import { AbilityCard } from "./ability";
import { Section } from "./base/section";
export async function MethodSection(method: Method, playbill: Playbill) {
const descriptionHTML = await renderMarkdown(method.description);
let ranksHTML = "";
let rankNumber = 1;
for (const rank of method.abilities) {
ranksHTML += `<div class="method-rank"><h3>Rank ${rankNumber}</h3><div class="method-rank-content">`;
for (const ability of rank) {
const html = await AbilityCard(playbill.abilities[ability]);
ranksHTML += html;
}
ranksHTML += "</div></div>";
rankNumber++;
}
const methodContent = html`
<div class="method-ranks">
${ranksHTML}
</div>
`;
return Section({
type: "method",
componentId: method.id,
title: method.name,
preInfo: html`<p class="method-curator">${method.curator}'s Method of</p>`,
info: html`<p class="method-description">${descriptionHTML}</p>`,
content: methodContent,
});
}

View file

@ -0,0 +1,102 @@
import type {
ImageResource,
Resource,
TableResource,
TextResource,
} from "@proscenium/playbill";
import { html, renderMarkdown } from "#lib";
import { Section } from "./base/section";
async function TextResourceSection(resource: TextResource): Promise<string> {
const renderedMarkdown = await renderMarkdown(resource.description);
const info = resource.categories
.map((category) => {
return html`<li>${category}</li>`;
})
.join("\n");
return Section({
type: "resource",
componentId: resource.id,
title: resource.name,
content: renderedMarkdown,
info: `<ul class="resource-categories">${info}</ul>`,
});
}
async function ImageResourceSection(resource: ImageResource): Promise<string> {
const renderedMarkdown = await renderMarkdown(resource.description);
const info = resource.categories
.map((category) => {
return html`<li>${category}</li>`;
})
.join("\n");
const fig = html`
<figure class="resource-image">
<img src="${resource.url}" alt="${resource.name}" />
<figcaption>${renderedMarkdown}</figcaption>
</figure>
`;
return Section({
type: "resource",
componentId: resource.id,
title: resource.name,
content: fig,
info: `<ul class="resource-categories">${info}</ul>`,
});
}
async function TableResourceSection(resource: TableResource): Promise<string> {
const renderedMarkdown = await renderMarkdown(resource.description);
const info = resource.categories
.map((category) => {
return html`<li>${category}</li>`;
})
.join("\n");
const tableData = resource.data;
const [headerRow, ...rows] = tableData;
let tableHTML = html`<table class="resource-table"><thead><tr>`;
for (const header of headerRow) {
tableHTML += html`<th>${await renderMarkdown(header)}</th>`;
}
tableHTML += html`</tr></thead><tbody>`;
for (const row of rows) {
tableHTML += html`<tr>`;
for (const cell of row) {
tableHTML += html`<td>${await renderMarkdown(cell)}</td>`;
}
tableHTML += html`</tr>`;
}
tableHTML += html`</tbody></table>`;
return Section({
type: "resource",
componentId: resource.id,
title: resource.name,
content: renderedMarkdown + tableHTML,
info: `<ul class="resource-categories">${info}</ul>`,
});
}
export async function ResourceSection(resource: Resource): Promise<string> {
switch (resource.type) {
case "text":
return TextResourceSection(resource);
case "image":
return ImageResourceSection(resource);
case "table":
return TableResourceSection(resource);
}
}

View file

@ -0,0 +1,14 @@
import type { Rule } from "@proscenium/playbill";
import { renderMarkdown } from "#lib";
import { Section } from "./base/section";
export async function RuleSection(rule: Rule): Promise<string> {
const renderedMarkdown = await renderMarkdown(rule.description);
return Section({
title: rule.name,
componentId: rule.id,
content: renderedMarkdown,
type: "rule",
});
}

View file

@ -0,0 +1,97 @@
import type { Playbill, Species } from "@proscenium/playbill";
import { html, renderMarkdown } from "#lib";
import { AbilityCard } from "./ability";
import { Section } from "./base/section";
export async function SpeciesSection(species: Species, playbill: Playbill) {
const descriptionHTML = await renderMarkdown(species.description);
let abilitiesHTML = "";
for (const ability of species.abilities) {
abilitiesHTML += await AbilityCard(playbill.abilities[ability]);
}
const hpString = species.hp >= 0 ? `+${species.hp}` : `-${species.hp}`;
const apString = species.ap >= 0 ? `+${species.ap}` : `-${species.ap}`;
const epString = species.ep >= 0 ? `+${species.ep}` : `-${species.ep}`;
const hasAbilities = species.abilities.length > 0;
const abilitySection = hasAbilities
? html`
<div class="species-abilities">
<h3>Innate Abilities</h3>
${abilitiesHTML}
</div>
`
: "";
const sectionContent = html`
<div class="species-stats">
<div class="species-stat-hp">
<h4 class="hp species-stat-hp-title" title="Health Points">HP</h4>
<p class="species-stat-hp-value">${hpString}</p>
</div>
<div class="species-stat-ap">
<h4 class="ap species-stat-ap-title" title="Action Points">AP</h4>
<p class="species-stat-ap-value">${apString}</p>
</div>
<div class="species-stat-ep">
<h4 class="ep species-stat-ep-title" title="Exhaustion Points">EP</h4>
<p class="species-stat-ep-value">${epString}</p>
</div>
</div>
<div class="species-talents">
<div class="species-talent-muscle">
<h4 class="species-talent-muscle-title muscle">Muscle</h4>
<p class="species-talent-muscle-value ${species.muscle}">${species.muscle}</p>
</div>
<div class="species-talent-focus">
<h4 class="species-talent-focus-title focus">Focus</h4>
<p class="species-talent-focus-value ${species.focus}">${species.focus}</p>
</div>
<div class="species-talent-knowledge">
<h4 class="species-talent-knowledge-title knowledge">Knowledge</h4>
<p class="species-talent-knowledge-value ${species.knowledge}">${species.knowledge}</p>
</div>
<div class="species-talent-charm">
<h4 class="species-talent-charm-title charm">Charm</h4>
<p class="species-talent-charm-value ${species.charm}">${species.charm}</p>
</div>
<div class="species-talent-cunning">
<h4 class="species-talent-cunning-title cunning">Cunning</h4>
<p class="species-talent-cunning-value ${species.cunning}">${species.cunning}</p>
</div>
<div class="species-talent-spark">
<h4 class="species-talent-spark-title spark">Spark</h4>
<p class="species-talent-spark-value ${species.spark}">${species.spark}</p>
</div>
</div>
<div class="species-content">
${descriptionHTML}
</div>
${abilitySection}
`;
return Section({
type: "species",
componentId: species.id,
title: species.name,
content: sectionContent,
info: "<p>Species</p>",
});
}

164
src/render/html/index.ts Normal file
View file

@ -0,0 +1,164 @@
import type { Playbill } from "@proscenium/playbill";
import sortBy from "lodash-es/sortBy";
import { html } from "#lib";
import { GlossaryTable } from "./component/glossary";
import { PageHeader } from "./component/header";
import { ItemCard } from "./component/item";
import { MethodSection } from "./component/method";
import { ResourceSection } from "./component/resource";
import { RuleSection } from "./component/rule";
import { SpeciesSection } from "./component/species";
interface HTMLRenderOptions {
styles?: string;
}
export async function renderPlaybillToHTML(
playbill: Playbill,
opts: HTMLRenderOptions = {},
): Promise<string> {
// Render various resources
const resources = (
await Promise.all(Object.values(playbill.resources).map(ResourceSection))
).join("\n");
const methods = (
await Promise.all(
Object.values(playbill.methods).map((method) => {
return MethodSection(method, playbill);
}),
)
).join("\n");
const species = (
await Promise.all(
Object.values(playbill.species).map((species) => {
return SpeciesSection(species, playbill);
}),
)
).join("\n");
const items = (
await Promise.all(
Object.values(playbill.items).map((item) => {
return ItemCard(item);
}),
)
).join("\n");
const rulesByOrder = sortBy(Object.values(playbill.rules), "order");
const rules = (await Promise.all(rulesByOrder.map(RuleSection))).join("\n");
const glossaryHTML = await GlossaryTable(playbill.glossary);
// Prepare stylesheet
const css = opts.styles ? html`<style>${opts.styles}</style>` : "";
// Main document
const doc = html`
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Playbill: ${playbill.name}</title>
${css}
<script>
function removeHash() {
history.pushState("", document.title, window.location.pathname);
}
function setHash(hash) {
if (hash.length > 0) {
history.pushState(null, null, "#" + hash);
} else {
removeHash();
}
}
function hideAllViews() {
document.querySelectorAll(".view").forEach((view) => {
view.style.display = "none";
});
}
function showAllViews() {
document.querySelectorAll(".view").forEach((view) => {
view.style.display = "block";
});
setHash("");
}
function showView(viewId) {
document.getElementById(viewId).style.display = "block";
setHash(viewId);
}
function hideAllAndShow(viewId) {
hideAllViews();
showView(viewId);
}
function navigateFromHash() {
const hash = window.location.hash.slice(1);
if (hash.length > 0) {
const view = document.getElementById(hash);
if (view !== null) {
hideAllAndShow(hash);
} else {
showAllViews();
}
} else {
showAllViews();
}
}
document.addEventListener("DOMContentLoaded", navigateFromHash());
window.addEventListener("hashchange", navigateFromHash())
</script>
</head>
<body>
${await PageHeader(playbill)}
<blockquote class="overview"><p>${playbill.description}</p></blockquote>
<nav>
<button onclick="hideAllAndShow('species')" id="species-nav">Species</button>
<button onclick="hideAllAndShow('methods')" id="methods-nav">Methods</button>
<button onclick="hideAllAndShow('items')" id="items-nav">Items</button>
<button onclick="hideAllAndShow('rules')" id="rules-nav">Rulebook</button>
<button onclick="hideAllAndShow('glossary')" id="glossary-nav">Glossary</button>
<button onclick="hideAllAndShow('resources')" id="resources-nav">Resources</button>
<button onclick="showAllViews()" id="all-nav">Show All</button>
</nav>
<article id="species" class="view">
${species}
</article>
<article id="methods" class="view">
${methods}
</article>
<article id="items" class="view">
${items}
</article>
<article id="rules" class="view">
${rules}
</article>
<article id="glossary" class="view">
${glossaryHTML}
</article>
<article id="resources" class="view">
${resources}
</article>
</article>
</body>
</html>
`;
return doc;
}