init with react+axum+spacetimedb
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 18:06:23 +08:00
commit cbc27bad4a
20199 changed files with 883714 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
import { describe, expect, it } from 'vitest';
import { AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS } from '../data/affinityLevels';
import { getCurrencyName } from '../data/economy';
import { WorldType } from '../types';
import { normalizeCustomWorldProfile } from './customWorld';
describe('normalizeCustomWorldProfile', () => {
it('forces NPC backstory chapter thresholds to match shared affinity levels', () => {
const rawChapterThresholds = [20, 40, 65, 85];
const rawProfile = {
name: '裂谷边城',
playableNpcs: [
{
name: '沈砺',
title: '灰炬向导',
role: '向导',
description: '常年带人穿过裂谷旧道。',
backstory: '曾在塌桥夜里失去整支同行队伍。',
personality: '谨慎寡言,却记得每一道风口。',
motivation: '想查清旧道频繁异变的根源。',
combatStyle: '短弓牵制后再逼近补刀。',
initialAffinity: 18,
relationshipHooks: ['带路', '旧案'],
tags: ['裂谷', '向导'],
backstoryReveal: {
publicSummary: '他只说自己熟悉旧道。',
chapters: rawChapterThresholds.map((affinityRequired, index) => ({
id: `playable-${index + 1}`,
title: `章节${index + 1}`,
affinityRequired,
teaser: `提示${index + 1}`,
content: `内容${index + 1}`,
contextSnippet: `摘要${index + 1}`,
})),
},
skills: [
{ name: '灰炬起手', summary: '先以火光扰乱视线。', style: '起手压制' },
{ name: '窄道游移', summary: '借地形不断换位牵制。', style: '机动周旋' },
{ name: '崖风绝射', summary: '抓住破绽给出终结一箭。', style: '爆发终结' },
],
initialItems: [
{ name: '旧道短弓', category: '武器', quantity: 1, rarity: 'rare', description: '磨损严重却极趁手。', tags: ['裂谷'] },
{ name: '裂谷补给', category: '消耗品', quantity: 2, rarity: 'uncommon', description: '防风与止血一并备齐。', tags: ['补给'] },
{ name: '断绳铜哨', category: '专属物品', quantity: 1, rarity: 'rare', description: '那场事故后仅存的信物。', tags: ['旧案'] },
],
},
],
storyNpcs: [
{
name: '裂谷巡哨蛛',
title: '巡哨怪',
role: '怪物哨兵',
description: '伏在岩壁缝间监视往来活物。',
backstory: '长期吞食矿脉异潮后逐渐拥有巡猎习性。',
personality: '极度警觉,会反复试探猎物退路。',
motivation: '守住巢穴上层不断扩大的裂口。',
combatStyle: '吐丝封路,再借高处俯冲撕咬。',
initialAffinity: -20,
relationshipHooks: ['巢穴', '异潮'],
tags: ['怪物', '裂谷'],
backstoryReveal: {
publicSummary: '它始终盘踞在峭壁阴影里。',
chapters: rawChapterThresholds.map((affinityRequired, index) => ({
id: `story-${index + 1}`,
title: `章节${index + 1}`,
affinityRequired,
teaser: `怪物提示${index + 1}`,
content: `怪物内容${index + 1}`,
contextSnippet: `怪物摘要${index + 1}`,
})),
},
skills: [
{ name: '蛛丝封步', summary: '先缠住脚步再逼近。', style: '起手压制' },
{ name: '壁缝换位', summary: '沿岩壁快速转移位置。', style: '机动周旋' },
{ name: '坠崖扑杀', summary: '从高处俯冲撕裂目标。', style: '爆发终结' },
],
initialItems: [
{ name: '硬化毒牙', category: '材料', quantity: 1, rarity: 'rare', description: '可提炼出刺激性毒液。', tags: ['怪物'] },
{ name: '粘稠丝囊', category: '材料', quantity: 2, rarity: 'uncommon', description: '能用于制作束缚陷阱。', tags: ['巢穴'] },
{ name: '矿潮节壳', category: '稀有品', quantity: 1, rarity: 'rare', description: '受异潮侵染后的外壳碎片。', tags: ['异潮'] },
],
},
],
landmarks: [
{
name: '北侧塌桥',
description: '横跨裂谷的旧桥只剩半截石拱。',
},
],
};
const profile = normalizeCustomWorldProfile(rawProfile, '玩家想要一个裂谷边城与怪物共存的世界。');
expect(
profile.playableNpcs[0]?.backstoryReveal.chapters.map(
(chapter) => chapter.affinityRequired,
),
).toEqual(AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS);
expect(
profile.storyNpcs[0]?.backstoryReveal.chapters.map(
(chapter) => chapter.affinityRequired,
),
).toEqual(AFFINITY_BACKSTORY_CHAPTER_THRESHOLDS);
});
it('resolves landmark scene NPCs and relative connections into the final scene graph', () => {
const rawProfile = {
name: '裂界巡旅',
playableNpcs: [
{
name: '岑舟',
title: '裂界行脚',
role: '引路人',
description: '擅长在断层边缘辨路。',
backstory: '长期在裂界边缘押送队伍。',
personality: '稳重少言,但反应很快。',
motivation: '想把几条旧通路重新串起来。',
combatStyle: '短兵贴身后迅速换位。',
initialAffinity: 18,
relationshipHooks: ['带路', '断层'],
tags: ['裂界', '向导'],
skills: [],
initialItems: [],
},
],
storyNpcs: [
{
name: '梁砺',
title: '桥索修补匠',
role: '修桥人',
description: '守着断桥口修缮索道。',
backstory: '曾在崩桥夜里救下半队人。',
personality: '谨慎,习惯先看绳结再说话。',
motivation: '想守住最后几条安全通路。',
combatStyle: '铁钩牵制后贴近补击。',
initialAffinity: 6,
relationshipHooks: ['断桥', '索道'],
tags: ['桥', '工匠'],
skills: [],
initialItems: [],
},
{
name: '苏雾',
title: '雾港采录者',
role: '记录员',
description: '在雾港整理各路来客口供。',
backstory: '长期记录裂雾里消失的队伍名单。',
personality: '敏感细致,总在核对细节。',
motivation: '查清名单上重复出现的名字。',
combatStyle: '保持距离,借器物扰乱节奏。',
initialAffinity: 6,
relationshipHooks: ['雾港', '名单'],
tags: ['港口', '记录'],
skills: [],
initialItems: [],
},
{
name: '顾岚',
title: '界崖巡哨',
role: '巡哨',
description: '沿着崖线巡查异动和回声。',
backstory: '常年住在界崖边的哨点里。',
personality: '警觉直接,不喜欢绕弯。',
motivation: '找出最近总在夜里响起的回声来源。',
combatStyle: '长兵抢先压住身位。',
initialAffinity: 6,
relationshipHooks: ['巡查', '崖线'],
tags: ['哨点', '崖线'],
skills: [],
initialItems: [],
},
{
name: '闻砂',
title: '砂塔守更人',
role: '守更人',
description: '夜里守着砂塔边的旧灯火。',
backstory: '见过太多从塔下走失的人。',
personality: '冷静克制,习惯留后手。',
motivation: '想确认旧塔下方的回响是否重新苏醒。',
combatStyle: '借高差压制后再收拢路线。',
initialAffinity: 6,
relationshipHooks: ['守夜', '砂塔'],
tags: ['砂塔', '旧灯'],
skills: [],
initialItems: [],
},
],
landmarks: [
{
name: '北侧塌桥',
description: '断桥上方还残留着旧索道。',
sceneNpcNames: ['梁砺'],
connections: [
{
targetLandmarkName: '雾潮码头',
relativePosition: 'south',
summary: '顺着残桥往南下坡可到雾港。',
},
],
},
{
name: '雾潮码头',
description: '潮雾会把来路和去路都遮住一半。',
sceneNpcNames: ['苏雾', '顾岚'],
connections: [],
},
],
};
const profile = normalizeCustomWorldProfile(
rawProfile,
'玩家想要一个围绕裂界断桥与雾港巡旅展开的世界。',
);
expect(profile.landmarks).toHaveLength(2);
expect(profile.landmarks[0]?.sceneNpcIds).toHaveLength(3);
expect(profile.landmarks[1]?.sceneNpcIds).toHaveLength(3);
expect(profile.landmarks[0]?.connections[0]?.targetLandmarkId).toBe(
profile.landmarks[1]?.id,
);
expect(profile.landmarks[1]?.connections.some(
(connection) => connection.targetLandmarkId === profile.landmarks[0]?.id,
)).toBe(true);
});
it('compiles and preserves owned setting layers for runtime consumption', () => {
const profile = normalizeCustomWorldProfile(
{
name: '雾潮港',
summary: '被潮灾旧闻反复撕开的边港。',
tone: '潮湿、迷雾、压抑',
playerGoal: '查清港区失踪名单为何重复出现',
templateWorldType: WorldType.WUXIA,
ownedSettingLayers: {
ruleProfile: {
resourceLabels: {
hp: '潮命',
mp: '潮息',
maxHp: '潮命上限',
maxMp: '潮息上限',
damage: '潮势',
guard: '潮护',
range: '潮距',
cooldown: '回潮',
manaCost: '潮息消耗',
currency: '雾银',
},
economyProfile: {
initialCurrency: 188,
},
},
semanticAnchor: {
genreSignals: ['海岸悬疑'],
conflictForms: ['追查失踪'],
institutionTypes: ['港务'],
tabooTypes: ['回潮夜'],
carrierTypes: ['航图'],
forceSystemTypes: ['潮汐'],
atmosphereTags: ['迷雾'],
},
},
},
'玩家想要一个围绕迷雾港区与潮灾旧闻展开的世界。',
);
expect(profile.ownedSettingLayers?.ruleProfile.resourceLabels.currency).toBe(
'雾银',
);
expect(profile.ownedSettingLayers?.ruleProfile.economyProfile.initialCurrency).toBe(
188,
);
expect(getCurrencyName(WorldType.CUSTOM, profile)).toBe('雾银');
expect(
profile.ownedSettingLayers?.compatibilityProfile?.legacyTemplateWorldType,
).toBe(WorldType.WUXIA);
expect(
profile.ownedSettingLayers?.referenceProfile.creatureArchetypes.length,
).toBeGreaterThan(0);
});
});