优化:文案优化,去主题化
This commit is contained in:
1 parent
5b822f7dc8
commit
81a48229d8
16 files changed
+396
-494
No files matched your search
@@ -1,13 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { PRODUCTION_SECT_PRESENTATIONS } from './productionRuntime';
|
||||
import { StandardSectOrganizationModule } from '../core';
|
||||
import {
|
||||
PRODUCTION_SECT_PRESENTATIONS,
|
||||
PRODUCTION_SECTS,
|
||||
} from './productionRuntime';
|
||||
|
||||
const taskIds = [
|
||||
'gate_sweep',
|
||||
'mine_patrol',
|
||||
'pill_delivery',
|
||||
'artifact_delivery',
|
||||
'weekly_diligence',
|
||||
'weekly_tournament',
|
||||
'weekly_bounty',
|
||||
'elder_trial',
|
||||
] as const;
|
||||
|
||||
const canonicalNpcRoles = [
|
||||
{
|
||||
sigil: '执',
|
||||
identity: '值日执事',
|
||||
responsibility: '负责日常委托。',
|
||||
},
|
||||
{
|
||||
sigil: '簿',
|
||||
identity: '功簿执事',
|
||||
responsibility: '负责周常委托。',
|
||||
},
|
||||
{
|
||||
sigil: '传',
|
||||
identity: '传功长老',
|
||||
responsibility: '负责晋升试炼。',
|
||||
},
|
||||
] as const;
|
||||
|
||||
describe('production sect affairs presentations', () => {
|
||||
it('provides twelve named NPCs across the four production sects', () => {
|
||||
it('provides twelve named NPCs with canonical roles', () => {
|
||||
const expectedNames = [
|
||||
['林砚秋', '顾闻锋', '谢停云'],
|
||||
['明尘', '照业', '渡厄'],
|
||||
['纪司辰', '许衡章', '容观澜'],
|
||||
['沈照灯', '白守簿', '楚引归'],
|
||||
['陆青崖', '裴守拙', '听剑老人'],
|
||||
['法明', '慧觉', '空慈方丈'],
|
||||
['知微', '玄衡道人', '观澜真人'],
|
||||
['照灯', '守簿翁', '归魂婆婆'],
|
||||
];
|
||||
const presentations = Object.values(PRODUCTION_SECT_PRESENTATIONS);
|
||||
|
||||
@@ -20,10 +53,21 @@ describe('production sect affairs presentations', () => {
|
||||
for (const presentation of presentations) {
|
||||
const npcs = Object.values(presentation.affairsRoom.taskNpcs);
|
||||
expect(new Set(npcs.map((npc) => npc.id)).size).toBe(3);
|
||||
for (const npc of npcs) {
|
||||
expect(npc.identity.trim()).not.toBe('');
|
||||
expect(npc.responsibility.trim()).not.toBe('');
|
||||
expect(npc.greeting.trim()).not.toBe('');
|
||||
expect(npcs).toMatchObject(canonicalNpcRoles);
|
||||
for (const npc of npcs) expect(npc.greeting.trim()).not.toBe('');
|
||||
expect(presentation.terms.sweepActivity).toBe('清扫山门');
|
||||
expect(presentation.terms.sweepCanvasLabel).toBe('清扫山门游戏画布');
|
||||
}
|
||||
});
|
||||
|
||||
it('uses one canonical presentation for every standard task', () => {
|
||||
const standard = new StandardSectOrganizationModule();
|
||||
|
||||
for (const { module } of PRODUCTION_SECTS) {
|
||||
for (const taskId of taskIds) {
|
||||
expect(module.organization.tasks.get(taskId)?.presentation).toEqual(
|
||||
standard.tasks.get(taskId)?.presentation,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+16
@@ -151,4 +151,20 @@ describe('LingxiaoOrganizationModule', () => {
|
||||
expect(first?.inventory.artifacts).toEqual([]);
|
||||
expect(first?.spiritual_roots).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['mine_patrol', '矿场巡视', '矿脉侵扰妖兽'],
|
||||
['weekly_tournament', '宗门小比', '同门演武傀儡'],
|
||||
['weekly_bounty', '悬赏残影战', '叛徒残影'],
|
||||
['elder_trial', '长老试炼', '传功长老化身'],
|
||||
])('uses canonical copy for %s battle opponents', (taskId, title, name) => {
|
||||
const result = LINGXIAO_ORGANIZATION.battles.get(taskId)?.create({
|
||||
player: playerFixture(),
|
||||
mirror: playerFixture({ id: 'mirror', name: '同门' }),
|
||||
opponentId: `npc-${taskId}`,
|
||||
});
|
||||
|
||||
expect(result?.title).toBe(title);
|
||||
expect(result?.opponent.name).toBe(name);
|
||||
});
|
||||
});
|
||||
@@ -5,45 +5,6 @@ import {
|
||||
|
||||
/** 红尘剑宗只声明组织玩法的展示主题;核心流程由标准组织模块提供。 */
|
||||
export const LINGXIAO_ORGANIZATION_THEME: SectOrganizationTheme = {
|
||||
taskPresentation: {
|
||||
gate_sweep: {
|
||||
description: '沿通往山下的石阶清理落叶,完成一轮山门勤务。',
|
||||
actionLabel: '前往山阶清扫',
|
||||
dialogue: {
|
||||
offeredReply: '山阶扫叶便交给我吧',
|
||||
activeReply: '山阶那桩勤务,请再说一遍',
|
||||
claimableReply: '山阶已经扫净,请录事查验',
|
||||
claimedReply: '请替我查查山阶勤务的功簿',
|
||||
instruction: {
|
||||
text: '沿通往山下的石阶清去落叶,晨钟再响前回来复命。',
|
||||
},
|
||||
},
|
||||
},
|
||||
weekly_tournament: {
|
||||
description: '在试剑台与同门剑影验证本周修行。',
|
||||
dialogue: {
|
||||
offeredReply: '本周试剑,我来应战',
|
||||
activeReply: '试剑台的安排,请再说一遍',
|
||||
claimableReply: '本周试剑已经结束,请录事查验',
|
||||
claimedReply: '请替我查查本周试剑的功簿',
|
||||
instruction: {
|
||||
text: '去试剑台与同门剑影交手,以本周所得胜过一场,再回来复命。',
|
||||
},
|
||||
},
|
||||
},
|
||||
elder_trial: {
|
||||
description: '接下传功长老三式问剑,取得真传资格。',
|
||||
dialogue: {
|
||||
offeredReply: '弟子愿受三式问剑',
|
||||
activeReply: '三式问剑,请长老再作指点',
|
||||
claimableReply: '弟子已经过关,请长老查验',
|
||||
claimedReply: '请长老查验弟子的问剑记录',
|
||||
instruction: {
|
||||
text: '去试炼场接我三式问剑。胜过剑影,才算有资格在真传名册上落笔。',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shopGrants: {
|
||||
true_cloud_ore: {
|
||||
name: '百炼山铁',
|
||||
@@ -51,10 +12,6 @@ export const LINGXIAO_ORGANIZATION_THEME: SectOrganizationTheme = {
|
||||
},
|
||||
true_spirit_pill: { name: '问剑蕴神丹' },
|
||||
},
|
||||
opponents: {
|
||||
weekly_tournament: { name: '同门试剑傀儡' },
|
||||
elder_trial: { name: '传功长老剑影' },
|
||||
},
|
||||
stipendGrantNames: { trueHerb: '剑叶灵草' },
|
||||
};
|
||||
|
||||
|
||||
@@ -325,30 +325,21 @@ export const LINGXIAO_SECT_PRESENTATION: SectPresentationTheme = {
|
||||
},
|
||||
affairsRoom: {
|
||||
description:
|
||||
'旧剑与归宗名册分列堂中,三席录事各守一案。寻到经办之人,便可当面接下或交回事务。',
|
||||
'旧剑与功簿分列堂中,三席经办之人各守一案。寻到对应之人,便可当面接下或交回事务。',
|
||||
taskNpcs: {
|
||||
daily: {
|
||||
id: 'lingxiao-lin-yanqiu',
|
||||
sigil: '录',
|
||||
name: '林砚秋',
|
||||
identity: '外务录事',
|
||||
responsibility: '山门勤务、矿场巡视与丹器交付。',
|
||||
greeting: '今日外务已经理清,你先说想办哪一桩。',
|
||||
name: '陆青崖',
|
||||
greeting: '今日事务已经理清,你先说想办哪一桩。',
|
||||
},
|
||||
weekly: {
|
||||
id: 'lingxiao-gu-wenfeng',
|
||||
sigil: '簿',
|
||||
name: '顾闻锋',
|
||||
identity: '功簿录事',
|
||||
responsibility: '周勤、试剑与宗门悬赏。',
|
||||
name: '裴守拙',
|
||||
greeting: '本周功簿在此,小比与悬赏也都记着。',
|
||||
},
|
||||
promotion: {
|
||||
id: 'lingxiao-xie-tingyun',
|
||||
sigil: '剑',
|
||||
name: '谢停云',
|
||||
identity: '传功长老',
|
||||
responsibility: '三式问剑与真传考校。',
|
||||
name: '听剑老人',
|
||||
greeting: '晋升问的不是锋芒多少,而是这一剑为何而出。',
|
||||
},
|
||||
},
|
||||
@@ -359,7 +350,5 @@ export const LINGXIAO_SECT_PRESENTATION: SectPresentationTheme = {
|
||||
meridianLoadout: '参悟方案',
|
||||
abilityChanges: '神通变化',
|
||||
returnToAffairs: '返回执事堂',
|
||||
sweepActivity: '山阶扫叶',
|
||||
sweepCanvasLabel: '山阶扫叶游戏画布',
|
||||
},
|
||||
};
|
||||
@@ -1,48 +1,6 @@
|
||||
import type { SectOrganizationTheme } from '../../core';
|
||||
|
||||
export const TIANYAN_ORGANIZATION_THEME: SectOrganizationTheme = {
|
||||
taskPresentation: {
|
||||
gate_sweep: {
|
||||
description:
|
||||
'沿观象门与中宫之间校正被云雨冲乱的五行地刻,并记录今日偏差。',
|
||||
actionLabel: '前往观象阶校正地刻',
|
||||
dialogue: {
|
||||
offeredReply: '今日地刻由我来校正',
|
||||
activeReply: '地刻校正之事,请再说一遍',
|
||||
claimableReply: '地刻已经校正,请算使查验',
|
||||
claimedReply: '请替我查查今日校正的记录',
|
||||
instruction: {
|
||||
text: '沿观象门至中宫逐段校正被云雨冲乱的五行地刻,并记下今日偏差。',
|
||||
},
|
||||
},
|
||||
},
|
||||
weekly_tournament: {
|
||||
description: '在中宫演法台与同门推演傀儡验证本周神通次序。',
|
||||
actionLabel: '参加中宫衍法',
|
||||
dialogue: {
|
||||
offeredReply: '本周衍法,我来落子',
|
||||
activeReply: '中宫衍法的次序,请再说一遍',
|
||||
claimableReply: '本周衍法已经完成,请算使查验',
|
||||
claimedReply: '请替我查查本周衍法的记录',
|
||||
instruction: {
|
||||
text: '去中宫演法台与推演傀儡交手,以本周神通次序完成一局。',
|
||||
},
|
||||
},
|
||||
},
|
||||
elder_trial: {
|
||||
description: '接下河洛长老布置的三局残阵,在有限术式中完成破局。',
|
||||
actionLabel: '应对河洛问局',
|
||||
dialogue: {
|
||||
offeredReply: '弟子愿解三局残阵',
|
||||
activeReply: '三局残阵,请长老再作指点',
|
||||
claimableReply: '残阵已经解开,请长老查验',
|
||||
claimedReply: '请长老查验弟子的破局记录',
|
||||
instruction: {
|
||||
text: '入河洛台解开三局残阵。术式虽有限,落子却不止一种,破局后再来见我。',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shopGrants: {
|
||||
true_cloud_ore: {
|
||||
name: '归流玄铜',
|
||||
@@ -50,10 +8,6 @@ export const TIANYAN_ORGANIZATION_THEME: SectOrganizationTheme = {
|
||||
},
|
||||
true_spirit_pill: { name: '太初蕴神丹' },
|
||||
},
|
||||
opponents: {
|
||||
weekly_tournament: { name: '同门推演傀儡' },
|
||||
elder_trial: { name: '河洛长老法影' },
|
||||
},
|
||||
facilityNames: {
|
||||
archive: '五经阁',
|
||||
cultivation_room: '太初静室',
|
||||
|
||||
@@ -324,31 +324,22 @@ export const TIANYAN_SECT_PRESENTATION: SectPresentationTheme = {
|
||||
},
|
||||
},
|
||||
affairsRoom: {
|
||||
description: '司算院长卷依时辰铺开,三席各自校验日务、周录与晋升问局。',
|
||||
description: '司算院长卷依时辰铺开,日常、周常与晋升事务各归一席。',
|
||||
taskNpcs: {
|
||||
daily: {
|
||||
id: 'tianyan-ji-sichen',
|
||||
sigil: '辰',
|
||||
name: '纪司辰',
|
||||
identity: '当值算使',
|
||||
responsibility: '日常校正、地脉与丹器差事。',
|
||||
name: '知微',
|
||||
greeting: '今日时序已经排定,眼下可办的差事都在这里。',
|
||||
},
|
||||
weekly: {
|
||||
id: 'tianyan-xu-hengzhang',
|
||||
sigil: '衡',
|
||||
name: '许衡章',
|
||||
identity: '功簿算使',
|
||||
responsibility: '周录、衍法与宗门悬契。',
|
||||
name: '玄衡道人',
|
||||
greeting: '本周诸事已有定数,但落子之人仍要你自己选。',
|
||||
},
|
||||
promotion: {
|
||||
id: 'tianyan-rong-guanlan',
|
||||
sigil: '洛',
|
||||
name: '容观澜',
|
||||
identity: '河洛长老',
|
||||
responsibility: '三局残阵与晋升考校。',
|
||||
greeting: '残阵不求唯一解法,只看你能否辨清下一步。',
|
||||
name: '观澜真人',
|
||||
greeting: '晋升试炼不求捷径,只看你能否辨清下一步。',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -358,7 +349,5 @@ export const TIANYAN_SECT_PRESENTATION: SectPresentationTheme = {
|
||||
meridianLoadout: '推演方案',
|
||||
abilityChanges: '神通衍变',
|
||||
returnToAffairs: '返回司算院',
|
||||
sweepActivity: '校正地刻',
|
||||
sweepCanvasLabel: '天衍地刻校正游戏画布',
|
||||
},
|
||||
};
|
||||
@@ -1,12 +1,5 @@
|
||||
import { GameplayTags } from '@shared/engine/shared/tag-domain';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
PRODUCTION_SECT_IDS,
|
||||
projectSectCombat,
|
||||
resolveSectAbility,
|
||||
resolveSectAbilities,
|
||||
} from '../..';
|
||||
import { SectStateValidator, isListedSectAbility } from '../../../core';
|
||||
import {
|
||||
TIANYAN_HETU_PATH_ID,
|
||||
TIANYAN_LANDING_ABILITY_IDS,
|
||||
@@ -17,6 +10,13 @@ import {
|
||||
TIANYAN_SECT_PRESENTATION,
|
||||
TIANYAN_VISIBLE_ABILITY_IDS,
|
||||
} from '..';
|
||||
import {
|
||||
PRODUCTION_SECT_IDS,
|
||||
projectSectCombat,
|
||||
resolveSectAbilities,
|
||||
resolveSectAbility,
|
||||
} from '../..';
|
||||
import { SectStateValidator, isListedSectAbility } from '../../../core';
|
||||
import { tianyanState } from './testState';
|
||||
|
||||
describe('天衍圣地战斗投影', () => {
|
||||
@@ -24,10 +24,17 @@ describe('天衍圣地战斗投影', () => {
|
||||
const definition = TIANYAN_MODULE.definition;
|
||||
expect(PRODUCTION_SECT_IDS).toContain('tianyan');
|
||||
expect(definition.configVersion).toBe(1);
|
||||
expect(definition.methods.map((method) => method.id)).toEqual(TIANYAN_METHOD_IDS);
|
||||
expect(definition.abilities.filter(isListedSectAbility).map((ability) => ability.id))
|
||||
.toEqual(TIANYAN_VISIBLE_ABILITY_IDS);
|
||||
expect(definition.abilities.filter((ability) => ability.kind === 'default')).toHaveLength(1);
|
||||
expect(definition.methods.map((method) => method.id)).toEqual(
|
||||
TIANYAN_METHOD_IDS,
|
||||
);
|
||||
expect(
|
||||
definition.abilities
|
||||
.filter(isListedSectAbility)
|
||||
.map((ability) => ability.id),
|
||||
).toEqual(TIANYAN_VISIBLE_ABILITY_IDS);
|
||||
expect(
|
||||
definition.abilities.filter((ability) => ability.kind === 'default'),
|
||||
).toHaveLength(1);
|
||||
expect(definition.paths.map((path) => path.id)).toEqual([
|
||||
TIANYAN_HETU_PATH_ID,
|
||||
TIANYAN_LUOSHU_PATH_ID,
|
||||
@@ -37,7 +44,9 @@ describe('天衍圣地战斗投影', () => {
|
||||
expect(path.nodes).toHaveLength(18);
|
||||
expect(path.tactics).toHaveLength(3);
|
||||
for (const layer of path.layers) {
|
||||
expect(path.nodes.filter((node) => node.layerId === layer.id)).toHaveLength(3);
|
||||
expect(
|
||||
path.nodes.filter((node) => node.layerId === layer.id),
|
||||
).toHaveLength(3);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -86,7 +95,9 @@ describe('天衍圣地战斗投影', () => {
|
||||
expect(new Set(definition.methods.map((entry) => entry.slot)).size).toBe(6);
|
||||
expect(definition.onboarding.initialAbilityLoadout).toHaveLength(4);
|
||||
for (const abilityId of definition.onboarding.initialAbilityLoadout) {
|
||||
const ability = definition.abilities.find((entry) => entry.id === abilityId);
|
||||
const ability = definition.abilities.find(
|
||||
(entry) => entry.id === abilityId,
|
||||
);
|
||||
expect(ability).toBeDefined();
|
||||
expect(ability?.kind).toBe('active');
|
||||
}
|
||||
@@ -95,39 +106,69 @@ describe('天衍圣地战斗投影', () => {
|
||||
it('五幕入门、15个标准地图热点与组织展示主题完整', () => {
|
||||
expect(TIANYAN_SECT_PRESENTATION.sectId).toBe('tianyan');
|
||||
expect(TIANYAN_SECT_PRESENTATION.onboarding?.script?.acts).toHaveLength(5);
|
||||
expect(TIANYAN_SECT_PRESENTATION.onboarding?.script?.backdrop.src)
|
||||
.toBe('/assets/sect/onboarding/tianyan.webp');
|
||||
expect(TIANYAN_SECT_PRESENTATION.map?.image)
|
||||
.toBe('/assets/sect/tianyan-map.webp');
|
||||
expect(TIANYAN_SECT_PRESENTATION.map?.hotspots?.map((spot) => spot.id))
|
||||
.toEqual([
|
||||
'hall', 'archive', 'cliff', 'arena', 'affairs', 'treasury',
|
||||
'industries', 'cultivation', 'alchemy', 'refinery', 'vein',
|
||||
'garden', 'gate', 'cave', 'formation',
|
||||
]);
|
||||
expect(TIANYAN_SECT_PRESENTATION.map?.hotspots?.find(
|
||||
(spot) => spot.id === 'formation',
|
||||
)).toMatchObject({ locked: true, facility: 'formation' });
|
||||
expect(TIANYAN_ORGANIZATION_THEME.taskPresentation).toMatchObject({
|
||||
gate_sweep: { actionLabel: '前往观象阶校正地刻' },
|
||||
weekly_tournament: { actionLabel: '参加中宫衍法' },
|
||||
elder_trial: { actionLabel: '应对河洛问局' },
|
||||
});
|
||||
expect(TIANYAN_ORGANIZATION_THEME.opponents).toMatchObject({
|
||||
weekly_tournament: { name: '同门推演傀儡' },
|
||||
elder_trial: { name: '河洛长老法影' },
|
||||
});
|
||||
expect(TIANYAN_SECT_PRESENTATION.onboarding?.script?.backdrop.src).toBe(
|
||||
'/assets/sect/onboarding/tianyan.webp',
|
||||
);
|
||||
expect(TIANYAN_SECT_PRESENTATION.map?.image).toBe(
|
||||
'/assets/sect/tianyan-map.webp',
|
||||
);
|
||||
expect(
|
||||
TIANYAN_SECT_PRESENTATION.map?.hotspots?.map((spot) => spot.id),
|
||||
).toEqual([
|
||||
'hall',
|
||||
'archive',
|
||||
'cliff',
|
||||
'arena',
|
||||
'affairs',
|
||||
'treasury',
|
||||
'industries',
|
||||
'cultivation',
|
||||
'alchemy',
|
||||
'refinery',
|
||||
'vein',
|
||||
'garden',
|
||||
'gate',
|
||||
'cave',
|
||||
'formation',
|
||||
]);
|
||||
expect(
|
||||
TIANYAN_SECT_PRESENTATION.map?.hotspots?.find(
|
||||
(spot) => spot.id === 'formation',
|
||||
),
|
||||
).toMatchObject({ locked: true, facility: 'formation' });
|
||||
expect(
|
||||
TIANYAN_MODULE.organization.tasks.get('gate_sweep')?.presentation
|
||||
.actionLabel,
|
||||
).toBe('开始清扫');
|
||||
expect(
|
||||
TIANYAN_MODULE.organization.tasks.get('weekly_tournament')?.presentation
|
||||
.actionLabel,
|
||||
).toBe('参加小比');
|
||||
expect(
|
||||
TIANYAN_MODULE.organization.tasks.get('elder_trial')?.presentation
|
||||
.actionLabel,
|
||||
).toBe('挑战长老试炼');
|
||||
expect(TIANYAN_ORGANIZATION_THEME).not.toHaveProperty('taskPresentation');
|
||||
expect(TIANYAN_ORGANIZATION_THEME).not.toHaveProperty('opponents');
|
||||
});
|
||||
|
||||
it.each([TIANYAN_HETU_PATH_ID, TIANYAN_LUOSHU_PATH_ID])(
|
||||
'%s 状态可通过标准校验并生成唯一衍数资源',
|
||||
(pathId) => {
|
||||
const sect = tianyanState(pathId);
|
||||
expect(() => new SectStateValidator().validate(TIANYAN_MODULE, sect)).not.toThrow();
|
||||
expect(() =>
|
||||
new SectStateValidator().validate(TIANYAN_MODULE, sect),
|
||||
).not.toThrow();
|
||||
const projection = projectSectCombat({ sect, realm: '化神' });
|
||||
expect(projection).not.toBeNull();
|
||||
expect(projection?.resources).toEqual([
|
||||
{ id: 'sect.tianyan.derivation', name: '衍数', icon: '✨', initial: 0, max: 3 },
|
||||
{
|
||||
id: 'sect.tianyan.derivation',
|
||||
name: '衍数',
|
||||
icon: '✨',
|
||||
initial: 0,
|
||||
max: 3,
|
||||
},
|
||||
]);
|
||||
expect(projection?.selectionStrategy).toBeDefined();
|
||||
},
|
||||
@@ -136,23 +177,36 @@ describe('天衍圣地战斗投影', () => {
|
||||
it('六门落印术共享无印与五旧印六分支,并携带元素及灵根失配豁免', () => {
|
||||
const sect = tianyanState(TIANYAN_HETU_PATH_ID);
|
||||
for (const abilityId of TIANYAN_LANDING_ABILITY_IDS) {
|
||||
const config = resolveSectAbility({ sect, realm: '化神', abilityId }).config;
|
||||
const config = resolveSectAbility({
|
||||
sect,
|
||||
realm: '化神',
|
||||
abilityId,
|
||||
}).config;
|
||||
expect(config.effectLayers?.map((layer) => layer.id)).toEqual([
|
||||
'no-seal', 'old-wood', 'old-fire', 'old-earth', 'old-metal', 'old-water',
|
||||
'no-seal',
|
||||
'old-wood',
|
||||
'old-fire',
|
||||
'old-earth',
|
||||
'old-metal',
|
||||
'old-water',
|
||||
]);
|
||||
expect(config.effectPlans).toHaveLength(6);
|
||||
expect(config.tags).toContain(
|
||||
GameplayTags.ABILITY.MECHANIC.IGNORE_SPIRITUAL_ROOT_MISMATCH,
|
||||
);
|
||||
expect(
|
||||
config.tags?.some((tag) => tag.startsWith(`${GameplayTags.ABILITY.ELEMENT.ROOT}.`)),
|
||||
config.tags?.some((tag) =>
|
||||
tag.startsWith(`${GameplayTags.ABILITY.ELEMENT.ROOT}.`),
|
||||
),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('太初玄光是无属性、零耗且不具备落印或灵根失配豁免标签', () => {
|
||||
const config = resolveSectAbility({
|
||||
sect: tianyanState(), realm: '化神', abilityId: 'primordial-ray',
|
||||
sect: tianyanState(),
|
||||
realm: '化神',
|
||||
abilityId: 'primordial-ray',
|
||||
}).config;
|
||||
expect(config.mpCost).toBe(0);
|
||||
expect(config.cooldown).toBe(0);
|
||||
@@ -160,27 +214,33 @@ describe('天衍圣地战斗投影', () => {
|
||||
GameplayTags.ABILITY.MECHANIC.IGNORE_SPIRITUAL_ROOT_MISMATCH,
|
||||
);
|
||||
expect(
|
||||
config.tags?.some((tag) => tag.startsWith(`${GameplayTags.ABILITY.ELEMENT.ROOT}.`)),
|
||||
config.tags?.some((tag) =>
|
||||
tag.startsWith(`${GameplayTags.ABILITY.ELEMENT.ROOT}.`),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('灼烧与熔岩来源Buff携带火元素及异灵根失配豁免标签', () => {
|
||||
const sect = tianyanState(TIANYAN_HETU_PATH_ID);
|
||||
const buffTags = (abilityId: string, layerId: string, buffId: string) => {
|
||||
const config = resolveSectAbility({ sect, realm: '化神', abilityId }).config;
|
||||
const config = resolveSectAbility({
|
||||
sect,
|
||||
realm: '化神',
|
||||
abilityId,
|
||||
}).config;
|
||||
const effect = config.effectLayers
|
||||
?.find((layer) => layer.id === layerId)
|
||||
?.effects.find((entry) =>
|
||||
entry.type === 'apply_buff' && entry.params.buffConfig.id === buffId,
|
||||
?.effects.find(
|
||||
(entry) =>
|
||||
entry.type === 'apply_buff' &&
|
||||
entry.params.buffConfig.id === buffId,
|
||||
);
|
||||
return effect?.type === 'apply_buff' ? effect.params.buffConfig.tags ?? [] : [];
|
||||
return effect?.type === 'apply_buff'
|
||||
? (effect.params.buffConfig.tags ?? [])
|
||||
: [];
|
||||
};
|
||||
|
||||
const burnTags = buffTags(
|
||||
'flowing-flame',
|
||||
'no-seal',
|
||||
'sect.tianyan.burn',
|
||||
);
|
||||
const burnTags = buffTags('flowing-flame', 'no-seal', 'sect.tianyan.burn');
|
||||
const lavaTags = buffTags(
|
||||
'earth-bearing-seal',
|
||||
'old-fire',
|
||||
@@ -197,11 +257,20 @@ describe('天衍圣地战斗投影', () => {
|
||||
});
|
||||
|
||||
it('基础态可完整解析全部定义能力且只有13门列入玩家列表', () => {
|
||||
const resolved = resolveSectAbilities({ sect: tianyanState(), realm: '化神' });
|
||||
expect(resolved).toHaveLength(TIANYAN_MODULE.definition.abilities.length - 2);
|
||||
expect(resolved.filter((ability) => TIANYAN_VISIBLE_ABILITY_IDS.includes(
|
||||
ability.id as (typeof TIANYAN_VISIBLE_ABILITY_IDS)[number],
|
||||
))).toHaveLength(13);
|
||||
const resolved = resolveSectAbilities({
|
||||
sect: tianyanState(),
|
||||
realm: '化神',
|
||||
});
|
||||
expect(resolved).toHaveLength(
|
||||
TIANYAN_MODULE.definition.abilities.length - 2,
|
||||
);
|
||||
expect(
|
||||
resolved.filter((ability) =>
|
||||
TIANYAN_VISIBLE_ABILITY_IDS.includes(
|
||||
ability.id as (typeof TIANYAN_VISIBLE_ABILITY_IDS)[number],
|
||||
),
|
||||
),
|
||||
).toHaveLength(13);
|
||||
});
|
||||
|
||||
it('技能详情区分落印术、内景法与天衍秘法', () => {
|
||||
@@ -222,7 +291,9 @@ describe('天衍圣地战斗投影', () => {
|
||||
const details = resolveSectAbilities({
|
||||
sect: tianyanState(TIANYAN_HETU_PATH_ID),
|
||||
realm: '化神',
|
||||
}).flatMap((ability) => ability.detailRows).join(';');
|
||||
})
|
||||
.flatMap((ability) => ability.detailRows)
|
||||
.join(';');
|
||||
|
||||
expect(details).not.toContain('气血高于0%');
|
||||
});
|
||||
|
||||
@@ -1,45 +1,6 @@
|
||||
import type { SectOrganizationTheme } from '../../core';
|
||||
|
||||
export const WUXIANG_ORGANIZATION_THEME: SectOrganizationTheme = {
|
||||
taskPresentation: {
|
||||
gate_sweep: {
|
||||
description: '沿照壁清理昨夜落下的香灰与血莲残瓣。',
|
||||
actionLabel: '前往照壁洒扫',
|
||||
dialogue: {
|
||||
offeredReply: '照壁拂尘便交给我吧',
|
||||
activeReply: '照壁那桩日务,请再说一遍',
|
||||
claimableReply: '照壁已经洒扫干净,请师兄查验',
|
||||
claimedReply: '请替我查查照壁日务的功簿',
|
||||
instruction: {
|
||||
text: '沿照壁清去昨夜落下的香灰与血莲残瓣,做完便回来复命。',
|
||||
},
|
||||
},
|
||||
},
|
||||
weekly_tournament: {
|
||||
description: '在无相木人前验证佛魔转相是否仍守得住一念。',
|
||||
dialogue: {
|
||||
offeredReply: '本周校身,我愿一试',
|
||||
activeReply: '无相木人的考校,请再说一遍',
|
||||
claimableReply: '校身已经结束,请师兄查验',
|
||||
claimedReply: '请替我查查本周校身的功簿',
|
||||
instruction: {
|
||||
text: '去问身场面对无相木人,在佛魔转相之间守住一念,胜过一场再回来。',
|
||||
},
|
||||
},
|
||||
},
|
||||
elder_trial: {
|
||||
description: '承受戒律师三问,再以所得之业照还其身。',
|
||||
dialogue: {
|
||||
offeredReply: '弟子愿受三问照业',
|
||||
activeReply: '三问照业,请戒律师再作指点',
|
||||
claimableReply: '弟子已经照还此业,请戒律师查验',
|
||||
claimedReply: '请戒律师查验弟子的照业记录',
|
||||
instruction: {
|
||||
text: '入问身场承受三问,再以所见之业照还法身。能见、能受、能渡,方算过关。',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shopGrants: {
|
||||
true_cloud_ore: {
|
||||
name: '无相骨玉',
|
||||
@@ -47,10 +8,6 @@ export const WUXIANG_ORGANIZATION_THEME: SectOrganizationTheme = {
|
||||
},
|
||||
true_spirit_pill: { name: '照身定念丹' },
|
||||
},
|
||||
opponents: {
|
||||
weekly_tournament: { name: '无相试身木人' },
|
||||
elder_trial: { name: '戒律师法身' },
|
||||
},
|
||||
facilityNames: {
|
||||
archive: '贝叶藏',
|
||||
cultivation_room: '止观室',
|
||||
|
||||
@@ -319,26 +319,17 @@ export const WUXIANG_SECT_PRESENTATION: SectPresentationTheme = {
|
||||
taskNpcs: {
|
||||
daily: {
|
||||
id: 'wuxiang-mingchen',
|
||||
sigil: '尘',
|
||||
name: '明尘',
|
||||
identity: '知客僧',
|
||||
responsibility: '洒扫、问身与日用交付。',
|
||||
name: '法明',
|
||||
greeting: '白榜上的日务都在这里,肯做哪一件便说。',
|
||||
},
|
||||
weekly: {
|
||||
id: 'wuxiang-zhaoye',
|
||||
sigil: '业',
|
||||
name: '照业',
|
||||
identity: '记功僧',
|
||||
responsibility: '周勤、校身与宗门外契。',
|
||||
name: '慧觉',
|
||||
greeting: '功簿只记所行,不替人评说,你可自行查问。',
|
||||
},
|
||||
promotion: {
|
||||
id: 'wuxiang-due',
|
||||
sigil: '戒',
|
||||
name: '渡厄',
|
||||
identity: '戒律师',
|
||||
responsibility: '三问照业与晋升考校。',
|
||||
name: '空慈方丈',
|
||||
greeting: '要过此关,先要肯照见自己带来的业。',
|
||||
},
|
||||
},
|
||||
@@ -349,7 +340,5 @@ export const WUXIANG_SECT_PRESENTATION: SectPresentationTheme = {
|
||||
meridianLoadout: '参悟方案',
|
||||
abilityChanges: '三相神通',
|
||||
returnToAffairs: '返回知客寮',
|
||||
sweepActivity: '照壁拂尘',
|
||||
sweepCanvasLabel: '照壁拂尘游戏画布',
|
||||
},
|
||||
};
|
||||
@@ -1,126 +1,6 @@
|
||||
import type { SectOrganizationTheme } from '../../core';
|
||||
|
||||
export const YOUDU_ORGANIZATION_THEME: SectOrganizationTheme = {
|
||||
taskPresentation: {
|
||||
gate_sweep: {
|
||||
title: '巡查魂灯',
|
||||
description: '沿黑水石径巡查魂灯,记下倒影与行人错开的每一瞬。',
|
||||
actionLabel: '前往黑水巡灯',
|
||||
dialogue: {
|
||||
offeredReply: '今日魂灯由我来巡',
|
||||
activeReply: '巡灯的路径,请再说一遍',
|
||||
claimableReply: '魂灯已经巡完,请巡灯使查验',
|
||||
claimedReply: '请替我查查今日巡灯的记录',
|
||||
instruction: {
|
||||
text: '沿黑水石径逐盏巡查魂灯,记下倒影与行人错开的地方,再回来复命。',
|
||||
},
|
||||
},
|
||||
},
|
||||
mine_patrol: {
|
||||
title: '平息界隙回声',
|
||||
description: '前往黑水阴脉,收束从界隙中逸出的失控念灵。',
|
||||
actionLabel: '前往阴脉',
|
||||
dialogue: {
|
||||
offeredReply: '阴脉的回声由我来收束',
|
||||
activeReply: '界隙回声一事,请再说一遍',
|
||||
claimableReply: '失控念灵已经平息,请巡灯使查验',
|
||||
claimedReply: '请替我查查阴脉回声的记录',
|
||||
instruction: {
|
||||
text: '去黑水阴脉找到界隙回声,将逸出的失控念灵收束干净,再回来复命。',
|
||||
},
|
||||
},
|
||||
},
|
||||
pill_delivery: {
|
||||
title: '返照药契',
|
||||
description: '寻来一枚合用丹药,供返照室安定离魂弟子的神识。',
|
||||
actionLabel: '交付定魂丹药',
|
||||
dialogue: {
|
||||
offeredReply: '返照室所需丹药,我来寻',
|
||||
activeReply: '返照药契的要求,请再说一遍',
|
||||
claimableReply: '丹药已经入契,请巡灯使查验',
|
||||
claimedReply: '请替我查查返照药契的记录',
|
||||
instruction: {
|
||||
text: '替返照室寻来一枚合用丹药,用来安定离魂弟子的神识。',
|
||||
requirementPrefix: '替返照室寻来',
|
||||
requirementSuffix: ',带回招魂司验契。',
|
||||
},
|
||||
},
|
||||
},
|
||||
artifact_delivery: {
|
||||
title: '镇魂器契',
|
||||
description: '寻来一件合用且未装备的法宝,用于修补镇魂仪轨。',
|
||||
actionLabel: '交付镇魂法器',
|
||||
dialogue: {
|
||||
offeredReply: '修补镇魂仪轨的法器,我来寻',
|
||||
activeReply: '镇魂器契的要求,请再说一遍',
|
||||
claimableReply: '法器已经入契,请巡灯使查验',
|
||||
claimedReply: '请替我查查镇魂器契的记录',
|
||||
instruction: {
|
||||
text: '替招魂司寻来一件合用的未装备法宝,用于修补镇魂仪轨。',
|
||||
requirementPrefix: '替招魂司寻来',
|
||||
requirementSuffix: ',带回后用于修补镇魂仪轨。',
|
||||
},
|
||||
},
|
||||
},
|
||||
weekly_diligence: {
|
||||
title: '巡灯周录',
|
||||
description: '一周完成五次招魂司勤务,补全黑水沿岸的巡灯簿。',
|
||||
actionLabel: '查看巡灯簿',
|
||||
dialogue: {
|
||||
offeredReply: '本周巡灯簿也记我一份',
|
||||
activeReply: '巡灯周录已经记到哪里了',
|
||||
claimableReply: '本周巡灯簿已经补全,请命簿使查验',
|
||||
claimedReply: '请替我翻翻本周巡灯簿',
|
||||
instruction: {
|
||||
text: '本周完成五次招魂司勤务,将黑水沿岸的巡灯簿补全。',
|
||||
},
|
||||
},
|
||||
},
|
||||
weekly_tournament: {
|
||||
title: '照影校术',
|
||||
description: '在照影场校验招魂、镇魄与送魂之法。',
|
||||
actionLabel: '进入照影场',
|
||||
dialogue: {
|
||||
offeredReply: '本周照影校术,我来应试',
|
||||
activeReply: '照影场的考校,请再说一遍',
|
||||
claimableReply: '照影校术已经完成,请命簿使查验',
|
||||
claimedReply: '请替我查查照影校术的记录',
|
||||
instruction: {
|
||||
text: '去照影场校验招魂、镇魄与送魂之法,胜过试魂傀再回来。',
|
||||
},
|
||||
},
|
||||
},
|
||||
weekly_bounty: {
|
||||
title: '失名悬契',
|
||||
description: '追查夺舍邪修、役魂商贩或伪造招魂契者留下的线索。',
|
||||
actionLabel: '领取悬契',
|
||||
dialogue: {
|
||||
offeredReply: '这份失名悬契由我来查',
|
||||
activeReply: '失名悬契的线索,请再说一遍',
|
||||
claimableReply: '失名悬契已经办妥,请命簿使查验',
|
||||
claimedReply: '请替我查查失名悬契的记录',
|
||||
instruction: {
|
||||
text: '循悬契追查夺舍邪修、役魂商贩或伪造契书者,找到残影后将其收伏。',
|
||||
requirementPrefix: '这份悬契要验一件遗物。替我寻来',
|
||||
requirementSuffix: ',带回后我会核验其魂息。',
|
||||
},
|
||||
},
|
||||
},
|
||||
elder_trial: {
|
||||
title: '七灯问名',
|
||||
description: '随引魂师穿过七灯,守住自己的姓名与归路。',
|
||||
actionLabel: '踏入七灯阵',
|
||||
dialogue: {
|
||||
offeredReply: '弟子愿入七灯问名',
|
||||
activeReply: '七灯问名,请引魂师再作指点',
|
||||
claimableReply: '弟子已经走过七灯,请引魂师查验',
|
||||
claimedReply: '请引魂师查验弟子的问名记录',
|
||||
instruction: {
|
||||
text: '踏入七灯阵,依次守住姓名、来处与归路。走过七灯仍不失本心,才算过关。',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shopGrants: {
|
||||
outer_qinglu: {
|
||||
name: '魂灯油',
|
||||
@@ -147,12 +27,6 @@ export const YOUDU_ORGANIZATION_THEME: SectOrganizationTheme = {
|
||||
description: '真传弟子渡魂远行前服用的高阶回气丹。',
|
||||
},
|
||||
},
|
||||
opponents: {
|
||||
mine_patrol: { title: '阴脉回声', name: '失控念灵' },
|
||||
weekly_tournament: { title: '照影校术', name: '照影试魂傀' },
|
||||
weekly_bounty: { title: '失名悬契', name: '役魂商贩残影' },
|
||||
elder_trial: { title: '七灯问名', name: '引魂师法身' },
|
||||
},
|
||||
facilityNames: {
|
||||
archive: '三魂阁',
|
||||
cultivation_room: '返照室',
|
||||
|
||||
@@ -310,31 +310,22 @@ export const YOUDU_SECT_PRESENTATION: SectPresentationTheme = {
|
||||
},
|
||||
},
|
||||
affairsRoom: {
|
||||
description: '招魂司内灯影不灭,巡灯、命簿与七灯问名各由一席经办。',
|
||||
description: '招魂司内灯影不灭,日常、周常与晋升事务各由一席经办。',
|
||||
taskNpcs: {
|
||||
daily: {
|
||||
id: 'youdu-shen-zhaodeng',
|
||||
sigil: '灯',
|
||||
name: '沈照灯',
|
||||
identity: '巡灯使',
|
||||
responsibility: '巡灯、阴脉与药器交付。',
|
||||
greeting: '今日还有几盏灯要照、几条归路要寻,你先挑一件。',
|
||||
name: '照灯',
|
||||
greeting: '今日卷宗都在灯下,你想办哪一件,直说便是。',
|
||||
},
|
||||
weekly: {
|
||||
id: 'youdu-bai-shoubu',
|
||||
sigil: '簿',
|
||||
name: '白守簿',
|
||||
identity: '命簿使',
|
||||
responsibility: '周录、照影与失名悬契。',
|
||||
greeting: '名字与归路都已写入本周命簿,你可逐项查验。',
|
||||
name: '守簿翁',
|
||||
greeting: '本周功簿已经归拢,你可逐项查验。',
|
||||
},
|
||||
promotion: {
|
||||
id: 'youdu-chu-yingui',
|
||||
sigil: '引',
|
||||
name: '楚引归',
|
||||
identity: '引魂师',
|
||||
responsibility: '七灯问名与晋升考校。',
|
||||
greeting: '七灯之下,守得住自己的名字,才有资格再往前走。',
|
||||
name: '归魂婆婆',
|
||||
greeting: '晋升这一关,先要记得自己为何而来。',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -344,7 +335,5 @@ export const YOUDU_SECT_PRESENTATION: SectPresentationTheme = {
|
||||
meridianLoadout: '参悟方案',
|
||||
abilityChanges: '魂术变化',
|
||||
returnToAffairs: '返回招魂司',
|
||||
sweepActivity: '巡灯照影',
|
||||
sweepCanvasLabel: '巡灯照影游戏画布',
|
||||
},
|
||||
};
|
||||
@@ -1,11 +1,5 @@
|
||||
import { GameplayTags } from '@shared/engine/shared/tag-domain';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
PRODUCTION_SECT_IDS,
|
||||
projectSectCombat,
|
||||
resolveSectAbility,
|
||||
} from '../..';
|
||||
import { isListedSectAbility, SectStateValidator } from '../../../core';
|
||||
import {
|
||||
YOUDU_DECREE_PATH_ID,
|
||||
YOUDU_MODULE,
|
||||
@@ -14,6 +8,12 @@ import {
|
||||
YOUDU_TIDE_PATH_ID,
|
||||
YOUDU_VISIBLE_ABILITY_IDS,
|
||||
} from '..';
|
||||
import {
|
||||
PRODUCTION_SECT_IDS,
|
||||
projectSectCombat,
|
||||
resolveSectAbility,
|
||||
} from '../..';
|
||||
import { isListedSectAbility, SectStateValidator } from '../../../core';
|
||||
import { YOUDU_ORGANIZATION_THEME } from '../organization';
|
||||
import { youduState } from './testState';
|
||||
|
||||
@@ -22,10 +22,17 @@ describe('幽都战斗与展示投影', () => {
|
||||
const definition = YOUDU_MODULE.definition;
|
||||
expect(PRODUCTION_SECT_IDS).toContain('youdu');
|
||||
expect(definition.methods).toHaveLength(6);
|
||||
expect(definition.methods.filter((method) => method.isPrimary)).toHaveLength(1);
|
||||
expect(definition.abilities.filter(isListedSectAbility).map((ability) => ability.id))
|
||||
.toEqual(YOUDU_VISIBLE_ABILITY_IDS);
|
||||
expect(definition.abilities.filter((ability) => ability.kind === 'default')).toHaveLength(1);
|
||||
expect(
|
||||
definition.methods.filter((method) => method.isPrimary),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
definition.abilities
|
||||
.filter(isListedSectAbility)
|
||||
.map((ability) => ability.id),
|
||||
).toEqual(YOUDU_VISIBLE_ABILITY_IDS);
|
||||
expect(
|
||||
definition.abilities.filter((ability) => ability.kind === 'default'),
|
||||
).toHaveLength(1);
|
||||
expect(definition.paths.map((path) => path.id)).toEqual([
|
||||
YOUDU_TIDE_PATH_ID,
|
||||
YOUDU_DECREE_PATH_ID,
|
||||
@@ -34,10 +41,14 @@ describe('幽都战斗与展示投影', () => {
|
||||
expect(path.nodes).toHaveLength(18);
|
||||
expect(path.tactics).toHaveLength(3);
|
||||
for (const layer of path.layers) {
|
||||
expect(path.nodes.filter((node) => node.layerId === layer.id)).toHaveLength(3);
|
||||
expect(
|
||||
path.nodes.filter((node) => node.layerId === layer.id),
|
||||
).toHaveLength(3);
|
||||
}
|
||||
}
|
||||
const ids = definition.paths.flatMap((path) => path.nodes.map((node) => node.id));
|
||||
const ids = definition.paths.flatMap((path) =>
|
||||
path.nodes.map((node) => node.id),
|
||||
);
|
||||
expect(new Set(ids).size).toBe(36);
|
||||
});
|
||||
|
||||
@@ -55,12 +66,18 @@ describe('幽都战斗与展示投影', () => {
|
||||
initialAbilityLoadout: ['soul-severing-call', null, null, null],
|
||||
});
|
||||
expect(YOUDU_SECT_PRESENTATION.onboarding?.script?.acts).toHaveLength(5);
|
||||
expect(YOUDU_SECT_PRESENTATION.onboarding?.script?.backdrop.src)
|
||||
.toBe('/assets/sect/onboarding/youdu.webp');
|
||||
expect(YOUDU_SECT_PRESENTATION.map?.image).toBe('/assets/sect/youdu-map.webp');
|
||||
expect(YOUDU_SECT_PRESENTATION.onboarding?.script?.backdrop.src).toBe(
|
||||
'/assets/sect/onboarding/youdu.webp',
|
||||
);
|
||||
expect(YOUDU_SECT_PRESENTATION.map?.image).toBe(
|
||||
'/assets/sect/youdu-map.webp',
|
||||
);
|
||||
expect(YOUDU_SECT_PRESENTATION.map?.hotspots).toHaveLength(15);
|
||||
expect(YOUDU_SECT_PRESENTATION.map?.hotspots?.find((spot) => spot.id === 'formation'))
|
||||
.toMatchObject({ locked: true, facility: 'formation' });
|
||||
expect(
|
||||
YOUDU_SECT_PRESENTATION.map?.hotspots?.find(
|
||||
(spot) => spot.id === 'formation',
|
||||
),
|
||||
).toMatchObject({ locked: true, facility: 'formation' });
|
||||
expect(YOUDU_SECT_PRESENTATION.facilityLabels).toMatchObject({
|
||||
alchemy: '还魂药庐',
|
||||
refinery: '镇铁炉',
|
||||
@@ -71,11 +88,19 @@ describe('幽都战斗与展示投影', () => {
|
||||
'%s 可通过标准状态校验并投影唯一魂火资源',
|
||||
(pathId) => {
|
||||
const state = youduState(pathId);
|
||||
expect(() => new SectStateValidator().validate(YOUDU_MODULE, state)).not.toThrow();
|
||||
expect(() =>
|
||||
new SectStateValidator().validate(YOUDU_MODULE, state),
|
||||
).not.toThrow();
|
||||
const projection = projectSectCombat({ sect: state, realm: '化神' })!;
|
||||
expect(projection.resources).toEqual([{
|
||||
id: YOUDU_SOUL_FIRE, name: '魂火', icon: '🔥', initial: 0, max: 3,
|
||||
}]);
|
||||
expect(projection.resources).toEqual([
|
||||
{
|
||||
id: YOUDU_SOUL_FIRE,
|
||||
name: '魂火',
|
||||
icon: '🔥',
|
||||
initial: 0,
|
||||
max: 3,
|
||||
},
|
||||
]);
|
||||
expect(projection.selectionStrategy).toBeDefined();
|
||||
expect(projection.abilities.map((ability) => ability.slug)).not.toContain(
|
||||
`sect.youdu.${pathId}-runtime`,
|
||||
@@ -95,17 +120,31 @@ describe('幽都战斗与展示投影', () => {
|
||||
'soul-shall-not-return': [80, 5],
|
||||
} as const;
|
||||
for (const [abilityId, [mpCost, cooldown]] of Object.entries(expected)) {
|
||||
const config = resolveSectAbility({ sect: state, realm: '化神', abilityId }).config;
|
||||
const config = resolveSectAbility({
|
||||
sect: state,
|
||||
realm: '化神',
|
||||
abilityId,
|
||||
}).config;
|
||||
expect(config.mpCost ?? 0).toBe(mpCost);
|
||||
expect(config.cooldown ?? 0).toBe(cooldown);
|
||||
}
|
||||
const shadow = resolveSectAbility({ sect: state, realm: '化神', abilityId: 'reveal-shadow' }).config;
|
||||
const shadow = resolveSectAbility({
|
||||
sect: state,
|
||||
realm: '化神',
|
||||
abilityId: 'reveal-shadow',
|
||||
}).config;
|
||||
expect(shadow.hitPolicy).toBe('guaranteed');
|
||||
const finish = resolveSectAbility({ sect: state, realm: '化神', abilityId: 'soul-shall-not-return' }).config;
|
||||
expect(finish.castConditions).toContainEqual(expect.objectContaining({
|
||||
type: 'buff_layer_at_least',
|
||||
params: expect.objectContaining({ value: 4 }),
|
||||
}));
|
||||
const finish = resolveSectAbility({
|
||||
sect: state,
|
||||
realm: '化神',
|
||||
abilityId: 'soul-shall-not-return',
|
||||
}).config;
|
||||
expect(finish.castConditions).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'buff_layer_at_least',
|
||||
params: expect.objectContaining({ value: 4 }),
|
||||
}),
|
||||
);
|
||||
expect(finish.tags).toContain(GameplayTags.ABILITY.CHANNEL.TRUE);
|
||||
});
|
||||
|
||||
@@ -231,13 +270,20 @@ describe('幽都战斗与展示投影', () => {
|
||||
'tide-hundred-ghosts',
|
||||
'tide-dream-invasion',
|
||||
'tide-last-ferry',
|
||||
].flatMap((nodeId) => resolveSectAbility({
|
||||
sect: youduState(YOUDU_TIDE_PATH_ID, [nodeId]),
|
||||
realm: '化神',
|
||||
abilityId: 'one-sigh',
|
||||
}).detailRows).join(';');
|
||||
]
|
||||
.flatMap(
|
||||
(nodeId) =>
|
||||
resolveSectAbility({
|
||||
sect: youduState(YOUDU_TIDE_PATH_ID, [nodeId]),
|
||||
realm: '化神',
|
||||
abilityId: 'one-sigh',
|
||||
}).detailRows,
|
||||
)
|
||||
.join(';');
|
||||
const decreeRuntime = resolveSectAbility({
|
||||
sect: decree, realm: '化神', abilityId: 'youdu-runtime',
|
||||
sect: decree,
|
||||
realm: '化神',
|
||||
abilityId: 'youdu-runtime',
|
||||
}).detailRows.join(';');
|
||||
|
||||
expect(tideRuntime).toContain('每回合首次忘川有效伤害获得1点魂火');
|
||||
@@ -255,19 +301,26 @@ describe('幽都战斗与展示投影', () => {
|
||||
sect: youduState(YOUDU_DECREE_PATH_ID),
|
||||
realm: '化神',
|
||||
})!;
|
||||
const listeners = projection.abilities.flatMap((ability) => ability.listeners ?? []);
|
||||
const listeners = projection.abilities.flatMap(
|
||||
(ability) => ability.listeners ?? [],
|
||||
);
|
||||
const ids = listeners.map((listener) => listener.id).filter(Boolean);
|
||||
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
expect(listeners.filter((listener) =>
|
||||
listener.budget?.group === 'sect.youdu.decree-control-response-fire'
|
||||
)).toHaveLength(4);
|
||||
expect(
|
||||
listeners.filter(
|
||||
(listener) =>
|
||||
listener.budget?.group === 'sect.youdu.decree-control-response-fire',
|
||||
),
|
||||
).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('组织主题覆盖全部任务、战斗对手、商店奖励与设施称谓', () => {
|
||||
expect(Object.keys(YOUDU_ORGANIZATION_THEME.taskPresentation ?? {})).toHaveLength(8);
|
||||
expect(Object.keys(YOUDU_ORGANIZATION_THEME.opponents ?? {})).toHaveLength(4);
|
||||
expect(Object.keys(YOUDU_ORGANIZATION_THEME.shopGrants ?? {})).toHaveLength(6);
|
||||
it('组织主题只覆盖非任务展示内容', () => {
|
||||
expect(YOUDU_ORGANIZATION_THEME).not.toHaveProperty('taskPresentation');
|
||||
expect(YOUDU_ORGANIZATION_THEME).not.toHaveProperty('opponents');
|
||||
expect(Object.keys(YOUDU_ORGANIZATION_THEME.shopGrants ?? {})).toHaveLength(
|
||||
6,
|
||||
);
|
||||
expect(YOUDU_ORGANIZATION_THEME.facilityNames).toMatchObject({
|
||||
archive: '三魂阁',
|
||||
cultivation_room: '返照室',
|
||||
|
||||
@@ -342,41 +342,22 @@ const tasks: readonly SectTaskDefinition[] = [
|
||||
];
|
||||
|
||||
class StandardSectTaskCatalog implements SectTaskCatalog {
|
||||
private readonly themedTasks: readonly SectTaskDefinition[];
|
||||
private readonly byId: ReadonlyMap<string, SectTaskDefinition>;
|
||||
|
||||
constructor(theme: SectOrganizationTheme = {}) {
|
||||
this.themedTasks = tasks.map((task) => {
|
||||
const override = theme.taskPresentation?.[task.id];
|
||||
return {
|
||||
...task,
|
||||
presentation: {
|
||||
...task.presentation,
|
||||
...override,
|
||||
dialogue: {
|
||||
...task.presentation.dialogue,
|
||||
...override?.dialogue,
|
||||
instruction: {
|
||||
...task.presentation.dialogue.instruction,
|
||||
...override?.dialogue?.instruction,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
this.byId = new Map(this.themedTasks.map((task) => [task.id, task]));
|
||||
constructor() {
|
||||
this.byId = new Map(tasks.map((task) => [task.id, task]));
|
||||
}
|
||||
|
||||
listDaily(): readonly SectTaskDefinition[] {
|
||||
return this.themedTasks.filter((task) => task.kind === 'daily');
|
||||
return tasks.filter((task) => task.kind === 'daily');
|
||||
}
|
||||
|
||||
listWeekly(): readonly SectTaskDefinition[] {
|
||||
return this.themedTasks.filter((task) => task.kind === 'weekly');
|
||||
return tasks.filter((task) => task.kind === 'weekly');
|
||||
}
|
||||
|
||||
listPromotion(): readonly SectTaskDefinition[] {
|
||||
return this.themedTasks.filter((task) => task.kind === 'promotion');
|
||||
return tasks.filter((task) => task.kind === 'promotion');
|
||||
}
|
||||
|
||||
get(id: SectOrganizationTaskId): SectTaskDefinition | undefined {
|
||||
@@ -384,7 +365,7 @@ class StandardSectTaskCatalog implements SectTaskCatalog {
|
||||
}
|
||||
|
||||
findByCompletionTag(tag: string) {
|
||||
return this.themedTasks.find((task) => task.completionTags?.includes(tag));
|
||||
return tasks.find((task) => task.completionTags?.includes(tag));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,14 +789,11 @@ const battleScenarioDefinitions = {
|
||||
class StandardSectBattleScenarioCatalog implements SectBattleScenarioCatalog {
|
||||
private readonly scenarios: ReadonlyMap<string, SectOpponentFactory>;
|
||||
|
||||
constructor(theme: SectOrganizationTheme = {}) {
|
||||
constructor() {
|
||||
this.scenarios = new Map(
|
||||
Object.entries(battleScenarioDefinitions).map(([taskId, definition]) => [
|
||||
taskId,
|
||||
opponentFactory({
|
||||
...definition,
|
||||
...(theme.opponents?.[taskId] ?? {}),
|
||||
}),
|
||||
opponentFactory(definition),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -1049,18 +1027,8 @@ class StandardSectBenefitPolicy implements SectBenefitPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
export interface SectTaskPresentationOverride extends Partial<
|
||||
Omit<SectTaskDefinition['presentation'], 'dialogue'>
|
||||
> {
|
||||
dialogue?: Partial<Omit<SectTaskDialogueDefinition, 'instruction'>> & {
|
||||
instruction?: Partial<SectTaskDialogueDefinition['instruction']>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SectOrganizationTheme {
|
||||
taskPresentation?: Partial<Record<string, SectTaskPresentationOverride>>;
|
||||
shopGrants?: Partial<Record<string, Partial<SectShopDefinition['grant']>>>;
|
||||
opponents?: Partial<Record<string, { title?: string; name?: string }>>;
|
||||
facilityNames?: Partial<Record<string, string>>;
|
||||
stipendGrantNames?: {
|
||||
herb?: string;
|
||||
@@ -1079,9 +1047,9 @@ export class StandardSectOrganizationModule implements SectOrganizationModule {
|
||||
readonly benefits: SectBenefitPolicy;
|
||||
|
||||
constructor(readonly theme: SectOrganizationTheme = {}) {
|
||||
this.tasks = new StandardSectTaskCatalog(theme);
|
||||
this.tasks = new StandardSectTaskCatalog();
|
||||
this.economy = new StandardSectEconomyPolicy(theme);
|
||||
this.battles = new StandardSectBattleScenarioCatalog(theme);
|
||||
this.battles = new StandardSectBattleScenarioCatalog();
|
||||
this.benefits = new StandardSectBenefitPolicy(theme);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,17 +71,13 @@ describe('sect task dialogue presentation', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('deep-merges sect dialogue overrides with standard instructions', () => {
|
||||
it('keeps standard dialogue while applying non-task organization themes', () => {
|
||||
const definition = new StandardSectOrganizationModule({
|
||||
taskPresentation: {
|
||||
gate_sweep: {
|
||||
dialogue: { offeredReply: '这段山路由我来扫' },
|
||||
},
|
||||
},
|
||||
facilityNames: { archive: '宗门藏书阁' },
|
||||
}).tasks.get('gate_sweep')!;
|
||||
|
||||
expect(definition.presentation.dialogue.offeredReply).toBe(
|
||||
'这段山路由我来扫',
|
||||
'山门洒扫便交给我吧',
|
||||
);
|
||||
expect(definition.presentation.dialogue.instruction.text).toBe(
|
||||
'去山门步道清理落叶,完成一轮洒扫后回来复命。',
|
||||
|
||||
@@ -15,10 +15,21 @@ describe('sect presentation affairs room', () => {
|
||||
]);
|
||||
expect(presentation.affairsRoom.taskNpcs.daily.name).toBe('值日执事');
|
||||
expect(presentation.affairsRoom.taskNpcs.daily.sigil).toBe('执');
|
||||
expect(presentation.affairsRoom.taskNpcs.daily.responsibility).toBe(
|
||||
'负责日常委托。',
|
||||
);
|
||||
expect(presentation.affairsRoom.taskNpcs.weekly.name).toBe('功簿执事');
|
||||
expect(presentation.affairsRoom.taskNpcs.weekly.sigil).toBe('簿');
|
||||
expect(presentation.affairsRoom.taskNpcs.weekly.responsibility).toBe(
|
||||
'负责周常委托。',
|
||||
);
|
||||
expect(presentation.affairsRoom.taskNpcs.promotion.name).toBe('传功长老');
|
||||
expect(presentation.affairsRoom.taskNpcs.promotion.sigil).toBe('传');
|
||||
expect(presentation.affairsRoom.taskNpcs.promotion.responsibility).toBe(
|
||||
'负责晋升试炼。',
|
||||
);
|
||||
expect(presentation.terms.sweepActivity).toBe('清扫山门');
|
||||
expect(presentation.terms.sweepCanvasLabel).toBe('清扫山门游戏画布');
|
||||
});
|
||||
|
||||
it('merges sect-specific room and NPC copy with the defaults', () => {
|
||||
@@ -28,7 +39,6 @@ describe('sect presentation affairs room', () => {
|
||||
description: '星轨交汇之处,诸般事务各归其席。',
|
||||
taskNpcs: {
|
||||
daily: {
|
||||
sigil: '辰',
|
||||
name: '司辰使',
|
||||
greeting: '今日星轨已经排定。',
|
||||
},
|
||||
@@ -41,7 +51,7 @@ describe('sect presentation affairs room', () => {
|
||||
);
|
||||
expect(presentation.affairsRoom.taskNpcs.daily).toMatchObject({
|
||||
id: 'daily-steward',
|
||||
sigil: '辰',
|
||||
sigil: '执',
|
||||
name: '司辰使',
|
||||
identity: '值日执事',
|
||||
greeting: '今日星轨已经排定。',
|
||||
@@ -49,6 +59,36 @@ describe('sect presentation affairs room', () => {
|
||||
expect(presentation.affairsRoom.taskNpcs.weekly.name).toBe('功簿执事');
|
||||
});
|
||||
|
||||
it('ignores runtime attempts to override canonical NPC role fields', () => {
|
||||
const unsafeTheme = {
|
||||
sectId: 'sample-sect',
|
||||
affairsRoom: {
|
||||
taskNpcs: {
|
||||
daily: {
|
||||
name: '司辰使',
|
||||
sigil: '辰',
|
||||
identity: '当值算使',
|
||||
responsibility: '负责校正地刻。',
|
||||
},
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
sweepActivity: '校正地刻',
|
||||
sweepCanvasLabel: '校正地刻游戏画布',
|
||||
},
|
||||
} as unknown as SectPresentationTheme;
|
||||
const presentation = resolveSectPresentation('sample-sect', unsafeTheme);
|
||||
|
||||
expect(presentation.affairsRoom.taskNpcs.daily).toMatchObject({
|
||||
name: '司辰使',
|
||||
sigil: '执',
|
||||
identity: '值日执事',
|
||||
responsibility: '负责日常委托。',
|
||||
});
|
||||
expect(presentation.terms.sweepActivity).toBe('清扫山门');
|
||||
expect(presentation.terms.sweepCanvasLabel).toBe('清扫山门游戏画布');
|
||||
});
|
||||
|
||||
it('rejects blank fields and duplicate NPC identifiers', () => {
|
||||
expect(() =>
|
||||
resolveSectPresentation('sample-sect', {
|
||||
|
||||
@@ -85,10 +85,15 @@ export interface SectPresentationTheme {
|
||||
affairsRoom?: {
|
||||
description?: string;
|
||||
taskNpcs?: Partial<
|
||||
Record<SectAffairsTaskKind, Partial<SectRoomNpcPresentation>>
|
||||
Record<
|
||||
SectAffairsTaskKind,
|
||||
Partial<Pick<SectRoomNpcPresentation, 'id' | 'name' | 'greeting'>>
|
||||
>
|
||||
>;
|
||||
};
|
||||
terms?: Partial<SectPresentationTerms>;
|
||||
terms?: Partial<
|
||||
Omit<SectPresentationTerms, 'sweepActivity' | 'sweepCanvasLabel'>
|
||||
>;
|
||||
}
|
||||
|
||||
export interface ResolvedSectPresentation {
|
||||
@@ -331,7 +336,7 @@ const STANDARD_AFFAIRS_ROOM: SectAffairsRoomPresentation = {
|
||||
sigil: '执',
|
||||
name: '值日执事',
|
||||
identity: '值日执事',
|
||||
responsibility: '登记山门勤务、巡视与日常交付。',
|
||||
responsibility: '负责日常委托。',
|
||||
greeting: '今日事务都在这里。你要先看哪一件?',
|
||||
},
|
||||
weekly: {
|
||||
@@ -339,7 +344,7 @@ const STANDARD_AFFAIRS_ROOM: SectAffairsRoomPresentation = {
|
||||
sigil: '簿',
|
||||
name: '功簿执事',
|
||||
identity: '功簿执事',
|
||||
responsibility: '查验本周功簿、小比与悬赏。',
|
||||
responsibility: '负责周常委托。',
|
||||
greeting: '本周卷宗已经归拢,你可逐项查验。',
|
||||
},
|
||||
promotion: {
|
||||
@@ -347,7 +352,7 @@ const STANDARD_AFFAIRS_ROOM: SectAffairsRoomPresentation = {
|
||||
sigil: '传',
|
||||
name: '传功长老',
|
||||
identity: '传功长老',
|
||||
responsibility: '核验身份进境与晋升试炼。',
|
||||
responsibility: '负责晋升试炼。',
|
||||
greeting: '晋升不可躁进。先看看你当前应过的关。',
|
||||
},
|
||||
},
|
||||
@@ -378,8 +383,8 @@ export const STANDARD_SECT_PRESENTATION: Omit<
|
||||
meridianLoadout: '参悟方案',
|
||||
abilityChanges: '神通变化',
|
||||
returnToAffairs: '返回事务堂',
|
||||
sweepActivity: '宗门勤务',
|
||||
sweepCanvasLabel: '宗门勤务游戏画布',
|
||||
sweepActivity: '清扫山门',
|
||||
sweepCanvasLabel: '清扫山门游戏画布',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -414,10 +419,16 @@ export function resolveSectPresentation(
|
||||
) as SectAffairsTaskKind[]
|
||||
).map((kind) => [
|
||||
kind,
|
||||
{
|
||||
...STANDARD_SECT_PRESENTATION.affairsRoom.taskNpcs[kind],
|
||||
...theme?.affairsRoom?.taskNpcs?.[kind],
|
||||
},
|
||||
(() => {
|
||||
const standard = STANDARD_SECT_PRESENTATION.affairsRoom.taskNpcs[kind];
|
||||
const override = theme?.affairsRoom?.taskNpcs?.[kind];
|
||||
return {
|
||||
...standard,
|
||||
id: override?.id ?? standard.id,
|
||||
name: override?.name ?? standard.name,
|
||||
greeting: override?.greeting ?? standard.greeting,
|
||||
};
|
||||
})(),
|
||||
]),
|
||||
) as Record<SectAffairsTaskKind, SectRoomNpcPresentation>;
|
||||
const resolved: ResolvedSectPresentation = {
|
||||
@@ -437,7 +448,12 @@ export function resolveSectPresentation(
|
||||
STANDARD_SECT_PRESENTATION.affairsRoom.description,
|
||||
taskNpcs: affairsTaskNpcs,
|
||||
},
|
||||
terms: { ...STANDARD_SECT_PRESENTATION.terms, ...theme?.terms },
|
||||
terms: {
|
||||
...STANDARD_SECT_PRESENTATION.terms,
|
||||
...theme?.terms,
|
||||
sweepActivity: STANDARD_SECT_PRESENTATION.terms.sweepActivity,
|
||||
sweepCanvasLabel: STANDARD_SECT_PRESENTATION.terms.sweepCanvasLabel,
|
||||
},
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(resolved.facilityLabels)) {
|
||||
|
||||
Reference in new issue
Block a user