Merge pull request '타격 시퀀스와 카메라 쉐이크 타이밍 정리' (#114) from codex/attack-sequence-feel-fix into main

Reviewed-on: #114
This commit was merged in pull request #114.
This commit is contained in:
2026-07-10 00:38:07 +09:00
6 changed files with 251 additions and 75 deletions

File diff suppressed because one or more lines are too long

View File

@@ -297,6 +297,7 @@ if m.block > 0 and pierce ~= true then
m.block = m.block - absorbed m.block = m.block - absorbed
dmg = dmg - absorbed dmg = dmg - absorbed
end end
self:ShakeCombatCamera()
m.hp = m.hp - dmg m.hp = m.hp - dmg
if dmg > 0 then if dmg > 0 then
local poison = self:AddPowerFieldTotal("attackPoison") local poison = self:AddPowerFieldTotal("attackPoison")
@@ -333,6 +334,7 @@ end
if m == nil then if m == nil then
return false return false
end end
self:ShakeCombatCamera()
m.hp = m.hp - amount m.hp = m.hp - amount
self:ShowDmgPop(m.slot, amount) self:ShowDmgPop(m.slot, amount)
self:MonsterHitMotion(m.slot) self:MonsterHitMotion(m.slot)
@@ -359,6 +361,7 @@ local m = alive[math.random(1, #alive)]
if m == nil then if m == nil then
return false return false
end end
self:ShakeCombatCamera()
m.hp = m.hp - amount m.hp = m.hp - amount
self:ShowDmgPop(m.slot, amount) self:ShowDmgPop(m.slot, amount)
self:MonsterHitMotion(m.slot) self:MonsterHitMotion(m.slot)
@@ -393,6 +396,17 @@ end`, [
method('DealDamageToAllMonsters', `if self.Monsters == nil then method('DealDamageToAllMonsters', `if self.Monsters == nil then
return false return false
end end
local anyAlive = false
for i = 1, #self.Monsters do
local m = self.Monsters[i]
if m ~= nil and m.alive == true then
anyAlive = true
break
end
end
if anyAlive == true and isAttack == true then
self:ShakeCombatCamera()
end
local killCount = 0 local killCount = 0
for i = 1, #self.Monsters do for i = 1, #self.Monsters do
local m = self.Monsters[i] local m = self.Monsters[i]
@@ -572,6 +586,7 @@ end
if dmg > 0 and self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 and dmg > 1 then if dmg > 0 and self.PlayerIntangible ~= nil and self.PlayerIntangible > 0 and dmg > 1 then
dmg = 1 dmg = 1
end end
self:ShakeCombatCamera()
if dmg > 0 then if dmg > 0 then
self.PlayerHp = self.PlayerHp - dmg self.PlayerHp = self.PlayerHp - dmg
local reactiveBlock = self:AddPowerFieldTotal("blockOnDamaged") local reactiveBlock = self:AddPowerFieldTotal("blockOnDamaged")

View File

@@ -313,6 +313,108 @@ if amount < 0 then
end end
self.PlayerBlock = self.PlayerBlock + amount self.PlayerBlock = self.PlayerBlock + amount
return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'), return amount`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'base' }], 0, 'number'),
method('RunAttackSequence', `if hitDamages == nil or #hitDamages <= 0 then
return
end
local function countAliveMonsters()
local n = 0
if self.Monsters ~= nil then
for mi = 1, #self.Monsters do
local om = self.Monsters[mi]
if om ~= nil and om.alive == true then
n = n + 1
end
end
end
return n
end
local function randomAliveMonsterIndex()
local alive = {}
if self.Monsters ~= nil then
for mi = 1, #self.Monsters do
local om = self.Monsters[mi]
if om ~= nil and om.alive == true then
table.insert(alive, mi)
end
end
end
if #alive <= 0 then
return 0
end
return alive[math.random(1, #alive)]
end
self.FxBusy = true
local hitIndex = 1
local lockedTargetIndex = self.TargetIndex or 0
local function finishSequence()
self.FxBusy = false
self.ActiveAttackDamageVsWeakMultiplier = 1
self:RenderCombat()
self:CheckCombatEnd()
end
local function step()
if self.CombatOver == true then
finishSequence()
return
end
if countAliveMonsters() <= 0 then
finishSequence()
return
end
if hitIndex == 1 then
self.DamageDealtThisTurn = (self.DamageDealtThisTurn or 0) + (roundPlannedDamage or 0)
end
local roundKilled = false
if useAoe == true then
roundKilled = self:DealDamageToAllMonsters(hitDamages[hitIndex] or 0, true)
else
local targetIdx = lockedTargetIndex
if randomTargetEachHit == true then
targetIdx = randomAliveMonsterIndex()
end
if targetIdx ~= nil and targetIdx > 0 then
if randomTargetEachHit ~= true then
local lockedMonster = nil
if self.Monsters ~= nil then
lockedMonster = self.Monsters[targetIdx]
end
if lockedMonster == nil or lockedMonster.alive ~= true then
finishSequence()
return
end
end
local prev = self.TargetIndex
self.TargetIndex = targetIdx
roundKilled = self:DealDamageToTarget(hitDamages[hitIndex] or 0, pierce == true)
self.TargetIndex = prev
end
end
self:RenderCombat()
self:CheckCombatEnd()
hitIndex = hitIndex + 1
if self.CombatOver == true then
finishSequence()
return
end
if hitIndex <= #hitDamages then
_TimerService:SetTimerOnce(step, 0.2)
return
end
if repeatOnKill == true and roundKilled == true and countAliveMonsters() > 0 then
hitIndex = 1
_TimerService:SetTimerOnce(step, 0.2)
return
end
finishSequence()
end
step()`, [
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'hitDamages' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'roundPlannedDamage' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'useAoe' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'randomTargetEachHit' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'repeatOnKill' },
]),
method('CountOtherHandSkills', `if self.Hand == nil then method('CountOtherHandSkills', `if self.Hand == nil then
return 0 return 0
end end
@@ -808,65 +910,7 @@ if c.kind == "Attack" then
if c.class == "shiv" and self.ShivFirstDamageBonusUsed ~= true and self:HasPowerField("firstShivDamageBonus") == true then if c.class == "shiv" and self.ShivFirstDamageBonusUsed ~= true and self:HasPowerField("firstShivDamageBonus") == true then
self.ShivFirstDamageBonusUsed = true self.ShivFirstDamageBonusUsed = true
end end
local function countAliveMonsters() self:RunAttackSequence(hitDamages, total, useAoe, c.randomTargetEachHit == true, c.pierce == true, c.repeatOnKill == true)
local n = 0
if self.Monsters ~= nil then
for mi = 1, #self.Monsters do
local om = self.Monsters[mi]
if om ~= nil and om.alive == true then n = n + 1 end
end
end
return n
end
local function randomAliveMonsterIndex()
local alive = {}
if self.Monsters ~= nil then
for mi = 1, #self.Monsters do
local om = self.Monsters[mi]
if om ~= nil and om.alive == true then
table.insert(alive, mi)
end
end
end
if #alive <= 0 then
return 0
end
return alive[math.random(1, #alive)]
end
local function resolveAttackRound()
local roundKilled = false
if useAoe == true then
local killed = self:DealDamageToAllMonsters(total, true)
if killed == true then roundKilled = true end
elseif c.randomTargetEachHit == true then
for h = 1, hitN do
local targetIdx = randomAliveMonsterIndex()
if targetIdx ~= nil and targetIdx > 0 then
local prev = self.TargetIndex
self.TargetIndex = targetIdx
local killed = self:DealDamageToTarget(hitDamages[h] or 0, c.pierce == true)
self.TargetIndex = prev
if killed == true then roundKilled = true end
end
end
elseif hitN > 1 then
for h = 1, hitN do
local killed = self:DealDamageToTarget(hitDamages[h] or 0, c.pierce == true)
if killed == true then roundKilled = true end
end
else
local killed = self:DealDamageToTarget(total, c.pierce == true)
if killed == true then roundKilled = true end
end
return roundKilled
end
local totalDamage = 0
local roundKilled = false
repeat
roundKilled = resolveAttackRound()
totalDamage = totalDamage + total
until c.repeatOnKill ~= true or roundKilled ~= true or countAliveMonsters() <= 0
self.DamageDealtThisTurn = (self.DamageDealtThisTurn or 0) + totalDamage
end end
if c.block ~= nil then if c.block ~= nil then
self:AddCardBlock(c.block + (self.HolyChargeCount or 0) * (c.blockPerHolyCharge or 0)) self:AddCardBlock(c.block + (self.HolyChargeCount or 0) * (c.blockPerHolyCharge or 0))

View File

@@ -113,7 +113,6 @@ local pop = _EntityService:GetEntityByPath(base)
if pop == nil then if pop == nil then
return return
end end
local startDelay = 0.2 * (popIndex - 1)
local function showNow() local function showNow()
self:SetEntityEnabled(base, false) self:SetEntityEnabled(base, false)
self:SetText(base, "") self:SetText(base, "")
@@ -193,13 +192,7 @@ local function showNow()
self:SetEntityEnabled(base, false) self:SetEntityEnabled(base, false)
end, 0.3) end, 0.3)
end end
if startDelay > 0 then showNow()`, [
_TimerService:SetTimerOnce(function()
showNow()
end, startDelay)
else
showNow()
end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }, { Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
]), ]),
@@ -211,6 +204,48 @@ else
end end
self:SetEntityEnabled(base, true) self:SetEntityEnabled(base, true)
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]), _TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
method('ShakeCombatCamera', `local lp = _UserService.LocalPlayer
local cam = nil
if lp ~= nil then
cam = lp.CameraComponent
end
if cam == nil then
cam = _CameraService:GetCurrentCameraComponent()
end
if cam == nil then
return
end
self.CombatCameraShakeSeq = (self.CombatCameraShakeSeq or 0) + 1
local seq = self.CombatCameraShakeSeq
local baseCameraOffset = Vector2(${CAM.cameraOffsetX}, ${CAM.cameraOffsetY})
cam.CameraOffset = baseCameraOffset
local offsets = {
Vector2(-0.22, 0),
Vector2(0.2, 0),
Vector2(-0.12, 0),
Vector2(0.06, 0),
Vector2(0, 0),
}
for i = 1, #offsets do
local delta = offsets[i]
_TimerService:SetTimerOnce(function()
local lp2 = _UserService.LocalPlayer
local cam2 = nil
if lp2 ~= nil then
cam2 = lp2.CameraComponent
end
if cam2 == nil then
cam2 = _CameraService:GetCurrentCameraComponent()
end
if cam2 == nil then
return
end
if seq ~= self.CombatCameraShakeSeq then
return
end
cam2.CameraOffset = Vector2(baseCameraOffset.x + delta.x, baseCameraOffset.y + delta.y)
end, 0.028 * (i - 1))
end`, []),
method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer
if lp == nil then if lp == nil then
return return

View File

@@ -106,6 +106,7 @@ self.HolyChargeCount = 0
self.DamagePowerStrengthUsed = false self.DamagePowerStrengthUsed = false
self.DamageDealtThisTurn = 0 self.DamageDealtThisTurn = 0
self.DmgPopSeq = 0 self.DmgPopSeq = 0
self.CombatCameraShakeSeq = 0
self.DmgPopSlotQueue = {} self.DmgPopSlotQueue = {}
self.FirstHpLossDone = false self.FirstHpLossDone = false
self.ClayBlockNext = 0 self.ClayBlockNext = 0

View File

@@ -36,6 +36,7 @@ function writeCodeblocks() {
prop('number', 'TweenEventId', '0'), prop('number', 'TweenEventId', '0'),
prop('number', 'CardHoverTweenId', '0'), prop('number', 'CardHoverTweenId', '0'),
prop('number', 'DmgPopSeq', '0'), prop('number', 'DmgPopSeq', '0'),
prop('number', 'CombatCameraShakeSeq', '0'),
prop('any', 'DmgPopSlotQueue'), prop('any', 'DmgPopSlotQueue'),
prop('any', 'EndTurnHandler'), prop('any', 'EndTurnHandler'),
prop('any', 'NewGameHandler'), prop('any', 'NewGameHandler'),