Instantly share code, notes, and snippets.
Last active
February 19, 2025 10:33
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save eugeny-dementev/c9c61e7a5d1e2b2e35f2402febabb110 to your computer and use it in GitHub Desktop.
Path of Exile 2 Trade Copy Item Button
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name PoE2 Trade Item Copy | |
// @namespace http://tampermonkey.net/ | |
// @version 1.0.1 | |
// @description Enable copy item button on https://www.pathofexile.com/trade2/search/poe2 | |
// @author Eugeny Dementev | |
// @match https://www.pathofexile.com/trade2* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=pathofexile.com | |
// @grant none | |
// ==/UserScript== | |
(function () { | |
'use strict'; | |
// Set up the MutationObserver for each item row appearing on trade site | |
const observer = new MutationObserver((mutationsList) => { | |
mutationsList.forEach((mutation) => { | |
if (mutation.type === 'childList') { | |
mutation.addedNodes.forEach((node) => { | |
// Check if the added node is a <div class="row"> | |
if (node.nodeType === 1 && node.matches('div.row')) { | |
showCopyItemButton(node); | |
} | |
}); | |
} | |
}); | |
}); | |
// Start observing the entire document body | |
observer.observe(document.body, { | |
childList: true, // Observe direct child additions/removals | |
subtree: true, // Observe all descendants | |
}); | |
} | |
)(); | |
const SEPARATOR = '--------'; | |
function extractItemClass(middleElem) { | |
return middleElem.querySelector('.property .lc').textContent.trim(); | |
} | |
function extractItemName(middleElem) { | |
return middleElem.querySelector('.itemName .lc').textContent.trim(); | |
} | |
function extractItemBase(middleElem) { | |
return middleElem.querySelector('.itemName.typeLine .lc').textContent.trim(); | |
} | |
function extractItemRarity(middleElem) { | |
const itemPopupContainer = middleElem.querySelector('.itemPopupContainer'); | |
if (itemPopupContainer.classList.contains('uniquePopup')) { | |
return 'Unique'; | |
} else if (itemPopupContainer.classList.contains('rarePopup')) { | |
return 'Rare'; | |
} else if (itemPopupContainer.classList.contains('magicPopup')) { | |
return 'Magic'; | |
} else if (itemPopupContainer.classList.contains('normalPopup')) { | |
return 'Normal'; | |
} else { | |
return 'Unknown'; | |
} | |
} | |
const ANY_WEAPON_CLASSES = [ // have phys/chaos damage, crit chance and attack per speed | |
'Bow', | |
'Crossbow', | |
'Axe', | |
'One Hand Mace', | |
'Two Hand Mace', | |
'Flail', | |
'Quarterstaff', | |
'Spear', | |
'Claw', | |
'Dagger', | |
// 'Trap', // not exactly an attack weapon | |
'Sword', | |
]; | |
const ARMOUR_CLASSES = [ | |
'Body Armour', | |
'Gloves', | |
'Helmets', | |
'Boots', | |
]; | |
const SCEPTRE_CLASSES = [ | |
'Sceptre', | |
]; | |
class Item { | |
// all items has more or less the same section for them | |
// - Base (item class, rarity, name, base) | |
// - Item Base stats (evasion, armour, energy shield, spirit, quality) | |
// - Requirements, level, dex, int, str | |
// - (optional) Amount of sockets in item | |
// - Item Level (separate section specifically for item level) | |
// - (Optional) Enchant from corrupt (enchant) | |
// - (Optional) Runes effects (rune) | |
// - (Optional) Implicit mod (implicit) | |
// - Mods section, prefixes and suffixes | |
// - (Optional) Item description (only for items with instruction on how to use and unique items) | |
// - Corrupt/Mirror tag | |
getSections() { | |
return [ | |
this.getBaseInfoSection()?.trim(), | |
this.getBaseStatsSection()?.trim(), | |
this.getRequirementsSection()?.trim(), | |
this.getAmountOfSocketsSection()?.trim(), | |
this.getItemLevelSection()?.trim(), | |
this.getEnchantSection()?.trim(), | |
this.getRunesEffectSection()?.trim(), | |
this.getImplicitModSection()?.trim(), | |
this.getModsSection()?.trim(), | |
this.getDescriptionSection()?.trim(), | |
this.getCorruptMirrorSection()?.trim(), | |
].filter(v => Boolean(v)); | |
} | |
toString() { | |
return this | |
.getSections() | |
.join(`\n${SEPARATOR}\n`); | |
} | |
middleElem | |
itemClass | |
itemRarity | |
itemName | |
itemBase | |
itemQuality | |
itemLvlRequirement | |
itemDexRequirement | |
itemIntRequirement | |
itemStrRequirement | |
itemMods // array of text lines | |
constructor(middleElem) { | |
this.middleElem = middleElem; | |
this.extractBaseInfo(); | |
this.extractRequirements(); | |
} | |
extractBaseInfo() { | |
this.itemClass = extractItemClass(this.middleElem); | |
this.itemRarity = extractItemRarity(this.middleElem); | |
this.itemName = extractItemName(this.middleElem); | |
this.itemBase = extractItemBase(this.middleElem) | |
this.itemQuality = this.middleElem.querySelector('[data-field="quality"]')?.textContent.trim() || undefined; | |
this.itemImplicitMod = this.middleElem.querySelector('.implicitMod .lc.s')?.textContent.trim() || undefined; | |
this.itemMods = this.extractItemMods(); | |
} | |
extractItemMods() { | |
return Array.from(this.middleElem.querySelectorAll('.explicitMod .lc.s')).map(modElem => modElem.textContent.trim()); | |
} | |
getBaseInfoSection() { | |
return [ | |
`Item Class: ${this.itemClass}`, | |
`Rarity: ${this.itemRarity}`, | |
this.itemName, | |
this.itemBase, | |
].join('\n'); | |
} | |
getDescriptionSection() { } | |
getCorruptMirrorSection() { | |
const corrupted = this.middleElem.querySelector('.unmet')?.textContent.trim(); | |
if (corrupted) { | |
return corrupted; | |
} | |
const mirrored = this.middleElem.querySelector('.augmented')?.textContent.trim(); | |
if (mirrored) { | |
return mirrored; | |
} | |
} | |
getBaseDataField(selector) { | |
const selectedElem = this.middleElem.querySelector(selector) | |
let elemeTextValue = selectedElem?.textContent.trim() || undefined; | |
if (!elemeTextValue) { | |
return; | |
} | |
if (selectedElem.querySelector('.colourAugmented')) { | |
elemeTextValue = `${elemeTextValue} (augmented)`; | |
} | |
return elemeTextValue; | |
} | |
getElemDamageDataField() { | |
const selectedElem = this.middleElem.querySelector('[data-field="edamage"]'); | |
const elemDamage = selectedElem?.textContent.trim() || undefined; | |
if (!elemDamage) { | |
return; | |
} | |
return `${elemDamage} (augmented)`; | |
} | |
getRadiusDataField() { | |
const selectedElems = this.middleElem.querySelectorAll('.property .lc'); | |
if (selectedElems.length < 1) { | |
return; | |
} | |
const radiusPropElem = Array.from(selectedElems).find(elem => elem.textContent.includes('Radius')) | |
if (!radiusPropElem) { | |
return; | |
} | |
let elemeTextValue = radiusPropElem.textContent; | |
if (radiusPropElem.querySelector('.colourAugmented')) { | |
elemeTextValue = `${elemeTextValue} (augmented)`; | |
} | |
return elemeTextValue; | |
} | |
getLimitDataField() { | |
const selectedElems = this.middleElem.querySelectorAll('.property .lc'); | |
if (selectedElems.length < 1) { | |
return; | |
} | |
const limitedToPropElem = Array.from(selectedElems).find(elem => elem.textContent.includes('Limited to')) | |
if (!limitedToPropElem) { | |
return; | |
} | |
let elemeTextValue = limitedToPropElem.textContent; | |
if (limitedToPropElem.querySelector('.colourAugmented')) { | |
elemeTextValue = `${elemeTextValue} (augmented)`; | |
} | |
return elemeTextValue; | |
} | |
getBaseStatsSection() { | |
const physicalDamage = this.getBaseDataField('[data-field="pdamage"]'); | |
const elementalDamage = this.getElemDamageDataField(); | |
const chaosDamage = this.getBaseDataField('[data-field="cdamage"]'); | |
const critChance = this.getBaseDataField('[data-field="crit"]'); | |
const attacksPerSecond = this.getBaseDataField('[data-field="aps"]'); | |
const armour = this.getBaseDataField('[data-field="ar"]'); | |
const evasion = this.getBaseDataField('[data-field="ev"]'); | |
const energyShield = this.getBaseDataField('[data-field="es"]'); | |
const spirit = this.getBaseDataField('[data-field="spirit"]'); | |
const limitedTo = this.getLimitDataField(); | |
const radius = this.getRadiusDataField(); | |
return [ | |
this.itemQuality && `${this.itemQuality} (augmented)`, | |
physicalDamage, | |
elementalDamage, | |
chaosDamage, | |
critChance, | |
attacksPerSecond, | |
armour, | |
evasion, | |
energyShield, | |
spirit, | |
limitedTo, | |
radius, | |
].filter(v => Boolean(v)).join('\n'); | |
} | |
getRequirementsSection() { | |
return [ | |
this.itemLvlRequirement, | |
this.itemStrRequirement, | |
this.itemDexRequirement, | |
this.itemIntRequirement, | |
].filter(v => Boolean(v)).join('\n'); | |
} | |
getAmountOfSocketsSection() { | |
const countSockets = this.middleElem.parentElement.querySelector('.left').querySelectorAll('.socket.socket--rune').length; | |
const value = new Array(countSockets).fill('S').join(' '); | |
if (value.length === 0) { | |
return; | |
} | |
return `Sockets: ${value}`; | |
} | |
getItemLevelSection() { | |
return this.middleElem.querySelector('[data-field="ilvl"]')?.textContent.trim(); | |
} | |
getEnchantSection() { | |
const enchantMods = Array | |
.from(this.middleElem.querySelectorAll('.enchantMod .lc.s')) | |
.map(enchantModElem => enchantModElem.textContent.trim()) | |
.map(enchantMod => `${enchantMod} (enchant)`); | |
return enchantMods.join('\n'); | |
} | |
getRunesEffectSection() { | |
const runeMods = Array | |
.from(this.middleElem.querySelectorAll('.runeMod .lc.s')) | |
.map(runeModElem => runeModElem.textContent.trim()) | |
.map(mod => `${mod} (rune)`); | |
return runeMods.join('\n'); | |
} | |
getModsSection() { | |
return this.itemMods.join('\n') | |
} | |
getImplicitModSection() { | |
const implicitMods = Array | |
.from(this.middleElem.querySelectorAll('.implicitMod .lc.s')) | |
.map(implicitModElem => implicitModElem.textContent.trim()) | |
.map(implicitMod => `${implicitMod} (implicit)`); | |
return implicitMods.join('\n'); | |
} | |
extractRequirements() { | |
this.itemLvlRequirement = this.middleElem.querySelector('[data-field="lvl"]')?.textContent.split(' ').join(': ') || undefined; | |
this.itemDexRequirement = this.middleElem.querySelector('[data-field="dex"]')?.textContent.split(' ').reverse().join(': ') || undefined; | |
this.itemIntRequirement = this.middleElem.querySelector('[data-field="int"]')?.textContent.split(' ').reverse().join(': ') || undefined; | |
this.itemStrRequirement = this.middleElem.querySelector('[data-field="str"]')?.textContent.split(' ').reverse().join(': ') || undefined; | |
} | |
} | |
function showCopyItemButton(row) { | |
const copyButton = row.querySelector('button.copy') | |
if (copyButton && copyButton.classList.contains('hidden')) { | |
copyButton.classList.remove('hidden'); | |
copyButton.style = undefined | |
copyButton.onclick = () => { | |
const middleElem = row.querySelector('div.middle'); | |
const itemInfoNew = extractItemDetails2(middleElem); | |
console.log(itemInfoNew); | |
navigator | |
.clipboard | |
.writeText(itemInfoNew) | |
.catch((err) => { | |
console.log('Failed to copy item'); | |
console.error(err); | |
}) | |
} | |
} | |
} | |
function getItemClass(itemClass) { | |
return Item; | |
} | |
function extractItemDetails2(middleElem) { | |
const item = new Item(middleElem); | |
return `${item}`; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment