mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-28 01:06:49 +08:00
update: character
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
+28
-31
@@ -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 <Loading />
|
||||
switch (step) {
|
||||
case Step.Idle:
|
||||
return <Home />
|
||||
case Step.Mode:
|
||||
return <Mode />
|
||||
case Step.Chara:
|
||||
return <Chara />
|
||||
case Step.Pick:
|
||||
return <Pick />
|
||||
case Step.Alloc:
|
||||
return <Alloc />
|
||||
case Step.Play:
|
||||
return <Play />
|
||||
case Step.Summary:
|
||||
return <Summary />
|
||||
default:
|
||||
return null
|
||||
export function Container() {
|
||||
/* prettier-ignore */
|
||||
switch (useStep()) {
|
||||
case Step.Idle: return <Home />
|
||||
case Step.Mode: return <Mode />
|
||||
case Step.Chara: return <Chara />
|
||||
case Step.Pick: return <Pick />
|
||||
case Step.Alloc: return <Alloc />
|
||||
case Step.Play: return <Play />
|
||||
case Step.Summary: return <Summary />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
/* prettier-ignore */
|
||||
const Loading = () => <div className="screen loading"><h2>载入中...</h2></div>
|
||||
const Saving = ({ active }: { active: boolean }) => (
|
||||
<div className={`saving ${active ? 'active' : ''}`}>保存中...</div>
|
||||
)
|
||||
|
||||
/* prettier-ignore */
|
||||
export function Game() {
|
||||
const [inited, init] = useInit()
|
||||
const saving = useWatcher()
|
||||
useEffect(() => { if (!inited) init() }, [inited, init])
|
||||
if (!inited) return <Loading />
|
||||
return <><Container /><Saving active={saving} /></>
|
||||
}
|
||||
export default Game
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -103,7 +103,7 @@ export function Alloc() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<div className="controls">
|
||||
<button className="info" onClick={() => random()}>
|
||||
随机分配
|
||||
</button>
|
||||
|
||||
@@ -1 +1,90 @@
|
||||
.screen.chara {}
|
||||
.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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="chara-details">
|
||||
<ul className="properties">
|
||||
{keys(props).map(key => (
|
||||
<li
|
||||
key={key}
|
||||
className={`property ${key} grade-${judgeGradeByValue(key, props[key])}`}
|
||||
>
|
||||
<span>{properties[key]}</span>
|
||||
<span className="font-mono">{props[key]}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ul className="talent-list">
|
||||
{detail.talent.map(id => (
|
||||
<li key={id}>
|
||||
<TalentComponent id={id} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface UniqueProps {
|
||||
selected?: boolean
|
||||
picker?: () => void
|
||||
}
|
||||
function Unique({ selected, picker }: UniqueProps) {
|
||||
const [unique, generator] = useUniqueGenerator()
|
||||
return (
|
||||
<li
|
||||
className={`character ${selected ? 'selected' : ''}`}
|
||||
onClick={picker}
|
||||
>
|
||||
<span className="name">独一无二的我</span>
|
||||
{unique && <Details detail={unique} />}
|
||||
{!unique && (
|
||||
<div className="chara-details generator">
|
||||
<ul>
|
||||
<li>6000万玩家中独一无二的角色卡</li>
|
||||
<li>所有属性 所有天赋 随机生成</li>
|
||||
<li>每人只能生成一次</li>
|
||||
</ul>
|
||||
<button
|
||||
className="primary focus"
|
||||
onClick={() => generator()}
|
||||
>
|
||||
生成唯一角色
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
interface CharacterProps {
|
||||
id: number
|
||||
selected?: boolean
|
||||
picker?: (id: number) => void
|
||||
}
|
||||
function Character({ id, selected, picker }: CharacterProps) {
|
||||
const character = characters.get(id)!
|
||||
return (
|
||||
<li
|
||||
className={`character ${selected ? 'selected' : ''}`}
|
||||
onClick={() => picker?.(id)}
|
||||
>
|
||||
<span className="name">{character.name}</span>
|
||||
<Details detail={character} />
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export function Chara() {
|
||||
return <div className="screen chara"></div>
|
||||
const u = useUnique()
|
||||
const [{ unique, characters }, puller] = useCharaPuller()
|
||||
const [picked, picker] = useCharaPicker()
|
||||
const start = useCharaStart()
|
||||
const ready = picked && (picked.type === 'unique' ? !!u : true)
|
||||
return (
|
||||
<div className="screen chara">
|
||||
<ul className="chara-list">
|
||||
{unique && (
|
||||
<Unique
|
||||
selected={picked?.type === 'unique'}
|
||||
picker={() => picker.unique()}
|
||||
/>
|
||||
)}
|
||||
{characters.map(id => (
|
||||
<Character
|
||||
key={id}
|
||||
id={id}
|
||||
selected={picked?.id === id}
|
||||
picker={id => picker.chara(id)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<div className="controls">
|
||||
<button className="info" onClick={puller}>
|
||||
都不是
|
||||
</button>
|
||||
<button
|
||||
className={ready ? 'primary' : 'error'}
|
||||
onClick={ready ? () => start(picked!) : undefined}
|
||||
>
|
||||
开始新人生
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Chara
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export function Loading() {
|
||||
return (
|
||||
<div className="screen loading">
|
||||
<h2>载入中...</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
||||
@@ -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<HTMLUListElement>(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
|
||||
|
||||
@@ -38,7 +38,7 @@ function TalentList({ picked, picker }: TalentListProps) {
|
||||
}
|
||||
hasInitialized.current = true
|
||||
}
|
||||
}, [picked, picker])
|
||||
}, [profile.locked, talents, picked, picker])
|
||||
|
||||
return (
|
||||
<ul className="talent-list">
|
||||
|
||||
@@ -26,19 +26,21 @@ function judges<T extends Judges, K extends keyof Record<T, number> = 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])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user