feat: release version 1.0.7

This commit is contained in:
2026-08-21 19:06:55 +09:00
parent 5d29b63f3a
commit 71dfe11bc4
406 changed files with 3086 additions and 1172 deletions
+44 -49
View File
@@ -58,6 +58,8 @@
type AutomaticPlacementResult = {
memberId: string;
memberName: string;
socketId: string;
socketName: string;
cardId: string;
cardName: string;
rarity: number;
@@ -155,7 +157,8 @@
: [];
$: efficiencyFormationReady = efficiencyFormation.cardIds.length === 5
&& efficiencyFormation.cardIds.every((id) => cardById.has(id))
&& new Set(efficiencyFormation.cardIds.map((id) => cardById.get(id)?.characterId)).size === 5;
&& new Set(efficiencyFormation.cardIds.map((id) => cardById.get(id)?.characterId)).size === 5
&& efficiencyFormation.cardIds.some((id) => cardById.get(id)?.characterId === efficiencyFormation.leaderCharacterId);
$: dataView = $page.url.pathname.endsWith('/cards') ? 'cards' : $page.url.pathname.endsWith('/boards') ? 'boards' : 'home';
onMount(() => {
try {
@@ -241,11 +244,12 @@
function importAnalysisFormation() {
if (!savedAnalysis) return;
const analysis = savedAnalysis;
saveEfficiencyFormation({
cardIds: [...savedAnalysis.cardIds],
awakenings: savedAnalysis.awakenings.map(normalizeEfficiencyAwakening),
leaderCharacterId: savedAnalysis.leaderCharacterId,
leaderOutfitCardId: savedAnalysis.leaderOutfitCardId,
cardIds: [...analysis.cardIds],
awakenings: analysis.awakenings.map(normalizeEfficiencyAwakening),
leaderCharacterId: analysis.leaderCharacterId,
leaderOutfitCardId: analysis.leaderOutfitCardId,
coreCubeFreeOnly: efficiencyFormation.coreCubeFreeOnly
});
}
@@ -570,7 +574,7 @@
}, { points: 0, greenCubes: 0, greenCoreCubes: 0 });
}
function currentFormationPower(memberBoards: Record<string, string[]>): number {
function currentFormationPower(memberBoards: Record<string, string[]>, boardCards: BoardCardPlacements = userData.boardCards): number {
if (!efficiencyFormationReady) return 0;
const members = efficiencyFormation.cardIds.map((id, index) => ({
card: cardById.get(id)!,
@@ -584,7 +588,7 @@
connect: card.details.connect
}));
const boardEffects = calculateBoardEffects(boardSummary, memberBoards, sourceCards, efficiencyFormation.leaderCharacterId, null, {
placements: userData.boardCards,
placements: boardCards,
cards: connections
});
const leaderOutfit = cardById.get(efficiencyFormation.leaderOutfitCardId)?.details.costume ?? null;
@@ -626,15 +630,6 @@
|| right.cost.greenCubes - left.cost.greenCubes;
}
function upgradeBeatsRemoval(upgrade: GreenNodeEfficiency, removal: GreenNodeEfficiency): boolean {
const cubeDifference = upgrade.greenCubeEfficiency - removal.greenCubeEfficiency;
if (Math.abs(cubeDifference) > 1e-9) return cubeDifference > 0;
if (upgrade.cost.greenCoreCubes === 0 && removal.cost.greenCoreCubes > 0) return true;
if (upgrade.cost.greenCoreCubes > 0 && removal.cost.greenCoreCubes === 0) return false;
if (upgrade.cost.greenCoreCubes === 0 && removal.cost.greenCoreCubes === 0) return false;
return upgrade.greenCoreCubeEfficiency > removal.greenCoreCubeEfficiency;
}
async function analyzeGreenNodeEfficiency() {
if (!efficiencyFormationReady) {
greenEfficiencyError = '서로 다른 멤버의 5성 카드 5장을 모두 선택해 주세요.';
@@ -702,11 +697,9 @@
}
const sortedRemovals = removals.sort(compareRemovalEfficiency);
const removalThreshold = sortedRemovals[0];
greenEfficiencyResult = {
basePower,
upgrades: upgrades.sort(compareUpgradeEfficiency)
.filter((upgrade) => !removalThreshold || upgradeBeatsRemoval(upgrade, removalThreshold)),
upgrades: upgrades.sort(compareUpgradeEfficiency),
removals: sortedRemovals
};
} catch (error) {
@@ -818,45 +811,37 @@
if (!socket || userData.boardCards[member.id]?.[socket.groupId]) return [];
return [{ member, socket }];
});
if (candidateCards.length === 0) {
showMessage('자동 배치에 사용할 수 있는 미배치 보유 카드가 없습니다.', true);
return;
}
if (emptyGreenSlots.length === 0) {
showMessage('비어 있는 초록색 카드 슬롯이 없습니다. 기존 배치는 변경하지 않았습니다.');
showMessage('비어 있는 중앙 연결 노드가 없습니다. 기존 배치는 변경하지 않았습니다.');
return;
}
const weights = candidateCards.map((card) => emptyGreenSlots.map(({ member, socket }) => greenSlotScore(member, socket, card)));
const matches = maximumWeightMatching(weights);
const matches = maximumWeightMatching(candidateCards.map((card) =>
emptyGreenSlots.map(({ member, socket }) => greenSlotScore(member, socket, card))
));
if (matches.length === 0) {
showMessage('활성화된 초록 노드에 효과를 주는 배치 조합을 찾지 못했습니다.', true);
showMessage('활성화된 초록 노드에 효과를 주는 중앙 배치를 찾지 못했습니다.', true);
return;
}
automaticPlacementSnapshot = cloneBoardCards(userData.boardCards);
const boardCards = cloneBoardCards(userData.boardCards);
const placementResult: AutomaticPlacementResult[] = [];
for (const match of matches) {
automaticPlacementResult = matches.map((match) => {
const card = candidateCards[match.rowIndex];
const slot = emptyGreenSlots[match.columnIndex];
if (!card || !slot) continue;
boardCards[slot.member.id] = { ...(boardCards[slot.member.id] ?? {}), [slot.socket.groupId]: card.id };
placementResult.push({
memberId: slot.member.id,
memberName: slot.member.name,
cardId: card.id,
cardName: cardLabel(card),
rarity: card.rarity
});
}
automaticPlacementResult = placementResult.sort((left, right) =>
const { member, socket } = emptyGreenSlots[match.columnIndex];
boardCards[member.id] = { ...(boardCards[member.id] ?? {}), [socket.groupId]: card.id };
return {
memberId: member.id, memberName: member.name, socketId: socket.groupId, socketName: '중앙',
cardId: card.id, cardName: cardLabel(card), rarity: card.rarity
};
}).sort((left, right) =>
(boardMembers.find((member) => member.id === left.memberId)?.order ?? Number.MAX_SAFE_INTEGER)
- (boardMembers.find((member) => member.id === right.memberId)?.order ?? Number.MAX_SAFE_INTEGER)
);
persist({ ...userData, boardCards });
showMessage(`기존 배치를 유지하고 초록색 빈 슬롯 ${matches.length}곳에 보유 카드를 최적 배치했습니다.`);
showMessage(`기존 배치를 유지하고 중앙 연결 노드 ${matches.length}곳에 보유 카드를 자동 배치했습니다.`);
}
function undoAutomaticPlacement() {
@@ -865,7 +850,7 @@
automaticPlacementSnapshot = null;
automaticPlacementResult = [];
persist({ ...userData, boardCards });
showMessage('방금 실행한 초록색 카드 자동 배치를 취소했습니다.');
showMessage('방금 실행한 중앙 연결 노드 자동 배치를 취소했습니다.');
}
function cardLabel(card: CardSummary): string {
@@ -875,7 +860,7 @@
function placedCardName(cardId: string | undefined): string {
if (!cardId) return '연결 노드';
const card = connectCardById.get(cardId);
return card ? cardLabel(card) : cardId;
return card ? `${card.rarity}★ ${cardLabel(card)}` : cardId;
}
function connectionBoostsForMember(
@@ -917,6 +902,7 @@
function assignBoardCard(cardId: string) {
if (!selectedSocketGroupId) return;
automaticPlacementSnapshot = null;
automaticPlacementResult = [];
const boardCards = Object.fromEntries(Object.entries(userData.boardCards).map(([memberId, placements]) => [memberId, { ...placements }]));
for (const [memberId, placements] of Object.entries(boardCards)) {
for (const [socketId, placedCardId] of Object.entries(placements)) {
@@ -932,6 +918,7 @@
function removeBoardCard() {
if (!selectedSocketGroupId) return;
automaticPlacementSnapshot = null;
automaticPlacementResult = [];
const placements = { ...(userData.boardCards[selectedMemberId] ?? {}) };
delete placements[selectedSocketGroupId];
const boardCards = { ...userData.boardCards };
@@ -1168,7 +1155,7 @@
<div class="automatic-placement">
<div>
<strong>간편 설정</strong>
<small>모든 멤버의 기본 초록 노드를 활성화하거나, 보유 카드를 중앙 연결 노드의 빈 슬롯에 자동 배치합니다.</small>
<small>모든 멤버의 기본 초록 노드를 활성화하거나, 기존 배치를 유지한 채 중앙 연결 노드의 빈 슬롯에 보유 카드를 자동 배치합니다.</small>
</div>
<div class="automatic-placement-actions">
<button type="button" on:click={activateCommonGreenNodesForAllMembers}>모든 멤버 초록 3칸 활성화</button>
@@ -1271,7 +1258,7 @@
<div class="efficiency-columns">
<section class="efficiency-column upgrade" aria-labelledby="upgrade-efficiency-title">
<div class="efficiency-column-heading">
<div><h3 id="upgrade-efficiency-title">우선 활성화</h3><p>초기화 검토 1순위보다 효율이 높은 노드만 표시</p></div>
<div><h3 id="upgrade-efficiency-title">우선 활성화</h3><p>초기화 효율보다 낮은 후보도 포함해 효율순으로 최대 20개 표시</p></div>
</div>
{#if greenEfficiencyResult.upgrades.length > 0}
<div class="efficiency-list">
@@ -1367,7 +1354,7 @@
class:efficiency-focus={position.groupId === efficiencyFocusGroupId}
style={`left: ${point.x}px; top: ${point.y}px; width: ${size}px; height: ${size}px;`}
title={`${boardNodeLabel(node.type)} · ${boardEffectDescription(node.effect, selectedBoardMember.name) || placedCardName(selectedBoardCardPlacements[position.groupId])}${selectedConnectionBoosts[position.groupId] ? ` · ${active ? '연결 효과 적용' : '연결 범위'} +${selectedConnectionBoosts[position.groupId].percent}%` : ''}`}
aria-label={`${boardNodeLabel(node.type)} ${node.grade}등급 노드, ${active ? '활성화됨' : '비활성화됨'}${selectedConnectionBoosts[position.groupId] ? `, 연결 ${active ? '효과 적용 중' : '범위에 포함'}` : ''}`}
aria-label={`${boardNodeLabel(node.type)} ${node.grade}등급 노드, ${active ? '활성화됨' : '비활성화됨'}${node.type === 'connection' && selectedBoardCardPlacements[position.groupId] ? `, 배치 카드 ${placedCardName(selectedBoardCardPlacements[position.groupId])}` : ''}${selectedConnectionBoosts[position.groupId] ? `, 연결 ${active ? '효과 적용 중' : '범위에 포함'}` : ''}`}
aria-pressed={active}
on:click={() => handleBoardNodeClick(position.groupId, node)}
>
@@ -1380,7 +1367,12 @@
{#if boardEffectIcon(node)}<img class="board-node-icon" src={boardEffectIcon(node) ?? ''} alt="" draggable="false" />{/if}
{#if node.type === 'connection' && selectedBoardCardPlacements[position.groupId]}
{@const placedCard = connectCardById.get(selectedBoardCardPlacements[position.groupId])}
{#if placedCard}<span class="socket-card" aria-hidden="true">{placedCard.name.slice(0, 2)}</span>{/if}
{#if placedCard}
<span class="socket-card" aria-hidden="true">
<span class="socket-card-name">{placedCard.name.slice(0, 2)}</span>
<small class:rarity-five={placedCard.rarity === 5}>{placedCard.rarity}★</small>
</span>
{/if}
{/if}
</button>
{/if}
@@ -1564,7 +1556,7 @@
.automatic-placement-result-item span { color: #91a59a; font-size: 12px; }
.automatic-placement-result-item b { display: flex; align-items: center; gap: 7px; min-width: 0; color: #dbe8df; font-size: 13px; }
.automatic-placement-result-item i { flex: 0 0 auto; color: #e7bd70; font-size: 11px; font-style: normal; }
.green-efficiency { order: 2; margin: 18px 0 0; padding: 0 16px 16px; border: 1px solid #394453; border-radius: 16px; background: #151b23; }
.green-efficiency { order: 3; margin: 18px 0 0; padding: 0 16px 16px; border: 1px solid #394453; border-radius: 16px; background: #151b23; }
.green-efficiency > summary { display: flex; align-items: center; min-height: 62px; margin: 0 -16px; padding: 12px 16px; color: #dce6e0; cursor: pointer; list-style: none; }
.green-efficiency > summary::-webkit-details-marker { display: none; }
.green-efficiency > summary:hover { background: rgb(255 255 255 / 3%); }
@@ -1657,7 +1649,10 @@
.board-node.connect-applied::after { inset: -8px; border: 4px solid #fff; }
.board-node.connect-applied::before { content: '+'; position: absolute; z-index: 4; top: -13px; right: -13px; display: grid; place-items: center; width: 19px; height: 19px; border: 2px solid #151a22; border-radius: 50%; color: #111820; background: #fff; font-size: 13px; font-weight: 950; line-height: 1; pointer-events: none; }
.board-node.efficiency-focus { z-index: 6; outline: 4px solid #fff; outline-offset: 6px; border-radius: 50%; }
.socket-card { position: absolute; inset: 19%; z-index: 2; display: grid; place-items: center; overflow: hidden; border-radius: 50%; color: #fff; background: #3b2d46; font-size: 12px; font-weight: 900; line-height: 1; pointer-events: none; }
.socket-card { position: absolute; inset: 19%; z-index: 2; pointer-events: none; }
.socket-card-name { display: grid; width: 100%; height: 100%; place-items: center; overflow: hidden; border-radius: 50%; color: #fff; background: #3b2d46; font-size: 12px; font-weight: 900; line-height: 1; }
.socket-card small { position: absolute; right: -12px; bottom: -8px; min-width: 24px; padding: 2px 4px; border: 2px solid #151a22; border-radius: 999px; color: #fff; background: #7562c9; font-size: 9px; font-weight: 950; line-height: 1.1; white-space: nowrap; }
.socket-card small.rarity-five { background: #d96f9d; }
.board-node:hover { z-index: 4; transform: translate(-50%, -50%) scale(1.1); }
.board-node.center { cursor: default; }
.board-node:focus-visible { z-index: 5; outline: 3px solid rgb(255 255 255 / 55%); outline-offset: 3px; border-radius: 50%; }