battle_damage_tooltip

Просмотр урона любого отряда, улучшенный лог боя, фиксы и фичи (перечислены в описаниее)

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name   battle_damage_tooltip
// @name:en battle_damage_tooltip
// @namespace    http://tampermonkey.net/
// @version      3.2.2
// @description  Просмотр урона любого отряда, улучшенный лог боя, фиксы и фичи (перечислены в описаниее)
// @description:en  View damage of any stack, improved battle log, plus fixes and additional features (see description)
// @author       Something begins
// @license      MIT
// @match       https://www.heroeswm.ru/war*
// @match       https://my.lordswm.com/war*
// @match       https://www.lordswm.com/war*
// @match       https://mirror.heroeswm.ru/war*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_setValue
// @grant        GM_getValue
// @grant unsafeWindow
// ==/UserScript==

const keyboardKeycodes = {
    "backspace": 8,
    "tab": 9,
    "enter": 13,
    "shift": 16,
    "ctrl": 17,
    "alt": 18,
    "pause": 19,
    "capslock": 20,
    "escape": 27,
    "space": 32,
    "pageup": 33,
    "pagedown": 34,
    "end": 35,
    "home": 36,
    "leftarrow": 37,
    "uparrow": 38,
    "rightarrow": 39,
    "downarrow": 40,
    "insert": 45,
    "delete": 46,
    "0": 48,
    "1": 49,
    "2": 50,
    "3": 51,
    "4": 52,
    "5": 53,
    "6": 54,
    "7": 55,
    "8": 56,
    "9": 57,
    "a": 65,
    "b": 66,
    "c": 67,
    "d": 68,
    "e": 69,
    "f": 70,
    "g": 71,
    "h": 72,
    "i": 73,
    "j": 74,
    "k": 75,
    "l": 76,
    "m": 77,
    "n": 78,
    "o": 79,
    "p": 80,
    "q": 81,
    "r": 82,
    "s": 83,
    "t": 84,
    "u": 85,
    "v": 86,
    "w": 87,
    "x": 88,
    "y": 89,
    "z": 90,
    "leftwindowkey": 91,
    "rightwindowkey": 92,
    "selectkey": 93,
    "numpad0": 96,
    "numpad1": 97,
    "numpad2": 98,
    "numpad3": 99,
    "numpad4": 100,
    "numpad5": 101,
    "numpad6": 102,
    "numpad7": 103,
    "numpad8": 104,
    "numpad9": 105,
    "multiply": 106,
    "add": 107,
    "subtract": 109,
    "decimalpoint": 110,
    "divide": 111,
    "f1": 112,
    "f2": 113,
    "f3": 114,
    "f4": 115,
    "f5": 116,
    "f6": 117,
    "f7": 118,
    "f8": 119,
    "f9": 120,
    "f10": 121,
    "f11": 122,
    "f12": 123,
    "numlock": 144,
    "scrolllock": 145,
    "semicolon": 186,
    "equal": 187,
    "comma": 188,
    "dash": 189,
    "period": 190,
    "forwardslash": 191,
    "graveaccent": 192,
    "openbracket": 219,
    "backslash": 220,
    "closebracket": 221,
    "singlequote": 222
};
function getBindCode(key, fallback) {
    const v = GM_getValue(key);
    const num = parseInt(v);
    return !isNaN(num) ? num : fallback;
}

function setBindCode(key, code) {
    if (code == null) GM_setValue(key, undefined);
    else GM_setValue(key, code);
}

function getBool(key, fallback) {
    const v = GM_getValue(key);
    if (v === undefined) return fallback;
    return v === "true" ? true : false;
}
let kb = {
    triggerKey:    getBindCode("kb_triggerKey", 18), // Alt
    seeDamage:    getBindCode("kb_seeDamage", 69),   // E
    seeMagShot:   getBindCode("kb_seeMagShot", 85),  // U
    autoBattle:   getBindCode("kb_autoBattle", 65),  // A
    toggleSpeed:  getBindCode("kb_toggleSpeed", 83), // S
    autoPlacement:getBindCode("kb_autoPlacement",82),// R
    backToGame:   getBindCode("kb_backToGame", 90),  // Z
    startBattle:  getBindCode("kb_startBattle",66),  // B
    useTrigger:   getBool("kb_useTrigger", true),
    filterLog:    getBindCode("kb_filterLog", 84), // T
};
function isActive(code) {
    return pressedKeys.has(code);
}

const pressedKeys = new Set();
/* ===========================
   i18n (ru/en)
   =========================== */
const I18N = {
    ru: {
        openKeyBinds : "Открыть настройку горячих кнопок",
        advancedTitle: "Расширенные",
        keybindHint: "Нажмите на поле и нажмите клавишу…",
        keybindUnassigned: "Не назначено",
        keybindInvalid: "Эта клавиша не поддерживается",
        keybindUseTriggerLabel: "Использовать кнопку-активатор",
        keybindTriggerLabel: "Кнопка активатор для биндов ниже (удерживать)",
        keybindSeeDamageLabel: "Посмотреть урон",
        keybindSeeMagShotLabel: "Посмотреть траекторию «рельсы»",
        keybindAutoBattleLabel: "Автобой",
        keybindToggleSpeedLabel: "Вкл/выкл свою скорость анимации",
        keybindAutoPlacementLabel: "Авторасстановка",
        keybindBackToGameLabel: "Назад",
        keybindStartBattleLabel: "Начать бой",
        keybindFilterLog: "Отфильтровать лог по существу и открыть",
        // UI
        btnListCollapsed: "Список🔽",
        btnListOpen: "🔄",
        btnChangeSide: "Сменить сторону",
        chosenDistance: "Выбранное расстояние",
        distanceJumpCells: (n) => `Прыжок на <u>${n}</u> клеток`,
        damage: "Урон",
        damageTo: "Урон по",
        gateOwner: "Владелец",
        upravaPlaceholder: "Ник для управы",
        helpDamage: "урон",
        mobileFirstTimeAlert:
        "Кнопка с вопросительным знаком -- активация просмотра урона в скрипте battle_damage_tooltip. " +
        "Чтобы посмотреть урон, нужно, чтобы эта кнопка была активной. " +
        "Тап на атакующее существо (клетка с существом помечается красным цветом), затем тап на атакуемое существо (клетка синим цветом). " +
        "Урон будет в боевом чате",
        // Settings labels/tooltips
        sCoeffLabel: "⚖️коэф. урона",
        sCoeffTip:
        "отношение урон/хп (т.е. у кого больше всех коэф., с того выгоднее начинать. <br>" +
        "Работает в списке уронов если нажать на \"Список\" в чате)",
        sDistanceLabel: "Расстояние между стеками",
        sDistanceTip:
        "Расстояние между атакующим и защищающимся стеками. Выбирать расстояние стрелочками в текстовом поле снизу. <br>" +
        "Влияет на статус урона стрелка (ближний/дальний урон, кривая/прямая стрела), " +
        "разбег и прочие абилки, зависящие от расстояния. <br> " +
        "Если выставить \"Расстояние: 1\", то стрелок будет считаться заблокированным. " +
        "Если выставить расстояние больше 1, то стрелок будет считаться не заблокированным (даже если рядом с ним вражеское существо).",
        sAnimSpeedLabel: "Скорость анимации",
        sAnimSpeedTip:
        "Скорость боевых анимаций. <br> " +
        "Выбирать расстояние стрелочками в текстовом поле снизу или если зажать кнопку Alt и нажимать на стрелки клавиатуры.<br>" +
        "Включить/выключить анимацию [Alt + P (русская З)].<br>" +
        "Анимацию можно как ускорить, так и замедлить.<br>" +
        "Верхний потолок у скорости 20, нижнего нету.<br>" +
        "Негативный показатель означает скорость ниже возможной гвдшной.",
        sMagCalcLabel: "Расчет маг. урона",
        sMagCalcTip: "Расчет магического урона не работает во время расстановки",
        // Combat calc strings
        hellfire: "Адское пламя",
        damageWord: "урона",
        abilityDamage: "Урон абилкой",
        kboDisclaimer: "(в КБО) это заклинание показывает <br> неправильный урон",
        poisonInaccuracy: "Погрешность +-10%",
        targets: "Целей:<br>",
        targetNo: "Цель №<br>",
        defaultCreature: "Высшие вампиры",
        // Spells
        spellNames: {
            meteor: "Метеоритный дождь",
            lighting: "Молния",
            implosion: "Взрыв",
            fireball: "Огненный шар",
            chainlighting: "Цепная молния",
            firewall: "Огненная стена",
            magicarrow: "Магическая стрела",
            stonespikes: "Каменные шипы",
            magicfist: "Магический кулак",
            icebolt: "Ледяная глыба",
            circle_of_winter: "Кольцо холода",
            swarm: "Осиный рой",
            angerofhorde: "Ярость орды",
            poison: "Разложение",
            stormcaller: "Зов бури",
            calllightning: "Зов молний",
            firearrow: "Огненная стрела",
            divinev: "Божественная месть"
        }
    },
    en: {
        openKeyBinds : "Open keybinds settings",
        advancedTitle: "Advanced",
        keybindHint: "Click a field and press a key…",
        keybindUnassigned: "Unassigned",
        keybindInvalid: "This key is not supported",
        keybindUseTriggerLabel: "Use activation key",
        keybindTriggerLabel: "Activation key for the keybinds below (hold)",
        keybindSeeDamageLabel: "Show damage",
        keybindSeeMagShotLabel: "Show piercing shot trajectory",
        keybindAutoBattleLabel: "Auto battle",
        keybindToggleSpeedLabel: "Toggle custom animation speed",
        keybindAutoPlacementLabel: "Auto placement",
        keybindBackToGameLabel: "Back",
        keybindStartBattleLabel: "Start battle",
        keybindFilterLog: "Filter log by one creature and open",
        // UI
        btnListCollapsed: "List🔽",
        btnListOpen: "🔄",
        btnChangeSide: "Switch side",
        chosenDistance: "Selected distance",
        distanceJumpCells: (n) => `Jump by <u>${n}</u> hexes`,
        damage: "Damage of",
        damageTo: "Damage to",
        gateOwner: "Owner",
        upravaPlaceholder: "Filter by nickname",
        helpDamage: "damage",
        mobileFirstTimeAlert:
        "The question-mark button enables damage preview in battle_damage_tooltip. " +
        "To view damage, the button must be active. Tap the attacker (the tile becomes red), then tap the target (the tile becomes blue). " +
        "Damage will be printed in the battle chat.",
        // Settings labels/tooltips
        sCoeffLabel: "⚖️ dmg coeff.",
        sCoeffTip:
        "damage/HP ratio (i.e., whoever has the highest coeff. is the best to focus first). <br>" +
        "Works in the damage list if you press \"List\" in chat.",
        sDistanceLabel: "Distance between stacks",
        sDistanceTip:
        "Distance between attacker and defender stacks. Change it with arrows in the number field below. <br>" +
        "Affects shooter status (melee/ranged damage, curved/straight shot), charge/run-up, and other distance-based abilities. <br>" +
        "If you set \"Distance: 1\", the shooter is considered blocked. " +
        "If you set distance > 1, the shooter is considered unblocked (even if an enemy is adjacent).",
        sAnimSpeedLabel: "Animation speed",
        sAnimSpeedTip:
        "Battle animation speed. <br> " +
        "Adjust with arrows in the field below, or hold Alt and press ArrowUp/ArrowDown.<br>" +
        "Toggle custom speed: [Alt + P].<br>" +
        "You can speed up or slow down animations.<br>" +
        "Max speed is 20, there's no lower limit.<br>" +
        "Negative values mean slower than the default game minimum.",
        sMagCalcLabel: "Magic dmg calc",
        sMagCalcTip: "Magic damage calculation doesn't work during placement",
        // Combat calc strings
        hellfire: "Hellfire",
        damageWord: "damage",
        abilityDamage: "Ability damage",
        kboDisclaimer: "(in KBO) this spell shows <br> incorrect damage",
        poisonInaccuracy: "Inaccuracy ±10%",
        targets: "Targets:<br>",
        targetNo: "Target #<br>",
        defaultCreature: "Vampire Lords",
        // Spells
        spellNames: {
            meteor: "Meteor shower",
            lighting: "Lightning",
            implosion: "Implosion",
            fireball: "Fireball",
            chainlighting: "Chain lightning",
            firewall: "Firewall",
            magicarrow: "Magic arrow",
            stonespikes: "Stone spikes",
            magicfist: "Magic fist",
            icebolt: "Ice bolt",
            circle_of_winter: "Circle of winter",
            swarm: "Wasp swarm",
            angerofhorde: "Anger of the horde",
            poison: "Poison",
            stormcaller: "Storm call",
            calllightning: "Call lightning",
            firearrow: "Fire arrow",
            divinev: "Divine vengeance"
        }
    }
};

// Determine locale: stored override -> browser language -> hostname heuristic
const LOCALE = location.href.includes("www.lordswm.") ? "en" : "ru";

// Simple translator (supports string or function entries)
function t(key, ...args) {
    const pack = I18N[LOCALE] || I18N.en;
    const val = pack[key] ?? I18N.en[key];
    if (typeof val === "function") return val(...args);
    return val ?? key;
}
const savedHPs = JSON.parse(localStorage.getItem("savedHPs")) || {};
const parser = new DOMParser();
const creatureHPs = {
    ...savedHPs,
"brevno": 100, "hellgate": 100, "derevo": 100, "house2": 100, "house1": 100, "house3": 100, "witchhouse": 100, "sbor1": 100, "imp_tent1": 100, "imp_tent2": 100, "imp_tent3": 100, "lamp": 100, "logovo3": 100, "logovo1": 100, "logovo2": 100, "grob1": 100, "grob2": 100, "vdutl": 100, "sknor": 100, "logovo4": 100, "sarkofag": 100, "sklep": 100, "yurt": 100, "elspawn": 100, "fabrik": 100, "gnh2": 100, "gnh1": 100, "gnh3": 100, "magictower": 0, "ballista": 200, "barrier": 50, "archtower": 10, "castertower": 10, "shroom2": 100, "bochka": 50, "bombochka": 50, "bochkae": 50, "bochkaw": 50, "shroom1": 50, "drova1": 50, "drova2": 100, "stswordman": 50, "katapult": 500, "kletka": 10, "kletkabig": 10, "kotel": 20, "guardtower": 10, "firstaid": 100, "telega": 20, "pugalo": 70, "cannon": 100, "setkomet": 80, "stgnoll": 50, "chest": 40, "chestn": 40, "taran": 200, "tykva": 1, "dom1": 20, "dom2": 29, "dom3": 20, "tip1": 20, "tip2": 29, "tip3": 20, "sdom1": 20, "sdom2": 20, "sdom3": 20, "yaschik": 5, "cow2009": 39, "byll2009": 69, "drak2012": 36, "evilsnake": 73, "evilhorse": 84, "gorilla": 66, "mad_rat": 32, "rat2020": 20, "gorynych": 112, "gorynych2024": 124, "kozel": 55, "rooster": 77, "bull2021": 71, "evilcat": 31, "evilbunny": 111, "evildog": 88, "eviltiger2010": 100, "snake2013": 13, "snake2025": 25, "cow2021": 21, "kot2023": 23, "rabbit": 51, "krol2023": 53, "rat2008": 16, "horsy": 14, "mouse2020": 20, "monkey": 6, "sheep": 15, "petuh": 7, "piggy": 19, "pig2019": 19, "pig2007": 24, "dog": 18, "tigor2010": 21, "tiger2022": 22, "bpirate": 16, "zealot": 80, "hellhound6": 22, "hellcharger": 50, "zhryak": 99, "hellhound": 15, "reanimatorup": 27, "succubus6": 20, "troglodyteup": 6, "cerber": 28, "iceelb": 90, "wanizame": 31, "sharkguard": 25, "diamondgolem": 60, "yetiup": 290, "alchemist": 60, "angel": 180, "marksman": 10, "cman": 22, "marks": 28, "archangel": 220, "archdemon": 211, "archdevil": 199, "archlich": 55, "archmage": 30, "assassin": 14, "assida": 30, "ghostdragon": 150, "yaga": 43, "banshee": 110, "behemoth": 210, "poukai": 120, "demented": 28, "wbear": 55, "whitetiger": 35, "berserker": 25, "maiden": 16, "imp": 4, "beholder": 22, "blud": 7, "wisp": 10, "elfik": 37, "bober": 5000, "battlegriffin": 35, "silverunicorn": 77, "mcentaur": 10, "battlemage": 29, "mamont": 140, "slon": 100, "ill": 2, "grib": 20, "mechspiderup": 14, "mechspider": 12, "poacher": 16, "buffalo": 120, "shootpirate": 15, "valkyrie": 61, "vampire": 30, "vampirelord6": 95, "yascher": 44, "warmong": 20, "basilisk": 35, "deadmage": 27, "priestessup": 35, "cursed": 20, "giant": 100, "giantarch": 100, "upleviathan": 250, "wendigo": 25, "druideld": 38, "vestal": 25, "wraith": 100, "wyvern": 90, "necra": 40, "djinn_vizier": 50, "pitlord6": 280, "matriarch": 90, "water": 43, "chieftain": 48, "air": 30, "anubisup": 200, "battlerager": 30, "mercfootman": 24, "panther6": 90, "jaguar6": 85, "shieldguard": 12, "armorgnom": 55, "faeriedragon": 500, "vorovka": 30, "thiefmage": 30, "thiefwarrior": 45, "thiefarcher": 40, "sunrider": 90, "seraph2": 220, "vampirelord": 35, "masterlich": 55, "highwayman": 24, "harpy": 15, "harpyhag": 15, "harpooner": 10, "praetorian": 32, "krab": 50, "bigspider": 55, "snegovik": 100, "lizard": 25, "hydra": 80, "darkeye": 26, "upseamonster": 105, "rotzombie": 23, "gnoll": 6, "gnollboss": 36, "gnollsh": 6, "gnompirate": 100, "goblin": 3, "goblinarcher": 3, "goblinmag": 3, "goblinhunter6": 26, "trapper": 7, "goblinshaman": 5, "gogachi": 13, "dgolem": 350, "brute": 8, "mountaingr": 12, "gremlin": 5, "saboteurgremlin": 6, "gekkonup": 21, "fungus": 29, "griffon": 30, "griffin": 75, "igriffin": 85, "mauler6": 30, "thunderlord": 120, "axegnom": 10, "nomadup": 33, "deserter": 25, "succubusmis": 30, "pitfiend6": 270, "smalllizard": 13, "djinn": 40, "djinn_sultan": 45, "gnollumup": 8, "savageent": 175, "robber": 5, "eadaughter": 35, "sdaughter": 35, "gnollka": 6, "mikrodragon": 30, "drak2024": 24, "ancientbehemoth": 250, "basiliskup": 40, "wendigoup": 35, "ancientpig": 15, "amummy": 80, "ancienent": 181, "sprite": 6, "lumberman": 4, "mechdron": 7, "druid": 34, "minidragon": 60, "ghostshaman": 200, "poltergeist": 20, "forestspirit": 50, "mizukami": 76, "ocean": 30, "springspirit": 70, "brigandup": 6, "devil": 166, "vermin": 6, "deertaur": 50, "unicorn": 57, "monk": 20, "iron_golem": 18, "pearlp": 22, "horse": 70, "krokodilmag": 60, "runekeeper": 65, "runepriest": 60, "priestmoon": 50, "priestess": 35, "gnomka": 40, "priestsun": 55, "vsad_unit": 200, "varan": 60, "charmer": 9, "bokopor": 40, "zasad": 70, "vindicator": 23, "defender": 7, "greendragon": 200, "earth": 75, "evileye": 22, "evilcat2023": 23, "evilbunny2023": 123, "eviltiger2022": 122, "golddragon": 169, "zombie": 17, "lacerator": 85, "traitor": 9, "alchemistup": 68, "emeralddragon": 200, "impergriffin": 35, "inquisitor": 80, "deserterup": 30, "seducer": 26, "efreeti": 90, "efreetisultan": 100, "yeti": 280, "boar": 17, "icegiant": 100, "stoneman": 100, "stone_gargoyle": 15, "kammon": 28, "kamnegryz": 55, "kamneed": 45, "kappa": 21, "vedma": 40, "gop": 20, "fcentaur": 6, "mcentaur6": 80, "kirin": 255, "caty": 21, "klop": 12, "zerg": 40, "vampireprince": 40, "kobra": 40, "outlaw": 6, "colossus": 175, "hellkon": 66, "pikeman": 15, "coralp": 18, "rustdragon": 750, "piratkaup": 12, "apirate": 13, "brawler": 20, "skeleton6": 18, "skgiant": 72, "bonehydra": 60, "bonedragon": 150, "skeletons6": 23, "dleviathan": 145, "skgiantarch": 67, "bonelizard": 30, "nomad": 30, "ncentaur": 9, "nightmare": 66, "reddragon": 235, "crusader": 30, "suncrusader": 95, "peasant": 4, "peasantw": 3, "crystaldragon": 200, "redlizard": 35, "bloodeyecyc": 235, "vampire6": 80, "crusher6": 36, "serpentfly": 20, "darkelf": 41, "rakshasa_kshatra": 135, "kensei": 90, "kenshi": 80, "stonemanup": 110, "lavadragon": 275, "azuredragon": 1000, "scout": 10, "banditka": 8, "squire": 26, "leviathan": 200, "dleviathanup": 175, "iceelup": 45, "chuvakup": 33, "yukionna": 72, "icedemonup": 55, "iceddragon": 220, "icequeenup": 39, "iceel": 45, "leprekon": 7, "arcaneelf": 12, "bobbit": 6, "shaman": 110, "fairy": 70, "lilim": 24, "lich": 50, "lich6": 65, "dreamreaver6": 100, "stalker": 15, "blazingglory": 70, "archer": 7, "mage": 18, "exile": 28, "magicel": 80, "magmadragon": 280, "magneticgolem": 28, "megogachi": 13, "shamanka": 41, "raremamont": 110, "manticore": 80, "scream": 33, "maroder": 7, "smaster": 84, "skirmesher": 12, "masterhunter": 14, "mbreeder": 75, "pteroup": 27, "negro": 17, "bloodsister": 24, "bear": 22, "medusa": 25, "medusaup": 30, "gnollum": 6, "spearwielder": 10, "throwgnom": 24, "mechanic": 30, "minotaur": 31, "taskmaster": 40, "minotaurguard": 35, "cbal": 65, "dgolemup": 400, "necrodog": 8, "zpirateup": 170, "kappashoya": 25, "shellmonster": 90, "gnomon": 9, "priest": 54, "frankenstein": 530, "iceddragonup": 250, "ppirate": 25, "piratemonster": 190, "seamonster": 90, "mummy": 50, "pharaoh": 70, "ant": 27, "mushroom": 90, "enforcer": 7, "naga": 110, "warden": 39, "varg": 44, "dromad": 40, "wolfrider": 10, "hyenarider": 14, "boarrider": 14, "bearrider": 25, "darkrider": 40, "ambal": 100, "dromadup": 45, "wolfraider": 12, "celestial": 300, "ravenousghoul": 32, "harpy6": 29, "goblin6": 23, "centaur6": 70, "cyclop6": 330, "reptiloid": 80, "reptiloidup": 90, "dryad": 6, "pushkarup": 76, "obsgargoyle": 20, "yascherup": 49, "hotdog": 15, "hornedoverseer": 13, "firedragon": 230, "firebird": 65, "fire": 43, "ogre": 50, "ogrebrutal": 70, "ogremagi": 65, "ogreshaman": 55, "demoniac": 5, "fatpirateup": 120, "ppirateup": 30, "conscript": 6, "ravager": 100, "orc": 12, "orcchief": 18, "orcrubak": 20, "orcshaman": 13, "ork": 24, "officer": 70, "witchhunter": 38, "paladin": 100, "executioner": 40, "mechtankup": 140, "mechtank": 120, "spider": 9, "pegasus": 30, "footman": 16, "pitlord": 120, "deephydra": 125, "pitfiend": 110, "pity": 140, "mechguard": 50, "mechguardup": 55, "piratka": 10, "piratemonsterup": 200, "zpirate": 150, "piroman": 20, "krabup": 60, "charmerup": 10, "gribok": 16, "hungerplant": 70, "snowwolfup": 53, "snowarcher": 18, "snowarcherup": 22, "snowowlup": 81, "maniac": 23, "breeder": 70, "sister": 19, "deadptic": 82, "spearthrower": 19, "ghost": 8, "ghost6": 21, "spectre": 19, "gpiratkaup": 9, "gpiratka": 8, "spectre6": 27, "spectraldragon": 160, "rakshasa_rani": 120, "briskrider": 50, "cursedbehemoth": 250, "predator": 35, "blackptic": 70, "cursedent": 215, "fatespinner": 280, "ptero": 20, "thunderbird": 65, "darkbird": 60, "vulture": 40, "duneraider": 12, "duneraiderup": 12, "rakshasa_raja": 140, "juggernaut": 90, "ecyclop6": 360, "shell": 6, "tombraiderup": 12, "tombraider": 10, "reanimator": 27, "barb": 45, "gladiator": 25, "horneddemon": 13, "rapukk": 99, "rocbird": 55, "brigand": 5, "gladiatorup": 27, "piratess": 33, "cavalier": 90, "deadknight": 100, "blackknight": 90, "tormentor": 80, "satyr": 36, "pristineunicorn": 80, "dbehemoth": 280, "untamedcyc": 225, "sacredkirin": 265, "scarabup": 6, "whitebearrider": 30, "adeptus": 46, "seraph": 325, "spegasus": 30, "sekhmet": 50, "kachok": 50, "gnumka": 70, "siren": 20, "upsiren": 24, "fury6": 33, "radiantglory": 65, "scarab": 6, "skeleton": 4, "skmarksman": 6, "sceletonwar": 5, "skeletonpirateup": 4, "skeletonarcher": 4, "cpirate": 4, "skeletonpirate": 4, "nomadbow": 31, "manticoreup": 80, "scorp": 4, "elephant": 200, "anubis": 160, "krokodil": 70, "smert": 70, "flake": 10, "chuvak": 27, "snowwolf": 50, "snowmaiden": 65, "icedemon": 50, "icequeen": 29, "snowmonster": 350, "snowowl": 76, "dreamwalker6": 85, "robberup": 6, "boletus": 17, "steelgolem": 24, "mechgolem": 180, "runepatriarch": 70, "mastergremlin": 6, "jdemon": 13, "ddhigh": 34, "mauler": 12, "warrior": 12, "swolf": 25, "goblinus": 3, "cyclopus": 220, "elgargoly": 16, "sentinel": 23, "krokodilup": 80, "gatekeeper": 120, "dragonfly": 20, "crossman": 8, "mercarcher": 8, "succubus": 20, "shadow_witch": 80, "shadowdragon": 200, "necrodogup": 9, "blackarcher": 13, "sphinx": 300, "wdancer": 14, "dancer": 12, "wardancer": 12, "thane": 100, "darkon": 70, "shad": 9, "grinchshad": 1000, "titan": 190, "stormtitan": 190, "necromancer": 33, "fatpirate": 100, "troglodyte": 5, "troll": 150, "drakonid": 160, "tikovka": 310, "pumkinhead": 111, "tengu": 45, "deadfootman": 30, "foulwyvern": 105, "grimrider": 50, "foulhydra": 125, "burbuly": 30, "blackmage": 20, "slayer": 34, "verblud": 35, "udav": 816, "wight": 95, "ghoul": 25, "drowned": 16, "pushkar": 64, "fahila": 100, "pixel": 5, "phoenix": 777, "shootpirateup": 18, "freddy": 44, "fury": 16, "djinna": 48, "plant": 60, "hobbit": 4, "hobgoblin": 4, "strashidlo": 7, "blackbearrider": 30, "mistress": 100, "vedmaup": 46, "cerberus": 15, "cyclop": 85, "cyclopod": 100, "cyclopking": 95, "shamancyclop": 105, "mercwizard": 36, "outlawup": 8, "champion": 100, "blackwidow": 14, "scorpup": 5, "blacktroll": 180, "familiar": 6, "plaguezombie": 17, "blackdragon": 240, "deadarcher": 16, "blackfootman": 25, "shakal": 24, "shakalup": 30, "shamaness": 30, "sheriff": 38, "banditkaup": 9, "wfassault": 100, "metal": 55, "battlegriffon": 52, "slonup": 110, "krevetko": 66, "acrossbowman": 24, "elf": 10, "elfhealer": 7, "treant": 175, "spiderpois": 11, "gekkon": 11, "tenguup": 60, "flamelord": 120, "gnollup": 9
}
function handleKeyDown(event) {
    pressedKeys.add(event.keyCode);
}

