update: game core flow pages

This commit is contained in:
Vick Scarlet
2026-08-12 02:38:37 +08:00
parent 5d50a4ea37
commit cd339c2103
34 changed files with 890 additions and 768 deletions
+3 -2
View File
@@ -1,6 +1,7 @@
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 { createState, nextProfile, propsEffect } from './state'
import { summary as stateSummary } from './state'
import type { PullOptions, ReplacementResult } from './talent'
@@ -109,6 +110,6 @@ export function end(
}
export { pull, exclude }
export type { GameState, ProfileState, Allocation, RNG }
export type { PullOptions, ReplacementResult }
export type { GameState, ProfileState, Properties, RNG }
export type { PullOptions, ReplacementResult, Allocation }
export type { AdditionalPoint, AdditionalPoints }
+5 -1
View File
@@ -1,4 +1,4 @@
import type { Achievement, Event, Talent } from '@remake/data'
import type { Achievement, Event, Talent, Character } from '@remake/data'
import { produce } from 'immer'
import { sum, keys } from '@remake/vitex'
@@ -37,6 +37,9 @@ export interface GameState {
talentTriggers: Map<Talent['id'], number> // 本局天赋触发次数
}
/** 唯一角色 */
export type UniqueCharacter = Omit<Character, 'id' | 'name'>
/** 持久化存储的数据 */
export interface ProfileState {
times: number // 游戏次数
@@ -46,6 +49,7 @@ export interface ProfileState {
achievements: Set<Achievement['id']> // 达成的成就
highest?: Properties // 历史最高属性
lowest?: Properties // 历史最低属性
unique?: UniqueCharacter // 唯一角色属性
}
export function createState(
+1
View File
@@ -7,6 +7,7 @@ export interface Config {
pull: PullOptions
points: number
allocate: number
spirit: number
mode: number
}
+106 -26
View File
@@ -1,9 +1,11 @@
import { useTransition, useCallback } from 'react'
import { useCallback, useRef, useState } 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 { useReplaced, useTalentReset } from './talent'
import { useAlloc, useAllocReset } from './alloc'
import { start, next, summary, end } from '@remake/core'
import type { GameState, Properties, NextResult } from '@remake/core'
import type { Talent } from '@remake/data/talent'
export enum Step {
@@ -15,45 +17,123 @@ export enum Step {
Summary = 'summary',
}
export type Log = Omit<NextResult, 'state'> & {
props: Omit<Properties, 'age'>
}
const stepAtom = atom<Step>(Step.Idle)
const gameStateAtom = atom<GameState | null>(null)
const logsAtom = atom<Log[]>([])
const summaryAtom = atom(0)
export const useStateReset = () => {
const setGameState = useSetAtom(gameStateAtom)
return useCallback(() => setGameState(null), [setGameState])
}
export const useLogsReset = () => {
const setLogs = useSetAtom(logsAtom)
return useCallback(() => setLogs([]), [setLogs])
}
export const useGameReset = () => {
const resetTalent = useTalentReset()
const resetAlloc = useAllocReset()
const resetState = useStateReset()
const resetLogs = useLogsReset()
return useCallback(() => {
resetTalent()
resetAlloc()
resetState()
resetLogs()
}, [resetTalent, resetAlloc, resetState, resetLogs])
}
export const useStep = () => useAtomValue(stepAtom)
export const useSetStep = () => useSetAtom(stepAtom)
export const useGameState = () => useAtomValue(gameStateAtom)
export const useSummary = () => useAtomValue(summaryAtom)
export const useLogs = () => useAtomValue(logsAtom)
export const useRemake = () => {
const config = useConfig()
const { mode } = useConfig()
const [{ times }] = useProfile()
const setStep = useSetAtom(stepAtom)
const reset = useGameReset()
return useCallback(() => {
reset()
setStep(times < mode ? Step.Pick : Step.Mode)
}, [mode, times, setStep, reset])
}
export const useStart = () => {
const { spirit } = useConfig()
const [profile] = useProfile()
const setStep = useSetAtom(stepAtom)
const setState = useSetAtom(gameStateAtom)
const allocate = useAlloc()
const { talents: tr } = useReplaced()
return useCallback(() => {
const step = profile.times < config.mode ? Step.Pick : Step.Mode
setStep(step)
}, [config, profile, setStep])
const alloc = { ...allocate, spirit }
const result = start(profile, alloc, tr.talents)
setState(result.state)
setStep(Step.Play)
return result.achievements
}, [profile, allocate, spirit, tr, setStep, setState])
}
export const useNext = () => {
const [profile] = useProfile()
const [state, setState] = useAtom(gameStateAtom)
const [logs, setLogs] = useAtom(logsAtom)
const endRef = useRef(false)
const [ended, setEnded] = useState(false)
if (!state) throw new Error('Game state is not available.')
const nexter = useCallback(() => {
if (!state) throw new Error('Game state is not available.')
if (endRef.current) throw new Error('Game state is already ended.')
const { state: s, ...result } = next(state, profile)
setState(s)
const { age, ...props } = s.props.current
const log = { ...result, props }
setLogs(prev => [...prev, log])
if (s.life <= 0) {
endRef.current = true
setEnded(true)
}
return result.achievements
}, [state, profile, setState])
return [{ state, logs, ended }, nexter] as const
}
export const useGotoSummary = () => {
const [profile] = useProfile()
const [state, setState] = useAtom(gameStateAtom)
const setStep = useSetAtom(stepAtom)
const setSummary = useSetAtom(summaryAtom)
return useCallback(() => {
if (!state) throw new Error('Game state is not available.')
const result = summary(state, profile)
setSummary(result.summary)
setState(result.state)
setStep(Step.Summary)
return result.achievements
}, [state, profile, setStep, setState, setSummary])
}
export const useEnd = () => {
const [profile, setProfile] = useProfile()
const [step, setStep] = useAtom(stepAtom)
const [state, setState] = useAtom(gameStateAtom)
const [pending, startTransition] = useTransition()
const ender = useCallback(
const state = useAtomValue(gameStateAtom)
const reset = useGameReset()
return 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)
})
if (!state)
throw new Error('Game state is not available or already ended.')
const result = end(state, profile, talent)
setStep(Step.Idle)
setProfile(result.profile)
reset()
return result.achievements
},
[state, step, profile, setStep, setState, setProfile],
[state, step, profile, setStep, setProfile, reset],
)
return [pending, ender] as const
}
+2
View File
@@ -4,6 +4,8 @@ import type { ProfileState } from '@remake/core'
const profileAtom = atom<ProfileState | null>(null)
export const useRawProfile = () => useAtom(profileAtom)
export const useProfile = () => {
const [profile, setProfile] = useAtom(profileAtom)
if (!profile) throw new Error('Profile is not loaded')
+15 -5
View File
@@ -10,6 +10,15 @@ import type { Talent } from '@remake/data/talent'
const pickedAtom = atom(new Set<Talent['id']>())
const replacedAtom = atom<TalentsPickedResult | null>(null)
export const useTalentReset = () => {
const setPicked = useSetAtom(pickedAtom)
const setReplaced = useSetAtom(replacedAtom)
return useCallback(() => {
setPicked(new Set())
setReplaced(null)
}, [setPicked, setReplaced])
}
export const usePicked = () => {
const picked = useAtomValue(pickedAtom)
if (!picked) throw new Error('No talents picked')
@@ -26,11 +35,12 @@ export const useTalentPuller = () => {
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,
// ]),
// (rng?: RNG) => setPulled(pull(p, profile, rng)),
(rng?: RNG) =>
setPulled([
1142, 1143, 1144, 1145, 1146, 1086, 1122, 1111, 1130, 1048,
1033,
]),
[p, profile, setPulled],
)
return [pulled, puller] as const
+8 -2
View File
@@ -34,6 +34,12 @@ export function shuffle<T>(array: T[], rng?: RNG): T[] {
return result
}
export function keys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[]
export function keys<T extends object, K extends keyof T = never>(
obj: T,
filter?: K[],
): Exclude<keyof T, K>[] {
const allKeys = Object.keys(obj) as (keyof T)[]
if (!filter || filter.length === 0) return allKeys as Exclude<keyof T, K>[]
const filterSet = new Set<keyof T>(filter)
return allKeys.filter(key => !filterSet.has(key)) as Exclude<keyof T, K>[]
}