NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name ABDestination
// @namespace fr.kergoz-panic.watilin
// @description Choisissez une destination et ce script vous dira quelle direction prendre et quand vous arriverez.
// @version 1.0
// @include http://www.alphabounce.com/
// @include http://www.alphabounce.com/user/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_getResourceText
// @resource ui-html ./ui.html
// @resource ui-css ./ui.css
// ==/UserScript==
"use strict";
// Note: this userscript is Firefox only.
// Table of Contents
// [CON] Script Constants
// [MAI] Main Script Section
// [UIM] UI Managment
// [ANI] Animation
// [UTI] Utils
// [@CON] Script Constants /////////////////////////////////////////////
const π = Math.PI;
const ANIM_DURATION = 1200; // ms
const WORMHOLE_THRESHOLD = 4000; // ms
const FREE_FUEL = 3; // not intended to remain constant
// [@MAI] Main Script Section //////////////////////////////////////////
if (self === top &&
"/" === location.pathname) { // top-level window
let iw = document.getElementById("iframe").contentWindow;
// hacking haxe.Http to retrieve player's coordinates
// this avoids making an unnecessary request to the server
let proto = unsafeWindow.haxe.Http.prototype;
proxifyFunction(proto, "request", function () {
if (!this.url.startsWith("/user/data.xml")) return;
proxifyFunction(this, "onData", function (str) {
var engineChanged = false;
var coordsChanged = false;
var engineMatch = /\bengine="(\d+)"/.exec(str);
if (engineMatch) {
let oldEngine = GM_getValue("engine", 1);
let engine = parseInt(engineMatch[1], 10);
engineChanged = oldEngine !== engine;
GM_setValue("engine", engine);
}
var xMatch = /\bx="(-?\d+)"/.exec(str);
var yMatch = /\by="(-?\d+)"/.exec(str);
if (xMatch && yMatch) {
let oldX = GM_getValue("x", 0);
let oldY = GM_getValue("y", 0);
let x = parseInt(xMatch[1], 10);
let y = parseInt(yMatch[1], 10);
coordsChanged = (oldX !== x) || (oldY !== y);
GM_setValue("x", x);
GM_setValue("y", y);
}
// sending one event for one or both changes
if (engineChanged || coordsChanged) {
document.getElementById("iframe").contentWindow
.dispatchEvent(new CustomEvent("gameDataChanged"));
}
});
});
} else if ("/" === parent.location.pathname &&
location.pathname.startsWith("/user/")) { // iframe
let $menuUl = document.querySelector("#menu ul");
let $newLi = document.createElement("li");
let $oldLi = $menuUl.querySelector(".active");
let $ui;
let $section = document.getElementById("section");
let $a = document.createElement("a");
$a.textContent = "Destination";
$a.href = "/user/destination";
$a.addEventListener("click", event => {
event.preventDefault();
if ($newLi.classList.contains("active")) return;
if (!$ui) {
let fragment = injectUI();
$ui = fragment.querySelector("#section");
$section.parentNode.insertBefore(fragment, $section.nextSibling);
}
$ui.style.display = "";
$section.style.display = "none";
$oldLi.classList.remove("active");
$newLi.classList.add("active");
requestAnimationFrame(updateUI);
});
$oldLi.querySelector("a").addEventListener("click", event => {
if (!$newLi.classList.contains("active")) return;
event.preventDefault();
$ui.style.display = "none";
$section.style.display = "";
$oldLi.classList.add("active");
$newLi.classList.remove("active");
});
$newLi.appendChild($a);
$menuUl.appendChild($newLi);
}
// [@UIM] UI Managment /////////////////////////////////////////////////
var [ injectUI, updateUI ] = (function () {
var $coordX, $coordY,
$engine,
$destX, $destY,
$distH, $distV, $distTot,
$trip,
$cape;
var paintStyles = {};
return [
function injectUI() {
var fragment = document.createDocumentFragment();
var $container = document.createElement("div");
var uiHtml = GM_getResourceText("ui-html");
var uiCss = GM_getResourceText("ui-css");
var $style = document.createElement("style");
$style.type = "text/css";
$style.media = "screen";
$style.textContent = uiCss;
document.head.appendChild($style);
var sheet = $style.sheet;
for (let rule of sheet.cssRules) {
if (rule.selectorText.startsWith("#cape .")) {
paintStyles[rule.selectorText] = rule.style;
}
}
window.addEventListener("gameDataChanged", function () {
requestAnimationFrame(updateUI);
});
$container.innerHTML = uiHtml;
$coordX = $container.querySelector("#coord-x");
$coordY = $container.querySelector("#coord-y");
$engine = $container.querySelector("#engine");
$destX = $container.querySelector("#dest-x");
$destY = $container.querySelector("#dest-y");
$distH = $container.querySelector("#dist-h");
$distV = $container.querySelector("#dist-v");
$distTot = $container.querySelector("#dist-tot");
$trip = $container.querySelector("#trip");
$cape = $container.querySelector("#cape");
var timerId;
var destinationChange = function (event) {
clearTimeout(timerId);
timerId = setTimeout(function () {
GM_setValue("destinationX", $destX.value);
GM_setValue("destinationY", $destY.value);
updateUI();
}, 200);
};
$destX.addEventListener("change", destinationChange);
$destX.addEventListener("keyup", destinationChange);
$destY.addEventListener("change", destinationChange);
$destY.addEventListener("keyup", destinationChange);
var cx = $cape.getContext("2d");
cx.translate($cape.width / 2, $cape.height / 2);
while ($container.firstChild) {
fragment.appendChild($container.firstChild);
}
return fragment;
},
function updateUI() {
var x = parseInt(GM_getValue("x", 0), 10);
var y = parseInt(GM_getValue("y", 0), 10);
var engine = parseInt(GM_getValue("engine", 1), 10);
var destX = parseInt(GM_getValue("destinationX", 0), 10);
var destY = parseInt(GM_getValue("destinationY", 0), 10);
$coordX.textContent = x;
$coordY.textContent = y;
$engine.textContent = engine;
$destX.value = destX;
$destY.value = destY;
var distH = destX - x;
var distV = destY - y;
var absDistH = Math.abs(distH);
var absDistV = Math.abs(distV);
var distTot = absDistH + absDistV;
$distH.textContent = absDistH;
$distV.textContent = absDistV;
$distTot.textContent = distTot;
var days = Math.ceil(distTot / (engine * FREE_FUEL));
$trip.textContent = days + (days >= 2 ? "\xA0jours" : "\xA0jour");
var angle = Math.atan(distV / distH);
if (distH < 0) angle += π;
animateAngleChange(angle, $cape, paintStyles);
}
];
}());
// [@ANI] Animation ////////////////////////////////////////////////////
var animateAngleChange = (function () {
var previousAngle = 0;
var currentAngle = 0;
var reqId;
return function animateAngleChange(newAngle, $cvs, paintStyles) {
cancelAnimationFrame(reqId);
previousAngle = currentAngle || 0;
var angleDiff = newAngle - previousAngle;
if (Math.abs(angleDiff) > π) {
angleDiff -= Math.sign(angleDiff) * 2*π;
}
var firstFrameTime = Date.now();
var lastFrameTime = Date.now();
var ease = easeOutCubic;
(function drawNextFrame() {
var now = Date.now();
var t = now - firstFrameTime;
if (t <= ANIM_DURATION) {
reqId = requestAnimationFrame(drawNextFrame);
}
// prevents “wormhole” effect
if (now - lastFrameTime > WORMHOLE_THRESHOLD) return;
lastFrameTime = now;
var angle = previousAngle + ease(t/ANIM_DURATION) * angleDiff;
currentAngle = angle;
draw($cvs, angle, paintStyles);
}());
}
}());
function easeOutCubic(x) {
if (x < 0) return 0;
if (x > 1) return 1;
var p = x - 1;
return p*p*p + 1;
}
function draw($cvs, θ, paintStyles) {
var cx = $cvs.getContext("2d");
var w = $cvs.width;
var h = $cvs.height;
var r = Math.min(w, h) / 2;
var cos = Math.cos.bind(Math);
var sin = Math.sin.bind(Math);
cx.clearRect(-w/2, -h/2, w, h);
// draws the background grid
cx.fillStyle = paintStyles["#cape .grid"].backgroundColor;
cx.fillRect(-w/2, -h/2, w, h);
cx.strokeStyle = paintStyles["#cape .grid"].color;
cx.lineWidth = 1;
var gridOffsetX = Math.round(20 * Math.SQRT1_2 * cos(θ)) - 0.5;
var gridOffsetY = Math.round(20 * Math.SQRT1_2 * sin(θ)) - 0.5;
cx.beginPath();
for (var gridX = -w/2 - gridOffsetX; gridX < w/2; gridX += 20) {
cx.moveTo(gridX, -h/2);
cx.lineTo(gridX, +h/2);
}
for (var gridY = -h/2 - gridOffsetY; gridY < h/2; gridY += 20) {
cx.moveTo(-w/2, gridY);
cx.lineTo(+w/2, gridY);
}
cx.stroke();
var ρ = w*2;
var a = Math.round(ρ*0.9 * cos(θ+π)) - 0.5;
var b = Math.round(ρ*0.9 * sin(θ+π)) - 0.5;
// draws the vernier-like scale
var scale = 1/60;
var dash = 2*π * ρ * scale;
cx.setLineDash([1, dash-1]);
cx.lineWidth = 20;
cx.strokeStyle = paintStyles["#cape .vernier"].color;
cx.beginPath();
cx.arc(a, b, ρ, θ*3, θ*3 + 2*π);
cx.stroke();
// draws the compass scale
cx.lineCap = "butt";
cx.strokeStyle = paintStyles["#cape .compass"].color;
var compassRadius = r-5;
dash = 2*π * compassRadius / 16;
cx.lineWidth = 5;
cx.setLineDash([1, dash-1]);
cx.lineDashOffset = 0.5;
cx.beginPath();
cx.arc(-0.5, -0.5, compassRadius, 0, 2*π);
cx.stroke();
compassRadius = r-8;
dash = 2*π * compassRadius / 4;
cx.lineWidth = 8;
cx.setLineDash([1, dash-1]);
cx.beginPath();
cx.arc(-0.5, -0.5, compassRadius, 0, 2*π);
cx.stroke();
cx.lineDashOffset = 0;
cx.setLineDash([]);
// traçage de la flèche
/* H
F____________________|\
\ G \
\E \A
/ /
/___________________C /
D |/
B
*/
var arrowRadius = r-4;
cx.lineWidth = 1.5;
cx.beginPath();
cx.moveTo(-0.5 + Math.round(arrowRadius * cos(θ)),
-0.5 + Math.round(arrowRadius * sin(θ))); // A
cx.lineTo(-0.5 + Math.round(0.5*arrowRadius * cos(0.6 + θ)),
-0.5 + Math.round(0.5*arrowRadius * sin(0.6 + θ))); // B
cx.lineTo(-0.5 + Math.round(0.5*arrowRadius * cos(0.09 + θ)),
-0.5 + Math.round(0.5*arrowRadius * sin(0.09 + θ))); // C
cx.lineTo(-0.5 + Math.round(0.96*arrowRadius * cos(π - 0.15 + θ)),
-0.5 + Math.round(0.96*arrowRadius * sin(π - 0.15 + θ))); // D
cx.lineTo(-0.5 + Math.round(0.9*arrowRadius * cos(π + θ)),
-0.5 + Math.round(0.9*arrowRadius * sin(π + θ))); // E
cx.lineTo(-0.5 + Math.round(0.96*arrowRadius * cos(π + 0.15 + θ)),
-0.5 + Math.round(0.96*arrowRadius * sin(π + 0.15 + θ))); // F
cx.lineTo(-0.5 + Math.round(0.5*arrowRadius * cos(-0.09 + θ)),
-0.5 + Math.round(0.5*arrowRadius * sin(-0.09 + θ))); // G
cx.lineTo(-0.5 + Math.round(0.5*arrowRadius * cos(-0.6 + θ)),
-0.5 + Math.round(0.5*arrowRadius * sin(-0.6 + θ))); // H
cx.closePath();
cx.strokeStyle = paintStyles["#cape .arrow"].color;
cx.fillStyle = paintStyles["#cape .arrow"].backgroundColor;
cx.stroke();
cx.fill();
}
// [@UTI] Utils ////////////////////////////////////////////////////////
function proxifyFunction(targetScope, funcName, action) {
targetScope = targetScope.wrappedJSObject || targetScope;
var desc = Object.getOwnPropertyDescriptor(targetScope, funcName);
if (!desc.configurable) {
throw new Error("Cannot proxify non configurable function");
}
var backup = exportFunction(targetScope[funcName],
targetScope,
{ allowCrossOriginArguments: true });
exportFunction(function () {
if (action) action.apply(this, arguments);
return backup.apply(this, arguments);
}, targetScope, { defineAs: funcName });
}