balance: 전사 계열 5섹션 성장 곡선 조정
This commit is contained in:
@@ -6,7 +6,10 @@ import {
|
||||
simulateCombat,
|
||||
} from './sim-balance.mjs';
|
||||
|
||||
const ROGUE_CLASSES = new Set(['rogue', 'thief', 'thiefmaster', 'assassin', 'hermit']);
|
||||
const AUDITED_CLASSES = new Set([
|
||||
'rogue', 'thief', 'thiefmaster', 'assassin', 'hermit',
|
||||
'warrior', 'fighter', 'crusader', 'page', 'knight',
|
||||
]);
|
||||
|
||||
const CONTEXT_DECKS = {
|
||||
rogue: [
|
||||
@@ -38,6 +41,27 @@ const CONTEXT_DECKS = {
|
||||
'Survivor', 'LeadingStrike', 'BladeDance', 'JavelinAcceleration',
|
||||
'JavelinMastery', 'TripleThrow', 'SpiritJavelin', 'SkilledJavelin',
|
||||
],
|
||||
warrior: [
|
||||
'Strike', 'Strike', 'Strike', 'Strike',
|
||||
'Defend', 'Defend', 'Defend', 'Defend',
|
||||
'Bash', 'SlashBlast', 'IronBody', 'WarriorMastery',
|
||||
],
|
||||
fighter: [
|
||||
'Strike', 'Strike', 'Strike', 'Defend', 'Defend', 'Defend',
|
||||
'Bash', 'SlashBlast', 'ComboAttack', 'Brandish', 'WeaponMastery', 'FlashSlash',
|
||||
],
|
||||
crusader: [
|
||||
'Strike', 'Strike', 'Defend', 'Defend', 'Bash', 'SlashBlast',
|
||||
'ComboAttack', 'Brandish', 'WeaponMastery', 'BraveSlash', 'ComboSynergy', 'Rush',
|
||||
],
|
||||
page: [
|
||||
'Strike', 'Strike', 'Strike', 'Defend', 'Defend', 'Defend',
|
||||
'Bash', 'SlashBlast', 'HolyCharge', 'DivineSwing', 'PageOrder', 'PageStance',
|
||||
],
|
||||
knight: [
|
||||
'Strike', 'Strike', 'Defend', 'Defend', 'Bash', 'SlashBlast',
|
||||
'HolyCharge', 'DivineSwing', 'PageOrder', 'DivineCharge', 'KnightRush', 'Restoration',
|
||||
],
|
||||
};
|
||||
|
||||
const ENCOUNTER_SCALE = {
|
||||
@@ -46,6 +70,11 @@ const ENCOUNTER_SCALE = {
|
||||
assassin: { hp: 2.25, attack: 1.65 },
|
||||
thiefmaster: { hp: 2.4, attack: 1.5 },
|
||||
hermit: { hp: 2.6, attack: 1.65 },
|
||||
warrior: { hp: 1.9, attack: 1.5 },
|
||||
fighter: { hp: 2.2, attack: 1.6 },
|
||||
crusader: { hp: 2.6, attack: 1.7 },
|
||||
page: { hp: 2.2, attack: 1.6 },
|
||||
knight: { hp: 2.6, attack: 1.7 },
|
||||
};
|
||||
|
||||
const median = (values) => {
|
||||
@@ -172,7 +201,7 @@ export function auditCardEfficiency({ runs = 300, seed = 20260701 } = {}) {
|
||||
|
||||
const rows = [];
|
||||
for (const [id, card] of Object.entries(cards)) {
|
||||
if (!ROGUE_CLASSES.has(card.class)) continue;
|
||||
if (!AUDITED_CLASSES.has(card.class)) continue;
|
||||
const deck = CONTEXT_DECKS[card.class].slice();
|
||||
deck[replacementIndex(deck, cards, card)] = id;
|
||||
const result = simulateDeck(scaledEncounter(data, card.class), deck, runs, seed, id);
|
||||
@@ -204,7 +233,7 @@ function formatPercent(value) {
|
||||
|
||||
export function formatEfficiencyReport(report) {
|
||||
const lines = [];
|
||||
lines.push(`도적 카드 효율 검증: 카드 ${report.rows.length}장, 카드당 ${report.runs}회`);
|
||||
lines.push(`카드 효율 검증: 카드 ${report.rows.length}장, 카드당 ${report.runs}회`);
|
||||
lines.push('기준 덱:');
|
||||
for (const [classId, baseline] of Object.entries(report.baselines)) {
|
||||
lines.push(` ${classId}: 승률 ${formatPercent(baseline.winRate)}, 평균 ${baseline.avgTurns.toFixed(2)}턴, 승리 HP ${baseline.avgHpOnWin.toFixed(1)}`);
|
||||
|
||||
@@ -6,7 +6,7 @@ const cardsData = JSON.parse(readFileSync('data/cards.json', 'utf8'));
|
||||
const enemiesData = JSON.parse(readFileSync('data/enemies.json', 'utf8'));
|
||||
const relicsData = JSON.parse(readFileSync('data/relics.json', 'utf8'));
|
||||
|
||||
const PLAYER_MAX_HP = 70;
|
||||
const PLAYER_MAX_HP = { rogue: 70, warrior: 80 };
|
||||
const REST_HEAL = 30;
|
||||
const SECTION_COUNT = 5;
|
||||
const NORMAL_FIGHTS = 4;
|
||||
@@ -18,6 +18,8 @@ const BOSS_POOL = ['king_slime', 'slime_boss'];
|
||||
const JOBS = {
|
||||
thief: { tier2: 'thief', tier3: 'thiefmaster', tier2Starter: 'DaggerAcceleration', tier3Starter: 'Venom' },
|
||||
assassin: { tier2: 'assassin', tier3: 'hermit', tier2Starter: 'JavelinAcceleration', tier3Starter: 'SpiritJavelin' },
|
||||
fighter: { root: 'warrior', tier2: 'fighter', tier3: 'crusader', tier2Starter: 'ComboAttack', tier3Starter: 'ComboSynergy' },
|
||||
page: { root: 'warrior', tier2: 'page', tier3: 'knight', tier2Starter: 'HolyCharge', tier3Starter: 'DivineCharge' },
|
||||
};
|
||||
|
||||
const LINEAGES = {
|
||||
@@ -26,12 +28,17 @@ const LINEAGES = {
|
||||
thiefmaster: ['rogue', 'thief', 'thiefmaster'],
|
||||
assassin: ['rogue', 'assassin'],
|
||||
hermit: ['rogue', 'assassin', 'hermit'],
|
||||
warrior: ['warrior'],
|
||||
fighter: ['warrior', 'fighter'],
|
||||
crusader: ['warrior', 'fighter', 'crusader'],
|
||||
page: ['warrior', 'page'],
|
||||
knight: ['warrior', 'page', 'knight'],
|
||||
};
|
||||
|
||||
const pick = (rng, values) => values[Math.floor(rng() * values.length)];
|
||||
|
||||
export function campaignJobAtSection(branch, section) {
|
||||
if (section <= 1) return 'rogue';
|
||||
if (section <= 1) return JOBS[branch].root || 'rogue';
|
||||
if (section === 2) return JOBS[branch].tier2;
|
||||
return JOBS[branch].tier3;
|
||||
}
|
||||
@@ -102,11 +109,22 @@ function branchCardValue(card, branch, deck, id) {
|
||||
value += card.sly ? 5 : 0;
|
||||
value += (card.discard || 0) * 2 + (card.drawPerDiscarded || 0) * 4;
|
||||
value += (card.poisonApplicationBurstDamage || 0) * 1.5;
|
||||
} else {
|
||||
} else if (branch === 'assassin') {
|
||||
value += (card.addShiv || 0) * 3 + (card.turnStartShiv || 0) * 8;
|
||||
value += (card.shivDamageBonus || 0) * 6 + (card.firstShivDamageBonus || 0) * 3;
|
||||
value += card.shivAoe ? 12 : 0;
|
||||
value += card.shivRetain ? 5 : 0;
|
||||
} else if (branch === 'fighter') {
|
||||
value += (card.hits || 1) * 1.5;
|
||||
value += (card.comboGain || 0) * 5 + (card.comboOnAttack || 0) * 10;
|
||||
value += (card.damagePerCombo || 0) * 8 + (card.attackDamagePerCombo || 0) * 12;
|
||||
value += (card.attackPlayedDamage || 0) * 5;
|
||||
} else if (branch === 'page') {
|
||||
value += (card.block || 0) * 0.5 + (card.cardPlayedBlock || 0) * 7;
|
||||
value += (card.holyChargeOnHolyForce || 0) * 12 + (card.damagePerHolyCharge || 0) * 7;
|
||||
value += (card.blockPerHolyCharge || 0) * 6 + (card.healPerHolyCharge || 0) * 3;
|
||||
value += (card.damageTakenReduction || 0) * 40;
|
||||
value += (card.blockOnDamaged || 0) * 3 + (card.strengthOnDamagedOnce || 0) * 5;
|
||||
}
|
||||
const copies = deck.filter((cardId) => cardId === id).length;
|
||||
value -= copies * (card.kind === 'Power' ? 10 : 3);
|
||||
@@ -203,11 +221,13 @@ export function simulateCampaign(branch, rng, {
|
||||
minimumRewardValue = 10,
|
||||
} = {}) {
|
||||
if (!JOBS[branch]) throw new Error(`지원하지 않는 도적 분기: ${branch}`);
|
||||
const root = JOBS[branch].root || 'rogue';
|
||||
const maxHp = PLAYER_MAX_HP[root];
|
||||
const state = {
|
||||
hp: PLAYER_MAX_HP,
|
||||
maxHp: PLAYER_MAX_HP,
|
||||
deck: cardsData.starterDecks.rogue.slice(),
|
||||
job: 'rogue',
|
||||
hp: maxHp,
|
||||
maxHp,
|
||||
deck: cardsData.starterDecks[root].slice(),
|
||||
job: root,
|
||||
turns: 0,
|
||||
sectionCleared: 0,
|
||||
diedAt: '',
|
||||
@@ -306,7 +326,7 @@ function main() {
|
||||
else if (args[i] === '--scale-step') scaleStep = Number.parseFloat(args[++i]);
|
||||
else if (args[i] === '--reward-min') minimumRewardValue = Number.parseFloat(args[++i]);
|
||||
}
|
||||
for (const branch of ['thief', 'assassin']) {
|
||||
for (const branch of ['thief', 'assassin', 'fighter', 'page']) {
|
||||
console.log(formatCampaignReport(runCampaignBatch(branch, runs, seed, { restHeal, sectionHeal, scaleStep, minimumRewardValue })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@ test('도적 전직 시점: 1섹션 Rogue, 2섹션 2차, 3섹션부터 3차', ()
|
||||
test('3차 직업은 자기 계보 카드만 사용', () => {
|
||||
assert.deepEqual(playableClassesForJob('thiefmaster'), ['rogue', 'thief', 'thiefmaster']);
|
||||
assert.deepEqual(playableClassesForJob('hermit'), ['rogue', 'assassin', 'hermit']);
|
||||
assert.deepEqual(playableClassesForJob('crusader'), ['warrior', 'fighter', 'crusader']);
|
||||
assert.deepEqual(playableClassesForJob('knight'), ['warrior', 'page', 'knight']);
|
||||
});
|
||||
|
||||
test('전사 전직 시점: 1섹션 Warrior, 2섹션 2차, 3섹션부터 3차', () => {
|
||||
assert.equal(campaignJobAtSection('fighter', 1), 'warrior');
|
||||
assert.equal(campaignJobAtSection('fighter', 2), 'fighter');
|
||||
assert.equal(campaignJobAtSection('fighter', 3), 'crusader');
|
||||
assert.equal(campaignJobAtSection('page', 2), 'page');
|
||||
assert.equal(campaignJobAtSection('page', 5), 'knight');
|
||||
});
|
||||
|
||||
test('섹션 난이도는 3차 이후 더 빠르게 증가', () => {
|
||||
|
||||
Reference in New Issue
Block a user