refactor(0.1.14-P1): 内核归位——game/ 按引擎架构重排(零行为漂移)
结构(旧 game/core+engine → 新 engine/ 域): - engine/kernel/ 时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core) - engine/narrative/ legacy/报告/列传/谱系/年轴(原 core 叙事族) - engine/runtime/ World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统) - engine/sim/ Market(未来 WorldSim 同行) - 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层) 验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化) typecheck 0 error
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import type { World } from '../runtime/World'
|
||||
|
||||
/**
|
||||
* 统一时轮调度:所有系统以 phase 注册,由时钟按固定顺序驱动。
|
||||
* - 月度 phase:production / aging / cultivation / missions / events / diplomacy / epilogue
|
||||
* - 年首钩子:onYearStart(婚配 → 声望岁贡 → 岁末族簿)
|
||||
* 新增系统 = 注册一行,不再修改 advanceMonth 本体。
|
||||
*/
|
||||
|
||||
export type PhaseId =
|
||||
| 'production'
|
||||
| 'aging'
|
||||
| 'cultivation'
|
||||
| 'missions'
|
||||
| 'events'
|
||||
| 'diplomacy'
|
||||
| 'epilogue'
|
||||
|
||||
export type SystemHook = (w: World) => void
|
||||
|
||||
export interface PhaseStat {
|
||||
phase: PhaseId
|
||||
ms: number
|
||||
count: number
|
||||
}
|
||||
|
||||
export const PHASE_ORDER: PhaseId[] = [
|
||||
'production',
|
||||
'aging',
|
||||
'cultivation',
|
||||
'missions',
|
||||
'events',
|
||||
'diplomacy',
|
||||
'epilogue'
|
||||
]
|
||||
|
||||
export class GameClock {
|
||||
private monthly = new Map<PhaseId, SystemHook[]>()
|
||||
private yearly: SystemHook[] = []
|
||||
|
||||
register(phase: PhaseId, fn: SystemHook): () => void {
|
||||
const list = this.monthly.get(phase) ?? []
|
||||
list.push(fn)
|
||||
this.monthly.set(phase, list)
|
||||
return () => {
|
||||
const li = list.indexOf(fn)
|
||||
if (li >= 0) list.splice(li, 1)
|
||||
}
|
||||
}
|
||||
|
||||
onYearStart(fn: SystemHook): () => void {
|
||||
this.yearly.push(fn)
|
||||
return () => {
|
||||
const i = this.yearly.indexOf(fn)
|
||||
if (i >= 0) this.yearly.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
fireYearStart(w: World): void {
|
||||
for (const fn of this.yearly) fn(w)
|
||||
}
|
||||
|
||||
stepMonthly(w: World): PhaseStat[] {
|
||||
const report: PhaseStat[] = []
|
||||
for (const phase of PHASE_ORDER) {
|
||||
const t0 = performance.now()
|
||||
const fns = [...(this.monthly.get(phase) ?? [])]
|
||||
for (const fn of fns) fn(w)
|
||||
const ms = performance.now() - t0
|
||||
report.push({ phase, ms, count: fns.length })
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
subscriptionCount(): number {
|
||||
let n = 0
|
||||
for (const list of this.monthly.values()) n += list.length
|
||||
return n + this.yearly.length
|
||||
}
|
||||
|
||||
snapshot(): Array<{ phase: PhaseId; count: number }> {
|
||||
return PHASE_ORDER.map((phase) => ({ phase, count: (this.monthly.get(phase) ?? []).length }))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
export type FxKind = 'spark' | 'mist' | 'ripple' | 'blade' | 'pulse' | 'seasonShift' | 'drift'
|
||||
|
||||
export type FxMode = 'soft' | 'std' | 'full'
|
||||
|
||||
export interface FxEmit {
|
||||
kind: FxKind
|
||||
payload?: Record<string, number | string>
|
||||
at: number
|
||||
}
|
||||
|
||||
export interface FxBus {
|
||||
emit(e: FxEmit): void
|
||||
}
|
||||
|
||||
/** 竞态安全发射闸:单循环、队列不覆盖、档位过滤、reduced-motion 降级 */
|
||||
export class FxGate {
|
||||
private queue: FxEmit[] = []
|
||||
private draining = false
|
||||
mode: FxMode = 'std'
|
||||
preferReduced = false
|
||||
|
||||
constructor(private bus: FxBus) {}
|
||||
|
||||
setMode(m: FxMode): void {
|
||||
this.mode = m
|
||||
}
|
||||
|
||||
setReduced(r: boolean): void {
|
||||
this.preferReduced = r
|
||||
}
|
||||
|
||||
allowed(kind: FxKind): boolean {
|
||||
if (this.mode === 'soft') return kind === 'ripple' || kind === 'pulse' || kind === 'drift'
|
||||
if (this.mode === 'full') return true
|
||||
return kind !== 'blade' // std:火花/雾/涟漪/脉动,刀光仅 full
|
||||
}
|
||||
|
||||
emit(kind: FxKind, payload?: FxEmit['payload']): void {
|
||||
if (!this.allowed(kind) || this.preferReduced || this.draining) return
|
||||
this.queue.push({ kind, payload, at: Date.now() })
|
||||
this.drain()
|
||||
}
|
||||
|
||||
private drain(): void {
|
||||
if (this.draining) return
|
||||
this.draining = true
|
||||
// 单帧集中派发(不逐个 setTimeout,防止栈深+竞态覆盖)
|
||||
const batch = this.queue
|
||||
this.queue = []
|
||||
try {
|
||||
for (const e of batch) this.bus.emit(e)
|
||||
} finally {
|
||||
this.draining = false
|
||||
}
|
||||
}
|
||||
|
||||
pending(): number {
|
||||
return this.queue.length + (this.draining ? 1 : 0)
|
||||
}
|
||||
|
||||
/** 纯逻辑:资源差量(防负值与 NaN,供漂字用) */
|
||||
static deltas(prev: Record<string, number>, next: Record<string, number>): Record<string, number> {
|
||||
const out: Record<string, number> = {}
|
||||
const keys = new Set([...Object.keys(prev), ...Object.keys(next)])
|
||||
for (const k of keys) {
|
||||
const a = prev[k] ?? 0
|
||||
const b = next[k] ?? 0
|
||||
if (Number.isFinite(a) && Number.isFinite(b) && b !== a) out[k] = b - a
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { World } from '../runtime/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
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export const SURNAME_POOL = [
|
||||
'林', '苏', '沈', '谢', '顾', '萧', '叶', '江', '秦', '裴',
|
||||
'柳', '陆', '云', '姜', '晏', '楚', '洛', '许', '宋', '薛',
|
||||
'韩', '白', '纪', '容', '卫', '燕', '温', '孟', '阮',
|
||||
'池', '岑', '傅', '虞', '尹', '霍', '曲', '齐'
|
||||
]
|
||||
|
||||
export const MALE_GIVEN = [
|
||||
'长青', '天行', '玄机', '惊羽', '逐月', '凌风', '无涯', '浩然', '慕白', '景行',
|
||||
'玄青', '清尘', '衡之', '元白', '既白', '拾遗', '忘机', '观澜', '承影', '风眠',
|
||||
'听澜', '常宁', '若拙', '怀瑾', '望舒', '断岳', '疏影', '临渊', '未央', '扶摇',
|
||||
'云疏', '星河', '知竹', '不归', '须眉', '长恨', '夜阑', '青崖', '子衿', '方舟',
|
||||
'晓风', '修远', '行舟', '暮雪', '佐卿', '御风', '惊蛰', '白露', '秋声', '冬青'
|
||||
]
|
||||
|
||||
export const FEMALE_GIVEN = [
|
||||
'青鸾', '月瑶', '星若', '清欢', '婉芯', '若雪', '凝霜', '沐婉', '疏桐', '归鹤',
|
||||
'临夏', '小满', '云笙', '芷若', '洛神', '巧儿', '霜华', '雨薇', '兰心', '扶摇',
|
||||
'晴雪', '芷凝', '吟月', '采薇', '爱雪', '听雨', '素问', '千雪', '疏影', '知微',
|
||||
'曼卿', '昭容', '碧衣', '青眉', '玲珑', '止水', '言心', '灼华', '云鬓', '惊鸿',
|
||||
'晚棠', '夜莺', '南絮', '海棠', '锦瑟', '泠泠', '天心', '妙音', '含章', '碧瑶'
|
||||
]
|
||||
|
||||
export const MISSION_DESCRIPTORS = [
|
||||
'顽劣', '沉静', '木讷', '机敏', '温厚', '孤高', '谦逊', '豪迈', '精细', '洒脱'
|
||||
]
|
||||
|
||||
export function randomSurname(rng: { pick<T>(a: T[]): T }): string {
|
||||
return rng.pick(SURNAME_POOL)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { GameClock, SystemHook } from './clock'
|
||||
import { DataPack } from '../../data/registry'
|
||||
import { EventDef } from '../../data/events'
|
||||
import type { World } from '../runtime/World'
|
||||
|
||||
export type PluginKind = 'system' | 'data' | 'events' | 'content'
|
||||
|
||||
export interface PluginHookGuard {
|
||||
onLoad?(): void
|
||||
onDisable?(): void
|
||||
onEnable?(): void
|
||||
onUninstall?(): void
|
||||
}
|
||||
|
||||
export interface CotycPlugin {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
author?: string
|
||||
description: string
|
||||
kind: PluginKind
|
||||
dependencies?: string[]
|
||||
conflicts?: string[]
|
||||
install(ctx: PluginContext): void
|
||||
uninstall?(ctx: PluginContext): void
|
||||
hooks?: PluginHookGuard
|
||||
protected?: boolean
|
||||
}
|
||||
|
||||
export interface PluginContext {
|
||||
world: World
|
||||
clock: GameClock
|
||||
register: (phase: Parameters<GameClock['register']>[0], fn: SystemHook) => () => void
|
||||
onYearStart: (fn: SystemHook) => () => void
|
||||
addCapability: (cap: { id: string; name: string; version: string; desc: string }) => void
|
||||
removeCapability: (id: string) => void
|
||||
enableCapability: (id: string, enabled: boolean) => void
|
||||
overridePack: (partial: Partial<DataPack>) => void
|
||||
resetPack: () => void
|
||||
addEventPool: (id: string, events: EventDef[]) => void
|
||||
removeEventPool: (id: string) => void
|
||||
}
|
||||
|
||||
export interface PluginStatus {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
kind: PluginKind
|
||||
installed: boolean
|
||||
enabled: boolean
|
||||
protected: boolean
|
||||
description: string
|
||||
dependencies?: string[]
|
||||
}
|
||||
|
||||
export type PluginChange = { id: string; action: 'install' | 'remove' | 'enable' | 'disable' }
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { RngState } from '../../types/domain'
|
||||
|
||||
function xmur3(str: string): () => number {
|
||||
let h = 1779033703 ^ str.length
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
h = Math.imul(h ^ str.charCodeAt(i), 3432918353)
|
||||
h = (h << 13) | (h >>> 19)
|
||||
}
|
||||
return function () {
|
||||
h = Math.imul(h ^ (h >>> 16), 2246822507)
|
||||
h = Math.imul(h ^ (h >>> 13), 3266489909)
|
||||
return (h ^= h >>> 16) >>> 0
|
||||
}
|
||||
}
|
||||
|
||||
export function seedToRng(seed: string): RngState {
|
||||
const s = xmur3(seed)
|
||||
let a = s()
|
||||
let b = s()
|
||||
let c = s()
|
||||
let d = s()
|
||||
if (a === 0 && b === 0 && c === 0 && d === 0) d = 0x9e3779b9
|
||||
return { a, b, c, d }
|
||||
}
|
||||
|
||||
const U32 = 4294967296
|
||||
|
||||
export class Rng {
|
||||
state: RngState
|
||||
nextCount = 0
|
||||
|
||||
constructor(state: RngState) {
|
||||
this.state = { ...state }
|
||||
}
|
||||
|
||||
getState(): RngState {
|
||||
return { ...this.state }
|
||||
}
|
||||
|
||||
next(): number {
|
||||
this.nextCount++
|
||||
const s = this.state
|
||||
const t = ((s.a + s.b + s.d) | 0) >>> 0
|
||||
s.d = (s.d + 1) | 0
|
||||
s.a = (s.b ^ (s.b >>> 9)) >>> 0
|
||||
s.b = (s.c + (s.c << 3)) | 0
|
||||
s.c = ((s.c << 21) | (s.c >>> 11)) >>> 0
|
||||
s.c = (s.c + t) | 0
|
||||
return t / U32
|
||||
}
|
||||
|
||||
int(min: number, max: number): number {
|
||||
return Math.floor(this.next() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
pick<T>(arr: T[]): T {
|
||||
if (arr.length === 0) throw new Error('Rng.pick: 空数组无可选项')
|
||||
return arr[this.int(0, arr.length - 1)]
|
||||
}
|
||||
|
||||
chance(p: number): boolean {
|
||||
return this.next() < p
|
||||
}
|
||||
|
||||
shuffle<T>(arr: T[]): T[] {
|
||||
const a = [...arr]
|
||||
if (a.length <= 1) return a
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = this.int(0, i)
|
||||
const t = a[i]
|
||||
a[i] = a[j]
|
||||
a[j] = t
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
between(min: number, max: number): number {
|
||||
return this.next() * (max - min) + min
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全工程统一随机收集器:
|
||||
* - rollSeed:开局播种(crypto 级)
|
||||
* - audioNoise:音效白噪(种子独立,永不干扰引擎序列)
|
||||
* - engineRng:引擎世界随机(World.rng 实例,见 world.ts)
|
||||
*/
|
||||
export class RngHub {
|
||||
private static audioRng = new Rng(seedToRng('cotyc-audio-dither-v1'))
|
||||
|
||||
static rollSeed(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return `seed-${crypto.randomUUID()}`
|
||||
}
|
||||
return `seed-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9)}`
|
||||
}
|
||||
|
||||
static audioNoise01(): number {
|
||||
return RngHub.audioRng.next()
|
||||
}
|
||||
}
|
||||
|
||||
export function rngAudit(rng: RigRng): { total: number } {
|
||||
return { total: rng.nextCount }
|
||||
}
|
||||
|
||||
type RigRng = Rng
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Season } from '../../data/season'
|
||||
|
||||
export const TIDES: string[] = [
|
||||
'立春', '雨水', '惊蛰', '春分', '清明', '谷雨',
|
||||
'立夏', '小满', '芒种', '夏至', '小暑', '大暑',
|
||||
'立秋', '处暑', '白露', '秋分', '寒露', '霜降',
|
||||
'立冬', '小雪', '大雪', '冬至', '小寒', '大寒'
|
||||
]
|
||||
|
||||
/** 季节 → 节气(每月两个节气:tideOf(month, phase) 返回 0..1 ) */
|
||||
export function tideOf(month: number, phase: 0 | 1): string {
|
||||
const idx = Math.min(23, (month - 1) * 2 + phase)
|
||||
return TIDES[idx] ?? '立春'
|
||||
}
|
||||
|
||||
/** 年轮进度(0..1)——用于顶栏年环弧段 */
|
||||
export function yearRing(month: number): number {
|
||||
return (month - 1) / 12
|
||||
}
|
||||
|
||||
export interface SeasonTint {
|
||||
accent: string
|
||||
tint: string
|
||||
glow: string
|
||||
name: string
|
||||
desc: string
|
||||
}
|
||||
|
||||
const TINTS: Record<Season, SeasonTint> = {
|
||||
spring: { accent: '#9cc27c', tint: '#7a9a5c', glow: '114, 178, 106', name: '春·熏风', desc: '草木萌芽,灵田争长。' },
|
||||
summer: { accent: '#d8b35f', tint: '#b48c46', glow: '216, 172, 96', name: '夏·蝉鸣', desc: '心火正旺,修炼如炉。' },
|
||||
autumn: { accent: '#d0954f', tint: '#a97a3e', glow: '212, 148, 84', name: '秋·霜叶', desc: '市集兴旺,收获正忙。' },
|
||||
winter: { accent: '#7f9eb5', tint: '#5c7d94', glow: '127, 158, 181', name: '冬·雪静', desc: '闭藏蓄势,闭关养气。' }
|
||||
}
|
||||
|
||||
export function seasonTint(season: Season): SeasonTint {
|
||||
return TINTS[season]
|
||||
}
|
||||
|
||||
/** CSS 变量包裹:data-season 驱动整页色温(4-8% 级,金碧基调内) */
|
||||
export function seasonCssVars(season: Season): Record<string, string> {
|
||||
const t = TINTS[season]
|
||||
return {
|
||||
'--season-accent': t.accent,
|
||||
'--season-glow': t.glow
|
||||
}
|
||||
}
|
||||
|
||||
export function tintLabel(season: Season): string {
|
||||
return TINTS[season].name
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { World } from '../runtime/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 }
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { GameState } from '../../types/domain'
|
||||
import { TECHNIQUES } from '../../data/techniques'
|
||||
import { describeRealm } from '../../data/realms'
|
||||
import { aspirationById } from '../../data/aspirations'
|
||||
import { postById } from '../../data/posts'
|
||||
|
||||
export interface BioLine {
|
||||
at?: number
|
||||
label: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export function buildBiography(s: GameState, memberId: string): BioLine[] {
|
||||
const c = s.members[memberId]
|
||||
if (!c) return []
|
||||
const lines: BioLine[] = []
|
||||
const tech = TECHNIQUES.find((t) => t.id === c.techniqueId)
|
||||
const asp = aspirationById(c.aspiration)
|
||||
const post = postById(c.post)
|
||||
|
||||
lines.push({
|
||||
label: '生平',
|
||||
text: `${c.name},第${c.generation}代子孙${c.gender === 'male' ? '男' : '女'},生于${c.bornYear}年${
|
||||
c.deathYear ? `,殁于${c.deathYear}年(${c.deathCause ?? '寿终'})` : ',今尚在世'
|
||||
}。${post ? `曾居族中「${post.name}」之位。` : ''}${asp ? `平生志向:${asp.name}。` : ''}${
|
||||
tech ? `主修《${tech.name}》。` : ''
|
||||
}`
|
||||
})
|
||||
|
||||
// 突破脉络
|
||||
const breaks = s.chronicle.filter((e) => e.category === 'breakthrough' && e.memberId === c.id)
|
||||
const trials = s.chronicle.filter((e) => e.text.includes(c.name) && (e.category === 'battle' || e.category === 'exploration'))
|
||||
for (const e of breaks) {
|
||||
lines.push({ at: e.year, label: '修行', text: `${e.year}年${e.month}月,${e.text.replace(new RegExp(`^${c.name}\\s*`), '')}`})
|
||||
}
|
||||
if (trials.length > 0) {
|
||||
lines.push({ at: trials[0].year, label: '历险', text: `${trials[0].year}年·${trials[0].text}` })
|
||||
}
|
||||
|
||||
// 婚育
|
||||
const marriages = s.chronicle.filter((e) => e.category === 'marriage' && e.memberId === c.id)
|
||||
for (const m of marriages) {
|
||||
lines.push({ at: m.year, label: '姻缘', text: m.text })
|
||||
}
|
||||
if (c.children.length > 0) {
|
||||
const kids = c.children.map((id) => s.members[id]?.name ?? '?').join('、')
|
||||
lines.push({ label: '子嗣', text: `抚育子女${c.children.length}人:${kids}。` })
|
||||
}
|
||||
|
||||
// 大比
|
||||
const hisTourneys = s.stats.tourneyHistory.filter((t) => {
|
||||
const battle = s.battles.find((b) => b.year === t.year && b.title.includes('大比'))
|
||||
return battle?.lines.some((l) => l.includes(c.name))
|
||||
})
|
||||
for (const t of hisTourneys) {
|
||||
lines.push({ at: t.year, label: '大比', text: `${t.year}年,随队参加太虚大比,列第${t.rank}名。` })
|
||||
}
|
||||
|
||||
// 墓碑铭文
|
||||
if (!c.alive) {
|
||||
const epitaph = EPITAPH[c.deathCause ?? '寿终'] ?? '一尘一土,终归青山。'
|
||||
lines.push({ label: '墓志', text: `${c.name}之墓。${epitaph}` })
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
const EPITAPH: Record<string, string> = {
|
||||
寿终: '一尘一土,终归青山。子孙焚香,岁岁在此。',
|
||||
'寿元将尽': '一尘一土,终归青山。子孙焚香,岁岁在此。',
|
||||
幼夭: '稚子长眠,天地垂爱。家人悬灯,常照归路。',
|
||||
'伤势不治': '壮士折戟,魂兮归来。家祠之下,与祖同列。',
|
||||
'突破走火': '一念求道,九死未悔。灵前法灯,为君长明。',
|
||||
'渡劫陨落': '雷霆战天,其志未竟。遗骨归山,英风尚在。',
|
||||
'战殁': '马革裹尸,归葬桑梓。长剑挂壁,子孙相传。',
|
||||
'云游飞升': '此去云深不知处,魂同列宿游太虚。'
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Character } from '../../types/domain'
|
||||
import type { World } from '../runtime/World'
|
||||
|
||||
export interface FamilyUnit {
|
||||
parents: [Character | undefined, Character | undefined]
|
||||
children: Character[]
|
||||
gen: number
|
||||
}
|
||||
|
||||
export interface GenRow {
|
||||
gen: number
|
||||
singles: Character[]
|
||||
units: FamilyUnit[]
|
||||
}
|
||||
|
||||
export function computeGenealogy(w: World): GenRow[] {
|
||||
const alive = w.aliveMembers()
|
||||
const byGen = new Map<number, Character[]>()
|
||||
for (const c of alive) {
|
||||
const g = byGen.get(c.generation) ?? []
|
||||
g.push(c)
|
||||
byGen.set(c.generation, g)
|
||||
}
|
||||
const gens = [...byGen.keys()].sort((a, b) => a - b)
|
||||
const rows: GenRow[] = []
|
||||
|
||||
for (const gen of gens) {
|
||||
const members = byGen.get(gen)!
|
||||
const units: FamilyUnit[] = []
|
||||
const used = new Set<string>()
|
||||
|
||||
for (const c of members) {
|
||||
if (used.has(c.id)) continue
|
||||
const spouse = c.spouseId ? w.state.members[c.spouseId] : undefined
|
||||
if (spouse && spouse.alive && !used.has(spouse.id)) {
|
||||
used.add(c.id)
|
||||
used.add(spouse.id)
|
||||
const children = [...c.children, ...spouse.children]
|
||||
.filter((id, i, arr) => arr.indexOf(id) === i)
|
||||
.map((id) => w.state.members[id])
|
||||
.filter((ch): ch is Character => !!ch && ch.alive)
|
||||
.sort((a, b) => a.bornYear - b.bornYear)
|
||||
units.push({ parents: [c, spouse], children, gen })
|
||||
}
|
||||
}
|
||||
|
||||
const singles = members.filter((c) => !used.has(c.id))
|
||||
rows.push({ gen, singles, units })
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
export function countDead(w: World): number {
|
||||
return Object.values(w.state.members).filter((c) => !c.alive).length
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { GameState } from '../../types/domain'
|
||||
import { MAJOR_ORDER } from '../../data/realms'
|
||||
|
||||
export interface LegacyDims {
|
||||
renXing: number
|
||||
daoXing: number
|
||||
weiMing: number
|
||||
xiangHuo: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface LegacyArch {
|
||||
title: string
|
||||
poem: string[]
|
||||
dims: LegacyDims
|
||||
rank: number
|
||||
verdict: string
|
||||
}
|
||||
|
||||
export const VERDICTS: Record<string, { title: string; poem: string[] }> = {
|
||||
feisheng: {
|
||||
title: '飞升之资',
|
||||
poem: ['海内升龙真气象,仙班亦录旧香名。', '遥看青山埋骨处,一脉烟霞送飞鸿。']
|
||||
},
|
||||
celestial: {
|
||||
title: '仙朝遗脉',
|
||||
poem: ['祖庭灯火三千里,曾照中天玉斗垂。', '后世儿孙开卷处,犹闻钟鼎旧清音。']
|
||||
},
|
||||
noble: {
|
||||
title: '中州望族',
|
||||
poem: ['钟鸣鼎食三百年,莫道朱门无圣贤。', '一炷心香传世绪,青灯犹自照芸编。']
|
||||
},
|
||||
warlord: {
|
||||
title: '一方枭雄',
|
||||
poem: ['剑气纵横镇八荒,家声不堕尽金汤。', '而今若问兴亡事,半壁江山问晚霜。']
|
||||
},
|
||||
recluse: {
|
||||
title: '隐世大族',
|
||||
poem: ['不向尘沙争利名,青山深处课桑耕。', '子孙自有莲舟意,一棹烟波到洞庭。']
|
||||
},
|
||||
modest: {
|
||||
title: '积余之家',
|
||||
poem: ['檐下燕飞春复秋,园蔬新雨补梁楸。', '粗茶淡饭家声在,犹胜浮云逐海流。']
|
||||
},
|
||||
dust: {
|
||||
title: '冢中枯骨',
|
||||
poem: ['一度花开一度尘,纸灰飞作旧人魂。', '若教重理当年事,且把残篇问故园。']
|
||||
}
|
||||
}
|
||||
|
||||
const RANK_MAP: [number, string][] = [
|
||||
[160, 'celestial'],
|
||||
[120, 'noble'],
|
||||
[85, 'warlord'],
|
||||
[55, 'recluse'],
|
||||
[30, 'modest']
|
||||
]
|
||||
|
||||
export function computeLegacy(s: GameState): LegacyDims {
|
||||
const st = s.stats
|
||||
const gens = Math.max(
|
||||
...Object.values(s.members).map((c) => c.generation).concat(1)
|
||||
)
|
||||
const renXing = Math.round(gens * 18 + Math.min(st.popPeak, 40) * 1.6)
|
||||
const daoXing = Math.round(st.maxRealmIdx * 26 + st.techniqueGrand * 4)
|
||||
const weiMing = Math.round(st.repPeak * 1.4 + (st.tourneyBest ? (6 - st.tourneyBest) * 12 : 0))
|
||||
const xiangHuo = Math.round(Math.min(s.year, 300) * 0.9 + st.feishengCount * 60)
|
||||
return { renXing, daoXing, weiMing, xiangHuo, total: renXing + daoXing + weiMing + xiangHuo }
|
||||
}
|
||||
|
||||
export function resolveLegacy(s: GameState): LegacyArch {
|
||||
const dims = computeLegacy(s)
|
||||
let key = 'dust'
|
||||
if (s.stats.feishengCount > 0) key = 'feisheng'
|
||||
else {
|
||||
for (const [need, k] of RANK_MAP) {
|
||||
if (dims.total >= need) {
|
||||
key = k
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const verdict = VERDICTS[key]
|
||||
return { title: verdict.title, poem: verdict.poem, dims, rank: dims.total, verdict: key }
|
||||
}
|
||||
|
||||
export function peakRealmIndex(members: GameState['members']): number {
|
||||
let max = -1
|
||||
for (const c of Object.values(members)) {
|
||||
const idx = MAJOR_ORDER.indexOf(c.realm.major) * 10 + c.realm.minor
|
||||
if (idx > max) max = idx
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
export function grandTechniqueCount(members: GameState['members']): number {
|
||||
let n = 0
|
||||
for (const c of Object.values(members)) {
|
||||
if ((c.techniqueRank ?? 0) >= 2) n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { GameState } from '../../types/domain'
|
||||
|
||||
export interface LegacyReport {
|
||||
years: number[]
|
||||
population: number[]
|
||||
reputation: number[]
|
||||
power: number[]
|
||||
deathCauses: Record<string, number>
|
||||
breakthroughs: { year: number; count: number }[]
|
||||
tourneys: { year: number; rank: number }[]
|
||||
genSpan: { gen: number; from: number; to: number }[]
|
||||
totalYears: number
|
||||
}
|
||||
|
||||
export function buildReport(s: GameState): LegacyReport {
|
||||
const reports = s.yearlyReports.slice().sort((a, b) => a.year - b.year)
|
||||
const years = reports.map((r) => r.year)
|
||||
const population = reports.map((r) => r.births - r.deaths)
|
||||
const reputation = reports.map((r) => r.rep)
|
||||
const power = reports.map((r) => r.power)
|
||||
|
||||
const deathCauses: Record<string, number> = {}
|
||||
for (const e of s.chronicle) {
|
||||
if (e.category !== 'death') continue
|
||||
const cause = (e.text.match(/(寿元将尽|幼夭|伤势不治|突破走火|渡劫陨落|战殁|云游飞升)/)?.[0] ?? e.text.split(',')[0] ?? '其他').slice(0, 14)
|
||||
deathCauses[cause] = (deathCauses[cause] ?? 0) + 1
|
||||
}
|
||||
|
||||
const btCount = new Map<number, number>()
|
||||
for (const e of s.chronicle) {
|
||||
if (e.category === 'breakthrough') btCount.set(e.year, (btCount.get(e.year) ?? 0) + 1)
|
||||
}
|
||||
const breakthroughs = [...btCount.entries()].map(([year, count]) => ({ year, count })).sort((a, b) => a.year - b.year)
|
||||
|
||||
const members = Object.values(s.members)
|
||||
const genSpan: LegacyReport['genSpan'] = []
|
||||
const byGen = new Map<number, { min: number; max: number }>()
|
||||
for (const c of members) {
|
||||
const span = byGen.get(c.generation) ?? { min: c.bornYear, max: c.bornYear }
|
||||
span.min = Math.min(span.min, c.bornYear)
|
||||
span.max = Math.max(span.max, c.deathYear ?? s.year)
|
||||
byGen.set(c.generation, span)
|
||||
}
|
||||
for (const [gen, span] of [...byGen.entries()].sort((a, b) => a[0] - b[0])) {
|
||||
genSpan.push({ gen, from: span.min, to: span.max })
|
||||
}
|
||||
|
||||
return {
|
||||
years,
|
||||
population,
|
||||
reputation,
|
||||
power,
|
||||
deathCauses,
|
||||
breakthroughs,
|
||||
tourneys: s.stats.tourneyHistory.slice(),
|
||||
genSpan,
|
||||
totalYears: s.year
|
||||
}
|
||||
}
|
||||
|
||||
/** 依据数据自动措辞的族史结语 */
|
||||
export function verdictLine(r: LegacyReport): string {
|
||||
if (r.years.length < 4) return '岁月尚浅,家族仍在晨雾中前行。'
|
||||
const finalRep = r.reputation[r.reputation.length - 1]
|
||||
const peakRep = Math.max(...r.reputation)
|
||||
const peakIndex = r.reputation.indexOf(peakRep)
|
||||
const peakPower = Math.max(...r.power)
|
||||
const btSum = r.breakthroughs.reduce((a, b) => a + b.count, 0)
|
||||
const mainCause = Object.entries(r.deathCauses).sort((a, b) => b[1] - a[1])[0]
|
||||
|
||||
const lines: string[] = []
|
||||
if (peakRep >= 60) lines.push(`家族声望曾至 ${peakRep} 之巅(第${peakIndex + 1}年),四方来贺。`)
|
||||
else if (finalRep < 0) lines.push('晚境声名凋零,门可罗雀。')
|
||||
else lines.push('不显山露水,然香火未冷。')
|
||||
lines.push(`战力峰值 ${peakPower},共记突破 ${btSum} 次。`)
|
||||
if (mainCause && mainCause[1] > 0) lines.push(`族中弃世者多缘「${mainCause[0]}」。`)
|
||||
const best = r.tourneys.reduce((m, t) => (m === null || t.rank < m.rank ? t : m), null as { year: number; rank: number } | null)
|
||||
if (best) lines.push(`太虚大比最佳战果:第${best.rank}名(${best.year}年)。`)
|
||||
return lines.join('')
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { GameState } from '../../types/domain'
|
||||
|
||||
export interface AxisEvent {
|
||||
year: number
|
||||
kind: '突破' | '大比' | '渡劫' | '婚' | '殇' | '战' | '飞升'
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface AxisCell {
|
||||
from: number
|
||||
to: number
|
||||
events: AxisEvent[]
|
||||
births: number
|
||||
deaths: number
|
||||
}
|
||||
|
||||
/** 横轴年表:十年一格,把族簿大事压缩进一根时间轴 */
|
||||
export function yearAxis(s: GameState, decadeSize = 10): AxisCell[] {
|
||||
const startYear = Math.max(1, Math.min(...s.chronicle.map((e) => e.year).concat(1)))
|
||||
const endYear = Math.max(startYear, s.year)
|
||||
const cells = new Map<number, AxisCell>()
|
||||
const cellOf = (year: number): AxisCell => {
|
||||
const from = Math.floor((year - 1) / decadeSize) * decadeSize + 1
|
||||
if (!cells.has(from)) cells.set(from, { from, to: Math.min(from + decadeSize - 1, endYear), events: [], births: 0, deaths: 0 })
|
||||
return cells.get(from)!
|
||||
}
|
||||
|
||||
for (const e of s.chronicle) {
|
||||
if (e.year < startYear) continue
|
||||
const cell = cellOf(e.year)
|
||||
const kind: AxisEvent['kind'] | null =
|
||||
e.category === 'breakthrough'
|
||||
? /渡劫|天劫/.test(e.text)
|
||||
? '渡劫'
|
||||
: '突破'
|
||||
: e.category === 'birth'
|
||||
? null
|
||||
: e.category === 'death'
|
||||
? /飞升/.test(e.text) ? '飞升' : '殇'
|
||||
: e.category === 'marriage'
|
||||
? '婚'
|
||||
: e.category === 'battle'
|
||||
? '战'
|
||||
: null
|
||||
if (kind) cell.events.push({ year: e.year, kind, label: e.text.slice(0, 26) })
|
||||
if (e.category === 'birth') cell.births++
|
||||
if (e.category === 'death') cell.deaths++
|
||||
}
|
||||
|
||||
return [...cells.entries()].sort((a, b) => a[0] - b[0]).map(([, c]) => c)
|
||||
}
|
||||
|
||||
export function decadeLabel(from: number, to: number): string {
|
||||
return from === to ? `${from}年` : `${from}-${to}年`
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { World } from './world'
|
||||
import { WorldEventBus } from './world'
|
||||
import { YearlyReport, BattleLog, ChronicleEntry, SaveMeta } from '../types/domain'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique } from './market'
|
||||
import { sendMission, recallAll } from './systems/missions'
|
||||
import { giftNpc, makePeace, marryNpcFamily } from './systems/diplomacy'
|
||||
import { combatPowerOf } from './systems/combat'
|
||||
import type { LogKind } from './world'
|
||||
import { computeLegacy, resolveLegacy, LegacyArch } from '../core/legacy'
|
||||
import { yearAxis, AxisCell } from '../core/yearaxis'
|
||||
import { PACK } from '../data/registry'
|
||||
import type { World } from './World'
|
||||
import { WorldEventBus } from './World'
|
||||
import { YearlyReport, BattleLog, ChronicleEntry, SaveMeta } from '../../types/domain'
|
||||
import { marketPrice, buyItem, sellItem, buyTechnique } from '../sim/Market'
|
||||
import { sendMission, recallAll } from './Systems/missions'
|
||||
import { giftNpc, makePeace, marryNpcFamily } from './Systems/diplomacy'
|
||||
import { combatPowerOf } from './Systems/combat'
|
||||
import type { LogKind } from './World'
|
||||
import { computeLegacy, resolveLegacy, LegacyArch } from '../narrative/legacy'
|
||||
import { yearAxis, AxisCell } from '../narrative/yearaxis'
|
||||
import { PACK } from '../../data/registry'
|
||||
|
||||
export type ActName =
|
||||
| 'head.set'
|
||||
+9
-9
@@ -1,14 +1,14 @@
|
||||
import type { World } from '../world'
|
||||
import { Character, BattleLog } from '../../types/domain'
|
||||
import { basePower, describeRealm } from '../../data/realms'
|
||||
import { pack } from '../../data/registry'
|
||||
import { techniqueById } from '../../data/techniques'
|
||||
import { EnemyDef, LootDef } from '../../data/secrets'
|
||||
import type { World } from '../World'
|
||||
import { Character, BattleLog } from '../../../types/domain'
|
||||
import { basePower, describeRealm } from '../../../data/realms'
|
||||
import { pack } from '../../../data/registry'
|
||||
import { techniqueById } from '../../../data/techniques'
|
||||
import { EnemyDef, LootDef } from '../../../data/secrets'
|
||||
|
||||
import { traitBonuses } from '../pcgen'
|
||||
import { npcById } from '../../data/npcs'
|
||||
import { aspirationById, fitBonusOf } from '../../data/aspirations'
|
||||
import { formationById, FormationId } from '../../data/formations'
|
||||
import { npcById } from '../../../data/npcs'
|
||||
import { aspirationById, fitBonusOf } from '../../../data/aspirations'
|
||||
import { formationById, FormationId } from '../../../data/formations'
|
||||
|
||||
export function combatPowerOf(w: World, c: Character): number {
|
||||
if (!c.alive) return 0
|
||||
+10
-10
@@ -1,16 +1,16 @@
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { ROOT_GRADES } from '../../data/elements'
|
||||
import { masteryRateOfMajor } from '../../data/pacing'
|
||||
import { aspirationById, fitBonusOf } from '../../data/aspirations'
|
||||
import { techniqueById } from '../../data/techniques'
|
||||
import { nextRealm, breakthroughBaseChance, realmDeathChance, describeRealm, MAJOR_ORDER } from '../../data/realms'
|
||||
import type { World } from '../World'
|
||||
import { Character } from '../../../types/domain'
|
||||
import { ROOT_GRADES } from '../../../data/elements'
|
||||
import { masteryRateOfMajor } from '../../../data/pacing'
|
||||
import { aspirationById, fitBonusOf } from '../../../data/aspirations'
|
||||
import { techniqueById } from '../../../data/techniques'
|
||||
import { nextRealm, breakthroughBaseChance, realmDeathChance, describeRealm, MAJOR_ORDER } from '../../../data/realms'
|
||||
import { lifespanOf } from './lifecycle'
|
||||
import { traitBonuses } from '../pcgen'
|
||||
import { newCharacter } from '../pcgen'
|
||||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../core/names'
|
||||
import { ASPIRATION_IDS } from '../../data/aspirations'
|
||||
import { seasonMod } from '../../data/season'
|
||||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../kernel/names'
|
||||
import { ASPIRATION_IDS } from '../../../data/aspirations'
|
||||
import { seasonMod } from '../../../data/season'
|
||||
import { needsTribulation, tribulationEventId } from './tribulation'
|
||||
|
||||
export function monthlyRate(w: World, c: Character): number {
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { World } from '../world'
|
||||
import { npcById } from '../../data/npcs'
|
||||
import type { World } from '../World'
|
||||
import { npcById } from '../../../data/npcs'
|
||||
import { findEvent, fire } from './events'
|
||||
|
||||
export function diplomacyTick(w: World): void {
|
||||
+7
-7
@@ -1,16 +1,16 @@
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { Cond, EffectDef, EventDef, EVENTS, MemberEffect } from '../../data/events'
|
||||
import { MAJOR_ORDER } from '../../data/realms'
|
||||
import type { World } from '../World'
|
||||
import { Character } from '../../../types/domain'
|
||||
import { Cond, EffectDef, EventDef, EVENTS, MemberEffect } from '../../../data/events'
|
||||
import { MAJOR_ORDER } from '../../../data/realms'
|
||||
|
||||
|
||||
import { npcById } from '../../data/npcs'
|
||||
import { npcById } from '../../../data/npcs'
|
||||
import { resolveRaid } from './combat'
|
||||
import { sendMission } from './missions'
|
||||
import { pack } from '../../data/registry'
|
||||
import { pack } from '../../../data/registry'
|
||||
import { findInheritor } from '../creation'
|
||||
import { resolveTribulation } from './tribulation'
|
||||
import { nextRealm, describeRealm } from '../../data/realms'
|
||||
import { nextRealm, describeRealm } from '../../../data/realms'
|
||||
|
||||
const ALL_EVENTS: EventDef[] = [...EVENTS]
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import type { World } from '../world'
|
||||
import { MAJORS } from '../../data/realms'
|
||||
import type { World } from '../World'
|
||||
import { MAJORS } from '../../../data/realms'
|
||||
import { calcLifespan } from '../pcgen'
|
||||
|
||||
export function lifespanOf(w: World, c: { realm: { major: keyof typeof MAJORS }; physique: number }): number {
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import type { World } from '../World'
|
||||
import { Character } from '../../../types/domain'
|
||||
import { produceOffspring } from './cultivation'
|
||||
import { yearGrowth } from './diplomacy'
|
||||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../core/names'
|
||||
import { aspirationById } from '../../data/aspirations'
|
||||
import { MALE_GIVEN, FEMALE_GIVEN } from '../../kernel/names'
|
||||
import { aspirationById } from '../../../data/aspirations'
|
||||
|
||||
export function yearStartMarriage(w: World): void {
|
||||
yearGrowth(w)
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
import type { World } from '../world'
|
||||
import { MissionState } from '../../types/domain'
|
||||
import { FormationId } from '../../data/formations'
|
||||
import { missionById, MissionDef, ENEMIES } from '../../data/secrets'
|
||||
import type { World } from '../World'
|
||||
import { MissionState } from '../../../types/domain'
|
||||
import { FormationId } from '../../../data/formations'
|
||||
import { missionById, MissionDef, ENEMIES } from '../../../data/secrets'
|
||||
import { resolveEncounter, rollWarbooty } from './combat'
|
||||
import { techniqueById } from '../../data/techniques'
|
||||
import { describeRealm } from '../../data/realms'
|
||||
import { techniqueById } from '../../../data/techniques'
|
||||
import { describeRealm } from '../../../data/realms'
|
||||
|
||||
export function missionTick(w: World): void {
|
||||
const active = w.state.missions.filter((m) => !m.done)
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import type { World } from '../world'
|
||||
import { aspirationById } from '../../data/aspirations'
|
||||
import { seasonMod } from '../../data/season'
|
||||
import type { World } from '../World'
|
||||
import { aspirationById } from '../../../data/aspirations'
|
||||
import { seasonMod } from '../../../data/season'
|
||||
|
||||
export function productionTick(w: World): void {
|
||||
const fam = w.state.family
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { ENEMIES, EnemyDef } from '../../data/secrets'
|
||||
import type { World } from '../World'
|
||||
import { Character } from '../../../types/domain'
|
||||
import { ENEMIES, EnemyDef } from '../../../data/secrets'
|
||||
import { resolveEncounter } from './combat'
|
||||
import { rankPower } from './events'
|
||||
import { resolveBreakthrough } from './cultivation'
|
||||
import { FormationId } from '../../data/formations'
|
||||
import { FormationId } from '../../../data/formations'
|
||||
|
||||
export const SECTS = ['云栖宗', '太谷书院', '玄微剑阁', '丹霞洞天']
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import type { World } from '../world'
|
||||
import { Character } from '../../types/domain'
|
||||
import { nextRealm, MAJOR_ORDER, realmDeathChance, describeRealm } from '../../data/realms'
|
||||
import type { World } from '../World'
|
||||
import { Character } from '../../../types/domain'
|
||||
import { nextRealm, MAJOR_ORDER, realmDeathChance, describeRealm } from '../../../data/realms'
|
||||
|
||||
export const TRIB_MAJORS: string[] = ['foundation', 'core', 'nascent', 'spirit']
|
||||
|
||||
@@ -7,25 +7,25 @@ import {
|
||||
LogItem,
|
||||
Realm,
|
||||
YearlyReport
|
||||
} from '../types/domain'
|
||||
import { Rng } from '../core/rng'
|
||||
import { BUILDINGS } from '../data/buildings'
|
||||
import { POSTS } from '../data/posts'
|
||||
import { aspirationById as aspirationOf } from '../data/aspirations'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../core/legacy'
|
||||
} from '../../types/domain'
|
||||
import { Rng } from '../kernel/rng'
|
||||
import { BUILDINGS } from '../../data/buildings'
|
||||
import { POSTS } from '../../data/posts'
|
||||
import { aspirationById as aspirationOf } from '../../data/aspirations'
|
||||
import { computeLegacy, resolveLegacy, peakRealmIndex, grandTechniqueCount, LegacyArch } from '../narrative/legacy'
|
||||
import { createWorldState, findInheritor } from './creation'
|
||||
import { SYSTEM_DEFS, SystemDef } from './capabilities'
|
||||
import { emptyClock } from './clocks'
|
||||
import { CotycPlugin, PluginContext, PluginStatus } from '../core/plugin'
|
||||
import { CotycPlugin, PluginContext, PluginStatus } from '../kernel/plugin'
|
||||
import { PluginManager } from './pluginManager'
|
||||
import { EventDef } from '../data/events'
|
||||
import { pack, DEFAULT_PACK, PACK } from '../data/registry'
|
||||
import { EventDef } from '../../data/events'
|
||||
import { pack, DEFAULT_PACK, PACK } from '../../data/registry'
|
||||
import { CORE_PLUGINS } from './plugin-bootstrap'
|
||||
import { GameClock } from '../core/clock'
|
||||
import { SystemHook } from '../core/clock'
|
||||
import { resolveBreakthrough } from './systems/cultivation'
|
||||
import { applyEventChoice } from './systems/events'
|
||||
import { combatPowerOf } from './systems/combat'
|
||||
import { GameClock } from '../kernel/clock'
|
||||
import { SystemHook } from '../kernel/clock'
|
||||
import { resolveBreakthrough } from './Systems/cultivation'
|
||||
import { applyEventChoice } from './Systems/events'
|
||||
import { combatPowerOf } from './Systems/combat'
|
||||
|
||||
export type LogKind = LogItem['kind']
|
||||
|
||||
@@ -138,7 +138,7 @@ export class World {
|
||||
}
|
||||
}
|
||||
|
||||
private packOverride(partial: Partial<import('../data/registry').DataPack>): void {
|
||||
private packOverride(partial: Partial<import('../../data/registry').DataPack>): void {
|
||||
PACK.override(partial)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { PhaseId } from '../core/clock'
|
||||
import type { PhaseId } from '../kernel/clock'
|
||||
|
||||
export interface SystemDef {
|
||||
id: string
|
||||
@@ -1,13 +1,13 @@
|
||||
import { GameClock } from '../core/clock'
|
||||
import { PluginContext } from '../core/plugin'
|
||||
import type { World } from './world'
|
||||
import { productionTick } from './systems/production'
|
||||
import { deathTick, woundHealTick } from './systems/lifecycle'
|
||||
import { cultivationTick } from './systems/cultivation'
|
||||
import { missionTick } from './systems/missions'
|
||||
import { eventRoll } from './systems/events'
|
||||
import { diplomacyTick } from './systems/diplomacy'
|
||||
import { yearStartMarriage } from './systems/marriage'
|
||||
import { GameClock } from '../kernel/clock'
|
||||
import { PluginContext } from '../kernel/plugin'
|
||||
import type { World } from './World'
|
||||
import { productionTick } from './Systems/production'
|
||||
import { deathTick, woundHealTick } from './Systems/lifecycle'
|
||||
import { cultivationTick } from './Systems/cultivation'
|
||||
import { missionTick } from './Systems/missions'
|
||||
import { eventRoll } from './Systems/events'
|
||||
import { diplomacyTick } from './Systems/diplomacy'
|
||||
import { yearStartMarriage } from './Systems/marriage'
|
||||
|
||||
/** 原语义保留:满门凋零后仅存生产/寿元与收尾 */
|
||||
function ifAlive(fn: (w: World) => void): (w: World) => void {
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Character, GameState, NpcFamilyState, RealmMajor } from '../types/domain'
|
||||
import { Rng, seedToRng } from '../core/rng'
|
||||
import { randomSurname, MALE_GIVEN, FEMALE_GIVEN } from '../core/names'
|
||||
import { Character, GameState, NpcFamilyState, RealmMajor } from '../../types/domain'
|
||||
import { Rng, seedToRng } from '../kernel/rng'
|
||||
import { randomSurname, MALE_GIVEN, FEMALE_GIVEN } from '../kernel/names'
|
||||
import { newCharacter } from './pcgen'
|
||||
import { NPCS } from '../data/npcs'
|
||||
import { World } from './world'
|
||||
import { NPCS } from '../../data/npcs'
|
||||
import { World } from './World'
|
||||
|
||||
export interface NewGameOptions {
|
||||
seed: string
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CotycPlugin } from '../core/plugin'
|
||||
import { CotycPlugin } from '../kernel/plugin'
|
||||
|
||||
/** 示例内容插件:注入事件池 + 一个护山能力(开发范本) */
|
||||
export const examplePlugin: CotycPlugin = {
|
||||
@@ -1,8 +1,8 @@
|
||||
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'
|
||||
import { Character, Element, Gender, Realm, RealmMajor } from '../../types/domain'
|
||||
import { Rng } from '../kernel/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
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { CotycPlugin } from '../core/plugin'
|
||||
import { CotycPlugin } from '../kernel/plugin'
|
||||
import { installCoreSystems } from './clocks'
|
||||
import { EVENTS } from '../data/events'
|
||||
import { DEFAULT_PACK } from '../data/registry'
|
||||
import { EVENTS } from '../../data/events'
|
||||
import { DEFAULT_PACK } from '../../data/registry'
|
||||
|
||||
/** 内置插件一:镇族基石(12 能力卡与时轮钩子) */
|
||||
export function makeCoreSystemsPlugin(): CotycPlugin {
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
import { CotycPlugin, PluginContext, PluginStatus, PluginChange } from '../core/plugin'
|
||||
import { SystemDef, SYSTEM_DEFS } from '../engine/capabilities'
|
||||
import type { World } from '../engine/world'
|
||||
import { SystemHook } from '../core/clock'
|
||||
import { GameClock } from '../core/clock'
|
||||
import { CotycPlugin, PluginContext, PluginStatus, PluginChange } from '../kernel/plugin'
|
||||
import { SystemDef, SYSTEM_DEFS } from './capabilities'
|
||||
import type { World } from './World'
|
||||
import { SystemHook } from '../kernel/clock'
|
||||
import { GameClock } from '../kernel/clock'
|
||||
|
||||
interface RuntimePlugin {
|
||||
plugin: CotycPlugin
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { World } from './world'
|
||||
import { pack } from '../data/registry'
|
||||
import { traitBonuses } from './pcgen'
|
||||
import type { World } from '../runtime/World'
|
||||
import { pack } from '../../data/registry'
|
||||
import { traitBonuses } from '../runtime/pcgen'
|
||||
|
||||
export function marketPrice(w: World, itemId: string): number {
|
||||
const item = pack().items[itemId]
|
||||
@@ -48,7 +48,7 @@ export function techniquePrice(techId: string): number {
|
||||
return TECH_GRADE_BASE[techId] ?? 200
|
||||
}
|
||||
|
||||
import { TECHNIQUES } from '../data/techniques'
|
||||
import { TECHNIQUES } from '../../data/techniques'
|
||||
|
||||
const TECHNIQUE_GRADE_PRICE: Record<number, number> = { 1: 120, 2: 300, 3: 700, 4: 1600 }
|
||||
const TECH_GRADE_BASE: Record<string, number> = Object.fromEntries(
|
||||
Reference in New Issue
Block a user