初始仓库迁移
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');
}