mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-27 08:46:48 +08:00
feature: multiple lock, pick min max
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
"author": "Vick Scarlet <vick@syaro.io>",
|
||||
"scripts": {
|
||||
"dev": "bunx --bun vite",
|
||||
"dev:prod": "bunx --bun vite --mode production",
|
||||
"build": "bunx --bun vite build",
|
||||
"preview": "bunx --bun vite preview",
|
||||
"lint": "bunx --bun eslint",
|
||||
@@ -14,7 +15,6 @@
|
||||
"@remake/data": "workspace:*",
|
||||
"@remake/hooks": "workspace:*",
|
||||
"@remake/vitex": "workspace:*",
|
||||
"classnames": "^2.5.1",
|
||||
"jotai": "^2.20.2",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
|
||||
+32
-8
@@ -1,20 +1,33 @@
|
||||
import type { Config } from '@remake/hooks'
|
||||
|
||||
export const isDev = import.meta.env.MODE === 'development'
|
||||
// 自动模式自动点击间隔
|
||||
export const AutoInterval = 500
|
||||
// 可锁定的天赋数量
|
||||
export const LockLimit = isDev ? 10 : 1
|
||||
// 名人模式重开次数限制
|
||||
export const ModeLimit = 10
|
||||
export const PickLimit = 3
|
||||
// 天赋选择数量限制
|
||||
export const PickMax = isDev ? 10 : 3
|
||||
// 天赋选择最少数量限制
|
||||
export const PickMin = 3
|
||||
// 基础属性点
|
||||
export const BasePoints = 20
|
||||
// 单项属性点最大限制
|
||||
export const AllocLimit = 10
|
||||
// 默认初始快乐
|
||||
export const DefaultSpirit = 5
|
||||
export const PullCount = 10
|
||||
// 天赋抽取个数
|
||||
export const PullCount = isDev ? 20 : 10
|
||||
// 天赋抽取基础概率
|
||||
export const PullRateBase: Config['pull']['rate']['base'] = new Map([
|
||||
[0, 889],
|
||||
[1, 100],
|
||||
[2, 10],
|
||||
[3, 1],
|
||||
])
|
||||
|
||||
export const JudgeMap = {
|
||||
// 属性评级线
|
||||
export const JudgeLineMap = {
|
||||
age: [0, 1, 10, 18, 40, 60, 70, 80, 90, 95, 100, 500],
|
||||
summary: [0, 41, 50, 60, 80, 100, 110, 120],
|
||||
times: [0, 10, 30, 50, 70, 100],
|
||||
@@ -27,15 +40,17 @@ export const JudgeMap = {
|
||||
talent: [0, 0.3, 0.6, 0.9],
|
||||
event: [0, 0.2, 0.4, 0.6],
|
||||
}
|
||||
export type Judges = keyof typeof JudgeMap
|
||||
// 可评级属性
|
||||
export type Judges = keyof typeof JudgeLineMap
|
||||
// 获取属性评级
|
||||
export function judge(key: Judges, value: number) {
|
||||
const arr = JudgeMap[key]
|
||||
const arr = JudgeLineMap[key]
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (value >= arr[i]!) return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 属性稀有度等级线
|
||||
export const JudgeGradeMap = {
|
||||
age: [0, 5, 7, 9],
|
||||
summary: [0, 4, 5, 6],
|
||||
@@ -49,6 +64,7 @@ export const JudgeGradeMap = {
|
||||
talent: [0, 1, 2, 3],
|
||||
event: [0, 1, 2, 3],
|
||||
}
|
||||
// 获取属性稀有度
|
||||
export function judgeGrade(key: Judges, level: number) {
|
||||
const arr = JudgeGradeMap[key]
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
@@ -56,7 +72,13 @@ export function judgeGrade(key: Judges, level: number) {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
// 根据属性值获取属性稀有度
|
||||
export function judgeGradeByValue(key: Judges, value: number) {
|
||||
const level = judge(key, value)
|
||||
return judgeGrade(key, level)
|
||||
}
|
||||
|
||||
// 天赋抽取概率加成
|
||||
export const PullRateAdditions: Config['pull']['rate']['additions'] = {
|
||||
times: value => {
|
||||
const level = judge('times', value)
|
||||
@@ -69,8 +91,10 @@ export const PullRateAdditions: Config['pull']['rate']['additions'] = {
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
lock: LockLimit,
|
||||
mode: ModeLimit,
|
||||
pick: PickLimit,
|
||||
max: PickMax,
|
||||
min: PickMin,
|
||||
points: BasePoints,
|
||||
allocate: AllocLimit,
|
||||
spirit: DefaultSpirit,
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
display: flex;
|
||||
gap: 0;
|
||||
height: 2rem;
|
||||
border: 0.0625rem solid var(--base-text-color);
|
||||
border: 0.0625rem solid var(--grade-color);
|
||||
color: var(--grade-color);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
transition: all 0.2s ease-in-out;
|
||||
button {
|
||||
--style-color: var(--grade-color);
|
||||
flex: 1 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -25,6 +28,8 @@
|
||||
height: 100%;
|
||||
width: 2rem;
|
||||
font-weight: bold;
|
||||
color: var(--grade-color);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
.points-detail {
|
||||
@@ -73,23 +78,27 @@
|
||||
> span {
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
border: 0.0625rem solid var(--base-text-color);
|
||||
color: var(--base-background-color);
|
||||
background: var(--grade-color);
|
||||
border: 0.0625rem solid var(--grade-color);
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
font-weight: bold;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
> li.left {
|
||||
--left-color: var(--color-error);
|
||||
&.left-0 { --left-color: var(--color-primary); }
|
||||
> span {
|
||||
background: var(--color-primary);
|
||||
color: var(--base-background-color);
|
||||
border: 0.0625rem solid var(--color-primary);
|
||||
border-bottom: none;
|
||||
background: var(--left-color);
|
||||
border: 0.0625rem solid var(--left-color);
|
||||
}
|
||||
> button {
|
||||
--style-color: var(--left-color);
|
||||
flex: 1;
|
||||
font-size: 1rem;
|
||||
height: 2rem;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { usePicked, useReplaced, useStart } from '@remake/hooks'
|
||||
import type { AdditionalPoint } from '@remake/hooks'
|
||||
import { properties } from '@/display'
|
||||
import { keys } from '@remake/vitex'
|
||||
import { BasePoints } from '@/config'
|
||||
import { BasePoints, judgeGradeByValue } from '@/config'
|
||||
import { talents } from '@remake/data'
|
||||
import Replaced from '@/components/Replaced'
|
||||
import './Alloc.css'
|
||||
@@ -62,6 +62,10 @@ export function Alloc() {
|
||||
const start = useStart()
|
||||
const [showDetail, setShowDetail] = useState(false)
|
||||
const handleNext = () => {
|
||||
if (left) {
|
||||
// TODO: Show a warning that there are still points left
|
||||
return
|
||||
}
|
||||
const achievements = start()
|
||||
// TODO: Show achievements
|
||||
console.log('Achievements', achievements)
|
||||
@@ -76,10 +80,10 @@ export function Alloc() {
|
||||
))}
|
||||
</ul>
|
||||
<ul className="alloc">
|
||||
<li className="left">
|
||||
<li className={`left left-${left}`}>
|
||||
<span>剩余点数</span>
|
||||
<button
|
||||
className="primary font-mono"
|
||||
className="font-mono"
|
||||
onClick={() => setShowDetail(!showDetail)}
|
||||
>
|
||||
{left}
|
||||
@@ -87,7 +91,10 @@ export function Alloc() {
|
||||
{showDetail && <PointsDetail source={source} />}
|
||||
</li>
|
||||
{keys(allocation).map(key => (
|
||||
<li key={key}>
|
||||
<li
|
||||
key={key}
|
||||
className={`property grade-${judgeGradeByValue(key, allocation[key])}`}
|
||||
>
|
||||
<span>{properties[key]}</span>
|
||||
<AllocInput
|
||||
point={allocation[key]}
|
||||
@@ -100,7 +107,10 @@ export function Alloc() {
|
||||
<button className="info" onClick={() => random()}>
|
||||
随机分配
|
||||
</button>
|
||||
<button className="primary" onClick={handleNext}>
|
||||
<button
|
||||
className={left ? 'error' : 'primary'}
|
||||
onClick={handleNext}
|
||||
>
|
||||
开始新人生
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -15,21 +15,21 @@
|
||||
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(--base-text-color);
|
||||
border: 0.0625rem solid var(--grade-color);
|
||||
box-sizing: border-box;
|
||||
height: 2rem;
|
||||
&:first-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
.value {
|
||||
color: var(--color-primary);
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
.name {
|
||||
color: var(--base-background-color);
|
||||
background-color: var(--grade-color);
|
||||
}
|
||||
&.charm { order: 1 }
|
||||
&.intelligence { order: 2 }
|
||||
@@ -111,4 +111,55 @@
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.screen.play > ul.properties > li {
|
||||
.value {
|
||||
position: relative;
|
||||
background-color: var(--base-background-color);
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
&[class*="trend-up"] .value::after,
|
||||
&[class*="trend-down"] .value::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-style: solid;
|
||||
animation: triangle-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||
}
|
||||
&[class*="trend-up"] .value::after {
|
||||
border-width: 0 0.3125rem 0.375rem 0.3125rem;
|
||||
border-color: transparent transparent var(--color-success) transparent;
|
||||
}
|
||||
&[class*="trend-down"] .value::after {
|
||||
border-width: 0.375rem 0.3125rem 0 0.3125rem;
|
||||
border-color: var(--color-error) transparent transparent transparent;
|
||||
}
|
||||
&[class*="trend-up-1"] .value { animation: flash-success-odd 0.5s ease-out forwards; }
|
||||
&[class*="trend-up-0"] .value { animation: flash-success-even 0.5s ease-out forwards; }
|
||||
&[class*="trend-down-1"] .value { animation: flash-error-odd 0.5s ease-out forwards; }
|
||||
&[class*="trend-down-0"] .value { animation: flash-error-even 0.5s ease-out forwards; }
|
||||
}
|
||||
|
||||
@keyframes flash-success-odd {
|
||||
0% { background-color: color-mix(in srgb, var(--color-success) 30%, var(--base-background-color)); }
|
||||
100% { background-color: var(--base-background-color); }
|
||||
}
|
||||
@keyframes flash-success-even {
|
||||
0% { background-color: color-mix(in srgb, var(--color-success) 30%, var(--base-background-color)); }
|
||||
100% { background-color: var(--base-background-color); }
|
||||
}
|
||||
@keyframes flash-error-odd {
|
||||
0% { background-color: color-mix(in srgb, var(--color-error) 30%, var(--base-background-color)); }
|
||||
100% { background-color: var(--base-background-color); }
|
||||
}
|
||||
@keyframes flash-error-even {
|
||||
0% { background-color: color-mix(in srgb, var(--color-error) 30%, var(--base-background-color)); }
|
||||
100% { background-color: var(--base-background-color); }
|
||||
}
|
||||
@keyframes triangle-pop {
|
||||
0% { opacity: 0; transform: translateY(-50%) scale(0.5); }
|
||||
100% { opacity: 1; transform: translateY(-50%) scale(1); }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { useLayoutEffect, useEffect } from 'react'
|
||||
import { useNext, useGotoSummary } from '@remake/hooks'
|
||||
import { useJudge } from '@/hooks/judge'
|
||||
import type { Log } from '@remake/hooks'
|
||||
import { properties } from '@/display'
|
||||
import { keys } from '@remake/vitex'
|
||||
import { achievements, events, talents } from '@remake/data'
|
||||
import { AutoInterval } from '@/config'
|
||||
import './Play.css'
|
||||
@@ -66,10 +66,7 @@ function LogAchievements({ items }: { items: number[] }) {
|
||||
return <ul className="log-inner log-achievements">{els}</ul>
|
||||
}
|
||||
|
||||
interface LogProps {
|
||||
log: Log
|
||||
}
|
||||
function Log({ log }: LogProps) {
|
||||
function Log({ log }: { log: Log }) {
|
||||
return (
|
||||
<li className="log">
|
||||
<span className="age font-mono">{log.age}岁</span>
|
||||
@@ -82,8 +79,51 @@ function Log({ log }: LogProps) {
|
||||
)
|
||||
}
|
||||
|
||||
interface PropProps {
|
||||
prop: keyof typeof properties
|
||||
value: number
|
||||
grade: number
|
||||
}
|
||||
|
||||
function Prop({ prop, value, grade }: PropProps) {
|
||||
const prevRef = useRef<number>(value)
|
||||
const [trend, setTrend] = useState<'up' | 'down' | 'normal'>('normal')
|
||||
const [animCount, setAnimCount] = useState<number>(0)
|
||||
useEffect(() => {
|
||||
const prev = prevRef.current
|
||||
if (value === prev) return
|
||||
prevRef.current = value
|
||||
setTrend(value > prev ? 'up' : 'down')
|
||||
setAnimCount(c => c + 1)
|
||||
const timer = setTimeout(() => {
|
||||
setTrend('normal')
|
||||
}, 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<li
|
||||
className={`${prop} grade-${grade} trend-${trend}-${animCount % 2}`}
|
||||
>
|
||||
<span className="name">{properties[prop]}</span>
|
||||
<span className="value font-mono">{value}</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function Properties() {
|
||||
const judges = useJudge()
|
||||
return (
|
||||
<ul className="properties">
|
||||
{judges.map(([key, { value, grade }]) => (
|
||||
<Prop key={key} prop={key} value={value} grade={grade} />
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export function Play() {
|
||||
const [{ state, logs, ended }, next] = useNext()
|
||||
const [{ logs, ended }, next] = useNext()
|
||||
const [auto, setAuto] = useState(false)
|
||||
const logRef = useRef<HTMLUListElement>(null)
|
||||
const autoRef = useRef(0)
|
||||
@@ -113,16 +153,7 @@ export function Play() {
|
||||
}, [auto, handleNext])
|
||||
return (
|
||||
<div className="screen play">
|
||||
<ul className="properties">
|
||||
{keys(state.props.current, ['age']).map(key => (
|
||||
<li className={key} key={key}>
|
||||
<span>{properties[key]}</span>
|
||||
<span className="font-mono">
|
||||
{state.props.current[key]}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Properties />
|
||||
<ul
|
||||
className="logs hide-scrollbar"
|
||||
onClick={handleNext}
|
||||
@@ -135,7 +166,7 @@ export function Play() {
|
||||
<div className="controls">
|
||||
{!ended && (
|
||||
<button className="primary" onClick={() => setAuto(!auto)}>
|
||||
{auto ? '手动' : '自动'}
|
||||
{auto ? '关闭自动' : '开启自动'}
|
||||
</button>
|
||||
)}
|
||||
{ended && (
|
||||
|
||||
@@ -1,35 +1,65 @@
|
||||
import { useState } from 'react'
|
||||
import { usePicked, useEnd } from '@remake/hooks'
|
||||
import { useJudge } from '@/hooks/judge'
|
||||
import type { JudgeKeys, Judge } from '@/hooks/judge'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { usePicked, useEnd, useProfile } from '@remake/hooks'
|
||||
import { useEndJudge } from '@/hooks/judge'
|
||||
import { properties, judgeDisplay } from '@/display'
|
||||
import { isDev } from '@/config'
|
||||
import TalentComponent from '@/components/Talent'
|
||||
import './Summary.css'
|
||||
|
||||
interface JudgeItemProps {
|
||||
prop: JudgeKeys
|
||||
judge: Judge
|
||||
}
|
||||
function JudgeItem({ prop, judge }: JudgeItemProps) {
|
||||
const { value, grade, level } = judge
|
||||
function Judges() {
|
||||
const judges = useEndJudge()
|
||||
return (
|
||||
<li className={`${prop} grade-${grade}`}>
|
||||
<span className="property">{properties[prop]}</span>
|
||||
<span className="value font-mono">{value}</span>
|
||||
<span className="level">{judgeDisplay(prop, level)}</span>
|
||||
</li>
|
||||
<ul className="judge-list">
|
||||
{judges.map(([key, { value, grade, level }]) => (
|
||||
<li className={`${key} grade-${grade}`} key={key}>
|
||||
<span className="property">{properties[key]}</span>
|
||||
<span className="value font-mono">{value}</span>
|
||||
<span className="level">{judgeDisplay(key, level)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
interface TalentListProps {
|
||||
picked: Set<number>
|
||||
picker: (id: number) => void
|
||||
}
|
||||
function TalentList({ picked, picker }: TalentListProps) {
|
||||
const [profile] = useProfile()
|
||||
const [talents, setTalents] = useState(usePicked())
|
||||
const hasInitialized = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!profile.locked) return
|
||||
let currentTalents = talents
|
||||
if (isDev) {
|
||||
currentTalents = new Set([...talents, ...profile.locked])
|
||||
setTalents(currentTalents)
|
||||
}
|
||||
|
||||
if (!hasInitialized.current) {
|
||||
for (const id of profile.locked) {
|
||||
if (currentTalents.has(id) && !picked.has(id)) {
|
||||
picker(id)
|
||||
}
|
||||
}
|
||||
hasInitialized.current = true
|
||||
}
|
||||
}, [profile, talents, picked, picker])
|
||||
return (
|
||||
<ul className="talent-list">
|
||||
{Array.from(talents, id => (
|
||||
<li key={id} onClick={() => picker(id)}>
|
||||
<TalentComponent id={id} selected={picked.has(id)} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Summary() {
|
||||
const picked = usePicked()
|
||||
const judges = useJudge()
|
||||
const end = useEnd()
|
||||
const [locked, setLocked] = useState<number | null>(null)
|
||||
const handleSelect = (id: number) => {
|
||||
if (locked === id) setLocked(null)
|
||||
else setLocked(id)
|
||||
}
|
||||
const [locked, picker, end] = useEnd()
|
||||
const handleEnd = () => {
|
||||
const achievements = end()
|
||||
// TODO: Show achievements
|
||||
@@ -37,24 +67,10 @@ export default function Summary() {
|
||||
}
|
||||
return (
|
||||
<div className="screen summary">
|
||||
<ul className="judge-list">
|
||||
{judges.map(([key, { value, grade, level }]) => (
|
||||
<JudgeItem
|
||||
key={key}
|
||||
prop={key}
|
||||
judge={{ value, grade, level }}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<Judges />
|
||||
<div className="section">
|
||||
<div className="title">你可以锁定一个天赋,下辈子还能抽到</div>
|
||||
<ul className="talent-list">
|
||||
{Array.from(picked, id => (
|
||||
<li key={id} onClick={() => handleSelect(id)}>
|
||||
<TalentComponent id={id} selected={locked === id} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<TalentList picked={locked} picker={picker} />
|
||||
</div>
|
||||
<button className="primary" onClick={handleEnd}>
|
||||
再次重开
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
// import { useEffect } from 'react'
|
||||
import { useTalentPuller, useTalentPicker } from '@remake/hooks'
|
||||
import { useTalentSubmit, useSubmitIsEnable } from '@remake/hooks'
|
||||
import { PullCount } from '@/config'
|
||||
import TalentComponent from '@/components/Talent'
|
||||
import './TalentPick.css'
|
||||
|
||||
export default function TalentPick() {
|
||||
const [pulled, puller] = useTalentPuller()
|
||||
const [talents, picker] = useTalentPicker()
|
||||
const [enabled, limit] = useSubmitIsEnable()
|
||||
const { enabled, min, max } = useSubmitIsEnable()
|
||||
const submit = useTalentSubmit()
|
||||
// useEffect(puller, [])
|
||||
if (!pulled)
|
||||
return (
|
||||
<div className="screen talent-pick">
|
||||
<button className="primary" onClick={() => puller()}>
|
||||
十连抽!
|
||||
<button className="primary font-mono" onClick={() => puller()}>
|
||||
{PullCount}连抽!
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -32,7 +33,9 @@ export default function TalentPick() {
|
||||
下一步
|
||||
</button>
|
||||
) : (
|
||||
<button className="error">请选取 {limit} 个天赋</button>
|
||||
<button className="error">
|
||||
请选取 {min == max ? min : `${min}~${max}`} 个天赋
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
+26
-12
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useSummary, useGameState } from '@remake/hooks'
|
||||
import type { Properties } from '@remake/hooks'
|
||||
import { judge, judgeGrade } from '@/config'
|
||||
import type { Judges } from '@/config'
|
||||
import { keys } from '@remake/vitex'
|
||||
|
||||
export interface Judge {
|
||||
@@ -9,22 +9,36 @@ export interface Judge {
|
||||
level: number
|
||||
grade: number
|
||||
}
|
||||
export type JudgePropKeys = keyof Properties
|
||||
export type JudgeKeys = 'summary' | JudgePropKeys
|
||||
export type Judges = [JudgeKeys, Judge][]
|
||||
function judges<T extends Judges, K extends keyof Record<T, number> = never>(
|
||||
props: Record<T, number>,
|
||||
filter?: K[],
|
||||
) {
|
||||
const judges: [Exclude<T, K>, Judge][] = []
|
||||
for (const key of keys(props, filter)) {
|
||||
const value = props[key]
|
||||
const level = judge(key, value)
|
||||
const grade = judgeGrade(key, level)
|
||||
judges.push([key, { value, level, grade }])
|
||||
}
|
||||
return judges
|
||||
}
|
||||
|
||||
export const useJudge = () => {
|
||||
const state = useGameState()
|
||||
if (!state) throw new Error('Game state is not set')
|
||||
return useMemo(() => {
|
||||
const result = judges(state.props.current, ['age'])
|
||||
return result
|
||||
}, [state.props.current])
|
||||
}
|
||||
|
||||
export const useEndJudge = () => {
|
||||
const summary = useSummary()
|
||||
const state = useGameState()
|
||||
if (!state) throw new Error('Game state is not set')
|
||||
return useMemo(() => {
|
||||
const props = { ...state.props.highest, summary }
|
||||
const judges: Judges = []
|
||||
for (const key of keys(props)) {
|
||||
const value = props[key]
|
||||
const level = judge(key, value)
|
||||
const grade = judgeGrade(key, level)
|
||||
judges.push([key, { value, level, grade }])
|
||||
}
|
||||
return judges
|
||||
const result = judges(props)
|
||||
return result
|
||||
}, [state.props.highest, summary])
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ if (localStorage.getItem('version') !== '3.0.0') {
|
||||
const lockedTalents = JSON.parse(
|
||||
localStorage.getItem('extendTalent') || 'null',
|
||||
)
|
||||
if (lockedTalents) profile.lockedTalent = lockedTalents
|
||||
if (lockedTalents) profile.locked = [lockedTalents]
|
||||
const unique = JSON.parse(
|
||||
localStorage.getItem('uniqueWaTaShi') || 'null',
|
||||
)
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
--color-grade-2: light-dark(#b03fe4, #e09eff);
|
||||
--color-grade-3: light-dark(#ffaf2d, #ffe48d);
|
||||
|
||||
--color-primary: light-dark(#61b700, #b9ff69);
|
||||
--color-success: light-dark(#61b700, #b9ff69);
|
||||
--color-info: light-dark(#454ad3, #b5b7ff);
|
||||
--color-error: light-dark(#e42b2b, #ff6969);
|
||||
--color-primary: var(--color-success);
|
||||
}
|
||||
|
||||
.grade-0 { --grade-color: var(--color-grade-0); }
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
const dataSplitRule = {
|
||||
test: /[\\/]packages[\\/]data[\\/]|@remake\/data/,
|
||||
priority: 40,
|
||||
name(id: string) {
|
||||
const name = id.split(/[\/\\]/).pop()!
|
||||
const baseName = name.substring(0, name.lastIndexOf('.'))
|
||||
if (baseName && baseName !== 'index') return `data-${baseName}`
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: { tsconfigPaths: true },
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
codeSplitting: {
|
||||
minSize: 1024,
|
||||
groups: [dataSplitRule],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
+19
-18
@@ -4,25 +4,26 @@
|
||||
"version": "3.0.0",
|
||||
"author": "Vick Scarlet <vick@syaro.io>",
|
||||
"scripts": {
|
||||
"dev": "bun --filter @remake/web dev",
|
||||
"build:web": "bun --filter @remake/web build",
|
||||
"build:data": "bun --filter @remake/data build",
|
||||
"preview": "bun --filter @remake/web preview",
|
||||
"start": "bun --filter @remake/console start",
|
||||
"dev": "pnpm --filter @remake/web dev",
|
||||
"dev:prod": "pnpm --filter @remake/web dev:prod",
|
||||
"build:web": "pnpm --filter @remake/web build",
|
||||
"build:data": "pnpm --filter @remake/data build",
|
||||
"preview": "pnpm --filter @remake/web preview",
|
||||
"start": "pnpm --filter @remake/console start",
|
||||
"lint": "bunx --bun eslint",
|
||||
"lint:console": "bun --filter @remake/console lint",
|
||||
"lint:web": "bun --filter @remake/web lint",
|
||||
"lint:core": "bun --filter @remake/core lint",
|
||||
"lint:data": "bun --filter @remake/data lint",
|
||||
"lint:condition": "bun --filter @remake/condition lint",
|
||||
"lint:hooks": "bun --filter @remake/hooks lint",
|
||||
"test": "bun --filter @remake/web test",
|
||||
"test:console": "bun --filter @remake/console test",
|
||||
"test:web": "bun --filter @remake/web test",
|
||||
"test:core": "bun --filter @remake/core test",
|
||||
"test:data": "bun --filter @remake/data test",
|
||||
"test:condition": "bun --filter @remake/condition test",
|
||||
"test:hooks": "bun --filter @remake/hooks test"
|
||||
"lint:console": "pnpm --filter @remake/console lint",
|
||||
"lint:web": "pnpm --filter @remake/web lint",
|
||||
"lint:core": "pnpm --filter @remake/core lint",
|
||||
"lint:data": "pnpm --filter @remake/data lint",
|
||||
"lint:condition": "pnpm --filter @remake/condition lint",
|
||||
"lint:hooks": "pnpm --filter @remake/hooks lint",
|
||||
"test": "pnpm --filter @remake/web test",
|
||||
"test:console": "pnpm --filter @remake/console test",
|
||||
"test:web": "pnpm --filter @remake/web test",
|
||||
"test:core": "pnpm --filter @remake/core test",
|
||||
"test:data": "pnpm --filter @remake/data test",
|
||||
"test:condition": "pnpm --filter @remake/condition test",
|
||||
"test:hooks": "pnpm --filter @remake/hooks test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "1.3.14",
|
||||
|
||||
@@ -102,10 +102,10 @@ export interface EndResult {
|
||||
export function end(
|
||||
state: GameState,
|
||||
profile: ProfileState,
|
||||
lockedTalent?: Talent['id'],
|
||||
locked?: Talent['id'][],
|
||||
) {
|
||||
const ar = atr(Ao.End, state, profile)
|
||||
const p = nextProfile(profile, ar.state, lockedTalent)
|
||||
const p = nextProfile(profile, ar.state, locked)
|
||||
return { profile: p, achievements: ar.triggers }
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export type UniqueCharacter = Omit<Character, 'id' | 'name'>
|
||||
/** 持久化存储的数据 */
|
||||
export interface ProfileState {
|
||||
times: number // 游戏次数
|
||||
lockedTalent?: Talent['id'] // 继承的天赋
|
||||
locked?: Talent['id'][] // 锁定的天赋
|
||||
talents: Set<Talent['id']> // 拥有过的天赋
|
||||
events: Set<Event['id']> // 触发过的事件
|
||||
achievements: Set<Achievement['id']> // 达成的成就
|
||||
@@ -195,7 +195,7 @@ export function lowestProperties(a: Properties, b?: Properties): Properties {
|
||||
export function nextProfile(
|
||||
profile: ProfileState,
|
||||
state: GameState,
|
||||
lockedTalent?: Talent['id'],
|
||||
locked?: Talent['id'][],
|
||||
) {
|
||||
return {
|
||||
times: profile.times + 1,
|
||||
@@ -204,6 +204,6 @@ export function nextProfile(
|
||||
achievements: profile.achievements.union(state.achievements),
|
||||
highest: highestProperties(state.props.highest, profile.highest),
|
||||
lowest: lowestProperties(state.props.lowest, profile.lowest),
|
||||
lockedTalent: lockedTalent ?? profile.lockedTalent,
|
||||
locked,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +70,13 @@ export function pull(
|
||||
)
|
||||
const result = []
|
||||
const map = new Map(Grades.map(g => [g, new Set(GradeMap.get(g))]))
|
||||
if (profile.lockedTalent) {
|
||||
result.push(profile.lockedTalent)
|
||||
const { grade } = talents.get(profile.lockedTalent)!
|
||||
map.get(grade)!.delete(profile.lockedTalent)
|
||||
if (profile.locked) {
|
||||
result.push(...profile.locked)
|
||||
if (result.length >= options.count) return result
|
||||
for (const talent of result) {
|
||||
const { grade } = talents.get(talent)!
|
||||
map.get(grade)!.delete(talent)
|
||||
}
|
||||
}
|
||||
for (let i = options.count - result.length; i > 0; i--) {
|
||||
const grade = pickWeight(rate, rng)! ?? 0
|
||||
|
||||
@@ -3,11 +3,21 @@ import { atom, useSetAtom, useAtomValue } from 'jotai'
|
||||
import type { PullOptions } from '@remake/core'
|
||||
|
||||
export interface Config {
|
||||
pick: number
|
||||
/** 游戏锁定天赋数量 */
|
||||
lock: number
|
||||
/** 游戏可选天赋数量 */
|
||||
max: number
|
||||
/** 游戏最少选择天赋数量 */
|
||||
min: number
|
||||
/** 天赋抽取个数 */
|
||||
pull: PullOptions
|
||||
/** 初始属性点 */
|
||||
points: number
|
||||
/** 单项属性点最大限制 */
|
||||
allocate: number
|
||||
/** 初始快乐 */
|
||||
spirit: number
|
||||
/** 模式选择限制 */
|
||||
mode: number
|
||||
}
|
||||
|
||||
|
||||
+27
-11
@@ -101,7 +101,7 @@ export const useNext = () => {
|
||||
}
|
||||
return result.achievements
|
||||
}, [state, profile, setState])
|
||||
return [{ state, logs, ended }, nexter] as const
|
||||
return [{ logs, ended }, nexter] as const
|
||||
}
|
||||
|
||||
export const useGotoSummary = () => {
|
||||
@@ -120,20 +120,36 @@ export const useGotoSummary = () => {
|
||||
}
|
||||
|
||||
export const useEnd = () => {
|
||||
const { lock } = useConfig()
|
||||
const [profile, setProfile] = useProfile()
|
||||
const [step, setStep] = useAtom(stepAtom)
|
||||
const state = useAtomValue(gameStateAtom)
|
||||
const reset = useGameReset()
|
||||
return useCallback(
|
||||
(talent?: Talent['id']) => {
|
||||
if (!state)
|
||||
throw new Error('Game state is not available or already ended.')
|
||||
const result = end(state, profile, talent)
|
||||
setStep(Step.Idle)
|
||||
setProfile(result.profile)
|
||||
reset()
|
||||
return result.achievements
|
||||
const [locked, setLocked] = useState<Set<Talent['id']>>(new Set())
|
||||
const picker = useCallback(
|
||||
(talent: Talent['id']) => {
|
||||
setLocked(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(talent)) {
|
||||
next.delete(talent)
|
||||
return next
|
||||
}
|
||||
if (next.size >= lock) return prev
|
||||
next.add(talent)
|
||||
return next
|
||||
})
|
||||
},
|
||||
[state, step, profile, setStep, setProfile, reset],
|
||||
[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 result = end(state, profile, l)
|
||||
setStep(Step.Idle)
|
||||
setProfile(result.profile)
|
||||
reset()
|
||||
return result.achievements
|
||||
}, [state, step, profile, locked, setStep, setProfile, reset])
|
||||
return [locked, picker, ender] as const
|
||||
}
|
||||
|
||||
@@ -35,12 +35,7 @@ export const useTalentPuller = () => {
|
||||
const [profile] = useProfile()
|
||||
const [pulled, setPulled] = useState<Talent['id'][] | null>(null)
|
||||
const puller = useCallback(
|
||||
// (rng?: RNG) => setPulled(pull(p, profile, rng)),
|
||||
(rng?: RNG) =>
|
||||
setPulled([
|
||||
1142, 1143, 1144, 1145, 1146, 1086, 1122, 1111, 1130, 1048,
|
||||
1033,
|
||||
]),
|
||||
(rng?: RNG) => setPulled(pull(p, profile, rng)),
|
||||
[p, profile, setPulled],
|
||||
)
|
||||
return [pulled, puller] as const
|
||||
@@ -61,7 +56,7 @@ export type TalentPickerResult =
|
||||
}
|
||||
|
||||
export const useTalentPicker = () => {
|
||||
const { pick } = useConfig()
|
||||
const { max } = useConfig()
|
||||
const [picked, setPicked] = useAtom(pickedAtom)
|
||||
const picker = useCallback(
|
||||
(talent: Talent['id']): TalentPickerResult => {
|
||||
@@ -75,29 +70,29 @@ export const useTalentPicker = () => {
|
||||
setPicked(next)
|
||||
return { type: 'ok' }
|
||||
}
|
||||
if (picked.size >= pick) return { type: 'not-enough' }
|
||||
if (picked.size >= max) return { type: 'not-enough' }
|
||||
const e = exclude(talent, picked)
|
||||
if (e) return { type: 'exclude', talent: e }
|
||||
const next = new Set([...picked, talent])
|
||||
setPicked(next)
|
||||
return { type: 'ok' }
|
||||
},
|
||||
[pick, picked, setPicked],
|
||||
[max, picked, setPicked],
|
||||
)
|
||||
return [picked, picker] as const
|
||||
}
|
||||
|
||||
export const useSubmitIsEnable = () => {
|
||||
const { pick } = useConfig()
|
||||
const { max, min } = useConfig()
|
||||
const picked = useAtomValue(pickedAtom)
|
||||
const enabled = picked.size >= pick
|
||||
return [enabled, pick] as const
|
||||
const enabled = picked.size >= min && picked.size <= max
|
||||
return { min, max, enabled } as const
|
||||
}
|
||||
|
||||
export const useTalentSubmit = () => {
|
||||
const picked = useAtomValue(pickedAtom)
|
||||
const setReplaced = useSetAtom(replacedAtom)
|
||||
const [enabled] = useSubmitIsEnable()
|
||||
const { enabled } = useSubmitIsEnable()
|
||||
const next = useSetStep()
|
||||
return useCallback(
|
||||
(rng?: RNG) => {
|
||||
|
||||
Generated
-8
@@ -47,9 +47,6 @@ importers:
|
||||
'@remake/vitex':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/vitex
|
||||
classnames:
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
jotai:
|
||||
specifier: ^2.20.2
|
||||
version: 2.20.2(@types/react@19.2.18)(react@19.2.8)
|
||||
@@ -672,9 +669,6 @@ packages:
|
||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
classnames@2.5.1:
|
||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
@@ -1735,8 +1729,6 @@ snapshots:
|
||||
|
||||
chai@6.2.2: {}
|
||||
|
||||
classnames@2.5.1: {}
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
|
||||
Reference in New Issue
Block a user