Files
ChronicleOfTheImmortalClan/src/renderer/game/engine/systems/combat.ts
T
thzxx f38669fa4a v0.1.3: 深度审计修复(兼容/血缘/联姻/远征/数值)
- 旧档兼容:<0.1.1 存档缺 finance/yearStats/yearlyReports 字段加载归一化(防运行期崩溃)
- 血缘防护:同母异父兄妹也禁止婚配(指婚/媒人/联姻三路齐封)
- 联姻修正:一家一姻亲(allied 锁),再娶再嫁不再无限刷;男儿娶亲也子孙来归
- 和约语义:议和强推关系至 +30(真停战),不再打完一场还是仇雠
- 远征收队:任务完成/召回自动释放队员闲居;重伤者当月离队疗伤、不足定员提前撤队(修复人员永久困远征/卡状态)
- 数值:婴儿夭折率 2%→0.8%;劫掠队强度 0.9→0.78;事件负资源钳制不为负灵石
- UI:丹药按钮显示持有并禁点;点将模式支持取消重选;设置页移除摆设项
- 新增 audit 套件:8 项(含 600 月长跑耐力)总测试 38→46 全绿
2026-08-23 08:34:13 +08:00

210 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { World } from '../world'
import { Character, BattleLog } from '../../types/domain'
import { basePower, describeRealm } from '../../data/realms'
import { ARTIFACT_POWER } from '../../data/items'
import { techniqueById } from '../../data/techniques'
import { EnemyDef, LootDef } from '../../data/secrets'
import { TECHNIQUES } from '../../data/techniques'
import { traitBonuses } from '../pcgen'
import { npcById } from '../../data/npcs'
export function combatPowerOf(w: World, c: Character): number {
if (!c.alive) return 0
const base = basePower(c.realm)
const stat = 1 + (c.perception + c.physique) / 32
const tech = techniqueById(c.techniqueId)
const techBonus = tech ? 1 + tech.powerBonus : 1
const equip = c.equipment ? 1 + (ARTIFACT_POWER[c.equipment] ?? 0) : 1
const trait = 1 + traitBonuses(c).windBonus
const health = 0.5 + 0.5 * (c.health / 100)
return round1(base * stat * techBonus * equip * trait * health)
}
function round1(n: number): number {
return Math.round(n * 10) / 10
}
export function enemyPowerOf(enemy: EnemyDef, risk: number): number {
const base = basePower({ major: enemy.realm, minor: 2 })
return Math.round(base * enemy.strength * (1.05 + risk * 0.55))
}
export interface EncounterResult {
win: boolean
draw: boolean
lines: string[]
loot?: Record<string, number>
losses: string[]
}
const WIN_DESC = [
'你我咬紧牙关,剑光铺天盖地,那厮节节败退。',
'阵中爆出一声大喝,众人齐攻要害,对方哀嚎退走。',
'硬撼三合,杀得对方胆寒,丢下敌辎拽着尾巴逃了。'
]
const LOSE_DESC = [
'对方攻势如潮,我方左支右绌,且战且退。',
'眼睁睁瞧着族中子弟咳血倒地,只得弃了阵脚。',
'护山大阵差点被轰裂,残兵败将忍着羞辱撤回。',
'突袭来得隐秘,伤亡不小,幸好退路还在。'
]
const DRAW_DESC = [
'杀了个天昏地暗,双方均伤,各自罢手。',
'僵持半晌,天入暮色,双方收阵戒备而退。'
]
export function resolveEncounter(
w: World,
opts: {
title: string
enemy: EnemyDef
risk: number
team: Character[]
kind: BattleLog['kind']
year: number
month: number
}
): EncounterResult {
let team = 0
for (const c of opts.team) team += combatPowerOf(w, c)
const enemy = enemyPowerOf(opts.enemy, opts.risk)
const jitter = w.rng.between(0.88, 1.12)
const teamFinal = Math.round(team * jitter)
const roll = w.rng.next()
const win = teamFinal >= enemy * 1.08
const lose = teamFinal < enemy * 0.82
const draw = !win && !lose
const year = opts.year
const month = opts.month
const names = opts.team.map((c) => c.name).join('、')
const lines: string[] = []
lines.push(`—— ${opts.title} ——`)
lines.push(`${year}${month}月,${names}遇上了【${opts.enemy.name}】。(敌势 ${enemy},我阵 ${teamFinal}`)
if (win) {
lines.push(`首战告捷:${w.rng.pick(WIN_DESC)}`)
} else if (lose) {
lines.push(`败象已成:${w.rng.pick(LOSE_DESC)}`)
} else {
lines.push(`来回缠斗:${w.rng.pick(DRAW_DESC)}`)
}
const loss: string[] = []
for (const c of opts.team) {
if (!c.alive || c.state === 'wounded') continue
const severity = w.rng.next()
if (!win) {
if (severity < 0.1 && w.rng.chance(opts.risk * 0.08 + 0.02)) {
c.alive = false
c.deathYear = year
c.deathCause = `战殁于${opts.enemy.name}之手`
loss.push(`${c.name} 陨落`)
w.chronicle('death', `${c.name} 战殁于${opts.enemy.name},一身所学俱付尘烟。`, c.id, true)
} else if (severity < 0.45) {
c.health = Math.max(1, c.health - 40 - w.rng.int(0, 25))
c.state = 'wounded'
loss.push(`${c.name} 重伤`)
}
} else if (w.rng.chance(0.12)) {
c.health = Math.max(1, c.health - 20 - w.rng.int(0, 15))
if (c.health < 35) c.state = 'wounded'
loss.push(`${c.name} 轻伤`)
}
}
let loot: Record<string, number> | undefined
if (win) {
loot = {}
const res = opts.risk > 0.8 ? { lingkuang: [20, 60], lingcao: [15, 40] } : { lingcao: [10, 30], lingkuang: [5, 20] }
for (const [k, r] of Object.entries(res)) {
const v = w.rng.int(r[0], r[1])
loot[k] = v
w.state.family.inventory[k] = (w.state.family.inventory[k] ?? 0) + v
}
lines.push(`此战缴获:${Object.entries(loot).map(([k, v]) => `${itemName(k)} ×${v}`).join('、')}。`)
}
const result: EncounterResult = { win, draw, lines, loot, losses: loss }
const log: BattleLog = {
id: w.seq(),
year,
month,
title: opts.title,
kind: opts.kind,
lines,
winner: win ? 'player' : draw ? 'none' : 'enemy',
loot,
losses: loss
}
w.battle(log)
return result
}
function itemName(id: string): string {
const names: Record<string, string> = {
lingcao: '灵草',
lingkuang: '灵矿',
beastcore: '兽核',
stones: '灵石'
}
return names[id] ?? id
}
export function resolveRaid(
w: World,
npcId: string,
team: Character[]
): EncounterResult {
const npc = w.state.npcFamilies[npcId]
const def = npcById(npcId)
const enemy: EnemyDef = {
id: npcId,
name: `${npc.name}的劫掠队`,
realm: def.leaderRealm,
strength: 0.78,
icon: '袭',
desc: def.desc
}
const risk = 0.55
const res = resolveEncounter(w, {
title: `${npc.name}来袭!`,
enemy,
risk,
team,
kind: 'war',
year: w.state.year,
month: w.state.month
})
if (res.win) {
npc.relation = Math.min(60, npc.relation + 25)
w.state.family.reputation += 6
w.chronicle('battle', `击退${npc.name}的犯境,家族声威大振。`, undefined, true)
} else if (!res.draw) {
npc.relation = Math.max(-100, npc.relation - 15)
const st = w.state.family.stones
const lostFew = Math.min(st, Math.round(st * 0.25))
w.state.family.stones -= lostFew
if (lostFew > 0) w.log('bad', `宗族仓廪被劫掠,损失灵石 ${lostFew}。`)
}
return res
}
export function rollWarbooty(w: World, loot: LootDef): Record<string, number> {
const result: Record<string, number> = {}
for (const [k, r] of Object.entries(loot.resources)) {
const v = w.rng.int(r[0], r[1])
result[k] = v
w.state.family.inventory[k] = (w.state.family.inventory[k] ?? 0) + v
}
if (loot.artifactChance && w.rng.chance(loot.artifactChance)) {
const pool = ['weapon-fan', 'weapon-qi', 'weapon-ling']
const a = w.rng.pick(pool)
w.state.family.inventory[a] = (w.state.family.inventory[a] ?? 0) + 1
result[a] = 1
}
if (loot.techniqueChance && w.rng.chance(loot.techniqueChance)) {
const t = w.rng.pick(TECHNIQUES)
w.state.family.techniques.push(t.id)
result['tech'] = 1
}
return result
}