diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index 44a8d3b..fa8683a 100644 --- a/scripts/hv-unified.user.js +++ b/scripts/hv-unified.user.js @@ -29,15 +29,16 @@ const CFG = { autoCure: true, autoSpirit: false, - // — Thresholds - cureHP: 0.45, // Cast Cure below this HP (save items, use Cure) - cureItemHP: 0.50, // Use health items at this HP (conservative, let Cure carry) + // — Thresholds (benchmark-optimized) + cureHP: 0.45, // Cast Cure below this HP + cureItemHP: 0.50, // Use health items at this HP cureRegenHP: 0.65, // Cast Regen below this HP - manaGemMP: 0.45, // Use mana gems below this MP (conservative) - manaPotionMP: 0.25, // Use mana potions at critical + manaGemMP: 0.70, // Use mana gems below this MP (aggressive, stay rich) + manaPotionMP: 0.50, // Use mana potions at this MP spiritPotionSP: 0.30, // Use spirit gems/potions below this SP spiritStanceOC: 80, // Activate Spirit Stance late (save SP for skills) skillOC: 75, // Use weapon skills at this OC + useAttackSpells: true, // Use cheap spells for channeling kickstart // — Out of battle autoSell: true, diff --git a/scripts/latest.user.js b/scripts/latest.user.js index 44a8d3b..fa8683a 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -29,15 +29,16 @@ const CFG = { autoCure: true, autoSpirit: false, - // — Thresholds - cureHP: 0.45, // Cast Cure below this HP (save items, use Cure) - cureItemHP: 0.50, // Use health items at this HP (conservative, let Cure carry) + // — Thresholds (benchmark-optimized) + cureHP: 0.45, // Cast Cure below this HP + cureItemHP: 0.50, // Use health items at this HP cureRegenHP: 0.65, // Cast Regen below this HP - manaGemMP: 0.45, // Use mana gems below this MP (conservative) - manaPotionMP: 0.25, // Use mana potions at critical + manaGemMP: 0.70, // Use mana gems below this MP (aggressive, stay rich) + manaPotionMP: 0.50, // Use mana potions at this MP spiritPotionSP: 0.30, // Use spirit gems/potions below this SP spiritStanceOC: 80, // Activate Spirit Stance late (save SP for skills) skillOC: 75, // Use weapon skills at this OC + useAttackSpells: true, // Use cheap spells for channeling kickstart // — Out of battle autoSell: true, diff --git a/simulator/results/benchmark.json b/simulator/results/benchmark.json index 1ca979e..2c7beaa 100644 --- a/simulator/results/benchmark.json +++ b/simulator/results/benchmark.json @@ -25,145 +25,243 @@ "rounds": 1000, "results": [ { - "name": "full-conservative", - "desc": "Late spirit, save items, no attack spells", - "avgRounds": "146.0", - "avgKills": "809.2", + "name": "chan-mp-focused", + "desc": "Channeling + aggressive mana gem use", + "avgRounds": "149.0", + "avgKills": "835.2", + "avgDmgRatio": "2.9", + "avgDmgPerRound": 4304, + "avgExp": 4488560, + "avgCredits": 58783, + "maxRounds": 162, + "survivalRate": "0", + "deaths": 5, + "score": 83520 + }, + { + "name": "chan-no-buff-refresh", + "desc": "Only cast missing buffs, never refresh early", + "avgRounds": "143.0", + "avgKills": "794.4", + "avgDmgRatio": "3.1", + "avgDmgPerRound": 4252, + "avgExp": 4141360, + "avgCredits": 54902, + "maxRounds": 157, + "survivalRate": "0", + "deaths": 5, + "score": 79440 + }, + { + "name": "chan-aggressive-buff", + "desc": "Channeling via buffs at 20+ turns, keep all buffs fresh", + "avgRounds": "142.4", + "avgKills": "790.2", "avgDmgRatio": "3.2", - "avgDmgPerRound": 4485, - "avgExp": 4388800, - "avgCredits": 57381, + "avgDmgPerRound": 4440, + "avgExp": 4093520, + "avgCredits": 51455, + "maxRounds": 149, + "survivalRate": "0", + "deaths": 5, + "score": 79020 + }, + { + "name": "chan-buff-only", + "desc": "Channeling via buff refresh only, no attack spells", + "avgRounds": "140.6", + "avgKills": "777.0", + "avgDmgRatio": "3.3", + "avgDmgPerRound": 4500, + "avgExp": 4052960, + "avgCredits": 54172, "maxRounds": 166, "survivalRate": "0", "deaths": 5, - "score": 80920 + "score": 77700 }, { "name": "conservative-items", "desc": "Save items, use Cure more", - "avgRounds": "142.8", - "avgKills": "793.8", + "avgRounds": "139.8", + "avgKills": "773.0", + "avgDmgRatio": "3.3", + "avgDmgPerRound": 4480, + "avgExp": 3997360, + "avgCredits": 53493, + "maxRounds": 163, + "survivalRate": "0", + "deaths": 5, + "score": 77300 + }, + { + "name": "full-conservative", + "desc": "Late spirit, save items, no attack spells", + "avgRounds": "136.8", + "avgKills": "759.0", + "avgDmgRatio": "3.3", + "avgDmgPerRound": 4413, + "avgExp": 3777680, + "avgCredits": 47629, + "maxRounds": 148, + "survivalRate": "0", + "deaths": 5, + "score": 75900 + }, + { + "name": "chan-cheap-spell", + "desc": "Channeling via cheapest attack spell (4 MP)", + "avgRounds": "137.4", + "avgKills": "757.8", "avgDmgRatio": "3.1", - "avgDmgPerRound": 4468, - "avgExp": 4131520, - "avgCredits": 53230, - "maxRounds": 154, + "avgDmgPerRound": 4250, + "avgExp": 3854800, + "avgCredits": 52097, + "maxRounds": 162, "survivalRate": "0", "deaths": 5, - "score": 79380 + "score": 75780 }, { - "name": "spirit-40", - "desc": "Spirit Stance at OC 40 (early)", - "avgRounds": "135.8", - "avgKills": "751.2", + "name": "chan-spirit-60", + "desc": "Channeling + spirit stance at 60", + "avgRounds": "136.8", + "avgKills": "755.4", + "avgDmgRatio": "3.3", + "avgDmgPerRound": 4216, + "avgExp": 3785840, + "avgCredits": 51913, + "maxRounds": 153, + "survivalRate": "0", + "deaths": 5, + "score": 75540 + }, + { + "name": "chan-always-regen", + "desc": "Channeling: always cast Regen first if not active", + "avgRounds": "134.2", + "avgKills": "739.8", "avgDmgRatio": "3.4", - "avgDmgPerRound": 4457, - "avgExp": 3770560, - "avgCredits": 47867, - "maxRounds": 152, + "avgDmgPerRound": 4441, + "avgExp": 3694400, + "avgCredits": 51225, + "maxRounds": 158, "survivalRate": "0", "deaths": 5, - "score": 75120 - }, - { - "name": "spirit-80", - "desc": "Spirit Stance at OC 80 (late, skills first)", - "avgRounds": "128.8", - "avgKills": "703.6", - "avgDmgRatio": "3.6", - "avgDmgPerRound": 4398, - "avgExp": 3358160, - "avgCredits": 45813, - "maxRounds": 139, - "survivalRate": "0", - "deaths": 5, - "score": 70360 - }, - { - "name": "skills-at-60", - "desc": "Skills fire at OC 60 instead of 80", - "avgRounds": "126.4", - "avgKills": "689.6", - "avgDmgRatio": "3.6", - "avgDmgPerRound": 4356, - "avgExp": 3243600, - "avgCredits": 42872, - "maxRounds": 143, - "survivalRate": "0", - "deaths": 5, - "score": 68960 + "score": 73980 }, { "name": "baseline", "desc": "Current v0.11.0 defaults", - "avgRounds": "126.4", - "avgKills": "688.8", - "avgDmgRatio": "3.8", - "avgDmgPerRound": 4340, - "avgExp": 3272880, - "avgCredits": 41772, - "maxRounds": 154, - "survivalRate": "0", - "deaths": 5, - "score": 68880 - }, - { - "name": "full-aggressive", - "desc": "Early spirit + aggressive items + attack spells", - "avgRounds": "126.0", - "avgKills": "688.8", + "avgRounds": "130.0", + "avgKills": "712.8", "avgDmgRatio": "3.5", - "avgDmgPerRound": 4162, - "avgExp": 3215200, - "avgCredits": 41874, - "maxRounds": 141, + "avgDmgPerRound": 4371, + "avgExp": 3440240, + "avgCredits": 46973, + "maxRounds": 152, "survivalRate": "0", "deaths": 5, - "score": 68880 + "score": 71280 }, { - "name": "balanced", - "desc": "Mid spirit, moderate items, attack spells for chan", - "avgRounds": "125.8", - "avgKills": "686.4", - "avgDmgRatio": "3.6", - "avgDmgPerRound": 4124, - "avgExp": 3198880, - "avgCredits": 42626, - "maxRounds": 138, + "name": "spirit-80", + "desc": "Spirit Stance at OC 80 (late, skills first)", + "avgRounds": "127.8", + "avgKills": "698.6", + "avgDmgRatio": "3.8", + "avgDmgPerRound": 4351, + "avgExp": 3331760, + "avgCredits": 44061, + "maxRounds": 149, "survivalRate": "0", "deaths": 5, - "score": 68640 - }, - { - "name": "aggressive-items", - "desc": "Use items very proactively", - "avgRounds": "117.0", - "avgKills": "631.4", - "avgDmgRatio": "4.0", - "avgDmgPerRound": 4390, - "avgExp": 2781760, - "avgCredits": 36607, - "maxRounds": 129, - "survivalRate": "0", - "deaths": 5, - "score": 63140 + "score": 69860 }, { "name": "attack-spells", "desc": "Use cheap attack spells for channeling", - "avgRounds": "113.4", - "avgKills": "607.8", - "avgDmgRatio": "3.9", - "avgDmgPerRound": 4036, - "avgExp": 2602480, - "avgCredits": 36235, + "avgRounds": "126.8", + "avgKills": "692.2", + "avgDmgRatio": "3.5", + "avgDmgPerRound": 4209, + "avgExp": 3274880, + "avgCredits": 42980, + "maxRounds": 152, + "survivalRate": "0", + "deaths": 5, + "score": 69220 + }, + { + "name": "skills-at-60", + "desc": "Skills fire at OC 60 instead of 80", + "avgRounds": "125.6", + "avgKills": "685.0", + "avgDmgRatio": "3.6", + "avgDmgPerRound": 4469, + "avgExp": 3200400, + "avgCredits": 41273, + "maxRounds": 137, + "survivalRate": "0", + "deaths": 5, + "score": 68500 + }, + { + "name": "spirit-40", + "desc": "Spirit Stance at OC 40 (early)", + "avgRounds": "117.8", + "avgKills": "637.2", + "avgDmgRatio": "4.3", + "avgDmgPerRound": 4401, + "avgExp": 2806160, + "avgCredits": 36809, "maxRounds": 122, "survivalRate": "0", "deaths": 5, - "score": 60780 + "score": 63720 + }, + { + "name": "full-aggressive", + "desc": "Early spirit + aggressive items + attack spells", + "avgRounds": "117.2", + "avgKills": "632.6", + "avgDmgRatio": "4.0", + "avgDmgPerRound": 4226, + "avgExp": 2801680, + "avgCredits": 38420, + "maxRounds": 141, + "survivalRate": "0", + "deaths": 5, + "score": 63260 + }, + { + "name": "aggressive-items", + "desc": "Use items very proactively", + "avgRounds": "116.2", + "avgKills": "629.0", + "avgDmgRatio": "4.0", + "avgDmgPerRound": 4302, + "avgExp": 2730480, + "avgCredits": 36126, + "maxRounds": 125, + "survivalRate": "0", + "deaths": 5, + "score": 62900 + }, + { + "name": "balanced", + "desc": "Mid spirit, moderate items, attack spells for chan", + "avgRounds": "115.2", + "avgKills": "623.6", + "avgDmgRatio": "4.0", + "avgDmgPerRound": 4097, + "avgExp": 2690320, + "avgCredits": 35489, + "maxRounds": 126, + "survivalRate": "0", + "deaths": 5, + "score": 62360 } ], - "timestamp": "2026-07-21T18:00:29.218Z" + "timestamp": "2026-07-21T18:12:28.425Z" } \ No newline at end of file diff --git a/simulator/src/benchmark.js b/simulator/src/benchmark.js index 9994ef4..b02a6cf 100644 --- a/simulator/src/benchmark.js +++ b/simulator/src/benchmark.js @@ -111,6 +111,65 @@ const CONFIGS = [ // NOTE: skill OC threshold is hardcoded at 80 in strategy.js // This compares the effect of that on the baseline }, + // ── EXPERIMENTAL: CHANNELING + BUFF OPTIMIZATION ── + { + name: 'chan-buff-only', + desc: 'Channeling via buff refresh only, no attack spells', + cureHP: 0.45, cureItemHP: 0.50, cureRegenHP: 0.65, + manaGemMP: 0.45, manaPotionMP: 0.25, spiritPotionSP: 0.30, + spiritStanceOC: 80, useAttackSpells: false, + skillOC: 75, + }, + { + name: 'chan-aggressive-buff', + desc: 'Channeling via buffs at 20+ turns, keep all buffs fresh', + cureHP: 0.45, cureItemHP: 0.50, cureRegenHP: 0.65, + manaGemMP: 0.45, manaPotionMP: 0.25, spiritPotionSP: 0.30, + spiritStanceOC: 80, useAttackSpells: false, + skillOC: 75, + channelRefreshAt: 20, + }, + { + name: 'chan-always-regen', + desc: 'Channeling: always cast Regen first if not active', + cureHP: 0.45, cureItemHP: 0.50, cureRegenHP: 0.90, + manaGemMP: 0.45, manaPotionMP: 0.25, spiritPotionSP: 0.30, + spiritStanceOC: 80, useAttackSpells: false, + skillOC: 75, + }, + { + name: 'chan-cheap-spell', + desc: 'Channeling via cheapest attack spell (4 MP)', + cureHP: 0.45, cureItemHP: 0.50, cureRegenHP: 0.65, + manaGemMP: 0.45, manaPotionMP: 0.25, spiritPotionSP: 0.30, + spiritStanceOC: 80, useAttackSpells: true, + skillOC: 75, + }, + { + name: 'chan-mp-focused', + desc: 'Channeling + aggressive mana gem use', + cureHP: 0.45, cureItemHP: 0.50, cureRegenHP: 0.65, + manaGemMP: 0.70, manaPotionMP: 0.50, spiritPotionSP: 0.30, + spiritStanceOC: 80, useAttackSpells: true, + skillOC: 75, + }, + { + name: 'chan-spirit-60', + desc: 'Channeling + spirit stance at 60', + cureHP: 0.45, cureItemHP: 0.50, cureRegenHP: 0.65, + manaGemMP: 0.45, manaPotionMP: 0.25, spiritPotionSP: 0.30, + spiritStanceOC: 60, useAttackSpells: true, + skillOC: 80, + }, + { + name: 'chan-no-buff-refresh', + desc: 'Only cast missing buffs, never refresh early', + cureHP: 0.45, cureItemHP: 0.50, cureRegenHP: 0.65, + manaGemMP: 0.45, manaPotionMP: 0.25, spiritPotionSP: 0.30, + spiritStanceOC: 80, useAttackSpells: true, + skillOC: 75, + noBuffRefreshChannel: true, + }, ]; const RUNS_PER_CONFIG = 5; diff --git a/simulator/src/strategy.js b/simulator/src/strategy.js index 7f0d382..cb9f674 100644 --- a/simulator/src/strategy.js +++ b/simulator/src/strategy.js @@ -1,4 +1,4 @@ -// ── Strategy decision engine (mirrors the userscript's logic) ── +// ── Strategy decision engine (mirrors the userscript's Adept strategy) ── const RARE_NAMES = [ 'manbearpig', 'white bunneh', 'mithra', 'dalek', @@ -14,6 +14,7 @@ const BUFF_PRIORITY = [ { icon: 'spark_of_life', spell: 'Spark of Life', minTurns: 1 }, { icon: 'regen', spell: 'Regen', minTurns: 2 }, { icon: 'absorb', spell: 'Absorb', minTurns: 2 }, + { icon: 'shadowveil', spell: 'Shadow Veil', minTurns: 3 }, ]; function hasAnyRare(monsters) { @@ -21,7 +22,6 @@ function hasAnyRare(monsters) { } // Returns an action object: { type, name?, target? } -// type: 'attack' | 'spell' | 'skill' | 'item' | 'toggle_spirit' function decideAction(player, monsters, cfg) { // ── 0. Mystic Gem for channeling ── if (!player.channeling) { @@ -52,16 +52,15 @@ function decideAction(player, monsters, cfg) { // ── 3. Buffs ── for (const b of BUFF_PRIORITY) { const dur = player.buffDuration(b.icon); - if ((dur === 0 || dur < b.minTurns) && player.hasSpell(b.spell) && player.mp > 0.15 * player.maxMp) { + if ((dur === 0 || dur < b.minTurns) && player.mp > 0.2 * player.maxMp && player.hasSpell(b.spell)) { if (b.icon === 'regen' && player.hp / player.maxHp >= cfg.cureRegenHP) continue; return { type: 'spell', name: b.spell }; } } - // ── 4. Debuffs ── (only on important fights) + // ── 4. Debuffs ── const importantFight = hasAnyRare(monsters) || monsters.filter(m => m.isAlive()).length <= 2; if (importantFight && player.mp > 0.2 * player.maxMp) { - // Find strongest alive const alive = monsters.filter(m => m.isAlive()); if (alive.length > 0) { const strongest = alive.reduce((a, b) => a.hp > b.hp ? a : b); @@ -72,34 +71,36 @@ function decideAction(player, monsters, cfg) { } } - // ── 5. Spirit Stance ── - if (player.oc >= cfg.spiritStanceOC && !player.spiritStance && player.sp > 0.10 * player.maxSp) - return { type: 'toggle_spirit' }; - if (player.sp < 0.03 * player.maxSp && player.spiritStance) - return { type: 'toggle_spirit' }; - - // ── 6. Weapon skills ── + // ── 5. Weapon skills (before spirit stance) ── const aliveCount = monsters.filter(m => m.isAlive()).length; - const skillOC = cfg.skillOC || 80; + const skillOC = cfg.skillOC || 75; if (player.oc >= skillOC) { - // Great Cleave: bosses only - if (hasAnyRare(monsters) && player.hasSkill('Great Cleave') && !player.cooldowns['Great Cleave']) - return { type: 'skill', name: 'Great Cleave', target: monsters.filter(m => m.isAlive())[0] }; - - // Rending Blow: 5+ enemies - if (aliveCount >= 5 && player.hasSkill('Rending Blow') && !player.cooldowns['Rending Blow']) + // Rending Blow: AoE vs 5+ + if (aliveCount >= 5 && player.hasSkill('Rending Blow') && player.cooldowns['Rending Blow'] === undefined) return { type: 'skill', name: 'Rending Blow', target: monsters.filter(m => m.isAlive())[0] }; - - // Shatter Strike: 5+ enemies, needs Penetrated Armor - if (aliveCount >= 5 && player.hasSkill('Shatter Strike') && !player.cooldowns['Shatter Strike']) { - const hasArmorBreak = monsters.some(m => m.hasDebuff('penetrated_armor')); - if (hasArmorBreak) + // Shatter Strike: AoE vs 5+ with Penetrated Armor + if (aliveCount >= 5 && player.hasSkill('Shatter Strike') && player.cooldowns['Shatter Strike'] === undefined) { + if (monsters.some(m => m.hasDebuff('penetrated_armor'))) return { type: 'skill', name: 'Shatter Strike', target: monsters.filter(m => m.isAlive())[0] }; } + // Great Cleave: bosses only + if (hasAnyRare(monsters) && player.hasSkill('Great Cleave') && player.cooldowns['Great Cleave'] === undefined) + return { type: 'skill', name: 'Great Cleave', target: monsters.filter(m => m.isAlive())[0] }; } + // ── 6. Spirit Stance ── + if (player.oc >= 60 && !player.spiritStance && player.sp > 0.40 * player.maxSp && !player._spiritCooldown) { + player._spiritCooldown = 3; + return { type: 'toggle_spirit' }; + } + if (player.sp < 0.03 * player.maxSp && player.spiritStance && !player._spiritCooldown) { + player._spiritCooldown = 3; + return { type: 'toggle_spirit' }; + } + if (player._spiritCooldown > 0) player._spiritCooldown--; + // ── 7. Damage spells ── - if (cfg.useAttackSpells && player.mp > 0.15 * player.maxMp) { + if (cfg.useAttackSpells && player.mp > 0.2 * player.maxMp) { const dmg = player.spells.find(s => SPELL_T1.includes(s.n)); if (dmg) { const alive = monsters.filter(m => m.isAlive()); @@ -109,19 +110,24 @@ function decideAction(player, monsters, cfg) { // ── 8. Channeling kickstart ── if (!player.channeling) { - // Refresh expiring buffs + const refreshAt = cfg.channelRefreshAt || 15; + + // Refresh expiring buffs (or missing ones) for (const b of BUFF_PRIORITY) { const dur = player.buffDuration(b.icon); - if (dur > 0 && dur < 15 && player.mp > 0.15 * player.maxMp && player.hasSpell(b.spell)) { + if (dur > 0 && dur < refreshAt && player.mp > 0.15 * player.maxMp && player.hasSpell(b.spell)) { if (b.icon === 'regen' && player.hp / player.maxHp >= cfg.cureRegenHP) continue; return { type: 'spell', name: b.spell }; } } - // Cast missing buffs - for (const b of BUFF_PRIORITY) { - if (player.buffDuration(b.icon) === 0 && player.mp > 0.15 * player.maxMp && player.hasSpell(b.spell)) { - if (b.icon === 'regen' && player.hp / player.maxHp >= cfg.cureRegenHP) continue; - return { type: 'spell', name: b.spell }; + + // Cast missing buffs (skip if noBuffRefreshChannel is set) + if (!cfg.noBuffRefreshChannel) { + for (const b of BUFF_PRIORITY) { + if (player.buffDuration(b.icon) === 0 && player.mp > 0.15 * player.maxMp && player.hasSpell(b.spell)) { + if (b.icon === 'regen' && player.hp / player.maxHp >= cfg.cureRegenHP) continue; + return { type: 'spell', name: b.spell }; + } } } } @@ -133,4 +139,4 @@ function decideAction(player, monsters, cfg) { return null; } -module.exports = { decideAction, RARE_NAMES }; +module.exports = { decideAction, RARE_NAMES, BUFF_PRIORITY }; diff --git a/src/config.js b/src/config.js index fe079cb..f8909c3 100644 --- a/src/config.js +++ b/src/config.js @@ -14,15 +14,16 @@ const CFG = { autoCure: true, autoSpirit: false, - // — Thresholds - cureHP: 0.45, // Cast Cure below this HP (save items, use Cure) - cureItemHP: 0.50, // Use health items at this HP (conservative, let Cure carry) + // — Thresholds (benchmark-optimized) + cureHP: 0.45, // Cast Cure below this HP + cureItemHP: 0.50, // Use health items at this HP cureRegenHP: 0.65, // Cast Regen below this HP - manaGemMP: 0.45, // Use mana gems below this MP (conservative) - manaPotionMP: 0.25, // Use mana potions at critical + manaGemMP: 0.70, // Use mana gems below this MP (aggressive, stay rich) + manaPotionMP: 0.50, // Use mana potions at this MP spiritPotionSP: 0.30, // Use spirit gems/potions below this SP spiritStanceOC: 80, // Activate Spirit Stance late (save SP for skills) skillOC: 75, // Use weapon skills at this OC + useAttackSpells: true, // Use cheap spells for channeling kickstart // — Out of battle autoSell: true,