diff --git a/UPDATES.md b/UPDATES.md index 93ad825..e720022 100644 --- a/UPDATES.md +++ b/UPDATES.md @@ -1,9 +1,13 @@ --- -updatedAt: 2026-09-21T00:42:09Z +updatedAt: 2026-09-21T01:10:33Z --- ## 2026년 9월 21일 +### 초록 보드 간편 활성화 확장 + +`IMPROVED` 모든 멤버의 초록 보드를 3칸·5칸·7칸 중 선택해 활성화할 수 있습니다. 5칸은 기존 세로 3칸의 가운데 양옆을, 7칸은 그 아래 2칸을 추가합니다. 이미 활성화한 다른 노드는 유지됩니다. 모든 멤버 간편 설정의 3칸·5칸·7칸 버튼에서 적용 범위를 작은 보드 그림으로 확인할 수 있습니다. + ### 사이트 주소 변경 사전 안내 `NOTICE` 모든 페이지 상단에 2026년 9월 28일 00:00(한국 시간) doricalc.cc 이전 예정 안내를 추가했습니다. 주소 변경 후 기존 저장 데이터를 사용하려면 JSON 백업 내보내기·가져오기가 필요합니다. 안내에서 사용자 데이터로 바로 이동할 수 있으며, 이전 직전 최신 데이터로 다시 백업해 주세요. diff --git a/src/lib/engine/board-green-presets.test.ts b/src/lib/engine/board-green-presets.test.ts new file mode 100644 index 0000000..1cc01db --- /dev/null +++ b/src/lib/engine/board-green-presets.test.ts @@ -0,0 +1,39 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { activateGreenNodePreset, type GreenNodePreset } from './board-green-presets.ts'; + +const summary = JSON.parse(readFileSync(new URL('../data/boards-summary.json', import.meta.url), 'utf8')); +const expected: Record = { + 3: ['G-001', 'G-002', 'G-005'], + 5: ['G-001', 'G-002', 'G-003', 'G-004', 'G-005'], + 7: ['G-001', 'G-002', 'G-003', 'G-004', 'G-005', 'G-006', 'G-007'] +}; + +for (const count of [3, 5, 7] as const) { + test(`${count}칸은 모든 멤버의 지정 초록 노드만 활성화한다`, () => { + const result = activateGreenNodePreset({}, summary.members, summary.models, count); + assert.equal(Object.keys(result).length, summary.members.length); + for (const member of summary.members) { + assert.deepEqual(result[member.id].toSorted(), expected[count], member.id); + } + }); +} + +test('7칸 적용 후 3칸·5칸을 반복 적용해도 기존 활성 상태를 보존하며 원본을 변경하지 않는다', () => { + const memberId = summary.members[0].id; + const original = { [memberId]: ['B-001', 'Y-001', 'G-001', 'G-008'], other: ['R-001'] }; + const snapshot = structuredClone(original); + const seven = activateGreenNodePreset(original, summary.members, summary.models, 7); + const three = activateGreenNodePreset(seven, summary.members, summary.models, 3); + const five = activateGreenNodePreset(three, summary.members, summary.models, 5); + assert.deepEqual(original, snapshot); + assert.deepEqual(five, seven); + assert.deepEqual(five[memberId].toSorted(), ['B-001', ...expected[7], 'G-008', 'Y-001']); + assert.deepEqual(five.other, ['R-001']); +}); + +test('선택 좌표에 다른 색 노드가 있으면 활성화하지 않는다', () => { + const models = { model: [{ groupId: 'B-001', x: 0, y: -1 }, { groupId: 'Y-001', x: -1, y: -2 }] }; + assert.deepEqual(activateGreenNodePreset({}, [{ id: 'a', positionGroupId: 'model' }], models, 7), { a: [] }); +}); diff --git a/src/lib/engine/board-green-presets.ts b/src/lib/engine/board-green-presets.ts new file mode 100644 index 0000000..9949f0d --- /dev/null +++ b/src/lib/engine/board-green-presets.ts @@ -0,0 +1,29 @@ +export type GreenNodePreset = 3 | 5 | 7; + +type BoardPosition = { groupId: string; x: number; y: number }; +type BoardMember = { id: string; positionGroupId: string }; + +// Ordered so the first 3, 5 or 7 coordinates define each preset and its preview. +export const greenNodePresetPositions = [ + { x: 0, y: -1 }, { x: 0, y: -2 }, { x: 0, y: -3 }, + { x: -1, y: -2 }, { x: 1, y: -2 }, + { x: 0, y: -4 }, { x: 0, y: -5 } +] as const; + +/** Add the central green branch without resetting any existing board progress. */ +export function activateGreenNodePreset( + memberBoards: Readonly>, + members: readonly BoardMember[], + models: Readonly>, + count: GreenNodePreset +): Record { + const result = Object.fromEntries(Object.entries(memberBoards).map(([id, nodes]) => [id, [...nodes]])); + const coordinates = greenNodePresetPositions.slice(0, count); + for (const member of members) { + const nodes = (models[member.positionGroupId] ?? []).filter(({ groupId, x, y }) => + groupId.startsWith('G-') && coordinates.some((position) => position.x === x && position.y === y) + ); + result[member.id] = [...new Set([...(result[member.id] ?? []), ...nodes.map((node) => node.groupId)])]; + } + return result; +} diff --git a/src/routes/data/+page.svelte b/src/routes/data/+page.svelte index b9fa8da..4419be8 100644 --- a/src/routes/data/+page.svelte +++ b/src/routes/data/+page.svelte @@ -7,6 +7,7 @@ import memorySummary from '$lib/data/memory-summary.json'; import MemberBoardEffectSummary from '$lib/components/MemberBoardEffectSummary.svelte'; import { summarizeMemberBoardEffects } from '$lib/engine/board-effects'; + import { activateGreenNodePreset, greenNodePresetPositions, type GreenNodePreset } from '$lib/engine/board-green-presets.ts'; import { calculateBoardEffects, calculateUnitPower, cardMatchesSearch, maximumWeightMatching, plainSkillText } from '$lib/engine/index.ts'; import { memoryParameterPermil } from '$lib/engine/memory.ts'; import type { BoardCardPlacements, CardListPreferences, CollectionCardState } from '$lib/models/user-data'; @@ -539,16 +540,10 @@ }); } - function activateCommonGreenNodesForAllMembers() { - const memberBoards = { ...userData.memberBoards }; - for (const member of boardMembers) { - const commonGreenNodes = boardModels[member.positionGroupId] - .filter((position) => position.x === 0 && [-1, -2, -3].includes(position.y) && boardColor(position.groupId) === 'green') - .map((position) => position.groupId); - memberBoards[member.id] = [...new Set([...(memberBoards[member.id] ?? []), ...commonGreenNodes])]; - } + function activateCommonGreenNodesForAllMembers(count: GreenNodePreset) { + const memberBoards = activateGreenNodePreset(userData.memberBoards, boardMembers, boardModels, count); persist({ ...userData, memberBoards }); - showMessage('모든 멤버의 중심 아래 초록 노드 3개를 활성화했습니다.'); + showMessage(`모든 멤버의 초록 ${count}칸을 활성화했습니다. 기존 활성 노드는 유지됩니다.`); } function toggleBoardNode(groupId: string) { @@ -1204,11 +1199,25 @@
- 간편 설정 + 모든 멤버 간편 설정 모든 멤버의 기본 초록 노드를 활성화하거나, 기존 배치를 유지한 채 중앙 연결 노드의 빈 슬롯에 보유 카드를 자동 배치합니다. + 초록색으로 채워진 노드가 적용 범위입니다. 맨 위 마름모는 중앙 연결 노드이며, 기존 활성 노드는 유지됩니다.
- +
+ {#each [3, 5, 7] as count} + + {/each} +
{#if automaticPlacementSnapshot}{/if}
@@ -1616,7 +1625,14 @@ .automatic-placement > div:first-child { display: grid; gap: 4px; min-width: 0; } .automatic-placement strong { color: #b7dec7; font-size: 14px; } .automatic-placement small { color: #82988c; font-size: 12px; line-height: 1.45; } - .automatic-placement-actions { display: flex; flex: 0 0 auto; gap: 7px; } + .automatic-placement-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; } + .green-presets { display: flex; gap: 7px; } + .automatic-placement button.green-preset { display: grid; justify-items: center; gap: 4px; width: 78px; padding: 8px; } + .green-preset svg { display: block; width: 60px; height: 100px; } + .preset-edge { fill: none; stroke: #4e6658; stroke-width: 2; } + .preset-center { fill: #bec8d5; stroke: #e4ebf2; stroke-width: 1.5; } + .preset-node { fill: #202d26; stroke: #71867a; stroke-width: 1.5; } + .preset-node.included { fill: #86e3a6; stroke: #b5f5ca; stroke-width: 2; } .automatic-placement button { min-height: 34px; padding: 7px 11px; border: 1px solid #466151; border-radius: 8px; color: #b9d7c5; background: #202d26; font-size: 12px; font-weight: 800; cursor: pointer; } .automatic-placement button.primary { border-color: #62aa7e; color: #101913; background: #78c493; } .automatic-placement button.undo { color: #c4ccd5; background: transparent; }