0.1.37 天人感应·因果织锦:功德系统+飞升试炼+世界奇观+MOD扩展口
This commit is contained in:
@@ -45,6 +45,8 @@ export interface PluginContext {
|
||||
/** 世界生成词库池注入(styles/regions/fams/suffixes) */
|
||||
addWorldGenPool: (part: { styles?: string[]; regions?: string[]; fams?: string[]; suffixes?: string[] }, source?: string) => void
|
||||
addCalamity: (name: string, opts: { brief?: string; family?: Partial<Record<string, number>>; effect?: Partial<Record<string, number>> }) => boolean
|
||||
/** 0.1.37 奇观类型扩展(MOD 可注册自定义世界奇观——零 rng 消耗,种子派生选取) */
|
||||
addWonderType: (def: { kind: string; name?: string; desc?: string; duration?: number; effect?: 'tide_up' | 'tide_down' | 'none'; oneshot?: 'bless' | 'loot' | 'none' }) => boolean
|
||||
/** 0.1.31 内容面口 */
|
||||
packExtend: (key: 'techniques', additions: Record<string, unknown>) => void
|
||||
mergeAddPack: (key: 'events' | 'techniques', items: unknown[]) => void
|
||||
|
||||
@@ -56,6 +56,40 @@ export function buildBiography(s: GameState, memberId: string): BioLine[] {
|
||||
lines.push({ at: t.year, label: '大比', text: `${t.year}年,随队参加太虚大比,列第${t.rank}名。` })
|
||||
}
|
||||
|
||||
// 0.1.37 飞升试炼传记
|
||||
if (c.deathCause === '飞升太虚') {
|
||||
const ascensionChronicles = s.chronicle.filter((e) =>
|
||||
e.category === 'breakthrough' && e.memberId === c.id &&
|
||||
(e.text.includes('心魔') || e.text.includes('天劫') || e.text.includes('飞升'))
|
||||
)
|
||||
for (const e of ascensionChronicles) {
|
||||
lines.push({ at: e.year, label: '飞升', text: `${e.year}年${e.month}月,${e.text.replace(new RegExp(`^${c.name}\\s*`), '')}` })
|
||||
}
|
||||
lines.push({
|
||||
at: c.deathYear,
|
||||
label: '飞升',
|
||||
text: `${c.name} 历经心魔、天劫、飞升三重试炼,终成正果——飞升太虚而去。此去仙凡永隔,然道韵余泽笼罩宗族,万世蒙荫。`
|
||||
})
|
||||
} else if (c.deathCause === '飞升劫陨') {
|
||||
lines.push({
|
||||
at: c.deathYear,
|
||||
label: '飞升',
|
||||
text: `${c.name} 于飞升天劫中形神俱灭——仙路终成绝响。壮志未酬,英魂归天。`
|
||||
})
|
||||
} else if (c.traits.includes('fengxian') && c.alive) {
|
||||
// 留仙坐镇者
|
||||
const stayEvent = s.chronicle.find((e) =>
|
||||
e.category === 'event' && e.memberId === c.id && e.text.includes('留世坐镇')
|
||||
)
|
||||
if (stayEvent) {
|
||||
lines.push({
|
||||
at: stayEvent.year,
|
||||
label: '飞升',
|
||||
text: `${stayEvent.year}年,${c.name} 谢绝仙门征召,留世镇守宗族——仙风入体,护佑万世。`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 墓碑铭文
|
||||
if (!c.alive) {
|
||||
const epitaph = EPITAPH[c.deathCause ?? '寿终'] ?? '一尘一土,终归青山。'
|
||||
@@ -72,6 +106,8 @@ const EPITAPH: Record<string, string> = {
|
||||
'伤势不治': '壮士折戟,魂兮归来。家祠之下,与祖同列。',
|
||||
'突破走火': '一念求道,九死未悔。灵前法灯,为君长明。',
|
||||
'渡劫陨落': '雷霆战天,其志未竟。遗骨归山,英风尚在。',
|
||||
'飞升太虚': '三劫渡尽化虹去,道韵长存荫后人。仙凡虽隔,香火永继。',
|
||||
'飞升劫陨': '壮志凌云欲破天,一着未成殒飞仙。英魂不灭,长照宗门。',
|
||||
'战殁': '马革裹尸,归葬桑梓。长剑挂壁,子孙相传。',
|
||||
'云游飞升': '此去云深不知处,魂同列宿游太虚。'
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface LegacyDims {
|
||||
daoXing: number
|
||||
weiMing: number
|
||||
xiangHuo: number
|
||||
/** 0.1.37 因果维度:家族 karma 值的定鼎映射 */
|
||||
yinGuo: number
|
||||
total: number
|
||||
}
|
||||
|
||||
@@ -22,6 +24,11 @@ export const VERDICTS: Record<string, { title: string; poem: string[] }> = {
|
||||
title: '飞升之资',
|
||||
poem: ['海内升龙真气象,仙班亦录旧香名。', '遥看青山埋骨处,一脉烟霞送飞鸿。']
|
||||
},
|
||||
// 0.1.37 飞升 + 高 karma 的至高档位
|
||||
daofruit: {
|
||||
title: '道果圆满',
|
||||
poem: ['三劫渡尽化虹去,功德如海润苍生。', '后人仰首望星处,犹见仙班旧时名。']
|
||||
},
|
||||
celestial: {
|
||||
title: '仙朝遗脉',
|
||||
poem: ['祖庭灯火三千里,曾照中天玉斗垂。', '后世儿孙开卷处,犹闻钟鼎旧清音。']
|
||||
@@ -65,14 +72,22 @@ export function computeLegacy(s: GameState): LegacyDims {
|
||||
const daoXing = Math.round(st.maxRealmIdx * 26 + st.techniqueGrand * 4)
|
||||
const weiMing = Math.round(st.repPeak * 1.4 + (st.tourneyBest ? (6 - st.tourneyBest) * 12 : 0))
|
||||
const xiangHuo = Math.round(Math.min(s.year, 300) * 0.9 + st.feishengCount * 60)
|
||||
return { renXing, daoXing, weiMing, xiangHuo, total: renXing + daoXing + weiMing + xiangHuo }
|
||||
// 0.1.37 因果维度:karma 映射到 0~40 分(正 karma 加分,负 karma 扣分)
|
||||
// karma=50 → +20;karma=-30 → -12;让善恶影响最终评价
|
||||
const karma = s.family.karma ?? 0
|
||||
const yinGuo = Math.round(Math.max(-20, Math.min(30, karma * 0.4)))
|
||||
return { renXing, daoXing, weiMing, xiangHuo, yinGuo, total: renXing + daoXing + weiMing + xiangHuo + yinGuo }
|
||||
}
|
||||
|
||||
export function resolveLegacy(s: GameState): LegacyArch {
|
||||
const dims = computeLegacy(s)
|
||||
let key = 'dust'
|
||||
if (s.stats.feishengCount > 0) key = 'feisheng'
|
||||
else {
|
||||
// 0.1.37 飞升 + 高 karma → 道果圆满(至高档位)
|
||||
if (s.stats.feishengCount > 0 && (s.family.karma ?? 0) > 30) {
|
||||
key = 'daofruit'
|
||||
} else if (s.stats.feishengCount > 0) {
|
||||
key = 'feisheng'
|
||||
} else {
|
||||
for (const [need, k] of RANK_MAP) {
|
||||
if (dims.total >= need) {
|
||||
key = k
|
||||
|
||||
@@ -14,9 +14,12 @@ import { ASPIRATION_IDS } from '../../../data/aspirations'
|
||||
import { seasonMod } from '../../../data/season'
|
||||
import { bonusOf } from '../../../data/buildings'
|
||||
import { WorldSim } from '../../sim/WorldSim'
|
||||
import { needsTribulation, tribulationEventId } from './tribulation'
|
||||
import { needsTribulation, tribulationEventId, ascensionEventId } from './tribulation'
|
||||
import { fire } from './events'
|
||||
|
||||
/** 化神最高 minor(化神3层 → minor=2 为圆满) */
|
||||
const MAJORS_SPIRIT_MAX_MINOR = 2
|
||||
|
||||
export function monthlyRate(w: World, c: Character, herbFactor = 1): number {
|
||||
const st = w.state
|
||||
let rate = herbFactor
|
||||
@@ -134,6 +137,27 @@ export function cultivationTick(w: World): void {
|
||||
}
|
||||
}
|
||||
if (c.realmProgress >= 100) {
|
||||
// 0.1.37 飞升试炼:化神圆满(major=spirit, minor=2, progress=100)时 fire 心魔试炼
|
||||
if (c.realm.major === 'spirit' && c.realm.minor >= MAJORS_SPIRIT_MAX_MINOR && w.sysEnabled('tribulation')) {
|
||||
// 已在飞升试炼流程中(pending 或 delay 内)不重复触发
|
||||
const ascPending = w.state.pendingEvent?.startsWith('ev-ascension-') === true
|
||||
if (ascPending) {
|
||||
c.realmProgress = 100
|
||||
continue
|
||||
}
|
||||
if (c.tribDelayYear && w.state.year < c.tribDelayYear) {
|
||||
c.realmProgress = 100
|
||||
continue
|
||||
}
|
||||
// 旧版 ev-feisheng 已完成则不再触发
|
||||
if (w.state.completedEvents.includes('ev-feisheng')) {
|
||||
c.realmProgress = 100
|
||||
continue
|
||||
}
|
||||
c.tribDelayYear = undefined
|
||||
fire(w, ascensionEventId(c.id, 'heart'), 1)
|
||||
continue
|
||||
}
|
||||
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')) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { resolveRaid } from './combat'
|
||||
import { sendMission } from './missions'
|
||||
import { pack } from '../../../data/registry'
|
||||
import { findInheritor } from '../creation'
|
||||
import { resolveTribulation } from './tribulation'
|
||||
import { resolveTribulation, resolveAscension } from './tribulation'
|
||||
import { nextRealm, describeRealm } from '../../../data/realms'
|
||||
|
||||
const ALL_EVENTS: EventDef[] = [...EVENTS]
|
||||
@@ -52,8 +52,8 @@ export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
|
||||
once: true,
|
||||
text: '百年元辰已至:这百年间,苗裔繁衍、香火未断。大宴宾客?还是告天祭祖?',
|
||||
options: [
|
||||
{ label: '大开宴席,张灯结彩', hint: '灵石-200,声望+15,全族心境大悦', eff: { res: { stones: -200 }, rep: 15, memberBy: { by: 'inspire', target: 'all', n: 4 } } },
|
||||
{ label: '设坛告天', hint: '灵石-100,声望+10', eff: { res: { stones: -100 }, rep: 10, flag: { centennial: 'rite' } } },
|
||||
{ label: '大开宴席,张灯结彩', hint: '灵石-200,声望+15,全族心境大悦', eff: { res: { stones: -200 }, rep: 15, karma: 3, memberBy: { by: 'inspire', target: 'all', n: 4 } } },
|
||||
{ label: '设坛告天', hint: '灵石-100,声望+10,功德+6', eff: { res: { stones: -100 }, rep: 10, karma: 6, flag: { centennial: 'rite' } } },
|
||||
{ label: '阖家简庆', hint: '声望+5', eff: { rep: 5 } }
|
||||
]
|
||||
}
|
||||
@@ -200,6 +200,62 @@ export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
|
||||
]
|
||||
}
|
||||
}
|
||||
// ---- 0.1.37 飞升试炼三阶段(心魔→天劫→飞升) ----
|
||||
if (id.startsWith('ev-ascension-heart-')) {
|
||||
const memberId = id.replace('ev-ascension-heart-', '')
|
||||
const member = w?.state.members[memberId]
|
||||
if (!member || !member.alive) return undefined
|
||||
const karma = w?.state.family.karma ?? 0
|
||||
const karmaHint = karma > 30 ? '功德深厚,心魔难侵' : karma < -20 ? '因果亏缺,心魔猖獗' : '功德平平,全凭心境'
|
||||
return {
|
||||
id,
|
||||
name: '心魔试炼',
|
||||
category: 'fate',
|
||||
weight: 0,
|
||||
once: true,
|
||||
text: `${member.name} 化神圆满,修为已至化境。然飞升之前,先需直面心魔——前世恩怨、今生执念化作幻象,如潮涌来。${karmaHint}。族中当如何助其破障?`,
|
||||
options: [
|
||||
{ label: '以心境抗衡', hint: '成败由心,心境高则破', eff: { ascension: { memberId, stage: 'heart', mode: 'rash' } } },
|
||||
{ label: '以功德化解(灵石200)', hint: '消耗灵石,功德护心', eff: { ascension: { memberId, stage: 'heart', mode: 'guard' } } },
|
||||
{ label: '压制一年再试', hint: '养心蓄锐,来年再渡', eff: { ascension: { memberId, stage: 'heart', mode: 'delay' } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
if (id.startsWith('ev-ascension-heaven-')) {
|
||||
const memberId = id.replace('ev-ascension-heaven-', '')
|
||||
const member = w?.state.members[memberId]
|
||||
if (!member || !member.alive) return undefined
|
||||
return {
|
||||
id,
|
||||
name: '飞升天劫',
|
||||
category: 'fate',
|
||||
weight: 0,
|
||||
once: true,
|
||||
text: `${member.name} 破开心魔障壁,灵台清明。然飞升之劫已至——九天雷池翻涌,紫电如海。此劫远非渡劫可比,一着不慎即形神俱灭。族中当如何?`,
|
||||
options: [
|
||||
{ label: '硬渡天劫!', hint: '生死由天,一往无前', eff: { ascension: { memberId, stage: 'heaven', mode: 'rash' } } },
|
||||
{ label: '请族中护法(灵石300)', hint: '族人护持,成算大增', eff: { ascension: { memberId, stage: 'heaven', mode: 'guard' } } },
|
||||
{ label: '压制一年再渡', hint: '蓄势待发,丹力渐散', eff: { ascension: { memberId, stage: 'heaven', mode: 'delay' } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
if (id.startsWith('ev-ascension-ascend-')) {
|
||||
const memberId = id.replace('ev-ascension-ascend-', '')
|
||||
const member = w?.state.members[memberId]
|
||||
if (!member || !member.alive) return undefined
|
||||
return {
|
||||
id,
|
||||
name: '飞升抉择',
|
||||
category: 'fate',
|
||||
weight: 0,
|
||||
once: true,
|
||||
text: `${member.name} 历经心魔天劫,灵台澄明如镜,仙门已开。此刻可选择飞升而去、云游太虚,亦可留世坐镇、护佑宗族万世。此去一别,或成永诀——宗族当如何抉择?`,
|
||||
options: [
|
||||
{ label: '留仙坐镇', hint: '他留世镇族:仙风入体,天赋大盛', eff: { ascension: { memberId, stage: 'ascend', mode: 'rash' } } },
|
||||
{ label: '放仙飞升', hint: '他飞升太虚:宗族永蒙仙荫,声望大涨', eff: { ascension: { memberId, stage: 'ascend', mode: 'guard' } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
if (id === 'ev-feisheng') {
|
||||
return {
|
||||
id,
|
||||
@@ -314,10 +370,17 @@ export function eventRoll(w: World): void {
|
||||
fire(w, 'ev-centennial')
|
||||
return
|
||||
}
|
||||
// 0.1.37 飞升试炼:化神圆满由 cultivation.ts 直接 fire 心魔试炼
|
||||
// 旧版 ev-feisheng 兼容路径:firstSpirit + 2 年 且 没有化神圆满者 → 旧版简化飞升
|
||||
const firstSpirit = s.flags['firstSpirit'] as number | undefined
|
||||
if (firstSpirit && s.year - firstSpirit >= 2 && !s.completedEvents.includes('ev-feisheng')) {
|
||||
fire(w, 'ev-feisheng')
|
||||
return
|
||||
// 新版飞升试炼由 cultivation.ts 在化神修为满 100% 时触发
|
||||
// 检查是否有化神圆满者(如果有,说明新系统已接管或即将接管)
|
||||
const hasSpiritMaxed = w.aliveMembers().some((c) => c.realm.major === 'spirit' && c.realm.minor >= 2 && c.realmProgress >= 100)
|
||||
if (!hasSpiritMaxed) {
|
||||
fire(w, 'ev-feisheng')
|
||||
return
|
||||
}
|
||||
}
|
||||
checkApprenticeExpiry(w)
|
||||
recruitCheck(w)
|
||||
@@ -526,6 +589,12 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[], _extra?:
|
||||
if (eff.pillGain) {
|
||||
for (const [k, v] of Object.entries(eff.pillGain)) fam.inventory[k] = (fam.inventory[k] ?? 0) + v
|
||||
}
|
||||
if (eff.karma) {
|
||||
fam.karma += eff.karma
|
||||
if (Math.abs(eff.karma) >= 3) {
|
||||
w.log(eff.karma > 0 ? 'good' : 'bad', `功德${eff.karma > 0 ? '增加' : '损耗'}${Math.abs(eff.karma)}点。`)
|
||||
}
|
||||
}
|
||||
if (eff.rep) {
|
||||
fam.reputation += eff.rep
|
||||
if (Math.abs(eff.rep) >= 4) w.log(eff.rep > 0 ? 'good' : 'bad', `家族声望${eff.rep > 0 ? '上升' : '下跌'}${Math.abs(eff.rep)}。`)
|
||||
@@ -639,6 +708,15 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[], _extra?:
|
||||
}
|
||||
}
|
||||
}
|
||||
// 0.1.37 飞升试炼三阶段
|
||||
if (eff.ascension) {
|
||||
const c = w.state.members[eff.ascension.memberId]
|
||||
if (c?.alive) {
|
||||
c.tribPendingMonths = 0
|
||||
resolveAscension(w, c, eff.ascension.stage, eff.ascension.mode ?? 'rash')
|
||||
}
|
||||
return
|
||||
}
|
||||
if (eff.memberBy) {
|
||||
const targets = pickMember(w, eff.memberBy)
|
||||
const by = eff.memberBy.by
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { World } from '../World'
|
||||
import { Character } from '../../../types/domain'
|
||||
import { nextRealm, MAJOR_ORDER, realmDeathChance, describeRealm } from '../../../data/realms'
|
||||
import { techniqueById } from '../../../data/techniques'
|
||||
import { fire } from './events'
|
||||
import { findInheritor } from '../creation'
|
||||
|
||||
export const TRIB_MAJORS: string[] = ['foundation', 'core', 'nascent', 'spirit']
|
||||
|
||||
@@ -102,5 +104,197 @@ export function perTribChance(w: World, c: Character): number {
|
||||
spirit: 0.26
|
||||
} as Record<string, number>)[c.realm.major] ?? 0.9
|
||||
const tb = techniqueById(c.techniqueId)?.tribBonus ?? 0
|
||||
return Math.min(0.92, base + c.mind * 0.01 + c.health / 400 + (c.tribBoost ?? 0) + tb)
|
||||
// 0.1.37 因果调制:功德深厚者天劫顺遂,因果亏缺者天道加码
|
||||
// karma > 30 → +5% 渡劫成功率(积善有余庆,天道暗佑)
|
||||
// karma < -20 → -5% 渡劫成功率(因果缠身,劫罚加重)
|
||||
const karma = w.state.family.karma ?? 0
|
||||
const karmaTribMod = karma > 30 ? 0.05 : karma < -20 ? -0.05 : 0
|
||||
return Math.min(0.92, base + c.mind * 0.01 + c.health / 400 + (c.tribBoost ?? 0) + tb + karmaTribMod)
|
||||
}
|
||||
|
||||
// ==================== 0.1.37 飞升试炼 ====================
|
||||
|
||||
/** 飞升试炼事件 ID 生成 */
|
||||
export function ascensionEventId(memberId: string, stage: 'heart' | 'heaven' | 'ascend'): string {
|
||||
return `ev-ascension-${stage}-${memberId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞升试炼三阶段结算(0.1.37 天人感应·因果织锦)
|
||||
*
|
||||
* 阶段一 · 心魔试炼:心境 + 功德 vs 心魔强度
|
||||
* - rash:心境硬抗(base 40% + mind×2% + karma 调制)
|
||||
* - guard:消耗灵石200,功德护心(base 55% + mind×2% + karma 调制 +10%)
|
||||
* - delay:压制一年再试
|
||||
* 通过 → fire 天劫试炼
|
||||
*
|
||||
* 阶段二 · 天劫试炼:飞升级天劫(远难于渡劫)
|
||||
* - rash:硬渡(base 20% + mind×1.5% + health/500 + karma 调制)
|
||||
* - guard:消耗灵石300,护法加持(base 35% + mind×1.5% + health/500 + karma 调制 +15%)
|
||||
* - delay:压制一年再试
|
||||
* 通过 → fire 飞升抉择
|
||||
* 失败可能陨落(danger = 0.08)
|
||||
*
|
||||
* 阶段三 · 飞升抉择:留仙 / 飞升
|
||||
* - rash(留仙):获得 fengxian 特质,修为 ×1.3,声望+15
|
||||
* - guard(飞升):飞升者离族,feishengCount+1,声望+30,karma+10,全族修为提振
|
||||
*/
|
||||
export function resolveAscension(
|
||||
w: World,
|
||||
c: Character,
|
||||
stage: 'heart' | 'heaven' | 'ascend',
|
||||
mode: 'rash' | 'guard' | 'delay'
|
||||
): string {
|
||||
const s = w.state
|
||||
|
||||
// ---- 阶段一:心魔试炼 ----
|
||||
if (stage === 'heart') {
|
||||
if (mode === 'delay') {
|
||||
c.tribDelayYear = w.state.year + 1
|
||||
w.log('info', `${c.name} 收束心神,暂压心魔,待来年再试。`)
|
||||
return 'delayed'
|
||||
}
|
||||
|
||||
const karma = s.family.karma ?? 0
|
||||
const karmaMod = karma > 30 ? 0.12 : karma < -20 ? -0.12 : 0
|
||||
|
||||
let chance: number
|
||||
if (mode === 'guard') {
|
||||
if (s.family.stones < 200) {
|
||||
w.log('bad', '功德法事之资不足,只得强渡心魔。')
|
||||
chance = 0.40 + c.mind * 0.02 + karmaMod
|
||||
} else {
|
||||
s.family.stones -= 200
|
||||
chance = 0.55 + c.mind * 0.02 + karmaMod + 0.10
|
||||
w.log('info', `设坛焚香,以功德化解心魔——${c.name} 心境渐稳。`)
|
||||
}
|
||||
} else {
|
||||
chance = 0.40 + c.mind * 0.02 + karmaMod
|
||||
}
|
||||
|
||||
const p = Math.min(0.88, Math.max(0.05, chance))
|
||||
if (w.rng.chance(p)) {
|
||||
w.emitFx('spark', `heart:${c.id}`)
|
||||
w.chronicle('breakthrough', `${c.name} 破开心魔障壁,灵台清明如镜。`, c.id, true)
|
||||
w.log('good', `✦ ${c.name} 斩碎心魔,灵台澄明——飞升之门渐开。`)
|
||||
// 心魔通过 → fire 天劫试炼
|
||||
fire(w, ascensionEventId(c.id, 'heaven'), 1)
|
||||
return 'success'
|
||||
} else {
|
||||
c.health = Math.max(1, c.health - 15 - w.rng.int(0, 10))
|
||||
if (c.health < 25) c.state = 'wounded'
|
||||
c.realmProgress = Math.max(0, 90 - w.rng.int(0, 15))
|
||||
w.log('bad', `${c.name} 心魔反噬,神识受创——修为跌落。`)
|
||||
w.chronicle('event', `${c.name} 心魔试炼受挫,神识震荡。`, c.id, false)
|
||||
return 'fail'
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 阶段二:天劫试炼 ----
|
||||
if (stage === 'heaven') {
|
||||
if (mode === 'delay') {
|
||||
c.tribDelayYear = w.state.year + 1
|
||||
c.tribBoost = 0
|
||||
w.log('info', `${c.name} 引气蓄势,按兵不动——来年再渡飞升之劫。`)
|
||||
return 'delayed'
|
||||
}
|
||||
|
||||
const karma = s.family.karma ?? 0
|
||||
const karmaMod = karma > 30 ? 0.06 : karma < -20 ? -0.06 : 0
|
||||
|
||||
let chance: number
|
||||
let guardian: Character | null = null
|
||||
if (mode === 'guard') {
|
||||
if (s.family.stones < 300) {
|
||||
w.log('bad', '护法之资不足,只得独渡天劫。')
|
||||
chance = 0.20 + c.mind * 0.015 + c.health / 500 + karmaMod
|
||||
} else {
|
||||
s.family.stones -= 300
|
||||
chance = 0.35 + c.mind * 0.015 + c.health / 500 + karmaMod + 0.15
|
||||
guardian = w
|
||||
.aliveMembers()
|
||||
.filter((m) => m.id !== c.id && MAJOR_ORDER.indexOf(m.realm.major) >= MAJOR_ORDER.indexOf('spirit'))
|
||||
.sort((a, b) => MAJOR_ORDER.indexOf(b.realm.major) - MAJOR_ORDER.indexOf(a.realm.major))[0] ?? null
|
||||
if (guardian) {
|
||||
w.log('info', `${guardian.name} 护法加持,天劫成算大增。`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chance = 0.20 + c.mind * 0.015 + c.health / 500 + karmaMod
|
||||
}
|
||||
|
||||
const p = Math.min(0.80, Math.max(0.03, chance))
|
||||
if (w.rng.chance(p)) {
|
||||
w.emitFx('pulse', `heaven:${c.id}`)
|
||||
c.health = Math.min(100, c.health + 20)
|
||||
w.chronicle('breakthrough', `${c.name} 飞升天劫功成,九天雷池尽散——仙路已通!`, c.id, true)
|
||||
w.log('good', `✦ 紫电消散,${c.name} 渡过飞升天劫——仙门洞开!`)
|
||||
// 天劫通过 → fire 飞升抉择
|
||||
fire(w, ascensionEventId(c.id, 'ascend'), 1)
|
||||
return 'success'
|
||||
} else {
|
||||
c.health = Math.max(1, c.health - 30 - w.rng.int(0, 20))
|
||||
if (c.health < 20) c.state = 'wounded'
|
||||
const danger = 0.08
|
||||
if (w.rng.chance(danger)) {
|
||||
c.alive = false
|
||||
c.deathYear = s.year
|
||||
c.deathCause = '飞升劫陨'
|
||||
w.chronicle('death', `${c.name} 飞升天劫加身,形神俱焚而陨——仙路终成绝响。`, c.id, true)
|
||||
w.log('bad', `☠ ${c.name} 飞升天劫陨落,形神俱灭。`)
|
||||
c.tribBoost = 0
|
||||
return 'dead'
|
||||
}
|
||||
c.realmProgress = Math.max(0, 70 - w.rng.int(0, 20))
|
||||
w.log('bad', `${c.name} 飞升天劫失败,灵力逆行——修为大跌。`)
|
||||
w.chronicle('event', `${c.name} 飞升天劫受挫,灵力逆行。`, c.id, false)
|
||||
if (guardian) {
|
||||
const g = guardian
|
||||
if (w.rng.chance(0.4)) {
|
||||
g.health = Math.max(1, g.health - 20 - w.rng.int(0, 15))
|
||||
if (g.health < 25) g.state = 'wounded'
|
||||
w.log('bad', `${g.name} 护法受牵连,灵力受损。`)
|
||||
}
|
||||
}
|
||||
c.tribBoost = 0
|
||||
return 'fail'
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 阶段三:飞升抉择 ----
|
||||
if (stage === 'ascend') {
|
||||
if (mode === 'rash') {
|
||||
// 留仙坐镇
|
||||
if (!c.traits.includes('fengxian')) c.traits.push('fengxian')
|
||||
s.family.reputation += 15
|
||||
s.family.karma += 5
|
||||
w.chronicle('event', `${c.name} 谢绝仙门征召,愿镇守此世,护佑宗族万世——仙风入体,道行精进。`, c.id, true)
|
||||
w.log('good', `${c.name} 留世坐镇(仙风入体),声望大涨。`)
|
||||
w.emitFx('ripple', `stay:${c.id}`)
|
||||
return 'stay'
|
||||
} else {
|
||||
// 放仙飞升
|
||||
c.alive = false
|
||||
c.deathYear = s.year
|
||||
c.deathCause = '飞升太虚'
|
||||
s.stats.feishengCount = (s.stats.feishengCount ?? 0) + 1
|
||||
s.family.flag['fengFeiBless'] = true
|
||||
s.family.reputation += 30
|
||||
s.family.karma += 10
|
||||
// 全族修为提振(飞升者的道韵余泽)
|
||||
for (const m of w.aliveMembers()) {
|
||||
m.realmProgress = Math.min(100, m.realmProgress + 8)
|
||||
}
|
||||
w.chronicle('event', `${c.name} 历经三劫,终成正果——飞升太虚而去。仙韵余泽笼罩宗族,万世蒙荫。`, c.id, true)
|
||||
w.log('good', `✦ ${c.name} 飞升太虚!宗族永蒙仙荫,声望大涨,全族修为提振。`)
|
||||
w.emitFx('pulse', `ascend:${c.id}`)
|
||||
if (s.family.headId === c.id) {
|
||||
const heir = findInheritor(w)
|
||||
if (heir) w.assignHead(heir.id, true)
|
||||
}
|
||||
return 'ascended'
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { registerFormationDef } from '../../data/formations'
|
||||
import { registerTraitDef } from '../../data/traits'
|
||||
import { addPillRecipe, addForgeRecipe } from '../../data/items'
|
||||
import { WorldGenPools } from '../sim/worldgen'
|
||||
import { addCalamity as addCalamityImpl } from '../sim/worldsim-data'
|
||||
import { addCalamity as addCalamityImpl, addWonderType as addWonderTypeImpl } from '../sim/worldsim-data'
|
||||
import { fire } from './Systems/events'
|
||||
import { createWorldState, findInheritor } from './creation'
|
||||
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||
@@ -96,6 +96,8 @@ export function normalizeGameState(state: GameState): GameState {
|
||||
if (typeof c.techniqueProgress !== 'number') c.techniqueProgress = 0
|
||||
if (typeof c.health !== 'number') c.health = 100
|
||||
}
|
||||
// 0.1.37 karma 兜底(旧档/测试注入缺失时归零)
|
||||
if (typeof state.family.karma !== 'number') state.family.karma = 0
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -166,6 +168,7 @@ export class World {
|
||||
addForgeRecipe: (r) => addForgeRecipe(r),
|
||||
addWorldGenPool: (part, source) => WorldGenPools.add(part, source ?? 'ctx'),
|
||||
addCalamity: (name, opts) => addCalamityImpl(name, opts),
|
||||
addWonderType: (def) => addWonderTypeImpl(def),
|
||||
packExtend: (key, additions) => PACK.extend(key as never, additions),
|
||||
mergeAddPack: (key, items) => PACK.mergeAdd(key as never, items),
|
||||
addBuildingDef: (def) => registerBuildingDef(def),
|
||||
@@ -549,7 +552,8 @@ export class World {
|
||||
}
|
||||
fam.stones -= cost
|
||||
target.relation = Math.min(100, target.relation + 8)
|
||||
this.log('info', `开仓济世(灵石${cost}),${target.name} 感念恩义,关系+8。`)
|
||||
fam.karma += 8
|
||||
this.log('info', `开仓济世(灵石${cost}),${target.name} 感念恩义,关系+8。功德+8。`)
|
||||
void ws
|
||||
return true
|
||||
}
|
||||
@@ -566,8 +570,9 @@ export class World {
|
||||
npc.power = Math.min(900, npc.power + 15)
|
||||
npc.relation = Math.min(100, npc.relation + 8)
|
||||
fam.reputation += 3
|
||||
fam.karma += 5
|
||||
ws.distress = undefined
|
||||
this.log('good', `驰援${npc.name}——敌锋既退,盟谊愈坚(声望+3)。`)
|
||||
this.log('good', `驰援${npc.name}——敌锋既退,盟谊愈坚(声望+3,功德+5)。`)
|
||||
this.chronicle('diplomacy', `助${npc.name}重整武备,同盟益固。`, undefined, true)
|
||||
return true
|
||||
}
|
||||
@@ -584,7 +589,8 @@ export class World {
|
||||
if (rel < -30) dyns[a].relationsWithOthers[b] = -25
|
||||
}
|
||||
fam.reputation += 2
|
||||
this.log('info', `调停${this.state.npcFamilies[npcA]?.name ?? npcA}与${this.state.npcFamilies[npcB]?.name ?? npcB}——干戈化玉帛,声望+2。`)
|
||||
fam.karma += 3
|
||||
this.log('info', `调停${this.state.npcFamilies[npcA]?.name ?? npcA}与${this.state.npcFamilies[npcB]?.name ?? npcB}——干戈化玉帛,声望+2,功德+3。`)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -948,7 +954,8 @@ export class World {
|
||||
if (this.state.year - last < 1) return false
|
||||
fam.flag[`tauntCD-${npcId}`] = this.state.year
|
||||
npc.relation = Math.max(-100, npc.relation - 20)
|
||||
this.log('bad', `指桑骂槐,${npc.name}记恨于心。`)
|
||||
fam.karma -= 4
|
||||
this.log('bad', `指桑骂槐,${npc.name}记恨于心。(功德-4)`)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,8 @@ export function createWorldState(opts: NewGameOptions): GameState {
|
||||
missionIds: [],
|
||||
headId: '',
|
||||
difficulty: diff,
|
||||
flag: { priceMult: 1, tenants: 0, headBless: 0 }
|
||||
flag: { priceMult: 1, tenants: 0, headBless: 0 },
|
||||
karma: 0
|
||||
},
|
||||
members: {},
|
||||
npcFamilies: Object.fromEntries(
|
||||
|
||||
@@ -10,7 +10,7 @@ import { World } from './World'
|
||||
import { WorldGenPools } from '../sim/worldgen'
|
||||
import { arith } from '../../data/arith'
|
||||
import { pack as dataPack } from '../../data/registry'
|
||||
import { removeCalamityByName, overrideWorldNum, resetWorldNum } from '../sim/worldsim-data'
|
||||
import { removeCalamityByName, overrideWorldNum, resetWorldNum, addWonderType, removeWonderType } from '../sim/worldsim-data'
|
||||
import { removePillRecipe, removeForgeRecipe } from '../../data/items'
|
||||
import { registerBuildingDef } from '../../data/buildings'
|
||||
import { registerPostDef } from '../../data/posts'
|
||||
@@ -63,7 +63,7 @@ function booleanExprTable(expr: string | Record<string, string>): (level: number
|
||||
}
|
||||
|
||||
/** 安装前检查:事件 id 与核心/已装池冲突 + 范式校验警告 */
|
||||
const DYNAMIC_PREFIXES = ['ev-raid-', 'ev-trib-', 'ev-tournament-', 'ev-echo-', 'ev-auction', 'ev-centennial', 'ev-feisheng', 'ev-legacypass', 'ev-winterprayer', 'ev-jobai']
|
||||
const DYNAMIC_PREFIXES = ['ev-raid-', 'ev-trib-', 'ev-tournament-', 'ev-echo-', 'ev-auction', 'ev-centennial', 'ev-feisheng', 'ev-legacypass', 'ev-winterprayer', 'ev-jobai', 'ev-ascension-']
|
||||
|
||||
export function modPreflight(w: World, pack: ModPack): { ok: boolean; reason?: string; warnings: string[] } {
|
||||
const v = validateMod(pack)
|
||||
@@ -100,6 +100,10 @@ export function modToPlugin(pack: ModPack): CotycPlugin {
|
||||
for (const c of pack.data.calamities ?? []) {
|
||||
ctx.addCalamity(c.name, { brief: c.brief, family: c.family, effect: c.effect })
|
||||
}
|
||||
// 0.1.37 奇观类型扩展(MOD 奇观进入种子派生选取池——零 rng 消耗)
|
||||
for (const w of pack.data.wonders ?? []) {
|
||||
ctx.addWonderType(w)
|
||||
}
|
||||
// P1-11 worldNum 白名单覆写(0.1.28 接线:仅 WORLDSIM 内键生效,卸载复位)
|
||||
for (const [k, v] of Object.entries(pack.data.worldNum ?? {})) {
|
||||
overrideWorldNum(k, v)
|
||||
@@ -136,6 +140,7 @@ export function modToPlugin(pack: ModPack): CotycPlugin {
|
||||
// 0.1.28 精确回滚:词库池/灾因/配方/世界数值按来源摘除(其余 MOD 共享词条保留)
|
||||
WorldGenPools.removePool(`mod-${pack.id}`)
|
||||
for (const c of pack.data.calamities ?? []) removeCalamityByName(c.name)
|
||||
for (const w of pack.data.wonders ?? []) removeWonderType(w.kind)
|
||||
for (const r of pack.data.pills ?? []) removePillRecipe(r.output)
|
||||
for (const r of pack.data.forges ?? []) removeForgeRecipe(r.output)
|
||||
if (pack.data.worldNum) for (const k of Object.keys(pack.data.worldNum)) resetWorldNum(k)
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface ModPackData {
|
||||
aspirations?: Array<{ id: string; name: string; desc: string; effectType: string; value: number }>
|
||||
formations?: Array<{ id: string; name: string; icon: string; desc: string; atk: number; def: number; retreatWound: number }>
|
||||
traits?: Array<{ id: string; name: string; desc: string; bonus?: Record<string, number> }>
|
||||
/** 0.1.37 奇观类型扩展(MOD 可注册自定义世界奇观——零 rng 消耗,种子派生选取) */
|
||||
wonders?: Array<{ kind: string; name: string; desc: string; duration?: number; effect?: 'tide_up' | 'tide_down' | 'none'; oneshot?: 'bless' | 'loot' | 'none' }>
|
||||
}
|
||||
|
||||
export interface ModPack {
|
||||
@@ -167,5 +169,11 @@ export function validateMod(pack: ModPack): ModValidation {
|
||||
if (!(p.mult === undefined || (p.mult >= 0.5 && p.mult <= 2))) warnings.push(`patch ${p.target}:${p.id} mult 限 0.5~2。`)
|
||||
if (p.mult !== undefined && p.add !== undefined) warnings.push(`patch ${p.target}:${p.id} mult/add 二选一(勿同给)。`)
|
||||
}
|
||||
// 0.1.37 奇观类型校验
|
||||
for (const w of pack.data.wonders ?? []) {
|
||||
if (!w.kind || typeof w.kind !== 'string') warnings.push('奇观缺 kind(唯一 id)。')
|
||||
if (['tide_surge', 'ancient_ruin', 'heaven_bless', 'qi_wane'].includes(w.kind)) warnings.push(`奇观 ${w.kind} 与内置冲突。`)
|
||||
if (w.duration !== undefined && (w.duration < 0 || w.duration > 60)) warnings.push(`奇观 ${w.kind} duration 建议 0~60。`)
|
||||
}
|
||||
return { ok: warnings.length === 0, warnings }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** 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 { WorldSimState, NpcDynamics, WORLDSIM, ERA_CONF, makeWorldSimState, NpcStance, REGION_TIDE, eraDuraOf, regionTideOf, calamityNames, calamityFamilyOf, calamityEffectOf, CalamityName, POOL_BASE, worldNum, WONDER_CHANCE, WONDER_NAMES, WONDER_DESCS, WONDER_DURATION, WorldWonderKind, WorldWonder, allWonderKinds, wonderDefOf } from './worldsim-data'
|
||||
import { pack } from '../../data/registry'
|
||||
import { ITEMS } from '../../data/items'
|
||||
import { npcById, getNpcDefs, registerNpcDef, unregisterNpcDef } from '../../data/npcs'
|
||||
@@ -85,10 +85,108 @@ export class WorldSim {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- D-0.8 功德因果年首回馈(0.1.37) ----
|
||||
if (this.w.state.month === 1) {
|
||||
const fam = this.w.state.family
|
||||
// karma 年首向中性衰减(0.1.37:衰减率可经 worldNum('karmaDecayRate') 覆写)
|
||||
const decayRate = worldNum('karmaDecayRate')
|
||||
if (fam.karma !== 0) {
|
||||
const decay = Math.round(fam.karma * decayRate * 10) / 10
|
||||
fam.karma = Math.round((fam.karma - decay) * 10) / 10
|
||||
if (Math.abs(fam.karma) < 0.1) fam.karma = 0
|
||||
}
|
||||
// 因果回馈:高 karma 年首广播福泽
|
||||
const karmaHigh = worldNum('karmaHighThreshold')
|
||||
if (fam.karma > karmaHigh && !fam.flag['karmaBlessed-' + this.w.state.year]) {
|
||||
fam.flag['karmaBlessed-' + this.w.state.year] = true
|
||||
this.w.log('good', `积善之家必有余庆——功德深厚(${Math.round(fam.karma)}),天地福泽暗佑。`)
|
||||
}
|
||||
// 因果回馈:低 karma 年首天谴预警
|
||||
const karmaLow = worldNum('karmaLowThreshold')
|
||||
if (fam.karma < karmaLow && !fam.flag['karmaWarned-' + this.w.state.year]) {
|
||||
fam.flag['karmaWarned-' + this.w.state.year] = true
|
||||
this.w.log('bad', `因果缠身——功德亏缺(${Math.round(fam.karma)}),灾祸或至。`)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- D-0.9 世界奇观年首判定(0.1.37 种子派生——零 rng 消耗) ----
|
||||
if (this.w.state.month === 1) {
|
||||
// 上一年奇观到期清理
|
||||
if (s.wonder && s.wonder.duration && s.wonder.duration > 0) {
|
||||
s.wonder.elapsed = (s.wonder.elapsed ?? 0) + 12
|
||||
if (s.wonder.elapsed >= s.wonder.duration) {
|
||||
const wdefEnd = wonderDefOf(s.wonder.kind)
|
||||
const name = wdefEnd?.name ?? s.wonder.kind
|
||||
s.newsFeed.push({ year: this.w.state.year, month: 1, src: '天道', text: `${name}消散——天地复常。`, kind: 'annal' })
|
||||
this.w.log('info', `【天下】${name}消散,天地复常。`)
|
||||
s.wonder = undefined
|
||||
}
|
||||
}
|
||||
// 年首种子派生判定(不消耗 rng 主序列)
|
||||
if (!s.wonder) {
|
||||
let sh = 0
|
||||
for (let i = 0; i < this.w.state.seed.length; i++) sh = (sh * 31 + this.w.state.seed.charCodeAt(i)) >>> 0
|
||||
const wonderRoll = ((sh ^ (this.w.state.year * 16777619) ^ (s.npcSuccessions * 2654435761)) >>> 0) % 1000 / 1000
|
||||
if (wonderRoll < worldNum('wonderChance')) {
|
||||
// 0.1.37 P5:奇观类型从聚合池选取(内置 + MOD 扩展——零 rng 消耗)
|
||||
const allKinds = allWonderKinds()
|
||||
const kindIdx = ((sh + this.w.state.year * 40503) >>> 0) % allKinds.length
|
||||
const wdef = allKinds[kindIdx]!
|
||||
const kind = wdef.kind as WorldWonderKind
|
||||
s.wonder = {
|
||||
kind,
|
||||
year: this.w.state.year,
|
||||
duration: wdef.duration,
|
||||
elapsed: 0
|
||||
}
|
||||
const name = wdef.name
|
||||
const desc = wdef.desc
|
||||
s.newsFeed.push({ year: this.w.state.year, month: 1, src: '天机', text: `${name}——${desc}`, kind: 'annal' })
|
||||
this.w.log('good', `【天下异变】${name}:${desc}`)
|
||||
this.w.emitFx('pulse', `wonder:${kind}`)
|
||||
// 一次性奇观效果(0.1.37 P5:通过 wonderDefOf 识别扩展奇观的 oneshot 类型)
|
||||
const wdef2 = wonderDefOf(kind)
|
||||
if (wdef2?.oneshot === 'bless') {
|
||||
// 天降祥瑞:灾年消弭
|
||||
if (s.calamity) {
|
||||
s.calamity = undefined
|
||||
s.calamityLeft = 0
|
||||
this.w.log('good', '祥瑞降世,灾厄消弭——灾云散尽。')
|
||||
}
|
||||
// 声望 +5
|
||||
this.w.state.family.reputation += 5
|
||||
} else if (wdef2?.oneshot === 'loot') {
|
||||
// 古遗迹/宝物型奇观:获得随机资源
|
||||
const loot = ['lingcao', 'lingkuang', 'beastcore', 'lingyu'][((sh + this.w.state.year * 7919) >>> 0) % 4]
|
||||
const qty = 10 + ((sh + this.w.state.year * 13) >>> 0) % 15
|
||||
this.w.state.family.inventory[loot] = (this.w.state.family.inventory[loot] ?? 0) + qty
|
||||
const lootName = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', lingyu: '灵玉' }[loot] ?? loot
|
||||
this.w.log('good', `${name}中得${lootName}×${qty}——前世遗珍。`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 0.1.37 奇观持续效果(月度——通过 wonderDefOf 识别扩展奇观的 effect 类型) ----
|
||||
if (s.wonder) {
|
||||
const wdefCont = wonderDefOf(s.wonder.kind)
|
||||
if (wdefCont?.effect === 'tide_up') {
|
||||
// 灵气增益:tide 缓升(修炼加成)
|
||||
s.tide = clamp(s.tide + 0.003, WORLDSIM.tideMin - 0.05, WORLDSIM.tideMax + 0.2)
|
||||
} else if (wdefCont?.effect === 'tide_down') {
|
||||
// 灵气衰减:tide 缓降(修行衰减)
|
||||
s.tide = clamp(s.tide - 0.003, WORLDSIM.tideMin - 0.2, WORLDSIM.tideMax + 0.05)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- D. 灾年(年签一掷 + 持续渐退;B12) ----
|
||||
// 剩数递减
|
||||
if ((s.calamityLeft ?? 0) > 0) s.calamityLeft = (s.calamityLeft ?? 0) - 1
|
||||
const calamityP = worldNum('calamityChance') * ERA_CONF[s.era ?? 'pingshi'].calamityMult
|
||||
// 0.1.37 因果调制:karma 高于阈值灾年概率 -15%,低于阈值 +15%(阈值可经 worldNum 覆写)
|
||||
const kHigh = worldNum('karmaHighThreshold')
|
||||
const kLow = worldNum('karmaLowThreshold')
|
||||
const karmaCalamityMod = this.w.state.family.karma > kHigh ? 0.85 : this.w.state.family.karma < kLow ? 1.15 : 1
|
||||
const calamityP = worldNum('calamityChance') * ERA_CONF[s.era ?? 'pingshi'].calamityMult * karmaCalamityMod
|
||||
if (this.w.state.month === 1 && rng.chance(calamityP)) {
|
||||
const cl = rng.pick(calamityNames())
|
||||
s.calamity = cl
|
||||
@@ -720,13 +818,24 @@ function assessStance(w: World, id: string, dyn: NpcDynamics): NpcStance {
|
||||
const prosperity = dyn.prosperity ?? 50
|
||||
const power = npc?.power ?? 100
|
||||
const relation = npc?.relation ?? 0
|
||||
// 0.1.37 因果调制:玩家家族 karma 影响 NPC 姿态倾向(阈值可经 worldNum 覆写)
|
||||
const karma = w.state.family.karma ?? 0
|
||||
const kHighS = worldNum('karmaHighThreshold')
|
||||
const kLowS = worldNum('karmaLowThreshold')
|
||||
const karmaAllyMod = karma > kHighS ? 2 : karma < kLowS ? -1 : 0
|
||||
const karmaExpandMod = karma < kLowS ? 2 : karma > kHighS ? -1 : 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<NpcStance, number> = { guardian: 3, expand: era === 'luanshi' ? 6 : 2, endure: era === 'mofa' ? 5 : 2, ally: era === 'shengshi' || era === 'pingshi' ? 4 : 1 }
|
||||
// 软权重:乱世偏扩张、盛世偏守成、平世均衡 + karma 调制
|
||||
const W: Record<NpcStance, number> = {
|
||||
guardian: 3,
|
||||
expand: Math.max(0, (era === 'luanshi' ? 6 : 2) + karmaExpandMod),
|
||||
endure: era === 'mofa' ? 5 : 2,
|
||||
ally: Math.max(0, (era === 'shengshi' || era === 'pingshi' ? 4 : 1) + karmaAllyMod)
|
||||
}
|
||||
const pick = (Object.keys(W) as NpcStance[]).sort((a, b) => W[b] - W[a])
|
||||
// 用幂等 rng 抽取(先取最高权)+ 30% 软翻转
|
||||
const top = pick[0]!
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { World } from '../runtime/World'
|
||||
import { WONDER_NAMES } from './worldsim-data'
|
||||
|
||||
export function worldSimBrief(w: World): { tideLabel: string; priceLabel: string; latest: string } | null {
|
||||
export function worldSimBrief(w: World): { tideLabel: string; priceLabel: string; latest: string; wonderLabel?: string } | null {
|
||||
const ws = w.state.worldSim
|
||||
if (!ws) return null
|
||||
const tide = ws.tide ?? 0.5
|
||||
@@ -9,5 +10,8 @@ export function worldSimBrief(w: World): { tideLabel: string; priceLabel: string
|
||||
const ratio = (pool['lingcao'] ?? 600) / 600
|
||||
const priceLabel = ratio > 1.15 ? '物腾' : ratio < 0.85 ? '物贱' : '平稳'
|
||||
const latest = ws.newsFeed?.[ws.newsFeed.length - 1]?.text ?? ''
|
||||
return { tideLabel, priceLabel, latest }
|
||||
// 0.1.37 世界奇观标签
|
||||
const wonder = (ws as { wonder?: { kind: string } }).wonder
|
||||
const wonderLabel = wonder ? WONDER_NAMES[wonder.kind as keyof typeof WONDER_NAMES] : undefined
|
||||
return { tideLabel, priceLabel, latest, wonderLabel }
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ export interface WorldSimState {
|
||||
worldAnnals?: { year: number; text: string }[]
|
||||
/** 世鉴(50/100 年宏观:开局 vs 现在/era 路径/最强家更替——独立数组零漂) */
|
||||
sagaAnnals?: { year: number; kind: 'quinquagenary' | 'centennial'; text: string }[]
|
||||
/** 0.1.37 世界奇观(当前活跃奇观;年首种子派生判定——零 rng 消耗) */
|
||||
wonder?: WorldWonder
|
||||
}
|
||||
|
||||
export function makeWorldSimState(): WorldSimState {
|
||||
@@ -225,11 +227,57 @@ export const WORLDSIM = {
|
||||
shortageLine: 0.7, // 断供警戒线:池/基准 < 此值起计连续月
|
||||
shortageMonths: 3,
|
||||
shortagePrice: 1.5, // 断供价乘
|
||||
newSecretChance: 0.04 // 年首新秘境现身概率(乱世 ×2)
|
||||
newSecretChance: 0.04, // 年首新秘境现身概率(乱世 ×2)
|
||||
// —— 0.1.37 因果/奇观 ——
|
||||
wonderChance: 0.08, // 世界奇观年首现身概率(种子派生——零 rng 消耗)
|
||||
karmaDecayRate: 0.1, // 功德年首衰减率(向中性靠拢——善行需持续)
|
||||
karmaHighThreshold: 30, // 功德深厚阈值(触发灾年 -15%/渡劫 +5%)
|
||||
karmaLowThreshold: -20 // 功德亏缺阈值(触发灾年 +15%/渡劫 -5%)
|
||||
}
|
||||
|
||||
export type CalamityName = string
|
||||
|
||||
// ==================== 0.1.37 世界奇观 ====================
|
||||
|
||||
/** 世界奇观类型(年首种子派生判定——零 rng 消耗) */
|
||||
export type WorldWonderKind = 'tide_surge' | 'ancient_ruin' | 'heaven_bless' | 'qi_wane' | string
|
||||
|
||||
export interface WorldWonder {
|
||||
kind: WorldWonderKind
|
||||
year: number
|
||||
/** 持续月数(灵气涌潮/灵脉枯竭有持续效果;古遗迹/天降祥瑞一次性) */
|
||||
duration?: number
|
||||
/** 已过月数 */
|
||||
elapsed?: number
|
||||
}
|
||||
|
||||
/** 世界奇观年首现身概率(聚合读口——MOD 可经 worldNum('wonderChance') 覆写) */
|
||||
export const WONDER_CHANCE = 0.08
|
||||
|
||||
/** 奇观名称映射 */
|
||||
export const WONDER_NAMES: Record<WorldWonderKind, string> = {
|
||||
tide_surge: '灵气涌潮',
|
||||
ancient_ruin: '古遗迹现身',
|
||||
heaven_bless: '天降祥瑞',
|
||||
qi_wane: '灵脉枯竭'
|
||||
}
|
||||
|
||||
/** 奇观描述 */
|
||||
export const WONDER_DESCS: Record<WorldWonderKind, string> = {
|
||||
tide_surge: '天地灵气如潮涌至,万灵共鸣——修炼事半功倍。',
|
||||
ancient_ruin: '远古修士洞府现世——其中或藏功法丹材之珍。',
|
||||
heaven_bless: '祥瑞降世——风调雨顺,灾厄消弭。',
|
||||
qi_wane: '灵脉逆行——天地灵气骤衰,修行维艰。'
|
||||
}
|
||||
|
||||
/** 奇观持续月数(灵气涌潮/灵脉枯竭持续 12 月;古遗迹/天降祥瑞一次性) */
|
||||
export const WONDER_DURATION: Record<WorldWonderKind, number> = {
|
||||
tide_surge: 12,
|
||||
ancient_ruin: 0,
|
||||
heaven_bless: 0,
|
||||
qi_wane: 12
|
||||
}
|
||||
|
||||
/** 默认灾因集(聚合读口:默认+MOD/插件扩展——默认表和不变 → 基准世界指纹不漂) */
|
||||
export const DEFAULT_CALAMITIES = ['旱灾', '涝灾', '蝗灾', '疫病', '兽潮', '寒潮'] as const
|
||||
|
||||
@@ -292,3 +340,61 @@ export function calamityEffectOf(name: string): Partial<Record<string, number>>
|
||||
export function calamityDescOf(name: string): string {
|
||||
return EXTENDED_BRIEF[name] ?? `${name}——灵植减产,市价将行。`
|
||||
}
|
||||
|
||||
// ==================== 0.1.37 奇观类型扩展注册表(MOD 口) ====================
|
||||
|
||||
/** 扩展奇观类型定义(MOD 可注入自定义奇观——零 rng 消耗,种子派生选取) */
|
||||
export interface ExtendedWonderDef {
|
||||
/** 奇观类型 id(唯一——与内置不冲突) */
|
||||
kind: string
|
||||
/** 中文名称 */
|
||||
name: string
|
||||
/** 描述文本 */
|
||||
desc: string
|
||||
/** 持续月数(0 = 一次性) */
|
||||
duration: number
|
||||
/** 持续效果类型:tide_up = 灵气增益 / tide_down = 灵气衰减 / none = 无持续效果 */
|
||||
effect: 'tide_up' | 'tide_down' | 'none'
|
||||
/** 一次性效果:bless = 灾年消弭+声望 / loot = 资源获取 / none = 无 */
|
||||
oneshot: 'bless' | 'loot' | 'none'
|
||||
}
|
||||
|
||||
const EXTENDED_WONDERS: ExtendedWonderDef[] = []
|
||||
|
||||
/** 内置奇观定义(供聚合读口统一访问) */
|
||||
const BUILTIN_WONDERS: ExtendedWonderDef[] = [
|
||||
{ kind: 'tide_surge', name: '灵气涌潮', desc: '天地灵气如潮涌至,万灵共鸣——修炼事半功倍。', duration: 12, effect: 'tide_up', oneshot: 'none' },
|
||||
{ kind: 'ancient_ruin', name: '古遗迹现身', desc: '远古修士洞府现世——其中或藏功法丹材之珍。', duration: 0, effect: 'none', oneshot: 'loot' },
|
||||
{ kind: 'heaven_bless', name: '天降祥瑞', desc: '祥瑞降世——风调雨顺,灾厄消弭。', duration: 0, effect: 'none', oneshot: 'bless' },
|
||||
{ kind: 'qi_wane', name: '灵脉枯竭', desc: '灵脉逆行——天地灵气骤衰,修行维艰。', duration: 12, effect: 'tide_down', oneshot: 'none' }
|
||||
]
|
||||
|
||||
/** MOD/插件注册奇观类型(kind 唯一——与内置+已注册不冲突) */
|
||||
export function addWonderType(def: Omit<ExtendedWonderDef, 'name' | 'desc' | 'duration' | 'effect' | 'oneshot'> & Partial<Omit<ExtendedWonderDef, 'kind'>>): boolean {
|
||||
if (!def.kind || BUILTIN_WONDERS.some((w) => w.kind === def.kind) || EXTENDED_WONDERS.some((w) => w.kind === def.kind)) return false
|
||||
EXTENDED_WONDERS.push({
|
||||
kind: def.kind,
|
||||
name: def.name ?? '异变',
|
||||
desc: def.desc ?? '天地异变,灵机暗涌。',
|
||||
duration: def.duration ?? 0,
|
||||
effect: def.effect ?? 'none',
|
||||
oneshot: def.oneshot ?? 'none'
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
/** 卸载时按 kind 摘除扩展奇观 */
|
||||
export function removeWonderType(kind: string): void {
|
||||
const idx = EXTENDED_WONDERS.findIndex((w) => w.kind === kind)
|
||||
if (idx >= 0) EXTENDED_WONDERS.splice(idx, 1)
|
||||
}
|
||||
|
||||
/** 聚合读口:全部奇观类型(内置 + 扩展) */
|
||||
export function allWonderKinds(): ExtendedWonderDef[] {
|
||||
return [...BUILTIN_WONDERS, ...EXTENDED_WONDERS]
|
||||
}
|
||||
|
||||
/** 聚合读口:按 kind 查奇观定义 */
|
||||
export function wonderDefOf(kind: string): ExtendedWonderDef | undefined {
|
||||
return BUILTIN_WONDERS.find((w) => w.kind === kind) ?? EXTENDED_WONDERS.find((w) => w.kind === kind)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user