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:
parent
f252fa5566
commit
39ad28007c
8 changed files with 438 additions and 31 deletions
|
|
@ -31,12 +31,13 @@ const CFG = {
|
||||||
|
|
||||||
// — Thresholds
|
// — Thresholds
|
||||||
cureHP: 0.35, // Cast Cure below this HP
|
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
|
cureRegenHP: 0.75, // Cast Regen below this HP
|
||||||
manaGemMP: 0.60, // Use mana gems below this MP (proactive)
|
manaGemMP: 0.60, // Use mana gems below this MP (proactive)
|
||||||
manaPotionMP: 0.30, // Use mana potions at critical
|
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
|
spiritStanceOC: 60, // Activate Spirit Stance at this OC
|
||||||
|
skillOC: 70, // Use weapon skills at this OC (balance between spirit and damage)
|
||||||
|
|
||||||
// — Out of battle
|
// — Out of battle
|
||||||
autoSell: true,
|
autoSell: true,
|
||||||
|
|
@ -882,8 +883,9 @@ function strategyNovice() {
|
||||||
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
||||||
return { type: 'toggle_spirit' };
|
return { type: 'toggle_spirit' };
|
||||||
|
|
||||||
// 6. Weapon skills only at OC 80+ with specific conditions
|
// 6. Weapon skills only at OC thresholds with specific conditions
|
||||||
if (STATE.oc >= 80) {
|
const skillOC = CFG.skillOC || 80;
|
||||||
|
if (STATE.oc >= skillOC) {
|
||||||
// Great Cleave: boss/rare fights only
|
// Great Cleave: boss/rare fights only
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
@ -984,10 +986,11 @@ function strategyAdept() {
|
||||||
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
||||||
return { type: 'toggle_spirit' };
|
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,
|
// (checked regardless of spirit stance — if stance failed due to low SP,
|
||||||
// still try to use the OC on skills)
|
// 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
|
// Great Cleave: boss/rare fights only
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
@ -1086,8 +1089,9 @@ function strategyVeteran() {
|
||||||
if (STATE.sp < 0.03 && STATE.spiritStance)
|
if (STATE.sp < 0.03 && STATE.spiritStance)
|
||||||
return { type: 'toggle_spirit' };
|
return { type: 'toggle_spirit' };
|
||||||
|
|
||||||
// 6. Weapon skills only at OC 80+ (let OC build via basic attacks)
|
// 6. Weapon skills only at OC thresholds (let OC build via basic attacks)
|
||||||
if (STATE.oc >= 80) {
|
const askillOC3 = CFG.skillOC || 80;
|
||||||
|
if (STATE.oc >= askillOC3) {
|
||||||
// Great Cleave: boss/rare fights only
|
// Great Cleave: boss/rare fights only
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,13 @@ const CFG = {
|
||||||
|
|
||||||
// — Thresholds
|
// — Thresholds
|
||||||
cureHP: 0.35, // Cast Cure below this HP
|
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
|
cureRegenHP: 0.75, // Cast Regen below this HP
|
||||||
manaGemMP: 0.60, // Use mana gems below this MP (proactive)
|
manaGemMP: 0.60, // Use mana gems below this MP (proactive)
|
||||||
manaPotionMP: 0.30, // Use mana potions at critical
|
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
|
spiritStanceOC: 60, // Activate Spirit Stance at this OC
|
||||||
|
skillOC: 70, // Use weapon skills at this OC (balance between spirit and damage)
|
||||||
|
|
||||||
// — Out of battle
|
// — Out of battle
|
||||||
autoSell: true,
|
autoSell: true,
|
||||||
|
|
@ -882,8 +883,9 @@ function strategyNovice() {
|
||||||
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
||||||
return { type: 'toggle_spirit' };
|
return { type: 'toggle_spirit' };
|
||||||
|
|
||||||
// 6. Weapon skills only at OC 80+ with specific conditions
|
// 6. Weapon skills only at OC thresholds with specific conditions
|
||||||
if (STATE.oc >= 80) {
|
const skillOC = CFG.skillOC || 80;
|
||||||
|
if (STATE.oc >= skillOC) {
|
||||||
// Great Cleave: boss/rare fights only
|
// Great Cleave: boss/rare fights only
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
@ -984,10 +986,11 @@ function strategyAdept() {
|
||||||
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
||||||
return { type: 'toggle_spirit' };
|
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,
|
// (checked regardless of spirit stance — if stance failed due to low SP,
|
||||||
// still try to use the OC on skills)
|
// 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
|
// Great Cleave: boss/rare fights only
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
@ -1086,8 +1089,9 @@ function strategyVeteran() {
|
||||||
if (STATE.sp < 0.03 && STATE.spiritStance)
|
if (STATE.sp < 0.03 && STATE.spiritStance)
|
||||||
return { type: 'toggle_spirit' };
|
return { type: 'toggle_spirit' };
|
||||||
|
|
||||||
// 6. Weapon skills only at OC 80+ (let OC build via basic attacks)
|
// 6. Weapon skills only at OC thresholds (let OC build via basic attacks)
|
||||||
if (STATE.oc >= 80) {
|
const askillOC3 = CFG.skillOC || 80;
|
||||||
|
if (STATE.oc >= askillOC3) {
|
||||||
// Great Cleave: boss/rare fights only
|
// Great Cleave: boss/rare fights only
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
|
||||||
162
simulator/results/benchmark.json
Normal file
162
simulator/results/benchmark.json
Normal 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
226
simulator/src/benchmark.js
Normal 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`);
|
||||||
|
|
@ -35,8 +35,13 @@ function simulateGrindfest(playerConfig, strategyConfig, roundCount) {
|
||||||
player.heal(F.calcHpRegen(player.end));
|
player.heal(F.calcHpRegen(player.end));
|
||||||
player.regenMp(F.calcMpRegen(player.wis));
|
player.regenMp(F.calcMpRegen(player.wis));
|
||||||
|
|
||||||
// Spawn monsters (5-8 per round on Grindfest)
|
// Monsters per round increases with round: ~5 at start, up to ~12 at round 1000
|
||||||
const monsterCount = 5 + Math.floor(Math.random() * 4);
|
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 monsters = spawnWave(player.level, ['Normal','Hard','Nightmare','Hell','Nintendo','IWBTH','PFUDOR'].indexOf(player.difficulty), monsterCount, false);
|
||||||
|
|
||||||
const roundLog = [];
|
const roundLog = [];
|
||||||
|
|
@ -64,11 +69,12 @@ function simulateGrindfest(playerConfig, strategyConfig, roundCount) {
|
||||||
// Check if all monsters dead
|
// Check if all monsters dead
|
||||||
if (!monsters.some(m => m.isAlive())) break;
|
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) {
|
for (const monster of monsters) {
|
||||||
if (!monster.isAlive()) continue;
|
if (!monster.isAlive()) continue;
|
||||||
if (Math.random() > 0.40) continue; // Not all monsters act each turn
|
if (Math.random() > monsterActionRate) continue;
|
||||||
const atk = Math.round(monster.baseAtk * (0.8 + Math.random() * 0.4)); // 80-120% of base
|
const atk = Math.round(monster.baseAtk * (0.8 + Math.random() * 0.4) * dmgScale); // 80-120% of base
|
||||||
const crit = Math.random() < monster.critChance;
|
const crit = Math.random() < monster.critChance;
|
||||||
const dmg = player.takeDamage(Math.round(crit ? atk * 1.5 : atk), 'physical');
|
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.`);
|
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.`);
|
log.push(` ${action.target.name} is weakened.`);
|
||||||
} else {
|
} else {
|
||||||
// Damage spell
|
// 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');
|
action.target.takeDamage(dmg, 'fire');
|
||||||
log.push(` ${action.target.name} takes ${dmg} damage.`);
|
log.push(` ${action.target.name} takes ${dmg} damage.`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,8 @@ function decideAction(player, monsters, cfg) {
|
||||||
|
|
||||||
// ── 6. Weapon skills ──
|
// ── 6. Weapon skills ──
|
||||||
const aliveCount = monsters.filter(m => m.isAlive()).length;
|
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
|
// Great Cleave: bosses only
|
||||||
if (hasAnyRare(monsters) && player.hasSkill('Great Cleave') && !player.cooldowns['Great Cleave'])
|
if (hasAnyRare(monsters) && player.hasSkill('Great Cleave') && !player.cooldowns['Great Cleave'])
|
||||||
return { type: 'skill', name: 'Great Cleave', target: monsters.filter(m => m.isAlive())[0] };
|
return { type: 'skill', name: 'Great Cleave', target: monsters.filter(m => m.isAlive())[0] };
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,13 @@ const CFG = {
|
||||||
|
|
||||||
// — Thresholds
|
// — Thresholds
|
||||||
cureHP: 0.35, // Cast Cure below this HP
|
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
|
cureRegenHP: 0.75, // Cast Regen below this HP
|
||||||
manaGemMP: 0.60, // Use mana gems below this MP (proactive)
|
manaGemMP: 0.60, // Use mana gems below this MP (proactive)
|
||||||
manaPotionMP: 0.30, // Use mana potions at critical
|
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
|
spiritStanceOC: 60, // Activate Spirit Stance at this OC
|
||||||
|
skillOC: 70, // Use weapon skills at this OC (balance between spirit and damage)
|
||||||
|
|
||||||
// — Out of battle
|
// — Out of battle
|
||||||
autoSell: true,
|
autoSell: true,
|
||||||
|
|
|
||||||
|
|
@ -240,8 +240,9 @@ function strategyNovice() {
|
||||||
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
||||||
return { type: 'toggle_spirit' };
|
return { type: 'toggle_spirit' };
|
||||||
|
|
||||||
// 6. Weapon skills only at OC 80+ with specific conditions
|
// 6. Weapon skills only at OC thresholds with specific conditions
|
||||||
if (STATE.oc >= 80) {
|
const skillOC = CFG.skillOC || 80;
|
||||||
|
if (STATE.oc >= skillOC) {
|
||||||
// Great Cleave: boss/rare fights only
|
// Great Cleave: boss/rare fights only
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
@ -342,10 +343,11 @@ function strategyAdept() {
|
||||||
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
if (STATE.oc >= 60 && !STATE.spiritStance && STATE.sp > 0.10)
|
||||||
return { type: 'toggle_spirit' };
|
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,
|
// (checked regardless of spirit stance — if stance failed due to low SP,
|
||||||
// still try to use the OC on skills)
|
// 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
|
// Great Cleave: boss/rare fights only
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
@ -444,8 +446,9 @@ function strategyVeteran() {
|
||||||
if (STATE.sp < 0.03 && STATE.spiritStance)
|
if (STATE.sp < 0.03 && STATE.spiritStance)
|
||||||
return { type: 'toggle_spirit' };
|
return { type: 'toggle_spirit' };
|
||||||
|
|
||||||
// 6. Weapon skills only at OC 80+ (let OC build via basic attacks)
|
// 6. Weapon skills only at OC thresholds (let OC build via basic attacks)
|
||||||
if (STATE.oc >= 80) {
|
const askillOC3 = CFG.skillOC || 80;
|
||||||
|
if (STATE.oc >= askillOC3) {
|
||||||
// Great Cleave: boss/rare fights only
|
// Great Cleave: boss/rare fights only
|
||||||
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
if (anyRareMonster() && STATE.skillsKnown.includes('Great Cleave')) {
|
||||||
const t = findStrongestMonster();
|
const t = findStrongestMonster();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue