feat: stage sortie preparation across campaign
This commit is contained in:
@@ -18881,7 +18881,12 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private updateObjectiveTracker() {
|
||||
if (!this.objectiveTrackerText || !this.objectiveTrackerSubText) {
|
||||
if (
|
||||
!this.objectiveTrackerText?.active ||
|
||||
!this.objectiveTrackerSubText?.active ||
|
||||
!this.objectiveTrackerText.frame?.texture ||
|
||||
!this.objectiveTrackerSubText.frame?.texture
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -12221,7 +12221,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.sortieObjects.length === 0 ||
|
||||
this.saveSlotObjects.length > 0 ||
|
||||
this.saveSlotConfirmObjects.length > 0 ||
|
||||
!this.isFirstSortiePrepSlice()
|
||||
!this.usesStagedSortiePrep()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -12348,7 +12348,8 @@ export class CampScene extends Phaser.Scene {
|
||||
const flow = this.currentSortieFlow();
|
||||
const isBattleSortie = Boolean(flow.nextBattleId);
|
||||
const isFirstSortieSlice = this.isFirstSortiePrepSlice(flow);
|
||||
if (isFirstSortieSlice && !wasVisible) {
|
||||
const usesStagedPrep = this.usesStagedSortiePrep(flow);
|
||||
if (usesStagedPrep && !wasVisible) {
|
||||
this.sortiePrepStep = 'briefing';
|
||||
}
|
||||
const checklist = this.sortieChecklist();
|
||||
@@ -12358,8 +12359,13 @@ export class CampScene extends Phaser.Scene {
|
||||
formation: '큰 초상 카드를 고르고 전열·돌파·후원 역할을 맞바꾸십시오.',
|
||||
loadout: '장비·보급과 필수 조건을 마지막으로 확인하십시오.'
|
||||
};
|
||||
const prepSubtitle = isFirstSortieSlice
|
||||
? firstSliceSubtitle[this.sortiePrepStep]
|
||||
const campaignSortieSubtitle: Record<SortiePrepStep, string> = {
|
||||
briefing: `${this.nextSortieBriefing().title}의 목표·지형·적 위협을 먼저 읽으십시오.`,
|
||||
formation: '초상 카드로 무장을 고르고 전열·돌파·후원 역할과 공명을 조율하십시오.',
|
||||
loadout: '선택한 무장의 장비·보급과 필수 출전 조건을 마지막으로 점검하십시오.'
|
||||
};
|
||||
const prepSubtitle = usesStagedPrep
|
||||
? (isFirstSortieSlice ? firstSliceSubtitle : campaignSortieSubtitle)[this.sortiePrepStep]
|
||||
: isBattleSortie
|
||||
? this.sortiePrepSubtitle(checklist)
|
||||
: this.isFinalEpilogueFlow(flow)
|
||||
@@ -12371,7 +12377,7 @@ export class CampScene extends Phaser.Scene {
|
||||
shade.setDepth(depth);
|
||||
shade.setInteractive();
|
||||
|
||||
const panel = this.trackSortie(this.add.rectangle(x, y, width, height, isFirstSortieSlice ? 0x0b1118 : 0x101820, isFirstSortieSlice ? 0.94 : 0.98));
|
||||
const panel = this.trackSortie(this.add.rectangle(x, y, width, height, usesStagedPrep ? 0x0b1118 : 0x101820, usesStagedPrep ? 0.96 : 0.98));
|
||||
panel.setOrigin(0);
|
||||
panel.setDepth(depth + 1);
|
||||
panel.setStrokeStyle(3, palette.gold, 0.92);
|
||||
@@ -12379,20 +12385,20 @@ export class CampScene extends Phaser.Scene {
|
||||
this.renderFirstSortieArtwork(x + 3, y + 3, width - 6, height - 6, depth + 1, 0.13);
|
||||
}
|
||||
|
||||
const title = this.trackSortie(this.add.text(x + 34, y + (isFirstSortieSlice ? 22 : 28), prepTitle, this.textStyle(isFirstSortieSlice ? 28 : 30, '#f2e3bf', true)));
|
||||
const title = this.trackSortie(this.add.text(x + 34, y + (usesStagedPrep ? 22 : 28), prepTitle, this.textStyle(usesStagedPrep ? 28 : 30, '#f2e3bf', true)));
|
||||
title.setDepth(depth + 2);
|
||||
const subtitle = this.trackSortie(
|
||||
this.add.text(
|
||||
x + 34,
|
||||
y + (isFirstSortieSlice ? 60 : 70),
|
||||
this.compactText(prepSubtitle, isFirstSortieSlice ? 54 : 72),
|
||||
y + (usesStagedPrep ? 60 : 70),
|
||||
this.compactText(prepSubtitle, usesStagedPrep ? 62 : 72),
|
||||
this.textStyle(15, '#d4dce6')
|
||||
)
|
||||
);
|
||||
subtitle.setDepth(depth + 2);
|
||||
this.renderSortieHeaderSummary(x + (isFirstSortieSlice ? 748 : 690), y + 26, isFirstSortieSlice ? 406 : 464, 58, depth + 2);
|
||||
this.renderSortieHeaderSummary(x + (usesStagedPrep ? 748 : 690), y + 26, usesStagedPrep ? 406 : 464, 58, depth + 2);
|
||||
|
||||
if (isFirstSortieSlice) {
|
||||
if (usesStagedPrep) {
|
||||
this.renderFirstSortieStepNavigation(x + 34, y + 92, width - 68, 42, depth + 2, checklist);
|
||||
const contentX = x + 34;
|
||||
const contentY = y + 150;
|
||||
@@ -12401,7 +12407,11 @@ export class CampScene extends Phaser.Scene {
|
||||
if (this.sortiePrepStep === 'briefing') {
|
||||
this.renderFirstSortieBriefingStep(contentX, contentY, contentWidth, contentHeight, depth + 2);
|
||||
} else if (this.sortiePrepStep === 'formation') {
|
||||
this.renderFirstSortieFormationStep(contentX, contentY, contentWidth, contentHeight, depth + 2);
|
||||
if (isFirstSortieSlice) {
|
||||
this.renderFirstSortieFormationStep(contentX, contentY, contentWidth, contentHeight, depth + 2);
|
||||
} else {
|
||||
this.renderCampaignSortieFormationStep(contentX, contentY, contentWidth, contentHeight, depth + 2);
|
||||
}
|
||||
} else {
|
||||
this.renderFirstSortieLoadoutStep(contentX, contentY, contentWidth, contentHeight, depth + 2, checklist);
|
||||
}
|
||||
@@ -12460,11 +12470,16 @@ export class CampScene extends Phaser.Scene {
|
||||
return flow.afterBattleId === campBattleIds.first && flow.nextBattleId === campBattleIds.second;
|
||||
}
|
||||
|
||||
private usesStagedSortiePrep(flow = this.currentSortieFlow()) {
|
||||
return Boolean(flow.nextBattleId);
|
||||
}
|
||||
|
||||
private setSortiePrepStep(step: SortiePrepStep) {
|
||||
if (this.sortiePrepStep === step) {
|
||||
return;
|
||||
}
|
||||
this.sortiePrepStep = step;
|
||||
this.sortieRosterScroll = 0;
|
||||
soundDirector.playSelect();
|
||||
this.showSortiePrep();
|
||||
}
|
||||
@@ -12531,8 +12546,9 @@ export class CampScene extends Phaser.Scene {
|
||||
const gap = 18;
|
||||
const briefingWidth = 690;
|
||||
const tacticsWidth = width - briefingWidth - gap;
|
||||
const useFirstSortieArtwork = this.isFirstSortiePrepSlice();
|
||||
this.renderSortieBriefing(x, y, briefingWidth, 220, depth);
|
||||
this.renderSortieDeploymentMap(x, y + 238, briefingWidth, height - 238, depth, true);
|
||||
this.renderSortieDeploymentMap(x, y + 238, briefingWidth, height - 238, depth, useFirstSortieArtwork);
|
||||
const mapLabelBg = this.trackSortie(this.add.rectangle(x + 12, y + 250, 162, 28, 0x070b10, 0.88));
|
||||
mapLabelBg.setOrigin(0);
|
||||
mapLabelBg.setDepth(depth + 3);
|
||||
@@ -12543,12 +12559,20 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private renderFirstSortieTacticalNotes(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const scenario = this.nextSortieScenario();
|
||||
const firstSortie = this.isFirstSortiePrepSlice();
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.94));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, palette.gold, 0.58);
|
||||
this.trackSortie(this.add.text(x + 20, y + 16, '전술 핵심', this.textStyle(20, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 20, y + 44, '세 형제의 역할이 갈리는 첫 추격전입니다.', this.textStyle(12, '#9fb0bf'))).setDepth(depth + 1);
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
x + 20,
|
||||
y + 44,
|
||||
firstSortie ? '세 형제의 역할이 갈리는 첫 추격전입니다.' : '목표와 지형에 맞춰 출진 교리를 세우십시오.',
|
||||
this.textStyle(12, '#9fb0bf')
|
||||
)
|
||||
).setDepth(depth + 1);
|
||||
if (!scenario) {
|
||||
return;
|
||||
}
|
||||
@@ -12589,7 +12613,9 @@ export class CampScene extends Phaser.Scene {
|
||||
command.setStrokeStyle(1, palette.gold, 0.6);
|
||||
this.trackSortie(this.add.text(x + 34, commandY + 12, '지휘관 판단', this.textStyle(14, '#ffdf7b', true))).setDepth(depth + 2);
|
||||
this.trackSortie(
|
||||
this.add.text(x + 34, commandY + 36, scenario.tacticalGuide?.summary ?? scenario.openingObjectiveLines[1] ?? '세 형제가 서로 다른 길을 맡아 적의 전열을 흔드십시오.', {
|
||||
this.add.text(x + 34, commandY + 36, scenario.tacticalGuide?.summary ?? scenario.openingObjectiveLines[1] ?? (firstSortie
|
||||
? '세 형제가 서로 다른 길을 맡아 적의 전열을 흔드십시오.'
|
||||
: '전열·돌파·후원의 균형으로 적의 주력을 나누십시오.'), {
|
||||
...this.textStyle(13, '#d4dce6'),
|
||||
wordWrap: { width: width - 68, useAdvancedWrap: true },
|
||||
lineSpacing: 3
|
||||
@@ -12623,6 +12649,193 @@ export class CampScene extends Phaser.Scene {
|
||||
this.renderFirstSortieSynergyPanel(x + rosterWidth + gap, y + 310, planWidth, height - 310, depth);
|
||||
}
|
||||
|
||||
private renderCampaignSortieFormationStep(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const gap = 18;
|
||||
const rosterWidth = 610;
|
||||
const planWidth = width - rosterWidth - gap;
|
||||
this.renderCampaignSortiePortraitRoster(x, y, rosterWidth, height, depth);
|
||||
this.renderSortieFormationPlan(x + rosterWidth + gap, y, planWidth, 286, depth);
|
||||
this.renderCampaignSortieComparisonPanel(x + rosterWidth + gap, y + 304, planWidth, height - 304, depth);
|
||||
}
|
||||
|
||||
private renderCampaignSortiePortraitRoster(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.96));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, palette.blue, 0.58);
|
||||
|
||||
const rosterUnits = this.sortieRosterUnits();
|
||||
const displayUnits = this.sortieRosterDisplayUnits(rosterUnits);
|
||||
const visibleCount = 8;
|
||||
const maxScroll = Math.max(0, displayUnits.length - visibleCount);
|
||||
this.sortieRosterScroll = Phaser.Math.Clamp(this.sortieRosterScroll, 0, maxScroll);
|
||||
const visibleUnits = displayUnits.slice(this.sortieRosterScroll, this.sortieRosterScroll + visibleCount);
|
||||
const selectedCount = this.selectedSortieUnitIds.length;
|
||||
const maxUnits = this.sortieMaxUnits();
|
||||
const rangeEnd = this.sortieRosterScroll + visibleUnits.length;
|
||||
this.trackSortie(this.add.text(x + 18, y + 13, `무장 선택 · 출전 ${selectedCount}/${maxUnits}`, this.textStyle(19, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
const rangeText = this.trackSortie(
|
||||
this.add.text(
|
||||
x + width - 116,
|
||||
y + 18,
|
||||
`${displayUnits.length > 0 ? this.sortieRosterScroll + 1 : 0}-${rangeEnd}/${displayUnits.length}`,
|
||||
this.textStyle(11, '#9fb0bf', true)
|
||||
)
|
||||
);
|
||||
rangeText.setOrigin(1, 0);
|
||||
rangeText.setDepth(depth + 1);
|
||||
this.renderFirstSortieInlineButton('‹', x + width - 100, y + 9, 40, 30, this.sortieRosterScroll > 0, false, () => {
|
||||
soundDirector.playSelect();
|
||||
this.updateSortieRosterScroll(this.sortieRosterScroll - visibleCount, maxScroll);
|
||||
}, depth + 1);
|
||||
this.renderFirstSortieInlineButton('›', x + width - 52, y + 9, 40, 30, this.sortieRosterScroll < maxScroll, false, () => {
|
||||
soundDirector.playSelect();
|
||||
this.updateSortieRosterScroll(this.sortieRosterScroll + visibleCount, maxScroll);
|
||||
}, depth + 1);
|
||||
|
||||
const handleRosterWheel = (
|
||||
_pointer: Phaser.Input.Pointer,
|
||||
_deltaX: number,
|
||||
deltaY: number,
|
||||
_deltaZ: number,
|
||||
event?: { stopPropagation: () => void }
|
||||
) => {
|
||||
event?.stopPropagation();
|
||||
const direction = Math.sign(deltaY);
|
||||
if (direction === 0 || maxScroll <= 0) {
|
||||
return;
|
||||
}
|
||||
this.updateSortieRosterScroll(this.sortieRosterScroll + direction * 2, maxScroll);
|
||||
};
|
||||
if (maxScroll > 0) {
|
||||
bg.setInteractive({ useHandCursor: false });
|
||||
bg.on('wheel', handleRosterWheel);
|
||||
}
|
||||
|
||||
const columnGap = 10;
|
||||
const rowGap = 8;
|
||||
const cardWidth = Math.floor((width - 36 - columnGap) / 2);
|
||||
const cardTop = y + 50;
|
||||
const cardHeight = Math.floor((height - 64 - rowGap * 3) / 4);
|
||||
visibleUnits.forEach((unit, index) => {
|
||||
const column = index % 2;
|
||||
const rowIndex = Math.floor(index / 2);
|
||||
const cardX = x + 18 + column * (cardWidth + columnGap);
|
||||
const cardY = cardTop + rowIndex * (cardHeight + rowGap);
|
||||
const selected = this.isSortieSelected(unit.id);
|
||||
const focused = this.sortieFocusedUnitId === unit.id;
|
||||
const required = this.isRequiredSortieUnit(unit.id);
|
||||
const recommendation = this.sortieRecommendation(unit.id);
|
||||
const availability = this.sortieUnitAvailability(unit);
|
||||
const role = this.sortieFormationRole(unit);
|
||||
const preview = this.sortieUnitSynergyPreview(unit);
|
||||
const fill = !availability.available ? 0x11151a : selected ? 0x172a22 : recommendation ? 0x241f17 : 0x151b24;
|
||||
const card = this.trackSortie(this.add.rectangle(cardX, cardY, cardWidth, cardHeight, fill, !availability.available ? 0.62 : 0.96));
|
||||
card.setOrigin(0);
|
||||
card.setDepth(depth + 1);
|
||||
card.setStrokeStyle(
|
||||
focused ? 2 : 1,
|
||||
focused ? palette.gold : !availability.available ? 0x53606c : selected ? palette.green : recommendation ? palette.gold : 0x53606c,
|
||||
focused ? 0.96 : selected ? 0.64 : recommendation ? 0.48 : 0.34
|
||||
);
|
||||
card.setInteractive({ useHandCursor: true });
|
||||
card.on('wheel', handleRosterWheel);
|
||||
card.on('pointerover', () => card.setFillStyle(!availability.available ? 0x15191e : selected ? 0x20362a : 0x1d2731, 0.98));
|
||||
card.on('pointerout', () => card.setFillStyle(fill, !availability.available ? 0.62 : 0.96));
|
||||
card.on('pointerdown', () => {
|
||||
this.sortieFocusedUnitId = unit.id;
|
||||
soundDirector.playSelect();
|
||||
if (!availability.available) {
|
||||
this.showCampNotice(`${unit.name}: ${availability.reason}`);
|
||||
}
|
||||
this.showSortiePrep();
|
||||
});
|
||||
|
||||
this.renderFirstSortiePortrait(cardX + 39, cardY + cardHeight / 2, 56, unit, depth + 2, availability.available ? 1 : 0.42, false);
|
||||
const statusLabel = required ? '필수' : selected ? '출전' : recommendation ? '추천' : '대기';
|
||||
const statusColor = required || recommendation ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf';
|
||||
const status = this.trackSortie(this.add.text(cardX + cardWidth - 10, cardY + 8, statusLabel, this.textStyle(9, statusColor, true)));
|
||||
status.setOrigin(1, 0);
|
||||
status.setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(cardX + 75, cardY + 8, `${unit.name} Lv${unit.level}`, this.textStyle(14, availability.available ? '#f2e3bf' : '#77818c', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(cardX + 75, cardY + 28, `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(9, availability.available ? '#c8d2dd' : '#68727e', true))).setDepth(depth + 2);
|
||||
const previewColor = preview.mode === 'remove'
|
||||
? '#ff9d7d'
|
||||
: preview.mode === 'add' || preview.mode === 'current'
|
||||
? '#a8ffd0'
|
||||
: preview.mode === 'waiting'
|
||||
? '#ffdf7b'
|
||||
: '#87919c';
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
cardX + 75,
|
||||
cardY + 45,
|
||||
this.compactText(`${this.sortieFormationRoleLabel(role)} · ${preview.compactLabel}`, 28),
|
||||
this.textStyle(9, availability.available ? previewColor : '#68727e', selected || Boolean(recommendation))
|
||||
)
|
||||
).setDepth(depth + 2);
|
||||
|
||||
const canToggle = availability.available && !(selected && required);
|
||||
this.renderFirstSortieInlineButton(
|
||||
required ? '필수 출전' : selected ? '출전 해제' : '출전 등록',
|
||||
cardX + cardWidth - 80,
|
||||
cardY + cardHeight - 25,
|
||||
70,
|
||||
20,
|
||||
canToggle,
|
||||
selected,
|
||||
() => {
|
||||
this.sortieFocusedUnitId = unit.id;
|
||||
this.toggleSortieUnit(unit.id);
|
||||
},
|
||||
depth + 3
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private renderCampaignSortieComparisonPanel(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const focused = this.sortieFocusedUnit();
|
||||
const synergy = this.sortieSynergySnapshot();
|
||||
const plan = this.sortiePlanSummary();
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x151f2a, 0.96));
|
||||
bg.setOrigin(0);
|
||||
bg.setDepth(depth);
|
||||
bg.setStrokeStyle(1, synergy.coveredRoleCount === 3 ? palette.green : palette.gold, 0.58);
|
||||
this.trackSortie(this.add.text(x + 18, y + 12, focused ? `편성 변화 · ${focused.name}` : '편성 변화', this.textStyle(16, '#f2e3bf', true))).setDepth(depth + 1);
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
x + width - 18,
|
||||
y + 14,
|
||||
`공명 ${synergy.activeBondCount} · 역할 ${synergy.coveredRoleCount}/3 · 지형 ${synergy.terrainGrade}`,
|
||||
this.textStyle(10, '#d8b15f', true)
|
||||
)
|
||||
).setOrigin(1, 0).setDepth(depth + 1);
|
||||
|
||||
if (!focused) {
|
||||
this.trackSortie(this.add.text(x + 18, y + 44, '왼쪽 초상 카드를 선택하면 합류·해제 전후가 표시됩니다.', this.textStyle(12, '#9fb0bf'))).setDepth(depth + 1);
|
||||
return;
|
||||
}
|
||||
const preview = this.sortieUnitSynergyPreview(focused);
|
||||
const previewColor = preview.mode === 'remove'
|
||||
? '#ff9d7d'
|
||||
: preview.mode === 'add' || preview.mode === 'current'
|
||||
? '#a8ffd0'
|
||||
: preview.mode === 'waiting'
|
||||
? '#ffdf7b'
|
||||
: '#87919c';
|
||||
this.trackSortie(this.add.text(x + 18, y + 39, this.compactText(preview.headline, 54), this.textStyle(12, previewColor, true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + 18, y + 62, this.compactText(preview.detail, 66), this.textStyle(10, '#c8d2dd'))).setDepth(depth + 1);
|
||||
const feedback = this.sortiePlanFeedback || plan.warningLine;
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
x + 18,
|
||||
y + height - 27,
|
||||
this.compactText(feedback, 64),
|
||||
this.textStyle(10, this.sortiePlanFeedback ? '#a8ffd0' : plan.warnings.length > 0 ? '#ffdf7b' : '#9fb0bf', true)
|
||||
)
|
||||
).setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private renderFirstSortieRoleDiagram(x: number, y: number, width: number, height: number, depth: number) {
|
||||
const bg = this.trackSortie(this.add.rectangle(x, y, width, height, 0x0d141c, 0.94));
|
||||
bg.setOrigin(0);
|
||||
@@ -12742,12 +12955,20 @@ export class CampScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
private renderFirstSortiePortrait(x: number, y: number, size: number, unit: UnitData, depth: number, alpha = 1) {
|
||||
private renderFirstSortiePortrait(
|
||||
x: number,
|
||||
y: number,
|
||||
size: number,
|
||||
unit: UnitData,
|
||||
depth: number,
|
||||
alpha = 1,
|
||||
preferPeriodPortrait = true
|
||||
) {
|
||||
const frame = this.trackSortie(this.add.rectangle(x, y, size + 8, size + 8, 0x090e14, 0.98));
|
||||
frame.setDepth(depth);
|
||||
frame.setStrokeStyle(2, palette.gold, 0.58);
|
||||
const portraitKey = portraitByUnitId[unit.id];
|
||||
const periodPortraitTexture = portraitKey
|
||||
const periodPortraitTexture = portraitKey && preferPeriodPortrait
|
||||
? portraitAssetEntriesForKey(portraitKey).find((entry) => entry.textureKey.endsWith('-yellow-turban'))?.textureKey
|
||||
: undefined;
|
||||
const portraitTexture = periodPortraitTexture && this.textures.exists(periodPortraitTexture)
|
||||
@@ -12853,17 +13074,21 @@ export class CampScene extends Phaser.Scene {
|
||||
const checksWidth = width - rosterWidth - focusWidth - gap * 2;
|
||||
this.renderSortieUnitSummary(x, y, rosterWidth, height, depth, 'focus');
|
||||
this.renderSortieFocusPanel(x + rosterWidth + gap, y, focusWidth, 320, depth);
|
||||
this.renderSortieRewardHint(x + rosterWidth + gap, y + 338, focusWidth, height - 338, depth);
|
||||
const focused = this.sortieFocusedUnit();
|
||||
if (focused) {
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
x + rosterWidth + gap + 16,
|
||||
y + 397,
|
||||
this.compactText(this.sortieBondLine(focused), 52),
|
||||
this.textStyle(12, '#a8ffd0', true)
|
||||
)
|
||||
).setDepth(depth + 2);
|
||||
if (this.isFirstSortiePrepSlice()) {
|
||||
this.renderSortieRewardHint(x + rosterWidth + gap, y + 338, focusWidth, height - 338, depth);
|
||||
const focused = this.sortieFocusedUnit();
|
||||
if (focused) {
|
||||
this.trackSortie(
|
||||
this.add.text(
|
||||
x + rosterWidth + gap + 16,
|
||||
y + 397,
|
||||
this.compactText(this.sortieBondLine(focused), 52),
|
||||
this.textStyle(12, '#a8ffd0', true)
|
||||
)
|
||||
).setDepth(depth + 2);
|
||||
}
|
||||
} else {
|
||||
this.renderSortieReserveTraining(x + rosterWidth + gap, y + 338, focusWidth, height - 338, depth);
|
||||
}
|
||||
this.renderFirstSortieFinalChecks(x + rosterWidth + focusWidth + gap * 2, y, checksWidth, height, depth, checklist);
|
||||
}
|
||||
@@ -16306,6 +16531,7 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(Array.from(selected));
|
||||
this.sortieRosterScroll = 0;
|
||||
const selectionLabel = hasBattle ? '출전' : '동행';
|
||||
const synergyResultLine = synergyPreview.headline.replace(/^(합류|해제) 시 /, '');
|
||||
this.sortiePlanFeedback = `${unit.name} ${selected.has(unitId) ? `${selectionLabel} 추가` : `${selectionLabel} 해제`} · ${synergyResultLine}`;
|
||||
@@ -16570,12 +16796,41 @@ export class CampScene extends Phaser.Scene {
|
||||
const reserveTrainingFocus = this.reserveTrainingFocusDefinition();
|
||||
const reserveTrainingUnits = this.reserveTrainingPreviewUnits();
|
||||
const focusedSortieUnit = this.sortieFocusedUnit();
|
||||
const stagedSortiePrep = this.usesStagedSortiePrep();
|
||||
const sortieStepIndex = firstSortiePrepSteps.findIndex((step) => step.id === this.sortiePrepStep);
|
||||
const nextSortiePrepStep = stagedSortiePrep ? firstSortiePrepSteps[sortieStepIndex + 1]?.id ?? null : null;
|
||||
const portraitRosterUnits = this.sortieRosterDisplayUnits(this.sortieRosterUnits());
|
||||
const portraitRosterVisibleCount = 8;
|
||||
const portraitRosterMaxScroll = Math.max(0, portraitRosterUnits.length - portraitRosterVisibleCount);
|
||||
const portraitRosterScroll = Phaser.Math.Clamp(this.sortieRosterScroll, 0, portraitRosterMaxScroll);
|
||||
return {
|
||||
scene: this.scene.key,
|
||||
activeTab: this.activeTab,
|
||||
sortieVisible: this.sortieObjects.length > 0,
|
||||
sortiePrepStep: this.sortiePrepStep,
|
||||
stagedSortiePrep: this.isFirstSortiePrepSlice(),
|
||||
stagedSortiePrep,
|
||||
firstSortiePrep: this.isFirstSortiePrepSlice(),
|
||||
sortiePrepSteps: firstSortiePrepSteps.map((step) => ({
|
||||
id: step.id,
|
||||
active: step.id === this.sortiePrepStep,
|
||||
complete: this.firstSortieStepComplete(step.id, sortieChecklist)
|
||||
})),
|
||||
sortiePrimaryAction: stagedSortiePrep
|
||||
? {
|
||||
kind: nextSortiePrepStep ? 'next' : 'launch',
|
||||
nextStep: nextSortiePrepStep,
|
||||
label: nextSortiePrepStep === 'formation' ? '편성하기' : nextSortiePrepStep === 'loadout' ? '장비·보급' : '출진'
|
||||
}
|
||||
: null,
|
||||
sortiePortraitRosterView: {
|
||||
enabled: stagedSortiePrep && !this.isFirstSortiePrepSlice(),
|
||||
total: portraitRosterUnits.length,
|
||||
visibleCount: Math.min(portraitRosterVisibleCount, portraitRosterUnits.length),
|
||||
start: portraitRosterUnits.length > 0 ? portraitRosterScroll + 1 : 0,
|
||||
end: portraitRosterScroll + Math.min(portraitRosterVisibleCount, portraitRosterUnits.length - portraitRosterScroll),
|
||||
maxScroll: portraitRosterMaxScroll,
|
||||
scroll: portraitRosterScroll
|
||||
},
|
||||
sortieHasBattle: Boolean(sortieScenario),
|
||||
nextSortieBattleId: sortieScenario?.id ?? null,
|
||||
nextSortieBattleTitle: sortieScenario?.title ?? null,
|
||||
@@ -16661,6 +16916,8 @@ export class CampScene extends Phaser.Scene {
|
||||
inventory: this.campaign.inventory,
|
||||
selectedSortieUnitIds: this.campaign.selectedSortieUnitIds,
|
||||
sortieFormationAssignments: this.campaign.sortieFormationAssignments,
|
||||
sortieItemAssignments: this.campaign.sortieItemAssignments,
|
||||
activeSaveSlot: this.campaign.activeSaveSlot,
|
||||
reserveTrainingAssignments: this.campaign.reserveTrainingAssignments,
|
||||
reserveTrainingFocus: this.campaign.reserveTrainingFocus,
|
||||
roster: this.campaign.roster.map((unit) => ({
|
||||
|
||||
Reference in New Issue
Block a user