Files
ChronicleOfTheImmortalClan/src/renderer/game/engine/sim/worldgen.ts
T
thzxx 5d9c333c76 v0.1.25: 乾坤一统——MOD 系统首版(1040 全绿,金钟罩不漂)
【P0 修复(0.1.24 遗留)】worldGen.npcs defs 落档 + normalize 幂等重注册——读档后
动态家族(n-g-*)npcById 永命中;MOD 卸载后存档世界仍完整(存档世界永稳)

【MOD 系统四层】
- M2 内容层:modSchema.ts(ModPack 全 JSON:manifest+data{events/npcs/worldgen/pills/forges};
  modParse 纯解析校验 id/name/version/事件模板合法性)
- M3 管理器:modToPlugin(MOD→CotycPlugin 一等子类——插件持久化/双闸/启停/回滚全复用)
- M1 传输层:main IPC mods:scan/read(userData/mods/*.json|*.cotymod)+ preload + env.d.ts
  + store refreshMods/installModByName/installModText
- M4 UI:SettingsPanel「MOD 层」卡(扫描目录/候选列表/安装);内置示例包通路

【世界/插件协同】
- WorldGenPools 聚合词库池(默认不变→基准世界不变;reset() 供隔离);MOD 于新档参与生成
- items.ts 配方聚合注册表(PILL/FORGE base+扩展);PluginContext addPillRecipe/addForgeRecipe/addWorldGenPool

【边界(明注)】灾因/世界数值表注入归平衡版;脚本型 MOD 不做;MOD 只影响新档世界(老档不变形)

【测试】1040 全绿(46 套件):worldGen 存档 1 例 + mod-system 5 例(解析/转换/模板/依赖/基准指纹);
金钟罩 0.1.24 基线未漂(世界生成默认池不变);build 通过
2026-08-23 16:29:29 +08:00

159 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 世界种子生成器(0.1.24《寰宇初构》)
* 游戏种子 → 确定性世界布局:NPC 势力(4~8 家)/初始关系网/开局时代/区域风味/市场偏移。
* 关键纪律:使用独立派生 rngseed + '::worldgen')——World.rng 主序列零消耗,
* 玩家玩法随机序列不被生成器移动(金钟罩玩法序保护);同 seed 世界可重放。
*/
import { Rng, seedToRng } from '../kernel/rng'
import { NpcFamilyDef } from '../../data/npcs'
import { registerNpcDef } from '../../data/npcs'
/** 老牌世家模板(创世第一轮种子池核心;worldgen 决定/谁出现) */
export const LEGACY_TEMPLATES: NpcFamilyDef[] = [
{ id: 'n-xuanying', name: '玄影沈氏', region: '北岳玄影峰', style: '剑修世家', desc: '隐于北岳的剑修沈氏,剑意凛冽,最为孤傲。', leaderRealm: 'foundation', initialPower: 240, powerGrowth: [4, 10], sells: ['weapon-qi', 'weapon-ling'], buys: ['lingcao', 'lingkuang'] },
{ id: 'n-danxin', name: '丹心木氏', region: '西川药谷', style: '丹道世家', desc: '悬壶济世的丹道世家,人脉广博,风格和缓。', leaderRealm: 'foundation', initialPower: 200, powerGrowth: [3, 9], sells: ['pill-qiyuan', 'pill-ningyuan', 'pill-pojing'], buys: ['lingcao', 'beastcore'] },
{ id: 'n-sihai', name: '四海王氏', region: '南都连港', style: '商盟世族', desc: '商通四海,富可敌国,只认灵石不认人。', leaderRealm: 'foundation', initialPower: 160, powerGrowth: [5, 12], sells: ['lingcao', 'lingkuang'], buys: ['beastcore', 'lingkuang'] },
{ id: 'n-nulei', name: '怒雷祝氏', region: '东丘雷泽', style: '兵修蛮门', desc: '雷泽蛮族的世仇,性情火爆,最易生衅。', leaderRealm: 'core', initialPower: 300, powerGrowth: [5, 13], sells: [], buys: ['lingkuang', 'beastcore'] }
]
/** 新贵词库(默认池——MOD/插件可注入扩展;默认不变 → 基准世界不变) */
const DEFAULT_GEN_STYLES = ['剑修世家', '丹道世家', '商盟世族', '兵修蛮门', '符箓仙门', '灵植谷户', '阵道门阀', '散修聚落']
const DEFAULT_GEN_REGIONS = ['北岳玄影峰', '西川药谷', '南都连港', '东丘雷泽', '南麓青泽', '西山雾谷', '东溪云汉', '北原古井']
const DEFAULT_GEN_SUFFIX = ['氏', '氏', '宗', '寨', '门']
const DEFAULT_GEN_FAM = ['玄', '墨', '楚', '白', '萧', '洛', '燕', '秦', '顾', '周', '华', '苏']
/** 聚合词库池(MOD/插件注入端;读取端 worldgen 消费) */
export const WorldGenPools = {
styles: [...DEFAULT_GEN_STYLES],
regions: [...DEFAULT_GEN_REGIONS],
suffixes: [...DEFAULT_GEN_SUFFIX],
fams: [...DEFAULT_GEN_FAM],
add(part: { styles?: string[]; regions?: string[]; suffixes?: string[]; fams?: string[] }): void {
if (part.styles) for (const s of part.styles) if (!this.styles.includes(s)) this.styles.push(s)
if (part.regions) for (const r of part.regions) if (!this.regions.includes(r)) this.regions.push(r)
if (part.suffixes) for (const s of part.suffixes) if (!this.suffixes.includes(s)) this.suffixes.push(s)
if (part.fams) for (const f of part.fams) if (!this.fams.includes(f)) this.fams.push(f)
},
reset(): void {
this.styles = [...DEFAULT_GEN_STYLES]
this.regions = [...DEFAULT_GEN_REGIONS]
this.suffixes = [...DEFAULT_GEN_SUFFIX]
this.fams = [...DEFAULT_GEN_FAM]
}
}
export interface WorldGenResult {
seed: string
npcs: NpcFamilyDef[]
relations: Record<string, Record<string, number>>
eras: 'shengshi' | 'pingshi' | 'luanshi'
regionFlavor: Record<string, number>
marketOffset: Record<string, number>
alliances: Array<[string, string]>
feuds: Array<[string, string]>
}
/** 世界生成(确定性;独立 rng) */
export function generateWorld(seed: string): WorldGenResult {
const rng = new Rng(seedToRng(`${seed}::worldgen`))
// 家数 4~8(乱世开局偏少);老牌世家参与率 60~100%
const count = 4 + rng.int(0, 4)
const legacyPick = 1 + rng.int(0, 4) // 1~4 家老牌
const legacy: NpcFamilyDef[] = []
const pool = [...LEGACY_TEMPLATES]
while (legacy.length < legacyPick && pool.length > 0) {
const i = rng.int(0, pool.length - 1)
legacy.push(pool[i]!)
pool.splice(i, 1)
}
const usedRegions = new Set(legacy.map((l) => l.region))
const usedNames = new Set(legacy.map((l) => l.name))
const npcs: NpcFamilyDef[] = [...legacy]
const frontier = count - legacy.length
for (let k = 0; k < frontier; k++) {
const region = WorldGenPools.regions.filter((r) => !usedRegions.has(r))
const regionPick = region.length > 0 ? region[rng.int(0, region.length - 1)]! : WorldGenPools.regions[rng.int(0, WorldGenPools.regions.length - 1)]!
const stylePick = WorldGenPools.styles[rng.int(0, WorldGenPools.styles.length - 1)]!
usedRegions.add(regionPick)
// 去重:家名组合回溯(同 seed 确定性;至多 8 次重抽)
let fam = WorldGenPools.fams[rng.int(0, WorldGenPools.fams.length - 1)]!
let suffix = WorldGenPools.suffixes[rng.int(0, WorldGenPools.suffixes.length - 1)]!
let name = `${fam}${suffix}`
for (let t = 0; t < 8 && usedNames.has(name); t++) {
fam = WorldGenPools.fams[rng.int(0, WorldGenPools.fams.length - 1)]!
suffix = WorldGenPools.suffixes[rng.int(0, WorldGenPools.suffixes.length - 1)]!
name = `${fam}${suffix}`
}
usedNames.add(name)
const power = 90 + rng.int(0, 130)
npcs.push({
id: `n-g${k + 1}-${fam}`,
name: `${fam}${suffix}`,
region: regionPick,
style: stylePick,
desc: `${stylePick}新立足${regionPick},渐成气候。`,
leaderRealm: power > 180 ? 'core' : 'foundation',
initialPower: power,
powerGrowth: [3, 10],
sells: rng.chance(0.5) ? ['lingcao'] : [],
buys: ['lingcao', 'lingkuang']
})
}
// 注册动态 defnpcById 双源命中)
for (const n of npcs) registerNpcDef(n)
// 初始关系网:随机世仇 1-2 对 + 盟友 1 对 + 其余 ±20 漂移
const relations: Record<string, Record<string, number>> = {}
for (const a of npcs) relations[a.id] = {}
const pairAll = (a: string, b: string, v: number) => {
relations[a]![b] = v
relations[b]![a] = v
}
const shuffle = [...npcs]
for (let i = shuffle.length - 1; i > 0; i--) {
const j = rng.int(0, i)
;[shuffle[i], shuffle[j]] = [shuffle[j]!, shuffle[i]!]
}
const feuds: Array<[string, string]> = []
const alliances: Array<[string, string]> = []
if (shuffle.length >= 2) {
pairAll(shuffle[0]!.id, shuffle[1]!.id, -60 - rng.int(0, 20))
feuds.push([shuffle[0]!.id, shuffle[1]!.id])
if (shuffle.length >= 3) {
pairAll(shuffle[2]!.id, shuffle[3]!.id, 45 + rng.int(0, 20))
alliances.push([shuffle[2]!.id, shuffle[3]!.id])
} else {
pairAll(shuffle[0]!.id, shuffle[1]!.id, -60 - rng.int(0, 20))
}
}
for (let i = 0; i < npcs.length; i++) {
for (let j = i + 1; j < npcs.length; j++) {
const a = npcs[i]!.id
const b = npcs[j]!.id
if (relations[a]?.[b] === undefined) {
pairAll(a, b, rng.int(-25, 25))
}
}
}
// 开局 era:盛世/平世/乱世(乱世世界凶兆与繁华初现的分野)
const eras = ['shengshi', 'pingshi', 'luanshi'] as const
const eraPick = eras[rng.int(0, eras.length < 2 ? 0 : 2)]!
// 区域风味(每 region 商品偏移 ±15%对称)
const regionFlavor: Record<string, number> = {}
for (const n of npcs) {
regionFlavor[n.id] = (rng.next() - 0.5) * 0.3
}
const marketOffset: Record<string, number> = {
lingcao: (rng.next() - 0.5) * 0.24,
lingkuang: (rng.next() - 0.5) * 0.24,
beastcore: (rng.next() - 0.5) * 0.24,
'pill-qiyuan': (rng.next() - 0.5) * 0.18,
'pill-ningyuan': (rng.next() - 0.5) * 0.18
}
return { seed, npcs, relations, eras: eraPick, regionFlavor, marketOffset, alliances, feuds }
}