feat: preview sortie composition synergy
This commit is contained in:
@@ -23,6 +23,14 @@ import {
|
||||
type SortieFormationAssignments,
|
||||
type SortieFormationRole
|
||||
} from '../data/sortieDeployment';
|
||||
import {
|
||||
compareSortieSynergy,
|
||||
coreSortieSynergyRoles,
|
||||
evaluateSortieSynergy,
|
||||
firstPursuitRolePreviewSummaries,
|
||||
type SortieSynergyComparison,
|
||||
type SortieSynergySnapshot
|
||||
} from '../data/sortieSynergy';
|
||||
import {
|
||||
caoBreakRecruitBonds,
|
||||
caoBreakRecruitUnits,
|
||||
@@ -92,6 +100,7 @@ import {
|
||||
yilingVanguardBonds,
|
||||
zhugeRecruitBonds,
|
||||
zhugeRecruitUnits,
|
||||
type BattleBond,
|
||||
type PortraitKey,
|
||||
type UnitData
|
||||
} from '../data/scenario';
|
||||
@@ -312,6 +321,16 @@ type SortiePlanSummary = {
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
type SortieUnitSynergyPreview = {
|
||||
mode: 'current' | 'add' | 'remove' | 'waiting' | 'blocked';
|
||||
headline: string;
|
||||
detail: string;
|
||||
compactLabel: string;
|
||||
current: SortieSynergySnapshot;
|
||||
projected?: SortieSynergySnapshot;
|
||||
comparison?: SortieSynergyComparison;
|
||||
};
|
||||
|
||||
type CampaignTimelineChapter = {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -12631,7 +12650,13 @@ export class CampScene extends Phaser.Scene {
|
||||
this.renderSortieBattleUiIcon(this.formationRoleIconKey(definition.role), slotX + 18, slotY + 22, 26, depth + 2, filled ? 1 : 0.62);
|
||||
this.trackSortie(this.add.text(slotX + 38, slotY + 10, definition.label, this.textStyle(13, filled ? '#a8ffd0' : '#ffdf7b', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(slotX + 12, slotY + 40, unitNames.join(', ') || '담당 없음', this.textStyle(13, filled ? '#f2e3bf' : '#ff9d7d', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(slotX + 12, slotY + 62, this.compactText(definition.description, 12), this.textStyle(9, '#9fb0bf'))).setDepth(depth + 2);
|
||||
this.trackSortie(
|
||||
this.add.text(slotX + 12, slotY + 59, firstPursuitRolePreviewSummaries[definition.role] ?? definition.description, {
|
||||
...this.textStyle(8, '#9fb0bf'),
|
||||
wordWrap: { width: slotWidth - 24, useAdvancedWrap: true },
|
||||
lineSpacing: 0
|
||||
})
|
||||
).setDepth(depth + 2);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12773,29 +12798,44 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private renderFirstSortieSynergyPanel(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const plan = this.sortiePlanSummary();
|
||||
const selectedIds = new Set(this.selectedSortieUnitIds);
|
||||
const activeBonds = this.currentBonds()
|
||||
.filter((bond) => bond.unitIds.every((unitId) => selectedIds.has(unitId)))
|
||||
.sort((a, b) => b.level - a.level || b.exp - a.exp)
|
||||
.slice(0, 3);
|
||||
const synergy = this.sortieSynergySnapshot();
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x151f2a, 0.94));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, plan.warningCount > 0 ? palette.gold : palette.green, 0.54);
|
||||
this.trackSortie(this.add.text(x + 18, y + 14, `공명 연계 ${plan.activeBondCount}`, this.textStyle(18, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + width - 18, y + 17, `지형 ${plan.terrainGrade}`, this.textStyle(12, '#d8b15f', true))).setOrigin(1, 0).setDepth(depth + 1);
|
||||
const activeBondLine = activeBonds.length > 0
|
||||
? activeBonds.map((bond) => `${bond.title} Lv${bond.level}`).join(' · ')
|
||||
: '활성 공명 없음';
|
||||
this.trackSortie(this.add.text(x + 18, y + 44, this.compactText(activeBondLine, 34), this.textStyle(13, activeBonds.length > 0 ? '#a8ffd0' : '#ffdf7b', true))).setDepth(depth + 1);
|
||||
const feedback = this.sortiePlanFeedback || plan.warningLine || '전열·돌파·후원이 갖춰졌습니다.';
|
||||
bg.setStrokeStyle(1, synergy.firstPursuitTrinityConfigured ? palette.green : palette.gold, 0.62);
|
||||
this.trackSortie(
|
||||
this.add.text(x + 18, y + 70, feedback, {
|
||||
...this.textStyle(12, plan.warningCount > 0 && !this.sortiePlanFeedback ? '#ffdf7b' : '#d4dce6'),
|
||||
wordWrap: { width: width - 36, useAdvancedWrap: true },
|
||||
lineSpacing: 2
|
||||
})
|
||||
this.add.text(
|
||||
x + 18,
|
||||
y + 12,
|
||||
`조합 시너지 · 공명 ${synergy.activeBondCount} · 역할 ${synergy.coveredRoleCount}/3`,
|
||||
this.textStyle(17, '#f2e3bf', true)
|
||||
)
|
||||
).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + width - 18, y + 15, `지형 ${synergy.terrainGrade}`, this.textStyle(11, '#d8b15f', true))).setOrigin(1, 0).setDepth(depth + 1);
|
||||
const activeBondLine = synergy.activeBonds.length > 0
|
||||
? `${synergy.activeBonds.slice(0, 2).map((bond) => `${bond.title} Lv${bond.level}`).join(' · ')}${synergy.activeBonds.length > 2 ? ` · 외 ${synergy.activeBonds.length - 2}` : ''}`
|
||||
: '활성 공명 없음';
|
||||
this.trackSortie(this.add.text(x + 18, y + 40, this.compactText(activeBondLine, 38), this.textStyle(12, synergy.activeBonds.length > 0 ? '#a8ffd0' : '#ffdf7b', true))).setDepth(depth + 1);
|
||||
const strongest = synergy.strongestBond;
|
||||
const strongestLine = strongest
|
||||
? `최강 공명 · 조건 충족 시 피해 +${strongest.damageBonus}% / 연계 ${strongest.chainRate}%`
|
||||
: '공명 파트너를 함께 편성하면 전투 연계가 열립니다.';
|
||||
this.trackSortie(
|
||||
this.add.text(x + 18, y + 64, this.compactText(strongestLine, 44), this.textStyle(11, strongest ? '#d4dce6' : '#9fb0bf'))
|
||||
).setDepth(depth + 1);
|
||||
const missingRoleLabels: Record<(typeof coreSortieSynergyRoles)[number], string> = {
|
||||
front: '전열',
|
||||
flank: '돌파',
|
||||
support: '후원'
|
||||
};
|
||||
const formationLine = synergy.firstPursuitTrinityConfigured
|
||||
? '삼재진 활성 · 공명 지원 거리 2칸'
|
||||
: synergy.missingRoles.length > 0
|
||||
? `삼재진 대기 · ${synergy.missingRoles.map((role) => missingRoleLabels[role]).join('·')} 역할 필요`
|
||||
: '삼재진 대기 · 세 형제의 역할을 나누십시오.';
|
||||
const bottomLine = this.sortiePlanFeedback ? `${formationLine} · ${this.sortiePlanFeedback}` : formationLine;
|
||||
this.trackSortie(
|
||||
this.add.text(x + 18, y + 88, this.compactText(bottomLine, 47), this.textStyle(11, synergy.firstPursuitTrinityConfigured ? '#a8ffd0' : '#ffdf7b', true))
|
||||
).setDepth(depth + 1);
|
||||
}
|
||||
|
||||
@@ -12910,9 +12950,9 @@ export class CampScene extends Phaser.Scene {
|
||||
const selectedIds = new Set(selectedUnits.map((unit) => unit.id));
|
||||
const reserveUnits = this.sortieAllies().filter((unit) => !selectedIds.has(unit.id));
|
||||
const selectedNames = selectedUnits.map((unit) => unit.name).join(', ');
|
||||
const recommendedClassLine = this.sortieRecommendedClassLine();
|
||||
const maxUnits = this.sortieMaxUnits();
|
||||
const hasBattle = Boolean(this.nextSortieScenario());
|
||||
const synergy = this.sortieSynergySnapshot();
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.78));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
@@ -12932,7 +12972,10 @@ export class CampScene extends Phaser.Scene {
|
||||
this.add.text(
|
||||
x + 44,
|
||||
y + 35,
|
||||
this.compactText(`${hasBattle ? '추천' : '정비'} ${recommendedClassLine} · 대기 ${reserveUnits.length}명`, 35),
|
||||
this.compactText(
|
||||
`조합 공명 ${synergy.activeBondCount} · 역할 ${synergy.coveredRoleCount}/3${hasBattle ? ` · 지형 ${synergy.terrainGrade}` : ''} · 대기 ${reserveUnits.length}명`,
|
||||
35
|
||||
),
|
||||
this.textStyle(12, reserveUnits.length > 0 ? '#c8d2dd' : '#9fb0bf', true)
|
||||
)
|
||||
).setDepth(depth + 1);
|
||||
@@ -13052,11 +13095,10 @@ export class CampScene extends Phaser.Scene {
|
||||
visibleAllies.forEach((unit, index) => {
|
||||
const rowY = y + listTopOffset + index * rowGap;
|
||||
const selected = this.isSortieSelected(unit.id);
|
||||
const required = this.isRequiredSortieUnit(unit.id);
|
||||
const recommendation = this.sortieRecommendation(unit.id);
|
||||
const summary = this.sortieUnitTacticalSummary(unit);
|
||||
const availability = this.sortieUnitAvailability(unit);
|
||||
const status = this.unitRosterStatus(unit);
|
||||
const synergyPreview = this.sortieUnitSynergyPreview(unit);
|
||||
const recommendedReserve = Boolean(recommendation && !selected);
|
||||
const disabled = !availability.available;
|
||||
const rowColor = disabled ? 0x12161b : selected ? 0x172a22 : recommendedReserve ? 0x241f17 : 0x151b24;
|
||||
@@ -13096,7 +13138,10 @@ export class CampScene extends Phaser.Scene {
|
||||
const statusText = this.trackSortie(this.add.text(x + width - 28, rowY - 1, status.label, this.textStyle(denseRoster ? 8 : 10, statusColor, true)));
|
||||
statusText.setOrigin(1, 0);
|
||||
statusText.setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(x + 58, rowY + (denseRoster ? 10 : 17), this.compactText(`${unit.className} · HP ${unit.hp}/${unit.maxHp}`, denseRoster ? 32 : 23), this.textStyle(denseRoster ? 8 : 10, disabled ? '#68727e' : '#d4dce6', true))).setDepth(depth + 2);
|
||||
const classLine = denseRoster
|
||||
? `${unit.className} · ${synergyPreview.compactLabel}`
|
||||
: `${unit.className} · HP ${unit.hp}/${unit.maxHp}`;
|
||||
this.trackSortie(this.add.text(x + 58, rowY + (denseRoster ? 10 : 17), this.compactText(classLine, denseRoster ? 32 : 23), this.textStyle(denseRoster ? 8 : 10, disabled ? '#68727e' : '#d4dce6', true))).setDepth(depth + 2);
|
||||
if (denseRoster) {
|
||||
return;
|
||||
}
|
||||
@@ -13104,8 +13149,17 @@ export class CampScene extends Phaser.Scene {
|
||||
this.drawSortieBar(x + 218, rowY + 22, 58, 6, unit.hp / unit.maxHp, disabled ? 0x53606c : selected ? palette.green : palette.gold, depth + 2);
|
||||
this.renderSortieEquipmentIcons(unit, x + 292, rowY + 24, !disabled && selected, depth + 2, 16, 12, 20);
|
||||
}
|
||||
const secondLine = disabled ? availability.reason : recommendation && !selected ? this.sortieRecommendationHint(recommendation, unit, 22) : summary.bondLine;
|
||||
this.trackSortie(this.add.text(x + 58, rowY + 32, this.compactText(secondLine, 36), this.textStyle(9, disabled ? '#68727e' : selected ? '#a8ffd0' : recommendedReserve ? '#ffdf7b' : '#87919c', selected || recommendedReserve))).setDepth(depth + 2);
|
||||
const secondLine = disabled ? availability.reason : synergyPreview.headline;
|
||||
const synergyColor = synergyPreview.mode === 'remove'
|
||||
? '#ff9d7d'
|
||||
: synergyPreview.mode === 'add'
|
||||
? '#a8ffd0'
|
||||
: synergyPreview.mode === 'waiting'
|
||||
? '#ffdf7b'
|
||||
: selected
|
||||
? '#a8ffd0'
|
||||
: '#87919c';
|
||||
this.trackSortie(this.add.text(x + 58, rowY + 32, this.compactText(secondLine, 36), this.textStyle(9, disabled ? '#68727e' : synergyColor, selected || recommendedReserve))).setDepth(depth + 2);
|
||||
});
|
||||
|
||||
if (maxScroll > 0) {
|
||||
@@ -13344,24 +13398,19 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
const status = this.unitRosterStatus(unit);
|
||||
const availability = this.sortieUnitAvailability(unit);
|
||||
const recommendation = this.sortieRecommendation(unit.id);
|
||||
const hasBattle = Boolean(this.nextSortieScenario());
|
||||
const roleLabel = this.sortieFormationRoleLabel(this.sortieFormationRole(unit), hasBattle);
|
||||
const terrainScore = this.sortieTerrainScore(unit, this.nextSortieScenario());
|
||||
const terrainGrade = this.sortieTerrainGrade(terrainScore);
|
||||
const statusColor = status.tone === 'disabled' ? '#77818c' : status.tone === 'selected' ? '#a8ffd0' : status.tone === 'recommended' ? '#ffdf7b' : '#9fb0bf';
|
||||
const selected = this.isSortieSelected(unit.id);
|
||||
const reason = !availability.available
|
||||
? availability.reason
|
||||
: recommendation
|
||||
? this.sortieRecommendationHint(recommendation, unit, 26)
|
||||
: selected
|
||||
? hasBattle
|
||||
? `${roleLabel} 배치 중입니다. 역할 버튼으로 시작 위치 성향을 바꿀 수 있습니다.`
|
||||
: `${roleLabel} 성향으로 동행 중입니다. 역할 버튼으로 군영 기록 성향을 바꿀 수 있습니다.`
|
||||
: hasBattle
|
||||
? '명단이나 버튼을 눌러 출전 슬롯에 배치할 수 있습니다.'
|
||||
: '명단이나 버튼을 눌러 다음 장면 동행 명단에 넣을 수 있습니다.';
|
||||
const synergyPreview = this.sortieUnitSynergyPreview(unit);
|
||||
const synergyColor = synergyPreview.mode === 'remove'
|
||||
? '#ff9d7d'
|
||||
: synergyPreview.mode === 'add' || synergyPreview.mode === 'current'
|
||||
? '#a8ffd0'
|
||||
: synergyPreview.mode === 'waiting'
|
||||
? '#ffdf7b'
|
||||
: '#87919c';
|
||||
|
||||
this.trackSortie(this.add.text(x + 18, y + 14, '선택 무장 상세', this.textStyle(17, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + width - 16, y + 16, status.label, this.textStyle(11, statusColor, true))).setOrigin(1, 0).setDepth(depth + 1);
|
||||
@@ -13379,7 +13428,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.renderSortieStatChip('move', '이', unit.move, x + 252, statY, chipWidth, depth + 1);
|
||||
|
||||
this.trackSortie(this.add.text(x + 18, y + 137, `지형 ${terrainGrade} ${terrainScore} · ${this.sortieTerrainLine(unit)}`, this.textStyle(11, '#d8b15f', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 18, y + 157, this.compactText(reason, 38), this.textStyle(11, recommendation ? '#ffdf7b' : selected ? '#a8ffd0' : availability.available ? '#c8d2dd' : '#87919c', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 18, y + 157, this.compactText(synergyPreview.headline, 38), this.textStyle(11, synergyColor, true))).setDepth(depth + 1);
|
||||
|
||||
equipmentSlots.forEach((slot, index) => {
|
||||
const rowY = y + 174 + index * 20;
|
||||
@@ -13408,12 +13457,14 @@ export class CampScene extends Phaser.Scene {
|
||||
? '출전 등록'
|
||||
: '동행 등록';
|
||||
this.renderSortiePanelButton(actionLabel, x + 18, y + height - 28, 108, 22, availability.available, selected, () => this.toggleSortieUnit(unit.id), depth + 1);
|
||||
const footerText = selected
|
||||
? hasBattle
|
||||
? `${roleLabel} · 역할은 중앙 버튼으로 변경`
|
||||
: `${roleLabel} · 의정 역할은 중앙 버튼으로 변경`
|
||||
: this.reserveTrainingPlanLine(unit);
|
||||
this.trackSortie(this.add.text(x + 140, y + height - 24, footerText, this.textStyle(10, selected ? '#a8ffd0' : '#9fb0bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
x + 140,
|
||||
y + height - 24,
|
||||
this.compactText(synergyPreview.detail, 30),
|
||||
this.textStyle(10, synergyColor, true)
|
||||
)
|
||||
).setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private renderSortieBattleUiIcon(iconKey: BattleUiIconKey, x: number, y: number, size: number, depth: number, alpha = 1) {
|
||||
@@ -13630,6 +13681,117 @@ export class CampScene extends Phaser.Scene {
|
||||
return '공명 관계 없음';
|
||||
}
|
||||
|
||||
private sortieSynergyBonds(scenario = this.nextSortieScenario()): BattleBond[] {
|
||||
const campaignProgress = new Map(this.currentBonds().map((bond) => [bond.id, bond]));
|
||||
const sourceBonds = scenario?.bonds ?? this.currentBonds();
|
||||
return sourceBonds.map((bond) => {
|
||||
const progress = campaignProgress.get(bond.id);
|
||||
return progress
|
||||
? { ...bond, level: progress.level, exp: progress.exp }
|
||||
: { ...bond };
|
||||
});
|
||||
}
|
||||
|
||||
private sortieSynergySnapshot(
|
||||
selectedUnitIds: readonly string[] = this.selectedSortieUnitIds,
|
||||
scenario = this.nextSortieScenario()
|
||||
) {
|
||||
return evaluateSortieSynergy({
|
||||
members: this.sortieRosterUnits().map((unit) => ({
|
||||
id: unit.id,
|
||||
name: unit.name,
|
||||
role: this.sortieFormationRole(unit, scenario),
|
||||
terrainScore: this.sortieTerrainScore(unit, scenario)
|
||||
})),
|
||||
bonds: this.sortieSynergyBonds(scenario),
|
||||
selectedUnitIds,
|
||||
battleId: scenario?.id
|
||||
});
|
||||
}
|
||||
|
||||
private sortieUnitSynergyPreview(unit: UnitData): SortieUnitSynergyPreview {
|
||||
const scenario = this.nextSortieScenario();
|
||||
const current = this.sortieSynergySnapshot(this.selectedSortieUnitIds, scenario);
|
||||
const availability = this.sortieUnitAvailability(unit, scenario);
|
||||
const selected = this.isSortieSelected(unit.id);
|
||||
const role = this.sortieFormationRole(unit, scenario);
|
||||
const roleLabel = this.sortieFormationRoleLabel(role, Boolean(scenario));
|
||||
|
||||
if (!availability.available) {
|
||||
return {
|
||||
mode: 'blocked',
|
||||
headline: availability.reason,
|
||||
detail: `${unit.name}은 현재 편성할 수 없습니다.`,
|
||||
compactLabel: '편성 불가',
|
||||
current
|
||||
};
|
||||
}
|
||||
|
||||
if (selected && this.isRequiredSortieUnit(unit.id)) {
|
||||
const relatedBonds = current.activeBonds.filter((bond) => bond.unitIds.includes(unit.id));
|
||||
const strongest = relatedBonds[0];
|
||||
return {
|
||||
mode: 'current',
|
||||
headline: `필수 편성 · 공명 ${relatedBonds.length}개 연결 · ${roleLabel} 유지`,
|
||||
detail: strongest
|
||||
? `${strongest.title} Lv${strongest.level} · 조건 충족 시 피해 +${strongest.damageBonus}% / 연계 ${strongest.chainRate}%`
|
||||
: `현재 활성 공명 없음 · 지형 ${current.terrainGrade} ${current.terrainScore}`,
|
||||
compactLabel: `필수 · 공명 ${relatedBonds.length}`,
|
||||
current
|
||||
};
|
||||
}
|
||||
|
||||
if (!selected && this.selectedSortieUnitIds.length >= this.sortieMaxUnits(scenario)) {
|
||||
const selectedIds = new Set(this.selectedSortieUnitIds);
|
||||
const potentialBonds = this.sortieSynergyBonds(scenario)
|
||||
.filter((bond) => bond.unitIds.includes(unit.id) && bond.unitIds.some((unitId) => unitId !== unit.id && selectedIds.has(unitId)))
|
||||
.sort((left, right) => right.level - left.level || left.title.localeCompare(right.title));
|
||||
const strongest = potentialBonds[0];
|
||||
const strongestPartnerId = strongest?.unitIds.find((unitId) => unitId !== unit.id);
|
||||
const terrainScore = this.sortieTerrainScore(unit, scenario);
|
||||
return {
|
||||
mode: 'waiting',
|
||||
headline: strongest
|
||||
? `교대 후보 · ${potentialBonds.length}개 공명 연결 가능`
|
||||
: `교대 후보 · 연결 가능한 공명 없음`,
|
||||
detail: strongest
|
||||
? `${this.unitName(strongestPartnerId ?? '')}와 ${strongest.title} Lv${strongest.level} · ${roleLabel} · 지형 ${this.sortieTerrainGrade(terrainScore)}`
|
||||
: `${roleLabel} 후보 · 지형 ${this.sortieTerrainGrade(terrainScore)} ${terrainScore} · 먼저 슬롯을 비우십시오.`,
|
||||
compactLabel: `교대 · 공명 ${potentialBonds.length}`,
|
||||
current
|
||||
};
|
||||
}
|
||||
|
||||
const projectedIds = selected
|
||||
? this.selectedSortieUnitIds.filter((unitId) => unitId !== unit.id)
|
||||
: [...this.selectedSortieUnitIds, unit.id];
|
||||
const projected = this.sortieSynergySnapshot(projectedIds, scenario);
|
||||
const comparison = compareSortieSynergy(current, projected);
|
||||
const prefix = selected ? '해제 시' : '합류 시';
|
||||
const changedBonds = selected ? comparison.lostBonds : comparison.gainedBonds;
|
||||
const bondMark = selected ? '−' : '+';
|
||||
const bondDetail = changedBonds.length > 0
|
||||
? `${bondMark} ${changedBonds.slice(0, 2).map((bond) => `${bond.title} Lv${bond.level}`).join(' · ')}`
|
||||
: '공명 변화 없음';
|
||||
const strongest = projected.strongestBond;
|
||||
const terrainLine = `지형 ${current.terrainGrade} ${current.terrainScore}→${projected.terrainGrade} ${projected.terrainScore}`;
|
||||
const strongestLine = strongest
|
||||
? `최강 피해 +${strongest.damageBonus}% / 연계 ${strongest.chainRate}%`
|
||||
: '활성 공명 없음';
|
||||
const signedBondDelta = comparison.activeBondDelta > 0 ? `+${comparison.activeBondDelta}` : `${comparison.activeBondDelta}`;
|
||||
const signedRoleDelta = comparison.roleCoverageDelta > 0 ? `+${comparison.roleCoverageDelta}` : `${comparison.roleCoverageDelta}`;
|
||||
|
||||
return {
|
||||
mode: selected ? 'remove' : 'add',
|
||||
headline: `${prefix} 공명 ${current.activeBondCount}→${projected.activeBondCount} · 역할 ${current.coveredRoleCount}/3→${projected.coveredRoleCount}/3`,
|
||||
detail: `${bondDetail} · ${strongestLine} · ${terrainLine}`,
|
||||
compactLabel: `공명 ${signedBondDelta} · 역할 ${signedRoleDelta}`,
|
||||
current,
|
||||
projected,
|
||||
comparison
|
||||
};
|
||||
}
|
||||
|
||||
private sortieTerrainLine(unit: UnitData) {
|
||||
const scenario = this.nextSortieScenario();
|
||||
if (!scenario) {
|
||||
@@ -13950,7 +14112,7 @@ export class CampScene extends Phaser.Scene {
|
||||
? Math.round(terrainScores.reduce((sum, score) => sum + score, 0) / terrainScores.length)
|
||||
: 0;
|
||||
const terrainGrade = this.sortieTerrainGrade(terrainScore);
|
||||
const activeBondCount = this.currentBonds().filter((bond) => bond.unitIds.every((unitId) => selectedIds.has(unitId))).length;
|
||||
const activeBondCount = this.sortieSynergyBonds(scenario).filter((bond) => bond.unitIds.every((unitId) => selectedIds.has(unitId))).length;
|
||||
const recommendedSelectedCount = recommended.filter((entry) => selectedIds.has(entry.unitId)).length;
|
||||
const recommendedClassSelectedCount = recommendedClasses.length - missingClassRecommendations.length;
|
||||
const recommendedClassCoverageLine = recommendedClasses.length > 0
|
||||
@@ -16067,7 +16229,7 @@ export class CampScene extends Phaser.Scene {
|
||||
candidateSet.add(unitId);
|
||||
}
|
||||
};
|
||||
if (useDefaultSelection || candidateSet.size < maxUnits) {
|
||||
if (useDefaultSelection) {
|
||||
const rule = this.nextSortieRule();
|
||||
rule.recommended.forEach((entry) => addDefaultCandidate(entry.unitId));
|
||||
(rule.recommendedClasses ?? []).forEach((entry) => {
|
||||
@@ -16114,6 +16276,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.showCampNotice(`${unit.name}: ${availability.reason}`);
|
||||
return;
|
||||
}
|
||||
const synergyPreview = this.sortieUnitSynergyPreview(unit);
|
||||
if (this.isRequiredSortieUnit(unitId)) {
|
||||
this.showCampNotice(`${unit.name}는 반드시 ${hasBattle ? '출전' : '동행'}해야 합니다.`);
|
||||
return;
|
||||
@@ -16129,7 +16292,9 @@ export class CampScene extends Phaser.Scene {
|
||||
delete nextItemAssignments[unitId];
|
||||
this.sortieItemAssignments = nextItemAssignments;
|
||||
} else if (selected.size >= this.sortieMaxUnits()) {
|
||||
this.showCampNotice(`${hasBattle ? '이번 전투' : '이번 장면'}는 최대 ${this.sortieMaxUnits()}명까지 ${hasBattle ? '출전' : '동행'}할 수 있습니다.`);
|
||||
this.sortiePlanFeedback = `${unit.name} 교대 후보 · 먼저 ${hasBattle ? '출전' : '동행'} 슬롯을 비우십시오.`;
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
return;
|
||||
} else {
|
||||
selected.add(unitId);
|
||||
@@ -16137,9 +16302,8 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(Array.from(selected));
|
||||
const selectionLabel = hasBattle ? '출전' : '동행';
|
||||
this.sortiePlanFeedback = selected.has(unitId)
|
||||
? `${unit.name} ${selectionLabel} 추가 · 직접 편성 중`
|
||||
: `${unit.name} ${selectionLabel} 해제 · 직접 편성 중`;
|
||||
const synergyResultLine = synergyPreview.headline.replace(/^(합류|해제) 시 /, '');
|
||||
this.sortiePlanFeedback = `${unit.name} ${selected.has(unitId) ? `${selectionLabel} 추가` : `${selectionLabel} 해제`} · ${synergyResultLine}`;
|
||||
this.persistSortieSelection();
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
@@ -16400,6 +16564,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const reserveTrainingAssignments = this.campaign?.reserveTrainingAssignments ?? {};
|
||||
const reserveTrainingFocus = this.reserveTrainingFocusDefinition();
|
||||
const reserveTrainingUnits = this.reserveTrainingPreviewUnits();
|
||||
const focusedSortieUnit = this.sortieFocusedUnit();
|
||||
return {
|
||||
scene: this.scene.key,
|
||||
activeTab: this.activeTab,
|
||||
@@ -16434,9 +16599,12 @@ export class CampScene extends Phaser.Scene {
|
||||
sortieRoster: this.sortieAllies().map((unit) => {
|
||||
const summary = this.sortieUnitTacticalSummary(unit);
|
||||
const reserveTrainingFocus = this.reserveTrainingFocusDefinitionForUnit(unit.id);
|
||||
const availability = this.sortieUnitAvailability(unit);
|
||||
return {
|
||||
id: unit.id,
|
||||
name: unit.name,
|
||||
available: availability.available,
|
||||
availabilityReason: availability.reason,
|
||||
selected: this.isSortieSelected(unit.id),
|
||||
recruited: !foundingSortieUnitIds.has(unit.id),
|
||||
required: this.isRequiredSortieUnit(unit.id),
|
||||
@@ -16466,6 +16634,10 @@ export class CampScene extends Phaser.Scene {
|
||||
bonusText: this.itemBonusText(entry.item)
|
||||
})),
|
||||
sortiePlan: this.sortiePlanSummary(),
|
||||
sortieSynergyPreview: this.sortieSynergySnapshot(),
|
||||
sortieFocusedSynergyPreview: focusedSortieUnit
|
||||
? this.sortieUnitSynergyPreview(focusedSortieUnit)
|
||||
: null,
|
||||
rosterCollection: this.rosterCollectionSummary(),
|
||||
reserveTrainingAwards: this.latestReserveTrainingAwards(),
|
||||
campaignProgress: this.campaignTimelineProgress(),
|
||||
|
||||
Reference in New Issue
Block a user