function handleKeyUp(event) {
    pressedKeys.delete(event.keyCode);
}

document.addEventListener("keydown", handleKeyDown);
document.addEventListener("keyup", handleKeyUp);

// Странные способы в некоторых местах обусловлены конфликтом со скриптом battleHelper от omne
let outer_chat = document.getElementById("chat_format");
const physCalcColor = "#141736";
let lastTurnDefended;
const magCalcColor = "#150f1c";
unsafeWindow.lastTurnDefended = lastTurnDefended;
const calcHellFireColor = "rgba(255,0,0,0.1)";
const inputHTML = `<input type="text" id="uprava_filter_input" placeholder="${t('upravaPlaceholder')}">`;
const infoImgHTML = `<img style='height: 1.3em; width: auto; vertical-align: middle; margin-left: 1em' src="https://dcdn2.heroeswm.ru/i/combat/btn_info.png?v=6">`;
document.body.insertAdjacentHTML("afterbegin", `
    <style>
        .open-keybinds{
            margin: 0.5em;
            cursor: pointer;
            text-decoration: underline;
        }
        .custom-popup {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background-color: rgba(0, 0, 0, 0.8);
            color: white;
            padding: 15px 20px;
            border-radius: 10px;
            font-family: Arial, sans-serif;
            font-size: 24px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            display: none;
            z-index: 1000;
        }
    .follow-cre-filtered-button{
        cursor: pointer;
        margin: 0 2em;
        opacity: 0.4;
    }
    #individual_cre_heading span, .physcalc {
        background-color: ${physCalcColor};
    }
#topInput {
  position: fixed;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 9999;
  width: 11em;
  padding: 0.5em 1em;

  border: 1px solid rgba(0,0,0,0.6);
  border-radius: 12px;
  outline: none;

  background: rgba(255, 255, 255, 0.12);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);

  box-shadow: 0 10px 18px -8px green;
}

#topInput::placeholder {
  color: rgba(0, 0, 0, 0.7);
}

    </style>
    <div class="custom-popup" id="customPopup">
        <span class="popup-icon">⚠️</span>
        <i id="popupMessage"></i>
    </div>`);
outer_chat.insertAdjacentHTML("beforeend", `
<div id="cre_distance_div" style="display: none"></div>
<div id="individual_calc"></div>
<div id="mag_calc"></div>
<div id="dmg_list_container"></div>
<button id="dmg_list_refresh" style="background-color: #3d3d29; opacity: 0.7; color: white; padding: 5px 10px; border: none; border-radius: 4px;  cursor: pointer">${t('btnListCollapsed')}</button>
<select style="display : none; background-color: #333; color: white; margin: 10px" id="choose_cre"></select>
<button id="change_side" style="background-color: #6b6b47; color: white; padding: 5px 10px; border: none; border-radius: 4px;  cursor: pointer; display: none">${t('btnChangeSide')}</button>
<button id="collapse" style="background-color: #000000; color: white; padding: 5px 10px; border: none; border-radius: 4px;  cursor: pointer; display: none; margin:10px">❌</button> `)

let last_individual_calc = {}
let isOpen = false
let ini_weight = 10;
let chosen = { side: 1, creature: t("defaultCreature"), afterSideSwitchCre: { "-1": "", "1": "" } }
let chat = document.getElementById("chat_inside");
let select = document.getElementById("choose_cre")
let refresh_button = document.getElementById("dmg_list_refresh")
let side_button = document.getElementById("change_side")
let collapse_button = document.getElementById("collapse")
let individual_calc = document.querySelector("#individual_calc")
let cre_distance_div = document.querySelector("#cre_distance_div")
const settings_panel = document.querySelector("#webgl_settings_whole")
let dmg_list_container = document.querySelector("#dmg_list_container")
cre_distance_div.innerHTML = `<span>${t('chosenDistance')}: ${GM_getValue('cre_distance')}</span><br>`;

// ========= utils ============
function showPopup(message) {
    const popup = document.getElementById('customPopup');
    popup.querySelector("i").textContent = message;
    popup.style.display = 'block';
    setTimeout(() => {
        popup.style.display = 'none';
    }, 2000);
}
unsafeWindow.showPopup = showPopup;
function patchFunction(parent, originalFuncName, searchText, replacementText) {
    const originalFunc = parent[originalFuncName];
    let funcString = originalFunc.toString();
    let patchedString = funcString.replace(searchText, replacementText);
    const args = patchedString.slice(patchedString.indexOf('(') + 1, patchedString.indexOf(')'));
    const body = patchedString.slice(patchedString.indexOf('{') + 1, patchedString.lastIndexOf('}'));
    parent[originalFuncName] = new Function(args, body);
}
function get_GM_value_if_exists(GM_key, default_value) {
    const GM_value = GM_getValue(GM_key)
    return GM_value != undefined ? GM_value : default_value;
}

function triggerMouseUpEvent(element) {
    let clickEvent = document.createEvent('MouseEvents');
    clickEvent.initEvent("mouseup", true, true);
    element.dispatchEvent(clickEvent);
}

function getCurrentBattleSpeed() {
    for (let i = 1; i <= 3; i++) {
        let div = document.querySelector(`#speed${i}_button`)
        if (div.style.display === 'none') continue
        if (i === 1) return 2
        else if (i === 3) return 1
        else return 4
    }
}

function setBattleSpeed(value) {
    if (value === 0) return value
    else if (value < 1) {
        timer_interval = Math.abs(value) * 20;
        return value
    }
    animspeed_def = animspeed = value
    animspeed_init = animspeed > 4 ? 0.5 : 2;
    timer_interval = Math.abs(value - 20);
    !timer_interval && timer_interval++;
    return value
}

function countOccurrences(arr, element) {
    return arr.reduce((acc, curr) => (curr === element ? acc + 1 : acc), 0);
}

function insertInput() {
    const parent = document.querySelector("#bcontrol_users");
    if (parent.querySelector("#uprava_filter_input")) return;
    parent.insertAdjacentHTML("afterbegin", inputHTML);
}


function upravaEvent(event) {
    const parent = document.querySelector("#bcontrol_users");
    for (const child of parent.children) {
        if (child.tagName === "INPUT") continue;
        if (event.target.value === "") {
            child.classList.remove("hidden");
            continue;
        }
        const relevant = child.querySelector("span").textContent.toLowerCase().includes(event.target.value.toLowerCase());
        if (relevant) {
            child.classList.remove("hidden");
        } else {
            child.classList.add("hidden");
        }
    }
}
const damageMultipliers = {
    "doublestrike": 1.8, // двойной удар
    "cleave": 1.75, // колун
    "triplestrike": 2.5, // тройной удар
    "doubleshoot": 2, // двойной выстрел
    "assault": 1.3 // штурм
}

function evalStrength(attacker, defender) {
    const cre_collection = stage.pole.obj;
    let dmg = get_dmg_info(attacker.obj_index, defender.obj_index)
    let practical_overall_hp;
    if (cre_collection[defender.obj_index].attack > attacker.defence) {
        practical_overall_hp = attacker.maxhealth * attacker.nownumber / (1 + 0.05 * Math.abs(cre_collection[defender.obj_index].attack - attacker.defence))
    } else {
        practical_overall_hp = attacker.maxhealth * attacker.nownumber * (1 + 0.05 * Math.abs(cre_collection[defender.obj_index].attack - attacker.defence))
    }
    let multiplier = 1;
    for (const abil in damageMultipliers) {
        if (attacker.data_string.includes(abil)) multiplier *= damageMultipliers[abil];
    }
    let avgDmg = ((dmg.max + dmg.min) / 2) * multiplier * (attacker.maxinit * attacker.initmodifier / 10);

    let koef = avgDmg / ((attacker.nownumber - 1) * attacker.maxhealth + attacker.nowhealth);
    if (attacker.hero || defender.hero) koef = 0;
    return { avgDmg: avgDmg, koef: koef, practical_overall_hp: practical_overall_hp };
}
// инфа о гейтах на карте
function initGates() {
    const match = document.body.innerHTML.match(/umka\|(.+?)\|/);
    if (!match) return;
    const umka = match[1];
    const factions = [];
    for (let i = 1; i < umka.length; i += 14) {
        factions.push(umka[i]);
    }
    const demonsNo = countOccurrences(factions, "7");
    if (demonsNo < 1) return;
    document.head.insertAdjacentHTML("beforeEnd", `
    <style>
        #floatingBox {
            position: absolute;
            background-color: rgba(255, 255, 255, 0.5);
            color: white;
            padding: 10px;
            border-radius: 8px;
            pointer-events: none;
            white-space: normal;
            word-wrap: break-word;
            display: none;
        }
        .line {
            font-family: Arial, sans-serif;
            font-size: 16px;
            margin: 5px 0;
        }
    </style>
    `);
    document.body.insertAdjacentHTML("beforeEnd", `
    <div id="floatingBox">
        <div class="line" id = "gate_name">Гейт [#] </div>
        ${demonsNo > 1 ? `<div class="line" id = "gate_owner"> ${t("gateOwner")}</div>` : ''}
    </div>
    `);
    const floatingBox = document.getElementById('floatingBox');

    function showGates(event = null) {
        const [gate_x, gate_y] = [xr_last, yr_last];
        if (gate_x > defxn || gate_y > defyn || gate_x < 0 || gate_y < 0 || (event && event.target.tagName !== "CANVAS")) {
            floatingBox.style.display = "none";
            return;
        }
        let foundGate = false;
        let curGate;
        for (const cre of Object.values(stage.pole.obj)) {
            if (cre.nownumber !== -1) continue;
            if (cre.x !== gate_x || cre.y !== gate_y) continue;
            foundGate = true;
            curGate = cre;
            break;
        }
        if (!foundGate) {
            floatingBox.style.display = "none";
            return;
        };
        floatingBox.style.display = "block";
        const mouseX = event.pageX || event.touches[0].pageX;
        const mouseY = event.pageY || event.touches[0].pageY;
        floatingBox.style.left = mouseX + 10 + 'px';
        floatingBox.style.top = mouseY + 10 + 'px';
        document.querySelector("#gate_name").innerHTML = `${curGate.nametxt} [${curGate.maxnumber}]`;
        document.querySelector("#floatingBox").style.color = curGate.get_color();

        if (demonsNo < 2) return;
        const owner = stage.pole.obj[heroes[curGate.owner]]
        document.querySelector("#gate_owner").innerHTML = `${owner.nametxt} [${owner.maxhealth}]`
    }
    document.addEventListener("mousemove", showGates);
    document.addEventListener("touchstart", event => {
        showGates(event);
    });
}

function calcKilled(dmg, defender) {
    let killed;
    if (dmg % defender.maxhealth > defender.nowhealth) killed = Math.floor(dmg / defender.maxhealth) + 1
    else killed = Math.floor(dmg / defender.maxhealth);
    return killed
}

function calcHellFireHTML(attacker, defender, cre_collection, physDamage) {
    if (!isperk(attacker.obj_index, 7) || attacker.hero === 1) {
        return "";
    }
    const dmg = calcHellFire(attacker, defender, cre_collection);
    const minDamage = dmg + physDamage.min;
    const maxDamage = dmg + physDamage.max;
    const minKilled = calcKilled(minDamage, defender);
    const maxKilled = calcKilled(dmg + physDamage.max, defender);
    return `<p id="${0}" style=" background-color: ${calcHellFireColor}">
    <span style="color:white; font-size: 90%">${t('hellfire')}: </span> <span style = "color:red">${dmg}</span> <span style = "font-size: 80%">${t('damageWord')}</span><br>
    <b style="color:#ffffff; font-size: 120%; text-decoration: underline;">${icons.dead.html} ${minKilled}-${maxKilled}</b><span style="color:#ffffff">${icons.damage.html}${minDamage}-${maxDamage}
  </p><br>`;
}

function calcStormHTML(attacker, defender) {
    let koef;
    if (attacker.stormstrike) koef = 0.5;
    else if (attacker.flamestrike) koef = 1.2;
    else return "";
    let xr = defender.x;
    let yr = defender.y;
    let i = attacker.obj_index;
    let dmgMap;
    Totalmagicdamage = 0;
    Totalmagickills = 0;
    var ok = false;
    var xx = 0,
        yy = 0,
        xp = 0,
        yp = 0;
    mul = 1;
    var len = stage.pole.obj_array.length;
    for (var k1 = 0; k1 < len; k1++) {
        var j = stage.pole.obj_array[k1];
        stage.pole.obj[j]['attacked'] = 1;
        stage.pole.obj[j]['attacked2'] = 1;
    }
    var herd = 0;
    var hera = 0;
    for (var k1 = 0; k1 < len; k1++) {
        k = stage.pole.obj_array[k1];
        if ((stage.pole.obj[k].hero) && (stage.pole.obj[k].owner == stage.pole.obj[mapobj[xr + yr * defxn]].owner)) herd = k;
        if ((stage.pole.obj[k].hero) && (stage.pole.obj[k].owner == stage.pole.obj[i].owner)) hera = k;
    }
    let b = 0;
    if ((magic[hera]) && (magic[hera]['mle'])) {
        b = magic[hera]['mle']['effect'];
        magic[hera]['mle']['effect'] = 0;
    }
    if (magic[herd]) {
        let rangedDef;
        if (magic[herd]['msk']) rangedDef = magic[herd]['msk']['effect'];
        else rangedDef = 0;
        if (magic[herd]['mld']) {
            let b = magic[herd]['mld']['effect'];
            magic[herd]['mld']['effect'] = rangedDef;
            dmgMap = get_dmg_info(attacker.obj_index, defender.obj_index, koef);
            magic[herd]['mld']['effect'] = b;
        } else {
            magic[herd]['mld'] = [];
            magic[herd]['mld']['effect'] = rangedDef;
            dmgMap = get_dmg_info(attacker.obj_index, defender.obj_index, koef);
            delete magic[herd]['mld'];
        }
    } else {
        dmgMap = get_dmg_info(attacker.obj_index, defender.obj_index, koef);
    }
    if (b != 0) {
        magic[hera]['mle']['effect'] = b;
    }
    return `<p id="${0}" style=" background-color: ${calcHellFireColor}">
    <span style="color:white; font-size: 90%">${t('abilityDamage')}: </span><br>
    ${icons.dead.html} <b style="color:#ffffff; font-size: 120%; text-decoration: underline;"> ${dmgMap.min_killed}-${dmgMap.max_killed}</b><span style="color:#ffffff">${icons.damage.html}${dmgMap.min}-${dmgMap.max}
  </p><br>`;
}

