feat: polish camp and progression interactions

This commit is contained in:
2026-07-18 00:24:24 +09:00
parent 991683a476
commit 470516bfd9
4 changed files with 325 additions and 52 deletions

View File

@@ -240,6 +240,15 @@ type CampTabButtonView = {
tab: CampTab;
bg: Phaser.GameObjects.Rectangle;
text: Phaser.GameObjects.Text;
indicator: Phaser.GameObjects.Rectangle;
hovered: boolean;
};
type CampActionButtonView = {
background: Phaser.GameObjects.Rectangle;
label: Phaser.GameObjects.Text;
primary: boolean;
hovered: boolean;
};
type CampRosterBounds = {
@@ -11247,6 +11256,7 @@ export class CampScene extends Phaser.Scene {
private reportFormationHistoryView?: ReportFormationHistoryView;
private reportFormationHistoryUndoState?: ReportFormationHistoryUndoState;
private tabButtons: CampTabButtonView[] = [];
private sortiePrimaryActionButton?: CampActionButtonView;
private visitedTabs = new Set<CampTab>();
private selectedSortieUnitIds: string[] = [];
private sortieFormationAssignments: SortieFormationAssignments = {};
@@ -11355,6 +11365,7 @@ export class CampScene extends Phaser.Scene {
this.reportFormationHistoryView = undefined;
this.reportFormationHistoryUndoState = undefined;
this.tabButtons = [];
this.sortiePrimaryActionButton = undefined;
this.visitedTabs = new Set(['status']);
this.campaign = getCampaignState();
this.report = this.campaign.firstBattleReport ?? getFirstBattleReport() ?? this.createFallbackReport();
@@ -12594,7 +12605,7 @@ export class CampScene extends Phaser.Scene {
return;
}
this.showSortiePrep();
});
}, true);
}
private showCampSaveSlotPanel() {
@@ -12938,11 +12949,15 @@ export class CampScene extends Phaser.Scene {
const bg = this.scaleLegacyCampUi(this.add.rectangle(x, y, 76, 34, 0x182431, 0.94));
bg.setStrokeStyle(1, palette.blue, 0.62);
bg.setInteractive({ useHandCursor: true });
bg.on('pointerdown', () => {
const indicator = this.scaleLegacyCampUi(this.add.rectangle(x, y + 14, 52, 3, palette.gold, 1));
indicator.setVisible(false);
const selectTab = () => {
soundDirector.playSelect();
this.activeTab = tab;
this.render();
});
};
const text = this.scaleLegacyCampUi(this.add.text(x, y, label, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
@@ -12952,42 +12967,58 @@ export class CampScene extends Phaser.Scene {
}));
text.setOrigin(0.5);
text.setInteractive({ useHandCursor: true });
text.on('pointerdown', () => {
soundDirector.playSelect();
this.activeTab = tab;
this.render();
const view: CampTabButtonView = { tab, bg, text, indicator, hovered: false };
const setHovered = (hovered: boolean) => {
view.hovered = hovered;
this.updateTabButtons();
};
[bg, text].forEach((target) => {
target.on('pointerover', () => setHovered(true));
target.on('pointerout', () => setHovered(false));
target.on('pointerdown', selectTab);
});
this.tabButtons.push({ tab, bg, text });
this.tabButtons.push(view);
this.updateTabButtons();
}
private updateTabButtons() {
const accentColor = this.campSkinSelection?.skin.accentColor ?? palette.gold;
this.tabButtons.forEach(({ tab, bg, text }) => {
this.tabButtons.forEach(({ tab, bg, text, indicator, hovered }) => {
const active = this.activeTab === tab;
bg.setFillStyle(active ? 0x25384a : 0x182431, active ? 0.98 : 0.94);
bg.setStrokeStyle(1, active ? accentColor : palette.blue, active ? 0.9 : 0.62);
text.setColor(active ? '#fff2b8' : '#f2e3bf');
bg.setFillStyle(active ? 0x2b4052 : hovered ? 0x243747 : 0x182431, active || hovered ? 0.98 : 0.94);
bg.setStrokeStyle(active || hovered ? 2 : 1, active || hovered ? accentColor : palette.blue, active ? 0.96 : hovered ? 0.82 : 0.62);
text.setColor(active || hovered ? '#fff2b8' : '#f2e3bf');
indicator.setFillStyle(accentColor, 1);
indicator.setVisible(active || hovered);
indicator.setAlpha(active ? 1 : 0.66);
});
}
private addCommandButton(label: string, x: number, y: number, width: number, action: () => void) {
const bg = this.scaleLegacyCampUi(this.add.rectangle(x, y, width, 36, 0x1a2630, 0.96));
bg.setStrokeStyle(1, this.campSkinSelection?.skin.accentColor ?? palette.gold, 0.84);
private addCommandButton(label: string, x: number, y: number, width: number, action: () => void, primary = false) {
const restingFill = primary ? 0x4a371d : 0x1a2630;
const hoverFill = primary ? 0x654c25 : 0x283947;
const accentColor = this.campSkinSelection?.skin.accentColor ?? palette.gold;
const bg = this.scaleLegacyCampUi(this.add.rectangle(x, y, width, 36, restingFill, 0.96));
bg.setStrokeStyle(primary ? 2 : 1, accentColor, primary ? 0.96 : 0.84);
bg.setInteractive({ useHandCursor: true });
bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98));
bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.96));
bg.on('pointerdown', action);
const text = this.scaleLegacyCampUi(this.add.text(x, y, label, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '16px',
color: '#f2e3bf',
color: primary ? '#fff2b8' : '#f2e3bf',
fontStyle: '700'
}));
text.setOrigin(0.5);
text.setInteractive({ useHandCursor: true });
text.on('pointerdown', action);
const setHovered = (hovered: boolean) => {
bg.setFillStyle(hovered ? hoverFill : restingFill, hovered ? 0.99 : 0.96);
text.setColor(hovered || primary ? '#fff2b8' : '#f2e3bf');
};
[bg, text].forEach((target) => {
target.on('pointerover', () => setHovered(true));
target.on('pointerout', () => setHovered(false));
target.on('pointerdown', action);
});
}
private render() {
@@ -15077,10 +15108,12 @@ export class CampScene extends Phaser.Scene {
bg.setOrigin(0);
bg.setDepth(depth);
bg.setStrokeStyle(1, active ? palette.green : enabled ? palette.gold : 0x53606c, active ? 0.78 : enabled ? 0.56 : 0.3);
const hoverFill = active ? 0x314738 : 0x283947;
const setHovered = (hovered: boolean) => bg.setFillStyle(hovered ? hoverFill : fill, hovered ? 0.98 : 0.96);
if (enabled) {
bg.setInteractive({ useHandCursor: true });
bg.on('pointerover', () => bg.setFillStyle(active ? 0x314738 : 0x283947, 0.98));
bg.on('pointerout', () => bg.setFillStyle(fill, 0.96));
bg.on('pointerover', () => setHovered(true));
bg.on('pointerout', () => setHovered(false));
bg.on('pointerdown', action);
}
const text = this.trackSortie(this.add.text(x + width / 2, y + height / 2, label, this.textStyle(12, enabled || active ? '#f2e3bf' : '#77818c', true)));
@@ -15088,6 +15121,8 @@ export class CampScene extends Phaser.Scene {
text.setDepth(depth + 1);
if (enabled) {
text.setInteractive({ useHandCursor: true });
text.on('pointerover', () => setHovered(true));
text.on('pointerout', () => setHovered(false));
text.on('pointerdown', action);
}
}
@@ -18946,19 +18981,32 @@ export class CampScene extends Phaser.Scene {
}
private addSortieButton(label: string, x: number, y: number, width: number, action: () => void, depth: number, primary = false) {
const bg = this.trackSortie(this.add.rectangle(x, y, width, 36, 0x1a2630, 0.96));
const restingFill = primary ? 0x4a371d : 0x1a2630;
const hoverFill = primary ? 0x654c25 : 0x283947;
const bg = this.trackSortie(this.add.rectangle(x, y, width, 36, restingFill, 0.96));
bg.setDepth(depth);
bg.setStrokeStyle(1, primary ? palette.gold : palette.blue, 0.78);
bg.setStrokeStyle(primary ? 2 : 1, primary ? palette.gold : palette.blue, primary ? 0.96 : 0.78);
bg.setInteractive({ useHandCursor: true });
bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98));
bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.96));
bg.on('pointerdown', action);
const text = this.trackSortie(this.add.text(x, y, label, this.textStyle(15, '#f2e3bf', true)));
const text = this.trackSortie(this.add.text(x, y, label, this.textStyle(15, primary ? '#fff2b8' : '#f2e3bf', true)));
text.setOrigin(0.5);
text.setDepth(depth + 1);
text.setInteractive({ useHandCursor: true });
text.on('pointerdown', action);
const view: CampActionButtonView = { background: bg, label: text, primary, hovered: false };
const setHovered = (hovered: boolean) => {
view.hovered = hovered;
bg.setFillStyle(hovered ? hoverFill : restingFill, hovered ? 0.99 : 0.96);
text.setColor(hovered || primary ? '#fff2b8' : '#f2e3bf');
};
[bg, text].forEach((target) => {
target.on('pointerover', () => setHovered(true));
target.on('pointerout', () => setHovered(false));
target.on('pointerdown', action);
});
if (primary) {
this.sortiePrimaryActionButton = view;
}
return view;
}
private startVictoryStory() {
@@ -19072,6 +19120,7 @@ export class CampScene extends Phaser.Scene {
private hideSortiePrep(clearPinnedSwap = true) {
this.sortieObjects.forEach((object) => object.destroy());
this.sortieObjects = [];
this.sortiePrimaryActionButton = undefined;
this.sortiePortraitRosterLayout = undefined;
this.sortieHoveredUnitId = undefined;
if (clearPinnedSwap) {
@@ -19139,11 +19188,24 @@ export class CampScene extends Phaser.Scene {
const y = listY + index * rowGap;
const active = this.selectedUnitId === unit.id;
const rosterStatus = this.unitRosterStatus(unit);
const disabled = rosterStatus.tone === 'disabled';
const training = this.latestReserveTraining(unit.id);
const bg = this.track(this.add.rectangle(x, y, width, cardHeight, active ? 0x25384a : 0x111922, active ? 0.96 : 0.86));
const restingFill = active ? 0x25384a : disabled ? 0x0d131a : 0x111922;
const restingAlpha = active ? 0.96 : disabled ? 0.72 : 0.86;
const restingStroke = active ? palette.gold : disabled ? 0x53606c : palette.blue;
const restingStrokeAlpha = active ? 0.9 : disabled ? 0.42 : 0.46;
const bg = this.track(this.add.rectangle(x, y, width, cardHeight, restingFill, restingAlpha));
bg.setOrigin(0);
bg.setStrokeStyle(2, active ? palette.gold : palette.blue, active ? 0.9 : 0.46);
bg.setStrokeStyle(2, restingStroke, restingStrokeAlpha);
bg.setInteractive({ useHandCursor: true });
bg.on('pointerover', () => {
bg.setFillStyle(active ? 0x31495d : disabled ? 0x182029 : 0x1c2a36, active || !disabled ? 0.98 : 0.82);
bg.setStrokeStyle(2, active || !disabled ? palette.gold : 0x71808e, active ? 0.98 : disabled ? 0.62 : 0.88);
});
bg.on('pointerout', () => {
bg.setFillStyle(restingFill, restingAlpha);
bg.setStrokeStyle(2, restingStroke, restingStrokeAlpha);
});
bg.on('pointerdown', () => {
soundDirector.playSelect();
this.selectedUnitId = unit.id;
@@ -22669,6 +22731,18 @@ export class CampScene extends Phaser.Scene {
return {
scene: this.scene.key,
activeTab: this.activeTab,
campTabs: this.tabButtons.map((button) => ({
id: button.tab,
active: this.activeTab === button.tab,
hovered: button.hovered,
visualState: this.activeTab === button.tab ? 'active' : button.hovered ? 'hover' : 'idle',
bounds: this.sortieObjectBoundsDebug(button.bg),
interactive: Boolean(button.bg.input?.enabled && button.text.input?.enabled),
indicatorVisible: button.indicator.visible,
fillColor: button.bg.fillColor,
strokeColor: button.bg.strokeColor,
lineWidth: button.bg.lineWidth
})),
campRoster: this.campRosterLayout
? {
...this.campRosterLayout,
@@ -23032,11 +23106,21 @@ export class CampScene extends Phaser.Scene {
active: step.id === this.sortiePrepStep,
complete: this.firstSortieStepComplete(step.id, sortieChecklist)
})),
sortiePrimaryAction: stagedSortiePrep
sortiePrimaryAction: this.sortiePrimaryActionButton?.background.active
? {
kind: nextSortiePrepStep ? 'next' : 'launch',
nextStep: nextSortiePrepStep,
label: nextSortiePrepStep === 'formation' ? '편성하기' : nextSortiePrepStep === 'loadout' ? '장비·보급' : '출진'
label: this.sortiePrimaryActionButton.label.text,
primary: this.sortiePrimaryActionButton.primary,
hovered: this.sortiePrimaryActionButton.hovered,
visualState: this.sortiePrimaryActionButton.hovered ? 'hover' : 'primary',
bounds: this.sortieObjectBoundsDebug(this.sortiePrimaryActionButton.background),
interactive: Boolean(
this.sortiePrimaryActionButton.background.input?.enabled && this.sortiePrimaryActionButton.label.input?.enabled
),
fillColor: this.sortiePrimaryActionButton.background.fillColor,
strokeColor: this.sortiePrimaryActionButton.background.strokeColor,
lineWidth: this.sortiePrimaryActionButton.background.lineWidth
}
: null,
sortiePortraitRosterView: {