feat: add era-based camp skins
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user