feat(0.1.14-P3): 世界自进化 WorldSim(五大闭环)+ 引擎时钟合一

- WorldSim(engine/sim/WorldSim.ts + worldsim-data.ts):
  A 资源循环市场(库存池/再平衡/价格弹性——Market 行情联动 sim.mult)
  B NPC 聚合演化(宗主换代/境界成长/势力=境界+财力+兵员;换代计数)
  C 秘境灵气(探索消耗/恢复)+ 灵气潮汐(6年周期×修炼倍率)
  D 灾年签(旱涝蝗疫兽寒,联动市场池)
  E 天下快讯(节流 24 月滚动 120 条,世界动态 feed)
- 相位:新增 worldsim(diplomacy 后 epilogue 前);capabilities 加「天下演序」卡可停用
- 时钟合一:World.clock = Kernel.clock(单一时钟驱动,消除双时钟漂移)
- 发现并修复:worldsim 注册被文件搬迁覆盖丢失(修改纪律验证中招——已补回);
  WorldSim 内 Math.random 根除(改 w.rng)
- fingerprint 纳入 worldSim 概要(市场/灵脉/潮汐/换代/快讯数)
- 金钟罩三档重固化(0.1.14 正式基线,受控变更)
- 验证:35 套件/967 测试全绿;typecheck 0 error
This commit is contained in:
2026-08-23 13:31:34 +08:00
parent 8dfdaf843a
commit 5dd5d6b558
16 changed files with 370 additions and 45 deletions
+13 -1
View File
@@ -2,6 +2,8 @@ import type { World } from '../runtime/World'
import { pack } from '../../data/registry'
import { traitBonuses } from '../runtime/pcgen'
import { WorldSim } from './WorldSim'
export function marketPrice(w: World, itemId: string): number {
const item = pack().items[itemId]
if (!item) return 0
@@ -9,10 +11,18 @@ export function marketPrice(w: World, itemId: string): number {
const fam = w.state.family
const mult = typeof fam.flag['priceMult'] === 'number' ? (fam.flag['priceMult'] as number) : 1
const mood = fam.reputation >= 40 ? 1.06 : fam.reputation >= 20 ? 1.02 : 0.98
// 世界行情乘子(缺省 1
let simMult = 1
if (w.state.worldSim?.marketPool) {
const pool = w.state.worldSim.marketPool[itemId] as number | undefined
const basePool = POOL_BASE[itemId] ?? 100
const r = (pool ?? basePool) / basePool
simMult = Math.max(0.55, Math.min(2.3, r))
}
// 利己(priceMult)与信誉修正
const sellers = w.aliveMembers().filter((c) => traitBonuses(c).priceMult > 0).length
const liarPct = Math.min(0.2, sellers * 0.04)
return Math.max(1, Math.round(base * mult * mood * (1 - liarPct)))
return Math.max(1, Math.round(base * mult * mood * simMult * (1 - liarPct)))
}
export function buyItem(w: World, itemId: string, count: number): boolean {
@@ -50,6 +60,8 @@ export function techniquePrice(techId: string): number {
import { TECHNIQUES } from '../../data/techniques'
const POOL_BASE: Record<string, number> = { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 }
const TECHNIQUE_GRADE_PRICE: Record<number, number> = { 1: 120, 2: 300, 3: 700, 4: 1600 }
const TECH_GRADE_BASE: Record<string, number> = Object.fromEntries(
TECHNIQUES.map((t) => [t.id, TECHNIQUE_GRADE_PRICE[t.grade] ?? 300])
+211
View File
@@ -0,0 +1,211 @@
/** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts */
import { World } from '../runtime/World'
import { WorldSimState, NpcDynamics, WORLDSIM, CALAMITY_EFFECT, CalamityName } from './worldsim-data'
import { ITEMS } from '../../data/items'
import { npcById } from '../../data/npcs'
import { MAJORS, MAJOR_ORDER } from '../../data/realms'
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'pill-qiyuan', 'pill-ningyuan']
export class WorldSim {
constructor(private w: World) {}
private s(): WorldSimState {
const w = this.w
if (!w.state.worldSim) w.state.worldSim = initSim(w) as never
return w.state.worldSim as WorldSimState
}
tick(): void {
const s = this.s()
const rng = this.w.rng
// ---- C. 灵气潮汐(大周期) ----
s.tideTicks++
const phase = (s.tideTicks % WORLDSIM.tideCycle) / WORLDSIM.tideCycle
const sine = 0.5 + 0.5 * Math.sin(phase * Math.PI * 2)
s.tide = WORLDSIM.tideMin + sine * (WORLDSIM.tideMax - WORLDSIM.tideMin)
// 秘境灵气(自动恢复 + 探索消耗由 missions 在探索时扣)
for (const key of Object.keys(s.secretQi)) {
s.secretQi[key] = clamp(s.secretQi[key] + (s.tide > 0.85 ? 5 : 2) - (s.secretQi[key] > 70 ? 2 : 0), 0, 100)
}
// ---- D. 灾变年签(每年首月一掷) ----
if (this.w.state.month === 1 && rng.chance(WORLDSIM.calamityChance)) {
const cl = rng.pick([...WORLDSIM.calamities])
s.calamity = cl
s.calamityYear = this.w.state.year
applyCalamityToMarket(s, cl as CalamityName)
this.w.log('bad', `【天下灾年】${cl}——灵植减产,市价将行。`)
}
// ---- A. 资源循环市场(库存自然流向 + 再平衡 + 价格信号) ----
driftMarket(s, rng.next())
// ---- B. NPC 演化(聚合模拟) ----
evolveNpc(this.w, s, rng.next())
// ---- E. 天下快讯(节流) ----
if (this.w.state.month !== s.lastNewsMonth && this.w.state.totalTicks % WORLDSIM.newsEvery === 0) {
pushNews(this.w, s, MARKET_IDS)
}
// ---- C2. 快讯裁剪 ----
if (s.newsFeed.length > WORLDSIM.newsKeep) s.newsFeed.splice(0, s.newsFeed.length - WORLDSIM.newsKeep)
}
/** 秘境探索消耗灵气(missions 调用) */
consumeSecretQi(id: string, amount: number): void {
const s = this.s()
if (s.secretQi[id] === undefined) s.secretQi[id] = 50
s.secretQi[id] = clamp(s.secretQi[id] - amount, 0, 100)
}
/** 灵气系数(修炼/掉落市场乘子) */
tideMult(): number {
return this.s().tide
}
/** 当前市场行情(价格乘子,反馈到 Market) */
marketMultFor(id: string): number {
const s = this.s()
const pool = s.marketPool[id] ?? 50
const base = poolBase(id)
return clamp(pool / base, WORLDSIM.priceFloor, WORLDSIM.priceCeil)
}
news(): WorldSimState['newsFeed'] {
return this.s().newsFeed
}
secretLis(): Record<string, number> {
return this.s().secretQi
}
npcDyn(): Record<string, NpcDynamics> {
return this.s().npcDyn
}
}
function initSim(w: World): WorldSimState {
const s = { ...empty() }
for (const id of Object.keys(w.state.npcFamilies)) {
s.npcDyn[id] = {
leaderName: '新任宗主',
leaderRealmIdx: MAJOR_ORDER.indexOf(npcById(id).leaderRealm),
leaderAge: 40 + w.rng.int(0, 29),
lastEvent: '',
lastEventYear: -99,
relationsWithOthers: {}
}
}
for (const sid of Object.keys(w.state.missions ?? {})) void sid
// 秘境灵气初始(从 mission 定义 id
s.secretQi = {}
return s
}
function empty(): WorldSimState {
return {
marketPool: { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 },
npcDyn: {},
secretQi: {},
tide: 0.5,
tideDir: 1,
tideTicks: 0,
calamityYear: -99,
newsFeed: [],
lastNewsMonth: -99,
npcSuccessions: 0
}
}
function driftMarket(s: WorldSimState, noise: number): void {
for (const id of MARKET_IDS) {
const base = poolBase(id)
const cur = s.marketPool[id] ?? base
// 向基准再平衡 + 噪声漂移(价格弹性)
const rebalance = (base - cur) * WORLDSIM.marketRebalance
const drift = (noise - 0.5) * WORLDSIM.marketDriftRate * base
s.marketPool[id] = Math.max(base * 0.3, cur + rebalance + drift)
}
}
function applyCalamityToMarket(s: WorldSimState, cl: CalamityName): void {
const eff = CALAMITY_EFFECT[cl]
for (const [id, pct] of Object.entries(eff) as [string, number][]) {
s.marketPool[id] = Math.max(10, (s.marketPool[id] ?? 50) * (1 + pct))
}
}
function poolBase(id: string): number {
return MARKET_BASE[id] ?? 100
}
const MARKET_BASE: Record<string, number> = { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 }
function evolveNpc(w: World, s: WorldSimState, noise: number): void {
void noise
const year = w.state.year
for (const [id, npc] of Object.entries(w.state.npcFamilies)) {
const dyn = s.npcDyn[id] ?? initDynFor(id)
s.npcDyn[id] = dyn
dyn.leaderAge++
// 宗主换代:寿终(年龄>预期)
const def = npcById(id)
const lifespan = MAJORS[def.leaderRealm].lifespan
if (dyn.leaderAge > lifespan * 0.9 || w.rng.chance(0.006)) {
if (!w.rng.chance(0.25)) {
dyn.leaderAge = 30 + w.rng.int(0, 25)
dyn.leaderRealmIdx = Math.min(dyn.leaderRealmIdx + 1, 5)
dyn.leaderName = `${def.name.replace('氏', '')}氏新主`
dyn.lastEvent = '宗祧更替'
dyn.lastEventYear = year
s.npcSuccessions++
pushNews(w, s, [id])
w.log('info', `【天下】${def.name} 更易宗主,气象一新。`)
}
}
// 势力聚合:境界+财力+兵员(前有 power 为基础)
npc.power = Math.max(40, Math.round(npc.power * 0.95 + (dyn.leaderRealmIdx * 20 + 40) * 0.5))
if (Math.abs(npc.relation) > 2) {
npc.relation += w.rng.chance(0.5) ? 0 : (npc.relation > 0 ? -0.3 : 0.3)
}
}
}
function initDynFor(id: string): NpcDynamics {
const def = npcById(id)
return {
leaderName: `${def.name.replace('氏', '')}氏宗主`,
leaderRealmIdx: MAJOR_ORDER.indexOf(def.leaderRealm),
leaderAge: 45,
lastEvent: '',
lastEventYear: -99,
relationsWithOthers: {}
}
}
function pushNews(w: World, s: WorldSimState, about: string[]): void {
const rng = w.rng
const idx = rng.int(0, about.length - 1)
const id = about[idx]
const pool = s.marketPool[id] ?? 50
const base = poolBase(id)
const pct = Math.round(((pool - base) / base) * 100)
const itemName = ITEMS[id]?.name ?? id
const tide = s.tide > 1.0 ? '灵潮上涨' : s.tide < 0.75 ? '灵潮回落' : '汐平'
const row = {
year: w.state.year,
month: w.state.month,
src: id.startsWith('n-') ? npcById(id).name : `世界·${itemName}`,
text: id.startsWith('n-')
? `${npcById(id).name} 世务维系(宗主更替率);${tide}${pct !== 0 ? `${itemName}行情${pct > 0 ? '升' : '降'}${Math.abs(pct)}%` : ''}`
: `${tide}·${itemName}行情${pct > 0 ? '升' : '降'}${Math.abs(pct)}%`
}
s.newsFeed.push(row)
s.lastNewsMonth = w.state.month
}
function clamp(v: number, lo: number, hi: number): number {
return Math.max(lo, Math.min(hi, v))
}
@@ -0,0 +1,81 @@
export interface NpcDynamics {
/** 宗主姓名快照(换代时更新) */
leaderName: string
leaderRealmIdx: number
leaderAge: number
/** 演化旗标 */
lastEvent: string
lastEventYear: number
relationsWithOthers: Record<string, number>
}
export interface SecretQi {
qi: number
peak: number
}
export interface WorldSimState {
/** 世界库存池(资源循环) */
marketPool: Record<string, number>
/** NPC 动力学 */
npcDyn: Record<string, NpcDynamics>
/** 秘境灵气条 */
secretQi: Record<string, number>
/** 灵气潮汐(0-100 全局系数) */
tide: number
tideDir: 1 | -1
tideTicks: number
/** 灾年(当年灾因 id */
calamity?: string
calamityYear: number
/** 天下快讯(滚动 N=120 */
newsFeed: { year: number; month: number; src: string; text: string }[]
lastNewsMonth: number
/** NPC 换代计数 */
npcSuccessions: number
}
export function makeWorldSimState(): WorldSimState {
return {
marketPool: { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 },
npcDyn: {},
secretQi: {},
tide: 0.5,
tideDir: 1,
tideTicks: 0,
calamityYear: -99,
newsFeed: [],
lastNewsMonth: -99,
npcSuccessions: 0
}
}
/** 世界演化参数表(全部可调) */
export const WORLDSIM = {
marketDriftRate: 0.03,
marketRebalance: 0.1,
priceFloor: 0.55,
priceCeil: 2.3,
tideCycle: 72, // 月周期(6年)
tideMin: 0.65,
tideMax: 1.35,
secretRecover: 4,
secretConsume: 0,
calamityChance: 0.18,
calamities: ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const,
npcEventChance: 0.05,
newsEvery: 24, // 月
newsKeep: 120
}
export type CalamityName = (typeof WORLDSIM.calamities)[number]
/** 灾因 → 资源方向 */
export const CALAMITY_EFFECT: Record<CalamityName, Partial<Record<string, number>>> = {
: { lingcao: -0.3 },
: { lingcao: -0.2, lingkuang: -0.1 },
: { lingcao: -0.45 },
: { beastcore: -0.25 },
: { beastcore: 0.3 },
: { lingcao: -0.2, beastcore: -0.15 }
}