v30.18: Pentagon build radar + armor target selector with PDA storage
// ==UserScript==
// @name ADPT Sim Inventory Support
// @namespace http://tampermonkey.net/
// @version 30.18
// @description v30.18: Pentagon build radar + armor target selector with PDA storage
// @author csv construct & AI Update
// @match https://www.torn.com/page.php?sid=ItemMarket*
// @match https://www.torn.com/amarket.php*
// @match https://www.torn.com/item.php*
// @match https://www.torn.com/factions.php?step=your&type=5*
// @license me
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function() {
'use strict';
// ── DEBUG FLAG ── set true to enable console debug output ──────────────
const DEBUG = false;
const dbg = (...args) => { if (DEBUG) console.log(...args); };
// ───────────────────────────────────────────────────────────────────────
// ==========================================
// PDA & STORAGE WRAPPERS
// ==========================================
var rD_xmlhttpRequest;
var rD_setValue;
var rD_getValue;
var rD_listValues;
var rD_deleteValue;
var rD_registerMenuCommand;
var rD_PDA;
var apikey = "###PDA-APIKEY###";
if (apikey[0] !== "#") {
rD_PDA = true;
rD_xmlhttpRequest = function (details) {
if (details.method.toLowerCase() === "get") {
return PDA_httpGet(details.url).then(details.onload).catch(details.onerror || ((e) => console.error("[ADPT] Request Error:", e)));
} else if (details.method.toLowerCase() === "post") {
return PDA_httpPost(details.url, details.headers || {}, details.body || details.data || "").then(details.onload).catch(details.onerror || ((e) => console.error("[ADPT] Request Error:", e)));
}
};
rD_setValue = (name, value) => localStorage.setItem(name, JSON.stringify(value));
rD_getValue = (name, defaultValue) => { try { const v = localStorage.getItem(name); return v !== null ? JSON.parse(v) : defaultValue; } catch(e) { return defaultValue; } };
rD_listValues = () => Object.keys(localStorage);
rD_deleteValue = (name) => localStorage.removeItem(name);
rD_registerMenuCommand = () => { /* Disabled on PDA */ };
} else {
rD_PDA = false;
rD_xmlhttpRequest = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : () => {};
rD_setValue = (typeof GM_setValue !== 'undefined') ? GM_setValue : (k,v) => localStorage.setItem(k, JSON.stringify(v));
rD_getValue = (typeof GM_getValue !== 'undefined') ? GM_getValue : (k,d) => { try { const v = localStorage.getItem(k); return v !== null ? JSON.parse(v) : d; } catch(e) { return d; } };
rD_listValues = (typeof GM_listValues !== 'undefined') ? GM_listValues : () => Object.keys(localStorage);
rD_deleteValue = (typeof GM_deleteValue !== 'undefined') ? GM_deleteValue : (k) => localStorage.removeItem(k);
rD_registerMenuCommand = (typeof GM_registerMenuCommand !== 'undefined') ? GM_registerMenuCommand : () => {};
}
const adptStorage = {
set: (k, v) => rD_setValue('adpt_' + k, v),
get: (k, d) => rD_getValue('adpt_' + k, d)
};
// --- CONFIG STATS ---
const MY_STATS = { str: 1000000000, spd: 1000000000 };
const ENEMY_STATS = { def: 1000000000, dex: 1000000000 };
const SIM_LIMIT =25;
const ARMOR_BASE = 40;
const JAPANESE_WEAPONS = [
"Dual Samurai Swords", "Kama", "Katana", "Kodachi", "Sai", "Samurai Sword", "Yasukuni Sword"
];
const WEAPON_DB = {
"9mm Uzi": { "mag": 30, "rof": 20.0 }, "AK-47": { "mag": 30, "rof": 6.0 }, "AK74U": { "mag": 30, "rof": 5.0 },
"ArmaLite M-15A4": { "mag": 15, "rof": 4.0 }, "Benelli M1 Tactical": { "mag": 7, "rof": 2.5 }, "Benelli M4 Super": { "mag": 7, "rof": 2.5 },
"Bushmaster Carbon 15": { "mag": 30, "rof": 15.0 }, "Dual Bushmasters": { "mag": 60, "rof": 17.0 }, "Dual MP5s": { "mag": 60, "rof": 13.0 },
"Dual P90s": { "mag": 100, "rof": 15.0 }, "Dual TMPs": { "mag": 30, "rof": 20.0 }, "Dual Uzis": { "mag": 60, "rof": 20.0 },
"Egg Propelled Launcher": { "mag": 1000, "rof": 405.0 }, "Enfield SA-80": { "mag": 30, "rof": 3.5 }, "Gold Plated AK-47": { "mag": 45, "rof": 5.0 },
"Heckler & Koch SL8": { "mag": 10, "rof": 5.5 }, "Ithaca 37": { "mag": 4, "rof": 2.5 }, "Jackhammer": { "mag": 10, "rof": 7.5 },
"M16 A2 Rifle": { "mag": 30, "rof": 7.0 }, "M249 SAW": { "mag": 100, "rof": 20.0 }, "M4A1 Colt Carbine": { "mag": 30, "rof": 6.5 },
"Mag 7": { "mag": 5, "rof": 3.0 }, "Minigun": { "mag": 200, "rof": 25.0 }, "MP 40": { "mag": 32, "rof": 4.0 }, "MP5 Navy": { "mag": 30, "rof": 6.5 },
"Negev NG-5": { "mag": 100, "rof": 20.0 }, "Neutrilux 2000": { "mag": 1000, "rof": 383.0 }, "Nock Gun": { "mag": 7, "rof": 7.0 }, "P90": { "mag": 50, "rof": 20.0 },
"PKM": { "mag": 50, "rof": 13.0 }, "Prototype": { "mag": 100, "rof": 25.0 }, "Rheinmetall MG 3": { "mag": 100, "rof": 25.0 },
"Sawed-Off Shotgun": { "mag": 2, "rof": 1.5 }, "SIG 550": { "mag": 20, "rof": 5.5 }, "SIG 552": { "mag": 30, "rof": 5.5 },
"SKS Carbine": { "mag": 10, "rof": 4.5 }, "Snow Cannon": { "mag": 950, "rof": 6.5 }, "Steyr AUG": { "mag": 30, "rof": 6.5 },
"Stoner 96": { "mag": 100, "rof": 11.5 }, "Tavor TAR-21": { "mag": 30, "rof": 6.0 }, "Thompson": { "mag": 20, "rof": 4.0 },
"Vektor CR-21": { "mag": 30, "rof": 6.0 }, "XM8 Rifle": { "mag": 30, "rof": 6.0 }, "Beretta 92FS": { "mag": 20, "rof": 4.5 },
"Beretta M9": { "mag": 17, "rof": 4.5 }, "Beretta Pico": { "mag": 6, "rof": 1.5 }, "Blowgun": { "mag": 1, "rof": 1.0 },
"Blunderbuss": { "mag": 1, "rof": 1.0 }, "BT MP9": { "mag": 30, "rof": 13.5 }, "China Lake": { "mag": 3, "rof": 1.0 },
"Cobra Derringer": { "mag": 2, "rof": 1.5 }, "Crossbow": { "mag": 1, "rof": 1.0 }, "Desert Eagle": { "mag": 7, "rof": 2.5 },
"Dual 92G Berettas": { "mag": 46, "rof": 10.0 }, "Fiveseven": { "mag": 20, "rof": 6.5 }, "Flamethrower": { "mag": 1, "rof": 1.0 },
"Flare Gun": { "mag": 1, "rof": 1.0 }, "Glock 17": { "mag": 20, "rof": 4.5 }, "Harpoon": { "mag": 1, "rof": 1.0 },
"Homemade Pocket Shotgun": { "mag": 1, "rof": 1.0 }, "Lorcin 380": { "mag": 6, "rof": 3.0 }, "Luger": { "mag": 8, "rof": 2.0 },
"Magnum": { "mag": 6, "rof": 1.5 }, "Milkor MGL": { "mag": 6, "rof": 1.0 }, "MP5k": { "mag": 15, "rof": 6.0 },
"Pink Mac-10": { "mag": 32, "rof": 12.0 }, "Qsz-92": { "mag": 15, "rof": 4.0 }, "Raven MP25": { "mag": 6, "rof": 4.0 },
"RPG Launcher": { "mag": 1, "rof": 1.0 }, "SMAW Launcher": { "mag": 1, "rof": 1.0 }, "Ruger 57": { "mag": 20, "rof": 3.5 },
"S&W M29": { "mag": 6, "rof": 1.5 }, "S&W Revolver": { "mag": 6, "rof": 1.5 }, "Skorpion": { "mag": 20, "rof": 9.0 },
"Slingshot": { "mag": 1, "rof": 1.0 }, "Springfield 1911": { "mag": 7, "rof": 3.0 }, "Taser": { "mag": 1, "rof": 1.0 },
"Taurus": { "mag": 5, "rof": 2.0 }, "TMP": { "mag": 30, "rof": 15.0 }, "Tranquilizer Gun": { "mag": 1, "rof": 1.0 },
"USP": { "mag": 12, "rof": 4.5 }, "Macana": { "mag": 0, "rof": 1.0 }, "Diamond Bladed Knife": { "mag": 0, "rof": 1.0 },
"Kodachi": { "mag": 0, "rof": 1.0 }, "Katana": { "mag": 0, "rof": 1.0 }, "Naval Cutlass": { "mag": 0, "rof": 1.0 },
"Butterfly Knife": { "mag": 0, "rof": 1.0 }, "Kitchen Knife": { "mag": 0, "rof": 1.0 }, "Hammer": { "mag": 0, "rof": 1.0 },
"Crowbar": { "mag": 0, "rof": 1.0 }, "Yasukuni Sword": { "mag": 0, "rof": 1.0 }, "Sledgehammer": { "mag": 0, "rof": 1.0 },
"Claymore Sword": { "mag": 0, "rof": 1.0 }, "Sai": { "mag": 0, "rof": 1.0 }, "Kama": { "mag": 0, "rof": 1.0 }, "Dual Samurai Swords": { "mag": 0, "rof": 1.0 }, "Samurai Sword": { "mag": 0, "rof": 1.0 },
"Axe": { "mag": 0, "rof": 1.0 }, "Baseball Bat": { "mag": 0, "rof": 1.0 }, "Blood Spattered Sickle": { "mag": 0, "rof": 1.0 },
"Bone Saw": { "mag": 0, "rof": 1.0 }, "Bo Staff": { "mag": 0, "rof": 1.0 }, "Bread Knife": { "mag": 0, "rof": 1.0 },
"Bug Swatter": { "mag": 0, "rof": 1.0 }, "Cattle Prod": { "mag": 0, "rof": 1.0 }, "Chain Whip": { "mag": 0, "rof": 1.0 },
"Chainsaw": { "mag": 0, "rof": 1.0 }, "Cleaver": { "mag": 0, "rof": 1.0 }, "Cricket Bat": { "mag": 0, "rof": 1.0 },
"Dagger": { "mag": 0, "rof": 1.0 }, "Devil's Pitchfork": { "mag": 0, "rof": 1.0 }, "Diamond Icicle": { "mag": 0, "rof": 1.0 },
"Dual Axes": { "mag": 0, "rof": 1.0 }, "Dual Hammers": { "mag": 0, "rof": 1.0 }, "Dual Scimitars": { "mag": 0, "rof": 1.0 },
"Duke's Hammer": { "mag": 0, "rof": 1.0 }, "Fine Chisel": { "mag": 0, "rof": 1.0 }, "Flail": { "mag": 0, "rof": 1.0 },
"Frying Pan": { "mag": 0, "rof": 1.0 }, "Golden Broomstick": { "mag": 0, "rof": 1.0 }, "Golf Club": { "mag": 0, "rof": 1.0 },
"Guandao": { "mag": 0, "rof": 1.0 }, "Handbag": { "mag": 0, "rof": 1.0 }, "Ice Pick": { "mag": 0, "rof": 1.0 },
"Ivory Walking Cane": { "mag": 0, "rof": 1.0 }, "Knuckle Dusters": { "mag": 0, "rof": 1.0 }, "Lead Pipe": { "mag": 0, "rof": 1.0 },
"Leather Bullwhip": { "mag": 0, "rof": 1.0 }, "Madball": { "mag": 0, "rof": 1.0 }, "Meat Hook": { "mag": 0, "rof": 1.0 },
"Metal Nunchakus": { "mag": 0, "rof": 1.0 }, "Ninja Claws": { "mag": 0, "rof": 1.0 }, "Pair of High Heels": { "mag": 0, "rof": 1.0 },
"Pair of Ice Skates": { "mag": 0, "rof": 1.0 }, "Pen Knife": { "mag": 0, "rof": 1.0 }, "Penelope": { "mag": 0, "rof": 1.0 },
"Petrified Humerus": { "mag": 0, "rof": 1.0 }, "Pillow": { "mag": 0, "rof": 1.0 }, "Plastic Sword": { "mag": 0, "rof": 1.0 },
"Poison Umbrella": { "mag": 0, "rof": 1.0 }, "Riding Crop": { "mag": 0, "rof": 1.0 }, "Rusty Sword": { "mag": 0, "rof": 1.0 },
"Scalpel": { "mag": 0, "rof": 1.0 }, "Scimitar": { "mag": 0, "rof": 1.0 }, "Spear": { "mag": 0, "rof": 1.0 },
"Swiss Army Knife": { "mag": 0, "rof": 1.0 }, "Twin Tiger Hooks": { "mag": 0, "rof": 1.0 }, "Wand of Destruction": { "mag": 0, "rof": 1.0 },
"Wooden Nunchaku": { "mag": 0, "rof": 1.0 }, "Wushu Double Axes": { "mag": 0, "rof": 1.0 },
"Type 98 Anti Tank": { "mag": 1, "rof": 1.0 }
};
const BONUS_DB = {
"Japanese Blade Mastery": {"type": "Damage", "scope": "Permanent", "weight": 1.0},
"Assassinate": {"type": "Damage", "scope": "Turn_1", "weight": 1.0},
"Backstab": {"type": "Damage", "scope": "Turn_1", "weight": 1.0},
"Blindside": {"type": "Damage", "scope": "Turn_1", "weight": 1.0},
"Specialist": {"type": "Damage", "scope": "Mag_1", "weight": 1.0},
"Powerful": {"type": "Damage", "scope": "Permanent", "weight": 1.0},
"Puncture": {"type": "Damage", "scope": "Permanent", "weight": 1.0},
"Piercing": {"type": "Damage", "scope": "Permanent", "weight": 1.0},
"Eviscerate": {"type": "Damage", "scope": "Permanent", "weight": 1.0},
"Empower": {"type": "Stat_Buff", "scope": "Permanent", "weight": 0.1},
"Smurf": {"type": "Damage", "scope": "Permanent", "weight": 1.0},
"Double Tap": {"type": "Multi-Hit", "scope": "Probabilistic", "weight": 1.0},
"Fury": {"type": "Multi-Hit", "scope": "Probabilistic", "weight": 1.0},
"Rage": {"type": "Multi-Hit", "scope": "Probabilistic", "weight": 4.0},
"Double-edged": {"type": "Damage", "scope": "Probabilistic", "weight": 1.0},
"Deadly": {"type": "Crit", "scope": "Permanent", "weight": 1.0},
"Expose": {"type": "Crit", "scope": "Permanent", "weight": 1.0},
"Sure Shot": {"type": "Accuracy", "scope": "Permanent", "weight": 1.0},
"Quicken": {"type": "Accuracy", "scope": "Permanent", "weight": 0.1},
"Focus": {"type": "HitChance", "scope": "Scaling", "weight": 1.0},
"Grace": {"type": "Mixed", "scope": "Permanent", "weight": 1.0},
"Berserk": {"type": "Mixed", "scope": "Permanent", "weight": 1.0},
"Weaken": {"type": "Stat_Debuff", "scope": "Scaling", "weight": 0.25},
"Wither": {"type": "Stat_Debuff", "scope": "Scaling", "weight": 0.25},
"Slow": {"type": "Stat_Debuff", "scope": "Scaling", "weight": 0.25},
"Cripple": {"type": "Stat_Debuff", "scope": "Scaling", "weight": 0.25},
"Freeze": {"type": "Stat_Debuff", "scope": "Scaling", "weight": 0.3},
"Motivation": {"type": "Stat_Buff", "scope": "Scaling", "weight": 0.1},
"Inspiration": {"type": "Stat_Buff", "scope": "Scaling", "weight": 0.2},
"Frenzy": {"type": "Stat_Buff", "scope": "Scaling", "weight": 1.0},
"Shred": {"type": "Damage_Taken", "scope": "Scaling", "weight": 1.0},
"Wind-up": {"type": "Damage", "scope": "Special_Cycle", "weight": 1.0},
"Bleed": {"type": "DOT", "scope": "DOT", "weight": 1.0},
"Poison": {"type": "DOT", "scope": "DOT", "weight": 1.0},
"Burning": {"type": "DOT", "scope": "DOT", "weight": 1.0},
"Radiation": {"type": "DOT", "scope": "DOT", "weight": 1.0},
"Irradiate": {"type": "DOT", "scope": "DOT", "weight": 1.0},
"Throttle": {"type": "HitLoc", "scope": "Permanent", "weight": 1.0},
"Deadeye": {"type": "HitLoc", "scope": "Permanent", "weight": 1.0},
"Achilles": {"type": "HitLoc", "scope": "Permanent", "weight": 1.0},
"Cupid": {"type": "HitLoc", "scope": "Permanent", "weight": 1.0},
"Crusher": {"type": "HitLoc", "scope": "Permanent", "weight": 1.0},
"Roshambo": {"type": "HitLoc", "scope": "Permanent", "weight": 1.0},
"Execute": {"type": "Finisher", "scope": "Threshold", "weight": 1.0},
"Execution": {"type": "Finisher", "scope": "Threshold", "weight": 1.0},
"Comeback": {"type": "Damage", "scope": "Permanent", "weight": 0.5},
"Penetrate": {"type": "Armor", "scope": "Permanent", "weight": 1.0},
"Finale": {"type": "Damage", "scope": "Permanent", "weight": 0.1},
"Conserve": {"type": "Utility", "scope": "Ammo_Eff", "weight": 1.0}
};
const RAW_HIT_LOCS = [
{n: "Head", w: 9.6, m: 3.5, b: ["Deadeye", "Crusher"], isCrit: true},
{n: "Throat", w: 1.2, m: 3.5, b: ["Throttle"], isCrit: true},
{n: "Heart", w: 1.2, m: 3.5, b: ["Cupid"], isCrit: true},
{n: "Chest", w: 22.0, m: 2.0, b: [], isCrit: false},
{n: "Stomach", w: 17.6, m: 2.0, b: [], isCrit: false},
{n: "Groin", w: 4.4, m: 2.0, b: ["Roshambo"], isCrit: false},
{n: "Arms", w: 8.8, m: 1.0, b: [], isCrit: false},
{n: "Hand", w: 8.8, m: 0.7, b: [], isCrit: false},
{n: "Legs", w: 17.6, m: 1.0, b: ["Achilles"], isCrit: false},
{n: "Foot", w: 8.8, m: 0.7, b: ["Achilles"], isCrit: false}
];
function calculateAvgHitMultiplier(activeBonuses) {
let locs = JSON.parse(JSON.stringify(RAW_HIT_LOCS));
activeBonuses.forEach(bonus => {
const bN = bonus.name;
const p = bonus.val / 100;
const bData = BONUS_DB[bN];
if (bN === "Expose") {
let shiftChance = p * 100;
let totalNonCritW = locs.reduce((sum, l) => sum + (l.isCrit ? 0 : l.w), 0);
if (totalNonCritW > 0) {
locs.forEach(l => {
if (!l.isCrit) {
l.w -= (l.w / totalNonCritW) * shiftChance;
if (l.w < 0) l.w = 0;
} else {
let baseCritTotal = 12.0;
l.w += (l.w / baseCritTotal) * shiftChance;
}
});
}
}
if (bData && bData.type === "HitLoc") {
locs.forEach(l => {
if (l.b.includes(bN)) {
l.m *= (1 + p);
}
});
}
});
let totalW = 0, weightedSum = 0;
locs.forEach(l => {
weightedSum += (l.w * l.m);
totalW += l.w;
});
return weightedSum / totalW;
}
function getHitChance(userSpeed, enemyDex, accuracy) {
let effectiveSpeed = userSpeed * (accuracy / 50);
let ratio = effectiveSpeed / enemyDex;
let baseHitChance;
if (ratio > 64) baseHitChance = 100;
else if (ratio >= 1 && ratio <= 64) baseHitChance = 100 - 50 / 7 * (8 * Math.sqrt(1 / ratio) - 1);
else if (ratio > 1 / 64 && ratio < 1) baseHitChance = 50 / 7 * (8 * Math.sqrt(ratio) - 1);
else baseHitChance = 0;
return baseHitChance / 100.0;
}
// Returns { armorVal, dexMult } for the currently saved armor selection,
// accounting for weapon type (melee vs gun).
// Used by simulateADPT so all injected ADPT scores reflect selected armor.
function getActiveArmorForSim(weaponName) {
const ADPT_ARMOR_TABLE = {
combat: { armorMelee: 40, armorGun: 40, dexMult: 1.0 },
riot: { armorMelee: 58.5, armorGun: 45, dexMult: 1.0 },
assault: { armorMelee: 45, armorGun: 58.5, dexMult: 1.0 },
vanguard: { armorMelee: 50, armorGun: 50, dexMult: 2.5 }
};
const savedId = rD_getValue('adpt_target_armor', 'combat');
const opt = ADPT_ARMOR_TABLE[savedId] || ADPT_ARMOR_TABLE.combat;
const wEntry = WEAPON_DB[weaponName];
const isMeleeWeapon = wEntry != null ? wEntry.mag === 0 : false;
return {
armorVal: isMeleeWeapon ? opt.armorMelee : opt.armorGun,
dexMult: opt.dexMult
};
}
function simulateADPT(name, baseDmg, baseAcc, activeBonuses, ammoType = "") {
const w = WEAPON_DB[name] || { mag: 0, rof: 1.0 };
let conserveBonus = activeBonuses.find(b => b.name === "Conserve");
let effMag = w.mag;
if (conserveBonus) { effMag = w.mag / (1 - (conserveBonus.val / 100)); }
if (ammoType === "Tracer") baseAcc += 10.0;
let totalDmg = 0, bulletsSpent = 0, stacks = 0;
let frenzyStacks = 0, focusStacks = 0, motivationStacks = 0;
let currentMag = effMag;
const locAvgM = calculateAvgHitMultiplier(activeBonuses);
let history = [];
const hasSpecialist = activeBonuses.some(b => b.name === "Specialist");
// --- NEW AMMO LIMIT LOGIC ---
// Determines maximum number of magazines allowed (1 if Specialist, else 3)
const maxMags = hasSpecialist ? 1 : 3;
// Tracks how many magazines have been loaded (starting with 1 initially)
let magsUsed = 1;
// Use selected armor for this simulation
const _activeArmor = getActiveArmorForSim(name);
for (let t = 1; t <= SIM_LIMIT; t++) {
let currStr = MY_STATS.str, currSpd = MY_STATS.spd;
let currDef = ENEMY_STATS.def, currDex = ENEMY_STATS.dex * _activeArmor.dexMult;
// --- RELOAD LOGIC UPDATE ---
if (w.mag > 0 && currentMag <= 0) {
if (magsUsed >= maxMags) {
// Out of total magazines, deal 0 damage for the rest of the sim
history.push(0);
continue;
} else {
// Reload weapon: consumes a turn, no damage done, increment magsUsed
magsUsed++;
currentMag = effMag;
history.push(0);
continue;
}
}
let tM = 1.0, arm = _activeArmor.armorVal, acc = baseAcc;
// Ammo type modifiers — matched to chart: HP=baseDmg*1.5 arm*1.5, IN=baseDmg*1.4 arm*1.1, PI=arm*0.5
let ammoDmgMult = 1.0;
if (ammoType === "Hollow point") { ammoDmgMult = 1.50; arm *= 1.50; }
else if (ammoType === "Incendiary") { ammoDmgMult = 1.40; arm *= 1.10; }
else if (ammoType === "Piercing") { arm *= 0.50; }
let deadlyChance = 0, ammoUsageMult = 1.0;
activeBonuses.forEach(bonus => {
const bN = bonus.name;
const p = bonus.val / 100;
const b = BONUS_DB[bN];
if (b) {
switch (b.scope) {
case "Turn_1": if (t === 1) tM *= (1 + p * b.weight); break;
case "Mag_1": if (bulletsSpent < effMag) tM *= (1 + p * b.weight); break;
case "Probabilistic":
tM *= (1 + p * b.weight);
if (b.type === "Multi-Hit") ammoUsageMult += p * b.weight;
break;
case "DOT": tM *= (1 + p * b.weight); break;
case "Threshold": if (p < 0.99) tM *= (1 / (1 - p)); break;
case "Special_Cycle": if (t % 2 !== 0) tM = 0; else tM *= (1 + p); break;
case "Scaling":
if (b.type === "Stat_Debuff") {
stacks = Math.min(3.0, stacks + p);
if (bN === "Weaken") currDef *= (1 - (stacks * 0.25));
else if (bN === "Cripple") currDex *= (1 - (stacks * 0.25));
} else if (b.type === "Stat_Buff") {
if (bN === "Motivation") { motivationStacks = Math.min(5, motivationStacks + p); currStr *= (1 + motivationStacks * 0.1); currSpd *= (1 + motivationStacks * 0.1); }
else if (bN === "Inspiration") { motivationStacks = Math.min(10, motivationStacks + p); currStr *= (1 + motivationStacks * b.weight); currSpd *= (1 + motivationStacks * b.weight); }
else if (bN === "Frenzy") {
tM *= (1 + frenzyStacks * p);
acc *= (1 + frenzyStacks * p);
}
} else {
if (bN === "Focus") {
} else {
stacks = Math.min(10, stacks + 1);
if (b.type === "Accuracy") acc *= (1 + stacks * p);
else if (b.type === "Damage_Taken") tM *= (1 + stacks * p);
}
}
break;
case "Permanent":
if (bN === "Deadly") deadlyChance = p;
if (b.type === "Armor" || bN === "Puncture") arm *= (1 - p);
if (bN === "Quicken") currSpd *= (1 + p);
else if (bN === "Empower") currStr *= (1 + p);
else if (b.type === "Accuracy") acc *= (1 + p * b.weight);
else if (b.type === "Damage" && bN !== "Puncture") tM *= (1 + p * b.weight);
if (b.type === "Mixed") { if (bN === "Berserk") { tM *= (1 + p); acc *= (1 - p/2); } if (bN === "Grace") { acc *= (1 + p); tM *= (1 - p/2); } }
break;
}
}
});
let probHit = getHitChance(currSpd, currDex, acc);
let focusBonus = activeBonuses.find(b => b.name === "Focus");
if (focusBonus) {
let focusP = focusBonus.val / 100;
probHit = Math.min(1.0, probHit + (focusStacks * focusP));
}
let turnDmg = 0;
if (tM > 0) {
let finalDamageMult = locAvgM;
if (deadlyChance > 0) finalDamageMult = (deadlyChance * 6.0) + ((1 - deadlyChance) * locAvgM);
let statDmgFactor = (currDef > 0) ? (currStr / (currStr + currDef)) / (MY_STATS.str / (MY_STATS.str + ENEMY_STATS.def)) : 1.0;
turnDmg = (baseDmg * ammoDmgMult * (1 - arm/100) * finalDamageMult * tM * probHit * statDmgFactor);
totalDmg += turnDmg;
}
history.push(turnDmg * 2);
if (activeBonuses.some(b => b.name === "Frenzy")) frenzyStacks = Math.min(20, probHit * (frenzyStacks + 1));
if (focusBonus) focusStacks = Math.min(20, (1 - probHit) * (focusStacks + 1));
if (w.mag > 0) { let shots = w.rof * ammoUsageMult; currentMag -= shots; bulletsSpent += shots; }
}
return {
score: (2 * totalDmg / SIM_LIMIT).toFixed(1),
history: history
};
}
// Simulate ADPT with custom stat overrides (for pentagon build comparison & armor)
// statOverride: { str, spd, def, dex } — absolute values in same scale as MY_STATS
// armorOverride: armor % value (numeric), null = use ARMOR_BASE
function simulateADPTCustom(name, baseDmg, baseAcc, activeBonuses, ammoType, statOverride, armorOverride) {
const w = WEAPON_DB[name] || { mag: 0, rof: 1.0 };
let conserveBonus = activeBonuses.find(b => b.name === "Conserve");
let effMag = w.mag;
if (conserveBonus) { effMag = w.mag / (1 - (conserveBonus.val / 100)); }
if (ammoType === "Tracer") baseAcc += 10.0;
let totalDmg = 0, bulletsSpent = 0, stacks = 0;
let frenzyStacks = 0, focusStacks = 0, motivationStacks = 0;
let currentMag = effMag;
const locAvgM = calculateAvgHitMultiplier(activeBonuses);
const hasSpecialist = activeBonuses.some(b => b.name === "Specialist");
const maxMags = hasSpecialist ? 1 : 3;
let magsUsed = 1;
let history_c = [];
const baseStr = (statOverride && statOverride.str != null) ? statOverride.str : MY_STATS.str;
const baseSpd = (statOverride && statOverride.spd != null) ? statOverride.spd : MY_STATS.spd;
const baseDef = (statOverride && statOverride.def != null) ? statOverride.def : ENEMY_STATS.def;
const baseDex = (statOverride && statOverride.dex != null) ? statOverride.dex : ENEMY_STATS.dex;
// Reference ratio for normalization (always the global baseline)
const refRatio = MY_STATS.str / (MY_STATS.str + ENEMY_STATS.def); // = 0.5
for (let t = 1; t <= SIM_LIMIT; t++) {
let currStr = baseStr, currSpd = baseSpd;
let currDef = baseDef, currDex = baseDex;
if (w.mag > 0 && currentMag <= 0) {
if (magsUsed >= maxMags) { history_c.push(0); continue; }
else { magsUsed++; currentMag = effMag; history_c.push(0); continue; }
}
let tM = 1.0;
let arm = (armorOverride != null) ? armorOverride : ARMOR_BASE;
let acc = baseAcc;
let ammoDmgMult = 1.0;
if (ammoType === "Hollow point") { ammoDmgMult = 1.50; arm *= 1.50; }
else if (ammoType === "Incendiary") { ammoDmgMult = 1.40; arm *= 1.10; }
else if (ammoType === "Piercing") { arm *= 0.50; }
let deadlyChance = 0, ammoUsageMult = 1.0;
activeBonuses.forEach(bonus => {
const bN = bonus.name; const p = bonus.val / 100; const b = BONUS_DB[bN];
if (b) {
switch (b.scope) {
case "Turn_1": if (t === 1) tM *= (1 + p * b.weight); break;
case "Mag_1": if (bulletsSpent < effMag) tM *= (1 + p * b.weight); break;
case "Probabilistic": tM *= (1 + p * b.weight); if (b.type === "Multi-Hit") ammoUsageMult += p * b.weight; break;
case "DOT": tM *= (1 + p * b.weight); break;
case "Threshold": if (p < 0.99) tM *= (1 / (1 - p)); break;
case "Special_Cycle": if (t % 2 !== 0) tM = 0; else tM *= (1 + p); break;
case "Scaling":
if (b.type === "Stat_Debuff") {
stacks = Math.min(3.0, stacks + p);
if (bN === "Weaken") currDef *= (1 - (stacks * 0.25));
else if (bN === "Cripple") currDex *= (1 - (stacks * 0.25));
} else if (b.type === "Stat_Buff") {
if (bN === "Motivation") { motivationStacks = Math.min(5, motivationStacks + p); currStr *= (1 + motivationStacks * 0.1); currSpd *= (1 + motivationStacks * 0.1); }
else if (bN === "Inspiration") { motivationStacks = Math.min(10, motivationStacks + p); currStr *= (1 + motivationStacks * b.weight); currSpd *= (1 + motivationStacks * b.weight); }
else if (bN === "Frenzy") { tM *= (1 + frenzyStacks * p); acc *= (1 + frenzyStacks * p); }
} else {
if (bN !== "Focus") { stacks = Math.min(10, stacks + 1); if (b.type === "Accuracy") acc *= (1 + stacks * p); else if (b.type === "Damage_Taken") tM *= (1 + stacks * p); }
}
break;
case "Permanent":
if (bN === "Deadly") deadlyChance = p;
if (b.type === "Armor" || bN === "Puncture") arm *= (1 - p);
if (bN === "Quicken") currSpd *= (1 + p);
else if (bN === "Empower") currStr *= (1 + p);
else if (b.type === "Accuracy") acc *= (1 + p * b.weight);
else if (b.type === "Damage" && bN !== "Puncture") tM *= (1 + p * b.weight);
if (b.type === "Mixed") { if (bN === "Berserk") { tM *= (1 + p); acc *= (1 - p/2); } if (bN === "Grace") { acc *= (1 + p); tM *= (1 - p/2); } }
break;
}
}
});
let probHit = getHitChance(currSpd, currDex, acc);
let focusBonus = activeBonuses.find(b => b.name === "Focus");
if (focusBonus) { let focusP = focusBonus.val / 100; probHit = Math.min(1.0, probHit + (focusStacks * focusP)); }
let turnDmg = 0;
if (tM > 0) {
let finalDamageMult = locAvgM;
if (deadlyChance > 0) finalDamageMult = (deadlyChance * 6.0) + ((1 - deadlyChance) * locAvgM);
// statDmgFactor: ratio of (our str vs their def) normalised to global baseline
let statDmgFactor = (currDef > 0) ? (currStr / (currStr + currDef)) / refRatio : 1.0;
turnDmg = (baseDmg * ammoDmgMult * (1 - arm/100) * finalDamageMult * tM * probHit * statDmgFactor);
totalDmg += turnDmg;
}
history_c.push(turnDmg * 2);
if (activeBonuses.some(b => b.name === "Frenzy")) frenzyStacks = Math.min(20, probHit * (frenzyStacks + 1));
if (focusBonus) focusStacks = Math.min(20, (1 - probHit) * (focusStacks + 1));
if (w.mag > 0) { let shots = w.rof * ammoUsageMult; currentMag -= shots; bulletsSpent += shots; }
}
return { score: parseFloat((2 * totalDmg / SIM_LIMIT).toFixed(1)), history: history_c };
}
// --- UI INJECTION FUNCTIONS ---
function getADPTColor(v) {
let val = Math.max(10, Math.min(v, 65));
return `hsl(${(val - 10) / 55 * 180}, 100%, 50%)`;
}
function showGraphTooltip(e, history, weaponContext) {
e.stopPropagation();
e.preventDefault();
let existing = document.getElementById('adpt-graph-tooltip');
if (existing) existing.remove();
const maxDmg = Math.max(...history, 1);
const minDmg = Math.min(...history);
// ── ARMOR OPTIONS (saved via PDA-compatible storage) ──────────────
// effectiveArmorMelee / effectiveArmorGun:
// Riot: base 50 + 30% melee reduction → melee sees (1-0.5)*0.7=0.35 dmg factor
// equivalent armor% = (1-0.35)*100 = 65% for melee; 50% for gun
// Assault: same logic swapped for gun
// Vanguard: 55% armor, no weapon-type reduction; +150% dex bonus handled separately
const ARMOR_OPTIONS = [
{ id: 'combat', label: 'Combat (40)', armorMelee: 40, armorGun: 40, dexMult: 1.0 },
{ id: 'riot', label: 'Riot (45) -30% melee', armorMelee: 58.5, armorGun: 45, dexMult: 1.0 },
{ id: 'assault', label: 'Assault (45) -30% gun',armorMelee: 45, armorGun: 58.5, dexMult: 1.0 },
{ id: 'vanguard', label: 'Vanguard (50) +150%dex',armorMelee: 50, armorGun: 50, dexMult: 2.5 }
];
let selectedArmorId = adptStorage.get('target_armor', 'combat');
// ── Detect weapon type: use isMelee stored in weaponContext at inject time ──
// Computed once from WEAPON_DB when the item was scanned; unknown = false (gun).
const isMelee = !!(weaponContext && weaponContext.isMelee);
// ── DEBUG ─────────────────────────────────────────────────────────
if (DEBUG && weaponContext) {
const wdbEntry = WEAPON_DB[weaponContext.name];
dbg(
'%c[ADPT Debug]%c ' + weaponContext.name,
'color:#4cf;font-weight:bold', 'color:#fff',
'\n Type :', isMelee ? '⚔️ MELEE' : '🔫 RANGED',
'\n isMelee :', isMelee,
'\n WEAPON_DB entry:', wdbEntry ? 'mag=' + wdbEntry.mag + ' rof=' + wdbEntry.rof : '❌ NOT FOUND (treated as ranged)',
'\n Armor :', selectedArmorId,
'\n baseDmg :', weaponContext.baseDmg,
'\n baseAcc :', weaponContext.baseAcc,
'\n bonuses :', weaponContext.activeBonuses.map(b => b.name + '(' + b.val + '%)').join(', ') || 'none'
);
const activeOpt = ARMOR_OPTIONS.find(o => o.id === selectedArmorId) || ARMOR_OPTIONS[0];
dbg(
'%c[ADPT Debug]%c Armor effect',
'color:#fca;font-weight:bold', 'color:#fff',
'\n Selected armor :', activeOpt.label,
'\n Effective arm% :', isMelee ? activeOpt.armorMelee + '% (melee)' : activeOpt.armorGun + '% (gun)',
'\n Dmg factor :', isMelee ? (1 - activeOpt.armorMelee/100).toFixed(3) : (1 - activeOpt.armorGun/100).toFixed(3),
'\n dexMult :', activeOpt.dexMult
);
}
// ── END DEBUG ─────────────────────────────────────────────────────
// ── Compute ADPT for selected armor ───────────────────────────────
// Armor simulation: use effective armor value that already encodes
// Riot -30% melee / Assault -30% gun as equivalent armor percentage.
// Vanguard dexMult scales enemy DEX (2.5x = base + 150%).
function getArmorADPT(armorId) {
if (!weaponContext) return null;
const opt = ARMOR_OPTIONS.find(o => o.id === armorId) || ARMOR_OPTIONS[0];
const armorVal = isMelee ? opt.armorMelee : opt.armorGun;
const dexMod = ENEMY_STATS.dex * opt.dexMult;
const statOverride = { str: MY_STATS.str, spd: MY_STATS.spd, def: ENEMY_STATS.def, dex: dexMod };
const result = simulateADPTCustom(
weaponContext.name, weaponContext.baseDmg, weaponContext.baseAcc,
weaponContext.activeBonuses, weaponContext.ammoType || '',
statOverride, armorVal
);
return { score: result.score.toFixed(1), history: result.history };
}
// ── Pentagon data: 5 build scenarios ─────────────────────────────
// Builds: Normal (balanced), No Strength, No Speed, No Dex, No Def
// "No X" means that stat is 1 (essentially zero contribution)
// Pentagon: enemy stat allocations from a 100-point pool
// Only def and dex affect the attacker's formulas.
// Attacker is fixed at 25/25 str/spd. Enemy varies def/dex allocation.
// P = pool unit = MY_STATS.str (1e9) represents 100 stat points
// 25 pts = 0.25*P, 33.3 pts = 0.333*P
function getBuildADPT(buildId) {
if (!weaponContext) return 0;
const P = MY_STATS.str; // = 1e9, represents 100 stat pts
// Attacker always fixed at 25 pts str, 25 pts spd
const atkStr = 0.25 * P;
const atkSpd = 0.25 * P;
let enemyDef, enemyDex;
// Enemy distributes 100 pts; "no X" means that stat = 0, rest split equally
switch (buildId) {
case 'normal':
enemyDef = 0.25 * P; enemyDex = 0.25 * P; break;
case 'no_str': // enemy 0str 33spd 33def 33dex
enemyDef = (1/3) * P; enemyDex = (1/3) * P; break;
case 'no_spd': // enemy 33str 0spd 33def 33dex
enemyDef = (1/3) * P; enemyDex = (1/3) * P; break;
case 'no_def': // enemy 33str 33spd 0def 33dex
enemyDef = 0.001 * P; enemyDex = (1/3) * P; break;
case 'no_dex': // enemy 33str 33spd 33def 0dex
enemyDef = (1/3) * P; enemyDex = 0.001 * P; break;
default:
enemyDef = 0.25 * P; enemyDex = 0.25 * P;
}
const activeOpt = ARMOR_OPTIONS.find(o => o.id === selectedArmorId) || ARMOR_OPTIONS[0];
// Effective armor already encodes Riot/Assault weapon-type reduction
const armorVal = isMelee ? activeOpt.armorMelee : activeOpt.armorGun;
// Vanguard dexMult scales enemy dex on top of build's dex allocation
const dexWithArmor = enemyDex * activeOpt.dexMult;
const statOverrideWithArmor = { str: atkStr, spd: atkSpd, def: enemyDef, dex: dexWithArmor };
return simulateADPTCustom(
weaponContext.name, weaponContext.baseDmg, weaponContext.baseAcc,
weaponContext.activeBonuses, weaponContext.ammoType || '',
statOverrideWithArmor, armorVal
).score;
}
const pentagonBuilds = [
{ id: 'normal', label: 'Normal' },
{ id: 'no_str', label: 'No STR' },
{ id: 'no_spd', label: 'No SPD' },
{ id: 'no_dex', label: 'No DEX' },
{ id: 'no_def', label: 'No DEF' }
];
// ── Build pentagon SVG ────────────────────────────────────────────
function buildPentagonSVG(scores, maxVal) {
const cx = 75, cy = 72, r = 55;
const n = scores.length;
const toXY = (i, frac) => {
const angle = (Math.PI * 2 * i / n) - Math.PI / 2;
return { x: cx + r * frac * Math.cos(angle), y: cy + r * frac * Math.sin(angle) };
};
// Grid lines
let gridLines = '';
[0.25, 0.5, 0.75, 1.0].forEach(frac => {
const pts = scores.map((_, i) => { const p = toXY(i, frac); return `${p.x},${p.y}`; }).join(' ');
gridLines += `<polygon points="${pts}" fill="none" stroke="#444" stroke-width="${frac === 1 ? 0.8 : 0.5}" opacity="0.7"/>`;
});
// Axis lines
let axisLines = '';
scores.forEach((_, i) => {
const outer = toXY(i, 1.0);
axisLines += `<line x1="${cx}" y1="${cy}" x2="${outer.x}" y2="${outer.y}" stroke="#555" stroke-width="0.5"/>`;
});
// Data polygon
const dataFracs = scores.map(s => Math.max(0.05, s.score / maxVal));
const dataPts = dataFracs.map((f, i) => { const p = toXY(i, f); return `${p.x},${p.y}`; }).join(' ');
// Labels
let labels = '';
scores.forEach((s, i) => {
const lp = toXY(i, 1.18);
const labelColor = s.id === 'normal' ? '#7af' : '#fa7';
labels += `<text x="${lp.x}" y="${lp.y}" text-anchor="middle" dominant-baseline="middle" font-size="7" fill="${labelColor}" font-weight="bold">${s.label}</text>`;
// Score label: follow actual data point (may be beyond outer ring if >100)
const dp = toXY(i, dataFracs[i]);
// Scores >100 get a gold highlight to show they overflow the scale
const valColor = s.score > maxVal ? '#ffd700' : '#fff';
labels += `<text x="${dp.x}" y="${dp.y - 4}" text-anchor="middle" font-size="6" fill="${valColor}" font-weight="${s.score > maxVal ? 'bold' : 'normal'}" opacity="0.95">${s.score.toFixed(0)}</text>`;
});
return `<svg width="150" height="145" xmlns="http://www.w3.org/2000/svg">
${gridLines}${axisLines}
<polygon points="${dataPts}" fill="rgba(100,200,255,0.25)" stroke="#4cf" stroke-width="1.5"/>
<circle cx="${cx}" cy="${cy}" r="2" fill="#4cf"/>
${labels}
<text x="${cx}" y="140" text-anchor="middle" font-size="7" fill="#888">Scale: 100 max | gold = over</text>
</svg>`;
}
// ── Main tooltip container ────────────────────────────────────────
const tooltip = document.createElement('div');
tooltip.id = 'adpt-graph-tooltip';
tooltip.style.cssText = `
position: absolute;
background: rgba(15, 15, 15, 0.97);
border: 1px solid #555;
border-radius: 6px;
padding: 6px 8px;
z-index: 999999;
box-shadow: 0 4px 14px rgba(0,0,0,0.9);
width: 170px;
pointer-events: auto;
color: white;
font-family: Arial, sans-serif;
font-size: 9px;
`;
// ── 1. Damage-per-turn bar chart (existing) ───────────────────────
const chartTitle = document.createElement('div');
chartTitle.style.cssText = 'font-size:8px; color:#aaa; margin-bottom:3px; text-align:center;';
chartTitle.textContent = 'Damage Per Turn';
tooltip.appendChild(chartTitle);
const mainArea = document.createElement('div');
mainArea.style.cssText = 'display: flex; height: 40px; width: 100%; gap: 4px; margin-bottom: 6px;';
const yAxis = document.createElement('div');
yAxis.style.cssText = 'display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end; font-size: 8px; color: #aaa; min-width: 22px; padding-bottom: 1px;';
const maxLabel = document.createElement('span');
const minLabel = document.createElement('span');
yAxis.appendChild(maxLabel);
yAxis.appendChild(minLabel);
const graphContainer = document.createElement('div');
graphContainer.style.cssText = 'display: flex; align-items: flex-end; flex: 1; height: 100%; gap: 1px; border-bottom: 1px solid #777; border-left: 1px solid #777;';
const renderBarChart = (histData) => {
const hi = Math.max(...histData, 1);
const lo = Math.min(...histData);
maxLabel.innerText = hi.toFixed(1);
minLabel.innerText = lo.toFixed(1);
graphContainer.innerHTML = '';
histData.forEach((dmg, idx) => {
const bar = document.createElement('div');
const h = hi > 0 ? (dmg / hi * 100) : 0;
bar.style.cssText = `flex: 1; background: ${getADPTColor(dmg)}; height: ${h}%; min-height: 1px; transition: height 0.15s, background 0.15s; cursor: crosshair;`;
bar.title = `Turn ${idx + 1}: ${dmg.toFixed(1)} dmg`;
bar.onmouseover = () => bar.style.opacity = '0.6';
bar.onmouseout = () => bar.style.opacity = '1';
graphContainer.appendChild(bar);
});
};
renderBarChart(history);
mainArea.appendChild(yAxis);
mainArea.appendChild(graphContainer);
tooltip.appendChild(mainArea);
// ── 2 & 3. Pentagon + Armor selector (single shared scope) ────────
if (weaponContext) {
// ── separator ──────────────────────────────────────────────────
const sep1 = document.createElement('div');
sep1.style.cssText = 'border-top: 1px solid #333; margin: 4px 0;';
tooltip.appendChild(sep1);
// ── Pentagon title ──────────────────────────────────────────────
const penTitle = document.createElement('div');
penTitle.style.cssText = 'font-size:8px; color:#7af; margin-bottom:2px; text-align:center; font-weight:bold;';
penTitle.textContent = '⬠ Build Impact Radar';
tooltip.appendChild(penTitle);
// ── Pentagon container ──────────────────────────────────────────
const penDiv = document.createElement('div');
penDiv.style.cssText = 'display:flex; justify-content:center; margin: 0 auto;';
tooltip.appendChild(penDiv);
// renderPentagon declared here — shared scope with armor radio handler
// Fixed outer scale = 100; scores >100 extend beyond the outer ring
const renderPentagon = () => {
const ps = pentagonBuilds.map(b => ({ ...b, score: getBuildADPT(b.id) }));
penDiv.innerHTML = buildPentagonSVG(ps, 100);
};
renderPentagon();
// ── separator ──────────────────────────────────────────────────
const sep2 = document.createElement('div');
sep2.style.cssText = 'border-top: 1px solid #333; margin: 6px 0 3px 0;';
tooltip.appendChild(sep2);
// ── Armor title ─────────────────────────────────────────────────
const armorTitle = document.createElement('div');
armorTitle.style.cssText = 'font-size:8px; color:#fca; margin-bottom:3px; text-align:center; font-weight:bold;';
armorTitle.textContent = '🛡 Target Armor';
tooltip.appendChild(armorTitle);
// ── ADPT-vs-armor display ───────────────────────────────────────
const adptDisplay = document.createElement('div');
adptDisplay.style.cssText = 'text-align:center; font-size:9px; margin-top:3px; padding:2px 4px; background:rgba(255,255,255,0.07); border-radius:3px;';
const updateArmorDisplay = (armorId) => {
const result = getArmorADPT(armorId);
const opt = ARMOR_OPTIONS.find(o => o.id === armorId);
const color = getADPTColor(parseFloat(result.score));
adptDisplay.innerHTML = `ADPT vs <b style="color:#fca">${opt.label.split(' ')[0]}</b>: <span style="color:${color};font-weight:bold;">${result.score}</span>`;
};
// ── Radio rows ──────────────────────────────────────────────────
const armorContainer = document.createElement('div');
armorContainer.style.cssText = 'display:flex; flex-direction:column; gap:3px;';
ARMOR_OPTIONS.forEach(opt => {
const row = document.createElement('label');
row.style.cssText = 'display:flex; align-items:center; gap:5px; cursor:pointer; padding:1px 3px; border-radius:3px; transition:background 0.15s;';
row.onmouseover = () => row.style.background = 'rgba(255,255,255,0.06)';
row.onmouseout = () => row.style.background = '';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'adpt-armor-target';
radio.value = opt.id;
radio.checked = (opt.id === selectedArmorId);
radio.style.cssText = 'cursor:pointer; accent-color:#4cf; margin:0; flex-shrink:0;';
const lbl = document.createElement('span');
lbl.style.cssText = 'font-size:8px; color:#ddd; flex:1;';
lbl.textContent = opt.label;
radio.addEventListener('change', () => {
selectedArmorId = opt.id;
adptStorage.set('target_armor', opt.id);
// Update armor ADPT text display
updateArmorDisplay(opt.id);
// Re-render pentagon with new armor
renderPentagon();
// Re-render bar chart with fresh history for new armor
const newResult = getArmorADPT(opt.id);
// DEBUG: log armor switch
dbg(
'%c[ADPT Debug]%c Armor changed → ' + opt.id,
'color:#fca;font-weight:bold', 'color:#fff',
'\n Weapon :', weaponContext ? weaponContext.name : 'n/a',
'\n isMelee :', isMelee,
'\n armorMelee% :', opt.armorMelee, '| armorGun%:', opt.armorGun,
'\n Applied arm%:', isMelee ? opt.armorMelee + ' (melee path)' : opt.armorGun + ' (gun path)',
'\n New ADPT :', newResult.score
);
renderBarChart(newResult.history);
// Force re-injection of all page ADPT badges with new armor
document.querySelectorAll('[data-v30="true"]').forEach(el => {
el.removeAttribute('data-v30');
el.removeAttribute('data-last-ammo');
});
mainInject();
});
row.appendChild(radio);
row.appendChild(lbl);
armorContainer.appendChild(row);
});
tooltip.appendChild(armorContainer);
tooltip.appendChild(adptDisplay);
updateArmorDisplay(selectedArmorId);
}
// ── Dismiss logic ─────────────────────────────────────────────────
// No stopPropagation on tooltip — would block radio label→input clicks
document.body.appendChild(tooltip);
const rect = e.target.getBoundingClientRect();
let left = rect.left + window.scrollX - 85 + (rect.width / 2);
let top = rect.top + window.scrollY - 60;
// Clamp to viewport
left = Math.max(5, Math.min(left, window.innerWidth + window.scrollX - 185));
top = Math.max(5, top);
tooltip.style.left = `${left}px`;
tooltip.style.top = `${top}px`;
const removeTooltip = (evt) => {
// Only remove if click is outside the tooltip
if (tooltip && tooltip.parentNode && !tooltip.contains(evt.target)) {
tooltip.remove();
document.removeEventListener('click', removeTooltip);
}
};
setTimeout(() => document.addEventListener('click', removeTooltip), 10);
}
function makeShowGraphHandler(historyToUse, n, d, a, activeBonuses, ammoType) {
const wData = WEAPON_DB[n];
// isMelee: weapon is in WEAPON_DB AND has mag===0 (swords/knives/blunt)
// Unknown weapons default to false (gun) — never mag:0 fallback for unknown
const isMeleeCtx = wData != null ? wData.mag === 0 : false;
const ctx = { name: n, baseDmg: d, baseAcc: a, activeBonuses: activeBonuses, ammoType: ammoType || '', isMelee: isMeleeCtx };
return function(e) { showGraphTooltip(e, historyToUse, ctx); };
}
function createADPTElement(displayText, adptValueForColor, activeBonuses, historyToUse, weaponCtx) {
const color = getADPTColor(parseFloat(adptValueForColor));
const div = document.createElement('div');
div.className = 'adpt-badge';
div.style.cssText = `color: ${color}; font-weight: bold; font-size: 12px; margin-top: 4px; border-top: 1px solid #333; padding: 2px 0; text-shadow: 1px 1px 0 #000; cursor: pointer;`;
div.innerHTML = displayText;
if(activeBonuses.length > 0) div.title = activeBonuses.map(b => b.name).join(' & ') + " (Click for Graph)";
div.addEventListener('click', (e) => showGraphTooltip(e, historyToUse, weaponCtx || null));
return div;
}
function injectItemMarket() {
// Tìm thẻ bao ngoài của item bằng chuỗi linh hoạt
document.querySelectorAll('[class*="itemTile_"]:not([data-v30])').forEach(item => {
try {
// Thêm chữ 'i' để không phân biệt hoa thường (Damage hay damage đều nhận)
const dmgEl = item.querySelector('[aria-label*="damage" i] span[aria-hidden="true"]');
const accEl = item.querySelector('[aria-label*="accuracy" i] span[aria-hidden="true"]');
if(!dmgEl || !accEl) return;
const d = parseFloat(dmgEl.innerText);
const a = parseFloat(accEl.innerText);
// Tìm thẻ Tên vũ khí (vd: Thompson)
const nameEl = item.querySelector('[class*="name_"]');
if(!nameEl) return;
const n = nameEl.innerText.trim();
let activeBonuses = [];
// Tìm các hiệu ứng vũ khí
item.querySelectorAll('[class*="bonuses_"] i, [data-bonus-name]').forEach(node => {
let bN = node.getAttribute('data-bonus-attachment-title') || node.getAttribute('data-bonus-name') || "";
let bV = 0;
const m = (node.getAttribute('aria-label') || "").match(/(\d+)/);
if (m) bV = parseInt(m[1]);
if (bN && BONUS_DB[bN]) activeBonuses.push({ name: bN, val: bV });
});
const adptStandard = simulateADPT(n, d, a, activeBonuses);
let displayText = `[ ${adptStandard.score} ]`, valueForColor = adptStandard.score;
let historyToUse = adptStandard.history;
if (JAPANESE_WEAPONS.includes(n)) {
const adptJap = simulateADPT(n, d, a, [...activeBonuses, {name: "Japanese Blade Mastery", val: 10}]);
displayText = `[${adptStandard.score} / ${adptJap.score}]`;
valueForColor = adptJap.score;
historyToUse = adptJap.history;
}
// [FIX QUAN TRỌNG]: Đặt Element ADPT vào đúng khu vực chứa tên vũ khí thay vì nút View Info
const titleContainer = nameEl.parentNode;
if(titleContainer) {
titleContainer.querySelectorAll('.adpt-badge').forEach(el => el.remove());
const _wM = WEAPON_DB[n]; const wCtxM = { name: n, baseDmg: d, baseAcc: a, activeBonuses: activeBonuses, ammoType: '', isMelee: _wM != null ? _wM.mag === 0 : false };
titleContainer.appendChild(createADPTElement(displayText, valueForColor, activeBonuses, historyToUse, wCtxM));
}
item.setAttribute('data-v30', 'true');
} catch (e) { console.error("ADPT Item Market Error:", e); }
});
}
function injectAuctionHouse() {
document.querySelectorAll('li .item-cont-wrap:not([data-v30])').forEach(wrap => {
try {
const nameEl = wrap.querySelector('.item-name'); if (!nameEl) return;
let n = nameEl.innerText.split('[')[0].trim();
const d = parseFloat(wrap.querySelector('.bonus-attachment-item-damage-bonus').parentElement.querySelector('.label-value').innerText);
const a = parseFloat(wrap.querySelector('.bonus-attachment-item-accuracy-bonus').parentElement.querySelector('.label-value').innerText);
let activeBonuses = [];
wrap.querySelectorAll('.bonus-attachment-icons').forEach(node => {
let titleText = document.createElement("textarea"); titleText.innerHTML = node.getAttribute('title') || "";
for (const key in BONUS_DB) {
if (titleText.value.includes(key)) {
const m = titleText.value.match(/(\d+)%/);
if (m) activeBonuses.push({ name: key, val: parseInt(m[1]) });
break;
}
}
});
const adptStandard = simulateADPT(n, d, a, activeBonuses);
let displayText = `[ ${adptStandard.score} ]`, valueForColor = adptStandard.score;
let historyToUse = adptStandard.history;
if (JAPANESE_WEAPONS.includes(n)) {
const adptJap = simulateADPT(n, d, a, [...activeBonuses, {name: "Japanese Blade Mastery", val: 10}]);
displayText = `[${adptStandard.score} / ${adptJap.score}]`;
valueForColor = adptJap.score;
historyToUse = adptJap.history;
}
const titleCont = wrap.querySelector('.title');
if (titleCont) {
titleCont.querySelectorAll('.adpt-badge').forEach(el => el.remove());
const _wA = WEAPON_DB[n]; const wCtxA = { name: n, baseDmg: d, baseAcc: a, activeBonuses: activeBonuses, ammoType: '', isMelee: _wA != null ? _wA.mag === 0 : false };
titleCont.appendChild(createADPTElement(displayText, valueForColor, activeBonuses, historyToUse, wCtxA));
}
wrap.setAttribute('data-v30', 'true');
} catch (e) { }
});
}
function injectInventory() {
document.querySelectorAll('li[data-weaponinfo="1"]:not([data-v30])').forEach(item => {
try {
const nameEl = item.querySelector('.name-wrap .name');
if (!nameEl) return;
const n = nameEl.innerText.trim();
const dmgIconEl = item.querySelector('.bonus-attachment-item-damage-bonus');
const accIconEl = item.querySelector('.bonus-attachment-item-accuracy-bonus');
if (!dmgIconEl || !accIconEl) return;
const d = parseFloat(dmgIconEl.nextElementSibling.innerText);
const a = parseFloat(accIconEl.nextElementSibling.innerText);
let activeBonuses = [];
item.querySelectorAll('.bonuses-wrap i[title]').forEach(node => {
let titleText = document.createElement("textarea");
titleText.innerHTML = node.getAttribute('title') || "";
for (const key in BONUS_DB) {
if (titleText.value.includes(key)) {
const m = titleText.value.match(/(\d+)%/);
if (m) activeBonuses.push({ name: key, val: parseInt(m[1]) });
break;
}
}
});
const adptStandard = simulateADPT(n, d, a, activeBonuses);
let displayText = `[ ${adptStandard.score} ]`, valueForColor = adptStandard.score;
let historyToUse = adptStandard.history;
if (JAPANESE_WEAPONS.includes(n)) {
const adptJap = simulateADPT(n, d, a, [...activeBonuses, {name: "Japanese Blade Mastery", val: 10}]);
displayText = `[${adptStandard.score}/${adptJap.score}]`;
valueForColor = adptJap.score;
historyToUse = adptJap.history;
}
nameEl.parentNode.querySelectorAll('.adpt-badge').forEach(el => el.remove());
const adptSpan = document.createElement('span');
adptSpan.className = 'adpt-badge';
const color = getADPTColor(parseFloat(valueForColor));
adptSpan.style.cssText = `color: ${color}; margin-right: 6px; font-weight: bold; font-size: 12px; text-shadow: 1px 1px 0 #000; background-color: rgba(0,0,0,0.3); padding: 1px 4px; border-radius: 3px; cursor: pointer;`;
adptSpan.innerHTML = displayText;
if(activeBonuses.length > 0) adptSpan.title = activeBonuses.map(b => b.name).join(' & ') + " (Click for Graph)";
adptSpan.addEventListener('click', makeShowGraphHandler(historyToUse, n, d, a, activeBonuses, ''));
nameEl.parentNode.insertBefore(adptSpan, nameEl);
item.setAttribute('data-v30', 'true');
} catch (e) { }
});
}
function injectFactionArmory() {
document.querySelectorAll('li:not([data-v30])').forEach(item => {
try {
// Determine if it's an armory item via data-armoryid
const armoryWrap = item.querySelector('.img-wrap[data-armoryid]');
if (!armoryWrap) return;
const nameEl = item.querySelector('.name');
if (!nameEl) return;
const n = nameEl.innerText.trim();
const dmgIconEl = item.querySelector('.bonus-attachment-item-damage-bonus');
const accIconEl = item.querySelector('.bonus-attachment-item-accuracy-bonus');
// Only process items that have weapons stats
if (!dmgIconEl || !accIconEl) return;
const d = parseFloat(dmgIconEl.nextElementSibling.innerText);
const a = parseFloat(accIconEl.nextElementSibling.innerText);
let activeBonuses = [];
// Retrieve bonuses inside ul.bonuses
item.querySelectorAll('.bonuses .bonus i').forEach(node => {
let titleText = document.createElement("textarea");
titleText.innerHTML = node.getAttribute('title') || node.getAttribute('aria-label') || node.getAttribute('data-bonus-name') || node.className || "";
for (const key in BONUS_DB) {
if (titleText.value.includes(key) || titleText.value.replace(/-/g, " ").includes(key.toLowerCase())) {
const m = titleText.value.match(/(\d+)%/);
if (m) {
activeBonuses.push({ name: key, val: parseInt(m[1]) });
} else {
const classMatch = node.className.match(/(\d+)/);
if (classMatch) {
activeBonuses.push({ name: key, val: parseInt(classMatch[1]) });
}
}
break;
}
}
});
const adptStandard = simulateADPT(n, d, a, activeBonuses);
let displayText = `[ ${adptStandard.score} ]`, valueForColor = adptStandard.score;
let historyToUse = adptStandard.history;
if (JAPANESE_WEAPONS.includes(n)) {
const adptJap = simulateADPT(n, d, a, [...activeBonuses, {name: "Japanese Blade Mastery", val: 10}]);
displayText = `[${adptStandard.score} / ${adptJap.score}]`;
valueForColor = adptJap.score;
historyToUse = adptJap.history;
}
// Remove old badge, then inject fresh one BEFORE the item name text
nameEl.querySelectorAll('.adpt-badge').forEach(el => el.remove());
const adptSpan = document.createElement('span');
adptSpan.className = 'adpt-badge';
const color = getADPTColor(parseFloat(valueForColor));
adptSpan.style.cssText = `color: ${color}; margin-right: 8px; font-weight: bold; font-size: 12px; text-shadow: 1px 1px 0 #000; cursor: pointer; display: inline-block;`;
adptSpan.innerHTML = displayText;
if(activeBonuses.length > 0) adptSpan.title = activeBonuses.map(b => b.name).join(' & ') + " (Click for Graph)";
adptSpan.addEventListener('click', makeShowGraphHandler(historyToUse, n, d, a, activeBonuses, ''));
nameEl.insertBefore(adptSpan, nameEl.firstChild);
item.setAttribute('data-v30', 'true');
} catch (e) { console.error("ADPT Faction Scan Error:", e); }
});
}
function injectItemReview() {
// Đổi .itemReview___b8uF1 thành [class*="itemReview_"]
document.querySelectorAll('[class*="itemReview_"]').forEach(review => {
try {
let ammoType = "";
// Đổi .ammo___fMLmp thành [class*="ammo_"]
const ammoEl = review.querySelector('[class*="ammo_"]');
if (ammoEl) {
const ammoLabel = ammoEl.getAttribute('aria-label') || "";
const ammoMatch = ammoLabel.match(/Ammo type is "([^"]+)"/);
if (ammoMatch) {
ammoType = ammoMatch[1];
}
}
const lastAmmo = review.getAttribute('data-last-ammo');
if (review.hasAttribute('data-v30') && lastAmmo === ammoType) {
return;
}
// Đổi .img___lW9P3 thành img[class*="img_"]
const nameEl = review.querySelector('img[class*="img_"]');
if (!nameEl) return;
const n = nameEl.getAttribute('alt').trim();
// Đổi .damage___NJy0A và .accuracy___udZQR
const dmgEl = review.querySelector('[class*="damage_"]');
const accEl = review.querySelector('[class*="accuracy_"]');
if (!dmgEl || !accEl) return;
const d = parseFloat(dmgEl.innerText);
const a = parseFloat(accEl.innerText);
let activeBonuses = [];
review.querySelectorAll('button[aria-label*="bonus:"]').forEach(btn => {
const label = btn.getAttribute('aria-label') || "";
const match = label.match(/"([^"]+)" bonus:.*?(\d+)%/);
if (match) {
const bN = match[1];
const bV = parseInt(match[2]);
if (BONUS_DB[bN]) {
activeBonuses.push({ name: bN, val: bV });
}
}
});
const adptStandard = simulateADPT(n, d, a, activeBonuses, ammoType);
let displayText = `[ ${adptStandard.score} ]`;
let valueForColor = adptStandard.score;
let historyToUse = adptStandard.history;
if (JAPANESE_WEAPONS.includes(n)) {
const adptJap = simulateADPT(n, d, a, [...activeBonuses, {name: "Japanese Blade Mastery", val: 10}], ammoType);
displayText = `[${adptStandard.score}/${adptJap.score}]`;
valueForColor = adptJap.score;
historyToUse = adptJap.history;
}
// Đổi .type___hrzuz thành [class*="type_"]
const typeEl = review.querySelector('[class*="type_"]');
if (typeEl) {
if (!typeEl.hasAttribute('data-orig-text')) {
typeEl.setAttribute('data-orig-text', typeEl.innerText.trim());
}
const origText = typeEl.getAttribute('data-orig-text');
const color = getADPTColor(parseFloat(valueForColor));
let titleTooltip = activeBonuses.map(b => b.name).join(' & ');
if (ammoType) titleTooltip += (titleTooltip ? ' + ' : '') + ammoType + ' Ammo';
titleTooltip += " (Click for Graph)";
typeEl.innerHTML = ``;
const adptSpan = document.createElement('span');
adptSpan.className = 'adpt-badge';
adptSpan.style.cssText = `color: ${color}; font-weight: bold; text-shadow: 1px 1px 0 #000; cursor: pointer;`;
adptSpan.title = titleTooltip;
adptSpan.innerHTML = displayText;
adptSpan.addEventListener('click', makeShowGraphHandler(historyToUse, n, d, a, activeBonuses, ammoType));
typeEl.appendChild(adptSpan);
}
review.setAttribute('data-v30', 'true');
review.setAttribute('data-last-ammo', ammoType);
} catch (e) { console.error("ADPT Item Review Error:", e); }
});
}
function mainInject() {
if (location.href.includes('amarket.php')) injectAuctionHouse();
else if (location.href.includes('item.php')) injectInventory();
else if (location.href.includes('factions.php')) injectFactionArmory();
else injectItemMarket();
injectItemReview();
}
const obs = new MutationObserver(mainInject);
obs.observe(document.body, { childList: true, subtree: true });
mainInject();
})();