feat: enrich campaign reward and roster visuals
This commit is contained in:
257
src/game/data/sortieRosterHistory.ts
Normal file
257
src/game/data/sortieRosterHistory.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
export type SortieHistoryRecruitLike = string | {
|
||||
unitId?: string;
|
||||
};
|
||||
|
||||
export type SortieHistoryBattleLike = {
|
||||
battleId?: string;
|
||||
completedAt?: string;
|
||||
sortiePerformance?: {
|
||||
selectedUnitIds?: readonly string[];
|
||||
};
|
||||
campaignRewards?: {
|
||||
recruits?: readonly SortieHistoryRecruitLike[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SortieBattleHistoryLike =
|
||||
| Readonly<Record<string, SortieHistoryBattleLike>>
|
||||
| readonly SortieHistoryBattleLike[];
|
||||
|
||||
export type SortieRosterHistoryBadgeKind = 'recent-recruit' | 'first-sortie' | 'long-wait' | 'active';
|
||||
|
||||
export type SortieRosterHistoryBadge = {
|
||||
kind: SortieRosterHistoryBadgeKind;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type SortieRosterUnitHistory = {
|
||||
unitId: string;
|
||||
sortieCount: number;
|
||||
firstSortiePending: boolean;
|
||||
battlesSinceLastSortie: number | null;
|
||||
recentRecruit: boolean;
|
||||
battlesSinceRecruit: number | null;
|
||||
recruitBattleId?: string;
|
||||
lastSortieBattleId?: string;
|
||||
badge: SortieRosterHistoryBadge;
|
||||
summary: string;
|
||||
};
|
||||
|
||||
export type SortieRosterHistoryOptions = {
|
||||
recentRecruitBattleWindow?: number;
|
||||
longWaitBattleThreshold?: number;
|
||||
};
|
||||
|
||||
export const defaultRecentRecruitBattleWindow = 1;
|
||||
export const defaultLongWaitBattleThreshold = 3;
|
||||
|
||||
type NormalizedHistoryBattle = {
|
||||
battleId?: string;
|
||||
completedAtMs?: number;
|
||||
selectedUnitIds: string[];
|
||||
recruitUnitIds: string[];
|
||||
sourceIndex: number;
|
||||
};
|
||||
|
||||
type UnitHistoryAccumulator = {
|
||||
sortieCount: number;
|
||||
lastSortieIndex?: number;
|
||||
recruitBattleIndex?: number;
|
||||
};
|
||||
|
||||
export function deriveSortieRosterHistory(
|
||||
unitIds: readonly string[],
|
||||
battleHistory: SortieBattleHistoryLike,
|
||||
options: SortieRosterHistoryOptions = {}
|
||||
): SortieRosterUnitHistory[] {
|
||||
const normalizedUnitIds = uniqueIds(unitIds);
|
||||
const battles = normalizeBattleHistory(battleHistory);
|
||||
const recentRecruitBattleWindow = nonNegativeInteger(
|
||||
options.recentRecruitBattleWindow,
|
||||
defaultRecentRecruitBattleWindow
|
||||
);
|
||||
const longWaitBattleThreshold = positiveInteger(
|
||||
options.longWaitBattleThreshold,
|
||||
defaultLongWaitBattleThreshold
|
||||
);
|
||||
const accumulatorByUnitId = new Map<string, UnitHistoryAccumulator>(
|
||||
normalizedUnitIds.map((unitId) => [unitId, { sortieCount: 0 }])
|
||||
);
|
||||
|
||||
battles.forEach((battle, battleIndex) => {
|
||||
battle.selectedUnitIds.forEach((unitId) => {
|
||||
const accumulator = accumulatorByUnitId.get(unitId);
|
||||
if (!accumulator) {
|
||||
return;
|
||||
}
|
||||
accumulator.sortieCount += 1;
|
||||
accumulator.lastSortieIndex = battleIndex;
|
||||
});
|
||||
battle.recruitUnitIds.forEach((unitId) => {
|
||||
const accumulator = accumulatorByUnitId.get(unitId);
|
||||
if (accumulator && accumulator.recruitBattleIndex === undefined) {
|
||||
accumulator.recruitBattleIndex = battleIndex;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return normalizedUnitIds.map((unitId) => {
|
||||
const accumulator = accumulatorByUnitId.get(unitId) ?? { sortieCount: 0 };
|
||||
const firstSortiePending = accumulator.sortieCount === 0;
|
||||
const battlesSinceLastSortie = accumulator.lastSortieIndex === undefined
|
||||
? null
|
||||
: battles.length - accumulator.lastSortieIndex - 1;
|
||||
const battlesSinceRecruit = accumulator.recruitBattleIndex === undefined
|
||||
? null
|
||||
: battles.length - accumulator.recruitBattleIndex - 1;
|
||||
const recentRecruit = battlesSinceRecruit !== null && battlesSinceRecruit <= recentRecruitBattleWindow;
|
||||
const badge = createHistoryBadge({
|
||||
sortieCount: accumulator.sortieCount,
|
||||
firstSortiePending,
|
||||
battlesSinceLastSortie,
|
||||
recentRecruit,
|
||||
longWaitBattleThreshold
|
||||
});
|
||||
|
||||
return {
|
||||
unitId,
|
||||
sortieCount: accumulator.sortieCount,
|
||||
firstSortiePending,
|
||||
battlesSinceLastSortie,
|
||||
recentRecruit,
|
||||
battlesSinceRecruit,
|
||||
...(accumulator.recruitBattleIndex === undefined
|
||||
? {}
|
||||
: { recruitBattleId: battles[accumulator.recruitBattleIndex]?.battleId }),
|
||||
...(accumulator.lastSortieIndex === undefined
|
||||
? {}
|
||||
: { lastSortieBattleId: battles[accumulator.lastSortieIndex]?.battleId }),
|
||||
badge,
|
||||
summary: createHistorySummary({
|
||||
sortieCount: accumulator.sortieCount,
|
||||
firstSortiePending,
|
||||
battlesSinceLastSortie,
|
||||
recentRecruit,
|
||||
battlesSinceRecruit
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function deriveSortieRosterHistoryByUnitId(
|
||||
unitIds: readonly string[],
|
||||
battleHistory: SortieBattleHistoryLike,
|
||||
options: SortieRosterHistoryOptions = {}
|
||||
) {
|
||||
return new Map(
|
||||
deriveSortieRosterHistory(unitIds, battleHistory, options)
|
||||
.map((history) => [history.unitId, history] as const)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeBattleHistory(battleHistory: SortieBattleHistoryLike): NormalizedHistoryBattle[] {
|
||||
const entries = Array.isArray(battleHistory)
|
||||
? battleHistory.map((battle, sourceIndex) => ({ battle, sourceIndex }))
|
||||
: Object.entries(battleHistory).map(([battleId, battle], sourceIndex) => ({
|
||||
battle: { ...battle, battleId: normalizedId(battle.battleId) ?? normalizedId(battleId) },
|
||||
sourceIndex
|
||||
}));
|
||||
const normalized = entries.map(({ battle, sourceIndex }) => ({
|
||||
battleId: normalizedId(battle.battleId),
|
||||
completedAtMs: validTimestamp(battle.completedAt),
|
||||
selectedUnitIds: uniqueIds(battle.sortiePerformance?.selectedUnitIds ?? []),
|
||||
recruitUnitIds: uniqueIds((battle.campaignRewards?.recruits ?? []).map(recruitUnitId)),
|
||||
sourceIndex
|
||||
}));
|
||||
|
||||
if (normalized.length > 1 && normalized.every((battle) => battle.completedAtMs !== undefined)) {
|
||||
normalized.sort((left, right) =>
|
||||
(left.completedAtMs ?? 0) - (right.completedAtMs ?? 0) || left.sourceIndex - right.sourceIndex
|
||||
);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function createHistoryBadge(context: {
|
||||
sortieCount: number;
|
||||
firstSortiePending: boolean;
|
||||
battlesSinceLastSortie: number | null;
|
||||
recentRecruit: boolean;
|
||||
longWaitBattleThreshold: number;
|
||||
}): SortieRosterHistoryBadge {
|
||||
if (context.recentRecruit) {
|
||||
return { kind: 'recent-recruit', label: 'NEW' };
|
||||
}
|
||||
if (context.firstSortiePending) {
|
||||
return { kind: 'first-sortie', label: '미출전' };
|
||||
}
|
||||
if (
|
||||
context.battlesSinceLastSortie !== null &&
|
||||
context.battlesSinceLastSortie >= context.longWaitBattleThreshold
|
||||
) {
|
||||
return { kind: 'long-wait', label: `대기 ${context.battlesSinceLastSortie}전` };
|
||||
}
|
||||
return { kind: 'active', label: `출전 ${context.sortieCount}회` };
|
||||
}
|
||||
|
||||
function createHistorySummary(context: {
|
||||
sortieCount: number;
|
||||
firstSortiePending: boolean;
|
||||
battlesSinceLastSortie: number | null;
|
||||
recentRecruit: boolean;
|
||||
battlesSinceRecruit: number | null;
|
||||
}) {
|
||||
if (context.recentRecruit && context.firstSortiePending) {
|
||||
return '최근 합류 · 첫 출전 대기';
|
||||
}
|
||||
if (context.firstSortiePending) {
|
||||
return context.battlesSinceRecruit === null
|
||||
? '출전 기록 없음 · 첫 출전 대기'
|
||||
: `합류 후 ${context.battlesSinceRecruit}전 경과 · 첫 출전 대기`;
|
||||
}
|
||||
|
||||
const lastSortie = context.battlesSinceLastSortie === 0
|
||||
? '직전 전투 출전'
|
||||
: `${context.battlesSinceLastSortie}전 전 마지막 출전`;
|
||||
return `${context.recentRecruit ? '최근 합류 · ' : ''}출전 ${context.sortieCount}회 · ${lastSortie}`;
|
||||
}
|
||||
|
||||
function recruitUnitId(recruit: SortieHistoryRecruitLike) {
|
||||
return typeof recruit === 'string' ? recruit : recruit?.unitId;
|
||||
}
|
||||
|
||||
function uniqueIds(values: readonly unknown[]) {
|
||||
const seen = new Set<string>();
|
||||
return values.flatMap((value) => {
|
||||
const id = normalizedId(value);
|
||||
if (!id || seen.has(id)) {
|
||||
return [];
|
||||
}
|
||||
seen.add(id);
|
||||
return [id];
|
||||
});
|
||||
}
|
||||
|
||||
function normalizedId(value: unknown) {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = value.trim();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function validTimestamp(value: unknown) {
|
||||
if (typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
const timestamp = Date.parse(value);
|
||||
return Number.isFinite(timestamp) ? timestamp : undefined;
|
||||
}
|
||||
|
||||
function nonNegativeInteger(value: number | undefined, fallback: number) {
|
||||
return Number.isFinite(value) ? Math.max(0, Math.floor(value ?? fallback)) : fallback;
|
||||
}
|
||||
|
||||
function positiveInteger(value: number | undefined, fallback: number) {
|
||||
return Number.isFinite(value) ? Math.max(1, Math.floor(value ?? fallback)) : fallback;
|
||||
}
|
||||
@@ -100,6 +100,90 @@ export function storyBackgroundKeyForPage(baseKey: string, pageId: string) {
|
||||
return keys[stableStoryHash(pageId) % keys.length];
|
||||
}
|
||||
|
||||
type StoryBackgroundPageReference = Readonly<{
|
||||
id: string;
|
||||
background: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Select backgrounds in page order while preserving stable per-page defaults.
|
||||
* The planner minimizes physical-image repeats across the whole sequence
|
||||
* first, then minimizes deviations from each page's hash-selected preference.
|
||||
* Explicit assignments remain authoritative.
|
||||
*/
|
||||
export function storyBackgroundKeysForPages(pages: readonly StoryBackgroundPageReference[]) {
|
||||
let plans: StoryBackgroundSequencePlan[] = [];
|
||||
|
||||
pages.forEach((page, pageIndex) => {
|
||||
const options = storyBackgroundOptionsForPage(page);
|
||||
plans = options.map((key, preferenceRank) => {
|
||||
if (pageIndex === 0) {
|
||||
return { keys: [key], repeats: 0, preferenceCost: preferenceRank };
|
||||
}
|
||||
|
||||
let bestPlan: StoryBackgroundSequencePlan | undefined;
|
||||
plans.forEach((previousPlan) => {
|
||||
const previousKey = previousPlan.keys[previousPlan.keys.length - 1];
|
||||
const repeats = previousPlan.repeats + (storyBackgroundUrl(previousKey) === storyBackgroundUrl(key) ? 1 : 0);
|
||||
const candidatePlan = {
|
||||
keys: [...previousPlan.keys, key],
|
||||
repeats,
|
||||
preferenceCost: previousPlan.preferenceCost + preferenceRank
|
||||
};
|
||||
if (!bestPlan || storyBackgroundPlanIsBetter(candidatePlan, bestPlan)) {
|
||||
bestPlan = candidatePlan;
|
||||
}
|
||||
});
|
||||
return bestPlan ?? { keys: [key], repeats: 0, preferenceCost: preferenceRank };
|
||||
});
|
||||
});
|
||||
|
||||
return plans.reduce<StoryBackgroundSequencePlan | undefined>(
|
||||
(bestPlan, candidatePlan) =>
|
||||
!bestPlan || storyBackgroundPlanIsBetter(candidatePlan, bestPlan) ? candidatePlan : bestPlan,
|
||||
undefined
|
||||
)?.keys ?? [];
|
||||
}
|
||||
|
||||
export function storyBackgroundPageHasFixedAssignment(pageId: string) {
|
||||
return Object.prototype.hasOwnProperty.call(storyBackgroundPageAssignments, pageId);
|
||||
}
|
||||
|
||||
type StoryBackgroundSequencePlan = {
|
||||
keys: string[];
|
||||
repeats: number;
|
||||
preferenceCost: number;
|
||||
};
|
||||
|
||||
function storyBackgroundOptionsForPage(page: StoryBackgroundPageReference) {
|
||||
const keys = storyBackgroundKeysFor(page.background);
|
||||
if (keys.length === 0) {
|
||||
return [page.background];
|
||||
}
|
||||
|
||||
const fixedKey = storyBackgroundPageAssignments[page.id];
|
||||
if (fixedKey && keys.includes(fixedKey)) {
|
||||
return [fixedKey];
|
||||
}
|
||||
|
||||
const preferredKey = storyBackgroundKeyForPage(page.background, page.id);
|
||||
const preferredIndex = Math.max(0, keys.indexOf(preferredKey));
|
||||
return [
|
||||
preferredKey,
|
||||
...keys.slice(preferredIndex + 1),
|
||||
...keys.slice(0, preferredIndex)
|
||||
].filter((key, index, options) => options.indexOf(key) === index);
|
||||
}
|
||||
|
||||
function storyBackgroundPlanIsBetter(candidate: StoryBackgroundSequencePlan, current: StoryBackgroundSequencePlan) {
|
||||
return candidate.repeats < current.repeats ||
|
||||
(candidate.repeats === current.repeats && candidate.preferenceCost < current.preferenceCost);
|
||||
}
|
||||
|
||||
function storyBackgroundUrl(key: string) {
|
||||
return storyBackgroundAssets[key] ?? key;
|
||||
}
|
||||
|
||||
function stableStoryHash(value: string) {
|
||||
let hash = 0;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
|
||||
@@ -51,6 +51,10 @@ import {
|
||||
type SortieSynergyComparison,
|
||||
type SortieSynergySnapshot
|
||||
} from '../data/sortieSynergy';
|
||||
import {
|
||||
deriveSortieRosterHistoryByUnitId,
|
||||
type SortieRosterUnitHistory
|
||||
} from '../data/sortieRosterHistory';
|
||||
import {
|
||||
evaluateSortiePerformanceReview,
|
||||
sortiePerformanceGradeForScore,
|
||||
@@ -255,11 +259,22 @@ type VictoryRewardCardId = 'gold' | 'equipment' | 'supplies' | 'reputation' | 'r
|
||||
|
||||
type VictoryRewardActionId = 'equipment' | 'supplies' | 'sortie' | 'close';
|
||||
|
||||
type VictoryRewardIcon =
|
||||
| { kind: 'mark'; mark: string }
|
||||
| { kind: 'battle-ui'; iconKey: BattleUiIconKey }
|
||||
| { kind: 'item'; itemId: string }
|
||||
| { kind: 'portrait'; unitId: string };
|
||||
|
||||
type VictoryRewardCardView = {
|
||||
id: VictoryRewardCardId;
|
||||
label: string;
|
||||
items: string[];
|
||||
background: Phaser.GameObjects.Rectangle;
|
||||
iconKind: VictoryRewardIcon['kind'];
|
||||
iconKey: string;
|
||||
iconReady: boolean;
|
||||
isNew: boolean;
|
||||
actionId?: VictoryRewardActionId;
|
||||
};
|
||||
|
||||
type VictoryRewardActionView = {
|
||||
@@ -386,6 +401,7 @@ type SortieFocusedUnitSummary = SortieUnitTacticalSummary & {
|
||||
reserveTrainingBondPreviewLine: string;
|
||||
reserveTrainingBondPartnerCount: number;
|
||||
reserveTrainingPlanLine: string;
|
||||
serviceHistory: SortieRosterUnitHistory;
|
||||
terrainScore: number;
|
||||
terrainGrade: string;
|
||||
equipment: {
|
||||
@@ -13045,7 +13061,21 @@ export class CampScene extends Phaser.Scene {
|
||||
);
|
||||
subtitle.setDepth(depth + 2);
|
||||
|
||||
const cardDefinitions: Array<{ id: VictoryRewardCardId; label: string; items: string[] }> = [
|
||||
const equipmentReward = rewards?.equipment[0];
|
||||
const equipmentItem = equipmentReward ? this.itemForRewardLabel(equipmentReward) : undefined;
|
||||
const supplyReward = supplyItems[0];
|
||||
const supply = supplyReward ? this.supplyForRewardLabel(supplyReward) : undefined;
|
||||
const recruit = rewards?.recruits[0];
|
||||
const cardDefinitions: Array<{
|
||||
id: VictoryRewardCardId;
|
||||
label: string;
|
||||
items: string[];
|
||||
accent: number;
|
||||
icon: VictoryRewardIcon;
|
||||
actionId?: VictoryRewardActionId;
|
||||
actionHint: string;
|
||||
isNew?: boolean;
|
||||
}> = [
|
||||
{
|
||||
id: 'gold',
|
||||
label: '군자금',
|
||||
@@ -13054,13 +13084,58 @@ export class CampScene extends Phaser.Scene {
|
||||
...(presentation.sortieOrderBonus
|
||||
? [`${presentation.sortieOrderBonus.label} 첫 공훈 +${presentation.sortieOrderBonus.rewardGold} 포함`]
|
||||
: [])
|
||||
]
|
||||
],
|
||||
accent: palette.gold,
|
||||
icon: { kind: 'mark', mark: '金' },
|
||||
actionHint: '승전 자산에 반영 완료'
|
||||
},
|
||||
{ id: 'equipment', label: '장비', items: [...(rewards?.equipment ?? [])] },
|
||||
{ id: 'supplies', label: '보급', items: supplyItems },
|
||||
{ id: 'reputation', label: '명성·기록', items: [...(rewards?.reputation ?? [])] },
|
||||
{ id: 'recruits', label: '합류 무장', items: rewards?.recruits.map((recruit) => recruit.name) ?? [] },
|
||||
{ id: 'unlocks', label: '새 전장', items: rewards?.unlocks.map((unlock) => unlock.title) ?? [] }
|
||||
{
|
||||
id: 'equipment',
|
||||
label: '장비',
|
||||
items: [...(rewards?.equipment ?? [])],
|
||||
accent: equipmentItem?.rank === 'treasure' ? palette.gold : palette.blue,
|
||||
icon: equipmentItem ? { kind: 'item', itemId: equipmentItem.id } : { kind: 'battle-ui', iconKey: 'sword' },
|
||||
actionId: 'equipment',
|
||||
actionHint: '눌러서 장비 비교',
|
||||
isNew: Boolean(equipmentReward)
|
||||
},
|
||||
{
|
||||
id: 'supplies',
|
||||
label: '보급',
|
||||
items: supplyItems,
|
||||
accent: palette.green,
|
||||
icon: { kind: 'battle-ui', iconKey: supply?.usableId ?? 'bean' },
|
||||
actionId: 'supplies',
|
||||
actionHint: '눌러서 보급 확인'
|
||||
},
|
||||
{
|
||||
id: 'reputation',
|
||||
label: '명성·기록',
|
||||
items: [...(rewards?.reputation ?? [])],
|
||||
accent: 0xc887dc,
|
||||
icon: { kind: 'mark', mark: '名' },
|
||||
actionHint: '전황 기록에 반영 완료'
|
||||
},
|
||||
{
|
||||
id: 'recruits',
|
||||
label: '합류 무장',
|
||||
items: rewards?.recruits.map((rewardRecruit) => rewardRecruit.name) ?? [],
|
||||
accent: palette.gold,
|
||||
icon: recruit ? { kind: 'portrait', unitId: recruit.unitId } : { kind: 'battle-ui', iconKey: 'leadership' },
|
||||
actionId: 'sortie',
|
||||
actionHint: '눌러서 편성에 배치',
|
||||
isNew: Boolean(recruit)
|
||||
},
|
||||
{
|
||||
id: 'unlocks',
|
||||
label: '새 전장',
|
||||
items: rewards?.unlocks.map((unlock) => unlock.title) ?? [],
|
||||
accent: 0xe78a55,
|
||||
icon: { kind: 'battle-ui', iconKey: 'terrain' },
|
||||
actionId: 'sortie',
|
||||
actionHint: '눌러서 다음 전장 준비',
|
||||
isNew: Boolean(rewards?.unlocks.length)
|
||||
}
|
||||
];
|
||||
const cardWidth = 252;
|
||||
const cardHeight = 104;
|
||||
@@ -13069,28 +13144,75 @@ export class CampScene extends Phaser.Scene {
|
||||
const row = Math.floor(index / 3);
|
||||
const cardX = panelX + 34 + column * 268;
|
||||
const cardY = panelY + 128 + row * 120;
|
||||
const background = this.trackVictoryReward(
|
||||
this.add.rectangle(cardX, cardY, cardWidth, cardHeight, definition.id === 'gold' ? 0x2d2619 : 0x141f2a, 0.98)
|
||||
);
|
||||
const hasReward = definition.items.length > 0;
|
||||
const actionable = hasReward && Boolean(definition.actionId);
|
||||
const restingFill = definition.id === 'gold' ? 0x2d2619 : hasReward ? 0x17232e : 0x111820;
|
||||
const background = this.trackVictoryReward(this.add.rectangle(cardX, cardY, cardWidth, cardHeight, restingFill, 0.98));
|
||||
background.setOrigin(0);
|
||||
background.setDepth(depth + 2);
|
||||
background.setStrokeStyle(1, definition.id === 'gold' ? palette.gold : palette.blue, definition.items.length > 0 ? 0.78 : 0.38);
|
||||
background.setStrokeStyle(hasReward ? 2 : 1, definition.accent, hasReward ? 0.78 : 0.3);
|
||||
|
||||
const iconPlate = this.trackVictoryReward(this.add.rectangle(cardX + 12, cardY + 14, 62, 62, 0x080d13, hasReward ? 0.96 : 0.64));
|
||||
iconPlate.setOrigin(0);
|
||||
iconPlate.setDepth(depth + 3);
|
||||
iconPlate.setStrokeStyle(1, definition.accent, hasReward ? 0.72 : 0.26);
|
||||
const iconState = this.renderVictoryRewardIcon(definition.icon, cardX + 43, cardY + 45, 50, depth + 4, hasReward ? 1 : 0.42);
|
||||
|
||||
const label = this.trackVictoryReward(
|
||||
this.add.text(cardX + 16, cardY + 13, definition.label, this.textStyle(13, definition.id === 'gold' ? '#ffdf7b' : '#9fc8e8', true))
|
||||
this.add.text(cardX + 86, cardY + 11, definition.label, this.textStyle(12, hasReward ? '#9fc8e8' : '#77818c', true))
|
||||
);
|
||||
label.setDepth(depth + 3);
|
||||
|
||||
if (definition.isNew && hasReward) {
|
||||
const newBadge = this.trackVictoryReward(this.add.rectangle(cardX + cardWidth - 30, cardY + 16, 44, 19, 0x7b271f, 0.98));
|
||||
newBadge.setDepth(depth + 4);
|
||||
newBadge.setStrokeStyle(1, 0xffb08e, 0.9);
|
||||
const newLabel = this.trackVictoryReward(this.add.text(cardX + cardWidth - 30, cardY + 16, 'NEW', this.textStyle(9, '#fff0d8', true)));
|
||||
newLabel.setOrigin(0.5);
|
||||
newLabel.setDepth(depth + 5);
|
||||
}
|
||||
|
||||
const itemText = definition.items.length > 0 ? definition.items.join(' · ') : '변동 없음';
|
||||
const value = this.trackVictoryReward(
|
||||
this.add.text(cardX + 16, cardY + 40, itemText, {
|
||||
...this.textStyle(definition.id === 'gold' ? 17 : 12, definition.items.length > 0 ? '#f2e3bf' : '#77818c', true),
|
||||
wordWrap: { width: cardWidth - 32, useAdvancedWrap: true },
|
||||
lineSpacing: 4
|
||||
this.add.text(cardX + 86, cardY + 34, itemText, {
|
||||
...this.textStyle(definition.id === 'gold' ? 16 : 12, hasReward ? '#f2e3bf' : '#77818c', true),
|
||||
wordWrap: { width: cardWidth - 100, useAdvancedWrap: true },
|
||||
lineSpacing: 3,
|
||||
maxLines: 2
|
||||
})
|
||||
);
|
||||
value.setDepth(depth + 3);
|
||||
this.victoryRewardCards.push({ ...definition, background });
|
||||
|
||||
const actionHint = this.trackVictoryReward(
|
||||
this.add.text(cardX + 86, cardY + cardHeight - 18, hasReward ? definition.actionHint : '이번 전투 변동 없음', this.textStyle(9, actionable ? '#ffdf7b' : '#7f8994', actionable))
|
||||
);
|
||||
actionHint.setDepth(depth + 3);
|
||||
|
||||
if (actionable && definition.actionId) {
|
||||
background.setInteractive({ useHandCursor: true });
|
||||
background.on('pointerover', () => {
|
||||
background.setFillStyle(definition.id === 'gold' ? 0x3a3020 : 0x223342, 1);
|
||||
background.setStrokeStyle(2, palette.gold, 0.98);
|
||||
iconPlate.setStrokeStyle(2, palette.gold, 0.92);
|
||||
});
|
||||
background.on('pointerout', () => {
|
||||
background.setFillStyle(restingFill, 0.98);
|
||||
background.setStrokeStyle(2, definition.accent, 0.78);
|
||||
iconPlate.setStrokeStyle(1, definition.accent, 0.72);
|
||||
});
|
||||
background.on('pointerdown', () => this.completeVictoryRewardAcknowledgement(definition.actionId!));
|
||||
}
|
||||
this.victoryRewardCards.push({
|
||||
id: definition.id,
|
||||
label: definition.label,
|
||||
items: definition.items,
|
||||
background,
|
||||
iconKind: definition.icon.kind,
|
||||
iconKey: iconState.key,
|
||||
iconReady: iconState.ready,
|
||||
isNew: Boolean(definition.isNew && hasReward),
|
||||
...(actionable && definition.actionId ? { actionId: definition.actionId } : {})
|
||||
});
|
||||
});
|
||||
|
||||
const note = rewards?.note?.trim();
|
||||
@@ -13150,6 +13272,13 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private completeVictoryRewardAcknowledgement(action: VictoryRewardActionId) {
|
||||
const report = this.pendingVictoryRewardReport;
|
||||
const rewardedRecruitId = report?.campaignRewards?.recruits[0]?.unitId;
|
||||
if (action === 'sortie' && rewardedRecruitId && this.sortieAllies().some((unit) => unit.id === rewardedRecruitId)) {
|
||||
this.focusVictoryRewardRecruit(rewardedRecruitId);
|
||||
}
|
||||
if (action === 'equipment' && report) {
|
||||
this.focusVictoryRewardEquipment(report);
|
||||
}
|
||||
if (report) {
|
||||
this.campaign = acknowledgeCampaignVictoryReward(report.battleId);
|
||||
if (this.campaign.acknowledgedVictoryRewardBattleIds.includes(report.battleId)) {
|
||||
@@ -13191,6 +13320,108 @@ export class CampScene extends Phaser.Scene {
|
||||
this.victoryRewardActions = [];
|
||||
}
|
||||
|
||||
private rewardInventoryLabel(label: string) {
|
||||
return label.replace(/\s*(?:[+xX×-]?\s*\d+)\s*$/u, '').trim();
|
||||
}
|
||||
|
||||
private itemForRewardLabel(label: string) {
|
||||
return findItemByName(this.rewardInventoryLabel(label));
|
||||
}
|
||||
|
||||
private supplyForRewardLabel(label: string) {
|
||||
return campSupplyByLabel.get(this.rewardInventoryLabel(label));
|
||||
}
|
||||
|
||||
private focusVictoryRewardEquipment(report: FirstBattleReport) {
|
||||
const rewardItemIds = new Set(
|
||||
(report.campaignRewards?.equipment ?? [])
|
||||
.map((label) => this.itemForRewardLabel(label)?.id)
|
||||
.filter((itemId): itemId is string => Boolean(itemId))
|
||||
);
|
||||
if (rewardItemIds.size === 0) {
|
||||
return;
|
||||
}
|
||||
const rewardIndex = this.equipmentInventoryEntries().findIndex((entry) => rewardItemIds.has(entry.item.id));
|
||||
if (rewardIndex >= 0) {
|
||||
this.equipmentInventoryPage = Math.floor(rewardIndex / equipmentInventoryPageSize);
|
||||
}
|
||||
}
|
||||
|
||||
private focusVictoryRewardRecruit(unitId: string) {
|
||||
this.sortieFocusedUnitId = unitId;
|
||||
const displayUnits = this.sortieRosterDisplayUnits(this.sortieRosterUnits());
|
||||
const recruitIndex = displayUnits.findIndex((unit) => unit.id === unitId);
|
||||
if (recruitIndex >= 0) {
|
||||
this.sortiePortraitRosterPage = Math.floor(recruitIndex / sortiePortraitRosterPageSize);
|
||||
}
|
||||
}
|
||||
|
||||
private renderVictoryRewardIcon(
|
||||
icon: VictoryRewardIcon,
|
||||
x: number,
|
||||
y: number,
|
||||
size: number,
|
||||
depth: number,
|
||||
alpha: number
|
||||
): { key: string; ready: boolean } {
|
||||
if (icon.kind === 'item') {
|
||||
const textureKey = `item-${icon.itemId}`;
|
||||
if (this.textures.exists(textureKey)) {
|
||||
const image = this.trackVictoryReward(this.add.image(x, y, textureKey));
|
||||
image.setDisplaySize(this.campUiLength(size), this.campUiLength(size));
|
||||
image.setAlpha(alpha);
|
||||
image.setDepth(depth);
|
||||
return { key: textureKey, ready: true };
|
||||
}
|
||||
return this.renderVictoryRewardIcon({ kind: 'battle-ui', iconKey: 'sword' }, x, y, size, depth, alpha);
|
||||
}
|
||||
|
||||
if (icon.kind === 'portrait') {
|
||||
const unit = this.currentUnits().find((candidate) => candidate.id === icon.unitId);
|
||||
const portraitKey = campaignPortraitKeysByUnitId[icon.unitId];
|
||||
const textureKey = portraitKey
|
||||
? [portraitCardTextureKey(portraitKey), portraitTextureKey(portraitKey)].find(
|
||||
(candidate): candidate is string => Boolean(candidate && this.textures.exists(candidate))
|
||||
)
|
||||
: undefined;
|
||||
if (textureKey) {
|
||||
const image = this.trackVictoryReward(this.add.image(x, y, textureKey));
|
||||
image.setDisplaySize(this.campUiLength(size), this.campUiLength(size));
|
||||
image.setAlpha(alpha);
|
||||
image.setDepth(depth);
|
||||
return { key: textureKey, ready: true };
|
||||
}
|
||||
return this.renderVictoryRewardMark(unit?.name.slice(0, 1) ?? '將', x, y, size, depth, alpha, `portrait-${icon.unitId}`);
|
||||
}
|
||||
|
||||
if (icon.kind === 'battle-ui') {
|
||||
const displaySize = this.campUiLength(size);
|
||||
const textureKey = battleUiIconTextureForSize(displaySize);
|
||||
if (this.textures.exists(textureKey)) {
|
||||
const image = this.trackVictoryReward(this.add.image(x, y, textureKey, battleUiIconFrames[icon.iconKey]));
|
||||
image.setDisplaySize(displaySize, displaySize);
|
||||
image.setAlpha(alpha);
|
||||
image.setDepth(depth);
|
||||
return { key: icon.iconKey, ready: true };
|
||||
}
|
||||
return this.renderVictoryRewardMark('賞', x, y, size, depth, alpha, icon.iconKey);
|
||||
}
|
||||
|
||||
return this.renderVictoryRewardMark(icon.mark, x, y, size, depth, alpha, icon.mark);
|
||||
}
|
||||
|
||||
private renderVictoryRewardMark(mark: string, x: number, y: number, size: number, depth: number, alpha: number, key: string) {
|
||||
const seal = this.trackVictoryReward(this.add.circle(x, y, size / 2, 0x2d2619, 0.94));
|
||||
seal.setStrokeStyle(2, palette.gold, 0.74);
|
||||
seal.setAlpha(alpha);
|
||||
seal.setDepth(depth);
|
||||
const label = this.trackVictoryReward(this.add.text(x, y, mark, this.textStyle(Math.max(20, Math.round(size * 0.52)), '#ffdf7b', true)));
|
||||
label.setOrigin(0.5);
|
||||
label.setAlpha(alpha);
|
||||
label.setDepth(depth + 1);
|
||||
return { key, ready: true };
|
||||
}
|
||||
|
||||
private handleEscapeKey() {
|
||||
if (this.victoryRewardObjects.length > 0) {
|
||||
this.completeVictoryRewardAcknowledgement('close');
|
||||
@@ -13777,6 +14008,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const selectedCount = this.selectedSortieUnitIds.length;
|
||||
const maxUnits = this.sortieMaxUnits();
|
||||
const rangeEnd = pageStart + visibleUnits.length;
|
||||
const serviceHistoryByUnitId = this.sortieRosterHistoryByUnitId();
|
||||
const previousEnabled = this.sortiePortraitRosterPage > 0;
|
||||
const nextEnabled = this.sortiePortraitRosterPage < pageCount - 1;
|
||||
const previousButtonBounds = { x: x + width - 100, y: y + 9, width: 40, height: 30 };
|
||||
@@ -13852,6 +14084,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const availability = this.sortieUnitAvailability(unit);
|
||||
const role = this.sortieFormationRole(unit);
|
||||
const preview = this.sortieUnitSynergyPreview(unit);
|
||||
const serviceHistory = serviceHistoryByUnitId.get(unit.id);
|
||||
const canPinSwap = availability.available && !selected && selectedCount >= maxUnits &&
|
||||
this.isSortieSelected(this.sortieFocusedUnitId) && !this.isRequiredSortieUnit(this.sortieFocusedUnitId);
|
||||
const fill = !availability.available
|
||||
@@ -13937,10 +14170,16 @@ export class CampScene extends Phaser.Scene {
|
||||
portraitView.frame.setStrokeStyle(2, accentColor, focused ? 0.92 : selected ? 0.78 : recommendation ? 0.62 : 0.42);
|
||||
portraitView.image?.setDisplaySize(this.campUiLength(70), this.campUiLength(70));
|
||||
});
|
||||
const statusLabel = !availability.available
|
||||
const rosterStatusLabel = !availability.available
|
||||
? required ? `필수 · ${availability.label}` : availability.label
|
||||
: required ? '필수' : selected ? '출전' : pinnedSwapCandidate ? '교체안' : recommendation ? '추천' : '대기';
|
||||
const statusColor = !availability.available
|
||||
const serviceStatusLabel = availability.available && serviceHistory?.badge.kind !== 'active'
|
||||
? serviceHistory?.badge.label
|
||||
: undefined;
|
||||
const statusLabel = serviceStatusLabel ? `${serviceStatusLabel} · ${rosterStatusLabel}` : rosterStatusLabel;
|
||||
const statusColor = serviceStatusLabel && serviceHistory
|
||||
? this.sortieRosterHistoryTone(serviceHistory).text
|
||||
: !availability.available
|
||||
? '#ff9d7d'
|
||||
: required || recommendation || pinnedSwapCandidate ? '#ffdf7b' : selected ? '#a8ffd0' : '#9fb0bf';
|
||||
const status = this.trackSortie(this.add.text(cardX + cardWidth - 10, cardY + 9, statusLabel, this.textStyle(10, statusColor, true)));
|
||||
@@ -15066,6 +15305,7 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
this.sortieRosterToggleBounds = [];
|
||||
const units = this.sortieRosterDisplayUnits(this.sortieRosterUnits());
|
||||
const serviceHistoryByUnitId = this.sortieRosterHistoryByUnitId();
|
||||
const compactGrid = units.length > 3;
|
||||
const columns = compactGrid ? 2 : 3;
|
||||
const rows = Math.max(1, Math.ceil(units.length / columns));
|
||||
@@ -15084,6 +15324,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const required = this.isRequiredSortieUnit(unit.id);
|
||||
const recommendation = this.sortieRecommendation(unit.id);
|
||||
const role = this.sortieFormationRole(unit);
|
||||
const serviceHistory = serviceHistoryByUnitId.get(unit.id);
|
||||
const card = this.trackSortie(this.add.rectangle(cardX, cardY, cardWidth, cardHeight, selected ? 0x172a22 : 0x121820, selected ? 0.98 : 0.78));
|
||||
card.setOrigin(0);
|
||||
card.setDepth(depth + 1);
|
||||
@@ -15102,6 +15343,9 @@ export class CampScene extends Phaser.Scene {
|
||||
: `${this.sortieFormationRoleLabel(role)} 역할로 전장에 합류합니다.`;
|
||||
if (compactGrid) {
|
||||
this.renderFirstSortiePortrait(cardX + 50, cardY + 55, 72, unit, depth + 2, selected ? 1 : 0.5);
|
||||
if (serviceHistory) {
|
||||
this.renderSortieRosterHistoryBadge(serviceHistory, cardX + 50, cardY + 20, 68, depth + 5);
|
||||
}
|
||||
this.trackSortie(this.add.text(cardX + 96, cardY + 10, `${unit.name} Lv${unit.level}`, this.textStyle(16, selected ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(cardX + 96, cardY + 36, `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(10, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2);
|
||||
this.drawSortieBar(cardX + 96, cardY + 56, cardWidth - 110, 6, unit.hp / unit.maxHp, selected ? palette.green : 0x53606c, depth + 2);
|
||||
@@ -15110,6 +15354,9 @@ export class CampScene extends Phaser.Scene {
|
||||
this.trackSortie(this.add.text(cardX + 14, cardY + 113, this.compactText(this.sortieStrategyLine(unit.id), 34), this.textStyle(9, selected ? '#9fc8e8' : '#77818c', true))).setDepth(depth + 2);
|
||||
} else {
|
||||
this.renderFirstSortiePortrait(cardX + cardWidth / 2, cardY + 88, 132, unit, depth + 2, selected ? 1 : 0.5);
|
||||
if (serviceHistory) {
|
||||
this.renderSortieRosterHistoryBadge(serviceHistory, cardX + cardWidth / 2, cardY + 30, 78, depth + 5);
|
||||
}
|
||||
this.trackSortie(this.add.text(cardX + 14, cardY + 162, `${unit.name} Lv${unit.level}`, this.textStyle(20, selected ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 2);
|
||||
this.trackSortie(this.add.text(cardX + 14, cardY + 190, `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(12, selected ? '#d4dce6' : '#77818c', true))).setDepth(depth + 2);
|
||||
this.drawSortieBar(cardX + 14, cardY + 211, cardWidth - 28, 7, unit.hp / unit.maxHp, selected ? palette.green : 0x53606c, depth + 2);
|
||||
@@ -16243,6 +16490,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const statusColor = status.tone === 'disabled' ? '#77818c' : status.tone === 'selected' ? '#a8ffd0' : status.tone === 'recommended' ? '#ffdf7b' : '#9fb0bf';
|
||||
const selected = this.isSortieSelected(unit.id);
|
||||
const synergyPreview = this.sortieUnitSynergyPreview(unit);
|
||||
const serviceHistory = this.sortieRosterHistoryByUnitId().get(unit.id);
|
||||
const synergyColor = synergyPreview.mode === 'remove'
|
||||
? '#ff9d7d'
|
||||
: synergyPreview.mode === 'add' || synergyPreview.mode === 'current'
|
||||
@@ -16255,6 +16503,12 @@ export class CampScene extends Phaser.Scene {
|
||||
this.trackSortie(this.add.text(x + width - 16, y + 16, status.label, this.textStyle(11, statusColor, true))).setOrigin(1, 0).setDepth(depth + 1);
|
||||
this.renderSortieBattleUiIcon(this.unitClassIconKey(unit), x + 34, y + 58, 42, depth + 1, availability.available ? 1 : 0.5);
|
||||
this.trackSortie(this.add.text(x + 66, y + 42, `${unit.name} Lv${unit.level}`, this.textStyle(18, availability.available ? '#f2e3bf' : '#87919c', true))).setDepth(depth + 1);
|
||||
if (serviceHistory) {
|
||||
const serviceTone = this.sortieRosterHistoryTone(serviceHistory);
|
||||
this.trackSortie(
|
||||
this.add.text(x + width - 16, y + 43, this.compactText(serviceHistory.summary, 25), this.textStyle(10, serviceTone.text, true))
|
||||
).setOrigin(1, 0).setDepth(depth + 1);
|
||||
}
|
||||
this.trackSortie(this.add.text(x + 66, y + 68, `${unit.className} · ${getUnitClass(unit.classKey).role}`, this.textStyle(12, availability.available ? '#d4dce6' : '#77818c', true))).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + width - 16, y + 68, `HP ${unit.hp}/${unit.maxHp}`, this.textStyle(12, unit.hp < unit.maxHp ? '#ffdf7b' : '#a8ffd0', true))).setOrigin(1, 0).setDepth(depth + 1);
|
||||
this.drawSortieBar(x + 66, y + 88, width - 92, 7, unit.hp / unit.maxHp, availability.available ? palette.green : 0x53606c, depth + 1);
|
||||
@@ -18565,6 +18819,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const terrainScore = this.sortieTerrainScore(unit, scenario);
|
||||
const assignedReserveTrainingFocus = this.campaign?.reserveTrainingAssignments[unit.id];
|
||||
const reserveTrainingFocus = this.reserveTrainingFocusDefinitionForUnit(unit.id);
|
||||
const serviceHistory = this.sortieRosterHistoryByUnitId().get(unit.id) ?? this.emptySortieRosterHistory(unit.id);
|
||||
return {
|
||||
id: unit.id,
|
||||
name: unit.name,
|
||||
@@ -18581,6 +18836,7 @@ export class CampScene extends Phaser.Scene {
|
||||
reserveTrainingBondPreviewLine: this.reserveTrainingBondPreviewLine(unit),
|
||||
reserveTrainingBondPartnerCount: this.reserveTrainingBondPartnerNames(unit).length,
|
||||
reserveTrainingPlanLine: this.reserveTrainingPlanLine(unit),
|
||||
serviceHistory,
|
||||
terrainScore,
|
||||
terrainGrade: this.sortieTerrainGrade(terrainScore),
|
||||
statLine: summary.statLine,
|
||||
@@ -19810,6 +20066,55 @@ export class CampScene extends Phaser.Scene {
|
||||
return { label: '대기', tone: 'reserve' as const };
|
||||
}
|
||||
|
||||
private sortieRosterHistoryByUnitId(allies: UnitData[] = this.sortieAllies()) {
|
||||
return deriveSortieRosterHistoryByUnitId(
|
||||
allies.map((unit) => unit.id),
|
||||
this.campaign?.battleHistory ?? {}
|
||||
);
|
||||
}
|
||||
|
||||
private emptySortieRosterHistory(unitId: string): SortieRosterUnitHistory {
|
||||
return {
|
||||
unitId,
|
||||
sortieCount: 0,
|
||||
firstSortiePending: true,
|
||||
battlesSinceLastSortie: null,
|
||||
recentRecruit: false,
|
||||
battlesSinceRecruit: null,
|
||||
badge: { kind: 'first-sortie', label: '미출전' },
|
||||
summary: '출전 기록 없음 · 첫 출전 대기'
|
||||
};
|
||||
}
|
||||
|
||||
private sortieRosterHistoryTone(history: SortieRosterUnitHistory) {
|
||||
switch (history.badge.kind) {
|
||||
case 'recent-recruit':
|
||||
return { frame: palette.red, text: '#fff0d8' };
|
||||
case 'first-sortie':
|
||||
return { frame: palette.gold, text: '#ffdf7b' };
|
||||
case 'long-wait':
|
||||
return { frame: 0xe78a55, text: '#ffc29d' };
|
||||
default:
|
||||
return { frame: palette.green, text: '#a8ffd0' };
|
||||
}
|
||||
}
|
||||
|
||||
private renderSortieRosterHistoryBadge(
|
||||
history: SortieRosterUnitHistory,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
depth: number
|
||||
) {
|
||||
const tone = this.sortieRosterHistoryTone(history);
|
||||
const background = this.trackSortie(this.add.rectangle(x, y, width, 18, 0x090e14, 0.96));
|
||||
background.setStrokeStyle(1, tone.frame, 0.9);
|
||||
background.setDepth(depth);
|
||||
this.trackSortie(this.add.text(x, y, history.badge.label, this.textStyle(8, tone.text, true)))
|
||||
.setOrigin(0.5)
|
||||
.setDepth(depth + 1);
|
||||
}
|
||||
|
||||
private renderUnitFallbackPortrait(x: number, y: number, radius: number, unit: UnitData) {
|
||||
const bg = this.track(this.add.circle(x, y, radius, 0x1f3140, 0.96));
|
||||
bg.setStrokeStyle(2, palette.gold, 0.58);
|
||||
@@ -23430,6 +23735,7 @@ export class CampScene extends Phaser.Scene {
|
||||
const sortieCoreResonanceCandidates = this.sortieCoreResonanceCandidates();
|
||||
const sortieCoreResonanceSelection = this.campaign?.sortieResonanceSelection;
|
||||
const sortieRecommendationPlanResults = this.sortieRecommendationPlans();
|
||||
const sortieRosterHistoryByUnitId = this.sortieRosterHistoryByUnitId();
|
||||
const sortieRecommendationPlans = this.sortieRecommendationProjections();
|
||||
const recommendedPresetId = this.recommendedSortiePresetId();
|
||||
const sortieFormationPresets = this.sortieFormationPresets();
|
||||
@@ -23484,6 +23790,12 @@ export class CampScene extends Phaser.Scene {
|
||||
id: card.id,
|
||||
label: card.label,
|
||||
items: [...card.items],
|
||||
iconKind: card.iconKind,
|
||||
iconKey: card.iconKey,
|
||||
iconReady: card.iconReady,
|
||||
new: card.isNew,
|
||||
actionId: card.actionId ?? null,
|
||||
interactive: Boolean(card.background.input?.enabled),
|
||||
bounds: this.sortieObjectBoundsDebug(card.background)
|
||||
})),
|
||||
actions: this.victoryRewardActions.map((action) => ({
|
||||
@@ -24208,6 +24520,10 @@ export class CampScene extends Phaser.Scene {
|
||||
reserveTrainingPlanLine: this.reserveTrainingPlanLine(unit),
|
||||
recommended: Boolean(this.sortieRecommendation(unit.id)),
|
||||
recommendationReason: this.sortieRecommendation(unit.id)?.reason ?? null,
|
||||
serviceHistory: (() => {
|
||||
const history = sortieRosterHistoryByUnitId.get(unit.id) ?? this.emptySortieRosterHistory(unit.id);
|
||||
return { ...history, badge: { ...history.badge } };
|
||||
})(),
|
||||
statLine: summary.statLine,
|
||||
equipmentLine: summary.equipmentLine,
|
||||
bondLine: summary.bondLine,
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
type BattleUiIconKey
|
||||
} from '../data/battleUiIcons';
|
||||
import { portraitAssetEntriesForKey, portraitKeyForSpeaker, type PortraitAssetEntry } from '../data/portraitAssets';
|
||||
import { storyBackgroundAssets, storyBackgroundKeyForPage } from '../data/storyAssets';
|
||||
import {
|
||||
storyBackgroundAssets,
|
||||
storyBackgroundKeyForPage,
|
||||
storyBackgroundKeysForPages
|
||||
} from '../data/storyAssets';
|
||||
import {
|
||||
ensureUnitAnimations,
|
||||
loadUnitBaseSheets,
|
||||
@@ -130,6 +134,7 @@ const firstBattleWarmupUnitTextureKeys = [
|
||||
export class StoryScene extends Phaser.Scene {
|
||||
private pageIndex = 0;
|
||||
private pages: StoryPage[] = prologuePages;
|
||||
private selectedStoryBackgroundKeys = storyBackgroundKeysForPages(prologuePages);
|
||||
private nextScene = 'BattleScene';
|
||||
private nextSceneData?: Record<string, unknown>;
|
||||
private transitioning = false;
|
||||
@@ -159,6 +164,7 @@ export class StoryScene extends Phaser.Scene {
|
||||
|
||||
init(data?: StorySceneData) {
|
||||
this.pages = data?.pages?.length ? data.pages : prologuePages;
|
||||
this.selectedStoryBackgroundKeys = storyBackgroundKeysForPages(this.pages);
|
||||
this.nextScene = data?.nextScene ?? 'BattleScene';
|
||||
this.nextSceneData = data?.nextSceneData;
|
||||
this.pageIndex = 0;
|
||||
@@ -745,7 +751,10 @@ export class StoryScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private pageBackgroundAssetKey(page: StoryPage) {
|
||||
const key = storyBackgroundKeyForPage(page.background, page.id);
|
||||
const pageIndex = this.pages.indexOf(page);
|
||||
const key = pageIndex >= 0
|
||||
? this.selectedStoryBackgroundKeys[pageIndex]
|
||||
: storyBackgroundKeyForPage(page.background, page.id);
|
||||
return storyBackgroundAssets[key] ? key : page.background;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user