Files
ChronicleOfTheImmortalClan/src/renderer/game/engine/pcgen.ts
T
thzxx adb22f0558 v0.1.0: 仙途家族志 · Chronicle of the Immortal Clan
家族模拟器首版:修仙/经营/战斗/外交/叙事全套系统
- Electron + React + TS + MetonaSqlark(aria+OPFS)
- 水墨中国风 UI
- 引擎种子随机、确定性可回放
- 19 项单元测试、端到端冒烟验证
2026-08-23 00:04:16 +08:00

121 lines
3.9 KiB
TypeScript

import { Character, Element, Gender, Realm, RealmMajor } from '../types/domain'
import { Rng } from '../core/rng'
import { ELEMENT_LIST, ROOT_GRADES } from '../data/elements'
import { MAJORS } from '../data/realms'
import { TRAIT_POOL, TRAITS } from '../data/traits'
export function rollRoots(rng: Rng, parents?: { m?: Character; f?: Character }): { grade: number; primary: Element; secondary: Element[] } {
let grade: number
if (parents && parents.m && parents.f) {
const mix = (parents.m.roots.grade + parents.f.roots.grade) / 2
const roll = rng.next()
if (roll < 0.3) grade = Math.round(mix)
else if (roll < 0.8) grade = Math.round(mix) + 1
else grade = Math.round(mix) - 1
if (rng.chance(0.15)) grade = Math.min(5, grade + 2)
} else {
const total = Object.entries(ROOT_GRADES).reduce((s, [k, v]) => s + v.drawWeight, 0)
let r = rng.next() * total
grade = 1
for (const [k, v] of Object.entries(ROOT_GRADES)) {
r -= v.drawWeight
if (r <= 0) {
grade = Number(k)
break
}
}
}
grade = Math.max(0, Math.min(5, grade))
const primaryCandidate = parents && (parents.m || parents.f)
? [parents.m!.roots.primary, parents.f!.roots.primary]
: ELEMENT_LIST
const primary = rng.pick(primaryCandidate)
const secondaryCount = grade >= 3 ? rng.int(1, 2) : grade === 2 ? rng.int(0, 1) : 0
const rest = ELEMENT_LIST.filter((e) => e !== primary)
const secondary = rng.shuffle(rest).slice(0, secondaryCount)
return { grade, primary, secondary }
}
export function rollPersonality(rng: Rng): string[] {
const n = rng.chance(0.5) ? 2 : 1
return rng.shuffle([...TRAIT_POOL]).slice(0, n)
}
export function rollAttributes(rng: Rng, base: number, variance: number): number {
const v = Math.round(base + rng.between(-variance, variance))
return Math.max(1, Math.min(10, v))
}
export function calcLifespan(major: RealmMajor, physique: number): number {
const base = MAJORS[major].lifespan
return Math.round(base * (0.9 + physique * 0.02))
}
export function newCharacter(
rng: Rng,
opts: {
name: string
gender: Gender
generation: number
bornYear: number
age: number
mother?: Character
father?: Character
realm?: Realm
isHead?: boolean
isFounder?: boolean
fortuneBase?: number
}
): Character {
const realm: Realm = opts.realm ?? { major: 'mortal', minor: 0 }
const roots = rollRoots(rng, opts.father || opts.mother ? { m: opts.father, f: opts.mother } : undefined)
const levelBonus = MAJORS[realm.major as RealmMajor].minorLayers > 0 ? realm.minor * 0.4 : 0
const perception = rollAttributes(rng, 4.5 + roots.grade * 0.8 + levelBonus * 0.25, 1.8)
const physique = rollAttributes(rng, 4 + roots.grade * 0.5 + levelBonus * 0.3, 1.8)
const mind = rollAttributes(rng, 4.5 + roots.grade * 0.3, 1.8)
const charm = rollAttributes(rng, 4.5, 2.2)
const fortune = rollAttributes(rng, (opts.fortuneBase ?? 5) + roots.grade * 0.4, 2.2)
return {
id: '',
name: opts.name,
gender: opts.gender,
generation: opts.generation,
children: [],
bornYear: opts.bornYear,
age: opts.age,
realm,
realmProgress: 0,
roots,
perception,
physique,
mind,
charm,
fortune,
traits: rollPersonality(rng),
state: 'idle',
health: 100,
alive: true,
isHead: opts.isHead,
isFounder: opts.isFounder
}
}
export function traitBonuses(character: Character): { exp: number; breakBonus: number; windBonus: number; charmBonus: number; priceMult: number } {
let exp = 0
let breakBonus = 0
let windBonus = 0
let charmBonus = 0
let priceMult = 0
for (const t of character.traits) {
const def = TRAITS[t]
if (!def) continue
exp += def.expBonus ?? 0
breakBonus += def.breakBonus ?? 0
windBonus += def.windBonus ?? 0
charmBonus += def.charmBonus ?? 0
priceMult += def.priceMult ?? 0
}
return { exp: 1 + exp, breakBonus, windBonus, charmBonus, priceMult }
}