update: achievement

This commit is contained in:
Vick Scarlet
2026-08-15 22:11:37 +08:00
parent a2e40230c7
commit f2e8fac4de
12 changed files with 284 additions and 17 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
width: 100%;
padding: 0;
box-sizing: border-box;
.title {
> .title {
display: flex;
flex-direction: row;
justify-content: center;
+2
View File
@@ -8,6 +8,7 @@ import Pick from '@/containers/TalentPick'
import Alloc from '@/containers/Alloc'
import Play from '@/containers/Play'
import Summary from '@/containers/Summary'
import Achv from '@/containers/Achv'
import './Game.css'
export function Container() {
@@ -20,6 +21,7 @@ export function Container() {
case Step.Alloc: return <Alloc />
case Step.Play: return <Play />
case Step.Summary: return <Summary />
case Step.Achv: return <Achv />
default: return null
}
}
+2 -2
View File
@@ -52,8 +52,8 @@ export const JudgeLineMap = {
spirit: [0, 1, 2, 4, 7, 9, 11],
intelligence: [0, 1, 2, 4, 7, 9, 11, 21, 131, 501],
strength: [0, 1, 2, 4, 7, 9, 11, 21, 101, 401, 1001, 2001],
talent: [0, 0.3, 0.6, 0.9],
event: [0, 0.2, 0.4, 0.6],
talent: [0, 30, 60, 90],
event: [0, 20, 40, 60],
}
// 可评级属性
export type Judges = keyof typeof JudgeLineMap
+120
View File
@@ -0,0 +1,120 @@
.screen.achv {
height: calc(100dvh - 2rem);
width: min(calc(100dvw - 2rem), 48rem);
gap: 1rem;
> .stats {
> .details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
> div {
display: flex;
flex-direction: column;
align-items: center;
border: 0.0625rem solid var(--grade-color);
color: var(--grade-color);
box-sizing: border-box;
position: relative;
transform-style: preserve-3d;
user-select: none;
> span {
display: flex;
align-items: center;
justify-content: center;
}
> .title {
font-weight: bold;
font-size: 1.5rem;
height: 2.5rem;
}
> .value {
height: 2rem;
}
}
> .prg {
padding-bottom: 0.1875rem;
&::before,
&::after {
content: '';
display: block;
position: absolute;
height: 0.1875rem;
left: 0;
bottom: 0;
transform: translateZ(-1px);
}
&::after {
width: var(--progress, 0%);
background: var(--grade-color);
transition: width 0.2s ease-in-out;
}
&::before {
width: 100%;
border-top: 0.0625rem solid var(--grade-color);
box-sizing: border-box;
}
}
}
}
> .achievement-list {
flex: 1 0;
width: 100%;
> ul {
flex: 1 0;
display: grid;
flex-direction: column;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
gap: 0.5rem;
box-sizing: border-box;
> li.achievement {
margin-right: 0.0625rem;
display: flex;
flex-direction: column;
align-items: center;
color: var(--grade-color);
border: 0.0625rem solid var(--grade-color);
padding: 0 0.0625rem;
box-sizing: border-box;
transform-style: preserve-3d;
position: relative;
user-select: none;
>.name {
font-weight: bold;
font-size: 1.2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
>.description {
height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
&::before{
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
padding: 0.0625rem;
margin: -0.0625rem;
transform: translateZ(1px);
background: var(--base-background-color);
opacity: 0.8;
}
&.colled {
&::before{
transform: translateZ(-1px);
background: var(--grade-color);
opacity: 0.15;
}
}
}
}
}
}
+82
View File
@@ -0,0 +1,82 @@
import { useAchv, useGoHome } from '@remake/hooks'
import { judge, judgeGrade, judgeGradeByValue } from '@/config'
import { rates } from '@/display'
import { achievements, type Achievement } from '@remake/data'
import './Achv.css'
interface AchievementProps {
id: Achievement['id']
colled: boolean
}
export function AchievementItem({ id, colled }: AchievementProps) {
const { name, grade, description, hide } = achievements.get(id)!
return (
<li className={`achievement grade-${grade} ${colled ? 'colled' : ''}`}>
<span className="name">{hide && !colled ? '???' : name}</span>
<span className="description">
{hide && !colled ? '???' : description}
</span>
</li>
)
}
export function Achv() {
const goHome = useGoHome()
const [stats, sorted] = useAchv()
const jtimes = judge('times', stats.times)
const gtimes = judgeGrade('times', jtimes)
const jachv = judge('achievements', stats.achv)
const gachv = judgeGrade('achievements', jachv)
const gevent = judgeGradeByValue('event', stats.event)
const revent = stats.event.toFixed(2) + '%'
const gtalent = judgeGradeByValue('talent', stats.talent)
const rtalent = stats.talent.toFixed(2) + '%'
return (
<div className="screen achv">
<div className="controls">
<button className="info"></button>
<button className="primary" onClick={goHome}>
</button>
</div>
<div className="section stats">
<div className="title"></div>
<div className="details">
<div className={`grade-${gtimes}`}>
<span className="title">{stats.times}</span>
<span className="value">{rates.times[jtimes]}</span>
</div>
<div className={`grade-${gachv}`}>
<span className="title">{stats.achv}</span>
<span className="value">
{rates.achievement[jachv]}
</span>
</div>
<div
className={`prg grade-${gevent}`}
style={{ '--progress': revent } as React.CSSProperties}
>
<span className="title"></span>
<span className="value">{revent}</span>
</div>
<div
className={`prg grade-${gtalent}`}
style={{ '--progress': rtalent } as React.CSSProperties}
>
<span className="title"></span>
<span className="value">{rtalent}</span>
</div>
</div>
</div>
<div className="section achievement-list hide-scrollbar">
<div className="title"></div>
<ul className="details hide-scrollbar">
{sorted.map(({ id, colled }) => (
<AchievementItem key={id} id={id} colled={colled} />
))}
</ul>
</div>
</div>
)
}
export default Achv
+11
View File
@@ -21,4 +21,15 @@
-webkit-tap-highlight-color: transparent;
transition: background-color 0.3s;
}
>.controls {
flex-direction: column;
> div {
flex: 1 0;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
> button { flex: 1 0; }
}
}
}
+17 -4
View File
@@ -1,19 +1,32 @@
import { useRemake } from '@remake/hooks'
import { useRemake, useGoAchv } from '@remake/hooks'
import { TextSvg } from '@/components/TextSvg'
import { ThemeToggle } from '@/components/ThemeToggle'
import './Home.css'
export default function Home() {
const remake = useRemake()
const goAchv = useGoAchv()
return (
<div className="screen home">
<div className="title">
<TextSvg text="人生重开模拟器" className="main" />
<TextSvg text="这垃圾人生一秒也不想待了" className="sub" />
</div>
<button className="primary focus" onClick={remake}>
</button>
<div className="controls">
<div>
<button className="primary focus" onClick={remake}>
</button>
</div>
<div>
<button className="info" onClick={goAchv}>
</button>
<button className="info" onClick={goAchv}>
</button>
</div>
</div>
<ThemeToggle />
</div>
)
+34
View File
@@ -0,0 +1,34 @@
import { useCallback } from 'react'
import { events, talents, achievements } from '@remake/data'
import { useProfile, useSetStep, Step } from '.'
const TotalEvents = events.size
const TotalTalents = talents.size
export const useAchv = () => {
const [profile] = useProfile()
const o = new Map(Array.from(profile.achievements, (id, i) => [id, i]))
const sorted = Array.from(achievements.keys())
.sort((a, b) => {
if (o.has(a) && o.has(b)) return o.get(b)! - o.get(a)!
if (o.has(a)) return -1
if (o.has(b)) return 1
const { hide: ha, grade: ga } = achievements.get(a)!
const { hide: hb, grade: gb } = achievements.get(b)!
if (ha == hb) return gb - ga
return ha ? 1 : -1
})
.map(id => ({ id, colled: o.has(id) }))
const stats = {
times: profile.times,
achv: profile.achievements.size,
event: (profile.events.size / TotalEvents) * 100,
talent: (profile.talents.size / TotalTalents) * 100,
}
return [stats, sorted] as const
}
export const useGoAchv = () => {
const setStep = useSetStep()
return useCallback(() => setStep(Step.Achv), [setStep])
}
+4 -5
View File
@@ -42,12 +42,11 @@ export const usePoints = () => {
const isClassic = useIsClassic()
const { additionalPoints } = useReplaced()
const additional = additionalPoints.points
if (!isClassic) {
const base = useAtomValue(baseAllocAtom)
return { base: alloced(base), total: additional, additional }
}
const ba = useAtomValue(baseAllocAtom)
const { points } = useConfig()
return { base: points, total: points + additional, additional }
const base = isClassic ? points : alloced(ba)
const total = isClassic ? points + additional : additional
return { base, total, additional }
}
export const useLeftPoints = () => {
+2 -3
View File
@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react'
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { pickedAtom, replacedAtom, baseAllocAtom } from '.'
import { useConfig, useProfile, useSetStep, Step } from '.'
import { useConfig, useSetStep, Step } from '.'
import { uniqueGenerate, startUnique, startChara } from '@remake/core'
import { pullChara, pick } from '@remake/core'
import type { BaseChara, PullCharaTms } from '@remake/core'
@@ -72,7 +72,6 @@ export const useCharaPicker = () => {
}
export const useCharaSubmit = () => {
const [profile] = useProfile()
const unique = useAtomValue(uniqueAtom)
const setStep = useSetStep()
const setPicked = useSetAtom(pickedAtom)
@@ -93,7 +92,7 @@ export const useCharaSubmit = () => {
setBaseAlloc(result.allocation)
setStep(Step.Alloc)
},
[profile, unique, setPicked, setReplaced, setStep],
[unique, setPicked, setReplaced, setBaseAlloc, setStep],
)
}
+1
View File
@@ -4,4 +4,5 @@ export * from './talent'
export * from './play'
export * from './alloc'
export * from './character'
export * from './achievement'
export type * from '@remake/core'
+8 -2
View File
@@ -14,6 +14,7 @@ export enum Step {
Alloc = 'alloc',
Play = 'play',
Summary = 'summary',
Achv = 'achv',
}
export enum Mode {
@@ -168,7 +169,7 @@ export const useEnd = () => {
return next
})
},
[lock, setLocked],
[lock, isClassic, setLocked],
)
const ender = useCallback(() => {
if (!state)
@@ -183,6 +184,11 @@ export const useEnd = () => {
setProfile(result.profile)
reset()
return result.achievements
}, [state, profile, locked, setStep, setProfile, reset])
}, [state, profile, locked, isClassic, setStep, setProfile, reset])
return [locked, picker, ender] as const
}
export const useGoHome = () => {
const setStep = useSetAtom(stepAtom)
return useCallback(() => setStep(Step.Idle), [setStep])
}