feat: P14 — 반복 런·로비·영혼·도적·몬스터 랜덤성 (요청 17항목) #52

Merged
maple merged 11 commits from feature/p14-loop-lobby-soul into main 2026-06-14 01:50:20 +09:00
3 changed files with 3394 additions and 5 deletions
Showing only changes of commit 8879647b26 - Show all commits

File diff suppressed because one or more lines are too long

View File

@@ -37,6 +37,17 @@ for (const [cls, jobs] of Object.entries(JOBS)) {
if (!CARDS.cards[j.starter]) throw new Error(`[gen-slaydeck] JOBS.${cls}.${j.id} 대표 카드 없음: ${j.starter}`);
}
}
// 영혼(soul) 메타 해금 — 2차 전직 후 보스 클리어로 영혼 적립, 로비 영혼상점에서 구매 → 다음 런 이점
const SOUL_UNLOCKS = [
{ key: 'meso', name: '두둑한 지갑', desc: '런 시작 시 메소 +60', cost: 3 },
{ key: 'hp', name: '단련된 육체', desc: '시작 최대 HP +15', cost: 4 },
{ key: 'trim', name: '덱 정제', desc: '시작 덱에서 기본 카드 1장 제거', cost: 5 },
{ key: 'relic', name: '유물 수집가', desc: '런 시작 시 유물 1개 추가', cost: 6 },
];
function luaSoulShopTable(unlocks) {
const items = unlocks.map((u) => `\t{ key = ${luaStr(u.key)}, name = ${luaStr(u.name)}, desc = ${luaStr(u.desc)}, cost = ${u.cost} },`).join('\n');
return `self.SoulShopDef = {\n${items}\n}`;
}
if (!ENEMIES.enemies[ENEMIES.activeEnemy]) {
throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`);
}
@@ -2621,6 +2632,38 @@ function upsertUi() {
text({ value: '닫기', fontSize: 28, bold: true, color: WHITE, alignment: 4 }),
],
}));
for (let i = 1; i <= 4; i++) {
const ip = `/ui/DefaultGroup/SoulShopHud/Item${i}`;
const iy = 230 - (i - 1) * 125;
soulShop.push(entity({
id: guid('soul', soulId++),
path: ip, modelId: 'uibutton', entryId: 'UIButton',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
displayOrder: 2,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 760, y: 104 }, pos: { x: 0, y: iy }, align: ALIGN_CENTER }),
sprite({ color: { r: 0.14, g: 0.16, b: 0.22, a: 1 }, type: 1, raycast: true }),
button(),
],
}));
for (const [suffix, x, y, w, fs, color] of [
['Name', -180, 22, 360, 28, GOLD],
['Desc', -180, -24, 440, 20, { r: 0.86, g: 0.9, b: 0.94, a: 1 }],
['Status', 270, 0, 220, 22, { r: 0.6, g: 0.85, b: 1, a: 1 }],
]) {
soulShop.push(entity({
id: guid('soul', soulId++),
path: `${ip}/${suffix}`, modelId: 'uitext', entryId: 'UIText',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 1,
components: [
transform({ parentW: 760, parentH: 104, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: w, y: 38 }, pos: { x, y } }),
sprite({ color: TRANSPARENT }),
text({ value: '', fontSize: fs, bold: suffix === 'Name', color, alignment: 4 }),
],
}));
}
}
emit('SoulShopHud', soulShop);
for (const section of UI_APPEND_ORDER) {
@@ -2730,6 +2773,9 @@ function writeCodeblocks() {
prop('boolean', 'LobbyBound', 'false'),
prop('boolean', 'CodexMode', 'false'),
prop('any', 'CodexCards'),
prop('any', 'SoulUnlocks'),
prop('any', 'SoulShopDef'),
prop('boolean', 'SoulShopBound', 'false'),
prop('string', 'DeckInspectKind', '""'),
prop('boolean', 'DeckAllOpen', 'false'),
prop('any', 'Cards'),
@@ -2781,10 +2827,14 @@ function writeCodeblocks() {
prop('boolean', 'ChestOpened', 'false'),
prop('string', 'PlayerJob', '""'),
], [
method('OnBeginPlay', `self:ShowLobby()
method('OnBeginPlay', `${luaSoulShopTable(SOUL_UNLOCKS)}
self.SoulUnlocks = {}
self.SoulPoints = self.SoulPoints or 0
self:ShowLobby()
local lp = _UserService.LocalPlayer
if lp ~= nil then
self:ReqLoadAscension(lp.PlayerComponent.UserId)
self:ReqLoadSouls(lp.PlayerComponent.UserId)
end`),
method('ReqLoadAscension', `local ds = _DataStorageService:GetUserDataStorage(userId)
local errCode, value = ds:GetAndWait("ascensionUnlocked")
@@ -2985,8 +3035,113 @@ self:RenderAllDeck()`),
method('ShowBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", true)`),
method('CloseBoard', `self:SetEntityEnabled("/ui/DefaultGroup/BoardHud", false)`),
method('ShowSoulShop', `self:RenderSoulLabel()
self:RenderSoulShop()
self:BindSoulShopButtons()
self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", true)`),
method('CloseSoulShop', `self:SetEntityEnabled("/ui/DefaultGroup/SoulShopHud", false)`),
method('ReqLoadSouls', `local ds = _DataStorageService:GetUserDataStorage(userId)
local e1, pts = ds:GetAndWait("soulPoints")
local e2, unl = ds:GetAndWait("soulUnlocks")
local p = 0
if e1 == 0 and pts ~= nil and pts ~= "" then p = tonumber(pts) or 0 end
local u = ""
if e2 == 0 and unl ~= nil then u = unl end
self:RecvSouls(p, u, userId)`, [{ Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5),
method('RecvSouls', `self.SoulPoints = p
self.SoulUnlocks = {}
if u ~= nil and u ~= "" then
for key in string.gmatch(u, "([^,]+)") do
self.SoulUnlocks[key] = true
end
end
self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 6),
method('SaveSouls', `local ds = _DataStorageService:GetUserDataStorage(userId)
ds:SetAndWait("soulPoints", tostring(p))
ds:SetAndWait("soulUnlocks", u)`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "p" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "u" }, { Type: "string", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "userId" }], 5),
method('SerializeUnlocks', `local parts = {}
if self.SoulUnlocks ~= nil then
for k, v in pairs(self.SoulUnlocks) do
if v == true then table.insert(parts, k) end
end
end
return table.concat(parts, ",")`, [], 0, 'string'),
method('AwardSouls', `self.SoulPoints = (self.SoulPoints or 0) + n
local lp = _UserService.LocalPlayer
if lp ~= nil then
self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId)
end
self:RenderSoulLabel()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "n" }]),
method('BuySoulUnlock', `local d = nil
if self.SoulShopDef ~= nil then d = self.SoulShopDef[slot] end
if d == nil then return end
if self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true then
self:Toast("이미 보유 중입니다")
return
end
if (self.SoulPoints or 0) < d.cost then
self:Toast("영혼이 부족합니다")
return
end
self.SoulPoints = self.SoulPoints - d.cost
if self.SoulUnlocks == nil then self.SoulUnlocks = {} end
self.SoulUnlocks[d.key] = true
local lp = _UserService.LocalPlayer
if lp ~= nil then
self:SaveSouls(self.SoulPoints, self:SerializeUnlocks(), lp.PlayerComponent.UserId)
end
self:Toast(d.name .. " 해금!")
self:RenderSoulLabel()
self:RenderSoulShop()`, [{ Type: "number", DefaultValue: null, SyncDirection: 0, Attributes: [], Name: "slot" }]),
method('RenderSoulShop', `local defs = self.SoulShopDef or {}
for i = 1, 4 do
local base = "/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i)
local d = defs[i]
if d == nil then
self:SetEntityEnabled(base, false)
else
self:SetEntityEnabled(base, true)
self:SetText(base .. "/Name", d.name)
self:SetText(base .. "/Desc", d.desc)
local owned = self.SoulUnlocks ~= nil and self.SoulUnlocks[d.key] == true
if owned then
self:SetText(base .. "/Status", "보유 중")
elseif (self.SoulPoints or 0) >= d.cost then
self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 구매")
else
self:SetText(base .. "/Status", tostring(d.cost) .. " 영혼 · 부족")
end
end
end`),
method('BindSoulShopButtons', `if self.SoulShopBound == true then
return
end
self.SoulShopBound = true
for i = 1, 4 do
local idx = i
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/SoulShopHud/Item" .. tostring(i))
if e ~= nil and e.ButtonComponent ~= nil then
e:ConnectEvent(ButtonClickEvent, function() self:BuySoulUnlock(idx) end)
end
end`),
method('ApplySoulUnlocks', `if self.SoulUnlocks == nil then return end
if self.SoulUnlocks["meso"] == true then self.Gold = self.Gold + 60 end
if self.SoulUnlocks["hp"] == true then
self.PlayerMaxHp = self.PlayerMaxHp + 15
self.PlayerHp = self.PlayerMaxHp
end
if self.SoulUnlocks["trim"] == true then
for i = 1, #self.RunDeck do
local cid = self.RunDeck[i]
if cid == "Defend" or cid == "MagicGuard" or cid == "DarkSight" then
table.remove(self.RunDeck, i)
break
end
end
end
if self.SoulUnlocks["relic"] == true then
local nid = self:PickNewRelic()
if nid ~= "" then self:AddRelic(nid) end
end`),
method('ShowCharacterSelect', `self.SelectedClass = ""
self:ShowState("charselect")
self:RenderCharacterSelect()`),
@@ -3070,6 +3225,7 @@ ${luaFramesTable()}
self:GenerateMap()
self:BindButtons()
self:AddRelic("${RELICS.startingRelic}")
self:ApplySoulUnlocks()
self:RenderPotions()
self:ShowMap()`),
method('StartCombat', `self:ShowState("combat")
@@ -4110,6 +4266,7 @@ if anyAlive == false 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)

File diff suppressed because it is too large Load Diff