From f4204601eec2344ac1ffd4e53ccc48218bcbcb73 Mon Sep 17 00:00:00 2001 From: Vick Scarlet Date: Fri, 14 Aug 2026 21:00:59 +0800 Subject: [PATCH] update: character --- apps/web/src/Game.css | 20 +++++ apps/web/src/Game.tsx | 59 +++++++------- apps/web/src/components/Talent.tsx | 2 +- apps/web/src/config.ts | 22 +++++ apps/web/src/containers/Alloc.css | 8 -- apps/web/src/containers/Alloc.tsx | 2 +- apps/web/src/containers/Chara.css | 91 ++++++++++++++++++++- apps/web/src/containers/Chara.tsx | 121 +++++++++++++++++++++++++++- apps/web/src/containers/Loading.tsx | 9 --- apps/web/src/containers/Play.tsx | 12 +-- apps/web/src/containers/Summary.tsx | 2 +- apps/web/src/hooks/judge.ts | 10 ++- apps/web/src/hooks/storage.ts | 33 ++++++-- packages/core/src/character.spec.ts | 11 ++- packages/core/src/character.ts | 44 ++++++++-- packages/core/src/index.ts | 5 +- packages/hooks/src/alloc.ts | 4 +- packages/hooks/src/character.ts | 104 ++++++++++++++++++++++++ packages/hooks/src/config.ts | 7 +- packages/hooks/src/index.ts | 1 + packages/hooks/src/play.ts | 25 +++--- packages/hooks/src/talent.ts | 4 +- 22 files changed, 495 insertions(+), 101 deletions(-) delete mode 100644 apps/web/src/containers/Loading.tsx create mode 100644 packages/hooks/src/character.ts diff --git a/apps/web/src/Game.css b/apps/web/src/Game.css index 55d1e92..549d215 100644 --- a/apps/web/src/Game.css +++ b/apps/web/src/Game.css @@ -32,4 +32,24 @@ user-select: none; } } + > .controls { + display: flex; + flex-direction: row; + justify-content: center; + gap: 0.5rem; + width: 100%; + > button { flex: 1 0; } + } +} +.saving { + position: fixed; + bottom: 0; + left: 0; + opacity: 0; + user-select: none; + transition: opacity 0.3s; + &.active { + opacity: 1; + transition: opacity 0.1s; + } } \ No newline at end of file diff --git a/apps/web/src/Game.tsx b/apps/web/src/Game.tsx index c1922aa..39b8c76 100644 --- a/apps/web/src/Game.tsx +++ b/apps/web/src/Game.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { useInit, useSaver } from '@/hooks/storage' -import { useStep } from '@remake/hooks' +import { useInit, useWatcher } from '@/hooks/storage' +import { useStep, Step } from '@remake/hooks' import Home from '@/containers/Home' import Mode from '@/containers/Mode' import Chara from '@/containers/Chara' @@ -8,37 +8,34 @@ import Pick from '@/containers/TalentPick' import Alloc from '@/containers/Alloc' import Play from '@/containers/Play' import Summary from '@/containers/Summary' -import { Loading } from './containers/Loading' import './Game.css' -export function Game() { - const [inited, init] = useInit() - const saver = useSaver() - const [step, Step] = useStep() - useEffect(() => { - if (!inited) init() - }, [inited]) - useEffect(() => { - saver() - }, [saver]) - if (!inited) return - switch (step) { - case Step.Idle: - return - case Step.Mode: - return - case Step.Chara: - return - case Step.Pick: - return - case Step.Alloc: - return - case Step.Play: - return - case Step.Summary: - return - default: - return null +export function Container() { + /* prettier-ignore */ + switch (useStep()) { + case Step.Idle: return + case Step.Mode: return + case Step.Chara: return + case Step.Pick: return + case Step.Alloc: return + case Step.Play: return + case Step.Summary: return + default: return null } } + +/* prettier-ignore */ +const Loading = () =>

载入中...

+const Saving = ({ active }: { active: boolean }) => ( +
保存中...
+) + +/* prettier-ignore */ +export function Game() { + const [inited, init] = useInit() + const saving = useWatcher() + useEffect(() => { if (!inited) init() }, [inited, init]) + if (!inited) return + return <> +} export default Game diff --git a/apps/web/src/components/Talent.tsx b/apps/web/src/components/Talent.tsx index 20daa65..95ef8e3 100644 --- a/apps/web/src/components/Talent.tsx +++ b/apps/web/src/components/Talent.tsx @@ -3,7 +3,7 @@ import './Talent.css' export interface TalentProps { id: Talent['id'] - selected: boolean + selected?: boolean } export function TalentComponent({ id, selected }: TalentProps) { const talent = talents.get(id) diff --git a/apps/web/src/config.ts b/apps/web/src/config.ts index c956fef..8be5239 100644 --- a/apps/web/src/config.ts +++ b/apps/web/src/config.ts @@ -31,6 +31,8 @@ export const PullCount = dev.pull ?? 10 export const CharacterPullCount = dev.chara ?? 3 // 名人模式抽取权重切线 export const CharacterWeightKnife = dev.knife ?? 10 +// 唯一角色生成抽卡次数限制 +export const UniqueLimit = dev.unique ?? 10 // 天赋抽取基础概率 export const PullRateBase: Config['pull']['rate']['base'] = new Map([ @@ -103,6 +105,19 @@ export const PullRateAdditions: Config['pull']['rate']['additions'] = { }, } +// 权重生成器 +function wg(s: number, e: number) { + const length = e - s + 1 + return Array.from( + { length }, + (_, i) => [s + i, Math.min(i + 1, length - i)] as const, + ) +} +// 唯一角色属性分配权重 +export const UniquePropWeight = wg(0, 10) +// 唯一角色天赋个数权重 +export const UniqueTalentWeight = wg(1, 5) + export const config: Config = { lock: LockLimit, mode: ModeLimit, @@ -122,6 +137,13 @@ export const config: Config = { count: CharacterPullCount, knife: CharacterWeightKnife, }, + unique: { + limit: UniqueLimit, + config: { + prop: UniquePropWeight, + talent: UniqueTalentWeight, + }, + }, } export default config diff --git a/apps/web/src/containers/Alloc.css b/apps/web/src/containers/Alloc.css index a3f59e5..65605bf 100644 --- a/apps/web/src/containers/Alloc.css +++ b/apps/web/src/containers/Alloc.css @@ -109,14 +109,6 @@ } } } - > div { - display: flex; - flex-direction: row; - justify-content: center; - width: 100%; - gap: 0.5rem; - button { flex: 1 0; } - } > ul.talent-list { display: flex; flex-direction: column; diff --git a/apps/web/src/containers/Alloc.tsx b/apps/web/src/containers/Alloc.tsx index a20c06a..0b06b4e 100644 --- a/apps/web/src/containers/Alloc.tsx +++ b/apps/web/src/containers/Alloc.tsx @@ -103,7 +103,7 @@ export function Alloc() { ))} -
+
diff --git a/apps/web/src/containers/Chara.css b/apps/web/src/containers/Chara.css index d3ed08d..1ea8928 100644 --- a/apps/web/src/containers/Chara.css +++ b/apps/web/src/containers/Chara.css @@ -1 +1,90 @@ -.screen.chara {} \ No newline at end of file +.screen.chara { + gap: 1rem; + > .chara-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + > .character { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + border: 0.0625rem solid var(--base-text-color); + box-sizing: border-box; + cursor: pointer; + > .generator > ul > li { text-align: center; } + > .name { + width: 100%; + display: flex; + justify-content: center; + align-items: center; + height: 2rem; + } + &.selected { + border: 0.0625rem solid var(--color-primary); + > .name { + background-color: var(--color-primary); + color: var(--base-background-color); + } + > .chara-details { + height: auto; + padding: 0.5rem; + } + } + } + } + .chara-details { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; + border-top: 0.0625rem solid var(--base-text-color); + box-sizing: border-box; + height: 0; + padding: 0 0.5rem; + overflow-y: hidden; + transition: all 0.3s ease-in-out; + interpolate-size: allow-keywords; + > ul.talent-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + > ul.properties { + display: flex; + flex-direction: row; + gap: 0.5rem; + width: 100%; + font-weight: bold; + > li { + display: flex; + flex-direction: column; + flex: 1 0; + justify-content: space-between; + text-align: center; + min-width: 6rem; + user-select: none; + color: var(--grade-color); + span { + display: flex; + justify-content: center; + align-items: center; + border: 0.0625rem solid var(--grade-color); + box-sizing: border-box; + height: 2rem; + &:first-child { + border-bottom: none; + } + } + .name { + color: var(--base-background-color); + background-color: var(--grade-color); + } + &.charm { order: 1 } + &.intelligence { order: 2 } + &.strength { order: 3 } + &.money { order: 4 } + } + } + } +} \ No newline at end of file diff --git a/apps/web/src/containers/Chara.tsx b/apps/web/src/containers/Chara.tsx index a702bb3..6dce993 100644 --- a/apps/web/src/containers/Chara.tsx +++ b/apps/web/src/containers/Chara.tsx @@ -1,7 +1,126 @@ +import { useCharaPuller, useCharaPicker, convertProps } from '@remake/hooks' +import { useUnique, useUniqueGenerator, useCharaStart } from '@remake/hooks' +import type { BaseChara } from '@remake/hooks' +import { TalentComponent } from '@/components/Talent' +import { judgeGradeByValue } from '@/config' +import { characters } from '@remake/data' +import { properties } from '@/display' +import { keys } from '@remake/vitex' import './Chara.css' +function Details({ detail }: { detail: BaseChara }) { + const props = convertProps(detail.property) + return ( +
+
    + {keys(props).map(key => ( +
  • + {properties[key]} + {props[key]} +
  • + ))} +
+
    + {detail.talent.map(id => ( +
  • + +
  • + ))} +
+
+ ) +} + +interface UniqueProps { + selected?: boolean + picker?: () => void +} +function Unique({ selected, picker }: UniqueProps) { + const [unique, generator] = useUniqueGenerator() + return ( +
  • + 独一无二的我 + {unique &&
    } + {!unique && ( +
    +
      +
    • 6000万玩家中独一无二的角色卡
    • +
    • 所有属性 所有天赋 随机生成
    • +
    • 每人只能生成一次
    • +
    + +
    + )} +
  • + ) +} + +interface CharacterProps { + id: number + selected?: boolean + picker?: (id: number) => void +} +function Character({ id, selected, picker }: CharacterProps) { + const character = characters.get(id)! + return ( +
  • picker?.(id)} + > + {character.name} +
    +
  • + ) +} + export function Chara() { - return
    + const u = useUnique() + const [{ unique, characters }, puller] = useCharaPuller() + const [picked, picker] = useCharaPicker() + const start = useCharaStart() + const ready = picked && (picked.type === 'unique' ? !!u : true) + return ( +
    +
      + {unique && ( + picker.unique()} + /> + )} + {characters.map(id => ( + picker.chara(id)} + /> + ))} +
    +
    + + +
    +
    + ) } export default Chara diff --git a/apps/web/src/containers/Loading.tsx b/apps/web/src/containers/Loading.tsx deleted file mode 100644 index 202a39f..0000000 --- a/apps/web/src/containers/Loading.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export function Loading() { - return ( -
    -

    载入中...

    -
    - ) -} - -export default Loading diff --git a/apps/web/src/containers/Play.tsx b/apps/web/src/containers/Play.tsx index a945f2f..2b4d3bd 100644 --- a/apps/web/src/containers/Play.tsx +++ b/apps/web/src/containers/Play.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react' +import { useState, useRef, useCallback } from 'react' import { useLayoutEffect, useEffect } from 'react' import { useNext, useGotoSummary, type Log } from '@remake/hooks' import { useJudge } from '@/hooks/judge' @@ -100,12 +100,12 @@ function Prop({ prop, value, grade }: PropProps) { useEffect(() => { const prev = prevRef.current if (value === prev) return + const startValue = prevRef.current prevRef.current = value setTrend(value > prev ? 'up' : 'down') setFlip(f => (f + 1) % 2) let startTimestamp: number | null = null const duration = 400 - const startValue = displayValue let end = false const step = (timestamp: number) => { if (!startTimestamp) startTimestamp = timestamp @@ -150,18 +150,18 @@ export function Play() { const logRef = useRef(null) const autoRef = useRef(0) const gotoSummary = useGotoSummary() - const handleNext = () => { + const handleNext = useCallback(() => { if (ended) return const achievements = next() // TODO: Show achievements console.debug('Achievements:', achievements) - } - const handleGotoSummary = () => { + }, [ended, next]) + const handleGotoSummary = useCallback(() => { if (!ended) return const achievements = gotoSummary() // TODO: Show achievements console.debug('Achievements:', achievements) - } + }, [ended, gotoSummary]) useLayoutEffect(() => { requestAnimationFrame(() => { if (!logRef.current) return diff --git a/apps/web/src/containers/Summary.tsx b/apps/web/src/containers/Summary.tsx index 6610895..b84ab94 100644 --- a/apps/web/src/containers/Summary.tsx +++ b/apps/web/src/containers/Summary.tsx @@ -38,7 +38,7 @@ function TalentList({ picked, picker }: TalentListProps) { } hasInitialized.current = true } - }, [picked, picker]) + }, [profile.locked, talents, picked, picker]) return (
      diff --git a/apps/web/src/hooks/judge.ts b/apps/web/src/hooks/judge.ts index 04bfe91..6b258eb 100644 --- a/apps/web/src/hooks/judge.ts +++ b/apps/web/src/hooks/judge.ts @@ -26,19 +26,21 @@ function judges = never>( export const useJudge = () => { const state = useGameState() if (!state) throw new Error('Game state is not set') + const current = state.props.current return useMemo(() => { - const result = judges(state.props.current, ['age']) + const result = judges(current, ['age']) return result - }, [state.props.current]) + }, [current]) } export const useEndJudge = () => { const summary = useSummary() const state = useGameState() if (!state) throw new Error('Game state is not set') + const highest = state.props.highest return useMemo(() => { - const props = { ...state.props.highest, summary } + const props = { ...highest, summary } const result = judges(props) return result - }, [state.props.highest, summary]) + }, [highest, summary]) } diff --git a/apps/web/src/hooks/storage.ts b/apps/web/src/hooks/storage.ts index cbbd55a..b31140f 100644 --- a/apps/web/src/hooks/storage.ts +++ b/apps/web/src/hooks/storage.ts @@ -1,6 +1,7 @@ -import { useCallback } from 'react' +import { useEffect, useCallback, useTransition } from 'react' import { atom, useAtom } from 'jotai' -import { useConfigInject, useRawProfile, useProfileInject } from '@remake/hooks' +import { useConfigInject, useProfileInject, useRawProfile } from '@remake/hooks' +import { useUniqueInject, useUnique } from '@remake/hooks' import { get, set } from '@/storage' import { config } from '@/config' @@ -9,11 +10,12 @@ const initedAtom = atom(false) export const useInit = () => { const configInject = useConfigInject() const profileInject = useProfileInject() + const uniqueInject = useUniqueInject() const [inited, setInited] = useAtom(initedAtom) const loader = useCallback(async () => { if (inited) return configInject(config) - const { profile } = await get(['profile']) + const { profile, unique } = await get(['profile', 'unique']) const parsed = profile ? JSON.parse(profile) || {} : {} profileInject({ ...parsed, @@ -22,15 +24,19 @@ export const useInit = () => { events: new Set(parsed.events || []), talents: new Set(parsed.talents || []), }) + if (unique) uniqueInject(JSON.parse(unique)) setInited(true) - }, [inited, configInject, profileInject, setInited]) + }, [inited, configInject, profileInject, uniqueInject, setInited]) return [inited, loader] as const } -export const useSaver = () => { +export const useWatcher = () => { const [profile] = useRawProfile() + const unique = useUnique() const [inited] = useAtom(initedAtom) - return useCallback(async () => { + const [p, saveProfile] = useTransition() + const [u, saveUnique] = useTransition() + useEffect(() => { if (!inited || !profile) return const str = JSON.stringify({ ...profile, @@ -39,6 +45,17 @@ export const useSaver = () => { events: Array.from(profile.events), talents: Array.from(profile.talents), }) - return await set({ profile: str }) - }, [inited, profile]) + saveProfile(async () => { + await set({ profile: str }) + }) + }, [inited, profile, saveProfile]) + useEffect(() => { + if (!inited || !unique) return + const str = JSON.stringify(unique) + saveUnique(async () => { + await set({ unique: str }) + }) + }, [inited, unique, saveUnique]) + + return p || u } diff --git a/packages/core/src/character.spec.ts b/packages/core/src/character.spec.ts index 78db4c1..b6354d5 100644 --- a/packages/core/src/character.spec.ts +++ b/packages/core/src/character.spec.ts @@ -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) => 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) }) }) diff --git a/packages/core/src/character.ts b/packages/core/src/character.ts index 6859c0c..3f208e4 100644 --- a/packages/core/src/character.ts +++ b/packages/core/src/character.ts @@ -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 +export type BaseChara = Omit +export type BaseAlloc = Omit export interface UniqueGenCfg { prop: WeightItem[] talent: WeightItem[] @@ -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) +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 000d8cd..c06f033 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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' diff --git a/packages/hooks/src/alloc.ts b/packages/hooks/src/alloc.ts index e48dc29..5036a6c 100644 --- a/packages/hooks/src/alloc.ts +++ b/packages/hooks/src/alloc.ts @@ -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], ) } diff --git a/packages/hooks/src/character.ts b/packages/hooks/src/character.ts new file mode 100644 index 0000000..a7c63c9 --- /dev/null +++ b/packages/hooks/src/character.ts @@ -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(null) +export const timesAtom = atom(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({ + 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(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' diff --git a/packages/hooks/src/config.ts b/packages/hooks/src/config.ts index 81d300b..3195cbf 100644 --- a/packages/hooks/src/config.ts +++ b/packages/hooks/src/config.ts @@ -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(null) diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 91ad7c0..3626b85 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -3,4 +3,5 @@ export * from './profile' export * from './talent' export * from './play' export * from './alloc' +export * from './character' export type * from '@remake/core' diff --git a/packages/hooks/src/play.ts b/packages/hooks/src/play.ts index 2e5e38b..e887f7e 100644 --- a/packages/hooks/src/play.ts +++ b/packages/hooks/src/play.ts @@ -25,6 +25,7 @@ export type Log = Omit & { props: Omit } +export const modeAtom = atom(Mode.Classic) export const stepAtom = atom(Step.Idle) export const gameStateAtom = atom(null) export const logsAtom = atom([]) @@ -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>(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 } diff --git a/packages/hooks/src/talent.ts b/packages/hooks/src/talent.ts index 48dcbac..754c68c 100644 --- a/packages/hooks/src/talent.ts +++ b/packages/hooks/src/talent.ts @@ -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')