WritingTeam Achievement Plan Generator

Writing Plan generator compatible with new grouped achievements.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WritingTeam Achievement Plan Generator
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  Writing Plan generator compatible with new grouped achievements.
// @author       PS2Hagrid / Player1041
// @match        https://retroachievements.org/game2/*
// @match        https://retroachievements.org/game/*
// @icon         https://static.retroachievements.org/assets/images/favicon.webp
// @grant        GM_xmlhttpRequest
// @connect      script.googleusercontent.com
// @connect      script.google.com
// ==/UserScript==

(function () {
    'use strict';

    let hasGeneratedSheet = false;
    let extractedData = null;
    let sheetURL = null;

    // changing this allows you to edit how the sheet title is formatted
    // by default, it is 1, so (Game System) Game Title [Game ID]
    // you can change it to:
    // 1 - Spyro the Dragon (PlayStation) [11279]
    // 2 - (PlayStation) Spyro the Dragon [11279]
    // 3 - Spyro the Dragon [11279] (PlayStation)
    // 4 - [11279] Spyro the Dragon (PlayStation)
    // 5 - [11279] (PlayStation) Spyro the Dragon
    // 6 - (PlayStation) [11279] Spyro the Dragon
    let titleOrder = parseInt(localStorage.getItem("titleOrder") || "1", 10);

    const titleAndID = document.querySelector("#app > div > main > article > div > div.flex.flex-col.gap-3 > div.flex.gap-4.sm\\:gap-6 > div > div.flex.flex-col.gap-1.sm\\:gap-0\\.5");

    const gameTitle = titleAndID?.querySelector('h1 > span')?.textContent.trim();
    const gameSystem = titleAndID?.querySelector('span.flex.items-center > span')?.textContent.trim();
    const gameId = window.location.pathname.split('/').pop();
    const appsScriptUrl = 'https://script.google.com/macros/s/AKfycbwzQCW8p467nvosPhe9f7WvRpExy3LmmXEaj-yorqc80sUhFmOYcbmjR-OOlVGdUB9AaQ/exec';

    function extractData() {
        const achievements = [];
        let totalCheevos = 0;
        let headerCount = 0

        const cheevoContainer = document.querySelector(
            "#game-achievement-sets-container > div > div.relative"
        );

        const groups = cheevoContainer.querySelectorAll("ul");
        console.log(groups.length);

        if (groups.length > 1) {
            // group list
            const groupLis = document.querySelectorAll(
                "#game-achievement-sets-container > div > div.relative > ul > li"
            );

            groupLis.forEach(groupLi => {
                //  group title
                const groupSpan = groupLi.querySelector("button > span");
                let groupTitle = '';
                if (groupSpan) {
                    const text = groupSpan.textContent.trim();
                    const match = text.match(/^(.*?)(\s*\(\d+\s+achievements?\))$/);
                    groupTitle = match ? match[1].trim() : text;
                }

                // add group title
                achievements.push({
                    title: groupTitle,
                    description: '',
                    link: '',
                    id: ''
                });

                // achievements
                const achLis = groupLi.querySelectorAll("li.game-set-item");
                achLis.forEach(li => {
                    const linkEl = li.querySelector(
                        "div.grid div.md\\:col-span-4 span.mr-2 > a[href*='/achievement/']"
                    );
                    const descEl = li.querySelector(
                        "div.grid div.md\\:col-span-4 > p"
                    );
                    if (!linkEl || !descEl) return;

                    const id = linkEl.href.split("/").pop();

                    achievements.push({
                        title: linkEl.textContent.trim(),
                        description: descEl.textContent.trim(),
                        link: linkEl.href,
                        id
                    });
                });

                // spacer row
                achievements.push({
                    title: '---',
                    description: '---',
                    link: '---',
                    id: '---'
                });
            });

        } else {
            // no groups
            const titleAndID = document.querySelector("#app > div > main > article > div > div.flex.flex-col.gap-3 > div.flex.gap-4.sm\\:gap-6 > div > div.flex.flex-col.gap-1.sm\\:gap-0\\.5");
            const gameTitle = titleAndID?.querySelector('h1 > span')?.textContent.trim();
            const gameSystem = titleAndID?.querySelector('span.flex.items-center > span')?.textContent.trim();
            const gameId = window.location.pathname.split('/').pop();

            const listItems = document.querySelectorAll("#game-achievement-sets-container > div > div.relative > ul > li");

            listItems.forEach(li => {
                const linkElement = li.querySelector("div.grid.w-full.gap-x-5.gap-y-1\\.5.leading-4.md\\:grid-cols-6 > div.md\\:col-span-4 > div.mb-0\\.5.flex.justify-between.gap-x-2 > div.-mt-1.mb-0\\.5.md\\:mt-0 > span.mr-2 > a");
                const descElement = li.querySelector("div.grid.w-full.gap-x-5.gap-y-1\\.5.leading-4.md\\:grid-cols-6 > div.md\\:col-span-4 > p");

                if (linkElement && descElement) {
                    const title = linkElement.textContent.trim();
                    const link = linkElement.href;
                    const description = descElement.textContent.trim();
                    const id = link.split('/').pop();
                    totalCheevos++;

                    achievements.push({ title, description, link, id });
                }
            });
        }

        console.log({
            gameTitle,
            gameId,
            gameSystem,
            achievements,
            titleOrder,
        });

        return {
            gameTitle,
            gameId,
            gameSystem,
            achievements,
            titleOrder,
        };
    }
    function insertButton() {
        const targetDiv = document.querySelector(
            "#app > div > main > article > div > div.flex.flex-col.gap-3 > div.flex.gap-4.sm\\:gap-6 > div > div.hidden.flex-wrap.gap-x-2.gap-y-1.text-neutral-300.light\\:text-neutral-700.sm\\:flex > div"
        );

        if (!targetDiv || document.getElementById("writing-generator")) return;

        // Create button
        const button = document.createElement("button");
        button.id = "writing-generator";
        button.className = "btn-base btn-base--default btn-base--size-sm flex items-center gap-1.5 rounded-full !py-0 !text-xs";
        button.type = "button";
        button.setAttribute("aria-haspopup", "dialog");
        button.setAttribute("aria-expanded", "false");
        button.setAttribute("aria-controls", "radix-r3");
        button.setAttribute("data-state", "closed");

        // SVG icon
        const svgNS = "http://www.w3.org/2000/svg";
        const svg = document.createElementNS(svgNS, "svg");

        svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
        svg.setAttribute("width", "16");
        svg.setAttribute("height", "16");
        svg.setAttribute("viewBox", "0 0 24 24");
        svg.setAttribute("fill", "none");
        svg.setAttribute("stroke", "currentColor");
        svg.setAttribute("stroke-width", "2");
        svg.setAttribute("stroke-linecap", "round");
        svg.setAttribute("stroke-linejoin", "round");
        svg.classList.add("lucide", "lucide-pen-line-icon", "lucide-pen-line");

        const path1 = document.createElementNS(svgNS, "path");
        path1.setAttribute("d", "M13 21h8");

        const path2 = document.createElementNS(svgNS, "path");
        path2.setAttribute("d", "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z");

        svg.appendChild(path1);
        svg.appendChild(path2);

        // Text span
        const label = document.createElement("span");
        label.className = "button-label";
        label.textContent = "Generate Writing Sheet";

        // Assemble
        button.appendChild(svg);
        button.appendChild(label);

        const dropdownWrapper = document.createElement("div");
        dropdownWrapper.style.position = "relative";
        dropdownWrapper.style.display = "inline-block";

        // Create dropdown button
        const dropdownButton = document.createElement("button");
        dropdownButton.className = "btn-base btn-base--default btn-base--size-sm gap-1 transition-none lg:active:translate-y-0 lg:active:scale-100 w-full sm:w-auto";
        dropdownButton.type = "button";
        const savedTitleOrder = parseInt(localStorage.getItem("titleOrder") || "1", 10);
        dropdownButton.innerHTML = `
    <svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24"
        stroke-linecap="round" stroke-linejoin="round"
        class="size-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
        <path d="m5 12 7-7 7 7"></path>
        <path d="M12 19V5"></path>
    </svg>
    Format: ${savedTitleOrder}
`;

        // Create dropdown menu
        const dropdownMenu = document.createElement("ul");
        dropdownMenu.style.position = "absolute";
        dropdownMenu.style.top = "100%";
        dropdownMenu.style.left = "0";
        dropdownMenu.style.zIndex = "1000";
        dropdownMenu.style.backgroundColor = "#0a0a0a";
        dropdownMenu.style.border = "1px solid #ccc";
        dropdownMenu.style.padding = "5px";
        dropdownMenu.style.display = "none";
        dropdownMenu.style.listStyle = "none";
        dropdownMenu.style.borderRadius = "6px";
        dropdownMenu.style.minWidth = "200px";
        dropdownMenu.style.boxShadow = "0px 2px 6px rgba(0,0,0,0.2)";
        dropdownMenu.style.fontSize = "0.875rem";

        // Define title options
        const titleOptions = [
            `${gameTitle} (${gameSystem}) [${gameId}]`,
            `(${gameSystem}) ${gameTitle} [${gameId}]`,
            `${gameTitle} [${gameId}] (${gameSystem})`,
            `[${gameId}] ${gameTitle} (${gameSystem})`,
            `[${gameId}] (${gameSystem}) ${gameTitle}`,
            `(${gameSystem}) [${gameId}] ${gameTitle}`
        ];

        // Add menu items
        titleOptions.forEach((text, index) => {
            const li = document.createElement("li");
            li.textContent = text;
            li.style.padding = "5px 10px";
            li.style.cursor = "pointer";

            li.addEventListener("click", () => {
                localStorage.setItem("titleOrder", index + 1);
                dropdownButton.innerHTML = `<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24"
                    stroke-linecap="round" stroke-linejoin="round"
                    class="size-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
                    <path d="m5 12 7-7 7 7"></path>
                    <path d="M12 19V5"></path>
                </svg>Format: ${index + 1}`;
                dropdownMenu.style.display = "none";
            });
            li.addEventListener("mouseover", function () {
                this.style.backgroundColor = "#1a1a1a";
            });
            li.addEventListener("mouseout", function () {
                this.style.backgroundColor = "#0a0a0a";
            });

            dropdownMenu.appendChild(li);
        });

        // Show/hide menu on button click
        dropdownButton.addEventListener("click", () => {
            dropdownMenu.style.display = dropdownMenu.style.display === "none" ? "block" : "none";
        });

        // Append dropdown
        dropdownWrapper.appendChild(dropdownButton);
        dropdownWrapper.appendChild(dropdownMenu);

        targetDiv.appendChild(button);
        targetDiv.appendChild(dropdownWrapper);


        // Click handler
        button.addEventListener("click", () => {
            if (!hasGeneratedSheet) {
                label.textContent = "Gathering Data...";
                const data = extractData();

                label.textContent = "Uploading Data...";
                GM_xmlhttpRequest({
                    method: "POST",
                    url: appsScriptUrl,
                    data: JSON.stringify(data),
                    headers: {
                        "Content-Type": "application/json"
                    },
                    onload: function (response) {
                        sheetURL = response.responseText.trim();
                        console.log("✅ Sheet Created:", sheetURL);
                        label.textContent = "Open Writing Plan";
                        hasGeneratedSheet = true;
                    },
                    onerror: function (error) {
                        console.error("❌ Error sending data:", error);
                        label.textContent = "Error: check console and send to PS2Hagrid";
                    }
                });
            } else if (sheetURL) {
                window.open(sheetURL, "_blank");
            }
        });
    }

    // Wait for load to allow elements to render
    window.addEventListener('load', () => {
        setTimeout(() => {
            insertButton();
        }, 1000);
    });

    console.log("✅ Userscript is running");
})();