refactor(0.1.14-P1): 内核归位——game/ 按引擎架构重排(零行为漂移)
结构(旧 game/core+engine → 新 engine/ 域): - engine/kernel/ 时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core) - engine/narrative/ legacy/报告/列传/谱系/年轴(原 core 叙事族) - engine/runtime/ World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统) - engine/sim/ Market(未来 WorldSim 同行) - 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层) 验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化) typecheck 0 error
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
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 { needsTribulation, tribulationEventId } from './tribulation'
|
||||
|
||||
export function monthlyRate(w: World, c: Character): number {
|
||||
const st = w.state
|
||||
let rate = 1
|
||||
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
|
||||
const juling = buildings['juling'] ?? 0
|
||||
rate *= 1 + juling * 0.05
|
||||
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)
|
||||
const dongfu = buildings['dongfu'] ?? 0
|
||||
rate *= 1 + dongfu * 0.08
|
||||
} 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
for (const c of Object.values(w.state.members)) {
|
||||
if (!c.alive || c.state === 'apprentice') continue
|
||||
const rate = monthlyRate(w, c)
|
||||
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}!`)
|
||||
if (next.major === 'spirit') {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user