【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灵石
256 lines
8.4 KiB
TypeScript
256 lines
8.4 KiB
TypeScript
/** 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))
|
||
}
|