// ==UserScript==
// @name Pokédex Edge for SangTacViet
// @namespace http://tampermonkey.net/
// @version 3.0
// @description Pokedex for SangTacViet pokemon novels.
// @author @playrough
// @license MIT
// @match *://sangtacviet.vip/truyen/*
// @match *://sangtacviet.pro/truyen/*
// @match *://sangtacviet.com/truyen/*
// @match *://sangtacviet.app/truyen/*
// @match *://sangtacviet.xyz/truyen/*
// @match *://14.225.254.182/truyen/*
// @icon https://i.postimg.cc/8kqyjRg7/pokeball.png
// @grant GM_addStyle
// @run-at document-start
// ==/UserScript==
(function () {
const PokemonApp = {
DATA_URLS: {
stats: 'https://raw.githubusercontent.com/playrough/stv-pokemon-name/main/pokemon-stats.json',
images: 'https://raw.githubusercontent.com/playrough/stv-pokemon-name/main/pokemon-name-image.json',
forms: 'https://raw.githubusercontent.com/playrough/stv-pokemon-name/main/pokemon-form.json',
abilities: 'https://raw.githubusercontent.com/playrough/stv-pokemon-name/main/pokemon-abilities.json',
moves: 'https://raw.githubusercontent.com/playrough/stv-pokemon-name/main/pokemon-moves.json',
types: 'https://raw.githubusercontent.com/playrough/stv-pokemon-name/main/pokemon-type.json',
typeChart: 'https://raw.githubusercontent.com/playrough/stv-pokemon-name/main/pokemon-type-chart.json'
},
EFFECTIVENESS: {
NO_EFFECT: 0,
NOT_VERY_EFFECTIVE: 0.5,
NEUTRAL: 1,
SUPER_EFFECTIVE: 2,
},
TYPE_COLORS: {
normal: '#8c8c8c',
fire: '#ff972f',
water: '#5aafdb',
electric: '#f1b700',
grass: '#2b9734',
ice: '#59c0c1',
fighting: '#c44353',
poison: '#c11bde',
ground: '#cd7b40',
flying: '#8f94f3',
psychic: '#ff5c56',
bug: '#76bc00',
rock: '#b59f5a',
ghost: '#7048b6',
dragon: '#1876c5',
dark: '#a575d1',
steel: '#2b7a8e',
fairy: '#eb5bbf'
},
data: {
stats: null,
images: null,
forms: null,
abilities: null,
moves: null,
types: null,
typeChart: null
},
init() {
window.addEventListener("DOMContentLoaded", () => {
const targetNode = document.querySelector("#content-container .contentbox");
if (!targetNode) return;
const observer = new MutationObserver((mutationsList, observer) => {
if (document.querySelector("#content-container .contentbox i")) {
observer.disconnect();
this.run();
}
});
observer.observe(targetNode, {
childList: true,
subtree: true
});
if (document.querySelector("#content-container .contentbox i")) {
observer.disconnect();
this.run();
}
});
},
run() {
setTimeout(async () => {
try {
await this.loadAllData();
this.injectStyles();
this.renderPokemonElements();
this.setupAutoReload();
} catch (e) {
console.error("Initialization error:", e);
}
}, 1000);
},
async loadAllData() {
const requests = Object.entries(this.DATA_URLS).map(async ([key, url]) => {
const response = await fetch(url);
this.data[key] = await response.json();
});
await Promise.all(requests);
},
setupAutoReload() {
const actions = [
"addSuperName('hv','z')", "addSuperName('hv','f')", "addSuperName('hv','s')",
"addSuperName('hv','l')", "addSuperName('hv','a')", "addSuperName('el')",
"addSuperName('vp')", "addSuperName('kn')", 'saveNS();excute();', 'excute()'
];
actions.forEach(action => {
const button = document.querySelector(`[onclick="${action}"]`);
if (button) {
button.addEventListener('click', () => this.renderPokemonElements());
}
});
},
renderPokemonElements() {
const contentBoxes = document.querySelectorAll("#content-container .contentbox i");
contentBoxes.forEach(box => {
const text = box.textContent.trim();
const html = this.getElementHTML(text);
if (html && html !== text) {
box.innerHTML = html;
}
});
this.setupEventListeners();
},
getElementHTML(name) {
const { images, forms, abilities, moves, types } = this.data;
if (images[name]) {
return forms[name]
? this.generateFormImagesHTML(name)
: this.generatePokemonImageHTML(name);
}
if (abilities[name]) return this.generateAbilityHTML(name);
if (moves[name]) return this.generateMoveHTML(name);
if (types[name]) return this.generateTypeHTML(name);
return name;
},
generatePokemonImageHTML(name) {
return `${name}<img class="pokemon-image" src="${this.data.images[name]}" loading="lazy" data-pokemon="${name}">`;
},
generateFormImagesHTML(name) {
const images = this.data.forms[name]
.map(form =>
`<img class="pokemon-image" src="${this.data.images[form]}" loading="lazy" data-pokemon="${form}">`
).join("");
return `${name}${images}`;
},
generateAbilityHTML(name) {
const icon = "Ability";
return `<span>${name}</span><img class="pokemon-ability" src="${this.data.images[icon]}" loading="lazy" data-ability="${name}">`;
},
generateMoveHTML(name) {
const type = this.data.moves[name].type || '';
return `<span class="${type}">${name}</span><img class="pokemon-move" src="${this.data.types[type]}" loading="lazy" data-move="${name}">`;
},
generateTypeHTML(name) {
return `<span class="${name}">${name}</span><img class="pokemon-type" src="${this.data.types[name]}" loading="lazy" data-type="${name}">`;
},
setupEventListeners() {
this.addClickListener("img.pokemon-image", (element) => {
const name = element.dataset.pokemon;
if (this.data.stats[name]) this.showPokemonInfoPopup(this.data.stats[name]);
});
this.addClickListener("img.pokemon-ability", (element) => {
const name = element.dataset.ability;
if (this.data.abilities[name]) this.showAbilityInfoPopup(this.data.abilities[name]);
});
this.addClickListener("img.pokemon-move", (element) => {
const name = element.dataset.move;
if (this.data.moves[name]) this.showMoveInfoPopup(this.data.moves[name]);
});
this.addClickListener("img.pokemon-type", (element) => {
const name = element.dataset.type;
if (this.data.types[name] && this.data.typeChart) {
const typeData = this.setupTypeData(name, this.data.typeChart);
this.showTypeInfoPopup(name, typeData);
}
});
},
addClickListener(selector, callback) {
document.querySelectorAll(selector).forEach(element => {
element.addEventListener("click", (e) => {
e.stopPropagation();
callback(element);
});
});
},
showPokemonInfoPopup(pokemon) {
this.removeExistingPopup();
const popup = this.createPopup(`
<div class="popup-header">
<h2>
<span class="pokemon-name">${pokemon.name} </span>
<span class="pokemon-number">${pokemon.number}</span>
</h2>
<button id="close-pokemon-info" title="Đóng">×</button>
</div>
<p><b>Type:</b> ${this.formatTypes(pokemon.type)}</p>
<p><b>Abilities:</b> ${this.formatAbilities(pokemon.abilities)}</p>
<div class="stat-grid">
${this.generateStatHTML('HP', pokemon.hp)}
${this.generateStatHTML('Attack', pokemon.attack)}
${this.generateStatHTML('Defense', pokemon.defense)}
${this.generateStatHTML('Sp. Atk', pokemon.spAttack)}
${this.generateStatHTML('Sp. Def', pokemon.spDefense)}
${this.generateStatHTML('Speed', pokemon.speed)}
<div class="total-stat"><span>Total</span><strong>${pokemon.total}</strong></div>
</div>
`);
document.body.appendChild(popup);
this.setupPopupCloseButton(popup);
},
showAbilityInfoPopup(ability) {
this.removeExistingPopup();
const popup = this.createPopup(`
<div class="popup-header">
<h2>${ability.name}</h2>
<button id="close-pokemon-info" title="Đóng">×</button>
</div>
<p>${ability.description}</p>
`);
document.body.appendChild(popup);
this.setupPopupCloseButton(popup);
},
showMoveInfoPopup(move) {
this.removeExistingPopup();
const popup = this.createPopup(`
<div class="popup-header">
<h2>${move.name}</h2>
<button id="close-pokemon-info" title="Đóng">×</button>
</div>
<p><b>Type:</b> <span class="${move.type}">${move.type}</span></p>
<p><b>Category:</b> ${move.category}</p>
<p><b>Power:</b> ${move.power || '—'}</p>
<p><b>Accuracy:</b> ${move.accuracy || '—'}</p>
<p><b>PP:</b> ${move.pp}</p>
<p><b>Effect:</b> ${move.effect}</p>
`);
document.body.appendChild(popup);
this.setupPopupCloseButton(popup);
},
showTypeInfoPopup(typeName, typeData) {
this.removeExistingPopup();
const popup = this.createPopup(`
<div class="popup-header">
<h2>${typeName} Type</h2>
<button id="close-pokemon-info" title="Đóng">×</button>
</div>
<h4>Attack Effectiveness</h4>
${this.renderEffectivenessLine("Super effective against", typeData.attackEffectiveness.superEffective)}
${this.renderEffectivenessLine("Not very effective against", typeData.attackEffectiveness.notVeryEffective)}
${this.renderEffectivenessLine("No effect against", typeData.attackEffectiveness.noEffect)}
<h4>Defense Effectiveness</h4>
${this.renderEffectivenessLine("Weak to", typeData.defenseEffectiveness.superEffective)}
${this.renderEffectivenessLine("Resists", typeData.defenseEffectiveness.notVeryEffective)}
${this.renderEffectivenessLine("Immune to", typeData.defenseEffectiveness.noEffect)}
`);
document.body.appendChild(popup);
this.setupPopupCloseButton(popup);
},
createPopup(content) {
const popup = document.createElement("div");
popup.id = "pokemon-info-popup";
popup.innerHTML = content;
return popup;
},
setupPopupCloseButton(popup) {
popup.querySelector("#close-pokemon-info").addEventListener("click", (e) => {
e.stopPropagation();
popup.remove();
});
},
removeExistingPopup() {
const existingPopup = document.getElementById("pokemon-info-popup");
if (existingPopup) existingPopup.remove();
},
formatTypes(types) {
return types.map(type => `<span class="${type}">${type}</span>`).join(", ");
},
formatAbilities(abilities) {
let arr = [];
if (abilities.ability1) arr.push(abilities.ability1);
if (abilities.ability2) arr.push(abilities.ability2);
if (abilities.hidden) arr.push(`<i>(Hidden)</i> ${abilities.hidden}`);
return arr.join(", ");
},
generateStatHTML(label, value) {
return `<div><span>${label}</span><strong>${value}</strong></div>`;
},
renderEffectivenessLine(label, types) {
if (!types || types.length === 0) return "";
return `<p><b>${label}:</b> ${types.map(t => `<span class="${t}" data-type="${t}">${t}</span>`).join(", ")}</p>`;
},
setupTypeData(type, typeChart) {
return {
type,
attackEffectiveness: this.getTypeEffectiveness(type, typeChart),
defenseEffectiveness: this.getDefenseEffectiveness(type, typeChart),
};
},
getTypeEffectiveness(attackType, typeChart) {
const effectiveness = {
noEffect: [],
notVeryEffective: [],
superEffective: [],
};
Object.entries(typeChart[attackType]).forEach(([defenseType, value]) => {
if (value === this.EFFECTIVENESS.NO_EFFECT) effectiveness.noEffect.push(defenseType);
else if (value === this.EFFECTIVENESS.NOT_VERY_EFFECTIVE) effectiveness.notVeryEffective.push(defenseType);
else if (value === this.EFFECTIVENESS.SUPER_EFFECTIVE) effectiveness.superEffective.push(defenseType);
});
return effectiveness;
},
getDefenseEffectiveness(defenseType, typeChart) {
const effectiveness = {
noEffect: [],
notVeryEffective: [],
superEffective: [],
};
Object.entries(typeChart).forEach(([attackType, values]) => {
const defenseEffectiveness = values[defenseType];
if (defenseEffectiveness === this.EFFECTIVENESS.NO_EFFECT) effectiveness.noEffect.push(attackType);
else if (defenseEffectiveness === this.EFFECTIVENESS.NOT_VERY_EFFECTIVE) effectiveness.notVeryEffective.push(attackType);
else if (defenseEffectiveness === this.EFFECTIVENESS.SUPER_EFFECTIVE) effectiveness.superEffective.push(attackType);
});
return effectiveness;
},
injectStyles() {
let typeCss = Object.entries(this.TYPE_COLORS).map(([type, color]) => `
.${type} {
color: ${color};
}
`).join('');
GM_addStyle(`
${typeCss}
.pokemon-image,
.pokemon-move,
.pokemon-ability,
.pokemon-type {
display: inline-block !important;
margin: 0 !important;
margin-left: 2px !important;
margin-bottom: 2px !important;
cursor: pointer;
vertical-align: middle;
transition: transform 0.2s ease;
}
.pokemon-image {
width: 45px;
height: 45px;
}
.pokemon-ability {
width: 25px;
height: 25px;
}
.pokemon-move,
.pokemon-type {
width: 20px;
height: 20px;
}
.pokemon-image:hover,
.pokemon-move:hover,
.pokemon-ability:hover,
.pokemon-type:hover {
transform: scale(1.3);
}
#pokemon-info-popup {
position: fixed;
bottom: 20px;
right: 20px;
color: rgb(75, 75, 75);
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
padding: 20px;
width: 300px;
max-height: 80vh;
overflow-y: auto;
font: inherit;
font-weight: 300;
z-index: 10001;
animation: fadeIn 0.3s ease;
}
#pokemon-info-popup .popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
#pokemon-info-popup h2 {
margin: 0;
font-size: 20px;
text-align: left;
}
#pokemon-info-popup h4 {
margin: 15px 0 5px 0;
font-size: 16px;
color: #555;
}
#pokemon-info-popup p {
margin: 5px 0;
font-size: 14px;
line-height: 1.4;
}
.pokemon-name {
margin-right: 5px;
}
.pokemon-number {
display: inline-block;
margin-top: 2px;
color: #999;
font-size: 14px;
}
#close-pokemon-info {
background: #eee;
border: none;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 24px;
line-height: 36px;
text-align: center;
cursor: pointer;
color: #444;
transition: background 0.2s, transform 0.2s;
outline: none;
}
#close-pokemon-info:hover {
background: #ccc;
transform: scale(1.1);
}
.stat-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 15px;
}
.stat-grid div {
background: #f5f5f5;
padding: 8px 12px;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
}
.stat-grid span {
font-size: 12px;
color: #555;
}
.stat-grid strong {
font-size: 16px;
font-weight: bold;
}
.total-stat {
grid-column: span 2;
background: #dff0d8;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`);
}
};
PokemonApp.init();
})();