feat: refine battle feedback and early guidance
This commit is contained in:
@@ -1261,6 +1261,9 @@ type RosterPanelLayout = {
|
||||
|
||||
type DeploymentPanelLayout = {
|
||||
headerBounds: { x: number; y: number; width: number; height: number };
|
||||
statusBounds: { x: number; y: number; width: number; height: number };
|
||||
changeCount: number;
|
||||
recommended: boolean;
|
||||
noticeBounds: { x: number; y: number; width: number; height: number };
|
||||
noticeTextBounds: { x: number; y: number; width: number; height: number };
|
||||
roleBounds: Array<{ x: number; y: number; width: number; height: number }>;
|
||||
@@ -3510,6 +3513,17 @@ export class BattleScene extends Phaser.Scene {
|
||||
private turnPromptMode?: TurnPromptMode;
|
||||
private turnPromptPrimaryAction?: () => void;
|
||||
private turnPromptSecondaryAction?: () => void;
|
||||
private mapResultPopupObjects: Phaser.GameObjects.Text[] = [];
|
||||
private mapResultPopupPeakCount = 0;
|
||||
private mapResultPopupLast?: {
|
||||
unitId: string;
|
||||
lines: string[];
|
||||
bounds: HudBounds;
|
||||
endBounds: HudBounds;
|
||||
duration: number;
|
||||
delay: number;
|
||||
lift: number;
|
||||
};
|
||||
private combatCutInRoot?: Phaser.GameObjects.Container;
|
||||
private combatCutInObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private combatCutInStageActive?: CombatCutInStageSnapshot;
|
||||
@@ -3647,6 +3661,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.hideBattleResult();
|
||||
this.hideTacticalCommandCutIn();
|
||||
this.hideCombatCutIn();
|
||||
this.mapResultPopupObjects.forEach((object) => object.active && object.destroy());
|
||||
this.mapResultPopupObjects = [];
|
||||
this.mapResultPopupPeakCount = 0;
|
||||
this.mapResultPopupLast = undefined;
|
||||
this.combatCutInStageActive = undefined;
|
||||
this.combatCutInStageLast = undefined;
|
||||
this.tacticalCommandCutInCount = 0;
|
||||
@@ -3655,6 +3673,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
|
||||
this.hideTacticalCommandCutIn();
|
||||
this.hideCombatCutIn();
|
||||
this.mapResultPopupObjects.forEach((object) => object.active && object.destroy());
|
||||
this.mapResultPopupObjects = [];
|
||||
});
|
||||
this.resultFormationReviewVisible = false;
|
||||
this.resultFormationReviewFeedback = '';
|
||||
@@ -7244,6 +7264,15 @@ export class BattleScene extends Phaser.Scene {
|
||||
return '추천 전열이 적용되어 있습니다. 장수를 선택한 뒤 시작 구역 안에서 위치를 바꿀 수 있습니다.';
|
||||
}
|
||||
|
||||
private deploymentChangeCount() {
|
||||
return this.deploymentSlots()
|
||||
.filter((slot): slot is DeploymentSlot & { unitId: string } => Boolean(slot.unitId))
|
||||
.filter((slot) => {
|
||||
const unit = battleUnits.find((candidate) => candidate.id === slot.unitId && candidate.hp > 0);
|
||||
return !unit || unit.x !== slot.x || unit.y !== slot.y;
|
||||
}).length;
|
||||
}
|
||||
|
||||
private deploymentSubtitle() {
|
||||
if (this.launchSortieRecommendation) {
|
||||
return `선택 전략 · ${this.launchSortieRecommendation.label} · ${this.launchSortieRecommendation.summary}`;
|
||||
@@ -7648,6 +7677,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
const noticeHeight = this.battleUiLength(52);
|
||||
const roleRowHeight = this.battleUiLength(40);
|
||||
const buttonHeight = this.battleUiLength(30);
|
||||
const deploymentChangeCount = this.deploymentChangeCount();
|
||||
const recommendedDeployment = deploymentChangeCount === 0;
|
||||
|
||||
const header = this.trackSideObject(this.add.rectangle(left, top, width, headerHeight, 0x101820, 0.96));
|
||||
header.setOrigin(0);
|
||||
@@ -7658,6 +7689,26 @@ export class BattleScene extends Phaser.Scene {
|
||||
color: '#f4dfad',
|
||||
fontStyle: '700'
|
||||
}));
|
||||
const statusWidth = this.battleUiLength(122);
|
||||
const statusHeight = this.battleUiLength(20);
|
||||
const statusLeft = left + width - statusWidth - this.battleUiLength(10);
|
||||
const statusTop = top + this.battleUiLength(8);
|
||||
const statusTone = recommendedDeployment ? palette.green : palette.gold;
|
||||
const status = this.trackSideObject(this.add.rectangle(statusLeft, statusTop, statusWidth, statusHeight, recommendedDeployment ? 0x15261f : 0x2b2114, 0.96));
|
||||
status.setOrigin(0);
|
||||
status.setStrokeStyle(this.battleUiLength(1), statusTone, recommendedDeployment ? 0.72 : 0.92);
|
||||
const statusText = this.trackSideObject(this.add.text(
|
||||
statusLeft + statusWidth / 2,
|
||||
statusTop + statusHeight / 2,
|
||||
recommendedDeployment ? '권장 배치 유지' : `변경 ${deploymentChangeCount}명 · 복원 가능`,
|
||||
{
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: this.battleUiFontSize(9),
|
||||
color: recommendedDeployment ? '#a8ffd0' : '#ffdf7b',
|
||||
fontStyle: '700'
|
||||
}
|
||||
));
|
||||
statusText.setOrigin(0.5);
|
||||
this.trackSideObject(this.add.text(left + this.battleUiLength(12), top + this.battleUiLength(32), this.deploymentSubtitle(), {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: this.battleUiFontSize(10),
|
||||
@@ -7738,9 +7789,19 @@ export class BattleScene extends Phaser.Scene {
|
||||
});
|
||||
|
||||
this.renderDeploymentButton(left, startButtonTop, width, '전투 시작', palette.gold, () => this.confirmPreBattleDeployment());
|
||||
this.renderDeploymentButton(left, restoreButtonTop, width, '추천 배치 복원', 0x647485, () => this.restoreRecommendedDeployment());
|
||||
this.renderDeploymentButton(
|
||||
left,
|
||||
restoreButtonTop,
|
||||
width,
|
||||
recommendedDeployment ? '추천 배치 유지 중' : `추천 배치 복원 · ${deploymentChangeCount}명`,
|
||||
recommendedDeployment ? 0x647485 : palette.gold,
|
||||
() => this.restoreRecommendedDeployment()
|
||||
);
|
||||
this.deploymentPanelLayout = {
|
||||
headerBounds: { x: left, y: top, width, height: headerHeight },
|
||||
statusBounds: { x: statusLeft, y: statusTop, width: statusWidth, height: statusHeight },
|
||||
changeCount: deploymentChangeCount,
|
||||
recommended: recommendedDeployment,
|
||||
noticeBounds: { x: left, y: noticeTop, width, height: noticeHeight },
|
||||
noticeTextBounds: this.gameObjectBoundsDebug(noticeText)!,
|
||||
roleBounds,
|
||||
@@ -10706,10 +10767,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
await this.delay(180);
|
||||
await this.playCombatCutIn(result.counter);
|
||||
}
|
||||
this.showCombatMapResult(result);
|
||||
if (result.counter) {
|
||||
this.showCombatMapResult(result.counter);
|
||||
}
|
||||
await this.showCombatExchangeMapResults(result);
|
||||
await this.delay(result.counter ? 220 : 160);
|
||||
this.finishUnitAction(attacker, this.formatCombatResult(result), {
|
||||
defeatedEnemyIds: result.defeated ? [result.defender.id] : []
|
||||
@@ -14632,9 +14690,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
const bondChain = bondChainResolution?.result;
|
||||
this.faceUnitToward(attacker, defender);
|
||||
if (hit) {
|
||||
this.flashDamage(defender, damage, critical, previousDefenderHp, bondChain?.previousDefenderHp);
|
||||
this.flashDamage(defender, bondChain?.previousDefenderHp ?? defender.hp);
|
||||
} else {
|
||||
this.flashMiss(defender, this.missFeedbackLabel(action, usable));
|
||||
this.flashMiss(defender);
|
||||
}
|
||||
|
||||
if (defender.hp <= 0) {
|
||||
@@ -14725,7 +14783,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
if (healAmount > 0) {
|
||||
target.hp = Math.min(target.maxHp, target.hp + healAmount);
|
||||
this.syncUnitMotion(target);
|
||||
this.flashSupport(target, `회복 +${healAmount}`, '#a8ffd0', `HP ${previousTargetHp}→${target.hp}`);
|
||||
this.flashSupport(target);
|
||||
this.updateMiniMap();
|
||||
}
|
||||
|
||||
@@ -14739,7 +14797,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
criticalBonus: usable.criticalBonus ?? 0
|
||||
};
|
||||
this.battleBuffs.set(target.id, buff);
|
||||
this.flashSupport(target, `강화 ${usable.duration ?? 1}턴`, '#ffdf7b', this.supportBuffCompactText(buff));
|
||||
this.flashSupport(target);
|
||||
}
|
||||
|
||||
const equipmentGrowth = this.awardEquipmentExp(user, 'accessory', this.supportEquipmentExpGain(user, usable));
|
||||
@@ -15251,6 +15309,9 @@ export class BattleScene extends Phaser.Scene {
|
||||
...(this.isFirstPursuitRoleEffectBattle()
|
||||
? [`적 의도 · 검=공격 · 발=추격 · 파훼 ${tacticalInitiativeThreshold}회 → 전술 명령`]
|
||||
: []),
|
||||
...(battleScenario.id === 'first-battle-zhuo-commandery'
|
||||
? ['첫 행동 · 아군 선택 → 파란 이동 칸 → 명령 공격 → 붉은 적 선택']
|
||||
: []),
|
||||
`작전 목표 · ${objectiveLines[0]}`
|
||||
]);
|
||||
return;
|
||||
@@ -15387,7 +15448,8 @@ export class BattleScene extends Phaser.Scene {
|
||||
const doctrineLines = lines.filter((line) => (
|
||||
line.startsWith('도원 공명') ||
|
||||
line.startsWith('공명 ') ||
|
||||
line.startsWith('적 의도')
|
||||
line.startsWith('적 의도') ||
|
||||
line.startsWith('첫 행동')
|
||||
));
|
||||
if (doctrineLines.length === 0) {
|
||||
doctrineLines.push('활성 공명 없음 · 역할 효과로 전선을 운용합니다.');
|
||||
@@ -15499,7 +15561,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
const doctrineText = this.add.text(left + 18, top + 145 + index * 20, this.truncateBattleLogText(line, 54), {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '11px',
|
||||
color: line.startsWith('적 의도') ? '#b9d9ec' : '#d8b15f',
|
||||
color: line.startsWith('적 의도') ? '#b9d9ec' : line.startsWith('첫 행동') ? '#a8ffd0' : '#d8b15f',
|
||||
fontStyle: '700',
|
||||
fixedWidth: width - 36
|
||||
});
|
||||
@@ -16810,10 +16872,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
await this.delay(180);
|
||||
await this.playCombatCutIn(result.counter);
|
||||
}
|
||||
this.showCombatMapResult(result);
|
||||
if (result.counter) {
|
||||
this.showCombatMapResult(result.counter);
|
||||
}
|
||||
await this.showCombatExchangeMapResults(result);
|
||||
await this.delay(result.counter ? 220 : 160);
|
||||
return this.formatEnemyCombatResult(result, plan.behavior);
|
||||
}
|
||||
@@ -19126,7 +19185,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
if (result.hit) {
|
||||
void this.playUnitActionFrames(defenderSprite, result.defender, defenderDirection, 'hurt', 70);
|
||||
this.cameras.main.shake(result.critical ? 130 : 90, result.critical ? 0.006 : 0.0035);
|
||||
this.cameras.main.shake(
|
||||
this.scaledBattleDuration(result.critical ? 130 : 90, 55),
|
||||
result.critical ? 0.006 : 0.0035
|
||||
);
|
||||
this.shakeCombatSprite(defenderSprite, defenderX, groundY, direction, result.critical);
|
||||
await this.delay(result.critical ? 130 : 92);
|
||||
} else {
|
||||
@@ -21815,18 +21877,14 @@ export class BattleScene extends Phaser.Scene {
|
||||
this.syncUnitMotion(unit, view, direction);
|
||||
}
|
||||
|
||||
private flashDamage(unit: UnitData, damage: number, critical = false, previousHp?: number, displayedHp = unit.hp) {
|
||||
private flashDamage(unit: UnitData, displayedHp = unit.hp) {
|
||||
const view = this.unitViews.get(unit.id);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = displayedHp <= 0 ? `${critical ? '치명 ' : ''}퇴각 -${damage}` : `${critical ? '치명 ' : '피해 '}-${damage}`;
|
||||
const detail = previousHp === undefined ? '' : `HP ${previousHp}→${displayedHp}`;
|
||||
this.showMapResultPopup(unit, [title, detail], critical ? '#ff8f5f' : '#ffdf7b', '#220909', critical ? 20 : 18, critical ? 34 : 30);
|
||||
|
||||
view.sprite.setTintFill(0xffe0a3);
|
||||
this.time.delayedCall(120, () => {
|
||||
this.time.delayedCall(this.scaledBattleDuration(120, 55), () => {
|
||||
if (displayedHp <= 0) {
|
||||
this.applyDefeatedStyle(unit);
|
||||
} else {
|
||||
@@ -21842,40 +21900,46 @@ export class BattleScene extends Phaser.Scene {
|
||||
});
|
||||
}
|
||||
|
||||
private flashMiss(unit: UnitData, label = '회피') {
|
||||
private flashMiss(unit: UnitData) {
|
||||
const view = this.unitViews.get(unit.id);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showMapResultPopup(unit, [label], '#d9f1ff', '#071623', label.length > 4 ? 17 : 19, 26);
|
||||
|
||||
this.tweens.add({
|
||||
targets: view.roleSeal ? [view.sprite, view.label, view.roleSeal] : [view.sprite, view.label],
|
||||
x: `+=${unit.faction === 'ally' ? -8 : 8}`,
|
||||
duration: 90,
|
||||
duration: this.scaledBattleDuration(90, 55),
|
||||
yoyo: true,
|
||||
ease: 'Sine.easeOut'
|
||||
});
|
||||
}
|
||||
|
||||
private flashSupport(unit: UnitData, label: string, color: string, detail = '') {
|
||||
private flashSupport(unit: UnitData) {
|
||||
const view = this.unitViews.get(unit.id);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showMapResultPopup(unit, [label, detail], color, '#071623', 18, 28);
|
||||
|
||||
this.tweens.add({
|
||||
targets: view.sprite,
|
||||
scale: view.sprite.scale * 1.08,
|
||||
duration: 120,
|
||||
duration: this.scaledBattleDuration(120, 55),
|
||||
yoyo: true,
|
||||
ease: 'Sine.easeOut'
|
||||
});
|
||||
}
|
||||
|
||||
private async showCombatExchangeMapResults(result: CombatResult) {
|
||||
this.showCombatMapResult(result);
|
||||
if (!result.counter) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.delay(110);
|
||||
this.showCombatMapResult(result.counter);
|
||||
}
|
||||
|
||||
private showCombatMapResult(result: CombatResult) {
|
||||
if (!result.hit) {
|
||||
this.showMapResultPopup(
|
||||
@@ -21966,17 +22030,13 @@ export class BattleScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
const visibleLines = lines.filter(Boolean);
|
||||
const widestLength = Math.max(...visibleLines.map((line) => line.length), 1);
|
||||
const estimatedWidth = Math.min(this.layout.mapWidth - 20, widestLength * fontSize * 0.78 + 16);
|
||||
const popupX = Phaser.Math.Clamp(
|
||||
view.sprite.x,
|
||||
this.layout.mapX + estimatedWidth / 2 + 6,
|
||||
this.layout.mapX + this.layout.mapWidth - estimatedWidth / 2 - 6
|
||||
);
|
||||
if (visibleLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const popup = this.add.text(
|
||||
popupX,
|
||||
view.sprite.y - this.layout.tileSize * 0.64,
|
||||
view.sprite.x,
|
||||
view.sprite.y,
|
||||
visibleLines.join('\n'),
|
||||
{
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
@@ -21986,19 +22046,60 @@ export class BattleScene extends Phaser.Scene {
|
||||
strokeThickness: 4,
|
||||
fontStyle: '700',
|
||||
align: 'center',
|
||||
lineSpacing: -3
|
||||
lineSpacing: -3,
|
||||
wordWrap: { width: Math.max(80, this.layout.mapWidth - 24), useAdvancedWrap: true }
|
||||
}
|
||||
);
|
||||
popup.setOrigin(0.5, 1);
|
||||
popup.setDepth(35);
|
||||
|
||||
const mapLeft = this.layout.mapX + 6;
|
||||
const mapRight = this.layout.mapX + this.layout.mapWidth - 6;
|
||||
const mapTop = this.layout.mapY + 6;
|
||||
const mapBottom = this.layout.mapY + this.layout.mapHeight - 6;
|
||||
const safeLift = Math.min(Math.max(0, lift), Math.max(0, this.layout.mapHeight - popup.height - 12));
|
||||
const minBottomY = mapTop + popup.height + safeLift;
|
||||
const popupX = Phaser.Math.Clamp(view.sprite.x, mapLeft + popup.width / 2, mapRight - popup.width / 2);
|
||||
const requestedBottomY = view.sprite.y - this.layout.tileSize * 0.64;
|
||||
const popupY = Phaser.Math.Clamp(requestedBottomY, minBottomY, mapBottom);
|
||||
popup.setPosition(popupX, popupY);
|
||||
|
||||
this.mapResultPopupObjects = this.mapResultPopupObjects.filter((object) => object.active);
|
||||
while (this.mapResultPopupObjects.length >= 4) {
|
||||
const oldest = this.mapResultPopupObjects.shift();
|
||||
if (oldest?.active) {
|
||||
this.tweens.killTweensOf(oldest);
|
||||
oldest.destroy();
|
||||
}
|
||||
}
|
||||
this.mapResultPopupObjects.push(popup);
|
||||
this.mapResultPopupPeakCount = Math.max(this.mapResultPopupPeakCount, this.mapResultPopupObjects.length);
|
||||
|
||||
const duration = this.scaledBattleDuration(680, 260);
|
||||
const delay = this.scaledBattleDuration(80, 30);
|
||||
const bounds = popup.getBounds();
|
||||
this.mapResultPopupLast = {
|
||||
unitId: unit.id,
|
||||
lines: [...visibleLines],
|
||||
bounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height },
|
||||
endBounds: { x: bounds.x, y: bounds.y - safeLift, width: bounds.width, height: bounds.height },
|
||||
duration,
|
||||
delay,
|
||||
lift: safeLift
|
||||
};
|
||||
this.tweens.add({
|
||||
targets: popup,
|
||||
y: popup.y - lift,
|
||||
y: popup.y - safeLift,
|
||||
alpha: 0,
|
||||
duration: 680,
|
||||
delay: 80,
|
||||
duration,
|
||||
delay,
|
||||
ease: 'Sine.easeOut',
|
||||
onComplete: () => popup.destroy()
|
||||
onComplete: () => {
|
||||
this.mapResultPopupObjects = this.mapResultPopupObjects.filter((object) => object !== popup && object.active);
|
||||
if (popup.active) {
|
||||
popup.destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24864,7 +24965,10 @@ export class BattleScene extends Phaser.Scene {
|
||||
};
|
||||
const deploymentPanel = this.deploymentPanelLayout
|
||||
? {
|
||||
changeCount: this.deploymentPanelLayout.changeCount,
|
||||
recommended: this.deploymentPanelLayout.recommended,
|
||||
headerBounds: { ...this.deploymentPanelLayout.headerBounds },
|
||||
statusBounds: { ...this.deploymentPanelLayout.statusBounds },
|
||||
noticeBounds: { ...this.deploymentPanelLayout.noticeBounds },
|
||||
noticeTextBounds: { ...this.deploymentPanelLayout.noticeTextBounds },
|
||||
roleBounds: this.deploymentPanelLayout.roleBounds.map((bounds) => ({ ...bounds })),
|
||||
@@ -25177,6 +25281,26 @@ export class BattleScene extends Phaser.Scene {
|
||||
: null,
|
||||
turnPromptVisible: this.turnPromptObjects.length > 0,
|
||||
turnPromptMode: this.turnPromptMode ?? null,
|
||||
mapResultFeedback: {
|
||||
activeCount: this.mapResultPopupObjects.filter((object) => object.active).length,
|
||||
peakCount: this.mapResultPopupPeakCount,
|
||||
maxActive: 4,
|
||||
speed: this.battleSpeed,
|
||||
mapBounds: {
|
||||
x: this.layout.mapX,
|
||||
y: this.layout.mapY,
|
||||
width: this.layout.mapWidth,
|
||||
height: this.layout.mapHeight
|
||||
},
|
||||
last: this.mapResultPopupLast
|
||||
? {
|
||||
...this.mapResultPopupLast,
|
||||
lines: [...this.mapResultPopupLast.lines],
|
||||
bounds: { ...this.mapResultPopupLast.bounds },
|
||||
endBounds: { ...this.mapResultPopupLast.endBounds }
|
||||
}
|
||||
: null
|
||||
},
|
||||
pointerFeedbackVisible: Boolean(this.pointerFeedbackMarker?.visible),
|
||||
pointerFeedbackTile: this.pointerFeedbackTile ? { ...this.pointerFeedbackTile } : null,
|
||||
bondChainReadyPreview: this.bondChainReadyDebugState(),
|
||||
|
||||
@@ -774,7 +774,7 @@ const campSupplies: CampSupplyDefinition[] = [
|
||||
label: '탁주',
|
||||
usableId: 'wine',
|
||||
title: '탁주',
|
||||
description: '선택 장수의 병력을 8 회복하고 연결된 공명 경험치를 2 올립니다.',
|
||||
description: '선택 장수의 병력을 8 회복하고 연결된 모든 공명 경험치를 각각 2 올립니다.',
|
||||
healHp: 8,
|
||||
bondExp: 2
|
||||
},
|
||||
@@ -789,6 +789,7 @@ const campSupplies: CampSupplyDefinition[] = [
|
||||
];
|
||||
|
||||
const campSupplyByLabel = new Map(campSupplies.map((supply) => [supply.label, supply]));
|
||||
const equipmentInventoryPageSize = 4;
|
||||
|
||||
const merchantItems: MerchantItemDefinition[] = [
|
||||
{
|
||||
@@ -11234,6 +11235,15 @@ export class CampScene extends Phaser.Scene {
|
||||
private activeTab: CampTab = 'status';
|
||||
private selectedDialogueId = campDialogues[0].id;
|
||||
private selectedVisitId = campVisits[0].id;
|
||||
private equipmentInventoryPage = 0;
|
||||
private equipmentPanelBackground?: Phaser.GameObjects.Rectangle;
|
||||
private equipmentInventoryPreviousButton?: Phaser.GameObjects.Rectangle;
|
||||
private equipmentInventoryNextButton?: Phaser.GameObjects.Rectangle;
|
||||
private equipmentInventoryVisibleItemIds: string[] = [];
|
||||
private suppliesPanelBackground?: Phaser.GameObjects.Rectangle;
|
||||
private suppliesEquipmentBoxes: Phaser.GameObjects.Rectangle[] = [];
|
||||
private suppliesTrophyBoxes: Phaser.GameObjects.Rectangle[] = [];
|
||||
private reportPanelBackground?: Phaser.GameObjects.Rectangle;
|
||||
private contentObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private dialogueObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
private sortieObjects: Phaser.GameObjects.GameObject[] = [];
|
||||
@@ -19409,6 +19419,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const x = 42;
|
||||
const y = 590;
|
||||
const bg = this.track(this.add.rectangle(x, y, 1180, 84, 0x101820, 0.86));
|
||||
this.reportPanelBackground = bg;
|
||||
bg.setOrigin(0);
|
||||
bg.setStrokeStyle(1, palette.gold, 0.48);
|
||||
this.track(this.add.text(x + 18, y + 14, '전투 보고', this.textStyle(18, '#f2e3bf', true)));
|
||||
@@ -21470,6 +21481,9 @@ export class CampScene extends Phaser.Scene {
|
||||
const width = 828;
|
||||
const height = 464;
|
||||
const bg = this.track(this.add.rectangle(x, y, width, height, 0x101820, 0.91));
|
||||
this.equipmentPanelBackground = bg;
|
||||
this.equipmentInventoryPreviousButton = undefined;
|
||||
this.equipmentInventoryNextButton = undefined;
|
||||
bg.setOrigin(0);
|
||||
bg.setStrokeStyle(1, palette.gold, 0.58);
|
||||
|
||||
@@ -21483,7 +21497,39 @@ export class CampScene extends Phaser.Scene {
|
||||
});
|
||||
|
||||
const inventoryEntries = this.equipmentInventoryEntries();
|
||||
this.track(this.add.text(x + 24, y + 266, `보유 장비 ${inventoryEntries.length}`, this.textStyle(18, '#f2e3bf', true)));
|
||||
const inventoryPageCount = Math.max(1, Math.ceil(inventoryEntries.length / equipmentInventoryPageSize));
|
||||
this.equipmentInventoryPage = Phaser.Math.Clamp(this.equipmentInventoryPage, 0, inventoryPageCount - 1);
|
||||
const inventoryStart = this.equipmentInventoryPage * equipmentInventoryPageSize;
|
||||
const visibleInventoryEntries = inventoryEntries.slice(inventoryStart, inventoryStart + equipmentInventoryPageSize);
|
||||
this.equipmentInventoryVisibleItemIds = visibleInventoryEntries.map((entry) => entry.item.id);
|
||||
this.track(this.add.text(
|
||||
x + 24,
|
||||
y + 266,
|
||||
`보유 장비 ${inventoryEntries.length}${inventoryPageCount > 1 ? ` · ${this.equipmentInventoryPage + 1}/${inventoryPageCount}` : ''}`,
|
||||
this.textStyle(18, '#f2e3bf', true)
|
||||
));
|
||||
if (inventoryPageCount > 1) {
|
||||
this.equipmentInventoryPreviousButton = this.renderEquipmentInventoryPageButton(
|
||||
x + 424,
|
||||
y + 278,
|
||||
'‹',
|
||||
this.equipmentInventoryPage > 0,
|
||||
() => {
|
||||
this.equipmentInventoryPage -= 1;
|
||||
this.render();
|
||||
}
|
||||
);
|
||||
this.equipmentInventoryNextButton = this.renderEquipmentInventoryPageButton(
|
||||
x + 462,
|
||||
y + 278,
|
||||
'›',
|
||||
this.equipmentInventoryPage < inventoryPageCount - 1,
|
||||
() => {
|
||||
this.equipmentInventoryPage += 1;
|
||||
this.render();
|
||||
}
|
||||
);
|
||||
}
|
||||
this.track(this.add.text(x + 516, y + 268, '장비 효과 요약', this.textStyle(17, '#f2e3bf', true)));
|
||||
|
||||
if (inventoryEntries.length === 0) {
|
||||
@@ -21493,7 +21539,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.track(this.add.text(x + 44, y + 324, '교체 가능한 보유 장비가 없습니다.', this.textStyle(15, '#9fb0bf', true)));
|
||||
this.track(this.add.text(x + 44, y + 352, '전투 보상이나 군영 상인을 통해 장비를 확보하면 이곳에서 비교할 수 있습니다.', this.textStyle(12, '#77818c')));
|
||||
} else {
|
||||
inventoryEntries.slice(0, 4).forEach((entry, index) => {
|
||||
visibleInventoryEntries.forEach((entry, index) => {
|
||||
this.renderEquipmentCompareRow(entry, unit, x + 24, y + 298 + index * 40, 468, 36);
|
||||
});
|
||||
}
|
||||
@@ -21502,6 +21548,31 @@ export class CampScene extends Phaser.Scene {
|
||||
this.renderEquipmentInventorySummary(inventoryEntries, x + 516, y + 432, 280);
|
||||
}
|
||||
|
||||
private renderEquipmentInventoryPageButton(
|
||||
x: number,
|
||||
y: number,
|
||||
label: string,
|
||||
enabled: boolean,
|
||||
action: () => void
|
||||
) {
|
||||
const bg = this.track(this.add.rectangle(x, y, 30, 24, enabled ? 0x1a2630 : 0x111820, enabled ? 0.96 : 0.68));
|
||||
bg.setStrokeStyle(1, enabled ? palette.gold : 0x53606c, enabled ? 0.72 : 0.34);
|
||||
const text = this.track(this.add.text(x, y - 1, label, this.textStyle(18, enabled ? '#f2e3bf' : '#6f7882', true)));
|
||||
text.setOrigin(0.5);
|
||||
if (!enabled) {
|
||||
return bg;
|
||||
}
|
||||
|
||||
const setHovered = (hovered: boolean) => bg.setFillStyle(hovered ? 0x654c25 : 0x1a2630, hovered ? 0.99 : 0.96);
|
||||
[bg, text].forEach((target) => {
|
||||
target.setInteractive({ useHandCursor: true });
|
||||
target.on('pointerover', () => setHovered(true));
|
||||
target.on('pointerout', () => setHovered(false));
|
||||
target.on('pointerdown', action);
|
||||
});
|
||||
return bg;
|
||||
}
|
||||
|
||||
private renderEquipmentUnitSummary(unit: UnitData, x: number, y: number, width: number, height: number) {
|
||||
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
||||
bg.setOrigin(0);
|
||||
@@ -21611,7 +21682,10 @@ export class CampScene extends Phaser.Scene {
|
||||
private renderSuppliesPanel() {
|
||||
const x = 394;
|
||||
const y = 120;
|
||||
const bg = this.track(this.add.rectangle(x, y, 828, 444, 0x101820, 0.9));
|
||||
const bg = this.track(this.add.rectangle(x, y, 828, 466, 0x101820, 0.9));
|
||||
this.suppliesPanelBackground = bg;
|
||||
this.suppliesEquipmentBoxes = [];
|
||||
this.suppliesTrophyBoxes = [];
|
||||
bg.setOrigin(0);
|
||||
bg.setStrokeStyle(1, palette.blue, 0.56);
|
||||
this.track(this.add.text(x + 24, y + 22, '정비와 보급', this.textStyle(24, '#f2e3bf', true)));
|
||||
@@ -21628,23 +21702,23 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
this.renderMerchantPanel(x + 24, y + 202, 342);
|
||||
|
||||
this.track(this.add.text(x + 390, y + 318, '보유 장비', this.textStyle(18, '#f2e3bf', true)));
|
||||
this.track(this.add.text(x + 390, y + 312, '보유 장비', this.textStyle(18, '#f2e3bf', true)));
|
||||
const equipmentEntries = this.equipmentInventoryEntries();
|
||||
if (equipmentEntries.length === 0) {
|
||||
this.track(this.add.text(x + 390, y + 346, '교체 가능한 장비 없음', this.textStyle(12, '#9fb0bf')));
|
||||
} else {
|
||||
equipmentEntries.slice(0, 3).forEach((entry, index) => {
|
||||
this.renderEquipmentInventoryBox(entry, x + 390 + index * 128, y + 344);
|
||||
this.suppliesEquipmentBoxes.push(this.renderEquipmentInventoryBox(entry, x + 390 + index * 128, y + 336));
|
||||
});
|
||||
}
|
||||
|
||||
this.track(this.add.text(x + 390, y + 410, '전리품과 명성', this.textStyle(17, '#f2e3bf', true)));
|
||||
this.track(this.add.text(x + 390, y + 394, '전리품과 명성', this.textStyle(17, '#f2e3bf', true)));
|
||||
const trophies = this.nonEquipmentInventoryLabels();
|
||||
if (trophies.length === 0) {
|
||||
this.track(this.add.text(x + 390, y + 438, '장부/명성 전리품 없음', this.textStyle(12, '#9fb0bf')));
|
||||
} else {
|
||||
trophies.slice(0, 3).forEach((reward, index) => {
|
||||
this.renderSupplyBox(reward, x + 390 + index * 128, y + 436);
|
||||
this.suppliesTrophyBoxes.push(this.renderSupplyBox(reward, x + 390 + index * 128, y + 418));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21708,6 +21782,7 @@ export class CampScene extends Phaser.Scene {
|
||||
bg.setOrigin(0);
|
||||
bg.setStrokeStyle(1, palette.gold, 0.52);
|
||||
this.track(this.add.text(x + 10, y + 14, this.compactText(label, 9), this.textStyle(13, '#f2e3bf', true)));
|
||||
return bg;
|
||||
}
|
||||
|
||||
private renderEquipmentInventoryBox(entry: { label: string; amount: number; item: ItemDefinition }, x: number, y: number) {
|
||||
@@ -21734,6 +21809,7 @@ export class CampScene extends Phaser.Scene {
|
||||
actionText.setInteractive({ useHandCursor: true });
|
||||
actionText.on('pointerdown', action);
|
||||
}
|
||||
return bg;
|
||||
}
|
||||
|
||||
private renderSupplyTargetCard(unit: UnitData, x: number, y: number, width: number, height: number) {
|
||||
@@ -21823,7 +21899,7 @@ export class CampScene extends Phaser.Scene {
|
||||
soundDirector.playEffect('exp-gain', { volume: 0.24, stopAfterMs: 620 });
|
||||
|
||||
const hpText = hpGain > 0 ? `병력 +${hpGain}` : '';
|
||||
const bondText = bondGain > 0 ? `공명 +${supply.bondExp}` : '';
|
||||
const bondText = bondGain > 0 ? `연결 공명 총 +${bondGain}` : '';
|
||||
this.showCampNotice(`${target.name} ${supply.title} 사용 · ${[hpText, bondText].filter(Boolean).join(' / ')}`);
|
||||
this.render();
|
||||
}
|
||||
@@ -22052,7 +22128,8 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private showCampNotice(message: string) {
|
||||
this.dialogueObjects.forEach((object) => object.destroy());
|
||||
this.tweens.killTweensOf(this.dialogueObjects);
|
||||
this.dialogueObjects.forEach((object) => object.active && object.destroy());
|
||||
this.dialogueObjects = [];
|
||||
const depth = this.sortieObjects.length > 0 ? 72 : 30;
|
||||
const noticeWidth = Math.min(640, Math.max(360, message.length * 13));
|
||||
@@ -22066,15 +22143,18 @@ export class CampScene extends Phaser.Scene {
|
||||
}));
|
||||
text.setOrigin(0.5);
|
||||
text.setDepth(depth + 1);
|
||||
this.dialogueObjects.push(box, text);
|
||||
const noticeObjects: Phaser.GameObjects.GameObject[] = [box, text];
|
||||
this.dialogueObjects = noticeObjects;
|
||||
this.tweens.add({
|
||||
targets: this.dialogueObjects,
|
||||
targets: noticeObjects,
|
||||
alpha: 0,
|
||||
delay: 1300,
|
||||
duration: 320,
|
||||
onComplete: () => {
|
||||
this.dialogueObjects.forEach((object) => object.destroy());
|
||||
this.dialogueObjects = [];
|
||||
noticeObjects.forEach((object) => object.active && object.destroy());
|
||||
if (this.dialogueObjects === noticeObjects) {
|
||||
this.dialogueObjects = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -22610,6 +22690,14 @@ export class CampScene extends Phaser.Scene {
|
||||
this.contentObjects.forEach((object) => object.destroy());
|
||||
this.contentObjects = [];
|
||||
this.campRosterLayout = undefined;
|
||||
this.equipmentPanelBackground = undefined;
|
||||
this.equipmentInventoryPreviousButton = undefined;
|
||||
this.equipmentInventoryNextButton = undefined;
|
||||
this.equipmentInventoryVisibleItemIds = [];
|
||||
this.suppliesPanelBackground = undefined;
|
||||
this.suppliesEquipmentBoxes = [];
|
||||
this.suppliesTrophyBoxes = [];
|
||||
this.reportPanelBackground = undefined;
|
||||
this.reportFormationReviewToggleButton = undefined;
|
||||
this.reportFormationHistoryToggleButton = undefined;
|
||||
this.reportSortieOrderRewardBadge = undefined;
|
||||
@@ -22728,6 +22816,8 @@ export class CampScene extends Phaser.Scene {
|
||||
1,
|
||||
Math.ceil(reportFormationHistoryRecords.length / reportFormationHistoryPageSize)
|
||||
);
|
||||
const equipmentInventory = this.equipmentInventoryEntries();
|
||||
const equipmentInventoryPageCount = Math.max(1, Math.ceil(equipmentInventory.length / equipmentInventoryPageSize));
|
||||
return {
|
||||
scene: this.scene.key,
|
||||
activeTab: this.activeTab,
|
||||
@@ -23422,7 +23512,35 @@ export class CampScene extends Phaser.Scene {
|
||||
strategyLine: summary.strategyLine
|
||||
};
|
||||
}),
|
||||
equipmentInventory: this.equipmentInventoryEntries().map((entry) => ({
|
||||
secondaryPanels: {
|
||||
report: {
|
||||
visible: Boolean(this.reportPanelBackground?.active),
|
||||
bounds: this.sortieObjectBoundsDebug(this.reportPanelBackground)
|
||||
},
|
||||
supplies: {
|
||||
visible: this.activeTab === 'supplies' && Boolean(this.suppliesPanelBackground?.active),
|
||||
bounds: this.sortieObjectBoundsDebug(this.suppliesPanelBackground),
|
||||
equipmentBoxBounds: this.suppliesEquipmentBoxes
|
||||
.map((box) => this.sortieObjectBoundsDebug(box))
|
||||
.filter((bounds) => bounds !== null),
|
||||
trophyBoxBounds: this.suppliesTrophyBoxes
|
||||
.map((box) => this.sortieObjectBoundsDebug(box))
|
||||
.filter((bounds) => bounds !== null)
|
||||
},
|
||||
equipment: {
|
||||
visible: this.activeTab === 'equipment' && Boolean(this.equipmentPanelBackground?.active),
|
||||
bounds: this.sortieObjectBoundsDebug(this.equipmentPanelBackground),
|
||||
page: this.equipmentInventoryPage,
|
||||
pageCount: equipmentInventoryPageCount,
|
||||
pageSize: equipmentInventoryPageSize,
|
||||
visibleItemIds: [...this.equipmentInventoryVisibleItemIds],
|
||||
previousButtonBounds: this.sortieObjectBoundsDebug(this.equipmentInventoryPreviousButton),
|
||||
nextButtonBounds: this.sortieObjectBoundsDebug(this.equipmentInventoryNextButton),
|
||||
previousEnabled: Boolean(this.equipmentInventoryPreviousButton?.input?.enabled),
|
||||
nextEnabled: Boolean(this.equipmentInventoryNextButton?.input?.enabled)
|
||||
}
|
||||
},
|
||||
equipmentInventory: equipmentInventory.map((entry) => ({
|
||||
label: entry.label,
|
||||
amount: entry.amount,
|
||||
itemId: entry.item.id,
|
||||
|
||||
@@ -123,7 +123,16 @@ export class StoryScene extends Phaser.Scene {
|
||||
? { x: progressPanelBounds.x, y: progressPanelBounds.y, width: progressPanelBounds.width, height: progressPanelBounds.height }
|
||||
: null,
|
||||
isLastPage,
|
||||
advanceHint: isLastPage ? 'continue' : 'next',
|
||||
advanceHint:
|
||||
isLastPage && this.nextScene === 'BattleScene'
|
||||
? 'battle-deployment'
|
||||
: isLastPage && this.nextScene === 'CampScene'
|
||||
? 'camp-return'
|
||||
: isLastPage
|
||||
? 'continue'
|
||||
: 'next',
|
||||
advanceLabel: this.progressActionLabel(isLastPage),
|
||||
nextScene: this.nextScene,
|
||||
cutsceneKind: page?.cutscene?.kind ?? null,
|
||||
cutsceneActors: page?.cutscene?.actors?.map((actor) => actor.unitId) ?? []
|
||||
};
|
||||
@@ -312,9 +321,9 @@ export class StoryScene extends Phaser.Scene {
|
||||
});
|
||||
this.bodyText.setDepth(dialogDepth + 2);
|
||||
|
||||
const progressX = width - ui(174);
|
||||
const progressX = width - ui(214);
|
||||
const progressY = panelY + panelH + ui(20);
|
||||
this.progressBackground = this.add.rectangle(progressX, progressY, ui(300), ui(34), cutsceneUiColors.ink, 0.82);
|
||||
this.progressBackground = this.add.rectangle(progressX, progressY, ui(380), ui(34), cutsceneUiColors.ink, 0.82);
|
||||
this.progressBackground.setStrokeStyle(ui(1), palette.gold, 0.42);
|
||||
this.progressBackground.setDepth(dialogDepth + 1);
|
||||
|
||||
@@ -324,7 +333,7 @@ export class StoryScene extends Phaser.Scene {
|
||||
color: '#d8b15f',
|
||||
fontStyle: '700',
|
||||
align: 'center',
|
||||
fixedWidth: ui(276)
|
||||
fixedWidth: ui(356)
|
||||
});
|
||||
this.progressText.setOrigin(0.5);
|
||||
this.progressText.setDepth(dialogDepth + 2);
|
||||
@@ -404,10 +413,23 @@ export class StoryScene extends Phaser.Scene {
|
||||
this.bodyText?.setText(page.text);
|
||||
const isLastPage = this.pageIndex >= this.pages.length - 1;
|
||||
this.progressText?.setText(
|
||||
`${this.pageIndex + 1} / ${this.pages.length} · 클릭 / 스페이스·엔터 · ${isLastPage ? '계속' : '다음'}`
|
||||
`${this.pageIndex + 1} / ${this.pages.length} · 클릭 / 스페이스·엔터 · ${this.progressActionLabel(isLastPage)}`
|
||||
);
|
||||
}
|
||||
|
||||
private progressActionLabel(isLastPage = this.pageIndex >= this.pages.length - 1) {
|
||||
if (!isLastPage) {
|
||||
return '다음';
|
||||
}
|
||||
if (this.nextScene === 'BattleScene') {
|
||||
return '전투 배치';
|
||||
}
|
||||
if (this.nextScene === 'CampScene') {
|
||||
return '군영으로';
|
||||
}
|
||||
return '계속';
|
||||
}
|
||||
|
||||
private pagePortraitKey(page: StoryPage) {
|
||||
return page.portrait ?? portraitKeyForSpeaker(page.speaker);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user