v0.1.23: 生灭千秋 + 插件生态化第一课(1027 全绿)
【世界模型:世界会死人也会新生】 - 家族生灭:declineYears(power<58+景气<38 连续8年)→ 覆灭(最强邻分食+史官广播)+ 新贵补位(6%/年,乱世×2) —— NPC 永远四家铁打的现状终结;玩家补刀(raid×0.82)可加速衰亡 - npcById 双源可空(静态+动态注册表;不再 throw——15 处消费者防御化) - purgeNpc 全局清理(raidCD/事件队列/恩怨网/distress/动态 def) - 秘境产出回池(loot 20% 经 tradeSettle 回流)——资源循环最后单向封口 - WorldSnapshot 只读摘要门面(UI/史书消费,零行为变更) - 潮汐长波(10 年正弦 ±0.08 叠加,灵潮三十年河东) 【插件生态化(0.1.8 后第一次大进化)】 - 持久化:GameState.plugins 落盘 + PLUGIN_REGISTRY 读档重装(engineFromSnapshot 真实路径验证) - plugin-public.ts 公共导出面(第三方单入口+纪律) - 双闸:能力卡随插件启停联动;池自动回滚 - 契约补完:Proxy 追踪(池/卡/pack 自动回滚)+ onUninstall 真调 + 能力卡入 World 实例(防跨档泄漏) - SettingsPanel 启停按钮/依赖详情;demo 与 tests 去重(发现并修复:npcById 双源静态源未绑定——全局 NPC 停滞回归) 【测试】1027 全绿(44 套件):dynasty-plugin 7 例(覆灭/新贵/回池/快照/持久化/双闸/自动回滚); 金钟罩 0.1.23 基线固化;build 通过
This commit is contained in:
@@ -3,7 +3,7 @@ import { World } from '../runtime/World'
|
||||
import { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, ERA_DURA, makeWorldSimState, NpcStance, REGION_TIDE, 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 { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
|
||||
import { MAJORS, MAJOR_ORDER } from '../../data/realms'
|
||||
|
||||
const MARKET_IDS = ['lingcao', 'lingkuang', 'beastcore', 'pill-qiyuan', 'pill-ningyuan']
|
||||
@@ -30,8 +30,11 @@ export class WorldSim {
|
||||
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) + ERA_CONF[s.era ?? 'pingshi'].tideBias,
|
||||
WORLDSIM.tideMin + sine * (WORLDSIM.tideMax - WORLDSIM.tideMin) + longSpan + ERA_CONF[s.era ?? 'pingshi'].tideBias,
|
||||
WORLDSIM.tideMin - 0.05,
|
||||
WORLDSIM.tideMax + 0.05
|
||||
)
|
||||
@@ -138,6 +141,8 @@ export class WorldSim {
|
||||
// ---- 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 })
|
||||
@@ -188,6 +193,49 @@ export class WorldSim {
|
||||
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<string, { depth: number; pct: number; dir: -1 | 0 | 1 }>
|
||||
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<string, { depth: number; pct: number; dir: -1 | 0 | 1 }> = {}
|
||||
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()
|
||||
@@ -239,7 +287,7 @@ function initSim(w: World): WorldSimState {
|
||||
stance: 'guardian',
|
||||
stanceSinceYear: 1,
|
||||
leaderName: '新任宗主',
|
||||
leaderRealmIdx: MAJOR_ORDER.indexOf(npcById(id).leaderRealm),
|
||||
leaderRealmIdx: MAJOR_ORDER.indexOf(npcById(id)?.leaderRealm ?? 'qi'),
|
||||
leaderAge: 40 + w.rng.int(0, 29),
|
||||
lastEvent: '',
|
||||
lastEventYear: -99,
|
||||
@@ -292,6 +340,7 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
|
||||
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
|
||||
@@ -326,7 +375,7 @@ function npcTrade(w: World, s: WorldSimState, noise: number): void {
|
||||
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(42, npc.power - 1)
|
||||
else if (dyn.prosperity < 30) npc.power = Math.max(52, npc.power - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,19 +415,22 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
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
|
||||
const sameStyle = mine.style === other?.style
|
||||
dyn.relationsWithOthers[oid] = sameStyle
|
||||
? 30 + w.rng.int(0, 25)
|
||||
: -35 + w.rng.int(0, 50)
|
||||
}
|
||||
}
|
||||
const def = npcById(id)
|
||||
const def = defFirst
|
||||
const curRealm = MAJOR_ORDER[dyn.leaderRealmIdx] ?? def.leaderRealm
|
||||
const lifespan = MAJORS[curRealm].lifespan
|
||||
if (w.state.month === 1) {
|
||||
@@ -393,6 +445,37 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
w.log('info', `【天下】${def.name} 态度一变——${STANCE_NAME[newStance]}。`)
|
||||
}
|
||||
}
|
||||
// W1 生灭:衰微累积/覆灭附庸/新贵补位(年首)
|
||||
if (w.state.month === 1) {
|
||||
if (!npc.allied && npc.power < 58 && (dyn.prosperity ?? 50) < 38) {
|
||||
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 且几率(乱世更频)——世界会新生
|
||||
const aliveCount = Object.keys(w.state.npcFamilies).length
|
||||
const cap = 4
|
||||
if (aliveCount < cap && w.rng.chance(WORLDSIM.greatNewbornChance * (s.era === 'luanshi' ? 2 : 1))) {
|
||||
spawnNewbornDynasty(w, s)
|
||||
}
|
||||
}
|
||||
// 关系漂移:年首各 ±5(邻近的讲合、世仇的愈深)
|
||||
for (const oid of ids) {
|
||||
if (oid === id) continue
|
||||
@@ -431,7 +514,7 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
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(40, Math.round(loser.power * 0.82))
|
||||
loser.power = Math.max(52, Math.round(loser.power * 0.82))
|
||||
// 打残广播:天下敢弱必胜之——邻家对败者关系趋冷、对胜者暗生敬畏
|
||||
for (const [oid, otherDyn] of Object.entries(s.npcDyn)) {
|
||||
if (oid === loser.id) continue
|
||||
@@ -462,9 +545,67 @@ function evolveNpc(w: World, s: WorldSimState, noise: number): void {
|
||||
}
|
||||
}
|
||||
|
||||
const STANCE_NAME: Record<NpcStance, string> = { guardian: '守成', expand: '扩张', endure: '隐忍', ally: '结盟' }
|
||||
/** 覆灭清理: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[`auction-${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])
|
||||
const id = `n-new-${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)
|
||||
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<NpcStance, string> = { 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
|
||||
@@ -487,12 +628,13 @@ function assessStance(w: World, id: string, dyn: NpcDynamics): NpcStance {
|
||||
|
||||
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),
|
||||
leaderName: `${def?.name.replace('氏', '') ?? '新宗'}氏宗主`,
|
||||
leaderRealmIdx: MAJOR_ORDER.indexOf(def?.leaderRealm ?? 'qi'),
|
||||
leaderAge: 45,
|
||||
lastEvent: '',
|
||||
lastEventYear: -99,
|
||||
@@ -509,6 +651,7 @@ function pushNews(w: World, s: WorldSimState, about: string[]): void {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user