Polish Wuzhang ending QA flow

This commit is contained in:
2026-06-26 23:32:29 +09:00
parent 8ef5800584
commit 16738e74c2
4 changed files with 122 additions and 20 deletions

View File

@@ -466,7 +466,7 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
'sixty-fifth-battle-weishui-northbank',
'sixty-sixth-battle-wuzhang-final'
],
nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보', '한중 우로 방어', '기산 재출정', '노성 추격', '위수 진영', '위수 북안']
nextHints: ['한중 창고 정비', '출전 무장 재편', '기산 출진로', '천수·가정 방면', '강유 합류', '진창 공성 재계산', '무도·음평 확보', '한중 우로 방어', '기산 재출정', '노성 추격', '위수 진영', '위수 북안', '오장원 최종전']
}
];
@@ -10007,6 +10007,7 @@ export class CampScene extends Phaser.Scene {
private visitedTabs = new Set<CampTab>();
private selectedSortieUnitIds: string[] = [];
private sortieFocusedUnitId = 'liu-bei';
private sortieRosterScroll = 0;
private terrainCountCache = new Map<BattleScenarioId, SortieTerrainCounts>();
constructor() {
@@ -11260,16 +11261,35 @@ export class CampScene extends Phaser.Scene {
const allies = this.sortieAllies();
const selectedCount = this.selectedSortieUnitIds.length;
const maxUnits = this.sortieMaxUnits();
const denseRoster = allies.length > 12;
const rowGap = denseRoster ? 17 : allies.length > 7 ? 30 : allies.length > 5 ? 38 : allies.length > 4 ? 43 : 48;
const rowHeight = denseRoster ? 15 : allies.length > 7 ? 24 : rowGap - 6;
const rosterView = this.sortieRosterViewState(allies.length, height);
const { denseRoster, rowGap, rowHeight, listTopOffset, listFooterHeight, maxVisibleRows, maxScroll } = rosterView;
this.sortieRosterScroll = Phaser.Math.Clamp(this.sortieRosterScroll, 0, maxScroll);
const visibleAllies = allies.slice(this.sortieRosterScroll, this.sortieRosterScroll + maxVisibleRows);
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 * 3, maxScroll);
};
if (maxScroll > 0) {
bg.setInteractive({ useHandCursor: false });
bg.on('wheel', handleRosterWheel);
}
this.trackSortie(
this.add.text(x + 18, y + 14, `출전 무장 선택 ${selectedCount}/${maxUnits}`, this.textStyle(18, '#f2e3bf', true))
).setDepth(depth + 1);
this.trackSortie(this.add.text(x + width - 18, y + 17, '클릭해서 분석 및 출전/대기 전환', this.textStyle(12, '#9fb0bf'))).setOrigin(1, 0).setDepth(depth + 1);
allies.forEach((unit, index) => {
const rowY = y + (denseRoster ? 48 : 50) + index * rowGap;
visibleAllies.forEach((unit, index) => {
const rowY = y + listTopOffset + index * rowGap;
const selected = this.isSortieSelected(unit.id);
const required = this.isRequiredSortieUnit(unit.id);
const recruited = !foundingSortieUnitIds.has(unit.id);
@@ -11280,6 +11300,7 @@ export class CampScene extends Phaser.Scene {
row.setDepth(depth + 1);
row.setStrokeStyle(1, selected ? palette.green : 0x53606c, selected ? 0.62 : 0.38);
row.setInteractive({ useHandCursor: true });
row.on('wheel', handleRosterWheel);
row.on('pointerover', () => {
this.sortieFocusedUnitId = unit.id;
});
@@ -11303,6 +11324,63 @@ export class CampScene extends Phaser.Scene {
this.trackSortie(this.add.text(x + 610, rowY + 18, this.compactText(summary.bondLine, 15), this.textStyle(11, selected ? '#a8ffd0' : '#68727e', selected))).setDepth(depth + 2);
}
});
if (maxScroll > 0) {
const rangeEnd = this.sortieRosterScroll + visibleAllies.length;
const info = this.trackSortie(
this.add.text(
x + width - 18,
y + height - 20,
`${this.sortieRosterScroll + 1}-${rangeEnd}/${allies.length} · 휠로 이동`,
this.textStyle(11, '#9fb0bf', true)
)
);
info.setOrigin(1, 0);
info.setDepth(depth + 1);
const trackX = x + width - 12;
const trackY = y + listTopOffset;
const trackHeight = height - listFooterHeight;
const scrollTrack = this.trackSortie(this.add.rectangle(trackX, trackY, 4, trackHeight, 0x05070a, 0.78));
scrollTrack.setOrigin(0, 0);
scrollTrack.setDepth(depth + 1);
const thumbHeight = Math.max(24, trackHeight * (maxVisibleRows / allies.length));
const thumbY = trackY + (trackHeight - thumbHeight) * (this.sortieRosterScroll / maxScroll);
const scrollThumb = this.trackSortie(this.add.rectangle(trackX, thumbY, 4, thumbHeight, palette.gold, 0.84));
scrollThumb.setOrigin(0, 0);
scrollThumb.setDepth(depth + 2);
}
}
private updateSortieRosterScroll(nextScroll: number, maxScroll: number) {
const clamped = Phaser.Math.Clamp(Math.round(nextScroll), 0, maxScroll);
if (clamped === this.sortieRosterScroll) {
return;
}
this.sortieRosterScroll = clamped;
this.showSortiePrep();
}
private sortieRosterViewState(totalUnits: number, height = 286) {
const denseRoster = totalUnits > 12;
const rowGap = denseRoster ? 17 : totalUnits > 7 ? 30 : totalUnits > 5 ? 38 : totalUnits > 4 ? 43 : 48;
const rowHeight = denseRoster ? 15 : totalUnits > 7 ? 24 : rowGap - 6;
const listTopOffset = denseRoster ? 48 : 50;
const listFooterHeight = denseRoster ? 74 : 68;
const naturalVisibleRows = Math.max(1, Math.floor((height - listTopOffset - 10 - rowHeight) / rowGap) + 1);
const scrollVisibleRows = Math.max(1, Math.floor((height - listFooterHeight) / rowGap));
const maxVisibleRows = totalUnits <= naturalVisibleRows ? totalUnits : scrollVisibleRows;
const maxScroll = Math.max(0, totalUnits - maxVisibleRows);
return {
denseRoster,
rowGap,
rowHeight,
listTopOffset,
listFooterHeight,
maxVisibleRows,
maxScroll,
scroll: Phaser.Math.Clamp(this.sortieRosterScroll, 0, maxScroll)
};
}
private renderSortieFormationPlan(x: number, y: number, width: number, height: number, depth: number) {
@@ -13201,6 +13279,7 @@ export class CampScene extends Phaser.Scene {
sortieFocusedUnit: this.sortieFocusedUnitSummary(),
reserveTrainingFocus: this.reserveTrainingFocusDefinition(),
reserveTrainingFocusOptions: campaignReserveTrainingFocusDefinitions.map((focus) => ({ ...focus })),
sortieRosterView: this.sortieRosterViewState(this.sortieAllies().length),
sortieRoster: this.sortieAllies().map((unit) => {
const summary = this.sortieUnitTacticalSummary(unit);
return {