feat: add visual green board activation presets
Publish GitHub Pages output / publish (push) Successful in 31s
Publish GitHub Pages output / publish (push) Successful in 31s
This commit is contained in:
+5
-1
@@ -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 백업 내보내기·가져오기가 필요합니다. 안내에서 사용자 데이터로 바로 이동할 수 있으며, 이전 직전 최신 데이터로 다시 백업해 주세요.
|
||||
|
||||
@@ -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<GreenNodePreset, string[]> = {
|
||||
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: [] });
|
||||
});
|
||||
@@ -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<Record<string, readonly string[]>>,
|
||||
members: readonly BoardMember[],
|
||||
models: Readonly<Record<string, readonly BoardPosition[]>>,
|
||||
count: GreenNodePreset
|
||||
): Record<string, string[]> {
|
||||
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;
|
||||
}
|
||||
@@ -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 @@
|
||||
</aside>
|
||||
<div class="automatic-placement">
|
||||
<div>
|
||||
<strong>간편 설정</strong>
|
||||
<strong>모든 멤버 간편 설정</strong>
|
||||
<small>모든 멤버의 기본 초록 노드를 활성화하거나, 기존 배치를 유지한 채 중앙 연결 노드의 빈 슬롯에 보유 카드를 자동 배치합니다.</small>
|
||||
<small>초록색으로 채워진 노드가 적용 범위입니다. 맨 위 마름모는 중앙 연결 노드이며, 기존 활성 노드는 유지됩니다.</small>
|
||||
</div>
|
||||
<div class="automatic-placement-actions">
|
||||
<button type="button" on:click={activateCommonGreenNodesForAllMembers}>모든 멤버 초록 3칸 활성화</button>
|
||||
<div class="green-presets" role="group" aria-label="모든 멤버 초록 노드 활성화">
|
||||
{#each [3, 5, 7] as count}
|
||||
<button type="button" class="green-preset" aria-label={`모든 멤버 초록 ${count}칸 활성화`} on:click={() => activateCommonGreenNodesForAllMembers(count as GreenNodePreset)}>
|
||||
<svg viewBox="0 0 76 126" aria-hidden="true">
|
||||
<path class="preset-edge" d="M38 12V112 M18 52H58" />
|
||||
<path class="preset-center" d="M38 5L45 12L38 19L31 12Z" />
|
||||
{#each greenNodePresetPositions as position, index}
|
||||
<circle class="preset-node" class:included={index < count} cx={38 + position.x * 20} cy={12 - position.y * 20} r="6" />
|
||||
{/each}
|
||||
</svg>
|
||||
<span>{count}칸</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if automaticPlacementSnapshot}<button type="button" class="undo" on:click={undoAutomaticPlacement}>방금 배치 취소</button>{/if}
|
||||
<button type="button" class="primary" on:click={optimizeEmptyGreenSlots}>중앙 연결 노드 자동 배치</button>
|
||||
</div>
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user