471 lines
27 KiB
Plaintext
471 lines
27 KiB
Plaintext
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 妗f锛歕n${playableNpcText || '- 鏆傛棤'}`,
|
||
`鏅€?NPC 妗f锛歕n${storyNpcText || '- 鏆傛棤'}`,
|
||
`鍏抽敭鐗╁搧妗f锛歕n${itemText || '- 鏆傛棤'}`,
|
||
`鍏抽敭鍦版爣妗f锛歕n${landmarkText || '- 鏆傛棤'}`,
|
||
].join('\n');
|
||
}
|