/** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts) */ import { World } from '../runtime/World' import { WorldSimState, NpcDynamics, WORLDSIM, CALAMITY_EFFECT, CALAMITY_FAMILY, CalamityName, POOL_BASE } from './worldsim-data' import { pack } from '../../data/registry' 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 const ws = w.state.worldSim as WorldSimState if (!ws.secretQi || Object.keys(ws.secretQi).length === 0) { const defs = pack().missions ws.secretQi = {} for (const m of defs) ws.secretQi[m.id] = 50 } return ws } 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}——灵植减产,市价将行。`) // B7 灾年落家族(一次性体感效果) if (cl === '疫病') { for (const c of this.w.aliveMembers()) { if (this.w.rng.chance(0.6)) { const dmg = 8 + this.w.rng.int(0, 14) c.health = Math.max(1, c.health - dmg) if (c.health < 20) c.state = 'wounded' } } this.w.log('bad', `瘟疫蔓延,族中大半病倒——灵药告急!`) } else if (cl === '兽潮') { const st = this.w.state.family.stones this.w.state.family.stones -= Math.round(st * 0.04) inv(this.w)['beastcore'] = (inv(this.w)['beastcore'] ?? 0) + 2 this.w.log('bad', `兽潮涌至,坊市稍有折损;猎得兽核数枚。`) } } else { s.calamity = undefined } // ---- A. 资源循环市场(库存自然流向 + 再平衡 + 价格信号) ---- driftMarket(s, rng.next()) // ---- B. NPC 演化(聚合模拟) ---- evolveNpc(this.w, s, rng.next()) // ---- E. 天下快讯(节流) ---- if ((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 调用;统一入口,默认 6 点) */ consumeSecretQi(id: string, amount = 6): void { const s = this.s() if (s.secretQi[id] === undefined) s.secretQi[id] = 50 s.secretQi[id] = clamp(s.secretQi[id] - amount, 0, 100) } /** 秘境灵气对探索收益的门槛(低灵气收益衰减提示) */ secretQiOf(id: string): number { const s = this.s() return s.secretQi[id] ?? 50 } /** 灵气系数(修炼/掉落市场乘子) */ 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 { return this.s().secretQi } npcDyn(): Record { 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: {} } } 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 { let n = noise for (const id of MARKET_IDS) { const per = n; n += 0.13 const base = poolBase(id) const cur = s.marketPool[id] ?? base // 向基准再平衡 + 噪声漂移(价格弹性) const rebalance = (base - cur) * WORLDSIM.marketRebalance const drift = ((per % 1) - 0.5) * WORLDSIM.marketDriftRate * base s.marketPool[id] = Math.max(base * 0.3, cur + rebalance + drift) } } function inv(w: World): Record { return w.state.family.inventory } 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 POOL_BASE[id] ?? 100 } 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 const def = npcById(id) const curRealm = MAJOR_ORDER[dyn.leaderRealmIdx] ?? def.leaderRealm const lifespan = MAJORS[curRealm].lifespan if (w.state.month === 1) { dyn.leaderAge++ // 换代评估:寿终阈值 或 年首小概率(退隐/遇害)——约 50 年一代 const chanceNow = dyn.leaderAge > lifespan * 0.9 ? 0.5 : 0.02 if (w.rng.chance(chanceNow) && !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('氏', '')}氏新主` npc.power = Math.round(Math.max(40, npc.power + 15)) dyn.lastEvent = '宗祧更替' dyn.lastEventYear = year s.npcSuccessions++ pushNews(w, s, [id]) w.log('info', `【天下】${def.name} 更易宗主,气象一新。`) } } void npc } } 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 tide = s.tide > 1.0 ? '灵潮上涨' : s.tide < 0.75 ? '灵潮回落' : '汐平' let row: { year: number; month: number; src: string; text: string } if (id.startsWith('n-')) { const def = npcById(id) const dyn = s.npcDyn[id] row = { year: w.state.year, month: w.state.month, src: def.name, text: dyn ? `灵潮气象:${dyn.leaderName} 宗主更替,势力重排。` : `${def.name} 世务新张。` } } else { const pool = clamp(s.marketPool[id] ?? poolBase(id), POOL_BASE[id] * 0.5, POOL_BASE[id] * 1.8) const base = poolBase(id) const pct = Math.round(((pool - base) / base) * 100) const itemName = ITEMS[id]?.name ?? id row = { year: w.state.year, month: w.state.month, src: `世界·${itemName}`, text: pct !== 0 ? `${itemName}行情${pct > 0 ? '升' : '降'}${Math.abs(pct)}% (${tide})` : `${itemName}行情平稳 (${tide})` } } 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)) }