refactor(cb): codeblock 메서드 161개를 cb/*.mjs 17 모듈로 분리 (codeblock 바이트 동일)

writeCodeblocks의 메서드를 연속-런 17 모듈(boot/state/soul/charselect/run/
deckturn/deckview/hand/combat/jobs/runend/render/reward/items/tooltip/map/shop)로
분리, methods 배열은 spread-concat(원본 순서 보존). prop 103개는 오케스트레이터 유지.
산출물 무변경(diffcheck: SlayDeckController.codeblock IDENTICAL).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 08:02:22 +09:00
parent 42eb33b579
commit a141939675
18 changed files with 3343 additions and 3207 deletions

480
tools/deck/cb/combat.mjs Normal file
View File

@@ -0,0 +1,480 @@
import { method, RUN_LENGTH, GOLD_PER_WIN, CARD_PRICE, REST_HEAL, RELIC_PRICE, ACT_COUNT, ACT_MAPS, LOBBY_MAP, LOBBY_SPAWN } from '../lib/codeblock.mjs';
import { CARDS, ENEMIES, CLASSES, JOBS, SOUL_UNLOCKS, CARDFRAMES, RARITIES, MAP_ROWS, MAP_COLS, CHEST_CLOSED_RUID, CHEST_OPEN_RUID, NODEICONS, CHARS, CAM, RELICS, POTIONS, luaSoulShopTable, frameRuid, luaFramesTable, luaNodeIconsTable, luaRelicsTable, luaPotionsTable, luaIntentsArray, luaEnemiesTable, luaStr, luaJobsTable, luaCardsTable, luaDeckTable } from '../lib/data.mjs';
import { UI_FILE, COMMON_FILE, UI_ROOT, GENERATED_UI_SECTIONS, UI_APPEND_ORDER, DISABLED_STOCK_CONTROLS, TRANSPARENT, DARK, GOLD, ATTACK, DEFEND, SKILL, DAMAGE_DIGIT_RUIDS, DAMAGE_POP_MAX_DIGITS, DAMAGE_POP_DIGIT_W, DAMAGE_POP_DIGIT_H, DAMAGE_POP_DIGIT_SPACING, MAX_MONSTERS, HEAD_OFFSET_Y, HP_BAR_W, WHITE, CARD_NAME_TEXT, CARD_DESC_TEXT, cardFaceLayout, CARD_W, CARD_H, CARD_SPACING, CARD_XS, ALIGN_CENTER, ALIGN_BOTTOM_CENTER, guid, transform, sprite, button, text, scrollLayoutGroup, popupLayerFor, uiOrderFor, displayOrderFor, applySortingOverride, entity, uiPath, sectionRoot, isGeneratedUiEntity, appendUiSection } from '../lib/ui-helpers.mjs';
export const combatMethods = [
method('PlayCard', `if self:IsDiscardSelecting() == true then
self:SelectDiscardSlot(slot)
return
end
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return
end
if self.Hand == nil then
return
end
local cardId = self.Hand[slot]
if cardId == nil then
return
end
local c = self.Cards[cardId]
if c == nil then
return
end
if c.unplayable == true then
self:Toast("사용할 수 없는 카드입니다")
return
end
if self.Energy < c.cost then
self:Toast("에너지가 부족합니다")
return
end
self.Energy = self.Energy - c.cost
self:ResolveCardEffects(cardId, c, false)
table.remove(self.Hand, slot)
if c.exhaust == true then
if self.ExhaustPile == nil then self.ExhaustPile = {} end
table.insert(self.ExhaustPile, cardId)
elseif c.kind ~= "Power" then
table.insert(self.DiscardPile, cardId)
end
self:RenderHand(false)
self:RenderPiles()
self:RenderCombat()
if self:BeginDiscardSelection(c) == true then
return
end
self:RenderHand(false)
self:RenderPiles()
self:RenderCombat()
self:CheckCombatEnd()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('OnCardButton', `if self:IsDiscardSelecting() == true then
self:SelectDiscardSlot(slot)
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('FindMonsterAtTouch', `local best = 0
local bestDist = 200
for i = 1, #self.Monsters do
local m = self.Monsters[i]
if m.alive == true and m.entity ~= nil and isvalid(m.entity) and m.entity.TransformComponent ~= nil then
local wp = m.entity.TransformComponent.WorldPosition
local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7))
local dx = sp.x - touchPoint.x
local dy = sp.y - touchPoint.y
local d = math.sqrt(dx * dx + dy * dy)
if d < bestDist then
bestDist = d
best = i
end
end
end
return best`, [{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' }], 0, 'number'),
method('RenderTargetFrames', `local dragActive = self.DragTargetIndex ~= nil and self.DragTargetIndex > 0
local shownTarget = self.TargetIndex
if dragActive == true then shownTarget = self.DragTargetIndex end
for i = 1, #self.Monsters do
local m = self.Monsters[i]
local active = false
if m ~= nil and m.alive == true and i == shownTarget then active = true end
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetFrame", active)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker", active and dragActive)
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(i) .. "/TargetMarker/Label", active and dragActive)
end`),
method('OnCardDragBegin', `if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return
end
if self.Hand == nil or self.Hand[slot] == nil then
return
end
if self.CardHoverTweenId ~= nil and self.CardHoverTweenId ~= 0 then
_TimerService:ClearTimer(self.CardHoverTweenId)
self.CardHoverTweenId = 0
end
for i = 1, 10 do
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(i))
if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(i), 0)
end
end
self.DragSlot = slot
self.DragTargetIndex = 0
self:RenderTargetFrames()`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('OnCardDrag', `if self.DragSlot ~= slot then
return
end
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
if e ~= nil and e.UITransformComponent ~= nil then
local ui = _UILogic:ScreenToUIPosition(touchPoint)
e.UITransformComponent.anchoredPosition = Vector2(ui.x, ui.y + 360)
end
local cardId = self.Hand[slot]
local c = nil
if cardId ~= nil then c = self.Cards[cardId] end
if c ~= nil and c.kind == "Attack" then
local best = self:FindMonsterAtTouch(touchPoint)
if best ~= self.DragTargetIndex then
self.DragTargetIndex = best
self:RenderTargetFrames()
end
end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
]),
method('OnCardDragEnd', `if self.DragSlot ~= slot then
return
end
self.DragSlot = 0
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/CardHand/Card" .. tostring(slot))
if e ~= nil and e.UITransformComponent ~= nil then
e.UITransformComponent.anchoredPosition = Vector2(self:GetHandSlotX(slot), 0)
e.UITransformComponent.UIScale = Vector3(1, 1, 1)
end
self:ResolveCardDrop(slot, touchPoint)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
]),
method('ResolveCardDrop', `if self:IsDiscardSelecting() == true then
self:SelectDiscardSlot(slot)
return
end
if self.CombatOver == true or self.FxBusy == true or self.TurnBusy == true then
return
end
local cardId = self.Hand[slot]
if cardId == nil then
return
end
local c = self.Cards[cardId]
if c == nil then
return
end
if c.kind == "Attack" then
local best = self.DragTargetIndex or 0
if best <= 0 then best = self:FindMonsterAtTouch(touchPoint) end
self.DragTargetIndex = 0
if best > 0 then
self.TargetIndex = best
self:PlayCard(slot)
self:RenderTargetFrames()
else
self:RenderTargetFrames()
end
else
self.DragTargetIndex = 0
self:RenderTargetFrames()
local ui = _UILogic:ScreenToUIPosition(touchPoint)
if ui.y > -180 then
self:PlayCard(slot)
end
end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' },
{ Type: 'any', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'touchPoint' },
]),
method('Toast', `log(message)`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'message' }]),
method('DealDamageToTarget', `local m = self.Monsters[self.TargetIndex]
if m == nil or m.alive ~= true then
m = nil
for i = 1, #self.Monsters do
if self.Monsters[i].alive == true then m = self.Monsters[i]; self.TargetIndex = i; break end
end
end
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 and pierce ~= true then
local absorbed = math.min(m.block, dmg)
m.block = m.block - absorbed
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)
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]
if m == nil or m.alive ~= true or m.entity == nil or not isvalid(m.entity) then
self:DealDamageToTarget(damage, pierce)
self:RenderCombat()
self:CheckCombatEnd()
return
end
self.FxBusy = true
local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx")
if fx ~= nil then
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
fx.SpriteGUIRendererComponent.ImageRUID = image
end
if fx.UITransformComponent ~= nil and m.entity.TransformComponent ~= nil then
local wp = m.entity.TransformComponent.WorldPosition
local sp = _UILogic:WorldToScreenPosition(Vector2(wp.x, wp.y + 0.7))
fx.UITransformComponent.anchoredPosition = _UILogic:ScreenToUIPosition(sp)
end
fx.Enable = true
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, pierce)
self:ShowDmgPop(targetIndex, shown)
self:RenderCombat()
self:CheckCombatEnd()
end, 0.35)`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'targetIndex' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' },
{ Type: 'boolean', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'pierce' },
]),
method('PlayAoeFx', `self.FxBusy = true
local fx = _EntityService:GetEntityByPath("/ui/DefaultGroup/CombatHud/SkillFx")
if fx ~= nil then
if fx.SpriteGUIRendererComponent ~= nil and image ~= nil and image ~= "" then
fx.SpriteGUIRendererComponent.ImageRUID = image
end
if fx.UITransformComponent ~= nil then
fx.UITransformComponent.anchoredPosition = Vector2(300, 60)
end
fx.Enable = true
end
_TimerService:SetTimerOnce(function()
if fx ~= nil then fx.Enable = false end
self.FxBusy = false
for i = 1, #self.Monsters do
local m = self.Monsters[i]
if m ~= nil and m.alive == true then
local dmg = damage
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
dmg = dmg - absorbed
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)
end
end
end
self:RenderCombat()
self:CheckCombatEnd()
end, 0.35)`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'image' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'damage' },
]),
method('KillMonster', `local m = self.Monsters[slot]
if m == nil then
return
end
m.alive = false
if m.entity ~= nil and isvalid(m.entity) then
local ent = m.entity
_TimerService:SetTimerOnce(function() if isvalid(ent) then ent:SetVisible(false) end end, 0.4)
end
self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(slot), false)
for i = 1, #self.Monsters do
if self.Monsters[i].alive == true then self.TargetIndex = i; break end
end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]),
method('DealDamageToPlayer', `local dmg = amount
if self.PlayerBlock > 0 then
local absorbed = math.min(self.PlayerBlock, dmg)
self.PlayerBlock = self.PlayerBlock - absorbed
dmg = dmg - absorbed
end
if dmg > 0 then
self.PlayerHp = self.PlayerHp - dmg
local reflect = self.PlayerThorns or 0
if self:HasRelic("bronzeScales") then
reflect = reflect + 3
end
if reflect > 0 and attackerSlot ~= nil and attackerSlot > 0 then
local am = self.Monsters[attackerSlot]
if am ~= nil and am.alive == true then
am.hp = am.hp - reflect
self:ShowDmgPop(am.slot, reflect)
self:MonsterHitMotion(am.slot)
if am.hp <= 0 then
am.hp = 0
self:KillMonster(am.slot)
end
end
end
if self:HasRelic("selfFormingClay") then
self.ClayBlockNext = self.ClayBlockNext + 3
end
if self:HasRelic("centennialPuzzle") and self.FirstHpLossDone == false then
self.FirstHpLossDone = true
self:DrawCards(3)
self:RenderHand(false)
end
end
if self.PlayerHp < 0 then
self.PlayerHp = 0
end`, [
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'amount' },
{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'attackerSlot' },
]),
method('EnemyTurn', `self.TurnBusy = true
self:EnemyActStep(1)`),
method('EnemyActStep', `local idx = 0
for i = fromIndex, #self.Monsters do
if self.Monsters[i].alive == true then idx = i; break end
end
if idx == 0 or self.PlayerHp <= 0 then
self:FinishEnemyTurn()
return
end
local m = self.Monsters[idx]
local base = "/ui/DefaultGroup/CombatHud/MonsterSlot" .. tostring(idx)
self:SetEntityEnabled(base .. "/ActFrame", true)
_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
self:KillMonster(m.slot)
self:RenderCombat()
self:SetEntityEnabled(base .. "/ActFrame", false)
_TimerService:SetTimerOnce(function() self:EnemyActStep(idx + 1) end, 0.15)
return
end
end
m.block = 0
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)
end
if self.PlayerVuln > 0 then
atk = math.floor(atk * 1.5)
end
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
if intent.effect == "weak" then
self.PlayerWeak = self.PlayerWeak + intent.value
elseif intent.effect == "vuln" then
self.PlayerVuln = self.PlayerVuln + intent.value
end
elseif intent.kind == "AddCard" then
local cnt = intent.count or 1
for ci = 1, cnt do
table.insert(self.DiscardPile, intent.card)
end
self:RenderPiles()
local cn = intent.card
local cc = self.Cards[intent.card]
if cc ~= nil then cn = cc.name end
self:Toast(m.name .. ": " .. cn .. " 추가!")
end
end
if #m.intents > 0 then
m.intentIdx = math.random(1, #m.intents)
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)
end, 0.45)`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromIndex' }]),
method('FinishEnemyTurn', `self.TurnBusy = false
self:CheckCombatEnd()
if self.CombatOver == true then
return
end
_TimerService:SetTimerOnce(function() self:StartPlayerTurn() end, 0.45)`),
method('ClearCombatCards', `self.DrawPile = {}
self.DiscardPile = {}
self.ExhaustPile = {}
self.Hand = {}
self.DiscardSelectRemaining = 0
self.DiscardSelectTotal = 0
self.DiscardPostShiv = 0
self.DiscardShivPerPick = 0
self:UpdateDiscardPrompt()
self:RenderHand(false)
self:RenderPiles()`),
method('CheckCombatEnd', `local anyAlive = false
for i = 1, #self.Monsters do
if self.Monsters[i].alive == true then anyAlive = true; break end
end
if anyAlive == false then
self.CombatOver = true
self:ClearCombatCards()
self.Gold = self.Gold + math.floor(${GOLD_PER_WIN} * self:AscGoldMult())
self:ApplyRelics("combatEnd")
self:ApplyRelics("combatReward")
self:MaybeDropPotion()
self:RenderRun()
local node = self.MapNodes[self.CurrentNodeId]
if node ~= nil and node.type == "elite" then
self.Gold = self.Gold + 15
local nid = self:PickNewRelic()
if nid ~= "" then
self:AddRelic(nid)
local nr = self.Relics[nid]
if nr ~= nil then
self:Toast("유물 획득: " .. nr.name)
end
end
end
if node ~= nil and node.type == "boss" then
if self.PlayerJob == "" and self.Floor < self.RunLength then
self:ShowJobChoice()
else
if self.PlayerJob ~= "" then self:AwardSouls(1) end
local bid = self:PickNewRelic()
if bid ~= "" then
self:AddRelic(bid)
local br = self.Relics[bid]
if br ~= nil then
self:Toast("유물 획득: " .. br.name)
end
end
self:ContinueAfterBoss()
end
else
self:OfferReward()
end
elseif self.PlayerHp <= 0 then
self.CombatOver = true
self:EndRun("패배...")
end`),
method('ContinueAfterBoss', `if self.Floor < self.RunLength then
self.Floor = self.Floor + 1
self.CurrentNodeId = ""
self.CurrentEnemyId = ""
self:GenerateMap()
self:RenderRun()
self:TeleportToActMap()
self:ShowMap()
else
self:EndRun("런 클리어!")
end`),
];