v0.1.0: 仙途家族志 · Chronicle of the Immortal Clan
家族模拟器首版:修仙/经营/战斗/外交/叙事全套系统 - Electron + React + TS + MetonaSqlark(aria+OPFS) - 水墨中国风 UI - 引擎种子随机、确定性可回放 - 19 项单元测试、端到端冒烟验证
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user