function calcMagicHTML(attacker, defender, cre_collection, dmg, inList = false) {
    // if (!mag_damage_on) return "";
    let calcHTML = "";
    const disclaimerHTML = `<span class="tooltip"> ⚠️ <span class="tooltiptext chat_tooltip" style = " transform: translateX(-30%);"> ${t("kboDisclaimer")} </span>
    </span>`;

    const incorrectSpellIDs = ["circle_of_winter", "swarm", "stormcaller"];
    // если темная сила, то дописать урон с усилком
    const isPowered = attacker.hero && isperk(attacker.obj_index, 6) ? true : false;
    for (let spellID of Object.keys(damageSpells)) {
        let additionalInfoHTML = "";
        if (attacker[spellID]) {
            if (spellID === "calllightning") spellID = "lighting";
            const dmg = stage.pole.calcmagic_script(attacker.x, attacker.y, defender.x, defender.y, spellID);
            if (spellID === "meteor") {
                let meteorText = t("targets");
                for (let i = 1; i <= 4; i++) {
                    let dmg2 = Math.floor(dmg * Math.pow(0.85, i));
                    let killed2 = calcKilled(dmg2, defender);
                    const poweredDamage = Math.round(dmg2 * 1.5);
                    const poweredKilled = calcKilled(poweredDamage, defender);
                    const poweredDamageText = isPowered ? `<span style = "font-style:italic; font-size:80%"><br>\t[1.5x] ${icons.dead.html}${poweredKilled}  ${icons.damage.html}${poweredDamage}<br></span>` : "";
                    meteorText += `[${i+1}]: ${icons.dead.html}${killed2} ${icons.damage.html}${dmg2} ${poweredDamageText}<br>`;
                }
                additionalInfoHTML = `<span class="tooltip"> ${infoImgHTML} <span class="tooltiptext chat_tooltip" style = " transform: ${isPowered? "translateY(30%);" : "translateX(-60%);"}">${meteorText}</span>
                </span>`;
            }
            if (spellID === "chainlighting") {
                let chainText = t("targetNo");
                const penaltyArr = Array(0.5, 0.25, 0.125);
                for (let i = 0; i < 3; i++) {
                    let dmg2 = stage.pole.calcmagic_script(attacker.x, attacker.y, defender.x, defender.y, spellID, penaltyArr[i]);
                    let killed2 = calcKilled(dmg2, defender);
                    const poweredDamage = Math.round(dmg2 * 1.5);
                    const poweredKilled = calcKilled(poweredDamage, defender);
                    const poweredDamageText = isPowered ? `<span style = "font-style:italic; font-size:80%"><br>\t[1.5x] ${icons.dead.html} ${poweredKilled} ${icons.damage.html}${poweredDamage}<br></span>` : "";
                    chainText += `${i+2} : ${icons.dead.html}${killed2}  ${icons.damage.html}${dmg2} ${poweredDamageText}<br>`;
                }
                additionalInfoHTML = `<span class="tooltip"> ${infoImgHTML} <span class="tooltiptext chat_tooltip" style = " transform: ${isPowered? "translateY(30%);" : "translateX(-60%);"};">${chainText}</span>
                </span>`;
            }
            if (spellID === "poison") {
                additionalInfoHTML = `<span class="tooltip"> ⚠️ <span class="tooltiptext chat_tooltip" style = " transform: ${isPowered? "translateY(30%);" : "translateX(-60%);"};">${t("poisonInaccuracy")}</span>
                </span>`;
            }
            let killed = calcKilled(dmg, defender);
            const poweredDamage = Math.round(dmg * 1.5);
            const poweredKilled = calcKilled(poweredDamage, defender);
            const poweredDamageText = isPowered ? `<span style = "font-style:italic; font-size:80%"><br>\t[1.5x] ${icons.dead.html}${poweredKilled}  ${icons.damage.html}${poweredDamage}<br></span>` : "";
            calcHTML += `<p id="${0}" style=" background-color: ${magCalcColor}">
            <span style="color:white; font-size: 90%">${damageSpells[spellID]}: </span><br>${icons.dead.html}
            <b style="color:#ffffff; font-size: 120%; text-decoration: underline;">${killed}</b>  <span style="color:#ffffff">${icons.damage.html}${dmg} ${poweredDamageText}</span> ${incorrectSpellIDs.includes(spellID) ? disclaimerHTML : additionalInfoHTML}
          </p>`;
        }
    }
    calcHTML += "<br>";
    return calcHTML;
}
function creHTML(cre) {
    if (!cre) return;
    return `<font class="log_cre_name ${creClassName(cre.get_color())}" id="cre${cre.obj_index}" color="${cre.get_color()}"><span class="cre_info">🔍 </span>${cre.nametxt}</font><span class="cre_quantity">${cre.hero ? `[${cre.maxhealth}]` : `[${cre.nownumber}]`}</span>`;
}
function calcPhysHTML(attacker, defender, dmg, distance_str) {
    return ` <div  id="individual_cre_heading">
    ${distance_str}
  ${t("damage")} <br>
    <span>${creHTML(attacker)}</span> ${LOCALE === "en" ? "to" : "по"} <br> <span>${creHTML(defender)}</span>: <br>
    <br>
</div>
<p class = "physcalc">
  ${icons.dead.html}<b style="color:#ffffff; font-size: 120%; text-decoration: underline;">${dmg.min_killed}-${dmg.max_killed}</b> <span style="color:#ffffff">${icons.damage.html}${dmg.min}-${dmg.max}</span>
</p>
<br>`;
}

function individual_calc_innerHTML(atk_obj_index, def_obj_index) {
    if (atk_obj_index === undefined || def_obj_index === undefined) return "";
    let cre_collection = stage.pole.obj;
    let attacker = cre_collection[atk_obj_index];
    let defender = cre_collection[def_obj_index];
    let dmg = get_dmg_info(atk_obj_index, def_obj_index);
    let distance_str = dmg.distance === "" ? "" : `<p>${t("distanceJumpCells", dmg.distance)}</p>`;
    last_individual_calc.atk_obj_index = atk_obj_index;
    last_individual_calc.def_obj_index = def_obj_index;
    let calcHTML = calcPhysHTML(attacker, defender, dmg, distance_str) + calcHellFireHTML(attacker, defender, cre_collection, dmg) + calcStormHTML(attacker, defender) + calcMagicHTML(attacker, defender, cre_collection, dmg);

    return calcHTML;
}

function paint_coords(x, y, color, timeout = 2077) {
    let tile = shado[x + y * defxn]
    if (tile == undefined) return
    tile.fill(color)
    set_visible(tile, 1)

    setTimeout(() => {
        tile.fill(null)
        set_visible(tile, 0)
    }, timeout)
}


function GM_toggle_boolean(GM_key, boolean) {
    boolean = !boolean
    GM_setValue(GM_key, boolean);
    return boolean
}
let send_get = function(url) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, false);
    xhr.overrideMimeType('text/plain; charset=windows-1251');
    xhr.send(null);
    if (xhr.status == 200) return xhr.responseText;
    return null;
};
function set_Display(element_arr, displayProperty) {
    element_arr.forEach(element => {
        if (element == null) return
        element.style.display = displayProperty
    })
}

function readjust_elements() {
    chat = document.getElementById("chat_inside");
    select = document.getElementById("choose_cre")
    refresh_button = document.getElementById("dmg_list_refresh")
    side_button = document.getElementById("change_side")
    collapse_button = document.getElementById("collapse")
    dmg_list_container = document.querySelector("#dmg_list_container")
    individual_calc = document.querySelector("#individual_calc")
    cre_distance_div = document.querySelector("#cre_distance_div")
}
// -----------------------------------
// =========   Настройки ============

let cre_distance = get_GM_value_if_exists('cre_distance', "")
let cre_distance_on = get_GM_value_if_exists('cre_distance_on', false)
let coeff_on = get_GM_value_if_exists('coeff_on', true)
let animation_speed_on = get_GM_value_if_exists("animation_speed_on", false);
let mag_damage_on = get_GM_value_if_exists("mag_damage_on", true);

const new_settings = `
<style>
  .tooltip {
    position: relative;
    display: inline-block;
    text-size: 120%;
    color: brown;
  }

.tooltip .tooltiptext {
    visibility: hidden;
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    padding: 5px;
    background-color: #555;
    color: #fff;
    border-radius: 6px;
    word-wrap: break-word;      /* wrap long words */
    white-space: normal;        /* allow multiple lines */
    z-index: 1000;              /* above panel content */
}
    .chat_tooltip{
        min-width: 500%;
    max-width: 700%;
    }
.settings_tooltip{
        min-width: 1500%;
    max-width: 3000%;
}
.tooltip:hover .tooltiptext,
.tooltip:active .tooltiptext {
    visibility: visible;
}

</style>
<div class="info_row">
  <label class="checkbox_container">${t("sDistanceLabel")} <span class="tooltip">🔍<span class="tooltiptext settings_tooltip" "style = "transform: translateX(-30%);">${t("sDistanceTip")}</span>
    </span>
    <input type="checkbox" checked="true" id="cre_distance_on">
    <span class="checkbox_checkmark"></span>
  </label>
  <ass style="display: inline-flex; align-items: center; gap: 2px;">
  <button type="button" id="cre_distance_minus" style="padding: 0 4px;">−</button>
  <input type="number" id="cre_distance" style="width: 40px; text-align: center;" value="${cre_distance}">
  <button type="button" id="cre_distance_plus" style="padding: 0 4px;">+</button>
</ass>
</div>
<div class="info_row">
    <label class="checkbox_container">${t("sAnimSpeedLabel")} <span class="tooltip">🔍<span class="tooltiptext settings_tooltip" style = " transform: translateX(-30%);"> ${t("sAnimSpeedTip")}</span>
      </span>
      <input type="checkbox" checked="true" id="animation_speed_on">
      <span class="checkbox_checkmark"></span>
    </label>
  <input type="range" id="anim_speed" min="1" max="20" step="1" value="4" style="width:150px;">
  <span id="anim_speed_val">4</span>
</div>
<div class="info_row">
  <span class="open-keybinds">🎮 ${t("openKeyBinds")}</span>
</div>
`
settings_panel.insertAdjacentHTML("beforeend", new_settings);


// logging the log gin login
const fiveSecWarning = false;
const includeTribalSpirit = false;
const includeEnrage = false;
const includeBloodLust = false;
const includeStatusIcons = false;
const highlightTileColor = "red";
/* -------------------------
   Locale + I18N
--------------------------*/

// Auto-detect (your original comment restored + hardened)
const locale =
      location.href.includes("www.lordswm.com")
? "en"
: "ru";
const I18N2 = {
    ru: {
        ui: {
            searchPlaceholder: "Поиск...",
            damageLabel: "повреждений",
            killedLabel: "Погибло",
            filterLabel: "Отфильтровать лог по",
            filterLabelCre: "(I) Отфильтровать лог по этому существу",
            alt: {
                castSpell: "наложил заклятие",
                wait: "ожидание",
                defend: "оборона",
                luck: "посетила удача",
                unluck: "посетила неудача",
                morale: "рвутся в бой",
                crit: "критический удар",
            },
        },
        kw: {
            // hit parsing
            hitVerb: "нанес",
            damageWordInLog: "повреждений",
            killedWordInLog: "Погибло",
            massCast : "массово ",
            // spell parsing
            castVerb: "наложил",
            spellWord: "заклятие",
            destroyedDefense: "разрушил защиту",
        },
        rx: {
            // status lines
            wait: "ожида",
            defend: "оборона",
            morale: "рвутся в бой",
            luck: "посетила удача",
            unluck: "посетила неудача",
            dismorale: "ожидают в страхе",
            crit: "критический удар по заклятому",

            // hit
            damageNum: /нанес.*?(\d+)/i,

            // spell
            spellName: /наложил.*? заклятие (.*?) на/i,
            spellDuration: /на <b>(\d+)<\/b> ход/i,

            // misc (miss)
            activeMisc: [" не попали в "],

            // cleanup exclusions
            bloodRage: (ln) =>
            (!includeTribalSpirit && includesOutsideFont(ln, "Гнева крови"))  ||
            (!includeTribalSpirit && includesOutsideFont(ln, "Ярост") && includesOutsideFont(ln, "крови")) ||
            (!includeEnrage && includesOutsideFont(ln, "впали в ярость")) ||
            (!includeBloodLust && includesOutsideFont(ln, "жаждут ещё")),
        },
        isHitLine: (ln) => includesOutsideFont(ln, "нанес") && includesOutsideFont(ln, "повреждений"),
        isSpellLine: (ln) => includesOutsideFont(ln, "наложил") || includesOutsideFont(ln, "разрушил защиту") || includesOutsideFont(ln, "разрушили защиту"),
    },

    en: {
        ui: {
            searchPlaceholder : "Search...", 
            damageLabel: "damage",
            killedLabel: "perish",
            filterLabel: "Filter log by",
            filterLabelCre: "(I) Filter log by this creature",
            alt: {
                castSpell: "cast",
                wait: "wait",
                defend: "defend",
                luck: "luck",
                unluck: "bad luck",
                morale: "bursting for more action",
                crit: "critical damage to favoured enemy",
            },
        },
        kw: {
            // hit parsing (best-effort for LordsWM EN log)
            hitVerb: "deal",
            damageWordInLog: "damage",
            killedWordInLog: "perish",
            massCast: "mass ",
            // spell parsing (best-effort)
            castVerb: "cast",
            spellWord: "spell",
            destroyedDefense: "destroyed defense",
        },
        rx: {
            // status lines (best-effort; adjust to your exact EN phrasing if needed)
            wait: "wait",
            defend: "defend",
            morale: "bursting for more action",
            luck: "Luck befalls",
            unluck: "Bad luck befalls",
            dismorale: "freeze in fear",
            crit: "critical damage to favoured enemy",

            // hit
            // examples: "dealt 123 damage", "dealt 123 damage to ..."
            damageNum: /deal.*?(\d+)/,

            // spell
            // example: "X cast spell <name> on Y"
            spellName: /\bcast(?:s)?\b\s+(.+?)\s+\bon\b/i,
            // example: "for <b>N</b> turns"
            spellDuration: /\bfor\s+<b>(\d+)<\/b>\s+turn/i,

            // misc (miss)
            activeMisc: [" missed ", " did not hit ", " didn't hit "],

            bloodRage: (ln) =>
            (!includeTribalSpirit && includesOutsideFont(ln, "Tribal spirit"))  ||
            (!includeEnrage && includesOutsideFont(ln, "get enraged")) ||
            (!includeEnrage && includesOutsideFont(ln, "feel bloodlusty")),
        },
        isHitLine: (ln) => includesOutsideFont(ln, "deal") && includesOutsideFont(ln, "damage"),
        isSpellLine: (ln) =>
        includesOutsideFont(ln, " cast") || includesOutsideFont(ln, "destroyed defense"),
    },
};

const L = I18N2[locale] || I18N2.ru;

/* -------------------------
   State + Icons
--------------------------*/

let lastObject, color;
unsafeWindow.lastObject = lastObject;

let damage_notIcon = localStorage.getItem("damage_notIcon") === "true";
let dead_notIcon = localStorage.getItem("dead_notIcon") === "true";

const icons = {
    damage: {
        html: `<span class="damage-icon ${damage_notIcon ? "not-icon" : ""}"><span> ${L.ui.damageLabel} </span><img></span>`,
        isIcon: damage_notIcon,
    },
    dead: {
        html: `<span class="dead-icon ${dead_notIcon ? "not-icon" : ""}"><span> ${L.ui.killedLabel} </span><img></span>`,
        isIcon: dead_notIcon,
    },
};


const hitIconHTML = `<img class="icon-m downshifted" src="https://daily.heroeswm.ru/mt/img/1.png">`;
const waitIconHTML = `<img alt="${L.ui.alt.wait}" style="opacity:0.4" class="downshifted icon-l" src="https://dcdn.heroeswm.ru/i/combat/icons/attr_initiative.png?v=6">`;
const defendButtonHtml = `<img alt="${L.ui.alt.defend}" style="opacity:0.4" class="icon-l downshifted" src="https://dcdn.heroeswm.ru/i/combat/icons/attr_defense.png?v=6">`;
const luckButtonHTML = `<img alt="${L.ui.alt.luck}" class="icon-xl downshifted2" style="opacity:0.6" src="https://dcdn.heroeswm.ru/i/help/lm0001.png">`;
const unLuckButtonHTML = `<img alt="${L.ui.alt.unluck}" style="opacity:0.6" class="icon-bordered icon-xl downshifted2" src="https://dcdn.heroeswm.ru/i/help/lm0002.png">`;
const moraleButtonHTML = `<img alt="${L.ui.alt.morale}" class="icon-xl downshifted" style="opacity:0.5" src="https://dcdn.heroeswm.ru/i/combat/icons/attr_morale.png?v=6">`;
const critButtonHTML = `<img alt="${L.ui.alt.crit}" style="opacity:0.5" class="icon-l downshifted" src="https://daily-help.ru/img/other/favenemy/lava.png">`;
const dismoraleButtonHTML = `<img class="icon-xl downshifted" style="opacity:0.6" src="https://dcdn.heroeswm.ru/i/help/lm0004.png">`;

/* -------------------------
   CSS + shadow style
--------------------------*/

document.body.insertAdjacentHTML(
    "afterBegin",
    `<style>
    .log_cre_name { cursor:pointer; transition:text-shadow 0.2s ease, transform 0.2s ease; }
    .line-hidden { display:none; }

    .damage-icon > span, .dead-icon > span { display:none; }
    .damage-icon.not-icon > span, .dead-icon.not-icon > span { display:inline; }
    .damage-icon.not-icon > img, .dead-icon.not-icon > img { display:none; }

    .cre_info { opacity:0.4; display:none; font-size:0.8em; padding: 0 0 0 0.4em }
    .log_cre_name:hover { text-shadow:0 0.3em 0.6em rgba(0,0,0,0.8); transform: translateY(-3px); }
    .cre_quantity { display:none; }
    .log_cre_name:hover> .cre_info:not(#win_ShortLog .cre_info, .area_chat .cre_info) { display:inline; }
    .log_cre_name:hover + .cre_quantity { display:inline; }
    .follow-cre-filtered-button{
        margin: 0 2em;
        cursor:pointer;
    }
    .cre_info:hover{
      filter:
        drop-shadow(1px 0 0 rgba(255,0,0,0.8))
        drop-shadow(-1px 0 0 rgba(255,0,0,0.8))
        drop-shadow(0 1px 0 rgba(255,0,0,0.8))
        drop-shadow(0 -1px 0 rgba(255,0,0,0.8));
    }

    .downshifted { position:relative; top:0.2em }
    .downshifted2 { position:relative; top:0.4em }

    .icon-bordered{
      filter:
        drop-shadow(1px 0 0 rgba(0,0,0,0.2))
        drop-shadow(-1px 0 0 rgba(0,0,0,0.2))
        drop-shadow(0 1px 0 rgba(0,0,0,0.2))
        drop-shadow(0 -1px 0 rgba(0,0,0,0.2));
    }
    .icon-sat{
      filter: saturate(5)
        drop-shadow(1px 0 0 rgba(0,0,0,0.2))
        drop-shadow(-1px 0 0 rgba(0,0,0,0.2))
        drop-shadow(0 1px 0 rgba(0,0,0,0.2))
        drop-shadow(0 -1px 0 rgba(0,0,0,0.2));
    }
    // .area_chat .cre-light{
    //     background: linear-gradient(
    //     to bottom,
    //     #434343ff 0%,
    //     #151515ff 35%,
    //     #000000ff 50%,
    //     #1c1c1cff 65%,
    //     #404040ff 100%
    //     );
    // }
    // .area_chat .cre-dark {
    //     background: linear-gradient(
    //     to bottom,
    //     #2b2b2bff 0%,
    //     #9a9999ff 45%,
    //     #bfbfbfff 55%,
    //     #a8a6a6ff 65%,
    //     #1a1a1aff 100%
    //     );
    // }
    .area_chat .log_cre_name {
        color:white;
        // font-size: 1.2em;
        // letter-spacing: 0.03em;
        // padding : 0.2em;
        // border-radius: 0.5em;
    }
    .area_chat .dead-icon > img{
    filter:
        drop-shadow(1px 0 0 rgba(255,255,255,1))
        drop-shadow(-1px 0 0 rgba(255,255,255,1))
        drop-shadow(0 1px 0 rgba(255,255,255,1))
        drop-shadow(0 -1px 0 rgba(255,255,255,1));
    }
    .area_chat .dead-icon, .damage-icon{
        margin : 0 0.2em;
    }
    .area_chat .cre_quantity{display:inline}
    .area_chat .not-icon > span {
        color:white;
    }
    .icon-s { height:0.8em; width:auto; font-size:0.8em; }
    .icon-m { height:1em; width:auto; }
    .icon-l { height:1.2em; width:auto; }
    .icon-xl{ height:1.4em; width:auto; }
    .log-line {
    background-color: rgba(255, 255, 255, 0.3);
    }

    .log-line.line-dark {
        background-color: rgba(128, 128, 128, 0.1);
    }

    .log-line.line-hidden {
        display: none;
    }
#win_FullLog.fullLog_container {
  background-size: 100% auto, 100% auto, 100% auto !important;
  background-position: top center, bottom center, top center !important;
}

#win_FullLog_area {
  width: 93% !important;
  margin: 0 auto !important;
  box-sizing: border-box !important;
}

#win_FullLog_txt {
  width: 100% !important;
  margin: 0 !important;
  max-width: none !important;
  box-sizing: border-box !important;
  overflow-x: hidden !important;
}

/* helps prevent tiny overflow that can trigger parent scrollbars */
#win_FullLog_txt > ul {
  margin: 0 !important;
}
</style>`
);

const shadowStyle = document.createElement("style");
shadowStyle.id = "shadowStyle";
document.body.append(shadowStyle);

/* -------------------------
   Sprite icons (skull/explosion)
--------------------------*/

