feat(rogue-map): 유물 방 상자 연출·TreasureHud·메소 표기 (생성기+산출물)
- TreasureHud: 보물 상자(클릭→흔들림 0.08s×6→열림 RUID 교체→유물+메소 보상) - 상자 닫힘/열림 RUID 메이커 선별, PickNewRelic 재사용 (소진 시 메소 대체) - ShowState/HideGameHud/LeaveNode treasure 등록, 표시 화폐 '골드'→'메소' 전환 - 테스트 30건(rogue-map 9 + sim 21) 통과 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,10 @@ if (!ENEMIES.enemies[ENEMIES.activeEnemy]) {
|
||||
const MAP_ROWS = 7; // 걷는 행 1..7, 보스 row 8
|
||||
const MAP_COLS = 4;
|
||||
|
||||
// 보물 상자 스프라이트 (공식 maplestory 리소스, 메이커 선별)
|
||||
const CHEST_CLOSED_RUID = '43df67920c0d43298e0d93c02c6afa71';
|
||||
const CHEST_OPEN_RUID = '09c5cee56fd640bf8ae3a18ce50f4759';
|
||||
|
||||
const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8'));
|
||||
if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`);
|
||||
for (const id of RELICS.relicPool) {
|
||||
@@ -85,6 +89,7 @@ const GENERATED_UI_SECTIONS = [
|
||||
'MapHud',
|
||||
'ShopHud',
|
||||
'RestHud',
|
||||
'TreasureHud',
|
||||
'MainMenu',
|
||||
'CharacterSelectHud',
|
||||
];
|
||||
@@ -95,6 +100,7 @@ const UI_APPEND_ORDER = [
|
||||
'MapHud',
|
||||
'ShopHud',
|
||||
'RestHud',
|
||||
'TreasureHud',
|
||||
'DeckInspectHud',
|
||||
'DeckAllHud',
|
||||
'MainMenu',
|
||||
@@ -123,7 +129,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 : 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 : 0xfe;
|
||||
const v = (ns * 0x100000 + n) >>> 0;
|
||||
return `${v.toString(16).padStart(8, '0')}-0000-4000-8000-${v.toString(16).padStart(12, '0')}`;
|
||||
}
|
||||
@@ -1145,7 +1151,7 @@ function upsertUi() {
|
||||
}));
|
||||
const topTexts = [
|
||||
['Floor', -520, 160, '막 1/3', GOLD],
|
||||
['Gold', -360, 160, '골드 0', { r: 0.98, g: 0.85, b: 0.4, a: 1 }],
|
||||
['Gold', -360, 160, '메소 0', { r: 0.98, g: 0.85, b: 0.4, a: 1 }],
|
||||
];
|
||||
topTexts.forEach(([suffix, x, w, value, color], ti) => {
|
||||
combat.push(entity({
|
||||
@@ -1566,7 +1572,7 @@ function upsertUi() {
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 300, y: 44 }, pos: { x: 0, y: 330 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '골드 0', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||||
text({ value: '메소 0', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
let shpN = 3;
|
||||
@@ -1590,7 +1596,7 @@ function upsertUi() {
|
||||
['Cost', { size: { x: 44, y: 44 }, pos: { x: -68, y: 103 }, value: '1', fontSize: 26, bold: true, color: { r: 1, g: 1, b: 1, a: 1 } }],
|
||||
['Name', { size: { x: 168, y: 30 }, pos: { x: 0, y: -8 }, value: '카드', fontSize: 20, bold: true, color: { r: 1, g: 1, b: 1, a: 1 } }],
|
||||
['Desc', { size: { x: 164, y: 56 }, pos: { x: 0, y: -58 }, value: '', fontSize: 18, bold: false, color: { r: 1, g: 1, b: 1, a: 1 } }],
|
||||
['Price', { size: { x: 160, y: 40 }, pos: { x: 0, y: -105 }, value: '30 골드', fontSize: 22, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 } }],
|
||||
['Price', { size: { x: 160, y: 40 }, pos: { x: 0, y: -105 }, value: '30 메소', fontSize: 22, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 } }],
|
||||
]) {
|
||||
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : suffix === 'Desc' ? 8 : 9;
|
||||
shop.push(entity({
|
||||
@@ -1662,7 +1668,7 @@ function upsertUi() {
|
||||
components: [
|
||||
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '60 골드', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||||
text({ value: '60 메소', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
shop.push(entity({
|
||||
@@ -1701,7 +1707,7 @@ function upsertUi() {
|
||||
components: [
|
||||
transform({ parentW: 560, parentH: 76, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 540, y: 30 }, pos: { x: 0, y: -22 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '20 골드', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||||
text({ value: '20 메소', fontSize: 20, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
shop.push(entity({
|
||||
@@ -1777,6 +1783,92 @@ function upsertUi() {
|
||||
}));
|
||||
emit('RestHud', rest);
|
||||
|
||||
// 유물 방 — 보물 상자 (P8)
|
||||
const treasure = [];
|
||||
const treasureHud = entity({
|
||||
id: guid('trs', 0),
|
||||
path: '/ui/DefaultGroup/TreasureHud',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
|
||||
displayOrder: 8,
|
||||
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 }),
|
||||
],
|
||||
});
|
||||
treasureHud.jsonString.enable = false;
|
||||
treasure.push(treasureHud);
|
||||
treasure.push(entity({
|
||||
id: guid('trs', 1),
|
||||
path: '/ui/DefaultGroup/TreasureHud/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: 700, y: 60 }, pos: { x: 0, y: 320 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '보물 상자', fontSize: 40, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
treasure.push(entity({
|
||||
id: guid('trs', 2),
|
||||
path: '/ui/DefaultGroup/TreasureHud/Chest',
|
||||
modelId: 'uisprite',
|
||||
entryId: 'UISprite',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
|
||||
displayOrder: 1,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 180, y: 180 }, pos: { x: 0, y: 40 } }),
|
||||
sprite({ dataId: CHEST_CLOSED_RUID, color: { r: 1, g: 1, b: 1, a: 1 }, type: 0, raycast: true }),
|
||||
button(),
|
||||
],
|
||||
}));
|
||||
treasure.push(entity({
|
||||
id: guid('trs', 3),
|
||||
path: '/ui/DefaultGroup/TreasureHud/Hint',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 2,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 500, y: 34 }, pos: { x: 0, y: -90 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '상자를 클릭해 여세요', fontSize: 20, bold: false, color: { r: 0.85, g: 0.85, b: 0.9, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
const treasureReward = entity({
|
||||
id: guid('trs', 4),
|
||||
path: '/ui/DefaultGroup/TreasureHud/Reward',
|
||||
modelId: 'uitext',
|
||||
entryId: 'UIText',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 3,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 800, y: 44 }, pos: { x: 0, y: -160 } }),
|
||||
sprite({ color: TRANSPARENT }),
|
||||
text({ value: '', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
|
||||
],
|
||||
});
|
||||
treasureReward.jsonString.enable = false;
|
||||
treasure.push(treasureReward);
|
||||
treasure.push(entity({
|
||||
id: guid('trs', 5),
|
||||
path: '/ui/DefaultGroup/TreasureHud/Leave',
|
||||
modelId: 'uibutton',
|
||||
entryId: 'UIButton',
|
||||
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent,MOD.Core.TextComponent',
|
||||
displayOrder: 4,
|
||||
components: [
|
||||
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 200, y: 60 }, pos: { x: 0, y: -280 } }),
|
||||
sprite({ color: DARK, type: 1, raycast: true }),
|
||||
button(),
|
||||
text({ value: '나가기', fontSize: 26, bold: true, color: GOLD, alignment: 4 }),
|
||||
],
|
||||
}));
|
||||
emit('TreasureHud', treasure);
|
||||
|
||||
const menu = [];
|
||||
menu.push(entity({
|
||||
id: guid('menu', 0),
|
||||
@@ -2157,6 +2249,7 @@ self:SetEntityEnabled("/ui/DefaultGroup/RewardHud", false)
|
||||
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/DeckInspectHud", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/DeckAllHud", false)`),
|
||||
method('ShowState', `self:HideGameHud()
|
||||
@@ -2172,6 +2265,8 @@ elseif state == "shop" then
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/ShopHud", true)
|
||||
elseif state == "rest" then
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/RestHud", true)
|
||||
elseif state == "treasure" then
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud", true)
|
||||
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'state' }]),
|
||||
method('ShowMainMenu', `self.SelectedClass = ""
|
||||
self:ShowState("menu")
|
||||
@@ -2505,6 +2600,14 @@ end
|
||||
local shopPotion = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion")
|
||||
if shopPotion ~= nil and shopPotion.ButtonComponent ~= nil then
|
||||
shopPotion:ConnectEvent(ButtonClickEvent, function() self:BuyPotion() end)
|
||||
end
|
||||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
||||
if chest ~= nil and chest.ButtonComponent ~= nil then
|
||||
chest:ConnectEvent(ButtonClickEvent, function() self:OpenChest() end)
|
||||
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`),
|
||||
method('StartPlayerTurn', `self.Turn = self.Turn + 1
|
||||
self.Energy = self.MaxEnergy
|
||||
@@ -3344,7 +3447,7 @@ for i = 1, #self.RelicPool do
|
||||
end
|
||||
if #pool == 0 then
|
||||
self.Gold = self.Gold + 25
|
||||
self:Toast("유물을 모두 모았습니다! 골드 +25")
|
||||
self:Toast("유물을 모두 모았습니다! 메소 +25")
|
||||
return ""
|
||||
end
|
||||
return pool[math.random(1, #pool)]`, [], 0, 'string'),
|
||||
@@ -3767,14 +3870,14 @@ self.ShopPotion = pkeys[math.random(1, #pkeys)]
|
||||
self.ShopPotionBought = false
|
||||
self:RenderShop()
|
||||
self:ShowState("shop")`),
|
||||
method('RenderShop', `self:SetText("/ui/DefaultGroup/ShopHud/Gold", "골드 " .. string.format("%d", self.Gold))
|
||||
method('RenderShop', `self:SetText("/ui/DefaultGroup/ShopHud/Gold", "메소 " .. string.format("%d", self.Gold))
|
||||
for i = 1, 3 do
|
||||
local cid = self.ShopChoices[i]
|
||||
local c = self.Cards[cid]
|
||||
local base = "/ui/DefaultGroup/ShopHud/Card" .. tostring(i)
|
||||
if c ~= nil then
|
||||
self:ApplyCardFace(base, cid)
|
||||
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 골드")
|
||||
self:SetText(base .. "/Price", string.format("%d", ${CARD_PRICE}) .. " 메소")
|
||||
local e = _EntityService:GetEntityByPath(base)
|
||||
if e ~= nil and e.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ShopBought[i] == true then
|
||||
@@ -3786,7 +3889,7 @@ end
|
||||
local rr = self.Relics[self.ShopRelic]
|
||||
if rr ~= nil then
|
||||
self:SetText("/ui/DefaultGroup/ShopHud/Relic/Label", rr.name .. " — " .. rr.desc)
|
||||
self:SetText("/ui/DefaultGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 골드")
|
||||
self:SetText("/ui/DefaultGroup/ShopHud/Relic/Price", string.format("%d", ${RELIC_PRICE}) .. " 메소")
|
||||
local re = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Relic")
|
||||
if re ~= nil and re.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ShopRelicBought == true then
|
||||
@@ -3799,7 +3902,7 @@ end
|
||||
local pp = self.Potions[self.ShopPotion]
|
||||
if pp ~= nil then
|
||||
self:SetText("/ui/DefaultGroup/ShopHud/Potion/Label", pp.name .. " — " .. pp.desc)
|
||||
self:SetText("/ui/DefaultGroup/ShopHud/Potion/Price", string.format("%d", ${POTIONS.shopPrice}) .. " 골드")
|
||||
self:SetText("/ui/DefaultGroup/ShopHud/Potion/Price", string.format("%d", ${POTIONS.shopPrice}) .. " 메소")
|
||||
local pe = _EntityService:GetEntityByPath("/ui/DefaultGroup/ShopHud/Potion")
|
||||
if pe ~= nil and pe.SpriteGUIRendererComponent ~= nil then
|
||||
if self.ShopPotionBought == true then
|
||||
@@ -3864,7 +3967,59 @@ local r = _EntityService:GetEntityByPath("/ui/DefaultGroup/RestHud")
|
||||
if r ~= nil then
|
||||
r.Enable = false
|
||||
end
|
||||
local t = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud")
|
||||
if t ~= nil then
|
||||
t.Enable = false
|
||||
end
|
||||
self:ShowMap()`),
|
||||
method('ShowTreasure', `self.ChestOpened = false
|
||||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
||||
if chest ~= nil then
|
||||
if chest.SpriteGUIRendererComponent ~= nil then
|
||||
chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_CLOSED_RUID}"
|
||||
end
|
||||
if chest.UITransformComponent ~= nil then
|
||||
chest.UITransformComponent.anchoredPosition = Vector2(0, 40)
|
||||
end
|
||||
end
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", false)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", true)
|
||||
self:ShowState("treasure")`),
|
||||
method('OpenChest', `if self.ChestOpened == true then
|
||||
return
|
||||
end
|
||||
self.ChestOpened = true
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Hint", false)
|
||||
local chest = _EntityService:GetEntityByPath("/ui/DefaultGroup/TreasureHud/Chest")
|
||||
local steps = { 10, -10, 8, -8, 5, 0 }
|
||||
for i = 1, #steps do
|
||||
local dx = steps[i]
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if chest ~= nil and isvalid(chest) and chest.UITransformComponent ~= nil then
|
||||
chest.UITransformComponent.anchoredPosition = Vector2(dx, 40)
|
||||
end
|
||||
end, 0.08 * i)
|
||||
end
|
||||
_TimerService:SetTimerOnce(function()
|
||||
if chest ~= nil and isvalid(chest) and chest.SpriteGUIRendererComponent ~= nil then
|
||||
chest.SpriteGUIRendererComponent.ImageRUID = "${CHEST_OPEN_RUID}"
|
||||
end
|
||||
local g = 40 + math.random(0, 20)
|
||||
local nid = self:PickNewRelic()
|
||||
local msg = ""
|
||||
if nid ~= "" then
|
||||
self:AddRelic(nid)
|
||||
local nr = self.Relics[nid]
|
||||
msg = "유물 획득: " .. nr.name .. " · 메소 +" .. tostring(g)
|
||||
else
|
||||
g = g + 30
|
||||
msg = "메소 +" .. tostring(g)
|
||||
end
|
||||
self.Gold = self.Gold + g
|
||||
self:RenderRun()
|
||||
self:SetText("/ui/DefaultGroup/TreasureHud/Reward", msg)
|
||||
self:SetEntityEnabled("/ui/DefaultGroup/TreasureHud/Reward", true)
|
||||
end, 0.55)`),
|
||||
]);
|
||||
for (const m of combat.ContentProto.Json.Methods) {
|
||||
m.ExecSpace = 6;
|
||||
|
||||
Reference in New Issue
Block a user