feat: add era-based camp skins

This commit is contained in:
2026-07-13 01:14:59 +09:00
parent 254dfb2b84
commit 9428c7c1c4
6 changed files with 830 additions and 14 deletions

312
src/game/data/campSkins.ts Normal file
View File

@@ -0,0 +1,312 @@
import antiDongCoalitionOathCampUrl from '../../assets/images/story/113-anti-dong-coalition-oath-camp.png';
import militiaTrainingYardUrl from '../../assets/images/story/126-militia-training-yard.png';
import wanderingNightCampUrl from '../../assets/images/story/41-wandering-night-camp.png';
import yizhouCouncilAtRiverUrl from '../../assets/images/story/45-yizhou-council-at-river.png';
import nanzhongRiverNightCampUrl from '../../assets/images/story/68-nanzhong-captures-river-night-camp.png';
import jingzhouRiverWatchtowersUrl from '../../assets/images/story/75-jingzhou-crisis-river-watchtowers.png';
import xuzhouGateCouncilUrl from '../../assets/images/story/77-xuzhou-gate-council.png';
import northernSupplyDepotUrl from '../../assets/images/story/92-northern-supply-depot.png';
import yizhouMountainCommandPostUrl from '../../assets/images/story/93-yizhou-mountain-pass-command-post.png';
import wolongAutumnStudyUrl from '../../assets/images/story/96-wolong-autumn-study.png';
import yilingBaidiLanternVigilUrl from '../../assets/images/story/102-yiling-baidi-lantern-vigil.png';
import redCliffsWindAltarUrl from '../../assets/images/story/114-red-cliffs-wind-altar.png';
import { battleScenarios, type BattleScenarioId } from './battles';
import type { CampaignStep } from '../state/campaignState';
export type CampSkinId =
| 'yellow-turban'
| 'anti-dong'
| 'xuzhou'
| 'wandering'
| 'wolong'
| 'red-cliffs'
| 'jing-yi'
| 'hanzhong-shuhan'
| 'jingzhou-crisis'
| 'yiling-baidi'
| 'nanzhong'
| 'northern';
export type CampSkinDefinition = {
id: CampSkinId;
textureKey: string;
assetUrl: string;
chapterLabel: string;
locationLabel: string;
seal: string;
accentColor: number;
headerColor: number;
washColor: number;
backdropAlpha: number;
washAlpha: number;
};
export type CampSkinBattleArc = {
skinId: CampSkinId;
firstBattleOrdinal: number;
lastBattleOrdinal: number;
};
export type CampSkinSelectionSource = 'special-step' | 'next-battle' | 'current-battle' | 'fallback';
export type CampSkinSelection = {
skin: CampSkinDefinition;
source: CampSkinSelectionSource;
requestedBattleId?: BattleScenarioId;
battleOrdinal?: number;
};
export type CampSkinSelectionInput = {
campaignStep?: CampaignStep;
nextBattleId?: string;
currentBattleId?: string;
};
export const campSkinDefinitions: Record<CampSkinId, CampSkinDefinition> = {
'yellow-turban': {
id: 'yellow-turban',
textureKey: 'story-militia-training-yard',
assetUrl: militiaTrainingYardUrl,
chapterLabel: '황건적 토벌',
locationLabel: '탁군 의병영',
seal: '義',
accentColor: 0xd6b35b,
headerColor: 0x392b1d,
washColor: 0x17120d,
backdropAlpha: 0.82,
washAlpha: 0.42
},
'anti-dong': {
id: 'anti-dong',
textureKey: 'story-anti-dong-coalition-oath-camp',
assetUrl: antiDongCoalitionOathCampUrl,
chapterLabel: '반동탁 연합',
locationLabel: '연합군 맹진',
seal: '盟',
accentColor: 0xc46b55,
headerColor: 0x3a211d,
washColor: 0x1b100e,
backdropAlpha: 0.81,
washAlpha: 0.43
},
xuzhou: {
id: 'xuzhou',
textureKey: 'story-xuzhou-gate-council',
assetUrl: xuzhouGateCouncilUrl,
chapterLabel: '서주 공방',
locationLabel: '서주 성문',
seal: '徐',
accentColor: 0x7fa0b8,
headerColor: 0x25323a,
washColor: 0x10171b,
backdropAlpha: 0.8,
washAlpha: 0.44
},
wandering: {
id: 'wandering',
textureKey: 'story-wandering-night-camp',
assetUrl: wanderingNightCampUrl,
chapterLabel: '천하 유랑',
locationLabel: '객군 야영지',
seal: '旅',
accentColor: 0x6f8fae,
headerColor: 0x202b39,
washColor: 0x0d131b,
backdropAlpha: 0.84,
washAlpha: 0.46
},
wolong: {
id: 'wolong',
textureKey: 'story-wolong-autumn-study',
assetUrl: wolongAutumnStudyUrl,
chapterLabel: '와룡의 계책',
locationLabel: '신야 군막',
seal: '龍',
accentColor: 0xb8894f,
headerColor: 0x33291f,
washColor: 0x17130f,
backdropAlpha: 0.8,
washAlpha: 0.4
},
'red-cliffs': {
id: 'red-cliffs',
textureKey: 'story-red-cliffs-wind-altar',
assetUrl: redCliffsWindAltarUrl,
chapterLabel: '적벽대전',
locationLabel: '장강 수군진',
seal: '赤',
accentColor: 0xd8732c,
headerColor: 0x3d2115,
washColor: 0x1c0e08,
backdropAlpha: 0.83,
washAlpha: 0.42
},
'jing-yi': {
id: 'jing-yi',
textureKey: 'story-yizhou-council-at-river',
assetUrl: yizhouCouncilAtRiverUrl,
chapterLabel: '형주·익주',
locationLabel: '서천 진군로',
seal: '益',
accentColor: 0x77a27a,
headerColor: 0x243328,
washColor: 0x101711,
backdropAlpha: 0.8,
washAlpha: 0.41
},
'hanzhong-shuhan': {
id: 'hanzhong-shuhan',
textureKey: 'story-yizhou-mountain-pass-command-post',
assetUrl: yizhouMountainCommandPostUrl,
chapterLabel: '한중·촉한',
locationLabel: '한중 산성',
seal: '漢',
accentColor: 0xd8b15f,
headerColor: 0x3b2d1c,
washColor: 0x18120b,
backdropAlpha: 0.79,
washAlpha: 0.39
},
'jingzhou-crisis': {
id: 'jingzhou-crisis',
textureKey: 'story-jingzhou-crisis-river-watchtowers',
assetUrl: jingzhouRiverWatchtowersUrl,
chapterLabel: '형주 위기',
locationLabel: '한수 방어진',
seal: '荊',
accentColor: 0xc78747,
headerColor: 0x35261d,
washColor: 0x18110d,
backdropAlpha: 0.82,
washAlpha: 0.45
},
'yiling-baidi': {
id: 'yiling-baidi',
textureKey: 'story-yiling-baidi-lantern-vigil',
assetUrl: yilingBaidiLanternVigilUrl,
chapterLabel: '이릉·백제성',
locationLabel: '백제성 야영',
seal: '白',
accentColor: 0x8aa7c2,
headerColor: 0x26313d,
washColor: 0x10161d,
backdropAlpha: 0.86,
washAlpha: 0.48
},
nanzhong: {
id: 'nanzhong',
textureKey: 'story-nanzhong-captures-river-night-camp',
assetUrl: nanzhongRiverNightCampUrl,
chapterLabel: '남중 평정',
locationLabel: '노수 원정군',
seal: '南',
accentColor: 0x7caa68,
headerColor: 0x263726,
washColor: 0x111a10,
backdropAlpha: 0.84,
washAlpha: 0.46
},
northern: {
id: 'northern',
textureKey: 'story-northern-supply-depot',
assetUrl: northernSupplyDepotUrl,
chapterLabel: '북벌',
locationLabel: '한중 보급진',
seal: '北',
accentColor: 0x8ea9d9,
headerColor: 0x263247,
washColor: 0x101621,
backdropAlpha: 0.82,
washAlpha: 0.44
}
};
export const campSkinBattleArcs: readonly CampSkinBattleArc[] = [
{ skinId: 'yellow-turban', firstBattleOrdinal: 1, lastBattleOrdinal: 4 },
{ skinId: 'anti-dong', firstBattleOrdinal: 5, lastBattleOrdinal: 6 },
{ skinId: 'xuzhou', firstBattleOrdinal: 7, lastBattleOrdinal: 9 },
{ skinId: 'wandering', firstBattleOrdinal: 10, lastBattleOrdinal: 15 },
{ skinId: 'wolong', firstBattleOrdinal: 16, lastBattleOrdinal: 18 },
{ skinId: 'red-cliffs', firstBattleOrdinal: 19, lastBattleOrdinal: 22 },
{ skinId: 'jing-yi', firstBattleOrdinal: 23, lastBattleOrdinal: 34 },
{ skinId: 'hanzhong-shuhan', firstBattleOrdinal: 35, lastBattleOrdinal: 37 },
{ skinId: 'jingzhou-crisis', firstBattleOrdinal: 38, lastBattleOrdinal: 44 },
{ skinId: 'yiling-baidi', firstBattleOrdinal: 45, lastBattleOrdinal: 46 },
{ skinId: 'nanzhong', firstBattleOrdinal: 47, lastBattleOrdinal: 54 },
{ skinId: 'northern', firstBattleOrdinal: 55, lastBattleOrdinal: 66 }
];
const specialStepSkinIds: Partial<Record<CampaignStep, CampSkinId>> = {
'hanzhong-king-camp': 'hanzhong-shuhan',
'shu-han-foundation-camp': 'hanzhong-shuhan',
'baidi-entrustment-camp': 'yiling-baidi',
'northern-campaign-prep-camp': 'northern',
'ending-complete': 'northern'
};
const orderedBattleIds = Object.keys(battleScenarios) as BattleScenarioId[];
const battleOrdinalById = new Map<BattleScenarioId, number>(
orderedBattleIds.map((battleId, index) => [battleId, index + 1])
);
export function campSkinBattleOrdinal(battleId: string | undefined): number | undefined {
if (!battleId || !(battleId in battleScenarios)) {
return undefined;
}
return battleOrdinalById.get(battleId as BattleScenarioId);
}
export function selectCampSkin({
campaignStep,
nextBattleId,
currentBattleId
}: CampSkinSelectionInput): CampSkinSelection {
const knownNextBattleId = knownBattleId(nextBattleId);
const knownCurrentBattleId = knownBattleId(currentBattleId);
const requestedBattleId = knownNextBattleId ?? knownCurrentBattleId;
const battleOrdinal = requestedBattleId ? battleOrdinalById.get(requestedBattleId) : undefined;
const specialSkinId = campaignStep ? specialStepSkinIds[campaignStep] : undefined;
if (specialSkinId) {
return {
skin: campSkinDefinitions[specialSkinId],
source: 'special-step',
requestedBattleId,
battleOrdinal
};
}
if (knownNextBattleId) {
return selectionForBattle(knownNextBattleId, 'next-battle');
}
if (knownCurrentBattleId) {
return selectionForBattle(knownCurrentBattleId, 'current-battle');
}
return {
skin: campSkinDefinitions['yellow-turban'],
source: 'fallback'
};
}
function selectionForBattle(
battleId: BattleScenarioId,
source: Extract<CampSkinSelectionSource, 'next-battle' | 'current-battle'>
): CampSkinSelection {
const battleOrdinal = battleOrdinalById.get(battleId);
const arc = campSkinBattleArcs.find(
({ firstBattleOrdinal, lastBattleOrdinal }) =>
battleOrdinal !== undefined && battleOrdinal >= firstBattleOrdinal && battleOrdinal <= lastBattleOrdinal
);
return {
skin: campSkinDefinitions[arc?.skinId ?? 'yellow-turban'],
source,
requestedBattleId: battleId,
battleOrdinal
};
}
function knownBattleId(battleId: string | undefined): BattleScenarioId | undefined {
return battleId && battleId in battleScenarios ? (battleId as BattleScenarioId) : undefined;
}

