feat: preview sortie composition synergy

This commit is contained in:
2026-07-10 13:08:56 +09:00
parent 3d2c09c24e
commit 3caf03fe2f
8 changed files with 609 additions and 70 deletions

View File

@@ -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(),