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:
@@ -55,26 +55,8 @@ export class GameEngine {
|
||||
|
||||
advance(): void {
|
||||
if (this.world.state.gameOver) return
|
||||
const kernelClock = this.kernel.clock
|
||||
const worldClock = this.world.clock
|
||||
// 内核统一驱动(执行内核时钟注册,world.clock 仅作镜像)
|
||||
void worldClock
|
||||
this.tickOn(kernelClock)
|
||||
this.world.advanceMonth()
|
||||
this.tickSinceSave++
|
||||
this.world.syncRng()
|
||||
}
|
||||
|
||||
private tickOn(clock: GameClock): void {
|
||||
const w = this.world
|
||||
const s = w.state
|
||||
s.month++
|
||||
if (s.month > 12) {
|
||||
s.month = 1
|
||||
s.year++
|
||||
clock.fireYearStart(w as never)
|
||||
}
|
||||
s.totalTicks++
|
||||
clock.stepMonthly(w as never)
|
||||
}
|
||||
|
||||
syncRng(): void {
|
||||
|
||||
@@ -14,6 +14,7 @@ export type PhaseId =
|
||||
| 'missions'
|
||||
| 'events'
|
||||
| 'diplomacy'
|
||||
| 'worldsim'
|
||||
| 'epilogue'
|
||||
|
||||
export type SystemHook = (w: World) => void
|
||||
@@ -31,6 +32,7 @@ export const PHASE_ORDER: PhaseId[] = [
|
||||
'missions',
|
||||
'events',
|
||||
'diplomacy',
|
||||
'worldsim',
|
||||
'epilogue'
|
||||
]
|
||||
|
||||
|
||||
@@ -53,6 +53,9 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
if (w.ageOf(c) < 8) rate *= 0.4
|
||||
if (w.ageOf(c) > 65) rate *= 0.72
|
||||
rate *= masteryRateOfMajor(c.realm.major)
|
||||
// 世界灵气潮汐
|
||||
const tide = w.state.worldSim?.tide ?? 1
|
||||
rate *= tide
|
||||
return rate
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,15 @@ export function sendMission(w: World, defId: string, members: string[], formatio
|
||||
const c = w.memberById(id)
|
||||
c.state = 'expedition'
|
||||
})
|
||||
// 世界秘境灵气消耗(有 sim 时)
|
||||
if (w.state.worldSim?.secretQi) {
|
||||
const qi = w.state.worldSim.secretQi[def.id] as number | undefined
|
||||
if (qi !== undefined) {
|
||||
w.state.worldSim.secretQi[def.id] = Math.max(0, qi - 6)
|
||||
} else {
|
||||
w.state.worldSim.secretQi[def.id] = 44
|
||||
}
|
||||
}
|
||||
w.state.missions.push(m)
|
||||
w.state.family.missionIds.push(m.id)
|
||||
w.log('info', `队伍出发探索【${def.name}】。`)
|
||||
|
||||
@@ -45,6 +45,12 @@ export interface WorldEventBus {
|
||||
onPluginChange?(id: string, action: string): void
|
||||
}
|
||||
|
||||
import { WorldSim } from '../sim/WorldSim'
|
||||
|
||||
export function worldSimOf(w: World): WorldSim {
|
||||
return new WorldSim(w)
|
||||
}
|
||||
|
||||
export function normalizeGameState(state: GameState): GameState {
|
||||
// 老版本存档(<0.1.1)缺少新增字段,加载时补齐,避免运行期 undefined 崩溃
|
||||
if (!state.finance) state.finance = { accum: 0 }
|
||||
@@ -99,12 +105,12 @@ export class World {
|
||||
normalizeGameState(state)
|
||||
this.state = state
|
||||
this.out = out
|
||||
this.clock = emptyClock()
|
||||
if (kernel) {
|
||||
this.clock = kernel.clock
|
||||
this.kernel = kernel
|
||||
this.rng = kernel.rng
|
||||
clockFromKernel(this.clock, kernel.clock)
|
||||
} else {
|
||||
this.clock = emptyClock()
|
||||
this.rng = new Rng(state.rng)
|
||||
}
|
||||
this.systems = Object.fromEntries(SYSTEM_DEFS.map((d) => [d.id, { enabled: true }]))
|
||||
|
||||
@@ -8,6 +8,7 @@ import { missionTick } from './Systems/missions'
|
||||
import { eventRoll } from './Systems/events'
|
||||
import { diplomacyTick } from './Systems/diplomacy'
|
||||
import { yearStartMarriage } from './Systems/marriage'
|
||||
import { WorldSim } from '../sim/WorldSim'
|
||||
|
||||
/** 原语义保留:满门凋零后仅存生产/寿元与收尾 */
|
||||
function ifAlive(fn: (w: World) => void): (w: World) => void {
|
||||
@@ -55,6 +56,11 @@ export function installCoreSystems(ctx: PluginContext): Array<() => void> {
|
||||
unsubs.push(clock.register('missions', viaCap('missions', ifAlive((w: World) => missionTick(w)))))
|
||||
unsubs.push(clock.register('events', viaCap('events', ifAlive((w: World) => eventRoll(w)))))
|
||||
unsubs.push(clock.register('diplomacy', viaCap('diplomacy', ifAlive((w: World) => diplomacyTick(w)))))
|
||||
unsubs.push(clock.register('worldsim', (w: World) => {
|
||||
if (w.sysEnabled('worldsim')) {
|
||||
new WorldSim(w).tick()
|
||||
}
|
||||
}))
|
||||
unsubs.push(clock.register('epilogue', (w: World) => w.epilogueTick()))
|
||||
|
||||
return unsubs
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { World } from '../runtime/World'
|
||||
import { pack } from '../../data/registry'
|
||||
import { traitBonuses } from '../runtime/pcgen'
|
||||
|
||||
import { WorldSim } from './WorldSim'
|
||||
|
||||
export function marketPrice(w: World, itemId: string): number {
|
||||
const item = pack().items[itemId]
|
||||
if (!item) return 0
|
||||
@@ -9,10 +11,18 @@ export function marketPrice(w: World, itemId: string): number {
|
||||
const fam = w.state.family
|
||||
const mult = typeof fam.flag['priceMult'] === 'number' ? (fam.flag['priceMult'] as number) : 1
|
||||
const mood = fam.reputation >= 40 ? 1.06 : fam.reputation >= 20 ? 1.02 : 0.98
|
||||
// 世界行情乘子(缺省 1)
|
||||
let simMult = 1
|
||||
if (w.state.worldSim?.marketPool) {
|
||||
const pool = w.state.worldSim.marketPool[itemId] as number | undefined
|
||||
const basePool = POOL_BASE[itemId] ?? 100
|
||||
const r = (pool ?? basePool) / basePool
|
||||
simMult = Math.max(0.55, Math.min(2.3, r))
|
||||
}
|
||||
// 利己(priceMult)与信誉修正
|
||||
const sellers = w.aliveMembers().filter((c) => traitBonuses(c).priceMult > 0).length
|
||||
const liarPct = Math.min(0.2, sellers * 0.04)
|
||||
return Math.max(1, Math.round(base * mult * mood * (1 - liarPct)))
|
||||
return Math.max(1, Math.round(base * mult * mood * simMult * (1 - liarPct)))
|
||||
}
|
||||
|
||||
export function buyItem(w: World, itemId: string, count: number): boolean {
|
||||
@@ -50,6 +60,8 @@ export function techniquePrice(techId: string): number {
|
||||
|
||||
import { TECHNIQUES } from '../../data/techniques'
|
||||
|
||||
const POOL_BASE: Record<string, number> = { lingcao: 600, lingkuang: 300, beastcore: 80, 'pill-qiyuan': 90, 'pill-ningyuan': 40 }
|
||||
|
||||
const TECHNIQUE_GRADE_PRICE: Record<number, number> = { 1: 120, 2: 300, 3: 700, 4: 1600 }
|
||||
const TECH_GRADE_BASE: Record<string, number> = Object.fromEntries(
|
||||
TECHNIQUES.map((t) => [t.id, TECHNIQUE_GRADE_PRICE[t.grade] ?? 300])
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
export interface NpcDynamics {
|
||||
/** 宗主姓名快照(换代时更新) */
|
||||
leaderName: string
|
||||
leaderRealmIdx: number
|
||||
leaderAge: number
|
||||
/** 演化旗标 */
|
||||
lastEvent: string
|
||||
lastEventYear: number
|
||||
relationsWithOthers: Record<string, number>
|
||||
}
|
||||
|
||||
export interface SecretQi {
|
||||
qi: number
|
||||
peak: number
|
||||
}
|
||||
|
||||
export interface WorldSimState {
|
||||
/** 世界库存池(资源循环) */
|
||||
marketPool: Record<string, number>
|
||||
/** NPC 动力学 */
|
||||
npcDyn: Record<string, NpcDynamics>
|
||||
/** 秘境灵气条 */
|
||||
secretQi: Record<string, number>
|
||||
/** 灵气潮汐(0-100 全局系数) */
|
||||
tide: number
|
||||
tideDir: 1 | -1
|
||||
tideTicks: number
|
||||
/** 灾年(当年灾因 id) */
|
||||
calamity?: string
|
||||
calamityYear: number
|
||||
/** 天下快讯(滚动 N=120) */
|
||||
newsFeed: { year: number; month: number; src: string; text: string }[]
|
||||
lastNewsMonth: number
|
||||
/** NPC 换代计数 */
|
||||
npcSuccessions: number
|
||||
}
|
||||
|
||||
export function makeWorldSimState(): 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
|
||||
}
|
||||
}
|
||||
|
||||
/** 世界演化参数表(全部可调) */
|
||||
export const WORLDSIM = {
|
||||
marketDriftRate: 0.03,
|
||||
marketRebalance: 0.1,
|
||||
priceFloor: 0.55,
|
||||
priceCeil: 2.3,
|
||||
tideCycle: 72, // 月周期(6年)
|
||||
tideMin: 0.65,
|
||||
tideMax: 1.35,
|
||||
secretRecover: 4,
|
||||
secretConsume: 0,
|
||||
calamityChance: 0.18,
|
||||
calamities: ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const,
|
||||
npcEventChance: 0.05,
|
||||
newsEvery: 24, // 月
|
||||
newsKeep: 120
|
||||
}
|
||||
|
||||
export type CalamityName = (typeof WORLDSIM.calamities)[number]
|
||||
|
||||
/** 灾因 → 资源方向 */
|
||||
export const CALAMITY_EFFECT: Record<CalamityName, Partial<Record<string, number>>> = {
|
||||
旱灾: { lingcao: -0.3 },
|
||||
涝灾: { lingcao: -0.2, lingkuang: -0.1 },
|
||||
蝗灾: { lingcao: -0.45 },
|
||||
疫病: { beastcore: -0.25 },
|
||||
兽潮: { beastcore: 0.3 },
|
||||
寒潮: { lingcao: -0.2, beastcore: -0.15 }
|
||||
}
|
||||
Reference in New Issue
Block a user