初始仓库迁移
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-04 23:57:06 +08:00
parent 80986b790d
commit c49c64896a
18446 changed files with 532435 additions and 2 deletions

View File

@@ -0,0 +1,470 @@
import {
CustomWorldItem,
CustomWorldLandmark,
CustomWorldNpc,
CustomWorldPlayableNpc,
CustomWorldProfile,
ItemRarity,
WorldType,
} from '../types';
const CUSTOM_WORLD_RARITIES: ItemRarity[] = ['common', 'uncommon', 'rare', 'epic', 'legendary'];
export const CUSTOM_WORLD_GENERATION_SYSTEM_PROMPT = `浣犳槸鍍忕礌鍔ㄤ綔 RPG 鐨勪笘鐣岃璁捐甯堛€備綘鍙兘杈撳嚭 JSON 瀵硅薄锛屼笉鑳借緭鍑鸿В閲娿€丮arkdown 鎴栦唬鐮佸潡銆?
杈撳嚭鏍煎紡蹇呴』涓ユ牸绗﹀悎锛?
{
"name": "涓栫晫鍚嶇О",
"subtitle": "涓栫晫鍓爣棰?,
"summary": "80瀛椾互鍐呯殑涓栫晫姒傝堪",
"tone": "涓栫晫姘涘洿涓庡熀璋?,
"playerGoal": "鐜╁杩涘叆杩欎釜涓栫晫鍚庣殑鏍稿績鐩爣",
"playableNpcs": [
{
"name": "濮撳悕",
"title": "韬唤鎴栫О鍙?,
"description": "40瀛椾互鍐呯畝浠?,
"backstory": "瑙掕壊鑳屾櫙",
"personality": "鎬ф牸鍏抽敭璇?,
"combatStyle": "鎴樻枟椋庢牸",
"tags": ["鏍囩1", "鏍囩2"]
}
],
"storyNpcs": [
{
"name": "濮撳悕",
"role": "韬唤鎴栬亴涓?,
"description": "40瀛椾互鍐呯畝浠?,
"motivation": "瑙掕壊鍔ㄦ満",
"relationshipHooks": ["鍙笌鐜╁浜掑姩鐨勫垏鍏ョ偣1", "鍒囧叆鐐?"]
}
],
"items": [
{
"name": "鐗╁搧鍚?,
"category": "姝﹀櫒|鎶ょ敳|楗板搧|娑堣€楀搧|鏉愭枡|绋€鏈夊搧|涓撳睘鐗?,
"rarity": "common|uncommon|rare|epic|legendary",
"description": "鐗╁搧鎻忚堪",
"tags": ["鏍囩1", "鏍囩2"]
}
],
"landmarks": [
{
"name": "鍦版爣鍚?,
"description": "鍦版爣鎻忚堪"
}
]
}
纭€ц鍒欙細
- 鎵€鏈夋枃鏈繀椤绘槸涓枃銆?
- 涓栫晫鍚嶇О銆丯PC 鍚嶇О銆佺墿鍝佸悕绉拌鍏蜂綋锛屼笉瑕佷娇鐢ㄢ€滆鑹蹭竴鈥濃€淣PC 涓€鈥濃€滅鍣ㄤ竴鍙封€濊繖绫诲崰浣嶈瘝銆?
- 涓嶈濂楃敤鍥哄畾鐨勨€滄渚犲簳绋库€濇垨鈥滀粰渚犲簳绋库€濇潵鍐欙紝鎵€鏈夎鑹层€佸娍鍔涖€佸湴鏍囥€佺墿鍝侀兘蹇呴』鐩存帴浠庣帺瀹剁粰鍑虹殑涓栫晫璁惧畾涓帹瀵笺€?
- playableNpcs 蹇呴』杈撳嚭 5 鍚嶅畬鏁磋鑹诧紱storyNpcs 杈撳嚭 8 鍒?12 鍚嶁€滄牳蹇?NPC 鏍锋湰鈥濓紱items 杈撳嚭 12 鍒?20 浠垛€滃叧閿墿鍝佹牱鏈€濓紱landmarks 杈撳嚭 4 鍒?6 涓湴鏍囥€?
- playableNpcs 蹇呴』閫傚悎琚帺瀹跺€熺敤鎴栨敼鍐欐垚涓昏璁惧畾锛屽洜姝よ儗鏅€佹€ф牸銆佹垬鏂楅鏍奸兘瑕佸畬鏁淬€?
- storyNpcs 蹇呴』瑕嗙洊涓嶅悓绀句細瑙掕壊锛屼笉鑳藉叏鏄垬鏂楀瀷浜虹墿銆?
- items 蹇呴』鍚屾椂鍖呭惈甯哥敤鐗╄祫涓庡叿鏈夊墽鎯呮剰鍛崇殑鍏抽敭鐗┿€?
- 绯荤粺浼氬湪浣犺緭鍑哄悗锛屽熀浜庢湰鍦扮礌鏉愬簱缁х画鎵╁睍鍒板畬鏁寸殑 30+ NPC 涓?1000+ 鐗╁搧妗f锛屾墍浠ヤ綘瑕佷紭鍏堝啓鈥滄渶鏍稿績銆佹渶鏈夎鲸璇嗗害鈥濈殑瑙掕壊鍜屽叧閿墿鍝侀鏋躲€?
- 涓嶈寮曠敤鐜板疄涓栫晫鍝佺墝銆両P 鎴栫幇鎴愪綔鍝佽鑹层€俙;
function toText(value: unknown) {
return typeof value === 'string' ? value.trim() : '';
}
function toRecordArray(value: unknown) {
return Array.isArray(value)
? value.filter(item => item && typeof item === 'object') as Array<Record<string, unknown>>
: [];
}
function normalizeTags(value: unknown, fallbackTags: string[] = []) {
const tags = Array.isArray(value)
? value.map(item => toText(item)).filter(Boolean)
: [];
return [...new Set((tags.length > 0 ? tags : fallbackTags).filter(Boolean))].slice(0, 5);
}
function normalizeWorldType(value: unknown, sourceText: string) {
const worldType = toText(value).toUpperCase();
if (worldType === WorldType.WUXIA || worldType === WorldType.XIANXIA) {
return worldType;
}
return inferWorldTypeFromSetting(sourceText);
}
function normalizeRarity(value: unknown, fallback: ItemRarity = 'rare'): ItemRarity {
const rarity = toText(value).toLowerCase() as ItemRarity;
return CUSTOM_WORLD_RARITIES.includes(rarity) ? rarity : fallback;
}
function slugify(value: string) {
const ascii = value
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
.replace(/^-+|-+$/g, '');
if (ascii) {
return ascii.slice(0, 24);
}
return 'entry';
}
function createEntryId(prefix: string, label: string, index: number) {
return `${prefix}-${slugify(label || `${prefix}-${index + 1}`)}-${index + 1}`;
}
function inferWorldTypeFromSetting(settingText: string) {
if (/[浠欑伒淇湡椋炲崌鐏佃剦瀹楅棬娉曞櫒澶╁绉樺閬撻鏄熻垷]/u.test(settingText)) {
return WorldType.XIANXIA;
}
return WorldType.WUXIA;
}
function buildSeedPhrase(settingText: string, fallback: string) {
const compact = settingText.replace(/\s+/g, '').trim();
return compact ? compact.slice(0, 10) : fallback;
}
function buildWorldName(settingText: string, worldType: WorldType) {
const seed = buildSeedPhrase(settingText, worldType === WorldType.XIANXIA ? '鐏垫疆' : '姹熸箹');
const suffix = worldType === WorldType.XIANXIA ? '鐣? : '褰?;
return `${seed}${suffix}`;
}
function buildFallbackPlayableNpcs(settingText: string, worldType: WorldType): CustomWorldPlayableNpc[] {
const seed = buildSeedPhrase(settingText, worldType === WorldType.XIANXIA ? '浜戞捣' : '姹熸箹');
if (worldType === WorldType.XIANXIA) {
return [
{
id: 'playable-su-yunhui-1',
name: '鑻忎簯寰?,
title: `${seed}宸$晫浣縛,
description: '鐔熸倝鐏佃剦寮傚姩涓庢棫绂佸埗鐥曡抗锛屽杽浜庤拷绱㈠け鎺ф簮澶淬€?,
backstory: `鑻忎簯寰婃浘濂夊懡宸℃煡${seed}闄勮繎鐨勭澧冭缂濓紝鍗村湪涓€娆″け鎺т簨鏁呬腑澶卞幓浜嗗悓闂ㄤ笌鏃ц亴浣嶏紝濡備粖鍙兂鏌ユ竻鐪熸鐨勫箷鍚庢搷鐩樿€呫€俙,
personality: '鍐烽潤銆佸厠鍒躲€佽拷鏍圭┒搴?,
combatStyle: '浠ョ伒鍒冧笌鐭拻鍘嬪埗鎴樺眬锛屾搮闀垮厛鎵嬪皝閿佷笌杩藉嚮',
tags: ['杩借抗', '绂佸埗', '鐏靛垉'],
},
{
id: 'playable-wen-xinglan-2',
name: '闂绘槦婢?,
title: '鏄熻垷娈嬭埅瀹?,
description: '鎿呴暱鍦ㄩ珮绌轰笌闄╁湴琛屽姩锛岀粡楠屾潵鑷极闀胯€屽け璐ョ殑杩滆埅銆?,
backstory: `闂绘槦婢滄浘鏄┛琛岃鍩熺殑鏄熻垷寮曡埅浜猴紝鍦?{seed}涓婄┖鐨勪竴娆″潬鑸悗娴佽惤姝ょ晫锛屽彧鍓╀竴寮犳畫缂鸿埅鍥句笌涓€韬笉鑲唲鐏殑鎵у康銆俙,
personality: '鏁忛攼銆佸菇榛樸€佹垝蹇冨緢閲?,
combatStyle: '鍋忓悜鏈哄姩杩滅▼锛屼範鎯竟绉诲姩杈瑰鎵捐嚧鍛借搴?,
tags: ['鏄熻垷', '杩滅▼', '鏈哄姩'],
},
{
id: 'playable-bai-luochen-3',
name: '鐧借惤灏?,
title: '娈嬪嵎绗︿慨',
description: '闈犳畫缂哄彜鍗疯嚜淇垚鏈紝瀵圭闂讳笌鏃у彶鏈夊紓甯告墽鐫€銆?,
backstory: `鐧借惤灏樺嚭韬钩鍑★紝鍗村洜鍋跺緱涓€鍗锋潵鑷?{seed}娣卞鐨勬畫鍗疯€岃笍涓婁慨琛屼箣璺€備负浜嗗紕娓呮畫鍗蜂负浣曞弽澶嶆寚鍚戝悓涓€澶勭伨鍘勶紝浠栦富鍔ㄨ蛋杩涢鏆翠腑蹇冦€俙,
personality: '娓╁拰銆佺粏鑷淬€佹剰蹇楅〗寮?,
combatStyle: '浠ョ绠撱€佹硶闃靛拰鎺у満瑙侀暱锛岄噸瑙嗗噯澶囦笌鑺傚',
tags: ['绗︿慨', '鎺у満', '鍙ゅ嵎'],
},
];
}
return [
{
id: 'playable-shen-tingyu-1',
name: '娌堝惉闆?,
title: `${seed}娓镐緺`,
description: '鎿呴暱浠庣悍涔辩嚎绱㈤噷鏁寸悊鐪熺浉锛屼篃鑳藉湪涔卞眬閲屽厛鍑烘墜绋充綇鍦洪潰銆?,
backstory: `娌堝惉闆ㄦ浘鏇夸汉鎶ら€佷竴浠戒笌${seed}鐩稿叧鐨勫瘑淇★紝鍗村湪閫斾腑閬汉鐏彛浼忓嚮銆傝嚜閭d互鍚庯紝浠栦笉鍐嶆浛璋佸崠鍛斤紝鍙拷鏌ラ偅灏佷俊鍒板簳瑙﹀姩浜嗚皝鐨勫埄鐩娿€俙,
personality: '娌夌潃銆佹灉鏂€侀噸瑙嗘壙璇?,
combatStyle: '鍒€鍓戝吋淇紝鑳藉揩閫熷垏鍏ユ闈㈠眬鍔垮苟缁存寔鍘嬪埗',
tags: ['娓镐緺', '杩芥煡', '鍒€鍓?],
},
{
id: 'playable-pei-jinghong-2',
name: '瑁存儕楦?,
title: '澶卞娍鍓戜緧',
description: '鍑鸿韩鏃у娍鍔涙牳蹇冿紝瑙佽繃鏉冨娍濡備綍鎶婁汉鎺ㄥ悜缁濊矾銆?,
backstory: `瑁存儕楦挎湰鏄浛鏃т富鎵у墤鐨勪汉锛岀洿鍒颁竴鍦哄洿鐚庢妸浠栦笌${seed}鏈夊叧鐨勭瀵嗕竴骞舵嫋鍏ヨ妗堛€傚浠婁粬鍙兂鍦ㄥ悇鏂瑰娍鍔涘弽搴旇繃鏉ヤ箣鍓嶅厛涓€姝ユ嬁鍒扮湡鐩搞€俙,
personality: '楂樺偛銆佹晱閿愩€佷笉杞讳俊浜?,
combatStyle: '浠ュ揩鍓戜笌韬硶瑙侀暱锛屾搮闀跨偣鏉€涓庡帇鑺傚',
tags: ['蹇墤', '鏃ф', '娼滆'],
},
{
id: 'playable-gu-cangfeng-3',
name: '椤捐棌閿?,
title: '闀栧眬鏆楀崼',
description: '甯稿勾娓歌蛋鐏拌壊鍦板甫锛岀煡閬撴€庢牱鍦ㄥ嵄闄╅潬杩戝墠鍏堢湅鍑洪鍚戙€?,
backstory: `椤捐棌閿嬫浘璐熻矗鎶ら€佺┛瓒?{seed}鍦板甫鐨勯噸瑕佷汉鐗╋紝鍚庢潵鏁存敮闃熶紞鍙墿浠栦竴浜烘椿鐫€鍥炴潵銆傞偅娆℃儴璐ヨ浠栧啀涓嶈偗杞绘槗杞韩绂诲紑浠讳綍鍙枒鐥曡抗銆俙,
personality: '璋ㄦ厧銆佽€愬績銆佹瀬鏈夐煣鎬?,
combatStyle: '鍋忓悜涓窛绂昏瘯鎺笌绮惧噯鐖嗗彂锛屽厛鐪嬬牬鍐嶄笅鎵?,
tags: ['鏆楀崼', '浼忓嚮', '闀栧眬'],
},
];
}
function buildFallbackStoryNpcs(settingText: string, worldType: WorldType): CustomWorldNpc[] {
const seed = buildSeedPhrase(settingText, worldType === WorldType.XIANXIA ? '浜戞捣' : '姹熸箹');
const base: Array<[string, string, string, string]> = worldType === WorldType.XIANXIA
? [
['椤鹃棶娓?, '鐏垫笭绠′簨', `璐熻矗鐪嬪畧${seed}闄勮繎鐨勭伒娓狅紝鏈€鍏堝療瑙夌伒娼紓鍙樸€俙, '鎯充繚浣忕伒娓犱笌闄勮繎鏉戣惤锛屼笉鎰夸笂灞傛妸鐏惧彉缁х画鍘嬩笅鍘?],
['绁濇媯闇?, '鑽渻鎵т簨', '鏀舵不杩囪澶氬洜绉樺寮傚彉鍙椾激鐨勪汉锛屽寮傚彉瑙勫緥鏈夎嚜宸辩殑鍒ゆ柇銆?, '鎯虫壘鍒拌浼よ€呭弽澶嶆伓鍖栫殑鏍瑰洜'],
['榻愮収澶?, '纰戝綍鍙镐功', '瀹堢潃涓€鎵瑰彜纰戞嫇鏈紝璁板緱璁稿琚埢鎰忔姽鎺夌殑鏃у彶銆?, '鎯虫妸琚殣鍘荤殑鏃ф閲嶆柊鎷煎畬鏁?],
['鍙舵矇鑸?, '鏁d慨鍟嗚穿', '寰€鏉ヤ簬鍚勫绉樺杈圭紭锛屼粈涔堝嵄闄╅兘鍋氳繃涓€鐐逛拱鍗栥€?, '鎯宠秮灞€鍔挎湭瀹氬厛璧屼竴鎶婂ぇ鐨?],
['瀹佹櫄姹?, '瑙傛槦濂充慨', '閫氳繃澶╄薄涓庤鐣屽洖鍝嶆帹婕旂伨鍘勭殑璧板悜銆?, '鎯抽樆姝㈡煇涓嵆灏嗗彂鐢熺殑澶╄薄鑺傜偣澶辨帶'],
]
: [
['涔斿畧鎴?, '娓″彛鎺屾煖', `鍦?{seed}涓€甯︾粡钀ユ秷鎭拰璐ц矾锛岃杩囪澶氫笉璇ュ嚭鐜扮殑浜恒€俙, '鎯充繚浣忚嚜宸辩殑鍟嗚矾锛屼篃鎯虫懜娓呰皝鍦ㄦ殫涓埅鏂揣婧?],
['璐洪潚妲?, '灞遍棬澶栭棬寮熷瓙', '甯稿勾瀹堝湪灞遍亾涓庡墠鍝紝瀵规潵寰€鍔垮姏鏈€涓烘晱鎰熴€?, '鎯宠瘉鏄庤嚜宸变笉鏄闂ㄦ淳闅忔椂鍙互涓㈡帀鐨勬瀛?],
['鏌充笁濞?, '鑽摵鎺屾煖', '鏀舵不杩囪澶氭睙婀栦汉锛岀煡閬撲粬浠粠鍝噷鏉ャ€佸張鍦ㄨ翰浠€涔堛€?, '鎯充繚浣忚嵂閾哄拰搴囨姢杩囩殑浜?],
['鍞愰椈绛?, '鏃у啗涔﹀悘', '閫€褰瑰悗浠嶄繚鐣欑潃鍐涗腑鏁寸悊鎯呮姤鐨勪範鎯€?, '鎯虫妸涓€妗╁啗涓棫妗堟煡涓槑鐧?],
['娓╃収涓?, '閾稿潑鍖犲笀', '璁ゅ緱鍏靛櫒缂哄彛涓庝激鐥曡儗鍚庨殣钘忕殑浜ら攱杞ㄨ抗銆?, '鎯虫壘鍒伴偅鎶婃浘姣佹帀甯堥棬蹇冭鐨勫叺鍣?],
];
return base.map(([name, role, description, motivation], index) => ({
id: createEntryId('story-npc', name, index),
name,
role,
description,
motivation,
relationshipHooks: worldType === WorldType.XIANXIA
? ['鎰挎剰鐢ㄦ儏鎶ヤ氦鎹㈠府鍔?, '瀵圭鍒朵笌寮傝薄鏈夌嫭鐗圭悊瑙?]
: ['鎵嬮噷鎻$潃鏃ф绾跨储', '瀵瑰悇鏂瑰娍鍔涚殑鍙嶅簲鏋佷负鏁忔劅'],
}));
}
function buildFallbackItems(settingText: string, worldType: WorldType): CustomWorldItem[] {
const seed = buildSeedPhrase(settingText, worldType === WorldType.XIANXIA ? '鐏垫疆' : '椋庝簯');
const base: Array<[string, string, ItemRarity, string, string[]]> = worldType === WorldType.XIANXIA
? [
['寮曠伒绗?, '娑堣€楀搧', 'uncommon', `鑳界煭鏆傜ǔ瀹?{seed}闄勮繎绱婁贡鐏垫皵鐨勭伒绗︺€俙, ['绗︾畵', '鐏垫皵']],
['鏄熺爞缃楃洏', '楗板搧', 'rare', '鍙湪绉樺涓庤鐣屽洖鍝嶄腑杈ㄨ鏂瑰悜銆?, ['瀵昏矾', '鏄熻薄']],
['鏂櫣鐏靛垉', '姝﹀櫒', 'epic', '鏇捐鏌愪綅宸$晫浣夸娇鐢ㄧ殑娈嬬己鐏靛垉锛屼粛鏈夐攱鑺掋€?, ['鐏靛垉', '鎴樻枟']],
['浜戦珦鎶ゅ績闀?, '鎶ょ敳', 'rare', '鑳藉墛寮辩伒鍘嬪啿鍑荤殑鎶ゅ績娉曞櫒銆?, ['闃叉姢', '鐏靛帇']],
['鏃у煙鎷撶鐗?, '绋€鏈夊搧', 'rare', '璁拌浇鐫€琚姽鍘绘棫鍙茬殑娈嬬墖銆?, ['绉橀椈', '鏃у彶']],
['鐣岄殭绉嶇伀', '涓撳睘鐗?, 'legendary', '涓庡ぇ瑙勬ā寮傚彉鏍规簮鏈夊叧鐨勫叧閿紩瀛愩€?, ['鍓ф儏鍏抽敭', '寮傚彉']],
]
: [
['鍥為鏁?, '娑堣€楀搧', 'uncommon', `甯稿湪${seed}鍦板甫娴侀€氱殑澶栦激鑽紝瑙佹晥寰堝揩銆俙, ['鐤椾激', '鑽墿']],
['鏂汗浠ょ墝', '绋€鏈夊搧', 'rare', '鏉ヨ嚜鏃у娍鍔涚殑浠ょ墝纰庣墖锛屽彲璇佹槑鏌愪簺浜烘浘缁忕殑韬唤銆?, ['鏃ф', '淇$墿']],
['娌夐攱鐭垁', '姝﹀櫒', 'rare', '渚夸簬杩戣韩鎼忔潃涓庢殫涓嚭鎵嬬殑鐭垁銆?, ['杩戞垬', '娼滆']],
['榛戦碁鎶よ噦', '鎶ょ敳', 'rare', '鑳芥壙鍙楄繛缁啿鍑荤殑鎶よ噦锛岄€傚悎纭纭幃鏉€銆?, ['闃插尽', '纭垬']],
['澶滆鐏專', '楗板搧', 'uncommon', '鍙湪闆惧涓庢殫宸蜂腑淇濇寔寰急绋冲畾鍏夋簮銆?, ['鎺㈢储', '澶滆']],
['瑾撶棔娈嬪墤', '涓撳睘鐗?, 'legendary', '涓€鏌勫拰琛€妗堟簮澶寸揣瀵嗙浉杩炵殑娈嬪墤銆?, ['鍓ф儏鍏抽敭', '瀹垮懡']],
];
return base.map(([name, category, rarity, description, tags], index) => ({
id: createEntryId('item', name, index),
name,
category,
rarity: normalizeRarity(rarity, 'rare'),
description,
tags: normalizeTags(tags, []),
}));
}
function buildFallbackLandmarks(settingText: string, worldType: WorldType): CustomWorldLandmark[] {
const seed = buildSeedPhrase(settingText, worldType === WorldType.XIANXIA ? '鐏垫疆' : '椋庝簯');
const base = worldType === WorldType.XIANXIA
? [
['瑁傜晫浜戦棬', `浼犺█鑳芥槧鍑?{seed}鐪熸婧愬ご鐨勯珮绌洪棬闃欍€俙, '楂?],
['娌夋槦鑽渻', '澶栬〃瀹侀潤銆佸疄闄呰寮傚寲鐏垫皵鎸佺画渚垫煋鐨勮嵂鍦冦€?, '涓?],
['鍙ょ瑙傛槦鍙?, '淇濆瓨鐫€鏃ф椂浠h娴嬭褰曠殑鏂楂樺彴銆?, '涓?],
['鏃犵伅鐣屼簳', '鎹瓒婇潬杩戜簳鍙o紝瓒婅兘鍚鏉ヨ嚜瑁傜晫娣卞鐨勫洖鍝嶃€?, '鏋侀珮'],
]
: [
['鏂ˉ鏃ч┛', `鍚勬柟浜洪┈閮戒細鍦?{seed}杈圭紭鐭殏鍋滅暀鐨勮鍐层€俙, '涓?],
['閿侀灞遍棬', '琛ㄩ潰娌夊瘋銆佸疄闄呮殫娴佹瀬澶氱殑鏃ч棬娲惧墠绾裤€?, '楂?],
['鍐烽搧閾稿潑', '鏃㈣兘琛ョ粰鍏靛櫒锛屼篃钘忕潃涓嶅皯琚帺鍩嬬殑鏃х棔杩广€?, '涓?],
['琛€鐥曞瘑寤?, '璁稿鍏抽敭浜虹墿閮芥浘浠庤繖閲岃繘鍑哄嵈涓嶆効鐣欎笅鍚嶅瓧銆?, '鏋侀珮'],
];
return base.map(([name, description, dangerLevel], index) => ({
id: createEntryId('landmark', name, index),
name,
description,
dangerLevel,
}));
}
export function buildFallbackCustomWorldProfile(settingText: string): CustomWorldProfile {
const templateWorldType = inferWorldTypeFromSetting(settingText);
const worldName = buildWorldName(settingText, templateWorldType);
const subtitle = templateWorldType === WorldType.XIANXIA ? '鐏垫疆鏈畾' : '椋庝簯鏆楁秾';
const summary = settingText.trim()
? `杩欎釜涓栫晫鍥寸粫鈥?{settingText.trim().slice(0, 28)}鈥濆睍寮€锛岃〃闈㈢З搴忓皻瀛橈紝鏆楀湴閲屽嵈宸叉湁浜哄紑濮嬩簤澶哄け鎺х殑鍏抽敭鍔涢噺銆俙
: templateWorldType === WorldType.XIANXIA
? '鐏垫疆姝e湪钄撳欢锛岀澧冧笌鏃х鍒剁浉缁уけ琛★紝鍚勬柟閮藉湪鎶㈠厛瀵绘壘鐪熸婧愬ご銆?
: '姹熸箹涓庢棫鍔垮姏涔嬮棿鐨勫钩琛℃鍦ㄧ牬瑁傦紝涓€妗╂棫妗堥噸鏂扮壍鍔ㄤ簡鎵€鏈変汉鐨勭珛鍦恒€?;
return {
id: `custom-world-${Date.now().toString(36)}-${slugify(worldName)}`,
settingText: settingText.trim(),
name: worldName,
subtitle,
summary,
tone: templateWorldType === WorldType.XIANXIA ? '绌虹伒銆佸嵄闄┿€佸眰灞傞€掕繘鐨勭伨鍙樻劅' : '绱у紶銆佸厠鍒躲€佸甫鐫€鏃ф闃村奖鐨勬睙婀栧帇杩劅',
playerGoal: templateWorldType === WorldType.XIANXIA
? '鏌ユ竻鐏垫疆澶辨帶婧愬ご锛屽苟鍦ㄥ悇澶т粰闂ㄥ弽搴旇繃鏉ヤ箣鍓嶆姠鍏堟嬁鍒板叧閿嚎绱€?
: '寰潃鏃ф鐣欎笅鐨勭棔杩硅拷鏌ュ箷鍚庝箣浜猴紝骞跺畧浣忎粛鍊煎緱鐩镐俊鐨勪汉涓庤矾銆?,
templateWorldType,
playableNpcs: buildFallbackPlayableNpcs(settingText, templateWorldType),
storyNpcs: buildFallbackStoryNpcs(settingText, templateWorldType),
items: buildFallbackItems(settingText, templateWorldType),
landmarks: buildFallbackLandmarks(settingText, templateWorldType),
};
}
function normalizePlayableNpcList(value: unknown, fallback: CustomWorldPlayableNpc[]) {
const entries = toRecordArray(value).map((item, index) => {
const name = toText(item.name) || fallback[index]?.name || `鍙壆婕旇鑹?{index + 1}`;
return {
id: createEntryId('playable-npc', name, index),
name,
title: toText(item.title) || fallback[index]?.title || '琛岃€?,
description: toText(item.description) || fallback[index]?.description || `${name}鎷ユ湁瓒冲椴滄槑鐨勮韩浠戒笌琛屽姩鐞嗙敱銆俙,
backstory: toText(item.backstory) || fallback[index]?.backstory || `${name}鑳屽悗钘忕潃灏氭湭琚畬鍏ㄦ彮寮€鐨勬棫浜嬨€俙,
personality: toText(item.personality) || fallback[index]?.personality || '鍐烽潤銆佸潥闊с€佹効鎰忚鍔?,
combatStyle: toText(item.combatStyle) || fallback[index]?.combatStyle || '鎿呴暱姝i潰鎺ㄨ繘涓庝复鍦哄簲鍙?,
tags: normalizeTags(item.tags, fallback[index]?.tags ?? ['涓昏鍊欓€?]),
} satisfies CustomWorldPlayableNpc;
});
return entries.length > 0 ? entries.slice(0, 5) : fallback;
}
function normalizeStoryNpcList(value: unknown, fallback: CustomWorldNpc[]) {
const entries = toRecordArray(value).map((item, index) => {
const name = toText(item.name) || fallback[index]?.name || `涓栫晫NPC${index + 1}`;
return {
id: createEntryId('story-npc', name, index),
name,
role: toText(item.role) || fallback[index]?.role || '鎯呮姤涓棿浜?,
description: toText(item.description) || fallback[index]?.description || `${name}涓庡綋鍓嶄笘鐣屼富绾跨籂钁涘緢娣便€俙,
motivation: toText(item.motivation) || fallback[index]?.motivation || '鎯充粠娣蜂贡閲屼繚浣忚嚜宸辨渶鐪嬮噸鐨勪笢瑗?,
relationshipHooks: normalizeTags(item.relationshipHooks, fallback[index]?.relationshipHooks ?? ['鎺屾彙鍏抽敭淇℃伅']),
} satisfies CustomWorldNpc;
});
return entries.length > 0 ? entries.slice(0, 12) : fallback;
}
function normalizeItemList(value: unknown, fallback: CustomWorldItem[]) {
const entries = toRecordArray(value).map((item, index) => {
const name = toText(item.name) || fallback[index]?.name || `鍏抽敭鐗╁搧${index + 1}`;
return {
id: createEntryId('item', name, index),
name,
category: toText(item.category) || fallback[index]?.category || '绋€鏈夊搧',
rarity: normalizeRarity(item.rarity, fallback[index]?.rarity ?? 'rare'),
description: toText(item.description) || fallback[index]?.description || `${name}涓庡綋鍓嶄笘鐣岀殑灞€鍔垮彉鍖栧瓨鍦ㄨ仈绯汇€俙,
tags: normalizeTags(item.tags, fallback[index]?.tags ?? ['涓栫晫鐗╁搧']),
} satisfies CustomWorldItem;
});
return entries.length > 0 ? entries.slice(0, 20) : fallback;
}
function normalizeLandmarkList(value: unknown, fallback: CustomWorldLandmark[]) {
const entries = toRecordArray(value).map((item, index) => {
const name = toText(item.name) || fallback[index]?.name || `鍏抽敭鍦版爣${index + 1}`;
return {
id: createEntryId('landmark', name, index),
name,
description: toText(item.description) || fallback[index]?.description || `${name}瀵瑰悗缁啋闄╁叿鏈夋槑纭奖鍝嶃€俙,
dangerLevel: toText(item.dangerLevel) || fallback[index]?.dangerLevel || '中',
} satisfies CustomWorldLandmark;
});
return entries.length > 0 ? entries.slice(0, 6) : fallback;
}
export function normalizeCustomWorldProfile(raw: unknown, settingText: string): CustomWorldProfile {
const fallback = buildFallbackCustomWorldProfile(settingText);
if (!raw || typeof raw !== 'object') {
return fallback;
}
const item = raw as Record<string, unknown>;
const worldSignalText = [
settingText,
toText(item.subtitle),
toText(item.summary),
toText(item.tone),
toText(item.playerGoal),
].join(' ');
const templateWorldType = normalizeWorldType(item.templateWorldType, worldSignalText);
const normalizedFallback = templateWorldType === fallback.templateWorldType
? fallback
: buildFallbackCustomWorldProfile(settingText);
const name = toText(item.name) || normalizedFallback.name;
return {
id: `custom-world-${Date.now().toString(36)}-${slugify(name)}`,
settingText: settingText.trim(),
name,
subtitle: toText(item.subtitle) || normalizedFallback.subtitle,
summary: toText(item.summary) || normalizedFallback.summary,
tone: toText(item.tone) || normalizedFallback.tone,
playerGoal: toText(item.playerGoal) || normalizedFallback.playerGoal,
templateWorldType,
playableNpcs: normalizePlayableNpcList(item.playableNpcs, normalizedFallback.playableNpcs),
storyNpcs: normalizeStoryNpcList(item.storyNpcs, normalizedFallback.storyNpcs),
items: normalizeItemList(item.items, normalizedFallback.items),
landmarks: normalizeLandmarkList(item.landmarks, normalizedFallback.landmarks),
};
}
export function buildCustomWorldGenerationPrompt(settingText: string) {
return [
'璇峰熀浜庝互涓嬬帺瀹惰緭鍏ョ殑涓栫晫璁惧畾锛岀敓鎴愪竴涓€傚悎鍍忕礌鍔ㄤ綔 RPG 鐨勫畬鏁磋嚜瀹氫箟涓栫晫妗f銆?,
'鐜╁杈撳叆锛?,
settingText.trim(),
'',
'棰濆瑕佹眰锛?,
'- 涓嶈浠ヤ换浣曠幇鎴愮殑姝︿緺銆佷粰渚犳垨鍏朵粬鍥哄畾妯℃澘涓哄簳绋匡紝蹇呴』鐩存帴浠庤繖娈典笘鐣岃瀹氭帹瀵间笘鐣岀粨鏋勩€?,
'- 涓栫晫蹇呴』鑳芥壙杞介暱绾垮啋闄┿€佹垬鏂椼€佹帰绱€佷氦鏄撲笌瑙掕壊鍏崇郴鍙戝睍銆?,
'- playableNpcs 瑕佺洿鎺ョ粰鍑?5 鍚嶅彲鎵紨瑙掕壊锛屾瘡鍚嶉兘瑕佹湁娓呮櫚韬唤銆佽儗鏅€佹€ф牸涓庢垬鏂楅鏍笺€?,
'- storyNpcs 鍙渶鎻愪緵 8 鍒?12 鍚嶆渶閲嶈鐨勬牳蹇?NPC 鏍锋湰锛屼絾瑕佽鐩栨儏鎶ャ€佹敮鎻淬€佷氦鏄撱€佸娍鍔涖€佸湴鏂硅璇佽€呯瓑涓嶅悓鍔熻兘銆?,
'- items 鍙渶鎻愪緵 12 鍒?20 浠舵渶鍏抽敭鐨勭墿鍝佹牱鏈紝鍏朵腑鑷冲皯鍖呭惈 1 涓墽鎯呭叧閿墿銆? 涓父鐢ㄦ秷鑰楁垨鎺㈢储鐗┿€? 涓澶囧悜鐗╁搧銆?,
'- landmarks 瑕佽兘鏀拺鍚庣画鍦板浘銆佸啋闄╁紑鍦哄拰鍓ф儏鎺ㄨ繘銆?,
'- 浣犵殑杈撳嚭浼氳绯荤粺杩涗竴姝ユ墿灞曚负瀹屾暣鐨勫ぇ鍨嬩笘鐣屾。妗堬紝鎵€浠ヨ浼樺厛淇濊瘉楠ㄦ灦椴滄槑銆佸彲鎵╁睍锛岃€屼笉鏄┓涓俱€?,
].join('\n');
}
export function buildCustomWorldReferenceText(profile: CustomWorldProfile) {
const playableNpcText = profile.playableNpcs
.slice(0, 3)
.map(npc => `- ${npc.name} / ${npc.title}锛?{npc.description}锛涜儗鏅細${npc.backstory}锛涢鏍硷細${npc.combatStyle}`)
.join('\n');
const storyNpcText = profile.storyNpcs
.slice(0, 5)
.map(npc => `- ${npc.name} / ${npc.role}锛?{npc.description}锛涘姩鏈猴細${npc.motivation}`)
.join('\n');
const itemText = profile.items
.slice(0, 6)
.map(item => `- ${item.name} / ${item.category} / ${item.rarity}锛?{item.description}`)
.join('\n');
const landmarkText = profile.landmarks
.slice(0, 4)
.map(landmark => `- ${landmark.name}锛?{landmark.description}`)
.join('\n');
return [
`鑷畾涔変笘鐣岋細${profile.name}`,
`鍓爣棰橈細${profile.subtitle}`,
`鐜╁鍘熷璁惧畾锛?{profile.settingText}`,
`涓栫晫姒傝堪锛?{profile.summary}`,
`涓栫晫鍩鸿皟锛?{profile.tone}`,
`鐜╁鏍稿績鐩爣锛?{profile.playerGoal}`,
`鍙壆婕?NPC 妗锛歕n${playableNpcText || '- 鏆傛棤'}`,
`鏅€?NPC 妗锛歕n${storyNpcText || '- 鏆傛棤'}`,
`鍏抽敭鐗╁搧妗锛歕n${itemText || '- 鏆傛棤'}`,
`鍏抽敭鍦版爣妗锛歕n${landmarkText || '- 鏆傛棤'}`,
].join('\n');
}

View File

@@ -0,0 +1,683 @@
import {
CustomWorldItem,
CustomWorldLandmark,
CustomWorldNpc,
CustomWorldPlayableNpc,
CustomWorldProfile,
ItemCatalogEntry,
ItemRarity,
WorldTemplateType,
WorldType,
} from '../types';
import {
buildFallbackCustomWorldProfile,
normalizeCustomWorldProfile,
} from './customWorld';
import {
buildThemedItemDescription,
buildThemedItemName,
inferCustomItemMechanics,
} from './customWorldPresentation';
import { mergeCustomWorldPlayableNpcTags } from '../data/customWorldBuildTags';
import { getDefaultCustomWorldNpcImage, getDefaultCustomWorldSceneImage } from '../data/customWorldVisuals';
import { resolveCustomWorldAnchorWorldType } from './customWorldTheme';
const MIN_PLAYABLE_NPC_COUNT = 5;
const MIN_STORY_NPC_COUNT = 30;
const MIN_ITEM_COUNT = 1000;
const MIN_LANDMARK_COUNT = 6;
const FEATURED_ITEM_COUNT = 24;
type ExpandedPlayableNpc = CustomWorldPlayableNpc & { templateCharacterId: string };
const PLAYABLE_TEMPLATE_CHARACTER_IDS = [
'sword-princess',
'archer-hero',
'girl-hero',
'punch-hero',
'fighter-4',
] as const;
const NAME_SURNAMES = [
'娌?,
'闄?,
'椤?,
'鑻?,
'娓?,
'瑁?,
'绁?,
'闂?,
'璋?,
'钀?,
'瀹?,
'姹?,
'绉?,
'鐧?,
'鍙?,
'宕?,
] as const;
const NAME_GIVEN_PREFIXES = [
'浜?,
'鏄?,
'闈?,
'鐜?,
'鎯?,
'鏅?,
'钀?,
'闇?,
'鐓?,
'娌?,
'娓?,
'闀?,
'鏄?,
'宀?,
'闇?,
'鍚?,
] as const;
const NAME_GIVEN_SUFFIXES = [
'鑸?,
'宀?,
'琛?,
'灏?,
'姝?,
'妫?,
'婢?,
'鏈?,
'閲?,
'宸?,
'鐮?,
'闇?,
'鐟?,
'婢?,
'瀵?,
'瀹?,
] as const;
const PLAYABLE_FILL_TEMPLATES: Record<
WorldTemplateType,
Array<Pick<CustomWorldPlayableNpc, 'title' | 'description' | 'backstory' | 'personality' | 'combatStyle' | 'tags'>>
> = {
[WorldType.WUXIA]: [
{
title: '椋庨洦鍓戝',
description: '鎿呴暱蹇墤绐佽繘涓庣牬鍔胯繛娈碉紝涔犳儻鍏堣瘯鎺㈠啀涓€鍑诲畾灞€銆?,
backstory: '鏃╁勾鏇夸汉鎶ら€佸瘑淇★紝鍗村湪鍗婇€斿嵎鍏ユ棫妗堬紝鍚庢潵绱㈡€ч『鐫€閭f潯琛€绾夸竴璺拷鍒颁簡濡備粖鐨勪笘鐣屼腑蹇冦€?,
personality: '鍏嬪埗銆佹晱閿愩€佸畞鍙厛鐪嬫竻灞€鍔垮啀鍔ㄦ墜',
combatStyle: '杞昏韩杩戞垬涓庡揩鍓戝帇鍒跺苟閲嶏紝鎿呴暱鎶撳彇鐮寸唤鍚庤繛缁拷鍑?,
tags: ['蹇墤', '杩芥煡', '绐佽繘'],
},
{
title: '娓稿巻鍒€淇?,
description: '鎿呴暱涓窛绂诲帇杩笌鍒囩嚎鎹綅锛屽杽浜庡湪娣锋垬閲岀ǔ浣忚妭濂忋€?,
backstory: '鏇捐窡闅忓晢闃熷拰闀栧眬璺戦亶杈瑰湴锛岀煡閬撹繖鐗囨睙婀栨瘡涓€鏉¤涓嶅緱鍏夌殑鎹㈣揣璺紝涔熺煡閬撹皝浼氬湪涔卞眬閲屽厛闇插嚭鐮寸唤銆?,
personality: '娌夌潃銆佺洿鎺ャ€佸鎵胯鐪嬪緱鏋侀噸',
combatStyle: '鍒€鍔垮帤閲嶄絾璧锋墜鏋佸揩锛屾搮闀块€氳繃姝ユ硶鍜岃妭濂忛€煎嚭瀵规墜澶辫',
tags: ['鍒€淇?, '闀栬矾', '鍘嬪埗'],
},
{
title: '鏈哄叧琛岃€?,
description: '绮句簬鎷嗚В鏈哄叧涓庡竷缃櫡闃憋紝閫傚悎鎺㈢储鍗遍櫓鍖哄煙涓庣牬瑙i仐杩广€?,
backstory: '鍑鸿韩鍖犻棬鏃佹敮锛屽師鏈彧璐熻矗淇ˉ搴熸棫鏈哄叧锛岀洿鍒版湁浜烘妸鍙よ€佺鍒堕噸鏂版嫋鍥炴睙婀栵紝浠栦篃琚揩鍐嶆鎵у尃鍏ュ眬銆?,
personality: '鑰愬績銆佽皑鎱庛€佸寮傚父缁嗚妭杩囩洰涓嶅繕',
combatStyle: '渚濋潬鏈哄叧銆佺储鍏蜂笌鐭叺鍗忓悓浣滄垬锛屾搮闀挎帶鍦轰笌鐮撮樀',
tags: ['鏈哄叧', '鐮撮樀', '鎺㈢储'],
},
{
title: '鑽簮澶栦紶寮熷瓙',
description: '鐔熸倝鍖荤悊銆佹瘨鐞嗕笌閲庡閲囬泦锛岃兘鍦ㄩ櫓澧冧腑缁存寔闃熶紞缁埅銆?,
backstory: '鍘熸湰鍙兂瀹堢潃鑽簮涓庢棫鏂瑰瓙瀹夌ǔ搴︽棩锛屽嵈鍥犱笘鐣屾殫娴佺壍鍑轰簡琚笀闂ㄥ埢鎰忓煁鎺夌殑寰€浜嬨€?,
personality: '娓╁拰銆佺粏鑷淬€侀瀛愰噷鏋佹湁闊ф€?,
combatStyle: '鍋忓悜涓窛绂荤壍鍒朵笌鐘舵€佸帇杩紝閲嶈娑堣€楀拰鑺傚鎺у埗',
tags: ['鍖荤悊', '缁埅', '鐘舵€?],
},
{
title: '榛戝競淇′娇',
description: '璧板緱蹇紝璁板緱鐗紝鎿呴暱鍦ㄥ鏂瑰娍鍔涘す缂濋噷甯﹀洖鍏抽敭鎯呮姤銆?,
backstory: '闀挎湡鏇夸笉鍚屼汉浼犱俊鎹㈣矾锛岀煡閬撳摢涓€鍙ョ湡璇濊浠€涔堟椂鍊欒锛屼篃鐭ラ亾鍝簺鍚嶅瓧涓€鏃︽彁璧峰氨浼氭浜恒€?,
personality: '鏈虹伒銆佽瑙夈€佽璇濇€讳細鐣欏崐鍒嗕綑鍦?,
combatStyle: '寮鸿皟鏈哄姩銆佹姇鎺蜂笌杩戣韩鐖嗗彂锛岄€傚悎娓稿嚮鍜屾挙绂?,
tags: ['鎯呮姤', '鏈哄姩', '娓稿嚮'],
},
],
[WorldType.XIANXIA]: [
{
title: '宸$晫鐏典娇',
description: '鐔熸倝瑁傞殭銆佹硶闃典笌鐏垫疆鍙樺姩锛岄€傚悎鎵挎媴涓栫晫寮傚彉涓荤嚎銆?,
backstory: '鏇惧彈鍛藉贰鏌ョ伒娼竟鐣岋紝鍗村湪涓€娆″け鎺т簨浠跺悗澶卞幓浜嗘棫浣嶄笌鏃у悓琚嶏紝鍙墿杩芥煡婧愬ご杩欎竴鏉¤矾銆?,
personality: '鍐烽潤銆佸厠鍒躲€佽拷鏍圭┒搴?,
combatStyle: '鐏靛垉涓庣煭鍜掔粨鍚堬紝鎿呴暱鍏堟墜灏侀攣鍜屾寔缁帇鍒?,
tags: ['宸$晫', '瑁傞殭', '灏侀攣'],
},
{
title: '鏄熻垷鍧犱慨',
description: '鎿呴暱杩滅▼鐗靛埗涓庨珮鏈哄姩浣嶇Щ锛屽楂樼┖涓庡紓鍩熼仐杩规瀬鏈夌粡楠屻€?,
backstory: '鏇剧粡鏄繙鑸慨澹紝鍚庢潵鍦ㄤ笘鐣岃竟鐣屽潬鑸熷け搴忥紝鍙兘闈犳畫鍥惧拰璁板繂閲嶆柊鎷煎嚭鍥炲幓鐨勮矾銆?,
personality: '鏁忔嵎銆佽鎯曘€佸伓灏斾細鐢ㄧ帺绗戞帺楗版墽蹇?,
combatStyle: '鍋忛噸杩滅▼鎶曞皠鍜岄珮閫熻吘鎸紝浠ユ媺鎵拰鍘嬭揩瑙侀暱',
tags: ['鏄熻垷', '鏈哄姩', '杩滅▼'],
},
{
title: '娈嬪嵎绗︿慨',
description: '鎿呴暱闃靛浘銆佺鍜掍笌灞€閮ㄦ帶鍦猴紝閫傚悎澶勭悊绉樺涓庡鏉傞伃閬囥€?,
backstory: '灏戝勾鏃跺伓寰楁畫鍗凤紝鑷琚嵎鍏ヤ竴杩炰覆涓嶈鍏紑鐨勬棫鍙蹭笌绂佷护锛岃秺鏌ヨ秺鍙戠幇杩欎笘鐣屽苟涓嶅畬鏁淬€?,
personality: '娓╁拰銆佷笓娉ㄣ€佸鐪熺浉鏈夎繎涔庢墽鎷楃殑鑰愬績',
combatStyle: '浠ョ闃点€佹嫎鏉熷拰鎸佺画鐗靛埗瑙侀暱锛岃绌跺噯澶囦笌鑺傚',
tags: ['绗︿慨', '闃靛浘', '鎺у満'],
},
{
title: '鐏垫灞卞',
description: '鐔熸倝鑽潗銆佺伒妞嶄笌绂佸湴杈圭紭鐢熸€侊紝鑳藉湪闄╁湴涓寔缁淮鎸佽ˉ缁欍€?,
backstory: '鍘熸湰鍙槸鏇垮北闂ㄧ収鐪嬬伒鐢颁笌鑽胺锛屽悗鏉ュ嵈鍙戠幇鐏垫寮傚父涓庝笘鐣岃鐥曟潵鑷悓涓€鏍硅剦缁溿€?,
personality: '娌夐潤銆佸杽瀵熴€佸績杞絾涓嶈蒋寮?,
combatStyle: '鍒╃敤鐏垫銆侀闆句笌鐭叺鍗忎綔锛屽己璋冪壍鍒朵笌缁埅',
tags: ['鐏垫', '缁埅', '鑽胺'],
},
{
title: '鐏伐鍣ㄤ慨',
description: '绮句簬鍣ㄧ墿鏀归€犮€佹硶鍣ㄦ嫾鎺ヤ笌涓村満鐖嗗彂锛屾搮闀跨牬瑙eけ鎺ф満鍏炽€?,
backstory: '鏇惧湪鍣ㄥ潑搴曞眰鍋氭渶鑴忔渶绱殑娲伙紝鐩村埌鏈変汉灏嗘棫鏃朵唬鐨勫嵄闄╁櫒姊伴噸鏂板敜閱掞紝浠栦篃琚揩绔欏埌鍙板墠銆?,
personality: '鐩寸巼銆佸浐鎵с€侀潰瀵瑰鏉傛満鍏虫椂鍙嶈€屾渶鍐烽潤',
combatStyle: '浠ユ硶鍣ㄧ垎鍙戝拰涓窛绂昏桨鍑讳负涓伙紝鍏奸【鎷嗚В鍜屾姢鎸?,
tags: ['鍣ㄤ慨', '娉曞櫒', '鐖嗗彂'],
},
],
};
const STORY_NPC_TEMPLATES: Record<
WorldTemplateType,
Array<{
role: string;
description: string;
motivation: string;
relationshipHooks: [string, string];
}>
> = {
[WorldType.WUXIA]: [
{ role: '鑼舵ゼ鎺屾煖', description: '鎺屾彙寰€鏉ユ秷鎭笌鍩庝腑椋庡悜銆?, motivation: '鎯充繚浣忚嚜宸辩殑娑堟伅缃戜笌鏉ュ線瀹㈣矾銆?, relationshipHooks: ['鑳戒氦鎹㈠競浜曟秷鎭?, '瀵瑰娍鍔涢鍚戞瀬鏁忔劅'] },
{ role: '娓″彛鑸瑰', description: '闀挎湡寰€杩旀按璺紝鐭ラ亾璋佸湪澶滈噷鍋锋浮銆?, motivation: '鎯虫煡娓呮煇娆″け韪鑳屽悗鐨勪拱鍗栫嚎銆?, relationshipHooks: ['鑳芥彁渚涢殣钘忚埅绾?, '鎰夸负鐪熺浉鏆傛椂鍐掗櫓'] },
{ role: '榛戝競鐗欎汉', description: '娓歌蛋浜庡悇鏂圭伆鑹蹭氦鏄撲箣闂达紝鎿呴暱鎾悎涓庨殣鍖裤€?, motivation: '鎯冲湪涔卞眬涓繚浣忚嚜宸辩殑绛圭爜涓庢€у懡銆?, relationshipHooks: ['鎺屾彙绋€鏈夌墿璧勫幓鍚?, '鐭ラ亾璋佸湪鏆楀湴閲屾敹浜?] },
{ role: '鏃ф涔﹀悘', description: '淇濈琚埢鎰忓帇涓嬬殑鏃ф。涓庡彛渚涖€?, motivation: '鎯虫妸琚姽鎺夌殑妗堝嵎閲嶆柊鎷煎畬鏁淬€?, relationshipHooks: ['鍙彁渚涙鍗风嚎绱?, '浼氳瘯鎺㈢帺瀹剁珛鍦?] },
{ role: '灞遍棬澶栭棬寮熷瓙', description: '瀹堝湪闂ㄧ涓庡北閬撻棿锛屾渶鍏堝療瑙変笉瀵瑰姴鐨勪汉銆?, motivation: '鎯宠瘉鏄庤嚜宸变笉鏄殢鏃跺彲琚壓鐗茬殑寮冨瓙銆?, relationshipHooks: ['浜嗚В灞遍棬鍔ㄦ€?, '鎰夸氦鎹㈠唴閮ㄨ闂?] },
{ role: '鑽摵閮庝腑', description: '甯稿勾鏁戞不姹熸箹瀹紝瑙佽繃澶涓嶈鍑虹幇鐨勪激鍙c€?, motivation: '鎯虫壘鍑鸿浼よ€呭弽澶嶆伓鍖栫殑鏍规簮銆?, relationshipHooks: ['鑳芥彁渚涚枟浼ゆ敮鎻?, '鐔熸倝杩戞湡浼ゅ娍寮傚父'] },
{ role: '宸″煄鎹曞揩', description: '鍚嶄箟涓婄淮鎸佺З搴忥紝瀹炲垯澶瑰湪澶氭柟涔嬮棿銆?, motivation: '鎯冲湪瀹橀潰涓庢睙婀栦箣闂翠繚浣忓簳绾裤€?, relationshipHooks: ['鐭ラ亾閫氱級涓庢悳鏌ュ姩鍚?, '鑳戒复鏃跺帇浣忛夯鐑?] },
{ role: '鐭垮満鎶婂ご', description: '鎵嬮噷鏈変汉鎵嬩笌杩涘嚭璁板綍锛岃杩囧お澶氱瀵嗚杞︺€?, motivation: '鎯抽樆姝㈡煇鎵瑰嵄闄╃熆鏂欑户缁娴併€?, relationshipHooks: ['鎺屾彙鐭胯剦鍏ュ彛', '鐭ラ亾璋佸湪澶滈噷杩愯揣'] },
{ role: '鏃у啗鍋忓皢', description: '閫€涓嬫潵鍚庝粛淇濈暀鍐涗腑鏁撮】鎯呮姤鐨勪範鎯€?, motivation: '鎯虫煡娓呮棫鍐涜В浣撳墠鐨勬渶鍚庝竴绾稿啗浠ゃ€?, relationshipHooks: ['鑳芥彁渚涙垬鏈缓璁?, '鐭ラ亾鏃у啗鍒嗚鍘熷洜'] },
{ role: '閾佸尃閾哄尃甯?, description: '浠庡叺鍒冪己鍙i噷灏辫兘鐪嬪嚭涓€鍦烘垬鏂楀彂鐢熶簡浠€涔堛€?, motivation: '鎯虫壘鍒版瘉鎺夊笀鎵夸箣鍒冪殑浜恒€?, relationshipHooks: ['鑳借鲸璁ゆ鍣ㄥ嚭澶?, '鎳傚緱淇ˉ涓庨噸閾?] },
],
[WorldType.XIANXIA]: [
{ role: '澶栭棬鎵т簨', description: '缁存寔瀹楅棬澶栧洿绉╁簭锛屼篃鏈€鍏堟帴瑙﹀紓甯搞€?, motivation: '鎯抽樆姝笂灞傜户缁妸鐏惧彉褰撴垚鍐呴儴鏈哄瘑銆?, relationshipHooks: ['鑳藉崗璋冨畻闂ㄦ祦绋?, '鐭ラ亾璋佽鍒绘剰鍘嬩笅'] },
{ role: '瑙傛槦淇+', description: '璐熻矗鎺ㄦ紨澶╄薄涓庣伒娼妭鐐癸紝璁板綍涓栫晫澶辫 銆?, motivation: '鎯抽伩鍏嶄笅涓€娆″ぇ瑙勬ā寮傝薄澶辨帶銆?, relationshipHooks: ['鍙В璇诲ぉ璞″彉鍖?, '鎺屾彙鏃舵満涓庤妭鐐?] },
{ role: '鐐煎櫒鍖犲笀', description: '鎿呴暱淇ˉ娉曞櫒涓庡彜鏃ц缃紝瀵瑰け鎺у櫒鍏峰緢鏁忔劅銆?, motivation: '鎯虫壘鍥炶浜虹洍璧扮殑鍏抽敭鍣ㄦ牳銆?, relationshipHooks: ['鑳借鲸璁ゆ硶鍣ㄦ潵婧?, '鑳戒慨澶嶇牬鎹熸満鍏?] },
{ role: '鐏垫鍖讳慨', description: '闀挎湡澶勭悊鐏垫疆涓庡紓鍙樺甫鏉ョ殑鍒涗激銆?, motivation: '鎯虫壘鍑鸿鐏垫鍜屼慨澹悓鏃跺け鎺х殑鐥呯伓銆?, relationshipHooks: ['鑳芥彁渚涜嵂鐞嗘敮鎻?, '鎺屾彙绂佸湴鐢熸€佸彉鍖?] },
{ role: '绉樺鍚戝', description: '甯稿勾鍦ㄧ澧冭竟缂樿鐢熸椿锛岀煡閬撳摢浜涜矾鑳芥椿鐫€鍥炴潵銆?, motivation: '鎯冲湪灞€鍔垮け鎺у墠鎹炲嚭浠嶅洶鍦ㄩ噷闈㈢殑浜恒€?, relationshipHooks: ['鐔熸倝鍗遍櫓璺嚎', '鐭ラ亾鍝被浜哄嚭涓嶆潵'] },
{ role: '鏁d慨鍟嗘梾', description: '鏉ュ線澶氬煙涔嬮棿锛屾秷鎭潅浣嗚寖鍥村箍銆?, motivation: '鎯宠秮鍔夸繚浣忚嚜宸辩殑鑸嚎涓庤揣婧愩€?, relationshipHooks: ['鎺屾彙寮傚煙浜ゆ槗鎯呮姤', '鑳藉甫鏉ョ█缂虹墿璧?] },
{ role: '瀹堥樀浜?, description: '璐熻矗缁存寔杈圭晫娉曢樀锛屾槸鏈€鏃╁惉瑙佸紓鍝嶇殑浜轰箣涓€銆?, motivation: '鎯冲湪娉曢樀瀹屽叏宕╂簝鍓嶆壘鍒颁唬鏇挎柟妗堛€?, relationshipHooks: ['浜嗚В娉曢樀寮辩偣', '鑳借В閲婅缂濇墿寮犺抗璞?] },
{ role: '鐏佃垷鑸垫墜', description: '鏉ュ線浜戞捣鍜岃闅欎箣闂达紝瀵归珮绌洪€氳矾鏋佺啛銆?, motivation: '鎯虫煡鏄庢煇娆″潬鑸熶簨鏁呯┒绔熸槸璋佸姩鎵嬨€?, relationshipHooks: ['鑳藉甫璺┛瓒婂嵄闄╃┖鍩?, '鐭ラ亾璋佹帶鍒惰埅璺?] },
{ role: '鏃у彶鎶勫綍鑰?, description: '淇濈暀浜嗚澶氳鍒犳敼杩囩殑鏃ф椂浠h杩般€?, motivation: '鎯虫妸鏂鍘嗗彶閲嶆柊鎷兼帴鎴愬畬鏁磋剦缁溿€?, relationshipHooks: ['鎺屾彙鍙ゆ棫璁板綍', '鑳藉府鍔╂牎瀵逛紶璇寸湡鍋?] },
{ role: '宸$晫鍓戜慨', description: '甯搁┗瑁傜晫鍓嶇嚎锛屽寮傜晫鐢熺伒鍜屾尝鍔ㄩ兘鏈夌粡楠屻€?, motivation: '鎯虫煡娓呭墠绾垮薄灞″け瀹堢殑鐪熸鍘熷洜銆?, relationshipHooks: ['鑳芥彁渚涘墠绾胯闂?, '鎿呴暱澶勭悊楂樺帇鍐茬獊'] },
],
};
const LANDMARK_TEMPLATES: Record<
WorldTemplateType,
Array<{
name: string;
description: string;
}>
> = {
[WorldType.WUXIA]: [
{ name: '鏂ˉ鏃ч┛', description: '澶氭柟鍔垮姏閮戒細鍦ㄦ鐭殏鍋滅暀锛屾秷鎭笌璐х墿娴佸悜鏋佷贡銆? },
{ name: '閿侀灞遍棬', description: '琛ㄩ潰娌夊瘋锛屽疄鍒欓棬绂佷笌鏆楁祦骞跺瓨锛岄€傚悎浣滀负鍔垮姏鍐茬獊鐒︾偣銆? },
{ name: '鍐烽搧鐭垮満', description: '鐭挎枡銆佸叺鍒冧笌鏃ф绾跨储閮藉湪姝や氦姹囷紝澶滈噷鏇村嵄闄┿€? },
{ name: '琛€鐥曞瘑宸?, description: '璁稿鍏抽敭浜虹墿閮芥浘浠庤繖閲岃繘鍑猴紝鍗存病浜烘効鎰忕暀涓嬪悕瀛椼€? },
{ name: '闆炬爤娓″彛', description: '鐧藉ぉ鍍忔甯告浮鍙o紝澶滈噷鍒欐祦閫氭渶瑙佷笉寰楀厜鐨勪笢瑗裤€? },
{ name: '鍙ょ鍓嶅涵', description: '浼犻椈涓庢棫瑾撴湁鍏崇殑鐭崇绔嬪湪杩欓噷锛屽父寮曟潵涓嶅悓绔嬪満鐨勪汉銆? },
],
[WorldType.XIANXIA]: [
{ name: '瑁傜晫浜戦棬', description: '楂樼┖闂ㄩ槞浼氭槧鍑虹伒娼簮澶达紝涔熸槸瑙傚療寮傝薄鐨勬渶濂戒綅缃箣涓€銆? },
{ name: '娌夋槦鑽胺', description: '鐪嬩技闈欒哀锛屽疄鍒欒寮傚父鐏垫皵鎸佺画渚佃殌锛岄€傚悎灞曞紑璋冩煡涓庢晳娌荤嚎銆? },
{ name: '鍙ら櫒瑙傛槦鍙?, description: '淇濆瓨鐫€鏃ф椂浠f帹婕旇褰曪紝鑳界壍鍑鸿鎶瑰幓鐨勬棫鍙层€? },
{ name: '鏃犵伅鐣屼簳', description: '瓒婃帴杩戜簳鍙h秺鑳藉惉瑙佹潵鑷鐣屾繁澶勭殑鍥炲搷锛屾槸楂樺嵄鏍稿績鍖恒€? },
{ name: '娴垷娈嬫腐', description: '澶变簨鐏佃垷涓庨粦甯備氦鏄撻兘鍦ㄦ鐣欎笅鐥曡抗锛岄€傚悎灞曞紑璧勬簮绾裤€? },
{ name: '鐏扮儸鍣ㄥ潑', description: '娉曞櫒纰庣墖涓庣鍒舵畫楠稿湪姝ゅ爢绉紝甯告湁鍖犱慨涓庡娍鍔涗簤澶恒€? },
],
};
const PROCEDURAL_ITEM_TEMPLATES: Record<
WorldTemplateType,
Array<{
category: string;
rarity: ItemRarity;
prefix: string;
noun: string;
description: string;
tags: string[];
}>
> = {
[WorldType.WUXIA]: [
{ category: '姝﹀櫒', rarity: 'rare', prefix: '鏂?, noun: '閿嬪垉', description: '閫傚悎姹熸箹杩戞垬涓庢棫妗堣拷鏌ョ嚎鐨勫鐢ㄥ叺鍣ㄣ€?, tags: ['weapon', '姹熸箹'] },
{ category: '鎶ょ敳', rarity: 'uncommon', prefix: '琛?, noun: '鏃呯敳', description: '閫傚悎闀跨嚎濂旇蛋涓庡闂存綔琛岀殑杞昏銆?, tags: ['armor', '鎺㈢储'] },
{ category: '娑堣€楀搧', rarity: 'common', prefix: '鍥?, noun: '椋庤嵂', description: '鐢ㄤ簬涓存椂璋冩暣鐘舵€佷笌缁х画鎺㈢储銆?, tags: ['healing', '琛ョ粰'] },
{ category: '鏉愭枡', rarity: 'common', prefix: '鍐?, noun: '閾佹枡', description: '鍙敤浜庨噸閾稿叺鍒冩垨鍏呭綋浜ゆ槗绱犳潗銆?, tags: ['material', '閿婚€?] },
{ category: '绋€鏈夊搧', rarity: 'epic', prefix: '鏃?, noun: '妗堟畫椤?, description: '涓庝笘鐣屼富绾垮叧绯荤揣瀵嗙殑绋€鏈夌嚎绱㈢墿銆?, tags: ['rare', '绾跨储'] },
],
[WorldType.XIANXIA]: [
{ category: '姝﹀櫒', rarity: 'rare', prefix: '瑁?, noun: '鐏靛垉', description: '閫傚悎鍓嶇嚎宸$晫涓庤闅欎綔鎴樼殑娉曞叺銆?, tags: ['weapon', '鐏靛垉'] },
{ category: '鎶ょ敳', rarity: 'uncommon', prefix: '浜?, noun: '绾硅。', description: '鍏奸【鏈哄姩涓庣伒鍘嬮槻鎶ょ殑鎶よ韩娉曡。銆?, tags: ['armor', '娉曡。'] },
{ category: '娑堣€楀搧', rarity: 'common', prefix: '鍥?, noun: '鐏甸湶', description: '鐢ㄤ簬鎭㈠姘旇銆佹硶鍔涙垨绋冲畾鐏垫疆渚佃殌銆?, tags: ['mana', '琛ョ粰'] },
{ category: '鏉愭枡', rarity: 'common', prefix: '娌?, noun: '鏄熺爞', description: '甯歌浜庡櫒淇笌闃典慨鐨勫熀纭€鏉愭枡銆?, tags: ['material', '鍣ㄤ慨'] },
{ category: '绋€鏈夊搧', rarity: 'epic', prefix: '鍙?, noun: '鍩熸畫鐗?, description: '璁板綍鏃ф椂浠h竟鐣屼笌绂佸埗淇℃伅鐨勫叧閿畫鐗囥€?, tags: ['rare', '鏃у彶'] },
],
};
export interface CustomWorldBuilderOptions {
itemCatalogEntries?: ItemCatalogEntry[];
}
export function buildExpandedCustomWorldProfile(
raw: unknown,
settingText: string,
options: CustomWorldBuilderOptions = {},
): CustomWorldProfile {
const normalizedProfile = normalizeCustomWorldProfile(raw, settingText);
const fallbackProfile = buildFallbackCustomWorldProfile(settingText);
const baseProfile = normalizedProfile ?? fallbackProfile;
const playableNpcs = expandPlayableNpcs(baseProfile);
const storyNpcs = expandStoryNpcs(baseProfile);
const landmarks = expandLandmarks(baseProfile);
const items: CustomWorldItem[] = [];
void options;
return {
...baseProfile,
playableNpcs,
storyNpcs,
items,
landmarks,
};
}
function expandPlayableNpcs(profile: CustomWorldProfile) {
const uniqueBase: ExpandedPlayableNpc[] = dedupeByName(profile.playableNpcs).map((npc, index) => ({
...npc,
templateCharacterId: npc.templateCharacterId ?? PLAYABLE_TEMPLATE_CHARACTER_IDS[index],
tags: normalizeTags(npc.tags),
}));
const next: ExpandedPlayableNpc[] = [...uniqueBase];
let templateIndex = next.length;
let safety = 0;
while (next.length < MIN_PLAYABLE_NPC_COUNT && safety < 40) {
const filler = buildPlayableFallbackNpc(profile, templateIndex);
if (!next.some(item => item.name === filler.name)) {
next.push(filler);
}
templateIndex += 1;
safety += 1;
}
return next
.slice(0, MIN_PLAYABLE_NPC_COUNT)
.map((npc, index) => ({
...npc,
id: createEntryId('playable-npc', npc.name, index),
templateCharacterId: PLAYABLE_TEMPLATE_CHARACTER_IDS[index],
tags: mergeCustomWorldPlayableNpcTags(profile, npc, {
templateCharacterId: PLAYABLE_TEMPLATE_CHARACTER_IDS[index],
maxCount: 5,
}),
}));
}
function expandStoryNpcs(profile: CustomWorldProfile) {
const uniqueBase = dedupeByName(profile.storyNpcs).map((npc, index) => ({
...npc,
id: createEntryId('story-npc', npc.name, index),
relationshipHooks: normalizeHooks(npc.relationshipHooks),
}));
const next = [...uniqueBase];
let templateIndex = 0;
let safety = 0;
while (next.length < MIN_STORY_NPC_COUNT && safety < 240) {
const filler = buildStoryNpcFallback(profile, next.length, templateIndex);
if (!next.some(item => item.name === filler.name)) {
next.push(filler);
}
templateIndex += 1;
safety += 1;
}
return next
.slice(0, MIN_STORY_NPC_COUNT)
.map((npc, index) => ({
...npc,
id: createEntryId('story-npc', npc.name, index),
relationshipHooks: normalizeHooks(npc.relationshipHooks),
imageSrc: npc.imageSrc || getDefaultCustomWorldNpcImage(profile.id || profile.name, index),
}));
}
function expandLandmarks(profile: CustomWorldProfile) {
const uniqueBase = dedupeByName(profile.landmarks);
const next = [...uniqueBase];
let templateIndex = 0;
let safety = 0;
while (next.length < MIN_LANDMARK_COUNT && safety < 40) {
const filler = buildLandmarkFallback(profile, next.length, templateIndex);
if (!next.some(item => item.name === filler.name)) {
next.push(filler);
}
templateIndex += 1;
safety += 1;
}
return next
.slice(0, Math.max(uniqueBase.length, MIN_LANDMARK_COUNT))
.map((landmark, index) => ({
...landmark,
id: createEntryId('landmark', landmark.name, index),
description: clampText(landmark.description, 72),
imageSrc: landmark.imageSrc || getDefaultCustomWorldSceneImage(
profile.id || profile.name,
index,
resolveCustomWorldAnchorWorldType(profile),
),
}));
}
function expandItems(profile: CustomWorldProfile, itemCatalogEntries: ItemCatalogEntry[]) {
const featuredSeeds = dedupeByName(profile.items).slice(0, FEATURED_ITEM_COUNT);
const rankedCatalogEntries = rankCatalogEntries(itemCatalogEntries, resolveCustomWorldAnchorWorldType(profile));
const usedCatalogIds = new Set<string>();
const featuredItems = featuredSeeds.map((item, index) => {
const matchedCatalog = matchCatalogEntry(item, rankedCatalogEntries, usedCatalogIds);
if (matchedCatalog) {
usedCatalogIds.add(matchedCatalog.id);
}
return buildFeaturedItem(item, matchedCatalog, index);
});
const catalogItems = rankedCatalogEntries
.filter(entry => !usedCatalogIds.has(entry.id))
.slice(0, Math.max(0, MIN_ITEM_COUNT - featuredItems.length))
.map((entry, index) => buildCatalogBackedItem(entry, profile, featuredItems.length + index));
const nextItems = dedupeByName([...featuredItems, ...catalogItems]);
let fillerIndex = 0;
while (nextItems.length < MIN_ITEM_COUNT) {
const filler = buildProceduralItem(profile, nextItems.length, fillerIndex);
if (!nextItems.some(item => item.name === filler.name)) {
nextItems.push(filler);
}
fillerIndex += 1;
}
return nextItems
.slice(0, MIN_ITEM_COUNT)
.map((item, index) => ({
...item,
id: createEntryId('item', item.name, index),
description: clampText(item.description, 72),
tags: normalizeTags(item.tags),
}));
}
function buildPlayableFallbackNpc(profile: CustomWorldProfile, index: number): ExpandedPlayableNpc {
const anchorWorldType = resolveCustomWorldAnchorWorldType(profile);
const templates = PLAYABLE_FILL_TEMPLATES[anchorWorldType];
const template = templates[index % templates.length];
const name = buildGeneratedName(profile, index);
return {
id: createEntryId('playable-npc', name, index),
name,
title: template.title,
description: template.description,
backstory: `${name}${template.backstory} 浠栬繘鍏?{profile.name}鐨勭悊鐢憋紝鏈€缁堥兘鎸囧悜鈥?{profile.playerGoal}鈥濄€俙,
personality: template.personality,
combatStyle: template.combatStyle,
tags: normalizeTags(template.tags, [anchorWorldType === WorldType.XIANXIA ? '浠欏煙' : '姹熸箹']),
templateCharacterId: PLAYABLE_TEMPLATE_CHARACTER_IDS[index % PLAYABLE_TEMPLATE_CHARACTER_IDS.length],
};
}
function buildStoryNpcFallback(profile: CustomWorldProfile, index: number, templateIndex: number): CustomWorldNpc {
const templates = STORY_NPC_TEMPLATES[resolveCustomWorldAnchorWorldType(profile)];
const template = templates[templateIndex % templates.length];
const name = buildGeneratedName(profile, index + templateIndex + 7);
return {
id: createEntryId('story-npc', name, index),
name,
role: template.role,
description: `${template.description} 涓庘€?{profile.name}鈥濆綋鍓嶅眬鍔垮叧绯诲瘑鍒囥€俙,
motivation: `${template.motivation} 鐪间笅鏈€鍏冲績鐨勶紝鏄€?{profile.playerGoal}鈥濅細鎶婂眬闈㈡帹鍚戝摢閲屻€俙,
relationshipHooks: normalizeHooks([
template.relationshipHooks[0],
template.relationshipHooks[1],
]),
};
}
function buildLandmarkFallback(profile: CustomWorldProfile, index: number, templateIndex: number): CustomWorldLandmark {
const templates = LANDMARK_TEMPLATES[resolveCustomWorldAnchorWorldType(profile)];
const template = templates[templateIndex % templates.length];
const suffix = index >= templates.length ? `绗?{index - templates.length + 2}鍖篳 : '';
const name = suffix ? `${template.name}${suffix}` : template.name;
return {
id: createEntryId('landmark', name, index),
name,
description: `${template.description} 杩欓噷涓庘€?{profile.playerGoal}鈥濇湁鐩存帴鑱旂郴銆俙,
dangerLevel: template.dangerLevel,
};
}
function buildFeaturedItem(
item: CustomWorldItem,
matchedCatalog: ItemCatalogEntry | null,
index: number,
): CustomWorldItem {
const mechanics = inferCustomItemMechanics(
item.category,
item.rarity,
normalizeTags(item.tags, matchedCatalog?.tags ?? []),
matchedCatalog?.id ?? item.id ?? `${item.name}-${index}`,
);
return {
id: createEntryId('item', item.name, index),
name: clampText(item.name, 24),
category: item.category,
rarity: item.rarity,
description: clampText(item.description, 72),
tags: normalizeTags(item.tags, matchedCatalog?.tags ?? []),
iconSrc: matchedCatalog?.iconSrc,
sourcePath: matchedCatalog?.sourcePath,
origin: 'generated',
equipmentSlotId: mechanics.equipmentSlotId ?? item.equipmentSlotId ?? null,
statProfile: mechanics.statProfile ?? item.statProfile ?? null,
useProfile: mechanics.useProfile ?? item.useProfile ?? null,
value: item.value ?? mechanics.value,
};
}
function buildCatalogBackedItem(
entry: ItemCatalogEntry,
profile: CustomWorldProfile,
index: number,
): CustomWorldItem {
const worldProfile = entry.worldProfiles?.[resolveCustomWorldAnchorWorldType(profile)];
const themedName = buildThemedItemName(
profile,
entry.category,
entry.id,
index,
);
const themedDescription = buildThemedItemDescription(
profile,
entry.category,
entry.rarity,
entry.id,
);
return {
id: createEntryId('item', themedName, index),
name: clampText(themedName, 24),
category: entry.category,
rarity: entry.rarity,
description: clampText(themedDescription || worldProfile?.description || entry.description, 72),
tags: normalizeTags(entry.tags, ['绱犳潗搴?]),
iconSrc: entry.iconSrc,
sourcePath: entry.sourcePath,
origin: 'catalog',
equipmentSlotId: entry.equipmentSlotId ?? null,
statProfile: entry.statProfile ?? null,
useProfile: entry.useProfile ?? null,
value: entry.value,
};
}
function buildProceduralItem(
profile: CustomWorldProfile,
index: number,
templateIndex: number,
): CustomWorldItem {
const anchorWorldType = resolveCustomWorldAnchorWorldType(profile);
const template = PROCEDURAL_ITEM_TEMPLATES[anchorWorldType][
templateIndex % PROCEDURAL_ITEM_TEMPLATES[anchorWorldType].length
];
const name = buildThemedItemName(profile, template.category, `${template.prefix}-${template.noun}`, index);
const mechanics = inferCustomItemMechanics(
template.category,
template.rarity,
normalizeTags(template.tags),
`${template.prefix}-${template.noun}-${index}`,
);
return {
id: createEntryId('item', name, index),
name,
category: template.category,
rarity: template.rarity,
description: buildThemedItemDescription(profile, template.category, template.rarity, `${template.prefix}-${template.noun}-${index}`),
tags: normalizeTags(template.tags, [anchorWorldType === WorldType.XIANXIA ? '浠欏煙' : '姹熸箹']),
origin: 'generated',
equipmentSlotId: mechanics.equipmentSlotId ?? null,
statProfile: mechanics.statProfile ?? null,
useProfile: mechanics.useProfile ?? null,
value: mechanics.value,
};
}
function rankCatalogEntries(entries: ItemCatalogEntry[], worldType: WorldType) {
const affinity = worldType === WorldType.XIANXIA ? 'xianxia' : 'wuxia';
const preferred = entries.filter(entry => entry.worldAffinity === affinity);
const neutral = entries.filter(entry => entry.worldAffinity === 'neutral');
const fallback = entries.filter(entry => entry.worldAffinity !== affinity && entry.worldAffinity !== 'neutral');
return dedupeById([...preferred, ...neutral, ...fallback]);
}
function matchCatalogEntry(
seedItem: CustomWorldItem,
entries: ItemCatalogEntry[],
usedCatalogIds: Set<string>,
) {
const normalizedCategory = seedItem.category.trim();
const normalizedTags = new Set(seedItem.tags.map(tag => tag.toLowerCase()));
const directMatch = entries.find(entry =>
!usedCatalogIds.has(entry.id)
&& entry.category === normalizedCategory,
);
if (directMatch) {
return directMatch;
}
return entries.find(entry =>
!usedCatalogIds.has(entry.id)
&& entry.tags.some(tag => normalizedTags.has(tag.toLowerCase())),
) ?? null;
}
function buildGeneratedName(profile: Pick<CustomWorldProfile, 'name' | 'settingText'>, index: number) {
const seed = hashText(`${profile.name}:${profile.settingText}:${index}`);
const surname = NAME_SURNAMES[seed % NAME_SURNAMES.length];
const prefix = NAME_GIVEN_PREFIXES[(seed >>> 3) % NAME_GIVEN_PREFIXES.length];
const suffix = NAME_GIVEN_SUFFIXES[(seed >>> 5) % NAME_GIVEN_SUFFIXES.length];
return `${surname}${prefix}${suffix}`;
}
function normalizeTags(tags: string[], fallbackTags: string[] = []) {
return [...new Set([...tags, ...fallbackTags].map(tag => tag.trim()).filter(Boolean))].slice(0, 5);
}
function normalizeHooks(hooks: string[]) {
const normalized = [...new Set(hooks.map(hook => hook.trim()).filter(Boolean))];
if (normalized.length >= 2) {
return normalized.slice(0, 3);
}
return [...normalized, '鎺屾彙鍙户缁繁鎸栫殑鍏抽敭绾跨储', '浼氭牴鎹帺瀹剁珛鍦烘敼鍙樺悎浣滄繁搴?].slice(0, 3);
}
function clampText(value: string, maxLength: number) {
const normalized = value.trim().replace(/\s+/g, ' ');
if (normalized.length <= maxLength) {
return normalized;
}
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trim()}鈥;
}
function dedupeByName<T extends { name: string }>(items: T[]) {
const seen = new Set<string>();
return items.filter(item => {
const key = item.name.trim();
if (!key || seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
function dedupeById<T extends { id: string }>(items: T[]) {
const seen = new Set<string>();
return items.filter(item => {
if (!item.id || seen.has(item.id)) {
return false;
}
seen.add(item.id);
return true;
});
}
function createEntryId(prefix: string, label: string, index: number) {
return `${prefix}-${slugify(label || `${prefix}-${index + 1}`)}-${index + 1}`;
}
function slugify(value: string) {
const ascii = value
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
.replace(/^-+|-+$/g, '');
if (ascii) {
return ascii.slice(0, 24);
}
return 'entry';
}
function hashText(value: string) {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
}
return hash;
}