feat(job): 다단히트·방어무시·자가취약·파워 2종 (생성기)

- PlayCard Attack: hits 합산(힘·펜닙 타격마다 적용), pierce 전달, selfVuln
- DealDamageToTarget/PlayAttackFx pierce 시그니처 (물약 화염병 false)
- StartPlayerTurn 파워 루프: energyPerTurn·blockPerTurn (블록 리셋 후)
- 카드 class 직렬화(누락 시 fail-fast)·PlayerJob prop

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:34:11 +09:00
parent 15b342972d
commit 74e3a70a19

View File

@@ -68,6 +68,11 @@ function luaCardsTable(cards) {
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`); if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`); if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
if (c.value != null) fields.push(`value = ${c.value}`); if (c.value != null) fields.push(`value = ${c.value}`);
if (!c.class) throw new Error(`[gen-slaydeck] 카드 ${id}에 class 누락`);
fields.push(`class = ${luaStr(c.class)}`);
if (c.hits != null) fields.push(`hits = ${c.hits}`);
if (c.pierce === true) fields.push('pierce = true');
if (c.selfVuln != null) fields.push(`selfVuln = ${c.selfVuln}`);
if (c.image != null) fields.push(`image = ${luaStr(c.image)}`); if (c.image != null) fields.push(`image = ${luaStr(c.image)}`);
return `\t${id} = { ${fields.join(', ')} },`; return `\t${id} = { ${fields.join(', ')} },`;
}); });
@@ -2237,6 +2242,7 @@ function writeCodeblocks() {
prop('number', 'Depth', '0'), prop('number', 'Depth', '0'),
prop('any', 'VisitedNodes'), prop('any', 'VisitedNodes'),
prop('boolean', 'ChestOpened', 'false'), prop('boolean', 'ChestOpened', 'false'),
prop('string', 'PlayerJob', '""'),
], [ ], [
method('OnBeginPlay', `self:ShowMainMenu()`), method('OnBeginPlay', `self:ShowMainMenu()`),
method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false) method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false)
@@ -2346,6 +2352,7 @@ self.RelicPool = { ${RELICS.relicPool.map(luaStr).join(', ')} }
${luaEnemiesTable(ENEMIES.enemies)} ${luaEnemiesTable(ENEMIES.enemies)}
self.CurrentNodeId = "" self.CurrentNodeId = ""
self.CurrentEnemyId = "" self.CurrentEnemyId = ""
self.PlayerJob = ""
self:GenerateMap() self:GenerateMap()
self:BindButtons() self:BindButtons()
self:AddRelic("${RELICS.startingRelic}") self:AddRelic("${RELICS.startingRelic}")
@@ -2612,19 +2619,25 @@ end`),
method('StartPlayerTurn', `self.Turn = self.Turn + 1 method('StartPlayerTurn', `self.Turn = self.Turn + 1
self.Energy = self.MaxEnergy self.Energy = self.MaxEnergy
self:ApplyRelics("turnStart") self:ApplyRelics("turnStart")
if self.PlayerPowers ~= nil then
for i = 1, #self.PlayerPowers do
local pc = self.Cards[self.PlayerPowers[i]]
if pc ~= nil and pc.powerEffect == "strengthPerTurn" then
self.PlayerStr = self.PlayerStr + pc.value
end
end
end
self.PlayerBlock = 0 self.PlayerBlock = 0
if self.ClayBlockNext > 0 then if self.ClayBlockNext > 0 then
self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext self.PlayerBlock = self.PlayerBlock + self.ClayBlockNext
self.ClayBlockNext = 0 self.ClayBlockNext = 0
end end
if self.PlayerPowers ~= nil then
for i = 1, #self.PlayerPowers do
local pc = self.Cards[self.PlayerPowers[i]]
if pc ~= nil then
if pc.powerEffect == "strengthPerTurn" then
self.PlayerStr = self.PlayerStr + pc.value
elseif pc.powerEffect == "energyPerTurn" then
self.Energy = self.Energy + pc.value
elseif pc.powerEffect == "blockPerTurn" then
self.PlayerBlock = self.PlayerBlock + pc.value
end
end
end
end
self:DrawCards(5) self:DrawCards(5)
self:RenderHand(true) self:RenderHand(true)
self:RenderCombat()`), self:RenderCombat()`),
@@ -2889,7 +2902,12 @@ end
self.Energy = self.Energy - c.cost self.Energy = self.Energy - c.cost
if c.kind == "Attack" then if c.kind == "Attack" then
if c.damage ~= nil then if c.damage ~= nil then
self:PlayAttackFx(self.TargetIndex, c.image, self:CalcPlayerAttack(c.damage)) local total = 0
local hitN = c.hits or 1
for h = 1, hitN do
total = total + self:CalcPlayerAttack(c.damage)
end
self:PlayAttackFx(self.TargetIndex, c.image, total, c.pierce == true)
end end
if c.block ~= nil then if c.block ~= nil then
self.PlayerBlock = self.PlayerBlock + c.block self.PlayerBlock = self.PlayerBlock + c.block
@@ -2907,6 +2925,9 @@ end
if c.strength ~= nil then if c.strength ~= nil then
self.PlayerStr = self.PlayerStr + c.strength self.PlayerStr = self.PlayerStr + c.strength
end end
if c.selfVuln ~= nil then
self.PlayerVuln = self.PlayerVuln + c.selfVuln
end
if c.weak ~= nil or c.vuln ~= nil then if c.weak ~= nil or c.vuln ~= nil then
local tm = self.Monsters[self.TargetIndex] local tm = self.Monsters[self.TargetIndex]
if tm ~= nil and tm.alive == true then if tm ~= nil and tm.alive == true then
@@ -3014,7 +3035,7 @@ local dmg = amount
if m.vuln > 0 then if m.vuln > 0 then
dmg = math.floor(dmg * 1.5) dmg = math.floor(dmg * 1.5)
end end
if m.block > 0 then if m.block > 0 and pierce ~= true then
local absorbed = math.min(m.block, dmg) local absorbed = math.min(m.block, dmg)
m.block = m.block - absorbed m.block = m.block - absorbed
dmg = dmg - absorbed dmg = dmg - absorbed
@@ -3023,10 +3044,13 @@ m.hp = m.hp - dmg
if m.hp <= 0 then if m.hp <= 0 then
m.hp = 0 m.hp = 0
self:KillMonster(m.slot) self:KillMonster(m.slot)
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]), end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
]),
method('PlayAttackFx', `local m = self.Monsters[targetIndex] method('PlayAttackFx', `local m = self.Monsters[targetIndex]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
self:DealDamageToTarget(damage) self:DealDamageToTarget(damage, pierce)
self:RenderCombat() self:RenderCombat()
self:CheckCombatEnd() self:CheckCombatEnd()
return return
@@ -3052,7 +3076,7 @@ _TimerService:SetTimerOnce(function()
if mt ~= nil and mt.alive == true and mt.vuln > 0 then if mt ~= nil and mt.alive == true and mt.vuln > 0 then
shown = math.floor(damage * 1.5) shown = math.floor(damage * 1.5)
end end
self:DealDamageToTarget(damage) self:DealDamageToTarget(damage, pierce)
self:ShowDmgPop(targetIndex, shown) self:ShowDmgPop(targetIndex, shown)
self:RenderCombat() self:RenderCombat()
self:CheckCombatEnd() self:CheckCombatEnd()
@@ -3060,6 +3084,7 @@ end, 0.35)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'targetIndex' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'targetIndex' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' }, { Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
]), ]),
method('KillMonster', `local m = self.Monsters[slot] method('KillMonster', `local m = self.Monsters[slot]
if m == nil then if m == nil then
@@ -3530,7 +3555,7 @@ end
if p.effect == "heal" then if p.effect == "heal" then
self.PlayerHp = math.min(self.PlayerHp + p.value, self.PlayerMaxHp) self.PlayerHp = math.min(self.PlayerHp + p.value, self.PlayerMaxHp)
elseif p.effect == "damage" then elseif p.effect == "damage" then
self:DealDamageToTarget(p.value) self:DealDamageToTarget(p.value, false)
self:ShowDmgPop(self.TargetIndex, p.value) self:ShowDmgPop(self.TargetIndex, p.value)
elseif p.effect == "strength" then elseif p.effect == "strength" then
self.PlayerStr = self.PlayerStr + p.value self.PlayerStr = self.PlayerStr + p.value