// ==UserScript==
// @name R00t Success Chance
// @description Calculates success chance for r00t field actions.
// @namespace http://userscripts.org/users/dtkarlsson
// @include http*://*animecubed.com/billy/bvs/villagefields*
// @include http*://*animecubedgaming.com/billy/bvs/villagefields*
// @licence MIT; http://www.opensource.org/licenses/mit-license.php
// @copyright 2009, Daniel Karlsson
// @version 0.12.0
// @history 0.12.0 New domain - animecubedgaming.com - Channel28
// @history 0.11.0 Now https compatible (Updated by Channel28)
// @history 0.10.0 Added rough estimate of max difficulty
// @history 0.9.1 Text was supposed to be white, now it is
// @grant none
// ==/UserScript==
var jutsuTypes = {
gen: "Genjutsu",
nin: "Ninjutsu",
tai: "Taijutsu",
dou: "Doujutsu"
}
var basicJutsuTypes = {
gen: "Genjutsu",
nin: "Ninjutsu",
tai: "Taijutsu"
}
function Ability()
{
this.lvl = 0;
this.str = 0;
this.rng = 0;
this.suc = 0;
}
function Ninja()
{
this.gen = new Ability();
this.nin = new Ability();
this.tai = new Ability();
this.dou = new Ability();
}
function Challenge()
{
this.dif = 0;
this.suc = 0;
}
function Mission()
{
this.gen = new Challenge();
this.nin = new Challenge();
this.tai = new Challenge();
this.dou = new Challenge();
this.order = [];
this.crank = 0;
}
// Math stuff below
function binomialCoefficient(n, k)
{
// n!/[k!(n-k)!]
if (k > n || k < 0)
return 0;
k = Math.max(k, n - k);
var i = 1;
var j = k + 1;
var c = 1;
// i = 1 ... n-k => (n-k)!
// j = k+1 ... n => n!/k!
while (j <= n)
c *= j++ / i++;
return c;
}
function binomdist(k, n, p, cumulative)
{
// Cumulative distribution, k or less successes in n trials
if (cumulative) {
var sum = 0;
for (var i = 0; i <= k; i++)
sum += binomdist(i, n, p, false);
return sum;
}
// Exactly k successes in n trials with probability p
return binomialCoefficient(n, k) * Math.pow(p, k) * Math.pow(1 - p, n - k);
}
function successChance(challenge, ability, crank)
{
if (!crank)
crank = 0;
// Successful rolls required
var req = (challenge.suc + crank)- ability.suc;
if (req > ability.lvl)
return 0;
else if (req <= 0)
return 1;
if ((challenge.dif + crank) > ability.rng)
return 0;
// Success probability per roll
var prob = (ability.str + ability.rng - (challenge.dif + crank) + 1) / ability.rng;
if (prob >= 1)
return 1;
return Math.min(0.9999, 1 - binomdist(req - 1, ability.lvl, prob, true));
}
function expectedSuccesses(challenge, ability, crank)
{
if (!crank)
crank = 0;
if ((challenge.dif + crank) > ability.rng)
return 0;
// Success probability per roll
var prob = (ability.str + ability.rng - (challenge.dif + crank) + 1) / ability.rng;
return ability.suc + Math.min(1, prob) * ability.lvl;
}
function percent(n)
{
if (n == 1.0)
return "100%";
else if (n == 0.0)
return "0%";
var p = Math.round(n * 1000) / 10;
if (p > 99.9)
return ">99.9%";
else if (p < 0.1)
return "<0.1%";
return p + "%";
}
// Parsing
function parseAbilities()
{
var ninja = new Ninja();
var tables = document.evaluate("//table[count(descendant::table)=4]//table",
document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for (var i = 0; i < tables.snapshotLength; i++) {
var ab = new Ability();
var type = "";
var lines = tables.snapshotItem(i).textContent.split(/\n/);
for (var l in lines) {
var m;
m = lines[l].match(/Suc: (\d+)( \(\+(\d+)\))?/);
if (m) {
ab.suc += parseInt(m[1]);
if (m[3])
ab.suc += parseInt(m[3]);
continue;
}
m = lines[l].match(/Str: (\d+)( \(\+(\d+)\))?/);
if (m) {
ab.str += parseInt(m[1]);
if (m[3])
ab.str += parseInt(m[3]);
continue;
}
m = lines[l].match(/Rng: (\d+)( \(\+(\d+)\))?/);
if (m) {
ab.rng += parseInt(m[1]);
if (m[3])
ab.rng += parseInt(m[3]);
continue;
}
m = lines[l].match(/Lvl: (\d+)( \(\+(\d+)\))?/);
if (m) {
ab.lvl += parseInt(m[1]);
if (m[3])
ab.lvl += parseInt(m[3]);
continue;
}
m = lines[l].match(/(\w{3}): (\d+)/);
if (m) {
type = m[1].toLowerCase();
ab.lvl += parseInt(m[2]);
continue;
}
}
ninja[type] = ab;
}
return ninja;
}
function parseMission()
{
var tables = document.evaluate("//table[count(descendant::table)=0]",
document, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
var node = null;
var mission = new Mission();
for (var i = 0; i < tables.snapshotLength; i++) {
var txt = tables.snapshotItem(i).textContent;
if (/virus attack/i.test(txt)) {
var lines = txt.split(/\n/);
// Gen D: 15 S: 15
for (var l in lines) {
var line = lines[l].replace(/\s+/g, " ");
var m = line.match(/(\w{3}) D: (\d+) S: (\d+)/);
if (m) {
var type = m[1].toLowerCase();
mission[type].dif = parseInt(m[2]);
mission[type].suc = parseInt(m[3]);
node = tables.snapshotItem(i);
}
}
break;
}
}
if (node)
return {mission: mission, node: node};
}
var ninja = parseAbilities();
var missionNode = parseMission();
if (missionNode) {
var mission = missionNode.mission;
var node = missionNode.node;
var p = 1;
for (var t in jutsuTypes) {
p *= successChance(mission[t], ninja[t]);
}
var tr = document.createElement("tr");
var td = document.createElement("td");
td.textContent = "Success chance: " + percent(p);
td.style.color = "white";
tr.appendChild(td);
node.firstChild.appendChild(tr);
}
// Rough estimate of average field difficulty
function fieldChallenge(diff)
{
diff = Math.min(230, diff);
diff = Math.max(0, diff);
var c = new Challenge();
c.dif = Math.round(0.13 * diff + 15);
c.suc = Math.round(0.11 * diff + 15);
return c;
}
// Calculate max difficulty a ninja can reliably handle
var diff, p;
for (diff = 230; diff >= 0; diff -= 5) {
p = 1;
for (var t in basicJutsuTypes)
p = Math.min(p, successChance(fieldChallenge(diff), ninja[t]));
if (p > 0.95)
break;
}
var form = document.getElementsByName("field")[0];
var div = document.createElement("div");
div.textContent = "Recommended difficulty (estimate): " + diff + " (" + percent(p) + ")";
form.parentNode.insertBefore(div, form);