v0.1.5: 人物志(志向/渡劫/列传/契合/回声)
- 志向系统:七向(求道/镇族/承宗/商略/兵略/耕读/云游)成年礼自动定志, 量化加成入修炼/战力/坊市/灵田/添丁/声望,云游减修但际遇更频 - 大境界渡劫:炼气→筑基起,晋升改走劫难事件三选——硬渡/请护法(100灵石)/ 压制来年;护法失败或受牵连;渡劫成败与心性、气血挂勾,高阶有小陨落率 - 灵根本命契合:功法元素×主灵根,契合者修炼+6%/战力+4%,详情页「本命」徽章 - 人物列传:自动列传生成器(生平/修行脉络/历险/姻缘/子嗣/大比/墓志铭), 成员详情小传分页 + 春秋原列传长卷 - 四邻回声:每两年一桩——友好赠礼/敌视暗桩/中立传闻三态动态事件 - 修复:回声事件 npcId 解析错误、事件优先级(惯例>传闻) - 测试 176→197(志向加成/契合/渡劫三轨/压制通道/列传结构/回声三态)
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { GameState } from '../types/domain'
|
||||
import { TECHNIQUES } from '../data/techniques'
|
||||
import { describeRealm } from '../data/realms'
|
||||
import { aspirationById } from '../data/aspirations'
|
||||
import { postById } from '../data/posts'
|
||||
|
||||
export interface BioLine {
|
||||
at?: number
|
||||
label: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export function buildBiography(s: GameState, memberId: string): BioLine[] {
|
||||
const c = s.members[memberId]
|
||||
if (!c) return []
|
||||
const lines: BioLine[] = []
|
||||
const tech = TECHNIQUES.find((t) => t.id === c.techniqueId)
|
||||
const asp = aspirationById(c.aspiration)
|
||||
const post = postById(c.post)
|
||||
|
||||
lines.push({
|
||||
label: '生平',
|
||||
text: `${c.name},第${c.generation}代子孙${c.gender === 'male' ? '男' : '女'},生于${c.bornYear}年${
|
||||
c.deathYear ? `,殁于${c.deathYear}年(${c.deathCause ?? '寿终'})` : ',今尚在世'
|
||||
}。${post ? `曾居族中「${post.name}」之位。` : ''}${asp ? `平生志向:${asp.name}。` : ''}${
|
||||
tech ? `主修《${tech.name}》。` : ''
|
||||
}`
|
||||
})
|
||||
|
||||
// 突破脉络
|
||||
const breaks = s.chronicle.filter((e) => e.category === 'breakthrough' && e.memberId === c.id)
|
||||
const trials = s.chronicle.filter((e) => e.text.includes(c.name) && (e.category === 'battle' || e.category === 'exploration'))
|
||||
for (const e of breaks) {
|
||||
lines.push({ at: e.year, label: '修行', text: `${e.year}年${e.month}月,${e.text.replace(new RegExp(`^${c.name}\\s*`), '')}`})
|
||||
}
|
||||
if (trials.length > 0) {
|
||||
lines.push({ at: trials[0].year, label: '历险', text: `${trials[0].year}年·${trials[0].text}` })
|
||||
}
|
||||
|
||||
// 婚育
|
||||
const marriages = s.chronicle.filter((e) => e.category === 'marriage' && e.memberId === c.id)
|
||||
for (const m of marriages) {
|
||||
lines.push({ at: m.year, label: '姻缘', text: m.text })
|
||||
}
|
||||
if (c.children.length > 0) {
|
||||
const kids = c.children.map((id) => s.members[id]?.name ?? '?').join('、')
|
||||
lines.push({ label: '子嗣', text: `抚育子女${c.children.length}人:${kids}。` })
|
||||
}
|
||||
|
||||
// 大比
|
||||
const tourneys = s.stats.tourneyHistory.filter((t) => true).slice(-1)
|
||||
const hisTourney = tourneys.filter((t) => {
|
||||
const battle = s.battles.find((b) => b.year === t.year && b.title.includes('大比'))
|
||||
return battle?.lines.some((l) => l.includes(c.name))
|
||||
})
|
||||
if (hisTourney.length > 0) {
|
||||
lines.push({ at: hisTourney[0].year, label: '大比', text: `${hisTourney[0].year}年,随队参加太虚大比,列第${hisTourney[0].rank}名。` })
|
||||
}
|
||||
|
||||
// 墓碑铭文
|
||||
if (!c.alive) {
|
||||
const epitaph = EPITAPH[c.deathCause ?? '寿终'] ?? '一尘一土,终归青山。'
|
||||
lines.push({ label: '墓志', text: `${c.name}之墓。${epitaph}` })
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
const EPITAPH: Record<string, string> = {
|
||||
寿终: '一尘一土,终归青山。子孙焚香,岁岁在此。',
|
||||
'寿元将尽': '一尘一土,终归青山。子孙焚香,岁岁在此。',
|
||||
幼夭: '稚子长眠,天地垂爱。家人悬灯,常照归路。',
|
||||
'伤势不治': '壮士折戟,魂兮归来。家祠之下,与祖同列。',
|
||||
'突破走火': '一念求道,九死未悔。灵前法灯,为君长明。',
|
||||
'渡劫陨落': '雷霆战天,其志未竟。遗骨归山,英风尚在。',
|
||||
'战殁': '马革裹尸,归葬桑梓。长剑挂壁,子孙相传。',
|
||||
'云游飞升': '此去云深不知处,魂同列宿游太虚。'
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Element } from '../types/domain'
|
||||
import { Character } from '../types/domain'
|
||||
import { techniqueById } from './techniques'
|
||||
|
||||
export type AspirationEffectType = 'cult' | 'battle' | 'market' | 'field' | 'offspring' | 'rep'
|
||||
|
||||
export interface AspirationDef {
|
||||
id: string
|
||||
name: string
|
||||
desc: string
|
||||
icon: string
|
||||
effect: { type: AspirationEffectType; value: number }
|
||||
}
|
||||
|
||||
export const ASPIRATIONS: Record<string, AspirationDef> = {
|
||||
dao: {
|
||||
id: 'dao',
|
||||
name: '求道',
|
||||
desc: '一心通天:修炼速度 +2%。',
|
||||
icon: '道',
|
||||
effect: { type: 'cult', value: 0.02 }
|
||||
},
|
||||
zhen: {
|
||||
id: 'zhen',
|
||||
name: '镇族',
|
||||
desc: '守土传家:家族声望年增 +0.3。',
|
||||
icon: '镇',
|
||||
effect: { type: 'rep', value: 0.3 }
|
||||
},
|
||||
cheng: {
|
||||
id: 'cheng',
|
||||
name: '承宗',
|
||||
desc: '开枝散叶:家宅添丁概率 +5%。',
|
||||
icon: '承',
|
||||
effect: { type: 'offspring', value: 0.05 }
|
||||
},
|
||||
shang: {
|
||||
id: 'shang',
|
||||
name: '商略',
|
||||
desc: '市井谙熟:坊市进项 +3%。',
|
||||
icon: '商',
|
||||
effect: { type: 'market', value: 0.03 }
|
||||
},
|
||||
bing: {
|
||||
id: 'bing',
|
||||
name: '兵略',
|
||||
desc: '韬略在胸:战力 +3%。',
|
||||
icon: '兵',
|
||||
effect: { type: 'battle', value: 0.03 }
|
||||
},
|
||||
geng: {
|
||||
id: 'geng',
|
||||
name: '耕读',
|
||||
desc: '稼穑不辍:灵田产出 +5%。',
|
||||
icon: '耕',
|
||||
effect: { type: 'field', value: 0.05 }
|
||||
},
|
||||
yun: {
|
||||
id: 'yun',
|
||||
name: '云游',
|
||||
desc: '天地为庐:修炼 -3%,但多见奇景(际遇更频)。',
|
||||
icon: '云',
|
||||
effect: { type: 'cult', value: -0.03 }
|
||||
}
|
||||
}
|
||||
|
||||
export const ASPIRATION_IDS = Object.keys(ASPIRATIONS)
|
||||
|
||||
export function aspirationById(id: string | undefined): AspirationDef | null {
|
||||
if (!id) return null
|
||||
return ASPIRATIONS[id] ?? null
|
||||
}
|
||||
|
||||
export function countAspiration(type: AspirationEffectType, members: Character[]): number {
|
||||
let n = 0
|
||||
for (const c of members) {
|
||||
const def = aspirationById(c.aspiration)
|
||||
if (def?.effect.type === type) n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
export function fitBonusOf(c: Character): { cult: boolean; battle: boolean } {
|
||||
const tech = techniqueById(c.techniqueId)
|
||||
if (!tech) return { cult: false, battle: false }
|
||||
const fit = tech.element === c.roots.primary
|
||||
return { cult: fit, battle: fit }
|
||||
}
|
||||
|
||||
export function isFit(techElement: Element, primary: Element): boolean {
|
||||
return techElement === primary
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export interface EffectDef {
|
||||
feisheng?: { stay: boolean }
|
||||
tournament?: boolean
|
||||
apprentice?: { build: boolean }
|
||||
trib?: { memberId: string; mode: 'rash' | 'guard' | 'delay' }
|
||||
}
|
||||
|
||||
export interface EventOptionDef {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { EnemyDef, LootDef } from '../../data/secrets'
|
||||
import { TECHNIQUES } from '../../data/techniques'
|
||||
import { traitBonuses } from '../pcgen'
|
||||
import { npcById } from '../../data/npcs'
|
||||
import { aspirationById, fitBonusOf } from '../../data/aspirations'
|
||||
|
||||
export function combatPowerOf(w: World, c: Character): number {
|
||||
if (!c.alive) return 0
|
||||
@@ -16,9 +17,12 @@ export function combatPowerOf(w: World, c: Character): number {
|
||||
const wuRank = (c.techniqueRank ?? 0) > 1 ? 0.2 : (c.techniqueRank ?? 0) === 1 ? 0.08 : 0
|
||||
const techBonus = tech ? 1 + tech.powerBonus + wuRank : 1
|
||||
const equip = c.equipment ? 1 + (ARTIFACT_POWER[c.equipment] ?? 0) : 1
|
||||
const aspiration = aspirationById(c.aspiration)
|
||||
const aspi = aspiration?.effect.type === 'battle' ? 1 + aspiration.effect.value : 1
|
||||
const fit = fitBonusOf(c).battle ? 1.04 : 1
|
||||
const trait = 1 + traitBonuses(c).windBonus
|
||||
const health = 0.5 + 0.5 * (c.health / 100)
|
||||
return round1(base * stat * techBonus * equip * trait * health)
|
||||
return round1(base * stat * techBonus * equip * trait * aspi * fit * health)
|
||||
}
|
||||
|
||||
function round1(n: number): number {
|
||||
|
||||
@@ -2,12 +2,15 @@ import { 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 '../../core/names'
|
||||
import { ASPIRATION_IDS } from '../../data/aspirations'
|
||||
import { needsTribulation, tribulationEventId } from './tribulation'
|
||||
|
||||
export function monthlyRate(w: World, c: Character): number {
|
||||
const st = w.state
|
||||
@@ -26,6 +29,9 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
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')
|
||||
@@ -48,6 +54,12 @@ export function monthlyRate(w: World, c: Character): number {
|
||||
}
|
||||
|
||||
export function cultivationTick(w: World): void {
|
||||
// 成年礼定志(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)
|
||||
@@ -75,7 +87,18 @@ export function cultivationTick(w: World): void {
|
||||
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))) {
|
||||
resolveBreakthrough(w, c, 0)
|
||||
if (needsTribulation(c)) {
|
||||
// 大境界晋升改走渡劫事件(玩家三选);压制者待来年
|
||||
if (!c.tribDelayYear || w.state.year >= c.tribDelayYear) {
|
||||
c.tribDelayYear = undefined
|
||||
w.pendingEvent(tribulationEventId(c))
|
||||
w.state.pendingEvent = tribulationEventId(c)
|
||||
} else {
|
||||
c.realmProgress = 100
|
||||
}
|
||||
} else {
|
||||
resolveBreakthrough(w, c, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { npcById } from '../../data/npcs'
|
||||
import { resolveRaid } from './combat'
|
||||
import { sendMission } from './missions'
|
||||
import { findInheritor } from '../creation'
|
||||
import { resolveTribulation } from './tribulation'
|
||||
import { nextRealm, describeRealm } from '../../data/realms'
|
||||
|
||||
const ALL_EVENTS: EventDef[] = [...EVENTS]
|
||||
|
||||
@@ -80,6 +82,60 @@ export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
|
||||
]
|
||||
}
|
||||
}
|
||||
if (id.startsWith('ev-echo-')) {
|
||||
const m = id.match(/^ev-echo-(.+)-\d+$/)
|
||||
const npcId = m ? m[1] : ''
|
||||
const npc = w?.state.npcFamilies[npcId]
|
||||
if (!npc) return undefined
|
||||
const rel = npc.relation
|
||||
if (rel > 40) {
|
||||
return {
|
||||
id,
|
||||
name: '四邻来使',
|
||||
category: 'daily',
|
||||
weight: 0,
|
||||
text: `${npc.name}遣使携礼来会:称去年宗主冬狩得灵材,念两家通好,特分润相赠。`,
|
||||
options: [{ label: '收下并回礼', hint: '灵石+80', eff: { res: { stones: 80 }, rep: 2 } }, { label: '婉谢盛情', hint: '关系+3', eff: { relation: { [npcId]: 3 } } }]
|
||||
}
|
||||
}
|
||||
if (rel < -40) {
|
||||
return {
|
||||
id,
|
||||
name: '四邻诡音',
|
||||
category: 'daily',
|
||||
weight: 0,
|
||||
text: `近日族中子弟夜猎,屡遭马失前蹄——痕迹指向${npc.name}的暗桩。`,
|
||||
options: [{ label: '斥资戒备', hint: '灵石-40', eff: { res: { stones: -40 }, rep: 2 } }, { label: '按兵不动', hint: '声望-4', eff: { rep: -4 } }]
|
||||
}
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name: '四邻传闻',
|
||||
category: 'daily',
|
||||
weight: 0,
|
||||
text: `商道上传来消息:${npc.name}有人丁、家声微变,坊市行情或生波澜。`,
|
||||
options: [{ label: '记下了', hint: '无', eff: {} }]
|
||||
}
|
||||
}
|
||||
if (id.startsWith('ev-trib-')) {
|
||||
const parts = id.replace('ev-trib-', '').split('-')
|
||||
const memberId = parts[0]
|
||||
const member = w?.state.members[memberId]
|
||||
if (!member || !member.alive) return undefined
|
||||
const next = describeNext(member)
|
||||
return {
|
||||
id,
|
||||
name: '天劫',
|
||||
category: 'major',
|
||||
weight: 0,
|
||||
text: `${member.name} 欲冲击【${next}】之关,天上已有雷云积聚。此劫一渡,百尺竿头再进一步;一步踏错,则伤损难料。族中当如何?`,
|
||||
options: [
|
||||
{ label: '硬渡!', hint: '心境不减,成败由天', eff: { trib: { memberId, mode: 'rash' } } },
|
||||
{ label: '请护法(灵石100)', hint: '成算大增,护法或受牵连', eff: { trib: { memberId, mode: 'guard' } } },
|
||||
{ label: '压制一年', hint: '养精蓄锐,来年再渡', eff: { trib: { memberId, mode: 'delay' } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
if (id === 'ev-recruit') {
|
||||
return {
|
||||
id,
|
||||
@@ -110,6 +166,11 @@ export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function describeNext(c: { realm: { major: string; minor: number } }): string {
|
||||
const next = nextRealm(c.realm as never)
|
||||
return next ? describeRealm(next) : '临峰'
|
||||
}
|
||||
|
||||
function asFlagString(flag: Record<string, number | boolean | string> | undefined, key: string): string | null {
|
||||
if (!flag) return null
|
||||
const v = flag[key]
|
||||
@@ -194,6 +255,17 @@ export function eventRoll(w: World): void {
|
||||
fire(w, `ev-tournament-${tourneyNext}`)
|
||||
return
|
||||
}
|
||||
// 四邻回声(每两年左右一桩)
|
||||
if (s.year % 2 === 0) {
|
||||
const key = `echoDone-${s.year}`
|
||||
if (!s.family.flag[key] && w.rng.chance(0.7)) {
|
||||
const npc = w.rng.pick(Object.values(s.npcFamilies))
|
||||
s.family.flag[key] = true
|
||||
fire(w, `ev-echo-${npc.id}-${s.year}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const roll = w.rng.next()
|
||||
const category: 'daily' | 'major' | 'fate' | undefined = roll < 0.5 ? 'daily' : roll < 0.78 ? 'major' : roll < 0.86 ? 'fate' : undefined
|
||||
@@ -336,6 +408,13 @@ function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||||
finalizeApprentice(w, stayId, 'stay')
|
||||
delete fam.flag['apprenticeStay']
|
||||
}
|
||||
if (eff.trib) {
|
||||
const c = w.state.members[eff.trib.memberId]
|
||||
if (c?.alive) {
|
||||
resolveTribulation(w, c, eff.trib.mode)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (eff.feisheng) {
|
||||
const s2 = w.state
|
||||
const immortal = w
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Character } from '../../types/domain'
|
||||
import { produceOffspring } from './cultivation'
|
||||
import { yearGrowth } from './diplomacy'
|
||||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../core/names'
|
||||
import { aspirationById } from '../../data/aspirations'
|
||||
|
||||
export function yearStartMarriage(w: World): void {
|
||||
yearGrowth(w)
|
||||
@@ -21,7 +22,10 @@ export function yearStartMarriage(w: World): void {
|
||||
const fatherAge = w.ageOf(father)
|
||||
const motherAge = w.ageOf(mother)
|
||||
if (fatherAge < 18 || fatherAge > 52 || motherAge < 16 || motherAge > 46) continue
|
||||
const p = birthBase * (0.75 + mother.physique * 0.05)
|
||||
const offspringBonus = Object.values(s.members)
|
||||
.filter((m) => m.alive && aspirationById(m.aspiration)?.effect.type === 'offspring')
|
||||
.length * 0.05
|
||||
const p = birthBase * (0.75 + mother.physique * 0.05) + offspringBonus
|
||||
if (!w.rng.chance(p)) continue
|
||||
const gen = Math.max(father.generation, mother.generation) + 1
|
||||
const first = produceOffspring(w, { father, mother, generation: gen, bornYear: s.year, surname: fam.surname })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { World } from '../world'
|
||||
import { aspirationById } from '../../data/aspirations'
|
||||
|
||||
export function productionTick(w: World): void {
|
||||
const fam = w.state.family
|
||||
@@ -12,8 +13,10 @@ export function productionTick(w: World): void {
|
||||
const fangshi = lvl('fangshi')
|
||||
const lingshou = lvl('lingshou')
|
||||
|
||||
const fielders = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'field').length
|
||||
const merchants = w.aliveMembers().filter((c) => aspirationById(c.aspiration)?.effect.type === 'market').length
|
||||
if (lingtian > 0) {
|
||||
const v = 10 * lingtian
|
||||
const v = Math.round(10 * lingtian * (1 + fielders * 0.05))
|
||||
inv.lingcao = (inv.lingcao ?? 0) + v
|
||||
parts.push(`灵田+${v}灵草`)
|
||||
}
|
||||
@@ -32,7 +35,7 @@ export function productionTick(w: World): void {
|
||||
parts.push(`灵矿+${v}灵矿`)
|
||||
}
|
||||
if (fangshi > 0) {
|
||||
const v = Math.round(55 * fangshi * (1 + w.postBonus('marketIncome')))
|
||||
const v = Math.round(55 * fangshi * (1 + w.postBonus('marketIncome') + merchants * 0.03))
|
||||
fam.stones += v
|
||||
parts.push(`坊市+${v}灵石`)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { nextRealm, MAJOR_ORDER, realmDeathChance, describeRealm } from '../../data/realms'
|
||||
|
||||
export const TRIB_MAJORS: string[] = ['foundation', 'core', 'nascent', 'spirit']
|
||||
|
||||
export function needsTribulation(c: Character): boolean {
|
||||
const next = nextRealm(c.realm)
|
||||
if (!next) return false
|
||||
if (next.major === c.realm.major) return false
|
||||
return TRIB_MAJORS.includes(next.major)
|
||||
}
|
||||
|
||||
export function tribulationEventId(c: Character): string {
|
||||
return `ev-trib-${c.id}-${c.realm.major}-${c.realm.minor}`
|
||||
}
|
||||
|
||||
export function resolveTribulation(w: World, c: Character, mode: 'rash' | 'guard' | 'delay'): string {
|
||||
const next = nextRealm(c.realm)
|
||||
if (!next) return 'peak'
|
||||
|
||||
if (mode === 'delay') {
|
||||
c.tribDelayYear = w.state.year + 1
|
||||
w.state.family.stones = w.state.family.stones
|
||||
w.log('info', `${c.name} 按兵不动,引而不发,待来年再渡。`)
|
||||
return 'delayed'
|
||||
}
|
||||
|
||||
let successChance = perTribChance(w, c)
|
||||
let guardian: Character | null = null
|
||||
if (mode === 'guard') {
|
||||
const fam = w.state.family
|
||||
if (fam.stones < 100) {
|
||||
w.log('bad', '护法之资不足,此行只得咬牙亲渡。')
|
||||
} else {
|
||||
fam.stones -= 100
|
||||
successChance += 0.08
|
||||
guardian = w
|
||||
.aliveMembers()
|
||||
.filter((m) => m.id !== c.id && MAJOR_ORDER.indexOf(m.realm.major) >= MAJOR_ORDER.indexOf(c.realm.major))
|
||||
.sort((a, b) => MAJOR_ORDER.indexOf(b.realm.major) - MAJOR_ORDER.indexOf(a.realm.major))[0] ?? null
|
||||
}
|
||||
}
|
||||
|
||||
const p = Math.min(0.92, Math.max(0.06, successChance))
|
||||
const s = w.state
|
||||
if (w.rng.chance(p)) {
|
||||
c.realm = next
|
||||
c.realmProgress = 0
|
||||
c.health = 100
|
||||
c.lastBreakthroughAttempt = s.year * 12 + s.month
|
||||
w.chronicle('breakthrough', `${c.name} 渡劫功成,踏入【${describeRealm(next)}】!`, c.id, true)
|
||||
w.log('good', `✦ 天雷散尽,${c.name} 渡劫成功,晋阶【${describeRealm(next)}】!`)
|
||||
if (next.major === 'spirit' && !s.flags['firstSpirit']) {
|
||||
s.flags['firstSpirit'] = s.year
|
||||
}
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// 失败
|
||||
c.lastBreakthroughAttempt = s.year * 12 + s.month
|
||||
c.realmProgress = Math.max(0, 100 - 45 - w.rng.int(0, 15))
|
||||
c.health = Math.max(1, c.health - 30 - w.rng.int(0, 15))
|
||||
if (c.health < 20) c.state = 'wounded'
|
||||
let text = `${c.name} 渡劫失败,肉身受创。`
|
||||
const danger = realmDeathChance(c.realm, c.mind)
|
||||
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} 渡劫陨落。`)
|
||||
return 'dead'
|
||||
}
|
||||
if (guardian) {
|
||||
const g = guardian
|
||||
if (w.rng.chance(0.5)) {
|
||||
g.health = Math.max(1, g.health - 15 - w.rng.int(0, 15))
|
||||
if (g.health < 25) g.state = 'wounded'
|
||||
w.log('bad', `${g.name} 为护法所伤。`)
|
||||
}
|
||||
}
|
||||
w.log('bad', text)
|
||||
return 'fail'
|
||||
}
|
||||
|
||||
export function perTribChance(w: World, c: Character): number {
|
||||
const base = ({
|
||||
foundation: 0.52,
|
||||
core: 0.42,
|
||||
nascent: 0.3,
|
||||
spirit: 0.2
|
||||
} as Record<string, number>)[c.realm.major] ?? 0.9
|
||||
return Math.min(0.92, base + c.mind * 0.01 + c.health / 400)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { Rng } from '../core/rng'
|
||||
import { BUILDINGS } from '../data/buildings'
|
||||
import { POSTS } from '../data/posts'
|
||||
import { TECHNIQUES } from '../data/techniques'
|
||||
import { aspirationById as aspirationOf } from '../data/aspirations'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../core/legacy'
|
||||
import { productionTick } from './systems/production'
|
||||
import { deathTick, woundHealTick } from './systems/lifecycle'
|
||||
@@ -188,6 +189,8 @@ export class World {
|
||||
private yearStart(): void {
|
||||
yearStartMarriage(this)
|
||||
this.state.family.reputation += this.postBonus('familyRep')
|
||||
const zhenCount = this.aliveMembers().filter((c) => aspirationOf(c.aspiration)?.effect.type === 'rep').length
|
||||
this.state.family.reputation += Math.round(zhenCount * 0.3 * 100) / 100
|
||||
const rep = this.state.family.reputation
|
||||
const power = this.familyPower()
|
||||
const report: YearlyReport = {
|
||||
|
||||
@@ -54,6 +54,8 @@ export interface Character {
|
||||
lastBreakthroughAttempt?: number
|
||||
monthProgress?: number
|
||||
apprentice?: { sect: string; untilYear: number; quiet: boolean }
|
||||
aspiration?: string
|
||||
tribDelayYear?: number
|
||||
}
|
||||
|
||||
export interface FamilyState {
|
||||
|
||||
Reference in New Issue
Block a user