hv-monsterdb-userscript
Version:
M-M-M-MONSTER DATABASE!
2,339 lines • 97.6 kB
JavaScript
// ==UserScript==
// @name HentaiVerse Monster Database UserScript
// @description M-M-M-MONSTER DATABASE!
// @namespace https://hentaiverse.org
// @run-at document-idle
// @match *://*.hentaiverse.org/*
// @exclude http*://hentaiverse.org/pages/showequip.php?*
// @exclude *hentaiverse.org/equip/*
// @version 2.0.0
// @author Sukka <https://skk.moe>
// @grant unsafeWindow
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// ==/UserScript==
// ============= vvv SETTINGS vvv =============
const SETTINGS = {
/**
* Debug
*
* true - enable verbose output in console, and access to debug method
* false - disable verbose output in console (default)
*/
debug: false,
/**
* Scan Expire Date
*
* Monsters that haven't been scanned in this number of days will be considered expired.
* In the Isekai, monsters will only expire once per season.
*/
scanExpireDays: 45,
/**
* Scan Highlight Color
*
* Highlight expired monsters (haven't been scanned in "scanExpireDays").
* Set to "false" (without quote) will disable the expired highlight feature.
*
* (In order to be compatible with Monsterbation's "monsterKeyword", only monster letters part will be highlighted)
*/
scanHighlightColor: 'coral',
/**
* Monster Info Box
*
* Monster Database Script provides a draggable float box during battle. If you don't
* like the ui, you can disable it here.
*/
showMonsterInfoBox: true,
/**
* Compact Monster Info Box
*
* true - only show trainer, PL, monster class in the float box, mitigation data will be hidden
* false - show all mitigation data along with trainer, PL, monster class in the float box (default)
*/
compactMonsterInfoBox: false,
/**
* Highlight Monster
*
* Similar to Monsterbation's "monsterKeyword" feature, highlight monsters where the name, id, class, attack matches.
* (In order to be compatible with Monsterbation's "monsterKeyword", only monster letters part will be highlighted)
*
* The configuration accepts an object. The key of the object accepts the color, supports any valid CSS color.
* The value of the object accepts a RegExp (same syntax as Monsterbation 1.3.2.1) or a function that returns boolean value.
*/
highlightMonster: {
/* This matches monsters whose trainer is Noni */
// '#66ccff': /"trainer":"Noni"/,
/**
* This matches monsters
* whose name (or whose trainer name) contains Meiling,
* OR whose PL is 2250,
* OR whose monster id is 70699,
* OR whose monster class is Undead AND attack type is Crushing
*/
// 'red': /(Meiling|"plvl":2250|"monsterId":70699|Undead.*Crushing)/
/**
* If you are an advanced player who can write javascript, I promise you will love this
*
* You can find the type definition of monsterInfo here:
* https://suka.js.org/hv-monsterdb-userscript/interfaces/hvmonsterdatabase.monsterinfo.html
*/
// 'rgb(28, 46, 69)': (monsterInfo) => {
// if (monsterInfo.monsterName.includes('Meiling')) return true;
// if (monsterInfo.plvl > 1700) return true;
// if (monsterInfo.monsterClass !== 'Giant') return true;
// if (monsterInfo.attack === 'Piercing' || monsterInfo.attack === 'Crushing') return true;
// return false
// }
},
/**
* Dark Mode
*
* Made by @raraha (https://forums.e-hentai.org/index.php?showuser=4071895)
* Enable Dark theme for the Monster Info Box
*/
darkMode: false
};
// ============= ^^^ SETTINGS ^^^ =============
/*
* The code below is generated by TypeScript Compiler (http://npm.im/typescript) and Rollup Bundler (https://rollupjs.org/guide/en/)
* If you want to make some modifications, it is recommeneded to build your own script from the source code. The source code is
* released on GitHub under MIT License: https://github.com/SukkaW/hv-monsterdb-userscript
*/
// -+-+-+ DO NOT EDIT THE CODE BELOW THE LINE BY HAND +-+-+-
this.unsafeWindow = this.unsafeWindow || {};
this.unsafeWindow.HVMonsterDB = (function (exports) {
'use strict';
const DOM_REF_FIELD = "__m_dom_ref";
const OLD_VNODE_FIELD = "__m_old_vnode";
const NODE_OBJECT_POOL_FIELD = "__m_node_object_pool";
const XLINK_NS = "http://www.w3.org/1999/xlink";
const XML_NS = "http://www.w3.org/XML/1998/namespace";
const COLON_CHAR = 58;
const X_CHAR = 120;
var Flags = /* @__PURE__ */ ((Flags2) => {
Flags2[Flags2["IGNORE_NODE"] = 0] = "IGNORE_NODE";
Flags2[Flags2["REPLACE_NODE"] = 1] = "REPLACE_NODE";
Flags2[Flags2["NO_CHILDREN"] = 2] = "NO_CHILDREN";
Flags2[Flags2["ONLY_TEXT_CHILDREN"] = 3] = "ONLY_TEXT_CHILDREN";
Flags2[Flags2["ONLY_KEYED_CHILDREN"] = 4] = "ONLY_KEYED_CHILDREN";
Flags2[Flags2["ANY_CHILDREN"] = 5] = "ANY_CHILDREN";
return Flags2;
})(Flags || {});
var EffectTypes = /* @__PURE__ */ ((EffectTypes2) => {
EffectTypes2[EffectTypes2["CREATE"] = 0] = "CREATE";
EffectTypes2[EffectTypes2["REMOVE"] = 1] = "REMOVE";
EffectTypes2[EffectTypes2["REPLACE"] = 2] = "REPLACE";
EffectTypes2[EffectTypes2["UPDATE"] = 3] = "UPDATE";
EffectTypes2[EffectTypes2["SET_PROP"] = 4] = "SET_PROP";
EffectTypes2[EffectTypes2["REMOVE_PROP"] = 5] = "REMOVE_PROP";
return EffectTypes2;
})(EffectTypes || {});
var DeltaTypes = /* @__PURE__ */ ((DeltaTypes2) => {
DeltaTypes2[DeltaTypes2["INSERT"] = 0] = "INSERT";
DeltaTypes2[DeltaTypes2["UPDATE"] = 1] = "UPDATE";
DeltaTypes2[DeltaTypes2["DELETE"] = 2] = "DELETE";
return DeltaTypes2;
})(DeltaTypes || {});
const svg = (vnode) => {
if (!vnode.props)
vnode.props = {};
ns(vnode.tag, vnode.props, vnode.children);
return vnode;
};
const ns = (tag, props, children) => {
if (props.className) {
props.class = props.className;
delete props.className;
}
props.ns = "http://www.w3.org/2000/svg";
if (children && tag !== "foreignObject") {
for (const child of children) {
if (typeof child !== "string" && child.props)
ns(child.tag, child.props, child.children);
}
}
};
const className = (classObject) => Object.keys(classObject).filter((className2) => classObject[className2]).join(" ");
const style = (styleObject) => Object.entries(styleObject).map((style2) => style2.join(":")).join(";");
const kebab = (camelCaseObject) => {
const kebabCaseObject = {};
for (const key in camelCaseObject) {
kebabCaseObject[key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()] = camelCaseObject[key];
}
return kebabCaseObject;
};
const m = (tag, props, children, flag, delta) => {
let key = void 0;
if (props?.key) {
key = props.key;
delete props.key;
}
const vnode = {
tag,
props,
children,
key,
flag,
delta
};
return vnode.tag?.toLowerCase() === "svg" ? svg(vnode) : vnode;
};
const normalize = (jsxVNode) => {
if (Array.isArray(jsxVNode)) {
const normalizedChildren = [];
for (let i = 0; i < jsxVNode.length; i++) {
normalizedChildren.push(normalize(jsxVNode[i]));
}
return normalizedChildren;
} else if (typeof jsxVNode === "string" || typeof jsxVNode === "number" || typeof jsxVNode === "boolean") {
return String(jsxVNode);
} else {
return jsxVNode;
}
};
const h = (tag, props, ...children) => {
if (typeof tag === "function")
return tag(props);
let flag = Flags.NO_CHILDREN;
let delta;
const normalizedChildren = [];
if (props) {
const rawDelta = props.delta;
if (rawDelta && rawDelta.length) {
delta = rawDelta;
delete props.delta;
}
}
if (children) {
const keysInChildren = /* @__PURE__ */ new Set();
let hasVElementChildren = false;
flag = Flags.ANY_CHILDREN;
if (children.every((child) => typeof child === "string")) {
flag = Flags.ONLY_TEXT_CHILDREN;
}
let childrenLength = 0;
for (let i = 0; i < children.length; ++i) {
if (children[i] !== void 0 && children[i] !== null && children[i] !== false && children[i] !== "") {
const unwrappedChild = normalize(children[i]);
const subChildren = Array.isArray(unwrappedChild) ? (childrenLength += unwrappedChild.length, unwrappedChild) : (childrenLength++, [unwrappedChild]);
for (let i2 = 0; i2 < subChildren.length; i2++) {
if (subChildren[i2] || subChildren[i2] === "") {
normalizedChildren.push(subChildren[i2]);
if (typeof subChildren[i2] === "object") {
hasVElementChildren = true;
if (typeof subChildren[i2].key === "string" && subChildren[i2].key !== "") {
keysInChildren.add(subChildren[i2].key);
}
}
}
}
}
}
if (keysInChildren.size === childrenLength) {
flag = Flags.ONLY_KEYED_CHILDREN;
}
if (!hasVElementChildren) {
flag = Flags.ONLY_TEXT_CHILDREN;
}
}
if (props) {
if (typeof props.flag === "number") {
flag = props.flag;
delete props.flag;
}
if (typeof props.className === "object") {
props.className = className(props.className);
}
if (typeof props.style === "object") {
const rawStyle = props.style;
const normalizedStyle = Object.keys(rawStyle).some((key) => /[-A-Z]/gim.test(key)) ? kebab(rawStyle) : rawStyle;
props.style = style(normalizedStyle);
}
}
const vnode = m(tag, props, normalizedChildren, flag, delta);
return tag === "svg" ? svg(vnode) : vnode;
};
const jsx = (tag, props, key) => {
if (typeof tag === "function")
return tag(props, key);
let children = [];
if (props) {
if (props.children) {
children = Array.isArray(props.children) ? props.children : [props.children];
delete props.children;
}
if (key)
props.key = key;
}
return h(tag, props, ...children);
};
function isIsekai() {
return window.location.pathname.includes('isekai');
}
function isFightingInBattle() {
return Boolean(document.getElementById('textlog'));
}
function getUTCDate() {
return new Date().toISOString().split('T')[0];
}
const MonsterDatabaseCompatibleDateCache = new Map();
const padNumber = (num)=>num.toString().padStart(2, '0')
;
function getMonsterDatabaseCompatibleDate(timestamp) {
if (timestamp && MonsterDatabaseCompatibleDateCache.has(timestamp)) return MonsterDatabaseCompatibleDateCache.get(timestamp);
const date = timestamp ? new Date(timestamp) : new Date();
const year = date.getUTCFullYear();
const month = date.getUTCMonth() + 1;
const day = date.getUTCDate();
const result = `${year}-${padNumber(month)}-${padNumber(day)}`;
if (timestamp) {
MonsterDatabaseCompatibleDateCache.set(timestamp, result);
}
return result;
}
function showPopup(msgHtml, color = '#000', title = 'HentaiVerse Monster Datase UserScript') {
const popupEl = document.body.appendChild(document.createElement('div'));
popupEl.style.cssText = 'position:fixed;top:0;left:0;width:1236px;height:702px;padding:3px 100% 100% 3px;background-color:rgba(0,0,0,.3);z-index:1001;display:flex;justify-content:center;align-items:center';
const popupMsgEl = popupEl.appendChild(document.createElement('div'));
popupMsgEl.style.cssText = 'min-width:400px;min-height:80px;max-width:100%;max-height:100%;padding:10px;background-color:#fff;border:1px solid #333;cursor:pointer;display:flex;flex-direction:column;justify-content:center;font-size:10pt';
const titleEl = popupMsgEl.appendChild(document.createElement('h3'));
titleEl.textContent = title;
titleEl.style.marginTop = '0';
const contentEl = popupMsgEl.appendChild(document.createElement('div'));
contentEl.style.color = color;
contentEl.innerHTML = msgHtml;
const closePopupKeyboardEventHandler = (e)=>{
if (e instanceof KeyboardEvent) {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Escape') {
popupEl.remove();
document.removeEventListener('keydown', closePopupKeyboardEventHandler);
}
}
};
popupEl.addEventListener('click', ()=>{
popupEl.remove();
});
document.addEventListener('keydown', closePopupKeyboardEventHandler);
}
var EncodedMonsterDatabase;
(function(EncodedMonsterDatabase1) {
(function(EMonsterClass) {
EMonsterClass[EMonsterClass["Arthropod"] = 0] = "Arthropod";
EMonsterClass[EMonsterClass["Avion"] = 1] = "Avion";
EMonsterClass[EMonsterClass["Beast"] = 2] = "Beast";
EMonsterClass[EMonsterClass["Celestial"] = 3] = "Celestial";
EMonsterClass[EMonsterClass["Daimon"] = 4] = "Daimon";
EMonsterClass[EMonsterClass["Dragonkin"] = 5] = "Dragonkin";
EMonsterClass[EMonsterClass["Elemental"] = 6] = "Elemental";
EMonsterClass[EMonsterClass["Giant"] = 7] = "Giant";
EMonsterClass[EMonsterClass["Humanoid"] = 8] = "Humanoid";
EMonsterClass[EMonsterClass["Mechanoid"] = 9] = "Mechanoid";
EMonsterClass[EMonsterClass["Reptilian"] = 10] = "Reptilian";
EMonsterClass[EMonsterClass["Sprite"] = 11] = "Sprite";
EMonsterClass[EMonsterClass["Undead"] = 12] = "Undead";
EMonsterClass[EMonsterClass["Rare"] = 13] = "Rare";
EMonsterClass[EMonsterClass["Legendary"] = 14] = "Legendary";
EMonsterClass[EMonsterClass["Ultimate"] = 15] = "Ultimate";
EMonsterClass[EMonsterClass["Common"] = 16] = "Common";
})(EncodedMonsterDatabase1.EMonsterClass || (EncodedMonsterDatabase1.EMonsterClass = {}));
(function(EMonsterAttack) {
EMonsterAttack[EMonsterAttack["Piercing"] = 0] = "Piercing";
EMonsterAttack[EMonsterAttack["Crushing"] = 1] = "Crushing";
EMonsterAttack[EMonsterAttack["Slashing"] = 2] = "Slashing";
EMonsterAttack[EMonsterAttack["Fire"] = 3] = "Fire";
EMonsterAttack[EMonsterAttack["Cold"] = 4] = "Cold";
EMonsterAttack[EMonsterAttack["Wind"] = 5] = "Wind";
EMonsterAttack[EMonsterAttack["Elec"] = 6] = "Elec";
EMonsterAttack[EMonsterAttack["Holy"] = 7] = "Holy";
EMonsterAttack[EMonsterAttack["Dark"] = 8] = "Dark";
EMonsterAttack[EMonsterAttack["Void"] = 9] = "Void";
})(EncodedMonsterDatabase1.EMonsterAttack || (EncodedMonsterDatabase1.EMonsterAttack = {}));
(function(EMonsterInfo) {
EMonsterInfo[EMonsterInfo["monsterName"] = 0] = "monsterName";
EMonsterInfo[EMonsterInfo["monsterId"] = 1] = "monsterId";
EMonsterInfo[EMonsterInfo["monsterClass"] = 2] = "monsterClass";
EMonsterInfo[EMonsterInfo["plvl"] = 3] = "plvl";
EMonsterInfo[EMonsterInfo["attack"] = 4] = "attack";
EMonsterInfo[EMonsterInfo["trainer"] = 5] = "trainer";
EMonsterInfo[EMonsterInfo["piercing"] = 6] = "piercing";
EMonsterInfo[EMonsterInfo["crushing"] = 7] = "crushing";
EMonsterInfo[EMonsterInfo["slashing"] = 8] = "slashing";
EMonsterInfo[EMonsterInfo["cold"] = 9] = "cold";
EMonsterInfo[EMonsterInfo["wind"] = 10] = "wind";
EMonsterInfo[EMonsterInfo["elec"] = 11] = "elec";
EMonsterInfo[EMonsterInfo["fire"] = 12] = "fire";
EMonsterInfo[EMonsterInfo["dark"] = 13] = "dark";
EMonsterInfo[EMonsterInfo["holy"] = 14] = "holy";
EMonsterInfo[EMonsterInfo["lastUpdate"] = 15] = "lastUpdate";
})(EncodedMonsterDatabase1.EMonsterInfo || (EncodedMonsterDatabase1.EMonsterInfo = {}));
})(EncodedMonsterDatabase || (EncodedMonsterDatabase = {}));
function convertMonsterInfoToEncodedMonsterInfo(monster) {
return {
[EncodedMonsterDatabase.EMonsterInfo.monsterName]: monster.monsterName,
[EncodedMonsterDatabase.EMonsterInfo.monsterClass]: EncodedMonsterDatabase.EMonsterClass[monster.monsterClass],
[EncodedMonsterDatabase.EMonsterInfo.plvl]: monster.plvl,
[EncodedMonsterDatabase.EMonsterInfo.attack]: EncodedMonsterDatabase.EMonsterAttack[monster.attack],
[EncodedMonsterDatabase.EMonsterInfo.trainer]: monster.trainer,
[EncodedMonsterDatabase.EMonsterInfo.piercing]: monster.piercing,
[EncodedMonsterDatabase.EMonsterInfo.crushing]: monster.crushing,
[EncodedMonsterDatabase.EMonsterInfo.slashing]: monster.slashing,
[EncodedMonsterDatabase.EMonsterInfo.cold]: monster.cold,
[EncodedMonsterDatabase.EMonsterInfo.wind]: monster.wind,
[EncodedMonsterDatabase.EMonsterInfo.elec]: monster.elec,
[EncodedMonsterDatabase.EMonsterInfo.fire]: monster.fire,
[EncodedMonsterDatabase.EMonsterInfo.dark]: monster.dark,
[EncodedMonsterDatabase.EMonsterInfo.holy]: monster.holy,
[EncodedMonsterDatabase.EMonsterInfo.lastUpdate]: monster.lastUpdate ? new Date(monster.lastUpdate).getTime() : new Date('1970-1-1').getTime()
};
}
function convertEncodedMonsterInfoToMonsterInfo(monsterId, simpleMonster) {
return {
monsterId,
monsterName: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.monsterName],
monsterClass: EncodedMonsterDatabase.EMonsterClass[simpleMonster[EncodedMonsterDatabase.EMonsterInfo.monsterClass]],
plvl: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.plvl],
attack: EncodedMonsterDatabase.EMonsterAttack[simpleMonster[EncodedMonsterDatabase.EMonsterInfo.attack]],
trainer: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.trainer],
piercing: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.piercing],
crushing: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.crushing],
slashing: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.slashing],
cold: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.cold],
wind: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.wind],
elec: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.elec],
fire: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.fire],
dark: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.dark],
holy: simpleMonster[EncodedMonsterDatabase.EMonsterInfo.holy],
lastUpdate: getMonsterDatabaseCompatibleDate(simpleMonster[EncodedMonsterDatabase.EMonsterInfo.lastUpdate])
};
}
function getStoredValue(key) {
return GM.getValue(key);
}
function setStoredValue(key, value) {
return GM.setValue(key, value);
}
function removeStoredValue(key) {
return GM.deleteValue(key);
}
class IDBKV {
static promisifyRequest(request) {
return new Promise((resolve, reject)=>{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - file size hacks
// eslint-disable-next-line no-multi-assign
request.oncomplete = request.onsuccess = ()=>resolve(request.result)
;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - file size hacks
// eslint-disable-next-line no-multi-assign
request.onabort = request.onerror = ()=>reject(request.error)
;
});
}
get(key) {
return this.performDatabaseOperation('readonly', (store)=>{
return IDBKV.promisifyRequest(store.get(key));
});
}
set(key, value) {
return this.performDatabaseOperation('readwrite', (store)=>{
store.put(value, key);
return IDBKV.promisifyRequest(store.transaction);
});
}
setMany(entries) {
return this.performDatabaseOperation('readwrite', (store)=>{
entries.forEach((entry)=>store.put(entry[1], entry[0])
);
return IDBKV.promisifyRequest(store.transaction);
});
}
getMany(keys) {
return this.performDatabaseOperation('readonly', (store)=>Promise.all(keys.map((key)=>IDBKV.promisifyRequest(store.get(key))
))
);
}
/** Update a value. This lets you see the old value and update it as an atomic operation. */ update(key, updater) {
return this.performDatabaseOperation('readwrite', // Need to create the promise manually.
// If I try to chain promises, the transaction closes in browsers
// that use a promise polyfill (IE10/11).
(store)=>new Promise((resolve, reject)=>{
store.get(key).onsuccess = function() {
try {
const newValue = updater(this.result);
if (newValue !== this.result) {
store.put(updater(this.result), key);
resolve(IDBKV.promisifyRequest(store.transaction));
} else {
resolve();
}
} catch (err) {
reject(err);
}
};
})
);
}
del(key) {
return this.performDatabaseOperation('readwrite', (store)=>{
store.delete(key);
return IDBKV.promisifyRequest(store.transaction);
});
}
delMany(keys) {
return this.performDatabaseOperation('readwrite', (store)=>{
keys.forEach((key)=>store.delete(key)
);
return IDBKV.promisifyRequest(store.transaction);
});
}
async clear() {
return this.performDatabaseOperation('readwrite', (store)=>{
store.clear();
return IDBKV.promisifyRequest(store.transaction);
});
}
keys() {
const items = [];
return this.eachCursor((cursor)=>items.push(cursor.key)
).then(()=>items
);
}
values() {
const items = [];
return this.eachCursor((cursor)=>items.push(cursor.value)
).then(()=>items
);
}
entries() {
const items = [];
return this.eachCursor((cursor)=>items.push([
cursor.key,
cursor.value
])
).then(()=>items
);
}
initializeOpenDatabasePromise() {
if (this.databasePromise === null) {
const promise = new Promise((resolve, reject)=>{
const request = self.indexedDB.open(this.dbName, this.dbVersion);
request.onsuccess = ()=>{
const database = request.result;
database.onclose = ()=>{
this.databasePromise = null;
};
database.onversionchange = ()=>{
database.close();
this.databasePromise = null;
};
resolve(database);
};
request.onerror = ()=>reject(request.error)
;
request.onupgradeneeded = ()=>{
try {
// Whatever the KV instance is opened, always create all objectStore we needed
OBJECT_STORES.forEach((storeName)=>request.result.createObjectStore(storeName)
);
} catch (e) {
reject(e);
}
};
});
this.databasePromise = promise;
return promise;
}
}
async performDatabaseOperation(txMode, callback) {
if (!this.databasePromise) {
this.initializeOpenDatabasePromise();
}
const db = await this.databasePromise;
const store = db.transaction(this.storeName, txMode).objectStore(this.storeName);
return callback(store);
}
eachCursor(callback) {
return this.performDatabaseOperation('readonly', (store)=>{
store.openCursor().onsuccess = function() {
if (!this.result) return;
callback(this.result);
this.result.continue();
};
return IDBKV.promisifyRequest(store.transaction);
});
}
constructor(dbName, storeName, dbVersion){
this.databasePromise = null;
this.dbName = dbName;
this.storeName = storeName;
this.dbVersion = dbVersion;
this.initializeOpenDatabasePromise();
}
}
const DBNAME = 'hv-monster-database-script';
const OBJECT_STORES = [
'MONSTER_NAME_ID_MAP',
'databaseV2',
'databaseIsekaiV2'
];
/** The position of monster info box */ let MONSTER_INFO_BOX_POSITION = {
x: 10,
y: 10
};
// eslint-disable-next-line @typescript-eslint/naming-convention
class MONSTER_NAME_ID_MAP {
static async get(monsterName) {
if (MONSTER_NAME_ID_MAP.cache.has(monsterName)) return MONSTER_NAME_ID_MAP.cache.get(monsterName);
const monsterId = await MONSTER_NAME_ID_MAP.store.get(monsterName);
if (monsterId) {
MONSTER_NAME_ID_MAP.cache.set(monsterName, monsterId);
}
return monsterId;
}
static async updateMany(entries) {
if (entries.length > 0) {
return MONSTER_NAME_ID_MAP.store.performDatabaseOperation('readwrite', (store)=>{
entries.forEach((entry)=>{
if (entry) {
const [monsterName, newMonsterId] = entry;
if (this.cache.get(monsterName) !== newMonsterId) {
this.cache.set(monsterName, newMonsterId);
store.get(monsterName).onsuccess = function() {
if (this.result !== newMonsterId) {
store.put(newMonsterId, monsterName);
}
};
}
}
});
return IDBKV.promisifyRequest(store.transaction);
});
}
}
}
MONSTER_NAME_ID_MAP.cache = new Map();
MONSTER_NAME_ID_MAP.store = new IDBKV(DBNAME, 'MONSTER_NAME_ID_MAP');
class LocalMonsterDatabase {
async get(monsterId) {
if (this.cache.has(monsterId)) return this.cache.get(monsterId);
const encodedMonsterInfo = await this.store.get(monsterId);
if (encodedMonsterInfo) {
this.cache.set(monsterId, encodedMonsterInfo);
return encodedMonsterInfo;
}
}
getAll() {
return this.store.performDatabaseOperation('readonly', (store)=>{
return Promise.all([
IDBKV.promisifyRequest(store.getAllKeys()),
IDBKV.promisifyRequest(store.getAll())
]).then(([keys, values])=>keys.map((key, i)=>[
key,
values[i]
]
)
);
});
}
getMany(monsterIds) {
if (monsterIds.map((id)=>id && this.cache.has(id)
).length === monsterIds.length) {
return Promise.resolve(monsterIds.map((id)=>id ? this.cache.get(id) : undefined
));
}
return this.store.performDatabaseOperation('readonly', (store)=>{
const resultPromises = [];
monsterIds.forEach((id)=>{
if (id) {
if (this.cache.has(id)) {
resultPromises.push(this.cache.get(id));
} else {
resultPromises.push(IDBKV.promisifyRequest(store.get(id)));
}
} else {
resultPromises.push(undefined);
}
});
return Promise.all(resultPromises);
});
}
set(monsterId, monsterInfo) {
this.cache.set(monsterId, monsterInfo);
return this.store.set(monsterId, monsterInfo);
}
updateMany(entries) {
if (entries.length > 0) {
this.cache.clear();
return this.store.performDatabaseOperation('readwrite', (store)=>{
entries.forEach((entry)=>{
if (entry) {
const [monsterId, newMonsterInfo] = entry;
store.get(monsterId).onsuccess = function() {
if (!LocalMonsterDatabase.monsterInfoIsEquial(this.result, newMonsterInfo)) {
store.put(newMonsterInfo, monsterId);
}
};
}
});
return IDBKV.promisifyRequest(store.transaction);
});
}
return Promise.resolve();
}
static monsterInfoIsEquial(monster1, monster2) {
if (!monster1) return false;
if ([
EncodedMonsterDatabase.EMonsterInfo.monsterName,
EncodedMonsterDatabase.EMonsterInfo.monsterClass,
EncodedMonsterDatabase.EMonsterInfo.plvl,
EncodedMonsterDatabase.EMonsterInfo.attack,
EncodedMonsterDatabase.EMonsterInfo.trainer,
EncodedMonsterDatabase.EMonsterInfo.piercing,
EncodedMonsterDatabase.EMonsterInfo.crushing,
EncodedMonsterDatabase.EMonsterInfo.slashing,
EncodedMonsterDatabase.EMonsterInfo.cold,
EncodedMonsterDatabase.EMonsterInfo.wind,
EncodedMonsterDatabase.EMonsterInfo.elec,
EncodedMonsterDatabase.EMonsterInfo.fire,
EncodedMonsterDatabase.EMonsterInfo.dark,
EncodedMonsterDatabase.EMonsterInfo.holy,
EncodedMonsterDatabase.EMonsterInfo.lastUpdate
].every((k)=>monster1[k] === monster2[k]
)) {
return true;
}
return false;
}
constructor(storeName){
this.cache = new Map();
this.store = new IDBKV(DBNAME, storeName);
}
}
const LOCAL_MONSTER_DATABASE_PERSISTENT = new LocalMonsterDatabase('databaseV2');
const LOCAL_MONSTER_DATABASE_ISEKAI = new LocalMonsterDatabase('databaseIsekaiV2');
const LOCAL_MONSTER_DATABASE = isIsekai() ? LOCAL_MONSTER_DATABASE_ISEKAI : LOCAL_MONSTER_DATABASE_PERSISTENT;
/**
* According to MDN:
* > (Map) performs better in scenarios involving frequent additions and removals of key-value pairs.
*
* And in modern V8 javascript engine, Map is about 40% faster than Object literal.
*
* However, as Map can not be serialized, it can not be stored using either localStorage or GM.setValue,
* so it is required to convert Map to Object literal before storing, and convert it back after retrieving.
*/ function storeTmpValue() {
return setStoredValue('monsterInfoBoxPosition', MONSTER_INFO_BOX_POSITION);
}
async function retrieveTmpValue() {
MONSTER_INFO_BOX_POSITION = await getStoredValue('monsterInfoBoxPosition') || {
x: 10,
y: 10
};
}
const rMatchMonsterId = /MID=(\d+) \((.+)\)/;
const rMatchScan = /Scanning (.+?)\.\.\..+?Monster Class.+>([A-Z][a-z]+)(?:, Power Level (\d+)<|<).+?Monster Trainer:<\/strong><\/td><td>([^<>]*)<.+?<\/strong><\/td><td>([A-Za-z]+)<.+?Fire:.+?>([+-])(\d+)%<.+?Cold:.+?>([+-])(\d+)%<.+?Elec:.+?>([+-])(\d+)%<.+?Wind:.+?>([+-])(\d+)%<.+?Holy:.+?>([+-])(\d+)%<.+?Dark:.+?>([+-])(\d+)%<.+?Crushing:.+?>([+-])(\d+)%<.+?Slashing:.+?>([+-])(\d+)%<.+?Piercing:.+?>([+-])(\d+)%/;
function parseMonsterNameAndId(singleLogText) {
const matches = singleLogText.match(rMatchMonsterId);
if (matches) {
const monsterId = Number(matches[1]);
const monsterName = matches[2];
if (!Number.isNaN(monsterId)) {
return {
monsterId,
monsterName
};
}
}
return null;
}
const isPositiveOrNegative = (modifier)=>modifier === '+' ? 1 : -1
;
async function parseScanResult(singleLogHtml) {
if (singleLogHtml.includes('Scanning')) {
const matches = singleLogHtml.match(rMatchScan);
if (matches) {
const monsterName = matches[1];
const monsterId = await MONSTER_NAME_ID_MAP.get(monsterName);
const lastUpdate = getMonsterDatabaseCompatibleDate();
// System Monster has no Power Level results in undefined
// Treat it as PL 0 instead
const plvl = Number(matches[3] ?? 0);
const fire = Number(matches[7]) * isPositiveOrNegative(matches[6]);
const cold = Number(matches[9]) * isPositiveOrNegative(matches[8]);
const elec = Number(matches[11]) * isPositiveOrNegative(matches[10]);
const wind = Number(matches[13]) * isPositiveOrNegative(matches[12]);
const holy = Number(matches[15]) * isPositiveOrNegative(matches[14]);
const dark = Number(matches[17]) * isPositiveOrNegative(matches[16]);
const crushing = Number(matches[19]) * isPositiveOrNegative(matches[18]);
const slashing = Number(matches[21]) * isPositiveOrNegative(matches[20]);
const piercing = Number(matches[23]) * isPositiveOrNegative(matches[22]);
if (![
plvl,
fire,
cold,
elec,
wind,
holy,
dark,
crushing,
slashing,
piercing
].some(Number.isNaN) && monsterId) {
return {
monsterName,
monsterId,
monsterClass: matches[2],
plvl,
trainer: matches[4],
attack: matches[5],
fire,
cold,
elec,
wind,
holy,
dark,
crushing,
slashing,
piercing,
lastUpdate
};
}
}
}
}
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = ".style-module_monsterdb_info__Rp7EW{opacity:1;position:absolute;width:175px;z-index:3}.style-module_monsterdb_dark__tgfWT{color:#dddad6}.style-module_compact__1rkaK{width:75px}.style-module_drag__T988-{opacity:.6}.style-module_drag__T988- .style-module_header__EOkm8,.style-module_drag__T988- .style-module_table__A3lw7{border-style:dashed}.style-module_header__EOkm8{background-color:#e5e2d5;border:2px solid #5c0d11;cursor:move;font-weight:600;height:20px;line-height:20px;margin-bottom:-2px;text-align:center;user-select:none;visibility:hidden}.style-module_monsterdb_dark__tgfWT .style-module_header__EOkm8{background-color:#16171d;border-color:#c41c26}.style-module_monsterdb_info__Rp7EW:hover .style-module_header__EOkm8{visibility:visible}.style-module_table_container__rgU-7{font-family:Consolas,Monaco,SFMono-Regular,Andale Mono,Liberation Mono,Ubuntu Mono,Menlo,monospace;font-weight:700;height:56px;margin:2px auto}.style-module_monsterdb_dark__tgfWT .style-module_table_container__rgU-7{background-color:#1f2129}.style-module_notify__nbf7p{color:red;font-size:20px;line-height:56px}.style-module_table__A3lw7{background-color:#eeece1;border:2px ridge #5c0d12;border-collapse:collapse;border-spacing:0;font-size:12px;height:100%;letter-spacing:-1px;line-height:1;width:100%}.style-module_monsterdb_dark__tgfWT .style-module_table__A3lw7{border-color:#c41c26}.style-module_table__A3lw7 td{border:1px solid #5c0d11;padding:0 1px}.style-module_table__A3lw7 td:last-child{max-width:70px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:70px}.style-module_fire__7TDZO{color:#ce3600}.style-module_cold__cEVYS{color:#1b78d4}.style-module_elec__Ck96M{color:#be900f}.style-module_wind__rnPOC{color:#16a084}.style-module_holy__n1BUU{color:#a19f02}.style-module_dark__LG-Fr{color:#814399}.style-module_hidden__SPpn9{display:none}";
var styles = {"monsterdb_info":"style-module_monsterdb_info__Rp7EW","monsterdb_dark":"style-module_monsterdb_dark__tgfWT","compact":"style-module_compact__1rkaK","drag":"style-module_drag__T988-","header":"style-module_header__EOkm8","table":"style-module_table__A3lw7","table_container":"style-module_table_container__rgU-7","notify":"style-module_notify__nbf7p","fire":"style-module_fire__7TDZO","cold":"style-module_cold__cEVYS","elec":"style-module_elec__Ck96M","wind":"style-module_wind__rnPOC","holy":"style-module_holy__n1BUU","dark":"style-module_dark__LG-Fr","hidden":"style-module_hidden__SPpn9"};
styleInject(css_248z);
const createElement = (vnode, attachField = true) => {
if (vnode?.data) {
if (vnode?.el)
return vnode.el;
else
return createElement(vnode?.resolve());
}
if (vnode === void 0 || vnode === null)
return document.createComment("");
if (typeof vnode === "string")
return document.createTextNode(vnode);
const velement = vnode;
const el = velement.props?.ns ? document.createElementNS(velement.props?.ns, velement.tag) : document.createElement(velement.tag);
if (velement.props) {
for (const propName in velement.props) {
const propValue = velement.props[propName];
if (propName.startsWith("on")) {
const eventPropName = propName.slice(2).toLowerCase();
el.addEventListener(eventPropName, propValue);
} else if (propName.charCodeAt(0) === X_CHAR) {
if (propName.charCodeAt(3) === COLON_CHAR) {
el.setAttributeNS(XML_NS, propName, String(propValue));
} else if (propName.charCodeAt(5) === COLON_CHAR) {
el.setAttributeNS(XLINK_NS, propName, String(propValue));
}
} else if (el[propName] !== void 0 && !(el instanceof SVGElement)) {
el[propName] = propValue;
} else {
el.setAttribute(propName, String(propValue));
}
}
}
if (velement.children) {
if (velement.flag === Flags.ONLY_TEXT_CHILDREN) {
el.textContent = Array.isArray(velement.children) ? velement.children?.join("") : velement.children;
} else {
for (let i = 0; i < velement.children.length; ++i) {
el.appendChild(createElement(velement.children[i], false));
}
}
}
if (attachField)
el[OLD_VNODE_FIELD] = vnode;
return el;
};
const useChildren = (drivers = []) => (el, newVNode, oldVNode, commit = (work) => work(), effects = [], driver) => {
const getData = (element) => ({
el: element,
newVNode,
oldVNode,
effects,
commit,
driver
});
const finish = (element) => {
const data = getData(element);
for (let i = 0; i < drivers.length; ++i) {
commit(() => {
drivers[i](el, newVNode, oldVNode, commit, effects, driver);
}, data);
}
return data;
};
const oldVNodeChildren = oldVNode?.children ?? [];
const newVNodeChildren = newVNode.children;
const delta = newVNode.delta;
const diff = (el2, newVNode2, oldVNode2) => driver(el2, newVNode2, oldVNode2, commit, effects).effects;
if (delta) {
for (let i = 0; i < delta.length; ++i) {
const [deltaType, deltaPosition] = delta[i];
const child = el.childNodes.item(deltaPosition);
if (deltaType === DeltaTypes.INSERT) {
effects.push({
type: EffectTypes.CREATE,
flush: () => el.insertBefore(createElement(newVNodeChildren[deltaPosition], false), child)
});
}
if (deltaType === DeltaTypes.UPDATE) {
commit(() => {
effects = diff(child, newVNodeChildren[deltaPosition], oldVNodeChildren[deltaPosition]);
}, getData(child));
}
if (deltaType === DeltaTypes.DELETE) {
effects.push({
type: EffectTypes.REMOVE,
flush: () => el.removeChild(child)
});
}
}
return finish(el);
}
if (!newVNodeChildren || newVNode.flag === Flags.NO_CHILDREN) {
if (!oldVNodeChildren)
return finish(el);
effects.push({
type: EffectTypes.REMOVE,
flush: () => el.textContent = ""
});
return finish(el);
}
if (!oldVNodeChildren || oldVNodeChildren?.length === 0) {
for (let i = 0; i < newVNodeChildren.length; ++i) {
effects.push({
type: EffectTypes.CREATE,
flush: () => el.appendChild(createElement(newVNodeChildren[i], false))
});
}
return finish(el);
}
if (newVNode.flag === Flags.ONLY_KEYED_CHILDREN) {
if (!el[NODE_OBJECT_POOL_FIELD])
el[NODE_OBJECT_POOL_FIELD] = {};
let oldHead = 0;
let newHead = 0;
let oldTail = oldVNodeChildren.length - 1;
let newTail = newVNodeChildren.length - 1;
while (oldHead <= oldTail && newHead <= newTail) {
const oldTailVNode = oldVNodeChildren[oldTail];
const newTailVNode = newVNodeChildren[newTail];
const oldHeadVNode = oldVNodeChildren[oldHead];
const newHeadVNode = newVNodeChildren[newHead];
if (oldTailVNode.key === newTailVNode.key) {
oldTail--;
newTail--;
} else if (oldHeadVNode.key === newHeadVNode.key) {
oldHead++;
newHead++;
} else if (oldHeadVNode.key === newTailVNode.key) {
const node = el.childNodes.item(oldHead++);
const tail = newTail--;
effects.push({
type: EffectTypes.CREATE,
flush: () => el.insertBefore(node, el.childNodes.item(tail).nextSibling)
});
} else if (oldTailVNode.key === newHeadVNode.key) {
const node = el.childNodes.item(oldTail--);
const head = newHead++;
effects.push({
type: EffectTypes.CREATE,
flush: () => el.insertBefore(node, el.childNodes.item(head))
});
} else
break;
}
if (oldHead > oldTail) {
while (newHead <= newTail) {
const head = newHead++;
effects.push({
type: EffectTypes.CREATE,
flush: () => el.insertBefore(el[NODE_OBJECT_POOL_FIELD][newVNodeChildren[head].key] ?? createElement(newVNodeChildren[head], false), el.childNodes.item(head))
});
}
} else if (newHead > newTail) {
while (oldHead <= oldTail) {
const head = oldHead++;
const node = el.childNodes.item(head);
el[NODE_OBJECT_POOL_FIELD][oldVNodeChildren[head].key] = node;
effects.push({
type: EffectTypes.REMOVE,
flush: () => el.removeChild(node)
});
}
} else {
const oldKeyMap = {};
for (; oldHead <= oldTail; ) {
oldKeyMap[oldVNodeChildren[oldHead].key] = oldHead++;
}
while (newHead <= newTail) {
const head = newHead++;
const newVNodeChild = newVNodeChildren[head];
const oldVNodePosition = oldKeyMap[newVNodeChild.key];
if (oldVNodePosition !== void 0) {
const node = el.childNodes.item(oldVNodePosition);
effects.push({
type: EffectTypes.CREATE,
flush: () => el.insertBefore(node, el.childNodes.item(head))
});
delete oldKeyMap[newVNodeChild.key];
} else {
effects.push({
type: EffectTypes.CREATE,
flush: () => el.insertBefore(el[NODE_OBJECT_POOL_FIELD][newVNodeChild.key] ?? createElement(newVNodeChild, false), el.childNodes.item(head))
});
}
}
for (const oldVNodeKey in oldKeyMap) {
const node = el.childNodes.item(oldKeyMap[oldVNodeKey]);
el[NODE_OBJECT_POOL_FIELD][oldVNodeKey] = node;
effects.push({
type: EffectTypes.REMOVE,
flush: () => el.removeChild(node)
});
}
}
return finish(el);
}
if (newVNode.flag === Flags.ONLY_TEXT_CHILDREN) {
const oldString = Array.isArray(oldVNode?.children) ? oldVNode?.children.join("") : oldVNode?.children;
const newString = Array.isArray(newVNode?.children) ? newVNode?.children.join("") : newVNode?.children;
if (oldString !== newString) {
effects.push({
type: EffectTypes.REPLACE,
flush: () => el.textContent = newString
});
}
return finish(el);
}
if (newVNode.flag === void 0 || newVNode.flag === Flags.ANY_CHILDREN) {
if (oldVNodeChildren && newVNodeChildren) {
const commonLength = Math.min(oldVNodeChildren.length, newVNodeChildren.length);
for (let i = commonLength - 1; i >= 0; --i) {
commit(() => {
effects = diff(el.childNodes.item(i), newVNodeChildren[i], oldVNodeChildren[i]);
}, getData(el));
}
if (newVNodeChildren.length > oldVNodeChildren.length) {
for (let i = commonLength; i < newVNodeChildren.length; ++i) {
const node = createElement(newVNodeChildren[i], false);
effects.push({ type: EffectTypes.CREATE, flush: () => el.appendChild(node) });
}
} else if (newVNodeChildren.length < oldVNodeChildren.length) {
for (let i = oldVNodeChildren.length - 1; i >= commonLength; --i) {
effects.push({
type: EffectTypes.REMOVE,
flush: () => el.removeChild(el.childNodes.item(i))
});
}
}
} else if (newVNodeChildren) {
for (let i = 0; i < newVNodeChildren.length; ++i) {
const node = createElement(newVNodeChildren[i], false);
effects.push({
type: EffectTypes.CREATE,
flush: () => el.appendChild(node)
});
}
}
return finish(el);
}
return finish(el);
};
const useNode = (drivers) => {
const nodeDriver = (el, newVNode, oldVNode, commit = (work) => work(), effects = []) => {
const finish = (element) => {
if (!oldVNode) {
effects.push({
type: EffectTypes.SET_PROP,
flush: () => element[OLD_VNODE_FIELD] = newVNode
});
}
return {
el: element,
newVNode,
oldVNode,
effects
};
};
if (newVNode?.flag === Flags.IGNORE_NODE || oldVNode?.flag === Flags.IGNORE_NODE) {
return finish(el);
}
if (newVNode === void 0 || newVNode === null) {
effects.push({
type: EffectTypes.REMOVE,
flush: () => el.remove()
});
return finish(el);
} else {
let prevVNode = oldVNode ?? el[OLD_VNODE_FIELD];
const hasString = typeof prevVNode === "string" || typeof newVNode === "string";
if (hasString && prevVNode !== newVNode) {
const newEl = createElement(newVNode, false);
effects.push({
type: EffectTypes.REPLACE,
flush: () => el.replaceWith(newEl)
});
return finish(newEl);
}
if (!hasString) {
const prevVEntity = prevVNode;
const newVEntity = newVNode;
if (newVEntity.ignore)
return finish(el);
if (prevVEntity?.data)
prevVNode = prevVEntity.resolve();
if (newVEntity?.data)
newVNode = newVEntity.resolve();
const oldVElement = prevVNode;
const newVElement = newVNode;
if (newVElement.flag === Flags.REPLACE_NODE || oldVElement.flag === Flags.REPLACE_NODE) {
const newEl = createElement(newVNode);
el.replaceWith(newEl);
return finish(el);
}
if (oldVElement?.key === void 0 && newVElement?.key === void 0 || oldVElement?.key !== newVElement?.key) {
if (oldVElement?.tag !== newVElement?.tag || el instanceof Text) {
const newEl = createElement(newVElement, false);
effects.push({
type: EffectTypes.REPLACE,
flush: () => el.replaceWith(newEl)
});
return finish(newEl);
}
for (let i = 0; i < drivers.length; ++i) {
commit(() => {
drivers[i](el, newVElement, oldVElement, commit, effects, nodeDriver);
}, {
el,
newVNode,
oldVNode,
effects
});
}
}
}
}
return finish(el);
};
return nodeDriver;
};
const updateProp = (el, propName, oldPropValue, newPropValue, effects) => {
if (oldPropValue === newPropValue)
return;
if (propName.startsWith("on")) {
const eventPropName = propName.slice(2).toLowerCase();
effects.push({
type: EffectTypes.SET_PROP,
flush: () => {
if (oldPropValue)
el.removeEventListener(eventPropName, oldPropValue);
el.addEventListener(eventPropName, newPropValue);
}
});
} else if (propName.charCodeAt(0) === X_CHAR) {
if (propName.charCodeAt(3) === COLON_CHAR) {
el.setAttributeNS(XML_NS, propName, String(newPropValue));
} else if (propName.charCodeAt(5) === COLON_CHAR) {
el.setAttributeNS(XLINK_NS, propName, String(newPropValue));
}
} else if (el[propName] !== void 0 && !(el instanceof SVGElement)) {
if (newPropValue) {
effects.push({
type: EffectTypes.SET_PROP,
flush: () => el[propName] = newPropValue
});
} else {
effects.push({
type: EffectTypes.REMOVE_PROP,
flush: () => {
el[propName] = "";
el.removeAttribute(propName);
delete el[propName];
}
});
}
} else if (!newPropValue) {
effects.push({
type: EffectTypes.REMOVE_PROP,
flush: () => el.removeAttribute(propName)
});
} else {
effects.push({
type: EffectTypes.SET_PROP,
flush: () => el.setAttribute(propName, String(newPropValue))
});
}
};
const useProps = (drivers = []) => (el, newVNode, oldVNode, commit = (work) => work(), effects = []) => {
const oldProps = oldVNode?.props;
const newProps = newVNode?.props;
const data = {
el,
newVNode,
oldVNode,
effects
};
if (oldProps !== newProps) {
if (oldProps === void 0 || newProps === null) {
for (const propName in newProps) {
updateProp(el, propName, void 0, newProps[propName], effects);
}
} else if (newProps === void 0 || newProps === null) {
for (const propName in oldProps) {
updateProp(el, propName, oldProps[propName], void 0, effects);
}
} else {
let matches = 0;
for (const propName in oldProps) {
updateProp(el, propName, oldProps[propName], Object.prototype.hasOwnProperty.call(newProps, propName) ? (matches++, newProps[propName]) : void 0, effects);
}
const keys = Object.keys(newProps);
for (let i = 0; matches < keys.length && i < keys.length; ++i) {
const propName = keys[i];
if (!Object.prototype.hasOwnProperty.call(oldProps, propName)) {
updateProp(el, propName, void 0, newProps[propName], effects);
++matches;
}
}
}
}
for (let i = 0; i < drivers.length; ++i) {
commit(() => {
drivers[i](el, newVNode, oldVNode, commit, effects);
}, data);
}
return data;
};
const diff = useNode([useChildren(), useProps()]);
const patch = (el, newVNode, oldVNode, hook = () => true, effects = []) => {
const commit = (work, data2) => {
if (hook(data2.el, data2.newVNode, data2.oldVNode)) {
work();
}
};
const data = diff(el, newVNode, oldVNode, commit, effects);
for (let i = 0; i < effects.length; i++) {
effects[i].flush();
}
return data.el;
};
const render = (parentEl, newVNode, oldVNode, hook) => {
const el = parentEl[DOM_REF_FIELD];
if (el) {
return patch(el, newVNode, oldVNode, hook);
} else {
const newEl = createElement(newVNode);
parentEl.textContent = "";
parentEl.appendChild(newEl);
parentEl[DOM_REF_FIELD] = newEl;
return newEl;
}
};
/** @jsxImportSource million */ function createMonsterInfoBox() {
// Monsterbation tends to completely wipe out DOM when changing round
// However we still have to make sure the info box won't be added again & again
if (document.getElementById('monsterdb_info')) return;
const boxEl = document.createElement('div');
boxEl.id = 'monsterdb_info';
boxEl.classList.add(styles.monsterdb_info);
if (SETTINGS.darkMode) {
boxEl.classList.add(styles.monsterdb_dark);
}
// Use saved position information
// A fix to prevent the info box being drag into outer window
const { width: screenWidth , height: screenHeight } = window.screen;
let left = MONSTER_INFO_BOX_POSITION.x;
let top = MONSTER_INFO_BOX_POSITION.y;
if (left > screenWidth - 120) {
left = screenWidth - 120;
} else if (left < 0) {
left = 0;
}
if (top > screenHeight - 590) {
top = screenHeight - 590;
} else if (top < 0) {
top = 0;
}
boxEl.style.left = `${left}px`;
boxEl.style.top = `${top}px`;
const headerEl = boxEl.appendChild(document.createElement('div'));
headerEl.classList.add(styles.header);
if (SETTINGS.compactMonsterInfoBox) {
boxEl.classList.add(styles.compact);
}
headerEl.textContent = 'drag\'n\'drop';
const containerEl = document.createElement('div');
containerEl.id = 'monsterdb_container';
boxEl.appendChild(containerEl);
document.body.appendChild(boxEl);
makeMonsterInfoBoxDraggable(boxEl, headerEl);
}
const isCompactMonsterInfoBox = !SETTINGS.compactMonsterInfoBox;
const padStr = (num)=>{
if (typeof num !== 'number') return '';
return String(num).padStart(2, ' ');
};
// eslint-disable-next-line no-nested-ternary
const symbolNum = (num)=>num ? num === 0 ? ' ' : num > 0 ? '+' : '' : ''
;
const MonsterTable = (props)=>/*#__PURE__*/ jsx("div", {
className: styles.table_container,
children: /*#__PURE__*/ jsx("table", {
className: className({
[styles.table]: true,
[styles.hidden]: !props.monsterInfo
}),
children: /*#__PURE__*/ jsx("tbody", {
children: [
/*#__PURE__*/ jsx("tr", {
children: [
isCompactMonsterInfoBox && [
'fire',
'cold',
'elec'
].map((i)=>{
return /*#__PURE__*/ jsx("td", {
className: styles[i],
children: [
i[0],
":",
symbolNum(props.monsterInfo?.[i]),
padStr(props.monsterInfo?.[i])
]
});
}),
/*#__PURE__*/ jsx("td", {
children: [
props.monsterInfo?.monsterClass?.toLocaleLowerCase()?.substring(0, 5),
"(",
props.monsterInfo?.attack?.toLocaleLowerCase()?.substring(0, 4),
")"
]
})
]
}),
/*#__PURE__*/ jsx("tr", {
children: [
isCompactMonsterInfoBox && [
'wind',
'holy',
'dark'
].map((i)=>{
return /*#__PURE__*/ jsx("td", {
className: styles[i],
children: [
i[0],
":",
symbolNum(props.monsterInfo?.[i]),
padStr(props.monsterInfo?.[i])
]
});
}),
/*#__PURE__*/ jsx("td", {
children: [
"PL: ",
props.monsterInfo?.plvl
]
})
]
}),
/*#__PURE__*/ jsx("tr", {
children: [
isCompactMonsterInfoBox && [
'crushing',
'slashing',
'piercing'
].map((i)=>{
return /*#__PURE__*/ jsx("td", {
children: [
i[0],
":",
symbolNum(props.monsterInfo?.[i]),
padStr(props.monsterInfo?.[i])
]
});
}),
/*#__PURE__*/ jsx("td", {
children: props.monsterInfo?.trainer === '' ? 'System' : props.monsterInfo?.trainer ?? 'Unknown'
})
]
})
]
})
})
})
;
const RANGE_OF_10 = [
...Array(10).keys()
];
function MonsterInfo(props) {
return /*#__PURE__*/ jsx("div", {
children: /* Provide all 10 MonsterTable and only toggle their display property, significantly improve virtual dom performance */ RANGE_OF_10.map((i)=>/*#__PURE__*/ jsx(MonsterTable, {
monsterInfo: props.allMonsterStatus[i] ?? null
})
)
});
}
function makeMonsterInfoBoxDraggable(boxEl, headerEl) {
headerEl.addEventListener('mousedown', (evt1)=>{
// Only respond to left click
if (evt1.buttons === 1) {
// flag for if box being dragged, used to avoid some race condition
let MOVE_FLAG = true;
let rAFId;
const shiftX = evt1.clientX - boxEl.getBoundingClientRect().left;
const shiftY = evt1.clientY - boxEl.getBoundingClientRect().top;
const moveTo = (pageX, pageY)=>{
// Always read innerHeight and innerWidth in realtime in case the browser window is resized
const winHeight = window.innerHeight;
const winWidth = window.innerWidth;
let left = pageX - shiftX;
if (left > winWidth - 120) {
left = winWidth - 120;
} else if (left < 0) {
left = 0;
}
let top = pageY - shiftY;
if (top > winHeight - 590) {
top = winHeight - 590;
} else if (top < 0) {
top = 0;
}
boxEl.style.left = `${left}px`;
MONSTER_INFO_BOX_POSITION.x = left;
boxEl.style.top = `${top}px`;
MONSTER_INFO_BOX_POSITION.y = top;
};
// Use window.requestAnimationFrame instead of throttle for better performance
const onMouseMove = (evt)=>{
if (MOVE_FLAG) {
if (rAFId) {
// clear previous rAF
window.cancelAnimationFrame(rAFId);
}
rAFId = window.requestAnimationFrame(()=>{
boxEl.classList.add(styles.drag);
moveTo(evt.pageX, evt.pageY);
});
}
};
const onReleaseMouse = ()=>{
MOVE_FLAG = false;
document.removeEventListener('mousemove', onMouseMove);
headerEl.removeEventListener('mouseup', onReleaseMouse);
window.removeEventListener('blur', onReleaseMouse);
boxEl.classList.remove(styles.drag);
};
document.addEventListener('mousemove', onMouseMove);
headerEl.addEventListener('mouseup', onReleaseMouse);
window.addEventListener('blur', onReleaseMouse);
}
});
}
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-console */ const nameStyle = 'background: #000; color: #fff';
const debugStyle = 'background: #7a7a7a; color: #fff';
const infoStyle = 'background: #257942; color: #fff';
const warnStyle = 'background: #947600; color: #fff';
const errorStyle = 'background: #cc0f35; color: #fff';
// const msgStyle = 'background: transparent; color: #000';
class Logger {
setDebugMode(debugMode = false) {
this.DEBUG = debugMode;
}
debug(...msg) {
if (this.DEBUG) {
console.debug('%c HVMDB %c DEBUG ', nameStyle, debugStyle, ...msg);
console.groupCollapsed();
console.trace();
console.groupEnd();
}
}
info(...msg) {
console.info('%c HVMDB %c INFO ', nameStyle, infoStyle, ...msg);
if (this.DEBUG) {
console.groupCollapsed();
console.trace();
console.groupEnd();
}
}
warn(...msg) {
console.warn('%c HVMDB %c WARN ', nameStyle, warnStyle, ...msg);
if (this.DEBUG) {
console.groupCollapsed();
console.trace();
console.groupEnd();
}
}
error(...msg) {
console.error('%c HVMDB %c ERROR ', nameStyle, errorStyle, ...msg);
if (this.DEBUG) {
console.groupCollapsed();
console.trace();
console.groupEnd();
}
}
constructor(){
this.DEBUG = false;
}
}
const logger = new Logger();
async function submitScanResults(payload) {
return fetch('https://hv-monster-submit.skk.moe/api/monsterdata', {
method: 'PUT',
headers: {
'Content-Type': 'application/json; charset=UTF-8'
},
body: JSON.stringify({
monsterId: payload.monsterId,
monsterClass: payload.monsterClass,
monsterName: payload.monsterName,
plvl: payload.plvl,
attack: payload.attack,
trainer: payload.trainer,
piercing: payload.piercing,
crushing: payload.crushing,
slashing: payload.slashing,
cold: payload.cold,
wind: payload.wind,
elec: payload.elec,
fire: payload.fire,
dark: payload.dark,
holy: payload.holy
})
});
}
const MOUNT = 5;
const UNMOUNT = 6;
const REVERT_MUTATION = 10;
let on = (object, listener, eventKey, mutateStore) => {
object.events = object.events || {};
if (!object.events[eventKey + REVERT_MUTATION]) {
object.events[eventKey + REVERT_MUTATION] = mutateStore(eventProps => {
// eslint-disable-next-line no-sequences
object.events[eventKey].reduceRight((event, l) => (l(event), event), {
shared: {},
...eventProps
});
});
}
object.events[eventKey] = object.events[eventKey] || [];
object.events[eventKey].push(listener);
return () => {
let currentListeners = object.events[eventKey];
let index = currentListeners.indexOf(listener);
currentListeners.splice(index, 1);
if (!currentListeners.length) {
delete object.events[eventKey];
object.events[eventKey + REVERT_MUTATION]();
delete object.events[eventKey + REVERT_MUTATION];
}
}
};
let STORE_UNMOUNT_DELAY = 1000;
let onMount = (store, initialize) => {
let listener = () => {
let destroy = initialize();
if (destroy) store.events[UNMOUNT].push(destroy);
};
return on(store, listener, MOUNT, runListeners => {
let originListen = store.listen;
store.listen = arg => {
if (!store.lc && !store.active) {
runListeners();
store.active = true;
}
return originListen(arg)
};
let originOff = store.off;
store.events[UNMOUNT] = [];
store.off = () => {
setTimeout(() => {
if (store.active && !store.lc) {
store.active = false;
for (let destroy of store.events[UNMOUNT]) destroy();
store.events[UNMOUNT] = [];
originOff();
}
}, STORE_UNMOUNT_DELAY);
};
return () => {
store.listen = originListen;
store.off = originOff;
}
})
};
let listenerQueue = [];
let notifyId = 0;
let atom = initialValue => {
let currentListeners;
let nextListeners = [];
let store = {
lc: 0,
value: initialValue,
set(data) {
store.value = data;
store.notify();
},
get() {
if (!store.lc) {
store.listen(() => {})();
}
return store.value
},
notify(changedKey) {
currentListeners = nextListeners;
let runListenerQueue = !listenerQueue.length;
for (let i = 0; i < currentListeners.length; i++) {
listenerQueue.push(currentListeners[i], store.value, changedKey);
}
if (runListenerQueue) {
notifyId++;
for (let i = 0; i < listenerQueue.length; i += 3) {
listenerQueue[i](listenerQueue[i + 1], listenerQueue[i + 2]);
}
listenerQueue.length = 0;
}
},
listen(listener) {
if (nextListeners === currentListeners) {
nextListeners = nextListeners.slice();
}
store.lc = nextListeners.push(listener);
return () => {
if (nextListeners === currentListeners) {
nextListeners = nextListeners.slice();
}
let index = nextListeners.indexOf(listener);
if (~index) {
nextListeners.splice(index, 1);
store.lc--;
if (!store.lc) store.off();
}
}
},
subscribe(cb) {
let unbind = store.listen(cb);
cb(store.value);
return unbind
},
off() {} /* It will be called on last listener unsubscribing.
We will redefine it in onMount and onStop. */
};
return store
};
let map = (value = {}) => {
let store = atom(value);
store.setKey = function (key, newValue) {
if (typeof newValue === 'undefined') {
if (key in store.value) {
store.value = { ...store.value };
delete store.value[key];
store.notify(key);
}
} else if (store.value[key] !== newValue) {
store.value = {
...store.value,
[key]: newValue
};
store.notify(key);
}
};
return store
};
let computed = (stores, cb) => {
if (!Array.isArray(stores)) stores = [stores];
let diamondNotifyId;
let diamondArgs = [];
let run = () => {
let args = stores.map(store => store.get());
if (
diamondNotifyId !== notifyId ||
args.some((arg, i) => arg !== diamondArgs[i])
) {
diamondNotifyId = notifyId;
diamondArgs = args;
derived.set(cb(...args));
}
};
let derived = atom();
onMount(derived, () => {
let unbinds = stores.map(store =>
store.listen(run, cb)
);
run();
return () => {
for (let unbind of unbinds) unbind();
}
});
return derived
};
const HowManyDaysSinceLastIsekaiReset = atom(0);
async function readHowManyDaysSinceLastIsekaiReset() {
const lastIsekaiReset = await getStoredValue('lastIsekaiReset') ?? null;
if (lastIsekaiReset) {
HowManyDaysSinceLastIsekaiReset.set((new Date().getTime() - lastIsekaiReset) / 1000 / 60 / 60 / 24);
}
}
async function isIsekaiHaveBeenResetSinceLastVisit() {
const storedLevel = await getStoredValue('isekaiLevel');
const currentLevel = Number(document.getElementById('level_readout')?.textContent?.match(/Lv.(\d+)/)?.[1]);
if (currentLevel !== storedLevel) {
await setStoredValue('isekaiLevel', currentLevel);
if (storedLevel && currentLevel < storedLevel) {
await setStoredValue('lastIsekaiReset', new Date().getTime());
// Isekai has been reset
return true;
}
}
return false;
}
const EFFECTS_AFFECTING_SCAN_REAULT = [
'nbardead.png',
'imperil.png',
'firedot.png',
'coldslow.png',
'elecweak.png',
'windmiss.png',
'holybreach.png',
'darknerf.png'
];
const NOW = new Date().getTime();
const checkScanResultValidity = (mkey)=>{
const monsterHtml = document.getElementById(mkey)?.innerHTML;
if (monsterHtml) {
return !EFFECTS_AFFECTING_SCAN_REAULT.some((effectImg)=>monsterHtml.includes(effectImg)
);
}
return false;
};
const getMonsterHighlightColor = (monsterInfo)=>{
if (typeof SETTINGS.highlightMonster === 'object') {
for (const [color, matcher] of Object.entries(SETTINGS.highlightMonster)){
if (matcher instanceof RegExp) {
if (matcher.test(JSON.stringify(monsterInfo))) {
return color;
}
} else if (typeof matcher === 'function') {
if (matcher(monsterInfo)) {
return color;
}
} else if (typeof matcher === 'string') {
if (new RegExp(matcher).test(JSON.stringify(monsterInfo))) {
return color;
}
}
}
}
return false;
};
const isMonsterNeedScan = (mkey, randomness, lastUpdate)=>{
const isDead = mkey && Boolean(document.getElementById(mkey)?.innerHTML.includes('nbardead.png'));
if (isDead) return false;
randomness ?? (randomness = Math.floor(Math.random() * Math.floor(SETTINGS.scanExpireDays / 5)) + 1);
if (lastUpdate) {
// How many days since lastUpdate to now.
const passedDays = Math.round((NOW - lastUpdate) / (24 * 60 * 60 * 1000));
if (isIsekai()) {
const howManyDaysSinceLastIsekaiReset = HowManyDaysSinceLastIsekaiReset.get();
if (howManyDaysSinceLastIsekaiReset && passedDays > howManyDaysSinceLastIsekaiReset && howManyDaysSinceLastIsekaiReset > randomness) {
return true;
}
// In isekai monsters won't get update. If lastUpdate is not undefined,
// it means the monster is already in the database, no need to scan it again
return false;
}
if (passedDays < SETTINGS.scanExpireDays + randomness) {
return false;
}
}
// When lastUpdate is undefined / null, it means it is not in local database.
// That also means it requires scan.
return true;
};
const isTruthy = (x)=>Boolean(x)
;
const MonstersInCurrentRound = map({});
// Store element id instead of element itself, as HentaiVerse always replace the whole element every turn
const MonstersAndMkeysInCurrentRound = atom({});
const MonsterLastUpdate = map({});
const MonstersAndTheirRandomness = atom({});
const MonsterNeedScan = computed([
MonstersInCurrentRound,
MonstersAndMkeysInCurrentRound,
MonsterLastUpdate,
MonstersAndTheirRandomness
], (monsters, monsterAndMkey, monsterLastUpdate, monstersAndTheirRandomness)=>{
return Object.entries(monsters).map(([monsterName, monsterInfo])=>{
const mkey = monsterAndMkey[monsterName];
const randomness = monstersAndTheirRandomness[monsterName];
if (mkey) {
if (checkScanResultValidity(mkey)) {
// If there is no monsterInfo, it means the monster need to be scanned
if (!monsterInfo) return {
name: monsterName,
mkey
};
const lastUpdate = monsterLastUpdate[monsterInfo.monsterId];
if (isMonsterNeedScan(mkey, randomness, lastUpdate)) {
return {
mkey,
name: monsterName
};
}
}
}
return null;
}).filter(isTruthy);
});
const MonsterNeedHighlight = computed([
MonstersInCurrentRound,
MonstersAndMkeysInCurrentRound
], (monsters, monsterAndMkey)=>{
return Object.entries(monsters).map(([monsterName, monsterInfo])=>{
const mkey = monsterAndMkey[monsterName];
const color = monsterInfo ? getMonsterHighlightColor(monsterInfo) : false;
if (color && mkey) {
return {
mkey,
color
};
}
return null;
}).filter(isTruthy);
});
const StateSubscribed = atom(false);
/** @jsxImportSource million */ /** Will execute at per round start */ async function inBattle$1() {
/**
* The implementation is from Monsterbation, to prevent inBattle from calling twice
* It is a comfirmed tampermonkey bug, see https://github.com/Tampermonkey/tampermonkey/issues/1218
*/ // Check if #monsterdb_in_battle_invoked has been injected
if (document.getElementById('monsterdb_in_battle_invoked')) {
logger.debug('Race condition of inBattle has been detected and mitigated!');
return;
}
// Inject #monsterdb_in_battle_invoked
const invokedEl = document.createElement('div');
invokedEl.id = 'monsterdb_in_battle_invoked';
invokedEl.style.display = 'none';
document.getElementById('battle_right')?.appendChild(invokedEl); // battle_right contains monster elements
const logEl = document.getElementById('textlog');
if (logEl && logEl.firstChild) {
await tasksRunAtStartOfPerRound();
const mo = new MutationObserver(tasksRunDuringTheBattle);
mo.observe(logEl.firstChild, {
childList: true
});
}
if (StateSubscribed.get() === false) {
// Show monster info box
if (SETTINGS.showMonsterInfoBox) {
let showMonsterInfoBoxRafId = null;
MonstersInCurrentRound.subscribe((monstersInCurrentRound)=>{
// This to prevent rendering when new round starts and monsters data is still being fetching
if (showMonsterInfoBoxRafId) {
window.cancelAnimationFrame(showMonsterInfoBoxRafId);
}
showMonsterInfoBoxRafId = window.requestAnimationFrame(()=>{
const allMonsterStatus = Object.values(monstersInCurrentRound);
// There is first monsterdb_info, then we have monsterdb_container
if (!document.getElementById('monsterdb_info')) {
createMonsterInfoBox();
}
const container = document.getElementById('monsterdb_container');
if (container) {
render(container, /*#__PURE__*/ jsx(MonsterInfo, {
allMonsterStatus: allMonsterStatus
}));
}
});
});
}
StateSubscribed.set(true);
}
}
/** To prevent multiple users scan the same monster over and over again, some randomness has been added. Generate it once per monster */ const createRandomness = ()=>isIsekai() ? Math.floor(Math.random() * Math.floor(SETTINGS.scanExpireDays / 3)) : Math.floor(Math.random() * Math.floor(SETTINGS.scanExpireDays / 5)) + 1
;
/** Tasks like "get monster id and monster name" only have to run at the start of per round */ async function tasksRunAtStartOfPerRound() {
const monsterInTheRoundNameIdMap = new Map();
if (document.getElementById('textlog')?.textContent?.includes('Spawned')) {
[
...document.querySelectorAll('#textlog > tbody > tr')
].forEach((logEl)=>{
// Get Monster Name & ID
if (logEl.textContent?.trim().startsWith('Spawned')) {
const monsterNameAndId = parseMonsterNameAndId(logEl.textContent);
if (monsterNameAndId) {
monsterInTheRoundNameIdMap.set(monsterNameAndId.monsterName, monsterNameAndId.monsterId);
}
}
});
// Update Monsters's ID only when browser is idle
window.requestIdleCallback(()=>MONSTER_NAME_ID_MAP.updateMany([
...monsterInTheRoundNameIdMap.entries()
])
, {
timeout: 2000
});
}
// I am not sure the order that monster showed up in battle log
// is consistent with actually in #battle_right. It is unstable
// and unreliable method. So I will manually get monster info
// directly from DOM.
// Use Map to ensure the order of monsters
const monsters = new Map();
const mkeys = {};
const monsterLastUpdates = {};
const monstersRandomness = {};
await Promise.all([
...document.getElementsByClassName('btm1')
].map(async (el)=>{
const mkey = el.id;
const monsterName = el.getElementsByClassName('btm3')[0].textContent?.trim();
if (mkey && monsterName) {
mkeys[monsterName] = mkey;
monstersRandomness[monsterName] = createRandomness();
// Add a null monster info to monsters object first, to prevent race condition later on.
monsters.set(monsterName, null);
const mid = monsterInTheRoundNameIdMap.get(monsterName) ?? await MONSTER_NAME_ID_MAP.get(monsterName);
if (mid) {
const encodedMonsterInfo = await LOCAL_MONSTER_DATABASE.get(mid);
if (encodedMonsterInfo) {
const monsterInfo = convertEncodedMonsterInfoToMonsterInfo(mid, encodedMonsterInfo);
const lastUpdate = encodedMonsterInfo[EncodedMonsterDatabase.EMonsterInfo.lastUpdate];
monsters.set(monsterName, monsterInfo);
monsterLastUpdates[mid] = lastUpdate;
return;
}
}
monsters.set(monsterName, null);
}
}));
MonstersInCurrentRound.set(Object.fromEntries(monsters.entries()));
MonstersAndMkeysInCurrentRound.set(mkeys);
MonsterLastUpdate.set(monsterLastUpdates);
MonstersAndTheirRandomness.set(monstersRandomness);
logger.debug('MonstersInCurrentRound', MonstersInCurrentRound.get());
highlightMonsters();
}
async function tasksRunDuringTheBattle() {
highlightMonsters();
// Handle batleLog
const logEls = document.querySelectorAll('#textlog > tbody > tr');
for (const { innerHTML: logHtml } of logEls){
// This turn is over, do not proceed
if (logHtml.includes('<td class="tls">')) break;
// This turn scan a monster
if (logHtml.includes('Scanning')) {
// Every turn you'd only scan one monster
// eslint-disable-next-line no-await-in-loop
const scanResult = await parseScanResult(logHtml);
if (scanResult) {
const { monsterName } = scanResult;
logger.info('Scanned a monster:', monsterName);
logger.debug('Scan result', scanResult);
const scannedMonsterMkey = MonstersAndMkeysInCurrentRound.get()[monsterName];
if (scannedMonsterMkey && checkScanResultValidity(scannedMonsterMkey)) {
logger.info(`Scan results for ${monsterName} is now queued to submit`);
window.requestIdleCallback(()=>submitScanResults(scanResult)
, {
timeout: 3000
});
// We already fetch monsterId during parseScanResult
LOCAL_MONSTER_DATABASE.set(scanResult.monsterId, convertMonsterInfoToEncodedMonsterInfo(scanResult));
MonsterLastUpdate.setKey(scanResult.monsterId, Date.now());
MonstersInCurrentRound.setKey(monsterName, scanResult);
// Highlight monsters again with newly scanned monsters' state
highlightMonsters();
} else {
logger.warn(`${monsterName} is not legible for scan, ignoring the scan result!`);
}
}
break;
}
}
}
let highlightNeedScanMonsterRafId = null;
let highlightMonsterRafId = null;
const highlightScanColor = SETTINGS.scanHighlightColor === true ? 'coral' : SETTINGS.scanHighlightColor;
const isHighlightMonsterEnabled = typeof SETTINGS.highlightMonster === 'object' && Object.keys(SETTINGS.highlightMonster).length > 0;
// HentaiVerse always completely replace monsters element, so we need to re-highlight monsters on every turn.
// Blame Tenboro for his lazily adapting new web technology.
function highlightMonsters() {
// Highlight Need Scanned Monsters
if (highlightNeedScanMonsterRafId) {
window.cancelAnimationFrame(highlightNeedScanMonsterRafId);
}
if (highlightScanColor) {
const needScanMonsters = MonsterNeedScan.get();
highlightNeedScanMonsterRafId = window.requestAnimationFrame(()=>{
needScanMonsters.forEach((needScanMonster)=>{
if (needScanMonster.mkey) {
const monsterBtm2El = document.getElementById(needScanMonster.mkey)?.querySelector('div.btm2');
if (monsterBtm2El) {
monsterBtm2El.style.backgroundColor = highlightScanColor;
}
}
});
});
}
if (isHighlightMonsterEnabled) {
// Highlight Monster
if (highlightMonsterRafId) {
window.cancelAnimationFrame(highlightMonsterRafId);
}
const needHighlightMonsters = MonsterNeedHighlight.get();
highlightMonsterRafId = window.requestAnimationFrame(()=>{
needHighlightMonsters.forEach((needHighlightMonster)=>{
const { color , mkey } = needHighlightMonster;
const monsterBtm2El = document.getElementById(mkey)?.querySelector('div.btm2');
if (monsterBtm2El) {
monsterBtm2El.style.backgroundColor = color;
}
});
});
}
}
async function updateLocalDatabase(force = false) {
const currentDate = getUTCDate();
const lastUpdateDate = await getStoredValue('lastUpdateV2');
const lastUpdateIsekaiDate = await getStoredValue('lastUpdateIsekaiV2');
if (isIsekai()) {
logger.info(`Local database (isekai) last updated: ${lastUpdateIsekaiDate}`);
} else {
logger.info(`Local database last updated: ${lastUpdateDate}`);
}
const needUpdateLocalDatabaseFromRemoteServer = force || (isIsekai() ? lastUpdateIsekaiDate : lastUpdateDate) !== currentDate || (await (isIsekai() ? LOCAL_MONSTER_DATABASE_ISEKAI : LOCAL_MONSTER_DATABASE_PERSISTENT).get(20))?.[EncodedMonsterDatabase.EMonsterInfo.monsterName] !== 'Konata';
if (needUpdateLocalDatabaseFromRemoteServer) {
try {
logger.info('Downloading Monster Database from the server...');
const resp = await fetch(isIsekai() ? 'https://hv-monsterdb-data.skk.moe/isekai.json' : 'https://hv-monsterdb-data.skk.moe/persistent.json');
const data = await resp.json();
// Use window.requestIdleCallback again since conevrt database is a CPU intensive task.
window.requestIdleCallback(async ()=>{
logger.info('Processing Monster Database...');
MONSTER_NAME_ID_MAP.updateMany(data.map((monster)=>[
monster.monsterName,
monster.monsterId
]
));
const db = data.map((monster)=>{
const EncodedMonsterInfo = convertMonsterInfoToEncodedMonsterInfo(monster);
return [
monster.monsterId,
EncodedMonsterInfo
];
});
logger.info(`${data.length} monsters' information processed.`);
if (isIsekai()) {
await Promise.all([
LOCAL_MONSTER_DATABASE_ISEKAI.updateMany(db),
setStoredValue('lastUpdateIsekaiV2', currentDate)
]);
} else {
await Promise.all([
LOCAL_MONSTER_DATABASE_PERSISTENT.updateMany(db),
setStoredValue('lastUpdateV2', currentDate)
]);
}
}, {
timeout: 10000
});
} catch (e) {
logger.error(e);
showPopup(`There is something wrong when trying to update the local database from the server!\n\n${JSON.stringify(e)}`);
}
} else {
logger.info('There is no need to update local database.');
/** Database Migration, only do when no need to download the remote database */ databaseMigration();
}
}
async function databaseMigration() {
// Migrate all old userscript storage to IndexedDB
const [monsterIdMap, databaseV2, databaseIsekaiV2] = await Promise.all([
getStoredValue('monsterIdMap'),
getStoredValue('databaseV2'),
getStoredValue('databaseIsekaiV2')
]);
return Promise.all([
monsterIdMap && (logger.debug('Migrating old monsterIdMap to IndexedDB'), MONSTER_NAME_ID_MAP.updateMany(Object.entries(monsterIdMap))),
databaseV2 && (logger.debug('Migrating old databaseV2 to IndexedDB'), LOCAL_MONSTER_DATABASE_PERSISTENT.updateMany(Object.entries(databaseV2).map(([k, v])=>{
const newId = Number(k);
if (Number.isInteger(newId)) return [
newId,
v
];
return null;
}))),
databaseIsekaiV2 && (logger.debug('Migrating old databaseIsekaiV2 to IndexedDB'), LOCAL_MONSTER_DATABASE_ISEKAI.updateMany(Object.entries(databaseIsekaiV2).map(([k, v])=>{
const newId = Number(k);
if (Number.isInteger(newId)) return [
newId,
v
];
return null;
}))),
// Remove version 2 from the userscript storage
removeStoredValue('monsterIdMap'),
removeStoredValue('databaseV2'),
removeStoredValue('databaseIsekaiV2'),
// Remove old version 1 database
removeStoredValue('database'),
removeStoredValue('databaseIsekai'),
removeStoredValue('lastUpdate'),
removeStoredValue('lastUpdateIsekai')
]);
}
/**
* Although Monster Database script have tried best to be compatible with Monsterbation's ajaxRound feature, in order to workaround TamperMonkey on Firefox cross userscript sandbox event handler issue (one userscript can't recevied a document Event event from another one), a fallback API is provided.
* There is no need to worry about if "inBattle" will be called serveral time as it has a built-in race condition mitigation approach.
*
* ```js
* window.HVMonsterDB?.inBattle();
*
* // If your prefer ES5 approach (no optional chain)
* if (window.HVMonsterDB && window.HVMonsterDB.inBattle) {
* window.HVMonsterDB.inBattle();
* }
* ```
*/ function inBattle() {
if (!isFightingInBattle()) {
logger.error('"inBattle" method is only avaliable during the battle!');
throw new Error('"inBattle" method is only avaliable during the battle!');
}
inBattle$1();
}
/**
* Get Monster ID by Monster Name. Could be used in some highlight matcher?
*
* ```js
* window.HVMonsterDB.getMonsterIdByName('Flying Spaghetti Monster')
* // 32
* ```
*/ async function getMonsterIdByName(name) {
const monsterId = await MONSTER_NAME_ID_MAP.get(name);
return monsterId || null;
}
/**
* Get all monsters' information in the round (in order). Only avaliable during the battle
*
* ```js
* window.HVMonsterDB.getCurrentMonstersInformation();
*
* // {
* // mkey_1: { ... }
* // mkey_2: { ... }
* // mkey_3: { ... }
* // ...
* // }
* ```
*/ function getCurrentMonstersInformation() {
if (!isFightingInBattle()) {
logger.error('"getCurrentMonstersInformation" method is only avaliable during the battle!');
throw new Error('"getCurrentMonstersInformation" method is only avaliable during the battle!');
}
const results = {};
for (const [monsterName, monsterInfo] of Object.entries(MonstersInCurrentRound.get())){
const mkey = MonstersAndMkeysInCurrentRound.get()[monsterName];
if (mkey) {
results[mkey] = monsterInfo;
}
}
return results;
}
/**
* Get one monsters' information by its name
*
* ```js
* window.HVMonsterDB.getMonsterInfoByName('Yggdrasil');
* // { ... }
* ```
*/ async function getMonsterInfoByName(name) {
const monsterId = await MONSTER_NAME_ID_MAP.get(name);
if (monsterId) {
const encodedMonsterInfo = await LOCAL_MONSTER_DATABASE.get(monsterId);
if (encodedMonsterInfo) {
return convertEncodedMonsterInfoToMonsterInfo(monsterId, encodedMonsterInfo);
}
}
return null;
}
/**
* Get a list of the monsters in the current round that require scan. Only avaliable during the battle.
*
* ```js
* window.HVMonsterDB.getCurrentMonstersInformation();
*
* []
* ```
*/ function getCurrentNeedScannedMonsters() {
if (!isFightingInBattle()) {
logger.error('"getCurrentNeedScannedMonsters" method is only avaliable during the battle!');
throw new Error('"getCurrentNeedScannedMonsters" method is only avaliable during the battle!');
}
return MonsterNeedScan.get();
}
/**
* DEBUG Method, force update local database from the server (only avaliable when "debug" setting is enabled)
*
* ```js
* window.HVMonsterDB.forceUpdateLocalDatabase();
* ```
*/ function forceUpdateLocalDatabase() {
if (!SETTINGS.debug) {
logger.error('"forceUpdateLocalDatabase" method is only avaliable when "debug" setting is enabled!');
return Promise.reject(new Error('"forceUpdateLocalDatabase" method is only avaliable when "debug" setting is enabled!'));
}
return updateLocalDatabase(true);
}
/**
* @deprecated DEBUG Method, dump raw local data base (only avaliable when "debug" setting is enabled)
*
* ```js
* window.HVMonsterDB.dumpRawLocalDataBase();
* ```
*/ async function dumpRawLocalDataBase() {
if (SETTINGS.debug) {
logger.warn('"dumpRawLocalDataBase" method is deprecated, please view the raw local database direcly in the DevTools -> IndexedDB!');
await Promise.all([
LOCAL_MONSTER_DATABASE_PERSISTENT,
LOCAL_MONSTER_DATABASE_ISEKAI
].map(async (db)=>{
const rawLocalDataBase = await db.getAll();
logger.info(JSON.stringify(rawLocalDataBase.map(([monsterId, encodedMonsterInfo])=>{
if (encodedMonsterInfo) {
return convertEncodedMonsterInfoToMonsterInfo(monsterId, encodedMonsterInfo);
}
return null;
})));
}));
} else {
logger.error('"dumpRawLocalDataBase" method is only avaliable when "debug" setting is enabled!');
return Promise.reject(new Error('"dumpRawLocalDataBase" method is only avaliable when "debug" setting is enabled!'));
}
}
(async ()=>{
logger.setDebugMode(SETTINGS.debug);
const hasTextLog = isFightingInBattle();
const hasRiddleMaster = Boolean(document.getElementById('riddlemaster'));
await Promise.all([
retrieveTmpValue(),
// Read how many days since last isekai reset for further usage
readHowManyDaysSinceLastIsekaiReset()
]);
if (hasTextLog || hasRiddleMaster) {
// Store in-memory value back to storage before window refresh / closes
window.addEventListener('beforeunload', storeTmpValue);
document.addEventListener('DOMContentLoaded', inBattle$1);
// Both "HVReload" and "DOMContentLoaded" are listened by "HentaiVerse Chinese Translation" userscript during battle
// I don't know what "HVReload" is, but I guess "something" will dispatch it per round start
document.addEventListener('HVReload', inBattle$1);
if (hasTextLog) {
/**
* We have already listened 'DOMContentLoaded' soon. If the browser inject the script right before
* DOMContentLoaded event, we will call inBattle() again, and let race condition mitigation does its job.
*/ inBattle$1();
}
} else {
// Out of Battle
// Just check if Isekai has been reset
if (isIsekai()) {
await isIsekaiHaveBeenResetSinceLastVisit();
}
// Trigger database update when out of battle.
// "updateLocalDatabase" method will only update local database once a day.
window.requestIdleCallback(()=>updateLocalDatabase()
); // Use window.requesrIdleCallback to avoid performance impact.
}
})();
exports.dumpRawLocalDataBase = dumpRawLocalDataBase;
exports.forceUpdateLocalDatabase = forceUpdateLocalDatabase;
exports.getCurrentMonstersInformation = getCurrentMonstersInformation;
exports.getCurrentNeedScannedMonsters = getCurrentNeedScannedMonsters;
exports.getMonsterIdByName = getMonsterIdByName;
exports.getMonsterInfoByName = getMonsterInfoByName;
exports.inBattle = inBattle;
return exports;
})({});