【主线一:世界自演进深化《时代绘卷》】 - 世纪弧 era 状态机:盛世/平世/乱世/末法四态(ERA_CONF 全参数化)。 灾年概率×0.5~1.6、世界供需×0.7~1.25、拍卖概率×0.6~1.3、tide 偏置 ±0.09; ERA_DURA 控制转换节奏(15~45年),转移时史官快讯+log。世界有大节律了 - NPC 景气仓廪:npcDyn.prosperity(8~100)月结余——买卖+0.5~0.8、断供-2、灾年-3; >75 景气 power+1、<30 衰闷 power-1;换代焕新(55~75)。NPC 在过日子 - 蝴蝶效应:互攻胜率 power 加权(强者恒强)、打残广播(败家邻望-8/胜家+4); 玩家劫掠胜利/结盟直接写 NPC·NPC 关系网(-6/-3)——玩家行动推进世界历史 - 天下十年一鉴:每十年史官综述(灾/换代/行情/灵潮聚合,kind='annal') - 灾年↔秘境:灾年灵脉恢复减半(末法感) - WorldPanel 天下志:era 徽标(悬停描述)/史官十年鉴/群雄势衡柱状图/衰微标注 【主线二:特效系统并入引擎(架构升级)】 - WorldEventBus.onFx + Kernel 转发 + World.emitFx 语义发射(零 rng 消耗) - 12 个语义发射点:突破/化神/渡劫/劫掠胜负/遭遇战/拍卖/大比/灾年起散/换代 —— 取代 UI log 文本正则猜测(spark/blade 双源锁死) - makeBus 桥接 FxGate(kind→gate.emit),gate 挡位/密度/reduced 逻辑保留在 UI 【测试】1002 全绿(40 套件):fxqueue-semantic 3 例 + era-world 5 例; 金钟罩 0.1.18 基线固化(era/prosperity/蝴蝶效应受控变更)
254 lines
10 KiB
TypeScript
254 lines
10 KiB
TypeScript
import type { World } from '../World'
|
||
import { Character } from '../../../types/domain'
|
||
import { ROOT_GRADES } from '../../../data/elements'
|
||
import { masteryRateOfMajor } from '../../../data/pacing'
|
||
import { aspirationById, fitBonusOf } from '../../../data/aspirations'
|
||
import { techniqueById } from '../../../data/techniques'
|
||
import { nextRealm, breakthroughBaseChance, realmDeathChance, describeRealm, MAJOR_ORDER } from '../../../data/realms'
|
||
import { lifespanOf } from './lifecycle'
|
||
import { traitBonuses } from '../pcgen'
|
||
import { newCharacter } from '../pcgen'
|
||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../kernel/names'
|
||
import { ASPIRATION_IDS } from '../../../data/aspirations'
|
||
import { seasonMod } from '../../../data/season'
|
||
import { bonusOf } from '../../../data/buildings'
|
||
import { needsTribulation, tribulationEventId } from './tribulation'
|
||
|
||
export function monthlyRate(w: World, c: Character, herbFactor = 1): number {
|
||
const st = w.state
|
||
let rate = herbFactor
|
||
rate *= 1.0 + c.perception * 0.18
|
||
rate *= ROOT_GRADES[c.roots.grade]?.expBonus ?? 0.5
|
||
const tech = techniqueById(c.techniqueId)
|
||
if (tech && c.realm.major !== 'mortal') {
|
||
rate *= 1 + tech.expBonus + (c.techniqueRank ?? 0) * 0.05
|
||
} else if (c.realm.major !== 'mortal') {
|
||
rate *= 0.65
|
||
}
|
||
const buildings = st.family.buildings
|
||
rate *= 1 + bonusOf('juling', 'expBonus', buildings['juling'] ?? 0)
|
||
if (w.sysEnabled('season')) rate *= 1 + seasonMod(st.month, 'cult')
|
||
rate *= 1 + w.postBonus('expAll')
|
||
if (st.family.flag['fengFeiBless']) rate *= 1.05
|
||
if (c.traits.includes('fengxian')) rate *= 1.3
|
||
const aspiration = aspirationById(c.aspiration)
|
||
if (aspiration?.effect.type === 'cult') rate *= 1 + aspiration.effect.value
|
||
if (fitBonusOf(c).cult) rate *= 1.06
|
||
if (c.state === 'meditation') {
|
||
rate *= 1.35
|
||
rate *= 1 + w.postBonus('meditation') + (w.sysEnabled('season') ? seasonMod(st.month, 'meditation') : 0)
|
||
rate *= 1 + bonusOf('dongfu', 'expBonus', buildings['dongfu'] ?? 0)
|
||
} else if (c.state === 'expedition') {
|
||
rate *= 0.25
|
||
} else if (c.state === 'wounded') {
|
||
rate *= c.health > 40 ? 0.5 : 0.15
|
||
}
|
||
// 带伤不愈者难以静修(与状态无关的直观削率)
|
||
if (c.alive && c.state !== 'wounded') {
|
||
if (c.health <= 40) rate *= 0.45
|
||
else if (c.health <= 70) rate *= 0.8
|
||
}
|
||
rate *= traitBonuses(c).exp
|
||
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
|
||
}
|
||
|
||
export function cultivationTick(w: World): void {
|
||
// 圣者护族:家族有 60+ 且超过筑基者时,幼儿修行门槛更低
|
||
const hasPatriarch = w.aliveMembers().some((c) => w.ageOf(c) >= 60 && c.realm.major !== 'mortal' && c.realm.major !== 'qi')
|
||
// 成年礼定志(16-18 岁首次)
|
||
for (const c of Object.values(w.state.members)) {
|
||
if (c.alive && !c.aspiration && w.ageOf(c) >= 16 && w.ageOf(c) <= 19) {
|
||
c.aspiration = w.rng.pick(ASPIRATION_IDS)
|
||
}
|
||
}
|
||
// ---- A1 修为经济化:灵草是修行燃料 ----
|
||
const inv = w.state.family.inventory
|
||
let demand = 0
|
||
for (const c of Object.values(w.state.members)) {
|
||
if (!c.alive || c.state === 'apprentice') continue
|
||
if (c.realm.major === 'mortal') continue
|
||
demand += c.state === 'meditation' ? 2 : 1
|
||
}
|
||
let herbFactor = 1
|
||
if (demand > 0) {
|
||
const supply = inv.lingcao ?? 0
|
||
const used = Math.min(supply, demand)
|
||
inv.lingcao = supply - used
|
||
const ratio = supply >= demand ? 1 : supply / demand
|
||
herbFactor = 0.6 + 0.4 * ratio
|
||
if (ratio < 0.5 && w.state.family.flag['herbWarnYear'] !== w.state.year) {
|
||
w.state.family.flag['herbWarnYear'] = w.state.year
|
||
w.log('bad', '灵草告罄,族中修行渐缓——需扩灵田药园或购草补续。')
|
||
}
|
||
}
|
||
for (const c of Object.values(w.state.members)) {
|
||
if (!c.alive || c.state === 'apprentice') continue
|
||
const rate = monthlyRate(w, c, herbFactor)
|
||
if (rate <= 0) continue
|
||
c.realmProgress = Math.min(100, c.realmProgress + rate)
|
||
// 悟道进度:有功法且修为之外,另积一分慧根
|
||
if (c.techniqueId && c.realm.major !== 'mortal') {
|
||
const rank = c.techniqueRank ?? 0
|
||
const gain = rate * (rank === 0 ? 0.5 : rank === 1 ? 0.3 : 0.15)
|
||
c.techniqueProgress = Math.min(100, (c.techniqueProgress ?? 0) + gain)
|
||
if (c.techniqueProgress >= 100) {
|
||
if (rank === 0) {
|
||
c.techniqueRank = 1
|
||
c.techniqueProgress = 0
|
||
w.log('good', `✦ ${c.name} 参悟《${techniqueName(c.techniqueId)}》小成,战力精进。`)
|
||
w.chronicle('breakthrough', `${c.name} 参悟《${techniqueName(c.techniqueId)}》小成。`, c.id, false)
|
||
} else if (rank === 1) {
|
||
c.techniqueRank = 2
|
||
c.techniqueProgress = 0
|
||
w.log('good', `✦ ${c.name} 于《${techniqueName(c.techniqueId)}》上再进一层,臻至大成!`)
|
||
w.chronicle('breakthrough', `${c.name} 将《${techniqueName(c.techniqueId)}》精修大成。`, c.id, true)
|
||
}
|
||
}
|
||
}
|
||
if (c.realmProgress >= 100) {
|
||
const months = (w.state.year * 12 + w.state.month) - (c.lastBreakthroughAttempt ?? -999)
|
||
if (months >= 6 && w.rng.chance(perAttemptChance(w, c))) {
|
||
if (needsTribulation(c) && w.sysEnabled('tribulation')) {
|
||
// 已在等待渡劫(pending 未清)或压制期内,不再重设
|
||
if (w.state.pendingEvent === tribulationEventId(c)) {
|
||
c.realmProgress = 100
|
||
continue
|
||
}
|
||
if (c.tribDelayYear && w.state.year < c.tribDelayYear) {
|
||
c.realmProgress = 100
|
||
continue
|
||
}
|
||
c.tribDelayYear = undefined
|
||
w.pendingEvent(tribulationEventId(c))
|
||
w.state.pendingEvent = tribulationEventId(c)
|
||
} else {
|
||
resolveBreakthrough(w, c, 0)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function techniqueName(id: string | undefined): string {
|
||
const t = techniqueById(id)
|
||
return t ? t.name : '无名功法'
|
||
}
|
||
|
||
function closestDisciple(w: World, c: Character): Character | null {
|
||
const heirs = c.children
|
||
.map((id) => w.state.members[id])
|
||
.filter((ch): ch is Character => !!ch?.alive && !!ch.techniqueId)
|
||
.sort((a, b) => rankScore(b) - rankScore(a))
|
||
if (heirs.length > 0) return heirs[0]
|
||
return null
|
||
}
|
||
|
||
function rankScore(c: Character): number {
|
||
const order = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit']
|
||
return order.indexOf(c.realm.major) * 10 + c.realm.minor
|
||
}
|
||
|
||
export function perAttemptChance(w: World, c: Character): number {
|
||
const base = breakthroughBaseChance(c.realm)
|
||
const mind = c.mind * 0.008
|
||
const traits = traitBonuses(c)
|
||
const headMind = (w.state.members[w.state.family.headId]?.mind ?? 5) * 0.004
|
||
const healthMod = c.health > 60 ? 0.04 : -0.06
|
||
const bless = w.state.family.flag['headBless'] ? 0.03 : 0
|
||
return Math.min(0.95, Math.max(0.02, base + mind + traits.breakBonus + headMind + healthMod + bless))
|
||
}
|
||
|
||
export function resolveBreakthrough(w: World, c: Character, boost: number): void {
|
||
if (!c.alive || c.realmProgress < 100) return
|
||
const next = nextRealm(c.realm)
|
||
if (!next) return
|
||
const p = Math.min(0.95, Math.max(0.05, perAttemptChance(w, c) + boost))
|
||
c.lastBreakthroughAttempt = w.state.year * 12 + w.state.month
|
||
|
||
if (w.rng.chance(p)) {
|
||
const majorJump = next.major !== c.realm.major
|
||
c.realm = next
|
||
c.realmProgress = 0
|
||
if (majorJump) {
|
||
c.health = 100
|
||
// 悟道传承:大境界圆满者,将心得传于同枝晚辈
|
||
const apprentice = closestDisciple(w, c)
|
||
if (apprentice) {
|
||
const cur = apprentice.techniqueProgress ?? 0
|
||
if (cur < 60) {
|
||
apprentice.techniqueProgress = Math.max(cur, 60)
|
||
w.log('info', `${apprentice.name} 承得 ${c.name} 破境感悟,修行一日千里。`)
|
||
}
|
||
}
|
||
}
|
||
const desc = describeRealm(next)
|
||
w.chronicle('breakthrough', `${c.name} 突破至【${desc}】。`, c.id, majorJump)
|
||
w.log('good', `✦ ${c.name} 突破到 ${desc}!`)
|
||
w.emitFx('spark', `break:${c.id}`)
|
||
if (next.major === 'spirit') {
|
||
w.emitFx('pulse', `spirit:${c.id}`)
|
||
w.chronicle('breakthrough', `华夏震惊:${c.name} 踏入化神之列。`, c.id, true)
|
||
if (!w.state.flags['firstSpirit']) {
|
||
w.state.flags['firstSpirit'] = w.state.year
|
||
w.log('bad', `天地异象:族中第一位化神出世,仙路大门洞开。`)
|
||
}
|
||
}
|
||
} else {
|
||
c.health = Math.max(1, c.health - 8 - w.rng.int(0, 10))
|
||
let log = `${c.name} 冲击瓶颈失败,灵力紊乱受创。`
|
||
if (c.traits.includes('jizao') || c.traits.includes('yiqi')) {
|
||
c.health = Math.max(1, c.health - 14)
|
||
log = `${c.name} 强行突破遭反噬,气息受创。`
|
||
}
|
||
const majorIdx = MAJOR_ORDER.indexOf(c.realm.major)
|
||
if (majorIdx >= 3 && w.rng.chance(realmDeathChance(c.realm, c.mind))) {
|
||
c.alive = false
|
||
c.deathYear = w.state.year
|
||
c.deathCause = '突破走火'
|
||
w.chronicle('death', `${c.name} 妄图冲击瓶颈,走火入魔而陨。`, c.id, true)
|
||
w.log('bad', `☠ ${c.name} 突破走火,当场陨落。`)
|
||
return
|
||
}
|
||
if (c.health < 20) c.state = 'wounded'
|
||
const loss = 26 + w.rng.int(0, 18)
|
||
c.realmProgress = Math.max(0, Math.min(95, 100 - loss - boost * 60))
|
||
w.log('bad', log)
|
||
}
|
||
}
|
||
|
||
export function produceOffspring(
|
||
w: World,
|
||
opts: {
|
||
father: Character | null
|
||
mother: Character | null
|
||
generation: number
|
||
bornYear: number
|
||
surname: string
|
||
spouseHouse?: string
|
||
}
|
||
): Character {
|
||
const rng = w.rng
|
||
const newborn = newCharacter(rng, {
|
||
name: `${opts.surname}${rng.pick(rng.chance(0.52) ? MALE_GIVEN : FEMALE_GIVEN)}`,
|
||
gender: rng.chance(0.52) ? 'male' : 'female',
|
||
generation: opts.generation,
|
||
bornYear: opts.bornYear,
|
||
age: 0,
|
||
realm: { major: 'mortal', minor: 0 },
|
||
father: opts.father ?? undefined,
|
||
mother: opts.mother ?? undefined
|
||
})
|
||
if (opts.spouseHouse) newborn.spouseHouse = opts.spouseHouse
|
||
return newborn
|
||
}
|
||
|
||
export function lifespanCheckPoint(w: World, c: Character): number {
|
||
return lifespanOf(w, c)
|
||
}
|