v0.1.0: 仙途家族志 · Chronicle of the Immortal Clan

家族模拟器首版:修仙/经营/战斗/外交/叙事全套系统
- Electron + React + TS + MetonaSqlark(aria+OPFS)
- 水墨中国风 UI
- 引擎种子随机、确定性可回放
- 19 项单元测试、端到端冒烟验证
This commit is contained in:
Hermes Agent
2026-08-23 00:04:16 +08:00
commit 2f81b29878
69 changed files with 14305 additions and 0 deletions
@@ -0,0 +1,112 @@
import { World } from '../world'
import { Character } from '../../types/domain'
import { produceOffspring } from './cultivation'
import { yearGrowth } from './diplomacy'
import { MALE_GIVEN, FEMALE_GIVEN } from '../../core/names'
export function yearStartMarriage(w: World): void {
yearGrowth(w)
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 couples = buildCouples(w)
// 族内夫妇
for (const couple of couples.internal) {
const [a, b] = couple
const father = a.gender === 'male' ? a : b
const mother = a.gender === 'male' ? b : a
const fatherAge = w.ageOf(father)
const motherAge = w.ageOf(mother)
if (fatherAge < 18 || fatherAge > 52 || motherAge < 16 || motherAge > 46) continue
const p = birthBase * (0.75 + mother.physique * 0.05)
if (!w.rng.chance(p)) continue
const gen = Math.max(father.generation, mother.generation) + 1
const first = produceOffspring(w, { father, mother, generation: gen, bornYear: s.year, surname: fam.surname })
w.addMember(first, father, mother)
let note = `${fam.surname}氏新增一员,名唤${first.name},生年 ${s.year}`
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)
note += ` 双生之喜!双子名唤${twin.name}`
}
w.chronicle('birth', note, first.id, true)
w.log('good', `${fam.surname}家诞下新丁:${first.name}`)
}
// 联姻外孙来投
for (const member of Object.values(s.members)) {
if (!member.alive || !member.spouseHouse) continue
if (member.gender !== 'female') continue
const age = w.ageOf(member)
if (age < 18 || age > 44) continue
if (!w.rng.chance(0.16)) continue
const gen = member.generation + 1
const house = member.spouseHouse
const child = produceOffspring(w, { father: null, mother: null, generation: gen, bornYear: s.year, surname: fam.surname })
child.spouseHouse = house
w.addMember(child)
w.chronicle('birth', `${member.name}${house}携幼子归来,名唤${child.name}`, child.id, true)
w.log('info', `${member.name} 领着小辈回门投亲。`)
}
// 媒人撮合族内婚(含续弦与再醮)
const isWidowed = (c: Character): boolean => {
if (!c.spouseId) return false
const sp = w.state.members[c.spouseId]
return !!sp && !sp.alive
}
const men = w
.aliveMembers()
.filter((c) => c.gender === 'male' && c.state !== 'expedition' && (isWidowed(c) || !c.spouseId))
.filter((c) => {
const age = w.ageOf(c)
return age >= 18 && age <= 48
})
const women = w
.aliveMembers()
.filter((c) => c.gender === 'female' && c.state !== 'expedition' && (isWidowed(c) || !c.spouseId))
.filter((c) => {
const age = w.ageOf(c)
return age >= 16 && age <= 42
})
for (const m of men) {
if (w.rng.chance(0.28) && women.length > 0) {
const candidate = women.filter(
(x) => !(x.fatherId && x.fatherId === m.fatherId) && x !== m && !x.children.includes(m.id) && !m.children.includes(x.id)
)
if (candidate.length === 0) continue
const bride = w.rng.pick(candidate)
m.spouseId = bride.id
bride.spouseId = m.id
w.chronicle('marriage', `${m.name}${bride.name}缔结连理。`, m.id, true)
w.log('info', `${m.name}${bride.name} 成婚。`)
const idx = women.indexOf(bride)
if (idx >= 0) women.splice(idx, 1)
}
}
fam.generation = Math.max(
fam.generation,
...Object.values(s.members).filter((c) => c.alive).map((c) => c.generation)
)
}
function buildCouples(w: World): { internal: [Character, Character][]; cross: Character[] } {
const internal: [Character, Character][] = []
const cross: Character[] = []
for (const member of Object.values(w.state.members)) {
if (!member.alive || !member.spouseId) continue
const spouse = w.state.members[member.spouseId]
if (!spouse?.alive) continue
const key = [member.id, spouse.id].sort().join('|')
if (internal.some(([a, b]) => [a.id, b.id].sort().join('|') === key)) continue
internal.push([member, spouse])
}
for (const member of Object.values(w.state.members)) {
if (member.alive && member.spouseHouse) cross.push(member)
}
return { internal, cross }
}