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:
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user