v0.1.11: 全源码审计 + 缺陷歼灭 + UI/UX 打磨

【引擎审计修复】
- P0:大比 once(原整年刷 12 场、奖励通胀);flag 年份键 3 年剪枝(长线膨胀)
- P1:飞升离世累加 feishengCount(飞升之资称号可达);圣者护族接消费(原死代码);
  大比/传薪挂能力卡门控;pending 抢渡劫时不再重设
- P2:version 硬编码统一 0.1.11;yearaxis 飞升归类;MonumentList 挂载

【UI 审计修复】
- P0:已故成员卡显示「已殁」(原秀活动 status);空队迎战禁用+提示(原静默失效)
- P1:指婚候选去截断(滚动列表);终局出口(回卷/主菜单按钮);saveNow 反馈;
  事件来临写入 feed(k-event 使用);遣队「最强 N 人/清空」一键;寻衅确认+联姻原因提示;
  未建建筑视觉 uc 类
- P2:speed 死代码;Modal 分层 z-index;FamilyPanel 在世/先人分隔

【UX 三件套】
- 警讯徽章(瓶颈/重伤/在外/媒缘/孤嗣之忧)顶栏一屏秒答「我该干嘛」
- 首启引导任务条(四步目标 + 完成推进,可跳不强制)
- 顶栏丹药瘦身(4 资源);数字千/万化;瓶颈卡红框高亮

