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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user