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,392 @@
import {
Character,
CustomWorldNpc,
CustomWorldPlayableNpc,
CustomWorldProfile,
CustomWorldRoleInitialItem,
EquipmentSlotId,
InventoryItem,
} from '../types';
import {
buildRuntimeCustomWorldInventoryItems,
getRuntimeCustomWorldProfile,
type RuntimeCustomWorldItemQueryOptions,
} from './customWorldRuntime';
const CATEGORY_ORDER = new Map<string, number>([
['武器', 0],
['护甲', 1],
['饰品', 2],
['消耗品', 3],
['材料', 4],
['稀有品', 5],
['专属物品', 6],
]);
const STOP_PHRASES = new Set([
'这个世界',
'当前世界',
'玩家进入',
'玩家核心',
'世界设定',
'世界概述',
'世界基调',
'角色背景',
'角色描述',
'角色设定',
'角色故事',
'剧情关键',
'后续冒险',
'完整角色',
'当前局<E5898D>?',
'进入世界',
'核心目标',
'可扮<E58FAF>?',
'主角候<E8A792>?',
'主要角色',
'当前角色',
'这趟旅程',
'No retreat',
'真正起点',
]);
const THEME_TAG_RULES: Array<{ pattern: RegExp; tags: string[] }> = [
{ pattern: /range|bow|shot|sniper|scout/i, tags: ['range', 'mobility', 'explore', 'weapon'] },
{ pattern: /blade|sword|slash|duel|charge/i, tags: ['melee', 'combat', 'weapon'] },
{ pattern: /fist|hammer|burst|smash|impact/i, tags: ['burst', 'combat', 'weapon'] },
{ pattern: /armor|shield|guard|wall|vanguard/i, tags: ['guard', 'defense', 'armor'] },
{ pattern: /medic|herb|potion|heal|remedy/i, tags: ['alchemy', 'healing', 'supply'] },
{ pattern: /rune|sigil|spell|mana|arcane|focus/i, tags: ['mana', 'arcane', 'glyph', 'focus'] },
{ pattern: /rare|relic|archive|key|history/i, tags: ['rare', 'clue', 'history', 'secret'] },
{ pattern: /travel|map|road|route|trail/i, tags: ['explore', 'route', 'supply'] },
{ pattern: /forge|craft|tool|gear|metal/i, tags: ['craft', 'material', 'forge'] },
{ pattern: /flora|seed|bloom|vine|root/i, tags: ['herb', 'alchemy', 'material'] },
];
function normalizeExplicitItemCategory(category: string) {
const normalized = category.trim();
return normalized === '专属物' ? '专属物品' : normalized;
}
function inferEquipmentSlotFromCategory(category: string): EquipmentSlotId | null {
const normalized = normalizeExplicitItemCategory(category);
if (normalized === '武器') return 'weapon';
if (normalized === '护甲') return 'armor';
if (
normalized === '饰品'
|| normalized === '稀有品'
|| normalized === '专属物品'
) {
return 'relic';
}
return null;
}
function buildExplicitRoleInventoryItem(
role: CustomWorldPlayableNpc | CustomWorldNpc,
item: CustomWorldRoleInitialItem,
index: number,
): InventoryItem {
const category = normalizeExplicitItemCategory(item.category);
return {
id: `custom-role-item:${role.id}:${index + 1}`,
category,
name: item.name,
quantity: Math.max(1, item.quantity),
rarity: item.rarity,
tags: [...item.tags],
description: item.description,
equipmentSlotId: inferEquipmentSlotFromCategory(category),
runtimeMetadata: {
origin: 'ai_compiled',
generationChannel: 'discovery',
seedKey: `${role.id}:${index + 1}`,
relationAnchor: {
type: 'npc',
npcId: role.id,
npcName: role.name,
roleText: role.role,
},
sourceReason: `${role.name}在自定义世界开局时自带的初始物品。`,
},
};
}
function buildExplicitRoleInventoryItems(
role: CustomWorldPlayableNpc | CustomWorldNpc | null,
) {
if (!role) {
return [] as InventoryItem[];
}
return role.initialItems.map((item, index) =>
buildExplicitRoleInventoryItem(role, item, index),
);
}
function resolveCustomWorldRole(
profile: CustomWorldProfile,
character: Character,
) {
return profile.playableNpcs.find(role => role.id === character.id)
?? profile.storyNpcs.find(role => role.id === character.id)
?? profile.playableNpcs.find(role => role.name === character.name)
?? profile.storyNpcs.find(role => role.name === character.name)
?? null;
}
function dedupeStrings(values: string[], max = 32) {
return [...new Set(values.map(value => value.trim()).filter(Boolean))].slice(0, max);
}
function sortInventoryByCategory(items: InventoryItem[]) {
return [...items].sort((left, right) => {
const categoryDelta = (CATEGORY_ORDER.get(left.category) ?? 99) - (CATEGORY_ORDER.get(right.category) ?? 99);
if (categoryDelta !== 0) {
return categoryDelta;
}
return left.name.localeCompare(right.name, 'zh-Hans-CN');
});
}
function collectPhrases(sourceTexts: string[]) {
return sourceTexts.flatMap(text =>
text
.split(/[[\]\s<EFBFBD>?.!?:()<EFBFBD>?]+/u)
.map(segment => segment.trim())
.filter(segment => segment.length >= 2 && segment.length <= 12)
.filter(segment => !STOP_PHRASES.has(segment)),
);
}
function collectChineseNgrams(value: string, minSize = 2, maxSize = 4, limit = 16) {
const source = value.replace(/[^\u4e00-\u9fa5]/g, '');
const grams: string[] = [];
for (let size = minSize; size <= maxSize; size += 1) {
for (let index = 0; index <= source.length - size; index += 1) {
const gram = source.slice(index, index + size);
if (STOP_PHRASES.has(gram)) {
continue;
}
grams.push(gram);
if (grams.length >= limit) {
return grams;
}
}
}
return grams;
}
function buildKeywordBundle(
profile: CustomWorldProfile,
character: Character,
role: CustomWorldPlayableNpc | CustomWorldNpc | null,
) {
const roleTexts = [
role?.title ?? '',
role?.description ?? '',
role?.backstory ?? '',
role?.backstoryReveal.publicSummary ?? '',
role?.combatStyle ?? '',
...(role?.skills.map(skill => `${skill.name} ${skill.summary} ${skill.style}`) ?? []),
...(role?.initialItems.map(item => `${item.name} ${item.category} ${item.description}`) ?? []),
...(role?.tags ?? []),
];
const characterTexts = [
character.description,
character.backstory,
character.personality,
...(character.combatTags ?? []),
];
const worldTexts = [
profile.name,
profile.settingText,
profile.summary,
profile.tone,
profile.playerGoal,
];
const sourceTexts = [...roleTexts, ...characterTexts, ...worldTexts].filter(Boolean);
const phrases = collectPhrases(sourceTexts);
const ngrams = [
...collectChineseNgrams(role?.title ?? '', 2, 4, 12),
...collectChineseNgrams(role?.combatStyle ?? '', 2, 4, 12),
...collectChineseNgrams((role?.tags ?? []).join(' '), 2, 4, 10),
...collectChineseNgrams(profile.name, 2, 4, 10),
];
const heuristics = THEME_TAG_RULES
.filter(rule => rule.pattern.test(sourceTexts.join(' ')))
.flatMap(rule => rule.tags);
return {
preferredTags: dedupeStrings([
...(role?.tags ?? []),
...(role?.initialItems.flatMap(item => item.tags) ?? []),
...(character.combatTags ?? []),
...heuristics,
], 18),
keywords: dedupeStrings([
...phrases,
...ngrams,
...(role?.skills.map(skill => skill.name) ?? []),
...(role?.initialItems.map(item => item.name) ?? []),
...heuristics,
], 36),
};
}
function queryItems(
seedKey: string,
baseOptions: RuntimeCustomWorldItemQueryOptions,
fallbackOptions?: RuntimeCustomWorldItemQueryOptions,
) {
const items = buildRuntimeCustomWorldInventoryItems(seedKey, baseOptions);
const categoryFallbackTriggered = Boolean(
fallbackOptions
&& baseOptions.categories?.length
&& items.some(item => !baseOptions.categories!.includes(item.category)),
);
if ((items.length > 0 && !categoryFallbackTriggered) || !fallbackOptions) {
return items;
}
return buildRuntimeCustomWorldInventoryItems(seedKey, fallbackOptions);
}
function mergeUniqueItems(...groups: InventoryItem[][]) {
const result: InventoryItem[] = [];
const seen = new Set<string>();
groups.flat().forEach(item => {
const key = `${item.category}:${item.name}`;
if (seen.has(key)) {
return;
}
seen.add(key);
result.push(item);
});
return result;
}
export function buildCustomWorldStarterEquipmentItems(
character: Character,
explicitProfile?: CustomWorldProfile | null,
) {
const profile = explicitProfile ?? getRuntimeCustomWorldProfile();
if (!profile) {
return {
weapon: null,
armor: null,
relic: null,
} satisfies Record<EquipmentSlotId, InventoryItem | null>;
}
const role = resolveCustomWorldRole(profile, character);
const explicitItems = buildExplicitRoleInventoryItems(role);
const explicitWeapon =
explicitItems.find(item => item.equipmentSlotId === 'weapon') ?? null;
const explicitArmor =
explicitItems.find(item => item.equipmentSlotId === 'armor') ?? null;
const explicitRelic =
explicitItems.find(item => item.equipmentSlotId === 'relic') ?? null;
const bundle = buildKeywordBundle(profile, character, role);
const baseTextKeywords = bundle.keywords;
const baseTags = bundle.preferredTags;
const [weapon] = queryItems(`equipment:${character.id}:weapon`, {
count: 1,
categories: ['武器'],
rarityFloor: 'rare',
preferredTags: dedupeStrings([...baseTags, 'weapon', '战斗']),
keywords: dedupeStrings([...baseTextKeywords, role?.combatStyle ?? '', '武器', '战斗']),
});
const [armor] = queryItems(`equipment:${character.id}:armor`, {
count: 1,
categories: ['护甲'],
rarityFloor: 'rare',
preferredTags: dedupeStrings([...baseTags, 'armor', '防护', '护体']),
keywords: dedupeStrings([...baseTextKeywords, role?.personality ?? character.personality, '护甲', '守御']),
});
const [relic] = queryItems(`equipment:${character.id}:relic`, {
count: 1,
categories: ['饰品'],
rarityFloor: 'rare',
preferredTags: dedupeStrings([...baseTags, 'relic', 'rare', 'mana', '线索']),
keywords: dedupeStrings([...baseTextKeywords, profile.playerGoal, profile.summary, '信物', '关键']),
}, {
count: 1,
categories: ['饰品', '稀有品', '专属物品'],
rarityFloor: 'rare',
preferredTags: dedupeStrings([...baseTags, 'relic', 'rare', 'mana', '线索']),
keywords: dedupeStrings([...baseTextKeywords, profile.playerGoal, profile.summary, '信物', '关键']),
});
return {
weapon: explicitWeapon ?? weapon ?? null,
armor: explicitArmor ?? armor ?? null,
relic: explicitRelic ?? relic ?? null,
} satisfies Record<EquipmentSlotId, InventoryItem | null>;
}
export function buildCustomWorldStarterInventoryItems(
character: Character,
explicitProfile?: CustomWorldProfile | null,
) {
const profile = explicitProfile ?? getRuntimeCustomWorldProfile();
if (!profile) {
return [] as InventoryItem[];
}
const role = resolveCustomWorldRole(profile, character);
const explicitItems = buildExplicitRoleInventoryItems(role);
const bundle = buildKeywordBundle(profile, character, role);
const consumables = queryItems(`inventory:${character.id}:consumables`, {
count: 2,
quantity: 2,
categories: ['消耗品'],
preferredTags: dedupeStrings([...bundle.preferredTags, 'healing', 'mana', '补给', '探索']),
keywords: dedupeStrings([
...bundle.keywords,
role?.combatStyle ?? '',
...explicitItems.map(item => item.name),
'调息',
'续战',
]),
});
const materials = queryItems(`inventory:${character.id}:materials`, {
count: 1,
quantity: 2,
categories: ['材料'],
preferredTags: dedupeStrings([...bundle.preferredTags, 'material', 'forge', 'alchemy']),
keywords: dedupeStrings([...bundle.keywords, role?.backstory ?? character.backstory, '材料']),
});
const rareUtility = queryItems(`inventory:${character.id}:rare-utility`, {
count: 1,
categories: ['饰品', '稀有品'],
rarityFloor: 'uncommon',
preferredTags: dedupeStrings([...bundle.preferredTags, 'relic', 'rare', '线索', '寻路']),
keywords: dedupeStrings([...bundle.keywords, profile.settingText, profile.summary, '线索', '寻路']),
});
const signature = queryItems(`inventory:${character.id}:signature`, {
count: 1,
categories: ['专属物品', '稀有品'],
rarityFloor: 'rare',
preferredTags: dedupeStrings([...bundle.preferredTags, '剧情关键', '异变', '旧史', 'rare']),
keywords: dedupeStrings([...bundle.keywords, profile.playerGoal, profile.name, '信物', '关键']),
});
const merged = mergeUniqueItems(explicitItems, consumables, materials, rareUtility, signature);
if (merged.length >= 5) {
return sortInventoryByCategory(merged.slice(0, 5));
}
const filler = queryItems(`inventory:${character.id}:filler`, {
count: 5 - merged.length,
categories: ['消耗品', '材料', '饰品', '稀有品', '专属物品'],
preferredTags: bundle.preferredTags,
keywords: bundle.keywords,
});
return sortInventoryByCategory(mergeUniqueItems(merged, filler).slice(0, 5));
}