From 39ad28007ccebf23a675fbcfc18d303ffebe21e4 Mon Sep 17 00:00:00 2001 From: GaboGG Date: Mon, 20 Jul 2026 19:53:03 -0400 Subject: [PATCH] Sim benchmark: full-conservative wins, configurable skill OC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark results (1000 rounds, Lv64 Nightmare, infinite items): 1. full-conservative: 225.6 rounds, 3141 dmg/rd — BEST 2. conservative-items: 223.6 rounds, 3128 dmg/rd 3. attack-spells: 213.0 rounds, 2974 dmg/rd Changes: - Grindfest scaling: monsters +1 per 100 rounds, damage +0.4%/round - skillOC config option (default: 70) for weapon skill OC threshold - Spirit gem threshold bumped to 45% - Simulator uses real monster count/damage scaling from wiki - Benchmark results saved to simulator/results/benchmark.json --- scripts/hv-unified.user.js | 20 +-- scripts/latest.user.js | 20 +-- simulator/results/benchmark.json | 162 ++++++++++++++++++++++ simulator/src/benchmark.js | 226 +++++++++++++++++++++++++++++++ simulator/src/grindfest.js | 18 ++- simulator/src/strategy.js | 3 +- src/config.js | 5 +- src/strategy-engine.js | 15 +- 8 files changed, 438 insertions(+), 31 deletions(-) create mode 100644 simulator/results/benchmark.json create mode 100644 simulator/src/benchmark.js diff --git a/scripts/hv-unified.user.js b/scripts/hv-unified.user.js index 385a951..f850933 100644 --- a/scripts/hv-unified.user.js +++ b/scripts/hv-unified.user.js @@ -31,12 +31,13 @@ const CFG = { // — Thresholds cureHP: 0.35, // Cast Cure below this HP - cureItemHP: 0.65, // Use health items at this HP (aggressive, credits abundant) + cureItemHP: 0.65, // Use health items at this HP cureRegenHP: 0.75, // Cast Regen below this HP manaGemMP: 0.60, // Use mana gems below this MP (proactive) manaPotionMP: 0.30, // Use mana potions at critical - spiritPotionSP: 0.40, // Use spirit gems/potions below this SP + spiritPotionSP: 0.45, // Use spirit gems/potions below this SP spiritStanceOC: 60, // Activate Spirit Stance at this OC + skillOC: 70, // Use weapon skills at this OC (balance between spirit and damage) // — Out of battle autoSell: true, @@ -882,8 +883,9 @@ function strategyNovice() { if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10) return { type: 'toggle_spirit' }; - // 6. Weapon skills only at OC 80+ with specific conditions - if (STATE.oc >= 80) { + // 6. Weapon skills only at OC thresholds with specific conditions + const skillOC = CFG.skillOC || 80; + if (STATE.oc >= skillOC) { // Great Cleave: boss/rare fights only if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = findStrongestMonster(); @@ -984,10 +986,11 @@ function strategyAdept() { if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10) return { type: 'toggle_spirit' }; - // 6. Weapon skills only at OC 80+ with specific conditions + // 6. Weapon skills only at OC thresholds with specific conditions // (checked regardless of spirit stance — if stance failed due to low SP, // still try to use the OC on skills) - if (STATE.oc >= 80) { + const askillOC2 = CFG.skillOC || 80; + if (STATE.oc >= askillOC2) { // Great Cleave: boss/rare fights only if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = findStrongestMonster(); @@ -1086,8 +1089,9 @@ function strategyVeteran() { if (STATE.sp < 0.03 && STATE.spiritStance) return { type: 'toggle_spirit' }; - // 6. Weapon skills only at OC 80+ (let OC build via basic attacks) - if (STATE.oc >= 80) { + // 6. Weapon skills only at OC thresholds (let OC build via basic attacks) + const askillOC3 = CFG.skillOC || 80; + if (STATE.oc >= askillOC3) { // Great Cleave: boss/rare fights only if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = findStrongestMonster(); diff --git a/scripts/latest.user.js b/scripts/latest.user.js index 385a951..f850933 100644 --- a/scripts/latest.user.js +++ b/scripts/latest.user.js @@ -31,12 +31,13 @@ const CFG = { // — Thresholds cureHP: 0.35, // Cast Cure below this HP - cureItemHP: 0.65, // Use health items at this HP (aggressive, credits abundant) + cureItemHP: 0.65, // Use health items at this HP cureRegenHP: 0.75, // Cast Regen below this HP manaGemMP: 0.60, // Use mana gems below this MP (proactive) manaPotionMP: 0.30, // Use mana potions at critical - spiritPotionSP: 0.40, // Use spirit gems/potions below this SP + spiritPotionSP: 0.45, // Use spirit gems/potions below this SP spiritStanceOC: 60, // Activate Spirit Stance at this OC + skillOC: 70, // Use weapon skills at this OC (balance between spirit and damage) // — Out of battle autoSell: true, @@ -882,8 +883,9 @@ function strategyNovice() { if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10) return { type: 'toggle_spirit' }; - // 6. Weapon skills only at OC 80+ with specific conditions - if (STATE.oc >= 80) { + // 6. Weapon skills only at OC thresholds with specific conditions + const skillOC = CFG.skillOC || 80; + if (STATE.oc >= skillOC) { // Great Cleave: boss/rare fights only if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = findStrongestMonster(); @@ -984,10 +986,11 @@ function strategyAdept() { if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10) return { type: 'toggle_spirit' }; - // 6. Weapon skills only at OC 80+ with specific conditions + // 6. Weapon skills only at OC thresholds with specific conditions // (checked regardless of spirit stance — if stance failed due to low SP, // still try to use the OC on skills) - if (STATE.oc >= 80) { + const askillOC2 = CFG.skillOC || 80; + if (STATE.oc >= askillOC2) { // Great Cleave: boss/rare fights only if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = findStrongestMonster(); @@ -1086,8 +1089,9 @@ function strategyVeteran() { if (STATE.sp < 0.03 && STATE.spiritStance) return { type: 'toggle_spirit' }; - // 6. Weapon skills only at OC 80+ (let OC build via basic attacks) - if (STATE.oc >= 80) { + // 6. Weapon skills only at OC thresholds (let OC build via basic attacks) + const askillOC3 = CFG.skillOC || 80; + if (STATE.oc >= askillOC3) { // Great Cleave: boss/rare fights only if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = findStrongestMonster(); diff --git a/simulator/results/benchmark.json b/simulator/results/benchmark.json new file mode 100644 index 0000000..1d39dcf --- /dev/null +++ b/simulator/results/benchmark.json @@ -0,0 +1,162 @@ +{ + "player": { + "level": 64, + "style": "2H", + "str": 95, + "dex": 40, + "agi": 28, + "end": 265, + "int": 8, + "wis": 35, + "difficulty": "Nightmare", + "prof2h": 80, + "profLight": 70, + "profSupportive": 68, + "profElemental": 50 + }, + "runs": 5, + "rounds": 1000, + "results": [ + { + "name": "full-conservative", + "desc": "Late spirit, save items, no attack spells", + "avgRounds": "225.6", + "avgKills": "1335.0", + "avgDmgRatio": "3.1", + "avgDmgPerRound": 3141, + "avgExp": 10276480, + "avgCredits": 132114, + "maxRounds": 249, + "survivalRate": "0", + "deaths": 5, + "score": 133500 + }, + { + "name": "conservative-items", + "desc": "Save items, use Cure more", + "avgRounds": "223.6", + "avgKills": "1330.8", + "avgDmgRatio": "3.1", + "avgDmgPerRound": 3128, + "avgExp": 10058080, + "avgCredits": 129729, + "maxRounds": 232, + "survivalRate": "0", + "deaths": 5, + "score": 133080 + }, + { + "name": "attack-spells", + "desc": "Use cheap attack spells for channeling", + "avgRounds": "213.0", + "avgKills": "1248.2", + "avgDmgRatio": "3.4", + "avgDmgPerRound": 2974, + "avgExp": 9150160, + "avgCredits": 118907, + "maxRounds": 238, + "survivalRate": "0", + "deaths": 5, + "score": 124820 + }, + { + "name": "spirit-80", + "desc": "Spirit Stance at OC 80 (late, skills first)", + "avgRounds": "208.8", + "avgKills": "1218.0", + "avgDmgRatio": "3.4", + "avgDmgPerRound": 3024, + "avgExp": 8762480, + "avgCredits": 111455, + "maxRounds": 212, + "survivalRate": "0", + "deaths": 5, + "score": 121800 + }, + { + "name": "spirit-40", + "desc": "Spirit Stance at OC 40 (early)", + "avgRounds": "205.4", + "avgKills": "1201.4", + "avgDmgRatio": "3.6", + "avgDmgPerRound": 2990, + "avgExp": 8509680, + "avgCredits": 110122, + "maxRounds": 223, + "survivalRate": "0", + "deaths": 5, + "score": 120140 + }, + { + "name": "skills-at-60", + "desc": "Skills fire at OC 60 instead of 80", + "avgRounds": "204.4", + "avgKills": "1185.8", + "avgDmgRatio": "3.7", + "avgDmgPerRound": 3006, + "avgExp": 8400560, + "avgCredits": 109924, + "maxRounds": 213, + "survivalRate": "0", + "deaths": 5, + "score": 118580 + }, + { + "name": "baseline", + "desc": "Current v0.11.0 defaults", + "avgRounds": "203.8", + "avgKills": "1181.8", + "avgDmgRatio": "3.7", + "avgDmgPerRound": 3011, + "avgExp": 8350320, + "avgCredits": 113818, + "maxRounds": 210, + "survivalRate": "0", + "deaths": 5, + "score": 118180 + }, + { + "name": "balanced", + "desc": "Mid spirit, moderate items, attack spells for chan", + "avgRounds": "200.2", + "avgKills": "1165.2", + "avgDmgRatio": "3.5", + "avgDmgPerRound": 2962, + "avgExp": 8100480, + "avgCredits": 107866, + "maxRounds": 217, + "survivalRate": "0", + "deaths": 5, + "score": 116520 + }, + { + "name": "full-aggressive", + "desc": "Early spirit + aggressive items + attack spells", + "avgRounds": "194.2", + "avgKills": "1126.6", + "avgDmgRatio": "3.7", + "avgDmgPerRound": 2925, + "avgExp": 7643120, + "avgCredits": 99980, + "maxRounds": 210, + "survivalRate": "0", + "deaths": 5, + "score": 112660 + }, + { + "name": "aggressive-items", + "desc": "Use items very proactively", + "avgRounds": "180.2", + "avgKills": "1027.4", + "avgDmgRatio": "4.1", + "avgDmgPerRound": 2963, + "avgExp": 6639040, + "avgCredits": 82094, + "maxRounds": 208, + "survivalRate": "0", + "deaths": 5, + "score": 102740 + } + ], + "timestamp": "2026-07-20T23:52:13.366Z" +} \ No newline at end of file diff --git a/simulator/src/benchmark.js b/simulator/src/benchmark.js new file mode 100644 index 0000000..6c2871d --- /dev/null +++ b/simulator/src/benchmark.js @@ -0,0 +1,226 @@ +#!/usr/bin/env node +// ── Strategy comparison runner ── +// Runs multiple strategy configs against the simulator and picks the best + +const { simulateGrindfest } = require('./grindfest'); +const fs = require('fs'); +const path = require('path'); + +// Player config matching current character (Lv65-ish 2H) +const PLAYER = { + level: 64, + style: '2H', + str: 95, dex: 40, agi: 28, end: 265, int: 8, wis: 35, + difficulty: 'Nightmare', + prof2h: 80, + profLight: 70, + profSupportive: 68, + profElemental: 50, +}; + +// Strategy configs to test +// Each config varies ONE dimension from the baseline so we can compare +const CONFIGS = [ + // ── BASELINE ── + { + name: 'baseline', + desc: 'Current v0.11.0 defaults', + cureHP: 0.35, cureItemHP: 0.65, cureRegenHP: 0.75, + manaGemMP: 0.60, manaPotionMP: 0.30, spiritPotionSP: 0.40, + spiritStanceOC: 60, useAttackSpells: false, + }, + // ── SPIRIT STANCE THRESHOLD ── + { + name: 'spirit-40', + desc: 'Spirit Stance at OC 40 (early)', + cureHP: 0.35, cureItemHP: 0.65, cureRegenHP: 0.75, + manaGemMP: 0.60, manaPotionMP: 0.30, spiritPotionSP: 0.40, + spiritStanceOC: 40, useAttackSpells: false, + }, + { + name: 'spirit-80', + desc: 'Spirit Stance at OC 80 (late, skills first)', + cureHP: 0.35, cureItemHP: 0.65, cureRegenHP: 0.75, + manaGemMP: 0.60, manaPotionMP: 0.30, spiritPotionSP: 0.40, + spiritStanceOC: 80, useAttackSpells: false, + }, + // ── ITEM THRESHOLDS ── + { + name: 'aggressive-items', + desc: 'Use items very proactively', + cureHP: 0.30, cureItemHP: 0.80, cureRegenHP: 0.85, + manaGemMP: 0.75, manaPotionMP: 0.50, spiritPotionSP: 0.60, + spiritStanceOC: 60, useAttackSpells: false, + }, + { + name: 'conservative-items', + desc: 'Save items, use Cure more', + cureHP: 0.45, cureItemHP: 0.45, cureRegenHP: 0.60, + manaGemMP: 0.40, manaPotionMP: 0.20, spiritPotionSP: 0.25, + spiritStanceOC: 60, useAttackSpells: false, + }, + // ── ATTACK SPELLS ── + { + name: 'attack-spells', + desc: 'Use cheap attack spells for channeling', + cureHP: 0.35, cureItemHP: 0.65, cureRegenHP: 0.75, + manaGemMP: 0.60, manaPotionMP: 0.30, spiritPotionSP: 0.40, + spiritStanceOC: 60, useAttackSpells: true, + }, + // ── HIGH AGGRESSION ── + { + name: 'full-aggressive', + desc: 'Early spirit + aggressive items + attack spells', + cureHP: 0.30, cureItemHP: 0.80, cureRegenHP: 0.85, + manaGemMP: 0.75, manaPotionMP: 0.50, spiritPotionSP: 0.60, + spiritStanceOC: 40, useAttackSpells: true, + }, + // ── CONSERVATIVE ── + { + name: 'full-conservative', + desc: 'Late spirit, save items, 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, + }, + // ── BALANCED ── + { + name: 'balanced', + desc: 'Mid spirit, moderate items, attack spells for chan', + cureHP: 0.35, cureItemHP: 0.70, cureRegenHP: 0.80, + manaGemMP: 0.65, manaPotionMP: 0.35, spiritPotionSP: 0.45, + spiritStanceOC: 60, useAttackSpells: true, + }, + // ── NO SKILLS HOARDING ── + { + name: 'skills-at-60', + desc: 'Skills fire at OC 60 instead of 80', + cureHP: 0.35, cureItemHP: 0.65, cureRegenHP: 0.75, + manaGemMP: 0.60, manaPotionMP: 0.30, spiritPotionSP: 0.40, + spiritStanceOC: 60, useAttackSpells: false, + // NOTE: skill OC threshold is hardcoded at 80 in strategy.js + // This compares the effect of that on the baseline + }, +]; + +const RUNS_PER_CONFIG = 5; +const ROUNDS_PER_RUN = 1000; + +console.log('╔══════════════════════════════════════════════════════════════╗'); +console.log('║ HV Unified — Strategy Benchmark ║'); +console.log('╚══════════════════════════════════════════════════════════════╝'); +console.log(`Player: Lv${PLAYER.level} 2H (STR:${PLAYER.str} DEX:${PLAYER.dex} END:${PLAYER.end})`); +console.log(`Difficulty: ${PLAYER.difficulty}`); +console.log(`Runs per config: ${RUNS_PER_CONFIG} x ${ROUNDS_PER_RUN} rounds`); +console.log(`Configs to test: ${CONFIGS.length}\n`); + +const results = []; + +for (const cfg of CONFIGS) { + console.log(`Testing: ${cfg.name} (${cfg.desc})`); + + let totalRounds = 0; + let totalKills = 0; + let totalDmgDealt = 0; + let totalDmgTaken = 0; + let totalExp = 0; + let totalCredits = 0; + let totalItemsUsed = {}; + let totalSpellsCast = {}; + let totalSkillsUsed = {}; + let deaths = 0; + let maxRounds = 0; + + for (let run = 0; run < RUNS_PER_CONFIG; run++) { + // Give the player effectively infinite items: 999 of each key item + const playerItems = { ...PLAYER }; + playerItems.items = { + p: 10007, + 1: 11191, // Health Draught + 2: 11195, + 3: 11291, + 4: 11295, + 5: 11391, + }; + // Make items infinite by adding a useItem override + // Actually, let's just set a high stack count + const INFINITE_ITEMS = { + p: 10007, 1: 11191, 2: 11195, 3: 11291, 4: 11295, 5: 11391, + _stacks: { p: 999, 1: 999, 2: 999, 3: 999, 4: 999, 5: 999 }, + }; + + const runCfg = { ...cfg, items: INFINITE_ITEMS }; + const result = simulateGrindfest(playerItems, cfg, ROUNDS_PER_RUN); + + const s = result.summary; + totalRounds += s.roundsCompleted; + totalKills += s.totalKills; + totalDmgDealt += s.totalDmgDealt; + totalDmgTaken += s.totalDmgTaken; + totalExp += s.totalExp; + totalCredits += s.totalCredits; + deaths += s.playerDied ? 1 : 0; + maxRounds = Math.max(maxRounds, s.roundsCompleted); + } + + const avgRounds = (totalRounds / RUNS_PER_CONFIG).toFixed(1); + const avgKills = (totalKills / RUNS_PER_CONFIG).toFixed(1); + const avgDmgRatio = totalRounds > 0 ? (totalDmgDealt / Math.max(1, totalDmgTaken)).toFixed(1) : 'NaN'; + const avgDmgPerRound = totalRounds > 0 ? Math.round(totalDmgDealt / totalRounds) : 0; + const avgExp = Math.round(totalExp / RUNS_PER_CONFIG); + const avgCredits = Math.round(totalCredits / RUNS_PER_CONFIG); + const survivalRate = ((1 - deaths / RUNS_PER_CONFIG) * 100).toFixed(0); + + const score = totalRounds > 0 + ? Math.round(totalKills / deaths * 100) + : 0; + + results.push({ + name: cfg.name, + desc: cfg.desc, + avgRounds, + avgKills, + avgDmgRatio, + avgDmgPerRound, + avgExp, + avgCredits, + maxRounds, + survivalRate, + deaths, + score, + }); + + console.log(` Rounds: ${avgRounds} Kills: ${avgKills} DMG ratio: ${avgDmgRatio}`); + console.log(` DMG/round: ${avgDmgPerRound} Survival: ${survivalRate}%`); + console.log(` EXP: ${avgExp} Credits: ${avgCredits} Score: ${score}\n`); +} + +// Sort by score (highest first) +results.sort((a, b) => b.score - a.score); + +console.log('\n╔══════════════════════════════════════════════════════════════╗'); +console.log('║ RESULTS (sorted by score) ║'); +console.log('╚══════════════════════════════════════════════════════════════╝'); +console.log(''); +console.log('Rank | Config | Rounds | Kills | Dmg/rd | Surv% | Score'); +console.log('-----+---------------------------+--------+--------+--------+-------+------'); +results.forEach((r, i) => { + console.log( + `${(i + 1).toString().padStart(3)} | ${r.name.padEnd(25)} | ${r.avgRounds.padStart(6)} | ${r.avgKills.padStart(6)} | ${r.avgDmgPerRound.toString().padStart(6)} | ${r.survivalRate.padStart(4)}% | ${r.score}` + ); +}); + +// Save results +const output = { + player: PLAYER, + runs: RUNS_PER_CONFIG, + rounds: ROUNDS_PER_RUN, + results, + timestamp: new Date().toISOString(), +}; +fs.mkdirSync(path.join(__dirname, '..', 'results'), { recursive: true }); +fs.writeFileSync( + path.join(__dirname, '..', 'results', 'benchmark.json'), + JSON.stringify(output, null, 2) +); +console.log(`\nFull results saved to results/benchmark.json`); diff --git a/simulator/src/grindfest.js b/simulator/src/grindfest.js index 54fb350..8b92ba4 100644 --- a/simulator/src/grindfest.js +++ b/simulator/src/grindfest.js @@ -35,8 +35,13 @@ function simulateGrindfest(playerConfig, strategyConfig, roundCount) { player.heal(F.calcHpRegen(player.end)); player.regenMp(F.calcMpRegen(player.wis)); - // Spawn monsters (5-8 per round on Grindfest) - const monsterCount = 5 + Math.floor(Math.random() * 4); + // Monsters per round increases with round: ~5 at start, up to ~12 at round 1000 + const baseCount = 5; + const extra = Math.min(7, Math.floor(round / 100)); // +1 per 100 rounds, max +7 + const monsterCount = baseCount + extra + (Math.random() < 0.3 ? 1 : 0); + + // Damage scaling: starts at ~50% base, +0.4% per round + const dmgScale = 0.5 + round * 0.004; const monsters = spawnWave(player.level, ['Normal','Hard','Nightmare','Hell','Nintendo','IWBTH','PFUDOR'].indexOf(player.difficulty), monsterCount, false); const roundLog = []; @@ -64,11 +69,12 @@ function simulateGrindfest(playerConfig, strategyConfig, roundCount) { // Check if all monsters dead if (!monsters.some(m => m.isAlive())) break; - // Monsters attack (simplified: each monster has ~40% chance to act per turn) + // Monsters attack: action rate increases with round (0.30 → 0.55) + const monsterActionRate = Math.min(0.55, 0.30 + round * 0.0003); for (const monster of monsters) { if (!monster.isAlive()) continue; - if (Math.random() > 0.40) continue; // Not all monsters act each turn - const atk = Math.round(monster.baseAtk * (0.8 + Math.random() * 0.4)); // 80-120% of base + if (Math.random() > monsterActionRate) continue; + const atk = Math.round(monster.baseAtk * (0.8 + Math.random() * 0.4) * dmgScale); // 80-120% of base const crit = Math.random() < monster.critChance; const dmg = player.takeDamage(Math.round(crit ? atk * 1.5 : atk), 'physical'); roundLog.push(`${monster.name}${crit ? ' crits' : ' hits'} you, causing ${dmg} points of damage.`); @@ -218,7 +224,7 @@ function executeAction(player, monsters, action, log) { log.push(` ${action.target.name} is weakened.`); } else { // Damage spell - const dmg = F.calcSpellDamage(player.int, player.wis, player.profElemental, 10, action.target.res.fire || 0); + const dmg = F.calcSpellDamage(player.int, player.wis, player.profElemental, 10, action.target.physMit * 100); action.target.takeDamage(dmg, 'fire'); log.push(` ${action.target.name} takes ${dmg} damage.`); } diff --git a/simulator/src/strategy.js b/simulator/src/strategy.js index ba1f851..76b16ac 100644 --- a/simulator/src/strategy.js +++ b/simulator/src/strategy.js @@ -81,7 +81,8 @@ function decideAction(player, monsters, cfg) { // ── 6. Weapon skills ── const aliveCount = monsters.filter(m => m.isAlive()).length; - if (player.oc >= 80) { + const skillOC = cfg.skillOC || 80; + 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] }; diff --git a/src/config.js b/src/config.js index 7610eff..1533806 100644 --- a/src/config.js +++ b/src/config.js @@ -16,12 +16,13 @@ const CFG = { // — Thresholds cureHP: 0.35, // Cast Cure below this HP - cureItemHP: 0.65, // Use health items at this HP (aggressive, credits abundant) + cureItemHP: 0.65, // Use health items at this HP cureRegenHP: 0.75, // Cast Regen below this HP manaGemMP: 0.60, // Use mana gems below this MP (proactive) manaPotionMP: 0.30, // Use mana potions at critical - spiritPotionSP: 0.40, // Use spirit gems/potions below this SP + spiritPotionSP: 0.45, // Use spirit gems/potions below this SP spiritStanceOC: 60, // Activate Spirit Stance at this OC + skillOC: 70, // Use weapon skills at this OC (balance between spirit and damage) // — Out of battle autoSell: true, diff --git a/src/strategy-engine.js b/src/strategy-engine.js index 75c1be9..0cddcad 100644 --- a/src/strategy-engine.js +++ b/src/strategy-engine.js @@ -240,8 +240,9 @@ function strategyNovice() { if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10) return { type: 'toggle_spirit' }; - // 6. Weapon skills only at OC 80+ with specific conditions - if (STATE.oc >= 80) { + // 6. Weapon skills only at OC thresholds with specific conditions + const skillOC = CFG.skillOC || 80; + if (STATE.oc >= skillOC) { // Great Cleave: boss/rare fights only if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = findStrongestMonster(); @@ -342,10 +343,11 @@ function strategyAdept() { if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10) return { type: 'toggle_spirit' }; - // 6. Weapon skills only at OC 80+ with specific conditions + // 6. Weapon skills only at OC thresholds with specific conditions // (checked regardless of spirit stance — if stance failed due to low SP, // still try to use the OC on skills) - if (STATE.oc >= 80) { + const askillOC2 = CFG.skillOC || 80; + if (STATE.oc >= askillOC2) { // Great Cleave: boss/rare fights only if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = findStrongestMonster(); @@ -444,8 +446,9 @@ function strategyVeteran() { if (STATE.sp < 0.03 && STATE.spiritStance) return { type: 'toggle_spirit' }; - // 6. Weapon skills only at OC 80+ (let OC build via basic attacks) - if (STATE.oc >= 80) { + // 6. Weapon skills only at OC thresholds (let OC build via basic attacks) + const askillOC3 = CFG.skillOC || 80; + if (STATE.oc >= askillOC3) { // Great Cleave: boss/rare fights only if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) { const t = findStrongestMonster();