async function getIcon(name) {
    const SPRITE_URL = "https://dcdn.heroeswm.ru/i/png40/war_images.png?v=7";

    let sprite = document.querySelector(`img[data-war-sprite="1"]`);
    if (!sprite) {
        sprite = new Image();
        sprite.crossOrigin = "anonymous";
        sprite.src = SPRITE_URL;
        sprite.dataset.warSprite = "1";
    }

    if (!sprite.complete || sprite.naturalWidth === 0) {
        if (sprite.decode) {
            await sprite.decode();
        } else {
            await new Promise((res, rej) => {
                sprite.onload = () => res();
                sprite.onerror = () => rej(new Error("Failed to load sprite image"));
            });
        }
    }
    const SKULL = { x: 388, y: 184, w: 26, h: 33 };
    const EXPLOSION = { x: 384, y: 230, w: 34, h: 33 };
    const coordData = name === "skull" ? SKULL : EXPLOSION;

    const canvas = document.createElement("canvas");
    canvas.width = coordData.w;
    canvas.height = coordData.h;

    const ctx = canvas.getContext("2d");
    ctx.drawImage(
        sprite,
        coordData.x, coordData.y, coordData.w, coordData.h,
        0, 0, coordData.w, coordData.h
    );

    const img = document.createElement("img");
    img.src = canvas.toDataURL("image/png");

    if (name === "skull") {
        img.title = L.ui.killedLabel;
        img.className = "icon-m downshifted";
        img.style.opacity = "0.8";
    } else {
        img.title = L.ui.damageLabel;
        img.className = "icon-m";
        img.style.position = "relative";
        img.style.top = "0.1em";
    }

    return img;
}

unsafeWindow.replaceOutsideFont =  function replaceOutsideFont(html, needle, replacement) {
    if (needle === "") return html; // avoid infinite loops / weirdness

    // Matches <font ...> ... </font> blocks (non-greedy across newlines)
    const fontBlockRe = /<font\b[^>]*>[\s\S]*?<\/font>/gi;

    let out = "";
    let lastIndex = 0;

    // Helper: replace all occurrences of a literal substring (not regex)
    const replaceAllLiteral = (str, find, rep) => str.split(find).join(rep);

    for (const match of html.matchAll(fontBlockRe)) {
        const start = match.index;
        const end = start + match[0].length;

        // Part before this <font> block: replace in it
        out += replaceAllLiteral(html.slice(lastIndex, start), needle, replacement);

        // The <font> block itself: keep untouched
        out += match[0];

        lastIndex = end;
    }

    // Tail after last <font> block
    out += replaceAllLiteral(html.slice(lastIndex), needle, replacement);

    return out;
}

unsafeWindow.includesOutsideFont = function includesOutsideFont(html, needle) {
    if (needle === "") return true;

    // Remove all <font>...</font> blocks
    const withoutFont = html.replace(
        /<font\b[^>]*>[\s\S]*?<\/font>/gi,
        ""
    );

    return withoutFont.includes(needle);
}
/* -------------------------
   Utilities
--------------------------*/

function isAncestor(element, parentID) {
    while (element.parentElement) {
        if (element.parentElement.id === parentID) return true;
        element = element.parentElement;
    }
    return false;
}
function hexToRgb(hex) {
    hex = hex.replace('#', '');
    if (hex.length === 8) hex = hex.slice(0, 6); // ignore alpha
    const num = parseInt(hex, 16);

    return {
        r: (num >> 16) & 255,
        g: (num >> 8) & 255,
        b: num & 255
    };
}
function relativeLuminance({ r, g, b }) {
    const toLinear = (c) => {
        c /= 255;
        return c <= 0.03928
            ? c / 12.92
        : Math.pow((c + 0.055) / 1.055, 2.4);
    };

    const R = toLinear(r);
    const G = toLinear(g);
    const B = toLinear(b);

    return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
function creClassName(hex) {
    const rgb = hexToRgb(hex);
    const lum = relativeLuminance(rgb);
    return lum > 0.13 ? 'cre-light' : 'cre-dark';
}

function safeMatch(str, regex, i) {
    const match = str.match(regex);
    if (match) return match[i];
}

/* -------------------------
   Filter UI + click handling
--------------------------*/

document.addEventListener("change", (event) => {
    if (event.target.id !== "filter-log-checkbox") return;
    const font = event.target.parentElement.querySelector("#filter-log-cre > font");
    event.target.checked ? filterLog({id: font.id}) : unFilterLog();
});

function toggleIcon(name) {
    for (const icon of document.querySelectorAll(`.log_full .${name}-icon`)) icon.classList.toggle("not-icon");
    icons[name].isIcon = !icons[name].isIcon;
    localStorage.setItem(`${name}_notIcon`, icons[name].isIcon);

    if (icons[name].isIcon) icons[name].html = icons[name].html.replace(`${name}-icon`, `${name}-icon not-icon`);
    else icons[name].html = icons[name].html.replace(`not-icon`, ``);

    for (const i in log_lines) {
        if (icons[name].isIcon) log_lines[i] = log_lines[i].replace(`${name}-icon`, `${name}-icon not-icon`);
        else log_lines[i] = log_lines[i].replace(`${name}-icon not-icon`, `${name}-icon`);
    }
    showtext();
}

document.body.addEventListener("click", (event) => {
    const el = event.target;
    if (el.id === "win_ShortLog" || isAncestor(el, "win_ShortLog") || el.className === "log_short_container_cap_bottom"){
        if (!android && !iOS) setTimeout(()=>{document.querySelector("#topInput").focus()}, 200);
        
    }
    if ( [el.id, el.parentElement?.id].includes("btn_close_fullLog")) {
        unFilterLog();
        shadowStyle.innerHTML = "";
        document.querySelector("#filter-log-div")?.remove();
    }
    if (isAncestor(el, "win_ShortLog")) return;
    if (el.className === "cre_info") {
        const wide = window.matchMedia("(min-width: 700px)").matches;
        const obj_index = parseInt(el.parentElement.id.split("cre")[1]);
        stage.pole.obj[obj_index].hero
            ? stage.pole.showheroinfo(obj_index, wide)
        : stage.pole.showinfo(obj_index, wide);
        return;
    }
    if (el.parentElement?.classList?.contains("damage-icon")) {
        toggleIcon("damage");
        return;
    }
    if (el.parentElement?.classList?.contains("dead-icon")) {
        toggleIcon("dead");
        return;
    }

    if (el.className !== "follow-cre-filtered-button" && !el.classList.contains("log_cre_name")) return;

    const hex = stage.pole.obj[parseInt(el.id.split("cre")[1])].get_color();
    const styleId = shadowStyle.innerHTML.match(/cre\d+/);
    const currentElGlowing = styleId && styleId[0] === el.id;
    shadowStyle.innerHTML = "";
    const isFiltering = document.querySelector("#filter-log-checkbox")?.checked;
    const creTriggered = el.className === "follow-cre-filtered-button";
    if (isFiltering) unFilterLog();
    document.querySelector("#filter-log-div")?.remove();
    if (!currentElGlowing || creTriggered) {
        document.querySelector("#btn_close_fullLog").insertAdjacentHTML(
            "beforebegin",
            `<div id="filter-log-div">
        <input type="checkbox" id="filter-log-checkbox" name="filter-log-checkbox"}>
        <label for="filter-log-checkbox">
          ${L.ui.filterLabel}
          <div id="filter-log-cre">${creHTML(stage.pole.obj[el.id.split("cre")[1]])}</div>
        </label>
      </div>`
        );
        if (isFiltering && !creTriggered) document.getElementById("filter-log-checkbox").click();
        shadowStyle.innerHTML = `
      #${el.id}{
        text-shadow:
          0 0 6px ${hex}99,
          0 0 14px ${hex}99,
          0 0 24px ${hex}99
      }
      #${el.id} + .cre_quantity { display:inline; }
    `;
        if (creTriggered) {
            document.getElementById("filter-log-checkbox").click();
            // hide_all_buttons();
            setTimeout(() => {
                document.querySelector("#win_ShortLog").click();
            }, 200);
        }
        if (isAncestor(el, "chat_format")) document.querySelector("#win_ShortLog").click();

    }
});

/* -------------------------
   Hover glow / strobe
--------------------------*/
function paintCoordsStatic(x, y, color, fallBack = false){
    let tile = shado[x + y * defxn];
    if (!tile) return;
    if (fallBack) {
        tile.fill(null);
        set_visible(tile, 0);
    }
    else{
        tile.fill(color);
        set_visible(tile, 1);
    }
}
let strobeId = null;
let strobeStep = 0;

function runStrobe(creature) {
    strobeStep += 0.1;
    const intensity = (Math.sin(strobeStep) + 1) * 0.75;

    if (Math.sin(strobeStep) > 0) {
        paintCoordsStatic(creature.x, creature.y, highlightTileColor);
        FiltersGlowTest.uniforms.m[0] = intensity;
        FiltersGlowTest.uniforms.m[6] = 0;
        FiltersGlowTest.uniforms.m[12] = 0;
        FiltersGlowTest.uniforms.m[4] = intensity * 0.5;
        FiltersGlowTest.uniforms.uAlpha = 1;
        showshadow(creature, true);
    } else {
        showshadow(creature, false);
    }

    strobeId = requestAnimationFrame(() => runStrobe(creature));
}

let hoverInTimer;
const delay = 1500;

function handlePointerOut(el) {
    clearTimeout(hoverInTimer);
    const creature = stage.pole.obj[parseInt(el.id.split("cre")[1])];
    paintCoordsStatic(creature.x, creature.y, null, true);
    cancelAnimationFrame(strobeId);
    strobeId = null;
    showshadow(creature, false);

    FiltersGlowTest.uniforms.m[0] = 1.15;
    FiltersGlowTest.uniforms.m[6] = 1.15;
    FiltersGlowTest.uniforms.m[12] = 1.0;
    FiltersGlowTest.uniforms.m[4] = 0;
    FiltersGlowTest.uniforms.uAlpha = 1;

    document.querySelector("#win_FullLog").style.opacity = 1;
}

function handlePointerOver(el) {
    if (!el.classList.contains("log_cre_name")) return;

    hoverInTimer = setTimeout(() => {
        const creature = stage.pole.obj[parseInt(el.id.split("cre")[1])];
        document.querySelector("#win_FullLog").style.opacity = 0.6;
        if (!strobeId) {
            strobeStep = 0;
            runStrobe(creature);
        }
    }, delay);
}

let pointerX = -1;
let pointerY = -1;
let lastElement = null;

function check({ emitOver } = { emitOver: true }) {
    if (pointerX < 0 || pointerY < 0) return;

    const el = document.elementFromPoint(pointerX, pointerY);
    if (el === lastElement) return;

    if (lastElement && lastElement.classList.contains("log_cre_name")) {
        handlePointerOut(lastElement);
    }
    if (emitOver && el && !isAncestor(el, "win_ShortLog")) handlePointerOver(el);

    lastElement = el;
}

window.addEventListener(
    "pointermove",
    (e) => {
        pointerX = e.clientX;
        pointerY = e.clientY;
        check({ emitOver: true });
    },
    true
);

/* -------------------------
   Turn background grouping + filtering
--------------------------*/

const primaryColor = "rgba(128, 128, 128, 0.1)";
const secondaryColor = "rgba(255, 255, 255, 0.3)";

function lastGroupColOpposite() {
    for (let i = log_lines.length - 1; i > 0; i--) {
        if (log_lines[i].includes("background-color")) {
            return log_lines[i].includes(primaryColor) ? secondaryColor : primaryColor;
        }
    }
}

const lineColors = {};

unsafeWindow.wrapLastTurn = function wrapLastTurn() {
    let color;
    const lastColorOP = lastGroupColOpposite();
    if (!lastColorOP) color = primaryColor;
    else color = lastColorOP;

    for (let i = log_lines.length - 1; i > 0; i--) {
        if (log_lines[i] === "") continue;
        if (log_lines[i].includes("background-color")) break;
        log_lines[i] = log_lines[i].replace("<p>", `<p style="background-color:${color}">`);
        lineColors[i] = { color: color, lineHTML: log_lines[i] };
    }
}

let colorToggle = false;

function processGroup(group, obj) {
    let isRelevant = false;
    for (const i of group) {
        let relevanceCheck;
        if (obj.id) relevanceCheck = log_lines[i].includes(`id="${obj.id}"`);
        else relevanceCheck = log_lines[i].toLowerCase().includes(obj.keyword.toLowerCase());
        if (relevanceCheck) {
            isRelevant = true;
            break;
        }
    }

    if (!isRelevant) {
        for (const i of group) {
            log_lines[i] = log_lines[i].replace("<p", `<p class="line-hidden"`);
        }
    } else {
        colorToggle = !colorToggle;
        for (const i of group) {
            let lineMatch = log_lines[i].match(/background-color:(.*?)\"/);
            if (lineMatch) log_lines[i] = log_lines[i].replace(lineMatch[1], colorToggle ? primaryColor : secondaryColor);
        }
    }
}

function filterLog(obj) {
    // const {keyword, id} = obj;
    let group = [];
    let lastColor;

    for (const [i, line] of log_lines.entries()) {
        if (!line || line === "") continue;

        let lineColor = line.match(/background-color:(.*?)\"/);
        if (lineColor) lineColor = lineColor[1];

        if (!lastColor) {
            group.push(i);
            lastColor = lineColor;
            continue;
        }

        if (lineColor && lineColor === lastColor) {
            group.push(i);
            lastColor = lineColor;
        } else {
            processGroup(group, obj);
            lastColor = lineColor;
            group = [i];
        }
    }

    processGroup(group, obj);
    showtext();
}

function unFilterLog() {
    for (const i in log_lines) {
        if (!log_lines[i] || log_lines[i] === "") continue;
        log_lines[i] = log_lines[i].replace(`<p class="line-hidden"`, "<p ");
        let lineMatch = log_lines[i].match(/background-color:(.*?)\"/);
        if (lineMatch && lineColors[i]) log_lines[i] = log_lines[i].replace(lineMatch[1], lineColors[i].color);
    }
    showtext();
}

/* -------------------------
   Log parsing + icons
--------------------------*/

const activeMisc = L.rx.activeMisc;

function isLineActiveMisc(line) {
    for (const keyword of activeMisc) {
        if (includesOutsideFont(line, keyword)) return keyword;
    }
    return false;
}

const iconRules = [
    [L.rx.wait, waitIconHTML],
    [L.rx.defend, defendButtonHtml],
    [L.rx.morale, moraleButtonHTML],
    [L.rx.luck, luckButtonHTML],
    [L.rx.unluck, unLuckButtonHTML],
    [L.rx.dismorale, dismoraleButtonHTML],
    [L.rx.crit, critButtonHTML],
];

function addStatus(i) {
    for (const [keyword, statusIcon] of iconRules) {
        if (includesOutsideFont(log_lines[i], keyword)) {
            if (keyword === L.rx.luck){
                if (includesOutsideFont(log_lines[i], L.rx.unluck)) continue;
            }
            log_lines[i] = log_lines[i].replace("<font", `${statusIcon}<font`);
        }
    }
}

let spellObj;
unsafeWindow.spellObj = spellObj;

unsafeWindow.processLine = function processLine(i) {
    const ln = log_lines[i];
    const idMatch = Array.from(ln.matchAll(/id="cre(\d+)"/g), (m) => m[1]);
    const miscAttack = isLineActiveMisc(ln);

    const obj = {
        lineHTML: ln,
        lnIndex: i,
        attackerID: idMatch[0],
        defenderID: idMatch[1],
        miscAttack,
    };

    obj.isSpell = L.isSpellLine(ln);
    obj.spellName = safeMatch(ln, L.rx.spellName, 1);
    obj.isHit = L.isHitLine(ln) || obj.isSpell || miscAttack;
    obj.damage = safeMatch(ln, L.rx.damageNum, 1);
    // special spell-like line
    //   if (ln.toLowerCase().includes(L.kw.destroyedDefense.toLowerCase())) {
    if (includesOutsideFont(ln, L.kw.destroyedDefense)) {
        obj.spellName = L.kw.destroyedDefense;
    }

    obj.spellDuration = safeMatch(ln, L.rx.spellDuration, 1);

    if (obj.isHit) {
        if (obj.damage || obj.miscAttack) {
            // Replace log words with icon blocks (locale-safe)
            log_lines[i] = replaceOutsideFont(log_lines[i], L.kw.damageWordInLog, icons.damage.html);
            log_lines[i] = replaceOutsideFont(log_lines[i], L.kw.killedWordInLog, icons.dead.html);
        }

        if (obj.isSpell) {
            // Remove generic "spell word" once + bold spell name
            if (obj.spellName) {
                log_lines[i] = replaceOutsideFont(log_lines[i], L.kw.spellWord, "");
                log_lines[i] = replaceOutsideFont(log_lines[i], obj.spellName, `<b>${obj.spellName}</b>`);
            }

            // Merge consecutive identical spell lines by appending defenders
            if (
                obj.attackerID === spellObj?.attackerID &&
                obj.spellName === spellObj?.spellName &&
                obj.spellDuration === spellObj?.spellDuration
            ) {
                const creSplitted = log_lines[spellObj.lnIndex].split("<endcre>");
                creSplitted[creSplitted.length-2] += `, ${creHTML(stage.pole.obj[obj.defenderID])}`;
                log_lines[spellObj.lnIndex] = creSplitted.join("<endcre>");
                if (!includesOutsideFont(log_lines[spellObj.lnIndex], L.kw.massCast) && !includesOutsideFont(log_lines[spellObj.lnIndex], "background-color")) log_lines[spellObj.lnIndex] = replaceOutsideFont(log_lines[spellObj.lnIndex], L.kw.castVerb, `<i>${L.kw.massCast}</i>` + L.kw.castVerb);
                log_lines[i] = "";
            } else {
                spellObj = obj;
            }
        } else {
            spellObj = null;
        }
    } else {
        // Remove “blood rage” spam lines (locale-aware)
        if (L.rx.bloodRage(log_lines[obj.lnIndex])) {
            log_lines[obj.lnIndex] = "";
        }
    }

    includeStatusIcons && addStatus(i);
}


/* -------------------------
   Log resizing + init patches
--------------------------*/

function resizeLog() {
    const isWide = window.matchMedia("(min-width: 700px)").matches;
    const win = document.getElementById("win_FullLog");
    if (!win) return;

    const f = 0.8

    const widthPx = document.documentElement.clientWidth * f;
    win.style.width = widthPx + "px";
}
window.addEventListener("resize", resizeLog);
function fixChat() {
    const input = document.querySelector('#chattext');
    const sendBtn = document.querySelector('#btn_SendChatMessage');
    const targetBtn = document.querySelector('#btn_ToggleChatTarget');
    if (!input || !sendBtn) return;

    // Inject base CSS so padding works as expected
    const style = document.createElement('style');
    style.textContent = `#chattext { box-sizing: border-box !important; }`;
    document.head.appendChild(style);

    // Capture original paddings once (so we "add" not "replace")
    const cs0 = getComputedStyle(input);
    const basePadL = parseFloat(cs0.paddingLeft) || 0;
    const basePadR = parseFloat(cs0.paddingRight) || 0;

    function overlapsHorizontally(a, b) {
        // overlap width on x-axis
        const left = Math.max(a.left, b.left);
        const right = Math.min(a.right, b.right);
        return right - left; // can be <= 0
    }

    function syncPadding() {
        const gap = 10;

        const inputRect = input.getBoundingClientRect();
        const sendRect = sendBtn.getBoundingClientRect();

        const sendOverlap = overlapsHorizontally(sendRect, inputRect);

        // If the send button truly overlaps the input, reserve its overlapped width.
        // If it's just a sibling (no overlap), do nothing on the right.
        const reserveRight = sendOverlap > 0 ? Math.ceil(sendOverlap) + gap : 0;

        input.style.paddingRight = (basePadR + reserveRight) + 'px';

        // LEFT SIDE: only reserve space if the left button overlaps the input.
        if (targetBtn) {
            const leftRect = targetBtn.getBoundingClientRect();
            const leftOverlap = overlapsHorizontally(leftRect, inputRect);
            const reserveLeft = leftOverlap > 0 ? Math.ceil(leftOverlap) + gap : 0;

            input.style.paddingLeft = (basePadL + reserveLeft) + 'px';
        } else {
            // Ensure we don't leave some old huge padding in place
            input.style.paddingLeft = basePadL + 'px';
        }
    }

    // Run now and on layout changes
    syncPadding();
    window.addEventListener('resize', syncPadding);
    window.addEventListener('orientationchange', syncPadding);

    const ro = new ResizeObserver(syncPadding);
    ro.observe(sendBtn);
    if (targetBtn) ro.observe(targetBtn);

    const chat = document.querySelector('#area_chat');
    if (chat) {
        const mo = new MutationObserver(syncPadding);
        mo.observe(chat, { attributes: true, childList: true, subtree: true });
    }
}
function init() {
    fixChat();
    resizeLog();

    const fullLogArea = document.getElementById("win_FullLog_area");
    const fullLogTxt = document.getElementById("win_FullLog_txt");
    const logContainer = document.querySelector("#win_FullLog");

    const resizeObserver = new ResizeObserver((entries) => {
        for (const entry of entries) {
            const containerWidth = parseFloat(window.getComputedStyle(logContainer).width);
            fullLogArea.style.width = containerWidth * 0.9 + "px";
            fullLogTxt.style.width = containerWidth * 0.9 + "px";
        }
    });

    resizeObserver.observe(logContainer);

    if (fullLogArea) {
        fullLogArea.addEventListener("scroll", () => check({ emitOver: false }), {
            passive: true,
            capture: true,
        });
    }

    if (fullLogTxt) {
        new MutationObserver(() => check({ emitOver: false })).observe(fullLogTxt, {
            childList: true,
            subtree: true,
        });
    }

    stage.pole.showheroinfo = function showheroinfo(i, notHideLog = false) {
        cur_unit_info = i;
        stage[war_scr].prepare_hero_info(i);
        hide_windows(notHideLog);
        show_button("win_InfoHero");
    };

    stage.pole.showinfo = function showinfo(i, notHideLog = false) {
        cur_unit_info = i;
        stage[war_scr].prepare_info(i);
        hide_windows(notHideLog);
        show_cre_info();
    };
    
    patchFunction(
        unsafeWindow,
        "play_button_onRelease",
        `hide_button('play_button');`,
        `if (document.activeElement.id ==="topInput") return;
        hide_button('play_button');`
    );
    patchFunction(
        stage.pole,
        "html",
        'return \'<font color=\\"\'+this.obj[i].get_color()+\'\\">\'+this.obj[i].nametxt+\'</font>\';',
        'return `<font class="log_cre_name" id="cre${i}" color="${this.obj[i].get_color()}"><span class="cre_info">🔍 </span>${this.obj[i].nametxt}</font><span class="cre_quantity">${this.obj[i].hero ? `[${this.obj[i].maxhealth}]` : `[${this.obj[i].nownumber}]`}</span><endcre>`;'
    );
    patchFunction(
        stage.pole,
        "prepare_hero_info",
        "document.getElementById('hero_info_head').innerHTML = this.get_name_html(i);",
        `document.getElementById('hero_info_head').innerHTML = \`<div style="position:relative; right: 2em"><span title="${L.ui.filterLabelCre}" id="cre\${this.obj[i].obj_index}" class="follow-cre-filtered-button">📜</span>\` + this.get_name_html(i)+"</div>";`
    )

    patchFunction(
        stage.pole,
        "prepare_info",
        `document.getElementById('cre_info_head').innerHTML = this.get_name_html(i);`,
        `document.getElementById('cre_info_head').innerHTML = this.get_name_html(i);
        document.querySelector(".info_head_name").insertAdjacentHTML("beforeend", \`&nbsp;\${this.obj[i].nownumber !== this.obj[i].maxnumber ? "<span><s>" + this.obj[i].maxnumber + "</s></span>" : ""} &nbsp;&nbsp;<span>\${this.obj[i].nownumber !== 0 ? (this.obj[i].nownumber - 1) * this.obj[i].maxhealth + this.obj[i].nowhealth : 0} ${LOCALE === "en" ? "hp" : "хп"} </span>\`);document.querySelector(".info_head_name").insertAdjacentHTML("beforebegin", \`<span title="${L.ui.filterLabelCre}" id="cre\${this.obj[i].obj_index}" class="follow-cre-filtered-button">📜</span>\`);`
    )
    patchFunction(
        stage.pole,
        "calcinitiative",
        "nowturnobj = k;",
        `nowturnobj = k;
     if (typeof lastObject !== "undefined" && lastObject !== nowturnobj) {
       wrapLastTurn();
       showtext();
     }
     lastObject = nowturnobj;`
    );

    patchFunction(
        unsafeWindow,
        "showtext",
        "log_lines[log_cnt] = htmllog.substr(0, i+4);",
        `log_lines[log_cnt] = "<p>" + htmllog.substr(0, i+4) + "</p>"; processLine(log_cnt);`
    );
}

/* -------------------------
   Boot
--------------------------*/

let settings_interval = setInterval(() => {
    const warImages = [...document.images].find((img) => img.src.includes("war_images.png"));
    if (!warImages || Object.keys(stage.pole.obj).length === 0) return;
    clearInterval(settings_interval);
    if (!classic_chat && document.querySelector("#area_chat_header_area").style.display === "none" && document.querySelector("#area_chat").style.display === "none") show_button("area_chat_header_area");
    initMagicCalc();
    if (!android && !iOS) document.querySelector("#win_FullLog").insertAdjacentHTML("afterbegin", `<input id="topInput" type="text" placeholder="${L.ui.searchPlaceholder}" />`);
    document.querySelector("#cre_distance_on").checked = cre_distance_on
    document.querySelector("#animation_speed_on").checked = animation_speed_on;
    let spd = get_GM_value_if_exists("anim_speed", getCurrentBattleSpeed());
    document.getElementById("anim_speed_val").textContent = spd;
    document.querySelector("#anim_speed").value = spd
    if (GM_getValue("animation_speed_on")) setBattleSpeed(spd);
    // авторасстановка в ГВ
    //if (btype === 66) setTimeout(make_ins_but, 1000);
    if (location.href.includes("&lt")) {
        const test = document.querySelector("#pause_button");
        if (test.style.display === "none") {
            show_button("pause_button");
            pause_button_onRelease()
        }
    }
    getIcon("skull")
        .then((skullImg) => {
        icons.dead.html = icons.dead.html.replace("<img>", skullImg.outerHTML);
    })
        .then(() => getIcon("damage"))
        .then((damageImg) => {
        icons.damage.html = icons.damage.html.replace("<img>", damageImg.outerHTML);
        init();
    });
}, 300);
let upravaRoot;
const upravaInterval = setInterval(() => {
    upravaRoot = document.querySelector("#win_BattleControl");
    if (!upravaRoot) return;
    else {
        clearInterval(upravaInterval);
        upravaRoot.insertAdjacentHTML("afterbegin",
                                      `
        <style>
            .hidden {
                display: none;
            }
        </style>
            `);
        insertInput();

        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    insertInput();
                }
            });
        });
        const config = { childList: true, subtree: true };
        observer.observe(upravaRoot, config);
    }

}, 100);
const distance_counter = document.getElementById("cre_distance");
const anim_speed_counter = document.querySelector("#anim_speed")


