From 2c9a1b351eeb4913e1697c16562daef0254c7c77 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 12 Jun 2026 13:36:46 +0900 Subject: [PATCH] =?UTF-8?q?feat(job):=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=ED=92=80=20=ED=95=84=ED=84=B0=C2=B7=EC=A0=84=EC=A7=81=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=ED=9D=90=EB=A6=84=C2=B7=EC=A0=84=EC=A7=81?= =?UTF-8?q?=20HUD=20(=EC=83=9D=EC=84=B1=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CardPool(클래스 필터) — 보상·상점 공용 - 보스 분기: 1차+비최종막 → JobChoiceHud(유물/전직), ContinueAfterBoss 추출 - JobSelectHud 3직업 패널, SetJob(대표 카드 지급·직업명 갱신) - guid 'job'=0xe4, HideGameHud·BindButtons 등록 Co-Authored-By: Claude Opus 4.8 (1M context) --- tools/deck/gen-slaydeck.mjs | 263 ++++++++++++++++++++++++++++++++---- 1 file changed, 235 insertions(+), 28 deletions(-) diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index bbb2e71..5689279 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -95,6 +95,8 @@ const GENERATED_UI_SECTIONS = [ 'ShopHud', 'RestHud', 'TreasureHud', + 'JobChoiceHud', + 'JobSelectHud', 'MainMenu', 'CharacterSelectHud', ]; @@ -106,6 +108,8 @@ const UI_APPEND_ORDER = [ 'ShopHud', 'RestHud', 'TreasureHud', + 'JobChoiceHud', + 'JobSelectHud', 'DeckInspectHud', 'DeckAllHud', 'MainMenu', @@ -134,7 +138,7 @@ const ALIGN_BOTTOM_CENTER = 6; function guid(prefix, n) { // 유효한 8-4-4-4-12 hex GUID 생성. prefix는 충돌 방지용 네임스페이스 바이트로 매핑. - const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : prefix === 'map' ? 0xcd : prefix === 'shp' ? 0xce : prefix === 'rst' ? 0xcf : prefix === 'menu' ? 0xe0 : prefix === 'ins' ? 0xe1 : prefix === 'all' ? 0xe2 : prefix === 'trs' ? 0xe3 : 0xfe; + const ns = prefix === 'hud' ? 0xd0 : prefix === 'dck' ? 0xca : prefix === 'cmb' ? 0xcb : prefix === 'rwd' ? 0xcc : prefix === 'map' ? 0xcd : prefix === 'shp' ? 0xce : prefix === 'rst' ? 0xcf : prefix === 'menu' ? 0xe0 : prefix === 'ins' ? 0xe1 : prefix === 'all' ? 0xe2 : prefix === 'trs' ? 0xe3 : prefix === 'job' ? 0xe4 : 0xfe; const v = (ns * 0x100000 + n) >>> 0; return `${v.toString(16).padStart(8, '0')}-0000-4000-8000-${v.toString(16).padStart(12, '0')}`; } @@ -1874,6 +1878,138 @@ function upsertUi() { })); emit('TreasureHud', treasure); + // 전직 선택 (P9) — 보스 보상: 유물 vs 2차 전직 + const jobChoice = []; + const jobChoiceHud = entity({ + id: guid('job', 0), + path: '/ui/DefaultGroup/JobChoiceHud', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 9, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.92 }, type: 1, raycast: true }), + ], + }); + jobChoiceHud.jsonString.enable = false; + jobChoice.push(jobChoiceHud); + jobChoice.push(entity({ + id: guid('job', 1), + path: '/ui/DefaultGroup/JobChoiceHud/Title', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 60 }, pos: { x: 0, y: 220 } }), + sprite({ color: TRANSPARENT }), + text({ value: '보스 처치 보상을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }), + ], + })); + const jcButtons = [ + ['RelicButton', '유물 획득', -240, { r: 0.7, g: 0.55, b: 0.85, a: 1 }], + ['JobButton', '2차 전직', 240, { r: 0.86, g: 0.6, b: 0.3, a: 1 }], + ]; + jcButtons.forEach(([suffix, label, x, color], bi) => { + jobChoice.push(entity({ + id: guid('job', 2 + bi), + path: `/ui/DefaultGroup/JobChoiceHud/${suffix}`, + modelId: 'uibutton', entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent', + displayOrder: 1 + bi, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 140 }, pos: { x, y: 0 } }), + sprite({ color, type: 1, raycast: true }), + button(), + text({ value: label, fontSize: 32, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + }); + emit('JobChoiceHud', jobChoice); + + const jobSelect = []; + const jobSelectHud = entity({ + id: guid('job', 10), + path: '/ui/DefaultGroup/JobSelectHud', + modelId: 'uisprite', entryId: 'UISprite', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent', + displayOrder: 10, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 1920, y: 1080 }, pos: { x: 0, y: 0 }, align: ALIGN_CENTER }), + sprite({ color: { r: 0.05, g: 0.06, b: 0.09, a: 0.94 }, type: 1, raycast: true }), + ], + }); + jobSelectHud.jsonString.enable = false; + jobSelect.push(jobSelectHud); + jobSelect.push(entity({ + id: guid('job', 11), + path: '/ui/DefaultGroup/JobSelectHud/Title', + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 60 }, pos: { x: 0, y: 300 } }), + sprite({ color: TRANSPARENT }), + text({ value: '2차 전직 — 직업을 선택하세요', fontSize: 36, bold: true, color: GOLD, alignment: 4 }), + ], + })); + const jobs = [ + ['fighter', '파이터', '공격 특화\n콤보 어택 · 버서크\n라이징 어택', '대표 카드: 콤보 어택', -440, { r: 0.82, g: 0.4, b: 0.34, a: 1 }], + ['page', '페이지', '속성 차지 특화\n썬더/블리자드 차지\n파워 가드', '대표 카드: 썬더 차지', 0, { r: 0.4, g: 0.55, b: 0.85, a: 1 }], + ['spearman', '스피어맨', '방어·관통 특화\n피어스 · 아이언 월\n하이퍼 바디', '대표 카드: 피어스', 440, { r: 0.42, g: 0.72, b: 0.46, a: 1 }], + ]; + jobs.forEach(([jobId, name, desc, starter, x, color], ji) => { + const base = `/ui/DefaultGroup/JobSelectHud/Job_${jobId}`; + jobSelect.push(entity({ + id: guid('job', 12 + ji * 4), + path: base, + modelId: 'uibutton', entryId: 'UIButton', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent', + displayOrder: 1 + ji, + components: [ + transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 380, y: 420 }, pos: { x, y: -20 } }), + sprite({ color, type: 1, raycast: true }), + button(), + ], + })); + jobSelect.push(entity({ + id: guid('job', 13 + ji * 4), + path: `${base}/Name`, + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 0, + components: [ + transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 360, y: 50 }, pos: { x: 0, y: 150 } }), + sprite({ color: TRANSPARENT }), + text({ value: name, fontSize: 34, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }), + ], + })); + jobSelect.push(entity({ + id: guid('job', 14 + ji * 4), + path: `${base}/Desc`, + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 1, + components: [ + transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 340, y: 160 }, pos: { x: 0, y: 0 } }), + sprite({ color: TRANSPARENT }), + text({ value: desc, fontSize: 22, bold: false, color: { r: 0.95, g: 0.95, b: 0.97, a: 1 }, alignment: 4 }), + ], + })); + jobSelect.push(entity({ + id: guid('job', 15 + ji * 4), + path: `${base}/Starter`, + modelId: 'uitext', entryId: 'UIText', + componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent', + displayOrder: 2, + components: [ + transform({ parentW: 380, parentH: 420, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 340, y: 32 }, pos: { x: 0, y: -160 } }), + sprite({ color: TRANSPARENT }), + text({ value: starter, fontSize: 18, bold: true, color: GOLD, alignment: 4 }), + ], + })); + }); + emit('JobSelectHud', jobSelect); + const menu = []; menu.push(entity({ id: guid('menu', 0), @@ -2256,6 +2392,8 @@ self:SetEntityEnabled("/ui/DefaultGroup/MapHud", false) self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", false) self:SetEntityEnabled("/ui/DefaultGroup/RestHud", false) self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false) self:SetEntityEnabled("/ui/DefaultGroup/DeckInspectHud", false) self:SetEntityEnabled("/ui/DefaultGroup/DeckAllHud", false)`), method('ShowState', `self:HideGameHud() @@ -2362,6 +2500,7 @@ self:ShowMap()`), self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/Result", false) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/PotionMenu", false) self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false) +self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel()) self.MaxEnergy = 3 self.Turn = 0 self.PlayerBlock = 0 @@ -2615,6 +2754,22 @@ end local treasureLeave = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Leave") if treasureLeave ~= nil and treasureLeave.ButtonComponent ~= nil then treasureLeave:ConnectEvent(ButtonClickEvent, function() self:LeaveNode() end) +end +local jcRelic = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/RelicButton") +if jcRelic ~= nil and jcRelic.ButtonComponent ~= nil then + jcRelic:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("relic") end) +end +local jcJob = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobChoiceHud/JobButton") +if jcJob ~= nil and jcJob.ButtonComponent ~= nil then + jcJob:ConnectEvent(ButtonClickEvent, function() self:PickJobReward("job") end) +end +local jobIds = { "fighter", "page", "spearman" } +for i = 1, #jobIds do + local jid = jobIds[i] + local jb = _EntityService:GetEntityByPath("/ui/DefaultGroup/JobSelectHud/Job_" .. jid) + if jb ~= nil and jb.ButtonComponent ~= nil then + jb:ConnectEvent(ButtonClickEvent, function() self:SetJob(jid) end) + end end`), method('StartPlayerTurn', `self.Turn = self.Turn + 1 self.Energy = self.MaxEnergy @@ -3210,26 +3365,18 @@ if anyAlive == false then end end if node ~= nil and node.type == "boss" then - 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 - end - if node ~= nil and node.type == "boss" then - if self.Floor < self.RunLength then - self.Floor = self.Floor + 1 - self.CurrentNodeId = "" - self.CurrentEnemyId = "" - self:GenerateMap() - self:RenderRun() - self:TeleportToActMap() - self:ShowMap() + if self.PlayerJob == "" and self.Floor < self.RunLength then + self:ShowJobChoice() else - self:EndRun("런 클리어!") + 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() @@ -3238,6 +3385,64 @@ 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`), + method('ShowJobChoice', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) +self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) +self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", true)`), + method('PickJobReward', `self:SetEntityEnabled("/ui/DefaultGroup/JobChoiceHud", false) +if kind == "relic" then + 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() +else + self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", true) +end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'kind' }]), + method('JobLabel', `if self.PlayerJob == "fighter" then + return "파이터" +elseif self.PlayerJob == "page" then + return "페이지" +elseif self.PlayerJob == "spearman" then + return "스피어맨" +end +if self.SelectedClass == "warrior" then + return "전사" +end +return "플레이어"`, [], 0, 'string'), + method('SetJob', `self.PlayerJob = jobId +local starter = "" +if jobId == "fighter" then + starter = "ComboAttack" +elseif jobId == "page" then + starter = "ThunderCharge" +elseif jobId == "spearman" then + starter = "Pierce" +end +if starter ~= "" then + table.insert(self.RunDeck, starter) + local sc = self.Cards[starter] + if sc ~= nil then + self:Toast("2차 전직: " .. self:JobLabel() .. "! 신규 카드 — " .. sc.name) + end +end +self:SetText("/ui/DefaultGroup/CombatHud/PlayerPanel/Name", self:JobLabel()) +self:SetEntityEnabled("/ui/DefaultGroup/JobSelectHud", false) +self:ContinueAfterBoss()`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'jobId' }]), method('TeleportToActMap', `local maps = { ${ACT_MAPS.map((m) => `"${m}"`).join(', ')} } local target = maps[self.Floor] if target == nil then @@ -3375,12 +3580,17 @@ end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], N end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'slot' }]), method('RenderRun', `self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Floor", "막 " .. string.format("%d", self.Floor) .. "/" .. string.format("%d", self.RunLength) .. " · " .. string.format("%d", self.Depth) .. "층") self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Gold", "메소 " .. string.format("%d", self.Gold))`), + method('CardPool', `local pool = {} +for id, c in pairs(self.Cards) do + if c.class == self.SelectedClass or (self.PlayerJob ~= "" and c.class == self.PlayerJob) then + table.insert(pool, id) + end +end +table.sort(pool) +return pool`, [], 0, 'any'), method('OfferReward', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) -local pool = {} -for id, _ in pairs(self.Cards) do - table.insert(pool, id) -end +local pool = self:CardPool() self.RewardChoices = {} for i = 1, 3 do self.RewardChoices[i] = pool[math.random(1, #pool)] @@ -3875,10 +4085,7 @@ else self.CurrentEnemyId = "" self:StartCombat() end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]), - method('ShowShop', `local pool = {} -for cid, _ in pairs(self.Cards) do - table.insert(pool, cid) -end + method('ShowShop', `local pool = self:CardPool() self.ShopChoices = {} self.ShopBought = { false, false, false } for i = 1, 3 do