feat(job): 클래스 풀 필터·전직 선택 흐름·전직 HUD (생성기)
- CardPool(클래스 필터) — 보상·상점 공용 - 보스 분기: 1차+비최종막 → JobChoiceHud(유물/전직), ContinueAfterBoss 추출 - JobSelectHud 3직업 패널, SetJob(대표 카드 지급·직업명 갱신) - guid 'job'=0xe4, HideGameHud·BindButtons 등록 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user