mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-19 18:43:53 +08:00
add: toast achievement & message
This commit is contained in:
@@ -17,7 +17,8 @@
|
||||
"@remake/vitex": "workspace:*",
|
||||
"jotai": "^2.20.2",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
"react-dom": "^19.2.8",
|
||||
"react-hot-toast": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.18",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useInit, useWatcher } from '@/hooks/storage'
|
||||
import { useStep, Step } from '@remake/hooks'
|
||||
import ToastContainer from '@/toast'
|
||||
import Home from '@/containers/Home'
|
||||
import Mode from '@/containers/Mode'
|
||||
import Chara from '@/containers/Chara'
|
||||
@@ -40,6 +41,6 @@ export function Game() {
|
||||
const saving = useWatcher()
|
||||
useEffect(() => { if (!inited) init() }, [inited, init])
|
||||
if (!inited) return <Loading />
|
||||
return <><Container /><Saving active={saving} /></>
|
||||
return <><Container /><ToastContainer /><Saving active={saving} /></>
|
||||
}
|
||||
export default Game
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.achievement-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
height: 2rem;
|
||||
gap: 1rem;
|
||||
color: var(--grade-color);
|
||||
background: var(--background, var(--base-background-color));
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 0.0625rem solid var(--grade-color);
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s ease-in-out;
|
||||
.name { font-weight: bold; }
|
||||
.description {
|
||||
font-size: 0.85rem;
|
||||
user-select: none;
|
||||
&::before { content: '['; margin-right: 0.125rem; }
|
||||
&::after { content: ']'; margin-left: 0.125rem; }
|
||||
&::before, &::after { color: var(--grade-color); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type Achievement as A, achievement } from '@remake/data'
|
||||
import './Achievement.css'
|
||||
|
||||
export interface AchievementProps {
|
||||
id: A['id']
|
||||
}
|
||||
export function Achievement({ id }: AchievementProps) {
|
||||
const ach = achievement.get(id)
|
||||
if (!ach) return null
|
||||
const grade = ach?.grade ?? 0
|
||||
return (
|
||||
<div className={`achievement-item grade-${grade}`}>
|
||||
<span className="name">{ach.name}</span>
|
||||
<span className="description">{ach.description}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Achievement
|
||||
@@ -0,0 +1,10 @@
|
||||
.message-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
border: 0.0625rem solid var(--base-text-color);
|
||||
color: var(--base-text-color);
|
||||
background: var(--base-background-color);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import './Message.css'
|
||||
|
||||
export interface MsgToastProps {
|
||||
message: React.ReactNode
|
||||
}
|
||||
export function MsgToast({ message }: MsgToastProps) {
|
||||
return <div className="message-item">{message}</div>
|
||||
}
|
||||
|
||||
export default MsgToast
|
||||
@@ -2,6 +2,7 @@ import { useAchv, useGoHome } from '@remake/hooks'
|
||||
import { judge, judgeGrade, judgeGradeByValue } from '@/config'
|
||||
import { rates } from '@/display'
|
||||
import { achievements, type Achievement } from '@remake/data'
|
||||
import { toastMsg } from '@/toast'
|
||||
import './Achv.css'
|
||||
|
||||
interface AchievementProps {
|
||||
@@ -22,6 +23,7 @@ export function AchievementItem({ id, colled }: AchievementProps) {
|
||||
|
||||
export function Achv() {
|
||||
const goHome = useGoHome()
|
||||
const showRank = () => toastMsg('别卷了,没有排行榜', 'achv-rank')
|
||||
const [stats, sorted] = useAchv()
|
||||
const jtimes = judge('times', stats.times)
|
||||
const gtimes = judgeGrade('times', jtimes)
|
||||
@@ -34,7 +36,9 @@ export function Achv() {
|
||||
return (
|
||||
<div className="screen achv">
|
||||
<div className="controls">
|
||||
<button className="secondary">排行榜</button>
|
||||
<button className="secondary" onClick={showRank}>
|
||||
排行榜
|
||||
</button>
|
||||
<button className="primary" onClick={goHome}>
|
||||
返回
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { properties } from '@/display'
|
||||
import { keys } from '@remake/vitex'
|
||||
import { judgeGradeByValue } from '@/config'
|
||||
import { talents } from '@remake/data'
|
||||
import { toastAchvs, toastMsg } from '@/toast'
|
||||
import Replaced from '@/components/Replaced'
|
||||
import './Alloc.css'
|
||||
|
||||
@@ -65,12 +66,9 @@ export function Alloc() {
|
||||
const start = useStart()
|
||||
const [showDetail, setShowDetail] = useState(false)
|
||||
const handleNext = () => {
|
||||
if (left != 0) {
|
||||
// TODO: Show a warning that there are still points left
|
||||
return
|
||||
}
|
||||
if (left != 0) return toastMsg('还有剩余点数未分配', 'alloc-toast')
|
||||
const achievements = start()
|
||||
// TODO: Show achievements
|
||||
toastAchvs(achievements)
|
||||
console.log('Achievements', achievements)
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { judgeGradeByValue } from '@/config'
|
||||
import { characters } from '@remake/data'
|
||||
import { properties } from '@/display'
|
||||
import { keys } from '@remake/vitex'
|
||||
import { toastMsg } from '@/toast'
|
||||
import Talent from '@/components/Talent'
|
||||
import './Chara.css'
|
||||
|
||||
@@ -89,6 +90,11 @@ export function Chara() {
|
||||
const [{ unique, characters }, puller] = useCharaPuller()
|
||||
const [picked, picker] = useCharaPicker()
|
||||
const submit = useCharaSubmit()
|
||||
const handleSubmit = () => {
|
||||
if (ready) return submit(picked)
|
||||
if (!picked) toastMsg('请先选择角色', 'chara-toast-pick')
|
||||
else toastMsg('请先生成唯一角色', 'chara-toast-unique')
|
||||
}
|
||||
const ready = picked && (picked.type === 'unique' ? !!u : true)
|
||||
return (
|
||||
<div className="screen chara">
|
||||
@@ -114,7 +120,7 @@ export function Chara() {
|
||||
</button>
|
||||
<button
|
||||
className={ready ? 'primary' : 'error'}
|
||||
onClick={ready ? () => submit(picked!) : undefined}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
开始新人生
|
||||
</button>
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
width: auto;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
>.github-btn,
|
||||
>.theme-toggle-btn {
|
||||
>button {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
padding: 0.25rem;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTalentPuller, useTalentPicker } from '@remake/hooks'
|
||||
import { useTalentSubmit, useSubmitIsEnable } from '@remake/hooks'
|
||||
import { PullCount, dev } from '@/config'
|
||||
import { toastMsg } from '@/toast'
|
||||
import Talent from '@/components/Talent'
|
||||
import './Pick.css'
|
||||
|
||||
@@ -9,7 +10,13 @@ export function Pick() {
|
||||
const [talents, picker] = useTalentPicker()
|
||||
const { enabled, min, max } = useSubmitIsEnable()
|
||||
const submit = useTalentSubmit()
|
||||
// useEffect(puller, [])
|
||||
const msg = enabled
|
||||
? '下一步'
|
||||
: `请选取 ${min == max ? min : `${min}~${max}`} 个天赋`
|
||||
const handleSubmit = () => {
|
||||
if (enabled) return submit()
|
||||
toastMsg(msg, 'pick-toast')
|
||||
}
|
||||
if (!pulled)
|
||||
return (
|
||||
<div className="screen talent-pick">
|
||||
@@ -33,15 +40,12 @@ export function Pick() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{enabled ? (
|
||||
<button className="primary" onClick={() => submit()}>
|
||||
下一步
|
||||
</button>
|
||||
) : (
|
||||
<button className="error">
|
||||
请选取 {min == max ? min : `${min}~${max}`} 个天赋
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={enabled ? 'primary' : 'error'}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{msg}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { achievements, events, talents } from '@remake/data'
|
||||
import { properties } from '@/display'
|
||||
import { AutoInterval } from '@/config'
|
||||
import { format } from '@remake/vitex'
|
||||
import { toastAchvs } from '@/toast/Achv'
|
||||
import './Play.css'
|
||||
|
||||
function LogTalent({ id }: { id: number }) {
|
||||
@@ -153,13 +154,13 @@ export function Play() {
|
||||
const handleNext = useCallback(() => {
|
||||
if (ended) return
|
||||
const achievements = next()
|
||||
// TODO: Show achievements
|
||||
toastAchvs(achievements)
|
||||
console.debug('Achievements:', achievements)
|
||||
}, [ended, next])
|
||||
const handleGotoSummary = useCallback(() => {
|
||||
if (!ended) return
|
||||
const achievements = gotoSummary()
|
||||
// TODO: Show achievements
|
||||
toastAchvs(achievements)
|
||||
console.debug('Achievements:', achievements)
|
||||
}, [ended, gotoSummary])
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react'
|
||||
import { usePicked, useEnd, useProfile, useIsClassic } from '@remake/hooks'
|
||||
import { useEndJudge } from '@/hooks/judge'
|
||||
import { properties, judgeDisplay } from '@/display'
|
||||
import { toastAchvs, toastMsg } from '@/toast'
|
||||
import Talent from '@/components/Talent'
|
||||
import './Summary.css'
|
||||
|
||||
@@ -54,9 +55,13 @@ function TalentList({ picked, picker }: TalentListProps) {
|
||||
export default function Summary() {
|
||||
const isClassic = useIsClassic()
|
||||
const [locked, picker, end] = useEnd()
|
||||
const handlePicker = (id: number) => {
|
||||
if (isClassic) picker(id)
|
||||
else toastMsg('名人天赋不可锁定', 'summary-toast')
|
||||
}
|
||||
const handleEnd = () => {
|
||||
const achievements = end()
|
||||
// TODO: Show achievements
|
||||
toastAchvs(achievements)
|
||||
console.log('Achievements', achievements)
|
||||
}
|
||||
return (
|
||||
@@ -68,7 +73,7 @@ export default function Summary() {
|
||||
? '你可以锁定一个天赋,下辈子还能抽到'
|
||||
: '名人天赋不可锁定'}
|
||||
</div>
|
||||
<TalentList picked={locked} picker={picker} />
|
||||
<TalentList picked={locked} picker={handlePicker} />
|
||||
</div>
|
||||
<button className="primary" onClick={handleEnd}>
|
||||
再次重开
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
.achv-toast-container {
|
||||
position: fixed;
|
||||
width: min(calc(100dvw - 2rem), 48rem);
|
||||
top: 6rem;
|
||||
left: 1rem;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
.toast-item {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&.visible {
|
||||
animation: achv-fade-in 0.2s ease-out forwards;
|
||||
}
|
||||
&.hidden {
|
||||
animation: achv-fade-out 0.2s ease-out forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes achv-slide-in {
|
||||
0% { opacity: 0; transform: translateX(-100%); }
|
||||
100% { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes achv-slide-out {
|
||||
0% { opacity: 1; transform: translateX(0); }
|
||||
100% { opacity: 0; transform: translateX(-100%); }
|
||||
}
|
||||
|
||||
@keyframes achv-fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes achv-fade-out {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useToaster, toast } from 'react-hot-toast/headless'
|
||||
import Achievement from '@/components/Achievement'
|
||||
import './Achv.css'
|
||||
|
||||
export function ToastAchvContainer() {
|
||||
const { toasts, handlers } = useToaster({ duration: 1500 }, 'achv-toast')
|
||||
const { startPause, endPause, calculateOffset, updateHeight } = handlers
|
||||
return (
|
||||
<div
|
||||
className="achv-toast-container"
|
||||
onMouseEnter={startPause}
|
||||
onMouseLeave={endPause}
|
||||
>
|
||||
{toasts.map(toast => {
|
||||
const offset = calculateOffset(toast, {
|
||||
reverseOrder: true,
|
||||
gutter: 8,
|
||||
})
|
||||
console.log('toast offset', toast.id, offset)
|
||||
return (
|
||||
<div
|
||||
className={`toast-item ${toast.visible ? 'visible' : 'hidden'}`}
|
||||
key={toast.id}
|
||||
ref={el => {
|
||||
if (el && typeof toast.height !== 'number') {
|
||||
const height = el.getBoundingClientRect().height
|
||||
console.log('toast height', toast.id, height)
|
||||
updateHeight(toast.id, height)
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
top: `${offset}px`,
|
||||
}}
|
||||
>
|
||||
{typeof toast.message == 'function'
|
||||
? toast.message(toast)
|
||||
: toast.message}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ToastAchvContainer
|
||||
|
||||
export function toastAchvs(achievements: number[]) {
|
||||
if (achievements.length === 0) return
|
||||
for (const id of achievements) {
|
||||
toast(<Achievement id={id} />, { id: '' + id, toasterId: 'achv-toast' })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
.msg-toast-container {
|
||||
position: fixed;
|
||||
width: min(calc(100dvw - 2rem), 48rem);
|
||||
top: 6rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
.toast-item {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&.visible {
|
||||
animation: msg-slide-in 0.2s ease-out forwards;
|
||||
}
|
||||
&.hidden {
|
||||
animation: msg-fade-out 0.2s ease-out forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes msg-slide-in {
|
||||
0% { opacity: 0; transform: translateY(-100%); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes msg-slide-out {
|
||||
0% { opacity: 1; transform: translateY(0); }
|
||||
100% { opacity: 0; transform: translateY(-100%); }
|
||||
}
|
||||
|
||||
@keyframes msg-fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes msg-fade-out {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useToaster, toast } from 'react-hot-toast/headless'
|
||||
import Message from '@/components/Message'
|
||||
import './Msg.css'
|
||||
|
||||
export function ToastMsgContainer() {
|
||||
const { toasts, handlers } = useToaster({ duration: 1500 }, 'msg-toast')
|
||||
const { startPause, endPause, calculateOffset, updateHeight } = handlers
|
||||
return (
|
||||
<div
|
||||
className="msg-toast-container"
|
||||
onMouseEnter={startPause}
|
||||
onMouseLeave={endPause}
|
||||
>
|
||||
{toasts.map(toast => {
|
||||
const offset = calculateOffset(toast, {
|
||||
reverseOrder: true,
|
||||
gutter: 8,
|
||||
})
|
||||
console.log('toast offset', toast.id, offset)
|
||||
return (
|
||||
<div
|
||||
className={`toast-item ${toast.visible ? 'visible' : 'hidden'}`}
|
||||
key={toast.id}
|
||||
ref={el => {
|
||||
if (el && typeof toast.height !== 'number') {
|
||||
const height = el.getBoundingClientRect().height
|
||||
console.log('toast height', toast.id, height)
|
||||
updateHeight(toast.id, height)
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
top: `${offset}px`,
|
||||
}}
|
||||
>
|
||||
{typeof toast.message == 'function'
|
||||
? toast.message(toast)
|
||||
: toast.message}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ToastMsgContainer
|
||||
|
||||
export function toastMsg(message: string, key: string) {
|
||||
toast(<Message message={message} />, { id: key, toasterId: 'msg-toast' })
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export * from './Msg'
|
||||
export * from './Achv'
|
||||
import ToastAchvContainer from './Achv'
|
||||
import ToastMsgContainer from './Msg'
|
||||
|
||||
export const ToastContainer = () => (
|
||||
<>
|
||||
<ToastAchvContainer />
|
||||
<ToastMsgContainer />
|
||||
</>
|
||||
)
|
||||
|
||||
export default ToastContainer
|
||||
Generated
+26
@@ -56,6 +56,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^19.2.8
|
||||
version: 19.2.8(react@19.2.8)
|
||||
react-hot-toast:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: ^19.2.18
|
||||
@@ -1059,6 +1062,11 @@ packages:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
goober@2.1.19:
|
||||
resolution: {integrity: sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==}
|
||||
peerDependencies:
|
||||
csstype: ^3.0.10
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1503,6 +1511,13 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^19.2.8
|
||||
|
||||
react-hot-toast@2.6.0:
|
||||
resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: '>=16'
|
||||
react-dom: '>=16'
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -2868,6 +2883,10 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
gopd: 1.2.0
|
||||
|
||||
goober@2.1.19(csstype@3.2.3):
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
happy-dom@20.11.2:
|
||||
@@ -3280,6 +3299,13 @@ snapshots:
|
||||
react: 19.2.8
|
||||
scheduler: 0.27.0
|
||||
|
||||
react-hot-toast@2.6.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
goober: 2.1.19(csstype@3.2.3)
|
||||
react: 19.2.8
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
Reference in New Issue
Block a user