From 1624ef6f3b40eb2a6bf569b1893d53863d6f09d0 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 12 Jun 2026 10:16:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(rogue-map):=20GenerateMap=20=EB=9F=B0?= =?UTF-8?q?=ED=83=80=EC=9E=84=20=EC=A0=88=EC=B0=A8=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?+=20=EC=B8=B5=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20(=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 정적 map.json·luaMapNodesTable·luaStartArray 제거 - GenerateMap: 경로 4개 걷기·행별 가중 타입·elite 연속 금지 (JS 미러 동기화) - Depth/VisitedNodes prop, PickNode treasure 분기·층 갱신, 보스 클리어 시 새 맵 - TopBar '막 F/3 · D층', 메소 표기 시작 Co-Authored-By: Claude Opus 4.8 (1M context) --- data/map.json | 12 --- tools/deck/gen-slaydeck.mjs | 147 +++++++++++++++++++++++++++++------- 2 files changed, 118 insertions(+), 41 deletions(-) delete mode 100644 data/map.json diff --git a/data/map.json b/data/map.json deleted file mode 100644 index 154719b..0000000 --- a/data/map.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "start": ["A", "B"], - "nodes": { - "A": { "type": "combat", "enemy": "slime", "row": 1, "col": -1, "next": ["C", "D"] }, - "B": { "type": "combat", "enemy": "slime", "row": 1, "col": 1, "next": ["C", "D"] }, - "C": { "type": "rest", "row": 2, "col": -1, "next": ["E", "F"] }, - "D": { "type": "shop", "row": 2, "col": 1, "next": ["E", "F"] }, - "E": { "type": "elite", "enemy": "slime_elite", "row": 3, "col": -1, "next": ["BOSS"] }, - "F": { "type": "combat", "enemy": "slime", "row": 3, "col": 1, "next": ["BOSS"] }, - "BOSS": { "type": "boss", "enemy": "slime_boss", "row": 4, "col": 0, "next": [] } - } -} diff --git a/tools/deck/gen-slaydeck.mjs b/tools/deck/gen-slaydeck.mjs index 5d6b1c8..736d302 100644 --- a/tools/deck/gen-slaydeck.mjs +++ b/tools/deck/gen-slaydeck.mjs @@ -13,17 +13,9 @@ if (!ENEMIES.enemies[ENEMIES.activeEnemy]) { throw new Error(`[gen-slaydeck] activeEnemy가 enemies에 없음: ${ENEMIES.activeEnemy}`); } -const MAP = JSON.parse(readFileSync('data/map.json', 'utf8')); -for (const id of MAP.start) { - if (!MAP.nodes[id]) throw new Error(`[gen-slaydeck] map.start에 없는 노드 id: ${id}`); -} -for (const [id, n] of Object.entries(MAP.nodes)) { - if (n.enemy && !ENEMIES.enemies[n.enemy]) throw new Error(`[gen-slaydeck] 노드 ${id}의 enemy 없음: ${n.enemy}`); - for (const nx of n.next) { - if (!MAP.nodes[nx]) throw new Error(`[gen-slaydeck] 노드 ${id}.next에 없는 노드 id: ${nx}`); - } -} -const MAX_ROW = Math.max(...Object.values(MAP.nodes).map((n) => n.row)); +// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨. +const MAP_ROWS = 7; // 걷는 행 1..7, 보스 row 8 +const MAP_COLS = 4; const RELICS = JSON.parse(readFileSync('data/relics.json', 'utf8')); if (!RELICS.relics[RELICS.startingRelic]) throw new Error(`[gen-slaydeck] startingRelic 없음: ${RELICS.startingRelic}`); @@ -58,18 +50,6 @@ function luaEnemiesTable(enemies) { `\t${id} = { name = ${luaStr(e.name)}, maxHp = ${e.maxHp}, intents = ${luaIntentsArray(e.intents)} },`); return `self.Enemies = {\n${lines.join('\n')}\n}`; } -function luaMapNodesTable(nodes) { - const lines = Object.entries(nodes).map(([id, n]) => { - const nx = '{ ' + n.next.map(luaStr).join(', ') + ' }'; - const enemyField = n.enemy ? `enemy = ${luaStr(n.enemy)}, ` : ''; - return `\t${id} = { type = ${luaStr(n.type)}, ${enemyField}row = ${n.row}, col = ${n.col}, next = ${nx} },`; - }); - return `self.MapNodes = {\n${lines.join('\n')}\n}`; -} -function luaStartArray(start) { - return 'self.MapStart = { ' + start.map(luaStr).join(', ') + ' }'; -} - // Lua 직렬화 헬퍼 function luaStr(s) { return '"' + String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; @@ -2121,6 +2101,9 @@ function writeCodeblocks() { prop('boolean', 'FirstHpLossDone', 'false'), prop('number', 'ClayBlockNext', '0'), prop('number', 'PotionMenuSlot', '0'), + prop('number', 'Depth', '0'), + prop('any', 'VisitedNodes'), + prop('boolean', 'ChestOpened', 'false'), ], [ method('OnBeginPlay', `self:ShowMainMenu()`), method('HideGameHud', `self:SetEntityEnabled("/ui/DefaultGroup/Button_Attack", false) @@ -2225,10 +2208,9 @@ ${luaPotionsTable(POTIONS.potions)} ${luaRelicsTable(RELICS.relics)} self.RelicPool = { ${RELICS.relicPool.map(luaStr).join(', ')} } ${luaEnemiesTable(ENEMIES.enemies)} -${luaMapNodesTable(MAP.nodes)} -${luaStartArray(MAP.start)} self.CurrentNodeId = "" self.CurrentEnemyId = "" +self:GenerateMap() self:BindButtons() self:AddRelic("${RELICS.startingRelic}") self:RenderPotions() @@ -2400,7 +2382,13 @@ local skip = _EntityService:GetEntityByPath("/ui/DefaultGroup/RewardHud/Skip") if skip ~= nil and skip.ButtonComponent ~= nil then skip:ConnectEvent(ButtonClickEvent, function() self:PickReward(0) end) end -local mapNodeIds = { ${Object.keys(MAP.nodes).map(luaStr).join(', ')} } +local mapNodeIds = {} +for r = 1, ${MAP_ROWS} do + for c = 1, ${MAP_COLS} do + table.insert(mapNodeIds, "r" .. tostring(r) .. "c" .. tostring(c)) + end +end +table.insert(mapNodeIds, "boss") for i = 1, #mapNodeIds do local nid = mapNodeIds[i] local mn = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud/Node_" .. nid) @@ -3067,6 +3055,7 @@ if anyAlive == false then self.Floor = self.Floor + 1 self.CurrentNodeId = "" self.CurrentEnemyId = "" + self:GenerateMap() self:RenderRun() self:TeleportToActMap() self:ShowMap() @@ -3215,8 +3204,8 @@ end`, [{ Type: 'number', DefaultValue: null, SyncDirection: 0, Attributes: [], N self.TargetIndex = slot self:RenderCombat() 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)) -self:SetText("/ui/DefaultGroup/CombatHud/TopBar/Gold", "골드 " .. string.format("%d", self.Gold))`), + 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('OfferReward', `self:SetEntityEnabled("/ui/DefaultGroup/CardHand", false) self:SetEntityEnabled("/ui/DefaultGroup/DeckHud", false) local pool = {} @@ -3473,6 +3462,98 @@ end`, [ method('HideTooltip', `self:SetEntityEnabled("/ui/DefaultGroup/CombatHud/TooltipBox", false)`), method('ShowMap', `self:ShowState("map") self:RenderMap()`), + method('GenerateMap', `-- 절차 생성 — tools/map/rogue-map.mjs(JS 미러)와 로직 동기화 유지 +self.MapNodes = {} +self.MapStart = {} +self.VisitedNodes = {} +self.Depth = 0 +self.MapNodes["boss"] = { type = "boss", row = ${MAP_ROWS} + 1, col = 0, next = {} } +local cols = { 1, 2, 3, 4 } +for i = #cols, 2, -1 do + local j = math.random(1, i) + cols[i], cols[j] = cols[j], cols[i] +end +local starts = { cols[1], cols[2], math.random(1, ${MAP_COLS}), math.random(1, ${MAP_COLS}) } +for p = 1, 4 do + local c = starts[p] + local sid = "r1c" .. tostring(c) + if self.MapNodes[sid] == nil then + self.MapNodes[sid] = { type = "combat", row = 1, col = c, next = {} } + end + local found = false + for i = 1, #self.MapStart do + if self.MapStart[i] == sid then found = true end + end + if found == false then + table.insert(self.MapStart, sid) + end + for r = 1, ${MAP_ROWS} - 1 do + local nc = c + math.random(-1, 1) + if nc < 1 then nc = 1 end + if nc > ${MAP_COLS} then nc = ${MAP_COLS} end + local nid = "r" .. tostring(r + 1) .. "c" .. tostring(nc) + if self.MapNodes[nid] == nil then + self.MapNodes[nid] = { type = "combat", row = r + 1, col = nc, next = {} } + end + local fid = "r" .. tostring(r) .. "c" .. tostring(c) + local dup = false + for i = 1, #self.MapNodes[fid].next do + if self.MapNodes[fid].next[i] == nid then dup = true end + end + if dup == false then + table.insert(self.MapNodes[fid].next, nid) + end + c = nc + end + local lid = "r" .. tostring(${MAP_ROWS}) .. "c" .. tostring(c) + local bdup = false + for i = 1, #self.MapNodes[lid].next do + if self.MapNodes[lid].next[i] == "boss" then bdup = true end + end + if bdup == false then + table.insert(self.MapNodes[lid].next, "boss") + end +end +for r = 3, ${MAP_ROWS} do + for c = 1, ${MAP_COLS} do + local id = "r" .. tostring(r) .. "c" .. tostring(c) + local node = self.MapNodes[id] + if node ~= nil then + local eliteParent = false + for pid, pn in pairs(self.MapNodes) do + if pn.row == r - 1 and pn.type == "elite" then + for i = 1, #pn.next do + if pn.next[i] == id then eliteParent = true end + end + end + end + local w + if r == ${MAP_ROWS} then + w = { { "rest", 50 }, { "combat", 25 }, { "shop", 10 }, { "elite", 8 }, { "treasure", 7 } } + elseif r >= 4 then + w = { { "combat", 45 }, { "elite", 16 }, { "shop", 12 }, { "rest", 12 }, { "treasure", 15 } } + else + w = { { "combat", 45 }, { "shop", 12 }, { "rest", 12 } } + end + local total = 0 + for i = 1, #w do + if w[i][1] == "elite" and eliteParent == true then + w[i][2] = 0 + end + total = total + w[i][2] + end + local roll = math.random() * total + local acc = 0 + for i = 1, #w do + acc = acc + w[i][2] + if roll <= acc then + node.type = w[i][1] + break + end + end + end + end +end`), method('IsReachable', `local list if self.CurrentNodeId == "" then list = self.MapStart @@ -3512,17 +3593,25 @@ if self:IsReachable(id) ~= true then return end self.CurrentNodeId = id +if self.VisitedNodes == nil then + self.VisitedNodes = {} +end +table.insert(self.VisitedNodes, id) local hud = _EntityService:GetEntityByPath("/ui/DefaultGroup/MapHud") if hud ~= nil then hud.Enable = false end local node = self.MapNodes[id] +self.Depth = node.row +self:RenderRun() if node.type == "shop" then self:ShowShop() elseif node.type == "rest" then self:ShowRest() +elseif node.type == "treasure" then + self:ShowTreasure() else - self.CurrentEnemyId = node.enemy + self.CurrentEnemyId = "" self:StartCombat() end`, [{ Type: 'string', DefaultValue: null, SyncDirection: 0, Attributes: [], Name: 'id' }]), method('ShowShop', `local pool = {}