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:
@@ -39,6 +39,8 @@ export const PHASE_ORDER: PhaseId[] = [
|
||||
export class GameClock {
|
||||
private monthly = new Map<PhaseId, SystemHook[]>()
|
||||
private yearly: SystemHook[] = []
|
||||
private anchorsBefore: Array<{ phase: PhaseId; fn: SystemHook }> = []
|
||||
private anchorsAfter: Array<{ phase: PhaseId; fn: SystemHook }> = []
|
||||
|
||||
register(phase: PhaseId, fn: SystemHook): () => void {
|
||||
const list = this.monthly.get(phase) ?? []
|
||||
@@ -58,6 +60,22 @@ export class GameClock {
|
||||
}
|
||||
}
|
||||
|
||||
/** 时轮锚点(0.1.24):phase 前挂勾(插件用;卸载时全摘) */
|
||||
beforePhase(phase: PhaseId, fn: SystemHook): () => void {
|
||||
this.anchorsBefore.push({ phase, fn })
|
||||
return () => {
|
||||
this.anchorsBefore = this.anchorsBefore.filter((a) => !(a.phase === phase && a.fn === fn))
|
||||
}
|
||||
}
|
||||
|
||||
/** 时轮锚点:phase 后挂勾 */
|
||||
afterPhase(phase: PhaseId, fn: SystemHook): () => void {
|
||||
this.anchorsAfter.push({ phase, fn })
|
||||
return () => {
|
||||
this.anchorsAfter = this.anchorsAfter.filter((a) => !(a.phase === phase && a.fn === fn))
|
||||
}
|
||||
}
|
||||
|
||||
fireYearStart(w: World): void {
|
||||
for (const fn of this.yearly) fn(w)
|
||||
}
|
||||
@@ -66,8 +84,10 @@ export class GameClock {
|
||||
const report: PhaseStat[] = []
|
||||
for (const phase of PHASE_ORDER) {
|
||||
const t0 = performance.now()
|
||||
for (const a of this.anchorsBefore) if (a.phase === phase) a.fn(w)
|
||||
const fns = [...(this.monthly.get(phase) ?? [])]
|
||||
for (const fn of fns) fn(w)
|
||||
for (const a of this.anchorsAfter) if (a.phase === phase) a.fn(w)
|
||||
const ms = performance.now() - t0
|
||||
report.push({ phase, ms, count: fns.length })
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@ export interface PluginContext {
|
||||
clock: GameClock
|
||||
register: (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void
|
||||
onYearStart: (fn: SystemHook) => () => void
|
||||
/** 时轮 anchor:phase 前/后挂勾(0.1.24;卸载时全摘) */
|
||||
beforePhase: (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void
|
||||
afterPhase: (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void
|
||||
/** 世界生成钩子:插件可注入 NPC 模板(worldgen 词库插件化) */
|
||||
addNpcTemplate: (def: import('../../data/npcs').NpcFamilyDef) => void
|
||||
addCapability: (cap: { id: string; name: string; version: string; desc: string }) => void
|
||||
removeCapability: (id: string) => void
|
||||
enableCapability: (id: string, enabled: boolean) => void
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
*
|
||||
* 能力范围:
|
||||
* - register(phase, fn):在时轮相位注册月度钩子(clocks 红线:phase 固定,勿自创)
|
||||
* - beforePhase/afterPhase(phase, fn):phase 前后锚点(0.1.24;卸载全摘)
|
||||
* - onYearStart(fn):年首钩子
|
||||
* - addNpcTemplate(def):注入世界生成模板(词库插件化)
|
||||
* - addEventPool/removeEventPool:注入/移除事件池(池对 world.eventPools 全量聚合)
|
||||
* - addCapability/removeCapability/enableCapability:能力卡(系统开关;受旺启停联动)
|
||||
* - overridePack/resetPack:数据包覆写/回滚(pack() 单源)
|
||||
|
||||
@@ -137,7 +137,7 @@ export class GameFacade {
|
||||
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
|
||||
return {
|
||||
title: '仙途家族志',
|
||||
version: '0.1.23',
|
||||
version: '0.1.24',
|
||||
modules: this.world.systemList().length,
|
||||
systems: this.world.systemList().filter((s) => s.enabled).length,
|
||||
plugins: this.world.pluginList().length,
|
||||
|
||||
@@ -15,6 +15,8 @@ import { POSTS } from '../../data/posts'
|
||||
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../narrative/legacy'
|
||||
import { needsTribulation, tribulationEventId } from './Systems/tribulation'
|
||||
import { generateWorld } from '../sim/worldgen'
|
||||
import { registerNpcDef } from '../../data/npcs'
|
||||
import { fire } from './Systems/events'
|
||||
import { createWorldState, findInheritor } from './creation'
|
||||
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||
@@ -61,6 +63,19 @@ export function worldSimOf(w: World): WorldSim {
|
||||
}
|
||||
|
||||
export function normalizeGameState(state: GameState): GameState {
|
||||
// 0.1.24 世界种子:旧档缺 worldGen → 按 seed 重放(同一世界)并注册动态 def
|
||||
if (!state.worldGen && state.seed) {
|
||||
const wg = generateWorld(state.seed)
|
||||
state.worldGen = {
|
||||
relations: wg.relations,
|
||||
eras: wg.eras,
|
||||
marketOffset: wg.marketOffset,
|
||||
regionFlavor: wg.regionFlavor,
|
||||
alliances: wg.alliances,
|
||||
feuds: wg.feuds,
|
||||
npcCount: wg.npcs.length
|
||||
}
|
||||
}
|
||||
// 老版本存档(<0.1.1)缺少新增字段,加载时补齐,避免运行期 undefined 崩溃
|
||||
if (!state.finance) state.finance = { accum: 0 }
|
||||
if (!state.yearStats) state.yearStats = { births: 0, deaths: 0 }
|
||||
@@ -172,6 +187,14 @@ export class World {
|
||||
clock: self.clock,
|
||||
register: (phase, fn: SystemHook) => self.clock.register(phase, fn),
|
||||
onYearStart: (fn: SystemHook) => self.clock.onYearStart(fn),
|
||||
beforePhase: (phase, fn) => self.clock.beforePhase(phase, fn),
|
||||
afterPhase: (phase, fn) => self.clock.afterPhase(phase, fn),
|
||||
addNpcTemplate: (def) => {
|
||||
registerNpcDef(def)
|
||||
if (self.state.worldGen?.relations && !self.state.worldGen.relations[def.id]) {
|
||||
self.state.worldGen.relations[def.id] = {}
|
||||
}
|
||||
},
|
||||
addCapability: (cap) => {
|
||||
// 0.1.23:能力卡注册入 World 实例(防跨档全局泄漏);UI 全局清单读 SYSTEM_DEFS 展示不受影响
|
||||
if (!self.systems[cap.id]) self.systems[cap.id] = { enabled: true }
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Character, GameState, NpcFamilyState, RealmMajor } from '../../types/do
|
||||
import { Rng, seedToRng } from '../kernel/rng'
|
||||
import { randomSurname, MALE_GIVEN, FEMALE_GIVEN } from '../kernel/names'
|
||||
import { newCharacter } from './pcgen'
|
||||
import { NPCS } from '../../data/npcs'
|
||||
import { generateWorld } from '../sim/worldgen'
|
||||
import { World } from './World'
|
||||
|
||||
export interface NewGameOptions {
|
||||
@@ -21,9 +21,19 @@ export function createWorldState(opts: NewGameOptions): GameState {
|
||||
const stones = diff === 'easy' ? 1200 : diff === 'normal' ? 800 : 550
|
||||
const npcStrength = diff === 'easy' ? 0.9 : diff === 'normal' ? 1 : 1.15
|
||||
|
||||
const worldGen = generateWorld(opts.seed)
|
||||
const state: GameState = {
|
||||
schemaVersion: 1,
|
||||
seed: opts.seed,
|
||||
worldGen: {
|
||||
relations: worldGen.relations,
|
||||
eras: worldGen.eras,
|
||||
marketOffset: worldGen.marketOffset,
|
||||
regionFlavor: worldGen.regionFlavor,
|
||||
alliances: worldGen.alliances,
|
||||
feuds: worldGen.feuds,
|
||||
npcCount: worldGen.npcs.length
|
||||
},
|
||||
rng: rng.getState(),
|
||||
year: 1,
|
||||
month: 1,
|
||||
@@ -54,7 +64,7 @@ export function createWorldState(opts: NewGameOptions): GameState {
|
||||
},
|
||||
members: {},
|
||||
npcFamilies: Object.fromEntries(
|
||||
NPCS.map((n) => [
|
||||
worldGen.npcs.map((n) => [
|
||||
n.id,
|
||||
{
|
||||
id: n.id,
|
||||
|
||||
@@ -281,9 +281,12 @@ export class WorldSim {
|
||||
|
||||
function initSim(w: World): WorldSimState {
|
||||
const s = { ...empty() }
|
||||
const wg = w.state.worldGen
|
||||
// 开局关系网(worldgen 产物)——首月即全球有恩怨,不再等年首扩散
|
||||
const relations = wg?.relations ?? {}
|
||||
for (const id of Object.keys(w.state.npcFamilies)) {
|
||||
s.npcDyn[id] = {
|
||||
prosperity: 50,
|
||||
prosperity: 40 + ((Math.abs(id.charCodeAt(0) * 7) % 2) === 0 ? 25 : 0),
|
||||
stance: 'guardian',
|
||||
stanceSinceYear: 1,
|
||||
leaderName: '新任宗主',
|
||||
@@ -291,7 +294,15 @@ function initSim(w: World): WorldSimState {
|
||||
leaderAge: 40 + w.rng.int(0, 29),
|
||||
lastEvent: '',
|
||||
lastEventYear: -99,
|
||||
relationsWithOthers: {}
|
||||
relationsWithOthers: { ...(relations[id] ?? {}) }
|
||||
}
|
||||
}
|
||||
// 开局 era + 市场偏置(worldgen 产物:同 seed 同天下)
|
||||
if (wg?.eras) s.era = wg.eras
|
||||
if (wg?.marketOffset) {
|
||||
for (const [k, off] of Object.entries(wg.marketOffset)) {
|
||||
const base = poolBase(k)
|
||||
if (s.marketPool[k]) s.marketPool[k] = Math.max(base * 0.5, s.marketPool[k]! * (1 + off))
|
||||
}
|
||||
}
|
||||
s.secretQi = {}
|
||||
@@ -469,9 +480,9 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
} else {
|
||||
dyn.declineYears = 0
|
||||
}
|
||||
// 新贵补位:我家数 < 4 且几率(乱世更频)——世界会新生
|
||||
// 新贵补位:家数 < 开局目标(4~8 的种子格)且几率(乱世更频)——世界会新生
|
||||
const aliveCount = Object.keys(w.state.npcFamilies).length
|
||||
const cap = 4
|
||||
const cap = Math.min(8, w.state.worldGen?.npcCount ?? 4)
|
||||
if (aliveCount < cap && w.rng.chance(WORLDSIM.greatNewbornChance * (s.era === 'luanshi' ? 2 : 1))) {
|
||||
spawnNewbornDynasty(w, s)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 世界种子生成器(0.1.24《寰宇初构》)
|
||||
* 游戏种子 → 确定性世界布局:NPC 势力(4~8 家)/初始关系网/开局时代/区域风味/市场偏移。
|
||||
* 关键纪律:使用独立派生 rng(seed + '::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']
|
||||
})
|
||||
}
|
||||
// 注册动态 def(npcById 双源命中)
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user