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
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
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.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.`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue