Sim benchmark: full-conservative wins, configurable skill OC

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
This commit is contained in:
GaboGG 2026-07-20 19:53:03 -04:00
parent f252fa5566
commit 39ad28007c
8 changed files with 438 additions and 31 deletions

View file

@ -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();

View file

@ -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();

View file

@ -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"
}

226
simulator/src/benchmark.js Normal file
View file

@ -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`);

View file

@ -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.`);
}

View file

@ -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] };

View file

@ -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,

View file

@ -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();