- WorldSim(engine/sim/WorldSim.ts + worldsim-data.ts): A 资源循环市场(库存池/再平衡/价格弹性——Market 行情联动 sim.mult) B NPC 聚合演化(宗主换代/境界成长/势力=境界+财力+兵员;换代计数) C 秘境灵气(探索消耗/恢复)+ 灵气潮汐(6年周期×修炼倍率) D 灾年签(旱涝蝗疫兽寒,联动市场池) E 天下快讯(节流 24 月滚动 120 条,世界动态 feed) - 相位:新增 worldsim(diplomacy 后 epilogue 前);capabilities 加「天下演序」卡可停用 - 时钟合一:World.clock = Kernel.clock(单一时钟驱动,消除双时钟漂移) - 发现并修复:worldsim 注册被文件搬迁覆盖丢失(修改纪律验证中招——已补回); WorldSim 内 Math.random 根除(改 w.rng) - fingerprint 纳入 worldSim 概要(市场/灵脉/潮汐/换代/快讯数) - 金钟罩三档重固化(0.1.14 正式基线,受控变更) - 验证:35 套件/967 测试全绿;typecheck 0 error
199 lines
6.7 KiB
TypeScript
199 lines
6.7 KiB
TypeScript
import type { World } from '../World'
|
||
import { MissionState } from '../../../types/domain'
|
||
import { FormationId } from '../../../data/formations'
|
||
import { missionById, MissionDef, ENEMIES } from '../../../data/secrets'
|
||
import { resolveEncounter, rollWarbooty } from './combat'
|
||
import { techniqueById } from '../../../data/techniques'
|
||
import { describeRealm } from '../../../data/realms'
|
||
|
||
export function missionTick(w: World): void {
|
||
const active = w.state.missions.filter((m) => !m.done)
|
||
for (const m of active) {
|
||
settleSquad(w, m)
|
||
if (m.done) continue
|
||
m.stageMonth++
|
||
|
||
if (m.stageMonth < 2) continue
|
||
const def = missionById(m.defId)
|
||
const stage = def.stages[m.stage]
|
||
if (!stage || m.stageMonth < stage.months) continue
|
||
|
||
if (stage.kind === 'event') {
|
||
const good = w.rng.chance(0.6)
|
||
if (good) {
|
||
m.log.push(`${m.stageMonth}月:${stage.title}——${stage.text?.safe ?? '安然无事。'}`)
|
||
if (w.rng.chance(0.2)) {
|
||
const squad = squadOf(w, m)
|
||
squad.forEach((c) => (c.realmProgress = Math.min(100, c.realmProgress + 4)))
|
||
m.log.push('途中参悟,众人皆有精进。')
|
||
}
|
||
} else {
|
||
m.log.push(`${m.stageMonth}月:${stage.title}——${stage.text?.bad ?? '遭遇凶险。'}`)
|
||
const squad = squadOf(w, m)
|
||
const victim = w.rng.pick(squad)
|
||
victim.health = Math.max(1, victim.health - 25 - w.rng.int(0, 15))
|
||
if (victim.health < 35) victim.state = 'wounded'
|
||
}
|
||
} else if (stage.kind === 'resource') {
|
||
const loot = rollWarbooty(w, stage.loot ?? def.completionLoot)
|
||
m.log.push(`${stage.title}:收获 ${lootText(loot)}。`)
|
||
} else if (stage.kind === 'combat' || stage.kind === 'boss') {
|
||
const enemy = ENEMIES.find((e) => e.id === stage.enemyId) ?? ENEMIES[0]
|
||
const squad = squadOf(w, m)
|
||
const res = resolveEncounter(w, {
|
||
title: `${def.name} · ${stage.title}`,
|
||
enemy,
|
||
risk: def.risk * (stage.kind === 'boss' ? 1.15 : 1),
|
||
team: squad,
|
||
kind: 'scout',
|
||
year: w.state.year,
|
||
month: w.state.month,
|
||
formation: m.formation as FormationId | undefined
|
||
})
|
||
const line = res.win ? '战而胜之,征程继续!' : res.draw ? '僵持之后双方罢手,队伍休整再进。' : '不敌,只得暂避锋芒。'
|
||
m.log.push(line)
|
||
if (!res.win) {
|
||
if (stage.kind === 'boss') {
|
||
m.done = true
|
||
m.result = res.draw ? 'stalemate' : 'retreat'
|
||
m.log.join(' ')
|
||
w.chronicle('exploration', `${def.name}探路不遂,${resultText(m)}。`, undefined, false)
|
||
}
|
||
}
|
||
}
|
||
|
||
m.stage++
|
||
m.stageMonth = 0
|
||
if (m.stage >= def.stages.length && !m.done) {
|
||
m.done = true
|
||
m.result = 'success'
|
||
releaseSquad(w, m)
|
||
const total = rollWarbooty(w, def.completionLoot)
|
||
m.log.push(`凯旋而归,清点战利:${lootText(total)}。`)
|
||
const survivors = squadOf(w, m).filter((c) => c.alive).map((c) => c.name).join('、')
|
||
w.chronicle(
|
||
'exploration',
|
||
`${survivors} 圆满完成【${def.name}】之行。`,
|
||
undefined,
|
||
true
|
||
)
|
||
w.log('good', `${def.name} 探索归来,获得丰厚收获。`)
|
||
}
|
||
}
|
||
}
|
||
|
||
function squadOf(w: World, m: MissionState) {
|
||
return m.memberIds.map((id) => w.memberById(id)).filter((c) => c.alive)
|
||
}
|
||
|
||
function lootText(loot: Record<string, number>): string {
|
||
const names: Record<string, string> = {
|
||
lingcao: '灵草',
|
||
lingkuang: '灵矿',
|
||
beastcore: '兽核',
|
||
stones: '灵石',
|
||
'weapon-fan': '凡器',
|
||
'weapon-qi': '法器',
|
||
'weapon-ling': '灵器',
|
||
'pill-qiyuan': '聚气丹',
|
||
'pill-ningyuan': '凝元丹',
|
||
tech: '功法'
|
||
}
|
||
return Object.entries(loot)
|
||
.map(([k, v]) => `${names[k] ?? k}×${v}`)
|
||
.join('、')
|
||
}
|
||
|
||
function resultText(m: MissionState): string {
|
||
if (m.result === 'success') return '平安返回'
|
||
if (m.result === 'retreat') return '败退而回'
|
||
return '铩羽归来'
|
||
}
|
||
|
||
export function canSendMission(w: World, def: MissionDef, members: string[]): boolean {
|
||
if (members.length < def.minMembers || members.length > def.maxMembers) return false
|
||
for (const id of members) {
|
||
const c = w.state.members[id]
|
||
if (!c || !c.alive || c.state === 'expedition' || c.state === 'apprentice') return false
|
||
}
|
||
return w.state.missions.filter((m) => !m.done).length < 3
|
||
}
|
||
|
||
export function sendMission(w: World, defId: string, members: string[], formation?: FormationId): boolean {
|
||
const def = missionById(defId)
|
||
if (!canSendMission(w, def, members)) return false
|
||
const m: MissionState = {
|
||
id: w.seq(),
|
||
defId,
|
||
memberIds: members,
|
||
startYear: w.state.year,
|
||
startMonth: w.state.month,
|
||
stage: 0,
|
||
stageMonth: 0,
|
||
log: [`冬衣已备,饯行酒干,众人于 ${w.state.year} 年 ${w.state.month} 月出发。`],
|
||
done: false,
|
||
formation
|
||
}
|
||
members.forEach((id) => {
|
||
const c = w.memberById(id)
|
||
c.state = 'expedition'
|
||
})
|
||
// 世界秘境灵气消耗(有 sim 时)
|
||
if (w.state.worldSim?.secretQi) {
|
||
const qi = w.state.worldSim.secretQi[def.id] as number | undefined
|
||
if (qi !== undefined) {
|
||
w.state.worldSim.secretQi[def.id] = Math.max(0, qi - 6)
|
||
} else {
|
||
w.state.worldSim.secretQi[def.id] = 44
|
||
}
|
||
}
|
||
w.state.missions.push(m)
|
||
w.state.family.missionIds.push(m.id)
|
||
w.log('info', `队伍出发探索【${def.name}】。`)
|
||
return true
|
||
}
|
||
|
||
export function recallAll(w: World, missionId: string): void {
|
||
const m = w.state.missions.find((x) => x.id === missionId)
|
||
if (!m || m.done) return
|
||
m.done = true
|
||
m.result = 'recall'
|
||
releaseSquad(w, m)
|
||
w.log('info', '探索队伍奉命返家。')
|
||
}
|
||
|
||
/** 队伍成员状态归位:伤者保持养伤,其余恢复闲居 */
|
||
function releaseSquad(w: World, m: MissionState): void {
|
||
for (const id of m.memberIds) {
|
||
const c = w.memberById(id)
|
||
if (!c.alive) continue
|
||
if (c.state === 'wounded') continue
|
||
c.state = 'idle'
|
||
}
|
||
}
|
||
|
||
/** 月度巡查:亡者除名、重伤者离队疗伤;人数不足则提前收队 */
|
||
function settleSquad(w: World, m: MissionState): void {
|
||
const def = missionById(m.defId)
|
||
let removed = 0
|
||
for (let i = m.memberIds.length - 1; i >= 0; i--) {
|
||
const c = w.memberById(m.memberIds[i])
|
||
if (!c.alive) {
|
||
m.memberIds.splice(i, 1)
|
||
removed++
|
||
} else if (c.state === 'wounded' || c.health < 30) {
|
||
c.state = 'wounded'
|
||
m.memberIds.splice(i, 1)
|
||
removed++
|
||
m.log.push(`${c.name} 身负重伤,离队回庄疗养。`)
|
||
}
|
||
}
|
||
if (removed > 0) w.log('info', `${def.name}队中有人离队。`)
|
||
if (m.memberIds.length < def.minMembers) {
|
||
m.done = true
|
||
m.result = 'disband'
|
||
releaseSquad(w, m)
|
||
w.log('bad', `${def.name}人手不足,队伍提前收队。`)
|
||
}
|
||
}
|