From 8ff50b428da32f9b203a9e52cbbd940ae6b6c11c Mon Sep 17 00:00:00 2001 From: gahusb Date: Thu, 11 Jun 2026 09:01:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(system-gaps):=20=EB=B3=B5=ED=95=A9=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C(=ED=94=BC=ED=95=B4+=EB=B0=A9=EC=96=B4)=C2=B7?= =?UTF-8?q?=EB=9F=B0=20=EC=A2=85=EB=A3=8C=20=ED=9B=84=20=EB=A9=94=EB=89=B4?= =?UTF-8?q?=20=EB=B3=B5=EA=B7=80(EndRun)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- tools/balance/sim-balance.mjs | 3 ++- tools/balance/sim-balance.test.mjs | 13 +++++++++++++ tools/deck/gen-slaydeck.mjs | 12 ++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/tools/balance/sim-balance.mjs b/tools/balance/sim-balance.mjs index b5a244f..209e970 100644 --- a/tools/balance/sim-balance.mjs +++ b/tools/balance/sim-balance.mjs @@ -120,7 +120,8 @@ export function simulateCombat(data, rng, stats) { const r = applyDamage(target.hp, target.block, c.damage || 0); target.hp = r.hp; target.block = r.block; if (target.hp <= 0) target.alive = false; - if (stats) stats[id] = bump(stats[id], c.cost, c.damage || 0, 0); + if (c.block) pBlock += c.block; + if (stats) stats[id] = bump(stats[id], c.cost, c.damage || 0, c.block || 0); } else { pBlock += c.block || 0; if (stats) stats[id] = bump(stats[id], c.cost, 0, c.block || 0); diff --git a/tools/balance/sim-balance.test.mjs b/tools/balance/sim-balance.test.mjs index 1866805..6401afd 100644 --- a/tools/balance/sim-balance.test.mjs +++ b/tools/balance/sim-balance.test.mjs @@ -118,3 +118,16 @@ test('runBatch: 집계 필드·승률 범위', () => { test('runBatch: 동일 시드 동일 결과', () => { assert.deepEqual(runBatch(100, 7), runBatch(100, 7)); }); + +test('simulateCombat: 복합 카드(공격+방어) 블록이 적 공격을 흡수', () => { + const data = { + cards: { Combo: { name: '콤보', cost: 1, kind: 'Attack', damage: 1, block: 3 } }, + starterDeck: ['Combo', 'Combo', 'Combo', 'Combo', 'Combo'], + monsters: [{ name: '적', maxHp: 9999, intents: [{ kind: 'Attack', value: 9 }] }], + }; + const r = simulateCombat(data, mulberry32(1)); + // 매 턴 3장(에너지3) → 블록 9 = 적 공격 9 전부 흡수 → 무피해로 MAX_TURNS 도달(draw), HP 유지. + // 블록 미적용이면 매턴 -9로 사망(win=false, draw 아님). + assert.equal(r.draw, true); + assert.equal(r.playerHpRemaining, 80); +}); diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index 1c88cea..d5774b7 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -2430,6 +2430,9 @@ if c.kind == "Attack" then if c.damage ~= nil then self:PlayAttackFx(self.TargetIndex, c.image, c.damage) end + if c.block ~= nil then + self.PlayerBlock = self.PlayerBlock + c.block + end self:ApplyRelics("cardPlayed") elseif c.kind == "Skill" then if c.block ~= nil then @@ -2653,16 +2656,14 @@ if anyAlive == false then self:TeleportToActMap() self:ShowMap() else - self:ShowResult("런 클리어!") - self.RunActive = false + self:EndRun("런 클리어!") end else self:OfferReward() end elseif self.PlayerHp <= 0 then self.CombatOver = true - self:ShowResult("패배...") - self.RunActive = false + self:EndRun("패배...") end`), method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((m) => `"${m}"`).join(', ')} } local target = maps[self.Floor] @@ -2682,6 +2683,9 @@ local entity = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/Result if entity ~= nil then entity.Enable = true end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]), + method('EndRun', `self:ShowResult(text) +self.RunActive = false +_TimerService:SetTimerOnce(function() self:ShowMainMenu() end, 4)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'text' }]), method('RenderCombat', `for i = 1, ${MAX_MONSTERS} do local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) local m = self.Monsters[i]