// =========  Event Listeners ============
let speedCount, distance;
document.body.addEventListener('input', function(event) {
    switch (event.target.id) {
        case "cre_distance":
            distance = parseInt(distance_counter.value);
            if (isNaN(distance)) {
                distance_counter.value = "";
                return;
            }
            if (distance < 1) {
                distance_counter.value = 1;
                return;
            }
            if (!cre_distance_on) return
            GM_setValue('cre_distance', distance)
            if (isOpen) refresh()
            individual_calc.innerHTML = individual_calc_innerHTML(last_individual_calc.atk_obj_index, last_individual_calc.def_obj_index)
            cre_distance_div.innerHTML = `${t('chosenDistance')}: <span style= "color:white">${GM_getValue('cre_distance')}</span><br>`
            break;
        case "anim_speed":
            speedCount = Number(event.target.value);
            GM_setValue('anim_speed', speedCount);
            document.getElementById("anim_speed_val").textContent = document.getElementById("anim_speed").value;
            if (!animation_speed_on) return;
            setBattleSpeed(speedCount);

            break;
    }
});
const anim_speed_input = document.querySelector('#anim_speed')
document.addEventListener("keydown", (event) => {
    const keyCode = Number(event.keyCode);
    if (event.keyCode === 113) {
        if (document.querySelector("#hwmkb_overlay_host")) document.querySelector("#hwmkb_overlay_host").remove();
        else openHwmkbOverlay();
    }
    // keep pressedKeys consistent
    pressedKeys.add(keyCode);

    if (isChatFocused()) return;

    // If trigger is required, block everything below unless trigger held
    if (!triggerOk()) return;

    // Toggle speed (keydown)
    if (isValidCode(kb.toggleSpeed) && keyCode === kb.toggleSpeed) {
        animation_speed_on = GM_toggle_boolean("animation_speed_on", GM_getValue("animation_speed_on"));
        const chk = document.querySelector("#animation_speed_on");
        if (chk) chk.checked = animation_speed_on;

        if (animation_speed_on) {
            GM_setValue("anim_speed", anim_speed_counter.value);
            setBattleSpeed(anim_speed_counter.value);
        } else {
            setBattleSpeed(getCurrentBattleSpeed());
        }
        return; // optional: prevent other actions on same keypress
    }

    // ArrowUp / ArrowDown for anim speed (only when triggerOk)
    if (event.key === "ArrowUp" || event.key === "ArrowDown") {
        event.preventDefault();
        const increment = event.key === "ArrowUp" ? 1 : -1;
        anim_speed_input.value = String(Number(anim_speed_input.value) + increment);
        anim_speed_input.dispatchEvent(new Event("input", { bubbles: true }));
        return;
    }

    // Auto battle
    if (isValidCode(kb.autoBattle) && keyCode === kb.autoBattle) {
        fastbut_onRelease2();
        return;
    }

    // Auto placement
    if (isValidCode(kb.autoPlacement) && keyCode === kb.autoPlacement) {
        make_ins_but();
        return;
    }

    // Start battle
    if (isValidCode(kb.startBattle) && keyCode === kb.startBattle) {
        const btn = document.querySelector("#confirm_ins_img");
        if (btn) triggerMouseUpEvent(btn);
        return;
    }

    // Back
    if (isValidCode(kb.backToGame) && keyCode === kb.backToGame) {
        if (history.length > 1) back_to_game_button_onRelease();
        else back_to_home_button_onRelease();
    }
});
document.body.addEventListener('change', function(event) {
    switch (event.target.id) {
        case "mag_damage_on":
            mag_damage_on = GM_toggle_boolean("mag_damage_on", mag_damage_on);
            if (isOpen) refresh();
            individual_calc.innerHTML = individual_calc_innerHTML(last_individual_calc.atk_obj_index, last_individual_calc.def_obj_index)
            break;
        case "coeff_on":
            coeff_on = GM_toggle_boolean("coeff_on", coeff_on);
            if (isOpen) refresh();
            break;
        case "choose_cre":
            chosen.creature = select.value
            refresh()
            break;
        case "cre_distance_on":
            cre_distance_on = GM_toggle_boolean("cre_distance_on", cre_distance_on)
            cre_distance_on ? GM_setValue('cre_distance', distance_counter.value) : GM_setValue('cre_distance', "")
            if (cre_distance_on) {
                cre_distance_div.innerHTML = `<span>${t('chosenDistance')}: ${GM_getValue('cre_distance')}</span><br>`
                cre_distance_div.style.display = "inline"
                if (isOpen) refresh()
                individual_calc.innerHTML = individual_calc_innerHTML(last_individual_calc.atk_obj_index, last_individual_calc.def_obj_index)
            } else {
                cre_distance_div.innerHTML = ""
            }
            if (isOpen) refresh()
            individual_calc.innerHTML = individual_calc_innerHTML(last_individual_calc.atk_obj_index, last_individual_calc.def_obj_index)
            break;
        case "animation_speed_on":
            animation_speed_on = GM_toggle_boolean("animation_speed_on", GM_getValue("animation_speed_on"))
            if (animation_speed_on) {
                GM_setValue('anim_speed', anim_speed_counter.value)
                setBattleSpeed(anim_speed_counter.value);
            } else {
                setBattleSpeed(getCurrentBattleSpeed());
            }
            break;
    }
});
document.body.addEventListener('click', function(event) {
    if (event.target.parentElement && /speed(.)_button/.test(event.target.parentElement.id)) {
        setBattleSpeed(getCurrentBattleSpeed());
        document.querySelector("#animation_speed_on").checked = false
        GM_setValue('animation_speed_on', false)
    }
    switch (event.target.id) {
        case "dmg_list_refresh":
            readjust_elements()
            refresh()
            break
        case "change_side":
            chosen.afterSideSwitchCre[chosen.side] = chosen.creature
            chosen.side = -chosen.side
            chosen.creature = chosen.afterSideSwitchCre[chosen.side]
            refresh()
            break
        case "collapse":
            readjust_elements()
            isOpen = false
            refresh_button.innerHTML = t('btnListCollapsed');
            set_Display([select, side_button, collapse_button, document.querySelector("#chosen_cre_heading"), dmg_list_container, individual_calc, cre_distance_div], "none")
            break;

        case "cre_distance_plus":
            document.getElementById('cre_distance').value = parseInt(document.getElementById('cre_distance').value || 0) + 1;
            document.getElementById('cre_distance').dispatchEvent(new Event('input', { bubbles: true }));
            break;
        case "cre_distance_minus":
            document.getElementById('cre_distance').value = parseInt(document.getElementById('cre_distance').value || 0) - 1;
            document.getElementById('cre_distance').dispatchEvent(new Event('input', { bubbles: true }));
            break;
    }
});

let calc_attacker, magshot_x, magshot_y;

function manageDamageCalc() {
    // если бой не начался, популизирует mapobj
    if (lastturn <= 0) {
        for (const cre of Object.values(stage.pole.obj)) {
            const coords = [{x: cre.x, y: cre.y}];
            if (cre.big) coords.push({x: cre.x+1, y: cre.y}, {x: cre.x+1, y: cre.y+1}, {x: cre.x, y: cre.y+1});
            for (const coord of coords){
                mapobj[coord.x + coord.y * defxn] = cre.obj_index;
            }
            
        }
    }
    let cre_collection = stage.pole.obj;
    if (mapobj[xr_last + yr_last * defxn] === undefined || cre_collection[mapobj[xr_last + yr_last * defxn]].rock === 1) {
        paint_coords(xr_last, yr_last, "#cccccc");
        return;
    }
    if (calc_attacker === undefined) {
        calc_attacker = cre_collection[mapobj[xr_last + yr_last * defxn]];
        paint_coords(xr_last, yr_last, "#800000");
        if (calc_attacker.hero === 1) {
            readjust_elements();
            individual_calc.innerHTML = ` <div id="individual_cre_heading" style="display:inline; background-color: ${physCalcColor}">
                  <span>${t("damage")} <br>
                  </span>
                    <b>${calc_attacker.nametxt}</b> ${LOCALE === "en" ? "to" : "по"}
                    <br>
                    </div>
                    `;
        }
    } else {
        readjust_elements();
        let def_obj_index = mapobj[xr_last + yr_last * defxn];
        set_Display([individual_calc, collapse_button], "inline");
        if (cre_distance_on) {
            cre_distance_div.style.display = "inline";
            cre_distance_div.innerHTML = `<span>${t('chosenDistance')}: ${GM_getValue('cre_distance')}</span><br>`
        }
        individual_calc.innerHTML = individual_calc_innerHTML(calc_attacker.obj_index, def_obj_index);
        calc_attacker = undefined;
        paint_coords(xr_last, yr_last, "blue");
    }
}

// если есть настройка подтверждения хода = моб. версия, + поддержка моб версии
const mobileInterval = setInterval(() => {
    if ([typeof android, typeof iOS].includes("undefined")) return;
    clearInterval(mobileInterval);
    if (!android && !iOS) return;
    const helpButtonHTML = `<div id="help_buttonScript" class="toolbars_mobile_img"><span style = "color:white;">${t('helpDamage')}</span><img id = "help_imgScript" src="https://dcdn.heroeswm.ru/i/combat/btn_help.png?v=6" style="opacity:0.5"><br></div>`
    document.querySelector("#left_button").insertAdjacentHTML("beforeEnd", helpButtonHTML);
    const helpButton = document.querySelector("#help_buttonScript");
    helpButton.addEventListener("touchend", event => {
        calc_x = calc_y = undefined;
        const firstTime = localStorage.getItem("battle_damage_tooltip_mobile_first_time");
        if (!firstTime) {
            localStorage.setItem("battle_damage_tooltip_mobile_first_time", 1);
            alert(t("mobileFirstTimeAlert"));
        }
        const img = helpButton.querySelector("img");
        img.style.opacity = img.style.opacity === "0.5" ? "1" : "0.5";
        helpButton.classList.toggle("active");
    })
    document.addEventListener("touchend", event => {
        info_btn_cnt = 0;
        if (!helpButton.classList.contains("active") || event.target.id === "help_imgScript" || event.target.tagName !== "CANVAS") return;
        manageDamageCalc();
    })

}, 100);
document.addEventListener("click", event => {
    if (event.target.className === "open-keybinds") openHwmkbOverlay();
    if (!info_btn_cnt) return;
    info_btn_cnt = 0;
})
// Урон одного стека по другому по выбору нажатием кнопки E
// Helper: don't steal typing
function isChatFocused() {
    return (
        document.querySelector("#chattext") === document.activeElement ||
        document.querySelector("#chattext_classic") === document.activeElement ||
        document.activeElement.id === "topInput"
    );
}

// Helper: if trigger is enabled, require it; otherwise allow direct binds
function triggerOk() {
    return !kb.useTrigger || pressedKeys.has(kb.triggerKey);
}

// Optional: if you want to ignore "unknown" keys now that you don't use keyboardKeycodes map
function isValidCode(code) {
    return Number.isFinite(code) && code > 0;
}

// ---- KEYUP (actions that should fire on release) ----
window.addEventListener("keyup", (event) => {
    const keyCode = Number(event.keyCode);

    // keep pressedKeys consistent
    pressedKeys.delete(keyCode);
    if (keyCode === 27 && document.querySelector("#win_FullLog")?.style.display !== "none") {
        unFilterLog();
        document.querySelector("#filter-log-div")?.remove();
        document.querySelector("#btn_close_fullLog")?.click();
    }
    if (document.activeElement.id === "topInput"){
        event.stopPropagation();
        event.stopImmediatePropagation();
        const logSearch = document.querySelector("#topInput");
        unFilterLog();
        if (logSearch.value !== ""){
            filterLog({keyword: logSearch.value});
            return;
        }
    }
    if (isChatFocused()) return;
    if (isValidCode(kb.filterLog) && keyCode === kb.filterLog){
        unFilterLog();
        document.querySelector("#filter-log-div")?.remove();
        const cre = stage.pole.obj[mapobj[xr_last + yr_last * defxn]];
        document.querySelector("#btn_close_fullLog").insertAdjacentHTML(
            "beforebegin",
            `<div id="filter-log-div">
        <input type="checkbox" id="filter-log-checkbox" name="filter-log-checkbox"}>
        <label for="filter-log-checkbox">
          ${L.ui.filterLabel}
          <div id="filter-log-cre">${creHTML(cre)}</div>
        </label>
      </div>`
        );
        document.getElementById("filter-log-checkbox").click();
        const hex = cre.get_color();
        shadowStyle.innerHTML = `
      #cre${cre.obj_index}{
        text-shadow:
          0 0 6px ${hex}99,
          0 0 14px ${hex}99,
          0 0 24px ${hex}99
      }
      #cre${cre.obj_index} + .cre_quantity { display:inline; }
    `;

        // hide_all_buttons();
        setTimeout(() => {
            document.querySelector("#win_ShortLog").click();
        }, 200);

    }
    // Your input-specific behavior stays
    if (event.target && event.target.id === "uprava_filter_input") {
        upravaEvent(event);
        return;
    }

    // See damage (keyup)
    if (isValidCode(kb.seeDamage) && keyCode === kb.seeDamage) {
        manageDamageCalc();
    }

    // Mag shot (keyup)
    if (isValidCode(kb.seeMagShot) && keyCode === kb.seeMagShot) {
        if (!magshot_x) {
            [magshot_x, magshot_y] = [xr_last, yr_last];
            paint_coords(xr_last, yr_last, "#FC7052", 4000);
        } else {
            paint_coords(xr_last, yr_last, "#7A71FE", 4000);
            magshot(magshot_x, magshot_y, xr_last, yr_last);
            magshot_x = magshot_y = null;
        }
    }
});
// -----------------------------------

// =========  маг. урон ============

const damageSpells = (I18N[LOCALE] || I18N.en).spellNames;

function calcFactionModifier(attacker, defender) {
    let modifier, k;
    if ((umelka[attacker['owner']][0] > 0) && (umelka[defender['owner']][0] > 0)) {
        k = umelka[attacker['owner']][0];
        if ((k > 0) && (k < 11)) {
            let j = umelka[defender['owner']][k];
            modifier = 1 - j * 0.03;
        };
    };
    return modifier;
}

