v0.1.9: 百年报告 & 发布之窗(报告/迁移/阵型/冬祷/哨兵)
- 百年族史报告:buildReport 纯函数(人口/声望/战力三线、死因榜、突破密度、 大比战绩、代际长度带)+ 春秋原 SVG 折线视图 + 自动措辞结语 - 存档迁移管线 v2:migrate 链(v1→v2)+ 未来版本拒载(TOO_NEW)+ 导出信封 带 appId/schemaVersion + 导入全链路迁移校验;saveState 自动写 CURRENT_SCHEMA - 战斗阵型:锋阵(+10%攻)/雁阵(御5%+溃伤-15%)/蛇阵(溃伤-30%),三处出战接入 (遣队/大比/劫掠)点将面板下拉 + 战报首行标阵 - 冬至岁祷:每年十一月三选(观星+3声望/祈福全族修为/问卜吉凶) - 发布准备:scripts/verify-release.mjs 验收哨兵(npm run verify/verify:package) + README 全面重写(架构/守护/版本沿革);版本号三处归一 - 测试 861→881(报告四例/迁移五链/阵型六则含胜率统计/冬祷四则); 金钟罩三指纹零漂移;typecheck/build 通过
This commit is contained in:
@@ -8,6 +8,7 @@ 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
|
||||
@@ -68,13 +69,15 @@ export function resolveEncounter(
|
||||
kind: BattleLog['kind']
|
||||
year: number
|
||||
month: number
|
||||
formation?: FormationId
|
||||
}
|
||||
): EncounterResult {
|
||||
const form = formationById(opts.formation)
|
||||
let team = 0
|
||||
for (const c of opts.team) team += combatPowerOf(w, c)
|
||||
const enemy = enemyPowerOf(opts.enemy, opts.risk)
|
||||
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 * jitter)
|
||||
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
|
||||
@@ -85,7 +88,7 @@ export function resolveEncounter(
|
||||
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})`)
|
||||
lines.push(`${year}年${month}月,${names}以「${form.name}」列阵,迎上了【${opts.enemy.name}】。(敌势 ${enemy},我阵 ${teamFinal})`)
|
||||
if (win) {
|
||||
lines.push(`首战告捷:${w.rng.pick(WIN_DESC)}`)
|
||||
} else if (lose) {
|
||||
@@ -105,7 +108,8 @@ export function resolveEncounter(
|
||||
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))
|
||||
const woundAmt = Math.round((40 + w.rng.int(0, 25)) * form.retreatWound)
|
||||
c.health = Math.max(1, c.health - woundAmt)
|
||||
c.state = 'wounded'
|
||||
loss.push(`${c.name} 重伤`)
|
||||
}
|
||||
@@ -156,7 +160,8 @@ function itemName(id: string): string {
|
||||
export function resolveRaid(
|
||||
w: World,
|
||||
npcId: string,
|
||||
team: Character[]
|
||||
team: Character[],
|
||||
formation?: FormationId
|
||||
): EncounterResult {
|
||||
const npc = w.state.npcFamilies[npcId]
|
||||
const def = npcById(npcId)
|
||||
@@ -176,7 +181,8 @@ export function resolveRaid(
|
||||
team,
|
||||
kind: 'war',
|
||||
year: w.state.year,
|
||||
month: w.state.month
|
||||
month: w.state.month,
|
||||
formation
|
||||
})
|
||||
if (res.win) {
|
||||
npc.relation = Math.min(60, npc.relation + 25)
|
||||
|
||||
@@ -143,6 +143,20 @@ export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
|
||||
]
|
||||
}
|
||||
}
|
||||
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,
|
||||
@@ -262,6 +276,17 @@ export function eventRoll(w: World): void {
|
||||
fire(w, `ev-tournament-${tourneyNext}`)
|
||||
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}`
|
||||
@@ -304,7 +329,14 @@ export function fire(w: World, id: string): void {
|
||||
w.pendingEvent(id)
|
||||
}
|
||||
|
||||
export function applyEventChoice(w: World, eventId: string, optionIdx: number, squad?: string[]): void {
|
||||
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) {
|
||||
@@ -313,6 +345,7 @@ export function applyEventChoice(w: World, eventId: string, optionIdx: number, s
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -404,7 +437,7 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||||
if (!fam.techniques.includes(eff.addTech)) fam.techniques.push(eff.addTech)
|
||||
}
|
||||
if (eff.tournament) {
|
||||
runTournament(w, squad)
|
||||
runTournament(w, squad, eff.formation as never)
|
||||
}
|
||||
if (eff.apprentice?.build) {
|
||||
buildApprentice(w)
|
||||
@@ -419,6 +452,18 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||||
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.trib) {
|
||||
const c = w.state.members[eff.trib.memberId]
|
||||
if (c?.alive) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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'
|
||||
@@ -46,7 +47,8 @@ export function missionTick(w: World): void {
|
||||
team: squad,
|
||||
kind: 'scout',
|
||||
year: w.state.year,
|
||||
month: w.state.month
|
||||
month: w.state.month,
|
||||
formation: m.formation as FormationId | undefined
|
||||
})
|
||||
const line = res.win ? '战而胜之,征程继续!' : res.draw ? '僵持之后双方罢手,队伍休整再进。' : '不敌,只得暂避锋芒。'
|
||||
m.log.push(line)
|
||||
@@ -117,7 +119,7 @@ export function canSendMission(w: World, def: MissionDef, members: string[]): bo
|
||||
return w.state.missions.filter((m) => !m.done).length < 3
|
||||
}
|
||||
|
||||
export function sendMission(w: World, defId: string, members: string[]): boolean {
|
||||
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 = {
|
||||
@@ -129,7 +131,8 @@ export function sendMission(w: World, defId: string, members: string[]): boolean
|
||||
stage: 0,
|
||||
stageMonth: 0,
|
||||
log: [`冬衣已备,饯行酒干,众人于 ${w.state.year} 年 ${w.state.month} 月出发。`],
|
||||
done: false
|
||||
done: false,
|
||||
formation
|
||||
}
|
||||
members.forEach((id) => {
|
||||
const c = w.memberById(id)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ENEMIES, EnemyDef } from '../../data/secrets'
|
||||
import { resolveEncounter } from './combat'
|
||||
import { rankPower } from './events'
|
||||
import { resolveBreakthrough } from './cultivation'
|
||||
import { FormationId } from '../../data/formations'
|
||||
|
||||
export const SECTS = ['云栖宗', '太谷书院', '玄微剑阁', '丹霞洞天']
|
||||
|
||||
@@ -19,7 +20,7 @@ export function gateEnemy(idx: number, year: number): EnemyDef {
|
||||
}
|
||||
}
|
||||
|
||||
export function runTournament(w: World, squad?: string[]): void {
|
||||
export function runTournament(w: World, squad?: string[], formation?: FormationId): void {
|
||||
if (!w.sysEnabled('tournament')) {
|
||||
w.log('info', '太虚大比:赛事未启。')
|
||||
return
|
||||
@@ -52,7 +53,8 @@ export function runTournament(w: World, squad?: string[]): void {
|
||||
team: aliveTeam,
|
||||
kind: 'scout',
|
||||
year: s.year,
|
||||
month: s.month
|
||||
month: s.month,
|
||||
formation
|
||||
})
|
||||
if (!res.win) break
|
||||
wins++
|
||||
|
||||
Reference in New Issue
Block a user