mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-30 02:24:42 +08:00
update: talent, state props
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
## 流程
|
||||||
|
|
||||||
|
重开
|
||||||
|
初始化
|
||||||
|
抽天赋(包含继承的天赋)
|
||||||
|
天赋选择(检测天赋冲突)
|
||||||
|
天赋替换(检测天赋冲突)
|
||||||
|
点数分配(天赋额外点数)
|
||||||
|
游戏循环
|
||||||
|
年龄增加一岁
|
||||||
|
天赋触发
|
||||||
|
事件触发(从对应年龄事件池随机事件)
|
||||||
|
判断是否结束(生命值小于 1 结束)
|
||||||
|
总评
|
||||||
|
天赋继承
|
||||||
|
存档数据
|
||||||
|
重回首页
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { expect, test, describe } from 'bun:test'
|
import { expect, test, describe } from 'bun:test'
|
||||||
import { createState } from './state'
|
import { createState } from './state'
|
||||||
import { trigger } from './event'
|
import { trigger } from './event'
|
||||||
|
import { enableMapSet } from 'immer'
|
||||||
|
enableMapSet()
|
||||||
|
|
||||||
describe('Event', () => {
|
describe('Event', () => {
|
||||||
const profile = {
|
const profile = {
|
||||||
@@ -16,7 +18,7 @@ describe('Event', () => {
|
|||||||
money: 0,
|
money: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
test('10003 branch 0 玉佩', () => {
|
test('branch 10003:0 [玉佩]', () => {
|
||||||
const result = trigger(
|
const result = trigger(
|
||||||
10003,
|
10003,
|
||||||
createState(allocation, [1001, 1002, 1003]),
|
createState(allocation, [1001, 1002, 1003]),
|
||||||
@@ -27,7 +29,7 @@ describe('Event', () => {
|
|||||||
expect(result.events).toEqual([10003, 10004])
|
expect(result.events).toEqual([10003, 10004])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('10003 branch 1 死了', () => {
|
test('branch 10003:1 [死了]', () => {
|
||||||
const result = trigger(10003, createState(allocation, []), profile)
|
const result = trigger(10003, createState(allocation, []), profile)
|
||||||
expect(result.state.life).toBe(0)
|
expect(result.state.life).toBe(0)
|
||||||
expect(result.state.events).toEqual(new Set([10003, 10000]))
|
expect(result.state.events).toEqual(new Set([10003, 10000]))
|
||||||
|
|||||||
+21
-27
@@ -1,23 +1,10 @@
|
|||||||
import type { EventEffect, Event } from '@remake/data/event'
|
import type { Event } from '@remake/data/event'
|
||||||
import events from '@remake/data/event'
|
import events from '@remake/data/event'
|
||||||
import type { Properties, Effect } from '@/state'
|
import type { Properties } from '@/state'
|
||||||
import type { GameState, ProfileState } from '@/state'
|
import type { GameState, ProfileState } from '@/state'
|
||||||
import { stateEffect, createFlatState } from '@/state'
|
import { propsEffect, createFlatState } from '@/state'
|
||||||
import { check } from '@remake/condition'
|
import { check } from '@remake/condition'
|
||||||
|
import { produce } from 'immer'
|
||||||
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 {
|
export interface TriggerResult {
|
||||||
state: GameState
|
state: GameState
|
||||||
@@ -29,17 +16,24 @@ export function trigger(
|
|||||||
state: GameState,
|
state: GameState,
|
||||||
profile: ProfileState,
|
profile: ProfileState,
|
||||||
): TriggerResult {
|
): TriggerResult {
|
||||||
const event = events.get(eventId)
|
const { effect, branch } = events.get(eventId)!
|
||||||
if (!event)
|
const newState = produce(state, draft => {
|
||||||
throw new Error(`[@remake/core][event] id:${eventId} not found.`)
|
draft.events.add(eventId)
|
||||||
const effect = event.effect ? convertEffect(event.effect) : {}
|
if (!effect) return
|
||||||
effect.events = [eventId]
|
if (effect.LIF) draft.life += effect.LIF
|
||||||
const newState = stateEffect(state, effect)
|
const pe = {} as Partial<Properties>
|
||||||
|
if (effect.CHR) pe.charm = effect.CHR
|
||||||
|
if (effect.INT) pe.intelligence = effect.INT
|
||||||
|
if (effect.STR) pe.strength = effect.STR
|
||||||
|
if (effect.MNY) pe.money = effect.MNY
|
||||||
|
if (effect.SPR) pe.spirit = effect.SPR
|
||||||
|
draft.props = propsEffect(draft.props, pe)
|
||||||
|
})
|
||||||
const flatState = createFlatState(newState, profile)
|
const flatState = createFlatState(newState, profile)
|
||||||
if (event.branch) {
|
if (branch) {
|
||||||
for (const branch of event.branch) {
|
for (const { condition, event } of branch) {
|
||||||
if (check(flatState, branch.condition)) {
|
if (check(flatState, condition)) {
|
||||||
const result = trigger(branch.event, newState, profile)
|
const result = trigger(event, newState, profile)
|
||||||
return {
|
return {
|
||||||
state: result.state,
|
state: result.state,
|
||||||
events: [eventId, ...result.events],
|
events: [eventId, ...result.events],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface GameEffects {
|
import type { GameState, ProfileState } from '@/state'
|
||||||
save: (profile: any) => Promise<boolean>
|
import { enableMapSet } from 'immer'
|
||||||
load: () => Promise<any>
|
enableMapSet()
|
||||||
}
|
|
||||||
|
export type { GameState, ProfileState }
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from '@/game'
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import achievements from '@remake/data/achievement'
|
|||||||
import events from '@remake/data/event'
|
import events from '@remake/data/event'
|
||||||
import talents from '@remake/data/talent'
|
import talents from '@remake/data/talent'
|
||||||
|
|
||||||
import { createFlatState } from './state'
|
import type { GameState, ProfileState, Properties } from './state'
|
||||||
|
import { createFlatState, createHLProperties, propsEffect } from './state'
|
||||||
const A = ['AGE', 'CHR', 'INT', 'STR', 'MNY', 'SPR', 'LIF', 'TLT', 'EVT', 'TMS']
|
import { SupportedFlatStateKeys } from './state'
|
||||||
const B = ['HAGE', 'HCHR', 'HINT', 'HSTR', 'HMNY', 'HSPR']
|
import { enableMapSet } from 'immer'
|
||||||
const C = ['LAGE', 'LCHR', 'LINT', 'LSTR', 'LMNY', 'LSPR']
|
enableMapSet()
|
||||||
const D = ['AEVT', 'ATLT', 'SUM']
|
|
||||||
const FlatProperties = new Set([...A, ...B, ...C, ...D])
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 核心递归辅助函数:从多维语法树节点中提取所有属性名
|
* 核心递归辅助函数:从多维语法树节点中提取所有属性名
|
||||||
@@ -42,8 +40,8 @@ function getPropertiesFromCondition(condition: string): Set<string> {
|
|||||||
return properties
|
return properties
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('FlatState', () => {
|
describe('State', () => {
|
||||||
test('[Cover] event(include, exclude, branch)', () => {
|
test('flat cover event(include, exclude, branch)', () => {
|
||||||
const unmappedKeys = new Set<string>()
|
const unmappedKeys = new Set<string>()
|
||||||
for (const item of events.values()) {
|
for (const item of events.values()) {
|
||||||
const conditionsToTrack: { label: string; text: string }[] = []
|
const conditionsToTrack: { label: string; text: string }[] = []
|
||||||
@@ -68,7 +66,7 @@ describe('FlatState', () => {
|
|||||||
for (const { label, text } of conditionsToTrack) {
|
for (const { label, text } of conditionsToTrack) {
|
||||||
const usedProperties = getPropertiesFromCondition(text)
|
const usedProperties = getPropertiesFromCondition(text)
|
||||||
for (const key of usedProperties) {
|
for (const key of usedProperties) {
|
||||||
if (!FlatProperties.has(key)) {
|
if (!SupportedFlatStateKeys.has(key)) {
|
||||||
unmappedKeys.add(
|
unmappedKeys.add(
|
||||||
`[事件 ID: ${item.id}] 的 "${label}" 字段使用了未定义的键名: "${key}" (完整条件: "${text}")`,
|
`[事件 ID: ${item.id}] 的 "${label}" 字段使用了未定义的键名: "${key}" (完整条件: "${text}")`,
|
||||||
)
|
)
|
||||||
@@ -85,7 +83,7 @@ describe('FlatState', () => {
|
|||||||
expect(unmappedKeys.size).toBe(0)
|
expect(unmappedKeys.size).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('[Cover] talent(condition)', () => {
|
test('flat cover talent(condition)', () => {
|
||||||
const unmappedKeys = new Set<string>()
|
const unmappedKeys = new Set<string>()
|
||||||
const allTalentItems = talents.values()
|
const allTalentItems = talents.values()
|
||||||
|
|
||||||
@@ -95,7 +93,7 @@ describe('FlatState', () => {
|
|||||||
|
|
||||||
const usedProperties = getPropertiesFromCondition(conditionStr)
|
const usedProperties = getPropertiesFromCondition(conditionStr)
|
||||||
for (const key of usedProperties) {
|
for (const key of usedProperties) {
|
||||||
if (!FlatProperties.has(key)) {
|
if (!SupportedFlatStateKeys.has(key)) {
|
||||||
unmappedKeys.add(
|
unmappedKeys.add(
|
||||||
`[天赋 ID: ${item.id}] 使用了未定义的键名: "${key}" (完整条件: "${conditionStr}")`,
|
`[天赋 ID: ${item.id}] 使用了未定义的键名: "${key}" (完整条件: "${conditionStr}")`,
|
||||||
)
|
)
|
||||||
@@ -111,7 +109,7 @@ describe('FlatState', () => {
|
|||||||
expect(unmappedKeys.size).toBe(0)
|
expect(unmappedKeys.size).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('[Cover] achievement(condition)', () => {
|
test('flat cover achievement(condition)', () => {
|
||||||
const unmappedKeys = new Set<string>()
|
const unmappedKeys = new Set<string>()
|
||||||
const allAchievementItems = achievements.values()
|
const allAchievementItems = achievements.values()
|
||||||
|
|
||||||
@@ -121,7 +119,7 @@ describe('FlatState', () => {
|
|||||||
|
|
||||||
const usedProperties = getPropertiesFromCondition(conditionStr)
|
const usedProperties = getPropertiesFromCondition(conditionStr)
|
||||||
for (const key of usedProperties) {
|
for (const key of usedProperties) {
|
||||||
if (!FlatProperties.has(key)) {
|
if (!SupportedFlatStateKeys.has(key)) {
|
||||||
unmappedKeys.add(
|
unmappedKeys.add(
|
||||||
`[成就 ID: ${item.id}] 使用了未定义的键名: "${key}" (完整条件: "${conditionStr}")`,
|
`[成就 ID: ${item.id}] 使用了未定义的键名: "${key}" (完整条件: "${conditionStr}")`,
|
||||||
)
|
)
|
||||||
@@ -137,37 +135,39 @@ describe('FlatState', () => {
|
|||||||
expect(unmappedKeys.size).toBe(0)
|
expect(unmappedKeys.size).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('get', () => {
|
test('flat get', () => {
|
||||||
const game = {
|
const game = {
|
||||||
lowest: {
|
props: {
|
||||||
age: 1,
|
lowest: {
|
||||||
charm: 2,
|
age: 1,
|
||||||
intelligence: 3,
|
charm: 2,
|
||||||
strength: 4,
|
intelligence: 3,
|
||||||
money: 5,
|
strength: 4,
|
||||||
spirit: 6,
|
money: 5,
|
||||||
},
|
spirit: 6,
|
||||||
properties: {
|
},
|
||||||
age: 11,
|
current: {
|
||||||
charm: 12,
|
age: 11,
|
||||||
intelligence: 13,
|
charm: 12,
|
||||||
strength: 14,
|
intelligence: 13,
|
||||||
money: 15,
|
strength: 14,
|
||||||
spirit: 16,
|
money: 15,
|
||||||
},
|
spirit: 16,
|
||||||
highest: {
|
},
|
||||||
age: 21,
|
highest: {
|
||||||
charm: 22,
|
age: 21,
|
||||||
intelligence: 23,
|
charm: 22,
|
||||||
strength: 24,
|
intelligence: 23,
|
||||||
money: 25,
|
strength: 24,
|
||||||
spirit: 26,
|
money: 25,
|
||||||
|
spirit: 26,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
life: 31,
|
life: 31,
|
||||||
talents: new Set([33]),
|
talents: new Set([33]),
|
||||||
events: new Set([34, 35]),
|
events: new Set([34, 35]),
|
||||||
achievements: new Set([36]),
|
achievements: new Set([36]),
|
||||||
}
|
} as GameState
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
times: 41,
|
times: 41,
|
||||||
@@ -175,30 +175,30 @@ describe('FlatState', () => {
|
|||||||
talents: new Set([43]),
|
talents: new Set([43]),
|
||||||
achievements: new Set([44]),
|
achievements: new Set([44]),
|
||||||
events: new Set([45, 46]),
|
events: new Set([45, 46]),
|
||||||
}
|
} as ProfileState
|
||||||
|
|
||||||
const flatState = createFlatState(game, profile)
|
const flatState = createFlatState(game, profile)
|
||||||
expect(flatState['AGE']).toBe(game.properties.age)
|
expect(flatState['AGE']).toBe(game.props.current.age)
|
||||||
expect(flatState['CHR']).toBe(game.properties.charm)
|
expect(flatState['CHR']).toBe(game.props.current.charm)
|
||||||
expect(flatState['INT']).toBe(game.properties.intelligence)
|
expect(flatState['INT']).toBe(game.props.current.intelligence)
|
||||||
expect(flatState['STR']).toBe(game.properties.strength)
|
expect(flatState['STR']).toBe(game.props.current.strength)
|
||||||
expect(flatState['MNY']).toBe(game.properties.money)
|
expect(flatState['MNY']).toBe(game.props.current.money)
|
||||||
expect(flatState['SPR']).toBe(game.properties.spirit)
|
expect(flatState['SPR']).toBe(game.props.current.spirit)
|
||||||
|
expect(flatState['HAGE']).toBe(game.props.highest.age)
|
||||||
|
expect(flatState['HCHR']).toBe(game.props.highest.charm)
|
||||||
|
expect(flatState['HINT']).toBe(game.props.highest.intelligence)
|
||||||
|
expect(flatState['HSTR']).toBe(game.props.highest.strength)
|
||||||
|
expect(flatState['HMNY']).toBe(game.props.highest.money)
|
||||||
|
expect(flatState['HSPR']).toBe(game.props.highest.spirit)
|
||||||
|
expect(flatState['LAGE']).toBe(game.props.lowest.age)
|
||||||
|
expect(flatState['LCHR']).toBe(game.props.lowest.charm)
|
||||||
|
expect(flatState['LINT']).toBe(game.props.lowest.intelligence)
|
||||||
|
expect(flatState['LSTR']).toBe(game.props.lowest.strength)
|
||||||
|
expect(flatState['LMNY']).toBe(game.props.lowest.money)
|
||||||
|
expect(flatState['LSPR']).toBe(game.props.lowest.spirit)
|
||||||
expect(flatState['LIF']).toBe(game.life)
|
expect(flatState['LIF']).toBe(game.life)
|
||||||
expect(flatState['TLT']).toEqual(game.talents)
|
expect(flatState['TLT']).toEqual(game.talents)
|
||||||
expect(flatState['EVT']).toEqual(game.events)
|
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['TMS']).toBe(profile.times)
|
||||||
expect(flatState['AEVT']).toEqual(profile.events)
|
expect(flatState['AEVT']).toEqual(profile.events)
|
||||||
expect(flatState['ATLT']).toEqual(profile.talents)
|
expect(flatState['ATLT']).toEqual(profile.talents)
|
||||||
@@ -207,4 +207,36 @@ describe('FlatState', () => {
|
|||||||
Math.floor((22 + 23 + 24 + 25 + 26) * 2 + 21 / 2),
|
Math.floor((22 + 23 + 24 + 25 + 26) * 2 + 21 / 2),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const allocation = { charm: 5, intelligence: 5, strength: 5, money: 5 }
|
||||||
|
test('createHLProperties', () => {
|
||||||
|
const props = createHLProperties(allocation)
|
||||||
|
expect(props.current.charm).toBe(allocation.charm)
|
||||||
|
expect(props.current.intelligence).toBe(allocation.intelligence)
|
||||||
|
expect(props.current.strength).toBe(allocation.strength)
|
||||||
|
expect(props.current.money).toBe(allocation.money)
|
||||||
|
expect(props.current.age).toBe(-1)
|
||||||
|
expect(props.current.spirit).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('propsEffect', () => {
|
||||||
|
const props = createHLProperties(allocation)
|
||||||
|
const effect = {
|
||||||
|
age: 6,
|
||||||
|
charm: -2,
|
||||||
|
intelligence: 3,
|
||||||
|
strength: 4,
|
||||||
|
money: -5,
|
||||||
|
spirit: 7,
|
||||||
|
}
|
||||||
|
const effected = propsEffect(props, effect)
|
||||||
|
for (const key in effect) {
|
||||||
|
const k = key as keyof Properties
|
||||||
|
const c = props.current[k]
|
||||||
|
const be = c + effect[k]
|
||||||
|
expect(c).toBe(be)
|
||||||
|
expect(effected.highest[k]).toBe(Math.max(c, be))
|
||||||
|
expect(effected.lowest[k]).toBe(Math.min(c, be))
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+73
-80
@@ -1,11 +1,9 @@
|
|||||||
import type { Talent } from '@remake/data/talent'
|
import type { Talent } from '@remake/data/talent'
|
||||||
import type { Event } from '@remake/data/event'
|
import type { Event } from '@remake/data/event'
|
||||||
import type { Achievement } from '@remake/data/achievement'
|
import type { Achievement } from '@remake/data/achievement'
|
||||||
import { produce, enableMapSet } from 'immer'
|
import { produce } from 'immer'
|
||||||
import { sum } from '@remake/vitex'
|
import { sum } from '@remake/vitex'
|
||||||
|
|
||||||
enableMapSet()
|
|
||||||
|
|
||||||
/** 基础的属性 */
|
/** 基础的属性 */
|
||||||
export interface Properties {
|
export interface Properties {
|
||||||
age: number
|
age: number
|
||||||
@@ -16,22 +14,35 @@ export interface Properties {
|
|||||||
spirit: number
|
spirit: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HLProperties {
|
||||||
|
current: Properties // 当前属性
|
||||||
|
highest: Properties // 历史最高属性
|
||||||
|
lowest: Properties // 历史最低属性
|
||||||
|
}
|
||||||
|
|
||||||
export type Allocation = Omit<Properties, 'age' | 'spirit'>
|
export type Allocation = Omit<Properties, 'age' | 'spirit'>
|
||||||
|
export function createProperties(allocation: Allocation) {
|
||||||
|
return { ...allocation, age: -1, spirit: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHLProperties(allocation: Allocation) {
|
||||||
|
const current = createProperties(allocation)
|
||||||
|
return { current, highest: { ...current }, lowest: { ...current } }
|
||||||
|
}
|
||||||
|
|
||||||
export interface GameState {
|
export interface GameState {
|
||||||
properties: Properties // 本局属性
|
props: HLProperties // 本局属性
|
||||||
highest: Properties // 本局最高属性
|
|
||||||
lowest: Properties // 本局最低属性
|
|
||||||
life: number // 本局生命值
|
life: number // 本局生命值
|
||||||
talents: Set<Talent['id']> // 本局拥有的天赋
|
talents: Set<Talent['id']> // 本局拥有的天赋
|
||||||
events: Set<Event['id']> // 本局触发过的事件
|
events: Set<Event['id']> // 本局触发过的事件
|
||||||
achievements: Set<Achievement['id']> // 本局达成的成就
|
achievements: Set<Achievement['id']> // 本局达成的成就
|
||||||
|
talentTriggers: Map<Talent['id'], number> // 本局天赋触发次数
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 持久化存储的数据 */
|
/** 持久化存储的数据 */
|
||||||
export interface ProfileState {
|
export interface ProfileState {
|
||||||
times: number // 游戏次数
|
times: number // 游戏次数
|
||||||
external?: Talent['id'] // 继承的天赋
|
lockedTalent?: Talent['id'] // 继承的天赋
|
||||||
talents: Set<Talent['id']> // 拥有过的天赋
|
talents: Set<Talent['id']> // 拥有过的天赋
|
||||||
events: Set<Event['id']> // 触发过的事件
|
events: Set<Event['id']> // 触发过的事件
|
||||||
achievements: Set<Achievement['id']> // 达成的成就
|
achievements: Set<Achievement['id']> // 达成的成就
|
||||||
@@ -43,39 +54,37 @@ export function createState(
|
|||||||
allocation: Allocation,
|
allocation: Allocation,
|
||||||
talents?: Iterable<Talent['id']>,
|
talents?: Iterable<Talent['id']>,
|
||||||
): GameState {
|
): GameState {
|
||||||
const properties = { ...allocation, age: -1, spirit: 0 }
|
|
||||||
return {
|
return {
|
||||||
properties,
|
props: createHLProperties(allocation),
|
||||||
highest: { ...properties },
|
|
||||||
lowest: { ...properties },
|
|
||||||
life: 1,
|
life: 1,
|
||||||
talents: new Set(talents),
|
talents: new Set(talents),
|
||||||
events: new Set(),
|
events: new Set(),
|
||||||
achievements: new Set(),
|
achievements: new Set(),
|
||||||
|
talentTriggers: new Map(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface FlatState {
|
export interface FlatState {
|
||||||
AGE: GameState['properties']['age']
|
AGE: GameState['props']['current']['age']
|
||||||
CHR: GameState['properties']['charm']
|
CHR: GameState['props']['current']['charm']
|
||||||
INT: GameState['properties']['intelligence']
|
INT: GameState['props']['current']['intelligence']
|
||||||
STR: GameState['properties']['strength']
|
STR: GameState['props']['current']['strength']
|
||||||
MNY: GameState['properties']['money']
|
MNY: GameState['props']['current']['money']
|
||||||
SPR: GameState['properties']['spirit']
|
SPR: GameState['props']['current']['spirit']
|
||||||
|
HAGE: GameState['props']['highest']['age']
|
||||||
|
HCHR: GameState['props']['highest']['charm']
|
||||||
|
HINT: GameState['props']['highest']['intelligence']
|
||||||
|
HSTR: GameState['props']['highest']['strength']
|
||||||
|
HMNY: GameState['props']['highest']['money']
|
||||||
|
HSPR: GameState['props']['highest']['spirit']
|
||||||
|
LAGE: GameState['props']['lowest']['age']
|
||||||
|
LCHR: GameState['props']['lowest']['charm']
|
||||||
|
LINT: GameState['props']['lowest']['intelligence']
|
||||||
|
LSTR: GameState['props']['lowest']['strength']
|
||||||
|
LMNY: GameState['props']['lowest']['money']
|
||||||
|
LSPR: GameState['props']['lowest']['spirit']
|
||||||
LIF: GameState['life']
|
LIF: GameState['life']
|
||||||
TLT: GameState['talents']
|
TLT: GameState['talents']
|
||||||
EVT: GameState['events']
|
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']
|
TMS: ProfileState['times']
|
||||||
AEVT: ProfileState['events']
|
AEVT: ProfileState['events']
|
||||||
@@ -97,38 +106,40 @@ type FlatMapper<Key extends FlatStateKey> = (
|
|||||||
) => FlatState[Key]
|
) => FlatState[Key]
|
||||||
|
|
||||||
const FlatMappers = {
|
const FlatMappers = {
|
||||||
AGE: state => state.game.properties.age,
|
AGE: state => state.game.props.current.age,
|
||||||
CHR: state => state.game.properties.charm,
|
CHR: state => state.game.props.current.charm,
|
||||||
INT: state => state.game.properties.intelligence,
|
INT: state => state.game.props.current.intelligence,
|
||||||
STR: state => state.game.properties.strength,
|
STR: state => state.game.props.current.strength,
|
||||||
MNY: state => state.game.properties.money,
|
MNY: state => state.game.props.current.money,
|
||||||
SPR: state => state.game.properties.spirit,
|
SPR: state => state.game.props.current.spirit,
|
||||||
|
HAGE: state => state.game.props.highest.age,
|
||||||
|
HCHR: state => state.game.props.highest.charm,
|
||||||
|
HINT: state => state.game.props.highest.intelligence,
|
||||||
|
HSTR: state => state.game.props.highest.strength,
|
||||||
|
HMNY: state => state.game.props.highest.money,
|
||||||
|
HSPR: state => state.game.props.highest.spirit,
|
||||||
|
LAGE: state => state.game.props.lowest.age,
|
||||||
|
LCHR: state => state.game.props.lowest.charm,
|
||||||
|
LINT: state => state.game.props.lowest.intelligence,
|
||||||
|
LSTR: state => state.game.props.lowest.strength,
|
||||||
|
LMNY: state => state.game.props.lowest.money,
|
||||||
|
LSPR: state => state.game.props.lowest.spirit,
|
||||||
LIF: state => state.game.life,
|
LIF: state => state.game.life,
|
||||||
TLT: state => state.game.talents,
|
TLT: state => state.game.talents,
|
||||||
EVT: state => state.game.events,
|
EVT: state => state.game.events,
|
||||||
LAGE: state => state.game.lowest.age,
|
|
||||||
HAGE: state => state.game.highest.age,
|
|
||||||
LCHR: state => state.game.lowest.charm,
|
|
||||||
HCHR: state => state.game.highest.charm,
|
|
||||||
LINT: state => state.game.lowest.intelligence,
|
|
||||||
HINT: state => state.game.highest.intelligence,
|
|
||||||
LSTR: state => state.game.lowest.strength,
|
|
||||||
HSTR: state => state.game.highest.strength,
|
|
||||||
LMNY: state => state.game.lowest.money,
|
|
||||||
HMNY: state => state.game.highest.money,
|
|
||||||
LSPR: state => state.game.lowest.spirit,
|
|
||||||
HSPR: state => state.game.highest.spirit,
|
|
||||||
TMS: state => state.profile.times,
|
TMS: state => state.profile.times,
|
||||||
AEVT: state => state.profile.events,
|
AEVT: state => state.profile.events,
|
||||||
ATLT: state => state.profile.talents,
|
ATLT: state => state.profile.talents,
|
||||||
AACH: state => state.profile.achievements,
|
AACH: state => state.profile.achievements,
|
||||||
SUM: state => {
|
SUM: ({ game: { props } }) => {
|
||||||
const { age, ...others } = state.game.highest
|
const { age, ...others } = props.highest
|
||||||
const s = sum(Object.values(others))
|
const s = sum(Object.values(others))
|
||||||
return Math.floor(s * 2 + age / 2)
|
return Math.floor(s * 2 + age / 2)
|
||||||
},
|
},
|
||||||
} as { [Key in FlatStateKey]: FlatMapper<Key> }
|
} as { [Key in FlatStateKey]: FlatMapper<Key> }
|
||||||
|
|
||||||
|
export const SupportedFlatStateKeys = new Set(Object.keys(FlatMappers))
|
||||||
|
|
||||||
const flatStateHandle = {
|
const flatStateHandle = {
|
||||||
get<Key extends FlatStateKey>(target: FlatTarget, prop: Key) {
|
get<Key extends FlatStateKey>(target: FlatTarget, prop: Key) {
|
||||||
return FlatMappers[prop]?.(target)
|
return FlatMappers[prop]?.(target)
|
||||||
@@ -140,43 +151,25 @@ export function createFlatState(game: GameState, profile: ProfileState) {
|
|||||||
return new Proxy({ game, profile }, flatStateHandle) as unknown as FlatState
|
return new Proxy({ game, profile }, flatStateHandle) as unknown as FlatState
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Effect {
|
export function propsEffect(hlp: HLProperties, effect: Partial<Properties>) {
|
||||||
properties?: Partial<Properties>
|
return produce(hlp, draft => {
|
||||||
life?: number
|
for (const key in effect) {
|
||||||
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 prop = key as keyof Properties
|
||||||
const value = effect.properties[prop]!
|
const value = effect[prop]!
|
||||||
draft.properties[prop] += value
|
draft.current[prop] += value
|
||||||
draft.highest[prop] = Math.max(
|
draft.highest[prop] = Math.max(
|
||||||
draft.highest[prop],
|
draft.highest[prop],
|
||||||
draft.properties[prop],
|
draft.current[prop],
|
||||||
)
|
)
|
||||||
draft.lowest[prop] = Math.min(
|
draft.lowest[prop] = Math.min(
|
||||||
draft.lowest[prop],
|
draft.lowest[prop],
|
||||||
draft.properties[prop],
|
draft.current[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 {
|
export function highestProperties(a: Properties, b?: Properties): Properties {
|
||||||
if (!b) return { ...a }
|
if (!b) return { ...a }
|
||||||
const result = {} as Properties
|
const result = {} as Properties
|
||||||
for (const key of Object.keys(a) as (keyof Properties)[]) {
|
for (const key of Object.keys(a) as (keyof Properties)[]) {
|
||||||
@@ -185,7 +178,7 @@ function highestProperties(a: Properties, b?: Properties): Properties {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function lowestProperties(a: Properties, b?: Properties): Properties {
|
export function lowestProperties(a: Properties, b?: Properties): Properties {
|
||||||
if (!b) return { ...a }
|
if (!b) return { ...a }
|
||||||
const result = {} as Properties
|
const result = {} as Properties
|
||||||
for (const key of Object.keys(a) as (keyof Properties)[]) {
|
for (const key of Object.keys(a) as (keyof Properties)[]) {
|
||||||
@@ -197,15 +190,15 @@ function lowestProperties(a: Properties, b?: Properties): Properties {
|
|||||||
export function nextProfile(
|
export function nextProfile(
|
||||||
profile: ProfileState,
|
profile: ProfileState,
|
||||||
state: GameState,
|
state: GameState,
|
||||||
external?: Talent['id'],
|
lockedTalent?: Talent['id'],
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
times: profile.times + 1,
|
times: profile.times + 1,
|
||||||
talents: profile.talents.union(state.talents),
|
talents: profile.talents.union(state.talents),
|
||||||
events: profile.events.union(state.events),
|
events: profile.events.union(state.events),
|
||||||
achievements: profile.achievements.union(state.achievements),
|
achievements: profile.achievements.union(state.achievements),
|
||||||
highest: highestProperties(state.highest, profile.highest),
|
highest: highestProperties(state.props.highest, profile.highest),
|
||||||
lowest: lowestProperties(state.lowest, profile.lowest),
|
lowest: lowestProperties(state.props.lowest, profile.lowest),
|
||||||
external: external ?? profile.external,
|
lockedTalent: lockedTalent ?? profile.lockedTalent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { expect, test, describe } from 'bun:test'
|
||||||
|
import type { ProfileState } from './state'
|
||||||
|
import type { TalentPullOptions } from './talent'
|
||||||
|
import { pull, exclude, additionalPoints, replacement } from './talent'
|
||||||
|
import { createState } from './state'
|
||||||
|
import talents from '@remake/data/talent'
|
||||||
|
import { produce } from 'immer'
|
||||||
|
import { enableMapSet } from 'immer'
|
||||||
|
enableMapSet()
|
||||||
|
|
||||||
|
describe('Talent', () => {
|
||||||
|
const profile = {
|
||||||
|
times: 0,
|
||||||
|
talents: new Set([]),
|
||||||
|
achievements: new Set([]),
|
||||||
|
events: new Set([]),
|
||||||
|
} as ProfileState
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
count: 10,
|
||||||
|
rate: {
|
||||||
|
base: new Map([
|
||||||
|
[0, 1000 - 111],
|
||||||
|
[1, 100],
|
||||||
|
[2, 10],
|
||||||
|
[3, 1],
|
||||||
|
]),
|
||||||
|
additions: {},
|
||||||
|
},
|
||||||
|
} as TalentPullOptions
|
||||||
|
|
||||||
|
test('pull external', () => {
|
||||||
|
const p = produce(profile, draft => {
|
||||||
|
draft.lockedTalent = 1001
|
||||||
|
})
|
||||||
|
const result = pull(options, p)
|
||||||
|
expect(result.length).toBe(options.count)
|
||||||
|
expect(result).toContain(p.lockedTalent!)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('pull rate', () => {
|
||||||
|
const opts = produce(options, draft => {
|
||||||
|
draft.rate.additions = {
|
||||||
|
times: () =>
|
||||||
|
new Map([
|
||||||
|
[0, { mode: 'multiply', value: 0 }],
|
||||||
|
[1, { mode: 'multiply', value: 0 }],
|
||||||
|
[2, { mode: 'multiply', value: 0 }],
|
||||||
|
[3, { mode: 'multiply', value: 1 }],
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const result = pull(opts, profile)
|
||||||
|
expect(result.some(id => talents.get(id)?.grade === 3)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('exclude', () => {
|
||||||
|
expect(exclude(1003, [1004])).toBe(1004)
|
||||||
|
expect(exclude(1004, [1003])).toBe(1003)
|
||||||
|
expect(exclude(1001, [1002, 1003])).toBe(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('additionalPoints', () => {
|
||||||
|
const result = additionalPoints([1001, 1002, 1003, 1007, 1019])
|
||||||
|
expect(result).toEqual({
|
||||||
|
source: [
|
||||||
|
{ talent: 1007, points: 2 },
|
||||||
|
{ talent: 1019, points: 4 },
|
||||||
|
],
|
||||||
|
points: 6,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const allo = { charm: 5, intelligence: 5, strength: 5, money: 5 }
|
||||||
|
test('replacement talent [阴间福袋]', () => {
|
||||||
|
const talent = 1145
|
||||||
|
const state = createState(allo, [talent])
|
||||||
|
const result = replacement(state)
|
||||||
|
expect(result.changed).toBe(true)
|
||||||
|
expect(result.state.talents.size).toBeGreaterThan(1)
|
||||||
|
expect(result.chains.get(talent)).toBeDefined()
|
||||||
|
const source = talents.get(talent)!.replacement!.talent!
|
||||||
|
const replaced = result.chains.get(talent)!.at(0)!
|
||||||
|
expect(source.map(t => t[0])).toContain(replaced)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('replacement grade [橙色转盘]', () => {
|
||||||
|
const talent = 1144
|
||||||
|
const state = createState(allo, [talent])
|
||||||
|
const result = replacement(state)
|
||||||
|
expect(result.changed).toBe(true)
|
||||||
|
expect(result.state.talents.size).toBeGreaterThan(1)
|
||||||
|
expect(result.chains.get(talent)).toBeDefined()
|
||||||
|
const source = talents.get(talent)!.replacement!.grade!
|
||||||
|
const replaced = result.chains.get(talent)!.at(0)!
|
||||||
|
expect(talents.get(replaced)?.grade).toBe(source)
|
||||||
|
})
|
||||||
|
})
|
||||||
+206
-12
@@ -1,16 +1,17 @@
|
|||||||
import type { TalentEffect } from '@remake/data/talent'
|
import type { Talent, TalentGrade } from '@remake/data/talent'
|
||||||
import talents from '@remake/data/talent'
|
import talents from '@remake/data/talent'
|
||||||
import type { Properties } from './state'
|
import type { Properties } from '@/state'
|
||||||
import { pick } from '@remake/vitex'
|
import type { GameState, ProfileState } from '@/state'
|
||||||
|
import { propsEffect, createFlatState } from './state'
|
||||||
|
import { check } from '@remake/condition'
|
||||||
|
import { pick, pickWeight } from '@remake/vitex'
|
||||||
|
import { produce } from 'immer'
|
||||||
|
|
||||||
const TalentEffectPropertiesKeyMapper = {
|
const Grades: TalentGrade[] = [0, 1, 2, 3] as const
|
||||||
MNY: () => 'money',
|
const GradeMap = Map.groupBy(
|
||||||
STR: () => 'strength',
|
talents.keys().filter(t => !talents.get(t)!.exclusive),
|
||||||
INT: () => 'intelligence',
|
t => talents.get(t)!.grade,
|
||||||
CHR: () => 'charm',
|
)
|
||||||
SPR: () => 'spirit',
|
|
||||||
RND: () => pick(['money', 'strength', 'intelligence', 'charm', 'spirit'])!,
|
|
||||||
} as Record<keyof TalentEffect, () => keyof Properties>
|
|
||||||
|
|
||||||
export const count = talents.size
|
export const count = talents.size
|
||||||
|
|
||||||
@@ -18,4 +19,197 @@ export function get(talent: number) {
|
|||||||
return talents.get(talent)
|
return talents.get(talent)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function random(talent: number) {}
|
export type TalentPullRate = Map<TalentGrade, number>
|
||||||
|
export type TalentPullRateAddition = {
|
||||||
|
mode: 'add' | 'multiply'
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
export type TalentPullRateAdditions = {
|
||||||
|
[key in keyof ProfileState]?: (
|
||||||
|
value: ProfileState[key],
|
||||||
|
) => Map<TalentGrade, TalentPullRateAddition>
|
||||||
|
}
|
||||||
|
export interface TalentRateOptions {
|
||||||
|
base: TalentPullRate
|
||||||
|
additions: TalentPullRateAdditions
|
||||||
|
}
|
||||||
|
const AddidtionInit = Grades.map(g => [g, 1]) as [TalentGrade, number][]
|
||||||
|
function talentRateWithAddition(
|
||||||
|
{ base, additions }: TalentRateOptions,
|
||||||
|
profile: ProfileState,
|
||||||
|
) {
|
||||||
|
const rate = new Map(AddidtionInit)
|
||||||
|
for (const [key, getAddition] of Object.entries(additions)) {
|
||||||
|
const value = profile[key as keyof ProfileState]
|
||||||
|
for (const [k, v] of getAddition(value as any).entries()) {
|
||||||
|
const current = rate.get(k)!
|
||||||
|
if (v.mode === 'add') {
|
||||||
|
rate.set(k, current + v.value)
|
||||||
|
} else if (v.mode === 'multiply') {
|
||||||
|
rate.set(k, current * v.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [g, addition] of rate.entries()) {
|
||||||
|
const b = base.get(g)
|
||||||
|
if (!b) throw new Error(`talent: base rate for grade ${g} is missing`)
|
||||||
|
rate.set(g, Math.max(b * addition, 0))
|
||||||
|
}
|
||||||
|
return rate
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TalentPullOptions {
|
||||||
|
count: number
|
||||||
|
rate: TalentRateOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pull(options: TalentPullOptions, profile: ProfileState) {
|
||||||
|
const rate = Array.from(
|
||||||
|
talentRateWithAddition(options.rate, profile).entries(),
|
||||||
|
)
|
||||||
|
const result = []
|
||||||
|
if (profile.lockedTalent) result.push(profile.lockedTalent)
|
||||||
|
const map = new Map(Grades.map(g => [g, new Set(GradeMap.get(g))]))
|
||||||
|
for (let i = options.count - result.length; i > 0; i--) {
|
||||||
|
const grade = pickWeight(rate)! ?? 0
|
||||||
|
const set = map.get(grade)!
|
||||||
|
if (set.size === 0) continue
|
||||||
|
const id = pick(Array.from(set))!
|
||||||
|
result.push(id)
|
||||||
|
set.delete(id)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exclude(talent: Talent['id'], list: Iterable<Talent['id']>) {
|
||||||
|
const { exclude } = talents.get(talent)!
|
||||||
|
for (const t of list) {
|
||||||
|
if (exclude) {
|
||||||
|
for (const e of exclude) {
|
||||||
|
if (t == e) return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const excludeReverse = talents.get(t)!.exclude
|
||||||
|
if (excludeReverse) {
|
||||||
|
for (const e of excludeReverse) {
|
||||||
|
if (talent == e) return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TalentReplacementResult {
|
||||||
|
state: GameState
|
||||||
|
chains: Map<Talent['id'], Talent['id'][]>
|
||||||
|
changed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function chainReplace(t: Talent['id'], ts: Set<Talent['id']>) {
|
||||||
|
const { replacement: r } = talents.get(t)!
|
||||||
|
if (!r) return null
|
||||||
|
let picked: Talent['id'] | null = null
|
||||||
|
if (r.talent) {
|
||||||
|
const filtered = r.talent.filter(([id]) => exclude(id, ts) == null)
|
||||||
|
picked = pickWeight(filtered)
|
||||||
|
} else if (r.grade) {
|
||||||
|
const filtered = GradeMap.get(r.grade)!.filter(
|
||||||
|
id => exclude(id, ts) == null,
|
||||||
|
)
|
||||||
|
picked = pick(filtered)
|
||||||
|
}
|
||||||
|
if (!picked) return null
|
||||||
|
const nts = new Set(ts)
|
||||||
|
nts.add(picked)
|
||||||
|
const next = chainReplace(picked, nts) as Talent['id'][] | null
|
||||||
|
if (next) return [picked, ...next]
|
||||||
|
return [picked]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replacement(state: GameState): TalentReplacementResult {
|
||||||
|
const chains = new Map() as TalentReplacementResult['chains']
|
||||||
|
const newTalents = new Set(state.talents)
|
||||||
|
let changed = false
|
||||||
|
for (const talent of state.talents) {
|
||||||
|
const chain = chainReplace(talent, newTalents)
|
||||||
|
if (!chain) continue
|
||||||
|
changed = true
|
||||||
|
chains.set(talent, chain)
|
||||||
|
chain.forEach(t => newTalents.add(t))
|
||||||
|
}
|
||||||
|
if (!changed) return { state, chains, changed }
|
||||||
|
const newState = produce(state, draft => {
|
||||||
|
draft.talents = newTalents
|
||||||
|
})
|
||||||
|
return { state: newState, chains, changed }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdditionalPoint {
|
||||||
|
talent: Talent['id']
|
||||||
|
points: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdditionalPoints {
|
||||||
|
source: AdditionalPoint[]
|
||||||
|
points: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function additionalPoints(list: Talent['id'][]) {
|
||||||
|
const source: AdditionalPoint[] = []
|
||||||
|
let total = 0
|
||||||
|
for (const id of list) {
|
||||||
|
const { points } = talents.get(id)!
|
||||||
|
if (!points) continue
|
||||||
|
source.push({ talent: id, points })
|
||||||
|
total += points
|
||||||
|
}
|
||||||
|
return { source, points: total }
|
||||||
|
}
|
||||||
|
|
||||||
|
const TalentEffectRandomProperties = [
|
||||||
|
'money',
|
||||||
|
'strength',
|
||||||
|
'intelligence',
|
||||||
|
'charm',
|
||||||
|
'spirit',
|
||||||
|
] as (keyof Properties)[]
|
||||||
|
export interface TalentTriggerResult {
|
||||||
|
state: GameState
|
||||||
|
triggers: Talent['id'][]
|
||||||
|
changed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trigger(
|
||||||
|
state: GameState,
|
||||||
|
profile: ProfileState,
|
||||||
|
): TalentTriggerResult {
|
||||||
|
const flatState = createFlatState(state, profile)
|
||||||
|
const triggers: Talent['id'][] = []
|
||||||
|
for (const talent of state.talents) {
|
||||||
|
const { condition, max } = talents.get(talent)!
|
||||||
|
if (state.talentTriggers.get(talent) ?? 0 >= max) continue
|
||||||
|
if (condition && !check(flatState, condition)) continue
|
||||||
|
triggers.push(talent)
|
||||||
|
}
|
||||||
|
if (triggers.length === 0) return { state, triggers, changed: false }
|
||||||
|
const newState = produce(state, draft => {
|
||||||
|
for (const talent of triggers) {
|
||||||
|
const times = draft.talentTriggers.get(talent) ?? 0
|
||||||
|
draft.talentTriggers.set(talent, times + 1)
|
||||||
|
const { effect } = talents.get(talent)!
|
||||||
|
if (!effect) continue
|
||||||
|
const pe = {} as Partial<Properties>
|
||||||
|
if (effect.CHR) pe.charm = effect.CHR
|
||||||
|
if (effect.INT) pe.intelligence = effect.INT
|
||||||
|
if (effect.STR) pe.strength = effect.STR
|
||||||
|
if (effect.MNY) pe.money = effect.MNY
|
||||||
|
if (effect.SPR) pe.spirit = effect.SPR
|
||||||
|
if (effect.RND) {
|
||||||
|
const key = pick(TalentEffectRandomProperties)!
|
||||||
|
pe[key] = (pe[key] ?? 0) || effect.RND
|
||||||
|
}
|
||||||
|
draft.props = propsEffect(draft.props, pe)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { state: newState, triggers, changed: true }
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,14 +20,21 @@ export type TalentEffect = {
|
|||||||
/** 权重天赋 */
|
/** 权重天赋 */
|
||||||
export type TalentWithWeight = [number, number]
|
export type TalentWithWeight = [number, number]
|
||||||
|
|
||||||
/** 天赋盲盒 */
|
/** 指定列表盲盒 */
|
||||||
export type TalentReplacement = {
|
export type TalentReplacementTalent = {
|
||||||
/** 指定列表盲盒 */
|
talent: TalentWithWeight[]
|
||||||
talent?: TalentWithWeight[]
|
grade?: never
|
||||||
/** 指定稀有度盲盒 */
|
|
||||||
grade?: TalentGrade[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 指定稀有度盲盒 */
|
||||||
|
export type TalentReplacementGrade = {
|
||||||
|
grade: TalentGrade
|
||||||
|
talent?: never
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 天赋盲盒 */
|
||||||
|
export type TalentReplacement = TalentReplacementTalent | TalentReplacementGrade
|
||||||
|
|
||||||
/** 天赋 */
|
/** 天赋 */
|
||||||
export type Talent = {
|
export type Talent = {
|
||||||
/** 序号 */
|
/** 序号 */
|
||||||
@@ -45,11 +52,13 @@ export type Talent = {
|
|||||||
/** 专属天赋 */
|
/** 专属天赋 */
|
||||||
readonly exclusive?: boolean
|
readonly exclusive?: boolean
|
||||||
/** 初始可用属性点调整 */
|
/** 初始可用属性点调整 */
|
||||||
readonly status?: number
|
readonly points?: number
|
||||||
/** 互斥天赋 */
|
/** 互斥天赋 */
|
||||||
readonly exclude?: number[]
|
readonly exclude?: number[]
|
||||||
/** 天赋盲盒 */
|
/** 天赋盲盒 */
|
||||||
readonly replacement?: TalentReplacement
|
readonly replacement?: TalentReplacement
|
||||||
|
/** 天赋触发上限 */
|
||||||
|
readonly max: number
|
||||||
}
|
}
|
||||||
// @vt-types-end
|
// @vt-types-end
|
||||||
|
|
||||||
@@ -80,7 +89,8 @@ export const transformers = {
|
|||||||
)
|
)
|
||||||
return [talent, weight ?? 1]
|
return [talent, weight ?? 1]
|
||||||
})
|
})
|
||||||
if (val.grade) val.grade = val.grade.map(Number)
|
if (val.grade) val.grade = Number(val.grade)
|
||||||
return val
|
return val
|
||||||
},
|
},
|
||||||
|
max: (val: any) => Number(val) || 1,
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -5,7 +5,7 @@ export function random(max: number, min: number = 0, rng?: RNG): number {
|
|||||||
}
|
}
|
||||||
export function pick<T>(items: T[], rng?: RNG) {
|
export function pick<T>(items: T[], rng?: RNG) {
|
||||||
if (items.length === 0) return null
|
if (items.length === 0) return null
|
||||||
return items[random(items.length - 1, 0, rng)]
|
return items[random(items.length - 1, 0, rng)] ?? null
|
||||||
}
|
}
|
||||||
export type WeightItem<T> = [T, number]
|
export type WeightItem<T> = [T, number]
|
||||||
export function pickWeight<T>(items: WeightItem<T>[], rng?: RNG) {
|
export function pickWeight<T>(items: WeightItem<T>[], rng?: RNG) {
|
||||||
|
|||||||
Reference in New Issue
Block a user