v0.1.1: 养成闭环 + 管理自主 + 存档安全
- 指婚/续弦:成员详情可主动找人结亲(血亲自动排除、鳏寡可再醮) - 装备 UI:法宝从府库装备/替换,战力实时反馈 - 御敌点将:劫掠事件可自选迎战阵容(默认 Top4 可改) - 宗祠祭祖:每两年一次,灵石150 → 声望+6/全族修为小升 - 回档时间轴:每季快照列表,一键回卷(回卷前自动保当前档) - 战报匣子:史书页全文战报留存可翻阅 - 年度族簿纸笺:每年初弹收支/人丁/战力简报 - 媒人提示条 + 寻衅一年冷却 + 出生率与劫掠频率微调 - 单测 19→29(storage mock roundtrip/回档/指婚血亲/祭祖/寻衅/装备/账本)
This commit is contained in:
@@ -73,7 +73,10 @@ export function createWorldState(opts: NewGameOptions): GameState {
|
||||
eventQueue: [],
|
||||
completedEvents: [],
|
||||
flags: {},
|
||||
totalTicks: 0
|
||||
totalTicks: 0,
|
||||
finance: { accum: 0 },
|
||||
yearStats: { births: 0, deaths: 0 },
|
||||
yearlyReports: []
|
||||
}
|
||||
|
||||
const w = new World(state, [])
|
||||
|
||||
@@ -12,7 +12,7 @@ export function diplomacyTick(w: World): void {
|
||||
}
|
||||
if (npc.relation < -50) {
|
||||
const last = (w.state.family.flag[`raidCD-${npc.id}`] as number | undefined) ?? 0
|
||||
if (s.year - last >= 2 && w.rng.chance(0.045)) {
|
||||
if (s.year - last >= 2 && w.rng.chance(0.03)) {
|
||||
fire(w, `ev-raid-${npc.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export function fire(w: World, id: string): void {
|
||||
w.pendingEvent(id)
|
||||
}
|
||||
|
||||
export function applyEventChoice(w: World, eventId: string, optionIdx: number): void {
|
||||
export function applyEventChoice(w: World, eventId: string, optionIdx: number, squad?: string[]): void {
|
||||
const s = w.state
|
||||
const def = findEvent(eventId)
|
||||
if (!def) {
|
||||
@@ -116,12 +116,14 @@ export function applyEventChoice(w: World, eventId: string, optionIdx: number):
|
||||
}
|
||||
const opt = def.options[optionIdx]
|
||||
if (opt) {
|
||||
applyEffect(w, opt.eff)
|
||||
applyEffect(w, opt.eff, squad)
|
||||
if (def.once && !s.completedEvents.includes(def.id)) s.completedEvents.push(def.id)
|
||||
}
|
||||
s.pendingEvent = undefined
|
||||
}
|
||||
|
||||
export type OptionSquadPicker = (w: World) => { pool: string[]; initial: string[] }
|
||||
|
||||
// ---------------- effects ----------------
|
||||
|
||||
function pickMember(w: World, spec: MemberEffect): Character | Character[] {
|
||||
@@ -160,7 +162,7 @@ export function rankPower(w: World, c: Character): number {
|
||||
return order.indexOf(c.realm.major) * 10 + c.realm.minor
|
||||
}
|
||||
|
||||
function applyEffect(w: World, eff: EffectDef): void {
|
||||
function applyEffect(w: World, eff: EffectDef, squad?: string[]): void {
|
||||
const s = w.state
|
||||
const fam = s.family
|
||||
|
||||
@@ -276,11 +278,14 @@ function applyEffect(w: World, eff: EffectDef): void {
|
||||
if (eff.raid) {
|
||||
const npc = s.npcFamilies[eff.raid.npcId]
|
||||
if (npc) {
|
||||
const team = w
|
||||
let team = w
|
||||
.aliveMembers()
|
||||
.filter((c) => w.ageOf(c) >= 16 && c.state !== 'expedition')
|
||||
.sort((a, b) => rankPower(w, b) - rankPower(w, a))
|
||||
.slice(0, 4)
|
||||
if (squad && squad.length > 0) {
|
||||
team = squad.map((id) => w.memberById(id)).filter((c) => c.alive && c.state !== 'expedition' && w.ageOf(c) >= 16)
|
||||
}
|
||||
if (team.length > 0) {
|
||||
resolveRaid(w, eff.raid.npcId, team)
|
||||
fam.flag[`raidCD-${eff.raid.npcId}`] = s.year
|
||||
|
||||
@@ -25,6 +25,7 @@ export function deathTick(w: World): void {
|
||||
c.deathYear = w.state.year
|
||||
const cause = age < 2 ? '幼夭' : c.health < 30 ? '伤势不治' : '寿元将尽'
|
||||
c.deathCause = cause
|
||||
w.state.yearStats.deaths += 1
|
||||
w.chronicle('death', `${c.name} 辞世,年 ${age}。${age < 2 ? '族人无不痛惜。' : c.health < 30 ? '临终前仍在牵挂家族。' : '族人焚香送别。'}`, c.id, true)
|
||||
w.log('bad', `${c.name}(${age}岁)${cause}。`)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export function yearStartMarriage(w: World): void {
|
||||
const s = w.state
|
||||
const fam = s.family
|
||||
const zongci = fam.buildings['zongci'] ?? 0
|
||||
const birthBase = 0.34 + zongci * 0.03 + (fam.difficulty === 'easy' ? 0.06 : fam.difficulty === 'hard' ? -0.06 : 0)
|
||||
const birthBase = 0.38 + zongci * 0.03 + (fam.difficulty === 'easy' ? 0.06 : fam.difficulty === 'hard' ? -0.06 : 0)
|
||||
|
||||
const couples = buildCouples(w)
|
||||
|
||||
@@ -30,9 +30,11 @@ export function yearStartMarriage(w: World): void {
|
||||
if (w.rng.chance(0.03)) {
|
||||
const twin = produceOffspring(w, { father, mother, generation: gen, bornYear: s.year, surname: fam.surname })
|
||||
w.addMember(twin, father, mother)
|
||||
w.state.yearStats.births += 1
|
||||
note += ` 双生之喜!双子名唤${twin.name}。`
|
||||
}
|
||||
w.chronicle('birth', note, first.id, true)
|
||||
w.state.yearStats.births += 1
|
||||
w.log('good', `${fam.surname}家诞下新丁:${first.name}。`)
|
||||
}
|
||||
|
||||
@@ -49,6 +51,7 @@ export function yearStartMarriage(w: World): void {
|
||||
child.spouseHouse = house
|
||||
w.addMember(child)
|
||||
w.chronicle('birth', `${member.name}自${house}携幼子归来,名唤${child.name}。`, child.id, true)
|
||||
w.state.yearStats.births += 1
|
||||
w.log('info', `${member.name} 领着小辈回门投亲。`)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
GameState,
|
||||
Id,
|
||||
LogItem,
|
||||
Realm
|
||||
Realm,
|
||||
YearlyReport
|
||||
} from '../types/domain'
|
||||
import { Rng } from '../core/rng'
|
||||
import { BUILDINGS } from '../data/buildings'
|
||||
@@ -27,6 +28,7 @@ export interface WorldEventBus {
|
||||
onBattle(log: BattleLog): void
|
||||
onPendingEvent(id: string): void
|
||||
onGameOver(reason: string, year: number): void
|
||||
onYearPaper?(entry: YearlyReport): void
|
||||
}
|
||||
|
||||
export class World {
|
||||
@@ -101,6 +103,7 @@ export class World {
|
||||
|
||||
advanceMonth(): void {
|
||||
const s = this.state
|
||||
const stonesStart = s.family.stones
|
||||
s.month++
|
||||
if (s.month > 12) {
|
||||
s.month = 1
|
||||
@@ -119,10 +122,29 @@ export class World {
|
||||
diplomacyTick(this)
|
||||
}
|
||||
this.checkHead()
|
||||
const stonesEnd = s.family.stones
|
||||
s.finance.accum += stonesEnd - stonesStart
|
||||
}
|
||||
|
||||
private yearStart(): void {
|
||||
yearStartMarriage(this)
|
||||
const rep = this.state.family.reputation
|
||||
const power = this.familyPower()
|
||||
const report: YearlyReport = {
|
||||
year: this.state.year - 1,
|
||||
nets: this.state.finance.accum,
|
||||
births: this.state.yearStats.births,
|
||||
deaths: this.state.yearStats.deaths,
|
||||
rep,
|
||||
power
|
||||
}
|
||||
this.state.yearlyReports.push(report)
|
||||
if (this.state.yearlyReports.length > 80) {
|
||||
this.state.yearlyReports.shift()
|
||||
}
|
||||
this.state.finance.accum = 0
|
||||
this.state.yearStats = { births: 0, deaths: 0 }
|
||||
this.out.forEach((o) => o.onYearPaper?.(report))
|
||||
}
|
||||
|
||||
reputationDrift(): void {
|
||||
@@ -297,6 +319,68 @@ export class World {
|
||||
return Math.round(top * bonus)
|
||||
}
|
||||
|
||||
ancestralRite(): boolean {
|
||||
const fam = this.state.family
|
||||
const last = (fam.flag['lastRiteYear'] as number | undefined) ?? -999
|
||||
if (this.state.year - last < 2) return false
|
||||
if (fam.stones < 150) return false
|
||||
fam.stones -= 150
|
||||
fam.flag['lastRiteYear'] = this.state.year
|
||||
fam.reputation += 6
|
||||
for (const c of this.aliveMembers()) {
|
||||
c.realmProgress = Math.min(100, c.realmProgress + 3)
|
||||
c.health = Math.min(100, c.health + 5)
|
||||
}
|
||||
const headName = this.head()?.name ?? '家主'
|
||||
this.chronicle('event', `${headName} 斋戒三日后开祠祭祖,先祖显灵赐福。`, undefined, true)
|
||||
this.log('good', `祭祖!族中众人灵力温润,族人受益。`)
|
||||
return true
|
||||
}
|
||||
|
||||
tauntNpc(npcId: string): boolean {
|
||||
const fam = this.state.family
|
||||
const npc = this.state.npcFamilies[npcId]
|
||||
if (!npc) return false
|
||||
const last = (fam.flag[`tauntCD-${npcId}`] as number | undefined) ?? 0
|
||||
if (this.state.year - last < 1) return false
|
||||
fam.flag[`tauntCD-${npcId}`] = this.state.year
|
||||
npc.relation = Math.max(-100, npc.relation - 20)
|
||||
this.log('bad', `指桑骂槐,${npc.name}记恨于心。`)
|
||||
return true
|
||||
}
|
||||
|
||||
isWidowed(member: Character): boolean {
|
||||
if (!member.spouseId) return false
|
||||
const sp = this.state.members[member.spouseId]
|
||||
return !!sp && !sp.alive
|
||||
}
|
||||
|
||||
marriageCandidatesOf(id: Id): Character[] {
|
||||
const me = this.memberById(id)
|
||||
const meG = me.gender
|
||||
return this.aliveMembers()
|
||||
.filter((c) => c.gender !== meG && c.state !== 'expedition')
|
||||
.filter((c) => w2age(this, c) >= 16 && w2age(this, c) <= 46)
|
||||
.filter((c) => !c.spouseId || this.isWidowed(c))
|
||||
.filter(
|
||||
(c) =>
|
||||
!(me.fatherId && me.fatherId === c.fatherId) &&
|
||||
!me.children.includes(c.id) &&
|
||||
!c.children.includes(me.id) &&
|
||||
me.id !== c.id
|
||||
)
|
||||
}
|
||||
|
||||
canMarry(memberId: Id): boolean {
|
||||
const me = this.memberById(memberId)
|
||||
if (!me.alive) return false
|
||||
return !me.spouseId || this.isWidowed(me)
|
||||
}
|
||||
|
||||
marryTo(aId: Id, bId: Id): boolean {
|
||||
return arrangeWeddingPublic(this, aId, bId)
|
||||
}
|
||||
|
||||
static create(opts: { seed: string; surname: string; familyName: string; motto: string; difficulty: 'easy' | 'normal' | 'hard' }): World {
|
||||
const state = createWorldState(opts)
|
||||
return new World(state)
|
||||
@@ -307,6 +391,25 @@ export function makeWorldFromSave(state: GameState): World {
|
||||
return new World(state, [])
|
||||
}
|
||||
|
||||
function w2age(w: World, c: Character): number {
|
||||
return w.ageOf(c)
|
||||
}
|
||||
|
||||
function arrangeWeddingPublic(w: World, aId: Id, bId: Id): boolean {
|
||||
const a = w.memberById(aId)
|
||||
const b = w.memberById(bId)
|
||||
if (!a.alive || !b.alive || a.spouseId || b.spouseId) return false
|
||||
if (a.gender === b.gender) return false
|
||||
if (a.fatherId && a.fatherId === b.fatherId) return false
|
||||
a.spouseId = b.id
|
||||
b.spouseId = a.id
|
||||
const aAge = w.ageOf(a)
|
||||
const bAge = w.ageOf(b)
|
||||
w.chronicle('marriage', `${a.name}(${aAge})与${b.name}(${bAge})拜堂成亲。`, a.id, true)
|
||||
w.log('good', `${a.name} 与 ${b.name} 结为连理。`)
|
||||
return true
|
||||
}
|
||||
|
||||
export function applyChoice(world: World, eventId: string, optionIdx: number): void {
|
||||
applyEventChoice(world, eventId, optionIdx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user