HexCasting/doc/test/__snapshots__/test_snapshots.ambr

1218 lines
859 KiB
Text
Raw Normal View History

2023-06-06 15:24:18 +02:00
# serializer version: 1
# name: test_cmd
'''
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="The Hex Book, all in one place.">
<meta name="author" content="petrak@, Alwinfy">
<link rel="icon" href="icon.png">
<title>Hex Book</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css"
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
<style>
summary { display: list-item; }
details.spell-collapsible {
display: inline-block;
border: 1px solid #aaa;
border-radius: 4px;
padding: .5em .5em 0;
margin-bottom: .5em;
}
summary.collapse-spell {
font-weight: bold;
margin: -.5em -.5em 0;
padding: .5em;
}
details.spell-collapsible[open] {
padding: .5em;
}
details[open] summary.collapse-spell {
border-bottom: 1px solid #aaa;
margin-bottom: .5em;
}
details .collapse-spell::before {
content: "Click to show spell";
}
details[open] .collapse-spell::before {
content: "Click to hide spell";
}
blockquote.crafting-info {
font-size: inherit;
}
a.permalink {
margin-left: 0.5em;
}
a.permalink:hover {
color: lightgray;
}
p {
margin: 0.5ex 0;
}
p.fake-li {
margin: 0;
}
p.fake-li::before {
content: "\2022";
margin: 1ex;
}
.linkout::before {
content: "Link: ";
}
p.todo-note {
font-style: italic;
color: lightgray;
}
.obfuscated {
filter: blur(1em);
}
.spoilered {
filter: blur(1ex);
-moz-transition: filter 0.04s linear;
}
.spoilered:hover {
filter: blur(0.5ex);
}
.spoilered.unspoilered {
filter: blur(0);
}
canvas.spell-viz {
--dot-color: #777f;
--dot-color: #777f;
--start-dot-color: #f009;
--moving-dot-color: #0fa9;
--path-color: darkgray;
--visited-path-color: #0c8;
--dot-scale: 0.0625;
--moving-dot-scale: 0.125;
--line-scale: 0.08333;
--pausetext-scale: 0.5;
--dark-mode: 0;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #201a20;
color: #ddd;
}
.jumbotron {
background-color: #323;
}
canvas.spell-viz {
/* hack */
--dark-mode: 1;
}
}
</style>
<noscript>
<style>
/* for accessibility */
.spoilered {
filter: none !important;
}
</style>
</noscript>
<script>
"use strict";
const speeds = [0, 0.25, 0.5, 1, 2, 4];
const scrollThreshold = 100;
const rfaQueue = [];
const colorCache = new Map();
function getColorRGB(ctx, str) {
if (!colorCache.has(str)) {
ctx.fillStyle = str;
ctx.clearRect(0, 0, 1, 1);
ctx.fillRect(0, 0, 1, 1);
const imgData = ctx.getImageData(0, 0, 1, 1);
colorCache.set(str, imgData.data);
}
return colorCache.get(str);
}
function startAngle(str) {
switch (str) {
case "east": return 0;
case "north_east": return 1;
case "north_west": return 2;
case "west": return 3;
case "south_west": return 4;
case "south_east": return 5;
default: return 0;
}
}
function offsetAngle(str) {
switch (str) {
case "w": return 0;
case "q": return 1;
case "a": return 2;
case "s": return 3;
case "d": return 4;
case "e": return 5;
default: return -1;
}
}
function initializeElem(canvas) {
const str = canvas.dataset.string;
let angle = startAngle(canvas.dataset.start);
const perWorld = canvas.dataset.perWorld === "True";
// build geometry
const points = [[0, 0]];
let lastPoint = points[0];
let minPoint = lastPoint, maxPoint = lastPoint;
for (const ch of "w" + str) {
const addAngle = offsetAngle(ch);
if (addAngle < 0) continue;
angle = (angle + addAngle) % 6;
const trueAngle = Math.PI / 3 * angle;
const [lx, ly] = lastPoint;
const newPoint = [lx + Math.cos(trueAngle), ly - Math.sin(trueAngle)];
points.push(newPoint);
lastPoint = newPoint;
const [mix, miy] = minPoint;
minPoint = [Math.min(mix, newPoint[0]), Math.min(miy, newPoint[1])];
const [max, may] = maxPoint;
maxPoint = [Math.max(max, newPoint[0]), Math.max(may, newPoint[1])];
}
const size = Math.min(canvas.width, canvas.height) * 0.8;
const scale = size / Math.max(3, Math.max(maxPoint[1] - minPoint[1], maxPoint[0] - minPoint[0]));
const center = [(minPoint[0] + maxPoint[0]) * 0.5, (minPoint[1] + maxPoint[1]) * 0.5];
const truePoints = points.map(p => [canvas.width * 0.5 + scale * (p[0] - center[0]), canvas.height * 0.5 + scale * (p[1] - center[1])]);
let uniqPoints = [];
l1: for (const point of truePoints) {
for (const pt of uniqPoints) {
if (Math.abs(point[0] - pt[0]) < 0.00001 && Math.abs(point[1] - pt[1]) < 0.00001) {
continue l1;
}
}
uniqPoints.push(point);
}
// rendering code
const speed = 0.0025;
const context = canvas.getContext("2d");
const negaProgress = -3;
let progress = 0;
let scrollTimeout = 1e309;
let speedLevel = 3;
let speedIncrement = 0;
function speedScale() {
return speeds[speedLevel];
}
const style = getComputedStyle(canvas);
const getProp = n => style.getPropertyValue(n);
const tick = dt => {
scrollTimeout += dt;
if (canvas.offsetParent === null) return;
const strokeStyle = getProp("--path-color");
const strokeVisitedStyle = getProp("--visited-path-color");
const startDotStyle = getProp("--start-dot-color");
const dotStyle = getProp("--dot-color");
const movDotStyle = getProp("--moving-dot-color");
const strokeWidth = scale * +getProp("--line-scale");
const dotRadius = scale * +getProp("--dot-scale");
const movDotRadius = scale * +getProp("--moving-dot-scale");
const pauseScale = scale * +getProp("--pausetext-scale");
const bodyBg = scale * +getProp("--pausetext-scale");
const darkMode = +getProp("--dark-mode");
const bgColors = getColorRGB(context, getComputedStyle(document.body).backgroundColor);
if (!perWorld) {
progress += speed * dt * (progress > 0 ? speedScale() : Math.sqrt(speedScale()));
}
if (progress >= truePoints.length - 1) {
progress = negaProgress;
}
let ix = Math.floor(progress), frac = progress - ix, core = null, fadeColor = 0;
if (ix < 0) {
const rawFade = 2 * progress / negaProgress - 1;
fadeColor = 1 - Math.abs(rawFade);
context.strokeStyle = rawFade > 0 ? strokeVisitedStyle : strokeStyle;
ix = rawFade > 0 ? truePoints.length - 2 : 0;
frac = +(rawFade > 0);
} else {
context.strokeStyle = strokeVisitedStyle;
}
const [lx, ly] = truePoints[ix];
const [rx, ry] = truePoints[ix + 1];
core = [lx + (rx - lx) * frac, ly + (ry - ly) * frac];
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.lineWidth = strokeWidth;
context.moveTo(truePoints[0][0], truePoints[0][1]);
for (let i = 1; i < ix + 1; i++) {
context.lineTo(truePoints[i][0], truePoints[i][1]);
}
context.lineTo(core[0], core[1]);
context.stroke();
context.beginPath();
context.strokeStyle = strokeStyle;
context.moveTo(core[0], core[1]);
for (let i = ix + 1; i < truePoints.length; i++) {
context.lineTo(truePoints[i][0], truePoints[i][1]);
}
context.stroke();
for (let i = 0; i < uniqPoints.length; i++) {
context.beginPath();
context.fillStyle = (i == 0 && !perWorld) ? startDotStyle : dotStyle;
const radius = (i == 0 && !perWorld) ? movDotRadius : dotRadius;
context.arc(uniqPoints[i][0], uniqPoints[i][1], radius, 0, 2 * Math.PI);
context.fill();
}
if (!perWorld) {
context.beginPath();
context.fillStyle = movDotStyle;
context.arc(core[0], core[1], movDotRadius, 0, 2 * Math.PI);
context.fill();
}
if (fadeColor) {
context.fillStyle = `rgba(${bgColors[0]}, ${bgColors[1]}, ${bgColors[2]}, ${fadeColor})`;
context.fillRect(0, 0, canvas.width, canvas.height);
}
if (scrollTimeout <= 2000) {
context.fillStyle = `rgba(200, 200, 200, ${(2000 - scrollTimeout) / 1000})`;
context.font = `${pauseScale}px sans-serif`;
context.fillText(speedScale() ? speedScale() + "x" : "Paused", 0.2 * scale, canvas.height - 0.2 * scale);
}
};
rfaQueue.push(tick);
// scrolling input
if (!perWorld) {
canvas.addEventListener("wheel", ev => {
speedIncrement += ev.deltaY;
const oldSpeedLevel = speedLevel;
if (speedIncrement >= scrollThreshold) {
speedLevel--;
} else if (speedIncrement <= -scrollThreshold) {
speedLevel++;
}
if (oldSpeedLevel != speedLevel) {
speedIncrement = 0;
speedLevel = Math.max(0, Math.min(speeds.length - 1, speedLevel));
scrollTimeout = 0;
}
ev.preventDefault();
});
}
}
function hookLoad(elem) {
let init = false;
const canvases = elem.querySelectorAll("canvas");
elem.addEventListener("toggle", () => {
if (!init) {
canvases.forEach(initializeElem);
init = true;
}
});
}
function hookToggle(elem) {
const details = Array.from(document.querySelectorAll("details." + elem.dataset.target));
elem.addEventListener("click", () => {
if (details.some(x => x.open)) {
details.forEach(x => x.open = false);
} else {
details.forEach(x => x.open = true);
}
});
}
const params = new URLSearchParams(document.location.search);
function hookSpoiler(elem) {
if (params.get("nospoiler") !== null) {
elem.classList.add("unspoilered");
} else {
const thunk = ev => {
if (!elem.classList.contains("unspoilered")) {
ev.preventDefault();
ev.stopImmediatePropagation();
elem.classList.add("unspoilered");
}
elem.removeEventListener("click", thunk);
};
elem.addEventListener("click", thunk);
if (elem instanceof HTMLAnchorElement) {
const href = elem.getAttribute("href");
if (href.startsWith("#")) {
elem.addEventListener("click", () => document.getElementById(href.substring(1)).querySelector(".spoilered").classList.add("unspoilered"));
}
}
}
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('details.spell-collapsible').forEach(hookLoad);
document.querySelectorAll('a.toggle-link').forEach(hookToggle);
document.querySelectorAll('.spoilered').forEach(hookSpoiler);
function tick(prevTime, time) {
const dt = time - prevTime;
for (const q of rfaQueue) {
q(dt);
}
requestAnimationFrame(t => tick(time, t));
}
requestAnimationFrame(t => tick(t, t));
});
</script>
</head>
<body>
<div class="container" style="margin-top: 3em;">
<blockquote>
<h1>This is the online version of the Hexcasting documentation.</h1>
<p>Embedded images and patterns are included, but not crafting recipes or items. There's an in-game book for
those.</p>
<p>Additionally, this is built from the latest code on GitHub. It may describe <b>newer</b> features that you
may not necessarily have, even on the latest CurseForge version!</p>
<p><b>Entries which are blurred are spoilers</b>. Click to reveal them, but be aware that they may spoil endgame
progression. Alternatively, click <a href="?nospoiler">here</a> to get a version with all spoilers showing.
</p>
</blockquote>
</div>
2023-06-11 01:14:53 +02:00
<div class='container'><header class='jumbotron'><h1 class='book-title'>Hex Notebook</h1><p>I seem to have discovered a new method of magical arts, in which one draws patterns strange and wild onto a hexagonal grid. It fascinates me. I&#x27;ve decided to start a journal of my thoughts and findings.</p><p><a href='https://discord.gg/4xxHGYteWk'>Discord Server Link</a></p></header><nav><h2 id='table-of-contents' class='page-header'>Table of Contents<a href='javascript:void(0)' class='permalink toggle-link small' data-target='toc-category' title='Toggle all'><i class='bi bi-list-nested'></i></a><a href='#table-of-contents' class='permalink small' title='Permalink'><i class='bi bi-link-45deg'></i></a></h2><details class='toc-category'><summary><a href='#basics' class=''>Getting Started</a></summary><ul><li><a href='#basics/media' class=''>Media</a></li><li><a href='#basics/geodes' class=''>Geodes</a></li><li><a href='#basics/couldnt_cast' class='spoilered'>A Frustration</a></li><li><a href='#basics/start_to_see' class='spoilered'>WHAT DID I SEE</a></li></ul></details><details class='toc-category'><summary><a href='#casting' class=''>Hex Casting</a></summary><ul><li><a href='#casting/101' class=''>Hexing 101</a></li><li><a href='#casting/vectors' class=''>A Primer on Vectors</a></li><li><a href='#casting/mishaps' class=''>Mishaps</a></li><li><a href='#casting/stack' class=''>Stacks</a></li><li><a href='#casting/naming' class=''>Naming Actions</a></li><li><a href='#casting/influences' class=''>Influences</a></li><li><a href='#casting/mishaps2' class='spoilered'>Enlightened Mishaps</a></li></ul></details><details class='toc-category'><summary><a href='#items' class=''>Items</a></summary><ul><li><a href='#items/amethyst' class=''>Amethyst</a></li><li><a href='#items/staff' class=''>Staff</a></li><li><a href='#items/lens' class=''>Scrying Lens</a></li><li><a href='#items/focus' class=''>Focus</a></li><li><a href='#items/abacus' class=''>Abacus</a></li><li><a href='#items/spellbook' class=''>Spellbook</a></li><li><a href='#items/scroll' class=''>Scrolls</a></li><li><a href='#items/slate' class=''>Slates</a></li><li><a href='#items/hexcasting' class=''>Casting Items</a></li><li><a href='#items/phials' class=''>Phials of Media</a></li><li><a href='#items/pigments' class=''>Pigments</a></li><li><a href='#items/edified' class=''>Edified Trees</a></li><li><a href='#items/jeweler_hammer' class=''>Jeweler&#x27;s Hammer</a></li><li><a href='#items/decoration' class=''>Decorative Blocks</a></li></ul></details><details class='toc-category'><summary><a href='#greatwork' class='spoilered'>The Great Work</a></summary><ul><li><a href='#greatwork/the_work' class='spoilered'>The Work</a></li><li><a href='#greatwork/brainsweeping' class='spoilered'>On the Flaying of Minds</a></li><li><a href='#greatwork/spellcircles' class='spoilered'>Spell Circles</a></li><li><a href='#greatwork/impetus' class='spoilered'>Impetuses</a></li><li><a href='#greatwork/directrix' class='spoilered'>Directrices</a></li><li><a href='#greatwork/akashiclib' class='spoilered'>Akashic Libraries</a></li></ul></details><details class='toc-category'><summary><a href='#lore' class=''>Lore</a></summary><ul><li><a href='#lore/terabithia1' class=''>Cardamom Steles, #1</a></li><li><a href='#lore/terabithia2' class=''>Cardamom Steles, #2</a></li><li><a href='#lore/terabithia3' class=''>Cardamom Steles, #3, 1/2</a></li><li><a href='#lore/terabithia4' class=''>Cardamom Steles, #3, 2/2</a></li><li><a href='#lore/terabithia5' class=''>Cardamom Steles, #4</a></li><li><a href='#lore/experiment1' class=''>Wooleye Instance Notes</a></li><li><a href='#lore/experiment2' class=''>Wooleye Interview Logs</a></li><li><a href='#lore/inventory' class=''>Restoration Log #72</a></li></ul></details><details class='toc-category'><summary><a href='#interop' class=''>Cross-Mod Compatibility</a></summary><ul><li><a href='#interop/interop' class=''>Cross-Mod Compatibility</a></li><li><a href='#interop/gravity' class=''>Gravity Changer</a></li><li><a href='#interop/pehkui' class=''>Pehkui</a></li></ul></de
</body>
</html>
'''
# ---
# name: test_file
2023-06-06 15:24:18 +02:00
'''
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="The Hex Book, all in one place.">
<meta name="author" content="petrak@, Alwinfy">
<link rel="icon" href="icon.png">
<title>Hex Book</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css"
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
<style>
summary { display: list-item; }
details.spell-collapsible {
display: inline-block;
border: 1px solid #aaa;
border-radius: 4px;
padding: .5em .5em 0;
margin-bottom: .5em;
}
summary.collapse-spell {
font-weight: bold;
margin: -.5em -.5em 0;
padding: .5em;
}
details.spell-collapsible[open] {
padding: .5em;
}
details[open] summary.collapse-spell {
border-bottom: 1px solid #aaa;
margin-bottom: .5em;
}
details .collapse-spell::before {
content: "Click to show spell";
}
details[open] .collapse-spell::before {
content: "Click to hide spell";
}
blockquote.crafting-info {
font-size: inherit;
}
a.permalink {
margin-left: 0.5em;
}
a.permalink:hover {
color: lightgray;
}
p {
margin: 0.5ex 0;
}
p.fake-li {
margin: 0;
}
p.fake-li::before {
content: "\2022";
margin: 1ex;
}
.linkout::before {
content: "Link: ";
}
p.todo-note {
font-style: italic;
color: lightgray;
}
.obfuscated {
filter: blur(1em);
}
.spoilered {
filter: blur(1ex);
-moz-transition: filter 0.04s linear;
}
.spoilered:hover {
filter: blur(0.5ex);
}
.spoilered.unspoilered {
filter: blur(0);
}
canvas.spell-viz {
--dot-color: #777f;
--dot-color: #777f;
--start-dot-color: #f009;
--moving-dot-color: #0fa9;
--path-color: darkgray;
--visited-path-color: #0c8;
--dot-scale: 0.0625;
--moving-dot-scale: 0.125;
--line-scale: 0.08333;
--pausetext-scale: 0.5;
--dark-mode: 0;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #201a20;
color: #ddd;
}
.jumbotron {
background-color: #323;
}
canvas.spell-viz {
/* hack */
--dark-mode: 1;
}
}
</style>
<noscript>
<style>
/* for accessibility */
.spoilered {
filter: none !important;
}
</style>
</noscript>
<script>
"use strict";
const speeds = [0, 0.25, 0.5, 1, 2, 4];
const scrollThreshold = 100;
const rfaQueue = [];
const colorCache = new Map();
function getColorRGB(ctx, str) {
if (!colorCache.has(str)) {
ctx.fillStyle = str;
ctx.clearRect(0, 0, 1, 1);
ctx.fillRect(0, 0, 1, 1);
const imgData = ctx.getImageData(0, 0, 1, 1);
colorCache.set(str, imgData.data);
}
return colorCache.get(str);
}
function startAngle(str) {
switch (str) {
case "east": return 0;
case "north_east": return 1;
case "north_west": return 2;
case "west": return 3;
case "south_west": return 4;
case "south_east": return 5;
default: return 0;
}
}
function offsetAngle(str) {
switch (str) {
case "w": return 0;
case "q": return 1;
case "a": return 2;
case "s": return 3;
case "d": return 4;
case "e": return 5;
default: return -1;
}
}
function initializeElem(canvas) {
const str = canvas.dataset.string;
let angle = startAngle(canvas.dataset.start);
const perWorld = canvas.dataset.perWorld === "True";
// build geometry
const points = [[0, 0]];
let lastPoint = points[0];
let minPoint = lastPoint, maxPoint = lastPoint;
for (const ch of "w" + str) {
const addAngle = offsetAngle(ch);
if (addAngle < 0) continue;
angle = (angle + addAngle) % 6;
const trueAngle = Math.PI / 3 * angle;
const [lx, ly] = lastPoint;
const newPoint = [lx + Math.cos(trueAngle), ly - Math.sin(trueAngle)];
points.push(newPoint);
lastPoint = newPoint;
const [mix, miy] = minPoint;
minPoint = [Math.min(mix, newPoint[0]), Math.min(miy, newPoint[1])];
const [max, may] = maxPoint;
maxPoint = [Math.max(max, newPoint[0]), Math.max(may, newPoint[1])];
}
const size = Math.min(canvas.width, canvas.height) * 0.8;
const scale = size / Math.max(3, Math.max(maxPoint[1] - minPoint[1], maxPoint[0] - minPoint[0]));
const center = [(minPoint[0] + maxPoint[0]) * 0.5, (minPoint[1] + maxPoint[1]) * 0.5];
const truePoints = points.map(p => [canvas.width * 0.5 + scale * (p[0] - center[0]), canvas.height * 0.5 + scale * (p[1] - center[1])]);
let uniqPoints = [];
l1: for (const point of truePoints) {
for (const pt of uniqPoints) {
if (Math.abs(point[0] - pt[0]) < 0.00001 && Math.abs(point[1] - pt[1]) < 0.00001) {
continue l1;
}
}
uniqPoints.push(point);
}
// rendering code
const speed = 0.0025;
const context = canvas.getContext("2d");
const negaProgress = -3;
let progress = 0;
let scrollTimeout = 1e309;
let speedLevel = 3;
let speedIncrement = 0;
function speedScale() {
return speeds[speedLevel];
}
const style = getComputedStyle(canvas);
const getProp = n => style.getPropertyValue(n);
const tick = dt => {
scrollTimeout += dt;
if (canvas.offsetParent === null) return;
const strokeStyle = getProp("--path-color");
const strokeVisitedStyle = getProp("--visited-path-color");
const startDotStyle = getProp("--start-dot-color");
const dotStyle = getProp("--dot-color");
const movDotStyle = getProp("--moving-dot-color");
const strokeWidth = scale * +getProp("--line-scale");
const dotRadius = scale * +getProp("--dot-scale");
const movDotRadius = scale * +getProp("--moving-dot-scale");
const pauseScale = scale * +getProp("--pausetext-scale");
const bodyBg = scale * +getProp("--pausetext-scale");
const darkMode = +getProp("--dark-mode");
const bgColors = getColorRGB(context, getComputedStyle(document.body).backgroundColor);
if (!perWorld) {
progress += speed * dt * (progress > 0 ? speedScale() : Math.sqrt(speedScale()));
}
if (progress >= truePoints.length - 1) {
progress = negaProgress;
}
let ix = Math.floor(progress), frac = progress - ix, core = null, fadeColor = 0;
if (ix < 0) {
const rawFade = 2 * progress / negaProgress - 1;
fadeColor = 1 - Math.abs(rawFade);
context.strokeStyle = rawFade > 0 ? strokeVisitedStyle : strokeStyle;
ix = rawFade > 0 ? truePoints.length - 2 : 0;
frac = +(rawFade > 0);
} else {
context.strokeStyle = strokeVisitedStyle;
}
const [lx, ly] = truePoints[ix];
const [rx, ry] = truePoints[ix + 1];
core = [lx + (rx - lx) * frac, ly + (ry - ly) * frac];
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.lineWidth = strokeWidth;
context.moveTo(truePoints[0][0], truePoints[0][1]);
for (let i = 1; i < ix + 1; i++) {
context.lineTo(truePoints[i][0], truePoints[i][1]);
}
context.lineTo(core[0], core[1]);
context.stroke();
context.beginPath();
context.strokeStyle = strokeStyle;
context.moveTo(core[0], core[1]);
for (let i = ix + 1; i < truePoints.length; i++) {
context.lineTo(truePoints[i][0], truePoints[i][1]);
}
context.stroke();
for (let i = 0; i < uniqPoints.length; i++) {
context.beginPath();
context.fillStyle = (i == 0 && !perWorld) ? startDotStyle : dotStyle;
const radius = (i == 0 && !perWorld) ? movDotRadius : dotRadius;
context.arc(uniqPoints[i][0], uniqPoints[i][1], radius, 0, 2 * Math.PI);
context.fill();
}
if (!perWorld) {
context.beginPath();
context.fillStyle = movDotStyle;
context.arc(core[0], core[1], movDotRadius, 0, 2 * Math.PI);
context.fill();
}
if (fadeColor) {
context.fillStyle = `rgba(${bgColors[0]}, ${bgColors[1]}, ${bgColors[2]}, ${fadeColor})`;
context.fillRect(0, 0, canvas.width, canvas.height);
}
if (scrollTimeout <= 2000) {
context.fillStyle = `rgba(200, 200, 200, ${(2000 - scrollTimeout) / 1000})`;
context.font = `${pauseScale}px sans-serif`;
context.fillText(speedScale() ? speedScale() + "x" : "Paused", 0.2 * scale, canvas.height - 0.2 * scale);
}
};
rfaQueue.push(tick);
// scrolling input
if (!perWorld) {
canvas.addEventListener("wheel", ev => {
speedIncrement += ev.deltaY;
const oldSpeedLevel = speedLevel;
if (speedIncrement >= scrollThreshold) {
speedLevel--;
} else if (speedIncrement <= -scrollThreshold) {
speedLevel++;
}
if (oldSpeedLevel != speedLevel) {
speedIncrement = 0;
speedLevel = Math.max(0, Math.min(speeds.length - 1, speedLevel));
scrollTimeout = 0;
}
ev.preventDefault();
});
}
}
function hookLoad(elem) {
let init = false;
const canvases = elem.querySelectorAll("canvas");
elem.addEventListener("toggle", () => {
if (!init) {
canvases.forEach(initializeElem);
init = true;
}
});
}
function hookToggle(elem) {
const details = Array.from(document.querySelectorAll("details." + elem.dataset.target));
elem.addEventListener("click", () => {
if (details.some(x => x.open)) {
details.forEach(x => x.open = false);
} else {
details.forEach(x => x.open = true);
}
});
}
const params = new URLSearchParams(document.location.search);
function hookSpoiler(elem) {
if (params.get("nospoiler") !== null) {
elem.classList.add("unspoilered");
} else {
const thunk = ev => {
if (!elem.classList.contains("unspoilered")) {
ev.preventDefault();
ev.stopImmediatePropagation();
elem.classList.add("unspoilered");
}
elem.removeEventListener("click", thunk);
};
elem.addEventListener("click", thunk);
if (elem instanceof HTMLAnchorElement) {
const href = elem.getAttribute("href");
if (href.startsWith("#")) {
elem.addEventListener("click", () => document.getElementById(href.substring(1)).querySelector(".spoilered").classList.add("unspoilered"));
}
}
}
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('details.spell-collapsible').forEach(hookLoad);
document.querySelectorAll('a.toggle-link').forEach(hookToggle);
document.querySelectorAll('.spoilered').forEach(hookSpoiler);
function tick(prevTime, time) {
const dt = time - prevTime;
for (const q of rfaQueue) {
q(dt);
}
requestAnimationFrame(t => tick(time, t));
}
requestAnimationFrame(t => tick(t, t));
});
</script>
</head>
<body>
<div class="container" style="margin-top: 3em;">
<blockquote>
<h1>This is the online version of the Hexcasting documentation.</h1>
<p>Embedded images and patterns are included, but not crafting recipes or items. There's an in-game book for
those.</p>
<p>Additionally, this is built from the latest code on GitHub. It may describe <b>newer</b> features that you
may not necessarily have, even on the latest CurseForge version!</p>
<p><b>Entries which are blurred are spoilers</b>. Click to reveal them, but be aware that they may spoil endgame
progression. Alternatively, click <a href="?nospoiler">here</a> to get a version with all spoilers showing.
</p>
</blockquote>
</div>
2023-06-11 01:14:53 +02:00
<div class='container'><header class='jumbotron'><h1 class='book-title'>Hex Notebook</h1><p>I seem to have discovered a new method of magical arts, in which one draws patterns strange and wild onto a hexagonal grid. It fascinates me. I&#x27;ve decided to start a journal of my thoughts and findings.</p><p><a href='https://discord.gg/4xxHGYteWk'>Discord Server Link</a></p></header><nav><h2 id='table-of-contents' class='page-header'>Table of Contents<a href='javascript:void(0)' class='permalink toggle-link small' data-target='toc-category' title='Toggle all'><i class='bi bi-list-nested'></i></a><a href='#table-of-contents' class='permalink small' title='Permalink'><i class='bi bi-link-45deg'></i></a></h2><details class='toc-category'><summary><a href='#basics' class=''>Getting Started</a></summary><ul><li><a href='#basics/media' class=''>Media</a></li><li><a href='#basics/geodes' class=''>Geodes</a></li><li><a href='#basics/couldnt_cast' class='spoilered'>A Frustration</a></li><li><a href='#basics/start_to_see' class='spoilered'>WHAT DID I SEE</a></li></ul></details><details class='toc-category'><summary><a href='#casting' class=''>Hex Casting</a></summary><ul><li><a href='#casting/101' class=''>Hexing 101</a></li><li><a href='#casting/vectors' class=''>A Primer on Vectors</a></li><li><a href='#casting/mishaps' class=''>Mishaps</a></li><li><a href='#casting/stack' class=''>Stacks</a></li><li><a href='#casting/naming' class=''>Naming Actions</a></li><li><a href='#casting/influences' class=''>Influences</a></li><li><a href='#casting/mishaps2' class='spoilered'>Enlightened Mishaps</a></li></ul></details><details class='toc-category'><summary><a href='#items' class=''>Items</a></summary><ul><li><a href='#items/amethyst' class=''>Amethyst</a></li><li><a href='#items/staff' class=''>Staff</a></li><li><a href='#items/lens' class=''>Scrying Lens</a></li><li><a href='#items/focus' class=''>Focus</a></li><li><a href='#items/abacus' class=''>Abacus</a></li><li><a href='#items/spellbook' class=''>Spellbook</a></li><li><a href='#items/scroll' class=''>Scrolls</a></li><li><a href='#items/slate' class=''>Slates</a></li><li><a href='#items/hexcasting' class=''>Casting Items</a></li><li><a href='#items/phials' class=''>Phials of Media</a></li><li><a href='#items/pigments' class=''>Pigments</a></li><li><a href='#items/edified' class=''>Edified Trees</a></li><li><a href='#items/jeweler_hammer' class=''>Jeweler&#x27;s Hammer</a></li><li><a href='#items/decoration' class=''>Decorative Blocks</a></li></ul></details><details class='toc-category'><summary><a href='#greatwork' class='spoilered'>The Great Work</a></summary><ul><li><a href='#greatwork/the_work' class='spoilered'>The Work</a></li><li><a href='#greatwork/brainsweeping' class='spoilered'>On the Flaying of Minds</a></li><li><a href='#greatwork/spellcircles' class='spoilered'>Spell Circles</a></li><li><a href='#greatwork/impetus' class='spoilered'>Impetuses</a></li><li><a href='#greatwork/directrix' class='spoilered'>Directrices</a></li><li><a href='#greatwork/akashiclib' class='spoilered'>Akashic Libraries</a></li></ul></details><details class='toc-category'><summary><a href='#lore' class=''>Lore</a></summary><ul><li><a href='#lore/terabithia1' class=''>Cardamom Steles, #1</a></li><li><a href='#lore/terabithia2' class=''>Cardamom Steles, #2</a></li><li><a href='#lore/terabithia3' class=''>Cardamom Steles, #3, 1/2</a></li><li><a href='#lore/terabithia4' class=''>Cardamom Steles, #3, 2/2</a></li><li><a href='#lore/terabithia5' class=''>Cardamom Steles, #4</a></li><li><a href='#lore/experiment1' class=''>Wooleye Instance Notes</a></li><li><a href='#lore/experiment2' class=''>Wooleye Interview Logs</a></li><li><a href='#lore/inventory' class=''>Restoration Log #72</a></li></ul></details><details class='toc-category'><summary><a href='#interop' class=''>Cross-Mod Compatibility</a></summary><ul><li><a href='#interop/interop' class=''>Cross-Mod Compatibility</a></li><li><a href='#interop/gravity' class=''>Gravity Changer</a></li><li><a href='#interop/pehkui' class=''>Pehkui</a></li></ul></de
2023-06-06 15:24:18 +02:00
</body>
</html>
'''
# ---
2023-06-07 05:01:25 +02:00
# name: test_stdout
CaptureResult(
err='',
out='''
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="The Hex Book, all in one place.">
<meta name="author" content="petrak@, Alwinfy">
<link rel="icon" href="icon.png">
<title>Hex Book</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css"
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
<style>
summary { display: list-item; }
details.spell-collapsible {
display: inline-block;
border: 1px solid #aaa;
border-radius: 4px;
padding: .5em .5em 0;
margin-bottom: .5em;
}
summary.collapse-spell {
font-weight: bold;
margin: -.5em -.5em 0;
padding: .5em;
}
details.spell-collapsible[open] {
padding: .5em;
}
details[open] summary.collapse-spell {
border-bottom: 1px solid #aaa;
margin-bottom: .5em;
}
details .collapse-spell::before {
content: "Click to show spell";
}
details[open] .collapse-spell::before {
content: "Click to hide spell";
}
blockquote.crafting-info {
font-size: inherit;
}
a.permalink {
margin-left: 0.5em;
}
a.permalink:hover {
color: lightgray;
}
p {
margin: 0.5ex 0;
}
p.fake-li {
margin: 0;
}
p.fake-li::before {
content: "\2022";
margin: 1ex;
}
.linkout::before {
content: "Link: ";
}
p.todo-note {
font-style: italic;
color: lightgray;
}
.obfuscated {
filter: blur(1em);
}
.spoilered {
filter: blur(1ex);
-moz-transition: filter 0.04s linear;
}
.spoilered:hover {
filter: blur(0.5ex);
}
.spoilered.unspoilered {
filter: blur(0);
}
canvas.spell-viz {
--dot-color: #777f;
--dot-color: #777f;
--start-dot-color: #f009;
--moving-dot-color: #0fa9;
--path-color: darkgray;
--visited-path-color: #0c8;
--dot-scale: 0.0625;
--moving-dot-scale: 0.125;
--line-scale: 0.08333;
--pausetext-scale: 0.5;
--dark-mode: 0;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #201a20;
color: #ddd;
}
.jumbotron {
background-color: #323;
}
canvas.spell-viz {
/* hack */
--dark-mode: 1;
}
}
</style>
<noscript>
<style>
/* for accessibility */
.spoilered {
filter: none !important;
}
</style>
</noscript>
<script>
"use strict";
const speeds = [0, 0.25, 0.5, 1, 2, 4];
const scrollThreshold = 100;
const rfaQueue = [];
const colorCache = new Map();
function getColorRGB(ctx, str) {
if (!colorCache.has(str)) {
ctx.fillStyle = str;
ctx.clearRect(0, 0, 1, 1);
ctx.fillRect(0, 0, 1, 1);
const imgData = ctx.getImageData(0, 0, 1, 1);
colorCache.set(str, imgData.data);
}
return colorCache.get(str);
}
function startAngle(str) {
switch (str) {
case "east": return 0;
case "north_east": return 1;
case "north_west": return 2;
case "west": return 3;
case "south_west": return 4;
case "south_east": return 5;
default: return 0;
}
}
function offsetAngle(str) {
switch (str) {
case "w": return 0;
case "q": return 1;
case "a": return 2;
case "s": return 3;
case "d": return 4;
case "e": return 5;
default: return -1;
}
}
function initializeElem(canvas) {
const str = canvas.dataset.string;
let angle = startAngle(canvas.dataset.start);
const perWorld = canvas.dataset.perWorld === "True";
// build geometry
const points = [[0, 0]];
let lastPoint = points[0];
let minPoint = lastPoint, maxPoint = lastPoint;
for (const ch of "w" + str) {
const addAngle = offsetAngle(ch);
if (addAngle < 0) continue;
angle = (angle + addAngle) % 6;
const trueAngle = Math.PI / 3 * angle;
const [lx, ly] = lastPoint;
const newPoint = [lx + Math.cos(trueAngle), ly - Math.sin(trueAngle)];
points.push(newPoint);
lastPoint = newPoint;
const [mix, miy] = minPoint;
minPoint = [Math.min(mix, newPoint[0]), Math.min(miy, newPoint[1])];
const [max, may] = maxPoint;
maxPoint = [Math.max(max, newPoint[0]), Math.max(may, newPoint[1])];
}
const size = Math.min(canvas.width, canvas.height) * 0.8;
const scale = size / Math.max(3, Math.max(maxPoint[1] - minPoint[1], maxPoint[0] - minPoint[0]));
const center = [(minPoint[0] + maxPoint[0]) * 0.5, (minPoint[1] + maxPoint[1]) * 0.5];
const truePoints = points.map(p => [canvas.width * 0.5 + scale * (p[0] - center[0]), canvas.height * 0.5 + scale * (p[1] - center[1])]);
let uniqPoints = [];
l1: for (const point of truePoints) {
for (const pt of uniqPoints) {
if (Math.abs(point[0] - pt[0]) < 0.00001 && Math.abs(point[1] - pt[1]) < 0.00001) {
continue l1;
}
}
uniqPoints.push(point);
}
// rendering code
const speed = 0.0025;
const context = canvas.getContext("2d");
const negaProgress = -3;
let progress = 0;
let scrollTimeout = 1e309;
let speedLevel = 3;
let speedIncrement = 0;
function speedScale() {
return speeds[speedLevel];
}
const style = getComputedStyle(canvas);
const getProp = n => style.getPropertyValue(n);
const tick = dt => {
scrollTimeout += dt;
if (canvas.offsetParent === null) return;
const strokeStyle = getProp("--path-color");
const strokeVisitedStyle = getProp("--visited-path-color");
const startDotStyle = getProp("--start-dot-color");
const dotStyle = getProp("--dot-color");
const movDotStyle = getProp("--moving-dot-color");
const strokeWidth = scale * +getProp("--line-scale");
const dotRadius = scale * +getProp("--dot-scale");
const movDotRadius = scale * +getProp("--moving-dot-scale");
const pauseScale = scale * +getProp("--pausetext-scale");
const bodyBg = scale * +getProp("--pausetext-scale");
const darkMode = +getProp("--dark-mode");
const bgColors = getColorRGB(context, getComputedStyle(document.body).backgroundColor);
if (!perWorld) {
progress += speed * dt * (progress > 0 ? speedScale() : Math.sqrt(speedScale()));
}
if (progress >= truePoints.length - 1) {
progress = negaProgress;
}
let ix = Math.floor(progress), frac = progress - ix, core = null, fadeColor = 0;
if (ix < 0) {
const rawFade = 2 * progress / negaProgress - 1;
fadeColor = 1 - Math.abs(rawFade);
context.strokeStyle = rawFade > 0 ? strokeVisitedStyle : strokeStyle;
ix = rawFade > 0 ? truePoints.length - 2 : 0;
frac = +(rawFade > 0);
} else {
context.strokeStyle = strokeVisitedStyle;
}
const [lx, ly] = truePoints[ix];
const [rx, ry] = truePoints[ix + 1];
core = [lx + (rx - lx) * frac, ly + (ry - ly) * frac];
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.lineWidth = strokeWidth;
context.moveTo(truePoints[0][0], truePoints[0][1]);
for (let i = 1; i < ix + 1; i++) {
context.lineTo(truePoints[i][0], truePoints[i][1]);
}
context.lineTo(core[0], core[1]);
context.stroke();
context.beginPath();
context.strokeStyle = strokeStyle;
context.moveTo(core[0], core[1]);
for (let i = ix + 1; i < truePoints.length; i++) {
context.lineTo(truePoints[i][0], truePoints[i][1]);
}
context.stroke();
for (let i = 0; i < uniqPoints.length; i++) {
context.beginPath();
context.fillStyle = (i == 0 && !perWorld) ? startDotStyle : dotStyle;
const radius = (i == 0 && !perWorld) ? movDotRadius : dotRadius;
context.arc(uniqPoints[i][0], uniqPoints[i][1], radius, 0, 2 * Math.PI);
context.fill();
}
if (!perWorld) {
context.beginPath();
context.fillStyle = movDotStyle;
context.arc(core[0], core[1], movDotRadius, 0, 2 * Math.PI);
context.fill();
}
if (fadeColor) {
context.fillStyle = `rgba(${bgColors[0]}, ${bgColors[1]}, ${bgColors[2]}, ${fadeColor})`;
context.fillRect(0, 0, canvas.width, canvas.height);
}
if (scrollTimeout <= 2000) {
context.fillStyle = `rgba(200, 200, 200, ${(2000 - scrollTimeout) / 1000})`;
context.font = `${pauseScale}px sans-serif`;
context.fillText(speedScale() ? speedScale() + "x" : "Paused", 0.2 * scale, canvas.height - 0.2 * scale);
}
};
rfaQueue.push(tick);
// scrolling input
if (!perWorld) {
canvas.addEventListener("wheel", ev => {
speedIncrement += ev.deltaY;
const oldSpeedLevel = speedLevel;
if (speedIncrement >= scrollThreshold) {
speedLevel--;
} else if (speedIncrement <= -scrollThreshold) {
speedLevel++;
}
if (oldSpeedLevel != speedLevel) {
speedIncrement = 0;
speedLevel = Math.max(0, Math.min(speeds.length - 1, speedLevel));
scrollTimeout = 0;
}
ev.preventDefault();
});
}
}
function hookLoad(elem) {
let init = false;
const canvases = elem.querySelectorAll("canvas");
elem.addEventListener("toggle", () => {
if (!init) {
canvases.forEach(initializeElem);
init = true;
}
});
}
function hookToggle(elem) {
const details = Array.from(document.querySelectorAll("details." + elem.dataset.target));
elem.addEventListener("click", () => {
if (details.some(x => x.open)) {
details.forEach(x => x.open = false);
} else {
details.forEach(x => x.open = true);
}
});
}
const params = new URLSearchParams(document.location.search);
function hookSpoiler(elem) {
if (params.get("nospoiler") !== null) {
elem.classList.add("unspoilered");
} else {
const thunk = ev => {
if (!elem.classList.contains("unspoilered")) {
ev.preventDefault();
ev.stopImmediatePropagation();
elem.classList.add("unspoilered");
}
elem.removeEventListener("click", thunk);
};
elem.addEventListener("click", thunk);
if (elem instanceof HTMLAnchorElement) {
const href = elem.getAttribute("href");
if (href.startsWith("#")) {
elem.addEventListener("click", () => document.getElementById(href.substring(1)).querySelector(".spoilered").classList.add("unspoilered"));
}
}
}
}
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('details.spell-collapsible').forEach(hookLoad);
document.querySelectorAll('a.toggle-link').forEach(hookToggle);
document.querySelectorAll('.spoilered').forEach(hookSpoiler);
function tick(prevTime, time) {
const dt = time - prevTime;
for (const q of rfaQueue) {
q(dt);
}
requestAnimationFrame(t => tick(time, t));
}
requestAnimationFrame(t => tick(t, t));
});
</script>
</head>
<body>
<div class="container" style="margin-top: 3em;">
<blockquote>
<h1>This is the online version of the Hexcasting documentation.</h1>
<p>Embedded images and patterns are included, but not crafting recipes or items. There's an in-game book for
those.</p>
<p>Additionally, this is built from the latest code on GitHub. It may describe <b>newer</b> features that you
may not necessarily have, even on the latest CurseForge version!</p>
<p><b>Entries which are blurred are spoilers</b>. Click to reveal them, but be aware that they may spoil endgame
progression. Alternatively, click <a href="?nospoiler">here</a> to get a version with all spoilers showing.
</p>
</blockquote>
</div>
<div class='container'><header class='jumbotron'><h1 class='book-title'>Hex Notebook</h1><p>I seem to have discovered a new method of magical arts, in which one draws patterns strange and wild onto a hexagonal grid. It fascinates me. I&#x27;ve decided to start a journal of my thoughts and findings.</p><p><a href='https://discord.gg/4xxHGYteWk'>Discord Server Link</a></p></header><nav><h2 id='table-of-contents' class='page-header'>Table of Contents<a href='javascript:void(0)' class='permalink toggle-link small' data-target='toc-category' title='Toggle all'><i class='bi bi-list-nested'></i></a><a href='#table-of-contents' class='permalink small' title='Permalink'><i class='bi bi-link-45deg'></i></a></h2><details class='toc-category'><summary><a href='#basics' class=''>Getting Started</a></summary><ul><li><a href='#basics/media' class=''>Media</a></li><li><a href='#basics/geodes' class=''>Geodes</a></li><li><a href='#basics/couldnt_cast' class='spoilered'>A Frustration</a></li><li><a href='#basics/start_to_see' class='spoilered'>WHAT DID I SEE</a></li></ul></details><details class='toc-category'><summary><a href='#casting' class=''>Hex Casting</a></summary><ul><li><a href='#casting/101' class=''>Hexing 101</a></li><li><a href='#casting/vectors' class=''>A Primer on Vectors</a></li><li><a href='#casting/mishaps' class=''>Mishaps</a></li><li><a href='#casting/stack' class=''>Stacks</a></li><li><a href='#casting/naming' class=''>Naming Actions</a></li><li><a href='#casting/influences' class=''>Influences</a></li><li><a href='#casting/mishaps2' class='spoilered'>Enlightened Mishaps</a></li></ul></details><details class='toc-category'><summary><a href='#items' class=''>Items</a></summary><ul><li><a href='#items/amethyst' class=''>Amethyst</a></li><li><a href='#items/staff' class=''>Staff</a></li><li><a href='#items/lens' class=''>Scrying Lens</a></li><li><a href='#items/focus' class=''>Focus</a></li><li><a href='#items/abacus' class=''>Abacus</a></li><li><a href='#items/spellbook' class=''>Spellbook</a></li><li><a href='#items/scroll' class=''>Scrolls</a></li><li><a href='#items/slate' class=''>Slates</a></li><li><a href='#items/hexcasting' class=''>Casting Items</a></li><li><a href='#items/phials' class=''>Phials of Media</a></li><li><a href='#items/pigments' class=''>Pigments</a></li><li><a href='#items/edified' class=''>Edified Trees</a></li><li><a href='#items/jeweler_hammer' class=''>Jeweler&#x27;s Hammer</a></li><li><a href='#items/decoration' class=''>Decorative Blocks</a></li></ul></details><details class='toc-category'><summary><a href='#greatwork' class='spoilered'>The Great Work</a></summary><ul><li><a href='#greatwork/the_work' class='spoilered'>The Work</a></li><li><a href='#greatwork/brainsweeping' class='spoilered'>On the Flaying of Minds</a></li><li><a href='#greatwork/spellcircles' class='spoilered'>Spell Circles</a></li><li><a href='#greatwork/impetus' class='spoilered'>Impetuses</a></li><li><a href='#greatwork/directrix' class='spoilered'>Directrices</a></li><li><a href='#greatwork/akashiclib' class='spoilered'>Akashic Libraries</a></li></ul></details><details class='toc-category'><summary><a href='#lore' class=''>Lore</a></summary><ul><li><a href='#lore/terabithia1' class=''>Cardamom Steles, #1</a></li><li><a href='#lore/terabithia2' class=''>Cardamom Steles, #2</a></li><li><a href='#lore/terabithia3' class=''>Cardamom Steles, #3, 1/2</a></li><li><a href='#lore/terabithia4' class=''>Cardamom Steles, #3, 2/2</a></li><li><a href='#lore/terabithia5' class=''>Cardamom Steles, #4</a></li><li><a href='#lore/experiment1' class=''>Wooleye Instance Notes</a></li><li><a href='#lore/experiment2' class=''>Wooleye Interview Logs</a></li><li><a href='#lore/inventory' class=''>Restoration Log #72</a></li></ul></details><details class='toc-category'><summary><a href='#interop' class=''>Cross-Mod Compatibility</a></summary><ul><li><a href='#interop/interop' class=''>Cross-Mod Compatibility</a></li><li><a href='#interop/gravity' class=''>Gravity Changer</a></li><li><a href='#interop/pehkui' class=''>Pehkui</a></li></ul>
</body>
</html>
''',
)
# ---