refactor(0.1.14-P1): 内核归位——game/ 按引擎架构重排(零行为漂移)

结构(旧 game/core+engine → 新 engine/ 域):
- engine/kernel/  时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core)
- engine/narrative/  legacy/报告/列传/谱系/年轴(原 core 叙事族)
- engine/runtime/   World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统)
- engine/sim/     Market(未来 WorldSim 同行)
- 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层)

验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化)
typecheck 0 error
This commit is contained in:
2026-08-23 13:23:25 +08:00
parent 2528226a9f
commit ff6df8054c
89 changed files with 281 additions and 281 deletions
@@ -0,0 +1,189 @@
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'
})
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}人手不足,队伍提前收队。`)
}
}