mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-29 01:36:47 +08:00
update: state, event, condition, data.event.types
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { checkCondition } from '.'
|
||||
import { check } from '.'
|
||||
|
||||
// 定义属性 mock 字典的类型契约
|
||||
interface MockProperties {
|
||||
@@ -10,12 +10,7 @@ interface MockProperties {
|
||||
* 属性注入高阶函数:模拟角色属性管理类
|
||||
*/
|
||||
function withProp(prop: MockProperties) {
|
||||
const p = {
|
||||
get(key: string): any {
|
||||
return prop[key]
|
||||
},
|
||||
}
|
||||
return (condition: string): boolean => checkCondition(p, condition)
|
||||
return (condition: string): boolean => check(prop, condition)
|
||||
}
|
||||
|
||||
describe('condition', () => {
|
||||
|
||||
+27
-24
@@ -1,17 +1,9 @@
|
||||
// 条件节点可以是一个纯字符串表达式(如 'AGE>18'),或者是一个无限嵌套自身的数组
|
||||
export type ConditionNode = string | ConditionTree
|
||||
export interface ConditionTree extends Array<ConditionNode> {}
|
||||
|
||||
// 🌟 核心类型定义 2:定义满足有 .get 提取器属性的游戏对象约束(如玩家属性管理类或原生 Map)
|
||||
export interface PropertyContainer {
|
||||
get(key: string): any
|
||||
}
|
||||
export type PropertyContainer = Record<string, any>
|
||||
|
||||
/**
|
||||
* 词法解析器:将条件字符串切分为多维嵌套的语法树树(AST)
|
||||
* @param condition 原始字符串表达式,例如 "AGE > 10 & (SEX = 1 | CHR >= 5)"
|
||||
*/
|
||||
function parseCondition(condition: string): ConditionTree {
|
||||
export function parse(condition: string): ConditionTree {
|
||||
const conditions: ConditionTree = []
|
||||
const length = condition.length
|
||||
const stack: ConditionTree[] = []
|
||||
@@ -60,16 +52,8 @@ function parseCondition(condition: string): ConditionTree {
|
||||
return conditions
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部核心接口:判定某个角色的属性是否完美符合该文本条件限制
|
||||
* @param property 玩家或局内属性提取器
|
||||
* @param condition 原始条件表达式
|
||||
*/
|
||||
export function checkCondition(
|
||||
property: PropertyContainer,
|
||||
condition: string,
|
||||
): boolean {
|
||||
const conditions = parseCondition(condition)
|
||||
export function check(property: PropertyContainer, condition: string): boolean {
|
||||
const conditions = parse(condition)
|
||||
return checkParsedConditions(property, conditions)
|
||||
}
|
||||
|
||||
@@ -115,9 +99,6 @@ function checkParsedConditions(
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子逻辑判定器:负责处理诸如 '>', '<', '=', '?', '!' 各种操作符的最终生死判定
|
||||
*/
|
||||
function checkProp(property: PropertyContainer, condition: string): boolean {
|
||||
const length = condition.length
|
||||
let i = condition.search(/[><!?=]/)
|
||||
@@ -131,7 +112,7 @@ function checkProp(property: PropertyContainer, condition: string): boolean {
|
||||
const symbol = condition.substring(i, (i += isDoubleSymbol ? 2 : 1))
|
||||
const d = condition.substring(i, length)
|
||||
|
||||
const propData = property.get(prop)
|
||||
const propData = property[prop]
|
||||
const conditionData: number | any[] =
|
||||
d[0] === '[' ? JSON.parse(d) : Number(d)
|
||||
|
||||
@@ -145,17 +126,31 @@ function checkProp(property: PropertyContainer, condition: string): boolean {
|
||||
case '<=':
|
||||
return propData <= conditionData
|
||||
case '=':
|
||||
if (propData instanceof Set) {
|
||||
return propData.has(conditionData)
|
||||
}
|
||||
if (Array.isArray(propData)) {
|
||||
return propData.includes(conditionData)
|
||||
}
|
||||
return propData == conditionData
|
||||
case '!=':
|
||||
if (propData instanceof Set) {
|
||||
return !propData.has(conditionData)
|
||||
}
|
||||
if (Array.isArray(propData)) {
|
||||
return !propData.includes(conditionData)
|
||||
}
|
||||
return propData != conditionData
|
||||
|
||||
case '?': // 🌟 包含判定符(如 属性值 是否在 [1,2,3] 数组范围内)
|
||||
if (propData instanceof Set) {
|
||||
if (Array.isArray(conditionData)) {
|
||||
for (const c of conditionData) {
|
||||
if (propData.has(c)) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(propData)) {
|
||||
if (Array.isArray(conditionData)) {
|
||||
for (const p of propData) {
|
||||
@@ -170,6 +165,14 @@ function checkProp(property: PropertyContainer, condition: string): boolean {
|
||||
return false
|
||||
|
||||
case '!': // 🌟 排除判定符(如 属性值 是否不存在于 [1,2,3] 数组中)
|
||||
if (propData instanceof Set) {
|
||||
if (Array.isArray(conditionData)) {
|
||||
for (const c of conditionData) {
|
||||
if (propData.has(c)) return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (Array.isArray(propData)) {
|
||||
if (Array.isArray(conditionData)) {
|
||||
for (const p of propData) {
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
"lint": "eslint .",
|
||||
"test": "bun test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dependencies": {
|
||||
"@remake/condition": "workspace:*",
|
||||
"@remake/data": "workspace:*"
|
||||
"@remake/data": "workspace:*",
|
||||
"@remake/vitex": "workspace:*",
|
||||
"immer": "^11.1.15"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "../../package.json"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { createState } from './state'
|
||||
import { trigger } from './event'
|
||||
|
||||
describe('Event', () => {
|
||||
const profile = {
|
||||
times: 0,
|
||||
talents: new Set([]),
|
||||
achievements: new Set([]),
|
||||
events: new Set([]),
|
||||
}
|
||||
const allocation = {
|
||||
charm: 10,
|
||||
intelligence: 10,
|
||||
strength: 0,
|
||||
money: 0,
|
||||
}
|
||||
|
||||
test('10003 branch 0 玉佩', () => {
|
||||
const result = trigger(
|
||||
10003,
|
||||
createState(allocation, [1001, 1002, 1003]),
|
||||
profile,
|
||||
)
|
||||
expect(result.state.life).toBe(1)
|
||||
expect(result.state.events).toEqual(new Set([10003, 10004]))
|
||||
expect(result.events).toEqual([10003, 10004])
|
||||
})
|
||||
|
||||
test('10003 branch 1 死了', () => {
|
||||
const result = trigger(10003, createState(allocation, []), profile)
|
||||
expect(result.state.life).toBe(0)
|
||||
expect(result.state.events).toEqual(new Set([10003, 10000]))
|
||||
expect(result.events).toEqual([10003, 10000])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { EventEffect, Event } from '@remake/data/event'
|
||||
import events from '@remake/data/event'
|
||||
import type { Properties, Effect } from '@/state'
|
||||
import type { GameState, ProfileState } from '@/state'
|
||||
import { stateEffect, createFlatState } from '@/state'
|
||||
import { check } from '@remake/condition'
|
||||
|
||||
export function convertEffect(effect: EventEffect): Effect {
|
||||
const converted = {} as Effect
|
||||
const properties = [] as [keyof Properties, number][]
|
||||
if (effect.CHR) properties.push(['charm', effect.CHR])
|
||||
if (effect.INT) properties.push(['intelligence', effect.INT])
|
||||
if (effect.STR) properties.push(['strength', effect.STR])
|
||||
if (effect.MNY) properties.push(['money', effect.MNY])
|
||||
if (effect.SPR) properties.push(['spirit', effect.SPR])
|
||||
if (properties.length > 0)
|
||||
converted.properties = Object.fromEntries(properties)
|
||||
if (effect.LIF) converted.life = effect.LIF
|
||||
return converted
|
||||
}
|
||||
|
||||
export interface TriggerResult {
|
||||
state: GameState
|
||||
events: Event['id'][]
|
||||
}
|
||||
|
||||
export function trigger(
|
||||
eventId: number,
|
||||
state: GameState,
|
||||
profile: ProfileState,
|
||||
): TriggerResult {
|
||||
const event = events.get(eventId)
|
||||
if (!event)
|
||||
throw new Error(`[@remake/core][event] id:${eventId} not found.`)
|
||||
const effect = event.effect ? convertEffect(event.effect) : {}
|
||||
effect.events = [eventId]
|
||||
const newState = stateEffect(state, effect)
|
||||
const flatState = createFlatState(newState, profile)
|
||||
if (event.branch) {
|
||||
for (const branch of event.branch) {
|
||||
if (check(flatState, branch.condition)) {
|
||||
const result = trigger(branch.event, newState, profile)
|
||||
return {
|
||||
state: result.state,
|
||||
events: [eventId, ...result.events],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { state: newState, events: [eventId] }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface GameEffects {
|
||||
save: (profile: any) => Promise<boolean>
|
||||
load: () => Promise<any>
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
import { age } from '@remake/data/age'
|
||||
import { checkCondition } from '@remake/condition'
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { parse, type ConditionNode } from '@remake/condition'
|
||||
|
||||
import achievements from '@remake/data/achievement'
|
||||
import events from '@remake/data/event'
|
||||
import talents from '@remake/data/talent'
|
||||
|
||||
import { createFlatState } from './state'
|
||||
|
||||
const A = ['AGE', 'CHR', 'INT', 'STR', 'MNY', 'SPR', 'LIF', 'TLT', 'EVT', 'TMS']
|
||||
const B = ['HAGE', 'HCHR', 'HINT', 'HSTR', 'HMNY', 'HSPR']
|
||||
const C = ['LAGE', 'LCHR', 'LINT', 'LSTR', 'LMNY', 'LSPR']
|
||||
const D = ['AEVT', 'ATLT', 'SUM']
|
||||
const FlatProperties = new Set([...A, ...B, ...C, ...D])
|
||||
|
||||
/**
|
||||
* 核心递归辅助函数:从多维语法树节点中提取所有属性名
|
||||
*/
|
||||
function extractKeysFromNode(node: ConditionNode, properties: Set<string>) {
|
||||
if (Array.isArray(node)) {
|
||||
for (const subNode of node) {
|
||||
extractKeysFromNode(subNode, properties)
|
||||
}
|
||||
} else {
|
||||
if (node === '&' || node === '|') return
|
||||
|
||||
// 匹配操作符(><!?=)前面的属性名部分
|
||||
const match = node.match(/^([^><!?=]+)/)
|
||||
if (match && match) {
|
||||
properties.add(match[1]!.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从条件字符串中提取所有使用到的属性键名
|
||||
*/
|
||||
function getPropertiesFromCondition(condition: string): Set<string> {
|
||||
const parsedConditions = parse(condition)
|
||||
const properties = new Set<string>()
|
||||
extractKeysFromNode(parsedConditions, properties)
|
||||
return properties
|
||||
}
|
||||
|
||||
describe('策划配置条件完整覆盖率测试', () => {
|
||||
test('event [include, exclude, branch]', () => {
|
||||
const unmappedKeys = new Set<string>()
|
||||
for (const item of events.values()) {
|
||||
const conditionsToTrack: { label: string; text: string }[] = []
|
||||
if (item.include) {
|
||||
conditionsToTrack.push({ label: 'include', text: item.include })
|
||||
}
|
||||
if (item.exclude) {
|
||||
conditionsToTrack.push({ label: 'exclude', text: item.exclude })
|
||||
}
|
||||
|
||||
if (item.branch && Array.isArray(item.branch)) {
|
||||
item.branch.forEach((branchObj, index) => {
|
||||
if (branchObj && branchObj.condition) {
|
||||
conditionsToTrack.push({
|
||||
label: `branch[${index}].condition (目标事件ID: ${branchObj.event})`,
|
||||
text: branchObj.condition,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const { label, text } of conditionsToTrack) {
|
||||
const usedProperties = getPropertiesFromCondition(text)
|
||||
for (const key of usedProperties) {
|
||||
if (!FlatProperties.has(key)) {
|
||||
unmappedKeys.add(
|
||||
`[事件 ID: ${item.id}] 的 "${label}" 字段使用了未定义的键名: "${key}" (完整条件: "${text}")`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unmappedKeys.size > 0) {
|
||||
console.error('\n❌ 发现事件配置表存在未定义的 Property 键名:')
|
||||
unmappedKeys.forEach(msg => console.error(msg))
|
||||
}
|
||||
|
||||
expect(unmappedKeys.size).toBe(0)
|
||||
})
|
||||
|
||||
test('talent [condition]', () => {
|
||||
const unmappedKeys = new Set<string>()
|
||||
const allTalentItems = talents.values()
|
||||
|
||||
for (const item of allTalentItems) {
|
||||
const conditionStr = item.condition
|
||||
if (!conditionStr) continue
|
||||
|
||||
const usedProperties = getPropertiesFromCondition(conditionStr)
|
||||
for (const key of usedProperties) {
|
||||
if (!FlatProperties.has(key)) {
|
||||
unmappedKeys.add(
|
||||
`[天赋 ID: ${item.id}] 使用了未定义的键名: "${key}" (完整条件: "${conditionStr}")`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unmappedKeys.size > 0) {
|
||||
console.error('\n❌ 发现天赋配置表存在未定义的 Property 键名:')
|
||||
unmappedKeys.forEach(msg => console.error(msg))
|
||||
}
|
||||
|
||||
expect(unmappedKeys.size).toBe(0)
|
||||
})
|
||||
|
||||
test('achievement [condition]', () => {
|
||||
const unmappedKeys = new Set<string>()
|
||||
const allAchievementItems = achievements.values()
|
||||
|
||||
for (const item of allAchievementItems) {
|
||||
const conditionStr = item.condition
|
||||
if (!conditionStr) continue
|
||||
|
||||
const usedProperties = getPropertiesFromCondition(conditionStr)
|
||||
for (const key of usedProperties) {
|
||||
if (!FlatProperties.has(key)) {
|
||||
unmappedKeys.add(
|
||||
`[成就 ID: ${item.id}] 使用了未定义的键名: "${key}" (完整条件: "${conditionStr}")`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unmappedKeys.size > 0) {
|
||||
console.error('\n❌ 发现成就配置表存在未定义的 Property 键名:')
|
||||
unmappedKeys.forEach(msg => console.error(msg))
|
||||
}
|
||||
|
||||
expect(unmappedKeys.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('FlatProperties', () => {
|
||||
test('check key', () => {
|
||||
const game = {
|
||||
lowest: {
|
||||
age: 1,
|
||||
charm: 2,
|
||||
intelligence: 3,
|
||||
strength: 4,
|
||||
money: 5,
|
||||
spirit: 6,
|
||||
},
|
||||
properties: {
|
||||
age: 11,
|
||||
charm: 12,
|
||||
intelligence: 13,
|
||||
strength: 14,
|
||||
money: 15,
|
||||
spirit: 16,
|
||||
},
|
||||
highest: {
|
||||
age: 21,
|
||||
charm: 22,
|
||||
intelligence: 23,
|
||||
strength: 24,
|
||||
money: 25,
|
||||
spirit: 26,
|
||||
},
|
||||
life: 31,
|
||||
talents: new Set([33]),
|
||||
events: new Set([34, 35]),
|
||||
achievements: new Set([36]),
|
||||
}
|
||||
|
||||
const profile = {
|
||||
times: 41,
|
||||
external: 42,
|
||||
talents: new Set([43]),
|
||||
achievements: new Set([44]),
|
||||
events: new Set([45, 46]),
|
||||
}
|
||||
|
||||
const flatState = createFlatState(game, profile)
|
||||
expect(flatState['AGE']).toBe(game.properties.age)
|
||||
expect(flatState['CHR']).toBe(game.properties.charm)
|
||||
expect(flatState['INT']).toBe(game.properties.intelligence)
|
||||
expect(flatState['STR']).toBe(game.properties.strength)
|
||||
expect(flatState['MNY']).toBe(game.properties.money)
|
||||
expect(flatState['SPR']).toBe(game.properties.spirit)
|
||||
expect(flatState['LIF']).toBe(game.life)
|
||||
expect(flatState['TLT']).toEqual(game.talents)
|
||||
expect(flatState['EVT']).toEqual(game.events)
|
||||
expect(flatState['LAGE']).toBe(game.lowest.age)
|
||||
expect(flatState['HAGE']).toBe(game.highest.age)
|
||||
expect(flatState['LCHR']).toBe(game.lowest.charm)
|
||||
expect(flatState['HCHR']).toBe(game.highest.charm)
|
||||
expect(flatState['LINT']).toBe(game.lowest.intelligence)
|
||||
expect(flatState['HINT']).toBe(game.highest.intelligence)
|
||||
expect(flatState['LSTR']).toBe(game.lowest.strength)
|
||||
expect(flatState['HSTR']).toBe(game.highest.strength)
|
||||
expect(flatState['LMNY']).toBe(game.lowest.money)
|
||||
expect(flatState['HMNY']).toBe(game.highest.money)
|
||||
expect(flatState['LSPR']).toBe(game.lowest.spirit)
|
||||
expect(flatState['HSPR']).toBe(game.highest.spirit)
|
||||
expect(flatState['TMS']).toBe(profile.times)
|
||||
expect(flatState['AEVT']).toEqual(profile.events)
|
||||
expect(flatState['ATLT']).toEqual(profile.talents)
|
||||
expect(flatState['AACH']).toEqual(profile.achievements)
|
||||
expect(flatState['SUM']).toBe(
|
||||
Math.floor((22 + 23 + 24 + 25 + 26) * 2 + 21 / 2),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,212 @@
|
||||
import type { Talent } from '@remake/data/talent'
|
||||
import type { Event } from '@remake/data/event'
|
||||
import type { Achievement } from '@remake/data/achievement'
|
||||
import { produce, enableMapSet } from 'immer'
|
||||
import { sum } from '@remake/vitex'
|
||||
|
||||
enableMapSet()
|
||||
|
||||
/** 基础的属性 */
|
||||
export interface Properties {
|
||||
age: number
|
||||
charm: number
|
||||
intelligence: number
|
||||
strength: number
|
||||
money: number
|
||||
spirit: number
|
||||
}
|
||||
|
||||
export type Allocation = Omit<Properties, 'age' | 'spirit'>
|
||||
|
||||
export interface GameState {
|
||||
properties: Properties // 本局属性
|
||||
highest: Properties // 本局最高属性
|
||||
lowest: Properties // 本局最低属性
|
||||
life: number // 本局生命值
|
||||
talents: Set<Talent['id']> // 本局拥有的天赋
|
||||
events: Set<Event['id']> // 本局触发过的事件
|
||||
achievements: Set<Achievement['id']> // 本局达成的成就
|
||||
}
|
||||
|
||||
/** 持久化存储的数据 */
|
||||
export interface ProfileState {
|
||||
times: number // 游戏次数
|
||||
external?: Talent['id'] // 继承的天赋
|
||||
talents: Set<Talent['id']> // 拥有过的天赋
|
||||
events: Set<Event['id']> // 触发过的事件
|
||||
achievements: Set<Achievement['id']> // 达成的成就
|
||||
highest?: Properties // 历史最高属性
|
||||
lowest?: Properties // 历史最低属性
|
||||
}
|
||||
|
||||
export function createState(
|
||||
allocation: Allocation,
|
||||
talents?: Iterable<Talent['id']>,
|
||||
): GameState {
|
||||
const properties = { ...allocation, age: -1, spirit: 0 }
|
||||
return {
|
||||
properties,
|
||||
highest: { ...properties },
|
||||
lowest: { ...properties },
|
||||
life: 1,
|
||||
talents: new Set(talents),
|
||||
events: new Set(),
|
||||
achievements: new Set(),
|
||||
}
|
||||
}
|
||||
export interface FlatState {
|
||||
AGE: GameState['properties']['age']
|
||||
CHR: GameState['properties']['charm']
|
||||
INT: GameState['properties']['intelligence']
|
||||
STR: GameState['properties']['strength']
|
||||
MNY: GameState['properties']['money']
|
||||
SPR: GameState['properties']['spirit']
|
||||
LIF: GameState['life']
|
||||
TLT: GameState['talents']
|
||||
EVT: GameState['events']
|
||||
LAGE: GameState['lowest']['age']
|
||||
HAGE: GameState['highest']['age']
|
||||
LCHR: GameState['lowest']['charm']
|
||||
HCHR: GameState['highest']['charm']
|
||||
LINT: GameState['lowest']['intelligence']
|
||||
HINT: GameState['highest']['intelligence']
|
||||
LSTR: GameState['lowest']['strength']
|
||||
HSTR: GameState['highest']['strength']
|
||||
LMNY: GameState['lowest']['money']
|
||||
HMNY: GameState['highest']['money']
|
||||
LSPR: GameState['lowest']['spirit']
|
||||
HSPR: GameState['highest']['spirit']
|
||||
|
||||
TMS: ProfileState['times']
|
||||
AEVT: ProfileState['events']
|
||||
ATLT: ProfileState['talents']
|
||||
AACH: ProfileState['achievements']
|
||||
|
||||
SUM: number
|
||||
}
|
||||
|
||||
interface FlatPropertiesTarget {
|
||||
game: GameState
|
||||
profile: ProfileState
|
||||
}
|
||||
|
||||
const FlatMappers = {
|
||||
AGE: (state: FlatPropertiesTarget) => state.game.properties.age,
|
||||
CHR: (state: FlatPropertiesTarget) => state.game.properties.charm,
|
||||
INT: (state: FlatPropertiesTarget) => state.game.properties.intelligence,
|
||||
STR: (state: FlatPropertiesTarget) => state.game.properties.strength,
|
||||
MNY: (state: FlatPropertiesTarget) => state.game.properties.money,
|
||||
SPR: (state: FlatPropertiesTarget) => state.game.properties.spirit,
|
||||
LIF: (state: FlatPropertiesTarget) => state.game.life,
|
||||
TLT: (state: FlatPropertiesTarget) => state.game.talents,
|
||||
EVT: (state: FlatPropertiesTarget) => state.game.events,
|
||||
LAGE: (state: FlatPropertiesTarget) => state.game.lowest.age,
|
||||
HAGE: (state: FlatPropertiesTarget) => state.game.highest.age,
|
||||
LCHR: (state: FlatPropertiesTarget) => state.game.lowest.charm,
|
||||
HCHR: (state: FlatPropertiesTarget) => state.game.highest.charm,
|
||||
LINT: (state: FlatPropertiesTarget) => state.game.lowest.intelligence,
|
||||
HINT: (state: FlatPropertiesTarget) => state.game.highest.intelligence,
|
||||
LSTR: (state: FlatPropertiesTarget) => state.game.lowest.strength,
|
||||
HSTR: (state: FlatPropertiesTarget) => state.game.highest.strength,
|
||||
LMNY: (state: FlatPropertiesTarget) => state.game.lowest.money,
|
||||
HMNY: (state: FlatPropertiesTarget) => state.game.highest.money,
|
||||
LSPR: (state: FlatPropertiesTarget) => state.game.lowest.spirit,
|
||||
HSPR: (state: FlatPropertiesTarget) => state.game.highest.spirit,
|
||||
TMS: (state: FlatPropertiesTarget) => state.profile.times,
|
||||
AEVT: (state: FlatPropertiesTarget) => state.profile.events,
|
||||
ATLT: (state: FlatPropertiesTarget) => state.profile.talents,
|
||||
AACH: (state: FlatPropertiesTarget) => state.profile.achievements,
|
||||
SUM: (state: FlatPropertiesTarget) => {
|
||||
const { age, ...others } = state.game.highest
|
||||
const s = sum(Object.values(others))
|
||||
return Math.floor(s * 2 + age / 2)
|
||||
},
|
||||
}
|
||||
|
||||
const flatPropertiesHandle = {
|
||||
get(target: FlatPropertiesTarget, prop: string) {
|
||||
if (prop in FlatMappers) {
|
||||
return FlatMappers[prop as keyof typeof FlatMappers](target)
|
||||
}
|
||||
},
|
||||
set() {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
export function createFlatState(game: GameState, profile: ProfileState) {
|
||||
return new Proxy(
|
||||
{ game, profile },
|
||||
flatPropertiesHandle,
|
||||
) as unknown as FlatState
|
||||
}
|
||||
|
||||
export interface Effect {
|
||||
properties?: Partial<Properties>
|
||||
life?: number
|
||||
talents?: Iterable<Talent['id']>
|
||||
events?: Iterable<Event['id']>
|
||||
achievements?: Iterable<Achievement['id']>
|
||||
}
|
||||
|
||||
export function stateEffect(state: GameState, effect: Effect) {
|
||||
return produce(state, draft => {
|
||||
for (const key in effect.properties) {
|
||||
const prop = key as keyof Properties
|
||||
const value = effect.properties[prop]!
|
||||
draft.properties[prop] += value
|
||||
draft.highest[prop] = Math.max(
|
||||
draft.highest[prop],
|
||||
draft.properties[prop],
|
||||
)
|
||||
draft.lowest[prop] = Math.min(
|
||||
draft.lowest[prop],
|
||||
draft.properties[prop],
|
||||
)
|
||||
}
|
||||
if (effect.life) draft.life += effect.life
|
||||
if (effect.talents)
|
||||
draft.talents = new Set([...draft.talents, ...effect.talents])
|
||||
if (effect.events)
|
||||
draft.events = new Set([...draft.events, ...effect.events])
|
||||
if (effect.achievements)
|
||||
draft.achievements = new Set([
|
||||
...draft.achievements,
|
||||
...effect.achievements,
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
function highestProperties(a: Properties, b?: Properties): Properties {
|
||||
if (!b) return { ...a }
|
||||
const result = {} as Properties
|
||||
for (const key of Object.keys(a) as (keyof Properties)[]) {
|
||||
result[key] = Math.max(a[key], b[key])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function lowestProperties(a: Properties, b?: Properties): Properties {
|
||||
if (!b) return { ...a }
|
||||
const result = {} as Properties
|
||||
for (const key of Object.keys(a) as (keyof Properties)[]) {
|
||||
result[key] = Math.min(a[key], b[key])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function nextProfile(
|
||||
profile: ProfileState,
|
||||
state: GameState,
|
||||
external?: Talent['id'],
|
||||
) {
|
||||
return {
|
||||
times: profile.times + 1,
|
||||
talents: profile.talents.union(state.talents),
|
||||
events: profile.events.union(state.events),
|
||||
achievements: profile.achievements.union(state.achievements),
|
||||
highest: highestProperties(state.highest, profile.highest),
|
||||
lowest: lowestProperties(state.lowest, profile.lowest),
|
||||
external: external ?? profile.external,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { TalentEffect } from '@remake/data/talent'
|
||||
import talents from '@remake/data/talent'
|
||||
import type { Properties } from './state'
|
||||
import { pick } from '@remake/vitex'
|
||||
|
||||
const TalentEffectPropertiesKeyMapper = {
|
||||
MNY: () => 'money',
|
||||
STR: () => 'strength',
|
||||
INT: () => 'intelligence',
|
||||
CHR: () => 'charm',
|
||||
SPR: () => 'spirit',
|
||||
RND: () => pick(['money', 'strength', 'intelligence', 'charm', 'spirit'])!,
|
||||
} as Record<keyof TalentEffect, () => keyof Properties>
|
||||
|
||||
export const count = talents.size
|
||||
|
||||
export function get(talent: number) {
|
||||
return talents.get(talent)
|
||||
}
|
||||
|
||||
export function random(talent: number) {}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"main": "src/index.ts",
|
||||
"author": "Vick Scarlet <vick@syaro.io>",
|
||||
"scripts": {
|
||||
"build": "bunx --bun v-transform@3.0.0 transform -t ts -w src -d dist \"**/[!~$]*.xlsx\" ",
|
||||
"build": "bunx --bun v-transform@3.0.1 transform -t ts -w src -d dist \"**/[!~$]*.xlsx\" ",
|
||||
"lint": "eslint .",
|
||||
"test": "bun test"
|
||||
},
|
||||
|
||||
@@ -19,6 +19,14 @@ export type EventEffect = {
|
||||
readonly AGE?: number
|
||||
}
|
||||
|
||||
/** 分支路线 */
|
||||
export type EventBranch = {
|
||||
/** 分支条件 */
|
||||
condition: string
|
||||
/** 分支事件ID */
|
||||
event: number
|
||||
}
|
||||
|
||||
/** 事件 */
|
||||
export type Event = {
|
||||
/** ID */
|
||||
@@ -38,7 +46,7 @@ export type Event = {
|
||||
/** 有某事件时一定随机不到 */
|
||||
readonly exclude?: string
|
||||
/** 分支路线 */
|
||||
readonly branch?: string[]
|
||||
readonly branch?: EventBranch[]
|
||||
}
|
||||
// @vt-types-end
|
||||
|
||||
@@ -55,4 +63,15 @@ export const transformers = {
|
||||
}
|
||||
return val
|
||||
},
|
||||
branch: (val?: any[]) => {
|
||||
if (!val) return
|
||||
for (const key in val) {
|
||||
const [condition, eventId] = val[key]!.split(':')
|
||||
const event = Number(eventId)
|
||||
if (isNaN(event))
|
||||
throw new Error(`Invalid event ID in branch: ${eventId}`)
|
||||
val[key] = { condition, event }
|
||||
}
|
||||
return val
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export type RNG = (max?: number, min?: number) => number
|
||||
export function random(max: number, min: number = 0, rng?: RNG): number {
|
||||
if (rng) return rng(max, min)
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
export function pick<T>(items: T[], rng?: RNG) {
|
||||
if (items.length === 0) return null
|
||||
return items[random(items.length - 1, 0, rng)]
|
||||
}
|
||||
export type WeightItem<T> = [T, number]
|
||||
export function pickWeight<T>(items: WeightItem<T>[], rng?: RNG) {
|
||||
if (items.length === 0) return null
|
||||
const totalWeight = items.reduce((sum, [, weight]) => sum + weight, 0)
|
||||
let mark = random(totalWeight - 1, 0, rng)
|
||||
for (const [item, weight] of items) {
|
||||
if (mark < weight) return item
|
||||
mark -= weight
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function sum(arr: number[]) {
|
||||
return arr.reduce((sum, v) => sum + v, 0)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@remake/vitex",
|
||||
"type": "module",
|
||||
"version": "3.0.0",
|
||||
"main": "./index.ts",
|
||||
"author": "Vick Scarlet <vick@syaro.io>",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "bun test"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"types": ["bun"],
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noImplicitAny": true
|
||||
},
|
||||
"include": ["**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user