mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-19 18:43:53 +08:00
Compare commits
7
Commits
10debc6515
...
0e8bf1131a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e8bf1131a | ||
|
|
bc2d89fa52 | ||
|
|
63cc529479 | ||
|
|
847d72c384 | ||
|
|
ef55eb703a | ||
|
|
f2e8fac4de | ||
|
|
a2e40230c7 |
@@ -17,7 +17,7 @@
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
.title {
|
||||
> .title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
@@ -4,10 +4,12 @@ import { useStep, Step } from '@remake/hooks'
|
||||
import Home from '@/containers/Home'
|
||||
import Mode from '@/containers/Mode'
|
||||
import Chara from '@/containers/Chara'
|
||||
import Pick from '@/containers/TalentPick'
|
||||
import Pick from '@/containers/Pick'
|
||||
import Alloc from '@/containers/Alloc'
|
||||
import Play from '@/containers/Play'
|
||||
import Summary from '@/containers/Summary'
|
||||
import Achv from '@/containers/Achv'
|
||||
import Thanks from '@/containers/Thanks'
|
||||
import './Game.css'
|
||||
|
||||
export function Container() {
|
||||
@@ -20,6 +22,8 @@ export function Container() {
|
||||
case Step.Alloc: return <Alloc />
|
||||
case Step.Play: return <Play />
|
||||
case Step.Summary: return <Summary />
|
||||
case Step.Achv: return <Achv />
|
||||
case Step.Thanks: return <Thanks />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { type Talent, talents } from '@remake/data'
|
||||
import TalentComponent from './Talent'
|
||||
import { type Talent as T, talents } from '@remake/data'
|
||||
import Talent from './Talent'
|
||||
import './Replaced.css'
|
||||
|
||||
export interface TalentProps {
|
||||
id: Talent['id']
|
||||
chains?: Talent['id'][]
|
||||
id: T['id']
|
||||
chains?: T['id'][]
|
||||
}
|
||||
export function Replaced({ id, chains }: TalentProps) {
|
||||
return (
|
||||
<ul className="replaced">
|
||||
<li>
|
||||
<TalentComponent id={id} selected={true} />
|
||||
<Talent id={id} selected={true} />
|
||||
</li>
|
||||
{chains?.map(id => {
|
||||
const talent = talents.get(id)
|
||||
if (!talent) return null
|
||||
return (
|
||||
<li key={id}>
|
||||
<TalentComponent id={id} selected={false} />
|
||||
<Talent id={id} selected={false} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { type Talent, talents } from '@remake/data'
|
||||
import { type Talent as T, talents } from '@remake/data'
|
||||
import './Talent.css'
|
||||
|
||||
export interface TalentProps {
|
||||
id: Talent['id']
|
||||
id: T['id']
|
||||
selected?: boolean
|
||||
}
|
||||
export function TalentComponent({ id, selected }: TalentProps) {
|
||||
export function Talent({ id, selected }: TalentProps) {
|
||||
const talent = talents.get(id)
|
||||
if (!talent) return null
|
||||
const grade = talent?.grade ?? 0
|
||||
@@ -17,4 +17,4 @@ export function TalentComponent({ id, selected }: TalentProps) {
|
||||
)
|
||||
}
|
||||
|
||||
export default TalentComponent
|
||||
export default Talent
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
.theme-svg {
|
||||
--svg-color: oklch(from var(--text-color, var(--base-text-color)) calc(l + 0.2) c h);
|
||||
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform-origin: center;
|
||||
.sun-center, .sun-beams, .mask-cutter-group {
|
||||
@@ -7,13 +6,26 @@
|
||||
transform-origin: center;
|
||||
}
|
||||
.mask-cutter-group { transform: translate(0.75rem, -0.75rem); }
|
||||
.sun-center {
|
||||
.sun-center {
|
||||
transition: fill 0.2s ease-in-out;
|
||||
fill: var(--svg-color);
|
||||
fill: var(--style-color);
|
||||
}
|
||||
.sun-beams {
|
||||
.sun-beams {
|
||||
transition: stroke 0.2s ease-in-out;
|
||||
stroke: var(--svg-color);
|
||||
stroke: var(--style-color);
|
||||
}
|
||||
}
|
||||
button.theme-toggle-btn {
|
||||
@media (hover: hover) {
|
||||
&:hover .theme-svg{
|
||||
.sun-center { fill: var(--style-background); }
|
||||
.sun-beams { stroke: var(--style-background); }
|
||||
}
|
||||
}
|
||||
|
||||
&:active .theme-svg{
|
||||
.sun-center { fill: var(--style-background); }
|
||||
.sun-beams { stroke: var(--style-background); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ if (import.meta.env.MODE === 'development') {
|
||||
dev[key] = value.split(',').map(v => Number(v))
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
export const Features = dev.features ?? 1
|
||||
// 自动模式自动点击间隔
|
||||
export const AutoInterval = dev.interval ?? 500
|
||||
// 可锁定的天赋数量
|
||||
@@ -52,8 +53,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
|
||||
@@ -119,6 +120,7 @@ export const UniquePropWeight = wg(0, 10)
|
||||
export const UniqueTalentWeight = wg(1, 5)
|
||||
|
||||
export const config: Config = {
|
||||
features: Features,
|
||||
lock: LockLimit,
|
||||
mode: ModeLimit,
|
||||
max: PickMax,
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
.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;
|
||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 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.5;
|
||||
}
|
||||
&.colled {
|
||||
&::before{
|
||||
transform: translateZ(-1px);
|
||||
background: var(--grade-color);
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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="secondary">排行榜</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
|
||||
@@ -80,8 +80,8 @@
|
||||
> span {
|
||||
width: 100%;
|
||||
height: calc(2rem * var(--ratio));
|
||||
color: var(--base-background-color);
|
||||
background: var(--grade-color);
|
||||
color: var(--grade-color);
|
||||
background: var(--base-background-color);
|
||||
border: 0.0625rem solid var(--grade-color);
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
@@ -91,6 +91,10 @@
|
||||
font-weight: bold;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
> span.name {
|
||||
color: var(--base-background-color);
|
||||
background: var(--grade-color);
|
||||
}
|
||||
}
|
||||
> li.left {
|
||||
--left-color: var(--color-error);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from 'react'
|
||||
import { useAllocator, usePointRandomizer, useLeftPoints } from '@remake/hooks'
|
||||
import { usePicked, useReplaced, useStart } from '@remake/hooks'
|
||||
import { usePicked, useReplaced, useStart, useIsClassic } from '@remake/hooks'
|
||||
import type { AdditionalPoint } from '@remake/hooks'
|
||||
import { properties } from '@/display'
|
||||
import { keys } from '@remake/vitex'
|
||||
import { BasePoints, judgeGradeByValue } from '@/config'
|
||||
import { judgeGradeByValue } from '@/config'
|
||||
import { talents } from '@remake/data'
|
||||
import Replaced from '@/components/Replaced'
|
||||
import './Alloc.css'
|
||||
@@ -30,15 +30,17 @@ function AllocInput(props: AllocInputProps) {
|
||||
}
|
||||
|
||||
interface PointsDetailProps {
|
||||
base: number
|
||||
source: AdditionalPoint[]
|
||||
fixed?: boolean
|
||||
}
|
||||
|
||||
function PointsDetail({ source }: PointsDetailProps) {
|
||||
function PointsDetail({ base, source, fixed }: PointsDetailProps) {
|
||||
return (
|
||||
<ul className="points-detail">
|
||||
<li>
|
||||
<span>基础</span>
|
||||
<span className="font-mono">{BasePoints}</span>
|
||||
<span>{fixed ? '固定' : '基础'}</span>
|
||||
<span className="font-mono">{base}</span>
|
||||
</li>
|
||||
{source.map(({ talent, points }) => (
|
||||
<li key={talent}>
|
||||
@@ -51,18 +53,19 @@ function PointsDetail({ source }: PointsDetailProps) {
|
||||
}
|
||||
|
||||
export function Alloc() {
|
||||
const isClassic = useIsClassic()
|
||||
const picked = usePicked()
|
||||
const {
|
||||
talents: { chains },
|
||||
additionalPoints: { source },
|
||||
} = useReplaced()
|
||||
const left = useLeftPoints()
|
||||
const [allocation, allocator] = useAllocator()
|
||||
const { base, left } = useLeftPoints()
|
||||
const [{ alloc, final, base: ba }, allocator] = useAllocator()
|
||||
const random = usePointRandomizer()
|
||||
const start = useStart()
|
||||
const [showDetail, setShowDetail] = useState(false)
|
||||
const handleNext = () => {
|
||||
if (left) {
|
||||
if (left != 0) {
|
||||
// TODO: Show a warning that there are still points left
|
||||
return
|
||||
}
|
||||
@@ -79,32 +82,46 @@ export function Alloc() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ul className="alloc">
|
||||
<ul className={`alloc ${isClassic ? 'classic' : 'modify'}`}>
|
||||
<li className={`left left-${left}`}>
|
||||
<span>剩余点数</span>
|
||||
<span className="name">剩余点数</span>
|
||||
<button
|
||||
className="font-mono"
|
||||
onClick={() => setShowDetail(!showDetail)}
|
||||
>
|
||||
{left}
|
||||
</button>
|
||||
{showDetail && <PointsDetail source={source} />}
|
||||
{showDetail && (
|
||||
<PointsDetail
|
||||
base={base}
|
||||
source={source}
|
||||
fixed={!isClassic}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
{keys(allocation).map(key => (
|
||||
{keys(alloc).map(key => (
|
||||
<li
|
||||
key={key}
|
||||
className={`property grade-${judgeGradeByValue(key, allocation[key])}`}
|
||||
className={`property grade-${judgeGradeByValue(key, final[key])}`}
|
||||
>
|
||||
<span>{properties[key]}</span>
|
||||
<span className="name">
|
||||
{properties[key]}
|
||||
{!isClassic && (
|
||||
<span className="font-mono">[{ba[key]}]</span>
|
||||
)}
|
||||
</span>
|
||||
{!isClassic && (
|
||||
<span className="font-mono">{final[key]}</span>
|
||||
)}
|
||||
<AllocInput
|
||||
point={allocation[key]}
|
||||
point={alloc[key]}
|
||||
onChange={value => allocator(key, value)}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="controls">
|
||||
<button className="info" onClick={() => random()}>
|
||||
<button className="secondary" onClick={() => random()}>
|
||||
随机分配
|
||||
</button>
|
||||
<button
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> .chara-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: calc(0.5rem * var(--ratio));
|
||||
> .character {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -18,7 +18,8 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
font-size: calc(1rem * var(--ratio-font));
|
||||
height: calc(2rem * var(--ratio));
|
||||
}
|
||||
&.selected {
|
||||
border: 0.0625rem solid var(--color-primary);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCharaPuller, useCharaPicker, convertProps } from '@remake/hooks'
|
||||
import { useUnique, useUniqueGenerator, useCharaStart } from '@remake/hooks'
|
||||
import { useCharaPuller, useCharaPicker, useCharaSubmit } from '@remake/hooks'
|
||||
import { useUnique, useUniqueGenerator, convertProps } 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 Talent from '@/components/Talent'
|
||||
import './Chara.css'
|
||||
|
||||
function Details({ detail }: { detail: BaseChara }) {
|
||||
@@ -26,7 +26,7 @@ function Details({ detail }: { detail: BaseChara }) {
|
||||
<ul className="talent-list">
|
||||
{detail.talent.map(id => (
|
||||
<li key={id}>
|
||||
<TalentComponent id={id} />
|
||||
<Talent id={id} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -88,7 +88,7 @@ export function Chara() {
|
||||
const u = useUnique()
|
||||
const [{ unique, characters }, puller] = useCharaPuller()
|
||||
const [picked, picker] = useCharaPicker()
|
||||
const start = useCharaStart()
|
||||
const submit = useCharaSubmit()
|
||||
const ready = picked && (picked.type === 'unique' ? !!u : true)
|
||||
return (
|
||||
<div className="screen chara">
|
||||
@@ -109,12 +109,12 @@ export function Chara() {
|
||||
))}
|
||||
</ul>
|
||||
<div className="controls">
|
||||
<button className="info" onClick={puller}>
|
||||
<button className="secondary" onClick={puller}>
|
||||
都不是
|
||||
</button>
|
||||
<button
|
||||
className={ready ? 'primary' : 'error'}
|
||||
onClick={ready ? () => start(picked!) : undefined}
|
||||
onClick={ready ? () => submit(picked!) : undefined}
|
||||
>
|
||||
开始新人生
|
||||
</button>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,36 @@
|
||||
import { useRemake } from '@remake/hooks'
|
||||
import { useRemake, useFeatures, useGoAchv, useGoThanks } from '@remake/hooks'
|
||||
import { TextSvg } from '@/components/TextSvg'
|
||||
import { ThemeToggle } from '@/components/ThemeToggle'
|
||||
import ThemeToggle from '@/components/ThemeToggle'
|
||||
import './Home.css'
|
||||
|
||||
export default function Home() {
|
||||
const remake = useRemake()
|
||||
const features = useFeatures()
|
||||
const goAchv = useGoAchv()
|
||||
const goThanks = useGoThanks()
|
||||
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>
|
||||
{features && (
|
||||
<div>
|
||||
<button className="secondary" onClick={goAchv}>
|
||||
成就
|
||||
</button>
|
||||
<button className="secondary" onClick={goThanks}>
|
||||
感谢
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// import { useEffect } from 'react'
|
||||
import { useTalentPuller, useTalentPicker } from '@remake/hooks'
|
||||
import { useTalentSubmit, useSubmitIsEnable } from '@remake/hooks'
|
||||
import { PullCount, dev } from '@/config'
|
||||
import TalentComponent from '@/components/Talent'
|
||||
import './TalentPick.css'
|
||||
import Talent from '@/components/Talent'
|
||||
import './Pick.css'
|
||||
|
||||
export default function TalentPick() {
|
||||
export function Pick() {
|
||||
const [pulled, puller] = useTalentPuller()
|
||||
const [talents, picker] = useTalentPicker()
|
||||
const { enabled, min, max } = useSubmitIsEnable()
|
||||
@@ -30,7 +29,7 @@ export default function TalentPick() {
|
||||
<ul className="talent-list">
|
||||
{ps.map(id => (
|
||||
<li key={id} onClick={() => picker(id)}>
|
||||
<TalentComponent id={id} selected={talents.has(id)} />
|
||||
<Talent id={id} selected={talents.has(id)} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -46,3 +45,4 @@ export default function TalentPick() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Pick
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { usePicked, useEnd, useProfile } from '@remake/hooks'
|
||||
import { usePicked, useEnd, useProfile, useIsClassic } from '@remake/hooks'
|
||||
import { useEndJudge } from '@/hooks/judge'
|
||||
import { properties, judgeDisplay } from '@/display'
|
||||
import TalentComponent from '@/components/Talent'
|
||||
import Talent from '@/components/Talent'
|
||||
import './Summary.css'
|
||||
|
||||
function Judges() {
|
||||
@@ -44,7 +44,7 @@ function TalentList({ picked, picker }: TalentListProps) {
|
||||
<ul className="talent-list">
|
||||
{Array.from(talents, id => (
|
||||
<li key={id} onClick={() => picker(id)}>
|
||||
<TalentComponent id={id} selected={picked.has(id)} />
|
||||
<Talent id={id} selected={picked.has(id)} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -52,6 +52,7 @@ function TalentList({ picked, picker }: TalentListProps) {
|
||||
}
|
||||
|
||||
export default function Summary() {
|
||||
const isClassic = useIsClassic()
|
||||
const [locked, picker, end] = useEnd()
|
||||
const handleEnd = () => {
|
||||
const achievements = end()
|
||||
@@ -62,7 +63,11 @@ export default function Summary() {
|
||||
<div className="screen summary">
|
||||
<Judges />
|
||||
<div className="section">
|
||||
<div className="title">你可以锁定一个天赋,下辈子还能抽到</div>
|
||||
<div className="title">
|
||||
{isClassic
|
||||
? '你可以锁定一个天赋,下辈子还能抽到'
|
||||
: '名人天赋不可锁定'}
|
||||
</div>
|
||||
<TalentList picked={locked} picker={picker} />
|
||||
</div>
|
||||
<button className="primary" onClick={handleEnd}>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
.screen.thanks {
|
||||
height: calc(100dvh - 2rem);
|
||||
gap: 1rem;
|
||||
.person {
|
||||
--style-color: var(--custom-color, var(--base-text-color));
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--style-color);
|
||||
background: var(--base-background-color);
|
||||
transition: background 0.2s ease-in-out, color 0.2s ease-in-out;
|
||||
> .name {
|
||||
display: flex;
|
||||
text-wrap: wrap;
|
||||
word-break: break-all;
|
||||
min-height: 2.5rem;
|
||||
width: 12rem;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
align-items: center;
|
||||
text-shadow:
|
||||
-0.0625rem -0.0625rem 0.0625rem var(--base-shadow),
|
||||
0.0625rem -0.0625rem 0.0625rem var(--base-shadow),
|
||||
-0.0625rem 0.0625rem 0.0625rem var(--base-shadow),
|
||||
0.0625rem 0.0625rem 0.0625rem var(--base-shadow),
|
||||
0 -0.0625rem 0.0625rem var(--base-shadow),
|
||||
0 0.0625rem 0.0625rem var(--base-shadow),
|
||||
-0.0625rem 0 0.0625rem var(--base-shadow),
|
||||
0.0625rem 0 0.0625rem var(--base-shadow);
|
||||
}
|
||||
> .comment {
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
min-height: 2.5rem;
|
||||
flex: 1 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-shadow:
|
||||
-0.0625rem -0.0625rem 0.0625rem var(--base-shadow),
|
||||
0.0625rem -0.0625rem 0.0625rem var(--base-shadow),
|
||||
-0.0625rem 0.0625rem 0.0625rem var(--base-shadow),
|
||||
0.0625rem 0.0625rem 0.0625rem var(--base-shadow),
|
||||
0 -0.0625rem 0.0625rem var(--base-shadow),
|
||||
0 0.0625rem 0.0625rem var(--base-shadow),
|
||||
-0.0625rem 0 0.0625rem var(--base-shadow),
|
||||
0.0625rem 0 0.0625rem var(--base-shadow);
|
||||
}
|
||||
}
|
||||
> .group-1 {
|
||||
flex: 1 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
border: 0.0625rem solid var(--base-text-color);
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
> .person {
|
||||
margin: 0 0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
border: 0.0625rem solid var(--style-color);
|
||||
box-sizing: border-box;
|
||||
> .comment {
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
min-height: 2.5rem;
|
||||
flex: 1 0;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
> .group-2 {
|
||||
flex: 3 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
border: 0.0625rem solid var(--base-text-color);
|
||||
> .person {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-wrap: wrap;
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
margin-right: 0.0625rem;
|
||||
> .name { justify-content: center; }
|
||||
> .comment {display: none;}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useGoHome } from '@remake/hooks'
|
||||
import { specialThanks, SpecialThanksGroup as Group } from '@remake/data'
|
||||
import type { SpecialThanks } from '@remake/data'
|
||||
import { shuffle } from '@remake/vitex'
|
||||
import './Thanks.css'
|
||||
|
||||
const groups = Map.groupBy(specialThanks, item => item.group)
|
||||
|
||||
export function Person({ item }: { item: SpecialThanks }) {
|
||||
return (
|
||||
<li
|
||||
className="person"
|
||||
style={
|
||||
{
|
||||
'--custom-color': item.color,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<span className="name">{item.name}</span>
|
||||
{item.comment && <span className="comment">{item.comment}</span>}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export function Thanks() {
|
||||
const goHome = useGoHome()
|
||||
const group1 = shuffle(groups.get(Group.G1) ?? [])
|
||||
const group2 = shuffle(groups.get(Group.G2) ?? [])
|
||||
return (
|
||||
<div className="screen thanks">
|
||||
<button className="primary" onClick={goHome}>
|
||||
返回
|
||||
</button>
|
||||
<ul className="group group-1 hide-scrollbar">
|
||||
{group1.map((item, index) => (
|
||||
<Person key={index} item={item} />
|
||||
))}
|
||||
</ul>
|
||||
<ul className="group group-2 hide-scrollbar">
|
||||
{group2.map((item, index) => (
|
||||
<Person key={index} item={item} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Thanks
|
||||
@@ -2,6 +2,7 @@
|
||||
color-scheme: light dark;
|
||||
--base-text-color: light-dark(#121314, #f9fafb);
|
||||
--base-background-color: light-dark(#f7f7f7, #121314);
|
||||
--base-shadow: light-dark(#00000010, #23456710);
|
||||
|
||||
--color-grade-0: light-dark(#4e5052, #f9fafb);
|
||||
--color-grade-1: light-dark(#4aa8da, #afe3ff);
|
||||
@@ -12,6 +13,7 @@
|
||||
--color-info: light-dark(#454ad3, #b5b7ff);
|
||||
--color-error: light-dark(#e42b2b, #ff6969);
|
||||
--color-primary: var(--color-success);
|
||||
--color-secondary: var(--color-info);
|
||||
}
|
||||
|
||||
.grade-0 { --grade-color: var(--color-grade-0); }
|
||||
|
||||
@@ -68,20 +68,19 @@ button {
|
||||
outline: none;
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
border: var(--style-background) 0.0625rem solid;
|
||||
background: var(--style-color);
|
||||
color: var(--style-background);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: var(--style-background) 0.0625rem solid;
|
||||
background: var(--style-color);
|
||||
color: var(--style-background);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&.primary { --style-color: var(--color-primary); }
|
||||
&.secondary { --style-color: var(--color-secondary); }
|
||||
&.info { --style-color: var(--color-info); }
|
||||
&.error { --style-color: var(--color-error); }
|
||||
&.focus {
|
||||
|
||||
@@ -82,24 +82,17 @@ export function charaPropToBaseAlloc(props: CharacterProperty): BaseAlloc {
|
||||
}
|
||||
}
|
||||
|
||||
function charaPropToAlloc(
|
||||
props: CharacterProperty,
|
||||
spirit: number,
|
||||
): Allocation {
|
||||
return { ...charaPropToBaseAlloc(props), spirit }
|
||||
}
|
||||
|
||||
function convert(chara: BaseChara, spirit: number) {
|
||||
function convert(chara: BaseChara) {
|
||||
return {
|
||||
allocation: charaPropToAlloc(chara.property, spirit),
|
||||
allocation: charaPropToBaseAlloc(chara.property),
|
||||
talents: chara.talent,
|
||||
}
|
||||
}
|
||||
|
||||
export function startChara(id: Character['id'], spirit: number) {
|
||||
return convert(characters.get(id)!, spirit)
|
||||
export function startChara(id: Character['id']) {
|
||||
return convert(characters.get(id)!)
|
||||
}
|
||||
|
||||
export function startUnique(chara: BaseChara, spirit: number) {
|
||||
return convert(chara, spirit)
|
||||
export function startUnique(chara: BaseChara) {
|
||||
return convert(chara)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/** 特别鸣谢分组 */
|
||||
export type SpecialThanksGroup = 1 | 2
|
||||
export enum SpecialThanksGroup {
|
||||
G1 = 1,
|
||||
G2 = 2,
|
||||
}
|
||||
|
||||
/** 特别鸣谢 */
|
||||
export type SpecialThanks = {
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
+59
-22
@@ -1,8 +1,9 @@
|
||||
import { useCallback } from 'react'
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useConfig, useReplaced } from '.'
|
||||
import { atom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useConfig, useReplaced, useIsClassic } from '.'
|
||||
import type { Allocation } from '@remake/core'
|
||||
import { keys, shuffle, random as frandom, type RNG } from '@remake/vitex'
|
||||
import { keys, shuffle, zoneFit, random } from '@remake/vitex'
|
||||
import type { RNG } from '@remake/vitex'
|
||||
|
||||
export type UserAllocation = Omit<Allocation, 'spirit'>
|
||||
const init: UserAllocation = {
|
||||
@@ -11,50 +12,85 @@ const init: UserAllocation = {
|
||||
strength: 0,
|
||||
money: 0,
|
||||
}
|
||||
export const baseAllocAtom = atom<UserAllocation>({ ...init })
|
||||
export const allocAtom = atom<UserAllocation>({ ...init })
|
||||
|
||||
const alloced = (alloc: UserAllocation) =>
|
||||
Object.values(alloc).reduce((a, b) => a + b, 0)
|
||||
|
||||
export const useAllocReset = () => {
|
||||
const setBaseAlloc = useSetAtom(baseAllocAtom)
|
||||
const setAlloc = useSetAtom(allocAtom)
|
||||
return useCallback(() => setAlloc({ ...init }), [setAlloc])
|
||||
return useCallback(() => {
|
||||
setBaseAlloc({ ...init })
|
||||
setAlloc({ ...init })
|
||||
}, [setBaseAlloc, setAlloc])
|
||||
}
|
||||
export const useBaseAlloc = () => useAtomValue(baseAllocAtom)
|
||||
export const useAlloc = () => {
|
||||
const base = useAtomValue(baseAllocAtom)
|
||||
const alloc = useAtomValue(allocAtom)
|
||||
const final = {
|
||||
charm: base.charm + alloc.charm,
|
||||
intelligence: base.intelligence + alloc.intelligence,
|
||||
strength: base.strength + alloc.strength,
|
||||
money: base.money + alloc.money,
|
||||
}
|
||||
return { base, alloc, final }
|
||||
}
|
||||
export const useAlloc = () => useAtomValue(allocAtom)
|
||||
export const usePoints = () => {
|
||||
const { points } = useConfig()
|
||||
const isClassic = useIsClassic()
|
||||
const { additionalPoints } = useReplaced()
|
||||
return Math.max(points + additionalPoints.points, 0)
|
||||
const additional = additionalPoints.points
|
||||
const ba = useAtomValue(baseAllocAtom)
|
||||
const { points } = useConfig()
|
||||
const base = isClassic ? points : alloced(ba)
|
||||
const total = isClassic ? points + additional : additional
|
||||
return { base, total, additional }
|
||||
}
|
||||
|
||||
export const useLeftPoints = () => {
|
||||
const points = usePoints()
|
||||
const alloc = useAtomValue(allocAtom)
|
||||
return points - alloced(alloc)
|
||||
const ca = alloced(alloc)
|
||||
const left = points.total - ca
|
||||
return { ...points, left, alloced: ca }
|
||||
}
|
||||
|
||||
export const useAllocBefore = () => {
|
||||
const { allocate } = useConfig()
|
||||
const { total } = usePoints()
|
||||
const isClassic = useIsClassic()
|
||||
const isPositive = total >= 0
|
||||
const limit = isClassic
|
||||
? allocate
|
||||
: Math.min(Math.ceil(Math.abs(total) / 2), allocate) *
|
||||
(isPositive ? 1 : -1)
|
||||
return { total, limit, isPositive, isClassic }
|
||||
}
|
||||
|
||||
export const useAllocator = () => {
|
||||
const { allocate } = useConfig()
|
||||
const total = usePoints()
|
||||
const [alloc, setAlloc] = useAtom(allocAtom)
|
||||
const { total, limit } = useAllocBefore()
|
||||
const alloc = useAlloc()
|
||||
const setAlloc = useSetAtom(allocAtom)
|
||||
const allocator = useCallback(
|
||||
(key: keyof UserAllocation, value: number) => {
|
||||
setAlloc(prev => {
|
||||
const left = total - alloced(prev) + prev[key]
|
||||
const max = Math.min(left, allocate)
|
||||
const final = Math.max(Math.min(max, value), 0)
|
||||
if (prev[key] === final) return prev
|
||||
const last = prev[key]
|
||||
const left = total - alloced(prev) + last
|
||||
const max = zoneFit(left, [0, limit])
|
||||
const final = zoneFit(value, [0, max])
|
||||
if (last === final) return prev
|
||||
return { ...prev, [key]: final }
|
||||
})
|
||||
},
|
||||
[allocate, total, setAlloc],
|
||||
[limit, total, setAlloc],
|
||||
)
|
||||
return [alloc, allocator] as const
|
||||
}
|
||||
|
||||
export const usePointRandomizer = () => {
|
||||
const { allocate } = useConfig()
|
||||
const total = usePoints()
|
||||
const { total, limit } = useAllocBefore()
|
||||
const setAlloc = useSetAtom(allocAtom)
|
||||
return useCallback(
|
||||
(rng?: RNG) => {
|
||||
@@ -63,15 +99,16 @@ export const usePointRandomizer = () => {
|
||||
const alloc = { ...init }
|
||||
while (suffled.length) {
|
||||
const key = suffled.pop()!
|
||||
const max = Math.min(allocate, left)
|
||||
const min = Math.max(0, left - suffled.length * allocate)
|
||||
const value = frandom(max, min, rng)
|
||||
const n = suffled.length
|
||||
const max = zoneFit(limit, [0, left])
|
||||
const min = zoneFit(left - n * limit, [0, limit])
|
||||
const value = random(max, min, rng)
|
||||
alloc[key] = value
|
||||
left -= value
|
||||
}
|
||||
setAlloc(alloc)
|
||||
},
|
||||
[allocate, total, setAlloc],
|
||||
[total, limit, setAlloc],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useConfig, useProfile, useSetStep, useSetGameState, Step } from '.'
|
||||
import { pickedAtom, replacedAtom, baseAllocAtom } from '.'
|
||||
import { useConfig, useSetStep, Step } from '.'
|
||||
import { uniqueGenerate, startUnique, startChara } from '@remake/core'
|
||||
import { pullChara, start, pick } from '@remake/core'
|
||||
import { pullChara, pick } from '@remake/core'
|
||||
import type { BaseChara, PullCharaTms } from '@remake/core'
|
||||
import type { Character } from '@remake/data'
|
||||
import type { RNG } from '@remake/vitex'
|
||||
@@ -70,34 +71,28 @@ export const useCharaPicker = () => {
|
||||
return [picked, { chara, unique: uni }] as const
|
||||
}
|
||||
|
||||
export const useCharaStart = () => {
|
||||
const { spirit } = useConfig()
|
||||
const [profile] = useProfile()
|
||||
export const useCharaSubmit = () => {
|
||||
const unique = useAtomValue(uniqueAtom)
|
||||
const setStep = useSetStep()
|
||||
const setState = useSetGameState()
|
||||
const setPicked = useSetAtom(pickedAtom)
|
||||
const setReplaced = useSetAtom(replacedAtom)
|
||||
const setBaseAlloc = useSetAtom(baseAllocAtom)
|
||||
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)
|
||||
result = startUnique(unique)
|
||||
} else {
|
||||
result = startChara(picked.id, spirit)
|
||||
result = startChara(picked.id)
|
||||
}
|
||||
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
|
||||
setPicked(new Set(result.talents))
|
||||
setReplaced(pick(result.talents, rng))
|
||||
setBaseAlloc(result.allocation)
|
||||
setStep(Step.Alloc)
|
||||
},
|
||||
[spirit, profile, unique, setStep, setState],
|
||||
[unique, setPicked, setReplaced, setBaseAlloc, setStep],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useCallback } from 'react'
|
||||
import { atom, useSetAtom, useAtomValue } from 'jotai'
|
||||
import type { PullOptions, PullCharaOpt, UniqueGenCfg } from '@remake/core'
|
||||
export interface Config {
|
||||
/** 功能锁 */
|
||||
features: number
|
||||
/** 游戏锁定天赋数量 */
|
||||
lock: number
|
||||
/** 游戏可选天赋数量 */
|
||||
|
||||
@@ -4,4 +4,5 @@ export * from './talent'
|
||||
export * from './play'
|
||||
export * from './alloc'
|
||||
export * from './character'
|
||||
export * from './achievement'
|
||||
export type * from '@remake/core'
|
||||
|
||||
@@ -14,6 +14,8 @@ export enum Step {
|
||||
Alloc = 'alloc',
|
||||
Play = 'play',
|
||||
Summary = 'summary',
|
||||
Achv = 'achv',
|
||||
Thanks = 'thanks',
|
||||
}
|
||||
|
||||
export enum Mode {
|
||||
@@ -52,6 +54,8 @@ export const useGameReset = () => {
|
||||
}, [resetTalent, resetAlloc, resetState, resetLogs])
|
||||
}
|
||||
|
||||
export const useMode = () => useAtomValue(modeAtom)
|
||||
export const useIsClassic = () => useAtomValue(modeAtom) === Mode.Classic
|
||||
export const useStep = () => useAtomValue(stepAtom)
|
||||
export const useSetStep = () => useSetAtom(stepAtom)
|
||||
export const useGameState = () => useAtomValue(gameStateAtom)
|
||||
@@ -82,7 +86,7 @@ export const useModeChoose = () => {
|
||||
case Mode.Celebrity: return setStep(Step.Chara)
|
||||
}
|
||||
},
|
||||
[setStep],
|
||||
[setMode, setStep],
|
||||
)
|
||||
return [Mode, choose] as const
|
||||
}
|
||||
@@ -92,7 +96,7 @@ export const useStart = () => {
|
||||
const [profile] = useProfile()
|
||||
const setStep = useSetAtom(stepAtom)
|
||||
const setState = useSetAtom(gameStateAtom)
|
||||
const allocate = useAlloc()
|
||||
const { final: allocate } = useAlloc()
|
||||
const { talents: tr } = useReplaced()
|
||||
return useCallback(() => {
|
||||
const alloc = { ...allocate, spirit }
|
||||
@@ -148,10 +152,12 @@ export const useEnd = () => {
|
||||
const setStep = useSetAtom(stepAtom)
|
||||
const state = useAtomValue(gameStateAtom)
|
||||
const reset = useGameReset()
|
||||
const isClassic = useIsClassic()
|
||||
const [locked, setLocked] = useState<Set<Talent['id']>>(new Set())
|
||||
const picker = useCallback(
|
||||
(talent: Talent['id']) => {
|
||||
setLocked(prev => {
|
||||
if (!isClassic) return prev
|
||||
if (prev.has(talent)) {
|
||||
const next = new Set(prev)
|
||||
next.delete(talent)
|
||||
@@ -164,17 +170,37 @@ export const useEnd = () => {
|
||||
return next
|
||||
})
|
||||
},
|
||||
[lock, setLocked],
|
||||
[lock, isClassic, setLocked],
|
||||
)
|
||||
const ender = useCallback(() => {
|
||||
if (!state)
|
||||
throw new Error('Game state is not available or already ended.')
|
||||
const l = locked.size > 0 ? Array.from(locked) : undefined
|
||||
const l = isClassic
|
||||
? locked.size > 0
|
||||
? Array.from(locked)
|
||||
: undefined
|
||||
: profile.locked
|
||||
const result = end(state, profile, l)
|
||||
setStep(Step.Idle)
|
||||
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])
|
||||
}
|
||||
|
||||
export const useGoThanks = () => {
|
||||
const setStep = useSetAtom(stepAtom)
|
||||
return useCallback(() => setStep(Step.Thanks), [setStep])
|
||||
}
|
||||
|
||||
export const useFeatures = () => {
|
||||
const { features } = useConfig()
|
||||
const [{ times }] = useProfile()
|
||||
return times >= features
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type RNG = (max?: number, min?: number) => number
|
||||
export function random(max: number = 1, min: number = 0, rng?: RNG): number {
|
||||
if (rng) return rng(max, min)
|
||||
if (max < min) [max, min] = [min, max]
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
export function pick<T>(items: T[], rng?: RNG) {
|
||||
@@ -58,3 +59,11 @@ export function format(str: string, getValueFn: GetValueFn): string {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function zoneFit(value: number, zone: [number, number]) {
|
||||
const max = Math.max(zone[0], zone[1])
|
||||
if (value > max) return max
|
||||
const min = Math.min(zone[0], zone[1])
|
||||
if (value < min) return min
|
||||
return value
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user