feat: add era-based camp skins
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"verify:battle-save": "node scripts/verify-battle-save-normalization.mjs",
|
||||
"verify:sortie-synergy": "node scripts/verify-sortie-synergy.mjs",
|
||||
"verify:camp-rewards": "node scripts/verify-camp-reward-data.mjs",
|
||||
"verify:camp-skins": "node scripts/verify-camp-skin-data.mjs",
|
||||
"verify:campaign-recruits": "node scripts/verify-campaign-recruit-data.mjs",
|
||||
"verify:equipment-catalog": "node scripts/verify-equipment-catalog-data.mjs",
|
||||
"verify:inventory-rewards": "node scripts/verify-inventory-reward-data.mjs",
|
||||
|
||||
225
scripts/verify-camp-skin-data.mjs
Normal file
225
scripts/verify-camp-skin-data.mjs
Normal file
@@ -0,0 +1,225 @@
|
||||
import { createServer } from 'vite';
|
||||
|
||||
const expectedArcs = [
|
||||
{ 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 expectedTextureKeys = {
|
||||
'yellow-turban': 'story-militia-training-yard',
|
||||
'anti-dong': 'story-anti-dong-coalition-oath-camp',
|
||||
xuzhou: 'story-xuzhou-gate-council',
|
||||
wandering: 'story-wandering-night-camp',
|
||||
wolong: 'story-wolong-autumn-study',
|
||||
'red-cliffs': 'story-red-cliffs-wind-altar',
|
||||
'jing-yi': 'story-yizhou-council-at-river',
|
||||
'hanzhong-shuhan': 'story-yizhou-mountain-pass-command-post',
|
||||
'jingzhou-crisis': 'story-jingzhou-crisis-river-watchtowers',
|
||||
'yiling-baidi': 'story-yiling-baidi-lantern-vigil',
|
||||
nanzhong: 'story-nanzhong-captures-river-night-camp',
|
||||
northern: 'story-northern-supply-depot'
|
||||
};
|
||||
|
||||
const expectedSpecialSteps = {
|
||||
'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 errors = [];
|
||||
const server = await createServer({
|
||||
logLevel: 'error',
|
||||
server: { middlewareMode: true },
|
||||
appType: 'custom'
|
||||
});
|
||||
|
||||
try {
|
||||
const { campSkinDefinitions, campSkinBattleArcs, campSkinBattleOrdinal, selectCampSkin } =
|
||||
await server.ssrLoadModule('/src/game/data/campSkins.ts');
|
||||
const { storyBackgroundAssets } = await server.ssrLoadModule('/src/game/data/storyAssets.ts');
|
||||
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
|
||||
const expectedSkinIds = expectedArcs.map(({ skinId }) => skinId);
|
||||
const definitionEntries = Object.entries(campSkinDefinitions);
|
||||
const battleIds = Object.keys(battleScenarios);
|
||||
|
||||
assertEqual(definitionEntries.length, expectedSkinIds.length, 'camp skin definition count');
|
||||
assertEqual(campSkinBattleArcs.length, expectedArcs.length, 'camp skin arc count');
|
||||
assertEqual(battleIds.length, 66, 'battle count used by camp skin ordinals');
|
||||
|
||||
expectedSkinIds.forEach((skinId) => {
|
||||
const definition = campSkinDefinitions[skinId];
|
||||
assert(Boolean(definition), `missing camp skin definition "${skinId}"`);
|
||||
if (!definition) {
|
||||
return;
|
||||
}
|
||||
|
||||
assertEqual(definition.id, skinId, `${skinId}: id`);
|
||||
assertEqual(definition.textureKey, expectedTextureKeys[skinId], `${skinId}: texture key`);
|
||||
['textureKey', 'assetUrl', 'chapterLabel', 'locationLabel', 'seal'].forEach((fieldName) => {
|
||||
assertNonEmptyString(definition[fieldName], `${skinId}: ${fieldName}`);
|
||||
});
|
||||
['accentColor', 'headerColor', 'washColor'].forEach((fieldName) => {
|
||||
assertColor(definition[fieldName], `${skinId}: ${fieldName}`);
|
||||
});
|
||||
assertAlpha(definition.backdropAlpha, `${skinId}: backdropAlpha`);
|
||||
assertAlpha(definition.washAlpha, `${skinId}: washAlpha`);
|
||||
assert(
|
||||
Object.hasOwn(storyBackgroundAssets, definition.textureKey),
|
||||
`${skinId}: missing story background "${definition.textureKey}"`
|
||||
);
|
||||
assertEqual(
|
||||
definition.assetUrl,
|
||||
storyBackgroundAssets[definition.textureKey],
|
||||
`${skinId}: asset URL must match its story background`
|
||||
);
|
||||
});
|
||||
|
||||
definitionEntries.forEach(([skinId]) => {
|
||||
assert(expectedSkinIds.includes(skinId), `unexpected camp skin definition "${skinId}"`);
|
||||
});
|
||||
assertUnique(
|
||||
definitionEntries.map(([, definition]) => definition.textureKey),
|
||||
'camp skin texture keys'
|
||||
);
|
||||
assertUnique(
|
||||
definitionEntries.map(
|
||||
([, definition]) => `${definition.accentColor}:${definition.headerColor}:${definition.washColor}`
|
||||
),
|
||||
'camp skin visual color signatures'
|
||||
);
|
||||
|
||||
let nextExpectedOrdinal = 1;
|
||||
expectedArcs.forEach((expectedArc, index) => {
|
||||
const actualArc = campSkinBattleArcs[index];
|
||||
assert(Boolean(actualArc), `missing camp skin arc at index ${index}`);
|
||||
if (!actualArc) {
|
||||
return;
|
||||
}
|
||||
assertEqual(actualArc.skinId, expectedArc.skinId, `arc ${index}: skin id`);
|
||||
assertEqual(actualArc.firstBattleOrdinal, expectedArc.firstBattleOrdinal, `arc ${index}: first battle ordinal`);
|
||||
assertEqual(actualArc.lastBattleOrdinal, expectedArc.lastBattleOrdinal, `arc ${index}: last battle ordinal`);
|
||||
assertEqual(actualArc.firstBattleOrdinal, nextExpectedOrdinal, `arc ${index}: contiguous first battle ordinal`);
|
||||
assert(
|
||||
actualArc.lastBattleOrdinal >= actualArc.firstBattleOrdinal,
|
||||
`arc ${index}: last battle ordinal must not precede its first`
|
||||
);
|
||||
nextExpectedOrdinal = actualArc.lastBattleOrdinal + 1;
|
||||
});
|
||||
assertEqual(nextExpectedOrdinal, battleIds.length + 1, 'camp skin arcs must cover every battle ordinal');
|
||||
|
||||
battleIds.forEach((battleId, index) => {
|
||||
const battleOrdinal = index + 1;
|
||||
const expectedSkinId = expectedSkinForOrdinal(battleOrdinal);
|
||||
const nextBattleSelection = selectCampSkin({ nextBattleId: battleId });
|
||||
const currentBattleSelection = selectCampSkin({ currentBattleId: battleId });
|
||||
|
||||
assertEqual(campSkinBattleOrdinal(battleId), battleOrdinal, `${battleId}: battle ordinal lookup`);
|
||||
assertSelection(nextBattleSelection, {
|
||||
skinId: expectedSkinId,
|
||||
source: 'next-battle',
|
||||
requestedBattleId: battleId,
|
||||
battleOrdinal
|
||||
});
|
||||
assertSelection(currentBattleSelection, {
|
||||
skinId: expectedSkinId,
|
||||
source: 'current-battle',
|
||||
requestedBattleId: battleId,
|
||||
battleOrdinal
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(expectedSpecialSteps).forEach(([campaignStep, skinId]) => {
|
||||
const selection = selectCampSkin({ campaignStep, nextBattleId: battleIds[0], currentBattleId: battleIds.at(-1) });
|
||||
assertSelection(selection, {
|
||||
skinId,
|
||||
source: 'special-step',
|
||||
requestedBattleId: battleIds[0],
|
||||
battleOrdinal: 1
|
||||
});
|
||||
});
|
||||
|
||||
const currentBattleRecovery = selectCampSkin({ nextBattleId: 'unknown-battle', currentBattleId: battleIds.at(-1) });
|
||||
assertSelection(currentBattleRecovery, {
|
||||
skinId: 'northern',
|
||||
source: 'current-battle',
|
||||
requestedBattleId: battleIds.at(-1),
|
||||
battleOrdinal: battleIds.length
|
||||
});
|
||||
assertSelection(selectCampSkin({ nextBattleId: 'unknown-battle' }), {
|
||||
skinId: 'yellow-turban',
|
||||
source: 'fallback'
|
||||
});
|
||||
assertEqual(campSkinBattleOrdinal('unknown-battle'), undefined, 'unknown battle ordinal lookup');
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Camp skin data verification failed:\n${errors.map((error) => `- ${error}`).join('\n')}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Verified ${definitionEntries.length} camp skins, ${campSkinBattleArcs.length} contiguous era arcs, ` +
|
||||
`${battleIds.length} next/current battle selections, and ${Object.keys(expectedSpecialSteps).length} special-step overrides.`
|
||||
);
|
||||
} finally {
|
||||
await server.close();
|
||||
}
|
||||
|
||||
function expectedSkinForOrdinal(battleOrdinal) {
|
||||
return expectedArcs.find(
|
||||
({ firstBattleOrdinal, lastBattleOrdinal }) =>
|
||||
battleOrdinal >= firstBattleOrdinal && battleOrdinal <= lastBattleOrdinal
|
||||
)?.skinId;
|
||||
}
|
||||
|
||||
function assertSelection(selection, expected) {
|
||||
const context = `${expected.source}/${expected.requestedBattleId ?? expected.skinId}`;
|
||||
assert(Boolean(selection), `${context}: selection is missing`);
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
assertEqual(selection.skin?.id, expected.skinId, `${context}: selected skin`);
|
||||
assertEqual(selection.source, expected.source, `${context}: selection source`);
|
||||
assertEqual(selection.requestedBattleId, expected.requestedBattleId, `${context}: requested battle id`);
|
||||
assertEqual(selection.battleOrdinal, expected.battleOrdinal, `${context}: battle ordinal`);
|
||||
}
|
||||
|
||||
function assertNonEmptyString(value, context) {
|
||||
assert(typeof value === 'string' && value.trim().length > 0, `${context} must be a non-empty string`);
|
||||
}
|
||||
|
||||
function assertColor(value, context) {
|
||||
assert(Number.isInteger(value) && value >= 0 && value <= 0xffffff, `${context} must be a 24-bit integer color`);
|
||||
}
|
||||
|
||||
function assertAlpha(value, context) {
|
||||
assert(Number.isFinite(value) && value > 0 && value <= 1, `${context} must be greater than 0 and at most 1`);
|
||||
}
|
||||
|
||||
function assertUnique(values, context) {
|
||||
assertEqual(new Set(values).size, values.length, `${context} must be unique`);
|
||||
}
|
||||
|
||||
function assertEqual(actual, expected, context) {
|
||||
assert(Object.is(actual, expected), `${context}: expected ${format(expected)}, received ${format(actual)}`);
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
errors.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
function format(value) {
|
||||
return value === undefined ? 'undefined' : JSON.stringify(value);
|
||||
}
|
||||
@@ -2019,6 +2019,7 @@ try {
|
||||
|
||||
await clickLegacyUi(page, 738, 642);
|
||||
await waitForCampAfterBattleResult(page);
|
||||
await waitForCampSkinTransition(page, 'yellow-turban');
|
||||
await page.screenshot({ path: `${screenshotDir}/rc-first-camp.png`, fullPage: true });
|
||||
await assertCanvasPainted(page, 'first camp');
|
||||
|
||||
@@ -2035,6 +2036,7 @@ try {
|
||||
}
|
||||
});
|
||||
assert(firstCampProbe.state?.campaign?.step === 'first-camp', `Expected victory to return to first camp: ${JSON.stringify(firstCampProbe.state)}`);
|
||||
assertCampSkinState(firstCampProbe.state, 'yellow-turban', 'first camp');
|
||||
assert(
|
||||
firstCampProbe.state?.campRoster?.pageCount === 1 &&
|
||||
firstCampProbe.state.campRoster.totalCount === 3 &&
|
||||
@@ -5209,8 +5211,10 @@ try {
|
||||
await waitForTitle(page);
|
||||
await clickLegacyUi(page, 962, 310);
|
||||
await waitForCamp(page);
|
||||
await waitForCampSkinTransition(page, 'northern');
|
||||
const finalCampState = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||
assert(finalCampState?.campaign?.step === 'sixty-sixth-camp', `Expected final camp save to continue: ${JSON.stringify(finalCampState)}`);
|
||||
assertCampSkinState(finalCampState, 'northern', 'final camp');
|
||||
assert(finalCampState.sortieHasBattle === false, `Expected final camp to expose no playable sortie target: ${JSON.stringify(finalCampState)}`);
|
||||
assert(finalCampState.stagedSortiePrep === false, `Expected the non-battle final camp to avoid the three-stage sortie flow: ${JSON.stringify(finalCampState)}`);
|
||||
assert(finalCampState.nextSortieBattleId === null, `Expected final camp to route to ending instead of a battle: ${JSON.stringify(finalCampState)}`);
|
||||
@@ -7056,6 +7060,26 @@ async function waitForCamp(page) {
|
||||
}, undefined, { timeout: 90000 });
|
||||
}
|
||||
|
||||
async function waitForCampSkinTransition(page, expectedSkinId) {
|
||||
await page.waitForFunction((skinId) => {
|
||||
const activeScenes = window.__HEROS_DEBUG__?.activeScenes() ?? [];
|
||||
const campSkin = window.__HEROS_DEBUG__?.camp?.()?.campSkin;
|
||||
const transition = campSkin?.transition;
|
||||
return (
|
||||
activeScenes.includes('CampScene') &&
|
||||
campSkin?.id === skinId &&
|
||||
campSkin.textureReady === true &&
|
||||
Boolean(campSkin.renderedTextureKey) &&
|
||||
transition?.active === false &&
|
||||
transition.revision > 0 &&
|
||||
transition.completedRevision === transition.revision
|
||||
);
|
||||
}, expectedSkinId, { timeout: 30000 });
|
||||
await page.evaluate(() => new Promise((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(resolve));
|
||||
}));
|
||||
}
|
||||
|
||||
async function waitForCampVisualFrame(page, expectedPage) {
|
||||
await page.waitForFunction(
|
||||
(pageNumber) => window.__HEROS_DEBUG__?.camp()?.campRoster?.page === pageNumber,
|
||||
@@ -8410,6 +8434,68 @@ function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function assertCampSkinState(state, expectedSkinId, label) {
|
||||
const campSkin = state?.campSkin;
|
||||
const sceneBounds = campSkin?.sceneBounds;
|
||||
const transition = campSkin?.transition;
|
||||
const fullSceneLayers = [campSkin?.backdrop, campSkin?.wash, campSkin?.vignette];
|
||||
const ornamentLayers = [campSkin?.header, campSkin?.accentLine, campSkin?.seal, campSkin?.badge];
|
||||
|
||||
assert(
|
||||
campSkin?.id === expectedSkinId &&
|
||||
campSkin.textureReady === true &&
|
||||
typeof campSkin.textureKey === 'string' &&
|
||||
campSkin.textureKey.length > 0 &&
|
||||
campSkin.renderedTextureKey === campSkin.textureKey,
|
||||
`Expected ${label} to render the ${expectedSkinId} camp skin texture: ${JSON.stringify(campSkin)}`
|
||||
);
|
||||
assert(
|
||||
isFiniteBounds(sceneBounds) &&
|
||||
sceneBounds.x === 0 &&
|
||||
sceneBounds.y === 0 &&
|
||||
sceneBounds.width === baselineViewport.width &&
|
||||
sceneBounds.height === baselineViewport.height,
|
||||
`Expected ${label} camp skin to use the 1920x1080 scene bounds: ${JSON.stringify(campSkin)}`
|
||||
);
|
||||
assert(
|
||||
fullSceneLayers.every((view) =>
|
||||
isVisibleCampSkinView(view) && boundsCover(view.bounds, sceneBounds)
|
||||
),
|
||||
`Expected ${label} camp backdrop, color wash, and vignette to cover the FHD scene: ${JSON.stringify(campSkin)}`
|
||||
);
|
||||
assert(
|
||||
ornamentLayers.every((view) =>
|
||||
isVisibleCampSkinView(view) && boundsInside(view.bounds, sceneBounds)
|
||||
),
|
||||
`Expected ${label} camp header ornaments to remain inside the FHD scene: ${JSON.stringify(campSkin)}`
|
||||
);
|
||||
assert(
|
||||
transition?.active === false &&
|
||||
transition.revision > 0 &&
|
||||
transition.completedRevision === transition.revision &&
|
||||
transition.last?.kind === 'backdrop-fade' &&
|
||||
transition.last.skinId === expectedSkinId &&
|
||||
transition.last.durationMs >= 250 &&
|
||||
transition.last.durationMs <= 600 &&
|
||||
transition.last.fromAlpha === 0 &&
|
||||
transition.last.toAlpha > 0 &&
|
||||
transition.last.toAlpha <= 1 &&
|
||||
Math.abs(transition.last.toAlpha - campSkin.backdrop.alpha) <= 0.01,
|
||||
`Expected ${label} camp backdrop transition to finish cleanly: ${JSON.stringify(campSkin)}`
|
||||
);
|
||||
}
|
||||
|
||||
function isVisibleCampSkinView(view) {
|
||||
return Boolean(
|
||||
view?.visible === true &&
|
||||
Number.isFinite(view.alpha) &&
|
||||
view.alpha > 0 &&
|
||||
view.alpha <= 1 &&
|
||||
Number.isFinite(view.depth) &&
|
||||
isFiniteBounds(view.bounds)
|
||||
);
|
||||
}
|
||||
|
||||
function isFiniteBounds(bounds) {
|
||||
return Boolean(
|
||||
bounds &&
|
||||
@@ -8430,6 +8516,14 @@ function boundsInside(inner, outer, tolerance = 1) {
|
||||
inner.y + inner.height <= outer.y + outer.height + tolerance;
|
||||
}
|
||||
|
||||
function boundsCover(outer, inner, tolerance = 1) {
|
||||
return isFiniteBounds(outer) && isFiniteBounds(inner) &&
|
||||
outer.x <= inner.x + tolerance &&
|
||||
outer.y <= inner.y + tolerance &&
|
||||
outer.x + outer.width >= inner.x + inner.width - tolerance &&
|
||||
outer.y + outer.height >= inner.y + inner.height - tolerance;
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
|
||||
@@ -7,6 +7,7 @@ const checks = [
|
||||
'scripts/verify-sortie-synergy.mjs',
|
||||
'scripts/verify-battle-scenario-data.mjs',
|
||||
'scripts/verify-camp-reward-data.mjs',
|
||||
'scripts/verify-camp-skin-data.mjs',
|
||||
'scripts/verify-campaign-recruit-data.mjs',
|
||||
'scripts/verify-equipment-catalog-data.mjs',
|
||||
'scripts/verify-inventory-reward-data.mjs',
|
||||
|
||||
312
src/game/data/campSkins.ts
Normal file
312
src/game/data/campSkins.ts
Normal 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;
|
||||
}
|
||||
@@ -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