Muse owns muse-shaped files now
This commit is contained in:
parent
3e2ab6ec73
commit
bf444ccb1a
23 changed files with 1288 additions and 201 deletions
41
src/render/html/component/ability.ts
Normal file
41
src/render/html/component/ability.ts
Normal 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>
|
||||
`;
|
||||
}
|
39
src/render/html/component/base/section.ts
Normal file
39
src/render/html/component/base/section.ts
Normal 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>
|
||||
`;
|
||||
}
|
25
src/render/html/component/glossary.ts
Normal file
25
src/render/html/component/glossary.ts
Normal 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,
|
||||
});
|
||||
}
|
12
src/render/html/component/header.ts
Normal file
12
src/render/html/component/header.ts
Normal 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>
|
||||
`;
|
||||
}
|
51
src/render/html/component/item.ts
Normal file
51
src/render/html/component/item.ts
Normal 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,
|
||||
});
|
||||
}
|
38
src/render/html/component/method.ts
Normal file
38
src/render/html/component/method.ts
Normal 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,
|
||||
});
|
||||
}
|
102
src/render/html/component/resource.ts
Normal file
102
src/render/html/component/resource.ts
Normal 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);
|
||||
}
|
||||
}
|
14
src/render/html/component/rule.ts
Normal file
14
src/render/html/component/rule.ts
Normal 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",
|
||||
});
|
||||
}
|
97
src/render/html/component/species.ts
Normal file
97
src/render/html/component/species.ts
Normal 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
164
src/render/html/index.ts
Normal 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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue