mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-28 17:26:47 +08:00
update: character
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { pullCharacter, uniqueGenerate } from './character'
|
||||
import { pullChara, uniqueGenerate } from './character'
|
||||
import { enableMapSet } from 'immer'
|
||||
enableMapSet()
|
||||
|
||||
@@ -7,7 +7,7 @@ describe('Character', () => {
|
||||
const sum = (map: Map<any, number>) =>
|
||||
Array.from(map.values()).reduce((acc, val) => acc + val, 0)
|
||||
test('pull', () => {
|
||||
let result = pullCharacter(
|
||||
let result = pullChara(
|
||||
{ count: 10, knife: 10 },
|
||||
{ times: 0, drawns: new Map() },
|
||||
)
|
||||
@@ -15,17 +15,17 @@ describe('Character', () => {
|
||||
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)
|
||||
result = pullChara({ 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)
|
||||
result = pullChara({ 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)
|
||||
result = pullChara({ count: 20, knife: 1 }, result.times)
|
||||
expect(result.characters).not.toContainValue(null)
|
||||
expect(result.characters.length).toBe(20)
|
||||
expect(result.times.times).toBe(36)
|
||||
@@ -56,6 +56,5 @@ describe('Character', () => {
|
||||
expect(unique.property.STR).toBeLessThanOrEqual(10)
|
||||
expect(unique.property.MNY).toBeLessThanOrEqual(10)
|
||||
expect(unique.talent.length).toBeLessThanOrEqual(5)
|
||||
console.debug(unique)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { type Character, type Talent, characters, talents } from '@remake/data'
|
||||
import type { Character, CharacterProperty, Talent } from '@remake/data'
|
||||
import { characters, talents } from '@remake/data'
|
||||
import { type RNG, type WeightItem, pick, pickWeight } from '@remake/vitex'
|
||||
import type { Allocation } from './state'
|
||||
|
||||
export type UniqueChara = Omit<Character, 'id' | 'name'>
|
||||
export type BaseChara = Omit<Character, 'id' | 'name'>
|
||||
export type BaseAlloc = Omit<Allocation, 'spirit'>
|
||||
export interface UniqueGenCfg {
|
||||
prop: WeightItem<number>[]
|
||||
talent: WeightItem<number>[]
|
||||
@@ -22,7 +25,7 @@ export function uniqueGenerate(config: UniqueGenCfg, rng?: RNG) {
|
||||
picked.add(t)
|
||||
count--
|
||||
}
|
||||
return { property, talent: Array.from(picked) } satisfies UniqueChara
|
||||
return { property, talent: Array.from(picked) } satisfies BaseChara
|
||||
}
|
||||
|
||||
export interface PullCharaOpt {
|
||||
@@ -37,9 +40,9 @@ export interface PullCharaRet {
|
||||
characters: Character['id'][]
|
||||
times: PullCharaTms
|
||||
}
|
||||
export function pullCharacter(
|
||||
export function pullChara(
|
||||
opt: PullCharaOpt,
|
||||
tms: PullCharaTms,
|
||||
tms: PullCharaTms = { times: 0, drawns: new Map() },
|
||||
rng?: RNG,
|
||||
): PullCharaRet {
|
||||
const { count, knife } = opt
|
||||
@@ -69,3 +72,34 @@ function deriveWeightMap(
|
||||
Array.from(characters.keys(), id => [id, base - (drawns.get(id) ?? 0)]),
|
||||
)
|
||||
}
|
||||
|
||||
export function charaPropToBaseAlloc(props: CharacterProperty): BaseAlloc {
|
||||
return {
|
||||
charm: props.CHR,
|
||||
intelligence: props.INT,
|
||||
strength: props.STR,
|
||||
money: props.MNY,
|
||||
}
|
||||
}
|
||||
|
||||
function charaPropToAlloc(
|
||||
props: CharacterProperty,
|
||||
spirit: number,
|
||||
): Allocation {
|
||||
return { ...charaPropToBaseAlloc(props), spirit }
|
||||
}
|
||||
|
||||
function convert(chara: BaseChara, spirit: number) {
|
||||
return {
|
||||
allocation: charaPropToAlloc(chara.property, spirit),
|
||||
talents: chara.talent,
|
||||
}
|
||||
}
|
||||
|
||||
export function startChara(id: Character['id'], spirit: number) {
|
||||
return convert(characters.get(id)!, spirit)
|
||||
}
|
||||
|
||||
export function startUnique(chara: BaseChara, spirit: number) {
|
||||
return convert(chara, spirit)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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 { BaseChara, UniqueGenCfg } from './character'
|
||||
export type { PickResult, StartResult } from './game'
|
||||
export type { NextResult, SummaryResult, EndResult } from './game'
|
||||
export { pull, exclude } from './talent'
|
||||
export { uniqueGenerate, pullChara, charaPropToBaseAlloc } from './character'
|
||||
export { startUnique, startChara } from './character'
|
||||
export { pick, start, next, summary, end } from './game'
|
||||
|
||||
@@ -47,7 +47,7 @@ export const useAllocator = () => {
|
||||
return { ...prev, [key]: final }
|
||||
})
|
||||
},
|
||||
[allocate, total],
|
||||
[allocate, total, setAlloc],
|
||||
)
|
||||
return [alloc, allocator] as const
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export const usePointRandomizer = () => {
|
||||
}
|
||||
setAlloc(alloc)
|
||||
},
|
||||
[allocate, total],
|
||||
[allocate, total, setAlloc],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useConfig, useProfile, useSetStep, useSetGameState, Step } from '.'
|
||||
import { uniqueGenerate, startUnique, startChara } from '@remake/core'
|
||||
import { pullChara, start, pick } from '@remake/core'
|
||||
import type { BaseChara, PullCharaTms } from '@remake/core'
|
||||
import type { Character } from '@remake/data'
|
||||
import type { RNG } from '@remake/vitex'
|
||||
|
||||
export const uniqueAtom = atom<BaseChara | null>(null)
|
||||
export const timesAtom = atom<PullCharaTms | undefined>(undefined)
|
||||
|
||||
export const useUnique = () => useAtomValue(uniqueAtom)
|
||||
export const useUniqueInject = () => {
|
||||
const setUnique = useSetAtom(uniqueAtom)
|
||||
return useCallback((unique: BaseChara) => setUnique(unique), [setUnique])
|
||||
}
|
||||
|
||||
export const useUniqueGenerator = () => {
|
||||
const { config } = useConfig().unique
|
||||
const [unique, setUnique] = useAtom(uniqueAtom)
|
||||
const generator = useCallback(
|
||||
(rng?: RNG) => {
|
||||
if (unique) return unique
|
||||
const generated = uniqueGenerate(config, rng)
|
||||
setUnique(generated)
|
||||
return generated
|
||||
},
|
||||
[config, unique, setUnique],
|
||||
)
|
||||
return [unique, generator] as const
|
||||
}
|
||||
|
||||
export interface PullCharaResult {
|
||||
characters: Character['id'][]
|
||||
unique: boolean
|
||||
}
|
||||
export const useCharaPuller = (rng?: RNG) => {
|
||||
const { unique: u, chara } = useConfig()
|
||||
const [unique] = useAtom(uniqueAtom)
|
||||
const [times, setTimes] = useAtom(timesAtom)
|
||||
const [pulled, setPulled] = useState<PullCharaResult>({
|
||||
characters: pullChara(chara, times, rng).characters,
|
||||
unique: !!unique,
|
||||
})
|
||||
const puller = useCallback(() => {
|
||||
const result = pullChara(chara, times, rng)
|
||||
setTimes(result.times)
|
||||
setPulled({
|
||||
characters: result.characters,
|
||||
unique: !!unique || result.times?.times >= u.limit * chara.count,
|
||||
})
|
||||
}, [u, chara, unique, times, setTimes, rng])
|
||||
return [pulled, puller] as const
|
||||
}
|
||||
|
||||
export type PickUnique = { type: 'unique'; id?: never }
|
||||
export type PickCharacter = {
|
||||
type: 'character'
|
||||
id: Character['id']
|
||||
}
|
||||
export type CharaPick = PickUnique | PickCharacter
|
||||
export const useCharaPicker = () => {
|
||||
const [picked, setPicked] = useState<CharaPick | null>(null)
|
||||
const chara = useCallback(
|
||||
(id: Character['id']) => setPicked({ type: 'character', id }),
|
||||
[setPicked],
|
||||
)
|
||||
const uni = useCallback(() => setPicked({ type: 'unique' }), [setPicked])
|
||||
return [picked, { chara, unique: uni }] as const
|
||||
}
|
||||
|
||||
export const useCharaStart = () => {
|
||||
const { spirit } = useConfig()
|
||||
const [profile] = useProfile()
|
||||
const unique = useAtomValue(uniqueAtom)
|
||||
const setStep = useSetStep()
|
||||
const setState = useSetGameState()
|
||||
return useCallback(
|
||||
(picked: CharaPick, rng?: RNG) => {
|
||||
let result
|
||||
if (picked.type === 'unique') {
|
||||
if (!unique)
|
||||
throw new Error('Unique character not generated yet')
|
||||
result = startUnique(unique, spirit)
|
||||
} else {
|
||||
result = startChara(picked.id, spirit)
|
||||
}
|
||||
const { talents, additionalPoints } = pick(result.talents, rng)
|
||||
// TODO: additionalPoints
|
||||
const { state, achievements } = start(
|
||||
profile,
|
||||
result.allocation,
|
||||
talents.talents,
|
||||
)
|
||||
setState(state)
|
||||
setStep(Step.Play)
|
||||
return achievements
|
||||
},
|
||||
[spirit, profile, unique, setStep, setState],
|
||||
)
|
||||
}
|
||||
|
||||
export { charaPropToBaseAlloc as convertProps } from '@remake/core'
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react'
|
||||
import { atom, useSetAtom, useAtomValue } from 'jotai'
|
||||
import type { PullOptions, PullCharaOpt } from '@remake/core'
|
||||
import type { PullOptions, PullCharaOpt, UniqueGenCfg } from '@remake/core'
|
||||
export interface Config {
|
||||
/** 游戏锁定天赋数量 */
|
||||
lock: number
|
||||
@@ -20,6 +20,11 @@ export interface Config {
|
||||
pull: PullOptions
|
||||
/** 名人模式配置 */
|
||||
chara: PullCharaOpt
|
||||
/** 唯一角色限制 */
|
||||
unique: {
|
||||
limit: number
|
||||
config: UniqueGenCfg
|
||||
}
|
||||
}
|
||||
|
||||
export const configAtom = atom<Config | null>(null)
|
||||
|
||||
@@ -3,4 +3,5 @@ export * from './profile'
|
||||
export * from './talent'
|
||||
export * from './play'
|
||||
export * from './alloc'
|
||||
export * from './character'
|
||||
export type * from '@remake/core'
|
||||
|
||||
+13
-12
@@ -25,6 +25,7 @@ export type Log = Omit<NextResult, 'state'> & {
|
||||
props: Omit<Properties, 'age'>
|
||||
}
|
||||
|
||||
export const modeAtom = atom<Mode>(Mode.Classic)
|
||||
export const stepAtom = atom<Step>(Step.Idle)
|
||||
export const gameStateAtom = atom<GameState | null>(null)
|
||||
export const logsAtom = atom<Log[]>([])
|
||||
@@ -51,9 +52,10 @@ export const useGameReset = () => {
|
||||
}, [resetTalent, resetAlloc, resetState, resetLogs])
|
||||
}
|
||||
|
||||
export const useStep = () => [useAtomValue(stepAtom), Step] as const
|
||||
export const useSetStep = () => [useSetAtom(stepAtom), Step] as const
|
||||
export const useStep = () => useAtomValue(stepAtom)
|
||||
export const useSetStep = () => useSetAtom(stepAtom)
|
||||
export const useGameState = () => useAtomValue(gameStateAtom)
|
||||
export const useSetGameState = () => useSetAtom(gameStateAtom)
|
||||
export const useSummary = () => useAtomValue(summaryAtom)
|
||||
export const useLogs = () => useAtomValue(logsAtom)
|
||||
|
||||
@@ -69,16 +71,15 @@ export const useRemake = () => {
|
||||
}
|
||||
|
||||
export const useModeChoose = () => {
|
||||
const setMode = useSetAtom(modeAtom)
|
||||
const setStep = useSetAtom(stepAtom)
|
||||
const choose = useCallback(
|
||||
(mode: Mode) => {
|
||||
setMode(mode)
|
||||
/* prettier-ignore */
|
||||
switch (mode) {
|
||||
case Mode.Classic:
|
||||
setStep(Step.Pick)
|
||||
break
|
||||
case Mode.Celebrity:
|
||||
setStep(Step.Chara)
|
||||
break
|
||||
case Mode.Classic: return setStep(Step.Pick)
|
||||
case Mode.Celebrity: return setStep(Step.Chara)
|
||||
}
|
||||
},
|
||||
[setStep],
|
||||
@@ -122,7 +123,7 @@ export const useNext = () => {
|
||||
setEnded(true)
|
||||
}
|
||||
return result.achievements
|
||||
}, [state, profile, setState])
|
||||
}, [state, profile, setState, setLogs])
|
||||
return [{ logs, ended }, nexter] as const
|
||||
}
|
||||
|
||||
@@ -144,7 +145,7 @@ export const useGotoSummary = () => {
|
||||
export const useEnd = () => {
|
||||
const { lock } = useConfig()
|
||||
const [profile, setProfile] = useProfile()
|
||||
const [step, setStep] = useAtom(stepAtom)
|
||||
const setStep = useSetAtom(stepAtom)
|
||||
const state = useAtomValue(gameStateAtom)
|
||||
const reset = useGameReset()
|
||||
const [locked, setLocked] = useState<Set<Talent['id']>>(new Set())
|
||||
@@ -163,7 +164,7 @@ export const useEnd = () => {
|
||||
return next
|
||||
})
|
||||
},
|
||||
[setLocked],
|
||||
[lock, setLocked],
|
||||
)
|
||||
const ender = useCallback(() => {
|
||||
if (!state)
|
||||
@@ -174,6 +175,6 @@ export const useEnd = () => {
|
||||
setProfile(result.profile)
|
||||
reset()
|
||||
return result.achievements
|
||||
}, [state, step, profile, locked, setStep, setProfile, reset])
|
||||
}, [state, profile, locked, setStep, setProfile, reset])
|
||||
return [locked, picker, ender] as const
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useConfig, useProfile, useSetStep } from '.'
|
||||
import { useConfig, useProfile, useSetStep, Step } from '.'
|
||||
import { pull, exclude, pick, type PickResult } from '@remake/core'
|
||||
import type { Talent } from '@remake/data'
|
||||
import type { RNG } from '@remake/vitex'
|
||||
@@ -82,7 +82,7 @@ export const useTalentSubmit = () => {
|
||||
const picked = useAtomValue(pickedAtom)
|
||||
const setReplaced = useSetAtom(replacedAtom)
|
||||
const { enabled } = useSubmitIsEnable()
|
||||
const [setStep, Step] = useSetStep()
|
||||
const setStep = useSetStep()
|
||||
return useCallback(
|
||||
(rng?: RNG) => {
|
||||
if (!enabled) throw new Error('Not enough talents picked')
|
||||
|
||||
Reference in New Issue
Block a user