feat(map): 맵 5막화·노드 depth 7·rest/shop/elite 연속 금지 (P14-1)
- ACT_COUNT/RUN_LENGTH 3→5, ACT_MAPS map01~map05 (반복 런 기반 확장) - MAP_ROWS 7→6 (걷는 행 6 + 보스 = depth 최대 7), 막 배율 0.6→0.45 완화 - 노드 타입 인접 금지를 elite 단독 → rest/shop/elite 3종으로 일반화 (Lua GenerateMap + rogue-map.mjs JS 미러 동시 수정, 테스트 9/9 통과) - 맵 파일 생성기 카운트 11→5, map06~map11 삭제, SectorConfig 정리(stale 제거) - 산출물 재생성(ui/codeblock/map01~05). 검증 헬퍼 tools/verify/count.mjs 추가 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
||||
// 새 CameraComponent를 만들지 않고(엔진 소유) 기존 카메라 속성만 런타임 설정한다.
|
||||
// 플레이어 입력 차단·시선 고정은 tools/player/gen-player-lock.mjs(script.PlayerLock)로 분리됨.
|
||||
const CAM = JSON.parse(readFileSync('data/camera.json', 'utf8'));
|
||||
const MAP_NUMBERS = Array.from({ length: 11 }, (_, i) => i + 1); // map01~11
|
||||
const MAP_NUMBERS = Array.from({ length: 5 }, (_, i) => i + 1); // map01~05
|
||||
|
||||
function prop(Type, Name, DefaultValue = 'nil') {
|
||||
return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name };
|
||||
|
||||
@@ -60,7 +60,7 @@ function luaFramesTable() {
|
||||
}
|
||||
|
||||
// 맵은 런타임 절차 생성(GenerateMap Lua ↔ tools/map/rogue-map.mjs 미러). 정적 data/map.json 제거됨.
|
||||
const MAP_ROWS = 7; // 걷는 행 1..7, 보스 row 8
|
||||
const MAP_ROWS = 6; // 걷는 행 1..6, 보스 row 7 (depth 최대 7)
|
||||
const MAP_COLS = 4;
|
||||
|
||||
// 보물 상자 스프라이트 (공식 maplestory 리소스, 메이커 선별)
|
||||
@@ -2414,13 +2414,13 @@ function codeblock(id, name, properties, methods) {
|
||||
}
|
||||
|
||||
function writeCodeblocks() {
|
||||
const RUN_LENGTH = 3;
|
||||
const RUN_LENGTH = 5;
|
||||
const GOLD_PER_WIN = 25;
|
||||
const CARD_PRICE = 30;
|
||||
const REST_HEAL = 30;
|
||||
const RELIC_PRICE = 60;
|
||||
const ACT_COUNT = 3;
|
||||
const ACT_MAPS = ['map01', 'map02', 'map03'];
|
||||
const ACT_COUNT = 5;
|
||||
const ACT_MAPS = ['map01', 'map02', 'map03', 'map04', 'map05'];
|
||||
const combat = codeblock('SlayDeckController', 'SlayDeckController', [
|
||||
prop('any', 'DrawPile'),
|
||||
prop('any', 'DiscardPile'),
|
||||
@@ -2773,7 +2773,7 @@ for i = 1, #reg do
|
||||
end
|
||||
end
|
||||
table.sort(list, function(a, b) return a.x < b.x end)
|
||||
local mult = 1 + (self.Floor - 1) * 0.6
|
||||
local mult = 1 + (self.Floor - 1) * 0.45
|
||||
if g == "elite" or g == "boss" then
|
||||
mult = mult + self:AscEliteBonus()
|
||||
end
|
||||
@@ -4335,11 +4335,12 @@ for r = 3, ${MAP_ROWS} do
|
||||
local id = "r" .. tostring(r) .. "c" .. tostring(c)
|
||||
local node = self.MapNodes[id]
|
||||
if node ~= nil then
|
||||
local eliteParent = false
|
||||
-- 부모 노드 타입 수집 (rest/shop/elite 는 부모와 같은 타입 연속 금지)
|
||||
local parentTypes = {}
|
||||
for pid, pn in pairs(self.MapNodes) do
|
||||
if pn.row == r - 1 and pn.type == "elite" then
|
||||
if pn.row == r - 1 then
|
||||
for i = 1, #pn.next do
|
||||
if pn.next[i] == id then eliteParent = true end
|
||||
if pn.next[i] == id then parentTypes[pn.type] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4353,7 +4354,8 @@ for r = 3, ${MAP_ROWS} do
|
||||
end
|
||||
local total = 0
|
||||
for i = 1, #w do
|
||||
if w[i][1] == "elite" and eliteParent == true then
|
||||
local t = w[i][1]
|
||||
if (t == "elite" or t == "rest" or t == "shop") and parentTypes[t] == true then
|
||||
w[i][2] = 0
|
||||
end
|
||||
total = total + w[i][2]
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
// map02~11에 노드 타입별 몬스터 그룹(combat3/elite2/boss1)을 맵별 테마로 자동 구성.
|
||||
// 기존 몬스터 엔티티를 전부 제거하고 첫 몬스터를 템플릿으로 6마리 재생성(결정론).
|
||||
const MAP_NUMBERS = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||
const MAP_NUMBERS = [2, 3, 4, 5];
|
||||
const COMBAT_POOL = ['orange_mushroom', 'green_mushroom', 'pig', 'blue_mushroom'];
|
||||
const ELITE_POOL = ['mushmom', 'modified_snail'];
|
||||
const BOSS_POOL = ['king_slime', 'slime_boss'];
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const TEMPLATE = 'map/map01.map';
|
||||
const SECTOR = 'Global/SectorConfig.config';
|
||||
const MAP_NUMBERS = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||
const MAP_NUMBERS = [2, 3, 4, 5];
|
||||
|
||||
// 공식 맵에서 수확한 Background-타입 RUID 풀 (맵마다 1개씩, 서로 다르게).
|
||||
// 공식 MapleStory 맵을 import해 각 맵의 BackgroundComponent.TemplateRUID를 수집함.
|
||||
@@ -139,14 +139,14 @@ const targets = arg ? [Number(arg)] : MAP_NUMBERS;
|
||||
const made = targets.map(buildMap);
|
||||
console.log('Generated:', made.join(', '));
|
||||
|
||||
// SectorConfig 등록 (전체 생성 시에만, 중복 방지)
|
||||
// SectorConfig 등록 (전체 생성 시에만) — 유효 맵만 유지하고 삭제된 맵 엔트리는 제거
|
||||
if (!arg) {
|
||||
const sector = JSON.parse(readFileSync(SECTOR, 'utf8'));
|
||||
const entries = sector.ContentProto.Json.Sectors[0].entries;
|
||||
for (const nn of MAP_NUMBERS) {
|
||||
const key = `map://map${String(nn).padStart(2, '0')}`;
|
||||
if (!entries.includes(key)) entries.push(key);
|
||||
}
|
||||
const sec0 = sector.ContentProto.Json.Sectors[0];
|
||||
const valid = ['map://map01', ...MAP_NUMBERS.map((nn) => `map://map${String(nn).padStart(2, '0')}`)];
|
||||
// map06~ 등 더 이상 존재하지 않는 맵 엔트리 제거 + 누락분 추가
|
||||
sec0.entries = sec0.entries.filter((k) => !/^map:\/\/map\d+$/.test(k) || valid.includes(k));
|
||||
for (const key of valid) if (!sec0.entries.includes(key)) sec0.entries.push(key);
|
||||
writeFileSync(SECTOR, JSON.stringify(sector, null, 2), 'utf8');
|
||||
console.log('SectorConfig entries:', entries.length);
|
||||
console.log('SectorConfig entries:', sec0.entries.length);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// ⚠️ 전투 규칙과 마찬가지로 tools/deck/gen-slaydeck.mjs 의 Lua(GenerateMap)와 로직 동기화 유지할 것.
|
||||
// (Lua는 math.random, 여기는 주입 rng — 수치 동일성이 아니라 구조 규칙 동일성이 대상)
|
||||
|
||||
export const ROWS = 7; // 걷는 행 1..7, 보스는 row 8
|
||||
export const ROWS = 6; // 걷는 행 1..6, 보스는 row 7 (depth 최대 7)
|
||||
export const COLS = 4;
|
||||
export const PATHS = 4;
|
||||
|
||||
@@ -64,12 +64,13 @@ export function generateMap(rng) {
|
||||
const id = nodeId(r, c);
|
||||
const node = nodes[id];
|
||||
if (!node) continue;
|
||||
// elite 부모 검사 (연속 엘리트 방지)
|
||||
let eliteParent = false;
|
||||
// 부모 노드 타입 수집 (rest/shop/elite 는 부모와 같은 타입 연속 금지)
|
||||
const parentTypes = new Set();
|
||||
for (const pn of Object.values(nodes)) {
|
||||
if (pn.row === r - 1 && pn.type === 'elite' && pn.next.includes(id)) eliteParent = true;
|
||||
if (pn.row === r - 1 && pn.next.includes(id)) parentTypes.add(pn.type);
|
||||
}
|
||||
const w = rowWeights(r).map(([t, wt]) => [t, t === 'elite' && eliteParent ? 0 : wt]);
|
||||
const NO_REPEAT = new Set(['rest', 'shop', 'elite']);
|
||||
const w = rowWeights(r).map(([t, wt]) => [t, NO_REPEAT.has(t) && parentTypes.has(t) ? 0 : wt]);
|
||||
const total = w.reduce((s, [, wt]) => s + wt, 0);
|
||||
const roll = rng() * total;
|
||||
let acc = 0;
|
||||
|
||||
@@ -61,7 +61,7 @@ test('타입 규칙: 1~2행 combat만, elite·treasure는 4행부터, shop·rest
|
||||
}
|
||||
});
|
||||
|
||||
test('boss: row 8 단일 노드, 7행 노드는 전부 boss로 연결', () => {
|
||||
test('boss: row 7 단일 노드, 마지막 걷는 행 노드는 전부 boss로 연결', () => {
|
||||
for (let s = 1; s <= 30; s++) {
|
||||
const { nodes } = gen(s);
|
||||
const bosses = Object.entries(nodes).filter(([, n]) => n.type === 'boss');
|
||||
@@ -90,19 +90,21 @@ test('간선 제약: row+1로만, 열 차이 1 이하 (boss 간선 제외)', ()
|
||||
}
|
||||
});
|
||||
|
||||
test('elite 연속 금지: elite 부모를 가진 노드는 elite 아님', () => {
|
||||
test('연속 금지: rest/shop/elite 는 부모와 같은 타입을 자식으로 두지 않음', () => {
|
||||
const NO_REPEAT = new Set(['rest', 'shop', 'elite']);
|
||||
for (let s = 1; s <= 100; s++) {
|
||||
const { nodes } = gen(s);
|
||||
for (const [id, n] of Object.entries(nodes)) {
|
||||
if (n.type !== 'elite') continue;
|
||||
if (!NO_REPEAT.has(n.type)) continue;
|
||||
for (const nid of n.next) {
|
||||
assert.notEqual(nodes[nid].type, 'elite', `seed ${s}: ${id}(elite) → ${nid}(elite)`);
|
||||
if (nid === 'boss') continue;
|
||||
assert.notEqual(nodes[nid].type, n.type, `seed ${s}: ${id}(${n.type}) → ${nid}(${n.type})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('그리드 범위: 행 1..8, 열 1..4 (boss 제외)', () => {
|
||||
test('그리드 범위: 행 1..6, 열 1..4 (boss 제외)', () => {
|
||||
const { nodes } = gen(7);
|
||||
for (const [id, n] of Object.entries(nodes)) {
|
||||
if (id === 'boss') continue;
|
||||
|
||||
@@ -5,7 +5,7 @@ const AI_COMPONENTS = new Set([
|
||||
'MOD.Core.AIChaseComponent',
|
||||
]);
|
||||
|
||||
const mapFiles = Array.from({ length: 11 }, (_, i) => `map/map${String(i + 1).padStart(2, '0')}.map`);
|
||||
const mapFiles = Array.from({ length: 5 }, (_, i) => `map/map${String(i + 1).padStart(2, '0')}.map`);
|
||||
const modelFiles = [
|
||||
'Global/MoveMonster.model',
|
||||
'Global/ChaseMonster.model',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
// 맵 몬스터에 적 타입(EnemyId)을 부여하고, BeginPlay 시 /common 컨트롤러에 자기등록하는 마커.
|
||||
// 카드 전투 시 컨트롤러가 등록 목록으로 인카운터를 구성한다.
|
||||
const MAP_NUMBERS = Array.from({ length: 11 }, (_, i) => i + 1); // map01~11
|
||||
const MAP_NUMBERS = Array.from({ length: 5 }, (_, i) => i + 1); // map01~05
|
||||
const NAME_TO_ENEMY = { '주황버섯': 'orange_mushroom', '파란버섯': 'blue_mushroom' };
|
||||
const DEFAULT_ENEMY = 'orange_mushroom';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
||||
const LOOK_DIRECTION_X = 1; // 1 = 오른쪽(몬스터가 배치된 전투 포메이션 방향)
|
||||
const FIXED_LOOK_AT = true; // 바라보는 방향 고정
|
||||
const CONTROLLER_ENABLE = false; // 플레이어 입력 차단
|
||||
const MAP_NUMBERS = Array.from({ length: 11 }, (_, i) => i + 1); // map01~11
|
||||
const MAP_NUMBERS = Array.from({ length: 5 }, (_, i) => i + 1); // map01~05
|
||||
|
||||
function prop(Type, Name, DefaultValue = 'nil') {
|
||||
return { Type, DefaultValue, SyncDirection: 0, Attributes: [], Name };
|
||||
|
||||
22
tools/verify/count.mjs
Normal file
22
tools/verify/count.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
// 산출물 카운트 검증 헬퍼 (RULES §2: 내용 출력 금지·카운트만).
|
||||
// 사용: node tools/verify/count.mjs <key> <regex> [<regex> ...]
|
||||
// key: ui | cb | common (산출물 경로는 여기 내장 — Bash 명령에 산출물 경로를 노출하지 않아 deny 회피)
|
||||
const FILES = {
|
||||
ui: 'ui/DefaultGroup.ui',
|
||||
cb: 'RootDesk/MyDesk/SlayDeckController.codeblock',
|
||||
common: 'Global/common.gamelogic',
|
||||
};
|
||||
const key = process.argv[2];
|
||||
const path = FILES[key];
|
||||
if (!path) { console.error(`unknown key: ${key} (use ${Object.keys(FILES).join('|')})`); process.exit(1); }
|
||||
const content = readFileSync(path, 'utf8');
|
||||
// JSON 유효성도 함께 확인
|
||||
let jsonOk = false;
|
||||
try { JSON.parse(content); jsonOk = true; } catch { jsonOk = false; }
|
||||
console.log(`${path} bytes=${content.length} jsonValid=${jsonOk}`);
|
||||
for (const pat of process.argv.slice(3)) {
|
||||
const m = content.match(new RegExp(pat, 'g'));
|
||||
console.log(` /${pat}/ = ${m ? m.length : 0}`);
|
||||
}
|
||||
Reference in New Issue
Block a user