feat(buffs-power): 버프/디버프·Power 전투 규칙 (생성기)

- 약화(-25%)·취약(+50%)·힘(+N) 피해 공식 양방향 적용 (CalcPlayerAttack·EnemyActStep)
- Power 카드: 사용 시 소멸, PlayerPowers 등록, 매턴 strengthPerTurn 발동
- 적 Debuff 인텐트 처리·디버프 턴 감소(플레이어 턴 종료·적 행동 후)
- 인텐트 effect 직렬화·막 배율은 Debuff 값 제외

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 01:28:03 +09:00
parent 2bf5716fce
commit 6df3fcf01c

View File

@@ -37,7 +37,11 @@ function luaRelicsTable(relics) {
}
function luaIntentsArray(intents) {
return '{ ' + intents.map((it) => `{ kind = ${luaStr(it.kind)}, value = ${it.value} }`).join(', ') + ' }';
return '{ ' + intents.map((it) => {
const fields = [`kind = ${luaStr(it.kind)}`, `value = ${it.value}`];
if (it.effect != null) fields.push(`effect = ${luaStr(it.effect)}`);
return `{ ${fields.join(', ')} }`;
}).join(', ') + ' }';
}
function luaEnemiesTable(enemies) {
const lines = Object.entries(enemies).map(([id, e]) =>
@@ -65,6 +69,11 @@ function luaCardsTable(cards) {
const fields = [`name = ${luaStr(c.name)}`, `cost = ${c.cost}`, `desc = ${luaStr(c.desc)}`, `kind = ${luaStr(c.kind)}`];
if (c.damage != null) fields.push(`damage = ${c.damage}`);
if (c.block != null) fields.push(`block = ${c.block}`);
if (c.strength != null) fields.push(`strength = ${c.strength}`);
if (c.weak != null) fields.push(`weak = ${c.weak}`);
if (c.vuln != null) fields.push(`vuln = ${c.vuln}`);
if (c.powerEffect != null) fields.push(`powerEffect = ${luaStr(c.powerEffect)}`);
if (c.value != null) fields.push(`value = ${c.value}`);
if (c.image != null) fields.push(`image = ${luaStr(c.image)}`);
return `\t${id} = { ${fields.join(', ')} },`;
});
@@ -1888,6 +1897,10 @@ function writeCodeblocks() {
prop('number', 'DragSlot', '0'),
prop('boolean', 'FxBusy', 'false'),
prop('boolean', 'TurnBusy', 'false'),
prop('number', 'PlayerStr', '0'),
prop('number', 'PlayerWeak', '0'),
prop('number', 'PlayerVuln', '0'),
prop('any', 'PlayerPowers'),
], [
method('OnBeginPlay', `self:ShowMainMenu()`),
method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false)
@@ -2001,6 +2014,10 @@ self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false)
self.MaxEnergy = 3
self.Turn = 0
self.PlayerBlock = 0
self.PlayerStr = 0
self.PlayerWeak = 0
self.PlayerVuln = 0
self.PlayerPowers = {}
self.CombatOver = false
self.DiscardPile = {}
self.Hand = {}
@@ -2062,11 +2079,16 @@ for i = 1, n do
if e == nil then e = { name = item.enemyId, maxHp = 10, intents = { { kind = "Attack", value = 5 } } } end
local intents = {}
for k = 1, #e.intents do
intents[k] = { kind = e.intents[k].kind, value = math.floor(e.intents[k].value * mult) }
local v = e.intents[k].value
if e.intents[k].kind ~= "Debuff" then
v = math.floor(v * mult)
end
intents[k] = { kind = e.intents[k].kind, value = v, effect = e.intents[k].effect }
end
local maxHp = math.floor(e.maxHp * mult)
self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,
hp = maxHp, maxHp = maxHp, block = 0, intents = intents, intentIdx = 1, alive = true, slot = i }
hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0,
intents = intents, intentIdx = 1, alive = true, slot = i }
self:ReviveMonsterEntity(item.entity)
self:PositionMonsterSlot(i)
end
@@ -2184,6 +2206,14 @@ end`),
method('StartPlayerTurn', `self.Turn = self.Turn + 1
self.Energy = self.MaxEnergy
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:DrawCards(5)
self:RenderHand(true)
@@ -2195,6 +2225,8 @@ for i = 1, #self.Hand do
\ttable.insert(self.DiscardPile, self.Hand[i])
end
self.Hand = {}
if self.PlayerWeak > 0 then self.PlayerWeak = self.PlayerWeak - 1 end
if self.PlayerVuln > 0 then self.PlayerVuln = self.PlayerVuln - 1 end
self:RenderHand(false)
self:RenderPiles()
self:EnemyTurn()`),
@@ -2351,10 +2383,10 @@ local e = _EntityService:GetEntityByPath(base)
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
if c.kind == "Attack" then
e.SpriteGUIRendererComponent.Color = Color(0.86, 0.42, 0.38, 1)
elseif c.kind == "Skill" then
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
else
elseif c.kind == "Power" then
e.SpriteGUIRendererComponent.Color = Color(0.46, 0.68, 0.52, 1)
else
e.SpriteGUIRendererComponent.Color = Color(0.42, 0.55, 0.85, 1)
end
end
self:SetText(base .. "/Cost", string.format("%d", c.cost))
@@ -2407,6 +2439,14 @@ end, 1 / 60)`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toPos' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'duration' },
]),
method('CalcPlayerAttack', `local dmg = base + self.PlayerStr
if self.PlayerWeak > 0 then
dmg = math.floor(dmg * 0.75)
end
if dmg < 0 then
dmg = 0
end
return dmg`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
method('PlayCard', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return
end
@@ -2428,7 +2468,7 @@ end
self.Energy = self.Energy - c.cost
if c.kind == "Attack" then
if c.damage ~= nil then
self:PlayAttackFx(self.TargetIndex, c.image, c.damage)
self:PlayAttackFx(self.TargetIndex, c.image, self:CalcPlayerAttack(c.damage))
end
if c.block ~= nil then
self.PlayerBlock = self.PlayerBlock + c.block
@@ -2438,9 +2478,25 @@ elseif c.kind == "Skill" then
if c.block ~= nil then
self.PlayerBlock = self.PlayerBlock + c.block
end
elseif c.kind == "Power" then
if c.powerEffect ~= nil then
table.insert(self.PlayerPowers, cardId)
end
end
if c.strength ~= nil then
self.PlayerStr = self.PlayerStr + c.strength
end
if c.weak ~= nil or c.vuln ~= nil then
local tm = self.Monsters[self.TargetIndex]
if tm ~= nil and tm.alive == true then
if c.weak ~= nil then tm.weak = tm.weak + c.weak end
if c.vuln ~= nil then tm.vuln = tm.vuln + c.vuln end
end
end
table.remove(self.Hand, slot)
table.insert(self.DiscardPile, cardId)
if c.kind ~= "Power" then
table.insert(self.DiscardPile, cardId)
end
self:RenderHand(false)
self:RenderPiles()
self:RenderCombat()
@@ -2529,6 +2585,9 @@ if m == nil then
return
end
local dmg = amount
if m.vuln > 0 then
dmg = math.floor(dmg * 1.5)
end
if m.block > 0 then
local absorbed = math.min(m.block, dmg)
m.block = m.block - absorbed
@@ -2562,8 +2621,13 @@ end
_TimerService:SetTimerOnce(function()
if fx ~= nil then fx.Enable = false end
self.FxBusy = false
local shown = damage
local mt = self.Monsters[targetIndex]
if mt ~= nil and mt.alive == true and mt.vuln > 0 then
shown = math.floor(damage * 1.5)
end
self:DealDamageToTarget(damage)
self:ShowDmgPop(targetIndex, damage)
self:ShowDmgPop(targetIndex, shown)
self:RenderCombat()
self:CheckCombatEnd()
end, 0.35)`, [
@@ -2612,17 +2676,32 @@ _TimerService:SetTimerOnce(function()
local intent = m.intents[m.intentIdx]
if intent ~= nil then
if intent.kind == "Attack" then
local atk = intent.value + m.str
if m.weak > 0 then
atk = math.floor(atk * 0.75)
end
if self.PlayerVuln > 0 then
atk = math.floor(atk * 1.5)
end
local before = self.PlayerHp
self:DealDamageToPlayer(intent.value)
self:DealDamageToPlayer(atk)
self:ShowPlayerDmgPop(before - self.PlayerHp)
elseif intent.kind == "Defend" then
m.block = m.block + intent.value
elseif intent.kind == "Debuff" then
if intent.effect == "weak" then
self.PlayerWeak = self.PlayerWeak + intent.value
elseif intent.effect == "vuln" then
self.PlayerVuln = self.PlayerVuln + intent.value
end
end
end
m.intentIdx = m.intentIdx + 1
if m.intentIdx > #m.intents then
m.intentIdx = 1
end
if m.weak > 0 then m.weak = m.weak - 1 end
if m.vuln > 0 then m.vuln = m.vuln - 1 end
self:RenderCombat()
self:SetEntityEnabled(base .. "/ActFrame", false)
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)