본문으로 이동

사용자:Hsl0/연구소/5.js

리버티게임, 모두가 만들어가는 자유로운 게임

참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
(() => {
    const body = document.body;
    const title = document.getElementById('firstHeading');
    const root = document.querySelector(
        '#mw-content-text > .mw-parser-output'
    );
    const toolList = document.querySelector(
        '#p-cactions ul, #p-tb ul'
    );

    if (!title || !root || !toolList) return;
    if (document.getElementById('ca-lg-immersive')) return;

    if (
        root.classList.contains('game-layout') ||
        document.getElementById('lg-menu-button')
    ) {
        console.warn('새로고침 후 기존 JS 대신 실행하세요.');
        return;
    }

    let active = false;
    let cardState = null;
    let controls = null;

    const toolItem = document.createElement('li');
    toolItem.id = 'ca-lg-immersive';

    const modeLink = document.createElement('a');
    modeLink.href = '#';
    modeLink.textContent = '몰입 모드 켜기';

    toolItem.append(modeLink);
    toolList.append(toolItem);

    modeLink.addEventListener('click', event => {
        event.preventDefault();
        body.classList.add('lg-immersive');
    });

    function visibleLinks(element) {
        return [...element.querySelectorAll('a[href]')].filter(link =>
            link.getClientRects().length > 0 &&
            link.textContent.replace(/[\s\u200b]/g, '') !== ''
        );
    }

    function isArrow(link) {
        return [...link.querySelectorAll('span')].some(
            span => span.textContent.trim() === '→'
        );
    }

    function isAction(element) {
        if (element.matches('ul')) {
            const items = [...element.children];

            return items.length > 0 && items.every(item =>
                item.matches('li') &&
                !item.querySelector(
                    'ul, ol, table, input, button, select, textarea'
                ) &&
                visibleLinks(item).length === 1
            );
        }

        if (element.matches('p')) {
            const links = visibleLinks(element);

            return links.length === 1 &&
                isArrow(links[0]) &&
                element.textContent.trim().length < 160 &&
                !element.querySelector(
                    'img, input, button, select, textarea'
                );
        }

        return false;
    }

    function createCards() {
        const blocks = [...root.children].filter(element =>
            !element.matches('style, link, script') &&
            getComputedStyle(element).display !== 'none' &&
            (
                element.textContent.replace(/[\s\u200b]/g, '') !== '' ||
                element.querySelector(
                    'img, input, canvas, iframe, video'
                )
            )
        );

        const actionBlocks = [];

        for (let index = blocks.length - 1; index >= 0; index--) {
            if (!isAction(blocks[index])) break;
            actionBlocks.unshift(blocks[index]);
        }

        if (!actionBlocks.length) return;

        const originalNodes = [...root.childNodes];

        const context = document.createElement('div');
        context.className = 'game-card game-context';

        const actions = document.createElement('div');
        actions.className = 'game-card game-actions';

        for (const node of originalNodes) {
            (actionBlocks.includes(node) ? actions : context).append(node);
        }

        const markedLinks = [];

        for (const link of actions.querySelectorAll('a[href]')) {
            if (isArrow(link) && !link.classList.contains('game-button')) {
                link.classList.add('game-button');
                markedLinks.push(link);
            }
        }

        root.append(context, actions);
        root.classList.add('game-layout');

        cardState = { originalNodes, context, actions, markedLinks };
    }

    function restoreCards() {
        if (!cardState) return;

        const { originalNodes, context, actions, markedLinks } = cardState;

        for (const link of markedLinks) {
            link.classList.remove('game-button');
        }

        root.append(...originalNodes);
        context.remove();
        actions.remove();
        root.classList.remove('game-layout');

        cardState = null;
    }

    const icons = {
        menu: '<path d="M5 7h14M5 12h14M5 17h14"/>',
        settings: `
            <circle cx="12" cy="12" r="7"/>
            <circle cx="12" cy="12" r="3"/>
            <path d="
                M12 2v3 M12 19v3
                M2 12h3 M19 12h3
                M5 5l2 2 M17 17l2 2
                M5 19l2-2 M17 7l2-2
            "/>
        `,
        info: `
            <circle cx="12" cy="12" r="9"/>
            <path d="M12 11v6M12 7v.1"/>
        `
    };

    function makeButton(icon, label) {
        const button = document.createElement('button');
        button.type = 'button';
        button.className = 'lg-icon-button';
        button.title = label;
        button.setAttribute('aria-label', label);

        button.innerHTML = `
            <svg
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="1.7"
                stroke-linecap="round"
                stroke-linejoin="round"
                aria-hidden="true"
            >${icons[icon]}</svg>
        `;

        return button;
    }

    function createControls() {
        const menuButton = makeButton('menu', '리버티게임 메뉴');
        menuButton.id = 'lg-menu-button';
        menuButton.classList.add('lg-menu-button', 'lg-controls');

        const infoButton = makeButton('info', '게임 정보 및 문서 도구');

        // 각 버튼은 몰입 모드 클래스만 삭제합니다.
        menuButton.addEventListener('click', () => {
            body.classList.remove('lg-immersive');
        });

        infoButton.addEventListener('click', () => {
            body.classList.remove('lg-immersive');
        });

        const settingsButton = makeButton('settings', '설정 (시각적 시안)');
        settingsButton.disabled = true;

        const tools = document.createElement('div');
        tools.className = 'lg-tools lg-controls';
        tools.append(settingsButton, infoButton);

        title.before(menuButton);
        title.after(tools);

        controls = { menuButton, tools };
    }

    function removeControls() {
        if (!controls) return;

        controls.menuButton.remove();
        controls.tools.remove();
        controls = null;
    }

    function syncMode() {
        const enabled = body.classList.contains('lg-immersive');
        if (enabled === active) return;

        active = enabled;

        if (enabled) {
            createCards();
            createControls();
        } else {
            removeControls();
            restoreCards();
        }
    }

    new MutationObserver(syncMode).observe(body, {
        attributes: true,
        attributeFilter: ['class']
    });

    syncMode();
})();