Expand campaign visual assets

This commit is contained in:
2026-06-25 10:52:16 +09:00
parent 30291fe0fa
commit 03ca89e03f
18 changed files with 254 additions and 300 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,69 @@
const portraitModules = import.meta.glob('../../assets/images/portraits/*.png', {
eager: true,
import: 'default'
}) as Record<string, string>;
const legacyPortraitSlugs: Record<string, string> = {
liuBei: 'liu-bei',
guanYu: 'guan-yu',
zhangFei: 'zhang-fei',
zhugeLiang: 'zhuge-liang',
zhaoYun: 'zhao-yun',
jiangWei: 'jiang-wei',
simaYi: 'sima-yi'
};
const speakerPortraitKeys: Record<string, string> = {
: 'liuBei',
: 'guanYu',
: 'zhangFei',
: 'zhugeLiang',
: 'zhaoYun',
: 'jiangWei',
: 'simaYi',
: 'mi-zhu',
: 'jian-yong',
: 'sun-qian',
: 'ma-liang',
: 'pang-tong',
: 'fa-zheng',
: 'huang-zhong',
: 'wei-yan',
: 'ma-chao',
: 'ma-dai',
: 'wang-ping',
: 'li-yan',
: 'wu-yi',
: 'yi-ji',
: 'gong-zhi',
: 'huang-quan',
: 'meng-huo',
: 'zhou-yu',
: 'cao-cao'
};
function fileSlugFromPath(path: string) {
const fileName = path.split('/').pop() ?? path;
return fileName.replace(/\.png$/i, '');
}
function camelToKebab(value: string) {
return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
}
export const portraitAssets: Record<string, string> = Object.fromEntries(
Object.entries(portraitModules).map(([path, url]) => [fileSlugFromPath(path), url])
);
export function portraitSlugForKey(key: string) {
return legacyPortraitSlugs[key] ?? camelToKebab(key);
}
export function portraitTextureKey(key: string) {
const slug = portraitSlugForKey(key);
return portraitAssets[slug] ? `portrait-${slug}` : undefined;
}
export function portraitKeyForSpeaker(speaker?: string) {
return speaker ? speakerPortraitKeys[speaker] : undefined;
}

View File

@@ -1,7 +1,7 @@
import type { TerrainType, UnitClassKey } from './battleRules';
import type { EquipmentSet } from './battleItems';
export type PortraitKey = 'liuBei' | 'guanYu' | 'zhangFei' | 'zhugeLiang' | 'zhaoYun' | 'jiangWei' | 'simaYi';
export type PortraitKey = string;
export type StoryPage = {
id: string;

View File

@@ -1,68 +1,54 @@
import storyChaosUrl from '../../assets/images/story/01-yellow-turban-chaos.png';
import storyLiuBeiUrl from '../../assets/images/story/02-liu-bei-resolve.png';
import storyThreeHeroesUrl from '../../assets/images/story/03-three-heroes-meet.png';
import storyMilitiaUrl from '../../assets/images/story/04-volunteer-militia.png';
import storySortieUrl from '../../assets/images/story/05-first-sortie.png';
import storyYellowPursuitUrl from '../../assets/images/story/06-yellow-turban-pursuit.png';
import storyAntiDongPassUrl from '../../assets/images/story/07-anti-dong-pass.png';
import storyXuzhouHandoverUrl from '../../assets/images/story/08-xuzhou-handover.png';
import storyWanderingRoadUrl from '../../assets/images/story/09-wandering-road.png';
import storyWolongCottageUrl from '../../assets/images/story/10-wolong-cottage.png';
import storyRedCliffsFireUrl from '../../assets/images/story/11-red-cliffs-fire.png';
import storyYizhouMountainPassUrl from '../../assets/images/story/12-yizhou-mountain-pass.png';
import storyNorthernExpeditionUrl from '../../assets/images/story/13-northern-expedition.png';
import storyNanzhongPacificationUrl from '../../assets/images/story/14-nanzhong-pacification.png';
import storyYilingBaidiAftermathUrl from '../../assets/images/story/15-yiling-baidi-aftermath.png';
import storyQishanRenewedOffensiveUrl from '../../assets/images/story/16-qishan-renewed-offensive.png';
import storyTianshuiFrontUrl from '../../assets/images/story/17-tianshui-front.png';
import storyJietingCrisisUrl from '../../assets/images/story/18-jieting-crisis.png';
import storyChencangSiegeUrl from '../../assets/images/story/19-chencang-siege.png';
import storyWuduYinpingUrl from '../../assets/images/story/20-wudu-yinping.png';
import storyHanzhongRainDefenseUrl from '../../assets/images/story/21-hanzhong-rain-defense.png';
import storyLuchengPursuitUrl from '../../assets/images/story/22-lucheng-pursuit.png';
import storyWeishuiCampsUrl from '../../assets/images/story/23-weishui-camps.png';
import storyWeishuiNorthbankUrl from '../../assets/images/story/24-weishui-northbank.png';
import storyYizhouLuoRoadUrl from '../../assets/images/story/25-yizhou-luo-road.png';
import storyChengduSurrenderUrl from '../../assets/images/story/26-chengdu-surrender.png';
import storyJingzhouCrisisUrl from '../../assets/images/story/27-jingzhou-crisis.png';
import storyNanzhongCapturesUrl from '../../assets/images/story/28-nanzhong-seven-captures.png';
import storyFanCastleFloodUrl from '../../assets/images/story/29-fan-castle-flood.png';
import storyMaichengIsolationUrl from '../../assets/images/story/30-maicheng-isolation.png';
import storyYilingFireAttackUrl from '../../assets/images/story/31-yiling-fire-attack.png';
import storyBaidiEntrustmentUrl from '../../assets/images/story/32-baidi-entrustment.png';
const storyModules = import.meta.glob('../../assets/images/story/*.png', {
eager: true,
import: 'default'
}) as Record<string, string>;
export const storyBackgroundAssets: Record<string, string> = {
'story-chaos': storyChaosUrl,
'story-liu-bei': storyLiuBeiUrl,
'story-three-heroes': storyThreeHeroesUrl,
'story-militia': storyMilitiaUrl,
'story-sortie': storySortieUrl,
'story-yellow-pursuit': storyYellowPursuitUrl,
'story-anti-dong': storyAntiDongPassUrl,
'story-xuzhou': storyXuzhouHandoverUrl,
'story-wandering': storyWanderingRoadUrl,
'story-wolong': storyWolongCottageUrl,
'story-red-cliffs': storyRedCliffsFireUrl,
'story-yizhou': storyYizhouMountainPassUrl,
'story-northern': storyNorthernExpeditionUrl,
'story-nanzhong': storyNanzhongPacificationUrl,
'story-yiling-baidi': storyYilingBaidiAftermathUrl,
'story-qishan-renewed': storyQishanRenewedOffensiveUrl,
'story-tianshui-front': storyTianshuiFrontUrl,
'story-jieting-crisis': storyJietingCrisisUrl,
'story-chencang-siege': storyChencangSiegeUrl,
'story-wudu-yinping': storyWuduYinpingUrl,
'story-hanzhong-rain': storyHanzhongRainDefenseUrl,
'story-lucheng-pursuit': storyLuchengPursuitUrl,
'story-weishui-camps': storyWeishuiCampsUrl,
'story-weishui-northbank': storyWeishuiNorthbankUrl,
'story-yizhou-luo-road': storyYizhouLuoRoadUrl,
'story-chengdu-surrender': storyChengduSurrenderUrl,
'story-jingzhou-crisis': storyJingzhouCrisisUrl,
'story-nanzhong-captures': storyNanzhongCapturesUrl,
'story-fan-castle-flood': storyFanCastleFloodUrl,
'story-maicheng-isolation': storyMaichengIsolationUrl,
'story-yiling-fire-attack': storyYilingFireAttackUrl,
'story-baidi-entrustment': storyBaidiEntrustmentUrl,
'story-resolve': storyLiuBeiUrl
const storyAssetAliases: Record<string, string> = {
'story-chaos': 'story-yellow-turban-chaos',
'story-liu-bei': 'story-liu-bei-resolve',
'story-three-heroes': 'story-three-heroes-meet',
'story-militia': 'story-volunteer-militia',
'story-sortie': 'story-first-sortie',
'story-yellow-pursuit': 'story-yellow-turban-pursuit',
'story-anti-dong': 'story-anti-dong-pass',
'story-xuzhou': 'story-xuzhou-handover',
'story-wandering': 'story-wandering-road',
'story-wolong': 'story-wolong-cottage',
'story-red-cliffs': 'story-red-cliffs-fire',
'story-yizhou': 'story-yizhou-mountain-pass',
'story-northern': 'story-northern-expedition',
'story-nanzhong': 'story-nanzhong-pacification',
'story-yiling-baidi': 'story-yiling-baidi-aftermath',
'story-qishan-renewed': 'story-qishan-renewed-offensive',
'story-hanzhong-rain': 'story-hanzhong-rain-defense',
'story-nanzhong-captures': 'story-nanzhong-seven-captures',
'story-resolve': 'story-liu-bei-resolve'
};
function storyKeyFromPath(path: string) {
const fileName = path.split('/').pop() ?? path;
const slug = fileName.replace(/\.png$/i, '').replace(/^\d+-/, '');
return `story-${slug}`;
}
const discoveredStoryAssets = Object.fromEntries(
Object.entries(storyModules).map(([path, url]) => [storyKeyFromPath(path), url])
);
export const storyBackgroundAssets: Record<string, string> = { ...discoveredStoryAssets };
Object.entries(storyAssetAliases).forEach(([alias, target]) => {
const targetUrl = storyBackgroundAssets[target];
if (targetUrl) {
storyBackgroundAssets[alias] = targetUrl;
}
});
export function storyBackgroundKeysFor(baseKey: string) {
const prefix = `${baseKey}-`;
const keys = Object.keys(storyBackgroundAssets)
.filter((key) => key === baseKey || key.startsWith(prefix))
.sort();
return Array.from(new Set(keys));
}

View File

@@ -8266,7 +8266,7 @@ export class BattleScene extends Phaser.Scene {
unit.id.startsWith('northern-second-cavalry') ||
unit.id.startsWith('northern-third-cavalry')
) {
return 'unit-rebel-cavalry';
return this.resolveUnitTextureVariant('unit-rebel-cavalry', unit.id);
}
if (
unit.id.startsWith('northern-first-archer') ||
@@ -8276,7 +8276,7 @@ export class BattleScene extends Phaser.Scene {
unit.id.startsWith('northern-second-strategist') ||
unit.id.startsWith('northern-third-strategist')
) {
return 'unit-rebel-archer';
return this.resolveUnitTextureVariant('unit-rebel-archer', unit.id);
}
if (
unit.id.startsWith('northern-first-officer') ||
@@ -8286,9 +8286,9 @@ export class BattleScene extends Phaser.Scene {
unit.id === 'northern-second-leader-guo-huai' ||
unit.id === 'northern-third-leader-zhang-he'
) {
return 'unit-rebel-leader';
return this.resolveUnitTextureVariant('unit-rebel-leader', unit.id);
}
return unitTexture[unit.id] ?? unitTextureByClass[unit.classKey] ?? 'unit-rebel';
return this.resolveUnitTextureVariant(unitTexture[unit.id] ?? unitTextureByClass[unit.classKey] ?? 'unit-rebel', unit.id);
}
private signatureUnitTextureKey(unit: UnitData) {
@@ -8306,65 +8306,90 @@ export class BattleScene extends Phaser.Scene {
if (coreAllyTextureIds.has(unit.id)) {
return undefined;
}
return this.unitTextureForClass('shu', unit.classKey);
return this.unitTextureForClass('shu', unit.classKey, unit.id);
}
if (earlyRebelTexturePrefixes.some((prefix) => unit.id.startsWith(prefix))) {
return undefined;
}
if (nanmanTexturePrefixes.some((prefix) => unit.id.startsWith(prefix))) {
return this.unitTextureForClass('nanman', unit.classKey);
return this.unitTextureForClass('nanman', unit.classKey, unit.id);
}
if (wuTexturePrefixes.some((prefix) => unit.id.startsWith(prefix))) {
return this.unitTextureForClass('wu', unit.classKey);
return this.unitTextureForClass('wu', unit.classKey, unit.id);
}
return this.unitTextureForClass('wei', unit.classKey);
return this.unitTextureForClass('wei', unit.classKey, unit.id);
}
private unitTextureForClass(style: 'shu' | 'wei' | 'wu' | 'nanman', classKey: UnitClassKey) {
private unitTextureForClass(style: 'shu' | 'wei' | 'wu' | 'nanman', classKey: UnitClassKey, seed: string) {
let baseKey: string;
if (style === 'shu') {
if (classKey === 'spearman') {
return 'unit-shu-spearman';
baseKey = 'unit-shu-spearman';
} else if (classKey === 'archer') {
baseKey = 'unit-shu-archer';
} else if (classKey === 'cavalry') {
baseKey = 'unit-shu-cavalry';
} else if (classKey === 'strategist' || classKey === 'quartermaster') {
baseKey = 'unit-shu-strategist';
} else if (classKey === 'rebelLeader') {
baseKey = 'unit-shu-officer';
} else {
baseKey = 'unit-shu-infantry';
}
if (classKey === 'archer') {
return 'unit-shu-archer';
}
if (classKey === 'cavalry') {
return 'unit-shu-cavalry';
}
if (classKey === 'strategist' || classKey === 'quartermaster') {
return 'unit-shu-strategist';
}
if (classKey === 'rebelLeader') {
return 'unit-shu-officer';
}
return 'unit-shu-infantry';
}
if (style === 'nanman') {
} else if (style === 'nanman') {
if (classKey === 'archer' || classKey === 'strategist' || classKey === 'quartermaster') {
return 'unit-nanman-shaman';
baseKey = 'unit-nanman-shaman';
} else if (classKey === 'rebelLeader' || classKey === 'cavalry') {
baseKey = 'unit-nanman-officer';
} else {
baseKey = 'unit-nanman-infantry';
}
if (classKey === 'rebelLeader' || classKey === 'cavalry') {
return 'unit-nanman-officer';
}
return 'unit-nanman-infantry';
} else if (classKey === 'archer') {
baseKey = `unit-${style}-archer`;
} else if (classKey === 'cavalry') {
baseKey = `unit-${style}-cavalry`;
} else if (classKey === 'strategist' || classKey === 'quartermaster') {
baseKey = `unit-${style}-strategist`;
} else if (classKey === 'rebelLeader') {
baseKey = `unit-${style}-officer`;
} else {
baseKey = `unit-${style}-infantry`;
}
if (classKey === 'archer') {
return `unit-${style}-archer`;
return this.resolveUnitTextureVariant(baseKey, seed);
}
private resolveUnitTextureVariant(baseKey: string, seed: string) {
const variantKeys = this.unitTextureVariantKeys(baseKey);
if (variantKeys.length <= 1) {
return baseKey;
}
if (classKey === 'cavalry') {
return `unit-${style}-cavalry`;
return variantKeys[this.hashString(seed) % variantKeys.length];
}
private unitTextureVariantKeys(baseKey: string) {
const prefix = `${baseKey}-`;
return this.textures
.getTextureKeys()
.filter((key) => {
if (key === baseKey) {
return true;
}
return key.startsWith(prefix) && !key.endsWith('-actions') && this.textures.exists(`${key}-actions`);
})
.sort();
}
private hashString(value: string) {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
}
if (classKey === 'strategist' || classKey === 'quartermaster') {
return `unit-${style}-strategist`;
}
if (classKey === 'rebelLeader') {
return `unit-${style}-officer`;
}
return `unit-${style}-infantry`;
return hash;
}
private unitActionTexture(unit: UnitData) {

View File

@@ -1,108 +1,6 @@
import Phaser from 'phaser';
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
import jiangWeiPortraitUrl from '../../assets/images/portraits/jiang-wei.png';
import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png';
import simaYiPortraitUrl from '../../assets/images/portraits/sima-yi.png';
import zhaoYunPortraitUrl from '../../assets/images/portraits/zhao-yun.png';
import zhangFeiPortraitUrl from '../../assets/images/portraits/zhang-fei.png';
import zhugeLiangPortraitUrl from '../../assets/images/portraits/zhuge-liang.png';
import titleBackgroundUrl from '../../assets/images/taoyuan-oath-title.png';
import caoCaoActionSheetUrl from '../../assets/images/units/unit-cao-cao-actions.png';
import caoCaoUnitSheetUrl from '../../assets/images/units/unit-cao-cao.png';
import faZhengActionSheetUrl from '../../assets/images/units/unit-fa-zheng-actions.png';
import faZhengUnitSheetUrl from '../../assets/images/units/unit-fa-zheng.png';
import gongZhiActionSheetUrl from '../../assets/images/units/unit-gong-zhi-actions.png';
import gongZhiUnitSheetUrl from '../../assets/images/units/unit-gong-zhi.png';
import guanYuActionSheetUrl from '../../assets/images/units/unit-guan-yu-actions.png';
import guanYuUnitSheetUrl from '../../assets/images/units/unit-guan-yu.png';
import huangQuanActionSheetUrl from '../../assets/images/units/unit-huang-quan-actions.png';
import huangQuanUnitSheetUrl from '../../assets/images/units/unit-huang-quan.png';
import huangZhongActionSheetUrl from '../../assets/images/units/unit-huang-zhong-actions.png';
import huangZhongUnitSheetUrl from '../../assets/images/units/unit-huang-zhong.png';
import jiangWeiActionSheetUrl from '../../assets/images/units/unit-jiang-wei-actions.png';
import jiangWeiUnitSheetUrl from '../../assets/images/units/unit-jiang-wei.png';
import liYanActionSheetUrl from '../../assets/images/units/unit-li-yan-actions.png';
import liYanUnitSheetUrl from '../../assets/images/units/unit-li-yan.png';
import liuBeiActionSheetUrl from '../../assets/images/units/unit-liu-bei-actions.png';
import liuBeiUnitSheetUrl from '../../assets/images/units/unit-liu-bei.png';
import luBuActionSheetUrl from '../../assets/images/units/unit-lu-bu-actions.png';
import luBuUnitSheetUrl from '../../assets/images/units/unit-lu-bu.png';
import luMengActionSheetUrl from '../../assets/images/units/unit-lu-meng-actions.png';
import luMengUnitSheetUrl from '../../assets/images/units/unit-lu-meng.png';
import maChaoActionSheetUrl from '../../assets/images/units/unit-ma-chao-actions.png';
import maChaoUnitSheetUrl from '../../assets/images/units/unit-ma-chao.png';
import maDaiActionSheetUrl from '../../assets/images/units/unit-ma-dai-actions.png';
import maDaiUnitSheetUrl from '../../assets/images/units/unit-ma-dai.png';
import maLiangActionSheetUrl from '../../assets/images/units/unit-ma-liang-actions.png';
import maLiangUnitSheetUrl from '../../assets/images/units/unit-ma-liang.png';
import mengHuoActionSheetUrl from '../../assets/images/units/unit-meng-huo-actions.png';
import mengHuoUnitSheetUrl from '../../assets/images/units/unit-meng-huo.png';
import pangTongActionSheetUrl from '../../assets/images/units/unit-pang-tong-actions.png';
import pangTongUnitSheetUrl from '../../assets/images/units/unit-pang-tong.png';
import rebelArcherActionSheetUrl from '../../assets/images/units/unit-rebel-archer-actions.png';
import rebelArcherUnitSheetUrl from '../../assets/images/units/unit-rebel-archer.png';
import rebelCavalryActionSheetUrl from '../../assets/images/units/unit-rebel-cavalry-actions.png';
import rebelCavalryUnitSheetUrl from '../../assets/images/units/unit-rebel-cavalry.png';
import rebelLeaderActionSheetUrl from '../../assets/images/units/unit-rebel-leader-actions.png';
import rebelLeaderUnitSheetUrl from '../../assets/images/units/unit-rebel-leader.png';
import rebelActionSheetUrl from '../../assets/images/units/unit-rebel-actions.png';
import rebelUnitSheetUrl from '../../assets/images/units/unit-rebel.png';
import nanmanInfantryActionSheetUrl from '../../assets/images/units/unit-nanman-infantry-actions.png';
import nanmanInfantryUnitSheetUrl from '../../assets/images/units/unit-nanman-infantry.png';
import nanmanOfficerActionSheetUrl from '../../assets/images/units/unit-nanman-officer-actions.png';
import nanmanOfficerUnitSheetUrl from '../../assets/images/units/unit-nanman-officer.png';
import nanmanShamanActionSheetUrl from '../../assets/images/units/unit-nanman-shaman-actions.png';
import nanmanShamanUnitSheetUrl from '../../assets/images/units/unit-nanman-shaman.png';
import shuArcherActionSheetUrl from '../../assets/images/units/unit-shu-archer-actions.png';
import shuArcherUnitSheetUrl from '../../assets/images/units/unit-shu-archer.png';
import shuCavalryActionSheetUrl from '../../assets/images/units/unit-shu-cavalry-actions.png';
import shuCavalryUnitSheetUrl from '../../assets/images/units/unit-shu-cavalry.png';
import shuInfantryActionSheetUrl from '../../assets/images/units/unit-shu-infantry-actions.png';
import shuInfantryUnitSheetUrl from '../../assets/images/units/unit-shu-infantry.png';
import shuOfficerActionSheetUrl from '../../assets/images/units/unit-shu-officer-actions.png';
import shuOfficerUnitSheetUrl from '../../assets/images/units/unit-shu-officer.png';
import shuSpearmanActionSheetUrl from '../../assets/images/units/unit-shu-spearman-actions.png';
import shuSpearmanUnitSheetUrl from '../../assets/images/units/unit-shu-spearman.png';
import shuStrategistActionSheetUrl from '../../assets/images/units/unit-shu-strategist-actions.png';
import shuStrategistUnitSheetUrl from '../../assets/images/units/unit-shu-strategist.png';
import simaYiActionSheetUrl from '../../assets/images/units/unit-sima-yi-actions.png';
import simaYiUnitSheetUrl from '../../assets/images/units/unit-sima-yi.png';
import wangPingActionSheetUrl from '../../assets/images/units/unit-wang-ping-actions.png';
import wangPingUnitSheetUrl from '../../assets/images/units/unit-wang-ping.png';
import weiArcherActionSheetUrl from '../../assets/images/units/unit-wei-archer-actions.png';
import weiArcherUnitSheetUrl from '../../assets/images/units/unit-wei-archer.png';
import weiCavalryActionSheetUrl from '../../assets/images/units/unit-wei-cavalry-actions.png';
import weiCavalryUnitSheetUrl from '../../assets/images/units/unit-wei-cavalry.png';
import weiInfantryActionSheetUrl from '../../assets/images/units/unit-wei-infantry-actions.png';
import weiInfantryUnitSheetUrl from '../../assets/images/units/unit-wei-infantry.png';
import weiOfficerActionSheetUrl from '../../assets/images/units/unit-wei-officer-actions.png';
import weiOfficerUnitSheetUrl from '../../assets/images/units/unit-wei-officer.png';
import weiStrategistActionSheetUrl from '../../assets/images/units/unit-wei-strategist-actions.png';
import weiStrategistUnitSheetUrl from '../../assets/images/units/unit-wei-strategist.png';
import weiYanActionSheetUrl from '../../assets/images/units/unit-wei-yan-actions.png';
import weiYanUnitSheetUrl from '../../assets/images/units/unit-wei-yan.png';
import wuArcherActionSheetUrl from '../../assets/images/units/unit-wu-archer-actions.png';
import wuArcherUnitSheetUrl from '../../assets/images/units/unit-wu-archer.png';
import wuCavalryActionSheetUrl from '../../assets/images/units/unit-wu-cavalry-actions.png';
import wuCavalryUnitSheetUrl from '../../assets/images/units/unit-wu-cavalry.png';
import wuInfantryActionSheetUrl from '../../assets/images/units/unit-wu-infantry-actions.png';
import wuInfantryUnitSheetUrl from '../../assets/images/units/unit-wu-infantry.png';
import wuOfficerActionSheetUrl from '../../assets/images/units/unit-wu-officer-actions.png';
import wuOfficerUnitSheetUrl from '../../assets/images/units/unit-wu-officer.png';
import wuStrategistActionSheetUrl from '../../assets/images/units/unit-wu-strategist-actions.png';
import wuStrategistUnitSheetUrl from '../../assets/images/units/unit-wu-strategist.png';
import wuYiActionSheetUrl from '../../assets/images/units/unit-wu-yi-actions.png';
import wuYiUnitSheetUrl from '../../assets/images/units/unit-wu-yi.png';
import yanYanActionSheetUrl from '../../assets/images/units/unit-yan-yan-actions.png';
import yanYanUnitSheetUrl from '../../assets/images/units/unit-yan-yan.png';
import yiJiActionSheetUrl from '../../assets/images/units/unit-yi-ji-actions.png';
import yiJiUnitSheetUrl from '../../assets/images/units/unit-yi-ji.png';
import zhangFeiActionSheetUrl from '../../assets/images/units/unit-zhang-fei-actions.png';
import zhangFeiUnitSheetUrl from '../../assets/images/units/unit-zhang-fei.png';
import zhaoYunActionSheetUrl from '../../assets/images/units/unit-zhao-yun-actions.png';
import zhaoYunUnitSheetUrl from '../../assets/images/units/unit-zhao-yun.png';
import zhugeLiangActionSheetUrl from '../../assets/images/units/unit-zhuge-liang-actions.png';
import zhugeLiangUnitSheetUrl from '../../assets/images/units/unit-zhuge-liang.png';
import { portraitAssets } from '../data/portraitAssets';
import { storyBackgroundAssets } from '../data/storyAssets';
type UnitDirection = 'south' | 'east' | 'north' | 'west';
@@ -118,56 +16,37 @@ const unitSheetRows: Record<UnitDirection, number> = {
west: 3
};
const sourceUnitSheets = [
{ key: 'unit-liu-bei', url: liuBeiUnitSheetUrl, actionKey: 'unit-liu-bei-actions', actionUrl: liuBeiActionSheetUrl },
{ key: 'unit-guan-yu', url: guanYuUnitSheetUrl, actionKey: 'unit-guan-yu-actions', actionUrl: guanYuActionSheetUrl },
{ key: 'unit-zhang-fei', url: zhangFeiUnitSheetUrl, actionKey: 'unit-zhang-fei-actions', actionUrl: zhangFeiActionSheetUrl },
{ key: 'unit-rebel', url: rebelUnitSheetUrl, actionKey: 'unit-rebel-actions', actionUrl: rebelActionSheetUrl },
{ key: 'unit-rebel-archer', url: rebelArcherUnitSheetUrl, actionKey: 'unit-rebel-archer-actions', actionUrl: rebelArcherActionSheetUrl },
{ key: 'unit-rebel-cavalry', url: rebelCavalryUnitSheetUrl, actionKey: 'unit-rebel-cavalry-actions', actionUrl: rebelCavalryActionSheetUrl },
{ key: 'unit-rebel-leader', url: rebelLeaderUnitSheetUrl, actionKey: 'unit-rebel-leader-actions', actionUrl: rebelLeaderActionSheetUrl },
{ key: 'unit-lu-bu', url: luBuUnitSheetUrl, actionKey: 'unit-lu-bu-actions', actionUrl: luBuActionSheetUrl },
{ key: 'unit-cao-cao', url: caoCaoUnitSheetUrl, actionKey: 'unit-cao-cao-actions', actionUrl: caoCaoActionSheetUrl },
{ key: 'unit-lu-meng', url: luMengUnitSheetUrl, actionKey: 'unit-lu-meng-actions', actionUrl: luMengActionSheetUrl },
{ key: 'unit-meng-huo', url: mengHuoUnitSheetUrl, actionKey: 'unit-meng-huo-actions', actionUrl: mengHuoActionSheetUrl },
{ key: 'unit-zhao-yun', url: zhaoYunUnitSheetUrl, actionKey: 'unit-zhao-yun-actions', actionUrl: zhaoYunActionSheetUrl },
{ key: 'unit-zhuge-liang', url: zhugeLiangUnitSheetUrl, actionKey: 'unit-zhuge-liang-actions', actionUrl: zhugeLiangActionSheetUrl },
{ key: 'unit-jiang-wei', url: jiangWeiUnitSheetUrl, actionKey: 'unit-jiang-wei-actions', actionUrl: jiangWeiActionSheetUrl },
{ key: 'unit-sima-yi', url: simaYiUnitSheetUrl, actionKey: 'unit-sima-yi-actions', actionUrl: simaYiActionSheetUrl },
{ key: 'unit-ma-liang', url: maLiangUnitSheetUrl, actionKey: 'unit-ma-liang-actions', actionUrl: maLiangActionSheetUrl },
{ key: 'unit-yi-ji', url: yiJiUnitSheetUrl, actionKey: 'unit-yi-ji-actions', actionUrl: yiJiActionSheetUrl },
{ key: 'unit-gong-zhi', url: gongZhiUnitSheetUrl, actionKey: 'unit-gong-zhi-actions', actionUrl: gongZhiActionSheetUrl },
{ key: 'unit-huang-zhong', url: huangZhongUnitSheetUrl, actionKey: 'unit-huang-zhong-actions', actionUrl: huangZhongActionSheetUrl },
{ key: 'unit-wei-yan', url: weiYanUnitSheetUrl, actionKey: 'unit-wei-yan-actions', actionUrl: weiYanActionSheetUrl },
{ key: 'unit-pang-tong', url: pangTongUnitSheetUrl, actionKey: 'unit-pang-tong-actions', actionUrl: pangTongActionSheetUrl },
{ key: 'unit-fa-zheng', url: faZhengUnitSheetUrl, actionKey: 'unit-fa-zheng-actions', actionUrl: faZhengActionSheetUrl },
{ key: 'unit-wu-yi', url: wuYiUnitSheetUrl, actionKey: 'unit-wu-yi-actions', actionUrl: wuYiActionSheetUrl },
{ key: 'unit-yan-yan', url: yanYanUnitSheetUrl, actionKey: 'unit-yan-yan-actions', actionUrl: yanYanActionSheetUrl },
{ key: 'unit-li-yan', url: liYanUnitSheetUrl, actionKey: 'unit-li-yan-actions', actionUrl: liYanActionSheetUrl },
{ key: 'unit-huang-quan', url: huangQuanUnitSheetUrl, actionKey: 'unit-huang-quan-actions', actionUrl: huangQuanActionSheetUrl },
{ key: 'unit-ma-chao', url: maChaoUnitSheetUrl, actionKey: 'unit-ma-chao-actions', actionUrl: maChaoActionSheetUrl },
{ key: 'unit-ma-dai', url: maDaiUnitSheetUrl, actionKey: 'unit-ma-dai-actions', actionUrl: maDaiActionSheetUrl },
{ key: 'unit-wang-ping', url: wangPingUnitSheetUrl, actionKey: 'unit-wang-ping-actions', actionUrl: wangPingActionSheetUrl },
{ key: 'unit-shu-infantry', url: shuInfantryUnitSheetUrl, actionKey: 'unit-shu-infantry-actions', actionUrl: shuInfantryActionSheetUrl },
{ key: 'unit-shu-spearman', url: shuSpearmanUnitSheetUrl, actionKey: 'unit-shu-spearman-actions', actionUrl: shuSpearmanActionSheetUrl },
{ key: 'unit-shu-archer', url: shuArcherUnitSheetUrl, actionKey: 'unit-shu-archer-actions', actionUrl: shuArcherActionSheetUrl },
{ key: 'unit-shu-cavalry', url: shuCavalryUnitSheetUrl, actionKey: 'unit-shu-cavalry-actions', actionUrl: shuCavalryActionSheetUrl },
{ key: 'unit-shu-strategist', url: shuStrategistUnitSheetUrl, actionKey: 'unit-shu-strategist-actions', actionUrl: shuStrategistActionSheetUrl },
{ key: 'unit-shu-officer', url: shuOfficerUnitSheetUrl, actionKey: 'unit-shu-officer-actions', actionUrl: shuOfficerActionSheetUrl },
{ key: 'unit-wei-infantry', url: weiInfantryUnitSheetUrl, actionKey: 'unit-wei-infantry-actions', actionUrl: weiInfantryActionSheetUrl },
{ key: 'unit-wei-archer', url: weiArcherUnitSheetUrl, actionKey: 'unit-wei-archer-actions', actionUrl: weiArcherActionSheetUrl },
{ key: 'unit-wei-cavalry', url: weiCavalryUnitSheetUrl, actionKey: 'unit-wei-cavalry-actions', actionUrl: weiCavalryActionSheetUrl },
{ key: 'unit-wei-strategist', url: weiStrategistUnitSheetUrl, actionKey: 'unit-wei-strategist-actions', actionUrl: weiStrategistActionSheetUrl },
{ key: 'unit-wei-officer', url: weiOfficerUnitSheetUrl, actionKey: 'unit-wei-officer-actions', actionUrl: weiOfficerActionSheetUrl },
{ key: 'unit-wu-infantry', url: wuInfantryUnitSheetUrl, actionKey: 'unit-wu-infantry-actions', actionUrl: wuInfantryActionSheetUrl },
{ key: 'unit-wu-archer', url: wuArcherUnitSheetUrl, actionKey: 'unit-wu-archer-actions', actionUrl: wuArcherActionSheetUrl },
{ key: 'unit-wu-cavalry', url: wuCavalryUnitSheetUrl, actionKey: 'unit-wu-cavalry-actions', actionUrl: wuCavalryActionSheetUrl },
{ key: 'unit-wu-strategist', url: wuStrategistUnitSheetUrl, actionKey: 'unit-wu-strategist-actions', actionUrl: wuStrategistActionSheetUrl },
{ key: 'unit-wu-officer', url: wuOfficerUnitSheetUrl, actionKey: 'unit-wu-officer-actions', actionUrl: wuOfficerActionSheetUrl },
{ key: 'unit-nanman-infantry', url: nanmanInfantryUnitSheetUrl, actionKey: 'unit-nanman-infantry-actions', actionUrl: nanmanInfantryActionSheetUrl },
{ key: 'unit-nanman-shaman', url: nanmanShamanUnitSheetUrl, actionKey: 'unit-nanman-shaman-actions', actionUrl: nanmanShamanActionSheetUrl },
{ key: 'unit-nanman-officer', url: nanmanOfficerUnitSheetUrl, actionKey: 'unit-nanman-officer-actions', actionUrl: nanmanOfficerActionSheetUrl }
];
type UnitSheetAsset = {
key: string;
url: string;
actionKey: string;
actionUrl: string;
};
const unitSheetModules = import.meta.glob('../../assets/images/units/unit-*.png', {
eager: true,
import: 'default'
}) as Record<string, string>;
function textureKeyFromPath(path: string) {
const fileName = path.split('/').pop() ?? path;
return fileName.replace(/\.png$/i, '');
}
const unitSheetUrls = Object.fromEntries(
Object.entries(unitSheetModules).map(([path, url]) => [textureKeyFromPath(path), url])
);
const sourceUnitSheets: UnitSheetAsset[] = Object.entries(unitSheetUrls)
.filter(([key]) => !key.endsWith('-actions'))
.map(([key, url]) => ({
key,
url,
actionKey: `${key}-actions`,
actionUrl: unitSheetUrls[`${key}-actions`]
}))
.filter((sheet): sheet is UnitSheetAsset => Boolean(sheet.actionUrl))
.sort((left, right) => left.key.localeCompare(right.key));
const animatedUnitSheets = sourceUnitSheets.map(({ key }) => ({ key }));
@@ -179,13 +58,7 @@ export class BootScene extends Phaser.Scene {
preload() {
this.load.image('title-taoyuan', titleBackgroundUrl);
this.load.image('story-militia', storyBackgroundAssets['story-militia']);
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
this.load.image('portrait-zhuge-liang', zhugeLiangPortraitUrl);
this.load.image('portrait-zhao-yun', zhaoYunPortraitUrl);
this.load.image('portrait-jiang-wei', jiangWeiPortraitUrl);
this.load.image('portrait-sima-yi', simaYiPortraitUrl);
Object.entries(portraitAssets).forEach(([key, url]) => this.load.image(`portrait-${key}`, url));
sourceUnitSheets.forEach(({ key, url, actionKey, actionUrl }) => {
this.load.spritesheet(key, url, {

View File

@@ -1,29 +1,10 @@
import Phaser from 'phaser';
import { soundDirector } from '../audio/SoundDirector';
import { storyBackgroundAssets } from '../data/storyAssets';
import { portraitKeyForSpeaker, portraitTextureKey } from '../data/portraitAssets';
import { storyBackgroundAssets, storyBackgroundKeysFor } from '../data/storyAssets';
import { prologuePages, type PortraitKey, type StoryPage } from '../data/scenario';
import { palette } from '../ui/palette';
const portraitKeys: Record<PortraitKey, string> = {
liuBei: 'portrait-liu-bei',
guanYu: 'portrait-guan-yu',
zhangFei: 'portrait-zhang-fei',
zhugeLiang: 'portrait-zhuge-liang',
zhaoYun: 'portrait-zhao-yun',
jiangWei: 'portrait-jiang-wei',
simaYi: 'portrait-sima-yi'
};
const portraitBySpeaker: Record<string, PortraitKey> = {
: 'liuBei',
: 'guanYu',
: 'zhangFei',
: 'zhugeLiang',
: 'zhaoYun',
: 'jiangWei',
: 'simaYi'
};
type AlphaObject = Phaser.GameObjects.Image | Phaser.GameObjects.Rectangle | Phaser.GameObjects.Text;
type StorySceneData = {
@@ -69,8 +50,9 @@ export class StoryScene extends Phaser.Scene {
pageIndex: this.pageIndex,
totalPages: this.pages.length,
currentPageId: page?.id,
background: page?.background,
backgroundReady: page ? this.textures.exists(page.background) : false,
requestedBackground: page?.background,
background: page ? this.pageBackgroundKey(page) : undefined,
backgroundReady: page ? this.textures.exists(this.pageBackgroundKey(page)) : false,
activeTexture: this.background?.texture.key ?? null,
portraitKey: page ? this.pagePortraitKey(page) ?? null : null,
portraitTextureKey: this.portrait?.visible ? this.portrait.texture.key : null
@@ -93,7 +75,7 @@ export class StoryScene extends Phaser.Scene {
this.ensureStoryBackgroundsLoaded(() => {
this.loadingText?.destroy();
this.loadingText = undefined;
this.background = this.add.image(width / 2, height / 2, this.resolveBackgroundKey(this.pages[0].background));
this.background = this.add.image(width / 2, height / 2, this.resolveBackgroundKey(this.pageBackgroundKey(this.pages[0])));
this.drawSceneShade(width, height);
this.drawDialogPanel(width, height);
this.showPage(0, true);
@@ -105,7 +87,8 @@ export class StoryScene extends Phaser.Scene {
}
private ensureStoryBackgroundsLoaded(onReady: () => void) {
const missingKeys = Array.from(new Set(this.pages.map((page) => page.background))).filter((key) => !this.textures.exists(key));
const requestedKeys = this.pages.flatMap((page) => storyBackgroundKeysFor(page.background));
const missingKeys = Array.from(new Set(requestedKeys)).filter((key) => !this.textures.exists(key));
const loadableKeys = missingKeys.filter((key) => storyBackgroundAssets[key]);
missingKeys
@@ -252,16 +235,17 @@ export class StoryScene extends Phaser.Scene {
private applyPage(page: StoryPage) {
soundDirector.playMusic(page.bgm);
this.applyBackground(page.background);
this.applyBackground(page);
this.chapterText?.setText(page.chapter);
const portraitKey = this.pagePortraitKey(page);
const textureKey = portraitKey ? portraitTextureKey(portraitKey) : undefined;
if (portraitKey) {
if (textureKey && this.textures.exists(textureKey)) {
this.portraitFrame?.setVisible(true);
this.portrait?.setVisible(true);
this.portraitDivider?.setVisible(true);
this.portrait?.setTexture(portraitKeys[portraitKey]);
this.portrait?.setTexture(textureKey);
this.portrait?.setDisplaySize(128, 128);
this.nameText?.setPosition(270, this.scale.height - 210);
this.bodyText?.setPosition(270, this.scale.height - 160);
@@ -281,16 +265,25 @@ export class StoryScene extends Phaser.Scene {
}
private pagePortraitKey(page: StoryPage) {
return page.portrait ?? (page.speaker ? portraitBySpeaker[page.speaker] : undefined);
return page.portrait ?? portraitKeyForSpeaker(page.speaker);
}
private applyBackground(textureKey: string) {
private pageBackgroundKey(page: StoryPage) {
const keys = storyBackgroundKeysFor(page.background).filter((key) => this.textures.exists(key));
if (keys.length <= 1) {
return keys[0] ?? page.background;
}
return keys[this.hashString(page.id) % keys.length];
}
private applyBackground(page: StoryPage) {
if (!this.background) {
return;
}
const { width, height } = this.scale;
const safeTextureKey = this.resolveBackgroundKey(textureKey);
const safeTextureKey = this.resolveBackgroundKey(this.pageBackgroundKey(page));
const source = this.textures.get(safeTextureKey).getSourceImage();
const imageWidth = source.width;
const imageHeight = source.height;
@@ -318,6 +311,14 @@ export class StoryScene extends Phaser.Scene {
return this.textures.exists(textureKey) ? textureKey : 'story-fallback';
}
private hashString(value: string) {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
}
return hash;
}
private advance() {
if (this.transitioning) {
return;