fix: scope blue board coverage to selected unit members
Publish GitHub Pages output / publish (push) Successful in 48s
Publish GitHub Pages output / publish (push) Successful in 48s
This commit is contained in:
+5
-1
@@ -1,9 +1,13 @@
|
||||
---
|
||||
updatedAt: 2026-09-09T00:00:00+09:00
|
||||
updatedAt: 2026-09-09T12:00:00+09:00
|
||||
---
|
||||
|
||||
## 2026년 9월 9일
|
||||
|
||||
### 연결 카드 최적화의 편성 대상 수정
|
||||
|
||||
`FIXED` 배치 분석의 연결 카드 최적화에서 편성에 없는 멤버의 파란 보드에도 카드를 필수 배치하던 문제를 수정했습니다. 큰 발동률 노드를 포함하는 필수 배치 조건은 편성 멤버에게만 적용하며, 편성 밖 보드의 전체 능력치·가창 보너스 등 점수에 기여하는 효과는 계속 반영합니다.
|
||||
|
||||
### Illusion Night 악곡 추가
|
||||
|
||||
`DATA` 파볼리아 레이네의 Illusion Night를 추가했습니다. Easy·Normal·Hard·Expert 채보와 곡별 점수 계산을 지원합니다.
|
||||
|
||||
@@ -94,6 +94,51 @@ test('큰 노드를 덮는 카드가 부족하면 파란 소켓을 비우거나
|
||||
}
|
||||
});
|
||||
|
||||
test('편성 밖 파란 소켓은 필수 배치에서 제외하고 여분 카드로도 빈 슬롯을 채우지 않는다', () => {
|
||||
for (const side of [-1, 1]) {
|
||||
const scopedSource = blueSource(side);
|
||||
scopedSource.members = ['okayu', 'korone', 'member'].map((id) => ({ id, positionGroupId: 'model' }));
|
||||
const boards = Object.fromEntries(scopedSource.members.map(({ id }) => [id, ['S-blue', 'B-large']]));
|
||||
const coveringCard = { ...rightCard, connect: [{ level: 1, effectPermilUp: 1000, extent: [{ x: -side * 2, y: 0 }] }] };
|
||||
for (const cards of [[coveringCard], [coveringCard, { ...coveringCard, id: 'spare' }]]) {
|
||||
const result = optimizeBoardConnectionCards(scopedSource, boards, cards, {
|
||||
requireBlueActivationCoverage: ['member'],
|
||||
preservePlacementOnTie: () => true,
|
||||
placementWeight: () => 0,
|
||||
placementTieWeight: (_card, memberId, socketId) => memberId === 'member' && socketId === 'S-blue' ? 6 : 0
|
||||
});
|
||||
assert.equal(result.assignments.length, 1);
|
||||
assert.equal(result.assignments[0].memberId, 'member');
|
||||
assert.equal(result.assignments[0].socketId, 'S-blue');
|
||||
}
|
||||
assert.throws(() => optimizeBoardConnectionCards(scopedSource, boards, [], {
|
||||
requireBlueActivationCoverage: ['member'], placementWeight: () => 0
|
||||
}), /큰 발동률 노드를 포함할 수 없어/);
|
||||
}
|
||||
});
|
||||
|
||||
test('편성 밖 보드의 점수 기여와 동률인 기존 배치는 유지한다', () => {
|
||||
const scopedSource = blueSource(1);
|
||||
scopedSource.members = ['member', 'outsider'].map((id) => ({ id, positionGroupId: 'model' }));
|
||||
const cards = ['required', 'global', 'existing'].map((id) => ({
|
||||
id, awakening: 0, connect: [{ level: 1, effectPermilUp: 1000, extent: [{ x: -2, y: 0 }] }]
|
||||
}));
|
||||
const result = optimizeBoardConnectionCards(scopedSource, {
|
||||
member: ['S-blue', 'B-large'], outsider: ['S-blue', 'B-large']
|
||||
}, cards, {
|
||||
requireBlueActivationCoverage: ['member'],
|
||||
currentPlacements: { outsider: { 'S-blue': 'existing' } },
|
||||
preservePlacementOnTie: () => true,
|
||||
placementWeight: (card, memberId, socketId) =>
|
||||
card.id === 'global' && memberId === 'outsider' && socketId === 'S-center' ? 100 : 0,
|
||||
placementTieWeight: (_card, memberId, socketId) => memberId === 'member' && socketId === 'S-blue' ? 6 : 0
|
||||
});
|
||||
assert.deepEqual(result.placements, {
|
||||
member: { 'S-blue': 'required' }, outsider: { 'S-center': 'global', 'S-blue': 'existing' }
|
||||
});
|
||||
assert.equal(new Set(result.assignments.map(({ cardId }) => cardId)).size, 3);
|
||||
});
|
||||
|
||||
test('모양과 수치가 같은 카드는 기존 배치를 유지하고 중복 배치하지 않는다', () => {
|
||||
const cards = ['a', 'b', 'c'].map((id) => ({ ...rightCard, id }));
|
||||
const currentPlacements = { member: { 'S-center': 'c', 'S-right': 'a' } };
|
||||
|
||||
@@ -52,7 +52,8 @@ export type BoardConnectionOptimizationOptions = {
|
||||
targetMemberId?: string;
|
||||
requireSoloSongBonus?: boolean;
|
||||
requireGroupSongBonus?: boolean;
|
||||
requireBlueActivationCoverage?: boolean;
|
||||
/** true covers every member; a member ID list scopes coverage to the current unit. */
|
||||
requireBlueActivationCoverage?: boolean | readonly string[];
|
||||
placementWeight?: (card: OptimizableConnectionCard, memberId: string, socketId: string) => number;
|
||||
placementPriorityWeight?: (card: OptimizableConnectionCard, memberId: string, socketId: string) => number;
|
||||
placementTieWeight?: (card: OptimizableConnectionCard, memberId: string, socketId: string) => number;
|
||||
@@ -137,7 +138,9 @@ export function optimizeBoardConnectionCards(
|
||||
// Large blue activation nodes must be in the card's actual extent, even
|
||||
// when inactive. Identify the blue side from coordinates, not left/right.
|
||||
const requiredNodes = slots.map(({ member, socket, positions }) =>
|
||||
options.requireBlueActivationCoverage && socket.x !== 0
|
||||
(options.requireBlueActivationCoverage === true
|
||||
|| (Array.isArray(options.requireBlueActivationCoverage) && options.requireBlueActivationCoverage.includes(member.id)))
|
||||
&& socket.x !== 0
|
||||
? positions.filter((position) => {
|
||||
const node = nodeForMember(source, position.groupId, member.id);
|
||||
return position.groupId.startsWith('B-') && position.x * socket.x > 0
|
||||
|
||||
@@ -687,7 +687,7 @@
|
||||
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
||||
const optimized = optimizeBoardConnectionCards(boardSummary, effectiveMemberBoards, candidateCards, {
|
||||
currentPlacements: boardCards,
|
||||
requireBlueActivationCoverage: true,
|
||||
requireBlueActivationCoverage: selectedSourceCards.map((card) => card.characterId),
|
||||
preservePlacementOnTie: () => true,
|
||||
placementWeight: (card, memberId, socketId) => placementMetrics(card, memberId, socketId).score,
|
||||
placementTieWeight: (card, memberId, socketId) => placementMetrics(card, memberId, socketId).activationRateUp
|
||||
@@ -1104,7 +1104,7 @@
|
||||
|
||||
<section class="connection-optimizer" aria-labelledby="connection-optimizer-title">
|
||||
<div class="connection-optimizer-heading">
|
||||
<div><h3 id="connection-optimizer-title">연결 카드 최적화</h3><p>활성 노드는 유지하며, 파란 영역의 연결 카드는 반드시 인근의 큰 발동률 노드를 포함합니다. 이 조건 안에서 최고점을 최우선으로 비교하고 최고점이 같으면 액티브 발동률 UP 합계를 비교합니다. 두 값이 모두 같으면 기존 연결 카드 배치를 우선 유지합니다. 조건을 충족할 수 없거나 최종 검증에서 최고점이 낮아지면 기존 배치를 유지합니다.</p></div>
|
||||
<div><h3 id="connection-optimizer-title">연결 카드 최적화</h3><p>활성 노드는 유지하며, 편성 멤버의 파란 영역 연결 카드는 반드시 인근의 큰 발동률 노드를 포함합니다. 편성 밖 멤버의 보드도 현재 편성의 점수에 기여하는 효과는 최적화에 반영합니다. 이 조건 안에서 최고점을 최우선으로 비교하고 최고점이 같으면 액티브 발동률 UP 합계를 비교합니다. 두 값이 모두 같으면 기존 연결 카드 배치를 우선 유지합니다. 조건을 충족할 수 없거나 최종 검증에서 최고점이 낮아지면 기존 배치를 유지합니다.</p></div>
|
||||
<div class="connection-optimizer-actions">
|
||||
{#if connectionOptimizationSnapshot}<button type="button" disabled={running || connectionOptimizationRunning} on:click={undoConnectionOptimization}>직전 배치 복원</button>{/if}
|
||||
<button type="button" class="primary" disabled={running || connectionOptimizationRunning} on:click={optimizeConnectionPlacements}>{connectionOptimizationRunning ? '배치 탐색 중…' : '보유 카드로 최적화'}</button>
|
||||
|
||||
Reference in New Issue
Block a user