function calcHellFire(attacker, defender, cre_collection) {
    const factionModifier = calcFactionModifier(attacker, defender);
    const spellPower = cre_collection[heroes[attacker.owner]].maxnumber;
    const perkModifier = isperk(attacker.obj_index, 104) ? 1.5 : 1;
    const res = Math.floor(Math.round((7 * spellPower + 7) * perkModifier) * factionModifier);
    return res;
}
// добавление spellname_magiceff для объекта для дальнийших расчетов
function initEff(s, activeobj_S) { // s = spell name
    if (stage[war_scr].obj[activeobj_S][s + 'effmain'] > 0) {
        let eff;
        if (stage[war_scr].obj[activeobj_S].hero) {
            var s1 = 0;
            if ((isperk(activeobj_S, 93)) && ((s == 'magicfist') || (s == 'raisedead'))) { s1 = 4; };
            if ((isperk(activeobj_S, 78)) && ((s == 'poison') || (s == 'mpoison'))) { s1 += 5; };
            if ((isperk(activeobj_S, 89)) && ((s == 'poison') || (s == 'mpoison'))) { s1 += 3; };
            eff = (stage[war_scr].obj[activeobj_S][s + 'effmain'] + stage[war_scr].obj[activeobj_S][s + 'effmult'] * (stage[war_scr].getspellpower(activeobj_S, s) + s1));
            if (stage[war_scr].obj[activeobj_S][s + 'effmult'] == 1.5) { eff = Math.round(eff); };
            var teff = eff;
        } else {
            eff = Math.round(stage[war_scr].obj[activeobj_S][s + 'effmain'] + stage[war_scr].obj[activeobj_S][s + 'effmult'] * Math.pow(stage[war_scr].obj[activeobj_S]['nownumber'], 0.7));
            if (s == 'blind') {
                eff = Math.round(stage[war_scr].obj[activeobj_S][s + 'effmain'] + stage[war_scr].obj[activeobj_S][s + 'effmult'] * stage[war_scr].obj[activeobj_S]['nownumber']);
            };
            var teff = eff;
        }
        stage[war_scr].obj[activeobj_S][s + '_magiceff'] = eff;
    }
}
function fetchHP(id){
    const htmlText = send_get(`/army_info.php?name=${id}`);
    const doc = parser.parseFromString(htmlText, "text/html");
    let hp = doc.querySelector("body > center > table > tbody > tr > td > table > tbody > tr > td > div > div.info_text_content > div:nth-child(7) > div").textContent;
    return hp;
}
function initMagicCalc() {
    patchFunction(
        stage.pole,
        "attackmagic",
        `if (this.obj[attacker].getside()==this.obj[defender]['side']) return 0;`,
        ""
    )

    // родной calcmagic с удалением кодом массовых заклов и др. побочных эффектов наведения курсора с активным заклом
    stage.pole.calcmagic_script = function calcmagic(atk_x, atk_y, xr, yr, magicuse, penalty = 1) {
        let i = mapobj[atk_x + defxn * atk_y];
        const bossNownumber = stage.pole.obj[i].nownumber;
        if (stage.pole.obj[i].boss && btype !== 135 && btype !== 139){
            const creName = stage.pole.obj[i].filename.replace("ani", "");
            let regularHP = parseInt(creatureHPs[creName]);
            if (!regularHP) {
                regularHP = parseInt(fetchHP(creName));
                if (isNaN(regularHP)) {
                    alert(`Creature ${stage.pole.obj[i].nametxt} not found! Write to Something begins`);
                    regularHP = stage.pole.obj[i].maxhealth;
                }
                savedHPs[creName] = regularHP;
                creatureHPs[creName] = regularHP;
                localStorage.setItem("savedHPs", JSON.stringify(savedHPs));
            }
            stage.pole.obj[i].nownumber = stage.pole.obj[i].maxhealth / regularHP;
        }
        const activeobj_S = i;
        initEff(magicuse, i);
        Totalmagicdamage = 0;
        Totalmagickills = 0;

        var ok = false;
        mul = 1;
        var len = stage.pole.obj_array.length;
        for (var k1 = 0; k1 < len; k1++) {
            var j = stage.pole.obj_array[k1];
            stage.pole.obj[j]['attacked'] = 1;
            stage.pole.obj[j]['attacked2'] = 1;
        };

        if ((magicuse == 'magicfist') || (magicuse == 'angerofhorde')) {
            var eff = stage.pole.obj[activeobj_S][magicuse + '_magiceff'];
            if ((magicuse == 'magicfist') && (stage.pole.obj[mapobj[xr + yr * defxn]]['organicarmor'])) eff = Math.round(eff * 0.2);
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], eff, 'neutral', magicuse, 0, 0, 0);
            ok = true;
        };
        if (magicuse == 'swarm') {
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], stage.pole.obj[activeobj_S][magicuse + '_magiceff'], 'other', magicuse, 0, 0, 0);
            ok = true;
        };
        if (magicuse === "divinev") {
            var separhsum = (stage.pole.obj[mapobj[xr + yr * defxn]].separhsum ? 1 : 0);
            var eff = (stage.pole.obj[i]['divineveffmain'] + Math.round(stage.pole.obj[i]['divineveffmult'] * Math.pow(stage.pole.obj[i]['nownumber'], 0.7))) * Math.sqrt(separhsum);
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], eff, 'other', 'divinev', 0, 0, 0);
            ok = true;
        }
        if ((magicuse == 'magicarrow') || (magicuse == 'lighting')) {
            if (stage.pole.obj[activeobj_S]['calllightning']) {
                stage.pole.obj[activeobj_S]['lighting_magiceff'] = 50 * stage.pole.obj[activeobj_S]['nownumber'];
            };
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], Math.round(stage.pole.obj[activeobj_S][magicuse + '_magiceff'] * mul), 'air', magicuse, 0, 0, 0);
            ok = true;
        };
        if (magicuse == 'firearrow') {
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], Math.round(stage.pole.obj[activeobj_S][magicuse + '_magiceff'] * mul), 'fire', magicuse, 0, 0, 0);
            ok = true;
        };
        if (magicuse == 'icebolt') {
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], Math.round(stage.pole.obj[activeobj_S][magicuse + '_magiceff'] * mul), 'cold', magicuse, 0, 0, 0);
            ok = true;
        };
        if (magicuse == 'implosion') {
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], Math.round(stage.pole.obj[activeobj_S][magicuse + '_magiceff'] * mul), 'earth', magicuse, 0, 0, 0);
            ok = true;
        };

        if (magicuse == 'poison') {
            stage.pole.calcpoison(i, mapobj[xr + yr * defxn], stage.pole.obj[activeobj_S][magicuse + '_magiceff']);
            ok = true;
        };
        if (magicuse == 'meteor') {
            var eff = stage.pole.obj[activeobj_S][magicuse + '_magiceff'];
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], Math.round(eff * mul), 'earth', magicuse, 0, 0, 0);
            ok = true;
        };
        if (magicuse == 'chainlighting') {
            var eff = stage.pole.obj[activeobj_S][magicuse + '_magiceff'];
            if (stage.pole.obj[activeobj_S]['spmult'] > 1) {
                eff = Math.round(stage.pole.obj[activeobj_S]['spmult'] * (stage.pole.obj[activeobj_S]['chainlightingeffmain'] + stage.pole.obj[activeobj_S]['chainlightingeffmult'] * Math.pow(stage.pole.obj[activeobj_S]['nownumber'], 0.7)));
            }
            if (penalty === 1) {
                stage.pole.attackmagic(i, mapobj[xr + yr * defxn], Math.round(eff * mul), 'air', 'lighting', 0, 0, 0);
            } else {
                stage.pole.attackmagic(i, mapobj[xr + yr * defxn], Math.floor(Math.round(eff * mul) * penalty), 'air', 'lighting', 0, 0, 0);
            }
            ok = true;
        };

        if (magicuse == 'fireball') {
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], Math.round(stage.pole.obj[activeobj_S][magicuse + '_magiceff'] * mul), 'fire', magicuse, 0, 0, 0);
            ok = true;
        };
        if (magicuse == 'stormcaller') {
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], Math.round(stage.pole.obj[activeobj_S].nownumber * 10), 'air', magicuse, 0, 0, 0);
            ok = true;
        };
        if (magicuse == 'firewall') {
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], stage.pole.obj[activeobj_S][magicuse + '_magiceff'], 'fire', magicuse, 0, 0, 0);
            ok = true;
        };
        if (magicuse == 'circle_of_winter') {
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], Math.round(stage.pole.obj[activeobj_S][magicuse + '_magiceff'] * mul), 'water', magicuse, 0, 0, 0);
            ok = true;
        };

        if (magicuse == 'stonespikes') {
            stage.pole.attackmagic(i, mapobj[xr + yr * defxn], Math.round(stage.pole.obj[activeobj_S][magicuse + '_magiceff'] * mul), 'earth', magicuse, 0, 0, 0);
            ok = true;
        };
        if (stage.pole.obj[i].boss && btype !== 135) stage.pole.obj[i].nownumber = bossNownumber;
        return Totalmagicdamage;
    };
    patchFunction(
        unsafeWindow,
        "calcpoison",
        "var magicdamage=eff;",
        `
            var magicdamage=eff;
            if ((umelka[stage.pole.obj[attacker]['owner']][0] > 0) && (umelka[stage.pole.obj[defender]['owner']][0] > 0)) {

                var k = umelka[stage.pole.obj[attacker]['owner']][0];

                if ((k > 0) && (k < 11)) {

                    j = umelka[stage.pole.obj[defender]['owner']][k];

                    magicdamage = magicdamage * (100 - j * 3) / 100;

                };

            };

            magicdamage = Math.round(magicdamage);
        `
    )
    patchFunction(
        unsafeWindow,
        "check_keys",
        "(key==68)||(key==32)",
        "key==68"
    )
    patchFunction(
        unsafeWindow,
        "defend_button_release",
        "loader.loading = true;",
        `
        if (Object.values(heroes).includes(activeobj) && activeobj !== 0) {
            if (lastTurnDefended !== lastturn) {
                showPopup("Оборона была одноразово заблокирована");
                lastTurnDefended = lastturn;
                return 0;
            }
        };
        loader.loading = true;
        `
    )
    const fiveSecStr = fiveSecWarning ? `if (timer === 5 && activeobj) playsound(0, "readysound", 100);` : "";
    patchFunction(
        stage[war_scr],
        "check_timer",
        "document.getElementById('timer').innerHTML = timer;",
        `
        ${fiveSecStr}
        if (timer <= 5 && (activeobj || lastturn < 0)) {
            document.getElementById('timer').innerHTML = \`<span style="color:red">\${timer}</span>\`;
        } else {
            document.getElementById('timer').innerHTML = timer
        }
        `
    );

}
// -----------------------------------

// Функция рельсы гвд с поправкой на выбор клеток юзером
function magshot(x1, y1, xr, yr) {
    var x2 = xr;
    var y2 = yr;
    var dx = Math.abs(x1 - x2);
    var dy = Math.abs(y1 - y2);
    var skip = false;
    if (x1 < x2) {
        var xp = 1;
    } else {
        var xp = -1;
    };
    if (y1 < y2) {
        var yp = 1;
    } else {
        var yp = -1;
    };
    if (dx > dy) {
        if (x1 > x2) {
            var x = -5;
        } else {
            var x = defxn + 3 - 1;
        };
        var y = (y2 - y1) / (x2 - x1) * (x - x1) + y1;
    } else {
        if (y1 > y2) {
            var y = -5;
        } else {
            var y = defyn + 5 + 1;
        };
        var x = (x2 - x1) / (y2 - y1) * (y - y1) + x1;
    };
    x = x1;
    y = y1;
    while ((x > 0) && (y > 0) && (x <= defxn - 2) && (y <= defyn)) {
        if (dx > dy) {
            x += xp;
            y = (y2 - y1) / (x2 - x1) * (x - x1) + y1;
        } else {
            y += yp;
            x = (x2 - x1) / (y2 - y1) * (y - y1) + x1;
        };
        let shot_coords = []
        if ((Math.round(x) > 0) && (Math.round(y) > 0) && (Math.round(x) <= defxn - 2) && (Math.round(y) <= defyn)) {
            if (shado[Math.round(x) + Math.round(y) * defxn]) {
                set_visible(shado[Math.round(x) + Math.round(y) * defxn], 1);
                shot_coords.push({ x: Math.round(x), y: Math.round(y) })
            }
        };
        setTimeout(() => {
            for (const coord of shot_coords) {
                set_visible(shado[coord.x + coord.y * defxn], 0);
            }
        }, 4000)
    };
};

// Родная функция гвд с поправками на переменную l и модификаторами magic[]
function attackmonster(attacker, ax, ay, x, y, defender, cre_distance, shootok, koef, inuse) {
    let cre_collection = stage.pole.obj
    var mainattack = 1;
    var ax1 = ax;
    var ay1 = ay;
    if (defender == 1000) return 0;
    if (defender <= 0) return 0;
    if (!cre_collection[defender]) return 0;
    if (cre_collection[defender]['hero']) return 0;
    if (cre_collection[defender]['rock']) return 0;
    if (koef == undefined) koef = 1;
    if (inuse == undefined) inuse = '';
    var len = wmap2[y * defxn + x];
    if ((cre_collection[attacker].x == x) && (cre_collection[attacker].y == y)) len = spd;
    shootok = 1;

    function getAdjacentAndDiagonalCoords(x, y) {
        const adjacentAndDiagonalCoords = [];
        adjacentAndDiagonalCoords.push([x + 1, y]);
        adjacentAndDiagonalCoords.push([x - 1, y]);
        adjacentAndDiagonalCoords.push([x, y + 1]);
        adjacentAndDiagonalCoords.push([x, y - 1]);
        adjacentAndDiagonalCoords.push([x + 1, y + 1]);
        adjacentAndDiagonalCoords.push([x - 1, y + 1]);
        adjacentAndDiagonalCoords.push([x + 1, y - 1]);
        adjacentAndDiagonalCoords.push([x - 1, y - 1]);
        return adjacentAndDiagonalCoords;
    }

    if (cre_collection[attacker].shots === 0) {
        shootok = 0
    } else {
        let attacker_adjacent_coords = getAdjacentAndDiagonalCoords(stage.pole.obj[attacker].x, stage.pole.obj[attacker].y)
        let enemies_list = Object.values(stage.pole.obj).filter(creature => creature.side != stage.pole.obj[attacker].side)
        enemies_list.forEach(enemy => {
            attacker_adjacent_coords.forEach(coord => {
                if (coord[0] == enemy.x && coord[1] == enemy.y && ![0, -1].includes(enemy.nownumber)) shootok = 0
            })
        })
    }
    if (cre_collection[attacker]['big']) {
        if (ax > x) {
            x++;
        };
        if (ay > y) {
            y++;
        };
    };
    if (cre_collection[attacker]['bigx']) {
        if (ax > x) {
            x++;
        };
    };
    if (cre_collection[attacker]['bigy']) {
        if (ay > y) {
            y++;
        };
    };
    var spd = Math.max(0, Math.round((cre_collection[attacker].speed + cre_collection[attacker]['ragespeed'] + cre_collection[attacker]['speedaddon']) * cre_collection[attacker].speedmodifier));
    if (magic[attacker]['ent']) {
        spd = 0;
    };
    var movelen = spd - len;
    attacker_c = attacker;
    ax_c = ax;
    ay_c = ay;
    x_c = x;
    y_c = y;
    defender_c = defender;
    shootok_c = shootok;
    if ((x == 0) && (y == 0)) {
        x = cre_collection[attacker]['x'];
        y = cre_collection[attacker]['y'];
    };
    if ((defender > 0) && (cre_collection[defender]['big'])) {
        if ((x - ax > 1) && (ax < x) && (defender == mapobj[ay * defxn + ax + 1])) {
            ax++;
        };
        if ((y - ay > 1) && (ay < y) && (defender == mapobj[(ay + 1) * defxn + ax])) {
            ay++;
        };
        if ((ax - x > 1) && (ax > x) && (defender == mapobj[ay * defxn + ax - 1])) {
            ax--;
        };
        if ((ay - y > 1) && (ay > y) && (defender == mapobj[(ay - 1) * defxn + ax])) {
            ay--;
        };
    };
    if ((defender > 0) && (cre_collection[defender]['bigx'])) {
        if ((x - ax > 1) && (ax < x) && (defender == mapobj[ay * defxn + ax + 1])) {
            ax++;
        };
        if ((ax - x > 1) && (ax > x) && (defender == mapobj[ay * defxn + ax - 1])) {
            ax--;
        };
    };
    if ((defender > 0) && (cre_collection[defender]['bigy'])) {
        if ((y - ay > 1) && (ay < y) && (defender == mapobj[(ay + 1) * defxn + ax])) {
            ay++;
        };
        if ((ay - y > 1) && (ay > y) && (defender == mapobj[(ay - 1) * defxn + ax])) {
            ay--;
        };
    };
    let dx = x - ax;
    let dy = y - ay;
    l = dx * dx + dy * dy;
    if (movelen == undefined) movelen = 0;
    if (cre_distance !== "") {
        movelen = cre_distance
        l = Math.round(cre_distance * cre_distance)
        if (l > 2) shootok = 1
        else shootok = 0
        if (cre_collection[attacker].shots === 0) shootok = 0
    }
    PhysicalModifiers = 1;
    PhysicalModifiers *= koef;
    if (cre_collection[attacker]['shadowattack']) {
        l = 0;
        shootok = 0;
    }

    var hera = 0;
    var herd = 0;
    len = stage.pole.obj_array.length;
    for (var k1 = 0; k1 < len; k1++) {
        k = stage.pole.obj_array[k1];

        if ((cre_collection[k].hero) && (cre_collection[k].owner == cre_collection[attacker].owner)) hera = k;
        if ((cre_collection[k].hero) && (cre_collection[k].owner == cre_collection[defender].owner)) herd = k;
    };
    if ((cre_collection[defender]['pirate'])&&((magic[defender]['sea'])||(gtype==125)||(gtype==126)||(gtype==133)||(gtype==166))){
        PhysicalModifiers*=0.85;
    };

    if (cre_collection[defender]['deadflesh']) {
        PhysicalModifiers *= 0.8;
    };
    if (cre_collection[defender]['immaterial']) {
        PhysicalModifiers *= 0.65;
    };
    if ((cre_collection[attacker]['oppressionofweak']) && (cre_collection[defender]['level'] == 1)) {
        PhysicalModifiers *= 1.5;
    };
    if ((cre_collection[attacker]['fearofstrong']) && (cre_collection[defender]['level'] == 7)) {
        PhysicalModifiers *= 0.5;
    };

    if ((defender > 0) && (cre_collection[attacker]['sorcererslayer']) && (cre_collection[defender]['caster']) && (cre_collection[defender]['maxmanna'] > 3)) {
        PhysicalModifiers *= 1.4 + 0.02 * Math.max(0, cre_collection[defender]['maxmanna'] - cre_collection[defender]['nowmanna']);
    };
    if ((hera > 0) && (magic[hera]['bna'])) {
        PhysicalModifiers = PhysicalModifiers * (1 + magic[hera]['bna']['effect'] / 100);
        if ((cre_collection[defender]['mechanical']) && (magic[hera]['MEC'])) {
            PhysicalModifiers *= 1 + magic[hera]['MEC']['effect'] / 100;
        };
        if ((cre_collection[attacker]['mechanical']) && (magic[hera]['mch'])) {
            PhysicalModifiers *= 1 + magic[hera]['mch']['effect'] / 100;
        };
    };
    if ((cre_collection[defender]['building']) && (!cre_collection[attacker]['siegewalls'])) {
        PhysicalModifiers *= 0.05;
    };
    if ((defender > 0) && (cre_collection[attacker]['cruelty']) && ((cre_collection[defender]['nowhealth'] < cre_collection[defender]['maxhealth']) || (cre_collection[defender]['nownumber'] < cre_collection[defender]['maxnumber']))) {
        PhysicalModifiers *= 1.15;
    };
    if ((defender > 0) && (cre_collection[attacker]['morecruelty']) && ((cre_collection[defender]['nowhealth'] < cre_collection[defender]['maxhealth']) || (cre_collection[defender]['nownumber'] < cre_collection[defender]['maxnumber']))) {
        PhysicalModifiers *= 1.3;
    };
    if ((cre_collection[attacker]['giantkiller']) && (cre_collection[defender]['big'])) PhysicalModifiers *= 2;
    if ((cre_collection[attacker]['pygmykiller']) && (!cre_collection[defender]['big'])) PhysicalModifiers *= 1.33;
    if (cre_collection[attacker]['stormstrike']) PhysicalModifiers *= 2;
    if ((cre_collection[attacker]['undeadkiller']) && (cre_collection[defender]['undead'])) PhysicalModifiers *= 1.5;
    if ((cre_collection[attacker]['pirate']) && (magic[defender]['blb'])) PhysicalModifiers *= 1.5;
 if ((magic[attacker]['chd']) && (cre_collection[magic[attacker]['chd']['effect']]['nownumber'] > 0) && (magic[attacker]['chd']['effect'] != defender)) {
        PhysicalModifiers *= 0.55;
    };
    if ((magic[attacker]['jdd']) && (cre_collection[magic[attacker]['jdd']['effect']]['nownumber'] > 0) && (magic[attacker]['jdd']['effect'] == defender)) {
        PhysicalModifiers *= 0.75;
    };
    if (magic[defender]['jdd']) {
        PhysicalModifiers *= 1.2;
    };
    if ((!cre_collection[attacker]['hero']) && (magic[attacker]['zat'])) {
        PhysicalModifiers *= 1.15;
    };
    if ((herd > 0) && (magic[herd]['bnd'])) {
        PhysicalModifiers = PhysicalModifiers / (1 + magic[herd]['bnd']['effect'] / 100);
    };
    if ((herd > 0) && (magic[herd]['fld'])) {
        PhysicalModifiers = PhysicalModifiers * (1 - magic[herd]['fld']['effect'] / 100);
    };
    if ((herd > 0) && (magic[herd]['rcd']) && (monster_race[cre_collection[attacker]['id']] == magic[herd]['rcd']['effect'])) {
        PhysicalModifiers = PhysicalModifiers * 0.93;
    };
    if (magic[attacker]['prp']) {
        PhysicalModifiers = PhysicalModifiers * (1 + magic[attacker]['prp']['effect'] / 100);
    };
    if (magic[defender]['sta']) {
        PhysicalModifiers *= 0.5;
    };
    if ((magic[attacker]['chd']) && (cre_collection[magic[attacker]['chd']['effect']]['nownumber'] > 0) && (magic[attacker]['chd']['effect'] != defender)) {
        PhysicalModifiers *= 0.55;
    };
    PhysicalModifiers *= stage.pole.checkmembrane(defender);
    if (!cre_collection[attacker]['hero']) {
        if ((l <= 2 || shootok === 0) && (cre_collection[attacker]['shooter']) && (!cre_collection[attacker]['nopenalty']) && (!cre_collection[attacker]['warmachine']) && !cre_collection[attacker].shadowattack) {
            PhysicalModifiers = PhysicalModifiers * 0.5;
        };
        if ((l > 2) && (cre_collection[attacker]['rangepenalty'])) {
            PhysicalModifiers = PhysicalModifiers * 0.5;
        };
        rangemod = 1;
        if (l > 2 && (shootok !== 0 || cre_collection[attacker].shots !== 0) && (cre_collection[attacker]['shooter']) && (((cre_collection[attacker]['range'] < Math.sqrt(l)) && (!cre_collection[attacker].shadowattack)) || ((iswalls) && (!cre_collection[attacker]['hero']) && (checkwall(x, y, ax, ay))))) {
            PhysicalModifiers = PhysicalModifiers * 0.5;
            rangemod = 0.5;
        };
        if (l > 2 && (shootok !== 0 || cre_collection[attacker].shots !== 0) && (cre_collection[attacker]['shooter']) && (iswalls2) && (!cre_collection[attacker]['hero']) && (((!cre_collection[attacker].siegewalls) || (btype == 118)) || (!cre_collection[defender].stone)) && (checkwall2(x, y, ax, ay, attacker))) {
            PhysicalModifiers = PhysicalModifiers * 0.5;
            rangemod *= 0.5;
        };
    };
    var _PERK_ARCHERY = 11;
    var _PERK_EVASION = 22;
    if ((defender > 0) && ((((cre_collection[attacker].shooter && shootok == 0) || (cre_collection[attacker].shooter != 1) || cre_collection[attacker].shots == 0) && (!cre_collection[attacker]['ballista']) && (inuse != 'ssh') && (inuse != 'mga') && (inuse != 'dcd') && (inuse != 'chs') && (!cre_collection[attacker]['hero'])) || (inuse == 'brs') || (inuse == 'cpt'))) {
        if (cre_collection[defender]['dodge'])
            PhysicalModifiers *= 0.5;
        if (cre_collection[defender]['brittle'])
            PhysicalModifiers *= 1.25;
    };
    if ((shootok === 1) && (!cre_collection[attacker]['hero']) && (cre_collection[attacker]['shooter'])) {
        if (isperk(attacker, _PERK_ARCHERY)) PhysicalModifiers *= 1.2;
        if (isperk(defender, _PERK_EVASION)) PhysicalModifiers *= 0.8;
        if ((!cre_collection[defender]['lshield']) && (stage.pole.shieldother(defender))) {
            PhysicalModifiers = PhysicalModifiers * 0.75;
        };
        if ((cre_collection[defender]['lshield']) || (cre_collection[defender]['hollowbones'])) {
            PhysicalModifiers = PhysicalModifiers * 0.5;
        };
        if (cre_collection[defender]['diamondarmor']) {
            PhysicalModifiers = PhysicalModifiers * 0.1;
        };
        if (cre_collection[defender]['shielded']) {
            PhysicalModifiers = PhysicalModifiers * 0.75;
        };
        if (cre_collection[defender]['unprotectedtarget']) {
            PhysicalModifiers = PhysicalModifiers * 1.25;
        };
        if (magic[defender]['dfm']) {
            PhysicalModifiers = PhysicalModifiers * (1 - magic[defender]['dfm']['effect'] / 100);
        };
        if (magic[attacker]['cnf']) {
            PhysicalModifiers = PhysicalModifiers * (1 - magic[attacker]['cnf']['effect'] / 100);
        };

        if (hera > 0) {
            if (magic[hera]['sat']) {
                PhysicalModifiers = PhysicalModifiers * (100 + magic[hera]['sat']['effect']) / 100;
            };
        };
    };
    if ((!cre_collection[attacker]['hero']) && (isperk(attacker, _PERK_BLESS))) {
        PhysicalModifiers *= 1.04;
    };
    if (isperk(attacker, _PERK_MERCYOFCOLD) && (defender > 0) && (magic[defender]['pfr']) && (magic[defender]['pfr']['nowinit'] > 0)) {
        PhysicalModifiers *= (1 + 0.03 * magic[defender]['pfr']['effect']);
    };
    let o = cre_collection[attacker]['owner'];
    if (magic[defender]['mf' + o]) {
        PhysicalModifiers *= 1 + magic[defender]['mf' + o]['effect'] / 100;
    };
    if ((!cre_collection[attacker]['hero']) && (isperk(attacker, _PERK_FERVOR))) {
        PhysicalModifiers *= 1.03;
    };
    if (hera > 0) {
        var h = hera;
        if ((magic[h]['nut']) && ((plid2 == -2) || (ohotnik_set_neutral()))) {
            PhysicalModifiers = PhysicalModifiers * (100 + magic[h]['nut']['effect']) / 100;
        };
        if ((magic[h]['mle']) && ((cre_collection[attacker].shooter && shootok == 0) || (cre_collection[attacker].shooter != 1) || cre_collection[attacker].shots == 0)) {
            PhysicalModifiers = PhysicalModifiers * (100 + magic[h]['mle']['effect']) / 100;
        };
        if (magic[attacker]['fbd']) {
            PhysicalModifiers = PhysicalModifiers * (100 + Math.floor(magic[attacker]['fbd']['effect'] / 10)) / 100;
        };
    };
    let leap_atk_bonus, leap_distance = 0;

    if (cre_collection[attacker].leap && l >= 4) {
        leap_distance = cre_distance ? cre_distance : Math.min((movelen - 1) * 2, Math.round(Math.sqrt(l)));
        leap_atk_bonus = cre_collection[attacker].attack * (1 + leap_distance * 0.1) - cre_collection[attacker].attack
        cre_collection[attacker]['attackaddon'] += leap_atk_bonus
    }
    monatt = cre_collection[attacker]['attack'] + cre_collection[attacker]['attackaddon'] + cre_collection[attacker]['rageattack'];
    if ((defender > 0) && (cre_collection[attacker]['giantslayer']) && (cre_collection[defender]['big'])) monatt += 4;
    if ((!cre_collection[attacker]['undead']) && (!cre_collection[attacker]['hero']) && (!cre_collection[attacker]['perseverance'])) {
        frig2 = false;
        i = attacker;
        var bigx = cre_collection[i]['big'];
        var bigy = cre_collection[i]['big'];
        if (cre_collection[i]['bigx']) bigx = 1;
        if (cre_collection[i]['bigy']) bigy = 1;
        xd = cre_collection[i]['x'];
        yd = cre_collection[i]['y'];
        for (var xz = xd - 1; xz <= xd + 1 + bigx; xz++) {
            for (var yz = yd - 1; yz <= yd + 1 + bigy; yz++) {
                if ((!frig2) && (mapobj[yz * defxn + xz] > 0) && (cre_collection[mapobj[yz * defxn + xz]]['side'] != cre_collection[i]['side']) && (cre_collection[mapobj[yz * defxn + xz]]['festeringaura']) && (cre_collection[mapobj[yz * defxn + xz]]['nownumber'] > 0)) {
                    monatt -= 4;
                    frig2 = true;
                };
            };
        };
    };

    if ((magic[attacker]['bsr']) || (magic[attacker]['rof'])) {
        monatt += Math.floor((cre_collection[attacker]['defence'] + cre_collection[attacker]['defenceaddon'] + cre_collection[attacker]['ragedefence']) * cre_collection[attacker]['defencemodifier']);
    };
    if (herd > 0) {
        h = herd;
        if ((magic[h]['mld']) && ((cre_collection[attacker].shooter && shootok == 0) || (cre_collection[attacker].shooter != 1) || cre_collection[attacker].shots == 0)) {
            PhysicalModifiers = PhysicalModifiers * (100 - magic[h]['mld']['effect']) / 100;
        };
        if ((magic[h]['_ia']) && (!cre_collection[attacker]['perseverance'])) {
            monatt *= (1 - magic[h]['_ia']['effect'] / 100);
        };
        if ((!cre_collection[attacker]['hero']) && (cre_collection[attacker].shooter) && (cre_collection[attacker].shots != 0) && (magic[h]['msk']) && shootok == 1) {
            PhysicalModifiers = PhysicalModifiers * (100 - magic[h]['msk']['effect']) / 100;
        };
    };
      if ((l>2)&&(defender>0)&&(cre_collection[attacker]['skycontrol'])&&(cre_collection[defender]['flyer'])&&(!cre_collection[defender]['teleport'])){
         PhysicalModifiers=PhysicalModifiers*1.25;
      };

       if ((l>2)&&(defender>0)&&(cre_collection[attacker]['aimedshot'])&&(magic[attacker]['aim'])&&(magic[attacker]['ai2'])){
           var defpos = defender + (defxn*cre_collection[defender]['y'] + cre_collection[defender]['x'])*1000;
           if (defpos == cre_collection[attacker]['aim']['effect']){
               PhysicalModifiers=PhysicalModifiers*Math.pow(1.4, magic[attacker]['ai2']['effect']);
           };
       };

       if ((l>2)&&(cre_collection[attacker]['shooter'])){
			var len = stage.pole.obj_array.length;
         var z = 0;
			for (var k1=0;k1<len;k1++)
			{
				k = stage.pole.obj_array[k1];
				if ((cre_collection[k]['omnipresentgaze'])&&(cre_collection[k]['owner']==cre_collection[attacker]['owner'])&&(cre_collection[k]['nownumber']>0)){
                   PhysicalModifiers=PhysicalModifiers*1.15;
                   break;
				};
			};
       };
    defadd = 0;
    if (cre_collection[defender]['agility']) {
        if (!magic[defender]['agl']) defadd = cre_collection[defender]['speed'] * 2;
    };
    if ((cre_collection[defender]['spirit']) && (!magic[defender]['spi'])) {
        PhysicalModifiers *= 0.5;
    };
    if ((cre_collection[attacker]['rageagainsttheliving']) && (cre_collection[defender]['alive'])) {
        PhysicalModifiers *= 1.3;
    };
    if ((cre_collection[defender]['defensivestance']) && (!magic[defender]['mvd'])) {
        defadd += 5;
    };
    if ((!cre_collection[defender]['undead']) && (!cre_collection[defender]['armoured']) && (!cre_collection[defender]['organicarmor'])) {
        frig2 = false;
        i = defender;
        bigx = cre_collection[i]['big'];
        bigy = cre_collection[i]['big'];
        if (cre_collection[i]['bigx']) bigx = 1;
        if (cre_collection[i]['bigy']) bigy = 1;
        xd = cre_collection[i]['x'];
        yd = cre_collection[i]['y'];
        for (let xz = xd - 1; xz <= xd + 1 + bigx; xz++) {
            for (let yz = yd - 1; yz <= yd + 1 + bigy; yz++) {
                if ((!frig2) && (mapobj[yz * defxn + xz] > 0) && (cre_collection[mapobj[yz * defxn + xz]]['side'] != cre_collection[i]['side']) && (cre_collection[mapobj[yz * defxn + xz]]['festeringaura']) && (cre_collection[mapobj[yz * defxn + xz]]['nownumber'] > 0)) {
                    defadd -= 4;
                    frig2 = true;
                };
            };
        };
    };
    if ((attacker > 0) && (cre_collection[defender]['giantslayer']) && (cre_collection[attacker]['big'])) defadd += 4;
    mondef = Math.round((cre_collection[defender]['defence'] + cre_collection[defender]['defenceaddon'] + defadd + cre_collection[defender]['ragedefence']) * cre_collection[defender]['defencemodifier']);
    if (magic[defender]['bsr']) {
        mondef = 0;
    };

    if ((cre_collection[attacker]['preciseshot']) && (l > 2) && (l <= 9) && (rangemod >= 1)) {
        mondef = 0;
    };
    if ((cre_collection[attacker]['ignoredefence'])) {
        mondef *= (1 - cre_collection[attacker]['ignoredefence'] / 100);
    };
    if (cre_collection[attacker]['crushingleadership']) {
        var morale_delta = stage.pole.getmorale(attacker) - stage.pole.getmorale(defender);
        if (morale_delta > 0) {
            mondef *= Math.max(0, 1 - morale_delta / 10);
        };
    };
    if (cre_collection[attacker]['sacredweapon']) {
        var dark_count = get_dark_count(defender);
        if (dark_count > 0) {
            mondef *= Math.max(0, 1 - 0.15 * dark_count);
        };
    };
    if (isperk(attacker, _PERK_PIERCING_LUCK)) {
        mondef *= 1 - Math.max(0, 0.025 * (cre_collection[attacker]['luck'] + cre_collection[attacker]['luckaddon']));
    };
    if ((cre_collection[defender]['ignoreattack'])) {
        monatt *= (1 - cre_collection[defender]['ignoreattack'] / 100);
    };
    if ((cre_collection[attacker]['ridercharge']) && (movelen > 0)) {
        mondef = mondef * (5 - movelen) / 5;
    };
    if ((cre_collection[attacker]['forcearrow']) && (!cre_collection[defender]['armoured']) && (!cre_collection[defender]['organicarmor']) && (l > 2)) {
        mondef *= 0.8;
    };
    if ((cre_collection[attacker]['armorpiercing']) && (!cre_collection[defender]['armoured']) && (!cre_collection[defender]['organicarmor']) && (l > 2)) {
        mondef *= 0.5;
    };
    if (cre_collection[defender]['shroudofdarkness']) {
        PhysicalModifiers *= Math.max(0, 1 - 0.15 * get_dark_count(defender));
    };
    if (cre_collection[attacker]['tasteofdarkness']) {
        PhysicalModifiers *= 1 + get_dark_count(defender) * 0.12;
    };
    if ((attacker > 0) && (!cre_collection[attacker]['hero']) && (isperk(attacker, _PERK_FALLEN_KNIGHT)) && (defender > 0)) {
            PhysicalModifiers *= 1 + get_dark_count(defender) * 0.06;
        };
    if ((cre_collection[attacker]['jousting']) && (movelen > 0)) {
        PhysicalModifiers = PhysicalModifiers * (1 + 0.05 * movelen);
    };
    if (((cre_collection[attacker]['blindingcharge']) || (cre_collection[attacker]['charge'])) && (movelen > 0)) {
        PhysicalModifiers = PhysicalModifiers * (1 + 0.1 * movelen);
    };
    if ((cre_collection[defender]['shieldwall']) && (movelen > 0)) {
        PhysicalModifiers = PhysicalModifiers * Math.max(0.1, 1 - 0.1 * movelen);
    };
    if ((magic[defender]['enc']) && (magic[defender]['enc']['effect'] == 1)) {
        PhysicalModifiers *= 0.5;
    };
    if ((cre_collection[attacker]['safeposition']) && (movelen == 0)) {
        PhysicalModifiers *= 1.5;
    };
    if ((cre_collection[attacker]['agilesteed']) && (movelen > 0)) {
        PhysicalModifiers *= 1 - 0.05 * movelen;
    };
    if (mondef < 0) {
        mondef = 0;
    };

    air = 0;
    fire = 0;
    water = 0;
    earth = 0;
    if ((hera > 0) && (!cre_collection[attacker]['taran'])) {
        h = hera;
        if (magic[h]['_id']) {
            mondef *= (1 - magic[h]['_id']['effect'] / 100);
        };
        if (magic[h]['_aa']) {
            air = magic[h]['_aa']['effect'] / 100;
        };
        if (magic[h]['_af']) {
            fire = magic[h]['_af']['effect'] / 100;
        };
        if (magic[h]['_aw']) {
            water = magic[h]['_aw']['effect'] / 100;
        };
        if (magic[h]['_ae']) {
            earth = magic[h]['_ae']['effect'] / 100;
        };
    };
    if ((cre_collection[defender]['armoured']) || (cre_collection[defender]['organicarmor'])) {
        mondef = Math.round((cre_collection[defender]['defence'] + cre_collection[defender]['defenceaddon'] + cre_collection[defender]['ragedefence']) * cre_collection[defender]['defencemodifier']);
    };
    if (monatt < 0) {
        monatt = 0;
    };
    if (monatt > mondef) {
        AttackDefenseModifier = 1 + (monatt - mondef) * 0.05;
    } else {
        AttackDefenseModifier = 1 / (1 + (mondef - monatt) * 0.05);
    };
    if (cre_collection[attacker]['hero']) {
        AttackDefenseModifier = 1;
    };
    var _PERK_ATTACK1 = 8;
    var _PERK_ATTACK2 = 9;
    var _PERK_ATTACK3 = 10;
    var _PERK_DEFENSE1 = 19;
    var _PERK_DEFENSE2 = 20;
    var _PERK_DEFENSE3 = 21;

    if ((!cre_collection[attacker]['hero']) && ((cre_collection[attacker].shooter && shootok == 0) || (cre_collection[attacker].shooter != 1))) {
        if (isperk(attacker, _PERK_ATTACK3)) {
            PhysicalModifiers *= 1.3;
        } else {
            if (isperk(attacker, _PERK_ATTACK2)) {
                PhysicalModifiers *= 1.2;
            } else
                if (isperk(attacker, _PERK_ATTACK1)) PhysicalModifiers *= 1.1;
        };
        if (isperk(defender, _PERK_DEFENSE3)) {
            PhysicalModifiers *= 0.7;
        } else {
            if (isperk(defender, _PERK_DEFENSE2)) {
                PhysicalModifiers *= 0.8;
            } else {
                if (isperk(defender, _PERK_DEFENSE1)) PhysicalModifiers *= 0.9;
            };
        };
    };
    if ((cre_collection[attacker]['siegewalls']) && (cre_collection[defender]['stone'])) {
        PhysicalModifiers *= 10;
    };
    var _PERK_COLD_STEEL = 14;
    var _PERK_FIERY_WRATH = 101;
    var _PERK_HELLFIRE_AURA = 123;
    var _PERK_RETRIBUTION = 16;

    if (isperk(attacker, _PERK_COLD_STEEL)) water = 1 - (1 - water) * (0.9);
    if (isperk(attacker, _PERK_FIERY_WRATH)) fire = 1 - (1 - fire) * (0.85);
    if (isperk(attacker, _PERK_HELLFIRE_AURA)) fire = 1 - (1 - fire) * (0.95);

    if (magic[attacker]['cre']) {
        air = 1 - (1 - air) * (1 - magic[attacker]['cre']['effect'] / 100);
    };

    if (isperk(attacker, _PERK_RETRIBUTION)) PhysicalModifiers *= (1 + Math.min(Math.max(stage.pole.getmorale(attacker, x, y), 0), 5) / 20);
    if ((cre_collection[attacker]['viciousstrike']) && (Math.max(0, Math.round((cre_collection[defender]['speed'] + cre_collection[defender]['ragespeed'] + cre_collection[defender]['speedaddon']) * cre_collection[defender]['speedmodifier'])) == 0)) PhysicalModifiers *= 1.5;
    PhysicalModifiers *= stage.pole.magicmod(attacker, defender, fire, air, water, earth, 0.1);
    if ((cre_collection[attacker]['bloodfrenzy']) && (magic[defender]['fd1'])) {
        PhysicalModifiers *= 1.3;
    };
    UmelkaModifiers = 1;

    if ((umelka[cre_collection[attacker]['owner']][0] > 0) && (umelka[cre_collection[defender]['owner']][0] > 0)) {
        k = umelka[cre_collection[attacker]['owner']][0];
        if ((k > 0) && (k < 11)) {
            let j = umelka[cre_collection[defender]['owner']][k];
            UmelkaModifiers = 1 - j * 0.03;
        };
    };
    NumCreatures = cre_collection[attacker]['nownumber'];
    let tsc = 0;

    bigx = cre_collection[defender]['big'];
    bigy = cre_collection[defender]['big'];
    if (cre_collection[defender]['bigx']) bigx = 1;
    if (cre_collection[defender]['bigy']) bigy = 1;
    for (var xs = cre_collection[defender]['x'] - 1; xs <= cre_collection[defender]['x'] + 1 + bigx; xs++) {
        for (var ys = cre_collection[defender]['y'] - 1; ys <= cre_collection[defender]['y'] + 1 + bigy; ys++) {
            if ((mapobj[xs + ys * defxn] > 0) && (mapobj[xs + ys * defxn] != defender) && (cre_collection[mapobj[xs + ys * defxn]]['shieldguard']) && (cre_collection[defender]['side'] == cre_collection[mapobj[xs + ys * defxn]]['side'])) {
                tsc++;
            };
        };
    };


    PhysicalModifiers /= (tsc + 1);

    var minmag = 0;
    var maxmag = 0;
    if ((inuse == 'lep') && (cre_collection[attacker]['crashingleap'])) {
        Totalmagicdamage = 0;
        cre_collection[defender]['attacked'] = 1;
        stage.pole.attackmagic(attacker, defender, cre_collection[attacker]['nownumber'] * 4, 'cold', '', 0, 0, 0);
        minmag = Totalmagicdamage;
        Totalmagicdamage = 0;
        cre_collection[defender]['attacked'] = 1;
        stage.pole.attackmagic(attacker, defender, cre_collection[attacker]['nownumber'] * 6, 'cold', '', 0, 0, 0);
        maxmag = Totalmagicdamage;
    };

    mindam = cre_collection[attacker]['mindam'] + cre_collection[attacker]['damageaddon'] + (cre_collection[attacker]['maxdam'] - cre_collection[attacker]['mindam']) * (cre_collection[attacker]['mindamaddon']) + cre_collection[attacker]['ragedamage'];
    maxdam = cre_collection[attacker]['maxdam'] + cre_collection[attacker]['damageaddon'] - (cre_collection[attacker]['maxdam'] - cre_collection[attacker]['mindam']) * (cre_collection[attacker]['maxdamaddon']) + cre_collection[attacker]['ragedamage'];
    h = hera;
    if ((h > 0) && (magic[h]) && (magic[h]['BLS']) && (magic[h]['BLS']['effect'] > 0)) mindam = maxdam;
    if ((h > 0) && (magic[h]) && (magic[h]['CRS']) && (magic[h]['CRS']['effect'] > 0)) maxdam = mindam;
    if ((cre_collection[attacker]['taran']) && (cre_collection[defender]['stone'])) {
        h = hera;
        mindam = Math.floor(Math.pow(cre_collection[h]['maxhealth'], 0.5) * 200 * cre_collection[attacker]['mindam']);
        maxdam = Math.floor(Math.pow(cre_collection[h]['maxhealth'], 0.5) * 400 * cre_collection[attacker]['maxdam']);
    };
    if (cre_collection[attacker]['accuracy']) mindam = maxdam;
    BaseDamage = mindam;
    PhysicalDamage = NumCreatures * BaseDamage * AttackDefenseModifier * PhysicalModifiers * UmelkaModifiers + minmag;
    PhysicalDamage2 = NumCreatures * maxdam * AttackDefenseModifier * PhysicalModifiers * UmelkaModifiers + maxmag;
    if ((cre_collection[attacker]['deathstrike']) && (cre_collection[defender]['maxhealth'] < 400) && (!cre_collection[defender]['stone'])) {
        if ((cre_collection[defender]['nownumber'] - 1) * cre_collection[defender]['maxhealth'] + cre_collection[defender]['nowhealth'] > PhysicalDamage) {
            PhysicalDamage += cre_collection[defender]['maxhealth'] - PhysicalDamage % cre_collection[defender]['maxhealth'];
        };
        if ((cre_collection[defender]['nownumber'] - 1) * cre_collection[defender]['maxhealth'] + cre_collection[defender]['nowhealth'] > PhysicalDamage2) {
            PhysicalDamage2 += cre_collection[defender]['maxhealth'] - PhysicalDamage2 % cre_collection[defender]['maxhealth'];
        };
    };

    if (cre_collection[attacker]['bladeofslaughter']) {
        PhysicalDamage += Math.min(500, cre_collection[defender]['nownumber'] * 2);
        PhysicalDamage2 += Math.min(500, cre_collection[defender]['nownumber'] * 2);
    };
    if (magic[attacker]['brk']) {
        PhysicalDamage *= (1 + magic[attacker]['brk']['effect'] * 0.03);
        PhysicalDamage2 *= (1 + magic[attacker]['brk']['effect'] * 0.03);
    };
    if (PhysicalDamage < 1) {
        PhysicalDamage = 1;
    };
    if (PhysicalDamage2 < 1) {
        PhysicalDamage2 = 1;
    };
    if ((cre_collection[attacker]['magicattack']) && (l > 2) && (stage.pole.issomething(defender, 'dampenmagic'))) PhysicalDamage = 0;
    if (magic[defender]['rag']) {
        PhysicalDamage = stage.pole.ragedamage(defender, PhysicalDamage);
        PhysicalDamage2 = stage.pole.ragedamage(defender, PhysicalDamage2);
    };
    if ((cre_collection[attacker]['vorpalsword']) && (cre_collection[defender]['maxhealth'] < 400) && (!cre_collection[defender]['stone'])) {
        PhysicalDamage += cre_collection[defender]['maxhealth'];
        PhysicalDamage2 += cre_collection[defender]['maxhealth'];
    };

    PhysicalDamage = Math.round(PhysicalDamage);
    PhysicalDamage2 = Math.round(PhysicalDamage2);
    if (cre_collection[defender]['pleasureinpain']) {
        PhysicalDamage = Math.round(PhysicalDamage * 0.9);
        PhysicalDamage2 = Math.round(PhysicalDamage2 * 0.9);
    };
    if (cre_collection[defender]['raptureinagony']) {
        PhysicalDamage = Math.round(PhysicalDamage * 0.8);
        PhysicalDamage2 = Math.round(PhysicalDamage2 * 0.8);
    };
    var totalh = (cre_collection[defender]['nownumber'] - 1) * cre_collection[defender]['maxhealth'] + cre_collection[defender]['nowhealth'];
    Uronkills = Math.floor(Math.min(PhysicalDamage, totalh) / cre_collection[defender]['maxhealth']);
    Uronkills2 = Math.floor(Math.min(PhysicalDamage2, totalh) / cre_collection[defender]['maxhealth']);
    var nowhealth = cre_collection[defender]['nowhealth'] - (Math.min(PhysicalDamage, totalh) - Uronkills * cre_collection[defender]['maxhealth']);
    var nowhealth2 = cre_collection[defender]['nowhealth'] - (Math.min(PhysicalDamage2, totalh) - Uronkills2 * cre_collection[defender]['maxhealth']);
    if (nowhealth <= 0) Uronkills++;
    if (nowhealth2 <= 0) Uronkills2++;
    tUronkills += Uronkills;
    tUronkills2 += Uronkills2;
    tPhysicalDamage += PhysicalDamage;
    tPhysicalDamage2 += PhysicalDamage2;
    if (![0, 1].includes(leap_distance)) cre_collection[attacker].attackaddon -= leap_atk_bonus;
    let leap_display_distance = ""
    if (leap_atk_bonus) leap_display_distance = cre_distance ? "" : leap_distance;
    return { distance: leap_display_distance, leap_atk_bonus: leap_atk_bonus }
}

function get_dmg_info(attacker_obj_index, defender_obj_index, koef = 1) {
    let cre_collection = stage.pole.obj
    let attacker = cre_collection[attacker_obj_index]
    let defender = cre_collection[defender_obj_index]
    let dmg_dict = attackmonster(attacker_obj_index, attacker.x, attacker.y, defender.x, defender.y, defender_obj_index, GM_getValue("cre_distance"), 1, koef);
    let min_damage = PhysicalDamage
    let max_damage = PhysicalDamage2
    let min_killed, max_killed;
    if (min_damage % defender.maxhealth >= defender.nowhealth) min_killed = Math.floor(min_damage / defender.maxhealth) + 1
    else min_killed = Math.floor(min_damage / defender.maxhealth)
    if (max_damage % defender.maxhealth >= defender.nowhealth) max_killed = Math.floor(max_damage / defender.maxhealth) + 1
    else max_killed = Math.floor(max_damage / defender.maxhealth)
    return { min: min_damage, max: max_damage, min_killed: min_killed, max_killed: max_killed, distance: dmg_dict.distance }
}

let defender_obj_id = 0
let selected_id = 0

function refresh() {
    isOpen = true;
    let cre_collection = stage.pole.obj;

    if (cre_distance_on) {
        cre_distance_div.style.display = "inline";
        cre_distance_div.innerHTML = `<span>${t('chosenDistance')}: ${GM_getValue('cre_distance')}</span><br>`;
    }

    set_Display([select, side_button, collapse_button, document.querySelector("#chosen_cre_heading"), dmg_list_container, individual_calc], "inline");

    refresh_button.innerHTML = "🔄";

    let cre_list = Object.values(cre_collection);
    cre_list.sort((a, b) => a.obj_index - b.obj_index);

    dmg_list_container.innerHTML = "";
    [...select.children].forEach(child => child.remove());

    let found_defender = false;

    cre_list.forEach(defender => {
        if (defender.nownumber > 0 && defender.nametxt != "" && defender.side == chosen.side && defender.hero === undefined) {
            let option_id = `cre_no${cre_list.indexOf(defender)}`;
            select.insertAdjacentHTML("beforeend",
                                      `<option id="${option_id}" value="${defender.obj_index}">
                    ${defender.nametxt} [${defender.nownumber}]
                </option>`
                                     );

            if (!found_defender) {
                if (`${defender.obj_index}` == chosen.creature) found_defender = true;
                defender_obj_id = defender.obj_index;
                selected_id = [...select.children].indexOf(select.lastChild);
            }
        }
    });

    dmg_list_container.insertAdjacentHTML("beforeend", `
        <div id="chosen_cre_heading" style="display:inline; background-color: ${physCalcColor}">
            <span>${t("damageTo")} </span>
            <span style="color:#ffffff; font-size: 110%; font-weight: bold;">
                ${cre_collection[defender_obj_id].nametxt} [${cre_collection[defender_obj_id].nownumber}] :
            </span>
        </div>
    `);

    /* ===========================
       COLLECT + CALCULATE
    ============================ */

    let attackerRows = [];

    cre_list.forEach(attacker => {
        if (attacker.side == -chosen.side && attacker.nownumber > 0 && attacker.nametxt != "") {
            let dmg = get_dmg_info(attacker.obj_index, defender_obj_id);
            let practical_overall_hp;

            if (cre_collection[defender_obj_id].attack > attacker.defence) {
                practical_overall_hp = attacker.maxhealth * attacker.nownumber /
                    (1 + 0.05 * Math.abs(cre_collection[defender_obj_id].attack - attacker.defence));
            } else {
                practical_overall_hp = attacker.maxhealth * attacker.nownumber *
                    (1 + 0.05 * Math.abs(cre_collection[defender_obj_id].attack - attacker.defence));
            }

            let coef = evalStrength(attacker, cre_collection[defender_obj_id]).koef;

            attackerRows.push({ attacker, dmg, coef });
        }
    });


    attackerRows.sort((a, b) => b.coef - a.coef);
    const color1 = "#1a1a1a"; // dark row
    const color2 = "#262626"; // slightly lighter row

    attackerRows.forEach((row, index) => {
        let { attacker, dmg, coef } = row;

        let row_id = `row_no${index}`;
        let koef_string = `<span title="коэф. урона">⚖️</span><b style="color:white">${coef.toFixed(2)}</b>`;

        // pick alternating background color
        let bgColor = index % 2 === 0 ? color1 : color2;

        dmg_list_container.insertAdjacentHTML("beforeend", `
            <p id="${row_id}" style="color:white; background-color:${bgColor}; padding:2px 5px; margin:0;">
                <span style="text-decoration: underline;color:#bfbfbf">${attacker.nametxt}</span>
                [${attacker.nownumber}] |
                ${icons.dead.html}<b style="color:#bfbfbf">${dmg.min_killed}-${dmg.max_killed}</b>
                 ${icons.damage.html}${dmg.min}-${dmg.max}
                ${!attacker.hero ? koef_string : ""}
            </p>
        `);

        dmg_list_container.insertAdjacentHTML("beforeend",
                                              calcHellFireHTML(attacker, cre_collection[defender_obj_id], cre_collection, dmg)
                                             );

        dmg_list_container.insertAdjacentHTML("beforeend",
                                              calcStormHTML(attacker, cre_collection[defender_obj_id])
                                             );

            dmg_list_container.insertAdjacentHTML("beforeend",
                                                  calcMagicHTML(attacker, cre_collection[defender_obj_id], cre_collection, dmg)
                                                 );
    });

    select.options.item(selected_id).selected = true;
}
function openHwmkbOverlay() {
    // Don't inject twice
    if (document.getElementById("hwmkb_overlay_host")) return;

    // Host fixed overlay (in real DOM)
    const host = document.createElement("div");
    host.id = "hwmkb_overlay_host";
    host.style.cssText = "position:fixed;left:16px;top:16px;z-index:2147483647;";
    document.body.appendChild(host);

    // Shadow root = isolation from page CSS + no ID collisions
    const shadow = host.attachShadow({ mode: "open" });

    shadow.innerHTML = `
    <style>
      :host { all: initial; }
      * { box-sizing: border-box; font-family: Arial, sans-serif; }

      /* backdrop */
      #backdrop{
        position: fixed; inset: 0;
        background: rgba(0,0,0,0.25);
      }

      /* panel */
      #panel{
        position: fixed;
        left: 16px; top: 16px;
        max-width: 720px;
        padding: 14px;
        color: #fff;
        background: #2b2b2b;
        border: 1px solid rgba(255,255,255,0.15);
        border-radius: 10px;
      }

      #header{
        display:flex; align-items:center; justify-content:space-between;
        gap: 10px;
        margin-bottom: 8px;
        user-select: none;
        cursor: move;
      }
        #closeBtn{
        margin-left: auto;
        cursor:pointer;
        border:1px solid rgba(255,255,255,0.25);
        background:rgba(0,0,0,0.25);
        color:#fff;
        border-radius:6px;
        padding:2px 8px;
        }

      .sep{ height:1px; background:rgba(255,255,255,0.18); margin:10px 0; }
      .row{ margin: 6px 0; }

      /* checkbox */
      .checkbox_container{
        display: inline-flex;
        align-items: center;
        gap: 10px;
        cursor: pointer;
        user-select:none;
        position: relative;
        padding-left: 30px;
        line-height: 20px;
      }
      .checkbox_container input{
        position:absolute;
        opacity:0;
        cursor:pointer;
        height:0;
        width:0;
      }
      .checkbox_checkmark{
        position:absolute;
        left:0;
        top:50%;
        transform: translateY(-50%);
        height:18px;
        width:18px;
        border-radius:4px;
        border:1px solid rgba(255,255,255,0.25);
        background: rgba(0,0,0,0.25);
      }
      .checkbox_container input:checked ~ .checkbox_checkmark:after{
        content:"";
        position:absolute;
        left:6px;
        top:2px;
        width:5px;
        height:10px;
        border: solid rgba(255,215,0,0.95);
        border-width:0 2px 2px 0;
        transform: rotate(45deg);
      }

      /* keybind layout */
      .kb_row { display:flex; align-items:center; justify-content:space-between; gap:10px; margin: 6px 0; }
      .kb_left { flex:1 1 auto; }
      .kb_btn{
        flex:0 0 auto;
        min-width: 150px;
        text-align: center;
        padding: 4px 10px;
        border-radius: 6px;
        border: 1px solid rgba(255,255,255,0.25);
        background: rgba(0,0,0,0.25);
        color: #fff;
        cursor: pointer;
        user-select: none;
      }
      .kb_btn.kb_listen { outline: 2px solid rgba(255,215,0,0.6); }
      .kb_btn.kb_warn { outline: 2px solid rgba(255,60,60,0.85); border-color: rgba(255,60,60,0.85); }

      /* toast */
      #toast{
        position: fixed;
        left: 50%;
        bottom: 18px;
        transform: translateX(-50%);
        background: rgba(0,0,0,0.8);
        border: 1px solid rgba(255,255,255,0.2);
        color: #fff;
        padding: 8px 12px;
        border-radius: 8px;
        opacity: 0;
        pointer-events: none;
        transition: opacity 160ms ease;
        font-size: 14px;
      }
      #toast.show{ opacity: 1; }
      .hint{ opacity:0.75; font-size: 90%; margin: 4px 0 8px; }
    </style>

    <div id="backdrop"></div>

    <div id="panel">
      <div id="header">
        <button id="closeBtn" type="button">✕</button>
      </div>
      <div class="hint" id="kb_hint"></div>
      <div class="kb_row">
        <div class="kb_left" id="lbl_damage"></div>
        <div class="kb_btn" id="kb_seeDamage_btn" data-kb="kb_seeDamage"></div>
      </div>
      <div class="kb_row">
        <div class="kb_left" id="lbl_filterLog"></div>
        <div class="kb_btn" id="kb_filterLog_btn" data-kb="kb_filterLog"></div>
      </div>

      <div class="kb_row">
        <div class="kb_left" id="lbl_magshot"></div>
        <div class="kb_btn" id="kb_seeMagShot_btn" data-kb="kb_seeMagShot"></div>
      </div>
      <div class="row">
        <label class="checkbox_container">
          <span id="kb_useTrigger_text"></span>
          <input type="checkbox" id="kb_useTrigger_checkbox" checked="true">
          <span class="checkbox_checkmark"></span>
        </label>
      </div>
      <div class="kb_row">
        <div class="kb_left" id="lbl_trigger"></div>
        <div class="kb_btn" id="kb_triggerKey_btn" data-kb="kb_triggerKey"></div>
      </div>
      <div class="kb_row">
        <div class="kb_left" id="lbl_autobattle"></div>
        <div class="kb_btn" id="kb_autoBattle_btn" data-kb="kb_autoBattle"></div>
      </div>

      <div class="kb_row">
        <div class="kb_left" id="lbl_togglespeed"></div>
        <div class="kb_btn" id="kb_toggleSpeed_btn" data-kb="kb_toggleSpeed"></div>
      </div>

      <div class="kb_row">
        <div class="kb_left" id="lbl_autoplacement"></div>
        <div class="kb_btn" id="kb_autoPlacement_btn" data-kb="kb_autoPlacement"></div>
      </div>

      <div class="kb_row">
        <div class="kb_left" id="lbl_back"></div>
        <div class="kb_btn" id="kb_backToGame_btn" data-kb="kb_backToGame"></div>
      </div>

      <div class="kb_row">
        <div class="kb_left" id="lbl_startbattle"></div>
        <div class="kb_btn" id="kb_startBattle_btn" data-kb="kb_startBattle"></div>
      </div>
    </div>

    <div id="toast"></div>
  `;

    // ---- scoped helpers ----
    const $ = (sel) => shadow.querySelector(sel);

    const backdrop = $("#backdrop");
    const panel = $("#panel");
    const header = $("#header");
    const closeBtn = $("#closeBtn");
    const toast = $("#toast");

    function close() {
        host.remove();
    }
    closeBtn.addEventListener("click", close);
    backdrop.addEventListener("click", close);

    // Drag panel (fixed)
    (function draggable(){
        let dragging = false, sx = 0, sy = 0, sl = 16, st = 16;

        header.addEventListener("mousedown", (e) => {
            if (e.button !== 0) return;
            dragging = true;
            sx = e.clientX; sy = e.clientY;
            sl = parseInt(panel.style.left || "16", 10);
            st = parseInt(panel.style.top || "16", 10);
            e.preventDefault();
        });

        document.addEventListener("mousemove", (e) => {
            if (!dragging) return;
            panel.style.left = (sl + (e.clientX - sx)) + "px";
            panel.style.top  = (st + (e.clientY - sy)) + "px";
        }, true);

        document.addEventListener("mouseup", () => { dragging = false; }, true);
    })();

    function showToast(msg){
        toast.textContent = msg;
        toast.classList.add("show");
        clearTimeout(showToast._t);
        showToast._t = setTimeout(() => toast.classList.remove("show"), 900);
    }

    // ---- I18N (for demo). In your real script: use your existing I18N+t() ----

    // Populate labels (THIS is where you were failing before due to ID collisions / CSS overrides)
    $("#kb_hint").textContent = t("keybindHint");
    $("#kb_useTrigger_text").textContent = t("keybindUseTriggerLabel");
    $("#lbl_trigger").textContent = t("keybindTriggerLabel");
    $("#lbl_damage").textContent = t("keybindSeeDamageLabel");
    $("#lbl_magshot").textContent = t("keybindSeeMagShotLabel");
    $("#lbl_autobattle").textContent = t("keybindAutoBattleLabel");
    $("#lbl_togglespeed").textContent = t("keybindToggleSpeedLabel");
    $("#lbl_autoplacement").textContent = t("keybindAutoPlacementLabel");
    $("#lbl_back").textContent = t("keybindBackToGameLabel");
    $("#lbl_startbattle").textContent = t("keybindStartBattleLabel");
    $("#lbl_filterLog").textContent = t("keybindFilterLog");
    $("#kb_useTrigger_checkbox").checked = kb.useTrigger;
    const setBool = (key, val) => GM_setValue(key, String(!!val));

    function prettyKeyNameFromCode(code){
        if (!Number.isFinite(code)) return t("keybindUnassigned");
        const specials = { 18:"Alt", 17:"Ctrl", 16:"Shift", 32:"Space", 13:"Enter", 27:"Escape",
                          37:"ArrowLeft", 38:"ArrowUp", 39:"ArrowRight", 40:"ArrowDown" };
        if (specials[code]) return specials[code];
        if (code >= 48 && code <= 57) return String.fromCharCode(code);
        if (code >= 65 && code <= 90) return String.fromCharCode(code);
        if (code >= 112 && code <= 123) return "F" + (code - 111);
        return "Code " + code;
    }

    function bindMap(){
        return {
            kb_triggerKey: kb.triggerKey,
            kb_seeDamage: kb.seeDamage,
            kb_seeMagShot: kb.seeMagShot,
            kb_autoBattle: kb.autoBattle,
            kb_toggleSpeed: kb.toggleSpeed,
            kb_autoPlacement: kb.autoPlacement,
            kb_backToGame: kb.backToGame,
            kb_startBattle: kb.startBattle,
            kb_filterLog: kb.filterLog,
        };
    }

    function setBindAndSync(key, code){
        setBindCode(key, Number.isFinite(code) ? code : null);
        kb.triggerKey     = getBindCode("kb_triggerKey", kb.triggerKey);
        kb.seeDamage      = getBindCode("kb_seeDamage", kb.seeDamage);
        kb.seeMagShot     = getBindCode("kb_seeMagShot", kb.seeMagShot);
        kb.autoBattle     = getBindCode("kb_autoBattle", kb.autoBattle);
        kb.toggleSpeed    = getBindCode("kb_toggleSpeed", kb.toggleSpeed);
        kb.autoPlacement  = getBindCode("kb_autoPlacement", kb.autoPlacement);
        kb.backToGame     = getBindCode("kb_backToGame", kb.backToGame);
        kb.startBattle    = getBindCode("kb_startBattle", kb.startBattle);
        kb.useTrigger     = getBool("kb_useTrigger", kb.useTrigger);
        kb.filterLog      = getBindCode("kb_filterLog", kb.filterLog);
    }

    function refreshUI(){
        const map = bindMap();
        for (const [k, v] of Object.entries(map)){
            const btn = shadow.getElementById(k + "_btn");
            if (btn) btn.textContent = prettyKeyNameFromCode(v);
        }
        shadow.getElementById("kb_useTrigger_checkbox").checked = !!kb.useTrigger;
    }

    function flashRed(key){
        const btn = shadow.getElementById(key + "_btn");
        if (!btn) return;
        btn.classList.add("kb_warn");
        setTimeout(() => btn.classList.remove("kb_warn"), 700);
    }

    function enforceUniqueness(newKey, code){
        const map = bindMap();
        for (const [k, v] of Object.entries(map)){
            if (k !== newKey && v === code) {
                setBindAndSync(k, NaN);
                flashRed(k);
                break;
            }
        }
    }

    // Record behavior (scoped to panel)
    let listeningBtn = null;

    function stopListening(restore){
        if (listeningBtn) {
            listeningBtn.classList.remove("kb_listen");
            if (restore) refreshUI();
        }
        listeningBtn = null;
    }

    panel.addEventListener("click", (e) => {
        const btn = e.target && e.target.classList && e.target.classList.contains("kb_btn") ? e.target : null;
        if (!btn) return;

        if (listeningBtn === btn) {
            stopListening(true);
            return;
        }
        stopListening(true);

        listeningBtn = btn;
        listeningBtn.classList.add("kb_listen");
        listeningBtn.textContent = "…";
    }, true);

    document.addEventListener("keydown", (e) => {
        if (!listeningBtn || isChatFocused()) return;

        // Don't steal typing (optional)
        const tag = (e.target && e.target.tagName) ? e.target.tagName.toLowerCase() : "";
        if (tag === "input" || tag === "textarea") return;

        e.preventDefault();
        e.stopPropagation();

        const code = Number(e.keyCode);
        if (!Number.isFinite(code) || code === 0) {
            showToast(t("keybindInvalid"));
            stopListening(true);
            return;
        }

        const newKey = listeningBtn.dataset.kb;
        enforceUniqueness(newKey, code);
        setBindAndSync(newKey, code);
        stopListening(true);
    }, true);

    shadow.getElementById("kb_useTrigger_checkbox").addEventListener("change", (e) => {
        kb.useTrigger = !!e.target.checked;

        setBool("kb_useTrigger", kb.useTrigger);
    });

    refreshUI();
}
initGates();