mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-28 01:06:49 +08:00
update: hooks, web
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { useCallback } from 'react'
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useConfig } from './config'
|
||||
import { useReplaced } from './talent'
|
||||
import type { Allocation } from '@remake/core'
|
||||
import type { RNG } from '@remake/vitex'
|
||||
import { keys, shuffle, random as frandom } from '@remake/vitex'
|
||||
|
||||
const init: Allocation = { charm: 0, intelligence: 0, strength: 0, money: 0 }
|
||||
const allocAtom = atom<Allocation>(init)
|
||||
|
||||
const alloced = (alloc: Allocation) =>
|
||||
Object.values(alloc).reduce((a, b) => a + b, 0)
|
||||
|
||||
export const usePoints = () => {
|
||||
const { points } = useConfig()
|
||||
const { additionalPoints } = useReplaced()
|
||||
return Math.max(points + additionalPoints.points, 0)
|
||||
}
|
||||
|
||||
export const useLeftPoints = () => {
|
||||
const points = usePoints()
|
||||
const alloc = useAtomValue(allocAtom)
|
||||
return points - alloced(alloc)
|
||||
}
|
||||
|
||||
export const useAllocator = () => {
|
||||
const { allocate } = useConfig()
|
||||
const total = usePoints()
|
||||
const [alloc, setAlloc] = useAtom(allocAtom)
|
||||
const allocator = useCallback(
|
||||
(key: keyof Allocation, value: number) => {
|
||||
setAlloc(prev => {
|
||||
const left = total - alloced(prev) + prev[key]
|
||||
const max = Math.min(left, allocate)
|
||||
const final = Math.max(Math.min(max, value), 0)
|
||||
if (prev[key] === final) return prev
|
||||
return { ...prev, [key]: final }
|
||||
})
|
||||
},
|
||||
[allocate, total],
|
||||
)
|
||||
return [alloc, allocator] as const
|
||||
}
|
||||
|
||||
export const usePointRandomizer = () => {
|
||||
const { allocate } = useConfig()
|
||||
const total = usePoints()
|
||||
const setAlloc = useSetAtom(allocAtom)
|
||||
return useCallback(
|
||||
(rng?: RNG) => {
|
||||
const suffled = shuffle(keys(init), rng)
|
||||
let left = total
|
||||
const alloc = { ...init }
|
||||
while (suffled.length) {
|
||||
const key = suffled.pop()!
|
||||
const max = Math.min(allocate, left)
|
||||
const min = Math.max(0, left - suffled.length * allocate)
|
||||
const value = frandom(max, min, rng)
|
||||
alloc[key] = value
|
||||
left -= value
|
||||
}
|
||||
setAlloc(alloc)
|
||||
},
|
||||
[allocate, total],
|
||||
)
|
||||
}
|
||||
|
||||
export const useAllocSubmit = () => {}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useCallback } from 'react'
|
||||
import { atom, useSetAtom, useAtomValue } from 'jotai'
|
||||
import type { PullOptions } from '@remake/core'
|
||||
|
||||
export interface Config {
|
||||
pick: number
|
||||
pull: PullOptions
|
||||
points: number
|
||||
allocate: number
|
||||
mode: number
|
||||
}
|
||||
|
||||
export const configAtom = atom<Config | null>(null)
|
||||
|
||||
export const useConfigInject = () => {
|
||||
const setConfig = useSetAtom(configAtom)
|
||||
return useCallback((config: Config) => setConfig(config), [setConfig])
|
||||
}
|
||||
|
||||
export const useConfig = () => {
|
||||
const config = useAtomValue(configAtom)
|
||||
if (!config) throw new Error('Game config is not set')
|
||||
return config
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './config'
|
||||
export * from './profile'
|
||||
export * from './talent'
|
||||
export * from './play'
|
||||
export * from './alloc'
|
||||
export type * from '@remake/core'
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useTransition, useCallback } from 'react'
|
||||
import { atom, useSetAtom, useAtomValue, useAtom } from 'jotai'
|
||||
import { useConfig } from './config'
|
||||
import { useProfile } from './profile'
|
||||
import { end } from '@remake/core'
|
||||
import type { GameState } from '@remake/core'
|
||||
import type { Talent } from '@remake/data/talent'
|
||||
|
||||
export enum Step {
|
||||
Idle = 'idle',
|
||||
Mode = 'mode',
|
||||
Pick = 'pick',
|
||||
Alloc = 'alloc',
|
||||
Play = 'play',
|
||||
Summary = 'summary',
|
||||
}
|
||||
|
||||
const stepAtom = atom<Step>(Step.Idle)
|
||||
const gameStateAtom = atom<GameState | null>(null)
|
||||
|
||||
export const useStep = () => useAtomValue(stepAtom)
|
||||
export const useSetStep = () => useSetAtom(stepAtom)
|
||||
|
||||
export const useRemake = () => {
|
||||
const config = useConfig()
|
||||
const [profile] = useProfile()
|
||||
const setStep = useSetAtom(stepAtom)
|
||||
return useCallback(() => {
|
||||
const step = profile.times < config.mode ? Step.Pick : Step.Mode
|
||||
setStep(step)
|
||||
}, [config, profile, setStep])
|
||||
}
|
||||
|
||||
export const useEnd = () => {
|
||||
const [profile, setProfile] = useProfile()
|
||||
const [step, setStep] = useAtom(stepAtom)
|
||||
const [state, setState] = useAtom(gameStateAtom)
|
||||
const [pending, startTransition] = useTransition()
|
||||
const ender = useCallback(
|
||||
(talent?: Talent['id']) => {
|
||||
startTransition(async () => {
|
||||
if (!state || step === Step.Summary)
|
||||
throw new Error(
|
||||
'Game state is not available or already ended.',
|
||||
)
|
||||
const { profile: next, achievements } = end(
|
||||
state,
|
||||
profile,
|
||||
talent,
|
||||
)
|
||||
setStep(Step.Idle)
|
||||
setState(null)
|
||||
setProfile(next)
|
||||
})
|
||||
},
|
||||
[state, step, profile, setStep, setState, setProfile],
|
||||
)
|
||||
return [pending, ender] as const
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { screen, renderHook } from '@testing-library/react'
|
||||
import { useProfile, useProfileInject } from './profile'
|
||||
|
||||
describe('profile', () => {
|
||||
test('inject', () => {
|
||||
expect(() => renderHook(() => useProfile())).toThrow()
|
||||
// const inject = renderHook(() => useProfileInject())
|
||||
})
|
||||
test('no inject throw', () => {
|
||||
expect(() => renderHook(() => useProfile())).toThrow()
|
||||
// const inject = renderHook(() => useProfileInject())
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useCallback } from 'react'
|
||||
import { atom, useAtom, useSetAtom } from 'jotai'
|
||||
import type { ProfileState } from '@remake/core'
|
||||
|
||||
const profileAtom = atom<ProfileState | null>(null)
|
||||
|
||||
export const useProfile = () => {
|
||||
const [profile, setProfile] = useAtom(profileAtom)
|
||||
if (!profile) throw new Error('Profile is not loaded')
|
||||
return [profile, setProfile] as const
|
||||
}
|
||||
|
||||
export const useProfileInject = () => {
|
||||
const setProfile = useSetAtom(profileAtom)
|
||||
return useCallback(
|
||||
(profile: ProfileState) => setProfile(profile),
|
||||
[setProfile],
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useConfig } from './config'
|
||||
import { useProfile } from './profile'
|
||||
import { useSetStep, Step } from './play'
|
||||
import { pull, exclude, talentsPicked } from '@remake/core'
|
||||
import type { TalentsPickedResult, RNG } from '@remake/core'
|
||||
import type { Talent } from '@remake/data/talent'
|
||||
|
||||
const pickedAtom = atom(new Set<Talent['id']>())
|
||||
const replacedAtom = atom<TalentsPickedResult | null>(null)
|
||||
|
||||
export const usePicked = () => {
|
||||
const picked = useAtomValue(pickedAtom)
|
||||
if (!picked) throw new Error('No talents picked')
|
||||
return picked
|
||||
}
|
||||
export const useReplaced = () => {
|
||||
const replaced = useAtomValue(replacedAtom)
|
||||
if (!replaced) throw new Error('No talents replaced')
|
||||
return replaced
|
||||
}
|
||||
|
||||
export const useTalentPuller = () => {
|
||||
const { pull: p } = useConfig()
|
||||
const [profile] = useProfile()
|
||||
const [pulled, setPulled] = useState<Talent['id'][] | null>(null)
|
||||
const puller = useCallback(
|
||||
(rng?: RNG) => setPulled(pull(p, profile, rng)),
|
||||
// (rng?: RNG) =>
|
||||
// setPulled([
|
||||
// 1142, 1143, 1144, 1145, 1146, 1086, 1122, 1111, 1130, 1048,
|
||||
// ]),
|
||||
[p, profile, setPulled],
|
||||
)
|
||||
return [pulled, puller] as const
|
||||
}
|
||||
|
||||
export type TalentPickerResult =
|
||||
| {
|
||||
type: 'ok'
|
||||
talent?: never
|
||||
}
|
||||
| {
|
||||
type: 'not-enough'
|
||||
talent?: never
|
||||
}
|
||||
| {
|
||||
type: 'exclude'
|
||||
talent: Talent['id']
|
||||
}
|
||||
|
||||
export const useTalentPicker = () => {
|
||||
const { pick } = useConfig()
|
||||
const [picked, setPicked] = useAtom(pickedAtom)
|
||||
const picker = useCallback(
|
||||
(talent: Talent['id']): TalentPickerResult => {
|
||||
if (!picked) {
|
||||
setPicked(new Set([talent]))
|
||||
return { type: 'ok' }
|
||||
}
|
||||
if (picked.has(talent)) {
|
||||
const next = new Set(picked)
|
||||
next.delete(talent)
|
||||
setPicked(next)
|
||||
return { type: 'ok' }
|
||||
}
|
||||
if (picked.size >= pick) return { type: 'not-enough' }
|
||||
const e = exclude(talent, picked)
|
||||
if (e) return { type: 'exclude', talent: e }
|
||||
const next = new Set([...picked, talent])
|
||||
setPicked(next)
|
||||
return { type: 'ok' }
|
||||
},
|
||||
[pick, picked, setPicked],
|
||||
)
|
||||
return [picked, picker] as const
|
||||
}
|
||||
|
||||
export const useSubmitIsEnable = () => {
|
||||
const { pick } = useConfig()
|
||||
const picked = useAtomValue(pickedAtom)
|
||||
const enabled = picked.size >= pick
|
||||
return [enabled, pick] as const
|
||||
}
|
||||
|
||||
export const useTalentSubmit = () => {
|
||||
const picked = useAtomValue(pickedAtom)
|
||||
const setReplaced = useSetAtom(replacedAtom)
|
||||
const [enabled] = useSubmitIsEnable()
|
||||
const next = useSetStep()
|
||||
return useCallback(
|
||||
(rng?: RNG) => {
|
||||
if (!enabled) throw new Error('Not enough talents picked')
|
||||
setReplaced(talentsPicked(picked, rng))
|
||||
next(Step.Alloc)
|
||||
},
|
||||
[enabled, picked, setReplaced, next],
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user