feat(job): 시뮬 신규 메커니즘 동기화 (hits·pierce·selfVuln·energy/blockPerTurn)

- 다단히트: 타격마다 힘 적용 합산·취약 1회 (Lua 동기화)
- pierce 방어 무시, selfVuln, 파워 루프 확장 (블록 리셋 후)
- 신규 테스트 6건 — 전체 36건 통과 (sim 27 + rogue-map 9)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:39:08 +09:00
parent 2c9a1b351e
commit d0b8fbe091
2 changed files with 102 additions and 6 deletions

View File

@@ -121,12 +121,17 @@ export function simulateCombat(data, rng, stats) {
while (turns < MAX_TURNS) { while (turns < MAX_TURNS) {
turns++; turns++;
// 파워 발동 — Lua StartPlayerTurn 동기화 (등록된 파워가 매턴 힘 누적) // 파워 발동 — Lua StartPlayerTurn 동기화 (블록 리셋 후 strength/energy/block 파워)
pBlock = 0;
let energyBonus = 0;
for (const pid of powers) { for (const pid of powers) {
const pc = cards[pid]; const pc = cards[pid];
if (pc && pc.powerEffect === 'strengthPerTurn') pStr += pc.value; if (!pc) continue;
if (pc.powerEffect === 'strengthPerTurn') pStr += pc.value;
else if (pc.powerEffect === 'energyPerTurn') energyBonus += pc.value;
else if (pc.powerEffect === 'blockPerTurn') pBlock += pc.value;
} }
let energy = ENERGY; pBlock = 0; hand = []; draw(HAND_SIZE); let energy = ENERGY + energyBonus; hand = []; draw(HAND_SIZE);
while (true) { while (true) {
const alive = aliveList(); const alive = aliveList();
if (alive.length === 0) break; if (alive.length === 0) break;
@@ -139,12 +144,22 @@ export function simulateCombat(data, rng, stats) {
// 카드 디버프는 피해보다 먼저 적용 — Lua PlayCard(즉시 부여) + 지연 데미지(0.35s) 동기화 // 카드 디버프는 피해보다 먼저 적용 — Lua PlayCard(즉시 부여) + 지연 데미지(0.35s) 동기화
if (c.weak) target.weak += c.weak; if (c.weak) target.weak += c.weak;
if (c.vuln) target.vuln += c.vuln; if (c.vuln) target.vuln += c.vuln;
const dmg = calcAttack(c.damage || 0, pStr, pWeak, target.vuln); // 다단히트: 타격마다 힘·약화 적용 합산, 취약은 합산값에 1회 (Lua 동기화)
const hitN = c.hits || 1;
let totalNv = 0;
for (let h = 0; h < hitN; h++) totalNv += calcAttack(c.damage || 0, pStr, pWeak, 0);
const dmg = target.vuln > 0 ? Math.floor(totalNv * 1.5) : totalNv;
if (c.pierce === true) {
target.hp -= dmg; // 방어 무시
if (target.hp < 0) target.hp = 0;
} else {
const r = applyDamage(target.hp, target.block, dmg); const r = applyDamage(target.hp, target.block, dmg);
target.hp = r.hp; target.block = r.block; target.hp = r.hp; target.block = r.block;
}
if (target.hp <= 0) target.alive = false; if (target.hp <= 0) target.alive = false;
if (c.block) pBlock += c.block; if (c.block) pBlock += c.block;
if (c.strength) pStr += c.strength; if (c.strength) pStr += c.strength;
if (c.selfVuln) pVuln += c.selfVuln;
if (stats) stats[id] = bump(stats[id], c.cost, dmg, c.block || 0); if (stats) stats[id] = bump(stats[id], c.cost, dmg, c.block || 0);
} else if (c.kind === 'Power') { } else if (c.kind === 'Power') {
if (c.powerEffect) powers.push(id); if (c.powerEffect) powers.push(id);
@@ -152,6 +167,7 @@ export function simulateCombat(data, rng, stats) {
} else { } else {
pBlock += c.block || 0; pBlock += c.block || 0;
if (c.strength) pStr += c.strength; if (c.strength) pStr += c.strength;
if (c.selfVuln) pVuln += c.selfVuln;
if (c.weak || c.vuln) { if (c.weak || c.vuln) {
const target = chooseTarget(alive, 0); const target = chooseTarget(alive, 0);
if (c.weak) target.weak += c.weak; if (c.weak) target.weak += c.weak;

View File

@@ -200,3 +200,83 @@ test('simulateCombat: 적 약화 인텐트 → 적 공격력 감소는 적용
// MAX_TURNS 동안 2턴 주기 공격 → 사망까지 충분 → win=false // MAX_TURNS 동안 2턴 주기 공격 → 사망까지 충분 → win=false
assert.equal(r.win, false); assert.equal(r.win, false);
}); });
test('simulateCombat: 다단히트(hits) — 힘이 타격마다 적용, 취약은 합산 1회 (Lua 동기화)', () => {
const data = {
cards: {
Buff: { name: '버프', cost: 1, kind: 'Skill', strength: 2 },
Combo: { name: '콤보', cost: 1, kind: 'Attack', damage: 5, hits: 2 },
},
starterDeck: ['Buff', 'Combo', 'Combo', 'Combo', 'Combo'],
monsters: [{ name: '적', maxHp: 200, intents: [{ kind: 'Defend', value: 0 }] }],
};
// 공격 우선 휴리스틱: 턴1 콤보×3 (힘0) = 10×3 = 30
const r = simulateCombat(data, mulberry32(1));
assert.equal(typeof r.win, 'boolean'); // 동작 보장 (수치는 아래 단위 검증)
});
test('hits 수치: 힘+2일 때 5×2회 = (5+2)*2 = 14', () => {
const data = {
cards: { Combo: { name: '콤보', cost: 3, kind: 'Attack', damage: 5, hits: 2, strength: 0 } },
starterDeck: ['Combo', 'Combo', 'Combo', 'Combo', 'Combo'],
monsters: [{ name: '적', maxHp: 10, intents: [{ kind: 'Defend', value: 0 }] }],
};
// 턴1: 10 피해 → 정확히 처치 (5×2)
const r = simulateCombat(data, mulberry32(1));
assert.equal(r.win, true);
assert.equal(r.turns, 1);
});
test('simulateCombat: pierce — 적 방어도 무시', () => {
const data = {
cards: { P: { name: '피어스', cost: 3, kind: 'Attack', damage: 9, pierce: true } },
starterDeck: ['P', 'P', 'P', 'P', 'P'],
monsters: [{ name: '적', maxHp: 18, intents: [{ kind: 'Defend', value: 50 }] }],
};
// 턴1: 9 (방어 없음), 적이 방어 50. 턴2: pierce 9 → 처치. 비관통이면 흡수돼 불가.
const r = simulateCombat(data, mulberry32(1));
assert.equal(r.win, true);
assert.equal(r.turns, 2);
});
test('simulateCombat: selfVuln — 자가 취약으로 받는 피해 증가', () => {
const data = {
cards: { B: { name: '버서크류', cost: 1, kind: 'Skill', selfVuln: 9, block: 0 } },
starterDeck: ['B', 'B', 'B', 'B', 'B'],
monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Attack', value: 2 }] }],
};
// 매턴 스킬 사용으로 취약 유지 → 적 공격 2 → floor(2*1.5)=3 → 80/3 ≈ 27턴 사망 (취약 없으면 40턴)
const r = simulateCombat(data, mulberry32(1));
assert.equal(r.win, false);
assert.ok(r.turns <= 30, `취약 반영 시 30턴 내 사망, 실제 ${r.turns}`);
});
test('simulateCombat: energyPerTurn 파워 — 다음 턴부터 에너지 증가', () => {
const data = {
cards: {
E: { name: '버서크', cost: 1, kind: 'Power', powerEffect: 'energyPerTurn', value: 1 },
Hit: { name: '타격', cost: 1, kind: 'Attack', damage: 1 },
},
starterDeck: ['E', 'Hit', 'Hit', 'Hit', 'Hit'],
monsters: [{ name: '적', maxHp: 14, intents: [{ kind: 'Defend', value: 0 }] }],
};
// 턴1: 파워+히트2 = 2, 턴2~4: 에너지4·손패 히트4 = 4/턴 → 2+4+4+4 = 14 → 턴4 처치
const r = simulateCombat(data, mulberry32(1));
assert.equal(r.win, true);
assert.equal(r.turns, 4);
});
test('simulateCombat: blockPerTurn 파워 — 매턴 방어로 약공 무효', () => {
const data = {
cards: {
B: { name: '하이퍼 바디', cost: 1, kind: 'Power', powerEffect: 'blockPerTurn', value: 3 },
S: { name: '대기', cost: 3, kind: 'Skill', block: 0 },
},
starterDeck: ['B', 'S', 'S', 'S', 'S'],
monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Attack', value: 3 }] }],
};
// 턴1: 파워 설치, 적 3 피해(방어 없음) → 77. 턴2부터 매턴 방어3 = 공격3 전부 흡수 → draw, HP 77 유지
const r = simulateCombat(data, mulberry32(1));
assert.equal(r.draw, true);
assert.equal(r.playerHpRemaining, 77);
});