mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-27 08:46:48 +08:00
add: character
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import type { Talent } from '@remake/data/talent'
|
||||
import { type Talent, talents } from '@remake/data'
|
||||
import TalentComponent from './Talent'
|
||||
import talents from '@remake/data/talent'
|
||||
import './Replaced.css'
|
||||
|
||||
export interface TalentProps {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Talent } from '@remake/data/talent'
|
||||
import talents from '@remake/data/talent'
|
||||
import { type Talent, talents } from '@remake/data'
|
||||
import './Talent.css'
|
||||
|
||||
export interface TalentProps {
|
||||
|
||||
@@ -27,6 +27,11 @@ export const AllocLimit = dev.allocate ?? 10
|
||||
export const DefaultSpirit = dev.spirit ?? 5
|
||||
// 天赋抽取个数
|
||||
export const PullCount = dev.pull ?? 10
|
||||
// 名人模式抽取个数
|
||||
export const CharacterPullCount = dev.chara ?? 3
|
||||
// 名人模式抽取权重切线
|
||||
export const CharacterWeightKnife = dev.knife ?? 10
|
||||
|
||||
// 天赋抽取基础概率
|
||||
export const PullRateBase: Config['pull']['rate']['base'] = new Map([
|
||||
[0, 889],
|
||||
@@ -113,6 +118,10 @@ export const config: Config = {
|
||||
additions: PullRateAdditions,
|
||||
},
|
||||
},
|
||||
chara: {
|
||||
count: CharacterPullCount,
|
||||
knife: CharacterWeightKnife,
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useLayoutEffect, useEffect } from 'react'
|
||||
import { useNext, useGotoSummary } from '@remake/hooks'
|
||||
import { useNext, useGotoSummary, type Log } from '@remake/hooks'
|
||||
import { useJudge } from '@/hooks/judge'
|
||||
import type { Log } from '@remake/hooks'
|
||||
import { properties } from '@/display'
|
||||
import { achievements, events, talents } from '@remake/data'
|
||||
import { properties } from '@/display'
|
||||
import { AutoInterval } from '@/config'
|
||||
import { format } from '@remake/vitex'
|
||||
import './Play.css'
|
||||
|
||||
@@ -16,16 +16,14 @@ if (localStorage.getItem('version') !== '3.0.0') {
|
||||
localStorage.getItem('extendTalent') || 'null',
|
||||
)
|
||||
if (lockedTalents) profile.locked = [lockedTalents]
|
||||
const unique = JSON.parse(
|
||||
localStorage.getItem('uniqueWaTaShi') || 'null',
|
||||
)
|
||||
if (unique) profile.unique = unique
|
||||
localStorage.setItem('profile', JSON.stringify(profile))
|
||||
localStorage.removeItem('times')
|
||||
localStorage.removeItem('ACHV')
|
||||
localStorage.removeItem('AEVT')
|
||||
localStorage.removeItem('ATLT')
|
||||
localStorage.removeItem('extendTalent')
|
||||
const unique = localStorage.getItem('uniqueWaTaShi')
|
||||
if (unique) localStorage.setItem('unique', unique)
|
||||
localStorage.removeItem('uniqueWaTaShi')
|
||||
}
|
||||
localStorage.setItem('version', '3.0.0')
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { pullCharacter, uniqueGenerate } from './character'
|
||||
import { enableMapSet } from 'immer'
|
||||
enableMapSet()
|
||||
|
||||
describe('Character', () => {
|
||||
const sum = (map: Map<any, number>) =>
|
||||
Array.from(map.values()).reduce((acc, val) => acc + val, 0)
|
||||
test('pull', () => {
|
||||
let result = pullCharacter(
|
||||
{ count: 10, knife: 10 },
|
||||
{ times: 0, drawns: new Map() },
|
||||
)
|
||||
expect(result.characters).not.toContainValue(null)
|
||||
expect(result.characters.length).toBe(10)
|
||||
expect(result.times.times).toBe(10)
|
||||
expect(result.times.drawns.size).toBe(10)
|
||||
result = pullCharacter({ count: 3, knife: 10 }, result.times)
|
||||
expect(result.characters).not.toContainValue(null)
|
||||
expect(result.characters.length).toBe(3)
|
||||
expect(result.times.times).toBe(13)
|
||||
expect(sum(result.times.drawns)).toBe(13)
|
||||
result = pullCharacter({ count: 3, knife: 10 }, result.times)
|
||||
expect(result.characters).not.toContainValue(null)
|
||||
expect(result.characters.length).toBe(3)
|
||||
expect(result.times.times).toBe(16)
|
||||
expect(sum(result.times.drawns)).toBe(16)
|
||||
result = pullCharacter({ count: 20, knife: 1 }, result.times)
|
||||
expect(result.characters).not.toContainValue(null)
|
||||
expect(result.characters.length).toBe(20)
|
||||
expect(result.times.times).toBe(36)
|
||||
expect(sum(result.times.drawns)).toBe(36)
|
||||
})
|
||||
|
||||
const wr = (s: number, e: number) => {
|
||||
const length = e - s + 1
|
||||
return Array.from(
|
||||
{ length },
|
||||
(_, i) => [s + i, Math.min(i + 1, length - i)] as const,
|
||||
)
|
||||
}
|
||||
|
||||
test('unique', () => {
|
||||
const unique = uniqueGenerate({ prop: wr(0, 10), talent: wr(1, 5) })
|
||||
expect(unique.property).toHaveProperty('CHR')
|
||||
expect(unique.property).toHaveProperty('INT')
|
||||
expect(unique.property).toHaveProperty('STR')
|
||||
expect(unique.property).toHaveProperty('MNY')
|
||||
expect(unique.property.CHR).toBeGreaterThanOrEqual(0)
|
||||
expect(unique.property.INT).toBeGreaterThanOrEqual(0)
|
||||
expect(unique.property.STR).toBeGreaterThanOrEqual(0)
|
||||
expect(unique.property.MNY).toBeGreaterThanOrEqual(0)
|
||||
expect(unique.talent.length).toBeGreaterThanOrEqual(0)
|
||||
expect(unique.property.CHR).toBeLessThanOrEqual(10)
|
||||
expect(unique.property.INT).toBeLessThanOrEqual(10)
|
||||
expect(unique.property.STR).toBeLessThanOrEqual(10)
|
||||
expect(unique.property.MNY).toBeLessThanOrEqual(10)
|
||||
expect(unique.talent.length).toBeLessThanOrEqual(5)
|
||||
console.debug(unique)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
import { type Character, type Talent, characters, talents } from '@remake/data'
|
||||
import { type RNG, type WeightItem, pick, pickWeight } from '@remake/vitex'
|
||||
|
||||
export type UniqueChara = Omit<Character, 'id' | 'name'>
|
||||
export interface UniqueGenCfg {
|
||||
prop: WeightItem<number>[]
|
||||
talent: WeightItem<number>[]
|
||||
}
|
||||
export function uniqueGenerate(config: UniqueGenCfg, rng?: RNG) {
|
||||
const CHR = pickWeight(config.prop, rng) ?? 0
|
||||
const INT = pickWeight(config.prop, rng) ?? 0
|
||||
const STR = pickWeight(config.prop, rng) ?? 0
|
||||
const MNY = pickWeight(config.prop, rng) ?? 0
|
||||
let count = pickWeight(config.talent, rng) ?? 0
|
||||
const property = { CHR, INT, STR, MNY }
|
||||
const ids = Array.from(talents.keys())
|
||||
const picked = new Set<Talent['id']>()
|
||||
while (count > 0) {
|
||||
const t = pick(ids, rng)!
|
||||
if (picked.has(t)) continue
|
||||
if (talents.get(t)!.exclusive) continue
|
||||
picked.add(t)
|
||||
count--
|
||||
}
|
||||
return { property, talent: Array.from(picked) } satisfies UniqueChara
|
||||
}
|
||||
|
||||
export interface PullCharaOpt {
|
||||
count: number
|
||||
knife: number
|
||||
}
|
||||
export interface PullCharaTms {
|
||||
times: number
|
||||
drawns: Map<Character['id'], number>
|
||||
}
|
||||
export interface PullCharaRet {
|
||||
characters: Character['id'][]
|
||||
times: PullCharaTms
|
||||
}
|
||||
export function pullCharacter(
|
||||
opt: PullCharaOpt,
|
||||
tms: PullCharaTms,
|
||||
rng?: RNG,
|
||||
): PullCharaRet {
|
||||
const { count, knife } = opt
|
||||
const drawns = new Map(tms.drawns)
|
||||
const picked = new Set<Character['id']>()
|
||||
for (let i = 0; i < count; i++) {
|
||||
const weightMap = deriveWeightMap(tms.times + i, knife, drawns)
|
||||
picked.forEach(id => weightMap.delete(id))
|
||||
const id = pickWeight(Array.from(weightMap.entries()), rng)!
|
||||
picked.add(id)
|
||||
drawns.set(id, (drawns.get(id) ?? 0) + 1)
|
||||
}
|
||||
return {
|
||||
characters: Array.from(picked),
|
||||
times: { times: tms.times + count, drawns },
|
||||
}
|
||||
}
|
||||
|
||||
function deriveWeightMap(
|
||||
times: number,
|
||||
knife: number,
|
||||
drawns: PullCharaTms['drawns'],
|
||||
): Map<Character['id'], number> {
|
||||
const max = Math.max(0, ...drawns.values())
|
||||
const base = times - knife * Math.floor((times - max) / (knife || 1)) || 1
|
||||
return new Map(
|
||||
Array.from(characters.keys(), id => [id, base - (drawns.get(id) ?? 0)]),
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { Event } from '@remake/data/event'
|
||||
import events from '@remake/data/event'
|
||||
import type { Properties } from './state'
|
||||
import type { GameState, ProfileState } from './state'
|
||||
import { type Event, events } from '@remake/data'
|
||||
import type { Properties, GameState, ProfileState } from './state'
|
||||
import { propsEffect, createFlatState } from './state'
|
||||
import { check as checkCondition } from '@remake/condition'
|
||||
import { produce } from 'immer'
|
||||
import type { TriggerResult } from './game'
|
||||
import { produce } from 'immer'
|
||||
|
||||
export function check(
|
||||
event: Event['id'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { createState, propsEffect, summary as stateSummary } from './state'
|
||||
import { talentsPicked, start, next, summary, end } from './game'
|
||||
import { pick, start, next, summary, end } from './game'
|
||||
import { produce } from 'immer'
|
||||
import { enableMapSet } from 'immer'
|
||||
enableMapSet()
|
||||
@@ -22,7 +22,7 @@ describe('Achievement', () => {
|
||||
|
||||
test('talentsPicked', () => {
|
||||
// 挑战者、阴间福袋、轮盘赌
|
||||
const result = talentsPicked([1122, 1145, 1146])
|
||||
const result = pick([1122, 1145, 1146])
|
||||
expect(result.talents.chains.size).toBeGreaterThan(1)
|
||||
expect(result.additionalPoints.source).toContainEqual({
|
||||
talent: 1122,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { Achievement, Event, Talent } from '@remake/data'
|
||||
import { ages, AchievementOpportunity as Ao } from '@remake/data'
|
||||
import type { Allocation, GameState, ProfileState } from './state'
|
||||
import type { Properties } from './state'
|
||||
import type { GameState, ProfileState } from './state'
|
||||
import { createState, nextProfile, propsEffect } from './state'
|
||||
import { summary as stateSummary } from './state'
|
||||
import type { PullOptions, ReplacementResult } from './talent'
|
||||
import type { AdditionalPoint, AdditionalPoints } from './talent'
|
||||
import type { ReplacementResult, AdditionalPoints } from './talent'
|
||||
import { pull, exclude, replacement, additionalPoints } from './talent'
|
||||
import { trigger as ttr } from './talent'
|
||||
import { trigger as atr } from './achievement'
|
||||
@@ -19,14 +17,11 @@ export interface TriggerResult<T> {
|
||||
state: GameState
|
||||
triggers: T[]
|
||||
}
|
||||
export interface TalentsPickedResult {
|
||||
export interface PickResult {
|
||||
talents: ReplacementResult
|
||||
additionalPoints: AdditionalPoints
|
||||
}
|
||||
export function talentsPicked(
|
||||
talents: Iterable<Talent['id']>,
|
||||
rng?: RNG,
|
||||
): TalentsPickedResult {
|
||||
export function pick(talents: Iterable<Talent['id']>, rng?: RNG): PickResult {
|
||||
const r = replacement(talents, rng)
|
||||
const ap = additionalPoints(r.talents)
|
||||
return { talents: r, additionalPoints: ap }
|
||||
@@ -110,6 +105,3 @@ export function end(
|
||||
}
|
||||
|
||||
export { pull, exclude }
|
||||
export type { GameState, ProfileState, Properties, RNG }
|
||||
export type { PullOptions, ReplacementResult, Allocation }
|
||||
export type { AdditionalPoint, AdditionalPoints }
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
export * from './game'
|
||||
export type { RNG } from '@remake/vitex'
|
||||
export type { GameState, ProfileState, Properties, Allocation } from './state'
|
||||
export type { PullOptions, ReplacementResult } from './talent'
|
||||
export type { AdditionalPoint, AdditionalPoints } from './talent'
|
||||
export type { PullCharaOpt, PullCharaTms, PullCharaRet } from './character'
|
||||
export type { UniqueChara, UniqueGenCfg } from './character'
|
||||
export type { PickResult, StartResult } from './game'
|
||||
export type { NextResult, SummaryResult, EndResult } from './game'
|
||||
export { pull, exclude } from './talent'
|
||||
export { pick, start, next, summary, end } from './game'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Achievement, Event, Talent, Character } from '@remake/data'
|
||||
import type { Achievement, Event, Talent } from '@remake/data'
|
||||
import { produce } from 'immer'
|
||||
import { sum, keys } from '@remake/vitex'
|
||||
|
||||
@@ -37,9 +37,6 @@ export interface GameState {
|
||||
talentTriggers: Map<Talent['id'], number> // 本局天赋触发次数
|
||||
}
|
||||
|
||||
/** 唯一角色 */
|
||||
export type UniqueCharacter = Omit<Character, 'id' | 'name'>
|
||||
|
||||
/** 持久化存储的数据 */
|
||||
export interface ProfileState {
|
||||
times: number // 游戏次数
|
||||
@@ -49,7 +46,6 @@ export interface ProfileState {
|
||||
achievements: Set<Achievement['id']> // 达成的成就
|
||||
highest?: Properties // 历史最高属性
|
||||
lowest?: Properties // 历史最低属性
|
||||
unique?: UniqueCharacter // 唯一角色属性
|
||||
}
|
||||
|
||||
export function createState(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { Talent, TalentGrade } from '@remake/data'
|
||||
import { talents } from '@remake/data'
|
||||
import type { Properties } from './state'
|
||||
import type { GameState, ProfileState } from './state'
|
||||
import { type Talent, type TalentGrade, talents } from '@remake/data'
|
||||
import type { Properties, GameState, ProfileState } from './state'
|
||||
import { propsEffect, createFlatState } from './state'
|
||||
import { check } from '@remake/condition'
|
||||
import { pick, pickWeight, type RNG } from '@remake/vitex'
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useCallback } from 'react'
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useConfig } from './config'
|
||||
import { useReplaced } from './talent'
|
||||
import { useConfig, useReplaced } from '.'
|
||||
import type { Allocation } from '@remake/core'
|
||||
import type { RNG } from '@remake/vitex'
|
||||
import { keys, shuffle, random as frandom } from '@remake/vitex'
|
||||
import { keys, shuffle, random as frandom, type RNG } from '@remake/vitex'
|
||||
|
||||
export type UserAllocation = Omit<Allocation, 'spirit'>
|
||||
const init: UserAllocation = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback } from 'react'
|
||||
import { atom, useSetAtom, useAtomValue } from 'jotai'
|
||||
import type { PullOptions } from '@remake/core'
|
||||
|
||||
import type { PullOptions, PullCharaOpt } from '@remake/core'
|
||||
export interface Config {
|
||||
/** 游戏锁定天赋数量 */
|
||||
lock: number
|
||||
@@ -9,8 +8,6 @@ export interface Config {
|
||||
max: number
|
||||
/** 游戏最少选择天赋数量 */
|
||||
min: number
|
||||
/** 天赋抽取个数 */
|
||||
pull: PullOptions
|
||||
/** 初始属性点 */
|
||||
points: number
|
||||
/** 单项属性点最大限制 */
|
||||
@@ -19,6 +16,10 @@ export interface Config {
|
||||
spirit: number
|
||||
/** 模式选择限制 */
|
||||
mode: number
|
||||
/** 天赋抽取个数 */
|
||||
pull: PullOptions
|
||||
/** 名人模式配置 */
|
||||
chara: PullCharaOpt
|
||||
}
|
||||
|
||||
export const configAtom = atom<Config | null>(null)
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { atom, useSetAtom, useAtomValue, useAtom } from 'jotai'
|
||||
import { useConfig } from './config'
|
||||
import { useProfile } from './profile'
|
||||
import { useReplaced, useTalentReset } from './talent'
|
||||
import { useAlloc, useAllocReset } from './alloc'
|
||||
import { useConfig, useProfile, useReplaced, useAlloc } from '.'
|
||||
import { useTalentReset, useAllocReset } from '.'
|
||||
import { start, next, summary, end } from '@remake/core'
|
||||
import type { GameState, Properties, NextResult } from '@remake/core'
|
||||
import type { Talent } from '@remake/data/talent'
|
||||
import type { Talent } from '@remake/data'
|
||||
|
||||
export enum Step {
|
||||
Idle = 'idle',
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useConfig } from './config'
|
||||
import { useProfile } from './profile'
|
||||
import { useSetStep } from './play'
|
||||
import { pull, exclude, talentsPicked } from '@remake/core'
|
||||
import type { TalentsPickedResult, RNG } from '@remake/core'
|
||||
import type { Talent } from '@remake/data/talent'
|
||||
import { useConfig, useProfile, useSetStep } from '.'
|
||||
import { pull, exclude, pick, type PickResult } from '@remake/core'
|
||||
import type { Talent } from '@remake/data'
|
||||
import type { RNG } from '@remake/vitex'
|
||||
|
||||
export const pickedAtom = atom(new Set<Talent['id']>())
|
||||
export const replacedAtom = atom<TalentsPickedResult | null>(null)
|
||||
export const replacedAtom = atom<PickResult | null>(null)
|
||||
|
||||
export const useTalentReset = () => {
|
||||
const setPicked = useSetAtom(pickedAtom)
|
||||
@@ -41,25 +39,16 @@ export const useTalentPuller = () => {
|
||||
return [pulled, puller] as const
|
||||
}
|
||||
|
||||
export type TalentPickerResult =
|
||||
| {
|
||||
type: 'ok'
|
||||
talent?: never
|
||||
}
|
||||
| {
|
||||
type: 'not-enough'
|
||||
talent?: never
|
||||
}
|
||||
| {
|
||||
type: 'exclude'
|
||||
talent: Talent['id']
|
||||
}
|
||||
export type PickOk = { type: 'ok'; talent?: never }
|
||||
export type PickNe = { type: 'ne'; talent?: never }
|
||||
export type PickEx = { type: 'ex'; talent: Talent['id'] }
|
||||
export type PickerResult = PickOk | PickNe | PickEx
|
||||
|
||||
export const useTalentPicker = () => {
|
||||
const { max } = useConfig()
|
||||
const [picked, setPicked] = useAtom(pickedAtom)
|
||||
const picker = useCallback(
|
||||
(talent: Talent['id']): TalentPickerResult => {
|
||||
(talent: Talent['id']): PickerResult => {
|
||||
if (!picked) {
|
||||
setPicked(new Set([talent]))
|
||||
return { type: 'ok' }
|
||||
@@ -70,9 +59,9 @@ export const useTalentPicker = () => {
|
||||
setPicked(next)
|
||||
return { type: 'ok' }
|
||||
}
|
||||
if (picked.size >= max) return { type: 'not-enough' }
|
||||
if (picked.size >= max) return { type: 'ne' }
|
||||
const e = exclude(talent, picked)
|
||||
if (e) return { type: 'exclude', talent: e }
|
||||
if (e) return { type: 'ex', talent: e }
|
||||
const next = new Set([...picked, talent])
|
||||
setPicked(next)
|
||||
return { type: 'ok' }
|
||||
@@ -97,7 +86,7 @@ export const useTalentSubmit = () => {
|
||||
return useCallback(
|
||||
(rng?: RNG) => {
|
||||
if (!enabled) throw new Error('Not enough talents picked')
|
||||
setReplaced(talentsPicked(picked, rng))
|
||||
setReplaced(pick(picked, rng))
|
||||
setStep(Step.Alloc)
|
||||
},
|
||||
[enabled, picked, setReplaced, setStep],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type RNG = (max?: number, min?: number) => number
|
||||
export function random(max: number, min: number = 0, rng?: RNG): number {
|
||||
export function random(max: number = 1, min: number = 0, rng?: RNG): number {
|
||||
if (rng) return rng(max, min)
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export function pick<T>(items: T[], rng?: RNG) {
|
||||
if (items.length === 0) return null
|
||||
return items[random(items.length - 1, 0, rng)] ?? null
|
||||
}
|
||||
export type WeightItem<T> = [T, number]
|
||||
export type WeightItem<T> = readonly [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)
|
||||
|
||||
Reference in New Issue
Block a user