/** WorldSim —— 世界自进化引擎(game/engine/sim/WorldSim.ts) */ import { World } from '../runtime/World' import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, makeWorldSimState, NpcStance, REGION_TIDE, eraDuraOf, regionTideOf, calamityNames, calamityFamilyOf, calamityEffectOf, CalamityName, POOL_BASE, worldNum } from './worldsim-data' import { pack } from '../../data/registry' import { ITEMS } from '../../data/items' import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs' import { allMissions, addMission, WONDER_TEMPLATES, type MissionDef } from '../../data/secrets' import { MAJORS, MAJOR_ORDER } from '../../data/realms' const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'lingyu', 'lingmu', 'shoupi', 'lingguo', '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) const ws = w.state.worldSim as WorldSimState if (!ws.secretQi || Object.keys(ws.secretQi).length === 0) { const defs = allMissions() 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) // 0.1.23 潮汐长波:10 年档第二正弦叠加(·灵潮三十年河东·) const longWave = 0.5 + 0.5 * Math.sin((s.tideTicks % WORLDSIM.longWave) / WORLDSIM.longWave * Math.PI * 2) const longSpan = (longWave - 0.5) * 2 * WORLDSIM.longWaveSpan s.tide = clamp( WORLDSIM.tideMin + sine * (WORLDSIM.tideMax - WORLDSIM.tideMin) + longSpan + ERA_CONF[s.era ?? 'pingshi'].tideBias, WORLDSIM.tideMin - 0.05, WORLDSIM.tideMax + 0.05 ) // 秘境灵气(自动恢复 + 探索消耗由 missions 在探索时扣;灾年灵脉闭锁 0.5 恢复) const qiRecover = s.calamity ? worldNum('secretRecover') * 0.5 : s.tide > 0.85 ? worldNum('secretRecoverHi') : worldNum('secretRecover') for (const key of Object.keys(s.secretQi)) { s.secretQi[key] = clamp(s.secretQi[key] + qiRecover - (s.secretQi[key] > 70 ? 2 : 0), 0, 100) } // ---- D0. 世纪弧转移(年首评估) ---- if (this.w.state.month === 1) { const curEra = s.era ?? 'pingshi' const dur = eraDuraOf(curEra) const age = this.w.state.year - (s.eraStartYear ?? 1) if (age >= dur[0] && age < dur[1] + 5) { const flow = ERA_CONF[curEra].flow const roll = rng.next() let acc = 0 let moved = false // W1 景气闭环:世界温度调制转移概率(大世托盛世、市道衰微催乱世) const temp = this.worldTemperature() const k = WORLDSIM.tempEraK const tempMod = (next: string): number => { if (next === 'shengshi') return 1 + (temp - 1) * k if (next === 'luanshi' || next === 'mofa') return 1 + (1 - temp) * k return 1 } for (const [next, wgt] of flow) { const wgtMod = wgt * tempMod(next) acc += wgtMod if (roll < acc) { s.era = next s.eraStartYear = this.w.state.year const conf = ERA_CONF[next] this.w.log('info', `【天下】世道更替——${conf.name}。${conf.desc}`) s.newsFeed.push({ year: this.w.state.year, month: 1, src: '天道', text: `世道更替:${conf.name}来临。${conf.desc}`, kind: 'calamity' }) moved = true break } } void moved // roll 落在 flow 权重之外 → 延续本届(era 有厚度) } } // ---- D. 灾年(年签一掷 + 持续渐退;B12) ---- // 剩数递减 if ((s.calamityLeft ?? 0) > 0) s.calamityLeft = (s.calamityLeft ?? 0) - 1 const calamityP = worldNum('calamityChance') * ERA_CONF[s.era ?? 'pingshi'].calamityMult if (this.w.state.month === 1 && rng.chance(calamityP)) { const cl = rng.pick(calamityNames()) s.calamity = cl s.calamityYear = this.w.state.year s.calamityLeft = WORLDSIM.calamityMonths applyCalamityToMarket(s, cl as CalamityName) this.w.log('bad', `【天下灾年】${cl}——灵植减产,市价将行(约半年风雨)。`) this.w.emitFx('pulse', `calamity:${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', `兽潮涌至,坊市稍有折损;猎得兽核数枚。`) } } // 灾年到期散去 if (s.calamity && (s.calamityLeft ?? 0) <= 0) { s.calamity = undefined s.newsFeed.push({ year: this.w.state.year, month: this.w.state.month, src: '天道', text: `灾云散尽,灵气复清,天下重归平宁。`, kind: 'calamity' }) this.w.log('info', `【天下】灾云散尽,灵气复清。`) this.w.emitFx('ripple', 'calm') } // ---- A. 资源循环市场(世界供给/需求 + NPC 上桌 + 再平衡) ---- worldBreath(this.w, s) // A2+A3:常驻供给与需求(潮汐乘化) npcTrade(this.w, s, rng.next()) // A4:NPC 按 sells/buys 与池交易 driftMarket(s, rng.next(), this.w) // 波动项(保留噪声弹性) // ---- B0. 新秘境灵机(0.1.34 万象归元:年首机率——乱世×2) ---- if (this.w.state.month === 1) { const era2 = s.era === 'luanshi' ? 2 : 1 if (rng.chance(worldNum('newSecretChance') * era2)) { const tpl = rng.pick(WONDER_TEMPLATES) const seedS = rng.int(1000, 9999) const def: MissionDef = { ...tpl, id: `m-wonder-${seedS}`, name: `${tpl.icon}地灵诏——「${'古幽玄凌'}${rng.int(1, 9)}」` } addMission(def) s.secretQi[def.id] = Math.max(50, s.secretQi[def.id] ?? 50) s.newsFeed.push({ year: this.w.state.year, month: 1, src: '天机', text: `${def.name} 现世——灵光冲霄,远近皆见。`, kind: 'annal' }) this.w.log('info', `【天下】有异宝出世:${def.name}。`) } } // ---- B. NPC 演化(换代 + 关系网 + 互攻;B7) ---- evolveNpc(this.w, s, rng.next()) // ---- D1.5 盟友求援时效(18 月自清) ---- if (s.distress) { const distAge = (this.w.state.year - s.distress.year) * 12 + (this.w.state.month - s.distress.month) if (distAge > 18 && !this.w.state.npcFamilies[s.distress.id]?.allied) s.distress = undefined } // ---- D1.6 世鉴(50/100 年:开局 vs 现在——世界变过多少) ---- if (this.w.state.month === 1 && (this.w.state.year % 50 === 0 || this.w.state.year % 100 === 0)) { const y = this.w.state.year const wg = this.w.state.worldGen const initial = wg?.npcCount ?? 0 const now = Object.keys(this.w.state.npcFamilies).length const topNow = Math.max(...Object.values(this.w.state.npcFamilies).map((n) => n.power)) const topNowName = Object.values(this.w.state.npcFamilies).sort((a, b) => b.power - a.power)[0]?.name ?? '?' const eraName = ERA_CONF[s.era ?? 'pingshi'].name const kind = y % 100 === 0 ? 'centennial' : 'quinquagenary' const text = `${y}年世鉴:天下历经${eraName}之世,${ now < initial ? `原有${initial}家凋零至${now}家——大浪淘沙` : now > initial ? `新版图扩展至${now}家——后浪迭起` : `格局维持${now}家之衡` };当世最强为 ${topNowName}(${topNow})。` if (!s.sagaAnnals) s.sagaAnnals = [] s.sagaAnnals.push({ year: y, kind, text }) if (s.sagaAnnals.length > 40) s.sagaAnnals.splice(0, s.sagaAnnals.length - 40) this.w.log('info', `【世鉴】${text}`) } // ---- D2. 天下十年一鉴(史官综述;入 newsFeed 展示 + 独立留档) ---- if (this.w.state.month === 1 && this.w.state.year % 10 === 0) { const text = decadeChronicle(s, this.w.state.year) // 入史表前附统计锚(温度/衰退家数) void text s.newsFeed.push({ year: this.w.state.year, month: 1, src: '史官', text, kind: 'annal' }) if (!s.worldAnnals) s.worldAnnals = [] s.worldAnnals.push({ year: this.w.state.year, text }) if (s.worldAnnals.length > 80) s.worldAnnals.splice(0, s.worldAnnals.length - 80) } // ---- 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] ?? 60 const base = poolBase(id) return clamp(pool / base, WORLDSIM.priceFloor, WORLDSIM.priceCeil) } /** 玩家交易回写池:买 -delta / 卖 +delta(单件约 0.35 池份额波动) */ tradeSettle(resKey: string, delta: number): void { const s = this.s() const base = poolBase(resKey) const cur = s.marketPool[resKey] ?? base const impact = delta * 0.35 s.marketPool[resKey] = clamp(cur + impact, base * 0.12, base * WORLDSIM.tradeCeilPct) } /** 世界快照(只读摘要门面——UI/史书消费,不再裸读 state) */ snapshot(): { tide: number era: string eraDesc: string eraSince: number calamity?: string calamityLeft: number temperature: number market: Record npcs: Array<{ id: string; name: string; power: number; relation: number; stance: string; prosperity: number; declineYears: number }> annals: Array<{ year: number; text: string }> distress?: { id: string; year: number; month: number } } { const s = this.s() const era = (s.era ?? 'pingshi') as keyof typeof ERA_CONF const temp = this.worldTemperature() const market: Record = {} for (const id of MARKET_IDS) { const base = poolBase(id) const pool = s.marketPool[id] ?? base const depth = pool / base market[id] = { depth, pct: Math.round((depth - 1) * 100), dir: depth > 1.05 ? 1 : depth < 0.95 ? -1 : 0 } } const npcs = Object.entries(this.w.state.npcFamilies).map(([id, npc]) => { const dyn = s.npcDyn[id] return { id, name: String(npc.name), power: Number(npc.power), relation: Number(npc.relation), stance: dyn?.stance ?? 'guardian', prosperity: dyn?.prosperity ?? 50, declineYears: dyn?.declineYears ?? 0 } }) return { tide: s.tide, era: ERA_CONF[era].name, eraDesc: ERA_CONF[era].desc, eraSince: s.eraStartYear ?? 1, calamity: s.calamity, calamityLeft: s.calamityLeft ?? 0, temperature: temp, market, npcs, annals: s.worldAnnals ?? [], distress: s.distress } } /** 池深比率(0.12~2.4):断供告急时 < tradeFloorPct */ poolDepthOf(resKey: string): number { const s = this.s() const base = poolBase(resKey) return (s.marketPool[resKey] ?? base) / base } /** 时代名(UI 与事件链读取) */ era(): string { return ERA_CONF[(this.s().era ?? 'pingshi')].name } /** 时代拍卖/大比加成(events 周期链调用) */ eraAuctionMult(): number { return ERA_CONF[(this.s().era ?? 'pingshi')].auctionMult } /** 世界温度(市场景气 0.5~2.4;era 转移的反馈输入) */ worldTemperature(): number { const s = this.s() let sum = 0 let n = 0 for (const id of MARKET_IDS) { const base = poolBase(id) sum += (s.marketPool[id] ?? base) / base n++ } const cold = n > 0 ? sum / n : 1 // 0.1.34 战争疲劳上浮:天下杀伐不止,寒气怨气凝而灵脉皆冷(打不动即荒年) return cold - (s.warFatigue ?? 0) * worldNum('warFatigueTempK') } 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() } const wg = w.state.worldGen // 开局关系网(worldgen 产物)——首月即全球有恩怨,不再等年首扩散 const relations = wg?.relations ?? {} for (const id of Object.keys(w.state.npcFamilies)) { s.npcDyn[id] = { prosperity: 40 + ((Math.abs(id.charCodeAt(0) * 7) % 2) === 0 ? 25 : 0), stance: 'guardian', stanceSinceYear: 1, leaderName: '新任宗主', leaderRealmIdx: MAJOR_ORDER.indexOf(npcById(id)?.leaderRealm ?? 'qi'), leaderAge: 40 + w.rng.int(0, 29), lastEvent: '', lastEventYear: -99, relationsWithOthers: { ...(relations[id] ?? {}) } } } // 开局 era + 市场偏置(worldgen 产物:同 seed 同天下) if (wg?.eras) s.era = wg.eras if (wg?.marketOffset) { for (const [k, off] of Object.entries(wg.marketOffset)) { const base = poolBase(k) if (s.marketPool[k]) s.marketPool[k] = Math.max(base * 0.5, s.marketPool[k]! * (1 + off)) } } s.secretQi = {} return s } function empty(): WorldSimState { return { ...makeWorldSimState() } } /** A2+A3:世界常驻供给与需求(潮汐乘化)——池有了呼吸 */ function worldBreath(w: World, s: WorldSimState): void { const tide = s.tide const era = ERA_CONF[s.era ?? 'pingshi'] // 潮汐供给:灵涨万物丰(tide>1 供给×1.3、灵衰×0.7)——0.1.19 符号反转修复 const span = 1 + (tide - 1) * WORLDSIM.tideSupplySpan const supplySpan = Math.max(0.75, Math.min(1.35, span)) const eraSupply = era.supplyMult const eraDemand = era.demandMult let overflow = false for (const id of MARKET_IDS) { const base = poolBase(id) const cur = s.marketPool[id] ?? base // 超卖潮:池过剩 > 1.8×base 时世界产能自发回调(谷贱伤农) const saturated = cur > base * 1.8 if (saturated) overflow = true const supply = base * worldNum('worldSupplyRate') * supplySpan * eraSupply * (saturated ? 0.7 : 1) const demand = base * worldNum('worldDemandRate') * eraDemand s.marketPool[id] = clamp(cur + supply - demand, base * 0.12, base * WORLDSIM.tradeCeilPct) } // 超卖播报(年一次)——玩家能看到"别把市场玩崩" if (overflow && s.overfluxYear !== w.state.year) { s.overfluxYear = w.state.year s.newsFeed.push({ year: w.state.year, month: w.state.month, src: '坊市', text: '谷贱伤农——市面货丰价滞,灵田多有弃耕,天下产能暂歇。', kind: 'quote' }) if (w.state.totalTicks > 12) w.log('bad', '【坊市】谷贱伤农,世界产能回调。') } } /** A4:NPC 按 def.sells/buys 与池交易——世界波动的背后有了玩家(NPC 化) */ function npcTrade(w: World, s: WorldSimState, noise: number): void { let n = noise for (const [id, npc] of Object.entries(w.state.npcFamilies)) { const per = n; n += 0.31 const def = npcById(id) const dyn = s.npcDyn[id] ?? initDynFor(id) s.npcDyn[id] = dyn if (!def) continue const isCalamity = !!s.calamity const stance2 = (dyn.stance ?? 'guardian') as string const tradeMult = stance2 === 'expand' ? 1.3 : stance2 === 'endure' ? 0.7 : 1 // W3 区域灵势:各 era 修正(e.g. 乱世北岳玄影峰灵衰 0.08) const regionMod = 1 + regionTideOf(id, s.era ?? 'pingshi') const rate = worldNum('npcTradeRate') * (isCalamity ? 0.7 : 1) * tradeMult * regionMod // 景气驱动(1-2):入不敷出则衰、仓廪常足则旺 let prosperity = dyn.prosperity ?? 50 // 卖:NPC 抛货 → 池增;有货自给,景气微涨 for (const itemId of def.sells ?? []) { if (!MARKET_IDS.includes(itemId)) continue const base = poolBase(itemId) const vol = base * rate * (0.6 + (per % 1) * 0.8) s.marketPool[itemId] = clamp((s.marketPool[itemId] ?? base) + vol, base * 0.12, base * WORLDSIM.tradeCeilPct) prosperity += 0.5 } // 买:NPC 采购 → 池减;池太浅采不到 → 断供受挫(景气大伤) for (const itemId of def.buys ?? []) { if (!MARKET_IDS.includes(itemId)) continue const base = poolBase(itemId) const cur = s.marketPool[itemId] ?? base const vol = base * rate * (0.6 + ((per * 7) % 1) * 0.8) if (cur - vol < base * 0.12) { prosperity -= 2 } else { s.marketPool[itemId] = cur - vol prosperity += 0.8 } } // 灾年支出(共苦):景气再挫,若已困顿伤其筋骨 if (isCalamity) prosperity -= 3 dyn.prosperity = clamp(prosperity, 8, 100) // 景气→power 联动:旺者增益、困者式微 if (dyn.prosperity > 75) npc.power = Math.min(900, npc.power + 1) else if (dyn.prosperity < 30) npc.power = Math.max(52, npc.power - 1) } } function itemNameOf(id: string): string { const N: Record = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', lingyu: '灵玉', lingmu: '灵木', shoupi: '兽皮', lingguo: '灵果' } return N[id] ?? id } function driftMarket(s: WorldSimState, noise: number, w: World): void { let n = noise // W3 材料生态相关:自然资源同向微漂(灵木/符链共享偏置——物产同源) const matBias = (noise - 0.5) * 0.012 for (const id of MARKET_IDS) { const per = n; n += 0.13 const base = poolBase(id) const cur = s.marketPool[id] ?? base // 向基准再平衡(弱化锚定:让供需流与 era 真正撬动价格)+ 噪声漂移(价格弹性) const rebalance = (base - cur) * WORLDSIM.marketRebalance const drift = ((per % 1) - 0.5) * WORLDSIM.marketDriftRate * base const isMaterial = ['lingyu', 'lingmu', 'shoupi', 'lingguo'].includes(id) s.marketPool[id] = Math.max(base * WORLDSIM.poolFloorPct, cur + rebalance + drift + (isMaterial ? matBias * base : 0)) // 0.1.34 断供监测:货源紧张连续月计数(触发价上浮×世界广播) const line = base * WORLDSIM.shortageLine if (s.marketPool[id] < line) { s.shortage = s.shortage ?? {} s.shortage[id] = (s.shortage[id] ?? 0) + 1 if (s.shortage[id] === worldNum('shortageMonths')) { s.shortageWarned = s.shortageWarned ?? {} s.shortageWarned[id] = w.state.year s.newsFeed.push({ year: w.state.year, month: 6, src: '行商', text: `${itemNameOf(id)}断货数月——坊市惜售,价腾三成。`, kind: 'annal' }) } } else if (s.shortage?.[id]) { delete s.shortage[id] } } } function inv(w: World): Record { return w.state.family.inventory } function applyCalamityToMarket(s: WorldSimState, cl: CalamityName): void { const eff = calamityEffectOf(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 const ids = Object.keys(w.state.npcFamilies) for (const [id, npc] of Object.entries(w.state.npcFamilies)) { const dyn = s.npcDyn[id] ?? initDynFor(id) s.npcDyn[id] = dyn const defFirst = npcById(id) if (!defFirst) continue if (!dyn.relationsWithOthers || Object.keys(dyn.relationsWithOthers).length === 0) { // 关系网初始化:同风格亲近(剑修→剑修 +30~55),异风格事仇(-35~+15) const mine = npcById(id) if (!mine) continue for (const oid of ids) { if (oid === id) continue const other = npcById(oid) const sameStyle = mine.style === other?.style dyn.relationsWithOthers[oid] = sameStyle ? 30 + w.rng.int(0, 25) : -35 + w.rng.int(0, 50) } } const def = defFirst const curRealm = MAJOR_ORDER[dyn.leaderRealmIdx] ?? def.leaderRealm const lifespan = MAJORS[curRealm].lifespan if (w.state.month === 1) { dyn.leaderAge++ // 姿态评估(era>景气>power>relation) if (w.state.month === 1 && (w.state.year - (dyn.stanceSinceYear ?? 1)) >= 8) { const newStance = assessStance(w, id, dyn) if (newStance !== dyn.stance) { dyn.stance = newStance dyn.stanceSinceYear = w.state.year pushNews(w, s, [id]) w.log('info', `【天下】${def.name} 态度一变——${STANCE_NAME[newStance]}。`) } } // W1 生灭:衰微累积/覆灭附庸/新贵补位(年首) if (w.state.month === 1) { // 残衰减判定(0.1.27 可达化):弱即衰(盛世会被增长拉回不灭、乱世真灭——世道越乱换血越烈) const fragile = npc.power < 150 const battleWorn = (dyn.warLosses ?? 0) >= 2 && npc.power < 200 if (!npc.allied && (fragile || battleWorn)) { dyn.declineYears = (dyn.declineYears ?? 0) + 1 if (dyn.declineYears >= 8) { // 覆灭:最强邻家分食 const peers = Object.entries(w.state.npcFamilies).filter(([pid]) => pid !== id) if (peers.length > 0) { const strongest = peers.sort(([, a], [, b]) => b.power - a.power)[0]![0] w.state.npcFamilies[strongest]!.power = Math.min(900, Math.round(w.state.npcFamilies[strongest]!.power * 1.05)) s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `${def.name} 势微不振,终被${w.state.npcFamilies[strongest]!.name}吞并——天下又少一家。`, kind: 'annal' }) w.log('bad', `【天下】${def.name} 吞并于 ${w.state.npcFamilies[strongest]!.name},势力重排。`) } else { s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `${def.name} 势微而亡,悄无遗响。`, kind: 'annal' }) } purgeNpc(w, s, id) continue } } else { dyn.declineYears = 0 } // 新贵补位:家数 < 开局目标(4~8 的种子格)且几率(乱世更频)——世界会新生 const aliveCount = Object.keys(w.state.npcFamilies).length const cap = Math.min(8, w.state.worldGen?.npcCount ?? 4) if (aliveCount < cap && w.rng.chance(worldNum('greatNewbornChance') * (s.era === 'luanshi' ? 2 : 1))) { spawnNewbornDynasty(w, s) } } // 顶峰衰(0.1.27):绝顶不可久踞——power>=700 年首回落(豪强被制衡) if (w.state.month === 1 && npc.power >= 700 && w.rng.chance(0.08)) { npc.power = Math.round(npc.power * 0.9) dyn.lastEvent = '顶峰回落' dyn.lastEventYear = w.state.year s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `${def.name} 盛极而衰,锋芒微敛。`, kind: 'annal' }) } // 关系漂移:年首各 ±5(邻近的讲合、世仇的愈深) for (const oid of ids) { if (oid === id) continue const cur = dyn.relationsWithOthers[oid] ?? 0 dyn.relationsWithOthers[oid] = clamp(cur + (w.rng.chance(0.5) ? 1 : -1) * 5, -100, 100) } // 换代评估:寿终阈值 或 年首小概率(退隐/遇害)——约 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('氏', '')}氏新主` dyn.prosperity = 55 + w.rng.int(0, 20) 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} 更易宗主,气象一新。`) w.emitFx('ripple', `succession:${id}`) } // B7 互攻:与世仇(关系<-50)年首相搏——败方伤筋动骨 for (const oid of ids) { if (oid === id) continue const foe = w.state.npcFamilies[oid] if (!foe) continue const rel = dyn.relationsWithOthers[oid] ?? 0 const foeStance = dyn.stance ?? 'guardian' const hateBar = foeStance === 'expand' ? -40 : foeStance === 'endure' ? -70 : -50 if (foeStance === 'ally') void hateBar // W4 灾年全环:天下凶年群雄相噬——互攻概率 ×1.5 const foeEventChance = (s.calamity ? WORLDSIM.npcEventChance * 1.5 : WORLDSIM.npcEventChance) if (rel < hateBar && foeStance !== 'ally' && w.rng.chance(foeEventChance)) { // 1-3 蝴蝶效应:互攻按实力加权(强者越可能胜,弱者一败再败) const wSum = npc.power + foe.power const winner = w.rng.chance(wSum > 0 ? npc.power / wSum : 0.5) ? npc : foe const loser = winner === npc ? foe : npc winner.power = Math.min(900, Math.round(winner.power * 1.06)) loser.power = Math.max(52, Math.round(loser.power * 0.82)) ;(s.npcDyn[loser.id] as { warLosses?: number }).warLosses = ((s.npcDyn[loser.id] as { warLosses?: number }).warLosses ?? 0) + 1 ;(s.npcDyn[winner.id] as { warLosses?: number }).warLosses = 0 // 打残广播:天下敢弱必胜之——邻家对败者关系趋冷、对胜者暗生敬畏 for (const [oid, otherDyn] of Object.entries(s.npcDyn)) { if (oid === loser.id) continue const rr = otherDyn.relationsWithOthers[loser.id] ?? 0 otherDyn.relationsWithOthers[loser.id] = clamp(rr - 8, -100, 100) if (oid === winner.id) continue const wr = otherDyn.relationsWithOthers[winner.id] ?? 0 otherDyn.relationsWithOthers[winner.id] = clamp(wr + 4, -100, 100) } // 0.1.34 战争疲劳:乱象(互攻/大劫)累积——世界温度上浮,乱世加速 s.warFatigue = clamp((s.warFatigue ?? 0) + worldNum('warFatigueRise'), 0, 1) s.newsFeed.push({ year, month: 1, src: winner.name, text: `${winner.name} 与 ${loser.name} 起衅——痛挫其锋,势力大动。` }) w.log('info', `【天下】${winner.name} 击破 ${loser.name},气象一新。`) // 盟友求援:下盟友处告急,请其同盟(玩家)出手 if (loser.allied) { s.distress = { id: loser.id, year, month: 1 } s.newsFeed.push({ year, month: 1, src: loser.name, text: `${loser.name} 遭${winner.name}重创,遣使来吾族求援:请解囊相助。`, kind: 'npc' }) w.log('bad', `【天下】盟邦 ${loser.name} 遇袭告急!`) } break } } } } } /** 覆灭清理:npcFamilies/npcDyn/关系网/raidCD/排队事件/distress/dynamic def 全摘 */ function purgeNpc(w: World, s: WorldSimState, id: string): void { delete w.state.npcFamilies[id] delete s.npcDyn[id] for (const dyn of Object.values(s.npcDyn)) { if (dyn?.relationsWithOthers && id in dyn.relationsWithOthers) delete dyn.relationsWithOthers[id] } delete w.state.family.flag[`raidCD-${id}`] delete w.state.family.flag[`tauntCD-${id}`] w.state.eventQueue = (w.state.eventQueue ?? []).filter((e) => !e.startsWith('ev-raid-') || !e.endsWith(id)) if (s.distress?.id === id) s.distress = undefined unregisterNpcDef(id) w.emitFx('ripple', `purge:${id}`) } const NEWBORN_STYLES = ['新锐剑宗', '灵植世家', '商盟豪族', '兵修门阀', '符箓仙门'] as const const NEWBORN_REGIONS = ['南麓青泽', '西山雾谷', '东溪云汉', '北原古井'] as const /** 新贵补位:随机风格/区域/初始 power 的新家族(乱世更频) */ function spawnNewbornDynasty(w: World, s: WorldSimState): void { const rng = w.rng const style = rng.pick([...NEWBORN_STYLES]) const region = rng.pick([...NEWBORN_REGIONS]) // P1-10 id 掺 seed 指纹(防跨档碰撞) let seedHash = 0 for (let i = 0; i < w.state.seed.length; i++) seedHash = (seedHash * 31 + w.state.seed.charCodeAt(i)) >>> 0 const id = `n-new-${(seedHash % 4096).toString(36)}-${rng.int(1, 9999)}` const name = `${region.slice(0, 2)}${NEWBORN_NAME_SUFFIX[rng.int(0, NEWBORN_NAME_SUFFIX.length - 1)]}` const def = { id, name, region, desc: `${style}初立,闷头搞了十年发展,如今渐攒起一份家业。`, style, leaderRealm: 'foundation' as const, initialPower: 100 + rng.int(0, 60), powerGrowth: [3, 9] as [number, number], sells: [], buys: ['lingcao', 'lingkuang'] } registerNpcDef(def) // P0-2 生成即落档(worldGen.npcs 追加——读档幂等重注册,防御新贵僵尸) if (w.state.worldGen) { if (!w.state.worldGen.npcs) w.state.worldGen.npcs = [] w.state.worldGen.npcs.push({ id: def.id, name: def.name, region: def.region, style: def.style, desc: def.desc, leaderRealm: def.leaderRealm, initialPower: def.initialPower, powerGrowth: def.powerGrowth, sells: def.sells, buys: def.buys }) } w.state.npcFamilies[id] = { id, name, region, power: def.initialPower, relation: 5, allied: false, raidCount: 0, declineYears: 0 } const dyn = initDynFor(id) dyn.leaderName = `${name.slice(0, 2)}氏新主` s.npcDyn[id] = dyn const asr = s.era ?? 'pingshi' w.log('good', `【天下】新贵${name}在${region}崛起(${rgStyleName(def.style)}),天下格局松动。`) s.newsFeed.push({ year: w.state.year, month: 1, src: '史官', text: `新贵${name}崛起于${region}——天下格局松动。`, kind: 'annal' }) void asr } function rgStyleName(st: string): string { return st } const NEWBORN_NAME_SUFFIX = ['氏', '氏', '宗', '寨'] as const /** 姿态评估:乱世逼扩张、低谷存隐忍、盛世好结盟、元气足则守成 */ const STANCE_NAME: Record = { guardian: '守成', expand: '扩张', endure: '隐忍', ally: '结盟' } function assessStance(w: World, id: string, dyn: NpcDynamics): NpcStance { const npc = w.state.npcFamilies[id] const era = ((w.state.worldSim as WorldSimState)?.era ?? 'pingshi') as string const prosperity = dyn.prosperity ?? 50 const power = npc?.power ?? 100 const relation = npc?.relation ?? 0 // 硬判据优先 if (prosperity < 30 || power < 45) return 'endure' if (relation >= 50) return 'ally' if (era === 'luanshi' && power >= 120) return 'expand' if (era === 'mofa') return 'endure' // 软权重:乱世偏扩张、盛世偏守成、平世均衡 const W: Record = { guardian: 3, expand: era === 'luanshi' ? 6 : 2, endure: era === 'mofa' ? 5 : 2, ally: era === 'shengshi' || era === 'pingshi' ? 4 : 1 } const pick = (Object.keys(W) as NpcStance[]).sort((a, b) => W[b] - W[a]) // 用幂等 rng 抽取(先取最高权)+ 30% 软翻转 const top = pick[0]! if (w.rng.chance(0.7)) return top return pick[1]! } function initDynFor(id: string): NpcDynamics { const def = npcById(id) if (!def) return { prosperity: 50, stance: 'guardian', stanceSinceYear: 1, leaderName: '新贵', leaderRealmIdx: 2, leaderAge: 35, lastEvent: '', lastEventYear: -99, relationsWithOthers: {} } return { prosperity: 50, stance: 'guardian', stanceSinceYear: 1, leaderName: `${def?.name.replace('氏', '') ?? '新宗'}氏宗主`, leaderRealmIdx: MAJOR_ORDER.indexOf(def?.leaderRealm ?? 'qi'), 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; itemId?: string; kind?: 'quote' | 'npc' | 'calamity' | 'annal' } if (id.startsWith('n-')) { const def = npcById(id) const dyn = s.npcDyn[id] if (!def) return row = { year: w.state.year, month: w.state.month, src: def.name, text: dyn ? `灵潮气象:${dyn.leaderName} 宗主更替,势力重排。` : `${def.name} 世务新张。`, kind: 'npc' } } 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})`, itemId: id, kind: 'quote' } } s.newsFeed.push(row) s.lastNewsMonth = w.state.month } /** 十年一鉴:从近十年快讯聚合综述(史官体) */ function decadeChronicle(s: WorldSimState, year: number): string { const from = year - 9 const rows = s.newsFeed.filter((r) => r.year >= from && r.year < year) const calamities = rows.filter((r) => r.kind === 'calamity' && r.text.includes('灾')).length const successions = rows.filter((r) => r.kind === 'npc' && !r.text.includes('态度')).length const quotes = rows.filter((r) => r.kind === 'quote').length const era = ERA_CONF[s.era ?? 'pingshi'].name const tideAvg = s.tide const parts: string[] = [] parts.push(`${from}年~${year - 1}年,天下${era}之期`) if (calamities) parts.push(`灾年${calamities}现`) if (successions) parts.push(`宗祧更替${successions}家`) if (quotes) parts.push(`行情起落${quotes}波`) parts.push(`灵潮${tideAvg > 1 ? '善' : tideAvg < 0.8 ? '劣' : '平'}`) return `史官曰:${parts.join(',')}。` } function clamp(v: number, lo: number, hi: number): number { return Math.max(lo, Math.min(hi, v)) }