feat: paginate large camp rosters
This commit is contained in:
@@ -215,6 +215,31 @@ type CampTabButtonView = {
|
||||
text: Phaser.GameObjects.Text;
|
||||
};
|
||||
|
||||
type CampRosterBounds = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type CampRosterLayout = {
|
||||
page: number;
|
||||
pageCount: number;
|
||||
rowsPerPage: number;
|
||||
totalCount: number;
|
||||
visibleUnitIds: string[];
|
||||
listBounds: CampRosterBounds;
|
||||
navigationBounds: {
|
||||
container: CampRosterBounds;
|
||||
previous: CampRosterBounds;
|
||||
label: CampRosterBounds;
|
||||
next: CampRosterBounds;
|
||||
} | null;
|
||||
rowBounds: Array<CampRosterBounds & { unitId: string }>;
|
||||
previousEnabled: boolean;
|
||||
nextEnabled: boolean;
|
||||
};
|
||||
|
||||
type CampSupplyDefinition = {
|
||||
label: string;
|
||||
usableId: string;
|
||||
@@ -11015,6 +11040,8 @@ export class CampScene extends Phaser.Scene {
|
||||
private campaign?: CampaignState;
|
||||
private report?: FirstBattleReport;
|
||||
private selectedUnitId = 'liu-bei';
|
||||
private campRosterPage = 0;
|
||||
private campRosterLayout?: CampRosterLayout;
|
||||
private activeTab: CampTab = 'status';
|
||||
private selectedDialogueId = campDialogues[0].id;
|
||||
private selectedVisitId = campVisits[0].id;
|
||||
@@ -11081,6 +11108,8 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
create() {
|
||||
this.contentObjects = [];
|
||||
this.campRosterPage = 0;
|
||||
this.campRosterLayout = undefined;
|
||||
this.dialogueObjects = [];
|
||||
this.sortieObjects = [];
|
||||
this.sortieComparisonPanelView = undefined;
|
||||
@@ -17419,18 +17448,35 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private renderUnitColumn() {
|
||||
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
||||
const rowsPerPage = 6;
|
||||
const pageCount = Math.max(1, Math.ceil(allies.length / rowsPerPage));
|
||||
const paginated = pageCount > 1;
|
||||
this.campRosterPage = Phaser.Math.Clamp(this.campRosterPage, 0, pageCount - 1);
|
||||
const visibleAllies = allies.slice(
|
||||
this.campRosterPage * rowsPerPage,
|
||||
(this.campRosterPage + 1) * rowsPerPage
|
||||
);
|
||||
const x = 42;
|
||||
const listY = 120;
|
||||
const width = 318;
|
||||
const availableHeight = 462;
|
||||
const rowGap = allies.length > 1 ? Math.min(132, Math.floor(availableHeight / allies.length)) : 132;
|
||||
const cardHeight = Math.min(118, Math.max(48, rowGap - 8));
|
||||
const compact = cardHeight < 88;
|
||||
const rowGap = paginated
|
||||
? 70
|
||||
: allies.length > 1
|
||||
? Math.min(132, Math.floor(availableHeight / allies.length))
|
||||
: 132;
|
||||
const cardHeight = paginated ? 64 : Math.min(118, Math.max(48, rowGap - 8));
|
||||
const compact = paginated || cardHeight < 88;
|
||||
const navigationY = 542;
|
||||
const navigationHeight = 28;
|
||||
const rowBounds: CampRosterLayout['rowBounds'] = [];
|
||||
this.renderRosterColumnSummary(42, 86, 318, allies);
|
||||
allies.forEach((unit, index) => {
|
||||
const x = 42;
|
||||
const y = 120 + index * rowGap;
|
||||
visibleAllies.forEach((unit, index) => {
|
||||
const y = listY + index * rowGap;
|
||||
const active = this.selectedUnitId === unit.id;
|
||||
const rosterStatus = this.unitRosterStatus(unit);
|
||||
const training = this.latestReserveTraining(unit.id);
|
||||
const bg = this.track(this.add.rectangle(x, y, 318, cardHeight, active ? 0x25384a : 0x111922, active ? 0.96 : 0.86));
|
||||
const bg = this.track(this.add.rectangle(x, y, width, cardHeight, active ? 0x25384a : 0x111922, active ? 0.96 : 0.86));
|
||||
bg.setOrigin(0);
|
||||
bg.setStrokeStyle(2, active ? palette.gold : palette.blue, active ? 0.9 : 0.46);
|
||||
bg.setInteractive({ useHandCursor: true });
|
||||
@@ -17439,6 +17485,8 @@ export class CampScene extends Phaser.Scene {
|
||||
this.selectedUnitId = unit.id;
|
||||
this.render();
|
||||
});
|
||||
const bounds = bg.getBounds();
|
||||
rowBounds.push({ unitId: unit.id, x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height });
|
||||
|
||||
const portraitKey = campaignPortraitKeysByUnitId[unit.id];
|
||||
const portraitTexture = portraitKey
|
||||
@@ -17446,27 +17494,120 @@ export class CampScene extends Phaser.Scene {
|
||||
(texture): texture is string => Boolean(texture && this.textures.exists(texture))
|
||||
)
|
||||
: undefined;
|
||||
const portraitCenterX = paginated ? x + 38 : x + 58;
|
||||
const portraitCenterY = y + cardHeight / 2;
|
||||
const portraitSize = compact ? 40 : 84;
|
||||
const portraitSize = paginated ? 48 : compact ? 40 : 84;
|
||||
if (portraitTexture && this.textures.exists(portraitTexture)) {
|
||||
const portrait = this.track(this.add.image(x + 58, portraitCenterY, portraitTexture));
|
||||
const portrait = this.track(this.add.image(portraitCenterX, portraitCenterY, portraitTexture));
|
||||
portrait.setDisplaySize(portraitSize, portraitSize);
|
||||
} else {
|
||||
this.renderUnitFallbackPortrait(x + 58, portraitCenterY, compact ? 20 : 40, unit);
|
||||
this.renderUnitFallbackPortrait(portraitCenterX, portraitCenterY, paginated ? 24 : compact ? 20 : 40, unit);
|
||||
}
|
||||
|
||||
const textX = compact ? x + 102 : x + 116;
|
||||
this.track(this.add.text(textX, y + (compact ? 7 : 17), `${unit.name} Lv ${unit.level}`, this.textStyle(compact ? 14 : 20, '#f2e3bf', true)));
|
||||
this.track(this.add.text(textX, y + (compact ? 27 : 47), `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(compact ? 10 : 13, '#d4dce6')));
|
||||
const textX = paginated ? x + 70 : compact ? x + 102 : x + 116;
|
||||
const nameY = paginated ? y + 6 : y + (compact ? 7 : 17);
|
||||
const detailY = paginated ? y + 26 : y + (compact ? 27 : 47);
|
||||
const expY = paginated ? y + 42 : y + (compact ? 40 : 72);
|
||||
this.track(this.add.text(textX, nameY, `${unit.name} Lv ${unit.level}`, this.textStyle(paginated ? 15 : compact ? 14 : 20, '#f2e3bf', true)));
|
||||
this.track(this.add.text(textX, detailY, `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(paginated || compact ? 10 : 13, '#d4dce6')));
|
||||
const expLine = training
|
||||
? `경험 ${unit.exp}/100 · ${training.focusLabel ?? '대기'} +${training.expGained}`
|
||||
: `경험 ${unit.exp}/100`;
|
||||
this.track(this.add.text(textX, y + (compact ? 40 : 72), expLine, this.textStyle(compact ? 10 : 13, training ? '#a8ffd0' : '#d8b15f', true)));
|
||||
this.drawBar(textX, y + cardHeight - (compact ? 10 : 17), compact ? 142 : 176, compact ? 5 : 8, unit.exp / 100, training ? palette.green : palette.gold);
|
||||
this.track(
|
||||
this.add.text(
|
||||
textX,
|
||||
expY,
|
||||
paginated ? this.compactText(expLine, 28) : expLine,
|
||||
this.textStyle(paginated || compact ? 10 : 13, training ? '#a8ffd0' : '#d8b15f', true)
|
||||
)
|
||||
);
|
||||
this.drawBar(
|
||||
textX,
|
||||
paginated ? y + 57 : y + cardHeight - (compact ? 10 : 17),
|
||||
paginated ? width - 82 : compact ? 142 : 176,
|
||||
paginated ? 4 : compact ? 5 : 8,
|
||||
unit.exp / 100,
|
||||
training ? palette.green : palette.gold
|
||||
);
|
||||
const badgeColor = rosterStatus.tone === 'selected' ? '#a8ffd0' : rosterStatus.tone === 'recommended' ? '#ffdf7b' : '#9fb0bf';
|
||||
const badge = this.track(this.add.text(x + 302, y + (compact ? 7 : 17), rosterStatus.label, this.textStyle(compact ? 10 : 11, badgeColor, true)));
|
||||
const badge = this.track(
|
||||
this.add.text(
|
||||
paginated ? x + width - 12 : x + 302,
|
||||
paginated ? y + 7 : y + (compact ? 7 : 17),
|
||||
rosterStatus.label,
|
||||
this.textStyle(paginated || compact ? 10 : 11, badgeColor, true)
|
||||
)
|
||||
);
|
||||
badge.setOrigin(1, 0);
|
||||
});
|
||||
|
||||
const debugBounds = (object: Phaser.GameObjects.Rectangle | Phaser.GameObjects.Text): CampRosterBounds => {
|
||||
const bounds = object.getBounds();
|
||||
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
|
||||
};
|
||||
const previousEnabled = paginated && this.campRosterPage > 0;
|
||||
const nextEnabled = paginated && this.campRosterPage < pageCount - 1;
|
||||
let navigationBounds: CampRosterLayout['navigationBounds'] = null;
|
||||
if (paginated) {
|
||||
const navigation = this.track(this.add.rectangle(x, navigationY, width, navigationHeight, 0x0b1118, 0.96));
|
||||
navigation.setOrigin(0);
|
||||
navigation.setStrokeStyle(1, 0x647485, 0.62);
|
||||
const renderNavigationButton = (buttonX: number, label: string, enabled: boolean, page: number) => {
|
||||
const button = this.track(this.add.rectangle(buttonX, navigationY + 2, 70, 24, enabled ? 0x25384a : 0x141b22, 0.96));
|
||||
button.setOrigin(0);
|
||||
button.setStrokeStyle(1, enabled ? palette.gold : 0x53606c, enabled ? 0.78 : 0.36);
|
||||
if (enabled) {
|
||||
button.setInteractive({ useHandCursor: true });
|
||||
button.on('pointerover', () => button.setFillStyle(0x31475a, 0.98));
|
||||
button.on('pointerout', () => button.setFillStyle(0x25384a, 0.96));
|
||||
button.on('pointerdown', () => this.setCampRosterPage(page));
|
||||
}
|
||||
const text = this.track(this.add.text(buttonX + 35, navigationY + 14, label, this.textStyle(12, enabled ? '#f4dfad' : '#77828c', true)));
|
||||
text.setOrigin(0.5);
|
||||
return button;
|
||||
};
|
||||
|
||||
const previousButton = renderNavigationButton(x + 2, '이전', previousEnabled, this.campRosterPage - 1);
|
||||
const nextButton = renderNavigationButton(x + width - 72, '다음', nextEnabled, this.campRosterPage + 1);
|
||||
const pageLabel = this.track(this.add.text(x + width / 2, navigationY + 14, `${this.campRosterPage + 1} / ${pageCount}`, this.textStyle(13, '#d4dce6', true)));
|
||||
pageLabel.setOrigin(0.5);
|
||||
navigationBounds = {
|
||||
container: debugBounds(navigation),
|
||||
previous: debugBounds(previousButton),
|
||||
label: debugBounds(pageLabel),
|
||||
next: debugBounds(nextButton)
|
||||
};
|
||||
}
|
||||
|
||||
this.campRosterLayout = {
|
||||
page: this.campRosterPage,
|
||||
pageCount,
|
||||
rowsPerPage,
|
||||
totalCount: allies.length,
|
||||
visibleUnitIds: visibleAllies.map((unit) => unit.id),
|
||||
listBounds: {
|
||||
x,
|
||||
y: listY,
|
||||
width,
|
||||
height: visibleAllies.length > 0 ? (visibleAllies.length - 1) * rowGap + cardHeight : 0
|
||||
},
|
||||
navigationBounds,
|
||||
rowBounds,
|
||||
previousEnabled,
|
||||
nextEnabled
|
||||
};
|
||||
}
|
||||
|
||||
private setCampRosterPage(page: number) {
|
||||
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
||||
const pageCount = Math.max(1, Math.ceil(allies.length / 6));
|
||||
const nextPage = Phaser.Math.Clamp(page, 0, pageCount - 1);
|
||||
if (nextPage === this.campRosterPage) {
|
||||
return;
|
||||
}
|
||||
soundDirector.playSelect();
|
||||
this.campRosterPage = nextPage;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private renderRosterColumnSummary(x: number, y: number, width: number, allies: UnitData[]) {
|
||||
@@ -20524,6 +20665,7 @@ export class CampScene extends Phaser.Scene {
|
||||
private clearContent() {
|
||||
this.contentObjects.forEach((object) => object.destroy());
|
||||
this.contentObjects = [];
|
||||
this.campRosterLayout = undefined;
|
||||
this.reportFormationReviewToggleButton = undefined;
|
||||
this.reportFormationHistoryToggleButton = undefined;
|
||||
this.reportSortieOrderRewardBadge = undefined;
|
||||
@@ -20609,6 +20751,22 @@ export class CampScene extends Phaser.Scene {
|
||||
return {
|
||||
scene: this.scene.key,
|
||||
activeTab: this.activeTab,
|
||||
campRoster: this.campRosterLayout
|
||||
? {
|
||||
...this.campRosterLayout,
|
||||
visibleUnitIds: [...this.campRosterLayout.visibleUnitIds],
|
||||
listBounds: { ...this.campRosterLayout.listBounds },
|
||||
navigationBounds: this.campRosterLayout.navigationBounds
|
||||
? {
|
||||
container: { ...this.campRosterLayout.navigationBounds.container },
|
||||
previous: { ...this.campRosterLayout.navigationBounds.previous },
|
||||
label: { ...this.campRosterLayout.navigationBounds.label },
|
||||
next: { ...this.campRosterLayout.navigationBounds.next }
|
||||
}
|
||||
: null,
|
||||
rowBounds: this.campRosterLayout.rowBounds.map((bounds) => ({ ...bounds }))
|
||||
}
|
||||
: null,
|
||||
sortieOperationOrder: {
|
||||
available: Boolean(sortieScenario),
|
||||
required: Boolean(sortieScenario && stagedSortiePrep),
|
||||
|
||||
Reference in New Issue
Block a user