feat(card-frames): 커스텀 카드 프레임 — 직업×등급 프레임·보상 가중 추첨 (P13) #50

Merged
gahusb merged 8 commits from feature/p13-card-frames into main 2026-06-13 00:11:02 +09:00
2 changed files with 959 additions and 944 deletions
Showing only changes of commit 675616bf51 - Show all commits

View File

@@ -791,14 +791,16 @@ function upsertUi() {
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
],
}));
let insN = 6;
const INSPECT_CARD_COUNT = 60;
const INSPECT_CARD_W = 158;
const INSPECT_CARD_H = 214;
// id는 구 레이아웃(stride 7: root,Cost,Name,Desc,[NamePlate],[CostPlate],Art) 매핑 보존 —
// 같은 path가 항상 같은 id를 갖지 않으면 메이커 refresh의 id 기준 in-place 병합이 꼬임 (P13 실측)
for (let i = 1; i <= INSPECT_CARD_COUNT; i++) {
const insBase = 6 + (i - 1) * 7;
const cardPath = `/ui/DefaultGroup/DeckInspectHud/Grid/Card${i}`;
const card = entity({
id: guid('ins', insN++),
id: guid('ins', insBase),
path: cardPath,
modelId: 'uisprite',
entryId: 'UISprite',
@@ -812,10 +814,10 @@ function upsertUi() {
card.jsonString.enable = false;
inspect.push(card);
const inspectLayout = cardFaceLayout(INSPECT_CARD_W);
for (const [suffix, cfg] of inspectLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }])) {
for (const [tIdx, [suffix, cfg]] of inspectLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) {
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
inspect.push(entity({
id: guid('ins', insN++),
id: guid('ins', insBase + 1 + tIdx),
path: `${cardPath}/${suffix}`,
modelId: 'uitext',
entryId: 'UIText',
@@ -829,7 +831,7 @@ function upsertUi() {
}));
}
inspect.push(entity({
id: guid('ins', insN++),
id: guid('ins', insBase + 6),
path: `${cardPath}/Art`,
modelId: 'uisprite',
entryId: 'UISprite',
@@ -923,14 +925,15 @@ function upsertUi() {
scrollLayoutGroup({ cellSize: { x: 158, y: 214 }, spacing: { x: 22, y: 22 }, columns: 5 }),
],
}));
let allN = 6;
const ALL_DECK_CARD_COUNT = 120;
const ALL_DECK_CARD_W = 158;
const ALL_DECK_CARD_H = 214;
// id 매핑 보존 (stride 7) — DeckInspectHud 주석 참조
for (let i = 1; i <= ALL_DECK_CARD_COUNT; i++) {
const allBase = 6 + (i - 1) * 7;
const cardPath = `/ui/DefaultGroup/DeckAllHud/Grid/Card${i}`;
const card = entity({
id: guid('all', allN++),
id: guid('all', allBase),
path: cardPath,
modelId: 'uisprite',
entryId: 'UISprite',
@@ -944,10 +947,10 @@ function upsertUi() {
card.jsonString.enable = false;
allDeck.push(card);
const allDeckLayout = cardFaceLayout(ALL_DECK_CARD_W);
for (const [suffix, cfg] of allDeckLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }])) {
for (const [tIdx, [suffix, cfg]] of allDeckLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : '' }]).entries()) {
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
allDeck.push(entity({
id: guid('all', allN++),
id: guid('all', allBase + 1 + tIdx),
path: `${cardPath}/${suffix}`,
modelId: 'uitext',
entryId: 'UIText',
@@ -961,7 +964,7 @@ function upsertUi() {
}));
}
allDeck.push(entity({
id: guid('all', allN++),
id: guid('all', allBase + 6),
path: `${cardPath}/Art`,
modelId: 'uisprite',
entryId: 'UISprite',
@@ -1432,12 +1435,13 @@ function upsertUi() {
text({ value: '보상 카드 선택', fontSize: 44, bold: true, color: GOLD, alignment: 4 }),
],
}));
let rwdN = 2;
const rewardXs = [-300, 0, 300];
// id 매핑 보존 (stride 7) — DeckInspectHud 주석 참조
for (let i = 1; i <= 3; i++) {
const rwdBase = 2 + (i - 1) * 7;
const cardPath = `/ui/DefaultGroup/RewardHud/Reward${i}`;
reward.push(entity({
id: guid('rwd', rwdN++),
id: guid('rwd', rwdBase),
path: cardPath,
modelId: 'uisprite',
entryId: 'UISprite',
@@ -1450,10 +1454,10 @@ function upsertUi() {
],
}));
const rewardLayout = cardFaceLayout(CARD_W);
for (const [suffix, cfg] of rewardLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }])) {
for (const [tIdx, [suffix, cfg]] of rewardLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }]).entries()) {
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : 8;
reward.push(entity({
id: guid('rwd', rwdN++),
id: guid('rwd', rwdBase + 1 + tIdx),
path: `${cardPath}/${suffix}`,
modelId: 'uitext',
entryId: 'UIText',
@@ -1467,7 +1471,7 @@ function upsertUi() {
}));
}
reward.push(entity({
id: guid('rwd', rwdN++),
id: guid('rwd', rwdBase + 6),
path: `${cardPath}/Art`,
modelId: 'uisprite',
entryId: 'UISprite',
@@ -1479,6 +1483,7 @@ function upsertUi() {
],
}));
}
let rwdN = 2 + 3 * 7; // 구 시퀀스의 루프 종료 시점 값(23) 보존 — Skip 등 후속 id 불변
reward.push(entity({
id: guid('rwd', rwdN++),
path: '/ui/DefaultGroup/RewardHud/Skip',
@@ -1639,12 +1644,13 @@ function upsertUi() {
text({ value: '메소 0', fontSize: 28, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 }, alignment: 4 }),
],
}));
let shpN = 3;
const shopXs = [-300, 0, 300];
// id 매핑 보존 (stride 8: root,Cost,Name,Desc,Price,[NamePlate],[CostPlate],Art) — DeckInspectHud 주석 참조
for (let i = 1; i <= 3; i++) {
const shpBase = 3 + (i - 1) * 8;
const cardPath = `/ui/DefaultGroup/ShopHud/Card${i}`;
shop.push(entity({
id: guid('shp', shpN++),
id: guid('shp', shpBase),
path: cardPath,
modelId: 'uisprite',
entryId: 'UISprite',
@@ -1659,10 +1665,10 @@ function upsertUi() {
const shopLayout = cardFaceLayout(CARD_W);
const shopTexts = shopLayout.texts.map(([sfx, c]) => [sfx, { ...c, value: sfx === 'Cost' ? '1' : sfx === 'Name' ? '카드' : '' }]);
shopTexts.push(['Price', { size: { x: 160, y: 40 }, pos: { x: 0, y: -135 }, value: '30 메소', fontSize: 22, bold: true, color: { r: 0.98, g: 0.85, b: 0.4, a: 1 } }]);
for (const [suffix, cfg] of shopTexts) {
for (const [tIdx, [suffix, cfg]] of shopTexts.entries()) {
const dOrder = suffix === 'Cost' ? 7 : suffix === 'Name' ? 6 : suffix === 'Desc' ? 8 : 9;
shop.push(entity({
id: guid('shp', shpN++),
id: guid('shp', shpBase + 1 + tIdx),
path: `${cardPath}/${suffix}`,
modelId: 'uitext',
entryId: 'UIText',
@@ -1676,7 +1682,7 @@ function upsertUi() {
}));
}
shop.push(entity({
id: guid('shp', shpN++),
id: guid('shp', shpBase + 7),
path: `${cardPath}/Art`,
modelId: 'uisprite',
entryId: 'UISprite',
@@ -1688,6 +1694,7 @@ function upsertUi() {
],
}));
}
let shpN = 3 + 3 * 8; // 구 시퀀스의 루프 종료 시점 값(27) 보존 — Relic 등 후속 id 불변
shop.push(entity({
id: guid('shp', shpN++),
path: '/ui/DefaultGroup/ShopHud/Relic',
@@ -2342,6 +2349,14 @@ function upsertUi() {
appendUiSection(ui, section, entities);
}
// 엔티티 id 유일성 검증 — 같은 id가 다른 path에 재배정되면 메이커 refresh 병합이 꼬임
const seenIds = new Map();
for (const e of ui.ContentProto.Entities) {
const prev = seenIds.get(e.id);
if (prev != null) throw new Error(`[gen-slaydeck] 엔티티 id 중복: ${e.id} (${prev}${e.path})`);
seenIds.set(e.id, e.path);
}
JSON.parse(JSON.stringify(ui));
writeFileSync(UI_FILE, JSON.stringify(ui, null, 2), 'utf8');
}

File diff suppressed because it is too large Load Diff