【引擎审计修复】 - P0:大比 once(原整年刷 12 场、奖励通胀);flag 年份键 3 年剪枝(长线膨胀) - P1:飞升离世累加 feishengCount(飞升之资称号可达);圣者护族接消费(原死代码); 大比/传薪挂能力卡门控;pending 抢渡劫时不再重设 - P2:version 硬编码统一 0.1.11;yearaxis 飞升归类;MonumentList 挂载 【UI 审计修复】 - P0:已故成员卡显示「已殁」(原秀活动 status);空队迎战禁用+提示(原静默失效) - P1:指婚候选去截断(滚动列表);终局出口(回卷/主菜单按钮);saveNow 反馈; 事件来临写入 feed(k-event 使用);遣队「最强 N 人/清空」一键;寻衅确认+联姻原因提示; 未建建筑视觉 uc 类 - P2:speed 死代码;Modal 分层 z-index;FamilyPanel 在世/先人分隔 【UX 三件套】 - 警讯徽章(瓶颈/重伤/在外/媒缘/孤嗣之忧)顶栏一屏秒答「我该干嘛」 - 首启引导任务条(四步目标 + 完成推进,可跳不强制) - 顶栏丹药瘦身(4 资源);数字千/万化;瓶颈卡红框高亮 【测试】905→923(urgency/guide/format 三模块 + 审计修复回归 6 项) 金钟罩三档重固化(0.1.11),typecheck/build 通过
683 lines
25 KiB
TypeScript
683 lines
25 KiB
TypeScript
import type { World } from '../world'
|
||
import { Character } from '../../types/domain'
|
||
import { Cond, EffectDef, EventDef, EVENTS, MemberEffect } from '../../data/events'
|
||
import { MAJOR_ORDER } from '../../data/realms'
|
||
|
||
|
||
import { npcById } from '../../data/npcs'
|
||
import { resolveRaid } from './combat'
|
||
import { sendMission } from './missions'
|
||
import { pack } from '../../data/registry'
|
||
import { findInheritor } from '../creation'
|
||
import { resolveTribulation } from './tribulation'
|
||
import { nextRealm, describeRealm } from '../../data/realms'
|
||
|
||
const ALL_EVENTS: EventDef[] = [...EVENTS]
|
||
|
||
export function findEvent(id: string, world?: World): EventDef | undefined {
|
||
const found = ALL_EVENTS.find((e) => e.id === id)
|
||
if (found) return found
|
||
if (world) {
|
||
const pooled = world.allEvents().find((e) => e.id === id)
|
||
if (pooled) return pooled
|
||
}
|
||
return dynamicEventFor(id, world)
|
||
}
|
||
|
||
export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
|
||
if (id.startsWith('ev-raid-')) {
|
||
const npcId = id.replace('ev-raid-', '')
|
||
const npc = npcById(npcId)
|
||
return {
|
||
id,
|
||
name: `${npc.name}来犯`,
|
||
category: 'major',
|
||
weight: 0,
|
||
text: `${npc.name}与贵庄积怨已久,如今撕破脸面,遣来劫掠队围门叫战。`,
|
||
options: [
|
||
{ label: '迎战!', hint: '大战一场,胜则大利,败则伤财', eff: { raid: { npcId } } },
|
||
{ label: '割地求和', hint: '灵石-250,关系+25', eff: { res: { stones: -250 }, relation: { [npcId]: 25 }, flag: { [npcId]: 'paid' } } },
|
||
{ label: '先议和缓兵', hint: '关系+10', eff: { relation: { [npcId]: 10 } } }
|
||
]
|
||
}
|
||
}
|
||
if (id === 'ev-centennial') {
|
||
return {
|
||
id,
|
||
name: '百年庆典',
|
||
category: 'fate',
|
||
weight: 0,
|
||
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: '声望+5', eff: { rep: 5 } }
|
||
]
|
||
}
|
||
}
|
||
if (id.startsWith('ev-tournament-')) {
|
||
const year = Number(id.replace('ev-tournament-', ''))
|
||
return {
|
||
id,
|
||
name: '太虚大比',
|
||
category: 'major',
|
||
weight: 0,
|
||
once: true,
|
||
text: `太虚仙盟五年一会,新一期大比于祖庭开擂。观礼之众云集,百族竞锋——可遣嫡系赴赛,亦可称病让席。`,
|
||
options: [
|
||
{ label: '点将赴赛', hint: '战三关,位次定声望', eff: { tournament: true, flag: { [`tournamentYear-${year}`]: true } } },
|
||
{ label: '献礼买名', hint: '灵石-200,名次垫底,各族好感略升', eff: { res: { stones: -200 }, relation: { 'n-xuanying': 5, 'n-danxin': 5, 'n-sihai': 5, 'n-nulei': 5 } } },
|
||
{ label: '称病不出', hint: '声望小幅受挫', eff: { rep: -3 } }
|
||
]
|
||
}
|
||
}
|
||
if (id.startsWith('ev-apprentice-')) {
|
||
const rest = id.replace('ev-apprentice-', '')
|
||
const memberId = rest.split('-')[0]
|
||
const member = w?.state.members[memberId] ?? undefined
|
||
const sectName = member?.apprentice?.sect ?? '师门'
|
||
const name = member?.name ?? '弟子'
|
||
return {
|
||
id,
|
||
name: '寄读还乡',
|
||
category: 'major',
|
||
weight: 0,
|
||
text: `${name} 在${sectName}寄读期满。宗门遣人传书:或归家,或续练。`,
|
||
options: [
|
||
{ label: '接其归家', hint: '其悟道大进,或携功法而归', eff: { flag: { apprenticeRet: memberId } } },
|
||
{ label: '令其再拜师门', hint: '寄读二年,资源更厚', eff: { flag: { apprenticeStay: memberId } } }
|
||
]
|
||
}
|
||
}
|
||
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-legacypass') {
|
||
return {
|
||
id,
|
||
name: '名宿传薪',
|
||
category: 'major',
|
||
weight: 0,
|
||
text: '族中老修行者寿数将至,欲将一生所学倾囊相授一位后进。家祠议定传承对象。',
|
||
options: [
|
||
{ label: '按资质择少年传薪', hint: '其后进精进一大截,老修行者气血微亏', eff: { memberBy: { by: 'inspire', target: 'youngest', n: 12 }, flag: { passExchange: true } } },
|
||
{ label: '传于族中顶梁', hint: '巅峰者再得护持', eff: { memberBy: { by: 'exp', target: 'highestPower', n: 6 } } },
|
||
{ label: '留待其自然坐化', hint: '声望-2', eff: { rep: -2 } }
|
||
]
|
||
}
|
||
}
|
||
if (id === 'ev-auction') {
|
||
return {
|
||
id,
|
||
name: '仙门拍卖',
|
||
category: 'major',
|
||
weight: 0,
|
||
text: '季末仙门拍卖会张榜:百水阁将会有灵器与丹材出价。族中是否竞逐?',
|
||
options: [
|
||
{ label: '入市竞拍(灵石300)', hint: '可得一件灵器/破境丹/功法', eff: { res: { stones: -300 }, flag: { auctionWin: true } } },
|
||
{ label: '观望', hint: '声望不损,口袋不破', eff: {} }
|
||
]
|
||
}
|
||
}
|
||
if (id === 'ev-winterprayer') {
|
||
return {
|
||
id,
|
||
name: '冬至岁祷',
|
||
category: 'daily',
|
||
weight: 0,
|
||
text: '冬至夜长。家祠前灯影摇曳,族长问:今岁如何告辞?',
|
||
options: [
|
||
{ label: '登坛观星', hint: '声望+3', eff: { rep: 3 } },
|
||
{ label: '合族祈福', hint: '全族修为小精进', eff: { memberBy: { by: 'inspire', target: 'all', n: 2 } } },
|
||
{ label: '投签问卜', hint: '或有吉凶', eff: { flag: { prayerAsk: true } } }
|
||
]
|
||
}
|
||
}
|
||
if (id === 'ev-recruit') {
|
||
return {
|
||
id,
|
||
name: '宗门收徒',
|
||
category: 'major',
|
||
weight: 0,
|
||
text: '远方宗门遣师来访,观族中少年灵根不俗,欲收为记名弟子寄读。',
|
||
options: [
|
||
{ label: '送子寄读', hint: '其人外派,归时精进', eff: { apprentice: { build: true } } },
|
||
{ label: '婉拒', hint: '无', eff: {} }
|
||
]
|
||
}
|
||
}
|
||
if (id === 'ev-feisheng') {
|
||
return {
|
||
id,
|
||
name: '飞升之路',
|
||
category: 'fate',
|
||
weight: 0,
|
||
once: true,
|
||
text: '化神之上本为天堑,如今族中已有人触碰仙门。飞升之路敞开:宗族该作何抉择?',
|
||
options: [
|
||
{ label: '留仙坐镇', hint: '他留世镇族:天赋大盛', eff: { feisheng: { stay: true } } },
|
||
{ label: '放仙飞升', hint: '他云游而去寻仙门:宗族蒙荫', eff: { feisheng: { stay: false } } }
|
||
]
|
||
}
|
||
}
|
||
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]
|
||
return typeof v === 'string' ? v : null
|
||
}
|
||
|
||
import {
|
||
runTournament as _runTournament,
|
||
buildApprentice as _buildApprentice,
|
||
finalizeApprentice as _finalizeApprentice,
|
||
checkApprenticeExpiry,
|
||
recruitCheck
|
||
} from './tournament'
|
||
export const runTournament = _runTournament
|
||
export const buildApprentice = _buildApprentice
|
||
export const finalizeApprentice = _finalizeApprentice
|
||
|
||
export function matchesCond(w: World, cond?: Cond): boolean {
|
||
if (!cond) return true
|
||
const s = w.state
|
||
const fam = s.family
|
||
const alive = w.aliveMembers()
|
||
const adults = alive.filter((c) => w.ageOf(c) >= 16)
|
||
const head = s.members[fam.headId]
|
||
|
||
if (cond.all && !cond.all.every((c) => matchesCond(w, c))) return false
|
||
if (cond.any && !cond.any.some((c) => matchesCond(w, c))) return false
|
||
if (cond.not && matchesCond(w, cond.not)) return false
|
||
if (cond.minYear !== undefined && s.year < cond.minYear) return false
|
||
if (cond.minGeneration !== undefined && fam.generation < cond.minGeneration) return false
|
||
if (cond.minHeadRealm !== undefined && (!head || MAJOR_ORDER.indexOf(head.realm.major) < MAJOR_ORDER.indexOf(cond.minHeadRealm as never))) return false
|
||
if (cond.minBuilding && (fam.buildings[cond.minBuilding.id] ?? 0) < cond.minBuilding.level) return false
|
||
if (cond.minRep !== undefined && fam.reputation < cond.minRep) return false
|
||
if (cond.maxRep !== undefined && fam.reputation > cond.maxRep) return false
|
||
if (cond.minResource && (fam.inventory[cond.minResource.id] ?? 0) < cond.minResource.n) return false
|
||
if (cond.minAdult !== undefined && adults.length < cond.minAdult) return false
|
||
if (cond.maxAdult !== undefined && adults.length > cond.maxAdult) return false
|
||
if (cond.minMembers !== undefined && alive.length < cond.minMembers) return false
|
||
if (cond.eligibleAdult !== undefined) {
|
||
const eligible = adults.filter((c) => !c.spouseId && !c.spouseHouse)
|
||
if (eligible.length < cond.eligibleAdult) return false
|
||
}
|
||
if (cond.hasMeditation && !alive.some((c) => c.state === 'meditation')) return false
|
||
if (cond.relation) {
|
||
const r = s.npcFamilies[cond.relation.npcId]?.relation ?? 0
|
||
if (cond.relation.gt !== undefined && r <= cond.relation.gt) return false
|
||
if (cond.relation.lt !== undefined && r >= cond.relation.lt) return false
|
||
}
|
||
if (cond.flag) {
|
||
const v = fam.flag[cond.flag.key]
|
||
if (v !== cond.flag.eq) return false
|
||
}
|
||
if (cond.minTechCount !== undefined && fam.techniques.length < cond.minTechCount) return false
|
||
return true
|
||
}
|
||
|
||
export function eventRoll(w: World): void {
|
||
const s = w.state
|
||
if (s.pendingEvent) {
|
||
if (s.pendingEvent.startsWith('ev-trib-')) {
|
||
const memberId = s.pendingEvent.replace('ev-trib-', '').split('-')[0]
|
||
const c = w.state.members[memberId]
|
||
if (c?.alive) {
|
||
if (!c.tribDelayYear) {
|
||
c.tribPendingMonths = (c.tribPendingMonths ?? 0) + 1
|
||
if ((c.tribPendingMonths ?? 0) >= 12) {
|
||
// 悬置一年未应:自动压制(防无人/挂机软锁)
|
||
c.tribDelayYear = w.state.year + 1
|
||
c.tribPendingMonths = 0
|
||
w.log('bad', `${c.name} 的天劫悬而未决,只得引气压制,待来年。`)
|
||
s.pendingEvent = undefined
|
||
}
|
||
}
|
||
} else {
|
||
s.pendingEvent = undefined
|
||
}
|
||
}
|
||
return
|
||
}
|
||
if (s.eventQueue.length > 0) {
|
||
const evId = s.eventQueue.shift()!
|
||
const tm = evId.match(/^ev-tournament-(\d+)$/)
|
||
if (tm) delete s.family.flag[`tourneyPending-${tm[1]}`]
|
||
fire(w, evId)
|
||
return
|
||
}
|
||
|
||
// 命运里程碑最优先(百年庆典 > 飞升之路)
|
||
if (s.year >= 100 && !s.completedEvents.includes('ev-centennial')) {
|
||
fire(w, 'ev-centennial')
|
||
return
|
||
}
|
||
const firstSpirit = s.flags['firstSpirit'] as number | undefined
|
||
if (firstSpirit && s.year - firstSpirit >= 2 && !s.completedEvents.includes('ev-feisheng')) {
|
||
fire(w, 'ev-feisheng')
|
||
return
|
||
}
|
||
checkApprenticeExpiry(w)
|
||
recruitCheck(w)
|
||
|
||
// 五年一会的太虚大比(该年任一时刻优先;其他年次要事件避让)
|
||
const tourneyNext = Math.ceil(s.year / 5) * 5
|
||
if (s.year >= 10 && s.year === tourneyNext && w.sysEnabled('tournament') && !s.completedEvents.includes(`ev-tournament-${tourneyNext}`)) {
|
||
const tk = `tourneyPending-${tourneyNext}`
|
||
if (w.state.pendingEvent) {
|
||
// 另有事件在档:加入等待队列(事件优先)——用 flag 标记,防重复入队
|
||
if (!s.family.flag[tk] && !s.eventQueue.includes(`ev-tournament-${tourneyNext}`)) {
|
||
s.family.flag[tk] = true
|
||
s.eventQueue.unshift(`ev-tournament-${tourneyNext}`)
|
||
}
|
||
} else {
|
||
fire(w, `ev-tournament-${tourneyNext}`)
|
||
}
|
||
return
|
||
}
|
||
// 名宿传薪(每八年一掷)
|
||
if (s.year % 8 === 4 && w.sysEnabled('apprentice') && !s.completedEvents.includes('ev-legacypass')) {
|
||
const elder = w.aliveMembers().some((c) => w.ageOf(c) >= 66 && c.realm.major !== 'mortal')
|
||
if (elder) {
|
||
fire(w, 'ev-legacypass')
|
||
return
|
||
}
|
||
}
|
||
// 仙门拍卖(每年十月掷币)
|
||
if (s.month === 10 && w.sysEnabled('production')) {
|
||
const key = `auction-${s.year}`
|
||
if (!s.family.flag[key] && w.rng.chance(0.55)) {
|
||
s.family.flag[key] = true
|
||
fire(w, 'ev-auction')
|
||
return
|
||
}
|
||
}
|
||
// 冬至岁祷(每年十一月一掷),只在 season 系统开启时
|
||
if (s.month === 11 && w.sysEnabled('season')) {
|
||
const key = `prayerDone-${s.year}`
|
||
if (!s.family.flag[key]) {
|
||
s.family.flag[key] = true
|
||
if (w.rng.chance(0.6)) {
|
||
fire(w, 'ev-winterprayer')
|
||
return
|
||
}
|
||
}
|
||
}
|
||
// 四邻回声(偶数年一掷,不论成败封缄)
|
||
if (s.year % 2 === 0) {
|
||
const key = `echoDone-${s.year}`
|
||
if (!s.family.flag[key]) {
|
||
s.family.flag[key] = true
|
||
if (w.rng.chance(0.7)) {
|
||
const npc = w.rng.pick(Object.values(s.npcFamilies))
|
||
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
|
||
if (!category) return
|
||
const pool = w.allEvents()
|
||
const candidates = pool.filter(
|
||
(e) =>
|
||
e.category === category &&
|
||
!(e.once && s.completedEvents.includes(e.id)) &&
|
||
matchesCond(w, e.cond)
|
||
)
|
||
if (candidates.length === 0) return
|
||
const total = candidates.reduce((a, e) => a + e.weight, 0)
|
||
let r = w.rng.next() * total
|
||
for (const e of candidates) {
|
||
r -= e.weight
|
||
if (r <= 0) {
|
||
fire(w, e.id)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
export function fire(w: World, id: string): void {
|
||
if (w.state.pendingEvent) return
|
||
w.state.pendingEvent = id
|
||
w.pendingEvent(id)
|
||
}
|
||
|
||
export function applyEventChoice(
|
||
w: World,
|
||
eventId: string,
|
||
optionIdx: number,
|
||
squad?: string[],
|
||
_extra?: unknown,
|
||
formation?: string
|
||
): void {
|
||
const s = w.state
|
||
const def = findEvent(eventId, w)
|
||
if (!def) {
|
||
s.pendingEvent = undefined
|
||
return
|
||
}
|
||
const opt = def.options[optionIdx]
|
||
if (opt) {
|
||
if (formation) opt.eff.formation = formation
|
||
applyEffect(w, opt.eff, squad)
|
||
if (def.once && !s.completedEvents.includes(def.id)) s.completedEvents.push(def.id)
|
||
}
|
||
s.pendingEvent = undefined
|
||
}
|
||
|
||
export type OptionSquadPicker = (w: World) => { pool: string[]; initial: string[] }
|
||
|
||
// ---------------- effects ----------------
|
||
|
||
function pickMember(w: World, spec: MemberEffect): Character | Character[] {
|
||
const alive = w.aliveMembers()
|
||
if (alive.length === 0) return []
|
||
const byTarget = (t: string): Character[] => {
|
||
const sorted = [...alive]
|
||
switch (t) {
|
||
case 'random':
|
||
return [w.rng.pick(sorted)]
|
||
case 'head': {
|
||
const h = w.state.members[w.state.family.headId]
|
||
return h && h.alive ? [h] : []
|
||
}
|
||
case 'youngest':
|
||
return [sorted.sort((a, b) => w.ageOf(a) - w.ageOf(b))[0]]
|
||
case 'oldest':
|
||
return [sorted.sort((a, b) => w.ageOf(b) - w.ageOf(a))[0]]
|
||
case 'highestPerception':
|
||
return [sorted.sort((a, b) => b.perception - a.perception)[0]]
|
||
case 'highestPower':
|
||
return [sorted.sort((a, b) => rankPower(w, b) - rankPower(w, a))[0]]
|
||
case 'highestFortune':
|
||
return [sorted.sort((a, b) => b.fortune - a.fortune)[0]]
|
||
case 'all':
|
||
return sorted
|
||
default:
|
||
return [w.rng.pick(sorted)]
|
||
}
|
||
}
|
||
return byTarget(spec.target)
|
||
}
|
||
|
||
export function rankPower(w: World, c: Character): number {
|
||
const order = ['mortal', 'qi', 'foundation', 'core', 'nascent', 'spirit']
|
||
return order.indexOf(c.realm.major) * 10 + c.realm.minor
|
||
}
|
||
|
||
export function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||
const s = w.state
|
||
const fam = s.family
|
||
|
||
if (eff.res) {
|
||
for (const [k, v] of Object.entries(eff.res)) {
|
||
if (k === 'stones') fam.stones = Math.max(0, fam.stones + v)
|
||
else fam.inventory[k] = Math.max(0, (fam.inventory[k] ?? 0) + v)
|
||
}
|
||
}
|
||
if (eff.pillGain) {
|
||
for (const [k, v] of Object.entries(eff.pillGain)) fam.inventory[k] = (fam.inventory[k] ?? 0) + v
|
||
}
|
||
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)}。`)
|
||
}
|
||
if (eff.relation) {
|
||
for (const [k, v] of Object.entries(eff.relation)) {
|
||
const npc = s.npcFamilies[k]
|
||
if (npc) npc.relation = Math.max(-100, Math.min(100, npc.relation + v))
|
||
}
|
||
}
|
||
if (eff.addBuilding && !fam.buildings[eff.addBuilding]) {
|
||
fam.buildings[eff.addBuilding] = 1
|
||
}
|
||
if (eff.flag) {
|
||
Object.assign(fam.flag, eff.flag)
|
||
}
|
||
if (eff.techniqueChance && w.rng.chance(eff.techniqueChance)) {
|
||
const t = w.rng.pick(pack().techniques)
|
||
if (!fam.techniques.includes(t.id)) {
|
||
fam.techniques.push(t.id)
|
||
w.log('good', `得《${t.name}》残篇,录入藏书阁。`)
|
||
}
|
||
}
|
||
if (eff.artifactChance && w.rng.chance(eff.artifactChance)) {
|
||
const a = w.rng.pick(['weapon-fan', 'weapon-qi', 'weapon-ling'])
|
||
fam.inventory[a] = (fam.inventory[a] ?? 0) + 1
|
||
w.log('good', '库中多了一件法器。')
|
||
}
|
||
if (eff.addTech) {
|
||
if (!fam.techniques.includes(eff.addTech)) fam.techniques.push(eff.addTech)
|
||
}
|
||
if (eff.tournament) {
|
||
runTournament(w, squad, eff.formation as never)
|
||
}
|
||
if (eff.apprentice?.build) {
|
||
buildApprentice(w)
|
||
}
|
||
const retId = asFlagString(eff.flag, 'apprenticeRet')
|
||
const stayId = asFlagString(eff.flag, 'apprenticeStay')
|
||
if (retId) {
|
||
finalizeApprentice(w, retId, 'return')
|
||
delete fam.flag['apprenticeRet']
|
||
}
|
||
if (stayId) {
|
||
finalizeApprentice(w, stayId, 'stay')
|
||
delete fam.flag['apprenticeStay']
|
||
}
|
||
if (eff.flag?.['prayerAsk']) {
|
||
const blessing = w.rng.chance(0.6)
|
||
if (blessing) {
|
||
w.state.family.stones += 60
|
||
w.log('good', '问卜得吉:仓库中多出一笔异财。')
|
||
} else {
|
||
w.state.family.stones = Math.max(0, w.state.family.stones - 40)
|
||
w.state.family.reputation -= 1
|
||
w.log('bad', '问卜得凶:家宅小有晦气。')
|
||
}
|
||
delete w.state.family.flag['prayerAsk']
|
||
}
|
||
if (eff.flag?.['auctionWin']) {
|
||
const roll = w.rng.next()
|
||
if (roll < 0.4) {
|
||
fam.inventory['weapon-ling'] = (fam.inventory['weapon-ling'] ?? 0) + 1
|
||
w.log('good', '拍卖会落槌——购得一柄灵器!')
|
||
} else if (roll < 0.75) {
|
||
fam.inventory['pill-pojing'] = (fam.inventory['pill-pojing'] ?? 0) + 1
|
||
w.log('good', '拍卖会落槌——购得一枚破境丹!')
|
||
} else {
|
||
const t = w.rng.pick(pack().techniques)
|
||
if (!fam.techniques.includes(t.id)) {
|
||
fam.techniques.push(t.id)
|
||
w.log('good', `拍卖会落槌——竞得《${t.name}》!`)
|
||
} else {
|
||
fam.stones += 150
|
||
w.log('info', '拍卖会灵器已溢价转卖,回款150灵石。')
|
||
}
|
||
}
|
||
delete fam.flag['auctionWin']
|
||
}
|
||
if (eff.trib) {
|
||
const c = w.state.members[eff.trib.memberId]
|
||
if (c?.alive) {
|
||
c.tribPendingMonths = 0
|
||
resolveTribulation(w, c, eff.trib.mode)
|
||
}
|
||
return
|
||
}
|
||
if (eff.feisheng) {
|
||
const s2 = w.state
|
||
const immortal = w
|
||
.aliveMembers()
|
||
.sort((a, b) => rankPower(w, b) - rankPower(w, a))[0]
|
||
if (immortal) {
|
||
if (eff.feisheng.stay) {
|
||
if (!immortal.traits.includes('fengxian')) immortal.traits.push('fengxian')
|
||
w.chronicle('event', `${immortal.name} 谢绝仙门征召,愿镇守此世,护佑宗族万世。`, immortal.id, true)
|
||
w.log('good', `${immortal.name} 留世坐镇(仙风入体)。`)
|
||
} else {
|
||
immortal.alive = false
|
||
immortal.deathYear = s2.year
|
||
immortal.deathCause = '云游飞升'
|
||
s2.stats.feishengCount = (s2.stats.feishengCount ?? 0) + 1
|
||
s2.family.flag['fengFeiBless'] = true
|
||
w.chronicle('event', `${immortal.name} 于月首孤身东去,驾鹤而飞。天地留一缕仙风,庇佑宗族。`, immortal.id, true)
|
||
w.log('good', `${immortal.name} 飞升而去,宗族蒙庇。`)
|
||
if (s2.family.headId === immortal.id) {
|
||
const heir = findInheritor(w)
|
||
if (heir) w.assignHead(heir.id, true)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (eff.memberBy) {
|
||
const targets = pickMember(w, eff.memberBy)
|
||
const by = eff.memberBy.by
|
||
const n = eff.memberBy.n ?? 1
|
||
const list = Array.isArray(targets) ? targets : [targets]
|
||
for (const c of list) {
|
||
if (!c.alive) continue
|
||
switch (by) {
|
||
case 'exp':
|
||
c.realmProgress = Math.min(100, c.realmProgress + n)
|
||
w.log('info', `${c.name} 感悟顿生,修为精进。`)
|
||
break
|
||
case 'wound':
|
||
c.health = Math.max(1, c.health - 20 - n)
|
||
if (c.health < 35) c.state = 'wounded'
|
||
w.log('bad', `${c.name} 因此事负伤。`)
|
||
break
|
||
case 'heal':
|
||
c.health = Math.min(100, c.health + 20)
|
||
break
|
||
case 'breakthrough':
|
||
c.realmProgress = 100
|
||
break
|
||
case 'fatal': {
|
||
if (w.rng.chance(0.35)) {
|
||
c.alive = false
|
||
c.deathYear = s.year
|
||
c.deathCause = '遭遇不测'
|
||
w.chronicle('death', `${c.name} 突遭不测,殒命于家宅之内。`, c.id, true)
|
||
} else {
|
||
c.health = Math.max(1, c.health - 60)
|
||
c.state = 'wounded'
|
||
}
|
||
break
|
||
}
|
||
case 'repGain':
|
||
fam.reputation += 2
|
||
break
|
||
case 'inspire':
|
||
c.realmProgress = Math.min(100, c.realmProgress + n)
|
||
break
|
||
case 'loot':
|
||
c.fortune = Math.min(12, c.fortune + n)
|
||
break
|
||
case 'madness':
|
||
c.mind = Math.max(1, c.mind - 1)
|
||
c.health = Math.max(30, c.health - 10)
|
||
break
|
||
case 'genius':
|
||
c.perception = Math.min(10, c.perception + 1)
|
||
c.mind = Math.min(10, c.mind + 1)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
if (eff.mission) {
|
||
const def = pack().missions.find((m) => m.id === eff.mission)
|
||
if (def) {
|
||
const squad = w
|
||
.aliveMembers()
|
||
.filter((c) => w.ageOf(c) >= 16 && c.state !== 'expedition' && c.realm.major !== 'mortal')
|
||
.sort((a, b) => rankPower(w, b) - rankPower(w, a))
|
||
.slice(0, def.maxMembers)
|
||
if (squad.length >= def.minMembers) {
|
||
sendMission(w, def.id, squad.map((c) => c.id))
|
||
w.log('info', `家族闻讯而动,遣人奔赴【${def.name}】。`)
|
||
}
|
||
}
|
||
}
|
||
if (eff.raid) {
|
||
const npc = s.npcFamilies[eff.raid.npcId]
|
||
if (npc) {
|
||
let team = w
|
||
.aliveMembers()
|
||
.filter((c) => w.ageOf(c) >= 16 && c.state !== 'expedition')
|
||
.sort((a, b) => rankPower(w, b) - rankPower(w, a))
|
||
.slice(0, 4)
|
||
if (squad && squad.length > 0) {
|
||
team = squad.map((id) => w.memberById(id)).filter((c) => c.alive && c.state !== 'expedition' && w.ageOf(c) >= 16)
|
||
}
|
||
if (team.length > 0) {
|
||
resolveRaid(w, eff.raid.npcId, team)
|
||
fam.flag[`raidCD-${eff.raid.npcId}`] = s.year
|
||
}
|
||
}
|
||
}
|
||
}
|