Files
ChronicleOfTheImmortalClan/src/renderer/game/engine/sim/WorldSim.ts
T
thzxx de46808188 feat(0.1.16-P1): 资源闭环引擎(修为耗草/炼丹概率化/铸器/灵脉收益/NPC战力/灾变落效)+ C13C14
【A 经济闭环】
- 修为经济化:练气以上修行者月耗灵草(闭关2普通1),无草速修0.6~1.0衰减+缺草年告警
- 炼丹等级化:bonusOf 表达式引擎(白名单安全求值),craftChance 转正;丹方单源 PILL_RECIPES(聚气L1/凝元L2/破境L3)+ 失败折半退料
- 炼器筑宝:FORGE_RECIPES(法器/灵器/法宝,灵矿+兽核+灵石)
- 秘境灵脉:secretQiOf→rollWarbooty 收益门槛(0.5~1.3折算);掉落按秘境 techGrades 过滤
- NPC 实力参战:raid 强度/风险随 npc.power 浮动(0.5~1.8)
- 灾年落效:疫病/兽潮一次性袭击 + CALAMITY_FAMILY 月度减产乘子

【C 治理】
- C13 破境丹渡劫修正:大境界时药力→tribBoost 加成(+12%)入渡劫事件,不再跳过三选
- C14 存储防线:loadState 补 migrate;MigrationError(TOO_NEW/未知版本)提示升级

【UI 半批】
- 坊市新增 定制tab(丹方/铸器);ChroniclePanel 手写史书输入条;MemberModal 册立家主钮
- 年报摘要(要闻5条+辞世讣告);EventModal 选项收益预览
- 结盟引擎:setAlliance(relation≥40)/盟友岁差不降/年利15灵石
2026-08-23 14:10:42 +08:00

256 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 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<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: {}
}
}
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<string, number> {
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))
}