Wolfery Audio Notifier

Plays a sound when a mention occurs, or when you are whispered, directly messaged, or addressed.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Wolfery Audio Notifier
// @name:de      Wolfery Audio Benachrichtigungen
// @namespace    https://forum.wolfery.com/u/felinex/
// @version      1.5
// @description  Plays a sound when a mention occurs, or when you are whispered, directly messaged, or addressed.
// @description:de Spielt einen Sound ab, wenn eine Erwähnung (Mention) auftaucht, man angeflüstert, direkt angeschrieben (message) oder adressiert wird.
// @icon         https://static.f-list.net/images/eicon/wolfery.png
// @license      All Rights Reserved
// @author       Felinex Gloomfort
// @match        https://wolfery.com/*
// @match        https://test.wolfery.com/*
// @match        https://*.mucklet.com/*
// @icon
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    console.log("[AudioNotifier] Script loaded.");

    const SOUND_URL = 'https://actions.google.com/sounds/v1/alarms/beep_short.ogg';
    const audio = new Audio(SOUND_URL);
    audio.volume = 0.5;

    let lastPlay = 0;
    let initAttempts = 0;

    function init() {
        initAttempts++;
        if (typeof window.app === 'undefined') {
            if (initAttempts < 60) {
                setTimeout(init, 500);
            } else {
                console.error("[AudioNotifier] ERROR: window.app was not found.");
            }
            return;
        }

        console.log(`[AudioNotifier] window.app found! Trying to load module 'charLog' (Attempt ${initAttempts})...`);

        // Using getModule to fetch the module once the client initialized it.
        // This avoids the "App.require may only be called from an AppModule's constructor" error.
        const charLog = window.app.getModule('charLog');

        if (!charLog) {
            // Module is not ready yet, keep waiting
            if (initAttempts < 60) {
                setTimeout(init, 500);
            } else {
                console.error("[AudioNotifier] ERROR: 'charLog' module could not be loaded even after 30 seconds.");
            }
            return;
        }

        console.log("[AudioNotifier] Module 'charLog' successfully loaded. Adding EventModifier...");

        try {
            charLog.addEventModifier({
                id: 'userscriptSoundNotifier',
                sortOrder: 100,
                callback: (ev, ctrl, mod) => {

                    console.log(`[AudioNotifier] Event received: ${ev.type}`, { event: ev, mod: mod, ctrlId: ctrl.id });

                    // Exclude muted events and own character's actions
                    if (mod.muted) return;
                    if (ev.char && ev.char.id === ctrl.id) return;

                    // 1. Check for mentions (Set by the system when highlight triggers match)
                    const isMention = !!mod.mentioned;

                    // 2. Target check (Is the character the target of a whisper/message/address?)
                    // We manually check the target attributes of the original event
                    let isTargetedToMe = false;
                    const isDirectType = ['whisper', 'message', 'address'].includes(ev.type);

                    if (isDirectType) {
                        // Either it is explicitly in the mod object (set by previous modifiers)
                        if (mod.targeted) {
                            isTargetedToMe = true;
                        }
                        // Or we look for the character directly in the event targets
                        else if (ev.target && ev.target.id === ctrl.id) {
                            isTargetedToMe = true;
                        }
                        else if (ev.targets && Array.isArray(ev.targets)) {
                            // For group messages (multiple targets)
                            if (ev.targets.some(t => t.id === ctrl.id)) {
                                isTargetedToMe = true;
                            }
                        }
                    }

                    console.log(`[AudioNotifier] Evaluation: Mention=${isMention}, Targeted=${isTargetedToMe}`);

                    // Trigger sound
                    if (isMention || isTargetedToMe) {
                        playSound();
                    }
                }
            });
            console.log("[AudioNotifier] EventModifier successfully registered!");
        } catch (err) {
            console.error("[AudioNotifier] ERROR registering EventModifier:", err);
        }
    }

    function playSound() {
        const now = Date.now();
        if (now - lastPlay < 1000) return; // Spam protection / throttling

        lastPlay = now;

        audio.play().then(() => {
            console.log("[AudioNotifier] Sound played!");
        }).catch(e => {
            console.error("[AudioNotifier] ERROR playing sound. Browser blocking autoplay? Click anywhere on the page.", e);
        });
    }

    // Catch first click to unlock browser autoplay restrictions
    document.addEventListener('click', function unlockAudio() {
        audio.play().then(() => { audio.pause(); audio.currentTime = 0; }).catch(() => {});
        document.removeEventListener('click', unlockAudio);
    }, { once: true });

    // Wait briefly for the DOM loading process to begin before starting
    setTimeout(init, 1000);
})();