feat(motion): 전투 모션 — 공격/피격/독뎀 (생성기+산출물)

- PlayerAttackMotion(StateComponent ATTACK→IDLE)·PlayerHitMotion(HIT+넉백 틱)
- MonsterLunge(공격 시 런지)·MonsterHitMotion(hit 클립 스왑→stand 복귀, 폴백 흔들림)
- BuildMonsters에 hit/stand 클립 pcall 캐시·motionBusy
- 훅: PlayCard·DealDamageToTarget·PlayAoeFx·독 틱·체인메일 반사·EnemyActStep

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 18:37:42 +09:00
parent 2cd672b474
commit abd6d00052
2 changed files with 184 additions and 6 deletions

View File

@@ -2760,8 +2760,17 @@ for i = 1, n do
intents[k] = { kind = e.intents[k].kind, value = v, effect = e.intents[k].effect }
end
local maxHp = math.floor(e.maxHp * mult * self:AscHpMult())
local hitClip = nil
local standClip = nil
if item.entity.StateAnimationComponent ~= nil then
pcall(function()
hitClip = item.entity.StateAnimationComponent.ActionSheet["hit"]
standClip = item.entity.StateAnimationComponent.ActionSheet["stand"]
end)
end
self.Monsters[i] = { entity = item.entity, enemyId = item.enemyId, name = e.name,
hp = maxHp, maxHp = maxHp, block = 0, str = 0, weak = 0, vuln = 0, poison = 0,
hitClip = hitClip, standClip = standClip, motionBusy = false,
intents = intents, intentIdx = 1, alive = true, slot = i }
self:ReviveMonsterEntity(item.entity)
self:PositionMonsterSlot(i)
@@ -3241,6 +3250,7 @@ end
self.Energy = self.Energy - c.cost
if c.kind == "Attack" then
if c.damage ~= nil then
self:PlayerAttackMotion()
local total = 0
local hitN = c.hits or 1
for h = 1, hitN do
@@ -3391,6 +3401,7 @@ if m.block > 0 and pierce ~= true then
dmg = dmg - absorbed
end
m.hp = m.hp - dmg
self:MonsterHitMotion(m.slot)
if m.hp <= 0 then
m.hp = 0
self:KillMonster(m.slot)
@@ -3464,6 +3475,7 @@ _TimerService:SetTimerOnce(function()
end
m.hp = m.hp - dmg
self:ShowDmgPop(i, dmg)
self:MonsterHitMotion(i)
if m.hp <= 0 then
m.hp = 0
self:KillMonster(m.slot)
@@ -3501,6 +3513,7 @@ if dmg > 0 then
local am = self.Monsters[attackerSlot]
if am ~= nil and am.alive == true then
am.hp = am.hp - 3
self:MonsterHitMotion(am.slot)
if am.hp <= 0 then
am.hp = 0
self:KillMonster(am.slot)
@@ -3539,6 +3552,7 @@ _TimerService:SetTimerOnce(function()
if m.poison ~= nil and m.poison > 0 then
m.hp = m.hp - m.poison
self:ShowDmgPop(idx, m.poison)
self:MonsterHitMotion(idx)
m.poison = m.poison - 1
if m.hp <= 0 then
m.hp = 0
@@ -3553,6 +3567,7 @@ _TimerService:SetTimerOnce(function()
local intent = m.intents[m.intentIdx]
if intent ~= nil then
if intent.kind == "Attack" then
self:MonsterLunge(idx)
local atk = intent.value + m.str
if m.weak > 0 then
atk = math.floor(atk * 0.75)
@@ -3563,6 +3578,7 @@ _TimerService:SetTimerOnce(function()
local before = self.PlayerHp
self:DealDamageToPlayer(atk, idx)
self:ShowPlayerDmgPop(before - self.PlayerHp)
self:PlayerHitMotion()
elseif intent.kind == "Defend" then
m.block = m.block + intent.value
elseif intent.kind == "Debuff" then
@@ -3829,6 +3845,92 @@ else
end
self:SetEntityEnabled(base, true)
_TimerService:SetTimerOnce(function() self:SetEntityEnabled(base, false) end, 0.6)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' }]),
method('PlayerAttackMotion', `local lp = _UserService.LocalPlayer
if lp == nil or lp.StateComponent == nil then
return
end
pcall(function() lp.StateComponent:ChangeState("ATTACK") end)
_TimerService:SetTimerOnce(function()
if lp ~= nil and isvalid(lp) and lp.StateComponent ~= nil then
pcall(function() lp.StateComponent:ChangeState("IDLE") end)
end
end, 0.5)`),
method('PlayerHitMotion', `local lp = _UserService.LocalPlayer
if lp == nil then
return
end
if lp.StateComponent ~= nil then
pcall(function() lp.StateComponent:ChangeState("HIT") end)
end
local tr = lp.TransformComponent
if tr == nil then
return
end
local p = tr.Position
tr.Position = Vector3(p.x - 0.15, p.y, p.z)
_TimerService:SetTimerOnce(function()
if lp ~= nil and isvalid(lp) and lp.TransformComponent ~= nil then
lp.TransformComponent.Position = Vector3(p.x, p.y, p.z)
end
end, 0.15)`),
method('MonsterLunge', `local m = self.Monsters[idx]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
return
end
if m.motionBusy == true then
return
end
m.motionBusy = true
local e = m.entity
local tr = e.TransformComponent
if tr == nil then
m.motionBusy = false
return
end
local p = tr.Position
tr.Position = Vector3(p.x - 0.35, p.y, p.z)
_TimerService:SetTimerOnce(function()
if isvalid(e) and e.TransformComponent ~= nil then
e.TransformComponent.Position = Vector3(p.x, p.y, p.z)
end
m.motionBusy = false
end, 0.18)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'idx' }]),
method('MonsterHitMotion', `local m = self.Monsters[slot]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
return
end
local e = m.entity
if m.hitClip ~= nil and e.SpriteRendererComponent ~= nil then
e.SpriteRendererComponent.SpriteRUID = m.hitClip
_TimerService:SetTimerOnce(function()
if isvalid(e) and e.SpriteRendererComponent ~= nil and m.alive == true and m.standClip ~= nil then
e.SpriteRendererComponent.SpriteRUID = m.standClip
end
end, 0.5)
else
if m.motionBusy == true then
return
end
m.motionBusy = true
local tr = e.TransformComponent
if tr == nil then
m.motionBusy = false
return
end
local p = tr.Position
local seq = { 0.12, -0.12, 0 }
for i = 1, #seq do
local dx = seq[i]
_TimerService:SetTimerOnce(function()
if isvalid(e) and e.TransformComponent ~= nil then
e.TransformComponent.Position = Vector3(p.x + dx, p.y, p.z)
end
if i == #seq then
m.motionBusy = false
end
end, 0.06 * i)
end
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('SetHpBar', `local e = _EntityService:GetEntityByPath(path)
if e == nil or e.UITransformComponent == nil then
return