BotC homebrew script Prettyprinter

improves the print of scripts with much content

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         BotC homebrew script Prettyprinter
// @namespace    http://tampermonkey.net/
// @version      2025-12-06
// @description  improves the print of scripts with much content
// @author       You
// @match        https://script.bloodontheclocktower.com/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bloodontheclocktower.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    class CustomScript
    {
        static format(scriptName)
        {
            let alphanumName = scriptName.replace(/[^\w\d]/g, '');
            CustomScript[alphanumName]?.();
        }

        // example for script-specific custom settings. Use your script name without non-alphanumeric symbols as function name.
        static TrustMeBro()
        {
            let demonSectionHeader = document.querySelector(`h3[data-for="demon"]`);
            demonSectionHeader.style.transform = "translateY(1lh)";

            let fabledSectionBelowDjinn = document.querySelector(`div.jinxes-container + div.item`);
            fabledSectionBelowDjinn.style.marginTop = "3.5lh";  // add a margin between the jinxes list and the fabled on the next page
            let jinxesEntryContainer = document.querySelector(`div.jinxes-container .jinxes`);
            jinxesEntryContainer.style.rowGap = "0"; // squeezing too many Jinxes onto the 2nd page

            let fabledLoricContainer = document.querySelector(".fabled-and-loric-container");
            fabledLoricContainer.style.transform = "translateY(-2lh)";

            almanachLink.value = `https://www.bloodstar.xyz/p/Elmar/trust-me-bro/almanac.html`;

            homebrewJinxesInput.value =
`  Legionatic / Anarchist: The Anarchist *might* register as evil to Legion (instead of must).
  Legionatic / Spy: If a Legionatic has a drunk spy ability, they see no Legionatic characters in the grimoire but the Legionatics' drunk abilities plus 1 demon.
  Spy / Poppy Snitch: If the Poppy Snitch has their ability, the Spy does not see the Grimoire.
  Spy / Wicked: Both cannot be in play together.
  Soulmate: The Soulmate ability is drunk for players who learnt a current demon (e.g. via minion info).
  Atheist: In an atheist game, the story teller manipulates ≤ 1 person's abilities, misregisters ≤ X players as evil minions/demons, fakes minion signals & has 1 demon ability. [X = regular # of evil]
  Alsaahir: The Alsaahir's guess applies to the actual character type, else (if incorrect) to the misregistered one.
  Alsaahir / Legionatic: Alsaahir may guess the Townsfolk & Outsider players instead.
  Puzzlemaster / Legionatic: The Puzzledrunk is not a Legion(atic). When the sober+healthy puzzlemaster guesses right, they learn a 3rd non-Legion player.
  Puzzlemaster: The Puzzlemaster bypasses misregistration. If the Puzzlemaster guesses themself, they learn *another* non-demon player.
  Plotter: Outsiders/Minions who don't know their character type cannot be in play with the Plotter. [Lunatic, Puzzledrunk]
  Plotter / Wicked: If Plotter is in play, the Wicked gets equal to a Heretic.
  Diabolus / Sage: Each night*, the Leviathan chooses an alive good player (different to previous nights): a chosen Sage uses their ability but does not die.
  Polymath / Lunatic: If the Polymath is a Lunatic, the demon and Polymath don't know who is demon/Polymath.
  Trustmaker / Soulmate: A soulmate vessel nevertheless triggers the Trustmaker reward on death.`;

            let playerCountTableContainer = document.getElementById(`player-count-table`);
            playerCountTableContainer.style.width = "100%";
            playerCountTableContainer.innerHTML = `
  <table rules="cols">
    <tbody><tr>
      <th data-tl-key="keyword.player" data-tl-plural="">Players</th>
      <td>5</td><td>6</td><td>7</td><td>8</td><td>9</td><td>10</td><td>11</td><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td><td>19</td><td>20+</td>
    </tr>
    <tr>
      <th data-tl-key="keyword.townsfolk" data-tl-plural="">Townsfolk</th>
      <td>2</td><td>3</td><td>4</td><td>5</td><td>5</td><td>5</td><td>6</td><td>7</td><td>7</td><td>8</td><td>9</td><td>9</td><td>10</td><td>11</td><td>12</td><td>13</td>
    </tr>
    <tr>
      <th data-tl-key="keyword.outsider" data-tl-plural="">Outsiders</th>
      <td>1</td><td>1</td><td>1</td><td>1</td><td>2</td><td>2</td><td>2</td><td>2</td><td>2</td><td>3</td><td>3</td><td>3</td><td>3</td><td>4</td><td>4</td><td>4</td>
    </tr>
    <tr>
      <th data-tl-key="keyword.minion" data-tl-plural="">Minions</th>
      <td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>2</td><td>2</td><td>2</td><td>3</td><td>3</td><td>3</td><td>4</td><td>4</td><td>2</td><td>2</td><td>3</td>
    </tr>
    <tr>
      <th data-tl-key="keyword.demon" data-tl-plural="">Demons</th>
      <td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>1</td><td>2</td><td>2</td><td>2</td>
    </tr>
  </tbody></table>
`;
        }
    }

    console.log("start prettyprint BotC script");

    let form = document.createElement("form");
    form.innerHTML = `
    <label>Almanach-URL: </label>
    <input id="almanach-url-input" name="almanach-url" placeholder="&lt;URL&gt;" autocomplete="off" size="100">
    <label>Homebrew Jinxes: (Format: <code>‹character›/‹character›…: ‹description›</code>)</label>
    <textarea id="homebrew-jinxes-input" cols="100" oninput="event.target.style.height = '5px'; event.target.style.height = event.target.scrollHeight + 'px'"></textarea>
    `;

    let metadataForm = document.getElementById("metadata");
    metadataForm.prepend(form);

    var almanachLink = document.getElementById(`almanach-url-input`);
    var homebrewJinxesInput = document.getElementById(`homebrew-jinxes-input`);

    function formatPageForPrint()
    {
        let scriptName = document.getElementById("script-name-input").value;
        CustomScript.format(scriptName);

        let scriptNameElement = document.querySelector("#title .script-name");
        let link = document.createElement("a");
        link.textContent = scriptNameElement.textContent;
        link.href = almanachLink.value;
        scriptNameElement.replaceChildren(link);

        let playerCountTable = document.querySelector("page-two #player-count-table");
        if (!playerCountTable)
            return;

        playerCountTable.getElementsByTagName("table")[0].setAttribute("rules", "cols");
        Array.from(playerCountTable.getElementsByTagName("th")).forEach(th => { th.style.paddingRight = ".5em" });
        Array.from(playerCountTable.getElementsByTagName("td")).forEach(td => { td.style.minWidth = "1.2em" });

        let travellersSection = document.querySelector("div.recommended-travellers-container");
        //let playerCountTable2 = document.querySelector("body > #player-count-table");
        let roster = (document.querySelector("page-one"));

        roster?.append(travellersSection, /*playerCountTable*/);

        let table = playerCountTable.querySelector("table");
        table.style.display = "block";
        table.style.width = "100%";

        let jinxesContainer = document.querySelector(".jinxes-container");
        jinxesContainer.style.width = "100%";
        jinxesContainer.style.justifyContent = "start";
        addHomebrewJinxes(jinxesContainer);

        let bootleggerContainer = document.querySelector(".bootlegger-rules-container");
        bootleggerContainer.style.width = "100%";
        bootleggerContainer.style.justifyContent = "start";

        for (const node of document.querySelectorAll(".script-container .bootlegger-rules .rule"))
        {
            node.style.maxWidth = "none";
            let bulletPoint = node.parentElement.querySelector(".fa-circle")
            bulletPoint.style.scale = "1.0";
            bulletPoint.style.transform = "translateY(1em) scale(0.5)";
        }

        let page2 = (document.querySelector("page-two"));
        if (page2) page2.style.breakBefore = "avoid";
        if (page2) page2.style.breakAfter = "page";
        let page3 = (document.querySelector("page-three"));
        let page4 = (document.querySelector("page-four"));
        if (page4) page4.style.breakBefore = "avoid";
    }

    let printButton = document.querySelector("#print-form button");
    printButton.addEventListener("click", formatPageForPrint);

    function addHomebrewJinxes(jinxesContainer)
    {
        let tableString = document.getElementById(`homebrew-jinxes-input`)?.value;
        let entries = tableString.split("\n").filter(s => Boolean(s.trim())).map(createJinxEntry).filter(entry => entry !== null);

        let jinxesEntryContainer = jinxesContainer.querySelector(`.jinxes`);

        entries.forEach(([characterNames, entry]) => {
            let lastEntryOfCharacter = jinxesEntryContainer.querySelector(`:nth-last-child(n of .item:has(.icons img[id^="${characterNames[0]}"]:first-child))`);

            if (lastEntryOfCharacter !== null) lastEntryOfCharacter.after(entry);
            else jinxesEntryContainer.append(entry);

            jinxesEntryContainer.setAttribute("nitems", Number(jinxesEntryContainer.getAttribute("nitems")) + 1);
        });
    }

    function createJinxEntry(jinxRowString)
    {
        let [charactersString, ...descriptionString] = jinxRowString.split(":");
        descriptionString = descriptionString.join(":");

        let characterNames = charactersString.split("/").map(n => n.trim().replace(/\s+/g, "").toLowerCase());
        let imageUrls = characterNames.map((name) => [name, getUrlsFromCharacter(name)]);
        let id = `${characterNames.join("-")}-jinx`;

        if (document.getElementById(id) !== null)
            return null;
        //console.log(characterNames, descriptionString);

        let jinxEntry = document.createElement("div");
        jinxEntry.setAttribute("id", id);
        jinxEntry.classList.add("item");
        jinxEntry.innerHTML = `
            <div class="icons">
            ${imageUrls.map(([name, url]) => `
                <img id="${name}-icon-jinxes" class="icon " src="${url}">
            `).join("")}
            </div>
            <div class="jinx-text" style="max-width: none">${descriptionString}</div>
        `;

        addSmallIconToRoster(id, imageUrls, descriptionString);

        return [characterNames, jinxEntry];
    }

    function addSmallIconToRoster(id, nameUrlPairs, descriptionString)
    {
        if (nameUrlPairs.length < 2) return;

        let sourceName = nameUrlPairs[0][0];
        let characterEntry = document.querySelector(`div[id^="${sourceName}"].item`);
        let characterNameElement = characterEntry.querySelector(`.character-name`);
        let characterJinxesTray = characterNameElement.querySelector(`.character-jinxes`);

        if (characterJinxesTray === null)
        {
            characterJinxesTray = document.createElement("div");
            characterJinxesTray.classList.add("character-jinxes");
            characterNameElement.append(characterJinxesTray);
        }

        if (characterEntry.getAttribute("data-type") === "fabled" || characterEntry.getAttribute("data-type") === "loric")
        {
            characterJinxesTray.style.transform = "translateY(-1lh)";
        }

        let icon = document.createElement("img");
        icon.title = descriptionString;
        icon.classList.add("jinx-icon");
        icon.onclick = () => { document.getElementById(`${id}`).scrollIntoView({ behavior: 'smooth', block: 'center' }) };
        icon.src = nameUrlPairs[1][1];

        characterJinxesTray.append(icon);
    }

    function getUrlsFromCharacter(characterString)
    {
        return document.querySelector(`img[id^="${characterString}"][id$="-icon-script"]`).src;
    }
})();