feat(rogue-map): 로그라이크 절차 생성 맵·층 시스템·유물 방 (배포 퀄리티 P8) #41

Merged
gahusb merged 6 commits from feature/p8-rogue-map into main 2026-06-12 10:25:41 +09:00
3 changed files with 35802 additions and 229 deletions
Showing only changes of commit 1abd9f7987 - Show all commits

File diff suppressed because one or more lines are too long

View File

@@ -1454,23 +1454,28 @@ function upsertUi() {
text({ value: '다음 노드 선택', fontSize: 40, bold: true, color: GOLD, alignment: 4 }),
],
}));
// 절차 생성 맵용 정적 그리드 — 노드 7행×4열 + 보스, 점선 도트. RenderMap이 런타임 토글.
const nodeX = (c) => -270 + (c - 1) * 180;
const nodeY = (r) => -330 + (r - 1) * 105;
const BOSS_POS = { x: 0, y: 405 };
let mapN = 2;
for (const [id, node] of Object.entries(MAP.nodes)) {
const pushMapNode = (id, pos, size, label) => {
const nodePath = `/ui/DefaultGroup/MapHud/Node_${id}`;
const pos = { x: node.col * 180, y: (node.row - (MAX_ROW + 1) / 2) * 140 };
map.push(entity({
const nodeEnt = entity({
id: guid('map', mapN++),
path: nodePath,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ButtonComponent',
displayOrder: node.row,
displayOrder: 5,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 150, y: 80 }, pos }),
sprite({ color: { r: 0.3, g: 0.55, b: 0.85, a: 1 }, type: 1, raycast: true }),
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size, pos }),
sprite({ color: { r: 0.2, g: 0.22, b: 0.26, a: 1 }, type: 1, raycast: true }),
button(),
],
}));
});
nodeEnt.jsonString.enable = false;
map.push(nodeEnt);
map.push(entity({
id: guid('map', mapN++),
path: `${nodePath}/Label`,
@@ -1479,11 +1484,47 @@ function upsertUi() {
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent',
displayOrder: 0,
components: [
transform({ parentW: 150, parentH: 80, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 144, y: 72 }, pos: { x: 0, y: 0 } }),
transform({ parentW: size.x, parentH: size.y, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: size.x + 20, y: 30 }, pos: { x: 0, y: 0 } }),
sprite({ color: TRANSPARENT }),
text({ value: node.enemy ? `${TYPE_KO[node.type]}\n${ENEMIES.enemies[node.enemy].name}` : TYPE_KO[node.type], fontSize: 20, bold: true }),
text({ value: label, fontSize: id === 'boss' ? 18 : 15, bold: true, color: { r: 1, g: 1, b: 1, a: 1 }, alignment: 4 }),
],
}));
};
for (let r = 1; r <= MAP_ROWS; r++) {
for (let c = 1; c <= MAP_COLS; c++) {
pushMapNode(`r${r}c${c}`, { x: nodeX(c), y: nodeY(r) }, { x: 56, y: 56 }, '');
}
}
pushMapNode('boss', BOSS_POS, { x: 72, y: 72 }, '보스');
const pushDots = (dotId, from, to) => {
for (let k = 1; k <= 3; k++) {
const t = k / 4;
const dot = entity({
id: guid('map', mapN++),
path: `/ui/DefaultGroup/MapHud/Dot_${dotId}_${k}`,
modelId: 'uisprite',
entryId: 'UISprite',
componentNames: 'MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent',
displayOrder: 1,
components: [
transform({ parentW: 1920, parentH: 1080, anchor: { x: 0.5, y: 0.5 }, pivot: { x: 0.5, y: 0.5 }, size: { x: 8, y: 8 }, pos: { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t } }),
sprite({ color: { r: 0.5, g: 0.5, b: 0.55, a: 0.8 }, type: 1 }),
],
});
dot.jsonString.enable = false;
map.push(dot);
}
};
for (let r = 1; r < MAP_ROWS; r++) {
for (let c = 1; c <= MAP_COLS; c++) {
for (let c2 = c - 1; c2 <= c + 1; c2++) {
if (c2 < 1 || c2 > MAP_COLS) continue;
pushDots(`r${r}c${c}_${c2}`, { x: nodeX(c), y: nodeY(r) }, { x: nodeX(c2), y: nodeY(r + 1) });
}
}
}
for (let c = 1; c <= MAP_COLS; c++) {
pushDots(`r${MAP_ROWS}c${c}_b`, { x: nodeX(c), y: nodeY(MAP_ROWS) }, BOSS_POS);
}
emit('MapHud', map);
@@ -3570,22 +3611,114 @@ for i = 1, #list do
end
end
return false`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }], 0, 'boolean'),
method('RenderMap', `for id, node in pairs(self.MapNodes) do
local e = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. id)
if e ~= nil then
local reachable = self:IsReachable(id)
if e.SpriteGUIRendererComponent ~= nil then
if reachable then
e.SpriteGUIRendererComponent.Color = Color(0.3, 0.55, 0.85, 1)
method('RenderMapNode', `local base = "/ui/DefaultGroup/MapHud/Node_" .. id
local e = _EntityService:GetEntityByPath(base)
if e == nil then
return
end
local node = self.MapNodes[id]
if node == nil then
e.Enable = false
return
end
e.Enable = true
local tname = "전투"
local r0 = 0.78
local g0 = 0.36
local b0 = 0.32
if node.type == "elite" then
tname = "엘리트"
r0 = 0.62
g0 = 0.4
b0 = 0.85
elseif node.type == "shop" then
tname = "상점"
r0 = 0.9
g0 = 0.75
b0 = 0.35
elseif node.type == "rest" then
tname = "휴식"
r0 = 0.4
g0 = 0.75
b0 = 0.45
elseif node.type == "treasure" then
tname = "보물"
r0 = 0.35
g0 = 0.7
b0 = 0.75
elseif node.type == "boss" then
tname = "보스"
r0 = 0.85
g0 = 0.25
b0 = 0.25
end
self:SetText(base .. "/Label", tname)
local reachable = self:IsReachable(id)
local visited = false
if self.VisitedNodes ~= nil then
for i = 1, #self.VisitedNodes do
if self.VisitedNodes[i] == id then visited = true end
end
end
if e.SpriteGUIRendererComponent ~= nil then
if id == self.CurrentNodeId then
e.SpriteGUIRendererComponent.Color = Color(0.95, 0.8, 0.3, 1)
elseif visited == true then
e.SpriteGUIRendererComponent.Color = Color(0.18, 0.19, 0.22, 0.9)
elseif reachable == true then
e.SpriteGUIRendererComponent.Color = Color(r0, g0, b0, 1)
else
e.SpriteGUIRendererComponent.Color = Color(r0 * 0.45, g0 * 0.45, b0 * 0.45, 0.55)
end
end
if e.ButtonComponent ~= nil then
e.ButtonComponent.Enable = reachable
end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]),
method('RenderMapDots', `local node = self.MapNodes[fromId]
local has = false
if node ~= nil then
for i = 1, #node.next do
if node.next[i] == toId then has = true end
end
end
for k = 1, 3 do
local d = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Dot_" .. dotId .. "_" .. tostring(k))
if d ~= nil then
d.Enable = has
if has == true and d.SpriteGUIRendererComponent ~= nil then
if fromId == self.CurrentNodeId then
d.SpriteGUIRendererComponent.Color = Color(0.95, 0.8, 0.3, 1)
else
e.SpriteGUIRendererComponent.Color = Color(0.2, 0.22, 0.26, 0.6)
d.SpriteGUIRendererComponent.Color = Color(0.5, 0.5, 0.55, 0.8)
end
end
if e.ButtonComponent ~= nil then
e.ButtonComponent.Enable = reachable
end
end`, [
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'dotId' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'fromId' },
{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'toId' },
]),
method('RenderMap', `for r = 1, ${MAP_ROWS} do
for c = 1, ${MAP_COLS} do
self:RenderMapNode("r" .. tostring(r) .. "c" .. tostring(c))
end
end
self:RenderMapNode("boss")
for r = 1, ${MAP_ROWS} - 1 do
for c = 1, ${MAP_COLS} do
local fid = "r" .. tostring(r) .. "c" .. tostring(c)
for c2 = c - 1, c + 1 do
if c2 >= 1 and c2 <= ${MAP_COLS} then
self:RenderMapDots(fid .. "_" .. tostring(c2), fid, "r" .. tostring(r + 1) .. "c" .. tostring(c2))
end
end
end
end`),
end
for c = 1, ${MAP_COLS} do
local fid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c)
self:RenderMapDots(fid .. "_b", fid, "boss")
end
`),
method('PickNode', `if self.RunActive ~= true then
return
end

File diff suppressed because it is too large Load Diff