【测试】905→923(urgency/guide/format 三模块 + 审计修复回归 6 项)
金钟罩三档重固化(0.1.11),typecheck/build 通过
This commit is contained in:
2026-08-23 11:30:16 +08:00
parent 586fa643b1
commit 9bcb5d6af8
25 changed files with 417 additions and 28 deletions
+11
View File
@@ -0,0 +1,11 @@
export function fmt(n: number): string {
const abs = Math.abs(n)
if (abs >= 10000) return `${(n / 10000).toFixed(1)}`
if (abs >= 1000 && abs < 10000) {
// 1.2千
const t = Math.floor(abs / 1000)
const rem = Math.floor((abs % 1000) / 100)
return rem > 0 ? `${n < 0 ? '-' : ''}${t}.${rem}` : `${n < 0 ? '-' : ''}${t}`
}
return String(n)
}
+33
View File
@@ -0,0 +1,33 @@
import type { World } from '../engine/world'
export interface GuideStep {
id: string
title: string
hint: string
reward: string
}
export const GUIDE_STEPS: GuideStep[] = [
{ id: 'intro', title: '认识你的族人', hint: '点开「宗族」页中任意成员,看看他的灵根、境界与禀性。', reward: '感悟 +20 灵石' },
{ id: 'cultivate', title: '点一人闭关', hint: '在成员详情中点「闭关」,让他专心致志地修行。', reward: '修为小成' },
{ id: 'build', title: '营建一项产业', hint: '到「领地」页营建一座【灵田】或【坊市】,让家中有进项。', reward: '家族起步' },
{ id: 'progress', title: '推进一个月', hint: '点顶部「推进一月」,看族人锻、金堂响、家书至。', reward: '正式开族' }
]
export interface GuideState {
step: number
done: boolean
}
export function computeGuide(w: World): GuideState {
const step = w.state.family.flag['guideStep'] as number | undefined ?? 0
return { step, done: step >= GUIDE_STEPS.length }
}
export function guideAdvance(w: World, targetStep: number): number {
const cur = w.state.family.flag['guideStep'] as number | undefined ?? 0
if (targetStep > cur) {
w.state.family.flag['guideStep'] = Math.min(GUIDE_STEPS.length, targetStep)
}
return w.state.family.flag['guideStep'] as number
}
+25
View File
@@ -0,0 +1,25 @@
import type { World } from '../engine/world'
export interface Urgency {
blockedCount: number
injuredCount: number
missionCount: number
marriageReady: number
noHeir: boolean
headFrail: boolean
}
export function computeUrgency(w: World): Urgency {
const alive = w.aliveMembers()
const blockedCount = alive.filter((c) => c.realmProgress >= 100 && c.realm.major !== 'spirit').length
const injuredCount = alive.filter((c) => c.state === 'wounded' || c.health < 30).length
const missionCount = w.state.missions.filter((m) => !m.done).length
const marriageReady = alive.filter(
(c) => c.state !== 'expedition' && c.state !== 'apprentice' && (!c.spouseId || w.isWidowed(c)) && w.ageOf(c) >= 16 && w.ageOf(c) <= 46
).length
const head = w.state.members[w.state.family.headId]
const headFrail = !!head && head.alive && w.ageOf(head) > 55
const adultHeir = alive.some((c) => c.id !== head?.id && w.ageOf(c) >= 16)
const noHeir = !adultHeir || !head?.alive
return { blockedCount, injuredCount, missionCount, marriageReady, noHeir, headFrail }
}
+1 -1
View File
@@ -36,7 +36,7 @@ export function yearAxis(s: GameState, decadeSize = 10): AxisCell[] {
: e.category === 'birth'
? null
: e.category === 'death'
? '殇'
? /飞升/.test(e.text) ? '飞升' : '殇'
: e.category === 'marriage'
? '婚'
: e.category === 'battle'
+1 -1
View File
@@ -136,7 +136,7 @@ export class GameFacade {
about(): { title: string; version: string; modules: number; systems: number; plugins: number; packFingerprint: string } {
return {
title: '仙途家族志',
version: '0.1.8',
version: '0.1.11',
modules: this.world.systemList().length,
systems: this.world.systemList().filter((s) => s.enabled).length,
plugins: this.world.pluginList().length,
+4 -2
View File
@@ -63,6 +63,7 @@ export function dynamicEventFor(id: string, w?: World): EventDef | undefined {
name: '太虚大比',
category: 'major',
weight: 0,
once: true,
text: `太虚仙盟五年一会,新一期大比于祖庭开擂。观礼之众云集,百族竞锋——可遣嫡系赴赛,亦可称病让席。`,
options: [
{ label: '点将赴赛', hint: '战三关,位次定声望', eff: { tournament: true, flag: { [`tournamentYear-${year}`]: true } } },
@@ -321,7 +322,7 @@ export function eventRoll(w: World): void {
// 五年一会的太虚大比(该年任一时刻优先;其他年次要事件避让)
const tourneyNext = Math.ceil(s.year / 5) * 5
if (s.year >= 10 && s.year === tourneyNext && !s.completedEvents.includes(`ev-tournament-${tourneyNext}`)) {
if (s.year >= 10 && s.year === tourneyNext && w.sysEnabled('tournament') && !s.completedEvents.includes(`ev-tournament-${tourneyNext}`)) {
const tk = `tourneyPending-${tourneyNext}`
if (w.state.pendingEvent) {
// 另有事件在档:加入等待队列(事件优先)——用 flag 标记,防重复入队
@@ -335,7 +336,7 @@ export function eventRoll(w: World): void {
return
}
// 名宿传薪(每八年一掷)
if (s.year % 8 === 4 && !s.completedEvents.includes('ev-legacypass')) {
if (s.year % 8 === 4 && w.sysEnabled('apprentice') && !s.completedEvents.includes('ev-legacypass')) {
const elder = w.aliveMembers().some((c) => w.ageOf(c) >= 66 && c.realm.major !== 'mortal')
if (elder) {
fire(w, 'ev-legacypass')
@@ -581,6 +582,7 @@ export function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
immortal.alive = false
immortal.deathYear = s2.year
immortal.deathCause = '云游飞升'
s2.stats.feishengCount = (s2.stats.feishengCount ?? 0) + 1
s2.family.flag['fengFeiBless'] = true
w.chronicle('event', `${immortal.name} 于月首孤身东去,驾鹤而飞。天地留一缕仙风,庇佑宗族。`, immortal.id, true)
w.log('good', `${immortal.name} 飞升而去,宗族蒙庇。`)
+11
View File
@@ -295,6 +295,17 @@ export class World {
s.finance.accum += stonesEnd - stonesStart
this.trackStats()
this.clampState()
this.pruneYearFlags()
}
/** 年度清理:剔除 3 年以前的年份前缀 flag 键(防长线膨胀) */
private pruneYearFlags(): void {
const fam = this.state.family
const cutoff = this.state.year - 3
for (const key of Object.keys(fam.flag)) {
const m = /^(auction-|prayerDone-|echoDone-|recruitDone-)(\d+)$/.exec(key)
if (m && Number(m[2]) < cutoff) delete fam.flag[key]
}
}
/** 月底统一数值钳制:修为/气血/库存/金钱永不越界 */