v0.1.24: 寰宇初构——世界种子生成器 + 插件时轮锚点(1034 全绿)

【世界种子生成器(NPC 数量不再定死)】
- sim/worldgen.ts:generateWorld(seed) 确定性塑造天下——NPC 4~8 家随机
  (老牌世家模板 + 词库组合新贵,名字去重)、初始关系网(1-2世仇+1盟友)、
  开局 era 三态、区域风味、市场偏移 ±15%
- 独立派生 rng(seed::worldgen)——World.rng 主序列零消耗:同 seed 世界可重放、
  玩法随机序列不受生成器移动(金钟罩玩法序保护)
- state.worldGen 落档(normalize 旧档按 seed 重放=同世界,存档兼容)
- creation 初始化 npcFamilies/initSim 关系网/era/池偏置全走 worldgen——开局即恩怨
- 局内补位目标 = 开局家数(4~8),生灭闭环持续

【插件深化(0.1.24 第二组钩子)】
- PluginContext.beforePhase/afterPhase(时轮锚点:phase 前后挂勾,卸载全摘)
- PluginContext.addNpcTemplate(世界模板注入——词库插件化)
- plugin-public 文档同步;示例 Plugin 契约升级

【测试适配(世界名单随机化)】
- 静态 npc id 假设全面改动态取家(anyNpc helper 共享);
- 难度梯度用例改「同 seed 不同难度」比较;worldgen 确定性/范围/去重/关系对称 7 例

【测试】1034 全绿(45 套件);金钟罩 0.1.24 基线固化(worldgen 随机世界)+ worldAnnals 等;
build 通过
This commit is contained in:
2026-08-23 16:15:22 +08:00
parent 04ef80faa5
commit ec25bf92fc
28 changed files with 382 additions and 72 deletions
+138
View File
@@ -0,0 +1,138 @@
/**
* 世界种子生成器(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'] }
]
/** 新贵词库(风格/区域/词根——生成非老牌世家模板) */
const GEN_STYLES = ['剑修世家', '丹道世家', '商盟世族', '兵修蛮门', '符箓仙门', '灵植谷户', '阵道门阀', '散修聚落']
const GEN_REGIONS = ['北岳玄影峰', '西川药谷', '南都连港', '东丘雷泽', '南麓青泽', '西山雾谷', '东溪云汉', '北原古井']
const GEN_SUFFIX = ['氏', '氏', '宗', '寨', '门']
const 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 = GEN_REGIONS.filter((r) => !usedRegions.has(r))
const regionPick = region.length > 0 ? region[rng.int(0, region.length - 1)]! : GEN_REGIONS[rng.int(0, GEN_REGIONS.length - 1)]!
const stylePick = GEN_STYLES[rng.int(0, GEN_STYLES.length - 1)]!
usedRegions.add(regionPick)
// 去重:家名组合回溯(同 seed 确定性;至多 8 次重抽)
let fam = GEN_FAM[rng.int(0, GEN_FAM.length - 1)]!
let suffix = GEN_SUFFIX[rng.int(0, GEN_SUFFIX.length - 1)]!
let name = `${fam}${suffix}`
for (let t = 0; t < 8 && usedNames.has(name); t++) {
fam = GEN_FAM[rng.int(0, GEN_FAM.length - 1)]!
suffix = GEN_SUFFIX[rng.int(0, GEN_SUFFIX.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 }
}