// ── Simulated Player ── const F = require('./formulas'); class SimPlayer { constructor(config) { this.level = config.level || 52; this.tier = this.level >= 300 ? 'master' : this.level >= 150 ? 'veteran' : this.level >= 50 ? 'adept' : 'novice'; this.style = config.style || '2H'; this.difficulty = config.difficulty || 'Nightmare'; // Base stats matching Lv52 2H character from real game data (HP ~1804) this.str = config.str || 85; this.dex = config.dex || 34; this.agi = config.agi || 24; this.end = config.end || 258; // ~1804 HP at Lv52 from gear this.int = config.int || 6; this.wis = config.wis || 31; // ~93 MP from gear // Derived stats (use effective values from config if provided) this.maxHp = config.maxHp || F.calcMaxHp(this.end, this.level); this.maxMp = config.maxMp || F.calcMaxMp(this.wis, this.level); this.maxSp = config.maxSp || F.calcMaxSp(this.int, this.level); this.hp = this.maxHp; this.mp = this.maxMp; this.sp = this.maxSp; this.oc = 0; // Proficiencies this.prof2h = config.prof2h || 60; this.prof1h = config.prof1h || 7; this.profLight = config.profLight || 60; this.profHeavy = config.profHeavy || 47; this.profSupportive = config.profSupportive || 60; this.profElemental = config.profElemental || 42; // Damage stats this.baseAtk = config.baseAtk || F.calcBasePhysDamage(this.str, this.dex, this.level, this.prof2h); this.critChance = config.critChance || F.calcCritChance(this.str, this.dex); this.critDamage = config.critDmg || F.calcCritDamage(68); // +68% from gear // Mitigation this.physMit = (config.physMit / 100) || 0.315; this.magMit = (config.magMit / 100) || 0.299; // Buffs this.buffs = {}; // { name: {turns: N} } this.spiritStance = false; this.channeling = false; // Spells known this.spells = config.spells || [ { n: 'Cure', mp: 11 }, { n: 'Protection', mp: 14 }, { n: 'Fiery Blast', mp: 4 }, { n: 'Freeze', mp: 4 }, { n: 'Shockblast', mp: 4 }, { n: 'Gale', mp: 4 }, { n: 'Regen', mp: 17 }, { n: 'Absorb', mp: 17 }, { n: 'Drain', mp: 9 }, { n: 'Slow', mp: 12 }, ]; // Skills known this.skills = config.skills || [ 'Great Cleave', 'Rending Blow', ]; // Items this.items = (config && config.items) ? config.items : { p: 10007, 1: 11191, 2: 11195, 3: 11291, 4: 11295, 5: 11391, }; // Item definitions (subset) this.itemDefs = { 10005: { n: 'Health Gem', t: 'heal' }, 10006: { n: 'Mana Gem', t: 'mana' }, 10007: { n: 'Spirit Gem', t: 'spirit' }, 10008: { n: 'Mystic Gem', t: 'channel' }, 11191: { n: 'Health Draught', t: 'heal' }, 11195: { n: 'Health Potion', t: 'heal' }, 11291: { n: 'Mana Draught', t: 'mana' }, 11295: { n: 'Mana Potion', t: 'mana' }, 11391: { n: 'Spirit Draught', t: 'spirit' }, }; // Cooldowns tracking this.cooldowns = {}; this.skillCosts = { 'Great Cleave': 50, 'Rending Blow': 50, 'Shatter Strike': 50, }; this.skillCooldowns = { 'Great Cleave': 5, 'Rending Blow': 5, 'Shatter Strike': 5, }; // Action log for this round this.roundLog = []; // Strategy config (overridable) this.cfg = { cureHP: 0.35, cureItemHP: 0.65, cureRegenHP: 0.75, manaGemMP: 0.60, manaPotionMP: 0.30, spiritPotionSP: 0.40, spiritStanceOC: 60, useAttackSpells: false, autoBuff: true, autoDebuff: true, autoSpirit: false, }; } isAlive() { return this.hp > 0; } useMp(cost) { cost = Math.min(cost, this.mp); this.mp -= cost; return cost; } addOc(amount) { this.oc = Math.min(100, this.oc + amount); } spendOc(amount) { if (this.oc < amount) return false; this.oc -= amount; return true; } hasBuff(name) { return this.buffs[name] && this.buffs[name].turns > 0; } buffDuration(name) { return this.buffs[name] ? this.buffs[name].turns : 0; } addBuff(name, turns) { this.buffs[name] = { turns: Math.max(this.buffs[name]?.turns || 0, turns) }; } hasSpell(name) { return this.spells.some(s => s.n === name); } hasSkill(name) { return this.skills.includes(name); } hasUsableItem(type) { for (const [slot, id] of Object.entries(this.items)) { if (!id || id === 0) continue; const def = this.itemDefs[id]; if (!def || def.t !== type) continue; return { id: slot, item: def }; } return null; } useItem(slot) { const id = this.items[slot]; if (!id) return null; const def = this.itemDefs[id]; this.items[slot] = 0; // consumed this.roundLog.push(`You use ${def.n}.`); return def; } takeDamage(amount, type) { const mit = type === 'physical' ? this.physMit : this.magMit; const effective = Math.round(amount * (1 - mit / 100)); this.hp -= effective; if (this.hp < 0) this.hp = 0; return effective; } heal(amount) { this.hp = Math.min(this.maxHp, this.hp + amount); } regenMp(amount) { this.mp = Math.min(this.maxMp, this.mp + amount); } regenSp(amount) { this.sp = Math.min(this.maxSp, this.sp + amount); } tickBuffs() { // Tick down buff durations for (const key of Object.keys(this.buffs)) { this.buffs[key].turns--; if (this.buffs[key].turns <= 0) delete this.buffs[key]; } // Tick cooldowns for (const key of Object.keys(this.cooldowns)) { this.cooldowns[key]--; if (this.cooldowns[key] <= 0) delete this.cooldowns[key]; } } } // ── Monster definitions and factory ── const MONSTERS = { low: [ { name: 'Tentacle Monster', mid: 2, pl: 50, hpBase: 1000, res: { phys: 10 } }, { name: 'Cockatrice', mid: 4, pl: 50, hpBase: 1000, res: { phys: 10 } }, { name: 'Giant Panda', mid: 5, pl: 50, hpBase: 1000, res: { phys: 10 } }, { name: 'Scary Ghost', mid: 7, pl: 50, hpBase: 1000, res: { phys: 10 } }, { name: 'Rabid Hamster', mid: 8, pl: 50, hpBase: 1000, res: { phys: 10 } }, { name: 'Cookie Monster', mid: 9, pl: 50, hpBase: 1000, res: { phys: 10 } }, { name: 'Blue Slime', mid: 10, pl: 50, hpBase: 1000, res: { phys: 10 } }, { name: 'Mantitcore', mid: 3, pl: 55, hpBase: 1100, res: { phys: 15 } }, { name: 'Green Slime', mid: 11, pl: 50, hpBase: 1000, res: { phys: 10 } }, { name: 'Fire Fox', mid: 12, pl: 55, hpBase: 1100, res: { phys: 15 } }, { name: 'Punishment Dragon', mid: 13, pl: 60, hpBase: 1200, res: { phys: 20 } }, ], bosses: [ { name: 'Manbearpig', mid: 16, pl: 100, hpBase: 5000, res: { phys: 50 }, boss: true }, { name: 'White Bunneh', mid: 17, pl: 100, hpBase: 5000, res: { phys: 50 }, boss: true }, { name: 'Mithra', mid: 18, pl: 100, hpBase: 5000, res: { phys: 50 }, boss: true }, { name: 'Dalek', mid: 19, pl: 100, hpBase: 5000, res: { phys: 50 }, boss: true }, ], }; class Monster { constructor(template, level, difficultyMult) { this.name = template.name; this.mid = template.mid; this.boss = template.boss || false; // Generate base stats for this monster (scaled to player level) const pl = template.pl || level; const baseEnd = template.boss ? 200 + Math.round(Math.random() * 100) : 30 + Math.round(Math.random() * 50); const baseStr = template.boss ? 150 + Math.round(Math.random() * 100) : 20 + Math.round(Math.random() * 40); const baseDex = template.boss ? 80 + Math.round(Math.random() * 40) : 10 + Math.round(Math.random() * 30); const baseAgi = template.boss ? 60 + Math.round(Math.random() * 40) : 5 + Math.round(Math.random() * 20); const baseWis = template.boss ? 60 + Math.round(Math.random() * 40) : 5 + Math.round(Math.random() * 20); const baseInt = template.boss ? 40 + Math.round(Math.random() * 30) : 5 + Math.round(Math.random() * 15); // Scale monster stats to level using wiki formula: int(0.01 * base * level + level^1.076675 * 0.3325) const scale = (base) => Math.round(0.01 * base * level + Math.pow(level, 1.076675) * 0.3325); this.str = scale(baseStr); this.dex = scale(baseDex); this.agi = scale(baseAgi); this.end = scale(baseEnd); this.int = scale(baseInt); this.wis = scale(baseWis); // HP, MP, SP from wiki formulas const diffMult = template.boss ? difficultyMult * 2 : difficultyMult; this.maxHp = F.monsterHp(level, this.end, diffMult, template.classMod || 1); this.hp = this.maxHp; this.maxMp = F.monsterMp(level, this.wis); this.mp = this.maxMp; this.maxSp = template.boss ? F.monsterSp(this) : 0; this.sp = this.maxSp; // Derived combat stats this.baseAtk = F.monsterBasePhysDmg(this.str, this.dex); this.critChance = F.monsterPhysCrit(this.dex, this.str); this.physMit = F.monsterPhysMit(this.end, this.agi); this.magMit = F.monsterMagMit(this.end, this.wis); this.evade = F.monsterEvade(this.agi, level); this.parry = F.monsterParry(this.dex, level); this.alive = true; this.buffs = {}; this.debuffs = {}; } isAlive() { return this.alive && this.hp > 0; } takeDamage(amount, type) { // Apply hit/evade/parry check if (type && Math.random() < this.evade) return { dealt: 0, resisted: amount, evaded: true }; if (type && Math.random() < this.parry) return { dealt: 0, resisted: amount, parried: true }; const mit = type === 'fire' || type === 'cold' || type === 'elec' || type === 'wind' || type === 'holy' || type === 'dark' ? this.magMit : this.physMit; const effective = Math.round(amount * (1 - mit)); this.hp -= effective; if (this.hp <= 0) { this.hp = 0; this.alive = false; } return { dealt: effective, resisted: amount - effective }; } hasDebuff(name) { return this.debuffs[name] && this.debuffs[name].turns > 0; } addDebuff(name, turns, stacks) { if (!this.debuffs[name]) this.debuffs[name] = { turns: 0, stacks: 0 }; this.debuffs[name].turns = Math.max(this.debuffs[name].turns, turns); this.debuffs[name].stacks = Math.max(this.debuffs[name].stacks, stacks || 1); } tickBuffs() { for (const key of Object.keys(this.debuffs)) { this.debuffs[key].turns--; if (this.debuffs[key].turns <= 0) delete this.debuffs[key]; } } getPhysMitigation() { const base = this.res.phys || 10; const penStacks = this.debuffs['penetrated_armor'] ? this.debuffs['penetrated_armor'].stacks : 0; return Math.max(0, base - penStacks * 10); } } function spawnWave(level, difficulty, count, includeBoss) { const diffMult = 1 + difficulty * 0.15; const monsters = []; const pool = MONSTERS.low; if (includeBoss && Math.random() < 0.02) { const boss = MONSTERS.bosses[Math.floor(Math.random() * MONSTERS.bosses.length)]; monsters.push(new Monster(boss, level, diffMult)); } while (monsters.length < count) { const t = pool[Math.floor(Math.random() * pool.length)]; monsters.push(new Monster(t, level, diffMult)); } return monsters; } module.exports = { SimPlayer, Monster, MONSTERS, spawnWave };