diff --git a/HentaiVerse/hvAutoAttack/README.md b/HentaiVerse/hvAutoAttack/README.md index df72b5e5..5c376cb7 100644 --- a/HentaiVerse/hvAutoAttack/README.md +++ b/HentaiVerse/hvAutoAttack/README.md @@ -50,9 +50,10 @@ *** ### 自定义判断条件 - 每一个拥有红色虚线边框的区域,都可以设置自定义判断条件。 +现已支持自定义的条件公式,例如`hp > mp` 或 `2 * ( hp + mp ) > sp`,支持运算符: `+` `-` `*` `/` `%` `**`(幂) `^`(异或) `~`(Log10) `&&` `||` `!` `>` `<` `>=`(`≥`) `<=`(`≤`) `==`(`=`,`===`) `!=`(`≠`,`~=`,`<>`),逻辑运算符返回`0`或`1` (表示false或true) + * 注意:如果这些区域留空(一个条件也没设置),那么就相当于真。 当鼠标在这些区域内移动时,右上角会显示一个盒子(当鼠标不在这些区域内,盒子消失) @@ -63,32 +64,77 @@ * 下拉列表2/4: 比较值A/比较值B -* 下拉列表3: 只支持比较运算符(`1`:大于, `2`:小于, `3`: 大于等于, `4`: 小于等于, `5`:等于, `6`:不等于) +* 下拉列表3: 运算符 -* ADD按钮: 生成一个值为`比较值A,比较值,比较值B`的输入框 +* ADD按钮: 生成一个值为 `比较值A 运算符 比较值B` 的输入框 + +* 仍兼容旧版本 `比较值A,比较运算符,比较值B` 的条件,其中比较运算符:(`1`:大于, `2`:小于, `3`: 大于等于, `4`: 小于等于, `5`:等于, `6`:不等于) #### 比较值 -1. `hp`/`mp`/`sp`: hp/mp/sp的*百分比 (percent)* -2. `oc`: Overcharge, 250==>250% -3. `monsterAll`/`monsterAlive`/`bossAll`/`bossAlive`: 怪兽/Boss的总数目/存活数目 +1. `hp`/`mp`/`sp`: hp/mp/sp的*百分比的整数形式 (percent)*;`_hpDecimal`/`_mpDecimal`/`_spDecimal`: hp/mp/sp的*百分比的小数形式 (percent decimal)* +2. `oc`: Overcharge, 250==>250% *百分比的整数形式 (percent)*;`_ocDecimal`: oc的*百分比的小数形式 (percent decimal)* +3. `monsterAll`/`monsterAlive`/`bossAll`/`bossAlive`: 怪兽/Boss的总数目/存活数目,boss只按照有序号背景色的。包含`龙、大树、额外游戏内容、小马`的存活boss数可改为使用`_targetBossType_count`(见后11.3和12.) 4. `roundNow`/`roundAll`/`roundLeft`: 当前回合数/总回合数/剩余回合数 -5. `roundType`: 战役模式 (`ar`: The Arena, `rb`: Ring of Blood, `gr`: GrindFest, `iw`: Item World, `ba`: Random Encounter) - - **注意**: 由于是字符串之间的比较,所以请加上引号,如"ar"/'ar' - -6. `attackStatus`: 攻击模式 (`0`: Physical, `1`: Fire, `2`: Cold, `3`: Elec, `4`: Wind, `5`: Divine, `6`: Forbidden) -7. `isCd`: 技能/物品是否cd,格式`_isCd_id` - - **示例1**: Protection的id为411,则`_isCd_411,5,0`表示不可施放,`_isCd_411,5,1`表示可以施放 - - **示例2**: ManaElixir的id为11295,则`_isCd_11295,5,0`表示不可使用,`_isCd_11295,5,1`表示可以使用 - -8. `buffTurn`: 人物Buff剩余时间,格式`_buffTurn_img` - - **示例**: Protection的img为protection,则`_buffTurn_protection,5,0`表示不存在Protection的buff,`_buffTurn_protection,3,10`表示Protection的buff至少剩余10回合 - -9. 空白(blank): 自己输入 (the value you want to put in) +5. `isRoundType`、`ar`、`ba`、`iw`、`tw`、`gr`、`rb`: 当前是否是某战役模式,例如`_isRoundType_ar`或`_ar`均返回 `当前是否是The Arena` +6. `roundType`: 战役模式 (`ar`: The Arena, `rb`: Ring of Blood, `gr`: GrindFest, `iw`: Item World, `ba`: Random Encounter, `tw`: The Tower) + + **注意**: 字符串之间的比较会自动去除最外层的引号,如`"ar"`/`'ar'`均和`ar`一致 + +7. `attackStatus`: 攻击模式 (`0`: Physical, `1`: Fire, `2`: Cold, `3`: Elec, `4`: Wind, `5`: Divine, `6`: Forbidden)。 或使用 `_phys`, `_fire`, `_cold`, `_elec`, `_wind`, `_divi`, `_forb` 表示 `当前 attack mode 是否为 ...`,例如 `_phys` 等价于 `attackStatus == 0`。 + - 获取到的是默认攻击模式,可加后缀`Cur`来表示判断次要模式后的攻击模式(直接获取当前`attackStatus`需要加`_`前缀),即:`_attackStatusCur`,`_physCur`, `_fireCur`, `_coldCur`, `_elecCur`, `_windCur`, `_diviCur`, `_forbCur` + - 具体流程: + - 尝试次要模式攻击的流程中,会直接用尝试中的模式 + - 如果在`次要模式`的条件中配置了这些条件,都会按照`条件所在模式`来获取值,例如:切换火属性的条件中配置了`_fireCur`,会直接判定为`true`,配置了`_windCur`则是会判定为`false` + - 其他地方则会模拟一遍攻击,然后判断能放出来的时候 给`临时变量`赋值`模拟尝试成功的模式`,然后返回临时变量 +8. `fightingStyle`: 战斗风格 (`1`: 二天, `2`: 单手, `3`: 双手, `4`: 双持, `5`: 法杖)。 或使用 `_nt`, `_1h`, `_2h`, `_dw`, `_staff` 表示 `当前 fighting style 是否为 ...`,例如 `_nt` 等价于 `fightStyle == 1` +9. `isCd`: 技能/物品是否cd,格式`_isCd_id` + + **示例1**: Protection的id为411,则`!_isCd_411`表示不可施放,`_isCd_411`表示可以施放 + + **示例2**: ManaElixir的id为11295,则`!_isCd_11295`表示不可使用,`_isCd_11295`表示可以使用 + +10. `buffStack`、`buffTurn`: 人物Buff层数/剩余时间,格式`_buffTurn_img`。可使用`_scroll`限定为卷轴buff`_buffTurn_sparklife_scroll`、使用`_png`限定为非卷轴buff`_buffTurn_sparklife_png`. 可用 `{buffA,buffB,...}` 表示获取 buffA **或** buffB (**或** 括号 `{ }` 中的任意其他buff,将返回匹配成功的第一个buff),注意内部不要包含空格,例如`_buffTurn_{stun,sleep}`. + + **示例**: Protection的img为protection,则`_buffTurn_protection == 0`表示不存在Protection的buff,`_buffTurn_protection >= 10`表示Protection的buff至少剩余10回合 + +11. `_targetHp`、`_targetMp`、`_targetSp`、`_targetHpDecimal`、`_targetMpDecimal`、`_targetSpDecimal`、`_targetBuffStack`、`_targetBuffTurn`、`_targetRank`、`_targetOrder`、`_targetWeight`、`_targetIsAlive`: 目标怪物的HP%、SP%、MP%、HP%(小数形式)、SP%(小数形式)、MP%(小数形式)、buff剩余时间、优先级、位置、当前权重、是否存活 + 1. `_targetBuffStack`、`_targetBuffTurn_`后缀参照10.`buffStack`、`buffTurn`(如:`_targetBuffTurn_bleed != 0`表示目标bleed的buff剩余回合不等于0)。target的目标怪物遵循以下规则 + 1. 默认情况的target均为权重优先级最高的目标 + 2. 武器技能(马炮、T1~T3等)、法术技能(中阶、高阶):按照 逐条条件判断>按权重逐个目标>满足任意一条条件内的所有子条目,则对该目标释放。例如下图最后的慈悲的条件:仅释放hp小于25%、拥有流血buff + ![示例](https://github.com/user-attachments/assets/b4d0c57d-fdb1-464b-88d6-107643809339) + + 2. `_targetRank`和攻击规则给出的顺序值相同(0~9,数字越小,优先级越高) + 3. `targetBossType(见12.)`和除了`targetName`的其他 目标参数可使用`max/min/sum/count`后缀来表示所有**存活**怪物中的最大/最小值/总和/sign和(`sum(sign(value))`),如:`_targetBuffTurn_max_bleed`. + 1. `max/min/sum/count`可加前缀`a`/`ag`or`ga`/`g`表示按照`包含死亡目标(仍是全体目标)`/`分组内统计且包含死亡目标`/`分组内统计(仍忽略死亡目标)`. 分组相关见(13. `targetGroup`) +12. `targetName`、`targetBossType`: 目标怪物的名称、Boss类型。 + 1. `_targetName`返回目标的名称字符串(**注意**: 字符串之间的比较会自动去除最外层的引号,且请使用下划线`_`代替空格` `,如`Yugi_Nagato`/`'Yugi_Nagato'`/`"Yugi_Nagato"`) + 2. 其中类Boss型`_targetBossType`根据 名称进行判断: + 1. `Manbearpig`、`White Bunneh`、`Mithra`、`Dalek`: 1 (BOSS) + 2. `Konata`、`Mikuru Asahina`、`Ryouko Asakura`、`Yuki Nagato`: 2 (Legendaries) + 3. `Real Life`、`Invisible Pink Unicorn`、`Flying Spaghetti Monster`: 3 (Gods) + 4. `Rhaegal`、`Viserion`、`Drogon`: 4 (A Dance with Dragons) + 5. `Skuld`、`Urd`、`Verdandi`、`Yggdrasil`: 5 (Trio and the Tree) + 6. `Recycled Boss Rush`、`Bottomless Dungeon`、`New Game +`、`Achievement Grind`、`Time Trial Mode`、`Hardcore Mode`:6 (Post Game Content) + 7. `Fluttershy`、`Gummy`、`Rainbow Dash`、`Twilight Sparkle`、`Rarity`、`Applejack`、`Pinkie Pie`、`Angel Bunny`、`Spike`:7 (Ponies) + 8. 其他: 0 (非boss) +13. `targetGroup`: 获取分组内的目标个数(包含死亡),并更改当前公式内的`目标分组`(见11.3.1). 每次公式计算的初始目标分组为`null`的空分组. 可选模式如下: + 1. `a`: 选择全体. `targetGroup_a` + 2. `s`: 按照给定数量从上往下划分,然后选取包含当前目标的一组 `targetGroup_s_2` 相当于每2个1组 + 3. `r`: 按照给定的距离,以当前目标为起点划分 + 1. 只有一个参数时,为对称范围. `targetGroup_r_1` 相当于`当前目标 ± 1`(共3格) + 2. 两个参数时,分别为 `targetGroup_r_向上_向下`. `targetGroup_r_1_2` 相当于当前 \[`目标 - 1`, `目标 + 2`\](共4格) + 3. `距离 >= 10`时,溢出为 `9 - 距离`,例如:`targetGroup_r_4_10` 相当于 \[`目标 - 4`, `目标 - 1`\] (共4格) + 4. 参数缺省=`-1`/`10`: `targetGroup_r_` 相当于 `targetGroup_r_10`;`targetGroup_r_1_` 相当于 `targetGroup_r_1_10`; `targetGroup_r__1` 相当于 `targetGroup_r_10_1`. 仅在有2个`_`划分参数时会补充第2个参数 + 5. `o`/`oa`: 按照给定的order(0~9)划定区域. `o`需要当前目标在区域内,否则返回0和`null`的空分组;`oa`不需要当前目标在区域内. `targetGroup_o_1_3` 相当于 `order=[1,2,3]` 即第2,3,4格 + 1. 两个参数分别为上下界,缺省分别为`-1`/`10` +14. `skillOTOS`: 获取对应技能/法术/道具/防御/集中/灵动架势开/关 的**自动使用次数**(不包含手动释放的次数), `skillOTOS_[类型]` 如 `skillOTOS_FRD`(马炮) /`skillOTOS_T1`(武器T1技能) /`skillOTOS_defend`(防御) + - 技能: `OFC`/`FRD`/`T3`/`T2`/`T1` + - 法术/道具: 按 `id` + - 防御/集中/SS开关: `defend`/`focus`/`spiriton`/`spiritoff` +15. 空白(blank): 自己输入 (the value you want to put in) + +PS: 对于需要带下划线`_`开头的func式变量,可以省略`_`开头(省略时,获取值会先尝试按照dict式获取,失败时再按照func式获取) #### 示例 @@ -153,7 +199,7 @@ | 1 | 2 | 3 | | - | - | - | -| - | Regen / regen | - | +| Channeling / channeling | Regen / regen | Focus / focus | | Protection / protection | Haste / haste | Shadow Veil / shadowveil | | Absorb / absorb | Spark of Life / sparklife | Spirit Shield / spiritshield | | Arcane Focus / arcanemeditation | Heartseeker / heartseeker | Cloak of the Fallen / fallenshield | @@ -161,7 +207,17 @@ | Infusion of Flames / fireinfusion | Infusion of Frost / coldinfusion | Infusion of Lightning / elecinfusion | | Infusion of Storms / windinfusion | Infusion of Divinity / holyinfusion | Infusion of Darkness / darkinfusion | | Scroll of Swiftness / haste_scroll | - | - | -| Flower Vase / flowers | Bubble-Gum / gum | - | +| Flower Vase / flowers | Bubble-Gum / gum | Blessing of the RiddleMaster / riddlemaster | +| Sleep / sleep | Blind / blind | Slow / slow | +| Imperil / imperil | MagNet / magnet | Silence / silence | +| Drain / drainhp | Weaken / weaken | Confuse / confuse | +| Coalesced Mana / coalescemana | Stunned / stun / wpn_stun | Penetrated Armor / ap / wpn_ap | +| Bleeding Wound / bleed / wpn_bleed | Absorbing Ward / absorb | Fury of the Sisters / trio_furyofthesisters | +| Lamentations of the Future / trio_skuld | Screams of the Past / trio_urd | Wailings of the Present / trio_verdandi | Searing Skin / firedot | Freezing Limbs / coldslow | +| Freezing Limbs / coldslow | Turbulent Air / windmiss | Deep Burns / elecweak | +| Searing Skin / firedot | - | Breached Defense / holybreach | +Blunted Attack / darknerf | Burning Soul / soulfire | Ripened Soul / ripesoul | +| Ether Tap / wpn_et | - | - | *** @@ -382,3 +438,20 @@ 灵感来自hoverplay,刚开始接触js,初步完成代码 功能有:答题警报、其他警报、快捷键、自动前进、自动使用宝石、自动回复、自动使用增益技能、自动打怪 很可惜,玩游戏不走心,一直搞不懂HVSTAT是怎么知道每个怪的血量的,直到[版本2.0](#20) + + + + + + + + + + + + + + + + + diff --git a/HentaiVerse/hvAutoAttack/README_en.md b/HentaiVerse/hvAutoAttack/README_en.md index c44fef8f..958539c7 100644 --- a/HentaiVerse/hvAutoAttack/README_en.md +++ b/HentaiVerse/hvAutoAttack/README_en.md @@ -49,6 +49,8 @@ Scripts get information through text, and if you have not yet modified the font, Each area with a red dotted border can be set to a customize condition. +Customizable Formula is support now, such as `hp > mp` or `2 * ( hp + mp ) > sp`, supported operators: `+` `-` `*` `/` `%` `**`(pow) `^`(xor) `~`(Log10) `&&` `||` `!` `>` `<` `>=`(`≥`) `<=`(`≤`) `==`(`=`,`===`) `!=`(`≠`,`~=`,`<>`), logical operators returns 0 or 1 (as false or true) + * If these areas are left blank (a condition is not set), then it's equivalent to true. When the mouse moves in these areas, a box is displayed in the upper right corner. (When the mouse out, the box disappears) @@ -59,32 +61,78 @@ Four drop down lists and one button are visible in the box * Drop-down List 2/4: comparison value A / comparison value B -* Drop-down List 3: only support comparison operator (`1`: >, `2`: <, `3`: ≥, `4`: ≤, `5`: =, `6`: ≠) +* Drop-down List 3: operator + +* Button ADD: Generates an input box with a value of `A Operator B` -* Button ADD: Generates an input box with a value of `A,Comparison-Operator,B` +* Legacy version condition such as `A,Comparison-Operator,B` is still supported, Comparison-Operator: (`1`: >, `2`: <, `3`: ≥, `4`: ≤, `5`: =, `6`: ≠) #### Comparison Value -1. `hp`/`mp`/`sp`: **percent** of hp/mp/sp, 0-100 -2. `oc`: Overcharge, 0-250 -3. `monsterAll`/`monsterAlive`/`bossAll`/`bossAlive`: amount of all monster/boss (alive) +1. `hp`/`mp`/`sp`: **percent integer** of hp/mp/sp, 0-100; `_hpDecimal`/`_mpDecimal`/`_spDecimal`: **percent decimal** of hp/mp/sp, 0-1 +2. `oc`: Overcharge, 0-250 (integer); `_ocDecimal`: Overcharge in decimal 0-2.5 +3. `monsterAll`/`monsterAlive`/`bossAll`/`bossAlive`: amount of all monster/boss (alive), only monster with serial letter background is considered as boss. Use `_targetBossType_count` (see 11.3 and 12. below) as the Alive boss count including `dragons, trio and the tree, Post Game Content, Ponies` 4. `roundNow`/`roundAll`/`roundLeft` -5. `roundType`: Battle Type (`ar`: The Arena, `rb`: Ring of Blood, `gr`: GrindFest, `iw`: Item World, `ba`: Random Encounter) - - (**Note**: Because comparison between strings, please add quotation, such as `"ar"`/`'ar'`) - -6. `attackStatus`: Attack Mode (`0`: Physical, `1`: Fire, `2`: Cold, `3`: Elec, `4`: Wind, `5`: Divine, `6`: Forbidden) -7. `isCd`: whether the skill/item is cooldowning, format: `_isCd_id` - - **example 1**: the id of Protection is 411 , `_isCd_411,5,0` means Protection can't be casted or `_isCd_411,5,1` means Protection can be casted - - **example 2**: the id of ManaElixir is 11295, `_isCd_11295,5,0` means ManaElixir can't be used or `_isCd_11295,5,1` means ManaElixir can be used - -8. `buffTurn`: time the buff last in person, format`_buffTurn_img` - - **example**: the image of Protection is protection, `_buffTurn_protection,5,0` means you don't have the buff of Protection or `_buffTurn_protection,3,10` means the the buff of Protection on you last at least 10 turns - -9. blank: the value you want to put in +5. `isRoundType`、`ar`、`ba`、`iw`、`tw`、`gr`、`rb`: is current round type as the target type, such as: both `_isRoundType_ar` and `_ar` returns `is currently in The Arena` +6. `roundType`: Battle Type (`ar`: The Arena, `rb`: Ring of Blood, `gr`: GrindFest, `iw`: Item World, `ba`: Random Encounter, `tw`: The Tower) + + (**Note**: Comparison between strings will automatically remove the outermost quotation marks, such as `"ar"`/`'ar'` is as same as `ar` + +7. `attackStatus`: Attack Mode (`0`: Physical, `1`: Fire, `2`: Cold, `3`: Elec, `4`: Wind, `5`: Divine, `6`: Forbidden). Or use `_phys`, `_fire`, `_cold`, `_elec`, `_wind`, `_divi`, `_forb` as `if current attack mode is ...`, such as `_phys` equals `attackStatus == 0`。 + - Value acquired above is the default Attack Mode, to get current Attack Mode after calculating Secondary Attack Mode, use suffix `Cur` ( and prefix `_` for `attackStatus`), such as:`_attackStatusCur`,`_physCur`, `_fireCur`, `_coldCur`, `_elecCur`, `_windCur`, `_diviCur`, `_forbCur` + - current value getting by: + - During `Secondary Attack`, return `current attempting mode` + - If configed any current condition in `Secondary Attack Mode`, returns `mode that the condition is in`. Such as: In conditions of `Attack mode Fire`, `_fireCur` is as `true`,`_windCur` is as `false` + - In other options, attack will be simulated until the spell is callable ( cache the successfully simulated mode into a temp variable and returns it) +8. `fightingStyle`: Fighting Style (`1`: Niten, `2`: 1H, `3`: 2H, `4`: DW, `5`: Staff). Or use `_nt`, `_1h`, `_2h`, `_dw`, `_staff` as `if current fighting style is ...`, such as `_nt` equals `fightStyle == 1` +9. `isCd`: whether the skill/item is cooldowning, format: `_isCd_id` + + **example 1**: the id of Protection is 411 , `!_isCd_411` means Protection can't be casted or `_isCd_411` means Protection can be casted + + **example 2**: the id of ManaElixir is 11295, `!_isCd_11295` means ManaElixir can't be used or `_isCd_11295` means ManaElixir can be used + +10. `buffStack`/`buffTurn`: buff stack / time the buff last in person, format`_buffTurn_img`. Use suffix `_scroll` for scroll buff only `_buffTurn_sparklife_scroll`, `_png` for not scroll buff only `_buffTurn_sparklife_png`. `{buffA,buffB,...}` means to get buffA **or** buffB (**or** any else in `{ }`, matching the first available one), be aware that no space ` ` should be included in between, such as `_buffTurn_{stun,sleep}`. + + **example 1**: the image of Protection is protection, `_buffTurn_protection == 0` means you don't have the buff of Protection or `_buffTurn_protection >= 10` means the buff of Protection on you last at least 10 turns + +11. `_targetHp`/`_targetMp`/`_targetSp`/`_targetHpDecimal`/`_targetMpDecimal`/`_targetSpDecimal`/`_targetBuffStack`/`_targetBuffTurn`/`_targetRank`: `HP%`/`SP%`/`MP%`/`HP% in decimal`/`SP% in decimal`/`MP% in decimal`/`buffRemainTime`/`attackRank`/`_targetRank`/`_targetOrder`/`_targetWeight`/`_targetIsAlive` of target monster + 1. , suffix of `_targetBuffStack`/`_targetBuffTurn_` is same as 10.`buffStack`/`buffTurn`(such as:`_targetBuffTurn_bleed != 0` means remain turns of bleed buff on target monster is not equal to 0. Target that is calculating is chosen by following rules: + 1. The highest priority monster by rank in default situations. + 2. Weapon skills (OFC, T1~T3, etc.), Offensive Spell skills (Tire2, Tire3): by each condition > for each ranked target > find the target fit all sub-condition in the condition and cast to it. Such as the pic below: condition for Merciful Blow: only cast to targets which with hp below 25% and a bleed buff. + + ![example](https://github.com/user-attachments/assets/da181eac-e634-41ad-97a7-ff59a7b28b6d) + + 2. `_targetBuffTurn` returns the value as same as the ranked order given by Attack Rule (0~9, smaller number as higher priority) + 3. `targetBossType`(see 12.) & other target params (except `targetName`) can get maxim/minimum/sum/'sign_sum(`sum(sign(value))`)' value from all alive monsters by using suffix `max/min/sum/count` such as: `_targetBuffTurn_max_bleed`. + 1. For `max/min/sum/count` , prefixs `a`/`ag`or`ga`/`g` are available and represent `a: including dead targets (still from all targets)`/`ag/ga: only from grouped and including dead targets`/`g: only from grouped (still ingore dead targets)`. About grouping, see 13. `targetGroup`. +12. `targetName`/`targetBossType`: name and boss type for target monster + 1. `_targetName` returns a string of the target name (**Note**: Comparison between strings will automatically remove the outermost quotation marks, meanwhile **please replace space` ` with underline`_`**, such as `Yugi_Nagato`/`'Yugi_Nagato'`/`"Yugi_Nagato"`) + 2. `_targetBossType` is determined by name: + 1. `Manbearpig`、`White Bunneh`、`Mithra`、`Dalek`: 1 (BOSS) + 2. `Konata`、`Mikuru Asahina`、`Ryouko Asakura`、`Yuki Nagato`: 2 (Legendaries) + 3. `Real Life`、`Invisible Pink Unicorn`、`Flying Spaghetti Monster`: 3 (Gods) + 4. `Rhaegal`、`Viserion`、`Drogon`: 4 (A Dance with Dragons) + 5. `Skuld`、`Urd`、`Verdandi`、`Yggdrasil`: 5 (Trio and the Tree) + 6. `Recycled Boss Rush`、`Bottomless Dungeon`、`New Game +`、`Achievement Grind`、`Time Trial Mode`、`Hardcore Mode`:6 (Post Game Content) + 7. `Fluttershy`、`Gummy`、`Rainbow Dash`、`Twilight Sparkle`、`Rarity`、`Applejack`、`Pinkie Pie`、`Angel Bunny`、`Spike`:7 (Ponies) + 8. others: 0 (not boss) +13. `targetGroup`: amount of target in group by certain mode (including dead targets), and change the `grouped targets` of current formula (see 11.3.1).1). For each calculation of each formula, `grouped targets` is dafault as a empty `null` group. Available group modes: + 1. `a`: Select all targets. `targetGroup_a` + 2. `s`: Split by given amount from the top down‌, and select the group contains current target. `targetGroup_s_2` means 2 targets as a group. + 3. `r`: A ranged group using the current target as a central starting point + 1. For 1 param only, using symmetry range. `targetGroup_r_1` means `current_target ± 1` (3 targets) + 2. For 2 param, params as `targetGroup_r_[up]_[down]`. `targetGroup_r_1_2` means \[`current_target - 1`, `current_target + 2`\] (4 targets) + 3. While `param(distance) >= 10`, overflows as `9 - param`, such as: `targetGroup_r_4_10` means \[`current_target - 4`, `current_target - 1`\] (4 targets) + 4. Params default as `-1`/`10`: `targetGroup_r_` means `targetGroup_r_10`; `targetGroup_r_1_` means `targetGroup_r_1_10`; `targetGroup_r__1` means `targetGroup_r_10_1`. The second param will be completed by default only if 2 `_` are spliting the expression. + 5. `o`/`oa`: Select the range by given order(0~9). `o` requires the current target is in the range, or returns 0 and selecting a empty `null` group; `oa` do NOT requires the current target is in the range. `targetGroup_o_1_3` means `order=[1,2,3]` (the 2nd, 3rd, 4th target) + 1. Params as `targetGroup_[o/oa]_[top]_[bottom]` and default as `-1`/`10` +14. `skillOTOS`: Amount of the **auto cast times** of Skill / Spell / Item / Defend / Focus / Spirit On / Spirit Off (not including the manually ones), `skillOTOS_[type]` such as `skillOTOS_FRD` (FUS RO DAH) /`skillOTOS_T1`(Tire 1 weapon skill) /`skillOTOS_defend`(Defend) + - Skills: `OFC`/`FRD`/`T3`/`T2`/`T1` + - Spells/Items: Spell or item `id` + - Defend/Focus/Spirit On/Spirit Off: `defend`/`focus`/`spiriton`/`spiritoff` +15. blank: the value you want to put in + +PS: For params get from func (starts with `_`), `_` from start can be omited (while omited, params will try get values as dict than as func if failed) #### Example @@ -147,7 +195,7 @@ The following is a schematic diagram of the circuit diagram | 1 | 2 | 3 | | - | - | - | -| - | Regen / regen | - | +| Channeling / channeling | Regen / regen | Focus / focus | | Protection / protection | Haste / haste | Shadow Veil / shadowveil | | Absorb / absorb | Spark of Life / sparklife | Spirit Shield / spiritshield | | Arcane Focus / arcanemeditation | Heartseeker / heartseeker | Cloak of the Fallen / fallenshield | @@ -155,7 +203,17 @@ The following is a schematic diagram of the circuit diagram | Infusion of Flames / fireinfusion | Infusion of Frost / coldinfusion | Infusion of Lightning / elecinfusion | | Infusion of Storms / windinfusion | Infusion of Divinity / holyinfusion | Infusion of Darkness / darkinfusion | | Scroll of Swiftness / haste_scroll | - | - | -| Flower Vase / flowers | Bubble-Gum / gum | - | +| Flower Vase / flowers | Bubble-Gum / gum | Blessing of the RiddleMaster / riddlemaster | +| Sleep / sleep | Blind / blind | Slow / slow | +| Imperil / imperil | MagNet / magnet | Silence / silence | +| Drain / drainhp | Weaken / weaken | Confuse / confuse | +| Coalesced Mana / coalescemana | Stunned / stun / wpn_stun | Penetrated Armor / ap / wpn_ap | +| Bleeding Wound / bleed / wpn_bleed | Absorbing Ward / absorb | Fury of the Sisters / trio_furyofthesisters | +| Lamentations of the Future / trio_skuld | Screams of the Past / trio_urd | Wailings of the Present / trio_verdandi | Searing Skin / firedot | Freezing Limbs / coldslow | +| Freezing Limbs / coldslow | Turbulent Air / windmiss | Deep Burns / elecweak | +| Searing Skin / firedot | - | Breached Defense / holybreach | +Blunted Attack / darknerf | Burning Soul / soulfire | Ripened Soul / ripesoul | +| Ether Tap / wpn_et | - | - | *** @@ -188,3 +246,19 @@ In this example, the script will attack enemy 1 next. * Old 1. see [README_Chinese#更新历史](https://github.com/dodying/UserJs/blob/master/HentaiVerse/hvAutoAttack/README.md#更新历史) + + + + + + + + + + + + + + + + diff --git a/HentaiVerse/hvAutoAttack/hvAutoAttack.user.js b/HentaiVerse/hvAutoAttack/hvAutoAttack.user.js index 869e6349..e3c571ba 100644 --- a/HentaiVerse/hvAutoAttack/hvAutoAttack.user.js +++ b/HentaiVerse/hvAutoAttack/hvAutoAttack.user.js @@ -6,7 +6,7 @@ // @description HV auto attack script, for the first user, should configure before use it. // @description:zh-CN HV自动打怪脚本,初次使用,请先设置好选项,请确认字体设置正常 // @description:zh-TW HV自動打怪腳本,初次使用,請先設置好選項,請確認字體設置正常 -// @version 2.90.22.12 +// @version 2.91.150 // @author dodying // @namespace https://github.com/dodying/ // @supportURL https://github.com/dodying/UserJs/issues @@ -14,6 +14,8 @@ // @include http*://hentaiverse.org/* // @include http*://alt.hentaiverse.org/* // @include http*://e-hentai.org/* +// @exclude http*://*hentaiverse.org/*/y/* +// @exclude http*://*hentaiverse.org/*/z/* // @connect hentaiverse.org // @connect e-hentai.org // @compatible Firefox + Greasemonkey @@ -28,687 +30,2040 @@ // @grant unsafeWindow // @run-at document-end // ==/UserScript== -/* eslint-disable camelcase */ - -const standalone = ['option', 'arena', 'drop', 'stats', 'staminaLostLog', 'battleCode', 'disabled', 'stamina', 'staminaTime', 'lastHref', 'battle', 'monsterDB', 'monsterMID', 'ability']; -const sharable = ['option']; -const excludeStandalone = { 'option': ['optionStandalone', 'version', 'lang'] }; -const href = window.location.href; -const isIsekai = href.indexOf('isekai') !== -1; -const current = isIsekai ? 'isekai' : 'persistent'; -const other = isIsekai ? 'persistent' : 'isekai'; -let GM_cache; - -const _1s = 1000; -const _1m = 60 * _1s; -const _1h = 60 * _1m; -const _1d = 24 * _1h; - -try { - const isFrame = window.self !== window.top; - if (isFrame) { - if (!window.top.location.href.match(`/equip/`) && (gE('#riddlecounter') || !gE('#navbar'))) { - if(!window.top.location.href.endsWith(`?s=Battle`)){ - setValue('lastHref', window.top.location.href); - } - window.top.location.href = window.self.location.href; - } - if(window.location.href.indexOf(`?s=Battle&ss=ar`) !== -1 || window.location.href.indexOf(`?s=Battle&ss=rb`) !== -1){ - loadOption(); - setArenaDisplay(); - } - return; - } - try { - if(window.location.href.startsWith('https://')) { - MAIN_URL = MAIN_URL.replace(/^http:/, /^https:/); - } else { - MAIN_URL = MAIN_URL.replace(/^https:/, /^http:/); - } - } catch (e) {} - const Debug = { - Stack: class extends Error { - constructor(description, ...params) { - super(...params); - this.name = 'Debug.Stack'; - } + +(function () { try { + 'use strict'; + // constant + const dataFlags = { sharable: ['option'] }; + dataFlags.portable = ['drop', 'stats', 'dropOld', 'statsOld', 'monsterDB', 'monsterMID']; + dataFlags.battleDatas = [...dataFlags.portable, 'battle', 'battleCode', 'disabled', 'stepIn', 'skillOTOS', 'onriddle', 'rec']; + dataFlags.local = [...dataFlags.battleDatas, 'stamina']; + dataFlags.standalone = [...dataFlags.sharable, ...dataFlags.local, 'arena', 'lastUrl', 'ability', 'proficiency', 'lastSwitch', 'itemWorldDatas', 'lastPersona', 'lastEquipSet']; + dataFlags.excludeStandalone = { 'option': ['optionStandalone', 'version', 'lang'] }; + + const _1s = 1000; + const _1m = 60 * _1s; + const _1h = 60 * _1m; + const _1d = 24 * _1h; + + const monsterStateKeys = { obj: `div.btm1`, lv: `div.btm2`, name: `div.btm3`, bars: `div.btm4>div.btm5`, buffs: `div.btm6` }; + + const setMonsterBuffSkillLib = () => { return { + // debuff skill ------------ + We: { + proficiency: ['Deprecating', 0, 345], // ??? + buff: 'Weakened', + name: 'Weaken', + img: 'weaken', + id: '212', + range: { 4202: [1, 1, 2, 3] }, + duration: { 4201: [10, 11, 12, 13, 14, 15] }, + channeling: true, + description: "'The target has been weakened, making it deal less damage, and preventing it from scoring critical hits.'" }, - realtime: false, - logList: [], - maxLogCache: 100, - switchRealtimeLog: function () { - Debug.enableRealtimeLog(Debug.realtime); + Im: { + proficiency: ['Deprecating', 30, 495], // ??? + buff: 'Imperiled', + name: 'Imperil', + img: 'imperil', + id: '213', + range: { 4204: [1, 1, 2, 3] }, + duration: { 4203: [10, 11, 12, 13, 14, 15] }, + channeling: true, + description: "'The target has been imperiled, reducing physical and magical mitigation as well as elemental mitigation.'" }, - enableRealtimeLog: function (enabled) { - Debug.realtime = enabled; - if (enabled) { - Debug.shiftLog(); - } + Bl: { + proficiency: ['Deprecating', 30, 610], // ??? + buff: 'Blinded', + name: 'Blind', + img: 'blind', + id: '231', + range: { 4206: [1, 1, 2, 3] }, + duration: { 4205: [10, 11, 12, 13, 14, 15] }, + channeling: true, + description: "'The target has been blinded, reducing the chance of landing attacks and hitting with magic spells.'" }, - log: function () { - if (Debug.realtime) { - console.log(...arguments, `\n`, (new Debug.Stack()).stack); - return; - } - Debug.logList.push({ - args: arguments, - stack: (new Debug.Stack()).stack - }); - if (Debug.logList.length > Debug.maxLogCache) { - Debug.logList.shift(); - } + Sle: { + proficiency: ['Deprecating', 0, 410], // ??? + buff: 'Asleep', + name: 'Sleep', + img: 'sleep', + id: '222', + range: { 4207: [1, 1, 2, 3] }, + duration: { 4207: [5, 6, 6, 7] }, + channeling: true, + description: "'The target has been lulled to sleep, preventing it from taking any actions. Any attacks against this target are guaranteed to hit, but can also wake it up.'" + }, + Co: { + proficiency: ['Deprecating', 45, 620], // ??? + buff: 'Confused', + name: 'Confuse', + img: 'confuse', + id: '223', + range: { 4207: [1, 1, 2, 3] }, + duration: { 4207: [10, 11, 12, 12] }, + channeling: true, + description: "'The target has been confused, making it lunge out wildly and strike friends and foes alike.'" + }, + Si: { + proficiency: ['Deprecating', 40, 600], // ??? + buff: 'Silenced', + name: 'Silence', + img: 'silence', + id: '232', + range: { 4211: [1, 1, 2, 3] }, + duration: { 4211: [10, 11, 12, 13] }, + channeling: true, + description: "'The target has been silenced, preventing it from using special attacks and magic.'" + }, + MN: { + proficiency: ['Deprecating', 100, 700], // ??? + buff: 'Immobilized', + name: 'Immobilize', + img: 'magnet', + id: '233', + range: { 4212: [1, 1, 1, 2, 2, 3] }, + duration: { 4212: [10, 11, 12, 13, 14, 15] }, + channeling: true, + description: "'The target has been immobilized, eliminating its chance to evade and reducing its magic resistance.'" + }, + Slo: { + proficiency: ['Deprecating', 0, 300], // ??? + buff: 'Slowed', + name: 'Slow', + img: 'slow', + id: '221', + range: { 4213: [1, 1, 2, 2, 2, 3] }, + duration: { 4213: [10, 11, 12, 13, 14, 15] }, + channeling: true, + description: `'The target has been slowed by ${[30, 40, 40, 45, 50, 50][ability[4213] ?? 0]}%, making it attack less frequently.'` + }, + // debuff skills not checked ------------ ?? + Dr: { + proficiency: ['Deprecating', 0, 300], // ??? + buff: 'Vital Theft', + name: 'Drain', + img: 'drainhp', + id: '211', + duration: 10, + channeling: true, + description: "'Siphons off the target\\'s life essence over time, and gives it to the player.'" + }, + ET: { + proficiency: ['Deprecating', 0, 300], // ??? + name: 'Ether Theft', + img: 'drainmp', + duration: 10, // ?? + description: "'Siphons off the target\\'s mana over time, and gives it to the player.'" + }, + ST: { + proficiency: ['Deprecating', 0, 300], // ??? + name: 'Spirit Theft', + img: 'drainsp', + duration: 10, // ?? + description: "'Siphons off the target\\'s spirit over time, and gives it to the player.'" + }, + // elem attack debuff ------------ ?? + SS: { + proficiency: ['Elemental', 0, 800], // ??? + name: 'Searing Skin', + img: 'firedot', + elem: 2, + duration: 3, + channeling: true, + description: "'The skin of the target has been scorched, inhibiting its attack damage. Cold resistance is lowered.'" + }, + FL: { + proficiency: ['Elemental', 0, 800], // ??? + name: 'Freezing Limbs', + img: 'coldslow', + elem: 1, + duration: 3, + channeling: true, + description: "'The limbs of the target have been frozen, causing slower movement. Wind resistance is lowered.'" + }, + TA: { + proficiency: ['Elemental', 0, 800], // ??? + name: 'Turbulent Air', + img: 'windmiss', + elem: 4, + duration: 3, + channeling: true, + description: "'The air around the target has been upset, blowing up dust and increasing its miss chance. Elec resistance is lowered.'" + }, + DB: { + proficiency: ['Elemental', 0, 800], // ??? + name: 'Deep Burns', + img: 'elecweak', + elem: 3, + duration: 3, + channeling: true, + description: "'Internal damage causes slower reactions and lowers evade and resist chance. Fire resistance is lowered.'" + }, + BD: { + proficiency: ['Forbidden', 0, 800], // ??? + name: 'Breached Defense', + img: 'holybreach', + elem: 6, + duration: 3, + channeling: true, + description: "'The holy attack has penetrated the target defenses, making it take more damage. Dark resistance is lowered.'" + }, + BA: { + proficiency: ['Divine', 0, 800], // ??? + name: 'Blunted Attack', + img: 'darknerf', + elem: 5, + duration: 3, + channeling: true, + description: "'The decaying effects of the spell has blunted the target offenses, making it deal less damage. Holy resistance is lowered.'" + }, + BS: { + proficiency: ['Divine', 0, 800], // ??? + name: 'Burning Soul', + img: 'soulfire', + duration: 7, + channeling: true, + description: "'The life essence of the target has been set ablaze, damaging its physical form over time.'" + }, + RS: { + proficiency: ['Forbidden', 0, 800], // ??? + name: 'Ripened Soul', + img: 'ripesoul', + duration: 7, + channeling: true, + description: "'The life essence of the target has been corrupted beyond repair, damaging its physical form over time.'" + }, + // weapon debuff ------------ ?? + PA: { + name: 'Penetrated Armor', + img: 'wpn_ap', + duration: 7, + description: "'The armor of this target has been breached, reducing its physical defenses.'" + }, + BW: { + name: 'Bleeding Wound', + img: 'wpn_bleed', + stack: 80, + duration: 7, + description: "'Gashing wounds are making this target take damage over time.'" + }, + Stun: { + name: 'Stunned', + img: 'wpn_stun', + duration: 4, + description: "'A powerful blow has temporarily stunned this target.'" + }, + // else from player ------------ ?? + Po: { + name: 'Spreading Poison', + img: 'poison', + duration: 15, + description: "'Poison courses through the target\'s veins. This causes a damage-over-time effect, and eliminates its evade chance.'" + }, + CM: { + name: 'Coalesced Mana', + img: 'coalescemana', + duration: 5, + description: "'Mystical energies have converged on this target. Striking it with any magic spell will consume only half the normal mana.'" + }, + AW: { + name: 'Absorbing Ward', + img: 'absorb', + duration: 'permanent', + description: "'The next magical attack against the target has a chance to be absorbed and partially converted to MP.'" + }, + FoS: { + name: 'Fury of the Sisters', + img: 'trio_furyofthesisters', + duration: 'permanent', + description: "'The destruction of the world tree has infuriated its defenders, increasing their accuracy.'" + }, + LoF: { + name: 'Lamentations of the Future', + img: 'trio_skuld', + duration: 'permanent', + description: "'The destruction of the future has increased the attack power of her allies.'" + }, + SoP: { + name: 'Screams of the Past', + img: 'trio_urd', + duration: 'permanent', + description: "'The destruction of the past has increased the defensive power of her allies.'" + }, + WoP: { + buff: 'Wails of the Present', + name: 'Wailings of the Present', + img: 'trio_verdandi', + duration: 'permanent', + description: "'The destruction the present has increased the attack speed of her allies.'" + }, + } }; + const playerBuffSkillLib = { + SS: { + name: 'Spirit Shield', + id: '423', + img: 'spiritshield', + }, + SL: { + name: 'Spark of Life', + id: '422', + img: 'sparklife', + }, + Pr: { + name: 'Protection', + id: '411', + img: 'protection', + }, + Ab: { + name: 'Absorb', + id: '421', + img: 'absorb', + }, + SV: { + name: 'Shadow Veil', + id: '413', + img: 'shadowveil', + }, + Re: { + name: 'Regen', + id: '312', + img: 'regen', + }, + Ha: { + name: 'Haste', + id: '412', + img: 'haste', + }, + He: { + name: 'Heartseeker', + id: '431', + img: 'heartseeker', + }, + AF: { + name: 'Arcane Focus', + id: '432', + img: 'arcanemeditation', }, - shiftLog: function () { - while (Debug.logList.length) { - const log = Debug.logList.shift(); - console.log(...log.args, `\n`, log.stack); - } - } - } - const asyncList = []; - function consoleAsyncTasks(name, state) { - if (!state) { - asyncList.splice(asyncList.indexOf(name), 1); - } else { - asyncList.push(name); + CF: { + name: 'Cloak of the Fallen', + id: 422, + img: 'fallenshield', } - console.log(`${state ? 'Start' : 'End'} ${name}\n`, JSON.parse(JSON.stringify(asyncList))); - } - function logSwitchAsyncTask(args) { - try{ - const argsStr = Array.from(args).join(','); - const name = `${args.callee.name}${argsStr === '' ? argsStr : `(${argsStr})`}`; - consoleAsyncTasks(name, asyncList.indexOf(name) === -1); - }catch(e){} - } + }; + const itemMap = { + ...{ // Consumables + 11191: ['体力长效药', '體力長效藥', 'Health Draught'], + 11195: ['体力药水', '體力藥水', 'Health Potion'], + 11199: ['体力秘药', '體力秘藥', 'Health Elixir'], + 11291: ['魔力长效药', '魔力長效藥', 'Mana Draught'], + 11295: ['魔力药水', '魔力藥水', 'Mana Potion'], + 11299: ['魔力秘药', '魔力秘藥', 'Mana Elixir'], + 11391: ['灵力长效药', '靈力長效藥', 'Spirit Draught'], + 11395: ['灵力药水', '靈力藥水', 'Spirit Potion'], + 11399: ['灵力秘药', '靈力秘藥', 'Spirit Elixir'], + 12101: ['火焰魔药', '火焰魔藥', 'Infusion of Flames'], + 12201: ['冰冷魔药', '冰冷魔藥', 'Infusion of Frost'], + 12301: ['闪电魔药', '閃電魔藥', 'Infusion of Lightning'], + 12401: ['风暴魔药', '風暴魔藥', 'Infusion of Storms'], + 12501: ['神圣魔药', '神聖魔藥', 'Infusion of Divinity'], + 12601: ['黑暗魔药', '黑暗魔藥', 'Infusion of Darkness'], + 13299: ['众神卷轴', '眾神捲軸', 'Scroll of the Gods'], + 13221: ['生命卷轴', '生命捲軸', 'Scroll of Life'], + 13211: ['幻影卷轴', '幻影捲軸', 'Scroll of Shadows'], + 13201: ['吸收卷轴', '吸收捲軸', 'Scroll of Absorption'], + 13199: ['化身卷轴', '化身捲軸', 'Scroll of the Avatar'], + 13111: ['守护卷轴', '守護捲軸', 'Scroll of Protection'], + 13101: ['加速卷轴', '加速捲軸', 'Scroll of Swiftness'], + 19111: ['花瓶', '花瓶', 'Flower Vase'], + 19131: ['泡泡糖', '泡泡糖', 'Bubble-Gum'], + 11501: ['终极秘药', '終極秘藥', 'Last Elixir'], + 11401: ['能量饮料', '能量飲料', 'Energy Drink'], + 11402: ['咖啡因糖果', '咖啡因糖果', 'Caffeinated Candy'], + } // Consumables + }; - //ajax - function $doc(h) { - const d = document.implementation.createHTMLDocument(''); d.documentElement.innerHTML = h; return d; + // runtime + const isFrame = window.self !== window.top; + function $id(id, d) { return (d || document).getElementById(id); } + const _servername = location.pathname.includes('/isekai/') ? 'isekai' : 'persistent'; + const addition = { + other: _servername === 'isekai' ? 'persistent' : 'isekai', + utils: _servername === 'isekai' ? 'hvuti' : 'hvut', + }; + const _server = { + name: _servername, + season: $id('world_text')?.textContent.match(/\d+ Season \d+/)?.[0] || '1', + [_servername]: true, // _server.persistent || _server.isekai + ...addition, + }; + const isEquipDetail = window.location.href.includes('/equip/'); + const isMaintaining = !gE('#csp') && !isEquipDetail; + const scriptVersion = Version(GM_info ? GM_info.script.version : '2.91'); + let hvVersion; + let onIsekaiEncounter; + let monsterBuffSkillLib; + let ability = getValue('ability', true) ?? {}; + let lastResponsive = new Date().getTime(); + + // util methods + function repeat(value, times) { + return range(times).map(_ => value); + } + function range(start, stop, step = 1) { + start = start?.length ?? start; + stop = stop?.length ?? stop; + if (stop === undefined) [start, stop] = [0, start]; + const result = []; + switch (true) { + case step === 0: + case step > 0: + while (start < stop) { + result.push(start); + start += step; + } + break; + case step < 0: + while (start > stop) { + result.push(start); + start += step; + } + break; + default: throw new Error('range() arg 3 must not be zero'); + } + return result; } - var $ajax = { - - interval: 300, // DO NOT DECREASE THIS NUMBER, OR IT MAY TRIGGER THE SERVER'S LIMITER AND YOU WILL GET BANNED - max: 4, - tid: null, - conn: 0, - index: 0, - queue: [], - fetch: function (url, data, method, context = {}, headers = {}) { - return new Promise((resolve, reject) => { - $ajax.add(method, url, data, resolve, reject, context, headers); + const UI = { + langs: 3, + byLang: function (...args) { + if (Array.isArray(args[0])) args = args[0]; + return args ? args[g().lang] : ''; + }, + cutLang: function (str) { + const lang = g().lang; + return str.replaceAll(/(.*?)<\/l(\d+)>/g, (matched, lang1, inner, lang2) => { + switch (true) { + case lang1 !== lang2: return inner; + case lang1 !== lang: return ''; + default: return inner; + } }); }, - open: function (url, data, method, context = {}, headers = {}) { - $ajax.fetch(url, data, method, context, headers).then(goto).catch(e=>{console.error(e)}); + alert: (...args) => window.alert(UI.byLang(args)), + confirm: (...args) => window.confirm(UI.byLang(args)), + prompt: (...args) => window.prompt(UI.byLang(args.slice(0, UI.langs)), args[UI.langs]), + l: function (...args) { + if (!args.length) return ''; + if (typeof args[0] !== 'string') args = args[0]; + let extra = args?.[UI.langs]; + extra = extra ? ' ' + extra : ''; + return range(UI.langs).map(i => `${args?.[i] ?? ''}`).join(''); }, - openNoFetch: function (url, newTab) { - window.open(url, newTab ? '_blank' : '_self') - // const a = gE('body').appendChild(cE('a')); - // a.href = url; - // a.target = newTab ? '_blank' : '_self'; - // a.click(); + button: { + class: function (className, ...inner) { + return `` + }, + details: isDisplay => `${UI.l('详情', '詳情', 'Details')}${isDisplay ? `▲` : `▼`}`, + pause: function () { + const option = getOption(); + return `${UI.l('暂停', '暫停', 'Pause')}${(option.pauseHotkey && option.pauseHotkeyStr) ? `(${option.pauseHotkeyStr})` : ''}`; + }, + stepIn: function () { + const option = getOption(); + return `${UI.l('步进', '步進', 'StepIn')}${(option.stepInHotkey && option.stepInHotkeyStr) ? `(${option.stepInHotkeyStr})` : ''}`; + }, + continue: function () { + const option = getOption(); + return `${UI.l('继续', '繼續', 'Continue', 'style="color:red;"')}${(option.pauseHotkey && option.pauseHotkeyStr) ? `(${option.pauseHotkeyStr})` : ''}`; + }, }, - repeat: function (count, func, ...args) { - const list = []; - for (let i = 0; i < count; i++) { - list.push(func(...args)); - } - return list; + expendData: function (datas, method) { + const mapped = datas.map(args => method(args.id, UI.l(args.names), ...(args.values ?? []))); + if (Array.isArray(mapped[0])) return mapped.reduce((acc, cur) => (acc ?? []).concat(cur ?? []), [])?.join(''); + return mapped.reduce((acc, cur) => (acc ?? '') + (cur ?? ''), ''); }, - add: function (method, url, data, onload, onerror, context = {}, headers = {}) { - method = !data ? 'GET' : method ?? 'POST'; - if (method === 'POST') { - headers['Content-Type'] ??= 'application/x-www-form-urlencoded'; - if (data && typeof data === 'object') { - data = Object.entries(data).map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&'); - } - } else if (method === 'JSON') { - method = 'POST'; - headers['Content-Type'] ??= 'application/json'; - if (data && typeof data === 'object') { - data = JSON.stringify(data); - } - } - context.onload = onload; - context.onerror = onerror; - $ajax.queue.push({ method, url, data, headers, context, onload: $ajax.onload, onerror: $ajax.onerror }); - $ajax.next(); + label: function (ids, inner, ...extra) { + const forTarget = Array.isArray(ids) ? [...ids, ''].join(',') : ids; + return `` }, - next: function () { - if (!$ajax.queue[$ajax.index] || $ajax.error) { - return; - } - if ($ajax.tid) { - if (!$ajax.conn) { - clearTimeout($ajax.tid); - $ajax.timer(); - $ajax.send(); - } - } else { - if ($ajax.conn < $ajax.max) { - $ajax.timer(); - $ajax.send(); - } - } + labeled: function (id, names, ...extra) { + return `${UI.label(id, names)}`; }, - timer: function () { - var _ns = isIsekai ? 'hvuti' : 'hvut'; - function getValue(k, d, p = _ns + '_') { const v = localStorage.getItem(p + k); return v === null ? d : JSON.parse(v); } - function setValue(k, v, p = _ns + '_', r) { localStorage.setItem(p + k, JSON.stringify(v, r)); } - function ontimer() { - const now = new Date().getTime(); - const last = getValue('last_post'); - if (last && last - now < $ajax.interval) { - $ajax.next(); - return; - } - setValue('last_post', now); - $ajax.tid = null; - $ajax.next(); - }; - $ajax.tid = setTimeout(ontimer, $ajax.interval); + orderValue: function (id, hidden) { + return UI.text(id, 'style="width:80%;" disabled="true"', hidden ? 'type="hidden"' : '') + (hidden ? '' : '
'); }, - send: function () { - GM_xmlhttpRequest($ajax.queue[$ajax.index]); - $ajax.index++; - $ajax.conn++; + text: function (id, ...extra) { + return ` e.match('type="hidden"')) ? '' : 'type="text"'} ${extra.join(' ')}>`; }, - onload: function (r) { - $ajax.conn--; - const text = r.responseText; - if (r.status !== 200) { - $ajax.error = `${r.status} ${r.statusText}: ${r.finalUrl}`; - r.context.onerror?.(); - } else if (text === 'state lock limiter in effect') { - if ($ajax.error !== text) { - // popup(`

${text}

Your connection speed is so fast that
you have reached the maximum connection limit.

Try again later.

`); - console.error(`${text}\nYour connection speed is so fast that you have reached the maximum connection limit. Try again later.`) - } - $ajax.error = text; - r.context.onerror?.(); - } else { - r.context.onload?.(text); - $ajax.next(); - } + number: function (id, placeholder = 0, type = 'number', classExtra, ...extra) { + return ``; }, - onerror: function (r) { - $ajax.conn--; - $ajax.error = `${r.status} ${r.statusText}: ${r.finalUrl}`; - r.context.onerror?.(); - $ajax.next(); + b: function (...inner) { + return `${inner.join('')}`; }, - }; + repeat: function (n, size = '1fr') { + return `repeat(${n}, ${size})` + }, + hvAATab: function (id, ...inner) { + return UI.div({ + args: { + class: 'hvAATab', id: `hvAATab-${id}`, + style: id === 'Main' ? 'display: block' : '', + }, + inner: inner + }); + }, + hvAATable: function (style, className, ...inner) { + return UI.div({ + args: `class="hvAATable ${className}" style="grid-template-columns: ${style};"`, + inner: inner + }); + }, + checkSupplyInnerExtra: function (key, names) { + return UI.div({ + args: { class: 'checkSupplyInner' }, + inner: [ + UI.labeled(`checkSupply${key}`, UI.b('[C!!]', UI.l(['[name]使用额外的库存检查', '[name]使用額外的庫存檢查', 'Extra supply check for [name]'].map(t => t.replace('[name]', UI.byLang(names)))), ';')), + ...getCheckSupplyOptionTable(key), + ] + }); + }, + div: function (...datas) { + if (datas.length === 0) return `
`; + if (datas.length > 1) { + return [`
`, ...datas, `
`].join(''); + } + datas = datas[0]; + if (typeof datas === 'string') { + return [`
`, datas, `
`].join(''); + } + if (datas.args || datas.inner) { + let args = ''; + if (typeof datas.args === 'string') { + args = datas.args; + } else { + for (const key in datas.args) { + args += ` ${key}="${datas.args[key]}"`; + } + } + return [`
`, ...(typeof datas.inner === 'string' ? [datas.inner] : datas.inner ?? ''), `
`].join(''); + } + return [`
`, ...datas, `
`].join(''); + }, + } + UI.attackStatusType = [ + UI.l('物理', '物理', 'Physical'), + UI.l('火', '火', 'Fire'), + UI.l('冰', '冰', 'Cold'), + UI.l('雷', '雷', 'Elec'), + UI.l('风', '風', 'Wind'), + UI.l('圣', '聖', 'Divine'), + UI.l('暗', '暗', 'Forbidden'), + ]; + UI.button = { + ...UI.button, + update: `${UI.l('更新', '更新', 'Update')}`, + updating: UI.l('更新中...', '更新中...', 'Updating...'), + clear: UI.l('清空', '清空', 'Clear'), + reset: UI.l('重置', '重置', 'Reset'), + } - window.addEventListener('unhandledrejection', (e) => { console.error($ajax.error, e); }); + if (typeof Object.sortBy === 'undefined') { + Object.defineProperty(Object.prototype, 'sortBy', { value: function sortBy(by) { + return this.sort((x, y) => by(x) < by(y) ? -1 : by(x) > by(y) ? 1 : 0) + }, enumerable: false }); + } - (function init() { - if (!checkIsHV()) { - return; - } + const [$RPN, $async, $debug, $ajax] = [initRPN(), initAsync(), initDebug(), window.top.$ajax ??= unsafeWindow.window.top.$ajax ??= initAjax()]; - if (!gE('#navbar,#riddlecounter,#textlog')) { - setTimeout(goto, 5 * _1m); - return; - } + // 初始化结束,开始实际流程 + for (let check of [checkIsHV, checkIsWindowTop, checkOption]) { + if (!check()) return; + } + for (let step of [onRiddle, onIdle, onBattle]) { + if (step()) return; + } + // 其他情况进行等待刷新(例如加载错误等) + setTimeout(goto, 5 * _1m); + + // ----------Process Steps---------- + function initRPN() { + const $RPN = { + operators: { + '>=': { precedence : 0, func: (a, b) => a >= b ? 1 : 0 }, + '<=': { precedence : 0, func: (a, b) => a <= b ? 1 : 0 }, + '==': { precedence : 0, func: (a, b) => a === b ? 1 : 0 }, + '!=': { precedence : 0, func: (a, b) => a !== b ? 1 : 0 }, + '&&': { precedence : -1, func: (a, b) => a && b ? 1 : 0 }, + '||': { precedence : -1, func: (a, b) => a || b ? 1 : 0 }, + '^': { precedence : -1, func: (a, b) => ((!a) ^ (!b)) ? 1 : 0 }, + '**': { precedence:3, func: (a, b) => Math.pow(a, b)}, + '-neg': { precedence: -2, func: (a) => -a }, + '~': { precedence : -2, func: (a) => Math.log10(a) }, + '!': { precedence : -2, func: (a) => a ? 0 : 1 }, + '+': { precedence : 1, func: (a, b) => a + b }, + '-': { precedence : 1, func: (a, b) => a - b }, + '*': { precedence : 2, func: (a, b) => a * b }, + '/': { precedence : 2, func: (a, b) => a / b }, + '%': { precedence : 2, func: (a, b) => a % b }, + '>': { precedence : 0, func: (a, b) => a > b ? 1 : 0 }, + '<': { precedence : 0, func: (a, b) => a < b ? 1 : 0 }, + }, - g('version', GM_info ? GM_info.script.version.substr(0, 4) : '2.90'); - if (!getValue('option')) { - g('lang', window.prompt('请输入以下语言代码对应的数字\nPlease put in the number of your preferred language (0, 1 or 2)\n0.简体中文\n1.繁體中文\n2.English', 0) || 2); - addStyle(g('lang')); - _alert(0, '请设置hvAutoAttack', '請設置hvAutoAttack', 'Please config this script'); - gE('.hvAAButton').click(); - return; - } - loadOption(); - g('lang', g('option').lang || '0'); - addStyle(g('lang')); - if (g('option').version !== g('version')) { - gE('.hvAAButton').click(); - if (_alert(1, 'hvAutoAttack版本更新,请重新设置\n强烈推荐【重置设置】后再设置。\n是否查看更新说明?', 'hvAutoAttack版本更新,請重新設置\n強烈推薦【重置設置】後再設置。\n是否查看更新說明?', 'hvAutoAttack version update, please reset\nIt\'s recommended to reset all configuration.\nDo you want to read the changelog?')) { - $ajax.openNoFetch('https://github.com/dodying/UserJs/commits/master/HentaiVerse/hvAutoAttack/hvAutoAttack.user.js', true); - } - gE('.hvAAReset').focus(); - return; - } + multiCharOperators: ['>=', '<=', '==', '!=', '&&', '||', '**'], - if (gE('[class^="c5"],[class^="c4"]') && _alert(1, '请设置字体\n使用默认字体可能使某些功能失效\n是否查看相关说明?', '請設置字體\n使用默認字體可能使某些功能失效\n是否查看相關說明?', 'Please set the font\nThe default font may make some functions fail to work\nDo you want to see instructions?')) { - $ajax.openNoFetch(`https://github.com/dodying/UserJs/blob/master/HentaiVerse/hvAutoAttack/README${g('lang') === '2' ? '_en.md#about-font' : '.md#关于字体的说明'}`, true); - return; - } + test: { + isNumber: str => /[0-9]/.test(str), + number: str => /[.0-9]/.test(str), + isParam: str => /[a-zA-Z_'",{}#]/.test(str), + param: str => /[.a-zA-Z_'",{}#^~0-9]/.test(str), + }, - unsafeWindow = typeof unsafeWindow === 'undefined' ? window : unsafeWindow; + isOperator: function (token) { + return token in $RPN.operators; + }, - if (gE('#riddlecounter')) { // 需要答题 - if (!g('option').riddlePopup || window.opener) { - riddleAlert(); // 答题警报 - return; - } - window.open(window.location.href, 'riddleWindow', 'resizable,scrollbars,width=1241,height=707'); - return; - } + hasHigherPrecedence: function (op1, op2) { + return $RPN.operators[op1].precedence >= $RPN.operators[op2].precedence; + }, - if (window.location.href.indexOf(`?s=Battle&ss=ba`) !== -1) { - // 补充记录(因写入冲突、网络卡顿等)未被记录的encounter链接 - const encounterURL = window.location.href?.split('/')[3]; - const encounter = getEncounter(); - if (!encounter.filter(e => e.href === encounterURL).length) { - encounter.unshift({ href: encounterURL, time: time(0), encountered: time(0) }); - } - setEncounter(encounter); - } - if (!gE('#navbar')) { // 战斗中 - const box2 = gE('#battle_main').appendChild(cE('div')); - box2.id = 'hvAABox2'; - setPauseUI(box2); - reloader(); - g('attackStatus', g('option').attackStatus); - g('timeNow', time(0)); - g('runSpeed', 1); - Debug.log('______________newRound', false); - newRound(false); - if (g('option').recordEach && !getValue('battleCode')) { - setValue('battleCode', `${time(1)}: ${g('battle')?.roundType?.toUpperCase()}-${g('battle')?.roundAll}`); - } - onBattle(); - updateEncounter(false, true); - return; - } - if(window.top.location.href.endsWith(`?s=Battle`)){ - $ajax.openNoFetch(getValue('lastHref')); - return; - } + tokenize: function (expression) { + const tokens = []; + let i = 0; + let lastTokenWasOperatorOrLeftParen = true; - // 战斗外 - if (window.location.href.indexOf(`?s=Battle&ss=ba`) === -1) { // 不缓存encounter - setValue('lastHref', window.top.location.href); // 缓存进入战斗前的页面地址 - setArenaDisplay(); - } - delValue(1); - if (g('option').showQuickSite && g('option').quickSite) { - quickSite(); - } - const hvAAPauseUI = document.body.appendChild(cE('div')); - hvAAPauseUI.classList.add('hvAAPauseUI'); - setPauseUI(hvAAPauseUI); - asyncOnIdle(); + while (i < expression.length) { + const ch = expression[i]; - }()); + if (ch === ' ') { + i++; + continue; + } - function setArenaDisplay(){ - if(window.location.href.indexOf(`?s=Battle&ss=ar`) === -1 && window.location.href.indexOf(`?s=Battle&ss=rb`) === -1){ - return; - } - var ar = g('option').idleArenaValue?.split(','); - if(!ar || ar.length === 0){ - return; - } - if(!g('option').obscureNotIdleArena){ - return; - } - gE('img[src*="startchallenge.png"]', 'all', document).forEach((btn) => { - const temp = btn.getAttribute('onclick').match(/init_battle\((\d+),\d+,'(.*?)'\)/); - if(ar.includes(temp[1])) { - return; - } - gE('div', 'all', btn.parentNode.parentNode).forEach(div=>{div.style.cssText += 'color:grey!important;'}); - }); - } + if (ch === '(' || ch === ')') { + tokens.push(ch); + i++; + lastTokenWasOperatorOrLeftParen = (ch === '('); + continue; + } - function loadOption() { - let option = getValue('option', true); - const aliasDict = { - 'debuffSkillImAll': 'debuffSkillAllIm', - 'debuffSkillWeAll': 'debuffSkillAllWk', - 'debuffSkillAllImCondition': 'debuffSkillImpCondition', - 'debuffSkillAllWeCondition': 'debuffSkillWkCondition', - 'battleUnresponsive_Alert': 'delayAlert', - 'battleUnresponsive_Reload': 'delayReload', - 'battleUnresponsive_Alt': 'delayAlt', - 'battleUnresponsiveTime_Alert': 'delayAlertTime', - 'battleUnresponsiveTime_Reload': 'delayReloadTime', - 'battleUnresponsiveTime_Alt': 'delayAltTime', - } - for (let key in aliasDict) { - const itemArray = key.split('_'); - if (itemArray.length === 1) { - option[key] ??= option[aliasDict[key]]; - option[aliasDict[key]] = undefined; - } else { - option[itemArray[0]] ??= {}; - option[itemArray[0]][itemArray[1]] ??= option[aliasDict[key]]; - } - } - if(isFrame){ - g('option', option); - } else{ - g('option', setValue('option', option)); - } - } + if (ch === "'" || ch === '"') { + const quote = ch; + let j = i; + while (j < expression.length) { + const current = expression[j]; + j++; + if (current === '\\' && expression[j] === quote) { + j++; + continue; + } + if (i !== j - 1 && current === quote) break; // 字符串结束 + } + if (j > expression.length) { + throw new Error(`Unclosed ${quote} string`); + } + tokens.push(expression.slice(i, j)); + i = j + 1; + lastTokenWasOperatorOrLeftParen = false; + continue; + } - function pauseAsync(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } + let isMultiChar = false; + for (const op of $RPN.multiCharOperators) { + if (expression.startsWith(op, i)) { + tokens.push(op); + i += op.length; + lastTokenWasOperatorOrLeftParen = true; + isMultiChar = true; + break; + } + } + if (isMultiChar) continue; - async function asyncOnIdle() { try { - if(getValue('disabled')){ - await pauseAsync(_1s); - return await asyncOnIdle(); - } - let notBattleReady = false; - const idleStart = time(0); - await Promise.all([ - (async () => { try { - await asyncGetItems(); - const checked = await asyncCheckSupply(); - notBattleReady ||= !checked; - } catch (e) {console.error(e)}})(), - asyncSetStamina(), - asyncSetEnergyDrinkHathperk(), - asyncSetAbilityData(), - updateArena(), - updateEncounter(g('option').encounter), - (async () => { try { - const checked = await asyncCheckRepair(); - notBattleReady ||= !checked; - } catch (e) {console.error(e)}})(), - ]); - if (notBattleReady) { - return; - } - if (g('option').idleArena && g('option').idleArenaValue) { - startUpdateArena(idleStart); - } - setTimeout(autoSwitchIsekai, (g('option').isekaiTime * (Math.random() * 20 + 90) / 100) * _1s - (time(0) - idleStart)); - } catch (e) {console.error(e)}} + if ($RPN.isOperator(ch)) { + if (ch === '-' && lastTokenWasOperatorOrLeftParen) { + tokens.push('-neg'); + } else { + tokens.push(ch); + } + i++; + lastTokenWasOperatorOrLeftParen = true; + continue; + } - // 通用// - function setPauseUI(parent) { - setPauseButton(parent); - setPauseHotkey(); - } + if ($RPN.test.isNumber(ch)) { + let num = ''; + while (i < expression.length && $RPN.test.number(expression[i])) { + num += expression[i]; + i++; + } + tokens.push(parseFloat(num)); + lastTokenWasOperatorOrLeftParen = false; + continue; + } - function setPauseButton(parent) { - if (!g('option').pauseButton) { - return; - } - const button = parent.appendChild(cE('button')); - button.innerHTML = '暂停暫停Pause'; - if (getValue('disabled')) { // 如果禁用 - document.title = _alert(-1, 'hvAutoAttack暂停中', 'hvAutoAttack暫停中', 'hvAutoAttack Paused'); - button.innerHTML = '继续繼續Continue'; - } - button.className = 'pauseChange'; - button.onclick = pauseChange; - } - function setPauseHotkey() { - if (!g('option').pauseHotkey) { - return; - } - document.addEventListener('keydown', (e) => { - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { - return; - } - if (e.keyCode === g('option').pauseHotkeyCode) { - pauseChange(); - } - }, false); - } + if ($RPN.test.isParam(ch)) { + let varName = ''; + while (i < expression.length && $RPN.test.param(expression[i])) { + varName += expression[i]; + i++; + } + tokens.push(varName); + lastTokenWasOperatorOrLeftParen = false; + continue; + } - function formatTime(t, size = 2) { - t = [t / _1h, (t / _1m) % 60, (t / _1s) % 60, (t % _1s) / 10].map(cdi => Math.floor(cdi)); - while (t.length > Math.max(size, g('option').encounterQuickCheck ? 2 : 3)) { // remove zero front - const front = t.shift(); - if (!front) { - continue; - } - t.unshift(front); - break; - } - return t; - } + throw new Error(`Unknown character: ${ch} from ${expression}`); + } - function getKeys(objArr, prop) { - let out = []; - objArr.forEach((_objArr) => { - out = prop ? out.concat(Object.keys(_objArr[prop])) : out.concat(Object.keys(_objArr)); - }); - out = out.sort(); - for (let i = 1; i < out.length; i++) { - if (out[i - 1] === out[i]) { - out.splice(i, 1); - i--; - } - } - return out; - } + return tokens; + }, - function time(e, stamp) { - const date = stamp ? new Date(stamp) : new Date(); - if (e === 0) { - return date.getTime(); - } if (e === 1) { - return `${date.getUTCMonth() + 1}/${date.getUTCDate()}`; - } if (e === 2) { - return `${date.getUTCFullYear()}/${date.getUTCMonth() + 1}/${date.getUTCDate()}`; - } if (e === 3) { - return date.toLocaleString(navigator.language, { - hour12: false, - }); - } - } + infixToPostfix: function (infixTokens) { + const output = []; + const stack = []; + for (const token of infixTokens) { + switch(true) { + case typeof token === 'number' || $RPN.test.isParam(token[0]): + output.push(token); + break; + case token === '(': + stack.push(token); + break; + case token === ')': + while (stack.length && stack[stack.length - 1] !== '(') { + output.push(stack.pop()); + } + stack.pop(); + break; + case $RPN.isOperator(token): + while ( + stack.length && + stack[stack.length - 1] !== '(' && + $RPN.hasHigherPrecedence(stack[stack.length - 1], token) && + $RPN.operators[token].func.length !== 1 + ) { + output.push(stack.pop()); + } + stack.push(token); + break; + default: + break; + } + if (!$RPN.isOperator(token) && stack.length && $RPN.isOperator(stack[stack.length - 1]) && $RPN.operators[stack[stack.length - 1]].func.length === 1) { + output.push(stack.pop()); + } + } - function gE(ele, mode, parent) { // 获取元素 - if (typeof ele === 'object') { - return ele; - } if (mode === undefined && parent === undefined) { - return (isNaN(ele * 1)) ? document.querySelector(ele) : document.getElementById(ele); - } if (mode === 'all') { - return (parent === undefined) ? document.querySelectorAll(ele) : parent.querySelectorAll(ele); - } if (typeof mode === 'object' && parent === undefined) { - return mode.querySelector(ele); - } + while (stack.length) { + output.push(stack.pop()); + } + + return output; + }, + + evaluatePostfix: function (postfixTokens, resolver) { + const stack = []; + for (const token of postfixTokens) { + if (typeof token === 'number') { + stack.push(token); + continue; + } + if (typeof token === 'string' && $RPN.test.isParam(token[0])) { + let value = resolver ? resolver(token) : token; + if (typeof value === 'string') { + if (value[0] !== `"` && value[0] != `'`) value = `'${value}'`; + else if (value[0] === `"` && value[value.length-1] === `"`) { + value = `'${value.slice(1, value.length - 1)}'`; + } + } + stack.push(value); + continue; + } + let a, b; + if ($RPN.operators[token].func.length === 1) { + a = stack.pop(); + b = undefined; + } + else if (stack.length < 2) { + if (token === '-') { + b = stack.pop(); + a = 0; + } else { + throw new Error('Wrong Expression.'); + } + } else { + b = stack.pop(); + a = stack.pop(); + } + + let result; + if (token in $RPN.operators) { + result = $RPN.operators[token].func(a, b); + } else { + throw new Error(`Unknow operator: ${token}`); + } + stack.push(result); + } + + if (stack.length !== 1) { + throw new Error('Wrong Expression.'); + } + + return stack[0]; + }, + + evaluate: function (expression, variableHandler = null) { + const tokens = $RPN.tokenize(expression); + const postfix = $RPN.infixToPostfix(tokens); + return $RPN.evaluatePostfix(postfix, variableHandler); + } + }; + return $RPN; } - function cE(name) { // 创建元素 - return document.createElement(name); + function initDebug() { + const $debug = { + Stack: class extends Error { + constructor(description, ...params) { + super(...params); + this.name = '$debug.Stack'; + } + }, + realtime: false, + logList: [], + maxLogCache: 50, + switchRealtimeLog: function () { + $debug.enableRealtimeLog($debug.realtime); + }, + enableRealtimeLog: function (enabled) { + $debug.realtime = enabled; + if (enabled) { + $debug.shiftLog(); + } + }, + log: function () { + if ($debug.realtime) { + console.trace(...arguments); + return; + } + $debug.logList.push({ + args: arguments, + stack: (new $debug.Stack()).stack + }); + if ($debug.logList.length > $debug.maxLogCache) { + $debug.logList.shift(); + } + }, + shiftLog: function () { + while ($debug.logList.length) { + const log = $debug.logList.shift(); + console.log(...log.args, `\n`, log.stack); + } + } + } + return $debug; } - function isOn(id) { // 是否可以施放技能/使用物品 - if (id * 1 > 10000) { // 使用物品 - return gE(`.bti3>div[onmouseover*="${id}"]`); - } // 施放技能 - return (gE(id) && gE(id).style.opacity !== '0.5') ? gE(id) : false; + function initAjax() { + const $ajax = { + debug: false, + interval: 300, // DO NOT DECREASE THIS NUMBER, OR IT MAY TRIGGER THE SERVER'S LIMITER AND YOU WILL GET BANNED + max: 4, + tid: null, + error: null, + conn: 0, + queue: [], + + insert: function (url, data, method, context = {}, headers = {}) { + return $ajax.fetch(url, data, method, context, headers, true); + }, + fetch: function (url, data, method, context = {}, headers = {}, isInsert = false) { + return new Promise((resolve, reject) => { + $ajax.add(method, url, data, resolve, reject, context, headers, isInsert); + }); + }, + open: function (url, data, method, context = {}, headers = {}) { + $ajax.fetch(url, data, method, context, headers).then(goto).catch( err => { console.error(err); }); + }, + openNoFetch: function (url, newTab) { + const newWindow = window.open(url, newTab ? '_blank' : '_self'); + if (!newTab && (!newWindow || newWindow.closed)) { + goto(url); + } + }, + repeat: function (count, func, ...args) { + const list = []; + range(count).forEach(_ => list.push(func(...args))); + return list; + }, + add: function (method, url, data, onload, onerror, context = {}, headers = {}, isInsert = false) { + method = !data ? 'GET' : method ?? 'POST'; + if (method === 'POST') { + headers['Content-Type'] ??= 'application/x-www-form-urlencoded'; + if (data && typeof data === 'object') { + data = Object.entries(data).map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&'); + } + } else if (method === 'JSON') { + method = 'POST'; + headers['Content-Type'] ??= 'application/json'; + if (data && typeof data === 'object') { + data = JSON.stringify(data); + } + } + context.onload = onload; + context.onerror = onerror; + if (isInsert) { + $ajax.queue.unshift({ method, url, data, headers, context, onload: $ajax.onload, onerror: $ajax.onerror }); + } else { + $ajax.queue.push({ method, url, data, headers, context, onload: $ajax.onload, onerror: $ajax.onerror }); + } + $ajax.next(); + }, + next: function () { + if (!$ajax.queue.length) { + return; + } + if ($ajax.tid) { + if (!$ajax.conn) { + clearTimeout($ajax.tid); + $ajax.tid = null; + $ajax.timer(); + $ajax.send(); + } + } else { + if ($ajax.conn < $ajax.max) { + $ajax.timer(); + $ajax.send(); + } + } + }, + getLast: function () { + const v = window.localStorage.getItem(_server.utils + '_last_post'); + return v === null ? undefined : JSON.parse(v); + }, + setLast: function (last) { + window.localStorage.setItem(_server.utils + '_last_post', JSON.stringify(last)); + }, + timer: function () { + function ontimer() { + const now = new Date().getTime(); + const last = $ajax.getLast(); + if (!last || now - last >= $ajax.interval) { + $ajax.next(); + $ajax.setLast(now); + return; + } + $ajax.tid = null; + $ajax.next(); + }; + $ajax.tid = setTimeout(ontimer, $ajax.interval); + }, + simplify: function (r) { + const info = {}; + info.url = r.url; + if (r.data) info.data = r.data; + if (r.method) info.method = r.method; + if (r.context && JSON.stringify(r.context) !== JSON.stringify({})) info.context = r.context; + if (r.headers && JSON.stringify(r.headers) !== JSON.stringify({})) info.headers = r.headers; + return info; + }, + send: function () { + const current = $ajax.queue.shift(); + GM_xmlhttpRequest(current); + $ajax.conn++; + if (!$ajax.debug) return; + const remain = $ajax.queue.map($ajax.simplify); + console.log('$ajax.send:', $ajax.simplify(current), ... remain?.length ? ['remain:', remain] : []); + }, + onload: function (r) { + $ajax.conn--; + const text = r.responseText; + if (r.status !== 200) { + $ajax.error = `${r.status} ${r.statusText}: ${r.finalUrl}`; + r.context.onerror?.(new Error($ajax.error)); + } else if (text === 'state lock limiter in effect') { + if ($ajax.error !== text) { + popup(`

${text}

Your connection speed is so fast that
you have reached the maximum connection limit.

Try again later.

`); + console.error(`${text}\nYour connection speed is so fast that you have reached the maximum connection limit. Try again later.`); + } + $ajax.error = text; + r.context.onerror?.(new Error($ajax.error)); + } else { + r.context.onload?.(text); + $ajax.next(); + } + }, + onerror: function (r) { + $ajax.conn--; + $ajax.error = `${r.status} ${r.statusText}: ${r.finalUrl}`; + r.context.onerror?.(new Error($ajax.error)); + $ajax.next(); + }, + }; + window.addEventListener('unhandledrejection', (e) => { console.error($ajax.error, e); }); + return $ajax; } - function setLocal(item, value) { - if (typeof GM_setValue === 'undefined') { - window.localStorage[`hvAA-${item}`] = (typeof value === 'string') ? value : JSON.stringify(value); - } else { - GM_setValue(item, value); - } + function initAsync() { + const $async = { + list: [], + logSwitchStrict: function (name, state) { try { + if (!state) { + $async.list.splice($async.list.indexOf(name), 1); + } else { + $async.list.push(name); + } + $debug.log(`${state ? 'Start' : 'End'} ${name}\n`, JSON.parse(JSON.stringify($async.list))); + } catch (err) { /* console.log(err) */ } }, + logSwitch: function (args) { try { + const argsStr = Array.from(args).join(','); + const name = `${args.callee.name}${argsStr === '' ? argsStr : `(${argsStr})`}`; + const state = $async.list.indexOf(name) === -1; + $async.logSwitchStrict(name, state); + } catch (err) { /* console.log(err) */ } } + } + return $async; } - function setValue(item, value) { // 储存数据 - if (!standalone.includes(item)) { - setLocal(item, value); - return value; - } - setLocal(`${current}_${item}`, value); - if (sharable.includes(item) && !getValue('option').optionStandalone) { - setLocal(`${other}_${item}`, value); + function formatTime(t, size = 2, quick) { + t = [t / _1h, (t / _1m) % 60, (t / _1s) % 60, (t % _1s) / 10].map(cdi => Math.floor(cdi)); + const option = getOption(); + while (t.length > Math.max(size, quick ? 2 : 3)) { // remove zero front + const front = t.shift(); + if (!front) { + continue; + } + t.unshift(front); + break; } - return value; + return t; } - function getLocal(item, toJSON) { - if (typeof GM_getValue === 'undefined' || !GM_getValue(item, null)) { - item = `hvAA-${item}`; - return (item in window.localStorage) ? ((toJSON) ? JSON.parse(window.localStorage[item]) : window.localStorage[item]) : null; - } - return GM_getValue(item, null); + function timeStr(time, size = 2, quick) { + let formated = formatTime(time, size, quick); + if (size) formated = formated.slice(0, size); + return formated.map(t => pad(t)).join(`:`); } - function getValue(key, toJSON) { // 读取数据 - if (!standalone.includes(key)) { - return getLocal(key, toJSON); - } - let otherWorldItem = getLocal(`${other}_${key}`); - // 将旧的数据迁移到新的数据 - if (!getLocal(`${current}_${key}`)) { - let itemExisted = getLocal(key); - if (!itemExisted && sharable.includes(key)) { - itemExisted = otherWorldItem; + function checkIsHV() { + if (window.location.host !== 'e-hentai.org') { + if (isMaintaining) { + // 维护中? 过一个小时再刷新 + (async function onwait() { try { + const body = document.body; + const blockTip = /Blocking requests for (\d+) seconds due to excessive request rate/; + let blocked = body.innerText?.match(blockTip)?.[1] * _1s; + const duration = isNaN(blocked) ? _1h : blocked; + const start = time(0); + let remain; + await until(() => { + remain = duration - time(0) + start; + document.title = `[M]${timeStr(remain)}`; + try { if (!isNaN(blocked)) { + body.innerText = body.innerText.replace(blockTip, (...args) => args[0].replace(args[1], remain)); + } } catch (err) { console.log(err) }; + return remain <= 0; + }); + goto(); + } catch (err) { console.error(err)} })(); + return true; } - if (!itemExisted) { - return null; // 若都没有该数据 + hvVersion = Version(...gE('script[src*="hvc.js"]', document)?.src.match(/z\/(\d+)(.*?)\/hvc.js/)?.slice(1, 3)); + setValue('url', window.location.origin); + monsterBuffSkillLib = setMonsterBuffSkillLib(); + + // 补充记录(因写入冲突、网络卡顿等)未被记录的encounter链接 + if (window.location.href.indexOf(`?s=Battle&ss=ba`) !== -1) { + const encounterURL = window.location.href?.split('/')[3]; + const encounter = getEncounter(); + const filtered = encounter.filter(e => e.url === encounterURL); + if (!filtered.length) { + encounter.unshift({ url: encounterURL, time: time(0), encountered: time(0) }); + } else { + filtered[0].encountered ??= time(0); + } + setEncounter(encounter); + if (!isInBattle()) { + backFromBattle(); + return; + } } - itemExisted = JSON.parse(JSON.stringify(itemExisted)); - setLocal(`${current}_${key}`, itemExisted); - delLocal(key); + + try { + if (window.location.href.startsWith('https://')) { + unsafeWindow.MAIN_URL = unsafeWindow.MAIN_URL.replace(/^http:/, `https:`); + } else { + unsafeWindow.MAIN_URL = unsafeWindow.MAIN_URL.replace(/^https:/, `http:`); + } + } catch (err) { /* console.log(err) */ } + + return true; } - if (Object.keys(excludeStandalone).includes(key)) { - otherWorldItem ??= getLocal(`${current}_${key}`) ?? {}; - for (let i of excludeStandalone[key]) { - otherWorldItem[i] = getLocal(`${current}_${key}`)[i]; + + setValue('lastEH', time(0)); + let encounter = getEncounter(); + const eventpane = gE('#eventpane'); + const now = time(0); + let url; + if (eventpane) { // 新一天或遭遇战 + url = gE('#eventpane>div>a')?.href.split('/')[3]; + if (url === undefined) encounter = []; // 新一天 + encounter.unshift({ url: url, time: now }); + setEncounter(encounter); + } else if (encounter.length) { + if (now - encounter[0]?.time > 0.5 * _1h) { // 延长最新一次的time, 避免因漏记录导致连续来回跳转 + encounter[0].time = now; + setEncounter(encounter); + } + for (let e of encounter) { + if (e.encountered || time(0) - e.time >= 30 * _1m) continue; + url = e.url; + break; } } - setLocal(`${other}_${key}`, otherWorldItem); - return getLocal(`${current}_${key}`); - } - function delLocal(key) { - if (typeof GM_deleteValue === 'undefined') { - window.localStorage.removeItem(`hvAA-${key}`); - return; + const isEngage = window.location.href === 'https://e-hentai.org/news.php?encounter'; + if (!url) { + if (isEngage && !getLocal('persistent_battle')) { + // 自动跳转,同时先刷新遭遇时间,延长下一次遭遇 + backFromBattle(); + } + return false; } - GM_deleteValue(key); + // 减少因在恒定世界处于战斗中时打开eh触发了遭遇而导致的错失 + // 缓存当前链接,等战斗结束时再自动打开,下次打开链接时: + // 1. 若新的遭遇未出现,进入已缓存的战斗链接 + // 2. 若新的遭遇已出现,则前一次已超时失效错过,重新获取新的一次 + if (!isEngage) { // 战斗外,非自动跳转 + if (eventpane) { + eventpane.style.cssText += 'color:red;'; // 链接标红提醒 + } + } else if (getLocal('persistent_battle')) { //战斗中 + if (eventpane) { + eventpane.style.cssText += 'color:gray;'; // 链接置灰提醒 + } + } else { // 战斗外,自动跳转 + let location = getLocal('url') ?? (document.referrer.match('hentaiverse.org') ? new URL(document.referrer).origin : 'https://hentaiverse.org'); + $ajax.openNoFetch(`${location.includes('https') ? 'https://' : 'http://'}${(location.includes('alt') || getOption().altBattleFirst) ? 'alt.' : ''}hentaiverse.org/${url}`); + } + return false; } - function delValue(key) { // 删除数据 - if (standalone.includes(key)) { - key = `${current}_${key}`; + // 答题// + async function riddleAlert() { try { + setAlarm('Riddle'); + const option = getOption(); + const answerTime = option.riddleAnswerTime; + let time; + const timeDiv = gE('#riddlecounter>div>div', 'all'); + while (time === undefined || time > answerTime) { + if (timeDiv.length === 0) { + await pauseAsync(_1s); + continue; + } + time = undefined; + for (let t of timeDiv) { + time = (t.style.backgroundPosition.match(/(\d+)px$/)[1] / 12).toString() + (time ?? ''); + } + time *= 1; + document.title = time; + await pauseAsync(_1s); } - if (typeof key === 'string') { - delLocal(key); + for (let ans of gE('#riddler1>*', 'all').children) { + if (!ans.children[0].children[0].checked) continue; + gE('#riddlesubmit').click(); return; } - if (typeof key !== 'number') { - return; + if (!option.riddleAnswerChoose) return; + // if no answer selected + const answers = ['aj', 'fs', 'pp', 'ra', 'rd', 'ts'].sort(Math.random); + const answer = `riddlesubmit=Submit+Answer` + answers.slice(0, Math.max(0, Math.min(6, option.riddleAnswerChoose))).map(ans => `&riddleanswer[]=${ans}`).join(''); + const battle = gE('#battle_main', $doc(await $ajax.fetch(window.location.href, answer))); + if (!battle) console.error('ERROR: Failed fetch submit.'); + goto(); + } catch (err) { console.error(err); }} + + function checkIsWindowTop() { + const currentUrl = window.self.location.href; + if (!isFrame) { + checkOption(); + if (!getOption().riddlePopup || gE('#riddlecounter')) { // 未开启使用弹窗或仍处于答题 + return true; + } + if (!window.opener || window.opener === window.self || window.opener.closed) { // 没有仍存在的opener + return true; + } + try { + if (!isInBattle(window.opener.document)) { // opener不处于战斗或答题中 + return true; + } + } catch (err) { + console.error(err); + return true; + } + try { + window.opener.location.href = currentUrl; + } catch (err) { + console.error(err); + console.error(`current: ${currentUrl}`); + console.error(`opener: ${window.opener}`); + console.error(`opener.location: ${window.opener.location}`); + console.error(`opener.location.href: ${window.opener.location.href}`); + window.opener.location.href = window.opener.location.href; + } + const isFirefox = typeof InstallTrigger !== 'undefined'; + tryClose(3, isFirefox ? 500 : 300); + return false; } - const itemMap = { - 0: ['disabled'], - 1: ['battle', 'battleCode'], + + if (isInBattle()) { + if (!window.top.location.href.endsWith(`?s=Battle`)) { + setValue('lastUrl', window.top.location.href); + } + window.top.location.href = currentUrl; + return false; } - for (let item of itemMap[key]) { - delValue(item); + if (currentUrl.match(/\?s=Battle&ss=(ar|rb)/)) { + checkOption(); + setArenaDisplay(); } + return false; } - function goto() { // 前进 - window.location.href = window.location; - setTimeout(goto, 5000); + async function safeClose(delay) { + try { window.close() } catch (err) { /* console.log(err) */ } + await pauseAsync(delay); + return !window || window.closed; } - function gotoAlt() { - const hv = 'hentaiverse.org'; - const alt = 'alt.' + hv; - if(window.location.host === hv) { - window.location.href = window.location.href.replace(`://${hv}`, `://${alt}`) - } else if (window.location.host === alt) { - window.location.href = window.location.href.replace(`://${alt}`, `://${hv}`) + + async function tryClose(attempts, delay) { try { + await pauseAsync(delay); + window.opener = null; + window.open('', '_self'); + if (await safeClose(delay)) return; + window.location.href = 'about:blank'; + if (await safeClose(delay)) return; + attempts--; + if (attempts <= 0) { + document.body.innerHTML = '
Auto close popup failed. Please manually close window.
'; + return; } + await tryClose(attempts, delay); + } catch (err) { console.error('Opener reload or popup close failed:', err) } } + + function getOption(unstable) { + return typeof GM_getValue === 'undefine' ? {} : (unstable ? g().option : g().stableOption) ?? {}; } - function g(key, value) { // 全局变量 - const hvAA = window.hvAA || {}; - if (key === undefined && value === undefined) { - return hvAA; - } if (value === undefined) { - return hvAA[key]; + + function checkOption() { + g('version', scriptVersion); + if (!getValue('option')) { + g('lang', window.prompt('请输入以下语言代码对应的数字\nPlease put in the number of your preferred language (0, 1 or 2)\n0.简体中文\n1.繁體中文\n2.English', 0) || 2); + addStyle(); + UI.alert('请设置hvAutoAttack', '請設置hvAutoAttack', 'Please config this script'); + gE('.hvAAButton').click(); + return false; } - hvAA[key] = value; - window.hvAA = hvAA; - return window.hvAA[key]; + + let option = loadOption(); + g('option', (isFrame || onIsekaiEncounter) ? option : setValue('option', option)); + writePortables(); + option = getOption(true); + g('lang', option.lang || '0'); + addStyle(); + if (onIsekaiEncounter) return; + g('stableOption', getOption(true)); + + // README等合并到主分支后再取消掉注释 + // if (option.version.substr(0, 4) !== scriptVersion.ver.substr(0, 4)) { + // gE('.hvAAButton').click(); + // if (UI.confirm('hvAutoAttack版本更新,请重新设置\n强烈推荐【重置设置】后再设置。\n是否查看更新说明?', 'hvAutoAttack版本更新,請重新設置\n強烈推薦【重置設置】後再設置。\n是否查看更新說明?', 'hvAutoAttack version update, please reset\nIt\'s recommended to reset all configuration.\nDo you want to read the changelog?')) { + // $ajax.openNoFetch('https://github.com/dodying/UserJs/commits/master/HentaiVerse/hvAutoAttack/hvAutoAttack.user.js', true); + // } + // gE('.hvAAReset').focus(); + // return false; + // } + + if (gE('[class^="c5"], [class^="c4"]') && UI.confirm('请设置字体\n使用默认字体可能使某些功能失效\n是否查看相关说明?', '請設置字體\n使用默認字體可能使某些功能失效\n是否查看相關說明?', 'Please set the font\nThe default font may make some functions fail to work\nDo you want to see instructions?')) { + $ajax.openNoFetch(`https://github.com/dodying/UserJs/blob/master/HentaiVerse/hvAutoAttack/README${g().lang === '2' ? '_en.md#about-font' : '.md#关于字体的说明'}`, true); + return false; + } + return true; } - function objArrSort(key) { // 对象数组排序函数,从小到大排序 - return function (obj1, obj2) { - return (obj2[key] < obj1[key]) ? 1 : (obj2[key] > obj1[key]) ? -1 : 0; - }; + function writePortables() { + const option = getOption(true); + if (!option.portable) return; + for (const key of dataFlags.portable) { + if (!(Object.keys(option.portable).includes(key))) continue; + setValue(key, getValue(key), true); + } } - function objSort(obj) { // 对象排序 - const objNew = {}; - const arr = Object.keys(obj).sort(); - arr.forEach((key) => { - objNew[key] = obj[key]; - }); - return objNew; + function backFromBattle() { + const beforeEncounter = getValue('beforeEncounter'); + if (beforeEncounter) { + setValue('lastUrl', beforeEncounter); + delValue('beforeEncounter'); + } + $ajax.openNoFetch(getValue('lastUrl')); } - function _alert(func, l0, l1, l2, answer) { - const lang = [l0, l1, l2][g('lang')]; - if (func === -1) { - return lang; - } if (func === 0) { - window.alert(lang); - } else if (func === 1) { - return window.confirm(lang); - } else if (func === 2) { - return window.prompt(lang, answer); + function onRiddle() { + if (!gE('#riddlecounter')) { + return false; + } + setValue('onriddle', true); + (window.opener ?? window).console.log('onriddle', { riddlePopup: getOption().riddlePopup, opener: window.opener }); + if (!getOption().riddlePopup || window.opener) { + riddleAlert(); + return true; } + window.open(window.location.href, 'riddleWindow', 'resizable, scrollbars, width=1241, height=707'); + return true; } - function addStyle(lang) { // CSS - const langStyle = gE('head').appendChild(cE('style')); - langStyle.className = 'hvAA-LangStyle'; - langStyle.textContent = `l${lang}{display:inline!important;}`; - if (/^[01]$/.test(lang)) { - langStyle.textContent = `${langStyle.textContent}l01{display:inline!important;}`; + function onIdle() { + if (!gE('#navbar')) { + return false; } - const globalStyle = gE('head').appendChild(cE('style')); - const cssContent = [ - // hvAA - 'l0,l1,l01,l2{display:none;}', // l0: 简体 l1: 繁体 l01:简繁体共用 l2: 英文 - '#hvAABox2{position:absolute;left:1075px;padding-top: 6px;}', - '.hvAALog{font-size:20px;}', - '.hvAAPauseUI{top:30px;left:1246px;position:absolute;z-index:9999}', - '.hvAAButton{top:5px;left:1255px;position:absolute;z-index:9999;cursor:pointer;width:24px;height:24px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAADi0lEQVRIiZVWPYgUZxj+dvGEk7vsNdPYCMul2J15n+d991PIMkWmOEyMyRW2FoJIUojYp5ADFbZJkyISY3EqKGpgz+Ma4bqrUojICaIsKGIXSSJcsZuD3RT3zWZucquXDwYG5n2f9/d5vnFuHwfAZySfAXgN4DXJzTiOj+3H90OnkmXZAe/9FMm3JJ8AuBGepyRfle2yLDvgnKt8EDVJkq8B3DGzjve+1m63p0n2AVzJbUh2SG455yre+5qZ/aCq983sxMfATwHYJvlCVYckHwFYVdURgO8LAS6RHJJcM7N1VR0CeE5yAGBxT3AR+QrA3wA20tQOq+pFkgOS90Tk85J51Xs9qaorqjoAcC6KohmSGyQHcRx/kbdv7AHgDskXaWqH0zSddc5Voyia2SOXapqmswsLvpam6ez8/Pwn+YcoimYAvARw04XZ5N8qZtZR1aGqXnTOVSd0cRd42U5EzqvqSFWX2u32tPd+yjnnXNiCGslHJAf7ybwM7r2vAdgWkYdZls157w+NK/DeT7Xb7WkAqyTvlZHjOD5oxgtmtqrKLsmze1VJsquqKwsLO9vnnKvkJHpLsq+qo/JAd8BtneTvqvqTiPwoIu9EZKUUpGpmi2Y2UtU+yTdJkhx1JJ8FEl0pruK/TrwA4F2r1WrkgI1G4wjJP0XkdLF9WaZzZnZZVa8GMj5xgf43JvXczFZbLb1ebgnJn0nenjQbEVkG0JsUYOykyi6Aa+XoQTJuTRr8OADJzVBOh+SlckYkz5L8Q0TquXOj0fhURN6r6pkSeAXAUsDaJPnYxXF8jOQrklskh97ryZJTVURWAPwF4DqAX0TkvRl/zTKdK2aeJMnxICFbAHrNZtOKVVdIrrVa2t1jz6sicprkbQC3VPVMGTzMpQvgQY63i8lBFddVdVCk/6TZlMFzopFci+P44H+YHCR3CODc/wUvDPY7ksMg9buZrKr3ATwvyoT3vrafzPP3er1eA9Azs7tjJhcqOBHkeSOKohkROR9K7prZYqnnlSRJjofhb4vIt/V6vUbyN1Xtt1qtb1zpZqs45xyAxXAnvCQ5FJGHqrpiZiMzu5xnHlZxCOABybXw3gvgp/Zq3/gA+BLATVVdyrJsbods2lfVq7lN4crMtapjZndD5pPBixWFLTgU7uQ3AJ6KyLKILAdy9sp25bZMBC//JSRJcjQIYg9Aj+TjZrNp+/mb+Ad711sdZZ1k/QAAAABJRU5ErkJggg==) center no-repeat transparent;}', - '#hvAABox{left:calc(50% - 350px);top:50px;font-size:16px!important;z-index:4;width:700px;height:538px;position:absolute;text-align:left;background-color:#E3E0D1;border:1px solid #000;border-radius:10px;font-family:"Microsoft Yahei";}', - '.hvAATablist{position:relative;left:14px;}', - '.hvAATabmenu{position:absolute;left:-9px;}', - '.hvAATabmenu>span{display:block;padding:5px 10px;margin:0 10px 0 0;border:1px solid #91a7b4;border-radius:5px;background-color:#E3F1F8;color:#000;text-decoration:none;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;cursor:pointer;}', - '.hvAATabmenu>span:hover{left:-5px;position:relative;color:#0000FF;z-index:2!important;}', - '.hvAATabmenu>span>input{margin:0 0 0 -8px;}', - '.hvAATab{position:absolute;width:605px;height:430px;left:36px;padding:15px;border:1px solid #91A7B4;border-radius:3px;box-shadow:0 2px 3px rgba(0,0,0,0.1);color:#666;background-color:#EDEBDF;overflow:auto;}', + // 战斗结束跳转回原链接 + if (window.top.location.href.endsWith(`?s=Battle`)) { + backFromBattle(); + return true; + } + const arena = getValue('arena', true) ?? {}; + delete arena?.equip; + arena.postoken = gE('input[name="postoken"]')?.value ?? arena.postoken; + setValue('arena', arena); + + if (window.location.href.indexOf(`?s=Battle&ss=ba`) === -1) { // 不缓存encounter + setValue('lastUrl', window.top.location.href); // 缓存进入战斗前的页面地址 + setArenaDisplay(); + } + delValue(1); + const option = getOption(); + if (option.showQuickSite && option.quickSite) { + quickSite(); + } + const hvAAPauseUI = document.body.appendChild(cE('div')); + hvAAPauseUI.classList.add('hvAAPauseUI'); + setPauseUI(hvAAPauseUI); + asyncOnIdle(); + return true; + } + + function onBattleBox() { + let box = gE('#hvAABox2'); + if (box) return box; + box = gE('#battle_main').appendChild(cE('div')); + box.id = 'hvAABox2'; + setPauseUI(box); + return box; + } + + function onBattle() { + if (!gE('#textlog')) { + return false; + } + checkResponsive(); + + if (getValue('onriddle')) { + window.history.replaceState(null, '', window.location.href); + delValue('onriddle'); + } + onBattleBox(); + reloader(); + const option = getOption(); + g('attackStatus', option.attackStatus); + // 1二天 2单手 3双手 4双持 5法杖 + range(5).map(s => s + 1).filter(s => gE(`2${s}01`)).forEach(s => g('fightingStyle', s.toString())); + g('timeNow', time(0)); + g('runSpeed', 1); + newRound(false); + updateMonsterEffects(false); + onBattleRound(); + const battle = g().battle; + if (option.recordEach) { + let code = getValue('battleCode', true); + const tokens = { token: document.body.innerHTML.match(`var battle_token = \"(.*)\";`)[1], postoken: battle?.postoken }; + const same = Object.keys(tokens).map(k => tokens[k] === code?.[k]).every(s => s); + if (!same || !code?.roundAll || !code?.roundNow) { + const now = same ? code?.time ?? time(1) : time(1); + const roundType = battle?.roundType?.toUpperCase(); + const [roundAll, roundNow] = [battle?.roundAll, battle?.roundNow]; + code = { + ...tokens, + time: now, + roundType, roundAll, roundNow, + name: `${now}: ${roundType}-${roundAll}`, + }; + setValue('battleCode', code); + } + } + updateEncounter(_server.isekai && option.encounter); + return true; + } + + // ----------methods---------- + // 通用// + function unique(arr) { + const newArr = []; + for (const i of range(arr)) { + if (newArr.indexOf(arr[i]) === -1) { + newArr.push(arr[i]); + } + } + return newArr; + } + + function object2Order(orderValue, ...args) { + switch (typeof orderValue) { + case 'string': + return orderValue?.split(',') ?? []; + case 'number': + case 'boolean': + return [orderValue]; + case 'function': + return object2Order(orderValue(...args)); + case 'object': + case 'undefined': + if (!orderValue) return []; // null or undefined + if (Array.isArray(orderValue)) return orderValue; + break; + default: + break; + } + throw new Error('Unsupported typeof orderValue:', orderValue, typeof orderValue); + } + + function getDefaultOrder(idMatch, map) { + const defaultOrder = g().defaultOrder ??= {}; + const key = idMatch + (map?.toString() ?? ''); + return (defaultOrder[key] ??= [...gE(`[id^="${idMatch}_"]`, 'all')].map(map ?? (ord => ord.id.match(/_(.*)/)[1]))); + } + + function splitOrders(orderValue, defaultOrder, ...args) { + return unique(object2Order(orderValue, ...args).concat(defaultOrder ?? []).map(v => isNaN(v * 1) ? v : v * 1)); + } + + function goto(url) { // 前进 + window.location.href = url ?? (window.location.search ? window.location.pathname + window.location.search : window.location.href); + setTimeout(goto, 5 * _1s); + setTimeout(() => { window.location.href = window.location.href }, 10 * _1s); + return true; + } + + function gotoAlt(isAltOnly) { + const hv = 'hentaiverse.org'; + const alt = 'alt.' + hv; + const current = window.location.href; + let next = current; + if (window.location.host === hv) { + next = current.replace(`://${hv}`, `://${alt}`); + } else if (window.location.host === alt) { + next = isAltOnly ? current : current.replace(`://${alt}`, `://${hv}`); + } + $ajax.openNoFetch(next); + return true; + } + + function pauseAsync(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + async function until(condition, delay){ try { + let result; + while (!(result = await condition())) await pauseAsync(delay); + return result; + } catch (err) { console.error(err); }} + + async function waitPause(ms) { try { + return await until(() => !getValue('disabled'), ms); + } catch (err) { console.error(err); }} + + function setTimeoutOrExecute(resolve, ms) { + if (ms) { + setTimeout(resolve, ms); + return; + } + resolve(); + } + + function pad(num, pad = '0', total = 2) { + return num.toString().padStart(total, pad); + } + + function gE(ele, mode, parent) { // 获取元素 + if (typeof ele === 'object') { + return ele; + } if (mode === undefined && parent === undefined) { + return (isNaN(ele * 1)) ? document.querySelector(ele) : document.getElementById(ele); + } if (mode === 'all') { + return (parent === undefined) ? document.querySelectorAll(ele) : parent.querySelectorAll(ele); + } if (typeof mode === 'object' && parent === undefined) { + return mode.querySelector(ele); + } + } + + function cE(name) { // 创建元素 + return document.createElement(name); + } + + function $doc(h) { + const doc = document.implementation.createHTMLDocument(''); + doc.documentElement.innerHTML = h; + return doc; + } + + function popup(text) { + if (!getOption().popup) return; + const popupWindow = cE('div'); + popupWindow.style.cssText += 'position:fixed;top:0;left:0;width:100%;height:100%;background-color:#0006;z-index:1001;cursor:pointer;display:flex;justify-content:center;align-items:center;' + popupWindow.addEventListener('click', r); + document.body.appendChild(popupWindow); + const display = cE('div'); + display.innerText = text; + display.style.cssText += 'min-width:400px;min-height:100px;max-width:100%;max-height:100%;padding:10px;background-color:#fff;border:1px solid;display:flex;flex-direction:column;justify-content:center;font-size:10pt;color:#333;'; + popupWindow.appendChild(display); + document.addEventListener('keydown', r); + return display; + + function r(e) { + switch(true) { + case e.key?.length >= 2 && e.key?.includes('F'): return; + case e.ctrlKey: return; + default: break; + } + e.preventDefault(); + e.stopImmediatePropagation(); + if (e.button !== 0 && !['Enter', ' ', 'Escape'].includes(e.key)) { + return; + } + popupWindow.remove(); + document.removeEventListener('keydown', r); + } + } + + function setArenaDisplay() { + const option = getOption(); + if (!option.obscureNotIdleArena) { + return; + } + if (window.location.href.indexOf(`?s=Battle&ss=ar`) === -1 && window.location.href.indexOf(`?s=Battle&ss=rb`) === -1) { + return; + } + const ar = splitOrders(option.idleArenaValue).map(String); + if (ar.length === 0) { + return; + } + getStartBattleButtons().forEach(btn => { + if (ar.includes(btn.id) && btn.cleared && !option.arLevelDisable[btn.id]) { + return; + } + gE('div', 'all', btn.closest('#arena_list tr')).forEach(div => { div.style.cssText += `color:${btn.cleared?'grey':'red'}!important;` }); + }); + } + + function getStartBattleButtons(doc = undefined, site = undefined) { + const idMap = { + ar: { 1: 1, 10: 3, 20: 5, 30: 8, 40: 9, 50: 11, 60: 12, 70: 13, 80: 15, 90: 16, 100: 17, 110: 19, 120: 20, 130: 21, 140: 23, 150: 24, 165: 26, 180: 27, 200: 28, 225: 29, 250: 32, 300: 33, 400: 34, 500: 35 }, + rb: [105, 106, 107, 108, 109, 110, 111, 112], + } + const option = getOption(true); + doc ??= document; + site ??= doc.location.href.match(/\?s=Battle\&ss=(.*)/)[1]; + const buttons = gE(`img[src*="startchallenge.png"], img[src*="startgrindfest.png"], img[src*="startchallenge_d.png"]`, 'all', doc); + buttons.forEach(btn => { + const tr = btn.closest('#arena_list tr'); + if (btn.enabled = 'challenge_d' !== btn.getAttribute('src').match(`${unsafeWindow.IMG_URL}(.*)/start(.*).png`)[2]) { + const onclick = btn.getAttribute('onclick'); + const match = onclick.match(/init_battle\((\d+)(,\d+)*\)/); + btn.id = site === 'gr' ? 'gr' : match[1] * 1; + } else { + const key = site === 'ar' ? gE('td:nth-child(3)>div>div', tr).innerText.match(`Lv. (.*)`)[1]*1 : (Array.from(tr.parentNode.children).indexOf(tr)-1); + btn.id = idMap[site][key]; + } + btn.cleared = site === 'gr' || gE('td:nth-child(2)>div>div', tr).innerText; + if (option.skipUnclearedArena && site !== 'gr') { + btn.cleared = btn.cleared !== '-'; + } + }); + return buttons; + } + + function Version(...verArgs) { + if (!(this instanceof Version)) { + return new Version(...verArgs); + } + this.ver = verArgs.join('.'); + + Version.prototype.upto ??= function(...args) { + return this.compareWith(...args) >= 0; + } + Version.prototype.eq ??= function(...args) { + return this.compareWith(...args) === 0; + }; + Version.prototype.compareWith ??= function(...args) { + return Version.compare(this, Version.asString(...args)); + } + Version.asString ??= function(...args) { + return args[0] instanceof Version ? args[0].ver : args.join('.'); + } + Version.compare ??= function(...args) { + const seg = args.slice(0, 2).map(arg => { + if (typeof arg === 'number') arg = String(arg); + if (arg instanceof Version) arg = arg.ver; + return arg.split('.'); + }); + const maxLen = Math.max(...seg.map(s => s.length)); + + for (const i of range(maxLen)) { + const si = seg.map(s => s[i]); + + const isEmpty = si.map(s => [undefined, ''].includes(s)); + + if (!isEmpty.some(x => !x)) continue; + if (isEmpty[0]) return -1; + if (isEmpty[1]) return 1; + + const isNum = si.map(s => /^\d+$/.test(s)); + if (isNum.some(x => !x)) { + if (!isNum[0] && isNum[1]) return -1; + if (isNum[0] && !isNum[1]) return 1; + + if (si[0] < si[1]) return -1; + if (si[0] > si[1]) return 1; + continue; + } + const trim = si.map(s => s.replace(/^0+/, '') || '0'); + const length = trim.map(t => t.length); + if (length[0] !== length[1]) return length[0] > length[1] ? 1 : -1; + for (const j of range(length[0])) { + if (trim[0][j] !== trim[1][j]) return trim[0][j] > trim[1][j] ? 1 : -1; + } + } + return 0; + } + } + + function loadOption(option) { + option ??= getValue('option', true); + const version = Version(option.version); + + if (!version.upto(scriptVersion)) { // 脚本升级时备份 + const backups = getValue('backup', true) || {}; + const autos = Object.values(backups).filter(b => b.auto && b.server === _server.name).sort((a, b) => -Version.compare(a.version, b.version)); + if (!Version(autos[0]?.version).upto(version)) backup(); + } + + // 迁移2.91.9及之前的权重背景配置 + if (option.weightBackground && Object.values(option.weightBackground).some(Array.isArray)) { + option.weightBackground = Object.fromEntries(Object.entries(option.weightBackground).map(([k, w]) => [(k * 1 + 9) % 10, w[0]])); + } + + // 迁移2.90.162及之前的targetHp等到targetHpDecimal等 + if (!version.upto(2, 90, 162)) { + option = JSON.parse(JSON.stringify(option).replace('targetHp', 'targetHpDecimal').replace('targetMp', 'targetMpDecimal').replace('targetSp', 'targetSpDecimal').replace('DecimalDecimal', 'Decimal')); + option.version = scriptVersion.ver; + g('version', option.version); + } + option = JSON.parse(JSON.stringify(option).replace('DecimalDecimal', 'Decimal')); + + // 迁移2.90.168及之前的channelSkill2Order_Cure的Name错误 + option.channelSkill2Order_Cure = option.channelSkill2Order_Cu; + delete option.channelSkill2Order_Cu; + option.channelSkill2OrderName = option.channelSkill2OrderName?.replace('Cu', 'Cure').replace('Curere', 'Cure'); + // 迁移2.90.178及之前的debuff警报设置 + if (option.debuffSkillTurnAlert === true) { + option.debuffSkillTurnAlert = 1; + } + + const legacies = { // current <= legacy + 'debuffSkillImAll': 'debuffSkillAllIm', + 'debuffSkillWeAll': 'debuffSkillAllWk', + 'debuffSkillAllImCondition': 'debuffSkillImpCondition', + 'debuffSkillAllWeCondition': 'debuffSkillWkCondition', + 'debuffSkillImAllCondition': 'debuffSkillAllImCondition', + 'debuffSkillWeAllCondition': 'debuffSkillAllWeCondition', + 'battleUnresponsive_Alert': 'delayAlert', + 'battleUnresponsive_Reload': 'delayReload', + 'battleUnresponsive_Alt': 'delayAlt', + 'battleUnresponsiveTime_Alert': 'delayAlertTime', + 'battleUnresponsiveTime_Reload': 'delayReloadTime', + 'battleUnresponsiveTime_Alt': 'delayAltTime', + } + for (let key in legacies) { + const array = key.split('_'); + const legacy = legacies[key]; + const data = option[legacy]; + if (!data) continue; + if (array.length === 1) { + option[key] ??= data; + } else { + (option[array[0]] ??= {})[array[1]] ??= data; + } + delete option[legacy]; + } + // 迁移旧版本最后的慈悲条件为可配置条件 + const mercifulBlowCondition = option.skillT3Condition ?? { "0": [] }; + const size = Object.keys(mercifulBlowCondition).length; + if (option.mercifulBlowStrict) { + option.mercifulBlow = false; + option.mercifulBlowStrict = false; + for (let id in mercifulBlowCondition) { + const condition = mercifulBlowCondition[id]; + condition.push("fightingStyle,5,2"); + condition.push("targetHp,2,0.25"); + condition.push("_targetBuffTurn_bleed,1,0"); + } + } else if (option.mercifulBlow) { + option.mercifulBlow = false; + const newCondition = {}; + for (let id in mercifulBlowCondition) { + const condition = mercifulBlowCondition[id]; + newCondition[id] = condition; + newCondition[(id * 1 + size).toString()] = [...condition]; + newCondition[(id * 1 + size).toString()].push("fightingStyle,6,2"); + condition.push("fightingStyle,5,2"); + condition.push("_targetHp,2,0.25"); + condition.push("_targetBuffTurn_bleed,1,0"); + } + option.skillT3Condition = newCondition; + } + return option; + } + + function setPauseUI(parent) { + setPauseButton(parent); + setPauseHotkey(); + setStepInButton(parent); + setStepInHotkey(); + setAltButton(parent); + setAltHotkey(); + } + + function setPauseButton(parent) { + const option = getOption(); + if (!option.pauseButton) { + return; + } + const button = parent.appendChild(cE('button')); + button.innerHTML = UI.button.pause(); + if (getValue('disabled')) { // 如果禁用 + document.title = titlePause(); + button.innerHTML = UI.button.continue(); + } + button.className = 'pauseChange'; + button.onclick = pauseChange; + } + + function setPauseHotkey() { + const option = getOption(); + if (!option.pauseHotkey) { + return; + } + document.addEventListener('keydown', (e) => { + if (e.target.tagName.toUpperCase() === 'INPUT' || e.target.tagName.toUpperCase() === 'TEXTAREA') { + return; + } + if (e.keyCode === option.pauseHotkeyCode) { + pauseChange(); + } + }, false); + } + + function setStepInButton(parent) { + const option = getOption(); + if (!option.stepInButton) { + return; + } + const button = parent.appendChild(cE('button')); + button.innerHTML = UI.button.stepIn(); + button.className = 'stepIn'; + button.onclick = stepIn; + } + + function setStepInHotkey() { + const option = getOption(); + if (!option.stepInHotkey) { + return; + } + document.addEventListener('keydown', (e) => { + if (e.target.tagName.toUpperCase() === 'INPUT' || e.target.tagName.toUpperCase() === 'TEXTAREA') { + return; + } + if (e.keyCode === option.stepInHotkeyCode) { + stepIn(); + } + }, false); + } + + function setAltButton(parent) { + const option = getOption(); + if (!option.altButton) { + return; + } + const button = parent.appendChild(cE('button')); + button.innerHTML = (window.location.host.includes('alt') ? `ExitAlt` : `ToAlt`) + `${(option.altHotkey && option.altHotkeyStr) ? `(${option.altHotkeyStr})` : '' }`; + button.className = 'gotoAlt'; + button.onclick = () => gotoAlt(); + } + + function setAltHotkey() { + const option = getOption(); + if (!option.altHotkey) { + return; + } + document.addEventListener('keydown', (e) => { + if (e.target.tagName.toUpperCase() === 'INPUT' || e.target.tagName.toUpperCase() === 'TEXTAREA') { + return; + } + if (e.keyCode === option.altHotkeyCode) { + gotoAlt(); + } + }, false); + } + + function getKeys(objArr, prop) { + let out = []; + objArr.forEach((_objArr) => { + out = !_objArr ? out :(prop && _objArr[prop]) ? out.concat(Object.keys(_objArr[prop])) : out.concat(Object.keys(_objArr)); + }); + out = out.sort(); + for (let i = 1; i < out.length; i++) { + if (out[i - 1] === out[i]) { + out.splice(i, 1); + i--; + } + } + return out; + } + + function time(e, stamp) { + const date = stamp ? new Date(stamp) : new Date(); + if (e === 0) { + return date.getTime(); + } if (e === 1) { + return `${date.getUTCMonth() + 1}/${date.getUTCDate()}`; + } if (e === 2) { + return `${date.getUTCFullYear()}/${date.getUTCMonth() + 1}/${date.getUTCDate()}`; + } if (e === 3) { + return date.toLocaleString(navigator.language, { + hour12: false, + }); + } + } + + function onRestoredBattleServer(key, method, ...addition) { + const isSwitch = [...dataFlags.battleDatas, ...addition].includes(key) && onIsekaiEncounter; + if (isSwitch) switchCurrent(true); + const result = method(); + if (isSwitch) switchCurrent(true); + return result; + } + + function setLocal(key, value, isLocalStroage) { + if (JSON.stringify(getLocal(key, isLocalStroage)) === JSON.stringify(value)) { + return; + } + if (typeof GM_setValue === 'undefined' || isLocalStroage) { + window.localStorage[`hvAA-${key}`] = (typeof value === 'string') ? value : JSON.stringify(value); + } else { + GM_setValue(key, value); + } + } + + function setValue(key, value, portable) { // 储存数据 + const isLocalStorage = dataFlags.local.includes(key) && !portable; + if (!dataFlags.standalone.includes(key)) { + setLocal(key, value, isLocalStorage); + return value; + } + onRestoredBattleServer(key, () => { + setLocal(`${_server.name}_${key}`, value, isLocalStorage); + if (!dataFlags.sharable.includes(key) || getValue('option').optionStandalone) return; + setLocal(`${_server.other}_${key}`, value, isLocalStorage); + }, 'option'); + return value; + } + + function getLocal(key, isLocalStorage, toJSON) { + let value; + if (isLocalStorage || typeof GM_getValue === 'undefined' || !GM_getValue(key, null)) { + key = `hvAA-${key}`; + value = window.localStorage[key]; + return toJSON ? JSONParse(value) : value; + } + value = GM_getValue(key, null); + if (!isLocalStorage) { + return value; + } + key = `hvAA-${key}`; + if (!(key in window.localStorage)) { + return value; + } + value = window.localStorage[key]; + value = toJSON ? JSONParse(value) : value; + return value; + + function JSONParse(object) { + if (object === undefined || object === '') { + return object; + } + return JSON.parse(object) + } + } + + function getValue(key, toJSON) { // 读取数据 + const isLocalStorage = dataFlags.local.includes(key); + if (!dataFlags.standalone.includes(key)) { + return getLocal(key, isLocalStorage, toJSON); + } + return onRestoredBattleServer(key, () => { + let otherWorldItem = getLocal(`${_server.other}_${key}`, isLocalStorage); + // 将旧的数据迁移到新的数据 + + if (!getLocal(`${_server.name}_${key}`, isLocalStorage)) { + let itemExisted = getLocal(key, isLocalStorage); + if (!itemExisted && dataFlags.sharable.includes(key)) { + itemExisted = otherWorldItem; + } + if (!itemExisted) return null; // 若都没有该数据 + itemExisted = JSON.parse(JSON.stringify(itemExisted)); + setLocal(`${_server.name}_${key}`, itemExisted); + delLocal(key, isLocalStorage); + } + if (Object.keys(dataFlags.excludeStandalone).includes(key)) { + otherWorldItem ??= getLocal(`${_server.name}_${key}`, isLocalStorage) ?? {}; + for (let i of dataFlags.excludeStandalone[key]) { + otherWorldItem[i] = getLocal(`${_server.name}_${key}`, isLocalStorage)[i]; + } + } + setLocal(`${_server.other}_${key}`, otherWorldItem); + return getLocal(`${_server.name}_${key}`, isLocalStorage, toJSON); + }); + } + + function delLocal(key, isLocalStorage) { + if (typeof GM_deleteValue === 'undefined') { + window.localStorage.removeItem(`hvAA-${key}`); + return; + } + if (isLocalStorage) { + window.localStorage.removeItem(`hvAA-${key}`); + } + GM_deleteValue(key); + } + + function delValue(key, portable) { // 删除数据 + const isLocalStorage = portable ? false : dataFlags.local.includes(key); + if (dataFlags.standalone.includes(key)) { + onRestoredBattleServer(key, () => { + key = `${_server.name}_${key}`; + }, 'option'); + } + if (typeof key === 'string') { + delLocal(key, isLocalStorage); + return; + } + if (typeof key !== 'number') { + return; + } + const itemMap = { + 0: ['disabled'], + 1: ['battle', 'battleCode'], + } + for (let item of itemMap[key]) { + delValue(item, portable); + } + } + + function g(key, value) { // 全局变量 + const hvAA = window.hvAA || {}; + if (key === undefined && value === undefined) { + return hvAA; + } if (value === undefined) { + return hvAA[key]; + } + hvAA[key] = value; + window.hvAA = hvAA; + return window.hvAA[key]; + } + + function objSort(obj) { // 对象排序 + const objNew = {}; + const arr = Object.keys(obj).sort(); + arr.forEach((key) => { + objNew[key] = obj[key]; + }); + return objNew; + } + + function addStyle() { // CSS + const lang = g().lang; + if (!gE('.hvAA-LangStyle')) { + const langStyle = gE('head').appendChild(cE('style')); + langStyle.className = 'hvAA-LangStyle'; + langStyle.textContent = `l${lang}{display:inline!important;}`; + if (/^[01]$/.test(lang)) { + langStyle.textContent = `${langStyle.textContent}l01{display:inline!important;}`; + } + } + const globalStyle = gE('head').appendChild(cE('style')); + const cssContent = [ + // hvAA + 'l0, l1, l01, l2 {display:none;}', // l0: 简体 l1: 繁体 l01:简繁体共用 l2: 英文 + '#hvAABox2{position:absolute;left:1075px;padding-top: 6px;}', + '.hvAALog{font-size:20px;}', + '.hvAAPauseUI{top:30px;left:1246px;position:absolute;z-index:9999; width:80px}', + '.hvAAButton{top:5px;left:' + ((isMaintaining || isEquipDetail)?'0':'1255') + 'px;position:absolute;z-index:9999;cursor:pointer;width:40px;height:24px;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAADi0lEQVRIiZVWPYgUZxj+dvGEk7vsNdPYCMul2J15n+d991PIMkWmOEyMyRW2FoJIUojYp5ADFbZJkyISY3EqKGpgz+Ma4bqrUojICaIsKGIXSSJcsZuD3RT3zWZucquXDwYG5n2f9/d5vnFuHwfAZySfAXgN4DXJzTiOj+3H90OnkmXZAe/9FMm3JJ8AuBGepyRfle2yLDvgnKt8EDVJkq8B3DGzjve+1m63p0n2AVzJbUh2SG455yre+5qZ/aCq983sxMfATwHYJvlCVYckHwFYVdURgO8LAS6RHJJcM7N1VR0CeE5yAGBxT3AR+QrA3wA20tQOq+pFkgOS90Tk85J51Xs9qaorqjoAcC6KohmSGyQHcRx/kbdv7AHgDskXaWqH0zSddc5Voyia2SOXapqmswsLvpam6ez8/Pwn+YcoimYAvARw04XZ5N8qZtZR1aGqXnTOVSd0cRd42U5EzqvqSFWX2u32tPd+yjnnXNiCGslHJAf7ybwM7r2vAdgWkYdZls157w+NK/DeT7Xb7WkAqyTvlZHjOD5oxgtmtqrKLsmze1VJsquqKwsLO9vnnKvkJHpLsq+qo/JAd8BtneTvqvqTiPwoIu9EZKUUpGpmi2Y2UtU+yTdJkhx1JJ8FEl0pruK/TrwA4F2r1WrkgI1G4wjJP0XkdLF9WaZzZnZZVa8GMj5xgf43JvXczFZbLb1ebgnJn0nenjQbEVkG0JsUYOykyi6Aa+XoQTJuTRr8OADJzVBOh+SlckYkz5L8Q0TquXOj0fhURN6r6pkSeAXAUsDaJPnYxXF8jOQrklskh97ryZJTVURWAPwF4DqAX0TkvRl/zTKdK2aeJMnxICFbAHrNZtOKVVdIrrVa2t1jz6sicprkbQC3VPVMGTzMpQvgQY63i8lBFddVdVCk/6TZlMFzopFci+P44H+YHCR3CODc/wUvDPY7ksMg9buZrKr3ATwvyoT3vrafzPP3er1eA9Azs7tjJhcqOBHkeSOKohkROR9K7prZYqnnlSRJjofhb4vIt/V6vUbyN1Xtt1qtb1zpZqs45xyAxXAnvCQ5FJGHqrpiZiMzu5xnHlZxCOABybXw3gvgp/Zq3/gA+BLATVVdyrJsbods2lfVq7lN4crMtapjZndD5pPBixWFLTgU7uQ3AJ6KyLKILAdy9sp25bZMBC//JSRJcjQIYg9Aj+TjZrNp+/mb+Ad711sdZZ1k/QAAAABJRU5ErkJggg==) center no-repeat transparent;}', + '#hvAABox{left:0;top:50px;font-size:16px!important;z-index:4;width:1238px;height:650px;position:absolute;text-align:left;background-color:#E3E0D1;border:1px solid #000;border-radius:10px;font-family:"Microsoft Yahei";}', + '#hvAABox a {display: unset!important;}', + '.hvAATab {display: none;}', + '.hvAATablist{position:relative;left:14px;width:calc(100% - 55px);height:calc(100% - 85px);}', + '.hvAATabmenu{position:absolute;left:-9px;}', + '.hvAATabmenu>span{display:block;padding:5px 10px;margin:0 10px 0 0;border:1px solid #91a7b4;border-radius:5px;background-color:#E3F1F8;color:#000;text-decoration:none;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;cursor:pointer;}', + '.hvAATabmenu>span:hover{left:-5px;position:relative;color:#0000FF;z-index:2!important;}', + '.hvAATabmenu>span>input{margin:0 0 0 -8px;}', + '.hvAATab{position:absolute;width:calc(100% - 10px);height:calc(100% - 30px);left:36px;padding:5px;border:1px solid #91A7B4;border-radius:3px;box-shadow:0 2px 3px rgba(0, 0, 0, 0.1);color:#666;background-color:#EDEBDF;overflow:auto;}', '.hvAATab>div:nth-child(2n){border:1px solid #EAEAEA;background-color:#FAFAFA;}', '.hvAATab>div:nth-child(2n+1){border:1px solid #808080;background-color:#DADADA;}', '.hvAATab a{margin:0 2px;}', '.hvAATab b{font-family:Georgia,Serif;font-size:larger;}', - '.hvAATab input.hvAANumber{width:24px;text-align:right;}', - '#hvAABox input[type=\'checkbox\']{top: 3px;}', + '.hvAATab input.hvAANumber{text-align:center;}', + 'input[type=\'checkbox\'] { width: 16px; height: 16px; margin: 0 2px; position: relative; top: 0; vertical-align: middle; }', '.hvAATab ul,.hvAATab ol{margin:0;}', '.hvAATab label{cursor:pointer;}', - '.hvAATab table{border:2px solid #000;border-collapse:collapse;margin:0 auto;}', + '.hvAATab table{border:2px solid #000;border-collapse:collapse;}', '.hvAATh>*{font-weight:bold;font-size:larger;}', '.hvAATab table>tbody>tr>*{border:1px solid #000;}', '#hvAATab-Drop tr>td:nth-child(1),#hvAATab-Usage tr>td:nth-child(1){text-align:left;}', '#hvAATab-Drop td,#hvAATab-Usage td{text-align:right;white-space:nowrap;}', - // '#hvAATab-Drop td:empty:before,#hvAATab-Usage td:empty:before{content:"";}', '.selectTable{cursor:pointer;}', `.selectTable:before{content:"${String.fromCharCode(0x22A0.toString(10))}";}`, '.hvAACenter{text-align:center;}', '.hvAATitle{font-weight:bolder;}', '.hvAAGoto{cursor:pointer;text-decoration:underline;}', + 'input[type="text"], input[type="number"]{min-width:2ch;max-width:calc( 100% - 10px);text-overflow:ellipsis; width: 2ch;}', + '.customizeInput{min-width:6ch;}', + '.customizeInput:not(.optionDefault){border: 2px solid!important}', + '.optionUnsaved{color:red;}', + '.optionEdited{color:#5C0D11;}', + '.optionDefault{color:unset!important;}', + '.hvAATable {display: grid;width: fit-content;}', + '.hvAATable>* {border: 1px solid;}', '.hvAANew{width:25px;height:25px;float:left;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAMCAYAAACX8hZLAAAAcElEQVQ4jbVRSQ4AIQjz/59mTiZIF3twmnCwFAq4FkeFXM+5vCzohYxjPMtfxS8CN6iqQ7TfE0wrODxVbzJNgoaTo4CmbBO1ZWICouQ0DHaL259MEzaU+w8pZOdSjcUgaPJDHCbO0A2kuAiuwPGQ+wBms12x8HExTwAAAABJRU5ErkJggg==) center no-repeat transparent;}', '#hvAATab-Alarm input[type="text"]{width:512px;}', '.testAlarms>div{border:2px solid #000;}', - '.hvAAArenaLevels{display:none; grid-template-columns:repeat(7, 20px 1fr);}', - '.hvAAcheckItems{display:grid; grid-template-columns:repeat(3, 0.1fr 0.3fr 1fr)}', + '.hvAAcheckItems{display:grid; grid-template-columns:repeat(5, 1fr)}', '.hvAAcheckItems>input.hvAANumber{width:32px}', '.hvAAConfig{width:100%;height:16px;}', - '.hvAAButtonBox{position:relative;top:468px;}', + '.hvAAButtonBox{position:relative;top:0px;}', + '.hvAAPauseUI>.encounterUI{font-weight:bold;position:unset;font-size:10pt;text-decoration:none;}', '.encounterUI{font-weight:bold;font-size:10pt;position:absolute;top:58px;left:1240px;text-decoration:none;}', '.quickSiteBar{position:absolute;top:0px;left:1290px;font-size:18px;text-align:left;width:165px;height:calc(100% - 10px);display:flex;flex-direction:column;flex-wrap:wrap;}', '.quickSiteBar>span{display:block;max-height:24px;overflow:hidden;text-overflow:ellipsis;}', @@ -723,7 +2078,7 @@ try { '.customizeBox>span a{text-decoration:none;}', '.customizeBox>select{max-width:60px;}', '.favicon{width:16px;height:16px;margin:-3px 1px;border:1px solid #000;border-radius:3px;}', - '.answerBar{z-index:1000;width:710px;height:40px;position:absolute;top:55px;left:282px;display:table;border-spacing:5px;}', + '.answerBar{000;width:710px;height:40px;position:absolute;top:55px;left:282px;display:table;border-spacing:5px;}', '.answerBar>div{border:4px solid red;display:table-cell;cursor:pointer;}', '.answerBar>div:hover{background:rgba(63,207,208,0.20);}', '#hvAAInspectBox{background-color:#EDEBDF;position:absolute;z-index:9;border: 2px solid #5C0D11;font-size:16px;font-weight:bold;padding:3px;display:none;}', @@ -737,1006 +2092,2268 @@ try { '.tlbWARN{text-align:left;font-weight:bold;color:red;font-size:20pt;}', // 标记检测出异常的日志行 // 怪物标号用数字替代字母,目前弃用 // '#pane_monster{counter-reset:order;}', - // '.btm2>div:nth-child(1):before{font-size:23px;font-weight:bold;text-shadow:1px 1px 2px;content:counter(order);counter-increment:order;}', - // '.btm2>div:nth-child(1)>img{display:none;}', + // `${monsterStateKeys.lv}>div:nth-child(1):before{font-size:23px;font-weight:bold;text-shadow:1px 1px 2px;content:counter(order);counter-increment:order;}`, + // `${monsterStateKeys.lv}>div:nth-child(1)>img{display:none;}`, ].join(''); globalStyle.textContent = cssContent; - optionButton(lang); + optionBox(); + optionButton(); + } + + function optionButton() { // 配置按钮 + if (gE('.hvAAButton')) return; + const optionButton = gE('body').appendChild(cE('div')); + optionButton.className = 'hvAAButton'; + optionButton.onclick = function () { + gE('#hvAABox').style.display = (gE('#hvAABox').style.display === 'none') ? 'block' : 'none'; + }; + } + + function rmListItem(code) { // 同步删除界面显示对应的项 + const configs = gE('#hvAATab-Tools > * > ul[class="hvAABackupList"] > li', 'all'); + for (const config of configs) { + if (config.textContent === code) config.remove(); + } + } + + function backup(code, alert) { + const currentOption = getValue('option'); + const auto = code ? undefined : `[auto backup for ${_server.name}@${currentOption.version}] ${time(3)}`; + const backups = getValue('backup', true) || {}; + code ??= auto; + if (code in backups) { // 覆写同名配置 + if (!alert || UI.confirm(alert)) { + delete backups[code]; + rmListItem(code); + } else return; + } + backups[code] = getValue('option'); + backups[code].auto = auto ? time(0) : undefined; + backups[code].server = _server.name; + const autos = Object.keys(backups).filter(c => backups[c].auto); + autos.sortBy(a => -backups[a].auto); + let i = 0, max = 5; + for (const a of autos) { + if (backups[a].server !== _server.name) continue; + i++; + if (i <= max) continue; + delete backups[a]; + } + setValue('backup', backups); + if (!gE('#hvAABox')) return; + const li = gE('.hvAABackupList', gE('#hvAABox')).appendChild(cE('li')); + li.textContent = code; + } + + function getCheckSupplyOptionTable(suffix = '', checkBoxOnly) { + const items = [ + 11191, 11291, 11391, 12101, 12201, + 11195, 11295, 11395, 12301, 12401, + 11199, 11299, 11399, 12501, 12601, + 13299, 13221, 13211, 13201, 0, + 13199, 13111, 13101, 0, 11401, + 19111, 19131, 11501, 0, 11402]; + return [ + checkBoxOnly ? '' : ` ${UI.l('库存', '庫存', 'Warn if supply')}<max(100%,${UI.number(`checkSupplyWarn${suffix}`, 100)}%)${UI.label(`checkSupplyWarn${suffix}`, `${UI.l('提示库存', '提示庫存', 'Supply warn')} ${suffix} %`, 'hidden')}${UI.l('时提示', '時提示')};
`, + UI.hvAATable(undefined, `hvAAcheckItems checkSupply${suffix}Inner`, ...items.map(item => { + if (!item) return UI.div(); + const names = itemMap[item].map((...args) => `${args[0]}`).join(''); + return UI.div(UI.labeled( + `isCheck${suffix}_${item}`, + `${!checkBoxOnly ? UI.number(`checkItem${suffix}_${item}`) : ''}${UI.label(`checkItem${suffix}_${item}`, UI.l('库存', '庫存', 'Supply')+' '+suffix, 'hidden')}${UI.label(`checkItem${suffix}_${item}`, names)}` + )) + })) + ]; + } + + function appendInput(container, value, innerHTML) { + const item = cE('div'); + item.innerHTML = innerHTML; + container.appendChild(item); + const input = gE('input', item); + switch(input.type) { + case 'number': + input.value = value; + customizeInputAutoFit(input); + break; + case 'checkbox': + input.checked = value; + displayCheckBoxNotDefault(input); + input.addEventListener('change', () => displayCheckBoxNotDefault(input)); + break; + default: + break; + } + } + + function appendSelection(container, name, value, list, map, inheritBy) { + const autoSwitchOptionText = { + inherit: UI.byLang('继承', '繼承', 'Inherit'), + keep: UI.byLang('不自动切换', '不自動切換', 'Disable auto switch') + } + const defaultNote = UI.byLang('(默认)', '(默認)', '(Default)'); + const currentOptionText = UI.byLang('(当前)', '(當前)', '(current)'); + const selection = cE('div'); + let innerHTML = []; + + const lang = g().lang; + innerHTML.push(`
`); + selection.innerHTML = innerHTML; + container.appendChild(selection); + const select = gE('select', selection); + select.value = value; + selectFit(select); + return select; + } + + function setEquipSetName(personaSelect, equipSetSelection, personas, equipSets) { + if ([personas, equipSets].includes(undefined)) { + const { e, p, s } = getValue('itemWorldDatas', true) ?? {}; + personas ??= p; + equipSets ??= s; + } + let current = { persona: Object.keys(personas).find(p => personas[p].selected), equipSet: Object.keys(equipSets).find(s => equipSets[s]) }; + let setNames = JSON.parse(window.localStorage.getItem(_server.utils + '_persona') ?? '[]'); + const currentOptionText = UI.byLang('(当前)', '(當前)', '(current)'); + const names = setNames?.[current.persona]; + [...gE('option', 'all', equipSetSelection)].forEach(option => { + const id = option.value; + option.innerText = ['-1', 'undefined'].includes(id) ? option.innerText : `Set ${id}${(personaSelect.value === current.persona && names?.[id]?.name) ? ` (${names?.[id]?.name})` : ''}${(personaSelect.value === current.persona && current.equipSet === id) ? currentOptionText : ''}`; + }); + } + + function bindPersonaEquipSetSelection(persona, equipSet, personas, equipSets) { + setEquipSetName(persona, equipSet, personas, equipSets); + equipSet.onchange = () => selectFit(equipSet); + persona.onchange = () => { + selectFit(persona); + setEquipSetName(persona, equipSet, personas, equipSets); + } } - function optionButton(lang) { // 配置按钮 - const optionButton = gE('body').appendChild(cE('div')); - optionButton.className = 'hvAAButton'; - optionButton.onclick = function () { - if (gE('#hvAABox')) { - gE('#hvAABox').style.display = (gE('#hvAABox').style.display === 'none') ? 'block' : 'none'; - } else { - optionBox(); - gE('#hvAATab-Main').style.zIndex = 1; - gE('select[name="lang"]').value = lang; - } + function updateEquipSetUI() { + const container = gE('.equipSetList'); + if (!container) return; + let innerHTML = [ + ['战斗', '戰鬥', 'Battle'], + ['挑战人物', '挑戰人物', 'Battle Persona'], + ['挑战套装', '挑戰套裝', 'Battle Equip Set'] + ].map(s => UI.b(UI.l(s))); + const option = getOption(); + const { equips, personas, equipSets } = getValue('itemWorldDatas', true) ?? {}; + if (!personas || !equipSets) { + return; + } + container.innerHTML = innerHTML.join(''); + const ordered = ['default', 'ba', 'gr', 'tw', 'rb', '105', '106', '107', '108', '109', '110', '111', '112', 'ar', '1', '3', '5', '8', '9', '11', '12', '13', '15', '16', '17', '19', '20', '21', '23', '24', '26', '27', '28', '29', '32', '33', '34', '35']; + const battles = { + 'default': 'Default', + 'ba': 'BA', + 'gr': 'GF', + 'tw': 'TW', + 'rb': 'RB', + 'ar': 'AR', + '1': 'AR1', + '3': 'AR10', + '5': 'AR20', + '8': 'AR30', + '9': 'AR40', + '11': 'AR50', + '12': 'AR60', + '13': 'AR70', + '15': 'AR80', + '16': 'AR90', + '17': 'AR100', + '19': 'AR110', + '20': 'AR120', + '21': 'AR130', + '23': 'AR140', + '24': 'AR150', + '26': 'AR165', + '27': 'AR180', + '28': 'AR200', + '29': 'AR225', + '32': 'AR250', + '33': 'AR300', + '34': 'AR400', + '35': 'AR500', + '105': 'RB50', + '106': 'RB75A', + '107': 'RB75B', + '108': 'RB75C', + '109': 'RB100', + '110': 'RB150', + '111': 'RB200', + '112': 'RB250', }; + for (const battle of ordered) { + const inherit = battle === 'default' ? undefined : isNaN(+battle) ? 'Default' : battle * 1 >= 105 ? 'RB' : 'AR'; + appendInput(container, option.enableEquipSet?.[battle], UI.labeled(`enableEquipSet_${battle}`, battles[battle])); + const persona = appendSelection(container, `switchPersona_${battle}`, option.switchPersona?.[battle], personas, (id, list) => list[id], inherit); + const equipSet = appendSelection(container, `switchEquipSet_${battle}`, option.switchEquipSet?.[battle], equipSets, (id, list) => { return { name: `Set ${id}`, selected: list[id] }; }, inherit); + bindPersonaEquipSetSelection(persona, equipSet, personas, equipSets); + } + } + + function updateItemWorldListUI() { + const container = gE('.autoItemWorldList'); + if (!container) return; + let innerHTML = [ + ['挑战顺序', '挑戰順序', 'Order'], + ['装备', '裝備', 'Equip'], + ['停止等级', '停止等級', 'Stop Level'], + ['挑战人物', '挑戰人物', 'Battle Persona'], + ['挑战套装', '挑戰套裝', 'Battle Equip Set'] + ].map(s => UI.b(UI.l(s))); + const option = getOption(); + const { equips, personas, equipSets } = getValue('itemWorldDatas', true) ?? {}; + if (!equips || !personas || !equipSets) { + gE('.itemWorldCounts').innerHTML = `${equips?.filter(eqp => option.enableItemWorld?.[eqp.id]).length ?? 0}/${equips?.length ?? 0}`; + return; + } + gE('.itemWorldCounts').innerHTML = `${equips.filter(eqp => option.enableItemWorld?.[eqp.id]).length}/${equips.length}`; + container.innerHTML = innerHTML.join(''); + + for (const equip of equips) { + const eid = equip.id; + if (equip.world >= equip.max) continue; + appendInput(container, option.ItemWorldOrder?.[eid], UI.label(`ItemWorldOrder_${eid}`, UI.l('道具界优先级 ', '道具界優先級 ', 'Item World Order ')+eid, 'hidden')+UI.number(`ItemWorldOrder_${eid}`)); + appendInput(container, option.enableItemWorld?.[eid], UI.labeled(`enableItemWorld_${equip.id}`, `[${eid}]${equip.name} (${equip.level}/${equip.world}/${equip.max})`)); + appendInput(container, option.levelItemWorld?.[eid], UI.label(`levelItemWorld_${eid}`, UI.l('道具界等级 ', '道具界等級 ', 'Item World Level ')+eid, 'hidden')+UI.number(`levelItemWorld_${eid}`)); + const persona = appendSelection(container, `itemWorldPersona_${eid}`, option.itemWorldPersona?.[eid], personas, (id, list) => list[id]); + const equipSet = appendSelection(container, `itemWorldEquipSet_${eid}`, option.itemWorldEquipSet?.[eid], equipSets, (id, list) => { return { name: '', selected: list[id] }; }); + bindPersonaEquipSetSelection(persona, equipSet, personas, equipSets); + } + } + + function displayCheckBoxNotDefault(input) { + const id = input.id; + if (!gE(`label[for="${id}"]`) || input.placeholder === undefined) { + return; + } + if (!!input.checked !== !!input.placeholder) { + gE(`label[for="${id}"]`).classList.add('optionEdited'); + } else { + gE(`label[for="${id}"]`).classList.remove('optionEdited'); + } + } + + async function updateItemWorldList(skipEquips, doc) { + const equips = skipEquips ? getValue('itemWorldDatas', true)?.equips : await asyncUpdateEquipModifyList(); + const personas = await asyncUpdatePersona(doc); + const equipSets = await asyncUpdateEquipSet(doc); + if ((!skipEquips && !equips?.length) || !personas || !equipSets) return; + setValue('itemWorldDatas', { equips, personas, equipSets }); } function optionBox() { // 配置界面 - const optionBox = gE('body').appendChild(cE('div')); - optionBox.id = 'hvAABox'; - optionBox.innerHTML = [ - '
', - '

hvAutoAttack

', - ' 更新历史更新歷史ChangeLog', - ' 使用说明README', - ' ', - (g('option')?.optionStandalone? isIsekai?'当前为异世界单独配置當前為異世界單獨配置Using Isekai standalone option':'当前为恒定世界单独配置當前為恆定世界單獨配置Using Persistent standalone option':''), - ' by Koko191
', - '
', - - '
', - ' 主要选项主要選項Main', - ' 战斗开启戰鬥開啟BattleStarter', - ' 恢复技能恢復技能Recovery', - ' 引导技能引導技能Channel Spells', - ' BUFF技能 Spells', - ' DEBUFF技能 Spells', - ' 其他技能Skills', - ' 卷轴捲軸Scroll', - ' 警报警報Alarm', - ' 攻击规则攻擊規則Attack Rule', - ' 掉落监测掉落監測Drops Tracking', - ' 数据记录數據記錄Usage Tracking', - ' 工具工具Tools', - ' 反馈Feedback', - '
', - - '
', - '
异世界相关異世界相關Isekai: ', - ' ', - ' ; ', - '
在任意页面停留

在任意頁面停留

Idle in any page for
秒后,进行跳转秒後,進行跳轉s, start switch check
', - '
小马答题小馬答題RIDDLE: ; ', - '
内置插件Built-in Plugin: ;
', - '
时间時間If ETR秒,如果输入框为空则随机生成答案并提交秒,如果輸入框為空則隨機生成答案並提交s and no answer has been chosen yet, a random answer will be generated and submitted
', - '
', - '
脚本行为腳本行為Script Activity', - '
暂停相关暫停相關Pause with: ', - ' ; ', - '
', - '
警告相关警告相關To Warn: ', - ' ; ', - ' ', - '
', - '
掉落及数据记录掉落及數據記錄Drops and Usage Tracking:
', - '
延迟延遲Delay: 1. Buff/Debuff/其他技能Buff/Debuff/其他技能Skills&BUFF/DEBUFF Spells: ms 2. 其他Other: ms (', - ' 说明: 单位毫秒,且在设定值基础上取其的50%-150%进行延迟,0表示不延迟說明: 單位毫秒,且在設定值基礎上取其的50%-150%進行延遲,0表示不延遲Note: unit milliseconds, and based on the set value multiply 50% -150% to delay, 0 means no delay)
', - '
', - '
*攻击模式攻擊模式Attack Mode:', - '
', - - '
战斗执行顺序(未配置的按照下面的顺序)戰鬥執行順序(未配置的按照下面的順序)Battal Order(Using order below as default if not configed):
', - ' ', - ' ', - ' ', - '
', - ' ', - ' ', - ' ', - '
', - ' ', - ' ', - ' ', - '
', - '
使用魔药(与攻击模式相同)使用魔藥(與攻擊模式相同)Use Infusion(same as attack mode){{infusionCondition}}
', - '
', - '
', - '
: {{etherTapCondition}}
', - '
: {{turnOnSSCondition}}
', - '
: {{turnOffSSCondition}}
', - '
: {{defendCondition}}
', - '
: {{focusCondition}}
', - '
: {{pauseCondition}}
', - '
: {{fleeCondition}}
', - '
', - '
继续新回合延时繼續新回合延時New round wait time: (秒)(秒)(s)
', - '
战斗结束退出延时戰鬥結束退出延時Exit battle wait time: (秒)(秒)(s)
', - '
当损失精力當損失精力If it lost Stamina: ', - ' ;', - ' ; ', - ' ', - '
', - '
战斗页面停留戰鬥頁面停留If the page for : ', - ' ; ', - ' ', - '
', - '
', - - '
', - '
', - '
;
', - ' 进行的竞技场相对应等级進行的競技場相對應等級The levels of the Arena you want to complete: ', - '
', - ' ', - '
', - ' ', - ' ', - ' ', - '
', - '
', - '
精力精力Stamina: 阈值閾值 threshold: Min(85, );
', - '
含本日自然恢复的阈值含本日自然恢復的閾值Stamina threshold with naturally recovers today.: ;
', - '
', - '
进入遭遇战的最低精力進入遭遇戰的最低精力Minimum stamina to engage encounter:
', - '
', - '
: ', - ' 耐久度耐久度Durability%
', - '
检查物品库存檢查物品庫存Check is item needs supply: ', - '
', - ' 体力药水體力藥水Health Potion', - ' 体力长效药體力長效藥Health Draught', - ' 体力秘药體力秘藥Health Elixir', - ' 魔力药水魔力藥水Mana Potion', - ' 魔力长效药魔力長效藥Mana Draught', - ' 魔力秘药魔力秘藥Mana Elixir', - ' 灵力药水靈力藥水Spirit Potion', - ' 灵力长效药靈力長效藥Spirit Draught', - ' 灵力秘药靈力秘藥Spirit Elixir', - ' 终极秘药終極秘藥Last Elixir', - ' 花瓶花瓶Flower Vase', - ' 泡泡糖泡泡糖Bubble-Gum', - ' 能量饮料能量飲料Energy Drink', - ' 咖啡因糖果咖啡因糖果Caffeinated Candy', - ' 火焰魔药火焰魔藥Infusion of Flames', - ' 冰冷魔药冰冷魔藥Infusion of Frost', - ' 闪电魔药閃電魔藥Infusion of Lightning', - ' 风暴魔药風暴魔藥Infusion of Storms', - ' 神圣魔药神聖魔藥Infusion of Divinity', - ' 黑暗魔药黑暗魔藥Infusion of Darkness', - ' 加速卷轴加速捲軸Scroll of Swiftness', - ' 守护卷轴守護捲軸Scroll of Protection', - ' 化身卷轴化身捲軸Scroll of the Avatar', - ' 吸收卷轴吸收捲軸Scroll of Absorption', - ' 幻影卷轴幻影捲軸Scroll of Shadows', - ' 生命卷轴生命捲軸Scroll of Life', - ' 众神卷轴眾神捲軸Scroll of the Gods', - '
', - '
', - - '
', - '
施放顺序施放順序Cast Order:
', - ' ', - '
', - ' ', - ' ', - ' ', - '
', - ' ', - ' ', - '
', - ' ', - ' ', - '
', - ' ', - ' ', - '
', - '
: {{itemHGCondition}}
', - '
: {{itemMGCondition}}
', - '
: {{itemSGCondition}}
', - '
: {{itemMysticCondition}}
', - '
: {{itemCureCondition}}
', - '
: {{itemFCCondition}}
', - '
: {{itemHPCondition}}
', - '
: {{itemHECondition}}
', - '
: {{itemMPCondition}}
', - '
: {{itemMECondition}}
', - '
: {{itemSPCondition}}
', - '
: {{itemSECondition}}
', - '
: {{itemLECondition}}
', - '
: {{itemEDCondition}}
', - '
: {{itemCCCondition}}
', - - '
', - ' 获得引导时(此时1点MP施法与150%伤害)獲得引導時(此時1點MP施法與150%傷害)During Channeling effect (1 mp spell cost and 150% spell damage):', - '
先施放引导技能先施放引導技能First cast:
', - ' 注意: 此处的施放顺序与注意: 此處的施放順序与Note: The cast order here is the same as inBUFF技能 Spells里的相同裡的相同
', - ' ', - ' ', - ' ', - '
', - ' ', - ' ', - ' ', - ' ', - '
', - '
: ', - '
施放顺序施放順序Cast Order:
', - ' ', - ' ', - ' ', - ' ', - ' ', - '
', - ' ', - ' ', - ' ', - ' ', - '
', - '
最后ReBuff: 重新施放最先消失的Buff最後ReBuff: 重新施放最先消失的BuffAt last, re-cast the spells which will expire first.
', - - '
', - '
施放顺序施放順序Cast Order: ', - '
', - ' ', - ' ', - ' ', - '
', - ' ', - ' ', - ' ', - ' ', - ' ', - '
', - '
Buff释放条件Buff釋放條件Cast spells Condition{{buffSkillCondition}}
', - '
{{buffSkillHDCondition}}
', - '
{{buffSkillMDCondition}}
', - '
{{buffSkillSDCondition}}
', - '
{{buffSkillFVCondition}}
', - '
{{buffSkillBGCondition}}
', - '
{{buffSkillPrCondition}}
', - '
{{buffSkillSLCondition}}
', - '
{{buffSkillSSCondition}}
', - '
{{buffSkillHaCondition}}
', - '
{{buffSkillAFCondition}}
', - '
{{buffSkillHeCondition}}
', - '
{{buffSkillReCondition}}
', - '
{{buffSkillSVCondition}}
', - '
{{buffSkillAbCondition}}
', - '
', - - '
', - '

', - ' 沉眠(Sl)沉眠(Sl)Sleep: ', - ' 致盲(Bl)致盲(Bl)Blind: ', - ' 缓慢(Slo)緩慢(Slo)Slow:
', - ' 陷危(Im)陷危(Im)Imperil: ', - ' 魔磁网(MN)魔磁網(MN)MagNet: ', - ' 沉默(Si)沉默(Si)Silence:
', - ' 枯竭(Dr)枯竭(Dr)Drain: ', - ' 虚弱(We)虛弱(We)Weaken: ', - ' 混乱(Co)混亂(Co)Confuse:
', - '
施放顺序施放順序Cast Order:', - '
', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '
', - '
特殊Special
{{debuffSkillWeAllCondition}}', - '
特殊Special
{{debuffSkillImAllCondition}}', - '
{{debuffSkillSleCondition}}
', - '
{{debuffSkillBlCondition}}
', - '
{{debuffSkillSloCondition}}
', - '
{{debuffSkillImCondition}}
', - '
{{debuffSkillMNCondition}}
', - '
{{debuffSkillSiCondition}}
', - '
{{debuffSkillDrCondition}}
', - '
{{debuffSkillWeCondition}}
', - '
{{debuffSkillCoCondition}}
', - '
', - - '
', - '
注意: 默认在灵动架式状态下使用,请在主要选项勾选并设置开启/关闭灵动架式注意: 默認在靈動架式狀態下使用,請在主要選項勾選並設置開啟/關閉靈動架式Note: use under Spirit by default, please check and set the Turn on/off Spirit Stance in Main
', - '
施放顺序施放順序Cast Order: ', - '
', - '
', - '
: {{skillOFCCondition}}
', - '
: {{skillFRDCondition}}
', - '
战斗风格戰鬥風格Fighting style:
', - '
:
{{skillT3Condition}}
', - '
: {{skillT2Condition}}
', - '
: {{skillT1Condition}}
', - - '
', - ' 战役模式戰役模式Battle type: ', - ' {{scrollCondition}}', - ' ', - '
{{scrollSwCondition}}
', - '
{{scrollPrCondition}}
', - '
{{scrollAvCondition}}
', - '
{{scrollAbCondition}}
', - '
{{scrollShCondition}}
', - '
{{scrollLiCondition}}
', - '
{{scrollGoCondition}}
', - - '
', - ' 自定义警报自定義警報Alarm
', - ' 注意:留空则使用默认音频,建议每个用户使用自定义音频注意:留空則使用默認音頻,建議每個用戶使用自定義音頻Note: Leave the box blank to use default audio, it\'s recommended for all user to use custom audio.', - '




', - '
请将将要测试的音频文件的地址填入这里請將將要測試的音頻文件的地址填入這裡Plz put in the audio file address you want to test:
', - - '
', - ' 攻击规则攻擊規則Attack Rule 示例Example', - '
1. 初始血量权重=Log10(目标血量/场上最低血量)初始血量權重=Log10(目標血量/場上最低血量)BaseHpWeight = BaseHpRatio*Log10(TargetHP/MaxHPOnField)
', - ' 初始权重系数(>0:低血量优先;<0:高血量优先)初始權重係數(>0:低血量優先;<0:高血量優先)BaseHpRatio(>0:low hp first;<0:high hp first)
', - ' 不可命中目标的权重不可名中目標的權重Unreachable Target Weight
', - '
', - '
2. 初始权重与下述各Buff权重相加初始權重與下述各Buff權重相加PW(X) = BaseHpWeight + Accumulated_Weight_of_Deprecating_Spells_In_Effect(X)
', - ' 虚弱(We)虛弱(We)Weaken: ', - ' 致盲(Bl)致盲(Bl)Blind: ', - ' 缓慢(Slo)緩慢(Slo)Slow: ', - ' 沉默(Si)沉默(Si)Silence: ', - ' 沉眠(Sl)沉眠(Sl)Sleep:
', - ' 陷危(Im)陷危(Im)Imperil: ', - ' 破甲(PA)破甲(PA)Penetrated Armor: ', - ' 流血(Bl)流血(Bl)Bleeding Wound:
', - ' 混乱(Co)混亂(Co)Confuse: ', - ' 枯竭(Dr)枯竭(Dr)Drain: ', - ' 魔磁网(MN)魔磁網(MN)MagNet: ', - ' 眩晕(St)眩暈(St)Stunned:
', - ' 魔力合流(CM)魔力合流(CM)Coalesced Mana:
', - '
', - '
3. PW(X) += Log10(1 + 武器攻击中央目标伤害倍率(副手及冲击技能)乘以武器攻擊中央目標傷害倍率(副手及衝擊技能)Weapon Attack Central Target Damage Ratio (Offhand & Strike))
额外伤害比例:額外傷害比例:Extra DMG Ratio: %
', - '
4. 优先选择权重最低的目标優先選擇權重最低的目標Choose target with lowest rank first
BOSS:Yggdrasil额外权重BOSS:Yggdrasil額外權重BOSS:Yggdrasil Extra Weight
', - '
显示权重及顺序顯示權重及順序DIsplay Weight and order', - ' 显示优先级背景色顯示優先級背景色DIsplay Priority Background Color', - ' CSS格式或可eval执行的公式(可用<rank>, <all>指代优先级和总优先级数量, <style_x>指代第x个的相同配置值),例如:CSS格式或可eval執行的公式(可用<rank>, <all>指代優先級和總優先級數量, <style_x>指代第x個的相同配置值):例如CSS or eval executable formula(use <rank> and <all> to refer to priority rank and total rank count, <style_x> to refer to the same option value of option No.x)Such as:
`hsl(${Math.round(240*<rank>/Math.max(1,<all>-1))}deg 50% 50%)`
', - ' 1.
', - ' 2. ', - ' 3. ', - ' 4.
', - ' 5. ', - ' 6. ', - ' 7.
', - ' 8. ', - ' 9. ', - ' 10. ', - '
', - '
PS. 如果你对各Buff权重有特别见解,请务必如果你對各Buff權重有特別見解,請務必If you have any suggestions, please 告诉我告訴我let me know.
参考公式为:參考公式為:Basic Weight Calculation as: PW(X) = Log10(
(HP/MaxHPOnField/(1+CentralAttackDamageExtraRatio)
*[HPActualEffectivenessRate:∏(1-debuff),debuff=Im|PA|Bl|Co|Dr|MN|St]
/[DMGActualEffectivenessRate:∏(1-debuff),debuff=We|Bl|Slo|Si|Sl|Co|Dr|MN|St])
)
', - '
', - - '
', - ' 掉落监测掉落監測Drops Tracking', - '
记录装备的最低品质記錄裝備的最低品質Minimum drop quality:
', - '
', - - '
', - ' 数据记录數據記錄Usage Tracking', - '
', - - '
', - '
当前状况當前狀況Current status: ', - ' 如果脚本长期暂停且网络无问题,请点击如果腳本長期暫停且網絡無問題,請點擊If the script does not work and you are sure that it\'s not because of your internet, click
', - ' 战役模式戰役模式Battle type: 当前回合當前回合Current round: 总回合總回合Total rounds:
', - '
快捷站点快捷站點Quick Site
', - ' 注意: 留空“姓名”一栏则表示删除该行,修改后请保存注意: 留空“姓名”一欄則表示刪除該行,修改後請保存Note: The "name" input box left blank will be deleted, after change please save in time.', - '
图标圖標ICON名称名稱Name链接鏈接Link
', - '
备份与还原備份與還原Backup and Restore
    ', - '
    导入与导出導入與導出Import and Export
    ', - - '
    ', - ' 反馈Feedback', - '
    链接鏈接Links: 1. GitHub2. GreasyFork
    ', - '
    反馈说明反饋說明Feedback Note:
    ', - ' 如果你遇见了Bug,想帮助作者修复它
    你应当提供以下多种资料:
    1. 场景描述
    2. 你的配置
    3. 控制台日志 (按Ctrl+Shift+i打开开发者助手,再选择Console(控制台)面板)
    4. 战斗日志 (如果是在战斗中)
    如果是无法容忍甚至使脚本失效的Bug,请尝试安装旧版本
    如果你有一些建议使这个脚本更加有用,那么:
    1. 请尽量简述你的想法
    2. 如果可以,请提供一些场景 (方便作者更好理解)
    ', - ' 如果你遇見了Bug,想幫助作者修復它
    你應當提供以下多種資料:
    1. 場景描述
    2. 你的配置
    3. 控制台日誌 (按Ctrl+Shift+i打開開發者助手,再選擇Console(控制台)面板)
    4. 戰鬥日誌 (如果是在戰鬥中)
    如果是無法容忍甚至使腳本失效的Bug,請嘗試安裝舊版本
    如果你有一些建議使這個腳本更加有用,那麼:
    1. 請盡量簡述你的想法
    2.如果可以,請提供一些場景 (方便作者更好理解)
    ', - ' If you encounter a bug and would like to help the author fix it
    You should provide the following information:
    1. the Situation
    2. Your Configuration
    3. Console Log (press Ctrl + Shift + i to open the Developer Assistant, And then select the Console panel)
    4. Battle Log (if in combat)
    If you are unable to tolerate this bug or even the bug made the script fail, try installing the old version
    If you have some suggestions to make this script more useful, then:
    1. Please briefly describe your thoughts
    2. If you can, please provide some scenes (to facilitate the author to better understand)
    PS. For English user, please express in basic English (Oh my poor English, thanks for Google Translate)
    ', - '
    ', - - '
    ', - '
    ', - ].join('').replace(/{{(.*?)}}/g, '
    '); - // 绑定事件 - gE('select[name="lang"]', optionBox).onchange = function () { // 选择语言 - gE('.hvAA-LangStyle').textContent = `l${this.value}{display:inline!important;}`; - if (/^[01]$/.test(this.value)) { - gE('.hvAA-LangStyle').textContent += 'l01{display:inline!important;}'; - } - g('lang', this.value); + const UIDatas = { + checkSupplyInnerExtra: [ + { id: 'IW', names: ['道具界', '道具界', 'Item World'] }, + { id: 'GF', names: ['压榨界', '壓榨界', 'Grind Fest'] }, + { id: 'TW', names: ['塔楼', '塔樓', 'The Tower'] }, + ], + tablist: [ + { id: 'Main', names: ['主要选项', '主要選項', 'Main'] }, + { id: 'BattleStarter', names: ['战斗开启', '戰鬥開啟', 'BattleStarter'] }, + { id: 'Recovery', names: ['恢复技能', '恢復技能', 'Recovery'] }, + { id: 'Channel', names: ['引导技能', '引導技能', 'Channel Spells'], values: ['channelSkillSwitch'] }, + { id: 'Buff', names: ['BUFF 技能', 'BUFF 技能', 'BUFF Spells'], values: ['buffSkillSwitch'] }, + { id: 'Debuff', names: ['DEBUFF 技能', 'DEBUFF 技能', 'DEBUFF Spells'], values: ['debuffSkillSwitch'] }, + { id: 'Skill', names: ['其他技能', '其他技能', 'Skills'], values: ['skillSwitch'] }, + { id: 'Infusion', names: ['魔药', '魔藥', 'Infusion'], values: ['infusionSwitch'] }, + { id: 'Scroll', names: ['卷轴', '捲軸', 'Scroll'], values: ['scrollSwitch'] }, + { id: 'Alarm', names: ['警报', '警報', 'Alarm'] }, + { id: 'Rule', names: ['攻击规则', '攻擊規則', 'Attack Rule'] }, + { id: 'Drop', names: ['掉落监测', '掉落監測', 'Drops Tracking'], values: ['dropMonitor'] }, + { id: 'Usage', names: ['数据记录', '數據記錄', 'Usage Tracking'], values: ['recordUsage'] }, + { id: 'Tools', names: ['工具', '工具', 'Tools'] }, + { id: 'Feedback', names: ['反馈', '反馈', 'Feedback'] }, + ], + repair: [ + { id: '', names: [''] }, + { id: 'GF', names: ['或 压榨界', '或 壓榨界', 'OR Grind Fest'] }, + { id: 'TW', names: ['或 塔楼', '或 塔樓', 'OR Tower'] }, + { id: 'IW', names: ['或 道具界/压榨界', '或 道具界', 'OR Item World'] }, + ], + repairCharm: [ + { id: '', names: ['自动战斗(含压榨界/道具界)', '自動戰鬥(含壓榨界/道具界)', 'Idle Battles(including Grind Fest & Item World)'] }, + { id: 'GF', names: ['压榨界', '壓榨界', 'Grind Fest'] }, + { id: 'IW', names: ['道具界', '道具界', 'Item World'] }, + { id: 'TW', names: ['塔楼', '塔樓', 'Tower'] }, + ], + staminaCheck: [ + { names: ['遭遇战', '遭遇戰', 'Random Encounter'], id: 'Encounter', values: [60] }, + { names: ['竞技场/浴血擂台', '競技場/浴血擂台', 'The Arena or Ring Of Blood'], id: 'Low', values: [60] }, + { names: ['道具界', '道具界', 'Item World'], id: 'ItemWorld', values: [60] }, + { names: ['压榨界', '壓榨界', 'GrindFest'], id: 'GrindFest', values: [100] }, + { names: ['塔楼', '塔樓', 'Tower'], id: 'Tower', values: [60] }, + { names: ['竞技场/浴血擂台/压榨界/道具界/塔楼(含本日自然恢复)', '競技場/浴血擂台/壓榨界/道具界/塔樓(含本日自然恢復)', 'Threshold with naturally recovers today for The Arena, Ring Of Bloog, GrindFest, Item World and Tower'], id: 'LowWithReNat', values: [0] }, + ], + battleUnresponsive: [ + { id: 'Alert', names: ['警报', '警報', 'alarm'] }, + { id: 'Reload', names: ['刷新页面', '刷新頁面', 'reload page'] }, + { id: 'Alt', names: ['切换主服务器与alt服务器', '切換主服務器與alt服務器', 'switch between alt.hentaiverse'] }, + ], + battleExitDelay: [ + { id: 'NewRound', names: ['继续新回合', '繼續新回合', 'New round'], values: [0] }, + { id: 'ExitBattle', names: ['战斗结束退出', '戰鬥結束退出', 'Exit battle'], values: [3] }, + ], + battleOrder: [ + { id: 'autoCure', names: ['使用治疗', '使用治療', 'Cure'], values: ['Cure'] }, + { id: 'autoPause', names: ['自动暂停', '自動暫停', 'Auto Pause'], values: ['Pause'] }, + { id: 'autoSSDisable', names: ['关闭灵动架式', '關閉靈動架式', 'Disable Sprite'], values: ['SSDisable'] }, + { id: 'autoRecover', names: ['恢复(含治疗)', '恢復(含治療)', 'Recover(& cure)'], values: ['Rec'] }, + { id: 'useScroll', names: ['使用卷轴', '使用捲軸', 'Use Scroll'], values: ['Scroll'] }, + { id: 'useInfusions', names: ['使用魔药', '使用魔藥', 'Infusions'], values: ['Infus'] }, + { id: 'autoDefend', names: ['自动防御', '自動防禦', 'Auto Defence'], values: ['Def'] }, + { id: 'useChannelSkill', names: ['引导技能', '引導技能', 'Channel Skill'], values: ['Channel'] }, + { id: 'useBuffSkill', names: ['Buff技能', 'Buff技能', 'Buff Skills'], values: ['Buff'] }, + { id: 'useDeSkill', names: ['Debuff技能', 'Debuff技能', 'Debuff Skills'], values: ['Debuff'] }, + { id: 'autoFocus', names: ['自动集中', '自動集中', 'Focus'], values: ['Focus'] }, + { id: 'autoSS', names: ['灵动架式(开&关)', '靈動架式(開&關)', 'On & Off Sprite'], values: ['SS'] }, + { id: 'autoSkill', names: ['释放技能', '釋放技能', 'Auto Skill'], values: ['Skill'] }, + { id: 'attack', names: ['自动攻击', '自動攻擊', 'Attack'], values: ['Atk'] }, + ], + hotkeys: [ + { id: 'pause', names: ['暂停', '暫停', 'Pause']}, + { id: 'stepIn', names: ['步进', '步進', 'Step In']}, + { id: 'alt', names: ['Alt切换', 'Alt切換', 'Alt Switch']}, + ], + attackStatus: [ + { id: 0, names: ['物理', '物理', 'Physical'], values: ['Phys'] }, + { id: 5, names: ['圣', '聖', 'Divine'], values: ['Divi'] }, + { id: 6, names: ['暗', '暗', 'Forbidden'], values: ['Forb'] }, + { id: 1, names: ['火', '火', 'Fire'], values: ['Fire'] }, + { id: 2, names: ['冰', '冰', 'Cold'], values: ['Cold'] }, + { id: 4, names: ['风', '風', 'Wind'], values: ['Wind'] }, + { id: 3, names: ['雷', '雷', 'Elec'], values: ['Elec'] }, + ], + battleCommons: [ + { id: 'lowSkill', names: ['低阶魔法技能', '低階魔法技能', '1st Tier Offensive Magic'], values: ['true'] }, + { id: 'middleSkill', names: ['中阶魔法技能', '中階魔法技能', '2nd Tier Offensive Magic'], values: ['true'] }, + { id: 'highSkill', names: ['高阶魔法技能', '高階魔法技能', '3rd Tier Offensive Magic'], values: ['true'] }, + { id: 'etherTap', names: ['以太之触', '以太之觸', 'Ether Tap'] }, + { id: 'turnOnSS', names: ['开启灵动架式', '開啟靈動架勢', 'Turn on Spirit Stance'] }, + { id: 'turnOffSS', names: ['关闭灵动架式', '關閉靈動架勢', 'Turn off Spirit Stance'] }, + { id: 'defend', names: ['Defend'] }, + { id: 'focus', names: ['Focus'] }, + ], + battleBreaks: [ + { id: 'autoPause', names: ['自动暂停', '自動暫停', 'Pause'], values: ['pause'] }, + { id: 'autoFlee', names: ['自动逃跑', '自動逃跑', 'Flee'], values: ['flee'] }, + ], + arena: [ + { id: 1, values: [1] }, + { id: 10, values: [3] }, + { id: 20, values: [5] }, + { id: 30, values: [8] }, + { id: 40, values: [9] }, + { id: 50, values: [11] }, + { id: 60, values: [12] }, + { id: 70, values: [13] }, + { id: 80, values: [15] }, + { id: 90, values: [16] }, + { id: 100, values: [17] }, + { id: 110, values: [19] }, + { id: 120, values: [20] }, + { id: 130, values: [21] }, + { id: 140, values: [23] }, + { id: 150, values: [24] }, + { id: 165, values: [26] }, + { id: 180, values: [27] }, + { id: 200, values: [28] }, + { id: 225, values: [29] }, + { id: 250, values: [32] }, + { id: 300, values: [33] }, + { id: 400, values: [34] }, + { id: 500, values: [35] }, + { id: 'RB50', values: [105] }, + { id: 'RB75A', values: [106] }, + { id: 'RB75B', values: [107] }, + { id: 'RB75C', values: [108] }, + { id: 'RB100', values: [109] }, + { id: 'RB150', values: [110] }, + { id: 'RB200', values: [111] }, + { id: 'RB250', values: [112] }, + { id: 'IW', name: 'ItemWorld', values: ['iw'] }, + { id: 'GF', name: 'GrindFest', values: ['gr'] }, + { id: 'TW', name: 'Tower', values: ['tw'] }, + ], + equipSlot: [ + { id: '1', names: ['主手', '主手', 'Main Hand'] }, + { id: '2', names: ['副手', '副手', 'Off Hand'] }, + { id: '13', names: ['头盔', '頭盔', 'Helmet'] }, + { id: '11', names: ['身体', '身體', 'Body'] }, + { id: '14', names: ['手部', '手部', 'Hands'] }, + { id: '12', names: ['腿部', '腿部', 'Legs'] }, + { id: '15', names: ['脚部', '腳部', 'Feet'] }, + ], + roundType: [ + { id: 'ar', names: ['竞技场(AR)', '競技場(AR)', 'The Arena'] }, + { id: 'rb', names: ['浴血擂台(RB)', '浴血擂台(RB)', 'Ring of Blood'] }, + { id: 'gr', names: ['压榨界(GF)', '壓榨界(GF)', 'GrindFest'] }, + { id: 'iw', names: ['道具届(IW)', '道具界(IW)', 'Item World'] }, + { id: 'ba', names: ['随机遭遇(ba)', '隨機遭遇(ba)', 'Encounter'] }, + { id: 'tw', names: ['塔楼(Tw)', '塔樓(Tw)', 'The Tower'] }, + ], + cure: [ + { id:'FC', names: ['完全治愈(FC)', '完全治愈(FC)', 'Full-Cure'], values: [313] }, + { id:'HE', names: ['生命秘药(HE)', '生命秘藥(HE)', 'Health Elixir'], values: [11199] }, + { id:'LE', names: ['最终秘药(LE)', '最終秘藥(LE)', 'Last Elixir'], values: [11501] }, + { id:'HG', names: ['生命宝石(HG)', '生命寶石(HG)', 'Health Gem'], values: [10005] }, + { id:'HP', names: ['生命药水(HP)', '生命藥水(HP)', 'Health Potion'], values: [11195] }, + { id:'Cure', names: ['治疗(Cure)', '治療(Cure)', 'Cure'], values: [311] }, + { id:'MG', names: ['魔力宝石(MG)', '魔力寶石(MG)', 'Mana Gem'], values: [10006] }, + { id:'MP', names: ['魔力药水(MP)', '魔力藥水(MP)', 'Mana Potion'], values: [11295] }, + { id:'ME', names: ['魔力秘药(ME)', '魔力秘藥(ME)', 'Mana Elixir'], values: [11299] }, + { id:'SG', names: ['灵力宝石(SG)', '靈力寶石(SG)', 'Spirit Gem'], values: [10007] }, + { id:'SP', names: ['灵力药水(SP)', '靈力藥水(SP)', 'Spirit Potion'], values: [11395] }, + { id:'SE', names: ['灵力秘药(SE)', '靈力秘藥(SE)', 'Spirit Elixir'], values: [11399] }, + { id:'Mystic', names: ['神秘宝石(Mystic)', '神秘寶石(Mystic)', 'Mystic Gem'], values: [10008] }, + { id:'CC', names: ['咖啡因糖果(CC)', '咖啡因糖果(CC)', 'Caffeinated Candy'], values: [11402] }, + { id:'ED', names: ['能量饮料(ED)', '能量飲料(ED)', 'Energy Drink'], values: [11401] }, + ], + buff: [ + { id: 'HD', names: ['生命长效药(HD)', '生命長效藥(HD)', 'Health Draught'], values: [true] }, + { id: 'MD', names: ['魔力长效药(MD)', '魔力長效藥(MD)', 'Mana Draught'], values: [true] }, + { id: 'SD', names: ['灵力长效药(MD)', '靈力長效藥(MD)', 'Spirit Draught'], values: [true] }, + { id: 'FV', names: ['花瓶(FV)', '花瓶(FV)', 'Flower Vase'], values: [true] }, + { id: 'BG', names: ['泡泡糖(BG)', '泡泡糖(BG)', 'Bubble-Gum'], values: [true] }, + { id: 'SS', names: ['灵力盾(SS)', '靈力盾(SS)', 'Spirit Shield'], values: [false] }, + { id: 'SL', names: ['生命火花(SL)', '生命火花(SL)', 'Spark of Life'], values: [false] }, + { id: 'Pr', names: ['守护(Pr)', '守護(Pr)', 'Protection'], values: [false] }, + { id: 'Ab', names: ['吸收(Ab)', '吸收(Ab)', 'Absorb'], values: [false] }, + { id: 'SV', names: ['影纱(SV)', '影紗(SV)', 'Shadow Veil'], values: [false] }, + { id: 'Re', names: ['细胞活化(Re)', '細胞活化(Re)', 'Regen'], values: [false] }, + { id: 'Ha', names: ['疾速(Ha)', '疾速(Ha)', 'Haste'], values: [false] }, + { id: 'He', names: ['穿心(He)', '穿心(He)', 'Heartseeker'], values: [false] }, + { id: 'AF', names: ['奥术集中(AF)', '奧術集中(AF)', 'Arcane Focus'], values: [false] }, + ], + debuff: [ + { id:'Sle', names: ['沉眠(Sl)', '沉眠(Sl)', 'Sleep'] }, + { id:'Bl', names: ['致盲(Bl)', '致盲(Bl)', 'Blind'] }, + { id:'Slo', names: ['缓慢(Slo)', '緩慢(Slo)', 'Slow'] }, + { id:'We', names: ['虚弱(We)', '虛弱(We)', 'Weaken'] }, + { id:'Si', names: ['沉默(Si)', '沉默(Si)', 'Silence'] }, + { id:'Dr', names: ['枯竭(Dr)', '枯竭(Dr)', 'Drain'] }, + { id:'Im', names: ['陷危(Im)', '陷危(Im)', 'Imperil'] }, + { id:'MN', names: ['固定(MN)', '固定(MN)', 'Immobilize(MagNet)'] }, + { id:'Co', names: ['混乱(Co)', '混亂(Co)', 'Confuse'] }, + ], + channel: [ + { id: 'FC', names: ['完全治愈(FC)', '完全治愈(FC)', 'Full-Cure'], values: [313] }, + { id: 'Cure', names: ['治疗(Cure)', '治療(Cure)', 'Cure'], values: [311] }, + { id: 'SS', names: ['灵力盾(SS)', '靈力盾(SS)', 'Spirit Shield'], values: [423] }, + { id: 'SL', names: ['生命火花(SL)', '生命火花(SL)', 'Spark of Life'], values: [422] }, + { id: 'Pr', names: ['守护(Pr)', '守護(Pr)', 'Protection'], values: [411] }, + { id: 'Ab', names: ['吸收(Ab)', '吸收(Ab)', 'Absorb'], values: [421] }, + { id: 'SV', names: ['影纱(SV)', '影紗(SV)', 'Shadow Veil'], values: [413] }, + { id: 'Re', names: ['细胞活化(Re)', '細胞活化(Re)', 'Regen'], values: [312] }, + { id: 'Ha', names: ['疾速(Ha)', '疾速(Ha)', 'Haste'], values: [412] }, + { id: 'He', names: ['穿心(He)', '穿心(He)', 'Heartseeker'], values: [431] }, + { id: 'AF', names: ['奥术集中(AF)', '奧術集中(AF)', 'Arcane Focus'], values: [432] }, + ], + infusion: [ + { id:'Divinity', names: ['神圣(Divinity)', '神聖(Divinity)', 'Divinity'] }, + { id:'Darkness', names: ['黑暗(Darkness)', '黑暗(Darkness)', 'Darkness'] }, + { id:'Flames', names: ['火焰(Flames)', '火焰(Flames)', 'Flames'] }, + { id:'Frost', names: ['冰冷(Frost)', '冰冷(Frost)', 'Frost'] }, + { id:'Lightning', names: ['闪电(Lightning)', '閃電(Lightning)', 'Lightning'] }, + { id:'Storms', names: ['风暴(Storms)', '風暴(Storms)', 'Storms'] }, + ], + scroll: [ + { id:'Sw', names: ['加速卷轴(Sw)', '加速捲軸(Sw)', 'Scroll of Swiftness'] }, + { id:'Pr', names: ['守护卷轴(Pr)', '守護捲軸(Pr)', 'Scroll of Protection'] }, + { id:'Av', names: ['化身卷轴(Av)', '化身捲軸(Av)', 'Scroll of the Avatar'] }, + { id:'Ab', names: ['吸收卷轴(Ab)', '吸收捲軸(Ab)', 'Scroll of Absorption'] }, + { id:'Sh', names: ['幻影卷轴(Sh)', '幻影捲軸(Sh)', 'Scroll of Shadows'] }, + { id:'Li', names: ['生命卷轴(Li)', '生命捲軸(Li)', 'Scroll of Life'] }, + { id:'Go', names: ['众神卷轴(Go)', '眾神捲軸(Go)', 'Scroll of the Gods'] }, + ], + weight1: [ + { id:'We', names:['虚弱(We)', '虛弱(We)', 'Weaken'], values: [12] }, + { id:'Bl', names:['致盲(Bl)', '致盲(Bl)', 'Blind'], values: [10] }, + { id:'Slo', names:['缓慢(Slo)', '緩慢(Slo)', 'Slow'], values: [15] }, + { id:'Si', names:['沉默(Si)', '沉默(Si)', 'Silence'], values: [10] }, + { id:'Sle', names:['沉眠(Sl)', '沉眠(Sl)', 'Sleep'], values: [100] }, + { id:'Im', names:['陷危(Im)', '陷危(Im)', 'Imperil'], values: [-15] }, + { id:'PA', names:['破甲(PA)', '破甲(PA)', 'Penetrated Armor'], values: [-12] }, + { id:'BW', names:['流血(Bl)', '流血(Bl)', 'Bleeding Wound'], values: [-10] }, + { id:'Co', names:['混乱(Co)', '混亂(Co)', 'Confuse'], values: [300] }, + { id:'Dr', names:['枯竭(Dr)', '枯竭(Dr)', 'Drain'], values: [2] }, + { id:'ET', names:['以太窃取(ET)', '以太竊取(ET)', 'Ether Theft'], values: [2] }, + { id:'ST', names:['灵力窃取(ST)', '靈力竊取(ST)', 'Spirit Theft'], values: [2] }, + { id:'MN', names:['固定(MN)', '固定(MN)', 'Immobilize(MagNet)'], values: [7] }, + { id:'Po', names:['流动毒性(Po)', '流动毒性(Po)', 'Spreading Poison'], values: [-10] }, + { id:'Stun', names:['眩晕(St)', '眩暈(St)', 'Stunned'], values: [290] }, + { id:'CM', names:['魔力合流(CM)', '魔力合流(CM)', 'Coalesced Mana'], values: [-20] }, + { id:'BS', names:['焚燒的靈魂(BS)', '焚燒的靈魂(BS)', 'Burning Soul'], values: [0] }, + { id:'RS', names:['鮮美的靈魂(RS)', '鮮美的靈魂(RS)', 'Ripened Soul'], values: [0] }, + ], + weight2: [ + { id: 'SS', names: ['灼烧的皮肤(SS)', '燒灼的皮膚(SS)', 'Searing Skin'], values: [-14, 5] }, + { id: 'FL', names: ['冰封的肢体(FL)', '冰封的肢體(FL)', 'Freezing Limbs'], values: [-14, 5] }, + { id: 'TA', names: ['湍流的空气(TA)', '湍流的空氣(TA)', 'Turbulent Air'], values: [-14, 5] }, + { id: 'DB', names: ['深层的烧伤(DB)', '深層的燒傷(DB)', 'Deep Burns'], values: [-19, -4] }, + { id: 'BD', names: ['崩溃的防御(BD)', '崩潰的防禦(BD)', 'Breached Defense'], values: [-19, -4] }, + { id: 'BA', names: ['钝化的攻击(BA)', '鈍化的攻擊(BA)', 'Blunted Attack'], values: [-14, 5] }, + ], + weight3: [ + { id: 'Fos', names: ['姊妹们的盛怒(FoS)', '姊妹們的盛怒(FoS)', 'Fury of the Sisters'], values: [0] }, + { id: 'Lof', names: ['未来的悲叹(LoF)', '未來的悲歎(LoF)', 'Lamentations of the Future'], values: [0] }, + { id: 'SoP', names: ['昔日的凄叫(SoP)', '昔日的淒叫(SoP)', 'Screams of the Past'], values: [0] }, + { id: 'WoP', names: ['此刻的恸哭(WoP)', '此刻的慟哭(WoP)', 'Wailings of the Present'], values: [0] }, + { id: 'AW', names: ['吸收结界(AW)', '吸收結界(AW)', 'Absorbing Ward'], values: [0] }, + ], + skill: [ + { id: 'OFC', names: ['友情小马炮(OFC)', '友情小馬砲(OFC)', 'OFC'] }, + { id: 'FRD', names: ['龙吼(FRD)', '龍吼(FRD)', 'FRD'] }, + { id: 'T3', names: ['3阶(如果有)', '3階(如果有)', 'T3(if exist)'] }, + { id: 'T2', names: ['2阶(如果有)', '2階(如果有)', 'T2(if exist)'] }, + { id: 'T1', names: ['1阶', '1階', 'T1'] }, + ], + record1: [ + { id: 'turn', names: ['Turns'] }, + { id: 'round', names: ['Rounds'] }, + { id: 'battle', names: ['Battle'] }, + { id: 'monster', names: ['Monster'] }, + { id: 'boss', names: ['Boss'] }, + { id: 'evade', names: ['闪避', '閃避', 'Evade'] }, + { id: 'miss', names: ['未命中', '未命中', 'Miss'] }, + { id: 'focus', names: ['集中', '集中', 'Focus'] }, + { id: 'mp', names: ['MP 总消耗', 'MP 總消耗', 'MP Cost'] }, + { id: 'oc', names: ['OC 总消耗', 'OC 總消耗', 'OC Cost'] }, + ], + record2: [ + { id: 'restore', names: ['回复 (总量)', '回复 (總量)', 'Restore (Amount)'] }, + { id: 'items', names: ['物品 (次数)', '物品 (次數)', 'Items (Count)'] }, + { id: 'magic', names: ['技能 (次数)', '技能 (次數)', 'Magic (Count)'] }, + { id: 'damage', names: ['伤害 (总量)', '傷害 (總量)', 'Damage (Amount)'] }, + { id: 'proficiency', names: ['熟练度 (总量)', '熟練度 (總量)', 'Proficiency (Amount)'] }, + ], + record3: [ + { id: 'hurtavg', names: ['平均', '平均', 'Avg'] }, + { id: 'hurtcount', names: ['次数', '次數', 'Count'] }, + { id: 'hurttotal', names: ['总量', '總量', 'Total'] }, + { id: 'hurtmavg', names: ['法术平均', '法術平均', 'Magic Avg'] }, + { id: 'hurtmcount', names: ['法术次数', '法術次數', 'Magic Count'] }, + { id: 'hurtmtotal', names: ['法术总量', '法術總量', 'Magic Total'] }, + { id: 'hurtpavg', names: ['物理平均', '物理平均', 'Physical Avg'] }, + { id: 'hurtpcount', names: ['物理次数', '物理次數', 'Physical Count'] }, + { id: 'hurtptotal', names: ['物理总量', '物理總量', 'Physical Total'] }, + ], + audio: [ + { id: 'Common', names: ['通用', '通用', 'Common']}, + { id: 'Pause', names: ['暂停', '暫停', 'Pause'], values: ['Common'] }, + { id: 'Flee', names: ['逃跑', '逃跑', 'Flee'], values: ['Common'] }, + { id: 'Error', names: ['错误', '錯誤', 'Error'] }, + { id: 'Defeat', names: ['失败', '失敗', 'Defeat'] }, + { id: 'Riddle', names: ['答题', '答題', 'Riddle'] }, + { id: 'Victory', names: ['胜利', '勝利', 'Victory'] }, + ], }; - gE('.hvAATabmenu', optionBox).onclick = function (e) { // 标签页事件 - if (e.target.tagName === 'INPUT') { - return; - } - const target = (e.target.tagName === 'SPAN') ? e.target : e.target.parentNode; - const name = target.getAttribute('name'); - let i; let - _html; - if (name === 'Drop') { // 掉落监测 - let drop = getValue('drop', true) || {}; - const dropOld = getValue('dropOld', true) || []; - drop = objSort(drop); - _html = ''; - if (dropOld.length === 0 || (dropOld.length === 1 && !getValue('drop', true))) { - if (dropOld.length === 1) { - drop = dropOld[0]; - } - _html = `${_html}数量數量Amount`; - for (i in drop) { - _html = `${_html}${i}${drop[i]}`; + + let option = getOption(true); + let optionBox = gE('#hvAABox'); + if (!optionBox) { + optionBox = gE('body').appendChild(cE('div')); + optionBox.id = 'hvAABox'; + optionBox.innerHTML = [ + UI.div({ + args: { class: 'hvAACenter' }, + inner: [ + `${UI.l('更新历史', '更新歷史', 'ChangeLog')}`, + '使用说明README', + 'by Koko191', + '

    hvAutoAttack

    ', + '', + (option.optionStandalone ? _server.isekai ? UI.l('当前为异世界单独配置', '當前為異世界單獨配置', 'Using Isekai standalone option') : UI.l('当前为恒定世界单独配置', '當前為恆定世界單獨配置', 'Using Persistent standalone option') : ''), + UI.l('配置版本', '配置版本', 'Option Version'), + UI.text('version', 'disabled="true"') + ] + }), + UI.div({ + args: { class: 'hvAATablist' }, + inner: [ + UI.div({ + args: { class: 'hvAATabmenu' }, + inner: UI.expendData(UIDatas.tablist, (id, names, v) => `${v ? `${UI.label(v, names, 'hidden')}` : ''}${names}`), + }), + UI.hvAATab( + 'Main', + UI.div( + UI.b(UI.l('异世界相关', '異世界相關', 'Isekai')), + ': ', + `${UI.labeled(`optionStandalone`, UI.l('两个世界使用不同的配置', '兩個世界使用不同的配置', 'Use standalone options.'))}`, + '
    ', + `${UI.labeled(`isekai`, `${UI.l('自动切换恒定世界和异世界', '自動切換恆定世界和異世界', 'auto switch between Isekai and Persistent')}`)}`, + '
    ', + UI.div({ + args: { class: 'isekaiInner' }, + inner: [ + `${UI.l('闲置达到', '閒置達到', 'While idled for ')}${UI.number('isekaiTime')}${UI.l('秒后切换', '秒後切換', ' (s), switch between isekai and persistent')}${UI.label('isekaiTime', UI.l('异世界切换时间', '異世界切換時間', 'Isekai Switch Wait'), 'hidden')}`, + ``, + '
    ', + '', + UI.l('距离开始计算闲置 ', '距離開始計算閒置', 'Time before recording idle '), + ' ', + '
    ', + `${UI.label('isekaiCD', UI.l('自动切换冷却时间', '自動切換冷卻時間', 'Cool down for auto switch'))}: ${UI.number('isekaiCD')}${UI.l('秒. 两个世界分别计算冷却', '秒. 兩個世界分別計算冷卻', ' (s). Isekai and Persistent cooldown separately')}. ` + ], + }), + ), + UI.div( + UI.b(UI.l('小马答题', '小馬答題', 'RIDDLE')), + ': ', + UI.labeled(`riddlePopup`, `${UI.l('弹窗答题', '弹窗答题', 'POPUP a window to answer')}`), + `${UI.l('(Firefox中可能导致报错)', '(Firefox中可能導致報錯)', '(Might cause in Firefox)')}; `, + UI.button.class('testPopup', UI.l('预处理', '預處理', 'Pretreat')), + UI.label(`riddleAnswerTime`, UI.l('随机答题时间', '隨機答題時間', 'Random Riddle Time'), 'hidden'), + UI.label(`riddleAnswerChoose`, UI.l('随机答题数量', '隨機答題數量', 'Random Riddle Count'), 'hidden'), + UI.div( + `${UI.l('时间', '時間', 'If ETR')} ≤ ${UI.number('riddleAnswerTime', 3)}${UI.l('秒,提交当前选中答案 或 为空时随机选中', '秒,提交當前選中答案 或 為空時隨機選中', 's submit chosen answers or random ')} ${UI.number('riddleAnswerChoose')}${UI.l( + '个答案并提交
    注意:错选小马比漏选小马的错误计数更多 - 所以有疑问时,最好不要猜测,留空就好', + '个答案並提交
    注意:錯選小馬比漏選小馬的錯誤計數更多 - 所以有疑問時,最好不要猜測,留空就好', + 'answers if none is chosen.
    Notice: Selecting a pony that is not in the picture will count more severe towards a penalty than missing one pony - so when in doubt, best not to guess but leave one blank' + )}` + ), + ), + UI.div( + UI.b(UI.l('脚本行为', '腳本行為', 'Script Activity')), + UI.expendData(UIDatas.hotkeys, (id, names, v) => UI.div( + `${UI.labeled(`${id}Button`, `${names}${UI.l('按钮', '按鈕', ' Button')}`)}; ${UI.labeled(`${id}Hotkey`, `${names}${UI.l('热键', '熱鍵', ' Hotkey')}${UI.text(`${id}HotkeyStr`)}`)}${UI.label(`${id}HotkeyStr`, `${names}${UI.l('热键', '熱鍵', ' Hotkey')}`, 'hidden')}: `, + `${UI.number(`${id}HotkeyCode`, 'undefined', 'hidden', '', 'disabled="true"')}`)), + UI.div( + `${UI.l('警告相关', '警告相關', ' To Warn')}`, + ': ', + `${UI.labeled(`alert`, UI.l('音频警报', '音頻警報', 'Audio Alarms'))}; `, + `${UI.labeled(`notification`, UI.l('桌面通知', '桌面通知', 'Notifications'))}; `, + UI.button.class('testNotification', UI.l('预处理', '預處理', 'Pretreat')), + `${UI.labeled(`focusNotification`, UI.l('桌面通知时聚焦页面(需要GM_notification)', '桌面通知時聚焦頁面(需要GM_notification)', 'Focus while Notifications (Requires GM_notification)'), 'placeholder="true"')}; `, + ), + UI.div( + UI.l('掉落及数据记录', '掉落及數據記錄', 'Drops and Usage Tracking'), + ': ', + `${UI.labeled(`recordEach`, UI.l( + '单独记录每场战役(建议使用便携数据模式以避免超出浏览器的localStorage配额限制,但请注意便携数据模式可能会显著增加硬盘读写量)', + '單獨記錄每場戰役(建議使用便攜數據模式以避免超出瀏覽器localStorage配額限制,但請注意便攜數據模式可能會顯著增加硬盤讀寫)', + 'Record each battle separately (It is recommended to use portable mode to prevent exceeding the localStorage quota, but note that this may significantly increase disk read/write activity.)' + ))}`), + UI.div( + `${UI.label(['delay', 'delay2'], UI.l('延迟', '延遲', 'Delay'))}: 1. ${UI.label('delay', UI.l('Buff/Debuff/其他技能', 'Buff/Debuff/其他技能', 'Skills&BUFF/DEBUFF Spells'))}: ${UI.number('delay', 200)}ms 2. ${UI.label('delay2', UI.l('其他', '其他', 'Other'))}: ${UI.number('delay2', 30)}ms (`, + UI.l('说明: 单位毫秒,且在设定值基础上取其的50%-150%进行延迟,0表示不延迟', '說明: 單位毫秒,且在設定值基礎上取其的50%-150%進行延遲,0表示不延遲', 'Note: unit milliseconds, and based on the set value multiply 50% -150% to delay, 0 means no delay'), + ), + UI.div(UI.l('频率指示符号', '頻率指示符號', 'Frequency Signal'), ': ', UI.text('frequencySign1'), ' & ', UI.text('frequencySign2')), + UI.label('frequencySign1,', UI.l('频率指示符号 1', '頻率指示符號 1', 'Frequency Signal 1'), 'hidden'), + UI.label('frequencySign2,', UI.l('频率指示符号 2', '頻率指示符號 2', 'Frequency Signal 2'), 'hidden'), + ), + UI.div({ + args: { style: 'color:red;' }, + inner: [ + UI.b(UI.label('attackStatus', UI.l('*默认攻击模式', '*默認攻擊模式', '*Default Attack Mode'))), + ': ', + ''] + }), + UI.div( + UI.b(UI.l('战斗执行顺序(未配置的按照下面的顺序)', '戰鬥執行順序(未配置的按照下面的順序)', 'Battal Order(Using order below as default if not configed)')), + ': ', + UI.label(`battleOrderName`, UI.l('战斗执行顺序', '戰鬥執行順序', 'Battal Order')), + UI.labeled('battleOrderDefaultOnly', UI.l('只使用默认顺序', '只使用默認順序', 'Default order only'), 'class="battleOrderNameInner"'), + UI.div({ + args: { class: 'battleOrder battleOrderDefaultOnlyInnerReverted' }, + inner: [ + UI.orderValue('battleOrderName'), + UI.hvAATable(UI.repeat(7), '', UI.expendData(UIDatas.battleOrder, (id, names, v) => UI.div(`${UI.labeled(`battleOrder_${id}`, names, `value="${v}"`, 'class="battleOrderNameInner"')}`))) + ]}), + ), + UI.div( + UI.b( + UI.label(['attackStatusOrderName', ...range(0, 7).map(s => `attackStatusOrder_${s}`)], UI.l('次要攻击模式顺序', '次要攻擊模式順序', 'Attack Mode Order')), + UI.l('(未配置的按照下面的顺序)', '(未配置的按照下面的順序)', '(Using order below as default if not configed)') + ), + ':', + `${UI.labeled(`attackStatusSwitchByTier`, UI.l('先尝试完所有模式的高阶魔法技能再继续中阶和低阶', '先嘗試完所有模式的高階魔法技能再繼續中階和低階', 'Try all 3rd Tier Magic for all Attack Mode then 2nd Tier and 1st Tier'))}`, + UI.div({ + args: { class: 'attackStatusOrder' }, + inner: [ + UI.orderValue('attackStatusOrderName'), + UI.orderValue('attackStatusOrderValue', true), + UI.hvAATable(UI.repeat(7), '', UI.expendData(UIDatas.attackStatus, (id, names, v) => UI.div(`${UI.labeled(`attackStatusOrder_${id}`, names, `value="${v},${id}"`)}`))), + ] + }), + ), + UI.expendData(UIDatas.attackStatus.map(x => x).sortBy(x => x.id), (id, names) => UI.div(`${UI.labeled(`attackStatusSwitch_${id}`, UI.b(`${UI.l('攻击模式', '攻擊模式', 'Attack Mode')}: ${names}`))}: {{attackStatusSwitchCondition${id}}}`)), + UI.expendData(UIDatas.battleCommons, (id, names, v) => UI.div(`${UI.labeled(id, `${names}`, v !== undefined ? `placeholder="${v}"`:'')}: {{${id}Condition}}`)), + UI.expendData(UIDatas.battleBreaks, (id, names, v) => UI.div(`${UI.labeled(id, `${names}`)}${UI.labeled(`${v}Alarm`, UI.l('警报', '警報', 'Alert'))}: {{${v}Condition}}`)), + UI.div(`${UI.labeled(`autoSkipDefeated`, UI.b(UI.l('战败自动退出战斗', '戰敗自動退出戰鬥', 'Exit battle when defeated.')))}`), + UI.div(`${UI.labeled(`nativeNewRound`, UI.b(UI.l('使用原生方式进入新回合', '使用原生方式進入新回合', 'Native new round')))}`), + UI.div( + UI.label('checkURLBeforeNewRound,checkURLBeforeNewRoundRetry,', UI.l('新回合前检查链接:', '新回合前檢查連接:', 'Check url before new round: ')), + UI.text('checkURLBeforeNewRound'), + UI.number('checkURLBeforeNewRoundRetry', 5), + UI.label('checkURLBeforeNewRoundRetry', UI.l('秒后重试', '秒後重試', '(s) to retry')) + ), + UI.div(UI.hvAATable( + UI.repeat(3), '', + UI.div(UI.b(UI.l('延时', '延時', 'Wait time for'))), + UI.expendData(UIDatas.battleExitDelay, (id, names, v) => UI.div( + UI.label(`${id}WaitTime`, names), + ': ', + UI.number(`${id}WaitTime`, v), + UI.l('(秒)', '(秒)', '(s)') + )), + )), + UI.div(UI.hvAATable( + '1fr 1fr 1.5fr 2fr', '', + UI.div(UI.b(UI.l('战斗页面停留 ', '戰鬥頁面停留 ', 'If not active for '))), + UI.expendData(UIDatas.battleUnresponsive, (id, names, v) => UI.div( + UI.label(`battleUnresponsive_${id}`, UI.l('战斗页面停留 ', '戰鬥頁面停留 ', 'If not active for ')), + UI.label(`battleUnresponsiveTime_${id}`, `${UI.l('战斗页面停留 ', '戰鬥頁面停留 ', 'If not active for ')} ${names}`), + UI.labeled(`battleUnresponsive_${id}`, `${UI.number(`battleUnresponsiveTime_${id}`, 1)} ${UI.l('秒,', '秒,', '(s), ')} ${names}`) + )) + )) + ), + UI.hvAATab( + 'BattleStarter', + UI.div(`${UI.labeled(`popup`, UI.l('进入失败时窗口内弹窗提示', '進入失敗時窗口內彈窗提示', 'In-window popup while failed start'))}`), + UI.div(`${UI.labeled(`altBattleFirst`, UI.b(UI.l('优先使用alt进入', '優先使用alt進入', 'Use alt.hentaiverse as default while auto start.')))}`), + UI.div( + UI.b(UI.l('随机遭遇战', '隨機遭遇戰', 'Random Encounter')), + '
    ', + `${UI.labeled(`encounter`, UI.l('自动遭遇', '自動遭遇', 'Auto Engage'))}`, + '', + `${UI.label(`encounterWaitCD`, UI.l('自动遭遇优先等待', '自動遭遇優先等待', 'Encounter Engage Wait First'), 'hidden')}`, + `${UI.l('倒计时', '倒計時', 'Wait first while count down')} ≤ ${UI.number('encounterWaitCD')}s ${UI.l('时优先等待', '時優先等待', '.')}; `, + '
    ', + `${UI.label(`encounterDelay`, UI.l('自动遭遇额外等待', '自動遭遇額外等待', 'Encounter Engage Extra Delay'), 'hidden')}`, + UI.l('进入前额外等待', '進入前額外等待', 'Extra delay '),UI.number('encounterDelay', 5),UI.l(' 秒,避免时间偏差导致需要等多一轮', ' 秒,避免時間偏差導致需要等多一輪', ' (s) before engage to fit potential time bias which might cause extra waiting round.'), + '; ', + '
    ', + '
    ', + UI.l('倒计时显示: ', '倒計時顯示: ', 'Count down display: '), + UI.labeled(`encounterQuickCheck`, UI.l('精准(影响性能); ', '精準(影響性能); ', 'Precise(might reduced performsance); ')), + UI.labeled(`encounterDisplay`, UI.l('不自动遭遇时显示', '不自動遭遇時顯', 'Display CountDown While Not Auto Engage')), + ), + UI.div( + UI.div(UI.labeled(`idleArena`, UI.b(UI.l('闲置竞技场: ', '閒置競技場: ', 'Idle Arena: ')))), + UI.label(`idleArenaLevels`, `${UI.l('闲置竞技场', '閒置競技場', 'Idle Arena')}`, 'hidden'), + '', + '', + `${UI.label(`onIdleDelay`, UI.l('闲置前等待', '閒置前等待', 'Time before Idle'), 'hidden')}`, + UI.l('等待', '等待', 'Wait '),UI.number('onIdleDelay', 5),UI.l(' 秒后计算闲置时间', ' 秒後計算閒置時間', ' (s) before start recording idled duration.'), + ';
    ', + '
    ', + UI.label(`idleArenaTime`, UI.l('在任意页面闲置: ', '在任意頁面閒置: ', 'Idle in any page for ')), + UI.number('idleArenaTime'), + UI.l('秒后,开始竞技场', '秒後,開始競技場', ' (s), start Arena'), + UI.button.class('idleArenaReset', UI.button.reset), + ';
    ', + UI.l('进行的竞技场相对应等级', '進行的競技場相對應等級', 'The levels of the Arena you want to complete'), + ': ', + UI.button.class('hvAAShowLevels', UI.button.details()), + UI.button.class('hvAALevelsClear', UI.button.clear), + '
    ', + UI.text('idleArenaLevels', 'style="width:calc(100% - 20px);" disabled="true"'), + UI.text('idleArenaValue', 'style="width:98%;" type="hidden" disabled="true"'), + UI.label(`idleArenaGrTime`, `${UI.l('闲置竞技场 GF次数', '閒置競技場 GF次數', 'Idle Arena Gr Time')}`, 'hidden'), + UI.label(`idleArenaTwTime`, `${UI.l('闲置竞技场 TW次数', '閒置競技場 TW次數', 'Idle Arena Tw Time')}`, 'hidden'), + UI.label(`idleArenaTwMax`, `${UI.l('闲置竞技场 TW层数', '閒置競技場 TW層數', 'Idle Arena Tw Floor')}`, 'hidden'), + UI.hvAATable(UI.repeat(5)+';display:none;', 'hvAAArenaLevels', UI.expendData(UIDatas.arena, (id, names, v) => UI.hvAATable( + '1fr 80px;width: 100%', '', + UI.div({ args: 'style="border: unset"', inner: [ + UI.labeled(`arLevel_${id}`, UIDatas.arena.find(ar => ar.id === id).name ?? id, `value="${id},${v}"`), + id === 'GF' ? UI.number('idleArenaGrTime', 1, 'number', `arLevel_GFInner`) : '', + id === 'TW' ? UI.number('idleArenaTwTime', 0, 'number', `arLevel_TWInner`)+'
    & Floor ≤ '+UI.number('idleArenaTwMax', 0, 'number', `arLevel_TWInner`) : '', + UI.label(`arLevel_${id}`, `${UI.l(' 竞技场顺序', ' 閒置競順序', ' Arena Order')}`, 'hidden'), + ]}), + UI.div({ args: 'style="border: unset"', inner: [ + UI.labeled(`arLevelDisable_${v}`, UI.l('禁用', '禁用', 'Disable'), `class="arLevel_${id}Inner"`), + UI.label(`arLevelDisable_${v}`, `${UI.l('闲置竞技场', '閒置競技場', 'Idle Arena')} ${id}`, 'hidden'), + ]}) + ))), + UI.div(`${UI.labeled(`skipUnclearedArena`, UI.l('跳过未通关过的', '跳過未通關過的', 'Skip not cleared Arena/RingOfBlood'), `placeholder="true"`)}`), + UI.div(`${UI.labeled(`obscureNotIdleArena`, UI.l('页面中置灰未设置且未完成的', '頁面中置灰未設置且未完成的', 'obscure not setted and not battled in Battle>Arena/RingOfBlood'))}`), + UI.div( + `${UI.labeled(`idleItemWorld`, UI.b(`${UI.l('道具界列表', '道具界列表', 'Item World List')}[0/0]`), `placeholder="true"`)}`, + UI.button.class('updateItemWorld', UI.button.update), + UI.button.class('hvAAShowItemWorld', UI.button.details()), + UI.button.class('hvAAClearItemWorld', UI.button.clear), + '
    ', + UI.hvAATable('0.2fr 3fr 0.2fr 1fr 1fr;display:none', 'autoItemWorldList'), + ), + '
    ', + ), + UI.div( + UI.b('[S!]', UI.l('精力: 进入战斗的最低精力', '精力: 戰鬥的最低精力', 'Stamina: Minimum stamina to auto start battles')), + ':
    ', + UI.expendData(UIDatas.staminaCheck, (id, names, v) => `${UI.label(`stamina${id}`, UI.l('精力: ', '精力: ', 'Stamina: ')+names, 'hidden')}${id === 'LowWithReNat' ? UI.b('
    [S!!]') : ''}${names}: ${id === 'Low' ? 'Min(85, ' : ''}${UI.number(`stamina${id}`, v)}${id === 'Low' ? ')' : ''};`), + '
    ', + `${UI.labeled(`restoreStamina`, UI.l('战前恢复', '戰前恢復', 'Restore stamina'))}`, + `${UI.labeled(`staminaRatio`, UI.l('检查惩罚倍率', '檢查懲罰倍率', 'Check Punishment Ratio'))}`, + ), + UI.div( + UI.labeled('repair', UI.b('[R!]', UI.l('修复装备', '修復裝備', 'Repair Equipment'))), + ': ', + UI.expendData(UIDatas.repair, (id, names) => `${UI.label(`repairValue${id}`, `${names}${UI.l('耐久度', '耐久度', ' Durability')}`, 'hidden')} ≤ ${UI.number(`repairValue${id}`)}% `), + UI.expendData(UIDatas.repairCharm, (id, names) => `
    ${UI.labeled(`repairCharm${id}`, `${UI.l('', '', 'Repair charm before ')}${names}${UI.l('前修复护石', '前修復護石')}`)};`), + '
    ', + UI.labeled('encounterRepair', UI.l('遭遇战前检查', '遭遇戰前檢查', 'Check before encounter'), 'class="repairInner"'), + UI.div(UI.l('检查非空装备槽位时忽略: ', '檢查非空裝備槽位時忽略: ', 'Skip when checking unslotted equipments: ')), + UI.hvAATable( + UI.repeat(7), 'hvAAcheckItems', + UI.expendData(UIDatas.equipSlot, (id, names) => UI.div(UI.labeled(`equipCheckSkip_${id}`, ``+names))) + ), + '
    ', + ), + UI.div( + UI.labeled(`equStorage`, UI.b('[E!]', UI.l('装备库存', '裝備庫存', 'Equipment Storage'))), + ' ≤ ', + UI.label('equStorageValue', UI.l('装备库存', '裝備庫存', 'Equipment Storage'), 'hidden'), + UI.number('equStorageValue', 150, 'number', '', 'style="width: 32px;"'), + `; ${UI.labeled(`encounterEquStorage`, UI.l('遭遇战前检查', '遭遇戰前檢查', 'Check before encounter'), 'class="equStorageInner"')}`), + UI.div( + UI.labeled(`changeEquipSet`, UI.b(UI.l('[!!实验性]切换套装', '[!!實驗性]切換套裝', '[!!Experimental]Switch Equip Set'))), + ``, + UI.button.class('updateEquipSet', UI.button.update), + UI.button.class('hvAAShowEquipSet', UI.button.details()), + '
    ', + UI.hvAATable(UI.repeat(3) + ';display:none', 'equipSetList changeEquipSetInner'), + ), + UI.div( + UI.labeled(`checkSupplySlotted`, UI.b('[C!]', UI.l('检查物品是否装备', '檢查物品是否裝備', 'Check is item slotted'), ';')), + ...getCheckSupplyOptionTable('Slotted', true), + ), + UI.div( + UI.labeled(`checkSupply`, UI.b('[C!]', UI.l('检查物品库存', '檢查物品庫存', 'Check is item needs supply'), ';')), + '', + UI.labeled(`encounterSupply`, UI.l('遭遇战前检查', '遭遇戰前檢查', 'Check before encounter')), + '
    ', + ...getCheckSupplyOptionTable(), + ), + UI.expendData(UIDatas.checkSupplyInnerExtra, (id, names, v) => UI.checkSupplyInnerExtra(id, names)), + ), + UI.hvAATab( + 'Recovery', + UI.div({ + args: { class: 'itemOrder' }, + inner: [ + UI.b(UI.l('施放顺序(未配置的按照下面的顺序)', '施放順序(未配置的按照下面的順序)', 'Cast Order(Using order below as default if not configed)')), + ': ', + UI.label(`itemOrderName`, UI.l('恢复技能/道具施放顺序', '恢復技能/道具施放順序', 'Cure Skill / Item Cast Order'), 'hidden'), + UI.text('itemOrderName', 'style="width:80%;"', 'disabled="true"'), + '
    ', + UI.hvAATable(UI.repeat(5), '', UI.expendData(UIDatas.cure, (id, names, v) => UI.div(UI.labeled(`itemOrder_${id}`, names, `value="${id},${v}"`, 'class="itemOrderNameInner"')))), + ] + }), + UI.expendData(UIDatas.cure, (id, names, v) => UI.div(`${UI.labeled(`item_${id}`, names)}: {{item${id}Condition}}`)), + ), + UI.hvAATab( + 'Channel', + UI.div( + UI.l('获得引导时(此时1点MP施法与150%伤害)', '獲得引導時(此時1點MP施法與150%傷害)', 'During Channeling effect (1 mp spell cost and 150% spell damage)'), ':' + ), + UI.div( + UI.b( + UI.l('超过时不释放', '超過時不釋放', 'Not cast if remain turns above'), + ' (', + UI.l('阈值 < 0 则不限制', '閾值 < 0 則不限制', ' Threshold < 0 as unlimited'), + ), + ') : ', + UI.hvAATable( + UI.repeat(5), '', + UI.expendData(UIDatas.buff, (id, names, v) => v ? '' : UI.div( + ``)) + ), + ), + UI.div( + UI.b(UI.l('先施放引导技能', '先施放引導技能', 'First cast')), + ':
    ', + UI.l('注意: 此处的施放顺序与', '注意: 此處的施放順序与', 'Note: The cast order here is the same as in'), + '', + UI.l('BUFF 技能', 'BUFF 技能', 'BUFF Spells'), + '', + UI.l('里的相同', '裡的相同'), + '
    ', + UI.hvAATable(UI.repeat(9), '', UI.expendData(UIDatas.buff, (id, names, v) => v ? '' : UI.div(UI.labeled(`channelSkill_${id}`, ``+names)))), + ), + UI.div( + UI.labeled('channelSkill2', UI.b(UI.l('再使用技能', '再使用技能', 'Then use Skill'))), + UI.div({ + args: { class: 'channelSkill2Order channelSkill2Inner', style:'grid-template-columns:repeat(5, 1fr);'}, + inner: [ + UI.label('channelSkill2OrderName', UI.l('施放顺序', '施放順序', 'Cast Order')), + ': ', + UI.text('channelSkill2OrderName', 'style="width:80%;"', 'disabled="true"'), + UI.text('channelSkill2OrderValue', 'style="width:80%;"', 'disabled="true"', 'hidden'), + '
    ', + UI.div({ + args: { class: 'hvAATable', style: 'grid-template-columns: repeat(6, 1fr);' }, + inner: UI.expendData(UIDatas.channel, (id, names, v) => UI.div(UI.labeled(`channelSkill2Order_${id}`, ``+names, `value="${id},${v}"`))), + }), + ] + }), + ), + UI.div(UI.labeled('channelRebuff', UI.l('最后ReBuff: 重新施放最先将要消失的Buff', '最後ReBuff: 重新施放最先將要消失的Buff', 'At last, re-cast the spells which will expire first'))), + ), + UI.hvAATab( + 'Buff', + UI.div({ + args: { class: 'buffSkillOrder '}, + inner: [ + UI.l('施放顺序(未配置的按照下面的顺序)', '施放順序(未配置的按照下面的順序)', 'Cast Order(Using order below as default if not configed)'), + ': ', + UI.label('buffSkillOrderValue', UI.l('Buff 施放顺序', 'Buff 施放順序', 'Buff Cast Order'), 'hidden'), + UI.text('buffSkillOrderValue', 'style="width:80%;" disabled="true"'), + '
    ', + UI.expendData(UIDatas.buff, (id, names, v) => v ? '' : UI.labeled(`buffSkillOrder_${id}`, names, 'class="buffSkillOrderValueInner"')), + ], + }), + UI.div(UI.label('buffSkillCondition', UI.l('Buff释放条件', 'Buff釋放條件', 'Cast buff spells Condition')), '{{buffSkillCondition}}'), + UI.expendData(UIDatas.buff, (id, names, v) => UI.div( + UI.label(`buffSkillThreshold_${id}`, `${names}${UI.l('阈值', '閾值', ' Threshold')}`, 'hidden'), + `${UI.labeled(`buffSkill_${id}`, `${names} <= ${UI.number(`buffSkillThreshold_${id}`)} (${UI.l('阈值 < 0 则不限制', '閾值 < 0 則不限制', ' Threshold < 0 as unlimited')})`)}{{buffSkill${id}Condition}}`)), + ), + UI.hvAATab( + 'Debuff', + UI.div(UI.label('debuffSkillCondition', UI.l('Debuff释放条件', 'Debuff釋放條件', 'Cast debuff spells Condition')), '{{debuffSkillCondition}}'), + UI.div(`${UI.labeled('debuffAutoFill', UI.l('[!!实验性]补全因超过默认显示上限未显示的怪物buff', '[!!實驗性]補全因超過默認顯示上限未顯示的怪物buff', '[!!Experimental]Auto fill hidden monster buffs due to display limitation'))}${UI.labeled('debuffAutoFillRec', 'DEBUG RECORD')}`), + UI.div( + UI.l('超出6个debuff的默认显示上限时(例如同时使用jpx时可忽略上限):', '超出6個debuff的默認顯示上限時(例如同時使用jpx時可忽略上限):', 'When debuff count overflows 6 as the default maximum display count (such as ignore limitation while using jpx): '), + '
    ', + UI.l('剩余Turns低于阈值时警报', '剩餘Turns低於閾值時警報', 'Alert when remain expire turns less than threshold'), + '
    ', + UI.hvAATable(UI.repeat(9), '', UI.expendData(UIDatas.debuff, (id, names) => UI.div( + UI.label(`debuffSkillTurn_${id}`, `${names}${UI.l('警报', '警報', 'Alert')}`, 'hidden'), + names, UI.number(`debuffSkillTurn_${id}`) + ))), + ), + UI.div({ + args: { class: 'debuffSkillOrderAll' }, + inner: [ + '1. ', + UI.label('debuffSkillOrderAllValue', UI.l('特殊先给全体施放的顺序(未配置的按照下面的顺序)', '特殊先給全體施放的順序(未配置的按照下面的順序)', 'Cast Order for Special Debuff all enemies first(Using order below as default if not configed)')), + ':', + UI.text('debuffSkillOrderAllValue', 'style="width:80%;" disabled="true"'), + '
    ', + UI.hvAATable(UI.repeat(7) + ' 1.5fr 1fr;', '', UI.expendData(UIDatas.debuff, (id, names) => UI.div(UI.labeled(`debuffSkillOrderAll_${id}`, names, 'class="debuffSkillOrderAllValueInner"')))), + ] + }), + UI.div( + '1.a. 特殊先给全体施放时,视作覆盖的互斥Debuff特殊特殊先給全體施放時,視作覆蓋的互斥DebuffExclusive debuffs during \'Cast Order for Special Debuff all enemies first\':', + UI.hvAATable(UI.repeat(7) + ' 1.5fr 1fr;', '', UI.expendData(UIDatas.debuff, (id, names) => UI.div(UI.labeled(`debuffAllExclusive_${id}`, names+UI.label(`debuffSkillTurn_${id}`, UI.l('互斥', '互斥', 'Exclusive'), 'hidden'))))), + ), + + '
    2. 单体施放顺序(未配置的按照下面的顺序)單體施放順序(未配置的按照下面的順序)Cast Order for each enemy(Using order below as default if not configed):', + UI.label(`debuffSkillOrderValue`, UI.l('单体Debuff施放顺序', '單體Debuff技能施放順序', 'Debuff for each Cast Order')), + UI.text('debuffSkillOrderValue', 'style="width:80%;" disabled="true"'), + '
    ', + UI.hvAATable(UI.repeat(7) + ' 1.5fr 1fr;', '', UI.expendData(UIDatas.debuff, (id, names) => UI.div(UI.labeled(`debuffSkillOrder_${id}`, names, 'class="debuffSkillOrderValueInner"')))), + '
    ', + + UI.div( + '特殊先给全体施放和单体施放使用共享的阈值、重复命中权重和各自独立的条件特殊先給全體施放和單體施放使用共享的閾值、重複命中權重和各自獨立的條件Using sharing threshold/duplicateCastWeight and standalone conditions between special cast for debuff all enemies first and cast for debuff each enemy
    ', + 'Buff持续时间 <= 释放阈值时可释放,阈值 < 0 则不限制Buff持續時間 <= 釋放閾值時可釋放,閾值 < 0 則不限制Cast available while buff remain duration <= threshold, threshold < 0 as unlimited
    ', + 'EWF: 重复释放权重公式重複釋放的權重公式Excluded Weight Formula for duplicate debuff targets', + ), + UI.hvAATable( + UI.repeat(2) + ';width: 100%', '', + UI.expendData(UIDatas.debuff, (id, names) => [ + UI.div( + UI.labeled(`debuffSkill_${id}`, names), + UI.label(`debuffSkillThreshold_${id}`, `${names}${UI.l('阈值', '閾值', ' Threshold')}`, 'hidden'), + UI.number(`debuffSkillThreshold_${id}`), + '; ', + UI.label(`excludedWeightFormula_${id}`, `${names} EWF`), + ': ', + UI.number(`excludedWeightFormula_${id}`, 900), + `{{debuffSkill${id}Condition}}` + ), + UI.div( + UI.l('特殊 ', '特殊 ', ' Special '), + UI.labeled(`debuffSkill${id}All`, `${UI.l('先给全体上', '先給全體上')}${names}${UI.l('', '', ' all enemies first.')}`), + ``, + `${UI.labeled(`debuffSkill${id}AllByIndex`, UI.l('按照顺序而非权重', '按照順序而非權重', 'By index instead of weight'), `class="debuffSkill${id}AllInner"`)}`, + `{{debuffSkill${id}AllCondition}}`) + ]), + ), + ), + UI.hvAATab( + 'Skill', + UI.div( + UI.labeled('skillSSOnly', `只在灵动架式状态下使用只在靈動架式狀態下使用Only use skills under Spirit by default`, 'placeholder="true"'), + '
    (请在主要选项勾选并设置开启/关闭灵动架式)(請在主要選項勾選並設置開啟/關閉靈動架式)(please check and set the Turn on/off Spirit Stance in Main)' + ), + + '
    施放顺序(未配置的按照下面的顺序)施放順序(未配置的按照下面的順序)Cast Order(Using order below as default if not configed): ', + UI.label(`skillOrderValue`, UI.l('技能施放顺序', '技能施放順序', 'Skill Cast Order')), + UI.text('skillOrderValue', 'style="width:80%;" disabled="true"'), + '
    ', + UI.expendData(UIDatas.skill, (id, names) => UI.labeled(`skillOrder_${id}`, names)), + '
    ', + UI.expendData(UIDatas.skill, (id, names) => UI.div(`${UI.labeled(`skill_${id}`, names)}: ${UI.label(`skillOTOS_${id}`, names, 'hidden')}${UI.labeled(`skillOTOS_${id}`, UI.l('一回合只使用一次', '一回合只使用一次', 'One round only spell one time'))}{{skill${id}Condition}}`)), + ), + UI.hvAATab( + 'Infusion', + UI.l('战役模式', '戰役模式', 'Battle type'), ': ', + UI.expendData(UIDatas.roundType, (id, names) => UI.labeled(`infusionRoundType_${id}`, names, 'placeholder="true"')), + UI.div(UI.label('infusionCondition', UI.l('魔药使用条件', '魔藥使用條件', 'Infusion Use Condition')),'{{infusionCondition}}'), + UI.div(UI.labeled('infusionDefaultOnly', '只使用与默认攻击模式相同的魔药只使用與默認攻擊模式相同的魔藥Use Infusion as same as default attack mode only.', 'placeholder="true"')), + '
    ', + '施放顺序(未配置的按照下面的顺序)施放順序(未配置的按照下面的順序)Cast Order(Using order below as default if not configed): ', + UI.label(`infusionOrderName`, UI.l('魔药施放顺序', '魔藥技能施放順序', 'Infusion Cast Order')), + UI.text('infusionOrderName', 'style="width:80%;" disabled="true"'), + '
    ', + UI.hvAATable( + UI.repeat(6), '', + UI.expendData(UIDatas.infusion, (id, names) => UI.div(UI.labeled(`infusionOrder_${id}`, names))), + ), + '
    ', + UI.expendData(UIDatas.infusion, (id, names) => UI.div(UI.labeled(`infusion_${id}`, names), `{{infusion${id}Condition}}`)), + ), + UI.hvAATab( + 'Scroll', + UI.l('战役模式', '戰役模式', 'Battle type'), ': ', + UI.expendData(UIDatas.roundType, (id, names) => UI.labeled(`scrollRoundType_${id}`, names)), + UI.div(UI.label('scrollCondition', UI.l('卷轴使用条件', '捲軸使用條件', 'Scroll Use Condition')),'{{scrollCondition}}'), + UI.labeled(`scrollFirst`, UI.l('存在技能生成的Buff时,仍然使用卷轴', '存在技能生成的Buff時,仍然使用捲軸', 'Use Scrolls even when there are effects from spells')), + UI.expendData(UIDatas.scroll, (id, names) => UI.div(UI.labeled(`scroll_${id}`, names), `{{scroll${id}Condition}}`)), + ), + UI.hvAATab( + 'Alarm', + '', + UI.l('自定义警报', '自定義警報', 'Alarm'), + '
    ', + UI.l('注意:留空则使用默认音频,建议每个用户使用自定义音频', '注意:留空則使用默認音頻,建議每個用戶使用自定義音頻', 'Note: Leave the box blank to use default audio, it\'s recommended for all user to use custom audio.'), + UI.div(UI.expendData(UIDatas.audio, (id, names, v) => UI.div( + UI.labeled(`audioEnable_${id}`, names), + ': ', + UI.label([`audioEnable_${id}`, `audio_${id}`], names+UI.l('音频', '音頻', 'Audio'), 'hidden'), + UI.text(`audio_${id}`, `placeholder="https://github.com/dodying/UserJs/raw/master/HentaiVerse/hvAutoAttack/${v ?? id}.ogg"`), + UI.button.class('testAlarm', UI.l('测试', '測試', 'Test')) + ))), + UI.div( + UI.l('请将将要测试的音频文件的地址填入这里', '請將將要測試的音頻文件的地址填入這裡', 'Plz put in the audio file address you want to test'), + ':
    ', + '' + ), + ), + UI.hvAATab( + 'Rule', + '', + UI.l('攻击规则', '攻擊規則', 'Attack Rule'), + ' 示例Example', + UI.div( + '1. ', + '初始血量权重=Log10(目标血量/场上最低血量)初始血量權重=Log10(目標血量/場上最低血量)BaseHpWeight = BaseHpRatio*Log10(TargetHP/MaxHPOnField)
    ', + UI.label('baseHpRatio', UI.l('初始权重系数(>0:低血量优先;<0:高血量优先)', '初始權重係數(>0:低血量優先;<0:高血量優先)', 'BaseHpRatio(>0:low hp first;<0:high hp first)')), + UI.number('baseHpRatio'), + '
    ', + UI.label('unreachableWeight', UI.l('不可命中目标的权重公式', '不可命中目標的權重公式', 'Unreachable Target Weight Formula')), + '不可命中目标的权重公式不可命中目標的權重公式Unreachable Target Weight Formula: ', + UI.text('unreachableWeight', 'placeholder="1000"'), + '
    ', + UI.label('YggdrasilExtraWeight', UI.l('BOSS:Yggdrasil额外权重', 'BOSS:Yggdrasil额外权重', 'BOSS:Yggdrasil Extra Weight')), + 'BOSS:Yggdrasil额外权重BOSS:Yggdrasil额外权重BOSS:Yggdrasil Extra Weight', + UI.number('YggdrasilExtraWeight', -1000), + '
    ', + UI.labeled('cacheMonsterHP', UI.l('启用HP缓存', '啟用HP緩存', 'Use HP Cache')), + UI.button.class('clearMonsterHPCache', UI.l('清空缓存', '清空緩存', 'Clear HP Cache')), + '', + UI.labeled('portable_monsterDB', '使用便携数据模式(导出脚本数据时将包含)使用便攜數據模式(導出腳本數據時將包含)Portable Mode (will be included while exporting script datas)注意:便携数据模式可能会显著增加硬盘读写注意:便攜數據模式可能會顯著增加硬盤讀寫Notice:portable mode may significantly increase hard disk I/O'), + '' + ), + '', + UI.div( + UI.b( + '2.', + UI.l('初始权重与下述各Buff权重相加', '初始權重與下述各Buff權重相加', 'PW(X) = BaseHpWeight + Accumulated_Weight_of_Deprecating_Spells_In_Effect(X)') + ), + '
    ', + UI.hvAATable(UI.repeat(6), '', UI.expendData(UIDatas.weight1, (id, names, v) => UI.div( + UI.label(`weight_${id}`, `${names} ${UI.l('权重', '權重', ' Weight')}`, 'hidden'), + UI.number(`weight_${id}`, v), + names + ))), + UI.b( + UI.l('降抗性和攻击模式属性', '降抗性和攻擊模式屬性', 'While elements between Resistance-lower-debuff and Attack-Mode matches'), + ` [${UI.attackStatusType[option.attackStatus ?? 0]}] `, + UI.l('相同时', '相同時') + ), + ':
    ', + UI.hvAATable(UI.repeat(4) + ' repeat(2, 1.25fr);', '', UI.expendData(UIDatas.weight1, (id, names, v) => UI.div( + UI.label(`weight_${id}`, `${names} ${UI.l('权重 (攻击模式属性相同时)', '權重 (攻擊模式屬性相同時)', ' Weight (Attack-Mode matches)')}`, 'hidden'), + UI.number(`weight_${id}`, v), + names + ))), + UI.b( + UI.l('降抗性和攻击模式属性', '降抗性和攻擊模式屬性', 'While elements between Resistance-lower-debuff and Attack-Mode NOT matches'), + ` [${UI.attackStatusType[option.attackStatus ?? 0]}] `, + UI.l('不相同时', '不相同時'), + ), + ':
    ', + UI.hvAATable(UI.repeat(4) + ' repeat(2, 1.25fr);', '', UI.expendData(UIDatas.weight2, (id, names, v, v2) => UI.div( + UI.label(`weight_${id}1`, `${names} ${UI.l('权重 (攻击模式属性不相同时)', '權重 (攻擊模式屬性不相同時)', ' Weight (Attack-Mode NOT matches)')}`, 'hidden'), + UI.number(`weight_${id}1`, v2), + names + ))), + UI.b(UI.l('敌方增益,暂不清楚具体效果,默认按0权重计算', '敵方增益,暫不清楚具體效果,默認按0權重計算', 'Enemy Procs, Evvecf value unknown, weight default as 0 for now.')), + ':
    ', + UI.hvAATable('1fr 1.25fr 1fr 1fr 1fr', '', UI.expendData(UIDatas.weight3, (id, names, v) => UI.div( + UI.label(`weight_${id}`, `${names} ${UI.l('权重', '權重', ' Weight')}`, 'hidden'), + UI.number(`weight_${id}`, v), + names + ))), + ), + UI.div( + '3. PW(X) -= Log10(1 + 武器攻击中央目标伤害倍率(副手及冲击技能)乘以武器攻擊中央目標傷害倍率(副手及衝擊技能)Weapon Attack Central Target Damage Ratio (Offhand & Strike))
    ', + UI.label(`centralExtraRatio`, UI.l('额外伤害比例:', '額外傷害比例:', ' Extra DMG Ratio: ')), + UI.number('centralExtraRatio'), + '%'), + UI.div(UI.b('4. ', UI.label('extraWeightFormula', UI.l('额外权重公式', '額外權重公式', 'Extra weight formula')), ': '), UI.text('extraWeightFormula')), + UI.div( + UI.b('5. ', UI.label('skillExtraWeight', UI.l('技能额外权重公式', '技能額外權重公式', 'Extra weight formula for each skill')), ': '), UI.text('skillExtraWeight'), + '
    ', + UI.l('公式中可用的技能参数(包括条件判定)', '公式中可用的技能參數(包括條件判定)', 'Skill params available in formulas (for conditions as well)'), + UI.button.class('hvAASkillFormulaParams', UI.button.details()), + UI.hvAATable( + '1fr 1fr 2fr 2fr;display:none','hvAASkillFormulaParamsTable', + + UI.div(UI.b(UI.l('公式', '公式', 'Formula'))), UI.div(UI.b(UI.l('取值.', '取值.', 'Values.')), UI.l('默认undefined (公式中为 _undefined)', '默認undefined (公式中为 _undefined)', 'default: undefined (as _undefined in formula)')), + UI.div(UI.b(UI.l('参数', '參數', 'Param'))), UI.div(UI.b(UI.l('说明', '說明', 'Notes'))), + + UI.div('skill.[skill_id]
    skill.111 == 1'), UI.div('1'), + UI.div(UI.l('技能id [skill_id]
    普通攻击(0)、武器技能、攻击/Buff/单体Debuff法术、FRD(1101)、OFC(1111)、逃跑(1001)', '技能id [skill_id]
    普通攻擊(id=0)、武器技能、攻擊/Buff/單體Debuff法術、FRD(1101)、OFC(1111)、逃跑(1001)', '[skill_id]
    attack(id=0), weapon skills, offensive/Buff/(single target)Debuff spells, FRD(1101), OFC(1111), Flee(1001)')), + UI.div(UI.l('用于带id技能判定', '用於帶id技能判定', 'For formulas before skills with id')), + + UI.div('skill.skill
    skill.skill == T2'), + UI.div('OFC, FRD, flee, T1, T2, T3'), + UI.div(), + UI.div(UI.l('武器/非法术技能名称', '武器/非法術技能名稱', 'weapon/not-spell skill name')), + + UI.div('skill.[debuff/debuffAll]
    skill.debuffAll == Sle'), + UI.div('debuff main switch : 1
    debuff/debuffAll: Sl, Bl, Slo, We, Si, Dr, Im, MN, Co'), + UI.div('debuff main switch: debuff
    debuff(singal): debuff
    debuff(all): debuff, debuffAll'), + UI.div(UI.l('Debuff技能简称', 'Debuff技能簡稱', 'Debuff spell short name')), + + UI.div('skill.attackTier
    skill.attackTier == 3'), + UI.div('Weapon attack: 0, Offensive spells: 1, 2, 3'), + UI.div(), + UI.div(UI.l('攻击阶数', '攻擊階數', 'Attack tiers')), + + UI.div('skill.all
    skill.all == 212'), UI.div('debuff skill ids'), + UI.div(), + UI.div(UI.l('用于全体debuff的判定', '用於全體debuff的判定', 'For formulas before debuff all')), + + UI.div('skill.tier
    skill.tier == 1'), UI.div('1, 2, 3'), + UI.div(), + UI.div(UI.l('用于以太之触或切换攻击模式时判断攻击法术阶数', '用於以太之触或切換攻擊模式時判斷攻擊法術階數', 'For offensive magic spell tiers before checks for attack status switch or ether tap')), + + UI.div('skill.id
    skill.id == 111'), + UI.div('[skill/item id], scroll, infusion, buff, debuff, defend, draught, focus, spiriton, spiritoff, OFC, FRD, T1, T2, T3, switch, etherTap'), + UI.div(), + UI.div('id'), + + UI.div('skill.[battleSteps]
    skill.defend === 1'), + UI.div('1'), + UI.div('[battleSteps]
    flee, defend, focus, scroll, buff, debuff, infusion'), + UI.div(UI.l('用于战斗步骤(逃跑、防御、集中、卷轴/BUFF/DEBUFF/魔药总开关)', '用於戰鬥步驟(逃跑、防禦、集中、捲軸/BUFF/DEBUFF/魔藥總開關)', 'For battle step checks (Flee, Defend, Focus, MAIN SWITCH for Scroll/Buff/Debuff/Infusion)')), + UI.div('skill.etherTap
    skill.etherTap === 1'), + UI.div('1'), + UI.div(), + UI.div(UI.l('用于以太之触', '用於以太之觸', 'For ether tap check')), + UI.div('skill.spirit
    skill.spirit === off'), + UI.div('on, off'), + UI.div(), + UI.div(UI.l('用于灵动架势', '用於靈動架勢', 'For spirit check')), + ), + ), + UI.div( + '6. 优先选择权重最低的目标優先選擇權重最低的目標Choose target with lowest rank first
    ', + UI.labeled('displayWeight', UI.l('显示权重及顺序', '顯示權重及順序', 'Display Weight and order')), + UI.labeled('displayWeightBackground', UI.l('显示优先级背景色', '顯示優先級背景色', 'Display Priority Background Color')), + '
    ', + '
    ', + 'CSS格式或可eval执行的公式(可用<rank>, <all>指代优先级和总优先级数量, <style_x>指代第x个的相同配置值),例如:CSS格式或可eval執行的公式(可用<rank>, <all>指代優先級和總優先級數量, <style_x>指代第x個的相同配置值):例如CSS or eval executable formula(use <rank> and <all> to refer to priority rank and total rank count, <style_x> to refer to the same option value of option No.x)Such as:
    `hsl(${Math.round(240*<rank>/Math.max(1,<all>-1))}deg 50% 50%)`
    ', + UI.hvAATable( + '0.05fr 1fr;width:100%', '', + ...range(0, 10).map(i => `${UI.div(UI.label(`weightBackground_${i}`, UI.l('显示优先级背景色', '顯示優先級背景色', 'Display Priority Background Color')+`${i === 9 ? '' : `  `}${i + 1}.`))}${UI.div(UI.text(`weightBackground_${i}`))}`), + ), + '
    ', + ), + UI.div( + 'PS. 如果你对各Buff权重有特别见解,请务必如果你對各Buff權重有特別見解,請務必If you have any suggestions, please 告诉我告訴我let me know.
    ', + '参考公式为:參考公式為:Basic Weight Calculation as: PW(X) = Log10(
    HP/MaxHPOnField/(1+CentralAttackDamageExtraRatio)
    *[HPActualEffectivenessRate:∏(1-debuff),debuff=Im|PA|Bl|Co|Dr|MN|St]
    /[DMGActualEffectivenessRate:∏(1-debuff),debuff=We|Bl|Slo|Si|Sl|Co|Dr|MN|St])' + ), + ), + UI.hvAATab( + 'Drop', + UI.div( + UI.button.class('reDropMonitor', UI.l('重置掉落监测', '重置掉落監測', 'Reset Drops Tracking')), + UI.labeled('portable_drop', '使用便携数据模式(导出脚本数据时将包含)使用便攜數據模式(導出腳本數據時將包含)Portable Mode (will be included while exporting script datas)注意:便携数据模式可能会显著增加硬盘读写注意:便攜數據模式可能會顯著增加硬盤讀寫Notice:portable mode may significantly increase hard disk I/O'), + '', + ), + '
    记录装备的最低品质記錄裝備的最低品質Minimum drop quality:
    ', + '
    ', + ), + UI.hvAATab( + 'Usage', + UI.div( + UI.button.class('reRecordUsage', UI.l('重置数据记录', '重置數據記錄', 'Reset Usage Tracking')), + UI.labeled('portable_stats', UI.l('使用便携数据模式(导出脚本数据时将包含)注意:便携数据模式可能会显著增加硬盘读写', '使用便攜數據模式(導出腳本數據時將包含)注意:便攜數據模式可能會顯著增加硬盤讀寫', 'Portable Mode (will be included while exporting script datas) Notice:portable mode may significantly increase hard disk I/O')), + '' + ), + UI.div( + UI.b(UI.l('自身', '自身', 'Self')), + UI.hvAATable( + UI.repeat(10), '', + UI.expendData(UIDatas.record1, (id, names, v) => UI.div(UI.labeled(`record_${id}`, names))), + ), + ), + UI.div( + UI.b(UI.l('操作', '操作', 'Actions')), + UI.hvAATable( + UI.repeat(5), '', + UI.expendData(UIDatas.record2, (id, names, v) => UI.div(UI.labeled(`record_${id}`, names))), + ), + ), + UI.div( + UI.labeled('record_hurt', UI.b(UI.l('受伤 (总量)', '受傷 (總量)', 'Hurt (Amount)'))), + UI.hvAATable( + `${UI.repeat(3)} ${UI.repeat(6, '2fr')}`, '', + UI.expendData(UIDatas.record3, (id, names, v) => UI.div(UI.labeled(`record_${id}`, names))), + ), + ), + '
    ', + ), + UI.hvAATab( + 'Tools', + UI.div( + '当前状况當前狀況Current status: ', + '如果脚本长期暂停且网络无问题,请点击如果腳本長期暫停且網絡無問題,請點擊If the script does not work and you are sure that it\'s not because of your internet, click', + UI.button.class('hvAAFix', UI.l('尝试修复', '嘗試修復', 'Try to fix')), + '
    ', + '战役模式戰役模式Battle type: 当前回合當前回合Current round: ', + UI.number('roundNow', undefined, 'number', 'hvAADebug'), + ' 总回合總回合Total rounds: ', + UI.number('roundAll', undefined, 'number', 'hvAADebug'), + ), + '
    ', + UI.labeled('showQuickSite', `${UI.l('快捷站点', '快捷站點', 'Quick Site')}`), + '', + UI.button.class('quickSiteAdd', UI.l('新增', '新增', 'Add')), + '
    ', + UI.l('注意: 留空“名称”一栏则表示删除该行,修改后请保存', '注意: 留空“名稱”一欄則表示刪除該行,修改後請保存', 'Note: The "name" input box left blank will be deleted, after change please save in time.'), + '
    图标圖標ICON名称名稱Name链接鏈接Link
    ', + UI.div( + '备份与还原備份與還原Backup and Restore
    ', + UI.button.class('hvAABackup', UI.l('备份设置', '備份設置', 'Backup Confiuration')), + UI.button.class('hvAARestore', UI.l('还原设置', '還原設置', 'Restore Confiuration')), + UI.button.class('hvAADelete', UI.l('删除设置', '刪除設置', 'Delete Confiuration')), + '
      '), + UI.div( + '', + '导入与导出導入與導出Import and Export', + '
      ', + UI.button.class('hvAAExport', UI.l('导出设置', '導出設置', 'Export Confiuration')), + UI.button.class('hvAAImport', UI.l('导入设置', '導入設置', 'Import Confiuration')), + ''), + ), + UI.hvAATab( + 'Feedback', + '', + UI.l('反馈', '反馈', 'Feedback'), + '', + UI.div( + UI.l('链接', '鏈接', 'Links'), + ': 1. GitHub2. GreasyFork'), + UI.div( + '', + UI.l('反馈说明', '反饋說明', 'Feedback Note'), + ':
      ', + UI.l( + '如果你遇见了Bug,想帮助作者修复它
      你应当提供以下多种资料:
      1. 场景描述
      2. 你的配置
      3. 控制台日志 (按Ctrl+Shift+i打开开发者助手,再选择Console(控制台)面板)
      4. 战斗日志 (如果是在战斗中)
      如果是无法容忍甚至使脚本失效的Bug,请尝试安装旧版本
      如果你有一些建议使这个脚本更加有用,那么:
      1. 请尽量简述你的想法
      2. 如果可以,请提供一些场景 (方便作者更好理解)', + '如果你遇見了Bug,想幫助作者修復它
      你應當提供以下多種資料:
      1. 場景描述
      2. 你的配置
      3. 控制台日誌 (按Ctrl+Shift+i打開開發者助手,再選擇Console(控制台)面板)
      4. 戰鬥日誌 (如果是在戰鬥中)
      如果是無法容忍甚至使腳本失效的Bug,請嘗試安裝舊版本
      如果你有一些建議使這個腳本更加有用,那麼:
      1. 請盡量簡述你的想法
      2.如果可以,請提供一些場景 (方便作者更好理解)', + 'If you encounter a bug and would like to help the author fix it
      You should provide the following information:
      1. the Situation
      2. Your Configuration
      3. Console Log (press Ctrl + Shift + i to open the Developer Assistant, And then select the Console panel)
      4. Battle Log (if in combat)
      If you are unable to tolerate this bug or even the bug made the script fail, try installing the old version
      If you have some suggestions to make this script more useful, then:
      1. Please briefly describe your thoughts
      2. If you can, please provide some scenes (to facilitate the author to better understand)
      PS. For English user, please express in basic English (Oh my poor English, thanks for Google Translate)' + ), + ), + UI.div(UI.labeled('debugCheckCondition', 'debugCheckCondition:
      prefix@/# to log result in console, @for formula, #for param: '), '{{debugCondition}}'), + ), + ] + }), + UI.div({ + args: { class: 'hvAAButtonBox hvAACenter', style:'display:grid; grid-template-columns: repeat(8, 1fr)' }, + inner: [ + UI.div(), UI.div(), + UI.button.class('hvAAApply', UI.l('应用', '應用', 'Apply')), + UI.button.class('hvAACancel', UI.l('关闭', '關閉', 'Close')), + UI.button.class('hvAAReset', UI.l('撤销', '撤銷', 'Revert')), + UI.button.class('hvAADefault', UI.l('默认', '默認', 'Default')), + UI.div(), UI.div(), + ]}), + ].join('').replace(/{{(.*?)}}/g, '
      '); + + [...gE('.customize', 'all', optionBox)].map(customize => { + const name = customize.getAttribute('name'); + let replaced = name.replace('Condition', ''); + let input = gE(`#${name.replace('Condition', '_')}`, optionBox) ?? gE(`#${replaced}`, optionBox) ?? gE(`#${replaced.replace(/(Skill|skill|item|infusion|scroll|pause|flee|debug)/, (...args) => { + switch(args[0]) { + case 'pause': + return 'autoPause'; + case 'flee': + return 'autoFlee'; + case 'debug': + return 'debugCheckCondition'; + default: // case 'skill': case 'Skill': case 'item': case 'infusion': case 'scroll': + return `${args[0]}_`; } - } else { - if (getValue('drop')) { - drop.__name = getValue('battleCode'); - dropOld.push(drop); + })}`, optionBox); + if (!input) return; + customize.classList.add(`${input.id}Inner`); + }); + + gE('.hvAATab', 'all', optionBox).forEach(tab => { tab.style.zIndex = 1; }); + optionBox.style.display = 'none'; + gE('select[name="lang"]', optionBox).value = g().lang; + bindEvents(); + } + updateItemWorldList(true, document); + updateEquipSetUI(); + updateItemWorldListUI(); + changeSelectOptionText(); + loadOptionUIData(); + + gE('input, select', 'all', optionBox).forEach(setInputTitle); + + [...gE('select:not([name="lang"])', 'all', optionBox)].forEach(s => { s.onchange ??= () => selectFit(s); }); + unique([...gE('[class$="Inner"]', 'all', optionBox)].map(inner => [...inner.classList].find(className => className.includes('Inner')))).forEach(innerName => { + const onchange = gE(`#${innerName.replace(/Inner$/, '')}`, optionBox)?.onchange; + if (onchange) onchange(); + }); + + function changeSelectOptionText() { + const lang = g().lang; + const attackStatus = { + 0: UI.byLang('物理', '物理', 'Physical'), + 1: UI.byLang('火', '火', 'Fire'), + 2: UI.byLang('冰', '冰', 'Cold'), + 3: UI.byLang('雷', '雷', 'Elec'), + 4: UI.byLang('风', '風', 'Wind'), + 5: UI.byLang('圣', '聖', 'Divine'), + 6: UI.byLang('暗', '暗', 'Forbidden'), + }; + [...gE('select[name="attackStatus"] > option', 'all', optionBox)].forEach(option => { + option.innerText = attackStatus[option.value.toString()] ?? option.innerText; + }); + const autoSwitchOptionText = [ + ['继承', '繼承', 'Inherit'], + ['不自动切换', '不自動切換', 'Disable auto switch'], + ['(默认)', '(默認)', '(Default)'], + ['(当前)', '(當前)', '(current)'] + ]; + [...gE('.equipSetList option, .autoItemWorldList option', 'all', optionBox)].forEach(option => { + for (const texts of autoSwitchOptionText) { + for (const text of texts) { + if (!option.innerText.includes(text)) continue; + option.innerText = option.innerText.replace(text, UI.byLang(texts)); + break; } - dropOld.reverse(); - _html = `${_html}`; - dropOld.forEach((_dropOld) => { - _html = `${_html}${_dropOld.__name}`; - }); - _html = `${_html}`; - getKeys(dropOld).forEach((key) => { - if (key === '__name') { - return; + } + }); + } + + function bindEvents() { + gE('select[name="lang"]', optionBox).onchange = function () { // 选择语言 + gE('.hvAA-LangStyle').textContent = `l${this.value}{display:inline!important;}`; + if (/^[01]$/.test(this.value)) { + gE('.hvAA-LangStyle').textContent += 'l01{display:inline!important;}'; + } + g('lang', this.value); + changeSelectOptionText(); + }; + gE('.hvAATabmenu', optionBox).onclick = function (e) { // 标签页事件 + if (e.target.tagName.toUpperCase() === 'INPUT') { + return; + } + const target = (e.target.tagName.toUpperCase() === 'SPAN') ? e.target : e.target.parentNode; + const name = target.getAttribute('name'); + let i, _html; + if (name === 'Drop') { // 掉落监测 + let drop = getValue('drop', true) || {}; + const dropOld = getValue('dropOld', true) || []; + drop = objSort(drop); + _html = ''; + if (dropOld.length === 0 || (dropOld.length === 1 && !getValue('drop', true))) { + if (dropOld.length === 1) { + drop = dropOld[0]; + } + _html = `${_html}数量數量Amount`; + for (i in drop) { + _html = `${_html}${i}${drop[i]}`; } - _html = `${_html}${key}`; + } else { + if (getValue('drop')) { + drop.__name = getValue('battleCode', true)?.name; + dropOld.push(drop); + } + dropOld.reverse(); + _html = `${_html}`; dropOld.forEach((_dropOld) => { - if (key in _dropOld) { - _html = `${_html}${_dropOld[key]}`; - } else { - _html = `${_html}`; - } + _html = `${_html}${_dropOld.__name}`; }); _html = `${_html}`; - }); - } - _html = `${_html}`; - gE('#hvAATab-Drop>table').innerHTML = _html; - } else if (name === 'Usage') { // 数据记录 - let stats = getValue('stats', true) || {}; - const statsOld = getValue('statsOld', true) || []; - const translation = { - self: '自身 (次数)自身 (次數)Self (Frequency)', - restore: '回复 (总量)回复 (總量)Restore (Amount)', - items: '物品 (次数)物品 (次數)Items (Frequency)', - magic: '技能 (次数)技能 (次數)Magic (Frequency)', - damage: '伤害 (总量)傷害 (總量)Damage (Amount)', - hurt: '受伤 (总量)受傷 (總量)Loss (Amount)', - proficiency: '熟练度 (总量)熟練度 (總量)Proficiency (Amount)', - }; - _html = ''; - if (statsOld.length === 0 || (statsOld.length === 1 && !getValue('stats', true))) { - if (statsOld.length === 1) { - stats = statsOld[0]; - } - for (i in stats) { - _html = `${_html}${translation[i]}Value`; - stats[i] = objSort(stats[i]); - for (const j in stats[i]) { - _html = `${_html}${j}${stats[i][j]}`; - } - } - } else { - if (getValue('stats')) { - stats.__name = getValue('battleCode'); - statsOld.push(stats); - } - statsOld.reverse(); - _html = `${_html}`; - statsOld.forEach((_dropOld) => { - _html = `${_html}${_dropOld.__name}`; - }); - _html = `${_html}`; - Object.keys(translation).forEach((i) => { - if (i === '__name') { - return; - } - _html = `${_html}${translation[i]}`; - getKeys(statsOld, i).forEach((key) => { + getKeys(dropOld).forEach((key) => { + if (key === '__name') { + return; + } _html = `${_html}${key}`; - statsOld.forEach((_statsOld) => { - if (key in _statsOld[i]) { - _html = `${_html}${_statsOld[i][key]}`; + dropOld.forEach((_dropOld) => { + if (key in _dropOld) { + _html = `${_html}${_dropOld[key]}`; } else { _html = `${_html}`; } }); + _html = `${_html}`; }); - }); - } - _html = `${_html}`; - gE('#hvAATab-Usage>table').innerHTML = _html; - } else if (name === 'Tools') { // 关于本脚本 - gE('.hvAADebug', 'all', optionBox).forEach((input) => { - if(getValue('battle') && getValue('battle')[input.name]){ - input.value = getValue('battle')[input.name]; - } else if (getValue(input.name)) { - input.value = getValue(input.name); } - }); - } else if (name === 'Drop' || name === 'Usage') { - gE('.selectTable', 'all', optionBox).forEach((i) => { - i.onclick = null; - i.onclick = function (e) { - const select = window.getSelection(); - select.removeAllRanges(); - const range = document.createRange(); - range.selectNodeContents(e.target.parentNode.parentNode.parentNode); - select.addRange(range); + _html = `${_html}`; + gE('#hvAATab-Drop>table').innerHTML = _html; + } else if (name === 'Usage') { // 数据记录 + let stats = getValue('stats', true) || {}; + const statsOld = getValue('statsOld', true) || []; + const translation = { + self: UI.l('自身', '自身', 'Self'), + restore: UI.l('回复 (总量)', '回复 (總量)', 'Restore (Amount)'), + items: UI.l('物品 (次数)', '物品 (次數)', 'Items (Frequency)'), + magic: UI.l('技能 (次数)', '技能 (次數)', 'Magic (Frequency)'), + damage: UI.l('伤害 (总量)', '傷害 (總量)', 'Damage (Amount)'), + proficiency: UI.l('熟练度 (总量)', '熟練度 (總量)', 'Proficiency (Amount)'), + hurt: UI.l('受伤 (总量)', '受傷 (總量)', 'Loss (Amount)'), }; + _html = ''; + if (statsOld.length === 0 || (statsOld.length === 1 && !getValue('stats', true))) { + if (statsOld.length === 1) { + stats = statsOld[0]; + } + for (i in stats) { + if (['itemsNames', 'magicNames'].includes(i)) continue; + _html = `${_html}${translation[i]}${UI.l('值', '值', 'Value')}`; + stats[i] = objSort(stats[i]); + let names = stats[`${i}Names`]; + for (const j in stats[i]) { + _html = `${_html}${j} ${names?.[j] ?? ''}${stats[i][j]}`; + } + } + } else { + if (getValue('stats')) { + stats.__name = getValue('battleCode', true)?.name; + statsOld.push(stats); + } + statsOld.reverse(); + _html = `${_html}`; + statsOld.forEach((_dropOld) => { + _html = `${_html}${_dropOld.__name}`; + }); + _html = `${_html}`; + Object.keys(translation).forEach((i) => { + if (['itemsNames', 'magicNames'].includes(i)) return; + if (i === '__name') { + return; + } + _html = `${_html}${translation[i]}`; + getKeys(statsOld, i).forEach((key) => { + let names = stats[`${key}Names`]; + _html = `${_html}${key} ${names?.[key] ?? ''}`; + statsOld.forEach((_statsOld) => { + if (_statsOld[i] && (key in _statsOld[i])) { + _html = `${_html}${_statsOld[i][key]}`; + } else { + _html = `${_html}`; + } + }); + }); + }); + } + _html = `${_html}`; + gE('#hvAATab-Usage>table').innerHTML = _html; + } else if (name === 'Tools') { // 关于本脚本 + gE('.hvAADebug', 'all', optionBox).forEach((input) => { + if (getValue('battle') && getValue('battle')[input.name]) { + input.value = getValue('battle')[input.name]; + } else if (getValue(input.name)) { + input.value = getValue(input.name); + } + }); + } + if (name === 'Drop' || name === 'Usage') { + gE('.selectTable', 'all', optionBox).forEach((i) => { + i.onclick = null; + i.onclick = function (e) { + const select = window.getSelection(); + select.removeAllRanges(); + const selectRange = document.createRange(); + selectRange.selectNodeContents(e.target.parentNode.parentNode.parentNode); + select.addRange(selectRange); + }; + }); + } + gE('.hvAATab', 'all', optionBox).forEach((i) => { + i.style.display = (i.id === `hvAATab-${name}`) ? 'block' : ''; }); - } - gE('.hvAATab', 'all', optionBox).forEach((i) => { - i.style.display = (i.id === `hvAATab-${name}`) ? 'block' : 'none'; - }); - }; - gE('.hvAAGoto', 'all', optionBox).forEach((i) => { - i.onclick = function () { - gE(`.hvAATabmenu>span[name="${this.name.replace('hvAATab-', '')}"]`).click(); }; - }); + gE('.hvAAGoto', 'all', optionBox).forEach((i) => { + i.onclick = function () { + gE(`.hvAATabmenu>span[name="${this.name.replace('hvAATab-', '')}"]`).click(); + }; + }); - function updateGroup() { - const group = gE('.customizeGroup', 'all', g('customizeTarget')); - const customizeBox = gE('.customizeBox'); - if (group.length + 1 === gE('select[name="groupChoose"]>option', 'all', customizeBox).length) { - return; - } - gE('select[name="groupChoose"]', customizeBox).textContent = ''; - for (let i = 0; i <= group.length; i++) { - const option = gE('select[name="groupChoose"]', customizeBox).appendChild(cE('option')); - if (i === group.length) { - option.value = 'new'; - option.textContent = 'new'; - } else { - option.value = i + 1; - option.textContent = i + 1; + optionBox.onmousemove = function (e) { // 自定义条件相关事件 + const target = e.target.closest('.customize'); + if (!target) { + if (gE('.customizeBox')) gE('.customizeBox').style.display = 'none' + return; } - } - } - optionBox.onmousemove = function (e) { // 自定义条件相关事件 - const target = (e.target.className === 'customize') ? e.target : (e.target.parentNode.className === 'customize') ? e.target.parentNode : e.target.parentNode.parentNode; - if (!gE('.customizeBox')) { - customizeBox(); - } - updateGroup(); - if (target.className !== 'customize' && target.parentNode.className !== 'customize') { - if (!target.className.match('customize')) { - gE('.customizeBox').style.zIndex = -1; + creatCustomizeBox(); + g('customizeTarget', target); + updateGroup(); + }; + // 标签页-主要选项 + gE('input[name="pauseHotkeyStr"]', optionBox).onkeyup = function (e) { + this.value = (/^[a-z]$/.test(e.key)) ? e.key.toUpperCase() : e.key; + gE('input[name="pauseHotkeyCode"]', optionBox).value = e.keyCode; + }; + gE('input[name="stepInHotkeyStr"]', optionBox).onkeyup = function (e) { + this.value = (/^[a-z]$/.test(e.key)) ? e.key.toUpperCase() : e.key; + gE('input[name="stepInHotkeyCode"]', optionBox).value = e.keyCode; + }; + gE('input[name="altHotkeyStr"]', optionBox).onkeyup = function (e) { + this.value = (/^[a-z]$/.test(e.key)) ? e.key.toUpperCase() : e.key; + gE('input[name="altHotkeyCode"]', optionBox).value = e.keyCode; + }; + gE('.testAlarm', 'all', optionBox).forEach(button => { + button.onclick = function () { + const srcInput = gE('input[type="text"]', button.parentNode); + const e = srcInput.name.split('_')[1]; + const src = srcInput.value ?? srcInput.placeholder; + console.log('test alarm', e, src); + setAlarm(e, src); } - return; - } - g('customizeTarget', target); - const position = target.getBoundingClientRect(); - gE('.customizeBox').style.zIndex = 5; - gE('.customizeBox').style.top = `${position.bottom + window.scrollY}px`; - gE('.customizeBox').style.left = `${position.left + window.scrollX}px`; - }; - // 标签页-主要选项 - gE('input[name="pauseHotkeyStr"]', optionBox).onkeyup = function (e) { - this.value = (/^[a-z]$/.test(e.key)) ? e.key.toUpperCase() : e.key; - gE('input[name="pauseHotkeyCode"]', optionBox).value = e.keyCode; - }; - gE('.testNotification', optionBox).onclick = function () { - _alert(0, '接下来开始预处理。\n如果询问是否允许,请选择允许', '接下來開始預處理。\n如果詢問是否允許,請選擇允許', 'Now, pretreat.\nPlease allow to receive notifications if you are asked for permission'); - setNotification('Test'); - }; - gE('.testPopup', optionBox).onclick = function () { - _alert(0, '接下来开始预处理。\n关闭本警告框之后,请切换到其他标签页,\n并在足够长的时间后再打开本标签页', '接下來開始預處理。\n關閉本警告框之後,請切換到其他標籤頁,\n並在足夠長的時間後再打開本標籤頁', 'Now, pretreat.\nAfter dismissing this alert, focus other tab,\nfocus this tab again after long time.'); - setTimeout(() => { - const riddleWindow = window.open(window.location.href, 'riddleWindow', 'resizable,scrollbars,width=1241,height=707'); - if (riddleWindow) { - setTimeout(() => { - riddleWindow.close(); - }, 200); - } - }, 3000); - }; - gE('.staminaLostLog', optionBox).onclick = function () { - const out = []; - const staminaLostLog = getValue('staminaLostLog', true); - for (const i in staminaLostLog) { - out.push(`${i}: ${staminaLostLog[i]}`); - } - if (window.confirm(`总共${out.length}条记录 (There are ${out.length} logs): \n${out.reverse().join('\n')}\n是否重置 (Whether to reset)?`)) { - setValue('staminaLostLog', {}); - } - }; - gE('.idleArenaReset', optionBox).onclick = function () { - if (_alert(1, '是否重置', '是否重置', 'Whether to reset')) { - delValue('arena'); - } - }; - gE('.hvAAShowLevels', optionBox).onclick = function () { - gE('.hvAAArenaLevels').style.display = (gE('.hvAAArenaLevels').style.display === 'grid') ? 'none' : 'grid'; - }; - gE('.hvAALevelsClear', optionBox).onclick = function () { - gE('[name="idleArenaLevels"]', optionBox).value = ''; - gE('[name="idleArenaValue"]', optionBox).value = ''; - gE('.hvAAArenaLevels>input', 'all', optionBox).forEach((input) => { - input.checked = false; }); - }; - gE('.hvAAArenaLevels', optionBox).onclick = function (e) { - if (e.target.tagName !== 'INPUT' && e.target.type !== 'checkbox') { - return; - } - const valueArray = e.target.value.split(','); - let levels = gE('input[name="idleArenaLevels"]').value; - let { value } = gE('input[name="idleArenaValue"]'); - if (e.target.checked) { - levels = levels + ((levels) ? `,${valueArray[0]}` : valueArray[0]); - value = value + ((value) ? `,${valueArray[1]}` : valueArray[1]); - } else { - levels = levels.replace(new RegExp(`(^|,)${valueArray[0]}(,|$)`), '$2').replace(/^,/, ''); - value = value.replace(new RegExp(`(^|,)${valueArray[1]}(,|$)`), '$2').replace(/^,/, ''); - } - gE('input[name="idleArenaLevels"]').value = levels; - gE('input[name="idleArenaValue"]').value = value; - }; - - gE('.battleOrder', optionBox).onclick = function (e) { - if (e.target.tagName !== 'INPUT' && e.target.type !== 'checkbox') { - return; - } - const valueArray = e.target.value.split(','); - let name = gE('input[name="battleOrderName"]').value; - // let { value } = gE('input[name="battleOrderValue"]'); - if (e.target.checked) { - name = name + ((name) ? `,${valueArray[0]}` : valueArray[0]); - // value = value + ((value) ? `,${valueArray[1]}` : valueArray[1]); - } else { - name = name.replace(new RegExp(`(^|,)${valueArray[0]}(,|$)`), '$2').replace(/^,/, ''); - // value = value.replace(new RegExp(`(^|,)${valueArray[1]}(,|$)`), '$2').replace(/^,/, ''); - } - gE('input[name="battleOrderName"]').value = name; - // gE('input[name="battleOrderValue"]').value = value; - }; + gE('.testNotification', optionBox).onclick = function () { + UI.alert('接下来开始预处理。\n如果询问是否允许,请选择允许', '接下來開始預處理。\n如果詢問是否允許,請選擇允許', 'Now, pretreat.\nPlease allow to receive notifications if you are asked for permission'); + setNotification('Test'); + }; + gE('.testPopup', optionBox).onclick = function () { + UI.alert('接下来开始预处理。\n关闭本警告框之后,请切换到其他标签页,\n并在足够长的时间后再打开本标签页', '接下來開始預處理。\n關閉本警告框之後,請切換到其他標籤頁,\n並在足夠長的時間後再打開本標籤頁', 'Now, pretreat.\nAfter dismissing this alert, focus other tab,\nfocus this tab again after long time.'); + setTimeout(() => { + const riddleWindow = window.open(window.location.href, 'riddleWindow', 'resizable, scrollbars, width=1241, height=707'); + if (riddleWindow) { + setTimeout(() => { + riddleWindow.close(); + }, 200); + } + }, 3 * _1s); + }; - // 标签页-物品 - gE('.itemOrder', optionBox).onclick = function (e) { - if (e.target.tagName !== 'INPUT' && e.target.type !== 'checkbox') { - return; - } - const valueArray = e.target.value.split(','); - let name = gE('input[name="itemOrderName"]').value; - let { value } = gE('input[name="itemOrderValue"]'); - if (e.target.checked) { - name = name + ((name) ? `,${valueArray[0]}` : valueArray[0]); - value = value + ((value) ? `,${valueArray[1]}` : valueArray[1]); - } else { - name = name.replace(new RegExp(`(^|,)${valueArray[0]}(,|$)`), '$2').replace(/^,/, ''); - value = value.replace(new RegExp(`(^|,)${valueArray[1]}(,|$)`), '$2').replace(/^,/, ''); - } - gE('input[name="itemOrderName"]').value = name; - gE('input[name="itemOrderValue"]').value = value; - }; - // 标签页-Channel技能 - gE('.channelSkill2Order', optionBox).onclick = function (e) { - if (e.target.tagName !== 'INPUT' && e.target.type !== 'checkbox') { - return; - } - const valueArray = e.target.value.split(','); - let name = gE('input[name="channelSkill2OrderName"]').value; - let { value } = gE('input[name="channelSkill2OrderValue"]'); - if (e.target.checked) { - name = name + ((name) ? `,${valueArray[0]}` : valueArray[0]); - value = value + ((value) ? `,${valueArray[1]}` : valueArray[1]); - } else { - name = name.replace(new RegExp(`(^|,)${valueArray[0]}(,|$)`), '$2').replace(/^,/, ''); - value = value.replace(new RegExp(`(^|,)${valueArray[1]}(,|$)`), '$2').replace(/^,/, ''); - } - gE('input[name="channelSkill2OrderName"]').value = name; - gE('input[name="channelSkill2OrderValue"]').value = value; - }; - // 标签页-BUFF技能 - gE('.buffSkillOrder', optionBox).onclick = function (e) { - if (e.target.tagName !== 'INPUT' && e.target.type !== 'checkbox') { - return; - } - const name = e.target.id.match(/_(.*)/)[1]; - let { value } = gE('input[name="buffSkillOrderValue"]'); - if (e.target.checked) { - value = value + ((value) ? `,${name}` : name); - } else { - value = value.replace(new RegExp(`(^|,)${name}(,|$)`), '$2').replace(/^,/, ''); - } - gE('input[name="buffSkillOrderValue"]').value = value; - }; - // 标签页-DEBUFF技能 - gE('.debuffSkillOrder', optionBox).onclick = function (e) { - if (e.target.tagName !== 'INPUT' && e.target.type !== 'checkbox') { - return; - } - const name = e.target.id.match(/_(.*)/)[1]; - let { value } = gE('input[name="debuffSkillOrderValue"]'); - if (e.target.checked) { - value = value + ((value) ? `,${name}` : name); - } else { - value = value.replace(new RegExp(`(^|,)${name}(,|$)`), '$2').replace(/^,/, ''); - } - gE('input[name="debuffSkillOrderValue"]').value = value; - }; - // 标签页-其他技能 - gE('.skillOrder', optionBox).onclick = function (e) { - if (e.target.tagName !== 'INPUT' && e.target.type !== 'checkbox') { - return; - } - const name = e.target.id.match(/_(.*)/)[1]; - let { value } = gE('input[name="skillOrderValue"]'); - if (e.target.checked) { - value = value + ((value) ? `,${name}` : name); - } else { - value = value.replace(new RegExp(`(^|,)${name}(,|$)`), '$2').replace(/^,/, ''); - } - gE('input[name="skillOrderValue"]').value = value; - }; - // 标签页-警报 - gE('input[name="audio_Text"]', optionBox).onchange = function () { - if (this.value === '') { - return; - } - if (!/^http(s)?:|^ftp:|^data:audio/.test(this.value)) { - _alert(0, '地址必须以"http:","https:","ftp:","data:audio"开头', '地址必須以"http:","https:","ftp:","data:audio"開頭', 'The address must start with "http:", "https:", "ftp:", and "data:audio"'); - return; - } - _alert(0, '接下来将测试该音频\n如果该音频无法播放或无法载入,请变更\n请测试完成后再键入另一个音频', '接下來將測試該音頻\n如果該音頻無法播放或無法載入,請變更\n請測試完成後再鍵入另一個音頻', 'The audio will be tested after you close this prompt\nIf the audio doesn\'t load or play, change the url'); - const box = gE('#hvAATab-Alarm').appendChild(cE('div')); - box.innerHTML = this.value; - const audio = box.appendChild(cE('audio')); - audio.controls = true; - audio.src = this.value; - audio.play(); - }; - // 标签页-攻击规则 - gE('.clearMonsterHPCache', optionBox).onclick = function () { - delValue('monsterDB'); - delValue('monsterMID'); - }; - // 标签页-掉落监测 - gE('.reDropMonitor', optionBox).onclick = function () { - if (_alert(1, '是否重置', '是否重置', 'Whether to reset')) { - delValue('drop'); - delValue('dropOld'); - } - }; - // 标签页-数据记录 - gE('.reRecordUsage', optionBox).onclick = function () { - if (_alert(1, '是否重置', '是否重置', 'Whether to reset')) { - delValue('stats'); - delValue('statsOld'); - } - }; - // 标签页-关于本脚本 - gE('.hvAAFix', optionBox).onclick = function () { - gE('.hvAADebug[name^="round"]', 'all', optionBox).forEach((input) => { - setValue(input.name, input.value || input.placeholder); + let inners = unique([...gE('[class$="Inner"]', 'all', optionBox)].map(inner => [...inner.classList].find(className => className.includes('Inner')))); + inners.forEach(innerName => { + const outter = gE(`#${innerName.replace(/Inner$/, '')}`, optionBox); + if (!outter) return; + outter.onchange = function () { + [...gE(`.${innerName}`, 'all', optionBox)].forEach(inner => { inner.style.filter = outter.checked ? 'opacity(1)' : 'opacity(0.3)'; }); + }; }); - }; - gE('.quickSiteAdd', optionBox).onclick = function () { - const tr = gE('.hvAAQuickSite>table>tbody', optionBox).appendChild(cE('tr')); - tr.innerHTML = ''; - }; - gE('.hvAAConfig', optionBox).onclick = function () { - this.style.height = 0; - this.style.height = `${this.scrollHeight}px`; - this.select(); - }; - function rmListItem(code) { // 同步删除界面显示对应的项 - const configs = gE('#hvAATab-Tools > * > ul[class="hvAABackupList"] > li', 'all'); - for (const config of configs) { - if (config.textContent == code) { - config.remove(); + inners = unique([...gE('[class$="InnerReverted"]', 'all', optionBox)].map(inner => [...inner.classList].find(className => className.includes('InnerReverted')))); + inners.forEach(innerName => { + const outter = gE(`#${innerName.replace(/InnerReverted$/, '')}`, optionBox); + outter.onchange = function () { + [...gE(`.${innerName}`, 'all', optionBox)].forEach(inner => { inner.style.filter = !outter.checked ? 'opacity(1)' : 'opacity(0.3)'; }); + }; + }); + gE('.idleArenaReset', optionBox).onclick = function () { + if (UI.confirm('是否重置', '是否重置', 'Whether to reset')) { + delValue('arena'); } + }; + gE('.hvAASkillFormulaParams', optionBox).onclick = function () { + const isDisplay = gE('.hvAASkillFormulaParamsTable', optionBox).style.display !== 'grid'; + this.innerHTML = UI.button.details(isDisplay); + gE('.hvAASkillFormulaParamsTable', optionBox).style.display = isDisplay ? 'grid' : 'none'; + }; + gE('.hvAAShowLevels', optionBox).onclick = function () { + const isDisplay = gE('.hvAAArenaLevels', optionBox).style.display !== 'grid'; + this.innerHTML = UI.button.details(isDisplay); + gE('.hvAAArenaLevels', optionBox).style.display = isDisplay ? 'grid' : 'none'; + }; + gE('.hvAALevelsClear', optionBox).onclick = function () { + gE('[name="idleArenaLevels"]', optionBox).value = ''; + gE('[name="idleArenaValue"]', optionBox).value = ''; + gE('.hvAAArenaLevels input[id^="arLevel_"]', 'all', optionBox).forEach((input) => { + input.checked = false; + displayCheckBoxNotDefault(input); + }); + }; + + gE('.updateEquipSet', optionBox).onclick = async function() { try { + this.innerHTML = UI.button.updating; + await updateItemWorldList(true); + updateEquipSetUI(); + this.innerHTML = UI.button.update; + } catch (err) { console.error(err); }}; + gE('.hvAAShowEquipSet', optionBox).onclick = function () { + const isDisplay = gE('.equipSetList', optionBox).style.display !== 'grid'; + this.innerHTML = UI.button.details(isDisplay); + gE('.equipSetList', optionBox).style.display = isDisplay ? 'grid' : 'none'; + }; + + gE('.updateItemWorld', optionBox).onclick = async function() { try { + this.innerHTML = UI.button.updating; + await updateItemWorldList(); + updateItemWorldListUI(); + this.innerHTML = UI.button.update; + } catch (err) { console.error(err); }}; + gE('.hvAAShowItemWorld', optionBox).onclick = function () { + const isDisplay = gE('.autoItemWorldList', optionBox).style.display !== 'grid'; + this.innerHTML = UI.button.details(isDisplay); + gE('.autoItemWorldList', optionBox).style.display = isDisplay ? 'grid' : 'none'; + }; + gE('.hvAAClearItemWorld', optionBox).onclick = function () { + const current = getValue('itemWorldDatas'); + delete current.equips; + setValue('itemWorldDatas', current); + gE('.autoItemWorldList', optionBox).innerHTML = ''; + updateItemWorldListUI(); + }; + + const optionBox2Order = (ids, valueFrom = undefined, index = 0) => function (e) { + if (Array.isArray(ids)) { + for (const i in ids) { + optionBox2Order(ids[i], valueFrom, i)(e); + } + return; + } + if (e.target.id.match(/^arLevelDisable_/)) return; + if (e.target.tagName.toUpperCase() !== 'INPUT' && e.target.type !== 'checkbox') { + return; + } + valueFrom ??= e => e.target.value.split(','); + const valueArray = valueFrom(e); + const latest = Array.isArray(valueArray) ? valueArray[index] : valueArray; + + const orderObject = gE(`input[${ids}]`); + let value = orderObject.value; + const regExp = new RegExp(`(^|,)${latest}(,|$)`, 'g'); + while (value.match(regExp)) { + value = value.replace(regExp, '$2').replace(/^,/, ''); + } + if (e.target.checked) { + value = value + ((value) ? `,${latest}` : latest); + } + orderObject.value = value; + } + const getOrderFromId = e => e.target.id.match(/_(.*)/)[1]; + const orderValues = { + '.attackStatusOrder': ['name="attackStatusOrderName"', 'name="attackStatusOrderValue"'], + '.battleOrder': 'name="battleOrderName"', + // 标签页-战斗开启 + '.hvAAArenaLevels': ['name="idleArenaLevels"', 'name="idleArenaValue"'], + // 标签页-恢复技能 + '.itemOrder': ['name="itemOrderName"', 'name="itemOrderValue"'], + + // 标签页-引导技能 + '.channelSkill2Order': ['name="channelSkill2OrderName"', 'name="channelSkill2OrderValue"'], + // 标签页-BUFF技能 + '.buffSkillOrder': 'name="buffSkillOrderValue"', + // 标签页-DEBUFF技能 + '.debuffSkillOrder': 'name="debuffSkillOrderValue"', + '.debuffSkillOrderAll': 'name="debuffSkillOrderAllValue"', + // 标签页-其他技能 + '.skillOrder': 'name="skillOrderValue"', + // 标签页-, + '.infusionOrder': 'name="infusionOrderName"', + } + const isGetOrderFromId = ['.buffSkillOrder', '.debuffSkillOrder', '.debuffSkillOrderAll', '.skillOrder', '.infusionOrder']; + for (let ui in orderValues) { + gE(ui, optionBox).onclick = optionBox2Order(orderValues[ui], isGetOrderFromId.includes(ui) ? getOrderFromId : undefined); + }; + + // 标签页-警报 + gE('input[name="audio_Text"]', optionBox).onchange = function () { + if (this.value === '') return; + if (!/^http(s)?:|^ftp:|^data:audio/.test(this.value)) { + UI.alert('地址必须以"http:", "https:", "ftp:", "data:audio"开头', '地址必須以"http:", "https:", "ftp:", "data:audio"開頭', 'The address must start with "http:", "https:", "ftp:", and "data:audio"'); + return; + } + UI.alert('接下来将测试该音频\n如果该音频无法播放或无法载入,请变更\n请测试完成后再键入另一个音频', '接下來將測試該音頻\n如果該音頻無法播放或無法載入,請變更\n請測試完成後再鍵入另一個音頻', 'The audio will be tested after you close this prompt\nIf the audio doesn\'t load or play, change the url'); + const box = gE('#hvAATab-Alarm').appendChild(cE('div')); + box.innerHTML = this.value; + const audio = box.appendChild(cE('audio')); + audio.controls = true; + audio.src = this.value; + playAudio(audio); + }; + // 标签页-攻击规则 + gE('.clearMonsterHPCache', optionBox).onclick = function () { + delValue('monsterDB', true); + delValue('monsterDB', false); + delValue('monsterMID', true); + delValue('monsterMID', false); + }; + gE('#portable_monsterDB', optionBox).onclick = function () { + gE('#portable_monsterMID', optionBox).checked = this.checked; + } + // 标签页-掉落监测 + gE('.reDropMonitor', optionBox).onclick = function () { + if (UI.confirm('是否重置', '是否重置', 'Whether to reset')) { + delValue('drop', true); + delValue('drop', false); + delValue('dropOld', true); + delValue('dropOld', false); + } + }; + gE('#portable_drop', optionBox).onclick = function () { + gE('#portable_dropOld', optionBox).checked = this.checked; + } + // 标签页-数据记录 + gE('.reRecordUsage', optionBox).onclick = function () { + if (UI.confirm('是否重置', '是否重置', 'Whether to reset')) { + delValue('stats', true); + delValue('stats', false); + delValue('statsOld', true); + delValue('statsOld', false); + } + }; + gE('#portable_stats', optionBox).onclick = function () { + gE('#portable_statsOld', optionBox).checked = this.checked; } - } - gE('.hvAABackup', optionBox).onclick = function () { - const code = _alert(2, '请输入当前配置代号', '請輸入當前配置代號', 'Please put in a name for the current configuration') || time(3); - const backups = getValue('backup', true) || {}; - if (code in backups) { // 覆写同名配置 - if (_alert(1, '是否覆盖已有的同名配置?', '是否覆蓋已有的同名配置?', 'Do you want to overwrite the configuration with the same name?')) { - delete backups[code]; - rmListItem(code); - } else return; - } - backups[code] = getValue('option'); - setValue('backup', backups); - const li = gE('.hvAABackupList', optionBox).appendChild(cE('li')); - li.textContent = code; - }; - gE('.hvAARestore', optionBox).onclick = function () { - const code = _alert(2, '请输入配置代号', '請輸入配置代號', 'Please put in a name for a configuration'); - const backups = getValue('backup', true) || {}; - if (!(code in backups) || !code) { - return; - } - setValue('option', backups[code]); - goto(); - }; - gE('.hvAADelete', optionBox).onclick = function () { - const code = _alert(2, '请输入配置代号', '請輸入配置代號', 'Please put in a name for a configuration'); - const backups = getValue('backup', true) || {}; - if (!(code in backups) || !code) { - return; - } - delete backups[code]; - setValue('backup', backups); - rmListItem(code); - }; - gE('.hvAAExport', optionBox).onclick = function () { - const t = getValue('option'); - gE('.hvAAConfig').value = typeof t === 'string' ? t : JSON.stringify(t); - }; - gE('.hvAAImport', optionBox).onclick = function () { - const option = JSON.parse(gE('.hvAAConfig').value); - if (!option) { - return; - } - if (_alert(1, '是否重置', '是否重置', 'Whether to reset')) { - setValue('option', option); + // 标签页-关于本脚本 + gE('.hvAAFix', optionBox).onclick = function () { + gE('.hvAADebug[name^="round"]', 'all', optionBox).forEach((input) => { + setValue(input.name, input.value || input.placeholder); + }); + }; + gE('.quickSiteAdd', optionBox).onclick = function () { + const tr = gE('.hvAAQuickSite>table>tbody', optionBox).appendChild(cE('tr')); + tr.innerHTML = ''; + }; + gE('.hvAAConfig', optionBox).onclick = function () { + this.style.height = 0; + this.style.height = `${this.scrollHeight}px`; + this.select(); + }; + gE('.hvAABackup', optionBox).onclick = function () { + const code = UI.prompt('请输入当前配置代号(或默认使用当前时间)', '請輸入當前配置代號(或默認使用當前時間)', 'Please put in a name for the current configuration (or use current time as default)'); + backup(code, '是否覆盖已有的同名配置?', '是否覆蓋已有的同名配置?', 'Do you want to overwrite the configuration with the same name?') + }; + gE('.hvAARestore', optionBox).onclick = function () { + const code = UI.prompt('请输入配置代号', '請輸入配置代號', 'Please put in a name for a configuration'); + const backups = getValue('backup', true) || {}; + if (!(code in backups) || !code) { + return; + } + setValue('option', backups[code]); goto(); - } - }; - gE('.hvAAReset', optionBox).onclick = function () { - if (_alert(1, '是否重置', '是否重置', 'Whether to reset')) { - delValue('option'); - } + }; + gE('.hvAADelete', optionBox).onclick = function () { + const code = UI.prompt('请输入配置代号', '請輸入配置代號', 'Please put in a name for a configuration'); + const backups = getValue('backup', true) || {}; + if (!(code in backups) || !code) { + return; + } + delete backups[code]; + setValue('backup', backups); + rmListItem(code); + }; + gE('.hvAAExport', optionBox).onclick = function () { + const t = getValue('option'); + gE('.hvAAConfig').value = typeof t === 'string' ? t : JSON.stringify(t); + }; + gE('.hvAAImport', optionBox).onclick = function () { + const optionImport = JSON.parse(gE('.hvAAConfig').value); + if (!optionImport) { + return; + } + if (UI.confirm('是否重置', '是否重置', 'Whether to reset')) { + setValue('option', optionImport); + goto(); + } + }; + function alertDiffs(...lang) { + const diffs = getOptionDiff(option); + if (!diffs) return true; + const log = UI.byLang(lang.map(str => str + diffs)); + console.log(log); + return UI.confirm(...lang.map(str => str + diffs)); + } + gE('.hvAADefault', optionBox).onclick = function () { + if (getOptionDiff() && !alertDiffs('有未保存的选项,是否仍要设置为默认值? 更改数:', '有未保存的選項,是否仍要設置為默認值?更改數:', 'Unsaved changes detected, continue to set options as default? Changes: ')) { + return; + } + loadOptionUIData({}); + }; + gE('.hvAAReset', optionBox).onclick = function () { + if (!alertDiffs('是否撤销未保存的更改? 更改数:', '是否撤銷未保存的更改?更改數:', 'Confirm to revert unsaved changes? Changes: ')) { + return; + } + loadOptionUIData(option); + }; + gE('.hvAAApply', optionBox).onclick = function () { + if (gE('select[name="attackStatus"] option[value="-1"]:checked', optionBox) || + !gE('select[name="attackStatus"] option:checked', optionBox)) { + UI.alert('请选择攻击模式', '請選擇攻擊模式', 'Please select the attack mode'); + gE('.hvAATabmenu>span[name="Main"]').click(); + gE('#attackStatus', optionBox).style.border = '1px solid red'; + setTimeout(() => { gE('#attackStatus', optionBox).style.border = ''; }, 0.5 * _1s); + return; + } + + const arenaPrev = getOption().idleArenaValue; + + const _option = getCurrentUIOption(); + _option.version = scriptVersion.ver; + setValue('option', _option); + + optionBox.style.display = 'none'; + // 清除不再需要的portable数据 + for (const key of dataFlags.portable) { + if (_option.portable && Object.keys(_option.portable).includes(key)) continue; + delValue(key, true, true); + } + // 更改设置后实时刷新竞技场数据 + const arenaNew = _option.idleArenaValue; + if (arenaNew === arenaPrev) { + goto(); + return; + } + if (_option.idleArena && _option.idleArenaValue) { + const arena = getValue('arena', true); + arena.isOptionUpdated = undefined; + setValue('arena', arena); + goto(); + } + }; + gE('.hvAACancel', optionBox).onclick = function () { + optionBox.style.display = 'none'; + }; }; - gE('.hvAAApply', optionBox).onclick = function () { - if (gE('select[name="attackStatus"] option[value="-1"]:checked', optionBox)) { - _alert(0, '请选择攻击模式', '請選擇攻擊模式', 'Please select the attack mode'); - gE('.hvAATabmenu>span[name="Main"]').click(); - gE('#attackStatus', optionBox).style.border = '1px solid red'; - setTimeout(() => { - gE('#attackStatus', optionBox).style.border = ''; - }, 0.5 * _1s); - return; + + function getOptionDiff(...options) { + options[0] ??= formatOption({}, true); // default + options[1] ??= formatOption(loadOption(getCurrentUIOption()), true); // from UI + diffData.prototype.excludes = 'version'; + const diffs = diffData(options); + const lang = g().lang; + if (!diffs) return; + let i = 1; + return Object.keys(diffs).length + '\n' + Object.entries(diffs).map(([key, data]) => { + const input = getOptionInput(key, optionBox); + if ((input?.type === 'hidden') || input?.hidden || input?.classList.contains('hvAADebug')) return; + let defaultStr = UI.byLang('默认', '默認', 'Default'); + const tab = UI.cutLang(gE(`.hvAATabmenu [name="${input.closest('.hvAATab').id.match(/-(.*)/)[1]}"]`).innerHTML); + return `[${i++}]${tab} ${input.title}: ${data.map(d => d ? String(d) : `${defaultStr}(${input?.placeholder||'undefined'})`)?.join(' -> ')}`; + }).filter(d => !!d).join('\n'); + + function diffData(datas, parents) { + let json = datas.map(JSON.stringify); + if (unique(json).length === 1) return undefined; + if (datas.some(d=> !['object', 'undefined'].includes(typeof d))) { + const diff = {}; + diff[parents] = datas; + return diff; + } + const keys = datas.map(data => data ? Object.keys(data) : undefined); + let differents; + unique(keys.reduce((acc, cur) => (acc ?? []).concat(cur ?? []), [])).forEach(key => { + if ([diffData.prototype.excludes, ...diffData.prototype.excludes].includes(key)) return; + let datasSub = datas.map(data => data?.[key]); + key = parents ? `${parents}_${key}` : key; + const diff = diffData(datasSub, key); + if (!diff) return; + differents = { ...(differents ??= {}), ...diff }; + }); + return differents; } - const arenaPrev = g('option')?.idleArenaValue; - const _option = { - version: g('version'), - }; - let inputs = gE('input,select', 'all', optionBox); - let itemName; let itemArray; let itemValue; let - i; - for (i = 0; i < inputs.length; i++) { - if (inputs[i].className === 'hvAADebug') { + } + + function getCurrentUIOption() { + const _option = {}; + let name, array, value, type; + for (const input of gE('input, select', 'all', optionBox)) { + [name, type, value] = [input.name, input.type, input.value]; + switch(input.className) { + case 'hvAADebug': continue; + case 'hvAANumber': type = 'number'; + } + switch (type) { + case 'number': + value = (value || (value === 0)) ? value * 1 : ''; + if (isNaN(value)) continue; + break; + case 'text': case 'hidden': + value = value || ''; + if (['', 'undefined'].includes(value)) continue; + break; + case 'checkbox': + [name, value] = [input.id, input.checked]; + if (value === false) { + if (!input.placeholder) continue; + value = 0; + } + break; + case 'select-one': + break; + } + if (['', 'undefined', input.placeholder, input.placeholder * 1, !!input.placeholder].includes(value)) + { + continue; + } + + if ((array = name.split('_')).length === 1) { + _option[name] = value; continue; - } else if (inputs[i].className === 'hvAANumber') { - itemName = inputs[i].name; - itemValue = (inputs[i].value || inputs[i].placeholder) * 1; - if (isNaN(itemValue)) { - continue; - } - } else if (inputs[i].type === 'text' || inputs[i].type === 'hidden') { - itemName = inputs[i].name; - itemValue = inputs[i].value || inputs[i].placeholder; - if (itemValue === '') { - continue; - } - } else if (inputs[i].type === 'checkbox') { - itemName = inputs[i].id; - itemValue = inputs[i].checked; - if (itemValue === false) { - continue; - } - } else if (inputs[i].type === 'select-one') { - itemName = inputs[i].name; - itemValue = inputs[i].value; } - itemArray = itemName.split('_'); - if (itemArray.length === 1) { - _option[itemName] = itemValue; + if (input.className === 'customizeInput') { + ((_option[array[0]] ??= {})[array[1]] ??= []).push(value); } else { - if (!(itemArray[0] in _option)) { - _option[itemArray[0]] = {}; - } - if (inputs[i].className === 'customizeInput') { - if (typeof _option[itemArray[0]][itemArray[1]] === 'undefined') { - _option[itemArray[0]][itemArray[1]] = []; - } - _option[itemArray[0]][itemArray[1]].push(itemValue); - } else { - _option[itemArray[0]][itemArray[1]] = itemValue; - } + (_option[array[0]] ??= {})[array[1]] = value; } } - inputs = gE('.hvAAQuickSite input[type="text"]', 'all', optionBox); - for (i = 0; 3 * i < inputs.length; i++) { - if (i === 0 && inputs.length !== 0) { - _option.quickSite = []; - } - if (inputs[3 * i + 1].value === '') { + + const inputs = gE('.hvAAQuickSite input[type="text"], .hvAAQuickSite input[type="number"]', 'all', optionBox); + if (inputs.length) _option.quickSite = []; + for (let i = 0; i < inputs.length; i += 3) { + const [fav, name, url] = Array.from(inputs).slice(i, i + 3).map(input => input.value); + if (name === '') continue; + _option.quickSite.push({ fav, name, url }); + } + return _option; + } + + function formatOption(option, skipUI) { + for (const obj in option) { + if (['auto', 'server'].includes(obj)) continue; + if (getOptionInput(obj, optionBox)) continue; + if (option[obj] instanceof Object) { + let found = false; + for (const key in option[obj]) { + if (found ||= getOptionInput(`${obj}_${key}`, optionBox)) continue; + if (!['enableItemWorld', 'levelItemWorld', 'enableItemWorld', 'itemWorldPersona', 'itemWorldEquipSet'].includes(obj)) console.log(`Legacy option deleted: ${obj}_${key}`); + delete option[obj][key]; + } + if (!found) delete option[obj]; continue; } - _option.quickSite.push({ - fav: inputs[3 * i].value, - name: inputs[3 * i + 1].value, - url: inputs[3 * i + 2].value, - }); + console.log(`Legacy option deleted: ${obj}`); + delete option[obj]; } - setValue('option', _option); - optionBox.style.display = 'none'; - // 更改设置后实时刷新竞技场数据 - const arenaNew = _option.idleArenaValue; - if(arenaNew === arenaPrev){ - goto(); - return; + + const inputs = gE('input, select', 'all', optionBox); + + let name, array, value, type, placeholder, num; + function formatValue(value, placeholder) { + if (['', undefined].includes(value) && placeholder) { + value = placeholder; + } + switch (type) { + case 'text': + case 'hidden': + case 'select-one': + case 'number': + num = value * 1; + return (!isNaN(num)) ? num : value; + case 'checkbox': + return value ? true : undefined; + default: + return value; + } } - if(_option.idleArena && _option.idleArenaValue){ - const arena = getValue('arena', true); - arena.isOptionUpdated = undefined; - setValue('arena', arena); - goto(); + + for (const input of inputs) { + [name, type, placeholder] = [input.name || input.id, input.type, input.placeholder]; + switch(input.className) { + case 'hvAADebug': continue; + case 'hvAANumber': type = 'number'; + } + [array, value] = [name.split('_'), undefined]; + if (array.length === 1) { + value = formatValue(option[name], placeholder); + if (value || value === 0) option[name] = value; + } else if (!input.classList.contains('customizeInput')) { + value = formatValue(option[array[0]]?.[array[1]], placeholder); + if (value || value === 0) (option[array[0]] ??= {})[array[1]] = value; + } + if (type !== 'checkbox' && ![placeholder * 1, placeholder].includes(value)) { + value = value === undefined ? '' : value; + } + + if (skipUI || onIsekaiEncounter) continue; + switch (type) { + case 'select-one' : + case 'text': + case 'hidden': + case 'number': + input.value = value; + customizeInputAutoFit(input); + break; + case 'checkbox': + input.checked = !!value; + displayCheckBoxNotDefault(input); + input.addEventListener('change', () => displayCheckBoxNotDefault(input)); + } } - }; - gE('.hvAACancel', optionBox).onclick = function () { - optionBox.style.display = 'none'; - }; - if (g('option')) { - let i; let j; let - k; - const _option = g('option'); - const inputs = gE('input,select', 'all', optionBox); - let itemName; let itemArray; let itemValue; let - _html; - for (i = 0; i < inputs.length; i++) { - if (inputs[i].className === 'hvAADebug') { + return option; + } + + function loadOptionUIData(uiOption) { + let isCache = !uiOption; + uiOption ??= option; + if (!uiOption) return; + uiOption = formatOption(uiOption); + if (onIsekaiEncounter) return; + const customizes = gE('.customize', 'all', optionBox); + customizes.forEach(n => { while(n.firstChild) { n.removeChild(n.firstChild); }}); + for (let customize of customizes) { + const name = customize.getAttribute('name'); + if (!(name in uiOption)) { + const group = customize.appendChild(cE('div')); + group.className = 'customizeGroup'; + group.innerHTML = `1. `; + const input = group.appendChild(cE('input')); + input.type = 'text'; + input.className = 'customizeInput'; + input.name = `${name}_0`; + customizeInputAutoFit(input, true); continue; } - itemName = inputs[i].name || inputs[i].id; - if (typeof _option[itemName] !== 'undefined') { - itemValue = _option[itemName]; - } else { - itemArray = itemName.split('_'); - itemValue = ''; - if (itemArray.length === 2 && typeof _option[itemArray[0]] === 'object' && inputs[i].className !== 'hvAACustomize' && typeof _option[itemArray[0]][itemArray[1]] !== 'undefined') { - itemValue = _option[itemArray[0]][itemArray[1]]; - } - } - if (inputs[i].type === 'text' || inputs[i].type === 'hidden' || inputs[i].type === 'select-one' || inputs[i].type === 'number') { - inputs[i].value = itemValue; - } else if (inputs[i].type === 'checkbox') { - inputs[i].checked = itemValue; - } - } - const customize = gE('.customize', 'all', optionBox); - for (i = 0; i < customize.length; i++) { - itemName = customize[i].getAttribute('name'); - if (itemName in _option) { - for (j in _option[itemName]) { - const group = customize[i].appendChild(cE('div')); - group.className = 'customizeGroup'; - group.innerHTML = `${j * 1 + 1}. `; - for (k = 0; k < _option[itemName][j].length; k++) { - const input = group.appendChild(cE('input')); - input.type = 'text'; - input.className = 'customizeInput'; - input.name = `${itemName}_${j}`; - input.value = _option[itemName][j][k]; - } + for (const groupIndex in uiOption[name]) { + const group = customize.appendChild(cE('div')); + group.className = 'customizeGroup'; + group.innerHTML = `${groupIndex * 1 + 1}. `; + for (const index of range(uiOption[name][groupIndex])) { + const input = group.appendChild(cE('input')); + input.type = 'text'; + input.className = 'customizeInput'; + input.name = `${name}_${groupIndex}`; + input.value = uiOption[name][groupIndex][index]; + customizeInputAutoFit(input, index === uiOption[name][groupIndex].length-1); } } } - if (_option.quickSite) { - _html = '图标圖標ICON名称名稱Name链接鏈接Link'; - _option.quickSite.forEach((i) => { + + if (!isCache) return; + g('option', uiOption); + let _html; + if (option.quickSite) { + _html = `${UI.l('图标', '圖標', 'ICON')}${UI.l('名称', '名稱', 'Name')}${UI.l('链接', '鏈接', 'Link')}`; + option.quickSite.forEach((i) => { _html = `${_html}`; }); gE('.hvAAQuickSite>table>tbody', optionBox).innerHTML = _html; } + if (getValue('backup')) { const backups = getValue('backup', true); _html = ''; - for (i in backups) { + for (const i in backups) { _html = `${_html}
    • ${i}
    • `; } gE('.hvAABackupList', optionBox).innerHTML = _html; } + }; + } + + function getInputTitle(input) { + input.title ||= input.id || input.name; + return input.title; + } + + function getOptionInput(id, optionBox) { + optionBox ??= gE('#hvAABox'); + const getter = id => gE(`[name="${id}"], [id="${id}"]`, optionBox); + return getter(id) ?? getter(id.replace(/_(\d+)$/, '')); + } + + function getLabelsFor(id, optionBox) { + return gE(`label[for="${id}"], label[for*="${id},"]`, 'all', optionBox ?? gE('#hvAABox')); + } + + function getInputFriendlyName(input, optionBox) { + const id = input.id || input.name; + let labels = getLabelsFor(id); + let inGroup, conditionDetail = '', prefix = ''; + if (!labels.length) { + const regExp = /_(\d+)$/; + inGroup = [...input.parentNode.childNodes].indexOf(input); + const group = (id.match(regExp)?.[1] ?? 0)*1; + const conditionGroup = id.replace(regExp, ''); + labels = getLabelsFor(conditionGroup, optionBox ?? gE('#hvAABox')); + conditionDetail = ` ${UI.byLang('条件', '條件', 'Condition')} ${group+1}. ${inGroup}`; + } + const customize = input.closest('.customize'); + if (customize) { + const inputs = [...gE('input', 'all', customize)].map(i => (i.id||i.name).replace(/_\d+$/, '')); + const forContent = unique(inputs.filter(s => s)); + const outter = getLabelsFor(customize.className.match(/([^\s]*)Inner/)?.[1]); + labels = [...outter, ...labels]; + } + const outter = getLabelsFor(input.className.match(/([^\s]*)Inner/)?.[1]); + labels = [...outter, ...labels]; + + return UI.cutLang(`${Array.from(labels).map(x => x ? `${x.innerHTML}${conditionDetail} [${id}${conditionDetail ? `_${inGroup-1}` : ''}]` : x).reduce((acc, cur) => (acc ?? '') + (cur ?? ''), '') || id}`.replaceAll(new RegExp(`\\[${id}\\]((.|\s)+)\\[${id}\\]`, 'g'), (match, inner) => `${inner}[${id}]`)).replaceAll(/<.*?>/g,'').replaceAll(/</g, '<').replaceAll(/>/g, '>'); + } + + async function setInputTitle(input) { try { + const id = input.id || input.name; + input.title ||= `Loading Name ... [${id}]`; + g().titleQueue ??= []; + if (g().titleQueue.includes(input)) return; + g().titleQueue.push(input); + applyLabelTitle(input); + if (g().isProcessingTitleQueue) return; + g().isProcessingTitleQueue = true; + processTitleQueue(); + } catch (err) { console.error(err); }} + + function applyLabelTitle(input) { + getLabelsFor(input.id || input.name).forEach(label => { + if (label.getAttribute('for')?.split(',').length !== 1) return; + label.title = input.title; + }); + const tab = input.closest('.hvAATabmenu>span'); + if (tab) tab.title = input.title; + } + + async function processTitleQueue(max = 10) { try { + const box = gE('#hvAABox'); + await until(() => max-- <= (box.style.display !== 'none' ? 9 : 0), 1000); + await until(() => { + let input = g().titleQueue.shift(); + if (!input) return; + input.title = getInputFriendlyName(input); + applyLabelTitle(input); + return !g().titleQueue.length; + }); + g().isProcessingTitleQueue = false; + } catch (err) { console.error(err); }} + + function customizeInputAutoFit(input, isLastCustomizeInput) { + const id = input.id || input.name; + if (input.type === 'select-one' || input.disabled && input.name !== 'version') { + setInputTitle(input); + return; + } + if (!input.title) setInputTitle(input); + + customizerInpuFit(input, isLastCustomizeInput); + input.addEventListener('input', _ => customizerInpuFit(input, true)); + input.addEventListener('keydown', function(event) { + if (event.key !== 'Enter') return; + event.preventDefault(); + const currentGroup = input.parentNode; + let nextGroup = event.shiftKey ? currentGroup.previousElementSibling : currentGroup.nextElementSibling; + if (!nextGroup && !event.shiftKey) { + const nextGroupIndex = input.name.match(/_(\d+)$/)[1] * 1 + 1; + nextGroup = input.closest('.customize').appendChild(cE('div')); + nextGroup.className = 'customizeGroup'; + nextGroup.innerHTML = `${nextGroupIndex + 1}. ` + + const newInput = nextGroup.appendChild(cE('input')); + newInput.type = 'text'; + newInput.className = 'customizeInput'; + newInput.name = input.name.replace(/_(\d+)$/, _ => `_${nextGroupIndex}`); + customizeInputAutoFit(newInput, true); + + const select = gE('.customizeBox select[name="groupChoose"]'); + if (select) { + const selectOptions = gE('option', 'all', select); + const prevSelected = select.value; + const optionUI = select.appendChild(cE('option')); + selectOptions[selectOptions.length - 1].value = nextGroupIndex + 1; + selectOptions[selectOptions.length - 1].textContent = nextGroupIndex + 1; + optionUI.value = 'new'; + optionUI.textContent = 'new'; + select.value = prevSelected; + } + } + ((nextGroup ? gE(event.shiftKey ? '.customizeInput:last-child' : '.customizeInput:first-child', nextGroup) : undefined) ?? gE('.customizeInput:first-child', currentGroup)).focus() + }); + } + + function selectFit(select) { + if (select.value === 'undefined') { + select.classList.add('optionDefault'); + } else { + select.classList.remove('optionDefault'); } } - function customizeBox() { // 自定义条件界面 - const customizeBox = gE('body').appendChild(cE('div')); + function customizerInpuFit(input, dynamic) { + if (input.value === input.placeholder) { + input.classList.add('optionDefault'); + } else { + input.classList.remove('optionDefault'); + } + autoResizeInput(input); + if (!input.classList.contains('customizeInput') || !dynamic) return; + if (input.nextElementSibling || !input.value) return; + const newInput = input.parentNode.appendChild(cE('input')); + newInput.type = 'text'; + newInput.className = 'customizeInput'; + newInput.name = input.getAttribute('name'); + customizeInputAutoFit(newInput); + } + + function autoResizeInput(input) { + const measure = cE('span'); + const styles = window.getComputedStyle(input); + measure.style.cssText = ` + visibility: hidden; + white-space: pre; + font-family: ${styles.fontFamily}; + font-size: ${styles.fontSize}; + font-weight: ${styles.fontWeight}; + letter-spacing: ${styles.letterSpacing}; + padding: ${styles.padding}; + border: ${styles.border}; + box-sizing: ${styles.boxSizing}; + position: absolute; + top: -9999px; + left: -9999px; + `; + measure.textContent = input.value; + document.body.appendChild(measure); + input.style.width = measure.offsetWidth + 'px'; + document.body.removeChild(measure); + } + + function creatCustomizeBox() { // 自定义条件界面 + let customizeBox = gE('.customizeBox'); + if (customizeBox) return customizeBox; + customizeBox = gE('body').appendChild(cE('div')); customizeBox.className = 'customizeBox'; - const statusOption = [ + const statusOption = creatCustomizeBox.prototype.statusOption ??= [ '', '', '', '', + '', + '', + '', + '', '', '', '', @@ -1747,21 +4364,69 @@ try { '', '', '', - '', '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', '', + '', '', + '', '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', '', ].join(''); + customizeBox.style.cssText += 'display: none;'; customizeBox.innerHTML = [ '??', `${String.fromCharCode(0x21F1.toString(10))}`, '', ``, - '', + '', ``, - '', + UI.button.class('groupAdd', 'ADD') ].join(' '); const funcSelect = function (e) { let box; @@ -1775,7 +4440,7 @@ try { let find = attr(target); while (!find) { target = target.parentNode; - if (target.id === 'csp' || target.tagName === 'BODY') { + if (target.id === 'csp' || target.tagName.toUpperCase() === 'BODY') { box.style.display = 'none'; return; } @@ -1796,25 +4461,43 @@ try { } }; gE('.groupAdd', customizeBox).onclick = function () { - const target = g('customizeTarget'); + const target = g().customizeTarget; const selects = gE('select', 'all', customizeBox); let groupChoose = selects[0].value; let group; if (groupChoose === 'new') { - groupChoose = gE('option', 'all', selects[0]).length; + const select = gE('select[name="groupChoose"]', customizeBox); + const selectOptions = gE('option', 'all', select); + groupChoose = selectOptions.length; group = target.appendChild(cE('div')); group.className = 'customizeGroup'; group.innerHTML = `${groupChoose}. `; selects[0].click(); + const prevSelected = select.value; + const optionUI = select.appendChild(cE('option')); + selectOptions[selectOptions.length-1].value = groupChoose; + selectOptions[selectOptions.length-1].textContent = groupChoose; + optionUI.value = 'new'; + optionUI.textContent = 'new'; + select.value = prevSelected; } else { group = gE('.customizeGroup', 'all', target)[groupChoose - 1]; } - const input = group.appendChild(cE('input')); + const items = gE('*', 'all', group); + let input; + for (let i of range(items, 0, -1)) { + if (items[i-1].value) break; + input = items[i-1]; + } + input ??= group.appendChild(cE('input')); input.type = 'text'; input.className = 'customizeInput'; input.name = `${target.getAttribute('name')}_${groupChoose - 1}`; - input.value = `${selects[1].value},${selects[2].value},${selects[3].value}`; + input.value = `${selects[1].value} ${selects[2].value} ${selects[3].value}`; + customizeInputAutoFit(input, true); + updateGroup(true); }; + return customizeBox; function attr(target) { const onmouseover = target.getAttribute('onmouseover'); @@ -1830,767 +4513,1841 @@ try { } } - function setAlarm(e) { // 发出警报 + function updateGroup(keepPosition) { + const target = g().customizeTarget; + const group = gE('.customizeGroup', 'all', target); + const customizeBox = gE('.customizeBox'); + if (group.length + 1 === gE('select[name="groupChoose"]>option', 'all', customizeBox).length) { + updateGroupUI(); + return; + } + const select = gE('select[name="groupChoose"]', customizeBox); + select.textContent = ''; + for (const i of range(group.length + 1)) { + const optionUI = select.appendChild(cE('option')); + optionUI.textContent = optionUI.value = i === group.length ? 'new' : i + 1; + } + updateGroupUI(); + + function updateGroupUI() { + const position = target.getBoundingClientRect(); + const bodyPosition = document.body.getBoundingClientRect(); + customizeBox.style.cssText += `z-index: 20;display: block; height: ${gE('.customizeGroup', 'all', target).length * 30 + 60}px;` + if (!keepPosition) { + customizeBox.style.top = `${position.bottom - bodyPosition.top}px`; + customizeBox.style.left = `${position.left - bodyPosition.left}px`; + } + } + } + + function setAlarm(e, testSrc) { // 发出警报 + const option = getOption(); e = e || 'Common'; - if (g('option').notification) { + if (option.notification || testSrc) { setNotification(e); } - if (g('option').alert && g('option').audioEnable && g('option').audioEnable[e]) { - setAudioAlarm(e); + if (option.alert && option.audioEnable?.[e] || testSrc) { + setAudioAlarm(e, testSrc); } } - function setAudioAlarm(e) { // 发出音频警报 - let audio; - if (gE(`#hvAAAlert-${e}`)) { - audio = gE(`#hvAAAlert-${e}`); - } else { + function playAudio(audio) { + audio.onPlay ??= () => { + audio.removeEventListener('canplaythrough', audio.onPlay); + audio.play(); + }; + audio.removeEventListener('canplaythrough', audio.onPlay); + audio.addEventListener('canplaythrough', audio.onPlay); + // 如果音频已缓存,canplaythrough 可能不会再次触发,此时可直接播放 + if (audio.readyState >= 3) { // HAVE_FUTURE_DATA 或更高 + audio.play(); + } + if (audio.loop) { + const battleNow = unsafeWindow.battle; + (async ()=> { + const start = time(0); + let focused; + await until(()=> { + if (!focused && document.hasFocus()) { + focused = true; + document.addEventListener('mousemove', () => { + audio.pause(); + audio.loop = false; + }, true); + } + return !audio.loop || (unsafeWindow.battle !== battleNow); + }); + audio.pause(); + })(); + } + } + + function setAudioAlarm(e, testSrc) { // 发出音频警报 + const option = getOption(); + let audio = gE(`#hvAAAlert-${e}`); + const fileType = '.ogg'; // var fileType = (/Chrome|Safari/.test(navigator.userAgent)) ? '.mp3' : '.wav'; + if (!audio) { audio = gE('body').appendChild(cE('audio')); audio.id = `hvAAAlert-${e}`; - const fileType = '.ogg'; // var fileType = (/Chrome|Safari/.test(navigator.userAgent)) ? '.mp3' : '.wav'; - audio.src = (g('option').audio && g('option').audio[e]) ? g('option').audio[e] : `https://github.com/dodying/UserJs/raw/master/HentaiVerse/hvAutoAttack/${e}${fileType}`; audio.controls = true; - audio.loop = (e === 'Riddle'); } - audio.play(); - - function pauseAudio(e) { - audio.pause(); - document.removeEventListener(e.type, pauseAudio, true); + if (!audio.src || audio.readyState <= 2 || (testSrc && audio.src !== testSrc)) { + audio.src = testSrc ?? option.audio?.[e] ?? `https://github.com/dodying/UserJs/raw/master/HentaiVerse/hvAutoAttack/${e}${fileType}`; } - document.addEventListener('mousemove', pauseAudio, true); + audio.loop = (e === 'Riddle') && !testSrc; + playAudio(audio); } function setNotification(e) { // 发出桌面通知 - const notification = [ - { - Common: { - text: '未知', - time: 5, - }, - Error: { - text: '某些错误发生了', - time: 10, - }, - Defeat: { - text: '游戏失败\n玩家可自行查看战斗Log寻找失败原因', - time: 5, - }, - Riddle: { - text: '小马答题\n紧急!\n紧急!\n紧急!', - time: 30, - }, - Victory: { - text: '游戏胜利\n页面将在3秒后刷新', - time: 3, - }, - Test: { - text: '测试文本', - time: 3, - }, - }, { - Common: { - text: '未知', - time: 5, - }, - Error: { - text: '某些錯誤發生了', - time: 10, - }, - Defeat: { - text: '遊戲失敗\n玩家可自行查看戰鬥Log尋找失敗原因', - time: 5, - }, - Riddle: { - text: '小馬答題\n緊急!\n緊急!\n緊急!', - time: 30, - }, - Victory: { - text: '遊戲勝利\n頁面將在3秒後刷新', - time: 3, - }, - Test: { - text: '測試文本', - time: 3, - }, - }, { - Common: { - text: 'unknown', - time: 5, - }, - Error: { - text: 'Some errors have occurred', - time: 10, - }, - Defeat: { - text: 'You have been defeated.\nYou can check the battle log.', - time: 5, - }, - Riddle: { - text: 'Riddle\nURGENT\nURGENT\nURGENT', - time: 30, - }, - Victory: { - text: 'You\'re victorious.\nThis page will refresh in 3 seconds.', - time: 3, - }, - Test: { - text: 'testText', - time: 3, - }, + const notification = setNotification.prototype.notification ??= { + Common: { + text: UI.byLang('未知', '未知', 'unknown'), + time: 5, + }, + Error: { + text: UI.byLang('某些错误发生了', '某些錯誤發生了', 'Some errors have occurred'), + time: 10, + }, + Defeat: { + text: UI.byLang('游戏失败\n玩家可自行查看战斗Log寻找失败原因', '遊戲失敗\n玩家可自行查看戰鬥Log尋找失敗原因', 'You have been defeated.\nYou can check the battle log.'), + time: 5, + }, + Riddle: { + text: UI.byLang('小马答题\n紧急!\n紧急!\n紧急!', '小馬答題\n緊急!\n緊急!\n緊急!', 'Riddle\nURGENT\nURGENT\nURGENT'), + time: 30, + }, + Victory: { + text: UI.byLang('游戏胜利\n页面将在3秒后刷新', '遊戲勝利\n頁面將在3秒後刷新', 'You\'re victorious.\nThis page will refresh in 3 seconds.'), + time: 3, + }, + Pause: { + text: UI.byLang('触发自动暂停', '觸發自動暫停', 'Auto paused'), + time: 3, }, - ][g('lang')][e]; + Flee: { + text: UI.byLang('触发自动逃跑', '觸發自動逃跑', 'Auto fleed'), + time: 3, + }, + BattleUnresponsive: { + text: UI.byLang('战斗无响应', '戰鬥無響應', 'Battle unresponsive'), + time: 3, + }, + Test: { + text: UI.byLang('测试文本', '測試文本', 'testText'), + time: 3, + }, + }[e]; if (typeof GM_notification !== 'undefined') { GM_notification({ text: notification.text, - image: `${window.location.origin}/y/hentaiverse.png`, - highlight: true, - timeout: 1000 * notification.time, + image: `${window.location.origin}${unsafeWindow.IMG_URL}hentaiverse.png`, + highlight: getOption().focusNotification, + timeout: notification.time * _1s, }); } if (window.Notification && window.Notification.permission !== 'denied') { window.Notification.requestPermission((status) => { if (status === 'granted') { const n = new window.Notification(notification.text, { - icon: '/y/hentaiverse.png', + icon: `${unsafeWindow.IMG_URL}hentaiverse.png`, }); - setTimeout(() => { - if (n) { - n.close(); - } - }, 1000 * notification.time); + setTimeout(() => n?.close(), notification.time * _1s); - var nClose = function (e) { - if (n) { - n.close(); - } + const nClose = function (e) { + n?.close(); document.removeEventListener(e.type, nClose, true); }; document.addEventListener('mousemove', nClose, true); - // document.addEventListener('click', nClose, true); } }); } } - function checkCondition(parms) { - if (typeof parms === 'undefined') { - return true; - } - let i; let j; let - k; - const result = []; - const returnValue = function (str) { - if (str.match(/^_/)) { - const arr = str.split('_'); - return func[arr[1]](...[...arr].splice(2)); - } if (str.match(/^'.*?'$|^".*?"$/)) { - return str.substr(1, str.length - 2); - } if (isNaN(str * 1)) { - const paramList = str.split('.'); + function imgArray2img(...img) { + return img.join('_').replace('_png', 'png'); + } + + function returnValueGetter(paramResultsGetter, targetGetter) { + let minmaxModes = returnValueGetter.prototype.minmaxModes ??= (() => { + const modes = ['min', 'max', 'count', 'sum']; + const flags = ['a', 'ag', 'g']; + return flags.reduce((result, f) => result.concat(modes.map(m => f + m)), []); + })(); + returnValueGetter.prototype.func ??= { + ar() { + return g().battle.roundType === 'ar' ? 1 : 0; + }, + gr() { + return g().battle.roundType === 'gr' ? 1 : 0; + }, + tw() { + return g().battle.roundType === 'tw' ? 1 : 0; + }, + rb() { + return g().battle.roundType === 'rb' ? 1 : 0; + }, + iw() { + return g().battle.roundType === 'iw' ? 1 : 0; + }, + ba() { + return g().battle.roundType === 'ba' ? 1 : 0; + }, + isRoundType(t) { + return g().battle.roundType === t ? 1 : 0; + }, + phys() { + return g().attackStatus * 1 === 0 ? 1 : 0; + }, + fire() { + return g().attackStatus * 1 === 1 ? 1 : 0; + }, + cold() { + return g().attackStatus * 1 === 2 ? 1 : 0; + }, + elec() { + return g().attackStatus * 1 === 3 ? 1 : 0; + }, + wind() { + return g().attackStatus * 1 === 4 ? 1 : 0; + }, + divi() { + return g().attackStatus * 1 === 5 ? 1 : 0; + }, + forb() { + return g().attackStatus * 1 === 6 ? 1 : 0; + }, + attackStatusCur() { + return getCurrentAttackStatus() * 1; + }, + physCur() { + return getCurrentAttackStatus() * 1 === 0 ? 1 : 0; + }, + fireCur() { + return getCurrentAttackStatus() * 1 === 1 ? 1 : 0; + }, + coldCur() { + return getCurrentAttackStatus() * 1 === 2 ? 1 : 0; + }, + elecCur() { + return getCurrentAttackStatus() * 1 === 3 ? 1 : 0; + }, + windCur() { + return getCurrentAttackStatus() * 1 === 4 ? 1 : 0; + }, + diviCur() { + return getCurrentAttackStatus() * 1 === 5 ? 1 : 0; + }, + forbCur() { + return getCurrentAttackStatus() * 1 === 6 ? 1 : 0; + }, + + nt() { + return g().fightingStyle * 1 === 1 ? 1 : 0; + }, + onehanded() { + return g().fightingStyle * 1 === 2 ? 1 : 0; + }, + twohanded() { + return g().fightingStyle * 1 === 3 ? 1 : 0; + }, + dw() { + return g().fightingStyle * 1 === 4 ? 1 : 0; + }, + staff() { + return g().fightingStyle * 1 === 5 ? 1 : 0; + }, + isCd(id) { // is cool down done + return isOn(id) ? 1 : 0; + }, + spirit() { + return gE('#ckey_spirit[src*="spirit_a"]') ? 1 : 0; + }, + buffTurn(...img) { + return getBuffTurnFromImg(getBuff(imgArray2img(...img))); + }, + buffStack(...img) { + return getBuffStackFromImg(getBuff(imgArray2img(...img))); + }, + hpDecimal() { + return g().hp / 100; + }, + mpDecimal() { + return g().mp / 100; + }, + spDecimal() { + return g().sp / 100; + }, + ocDecimal() { + return g().oc / 100; + }, + }; + let currentGroup = null; + const func = { + ...returnValueGetter.prototype.func, + weighted(...params) { + const funcName = params.shift(); + const battle = g().battle; let result; - for (let key of paramList) { - if (!result) { - result = (g('battle') ?? getValue('battle', true))[key] ?? g(key) ?? getValue(key); - continue; - } - result = result[key] + if (!g().inSkillExtraWeight) { + const origin = JSON.parse(JSON.stringify(g().battle.monsterStatus)); + battle.monsterStatus.forEach(t => { t.finWeight += resolveSkillExtraWeight(t); }); + battle.monsterStatus = battle.monsterStatus.sortBy(x => x.finWeight); + result = func[funcName](...params); + battle.monsterStatus = origin; + } else { + result = func[funcName](...params); } - return isNaN(result * 1) ? result : (result * 1); - } - return str * 1; - }; - var func = { - isCd(id) { - return isOn(id) ? 0 : 1; + return result; + }, + targetBuffStack(...img) { + const getter = (t, i) => getBuffStackFromImg(getBuff(imgArray2img(i), getMonsterID(t))); + let param = minmaxModes.includes(img[0]) ? img.shift() : undefined; + return switchMaxMin(param, t => getter(t, img)); + }, + targetBuffTurn(...img) { + const getter = (t, i) => getBuffTurnFromImg(getBuff(imgArray2img(i), getMonsterID(t))); + let param = minmaxModes.includes(img[0]) ? img.shift() : undefined; + return switchMaxMin(param, t => getter(t, img)); }, - buffTurn(img) { - let buff = gE(`#pane_effects>img[src*="${img}"]`); - if (!buff) { - return 0; + targetOrder(param) { + return switchMaxMin(param, t => t.order); + }, + targetWeight(param) { + return switchMaxMin(param, t => t.finWeight); + }, + targetRank(param) { + return switchMaxMin(param, t => Object.entries(g().battle.monsterStatus).find(([k, v]) => v.order === t.order)[0] * 1); + }, + targetName(param) { + param ??= targetGetter(); + const mon = getMonster(getMonsterID(param)); + return gE(`.btm3>div>div`, mon).innerText.replace(' ', '_'); + }, + targetBossType(param) { + return switchMaxMin(param, t => { + const name = func.targetName(t); + switch(name.replace('_', ' ')) { + case 'Manbearpig': + case 'White Bunneh': + case 'Mithra': + case 'Dalek': + return 1; // BOSS + case 'Konata': + case 'Mikuru Asahina': + case 'Ryouko Asakura': + case 'Yuki Nagato': + return 2; // Legendaries + case 'Real Life': + case 'Invisible Pink Unicorn': + case 'Flying Spaghetti Monster': + return 3; // Gods + case 'Rhaegal': + case 'Viserion': + case 'Drogon': + return 4; // A Dance with Dragons + case 'Skuld': + case 'Urd': + case 'Verdandi': + case 'Yggdrasil': + return 5; // Trio and the Tree + case 'Recycled Boss Rush': + case 'Bottomless Dungeon': + case 'New Game +': + case 'Achievement Grind': + case 'Time Trial Mode': + case 'Hardcore Mode': + return 6; // Post Game Content + case 'Fluttershy': + case 'Gummy': + case 'Rainbow Dash': + case 'Twilight Sparkle': + case 'Rarity': + case 'Applejack': + case 'Pinkie Pie': + case 'Angel Bunny': + case 'Spike': + return 7; // Ponies + default: + return 0; + }}); + }, + targetIsAlive(param) { + return switchMaxMin(param, t => t.isDead ? 0 : 1); + }, + targetHp(param) { + return switchMaxMin(param, t => Math.floor(func.targetHpDecimal() * 100)); + }, + targetMp(param) { + return switchMaxMin(param, t => Math.floor(func.targetMpDecimal() * 100)); + }, + targetSp(param) { + return switchMaxMin(param, t => Math.floor(func.targetSpDecimal() * 100)); + }, + targetHpDecimal(param) { + return switchMaxMin(param, t => t.hpNow / t.hp); + }, + targetMpDecimal(param) { + return switchMaxMin(param, t => t.mpNow); + }, + targetSpDecimal(param) { + return switchMaxMin(param, t => t.spNow); + }, + targetGroup(...args) { + const groupMode = args.shift(); + const numArgs = args.map(arg => arg === '' ? -1 : parseInt(arg)); + if (numArgs.some(isNaN)) throw new Error(`Error args for targetGroup, args: ${args}.`); + const currentTarget = targetGetter(); + const targets = g().battle.monsterStatus; + switch(groupMode) { + case 'a': // all targets (as default if currentGroup is undefined) + currentGroup = targets; + break; + case 's': // 等数量自动分组 + { + const groupSize = numArgs[0]; + if (groupSize <= 0) throw new Error(`Using zero/subzero or error groupSize in targetGroup, args: ${args}.`); + const getGroupIndex = t => Math.floor(t.order / groupSize); + const groupIndex = getGroupIndex(currentTarget); + currentGroup = targets.filter(t => getGroupIndex(t) === groupIndex); + } + break; + case 'r': // 按照和current的距离 + { + let [rangeUp, rangeDown] = [numArgs[0], numArgs[1]]; + if (rangeUp === undefined) throw new Error(`1 args at least is required for targetGroup as mode 'r'.`); + // 对称范围. 只有单参数的 `targetGroup_r_[r1]` 时会是该情况,双参数的`targetGroup_r_[r1]_`时range2在param split后赋值为 '',然后在numArgs中赋值为-1 + rangeDown ??= rangeUp; + // >= 10 的,视作反方向。即 10 <=> -1 <=> ''(省略但有_分割) + [rangeUp, rangeDown] = [rangeUp, rangeDown].map(r => r >= 10 ? 9 - r : r ); + const center = currentTarget.order; + const [startOrder, endOrder] = [center - rangeUp, center + rangeDown]; + currentGroup = targets.filter(t => t.order >= startOrder && t.order <= endOrder); + } + break; + case 'oa': // 按照指定order,忽略current是否在内 + case 'o': // 按照指定order + { + const [startOrder, endOrder] = numArgs; + startOrder ??= -1; + if ([undefined, -1].includes(endOrder)) + { + endOrder = 10; + } + // 检查当前评估的怪物是否在区间内 + if (groupMode !== 'oa' && (currentTarget.order < startOrder || currentTarget.order > endOrder)) { + currentGroup = null; + } else { + // 检查该区间内是否有活怪带有此 Buff + currentGroup = targets.filter(t => t.order >= startOrder && t.order <= endOrder); + } + } + break; + default: + throw new Error(`Unsupported targetGroup mode: ${groupMode}.`); } - buff = buff.getAttribute('onmouseover').match(/\(.*,.*, (.*?)\)$/)[1] * 1; - return isNaN(buff) ? Infinity : buff; + return currentGroup ? Object.keys(currentGroup).length : 0; }, + undefined: () => undefined, + null: () => null }; - for (i in parms) { - for (j = 0; j < parms[i].length; j++) { - if (!Array.isArray(parms[i])) { + function switchMaxMin(param, defaultResult, skipAliveCheck = false, targets = undefined) { + if (['gacount', 'gasum', 'gamax', 'gamin', 'gcount', 'gsum', 'gmax', 'gmin'].includes(param)) { + return switchMaxMin(param.replace(/^g/, ''), defaultResult, skipAliveCheck, currentGroup); + } + if (['acount', 'asum', 'amax', 'amin', 'agcount', 'agsum', 'agmax', 'agmin'].includes(param)) { + return switchMaxMin(param.replace(/^a/, ''), defaultResult, true, targets); + } + if (targets === undefined) targets = g().battle.monsterStatus; // 只处理 undefined,null 是空 group + if (!targets) return 0; + if (!skipAliveCheck) targets = targets.filter(t => !t.isDead); + switch (param) { + case 'count': + return targets.map(defaultResult).reduce((acc, cur) => acc + Math.sign(cur), 0); + case 'sum': + return targets.map(defaultResult).reduce((acc, cur) => acc + cur, 0); + case 'max': + return Math.max(...targets.map(defaultResult)); + case 'min': + return Math.min(...targets.map(defaultResult)); + default: + if (param !== undefined) console.warn(`Unknown param`, param, `for switchMaxMin, fallback to default.`); + return defaultResult(targetGetter()); + } + } + + const getter = function (str, isDebug) { + const debug = str.match(/^#/); + if (debug) str = str.replace(/^#/, ''); + + const onResult = (r) => { + if (debug || isDebug) paramResultsGetter()[str] = r; + if (debug) console.log([str], r); + return r; + } + + // 旧版本/强制使用func + if (str.match(/^_/) && !str.match(/\./)) { + const arr = str.split('_'); + return onResult(func[arr[1]](...[...arr].splice(2))); + } + if (!isNaN(str * 1)) { // 数字直接返回 + return onResult(str * 1); + } + const paramList = str.replace(/[^\d](\.)/g, (match, ...p) => { + return match.replace('.', '_'); // 将不是数字小数点的 . 转为 _ 以便进行参数分割 + }).split('_'); + let result, isInData; + const option = getOption(); + const battle = g().battle ?? {}; + while (paramList.length) { + const key = paramList.shift(); + if (typeof result === 'undefined') { // 获取顶层数据 + result = battle[key]; + if (typeof result === 'undefined' || result === null) { + result = getValue('battle', true) ? getValue('battle', true)[key] : undefined; + } + if (typeof result === 'undefined' || result === null) { + result = g(key); + } + if (typeof result === 'undefined' || result === null) { + result = getValue(key); + } + if (typeof result === 'undefined' || result === null) { + result = option[key]; + } + if ((typeof result === 'undefined' || result === null) && func[key]) { + result = func[key](...paramList); + } + if (typeof result === 'undefined' || result === null) break; + isInData = true; // 存在顶层数据 continue; } - k = parms[i][j].split(','); - const kk = k.toString(); - k[0] = returnValue(k[0]); - k[2] = returnValue(k[2]); + if (typeof result === 'string') { try { + result = JSON.parse(result); + } catch (err) {}} + if (['number', 'string'].includes(typeof result)) continue; + result = result[key]; + } - if (k[0] === undefined || k[0] === null || (typeof k[0] !== "string" && isNaN(k[0]))) { - Debug.log(kk[0], k[0]); - } - if (k[2] === undefined || k[2] === null || (typeof k[2] !== "string" && isNaN(k[2]))) { - Debug.log(kk[2], k[2]); - } + result ??= isInData ? 0 : result; // 存在顶层数据时默认为0 + return onResult(isNaN(result * 1) ? result ?? str : (result * 1)); + } + getter.paramResultsGetter = paramResultsGetter; + return getter; + } - switch (k[1]) { - case '1': - result[i] = k[0] > k[2]; - break; - case '2': - result[i] = k[0] < k[2]; - break; - case '3': - result[i] = k[0] >= k[2]; - break; - case '4': - result[i] = k[0] <= k[2]; - break; - case '5': - result[i] = k[0] === k[2]; - break; - case '6': - result[i] = k[0] !== k[2]; - break; - } - if (result[i] === false) { - j = parms[i].length; - } + function handleRPNFormula(formula, returnValue) { + let k, isDebug, result; + if (!formula) return 0; + if (!isNaN(formula * 1)) return formula * 1; // 纯数字直接处理 + k = formula.replace(/,\s*(.*)\s*,/, (match, p1) => { + switch (p1) { + case '>': + case '1': + return '>'; + case '<': + case '2': + return '<'; + case '≥': + case '>=': + case '3': + return '>='; + case '≤': + case '<=': + case '4': + return '<='; + case '=': + case '==': + case '===': + case '5': + return '=='; + case '≠': + case '~=': + case '<>': + case '!=': + case '6': + return '!='; + } + }).replace(/===/g, '==').replace(/(?!~])=(?!=)/g, '==') + .replace(/≥|≤|≠|~=|<>/g, (match) => { + switch (match) { + case '≥': + return '>='; + case '≤': + return '<='; + case '≠': + case '~=': + case '<>': + return '!='; + } + }).replace('_1h', '_onehanded').replace('_2h', '_twohanded'); + isDebug = k.match(/^@/); + result = $RPN.evaluate(k.replace(/^@/, ''), str => returnValue(str, isDebug)); + if (isDebug) console.log([k], result, returnValue.paramResultsGetter()); + return result; + } + + function resolveRPNFormula(formula, target) { + let paramResults = {}; + const returnValue = returnValueGetter(() => paramResults, () => target); + return handleRPNFormula(formula, returnValue); + } + + function checkCondition(params, targets = undefined) { + let i, j, target, paramResults = {}; + targets ??= [g().battle.monsterStatus[0]]; + if (!params || !Object.keys(params).length) { + return targets[0]; + } + const returnValue = returnValueGetter(() => paramResults, () => target); + for (i in params) { for (target of targets.filter(t => !t.isDead)) { + paramResults = {}; + let paramResult = true; + for (j of range(params[i])) { + let result = true; + if (!Array.isArray(params[i])) continue; + const formula = params[i][j]; + result = handleRPNFormula(formula, returnValue); + if (result) continue; + paramResult = false; + break; } - if (result[i] === true) { - return true; + if (paramResult) return target; + }} return undefined; + } + + function pauseChange() { // 暂停状态更改 + const option = getOption(); + if (getValue('disabled')) { + if (gE('.pauseChange')) { + gE('.pauseChange').innerHTML = UI.button.pause(); + } + document.title = gE('#navbar') ? 'The Hentaiverse' : getValue('disabled'); + delValue(0); + if (!gE('#navbar')) { // in battle + onBattleRound(); + } + } else { + if (gE('.pauseChange')) { + gE('.pauseChange').innerHTML = UI.button.continue(); } + setValue('disabled', document.title); + document.title = titlePause(); } - return false; } - // 答题// - function riddleAlert() { // 答题警报 - if (window.opener) { - gE('#riddleanswer+img').onclick = function () { - riddleSubmit(gE('#riddleanswer').value); - }; - } - setAlarm('Riddle'); - const answers = ['A', 'B', 'C']; - document.onkeydown = function (e) { - gE('#hvAAAlert-Riddle')?.pause(); - if (/^[abc]$/i.test(e.key)) { - riddleSubmit(e.key.toUpperCase()); - this.onkeydown = null; - } else if (/^[123]$/.test(e.key)) { - riddleSubmit(answers[e.key - 1]); - this.onkeydown = null; - } - }; - if (g('option').riddleRadio) { - const bar = gE('body').appendChild(cE('div')); - bar.className = 'answerBar'; - answers.forEach((answer) => { - const button = bar.appendChild(cE('div')); - button.value = answer; - button.onclick = function () { - riddleSubmit(this.value); - }; - }); + function stepIn() { + setValue('stepIn', true); + if (getValue('disabled')) { + g('timeNow', time(0)); + pauseChange(); } - const checkTime = function () { - let time; - if (typeof g('time') === 'undefined') { - const timeDiv = gE('#riddlecounter>div>div', 'all'); - if (timeDiv.length === 0) { - return; - } - time = ''; - for (let j = 0; j < timeDiv.length; j++) { - time = (timeDiv[j].style.backgroundPosition.match(/(\d+)px$/)[1] / 12).toString() + time; - } - g('time', time * 1); - } else { - time = g('time'); - time--; - g('time', time); - } - document.title = time; - if (time <= g('option').riddleAnswerTime) { - riddleSubmit(gE('#riddleanswer').value || answers[parseInt(Math.random() * 3)]); - } - }; - for (let i = 0; i < 30; i++) { - setTimeout(checkTime, i * _1s); + } + + function onStepInDone() { + if (!getValue('stepIn')) { + return; } + delValue('stepIn'); + pauseChange(); + } - function riddleSubmit(answer) { - if (!window.opener) { - gE('#riddleanswer').value = answer; - gE('#riddleanswer+img').click(); - } else { - $ajax.fetch(window.location.href, `riddleanswer=${answer}`).then(() => { // 待续 - window.opener.document.location.href = window.location.href; - window.close(); - }).catch(e=>console.error(e)); + function getCurrentUser() { + const cookie = document.cookie.split("; "); + for (const cookieObj of cookie) { + const match = cookieObj.match(/ipb_member_id=(\d+)/); + if (match) { + return match[1] * 1; } } } // 战斗外// - function checkIsHV() { - if (window.location.host !== 'e-hentai.org') { // is in HV - setValue('url', window.location.origin); - return true; - } - setValue('lastEH', time(0)); - const isEngage = window.location.href === 'https://e-hentai.org/news.php?encounter'; - let encounter = getEncounter(); - let href = getValue('url') ?? (document.referrer.match('hentaiverse.org') ? new URL(document.referrer).origin : 'https://hentaiverse.org'); - const eventpane = gE('#eventpane'); + function quickSite() { // 快捷站点 + const quickSiteBar = gE('body').appendChild(cE('div')); + quickSiteBar.className = 'quickSiteBar'; + quickSiteBar.innerHTML = '<<贴吧Forums'; + getOption().quickSite?.forEach((site) => { + quickSiteBar.innerHTML = `${quickSiteBar.innerHTML}${(site.fav) ? `` : ''}${site.name}`; + }); + gE('.quickSiteBarToggle', quickSiteBar).onclick = function () { + const spans = gE('span', 'all', quickSiteBar); + for (const i of range(spans)) { + spans[i].style.display = (this.textContent === '<<') ? 'none' : 'block'; + } + this.textContent = (this.textContent === '<<') ? '>>' : '<<'; + }; + } + + async function autoSwitchIsekai() { + const option = getOption(); + await pauseAsync(option.isekaiTime * _1s - (time(0) - g().idleStart)); + await waitPause(); + $async.logSwitch(arguments); + if (!option.isekai) return; // 若不启用自动跳转 const now = time(0); - let url; - if (eventpane) { // 新一天或遭遇战 - url = gE('#eventpane>div>a')?.href.split('/')[3]; - if(url === undefined){ // 新一天 - encounter = []; + const remain = (getValue('lastSwitch') ?? 0) * 1 + (option.isekaiCD ?? 0) * _1s - now; + await pauseAsync(remain); + await waitPause(); + setValue('lastSwitch', now); + $ajax.openNoFetch(`${window.location.href.slice(0, window.location.href.indexOf('.org') + 4)}/${_server.isekai ? '' : 'isekai/'}`); + $async.logSwitch(arguments); + } + + function switchCurrent(ignoreOption) { + [_server.name, _server.other] = [_server.other, _server.name]; + [_server.isekai, _server.persistent] = [!_server.isekai, !_server.persistent]; + _server.utils = _server.utils === 'hvut' ? 'hvuti' : 'hvut'; + if (!ignoreOption) checkOption(); + } + + function isInBattle(doc) { + return gE('#riddlecounter, #battle_main', doc ?? document); + } + + async function restorePersonaAndEquipSet() { + const persona = getValue('lastPersona'); + let changed; + if (persona) { + await $ajax.fetch(queryToPersistent(`?s=Character&ss=ch`), `persona_set=${persona}`); + delValue('lastPersona'); + changed = true; + } + const equipSet = getValue('lastequipSet'); + if (equipSet) { + await $ajax.fetch(queryToPersistent(`?s=Character&ss=eq`), `equip_set=${equipSet}`); + delValue('lastEquipSet'); + changed = true; + } + return changed; + } + + function remainTime2Str(r, short) { + const digits = Math.max(2, r >= _1h ? 3 : 2); + const detail = timeStr(r, digits); + if (short) return detail; + return ` ${UI.l('剩余', '剩餘', 'Remain')} ${Math.floor(r / _1s)} (${detail})`; + } + + function appendEncounterUITitle(key, inner) { + const ui = gE('.encounterUI'); + if (!ui) return; + const regExp = new RegExp(`\\n\\[${key}\\](.|\\n)*\\[${key}\\]`); + if (!ui.title.match(regExp)) ui.title += `\n[${key}][${key}]`; + ui.title = UI.cutLang(ui.title.replace(regExp, `\n[${key}]\n${inner}\n[${key}]`)); + } + + async function displayCDRemain() { try { + const option = getOption(); + await until(() => { + const optionBox = gE('#hvAABox'); + const ui = gE('.encounterUI'); + if (optionBox.style.display === 'none' && !ui) return; + const idleStart = g().idleStart; + const now = time(0); + const durations = { + onIdle: { name: UI.l('闲置延时', '閒置延時', 'Idle Delay'), selector: '.onIdleRemain', start: g().beforeIdle, wait: option.onIdleDelay }, + encounter: { name: UI.l('遭遇延时', '遭遇延時', 'Encounter Delay'), selector: '.encounterDelayRemain', start: g().encounterStart ?? now, wait: option.encounterDelay }, + arena: { name: UI.l('闲置竞技场', '閒置競技場', 'Idle Arena'), selector: '.arenaRemain', start: idleStart ?? now, wait: option.idleArenaTime }, + switch: { name: UI.l('闲置异世界', '閒置異世界', 'Idle Isekai'), selector: '.isekaiSwitchRemain', start: idleStart ?? now, wait: option.isekaiTime }, + switchCD: { name: UI.l('异世界CD', '異世界CD', 'Isekai CD'), selector: '.isekaiCDRemain', start: (getValue('lastSwitch') ?? 0), wait: option.isekaiCD }, + } + let idleStarted; + const remain = Object.fromEntries(Object.entries(durations).map(([k, data]) => { + const r = Math.floor(Math.max(0, data.start + (data.wait ?? 0) * _1s - now) / _1s) * _1s; + if (k === 'onIdle') idleStarted = r <= 0; + return [k, { ...data, time: r, str: remainTime2Str(r) }]; + })); + if (optionBox.style.display !== 'none') Object.values(remain).forEach(r => gE(r.selector, 'all').forEach(ui => { ui.innerHTML = r.str })); + if (ui) appendEncounterUITitle('i', Object.values(remain).map(r => `${r.name}: ${r.str}`).join('\n')) + return idleStarted && Object.values(remain).every(r => !r.time); + }, _1s); + } catch (err) { console.error(err); }} + + async function asyncOnIdle() { try { + let option = getOption(true); + const beforeIdle = g('beforeIdle', time(0)); + displayCDRemain(); + $async.logSwitch(arguments); + await updateEncounter(false); + if (!onIsekaiEncounter && option.onIdleDelay) { + const delay = option.onIdleDelay * _1s; + until(async () => { + const remain = beforeIdle + delay - time(0); + if (remain <= 0) return true; + await waitPause(); + displayProcess(`${remainTime2Str(remain, true)}${UI.byLang('等待进入闲置', '等待進入閒置', 'Wait for enter idle')}`); + }, 250); + await pauseAsync(option.onIdleDelay * _1s); + } + const idleStart = g('idleStart', time(0)); + await waitPause(); + displayProcess(UI.byLang('人物套装检查', '人物套裝檢查', 'Persona/EquipSet check')); + if (onIsekaiEncounter) { + const persistent = await $ajax.fetch(window.location.href.replace('/isekai', '')); + if (!persistent || isInBattle($doc(persistent))) { + $async.logSwitch(arguments); + displayProcess(); + return; } - encounter.unshift({ href: url, time: now }); - setEncounter(encounter); + switchCurrent(); } else { - if (encounter.length) { - if (now - encounter[0]?.time > 0.5 * _1h) { // 延长最新一次的time, 避免因漏记录导致连续来回跳转 - encounter[0].time = now; - setEncounter(encounter); - } - for (let e of encounter) { - if (e.encountered) { - continue; - } - url = e.href; - break; - } - } - } + await restorePersonaAndEquipSet(); + } + displayProcess(); + option = getOption(true); + + const steps = [ + [{ step: 'proficiency', method: asyncSetProficiency, condition: true }], + [{ step: 'ability', method: asyncSetAbilityData, condition: true }], + [{ step: 'stamina', method: asyncSetStamina, condition: true }], + [{ step: 'item', method: asyncGetItems, condition: option.restoreStamina }, + { step: 'supply', method: checkSupply, condition: option.encounterSupply, check: true }], + [{ step: 'repair', method: asyncCheckRepair, condition: option.encounterRepair, check: true }], + [{ step: 'storage', method: asyncCheckEquStorage, condition: option.encounterEquStorage, check: true }], + ]; - if (!url) { - if (isEngage && !getValue('battle')) { - // 自动跳转,同时先刷新遭遇时间,延长下一次遭遇 - $ajax.openNoFetch(getValue('lastHref')); + const ready = { isChecked: () => ready.encounter && !steps.find(group => group.find(step => step.check && !ready[step.step]))}; + let battleStarted; + if (_server.isekai) { + displayProcess(UI.byLang('异世界遭遇', '異世界遭遇', 'Isekai encounter')); + await setReady('encounter'); + if (!ready.encounter) { + $async.logSwitch(arguments); + displayProcess(); + return; } - return; } - - // 减少因在恒定世界处于战斗中时打开eh触发了遭遇而导致的错失 - // 缓存当前链接,等战斗结束时再自动打开,下次打开链接时: - // 1. 若新的遭遇未出现,进入已缓存的战斗链接 - // 2. 若新的遭遇已出现,则前一次已超时失效错过,重新获取新的一次 - if (!isEngage) { // 战斗外,非自动跳转 - eventpane.style.cssText += 'color:red;' // 链接标红提醒 - } else if (getValue('battle')) { //战斗中 - eventpane.style.cssText += 'color:gray;' // 链接置灰提醒 - } else { // 战斗外,自动跳转 - $ajax.openNoFetch(`${href}/${url}`); + let done = -1; + displayProcess(`[${++done}/${steps.length}] ${UI.byLang('检查闲置', '檢查閒置', 'Idle check')}`); + await Promise.all([...steps.map(group => (async () => { try { + for (const step of group) { + await setReady(step.step, await step.method() || !step.check); + } + displayProcess(`[${++done}/${steps.length}] ${UI.byLang('检查闲置', '檢查閒置', 'Idle check')}`); + } catch (err) { console.error(err); }})()), onIsekaiEncounter ? undefined : updateArena()]); + + if (!onIsekaiEncounter) { + if (ready.isChecked() && option.idleArena && option.idleArenaValue) { + displayProcess(UI.byLang('竞技场更新', '競技場更新', 'Arena update')); + battleStarted = await startUpdateArena(idleStart); + } + if (!battleStarted) { + displayProcess(UI.byLang('切换世界', '切換世界', 'World Switching')); + autoSwitchIsekai(); + } + } + displayProcess(); + $async.logSwitch(arguments); + return ready.encounter; + + function displayProcess(info) { + document.title = document.title.replace(/\[AR\].*\[AR\]/, ''); + if (!info) return; + document.title = `[AR]${info}[AR]` + document.title; + } + + async function setReady(step, value) { try { + ready[step] = value; + if (ready.encounterUpdated) return; + $async.logSwitch(arguments); + const onEncounter = option.encounter || onIsekaiEncounter; + if (_server.persistent) { + if (onEncounter && steps.find(group => group.find(step => step.condition && !ready[step.step]))) return; + ready.encounterUpdated = true; + } + const encounterStarted = await updateEncounter(onEncounter); + battleStarted ||= encounterStarted; + ready.encounter ||= !encounterStarted; + ready.encounterUpdated ||= ready.encounter; + $async.logSwitch(arguments); + } catch (err) { console.error(err); }} + } catch (err) { console.error(err); }} + + function getTodayEncounter(encounter) { return encounter.filter(e => time(2, e.time) === time(2)); } + + function getLocalEncounter(encounter = []) { + encounter = [ ... getValue('encounter', true) ?? [], ... getLocal('encounter', true, true) ?? [], ...encounter ]; + const uniqued = []; + while (encounter.length) { + const item = encounter.pop(); + const existed = uniqued.find(x => x.time === item.time || x.url === item.url); + if (existed) { + if (existed.encountered !== item.encountered) { + existed.encountered ||= item.encountered; + } + continue; + } + uniqued.unshift(item); } + return getTodayEncounter(uniqued); } function setEncounter(encounter) { + encounter = getLocalEncounter(encounter); + setLocal('encounter', encounter, true); return g('encounter', setValue('encounter', encounter)); } function getEncounter() { - const getToday = (encounter) => encounter.filter(e => time(2, e.time) === time(2)); - const current = g('encounter') ?? []; - let encounter = getValue('encounter', true) ?? []; + const current = g().encounter ?? []; + let last = 0; + current.forEach(e=> { + if (e.encountered > last) last = e.encountered; + if (e.time > last) last = e.time; + }); + let encounter = ((time(0) - last) >= (_1h * 0.5)) ? getLocal('encounter', true, true) : current; + if (!encounter || !last) { + encounter = getValue('encounter', true) ?? []; + setEncounter(encounter); + } if (JSON.stringify(current) === JSON.stringify(encounter)) { - return getToday(encounter); + return getTodayEncounter(encounter); } let dict = {}; for (let e of current) { - dict[e.href ?? `newDawn`] = e; + dict[e.url ?? `newDawn`] = e; + } + // if is not latest version data (old versions) + if (!Array.isArray(encounter)) { + const last = encounter.lastTime; + const times = encounter.time + 1; + encounter = []; + for (const i of range(times)) { + encounter.unshift({ url: i === 0 ? undefined : i, time: last, encountered: i === 0 ? undefined : time(0) }); + } + setEncounter(encounter); } for (let e of encounter) { - const key = e.href ?? `newDawn`; + const key = e.url ?? `newDawn`; dict[key] ??= e; dict[key].time = Math.max(dict[key].time, e.time); dict[key].encountered = (e.encountered || dict[key].encountered) ? Math.max(dict[key].encountered ?? 0, e.encountered ?? 0) : undefined; } - return getToday(Object.values(dict)).sort((x, y) => x.time < y.time ? 1 : x.time > y.time ? -1 : 0); + return getTodayEncounter(Object.values(dict)).sortBy(x => -x.time); } - function quickSite() { // 快捷站点 - const quickSiteBar = gE('body').appendChild(cE('div')); - quickSiteBar.className = 'quickSiteBar'; - quickSiteBar.innerHTML = '<<贴吧Forums'; - if (g('option').quickSite) { - g('option').quickSite.forEach((site) => { - quickSiteBar.innerHTML = `${quickSiteBar.innerHTML}${(site.fav) ? `` : ''}${site.name}`; - }); - } - gE('.quickSiteBarToggle', quickSiteBar).onclick = function () { - const spans = gE('span', 'all', quickSiteBar); - for (let i = 1; i < spans.length; i++) { - spans[i].style.display = (this.textContent === '<<') ? 'none' : 'block'; - } - this.textContent = (this.textContent === '<<') ? '>>' : '<<'; - }; + function queryToPersistent(query) { + return onIsekaiEncounter ? `${window.location.href.includes('https') ? 'https://' : 'http://'}${window.location.href.includes('alt') ? 'alt.' : ''}hentaiverse.org/${query}` : query; } - function autoSwitchIsekai() { - if (!g('option').isekai) { - // 若不启用自动跳转 - return; - } - window.location.href = `${href.slice(0, href.indexOf('.org') + 4)}/${isIsekai ? '' : 'isekai/'}`; - } + async function asyncSetProficiency() { try { + await waitPause(); + $async.logSwitch(arguments); + const doc = $doc(await $ajax.insert(queryToPersistent('?s=Character'))); + const proficiency = {}; + gE('#stats_scrollable table:last-child tr', 'all', doc).forEach((tr) => { + const exec = tr.innerHTML.match(/(.*)<\/td>.*(.*)<\/td>/); + proficiency[exec[2]] = exec[1] * 1; + }); + localStorage.setItem(`hvAA-${_server.name}_proficiency`, JSON.stringify(proficiency)); + $async.logSwitch(arguments); + } catch (err) { console.error(err); }} async function asyncSetAbilityData() { try { - if(getValue('disabled')){ - await pauseAsync(_1s); - return await asyncSetAbilityData(); - } - logSwitchAsyncTask(arguments); - const html = await $ajax.fetch('?s=Character&ss=ab'); + await waitPause(); + $async.logSwitch(arguments); + const html = await $ajax.insert(queryToPersistent('?s=Character&ss=ab')); const doc = $doc(html); - let ability = {}; - await Promise.all(Array.from(gE('#ability_treelist>div>img', 'all', doc)).map(async img => { try { - const _ = img.getAttribute('onclick')?.match(/(\?s=(.*)tree=(.*))'/); - const [href, type] = _ ? [_[1], _[3]] : ['?s=Character&ss=ab&tree=general', 'general']; - switch(type){ - case 'deprecating1': - case 'deprecating2': - case 'elemental': - case 'forbidden': - case 'divine': - break; - default: - return; - } - const html = await $ajax.fetch(href); - const doc = $doc(html); - const slots = Array.from(gE('.ability_slotbox>div>div', 'all', doc)).forEach(slot => { - const id = slot.id.match(/_(\d*)/)[1]; - const parent = slot.parentNode.parentNode.parentNode; - ability[id] = { - name: gE('.fc2', parent).innerText, - type: type, - level: Array.from(gE('.aw1,.aw2,.aw3,.aw4,.aw5,.aw6,.aw7,.aw8,.aw9,.aw10', parent).children).map(div => div.style.cssText.indexOf('f.png') === -1 ? 0 : 1).reduce((x, y) => x + y), - } - }); - } catch (e) {console.error(e)}})); - setValue('ability', ability); - logSwitchAsyncTask(arguments); - } catch (e) {console.error(e)}} + const abd = { + // 'HP Tank': { id: 1101, unlock: [0, 25, 50, 75, 100, 120, 150, 200, 250, 300], level: 0 }, + // 'MP Tank': { id: 1102, unlock: [0, 30, 60, 90, 120, 160, 210, 260, 310, 350], level: 0 }, + // 'SP Tank': { id: 1103, unlock: [0, 40, 80, 120, 170, 220, 270, 330, 390, 450], level: 0 }, + // 'Better Health Pots': { id: 1104, unlock: [0, 100, 200, 300, 400], level: 0 }, + // 'Better Mana Pots': { id: 1105, unlock: [0, 80, 140, 220, 380], level: 0 }, + // 'Better Spirit Pots': { id: 1106, unlock: [0, 90, 160, 240, 400], level: 0 }, + // '1H Damage': { id: 2101, unlock: [0, 100, 200], level: 0 }, + // '1H Accuracy': { id: 2102, unlock: [50, 150], level: 0 }, + // '1H Block': { id: 2103, unlock: [250], level: 0 }, + // '2H Damage': { id: 2201, unlock: [0, 100, 200], level: 0 }, + // '2H Accuracy': { id: 2202, unlock: [50, 150], level: 0 }, + // '2H Parry': { id: 2203, unlock: [250], level: 0 }, + // 'DW Damage': { id: 2301, unlock: [0, 100, 200], level: 0 }, + // 'DW Accuracy': { id: 2302, unlock: [50, 150], level: 0 }, + // 'DW Crit': { id: 2303, unlock: [250], level: 0 }, + // 'Staff Spell Damage': { id: 2501, unlock: [0, 100, 200], level: 0 }, + // 'Staff Accuracy': { id: 2502, unlock: [50, 150], level: 0 }, + // 'Staff Damage': { id: 2503, unlock: [0], level: 0 }, + // 'Cloth Spellacc': { id: 3101, unlock: [120], level: 0 }, + // 'Cloth Spellcrit': { id: 3102, unlock: [0, 40, 90, 130, 190], level: 0 }, + // 'Cloth Castspeed': { id: 3103, unlock: [150, 250], level: 0 }, + // 'Cloth MP': { id: 3104, unlock: [0, 60, 110, 170, 230, 290, 350], level: 0 }, + // 'Light Acc': { id: 3201, unlock: [0], level: 0 }, + // 'Light Crit': { id: 3202, unlock: [0, 40, 90, 130, 190], level: 0 }, + // 'Light Speed': { id: 3203, unlock: [150, 250], level: 0 }, + // 'Light HP/MP': { id: 3204, unlock: [0, 60, 110, 170, 230, 290, 350], level: 0 }, + // 'Heavy Crush': { id: 3301, unlock: [0, 75, 150], level: 0 }, + // 'Heavy Prcg': { id: 3302, unlock: [0, 75, 150], level: 0 }, + // 'Heavy Slsh': { id: 3303, unlock: [0, 75, 150], level: 0 }, + // 'Heavy HP': { id: 3304, unlock: [0, 60, 110, 170, 230, 290, 350], level: 0 }, + 'Better Weaken': { id: 4201, unlock: [70, 100, 130, 190, 250], level: 0 }, + 'Faster Weaken': { id: 4202, unlock: [80, 165, 250], level: 0 }, + 'Better Imperil': { id: 4203, unlock: [130, 175, 230, 285, 330], level: 0 }, + 'Faster Imperil': { id: 4204, unlock: [140, 225, 310], level: 0 }, + 'Better Blind': { id: 4205, unlock: [110, 130, 160, 190, 220], level: 0 }, + 'Faster Blind': { id: 4206, unlock: [120, 215, 275], level: 0 }, + 'Mind Control': { id: 4207, unlock: [80, 130, 170], level: 0 }, + 'Better Silence': { id: 4211, unlock: [120, 170, 215], level: 0 }, + 'Better Immobilize': { id: 4212, unlock: [250, 295, 340, 370, 400], level: 0 }, + 'Better Slow': { id: 4213, unlock: [30, 50, 75, 105, 135], level: 0 }, + // 'Better Drain': { id: 4216, unlock: [20, 50, 90], level: 0 }, + // 'Faster Drain': { id: 4217, unlock: [30, 70, 110, 150, 200], level: 0 }, + // 'Ether Theft': { id: 4218, unlock: [150], level: 0 }, + // 'Spirit Theft': { id: 4219, unlock: [150], level: 0 }, + // 'Better Haste': { id: 4102, unlock: [60, 75, 90, 110, 130], level: 0 }, + // 'Better Shadow Veil': { id: 4103, unlock: [90, 105, 120, 135, 155], level: 0 }, + // 'Better Absorb': { id: 4104, unlock: [40, 60, 80], level: 0 }, + // 'Stronger Spirit': { id: 4105, unlock: [200, 220, 240, 265, 285], level: 0 }, + // 'Better Heartseeker': { id: 4106, unlock: [140, 185, 225, 265, 305, 345, 385], level: 0 }, + // 'Better Arcane Focus': { id: 4107, unlock: [175, 205, 245, 285, 325, 365, 405], level: 0 }, + // 'Better Regen': { id: 4108, unlock: [50, 70, 95, 145, 195, 245, 295, 375, 445, 500], level: 0 }, + // 'Better Cure': { id: 4109, unlock: [0, 35, 65], level: 0 }, + // 'Better Spark': { id: 4110, unlock: [100, 125, 150], level: 0 }, + // 'Better Protection': { id: 4101, unlock: [40, 55, 75, 95, 120], level: 0 }, + // 'Flame Spike Shield': { id: 4111, unlock: [10, 65, 140, 220, 300], level: 0 }, + // 'Frost Spike Shield': { id: 4112, unlock: [10, 65, 140, 220, 300], level: 0 }, + // 'Shock Spike Shield': { id: 4113, unlock: [10, 65, 140, 220, 300], level: 0 }, + // 'Storm Spike Shield': { id: 4114, unlock: [10, 65, 140, 220, 300], level: 0 }, + 'Conflagration': { id: 4301, unlock: [50, 100, 150, 200, 250, 300, 400], level: 0 }, + 'Cryomancy': { id: 4302, unlock: [50, 100, 150, 200, 250, 300, 400], level: 0 }, + 'Havoc': { id: 4303, unlock: [50, 100, 150, 200, 250, 300, 400], level: 0 }, + 'Tempest': { id: 4304, unlock: [50, 100, 150, 200, 250, 300, 400], level: 0 }, + // 'Sorcery': { id: 4305, unlock: [70, 140, 210, 280, 350], level: 0 }, + // 'Elementalism': { id: 4306, unlock: [85, 170, 255, 340, 425], level: 0 }, + // 'Archmage': { id: 4307, unlock: [90, 180, 270, 360, 450], level: 0 }, + 'Better Corruption': { id: 4401, unlock: [75, 150], level: 0 }, + 'Better Disintegrate': { id: 4402, unlock: [175, 250], level: 0 }, + 'Better Ragnarok': { id: 4403, unlock: [250, 325, 400], level: 0 }, + // 'Ripened Soul': { id: 4404, unlock: [150, 300, 450], level: 0 }, + // 'Dark Imperil': { id: 4405, unlock: [175, 225, 275, 325, 375], level: 0 }, + 'Better Smite': { id: 4501, unlock: [75, 150], level: 0 }, + 'Better Banish': { id: 4502, unlock: [175, 250], level: 0 }, + 'Better Paradise': { id: 4503, unlock: [250, 325, 400], level: 0 }, + // 'Soul Fire': { id: 4504, unlock: [150, 300, 450], level: 0 }, + // 'Holy Imperil': { id: 4505, unlock: [175, 225, 275, 325, 375], level: 0 }, + } + + const newAbility = {}; + gE('#ability_top div[onmouseover*="overability"]', 'all', doc).forEach((div) => { + const exec = div.getAttribute('onmouseover').match(/overability\(\d+, '([^']+)','.+?','(?:(Not Acquired|At Maximum)|Requires Level (\d+).+?)','(Not Acquired|At Maximum|Requires Level (\d+).+?)'/); + const name = exec[1]; + const ab = abd[name]; + if (!ab) return; + newAbility[ab.id] = exec[2] ? 0 : ab.unlock.indexOf(1 * exec[3]) + 1; + }); + setValue('ability', newAbility); + ability = Object.keys(newAbility).length ? newAbility : ability; + $async.logSwitch(arguments); + } catch (err) { console.error(err); }} - async function asyncSetEnergyDrinkHathperk() { try { - if (isIsekai || !g('option').restoreStamina) { + async function asyncSetStamina() { try { + await waitPause(); + $async.logSwitch(arguments); + const stamina = getValue('stamina', true) ?? { ratio: 1 }; + let [last, lastTime] = [stamina.current, stamina.time]; + [stamina.current, stamina.punish, stamina.perk] = await Promise.all([ + ... (await getCurrentStamina()), + (await (async () => { try { + let perk = stamina.perk; + if (perk && !Array.isArray(perk)) { + perk = Object.keys(perk).map(id => id * 1); + } + if (!perk?.length) { + perk = undefined; + } + if (_server.isekai || !g().option?.restoreStamina) { + return perk; + } + let currentID, html; + if (perk && perk[currentID = getCurrentUser() * 1]) { + return perk; + } + if (!(html = await $ajax.insert('https://e-hentai.org/hathperks.php'))) { + return perk; + } + const doc = $doc(html); + const perks = gE('.stuffbox>table>tbody>tr', 'all', doc); + if (perks && perks[25]?.innerHTML.includes('Obtained') && !(perk ??= []).includes(currentID)) { + perk.push(currentID); + } + return perk; + } catch (err) { console.error(err); }})())?.filter(p=>p) + ]); + if (!stamina.current) { + if (!getValue('stamina')) { + setValue('stamina', stamina); + } + $async.logSwitch(arguments); return; } - if(getValue('disabled')){ - await pauseAsync(_1s); - return await asyncSetEnergyDrinkHathperk(); - } - logSwitchAsyncTask(arguments); - const html = await $ajax.fetch('https://e-hentai.org/hathperks.php'); - if(!html) { + stamina.time = time(0); + if (!stamina.punish) { + [stamina.lastRatio, stamina.lastRatioRaw] = [stamina.ratio, stamina.ratioRaw]; + delete stamina.ratio; + delete stamina.ratioRaw; + } + if (stamina.ratio === 1 && (stamina.lastRatio === 1 || !stamina.lastRatio)) { + delete stamina.ratio; + delete stamina.lastRatio; + delete stamina.lastRatioRaw; + delete stamina.ratioRaw; + } + const lastCost = stamina.lastCost; + delete stamina.lastCost; + if (!lastCost || lastCost <= 0.06 ) { + setValue('stamina', stamina); + $async.logSwitch(arguments); return; } - const doc = $doc(html); - const perks = gE('.stuffbox>table>tbody>tr', 'all', doc); - if (!perks) { + last += Math.floor(stamina.time / _1h) - Math.floor(lastTime / _1h); + const delta = last - stamina.current; + if (!delta) { + setValue('stamina', stamina); + $async.logSwitch(arguments); return; } - setValue('staminaHathperk', perks[25].innerHTML.includes('Obtained')); - logSwitchAsyncTask(arguments); - } catch (e) {console.error(e)}} - - async function asyncSetStamina() { try { - if(getValue('disabled')){ - await pauseAsync(_1s); - return await asyncSetStamina(); + const ratio = stamina.punish ? Math.max(1, Math.round(delta / lastCost / 0.25) * 0.25) : 1; + if (stamina.ratio === ratio) { + setValue('stamina', stamina); + $async.logSwitch(arguments); + return; } - logSwitchAsyncTask(arguments); - const html = await $ajax.fetch(window.location.href); - setValue('staminaTime', Math.floor(time(0) / 1000 / 60 / 60)); - setValue('stamina', gE('#stamina_readout .fc4.far>div', $doc(html)).textContent.match(/\d+/)[0] * 1); - logSwitchAsyncTask(arguments); - } catch (e) {console.error(e)}} + [stamina.lastRatio, stamina.lastRatioRaw] = [stamina.ratio ?? 1, stamina.ratioRaw]; + [stamina.ratio, stamina.ratioRaw] = [ratio, `${delta} / ${Math.round(lastCost * 100) / 100} = ${delta / lastCost}`] + setValue('stamina', stamina); + console.log('stamina', stamina, '\n', last, '->', stamina.current, '=', lastCost, '*', ratio); + $async.logSwitch(arguments); + } catch (err) { console.error(err); }} async function asyncGetItems() { try { - if (!g('option').checkSupply && (isIsekai || !g('option').restoreStamina)) { + const option = getOption(true); + if (!option.checkSupply && (_server.isekai || !option.restoreStamina)) { return; } - if(getValue('disabled')){ - await pauseAsync(_1s); - return await asyncGetItems(); - } - logSwitchAsyncTask(arguments); - const html = await $ajax.fetch('?s=Character&ss=it'); + await waitPause(); + $async.logSwitch(arguments); + const html = await $ajax.insert(queryToPersistent('?s=Character&ss=it')); const items = {}; - for (let each of gE('.nosel.itemlist>tbody', $doc(html)).children) { + const doc = $doc(html); + if (isInBattle(doc)) { + $async.logSwitch(arguments); + g('items', null); + return; + } + for (let each of gE('.nosel.itemlist>tbody', doc).children) { const name = each.children[0].children[0].innerText; const id = each.children[0].children[0].getAttribute('id').split('_')[1]; const count = each.children[1].innerText; items[id] = [name, count]; } g('items', items); - logSwitchAsyncTask(arguments); - } catch (e) {console.error(e)}} + g('slotItems', Array.from(gE('[id*="item_"]', 'all', gE('#item_slots', doc))).map(slot => slot.id.match(/item_(\d+)/)[1])); + $async.logSwitch(arguments); + } catch (err) { console.error(err); }} + + function popupFailedCheck(title, popupText, ...log) { + const option = getOption(); + if (title) document.title = `[${title}!${onIsekaiEncounter?'p':''}]` + document.title; + if (popupText) popup(`${onIsekaiEncounter ? UI.byLang('主世界', '主世界', '[Persistent]') ?? '[Persistent]' : ''}${UI.byLang(popupText) ?? popupText[2]}`); + if (log?.length) console.log(`${onIsekaiEncounter ? '[Persistent]' : ''}`, ...log); + } - async function asyncCheckSupply() { try { - if (!g('option').checkSupply) { - return true; - } - if(getValue('disabled')){ - await pauseAsync(_1s); - return await asyncCheckSupply(); + function checkSupply(extra) { + const option = getOption(true); + if (extra && !option[`checkSupply${extra}`]) return true; + extra = { + GF: { + name: { 0: '压榨界', 1: '壓榨界', 2: 'Grindfest' }, + thresholdList: option.checkItemGF, + checkList: option.isCheckGF, + percentage: option.checkSupplyWarnGF + }, + IW:{ + name: { 0: '道具界', 1: '道具界', 2: 'Itemworld' }, + thresholdList: option.checkItemIW, + checkList: option.isCheckIW, + percentage: option.checkSupplyWarnIW + }, + }[extra]; + if (!option.checkSupply) return true; + const items = g().items; + if (!items) return false; + const slotItems = g().slotItems; + const slotedCheckList = option.checkSupplySlotted ? option.isCheckSlotted : undefined; + const name = extra?.name ?? ''; + const thresholdList = extra?.checkItem ?? option.checkItem; + const checkList = extra?.isCheck ?? option.isCheck; + const percentage = extra?.checkSupplyWarn ?? option.checkSupplyWarn; + const unslotted = [], needs = [], warns = []; + const lang = option.lang; + for (let id in slotedCheckList) { + let [name, count] = items[id] ?? []; + if (!slotItems.includes(id)) unslotted.push(`\n${UI.byLang(itemMap[id]) ?? name}`); } - logSwitchAsyncTask(arguments); - const items = g('items'); - const thresholdList = g('option').checkItem; - const checkList = g('option').isCheck; - const needs = []; for (let id in checkList) { - const item = items[id]; - if (!item) { - continue; - } - const [name, count] = item; + let [name, count] = items[id] ?? []; + name = UI.byLang(itemMap[id]) ?? name; + count ??= 0; const threshold = thresholdList[id] ?? 0; - if ((count ?? 0) >= threshold) { + if (count < threshold) { + needs.push(`\n${name}(${count}<${threshold})`); continue; } - needs.push(`\n${name}(${count}<${threshold})`); - } - if (needs.length) { - console.log(`Needs supply:${needs}`); - document.title = `[C!]` + document.title; + const warnThreshold = threshold * percentage / 100; + if (count < warnThreshold) { + warns.push(`\n${name}(${count}<${warnThreshold}(${threshold}*${percentage}%))`); + } + } + extra = (UI.byLang(extra?.name) ?? ''); + extra = extra ? extra + ' ' : ''; + if (unslotted.length) { + popupFailedCheck(`C`, [ + `消耗品未装备:\n${unslotted}`, + `消耗品未裝備:\n${unslotted}`, + `Consumables not slotted:\n${unslotted}`, + ], `Unslotted items:${unslotted}`); + } else if (needs.length) { + popupFailedCheck(`C${extra ? '!' : ''}`, [ + `消耗品${extra ? `(${extra}额外配置)` : ''}不足:\n${needs}`, + `消耗品${extra ? `(${extra}額外配置)` : ''}不足:\n${needs}`, + `Failed supply check${extra ? ` for ${extra}extra` : ''}:\n${needs}`, + ], `${extra}Needs supply:${needs}`); + } else if (warns.length) { + popupFailedCheck(`C${extra ? '!' : ''}`, [ + `消耗品${extra ? `(${extra}额外配置)` : ''} < ${percentage}%:\n${warns}`, + `消耗品${extra ? `(${extra}額外配置)`: ''} < ${percentage}%:\n${warns}`, + `Supplys ${extra ? ` for ${extra}extra` : ''} < ${percentage}%:\n${warns}`, + ], `${extra}Warn supply:${warns}`); + } + return !needs.length && !unslotted.length; + } + + async function asyncCheckRepair(extra) { try { + const option = getOption(true); + if (extra && !option[`repairValue${extra}`]) return true; + if (!option.repair) { + return true; } - logSwitchAsyncTask(arguments); - return !needs.length; - } catch (e) {console.error(e)} return false; } + await waitPause(); + $async.logSwitch(arguments); + let eqps; + extra = { + GF: { + name: { 0: '压榨界', 1: '壓榨界', 2: 'Grindfest' }, + threshold: option.repairValueGF, + repairCharm: option.repairCharmGF, + }, + IW:{ + name: { 0: '道具界', 1: '道具界', 2: 'Itemworld' }, + threshold: option.repairValueIW, + repairCharm: option.repairCharmIW, + }, + }[extra]; - async function asyncCheckRepair() { try { - if (!g('option').repair) { + const threshold = extra?.repairValue ?? option.repairValue; + const repairCharm = extra?.repairCharm || option.repairCharm; + if (threshold === undefined || threshold < 0) { // skip because default repair has been checked before idleArena>GF + $async.logSwitch(arguments); return true; } - if(getValue('disabled')){ - await pauseAsync(_1s); - return await asyncCheckRepair(); - } - logSwitchAsyncTask(arguments); - const doc = $doc(await $ajax.fetch('?s=Forge&ss=re')); - const json = JSON.parse((await $ajax.fetch(gE('#mainpane>script[src]', doc).src)).match(/{.*}/)[0]); - const eqps = (await Promise.all(Array.from(gE('.eqp>[id]', 'all', doc)).map(async eqp => { try { - const id = eqp.id.match(/\d+/)[0]; - const condition = 1 * json[id].d.match(/Condition: \d+ \/ \d+ \((\d+)%\)/)[1]; - if (condition > g('option').repairValue) { - return; - } - return gE('.messagebox_error', $doc(await $ajax.fetch(`?s=Forge&ss=re`, `select_item=${id}`)))?.innerText ? undefined : id; - } catch (e) {console.error(e)}}))).filter(e => e); + const url = queryToPersistent(`?s=Bazaar&ss=am&screen=repair&filter=equipped`); + let [doc, equiped] = Array.from(await Promise.all([$ajax.insert(url), $ajax.insert(queryToPersistent(`?s=Character&ss=eq`))])).map($doc); + if (isInBattle(doc)) { + $async.logSwitch(arguments); + return undefined; + } + const lang = option.lang; + const slotMap = { + 1: ['主手', '主手', 'Main Hand'], + 2: ['副手', '副手', 'Off Hand'], + 13: ['头盔', '頭盔', 'Helmet'], + 11: ['身体', '身體', 'Body'], + 14: ['手部', '手部', 'Hands'], + 12: ['腿部', '腿部', 'Legs'], + 15: ['脚部', '腳部', 'Feet'] + } + let emptySlot = Array.from(gE('.eqb:not(.eqdisabled)', 'all', equiped)).map(slot => { return { id: slot.getAttribute('onclick').match(/equip_slot=(\d+)/)[1], empty: gE('.eqempty', slot) }; }); + emptySlot = emptySlot.filter(slot => slot.empty && !option.equipCheckSkip?.[slot.id]).map(slot => UI.byLang(slotMap[slot.id * 1])); + const token = gE('#equipform>input[name="postoken"]', doc).value; + const getCondition = eqp => 1 * gE('td:last-child', eqp).textContent.replace('%', ''); + let [material, materialNames] = doc.body.innerHTML.match(/const eqitems=(\{.*\});/)[1].split(/; const itemdata=/).map(JSON.parse); + equiped = Array.from(gE('[onmouseover*="equips.set("]', 'all', equiped)).map(eq => eq.getAttribute('onmouseover').match(/equips.set\((\d+),/)[1]); + const selector = '#equiplist>table>tbody>tr:not(.eqselall):not(.eqtplabel)'; + eqps = await Promise.all(Array.from(gE(selector, 'all', doc)).map(async eqp => { try { + const id = gE('input', eqp).value; + if (!equiped.includes(id)) return; + let condition = getCondition(eqp); + const needRepairCharm = repairCharm && Object.keys(material[id].m).some(m => materialNames[m].n.includes('Charm')); + if (condition > threshold && !needRepairCharm) return; + let after = $doc(await $ajax.insert(url, `&eqids[]=${id}&postoken=${token}&replace_charms=on`)); + let input = gE(`#e${id}`, after); + if (input && !needRepairCharm) { + condition = getCondition(input.closest(selector)); + if (condition > threshold) return; + after = $doc(await $ajax.insert(url, `&eqids[]=${id}&postoken=${token}&replace_charms=off`)); + } + return gE(`#e${id}`, after) ? gE('.lc', eqp).textContent : undefined; + } catch (err) { console.error(err); }})); + eqps = eqps.filter(e => e); + if (emptySlot.length) { + popupFailedCheck(`R`, [ + `缺少装备:\n${emptySlot.join('\n ')}`, + `缺少裝備:\n${emptySlot.join('\n ')}`, + `Empty equip slots:\n${emptySlot.join('\n ')}`, + ], `Empty equip slots:\n`, emptySlot.join('\n ')); + } + extra = UI.byLang(extra?.name); if (eqps.length) { - console.log('eqps need repair: ', eqps); - document.title = `[R!]` + document.title; + popupFailedCheck(`R`, [ + `${extra}装备需要修理:\n${eqps.join('\n ')}`, + `${extra}裝備需要修理:\n${eqps.join('\n ')}`, + `${extra}Equips need repair:\n${eqps.join('\n ')}`, + ], `${extra}Equips need repair:\n`, eqps.join('\n ')); } - logSwitchAsyncTask(arguments); + $async.logSwitch(arguments); return !eqps.length; - } catch (e) {console.error(e)}; return false; } - - function checkStamina(low, cost) { - let stamina = getValue('stamina'); - const lastTime = getValue('staminaTime'); - let timeNow = Math.floor(time(0) / _1h); - stamina += lastTime ? timeNow - lastTime : 0; - const stmNR = stamina + 24 - (timeNow % 24); - cost ??= 0; - const stmNRChecked = !cost || stmNR - cost >= g('option').staminaLowWithReNat; - console.log('stamina:', stamina,'\nstamina with nature recover:', stmNR, '\nnext arena stamina cost: ', cost.toString()); - if (stamina - cost >= (low ?? g('option').staminaLow) && stmNRChecked) { - return 1; + } catch (err) { console.error(err); }; return false; } + + async function asyncUpdateEquipModifyList() { try { + $async.logSwitch(arguments); + const option = getOption(); + const filters = ['weapon_1handed', 'weapon_2handed', 'weapon_staff', 'shield', 'armor_cloth', 'armor_light', 'armor_heavy']; + const equips = []; + const hvv = hvVersion.upto(Version('091.e')) + for (const filter of filters) { + const url = queryToPersistent(`?s=Bazaar&ss=am&screen=modify&filter=${filter}`); + const doc = $doc(await $ajax.insert(url)); + if (isInBattle(doc)) return; + const eqps = await Promise.all(Array.from(gE('#equiplist>table>tbody>tr:not(.eqselall):not(.eqtplabel)', 'all', doc)).map(async eqp => { try { + const id = gE('input', eqp).value; + const levels = gE('td:last-child', eqp).innerHTML; + if (!levels) return; + const [level, world, max] = levels.split(' / ').map(x => x * 1); + const name = gE('td:first-child', eqp).innerText; + const quality = name.match(/Fair|Average|Superior|Exquisite|Magnificent|Legendary|Peerless/)[0]; + const rounds = hvv ? range(5, 35) : + (()=> { switch (quality) { + case 'Fair': case 'Average': case 'Superior': case 'Exquisite': + return [...range(5, 10), ...repeat(10, 5)]; + case 'Magnificent': case 'Legendary': case 'Peerless': + return [...range(5, 20), ...repeat(20, 5), ...repeat(25, 5), ...repeat(30, 5)]; + default: + return; + }})(); + if (!rounds) return; + equips.push({name, id, filter, level, world, max, round: rounds[world]}); + } catch (err) { console.error(err); }})); + } + $async.logSwitch(arguments); + return equips; + } catch (err) { console.error(err); }} + + async function asyncUpdatePersona(doc) { try { + $async.logSwitch(arguments); + doc ??= $doc(await $ajax.insert('?s=Character&ss=ch')); + if (isInBattle(doc)) return; + const raws = gE('[name="persona_set"]>option', 'all', doc); + if (!raws?.length) return; + const personas = Object.fromEntries([...raws].map(option => [option.value, { name: option.innerHTML, selected: option.selected }])); + $async.logSwitch(arguments); + return personas; + } catch (err) { console.error(err); }} + + async function asyncUpdateEquipSet(doc) { try { + $async.logSwitch(arguments); + doc ??= $doc(await $ajax.insert('?s=Character&ss=eq')); + if (isInBattle(doc)) return; + const raws = gE('#eqsl img', 'all', doc); + if (!raws?.length) return; + const equipSets = Object.fromEntries([...raws].map(img => { + const [match, id, on] = img.src.match(/set(\d)_(on|off)/); + return [ id, on==='on' ]; + })); + $async.logSwitch(arguments); + return equipSets; + } catch (err) { console.error(err); }} + + async function asyncCheckEquStorage() { try { + const option = getOption(true); + if (!option.equStorage) { + return true; } - let checked = 0; - if (!stmNRChecked) { - checked = -1; + await waitPause(); + $async.logSwitch(arguments); + const url = queryToPersistent(`?s=Bazaar&ss=am`); + const doc = $doc(await $ajax.insert(url)); + if (isInBattle(doc)) { + $async.logSwitch(arguments); + return false; } - if (isIsekai || !g('option').restoreStamina) { - return checked; + const exec = /Inventory Capacity:<\/td>(\d+)(?: \+ (\d+))?<\/td>\/<\/td>(\d+)<\/td>/.exec(doc.body.innerHTML); + const count = parseInt(exec[1]); + parseInt(exec[2] || 0); + const checked = count <= option.equStorageValue; + if (!checked) { + popupFailedCheck(`E`, [ + `装备库存过多: ${count} / ${option.equStorageValue}`, + `裝備庫存過多: ${count} / ${option.equStorageValue}`, + `Equips storage upto threshold: ${count} / ${option.equStorageValue}`, + ], `Equips storage upto threshold: ${count} / ${option.equStorageValue}`); + } + $async.logSwitch(arguments); + return checked; + } catch (err) { console.error(err); }; return false; } + + async function checkBattleReady(method, condition = {}) { + await waitPause(); + if (condition.checkEncounter) { + const encounter = getEncounter(); + if (encounter[0]?.url && !encounter[0]?.encountered) { + console.log(getEncounter()); + return; + } } - const items = g('items'); - if (!items) { - return checked; + const option = getOption(true); + const stamina = getValue('stamina', true); + const [low, lowNR, cost, ratio] = [condition.staminaLow ?? option.staminaLow, option.staminaLowWithReNat ?? 0, Math.round((condition.staminaCost ?? 0) * 100) / 100, stamina.punish ? stamina.ratio ?? 1 : 1] + const checked = await checkStamina(low, cost); + const [staminaChecked, stmNR] = [checked.checked, checked.stmNR]; + const [neat, neatNR] = [stamina.current-low, stmNR-lowNR]; + console.log( + `${onIsekaiEncounter ? '[Persistent]' : ''}stamina check succeed:`, staminaChecked === 1, ...staminaChecked === -1 ? ['with nature recover', lowNR, 'stmNR:', stmNR, '(', ...neatNR >= 0 ? ['+', neatNR] : ['-', -neatNR], ')'] : [], + '\nlow:', low, ...cost ? ['cost:', cost, ...stamina.punish ? ['*', ratio, '=', Math.round(cost * ratio * 10000) / 10000] : [], 'current:', stamina.current, '(', neat >= 0 ? '+' : '-', neat, ')'] : [], + '\nstamina:', stamina, + ); + if (staminaChecked === 1) { // succeed + document.title = document.title.replace(`[S!${onIsekaiEncounter?'p':''}]`, ''); + return true; } - const recover = items[11402] ? 5 : items[11401] ? getValue('staminaHathperk') ? 20 : 10 : 0; - if (recover && stamina <= (100 - recover)) { - $ajax.open(window.location.href, 'recover=stamina'); - return checked; + if (staminaChecked === 0) { // failed currently + const now = time(0); + setTimeout(method, Math.floor(now / _1h + 1) * _1h - now); + // popup('Failed stamina check for now.'); + if (!document.title.includes(`[S!${onIsekaiEncounter?'p':''}]`)) { + document.title = `[S!${onIsekaiEncounter?'p':''}]` + document.title; + } + } else { // case -1: // failed with nature recover + popupFailedCheck(`S!`, [ + `当日精力不足(含自然恢复)`, + `當日精力不足(含自然恢復)`, + `Failed stamina check with nature recover.`, + ]); } } - async function updateEncounter(engage, isInBattle) { try { - if(getValue('disabled')){ - await pauseAsync(_1s); - return await updateEncounter(engage, isInBattle); + async function getCurrentStamina() { try { + // await waitPause(); + $async.logSwitch(arguments); + const doc = $doc(await $ajax.insert(onIsekaiEncounter ? window.location.href.replace(/\/isekai/, '') : window.location.href)); + if (isInBattle(doc)) { + $async.logSwitch(arguments); + return [ undefined, undefined ]; + } + const current = gE('#stamina_readout .fc4.far>div', doc).textContent.match(/\d+/)[0] * 1; + const punish = !!gE('#stamina_readout .fc4.far', doc).parentNode.title; + $async.logSwitch(arguments); + return [current, punish ? punish : undefined]; + } catch (err) { console.error(err); }} + + async function checkStamina(low, cost) { + // await waitPause(); + $async.logSwitch(arguments); + const stamina = getValue('stamina', true); + const option = getOption(true); + let now = time(0); + let hours = Math.floor(now / _1h); + let [current, punish] = await until(getCurrentStamina, _1m); + const stmNR = current + 24 - (hours % 24); + cost ??= 0; + if (punish && option.staminaRatio) { + cost *= stamina.ratio + } + const stmNRChecked = !cost || stmNR - cost >= option.staminaLowWithReNat; + const result = { checked: stmNRChecked ? (current - cost >= (low ?? option.staminaLow)) ? 1 : 0 : -1, stmNR: stmNR }; + $async.logSwitch(arguments); + if (result.checked === 1 || _server.isekai || !option.restoreStamina) return result; + const items = g().items; + $async.logSwitch(arguments); + if (!items) return result; + const recoverItems = { 11401: true, 11402: false } + const isPerk = stamina.perk?.includes(getCurrentUser()); + for (let id in recoverItems) { + if (!items[id]) continue; + const recover = recoverItems[id] ? isPerk ? 20 : 10 : 5; + if (current + recover >= 100) continue; // check if overflow by (20 or 10) -> (5) + const recovered = gE('#stamina_readout .fc4.far>div', $doc(await $ajax.insert(window.location.href, 'recover=stamina'))).textContent.match(/\d+/)[0] * 1; + goto(); + break; } - const encounter = getEncounter(); - const encountered = encounter.filter(e => e.encountered && e.href); - const count = encounter.filter(e => e.href).length; + $async.logSwitch(arguments); + return result; + } - const now = time(0); + async function updateEncounter(engage) { try { + const MAX = 24; + const option = getOption(true); + if (!option.encounter && !option.encounterDisplay) { + console.log("skip encounter check"); + return false; + } + $async.logSwitch(arguments); + const encounter = getEncounter(); + const count = encounter.filter(e => e.url).length; const last = encounter[0]?.time ?? getValue('lastEH', true) ?? 0; // 上次遭遇 或 上次打开EH 或 0 - let cd; - if (encountered.length >= 24) { - cd = Math.floor(encounter[0].time / _1d + 1) * _1d - now; - } else if (!last) { - cd = 0; - } else { - cd = _1h / 2 + last - now; + let now = time(0); + let cd = getCD(); + function setUITitle(ui) { + appendEncounterUITitle('e', `${time(3, last)}\n${UI.l('遭遇次数', '遭遇次數','Encounter Time')}: ${count}`); } - cd = Math.max(0, cd); const ui = gE('.encounterUI') ?? (() => { - const ui = gE('body').appendChild(cE('a')); + const ui = (gE('.hvAAPauseUI') ?? gE('body')).appendChild(cE('a')); ui.className = 'encounterUI'; - ui.title = `${time(3, last)}\nEncounter Time: ${count}`; - if (!isInBattle) { + setUITitle(ui); + if (!gE('#battle_main')) { ui.href = 'https://e-hentai.org/news.php?encounter'; } return ui; })(); - - const missed = count - encountered.length; - if (count === 24) { + const waitCD = option.encounterWaitCD * _1s; + const missed = count - encounter.filter(e => e.encountered && e.url).length; + if (count === MAX) { ui.style.cssText += 'color:orange!important;'; - } else if (!cd) { + } else if (cd <= waitCD) { ui.style.cssText += 'color:red!important;'; } else { ui.style.cssText += 'color:unset!important;'; } - ui.innerHTML = `${formatTime(cd).slice(0, 2).map(cdi => cdi.toString().padStart(2, '0')).join(`:`)}[${encounter.length ? (count >= 24 ? `☯` : count) : `✪`}${missed ? `-${missed}` : ``}]`; - if (engage && !cd) { - onEncounter(); - return true; + let uiTime = timeStr(cd, 2, option.encounterQuickCheck); + if (option.encounterQuickCheck && cd >= _1h && (Math.floor(cd / _1s) % 2)) { + uiTime = uiTime.replace(':', '.'); + } + const newHTML = `${uiTime}[${encounter.length ? (count >= MAX ? `☯` : count) : `✪`}${missed ? `-${missed}` : ``}]`;; + if (newHTML !== ui.innerHTML) { + ui.innerHTML = newHTML; + } + setUITitle(ui); + if (document.title.includes(titlePause())) { + document.title = ui.innerHTML + titlePause(); } - let interval = cd > _1h ? _1m : (!g('option').encounterQuickCheck || cd > _1m) ? _1s : 80; - interval = (g('option').encounterQuickCheck && cd > _1m) ? (interval - cd % interval) / 4 : interval; // 让倒计时显示更平滑 + if (engage && !getValue('disabled')) { + if (cd <= 0) { + $async.logSwitch(arguments); + return await onEncounter(); + } + if (cd < 30 * _1m && encounter[0]?.url && !encounter[0].encountered) { + $ajax.openNoFetch(encounter[0].url); + $async.logSwitch(arguments); + return true; + } + } + let interval = (!option.encounterQuickCheck && cd > _1h) ? _1m : (!option.encounterQuickCheck || cd > _1m) ? _1s : 80; + interval = (option.encounterQuickCheck && cd > _1m) ? (interval - cd % interval) / 4 : interval; // 让倒计时显示更平滑 setTimeout(() => updateEncounter(engage), interval); - } catch (e) {console.error(e)}} + $async.logSwitch(arguments); + return engage && cd <= waitCD; + + function getCD() { + now = time(0); + let cd = 0; + if (encounter.filter(e => e.url && (e.encountered || (time(0) - e.time >= 30 * _1m))).length >= MAX) { + cd = Math.floor(encounter[0].time / _1d + 1) * _1d - now; + } else if (last) { + cd = _1h / 2 + last - now; + } + return Math.max(0, cd); + } + } catch (err) { console.error(err); }} + + async function onChangeEquipSet(lastKey, target, list, toFetchParam, reload) { try { + let changed = false; + let last = getValue(lastKey); + let current = list ? Object.keys(list).find(p => list[p].selected) * 1 : undefined; + target ??= last; + if ([undefined, current].includes(target)) return; + if (!last) setValue(lastKey, current); + await $ajax.fetch(...toFetchParam(target)); + if (reload) await updateItemWorldList(true); + return true; + } catch (err) { console.error(err); }} + + async function switchEquipSet(persona, equipSet, personas, equipSets) { try { + $async.logSwitch(arguments); + if (personas !== undefined) g('personas', personas); + personas = g().personas; + if (equipSets !== undefined) g('equipSets', equipSets); + equipSets = g().equipSets; + if ([personas, equipSets].includes(undefined)) { + const { e, p, s } = getValue('itemWorldDatas', true) ?? {}; + g('personas', personas ??= p); + g('equipSets', equipSets ??= s); + } + + let changed = await onChangeEquipSet('lastPersona', persona, personas, id => [`?s=Character&ss=ch`, `persona_set=${id}`], true); + changed = await onChangeEquipSet('lastEquipSet', equipSet, equipSets, id => [`?s=Character&ss=eq`, `equip_set=${id}`], true) || changed; + $async.logSwitch(arguments); + return changed; + } catch (err) { console.error(err); }} + + function getArenaEquipSet(id) { + const option = getOption(); + if (!option.changeEquipSet) return; + + const onGetArenaEquipSet = function (id) { + if (!option.enableEquipSet?.[id]) return; + const persona = option.switchPersona?.[id]; + const equipSet = option.switchEquipSet?.[id]; + return { persona, equipSet }; + } + + let target = onGetArenaEquipSet(id); + let parent = !isNaN(+id) ? onGetArenaEquipSet(id >= 105 ? 'rb' : 'ar') : undefined; + let defaultSet = onGetArenaEquipSet('default'); + let persona = target?.persona !== undefined ? target?.persona : parent?.persona !== undefined ? parent?.persona : defaultSet?.persona !== undefined ? defaultSet?.persona : undefined; + let equipSet = target?.equipSet !== undefined ? target?.equipSet : parent?.equipSet !== undefined ? parent?.equipSet : defaultSet?.equipSet !== undefined ? defaultSet?.equipSet : undefined; + persona = persona === -1 ? undefined : persona; + equipSet = equipSet === -1 ? undefined : equipSet; + if (persona !== undefined || equipSet !== undefined) return { persona, equipSet }; + } + + function isSwitchEquipSet(target) { + const last = { persona: getValue('lastPersona'), equipSet: getValue('lastEquipSet') }; + const { persona, equipSet } = target ?? {}; + return { + persona: persona !== undefined && (last.persona !== persona) ? persona : undefined, + equipSet: equipSet !== undefined && (last.equipSet !== equipSet) ? equipSet : undefined + } + } - function onEncounter() { - if (getValue('disabled') || getValue('battle') || !checkBattleReady(onEncounter, { staminaLow: g('option').staminaEncounter })) { + async function changeArenaEquipSet(id) { try { + const target = getArenaEquipSet(id); + const { persona, equipSet} = isSwitchEquipSet(target); + console.log(`changeArenaEquipSet for arena [${id}]:`, target, '=>', { ... persona ? { persona } : {}, ... equipSet ? { equipSet } : {} }); + return await ([persona, equipSet].every(c => c === undefined) ? restorePersonaAndEquipSet() : switchEquipSet(persona, equipSet)); + } catch (err) { console.error(err); }} + + async function onEncounter() { try { + const option = getOption(true); + if (getValue('disabled')) return; + if (_server.isekai) { + onIsekaiEncounter = true; + const engaged = await asyncOnIdle(); + onIsekaiEncounter = false; + return engaged; + } + if (await changeArenaEquipSet('ba') && !(await asyncCheckRepair())) { + await restorePersonaAndEquipSet(); + return; + } + $async.logSwitchStrict('updateEncounter', true); + // persistent in battle + if (isInBattle(await $ajax.insert(window.location.href.replace(/\/isekai/, '')))) { + $async.logSwitchStrict('updateEncounter', false); + return; + } + // url check + await until( // perhaps network connect not available + async () => await $ajax.insert(window.location.href) && (!option.checkURLBeforeNewRound || await $ajax.insert(option.checkURLBeforeNewRound)), + option.checkURLBeforeNewRoundRetry + ); + // stamina + if (!await checkBattleReady(onEncounter, { staminaLow: option.staminaEncounter })) { + $async.logSwitchStrict('updateEncounter', false); return; } + g('encounterStart', time(0)); + await pauseAsync(option.encounterDelay * _1s); setEncounter(getEncounter()); // 离开页面前保存 - if(!window.top.location.href.endsWith(`?s=Battle`)){ - setValue('lastHref', window.top.location.href); + if (!window.top.location.href.endsWith(`?s=Battle`)) { + setValue('beforeEncounter', setValue('lastUrl', window.top.location.href)); } $ajax.openNoFetch('https://e-hentai.org/news.php?encounter'); - } + $async.logSwitchStrict('updateEncounter', false); + return true; + } catch (err) { console.error(err); }} - async function startUpdateArena(idleStart, startIdleArena=true) { try { + async function startUpdateArena(idleStart) { try { + $async.logSwitchStrict('startUpdateArena', true); const now = time(0); - if (!idleStart) { - await updateArena(); - } - let timeout = g('option').idleArenaTime * _1s; - if (idleStart) { - timeout -= time(0) - idleStart; - } - if(startIdleArena){ - setTimeout(idleArena, timeout); - } + if (!idleStart) await updateArena(); // new day + let timeout = getOption().idleArenaTime * _1s; + if (idleStart) timeout -= time(0) - idleStart; + if (timeout > 0) await pauseAsync(timeout); + const started = await idleArena(); const last = getValue('arena', true)?.date ?? now; - setTimeout(startUpdateArena, Math.max(0, Math.floor(last / _1d + 1) * _1d - now)); - } catch (e) {console.error(e)}} + const nextDay = Math.max(0, Math.floor(last / _1d + 1) * _1d - now); + if (nextDay > 0) { + setTimeout(startUpdateArena, nextDay); // next day + } else { + started ||= await startUpdateArena(); + } + $async.logSwitchStrict('startUpdateArena', false); + return started; + } catch (err) { console.error(err); }} async function updateArena(forceUpdateToken = false) { try { - if(getValue('disabled')){ - await pauseAsync(_1s); - return await updateArena(forceUpdateToken); - } + await waitPause(); + $async.logSwitch(arguments); let arena = getValue('arena', true) ?? {}; const isToday = arena.date && time(2, arena.date) === time(2); if (forceUpdateToken || !isToday || !arena.isOptionUpdated) { - arena.token = {}; - arena.sites ??= [ - '?s=Battle&ss=gr', - '?s=Battle&ss=ar', - '?s=Battle&ss=ar&page=2', - '?s=Battle&ss=rb' - ] - await Promise.all(arena.sites.map(async site => { try { - const doc = $doc(await $ajax.fetch(site)); - if (site === '?s=Battle&ss=gr') { - arena.token.gr = gE('img[src*="startgrindfest.png"]', doc).getAttribute('onclick').match(/init_battle\(1, '(.*?)'\)/)[1]; - return; - } - gE('img[src*="startchallenge.png"]', 'all', doc).forEach((_) => { - const temp = _.getAttribute('onclick').match(/init_battle\((\d+),\d+,'(.*?)'\)/); - arena.token[temp[1]] = temp[2]; + arena.enabled = []; + await Promise.all(['gr', 'ar', 'rb'].map(s => (async site => { try { + const doc = $doc(await $ajax.insert(`?s=Battle&ss=${site}`)); + getStartBattleButtons(doc, site).forEach(btn => { + if (!btn.enabled) return; + if (btn.cleared) { + arena.enabled.push(btn.id); + return; + } + const index = arena.enabled.indexOf(btn.id); + if (index !== -1) arena.enabled.splice(index, 1); }); - } catch (e) {console.error(e)}})); + } catch (err) { console.error(err); }})(s))); } - if(!isToday){ + + const option = getOption(); + if (!isToday) { arena.date = time(0); - arena.gr = g('option').idleArenaGrTime; + arena.gr = option.idleArenaGrTime; + arena.tw = undefined; arena.arrayDone = []; } - if (!isToday || !arena.isOptionUpdated) { - arena.array = g('option').idleArenaValue.split(',') ?? []; - arena.array.reverse(); - } + arena.arrayDone = arena.arrayDone.filter(id => id && (id === 'gr' || !arena.enabled?.includes(id.toString()))); + delete arena.equip; + $async.logSwitch(arguments); return setValue('arena', arena); - } catch (e) {console.error(e)}} + } catch (err) { console.error(err); }} - function checkBattleReady(method, condition = {}) { - if (getValue('disabled')) { - setTimeout(method, _1s); - return; - } - if (condition.checkEncounter && getEncounter()[0]?.href && !getEncounter()[0]?.encountered) { - Debug.log(getEncounter()); - return; - } - const staminaChecked = checkStamina(condition.staminaLow, condition.staminaCost); - console.log("staminaChecked", condition.staminaLow, condition.staminaCost, staminaChecked); - if(staminaChecked === 1){ // succeed - return true; - } - if(staminaChecked === 0){ // failed until today ends - setTimeout(method, Math.floor(time(0) / _1h + 1) * _1h - time(0)); - document.title = `[S!!]` + document.title; - } else { // case -1: // failed with nature recover - document.title = `[S!]` + document.title; - } + function titlePause() { + return UI.byLang('HVAA暂停中', 'HVAA', 'HVAA Paused'); } async function idleArena() { try { // 闲置竞技场 + let id; let arena = getValue('arena', true); - console.log('arena:', getValue('arena', true)); - if (arena.array.length === 0) { - setTimeout(autoSwitchIsekai, (g('option').isekaiTime * (Math.random() * 20 + 90) / 100) * _1s); - return; + const option = getOption(); + const array = splitOrders(option.idleArenaValue).map(String); + if (array.length === 0) { + autoSwitchIsekai(); + return false; + } + $async.logSwitch(arguments); + if (!arena.enabled?.length) { + arena.enabled = (await updateArena(true)).enabled; + setValue('arena', arena); } - logSwitchAsyncTask(arguments); - const array = [...arena.array]; - let id; - const RBundone = []; while (array.length > 0) { - id = array.pop() * 1; - id = isNaN(id) ? 'gr' : id; - if(arena.arrayDone?.includes(id)){ + id = array.shift(); + if (arena.arrayDone?.includes(id) || option.arLevelDisable?.[id]) { id = undefined; continue; } - if (id in arena.token) { - break; + + if (id === 'tw') { + id = undefined; + if (_server.persistent) continue; + const doc = $doc(await $ajax.insert('?s=Battle&ss=tw')); + let [_, floor, rounds, attempts, maxAttempts, clears, max] = gE('#towerstart', doc).innerText.match(/Current Floor: (\d+) \((\d+) Rounds\)\n\t.*\n\tDaily Attempts: (\d+) \/ (\d+)\n\tDaily Clears: (\d+) \/ (\d+)/).map(x => x * 1); + arena.tw = attempts; + setValue('arena', arena); + if (clears >= max || attempts >= maxAttempts) { + arena.arrayDone.push('tw'); + setValue('arena', arena); + continue; + } + if (attempts >= option.idleArenaTwTime) continue; + if (floor >= option.idleArenaTwMax) continue; + if (await gotoBattle('tw', rounds)) { + $async.logSwitch(arguments); + return true; + } + } + if (id === 'iw') { + id = undefined; + const iw = await idleItemWorld(); + if (!iw) continue; + $async.logSwitch(arguments); + return true; } + if (arena.enabled.includes(id)) break; if (id >= 105) { - arena.token = (await updateArena(true)).token; - if (id in arena.token) { - break; + arena.enabled = (await updateArena(true)).enabled; + setValue('arena', arena); + if (arena.enabled.includes(id)) break; + } + id = undefined; + } + if (!id) { + console.log('No Arena Id Available', arena); + await restorePersonaAndEquipSet(); + $async.logSwitch(arguments); + return false; + } + const done = await gotoBattle(id); + $async.logSwitch(arguments); + return done; + } catch (err) { console.error(err); }} + + async function idleItemWorld(arena) { try { + const option = getOption(); + if (!option.idleItemWorld) return; + $async.logSwitch(arguments); + await updateItemWorldList(); + const { equips, personas, equipSets } = getValue('itemWorldDatas', true) ?? {}; + if (!equips || !personas || !equipSets) { + $async.logSwitch(arguments); + return; + } + const list = option.enableItemWorld ? Object.keys(option.enableItemWorld).filter(id => option.enableItemWorld[id]) : undefined; + if (!list?.length) { + $async.logSwitch(arguments); + return; + } + list.sortBy(id => option.ItemWorldOrder?.[id] ?? 0); + + let doc, errorMsg; + for (const eid of list) { + const equip = equips.find(eqp => eqp.id === eid); + if (equip.world >= equip.max || equip.world >= option.levelItemWorld?.[eid]) continue; + doc = $doc(await $ajax.insert(`?s=Bazaar&ss=am&screen=modify&eqids[]=${eid}`)); + if (errorMsg = gE('.messagebox_error', doc)) { + console.log(equip, errorMsg.innerText); + continue; + } + let title = gE('#equpgrade button', 'all', doc)[1].getAttribute('title'); + if (title?.match(/You need \d+ more World Seeds to spawn this Item World./)) continue; + + if (switchEquipSet(option.itemWorldPersona?.[eid], option.itemWorldEquipSet?.[eid], personas, equipSets)) { + doc = $doc(await $ajax.insert(`?s=Bazaar&ss=am&screen=modify&eqids[]=${eid}`)); + if (errorMsg = gE('.messagebox_error', doc)) { + console.log(equip, errorMsg.innerText); + continue; } + title = gE('#equpgrade button', 'all', doc)[1].getAttribute('title'); + } + + if (title?.match(/You cannot enter the item world of a currently equipped item./)) { + console.log('Idle Item World: Skip currently equiped', eid, equip); + continue; + } + + if (await gotoBattle('iw', equip.round, equip)) { + $async.logSwitch(arguments); + return true; } - id = undefined; } - if (!id) { - setValue('arena', arena); - logSwitchAsyncTask(arguments); - return; + $async.logSwitch(arguments); + } catch (err) { console.error(err); }} + + async function gotoBattle(id, rounds, equip) { try { + $async.logSwitch(arguments); + const option = getOption(); + if (await changeArenaEquipSet(id) && !(await asyncCheckRepair())) { + await restorePersonaAndEquipSet(); + $async.logSwitch(arguments); + return false; } + let stamina = getValue('stamina', true); let staminaCost = { 1: 2, 3: 4, 5: 6, 8: 8, 9: 10, 11: 12, 12: 15, 13: 20, 15: 25, 16: 30, @@ -2598,71 +6355,109 @@ try { 24: 60, 26: 65, 27: 70, 28: 75, 29: 80, 32: 85, 33: 90, 34: 95, 35: 100, 105: 1, 106: 1, 107: 1, 108: 1, 109: 1, 110: 1, 111: 1, 112: 1, - gr: arena.gr - } - let stamina = getValue('stamina'); - const lastTime = getValue('staminaTime'); - let timeNow = Math.floor(time(0) / 1000 / 60 / 60); - stamina += lastTime ? timeNow - lastTime : 0; - for (let key in staminaCost) { - staminaCost[key] *= (isIsekai ? 2 : 1) * (stamina >= 60 ? 0.03 : 0.02) - } - staminaCost.gr += 1 - - let href, cost; - let token = arena.token[id]; - const key = id; - if (key === 'gr') { + gr: 0 + }; + [stamina.current, stamina.punish] = await getCurrentStamina(); + stamina.time = time(0); + rounds ??= staminaCost[id]; + const cost = rounds * (_server.isekai ? 2 : 1) * (stamina.current >= 60 ? 0.03 : 0.02); + + const arena = getValue('arena', true); + let query = id; + if (!['gr', 'iw', 'tw'].includes(id)) { + query = id >= 105 ? 'rb' : 'ar'; + } else if (id === 'gr') { if (arena.gr <= 0) { - setValue('arena', arena); - idleArena(); arena.arrayDone.push('gr'); - return; + setValue('arena', arena); + $async.logSwitch(arguments); + return await idleArena(); } - arena.gr--; - href = 'gr'; - key = 1; - cost = staminaCost.gr; - } else if (key >= 105) { - href = 'rb'; - } else if (key >= 19) { - href = 'ar&page=2'; - } else { - href = 'ar'; + } else if (id === 'iw') { + query = `iw&filter=${equip.filter}`; } - cost ??= staminaCost[key]; - if (!checkBattleReady(idleArena, { staminaCost: cost, checkEncounter: true })) { - logSwitchAsyncTask(arguments); - return; + query = `?s=Battle&ss=${query}`; + + let extra = { gr: 'GF', iw: 'IW', tw: 'TW' }[id]; + if (extra && ((!checkSupply(extra)) || !await asyncCheckRepair(extra))) { + console.log('Check Extra Battle Ready Failed in supply/repair', 'id:', id, `eid:${equip?.id}`, equip, arena); + $async.logSwitch(arguments); + return false; } - document.title = _alert(-1, '闲置竞技场开始', '閒置競技場開始', 'Idle Arena start'); - if(key !== 'gr'){ - arena.arrayDone.push(key); + extra = { gr: 'GrindFest', iw: 'ItemWorld', tw: 'Tower' }[id]; + if (!await checkBattleReady(idleArena, { staminaCost: cost, checkEncounter: option.encounter, staminaLow: extra ? option[`stamina${extra}`] : undefined })) { + console.log('Check Battle Ready Failed', 'id:', id, `eid:${equip?.id}`, equip, arena); + $async.logSwitch(arguments); + return false; } + await waitPause(); + + if (option.checkURLBeforeNewRound) { + await until(async () => { + if (await $ajax.insert(option.checkURLBeforeNewRound)) { + return true; + } + console.log('Failed arena wating url check before new round, retrying...\n', option.checkURLBeforeNewRound); + }, option.checkURLBeforeNewRoundRetry); + } + + console.log('Arena Start', equip ? `e${equip.id} (${equip.world} => ${equip.world + 1}) / ${equip.max}\n${JSON.stringify(equip)}` : id); + let result, debug = true; + const onTryFailed = (type, err) => debug ? console.log('Fetch', type, 'failed, retrying...', err||'', JSON.stringify(result)) : undefined; + await until(async () => { try { + result = { query, retryCD: option.checkURLBeforeNewRoundRetry, tried: (result?.tried ?? -1)+1 }; + if (!(result.queryFetch = await $ajax.insert(query))) return onTryFailed('postoken'); + if (!(result.queryDoc = $doc(result.queryFetch))) return onTryFailed('postoken doc'); + delete result.queryFetch; + if (isInBattle(result.queryDoc)) return true; // early return caused by battle direct enter; + if (!(result.postoken = gE('input[name="postoken"]', result.queryDoc)?.value)) return onTryFailed('postoken value'); + result.data = `${equip ? `eqids%5B%5D=${equip.id}` : `initid=${['gr', 'tw'].includes(id) ? 1 : id}`}&postoken=${result.postoken}`; + delete result.queryDoc; + delete result.postoken; + if (!(result.fetchResult = await $ajax.insert(query, result.data))) return onTryFailed('battle'); + if (!(result.fetchDoc = $doc(result.fetchResult))) return onTryFailed('battle doc'); + if (!(result.inBattle = !!isInBattle(result.fetchDoc))) return onTryFailed('in Battle'); + return true; + } catch (err) { onTryFailed('result erroring', err); }}, option.checkURLBeforeNewRoundRetry); + + if (id === 'gr') arena.gr--; + else if (!['tw', 'iw'].includes(id)) arena.arrayDone.push(id); + else if (equip) arena.equip = { data: equip }; setValue('arena', arena); - $ajax.open(`?s=Battle&ss=${href}`, `initid=${String(key)}&inittoken=${token}`); - logSwitchAsyncTask(arguments); - } catch (e) {console.error(e)}} + stamina.lastCost = id === 'gr' ? undefined : cost; + setValue('stamina', stamina); + const alt = option.altBattleFirst && await $ajax.insert(window.location.href.replace('://hentaiverse.org', '://alt.hentaiverse.org')); + console.log('Arena Fetch Done.', 'altBattleFirst:', option.altBattleFirst, 'Arena goto', alt ? 'alt' : '', arena); + alt ? gotoAlt(true) : goto(); + $async.logSwitch(arguments); + return true; + } catch (err) { console.error(err); }} + + function setBattleSkillParam(id, params) { + g().battle.skill = { id, [id]: 1, ...params }; + } // 战斗中// - function onBattle() { // 主程序 + function onBattleRound() { // 主程序 + if (!gE('#battle_main')) return; + lastResponsive = time(0); let battle = getValue('battle', true); if (!battle || !battle.roundAll) { // 修复因多个页面/世界同时读写造成缓存数据异常的情况 - battle = JSON.parse(JSON.stringify(g('battle'))); + battle = JSON.parse(JSON.stringify(g().battle)); battle.monsterStatus = battle.monsterStatus.map(ms => { return { order: ms.order, hp: ms.hp } - }) - battle.monsterStatus.sort(objArrSort('order')); + }); + battle.monsterStatus.sortBy(x => x.order); }; - Debug.log('onBattle', `\n`, JSON.stringify(battle, null, 4)); + $debug.log('onBattle', `\n`, battle); //人物状态 if (gE('#vbh')) { - g('hp', gE('#vbh>div>img').offsetWidth / 500 * 100); - g('mp', gE('#vbm>div>img').offsetWidth / 210 * 100); - g('sp', gE('#vbs>div>img').offsetWidth / 210 * 100); + g('hp', gE('#vbh>div>img').offsetWidth / 496 * 100); + g('mp', gE('#vbm>div>img').offsetWidth / 207 * 100); + g('sp', gE('#vbs>div>img').offsetWidth / 207 * 100); g('oc', gE('#vcp>div>div') ? (gE('#vcp>div>div', 'all').length - gE('#vcp>div>div#vcr', 'all').length) * 25 : 0); } else { g('hp', gE('#dvbh>div>img').offsetWidth / 418 * 100); @@ -2673,20 +6468,12 @@ try { // 战斗战况 if (!gE('.hvAALog')) { - const div = gE('#hvAABox2').appendChild(cE('div')); + const div = onBattleBox().appendChild(cE('div')); div.className = 'hvAALog'; } - const status = [ - '物理物理Physical', - 'Fire', - 'Cold', - 'Elec', - 'Wind', - 'Divine', - 'Forbidden', - ]; + function getBattleTypeDisplay(isTitle) { - const battleInfoList = { + const battleInfoList = getBattleTypeDisplay.prototype.battleInfoList ??= { 'gr': { name: ['压榨', '壓榨', 'Grindfest'], title: 'GF', @@ -2732,7 +6519,7 @@ try { title: 'RB', list: [ ['九死一树', '九死一樹', 'Triple Trio and the Tree', 250, 'Yggdrasil'], - ['飞天意面怪', '飛行義大利麵怪物', 'Flying Spaghetti Monster', 200], + ['飞天意面怪', '飛天義麵怪', 'Flying Spaghetti Monster', 200], ['隐形粉红独角兽', '隱形粉紅獨角獸', 'Invisible Pink Unicorn', 150], ['现实生活', '現實生活', 'Real Life', 100], ['长门有希', '長門有希', 'Yuki Nagato', 75], @@ -2759,17 +6546,24 @@ try { ['噩梦×4', '噩夢×4', 'Nightmare×4', 14], ['困难×2', '困難×2', 'Hard×2', 7], ['普通×1', '普通×1', 'Normal×1', 1], - ], + ].map(arr => arr.map(s => isNaN(+s) ? `
      ${s}` : s)), condition: (bt) => bt[3] && bt[3] <= battle.tower, content: (_) => battle.tower, - end: battle.tower > 40 ? `+${(battle.tower - 40) * 5}%DMG&HP` : '', + end: `${battle.tower > 40 ? `+${(battle.tower - 40) * 5}%DMG&HP` : ''}
      `, } } const type = battle.roundType; let subtype, title; - const monsterNames = Array.from(gE('div.btm3>div>div', 'all')).map(monster => monster.innerHTML); - const lang = g('lang') * 1; + const monsterNames = Array.from(gE(`${monsterStateKeys.name}>div>div`, 'all')).map(monster => monster.innerHTML); + const lang = g().lang * 1; const info = battleInfoList[type]; + const arena = getValue('arena', true); + const equip = arena?.equip; + battle.postoken ??= arena.postoken; + if (equip && type !== 'iw') { + delete arena.equip; + setValue('arena', arena); + } switch (type) { case 'ar': case 'rb': @@ -2780,91 +6574,145 @@ try { continue; } title = `${info.title}${info.content(sub)}`; - if (!sub[lang]) { + if (!UI.byLang(sub)) { break; } - subtype = `${sub[lang] ? `
      ${sub[lang]}` : ``}${info.end ? `
      ${info.end}` : ``}`; + subtype = `${UI.byLang(sub) ? `
      ${UI.byLang(sub)}` : ``}${info.end ? `
      ${info.end}` : ``}`; break; } break; case 'iw': + title = `${info.title}`; + if (equip) { + if (Object.keys(equip.tokens ?? {}).map(k => equip.tokens[k] === battle[k]).some(s => !s)) { + // 有旧token,移除 + delete arena.equip; + } else { + // 没有token或是当前token,正常显示 + equip.tokens = { token: battle.token, postoken: battle.postoken }; + title += `${equip.data.world + 1}/${equip.data.max}`; + subtype = `
      [${equip.data.id}]${equip.data.name}
      `; + } + setValue('arena', arena); + } + break; case 'gr': title = `${info.title}`; break; default: break; } - return isTitle ? title : `${(info?.name ?? ['未知', '未知', 'Unknown'])[lang]}:[${title}]${subtype ?? ''}`; + return isTitle ? title : `${UI.byLang(info?.name ?? ['未知', '未知', 'Unknown'])}:[${title}]${subtype ?? ''}`; } const currentTurn = (battle.turn ?? 0) + 1; gE('.hvAALog').innerHTML = [ - `攻击模式攻擊模式Attack Mode: ${status[g('attackStatus')]}`, - `${isIsekai ? '异世界異世界Isekai' : '恒定世界恆定世界Persistent'}`, // 战役模式显示 + `${UI.l('攻击模式', '攻擊模式', 'Attack Mode')}: ${UI.attackStatusType[g().attackStatus]}`, + `${(_server.isekai || onIsekaiEncounter) ? UI.l('异世界', '異世界', 'Isekai') : UI.l('恒定世界', '恆定世界', 'Persistent')}`, // 战役模式显示 `${getBattleTypeDisplay()}`, // 战役模式显示 `R${battle.roundNow}/${battle.roundAll}:T${currentTurn}`, - `TPS: ${g('runSpeed')}`, - `敌人敌人Monsters: ${g('monsterAlive')}/${g('monsterAll')}`, - ].join(`
      `); + `TPS: ${g().runSpeed}`, + `${UI.l('敌人', '敵人', 'Monsters')}: ${g().monsterAlive}/${g().monsterAll}`, + ].join(`
      `).replace(`
      `, ``); if (!battle.roundAll) { pauseChange(); - Debug.shiftLog(); + $debug.shiftLog(); } - document.title = `${getBattleTypeDisplay(true)}:R${battle.roundNow}/${battle.roundAll}:T${currentTurn}@${g('runSpeed')}tps,${g('monsterAlive')}/${g('monsterAll')}`; + const option = getOption(); + document.title = `${currentTurn % 2 ? option.frequencySign1 ?? '' : option.frequencySign2 ?? ''}${getBattleTypeDisplay(true)}:R${battle.roundNow}/${battle.roundAll}:T${currentTurn}@${g().runSpeed}tps,${g().monsterAlive}/${g().monsterAll}`; setValue('battle', battle); - if (!battle.monsterStatus || battle.monsterStatus.length !== g('monsterAll')) { + if (!battle.monsterStatus || battle.monsterStatus.length !== g().monsterAll) { fixMonsterStatus(); } countMonsterHP(); displayMonsterWeight(); displayPlayStatePercentage(); - if (getValue('disabled')) { // 如果禁用 - document.title = _alert(-1, 'hvAutoAttack暂停中', 'hvAutoAttack暫停中', 'hvAutoAttack Paused'); - gE('#hvAABox2>button').innerHTML = '继续繼續Continue'; + document.title = titlePause(); + const pauseChange = gE('#hvAABox2>button.pauseChange'); + if (!pauseChange) return; + pauseChange.innerHTML = UI.button.continue(); return; } battle = getValue('battle', true); - g('battle').turn = currentTurn; - battle.turn = currentTurn; + battle.turn = g().battle.turn = currentTurn; setValue('battle', battle); - killBug(); // 解决 HentaiVerse 可能出现的 bug - - if (g('option').autoFlee && checkCondition(g('option').fleeCondition)) { + setBattleSkillParam(1001, { flee: 1, skill: 'flee' }); + if (option.autoFlee && checkCondition(option.fleeCondition)) { + if (option.fleeAlarm) setAlarm('Flee'); gE('1001').click(); - SetExitBattleTimeout('Flee'); + setExitBattleTimeout('Flee'); return; } - var taskList = { + const taskList = { + 'Cure': () => autoRecover(true), 'Pause': autoPause, - 'Rec': autoRecover, - 'Def': autoDefend, + 'SSDisable': () => autoSS(true), + 'Rec': () => autoRecover(false), 'Scroll': useScroll, + 'Infus': useInfusions, + 'Def': autoDefend, 'Channel': useChannelSkill, 'Buff': useBuffSkill, - 'Infus': useInfusions, 'Debuff': useDeSkill, 'Focus': autoFocus, - 'SS': autoSS, + 'SS': () => autoSS(false), 'Skill': autoSkill, 'Atk': attack, }; - const names = g('option').battleOrderName?.split(',') ?? []; - for (let i = 0; i < names.length; i++) { - if(taskList[names[i]]()){ + const names = option.battleOrderDefaultOnly ? [] : splitOrders(option.battleOrderName); + if (option.debugCheckCondition) { + checkCondition(option.debugCondition); + } + for (const i of range(names)) { + if (taskList[names[i]]()) { + onStepInDone(); return; } delete taskList[names[i]]; } for (let name in taskList) { if (taskList[name]()) { + onStepInDone(); return; } } } + function matchBuffImg(buff) { + return buff?.getAttribute('onmouseover').match(/\(\s*'(?:[^']*?\(x(\d+)\)[^']*?|[^']*?)'\s*,\s*'.*'\s*,\s*(\d+|'.*?')\s*\)/); + } + + function getBuffStackFromImg(buff) { + return (matchBuffImg(buff)?.[1] ?? 1) * 1; + } + + function getBuffTurnFromImg(buff) { + let duration = matchBuffImg(buff)?.[2]; + switch (true) { + case !duration: + return 0; + case ['autocast', 'permanent', '-'].includes(duration.replaceAll(/'/g, '')): + return Infinity; + case duration.replaceAll(/'/g, '') === 'decaying': + { + duration = 0; + const decay = 1 - unsafeWindow.battle.set_infopane_effect.toString().match(/Decays by (\d+)% per turn/)[1]/100; + let stacks = getBuffStackFromImg(buff); + while((stacks = Math.floor(stacks * decay)) > 0) { + duration++; + } + return duration; + } + default: + duration = duration * 1; + if (isNaN(duration)) console.warn('NaN duration:', buff, duration); + return duration; + } + } + function getMonsterID(s) { if (s.order !== undefined) { return (s.order + 1) % 10; @@ -2872,123 +6720,186 @@ try { return (s + 1) % 10; // case is order } + function getMonster(id) { + return gE(`#mkey_${id % 10}`); + } + + function getBuff(buff, id) { + if (buff?.match(`^{.*}$`)) { + for (const b of buff.replace(/[\{\}\s]/g, '').split(',')) { + const found = getBuff(b, id); + if (found) return found; + } + return undefined; + } + if (id === undefined) { + return gE(`#pane_effects>img[src*="/${buff}"]`) ?? gE(`#pane_effects>img[src*="_${buff}"]`); + } + return gE(`${monsterStateKeys.buffs}>img[src*="/${buff}"]`, getMonster(id)) ?? gE(`${monsterStateKeys.buffs}>img[src*="_${buff}"]`, getMonster(id)); + } + + function isOn(id) { // 是否可以施放技能/使用物品 + if (id * 1 > 10000) { // 使用物品 + return gE(`.bti3>div[onmouseover*="(${id})"]`); + } // 施放技能 + return gE(id) && (gE(id).style.opacity * 1 !== 0.5); + } + /** - * 按照技能范围,获取包含原目标且范围内最终权重(finweight)之和最低的范围的中心目标 - * @param {int} id id from g('battle').monsterStatus.sort(objArrSort('finWeight')); - * @param {int} range radius, 0 for single-target and all-targets, 1 for treble-targets, ..., n for (2n+1) targets - * @param {(target) => bool} excludeCondition target with id - * @returns - */ - function getRangeCenterID(target, range = undefined, isWeaponAttack = false, excludeCondition = undefined) { - if (!range) { - return getMonsterID(target); - } - const centralExtraWeight = -1 * Math.log10(1 + (isWeaponAttack ? (g('option').centralExtraRatio / 100) ?? 0 : 0)); +* 按照技能范围,获取包含原目标且范围内最终权重(finweight)之和最低的范围的中心目标 +* @param {int} id id from g().battle.monsterStatus.sortBy(x => x.finWeight); +* @param {int} rangeSize radius, 0 for single-target and all-targets, 1 for treble-targets, ..., n for (2n+1) targets +* @param {(target) => number} excludeWeightRatio target with id +* @returns +*/ + function getRangeCenter(target, rangeSize, isWeaponAttack, excludeWeight, forceUseIndex) { + let msTemp = JSON.parse(JSON.stringify(g().battle.monsterStatus)); + msTemp.sortBy(x => x.order); + let minWeight = Number.MAX_SAFE_INTEGER; + // 0. 范围大于等于全体时,直接释放全体 + if (!rangeSize || rangeSize >= msTemp.length) { + return { id: getMonsterID(target), weight: minWeight }; + } + const option = getOption(); + const centralExtraWeight = -1 * Math.log10(1 + (isWeaponAttack ? option.centralExtraRatio / 100 : 0)); let order = target.order; let newOrder = order; // sort by order to fix id - let msTemp = JSON.parse(JSON.stringify(g('battle').monsterStatus)); - msTemp.sort(objArrSort('order')); - let unreachableWeight = g('option').unreachableWeight; - let minRank; - for (let i = order - range; i <= order + range; i++) { - if (i < 0 || i >= msTemp.length || msTemp[i].isDead) { - continue; // 无法选中 - } - let rank = 0; - for (let j = i - range; j <= (i + range); j++) { - let cew = j === i ? centralExtraWeight : 0; // cew <= 0, 增加未命中权重,降低命中权重 - let mon = msTemp[j]; - if (j < 0 || j >= msTemp.length // 超出范围 - || mon.isDead // 死亡目标 - || (excludeCondition && excludeCondition(mon))) { // 特殊排除判定 - rank += unreachableWeight - cew; + let unreachableWeight = resolveRPNFormula(option.unreachableWeight, target); + // 1. 以选中目标为中心,优先向上 + // 2. 超过顶部则向下找 + // 3. 死亡、超过底下的将被溢出抛弃 + const up = Math.floor(rangeSize / 2); + const down = rangeSize - up - 1; + const top = order < rangeSize ? 0 : Math.max(order - down, 0); + const bottom = Math.min(order + up, msTemp.length - 1); + for (const center of range(top, bottom + 1)) { + if (msTemp[center].isDead) continue; + let weight = 0; + let overflowed = center + Math.max(up - center, 0); + for (const inRange of range(overflowed - up, overflowed + down + 1)) { + let cew = inRange === center ? centralExtraWeight : 0; // cew <= 0, 增加未命中权重,降低命中权重 + let mon = msTemp[inRange]; + if (inRange < 0 || inRange >= msTemp.length || mon.isDead) { // 超出范围 或 死亡目标 + weight += unreachableWeight; continue; } - rank += mon.finWeight + cew; // 中心目标会受到副手及冲击攻击时,相当于有效生命值降低 + weight += resolveSkillExtraWeight(target); + if (excludeWeight) { + weight += excludeWeight(mon); + } + weight += cew; // 中心目标会受到副手及冲击攻击时,相当于有效生命值降低 + weight += forceUseIndex ? -1 : mon.finWeight; // 强制使用顺序而非权重时,全部使用统一的权重而非怪物状态 } - if (rank < minRank) { - newOrder = i; + if (weight < minWeight) { + newOrder = center; + minWeight = weight; + break; } } - return getMonsterID(newOrder); + return { id: getMonsterID(newOrder), weight: minWeight }; } function autoPause() { - if (g('option').autoPause && checkCondition(g('option').pauseCondition)) { + const option = getOption(); + let battle = getValue('battle', true); + if ( + battle.paused?.round !== battle.roundNow + || battle.paused?.token !== battle.token + || battle.paused?.postoken !== battle.postoken + ) { + battle.paused = { + count: 0, + round: battle.roundNow, + token: battle.token, + postoken: battle.postoken ??= (getValue('arena', true) ?? {}).postoken + }; + setValue('battle', battle); + } + if (option.autoPause && checkCondition(option.pauseCondition)) { + if (!getValue('stepIn')) { + battle = getValue('battle', true); + battle.paused.turn = battle.turn; + battle.paused.count++; + setValue('battle', battle); + } pauseChange(); + if (option.pauseAlarm) setAlarm('Pause'); return true; } return false; } function autoDefend() { - if (g('option').defend && checkCondition(g('option').defendCondition)) { + const option = getOption(); + setBattleSkillParam('defend'); + if (option.defend && checkCondition(option.defendCondition)) { + updateSkillOTOS('defend'); gE('#ckey_defend').click(); return true; } return false; } - function pauseChange() { // 暂停状态更改 - if (getValue('disabled')) { - if (gE('.pauseChange')) { - gE('.pauseChange').innerHTML = '暂停暫停Pause'; - } - document.title = getValue('disabled'); - delValue(0); - if (!gE('#navbar')) { // in battle - onBattle(); - } - } else { - if (gE('.pauseChange')) { - gE('.pauseChange').innerHTML = '继续繼續Continue'; - } - setValue('disabled', document.title); - document.title = _alert(-1, 'hvAutoAttack暂停中', 'hvAutoAttack暫停中', 'hvAutoAttack Paused'); - } - } - - function SetExitBattleTimeout(alarm){ + function setExitBattleTimeout(alarm) { + lastResponsive = Infinity; + g('battleExit', true); setAlarm(alarm); - if(alarm === 'SkipDefeated') return; - delValue(1); - if(g('option').ExitBattleWaitTime) { - setTimeout(() => { - $ajax.open(getValue('lastHref')); - }, g('option').ExitBattleWaitTime * _1s); + const option = getOption(); + if (alarm === 'Defeat' && !option.autoSkipDefeated) { return; } - $ajax.open(getValue('lastHref')); + delValue(1); + setTimeoutOrExecute(() => backFromBattle(), option.ExitBattleWaitTime * _1s); } - function reloader() { - let obj; let a; let cost; + async function checkResponsive() { + const option = getOption(); const battleUnresponsive = { - 'Alert': { Method: setAlarm }, - 'Reload': { Method: goto }, - 'Alt': { Method: gotoAlt } + 'Alert': { method: () => (setAlarm('BattleUnresponsive') || true) }, + 'Reload': { method: goto }, + 'Alt': { method: gotoAlt } + } + let min = Infinity; + for (let t in battleUnresponsive) { + if (!option.battleUnresponsive?.[t] || !battleUnresponsive[t]) { + delete battleUnresponsive[t]; + continue; + } + battleUnresponsive[t].time = Math.max(1, option.battleUnresponsiveTime?.[t] ?? 1) * _1s; + min = Math.min(min, battleUnresponsive[t].time); } - function clearBattleUnresponsive(){ - Object.keys(battleUnresponsive).forEach(t=>clearTimeout(battleUnresponsive[t].Timeout)); + if (!Object.keys(battleUnresponsive).length) return; + let isBreak; + while (true) { + await waitPause(); + const waited = new Date() - lastResponsive; + for (let t in battleUnresponsive) { + if (battleUnresponsive[t].time > waited) continue; + isBreak ||= battleUnresponsive[t].method(); + } + if (g().battleExit || isBreak) break; + await pauseAsync(min - waited); } + } + + function reloader() { + let obj, a, cost; const eventStart = cE('a'); eventStart.id = 'eventStart'; eventStart.onclick = function () { + const option = getOption(); a = unsafeWindow.info; - for(let t in g('option').battleUnresponsive) { - if (g('option').battleUnresponsive[t]) { - battleUnresponsive[t].Timeout = setTimeout(battleUnresponsive[t].Method, Math.max(1, g('option').battleUnresponsiveTime[t]) * _1s); - } - } - if (g('option').recordUsage) { + if (option.recordUsage) { obj = { mode: a.mode, }; if (a.mode === 'items') { - obj.item = gE(`#pane_item div[id^="ikey"][onclick*="skill('${a.skill}')"]`).textContent; + obj.itemName = gE(`#pane_item div[id^="ikey"][onclick*="skill('${a.skill}')"]`).textContent; + obj.item = gE(`#pane_item div[id^="ikey"][onclick*="skill('${a.skill}')"]`).getAttribute('onmouseover').match(/(\d+)/)[1]; } else if (a.mode === 'magic') { - obj.magic = gE(a.skill).textContent; + obj.magic = a.skill; + obj.magicName = gE(a.skill).textContent; cost = gE(a.skill).getAttribute('onmouseover').match(/\('.*', '.*', '.*', (\d+), (\d+), \d+\)/); obj.mp = cost[1] * 1; obj.oc = cost[2] * 1; @@ -2996,197 +6907,618 @@ try { } }; gE('body').appendChild(eventStart); + const eventEnd = cE('a'); eventEnd.id = 'eventEnd'; eventEnd.onclick = function () { + + const option = getOption(); const timeNow = time(0); - g('runSpeed', (1000 / (timeNow - g('timeNow'))).toFixed(2)); + g('runSpeed', (_1s / (timeNow - g().timeNow)).toFixed(2)); g('timeNow', timeNow); const monsterDead = gE('img[src*="nbardead"]', 'all').length; - g('monsterAlive', g('monsterAll') - monsterDead); - const bossDead = gE('div.btm1[style*="opacity"] div.btm2[style*="background"]', 'all').length; - g('bossAlive', g('bossAll') - bossDead); + g('monsterAlive', g().monsterAll - monsterDead); + const bossDead = gE(`${monsterStateKeys.obj}[style*="opacity"] ${monsterStateKeys.lv}[style*="background"]`, 'all').length; + g('bossAlive', g().bossAll - bossDead); const battleLog = gE('#textlog>tbody>tr>td', 'all'); - if (g('option').recordUsage) { + + let stats = getValue('stats', true) || {}; + const battle = g().battle; + if (Object.keys(stats.tokens ?? {}).map(k => battle[k] === stats.tokens?.[k]).some(s => !s)) { + if (option.recordUsage) recordUsage2(true); + if (option.dropMonitor) dropMonitor(battleLog, true); + } + + if (option.recordUsage) { obj.log = battleLog; recordUsage(obj); } - if (g('monsterAlive') && !gE('#btcp')) { - clearBattleUnresponsive(); - onBattle(); + if (g().monsterAlive && !gE('#btcp')) { + onBattleRound(); return; } - if (g('option').dropMonitor) { + if (option.dropMonitor) { dropMonitor(battleLog); } - if (g('option').recordUsage) { + if (option.recordUsage) { recordUsage2(); } - if (g('battle').roundNow !== g('battle').roundAll) { // Next Round - if(g('option').NewRoundWaitTime){ - setTimeout(onNewRound, g('option').NewRoundWaitTime * _1s); - } else { - onNewRound(); + onRoundEnd(); + async function onRoundEnd() { try { + await waitPause(); + $async.logSwitch(arguments); + if (g().monsterAlive > 0) { // Defeat + setExitBattleTimeout('Defeat'); + return; } - return; - - async function onNewRound(){ - try { - const html = await $ajax.fetch(window.location.href); - - gE('#pane_completion').removeChild(gE('#btcp')); - clearBattleUnresponsive(); - const doc = $doc(html) - if (gE('#riddlecounter', doc)) { - if (g('option').riddlePopup && !window.opener) { - window.open(window.location.href, 'riddleWindow', 'resizable,scrollbars,width=1241,height=707'); - return; - } - goto(); - return; - } - ['#battle_right', '#battle_left'].forEach(selector=>{ gE('#battle_main').replaceChild(gE(selector, doc), gE(selector)); }) - unsafeWindow.battle = new unsafeWindow.Battle(); - unsafeWindow.battle.clear_infopane(); - Debug.log('______________newRound', true); - newRound(true); - onBattle(); - } catch(e) { e=>console.error(e) } + if (g().battle.roundNow === g().battle.roundAll) { // Victory + setExitBattleTimeout('Victory'); + return; } - } - if (g('monsterAlive') > 0) { // Defeat - SetExitBattleTimeout(g('option').autoSkipDefeated ? 'SkipDefeated' : 'Defeat'); - } - if (g('battle').roundNow === g('battle').roundAll) { // Victory - SetExitBattleTimeout('Victory'); - } - clearBattleUnresponsive(); + if (option.NewRoundWaitTime) { // Next Round + await pauseAsync(option.NewRoundWaitTime * _1s); + await waitPause(); + } + if (gE('#btcp')?.innerHTML.includes("finishbattle.png")) return console.error(`gE('#btcp')?.innerHTML.includes("finishbattle.png")`); + let url = option.checkURLBeforeNewRound; + if (url) { + lastResponsive = Infinity; + await until(async () => { try { + await waitPause(); + return await $ajax.insert(url, undefined, undefined, {}, {}, true); + } catch (err) { console.error('Connect failed:', url) }}, option.checkURLBeforeNewRoundRetry * _1s); + lastResponsive = time(0); + } + const doc = $doc(await $ajax.insert(window.location.href)); + if (gE('#riddlecounter', doc)) { + lastResponsive = Infinity; + if (option.riddlePopup && !window.opener) { + window.open(window.location.href, 'riddleWindow', 'resizable, scrollbars, width=1241, height=707'); + $async.logSwitch(arguments); + return; + } + console.log(window.location.href); + goto(); + $async.logSwitch(arguments); + return; + } + if (gE('#btcp')) { // 在没有btcp时跳过,例如被Monsterbation、jpx等其他脚本移除时 + if (option.nativeNewRound) { + onStepInDone(); + gE('#btcp').click(); + $async.logSwitch(arguments); + return; + } + gE('#pane_completion').removeChild(gE('#btcp')); + } + ['#battle_right', '#battle_left'].forEach(selector => { gE('#battle_main').replaceChild(gE(selector, doc), gE(selector)); }); + unsafeWindow.battle = undefined; + if (!await loadUnsafeWindowBattle()) { + setExitBattleTimeout('Defeat'); + return; + } + newRound(true); + onStepInDone(); + onBattleRound(); + $async.logSwitch(arguments); + } catch (err) { console.error(err); }} }; gE('body').appendChild(eventEnd); - window.sessionStorage.delay = g('option').delay; - window.sessionStorage.delay2 = g('option').delay2; + + const option = getOption(); + window.sessionStorage.delay = option.delay; + window.sessionStorage.delay2 = option.delay2; const fakeApiCall = cE('script'); fakeApiCall.textContent = `api_call = ${function (b, a, d) { - const delay = window.sessionStorage.delay * 1; + let delay = window.sessionStorage.delay * 1; const delay2 = window.sessionStorage.delay2 * 1; window.info = a; - b.open('POST', `${MAIN_URL}json`); + unsafeWindow = typeof unsafeWindow === 'undefined' ? window : unsafeWindow; + b.open('POST', `${unsafeWindow.MAIN_URL}json`); b.setRequestHeader('Content-Type', 'application/json'); b.withCredentials = true; b.onreadystatechange = d; b.onload = function () { + updateMonsterEffects(); document.getElementById('eventEnd').click(); }; document.getElementById('eventStart').click(); - if (a.mode === 'magic' && a.skill >= 200) { - if (delay <= 0) { - b.send(JSON.stringify(a)); - } else { - setTimeout(() => { - b.send(JSON.stringify(a)); - }, delay * (Math.random() * 50 + 50) / 100); - } - } else if (delay2 <= 0) { + if (a.mode !== 'magic' || a.skill < 200) { + delay = delay2; + } + if (delay <= 0) { b.send(JSON.stringify(a)); } else { setTimeout(() => { b.send(JSON.stringify(a)); - }, delay2 * (Math.random() * 50 + 50) / 100); - } - }.toString()}`; + }, delay * (Math.random() * 50 + 50) / 100); + } + }.toString()}; +// bool +let isDisplay = ${option.isDisplayAllDebuff}; +let debuffAutoFill = ${option.debuffAutoFill?.toString() ?? 'undefined'}; +let debuffAutoFillRec = ${option.debuffAutoFillRec?.toString() ?? 'undefined'}; +let onIsekaiEncounter = ${onIsekaiEncounter ?? 'undefined'}; +// object +let dataFlags = ${JSON.stringify(dataFlags)}; +let _server = ${JSON.stringify(_server)}; +let monsterStateKeys = ${JSON.stringify(monsterStateKeys)}; +let ability = ${JSON.stringify(ability)}; +let monsterBuffSkillLib = ${JSON.stringify(monsterBuffSkillLib)}; +let hvVersion = Version(...${JSON.stringify(hvVersion.ver.split('.'))}); +// funciton +${[updateMonsterEffects, fixMonsterStatus, +getMonsterID, getMonster, getMonster, getBuff, +onRestoredBattleServer, getValue, setValue, delValue, +getLocal, setLocal, delLocal, +gE, cE, Version].map(f => f.toString()).join(';')}; +`; gE('head').appendChild(fakeApiCall); const fakeApiResponse = cE('script'); fakeApiResponse.textContent = `api_response = ${function (b) { - if (b.readyState === 4) { - if (b.status === 200) { - const a = JSON.parse(b.responseText); - if (a.login !== undefined) { - top.window.location.href = login_url; - } else { - if (a.error || a.reload) { - window.location.href = window.location.search; + if (b.readyState !== 4) { + return false; + } + if (b.status !== 200) { + window.location.href = window.location.search; + return false; + } + const a = JSON.parse(b.responseText); + if (a.login !== undefined) { + top.window.location.href = login_url; + return false; + } + if (a.error || a.reload) { + window.location.href = window.location.search; + } + return a; + }.toString()}`; + gE('head').appendChild(fakeApiResponse); + } + + function updateMonsterEffects(isNewTurn = true) { + const option = typeof GM_getValue === 'undefined' ? {} : getOption(); + if (!(typeof GM_getValue === 'undefined' ? debuffAutoFill : option.debuffAutoFill)) return; + let battle = getValue('battle', true); + if (!battle?.monsterStatus) return; + if (battle.monsterStatus.map(m => getMonster(getMonsterID(m))).filter(mon => mon === null).length) { + fixMonsterStatus(); + battle = getValue('battle', true); + } + + let regExp = updateMonsterEffects.prototype.regExp ??= { + locationQueries: /\w+=\w+/g, + playerInfo: /(\w+) Lv\.(\d+)/, + staminaInfo: /Stamina:\s(\d+)/, + spellInfo: /\('([\w\s-]+)'.*, '(\w+)', (\d+), (\d+), (\d+)\)/, //Name, iconID, MP, OC, CD + itemInfo: /set_infopane_item\((\d+)/, + battleTypeLog: /Initializing (.*) \.\.\./, + floor: /Floor (\d+)/, + round: /Round (\d+) \/ (\d+)/, + monster: /MID=(\d+) \(([^<>]+)\) \LV=(\d+) HP=(\d+)/g, + effectGain: /([\w\s-]+) gains the effect ([\w\s-]+)\./g, + effectExpired: /The effect ([\w\s-]+) on ([\w\s-]+) has expired\./g, + effectWear: /The effect ([\w\s-]+) on ([\w\s-]+) has worn off\./g, + effectWearAsleep: /([^<>]+) has been roused from its sleep\./g, + effectWearConfused: /([^<>]+) got knocked out of confuse\./g, + oc: /div/g, + ocHalf: /vcr/g, + /*isekai911*/ + spellMatch: /\('(?[\w\s-]+)(?:\s\(x(?\d+)\))?',\s?(?.*),\s?'?(?[\w\s-]+)'?\)/, + /*isekai912*/ + //battleRecorder + turnLog: /([^]+?)/, + //timeRecorder + action: />([^<>]+)<\/td><\/tr>(Spirit Stance Exhausted<\/td><\/tr>)*([^<>]+)<\/td><\/tr>[^<>]+<\/td><\/tr>(Spirit Stance Exhausted<\/td><\/tr>)*]+damage( \([^<>]+\))*(<\/td><\/tr>Your spirit shield absorbs \d+ |<|\.)/g, + damageType: /for (\d+) (\w+) damage/, + spiritShield: /absorbs (\d+)/, + crit: /(You crit| crits | blasts )/, + strike: /(Fire|Cold|Wind|Elec|Holy|Dark|Void) Strike hits/, + damagePlus: /for (\d+) damage/, + damagePhysicalPlus: /(Bleeding Wound|Spreading Poison)/, + damagePoints: /for (\d+) points of (\w+) damage/, + counter: />You counter/g, + // dealt magical + magicalDealtMiss: /to connect\./g, + magicalDealtEvade: /evades your spell\./g, + magicalDealtResist50: / (?:hits|blasts) [^y][^<>]+50%/g, + magicalDealtResist75: / (?:hits|blasts) [^y][^<>]+75%/g, + magicalDealtResist90: / (?:hits|blasts) [^y][^<>]+90%/g, + magicalDealtResist: /resists your spell\./g, + // dealt physical + physicalDealtMiss: /its mark\./g, + physicalDealtEvade: /(?: dodges your attack\.|evades your offhand attack\.)/g, + physicalDealtParry: /parries your attack\./g, + // taken magical + magicalTakenEvade: / casts [^<>]+evade the attack\./g, + magicalTakenBlock: / casts [^<>]+block the attack\./g, + magicalTakenResist50: / (?:hits|blasts) y[^<>]+50%/g, + magicalTakenResist75: / (?:hits|blasts) y[^<>]+75%/g, + magicalTakenResist90: / (?:hits|blasts) y[^<>]+90%/g, + // taken physical + physicalTakenMiss: /misses the attack against you\./g, + physicalTakenEvade: /(>You evade| uses [^<>]+evade the attack\.)/g, + physicalTakenParry: /(>You parry| uses [^<>]+parry the attack\.)/g, + physicalTakenBlock: /(>You block| uses [^<>]+block the attack\.)/g, + /*isekai911*/ + //combatRecorder_isekai + damage_isekai: /[^<>]+damage/g, + damageTaken1_isekai: /(?glances|hits|crits) you.*?(?\d+).*?(?\w+) damage/, + damageTaken2_isekai: /which (?glances|hits|crits).*?(?\d+).*?(?\w+) damage/, + spiritShield_isekai: /absorbs (\d+)/, + damageDealt1_isekai: /(?:You|Your offhand attack|Arcane Blow) (?:(?\d)x-)*(?glance|hit|crit).*?(?\d+).*?(?\w+) damage/, + damageDealt2_isekai: /(?:(?\d)x-)*(?glanced|hit|crit|eviscerated) for (?\d+) (?\w+) damage/, + strike_isekai: /(Fire|Cold|Wind|Elec|Holy|Dark|Void) Strike hits.*?(\d+).*?(\w+) damage/, + explode_isekai: /explodes for (\d+) (\w+) damage/, + damagePlus_isekai: /for (\d+) damage/, + damagePhysicalPlus_isekai: /(Bleeding Wound|Spreading Poison)/, + damagePoints_isekai: /for (\d+) points of (\w+) damage/, + debuffLog_isekai: /(?:[^<>]+(?: gains the effect | partially resists the effects of your spell\.| shrugs off the effects of your spell\.)+[^<>]*<\/td><\/tr>)+You cast [a-zA-Z]+\.<\/td><\/tr>/, + debuffResist0_isekai: / gains the effect /g, + debuffResist1_isekai: / partially resists the effects of your spell\./g, + debuffResist3_isekai: / shrugs off the effects of your spell\./g, + counter_isekai: />You counter/g, + // dealt magical + magicalDealtMiss_isekai: / to connect\./g, + magicalDealtEvade_isekai: / evades your spell\./g, + magicalDealtResistPartially_isekai: / resists, and was/g, + magicalDealtResist_isekai: / resists your spell\./g, + // dealt physical + physicalDealtMiss_isekai: / its mark\./g, + physicalDealtEvade_isekai: / dodges your attack\./g, + physicalDealtParryPartially_isekai: / parries[^<>]+?(\d+)[^<>]+?(\w+) damage/g, + physicalDealtParry_isekai: / parries your attack\./g, + // taken magical + magicalTakenMiss_isekai: /(?:casts[^<>]+, but misses the attack\.|casts[^<>]+, missing you completely\.)/g, + magicalTakenEvade_isekai: />You evade the attack\./g, + magicalTakenResistPartially_isekai: / resist the attack/g, + magicalTakenBlockPartially_isekai: /casts[^<>]+partially block (?:and|resist| )*the attack/g, + magicalTakenBlock_isekai: /(?]+, but misses the attack\.|(?:vigorously whiffs at a shadow|uses[^<>]+), missing you completely\.)/g, + physicalTakenEvade_isekai: />You evade the attack from/g, + physicalTakenParryPartially_isekai: /partially parry the attack/g, + physicalTakenParry_isekai: /(?]+|>)You|you) partially block (?:and|partially|parry| )*the attack/g, + physicalTakenBlock_isekai: /(?]+ proficiency/g, + proficiency: /(\d+\.\d+) points of ([^<>]+) proficiency/, + dropsLogs: /\S+ \[[^<>]+\](<\/span><\/td><\/tr>A traveling)*/g, + drop: /(\S+) \<.*#(.{6}).*\[(.*)\](.)*/, + quality: /(Crude|Fair|Average|Superior|Exquisite|Magnificent|Legendary|Peerless)/, + credit: /(\d+) Credit/, + crystal: /(?:(\d+)x )?(Crystal of \w+)/, + } + + function getDuration(skill, channeling) { + let [base, profRatio, prof] = [skill.duration, 1, 0]; + if (typeof base === 'number') { + base = base * 1; + } else if (base !== 'permanent') { for (const ab in base) { + base = base[ab][ability[ab] ?? 0]; + break; + } } + if (skill.proficiency) { + const [ptype, plow, phigh] = skill.proficiency; + prof = proficiency[ptype]; + profRatio = Math.max(1, Math.min(4, (prof - plow) / (phigh - plow) * 4).toFixed(6) * 1); + } + const channelingRatio = skill.channling ? channeling : 1; + const duration = typeof base === 'number' ? Math.round(base * channelingRatio * profRatio) : base; + return [duration, base, profRatio, prof, channelingRatio]; + } + + function getEffectChanges(turnLog) { + let effectsAdded = turnLog.matchAll(regExp.effectGain); + let effectsRemoved = [...turnLog.matchAll(regExp.effectExpired), ...turnLog.matchAll(regExp.effectWear)]; + let asleepRemoved = turnLog.matchAll(regExp.effectWearAsleep); + let confusedRemoved = turnLog.matchAll(regExp.effectWearConfused); + let effectChanges = {}; + + for (const match of effectsAdded) (effectChanges[match[1]] ??= { add: [], remove: [] }).add.push(match[2]); + for (const match of effectsRemoved) (effectChanges[match[2]] ??= { add: [], remove: [] }).remove.push(match[1]); + for (const match of asleepRemoved) (effectChanges[match[1]] ??= { add: [], remove: [] }).remove.push('Asleep'); + for (const match of confusedRemoved) (effectChanges[match[1]] ??= { add: [], remove: [] }).remove.push('Confused'); + + return effectChanges; + } + + function applyHiddenDelta(savedEffects, effectObj, delta) { + if (!savedEffects) return; + + let elementEffects = ['Searing Skin', 'Freezing Limbs', 'Turbulent Air', 'Deep Burns', 'Breached Defense', 'Blunted Attack']; + let effects = Object.keys(effectObj); + let elementCount = effects.filter(effect => elementEffects.includes(effect)).length; + + for (const savedEffect in savedEffects) { + if (effects.includes(savedEffect)) continue; + + if ( + (elementCount < 3 && elementEffects.includes(savedEffect)) || + savedEffect === 'Coalesced Mana' + ) { + delete savedEffects[savedEffect]; + continue; + } + + if (!delta || delta <= 0) continue; + let savedTurns = +savedEffects[savedEffect]?.turns; + if (isNaN(savedTurns)) continue; + + if (savedTurns - delta < 0 && elementEffects.includes(savedEffect)) { + delete savedEffects[savedEffect]; + continue; + } + savedEffects[savedEffect].turns = Math.max(0, savedTurns - delta); + } + } + + const turnLog = gE('#textlog').innerHTML.match(/([^]+?)(()|(<\/tbody>))/)[0]; + isNewTurn &&= turnLog !== battle.turnLog; + if (turnLog.match(regExp.battleTypeLog)) return; // skip if is new round + + // update proficiency + const proficiency = battle.proficiency ?? {}; + const ptypes = { + 'cloth armor': 'Cloth Armor', + 'deprecating magic' : 'Deprecating', + 'divine': 'Divine', + 'dual wielding' : 'Dual-wielding', + 'elemental': 'Elemental', + 'forbidden': 'Forbidden', + 'heavy armor': 'Heavy Armor', + 'light armor': 'Light Armor', + 'one-handed weapon': 'One-handed', + 'staff': 'Staff', + 'supportive magic' : 'Supportive', + 'two-handed weapon': 'Two-handed', + } + for (const prof of turnLog.match(regExp.proficiencies) ?? []) { + const [_, points, type] = prof.match(regExp.proficiency); + proficiency[ptypes[type]] += points * 1; + proficiency[ptypes[type]] = proficiency[ptypes[type]].toFixed(3) * 1; + } + + // cache last turn channeling + const channeling = battle.channeling || 1; + battle.channeling = getBuff('channeling') ? 1.5 : 1; + + let effectChanges = getEffectChanges(turnLog); + + // 消除对每次操作影响turns程度最大的加速buff(技能或卷轴) + let turnDelta = (isNewTurn && !turnLog.match(regExp.zeroturn)) ? 1 : 0; + turnDelta *= 100 / ((getBuff('haste')?.getAttribute('onmouseover').match(/increasing its action speed by (.*)%\./)[1] ?? 0) * 1 + 100); + + const getBuffSkill = (buff) => Object.values(monsterBuffSkillLib).find(skill => [skill.name, skill.buff].includes(buff)) ?? console.log('Unknown debuff skill', buff); + for (const activeMonster of battle.monsterStatus) { + const monster = getMonster(getMonsterID(activeMonster)); + if (gE('img[src*="nbardead.png"]', monster)) continue; // continue if dead + + let name = gE(monsterStateKeys.name, monster).innerText; + let effectObj = {}; + let jpxObj = {}; + let monster_btm6 = gE('.btm6', monster); + const abs = ability; + monster_btm6.querySelectorAll('img').forEach((effect) => { + let tooltip = effect.getAttribute('onmouseover'); + if (!tooltip) return; + + let matches = tooltip.match(regExp.spellMatch); + if (!matches?.groups) return; + + let { name, stack, description, turns } = matches.groups; + if (!name) return console.log('Undefined debuff name:', name); + const jpx = turns === '-' || description.includes('jpx Hidden Effects'); + if (jpx) { // 可能为jpx补全的buff,在hvAA中重新计算确认数据 + jpxObj[name] = effect; + return; + } + let dc = getBuffSkill(name)?.description; + if (dc !== description) { + // TODO 测试确保 ability[4213] Better Slow 效果描述正常,目前是描述均是30% + if (dc !== `'The target has been slowed by ${[30, 40, 40, 45, 50, 50][abs[4213] ?? 0]}%, making it attack less frequently.'` && description !== `'The target has been slowed by 30%, making it attack less frequently.'`) { + console.log('Unmatched debuff description:', description, '\n from', name, dc); + } + } + effectObj[name] = { turns, stack: stack ?? 1 }; + }); + let effects = Object.keys(effectObj); + + // DEBUG --------------------- + if (typeof GM_getValue === 'undefined' ? debuffAutoFillRec : option.debuffAutoFillRec) { + // 统计持续时间及熟练度相关数据,以便进行核验和测试 + onRestoredBattleServer('rec', () => { + const rec = JSON.parse(localStorage.getItem(`hvAA-${_server.name}_rec`) ?? `{}`); + for (const effect of effects) { + const turns = effectObj[effect].turns * 1; + if (isNaN(turns)) continue; + const skill = getBuffSkill(effect); + if (!skill) continue; + + rec[effect] ??= { t:0 }; + // 获取新增时间(忽略非新增的情况) + let [delta, added] = [turns - rec[effect].t, rec[effect].d]; + if (delta > 0) { + added = rec[effect].t ? delta : added; + } + // 获取基础、熟练度计算倍率、熟练度,设置及初始化主要数据 + let [duration, base, profRatio, prof, channelingRatio] = getDuration(skill, channeling); + if (profRatio === 4) rec[effect].f = prof; // 比例刚好是4时的熟练度(推测是公式中的熟练度上限) + rec[effect].b = base; // 基础持续时间 + rec[effect].c = profRatio; // 公式理论计算值 + rec[effect].ch = rec[effect].t && added > 0 ? channelingRatio : rec[effect].ch; // 引导倍率 + rec[effect].t = turns; // 当前剩余持续时间 + rec[effect].d = added; // 新增时间 + rec[effect].m = Math.max(rec[effect].m ?? 0, added); // 历史最大新增时间 + rec[effect].a ??= [0, 0]; // 推测熟练度倍率 [ 历史最大值, 按照‘缺失引导信息导致变成1.5倍’的修正值(除以1.5) ] + rec[effect].r ??= [0, 0, 0]; // 实际倍率 [ 0-4 应该正常, 4-6推测缺失引导信息, 6+ 异常] + rec.error ??= []; // 实际倍率异常时的相关信息 + // 计算推测倍率 + if (base <= added) { + const a = Math.max(base, added)/base/channelingRatio + rec[effect].a[0] = Math.max(a, rec[effect].a[0]).toFixed(4) * 1; + rec[effect].a[1] = Math.max(a / 1.5, rec[effect].a[1]).toFixed(4) * 1; + } + // 检查实际ratio + const ratio = Math.max(base, added)/duration; + if (ratio > 1.5) { + const e = `${effect}: ${Math.max(base, added).toFixed(4)}/(${base.toFixed(4)}*${channelingRatio.toFixed(4)}*${profRatio.toFixed(4)})=${ratio.toFixed(4)}` + if (!rec.error.includes(e)) rec.error.push(e); } - return a; + if (ratio > 1.5 && ratio > rec[effect].r[2]) { + rec[effect].r[2] = ratio.toFixed(4) * 1; + } else if (ratio <= 1.5 && ratio > 1 && ratio > rec[effect].r[1]) { + rec[effect].r[1] = ratio.toFixed(4) * 1; + } else if (ratio <= 1 && ratio > rec[effect].r[0]) { + rec[effect].r[0] = ratio.toFixed(4) * 1; + } + localStorage.setItem(`hvAA-${_server.name}_rec`, JSON.stringify(rec)); } - } else { - window.location.href = window.location.search; + }); + } + // DEBUG END --------------------- + + let savedEffects = activeMonster.effectObj ??= {}; + if (effects.length <= 5) for (const effect in savedEffects) delete savedEffects[effect]; + if (effects.length >= 5) for (const effect of effects) savedEffects[effect] = { ...effectObj[effect] }; // updated directly + if (effects.length !== 6) continue; // <= 5 && undetermined situation + + if (effectChanges[name]) { + for (const effect of effectChanges[name].add) { + const skill = getBuffSkill(effect); + if (!skill) continue; + /* TODO +1. TBD stack from monsterBuffSkillLib etc. +2. 测试检查非 减益技能(deprecating) 的debuff持续时间是否正确 (monsterBuffSkillLib) +3. 确认v091不同buff的叠加规则(部分抵抗无法估算?) +4. 确认熟练度倍率公式,已知最大为4。推测: +计算方式为 (p-pmin)/(pmax-pmin) * 4 +pmin/pmax 见 https://ehwiki.org/wiki/Spells#Deprecating_Magic +和 https://ehwiki.org/wiki/Spells#Offensive_Magic +减益技能(deprecating) 统一按照减益的熟练度 +元素攻击(应该包括Burning Soul/Ripened Soul?)带来的按各自的熟练度(推测是按T3的pmin/pmax) +至于取整方式则暂时无法确定 +*/ + let [duration, base, profRatio, prof, channelingRatio] = getDuration(skill, channeling); + if (savedEffects[name]) savedEffects[name][effect].channeling ??= channelingRatio; + if (effects.includes(effect)) continue; // updated directly above + if (!duration) { console.log('duration undefined saved effect:', effect, savedEffects[effect]) } + if (savedEffects[effect]) { + const turn = (savedEffects[effect].turns ?? 0) * 1; + if (isNewTurn && !isNaN(turn)) duration = turn + (duration ?? 0); + } + savedEffects[effect] = { turns: duration ?? '-', stack: '-' , channeling: channelingRatio}; } + for (const effect of effectChanges[name].remove) (!effects.includes(effect) && (effect in savedEffects)) && delete savedEffects[effect]; } - return false; - }.toString()}`; - gE('head').appendChild(fakeApiResponse); + + applyHiddenDelta(savedEffects, effectObj, turnDelta); + + monster_btm6.style.width = 'max-content'; + activeMonster.effectObj = savedEffects; + for (const effect in savedEffects) { + if (effect in effectObj) continue; + let { turns, stack } = savedEffects[effect]; + effectObj[effect] = { turns, stack }; + if (isNaN(+turns)) turns = `'${String(turns).replace(/'/g, "\\'")}'`; + let img = jpxObj[effect] ?? document.createElement('img'); + img.src = (`${_server.isekai ? '/isekai' : ''}/y/e/${ getBuffSkill(effect)?.img || 'channeling'}.png`); + let description = getBuffSkill(effect)?.description; + img.setAttribute('onmouseover', `battle.set_infopane_effect('${effect} (x${stack})', ${description}, ${isNaN(+turns) ? turns : Math.floor(turns)})`); + img.setAttribute('onmouseout', 'battle.clear_infopane()'); + + monster_btm6.appendChild(img); + } + } + if (!isNewTurn) return; + battle.turnLog = turnLog; + battle.time = new Date().getTime(); + setValue('battle', battle); } + async function loadUnsafeWindowBattle() { try { + unsafeWindow.battle = await until(() => gE('#vbd') ? true : new unsafeWindow.Battle(), 300); + if (!unsafeWindow.battle && gE('#vbd')) { + console.log('Initialization of unsafeWindow.battle stoped due to defeated.'); + return false; + } + unsafeWindow.battle.clear_infopane(); + return true; + } catch (err) { console.error(err); }} + function newRound(isNew) { // New Round - let battle = isNew ? {} : getValue('battle', true); + $debug.log('______________newRound', isNew); + const option = getOption(); + const token = document.documentElement.outerHTML.match(/var battle_token = "(.*)";/)[1]; + let battle = getValue('battle', true); + const arena = getValue('arena', true) ?? {}; + const same = battle?.token === token && arena?.postoken === battle?.postoken; + const prof = getValue('proficiency', true); + if (isNew) { + battle = { proficiency: same ? battle?.proficiency ?? prof : prof }; + } if (!battle) { - battle = JSON.parse(JSON.stringify(g('battle') ?? {})); - battle.monsterStatus?.sort(objArrSort('order')); + battle = JSON.parse(JSON.stringify(g().battle ?? {})); + battle.monsterStatus?.sortBy(x => x.order); }; + [battle.token, battle.postoken] = [token, arena.postoken]; + battle.proficiency = same ? battle?.proficiency ?? prof : prof; setValue('battle', battle); if (window.location.hash !== '') { goto(); } - g('monsterAll', gE('div.btm1', 'all').length); + g('monsterAll', gE(monsterStateKeys.obj, 'all').length); const monsterDead = gE('img[src*="nbardead"]', 'all').length; - g('monsterAlive', g('monsterAll') - monsterDead); - g('bossAll', gE('div.btm2[style^="background"]', 'all').length); - const bossDead = gE('div.btm1[style*="opacity"] div.btm2[style*="background"]', 'all').length; - g('bossAlive', g('bossAll') - bossDead); + g('monsterAlive', g().monsterAll - monsterDead); + g('bossAll', gE(`${monsterStateKeys.lv}[style^="background"]`, 'all').length); + const bossDead = gE(`${monsterStateKeys.obj}[style*="opacity"] ${monsterStateKeys.lv}[style*="background"]`, 'all').length; + g('bossAlive', g().bossAll - bossDead); + const types = { + ar: { + reg: /^Initializing arena challenge/, + extra: id => id <= 35, + }, + rb: { + reg: /^Initializing arena challenge/, + extra: id => id >= 105, + }, + iw: { reg: /^Initializing Item World/ }, + gr: { reg: /^Initializing Grindfest/ }, + tw: { reg: /^Initializing The Tower/ }, + ba: { reg: /^Initializing random encounter/ }, + } const battleLog = gE('#textlog>tbody>tr>td', 'all'); - if (!battle.roundType) { - const temp = battleLog[battleLog.length - 1].textContent; - const types = { - 'ar': { - reg: /^Initializing arena challenge/, - extra: (i) => i <= 35, - }, - 'rb': { - reg: /^Initializing arena challenge/, - extra: (i) => i >= 105, - }, - 'iw': { reg: /^Initializing Item World/ }, - 'gr': { reg: /^Initializing Grindfest/ }, - 'tw': { reg: /^Initializing The Tower/ }, - 'ba': { - reg: /^Initializing random encounter/, - extra: (_) => { - const encounter = getEncounter(); - if (encounter[0] && encounter[0].time >= time(0) - 0.5 * _1h) { - encounter[0].encountered = time(0); - setEncounter(encounter); - } - return true; - } - }, - } - battle.tower = (temp.match(/\(Floor (\d+)\)/) ?? [null])[1] * 1; - const id = (temp.match(/\d+/) ?? [null])[0] * 1; + const firstLog = battleLog[battleLog.length - 1].textContent; + if (!battle.roundType || firstLog.match(/^Initializing/)) { + battle.tower = (firstLog.match(/\(Floor (\d+)\)/) ?? [null])[1] * 1; + const id = (firstLog.match(/\d+/) ?? [null])[0] * 1; battle.roundType = undefined; for (let name in types) { const type = types[name]; - if (!temp.match(type.reg)) { - continue; - } - if (type.extra && !type.extra(id)) { - continue; - } + if (!firstLog.match(type.reg)) continue; + if (type.extra && !type.extra(id)) continue; battle.roundType = name; break; } + if (!battle.roundType) console.warn('Unable to get battle type from log:', _server, firstLog); } - if (/You lose \d+ Stamina/.test(battleLog[0].textContent)) { - const staminaLostLog = getValue('staminaLostLog', true) || {}; - staminaLostLog[time(3)] = battleLog[0].textContent.match(/You lose (\d+) Stamina/)[1] * 1; - setValue('staminaLostLog', staminaLostLog); - const losedStamina = battleLog[0].textContent.match(/\d+/)[0] * 1; - if (losedStamina >= g('option').staminaLose) { - setAlarm('Error'); - if (!_alert(1, '当前Stamina过低\n或Stamina损失过多\n是否继续?', '當前Stamina過低\n或Stamina損失過多\n是否繼續?', 'Continue?\nYou either have too little Stamina or have lost too much')) { - pauseChange(); - return; - } + if (battle.roundType === 'ba' || document.body.innerHTML.match(/Initializing random encounter/)) { + const encounter = getEncounter(); + if (encounter[0]) { + encounter[0].encountered = time(0); + setEncounter(encounter); } } @@ -3195,46 +7527,39 @@ try { if (battleLog[battleLog.length - 1].textContent.match('Initializing')) { const monsterStatus = []; let order = 0; - const monsterNames = Array.from(gE('div.btm3>div>div', 'all')).map(monster => monster.innerText); - const monsterLvs = Array.from(gE('div.btm2>div>div', 'all')).map(monster => monster.innerText); + const monsterNames = Array.from(gE(`${monsterStateKeys.name}>div>div`, 'all')).map(monster => monster.innerText); + const monsterLvs = Array.from(gE(`${monsterStateKeys.lv}>div>div`, 'all')).map(monster => monster.innerText); const monsterDB = getValue('monsterDB', true) ?? {}; const monsterMID = getValue('monsterMID', true) ?? {}; - const oldDB = JSON.stringify(monsterDB); - const oldMID = JSON.stringify(monsterMID); - for (let i = battleLog.length - 2; i > battleLog.length - 2 - g('monsterAll'); i--) { - let mid = battleLog[i].textContent.match(/MID=(\d+)/)[1] * 1; - let name = battleLog[i].textContent.match(/MID=(\d+) \((.*)\) LV/)[2]; - let lv = battleLog[i].textContent.match(/LV=(\d+)/)[1] * 1; + const start = battleLog.length - 2; + for (let i = battleLog.length - 2; i > battleLog.length - 2 - g().monsterAll; i--) { let hp = battleLog[i].textContent.match(/HP=(\d+)$/)[1] * 1; if (isNaN(hp)) { hp = getHPFromMonsterDB(monsterDB, monsterNames[order], monsterLvs[order]) ?? monsterStatus[monsterStatus.length - 1].hp; } - if (name && lv && mid) { - monsterDB[name] ??= {}; - if (monsterDB[name].mid && monsterDB[name].mid !== mid) { // 名称被其他mid被占用 - monsterMID[monsterDB[name].mid] = JSON.parse(JSON.stringify(monsterDB[name])); // 将之前mid的数据进行另外备份 - monsterDB[name] = {}; // 重置该名称的数据 - } - if (monsterMID[mid]) { - monsterDB[name] = JSON.parse(JSON.stringify(monsterMID[mid])); // 将之前备份的mid的数据进行恢复 - delete monsterMID[mid]; - } - monsterDB[name].mid = mid; - monsterDB[name][lv] = hp; - } - monsterStatus[order] = { - order: order, - hp, - }; + monsterStatus[order] = { order, hp }; order++; - } - if(g('option').cacheMonsterHP){ - if (oldDB !== JSON.stringify(monsterDB)) { - setValue('monsterDB', monsterDB); + + if (!option.cacheMonsterHP) continue; + let lv = battleLog[i].textContent.match(/LV=(\d+)/)[1] * 1; + let [_, mid, name] = battleLog[i].textContent.match(/MID=(\d+) \((.*)\) LV/); + mid*=1; + if (!name || isNaN(lv) || isNaN(mid)) continue; + monsterDB[name] ??= {}; + if (monsterDB[name].mid && monsterDB[name].mid !== mid) { // 名称被其他mid被占用 + monsterMID[monsterDB[name].mid] = JSON.parse(JSON.stringify(monsterDB[name])); // 将之前mid的数据进行另外备份 + monsterDB[name] = {}; // 重置该名称的数据 } - if (oldMID !== JSON.stringify(monsterMID)) { - setValue('monsterMID', monsterMID); + if (monsterMID[mid]) { + monsterDB[name] = JSON.parse(JSON.stringify(monsterMID[mid])); // 将之前备份的mid的数据进行恢复 + delete monsterMID[mid]; } + monsterDB[name].mid = mid; + monsterDB[name][lv] = hp; + } + if (option.cacheMonsterHP) { + setValue('monsterDB', monsterDB); + setValue('monsterMID', monsterMID); } battle.monsterStatus = monsterStatus; @@ -3246,34 +7571,27 @@ try { battle.roundNow = 1; battle.roundAll = 1; } - } else if (!battle.monsterStatus || battle.monsterStatus.length !== gE('div.btm2', 'all').length) { + } else if (!battle.monsterStatus || battle.monsterStatus.length !== gE(monsterStateKeys.lv, 'all').length) { battle.roundNow = 1; battle.roundAll = 1; } - if(roundPrev !== battle.roundNow) { + if (roundPrev !== battle.roundNow) { battle.turn = 0; + setValue('skillOTOS', {}); } battle.roundLeft = battle.roundAll - battle.roundNow; setValue('battle', battle); - - g('skillOTOS', { - OFC: 0, - FRD: 0, - T3: 0, - T2: 0, - T1: 0, - }); } function killBug() { // 在 HentaiVerse 发生导致 turn 损失的 bug 时发出警告并移除问题元素: https://ehwiki.org/wiki/HentaiVerse_Bugs_%26_Errors#Combat const bugLog = gE('#textlog > tbody > tr > td[class="tlb"]', 'all'); const isBug = /(Slot is currently not usable)|(Item does not exist)|(Inventory slot is empty)|(You do not have a powerup gem)/; - for (let i = 0; i < bugLog.length; i++) { + for (const i of range(bugLog)) { if (bugLog[i].textContent.match(isBug)) { bugLog[i].className = 'tlbWARN'; - setTimeout(() => { // 间隔时间以避免持续刷新 - window.location.href = window.location;// 刷新移除问题元素 + setTimeout(() => { // 刷新移除问题元素,间隔时间以避免持续刷新 + window.location.href = window.location.search ? window.location.pathname + window.location.search : window.location.href; }, 700); } else { bugLog[i].className = 'tlbQRA'; @@ -3283,116 +7601,106 @@ try { function countMonsterHP() { // 统计敌人血量 let i, j; - const monsterHp = gE('div.btm4>div.btm5:nth-child(1)', 'all'); + const monsterHp = gE(`${monsterStateKeys.bars}:nth-child(1)`, 'all'); + const monsterMp = gE(`${monsterStateKeys.bars}:nth-child(2)`, 'all'); + const monsterSp = gE(`${monsterStateKeys.bars}:nth-child(3)`, 'all'); let battle = getValue('battle', true); const monsterStatus = battle.monsterStatus; const hpArray = []; - for (i = 0; i < monsterHp.length; i++) { + for (i of range(monsterHp)) { + monsterStatus[i] ??= {}; if (gE('img[src*="nbardead.png"]', monsterHp[i])) { monsterStatus[i].isDead = true; monsterStatus[i].hpNow = Infinity; } else { monsterStatus[i].isDead = false; - monsterStatus[i].hpNow = Math.floor(monsterStatus[i].hp * parseFloat(gE('img', monsterHp[i]).style.width) / 120 + 1); + monsterStatus[i].hpNow = Math.floor(monsterStatus[i].hp * parseFloat(gE('img:first-child', monsterHp[i]).style.width) / 120 + 1); + monsterStatus[i].mpNow = parseFloat(gE('img:first-child', monsterMp[i]).style.width) / 120; + monsterStatus[i].spNow = parseFloat(gE('img:first-child', monsterSp[i]).style.width) / 120; hpArray.push(monsterStatus[i].hpNow); } } battle.monsterStatus = monsterStatus; - const skillLib = { - Sle: { - name: 'Sleep', - img: 'sleep', - }, - Bl: { - name: 'Blind', - img: 'blind', - }, - Slo: { - name: 'Slow', - img: 'slow', - }, - Im: { - name: 'Imperil', - img: 'imperil', - }, - MN: { - name: 'MagNet', - img: 'magnet', - }, - Si: { - name: 'Silence', - img: 'silence', - }, - Dr: { - name: 'Drain', - img: 'drainhp', - }, - We: { - name: 'Weaken', - img: 'weaken', - }, - Co: { - name: 'Confuse', - img: 'confuse', - }, - CM: { - name: 'Coalesced Mana', - img: 'coalescemana', - }, - Stun: { - name: 'Stunned', - img: 'wpn_stun', - }, - PA: { - name: 'Penetrated Armor', - img: 'wpn_ap', - }, - BW: { - name: 'Bleeding Wound', - img: 'wpn_bleed', - }, - }; - const monsterBuff = gE('div.btm6', 'all'); + const monsterBuff = gE(monsterStateKeys.buffs, 'all'); const hpMin = Math.min.apply(null, hpArray); - const yggdrasilExtraWeight = g('option').YggdrasilExtraWeight; - const unreachableWeight = g('option').unreachableWeight; - const baseHpRatio = g('option').baseHpRatio ?? 1; + const option = getOption(); + const yggdrasilExtraWeight = option.YggdrasilExtraWeight; + const baseHpRatio = option.baseHpRatio; // 权重越小,优先级越高 - for (i = 0; i < monsterStatus.length; i++) { // 死亡的排在最后(优先级最低) - if (monsterStatus[i].isDead) { - monsterStatus[i].finWeight = unreachableWeight; + for (i of range(monsterStatus)) { // 死亡的排在最后(优先级最低) + const target = monsterStatus[i]; + if (target.isDead) { + target.finWeight = resolveRPNFormula(option.unreachableWeight, target); continue; } - let weight = baseHpRatio * Math.log10(monsterStatus[i].hpNow / hpMin); // > 0 生命越低权重越低优先级越高 - monsterStatus[i].hpWeight = weight; - if (yggdrasilExtraWeight && ('Yggdrasil' === gE('div.btm3>div>div', monsterBuff[i].parentNode).innerText || '世界树 Yggdrasil' === gE('div.btm3>div>div', monsterBuff[i].parentNode).innerText)) { // 默认设置下,任何情况都优先击杀群体大量回血的boss"Yggdrasil" + let weight = baseHpRatio * Math.log10(target.hpNow / hpMin); // > 0 生命越低权重越低优先级越高 + const name = gE(`${monsterStateKeys.name}>div>div`, monsterBuff[i].parentNode).innerText; + if (yggdrasilExtraWeight && (['Yggdrasil', '世界树 Yggdrasil'].includes(name))) { // 默认设置下,任何情况都优先击杀群体大量回血的boss"Yggdrasil" weight += yggdrasilExtraWeight; // yggdrasilExtraWeight.defalut -1000 } - for (j in skillLib) { - if (gE(`img[src*="${skillLib[j].img}"]`, monsterBuff[i])) { - weight += g('option').weight[j]; + const known = {}; + for (j in monsterBuffSkillLib) { + const skill = monsterBuffSkillLib[j]; + if (!getBuff(skill.img, getMonsterID(target))) { + continue; + } + known[skill.img] = skill; + if (skill.elem && skill.elem !== g().attackStatus) { + weight += option.weight[`${j}1`] ?? 0; + continue; + } + weight += option.weight[j] ?? 0; + } + + let unknown = gE(`img`, 'all', monsterBuff[i]); + if (unknown?.length) { + unknown = Array.from(unknown).filter(buff => { + const img = buff.src.match(/\/y\/e\/(.*)\.png/)[1]; + return !(Object.keys(known).includes(img)); + }).map(buff => `${buff.getAttribute('onmouseover').match(/^battle.set_infopane_effect\('(.+)', *'.*',.+\)/)[1]}: ${buff.src.match(/\/y\/e\/(.*)\.png/)[1]}`); + if (unknown.length) { + console.log('unsupported buff weight:', unknown); } } monsterStatus[i].finWeight = weight; } - monsterStatus.sort(objArrSort('finWeight')); - battle.monsterStatus = monsterStatus; + + // 先存一次,用于下面的额外权重公式 + battle.monsterStatus = monsterStatus.sortBy(x => x.finWeight); + g('battle', battle); + const extraWeightFormula = option.extraWeightFormula; + // 额外权重公式 + monsterStatus.forEach(t => { t.finWeight += resolveRPNFormula(extraWeightFormula, t); }); + battle.monsterStatus = monsterStatus.sortBy(x => x.finWeight); g('battle', battle); } - function autoRecover() { // 自动回血回魔 - if (!g('option').item) { - return false; - } - if (!g('option').itemOrderValue) { + function resolveSkillExtraWeight(target) { + if (g().inSkillExtraWeight) return 0; + g('inSkillExtraWeight', true); + const result = resolveRPNFormula(g().option.skillExtraWeight, target); + g('inSkillExtraWeight', false); + return result; + } + + function autoRecover(isCureOnly) { // 自动回血回魔 + const option = getOption(); + if (!option.item) { return false; } - const name = g('option').itemOrderName.split(','); - const order = g('option').itemOrderValue.split(','); - for (let i = 0; i < name.length; i++) { - if (g('option').item[name[i]] && checkCondition(g('option')[`item${name[i]}Condition`]) && isOn(order[i])) { - isOn(order[i]).click(); + const name = splitOrders(option.itemOrderName, getDefaultOrder('itemOrder')); + const order = splitOrders(option.itemOrderValue, getDefaultOrder('itemOrder', ord => ord.value.match(/,(.*)/)[1] * 1)); + const cures = [313, 11199, 11501, 10005, 11195, 311]; + for (const i of range(name)) { + let id = order[i]; + if (isCureOnly && !cures.includes(id)) { + continue; + } + setBattleSkillParam(id); + if (option.item[name[i]] && checkCondition(option[`item${name[i]}Condition`]) && isOn(id)) { + updateSkillOTOS(id); + (gE(`.bti3>div[onmouseover*="(${id})"]`) ?? gE(id)).click(); return true; } } @@ -3400,22 +7708,24 @@ try { } function useScroll() { // 自动使用卷轴 - if (!g('option').scrollSwitch) { + const option = getOption(); + if (!option.scrollSwitch) { return false; } - if (!g('option').scroll) { + if (!option.scroll) { return false; } - if (!checkCondition(g('option').scrollCondition)) { + if (!option.scrollRoundType) { return false; } - if (!g('option').scrollRoundType) { + if (!option.scrollRoundType[g().battle.roundType]) { return false; } - if (!g('option').scrollRoundType[g('battle').roundType]) { + setBattleSkillParam('scroll'); + if (!checkCondition(option.scrollCondition)) { return false; } - const scrollLib = { + const scrollLib = useScroll.prototype.scrollLib ??= { Go: { name: 'Scroll of the Gods', id: 13299, @@ -3460,204 +7770,126 @@ try { id: 13201, mult: '1', img1: 'absorb', - }, - }; - const scrollFirst = (g('option').scrollFirst) ? '_scroll' : ''; - let isUsed; - for (const i in scrollLib) { - if (g('option').scroll[i] && gE(`.bti3>div[onmouseover*="${scrollLib[i].id}"]`) && checkCondition(g('option')[`scroll${i}Condition`])) { - for (let j = 1; j <= scrollLib[i].mult; j++) { - if (gE(`#pane_effects>img[src*="${scrollLib[i][`img${j}`]}${scrollFirst}"]`)) { - isUsed = true; - break; - } - isUsed = false; - } - if (!isUsed) { - gE(`.bti3>div[onmouseover*="${scrollLib[i].id}"]`).click(); - return true; + }, + }; + const scrollFirst = (option.scrollFirst) ? '_scroll' : ''; + for (const i in scrollLib) { + if (!option.scroll[i]) { + continue; + } + const id = scrollLib[i].id; + if (!gE(`.bti3>div[onmouseover*="(${id})"]`)) { + continue; + } + setBattleSkillParam(id, { scroll: 1} ); + if (!checkCondition(option[`scroll${i}Condition`])) { + continue; + } + for (const j of range(scrollLib[i].mult)) { + if (getBuff(scrollLib[i][`img${j+1}`] + scrollFirst)) { + continue; } + updateSkillOTOS(id); + gE(`.bti3>div[onmouseover*="(${id})"]`).click(); + return true; } } return false; } + function checkBuffThreshold(buff, threshold) { + const id = playerBuffSkillLib[buff].id; + const buffObj = getBuff(playerBuffSkillLib[buff].img); + threshold = threshold?.[buff] ?? 0; + const current = getBuffTurnFromImg(buffObj); + const checked = !isOn(id) || (current === Infinity || threshold >= 0 && current > threshold); + return { id, buffObj, threshold, current, checked }; + } + + function onClickBuff(id) { + updateSkillOTOS(id); + gE(id).click(); + } + function useChannelSkill() { // 自动施法Channel技能 - if (!g('option').channelSkillSwitch) { - return false; - } - if (!g('option').channelSkill) { + const option = getOption(); + if (!option.channelSkillSwitch) { return false; } - if (!gE('#pane_effects>img[src*="channeling"]')) { + if (!getBuff('channeling')) { return false; } - const skillLib = { - Pr: { - name: 'Protection', - id: '411', - img: 'protection', - }, - SL: { - name: 'Spark of Life', - id: '422', - img: 'sparklife', - }, - SS: { - name: 'Spirit Shield', - id: '423', - img: 'spiritshield', - }, - Ha: { - name: 'Haste', - id: '412', - img: 'haste', - }, - AF: { - name: 'Arcane Focus', - id: '432', - img: 'arcanemeditation', - }, - He: { - name: 'Heartseeker', - id: '431', - img: 'heartseeker', - }, - Re: { - name: 'Regen', - id: '312', - img: 'regen', - }, - SV: { - name: 'Shadow Veil', - id: '413', - img: 'shadowveil', - }, - Ab: { - name: 'Absorb', - id: '421', - img: 'absorb', - }, - }; - let i; let - j; - const skillPack = g('option').buffSkillOrderValue.split(','); - if (g('option').channelSkill) { - for (i = 0; i < skillPack.length; i++) { - j = skillPack[i]; - if (g('option').channelSkill[j] && !gE(`#pane_effects>img[src*="${skillLib[j].img}"]`) && isOn(skillLib[j].id)) { - gE(skillLib[j].id).click(); - return true; - } + + playerBuffSkillLib.CF.id = getBuff('sparklife') ? undefined : 422; + if (option.channelSkill) { + const skillPack = splitOrders(option.buffSkillOrderValue, getDefaultOrder('buffSkillOrder')); + for (const buff of skillPack) { + if (!option.channelSkill[buff]) continue; + + const { id, buffObj, current, threshold, checked } = checkBuffThreshold(buff, option.channelThreshold); + if (checked) continue; + + if (buffObj) continue; + onClickBuff(id); + return true; } } - if (g('option').channelSkill2 && g('option').channelSkill2OrderValue) { - const order = g('option').channelSkill2OrderValue.split(','); - for (i = 0; i < order.length; i++) { - if (isOn(order[i])) { - gE(order[i]).click(); - return true; - } + if (option.channelSkill2) { + const order = splitOrders(option.channelSkill2OrderValue); + const buffs = order.map(id => Object.keys(playerBuffSkillLib).find(s => playerBuffSkillLib[s].id * 1 === 1 * id)).filter(buff => buff); + for (const buff of buffs) { + const { id, buffObj, current, threshold, checked } = checkBuffThreshold(buff, option.channelThreshold); + if (checked) continue; + onClickBuff(id); + return true; } } - const buff = gE('#pane_effects>img', 'all'); - if (buff.length > 0) { - const name2Skill = { - 'Protection': 'Pr', - 'Spark of Life': 'SL', - 'Spirit Shield': 'SS', - 'Hastened': 'Ha', - 'Arcane Focus': 'AF', - 'Heartseeker': 'He', - 'Regen': 'Re', - 'Shadow Veil': 'SV', - }; - for (i = 0; i < buff.length; i++) { - const spellName = buff[i].getAttribute('onmouseover').match(/'(.*?)'/)[1]; - const buffLastTime = buff[i].getAttribute('onmouseover').match(/\(.*,.*, (.*?)\)$/)[1] * 1; - if (isNaN(buffLastTime) || buff[i].src.match(/_scroll.png$/)) { - continue; - } else { - if (spellName === 'Cloak of the Fallen' && !gE('#pane_effects>img[src*="sparklife"]') && isOn('422')) { - gE('422').click(); - return true; - } if (spellName in name2Skill && isOn(skillLib[name2Skill[spellName]].id)) { - gE(skillLib[name2Skill[spellName]].id).click(); - return true; - } - } + if (option.channelRebuff) { + let minBuff, minTime; + for (const buff in playerBuffSkillLib) { + const { id, buffObj, current, threshold, checked } = checkBuffThreshold(buff, option.channelThreshold); + if (checked) continue; + + if (buffObj?.src.match(/_scroll.png$/) || (minTime && current >= minTime)) continue; + if (!current && (!option.buffSkillSwitch || !option.buffSkill[buff])) continue; + + minBuff = id; + minTime = current; + } + if (minBuff && gE(minBuff)) { + onClickBuff(minBuff); + return true; } } return false; } function useBuffSkill() { // 自动施法BUFF技能 - const skillLib = { - Pr: { - name: 'Protection', - id: '411', - img: 'protection', - }, - SL: { - name: 'Spark of Life', - id: '422', - img: 'sparklife', - }, - SS: { - name: 'Spirit Shield', - id: '423', - img: 'spiritshield', - }, - Ha: { - name: 'Haste', - id: '412', - img: 'haste', - }, - AF: { - name: 'Arcane Focus', - id: '432', - img: 'arcanemeditation', - }, - He: { - name: 'Heartseeker', - id: '431', - img: 'heartseeker', - }, - Re: { - name: 'Regen', - id: '312', - img: 'regen', - }, - SV: { - name: 'Shadow Veil', - id: '413', - img: 'shadowveil', - }, - Ab: { - name: 'Absorb', - id: '421', - img: 'absorb', - }, - }; - if (!g('option').buffSkillSwitch) { + const option = getOption(); + if (!option.buffSkillSwitch) { return false; } - if (!g('option').buffSkill) { + if (!option.buffSkill) { return false; } - if (!checkCondition(g('option').buffSkillCondition)) { + setBattleSkillParam('buff'); + if (!checkCondition(option.buffSkillCondition)) { return false; } - let i; - const skillPack = g('option').buffSkillOrderValue.split(','); - for (i = 0; i < skillPack.length; i++) { - let buff = skillPack[i]; - if (g('option').buffSkill[buff] && checkCondition(g('option')[`buffSkill${buff}Condition`]) && !gE(`#pane_effects>img[src*="${skillLib[buff].img}"]`) && isOn(skillLib[buff].id)) { - gE(skillLib[buff].id).click(); + const skillPack = splitOrders(option.buffSkillOrderValue, getDefaultOrder('buffSkillOrder')); + for (const buff of skillPack) { + if (!option.buffSkill[buff]) continue; + + const { id, buffObj, current, threshold, checked } = checkBuffThreshold(buff, option.buffSkillThreshold); + if (checked) continue; + setBattleSkillParam(id, { buff: 1}); + if (checkCondition(option[`buffSkill${buff}Condition`])) { + onClickBuff(id); return true; } } - const draughtPack = { + + const draughtPack = useBuffSkill.prototype.draughtPack ??= { HD: { id: 11191, img: 'healthpot', @@ -3679,9 +7911,12 @@ try { img: 'gum', }, }; - for (i in draughtPack) { - if (!gE(`#pane_effects>img[src*="${draughtPack[i].img}"]`) && g('option').buffSkill && g('option').buffSkill[i] && checkCondition(g('option')[`buffSkill${i}Condition`]) && gE(`.bti3>div[onmouseover*="${draughtPack[i].id}"]`)) { - gE(`.bti3>div[onmouseover*="${draughtPack[i].id}"]`).click(); + for (const i in draughtPack) { + const id = draughtPack[i].id; + setBattleSkillParam(id, { draught: 1}); + if (!getBuff(draughtPack[i].img) && option.buffSkill && option.buffSkill[i] && checkCondition(option[`buffSkill${i}Condition`]) && gE(`.bti3>div[onmouseover*="(${id})"]`)) { + updateSkillOTOS(id); + gE(`.bti3>div[onmouseover*="(${id})"]`).click(); return true; } } @@ -3689,165 +7924,209 @@ try { } function useInfusions() { // 自动使用魔药 - if (g('attackStatus') === 0) { - return false; - } - if (!g('option').infusionSwitch) { - return false; - } - if (!checkCondition(g('option').infusionCondition)) { + const option = getOption(); + if (!option.infusionSwitch) return false; + setBattleSkillParam('infusion'); + if (!checkCondition(option.infusionCondition)) { return false; } - const infusionLib = [null, { - id: 12101, - img: 'fireinfusion', - }, { - id: 12201, - img: 'coldinfusion', - }, { - id: 12301, - img: 'elecinfusion', - }, { - id: 12401, - img: 'windinfusion', - }, { - id: 12501, - img: 'holyinfusion', - }, { - id: 12601, - img: 'darkinfusion', - }]; - if (gE(`.bti3>div[onmouseover*="${infusionLib[g('attackStatus')].id}"]`) && !gE(`#pane_effects>img[src*="${infusionLib[[g('attackStatus')]].img}"]`)) { - gE(`.bti3>div[onmouseover*="${infusionLib[g('attackStatus')].id}"]`).click(); + const onUse = function(status) { + if (getBuff(infusionLib[status].img)) return false; + const itemBtn = gE(`.bti3>div[onmouseover*="(${infusionLib[status].id})"]`); + if (!itemBtn) return false; + updateSkillOTOS(infusionLib[status].id); + itemBtn.click(); return true; } + const infusionLib = useInfusions.prototype.infusionLib ??= [ + null, { + id: 12101, + img: 'fireinfusion', + name: 'Flames', + }, { + id: 12201, + img: 'coldinfusion', + name: 'Frost', + }, { + id: 12301, + img: 'elecinfusion', + name: 'Lightning', + }, { + id: 12401, + img: 'windinfusion', + name: 'Storms', + }, { + id: 12501, + img: 'holyinfusion', + name: 'Divinity', + }, { + id: 12601, + img: 'darkinfusion', + name: 'Darkness', + } + ]; + + if (option.infusionDefaultOnly) { + const attackStatus = g().attackStatus; + if (attackStatus === 0) return false; + return onUse(attackStatus); + } + if (!option.infusion) return false; + const order = splitOrders(option.infusionOrderName, getDefaultOrder('infusionOrder')); + for (const name of order) { + const condition = option[`infusion${name}Condition`]; + const status = infusionLib.findIndex(i => i?.name === name); + const id = infusionLib[status].id; + setBattleSkillParam(id, { infusion: 1}); + if (!checkCondition(condition)) continue; + if (onUse(status)) return true; + } return false; } function autoFocus() { - if (g('option').focus && checkCondition(g('option').focusCondition)) { + const option = getOption(); + setBattleSkillParam('focus'); + if (option.focus && checkCondition(option.focusCondition)) { + updateSkillOTOS('focus'); gE('#ckey_focus').click(); return true; } return false; } - function autoSS() { - if ((g('option').turnOnSS && checkCondition(g('option').turnOnSSCondition) && !gE('#ckey_spirit[src*="spirit_a"]')) || (g('option').turnOffSS && checkCondition(g('option').turnOffSSCondition) && gE('#ckey_spirit[src*="spirit_a"]'))) { + function autoSS(isDisableOnly) { + const textSP = gE('#vrs') ?? gE('#dvrs'); + const spValue = textSP.childNodes[0].textContent * 1; + if (spValue <= 1) { + return false; + } + const option = getOption(); + const enabled = gE('#ckey_spirit[src*="spirit_a"]'); + setBattleSkillParam(enabled ? 'spiritoff' : 'spiriton', { spirit: enabled ? 'off' : 'on' }); + if ( + (!isDisableOnly && !enabled && option.turnOnSS && checkCondition(option.turnOnSSCondition)) + || (enabled && option.turnOffSS && checkCondition(option.turnOffSSCondition)) + ) { + updateSkillOTOS(enabled ? 'spiritoff' : 'spiriton'); gE('#ckey_spirit').click(); return true; } return false; } + async function clickMonster(id) { + if (!unsafeWindow.battle) { + console.log('loadUnsafeWindowBattle before click monster'); + if (!await loadUnsafeWindowBattle()) { + return; + } + } + getMonster(id).click(); + } + /** - * INNAT / WEAPON SKILLS - * - * 优先释放先天和武器技能 - */ +* INNAT / WEAPON SKILLS +* +* 优先释放先天和武器技能 +*/ function autoSkill() { - if (!g('option').skillSwitch) { - return false; - } - if (!gE('#ckey_spirit[src*="spirit_a"]')) { + const option = getOption(); + if (!option.skillSwitch) return false; + if (!option.skill) return false; + if (option.skillSSOnly && !gE('#ckey_spirit[src*="spirit_a"]')) { return false; } - - const skillOrder = (g('option').skillOrderValue || 'OFC,FRD,T3,T2,T1').split(','); + const skillOrder = splitOrders(option.skillOrderValue, getDefaultOrder('skillOrder')); + const fightStyle = g().fightingStyle; // 1二天 2单手 3双手 4双持 5法杖 const skillLib = { - OFC: { - id: '1111', - oc: 8, - }, - FRD: { - id: '1101', - oc: 4, - }, - T3: { - id: `2${g('option').fightingStyle}03`, - oc: 2, - }, - T2: { - id: `2${g('option').fightingStyle}02`, - oc: 2, - }, - T1: { - id: `2${g('option').fightingStyle}01`, - oc: 2, - }, + OFC: 1111, + FRD: 1101, + T3: fightStyle ? `2${fightStyle}03` * 1 : undefined, + T2: fightStyle ? `2${fightStyle}02` * 1 : undefined, + T1: fightStyle ? `2${fightStyle}01` * 1 : undefined, }; - const rangeSkills = { - 2101: 2, - 2403: 2, - 1111: 4, - } - const monsterStatus = g('battle').monsterStatus; + const skillInfos = autoSkill.prototype.skillInfos ??= { + 1101: { oc: 4, range: 10 }, + 1111: { oc: 8, range: 10 }, + 2101: { oc: 4, range: 5 }, + 2201: { oc: 1, }, + 2203: { oc: 4, }, + 2302: { range: 5 }, + 2303: { range: 5 }, + 2403: { oc: 3, range: 5 }, + } + const monsterStatus = g().battle.monsterStatus; for (let i in skillOrder) { let skill = skillOrder[i]; - let range = 0; - if (!checkCondition(g('option')[`skill${skill}Condition`])) { - continue; - } - if (!isOn(skillLib[skill].id)) { - continue; + if (!skill || !option.skill[skill]) { + return; } - if (g('oc') < skillLib[skill].oc) { + let id = skillLib[skill]; + if (!isOn(id)) { continue; } - if (g('option').skillOTOS && g('option').skillOTOS[skill] && g('skillOTOS')[skill] >= 1) { + if (g().oc < (skillInfos[id]?.oc ?? 2)) { continue; } - g('skillOTOS')[skill]++; - gE(skillLib[skill].id).click(); - if (skillLib[skill].id in rangeSkills) { - range = rangeSkills[skillLib[skill].id]; - } - if (!g('option').mercifulBlow || g('option').fightingStyle !== '2' || skill !== 'T3') { + const skillOTOS = getValue('skillOTOS', true) ?? {}; + skillOTOS[skill] ??= 0; + if (option.skillOTOS && option.skillOTOS[skill] && skillOTOS[skill] >= 1) { continue; } - // Merciful Blow - for (let j = 0; j < monsterStatus.length; j++) { - if (monsterStatus[j].hpNow / monsterStatus[j].hp < 0.25 && gE(`#mkey_${getMonsterID(monsterStatus[j])} img[src*="wpn_bleed"]`)) { - gE(`#mkey_${getRangeCenterID(monsterStatus[j])}`).click(); - return true; - } - } + setBattleSkillParam(id, { skill: skill }); + let target = checkCondition(option[`skill${skill}Condition`], monsterStatus); + if (!target) continue; + updateSkillOTOS(i, skillOTOS); + updateSkillOTOS(skill, skillOTOS); + gE(id).click(); + clickMonster(getRangeCenter(target, skillInfos[id]?.range ?? 1).id); + return true; } - gE(`#mkey_${getRangeCenterID(monsterStatus[0])}`).click(); - return true; + return false; } function useDeSkill() { // 自动施法DEBUFF技能 - if (!g('option').debuffSkillSwitch) { // 总开关是否开启 + const option = getOption(); + const monsterStatus = g().battle.monsterStatus; + setBattleSkillParam('debuff'); + if (!option.debuffSkillSwitch || !checkCondition(option.debuffSkillCondition, monsterStatus)) { // 总开关是否开启 return false; } + // 先处理特殊的 “先给全体上buff” - let skillPack = ['We', 'Im']; + let skillPack = splitOrders(option.debuffSkillOrderAllValue, getDefaultOrder('debuffSkillOrderAll')); for (let i = 0; i < skillPack.length; i++) { - if (g('option')[`debuffSkill${skillPack[i]}All`]) { // 是否启用 - continue; - } - if (!checkCondition(g('option')[`debuffSkill${skillPack[i]}AllCondition`])) { // 检查条件 - continue; + let buff = skillPack[i]; + if (option[`debuffSkill${buff}All`]) { // 是否启用 + const skill = monsterBuffSkillLib[buff]; + const id = skill.id*1; + setBattleSkillParam(id, { all: id, debuff: buff, debuffAll: buff }); + if (checkCondition(option[`debuffSkill${buff}AllCondition`], monsterStatus)) { // 检查条件 + continue; + } } skillPack.splice(i, 1); i--; } - skillPack.sort((x, y) => g('option').debuffSkillOrderValue.indexOf(x) - g('option').debuffSkillOrderValue.indexOf(y)) - let toAllCount = skillPack.length; - if (g('option').debuffSkill) { // 是否有启用的buff(不算两个特殊的) - skillPack = skillPack.concat(g('option').debuffSkillOrderValue.split(',')); + const toAllCount = skillPack.length; + + if (option.debuffSkill) { // 是否有启用的buff(不算两个特殊的) + skillPack = skillPack.concat(splitOrders(option.debuffSkillOrderValue, getDefaultOrder('debuffSkillOrder'))); } for (let i in skillPack) { let buff = skillPack[i]; - if (i >= toAllCount && !skillPack[i]) { // 检查buff是否启用 - continue; - } - if (!checkCondition(g('option')[`debuffSkill${buff}Condition`])) { // 检查条件 - continue; + const isToAll = i < toAllCount; + if (!isToAll) { // 非先全体 + const skill = monsterBuffSkillLib[skillPack[i]]; + const id = skill.id*1; + setBattleSkillParam(id, { debuff: buff }); + if (!buff || !option.debuffSkill[buff] || !checkCondition(option[`debuffSkill${buff}Condition`], monsterStatus)) { // 检查条件 + continue; + } } - let succeed = useDebuffSkill(skillPack[i], i < toAllCount); + let succeed = useDebuffSkill(buff, isToAll); // 前 toAllCount 个都是先给全体上的 if (succeed) { return true; @@ -3857,192 +8136,247 @@ try { } function useDebuffSkill(buff, isAll = false) { - const skillLib = { - Sle: { - name: 'Sleep', - id: '222', - img: 'sleep', - }, - Bl: { - name: 'Blind', - id: '231', - img: 'blind', - }, - Slo: { - name: 'Slow', - id: '221', - img: 'slow', - }, - Im: { - name: 'Imperil', - id: '213', - img: 'imperil', - range: { 4204: [0, 0, 0, 1] }, - }, - MN: { - name: 'MagNet', - id: '233', - img: 'magnet', - }, - Si: { - name: 'Silence', - id: '232', - img: 'silence', - }, - Dr: { - name: 'Drain', - id: '211', - img: 'drainhp', - }, - We: { - name: 'Weaken', - id: '212', - img: 'weaken', - range: { 4202: [0, 0, 0, 1] }, - }, - Co: { - name: 'Confuse', - id: '223', - img: 'confuse', - }, - }; - - if (!isOn(skillLib[buff].id)) { // 技能不可用 + const skill = monsterBuffSkillLib[buff]; + if (!isOn(skill.id)) { // 技能不可用 return false; } - const monsterStatus = g('battle').monsterStatus; - let isDebuffed = (target) => gE(`img[src*="${skillLib[buff].img}"]`, gE(`#mkey_${getMonsterID(target)}>.btm6`)); - let primaryTarget; - let max = isAll ? monsterStatus.length : 1; - for (let i = 0; i < max; i++) { - let target = buff === 'Dr' ? monsterStatus[max - i - 1] : monsterStatus[i]; - if (monsterStatus[i].isDead) { - continue; - } - if (isDebuffed(target)) { // 检查是否已有该buff + // 获取范围 + let skillRange = 1; + let ab; + const ability = getValue('ability', true); + for (ab in skill.range) { + const ranges = skill.range[ab]; + if (!ranges) { continue; } - primaryTarget = target; + skillRange = ranges[ability ? ability[ab] ?? 0 : 0]; break; } - if (primaryTarget === undefined) { - return false; + // 获取目标 + const option = getOption(); + const excludedWeight = target => resolveRPNFormula(option.excludedWeightFormula[buff], target); + let exclusiveBuffs; + if (isAll && option.debuffAllExclusive) { + exclusiveBuffs = Object.keys(option.debuffAllExclusive); + exclusiveBuffs = exclusiveBuffs?.includes(buff) ? exclusiveBuffs : undefined } - - let range = 0; - let ab; - for (ab in skillLib[buff].range) { - const ranges = skillLib[buff].range[ab][skillLib[buff].skill * 1]; - if (!ranges) { - continue; + let isDebuffed = (target, b) => { + if (b || !exclusiveBuffs) { + const current = getBuffTurnFromImg(getBuff(monsterBuffSkillLib[b ?? buff].img, getMonsterID(target))); + const threshold = option.debuffSkillThreshold ? option.debuffSkillThreshold[b ?? buff] : 0; + return threshold >= 0 && current > threshold; } - range = ranges[getValue('ability', true)[ab].level]; - break; + for (const exclusive of exclusiveBuffs) { + if (isDebuffed(target, exclusive)) return excludedWeight(target); + } + return 0; + }; + let debuffByIndex = isAll && option[`debuffSkill${buff}AllByIndex`]; + let monsterStatus = g().battle.monsterStatus; + if (debuffByIndex) { + monsterStatus = JSON.parse(JSON.stringify(monsterStatus)).sortBy(x => x.order); } - let id = getRangeCenterID(primaryTarget, range, isDebuffed); - const imgs = gE('img', 'all', gE(`#mkey_${id}>.btm6`)); + let max = isAll ? monsterStatus.length : 1; + let id; + let minWeight = Number.MAX_SAFE_INTEGER; + const condition = option[`debuffSkill${buff}${isAll ? 'All' : ''}Condition`]; + setBattleSkillParam(skill.id, { debuff: buff, ...isAll ? { all: skill.id, debuffAll: buff }: { }}); + const excludeCondition = target => checkCondition(condition, [target]) ? isDebuffed(target) : excludedWeight(target); + for (const i of range(max)) { + let target = buff === 'Dr' ? monsterStatus[max - i - 1] : monsterStatus[i]; + target = checkCondition(condition, [target]); + if (!target || target.isDead || isDebuffed(target)) continue; + const center = getRangeCenter(target, skillRange, false, excludeCondition, debuffByIndex); + if (!id || center.weight < minWeight) { + minWeight = center.weight; + id = center.id; + if (!isAll) break; // 只有覆盖全体才需要遍历全部 + } + } + if (id === undefined) { + return false; + } + const imgs = gE('img', 'all', gE(monsterStateKeys.buffs, getMonster(id))); + const buffs = Object.fromEntries(Array.from(imgs).map(img => [img.src.match(/\/y\/e\/(.*)\.png/)[1], img])); // 已有buff小于6个 // 未开启debuff失败警告 // buff剩余持续时间大于等于警报时间 - if (imgs.length < 6 || !g('option').debuffSkillTurnAlert || (g('option').debuffSkillTurn && imgs[imgs.length - 1].getAttribute('onmouseover').match(/\(.*,.*, (.*?)\)$/)[1] * 1 >= g('option').debuffSkillTurn[buff])) { - gE(skillLib[buff].id).click(); - gE(`#mkey_${id}`).click(); - return true; + if (imgs.length >= 6) { + switch (option.debuffSkillTurnAlert * 1) { + case 1: + if ((option.debuffSkillTurn && (getBuffTurnFromImg(buffs[skill.img]) ?? 0) >= option.debuffSkillTurn[buff])) { + return false; + } + UI.alert('无法正常施放DEBUFF技能,请尝试手动打怪', '無法正常施放DEBUFF技能,請嘗試手動打怪', 'Can not cast de-skills normally, continue the script?\nPlease try attack manually.'); + pauseChange(); + return true; + case 2: + break; + default: // case 0, "" or undefined + return false; + } } - - _alert(0, '无法正常施放DEBUFF技能,请尝试手动打怪', '無法正常施放DEBUFF技能,請嘗試手動打怪', 'Can not cast de-skills normally, continue the script?\nPlease try attack manually.'); - pauseChange(); + updateSkillOTOS(skill.id); + gE(skill.id).click(); + clickMonster(id); return true; } - function attack() { // 自动打怪 - // 如果 - // 1. 开启了自动以太水龙头 - // 2. 目标怪在魔力合流状态中 - // 3. 未获得以太水龙头*2 或 *1 - // 4. 满足条件 - // 使用物理普通攻击,跳过Offensive Magic - // 否则按照属性攻击模式释放Spell > Offensive Magic + function getCurrentAttackStatus() { + let current = g().attackStatusCurrent; + if (current === undefined) { // first stack of condition + attack(true); + current = g().attackStatusCurrent + g('attackStatusCurrent', undefined); + } + return current; + } + + function attack(selectStatusOnly = false) { // 自动打怪 + const option = getOption(); + const monsters = g().battle.monsterStatus; + if (option.attackStatusSwitch) { + let tier = option.attackStatusSwitchByTier ? 3 : undefined; + const order = splitOrders(option.attackStatusOrderValue, getDefaultOrder('attackStatusOrder')); + while (tier === undefined || tier-- !== 0) { + for (const status of order) { + if (!status && tier) continue; + if (!option.attackStatusSwitch[status]) continue; + g('attackStatusCurrent', status); + setBattleSkillParam('switch', { tier }); + if (!checkCondition(option[`attackStatusSwitchCondition${status}`], monsters)) continue; + if (onAttack(status, selectStatusOnly, tier)) return true; + } + if (tier === undefined) break; + } + } + g('attackStatusCurrent', g().attackStatus); + return onAttack(g().attackStatus, selectStatusOnly); + } - const updateAbility = { + function onAttack(attackStatus, selectStatusOnly = false, tier = undefined) { + const updateAbility = onAttack.prototype.updateAbility ??= { 4301: { //火 - 111: [0, 1, 1, 2, 2, 2, 2, 2], - 112: [0, 0, 2, 2, 2, 2, 3, 3], - 113: [0, 0, 0, 0, 3, 4, 4, 4] + 111: [3, 4, 4, 5, 5, 5, 5, 5], + 112: [4, 4, 6, 6, 6, 6, 7, 7], + 113: [7, 7, 7, 7, 8, 9, 9, 10] }, 4302: { //冰 - 121: [0, 1, 1, 2, 2, 2, 2, 2], - 122: [0, 0, 2, 2, 2, 2, 3, 3], - 123: [0, 0, 0, 0, 3, 4, 4, 4] + 121: [3, 4, 4, 5, 5, 5, 5, 5], + 122: [4, 4, 6, 6, 6, 6, 7, 7], + 123: [7, 7, 7, 7, 8, 9, 9, 10] }, 4303: { //雷 - 131: [0, 1, 1, 2, 2, 2, 2, 2], - 132: [0, 0, 2, 2, 2, 2, 3, 3], - 133: [0, 0, 0, 0, 3, 4, 4, 4] + 131: [3, 4, 4, 5, 5, 5, 5, 5], + 132: [4, 4, 6, 6, 6, 6, 7, 7], + 133: [7, 7, 7, 7, 8, 9, 9, 10] }, 4304: { //雷 - 141: [0, 1, 1, 2, 2, 2, 2, 2], - 142: [0, 0, 2, 2, 2, 2, 3, 3], - 143: [0, 0, 0, 0, 3, 4, 4, 4] + 141: [3, 4, 4, 5, 5, 5, 5, 5], + 142: [4, 4, 6, 6, 6, 6, 7, 7], + 143: [7, 7, 7, 7, 8, 9, 9, 10] }, //暗 - 4401: { 161: [0, 1, 2] }, - 4402: { 162: [0, 2, 3] }, - 4403: { 163: [0, 3, 4, 4] }, + 4401: { 161: [3, 4, 5] }, + 4402: { 162: [5, 6, 7] }, + 4403: { 163: [7, 8, 9, 10] }, //圣 - 4501: { 151: [0, 1, 2] }, - 4502: { 152: [0, 2, 3] }, - 4503: { 153: [0, 3, 4, 4] }, + 4501: { 151: [3, 4, 5] }, + 4502: { 152: [5, 6, 7] }, + 4503: { 153: [7, 8, 9, 10] }, } - let range = 0; - // Spell > Offensive Magic - const attackStatus = g('attackStatus'); - const monsterStatus = g('battle').monsterStatus; + const option = getOption(); + const battle = g().battle; + const monsters = battle.monsterStatus; + let target = monsters[0]; + + // 如果 + // 1. 开启了自动以太之触 + // 2. 目标怪在魔力合流状态中 + // 3. 满足条件 + // 使用物理普通攻击,跳过Offensive Magic + // 否则按照属性攻击模式释放Spell > Offensive Magic + let skillRange = 1, skill = 0, attackTier = 0; + const fightingStyle = g().fightingStyle*1; + // 1. physical if (attackStatus === 0) { - if (g('option').fightingStyle === '1') { // 二天一流 - range = 1; - } - } else { - if (g('option').etherTap && gE(`#mkey_${getMonsterID(monsterStatus[0])}>div.btm6>img[src*="coalescemana"]`) && (!gE('#pane_effects>img[onmouseover*="Ether Tap (x2)"]') || gE('#pane_effects>img[src*="wpn_et"][id*="effect_expire"]')) && checkCondition(g('option').etherTapCondition)) { - `pass` - } - else { - const skill = 1 * (() => { - let lv = 3; - for (let condition of [g('option').highSkillCondition, g('option').middleSkillCondition, undefined]) { - let id = `1${attackStatus}${lv--}`; - if (checkCondition(condition) && isOn(id)) return id; - } - })(); - gE(skill)?.click(); - for (let ab in updateAbility) { - const ranges = updateAbility[ab][skill]; - if (!ranges) { - continue; - } - range = ranges[getValue('ability', true)[ab]?.level ?? 0]; - break; - } + skillRange = fightingStyle === 1 ? 3 : 1; + return tryAttack(); + } + // 2. etherTap + if (option.etherTap && getBuff('coalescemana', getMonsterID(target))) { + const expiring = [getBuffStackFromImg, getBuffTurnFromImg].map(getter => getter(getBuff('wpn_et')) <= 1).reduce((acc, cur) => acc || cur); + setBattleSkillParam('etherTap', { tier }); + if (expiring && checkCondition(option.etherTapCondition)) { + return tryAttack(); + } + } + // 3.0 try check skill condition + skill = 1 * (() => { + const conditions = [option.lowSkillCondition, option.middleSkillCondition, option.highSkillCondition]; + for (const lv of range(tier ?? 2, -1, -1)) { + let id = `1${attackStatus}${lv + 1}` * 1; + attackTier = lv+1; + setBattleSkillParam(id, { attackTier }); + if (isOn(id) && (target = checkCondition(conditions[lv], monsters))) return id; + if (tier) return 0; } + })(); + // 3.a no skill available + if (!skill) { + attackTier = 0; + if (tier) return false; + return tryAttack(); + } + // 3.b cast skill + for (let ab in updateAbility) { + const ranges = updateAbility[ab][skill]; + if (!ranges) continue; + const ability = getValue('ability', true); + skillRange = ranges[ability ? ability[ab] ?? 0 : 0]; + break; } - gE(`#mkey_${getRangeCenterID(monsterStatus[0], range, !attackStatus)}`).click(); - return true; + return tryAttack(skill); + + function tryAttack (skill=0) { + if (!target || target.isDead) { + return false; + } + if (selectStatusOnly) { + return true; + } + if (skill && gE(skill)) { + updateSkillOTOS(skill); + updateSkillOTOS(`attackStatus${attackStatus}`); + updateSkillOTOS(`attackTier${attackTier}`); + gE(skill).click(); + } + setBattleSkillParam(skill, { attackTier }); + clickMonster(getRangeCenter(target, skillRange ?? 1, !attackStatus).id); + return true; + }; } + function updateSkillOTOS(id, skillOTOS) { + skillOTOS ??= getValue('skillOTOS', true) ?? {}; + skillOTOS[id] ??= 0; + skillOTOS[id]++; + return setValue('skillOTOS', skillOTOS); + } + + // TODO TBD 根据lv模糊推测(一般数据都是等级逐渐提升的,可能可以直接用缓存而不需要推测,异世界新赛季时可以自动刷新缓存?) function getHPFromMonsterDB(mdb, name, lv) { - let hp = (mdb && mdb[name]) ? mdb[name][lv] : undefined; - // TODO: 根据lv模糊推测 - return hp; + return mdb?.[name]?.[lv] } function fixMonsterStatus() { // 修复monsterStatus - // document.title = _alert(-1, 'monsterStatus错误,正在尝试修复', 'monsterStatus錯誤,正在嘗試修復', 'monsterStatus Error, trying to fix'); + // document.title = UI.byLang('monsterStatus错误,正在尝试修复', 'monsterStatus錯誤,正在嘗試修復', 'monsterStatus Error, trying to fix'); const monsterStatus = []; - const monsterNames = Array.from(gE('div.btm3>div>div', 'all')).map(monster => monster.innerText); - const monsterLvs = Array.from(gE('div.btm2>div>div', 'all')).map(monster => monster.innerText); + const monsterNames = Array.from(gE(`${monsterStateKeys.name}>div>div`, 'all')).map(monster => monster.innerText); + const monsterLvs = Array.from(gE(`${monsterStateKeys.lv}>div>div`, 'all')).map(monster => monster.innerText); const monsterDB = getValue('monsterDB', true); - gE('div.btm2', 'all').forEach((monster, order) => { + gE(monsterStateKeys.lv, 'all').forEach((monster, order) => { monsterStatus.push({ order: order, hp: getHPFromMonsterDB(monsterDB, monsterNames[order], monsterLvs[order]) ?? ((monster.style.background === '') ? 1000 : 100000), @@ -4055,7 +8389,7 @@ try { function displayMonsterWeight() { - const status = g('battle').monsterStatus.filter(m => !m.isDead); + const status = g().battle.monsterStatus.filter(m => !m.isDead); let rank = 0; const weights = []; @@ -4064,44 +8398,41 @@ try { return; } weights.push(s.finWeight); - }) + }); const sec = Math.max(1, weights.length - 1); const max = 360 * 2 / 3; const colorTextList = []; - if (g('option').weightBackground) { - status.forEach(s => { - const rank = weights.indexOf(s.finWeight); - let colorText = (g('option').weightBackground[rank + 1] ?? [])[0]; - colorTextList[rank] = colorText; - }); + const option = getOption(); + const weightBG = option.weightBackground; + if (weightBG) { + for (const i of range(weights)) { + colorTextList[i] = weightBG[i]; + } } status.forEach(s => { const rank = weights.indexOf(s.finWeight); const id = getMonsterID(s); - if (!gE(`#mkey_${id}`) || !gE(`#mkey_${id}>.btm3`)) { + if (!getMonster(id) || !gE(monsterStateKeys.name, getMonster(id))) { return; } - if (g('option').displayWeightBackground) { - if (g('option').weightBackground) { - let colorText = colorTextList[rank]; - let remainAttemp = 10; // 避免无穷递归 - while(remainAttemp > 0 && colorText && colorText.indexOf(``, colorTextList[i]); - } - remainAttemp--; - } - try { - colorText = eval(colorText.replace('', rank).replace('', weights.length)); - } - catch { + if (option.displayWeightBackground && weightBG) { + let colorText = colorTextList[rank]; + let remainAttemp = 10; // 避免无穷递归 + while (remainAttemp > 0 && colorText && colorText.indexOf(``, colorTextList[i]); } - gE(`#mkey_${id}`).style.cssText += `background: ${colorText};`; + remainAttemp--; } + try { + colorText = eval(colorText.replace('', rank).replace('', weights.length)); + } + catch { } + getMonster(id).style.cssText += `background: ${colorText};`; } - gE(`#mkey_${id}>.btm3`).style.cssText += 'display: flex; flex-direction: row;' - if (g('option').displayWeight) { - gE(`#mkey_${id}>.btm3`).innerHTML += `
      [${rank}|-${-rank + weights.length - 1}|${s.finWeight.toPrecision(s.finWeight >= 1 ? 5 : 4)}]
      `; + gE(monsterStateKeys.name, getMonster(id)).style.cssText += 'display: flex; flex-direction: row;' + if (option.displayWeight) { + gE(monsterStateKeys.name, getMonster(id)).innerHTML += `
      [${rank}|-${-rank + weights.length - 1}|${s.finWeight.toPrecision(s.finWeight >= 1 ? 5 : 4)}]
      `; } }); } @@ -4111,40 +8442,45 @@ try { const barMP = gE('#vbm') ?? gE('#dvbm'); const barSP = gE('#vbs') ?? gE('#dvbs'); const barOC = gE('#dvbc'); - const textHP = gE('#vrhd') ?? gE('#dvrhd'); + const textHP = gE('#vrhd') ?? gE('#dvrhd') ?? gE('#dvrhb'); const textMP = gE('#vrm') ?? gE('#dvrm'); const textSP = gE('#vrs') ?? gE('#dvrs'); const textOC = gE('#dvrc'); - - const percentages = [barHP, barMP, barSP, barOC].filter(bar => bar).map(bar => Math.floor((gE('div>img', bar).offsetWidth / bar.offsetWidth) * 100)); + const barWidth = gE('#dvbc') ? [418, 418, 418, 418] : [496, 207, 207, undefined] + const percentages = [barHP, barMP, barSP, barOC].filter(bar => bar).map((bar, i) => Math.floor((gE('div>img', bar).offsetWidth / barWidth[i]) * 100)); [textHP, textMP, textSP, textOC].filter(bar => bar).forEach((text, i) => { - const value = text.innerHTML * 1; - const percentage = value ? percentages[i] : 0; - const inner = `[${percentage.toString()}%]`; + text.style.cssText += textOC ? ` +display: grid; +grid-template-columns: 1fr 1fr; +width: 120px; +`: ""; const percentageDiv = gE('div', text); + const style = ` +position: relative; +top: ${textOC ? 0 : text === textHP ? -16.67 : -16}px; +right: ${textOC ? -10 : text === textMP ? -60 : text === textSP ? 40 : -100}px; +filter: brightness(0.2); +text-align: left; +` + const inner = `[${percentages[i].toString()}%]`; if (percentageDiv) { percentageDiv.innerHTML = inner; + percentageDiv.style.cssText = style; return; } - text.innerHTML += `
      ${inner}
      ` + text.innerHTML += `
      ${inner}
      ` }); } - function dropMonitor(battleLog) { // 掉落监测 + function dropMonitor(battleLog, different) { // 掉落监测 const drop = getValue('drop', true) || { '#startTime': time(3), '#EXP': 0, '#Credit': 0, }; - let item; let name; let amount; let - regexp; - for (let i = 0; i < battleLog.length; i++) { + const option = getOption(); + let item, name, amount, regexp; + for (const i of range(battleLog)) { if (/^You gain \d+ (EXP|Credit)/.test(battleLog[i].textContent)) { regexp = battleLog[i].textContent.match(/^You gain (\d+) (EXP|Credit)/); if (regexp) { @@ -4155,7 +8491,7 @@ try { name = item.textContent.match(/^\[(.*?)\]$/)[1]; if (item.style.color === 'rgb(255, 0, 0)') { const quality = ['Crude', 'Fair', 'Average', 'Superior', 'Exquisite', 'Magnificent', 'Legendary', 'Peerless']; - for (let j = g('option').dropQuality; j < quality.length; j++) { + for (const j of range(option.dropQuality, quality)) { if (name.match(quality[j])) { name = `Equipment of ${name.match(/^\w+/)[0]}`; drop[name] = (name in drop) ? drop[name] + 1 : 1; @@ -4181,10 +8517,10 @@ try { break; } } - const battle = g('battle'); - if (g('option').recordEach && battle.roundNow === battle.roundAll) { + const battle = g().battle; + if (option.recordEach && (different || battle.roundNow === battle.roundAll)) { const old = getValue('dropOld', true) || []; - drop.__name = getValue('battleCode'); + drop.__name = getValue('battleCode', true).name; drop['#endTime'] = time(3); old.push(drop); setValue('dropOld', old); @@ -4192,130 +8528,217 @@ try { } else { setValue('drop', drop); } + if (getComputedStyle(gE('#hvAATab-Drop')).display === 'block') { + gE(`.hvAATabmenu>span[name="Drop"]`).click(); + } } - function recordUsage(parm) { - const stats = getValue('stats', true) || { - self: { - _startTime: time(3), - _turn: 0, - _round: 0, - _battle: 0, - _monster: 0, - _boss: 0, - evade: 0, - miss: 0, - focus: 0, - }, - restore: { // 回复量 - }, - items: { // 物品使用次数 - }, - magic: { // 技能使用次数 - }, - damage: { // 技能攻击造成的伤害 - }, - hurt: { // 受到攻击造成的伤害 - mp: 0, - oc: 0, - _avg: 0, - _count: 0, - _total: 0, - _mavg: 0, - _mcount: 0, - _mtotal: 0, - _pavg: 0, - _pcount: 0, - _ptotal: 0, - }, - proficiency: { // 熟练度 - }, - }; - let text; let magic; let point; let - reg; - const battle = g('battle'); - if (g('monsterAlive') === 0) { - stats.self._turn += battle.turn; - stats.self._round += 1; - if (battle.roundNow === battle.roundAll) { - stats.self._battle += 1; - } - } - if (parm.mode === 'magic') { - magic = parm.magic; - stats.magic[magic] = (magic in stats.magic) ? stats.magic[magic] + 1 : 1; - stats.hurt.mp += parm.mp; - stats.hurt.oc += parm.oc; - } else if (parm.mode === 'items') { - stats.items[parm.item] = (parm.item in stats.items) ? stats.items[parm.item] + 1 : 1; + function matchDamageInfoFromLogText(text, isSkipUnmatched = true) { + const regList = [ + /you for (\d+) (\w+) damage/, + /and take (\d+) (\w+) damage/, + /You take (\d+) (\w+) damage/, + /hits you, causing (\d+) points of (\w+) damage/ + ]; + for (let reg of regList) { + let match = text.match(reg); + if (!match) { + continue; + } + return match; + } + if (!isSkipUnmatched) { + console.log(`Can't match damage info from: `, text); + } + } + + function recordUsage(param) { + const filter = getOption().record; + if (!filter) return; + let stats = getValue('stats', true) || {}; + const battle = g().battle; + stats.self ??= { _startTime: time(3) }; + stats.tokens ??= { token: battle.token, postoken: battle.postoken }; + stats.self._turn = filter.turn ? stats.self._turn ?? 0 : undefined; + stats.self._round = filter.round ? stats.self._round ?? 0 : undefined; + stats.self._battle = filter.battle ? stats.self._battle ?? 0 : undefined; + stats.self._monster = filter.monster ? stats.self._monster ?? 0 : undefined; + stats.self._boss = filter.boss ? stats.self._boss ?? 0 : undefined; + stats.self.evade = filter.evade ? stats.self.evade ?? 0 : undefined; + stats.self.miss = filter.miss ? stats.self.miss ?? 0 : undefined; + stats.self.focus = filter.focus ? stats.self.focus ?? 0 : undefined; + stats.self.mp = filter.mp ? stats.self.mp ?? 0 : undefined; + stats.self.oc = filter.oc ? stats.self.oc ?? 0 : undefined; + stats.restore = filter.restore ? stats.restore ?? {} : undefined; // 回复量 + stats.items = filter.items ? stats.items ?? {} : undefined; // 物品使用次数 + stats.magic = filter.magic ? stats.magic ?? {} : undefined; // 技能使用次数 + stats.damage = filter.damage ? stats.damage ?? {} : undefined; // 技能攻击造成的伤害 + stats.proficiency = filter.proficiency ? stats.proficiency ?? {} : undefined; // 熟练度 + stats.hurt = filter.hurt ? stats.hurt ?? {} : undefined; // 受到攻击造成的伤害 + if (filter.hurt) { + stats.hurt._avg = filter.hurtavg ? stats.hurt._avg ?? 0 : undefined; + stats.hurt._count = filter.hurtcount ? stats.hurt._count ?? 0 : undefined; + stats.hurt._total = filter.hurttotal ? stats.hurt._total ?? 0 : undefined; + stats.hurt._mavg = filter.hurtmavg ? stats.hurt._mavg ?? 0 : undefined; + stats.hurt._mcount = filter.hurtmcount ? stats.hurt._mcount ?? 0 : undefined; + stats.hurt._mtotal = filter.hurtmtotal ? stats.hurt._mtotal ?? 0 : undefined; + stats.hurt._pavg = filter.hurtpavg ? stats.hurt._pavg ?? 0 : undefined; + stats.hurt._pcount = filter.hurtpcount ? stats.hurt._pcount ?? 0 : undefined; + stats.hurt._ptotal = filter.hurtptotal ? stats.hurt._ptotal ?? 0 : undefined; + } + let text, magic, magicName, item, itemName, point, reg; + if (g().monsterAlive === 0) { + if (filter.turn) { + stats.self._turn += battle.turn; + } + if (filter.round) { + stats.self._round += 1; + } + if (filter.battle) { + if (battle.roundNow === battle.roundAll) { + stats.self._battle += 1; + } + } + } + if (param.mode === 'magic') { + [magic, magicName] = [param.magic, param.magicName]; + if (filter.magic) { + let prev = stats.magic[magic] ?? 0; + if (magicName in stats.magic) { + prev += stats.magic[magicName]; + delete stats.magic[magicName]; + } + stats.magic[magic] = prev + 1; + (stats.magicNames ??= {})[magic] = magicName; + } + if (filter.mp) { + stats.self.mp += param.mp; + } + if (filter.oc) { + stats.self.oc += param.oc; + } + } else if (param.mode === 'items') { + if (filter.items) { + [item, itemName] = [param.item, param.itemName]; + let prev = stats.items[item] ?? 0; + if (itemName in stats.items) { + prev += stats.items[itemName]; + delete stats.items[itemName]; + } + stats.items[item] = prev + 1; + (stats.itemsNames ??= {})[item] = itemName; + } } else { - stats.self[parm.mode] = (parm.mode in stats.self) ? stats.self[parm.mode] + 1 : 1; + if (filter[param.mode]) { + stats.self[param.mode] = (param.mode in stats.self) ? stats.self[param.mode] + 1 : 1; + } } + const debug = false; let log = false; - for (let i = 0; i < parm.log.length; i++) { - if (parm.log[i].className === 'tls') { + for (const i of range(param.log)) { + if (param.log[i].className === 'tls') { break; } - text = parm.log[i].textContent; + text = param.log[i].textContent; if (debug) { console.log(text); } - if (text.match(/you for \d+ \w+ damage/)) { - reg = text.match(/you for (\d+) (\w+) damage/); + if (reg = matchDamageInfoFromLogText(text)) { magic = reg[2].replace('ing', ''); point = reg[1] * 1; - stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point; - stats.hurt._count++; - stats.hurt._total += point; - stats.hurt._avg = Math.round(stats.hurt._total / stats.hurt._count); - if (magic.match(/pierc|crush|slash/)) { - stats.hurt._pcount++; - stats.hurt._ptotal += point; - stats.hurt._pavg = Math.round(stats.hurt._ptotal / stats.hurt._pcount); - } else { - stats.hurt._mcount++; - stats.hurt._mtotal += point; - stats.hurt._mavg = Math.round(stats.hurt._mtotal / stats.hurt._mcount); + if (filter.hurt) { + stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point; + if (filter.hurtcount || filter.hurtavg) { + stats.hurt._count++; + } + if (filter.hurttotal || filter.hurtavg) { + stats.hurt._total += point; + } + if (filter.hurtavg) { + stats.hurt._avg = Math.round(stats.hurt._total / stats.hurt._count); + } + if (magic.match(/pierc|crush|slash/)) { + if (filter.hurtpcount || filter.hurtpavg) { + stats.hurt._pcount++; + } + if (filter.hurtptotal || filter.hurtpavg) { + stats.hurt._ptotal += point; + } + if (filter.hurtpavg) { + stats.hurt._pavg = Math.round(stats.hurt._ptotal / stats.hurt._pcount); + } + } else { + if (filter.hurtmcount || filter.hurtmavg) { + stats.hurt._mcount++; + } + if (filter.hurtmtotal || filter.hurtmavg) { + stats.hurt._mtotal += point; + } + if (filter.hurtmavg) { + stats.hurt._mavg = Math.round(stats.hurt._mtotal / stats.hurt._mcount); + } + } + } + if (filter.evade && text.match(/You ((partially )*(evade|parry|block)( and )*)+ the attack/)) { + stats.self.evade++; } } else if (text.match(/^[\w ]+ [a-z]+s [\w+ -]+ for \d+( .*)? damage/) || text.match(/^You .* for \d+ .* damage/)) { - reg = text.match(/for (\d+)( .*)? damage/); - magic = text.match(/^[\w ]+ [a-z]+s [\w+ -]+ for/) ? text.match(/^([\w ]+) [a-z]+s [\w+ -]+ for/)[1].replace(/^Your /, '') : text.match(/^You (\w+)/)[1]; - point = reg[1] * 1; - stats.damage[magic] = (magic in stats.damage) ? stats.damage[magic] + point : point; + if (filter.damage) { + reg = text.match(/for (\d+)( .*)? damage/); + magic = text.match(/^[\w ]+ [a-z]+s [\w+ -]+ for/) ? text.match(/^([\w ]+) [a-z]+s [\w+ -]+ for/)[1].replace(/^Your /, '') : text.match(/^You (\w+)/)[1]; + point = reg[1] * 1; + stats.damage[magic] = (magic in stats.damage) ? stats.damage[magic] + point : point; + } } else if (text.match(/Vital Theft hits .*? for \d+ damage/)) { - magic = 'Vital Theft'; - point = text.match(/Vital Theft hits .*? for (\d+) damage/)[1] * 1; - stats.damage[magic] = (magic in stats.damage) ? stats.damage[magic] + point : point; + if (filter.damage) { + magic = 'Vital Theft'; + point = text.match(/Vital Theft hits .*? for (\d+) damage/)[1] * 1; + stats.damage[magic] = (magic in stats.damage) ? stats.damage[magic] + point : point; + } } else if (text.match(/You (evade|parry|block) the attack|misses the attack against you|(casts|uses) .* misses the attack/)) { - stats.self.evade++; + if (filter.evade) { + stats.self.evade++; + } } else if (text.match(/(resists your spell|Your spell is absorbed|(evades|parries) your (attack|spell))|Your attack misses its mark|Your spell fails to connect/)) { - stats.self.miss++; + if (filter.miss) { + stats.self.miss++; + } } else if (text.match(/You gain the effect Focusing/)) { - stats.self.focus++; + if (filter.focus) { + stats.self.focus++; + } } else if (text.match(/^Recovered \d+ points of/) || text.match(/You are healed for \d+ Health Points/) || text.match(/You drain \d+ HP from/)) { - magic = (parm.mode === 'defend') ? 'defend' : text.match(/You drain \d+ HP from/) ? 'drain' : parm.magic || parm.item; - point = text.match(/\d+/)[0] * 1; - stats.restore[magic] = (magic in stats.restore) ? stats.restore[magic] + point : point; + if (filter.restore) { + magic = (param.mode === 'defend') ? 'defend' : text.match(/You drain \d+ HP from/) ? 'drain' : param.magic || param.item; + point = text.match(/\d+/)[0] * 1; + stats.restore[magic] = (magic in stats.restore) ? stats.restore[magic] + point : point; + } } else if (text.match(/(restores|drain) \d+ points of/)) { - reg = text.match(/^(.*) restores (\d+) points of (\w+)/) || text.match(/^You (drain) (\d+) points of (\w+)/); - magic = reg[1]; - point = reg[2] * 1; - stats.restore[magic] = (magic in stats.restore) ? stats.restore[magic] + point : point; + if (filter.restore) { + reg = text.match(/^(.*) restores (\d+) points of (\w+)/) || text.match(/^You (drain) (\d+) points of (\w+)/); + magic = reg[1]; + point = reg[2] * 1; + stats.restore[magic] = (magic in stats.restore) ? stats.restore[magic] + point : point; + } } else if (text.match(/absorbs \d+ points of damage from the attack into \d+ points of \w+ damage/)) { - reg = text.match(/(.*) absorbs (\d+) points of damage from the attack into (\d+) points of (\w+) damage/); - point = reg[2] * 1; - magic = parm.log[i - 1].textContent.match(/you for (\d+) (\w+) damage/)[2].replace('ing', ''); - stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point; - point = reg[3] * 1; - magic = `${reg[1].replace('Your ', '')}_${reg[4]}`; - stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point; + if (filter.hurt) { + reg = text.match(/(.*) absorbs (\d+) points of damage from the attack into (\d+) points of (\w+) damage/); + point = reg[2] * 1; + magic = matchDamageInfoFromLogText(param.log[i - 1].textContent, false)[2].replace('ing', ''); + stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point; + point = reg[3] * 1; + magic = `${reg[1].replace('Your ', '')}_${reg[4]}`; + stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point; + } } else if (text.match(/You gain .* proficiency/)) { - reg = text.match(/You gain ([\d.]+) points of (.*?) proficiency/); - magic = reg[2]; - point = reg[1] * 1; - stats.proficiency[magic] = (magic in stats.proficiency) ? stats.proficiency[magic] + point : point; - stats.proficiency[magic] = stats.proficiency[magic].toFixed(3) * 1; + if (filter.proficiency) { + reg = text.match(/You gain ([\d.]+) points of (.*?) proficiency/); + magic = reg[2]; + point = reg[1] * 1; + stats.proficiency[magic] = (magic in stats.proficiency) ? stats.proficiency[magic] + point : point; + stats.proficiency[magic] = stats.proficiency[magic].toFixed(3) * 1; + } } else if (text.trim() === '' || text.match(/You (gain |cast |use |are Victorious|have reached Level|have obtained the title|do not have enough MP)/) || text.match(/Cooldown|has expired|Spirit Stance|gains the effect|insufficient Spirit|Stop beating dead ponies| defeat |Clear Bonus|brink of defeat|Stop \w+ing|Spawned Monster| drop(ped|s) |defeated/)) { // nothing; } else if (debug) { @@ -4329,25 +8752,36 @@ try { pauseChange(); } setValue('stats', stats); + if (getComputedStyle(gE('#hvAATab-Usage')).display === 'block') { + gE(`.hvAATabmenu>span[name="Usage"]`).click(); + } } - function recordUsage2() { + function recordUsage2(different) { + const option = getOption(); + const filter = option.record; + if (!filter) return; const stats = getValue('stats', true); - stats.self._monster += g('monsterAll'); - stats.self._boss += g('bossAll'); - const battle = g('battle'); - if (g('option').recordEach && battle.roundNow === battle.roundAll) { + if (!different) { + if (filter.monster) stats.self._monster += g().monsterAll; + if (filter.boss) stats.self._boss += g().bossAll; + } + const battle = g().battle; + if (option.recordEach && (different || battle.roundNow === battle.roundAll)) { const old = getValue('statsOld', true) || []; - stats.__name = getValue('battleCode'); + stats.__name = getValue('battleCode', true)?.name; stats.self._endTime = time(3); old.push(stats); setValue('statsOld', old); delValue('stats'); - } else { - setValue('stats', stats); + return; + } + setValue('stats', stats); + if (getComputedStyle(gE('#hvAATab-Usage')).display === 'block') { + gE(`.hvAATabmenu>span[name="Usage"]`).click(); } } -} catch (e) { - console.log(e); - document.title = e; -} +} catch (err) { + console.error(err); + document.title = err; +}})();