Files
Genarrative/.codex-logs/restore-backups/2026-03-29/customWorldBuilder.ts.broken
高物 c49c64896a
Some checks failed
CI / verify (push) Has been cancelled
初始仓库迁移
2026-04-04 23:57:06 +08:00

684 lines
33 KiB
Plaintext

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;
}