feature: multiple lock, pick min max

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