diff --git a/HentaiVerse/hvAutoAttack/README.md b/HentaiVerse/hvAutoAttack/README.md
index df72b5e5..5c376cb7 100644
--- a/HentaiVerse/hvAutoAttack/README.md
+++ b/HentaiVerse/hvAutoAttack/README.md
@@ -50,9 +50,10 @@
***
### 自定义判断条件
-
每一个拥有红色虚线边框的区域,都可以设置自定义判断条件。
+现已支持自定义的条件公式,例如`hp > mp` 或 `2 * ( hp + mp ) > sp`,支持运算符: `+` `-` `*` `/` `%` `**`(幂) `^`(异或) `~`(Log10) `&&` `||` `!` `>` `<` `>=`(`≥`) `<=`(`≤`) `==`(`=`,`===`) `!=`(`≠`,`~=`,`<>`),逻辑运算符返回`0`或`1` (表示false或true)
+
* 注意:如果这些区域留空(一个条件也没设置),那么就相当于真。
当鼠标在这些区域内移动时,右上角会显示一个盒子(当鼠标不在这些区域内,盒子消失)
@@ -63,32 +64,77 @@
* 下拉列表2/4: 比较值A/比较值B
-* 下拉列表3: 只支持比较运算符(`1`:大于, `2`:小于, `3`: 大于等于, `4`: 小于等于, `5`:等于, `6`:不等于)
+* 下拉列表3: 运算符
-* ADD按钮: 生成一个值为`比较值A,比较值,比较值B`的输入框
+* ADD按钮: 生成一个值为 `比较值A 运算符 比较值B` 的输入框
+
+* 仍兼容旧版本 `比较值A,比较运算符,比较值B` 的条件,其中比较运算符:(`1`:大于, `2`:小于, `3`: 大于等于, `4`: 小于等于, `5`:等于, `6`:不等于)
#### 比较值
-1. `hp`/`mp`/`sp`: hp/mp/sp的*百分比 (percent)*
-2. `oc`: Overcharge, 250==>250%
-3. `monsterAll`/`monsterAlive`/`bossAll`/`bossAlive`: 怪兽/Boss的总数目/存活数目
+1. `hp`/`mp`/`sp`: hp/mp/sp的*百分比的整数形式 (percent)*;`_hpDecimal`/`_mpDecimal`/`_spDecimal`: hp/mp/sp的*百分比的小数形式 (percent decimal)*
+2. `oc`: Overcharge, 250==>250% *百分比的整数形式 (percent)*;`_ocDecimal`: oc的*百分比的小数形式 (percent decimal)*
+3. `monsterAll`/`monsterAlive`/`bossAll`/`bossAlive`: 怪兽/Boss的总数目/存活数目,boss只按照有序号背景色的。包含`龙、大树、额外游戏内容、小马`的存活boss数可改为使用`_targetBossType_count`(见后11.3和12.)
4. `roundNow`/`roundAll`/`roundLeft`: 当前回合数/总回合数/剩余回合数
-5. `roundType`: 战役模式 (`ar`: The Arena, `rb`: Ring of Blood, `gr`: GrindFest, `iw`: Item World, `ba`: Random Encounter)
-
- **注意**: 由于是字符串之间的比较,所以请加上引号,如"ar"/'ar'
-
-6. `attackStatus`: 攻击模式 (`0`: Physical, `1`: Fire, `2`: Cold, `3`: Elec, `4`: Wind, `5`: Divine, `6`: Forbidden)
-7. `isCd`: 技能/物品是否cd,格式`_isCd_id`
-
- **示例1**: Protection的id为411,则`_isCd_411,5,0`表示不可施放,`_isCd_411,5,1`表示可以施放
-
- **示例2**: ManaElixir的id为11295,则`_isCd_11295,5,0`表示不可使用,`_isCd_11295,5,1`表示可以使用
-
-8. `buffTurn`: 人物Buff剩余时间,格式`_buffTurn_img`
-
- **示例**: Protection的img为protection,则`_buffTurn_protection,5,0`表示不存在Protection的buff,`_buffTurn_protection,3,10`表示Protection的buff至少剩余10回合
-
-9. 空白(blank): 自己输入 (the value you want to put in)
+5. `isRoundType`、`ar`、`ba`、`iw`、`tw`、`gr`、`rb`: 当前是否是某战役模式,例如`_isRoundType_ar`或`_ar`均返回 `当前是否是The Arena`
+6. `roundType`: 战役模式 (`ar`: The Arena, `rb`: Ring of Blood, `gr`: GrindFest, `iw`: Item World, `ba`: Random Encounter, `tw`: The Tower)
+
+ **注意**: 字符串之间的比较会自动去除最外层的引号,如`"ar"`/`'ar'`均和`ar`一致
+
+7. `attackStatus`: 攻击模式 (`0`: Physical, `1`: Fire, `2`: Cold, `3`: Elec, `4`: Wind, `5`: Divine, `6`: Forbidden)。 或使用 `_phys`, `_fire`, `_cold`, `_elec`, `_wind`, `_divi`, `_forb` 表示 `当前 attack mode 是否为 ...`,例如 `_phys` 等价于 `attackStatus == 0`。
+ - 获取到的是默认攻击模式,可加后缀`Cur`来表示判断次要模式后的攻击模式(直接获取当前`attackStatus`需要加`_`前缀),即:`_attackStatusCur`,`_physCur`, `_fireCur`, `_coldCur`, `_elecCur`, `_windCur`, `_diviCur`, `_forbCur`
+ - 具体流程:
+ - 尝试次要模式攻击的流程中,会直接用尝试中的模式
+ - 如果在`次要模式`的条件中配置了这些条件,都会按照`条件所在模式`来获取值,例如:切换火属性的条件中配置了`_fireCur`,会直接判定为`true`,配置了`_windCur`则是会判定为`false`
+ - 其他地方则会模拟一遍攻击,然后判断能放出来的时候 给`临时变量`赋值`模拟尝试成功的模式`,然后返回临时变量
+8. `fightingStyle`: 战斗风格 (`1`: 二天, `2`: 单手, `3`: 双手, `4`: 双持, `5`: 法杖)。 或使用 `_nt`, `_1h`, `_2h`, `_dw`, `_staff` 表示 `当前 fighting style 是否为 ...`,例如 `_nt` 等价于 `fightStyle == 1`
+9. `isCd`: 技能/物品是否cd,格式`_isCd_id`
+
+ **示例1**: Protection的id为411,则`!_isCd_411`表示不可施放,`_isCd_411`表示可以施放
+
+ **示例2**: ManaElixir的id为11295,则`!_isCd_11295`表示不可使用,`_isCd_11295`表示可以使用
+
+10. `buffStack`、`buffTurn`: 人物Buff层数/剩余时间,格式`_buffTurn_img`。可使用`_scroll`限定为卷轴buff`_buffTurn_sparklife_scroll`、使用`_png`限定为非卷轴buff`_buffTurn_sparklife_png`. 可用 `{buffA,buffB,...}` 表示获取 buffA **或** buffB (**或** 括号 `{ }` 中的任意其他buff,将返回匹配成功的第一个buff),注意内部不要包含空格,例如`_buffTurn_{stun,sleep}`.
+
+ **示例**: Protection的img为protection,则`_buffTurn_protection == 0`表示不存在Protection的buff,`_buffTurn_protection >= 10`表示Protection的buff至少剩余10回合
+
+11. `_targetHp`、`_targetMp`、`_targetSp`、`_targetHpDecimal`、`_targetMpDecimal`、`_targetSpDecimal`、`_targetBuffStack`、`_targetBuffTurn`、`_targetRank`、`_targetOrder`、`_targetWeight`、`_targetIsAlive`: 目标怪物的HP%、SP%、MP%、HP%(小数形式)、SP%(小数形式)、MP%(小数形式)、buff剩余时间、优先级、位置、当前权重、是否存活
+ 1. `_targetBuffStack`、`_targetBuffTurn_`后缀参照10.`buffStack`、`buffTurn`(如:`_targetBuffTurn_bleed != 0`表示目标bleed的buff剩余回合不等于0)。target的目标怪物遵循以下规则
+ 1. 默认情况的target均为权重优先级最高的目标
+ 2. 武器技能(马炮、T1~T3等)、法术技能(中阶、高阶):按照 逐条条件判断>按权重逐个目标>满足任意一条条件内的所有子条目,则对该目标释放。例如下图最后的慈悲的条件:仅释放hp小于25%、拥有流血buff
+ 
+
+ 2. `_targetRank`和攻击规则给出的顺序值相同(0~9,数字越小,优先级越高)
+ 3. `targetBossType(见12.)`和除了`targetName`的其他 目标参数可使用`max/min/sum/count`后缀来表示所有**存活**怪物中的最大/最小值/总和/sign和(`sum(sign(value))`),如:`_targetBuffTurn_max_bleed`.
+ 1. `max/min/sum/count`可加前缀`a`/`ag`or`ga`/`g`表示按照`包含死亡目标(仍是全体目标)`/`分组内统计且包含死亡目标`/`分组内统计(仍忽略死亡目标)`. 分组相关见(13. `targetGroup`)
+12. `targetName`、`targetBossType`: 目标怪物的名称、Boss类型。
+ 1. `_targetName`返回目标的名称字符串(**注意**: 字符串之间的比较会自动去除最外层的引号,且请使用下划线`_`代替空格` `,如`Yugi_Nagato`/`'Yugi_Nagato'`/`"Yugi_Nagato"`)
+ 2. 其中类Boss型`_targetBossType`根据 名称进行判断:
+ 1. `Manbearpig`、`White Bunneh`、`Mithra`、`Dalek`: 1 (BOSS)
+ 2. `Konata`、`Mikuru Asahina`、`Ryouko Asakura`、`Yuki Nagato`: 2 (Legendaries)
+ 3. `Real Life`、`Invisible Pink Unicorn`、`Flying Spaghetti Monster`: 3 (Gods)
+ 4. `Rhaegal`、`Viserion`、`Drogon`: 4 (A Dance with Dragons)
+ 5. `Skuld`、`Urd`、`Verdandi`、`Yggdrasil`: 5 (Trio and the Tree)
+ 6. `Recycled Boss Rush`、`Bottomless Dungeon`、`New Game +`、`Achievement Grind`、`Time Trial Mode`、`Hardcore Mode`:6 (Post Game Content)
+ 7. `Fluttershy`、`Gummy`、`Rainbow Dash`、`Twilight Sparkle`、`Rarity`、`Applejack`、`Pinkie Pie`、`Angel Bunny`、`Spike`:7 (Ponies)
+ 8. 其他: 0 (非boss)
+13. `targetGroup`: 获取分组内的目标个数(包含死亡),并更改当前公式内的`目标分组`(见11.3.1). 每次公式计算的初始目标分组为`null`的空分组. 可选模式如下:
+ 1. `a`: 选择全体. `targetGroup_a`
+ 2. `s`: 按照给定数量从上往下划分,然后选取包含当前目标的一组 `targetGroup_s_2` 相当于每2个1组
+ 3. `r`: 按照给定的距离,以当前目标为起点划分
+ 1. 只有一个参数时,为对称范围. `targetGroup_r_1` 相当于`当前目标 ± 1`(共3格)
+ 2. 两个参数时,分别为 `targetGroup_r_向上_向下`. `targetGroup_r_1_2` 相当于当前 \[`目标 - 1`, `目标 + 2`\](共4格)
+ 3. `距离 >= 10`时,溢出为 `9 - 距离`,例如:`targetGroup_r_4_10` 相当于 \[`目标 - 4`, `目标 - 1`\] (共4格)
+ 4. 参数缺省=`-1`/`10`: `targetGroup_r_` 相当于 `targetGroup_r_10`;`targetGroup_r_1_` 相当于 `targetGroup_r_1_10`; `targetGroup_r__1` 相当于 `targetGroup_r_10_1`. 仅在有2个`_`划分参数时会补充第2个参数
+ 5. `o`/`oa`: 按照给定的order(0~9)划定区域. `o`需要当前目标在区域内,否则返回0和`null`的空分组;`oa`不需要当前目标在区域内. `targetGroup_o_1_3` 相当于 `order=[1,2,3]` 即第2,3,4格
+ 1. 两个参数分别为上下界,缺省分别为`-1`/`10`
+14. `skillOTOS`: 获取对应技能/法术/道具/防御/集中/灵动架势开/关 的**自动使用次数**(不包含手动释放的次数), `skillOTOS_[类型]` 如 `skillOTOS_FRD`(马炮) /`skillOTOS_T1`(武器T1技能) /`skillOTOS_defend`(防御)
+ - 技能: `OFC`/`FRD`/`T3`/`T2`/`T1`
+ - 法术/道具: 按 `id`
+ - 防御/集中/SS开关: `defend`/`focus`/`spiriton`/`spiritoff`
+15. 空白(blank): 自己输入 (the value you want to put in)
+
+PS: 对于需要带下划线`_`开头的func式变量,可以省略`_`开头(省略时,获取值会先尝试按照dict式获取,失败时再按照func式获取)
#### 示例
@@ -153,7 +199,7 @@
| 1 | 2 | 3 |
| - | - | - |
-| - | Regen / regen | - |
+| Channeling / channeling | Regen / regen | Focus / focus |
| Protection / protection | Haste / haste | Shadow Veil / shadowveil |
| Absorb / absorb | Spark of Life / sparklife | Spirit Shield / spiritshield |
| Arcane Focus / arcanemeditation | Heartseeker / heartseeker | Cloak of the Fallen / fallenshield |
@@ -161,7 +207,17 @@
| Infusion of Flames / fireinfusion | Infusion of Frost / coldinfusion | Infusion of Lightning / elecinfusion |
| Infusion of Storms / windinfusion | Infusion of Divinity / holyinfusion | Infusion of Darkness / darkinfusion |
| Scroll of Swiftness / haste_scroll | - | - |
-| Flower Vase / flowers | Bubble-Gum / gum | - |
+| Flower Vase / flowers | Bubble-Gum / gum | Blessing of the RiddleMaster / riddlemaster |
+| Sleep / sleep | Blind / blind | Slow / slow |
+| Imperil / imperil | MagNet / magnet | Silence / silence |
+| Drain / drainhp | Weaken / weaken | Confuse / confuse |
+| Coalesced Mana / coalescemana | Stunned / stun / wpn_stun | Penetrated Armor / ap / wpn_ap |
+| Bleeding Wound / bleed / wpn_bleed | Absorbing Ward / absorb | Fury of the Sisters / trio_furyofthesisters |
+| Lamentations of the Future / trio_skuld | Screams of the Past / trio_urd | Wailings of the Present / trio_verdandi | Searing Skin / firedot | Freezing Limbs / coldslow |
+| Freezing Limbs / coldslow | Turbulent Air / windmiss | Deep Burns / elecweak |
+| Searing Skin / firedot | - | Breached Defense / holybreach |
+Blunted Attack / darknerf | Burning Soul / soulfire | Ripened Soul / ripesoul |
+| Ether Tap / wpn_et | - | - |
***
@@ -382,3 +438,20 @@
灵感来自hoverplay,刚开始接触js,初步完成代码
功能有:答题警报、其他警报、快捷键、自动前进、自动使用宝石、自动回复、自动使用增益技能、自动打怪
很可惜,玩游戏不走心,一直搞不懂HVSTAT是怎么知道每个怪的血量的,直到[版本2.0](#20)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HentaiVerse/hvAutoAttack/README_en.md b/HentaiVerse/hvAutoAttack/README_en.md
index c44fef8f..958539c7 100644
--- a/HentaiVerse/hvAutoAttack/README_en.md
+++ b/HentaiVerse/hvAutoAttack/README_en.md
@@ -49,6 +49,8 @@ Scripts get information through text, and if you have not yet modified the font,
Each area with a red dotted border can be set to a customize condition.
+Customizable Formula is support now, such as `hp > mp` or `2 * ( hp + mp ) > sp`, supported operators: `+` `-` `*` `/` `%` `**`(pow) `^`(xor) `~`(Log10) `&&` `||` `!` `>` `<` `>=`(`≥`) `<=`(`≤`) `==`(`=`,`===`) `!=`(`≠`,`~=`,`<>`), logical operators returns 0 or 1 (as false or true)
+
* If these areas are left blank (a condition is not set), then it's equivalent to true.
When the mouse moves in these areas, a box is displayed in the upper right corner. (When the mouse out, the box disappears)
@@ -59,32 +61,78 @@ Four drop down lists and one button are visible in the box
* Drop-down List 2/4: comparison value A / comparison value B
-* Drop-down List 3: only support comparison operator (`1`: >, `2`: <, `3`: ≥, `4`: ≤, `5`: =, `6`: ≠)
+* Drop-down List 3: operator
+
+* Button ADD: Generates an input box with a value of `A Operator B`
-* Button ADD: Generates an input box with a value of `A,Comparison-Operator,B`
+* Legacy version condition such as `A,Comparison-Operator,B` is still supported, Comparison-Operator: (`1`: >, `2`: <, `3`: ≥, `4`: ≤, `5`: =, `6`: ≠)
#### Comparison Value
-1. `hp`/`mp`/`sp`: **percent** of hp/mp/sp, 0-100
-2. `oc`: Overcharge, 0-250
-3. `monsterAll`/`monsterAlive`/`bossAll`/`bossAlive`: amount of all monster/boss (alive)
+1. `hp`/`mp`/`sp`: **percent integer** of hp/mp/sp, 0-100; `_hpDecimal`/`_mpDecimal`/`_spDecimal`: **percent decimal** of hp/mp/sp, 0-1
+2. `oc`: Overcharge, 0-250 (integer); `_ocDecimal`: Overcharge in decimal 0-2.5
+3. `monsterAll`/`monsterAlive`/`bossAll`/`bossAlive`: amount of all monster/boss (alive), only monster with serial letter background is considered as boss. Use `_targetBossType_count` (see 11.3 and 12. below) as the Alive boss count including `dragons, trio and the tree, Post Game Content, Ponies`
4. `roundNow`/`roundAll`/`roundLeft`
-5. `roundType`: Battle Type (`ar`: The Arena, `rb`: Ring of Blood, `gr`: GrindFest, `iw`: Item World, `ba`: Random Encounter)
-
- (**Note**: Because comparison between strings, please add quotation, such as `"ar"`/`'ar'`)
-
-6. `attackStatus`: Attack Mode (`0`: Physical, `1`: Fire, `2`: Cold, `3`: Elec, `4`: Wind, `5`: Divine, `6`: Forbidden)
-7. `isCd`: whether the skill/item is cooldowning, format: `_isCd_id`
-
- **example 1**: the id of Protection is 411 , `_isCd_411,5,0` means Protection can't be casted or `_isCd_411,5,1` means Protection can be casted
-
- **example 2**: the id of ManaElixir is 11295, `_isCd_11295,5,0` means ManaElixir can't be used or `_isCd_11295,5,1` means ManaElixir can be used
-
-8. `buffTurn`: time the buff last in person, format`_buffTurn_img`
-
- **example**: the image of Protection is protection, `_buffTurn_protection,5,0` means you don't have the buff of Protection or `_buffTurn_protection,3,10` means the the buff of Protection on you last at least 10 turns
-
-9. blank: the value you want to put in
+5. `isRoundType`、`ar`、`ba`、`iw`、`tw`、`gr`、`rb`: is current round type as the target type, such as: both `_isRoundType_ar` and `_ar` returns `is currently in The Arena`
+6. `roundType`: Battle Type (`ar`: The Arena, `rb`: Ring of Blood, `gr`: GrindFest, `iw`: Item World, `ba`: Random Encounter, `tw`: The Tower)
+
+ (**Note**: Comparison between strings will automatically remove the outermost quotation marks, such as `"ar"`/`'ar'` is as same as `ar`
+
+7. `attackStatus`: Attack Mode (`0`: Physical, `1`: Fire, `2`: Cold, `3`: Elec, `4`: Wind, `5`: Divine, `6`: Forbidden). Or use `_phys`, `_fire`, `_cold`, `_elec`, `_wind`, `_divi`, `_forb` as `if current attack mode is ...`, such as `_phys` equals `attackStatus == 0`。
+ - Value acquired above is the default Attack Mode, to get current Attack Mode after calculating Secondary Attack Mode, use suffix `Cur` ( and prefix `_` for `attackStatus`), such as:`_attackStatusCur`,`_physCur`, `_fireCur`, `_coldCur`, `_elecCur`, `_windCur`, `_diviCur`, `_forbCur`
+ - current value getting by:
+ - During `Secondary Attack`, return `current attempting mode`
+ - If configed any current condition in `Secondary Attack Mode`, returns `mode that the condition is in`. Such as: In conditions of `Attack mode Fire`, `_fireCur` is as `true`,`_windCur` is as `false`
+ - In other options, attack will be simulated until the spell is callable ( cache the successfully simulated mode into a temp variable and returns it)
+8. `fightingStyle`: Fighting Style (`1`: Niten, `2`: 1H, `3`: 2H, `4`: DW, `5`: Staff). Or use `_nt`, `_1h`, `_2h`, `_dw`, `_staff` as `if current fighting style is ...`, such as `_nt` equals `fightStyle == 1`
+9. `isCd`: whether the skill/item is cooldowning, format: `_isCd_id`
+
+ **example 1**: the id of Protection is 411 , `!_isCd_411` means Protection can't be casted or `_isCd_411` means Protection can be casted
+
+ **example 2**: the id of ManaElixir is 11295, `!_isCd_11295` means ManaElixir can't be used or `_isCd_11295` means ManaElixir can be used
+
+10. `buffStack`/`buffTurn`: buff stack / time the buff last in person, format`_buffTurn_img`. Use suffix `_scroll` for scroll buff only `_buffTurn_sparklife_scroll`, `_png` for not scroll buff only `_buffTurn_sparklife_png`. `{buffA,buffB,...}` means to get buffA **or** buffB (**or** any else in `{ }`, matching the first available one), be aware that no space ` ` should be included in between, such as `_buffTurn_{stun,sleep}`.
+
+ **example 1**: the image of Protection is protection, `_buffTurn_protection == 0` means you don't have the buff of Protection or `_buffTurn_protection >= 10` means the buff of Protection on you last at least 10 turns
+
+11. `_targetHp`/`_targetMp`/`_targetSp`/`_targetHpDecimal`/`_targetMpDecimal`/`_targetSpDecimal`/`_targetBuffStack`/`_targetBuffTurn`/`_targetRank`: `HP%`/`SP%`/`MP%`/`HP% in decimal`/`SP% in decimal`/`MP% in decimal`/`buffRemainTime`/`attackRank`/`_targetRank`/`_targetOrder`/`_targetWeight`/`_targetIsAlive` of target monster
+ 1. , suffix of `_targetBuffStack`/`_targetBuffTurn_` is same as 10.`buffStack`/`buffTurn`(such as:`_targetBuffTurn_bleed != 0` means remain turns of bleed buff on target monster is not equal to 0. Target that is calculating is chosen by following rules:
+ 1. The highest priority monster by rank in default situations.
+ 2. Weapon skills (OFC, T1~T3, etc.), Offensive Spell skills (Tire2, Tire3): by each condition > for each ranked target > find the target fit all sub-condition in the condition and cast to it. Such as the pic below: condition for Merciful Blow: only cast to targets which with hp below 25% and a bleed buff.
+
+ 
+
+ 2. `_targetBuffTurn` returns the value as same as the ranked order given by Attack Rule (0~9, smaller number as higher priority)
+ 3. `targetBossType`(see 12.) & other target params (except `targetName`) can get maxim/minimum/sum/'sign_sum(`sum(sign(value))`)' value from all alive monsters by using suffix `max/min/sum/count` such as: `_targetBuffTurn_max_bleed`.
+ 1. For `max/min/sum/count` , prefixs `a`/`ag`or`ga`/`g` are available and represent `a: including dead targets (still from all targets)`/`ag/ga: only from grouped and including dead targets`/`g: only from grouped (still ingore dead targets)`. About grouping, see 13. `targetGroup`.
+12. `targetName`/`targetBossType`: name and boss type for target monster
+ 1. `_targetName` returns a string of the target name (**Note**: Comparison between strings will automatically remove the outermost quotation marks, meanwhile **please replace space` ` with underline`_`**, such as `Yugi_Nagato`/`'Yugi_Nagato'`/`"Yugi_Nagato"`)
+ 2. `_targetBossType` is determined by name:
+ 1. `Manbearpig`、`White Bunneh`、`Mithra`、`Dalek`: 1 (BOSS)
+ 2. `Konata`、`Mikuru Asahina`、`Ryouko Asakura`、`Yuki Nagato`: 2 (Legendaries)
+ 3. `Real Life`、`Invisible Pink Unicorn`、`Flying Spaghetti Monster`: 3 (Gods)
+ 4. `Rhaegal`、`Viserion`、`Drogon`: 4 (A Dance with Dragons)
+ 5. `Skuld`、`Urd`、`Verdandi`、`Yggdrasil`: 5 (Trio and the Tree)
+ 6. `Recycled Boss Rush`、`Bottomless Dungeon`、`New Game +`、`Achievement Grind`、`Time Trial Mode`、`Hardcore Mode`:6 (Post Game Content)
+ 7. `Fluttershy`、`Gummy`、`Rainbow Dash`、`Twilight Sparkle`、`Rarity`、`Applejack`、`Pinkie Pie`、`Angel Bunny`、`Spike`:7 (Ponies)
+ 8. others: 0 (not boss)
+13. `targetGroup`: amount of target in group by certain mode (including dead targets), and change the `grouped targets` of current formula (see 11.3.1).1). For each calculation of each formula, `grouped targets` is dafault as a empty `null` group. Available group modes:
+ 1. `a`: Select all targets. `targetGroup_a`
+ 2. `s`: Split by given amount from the top down, and select the group contains current target. `targetGroup_s_2` means 2 targets as a group.
+ 3. `r`: A ranged group using the current target as a central starting point
+ 1. For 1 param only, using symmetry range. `targetGroup_r_1` means `current_target ± 1` (3 targets)
+ 2. For 2 param, params as `targetGroup_r_[up]_[down]`. `targetGroup_r_1_2` means \[`current_target - 1`, `current_target + 2`\] (4 targets)
+ 3. While `param(distance) >= 10`, overflows as `9 - param`, such as: `targetGroup_r_4_10` means \[`current_target - 4`, `current_target - 1`\] (4 targets)
+ 4. Params default as `-1`/`10`: `targetGroup_r_` means `targetGroup_r_10`; `targetGroup_r_1_` means `targetGroup_r_1_10`; `targetGroup_r__1` means `targetGroup_r_10_1`. The second param will be completed by default only if 2 `_` are spliting the expression.
+ 5. `o`/`oa`: Select the range by given order(0~9). `o` requires the current target is in the range, or returns 0 and selecting a empty `null` group; `oa` do NOT requires the current target is in the range. `targetGroup_o_1_3` means `order=[1,2,3]` (the 2nd, 3rd, 4th target)
+ 1. Params as `targetGroup_[o/oa]_[top]_[bottom]` and default as `-1`/`10`
+14. `skillOTOS`: Amount of the **auto cast times** of Skill / Spell / Item / Defend / Focus / Spirit On / Spirit Off (not including the manually ones), `skillOTOS_[type]` such as `skillOTOS_FRD` (FUS RO DAH) /`skillOTOS_T1`(Tire 1 weapon skill) /`skillOTOS_defend`(Defend)
+ - Skills: `OFC`/`FRD`/`T3`/`T2`/`T1`
+ - Spells/Items: Spell or item `id`
+ - Defend/Focus/Spirit On/Spirit Off: `defend`/`focus`/`spiriton`/`spiritoff`
+15. blank: the value you want to put in
+
+PS: For params get from func (starts with `_`), `_` from start can be omited (while omited, params will try get values as dict than as func if failed)
#### Example
@@ -147,7 +195,7 @@ The following is a schematic diagram of the circuit diagram
| 1 | 2 | 3 |
| - | - | - |
-| - | Regen / regen | - |
+| Channeling / channeling | Regen / regen | Focus / focus |
| Protection / protection | Haste / haste | Shadow Veil / shadowveil |
| Absorb / absorb | Spark of Life / sparklife | Spirit Shield / spiritshield |
| Arcane Focus / arcanemeditation | Heartseeker / heartseeker | Cloak of the Fallen / fallenshield |
@@ -155,7 +203,17 @@ The following is a schematic diagram of the circuit diagram
| Infusion of Flames / fireinfusion | Infusion of Frost / coldinfusion | Infusion of Lightning / elecinfusion |
| Infusion of Storms / windinfusion | Infusion of Divinity / holyinfusion | Infusion of Darkness / darkinfusion |
| Scroll of Swiftness / haste_scroll | - | - |
-| Flower Vase / flowers | Bubble-Gum / gum | - |
+| Flower Vase / flowers | Bubble-Gum / gum | Blessing of the RiddleMaster / riddlemaster |
+| Sleep / sleep | Blind / blind | Slow / slow |
+| Imperil / imperil | MagNet / magnet | Silence / silence |
+| Drain / drainhp | Weaken / weaken | Confuse / confuse |
+| Coalesced Mana / coalescemana | Stunned / stun / wpn_stun | Penetrated Armor / ap / wpn_ap |
+| Bleeding Wound / bleed / wpn_bleed | Absorbing Ward / absorb | Fury of the Sisters / trio_furyofthesisters |
+| Lamentations of the Future / trio_skuld | Screams of the Past / trio_urd | Wailings of the Present / trio_verdandi | Searing Skin / firedot | Freezing Limbs / coldslow |
+| Freezing Limbs / coldslow | Turbulent Air / windmiss | Deep Burns / elecweak |
+| Searing Skin / firedot | - | Breached Defense / holybreach |
+Blunted Attack / darknerf | Burning Soul / soulfire | Ripened Soul / ripesoul |
+| Ether Tap / wpn_et | - | - |
***
@@ -188,3 +246,19 @@ In this example, the script will attack enemy 1 next.
* Old
1. see [README_Chinese#更新历史](https://github.com/dodying/UserJs/blob/master/HentaiVerse/hvAutoAttack/README.md#更新历史)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HentaiVerse/hvAutoAttack/hvAutoAttack.user.js b/HentaiVerse/hvAutoAttack/hvAutoAttack.user.js
index 869e6349..41443268 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.119
// @author dodying
// @namespace https://github.com/dodying/
// @supportURL https://github.com/dodying/UserJs/issues
@@ -14,6 +14,8 @@
// @include http*://hentaiverse.org/*
// @include http*://alt.hentaiverse.org/*
// @include http*://e-hentai.org/*
+// @exclude http*://*hentaiverse.org/*/y/*
+// @exclude http*://*hentaiverse.org/*/z/*
// @connect hentaiverse.org
// @connect e-hentai.org
// @compatible Firefox + Greasemonkey
@@ -28,3058 +30,6779 @@
// @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;
+ 'use strict';
+ // constant
+ const dataFlags = { sharable: ['option'] };
+ dataFlags.portable = ['drop', 'stats', 'dropOld', 'statsOld', 'monsterDB', 'monsterMID'];
+ dataFlags.battleDatas = [...dataFlags.portable, 'battle', 'battleCode', 'disabled', 'stepIn', 'skillOTOS', 'onriddle', 'rec'];
+ dataFlags.local = [...dataFlags.battleDatas, 'stamina'];
+ dataFlags.standalone = [...dataFlags.sharable, ...dataFlags.local, 'arena', 'lastUrl', 'ability', 'proficiency', 'lastSwitch', 'itemWorldDatas', 'lastPersona', 'lastEquipSet'];
+ dataFlags.excludeStandalone = { 'option': ['optionStandalone', 'version', 'lang'] };
+
+ const _1s = 1000;
+ const _1m = 60 * _1s;
+ const _1h = 60 * _1m;
+ const _1d = 24 * _1h;
+
+ const monsterStateKeys = { obj: `div.btm1`, lv: `div.btm2`, name: `div.btm3`, bars: `div.btm4>div.btm5`, buffs: `div.btm6` };
+
+ const setMonsterBuffSkillLib = () => { return {
+ // debuff skill ------------
+ We: {
+ proficiency: ['Deprecating', 0, 345], // ???
+ buff: 'Weakened',
+ name: 'Weaken',
+ img: 'weaken',
+ id: '212',
+ range: { 4202: [1, 1, 2, 3] },
+ duration: { 4201: [10, 11, 12, 13, 14, 15] },
+ channeling: true,
+ description: "'The target has been weakened, making it deal less damage, and preventing it from scoring critical hits.'"
+ },
+ 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: 'Immobilize',
+ img: 'magnet',
+ id: '233',
+ range: { 4212: [1, 1, 1, 2, 2, 3] },
+ duration: { 4212: [10, 11, 12, 13, 14, 15] },
+ channeling: true,
+ description: "'The target has been immobilized, eliminating its chance to evade and reducing its magic resistance.'"
+ },
+ Slo: {
+ proficiency: ['Deprecating', 0, 300], // ???
+ buff: 'Slowed',
+ name: 'Slow',
+ img: 'slow',
+ id: '221',
+ range: { 4213: [1, 1, 2, 2, 2, 3] },
+ duration: { 4213: [10, 11, 12, 13, 14, 15] },
+ channeling: true,
+ description: `'The target has been slowed by ${[30, 40, 40, 45, 50, 50][ability[4213] ?? 0]}%, making it attack less frequently.'`
+ },
+ // debuff skills not checked ------------ ??
+ Dr: {
+ proficiency: ['Deprecating', 0, 300], // ???
+ buff: 'Vital Theft',
+ name: 'Drain',
+ img: 'drainhp',
+ id: '211',
+ duration: 10,
+ channeling: true,
+ description: "'Siphons off the target\\'s life essence over time, and gives it to the player.'"
+ },
+ ET: {
+ proficiency: ['Deprecating', 0, 300], // ???
+ name: 'Ether Theft',
+ img: 'drainmp',
+ duration: 10, // ??
+ description: "'Siphons off the target\\'s mana over time, and gives it to the player.'"
+ },
+ ST: {
+ proficiency: ['Deprecating', 0, 300], // ???
+ name: 'Spirit Theft',
+ img: 'drainsp',
+ duration: 10, // ??
+ description: "'Siphons off the target\\'s spirit over time, and gives it to the player.'"
+ },
+ // elem attack debuff ------------ ??
+ SS: {
+ proficiency: ['Elemental', 0, 800], // ???
+ name: 'Searing Skin',
+ img: 'firedot',
+ elem: 2,
+ duration: 3,
+ channeling: true,
+ description: "'The skin of the target has been scorched, inhibiting its attack damage. Cold resistance is lowered.'"
+ },
+ FL: {
+ proficiency: ['Elemental', 0, 800], // ???
+ name: 'Freezing Limbs',
+ img: 'coldslow',
+ elem: 1,
+ duration: 3,
+ channeling: true,
+ description: "'The limbs of the target have been frozen, causing slower movement. Wind resistance is lowered.'"
+ },
+ TA: {
+ proficiency: ['Elemental', 0, 800], // ???
+ name: 'Turbulent Air',
+ img: 'windmiss',
+ elem: 4,
+ duration: 3,
+ channeling: true,
+ description: "'The air around the target has been upset, blowing up dust and increasing its miss chance. Elec resistance is lowered.'"
+ },
+ DB: {
+ proficiency: ['Elemental', 0, 800], // ???
+ name: 'Deep Burns',
+ img: 'elecweak',
+ elem: 3,
+ duration: 3,
+ channeling: true,
+ description: "'Internal damage causes slower reactions and lowers evade and resist chance. Fire resistance is lowered.'"
+ },
+ BD: {
+ proficiency: ['Forbidden', 0, 800], // ???
+ name: 'Breached Defense',
+ img: 'holybreach',
+ elem: 6,
+ duration: 3,
+ channeling: true,
+ description: "'The holy attack has penetrated the target defenses, making it take more damage. Dark resistance is lowered.'"
+ },
+ BA: {
+ proficiency: ['Divine', 0, 800], // ???
+ name: 'Blunted Attack',
+ img: 'darknerf',
+ elem: 5,
+ duration: 3,
+ channeling: true,
+ description: "'The decaying effects of the spell has blunted the target offenses, making it deal less damage. Holy resistance is lowered.'"
+ },
+ BS: {
+ proficiency: ['Divine', 0, 800], // ???
+ name: 'Burning Soul',
+ img: 'soulfire',
+ duration: 7,
+ channeling: true,
+ description: "'The life essence of the target has been set ablaze, damaging its physical form over time.'"
+ },
+ RS: {
+ proficiency: ['Forbidden', 0, 800], // ???
+ name: 'Ripened Soul',
+ img: 'ripesoul',
+ duration: 7,
+ channeling: true,
+ description: "'The life essence of the target has been corrupted beyond repair, damaging its physical form over time.'"
+ },
+ // weapon debuff ------------ ??
+ PA: {
+ name: 'Penetrated Armor',
+ img: 'wpn_ap',
+ duration: 7,
+ description: "'The armor of this target has been breached, reducing its physical defenses.'"
+ },
+ BW: {
+ name: 'Bleeding Wound',
+ img: 'wpn_bleed',
+ stack: 80,
+ duration: 7,
+ description: "'Gashing wounds are making this target take damage over time.'"
+ },
+ Stun: {
+ name: 'Stunned',
+ img: 'wpn_stun',
+ duration: 4,
+ description: "'A powerful blow has temporarily stunned this target.'"
+ },
+ // else from player ------------ ??
+ Po: {
+ name: 'Spreading Poison',
+ img: 'poison',
+ duration: 15,
+ description: "'Poison courses through the target\'s veins. This causes a damage-over-time effect, and eliminates its evade chance.'"
+ },
+ CM: {
+ name: 'Coalesced Mana',
+ img: 'coalescemana',
+ duration: 5,
+ description: "'Mystical energies have converged on this target. Striking it with any magic spell will consume only half the normal mana.'"
+ },
+ AW: {
+ name: 'Absorbing Ward',
+ img: 'absorb',
+ duration: 'permanent',
+ description: "'The next magical attack against the target has a chance to be absorbed and partially converted to MP.'"
+ },
+ FoS: {
+ name: 'Fury of the Sisters',
+ img: 'trio_furyofthesisters',
+ duration: 'permanent',
+ description: "'The destruction of the world tree has infuriated its defenders, increasing their accuracy.'"
+ },
+ LoF: {
+ name: 'Lamentations of the Future',
+ img: 'trio_skuld',
+ duration: 'permanent',
+ description: "'The destruction of the future has increased the attack power of her allies.'"
+ },
+ SoP: {
+ name: 'Screams of the Past',
+ img: 'trio_urd',
+ duration: 'permanent',
+ description: "'The destruction of the past has increased the defensive power of her allies.'"
+ },
+ WoP: {
+ buff: 'Wails of the Present',
+ name: 'Wailings of the Present',
+ img: 'trio_verdandi',
+ duration: 'permanent',
+ description: "'The destruction the present has increased the attack speed of her allies.'"
+ },
+ } };
+ const playerBuffSkillLib = {
+ SS: {
+ name: 'Spirit Shield',
+ id: '423',
+ img: 'spiritshield',
+ },
+ SL: {
+ name: 'Spark of Life',
+ id: '422',
+ img: 'sparklife',
+ },
+ Pr: {
+ name: 'Protection',
+ id: '411',
+ img: 'protection',
+ },
+ Ab: {
+ name: 'Absorb',
+ id: '421',
+ img: 'absorb',
+ },
+ SV: {
+ name: 'Shadow Veil',
+ id: '413',
+ img: 'shadowveil',
+ },
+ Re: {
+ name: 'Regen',
+ id: '312',
+ img: 'regen',
+ },
+ Ha: {
+ name: 'Haste',
+ id: '412',
+ img: 'haste',
+ },
+ He: {
+ name: 'Heartseeker',
+ id: '431',
+ img: 'heartseeker',
+ },
+ AF: {
+ name: 'Arcane Focus',
+ id: '432',
+ img: 'arcanemeditation',
+ },
+
+ CF: {
+ name: 'Cloak of the Fallen',
+ id: 422,
+ img: 'fallenshield',
}
- Debug.logList.push({
- args: arguments,
- stack: (new Debug.Stack()).stack
- });
- if (Debug.logList.length > Debug.maxLogCache) {
- Debug.logList.shift();
+ };
+ const itemMap = {
+ ...{ // Consumables
+ 11191: ['体力长效药', '體力長效藥', 'Health Draught'],
+ 11195: ['体力药水', '體力藥水', 'Health Potion'],
+ 11199: ['体力秘药', '體力秘藥', 'Health Elixir'],
+ 11291: ['魔力长效药', '魔力長效藥', 'Mana Draught'],
+ 11295: ['魔力药水', '魔力藥水', 'Mana Potion'],
+ 11299: ['魔力秘药', '魔力秘藥', 'Mana Elixir'],
+ 11391: ['灵力长效药', '靈力長效藥', 'Spirit Draught'],
+ 11395: ['灵力药水', '靈力藥水', 'Spirit Potion'],
+ 11399: ['灵力秘药', '靈力秘藥', 'Spirit Elixir'],
+ 12101: ['火焰魔药', '火焰魔藥', 'Infusion of Flames'],
+ 12201: ['冰冷魔药', '冰冷魔藥', 'Infusion of Frost'],
+ 12301: ['闪电魔药', '閃電魔藥', 'Infusion of Lightning'],
+ 12401: ['风暴魔药', '風暴魔藥', 'Infusion of Storms'],
+ 12501: ['神圣魔药', '神聖魔藥', 'Infusion of Divinity'],
+ 12601: ['黑暗魔药', '黑暗魔藥', 'Infusion of Darkness'],
+ 13299: ['众神卷轴', '眾神捲軸', 'Scroll of the Gods'],
+ 13221: ['生命卷轴', '生命捲軸', 'Scroll of Life'],
+ 13211: ['幻影卷轴', '幻影捲軸', 'Scroll of Shadows'],
+ 13201: ['吸收卷轴', '吸收捲軸', 'Scroll of Absorption'],
+ 13199: ['化身卷轴', '化身捲軸', 'Scroll of the Avatar'],
+ 13111: ['守护卷轴', '守護捲軸', 'Scroll of Protection'],
+ 13101: ['加速卷轴', '加速捲軸', 'Scroll of Swiftness'],
+ 19111: ['花瓶', '花瓶', 'Flower Vase'],
+ 19131: ['泡泡糖', '泡泡糖', 'Bubble-Gum'],
+ 11501: ['终极秘药', '終極秘藥', 'Last Elixir'],
+ 11401: ['能量饮料', '能量飲料', 'Energy Drink'],
+ 11402: ['咖啡因糖果', '咖啡因糖果', 'Caffeinated Candy'],
+ } // Consumables
+ };
+
+ // runtime
+ const isFrame = window.self !== window.top;
+ function $id(id, d) { return (d || document).getElementById(id); }
+ const _servername = location.pathname.includes('/isekai/') ? 'isekai' : 'persistent';
+ const addition = {
+ other: _servername === 'isekai' ? 'persistent' : 'isekai',
+ utils: _servername === 'isekai' ? 'hvuti' : 'hvut',
+ };
+ const _server = {
+ name: _servername,
+ season: $id('world_text')?.textContent.match(/\d+ Season \d+/)?.[0] || '1',
+ [_servername]: true, // _server.persistent || _server.isekai
+ ...addition,
+ };
+ const isEquipDetail = window.location.href.includes('/equip/');
+ const isMaintaining = !gE('#csp') && !isEquipDetail;
+ const scriptVersion = Version(GM_info ? GM_info.script.version : '2.91');
+ let hvVersion;
+ let onIsekaiEncounter;
+ let monsterBuffSkillLib;
+ let ability = getValue('ability', true) ?? {};
+ let lastResponsive = new Date().getTime();
+
+ // util methods
+ function repeat(value, times) {
+ return range(times).map(_ => value);
+ }
+ function range(start, stop, step = 1) {
+ start = start?.length ?? start;
+ stop = stop?.length ?? stop;
+ if (stop === undefined) [start, stop] = [0, start];
+ const result = [];
+ switch (true) {
+ case step === 0:
+ case step > 0:
+ while (start < stop) {
+ result.push(start);
+ start += step;
+ }
+ break;
+ case step < 0:
+ while (start > stop) {
+ result.push(start);
+ start += step;
+ }
+ break;
+ default: throw new Error('range() arg 3 must not be zero');
}
- },
- shiftLog: function () {
- while (Debug.logList.length) {
- const log = Debug.logList.shift();
- console.log(...log.args, `\n`, log.stack);
+ return result;
+ }
+
+ const UI = {
+ langs: 3,
+ byLang: function (...args) {
+ if (Array.isArray(args[0])) args = args[0];
+ return args[g().lang];
+ },
+ alert: (...args) => window.alert(UI.byLang(args)),
+ confirm: (...args) => window.confirm(UI.byLang(args)),
+ prompt: (...args) => window.prompt(UI.byLang(args.slice(0, UI.langs)), args[UI.langs]),
+ l: function (...args) {
+ if (typeof args[0] !== 'string') args = args[0];
+ const extra = args[UI.langs] ?? '';
+ return range(UI.langs).map(i => `${args[i] ?? ''} `).join('');
+ },
+ button: {
+ class: function (className, ...inner) {
+ return `${inner?.join('') ?? ''} `
+ },
+ details: isDisplay => `${UI.l('详情', '詳情', 'Details')}${isDisplay ? `▲` : `▼`}`,
+ pause: function () {
+ const option = getOption();
+ return `${UI.l('暂停', '暫停', 'Pause')}${(option.pauseHotkey && option.pauseHotkeyStr) ? `(${option.pauseHotkeyStr})` : ''}`;
+ },
+ stepIn: function () {
+ const option = getOption();
+ return `${UI.l('步进', '步進', 'StepIn')}${(option.stepInHotkey && option.stepInHotkeyStr) ? `(${option.stepInHotkeyStr})` : ''}`;
+ },
+ continue: function () {
+ const option = getOption();
+ return `${UI.l('继续', '繼續', 'Continue', 'style="color:red;"')}${(option.pauseHotkey && option.pauseHotkeyStr) ? `(${option.pauseHotkeyStr})` : ''} `;
+ },
+ },
+ expendData: function (datas, method) {
+ const mapped = datas.map(args => method(args.id, UI.l(args.names), ...(args.values ?? [])));
+ if (Array.isArray(mapped[0])) return mapped.reduce((acc, cur) => (acc ?? []).concat(cur ?? []), [])?.join('');
+ return mapped.reduce((acc, cur) => (acc ?? '') + (cur ?? ''), '');
+ },
+ label: function (ids, inner, ...extra) {
+ return `${inner ?? ''} `
+ },
+ labeled: function (id, names, ...extra) {
+ return ` ${UI.label(id, names)}`;
+ },
+ orderValue: function (id, hidden) {
+ return UI.text(id, 'style="width:80%;" disabled="true"', hidden ? 'type="hidden"' : '') + (hidden ? '' : ' ');
+ },
+ text: function (id, ...extra) {
+ return ` e.match('type="hidden"')) ? '' : 'type="text"'} ${extra.join(' ')}>`;
+ },
+ number: function (id, placeholder = 0, type = 'number', classExtra, ...extra) {
+ return ``;
+ },
+ b: function (...inner) {
+ return `${inner.join('')} `;
+ },
+ repeat: function (n, size = '1fr') {
+ return `repeat(${n}, ${size})`
+ },
+ hvAATab: function (id, ...inner) {
+ return UI.div({
+ args: {
+ class: 'hvAATab', id: `hvAATab-${id}`,
+ style: id === 'Main' ? 'display: block' : '',
+ },
+ inner: inner
+ });
+ },
+ hvAATable: function (style, className, ...inner) {
+ return UI.div({
+ args: `class="hvAATable ${className}" style="grid-template-columns: ${style};"`,
+ inner: inner
+ });
+ },
+ div: function (...datas) {
+ if (datas.length === 0) return `
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;
}
- 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){}
- }
- //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;
+ 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 formatTime(t, size = 2, quick) {
+ t = [t / _1h, (t / _1m) % 60, (t / _1s) % 60, (t % _1s) / 10].map(cdi => Math.floor(cdi));
+ const option = getOption();
+ while (t.length > Math.max(size, quick ? 2 : 3)) { // remove zero front
+ const front = t.shift();
+ if (!front) {
+ continue;
+ }
+ t.unshift(front);
+ break;
}
- if ($ajax.tid) {
- if (!$ajax.conn) {
- clearTimeout($ajax.tid);
- $ajax.timer();
- $ajax.send();
+ return t;
+ }
+
+ function timeStr(time, size = 2, quick) {
+ let formated = formatTime(time, size, quick);
+ if (size) formated = formated.slice(0, size);
+ return formated.map(t => pad(t)).join(`:`);
+ }
+
+ function checkIsHV() {
+ if (window.location.host !== 'e-hentai.org') {
+ if (isMaintaining) {
+ // 维护中? 过一个小时再刷新
+ (async function onwait() { try {
+ const body = document.body;
+ const blockTip = /Blocking requests for (\d+) seconds due to excessive request rate/;
+ let blocked = body.innerText?.match(blockTip)?.[1] * _1s;
+ const duration = isNaN(blocked) ? _1h : blocked;
+ const start = time(0);
+ let remain;
+ await until(() => {
+ remain = duration - time(0) + start;
+ document.title = `[M]${timeStr(remain)}`;
+ try { if (!isNaN(blocked)) {
+ body.innerText = body.innerText.replace(blockTip, (...args) => args[0].replace(args[1], remain));
+ } } catch (err) { console.log(err) };
+ return remain <= 0;
+ });
+ goto();
+ } catch (err) { console.error(err)} })();
+ return true;
}
- } 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;
+ hvVersion = Version(...gE('script[src*="hvc.js"]', document)?.src.match(/z\/(\d+)(.*?)\/hvc.js/)?.slice(1, 3));
+ setValue('url', window.location.origin);
+ monsterBuffSkillLib = setMonsterBuffSkillLib();
+
+ // 补充记录(因写入冲突、网络卡顿等)未被记录的encounter链接
+ if (window.location.href.indexOf(`?s=Battle&ss=ba`) !== -1) {
+ const encounterURL = window.location.href?.split('/')[3];
+ const encounter = getEncounter();
+ const filtered = encounter.filter(e => e.url === encounterURL);
+ if (!filtered.length) {
+ encounter.unshift({ url: encounterURL, time: time(0), encountered: time(0) });
+ } else {
+ filtered[0].encountered ??= time(0);
+ }
+ setEncounter(encounter);
+ if (!isInBattle()) {
+ backFromBattle();
+ return;
+ }
}
- 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();
+
+ try {
+ if (window.location.href.startsWith('https://')) {
+ unsafeWindow.MAIN_URL = unsafeWindow.MAIN_URL.replace(/^http:/, `https:`);
+ } else {
+ unsafeWindow.MAIN_URL = unsafeWindow.MAIN_URL.replace(/^https:/, `http:`);
+ }
+ } catch (err) { /* console.log(err) */ }
+
+ return true;
}
- },
- 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); });
+ setValue('lastEH', time(0));
+ let encounter = getEncounter();
+ const eventpane = gE('#eventpane');
+ const now = time(0);
+ let url;
+ if (eventpane) { // 新一天或遭遇战
+ url = gE('#eventpane>div>a')?.href.split('/')[3];
+ if (url === undefined) encounter = []; // 新一天
+ encounter.unshift({ url: url, time: now });
+ setEncounter(encounter);
+ } else if (encounter.length) {
+ if (now - encounter[0]?.time > 0.5 * _1h) { // 延长最新一次的time, 避免因漏记录导致连续来回跳转
+ encounter[0].time = now;
+ setEncounter(encounter);
+ }
+ for (let e of encounter) {
+ if (e.encountered || time(0) - e.time >= 30 * _1m) continue;
+ url = e.url;
+ break;
+ }
+ }
- (function init() {
- if (!checkIsHV()) {
- return;
+ const isEngage = window.location.href === 'https://e-hentai.org/news.php?encounter';
+ if (!url) {
+ if (isEngage && !getLocal('persistent_battle')) {
+ // 自动跳转,同时先刷新遭遇时间,延长下一次遭遇
+ backFromBattle();
+ }
+ return false;
+ }
+ // 减少因在恒定世界处于战斗中时打开eh触发了遭遇而导致的错失
+ // 缓存当前链接,等战斗结束时再自动打开,下次打开链接时:
+ // 1. 若新的遭遇未出现,进入已缓存的战斗链接
+ // 2. 若新的遭遇已出现,则前一次已超时失效错过,重新获取新的一次
+ if (!isEngage) { // 战斗外,非自动跳转
+ if (eventpane) {
+ eventpane.style.cssText += 'color:red;'; // 链接标红提醒
+ }
+ } else if (getLocal('persistent_battle')) { //战斗中
+ if (eventpane) {
+ eventpane.style.cssText += 'color:gray;'; // 链接置灰提醒
+ }
+ } else { // 战斗外,自动跳转
+ let location = getLocal('url') ?? (document.referrer.match('hentaiverse.org') ? new URL(document.referrer).origin : 'https://hentaiverse.org');
+ $ajax.openNoFetch(`${location.includes('https') ? 'https://' : 'http://'}${(location.includes('alt') || getOption().altBattleFirst) ? 'alt.' : ''}hentaiverse.org/${url}`);
+ }
+ return false;
}
- if (!gE('#navbar,#riddlecounter,#textlog')) {
- setTimeout(goto, 5 * _1m);
- return;
+ // 答题//
+ async function riddleAlert() { try {
+ setAlarm('Riddle');
+ const option = getOption();
+ const answerTime = option.riddleAnswerTime;
+ let time;
+ const timeDiv = gE('#riddlecounter>div>div', 'all');
+ while (time === undefined || time > answerTime) {
+ if (timeDiv.length === 0) {
+ await pauseAsync(_1s);
+ continue;
+ }
+ time = undefined;
+ for (let t of timeDiv) {
+ time = (t.style.backgroundPosition.match(/(\d+)px$/)[1] / 12).toString() + (time ?? '');
+ }
+ time *= 1;
+ document.title = time;
+ await pauseAsync(_1s);
+ }
+ for (let ans of gE('#riddler1>*', 'all').children) {
+ if (!ans.children[0].children[0].checked) continue;
+ gE('#riddlesubmit').click();
+ return;
+ }
+ if (!option.riddleAnswerChoose) return;
+ // if no answer selected
+ const answers = ['aj', 'fs', 'pp', 'ra', 'rd', 'ts'].sort(Math.random);
+ const answer = `riddlesubmit=Submit+Answer` + answers.slice(0, Math.max(0, Math.min(6, option.riddleAnswerChoose))).map(ans => `&riddleanswer[]=${ans}`).join('');
+ const battle = gE('#battle_main', $doc(await $ajax.fetch(window.location.href, answer)));
+ if (!battle) console.error('ERROR: Failed fetch submit.');
+ goto();
+ } catch (err) { console.error(err); }}
+
+ function checkIsWindowTop() {
+ const currentUrl = window.self.location.href;
+ if (!isFrame) {
+ checkOption();
+ if (!getOption().riddlePopup || gE('#riddlecounter')) { // 未开启使用弹窗或仍处于答题
+ return true;
+ }
+ if (!window.opener || window.opener === window.self || window.opener.closed) { // 没有仍存在的opener
+ return true;
+ }
+ try {
+ if (!isInBattle(window.opener.document)) { // opener不处于战斗或答题中
+ return true;
+ }
+ } catch (err) {
+ console.error(err);
+ return true;
+ }
+ try {
+ window.opener.location.href = currentUrl;
+ } catch (err) {
+ console.error(err);
+ console.error(`current: ${currentUrl}`);
+ console.error(`opener: ${window.opener}`);
+ console.error(`opener.location: ${window.opener.location}`);
+ console.error(`opener.location.href: ${window.opener.location.href}`);
+ window.opener.location.href = window.opener.location.href;
+ }
+ const isFirefox = typeof InstallTrigger !== 'undefined';
+ tryClose(3, isFirefox ? 500 : 300);
+ return false;
+ }
+
+ if (isInBattle()) {
+ 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;
}
- 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;
+ async function safeClose(delay) {
+ try { window.close() } catch (err) { /* console.log(err) */ }
+ await pauseAsync(delay);
+ return !window || window.closed;
}
- 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);
+
+ async function tryClose(attempts, delay) { try {
+ await pauseAsync(delay);
+ window.opener = null;
+ window.open('', '_self');
+ if (await safeClose(delay)) return;
+ window.location.href = 'about:blank';
+ if (await safeClose(delay)) return;
+ attempts--;
+ if (attempts <= 0) {
+ document.body.innerHTML = 'Auto close popup failed. Please manually close window.
';
+ return;
+ }
+ await tryClose(attempts, delay);
+ } catch (err) { console.error('Opener reload or popup close failed:', err) } }
+
+ function getOption(unstable) {
+ return typeof GM_getValue === 'undefine' ? {} : (unstable ? g().option : g().stableOption) ?? {};
+ }
+
+ function checkOption() {
+ g('version', scriptVersion);
+ if (!getValue('option')) {
+ g('lang', window.prompt('请输入以下语言代码对应的数字\nPlease put in the number of your preferred language (0, 1 or 2)\n0.简体中文\n1.繁體中文\n2.English', 0) || 2);
+ addStyle();
+ UI.alert('请设置hvAutoAttack', '請設置hvAutoAttack', 'Please config this script');
+ gE('.hvAAButton').click();
+ return false;
+ }
+
+ let option = loadOption();
+ g('option', (isFrame || onIsekaiEncounter) ? option : setValue('option', option));
+ writePortables();
+ option = getOption(true);
+ g('lang', option.lang || '0');
+ addStyle();
+ if (onIsekaiEncounter) return;
+ g('stableOption', getOption(true));
+
+ // README等合并到主分支后再取消掉注释
+ // if (option.version.substr(0, 4) !== scriptVersion.ver.substr(0, 4)) {
+ // gE('.hvAAButton').click();
+ // if (UI.confirm('hvAutoAttack版本更新,请重新设置\n强烈推荐【重置设置】后再设置。\n是否查看更新说明?', 'hvAutoAttack版本更新,請重新設置\n強烈推薦【重置設置】後再設置。\n是否查看更新說明?', 'hvAutoAttack version update, please reset\nIt\'s recommended to reset all configuration.\nDo you want to read the changelog?')) {
+ // $ajax.openNoFetch('https://github.com/dodying/UserJs/commits/master/HentaiVerse/hvAutoAttack/hvAutoAttack.user.js', true);
+ // }
+ // gE('.hvAAReset').focus();
+ // return false;
+ // }
+
+ if (gE('[class^="c5"], [class^="c4"]') && UI.confirm('请设置字体\n使用默认字体可能使某些功能失效\n是否查看相关说明?', '請設置字體\n使用默認字體可能使某些功能失效\n是否查看相關說明?', 'Please set the font\nThe default font may make some functions fail to work\nDo you want to see instructions?')) {
+ $ajax.openNoFetch(`https://github.com/dodying/UserJs/blob/master/HentaiVerse/hvAutoAttack/README${g().lang === '2' ? '_en.md#about-font' : '.md#关于字体的说明'}`, true);
+ return false;
}
- gE('.hvAAReset').focus();
- return;
+ return true;
}
- 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;
+ function writePortables() {
+ const option = getOption(true);
+ if (!option.portable) return;
+ for (const key of dataFlags.portable) {
+ if (!(Object.keys(option.portable).includes(key))) continue;
+ setValue(key, getValue(key), true);
+ }
}
- unsafeWindow = typeof unsafeWindow === 'undefined' ? window : unsafeWindow;
+ function backFromBattle() {
+ const beforeEncounter = getValue('beforeEncounter');
+ if (beforeEncounter) {
+ setValue('lastUrl', beforeEncounter);
+ delValue('beforeEncounter');
+ }
+ $ajax.openNoFetch(getValue('lastUrl'));
+ }
- if (gE('#riddlecounter')) { // 需要答题
- if (!g('option').riddlePopup || window.opener) {
- riddleAlert(); // 答题警报
- return;
+ function onRiddle() {
+ if (!gE('#riddlecounter')) {
+ return false;
}
- window.open(window.location.href, 'riddleWindow', 'resizable,scrollbars,width=1241,height=707');
- return;
+ setValue('onriddle', true);
+ (window.opener ?? window).console.log('onriddle', { riddlePopup: getOption().riddlePopup, opener: window.opener });
+ if (!getOption().riddlePopup || window.opener) {
+ riddleAlert();
+ return true;
+ }
+ window.open(window.location.href, 'riddleWindow', 'resizable, scrollbars, width=1241, height=707');
+ return true;
}
- 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) });
+ function onIdle() {
+ if (!gE('#navbar')) {
+ return false;
}
- setEncounter(encounter);
+ // 战斗结束跳转回原链接
+ if (window.top.location.href.endsWith(`?s=Battle`)) {
+ backFromBattle();
+ return true;
+ }
+ const arena = getValue('arena', true) ?? {};
+ delete arena?.equip;
+ arena.postoken = gE('input[name="postoken"]')?.value ?? arena.postoken;
+ setValue('arena', arena);
+
+ if (window.location.href.indexOf(`?s=Battle&ss=ba`) === -1) { // 不缓存encounter
+ setValue('lastUrl', window.top.location.href); // 缓存进入战斗前的页面地址
+ setArenaDisplay();
+ }
+ delValue(1);
+ const option = getOption();
+ if (option.showQuickSite && option.quickSite) {
+ quickSite();
+ }
+ const hvAAPauseUI = document.body.appendChild(cE('div'));
+ hvAAPauseUI.classList.add('hvAAPauseUI');
+ setPauseUI(hvAAPauseUI);
+ asyncOnIdle();
+ return true;
+ }
+
+ function onBattleBox() {
+ let box = gE('#hvAABox2');
+ if (box) return box;
+ box = gE('#battle_main').appendChild(cE('div'));
+ box.id = 'hvAABox2';
+ setPauseUI(box);
+ return box;
}
- if (!gE('#navbar')) { // 战斗中
- const box2 = gE('#battle_main').appendChild(cE('div'));
- box2.id = 'hvAABox2';
- setPauseUI(box2);
+
+ function onBattle() {
+ if (!gE('#textlog')) {
+ return false;
+ }
+ checkResponsive();
+
+ if (getValue('onriddle')) {
+ window.history.replaceState(null, '', window.location.href);
+ delValue('onriddle');
+ }
+ onBattleBox();
reloader();
- g('attackStatus', g('option').attackStatus);
+ const option = getOption();
+ g('attackStatus', option.attackStatus);
+ // 1二天 2单手 3双手 4双持 5法杖
+ range(5).map(s => s + 1).filter(s => gE(`2${s}01`)).forEach(s => g('fightingStyle', s.toString()));
g('timeNow', time(0));
g('runSpeed', 1);
- Debug.log('______________newRound', false);
newRound(false);
- if (g('option').recordEach && !getValue('battleCode')) {
- setValue('battleCode', `${time(1)}: ${g('battle')?.roundType?.toUpperCase()}-${g('battle')?.roundAll}`);
+ updateMonsterEffects(false);
+ onBattleRound();
+ const battle = g().battle;
+ if (option.recordEach) {
+ let code = getValue('battleCode', true);
+ const tokens = { token: document.body.innerHTML.match(`var battle_token = \"(.*)\";`)[1], postoken: battle?.postoken };
+ const same = Object.keys(tokens).map(k => tokens[k] === code?.[k]).every(s => s);
+ if (!same || !code?.roundAll || !code?.roundNow) {
+ const now = same ? code?.time ?? time(1) : time(1);
+ const roundType = battle?.roundType?.toUpperCase();
+ const [roundAll, roundNow] = [battle?.roundAll, battle?.roundNow];
+ code = {
+ ...tokens,
+ time: now,
+ roundType, roundAll, roundNow,
+ name: `${now}: ${roundType}-${roundAll}`,
+ };
+ setValue('battleCode', code);
+ }
}
- onBattle();
- updateEncounter(false, true);
- return;
- }
- if(window.top.location.href.endsWith(`?s=Battle`)){
- $ajax.openNoFetch(getValue('lastHref'));
- return;
+ updateEncounter(_server.isekai && option.encounter);
+ return true;
}
- // 战斗外
- if (window.location.href.indexOf(`?s=Battle&ss=ba`) === -1) { // 不缓存encounter
- setValue('lastHref', window.top.location.href); // 缓存进入战斗前的页面地址
- setArenaDisplay();
+ // ----------methods----------
+ // 通用//
+ function unique(arr) {
+ const newArr = [];
+ for (const i of range(arr)) {
+ if (newArr.indexOf(arr[i]) === -1) {
+ newArr.push(arr[i]);
+ }
+ }
+ return newArr;
+ }
+
+ function object2Order(orderValue, ...args) {
+ switch (typeof orderValue) {
+ case 'string':
+ return orderValue?.split(',') ?? [];
+ case 'number':
+ case 'boolean':
+ return [orderValue];
+ case 'function':
+ return object2Order(orderValue(...args));
+ case 'object':
+ case 'undefined':
+ if (!orderValue) return []; // null or undefined
+ if (Array.isArray(orderValue)) return orderValue;
+ break;
+ default:
+ break;
+ }
+ throw new Error('Unsupported typeof orderValue:', orderValue, typeof orderValue);
}
- delValue(1);
- if (g('option').showQuickSite && g('option').quickSite) {
- quickSite();
+
+ function getDefaultOrder(idMatch, map) {
+ const defaultOrder = g().defaultOrder ??= {};
+ const key = idMatch + (map?.toString() ?? '');
+ return (defaultOrder[key] ??= [...gE(`[id^="${idMatch}_"]`, 'all')].map(map ?? (ord => ord.id.match(/_(.*)/)[1])));
}
- const hvAAPauseUI = document.body.appendChild(cE('div'));
- hvAAPauseUI.classList.add('hvAAPauseUI');
- setPauseUI(hvAAPauseUI);
- asyncOnIdle();
- }());
+ function splitOrders(orderValue, defaultOrder, ...args) {
+ return unique(object2Order(orderValue, ...args).concat(defaultOrder ?? []).map(v => isNaN(v * 1) ? v : v * 1));
+ }
- function setArenaDisplay(){
- if(window.location.href.indexOf(`?s=Battle&ss=ar`) === -1 && window.location.href.indexOf(`?s=Battle&ss=rb`) === -1){
- return;
+ function goto(url) { // 前进
+ window.location.href = url ?? (window.location.search ? window.location.pathname + window.location.search : window.location.href);
+ setTimeout(goto, 5 * _1s);
+ setTimeout(() => { window.location.href = window.location.href }, 10 * _1s);
+ return true;
}
- var ar = g('option').idleArenaValue?.split(',');
- if(!ar || ar.length === 0){
- return;
+
+ 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;
}
- if(!g('option').obscureNotIdleArena){
- return;
+
+ function pauseAsync(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
}
- 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 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;
}
- gE('div', 'all', btn.parentNode.parentNode).forEach(div=>{div.style.cssText += 'color:grey!important;'});
- });
- }
+ resolve();
+ }
- 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]];
+ function pad(num, pad = '0', total = 2) {
+ return num.toString().padStart(total, pad);
+ }
+
+ function gE(ele, mode, parent) { // 获取元素
+ if (typeof ele === 'object') {
+ return ele;
+ } if (mode === undefined && parent === undefined) {
+ return (isNaN(ele * 1)) ? document.querySelector(ele) : document.getElementById(ele);
+ } if (mode === 'all') {
+ return (parent === undefined) ? document.querySelectorAll(ele) : parent.querySelectorAll(ele);
+ } if (typeof mode === 'object' && parent === undefined) {
+ return mode.querySelector(ele);
}
}
- if(isFrame){
- g('option', option);
- } else{
- g('option', setValue('option', option));
+
+ function cE(name) { // 创建元素
+ return document.createElement(name);
}
- }
- function pauseAsync(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
+ function $doc(h) {
+ const doc = document.implementation.createHTMLDocument('');
+ doc.documentElement.innerHTML = h;
+ return doc;
+ }
- async function asyncOnIdle() { try {
- if(getValue('disabled')){
- await pauseAsync(_1s);
- return await asyncOnIdle();
- }
- let notBattleReady = false;
- const idleStart = time(0);
- await Promise.all([
- (async () => { try {
- await asyncGetItems();
- const checked = await asyncCheckSupply();
- notBattleReady ||= !checked;
- } catch (e) {console.error(e)}})(),
- asyncSetStamina(),
- asyncSetEnergyDrinkHathperk(),
- asyncSetAbilityData(),
- updateArena(),
- updateEncounter(g('option').encounter),
- (async () => { try {
- const checked = await asyncCheckRepair();
- notBattleReady ||= !checked;
- } catch (e) {console.error(e)}})(),
- ]);
- if (notBattleReady) {
- return;
- }
- if (g('option').idleArena && g('option').idleArenaValue) {
- startUpdateArena(idleStart);
- }
- setTimeout(autoSwitchIsekai, (g('option').isekaiTime * (Math.random() * 20 + 90) / 100) * _1s - (time(0) - idleStart));
- } catch (e) {console.error(e)}}
-
- // 通用//
- function setPauseUI(parent) {
- setPauseButton(parent);
- setPauseHotkey();
- }
+ function popup(text) {
+ if (!getOption().popup) return;
+ const popupWindow = cE('div');
+ popupWindow.style.cssText += 'position:fixed;top:0;left:0;width:100%;height:100%;background-color:#0006;z-index:1001;cursor:pointer;display:flex;justify-content:center;align-items:center;'
+ popupWindow.addEventListener('click', r);
+ document.body.appendChild(popupWindow);
+ const display = cE('div');
+ display.innerText = text;
+ display.style.cssText += 'min-width:400px;min-height:100px;max-width:100%;max-height:100%;padding:10px;background-color:#fff;border:1px solid;display:flex;flex-direction:column;justify-content:center;font-size:10pt;color:#333;';
+ popupWindow.appendChild(display);
+ document.addEventListener('keydown', r);
+ return display;
- function setPauseButton(parent) {
- if (!g('option').pauseButton) {
- return;
- }
- const button = parent.appendChild(cE('button'));
- button.innerHTML = '(.*?)<\/l(\d+)>/g, (matched, lang1, inner, lang2) => {
+ switch (true) {
+ case lang1 !== lang2: return inner;
+ case lang1 !== lang: return '';
+ default: return inner;
+ }
+ });
+ }).reduce((acc, cur) => (acc ?? '') + (cur ?? ''), '') || key}: ${data.map(d => d ? String(d) : defaultStr)?.join(' -> ')}`;
+ }).filter(d => d !== undefined).join('\n');
+
+ function diffData(datas, parents) {
+ let json = datas.map(JSON.stringify);
+ if (unique(json).length === 1) return undefined;
+ if (datas.some(d=> !['object', 'undefined'].includes(typeof d))) {
+ const diff = {};
+ diff[parents] = datas;
+ return diff;
+ }
+ const keys = datas.map(data => data ? Object.keys(data) : undefined);
+ let differents;
+ unique(keys.reduce((acc, cur) => (acc ?? []).concat(cur ?? []), [])).forEach(key => {
+ if ([diffData.prototype.excludes, ...diffData.prototype.excludes].includes(key)) return;
+ let datasSub = datas.map(data => data?.[key]);
+ key = parents?`${parents}_${key}`:key;
+ const diff = diffData(datasSub, key);
+ if (!diff) return;
+ differents = { ...(differents ??= {}), ...diff };
+ });
+ return differents;
+ }
+ }
+
+ function getCurrentUIOption() {
+ const _option = {};
+ let name, array, value, type;
+ for (const input of gE('input, select', 'all', optionBox)) {
+ [name, type, value] = [input.name, input.type, input.value];
+ switch(input.className) {
+ case 'hvAADebug': continue;
+ case 'hvAANumber': type = 'number';
}
- } else if (inputs[i].type === 'checkbox') {
- itemName = inputs[i].id;
- itemValue = inputs[i].checked;
- if (itemValue === false) {
+ 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;
}
- } 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]] = {};
+
+ if ((array = name.split('_')).length === 1) {
+ _option[name] = value;
+ continue;
}
- if (inputs[i].className === 'customizeInput') {
- if (typeof _option[itemArray[0]][itemArray[1]] === 'undefined') {
- _option[itemArray[0]][itemArray[1]] = [];
- }
- _option[itemArray[0]][itemArray[1]].push(itemValue);
+ if (input.className === 'customizeInput') {
+ ((_option[array[0]] ??= {})[array[1]] ??= []).push(value);
} else {
- _option[itemArray[0]][itemArray[1]] = itemValue;
+ (_option[array[0]] ??= {})[array[1]] = value;
}
}
- }
- inputs = gE('.hvAAQuickSite input[type="text"]', 'all', optionBox);
- for (i = 0; 3 * i < inputs.length; i++) {
- if (i === 0 && inputs.length !== 0) {
- _option.quickSite = [];
- }
- if (inputs[3 * i + 1].value === '') {
- continue;
+
+ 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 });
}
- _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;
+ return _option;
+ }
+
+ function formatOption(option, skipUI) {
+ for (const obj in option) {
+ if (['auto', 'server'].includes(obj)) continue;
+ 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;
+ if (!['enableItemWorld', 'levelItemWorld', 'enableItemWorld', 'itemWorldPersona', 'itemWorldEquipSet'].includes(obj)) console.log(`Legacy option deleted: ${obj}_${key}`);
+ delete option[obj][key];
+ }
+ if (!found) delete option[obj];
+ continue;
+ }
+ console.log(`Legacy option deleted: ${obj}`);
+ delete option[obj];
}
- 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]];
+
+ const inputs = gE('input, select', 'all', optionBox);
+
+ let name, array, value, type, placeholder, num;
+ function formatValue(value, placeholder) {
+ if (['', undefined].includes(value) && placeholder) {
+ value = placeholder;
+ }
+ switch (type) {
+ case 'text':
+ case 'hidden':
+ case 'select-one':
+ case 'number':
+ num = value * 1;
+ return (!isNaN(num)) ? num : value;
+ case 'checkbox':
+ return value ? true : undefined;
+ default:
+ return value;
}
}
- if (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;
+
+ for (const input of inputs) {
+ [name, type, placeholder] = [input.name || input.id, input.type, input.placeholder];
+ switch(input.className) {
+ case 'hvAADebug': continue;
+ case 'hvAANumber': type = 'number';
+ }
+ [array, value] = [name.split('_'), undefined];
+ if (array.length === 1) {
+ value = formatValue(option[name], placeholder);
+ if (value || value === 0) option[name] = value;
+ } else if (!input.classList.contains('customizeInput')) {
+ value = formatValue(option[array[0]]?.[array[1]], placeholder);
+ if (value || value === 0) (option[array[0]] ??= {})[array[1]] = value;
+ }
+ if (type !== 'checkbox' && ![placeholder * 1, placeholder].includes(value)) {
+ value = value === undefined ? '' : value;
+ }
+
+ if (skipUI || onIsekaiEncounter) continue;
+ switch (type) {
+ case 'select-one' :
+ case 'text':
+ case 'hidden':
+ case 'number':
+ input.value = value;
+ customizeInputAutoFit(input);
+ break;
+ case 'checkbox':
+ input.checked = !!value;
+ displayCheckBoxNotDefault(input);
+ input.addEventListener('change', () => displayCheckBoxNotDefault(input));
+ }
}
- }
- const customize = gE('.customize', 'all', optionBox);
- for (i = 0; i < customize.length; i++) {
- itemName = customize[i].getAttribute('name');
- if (itemName in _option) {
- for (j in _option[itemName]) {
- const group = customize[i].appendChild(cE('div'));
+ return option;
+ }
+
+ function loadOptionUIData(uiOption) {
+ let isCache = !uiOption;
+ uiOption ??= option;
+ if (!uiOption) return;
+ uiOption = formatOption(uiOption);
+ if (onIsekaiEncounter) return;
+ const customizes = gE('.customize', 'all', optionBox);
+ customizes.forEach(n => { while(n.firstChild) { n.removeChild(n.firstChild); }});
+ for (let customize of customizes) {
+ const name = customize.getAttribute('name');
+ if (!(name in uiOption)) {
+ const group = customize.appendChild(cE('div'));
+ group.className = 'customizeGroup';
+ group.innerHTML = `1. `;
+ const input = group.appendChild(cE('input'));
+ input.type = 'text';
+ input.className = 'customizeInput';
+ input.name = `${name}_0`;
+ customizeInputAutoFit(input, true);
+ continue;
+ }
+ for (const groupIndex in uiOption[name]) {
+ const group = customize.appendChild(cE('div'));
group.className = 'customizeGroup';
- group.innerHTML = `${j * 1 + 1}. `;
- for (k = 0; k < _option[itemName][j].length; k++) {
+ group.innerHTML = `${groupIndex * 1 + 1}. `;
+ for (const index of range(uiOption[name][groupIndex])) {
const input = group.appendChild(cE('input'));
input.type = 'text';
input.className = 'customizeInput';
- input.name = `${itemName}_${j}`;
- input.value = _option[itemName][j][k];
+ input.name = `${name}_${groupIndex}`;
+ input.value = uiOption[name][groupIndex][index];
+ customizeInputAutoFit(input, index === uiOption[name][groupIndex].length-1);
}
}
}
- }
- if (_option.quickSite) {
- _html = '图标 圖標 ICON 名称 名稱 Name 链接 鏈接 Link ';
- _option.quickSite.forEach((i) => {
- _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} `;
+
+ if (!isCache) return;
+ g('option', uiOption);
+ let _html;
+ if (option.quickSite) {
+ _html = `${UI.l('图标', '圖標', 'ICON')} ${UI.l('名称', '名稱', 'Name')} ${UI.l('链接', '鏈接', 'Link')} `;
+ option.quickSite.forEach((i) => {
+ _html = `${_html} `;
+ });
+ gE('.hvAAQuickSite>table>tbody', optionBox).innerHTML = _html;
}
- gE('.hvAABackupList', optionBox).innerHTML = _html;
- }
+
+ if (getValue('backup')) {
+ const backups = getValue('backup', true);
+ _html = '';
+ for (const i in backups) {
+ _html = `${_html}${i} `;
+ }
+ 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');
- } 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;
+ function customizeInputAutoFit(input, isLastCustomizeInput) {
+ if (input.type === 'select-one' || input.disabled && input.name !== 'version') return;
+ customizerInpuFit(input, isLastCustomizeInput);
+ input.addEventListener('input', _ => customizerInpuFit(input, true));
+ input.addEventListener('keydown', function(event) {
+ if (event.key !== 'Enter') return;
+ event.preventDefault();
+ const currentGroup = input.parentNode;
+ let nextGroup = event.shiftKey ? currentGroup.previousElementSibling : currentGroup.nextElementSibling;
+ if (!nextGroup && !event.shiftKey) {
+ const nextGroupIndex = input.name.match(/_(\d+)$/)[1] * 1 + 1;
+ nextGroup = input.closest('.customize').appendChild(cE('div'));
+ nextGroup.className = 'customizeGroup';
+ nextGroup.innerHTML = `${nextGroupIndex + 1}. `
+
+ const newInput = nextGroup.appendChild(cE('input'));
+ newInput.type = 'text';
+ newInput.className = 'customizeInput';
+ newInput.name = input.name.replace(/_(\d+)$/, _ => `_${nextGroupIndex}`);
+ customizeInputAutoFit(newInput, true);
+
+ const select = gE('.customizeBox select[name="groupChoose"]');
+ if (select) {
+ const selectOptions = gE('option', 'all', select);
+ const prevSelected = select.value;
+ const optionUI = select.appendChild(cE('option'));
+ selectOptions[selectOptions.length - 1].value = nextGroupIndex + 1;
+ selectOptions[selectOptions.length - 1].textContent = nextGroupIndex + 1;
+ optionUI.value = 'new';
+ optionUI.textContent = 'new';
+ select.value = prevSelected;
+ }
}
- 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);
+ ((nextGroup ? gE(event.shiftKey ? '.customizeInput:last-child' : '.customizeInput:first-child', nextGroup) : undefined) ?? gE('.customizeInput:first-child', currentGroup)).focus()
+ });
+ }
+
+ function selectFit(select) {
+ if (select.value === 'undefined') {
+ select.classList.add('optionDefault');
} else {
- this.title = 'on';
- gE('#csp').addEventListener('mousemove', funcSelect);
+ select.classList.remove('optionDefault');
}
- };
- 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();
+ }
+
+ function customizerInpuFit(input, dynamic) {
+ if (input.value === input.placeholder) {
+ input.classList.add('optionDefault');
} else {
- group = gE('.customizeGroup', 'all', target)[groupChoose - 1];
+ input.classList.remove('optionDefault');
+ }
+ autoResizeInput(input);
+ if (!input.classList.contains('customizeInput') || !dynamic) return;
+ if (input.nextElementSibling || !input.value) return;
+ const newInput = input.parentNode.appendChild(cE('input'));
+ newInput.type = 'text';
+ newInput.className = 'customizeInput';
+ newInput.name = input.getAttribute('name');
+ customizeInputAutoFit(newInput);
+ }
+
+ function autoResizeInput(input) {
+ const measure = cE('span');
+ const styles = window.getComputedStyle(input);
+ measure.style.cssText = `
+ visibility: hidden;
+ white-space: pre;
+ font-family: ${styles.fontFamily};
+ font-size: ${styles.fontSize};
+ font-weight: ${styles.fontWeight};
+ letter-spacing: ${styles.letterSpacing};
+ padding: ${styles.padding};
+ border: ${styles.border};
+ box-sizing: ${styles.boxSizing};
+ position: absolute;
+ top: -9999px;
+ left: -9999px;
+ `;
+ measure.textContent = input.value;
+ document.body.appendChild(measure);
+ input.style.width = measure.offsetWidth + 'px';
+ document.body.removeChild(measure);
+ }
+
+ function creatCustomizeBox() { // 自定义条件界面
+ let customizeBox = gE('.customizeBox');
+ if (customizeBox) return customizeBox;
+ customizeBox = gE('body').appendChild(cE('div'));
+ customizeBox.className = 'customizeBox';
+ const statusOption = 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 ',
+ 'buffStack ',
+ '- - - - ',
+ 'targetBuffStack ',
+ '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} `,
+ UI.button.class('groupAdd', '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') {
+ const select = gE('select[name="groupChoose"]', customizeBox);
+ const selectOptions = gE('option', 'all', select);
+ groupChoose = selectOptions.length;
+ group = target.appendChild(cE('div'));
+ group.className = 'customizeGroup';
+ group.innerHTML = `${groupChoose}. `;
+ selects[0].click();
+ const prevSelected = select.value;
+ const optionUI = select.appendChild(cE('option'));
+ selectOptions[selectOptions.length-1].value = groupChoose;
+ selectOptions[selectOptions.length-1].textContent = groupChoose;
+ optionUI.value = 'new';
+ optionUI.textContent = 'new';
+ select.value = prevSelected;
+ } else {
+ group = gE('.customizeGroup', 'all', target)[groupChoose - 1];
+ }
+ const items = gE('*', 'all', group);
+ let input;
+ for (let i of range(items, 0, -1)) {
+ if (items[i-1].value) break;
+ input = items[i-1];
+ }
+ input ??= group.appendChild(cE('input'));
+ input.type = 'text';
+ input.className = 'customizeInput';
+ input.name = `${target.getAttribute('name')}_${groupChoose - 1}`;
+ input.value = `${selects[1].value} ${selects[2].value} ${selects[3].value}`;
+ customizeInputAutoFit(input, true);
+ updateGroup(true);
+ };
+ return customizeBox;
+
+ 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]}`;
+ }
}
- 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}`;
- };
+ }
- 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 updateGroup(keepPosition) {
+ const target = g().customizeTarget;
+ const group = gE('.customizeGroup', 'all', target);
+ const customizeBox = gE('.customizeBox');
+ if (group.length + 1 === gE('select[name="groupChoose"]>option', 'all', customizeBox).length) {
+ updateGroupUI();
+ return;
+ }
+ const select = gE('select[name="groupChoose"]', customizeBox);
+ select.textContent = '';
+ for (const i of range(group.length + 1)) {
+ const optionUI = select.appendChild(cE('option'));
+ optionUI.textContent = optionUI.value = i === group.length ? 'new' : i + 1;
+ }
+ updateGroupUI();
+
+ function updateGroupUI() {
+ const position = target.getBoundingClientRect();
+ const bodyPosition = document.body.getBoundingClientRect();
+ customizeBox.style.cssText += `z-index: 20;display: block; height: ${gE('.customizeGroup', 'all', target).length * 30 + 60}px;`
+ if (!keepPosition) {
+ customizeBox.style.top = `${position.bottom - bodyPosition.top}px`;
+ customizeBox.style.left = `${position.left - bodyPosition.left}px`;
+ }
}
}
- }
- function setAlarm(e) { // 发出警报
- e = e || 'Common';
- if (g('option').notification) {
- setNotification(e);
+ function setAlarm(e, testSrc) { // 发出警报
+ const option = getOption();
+ e = e || 'Common';
+ if (option.notification || testSrc) {
+ setNotification(e);
+ }
+ if (option.alert && option.audioEnable?.[e] || testSrc) {
+ setAudioAlarm(e, testSrc);
+ }
}
- if (g('option').alert && g('option').audioEnable && g('option').audioEnable[e]) {
- setAudioAlarm(e);
+
+ function playAudio(audio) {
+ audio.onPlay ??= () => {
+ audio.removeEventListener('canplaythrough', audio.onPlay);
+ audio.play();
+ };
+ audio.removeEventListener('canplaythrough', audio.onPlay);
+ audio.addEventListener('canplaythrough', audio.onPlay);
+ // 如果音频已缓存,canplaythrough 可能不会再次触发,此时可直接播放
+ if (audio.readyState >= 3) { // HAVE_FUTURE_DATA 或更高
+ audio.play();
+ }
+ if (audio.loop) {
+ const battleNow = unsafeWindow.battle;
+ (async ()=> {
+ const start = time(0);
+ let focused;
+ await until(()=> {
+ if (!focused && document.hasFocus()) {
+ focused = true;
+ document.addEventListener('mousemove', () => {
+ audio.pause();
+ audio.loop = false;
+ }, true);
+ }
+ return !audio.loop || (unsafeWindow.battle !== battleNow);
+ });
+ audio.pause();
+ })();
+ }
}
- }
- function setAudioAlarm(e) { // 发出音频警报
- let audio;
- if (gE(`#hvAAAlert-${e}`)) {
- audio = gE(`#hvAAAlert-${e}`);
- } else {
- audio = gE('body').appendChild(cE('audio'));
- audio.id = `hvAAAlert-${e}`;
+ function setAudioAlarm(e, testSrc) { // 发出音频警报
+ const option = getOption();
+ let audio = gE(`#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');
+ if (!audio) {
+ audio = gE('body').appendChild(cE('audio'));
+ audio.id = `hvAAAlert-${e}`;
+ audio.controls = true;
+ }
+ if (!audio.src || audio.readyState <= 2 || (testSrc && audio.src !== testSrc)) {
+ audio.src = testSrc ?? option.audio?.[e] ?? `https://github.com/dodying/UserJs/raw/master/HentaiVerse/hvAutoAttack/${e}${fileType}`;
+ }
+ audio.loop = (e === 'Riddle') && !testSrc;
+ playAudio(audio);
+ }
+
+ 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,
+ },
+ Pause: {
+ text: '触发自动暂停',
+ time: 3,
+ },
+ Flee: {
+ text: '触发自动逃跑',
+ 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,
+ },
+ Pause: {
+ text: '觸發自動暫停',
+ time: 3,
+ },
+ Flee: {
+ text: '觸發自動逃跑',
+ 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,
+ },
+ Pause: {
+ text: 'Auto paused',
+ time: 3,
+ },
+ Flee: {
+ text: 'Auto fleed',
+ 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: getOption().focusNotification,
+ timeout: notification.time * _1s,
+ });
+ }
+ if (window.Notification && window.Notification.permission !== 'denied') {
+ window.Notification.requestPermission((status) => {
+ if (status === 'granted') {
+ const n = new window.Notification(notification.text, {
+ icon: `${unsafeWindow.IMG_URL}hentaiverse.png`,
+ });
+ setTimeout(() => n?.close(), notification.time * _1s);
+
+ const nClose = function (e) {
+ 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)));
+ },
+ buffStack(...img) {
+ return getBuffStackFromImg(getBuff(imgArray2img(...img)));
+ },
+ hpDecimal() {
+ return g().hp / 100;
+ },
+ mpDecimal() {
+ return g().mp / 100;
+ },
+ spDecimal() {
+ return g().sp / 100;
+ },
+ ocDecimal() {
+ return g().oc / 100;
+ },
+ };
+ let currentGroup = null;
+ const func = {
+ ...returnValueGetter.prototype.func,
+ targetBuffStack(...img) {
+ const getter = (t, i) => getBuffStackFromImg(getBuff(imgArray2img(i), getMonsterID(t)));
+ let param = minmaxModes.includes(img[0]) ? img.shift() : undefined;
+ return switchMaxMin(param, t => getter(t, img));
+ },
+ targetBuffTurn(...img) {
+ const getter = (t, i) => getBuffTurnFromImg(getBuff(imgArray2img(i), getMonsterID(t)));
+ let param = minmaxModes.includes(img[0]) ? img.shift() : undefined;
+ return switchMaxMin(param, t => getter(t, img));
+ },
+ 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 = getOption();
+ const battle = g().battle ?? {};
+ while (paramList.length) {
+ const key = paramList.shift();
+ if (typeof result === 'undefined') { // 获取顶层数据
+ result = battle[key];
+ if (typeof result === 'undefined' || result === null) {
+ result = getValue('battle', true) ? getValue('battle', true)[key] : undefined;
+ }
+ if (typeof result === 'undefined' || result === null) {
+ result = g(key);
+ }
+ if (typeof result === 'undefined' || result === null) {
+ result = getValue(key);
+ }
+ if (typeof result === 'undefined' || result === null) {
+ result = option[key];
+ }
+ if ((typeof result === 'undefined' || result === null) && func[key]) {
+ result = func[key](...paramList);
+ }
+ if (typeof result === 'undefined' || result === null) break;
+ isInData = true; // 存在顶层数据
+ continue;
+ }
+ 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 '!=';
+ }
+ }).replace(/===/g, '==').replace(/(?!~])=(?!=)/g, '==')
+ .replace(/≥|≤|≠|~=|<>/g, (match) => {
+ switch (match) {
+ case '≥':
+ return '>=';
+ case '≤':
+ return '<=';
+ case '≠':
+ case '~=':
+ case '<>':
+ return '!=';
+ }
+ }).replace('_1h', '_onehanded').replace('_2h', '_twohanded');
+ isDebug = k.match(/^@/);
+ result = $RPN.evaluate(k.replace(/^@/, ''), str => returnValue(str, isDebug));
+ if (isDebug) console.log([k], result, returnValue.paramResultsGetter());
+ return result;
+ }
+
+ function resolveRPNFormula(formula, target) {
+ let paramResults = {};
+ const returnValue = returnValueGetter(() => paramResults, () => target);
+ return handleRPNFormula(formula, returnValue);
+ }
+
+ function checkCondition(params, targets = undefined) {
+ let i, j, k, target, paramResults = {};
+ targets ??= [g().battle.monsterStatus[0]];
+ if (!params || !Object.keys(params).length) {
+ return targets[0];
+ }
+ const returnValue = returnValueGetter(() => paramResults, () => target);
+ for (i in params) { for (target of targets.filter(t => !t.isDead)) {
+ paramResults = {};
+ let paramResult = true;
+ for (j of range(params[i])) {
+ let result = true;
+ if (!Array.isArray(params[i])) continue;
+ const formula = params[i][j];
+ result = handleRPNFormula(formula, returnValue);
+ if (result) continue;
+ paramResult = false;
+ break;
+ }
+ if (paramResult) return target;
+ }} return undefined;
+ }
+
+ function pauseChange() { // 暂停状态更改
+ const option = getOption();
+ if (getValue('disabled')) {
+ if (gE('.pauseChange')) {
+ gE('.pauseChange').innerHTML = UI.button.pause();
}
- if (result[i] === false) {
- j = parms[i].length;
+ document.title = gE('#navbar') ? 'The Hentaiverse' : getValue('disabled');
+ delValue(0);
+ if (!gE('#navbar')) { // in battle
+ onBattleRound();
}
+ } else {
+ if (gE('.pauseChange')) {
+ gE('.pauseChange').innerHTML = UI.button.continue();
+ }
+ setValue('disabled', document.title);
+ document.title = titlePause();
}
- if (result[i] === true) {
- return true;
+ }
+
+ function stepIn() {
+ setValue('stepIn', true);
+ if (getValue('disabled')) {
+ g('timeNow', time(0));
+ pauseChange();
}
}
- return false;
- }
- // 答题//
- function riddleAlert() { // 答题警报
- if (window.opener) {
- gE('#riddleanswer+img').onclick = function () {
- riddleSubmit(gE('#riddleanswer').value);
- };
+ function onStepInDone() {
+ if (!getValue('stepIn')) {
+ return;
+ }
+ delValue('stepIn');
+ pauseChange();
}
- 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;
+
+ 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 (g('option').riddleRadio) {
- const bar = gE('body').appendChild(cE('div'));
- bar.className = 'answerBar';
- answers.forEach((answer) => {
- const button = bar.appendChild(cE('div'));
- button.value = answer;
- button.onclick = function () {
- riddleSubmit(this.value);
- };
+ }
+
+ // 战斗外//
+ function quickSite() { // 快捷站点
+ const quickSiteBar = gE('body').appendChild(cE('div'));
+ quickSiteBar.className = 'quickSiteBar';
+ quickSiteBar.innerHTML = '<< 贴吧 Forums ';
+ getOption().quickSite?.forEach((site) => {
+ quickSiteBar.innerHTML = `${quickSiteBar.innerHTML}${(site.fav) ? ` ` : ''}${site.name} `;
});
+ gE('.quickSiteBarToggle', quickSiteBar).onclick = function () {
+ const spans = gE('span', 'all', quickSiteBar);
+ for (const i of range(spans)) {
+ spans[i].style.display = (this.textContent === '<<') ? 'none' : 'block';
+ }
+ this.textContent = (this.textContent === '<<') ? '>>' : '<<';
+ };
}
- const checkTime = function () {
- let time;
- if (typeof g('time') === 'undefined') {
- const timeDiv = gE('#riddlecounter>div>div', 'all');
- if (timeDiv.length === 0) {
+
+ async function autoSwitchIsekai() {
+ const option = getOption();
+ await pauseAsync(option.isekaiTime * _1s - (time(0) - g().idleStart));
+ await waitPause();
+ $async.logSwitch(arguments);
+ if (!option.isekai) return; // 若不启用自动跳转
+ const now = time(0);
+ const remain = (getValue('lastSwitch') ?? 0) * 1 + (option.isekaiCD ?? 0) * _1s - now;
+ await pauseAsync(remain);
+ await waitPause();
+ setValue('lastSwitch', now);
+ $ajax.openNoFetch(`${window.location.href.slice(0, window.location.href.indexOf('.org') + 4)}/${_server.isekai ? '' : 'isekai/'}`);
+ $async.logSwitch(arguments);
+ }
+
+ function switchCurrent(ignoreOption) {
+ [_server.name, _server.other] = [_server.other, _server.name];
+ [_server.isekai, _server.persistent] = [!_server.isekai, !_server.persistent];
+ _server.utils = _server.utils === 'hvut' ? 'hvuti' : 'hvut';
+ if (!ignoreOption) checkOption();
+ }
+
+ function isInBattle(doc) {
+ return gE('#riddlecounter, #battle_main', doc ?? document);
+ }
+
+ async function restorePersonaAndEquipSet() {
+ const persona = getValue('lastPersona');
+ let changed;
+ if (persona) {
+ await $ajax.fetch(queryToPersistent(`?s=Character&ss=ch`), `persona_set=${persona}`);
+ delValue('lastPersona');
+ changed = true;
+ }
+ const equipSet = getValue('lastequipSet');
+ if (equipSet) {
+ await $ajax.fetch(queryToPersistent(`?s=Character&ss=eq`), `equip_set=${equipSet}`);
+ delValue('lastEquipSet');
+ changed = true;
+ }
+ return changed;
+ }
+
+ async function displayCDRemain() { try {
+ const option = getOption();
+ const idleStart = g().idleStart;
+ const next = {
+ arena: idleStart + (option.idleArenaTime ?? 0) * _1s,
+ switch: idleStart + (option.isekaiTime ?? 0) * _1s,
+ switchCD: (getValue('lastSwitch') ?? 0) + (option.isekaiCD ?? 0) * _1s,
+ };
+ await until(() => {
+ if (gE('#hvAABox').style.display === 'none') return;
+ const now = time(0);
+ const remain = Object.fromEntries(Object.entries(next).map(([k, v]) => {
+ const r = Math.floor(Math.max(0, v - now) / _1s) * _1s;
+ const digits = Math.max(2, r >= _1h ? 3 : 2);
+ return [k, `${UI.l('剩余', '剩餘', 'Remain')}${Math.floor(r / _1s)} (${timeStr(r, digits)})`];
+ }));
+ let done = (() => {
+ if (!option.idleArena) return true;
+ gE('.arenaRemain').innerHTML = remain.arena;
+ })();
+ done &= (() => {
+ if (!option.isekai) return true;
+ gE('.isekaiSwitchRemain').innerHTML = remain.switch;
+ gE('.isekaiCDRemain').innerHTML = remain.switchCD;
+ })();
+ return done;
+ }, _1s);
+ } catch (err) { console.error(err); }}
+
+ async function asyncOnIdle() { try {
+ const idleStart = g('idleStart', time(0));
+ displayCDRemain();
+ $async.logSwitch(arguments);
+ await updateEncounter(false);
+ await waitPause();
+ displayProcess(UI.byLang('人物套装检查', '人物套裝檢查', 'Persona/EquipSet check'));
+ if (onIsekaiEncounter) {
+ const persistent = await $ajax.fetch(window.location.href.replace('/isekai', ''));
+ if (!persistent || isInBattle($doc(persistent))) {
+ $async.logSwitch(arguments);
+ displayProcess();
return;
}
- time = '';
- for (let j = 0; j < timeDiv.length; j++) {
- time = (timeDiv[j].style.backgroundPosition.match(/(\d+)px$/)[1] / 12).toString() + time;
- }
- g('time', time * 1);
+ switchCurrent();
} 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)]);
+ await restorePersonaAndEquipSet();
+ }
+ const option = getOption(true);
+ const steps = [
+ [{ step: 'proficiency', method: asyncSetProficiency, condition: true }],
+ [{ step: 'ability', method: asyncSetAbilityData, condition: true }],
+ [{ step: 'stamina', method: asyncSetStamina, condition: true }],
+ [{ step: 'item', method: asyncGetItems, condition: option.restoreStamina },
+ { step: 'supply', method: checkSupply, condition: option.encounterSupply, check: true }],
+ [{ step: 'repair', method: asyncCheckRepair, condition: option.encounterRepair, check: true }],
+ [{ step: 'storage', method: asyncCheckEquStorage, condition: option.encounterEquStorage, check: true }],
+ ];
+
+ const ready = { isChecked: () => ready.encounter && !steps.find(group => group.find(step => step.check && !ready[step.step]))};
+ let battleStarted;
+ if (_server.isekai) {
+ displayProcess(UI.byLang('异世界遭遇', '異世界遭遇', 'Isekai encounter'));
+ await setReady('encounter');
+ if (!ready.encounter) {
+ $async.logSwitch(arguments);
+ displayProcess();
+ return;
+ }
}
- };
- for (let i = 0; i < 30; i++) {
- setTimeout(checkTime, i * _1s);
- }
+ let done = -1;
+ displayProcess(`[${++done}/${steps.length}] ${UI.byLang('检查闲置', '檢查閒置', 'Idle check')}`);
+ await Promise.all([...steps.map(group => (async () => { try {
+ for (const step of group) {
+ await setReady(step.step, await step.method() || !step.check);
+ }
+ displayProcess(`[${++done}/${steps.length}] ${UI.byLang('检查闲置', '檢查閒置', 'Idle check')}`);
+ } catch (err) { console.error(err); }})()), onIsekaiEncounter ? undefined : updateArena()]);
- 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));
+ if (!onIsekaiEncounter) {
+ if (ready.isChecked() && option.idleArena && option.idleArenaValue) {
+ displayProcess(UI.byLang('竞技场更新', '競技場更新', 'Arena update'));
+ battleStarted = await startUpdateArena(idleStart);
+ }
+ if (!battleStarted) {
+ displayProcess(UI.byLang('切换世界', '切換世界', 'World Switching'));
+ autoSwitchIsekai();
+ }
}
- }
- }
+ displayProcess();
+ $async.logSwitch(arguments);
+ return ready.encounter;
- // 战斗外//
- function 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 displayProcess(info) {
+ document.title = document.title.replace(/\[AR\].*\[AR\]/, '');
+ if (!info) return;
+ document.title = `[AR]${info}[AR]` + document.title;
}
- 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);
+
+ async function setReady(step, value) { try {
+ ready[step] = value;
+ if (ready.encounterUpdated) return;
+ $async.logSwitch(arguments);
+ const onEncounter = option.encounter || onIsekaiEncounter;
+ if (_server.persistent) {
+ if (onEncounter && steps.find(group => group.find(step => step.condition && !ready[step.step]))) return;
+ ready.encounterUpdated = true;
}
- for (let e of encounter) {
- if (e.encountered) {
- continue;
+ const encounterStarted = await updateEncounter(onEncounter);
+ battleStarted ||= encounterStarted;
+ ready.encounter ||= !encounterStarted;
+ ready.encounterUpdated ||= ready.encounter;
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err); }}
+ } catch (err) { console.error(err); }}
+
+ function getTodayEncounter(encounter) { return encounter.filter(e => time(2, e.time) === time(2)); }
+
+ function getLocalEncounter(encounter = []) {
+ encounter = [ ... getValue('encounter', true) ?? [], ... getLocal('encounter', true, true) ?? [], ...encounter ];
+ const uniqued = [];
+ while (encounter.length) {
+ const item = encounter.pop();
+ const existed = uniqued.find(x => x.time === item.time || x.url === item.url);
+ if (existed) {
+ if (existed.encountered !== item.encountered) {
+ existed.encountered ||= item.encountered;
}
- url = e.href;
- break;
+ continue;
}
+ uniqued.unshift(item);
}
+ return getTodayEncounter(uniqued);
}
- if (!url) {
- if (isEngage && !getValue('battle')) {
- // 自动跳转,同时先刷新遭遇时间,延长下一次遭遇
- $ajax.openNoFetch(getValue('lastHref'));
- }
- return;
+ function setEncounter(encounter) {
+ encounter = getLocalEncounter(encounter);
+ setLocal('encounter', encounter, true);
+ return g('encounter', setValue('encounter', encounter));
}
- // 减少因在恒定世界处于战斗中时打开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 getEncounter() {
+ const current = g().encounter ?? [];
+ let last = 0;
+ current.forEach(e=> {
+ if (e.encountered > last) last = e.encountered;
+ if (e.time > last) last = e.time;
+ });
+ let encounter = ((time(0) - last) >= (_1h * 0.5)) ? getLocal('encounter', true, true) : current;
+ if (!encounter || !last) {
+ encounter = getValue('encounter', true) ?? [];
+ setEncounter(encounter);
+ }
+ if (JSON.stringify(current) === JSON.stringify(encounter)) {
+ return getTodayEncounter(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 + 1;
+ encounter = [];
+ for (const i of range(times)) {
+ encounter.unshift({ url: i === 0 ? undefined : i, time: last, encountered: i === 0 ? undefined : time(0) });
+ }
+ setEncounter(encounter);
+ }
+ for (let e of encounter) {
+ const key = e.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 getTodayEncounter(Object.values(dict)).sortBy(x => -x.time);
}
- }
- 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.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;
+ function queryToPersistent(query) {
+ return onIsekaiEncounter ? `${window.location.href.includes('https') ? 'https://' : 'http://'}${window.location.href.includes('alt') ? 'alt.' : ''}hentaiverse.org/${query}` : query;
}
- 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(queryToPersistent('?s=Character')));
+ const proficiency = {};
+ gE('#stats_scrollable table:last-child tr', 'all', doc).forEach((tr) => {
+ const exec = tr.innerHTML.match(/(.*)<\/td>.* (.*)<\/td>/);
+ proficiency[exec[2]] = exec[1] * 1;
});
- }
- 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-${_server.name}_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(queryToPersistent('?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 (_server.isekai || !g().option?.restoreStamina) {
+ return perk;
+ }
+ let currentID, html;
+ if (perk && perk[currentID = getCurrentUser() * 1]) {
+ return perk;
+ }
+ if (!(html = await $ajax.insert('https://e-hentai.org/hathperks.php'))) {
+ return perk;
+ }
+ const doc = $doc(html);
+ const perks = gE('.stuffbox>table>tbody>tr', 'all', doc);
+ if (perks && perks[25]?.innerHTML.includes('Obtained') && !(perk ??= []).includes(currentID)) {
+ perk.push(currentID);
+ }
+ return perk;
+ } catch (err) { console.error(err); }})()
+ ]);
+ 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 = getOption(true);
+ if (!option.checkSupply && (_server.isekai || !option.restoreStamina)) {
+ return;
}
- const html = await $ajax.fetch(href);
+ await waitPause();
+ $async.logSwitch(arguments);
+ const html = await $ajax.insert(queryToPersistent('?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),
- }
- });
- } 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];
- }
- 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() { 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) {
+ if (isInBattle(doc)) {
+ $async.logSwitch(arguments);
+ g('items', null);
return;
}
- return gE('.messagebox_error', $doc(await $ajax.fetch(`?s=Forge&ss=re`, `select_item=${id}`)))?.innerText ? undefined : id;
- } catch (e) {console.error(e)}}))).filter(e => e);
- 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;
- }
- }
-
- 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)}}
+ 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);
+ g('slotItems', Array.from(gE('[id*="item_"]', 'all', gE('#item_slots', doc))).map(slot => slot.id.match(/item_(\d+)/)[1]));
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err); }}
+
+ function popupFailedCheck(title, popupText, ...log) {
+ const option = getOption();
+ if (title) document.title = `[${title}!${onIsekaiEncounter?'p':''}]` + document.title;
+ if (popupText) popup(`${onIsekaiEncounter ? { 0: '主世界', 1: '主世界', 2: '[Persistent]' }[option.lang] ?? '[Persistent]' : ''}${popupText[option.lang] ?? popupText[2]}`);
+ if (log?.length) console.log(`${onIsekaiEncounter ? '[Persistent]' : ''}`, ...log);
+ }
+
+ function checkSupply(standalone) {
+ const option = getOption(true);
+ standalone = {
+ GF: {
+ name: { 0: '压榨界', 1: '壓榨界', 2: 'Grindfest' },
+ thresholdList: option.checkItemGF,
+ checkList: option.isCheckGF,
+ percentage: option.checkSupplyWarnGF
+ },
+ IW:{
+ name: { 0: '道具界', 1: '道具界', 2: 'Itemworld' },
+ thresholdList: option.checkItemIW,
+ checkList: option.isCheckIW,
+ percentage: option.checkSupplyWarnIW
+ },
+ }[standalone]
+ if (!option.checkSupply) return true;
+ const items = g().items;
+ if (!items) return false;
+ const slotItems = g().slotItems;
+ const slotedCheckList = option.checkSupplySlotted ? option.isCheckSlotted : undefined;
+ const name = standalone?.name ?? '';
+ const thresholdList = standalone?.checkItem ?? option.checkItem;
+ const checkList = standalone?.isCheck ?? option.isCheck;
+ const percentage = standalone?.checkSupplyWarn ?? option.checkSupplyWarn;
+ const unslotted = [], needs = [], warns = [];
+ const lang = option.lang;
+ for (let id in slotedCheckList) {
+ let [name, count] = items[id] ?? [];
+ if (!slotItems.includes(id)) unslotted.push(`\n${itemMap[id][lang] ?? name}`);
+ }
+ for (let id in checkList) {
+ let [name, count] = items[id] ?? [];
+ name = itemMap[id][lang] ?? name;
+ count ??= 0;
+ const threshold = thresholdList[id] ?? 0;
+ if (count < threshold) {
+ needs.push(`\n${name}(${count}<${threshold})`);
+ continue;
+ }
+ const warnThreshold = threshold * percentage / 100;
+ if (count < warnThreshold) {
+ warns.push(`\n${name}(${count}<${warnThreshold}(${threshold}*${percentage}%))`);
+ }
+ }
- 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');
- }
+ if (unslotted.length) {
+ popupFailedCheck(`C`, {
+ 0: `消耗品未装备:\n${unslotted}`,
+ 1: `消耗品未裝備:\n${unslotted}`,
+ 2: `Consumables not slotted:\n${unslotted}`,
+ }, `Unslotted items:${unslotted}`);
+ } else if (needs.length) {
+ popupFailedCheck(`C${standalone ? '!' : ''}`, {
+ 0: `消耗品${standalone ? `(${standalone.name[option.lang]}独立配置)` : ''}不足:\n${needs}`,
+ 1: `消耗品${standalone ? `(${standalone.name[option.lang]}獨立配置)` : ''}不足:\n${needs}`,
+ 2: `Failed supply check${standalone ? ` for ${standalone.name[option.lang]} standalone` : ''}:\n${needs}`,
+ }, `${standalone ? `${standalone.name[2]} ` : ''}Needs supply:${needs}`);
+ } else if (warns.length) {
+ popupFailedCheck(`C${standalone ? '!' : ''}`, {
+ 0: `消耗品${standalone ? `(${standalone.name[option.lang]}独立配置)` : ''} < ${percentage}%:\n${warns}`,
+ 1: `消耗品${standalone ? `(${standalone.name[option.lang]}獨立配置)`: ''} < ${percentage}%:\n${warns}`,
+ 2: `Supplys ${standalone ? ` for ${standalone.name[option.lang]} standalone` : ''} < ${percentage}%:\n${warns}`,
+ }, `${standalone ? `${standalone.name[2]} ` : ''}Warn supply:${warns}`);
+ }
+ return !needs.length && !unslotted.length;
+ }
+
+ async function asyncCheckRepair(standalone) { try {
+ const option = getOption(true);
+ if (!option.repair) {
+ return true;
+ }
+ await waitPause();
+ $async.logSwitch(arguments);
+ let eqps;
+ standalone = {
+ GF: {
+ name: { 0: '压榨界', 1: '壓榨界', 2: 'Grindfest' },
+ threshold: option.repairValueGF,
+ repairCharm: option.repairCharmGF,
+ },
+ IW:{
+ name: { 0: '道具界', 1: '道具界', 2: 'Itemworld' },
+ threshold: option.repairValueIW,
+ repairCharm: option.repairCharmIW,
+ },
+ }[standalone];
- 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 threshold = standalone?.repairValue ?? option.repairValue;
+ const repairCharm = standalone?.repairCharm || option.repairCharm;
+ if (threshold === undefined || threshold < 0) { // skip because default repair has been checked before idleArena>GF
+ $async.logSwitch(arguments);
+ return true;
+ }
+ const url = queryToPersistent(`?s=Bazaar&ss=am&screen=repair&filter=equipped`);
+ let [doc, equiped] = Array.from(await Promise.all([$ajax.insert(url), $ajax.insert(queryToPersistent(`?s=Character&ss=eq`))])).map($doc);
+ if (isInBattle(doc)) {
+ $async.logSwitch(arguments);
+ return undefined;
+ }
+ const lang = option.lang;
+ const slotMap = {
+ 1: ['主手', '主手', 'Main Hand'],
+ 2: ['副手', '副手', 'Off Hand'],
+ 13: ['头盔', '頭盔', 'Helmet'],
+ 11: ['身体', '身體', 'Body'],
+ 14: ['手部', '手部', 'Hands'],
+ 12: ['腿部', '腿部', 'Legs'],
+ 15: ['脚部', '腳部', 'Feet']
+ }
+ let emptySlot = Array.from(gE('.eqb:not(.eqdisabled)', 'all', equiped)).map(slot => { return { id: slot.getAttribute('onclick').match(/equip_slot=(\d+)/)[1], empty: gE('.eqempty', slot) }; });
+ emptySlot = emptySlot.filter(slot => slot.empty && !option.equipCheckSkip?.[slot.id]).map(slot => UI.byLang(slotMap[slot.id * 1]));
+ const token = gE('#equipform>input[name="postoken"]', doc).value;
+ const getCondition = eqp => 1 * gE('td:last-child', eqp).textContent.replace('%', '');
+ let [material, materialNames] = doc.body.innerHTML.match(/const eqitems=(\{.*\});/)[1].split(/; const itemdata=/).map(JSON.parse);
+ equiped = Array.from(gE('[onmouseover*="equips.set("]', 'all', equiped)).map(eq => eq.getAttribute('onmouseover').match(/equips.set\((\d+),/)[1]);
+ const selector = '#equiplist>table>tbody>tr:not(.eqselall):not(.eqtplabel)';
+ eqps = await Promise.all(Array.from(gE(selector, 'all', doc)).map(async eqp => { try {
+ const id = gE('input', eqp).value;
+ if (!equiped.includes(id)) return;
+ let condition = getCondition(eqp);
+ const needRepairCharm = repairCharm && Object.keys(material[id].m).some(m => materialNames[m].n.includes('Charm'));
+ if (condition > threshold && !needRepairCharm) return;
+ let after = $doc(await $ajax.insert(url, `&eqids[]=${id}&postoken=${token}&replace_charms=on`));
+ let input = gE(`#e${id}`, after);
+ if (input && !needRepairCharm) {
+ condition = getCondition(input.closest(selector));
+ if (condition > threshold) return;
+ after = $doc(await $ajax.insert(url, `&eqids[]=${id}&postoken=${token}&replace_charms=off`));
+ }
+ return gE(`#e${id}`, after) ? gE('.lc', eqp).textContent : undefined;
+ } catch (err) { console.error(err); }}));
+ eqps = eqps.filter(e => e);
+ if (emptySlot.length) {
+ popupFailedCheck(`R`, {
+ 0: `缺少装备:\n${emptySlot.join('\n ')}`,
+ 1: `缺少裝備:\n${emptySlot.join('\n ')}`,
+ 2: `Empty equip slots:\n${emptySlot.join('\n ')}`,
+ }, `Empty equip slots:\n`, emptySlot.join('\n '));
+ }
+ if (eqps.length) {
+ popupFailedCheck(`R`, {
+ 0: `${standalone?.name?.[option.lang] ?? ''}装备需要修理:\n${eqps.join('\n ')}`,
+ 1: `${standalone?.name?.[option.lang] ?? ''}裝備需要修理:\n${eqps.join('\n ')}`,
+ 2: `${standalone?.name?.[option.lang] ?? ''}Equips need repair:\n${eqps.join('\n ')}`,
+ }, `${standalone?.name?.[option.lang] ?? ''}Equips need repair:\n`, eqps.join('\n '));
+ }
+ $async.logSwitch(arguments);
+ return !eqps.length;
+ } catch (err) { console.error(err); }; return false; }
+
+ async function asyncUpdateEquipModifyList() { try {
+ $async.logSwitch(arguments);
+ const option = getOption();
+ const filters = ['weapon_1handed', 'weapon_2handed', 'weapon_staff', 'shield', 'armor_cloth', 'armor_light', 'armor_heavy'];
+ const equips = [];
+ const hvv = hvVersion.upto(Version('091.e'))
+ for (const filter of filters) {
+ const url = queryToPersistent(`?s=Bazaar&ss=am&screen=modify&filter=${filter}`);
+ const doc = $doc(await $ajax.insert(url));
+ if (isInBattle(doc)) return;
+ const eqps = await Promise.all(Array.from(gE('#equiplist>table>tbody>tr:not(.eqselall):not(.eqtplabel)', 'all', doc)).map(async eqp => { try {
+ const id = gE('input', eqp).value;
+ const levels = gE('td:last-child', eqp).innerHTML;
+ if (!levels) return;
+ const [level, world, max] = levels.split(' / ').map(x => x * 1);
+ const name = gE('td:first-child', eqp).innerText;
+ const quality = name.match(/Fair|Average|Superior|Exquisite|Magnificent|Legendary|Peerless/)[0];
+ const rounds = hvv ? range(5, 35) :
+ (()=> { switch (quality) {
+ case 'Fair': case 'Average': case 'Superior': case 'Exquisite':
+ return [...range(5, 10), ...repeat(10, 5)];
+ case 'Magnificent': case 'Legendary': case 'Peerless':
+ return [...range(5, 20), ...repeat(20, 5), ...repeat(25, 5), ...repeat(30, 5)];
+ default:
+ return;
+ }})();
+ if (!rounds) return;
+ equips.push({name, id, filter, level, world, max, round: rounds[world]});
+ } catch (err) { console.error(err); }}));
+ }
+ $async.logSwitch(arguments);
+ return equips;
+ } catch (err) { console.error(err); }}
+
+ async function asyncUpdatePersona(doc) { try {
+ $async.logSwitch(arguments);
+ doc ??= $doc(await $ajax.insert('?s=Character&ss=ch'));
+ if (isInBattle(doc)) return;
+ const raws = gE('[name="persona_set"]>option', 'all', doc);
+ if (!raws?.length) return;
+ const personas = Object.fromEntries([...raws].map(option => [option.value, { name: option.innerHTML, selected: option.selected }]));
+ $async.logSwitch(arguments);
+ return personas;
+ } catch (err) { console.error(err); }}
+
+ async function asyncUpdateEquipSet(doc) { try {
+ $async.logSwitch(arguments);
+ doc ??= $doc(await $ajax.insert('?s=Character&ss=eq'));
+ if (isInBattle(doc)) return;
+ const raws = gE('#eqsl img', 'all', doc);
+ if (!raws?.length) return;
+ const equipSets = Object.fromEntries([...raws].map(img => {
+ const [match, id, on] = img.src.match(/set(\d)_(on|off)/);
+ return [ id, on==='on' ];
+ }));
+ $async.logSwitch(arguments);
+ return equipSets;
+ } catch (err) { console.error(err); }}
+
+ async function asyncCheckEquStorage() { try {
+ const option = getOption(true);
+ if (!option.equStorage) {
+ return true;
+ }
+ await waitPause();
+ $async.logSwitch(arguments);
+ const url = queryToPersistent(`?s=Bazaar&ss=am`);
+ const doc = $doc(await $ajax.insert(url));
+ if (isInBattle(doc)) {
+ $async.logSwitch(arguments);
+ return false;
+ }
+ const exec = /Inventory Capacity:<\/td> (\d+)(?: \+ (\d+))?<\/td> \/<\/td> (\d+)<\/td>/.exec(doc.body.innerHTML);
+ const count = parseInt(exec[1]); + parseInt(exec[2] || 0);
+ const checked = count <= option.equStorageValue;
+ if (!checked) {
+ popupFailedCheck(`E`, {
+ 0: `装备库存过多: ${count} / ${option.equStorageValue}`,
+ 1: `裝備庫存過多: ${count} / ${option.equStorageValue}`,
+ 2: `Equips storage upto threshold: ${count} / ${option.equStorageValue}`,
+ }, `Equips storage upto threshold: ${count} / ${option.equStorageValue}`);
+ }
+ $async.logSwitch(arguments);
+ return checked;
+ } catch (err) { console.error(err); }; return false; }
+
+ async function checkBattleReady(method, condition = {}) {
+ await waitPause();
+ if (condition.checkEncounter) {
+ const encounter = getEncounter();
+ if (encounter[0]?.url && !encounter[0]?.encountered) {
+ console.log(getEncounter());
return;
}
- gE('img[src*="startchallenge.png"]', 'all', doc).forEach((_) => {
- const temp = _.getAttribute('onclick').match(/init_battle\((\d+),\d+,'(.*?)'\)/);
- arena.token[temp[1]] = temp[2];
+ }
+ const option = getOption(true);
+ const stamina = getValue('stamina', true);
+ const [low, lowNR, cost, ratio] = [condition.staminaLow ?? option.staminaLow, option.staminaLowWithReNat ?? 0, Math.round((condition.staminaCost ?? 0) * 100) / 100, stamina.punish ? stamina.ratio ?? 1 : 1]
+ const checked = await checkStamina(low, cost);
+ const [staminaChecked, stmNR] = [checked.checked, checked.stmNR];
+ const [neat, neatNR] = [stamina.current-low, stmNR-lowNR];
+ console.log(
+ `${onIsekaiEncounter ? '[Persistent]' : ''}stamina check succeed:`, staminaChecked === 1, ...staminaChecked === -1 ? ['with nature recover', lowNR, 'stmNR:', stmNR, '(', ...neatNR >= 0 ? ['+', neatNR] : ['-', -neatNR], ')'] : [],
+ '\nlow:', low, ...cost ? ['cost:', cost, ...stamina.punish ? ['*', ratio, '=', Math.round(cost * ratio * 10000) / 10000] : [], 'current:', stamina.current, '(', neat >= 0 ? '+' : '-', neat, ')'] : [],
+ '\nstamina:', stamina,
+ );
+ if (staminaChecked === 1) { // succeed
+ document.title = document.title.replace(`[S!${onIsekaiEncounter?'p':''}]`, '');
+ return true;
+ }
+ if (staminaChecked === 0) { // failed currently
+ const now = time(0);
+ setTimeout(method, Math.floor(now / _1h + 1) * _1h - now);
+ // popup('Failed stamina check for now.');
+ if (!document.title.includes(`[S!${onIsekaiEncounter?'p':''}]`)) {
+ document.title = `[S!${onIsekaiEncounter?'p':''}]` + document.title;
+ }
+ } else { // case -1: // failed with nature recover
+ popupFailedCheck(`S!`, {
+ 0: `当日精力不足(含自然恢复)`,
+ 1: `當日精力不足(含自然恢復)`,
+ 2: `Failed stamina check with nature recover.`,
});
- } 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
- return true;
- }
- if(staminaChecked === 0){ // failed until today ends
- setTimeout(method, Math.floor(time(0) / _1h + 1) * _1h - time(0));
- document.title = `[S!!]` + document.title;
- } else { // case -1: // failed with nature recover
- document.title = `[S!]` + document.title;
+ async function getCurrentStamina() { try {
+ // await waitPause();
+ $async.logSwitch(arguments);
+ const doc = $doc(await $ajax.insert(onIsekaiEncounter ? window.location.href.replace(/\/isekai/, '') : window.location.href));
+ if (isInBattle(doc)) {
+ $async.logSwitch(arguments);
+ return [ undefined, undefined ];
+ }
+ const current = gE('#stamina_readout .fc4.far>div', doc).textContent.match(/\d+/)[0] * 1;
+ const punish = !!gE('#stamina_readout .fc4.far', doc).parentNode.title;
+ $async.logSwitch(arguments);
+ return [current, punish ? punish : undefined];
+ } catch (err) { console.error(err); }}
+
+ async function checkStamina(low, cost) {
+ // await waitPause();
+ $async.logSwitch(arguments);
+ const stamina = getValue('stamina', true);
+ const option = getOption(true);
+ let now = time(0);
+ let hours = Math.floor(now / _1h);
+ let [current, punish] = await until(getCurrentStamina, _1m);
+ const stmNR = current + 24 - (hours % 24);
+ cost ??= 0;
+ if (punish && option.staminaRatio) {
+ cost *= stamina.ratio
+ }
+ const stmNRChecked = !cost || stmNR - cost >= option.staminaLowWithReNat;
+ const result = { checked: stmNRChecked ? (current - cost >= (low ?? option.staminaLow)) ? 1 : 0 : -1, stmNR: stmNR };
+ $async.logSwitch(arguments);
+ if (result.checked === 1 || _server.isekai || !option.restoreStamina) return result;
+ const items = g().items;
+ $async.logSwitch(arguments);
+ if (!items) return result;
+ const recoverItems = { 11401: true, 11402: false }
+ const isPerk = stamina.perk?.includes(getCurrentUser());
+ for (let id in recoverItems) {
+ if (!items[id]) continue;
+ const recover = recoverItems[id] ? isPerk ? 20 : 10 : 5;
+ if (current + recover >= 100) continue; // check if overflow by (20 or 10) -> (5)
+ const recovered = gE('#stamina_readout .fc4.far>div', $doc(await $ajax.insert(window.location.href, 'recover=stamina'))).textContent.match(/\d+/)[0] * 1;
+ goto();
+ break;
+ }
+ $async.logSwitch(arguments);
+ return result;
}
- }
- 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 MAX = 24;
+ const option = getOption(true);
+ if (!option.encounter && !option.encounterDisplay) {
+ console.log("skip encounter check");
+ return false;
}
- if (id in arena.token) {
- break;
+ $async.logSwitch(arguments);
+ const encounter = getEncounter();
+ const count = encounter.filter(e => e.url).length;
+ const last = encounter[0]?.time ?? getValue('lastEH', true) ?? 0; // 上次遭遇 或 上次打开EH 或 0
+ let now = time(0);
+ let cd = getCD();
+ 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 * _1s;
+ const missed = count - encounter.filter(e => e.encountered && e.url).length;
+ if (count === MAX) {
+ ui.style.cssText += 'color:orange!important;';
+ } else if (cd <= waitCD) {
+ ui.style.cssText += 'color:red!important;';
+ } else {
+ ui.style.cssText += 'color:unset!important;';
}
- if (id >= 105) {
- arena.token = (await updateArena(true)).token;
- if (id in arena.token) {
- break;
+ let uiTime = timeStr(cd, 2, option.encounterQuickCheck);
+ if (option.encounterQuickCheck && cd >= 30 * _1m) {
+ uiTime = (Math.floor(cd / _1s) % 2) ? uiTime : uiTime.replace(':', '.');
+ }
+ ui.innerHTML = `${uiTime}[${encounter.length ? (count >= MAX ? `☯` : count) : `✪`}${missed ? `-${missed}` : ``}]`;
+ if (document.title.includes(titlePause())) {
+ document.title = ui.innerHTML + titlePause();
+ }
+ if (engage && !getValue('disabled')) {
+ if (cd <= 0) {
+ $async.logSwitch(arguments);
+ return await onEncounter();
+ }
+ if (cd < 30 * _1m && encounter[0]?.url && !encounter[0].encountered) {
+ $ajax.openNoFetch(encounter[0].url);
+ $async.logSwitch(arguments);
+ return true;
+ }
+ }
+ let interval = (!option.encounterQuickCheck && cd > _1h) ? _1m : (!option.encounterQuickCheck || cd > _1m) ? _1s : 80;
+ interval = (option.encounterQuickCheck && cd > _1m) ? (interval - cd % interval) / 4 : interval; // 让倒计时显示更平滑
+ setTimeout(() => updateEncounter(engage), interval);
+ $async.logSwitch(arguments);
+ return engage && cd <= waitCD;
+
+ function getCD() {
+ now = time(0);
+ let cd;
+ if (encounter.filter(e => e.url && (e.encountered || (time(0) - e.time >= 30 * _1m))).length >= MAX) {
+ cd = Math.floor(encounter[0].time / _1d + 1) * _1d - now;
+ } else if (!last) {
+ cd = 0;
+ } else {
+ cd = _1h / 2 + last - now;
}
+ return Math.max(0, cd);
+ }
+ } catch (err) { console.error(err); }}
+
+ async function onChangeEquipSet(lastKey, target, list, toFetchParam, reload) { try {
+ let changed = false;
+ let last = getValue(lastKey);
+ let current = list ? Object.keys(list).find(p => list[p].selected) * 1 : undefined;
+ target ??= last;
+ if ([undefined, current].includes(target)) return;
+ if (!last) setValue(lastKey, current);
+ await $ajax.fetch(...toFetchParam(target));
+ if (reload) await updateItemWorldList(true);
+ return true;
+ } catch (err) { console.error(err); }}
+
+ async function switchEquipSet(persona, equipSet, personas, equipSets) { try {
+ $async.logSwitch(arguments);
+ if (personas !== undefined) g('personas', personas);
+ personas = g().personas;
+ if (equipSets !== undefined) g('equipSets', equipSets);
+ equipSets = g().equipSets;
+ if ([personas, equipSets].includes(undefined)) {
+ const { e, p, s } = getValue('itemWorldDatas', true) ?? {};
+ g('personas', personas ??= p);
+ g('equipSets', equipSets ??= s);
+ }
+
+ let changed = await onChangeEquipSet('lastPersona', persona, personas, id => [`?s=Character&ss=ch`, `persona_set=${id}`], true);
+ changed = await onChangeEquipSet('lastEquipSet', equipSet, equipSets, id => [`?s=Character&ss=eq`, `equip_set=${id}`], true) || changed;
+ $async.logSwitch(arguments);
+ return changed;
+ } catch (err) { console.error(err); }}
+
+ function getArenaEquipSet(id) {
+ const option = getOption();
+ if (!option.changeEquipSet) return;
+
+ const onGetArenaEquipSet = function (id) {
+ if (!option.enableEquipSet?.[id]) return;
+ const persona = option.switchPersona?.[id];
+ const equipSet = option.switchEquipSet?.[id];
+ return { persona, equipSet };
+ }
+
+ let target = onGetArenaEquipSet(id);
+ let parent = !isNaN(+id) ? onGetArenaEquipSet(id >= 105 ? 'rb' : 'ar') : undefined;
+ let defaultSet = onGetArenaEquipSet('default');
+ let persona = target?.persona !== undefined ? target?.persona : parent?.persona !== undefined ? parent?.persona : defaultSet?.persona !== undefined ? defaultSet?.persona : undefined;
+ let equipSet = target?.equipSet !== undefined ? target?.equipSet : parent?.equipSet !== undefined ? parent?.equipSet : defaultSet?.equipSet !== undefined ? defaultSet?.equipSet : undefined;
+ persona = persona === -1 ? undefined : persona;
+ equipSet = equipSet === -1 ? undefined : equipSet;
+ if (persona !== undefined || equipSet !== undefined) return { persona, equipSet };
+ }
+
+ function isSwitchEquipSet(target) {
+ const last = { persona: getValue('lastPersona'), equipSet: getValue('lastEquipSet') };
+ const { persona, equipSet } = target ?? {};
+ return {
+ persona: persona !== undefined && (last.persona !== persona) ? persona : undefined,
+ equipSet: equipSet !== undefined && (last.equipSet !== equipSet) ? equipSet : undefined
+ }
+ }
+
+ async function changeArenaEquipSet(id) { try {
+ const target = getArenaEquipSet(id);
+ const { persona, equipSet} = isSwitchEquipSet(target);
+ console.log(`changeArenaEquipSet for arena [${id}]:`, target, '=>', { persona, equipSet});
+ return await ([persona, equipSet].every(c => c === undefined) ? restorePersonaAndEquipSet() : switchEquipSet(persona, equipSet));
+ } catch (err) { console.error(err); }}
+
+ async function onEncounter() { try {
+ const option = getOption(true);
+ if (getValue('disabled')) return;
+ if (_server.isekai) {
+ onIsekaiEncounter = true;
+ const engaged = await asyncOnIdle();
+ onIsekaiEncounter = false;
+ return engaged;
+ }
+ if (await changeArenaEquipSet('ba') && !(await asyncCheckRepair())) {
+ await restorePersonaAndEquipSet();
+ return;
+ }
+ $async.logSwitchStrict('updateEncounter', true);
+ // persistent in battle
+ if (isInBattle(await $ajax.insert(window.location.href.replace(/\/isekai/, '')))) {
+ $async.logSwitchStrict('updateEncounter', false);
+ return;
+ }
+ // url check
+ await until( // perhaps network connect not available
+ async () => await $ajax.insert(window.location.href) && (!option.checkURLBeforeNewRound || await $ajax.insert(option.checkURLBeforeNewRound)),
+ option.checkURLBeforeNewRoundRetry
+ );
+ // stamina
+ if (!await checkBattleReady(onEncounter, { staminaLow: option.staminaEncounter })) {
+ $async.logSwitchStrict('updateEncounter', false);
+ return;
+ }
+ setEncounter(getEncounter()); // 离开页面前保存
+ if (!window.top.location.href.endsWith(`?s=Battle`)) {
+ setValue('beforeEncounter', setValue('lastUrl', window.top.location.href));
+ }
+ $ajax.openNoFetch('https://e-hentai.org/news.php?encounter');
+ $async.logSwitchStrict('updateEncounter', false);
+ return true;
+ } catch (err) { console.error(err); }}
+
+ async function startUpdateArena(idleStart) { try {
+ $async.logSwitchStrict('startUpdateArena', true);
+ const now = time(0);
+ if (!idleStart) await updateArena(); // new day
+ let timeout = getOption().idleArenaTime * _1s;
+ if (idleStart) timeout -= time(0) - idleStart;
+ if (timeout > 0) await pauseAsync(timeout);
+ const started = await idleArena();
+ const last = getValue('arena', true)?.date ?? now;
+ const nextDay = Math.max(0, Math.floor(last / _1d + 1) * _1d - now);
+ if (nextDay > 0) {
+ setTimeout(startUpdateArena, nextDay); // next day
+ } else {
+ started ||= await startUpdateArena();
+ }
+ $async.logSwitchStrict('startUpdateArena', false);
+ return started;
+ } catch (err) { console.error(err); }}
+
+ async function updateArena(forceUpdateToken = false) { try {
+ 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 = getOption();
+ if (!isToday) {
+ arena.date = time(0);
+ arena.gr = option.idleArenaGrTime;
+ arena.arrayDone = [];
+ }
+ if (!isToday || !arena.isOptionUpdated) {
+ arena.array = splitOrders(option.idleArenaValue).map(String);
+ arena.array.reverse();
}
- id = undefined;
+ arena.arrayDone = arena.arrayDone.filter(id => id && (id === 'gr' || !arena.enabled?.includes(id.toString())));
+ delete arena.equip;
+ $async.logSwitch(arguments);
+ return setValue('arena', arena);
+ } catch (err) { console.error(err); }}
+
+ function titlePause() {
+ return UI.byLang('HVAA暂停中', 'HVAA', 'HVAA Paused');
}
- 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) {
+
+ async function idleArena() { try { // 闲置竞技场
+ let id;
+ let arena = getValue('arena', true);
+ const option = getOption();
+ const writeArenaStart = function (equip) {
+ console.log('Arena Start', equip ? `e${equip.id} (${equip.world} => ${equip.world + 1}) / ${equip.max}\n${JSON.stringify(equip)}` : id);
+ switch (id) {
+ case 'iw':
+ break;
+ case 'gr':
+ arena.gr--;
+ break;
+ default:
+ arena.arrayDone.push(id);
+ }
+ arena.equip = { data: equip };
+ setValue('arena', arena);
+ }
+ if (arena.array.length === 0) {
+ autoSwitchIsekai();
+ return false;
+ }
+ $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 (id === 'iw') {
+ id = undefined;
+ const iw = await idleItemWorld(writeArenaStart, arena);
+ if (iw) {
+ $async.logSwitch(arguments);
+ return true;
+ } else {
+ continue;
+ }
+ }
+ 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;
+ }
+ if (!id) {
+ console.log('No Arena Id Available', arena);
setValue('arena', arena);
- idleArena();
- arena.arrayDone.push('gr');
+ await restorePersonaAndEquipSet();
+ $async.logSwitch(arguments);
+ return false;
+ }
+ if (await changeArenaEquipSet(id) && !(await asyncCheckRepair())) {
+ await restorePersonaAndEquipSet();
+ $async.logSwitch(arguments);
+ return false;
+ }
+ 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] *= (_server.isekai ? 2 : 1) * (stamina.current >= 60 ? 0.03 : 0.02);
+ }
+
+ let query;
+ if (id !== 'gr') {
+ query = id >= 105 ? 'rb' : 'ar';
+ } else {
+ if (arena.gr <= 0) {
+ arena.arrayDone.push('gr');
+ setValue('arena', arena);
+ return await idleArena();
+ }
+ query = 'gr';
+ }
+ query = `?s=Battle&ss=${query}`;
+ if (id === 'gr' && ((option.checkSupplyGF && !checkSupply('GF')) || (option.repairValueGF && !await asyncCheckRepair('GF')))) {
+ console.log('Check gr Battle Ready Failed in supply/repair', 'id:', id, arena);
+ $async.logSwitch(arguments);
+ return false;
+ }
+ 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 false;
+ }
+ 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 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(window.location.href.replace('://hentaiverse.org', '://alt.hentaiverse.org'))) {
+ 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);
+ return true;
+ } catch (err) { console.error(err); }}
+
+ async function idleItemWorld(writeArenaStart, arena) { try {
+ const option = getOption();
+ if (!option.idleItemWorld) return;
+ $async.logSwitch(arguments);
+ await updateItemWorldList();
+ const { equips, personas, equipSets } = getValue('itemWorldDatas', true) ?? {};
+ if (!equips || !personas || !equipSets) {
+ $async.logSwitch(arguments);
return;
}
- 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);
- }
-
- // 战斗战况
- 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]) {
+ const list = option.enableItemWorld ? Object.keys(option.enableItemWorld).filter(id => option.enableItemWorld[id]) : undefined;
+ if (!list?.length) {
+ $async.logSwitch(arguments);
+ return;
+ }
+ list.sortBy(id => option.ItemWorldOrder?.[id] ?? 0);
+
+ let doc, errorMsg;
+ for (const eid of list) {
+ const equip = equips.find(eqp => eqp.id === eid);
+ if (equip.world >= equip.max || equip.world >= option.levelItemWorld?.[eid]) continue;
+ doc = $doc(await $ajax.insert(`?s=Bazaar&ss=am&screen=modify&eqids[]=${eid}`));
+ if (errorMsg = gE('.messagebox_error', doc)) {
+ console.log(equip, errorMsg.innerText);
+ continue;
+ }
+ let title = gE('#equpgrade button', 'all', doc)[1].getAttribute('title');
+ if (title?.match(/You need \d+ more World Seeds to spawn this Item World./)) continue;
+
+ if (switchEquipSet(option.itemWorldPersona?.[eid], option.itemWorldEquipSet?.[eid], personas, equipSets)) {
+ doc = $doc(await $ajax.insert(`?s=Bazaar&ss=am&screen=modify&eqids[]=${eid}`));
+ if (errorMsg = gE('.messagebox_error', doc)) {
+ console.log(equip, errorMsg.innerText);
+ continue;
+ }
+ title = gE('#equpgrade button', 'all', doc)[1].getAttribute('title');
+ }
+
+ if (title?.match(/You cannot enter the item world of a currently equipped item./)) {
+ console.log('Idle Item World: Skip currently equiped', eid, equip);
+ continue;
+ }
+
+ let query = `?s=Battle&ss=iw&filter=${equip.filter}`;
+ const id = 'iw';
+ if (((option.checkSupplyIW && !checkSupply('IW')) || (option.repairValueIW && !await asyncCheckRepair('IW')))) {
+ console.log('Check iw Battle Ready Failed in supply/repair', `id:e${eid}`, equip, arena);
+ continue;
+ }
+
+ let stamina = getValue('stamina', true);
+ [stamina.current, stamina.punish] = await getCurrentStamina();
+ stamina.time = time(0);
+ const cost = equip.round * (_server.isekai ? 2 : 1) * (stamina.current >= 60 ? 0.03 : 0.02);
+ if (!await checkBattleReady(idleArena, { staminaCost: cost, checkEncounter: option.encounter, staminaLow: option.staminaItemWorld })) {
+ console.log('Check Battle Ready Failed', `id:e${eid}`, equip, arena);
+ continue;
+ }
+
+ // switch to itemworld to get correct postoken
+ let token = `postoken=${gE('#equipform>input[name="postoken"]', $doc(await $ajax.insert(query))).value}&eqids%5B%5D=${eid}`;
+ await waitPause();
+ writeArenaStart(equip);
+ await until(async () => !option.checkURLBeforeNewRound || await $ajax.insert(option.checkURLBeforeNewRound), option.checkURLBeforeNewRoundRetry);
+ await until(async () => await $ajax.insert(query, token), option.checkURLBeforeNewRoundRetry);
+ stamina.lastCost = cost;
+ setValue('stamina', stamina);
+ if (option.altBattleFirst && await $ajax.insert(window.location.href.replace('://hentaiverse.org', '://alt.hentaiverse.org'))) {
+ console.log('Arena Fetch Done.', 'altBattleFirst:', option.altBattleFirst, 'Arena goto alt', arena);
+ gotoAlt(true);
+ } else {
+ console.log('Arena Fetch Done.', 'altBattleFirst:', option.altBattleFirst, 'Arena goto', arena);
+ goto();
+ }
+ $async.logSwitch(arguments);
+ return true;
+ }
+ $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.sortBy(x => x.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 = onBattleBox().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],
+ ].map(arr => arr.map(s => isNaN(+s) ? `${s}` : s)),
+ condition: (bt) => bt[3] && bt[3] <= battle.tower,
+ content: (_) => battle.tower,
+ end: `${battle.tower > 40 ? `+${(battle.tower - 40) * 5}%DMG&HP` : ''}
`,
+ }
+ }
+ 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];
+ const arena = getValue('arena', true);
+ const equip = arena?.equip;
+ battle.postoken ??= arena.postoken;
+ if (equip && type !== 'iw') {
+ delete arena.equip;
+ setValue('arena', arena);
+ }
+ switch (type) {
+ case 'ar':
+ case 'rb':
+ 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':
+ title = `${info.title}`;
+ if (equip) {
+ if (Object.keys(equip.tokens ?? {}).map(k => equip.tokens[k] === battle[k]).some(s => !s)) {
+ // 有旧token,移除
+ delete arena.equip;
+ } else {
+ // 没有token或是当前token,正常显示
+ equip.tokens = { token: battle.token, postoken: battle.postoken };
+ title += `${equip.data.world + 1}/${equip.data.max}`;
+ subtype = `[${equip.data.id}]${equip.data.name}
`;
+ }
+ setValue('arena', arena);
+ }
+ break;
+ case 'gr':
+ title = `${info.title}`;
+ break;
+ default:
+ break;
+ }
+ return isTitle ? title : `${(info?.name ?? ['未知', '未知', 'Unknown'])[lang]}:[${title}]${subtype ?? ''}`;
}
- return isTitle ? title : `${(info?.name ?? ['未知', '未知', 'Unknown'])[lang]}:[${title}]${subtype ?? ''}`;
- }
- const currentTurn = (battle.turn ?? 0) + 1;
+ 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
-
- 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 = [
+ `${UI.l('攻击模式', '攻擊模式', 'Attack Mode')}: ${UI.attackStatusType[g().attackStatus]}`,
+ `${(_server.isekai || onIsekaiEncounter) ? UI.l('异世界', '異世界', 'Isekai') : UI.l('恒定世界', '恆定世界', 'Persistent')}`, // 战役模式显示
+ `${getBattleTypeDisplay()}`, // 战役模式显示
+ `R${battle.roundNow}/${battle.roundAll}:T${currentTurn}`,
+ `TPS: ${g().runSpeed}`,
+ `${UI.l('敌人', '敵人', 'Monsters')}: ${g().monsterAlive}/${g().monsterAll}`,
+ ].join(` `).replace(` `, ``);
+ if (!battle.roundAll) {
+ pauseChange();
+ $debug.shiftLog();
+ }
+ const option = getOption();
+ document.title = `${currentTurn % 2 ? option.frequencySign1 ?? '' : option.frequencySign2 ?? ''}${getBattleTypeDisplay(true)}:R${battle.roundNow}/${battle.roundAll}:T${currentTurn}@${g().runSpeed}tps,${g().monsterAlive}/${g().monsterAll}`;
+ setValue('battle', battle);
+ if (!battle.monsterStatus || battle.monsterStatus.length !== g().monsterAll) {
+ fixMonsterStatus();
+ }
+ countMonsterHP();
+ displayMonsterWeight();
+ displayPlayStatePercentage();
+ if (getValue('disabled')) { // 如果禁用
+ document.title = titlePause();
+ const pauseChange = gE('#hvAABox2>button.pauseChange');
+ if (!pauseChange) return;
+ pauseChange.innerHTML = UI.button.continue();
return;
}
- delete taskList[names[i]];
- }
- for (let name in taskList) {
- if (taskList[name]()) {
+ battle = getValue('battle', true);
+ battle.turn = g().battle.turn = currentTurn;;
+ setValue('battle', battle);
+ killBug(); // 解决 HentaiVerse 可能出现的 bug
+
+ if (option.autoFlee && checkCondition(option.fleeCondition)) {
+ if (option.fleeAlarm) setAlarm('Flee');
+ 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 ? [] : splitOrders(option.battleOrderName);
+ 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 (const i of range(names)) {
+ 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 matchBuffImg(buff) {
+ return buff?.getAttribute('onmouseover').match(/\(\s*'(?:[^']*?\(x(\d+)\)[^']*?|[^']*?)'\s*,\s*'.*'\s*,\s*(\d+|'.*?')\s*\)/);
}
- return false;
- }
- function autoDefend() {
- if (g('option').defend && checkCondition(g('option').defendCondition)) {
- gE('#ckey_defend').click();
- return true;
+ function getBuffStackFromImg(buff) {
+ return (matchBuffImg(buff)?.[1] ?? 1) * 1;
}
- 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 ';
+ function getBuffTurnFromImg(buff) {
+ let duration = matchBuffImg(buff)?.[2];
+ switch (true) {
+ case !duration:
+ return 0;
+ case ['autocast', 'permanent', '-'].includes(duration.replaceAll(/'/g, '')):
+ return Infinity;
+ case duration.replaceAll(/'/g, '') === 'decaying':
+ {
+ duration = 0;
+ const decay = 1 - unsafeWindow.battle.set_infopane_effect.toString().match(/Decays by (\d+)% per turn/)[1]/100;
+ let stacks = getBuffStackFromImg(buff);
+ while((stacks = Math.floor(stacks * decay)) > 0) {
+ duration++;
+ }
+ return duration;
+ }
+ default:
+ duration = duration * 1;
+ if (isNaN(duration)) console.warn('NaN duration:', buff, duration);
+ return duration;
}
- setValue('disabled', document.title);
- document.title = _alert(-1, 'hvAutoAttack暂停中', 'hvAutoAttack暫停中', 'hvAutoAttack Paused');
}
- }
-
- 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;
- }
- $ajax.open(getValue('lastHref'));
- }
- function reloader() {
- let obj; let a; let cost;
- const battleUnresponsive = {
- 'Alert': { Method: setAlarm },
- 'Reload': { Method: goto },
- 'Alt': { Method: gotoAlt }
+ function getMonsterID(s) {
+ if (s.order !== undefined) {
+ return (s.order + 1) % 10;
+ } // case is monsterStatus
+ return (s + 1) % 10; // case is order
}
- function clearBattleUnresponsive(){
- Object.keys(battleUnresponsive).forEach(t=>clearTimeout(battleUnresponsive[t].Timeout));
+
+ function getMonster(id) {
+ return gE(`#mkey_${id % 10}`);
}
- 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);
+
+ 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.sortBy(x => x.finWeight);
+ * @param {int} rangeSize radius, 0 for single-target and all-targets, 1 for treble-targets, ..., n for (2n+1) targets
+ * @param {(target) => number} excludeWeightRatio target with id
+ * @returns
+ */
+ function getRangeCenter(target, rangeSize, isWeaponAttack, excludeWeight, forceUseIndex) {
+ let msTemp = JSON.parse(JSON.stringify(g().battle.monsterStatus));
+ msTemp.sortBy(x => x.order);
+ let minWeight = Number.MAX_SAFE_INTEGER;
+ // 0. 范围大于等于全体时,直接释放全体
+ if (!rangeSize || rangeSize >= msTemp.length) {
+ return { id: getMonsterID(target), weight: minWeight };
+ }
+ const option = getOption();
+ const centralExtraWeight = -1 * Math.log10(1 + (isWeaponAttack ? option.centralExtraRatio / 100 : 0));
+ let order = target.order;
+ let newOrder = order;
+ // sort by order to fix id
+ let unreachableWeight = resolveRPNFormula(option.unreachableWeight, target);
+ // 1. 以选中目标为中心,优先向上
+ // 2. 超过顶部则向下找
+ // 3. 死亡、超过底下的将被溢出抛弃
+ const up = Math.floor(rangeSize / 2);
+ const down = rangeSize - up - 1;
+ const top = order < rangeSize ? 0 : Math.max(order - down, 0);
+ const bottom = Math.min(order + up, msTemp.length - 1);
+ for (const center of range(top, bottom + 1)) {
+ if (msTemp[center].isDead) continue;
+ let weight = 0;
+ let overflowed = center + Math.max(up - center, 0);
+ for (const inRange of range(overflowed - up, overflowed + down + 1)) {
+ let cew = inRange === center ? centralExtraWeight : 0; // cew <= 0, 增加未命中权重,降低命中权重
+ let mon = msTemp[inRange];
+ if (inRange < 0 || inRange >= msTemp.length || mon.isDead) { // 超出范围 或 死亡目标
+ weight += unreachableWeight;
+ continue;
+ }
+ if (excludeWeight) {
+ weight += excludeWeight(mon);
+ }
+ weight += cew; // 中心目标会受到副手及冲击攻击时,相当于有效生命值降低
+ weight += forceUseIndex ? -1 : mon.finWeight; // 强制使用顺序而非权重时,全部使用统一的权重而非怪物状态
+ }
+ if (weight < minWeight) {
+ newOrder = center;
+ minWeight = weight;
+ break;
}
}
- if (g('option').recordUsage) {
- obj = {
- mode: a.mode,
+ return { id: getMonsterID(newOrder), weight: minWeight };
+ }
+
+ function autoPause() {
+ const option = getOption();
+ let battle = getValue('battle', true);
+ if (
+ battle.paused?.round !== battle.roundNow
+ || battle.paused?.token !== battle.token
+ || battle.paused?.postoken !== battle.postoken
+ ) {
+ battle.paused = {
+ count: 0,
+ round: battle.roundNow,
+ token: battle.token,
+ postoken: battle.postoken ??= (getValue('arena', true) ?? {}).postoken
};
- 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;
+ setValue('battle', battle);
+ }
+ if (option.autoPause && checkCondition(option.pauseCondition)) {
+ if (!getValue('stepIn')) {
+ battle = getValue('battle', true);
+ battle.paused.turn = battle.turn;
+ battle.paused.count++;
+ setValue('battle', battle);
}
+ pauseChange();
+ if (option.pauseAlarm) setAlarm('Pause');
+ return true;
}
- };
- 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 = getOption();
+ 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;
+ g('battleExit', true);
+ setAlarm(alarm);
+ const option = getOption();
+ if (alarm === 'Defeat' && !option.autoSkipDefeated) {
return;
}
- if (g('option').dropMonitor) {
- dropMonitor(battleLog);
+ delValue(1);
+ setTimeoutOrExecute(() => backFromBattle(), option.ExitBattleWaitTime * _1s);
+ }
+
+ async function checkResponsive() {
+ const option = getOption();
+ const battleUnresponsive = {
+ 'Alert': { method: () => (setAlarm('BattleUnresponsive') || true) },
+ 'Reload': { method: goto },
+ 'Alt': { method: gotoAlt }
}
- if (g('option').recordUsage) {
- recordUsage2();
+ let min = Infinity;
+ for (let t in battleUnresponsive) {
+ if (!option.battleUnresponsive?.[t] || !battleUnresponsive[t]) {
+ delete battleUnresponsive[t];
+ continue;
+ }
+ battleUnresponsive[t].time = Math.max(1, option.battleUnresponsiveTime?.[t] ?? 1) * _1s;
+ min = Math.min(min, battleUnresponsive[t].time);
+ }
+ if (!Object.keys(battleUnresponsive).length) return;
+ let isBreak;
+ while (true) {
+ await waitPause();
+ const waited = new Date() - lastResponsive;
+ for (let t in battleUnresponsive) {
+ if (battleUnresponsive[t].time > waited) continue;
+ isBreak ||= battleUnresponsive[t].method();
+ }
+ if (g().battleExit || isBreak) break;
+ await pauseAsync(min - waited);
}
- if (g('battle').roundNow !== g('battle').roundAll) { // Next Round
- if(g('option').NewRoundWaitTime){
- setTimeout(onNewRound, g('option').NewRoundWaitTime * _1s);
- } else {
- onNewRound();
+ }
+
+ function reloader() {
+ let obj, a, cost;
+ const eventStart = cE('a');
+ eventStart.id = 'eventStart';
+ eventStart.onclick = function () {
+ const option = getOption();
+ a = unsafeWindow.info;
+ if (option.recordUsage) {
+ obj = {
+ mode: a.mode,
+ };
+ if (a.mode === 'items') {
+ obj.itemName = gE(`#pane_item div[id^="ikey"][onclick*="skill('${a.skill}')"]`).textContent;
+ obj.item = gE(`#pane_item div[id^="ikey"][onclick*="skill('${a.skill}')"]`).getAttribute('onmouseover').match(/(\d+)/)[1];
+ } else if (a.mode === 'magic') {
+ obj.magic = a.skill;
+ obj.magicName = gE(a.skill).textContent;
+ cost = gE(a.skill).getAttribute('onmouseover').match(/\('.*', '.*', '.*', (\d+), (\d+), \d+\)/);
+ obj.mp = cost[1] * 1;
+ obj.oc = cost[2] * 1;
+ }
}
- return;
+ };
+ gE('body').appendChild(eventStart);
+
+ const eventEnd = cE('a');
+ eventEnd.id = 'eventEnd';
+ eventEnd.onclick = function () {
+
+ const option = getOption();
+ const timeNow = time(0);
+ g('runSpeed', (_1s / (timeNow - g().timeNow)).toFixed(2));
+ g('timeNow', timeNow);
+ const monsterDead = gE('img[src*="nbardead"]', 'all').length;
+ g('monsterAlive', g().monsterAll - monsterDead);
+ const bossDead = gE(`${monsterStateKeys.obj}[style*="opacity"] ${monsterStateKeys.lv}[style*="background"]`, 'all').length;
+ g('bossAlive', g().bossAll - bossDead);
+ const battleLog = gE('#textlog>tbody>tr>td', 'all');
- async function onNewRound(){
- try {
- const html = await $ajax.fetch(window.location.href);
+ let stats = getValue('stats', true) || {};
+ const battle = g().battle;
+ if (Object.keys(stats.tokens ?? {}).map(k => battle[k] === stats.tokens?.[k]).some(s => !s)) {
+ if (option.recordUsage) recordUsage2(true);
+ if (option.dropMonitor) dropMonitor(battleLog, true);
+ }
- gE('#pane_completion').removeChild(gE('#btcp'));
- clearBattleUnresponsive();
- const doc = $doc(html)
- if (gE('#riddlecounter', doc)) {
- if (g('option').riddlePopup && !window.opener) {
- window.open(window.location.href, 'riddleWindow', 'resizable,scrollbars,width=1241,height=707');
- return;
- }
- goto();
- return;
- }
- ['#battle_right', '#battle_left'].forEach(selector=>{ gE('#battle_main').replaceChild(gE(selector, doc), gE(selector)); })
- unsafeWindow.battle = new unsafeWindow.Battle();
- unsafeWindow.battle.clear_infopane();
- Debug.log('______________newRound', true);
- newRound(true);
- onBattle();
- } catch(e) { e=>console.error(e) }
+ if (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;
+ }
- 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();
+ if (option.NewRoundWaitTime) { // Next Round
+ await pauseAsync(option.NewRoundWaitTime * _1s);
+ await waitPause();
+ }
+ if (gE('#btcp')?.innerHTML.includes("finishbattle.png")) return console.error(`gE('#btcp')?.innerHTML.includes("finishbattle.png")`);
+ let url = option.checkURLBeforeNewRound;
+ if (url) {
+ lastResponsive = Infinity;
+ await until(async () => { try {
+ await waitPause();
+ return await $ajax.insert(url, undefined, undefined, {}, {}, true);
+ } catch (err) { console.error('Connect failed:', url) }}, option.checkURLBeforeNewRoundRetry * _1s);
+ lastResponsive = time(0);
+ }
+ const doc = $doc(await $ajax.insert(window.location.href));
+ if (gE('#riddlecounter', doc)) {
+ lastResponsive = Infinity;
+ if (option.riddlePopup && !window.opener) {
+ window.open(window.location.href, 'riddleWindow', 'resizable, scrollbars, width=1241, height=707');
+ $async.logSwitch(arguments);
+ return;
+ }
+ console.log(window.location.href);
+ goto();
+ $async.logSwitch(arguments);
+ return;
+ }
+ if (gE('#btcp')) { // 在没有btcp时跳过,例如被Monsterbation、jpx等其他脚本移除时
+ if (option.nativeNewRound) {
+ onStepInDone();
+ gE('#btcp').click();
+ $async.logSwitch(arguments);
+ return;
+ }
+ gE('#pane_completion').removeChild(gE('#btcp'));
+ }
+ ['#battle_right', '#battle_left'].forEach(selector => { gE('#battle_main').replaceChild(gE(selector, doc), gE(selector)); });
+ unsafeWindow.battle = undefined;
+ if (!await loadUnsafeWindowBattle()) {
+ setExitBattleTimeout('Defeat');
+ return;
+ }
+ newRound(true);
+ onStepInDone();
+ onBattleRound();
+ $async.logSwitch(arguments);
+ } catch (err) { console.error(err); }}
};
- document.getElementById('eventStart').click();
- if (a.mode === 'magic' && a.skill >= 200) {
+ gE('body').appendChild(eventEnd);
+
+ const option = getOption();
+ window.sessionStorage.delay = option.delay;
+ window.sessionStorage.delay2 = option.delay2;
+ const fakeApiCall = cE('script');
+ fakeApiCall.textContent = `api_call = ${function (b, a, d) {
+ 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,129 +6810,517 @@ 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
+ let isDisplay = ${option.isDisplayAllDebuff};
+ let debuffAutoFill = ${option.debuffAutoFill?.toString() ?? 'undefined'};
+ let debuffAutoFillRec = ${option.debuffAutoFillRec?.toString() ?? 'undefined'};
+ let onIsekaiEncounter = ${onIsekaiEncounter ?? 'undefined'};
+ // object
+ let dataFlags = ${JSON.stringify(dataFlags)};
+ let _server = ${JSON.stringify(_server)};
+ let monsterStateKeys = ${JSON.stringify(monsterStateKeys)};
+ let ability = ${JSON.stringify(ability)};
+ let monsterBuffSkillLib = ${JSON.stringify(monsterBuffSkillLib)};
+ let hvVersion = Version(...${JSON.stringify(hvVersion.ver.split('.'))});
+ // funciton
+ ${[updateMonsterEffects, fixMonsterStatus,
+ getMonsterID, getMonster, getMonster, getBuff,
+ onRestoredBattleServer, getValue, setValue, delValue,
+ getLocal, setLocal, delLocal,
+ gE, cE, Version].map(f => f.toString()).join(';')};
+ `;
+ gE('head').appendChild(fakeApiCall);
+ const fakeApiResponse = cE('script');
+ fakeApiResponse.textContent = `api_response = ${function (b) {
+ if (b.readyState !== 4) {
+ return false;
+ }
+ if (b.status !== 200) {
window.location.href = window.location.search;
+ return false;
}
+ const a = JSON.parse(b.responseText);
+ if (a.login !== undefined) {
+ top.window.location.href = login_url;
+ return false;
+ }
+ if (a.error || a.reload) {
+ window.location.href = window.location.search;
+ }
+ return a;
+ }.toString()}`;
+ gE('head').appendChild(fakeApiResponse);
+ }
+
+ function updateMonsterEffects(isNewTurn = true) {
+ const option = typeof GM_getValue === 'undefined' ? {} : getOption();
+ if (!(typeof GM_getValue === 'undefined' ? debuffAutoFill : option.debuffAutoFill)) return;
+ let battle = getValue('battle', true);
+ if (!battle?.monsterStatus) return;
+ if (battle.monsterStatus.map(m => getMonster(getMonsterID(m))).filter(mon => mon === null).length) {
+ fixMonsterStatus();
+ battle = getValue('battle', true);
+ }
+
+ let regExp = updateMonsterEffects.prototype.regExp ??= {
+ locationQueries: /\w+=\w+/g,
+ playerInfo: /(\w+) Lv\.(\d+)/,
+ staminaInfo: /Stamina:\s(\d+)/,
+ spellInfo: /\('([\w\s-]+)'.*, '(\w+)', (\d+), (\d+), (\d+)\)/, //Name, iconID, MP, OC, CD
+ itemInfo: /set_infopane_item\((\d+)/,
+ battleTypeLog: /Initializing (.*) \.\.\./,
+ floor: /Floor (\d+)/,
+ round: /Round (\d+) \/ (\d+)/,
+ monster: /MID=(\d+) \(([^<>]+)\) \LV=(\d+) HP=(\d+)/g,
+ effectGain: /([\w\s-]+) gains the effect ([\w\s-]+)\./g,
+ effectExpired: /The effect ([\w\s-]+) on ([\w\s-]+) has expired\./g,
+ effectWear: /The effect ([\w\s-]+) on ([\w\s-]+) has worn off\./g,
+ effectWearAsleep: /([^<>]+) has been roused from its sleep\./g,
+ effectWearConfused: /([^<>]+) got knocked out of confuse\./g,
+ oc: /div/g,
+ ocHalf: /vcr/g,
+ /*isekai911*/
+ spellMatch: /\('(?[\w\s-]+)(?:\s\(x(?\d+)\))?',\s?(?.*),\s?'?(?[\w\s-]+)'?\)/,
+ /*isekai912*/
+ //battleRecorder
+ turnLog: /([^]+?)/,
+ //timeRecorder
+ action: />([^<>]+)<\/td><\/tr>( Spirit Stance Exhausted<\/td><\/tr>)* ([^<>]+)<\/td><\/tr>[^<>]+<\/td><\/tr>( Spirit Stance Exhausted<\/td><\/tr>)* ]+damage( \([^<>]+\))*(<\/td><\/tr> Your spirit shield absorbs \d+ |<|\.)/g,
+ damageType: /for (\d+) (\w+) damage/,
+ spiritShield: /absorbs (\d+)/,
+ crit: /(You crit| crits | blasts )/,
+ strike: /(Fire|Cold|Wind|Elec|Holy|Dark|Void) Strike hits/,
+ damagePlus: /for (\d+) damage/,
+ damagePhysicalPlus: /(Bleeding Wound|Spreading Poison)/,
+ damagePoints: /for (\d+) points of (\w+) damage/,
+ counter: />You counter/g,
+ // dealt magical
+ magicalDealtMiss: /to connect\./g,
+ magicalDealtEvade: /evades your spell\./g,
+ magicalDealtResist50: / (?:hits|blasts) [^y][^<>]+50%/g,
+ magicalDealtResist75: / (?:hits|blasts) [^y][^<>]+75%/g,
+ magicalDealtResist90: / (?:hits|blasts) [^y][^<>]+90%/g,
+ magicalDealtResist: /resists your spell\./g,
+ // dealt physical
+ physicalDealtMiss: /its mark\./g,
+ physicalDealtEvade: /(?: dodges your attack\.|evades your offhand attack\.)/g,
+ physicalDealtParry: /parries your attack\./g,
+ // taken magical
+ magicalTakenEvade: / casts [^<>]+evade the attack\./g,
+ magicalTakenBlock: / casts [^<>]+block the attack\./g,
+ magicalTakenResist50: / (?:hits|blasts) y[^<>]+50%/g,
+ magicalTakenResist75: / (?:hits|blasts) y[^<>]+75%/g,
+ magicalTakenResist90: / (?:hits|blasts) y[^<>]+90%/g,
+ // taken physical
+ physicalTakenMiss: /misses the attack against you\./g,
+ physicalTakenEvade: /(>You evade| uses [^<>]+evade the attack\.)/g,
+ physicalTakenParry: /(>You parry| uses [^<>]+parry the attack\.)/g,
+ physicalTakenBlock: /(>You block| uses [^<>]+block the attack\.)/g,
+ /*isekai911*/
+ //combatRecorder_isekai
+ damage_isekai: /[^<>]+damage/g,
+ damageTaken1_isekai: /(?glances|hits|crits) you.*?(?\d+).*?(?\w+) damage/,
+ damageTaken2_isekai: /which (?glances|hits|crits).*?(?\d+).*?(?\w+) damage/,
+ spiritShield_isekai: /absorbs (\d+)/,
+ damageDealt1_isekai: /(?:You|Your offhand attack|Arcane Blow) (?:(?\d)x-)*(?glance|hit|crit).*?(?\d+).*?(?\w+) damage/,
+ damageDealt2_isekai: /(?:(?\d)x-)*(?glanced|hit|crit|eviscerated) for (?\d+) (?\w+) damage/,
+ strike_isekai: /(Fire|Cold|Wind|Elec|Holy|Dark|Void) Strike hits.*?(\d+).*?(\w+) damage/,
+ explode_isekai: /explodes for (\d+) (\w+) damage/,
+ damagePlus_isekai: /for (\d+) damage/,
+ damagePhysicalPlus_isekai: /(Bleeding Wound|Spreading Poison)/,
+ damagePoints_isekai: /for (\d+) points of (\w+) damage/,
+ debuffLog_isekai: /(?:[^<>]+(?: gains the effect | partially resists the effects of your spell\.| shrugs off the effects of your spell\.)+[^<>]*<\/td><\/tr>)+ You cast [a-zA-Z]+\.<\/td><\/tr>/,
+ debuffResist0_isekai: / gains the effect /g,
+ debuffResist1_isekai: / partially resists the effects of your spell\./g,
+ debuffResist3_isekai: / shrugs off the effects of your spell\./g,
+ counter_isekai: />You counter/g,
+ // dealt magical
+ magicalDealtMiss_isekai: / to connect\./g,
+ magicalDealtEvade_isekai: / evades your spell\./g,
+ magicalDealtResistPartially_isekai: / resists, and was/g,
+ magicalDealtResist_isekai: / resists your spell\./g,
+ // dealt physical
+ physicalDealtMiss_isekai: / its mark\./g,
+ physicalDealtEvade_isekai: / dodges your attack\./g,
+ physicalDealtParryPartially_isekai: / parries[^<>]+?(\d+)[^<>]+?(\w+) damage/g,
+ physicalDealtParry_isekai: / parries your attack\./g,
+ // taken magical
+ magicalTakenMiss_isekai: /(?:casts[^<>]+, but misses the attack\.|casts[^<>]+, missing you completely\.)/g,
+ magicalTakenEvade_isekai: />You evade the attack\./g,
+ magicalTakenResistPartially_isekai: / resist the attack/g,
+ magicalTakenBlockPartially_isekai: /casts[^<>]+partially block (?:and|resist| )*the attack/g,
+ magicalTakenBlock_isekai: /(?]+, but misses the attack\.|(?:vigorously whiffs at a shadow|uses[^<>]+), missing you completely\.)/g,
+ physicalTakenEvade_isekai: />You evade the attack from/g,
+ physicalTakenParryPartially_isekai: /partially parry the attack/g,
+ physicalTakenParry_isekai: /(?]+|>)You|you) partially block (?:and|partially|parry| )*the attack/g,
+ physicalTakenBlock_isekai: /(?]+ proficiency/g,
+ proficiency: /(\d+\.\d+) points of ([^<>]+) proficiency/,
+ dropsLogs: /\S+ \[[^<>]+\](<\/span><\/td><\/tr>A traveling)*/g,
+ drop: /(\S+) \<.*#(.{6}).*\[(.*)\](.)*/,
+ quality: /(Crude|Fair|Average|Superior|Exquisite|Magnificent|Legendary|Peerless)/,
+ credit: /(\d+) Credit/,
+ crystal: /(?:(\d+)x )?(Crystal of \w+)/,
+ }
+
+ function getDuration(skill, channeling) {
+ let [base, profRatio, prof] = [skill.duration, 1, 0];
+ if (typeof base === 'number') {
+ base = base * 1;
+ } else if (base !== 'permanent') { for (const ab in base) {
+ base = base[ab][ability[ab] ?? 0];
+ break;
+ } }
+ if (skill.proficiency) {
+ const [ptype, plow, phigh] = skill.proficiency;
+ prof = proficiency[ptype];
+ profRatio = Math.max(1, Math.min(4, (prof - plow) / (phigh - plow) * 4).toFixed(6) * 1);
+ }
+ const channelingRatio = skill.channling ? channeling : 1;
+ const duration = typeof base === 'number' ? Math.round(base * channelingRatio * profRatio) : base;
+ return [duration, base, profRatio, prof, channelingRatio];
}
- 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();
+ function getEffectChanges(turnLog) {
+ let effectsAdded = turnLog.matchAll(regExp.effectGain);
+ let effectsRemoved = [...turnLog.matchAll(regExp.effectExpired), ...turnLog.matchAll(regExp.effectWear)];
+ let asleepRemoved = turnLog.matchAll(regExp.effectWearAsleep);
+ let confusedRemoved = turnLog.matchAll(regExp.effectWearConfused);
+ let effectChanges = {};
+
+ for (const match of effectsAdded) (effectChanges[match[1]] ??= { add: [], remove: [] }).add.push(match[2]);
+ for (const match of effectsRemoved) (effectChanges[match[2]] ??= { add: [], remove: [] }).remove.push(match[1]);
+ for (const match of asleepRemoved) (effectChanges[match[1]] ??= { add: [], remove: [] }).remove.push('Asleep');
+ for (const match of confusedRemoved) (effectChanges[match[1]] ??= { add: [], remove: [] }).remove.push('Confused');
+
+ return effectChanges;
+ }
+
+ function applyHiddenDelta(savedEffects, effectObj, delta) {
+ if (!savedEffects) return;
+
+ let elementEffects = ['Searing Skin', 'Freezing Limbs', 'Turbulent Air', 'Deep Burns', 'Breached Defense', 'Blunted Attack'];
+ let effects = Object.keys(effectObj);
+ let elementCount = effects.filter(effect => elementEffects.includes(effect)).length;
+
+ for (const savedEffect in savedEffects) {
+ if (effects.includes(savedEffect)) continue;
+
+ if (
+ (elementCount < 3 && elementEffects.includes(savedEffect)) ||
+ savedEffect === 'Coalesced Mana'
+ ) {
+ delete savedEffects[savedEffect];
+ continue;
+ }
+
+ if (!delta || delta <= 0) continue;
+ let savedTurns = +savedEffects[savedEffect]?.turns;
+ if (isNaN(savedTurns)) continue;
+
+ if (savedTurns - delta < 0 && elementEffects.includes(savedEffect)) {
+ delete savedEffects[savedEffect];
+ continue;
+ }
+ savedEffects[savedEffect].turns = Math.max(0, savedTurns - delta);
+ }
+ }
+
+ const turnLog = gE('#textlog').innerHTML.match(/([^]+?)(( )|(<\/tbody>))/)[0];
+ isNewTurn &&= turnLog !== battle.turnLog;
+ if (turnLog.match(regExp.battleTypeLog)) return; // skip if is new round
+
+ // update proficiency
+ const proficiency = battle.proficiency ?? {};
+ const ptypes = {
+ 'cloth armor': 'Cloth Armor',
+ 'deprecating magic' : 'Deprecating',
+ 'divine': 'Divine',
+ 'dual wielding' : 'Dual-wielding',
+ 'elemental': 'Elemental',
+ 'forbidden': 'Forbidden',
+ 'heavy armor': 'Heavy Armor',
+ 'light armor': 'Light Armor',
+ 'one-handed weapon': 'One-handed',
+ 'staff': 'Staff',
+ 'supportive magic' : 'Supportive',
+ 'two-handed weapon': 'Two-handed',
+ }
+ for (const prof of turnLog.match(regExp.proficiencies) ?? []) {
+ const [_, points, type] = prof.match(regExp.proficiency);
+ proficiency[ptypes[type]] += points * 1;
+ proficiency[ptypes[type]] = proficiency[ptypes[type]].toFixed(3) * 1;
+ }
+
+ // cache last turn channeling
+ const channeling = battle.channeling || 1;
+ battle.channeling = getBuff('channeling') ? 1.5 : 1;
+
+ let effectChanges = getEffectChanges(turnLog);
+
+ // 消除对每次操作影响turns程度最大的加速buff(技能或卷轴)
+ let turnDelta = (isNewTurn && !turnLog.match(regExp.zeroturn)) ? 1 : 0;
+ turnDelta *= 100 / ((getBuff('haste')?.getAttribute('onmouseover').match(/increasing its action speed by (.*)%\./)[1] ?? 0) * 1 + 100);
+
+ const getBuffSkill = (buff) => Object.values(monsterBuffSkillLib).find(skill => [skill.name, skill.buff].includes(buff)) ?? console.log('Unknown debuff skill', buff);
+ for (const activeMonster of battle.monsterStatus) {
+ const monster = getMonster(getMonsterID(activeMonster));
+ if (gE('img[src*="nbardead.png"]', monster)) continue; // continue if dead
+
+ let name = gE(monsterStateKeys.name, monster).innerText;
+ let effectObj = {};
+ let jpxObj = {};
+ let monster_btm6 = gE('.btm6', monster);
+ const abs = ability;
+ monster_btm6.querySelectorAll('img').forEach((effect) => {
+ let tooltip = effect.getAttribute('onmouseover');
+ if (!tooltip) return;
+
+ let matches = tooltip.match(regExp.spellMatch);
+ if (!matches?.groups) return;
+
+ let { name, stack, description, turns } = matches.groups;
+ if (!name) return console.log('Undefined debuff name:', name);
+ const jpx = turns === '-' || description.includes('jpx Hidden Effects');
+ if (jpx) { // 可能为jpx补全的buff,在hvAA中重新计算确认数据
+ jpxObj[name] = effect;
+ return;
+ }
+ let dc = getBuffSkill(name)?.description;
+ if (dc !== description) {
+ // TODO 测试确保 ability[4213] Better Slow 效果描述正常,目前是描述均是30%
+ if (dc !== `'The target has been slowed by ${[30, 40, 40, 45, 50, 50][abs[4213] ?? 0]}%, making it attack less frequently.'` && description !== `'The target has been slowed by 30%, making it attack less frequently.'`) {
+ console.log('Unmatched debuff description:', description, '\n from', name, dc);
+ }
+ }
+ effectObj[name] = { turns, stack: stack ?? 1 };
+ });
+ let effects = Object.keys(effectObj);
+
+ // DEBUG ---------------------
+ if (typeof GM_getValue === 'undefined' ? debuffAutoFillRec : option.debuffAutoFillRec) {
+ // 统计持续时间及熟练度相关数据,以便进行核验和测试
+ onRestoredBattleServer('rec', () => {
+ const rec = JSON.parse(localStorage.getItem(`hvAA-${_server.name}_rec`) ?? `{}`);
+ for (const effect of effects) {
+ const turns = effectObj[effect].turns * 1;
+ if (isNaN(turns)) continue;
+ const skill = getBuffSkill(effect);
+ if (!skill) continue;
+
+ rec[effect] ??= { t:0 };
+ // 获取新增时间(忽略非新增的情况)
+ let [delta, added] = [turns - rec[effect].t, rec[effect].d];
+ if (delta > 0) {
+ added = rec[effect].t ? delta : added;
+ }
+ // 获取基础、熟练度计算倍率、熟练度,设置及初始化主要数据
+ let [duration, base, profRatio, prof, channelingRatio] = getDuration(skill, channeling);
+ if (profRatio === 4) rec[effect].f = prof; // 比例刚好是4时的熟练度(推测是公式中的熟练度上限)
+ rec[effect].b = base; // 基础持续时间
+ rec[effect].c = profRatio; // 公式理论计算值
+ rec[effect].ch = rec[effect].t && added > 0 ? channelingRatio : rec[effect].ch; // 引导倍率
+ rec[effect].t = turns; // 当前剩余持续时间
+ rec[effect].d = added; // 新增时间
+ rec[effect].m = Math.max(rec[effect].m ?? 0, added); // 历史最大新增时间
+ rec[effect].a ??= [0, 0]; // 推测熟练度倍率 [ 历史最大值, 按照‘缺失引导信息导致变成1.5倍’的修正值(除以1.5) ]
+ rec[effect].r ??= [0, 0, 0]; // 实际倍率 [ 0-4 应该正常, 4-6推测缺失引导信息, 6+ 异常]
+ rec.error ??= []; // 实际倍率异常时的相关信息
+ // 计算推测倍率
+ if (base <= added) {
+ const a = Math.max(base, added)/base/channelingRatio
+ rec[effect].a[0] = Math.max(a, rec[effect].a[0]).toFixed(4) * 1;
+ rec[effect].a[1] = Math.max(a / 1.5, rec[effect].a[1]).toFixed(4) * 1;
+ }
+ // 检查实际ratio
+ const ratio = Math.max(base, added)/duration;
+ if (ratio > 1.5) {
+ const e = `${effect}: ${Math.max(base, added).toFixed(4)}/(${base.toFixed(4)}*${channelingRatio.toFixed(4)}*${profRatio.toFixed(4)})=${ratio.toFixed(4)}`
+ if (!rec.error.includes(e)) rec.error.push(e);
+ }
+ if (ratio > 1.5 && ratio > rec[effect].r[2]) {
+ rec[effect].r[2] = ratio.toFixed(4) * 1;
+ } else if (ratio <= 1.5 && ratio > 1 && ratio > rec[effect].r[1]) {
+ rec[effect].r[1] = ratio.toFixed(4) * 1;
+ } else if (ratio <= 1 && ratio > rec[effect].r[0]) {
+ rec[effect].r[0] = ratio.toFixed(4) * 1;
+ }
+ localStorage.setItem(`hvAA-${_server.name}_rec`, JSON.stringify(rec));
+ }
+ });
+ }
+ // 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 = (`${_server.isekai ? '/isekai' : ''}/y/e/${ getBuffSkill(effect)?.img || 'channeling'}.png`);
+ let description = getBuffSkill(effect)?.description;
+ img.setAttribute('onmouseover', `battle.set_infopane_effect('${effect} (x${stack})', ${description}, ${isNaN(+turns) ? turns : Math.floor(turns)})`);
+ img.setAttribute('onmouseout', 'battle.clear_infopane()');
+
+ monster_btm6.appendChild(img);
+ }
+ }
+ if (!isNewTurn) return;
+ battle.turnLog = turnLog;
+ battle.time = new Date().getTime();
+ setValue('battle', battle);
}
- 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;
+
+ async function loadUnsafeWindowBattle() { try {
+ unsafeWindow.battle = await until(() => gE('#vbd') ? true : new unsafeWindow.Battle(), 300);
+ if (!unsafeWindow.battle && gE('#vbd')) {
+ console.log('Initialization of unsafeWindow.battle stoped due to defeated.');
+ return false;
+ }
+ unsafeWindow.battle.clear_infopane();
+ return true;
+ } catch (err) { console.error(err); }}
+
+ function newRound(isNew) { // New Round
+ $debug.log('______________newRound', isNew);
+ const option = getOption();
+ const token = document.documentElement.outerHTML.match(/var battle_token = "(.*)";/)[1];
+ let battle = getValue('battle', true);
+ const arena = getValue('arena', true) ?? {};
+ const same = battle?.token === token && arena?.postoken === battle?.postoken;
+ const prof = getValue('proficiency', true);
+ if (isNew) {
+ battle = { proficiency: same ? battle?.proficiency ?? prof : prof };
+ }
+ if (!battle) {
+ battle = JSON.parse(JSON.stringify(g().battle ?? {}));
+ battle.monsterStatus?.sortBy(x => x.order);
+ };
+ [battle.token, battle.postoken] = [token, arena.postoken];
+ battle.proficiency = same ? battle?.proficiency ?? prof : prof;
+ setValue('battle', battle);
+ if (window.location.hash !== '') {
+ goto();
+ }
+ g('monsterAll', gE(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,
+ extra: id => id <= 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;
- }
+ extra: id => id >= 105,
},
+ 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');
+ const firstLog = battleLog[battleLog.length - 1].textContent;
+ if (!battle.roundType || firstLog.match(/^Initializing/)) {
+ battle.tower = (firstLog.match(/\(Floor (\d+)\)/) ?? [null])[1] * 1;
+ const id = (firstLog.match(/\d+/) ?? [null])[0] * 1;
+ battle.roundType = undefined;
+ for (let name in types) {
+ const type = types[name];
+ if (!firstLog.match(type.reg)) continue;
+ if (type.extra && !type.extra(id)) continue;
+ battle.roundType = name;
+ break;
}
- battle.roundType = name;
- break;
+ if (!battle.roundType) console.warn('Unable to get battle type from log:', _server, firstLog);
}
- }
- if (/You lose \d+ Stamina/.test(battleLog[0].textContent)) {
- const staminaLostLog = getValue('staminaLostLog', true) || {};
- staminaLostLog[time(3)] = battleLog[0].textContent.match(/You lose (\d+) Stamina/)[1] * 1;
- setValue('staminaLostLog', staminaLostLog);
- const losedStamina = battleLog[0].textContent.match(/\d+/)[0] * 1;
- if (losedStamina >= g('option').staminaLose) {
- setAlarm('Error');
- if (!_alert(1, '当前Stamina过低\n或Stamina损失过多\n是否继续?', '當前Stamina過低\n或Stamina損失過多\n是否繼續?', 'Continue?\nYou either have too little Stamina or have lost too much')) {
- pauseChange();
- return;
+ if (battle.roundType === 'ba' || document.body.innerHTML.match(/Initializing random encounter/)) {
+ const encounter = getEncounter();
+ if (encounter[0]) {
+ encounter[0].encountered = time(0);
+ setEncounter(encounter);
}
}
- }
- 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) {
+ 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) ?? {};
+ const start = battleLog.length - 2;
+ for (let i = battleLog.length - 2; i > battleLog.length - 2 - g().monsterAll; i--) {
+ let hp = battleLog[i].textContent.match(/HP=(\d+)$/)[1] * 1;
+ if (isNaN(hp)) {
+ hp = getHPFromMonsterDB(monsterDB, monsterNames[order], monsterLvs[order]) ?? monsterStatus[monsterStatus.length - 1].hp;
+ }
+ monsterStatus[order] = { order, hp };
+ order++;
+
+ if (!option.cacheMonsterHP) continue;
+ let lv = battleLog[i].textContent.match(/LV=(\d+)/)[1] * 1;
+ let [_, mid, name] = battleLog[i].textContent.match(/MID=(\d+) \((.*)\) LV/);
+ mid*=1;
+ if (!name || isNaN(lv) || isNaN(mid)) continue;
monsterDB[name] ??= {};
if (monsterDB[name].mid && monsterDB[name].mid !== mid) { // 名称被其他mid被占用
monsterMID[monsterDB[name].mid] = JSON.parse(JSON.stringify(monsterDB[name])); // 将之前mid的数据进行另外备份
@@ -3219,1135 +7330,1200 @@ try {
monsterDB[name] = JSON.parse(JSON.stringify(monsterMID[mid])); // 将之前备份的mid的数据进行恢复
delete monsterMID[mid];
}
- monsterDB[name].mid = mid;
- monsterDB[name][lv] = hp;
- }
- monsterStatus[order] = {
- order: order,
- hp,
- };
- order++;
- }
- if(g('option').cacheMonsterHP){
- if (oldDB !== JSON.stringify(monsterDB)) {
- 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);
- }
+ monsterDB[name].mid = mid;
+ monsterDB[name][lv] = hp;
+ }
+ if (option.cacheMonsterHP) {
+ setValue('monsterDB', monsterDB);
+ setValue('monsterMID', monsterMID);
+ }
+ 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 (const i of range(bugLog)) {
+ 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 of range(monsterHp)) {
+ monsterStatus[i] ??= {};
+ if (gE('img[src*="nbardead.png"]', monsterHp[i])) {
+ monsterStatus[i].isDead = true;
+ monsterStatus[i].hpNow = Infinity;
+ } else {
+ monsterStatus[i].isDead = false;
+ monsterStatus[i].hpNow = Math.floor(monsterStatus[i].hp * parseFloat(gE('img: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 = getOption();
+ const yggdrasilExtraWeight = option.YggdrasilExtraWeight;
+ const baseHpRatio = option.baseHpRatio;
+ // 权重越小,优先级越高
+ for (i of range(monsterStatus)) { // 死亡的排在最后(优先级最低)
+ 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', '世界树 Yggdrasil'].includes(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;
- }
- if (!gE('#pane_effects>img[src*="channeling"]')) {
- return false;
+ // 先存一次,用于下面的额外权重公式
+ battle.monsterStatus = monsterStatus.sortBy(x => x.finWeight);
+ g('battle', battle);
+
+ // 额外权重公式
+ monsterStatus.forEach(t => t.finWeight += resolveRPNFormula(option.extraWeightFormula, t));
+ battle.monsterStatus = monsterStatus.sortBy(x => x.finWeight);
+ g('battle', battle);
}
- const skillLib = {
- Pr: {
- name: 'Protection',
- id: '411',
- img: 'protection',
- },
- SL: {
- name: 'Spark of Life',
- id: '422',
- img: 'sparklife',
- },
- SS: {
- name: 'Spirit Shield',
- id: '423',
- img: 'spiritshield',
- },
- Ha: {
- name: 'Haste',
- id: '412',
- img: 'haste',
- },
- AF: {
- name: 'Arcane Focus',
- id: '432',
- img: 'arcanemeditation',
- },
- He: {
- name: 'Heartseeker',
- id: '431',
- img: 'heartseeker',
- },
- Re: {
- name: 'Regen',
- id: '312',
- img: 'regen',
- },
- SV: {
- name: 'Shadow Veil',
- id: '413',
- img: 'shadowveil',
- },
- Ab: {
- name: 'Absorb',
- id: '421',
- img: 'absorb',
- },
- };
- let i; let
- j;
- const skillPack = g('option').buffSkillOrderValue.split(',');
- if (g('option').channelSkill) {
- for (i = 0; i < skillPack.length; i++) {
- j = skillPack[i];
- if (g('option').channelSkill[j] && !gE(`#pane_effects>img[src*="${skillLib[j].img}"]`) && isOn(skillLib[j].id)) {
- gE(skillLib[j].id).click();
- return true;
- }
+
+ function autoRecover(isCureOnly) { // 自动回血回魔
+ const option = getOption();
+ if (!option.item) {
+ 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();
+ const name = splitOrders(option.itemOrderName, getDefaultOrder('itemOrder'));
+ const order = splitOrders(option.itemOrderValue, getDefaultOrder('itemOrder', ord => ord.value.match(/,(.*)/)[1] * 1));
+ const cures = [313, 11199, 11501, 10005, 11195, 311];
+ for (const i of range(name)) {
+ let id = order[i];
+ if (isCureOnly && !cures.includes(id)) {
+ continue;
+ }
+ 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 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',
+
+ function useScroll() { // 自动使用卷轴
+ const option = getOption();
+ 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',
+ },
};
- 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$/)) {
+ const scrollFirst = (option.scrollFirst) ? '_scroll' : '';
+ for (const i in scrollLib) {
+ if (!option.scroll[i]) {
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;
+ }
+ const id = scrollLib[i].id;
+ if (!gE(`.bti3>div[onmouseover*="(${id})"]`)) {
+ continue;
+ }
+ if (!checkCondition(option[`scroll${i}Condition`])) {
+ continue;
+ }
+ for (const j of range(scrollLib[i].mult + 1)) {
+ if (getBuff(scrollLib[i][`img${j}`] + scrollFirst)) {
+ continue;
}
+ updateSkillOTOS(id);
+ gE(`.bti3>div[onmouseover*="(${id})"]`).click();
+ return true;
}
}
- }
- return false;
- }
-
- function useBuffSkill() { // 自动施法BUFF技能
- const skillLib = {
- Pr: {
- name: 'Protection',
- id: '411',
- img: 'protection',
- },
- SL: {
- name: 'Spark of Life',
- id: '422',
- img: 'sparklife',
- },
- SS: {
- name: 'Spirit Shield',
- id: '423',
- img: 'spiritshield',
- },
- Ha: {
- name: 'Haste',
- id: '412',
- img: 'haste',
- },
- AF: {
- name: 'Arcane Focus',
- id: '432',
- img: 'arcanemeditation',
- },
- He: {
- name: 'Heartseeker',
- id: '431',
- img: 'heartseeker',
- },
- Re: {
- name: 'Regen',
- id: '312',
- img: 'regen',
- },
- SV: {
- name: 'Shadow Veil',
- id: '413',
- img: 'shadowveil',
- },
- Ab: {
- name: 'Absorb',
- id: '421',
- img: 'absorb',
- },
- };
- if (!g('option').buffSkillSwitch) {
return false;
}
- if (!g('option').buffSkill) {
- return false;
+
+ function checkBuffThreshold(buff, threshold) {
+ const id = playerBuffSkillLib[buff].id;
+ const buffObj = getBuff(playerBuffSkillLib[buff].img);
+ threshold = threshold?.[buff] ?? 0;
+ const current = getBuffTurnFromImg(buffObj);
+ const checked = !isOn(id) || (current === Infinity || threshold >= 0 && current > threshold);
+ return { id, buffObj, threshold, current, checked };
}
- if (!checkCondition(g('option').buffSkillCondition)) {
- return false;
+
+ function onClickBuff(id) {
+ updateSkillOTOS(id);
+ gE(id).click();
}
- 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 useChannelSkill() { // 自动施法Channel技能
+ const option = getOption();
+ if (!option.channelSkillSwitch) {
+ 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();
- return true;
+ if (!getBuff('channeling')) {
+ return false;
}
- }
- return false;
- }
- function useInfusions() { // 自动使用魔药
- if (g('attackStatus') === 0) {
- return false;
- }
- if (!g('option').infusionSwitch) {
- return false;
- }
- if (!checkCondition(g('option').infusionCondition)) {
- return false;
- }
+ playerBuffSkillLib.CF.id = getBuff('sparklife') ? undefined : 422;
+ if (option.channelSkill) {
+ const skillPack = splitOrders(option.buffSkillOrderValue, getDefaultOrder('buffSkillOrder'));
+ for (const buff of skillPack) {
+ if (!option.channelSkill[buff]) continue;
- const 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;
- }
+ const { id, buffObj, current, threshold, checked } = checkBuffThreshold(buff, option.channelThreshold);
+ if (checked) continue;
- function autoFocus() {
- if (g('option').focus && checkCondition(g('option').focusCondition)) {
- gE('#ckey_focus').click();
- return true;
- }
- return false;
- }
+ if (buffObj) continue;
+ onClickBuff(id);
+ return true;
+ }
+ }
+ if (option.channelSkill2) {
+ const order = splitOrders(option.channelSkill2OrderValue);
+ const buffs = order.map(id => Object.keys(playerBuffSkillLib).find(s => playerBuffSkillLib[s].id * 1 === 1 * id)).filter(buff => buff);
+ for (const buff of buffs) {
+ const { id, buffObj, current, threshold, checked } = checkBuffThreshold(buff, option.channelThreshold);
+ if (checked) continue;
+ onClickBuff(id);
+ return true;
+ }
+ }
+ if (option.channelRebuff) {
+ let minBuff, minTime;
+ for (const buff in playerBuffSkillLib) {
+ const { id, buffObj, current, threshold, checked } = checkBuffThreshold(buff, option.channelThreshold);
+ if (checked) continue;
- 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;
- }
+ if (buffObj?.src.match(/_scroll.png$/) || (minTime && current >= minTime)) continue;
+ if (!current && (!option.buffSkillSwitch || !option.buffSkill[buff])) continue;
- /**
- * INNAT / WEAPON SKILLS
- *
- * 优先释放先天和武器技能
- */
- function autoSkill() {
- if (!g('option').skillSwitch) {
+ minBuff = id;
+ minTime = current;
+ }
+ if (minBuff && gE(minBuff)) {
+ onClickBuff(minBuff);
+ return true;
+ }
+ }
return false;
}
- if (!gE('#ckey_spirit[src*="spirit_a"]')) {
+
+ function useBuffSkill() { // 自动施法BUFF技能
+ const option = getOption();
+ if (!option.buffSkillSwitch) {
+ return false;
+ }
+ if (!option.buffSkill) {
+ return false;
+ }
+ if (!checkCondition(option.buffSkillCondition)) {
+ return false;
+ }
+ const skillPack = splitOrders(option.buffSkillOrderValue, getDefaultOrder('buffSkillOrder'));
+ for (const buff of skillPack) {
+ if (!option.buffSkill[buff]) continue;
+
+ const { id, buffObj, current, threshold, checked } = checkBuffThreshold(buff, option.buffSkillThreshold);
+ if (checked) continue;
+
+ if (checkCondition(option[`buffSkill${buff}Condition`])) {
+ onClickBuff(id);
+ return true;
+ }
+ }
+
+ 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 (const 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 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();
+ function useInfusions() { // 自动使用魔药
+ const option = getOption();
+ if (!option.infusionSwitch) return false;
+ if (!checkCondition(option.infusionCondition)) {
+ return false;
+ }
+
+ const onUse = function(status) {
+ if (getBuff(infusionLib[status].img)) return false;
+ const itemBtn = gE(`.bti3>div[onmouseover*="(${infusionLib[status].id})"]`);
+ if (!itemBtn) return false;
+ updateSkillOTOS(infusionLib[status].id);
+ itemBtn.click();
+ return true;
+ }
+ const infusionLib = useInfusions.prototype.infusionLib ??= [ null, {
+ id: 12101,
+ img: 'fireinfusion',
+ name: 'Flames',
+ }, {
+ id: 12201,
+ img: 'coldinfusion',
+ name: 'Frost',
+ }, {
+ id: 12301,
+ img: 'elecinfusion',
+ name: 'Lightning',
+ }, {
+ id: 12401,
+ img: 'windinfusion',
+ name: 'Storms',
+ }, {
+ id: 12501,
+ img: 'holyinfusion',
+ name: 'Divinity',
+ }, {
+ id: 12601,
+ img: 'darkinfusion',
+ name: 'Darkness',
+ }];
+
+ if (option.infusionDefaultOnly) {
+ const attackStatus = g().attackStatus;
+ if (attackStatus === 0) return false;
+ return onUse(attackStatus);
+ }
+ if (!option.infusion) return false;
+ const order = splitOrders(option.infusionOrderName, getDefaultOrder('infusionOrder'));
+ for (const name of order) {
+ const condition = option[`infusion${name}Condition`];
+ 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 = getOption();
+ 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 = getOption();
+ 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;
+
+ async function clickMonster(id) {
+ if (!unsafeWindow.battle) {
+ console.log('loadUnsafeWindowBattle before click monster');
+ if (!await loadUnsafeWindowBattle()) {
+ return;
+ }
}
- let succeed = useDebuffSkill(skillPack[i], i < toAllCount);
- // 前 toAllCount 个都是先给全体上的
- if (succeed) {
+ getMonster(id).click();
+ }
+
+ /**
+ * INNAT / WEAPON SKILLS
+ *
+ * 优先释放先天和武器技能
+ */
+ function autoSkill() {
+ const option = getOption();
+ if (!option.skillSwitch) return false;
+ if (!option.skill) return false;
+ if (option.skillSSOnly && !gE('#ckey_spirit[src*="spirit_a"]')) {
+ return false;
+ }
+ const skillOrder = splitOrders(option.skillOrderValue, getDefaultOrder('skillOrder'));
+ 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(i, skillOTOS);
+ 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 = getOption();
+ 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;
+ // 先处理特殊的 “先给全体上buff”
+ let skillPack = splitOrders(option.debuffSkillOrderAllValue, getDefaultOrder('debuffSkillOrderAll'));
+ 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--;
}
- if (isDebuffed(target)) { // 检查是否已有该buff
- continue;
+ const toAllCount = skillPack.length;
+
+ if (option.debuffSkill) { // 是否有启用的buff(不算两个特殊的)
+ skillPack = skillPack.concat(splitOrders(option.debuffSkillOrderValue, getDefaultOrder('debuffSkillOrder')));
+ }
+ for (let i in skillPack) {
+ let buff = skillPack[i];
+ 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;
+ }
}
- primaryTarget = target;
- break;
- }
- if (primaryTarget === undefined) {
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;
- }
- 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();
+ function useDebuffSkill(buff, isAll = false) {
+ const skill = monsterBuffSkillLib[buff];
+ if (!isOn(skill.id)) { // 技能不可用
+ return false;
+ }
+ // 获取范围
+ let skillRange = 1;
+ let ab;
+ const ability = getValue('ability', true);
+ for (ab in skill.range) {
+ const ranges = skill.range[ab];
+ if (!ranges) {
+ continue;
+ }
+ skillRange = ranges[ability ? ability[ab] ?? 0 : 0];
+ break;
+ }
+ // 获取目标
+ const option = getOption();
+ const excludedWeight = target => resolveRPNFormula(option.excludedWeightFormula[buff], target);
+ let exclusiveBuffs;
+ if (isAll && option.debuffAllExclusive) {
+ exclusiveBuffs = Object.keys(option.debuffAllExclusive);
+ exclusiveBuffs = exclusiveBuffs?.includes(buff) ? exclusiveBuffs : undefined
+ }
+ let 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)).sortBy(x => x.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 (const i of range(max)) {
+ let target = buff === 'Dr' ? monsterStatus[max - i - 1] : monsterStatus[i];
+ target = checkCondition(condition, [target]);
+ if (!target || target.isDead || isDebuffed(target)) {
+ continue;
+ }
+ const center = getRangeCenter(target, skillRange, false, excludeCondition, debuffByIndex);
+ if (!id || center.weight < minWeight) {
+ minWeight = center.weight;
+ id = center.id;
+ if (!isAll) break; // 只有覆盖全体才需要遍历全部
+ }
+ }
+ if (id === undefined) {
+ return false;
+ }
+ const imgs = gE('img', 'all', gE(monsterStateKeys.buffs, getMonster(id)));
+ const buffs = Object.fromEntries(Array.from(imgs).map(img => [img.src.match(/\/y\/e\/(.*)\.png/)[1], img]));
+ // 已有buff小于6个
+ // 未开启debuff失败警告
+ // buff剩余持续时间大于等于警报时间
+ if (imgs.length >= 6) {
+ switch (option.debuffSkillTurnAlert * 1) {
+ case 1:
+ if ((option.debuffSkillTurn && (getBuffTurnFromImg(buffs[skill.img]) ?? 0) >= option.debuffSkillTurn[buff])) {
+ return false;
+ }
+ UI.alert('无法正常施放DEBUFF技能,请尝试手动打怪', '無法正常施放DEBUFF技能,請嘗試手動打怪', 'Can not cast de-skills normally, continue the script?\nPlease try attack manually.');
+ pauseChange();
+ return true;
+ case 2:
+ break;
+ default: // case 0, "" or undefined
+ return false;
+ }
+ }
+ 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] },
- }
-
- 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;
- }
- })();
- gE(skill)?.click();
- for (let ab in updateAbility) {
- const ranges = updateAbility[ab][skill];
- if (!ranges) {
- continue;
+ function getCurrentAttackStatus() {
+ let current = g().attackStatusCurrent;
+ if (current === undefined) { // first stack of condition
+ attack(true);
+ current = g().attackStatusCurrent
+ g('attackStatusCurrent', undefined);
+ }
+ return current;
+ }
+
+ function attack(selectStatusOnly = false) { // 自动打怪
+ const option = getOption();
+ const monsters = g().battle.monsterStatus;
+ if (option.attackStatusSwitch) {
+ let tier = option.attackStatusSwitchByTier ? 3 : undefined;
+ const order = splitOrders(option.attackStatusOrderValue, getDefaultOrder('attackStatusOrder'));
+ while (tier === undefined || tier-- !== 0) {
+ for (const status of order) {
+ if (!status && tier) continue;
+ if (!option.attackStatusSwitch[status]) continue;
+ g('attackStatusCurrent', status);
+ if (!checkCondition(option[`attackStatusSwitchCondition${status}`], monsters)) continue;
+ if (onAttack(status, selectStatusOnly, tier)) return true;
}
- range = ranges[getValue('ability', true)[ab]?.level ?? 0];
- break;
+ if (tier === undefined) break;
}
}
+ g('attackStatusCurrent', g().attackStatus);
+ return onAttack(g().attackStatus, selectStatusOnly);
}
- 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 onAttack(attackStatus, selectStatusOnly = false, tier = undefined) {
+ 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] },
+ }
+
+ const option = getOption();
+ const monsters = g().battle.monsterStatus;
+ let target = monsters[0];
+
+ // 如果
+ // 1. 开启了自动以太之触
+ // 2. 目标怪在魔力合流状态中
+ // 3. 满足条件
+ // 使用物理普通攻击,跳过Offensive Magic
+ // 否则按照属性攻击模式释放Spell > Offensive Magic
+ let skillRange = 1;
+ // 1. physical
+ if (attackStatus === 0) {
+ skillRange = g().fightingStyle === '1' ? 3 : 1;
+ return tryAttack();
+ }
+ // 2. etherTap
+ if (option.etherTap && getBuff('coalescemana', getMonsterID(target))) {
+ const expiring = [getBuffStackFromImg, getBuffTurnFromImg].map(getter => getter(getBuff('wpn_et')) <= 1).reduce((acc, cur) => acc || cur);
+ if (expiring && checkCondition(option.etherTapCondition)) {
+ return tryAttack();
+ }
+ }
+ // 3.0 try check skill condition
+ const skill = 1 * (() => {
+ const conditions = [option.lowSkillCondition, option.middleSkillCondition, option.highSkillCondition];
+ for (const lv of range(tier ?? 2, -1, -1)) {
+ let id = `1${attackStatus}${lv + 1}` * 1;
+ if (isOn(id) && (target = checkCondition(conditions[lv], monsters))) return id;
+ if (tier) return 0;
+ }
+ })();
+ // 3.a no skill available
+ if (!skill) {
+ if (tier) return false;
+ return tryAttack();
+ }
+ // 3.b cast skill
+ for (let ab in updateAbility) {
+ const ranges = updateAbility[ab][skill];
+ if (!ranges) continue;
+ const ability = getValue('ability', true);
+ skillRange = ranges[ability ? ability[ab] ?? 0 : 0];
+ break;
+ }
+ return tryAttack(skill);
+
+ function tryAttack (skill) {
+ if (!target || target.isDead) {
+ return false;
+ }
+ if (selectStatusOnly) {
+ return true;
+ }
+ if (skill && gE(skill)) {
+ updateSkillOTOS(skill);
+ gE(skill).click();
+ }
+ clickMonster(getRangeCenter(target, skillRange ?? 1, !attackStatus).id);
+ return true;
+ };
+ }
+
+ function updateSkillOTOS(id, skillOTOS) {
+ skillOTOS ??= getValue('skillOTOS', true) ?? {};
+ skillOTOS[id] ??= 0;
+ skillOTOS[id]++;
+ return setValue('skillOTOS', skillOTOS);
+ }
+
+ // TODO TBD 根据lv模糊推测(一般数据都是等级逐渐提升的,可能可以直接用缓存而不需要推测,异世界新赛季时可以自动刷新缓存?)
+ function getHPFromMonsterDB(mdb, name, lv) {
+ return mdb?.[name]?.[lv]
+ }
- 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 = UI.byLang('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 = getOption();
+ const weightBG = option.weightBackground;
+ if (weightBG) {
+ for (const i of range(weights)) {
+ 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(``, colorTextList[i]);
+ while (remainAttemp > 0 && colorText && colorText.indexOf(``, colorTextList[i]);
}
remainAttemp--;
}
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, different) { // 掉落监测
+ const drop = getValue('drop', true) || {
+ '#startTime': time(3),
+ '#EXP': 0,
+ '#Credit': 0,
+ };
+ const option = getOption();
+ let item, name, amount, regexp;
+ for (const i of range(battleLog)) {
+ if (/^You gain \d+ (EXP|Credit)/.test(battleLog[i].textContent)) {
+ regexp = battleLog[i].textContent.match(/^You gain (\d+) (EXP|Credit)/);
if (regexp) {
- 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 (const j of range(option.dropQuality, quality)) {
+ if (name.match(quality[j])) {
+ name = `Equipment of ${name.match(/^\w+/)[0]}`;
+ drop[name] = (name in drop) ? drop[name] + 1 : 1;
+ 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 && (different || 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);
+ }
+ if (getComputedStyle(gE('#hvAATab-Drop')).display === 'block') {
+ gE(`.hvAATabmenu>span[name="Drop"]`).click();
}
}
- 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;
- }
- }
- 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 matchDamageInfoFromLogText(text, isSkipUnmatched = true) {
+ const regList = [
+ /you for (\d+) (\w+) damage/,
+ /and take (\d+) (\w+) damage/,
+ /You take (\d+) (\w+) damage/,
+ /hits you, causing (\d+) points of (\w+) damage/
+ ];
+ for (let reg of regList) {
+ let match = text.match(reg);
+ if (!match) {
+ continue;
+ }
+ return match;
+ }
+ if (!isSkipUnmatched) {
+ console.log(`Can't match damage info from: `, text);
+ }
+ }
+
+ function recordUsage(param) {
+ const filter = getOption().record;
+ if (!filter) return;
+ let stats = getValue('stats', true) || {};
+ const battle = g().battle;
+ stats.self ??= { _startTime: time(3) };
+ stats.tokens ??= { token: battle.token, postoken: battle.postoken };
+ stats.self._turn = filter.turn ? stats.self._turn ?? 0 : undefined;
+ stats.self._round = filter.round ? stats.self._round ?? 0 : undefined;
+ stats.self._battle = filter.battle ? stats.self._battle ?? 0 : undefined;
+ stats.self._monster = filter.monster ? stats.self._monster ?? 0 : undefined;
+ stats.self._boss = filter.boss ? stats.self._boss ?? 0 : undefined;
+ stats.self.evade = filter.evade ? stats.self.evade ?? 0 : undefined;
+ stats.self.miss = filter.miss ? stats.self.miss ?? 0 : undefined;
+ stats.self.focus = filter.focus ? stats.self.focus ?? 0 : undefined;
+ stats.self.mp = filter.mp ? stats.self.mp ?? 0 : undefined;
+ stats.self.oc = filter.oc ? stats.self.oc ?? 0 : undefined;
+ stats.restore = filter.restore ? stats.restore ?? {} : undefined; // 回复量
+ stats.items = filter.items ? stats.items ?? {} : undefined; // 物品使用次数
+ stats.magic = filter.magic ? stats.magic ?? {} : undefined; // 技能使用次数
+ stats.damage = filter.damage ? stats.damage ?? {} : undefined; // 技能攻击造成的伤害
+ stats.proficiency = filter.proficiency ? stats.proficiency ?? {} : undefined; // 熟练度
+ stats.hurt = filter.hurt ? stats.hurt ?? {} : undefined; // 受到攻击造成的伤害
+ if (filter.hurt) {
+ stats.hurt._avg = filter.hurtavg ? stats.hurt._avg ?? 0 : undefined;
+ stats.hurt._count = filter.hurtcount ? stats.hurt._count ?? 0 : undefined;
+ stats.hurt._total = filter.hurttotal ? stats.hurt._total ?? 0 : undefined;
+ stats.hurt._mavg = filter.hurtmavg ? stats.hurt._mavg ?? 0 : undefined;
+ stats.hurt._mcount = filter.hurtmcount ? stats.hurt._mcount ?? 0 : undefined;
+ stats.hurt._mtotal = filter.hurtmtotal ? stats.hurt._mtotal ?? 0 : undefined;
+ stats.hurt._pavg = filter.hurtpavg ? stats.hurt._pavg ?? 0 : undefined;
+ stats.hurt._pcount = filter.hurtpcount ? stats.hurt._pcount ?? 0 : undefined;
+ stats.hurt._ptotal = filter.hurtptotal ? stats.hurt._ptotal ?? 0 : undefined;
+ }
+ let text, magic, magicName, item, itemName, point, reg;
+ if (g().monsterAlive === 0) {
+ if (filter.turn) {
+ stats.self._turn += battle.turn;
+ }
+ if (filter.round) {
+ stats.self._round += 1;
+ }
+ if (filter.battle) {
+ if (battle.roundNow === battle.roundAll) {
+ stats.self._battle += 1;
+ }
+ }
+ }
+ if (param.mode === 'magic') {
+ [magic, magicName] = [param.magic, param.magicName];
+ if (filter.magic) {
+ let prev = stats.magic[magic] ?? 0;
+ if (magicName in stats.magic) {
+ prev += stats.magic[magicName];
+ delete stats.magic[magicName];
+ }
+ stats.magic[magic] = prev + 1;
+ (stats.magicNames ??= {})[magic] = magicName;
+ }
+ if (filter.mp) {
+ stats.self.mp += param.mp;
+ }
+ if (filter.oc) {
+ stats.self.oc += param.oc;
+ }
+ } else if (param.mode === 'items') {
+ if (filter.items) {
+ [item, itemName] = [param.item, param.itemName];
+ let prev = stats.items[item] ?? 0;
+ if (itemName in stats.items) {
+ prev += stats.items[itemName];
+ delete stats.items[itemName];
+ }
+ stats.items[item] = prev + 1;
+ (stats.itemsNames ??= {})[item] = itemName;
+ }
+ } else {
+ if (filter[param.mode]) {
+ stats.self[param.mode] = (param.mode in stats.self) ? stats.self[param.mode] + 1 : 1;
+ }
}
- 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);
- }
- }
- 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 {
+ const debug = false;
+ let log = false;
+ for (const i of range(param.log)) {
+ if (param.log[i].className === 'tls') {
+ break;
+ }
+ text = param.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 = (param.mode === 'defend') ? 'defend' : text.match(/You drain \d+ HP from/) ? 'drain' : param.magic || param.item;
+ point = text.match(/\d+/)[0] * 1;
+ stats.restore[magic] = (magic in stats.restore) ? stats.restore[magic] + point : point;
+ }
+ } else if (text.match(/(restores|drain) \d+ points of/)) {
+ 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(param.log[i - 1].textContent, false)[2].replace('ing', '');
+ stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point;
+ point = reg[3] * 1;
+ magic = `${reg[1].replace('Your ', '')}_${reg[4]}`;
+ stats.hurt[magic] = (magic in stats.hurt) ? stats.hurt[magic] + point : point;
+ }
+ } else if (text.match(/You gain .* proficiency/)) {
+ 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 (getComputedStyle(gE('#hvAATab-Usage')).display === 'block') {
+ gE(`.hvAATabmenu>span[name="Usage"]`).click();
+ }
+ }
+
+ function recordUsage2(different) {
+ const option = getOption();
+ const filter = option.record;
+ if (!filter) return;
+ const stats = getValue('stats', true);
+ if (!different) {
+ if (filter.monster) stats.self._monster += g().monsterAll;
+ if (filter.boss) stats.self._boss += g().bossAll;
+ }
+ const battle = g().battle;
+ if (option.recordEach && (different || battle.roundNow === battle.roundAll)) {
+ const old = getValue('statsOld', true) || [];
+ stats.__name = getValue('battleCode', true).name;
+ stats.self._endTime = time(3);
+ old.push(stats);
+ setValue('statsOld', old);
+ delValue('stats');
+ return;
+ }
setValue('stats', stats);
+ if (getComputedStyle(gE('#hvAATab-Usage')).display === 'block') {
+ gE(`.hvAATabmenu>span[name="Usage"]`).click();
+ }
}
+ } catch (err) {
+ console.error(err);
+ document.title = err;
}
-} catch (e) {
- console.log(e);
- document.title = e;
-}
+})();