KoL Detective Helper

Helps with solving the detective school puzzles in Kingdom of Loathing

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name           KoL Detective Helper
// @namespace      kol.interface.unfinished
// @description    Helps with solving the detective school puzzles in Kingdom of Loathing
// @include        https://*kingdomofloathing.com/wham.php*
// @grant          GM_setValue
// @grant          GM_getValue
// @version        1.2
// ==/UserScript==

// Version 1.2
// - highlight areas on hover over questions
// Version 1.1
// - include possible responses in lists of people and jobs, so they can be recognized in answers prior to visiting
// Version 1.0.1
// - highlight active row better
// Version 1.0

var titleKey;
var places;
var person;
var people;
var jobs;
var personPlaceJob = {};
var personSays = {};

function getTitle() {
	var t = document.evaluate( '//b[contains(.,"Who killed ")]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
	if (t.singleNodeValue) {
        var title = t.singleNodeValue.innerHTML;
        // console.log('title is '+title);
        titleKey = title;
	    var tt = document.evaluate( '//td[contains(.,"You have been on this case for ")]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
	    if (tt.singleNodeValue) {
            var re = tt.singleNodeValue.innerHTML.match(/You have been on this case for ([0-9][0-9]*) minute/);
            if (re) {
                var mins = re[1];
                // console.log('minutes is '+mins);
                if (mins!=0) 
                    getTruthAndLies();
            }
        }
    }
}

function gatherLocations() {
    var rc = [];
	var locs = document.evaluate( '//a[contains(@href,"wham.php?visit=")]', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
    for (var i=0;i<locs.snapshotLength;i++) {
        var loc = locs.snapshotItem(i);
        var aloc = loc.firstChild.firstChild.innerHTML;
        rc[rc.length] = aloc;
        // console.log('location: '+aloc);
    }
    return rc;
}

function gatherPeople() {
    var rc = [];

    for (var p in personPlaceJob) {
        rc[rc.length] = p;
    }

    return rc;
}

function gatherJobs() {
    var rc = [];

    for (var p in personPlaceJob) {
        rc[rc.length] = personPlaceJob[p].job;
    }

    return rc;
}

function highlightHover(e) {
    var tgt = this.getAttribute('tgt');
    var tgtnode = document.evaluate( '//a[@href="wham.php?visit='+tgt+'"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,null);
    if (tgtnode.singleNodeValue) {
        tgtnode.singleNodeValue.setAttribute('style','color:blue;');
        tgtnode.singleNodeValue.firstChild.setAttribute('size','3');
    }
}

function unhighlightHover(e) {
    var tgt = this.getAttribute('tgt');
    var tgtnode = document.evaluate( '//a[@href="wham.php?visit='+tgt+'"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE,null);
    if (tgtnode.singleNodeValue) {
        tgtnode.singleNodeValue.setAttribute('style','');
        tgtnode.singleNodeValue.firstChild.setAttribute('size','2');
    }
}

function gatherMorePeople(people) {
    var re = /^[A-Z][a-z][A-Za-z]* [A-Z][a-z][A-Za-z]*(\s[A-Z][a-z][A-Za-z]*)?$/;
    var rea = /^wham\.php\?ask=([1-9])/;
    var asks = document.evaluate( '//a[contains(@href,"wham.php?ask=")]', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
    for (var i=0;i<asks.snapshotLength;i++) {
        var a = asks.snapshotItem(i);
        var subj = a.innerHTML;
        if (subj.match(re)) {
            if (people.indexOf(subj)<0) {
                people[people.length] = subj;
            }
            //console.log('Extra person: '+subj);
        }
        var m = a.getAttribute('href').match(rea);
        if (m) {
            a.setAttribute('tgt',m[1]);
            a.addEventListener('mouseover',highlightHover,false);
            a.addEventListener('mouseout',unhighlightHover,false);
        }
    }
}

function gatherMoreJobs(jobs) {
    var re = /^the victim's ([-a-z ]*)$/;
    var rea = /^wham\.php\?ask=rel&w=([1-9])/;
    var asks = document.evaluate( '//a[contains(@href,"wham.php?ask=rel")]', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
    for (var i=0;i<asks.snapshotLength;i++) {
        var a = asks.snapshotItem(i);
        var subj = a.innerHTML;
        var w = subj.match(re);
        if (w) {
            if (jobs.indexOf(w[1])<0) {
                jobs[jobs.length] = w[1];
                //console.log('Extra job: '+w[1]);
            }
        }
        var m = a.getAttribute('href').match(rea);
        if (m) {
            a.setAttribute('tgt',m[1]);
            a.addEventListener('mouseover',highlightHover,false);
            a.addEventListener('mouseout',unhighlightHover,false);
        }
    }
}

// -------------------------------------------------

function gatherPerson() {
	var p = document.evaluate( '//input[@value="Accuse!"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
	if (p.singleNodeValue) {
	    p = document.evaluate( './/b', p.singleNodeValue.parentNode.parentNode, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
        if (p.singleNodeValue) {
            person = p.singleNodeValue.innerHTML;
            var role = String(p.singleNodeValue.nextSibling.nextSibling.nextSibling.nodeValue).trim();
            var loc = String(p.singleNodeValue.nextSibling.nextSibling.nextSibling.nextSibling.innerHTML).trim().replace(")","").replace("(","");
            //console.log(person+', '+role+' is at '+loc);

            var peep = personPlaceJob[person];
            if (peep) {
                peep.job = role;
                peep.place = loc;
            } else {
                personPlaceJob[person] = {
                    job:role,
                    place:loc
                };
            }
            if (people.indexOf(person)<0)
                people[people.length] = person;
            if (jobs.indexOf(role)<0)
                jobs[jobs.length] = role;
            //console.log('Person: '+person);
        } 
    } 
}

function gatherAnswer(person) {
    if (!person) {
        return;
    }
 	var a = document.evaluate( '//td[@colspan="2" and @width="500"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
 	if (a.singleNodeValue) {
        var t = a.singleNodeValue.innerHTML;
        var p,l,j;

        var accused = checkAccused(t);
        //console.log('accused? ' + accused);

        for (var i in people) {
            var re = new RegExp('\\b'+people[i]+'\\b');
            if (re.test(t)) {
                p = people[i];
                //console.log('Talking about person '+p);
            }
        }

        for (var i in places) {
            var re = new RegExp('\\b'+places[i]+'\\b');
            if (re.test(t)) {
                if (!l || l.length<places[i].length) {
                    l = places[i];
                }
                //console.log('Talking about location '+l);
            }
        }

        var ts = t.replace(/I don't know how s*he got stuck with [a-z -]* like me, either\./,'');
        // console.log('looking for jobs');
        for (var i in jobs) {
            var re = new RegExp('\\b'+jobs[i]+'\\b');
            var w = ts.search(re);
            if (w>=3 && ts.substr(w-3,3).toLowerCase()!="ex-") {
                if (!j || j.length<jobs[i].length) {
                    j = jobs[i];
                }
                // console.log('Talking about job '+j);
            }
        }

        var map = personSays[person];
        if (!map) {
            map = new Object();
            personSays[person] = map;
        }
        if (p && l) {
            //console.log(p + ' -> ' + l);
            map[p+'.place'] = l;
        }
        if (p && j) {
            //console.log(p + ' -> ' + j);
            map[p+'.job'] = j;
        }
        if (l && j) {
            //console.log(l + ' -> ' + j);
            map[l] = j;
        }
        if (accused) {
            if (p) map.accused = p;
            else if (j) map.accused = j;
            else map.accused = 'n/a';
        }
    } 
}

function checkAccused(text) {
    var re = /("Do you have any thoughts on who did it\?" you ask.|"Help me out here," you say\. "You know all the people in this house. Who's the most likely killer\?"|"Who do you think is the most likely to be the killer\?" you ask\.|"Who do you think the killer is\?" you ask.|"Does anyone else here seem particularly suspicious to you\?" you ask\.|"If you had to guess," you ask, "who would you say did it\?")/;

    return re.test(text);
}

// -------------------------------------------------

function saveTruthAndLies() {
    GM_setValue('truth',JSON.stringify(personPlaceJob));
    GM_setValue('lies',JSON.stringify(personSays));
    // var i = 1;
    // // console.log('Truth:');
    // for (var p in personPlaceJob) {
    //     console.log(i+'. '+p+': '+personPlaceJob[p].job+', '+personPlaceJob[p].place);
    //     i = i+1;
    // }
    // // console.log('Lies:');
    // i = 1;
    // for (var p in personSays) {
    //     var ps = personSays[p];
    //     var s = '';
    //     for (var rel in ps) {
    //         if (s)
    //             s += ', ';
    //         s += rel+':'+ps[rel];
    //     }
    //     console.log(i+'. '+p+' says '+s);
    //     i = i+1;
    // }
}

function getTruthAndLies() {
    var t = GM_getValue('truth','');
    if (t) {
        personPlaceJob = JSON.parse(t);
    }
    var lies = GM_getValue('lies','');
    if (lies) {
        personSays = JSON.parse(lies);
    }
}

function abbreviateName(name) {
    var re = /^([A-Z])[a-zA-Z]* ([A-Z])[a-zA-Z]*(\s?([A-Z]))?/;
    var m = name.match(re);
    if (m) {
        if (m[4])
            return m[1]+m[2]+m[4];
        return m[1]+m[2];
    }
    return name;
}

function display() {
    var tbl = document.createElement('table');
    tbl.setAttribute('style','width:95%;font-size:9pt;border-collapse:collapse;border:1px solid black;');
    tbl.setAttribute('cellpadding','3');
    tbl.setAttribute('cellspacing','2');

    // headers
    var c,r = tbl.insertRow();
    r.setAttribute('style','border:1px solid black;');
    c = r.insertCell();
    c.appendChild(document.createTextNode('Perp'));

    for (var ppl in people) {
        var name = abbreviateName(people[ppl]);
        c = r.insertCell();
        c.appendChild(document.createTextNode(name));
        // c = r.insertCell();
        // c.appendChild(document.createTextNode(name));
    }
    for (var loc in places) {
        c = r.insertCell();
        c.appendChild(document.createTextNode(places[loc]));
    }
    c = r.insertCell();
    c.appendChild(document.createTextNode('Acc'));

    for (var p in personSays) {
        r = tbl.insertRow();
        var rattrib = "border:1px solid black;";
        
        c = r.insertCell();
        if (p==person) {
            var b = document.createElement('b');
            b.appendChild(document.createTextNode(p));
            c.appendChild(b);
            rattrib = rattrib + "background-color:rgb(250,230,230);";
        } else {
            c.appendChild(document.createTextNode(p));
        }
        r.setAttribute('style',rattrib);
        c.appendChild(document.createElement('br'));
        if (personPlaceJob[p] && personPlaceJob[p].place) {
            var b = document.createElement('div');
            b.setAttribute('style','font-size:smaller;');
            if (personPlaceJob[p].job) {
                b.appendChild(document.createTextNode(personPlaceJob[p].job));
                b.appendChild(document.createElement('br'));
            }
            b.appendChild(document.createTextNode(personPlaceJob[p].place));
            c.appendChild(b);
        }
        
        for (var ppl in people) {
            c = r.insertCell();
            var place = personSays[p][people[ppl]+'.place'];
            if (place) {
                var d = document.createElement('div');
                // check truth
                if (personPlaceJob[people[ppl]] && personPlaceJob[people[ppl]].place) {
                    // we know this association
                    if (personPlaceJob[people[ppl]].place==place) {
                        d.setAttribute('style','color:green;border-bottom:1px solid black;');
                    } else {
                        d.setAttribute('style','color:red;border-bottom:1px solid black;');
                    }
                } else {
                    d.setAttribute('style','color:gray;border-bottom:1px solid black;');
                }
                d.appendChild(document.createTextNode(place));
                c.appendChild(d);
            } else {
                c.appendChild(document.createElement('br'));
            }

            //c = r.insertCell();
            //c.appendChild(document.createElement('br'));
            var job = personSays[p][people[ppl]+'.job'];
            if (job) {
                var d = document.createElement('div');
                // check truth
                if (personPlaceJob[people[ppl]] && personPlaceJob[people[ppl]].job) {
                    if (personPlaceJob[people[ppl]] && personPlaceJob[people[ppl]].job==job) {
                        d.setAttribute('style','color:green;border-top:1px solid black;');
                    } else {
                        d.setAttribute('style','color:red;border-top:1px solid black;');
                    }
                } else {
                    d.setAttribute('style','color:gray;border-top:1px solid black;');
                }
                d.appendChild(document.createTextNode(job));
                c.appendChild(d);
            } else {
                c.appendChild(document.createElement('br'));
            }
        }
        for (var loc in places) {
            c = r.insertCell();
            var job = personSays[p][places[loc]];
            if (job) {
                var d = document.createElement('div');
                d.setAttribute('style','color:gray;');
                // check truth
                for (var others in personPlaceJob) {
                    var other = personPlaceJob[others];
                    if (other.job==job && other.place) {
                        if (other.place==places[loc]) {
                            d.setAttribute('style','color:green;');
                        } else {
                            d.setAttribute('style','color:red;');
                        }
                        break;
                    } else if (other.place==places[loc] && other.job) {
                        if (other.job==job) {
                            d.setAttribute('style','color:green;');
                        } else {
                            d.setAttribute('style','color:red;');
                        }
                        break;
                    }
                }
                d.appendChild(document.createTextNode(job));
                c.appendChild(d);
            }
        }
        c = r.insertCell();
        if (personSays[p].accused) {
            c.appendChild(document.createTextNode(personSays[p].accused));
        }
    }
    document.body.appendChild(tbl);
}
    
// -------------------------------------------------

getTitle();
places = gatherLocations();
people = gatherPeople();
jobs = gatherJobs();
gatherPerson();
gatherMorePeople(people);
gatherMoreJobs(jobs);
gatherAnswer(person);
saveTruthAndLies();
display();