feat: improve analysis and release v1.0.2
This commit is contained in:
@@ -36,6 +36,7 @@
|
||||
awakenings: number[];
|
||||
leaderCharacterId: string;
|
||||
leaderOutfitCardId: string;
|
||||
coreCubeFreeOnly: boolean;
|
||||
};
|
||||
type GreenNodeCost = { points: number; greenCubes: number; greenCoreCubes: number };
|
||||
type GreenNodeEfficiency = {
|
||||
@@ -54,6 +55,13 @@
|
||||
upgrades: GreenNodeEfficiency[];
|
||||
removals: GreenNodeEfficiency[];
|
||||
};
|
||||
type AutomaticPlacementResult = {
|
||||
memberId: string;
|
||||
memberName: string;
|
||||
cardId: string;
|
||||
cardName: string;
|
||||
rarity: number;
|
||||
};
|
||||
const ANALYSIS_STORAGE_KEY = 'holodori.analysis.v2';
|
||||
const EFFICIENCY_FORMATION_STORAGE_KEY = 'holodori.green-efficiency-formation.v1';
|
||||
const GREEN_CUBE_ID = 'item-skill_tree_allmember-1';
|
||||
@@ -100,12 +108,14 @@
|
||||
let selectedSocketGroupId = '';
|
||||
let boardCardSearch = '';
|
||||
let automaticPlacementSnapshot: BoardCardPlacements | null = null;
|
||||
let automaticPlacementResult: AutomaticPlacementResult[] = [];
|
||||
let savedAnalysis: SavedAnalysisState | null = null;
|
||||
let efficiencyFormation: EfficiencyFormationState = {
|
||||
cardIds: ['', '', '', '', ''],
|
||||
awakenings: [0, 0, 0, 0, 0],
|
||||
leaderCharacterId: '',
|
||||
leaderOutfitCardId: ''
|
||||
leaderOutfitCardId: '',
|
||||
coreCubeFreeOnly: false
|
||||
};
|
||||
let greenEfficiencyResult: GreenEfficiencyResult | null = null;
|
||||
let greenEfficiencyRunning = false;
|
||||
@@ -163,7 +173,8 @@
|
||||
cardIds: stored.cardIds.map((id) => cardById.has(id) ? id : ''),
|
||||
awakenings: stored.awakenings.map(normalizeEfficiencyAwakening),
|
||||
leaderCharacterId: leaderMembers.some((member) => member.characterId === stored.leaderCharacterId) ? stored.leaderCharacterId : '',
|
||||
leaderOutfitCardId: cardById.get(stored.leaderOutfitCardId)?.details.costume ? stored.leaderOutfitCardId : ''
|
||||
leaderOutfitCardId: cardById.get(stored.leaderOutfitCardId)?.details.costume ? stored.leaderOutfitCardId : '',
|
||||
coreCubeFreeOnly: stored.coreCubeFreeOnly === true
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
@@ -222,13 +233,18 @@
|
||||
saveEfficiencyFormation({ ...efficiencyFormation, leaderOutfitCardId: cardId });
|
||||
}
|
||||
|
||||
function updateCoreCubeFreeOnly(coreCubeFreeOnly: boolean) {
|
||||
saveEfficiencyFormation({ ...efficiencyFormation, coreCubeFreeOnly });
|
||||
}
|
||||
|
||||
function importAnalysisFormation() {
|
||||
if (!savedAnalysis) return;
|
||||
saveEfficiencyFormation({
|
||||
cardIds: [...savedAnalysis.cardIds],
|
||||
awakenings: savedAnalysis.awakenings.map(normalizeEfficiencyAwakening),
|
||||
leaderCharacterId: savedAnalysis.leaderCharacterId,
|
||||
leaderOutfitCardId: savedAnalysis.leaderOutfitCardId
|
||||
leaderOutfitCardId: savedAnalysis.leaderOutfitCardId,
|
||||
coreCubeFreeOnly: efficiencyFormation.coreCubeFreeOnly
|
||||
});
|
||||
}
|
||||
|
||||
@@ -625,6 +641,7 @@
|
||||
const path = shortestPathToSet(position.groupId, active, positions);
|
||||
const added = path.filter((groupId) => groupId !== center && !active.has(groupId));
|
||||
if (added.length === 0) continue;
|
||||
if (efficiencyFormation.coreCubeFreeOnly && greenNodeCost(member.id, added).greenCoreCubes > 0) continue;
|
||||
const nextBoards = { ...userData.memberBoards, [member.id]: [...new Set([...currentNodes, ...added])] };
|
||||
const nextPower = currentFormationPower(nextBoards);
|
||||
const intermediateNodes = added.filter((groupId) => groupId !== position.groupId);
|
||||
@@ -643,6 +660,7 @@
|
||||
const reachable = reachableNodesForPositions(remaining, positions, center);
|
||||
const removed = currentNodes.filter((groupId) => !reachable.has(groupId));
|
||||
if (removed.length === 0) continue;
|
||||
if (efficiencyFormation.coreCubeFreeOnly && greenNodeCost(member.id, removed).greenCoreCubes > 0) continue;
|
||||
const nextMemberNodes = currentNodes.filter((groupId) => reachable.has(groupId));
|
||||
const nextBoards = { ...userData.memberBoards };
|
||||
if (nextMemberNodes.length > 0) nextBoards[member.id] = nextMemberNodes;
|
||||
@@ -788,12 +806,24 @@
|
||||
|
||||
automaticPlacementSnapshot = cloneBoardCards(userData.boardCards);
|
||||
const boardCards = cloneBoardCards(userData.boardCards);
|
||||
const placementResult: AutomaticPlacementResult[] = [];
|
||||
for (const match of matches) {
|
||||
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) =>
|
||||
(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}곳에 보유 카드를 최적 배치했습니다.`);
|
||||
}
|
||||
@@ -802,6 +832,7 @@
|
||||
if (!automaticPlacementSnapshot) return;
|
||||
const boardCards = cloneBoardCards(automaticPlacementSnapshot);
|
||||
automaticPlacementSnapshot = null;
|
||||
automaticPlacementResult = [];
|
||||
persist({ ...userData, boardCards });
|
||||
showMessage('방금 실행한 초록색 카드 자동 배치를 취소했습니다.');
|
||||
}
|
||||
@@ -1111,6 +1142,19 @@
|
||||
{#if automaticPlacementSnapshot}<button type="button" class="undo" on:click={undoAutomaticPlacement}>방금 배치 취소</button>{/if}
|
||||
<button type="button" class="primary" on:click={optimizeEmptyGreenSlots}>중앙 연결 노드 자동 배치</button>
|
||||
</div>
|
||||
{#if automaticPlacementResult.length > 0}
|
||||
<div class="automatic-placement-result" aria-live="polite">
|
||||
<strong>자동 배치 결과 <span>{automaticPlacementResult.length}건</span></strong>
|
||||
<div class="automatic-placement-result-list">
|
||||
{#each automaticPlacementResult as placement (placement.memberId)}
|
||||
<div class="automatic-placement-result-item">
|
||||
<span>{placement.memberName} 보드</span>
|
||||
<b><i>{placement.rarity}★</i>{placement.cardName}</b>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
<details class="green-efficiency">
|
||||
@@ -1173,6 +1217,15 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="efficiency-outfit-effect"><span>리더 의상 효과</span><p>{plainSkillText(cardById.get(efficiencyFormation.leaderOutfitCardId)?.details.costume?.description ?? '') || '적용하지 않음'}</p></div>
|
||||
<label class="efficiency-core-filter">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={efficiencyFormation.coreCubeFreeOnly}
|
||||
disabled={greenEfficiencyRunning}
|
||||
on:change={(event) => updateCoreCubeFreeOnly(event.currentTarget.checked)}
|
||||
/>
|
||||
<span><strong>코어 큐브 미소모만</strong><small>활성화 또는 초기화 경로 전체에서 그린 코어 큐브를 소모하지 않는 결과만 찾습니다.</small></span>
|
||||
</label>
|
||||
</div>
|
||||
{#if greenEfficiencyError}<p class="efficiency-error" role="alert">{greenEfficiencyError}</p>{/if}
|
||||
{#if greenEfficiencyResult}
|
||||
@@ -1201,7 +1254,7 @@
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}<p class="efficiency-empty">종합력을 올리는 비활성 초록 노드가 없습니다.</p>{/if}
|
||||
{:else}<p class="efficiency-empty">{efficiencyFormation.coreCubeFreeOnly ? '코어 큐브를 소모하지 않고 종합력을 올리는 비활성 초록 노드가 없습니다.' : '종합력을 올리는 비활성 초록 노드가 없습니다.'}</p>{/if}
|
||||
</section>
|
||||
<section class="efficiency-column removal" aria-labelledby="removal-efficiency-title">
|
||||
<div class="efficiency-column-heading">
|
||||
@@ -1221,7 +1274,7 @@
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}<p class="efficiency-empty">초기화를 검토할 활성 초록 노드가 없습니다.</p>{/if}
|
||||
{:else}<p class="efficiency-empty">{efficiencyFormation.coreCubeFreeOnly ? '코어 큐브를 환급하지 않는 활성 초록 노드가 없습니다.' : '초기화를 검토할 활성 초록 노드가 없습니다.'}</p>{/if}
|
||||
</section>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1457,7 +1510,7 @@
|
||||
.board-toolbar p strong { color: #f0a1c2; font-size: 16px; }
|
||||
.board-toolbar div { display: flex; gap: 6px; }
|
||||
.board-toolbar button { padding: 6px 9px; border: 1px solid #394252; border-radius: 7px; color: #aeb7c5; background: #202630; font-size: 12px; font-weight: 750; cursor: pointer; }
|
||||
.automatic-placement { display: flex; align-items: center; justify-content: space-between; gap: 18px; margin: 0 0 18px; padding: 12px 14px; border: 1px solid #355044; border-radius: 10px; background: rgb(58 113 83 / 10%); }
|
||||
.automatic-placement { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 18px; margin: 0 0 18px; padding: 12px 14px; border: 1px solid #355044; border-radius: 10px; background: rgb(58 113 83 / 10%); }
|
||||
.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; }
|
||||
@@ -1467,6 +1520,14 @@
|
||||
.automatic-placement button.undo { color: #c4ccd5; background: transparent; }
|
||||
.automatic-placement button:hover { filter: brightness(1.1); }
|
||||
.automatic-placement button:focus-visible { outline: 3px solid rgb(120 196 147 / 25%); outline-offset: 2px; }
|
||||
.automatic-placement-result { display: grid; flex: 1 0 100%; gap: 9px; padding-top: 12px; border-top: 1px solid rgb(120 196 147 / 18%); }
|
||||
.automatic-placement-result > strong { display: flex; align-items: center; gap: 7px; color: #d8e9df; font-size: 14px; }
|
||||
.automatic-placement-result > strong span { color: #78c493; font-size: 12px; }
|
||||
.automatic-placement-result-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 6px 10px; max-height: 280px; overflow-y: auto; padding-right: 4px; }
|
||||
.automatic-placement-result-item { display: grid; grid-template-columns: minmax(105px, 0.8fr) minmax(150px, 1.2fr); align-items: center; gap: 10px; min-height: 38px; padding: 7px 10px; border: 1px solid rgb(120 196 147 / 13%); border-radius: 7px; background: rgb(9 18 13 / 34%); }
|
||||
.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 > 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; }
|
||||
@@ -1503,6 +1564,12 @@
|
||||
.efficiency-outfit-effect { display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 10px; margin-top: 9px; padding: 9px 10px; border-radius: 8px; background: #171e26; }
|
||||
.efficiency-outfit-effect span { color: #d990af; font-size: 13px; font-weight: 850; }
|
||||
.efficiency-outfit-effect p { margin: 0; color: #a7b0bd; font-size: 13px; line-height: 1.5; }
|
||||
.efficiency-core-filter { display: flex; align-items: center; gap: 10px; margin-top: 9px; padding: 10px 11px; border: 1px solid #314238; border-radius: 8px; background: #111a15; cursor: pointer; }
|
||||
.efficiency-core-filter input { width: 18px; height: 18px; margin: 0; accent-color: #78c493; cursor: pointer; }
|
||||
.efficiency-core-filter input:disabled { cursor: default; }
|
||||
.efficiency-core-filter > span { display: grid; gap: 2px; }
|
||||
.efficiency-core-filter strong { color: #dbe5df; font-size: 14px; }
|
||||
.efficiency-core-filter small { color: #8f9e96; font-size: 12px; line-height: 1.45; }
|
||||
.efficiency-summary { display: flex; flex-wrap: wrap; align-items: baseline; gap: 5px 12px; margin-top: 14px; padding: 10px 12px; border-radius: 8px; background: #10151c; }
|
||||
.efficiency-summary span { color: #8b96a5; font-size: 13px; }
|
||||
.efficiency-summary strong { margin-right: 10px; color: #d6dde7; font-size: 16px; }
|
||||
|
||||
Reference in New Issue
Block a user