View File

@@ -1,8 +1,8 @@
import Phaser from 'phaser';
import storyMilitiaUrl from '../../assets/images/story/04-volunteer-militia.png';
import storyFirstSortieUrl from '../../assets/images/story/05-first-sortie.png';
import { soundDirector } from '../audio/SoundDirector';
import { battleUiIconFrames, battleUiIconTextureKey, loadBattleUiIcons, type BattleUiIconKey } from '../data/battleUiIcons';
import { selectCampSkin, type CampSkinSelection } from '../data/campSkins';
import {
equipmentExpToNext,
equipmentSlotLabels,
@@ -11061,6 +11061,26 @@ export class CampScene extends Phaser.Scene {
private readonly scaledCampUiObjects = new WeakSet<Phaser.GameObjects.GameObject>();
private campaign?: CampaignState;
private report?: FirstBattleReport;
private campSkinSelection?: CampSkinSelection;
private campSkinBackdrop?: Phaser.GameObjects.Image;
private campSkinWash?: Phaser.GameObjects.Rectangle;
private campSkinVignette?: Phaser.GameObjects.Rectangle;
private campSkinHeader?: Phaser.GameObjects.Rectangle;
private campSkinAccentLine?: Phaser.GameObjects.Rectangle;
private campSkinSeal?: Phaser.GameObjects.Arc;
private campSkinBadge?: Phaser.GameObjects.Rectangle;
private campSkinRenderedTextureKey?: string;
private campSkinUsedFallback = false;
private campSkinTransitionRevision = 0;
private campSkinTransitionCompletedRevision = 0;
private campSkinTransitionActive = false;
private campSkinTransitionLast?: {
kind: 'backdrop-fade';
durationMs: number;
skinId: CampSkinSelection['skin']['id'];
fromAlpha: number;
toAlpha: number;
};
private selectedUnitId = 'liu-bei';
private campRosterPage = 0;
private campRosterLayout?: CampRosterLayout;
@@ -11179,6 +11199,22 @@ export class CampScene extends Phaser.Scene {
this.campaign = getCampaignState();
this.report = this.campaign.firstBattleReport ?? getFirstBattleReport() ?? this.createFallbackReport();
this.ensureCurrentCampRecruitment();
const sortieFlow = this.currentSortieFlow();
this.campSkinSelection = selectCampSkin({
campaignStep: this.campaign.step,
nextBattleId: sortieFlow.nextBattleId,
currentBattleId: this.currentCampBattleId()
});
this.campSkinBackdrop = undefined;
this.campSkinWash = undefined;
this.campSkinVignette = undefined;
this.campSkinHeader = undefined;
this.campSkinAccentLine = undefined;
this.campSkinSeal = undefined;
this.campSkinBadge = undefined;
this.campSkinRenderedTextureKey = undefined;
this.campSkinUsedFallback = false;
this.campSkinTransitionActive = false;
this.selectedUnitId = this.currentUnits().find((unit) => unit.faction === 'ally')?.id ?? 'liu-bei';
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
this.sortieFormationAssignments = this.normalizedSortieFormationAssignments(this.campaign.sortieFormationAssignments);
@@ -11194,24 +11230,114 @@ export class CampScene extends Phaser.Scene {
private drawCampScene() {
const { width, height } = this.scale;
this.add.image(width / 2, height / 2, 'story-militia').setDisplaySize(width * 1.08, height * 1.08).setAlpha(0.62);
this.add.rectangle(0, 0, width, height, 0x06090d, 0.54).setOrigin(0);
this.scaleLegacyCampUi(this.add.rectangle(0, 0, campLegacyCanvasWidth, 84, 0x06090d, 0.52)).setOrigin(0);
const selection = this.campSkinSelection ?? selectCampSkin({
campaignStep: this.campaign?.step,
nextBattleId: this.currentSortieFlow().nextBattleId,
currentBattleId: this.currentCampBattleId()
});
this.campSkinSelection = selection;
const { skin } = selection;
const renderedTextureKey = this.textures.exists(skin.textureKey)
? skin.textureKey
: this.textures.exists('story-militia')
? 'story-militia'
: '__DEFAULT';
this.campSkinRenderedTextureKey = renderedTextureKey;
this.campSkinUsedFallback = renderedTextureKey !== skin.textureKey;
this.scaleLegacyCampUi(this.add.text(42, 28, this.currentCampTitle(), {
const backdrop = this.add.image(width / 2, height / 2, renderedTextureKey).setDepth(-30).setAlpha(0);
const source = backdrop.texture.getSourceImage() as { width?: number; height?: number };
const sourceWidth = Number(source?.width) || width;
const sourceHeight = Number(source?.height) || height;
const coverScale = Math.max(width / sourceWidth, height / sourceHeight) * 1.035;
backdrop.setScale(coverScale);
this.campSkinBackdrop = backdrop;
this.campSkinWash = this.add.rectangle(0, 0, width, height, skin.washColor, skin.washAlpha)
.setOrigin(0)
.setDepth(-29);
this.campSkinVignette = this.add.rectangle(width / 2, height / 2, width, height, 0x020407, 0.03)
.setStrokeStyle(Math.max(52, Math.round(height * 0.12)), 0x020407, 0.54)
.setDepth(-28);
this.add.text(width - 68, height - 30, skin.seal, {
fontFamily: '"Malgun Gothic", "Noto Serif KR", serif',
fontSize: `${Math.round(height * 0.15)}px`,
color: '#f4e4bf',
fontStyle: '700'
}).setOrigin(1, 1).setAlpha(0.075).setDepth(-27);
this.campSkinHeader = this.scaleLegacyCampUi(
this.add.rectangle(0, 0, campLegacyCanvasWidth, 84, skin.headerColor, 0.79)
).setOrigin(0).setDepth(-20);
this.campSkinAccentLine = this.scaleLegacyCampUi(
this.add.rectangle(0, 82, campLegacyCanvasWidth, 2, skin.accentColor, 0.9)
).setOrigin(0).setDepth(-19);
this.scaleLegacyCampUi(this.add.rectangle(0, 0, 5, 84, skin.accentColor, 0.94))
.setOrigin(0)
.setDepth(-19);
this.campSkinSeal = this.scaleLegacyCampUi(this.add.circle(48, 42, 24, skin.headerColor, 0.92))
.setStrokeStyle(2, skin.accentColor, 0.96)
.setDepth(-18);
this.scaleLegacyCampUi(this.add.text(48, 42, skin.seal, {
fontFamily: '"Malgun Gothic", "Noto Serif KR", serif',
fontSize: '26px',
color: '#f6e8c8',
fontStyle: '700',
stroke: '#05070a',
strokeThickness: 3
})).setOrigin(0.5).setDepth(-17);
this.scaleLegacyCampUi(this.add.text(88, 22, this.currentCampTitle(), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '30px',
fontSize: '28px',
color: '#f2e3bf',
fontStyle: '700',
stroke: '#05070a',
strokeThickness: 4
}));
this.scaleLegacyCampUi(this.add.text(42, 64, this.reportSummary(), {
this.scaleLegacyCampUi(this.add.text(88, 62, this.reportSummary(), {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '14px',
color: '#d4dce6'
color: '#d4dce6',
wordWrap: { width: 320, useAdvancedWrap: false }
}));
this.campSkinBadge = this.scaleLegacyCampUi(this.add.rectangle(476, 64, 112, 24, skin.headerColor, 0.9))
.setStrokeStyle(1, skin.accentColor, 0.88)
.setDepth(-18);
this.scaleLegacyCampUi(this.add.text(476, 64, skin.locationLabel, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '13px',
color: '#f3e6c7',
fontStyle: '700'
})).setOrigin(0.5).setDepth(-17);
const transitionDurationMs = 420;
const transitionRevision = ++this.campSkinTransitionRevision;
this.campSkinTransitionActive = true;
this.campSkinTransitionLast = {
kind: 'backdrop-fade',
durationMs: transitionDurationMs,
skinId: skin.id,
fromAlpha: 0,
toAlpha: skin.backdropAlpha
};
this.tweens.add({
targets: backdrop,
alpha: skin.backdropAlpha,
duration: transitionDurationMs,
ease: 'Sine.easeOut',
onComplete: () => {
if (this.campSkinBackdrop !== backdrop) {
return;
}
this.campSkinTransitionCompletedRevision = transitionRevision;
this.campSkinTransitionActive = false;
}
});
this.renderStaticButtons();
this.render();
if (this.openSortiePrepOnCreate) {
@@ -11221,6 +11347,12 @@ export class CampScene extends Phaser.Scene {
private ensureCampAssets(onReady: () => void) {
const useFirstSortieAssets = this.isFirstSortiePrepSlice();
const selection = this.campSkinSelection ?? selectCampSkin({
campaignStep: this.campaign?.step,
nextBattleId: this.currentSortieFlow().nextBattleId,
currentBattleId: this.currentCampBattleId()
});
this.campSkinSelection = selection;
const missingPortraits = this.currentUnits()
.flatMap((unit): PortraitAssetEntry[] => {
const portraitKey = campaignPortraitKeysByUnitId[unit.id];
@@ -11236,10 +11368,10 @@ export class CampScene extends Phaser.Scene {
})
.filter((entry, index, entries) => entries.findIndex((candidate) => candidate.textureKey === entry.textureKey) === index)
.filter((entry) => !this.textures.exists(entry.textureKey));
const missingMilitia = !this.textures.exists('story-militia');
const missingCampBackdrop = !this.textures.exists(selection.skin.textureKey);
const missingFirstSortieArtwork = useFirstSortieAssets && !this.textures.exists('story-first-sortie');
if (!missingMilitia && !missingFirstSortieArtwork && missingPortraits.length === 0) {
if (!missingCampBackdrop && !missingFirstSortieArtwork && missingPortraits.length === 0) {
onReady();
return;
}
@@ -11261,8 +11393,8 @@ export class CampScene extends Phaser.Scene {
console.warn(`Failed to load camp asset ${file.key}`);
});
if (missingMilitia) {
this.load.image('story-militia', storyMilitiaUrl);
if (missingCampBackdrop) {
this.load.image(selection.skin.textureKey, selection.skin.assetUrl);
}
if (missingFirstSortieArtwork) {
this.load.image('story-first-sortie', storyFirstSortieUrl);
@@ -12655,17 +12787,18 @@ export class CampScene extends Phaser.Scene {
}
private updateTabButtons() {
const accentColor = this.campSkinSelection?.skin.accentColor ?? palette.gold;
this.tabButtons.forEach(({ tab, bg, text }) => {
const active = this.activeTab === tab;
bg.setFillStyle(active ? 0x25384a : 0x182431, active ? 0.98 : 0.94);
bg.setStrokeStyle(1, active ? palette.gold : palette.blue, active ? 0.86 : 0.62);
bg.setStrokeStyle(1, active ? accentColor : palette.blue, active ? 0.9 : 0.62);
text.setColor(active ? '#fff2b8' : '#f2e3bf');
});
}
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, palette.gold, 0.8);
bg.setStrokeStyle(1, this.campSkinSelection?.skin.accentColor ?? palette.gold, 0.84);
bg.setInteractive({ useHandCursor: true });
bg.on('pointerover', () => bg.setFillStyle(0x283947, 0.98));
bg.on('pointerout', () => bg.setFillStyle(0x1a2630, 0.96));
@@ -20897,6 +21030,21 @@ export class CampScene extends Phaser.Scene {
return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
}
private campSkinObjectDebug(
object?: Phaser.GameObjects.Image | Phaser.GameObjects.Rectangle | Phaser.GameObjects.Arc
) {
if (!object?.active) {
return null;
}
const bounds = object.getBounds();
return {
bounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height },
alpha: object.alpha,
depth: object.depth,
visible: object.visible
};
}
getDebugState() {
const sortieChecklist = this.sortieChecklist();
const sortieScenario = this.nextSortieScenario();
@@ -21524,6 +21672,41 @@ export class CampScene extends Phaser.Scene {
campaignProgress: this.campaignTimelineProgress(),
campBattleId: this.currentCampBattleId(),
campTitle: this.currentCampTitle(),
campSkin: this.campSkinSelection
? {
id: this.campSkinSelection.skin.id,
chapterLabel: this.campSkinSelection.skin.chapterLabel,
locationLabel: this.campSkinSelection.skin.locationLabel,
sealGlyph: this.campSkinSelection.skin.seal,
selectionSource: this.campSkinSelection.source,
requestedBattleId: this.campSkinSelection.requestedBattleId ?? null,
battleOrdinal: this.campSkinSelection.battleOrdinal ?? null,
campaignStep: this.campaign?.step ?? null,
currentBattleId: this.currentCampBattleId(),
nextBattleId: this.currentSortieFlow().nextBattleId ?? null,
textureKey: this.campSkinSelection.skin.textureKey,
renderedTextureKey: this.campSkinRenderedTextureKey ?? null,
textureReady: this.textures.exists(this.campSkinSelection.skin.textureKey),
usedFallback: this.campSkinUsedFallback,
accentColor: this.campSkinSelection.skin.accentColor,
headerColor: this.campSkinSelection.skin.headerColor,
washColor: this.campSkinSelection.skin.washColor,
sceneBounds: { x: 0, y: 0, width: this.scale.width, height: this.scale.height },
backdrop: this.campSkinObjectDebug(this.campSkinBackdrop),
wash: this.campSkinObjectDebug(this.campSkinWash),
vignette: this.campSkinObjectDebug(this.campSkinVignette),
header: this.campSkinObjectDebug(this.campSkinHeader),
accentLine: this.campSkinObjectDebug(this.campSkinAccentLine),
seal: this.campSkinObjectDebug(this.campSkinSeal),
badge: this.campSkinObjectDebug(this.campSkinBadge),
transition: {
revision: this.campSkinTransitionRevision,
completedRevision: this.campSkinTransitionCompletedRevision,
active: this.campSkinTransitionActive,
last: this.campSkinTransitionLast ? { ...this.campSkinTransitionLast } : null
}
}
: null,
openSortiePrepOnCreate: this.openSortiePrepOnCreate,
skipIntroStoryForSortie: this.skipIntroStoryForSortie,
availableDialogueIds: this.availableCampDialogues().map((dialogue) => dialogue.id),