diff --git a/HentaiVerse/hvAutoAttack/README.md b/HentaiVerse/hvAutoAttack/README.md
index df72b5e5..89052520 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. `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`、`_targetBuffTurn`、`_targetRank`、`_targetOrder`、`_targetWeight`、`_targetIsAlive`: 目标怪物的HP%、SP%、MP%、HP%(小数形式)、SP%(小数形式)、MP%(小数形式)、buff剩余时间、优先级、位置、当前权重、是否存活
+ 1. `_targetBuffTurn_`后缀参照8.`buffTurn`(如:`_targetBuffTurn_bleed != 0`表示目标bleed的buff剩余回合不等于0)。target的目标怪物遵循以下规则
+ 1. 默认情况的target均为权重优先级最高的目标
+ 2. 武器技能(马炮、T1~T3等)、法术技能(中阶、高阶):按照 逐条条件判断>按权重逐个目标>满足任意一条条件内的所有子条目,则对该目标释放。例如下图最后的慈悲的条件:仅释放hp小于25%、拥有流血buff
+ 
+
+ 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 |
@@ -162,6 +208,14 @@
| Infusion of Storms / windinfusion | Infusion of Divinity / holyinfusion | Infusion of Darkness / darkinfusion |
| Scroll of Swiftness / haste_scroll | - | - |
| Flower Vase / flowers | Bubble-Gum / gum | - |
+| 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 |
+| Turbulent Air / windmiss | Deep Burns / elecweak | Breached Defense / holybreach |
+| Blunted Attack / darknerf | Burning Soul / soulfire | Ripened Soul / ripesoul |
***
@@ -382,3 +436,20 @@
灵感来自hoverplay,刚开始接触js,初步完成代码
功能有:答题警报、其他警报、快捷键、自动前进、自动使用宝石、自动回复、自动使用增益技能、自动打怪
很可惜,玩游戏不走心,一直搞不懂HVSTAT是怎么知道每个怪的血量的,直到[版本2.0](#20)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HentaiVerse/hvAutoAttack/README_en.md b/HentaiVerse/hvAutoAttack/README_en.md
index c44fef8f..5ee74a6f 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. `buffTurn`: 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`/`_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 `_targetBuffTurn_` is same as 10.`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.
+
+ 
+
+ 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 |
@@ -156,6 +204,14 @@ The following is a schematic diagram of the circuit diagram
| Infusion of Storms / windinfusion | Infusion of Divinity / holyinfusion | Infusion of Darkness / darkinfusion |
| Scroll of Swiftness / haste_scroll | - | - |
| Flower Vase / flowers | Bubble-Gum / gum | - |
+| 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 |
+| Turbulent Air / windmiss | Deep Burns / elecweak | Breached Defense / holybreach |
+| Blunted Attack / darknerf | Burning Soul / soulfire | Ripened Soul / ripesoul |
***
@@ -188,3 +244,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..5abf98f2 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.11
// @author dodying
// @namespace https://github.com/dodying/
// @supportURL https://github.com/dodying/UserJs/issues
@@ -28,3058 +28,5289 @@
// @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;
- }
+
+(function () {
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';
- }
- },
- realtime: false,
- logList: [],
- maxLogCache: 100,
- switchRealtimeLog: function () {
- Debug.enableRealtimeLog(Debug.realtime);
- },
- enableRealtimeLog: function (enabled) {
- Debug.realtime = enabled;
- if (enabled) {
- Debug.shiftLog();
- }
- },
- 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();
- }
- },
- shiftLog: function () {
- while (Debug.logList.length) {
- const log = Debug.logList.shift();
- console.log(...log.args, `\n`, log.stack);
+ 'use strict';
+ const standalone = ['option', 'arena', 'lastUrl', 'ability', 'proficiency', 'stamina', 'drop', 'stats', 'battleCode', 'disabled', 'stepIn', 'battle', 'monsterDB', 'monsterMID', 'skillOTOS', 'onriddle'];
+ const local = ['stamina', 'drop', 'stats', 'dropOld', 'statsOld', 'battleCode', 'disabled', 'stepIn', 'battle', 'monsterDB', 'monsterMID', 'skillOTOS', 'onriddle'];
+ const portable = ['drop', 'stats', 'dropOld', 'statsOld', 'monsterDB', 'monsterMID']
+ const sharable = ['option'];
+ const excludeStandalone = { 'option': ['optionStandalone', 'version', 'lang'] };
+ const location = window.location.href;
+ const isFrame = window.self !== window.top;
+
+ const isIsekai = location.indexOf('isekai') !== -1;
+ const current = isIsekai ? 'isekai' : 'persistent';
+ const other = isIsekai ? 'persistent' : 'isekai';
+ const isEquipDetail = window.location.href.includes('/equip/');
+ const isMaintaining = !gE('#csp') && !isEquipDetail;
+
+ let ability = getValue('ability', true)??{};
+ let lastResponsive = new Date().getTime();
+
+ const scriptVersion = Version(GM_info ? GM_info.script.version : '2.91');
+ let hvVersion = Version(gE('script[src*="hvc.js"]', document)?.src.match(/z\/(\d+)(.*)\/hvc.js/)?.slice(1,2));
+
+ const _1s = 1000;
+ const _1m = 60 * _1s;
+ const _1h = 60 * _1m;
+ const _1d = 24 * _1h;
+ const attackStatusType = [
+ '物理 物理 Physical ',
+ '火 火 Fire ',
+ '冰 冰 Cold ',
+ '雷 雷 Elec ',
+ '风 風 Wind ',
+ '圣 聖 Divine ',
+ '暗 暗 Forbidden ',
+ ];
+ const monsterStateKeys = { obj: `div.btm1`, lv: `div.btm2`, name: `div.btm3`, bars: `div.btm4>div.btm5`, buffs: `div.btm6` };
+ let monsterBuffSkillLib;
+ 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.'"
+ },
+ 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.'"
+ },
+ 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.'"
+ },
+ 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: 'MagNet',
+ 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',
+ 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.'"
+ },
+ // from monster ------------ ?? permanent or 800?
+ 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',
+ },
+
+ CF: {
+ name: 'Cloak of the Fallen',
+ id: 422,
+ img: 'fallenshield',
}
- }
- }
+ };
- const asyncList = [];
- function consoleAsyncTasks(name, state) {
- if (!state) {
- asyncList.splice(asyncList.indexOf(name), 1);
- } else {
- asyncList.push(name);
- }
- 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 [$RPN, $async, $debug, $ajax] = [initRPN(), initAsync(), initDebug(), window.top.$ajax ??= unsafeWindow.$ajax ??= initAjax()];
+
+ // 初始化结束,开始实际流程
+ 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 },
+ },
- //ajax
- function $doc(h) {
- const d = document.implementation.createHTMLDocument(''); d.documentElement.innerHTML = h; return d;
- }
- 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);
- });
- },
- open: function (url, data, method, context = {}, headers = {}) {
- $ajax.fetch(url, data, method, context, headers).then(goto).catch(e=>{console.error(e)});
- },
- 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();
- },
- repeat: function (count, func, ...args) {
- const list = [];
- for (let i = 0; i < count; i++) {
- list.push(func(...args));
- }
- return list;
- },
- 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();
- },
- 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();
- }
- }
- },
- 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;
+ multiCharOperators: ['>=', '<=', '==', '!=', '&&', '||', '**'],
+
+ 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),
+ },
+
+ isOperator: function (token) {
+ return token in $RPN.operators;
+ },
+
+ hasHigherPrecedence: function (op1, op2) {
+ return $RPN.operators[op1].precedence >= $RPN.operators[op2].precedence;
+ },
+
+ tokenize: function (expression) {
+ const tokens = [];
+ let i = 0;
+ let lastTokenWasOperatorOrLeftParen = true;
+
+ while (i < expression.length) {
+ const ch = expression[i];
+
+ if (ch === ' ') {
+ i++;
+ continue;
+ }
+
+ if (ch === '(' || ch === ')') {
+ tokens.push(ch);
+ i++;
+ lastTokenWasOperatorOrLeftParen = (ch === '(');
+ continue;
+ }
+
+ if (ch === "'" || ch === '"') {
+ const quote = ch;
+ let raw = '';
+ 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;
+ }
+
+ 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;
+
+ if ($RPN.isOperator(ch)) {
+ if (ch === '-' && lastTokenWasOperatorOrLeftParen) {
+ tokens.push('-neg');
+ } else {
+ tokens.push(ch);
+ }
+ i++;
+ lastTokenWasOperatorOrLeftParen = true;
+ continue;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ throw new Error(`Unknown character: ${ch} from ${expression}`);
+ }
+
+ return tokens;
+ },
+
+ 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());
+ }
+ }
+
+ 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);
}
- setValue('last_post', now);
- $ajax.tid = null;
- $ajax.next();
};
- $ajax.tid = setTimeout(ontimer, $ajax.interval);
- },
- send: function () {
- GM_xmlhttpRequest($ajax.queue[$ajax.index]);
- $ajax.index++;
- $ajax.conn++;
- },
- 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(`
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();
- }
- },
- onerror: function (r) {
- $ajax.conn--;
- $ajax.error = `${r.status} ${r.statusText}: ${r.finalUrl}`;
- r.context.onerror?.();
- $ajax.next();
- },
- };
-
- window.addEventListener('unhandledrejection', (e) => { console.error($ajax.error, e); });
-
- (function init() {
- if (!checkIsHV()) {
- return;
+ return $RPN;
}
- if (!gE('#navbar,#riddlecounter,#textlog')) {
- setTimeout(goto, 5 * _1m);
- return;
+ function initDebug() {
+ const $debug = {
+ Stack: class extends Error {
+ constructor(description, ...params) {
+ super(...params);
+ this.name = '$debug.Stack';
+ }
+ },
+ realtime: false,
+ logList: [],
+ maxLogCache: 100,
+ 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;
}
- 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;
- }
+ 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: [],
- 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;
+ 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) {
+ window.open(url, newTab ? '_blank' : '_self');
+ },
+ repeat: function (count, func, ...args) {
+ const list = [];
+ for (let i = 0; i < count; i++) {
+ 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((isIsekai ? 'hvuti' : 'hvut') + '_last_post');
+ return v === null ? undefined : JSON.parse(v);
+ },
+ setLast: function (last) {
+ window.localStorage.setItem((isIsekai ? 'hvuti' : 'hvut') + '_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(`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;
}
- unsafeWindow = typeof unsafeWindow === 'undefined' ? window : unsafeWindow;
+ 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 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;
+ let remain = isNaN(blocked) ? _1h : blocked;
+ await until(() => {
+ document.title = `[M]${pad(Math.floor(remain/_1m))}:${pad(Math.floor((remain%_1m)/_1s))}`;
+ try { if (!isNaN(blocked)) {
+ body.innerText = body.innerText.replace(blockTip, (...args) => args[0].replace(args[1], remain));
+ } } catch (err) { console.log(err) };
+ remain-=_1s;
+ return remain <= 0;
+ }, _1s);
+ goto();
+ } catch (err) { console.error(err)} })();
+ return true;
+ }
+ 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);
+ $ajax.openNoFetch(getValue('lastUrl'));
+ return;
+ }
- if (gE('#riddlecounter')) { // 需要答题
- if (!g('option').riddlePopup || window.opener) {
- riddleAlert(); // 答题警报
- return;
- }
- window.open(window.location.href, 'riddleWindow', 'resizable,scrollbars,width=1241,height=707');
- return;
- }
+ 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) */ }
- 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) });
+ return true;
}
- 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}`);
+
+ setValue('lastEH', time(0));
+ const isEngage = window.location.href === 'https://e-hentai.org/news.php?encounter';
+ let encounter = getEncounter();
+ let location = getValue('url') ?? (document.referrer.match('hentaiverse.org') ? new URL(document.referrer).origin : 'https://hentaiverse.org');
+ 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;
+ }
+ }
}
- onBattle();
- updateEncounter(false, true);
- return;
- }
- if(window.top.location.href.endsWith(`?s=Battle`)){
- $ajax.openNoFetch(getValue('lastHref'));
- return;
- }
- // 战斗外
- 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();
+ if (!url) {
+ if (isEngage && !getValue('battle')) {
+ // 自动跳转,同时先刷新遭遇时间,延长下一次遭遇
+ $ajax.openNoFetch(getValue('lastUrl'));
+ }
+ return false;
+ }
+ // 减少因在恒定世界处于战斗中时打开eh触发了遭遇而导致的错失
+ // 缓存当前链接,等战斗结束时再自动打开,下次打开链接时:
+ // 1. 若新的遭遇未出现,进入已缓存的战斗链接
+ // 2. 若新的遭遇已出现,则前一次已超时失效错过,重新获取新的一次
+ if (!isEngage) { // 战斗外,非自动跳转
+ if (eventpane) {
+ eventpane.style.cssText += 'color:red;'; // 链接标红提醒
+ }
+ } else if (getValue('battle')) { //战斗中
+ if (eventpane) {
+ eventpane.style.cssText += 'color:gray;'; // 链接置灰提醒
+ }
+ } else { // 战斗外,自动跳转
+ checkOption();
+ $ajax.openNoFetch(`${g().option?.altBattleFirst ? location.replace('hentaiverse.org', 'alt.hentaiverse.org').replace('alt.alt', 'alt') : location}/${url}`);
+ }
+ return false;
}
- const hvAAPauseUI = document.body.appendChild(cE('div'));
- hvAAPauseUI.classList.add('hvAAPauseUI');
- setPauseUI(hvAAPauseUI);
- asyncOnIdle();
- }());
-
- 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])) {
+ // 答题//
+ async function riddleAlert() { try {
+ setAlarm('Riddle');
+ const option = g().option??{};
+ 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);
+ }
+ for (let ans of gE('#riddler1>*', 'all').children) {
+ if (!ans.children[0].children[0].checked) continue;
+ gE('#riddlesubmit').click();
return;
}
- gE('div', 'all', btn.parentNode.parentNode).forEach(div=>{div.style.cssText += 'color:grey!important;'});
- });
- }
-
- 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 (!option.riddleAnswerChoose) return;
+ // if no answer selected
+ const answers = ['aj', 'fs', 'pp', 'ra', 'rd', 'ts'];
+ answers.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.');
}
- }
- if(isFrame){
- g('option', option);
- } else{
- g('option', setValue('option', option));
- }
- }
+ goto();
+ } catch (err) { console.error(err); }}
- function pauseAsync(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
+ function checkIsWindowTop() {
+ const currentUrl = window.self.location.href;
+ if (!isFrame) {
+ checkOption();
+ if (!g().option?.riddlePopup || gE('#riddlecounter')) { // 未开启使用弹窗或仍处于答题
+ return true;
+ }
+ if (!window.opener || window.opener === window.self || window.opener.closed) { // 没有仍存在的opener
+ return true;
+ }
+ try {
+ if (!gE('#riddlecounter,#battle_main', 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;
+ }
- 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);
+ if (gE('#riddlecounter') || gE('#battle_main')) {
+ if (!window.top.location.href.endsWith(`?s=Battle`)) {
+ setValue('lastUrl', window.top.location.href);
+ }
+ window.top.location.href = currentUrl;
+ return false;
+ }
+ if (currentUrl.match(/\?s=Battle&ss=(ar|rb)/)) {
+ checkOption();
+ setArenaDisplay();
+ }
+ return false;
}
- setTimeout(autoSwitchIsekai, (g('option').isekaiTime * (Math.random() * 20 + 90) / 100) * _1s - (time(0) - idleStart));
- } catch (e) {console.error(e)}}
- // 通用//
- function setPauseUI(parent) {
- setPauseButton(parent);
- setPauseHotkey();
- }
-
- function setPauseButton(parent) {
- if (!g('option').pauseButton) {
- return;
- }
- const button = parent.appendChild(cE('button'));
- button.innerHTML = 'Auto close popup failed. Please manually close window.
';
return;
}
- if (e.keyCode === g('option').pauseHotkeyCode) {
- pauseChange();
- }
- }, false);
- }
+ await tryClose(attempts, delay);
+ } catch (err) { console.error('Opener reload or popup close failed:', err) } }
- 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;
+ 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();
+ _alert(0, '请设置hvAutoAttack', '請設置hvAutoAttack', 'Please config this script');
+ gE('.hvAAButton').click();
+ return false;
}
- t.unshift(front);
- break;
+ loadOption();
+ writePortables();
+
+ const option = g().option??{};
+ g('lang', option.lang || '0');
+ addStyle();
+
+ // README等合并到主分支后再取消掉注释
+ // if (option.version.substr(0, 4) !== scriptVersion.ver.substr(0, 4)) {
+ // 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 false;
+ // }
+
+ 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 false;
+ }
+ return true;
}
- return t;
- }
- 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--;
+ function writePortables() {
+ const option = g().option??{};
+ if (!option.portable) return;
+ for (const key of portable) {
+ if (!(Object.keys(option.portable).includes)) continue;
+ setValue(key, getValue(key), true);
}
}
- 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 onRiddle() {
+ if (!gE('#riddlecounter')) {
+ return false;
+ }
+ setValue('onriddle', true);
+ if (!g().option?.riddlePopup || window.opener) {
+ riddleAlert();
+ return true;
+ }
+ window.open(window.location.href, 'riddleWindow', 'resizable,scrollbars,width=1241,height=707');
+ return true;
}
- }
- 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 onIdle() {
+ if (!gE('#navbar')) {
+ return false;
+ }
+ // 战斗结束跳转回原链接
+ if (window.top.location.href.endsWith(`?s=Battle`)) {
+ $ajax.openNoFetch(getValue('lastUrl'));
+ return true;
+ }
+ if (window.location.href.indexOf(`?s=Battle&ss=ba`) === -1) { // 不缓存encounter
+ setValue('lastUrl', window.top.location.href); // 缓存进入战斗前的页面地址
+ setArenaDisplay();
+ }
+ delValue(1);
+ const option = g().option??{};
+ if (option.showQuickSite && option.quickSite) {
+ quickSite();
+ }
+ const hvAAPauseUI = document.body.appendChild(cE('div'));
+ hvAAPauseUI.classList.add('hvAAPauseUI');
+ setPauseUI(hvAAPauseUI);
+ asyncOnIdle();
+ return true;
}
- }
- function cE(name) { // 创建元素
- return document.createElement(name);
- }
+ function onBattle() {
+ if (!gE('#textlog')) {
+ return false;
+ }
+ checkResponsive();
- 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;
- }
+ if (getValue('onriddle', true)) {
+ console.log('onBattle clean onriddle');
+ window.history.replaceState(null, '', window.location.href);
+ delValue('onriddle');
+ }
- 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);
+ const box2 = gE('#battle_main').appendChild(cE('div'));
+ box2.id = 'hvAABox2';
+ setPauseUI(box2);
+ reloader();
+ const option = g().option??{};
+ g('attackStatus', option.attackStatus);
+ // 1二天 2单手 3双手 4双持 5法杖
+ for (let fightingStyle = 1; fightingStyle < 6; fightingStyle++) {
+ if (gE(`2${fightingStyle}01`)) {
+ g('fightingStyle', fightingStyle.toString());
+ }
+ }
+ g('timeNow', time(0));
+ g('runSpeed', 1);
+ newRound(false);
+ updateMonsterEffects(false);
+ onBattleRound();
+ if (option.recordEach) {
+ const token = document.body.innerHTML.match(`var battle_token = \"(.*)\";`)[1];
+ let code = getValue('battleCode', true);
+ if (code?.token != token || !code?.r || !code?.rc) {
+ const now = code?.token === token ? code?.time ?? time(1) : time(1);
+ const type = g().battle?.roundType?.toUpperCase();
+ const roundAll = g().battle?.roundAll;
+ code = {
+ token: token,
+ time: now,
+ roundType: type,
+ roundAll: roundAll,
+ name: `${now}: ${type}-${roundAll}`,
+ };
+ setValue('battleCode', code);
+ }
+ }
+ updateEncounter(false);
+ return true;
}
- }
- 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);
+ // ----------methods----------
+ // 通用//
+ function unique(arr) {
+ const newArr = [];
+ for (let i = 0; i < arr.length; i++) {
+ if (newArr.indexOf(arr[i]) === -1) {
+ newArr.push(arr[i]);
+ }
+ }
+ return newArr;
}
- return value;
- }
- 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;
+ function splitOrders(orderValue, defaultOrder) {
+ const order = orderValue?.split(',') ?? [];
+ return unique(order.concat(defaultOrder ?? []));
}
- return GM_getValue(item, null);
- }
- 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;
- }
- if (!itemExisted) {
- return null; // 若都没有该数据
- }
- itemExisted = JSON.parse(JSON.stringify(itemExisted));
- setLocal(`${current}_${key}`, itemExisted);
- delLocal(key);
+ function goto() { // 前进
+ window.location.href = window.location.search ? window.location.pathname + window.location.search : window.location.href;
+ setTimeout(goto, 5000);
+ setTimeout(() => { window.location.href = window.location.href }, 10000);
+ return true;
}
- if (Object.keys(excludeStandalone).includes(key)) {
- otherWorldItem ??= getLocal(`${current}_${key}`) ?? {};
- for (let i of excludeStandalone[key]) {
- otherWorldItem[i] = getLocal(`${current}_${key}`)[i];
+
+ 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;
}
- setLocal(`${other}_${key}`, otherWorldItem);
- return getLocal(`${current}_${key}`);
- }
- function delLocal(key) {
- if (typeof GM_deleteValue === 'undefined') {
- window.localStorage.removeItem(`hvAA-${key}`);
- return;
+ function pauseAsync(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
}
- GM_deleteValue(key);
- }
- function delValue(key) { // 删除数据
- if (standalone.includes(key)) {
- key = `${current}_${key}`;
- }
- if (typeof key === 'string') {
- delLocal(key);
- return;
- }
- if (typeof key !== 'number') {
- return;
+ 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();
}
- const itemMap = {
- 0: ['disabled'],
- 1: ['battle', 'battleCode'],
+
+ function pad(num, pad='0', total=2) {
+ return num.toString().padStart(total, pad);
}
- for (let item of itemMap[key]) {
- delValue(item);
+
+ 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 goto() { // 前进
- window.location.href = window.location;
- setTimeout(goto, 5000);
- }
- 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}`)
+ function cE(name) { // 创建元素
+ return document.createElement(name);
}
- }
- function g(key, value) { // 全局变量
- const hvAA = window.hvAA || {};
- if (key === undefined && value === undefined) {
- return hvAA;
- } if (value === undefined) {
- return hvAA[key];
+
+ function $doc(h) {
+ const doc = document.implementation.createHTMLDocument('');
+ doc.documentElement.innerHTML = h;
+ return doc;
}
- hvAA[key] = value;
- window.hvAA = hvAA;
- return window.hvAA[key];
- }
- function objArrSort(key) { // 对象数组排序函数,从小到大排序
- return function (obj1, obj2) {
- return (obj2[key] < obj1[key]) ? 1 : (obj2[key] > obj1[key]) ? -1 : 0;
- };
- }
+ function popup(text) {
+ if (!g().option?.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 objSort(obj) { // 对象排序
- const objNew = {};
- const arr = Object.keys(obj).sort();
- arr.forEach((key) => {
- objNew[key] = obj[key];
- });
- return objNew;
- }
+ 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 _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 setArenaDisplay() {
+ const option = g().option??{};
+ 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 = option.idleArenaValue?.split(',');
+ if (!ar || ar.length === 0) {
+ return;
+ }
+ getStartBattleButtons().forEach(btn => {
+ if (ar.includes(btn.id) && btn.cleared) {
+ return;
+ }
+ gE('div', 'all', btn.parentNode.parentNode).forEach(div => { div.style.cssText += `color:${btn.cleared?'grey':'red'}!important;` });
+ });
}
- }
- 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 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 = g().option??{};
+ 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.parentNode.parentNode;
+ 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;
}
- 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;}',
- '.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 ul,.hvAATab ol{margin:0;}',
- '.hvAATab label{cursor:pointer;}',
- '.hvAATab table{border:2px solid #000;border-collapse:collapse;margin:0 auto;}',
- '.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;}',
- '.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>input.hvAANumber{width:32px}',
- '.hvAAConfig{width:100%;height:16px;}',
- '.hvAAButtonBox{position:relative;top:468px;}',
- '.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;}',
- '.quickSiteBar>span>a{text-decoration:none;}',
- '.customize{border: 2px dashed red!important;min-height:21px;}',
- '.customize>.customizeGroup{display:block;background-color:#FFF;}',
- '.customize>.customizeGroup:nth-child(2n){background-color:#C9DAF8;}',
- '.customizeBox{position:absolute;z-index:-1;border:1px solid #000;background-color:#EDEBDF;}',
- '.customizeBox>span{display:inline-block;font-size:16px;margin:0 1px;padding:0 5px;font-weight:bold;border:1px solid #5C0D11;border-radius:10px;}',
- '.customizeBox>span.hvAAInspect{padding:0 3px;cursor:pointer;}',
- '.customizeBox>span.hvAAInspect[title="on"]{background-color:red;}',
- '.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>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;}',
- // 全局
- 'button{border-radius:3px;border:2px solid #808080;cursor:pointer;margin:0 1px;}',
- // hv
- '#riddleform>div:nth-child(3)>img{width:700px;}',
- '#battle_right{overflow:visible;}',
- '#pane_log{height:403px;}',
- '.tlbQRA{text-align:left;font-weight:bold;}', // 标记已检测的日志行
- '.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;}',
- ].join('');
- globalStyle.textContent = cssContent;
- optionButton(lang);
- }
- 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 Version(...verArgs) {
+ if (!(this instanceof Version)) {
+ return new Version(...verArgs);
}
- };
- }
+ this.ver = verArgs.join('.');
- function optionBox() { // 配置界面
- const optionBox = gE('body').appendChild(cE('div'));
- optionBox.id = 'hvAABox';
- optionBox.innerHTML = [
- '${(option.pauseHotkey && option.pauseHotkeyStr) ? `(${option.pauseHotkeyStr})` : '' }012>`;
}
+ button.className = 'pauseChange';
+ button.onclick = pauseChange;
}
- 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();
+
+ function setPauseHotkey() {
+ const option = g().option??{};
+ if (!option.pauseHotkey) {
+ return;
}
- updateGroup();
- if (target.className !== 'customize' && target.parentNode.className !== 'customize') {
- if (!target.className.match('customize')) {
- gE('.customizeBox').style.zIndex = -1;
+ 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 = g().option??{};
+ if (!option.stepInButton) {
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);
+ const button = parent.appendChild(cE('button'));
+ button.innerHTML = `步进 步進 StepIn ${(option.stepInHotkey && option.stepInHotkeyStr) ? `(${option.stepInHotkeyStr})` : '' }`;
+ button.className = 'stepIn';
+ button.onclick = stepIn;
+ }
+
+ function setStepInHotkey() {
+ const option = g().option??{};
+ if (!option.stepInHotkey) {
+ return;
+ }
+ document.addEventListener('keydown', (e) => {
+ if (e.target.tagName.toUpperCase() === 'INPUT' || e.target.tagName.toUpperCase() === 'TEXTAREA') {
+ return;
}
- }, 3000);
- };
- gE('.staminaLostLog', optionBox).onclick = function () {
- const out = [];
- const staminaLostLog = getValue('staminaLostLog', true);
- for (const i in staminaLostLog) {
- out.push(`${i}: ${staminaLostLog[i]}`);
+ if (e.keyCode === option.stepInHotkeyCode) {
+ stepIn();
+ }
+ }, false);
+ }
+
+ function setAltButton(parent) {
+ const option = g().option??{};
+ if (!option.altButton) {
+ return;
}
- if (window.confirm(`总共${out.length}条记录 (There are ${out.length} logs): \n${out.reverse().join('\n')}\n是否重置 (Whether to reset)?`)) {
- setValue('staminaLostLog', {});
+ 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 = g().option??{};
+ if (!option.altHotkey) {
+ return;
}
- };
- gE('.idleArenaReset', optionBox).onclick = function () {
- if (_alert(1, '是否重置', '是否重置', 'Whether to reset')) {
- delValue('arena');
+ document.addEventListener('keydown', (e) => {
+ if (e.target.tagName.toUpperCase() === 'INPUT' || e.target.tagName.toUpperCase() === 'TEXTAREA') {
+ return;
+ }
+ if (e.keyCode === option.altHotkeyCode) {
+ gotoAlt();
+ }
+ }, false);
+ }
+
+ function formatTime(t, size = 2) {
+ t = [t / _1h, (t / _1m) % 60, (t / _1s) % 60, (t % _1s) / 10].map(cdi => Math.floor(cdi));
+ const option = g().option??{};
+ while (t.length > Math.max(size, option.encounterQuickCheck ? 2 : 3)) { // remove zero front
+ const front = t.shift();
+ if (!front) {
+ continue;
+ }
+ t.unshift(front);
+ break;
}
- };
- 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;
+ return t;
+ }
+
+ 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));
});
- };
- gE('.hvAAArenaLevels', optionBox).onclick = function (e) {
- if (e.target.tagName !== 'INPUT' && e.target.type !== 'checkbox') {
- return;
+ out = out.sort();
+ for (let i = 1; i < out.length; i++) {
+ if (out[i - 1] === out[i]) {
+ out.splice(i, 1);
+ i--;
+ }
}
- 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(/^,/, '');
+ 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,
+ });
}
- 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') {
+ function setLocal(key, value, isLocalStroage) {
+ if (JSON.stringify(getLocal(key, isLocalStroage)) === JSON.stringify(value)) {
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]);
+ if (typeof GM_setValue === 'undefined' || isLocalStroage) {
+ window.localStorage[`hvAA-${key}`] = (typeof value === 'string') ? value : JSON.stringify(value);
} else {
- name = name.replace(new RegExp(`(^|,)${valueArray[0]}(,|$)`), '$2').replace(/^,/, '');
- // value = value.replace(new RegExp(`(^|,)${valueArray[1]}(,|$)`), '$2').replace(/^,/, '');
+ GM_setValue(key, value);
}
- gE('input[name="battleOrderName"]').value = name;
- // gE('input[name="battleOrderValue"]').value = value;
- };
+ }
- // 标签页-物品
- gE('.itemOrder', optionBox).onclick = function (e) {
- if (e.target.tagName !== 'INPUT' && e.target.type !== 'checkbox') {
- return;
+ function setValue(key, value, portable=false) { // 储存数据
+ const isLocalStorage = local.includes(key) && !portable;
+ if (!standalone.includes(key)) {
+ setLocal(key, value, isLocalStorage);
+ return value;
}
- 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(/^,/, '');
+ setLocal(`${current}_${key}`, value, isLocalStorage);
+ if (sharable.includes(key) && !getValue('option').optionStandalone) {
+ setLocal(`${other}_${key}`, value, isLocalStorage);
}
- 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;
+ 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;
}
- 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(/^,/, '');
+ value = GM_getValue(key, null);
+ if (!isLocalStorage) {
+ return value;
}
- 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;
+ key = `hvAA-${key}`;
+ if (!(key in window.localStorage)) {
+ return value;
}
- 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(/^,/, '');
+ value = window.localStorage[key];
+ value = toJSON ? JSONParse(value) : value;
+ return value;
+
+ function JSONParse(object) {
+ if (object === undefined || object === '') {
+ return object;
+ }
+ return JSON.parse(object)
}
- gE('input[name="buffSkillOrderValue"]').value = value;
- };
- // 标签页-DEBUFF技能
- gE('.debuffSkillOrder', optionBox).onclick = function (e) {
- if (e.target.tagName !== 'INPUT' && e.target.type !== 'checkbox') {
- return;
+ }
+
+ function getValue(key, toJSON) { // 读取数据
+ const isLocalStorage = local.includes(key);
+ if (!standalone.includes(key)) {
+ return getLocal(key, isLocalStorage, toJSON);
}
- 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(/^,/, '');
+ let otherWorldItem = getLocal(`${other}_${key}`, isLocalStorage);
+ // 将旧的数据迁移到新的数据
+
+ if (!getLocal(`${current}_${key}`, isLocalStorage)) {
+ let itemExisted = getLocal(key, isLocalStorage);
+ if (!itemExisted && sharable.includes(key)) {
+ itemExisted = otherWorldItem;
+ }
+ if (!itemExisted) {
+ return null; // 若都没有该数据
+ }
+ itemExisted = JSON.parse(JSON.stringify(itemExisted));
+ setLocal(`${current}_${key}`, itemExisted);
+ delLocal(key, isLocalStorage);
+ }
+ if (Object.keys(excludeStandalone).includes(key)) {
+ otherWorldItem ??= getLocal(`${current}_${key}`, isLocalStorage) ?? {};
+ for (let i of excludeStandalone[key]) {
+ otherWorldItem[i] = getLocal(`${current}_${key}`, isLocalStorage)[i];
+ }
}
- gE('input[name="debuffSkillOrderValue"]').value = value;
- };
- // 标签页-其他技能
- gE('.skillOrder', optionBox).onclick = function (e) {
- if (e.target.tagName !== 'INPUT' && e.target.type !== 'checkbox') {
+ setLocal(`${other}_${key}`, otherWorldItem);
+ return getLocal(`${current}_${key}`, isLocalStorage, toJSON);
+ }
+
+ function delLocal(key, isLocalStorage) {
+ if (typeof GM_deleteValue === 'undefined') {
+ window.localStorage.removeItem(`hvAA-${key}`);
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(/^,/, '');
+ if (isLocalStorage) {
+ window.localStorage.removeItem(`hvAA-${key}`);
}
- gE('input[name="skillOrderValue"]').value = value;
- };
- // 标签页-警报
- gE('input[name="audio_Text"]', optionBox).onchange = function () {
- if (this.value === '') {
+ GM_deleteValue(key);
+ }
+
+ function delValue(key, portable=false) { // 删除数据
+ const isLocalStorage = portable ? false : local.includes(key);
+ if (standalone.includes(key)) {
+ key = `${current}_${key}`;
+ }
+ if (typeof key === 'string') {
+ delLocal(key, isLocalStorage);
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"');
+ if (typeof key !== 'number') {
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');
+ const itemMap = {
+ 0: ['disabled'],
+ 1: ['battle', 'battleCode'],
}
- };
- // 标签页-数据记录
- gE('.reRecordUsage', optionBox).onclick = function () {
- if (_alert(1, '是否重置', '是否重置', 'Whether to reset')) {
- delValue('stats');
- delValue('statsOld');
+ for (let item of itemMap[key]) {
+ delValue(item, portable);
}
- };
- // 标签页-关于本脚本
- gE('.hvAAFix', optionBox).onclick = function () {
- gE('.hvAADebug[name^="round"]', 'all', optionBox).forEach((input) => {
- setValue(input.name, input.value || input.placeholder);
+ }
+
+ 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 objArrSort(key) { // 对象数组排序函数,从小到大排序
+ return function (obj1, obj2) {
+ return (obj2[key] < obj1[key]) ? 1 : (obj2[key] > obj1[key]) ? -1 : 0;
+ };
+ }
+
+ function objSort(obj) { // 对象排序
+ const objNew = {};
+ const arr = Object.keys(obj).sort();
+ arr.forEach((key) => {
+ objNew[key] = obj[key];
});
- };
- 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();
- };
+ return objNew;
+ }
+
+ 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 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";}',
+ '.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{text-align:center;}',
+ '#hvAABox input[type=\'checkbox\']{top: 3px;}',
+ '.hvAATab ul,.hvAATab ol{margin:0;}',
+ '.hvAATab label{cursor:pointer;}',
+ '.hvAATab table{border:2px solid #000;border-collapse:collapse;margin:0 auto;}',
+ '.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;}',
+ '.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;}',
+ '.optionEdited{color:#5C0D11;}',
+ '.optionOrigin{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(5, 1fr)}',
+ '.hvAAcheckItems>input.hvAANumber{width:32px}',
+ '.hvAAConfig{width:100%;height:16px;}',
+ '.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;}',
+ '.quickSiteBar>span>a{text-decoration:none;}',
+ '.customize{border: 2px dashed red!important;min-height:21px;}',
+ '.customize>.customizeGroup{display:block;background-color:#FFF;}',
+ '.customize>.customizeGroup:nth-child(2n){background-color:#C9DAF8;}',
+ '.customizeBox{position:absolute;z-index:-1;border:1px solid #000;background-color:#EDEBDF;}',
+ '.customizeBox>span{display:inline-block;font-size:16px;margin:0 1px;padding:0 5px;font-weight:bold;border:1px solid #5C0D11;border-radius:10px;}',
+ '.customizeBox>span.hvAAInspect{padding:0 3px;cursor:pointer;}',
+ '.customizeBox>span.hvAAInspect[title="on"]{background-color:red;}',
+ '.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>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;}',
+ // 全局
+ 'button{border-radius:3px;border:2px solid #808080;cursor:pointer;margin:0 1px;}',
+ // hv
+ '#riddleform>div:nth-child(3)>img{width:700px;}',
+ '#battle_right{overflow:visible;}',
+ '#pane_log{height:403px;}',
+ '.tlbQRA{text-align:left;font-weight:bold;}', // 标记已检测的日志行
+ '.tlbWARN{text-align:left;font-weight:bold;color:red;font-size:20pt;}', // 标记检测出异常的日志行
+ // 怪物标号用数字替代字母,目前弃用
+ // '#pane_monster{counter-reset:order;}',
+ // `${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;
+ 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();
- }
+ if (config.textContent === code) config.remove();
}
}
- gE('.hvAABackup', optionBox).onclick = function () {
- const code = _alert(2, '请输入当前配置代号', '請輸入當前配置代號', 'Please put in a name for the current configuration') || time(3);
+
+ function backup(code, alert) {
+ const current = getValue('option');
+ const auto = code ? undefined : `[auto backup for ${current.version}] ${time(3)}`;
const backups = getValue('backup', true) || {};
+ code ??= auto;
if (code in backups) { // 覆写同名配置
- if (_alert(1, '是否覆盖已有的同名配置?', '是否覆蓋已有的同名配置?', 'Do you want to overwrite the configuration with the same name?')) {
+ if (!alert || _alert(1, alert)) {
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;
+ backups[code].auto = auto ? time(0) : undefined;
+ const autos = Object.keys(backups).filter(c => backups[c].auto);
+ autos.sort(a => -backups[a].auto);
+ let i=0;
+ for (const a of autos) {
+ i++;
+ if (i <= 5) continue;
+ delete backups[a];
}
- 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);
- goto();
- }
- };
- gE('.hvAAReset', optionBox).onclick = function () {
- if (_alert(1, '是否重置', '是否重置', 'Whether to reset')) {
- delValue('option');
- }
- };
- 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;
- }
- 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') {
- continue;
- } else if (inputs[i].className === 'hvAANumber') {
- itemName = inputs[i].name;
- itemValue = (inputs[i].value || inputs[i].placeholder) * 1;
- if (isNaN(itemValue)) {
- continue;
+ if (!gE('#hvAABox')) return;
+ const li = gE('.hvAABackupList', gE('#hvAABox')).appendChild(cE('li'));
+ li.textContent = code;
+ }
+
+ function optionBox() { // 配置界面
+ const option = g().option??{};
+ const optionBox = gE('body').appendChild(cE('div'));
+ optionBox.id = 'hvAABox';
+ optionBox.innerHTML = [
+ '',
+ '
更新历史 更新歷史 ChangeLog ',
+ '
使用说明 README ',
+ '
by Koko191 ',
+ '
hvAutoAttack ',
+ '
简体中文 繁體中文 English ',
+ (option.optionStandalone ? isIsekai ? '
当前为异世界单独配置 當前為異世界單獨配置 Using Isekai standalone option ' : '
当前为恒定世界单独配置 當前為恆定世界單獨配置 Using Persistent standalone option ' : ''),
+ '
配置版本 配置版本 Option Version ',
+ '
',
+ '',
+
+ '',
+
+ '
',
+ '
异世界相关 異世界相關 Isekai : ',
+ ' 两个世界使用不同的配置 兩個世界使用不同的配置 Use standalone options. ; ',
+ ' 在任意页面停留 在任意頁面停留 While idle in any page for 秒后,自动切换恒定世界和异世界 秒後,自動切換恆定世界和異世界 s, auto switch between Isekai and Persistent
',
+ '
',
+ '
小马答题 小馬答題 RIDDLE :
弹窗答题(Firefox中可能导致报错) 弹窗答题(Firefox中可能導致報錯) POPUP a window to answer(Might cause in Firefox) ; ',
+ '
',
+ '
',
+ '
脚本行为 腳本行為 Script Activity ',
+ '
',
+ ' 暂停按钮 暫停按鈕 Pause Button ; ',
+ ' 暂停热键 暫停熱鍵 Pause Hotkey : ',
+ ' 步进按钮 步進按鈕 StepIn Button ; ',
+ ' 步进热键 步進熱鍵 StepIn Hotkey : ',
+ '
',
+ '
Alt切换按钮 切換按鈕 Switch Button ; ',
+ ' 热键 熱鍵 Hotkey :
',
+ '
警告相关 警告相關 To Warn : ',
+ ' 音频警报 音頻警報 Audio Alarms ; ',
+ ' 桌面通知 桌面通知 Notifications ',
+ ' 预处理 預處理 Pretreat ',
+ ' 桌面通知时聚焦页面(需要GM_notification) 桌面通知時聚焦頁面(需要GM_notification) Focus while Notifications (Requires GM_notification)
',
+ '
掉落及数据记录 掉落及數據記錄 Drops and Usage Tracking : 单独记录每场战役 單獨記錄每場戰役 Record each battle separately
',
+ '
延迟 延遲 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 )
',
+ '
频率指示符号 頻率指示符號 Frequency Signal : &
',
+ '
',
+ '
*默认攻击模式 默認攻擊模式 Default Attack Mode :',
+ ' 物理 / Physical 火 / Fire 冰 / Cold 雷 / Elec 风 / 風 / Wind 圣 / 聖 / Divine 暗 / Forbidden
',
+
+ '
战斗执行顺序(未配置的按照下面的顺序) 戰鬥執行順序(未配置的按照下面的順序) Battal Order(Using order below as default if not configed) :
(只使用默认顺序 只使用默認順序 Default order only ) ',
+ '
',
+ '
',
+ '
使用治疗 使用治療 Cure
',
+ '
自动暂停 自動暫停 Auto Pause
',
+ '
关闭灵动架式 關閉靈動架式 Disable Sprite
',
+ '
恢复(含治疗) 恢復(含治療) Recover(& cure)
',
+ '
使用卷轴 使用捲軸 Use Scroll
',
+ '
使用魔药 使用魔藥 Infusions
',
+ '
自动防御 自動防禦 Auto Defence
',
+ '
引导技能 引導技能 Channel Skill
',
+ '
Buff技能 Buff技能 Buff Skills
',
+ '
Debuff技能 Debuff技能 Debuff Skills
',
+ '
自动集中 自動集中 Focus
',
+
+ '
灵动架式(开&关) 靈動架式(開&關) On & Off Sprite
',
+ '
释放技能 釋放技能 Auto Skill
',
+ '
自动攻击 自動攻擊 Attack
',
+ '
',
+
+ '
次要攻击模式顺序(未配置的按照下面的顺序) 次要攻擊模式順序(未配置的按照下面的順序) Attack Mode Order(Using order below as default if not configed) :',
+ '
',
+
+ '
攻击模式 物理 攻擊模式 物理 Attack Mode: Physical : {{attackStatusSwitchCondition0}}
',
+ '
攻击模式 火 攻擊模式 火 Attack Mode: Fire : {{attackStatusSwitchCondition1}}
',
+ '
攻击模式 冰 攻擊模式 冰 Attack Mode: Cold : {{attackStatusSwitchCondition2}}
',
+ '
攻击模式 雷 攻擊模式 雷 Attack Mode: Elec : {{attackStatusSwitchCondition3}}
',
+ '
攻击模式 风 攻擊模式 風 Attack Mode: Wind : {{attackStatusSwitchCondition4}}
',
+ '
攻击模式 圣 攻擊模式 聖 Attack Mode: Divine : {{attackStatusSwitchCondition5}}
',
+ '
攻击模式 暗 攻擊模式 暗 Attack Mode: Forbidden : {{attackStatusSwitchCondition6}}
',
+ '
低阶魔法技能使用条件 低階魔法技能使用條件 Conditions for 1st Tier Offensive Magic : {{lowSkillCondition}}
',
+ '
中阶魔法技能使用条件 中階魔法技能使用條件 Conditions for 2nd Tier Offensive Magic : {{middleSkillCondition}}
',
+ '
高阶魔法技能使用条件 高階魔法技能使用條件 Conditions for 3rd Tier Offensive Magic : {{highSkillCondition}}
',
+ '
以太水龙头 以太水龍頭 Ether Tap : {{etherTapCondition}}
',
+ '
开启灵动架式 開啟靈動架勢 Turn on Spirit Stance : {{turnOnSSCondition}}
',
+ '
关闭灵动架式 關閉靈動架勢 Turn off Spirit Stance : {{turnOffSSCondition}}
',
+ '
Defend : {{defendCondition}}
',
+ '
Focus : {{focusCondition}}
',
+ '
自动暂停 自動暫停 Pause : {{pauseCondition}}
',
+ '
自动逃跑 自動逃跑 Flee : {{fleeCondition}}
',
+ '
战败自动退出战斗 戰敗自動退出戰鬥 Exit battle when defeated.
',
+ '
使用原生方式进入新回合 使用原生方式進入新回合 Native new round
',
+ '
新回合前检查链接: 新回合前檢查連接: Check url before new round: ; 秒后重试 秒後重試 (s) to retry
',
+
+ '
',
+ '
延时 延時 Wait time for
',
+ '
继续新回合 繼續新回合 New round : (秒) (秒) (s)
',
+ '
战斗结束退出 戰鬥結束退出 Exit battle : (秒) (秒) (s)
',
+ '
',
+ '
',
+ '
',
+ '
',
+
+ '
',
+ '
进入失败时窗口内弹窗提示 進入失敗時窗口內彈窗提示 In-window popup while failed start ;
',
+ '
优先使用alt进入 優先使用alt進入 Use alt.hentaiverse as default while auto start.
',
+ '
自动遭遇战 自動遭遇戰 Auto Encounter ',
+ ' 精准倒计时(影响性能) 精準(影響性能) Precise encounter cd(might reduced performsance) ',
+ ' 不自动遭遇时显示倒计时 不自動遭遇時顯示倒計時 Display CountDown While Not Auto Encounter ',
+ ' 遭遇战倒计时 遭遇戰倒計時 Wait for encounter first while count down ≤ s时优先等待 時優先等待 . ',
+ '
',
+ '
闲置竞技场 閒置競技場 Idle Arena : ',
+ ' 在任意页面停留 在任意頁面停留 Idle in any page for 秒后,开始竞技场 秒後,開始競技場 (s), start Arena 重置 Reset ;
',
+ '
进行的竞技场相对应等级 進行的競技場相對應等級 The levels of the Arena you want to complete : ',
+ '
显示更多 顯示更多 Show more 清空 Clear ',
+ '
',
+ '
',
+ ' 1 10 20 30 40 50 60 70 80 90 100 110 ',
+ ' 120 130 140 150 165 180 200 225 250 300 400 500 ',
+ ' RB50 RB75A RB75B RB75C ',
+ ' RB100 RB150 RB200 RB250 GrindFest ',
+ '
',
+ '
跳过未通关过的 跳過未通關過的 Skip not cleared Arena/RingOfBlood ',
+ '
',
+ '
页面中置灰未设置且未完成的 頁面中置灰未設置且未完成的 obscure not setted and not battled in Battle>Arena/RingOfBlood ',
+ '
',
+ '
',
+ '
',
+ ' [S!]精力 精力 Stamina : ',
+ ' 进入遭遇战的最低精力 進入遭遇戰的最低精力 Minimum stamina to engage encounter : ',
+ ' 竞技场/浴血擂台阈值 競技場/浴血擂台閾值 Minimum stamina to auto start The Arena or Ring Of Blood : Min(85, ) ',
+ ' 进入压榨届的最低精力 進入壓榨屆的最低精力 Minimum stamina to auto start GrindFest : ',
+ ' [S!!] 进入竞技场/浴血擂台/压榨届时,含本日自然恢复的阈值 进入競技場/浴血擂台/壓榨屆时,含本日自然恢復的閾值 Stamina threshold with naturally recovers today for The Arena, Ring Of Bloog, GrindFest : ',
+ ' 战前恢复 戰前恢復 Restore stamina ',
+ ' 检查惩罚倍率 檢查懲罰倍率 Check Punishment Ratio ',
+ '
',
+ '
',
+ ' [R!]修复装备 修復裝備 Repair Equipment : ',
+ ' 耐久度 耐久度 Durability ≤ % 或 压榨届耐久度 或 壓榨屆耐久度 OR Grind Fest Durability ≤ %遭遇战前检查 遭遇戰前檢查 Check before encounter ',
+ '
',
+ '
',
+ ' [E!]装备库存 裝備庫存 Equipment Storage ≤ ; 遭遇战前检查 遭遇戰前檢查 Check before encounter ',
+ '
',
+ '
',
+ '
[C!]检查物品库存 檢查物品庫存 Check is item needs supply ;',
+ '
库存 庫存 Warn if supply <max(100%,
%)
时提示 時提示 ;',
+ '
遭遇战前检查 遭遇戰前檢查 Check before encounter ',
+ '
',
+ '
体力长效药 體力長效藥 Health Draught
',
+ '
魔力长效药 魔力長效藥 Mana Draught
',
+ '
灵力长效药 靈力長效藥 Spirit Draught
',
+ '
火焰魔药 火焰魔藥 Infusion of Flames
',
+ '
冰冷魔药 冰冷魔藥 Infusion of Frost
',
+
+ '
体力药水 體力藥水 Health Potion
',
+ '
魔力药水 魔力藥水 Mana Potion
',
+ '
灵力药水 靈力藥水 Spirit Potion
',
+ '
闪电魔药 閃電魔藥 Infusion of Lightning
',
+ '
风暴魔药 風暴魔藥 Infusion of Storms
',
+
+ '
体力秘药 體力秘藥 Health Elixir
',
+ '
魔力秘药 魔力秘藥 Mana Elixir
',
+ '
灵力秘药 靈力秘藥 Spirit Elixir
',
+ '
神圣魔药 神聖魔藥 Infusion of Divinity
',
+ '
黑暗魔药 黑暗魔藥 Infusion of Darkness
',
+
+ '
花瓶 花瓶 Flower Vase
',
+ '
泡泡糖 泡泡糖 Bubble-Gum
',
+ '
终极秘药 終極秘藥 Last Elixir
',
+ '
吸收卷轴 吸收捲軸 Scroll of Absorption
',
+ '
虚空碎片 虛空碎片 Voidseeker Shard
',
+
+ '
能量饮料 能量飲料 Energy Drink
',
+ '
幻影卷轴 幻影捲軸 Scroll of Shadows
',
+ '
生命卷轴 生命捲軸 Scroll of Life
',
+ '
众神卷轴 眾神捲軸 Scroll of the Gods
',
+ '
以太碎片 以太碎片 Aether Shard
',
+
+ '
咖啡因糖果 咖啡因糖果 Caffeinated Candy
',
+ '
加速卷轴 加速捲軸 Scroll of Swiftness
',
+ '
守护卷轴 守護捲軸 Scroll of Protection
',
+ '
化身卷轴 化身捲軸 Scroll of the Avatar
',
+ '
羽毛碎片 羽毛碎片 Featherweight Shard
',
+ '
',
+ '
',
+ '
[C!!]压榨届使用额外的库存检查 壓榨屆使用額外的庫存檢查 Extra supply check for Grind Fest ;',
+ '
库存 庫存 Warn if supply <max(100%,
%)
时提示 時提示 ;',
+ '
',
+ '
体力长效药 體力長效藥 Health Draught
',
+ '
魔力长效药 魔力長效藥 Mana Draught
',
+ '
灵力长效药 靈力長效藥 Spirit Draught
',
+ '
火焰魔药 火焰魔藥 Infusion of Flames
',
+ '
冰冷魔药 冰冷魔藥 Infusion of Frost
',
+
+ '
体力药水 體力藥水 Health Potion
',
+ '
魔力药水 魔力藥水 Mana Potion
',
+ '
灵力药水 靈力藥水 Spirit Potion
',
+ '
闪电魔药 閃電魔藥 Infusion of Lightning
',
+ '
风暴魔药 風暴魔藥 Infusion of Storms
',
+
+ '
体力秘药 體力秘藥 Health Elixir
',
+ '
魔力秘药 魔力秘藥 Mana Elixir
',
+ '
灵力秘药 靈力秘藥 Spirit Elixir
',
+ '
神圣魔药 神聖魔藥 Infusion of Divinity
',
+ '
黑暗魔药 黑暗魔藥 Infusion of Darkness
',
+
+ '
花瓶 花瓶 Flower Vase
',
+ '
泡泡糖 泡泡糖 Bubble-Gum
',
+ '
终极秘药 終極秘藥 Last Elixir
',
+ '
吸收卷轴 吸收捲軸 Scroll of Absorption
',
+ '
虚空碎片 虛空碎片 Voidseeker Shard
',
+
+ '
能量饮料 能量飲料 Energy Drink
',
+ '
幻影卷轴 幻影捲軸 Scroll of Shadows
',
+ '
生命卷轴 生命捲軸 Scroll of Life
',
+ '
众神卷轴 眾神捲軸 Scroll of the Gods
',
+ '
以太碎片 以太碎片 Aether Shard
',
+
+ '
咖啡因糖果 咖啡因糖果 Caffeinated Candy
',
+ '
加速卷轴 加速捲軸 Scroll of Swiftness
',
+ '
守护卷轴 守護捲軸 Scroll of Protection
',
+ '
化身卷轴 化身捲軸 Scroll of the Avatar
',
+ '
羽毛碎片 羽毛碎片 Featherweight Shard
',
+ '
',
+ '
',
+ '
',
+
+ '
',
+ '
施放顺序(未配置的按照下面的顺序) 施放順序(未配置的按照下面的順序) Cast Order(Using order below as default if not configed) :
',
+ '
' ,
+ '
完全治愈(FC) 完全治愈(FC) Full-Cure
',
+ '
生命秘药(HE) 生命秘藥(HE) Health Elixir
',
+ '
最终秘药(LE) 最終秘藥(LE) Last Elixir
',
+ '
生命宝石(HG) 生命寶石(HG) Health Gem
',
+ '
生命药水(HP) 生命藥水(HP) Health Potion
',
+ '
治疗(Cure) 治療(Cure) Cure
',
+ '
魔力宝石(MG) 魔力寶石(MG) Mana Gem
',
+ '
魔力药水(MP) 魔力藥水(MP) Mana Potion
',
+ '
魔力秘药(ME) 魔力秘藥(ME) Mana Elixir
',
+ '
灵力宝石(SG) 靈力寶石(SG) Spirit Gem
',
+ '
灵力药水(SP) 靈力藥水(SP) Spirit Potion
',
+ '
灵力秘药(SE) 靈力秘藥(SE) Spirit Elixir
',
+ '
神秘宝石(Mystic) 神秘寶石(Mystic) Mystic Gem
',
+ '
咖啡因糖果(CC) 咖啡因糖果(CC) Caffeinated Candy
',
+ '
能量饮料(ED) 能量飲料(ED) Energy Drink
',
+ '
',
+
+ '
完全治愈(FC) 完全治愈(FC) Full-Cure : {{itemFCCondition}}
',
+ '
生命秘药(HE) 生命秘藥(HE) Health Elixir : {{itemHECondition}}
',
+ '
最终秘药(LE) 最終秘藥(LE) Last Elixir : {{itemLECondition}}
',
+ '
生命宝石(HG) 生命寶石(HG) Health Gem : {{itemHGCondition}}
',
+ '
生命药水(HP) 生命藥水(HP) Health Potion : {{itemHPCondition}}
',
+ '
治疗(Cure) 治療(Cure) Cure : {{itemCureCondition}}
',
+ '
魔力宝石(MG) 魔力寶石(MG) Mana Gem : {{itemMGCondition}}
',
+ '
魔力药水(MP) 魔力藥水(MP) Mana Potion : {{itemMPCondition}}
',
+ '
魔力秘药(ME) 魔力秘藥(ME) Mana Elixir : {{itemMECondition}}
',
+ '
灵力宝石(SG) 靈力寶石(SG) Spirit Gem : {{itemSGCondition}}
',
+ '
灵力药水(SP) 靈力藥水(SP) Spirit Potion : {{itemSPCondition}}
',
+ '
灵力秘药(SE) 靈力秘藥(SE) Spirit Elixir : {{itemSECondition}}
',
+ '
神秘宝石(Mystic) 神秘寶石(Mystic) Mystic Gem : {{itemMysticCondition}}
',
+ '
咖啡因糖果(CC) 咖啡因糖果(CC) Caffeinated Candy : {{itemCCCondition}}
',
+ '
能量饮料(ED) 能量飲料(ED) Energy Drink : {{itemEDCondition}}
',
+
+ '
',
+ '
获得引导时 (此时1点MP施法与150%伤害)獲得引導時 (此時1點MP施法與150%傷害)During Channeling effect (1 mp spell cost and 150% spell damage) :
',
+ '
超过时不释放 超過時不釋放 Not cast if remain turns above (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ): ',
+ '
',
+ '
守护(Pr) 守護(Pr) Protection >=
',
+ '
生命火花(SL) 生命火花(SL) Spark of Life >=
',
+ '
灵力盾(SS) 靈力盾(SS) Spirit Shield >=
',
+ '
疾速(Ha) 疾速(Ha) Haste >=
',
+ '
奥术集中(AF) 奧術集中(AF) Arcane Focus >=
',
+ '
穿心(He) 穿心(He) Heartseeker >=
',
+ '
细胞活化(Re) 細胞活化(Re) Regen >=
',
+ '
影纱(SV) 影紗(SV) Shadow Veil >=
',
+ '
吸收(Ab) 吸收(Ab) Absorb >=
',
+
+ '
',
+ '
',
+ '
先施放引导技能 先施放引導技能 First cast :
',
+ '
注意: 此处的施放顺序与 注意: 此處的施放順序与 Note: The cast order here is the same as in BUFF技能 Spells 里的相同 裡的相同 ',
+ '
',
+ '
灵力盾(SS) 靈力盾(SS) Spirit Shield
',
+ '
生命火花(SL) 生命火花(SL) Spark of Life
',
+ '
守护(Pr) 守護(Pr) Protection
',
+ '
吸收(Ab) 吸收(Ab) Absorb
',
+ '
影纱(SV) 影紗(SV) Shadow Veil
',
+ '
细胞活化(Re) 細胞活化(Re) Regen
',
+ '
疾速(Ha) 疾速(Ha) Haste
',
+ '
穿心(He) 穿心(He) Heartseeker
',
+ '
奥术集中(AF) 奧術集中(AF) Arcane Focus
',
+ '
',
+ '
',
+ '
再使用技能 再使用技能 Then use Skill : ',
+ '
施放顺序 施放順序 Cast Order :
',
+ '
',
+ '
完全治愈(FC) 完全治愈(FC) Full-Cure
',
+ '
治疗(Cure) 治療(Cure) Cure
',
+ '
灵力盾(SS) 靈力盾(SS) Spirit Shield
',
+ '
生命火花(SL) 生命火花(SL) Spark of Life
',
+ '
守护(Pr) 守護(Pr) Protection
',
+ '
吸收(Ab) 吸收(Ab) Absorb
',
+ '
影纱(SV) 影紗(SV) Shadow Veil
',
+ '
细胞活化(Re) 細胞活化(Re) Regen
',
+ '
疾速(Ha) 疾速(Ha) Haste
',
+ '
穿心(He) 穿心(He) Heartseeker
',
+ '
奥术集中(AF) 奧術集中(AF) Arcane Focus
',
+ '
',
+ '
',
+ '
最后ReBuff : 重新施放最先将要消失的Buff最後ReBuff : 重新施放最先將要消失的BuffAt last, re-cast the spells which will expire first .
',
+ '
',
+
+ '
',
+ '
施放顺序(未配置的按照下面的顺序) 施放順序(未配置的按照下面的順序) Cast Order(Using order below as default if not configed) : ',
+ ' ',
+ ' 灵力盾(SS) 靈力盾(SS) Spirit Shield ',
+ ' 生命火花(SL) 生命火花(SL) Spark of Life ',
+ ' 守护(Pr) 守護(Pr) Protection ',
+ ' 吸收(Ab) 吸收(Ab) Absorb ',
+ ' 影纱(SV) 影紗(SV) Shadow Veil ',
+ ' 细胞活化(Re) 細胞活化(Re) Regen ',
+ ' 疾速(Ha) 疾速(Ha) Haste ',
+ ' 穿心(He) 穿心(He) Heartseeker ',
+ ' 奥术集中(AF) 奧術集中(AF) Arcane Focus ',
+ '
',
+ '
Buff释放条件 Buff釋放條件 Cast spells Condition {{buffSkillCondition}}
',
+ '
生命长效药(HD) 生命長效藥(HD) Health Draught <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillHDCondition}}
',
+ '
魔力长效药(MD) 魔力長效藥(MD) Mana Draught <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillMDCondition}}
',
+ '
灵力长效药(MD) 靈力長效藥(MD) Spirit Draught <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillSDCondition}}
',
+ '
花瓶(FV) 花瓶(FV) Flower Vase <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillFVCondition}}
',
+ '
泡泡糖(BG) 泡泡糖(BG) Bubble-Gum <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillBGCondition}}
',
+ '
守护(Pr) 守護(Pr) Protection <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillPrCondition}}
',
+ '
生命火花(SL) 生命火花(SL) Spark of Life <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillSLCondition}}
',
+ '
灵力盾(SS) 靈力盾(SS) Spirit Shield <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillSSCondition}}
',
+ '
疾速(Ha) 疾速(Ha) Haste <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillHaCondition}}
',
+ '
奥术集中(AF) 奧術集中(AF) Arcane Focus <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillAFCondition}}
',
+ '
穿心(He) 穿心(He) Heartseeker <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillHeCondition}}
',
+ '
细胞活化(Re) 細胞活化(Re) Regen <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillReCondition}}
',
+ '
影纱(SV) 影紗(SV) Shadow Veil <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillSVCondition}}
',
+ '
吸收(Ab) 吸收(Ab) Absorb <= (阈值 < 0 则不限制 閾值 < 0 則不限制 Threshold < 0 as unlimited ) {{buffSkillAbCondition}}
',
+ '
',
+
+ '
',
+ '
Debuff释放条件 Debuff釋放條件 Cast debuff spells Condition {{debuffSkillCondition}}
',
+ '
[!!实验性]补全因超过默认显示上限未显示的怪物buff [!!實驗性]補全因超過默認顯示上限未顯示的怪物buff [!!Experimental]Auto fill hidden monster buffs due to display limitation DEBUG RECORD
',
+ '
',
+ '
超出6个debuff的默认显示上限时(例如同时使用jpx时可忽略上限): 超出6個debuff的默認顯示上限時(例如同時使用jpx時可忽略上限): When debuff count overflows 6 as the default maximum display count (such as ignore limitation while using jpx): 跳过 / Skip 警报 / Alert 忽略 / Ignore ',
+ '
剩余Turns低于阈值时警报 剩餘Turns低於閾值時警報 Alert when remain expire turns less than threshold ',
+ '
',
+ '
沉眠(Sl) 沉眠(Sl) Sleep
',
+ '
致盲(Bl) 致盲(Bl) Blind
',
+ '
虚弱(We) 虛弱(We) Weaken
',
+ '
沉默(Si) 沉默(Si) Silence
',
+ '
缓慢(Slo) 緩慢(Slo) Slow
',
+ '
陷危(Im) 陷危(Im) Imperil
',
+ '
混乱(Co) 混亂(Co) Confuse
',
+ '
枯竭(Dr) 枯竭(Dr) Drain
',
+ '
魔磁网(MN) 魔磁網(MN) MagNet
',
+ '
',
+ '
',
+
+ '
1.
特殊先给全体施放的顺序(未配置的按照下面的顺序) 特殊先給全體施放的順序(未配置的按照下面的順序) Cast Order for Special Debuff all enemies first(Using order below as default if not configed) :',
+ '
',
+ '
',
+ // Dr, MN无法覆盖全体
+ '
沉眠(Sl) 沉眠(Sl) Sleep
',
+ '
致盲(Bl) 致盲(Bl) Blind
',
+ '
虚弱(We) 虛弱(We) Weaken
',
+ '
沉默(Si) 沉默(Si) Silence
',
+ '
缓慢(Slo) 緩慢(Slo) Slow
',
+ '
枯竭(Dr) 枯竭(Dr) Drain
',
+ '
陷危(Im) 陷危(Im) Imperil
',
+ '
魔磁网/固定(MN) 魔磁網/固定(MN) MagNet/Immobilize
',
+ '
混乱(Co) 混亂(Co) Confuse
',
+ '
',
+ '
',
+
+ '
1.a.
特殊先给全体施放时,视作覆盖的互斥Debuff 特殊特殊先給全體施放時,視作覆蓋的互斥Debuff Exclusive debuffs during \'Cast Order for Special Debuff all enemies first\' :',
+ '
',
+ '
沉眠(Sl) 沉眠(Sl) Sleep
',
+ '
致盲(Bl) 致盲(Bl) Blind
',
+ '
虚弱(We) 虛弱(We) Weaken
',
+ '
沉默(Si) 沉默(Si) Silence
',
+ '
缓慢(Slo) 緩慢(Slo) Slow
',
+ '
枯竭(Dr) 枯竭(Dr) Drain
',
+ '
陷危(Im) 陷危(Im) Imperil
',
+ '
魔磁网/固定(MN) 魔磁網/固定(MN) MagNet/Immobilize
',
+ '
混乱(Co) 混亂(Co) Confuse
',
+ '
',
+ '
',
+
+ '
2.
单体施放顺序(未配置的按照下面的顺序) 單體施放順序(未配置的按照下面的順序) Cast Order for each enemy(Using order below as default if not configed) :',
+ '
',
+ '
',
+ '
沉眠(Sl) 沉眠(Sl) Sleep
',
+ '
致盲(Bl) 致盲(Bl) Blind
',
+ '
虚弱(We) 虛弱(We) Weaken
',
+ '
沉默(Si) 沉默(Si) Silence
',
+ '
缓慢(Slo) 緩慢(Slo) Slow
',
+ '
枯竭(Dr) 枯竭(Dr) Drain
',
+ '
陷危(Im) 陷危(Im) Imperil
',
+ '
魔磁网/固定(MN) 魔磁網/固定(MN) MagNet/Immobilize
',
+ '
混乱(Co) 混亂(Co) Confuse
',
+ '
',
+ '
',
+
+ '
特殊先给全体施放和单体施放使用共享的阈值、重复命中权重和各自独立的条件 特殊先給全體施放和單體施放使用共享的閾值、重複命中權重和各自獨立的條件 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 ',
+ '
',
+
+ '
',
+
+ '
沉眠(Sl) 沉眠(Sl) Sleep 阈值: 閾值: Threshold: ; EWF: {{debuffSkillSleCondition}}
',
+ '
特殊 Special 先给全体上沉眠(Sl) 先給全體上沉眠(Sl) Sleep all enemies first. 按照顺序而非权重 按照順序而非權重 By index instead of weight {{debuffSkillSleAllCondition}}
',
+
+ '
致盲(Bl) 致盲(Bl) Blind 阈值: 閾值: Threshold: ; EWF: {{debuffSkillBlCondition}}
',
+ '
特殊 Special 先给全体上致盲(Bl) 先給全體上致盲(Bl) Blind all enemies first. 按照顺序而非权重 按照順序而非權重 By index instead of weight {{debuffSkillBlAllCondition}}
',
+
+ '
虚弱(We) 虛弱(We) Weaken 阈值: 閾值: Threshold: ; EWF: {{debuffSkillWeCondition}}
',
+ '
特殊 Special 先给全体上虚弱(We) 先給全體上虛弱(We) Weaken all enemies first. 按照顺序而非权重 按照順序而非權重 By index instead of weight {{debuffSkillWeAllCondition}}
',
+
+ '
沉默(Si) 沉默(Si) Silence 阈值: 閾值: Threshold: ; EWF: {{debuffSkillSiCondition}}
',
+ '
特殊 Special 先给全体上沉默(Si) 先給全體上沉默(Si) Silence all enemies first. 按照顺序而非权重 按照順序而非權重 By index instead of weight {{debuffSkillSiAllCondition}}
',
+
+ '
缓慢(Slo) 緩慢(Slo) Slow 阈值: 閾值: Threshold: ; EWF: {{debuffSkillSloCondition}}
',
+ '
特殊 Special 先给全体上缓慢(Slo) 先給全體上緩慢(Slo) Slow all enemies first. 按照顺序而非权重 按照順序而非權重 By index instead of weight {{debuffSkillSloAllCondition}}
',
+
+ '
枯竭(Dr) 枯竭(Dr) Drain 阈值: 閾值: Threshold: ; EWF: {{debuffSkillDrCondition}}
',
+ '
特殊 Special 先给全体上枯竭(Dr) 先給全體上枯竭(Dr) Drain all enemies first. 按照顺序而非权重 按照順序而非權重 By index instead of weight {{debuffSkillDrAllCondition}}
',
+
+ '
陷危(Im) 陷危(Im) Imperil 阈值: 閾值: Threshold: ; EWF: {{debuffSkillImCondition}}
',
+ '
特殊 Special 先给全体上陷危(Im) 先給全體上陷危(Im) Imperil all enemies first. 按照顺序而非权重 按照順序而非權重 By index instead of weight {{debuffSkillImAllCondition}}
',
+
+ '
魔磁网/固定(MN) 魔磁網/固定(MN) MagNet/Immobilize 阈值: 閾值: Threshold: ; EWF: {{debuffSkillMNCondition}}
',
+ '
特殊 Special 先给全体上魔磁网/固定(MN) 先給全體上魔磁網/固定(MN) MagNet/Immobilize all enemies first. 按照顺序而非权重 按照順序而非權重 By index instead of weight {{debuffSkillMNAllCondition}}
',
+
+ '
混乱(Co) 混亂(Co) Confuse 阈值: 閾值: Threshold: ; EWF: {{debuffSkillCoCondition}}
',
+ '
特殊 Special 先给全体上混乱(Co) 先給全體上混亂(Co) Confuse all enemies first. 按照顺序而非权重 按照順序而非權重 By index instead of weight {{debuffSkillCoAllCondition}}
',
+
+ '
',
+ '
',
+
+ '
',
+
+ '
',
+ '
战役模式 戰役模式 Battle type : ',
+ '
竞技场(AR) 競技場(AR) The Arena 浴血擂台(RB) 浴血擂台(RB) Ring of Blood 压榨届(GF) 壓榨界(GF) GrindFest 道具届(IW) 道具界(IW) Item World 随机遭遇(ba) 隨機遭遇(ba) Encounter 塔楼(Tw) 塔樓(Tw) The Tower ',
+ '
魔药使用条件 魔藥使用條件 Infusion Use Condition {{infusionCondition}}
',
+ '
只使用与默认攻击模式相同的魔药 只使用與默認攻擊模式相同的魔藥 Use Infusion as same as default attack mode only.
',
+ '
',
+
+ '
火焰魔药 火焰魔藥 Infusion of Flames {{infusionFlamesCondition}}
',
+ '
冰冷魔药 冰冷魔藥 Infusion of Frost {{infusionFrostCondition}}
',
+ '
闪电魔药 閃電魔藥 Infusion of Lightning {{infusionLightningCondition}}
',
+ '
风暴魔药 風暴魔藥 Infusion of Storms {{infusionStormsCondition}}
',
+ '
神圣魔药 神聖魔藥 Infusion of Divinity {{infusionDivinityCondition}}
',
+ '
黑暗魔药 黑暗魔藥 Infusion of Darkness {{infusionDarknessCondition}}
',
+ '
',
+
+ '
',
+
+ '
',
+ '
自定义警报 自定義警報 Alarm ',
+ '
注意:留空则使用默认音频,建议每个用户使用自定义音频 注意:留空則使用默認音頻,建議每個用戶使用自定義音頻 Note: Leave the box blank to use default audio, it\'s recommended for all user to use custom audio. ',
+ '
',
+ ' 通用 Common : ',
+ ' 错误 錯誤 Error : ',
+ ' 失败 失敗 Defeat : ',
+ ' 答题 答題 Riddle : ',
+ ' 胜利 勝利 Victory :
',
+ '
请将将要测试的音频文件的地址填入这里 請將將要測試的音頻文件的地址填入這裡 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 Formula : ',
+ ' BOSS:Yggdrasil额外权重 BOSS:Yggdrasil額外權重 BOSS:Yggdrasil Extra Weight ',
+ ' 启用HP缓存 啟用HP緩存 Use HP Cache 清空缓存 清空緩存 Clear HP Cache 使用便携数据模式(导出脚本数据时将包含) 使用便攜數據模式(導出腳本數據時將包含) Portable Mode (will be included while exporting script datas)
',
+ '
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
',
+ '
以太窃取(ET) 以太竊取(ET) Ether Theft
',
+ '
灵力窃取(ST) 靈力竊取(ST) Spirit Theft
',
+ '
魔磁网/固定(MN) 魔磁網/固定(MN) MagNet/Immobilize
',
+ '
流动毒性(Po) 流动毒性(Po) Spreading Poison
',
+ '
眩晕(St) 眩暈(St) Stunned
',
+ '
魔力合流() 魔力合流(CM) Coalesced Mana
',
+ '
焚燒的靈魂(BS) 焚燒的靈魂(BS) Burning Soul
',
+ '
鮮美的靈魂(RS) 鮮美的靈魂(RS) Ripened Soul
',
+ '
',
+ '
降抗性和攻击模式属性 降抗性和攻擊模式屬性 While elements between Resistance-lower-debuff and Attack-Mode matches [' + attackStatusType[option.attackStatus??0] + '] 相同时 相同時 :
',
+ '
',
+ '
降抗性和攻击模式属性 降抗性和攻擊模式屬性 While elements between Resistance-lower-debuff and Attack-Mode NOT matches [' + attackStatusType[option.attackStatus??0] + '] 不相同时 不相同時 :
',
+ '
',
+ '
敌方增益,暂不清楚具体效果,默认按0权重计算 敵方增益,暫不清楚具體效果,默認按0權重計算 Enemy Procs, Evvecf value unknown, weight default as 0 for now. :',
+ '
',
+ '
',
+ '
3. PW(X) -= Log10(1 + 武器攻击中央目标伤害倍率(副手及冲击技能) 乘以武器攻擊中央目標傷害倍率(副手及衝擊技能) Weapon Attack Central Target Damage Ratio (Offhand & Strike) ) 额外伤害比例: 額外傷害比例: Extra DMG Ratio: %
',
+ '
4. 额外权重公式 額外權重公式 Extra weight formula :
',
+ '
5. 优先选择权重最低的目标 優先選擇權重最低的目標 Choose target with lowest rank first ',
+ '
显示权重及顺序 顯示權重及順序 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]
)
',
+ '
',
+
+ '
',
+
+ '
',
+ '
重置数据记录 重置數據記錄 Reset Usage Tracking 使用便携数据模式(导出脚本数据时将包含) 使用便攜數據模式(導出腳本數據時將包含) Portable Mode (will be included while exporting script datas)
',
+ '
自身 自身 Self ',
+ '
' ,
+ '
Turns
',
+ '
Rounds
',
+ '
Battle
',
+ '
Monster
',
+ '
Boss
',
+ '
闪避 閃避 Evade
',
+ '
未命中 未命中 Miss
',
+ '
集中 集中 Focus
',
+ '
MP总消耗 總消耗 Cost
',
+ '
OC总消耗 總消耗 Cost
',
+ '
',
+ '
',
+ '
',
+ '
受伤 (总量) 受傷 (總量) Hurt (Amount)2> ',
+ '
' ,
+ '
平均 平均 Avg
',
+ '
次数 次數 Count
',
+ '
总量 總量 Total
',
+ '
法术平均 法術平均 Magic Avg
',
+ '
法术次数 法術次數 Magic Count
',
+ '
法术总量 法術總量 Magic Total
',
+ '
物理平均 物理平均 Physical Avg
',
+ '
物理次数 物理次數 Physical Count
',
+ '
物理总量 物理總量 Physical Total
',
+ '
',
+ '
',
+ '
',
+ '
',
+
+ '
',
+
+ '
',
+ '
反馈 Feedback ',
+ '
',
+ '
反馈说明 反饋說明 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)
',
+ '
debugCheckCondition: prefix@/# to log result in console, @for formula, #for param : {{debugCondition}}
',
+ '
',
+ '
',
+
+ '',
+ ' 重置设置 重置設置 Reset 应用 應用 Apply 取消 Cancel ',
+ '
',
+ ].join('').replace(/{{(.*?)}}/g, '
');
+
+ gE('#hvAATab-Main').style.zIndex = 1;
+ optionBox.style.display = 'none';
+ gE('select[name="lang"]').value = g().lang;
+
+ // 绑定事件
+ (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);
+ };
+ 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]} `;
+ }
+ } else {
+ if (getValue('drop')) {
+ drop.__name = getValue('battleCode', true)?.name;
+ dropOld.push(drop);
+ }
+ dropOld.reverse();
+ _html = `${_html} `;
+ dropOld.forEach((_dropOld) => {
+ _html = `${_html}${_dropOld.__name} `;
+ });
+ _html = `${_html} `;
+ getKeys(dropOld).forEach((key) => {
+ if (key === '__name') {
+ return;
+ }
+ _html = `${_html}${key} `;
+ dropOld.forEach((_dropOld) => {
+ if (key in _dropOld) {
+ _html = `${_html}${_dropOld[key]} `;
+ } else {
+ _html = `${_html} `;
+ }
+ });
+ _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 ',
+ restore: '回复 (总量) 回复 (總量) Restore (Amount) ',
+ items: '物品 (次数) 物品 (次數) Items (Frequency) ',
+ magic: '技能 (次数) 技能 (次數) Magic (Frequency) ',
+ damage: '伤害 (总量) 傷害 (總量) Damage (Amount) ',
+ proficiency: '熟练度 (总量) 熟練度 (總量) Proficiency (Amount) ',
+ hurt: '受伤 (总量) 受傷 (總量) Loss (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', true)?.name;
+ 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) => {
+ _html = `${_html}${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);
+ }
+ });
+ } 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);
+ };
+ });
}
- } else if (inputs[i].type === 'text' || inputs[i].type === 'hidden') {
- itemName = inputs[i].name;
- itemValue = inputs[i].value || inputs[i].placeholder;
- if (itemValue === '') {
- continue;
+ 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();
+ };
+ });
+
+ 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;
}
- } else if (inputs[i].type === 'checkbox') {
- itemName = inputs[i].id;
- itemValue = inputs[i].checked;
- if (itemValue === false) {
- continue;
+ const select = gE('select[name="groupChoose"]', customizeBox);
+ select.textContent = '';
+ for (let i = 0; i <= group.length; i++) {
+ const option = select.appendChild(cE('option'));
+ if (i === group.length) {
+ option.value = 'new';
+ option.textContent = 'new';
+ } else {
+ option.value = i + 1;
+ option.textContent = i + 1;
+ }
}
- } 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;
- } else {
- if (!(itemArray[0] in _option)) {
- _option[itemArray[0]] = {};
+ 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')) {
+ creatCustomizeBox();
}
- if (inputs[i].className === 'customizeInput') {
- if (typeof _option[itemArray[0]][itemArray[1]] === 'undefined') {
- _option[itemArray[0]][itemArray[1]] = [];
+ updateGroup();
+ if (target.className !== 'customize' && target.parentNode.className !== 'customize') {
+ if (!target.className.match('customize')) {
+ gE('.customizeBox').style.zIndex = -1;
}
- _option[itemArray[0]][itemArray[1]].push(itemValue);
- } else {
- _option[itemArray[0]][itemArray[1]] = itemValue;
+ return;
+ }
+ g('customizeTarget', target);
+ const position = target.getBoundingClientRect();
+ const bodyPosition = document.body.getBoundingClientRect();
+ gE('.customizeBox').style.zIndex = 20;
+ gE('.customizeBox').style.top = `${position.bottom - bodyPosition.top}px`;
+ gE('.customizeBox').style.left = `${position.left - bodyPosition.left}px`;
+ gE('.customizeBox').style.cssText += `display: block; height: ${gE('.customizeGroup', 'all', g().customizeTarget).length * 30 + 60}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('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('.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('.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;
+ });
+ };
+
+ 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.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;
}
- }
- inputs = gE('.hvAAQuickSite input[type="text"]', 'all', optionBox);
- for (i = 0; 3 * i < inputs.length; i++) {
- if (i === 0 && inputs.length !== 0) {
- _option.quickSite = [];
+ 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"',
}
- if (inputs[3 * i + 1].value === '') {
- continue;
+ const isGetOrderFromId = ['.buffSkillOrder', '.debuffSkillOrder', '.debuffSkillOrderAll', '.skillOrder', '.infusionOrder'];
+ for (let ui in orderValues) {
+ gE(ui, optionBox).onclick = optionBox2Order(orderValues[ui], isGetOrderFromId.includes(ui) ? getOrderFromId : undefined);
}
- _option.quickSite.push({
- fav: inputs[3 * i].value,
- name: inputs[3 * i + 1].value,
- url: inputs[3 * i + 2].value,
- });
- }
- setValue('option', _option);
- optionBox.style.display = 'none';
- // 更改设置后实时刷新竞技场数据
- 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';
- };
- 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') {
- continue;
+
+ // 标签页-警报
+ 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', true);
+ delValue('monsterDB', false);
+ delValue('monsterMID', true);
+ delValue('monsterMID', false);
+ };
+ gE('#portable_monsterDB', optionBox).onclick = function () {
+ gE('#portable_monsterMID', optionBox).checked = this.checked;
}
- 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]];
+ // 标签页-掉落监测
+ gE('.reDropMonitor', optionBox).onclick = function () {
+ if (_alert(1, '是否重置', '是否重置', '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;
}
- 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;
+ // 标签页-数据记录
+ gE('.reRecordUsage', optionBox).onclick = function () {
+ if (_alert(1, '是否重置', '是否重置', '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;
}
- }
- 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]) {
+ // 标签页-关于本脚本
+ 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 = _alert(2, '请输入当前配置代号(或默认使用当前时间)', '請輸入當前配置代號(或默認使用當前時間)', '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 = _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);
+ goto();
+ }
+ };
+ gE('.hvAAReset', optionBox).onclick = function () {
+ if (_alert(1, '是否重置', '是否重置', 'Whether to reset')) {
+ const option = getValue('option');
+ const newOption = {};
+ setValue('option', Object.fromEntries(excludeStandalone.option.map(ex => [ex, option[ex]])));
+ if (_alert(1, '已重置,是否刷新', '已重置,是否刷新', 'Reseted. Page reload?')) {
+ goto();
+ }
+ }
+ };
+ gE('.hvAAApply', optionBox).onclick = function () {
+ if (gE('select[name="attackStatus"] option[value="-1"]:checked', optionBox) ||
+ !gE('select[name="attackStatus"] option: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;
+ }
+
+ const arenaPrev = g().option?.idleArenaValue;
+
+ const _option = { version: scriptVersion.ver };
+ 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;
+ }
+ if (input.className === 'customizeInput') {
+ ((_option[array[0]] ??= {})[array[1]]??=[]).push(value);
+ } else {
+ (_option[array[0]] ??= {})[array[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 });
+ }
+ setValue('option', _option);
+
+ optionBox.style.display = 'none';
+ // 清除不再需要的portable数据
+ for (const key of 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';
+ };
+ })();
+
+ // 加载UI数据
+ if (option) {
+
+ for (const obj in option) {
+ if (gE(`[name="${obj}"],[id="${obj}"]`, optionBox)) continue;
+ if (option[obj] instanceof Object) {
+ let found = false;
+ for (const key in option[obj]) {
+ if (found ||= gE(`[name="${obj}_${key}"],[id="${obj}_${key}"]`, optionBox)) continue;
+ console.log(`Legacy option deleted: ${obj}_${key}`);
+ delete option[obj][key];
+ }
+ if (found) continue;
+ }
+ console.log(`Legacy option deleted: ${obj}`);
+ delete option[obj];
+ }
+
+ let i, j, k;
+ const inputs = gE('input,select', 'all', optionBox);
+ const displayCheckBoxNotDefault = function (input) {
+ if (!gE(`label[for="${input.id}"]`) || input.placeholder === undefined) {
+ return;
+ }
+ if (!!input.checked !== !!input.placeholder) {
+ gE(`label[for="${input.id}"]`).classList.add('optionEdited');
+ } else {
+ gE(`label[for="${input.id}"]`).classList.remove('optionEdited');
+ }
+ }
+ 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;
+ }
+ }
+
+ 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 = type === 'text' ? 'number' : type;
+ }
+ [array, value] = [name.split('_'), undefined];
+ if (array.length === 1) {
+ value = formatValue(option[name], placeholder);
+ if (value || value === 0) option[name] = value;
+ } else if (input.className !== 'hvAACustomize') {
+ 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;
+ }
+ 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));
+ }
+ }
+ g('option', option);
+
+ const customize = gE('.customize', 'all', optionBox);
+ for (i = 0; i < customize.length; i++) {
+ name = customize[i].getAttribute('name');
+ if (!(name in option)) continue;
+ for (j in option[name]) {
const group = customize[i].appendChild(cE('div'));
group.className = 'customizeGroup';
group.innerHTML = `${j * 1 + 1}. `;
- for (k = 0; k < _option[itemName][j].length; k++) {
+ for (k = 0; k < option[name][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];
+ input.name = `${name}_${j}`;
+ input.value = option[name][j][k];
+ customizeInputAutoFit(input);
}
}
}
- }
- if (_option.quickSite) {
- _html = '图标 圖標 ICON 名称 名稱 Name 链接 鏈接 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) {
- _html = `${_html}${i} `;
+
+ let _html;
+ if (option.quickSite) {
+ _html = '图标 圖標 ICON 名称 名稱 Name 链接 鏈接 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) {
+ _html = `${_html}${i} `;
+ }
+ gE('.hvAABackupList', optionBox).innerHTML = _html;
}
- gE('.hvAABackupList', optionBox).innerHTML = _html;
}
}
- }
- function customizeBox() { // 自定义条件界面
- const customizeBox = gE('body').appendChild(cE('div'));
- customizeBox.className = 'customizeBox';
- const statusOption = [
- 'hp ',
- 'mp ',
- 'sp ',
- 'oc ',
- '- - - - ',
- 'monsterAll ',
- 'monsterAlive ',
- 'bossAll ',
- 'bossAlive ',
- '- - - - ',
- 'roundNow ',
- 'roundAll ',
- 'roundLeft ',
- 'roundType ',
- 'attackStatus ',
- 'turn ',
- '- - - - ',
- 'isCd ',
- 'buffTurn ',
- ' ',
- ].join('');
- customizeBox.innerHTML = [
- '? ? ',
- `${String.fromCharCode(0x21F1.toString(10))} `,
- ' ',
- `${statusOption} `,
- '> < ≥ ≤ = ≠ ',
- `${statusOption} `,
- 'ADD ',
- ].join(' ');
- const funcSelect = function (e) {
- let box;
- if (gE('#hvAAInspectBox')) {
- box = gE('#hvAAInspectBox');
+ function customizeInputAutoFit(input) {
+ if (input.type === 'select-one' || input.disabled && input.name !== 'version') return;
+ customizerInpuFit(input);
+ input.addEventListener('input', function(event) {
+ customizerInpuFit(input);
+ });
+ }
+
+ function customizerInpuFit(input) {
+ if (input.value === input.placeholder) {
+ input.classList.add('optionOrigin');
} else {
- box = gE('body').appendChild(cE('div'));
- box.id = 'hvAAInspectBox';
- }
- let { target } = e;
- let find = attr(target);
- while (!find) {
- target = target.parentNode;
- if (target.id === 'csp' || target.tagName === 'BODY') {
- box.style.display = 'none';
- return;
+ input.classList.remove('optionOrigin');
+ }
+ let length = input.value.length || input.placeholder?.length;
+ input.style.cssText += `width:${length+1}ch;`
+ }
+
+ function creatCustomizeBox() { // 自定义条件界面
+ const customizeBox = gE('body').appendChild(cE('div'));
+ customizeBox.className = 'customizeBox';
+ const statusOption = creatCustomizeBox.prototype.statusOption ??= [
+ 'hp ',
+ 'mp ',
+ 'sp ',
+ 'oc ',
+ 'hpDecimal ',
+ 'mpDecimal ',
+ 'spDecimal ',
+ 'ocDecimal ',
+ '- - - - ',
+ 'monsterAll ',
+ 'monsterAlive ',
+ 'bossAll ',
+ 'bossAlive ',
+ '- - - - ',
+ 'roundNow ',
+ 'roundAll ',
+ 'roundLeft ',
+ 'roundType ',
+ 'turn ',
+ 'isRoundType ',
+ 'ba ',
+ 'gr ',
+ 'iw ',
+ 'ar ',
+ 'rb ',
+ 'tw ',
+ '- - - - ',
+ 'attackStatus ',
+ 'phys ',
+ 'fire ',
+ 'cold ',
+ 'elec ',
+ 'wind ',
+ 'divi ',
+ 'forb ',
+ 'attackStatusCur ',
+ 'physCur ',
+ 'fireCur ',
+ 'coldCur ',
+ 'elecCur ',
+ 'windCur ',
+ 'diviCur ',
+ 'forbCur ',
+ 'fightingStyle ',
+ 'nt ',
+ '1h ',
+ '2h ',
+ 'dw ',
+ 'staff ',
+ '- - - - ',
+ 'skillOTOS ',
+ 'isCd ',
+ 'spirit ',
+ 'buffTurn ',
+ '- - - - ',
+ 'targetBuffTurn ',
+ 'targetIsAlive ',
+ 'targetHp ',
+ 'targetMp ',
+ 'targetSp ',
+ 'targetHpDecimal ',
+ 'targetMpDecimal ',
+ 'targetSpDecimal ',
+ 'targetOrder ',
+ 'targetWeight ',
+ 'targetRank ',
+ 'targetName ',
+ 'targetBossType ',
+ ' ',
+ ].join('');
+ customizeBox.style.cssText += 'display: none;';
+ customizeBox.innerHTML = [
+ '? ? ',
+ `${String.fromCharCode(0x21F1.toString(10))} `,
+ ' ',
+ `${statusOption} `,
+ '> < ≥(>=) ≤(<=) = ≠(!=,<>,~=) ',
+ `${statusOption} `,
+ 'ADD ',
+ ].join(' ');
+ const funcSelect = function (e) {
+ let box;
+ if (gE('#hvAAInspectBox')) {
+ box = gE('#hvAAInspectBox');
+ } else {
+ box = gE('body').appendChild(cE('div'));
+ box.id = 'hvAAInspectBox';
+ }
+ let { target } = e;
+ let find = attr(target);
+ while (!find) {
+ target = target.parentNode;
+ if (target.id === 'csp' || target.tagName.toUpperCase() === 'BODY') {
+ box.style.display = 'none';
+ return;
+ }
+ find = attr(target);
+ }
+ box.textContent = find;
+ box.style.display = 'block';
+ box.style.left = `${e.pageX - e.offsetX + target.offsetWidth}px`;
+ box.style.top = `${e.pageY - e.offsetY + target.offsetHeight}px`;
+ };
+ gE('.hvAAInspect', customizeBox).onclick = function () {
+ if (this.title === 'on') {
+ this.title = 'off';
+ gE('#csp').removeEventListener('mousemove', funcSelect);
+ } else {
+ this.title = 'on';
+ gE('#csp').addEventListener('mousemove', funcSelect);
+ }
+ };
+ gE('.groupAdd', customizeBox).onclick = function () {
+ 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;
+ group = target.appendChild(cE('div'));
+ group.className = 'customizeGroup';
+ group.innerHTML = `${groupChoose}. `;
+ selects[0].click();
+ } else {
+ group = gE('.customizeGroup', 'all', target)[groupChoose - 1];
+ }
+ const 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}`;
+ customizeInputAutoFit(input);
+ };
+
+ function attr(target) {
+ const onmouseover = target.getAttribute('onmouseover');
+ if (target.className === 'btsd') {
+ return `Skill Id: ${target.id}`;
+ } if (onmouseover && onmouseover.match('common.show_itemc_box')) {
+ return `Item Id: ${onmouseover.match(/(\d+)\)/)[1]}`;
+ } if (onmouseover && onmouseover.match('equips.set')) {
+ return `Equip Id: ${onmouseover.match(/(\d+)/)[1]}`;
+ } if (onmouseover && onmouseover.match('battle.set_infopane_effect')) {
+ return `Buff Img: ${target.src.match(/\/e\/(.*?).png/)[1]}`;
}
- find = attr(target);
}
- box.textContent = find;
- box.style.display = 'block';
- box.style.left = `${e.pageX - e.offsetX + target.offsetWidth}px`;
- box.style.top = `${e.pageY - e.offsetY + target.offsetHeight}px`;
- };
- gE('.hvAAInspect', customizeBox).onclick = function () {
- if (this.title === 'on') {
- this.title = 'off';
- gE('#csp').removeEventListener('mousemove', funcSelect);
- } else {
- this.title = 'on';
- gE('#csp').addEventListener('mousemove', funcSelect);
+ }
+
+ function setAlarm(e) { // 发出警报
+ const option = g().option??{};
+ e = e || 'Common';
+ if (option.notification) {
+ setNotification(e);
}
- };
- gE('.groupAdd', customizeBox).onclick = function () {
- 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;
- group = target.appendChild(cE('div'));
- group.className = 'customizeGroup';
- group.innerHTML = `${groupChoose}. `;
- selects[0].click();
- } else {
- group = gE('.customizeGroup', 'all', target)[groupChoose - 1];
+ if (option.alert && option.audioEnable && option.audioEnable[e]) {
+ setAudioAlarm(e);
}
- const 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}`;
- };
+ return true;
+ }
- function attr(target) {
- const onmouseover = target.getAttribute('onmouseover');
- if (target.className === 'btsd') {
- return `Skill Id: ${target.id}`;
- } if (onmouseover && onmouseover.match('common.show_itemc_box')) {
- return `Item Id: ${onmouseover.match(/(\d+)\)/)[1]}`;
- } if (onmouseover && onmouseover.match('equips.set')) {
- return `Equip Id: ${onmouseover.match(/(\d+)/)[1]}`;
- } if (onmouseover && onmouseover.match('battle.set_infopane_effect')) {
- return `Buff Img: ${target.src.match(/\/e\/(.*?).png/)[1]}`;
+ function setAudioAlarm(e) { // 发出音频警报
+ const option = g().option??{};
+ let audio;
+ if (gE(`#hvAAAlert-${e}`)) {
+ audio = gE(`#hvAAAlert-${e}`);
+ } else {
+ audio = gE('body').appendChild(cE('audio'));
+ audio.id = `hvAAAlert-${e}`;
+ const fileType = '.ogg'; // var fileType = (/Chrome|Safari/.test(navigator.userAgent)) ? '.mp3' : '.wav';
+ audio.src = (option.audio && option.audio[e]) ? option.audio[e] : `https://github.com/dodying/UserJs/raw/master/HentaiVerse/hvAutoAttack/${e}${fileType}`;
+ audio.controls = true;
+ audio.loop = (e === 'Riddle');
}
- }
- }
+ audio.play();
- function setAlarm(e) { // 发出警报
- e = e || 'Common';
- if (g('option').notification) {
- setNotification(e);
- }
- if (g('option').alert && g('option').audioEnable && g('option').audioEnable[e]) {
- setAudioAlarm(e);
- }
- }
+ function pauseAudio(e) {
+ audio.pause();
+ document.removeEventListener(e.type, pauseAudio, true);
+ }
+ document.addEventListener('mousemove', pauseAudio, true);
+ }
+
+ function setNotification(e) { // 发出桌面通知
+ const notification = setNotification.prototype.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,
+ },
+ BattleUnresponsive: {
+ text: '战斗无响应',
+ 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,
+ },
+ BattleUnresponsive: {
+ text: '戰鬥無響應',
+ 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,
+ },
+ BattleUnresponsive: {
+ text: 'Battle unresponsive',
+ time: 3,
+ },
+ Test: {
+ text: 'testText',
+ time: 3,
+ },
+ },
+ ][g().lang][e];
+ if (typeof GM_notification !== 'undefined') {
+ GM_notification({
+ text: notification.text,
+ image: `${window.location.origin}${unsafeWindow.IMG_URL}hentaiverse.png`,
+ highlight: g().option?.focusNotification,
+ timeout: 1000 * notification.time,
+ });
+ }
+ if (window.Notification && window.Notification.permission !== 'denied') {
+ window.Notification.requestPermission((status) => {
+ if (status === 'granted') {
+ const n = new window.Notification(notification.text, {
+ icon: `${unsafeWindow.IMG_URL}hentaiverse.png`,
+ });
+ setTimeout(() => {
+ if (n) {
+ n.close();
+ }
+ }, 1000 * notification.time);
- function setAudioAlarm(e) { // 发出音频警报
- let audio;
- if (gE(`#hvAAAlert-${e}`)) {
- audio = gE(`#hvAAAlert-${e}`);
- } else {
- 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');
+ const nClose = function (e) {
+ if (n) {
+ n.close();
+ }
+ document.removeEventListener(e.type, nClose, true);
+ };
+ document.addEventListener('mousemove', nClose, true);
+ }
+ });
+ }
}
- audio.play();
- function pauseAudio(e) {
- audio.pause();
- document.removeEventListener(e.type, pauseAudio, true);
+ function imgArray2img(...img) {
+ return img.join('_').replace('_png', 'png');
}
- document.addEventListener('mousemove', pauseAudio, true);
- }
- function setNotification(e) { // 发出桌面通知
- const notification = [
- {
- Common: {
- text: '未知',
- time: 5,
+ 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;
},
- Error: {
- text: '某些错误发生了',
- time: 10,
+ gr() {
+ return g().battle.roundType === 'gr' ? 1 : 0;
},
- Defeat: {
- text: '游戏失败\n玩家可自行查看战斗Log寻找失败原因',
- time: 5,
+ tw() {
+ return g().battle.roundType === 'tw' ? 1 : 0;
},
- Riddle: {
- text: '小马答题\n紧急!\n紧急!\n紧急!',
- time: 30,
+ rb() {
+ return g().battle.roundType === 'rb' ? 1 : 0;
},
- Victory: {
- text: '游戏胜利\n页面将在3秒后刷新',
- time: 3,
+ iw() {
+ return g().battle.roundType === 'iw' ? 1 : 0;
},
- Test: {
- text: '测试文本',
- time: 3,
+ ba() {
+ return g().battle.roundType === 'ba' ? 1 : 0;
},
- }, {
- Common: {
- text: '未知',
- time: 5,
+ isRoundType(t) {
+ return g().battle.roundType === t ? 1 : 0;
},
- Error: {
- text: '某些錯誤發生了',
- time: 10,
+ phys() {
+ return g().attackStatus * 1 === 0 ? 1 : 0;
},
- Defeat: {
- text: '遊戲失敗\n玩家可自行查看戰鬥Log尋找失敗原因',
- time: 5,
+ fire() {
+ return g().attackStatus * 1 === 1 ? 1 : 0;
},
- Riddle: {
- text: '小馬答題\n緊急!\n緊急!\n緊急!',
- time: 30,
+ cold() {
+ return g().attackStatus * 1 === 2 ? 1 : 0;
},
- Victory: {
- text: '遊戲勝利\n頁面將在3秒後刷新',
- time: 3,
+ elec() {
+ return g().attackStatus * 1 === 3 ? 1 : 0;
},
- Test: {
- text: '測試文本',
- time: 3,
+ wind() {
+ return g().attackStatus * 1 === 4 ? 1 : 0;
},
- }, {
- Common: {
- text: 'unknown',
- time: 5,
+ divi() {
+ return g().attackStatus * 1 === 5 ? 1 : 0;
},
- Error: {
- text: 'Some errors have occurred',
- time: 10,
+ forb() {
+ return g().attackStatus * 1 === 6 ? 1 : 0;
},
- Defeat: {
- text: 'You have been defeated.\nYou can check the battle log.',
- time: 5,
+ attackStatusCur() {
+ return getCurrentAttackStatus() * 1;
},
- Riddle: {
- text: 'Riddle\nURGENT\nURGENT\nURGENT',
- time: 30,
+ physCur() {
+ return getCurrentAttackStatus() * 1 === 0 ? 1 : 0;
},
- Victory: {
- text: 'You\'re victorious.\nThis page will refresh in 3 seconds.',
- time: 3,
+ fireCur() {
+ return getCurrentAttackStatus() * 1 === 1 ? 1 : 0;
},
- Test: {
- text: 'testText',
- time: 3,
+ 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;
},
- },
- ][g('lang')][e];
- if (typeof GM_notification !== 'undefined') {
- GM_notification({
- text: notification.text,
- image: `${window.location.origin}/y/hentaiverse.png`,
- highlight: true,
- timeout: 1000 * notification.time,
- });
- }
- 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',
- });
- setTimeout(() => {
- if (n) {
- n.close();
- }
- }, 1000 * notification.time);
- var nClose = function (e) {
- if (n) {
- n.close();
- }
- document.removeEventListener(e.type, nClose, true);
- };
- document.addEventListener('mousemove', nClose, true);
- // document.addEventListener('click', nClose, true);
+ 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)));
+ },
+ 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,
+ targetBuffTurn(...img) {
+ const getBuffTurn = (t, i) => getBuffTurnFromImg(getBuff(imgArray2img(i), getMonsterID(t)));
+ let param = minmaxModes.includes(img[0]) ? img.shift() : undefined;
+ return switchMaxMin(param, t => getBuffTurn(t, img));
+ },
+ 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}.`);
+ }
+ return currentGroup ? Object.keys(currentGroup).length : 0;
}
- });
- }
- }
+ };
- 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('.');
- let result;
- for (let key of paramList) {
- if (!result) {
- result = (g('battle') ?? getValue('battle', true))[key] ?? g(key) ?? getValue(key);
- continue;
- }
- result = result[key]
+ 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);
}
- return isNaN(result * 1) ? result : (result * 1);
- }
- return str * 1;
- };
- var func = {
- isCd(id) {
- return isOn(id) ? 0 : 1;
- },
- buffTurn(img) {
- let buff = gE(`#pane_effects>img[src*="${img}"]`);
- if (!buff) {
- return 0;
+ if (['acount', 'asum', 'amax', 'amin', 'agcount', 'agsum', 'agmax', 'agmin'].includes(param)) {
+ return switchMaxMin(param.replace(/^a/,''), defaultResult, true, targets);
}
- buff = buff.getAttribute('onmouseover').match(/\(.*,.*, (.*?)\)$/)[1] * 1;
- return isNaN(buff) ? Infinity : buff;
- },
- };
+ 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());
+ }
+ }
- for (i in parms) {
- for (j = 0; j < parms[i].length; j++) {
- if (!Array.isArray(parms[i])) {
- continue;
+ 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;
}
- k = parms[i][j].split(',');
- const kk = k.toString();
- k[0] = returnValue(k[0]);
- k[2] = returnValue(k[2]);
- if (k[0] === undefined || k[0] === null || (typeof k[0] !== "string" && isNaN(k[0]))) {
- Debug.log(kk[0], k[0]);
+ // 旧版本/强制使用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);
}
- if (k[2] === undefined || k[2] === null || (typeof k[2] !== "string" && isNaN(k[2]))) {
- Debug.log(kk[2], k[2]);
+ const paramList = str.replace(/[^\d](\.)/g, (match, ...p) => {
+ return match.replace('.', '_'); // 将不是数字小数点的 . 转为 _ 以便进行参数分割
+ }).split('_');
+ let result, isInData;
+ const option = g().option??{};
+ 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;
+ }
+ if (typeof result === 'string') {
+ result = JSON.parse(result);
+ }
+ if (['number', 'string'].includes(typeof result)) continue;
+ result = result[key];
}
+ result ??= isInData ? 0 : result; // 存在顶层数据时默认为0
+ return onResult(isNaN(result * 1) ? result ?? str : (result * 1));
+ }
+ getter.paramResultsGetter = paramResultsGetter;
+ return getter;
+ }
- switch (k[1]) {
+ 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':
- result[i] = k[0] > k[2];
- break;
+ return '>';
+ case '<':
case '2':
- result[i] = k[0] < k[2];
- break;
+ return '<';
+ case '≥':
+ case '>=':
case '3':
- result[i] = k[0] >= k[2];
- break;
+ return '>=';
+ case '≤':
+ case '<=':
case '4':
- result[i] = k[0] <= k[2];
- break;
+ return '<=';
+ case '=':
+ case '==':
+ case '===':
case '5':
- result[i] = k[0] === k[2];
- break;
+ return '==';
+ case '≠':
+ case '~=':
+ case '<>':
+ case '!=':
case '6':
- result[i] = k[0] !== k[2];
- break;
+ return '!=';
}
- if (result[i] === false) {
- j = parms[i].length;
+ }).replace(/===/g, '==').replace(/(?!~])=(?!=)/g, '==')
+ .replace(/≥|≤|≠|~=|<>/g, (match) => {
+ switch (match) {
+ case '≥':
+ return '>=';
+ case '≤':
+ return '<=';
+ case '≠':
+ case '~=':
+ case '<>':
+ return '!=';
}
- }
- if (result[i] === true) {
- return true;
- }
+ }).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(parms, targets = undefined) {
+ let i, j, k, target, paramResults={};
+ targets ??= [g().battle.monsterStatus[0]];
+ if (!parms || !Object.keys(parms).length) {
+ return targets[0];
+ }
+ const returnValue = returnValueGetter(() => paramResults, () => target);
+ for (i in parms) { for (target of targets.filter(t => !t.isDead)) {
+ paramResults={};
+ let parmResult = true;
+ for (j = 0; j < parms[i].length; j++) {
+ let result = true;
+ if (!Array.isArray(parms[i])) continue;
+ const formula = parms[i][j];
+ result = handleRPNFormula(formula, returnValue);
+ if (result) continue;
+ parmResult = false;
+ break;
+ }
+ if (parmResult) return target;
+ }} return undefined;
}
- 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);
- };
- });
- }
- const checkTime = function () {
- let time;
- if (typeof g('time') === 'undefined') {
- const timeDiv = gE('#riddlecounter>div>div', 'all');
- if (timeDiv.length === 0) {
- return;
+ function pauseChange() { // 暂停状态更改
+ const option = g().option??{};
+ if (getValue('disabled')) {
+ if (gE('.pauseChange')) {
+ gE('.pauseChange').innerHTML = `暂停 暫停 Pause ${(option.pauseHotkey && option.pauseHotkeyStr) ? `(${option.pauseHotkeyStr})` : '' }`;
}
- time = '';
- for (let j = 0; j < timeDiv.length; j++) {
- time = (timeDiv[j].style.backgroundPosition.match(/(\d+)px$/)[1] / 12).toString() + time;
+ document.title = gE('#navbar') ? 'The Hentaiverse' : getValue('disabled');
+ delValue(0);
+ if (!gE('#navbar')) { // in battle
+ onBattleRound();
}
- 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)]);
+ if (gE('.pauseChange')) {
+ gE('.pauseChange').innerHTML = `继续 繼續 Continue ${(option.pauseHotkey && option.pauseHotkeyStr) ? `(${option.pauseHotkeyStr})` : '' } `;
+ }
+ setValue('disabled', document.title);
+ document.title = _alert(-1, 'hvAutoAttack暂停中', 'hvAutoAttack暫停中', 'hvAutoAttack Paused');
}
- };
- for (let i = 0; i < 30; i++) {
- setTimeout(checkTime, i * _1s);
}
- 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 stepIn() {
+ setValue('stepIn', true);
+ if (getValue('disabled')) {
+ g('timeNow', time(0));
+ pauseChange();
}
}
- }
- // 战斗外//
- 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');
- const now = time(0);
- let url;
- if (eventpane) { // 新一天或遭遇战
- url = gE('#eventpane>div>a')?.href.split('/')[3];
- if(url === undefined){ // 新一天
- encounter = [];
+ function onStepInDone() {
+ if (!getValue('stepIn')) {
+ return;
}
- encounter.unshift({ href: 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) {
- continue;
- }
- url = e.href;
- break;
+ delValue('stepIn');
+ pauseChange();
+ }
+
+ 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;
}
}
}
- if (!url) {
- if (isEngage && !getValue('battle')) {
- // 自动跳转,同时先刷新遭遇时间,延长下一次遭遇
- $ajax.openNoFetch(getValue('lastHref'));
- }
- return;
+ // 战斗外//
+ function quickSite() { // 快捷站点
+ const quickSiteBar = gE('body').appendChild(cE('div'));
+ quickSiteBar.className = 'quickSiteBar';
+ quickSiteBar.innerHTML = '<< 贴吧 Forums ';
+ 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 === '<<') ? '>>' : '<<';
+ };
}
- // 减少因在恒定世界处于战斗中时打开eh触发了遭遇而导致的错失
- // 缓存当前链接,等战斗结束时再自动打开,下次打开链接时:
- // 1. 若新的遭遇未出现,进入已缓存的战斗链接
- // 2. 若新的遭遇已出现,则前一次已超时失效错过,重新获取新的一次
- if (!isEngage) { // 战斗外,非自动跳转
- eventpane.style.cssText += 'color:red;' // 链接标红提醒
- } else if (getValue('battle')) { //战斗中
- eventpane.style.cssText += 'color:gray;' // 链接置灰提醒
- } else { // 战斗外,自动跳转
- $ajax.openNoFetch(`${href}/${url}`);
+ function autoSwitchIsekai() {
+ if (!g().option?.isekai) {
+ // 若不启用自动跳转
+ return;
+ }
+ $ajax.openNoFetch(`${location.slice(0, location.indexOf('.org') + 4)}/${isIsekai ? '' : 'isekai/'}`);
}
- }
- function setEncounter(encounter) {
- return g('encounter', setValue('encounter', encounter));
- }
+ async function asyncOnIdle() { try {
+ await updateEncounter(false);
+ await waitPause();
+ $async.logSwitch(arguments);
+ const option = g().option??{};
+ const ready = {
+ isChecked: () => ready.supply && ready.repair && ready.encounter,
+ };
+ const idleStart = time(0);
+ await Promise.all([
+ // proficiency
+ (async () => { try {
+ ready.proficiency = await asyncSetProficiency() || true;
+ await tryEncounter();
+ } catch (err) { console.error(err); }})(),
+ // ability
+ (async () => { try {
+ ready.ability = await asyncSetAbilityData() || true;
+ await tryEncounter();
+ } catch (err) { console.error(err); }})(),
+ // stamina & hathperk
+ (async () => { try {
+ ready.stamina = await asyncSetStamina() || true;
+ await tryEncounter();
+ } catch (err) { console.error(err); }})(),
+ // item & supply
+ (async () => { try {
+ ready.item = await asyncGetItems() || true;
+ await tryEncounter();
+ ready.supply = checkSupply();
+ await tryEncounter();
+ } catch (err) { console.error(err); }})(),
+ // repair
+ (async () => { try {
+ ready.repair = await asyncCheckRepair();
+ await tryEncounter();
+ } catch (err) { console.error(err); }})(),
+ // equipment storage
+ (async () => { try {
+ ready.storage = await asyncCheckEquStorage();
+ await tryEncounter();
+ } catch (err) { console.error(err); }})(),
+ // arena data
+ updateArena(),
+ ]);
+ if (!ready.isChecked()) {
+ $async.logSwitch(arguments);
+ return;
+ }
+ if (option.idleArena && option.idleArenaValue) {
+ startUpdateArena(idleStart);
+ } else {
+ console.log("skip arena check");
+ }
+ setTimeout(autoSwitchIsekai, (option.isekaiTime * (Math.random() * 20 + 90) / 100) * _1s - (time(0) - idleStart));
+ $async.logSwitch(arguments);
- function getEncounter() {
- const getToday = (encounter) => encounter.filter(e => time(2, e.time) === time(2));
- const current = g('encounter') ?? [];
- let encounter = getValue('encounter', true) ?? [];
- if (JSON.stringify(current) === JSON.stringify(encounter)) {
- return getToday(encounter);
- }
- let dict = {};
- for (let e of current) {
- dict[e.href ?? `newDawn`] = e;
- }
- for (let e of encounter) {
- const key = e.href ?? `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;
+ async function tryEncounter() { try {
+ if (ready.encounterUpdated) {
+ return;
+ }
+ if (option.encounter) {
+ switch (true) {
+ case !ready.proficiency:
+ case !ready.ability:
+ case !ready.stamina:
+ case option.restoreStamina && !ready.item:
+ case option.encounterSupply && !ready.supply:
+ case option.encounterRepair && !ready.repair:
+ case option.encounterEquStorage && !ready.storage:
+ return;
+ }
+ }
+ ready.encounterUpdated = true;
+ $async.logSwitch(arguments);
+ ready.encounter ||= !(await updateEncounter(option.encounter));
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err); }}
+ } catch (err) { console.error(err); }}
+
+ function setEncounter(encounter) {
+ 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) ?? [];
+ if (JSON.stringify(current) === JSON.stringify(encounter)) {
+ return getToday(encounter);
+ }
+ let dict = {};
+ for (let e of current) {
+ 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;
+ encounter = [];
+ for (let i = 0; i <= times; i++) {
+ 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.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 getToday(Object.values(dict)).sort((x, y) => x.time < y.time ? 1 : x.time > y.time ? -1 : 0);
- }
- 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} `;
+ async function asyncSetProficiency() { try {
+ await waitPause();
+ $async.logSwitch(arguments);
+ const doc = $doc(await $ajax.insert('?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;
});
- }
- 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';
+ localStorage.setItem(`hvAA-${current}_proficiency`, JSON.stringify(proficiency));
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err); }}
+
+ async function asyncSetAbilityData() { try {
+ await waitPause();
+ $async.logSwitch(arguments);
+ const html = await $ajax.insert('?s=Character&ss=ab');
+ const doc = $doc(html);
+ 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 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()),
+ (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 (isIsekai || !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); }})()
+ ]);
+ if (!stamina.current) {
+ if (!getValue('stamina')) {
+ setValue('stamina', stamina);
+ }
+ $async.logSwitch(arguments);
+ return;
}
- this.textContent = (this.textContent === '<<') ? '>>' : '<<';
- };
- }
-
- function autoSwitchIsekai() {
- if (!g('option').isekai) {
- // 若不启用自动跳转
- return;
- }
- window.location.href = `${href.slice(0, href.indexOf('.org') + 4)}/${isIsekai ? '' : 'isekai/'}`;
- }
+ stamina.time = time(0);
+ if (!stamina.punish) {
+ [stamina.lastRatio, stamina.lastRatioRaw] = [stamina.ratio, stamina.ratioRaw];
+ [stamina.ratio, stamina.ratioRaw] = [undefined, undefined]
+ }
+ if (stamina.ratio === 1 && (stamina.lastRatio === 1 || !stamina.lastRatio)) {
+ [stamina.ratio, stamina.lastRatio, stamina.lastRatioRaw, stamina.ratioRaw] = Array(4).fill(undefined);
+ }
+ const lastCost = stamina.lastCost;
+ stamina.lastCost = undefined;
+ if (!lastCost || lastCost <= 0.06 ) {
+ setValue('stamina', stamina);
+ $async.logSwitch(arguments);
+ return;
+ }
+ last += Math.floor(stamina.time / _1h) - Math.floor(lastTime / _1h);
+ const delta = last - stamina.current;
+ if (!delta) {
+ setValue('stamina', stamina);
+ $async.logSwitch(arguments);
+ return;
+ }
+ 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;
+ }
+ [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 asyncSetAbilityData() { try {
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await asyncSetAbilityData();
- }
- logSwitchAsyncTask(arguments);
- const html = await $ajax.fetch('?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;
+ async function asyncGetItems() { try {
+ const option = g().option??{};
+ if (!option.checkSupply && (isIsekai || !option.restoreStamina)) {
+ return;
}
- const html = await $ajax.fetch(href);
+ await waitPause();
+ $async.logSwitch(arguments);
+ const html = await $ajax.insert('?s=Character&ss=it');
+ const items = {};
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),
+ if (gE('#riddlecounter', doc) || gE('#battle_main', 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);
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err); }}
+
+ function checkSupply(isGFStandalone) {
+ const option = g().option??{};
+ if (!option.checkSupply) {
+ return true;
+ }
+ const items = g().items;
+ if (!items) {
+ return false;
+ }
+ const thresholdList = isGFStandalone ? option.checkItemGF : option.checkItem;
+ const checkList = isGFStandalone ? option.isCheckGF : option.isCheck;
+ const percentage = isGFStandalone ? option.checkSupplyWarnGF : option.checkSupplyWarn;
+ const needs = [];
+ const warns = [];
+ for (let id in checkList) {
+ const item = items[id];
+ if (!item) continue;
+ let [name, count] = item;
+ const threshold = thresholdList[id] ?? 0;
+ const warnThreshold = threshold * percentage / 100;
+ count ??= 0;
+ if (count < warnThreshold) {
+ warns.push(`\n${name}(${count}<${warnThreshold}(${threshold}*${percentage}%))`);
}
- });
- } catch (e) {console.error(e)}}));
- setValue('ability', ability);
- logSwitchAsyncTask(arguments);
- } catch (e) {console.error(e)}}
-
- async function asyncSetEnergyDrinkHathperk() { try {
- if (isIsekai || !g('option').restoreStamina) {
- 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) {
- return;
- }
- const doc = $doc(html);
- const perks = gE('.stuffbox>table>tbody>tr', 'all', doc);
- if (!perks) {
- 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();
- }
- 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)}}
-
- async function asyncGetItems() { try {
- if (!g('option').checkSupply && (isIsekai || !g('option').restoreStamina)) {
- return;
- }
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await asyncGetItems();
- }
- logSwitchAsyncTask(arguments);
- const html = await $ajax.fetch('?s=Character&ss=it');
- const items = {};
- for (let each of gE('.nosel.itemlist>tbody', $doc(html)).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];
+ if (count >= threshold) {
+ continue;
+ }
+ needs.push(`\n${name}(${count}<${threshold})`);
+ }
+ if (needs.length) {
+ console.log(`Needs supply:${needs}`);
+ document.title = `[C!${isGFStandalone ? '!' : ''}]` + document.title;
+ switch(option.lang * 1) {
+ case 0:
+ popup(`消耗品${isGFStandalone ? '(压榨届独立配置)' : ''}不足:\n${needs}`);
+ break
+ case 1:
+ popup(`消耗品${isGFStandalone ? '(壓榨屆獨立配置)' : ''}不足:\n${needs}`);
+ break
+ case 2:
+ default:
+ popup(`Failed supply check${isGFStandalone ? ' for Grindfest standalone' : ''}:\n${needs}`);
+ break
+ }
+ } else if (warns.length) {
+ console.log(`Warn supply:${warns}`);
+ document.title = `[C!${isGFStandalone ? '!' : ''}]` + document.title;
+ switch(option.lang * 1) {
+ case 0:
+ popup(`消耗品${isGFStandalone ? '(压榨届独立配置)' : ''} < ${percentage}%:\n${warns}`);
+ break
+ case 1:
+ popup(`消耗品${isGFStandalone ? '(壓榨屆獨立配置)' : ''} < ${percentage}%:\n${warns}`);
+ break
+ case 2:
+ default:
+ popup(`Supplys ${isGFStandalone ? ' for Grindfest standalone' : ''} < ${percentage}%:\n${warns}`);
+ break
+ }
+ }
+ return !needs.length;
}
- g('items', items);
- logSwitchAsyncTask(arguments);
- } catch (e) {console.error(e)}}
- async function asyncCheckSupply() { try {
- if (!g('option').checkSupply) {
- return true;
- }
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await asyncCheckSupply();
- }
- 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;
- const threshold = thresholdList[id] ?? 0;
- if ((count ?? 0) >= threshold) {
- continue;
- }
- needs.push(`\n${name}(${count}<${threshold})`);
- }
- if (needs.length) {
- console.log(`Needs supply:${needs}`);
- document.title = `[C!]` + document.title;
- }
- logSwitchAsyncTask(arguments);
- return !needs.length;
- } catch (e) {console.error(e)} return false; }
+ async function asyncCheckRepair(isGrindFestStandalone) { try {
+ const option = g().option??{};
+ if (!option.repair) {
+ return true;
+ }
+ await waitPause();
+ $async.logSwitch(arguments);
+ let eqps;
+ const threshold = isGrindFestStandalone ? option.repairValueGF : option.repairValue;
+ if (threshold === undefined || threshold < 0) { // skip because default repair has been checked before idleArena>GF
+ $async.logSwitch(arguments);
+ return true;
+ }
- async function asyncCheckRepair() { try {
- if (!g('option').repair) {
- 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;
+ const url = `?s=Bazaar&ss=am&screen=repair&filter=equipped`;
+ const doc = $doc(await $ajax.insert(url));
+ if (gE('#riddlecounter', doc) || gE('#battle_main', doc)) {
+ $async.logSwitch(arguments);
+ return undefined;
}
- 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);
- if (eqps.length) {
- console.log('eqps need repair: ', eqps);
- document.title = `[R!]` + document.title;
- }
- logSwitchAsyncTask(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;
- }
- let checked = 0;
- if (!stmNRChecked) {
- checked = -1;
- }
- if (isIsekai || !g('option').restoreStamina) {
- return checked;
- }
- const items = g('items');
- if (!items) {
- return checked;
- }
- 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;
- }
- }
+ const token = gE('#equipform>input[name="postoken"]', doc).value;
+ 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 condition = 1 * gE('td:last-child', eqp).textContent.replace('%', '');
+ if (condition > threshold) {
+ return;
+ }
+ const after = $doc(await $ajax.insert(url, `&eqids[]=${id}&postoken=${token}&replace_charms=on`));
+ return gE(`#e${id}`, after) ? gE('.lc', eqp).childNodes[2].textContent : undefined;
+ } catch (err) { console.error(err); }}));
+
+ eqps = eqps.filter(e => e);
+ if (eqps.length) {
+ console.log('equips need repair:\n', eqps.join('\n '));
+ switch(option.lang * 1) {
+ case 0:
+ popup(`装备需要修理:\n${eqps.join('\n ')}`);
+ break
+ case 1:
+ popup(`裝備需要修理:\n${eqps.join('\n ')}`);
+ break
+ case 2:
+ default:
+ popup(`Equips need repair:\n${eqps.join('\n ')}`);
+ break
+ }
+ document.title = `[R!]` + document.title;
+ }
+ $async.logSwitch(arguments);
+ return !eqps.length;
+ } catch (err) { console.error(err); }; return false; }
- async function updateEncounter(engage, isInBattle) { try {
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await updateEncounter(engage, isInBattle);
- }
- const encounter = getEncounter();
- const encountered = encounter.filter(e => e.encountered && e.href);
- const count = encounter.filter(e => e.href).length;
-
- const now = time(0);
- 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;
- }
- cd = Math.max(0, cd);
- const ui = gE('.encounterUI') ?? (() => {
- const ui = gE('body').appendChild(cE('a'));
- ui.className = 'encounterUI';
- ui.title = `${time(3, last)}\nEncounter Time: ${count}`;
- if (!isInBattle) {
- ui.href = 'https://e-hentai.org/news.php?encounter';
- }
- return ui;
- })();
-
- const missed = count - encountered.length;
- if (count === 24) {
- ui.style.cssText += 'color:orange!important;';
- } else if (!cd) {
- 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 interval = cd > _1h ? _1m : (!g('option').encounterQuickCheck || cd > _1m) ? _1s : 80;
- interval = (g('option').encounterQuickCheck && cd > _1m) ? (interval - cd % interval) / 4 : interval; // 让倒计时显示更平滑
- setTimeout(() => updateEncounter(engage), interval);
- } catch (e) {console.error(e)}}
-
- function onEncounter() {
- if (getValue('disabled') || getValue('battle') || !checkBattleReady(onEncounter, { staminaLow: g('option').staminaEncounter })) {
- return;
- }
- setEncounter(getEncounter()); // 离开页面前保存
- if(!window.top.location.href.endsWith(`?s=Battle`)){
- setValue('lastHref', window.top.location.href);
- }
- $ajax.openNoFetch('https://e-hentai.org/news.php?encounter');
- }
+ async function asyncCheckEquStorage() { try {
+ const option = g().option??{};
+ if (!option.equStorage) {
+ return true;
+ }
+ await waitPause();
+ $async.logSwitch(arguments);
+ let count;
- async function startUpdateArena(idleStart, startIdleArena=true) { try {
- 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);
- }
- const last = getValue('arena', true)?.date ?? now;
- setTimeout(startUpdateArena, Math.max(0, Math.floor(last / _1d + 1) * _1d - now));
- } catch (e) {console.error(e)}}
-
- async function updateArena(forceUpdateToken = false) { try {
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await updateArena(forceUpdateToken);
- }
- 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];
+ const url = `?s=Bazaar&ss=am`;
+ const doc = $doc(await $ajax.insert(url));
+ if (gE('#riddlecounter', doc) || gE('#battle_main', doc)) {
+ $async.logSwitch(arguments);
+ return false;
+ }
+ count = gE('#equipblurb>table>tbody>tr>td:nth-child(2)', doc).innerText;
+
+ $async.logSwitch(arguments);
+ return count * 1 <= option.equStorageValue;
+ } 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;
}
- gE('img[src*="startchallenge.png"]', 'all', doc).forEach((_) => {
- const temp = _.getAttribute('onclick').match(/init_battle\((\d+),\d+,'(.*?)'\)/);
- arena.token[temp[1]] = temp[2];
- });
- } catch (e) {console.error(e)}}));
- }
- if(!isToday){
- arena.date = time(0);
- arena.gr = g('option').idleArenaGrTime;
- arena.arrayDone = [];
- }
- if (!isToday || !arena.isOptionUpdated) {
- arena.array = g('option').idleArenaValue.split(',') ?? [];
- arena.array.reverse();
- }
- return setValue('arena', arena);
- } catch (e) {console.error(e)}}
-
- 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
+ }
+ const option = g().option??{};
+ 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(
+ '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!]`, '');
return true;
+ }
+ 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!]`)) {
+ document.title = `[S!]` + document.title;
+ }
+ } else { // case -1: // failed with nature recover
+ switch(option.lang * 1) {
+ case 0:
+ popup('当日精力不足(含自然恢复)');
+ break
+ case 1:
+ popup('當日精力不足(含自然恢復)');
+ break
+ case 2:
+ default:
+ popup('Failed stamina check with nature recover.');
+ break
+ }
+ document.title = `[S!!]` + document.title;
+ }
+ }
+
+ async function getCurrentStamina() { try {
+ // await waitPause();
+ $async.logSwitch(arguments);
+ const doc = $doc(await $ajax.insert(window.location.href));
+ if (gE('#riddlecounter', doc) || gE('#battle_main', 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 = g().option??{};
+ let now = time(0);
+ let hours = Math.floor(now / _1h);
+ let [current, punish] = await getCurrentStamina();
+ 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 || isIsekai || !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;
+ }
+ $async.logSwitch(arguments);
+ return result;
}
- 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;
- }
- }
- async function idleArena() { try { // 闲置竞技场
- 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;
- }
- 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 = undefined;
- continue;
+ async function updateEncounter(engage) { try {
+ const option = g().option??{};
+ if (!option.encounter && !option.encounterDisplay) {
+ console.log("skip encounter check");
+ return false;
}
- if (id in arena.token) {
- break;
+ // await waitPause();
+ $async.logSwitch(arguments);
+ const encounter = getEncounter();
+ const count = encounter.filter(e => e.url).length;
+ const now = time(0);
+ const last = encounter[0]?.time ?? getValue('lastEH', true) ?? 0; // 上次遭遇 或 上次打开EH 或 0
+ let cd;
+ if (encounter.filter(e => e.url && (e.encountered || (time(0) - e.time >= 30 * _1m))).length >= 24) {
+ cd = Math.floor(encounter[0].time / _1d + 1) * _1d - now;
+ } else if (!last) {
+ cd = 0;
+ } else {
+ cd = _1h / 2 + last - now;
+ }
+ cd = Math.max(0, cd);
+ const ui = gE('.encounterUI') ?? (() => {
+ const ui = (gE('.hvAAPauseUI') ?? gE('body')).appendChild(cE('a'));
+ ui.className = 'encounterUI';
+ ui.title = `${time(3, last)}\nEncounter Time: ${count}`;
+ if (!gE('#battle_main')) {
+ ui.href = 'https://e-hentai.org/news.php?encounter';
+ }
+ return ui;
+ })();
+ const waitCD = option.encounterWaitCD;
+ const missed = count - encounter.filter(e => e.encountered && e.url).length;
+ if (count === 24) {
+ ui.style.cssText += 'color:orange!important;';
+ } 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 => pad(cdi)).join(`:`)}[${encounter.length ? (count >= 24 ? `☯` : count) : `✪`}${missed ? `-${missed}` : ``}]`;
+ if (engage) {
+ if (!cd) {
+ await waitPause();
+ onEncounter();
+ $async.logSwitch(arguments);
+ return true;
+ }
+ if (cd < 30 * _1m && encounter[0]?.url && !encounter[0].encountered) {
+ await waitPause();
+ $ajax.openNoFetch(encounter[0].url);
+ $async.logSwitch(arguments);
+ return true;
+ }
+ }
+ let interval = cd > _1h ? _1m : (!option.encounterQuickCheck || cd > _1m) ? _1s : 80;
+ interval = (option.encounterQuickCheck && cd > _1m) ? (interval - cd % interval) / 4 : interval; // 让倒计时显示更平滑
+ setTimeout(() => updateEncounter(engage), interval);
+ $async.logSwitch(arguments);
+ return engage && cd <= waitCD;
+ } catch (err) { console.error(err); }}
+
+ async function onEncounter() { try {
+ const option = g().option??{};
+ await until( // perhaps network connect not available
+ async () => await $ajax.insert(location) && (!option.checkURLBeforeNewRound || await $ajax.insert(option.checkURLBeforeNewRound)),
+ option.checkURLBeforeNewRoundRetry
+ );
+ $async.logSwitchStrict('updateEncounter', true);
+ if (getValue('disabled') || getValue('battle') || !await checkBattleReady(onEncounter, { staminaLow: option.staminaEncounter })) {
+ $async.logSwitchStrict('updateEncounter', false);
+ return;
+ }
+ setEncounter(getEncounter()); // 离开页面前保存
+ if (!window.top.location.href.endsWith(`?s=Battle`)) {
+ setValue('lastUrl', window.top.location.href);
+ }
+ // if (option.enchantEncounter) await asyncCheckEnchant();
+ $ajax.openNoFetch('https://e-hentai.org/news.php?encounter');
+ $async.logSwitchStrict('updateEncounter', false);
+ } catch (err) { console.error(err); }}
+
+ async function startUpdateArena(idleStart, startIdleArena = true) { 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);
+ }
+ const last = getValue('arena', true)?.date ?? now;
+ setTimeout(startUpdateArena, Math.max(0, Math.floor(last / _1d + 1) * _1d - now));
+ $async.logSwitchStrict('startUpdateArena', false);
+ } catch (err) { console.error(err); }}
+
+ async function updateArena(forceUpdateToken = false) { try {
+ 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.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 (err) { console.error(err); }})(s)));
+ }
+
+ const option = g().option??{};
+ if (!isToday) {
+ arena.date = time(0);
+ arena.gr = option.idleArenaGrTime;
+ arena.arrayDone = [];
+ }
+ if (!isToday || !arena.isOptionUpdated) {
+ arena.array = option.idleArenaValue?.split(',') ?? [];
+ arena.array.reverse();
+ }
+ arena.arrayDone = arena.arrayDone.filter(id => id === 'gr' || !arena.enabled?.includes(id.toString()));
+ $async.logSwitch(arguments);
+ return setValue('arena', arena);
+ } catch (err) { console.error(err); }}
+
+ async function idleArena() { try { // 闲置竞技场
+ let id;
+ let arena = getValue('arena', true);
+ const option = g().option??{};
+ const writeArenaStart = function () {
+ console.log('Arena Start', id);
+ document.title = _alert(-1, '闲置竞技场开始', '閒置競技場開始', 'Idle Arena start');
+ if (id !== 'gr') {
+ arena.arrayDone.push(id);
+ } else {
+ arena.gr--;
+ }
+ setValue('arena', arena);
+ }
+ if (arena.array.length === 0) {
+ setTimeout(autoSwitchIsekai, (option.isekaiTime * (Math.random() * 20 + 90) / 100) * _1s);
+ return;
}
- if (id >= 105) {
- arena.token = (await updateArena(true)).token;
- if (id in arena.token) {
+ $async.logSwitch(arguments);
+ const array = [...arena.array];
+ const RBundone = [];
+ if (!arena.enabled?.length) {
+ arena.enabled = (await updateArena(true)).enabled;
+ }
+ while (array.length > 0) {
+ id = array.pop();
+ if (arena.arrayDone?.includes(id)) {
+ id = undefined;
+ continue;
+ }
+ if (arena.enabled.includes(id)) {
break;
}
+ if (id >= 105) {
+ arena.enabled = (await updateArena(true)).enabled;
+ if (arena.enabled.includes(id)) {
+ break;
+ }
+ }
+ id = undefined;
}
- id = undefined;
- }
- if (!id) {
- setValue('arena', arena);
- logSwitchAsyncTask(arguments);
- return;
- }
- let staminaCost = {
- 1: 2, 3: 4, 5: 6, 8: 8, 9: 10,
- 11: 12, 12: 15, 13: 20, 15: 25, 16: 30,
- 17: 35, 19: 40, 20: 45, 21: 50, 23: 55,
- 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') {
- if (arena.gr <= 0) {
+ if (!id) {
+ console.log('No Arena Id Available', arena);
setValue('arena', arena);
- idleArena();
- arena.arrayDone.push('gr');
+ $async.logSwitch(arguments);
return;
}
- 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';
- }
- cost ??= staminaCost[key];
- if (!checkBattleReady(idleArena, { staminaCost: cost, checkEncounter: true })) {
- logSwitchAsyncTask(arguments);
- return;
- }
- document.title = _alert(-1, '闲置竞技场开始', '閒置競技場開始', 'Idle Arena start');
- if(key !== 'gr'){
- arena.arrayDone.push(key);
- }
- setValue('arena', arena);
- $ajax.open(`?s=Battle&ss=${href}`, `initid=${String(key)}&inittoken=${token}`);
- logSwitchAsyncTask(arguments);
- } catch (e) {console.error(e)}}
-
- // 战斗中//
- function onBattle() { // 主程序
- let battle = getValue('battle', true);
- if (!battle || !battle.roundAll) { // 修复因多个页面/世界同时读写造成缓存数据异常的情况
- 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'));
- };
- Debug.log('onBattle', `\n`, JSON.stringify(battle, null, 4));
- //人物状态
- 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('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);
- g('mp', gE('#dvbm>div>img').offsetWidth / 418 * 100);
- g('sp', gE('#dvbs>div>img').offsetWidth / 418 * 100);
- g('oc', gE('#dvrc').childNodes[0].textContent * 1);
- }
+ let staminaCost = {
+ 1: 2, 3: 4, 5: 6, 8: 8, 9: 10,
+ 11: 12, 12: 15, 13: 20, 15: 25, 16: 30,
+ 17: 35, 19: 40, 20: 45, 21: 50, 23: 55,
+ 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: 0
+ }
+ let stamina = getValue('stamina', true);
+ [stamina.current, stamina.punish] = await getCurrentStamina();
+ stamina.time = time(0);
+ for (let idx in staminaCost) {
+ staminaCost[idx] *= (isIsekai ? 2 : 1) * (stamina.current >= 60 ? 0.03 : 0.02);
+ }
- // 战斗战况
- if (!gE('.hvAALog')) {
- const div = gE('#hvAABox2').appendChild(cE('div'));
- div.className = 'hvAALog';
- }
- const status = [
- '物理 物理 Physical ',
- '火 火 Fire ',
- '冰 冰 Cold ',
- '雷 雷 Elec ',
- '风 風 Wind ',
- '圣 聖 Divine ',
- '暗 暗 Forbidden ',
- ];
- function getBattleTypeDisplay(isTitle) {
- const battleInfoList = {
- 'gr': {
- name: ['压榨', '壓榨', 'Grindfest'],
- title: 'GF',
- },
- 'iw': {
- name: ['道具', '道具', 'Item World'],
- title: 'IW',
- },
- 'ar': {
- name: ['竞技', '競技', 'Arena'],
- title: 'AR',
- list: [
- ['第一滴血', '第一滴血', 'First Blood', 1, 2],
- ['经验曲线', '經驗曲綫', 'Learning Curves', 10, 4],
- ['毕业典礼', '畢業典禮', 'Graduation', 20, 6],
- ['荒凉之路', '荒涼之路', 'Road Less Traveled', 30, 8],
- ['浪迹天涯', '浪跡天涯', 'A Rolling Stone', 40, 10],
- ['鲜肉一族', '鮮肉一族', 'Fresh Meat', 50, 12],
- ['乌云密布', '烏雲密佈', 'Dark Skies', 60, 15],
- ['风暴成形', '風暴成形', 'Growing Storm', 70, 20],
- ['力量流失', '力量流失', 'Power Flux', 80, 25],
- ['杀戮地带', '殺戮地帶', 'Killzone', 90, 30],
- ['最终阶段', '最終階段', 'Endgame', 100, 35],
- ['无尽旅程', '無盡旅程', 'Longest Journey', 110, 40],
- ['梦陨之时', '夢隕之時', 'Dreamfall', 120, 45],
- ['流亡之途', '流亡之途', 'Exile', 130, 50],
- ['封印之力', '封印之力', 'Sealed Power', 140, 55],
- ['崭新之翼', '嶄新之翼', 'New Wings', 150, 60],
- ['弑神之路', '弑神之路', 'To Kill a God', 165, 65],
- ['死亡前夜', '死亡前夜', 'Eve of Death', 180, 70],
- ['命运三女神与树', '命運三女神與樹', 'The Trio and the Tree', 200, 75],
- ['世界末日', '世界末日', 'End of Days', 225, 80],
- ['永恒黑暗', '永恆黑暗', 'Eternal Darkness', 250, 85],
- ['与龙共舞', '與龍之舞', 'A Dance with Dragons', 300, 90],
- ['额外游戏内容', '額外游戲内容', 'Post Game Content', 400, 95],
- ['神秘小马领域', '神秘小馬領域', 'Secret Pony Level', 500, 100],
- ],
- condition: (bt) => bt[4] === battle.roundAll,
- content: (bt) => bt[3],
- },
- 'rb': {
- name: ['浴血', '浴血', 'Ring of Blood'],
- title: 'RB',
- list: [
- ['九死一树', '九死一樹', 'Triple Trio and the Tree', 250, 'Yggdrasil'],
- ['飞天意面怪', '飛行義大利麵怪物', 'Flying Spaghetti Monster', 200],
- ['隐形粉红独角兽', '隱形粉紅獨角獸', 'Invisible Pink Unicorn', 150],
- ['现实生活', '現實生活', 'Real Life', 100],
- ['长门有希', '長門有希', 'Yuki Nagato', 75],
- ['朝仓凉子', '朝倉涼子', 'Ryouko Asakura', 75],
- ['朝比奈实玖瑠', '朝比奈實玖瑠', 'Mikuru Asahina', 75],
- ['泉此方', '泉此方', 'Konata', 75],
- ],
- condition: (bt) => monsterNames.indexOf(bt[4] ?? bt[2]) !== -1,
- content: (bt) => bt[3],
- },
- 'ba': {
- name: ['遭遇', '遭遇', 'Random Encounter'],
- title: 'BA',
- content: (_) => getEncounter().filter(e => e.encountered).length,
- },
- 'tw': {
- name: ['塔楼', '塔樓', 'The Tower'],
- title: 'TW',
- list: [
- ['PFUDOR×20', 'PFUDOR×20', 'PFUDOR×20', 40],
- ['IWBTH×15', 'IWBTH×15', 'IWBTH×15', 34],
- ['任天堂×10', '任天堂×10', 'Nintendo×10', 27],
- ['地狱×7', '地獄×7', 'Hell×7', 20],
- ['噩梦×4', '噩夢×4', 'Nightmare×4', 14],
- ['困难×2', '困難×2', 'Hard×2', 7],
- ['普通×1', '普通×1', 'Normal×1', 1],
- ],
- condition: (bt) => bt[3] && bt[3] <= battle.tower,
- content: (_) => battle.tower,
- 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 info = battleInfoList[type];
- switch (type) {
- case 'ar':
- case 'rb':
- case 'tw':
- case 'ba':
- for (let sub of (info.list ?? [[]])) {
- if (info.condition && !info.condition(sub)) {
- continue;
- }
- title = `${info.title}${info.content(sub)}`;
- if (!sub[lang]) {
+ let query;
+ id = isNaN(id) ? 'gr' : id;
+ if (id !== 'gr') {
+ query = id >= 105 ? 'rb' : 'ar';
+ } else {
+ if (arena.gr <= 0) {
+ setValue('arena', arena);
+ idleArena();
+ arena.arrayDone.push('gr');
+ return;
+ }
+ query = 'gr';
+ }
+ query = `?s=Battle&ss=${query}`;
+ if (id === 'gr' && ((option.checkSupplyGF && !checkSupply(true)) || (option.repairValueGF && !await asyncCheckRepair(true)))) {
+ console.log('Check gr Battle Ready Failed in supply/repair', 'id:', id, arena);
+ $async.logSwitch(arguments);
+ return;
+ }
+ const cost = staminaCost[id];
+ if (!await checkBattleReady(idleArena, { staminaCost: cost, checkEncounter: option.encounter, staminaLow: id === 'gr' ? option.staminaGrindFest : undefined })) {
+ console.log('Check Battle Ready Failed', 'id:', id, arena);
+ $async.logSwitch(arguments);
+ return;
+ }
+ let token = `&postoken=${gE('#initform>input[name="postoken"]', $doc(await $ajax.insert(query))).value}`;
+ await waitPause();
+ writeArenaStart();
+ await until(async () => !option.checkURLBeforeNewRound || await $ajax.insert(option.checkURLBeforeNewRound), option.checkURLBeforeNewRoundRetry);
+ // await asyncCheckEnchant(id === 'gr');
+ await until(async () => await $ajax.insert(query, `initid=${id === 'gr' ? 1 : id}${token}`), option.checkURLBeforeNewRoundRetry);
+ stamina.lastCost = id === 'gr' ? undefined : cost;
+ setValue('stamina', stamina);
+ if (option.altBattleFirst && await $ajax.insert(location.replace('hentaiverse.org', 'alt.hentaiverse.org').replace('alt.alt', 'alt'))) {
+ console.log('Arena Fetch Done.', 'altBattleFirst:', option.altBattleFirst, 'Arena goto alt', arena);
+ gotoAlt(true);
+ } else {
+ console.log('Arena Fetch Done.', 'altBattleFirst:', option.altBattleFirst, 'Arena goto', arena);
+ goto();
+ }
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err); }}
+
+ // 战斗中//
+ 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.monsterStatus = battle.monsterStatus.map(ms => {
+ return {
+ order: ms.order,
+ hp: ms.hp
+ }
+ });
+ battle.monsterStatus.sort(objArrSort('order'));
+ };
+ $debug.log('onBattle', `\n`, battle);
+ //人物状态
+ if (gE('#vbh')) {
+ 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);
+ g('mp', gE('#dvbm>div>img').offsetWidth / 418 * 100);
+ g('sp', gE('#dvbs>div>img').offsetWidth / 418 * 100);
+ g('oc', gE('#dvrc').childNodes[0].textContent * 1);
+ }
+
+ // 战斗战况
+ if (!gE('.hvAALog')) {
+ const div = gE('#hvAABox2').appendChild(cE('div'));
+ div.className = 'hvAALog';
+ }
+
+ function getBattleTypeDisplay(isTitle) {
+ const battleInfoList = getBattleTypeDisplay.prototype.battleInfoList ??= {
+ 'gr': {
+ name: ['压榨', '壓榨', 'Grindfest'],
+ title: 'GF',
+ },
+ 'iw': {
+ name: ['道具', '道具', 'Item World'],
+ title: 'IW',
+ },
+ 'ar': {
+ name: ['竞技', '競技', 'Arena'],
+ title: 'AR',
+ list: [
+ ['第一滴血', '第一滴血', 'First Blood', 1, 2],
+ ['经验曲线', '經驗曲綫', 'Learning Curves', 10, 4],
+ ['毕业典礼', '畢業典禮', 'Graduation', 20, 6],
+ ['荒凉之路', '荒涼之路', 'Road Less Traveled', 30, 8],
+ ['浪迹天涯', '浪跡天涯', 'A Rolling Stone', 40, 10],
+ ['鲜肉一族', '鮮肉一族', 'Fresh Meat', 50, 12],
+ ['乌云密布', '烏雲密佈', 'Dark Skies', 60, 15],
+ ['风暴成形', '風暴成形', 'Growing Storm', 70, 20],
+ ['力量流失', '力量流失', 'Power Flux', 80, 25],
+ ['杀戮地带', '殺戮地帶', 'Killzone', 90, 30],
+ ['最终阶段', '最終階段', 'Endgame', 100, 35],
+ ['无尽旅程', '無盡旅程', 'Longest Journey', 110, 40],
+ ['梦陨之时', '夢隕之時', 'Dreamfall', 120, 45],
+ ['流亡之途', '流亡之途', 'Exile', 130, 50],
+ ['封印之力', '封印之力', 'Sealed Power', 140, 55],
+ ['崭新之翼', '嶄新之翼', 'New Wings', 150, 60],
+ ['弑神之路', '弑神之路', 'To Kill a God', 165, 65],
+ ['死亡前夜', '死亡前夜', 'Eve of Death', 180, 70],
+ ['命运三女神与树', '命運三女神與樹', 'The Trio and the Tree', 200, 75],
+ ['世界末日', '世界末日', 'End of Days', 225, 80],
+ ['永恒黑暗', '永恆黑暗', 'Eternal Darkness', 250, 85],
+ ['与龙共舞', '與龍之舞', 'A Dance with Dragons', 300, 90],
+ ['额外游戏内容', '額外游戲内容', 'Post Game Content', 400, 95],
+ ['神秘小马领域', '神秘小馬領域', 'Secret Pony Level', 500, 100],
+ ],
+ condition: (bt) => bt[4] === battle.roundAll,
+ content: (bt) => bt[3],
+ },
+ 'rb': {
+ name: ['浴血', '浴血', 'Ring of Blood'],
+ title: 'RB',
+ list: [
+ ['九死一树', '九死一樹', 'Triple Trio and the Tree', 250, 'Yggdrasil'],
+ ['飞天意面怪', '飛行義大利麵怪物', 'Flying Spaghetti Monster', 200],
+ ['隐形粉红独角兽', '隱形粉紅獨角獸', 'Invisible Pink Unicorn', 150],
+ ['现实生活', '現實生活', 'Real Life', 100],
+ ['长门有希', '長門有希', 'Yuki Nagato', 75],
+ ['朝仓凉子', '朝倉涼子', 'Ryouko Asakura', 75],
+ ['朝比奈实玖瑠', '朝比奈實玖瑠', 'Mikuru Asahina', 75],
+ ['泉此方', '泉此方', 'Konata', 75],
+ ],
+ condition: (bt) => monsterNames.indexOf(bt[4] ?? bt[2]) !== -1,
+ content: (bt) => bt[3],
+ },
+ 'ba': {
+ name: ['遭遇', '遭遇', 'Random Encounter'],
+ title: 'BA',
+ content: (_) => getEncounter().filter(e => e.encountered).length,
+ },
+ 'tw': {
+ name: ['塔楼', '塔樓', 'The Tower'],
+ title: 'TW',
+ list: [
+ ['PFUDOR×20', 'PFUDOR×20', 'PFUDOR×20', 40],
+ ['IWBTH×15', 'IWBTH×15', 'IWBTH×15', 34],
+ ['任天堂×10', '任天堂×10', 'Nintendo×10', 27],
+ ['地狱×7', '地獄×7', 'Hell×7', 20],
+ ['噩梦×4', '噩夢×4', 'Nightmare×4', 14],
+ ['困难×2', '困難×2', 'Hard×2', 7],
+ ['普通×1', '普通×1', 'Normal×1', 1],
+ ],
+ condition: (bt) => bt[3] && bt[3] <= battle.tower,
+ content: (_) => battle.tower,
+ end: battle.tower > 40 ? `+${(battle.tower - 40) * 5}%DMG&HP` : '',
+ }
+ }
+ const type = battle.roundType;
+ let subtype, title;
+ const monsterNames = Array.from(gE(`${monsterStateKeys.name}>div>div`, 'all')).map(monster => monster.innerHTML);
+ const lang = g().lang * 1;
+ const info = battleInfoList[type];
+ switch (type) {
+ case 'ar':
+ case 'rb':
+ case 'tw':
+ case 'ba':
+ for (let sub of (info.list ?? [[]])) {
+ if (info.condition && !info.condition(sub)) {
+ continue;
+ }
+ title = `${info.title}${info.content(sub)}`;
+ if (!sub[lang]) {
+ break;
+ }
+ subtype = `${sub[lang] ? ` ${sub[lang]}` : ``}${info.end ? ` ${info.end}` : ``}`;
break;
}
- subtype = `${sub[lang] ? ` ${sub[lang]}` : ``}${info.end ? ` ${info.end}` : ``}`;
break;
- }
- break;
- case 'iw':
- case 'gr':
- title = `${info.title}`;
- break;
- default:
- break;
+ case 'iw':
+ case 'gr':
+ title = `${info.title}`;
+ break;
+ default:
+ break;
+ }
+ return isTitle ? title : `${(info?.name ?? ['未知', '未知', 'Unknown'])[lang]}:[${title}]${subtype ?? ''}`;
}
- return isTitle ? title : `${(info?.name ?? ['未知', '未知', 'Unknown'])[lang]}:[${title}]${subtype ?? ''}`;
- }
-
- const currentTurn = (battle.turn ?? 0) + 1;
-
- gE('.hvAALog').innerHTML = [
- `攻击模式 攻擊模式 Attack Mode : ${status[g('attackStatus')]}`,
- `${isIsekai ? '异世界 異世界 Isekai ' : '恒定世界 恆定世界 Persistent '}`, // 战役模式显示
- `${getBattleTypeDisplay()}`, // 战役模式显示
- `R${battle.roundNow}/${battle.roundAll}:T${currentTurn}`,
- `TPS: ${g('runSpeed')}`,
- `敌人 敌人 Monsters : ${g('monsterAlive')}/${g('monsterAll')}`,
- ].join(` `);
- if (!battle.roundAll) {
- pauseChange();
- Debug.shiftLog();
- }
- document.title = `${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')) {
- fixMonsterStatus();
- }
- countMonsterHP();
- displayMonsterWeight();
- displayPlayStatePercentage();
-
- if (getValue('disabled')) { // 如果禁用
- document.title = _alert(-1, 'hvAutoAttack暂停中', 'hvAutoAttack暫停中', 'hvAutoAttack Paused');
- gE('#hvAABox2>button').innerHTML = '继续 繼續 Continue ';
- return;
- }
- battle = getValue('battle', true);
- g('battle').turn = currentTurn;
- battle.turn = currentTurn;
- setValue('battle', battle);
- killBug(); // 解决 HentaiVerse 可能出现的 bug
+ const currentTurn = (battle.turn ?? 0) + 1;
- if (g('option').autoFlee && checkCondition(g('option').fleeCondition)) {
- gE('1001').click();
- SetExitBattleTimeout('Flee');
- return;
- }
- var taskList = {
- 'Pause': autoPause,
- 'Rec': autoRecover,
- 'Def': autoDefend,
- 'Scroll': useScroll,
- 'Channel': useChannelSkill,
- 'Buff': useBuffSkill,
- 'Infus': useInfusions,
- 'Debuff': useDeSkill,
- 'Focus': autoFocus,
- 'SS': autoSS,
- 'Skill': autoSkill,
- 'Atk': attack,
- };
- const names = g('option').battleOrderName?.split(',') ?? [];
- for (let i = 0; i < names.length; i++) {
- if(taskList[names[i]]()){
+ gE('.hvAALog').innerHTML = [
+ `攻击模式 攻擊模式 Attack Mode : ${attackStatusType[g().attackStatus]}`,
+ `${isIsekai ? '异世界 異世界 Isekai ' : '恒定世界 恆定世界 Persistent '}`, // 战役模式显示
+ `${getBattleTypeDisplay()}`, // 战役模式显示
+ `R${battle.roundNow}/${battle.roundAll}:T${currentTurn}`,
+ `TPS: ${g().runSpeed}`,
+ `敌人 敵人 Monsters : ${g().monsterAlive}/${g().monsterAll}`,
+ ].join(` `);
+ if (!battle.roundAll) {
+ pauseChange();
+ $debug.shiftLog();
+ }
+ const option = g().option??{};
+ 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) {
+ fixMonsterStatus();
+ }
+ countMonsterHP();
+ displayMonsterWeight();
+ displayPlayStatePercentage();
+ if (getValue('disabled')) { // 如果禁用
+ document.title = _alert(-1, 'hvAutoAttack暂停中', 'hvAutoAttack暫停中', 'hvAutoAttack Paused');
+ gE('#hvAABox2>button').innerHTML = `继续 繼續 Continue ${(option.pauseHotkey && option.pauseHotkeyStr) ? `(${option.pauseHotkeyStr})` : '' }`;
return;
}
- delete taskList[names[i]];
- }
- for (let name in taskList) {
- if (taskList[name]()) {
+ battle = getValue('battle', true);
+ g().battle.turn = currentTurn;
+ battle.turn = currentTurn;
+ setValue('battle', battle);
+ killBug(); // 解决 HentaiVerse 可能出现的 bug
+
+ if (option.autoFlee && checkCondition(option.fleeCondition)) {
+ gE('1001').click();
+ setExitBattleTimeout('Flee');
return;
}
- }
- }
-
- function getMonsterID(s) {
- if (s.order !== undefined) {
- return (s.order + 1) % 10;
- } // case is monsterStatus
- return (s + 1) % 10; // case is order
- }
-
- /**
- * 按照技能范围,获取包含原目标且范围内最终权重(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));
- 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; // 无法选中
+ const taskList = {
+ 'Cure': () => autoRecover(true),
+ 'Pause': autoPause,
+ 'SSDisable': () => autoSS(true),
+ 'Rec': () => autoRecover(false),
+ 'Scroll': useScroll,
+ 'Infus': useInfusions,
+ 'Def': autoDefend,
+ 'Channel': useChannelSkill,
+ 'Buff': useBuffSkill,
+ 'Debuff': useDeSkill,
+ 'Focus': autoFocus,
+ 'SS': () => autoSS(false),
+ 'Skill': autoSkill,
+ 'Atk': attack,
+ };
+ const names = option.battleOrderDefaultOnly ? [] : option.battleOrderName?.split(',') ?? [];
+ if (option.debugCheckCondition) {
+ checkCondition(option.debugCondition);
}
- 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;
- continue;
+ for (let i = 0; i < names.length; i++) {
+ if (taskList[names[i]]()) {
+ onStepInDone();
+ return;
}
- rank += mon.finWeight + cew; // 中心目标会受到副手及冲击攻击时,相当于有效生命值降低
+ delete taskList[names[i]];
}
- if (rank < minRank) {
- newOrder = i;
+ for (let name in taskList) {
+ if (taskList[name]()) {
+ onStepInDone();
+ return;
+ }
}
}
- return getMonsterID(newOrder);
- }
- function autoPause() {
- if (g('option').autoPause && checkCondition(g('option').pauseCondition)) {
- pauseChange();
- return true;
+ function getBuffTurnFromImg(buff) {
+ let duration = buff?.getAttribute('onmouseover').match(/\(.*,.*,(\s*)(.*?)\)$/)[2];
+ if (!duration) {
+ duration = 0;
+ } else if (['permanent', '-', "'-'"].includes(duration)) {
+ duration = Infinity;
+ } else {
+ duration *= 1;
+ }
+ return duration;
}
- return false;
- }
- function autoDefend() {
- if (g('option').defend && checkCondition(g('option').defendCondition)) {
- gE('#ckey_defend').click();
- return true;
+ function getMonsterID(s) {
+ if (s.order !== undefined) {
+ return (s.order + 1) % 10;
+ } // case is monsterStatus
+ return (s + 1) % 10; // case is order
}
- 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 getMonster(id) {
+ return gE(`#mkey_${id % 10}`);
}
- }
- function SetExitBattleTimeout(alarm){
- setAlarm(alarm);
- if(alarm === 'SkipDefeated') return;
- delValue(1);
- if(g('option').ExitBattleWaitTime) {
- setTimeout(() => {
- $ajax.open(getValue('lastHref'));
- }, g('option').ExitBattleWaitTime * _1s);
- return;
+ 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) => number} excludeWeightRatio target with id
+ * @returns
+ */
+ function getRangeCenter(target, range, isWeaponAttack, excludeWeight, forceUseIndex) {
+ let msTemp = JSON.parse(JSON.stringify(g().battle.monsterStatus));
+ msTemp.sort(objArrSort('order'));
+ let minWeight = Number.MAX_SAFE_INTEGER;
+ // 0. 范围大于等于全体时,直接释放全体
+ if (!range || range >= msTemp.length) {
+ return { id: getMonsterID(target), weight: minWeight };
+ }
+ const option = g().option??{};
+ 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 unreachableWeight = resolveRPNFormula(option.unreachableWeight, target);
+ // 1. 以选中目标为中心,优先向上
+ // 2. 超过顶部则向下找
+ // 3. 死亡、超过底下的将被溢出抛弃
+ const up = Math.floor(range / 2);
+ const down = range - up - 1;
+ const top = order < range ? 0 : Math.max(order - down, 0);
+ const bottom = Math.min(order + up, msTemp.length-1);
+ for (let i = top; i <= bottom; i++) {
+ let center = i;
+ if (msTemp[center].isDead) continue;
+ let weight = 0;
+ let overflow = Math.max(up-center,0);
+ const [min, max] = [center - up + overflow, center + down + overflow];
+ for (let inRange = min; inRange <= max; inRange++) {
+ let cew = inRange === center ? centralExtraWeight : 0; // cew <= 0, 增加未命中权重,降低命中权重
+ let mon = msTemp[inRange];
+ if (inRange < 0 || inRange >= msTemp.length || mon.isDead) { // 超出范围 或 死亡目标
+ weight += unreachableWeight;
+ continue;
+ }
+ if (excludeWeight) {
+ weight += excludeWeight(mon);
+ }
+ weight += cew; // 中心目标会受到副手及冲击攻击时,相当于有效生命值降低
+ weight += forceUseIndex ? -1 : mon.finWeight; // 强制使用顺序而非权重时,全部使用统一的权重而非怪物状态
+ }
+ if (weight < minWeight) {
+ newOrder = center;
+ minWeight = weight;
+ break;
+ }
+ }
+ return { id: getMonsterID(newOrder), weight: minWeight };
}
- $ajax.open(getValue('lastHref'));
- }
- function reloader() {
- let obj; let a; let cost;
- const battleUnresponsive = {
- 'Alert': { Method: setAlarm },
- 'Reload': { Method: goto },
- 'Alt': { Method: gotoAlt }
- }
- function clearBattleUnresponsive(){
- Object.keys(battleUnresponsive).forEach(t=>clearTimeout(battleUnresponsive[t].Timeout));
- }
- const eventStart = cE('a');
- eventStart.id = 'eventStart';
- eventStart.onclick = function () {
- 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) {
- obj = {
- mode: a.mode,
- };
- if (a.mode === 'items') {
- obj.item = gE(`#pane_item div[id^="ikey"][onclick*="skill('${a.skill}')"]`).textContent;
- } else if (a.mode === 'magic') {
- obj.magic = gE(a.skill).textContent;
- cost = gE(a.skill).getAttribute('onmouseover').match(/\('.*', '.*', '.*', (\d+), (\d+), \d+\)/);
- obj.mp = cost[1] * 1;
- obj.oc = cost[2] * 1;
- }
+ function autoPause() {
+ const option = g().option??{};
+ if (option.autoPause && checkCondition(option.pauseCondition)) {
+ pauseChange();
+ return true;
}
- };
- gE('body').appendChild(eventStart);
- const eventEnd = cE('a');
- eventEnd.id = 'eventEnd';
- eventEnd.onclick = function () {
- const timeNow = time(0);
- g('runSpeed', (1000 / (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);
- const battleLog = gE('#textlog>tbody>tr>td', 'all');
- if (g('option').recordUsage) {
- obj.log = battleLog;
- recordUsage(obj);
+ return false;
+ }
+
+ function autoDefend() {
+ const option = g().option??{};
+ if (option.defend && checkCondition(option.defendCondition)) {
+ updateSkillOTOS('defend');
+ gE('#ckey_defend').click();
+ return true;
}
- if (g('monsterAlive') && !gE('#btcp')) {
- clearBattleUnresponsive();
- onBattle();
+ return false;
+ }
+
+ function setExitBattleTimeout(alarm) {
+ lastResponsive = Infinity;
+ setAlarm(alarm);
+ const option = g().option??{};
+ if (alarm === 'Defeat' && !option.autoSkipDefeated) {
return;
}
- if (g('option').dropMonitor) {
- dropMonitor(battleLog);
- }
- if (g('option').recordUsage) {
- recordUsage2();
+ delValue(1);
+ setTimeoutOrExecute(() => $ajax.openNoFetch(getValue('lastUrl')), option.ExitBattleWaitTime * _1s);
+ }
+
+ async function checkResponsive() {
+ const option = g().option??{};
+ const battleUnresponsive = {
+ 'Alert': { method: () => setAlarm('BattleUnresponsive') },
+ 'Reload': { method: goto },
+ 'Alt': { method: gotoAlt }
}
- if (g('battle').roundNow !== g('battle').roundAll) { // Next Round
- if(g('option').NewRoundWaitTime){
- setTimeout(onNewRound, g('option').NewRoundWaitTime * _1s);
- } else {
- onNewRound();
+ let min = Infinity;
+ for (let t in option.battleUnresponsive) {
+ if (!option.battleUnresponsive[t] || !battleUnresponsive[t]) {
+ delete battleUnresponsive[t];
+ continue;
}
- return;
+ battleUnresponsive[t].time = Math.max(1, option.battleUnresponsiveTime[t]) * _1s;
+ min = Math.min(min, battleUnresponsive[t].time);
+ }
+ 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 (isBreak) break;
+ await pauseAsync(min - waited);
+ }
+ }
- async function onNewRound(){
- try {
- const html = await $ajax.fetch(window.location.href);
+ function reloader() {
+ let obj, a, cost;
+ const eventStart = cE('a');
+ eventStart.id = 'eventStart';
+ eventStart.onclick = function () {
+ const option = g().option??{};
+ a = unsafeWindow.info;
+ if (option.recordUsage) {
+ obj = {
+ mode: a.mode,
+ };
+ if (a.mode === 'items') {
+ obj.item = gE(`#pane_item div[id^="ikey"][onclick*="skill('${a.skill}')"]`).textContent;
+ } else if (a.mode === 'magic') {
+ obj.magic = gE(a.skill).textContent;
+ cost = gE(a.skill).getAttribute('onmouseover').match(/\('.*', '.*', '.*', (\d+), (\d+), \d+\)/);
+ obj.mp = cost[1] * 1;
+ obj.oc = cost[2] * 1;
+ }
+ }
+ };
+ gE('body').appendChild(eventStart);
+
+ const eventEnd = cE('a');
+ eventEnd.id = 'eventEnd';
+ eventEnd.onclick = function () {
+
+ const option = g().option??{};
+ const timeNow = time(0);
+ g('runSpeed', (1000 / (timeNow - g().timeNow)).toFixed(2));
+ g('timeNow', timeNow);
+ const monsterDead = gE('img[src*="nbardead"]', 'all').length;
+ 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 (option.recordUsage) {
+ obj.log = battleLog;
+ recordUsage(obj);
+ }
+ if (g().monsterAlive && !gE('#btcp')) {
+ onBattleRound();
+ return;
+ }
+ if (option.dropMonitor) {
+ dropMonitor(battleLog);
+ }
+ if (option.recordUsage) {
+ recordUsage2();
+ }
+ onRoundEnd();
+ async function onRoundEnd() { try {
+ await waitPause();
+ $async.logSwitch(arguments);
+ if (g().monsterAlive > 0) { // Defeat
+ setExitBattleTimeout('Defeat');
+ return;
+ }
+ if (g().battle.roundNow === g().battle.roundAll) { // Victory
+ setExitBattleTimeout('Victory');
+ return;
+ }
- 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();
+ 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;
}
- ['#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('monsterAlive') > 0) { // Defeat
- SetExitBattleTimeout(g('option').autoSkipDefeated ? 'SkipDefeated' : 'Defeat');
- }
- if (g('battle').roundNow === g('battle').roundAll) { // Victory
- SetExitBattleTimeout('Victory');
- }
- clearBattleUnresponsive();
- };
- gE('body').appendChild(eventEnd);
- window.sessionStorage.delay = g('option').delay;
- window.sessionStorage.delay2 = g('option').delay2;
- const fakeApiCall = cE('script');
- fakeApiCall.textContent = `api_call = ${function (b, a, d) {
- const delay = window.sessionStorage.delay * 1;
- const delay2 = window.sessionStorage.delay2 * 1;
- window.info = a;
- b.open('POST', `${MAIN_URL}json`);
- b.setRequestHeader('Content-Type', 'application/json');
- b.withCredentials = true;
- b.onreadystatechange = d;
- b.onload = function () {
- document.getElementById('eventEnd').click();
+ 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;
+ await loadUnsafeWindowBattle();
+ newRound(true);
+ onStepInDone();
+ onBattleRound();
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err); }}
};
- document.getElementById('eventStart').click();
- if (a.mode === 'magic' && a.skill >= 200) {
+ gE('body').appendChild(eventEnd);
+
+ const option = g().option??{};
+ window.sessionStorage.delay = option.delay;
+ window.sessionStorage.delay2 = option.delay2;
+ const fakeApiCall = cE('script');
+ fakeApiCall.textContent = `api_call = ${function (b, a, d) {
+ let delay = window.sessionStorage.delay * 1;
+ const delay2 = window.sessionStorage.delay2 * 1;
+ window.info = a;
+ 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) {
+ delay = delay2;
+ }
if (delay <= 0) {
b.send(JSON.stringify(a));
} else {
@@ -3087,1006 +5318,1365 @@ try {
b.send(JSON.stringify(a));
}, delay * (Math.random() * 50 + 50) / 100);
}
- } else if (delay2 <= 0) {
- b.send(JSON.stringify(a));
- } else {
- setTimeout(() => {
- b.send(JSON.stringify(a));
- }, delay2 * (Math.random() * 50 + 50) / 100);
- }
- }.toString()}`;
- 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;
- }
- return a;
- }
- } else {
+ }.toString()};
+ // bool
+ const isIsekai = ${isIsekai};
+ const isDisplay = ${option.isDisplayAllDebuff};
+ const debuffAutoFill = ${option.debuffAutoFill?.toString() ?? 'undefined'};
+ const debuffAutoFillRec = ${option.debuffAutoFillRec?.toString() ?? 'undefined'};
+ // string
+ const current = "${current}";
+ const other = "${other}";
+ // object
+ const local = ${JSON.stringify(local)};
+ const standalone = ${JSON.stringify(standalone)};
+ const excludeStandalone = ${JSON.stringify(excludeStandalone)};
+ const portable = ${JSON.stringify(portable)};
+ const sharable = ${JSON.stringify(sharable)};
+ const monsterStateKeys = ${JSON.stringify(monsterStateKeys)};
+ const ability = ${JSON.stringify(ability)};
+ const monsterBuffSkillLib = ${JSON.stringify(monsterBuffSkillLib)};
+ const hvVersion = Version(${hvVersion.ver});
+ // funciton
+ ${[updateMonsterEffects, fixMonsterStatus,
+ getMonsterID, getMonster, getMonster, getBuff,
+ 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) {
+ 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' ? {} : g().option??{};
+ 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];
}
- return false;
- }.toString()}`;
- gE('head').appendChild(fakeApiResponse);
- }
- function newRound(isNew) { // New Round
- let battle = isNew ? {} : getValue('battle', true);
- if (!battle) {
- battle = JSON.parse(JSON.stringify(g('battle') ?? {}));
- battle.monsterStatus?.sort(objArrSort('order'));
- };
- setValue('battle', battle);
- if (window.location.hash !== '') {
- goto();
- }
- g('monsterAll', gE('div.btm1', '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);
- const battleLog = gE('#textlog>tbody>tr>td', 'all');
- if (!battle.roundType) {
- const temp = battleLog[battleLog.length - 1].textContent;
+ 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);
+
+ 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][ability[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) {
+ // 统计持续时间及熟练度相关数据,以便进行核验和测试
+ const rec = JSON.parse(localStorage.getItem(`hvAA-${current}_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);
+ }
+ 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-${current}_rec`, JSON.stringify(rec));
+ }
+ }
+ // 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];
+ }
+
+ 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 = (`${isIsekai ? '/isekai' : ''}/y/e/${ getBuffSkill(effect)?.img || 'channeling'}.png`);
+ let description = getBuffSkill(effect)?.description;
+ img.setAttribute('onmouseover', `battle.set_infopane_effect('${effect}', ${description}, ${Math.floor(turns)})`);
+ img.setAttribute('onmouseout', 'battle.clear_infopane()');
+
+ monster_btm6.appendChild(img);
+ }
+ }
+ if (!isNewTurn) return;
+ battle.turnLog = turnLog;
+ setValue('battle', battle);
+ }
+
+ async function loadUnsafeWindowBattle() { try {
+ unsafeWindow.battle = await until(() => new unsafeWindow.Battle(), 300);
+ unsafeWindow.battle.clear_infopane();
+ } catch (err) { console.error(err); }}
+
+ function newRound(isNew) { // New Round
+ $debug.log('______________newRound', isNew);
+ const option = g().option??{};
+ const token = document.documentElement.outerHTML.match(/var battle_token = "(.*)";/)[1];
+ let battle = getValue('battle', true);
+ const isSameBattle = battle?.token === token;
+ const prof = getValue('proficiency', true);
+ if (isNew) {
+ battle = { proficiency: isSameBattle ? battle?.proficiency ?? prof : prof };
+ }
+ if (!battle) {
+ battle = JSON.parse(JSON.stringify(g().battle ?? {}));
+ battle.monsterStatus?.sort(objArrSort('order'));
+ };
+ battle.token = token;
+ battle.proficiency = isSameBattle ? battle?.proficiency ?? prof : prof;
+ setValue('battle', battle);
+ if (window.location.hash !== '') {
+ goto();
+ }
+ g('monsterAll', gE(monsterStateKeys.obj, 'all').length);
+ const monsterDead = gE('img[src*="nbardead"]', 'all').length;
+ 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': {
+ ar: {
reg: /^Initializing arena challenge/,
extra: (i) => i <= 35,
},
- 'rb': {
+ 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;
- }
- },
+ iw: { reg: /^Initializing Item World/ },
+ gr: { reg: /^Initializing Grindfest/ },
+ tw: { reg: /^Initializing The Tower/ },
+ ba: { reg: /^Initializing random encounter/ },
}
- battle.tower = (temp.match(/\(Floor (\d+)\)/) ?? [null])[1] * 1;
- const id = (temp.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;
+ const battleLog = gE('#textlog>tbody>tr>td', 'all');
+ if (!battle.roundType) {
+ const temp = battleLog[battleLog.length - 1].textContent;
+ battle.tower = (temp.match(/\(Floor (\d+)\)/) ?? [null])[1] * 1;
+ const id = (temp.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;
+ }
+ battle.roundType = name;
+ break;
}
- battle.roundType = name;
- break;
}
- }
- 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);
}
}
- }
-
- const roundPrev = battle.roundNow;
- 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 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;
- 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] = {}; // 重置该名称的数据
+ const roundPrev = battle.roundNow;
+
+ if (battleLog[battleLog.length - 1].textContent.match('Initializing')) {
+ const monsterStatus = [];
+ let order = 0;
+ 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) ?? {};
+ 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;
+ 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 (monsterMID[mid]) {
- monsterDB[name] = JSON.parse(JSON.stringify(monsterMID[mid])); // 将之前备份的mid的数据进行恢复
- delete monsterMID[mid];
+ 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;
}
- monsterDB[name].mid = mid;
- monsterDB[name][lv] = hp;
+ monsterStatus[order] = { order, hp };
+ order++;
}
- monsterStatus[order] = {
- order: order,
- hp,
- };
- order++;
- }
- if(g('option').cacheMonsterHP){
- if (oldDB !== JSON.stringify(monsterDB)) {
+ if (option.cacheMonsterHP) {
setValue('monsterDB', monsterDB);
- }
- if (oldMID !== JSON.stringify(monsterMID)) {
setValue('monsterMID', monsterMID);
}
- }
- battle.monsterStatus = monsterStatus;
-
- const round = battleLog[battleLog.length - 1].textContent.match(/\(Round (\d+) \/ (\d+)\)/);
- if (round && battle.roundType !== 'ba') {
- battle.roundNow = round[1] * 1;
- battle.roundAll = round[2] * 1;
- } else {
- battle.roundNow = 1;
- battle.roundAll = 1;
- }
- } else if (!battle.monsterStatus || battle.monsterStatus.length !== gE('div.btm2', 'all').length) {
- battle.roundNow = 1;
- battle.roundAll = 1;
- }
-
- if(roundPrev !== battle.roundNow) {
- battle.turn = 0;
- }
- 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++) {
- if (bugLog[i].textContent.match(isBug)) {
- bugLog[i].className = 'tlbWARN';
- setTimeout(() => { // 间隔时间以避免持续刷新
- window.location.href = window.location;// 刷新移除问题元素
- }, 700);
- } else {
- bugLog[i].className = 'tlbQRA';
- }
- }
- }
-
- function countMonsterHP() { // 统计敌人血量
- let i, j;
- const monsterHp = gE('div.btm4>div.btm5:nth-child(1)', 'all');
- let battle = getValue('battle', true);
- const monsterStatus = battle.monsterStatus;
- const hpArray = [];
- for (i = 0; i < monsterHp.length; 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);
- 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 hpMin = Math.min.apply(null, hpArray);
- const yggdrasilExtraWeight = g('option').YggdrasilExtraWeight;
- const unreachableWeight = g('option').unreachableWeight;
- const baseHpRatio = g('option').baseHpRatio ?? 1;
- // 权重越小,优先级越高
- for (i = 0; i < monsterStatus.length; i++) { // 死亡的排在最后(优先级最低)
- if (monsterStatus[i].isDead) {
- monsterStatus[i].finWeight = unreachableWeight;
- 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"
- weight += yggdrasilExtraWeight; // yggdrasilExtraWeight.defalut -1000
- }
- for (j in skillLib) {
- if (gE(`img[src*="${skillLib[j].img}"]`, monsterBuff[i])) {
- weight += g('option').weight[j];
- }
- }
- monsterStatus[i].finWeight = weight;
- }
- monsterStatus.sort(objArrSort('finWeight'));
- battle.monsterStatus = monsterStatus;
- g('battle', battle);
- }
+ battle.monsterStatus = monsterStatus;
- function autoRecover() { // 自动回血回魔
- if (!g('option').item) {
- return false;
- }
- if (!g('option').itemOrderValue) {
- 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();
- return true;
+ const round = battleLog[battleLog.length - 1].textContent.match(/\(Round (\d+) \/ (\d+)\)/);
+ if (round && battle.roundType !== 'ba') {
+ battle.roundNow = round[1] * 1;
+ battle.roundAll = round[2] * 1;
+ } else {
+ battle.roundNow = 1;
+ battle.roundAll = 1;
+ }
+ } else if (!battle.monsterStatus || battle.monsterStatus.length !== gE(monsterStateKeys.lv, 'all').length) {
+ battle.roundNow = 1;
+ battle.roundAll = 1;
}
- }
- return false;
- }
- function useScroll() { // 自动使用卷轴
- if (!g('option').scrollSwitch) {
- return false;
- }
- if (!g('option').scroll) {
- return false;
- }
- if (!checkCondition(g('option').scrollCondition)) {
- return false;
- }
- if (!g('option').scrollRoundType) {
- return false;
+ if (roundPrev !== battle.roundNow) {
+ battle.turn = 0;
+ setValue('skillOTOS', {});
+ }
+ battle.roundLeft = battle.roundAll - battle.roundNow;
+ setValue('battle', battle);
}
- if (!g('option').scrollRoundType[g('battle').roundType]) {
- return false;
+
+ 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++) {
+ if (bugLog[i].textContent.match(isBug)) {
+ bugLog[i].className = 'tlbWARN';
+ setTimeout(() => { // 刷新移除问题元素,间隔时间以避免持续刷新
+ window.location.href = window.location.search ? window.location.pathname + window.location.search : window.location.href;
+ }, 700);
+ } else {
+ bugLog[i].className = 'tlbQRA';
+ }
+ }
}
- const scrollLib = {
- Go: {
- name: 'Scroll of the Gods',
- id: 13299,
- mult: '3',
- img1: 'absorb',
- img2: 'shadowveil',
- img3: 'sparklife',
- },
- Av: {
- name: 'Scroll of the Avatar',
- id: 13199,
- mult: '2',
- img1: 'haste',
- img2: 'protection',
- },
- Pr: {
- name: 'Scroll of Protection',
- id: 13111,
- mult: '1',
- img1: 'protection',
- },
- Sw: {
- name: 'Scroll of Swiftness',
- id: 13101,
- mult: '1',
- img1: 'haste',
- },
- Li: {
- name: 'Scroll of Life',
- id: 13221,
- mult: '1',
- img1: 'sparklife',
- },
- Sh: {
- name: 'Scroll of Shadows',
- id: 13211,
- mult: '1',
- img1: 'shadowveil',
- },
- Ab: {
- name: 'Scroll of Absorption',
- 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;
+
+ function countMonsterHP() { // 统计敌人血量
+ let i, j;
+ 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++) {
+ 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: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 monsterBuff = gE(monsterStateKeys.buffs, 'all');
+ const hpMin = Math.min.apply(null, hpArray);
+ const option = g().option??{};
+ const yggdrasilExtraWeight = option.YggdrasilExtraWeight;
+ const baseHpRatio = option.baseHpRatio;
+ // 权重越小,优先级越高
+ for (i = 0; i < monsterStatus.length; i++) { // 死亡的排在最后(优先级最低)
+ const target = monsterStatus[i];
+ if (target.isDead) {
+ target.finWeight = resolveRPNFormula(option.unreachableWeight, target);
+ continue;
+ }
+ let weight = baseHpRatio * Math.log10(target.hpNow / hpMin); // > 0 生命越低权重越低优先级越高
+ const name = gE(`${monsterStateKeys.name}>div>div`, monsterBuff[i].parentNode).innerText;
+ if (yggdrasilExtraWeight && ('Yggdrasil' === name || '世界树 Yggdrasil' === name)) { // 默认设置下,任何情况都优先击杀群体大量回血的boss"Yggdrasil"
+ weight += yggdrasilExtraWeight; // yggdrasilExtraWeight.defalut -1000
+ }
+ 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;
}
- isUsed = false;
+ weight += option.weight[j] ?? 0;
}
- if (!isUsed) {
- gE(`.bti3>div[onmouseover*="${scrollLib[i].id}"]`).click();
- return true;
+
+ 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;
}
- }
- return false;
- }
- function useChannelSkill() { // 自动施法Channel技能
- if (!g('option').channelSkillSwitch) {
- return false;
- }
- if (!g('option').channelSkill) {
- return false;
+ // 先存一次,用于下面的额外权重公式
+ monsterStatus.sort(objArrSort('finWeight'));
+ battle.monsterStatus = monsterStatus;
+ g('battle', battle);
+
+ // 额外权重公式
+ for (let i = 0; i < battle.monsterStatus.length; i++) {
+ const target = battle.monsterStatus[i];
+ target.finWeight += resolveRPNFormula(option.extraWeightFormula, target);
+ }
+ monsterStatus.sort(objArrSort('finWeight'));
+ battle.monsterStatus = monsterStatus;
+ g('battle', battle);
}
- if (!gE('#pane_effects>img[src*="channeling"]')) {
+
+ function autoRecover(isCureOnly) { // 自动回血回魔
+ const option = g().option??{};
+ if (!option.item) {
+ return false;
+ }
+ const name = splitOrders(option.itemOrderName, ['FC', 'HE', 'LE', 'HG', 'HP', 'Cure', 'MG', 'MP', 'ME', 'SG', 'SP', 'SE', 'Mystic', 'CC', 'ED']);
+ const order = splitOrders(option.itemOrderValue, [313, 11199, 11501, 10005, 11195, 311, 10006, 11295, 11299, 10007, 11395, 11399, 10008, 11402, 11401]);
+ const cures = [313, 11199, 11501, 10005, 11195, 311];
+ for (let i = 0; i < name.length; i++) {
+ let id = order[i];
+ if (isCureOnly && !cures.includes(id)) {
+ continue;
+ }
+ 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;
+ }
+ }
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();
+
+ function useScroll() { // 自动使用卷轴
+ const option = g().option??{};
+ if (!option.scrollSwitch) {
+ return false;
+ }
+ if (!option.scroll) {
+ return false;
+ }
+ if (!option.scrollRoundType) {
+ return false;
+ }
+ if (!option.scrollRoundType[g().battle.roundType]) {
+ return false;
+ }
+ if (!checkCondition(option.scrollCondition)) {
+ return false;
+ }
+ const scrollLib = useScroll.prototype.scrollLib ??= {
+ Go: {
+ name: 'Scroll of the Gods',
+ id: 13299,
+ mult: '3',
+ img1: 'absorb',
+ img2: 'shadowveil',
+ img3: 'sparklife',
+ },
+ Av: {
+ name: 'Scroll of the Avatar',
+ id: 13199,
+ mult: '2',
+ img1: 'haste',
+ img2: 'protection',
+ },
+ Pr: {
+ name: 'Scroll of Protection',
+ id: 13111,
+ mult: '1',
+ img1: 'protection',
+ },
+ Sw: {
+ name: 'Scroll of Swiftness',
+ id: 13101,
+ mult: '1',
+ img1: 'haste',
+ },
+ Li: {
+ name: 'Scroll of Life',
+ id: 13221,
+ mult: '1',
+ img1: 'sparklife',
+ },
+ Sh: {
+ name: 'Scroll of Shadows',
+ id: 13211,
+ mult: '1',
+ img1: 'shadowveil',
+ },
+ Ab: {
+ name: 'Scroll of Absorption',
+ id: 13201,
+ mult: '1',
+ img1: 'absorb',
+ },
+ };
+ 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;
+ }
+ if (!checkCondition(option[`scroll${i}Condition`])) {
+ continue;
+ }
+ for (let j = 1; j <= scrollLib[i].mult; j++) {
+ if (getBuff(scrollLib[i][`img${j}`] + scrollFirst)) {
+ continue;
+ }
+ updateSkillOTOS(id);
+ gE(`.bti3>div[onmouseover*="(${id})"]`).click();
return true;
}
}
+ return false;
}
- 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();
+
+ function useChannelSkill() { // 自动施法Channel技能
+ const option = g().option??{};
+ if (!option.channelSkillSwitch) {
+ return false;
+ }
+ if (!getBuff('channeling')) {
+ return false;
+ }
+
+ playerBuffSkillLib.CF.id = getBuff('sparklife') ? undefined : 422;
+ if (option.channelSkill) {
+ const skillPack = splitOrders(option.buffSkillOrderValue, ['SS', 'SL', 'Pr', 'Ab', 'SV', 'Re', 'Ha', 'He', 'AF']);
+ for (const buff of skillPack) {
+ const buffObj = getBuff(playerBuffSkillLib[buff].img);
+ const current = getBuffTurnFromImg(buffObj);
+ const threshold = option.channelThreshold ? option.channelThreshold[buff] : 0;
+ if (threshold > 0 && current >= threshold) continue;
+ if (!option.channelSkill[buff] || buffObj) continue;
+ const id = playerBuffSkillLib[buff].id;
+ if (!isOn(id)) continue;
+ updateSkillOTOS(id);
+ gE(id).click();
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.channelSkill2) {
+ const order = splitOrders(option.channelSkill2OrderValue);
+ for (const id of order) {
+ const buffs = Object.keys(playerBuffSkillLib).filter(s => playerBuffSkillLib[s].id * 1 === 1 * id);
+ for (const buff of buffs) {
+ const current = getBuffTurnFromImg(getBuff(playerBuffSkillLib[buff].img));
+ const threshold = option.channelThreshold ? option.channelThreshold[buff] : 0;
+ if (threshold > 0 && current > threshold) continue;
}
+ if (!isOn(id)) continue;
+ updateSkillOTOS(id);
+ gE(id).click();
+ return true;
}
}
- }
- return false;
- }
+ if (option.channelRebuff) {
+ let minBuff, minTime;
+ for (const buff in playerBuffSkillLib) {
+ const buffObj = getBuff(playerBuffSkillLib[buff].img);
+ let current = getBuffTurnFromImg(buffObj);
+ const threshold = option.channelThreshold ? option.channelThreshold[buff] : 0;
+ if (threshold > 0 && current > threshold) continue;
- 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) {
- return false;
- }
- if (!g('option').buffSkill) {
- return false;
- }
- if (!checkCondition(g('option').buffSkillCondition)) {
+ current = isNaN(current) ? 0 : current;
+ if (buffObj?.src.match(/_scroll.png$/) || (minTime && current >= minTime)) {
+ continue;
+ }
+ if (!current && (!option.buffSkillSwitch || !option.buffSkill[buff])) continue;
+
+ const id = playerBuffSkillLib[buff].id;
+ if (!isOn(id)) continue;
+ minBuff = id;
+ minTime = current;
+ }
+ if (minBuff && gE(minBuff)) {
+ updateSkillOTOS(minBuff);
+ gE(minBuff).click();
+ return true;
+ }
+ }
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();
- return true;
+
+ function useBuffSkill() { // 自动施法BUFF技能
+ const option = g().option??{};
+ if (!option.buffSkillSwitch) {
+ return false;
}
- }
- const draughtPack = {
- HD: {
- id: 11191,
- img: 'healthpot',
- },
- MD: {
- id: 11291,
- img: 'manapot',
- },
- SD: {
- id: 11391,
- img: 'spiritpot',
- },
- FV: {
- id: 19111,
- img: 'flowers',
- },
- BG: {
- id: 19131,
- 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();
+ if (!option.buffSkill) {
+ return false;
+ }
+ if (!checkCondition(option.buffSkillCondition)) {
+ return false;
+ }
+ let i;
+ const skillPack = splitOrders(option.buffSkillOrderValue, ['SS', 'SL', 'Pr', 'Ab', 'SV', 'Re', 'Ha', 'He', 'AF']);
+ for (i = 0; i < skillPack.length; i++) {
+ let buff = skillPack[i];
+ if (!option.buffSkill[buff]) continue;
+ const id = playerBuffSkillLib[buff].id;
+ if (!isOn(id)) continue;
+ if (!checkCondition(option[`buffSkill${buff}Condition`])) continue;
+ const current = getBuffTurnFromImg(getBuff(playerBuffSkillLib[buff].img));
+ const threshold = option.buffSkillThreshold ? option.buffSkillThreshold[buff] : 0;
+ if (threshold >= 0 && current > threshold) continue;
+ updateSkillOTOS(id);
+ gE(id).click();
return true;
}
- }
- return false;
- }
- function useInfusions() { // 自动使用魔药
- if (g('attackStatus') === 0) {
- return false;
- }
- if (!g('option').infusionSwitch) {
- return false;
- }
- if (!checkCondition(g('option').infusionCondition)) {
+ const draughtPack = useBuffSkill.prototype.draughtPack ??= {
+ HD: {
+ id: 11191,
+ img: 'healthpot',
+ },
+ MD: {
+ id: 11291,
+ img: 'manapot',
+ },
+ SD: {
+ id: 11391,
+ img: 'spiritpot',
+ },
+ FV: {
+ id: 19111,
+ img: 'flowers',
+ },
+ BG: {
+ id: 19131,
+ img: 'gum',
+ },
+ };
+ for (i in draughtPack) {
+ const id = draughtPack[i].id;
+ 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;
+ }
+ }
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();
- return true;
- }
- return false;
- }
-
- function autoFocus() {
- if (g('option').focus && checkCondition(g('option').focusCondition)) {
- 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"]'))) {
- gE('#ckey_spirit').click();
- return true;
- }
- return false;
- }
-
- /**
- * INNAT / WEAPON SKILLS
- *
- * 优先释放先天和武器技能
- */
- function autoSkill() {
- if (!g('option').skillSwitch) {
- return false;
- }
- if (!gE('#ckey_spirit[src*="spirit_a"]')) {
- return false;
- }
+ function useInfusions() { // 自动使用魔药
+ const option = g().option??{};
+ if (!option.infusionSwitch) return false;
+ if (!checkCondition(option.infusionCondition)) {
+ return false;
+ }
- const skillOrder = (g('option').skillOrderValue || 'OFC,FRD,T3,T2,T1').split(',');
- 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,
- },
- };
- const rangeSkills = {
- 2101: 2,
- 2403: 2,
- 1111: 4,
- }
- 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 (g('oc') < skillLib[skill].oc) {
- continue;
- }
- if (g('option').skillOTOS && g('option').skillOTOS[skill] && g('skillOTOS')[skill] >= 1) {
- 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') {
- 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();
+ 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, ['Divinity', 'Darkness', 'Flames', 'Frost', 'Lightning', 'Storms']);
+ for (const name of order) {
+ const condition = option[`infusion${name}Condition`];
+ if (!checkCondition(condition)) continue;
+ if (onUse(infusionLib.findIndex(i => i?.name === name))) {
return true;
}
}
+ return false;
}
- gE(`#mkey_${getRangeCenterID(monsterStatus[0])}`).click();
- return true;
- }
- function useDeSkill() { // 自动施法DEBUFF技能
- if (!g('option').debuffSkillSwitch) { // 总开关是否开启
+ function autoFocus() {
+ const option = g().option??{};
+ if (option.focus && checkCondition(option.focusCondition)) {
+ updateSkillOTOS('focus');
+ gE('#ckey_focus').click();
+ return true;
+ }
return false;
}
- // 先处理特殊的 “先给全体上buff”
- let skillPack = ['We', 'Im'];
- 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;
+
+ function autoSS(isDisableOnly) {
+ const textSP = gE('#vrs') ?? gE('#dvrs');
+ const spValue = textSP.childNodes[0].textContent * 1;
+ if (spValue <= 1) {
+ return false;
+ }
+ const option = g().option??{};
+ const enabled = gE('#ckey_spirit[src*="spirit_a"]');
+ if (
+ (!isDisableOnly && option.turnOnSS && checkCondition(option.turnOnSSCondition) && !enabled)
+ || (option.turnOffSS && checkCondition(option.turnOffSSCondition) && enabled)
+ ) {
+ updateSkillOTOS(enabled ? 'spiritoff' : 'spiriton');
+ gE('#ckey_spirit').click();
+ return true;
}
- 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(','));
+ return false;
}
- for (let i in skillPack) {
- let buff = skillPack[i];
- if (i >= toAllCount && !skillPack[i]) { // 检查buff是否启用
- continue;
- }
- if (!checkCondition(g('option')[`debuffSkill${buff}Condition`])) { // 检查条件
- continue;
- }
- let succeed = useDebuffSkill(skillPack[i], i < toAllCount);
- // 前 toAllCount 个都是先给全体上的
- if (succeed) {
+
+ async function clickMonster(id) {
+ if (!unsafeWindow.battle) {
+ console.log('loadUnsafeWindowBattle before click monster');
+ await loadUnsafeWindowBattle();
+ }
+ getMonster(id).click();
+ }
+
+ /**
+ * INNAT / WEAPON SKILLS
+ *
+ * 优先释放先天和武器技能
+ */
+ function autoSkill() {
+ const option = g().option??{};
+ if (!option.skillSwitch) return false;
+ if (!option.skill) return false;
+ if (option.skillSSOnly && !gE('#ckey_spirit[src*="spirit_a"]')) {
+ return false;
+ }
+ const skillOrder = splitOrders(option.skillOrderValue, ['OFC', 'FRD', 'T3', 'T2', 'T1']);
+ const fightStyle = g().fightingStyle; // 1二天 2单手 3双手 4双持 5法杖
+ const skillLib = {
+ 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 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];
+ if (!skill || !option.skill[skill]) {
+ return;
+ }
+ let id = skillLib[skill];
+ if (!isOn(id)) {
+ continue;
+ }
+ if (g().oc < (skillInfos[id]?.oc ?? 2)) {
+ continue;
+ }
+ const skillOTOS = getValue('skillOTOS', true) ?? {};
+ skillOTOS[skill] ??= 0;
+ if (option.skillOTOS && option.skillOTOS[skill] && skillOTOS[skill] >= 1) {
+ continue;
+ }
+ let target = checkCondition(option[`skill${skill}Condition`], monsterStatus);
+ if (!target) {
+ continue;
+ }
+ updateSkillOTOS(skill, skillOTOS);
+ gE(id).click();
+ clickMonster(getRangeCenter(target, skillInfos[id]?.range ?? 1).id);
return true;
}
+ return false;
}
- return false;
- }
- 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',
- },
- };
+ function useDeSkill() { // 自动施法DEBUFF技能
+ const option = g().option??{};
+ const monsterStatus = g().battle.monsterStatus;
+ if (!option.debuffSkillSwitch || !checkCondition(option.debuffSkillCondition, monsterStatus)) { // 总开关是否开启
+ return false;
+ }
- if (!isOn(skillLib[buff].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
- continue;
- }
- primaryTarget = target;
- break;
- }
- if (primaryTarget === undefined) {
+ // 先处理特殊的 “先给全体上buff”
+ let skillPack = splitOrders(option.debuffSkillOrderAllValue, ['Sle', 'Bl', 'We', 'Si', 'Slo', 'Dr', 'Im', 'MN', 'Co']);
+ for (let i = 0; i < skillPack.length; i++) {
+ if (option[`debuffSkill${skillPack[i]}All`]) { // 是否启用
+ if (checkCondition(option[`debuffSkill${skillPack[i]}AllCondition`], monsterStatus)) { // 检查条件
+ continue;
+ }
+ }
+ skillPack.splice(i, 1);
+ i--;
+ }
+ const toAllCount = skillPack.length;
+
+ if (option.debuffSkill) { // 是否有启用的buff(不算两个特殊的)
+ skillPack = skillPack.concat(splitOrders(option.debuffSkillOrderValue, ['Sle', 'Bl', 'We', 'Si', 'Slo', 'Dr', 'Im', 'MN', 'Co']));
+ }
+ for (let i in skillPack) {
+ let buff = skillPack[i];
+ const isToAll = i < toAllCount;
+ if (!isToAll) { // 非先全体
+ if (!buff || !option.debuffSkill[buff] || !checkCondition(option[`debuffSkill${buff}Condition`], monsterStatus)) { // 检查条件
+ continue;
+ }
+ }
+ let succeed = useDebuffSkill(buff, isToAll);
+ // 前 toAllCount 个都是先给全体上的
+ if (succeed) {
+ return true;
+ }
+ }
return false;
}
- let range = 0;
- let ab;
- for (ab in skillLib[buff].range) {
- const ranges = skillLib[buff].range[ab][skillLib[buff].skill * 1];
- if (!ranges) {
- continue;
+ function useDebuffSkill(buff, isAll = false) {
+ const skill = monsterBuffSkillLib[buff];
+ if (!isOn(skill.id)) { // 技能不可用
+ return false;
}
- range = ranges[getValue('ability', true)[ab].level];
- break;
- }
- let id = getRangeCenterID(primaryTarget, range, isDebuffed);
- const imgs = gE('img', 'all', gE(`#mkey_${id}>.btm6`));
- // 已有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();
+ // 获取范围
+ let range = 1;
+ let ab;
+ const ability = getValue('ability', true);
+ for (ab in skill.range) {
+ const ranges = skill.range[ab];
+ if (!ranges) {
+ continue;
+ }
+ range = ranges[ability ? ability[ab] ?? 0 : 0];
+ break;
+ }
+ // 获取目标
+ const option = g().option??{};
+ 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 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;
+ }
+ 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));
+ monsterStatus.sort(objArrSort('order'));
+ }
+ let max = isAll ? monsterStatus.length : 1;
+ let id;
+ let minWeight = Number.MAX_SAFE_INTEGER;
+ const condition = option[`debuffSkill${buff}${isAll ? 'All' : ''}Condition`];
+ const excludeCondition = target => checkCondition(condition, [target]) ? isDebuffed(target) : excludedWeight(target);
+ for (let i = 0; i < max; i++) {
+ 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, range, 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) {
+ switch (option.debuffSkillTurnAlert * 1) {
+ case 1:
+ if ((option.debuffSkillTurn && (getBuffTurnFromImg(buffs[skill.img]) ?? 0) >= option.debuffSkillTurn[buff])) {
+ return false;
+ }
+ _alert(0, '无法正常施放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;
+ }
+ }
+ updateSkillOTOS(skill.id);
+ gE(skill.id).click();
+ clickMonster(id);
return true;
}
- _alert(0, '无法正常施放DEBUFF技能,请尝试手动打怪', '無法正常施放DEBUFF技能,請嘗試手動打怪', 'Can not cast de-skills normally, continue the script?\nPlease try attack manually.');
- pauseChange();
- return true;
- }
-
- function attack() { // 自动打怪
- // 如果
- // 1. 开启了自动以太水龙头
- // 2. 目标怪在魔力合流状态中
- // 3. 未获得以太水龙头*2 或 *1
- // 4. 满足条件
- // 使用物理普通攻击,跳过Offensive Magic
- // 否则按照属性攻击模式释放Spell > Offensive Magic
-
- const 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]
- },
- 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]
- },
- 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]
- },
- 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]
- },
- //暗
- 4401: { 161: [0, 1, 2] },
- 4402: { 162: [0, 2, 3] },
- 4403: { 163: [0, 3, 4, 4] },
- //圣
- 4501: { 151: [0, 1, 2] },
- 4502: { 152: [0, 2, 3] },
- 4503: { 153: [0, 3, 4, 4] },
+ function getCurrentAttackStatus() {
+ if (g().attackStatusCurrent === undefined) {
+ attack(true);
+ }
+ const current = g().attackStatusCurrent;
+ g('attackStatusCurrent', undefined);
+ return current;
}
- let range = 0;
- // Spell > Offensive Magic
- const attackStatus = g('attackStatus');
- const monsterStatus = g('battle').monsterStatus;
- 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;
+ function attack(selectStatusOnly=false) { // 自动打怪
+ let range = g().fightingStyle === '1' ? 3 : 1;
+ const option = g().option??{};
+ const monsters = g().battle.monsterStatus;
+ let attackStatusOrder = option.attackStatusOrderValue?.split(',').map(ord => ord*1) ?? [];
+ attackStatusOrder = attackStatusOrder.concat([0,6,5,1,2,4,3].filter(ord => !(attackStatusOrder.includes(ord))));
+ if (option.attackStatusSwitch) {
+ for (const status of attackStatusOrder) {
+ if (!option.attackStatusSwitch[status]) continue;
+ const current = g().attackStatusCurrent;
+ g('attackStatusCurrent', status);
+ if (checkCondition(option[`attackStatusSwitchCondition${status}`], monsters) && onAttack(range, status, selectStatusOnly)) {
+ return true;
}
- })();
- gE(skill)?.click();
- for (let ab in updateAbility) {
- const ranges = updateAbility[ab][skill];
- if (!ranges) {
- continue;
+ g('attackStatusCurrent', current);
+ }
+ }
+ g('attackStatusCurrent', 0);
+ return onAttack(range, g().attackStatus, selectStatusOnly);
+ }
+
+ function onAttack(range, attackStatus, selectStatusOnly=false) {
+ const updateAbility = onAttack.prototype.updateAbility ??= {
+ 4301: { //火
+ 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: [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: [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: [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: [3, 4, 5] },
+ 4402: { 162: [5, 6, 7] },
+ 4403: { 163: [7, 8, 9, 10] },
+ //圣
+ 4501: { 151: [3, 4, 5] },
+ 4502: { 152: [5, 6, 7] },
+ 4503: { 153: [7, 8, 9, 10] },
+ }
+
+ // 如果
+ // 1. 开启了自动以太水龙头
+ // 2. 目标怪在魔力合流状态中
+ // 3. 未获得以太水龙头*2 或 *1
+ // 4. 满足条件
+ // 使用物理普通攻击,跳过Offensive Magic
+ // 否则按照属性攻击模式释放Spell > Offensive Magic
+
+ const option = g().option??{};
+ const monsters = g().battle.monsterStatus;
+ let target = monsters[0];
+ const tryAttack = (skill) => {
+ if (!target || target.isDead) {
+ return false;
+ }
+ if (selectStatusOnly) {
+ return true;
+ }
+ if (skill && gE(skill)) {
+ updateSkillOTOS(skill);
+ gE(skill).click();
+ }
+ clickMonster(getRangeCenter(target, range, !attackStatus).id);
+ return true;
+ };
+ // 1. physical
+ if (attackStatus === 0) {
+ return tryAttack();
+ }
+
+ // 2. etherTap
+ if (option.etherTap && getBuff('coalescemana', getMonsterID(target))
+ && (!gE('#pane_effects>img[onmouseover*="Ether Tap (x2)"]') || getBuff(`wpn_et"][id*="effect_expire`))
+ && checkCondition(option.etherTapCondition)) {
+ return tryAttack();
+ }
+ // 2.5 try check skill condition
+ const skill = 1 * (() => {
+ let lv = 3;
+ for (let condition of [option.highSkillCondition, option.middleSkillCondition, option.lowSkillCondition]) {
+ let id = `1${attackStatus}${lv--}`;
+ target = checkCondition(condition, monsters);
+ if (target && isOn(id)) {
+ return id;
}
- range = ranges[getValue('ability', true)[ab]?.level ?? 0];
- break;
}
+ })();
+ // 3. no skill available
+ if (!skill) {
+ return tryAttack();
+ }
+ // 4. cast skill
+ for (let ab in updateAbility) {
+ const ranges = updateAbility[ab][skill];
+ if (!ranges) {
+ continue;
+ }
+ const ability = getValue('ability', true);
+ range = ranges[ability ? ability[ab] ?? 0 : 0];
+ break;
}
+ if (!tryAttack(skill)) {
+ return false;
+ }
+ return true;
}
- gE(`#mkey_${getRangeCenterID(monsterStatus[0], range, !attackStatus)}`).click();
- return true;
- }
- function getHPFromMonsterDB(mdb, name, lv) {
- let hp = (mdb && mdb[name]) ? mdb[name][lv] : undefined;
- // TODO: 根据lv模糊推测
- return hp;
- }
+ function updateSkillOTOS(id, skillOTOS) {
+ skillOTOS ??= getValue('skillOTOS', true) ?? {};
+ skillOTOS[id] ??= 0;
+ skillOTOS[id]++;
+ return setValue('skillOTOS', skillOTOS);
+ }
+
+ function getHPFromMonsterDB(mdb, name, lv) {
+ let hp = (mdb && mdb[name]) ? mdb[name][lv] : undefined;
+ // TODO TBD 根据lv模糊推测(一般数据都是等级逐渐提升的,可能可以直接用缓存而不需要推测,异世界新赛季时可以自动刷新缓存?)
+ return hp;
+ }
- function fixMonsterStatus() { // 修复monsterStatus
- // document.title = _alert(-1, '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 monsterDB = getValue('monsterDB', true);
- gE('div.btm2', 'all').forEach((monster, order) => {
- monsterStatus.push({
- order: order,
- hp: getHPFromMonsterDB(monsterDB, monsterNames[order], monsterLvs[order]) ?? ((monster.style.background === '') ? 1000 : 100000),
+ function fixMonsterStatus() { // 修复monsterStatus
+ // document.title = _alert(-1, 'monsterStatus错误,正在尝试修复', 'monsterStatus錯誤,正在嘗試修復', 'monsterStatus Error, trying to fix');
+ const monsterStatus = [];
+ 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(monsterStateKeys.lv, 'all').forEach((monster, order) => {
+ monsterStatus.push({
+ order: order,
+ hp: getHPFromMonsterDB(monsterDB, monsterNames[order], monsterLvs[order]) ?? ((monster.style.background === '') ? 1000 : 100000),
+ });
});
- });
- const battle = getValue('battle', true);
- battle.monsterStatus = monsterStatus;
- setValue('battle', battle);
- }
+ const battle = getValue('battle', true);
+ battle.monsterStatus = monsterStatus;
+ setValue('battle', battle);
+ }
- function displayMonsterWeight() {
+ function displayMonsterWeight() {
- const status = g('battle').monsterStatus.filter(m => !m.isDead);
- let rank = 0;
+ const status = g().battle.monsterStatus.filter(m => !m.isDead);
+ let rank = 0;
- const weights = [];
- status.forEach(s => {
- if (weights.indexOf(s.finWeight) !== -1) {
- return;
- }
- weights.push(s.finWeight);
- })
- const sec = Math.max(1, weights.length - 1);
- const max = 360 * 2 / 3;
- const colorTextList = [];
- if (g('option').weightBackground) {
+ const weights = [];
status.forEach(s => {
- const rank = weights.indexOf(s.finWeight);
- let colorText = (g('option').weightBackground[rank + 1] ?? [])[0];
- colorTextList[rank] = colorText;
+ if (weights.indexOf(s.finWeight) !== -1) {
+ return;
+ }
+ weights.push(s.finWeight);
});
- }
- status.forEach(s => {
- const rank = weights.indexOf(s.finWeight);
- const id = getMonsterID(s);
- if (!gE(`#mkey_${id}`) || !gE(`#mkey_${id}>.btm3`)) {
- return;
+ const sec = Math.max(1, weights.length - 1);
+ const max = 360 * 2 / 3;
+ const colorTextList = [];
+ const option = g().option??{};
+ const weightBG = option.weightBackground;
+ if (weightBG) {
+ for (let i = 0; i < weights.length; i++) {
+ colorTextList[i] = weightBG[i];
+ }
}
- if (g('option').displayWeightBackground) {
- if (g('option').weightBackground) {
+ status.forEach(s => {
+ const rank = weights.indexOf(s.finWeight);
+ const id = getMonsterID(s);
+ if (!getMonster(id) || !gE(monsterStateKeys.name, getMonster(id))) {
+ return;
+ }
+ if (option.displayWeightBackground && weightBG) {
let colorText = colorTextList[rank];
let remainAttemp = 10; // 避免无穷递归
- while(remainAttemp > 0 && colorText && colorText.indexOf(` 0 && colorText && colorText.indexOf(``, colorTextList[i]);
}
remainAttemp--;
@@ -4094,260 +6684,345 @@ try {
try {
colorText = eval(colorText.replace('', rank).replace('', weights.length));
}
- catch {
- }
- gE(`#mkey_${id}`).style.cssText += `background: ${colorText};`;
+ 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)}]
`;
+ }
+ });
+ }
- function displayPlayStatePercentage() {
- const barHP = gE('#vbh') ?? gE('#dvbh');
- const barMP = gE('#vbm') ?? gE('#dvbm');
- const barSP = gE('#vbs') ?? gE('#dvbs');
- const barOC = gE('#dvbc');
- const textHP = gE('#vrhd') ?? gE('#dvrhd');
- 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));
- [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()}%]`;
- const percentageDiv = gE('div', text);
- if (percentageDiv) {
- percentageDiv.innerHTML = inner;
- return;
- }
- text.innerHTML += `${inner}
`
- });
- }
+ `
+ const inner = `[${percentages[i].toString()}%]`;
+ if (percentageDiv) {
+ percentageDiv.innerHTML = inner;
+ percentageDiv.style.cssText = style;
+ return;
+ }
+ text.innerHTML += `${inner}
`
+ });
+ }
- function dropMonitor(battleLog) { // 掉落监测
- 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++) {
- if (/^You gain \d+ (EXP|Credit)/.test(battleLog[i].textContent)) {
- regexp = battleLog[i].textContent.match(/^You gain (\d+) (EXP|Credit)/);
- if (regexp) {
- drop[`#${regexp[2]}`] += regexp[1] * 1;
- }
- } else if (gE('span', battleLog[i])) {
- item = gE('span', battleLog[i]);
- 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++) {
- if (name.match(quality[j])) {
- name = `Equipment of ${name.match(/^\w+/)[0]}`;
- drop[name] = (name in drop) ? drop[name] + 1 : 1;
- break;
- }
- }
- } else if (item.style.color === 'rgb(186, 5, 180)') {
- regexp = name.match(/^(\d+)x (Crystal of \w+)$/);
+ function dropMonitor(battleLog) { // 掉落监测
+ const drop = getValue('drop', true) || {
+ '#startTime': time(3),
+ '#EXP': 0,
+ '#Credit': 0,
+ };
+ const option = g().option??{};
+ let item, name, amount, regexp;
+ for (let i = 0; i < battleLog.length; i++) {
+ if (/^You gain \d+ (EXP|Credit)/.test(battleLog[i].textContent)) {
+ regexp = battleLog[i].textContent.match(/^You gain (\d+) (EXP|Credit)/);
if (regexp) {
- name = regexp[2];
- amount = regexp[1] * 1;
+ drop[`#${regexp[2]}`] += regexp[1] * 1;
+ }
+ } else if (gE('span', battleLog[i])) {
+ item = gE('span', battleLog[i]);
+ 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 = option.dropQuality; j < quality.length; j++) {
+ if (name.match(quality[j])) {
+ name = `Equipment of ${name.match(/^\w+/)[0]}`;
+ drop[name] = (name in drop) ? drop[name] + 1 : 1;
+ break;
+ }
+ }
+ } else if (item.style.color === 'rgb(186, 5, 180)') {
+ regexp = name.match(/^(\d+)x (Crystal of \w+)$/);
+ if (regexp) {
+ name = regexp[2];
+ amount = regexp[1] * 1;
+ } else {
+ name = name.match(/^(Crystal of \w+)$/)[1];
+ amount = 1;
+ }
+ drop[name] = (name in drop) ? drop[name] + amount : amount;
+ } else if (item.style.color === 'rgb(168, 144, 0)') {
+ drop['#Credit'] = drop['#Credit'] + name.match(/\d+/)[0] * 1;
} else {
- name = name.match(/^(Crystal of \w+)$/)[1];
- amount = 1;
+ drop[name] = (name in drop) ? drop[name] + 1 : 1;
}
- drop[name] = (name in drop) ? drop[name] + amount : amount;
- } else if (item.style.color === 'rgb(168, 144, 0)') {
- drop['#Credit'] = drop['#Credit'] + name.match(/\d+/)[0] * 1;
- } else {
- drop[name] = (name in drop) ? drop[name] + 1 : 1;
+ } else if (battleLog[i].textContent === 'You are Victorious!') {
+ break;
}
- } else if (battleLog[i].textContent === 'You are Victorious!') {
- break;
+ }
+ const battle = g().battle;
+ if (option.recordEach && battle.roundNow === battle.roundAll) {
+ const old = getValue('dropOld', true) || [];
+ drop.__name = getValue('battleCode', true).name;
+ drop['#endTime'] = time(3);
+ old.push(drop);
+ setValue('dropOld', old);
+ delValue('drop');
+ } else {
+ setValue('drop', drop);
}
}
- const battle = g('battle');
- if (g('option').recordEach && battle.roundNow === battle.roundAll) {
- const old = getValue('dropOld', true) || [];
- drop.__name = getValue('battleCode');
- drop['#endTime'] = time(3);
- old.push(drop);
- setValue('dropOld', old);
- delValue('drop');
- } else {
- setValue('drop', drop);
- }
- }
- 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;
+ 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);
}
}
- 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;
- } else {
- stats.self[parm.mode] = (parm.mode in stats.self) ? stats.self[parm.mode] + 1 : 1;
- }
- const debug = false;
- let log = false;
- for (let i = 0; i < parm.log.length; i++) {
- if (parm.log[i].className === 'tls') {
- break;
+
+ function recordUsage(parm) {
+ const filter = g().option?.record;
+ if (!filter) {
+ return;
}
- text = parm.log[i].textContent;
- if (debug) {
- console.log(text);
- }
- if (text.match(/you for \d+ \w+ damage/)) {
- reg = text.match(/you for (\d+) (\w+) damage/);
- 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);
- }
- } 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;
- } 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;
- } else if (text.match(/You (evade|parry|block) the attack|misses the attack against you|(casts|uses) .* misses the attack/)) {
- 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++;
- } else if (text.match(/You gain the effect Focusing/)) {
- 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;
- } 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;
- } 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;
- } 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;
- } 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) {
- log = true;
- setAudioAlarm('Error');
- console.log(text);
+ const stats = getValue('stats', true) || {};
+ stats.self ??= { _startTime: time(3) };
+ 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, point, reg;
+ const battle = g().battle;
+ 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 (parm.mode === 'magic') {
+ magic = parm.magic;
+ if (filter.magic) {
+ stats.magic[magic] = (magic in stats.magic) ? stats.magic[magic] + 1 : 1;
+ }
+ if (filter.mp) {
+ stats.self.mp += parm.mp;
+ }
+ if (filter.oc) {
+ stats.self.oc += parm.oc;
+ }
+ } else if (parm.mode === 'items') {
+ if (filter.items) {
+ stats.items[parm.item] = (parm.item in stats.items) ? stats.items[parm.item] + 1 : 1;
+ }
+ } else {
+ if (filter[parm.mode]) {
+ stats.self[parm.mode] = (parm.mode in stats.self) ? stats.self[parm.mode] + 1 : 1;
+ }
+ }
+
+ const debug = false;
+ let log = false;
+ for (let i = 0; i < parm.log.length; i++) {
+ if (parm.log[i].className === 'tls') {
+ break;
+ }
+ text = parm.log[i].textContent;
+ if (debug) {
+ console.log(text);
+ }
+ if (reg = matchDamageInfoFromLogText(text)) {
+ magic = reg[2].replace('ing', '');
+ point = reg[1] * 1;
+ 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/)) {
+ 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/)) {
+ 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/)) {
+ 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/)) {
+ if (filter.miss) {
+ stats.self.miss++;
+ }
+ } else if (text.match(/You gain the effect Focusing/)) {
+ 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/)) {
+ if (filter.restore) {
+ 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;
+ }
+ } else if (text.match(/(restores|drain) \d+ points of/)) {
+ 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/)) {
+ 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(parm.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/)) {
+ 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) {
+ log = true;
+ setAudioAlarm('Error');
+ console.log(text);
+ }
+ }
+ if (debug && log) {
+ console.table(stats);
+ pauseChange();
+ }
+ setValue('stats', stats);
}
- if (debug && log) {
- console.table(stats);
- pauseChange();
- }
- setValue('stats', stats);
- }
- function recordUsage2() {
- 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) {
- const old = getValue('statsOld', true) || [];
- stats.__name = getValue('battleCode');
- stats.self._endTime = time(3);
- old.push(stats);
- setValue('statsOld', old);
- delValue('stats');
- } else {
+ function recordUsage2() {
+ const option = g().option??{};
+ const filter = option.record;
+ if (!filter) {
+ return;
+ }
+ const stats = getValue('stats', true);
+ if (filter.monster) {
+ stats.self._monster += g().monsterAll;
+ }
+ if (filter.boss) {
+ stats.self._boss += g().bossAll;
+ }
+ const battle = g().battle;
+ if (option.recordEach && battle.roundNow === battle.roundAll) {
+ const old = getValue('statsOld', true) || [];
+ stats.__name = getValue('battleCode', true).name;
+ stats.self._endTime = time(3);
+ old.push(stats);
+ setValue('statsOld', old);
+ delValue('stats');
+ return;
+ }
setValue('stats', stats);
}
+ } catch (err) {
+ console.error(err);
+ document.title = err;
}
-} catch (e) {
- console.log(e);
- document.title = e;
-}
+})();