import type { World } from '../World' import { Character, BattleLog } from '../../../types/domain' import { basePower, describeRealm } from '../../../data/realms' import { pack } from '../../../data/registry' import { techniqueById } from '../../../data/techniques' import { EnemyDef, LootDef } from '../../../data/secrets' import { traitBonuses } from '../pcgen' import { npcById } from '../../../data/npcs' import { aspirationById, fitBonusOf } from '../../../data/aspirations' import { formationById, FormationId } from '../../../data/formations' 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 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 + (pack().artifacts[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 * aspi * fit * 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 losses: string[] } const WIN_DESC = [ '你我咬紧牙关,剑光铺天盖地,那厮节节败退。', '阵中爆出一声大喝,众人齐攻要害,对方哀嚎退走。', '硬撼三合,杀得对方胆寒,丢下敌辎拽着尾巴逃了。' ] const LOSE_DESC = [ '对方攻势如潮,我方左支右绌,且战且退。', '眼睁睁瞧着族中子弟咳血倒地,只得弃了阵脚。', '护山大阵差点被轰裂,残兵败将忍着羞辱撤回。', '突袭来得隐秘,伤亡不小,幸好退路还在。' ] const DRAW_DESC = [ '杀了个天昏地暗,双方均伤,各自罢手。', '僵持半晌,天入暮色,双方收阵戒备而退。' ] /** 0.1.33 符箓开战(零 rng):消耗 1 张并返回其类型——风符全体战力+12%,山符+15%且受创减半 */ function talismanWarPrep(w: World): 'feng' | 'shan' | null { const inv = w.state.family.inventory if ((inv['talisman-feng'] ?? 0) > 0) { inv['talisman-feng'] = inv['talisman-feng']! - 1 return 'feng' } if ((inv['talisman-shan'] ?? 0) > 0) { inv['talisman-shan'] = inv['talisman-shan']! - 1 return 'shan' } return null } export function resolveEncounter( w: World, opts: { title: string enemy: EnemyDef risk: number team: Character[] kind: BattleLog['kind'] year: number month: number formation?: FormationId } ): EncounterResult { const form = formationById(opts.formation) const talisman = opts.kind === 'war' ? talismanWarPrep(w) : null const talismMult = talisman === 'feng' ? 1.12 : talisman === 'shan' ? 1.15 : 1 let team = 0 let guard = 0 let crit = 0 for (const c of opts.team) { team += combatPowerOf(w, c) const t = techniqueById(c.techniqueId) guard += (t?.guardBonus ?? 0) crit += (t?.critChance ?? 0) } guard /= Math.max(1, opts.team.length) crit /= Math.max(1, opts.team.length) team *= talismMult const enemy = Math.round(enemyPowerOf(opts.enemy, opts.risk) * form.def) const jitter = w.rng.between(0.88, 1.12) const teamFinal = Math.round(team * form.atk * 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}以「${form.name}」列阵,迎上了【${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) * (1 - crit))) { 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) w.emitFx('ripple', `death:${c.id}`) } else if (severity < 0.45) { const woundAmt = Math.round((40 + w.rng.int(0, 25)) * form.retreatWound * (talisman === 'shan' ? 0.5 : 1) * Math.max(0, 1 - guard)) c.health = Math.max(1, c.health - woundAmt) c.state = 'wounded' loss.push(`${c.name} 重伤`) } } else if (w.rng.chance(0.12)) { c.health = Math.max(1, c.health - Math.round((20 + w.rng.int(0, 15)) * Math.max(0, 1 - guard))) if (c.health < 35) c.state = 'wounded' loss.push(`${c.name} 轻伤`) } } let loot: Record | 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('、')}。`) } if (talisman) lines.push(`武备先声:${talisman === 'feng' ? '风符化翼' : '山符镇阵'},家士气为之振。`) if (win && opts.kind === 'war') w.emitFx('blade', 'battle') 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 = { lingcao: '灵草', lingkuang: '灵矿', beastcore: '兽核', stones: '灵石' } return names[id] ?? id } export function resolveRaid( w: World, npcId: string, team: Character[], formation?: FormationId ): EncounterResult { const npc = w.state.npcFamilies[npcId] const def = npcById(npcId) if (!def) return { win: false, draw: true, lines: ['此族已散。'], loot: undefined, losses: [] } // P1-C 敌强度随玩家队伍战力动态化(0.1.29:防 5000+ 战力零风险碾压——战争永远有戏剧性) const teamPower = team.reduce((a, c) => a + combatPowerOf(w, c), 0) const npcPower = npc.power ?? 60 const rel = teamPower > 0 ? npcPower / teamPower : 0 const strength = Math.min(2.6, Math.max(0.5, 0.35 + rel * 0.55)) const enemy: EnemyDef = { id: npcId, name: `${npc.name}的劫掠队`, realm: def.leaderRealm, strength, icon: '袭', desc: def.desc } const risk = Math.min(0.75, 0.5 + (npcPower / 120) * 0.1) const res = resolveEncounter(w, { title: `${npc.name}来袭!`, enemy, risk, team, kind: 'war', year: w.state.year, month: w.state.month, formation }) if (res.win) { npc.relation = Math.min(60, npc.relation + 25) w.state.family.reputation += 6 // B5:战争伤骨——败方 power 重挫,次年不再来犯(warCooldownYear/raidCount 启用) w.emitFx('blade', `raid:${npcId}`) npc.power = Math.max(52, Math.round(npc.power * 0.82)) npc.raidCount = (npc.raidCount ?? 0) + 1 npc.warCooldownYear = w.state.year // 1-3 蝴蝶效应:玩家重创名声——邻家对败者关系趋冷 for (const dyn of Object.values(w.state.worldSim?.npcDyn ?? {})) { if (dyn && dyn.relationsWithOthers && npcId in dyn.relationsWithOthers) { dyn.relationsWithOthers[npcId] = Math.max(-100, (dyn.relationsWithOthers[npcId] ?? 0) - 6) } } w.chronicle('battle', `击退${npc.name}的犯境,家族声威大振。`, undefined, true) } else if (!res.draw) { npc.relation = Math.max(-100, npc.relation - 15) w.emitFx('pulse', `raid-lose:${npcId}`) npc.raidCount = (npc.raidCount ?? 0) + 1 npc.warCooldownYear = w.state.year npc.power = Math.min(900, Math.round(npc.power * 1.06)) 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, qiRatio = 1): Record { const result: Record = {} for (const [k, r] of Object.entries(loot.resources)) { const v = Math.round(w.rng.int(r[0], r[1]) * qiRatio) result[k] = v w.state.family.inventory[k] = (w.state.family.inventory[k] ?? 0) + v } const itemFactor = Math.min(1.4, Math.max(0.5, 0.6 + 0.4 * qiRatio)) if (loot.artifactChance && w.rng.chance(loot.artifactChance * itemFactor)) { 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 * itemFactor)) { const pool = loot.techGrades && loot.techGrades.length > 0 ? pack().techniques.filter((t) => loot.techGrades!.includes(t.grade)) : pack().techniques if (pool.length > 0) { const t = w.rng.pick(pool) w.state.family.techniques.push(t.id) result['tech'] = 1 } } return result }