45 Commits
Author SHA1 Message Date
Vick Scarlet 2d53a6ca33 feature: bili-toy cloud fallback local 2026-08-16 20:06:16 +08:00
Vick Scarlet 907d0e7d09 update: remove debug log 2026-08-16 20:06:16 +08:00
Vick Scarlet ae5d318e9b update: remove debugger 2026-08-16 20:06:16 +08:00
Vick Scarlet d3f47dd7e9 update: bili-toy 2026-08-16 20:06:16 +08:00
Vick Scarlet 5c951f4940 update: toast achievement icon & style 2026-08-16 20:06:16 +08:00
Vick Scarlet 1e87b8fb34 add: toast achievement & message 2026-08-16 20:06:16 +08:00
Vick Scarlet 31ea679a17 add: github button 2026-08-16 20:06:16 +08:00
Vick Scarlet 5c4c18a6fc update: rename component 2026-08-16 20:06:16 +08:00
Vick Scarlet 32fd52532b update: style 2026-08-16 20:06:16 +08:00
Vick Scarlet e6e601eeb4 update: style 2026-08-16 20:06:16 +08:00
Vick Scarlet 7679c79d30 update: thanks 2026-08-16 20:06:16 +08:00
Vick Scarlet 2f5e05f937 update: style 2026-08-16 20:06:16 +08:00
Vick Scarlet 9dc8a3e8bb update: achievement 2026-08-16 20:06:16 +08:00
Vick Scarlet a0e3f1b355 update: character allocation 2026-08-16 20:06:16 +08:00
Vick Scarlet f4204601ee update: character 2026-08-16 20:06:16 +08:00
Vick Scarlet ad4e83fc47 update: lint config 2026-08-16 20:06:16 +08:00
Vick Scarlet afb4276dd1 add: character 2026-08-16 20:06:16 +08:00
Vick Scarlet 46a8528864 add: mode & character page 2026-08-16 20:06:16 +08:00
Vick Scarlet 7e863021c9 update: phone style 2026-08-16 20:06:16 +08:00
Vick Scarlet 9ae4aa4ba2 update: theme-toggle-btn position 2026-08-16 20:06:16 +08:00
Vick Scarlet 1f455ba353 update: dev params 2026-08-16 20:06:16 +08:00
Vick Scarlet 6eea7d0d87 update: play property animation
update: event format field
fix: test
2026-08-16 20:06:16 +08:00
Vick Scarlet cd82d175c2 feature: lock only one use radio mode 2026-08-16 20:06:16 +08:00
Vick Scarlet 533e705fae feature: multiple lock, pick min max 2026-08-16 20:06:16 +08:00
Vick Scarlet 0c0fac5bfd update: game core flow pages 2026-08-16 20:06:16 +08:00
Vick Scarlet 25bd9ce2b7 feature: spirit allocation 2026-08-16 20:06:16 +08:00
Vick Scarlet dfdbfd69e7 fix: talent 2026-08-16 20:06:16 +08:00
Vick Scarlet 322f775796 update: hooks, web 2026-08-16 20:06:16 +08:00
Vick Scarlet 2f91709d1e update: core support rng 2026-08-16 20:06:16 +08:00
Vick Scarlet 9a0065c1e8 update: dependencies 2026-08-16 20:06:16 +08:00
Vick Scarlet e531cb2ede fix: trigger return id error 2026-08-16 20:06:16 +08:00
Vick Scarlet bcfd24bbba add: achievement, game 2026-08-16 20:06:16 +08:00
Vick Scarlet 32f703be0f update: talent, state props 2026-08-16 20:06:16 +08:00
Vick Scarlet 85c871cec1 update: state FlatState 2026-08-16 20:06:16 +08:00
Vick Scarlet 807a84de7f update: state FlatMapper 2026-08-16 20:06:16 +08:00
Vick Scarlet e646492e75 update: state, event, condition, data.event.types 2026-08-16 20:06:16 +08:00
Vick Scarlet e035cdfe90 update: data transformers 2026-08-16 20:06:16 +08:00
Vick Scarlet ce44237466 update: ts 2026-08-16 20:06:16 +08:00
Vick Scarlet cf79e2763b fix: number check 2026-08-16 20:06:16 +08:00
Vick Scarlet 05b59011ff change: document.body theme color 2026-08-16 20:06:16 +08:00
Vick Scarlet 0b69992037 update: 名人模式解锁条件 2026-08-16 20:06:16 +08:00
Vick Scarlet 90caa8ca92 add: bili-toy-sdk cloud save support 2026-08-16 20:06:16 +08:00
Vick Scarlet 4307904239 add: channel switch 2026-08-16 20:06:16 +08:00
Vick Scarlet ce74c5221f update: event 2026-08-16 20:06:16 +08:00
Vick Scarlet cf95014a0c remove unused libs from public 2026-08-16 20:06:16 +08:00
463 changed files with 10635 additions and 221451 deletions
-2
View File
@@ -1,2 +0,0 @@
node_modules
npm-debug.log
+19 -11
View File
@@ -6,24 +6,32 @@ permissions:
contents: write
jobs:
build-and-deploy:
concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession.
concurrency: ci-${{ github.ref }}
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
- name: 🛎️ Checkout
uses: actions/checkout@v4
- name: Install pnpm
- name: 🛠️ Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: 🛠️ Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
version: latest
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
run: |
pnpm install
pnpm xlsx2json
pnpm build
- name: 📦 Install Dependencies
run: pnpm install
- name: Deploy 🚀
- name: 🏗️ Build data
run: bun build:data
- name: 🏗️ Build web
run: bun build:web
- name: 🚀 Deploy
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: template # The folder the action should deploy.
folder: apps/web/dist # The folder the action should deploy.
+6
View File
@@ -4,6 +4,12 @@
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Editor Browser",
"request": "launch",
"type": "editor-browser",
"url": "http://localhost:5173",
},
{
"type": "node",
"request": "launch",
+3
View File
@@ -0,0 +1,3 @@
{
"js/ts.experimental.useTsgo": true
}
-15
View File
@@ -1,15 +0,0 @@
FROM node:alpine
ENV NPM_CONFIG_LOGLEVEL info
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8080
CMD ["yarn", "dev"]
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 神戸小鳥
Copyright (c) 2021-2026 神戸小鳥 and 神户小德
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+7 -44
View File
@@ -1,56 +1,19 @@
# Life Restart
# 人生重开模拟器
<a href="https://discord.gg/U3qrf49NMQ"><img src="https://img.shields.io/discord/883382868427014255?color=%23FEE75C&label=Discord&logo=discord&logoColor=white&style=for-the-badge" /></a>
[English](./README.md) | 简体中文
## 简介
## 开发环境
- Game Life Restart
包管理: `pnpm`
运行时: `bun`
## 使用
<details>
<summary><strong>网页版</strong></summary>
<br />
### 开发
```bash
# 1. 下载项目代码
git clone https://github.com/VickScarlet/lifeRestart.git my-project
# 2. 进入目录安装依赖
cd my-project
pnpm install
# 3. 转换XLSX表
pnpm xlsx2json
# 4. 启动本地开发服务器
pnpm build:data
pnpm dev
```
启动完成后打开浏览器访问 [http://localhost:5173](http://localhost:5173)。
</details>
<details>
<summary><strong>控制台版本</strong></summary>
<br />
```bash
node repl
```
</details>
## 其他版本
<details>
<summary><strong>版本列表</strong></summary>
<br />
- Cocos版:[gameall3d/LifeRestart_Cocos](https://github.com/gameall3d/LifeRestart_Cocos)
</details>
> 更多信息请参考 [官网文档](https://liferestart.syaro.io/)。
```
+7 -44
View File
@@ -1,56 +1,19 @@
# Life Restart
# Remake
<a href="https://discord.gg/U3qrf49NMQ"><img src="https://img.shields.io/discord/883382868427014255?color=%23FEE75C&label=Discord&logo=discord&logoColor=white&style=for-the-badge" /></a>
English | [简体中文](./README-zh_CN.md)
## Introduction
## Environment
- Game Life Restart
Package Manager: `pnpm`
Runtime: `bun`
## Usage
<details>
<summary><strong>Web Version</strong></summary>
<br />
### Dev
```bash
# 1. Clone project code.
git clone git@github.com:VickScarlet/lifeRestart.git my-project
cd my-project
# 2. Installation dependence.
pnpm install
# 3. Transform XLSX
pnpm xlsx2json
# 4. Start local dev server.
pnpm build:data
pnpm dev
```
After the startup is complete, open a browser and visit [http://localhost:5173](http://localhost:5173).
</details>
<details>
<summary><strong>Command Line Version</strong></summary>
<br />
```bash
node repl
```
</details>
## Other Version
<details>
<summary><strong>Versions</strong></summary>
<br />
- Cocos Ver: [gameall3d/LifeRestart_Cocos](https://github.com/gameall3d/LifeRestart_Cocos)
</details>
> More instructions at [documentation](https://liferestart.syaro.io/).
```
+25 -22
View File
@@ -4,8 +4,8 @@ import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
import { readFile } from 'fs/promises'
import Life from '../src/modules/life.js'
import $lang from '../src/i18n/zh-cn.js'
import Life from '../srco/modules/life.js'
import $lang from '../srco/i18n/zh-cn.js'
globalThis.$lang = $lang
globalThis.json = async fileName =>
@@ -235,7 +235,7 @@ class App {
this.#talentExtend = localStorage.talentExtend
await this.#life.initial(
dataSet => json(`${this.#language}/${dataSet}`),
json
json,
)
this.output(
`\rLoading Complete.
@@ -244,14 +244,14 @@ class App {
\x1B[4m/remake\x1B[24m
\x1B[4m/help\x1B[24m `,
true
true,
)
$$on('achievement', ({ name }) =>
this.output(`
-------------------------
${name}
-------------------------
`)
`),
)
}
@@ -561,19 +561,19 @@ class App {
if (this.#talentSelected.has(s)) continue
if (this.#talentSelected.size == 3)
return warn(
'你只能选3个天赋。请使用 \x1B[4m/unselect\x1B[24m 取消选择你不想要的天赋'
'你只能选3个天赋。请使用 \x1B[4m/unselect\x1B[24m 取消选择你不想要的天赋',
)
const exclusive = this.#life.exclude(
Array.from(this.#talentSelected).map(({ id }) => id),
s.id
s.id,
)
if (exclusive != null)
for (const { name, id } of this.#talentSelected)
if (id == exclusive)
return warn(
`天赋【${s.name}】与已选择的天赋【${name}】冲突`
`天赋【${s.name}】与已选择的天赋【${name}】冲突`,
)
this.#talentSelected.add(s)
@@ -628,8 +628,8 @@ class App {
`grade${talent.grade}b`,
`${check(talent) ? '√' : ' '} ${i} ${talent.name}${
talent.description
}`
)
}`,
),
),
]
.flat()
@@ -645,7 +645,7 @@ class App {
return warn(this.list(), `请选择 3 个天赋`)
this.#step = this.Steps.PROPERTY
this.#life.remake(
Array.from(this.#talentSelected).map(({ id }) => id)
Array.from(this.#talentSelected).map(({ id }) => id),
)
this.#propertyAllocation.total = this.#life.getPropertyPoints()
return this.prop()
@@ -673,15 +673,18 @@ class App {
this.#interval = null
this.#auto = false
} else if (this.#auto) {
this.#interval = setInterval(() => {
const trajectory = this.next()
if (this.#isEnd && this.#interval) {
clearInterval(this.#interval)
this.#interval = null
}
if (!this.#isEnd) return this.output(`${trajectory}\n`)
return this.output(trajectory, true)
}, Number(this.#auto) || 1000)
this.#interval = setInterval(
() => {
const trajectory = this.next()
if (this.#isEnd && this.#interval) {
clearInterval(this.#interval)
this.#interval = null
}
if (!this.#isEnd) return this.output(`${trajectory}\n`)
return this.output(trajectory, true)
},
Number(this.#auto) || 1000,
)
return
}
}
@@ -835,7 +838,7 @@ class App {
return this.prop()
case this.Steps.SUMMARY:
this.#talentExtend = Array.from(this.#talentSelected).sort(
() => Math.random() - 0.5
() => Math.random() - 0.5,
)[0].id
return this.list()
}
@@ -847,7 +850,7 @@ class App {
const { judge, grade, value } = summaryData[type]
return this.style(
`grade${grade}b`,
`${name}${value} ${$lang[judge]}`
`${name}${value} ${$lang[judge]}`,
)
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@remake/console",
"type": "module",
"version": "3.0.0",
"author": "Vick Scarlet <vick@syaro.io>",
"scripts": {
"start": "bun run src/index.ts",
"lint": "bunx --bun eslint .",
"test": "bun test"
},
"dependencies": {
"@remake/core": "workspace:*",
"@remake/data": "workspace:*"
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["**/*"]
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'eslint/config'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import defaultConfig from '../../eslint.config.ts'
export default defineConfig([
defaultConfig,
{ ignores: ['dist/**', 'node_modules/**'] },
{
...react.configs.flat.recommended!,
settings: { react: { version: '19' } },
},
react.configs.flat['jsx-runtime']!,
reactHooks.configs.flat.recommended,
])
+17
View File
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="description" content="人生重开模拟器 - 这垃圾人生一秒也不想待了"/>
<meta name="keywords" content="人生重开模拟器 liferestart life restart remake 人生重来"/>
<meta name="color-scheme" content="light dark" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)" />
<title>人生重开模拟器</title>
</head>
<body>
<div id="remake"></div>
<script type="module" src="src/index.tsx"></script>
</body>
</html>
+34
View File
@@ -0,0 +1,34 @@
{
"name": "@remake/web",
"type": "module",
"version": "3.0.0",
"author": "Vick Scarlet <vick@syaro.io>",
"scripts": {
"dev": "bunx --bun vite",
"dev:prod": "bunx --bun vite --mode production",
"build": "bunx --bun vite build",
"build:bili": "bunx --bun vite build --config vite.config.bili.ts",
"preview": "bunx --bun vite preview",
"lint": "bunx --bun eslint",
"test": "bunx --bun vitest"
},
"dependencies": {
"@remake/data": "workspace:*",
"@remake/hooks": "workspace:*",
"@remake/vitex": "workspace:*",
"@remake/thirdparty-bili-toy": "workspace:*",
"jotai": "^2.20.2",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-hot-toast": "^2.6.0"
},
"devDependencies": {
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"vite": "^8.1.3",
"vitest": "^4.1.10"
}
}
+64
View File
@@ -0,0 +1,64 @@
.screen {
min-width: 32rem;
max-width: min(calc(100dvw - 2rem), 48rem);
position: relative;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> * { width: 100%; }
> .section {
display: flex;
flex-direction: column;
gap: calc(0.5rem * var(--ratio));
width: 100%;
padding: 0;
box-sizing: border-box;
> .title {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-weight: bold;
width: 100%;
height: calc(2rem * var(--ratio));
color: var(--base-text-color);
border: 0.0625rem solid var(--base-text-color);
border-bottom: 0.1875rem solid var(--color-primary);
box-sizing: border-box;
user-select: none;
}
}
> .controls {
display: flex;
flex-direction: row;
justify-content: center;
gap: 0.5rem;
width: 100%;
> button { flex: 1 0; }
}
&.loading {
display: flex;
justify-content: center;
align-items: center;
> * { width: auto; }
animation: pulse 1.8s ease-in-out infinite;
}
}
.saving {
position: fixed;
bottom: 0;
left: 0;
opacity: 0;
user-select: none;
&.active {
animation: pulse 1.8s ease-in-out infinite;
}
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
+46
View File
@@ -0,0 +1,46 @@
import { useEffect } from 'react'
import { useInit, useWatcher } from '@/hooks/storage'
import { useStep, Step } from '@remake/hooks'
import ToastContainer from '@/toast'
import Home from '@/containers/Home'
import Mode from '@/containers/Mode'
import Chara from '@/containers/Chara'
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() {
/* prettier-ignore */
switch (useStep()) {
case Step.Idle: return <Home />
case Step.Mode: return <Mode />
case Step.Chara: return <Chara />
case Step.Pick: return <Pick />
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
}
}
/* prettier-ignore */
const Loading = () => <div className="screen loading"><h2>...</h2></div>
const Saving = ({ active }: { active: boolean }) => (
<div className={`saving ${active ? 'active' : ''}`}>...</div>
)
/* prettier-ignore */
export function Game() {
const [inited, init] = useInit()
const saving = useWatcher()
useEffect(() => { if (!inited) init() }, [inited, init])
if (!inited) return <Loading />
return <><Container /><ToastContainer /><Saving active={saving} /></>
}
export default Game
+31
View File
@@ -0,0 +1,31 @@
.achievement-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 calc(0.5rem * var(--ratio-font));
height: calc(2rem * var(--ratio));
gap: calc(1rem * var(--ratio-font));
font-size: calc(1rem * var(--ratio-font));
color: var(--grade-color);
background: var(--background, var(--base-background-color));
cursor: pointer;
user-select: none;
border: 0.0625rem solid var(--grade-color);
box-sizing: border-box;
transition: all 0.2s ease-in-out;
svg {
fill: var(--grade-color);
transition: fill 0.2s ease-in-out;
stroke: none;
width: calc(1.2rem * var(--ratio-font));
height: calc(1.2rem * var(--ratio-font));
}
.name { font-weight: bold; }
.description {
font-size: calc(0.85rem * var(--ratio-font));
user-select: none;
&::before { content: '['; margin-right: 0.125rem; }
&::after { content: ']'; margin-left: 0.125rem; }
&::before, &::after { color: var(--grade-color); }
}
}
+33
View File
@@ -0,0 +1,33 @@
import { type Achievement as A, achievement } from '@remake/data'
import './Achievement.css'
const Icon = () => (
<svg
width="800px"
height="800px"
viewBox="0 0 6.3500002 6.3500002"
fill="currentColor"
>
<g>
<path d="m 2.5032059,-0.0095735 c -0.043217,-6.35e-4 -0.0909,0.00531 -0.1493441,0.024289 C 2.2369795,0.0525379 2.1848433,0.1094109 2.1259683,0.1650936 2.0670958,0.2207129 2.0130256,0.2808305 1.9606037,0.3407933 1.8557572,0.4607694 1.7291409,0.5919178 1.7440793,0.5810887 1.7590282,0.5703466 1.5965582,0.6493864 1.4500399,0.7118302 1.3767847,0.7430018 1.3005529,0.7771219 1.2293826,0.8156998 1.1582044,0.8544035 1.0898255,0.885282 1.0175096,0.9846817 0.9451937,1.0840436 0.9358089,1.159797 0.9208732,1.2394466 0.9059507,1.319096 0.8985953,1.400403 0.8914171,1.479742 0.8770502,1.6384195 0.8500416,1.8161314 0.8557619,1.7985856 c 0.00572,-0.017558 -0.07988,0.1429176 -0.1617478,0.2795696 -0.040934,0.068351 -0.082447,0.141311 -0.1173056,0.214457 -0.034859,0.073146 -0.069638,0.1380463 -0.069763,0.2609662 -1.217e-4,0.1229198 0.03557,0.1882605 0.070279,0.2614827 0.034711,0.073247 0.07651,0.1434605 0.1173057,0.2118733 0.081592,0.1368131 0.1663991,0.2976496 0.1607158,0.2800863 -0.00569,-0.017526 0.019526,0.1632389 0.033589,0.3219442 0.00704,0.079296 0.014684,0.1606161 0.029456,0.2402954 0.014764,0.079742 0.022966,0.1557634 0.095083,0.2552817 0.072115,0.099531 0.1412954,0.130652 0.2123916,0.1694987 0.071099,0.038784 0.1469443,0.071021 0.2201413,0.1023191 0.1463886,0.062722 0.3099884,0.1442029 0.2950712,0.1333254 -0.014922,-0.010713 0.109855,0.1175901 0.2144581,0.2377115 0.0523,0.060092 0.1060847,0.1214847 0.1648486,0.1772502 0.058759,0.055816 0.1110827,0.1117997 0.2278909,0.1498616 0.1168056,0.03815 0.190463,0.023744 0.2707852,0.013435 0.080328,-0.010432 0.1605915,-0.026186 0.2382282,-0.043925 0.1552734,-0.035351 0.3341952,-0.068207 0.3157432,-0.068213 -0.018441,-1.85e-5 0.1595093,0.03148 0.3147086,0.06718 0.077602,0.017849 0.1568874,0.035993 0.2371963,0.046509 0.08029,0.010893 0.1544188,0.024387 0.2713012,-0.013436 C 4.1130209,5.0582357 4.1651597,5.0033666 4.2240321,4.9477472 4.2829046,4.8921276 4.3395598,4.8298801 4.3919791,4.7699803 4.4968281,4.6500042 4.6208595,4.521427 4.6059211,4.5322686 4.5909722,4.543008 4.7555086,4.4639712 4.9020269,4.4015274 4.9752821,4.3703558 5.0494475,4.3357186 5.1206178,4.2971408 5.1917933,4.258437 5.2601749,4.2280628 5.3324908,4.1286758 5.4048041,4.0293137 5.4141915,3.9535603 5.4291272,3.8739109 5.4440497,3.7942615 5.4514051,3.7129545 5.4585806,3.6336155 5.4729475,3.4749377 5.4999562,3.2946422 5.4942385,3.312188 5.4885235,3.3297458 5.5741162,3.1692831 5.6559863,3.0326187 5.69692,2.9642673 5.7384331,2.8938913 5.7732919,2.8207451 5.8081508,2.7475992 5.8429303,2.6827115 5.8430546,2.5597792 5.8431763,2.4368593 5.807484,2.3715183 5.7727734,2.2982965 5.7380653,2.2250492 5.6967824,2.152252 5.6559863,2.0838392 5.5743941,1.9470137 5.4890712,1.7882568 5.4947545,1.8058201 5.500443,1.8233461 5.4752282,1.6425809 5.4611656,1.4838759 5.4541542,1.404454 5.4464812,1.3232598 5.4317096,1.2435805 5.4169458,1.163838 5.4087437,1.0878171 5.3366262,0.9882988 5.2645114,0.8887679 5.1953308,0.8581762 5.1242346,0.8193169 5.0531358,0.7805332 4.977285,0.7458378 4.9040933,0.7144138 4.7577047,0.6516914 4.5941022,0.5728705 4.6090221,0.5836723 4.6239446,0.5943853 4.499683,0.4630319 4.39508,0.3428602 4.3427798,0.282768 4.2884767,0.2219049 4.2297154,0.1661267 4.1709567,0.1103108 4.1186327,0.0543271 4.0018218,0.0162652 3.8850188,-0.0218851 3.8113668,-0.0055365 3.7310393,0.004896 3.6507118,0.0153285 3.5704478,0.0315991 3.4928111,0.0493378 3.3375377,0.0846891 3.1586212,0.1174185 3.1770679,0.1175506 3.1955094,0.1175694 3.0175559,0.0834873 2.8623566,0.0477876 2.784757,0.0299388 2.7054693,0.0138614 2.6251629,0.0033458 2.5850177,-0.002102 2.5464229,-0.0089361 2.5032059,-0.0095735 Z m 0.6810957,1.3916462 a 0.26407552,0.26514563 1.664302 0 1 0.2098067,0.1069702 l 0.3064404,0.4149617 0.5167657,0.1953371 a 0.26407552,0.26514563 1.664302 0 1 0.1209225,0.403076 L 4.0395461,2.9240974 4.0142255,3.4780688 A 0.26407552,0.26514563 1.664302 0 1 3.6721272,3.7188812 L 3.1791343,3.563335 2.6468667,3.7090625 A 0.26407552,0.26514563 1.664302 0 1 2.3109702,3.4548146 L 2.3068427,2.9364998 2.0050537,2.4734789 A 0.26407552,0.26514563 1.664302 0 1 2.1404463,2.0760872 L 2.6324046,1.9133065 2.9781198,1.4807747 a 0.26407552,0.26514563 1.664302 0 1 0.1865524,-0.098185 0.26407552,0.26514563 1.664302 0 1 0.019632,-5.292e-4 z" />
<path d="m 1.32395,4.6940153 v 0.8454266 c 0,0.22689 0.085796,0.4353634 0.2320264,0.5999633 0.073102,0.0823 0.1640284,0.155757 0.2831888,0.1953369 0.1191578,0.0396 0.2710842,0.033444 0.396875,-0.029456 L 3.1755175,5.8365814 4.1149947,6.3052865 a 0.26460996,0.26460996 0 0 0 0.1188561,0.027389 c 0.4351602,0 0.7932315,-0.3580833 0.7932315,-0.7932332 V 4.6976326 c -0.1063255,0.047962 -0.2399666,0.1090639 -0.2284096,0.1007693 0.01696,-0.0123 -0.1239096,0.1336209 -0.2428795,0.2697509 -0.059468,0.06796 -0.1238859,0.1384379 -0.1906852,0.2015381 -0.066799,0.0631 -0.1257724,0.1255652 -0.2583815,0.168465 -0.1326198,0.0429 -0.2168922,0.027386 -0.3079909,0.014986 -0.09112,-0.011999 -0.1806178,-0.03241 -0.2687188,-0.05271 -0.1760908,-0.0405 -0.3779838,-0.075984 -0.3570843,-0.075964 0.020929,0 -0.1824646,0.036898 -0.3586348,0.076998 -0.08809,0.0201 -0.1791679,0.038326 -0.2702666,0.050126 -0.09113,0.0117 -0.1744398,0.027797 -0.306959,-0.015502 C 2.1045317,5.3929098 2.0448735,5.3293743 1.9781747,5.2660743 1.9114944,5.2028043 1.8504047,5.133253 1.7911063,5.0650529 1.6724248,4.9287629 1.5313251,4.7831021 1.5482267,4.795302 1.5595777,4.803572 1.4276667,4.741909 1.32395,4.6940153 Z" />
</g>
</svg>
)
export interface AchievementProps {
id: A['id']
}
export function Achievement({ id }: AchievementProps) {
const ach = achievement.get(id)
if (!ach) return null
const grade = ach?.grade ?? 0
return (
<div className={`achievement-item grade-${grade}`}>
<Icon />
<span className="name">{ach.name}</span>
<span className="description">{ach.description}</span>
</div>
)
}
export default Achievement
+12
View File
@@ -0,0 +1,12 @@
button.github-btn {
svg {
width: 100%;
height: 100%;
fill: var(--style-color);
transition: fill 0.2s ease-in-out;
}
@media (hover: hover) {
&:hover svg { fill: var(--style-background); }
}
&:active svg{ fill: var(--style-background); }
}
+23
View File
@@ -0,0 +1,23 @@
import { toastMsg } from '@/toast'
import './Github.css'
const repo = 'https://github.com/VickScarlet/remake'
export function Github() {
const handleClick = () => {
if (import.meta.env.VITE_CHANNEL === 'bili') toastMsg(repo, 'github')
else window.open(repo)
}
return (
<button className="github-btn" onClick={handleClick}>
<svg
fill="currentColor"
width="800px"
height="800px"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0 1 38.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z" />
</svg>
</button>
)
}
export default Github
+10
View File
@@ -0,0 +1,10 @@
.message-item {
display: flex;
justify-content: center;
align-items: center;
height: 2rem;
border: 0.0625rem solid var(--base-text-color);
color: var(--base-text-color);
background: var(--base-background-color);
padding: 0 1rem;
}
+10
View File
@@ -0,0 +1,10 @@
import './Message.css'
export interface MsgToastProps {
message: React.ReactNode
}
export function MsgToast({ message }: MsgToastProps) {
return <div className="message-item">{message}</div>
}
export default MsgToast
+50
View File
@@ -0,0 +1,50 @@
ul.replaced {
display: flex;
flex-direction: column;
gap: 0.5rem;
li {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 0;
.talent {
width: 100%;
}
}
li:not(:first-child) {
position: relative;
&::before {
content: '';
position: absolute;
display: block;
box-sizing: border-box;
border: 0.0625rem solid var(--base-text-color);
border-left: none;
right: 0;
top: -1rem;
margin-right: -1rem;
width: 0.625rem;
height: 1.5rem;
}
&::after {
content: '';
position: absolute;
display: block;
box-sizing: border-box;
border: 0.0625rem solid var(--base-text-color);
border-right: none;
border-bottom: none;
right: 0;
top: 0.5rem;
margin-right: -0.6875rem;
margin-top: -0.03125rem;
width: 0.375rem;
height: 0.375rem;
transform: rotate(-45deg);
transform-origin: 0 0;
}
}
}
+28
View File
@@ -0,0 +1,28 @@
import { type Talent as T, talents } from '@remake/data'
import Talent from './Talent'
import './Replaced.css'
export interface TalentProps {
id: T['id']
chains?: T['id'][]
}
export function Replaced({ id, chains }: TalentProps) {
return (
<ul className="replaced">
<li>
<Talent id={id} selected={true} />
</li>
{chains?.map(id => {
const talent = talents.get(id)
if (!talent) return null
return (
<li key={id}>
<Talent id={id} selected={false} />
</li>
)
})}
</ul>
)
}
export default Replaced
+62
View File
@@ -0,0 +1,62 @@
.talent {
--color: var(--grade-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1rem;
height: calc(2rem * var(--ratio));
font-size: calc(1rem * var(--ratio-font));
gap: 1rem;
color: var(--color);
background: var(--background, var(--base-background-color));
cursor: pointer;
user-select: none;
border: 0.0625rem solid var(--grade-color);
box-sizing: border-box;
transition: all 0.2s ease-in-out;
&.selected {
--color: var(--base-background-color);
--background: var(--grade-color);
@media (hover: hover) {
&:hover { background: oklch(from var(--grade-color) calc(l + 0.1) c h); }
}
&:active {
background: oklch(from var(--grade-color) calc(l + 0.1) c h);
transition: none;
}
}
@media (hover: hover) {
&:hover { background: oklch(from var(--grade-color) calc(l + 0.5) c h); }
}
&:active {
background: oklch(from var(--grade-color) calc(l + 0.5) c h);
transition: none;
}
.talent-name { font-weight: bold; }
.talent-description {
color: oklch(from var(--color) calc(l + 0.1) c h);
font-size: calc(0.85rem * var(--ratio-font));
user-select: none;
&::before { content: '['; margin-right: 0.125rem; }
&::after { content: ']'; margin-left: 0.125rem; }
&::before, &::after { color: var(--color); }
}
}
html[data-theme='dark'] {
.talent{
@media (hover: hover) {
&:hover { background: oklch(from var(--grade-color) calc(l - 0.5) c h); }
&.selected:hover { background: oklch(from var(--grade-color) calc(l - 0.1) c h); }
}
&:active {
background: oklch(from var(--grade-color) calc(l - 0.5) c h);
transition: none;
}
&.selected:active {
background: oklch(from var(--grade-color) calc(l - 0.1) c h);
transition: none;
}
.talent-description { color: oklch(from var(--color) calc(l - 0.2) c h); }
}
}
+20
View File
@@ -0,0 +1,20 @@
import { type Talent as T, talents } from '@remake/data'
import './Talent.css'
export interface TalentProps {
id: T['id']
selected?: boolean
}
export function Talent({ id, selected }: TalentProps) {
const talent = talents.get(id)
if (!talent) return null
const grade = talent?.grade ?? 0
return (
<div className={`talent grade-${grade} ${selected ? 'selected' : ''}`}>
<span className="talent-name">{talent.name}</span>
<span className="talent-description">{talent.description}</span>
</div>
)
}
export default Talent
+37
View File
@@ -0,0 +1,37 @@
import { useState, useEffect, useRef } from 'react'
export interface Props {
text: string
className: string
}
export function TextSvg({ text, className }: Readonly<Props>) {
const [height, setHeight] = useState(0)
const [width, setWidth] = useState(0)
const textRef = useRef<SVGTextElement>(null)
useEffect(() => {
if (!textRef.current) return
const { width, height } = textRef.current.getBBox()
setWidth(width)
setHeight(height)
}, [text, className, textRef])
return (
<svg
className={className}
fill="currentColor"
viewBox={`0 0 ${width} ${height}`}
>
<text
ref={textRef}
fontSize={10}
style={{
transform: 'translate(50%,50%)',
textAnchor: 'middle',
dominantBaseline: 'central',
}}
>
{text}
</text>
</svg>
)
}
export default TextSvg
+37
View File
@@ -0,0 +1,37 @@
.theme-svg {
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: center;
.sun-center, .sun-beams, .mask-cutter-group {
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: center;
}
.mask-cutter-group { transform: translate(0.75rem, -0.75rem); }
.sun-center {
transition: fill 0.2s ease-in-out;
fill: var(--style-color);
}
.sun-beams {
transition: stroke 0.2s ease-in-out;
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); }
}
}
html[data-theme='dark'] {
.mask-cutter-group { transform: translate(0, 0); }
.theme-svg { transform: rotate(375deg); }
.sun-center { transform: scale(1.5); }
.sun-beams { opacity: 0; transform: scale(0.6); }
}
+85
View File
@@ -0,0 +1,85 @@
import { useState, useEffect } from 'react'
import './ThemeToggle.css'
export const updateThemeColor = () => {
let metas = document.querySelectorAll('meta[name="theme-color"]')
if (!metas.length) return
const element = document.body
const end = Date.now() + 300
const update = () => {
const { backgroundColor } = getComputedStyle(element)
metas.forEach(meta => meta.setAttribute('content', backgroundColor))
if (Date.now() < end) requestAnimationFrame(update)
}
requestAnimationFrame(update)
}
export function ThemeToggle() {
const [isDark, setIsDark] = useState(() => {
const storedTheme = localStorage.getItem('theme')
if (storedTheme === 'dark') return true
if (storedTheme === 'light') return false
return matchMedia('(prefers-color-scheme: dark)').matches
})
useEffect(() => {
document.documentElement.dataset.theme = isDark ? 'dark' : 'light'
updateThemeColor()
localStorage.setItem('theme', isDark ? 'dark' : 'light')
}, [isDark])
return (
<button
className="theme-toggle-btn"
onClick={() => setIsDark(!isDark)}
aria-label="Toggle theme"
>
<svg
className="theme-svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<defs>
<mask id="ios-fail-proof-mask">
<rect
x="0"
y="0"
width="24"
height="24"
fill="#ffffff"
/>
<g className="mask-cutter-group">
<circle cx="18" cy="5" r="7" fill="#000000" />
</g>
</mask>
</defs>
<circle
className="sun-center"
cx="12"
cy="12"
r="5"
mask="url(#ios-fail-proof-mask)"
fill="currentColor"
/>
<g className="sun-beams" stroke="currentColor">
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</g>
</svg>
</button>
)
}
export default ThemeToggle
+151
View File
@@ -0,0 +1,151 @@
import type { Config } from '@remake/hooks'
export const dev = {} as Record<string, any>
if (import.meta.env.MODE === 'development') {
new URLSearchParams(window.location.search)
.entries()
.forEach(([key, value]) => {
if (key !== 'locked') return (dev[key] = Number(value))
dev[key] = value.split(',').map(v => Number(v))
})
}
//
export const Features = dev.features ?? 1
// 自动模式自动点击间隔
export const AutoInterval = dev.interval ?? 500
// 可锁定的天赋数量
export const LockLimit = dev.lock ?? 1
// 名人模式重开次数限制
export const ModeLimit = dev.mode ?? 10
// 天赋选择数量限制
export const PickMax = dev.max ?? 3
// 天赋选择最少数量限制
export const PickMin = dev.min ?? 3
// 基础属性点
export const BasePoints = dev.points ?? 20
// 单项属性点最大限制
export const AllocLimit = dev.allocate ?? 10
// 默认初始快乐
export const DefaultSpirit = dev.spirit ?? 5
// 天赋抽取个数
export const PullCount = dev.pull ?? 10
// 名人模式抽取个数
export const CharacterPullCount = dev.chara ?? 3
// 名人模式抽取权重切线
export const CharacterWeightKnife = dev.knife ?? 10
// 唯一角色生成抽卡次数限制
export const UniqueLimit = dev.unique ?? 10
// 天赋抽取基础概率
export const PullRateBase: Config['pull']['rate']['base'] = new Map([
[0, 889],
[1, 100],
[2, 10],
[3, 1],
])
// 属性评级线
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],
achievements: [0, 10, 30, 50, 70, 100],
charm: [0, 1, 2, 4, 7, 9, 11],
money: [0, 1, 2, 4, 7, 9, 11],
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, 30, 60, 90],
event: [0, 20, 40, 60],
}
// 可评级属性
export type Judges = keyof typeof JudgeLineMap
// 获取属性评级
export function judge(key: Judges, value: number) {
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],
times: [0, 1, 3, 5],
achievements: [0, 1, 3, 5],
charm: [0, 4, 5, 6],
money: [0, 4, 5, 6],
spirit: [0, 4, 5, 6],
intelligence: [0, 4, 5, 6],
strength: [0, 4, 5, 6],
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--) {
if (level >= arr[i]!) return i
}
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)
return new Map([[2, { mode: 'multiply', value: level + 1 } as const]])
},
achievements: value => {
const level = judge('achievements', value.size)
return new Map([[3, { mode: 'multiply', value: level + 1 } as const]])
},
}
// 权重生成器
function wg(s: number, e: number) {
const length = e - s + 1
return Array.from(
{ length },
(_, i) => [s + i, Math.min(i + 1, length - i)] as const,
)
}
// 唯一角色属性分配权重
export const UniquePropWeight = wg(0, 10)
// 唯一角色天赋个数权重
export const UniqueTalentWeight = wg(1, 5)
export const config: Config = {
features: Features,
lock: LockLimit,
mode: ModeLimit,
max: PickMax,
min: PickMin,
points: BasePoints,
allocate: AllocLimit,
spirit: DefaultSpirit,
pull: {
count: PullCount,
rate: {
base: PullRateBase,
additions: PullRateAdditions,
},
},
chara: {
count: CharacterPullCount,
knife: CharacterWeightKnife,
},
unique: {
limit: UniqueLimit,
config: {
prop: UniquePropWeight,
talent: UniqueTalentWeight,
},
},
}
export default config
+119
View File
@@ -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;
}
}
}
}
}
}
+86
View File
@@ -0,0 +1,86 @@
import { useAchv, useGoHome } from '@remake/hooks'
import { judge, judgeGrade, judgeGradeByValue } from '@/config'
import { rates } from '@/display'
import { achievements, type Achievement } from '@remake/data'
import { toastMsg } from '@/toast'
import './Achv.css'
interface AchievementProps {
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 showRank = () => toastMsg('别卷了,没有排行榜', 'achv-rank')
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" onClick={showRank}>
</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
+123
View File
@@ -0,0 +1,123 @@
.screen.point-allocation {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
width: 100%;
.allocation-input {
display: flex;
gap: 0;
height: calc(2rem * var(--ratio));
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;
height: 100%;
border: none;
text-align: center;
}
input {
flex: 1 0;
height: 100%;
width: 2rem;
font-weight: bold;
color: var(--grade-color);
transition: all 0.2s ease-in-out;
}
}
.points-detail {
min-width: 100%;
color: var(--color-primary);
display: flex;
position: absolute;
flex-direction: column;
white-space: nowrap;
background: var(--base-background-color);
border: 0.0625rem solid var(--color-primary);
box-sizing: border-box;
top: 0;
left: 0;
transform: translateY(-100%);
margin: 0;
font-size: calc(1rem * var(--ratio-font));
li {
display: flex;
justify-content: space-between;
align-items: center;
height: calc(2rem * var(--ratio));
padding: 0 0.5rem;
gap: 0.5rem;
}
li:not(:first-child) {
border-top: 0.0625rem solid var(--color-primary);
}
}
> ul.alloc {
display: flex;
flex-direction: row;
justify-content: center;
gap: 0.5rem;
padding: 0;
margin: 0;
list-style: none;
width: 100%;
font-size: calc(1rem * var(--ratio-font));
> li {
flex: 1 0;
display: flex;
position: relative;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
> span {
width: 100%;
height: calc(2rem * var(--ratio));
color: var(--grade-color);
background: var(--base-background-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;
}
> span.name {
color: var(--base-background-color);
background: var(--grade-color);
}
}
> li.left {
--left-color: var(--color-error);
&.left-0 { --left-color: var(--color-primary); }
> span {
background: var(--left-color);
border: 0.0625rem solid var(--left-color);
}
> button {
--style-color: var(--left-color);
flex: 1;
font-size: calc(1rem * var(--ratio-font));
height: calc(2rem * var(--ratio));
width: 6rem;
padding: 0;
}
}
}
> ul.talent-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
li { width: 100%; }
}
}
+134
View File
@@ -0,0 +1,134 @@
import { useState } from 'react'
import { useAllocator, usePointRandomizer, useLeftPoints } 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 { judgeGradeByValue } from '@/config'
import { talents } from '@remake/data'
import { toastAchvs, toastMsg } from '@/toast'
import Replaced from '@/components/Replaced'
import './Alloc.css'
interface AllocInputProps {
point: number
onChange: (point: number) => void
}
function AllocInput(props: AllocInputProps) {
return (
<div className="allocation-input">
<button onClick={() => props.onChange(props.point - 1)}></button>
<input
className="font-mono"
type="number"
value={props.point}
onChange={e => props.onChange(Number(e.target.value))}
/>
<button onClick={() => props.onChange(props.point + 1)}>+</button>
</div>
)
}
interface PointsDetailProps {
base: number
source: AdditionalPoint[]
fixed?: boolean
}
function PointsDetail({ base, source, fixed }: PointsDetailProps) {
return (
<ul className="points-detail">
<li>
<span>{fixed ? '固定' : '基础'}</span>
<span className="font-mono">{base}</span>
</li>
{source.map(({ talent, points }) => (
<li key={talent}>
<span>{talents.get(talent)?.name ?? talent}</span>
<span className="font-mono">{points}</span>
</li>
))}
</ul>
)
}
export function Alloc() {
const isClassic = useIsClassic()
const picked = usePicked()
const {
talents: { chains },
additionalPoints: { source },
} = useReplaced()
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 != 0) return toastMsg('还有剩余点数未分配', 'alloc-toast')
const achievements = start()
toastAchvs(achievements)
}
return (
<div className="screen point-allocation">
<ul className="talent-list">
{Array.from(picked, id => (
<li key={id}>
<Replaced id={id} chains={chains.get(id)} />
</li>
))}
</ul>
<ul className={`alloc ${isClassic ? 'classic' : 'modify'}`}>
<li className={`left left-${left}`}>
<span className="name"></span>
<button
className="font-mono"
onClick={() => setShowDetail(!showDetail)}
>
{left}
</button>
{showDetail && (
<PointsDetail
base={base}
source={source}
fixed={!isClassic}
/>
)}
</li>
{keys(alloc).map(key => (
<li
key={key}
className={`property grade-${judgeGradeByValue(key, final[key])}`}
>
<span className="name">
{properties[key]}
{!isClassic && (
<span className="font-mono">[{ba[key]}]</span>
)}
</span>
{!isClassic && (
<span className="font-mono">{final[key]}</span>
)}
<AllocInput
point={alloc[key]}
onChange={value => allocator(key, value)}
/>
</li>
))}
</ul>
<div className="controls">
<button className="secondary" onClick={() => random()}>
</button>
<button
className={left ? 'error' : 'primary'}
onClick={handleNext}
>
</button>
</div>
</div>
)
}
export default Alloc
+91
View File
@@ -0,0 +1,91 @@
.screen.chara {
gap: 1rem;
> .chara-list {
display: flex;
flex-direction: column;
gap: calc(0.5rem * var(--ratio));
> .character {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 0.0625rem solid var(--base-text-color);
box-sizing: border-box;
cursor: pointer;
> .generator > ul > li { text-align: center; }
> .name {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: calc(1rem * var(--ratio-font));
height: calc(2rem * var(--ratio));
}
&.selected {
border: 0.0625rem solid var(--color-primary);
> .name {
background-color: var(--color-primary);
color: var(--base-background-color);
}
> .chara-details {
height: auto;
padding: 0.5rem;
}
}
}
}
.chara-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
border-top: 0.0625rem solid var(--base-text-color);
box-sizing: border-box;
height: 0;
padding: 0 0.5rem;
overflow-y: hidden;
transition: all 0.3s ease-in-out;
interpolate-size: allow-keywords;
> ul.talent-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
> ul.properties {
display: flex;
flex-direction: row;
gap: 0.5rem;
width: 100%;
font-weight: bold;
> li {
display: flex;
flex-direction: column;
flex: 1 0;
justify-content: space-between;
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(--grade-color);
box-sizing: border-box;
height: 2rem;
&:first-child {
border-bottom: none;
}
}
.name {
color: var(--base-background-color);
background-color: var(--grade-color);
}
&.charm { order: 1 }
&.intelligence { order: 2 }
&.strength { order: 3 }
&.money { order: 4 }
}
}
}
}
+132
View File
@@ -0,0 +1,132 @@
import { useCharaPuller, useCharaPicker, useCharaSubmit } from '@remake/hooks'
import { useUnique, useUniqueGenerator, convertProps } from '@remake/hooks'
import type { BaseChara } from '@remake/hooks'
import { judgeGradeByValue } from '@/config'
import { characters } from '@remake/data'
import { properties } from '@/display'
import { keys } from '@remake/vitex'
import { toastMsg } from '@/toast'
import Talent from '@/components/Talent'
import './Chara.css'
function Details({ detail }: { detail: BaseChara }) {
const props = convertProps(detail.property)
return (
<div className="chara-details">
<ul className="properties">
{keys(props).map(key => (
<li
key={key}
className={`property ${key} grade-${judgeGradeByValue(key, props[key])}`}
>
<span>{properties[key]}</span>
<span className="font-mono">{props[key]}</span>
</li>
))}
</ul>
<ul className="talent-list">
{detail.talent.map(id => (
<li key={id}>
<Talent id={id} />
</li>
))}
</ul>
</div>
)
}
interface UniqueProps {
selected?: boolean
picker?: () => void
}
function Unique({ selected, picker }: UniqueProps) {
const [unique, generator] = useUniqueGenerator()
return (
<li
className={`character ${selected ? 'selected' : ''}`}
onClick={picker}
>
<span className="name"></span>
{unique && <Details detail={unique} />}
{!unique && (
<div className="chara-details generator">
<ul>
<li>6000</li>
<li> </li>
<li></li>
</ul>
<button
className="primary focus"
onClick={() => generator()}
>
</button>
</div>
)}
</li>
)
}
interface CharacterProps {
id: number
selected?: boolean
picker?: (id: number) => void
}
function Character({ id, selected, picker }: CharacterProps) {
const character = characters.get(id)!
return (
<li
className={`character ${selected ? 'selected' : ''}`}
onClick={() => picker?.(id)}
>
<span className="name">{character.name}</span>
<Details detail={character} />
</li>
)
}
export function Chara() {
const u = useUnique()
const [{ unique, characters }, puller] = useCharaPuller()
const [picked, picker] = useCharaPicker()
const submit = useCharaSubmit()
const handleSubmit = () => {
if (ready) return submit(picked)
if (!picked) toastMsg('请先选择角色', 'chara-toast-pick')
else toastMsg('请先生成唯一角色', 'chara-toast-unique')
}
const ready = picked && (picked.type === 'unique' ? !!u : true)
return (
<div className="screen chara">
<ul className="chara-list">
{unique && (
<Unique
selected={picked?.type === 'unique'}
picker={() => picker.unique()}
/>
)}
{characters.map(id => (
<Character
key={id}
id={id}
selected={picked?.id === id}
picker={id => picker.chara(id)}
/>
))}
</ul>
<div className="controls">
<button className="secondary" onClick={puller}>
</button>
<button
className={ready ? 'primary' : 'error'}
onClick={handleSubmit}
>
</button>
</div>
</div>
)
}
export default Chara
+34
View File
@@ -0,0 +1,34 @@
.screen.home {
gap: 5rem;
.title {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.main { font-weight: bold; }
}
> .actions {
position: fixed;
bottom: 1rem;
right: 1rem;
width: auto;
display: flex;
gap: 1rem;
>button {
width: 2.5rem;
height: 2.5rem;
padding: 0.25rem;
}
}
>.controls {
flex-direction: column;
> div {
flex: 1 0;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
> button { flex: 1 0; }
}
}
}
+41
View File
@@ -0,0 +1,41 @@
import { useRemake, useFeatures, useGoAchv, useGoThanks } from '@remake/hooks'
import { TextSvg } from '@/components/TextSvg'
import ThemeToggle from '@/components/ThemeToggle'
import Github from '@/components/Github'
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>
<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>
<div className="actions">
{features && <Github />}
<ThemeToggle />
</div>
</div>
)
}
+23
View File
@@ -0,0 +1,23 @@
.screen.mode {
gap: 1rem;
button {
display: flex;
flex-direction: column;
height: 10rem;
gap: 0.5rem;
.name {
font-size: 3.5rem;
font-weight: bold;
}
.description {
display: flex;
flex-direction: row;
gap: 1rem;
span {
font-size: 1rem;
&::before { content: '['; }
&::after { content: ']'; }
}
}
}
}
+26
View File
@@ -0,0 +1,26 @@
import { PullCount } from '@/config'
import { useModeChoose } from '@remake/hooks'
import './Mode.css'
export function Mode() {
const [Mode, choose] = useModeChoose()
return (
<div className="screen mode">
<button onClick={() => choose(Mode.Classic)}>
<div className="name"></div>
<div className="description">
<span>{PullCount} </span>
<span></span>
</div>
</button>
<button onClick={() => choose(Mode.Celebrity)}>
<div className="name"></div>
<div className="description">
<span></span>
</div>
</button>
</div>
)
}
export default Mode
+22
View File
@@ -0,0 +1,22 @@
.screen.talent-pick {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1rem;
width: 100%;
ul.talent-list {
display: flex;
flex-direction: column;
gap: calc(0.5rem * var(--ratio));
li {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 0;
.talent {
width: 100%;
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
import { useTalentPuller, useTalentPicker } from '@remake/hooks'
import { useTalentSubmit, useSubmitIsEnable } from '@remake/hooks'
import { PullCount, dev } from '@/config'
import { toastMsg } from '@/toast'
import Talent from '@/components/Talent'
import './Pick.css'
export function Pick() {
const [pulled, puller] = useTalentPuller()
const [talents, picker] = useTalentPicker()
const { enabled, min, max } = useSubmitIsEnable()
const submit = useTalentSubmit()
const msg = enabled
? '下一步'
: `请选取 ${min == max ? min : `${min}~${max}`} 个天赋`
const handleSubmit = () => {
if (enabled) return submit()
toastMsg(msg, 'pick-toast')
}
if (!pulled)
return (
<div className="screen talent-pick">
<button
className="primary font-mono focus"
onClick={() => puller()}
>
{PullCount} !
</button>
</div>
)
const ps = dev.locked
? Array.from(new Set([...dev.locked, ...pulled]))
: pulled
return (
<div className="screen talent-pick">
<ul className="talent-list">
{ps.map(id => (
<li key={id} onClick={() => picker(id)}>
<Talent id={id} selected={talents.has(id)} />
</li>
))}
</ul>
<button
className={enabled ? 'primary' : 'error'}
onClick={handleSubmit}
>
{msg}
</button>
</div>
)
}
export default Pick
+165
View File
@@ -0,0 +1,165 @@
.screen.play {
height: calc(100dvh - 2rem);
gap: 1rem;
> ul.properties {
display: flex;
flex-direction: row;
gap: 0.5rem;
width: 100%;
font-weight: bold;
> li {
display: flex;
flex-direction: column;
flex: 1 0;
justify-content: space-between;
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(--grade-color);
box-sizing: border-box;
height: 2rem;
&:first-child {
border-bottom: none;
}
}
.name {
color: var(--base-background-color);
background-color: var(--grade-color);
}
&.charm { order: 1 }
&.intelligence { order: 2 }
&.strength { order: 3 }
&.money { order: 4 }
&.spirit { order: 5 }
}
}
> ul.logs {
display: flex;
flex-direction: column;
flex: 1 0;
gap: 0.5rem;
padding: 0 0.5rem;
min-width: 32rem;
max-width: 100%;
height: 100%;
border: 0.0625rem solid var(--base-text-color);
box-sizing: border-box;
> li.log {
display: flex;
flex-direction: row;
line-height: 2rem;
gap: 0.5rem;
width: 100%;
user-select: none;
cursor: pointer;
> .age {
flex-shrink: 0;
width: 3rem;
height: 2rem;
text-align: right;
color: var(--color-primary);
font-weight: bold;
/* border-bottom: 0.0625rem solid var(--base-text-color); */
box-sizing: border-box;
}
> .content {
position: relative;
&::before {
content: '';
position: absolute;
left: -0.5rem;
bottom: 0;
height: calc(100% - 2rem);
width: calc(100% + 0.5rem);
border: 0.0625rem solid var(--base-text-color);
border-top: none;
border-right: none;
box-sizing: border-box;
pointer-events: none;
}
.tag, .name {
font-weight: bold;
}
}
}
>li:last-child {
margin-bottom: 2rem;
}
}
ul.log-inner {
> li {
color: var(--grade-color);
display: flex;
flex-direction: row;
gap: 0.5rem;
}
}
.controls {
display: flex;
flex-direction: row;
gap: 0.5rem;
width: 100%;
> button {
flex: 1 0;
min-width: 6rem;
font-size: calc(1.25rem * var(--ratio-font));
height: calc(2.5rem * var(--ratio));
}
}
}
.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); }
}
+203
View File
@@ -0,0 +1,203 @@
import { useState, useRef, useCallback } from 'react'
import { useLayoutEffect, useEffect } from 'react'
import { useNext, useGotoSummary, type Log } from '@remake/hooks'
import { useJudge } from '@/hooks/judge'
import { achievements, events, talents } from '@remake/data'
import { properties } from '@/display'
import { AutoInterval } from '@/config'
import { format } from '@remake/vitex'
import { toastAchvs } from '@/toast/Achv'
import './Play.css'
function LogTalent({ id }: { id: number }) {
const { name, description, grade } = talents.get(id)!
return (
<li className={`grade-${grade}`}>
<span className="tag font-mono">[]</span>
<span className="name">{name}</span>
<span className="description">{description}</span>
</li>
)
}
function LogTalents({ items }: { items: number[] }) {
if (items.length === 0) return null
const els = items.map(id => <LogTalent key={id} id={id} />)
return <ul className="log-inner log-talents">{els}</ul>
}
const year = new Date().getFullYear()
interface LogEventProps {
id: number
post: boolean
index: number
}
function LogEvent({ id, post, index }: LogEventProps) {
let { event, postEvent, grade, format: f } = events.get(id)!
if (f) {
const g = (key: string) => ({ CurrentYear: year + index })[key]
event = format(event, g)
if (post && postEvent) postEvent = format(postEvent, g)
}
return (
<>
<li className={`grade-${grade}`}>{event}</li>
{post && postEvent && (
<li className={`grade-${grade}`}>{postEvent}</li>
)}
</>
)
}
function LogEvents({ items, index }: { items: number[]; index: number }) {
const last = items.length - 1
const els = items.map((id, i) => (
<LogEvent key={id} id={id} post={i == last} index={index} />
))
return <ul className="log-inner log-events">{els}</ul>
}
function LogAchievement({ id }: { id: number }) {
const { name, description, grade } = achievements.get(id)!
return (
<li className={`grade-${grade}`}>
<span className="tag font-mono">[]</span>
<span className="name">{name}</span>
<span className="description">{description}</span>
</li>
)
}
function LogAchievements({ items }: { items: number[] }) {
if (items.length === 0) return null
const els = items.map(id => <LogAchievement key={id} id={id} />)
return <ul className="log-inner log-achievements">{els}</ul>
}
function Log({ log, index }: { log: Log; index: number }) {
return (
<li className="log">
<span className="age font-mono">{log.age}</span>
<div className="content">
<LogTalents items={log.talents} />
<LogEvents items={log.events} index={index} />
<LogAchievements items={log.achievements} />
</div>
</li>
)
}
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 [flip, setFlip] = useState(0)
const [displayValue, setDisplayValue] = useState<number>(value)
useEffect(() => {
const prev = prevRef.current
if (value === prev) return
const startValue = prevRef.current
prevRef.current = value
setTrend(value > prev ? 'up' : 'down')
setFlip(f => (f + 1) % 2)
let startTimestamp: number | null = null
const duration = 400
let end = false
const step = (timestamp: number) => {
if (!startTimestamp) startTimestamp = timestamp
const progress = timestamp - startTimestamp
const progressRatio = Math.min(progress / duration, 1)
const easeOutQuad = progressRatio * (2 - progressRatio)
const currentDec = startValue + (value - startValue) * easeOutQuad
setDisplayValue(Math.round(currentDec))
if (!end && progress < duration) requestAnimationFrame(step)
}
requestAnimationFrame(step)
const timer = setTimeout(() => setTrend('normal'), 2000)
return () => {
clearTimeout(timer)
setDisplayValue(value)
end = true
}
}, [value])
return (
<li className={`${prop} grade-${grade} trend-${trend}-${flip}`}>
<span className="name">{properties[prop]}</span>
<span className="value font-mono">{displayValue}</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 [{ logs, ended }, next] = useNext()
const [auto, setAuto] = useState(false)
const logRef = useRef<HTMLUListElement>(null)
const autoRef = useRef(0)
const gotoSummary = useGotoSummary()
const handleNext = useCallback(() => {
if (ended) return
const achievements = next()
toastAchvs(achievements)
}, [ended, next])
const handleGotoSummary = useCallback(() => {
if (!ended) return
const achievements = gotoSummary()
toastAchvs(achievements)
}, [ended, gotoSummary])
useLayoutEffect(() => {
requestAnimationFrame(() => {
if (!logRef.current) return
logRef.current.scrollTop = logRef.current.scrollHeight
})
}, [logs])
useEffect(() => {
if (!auto) window.clearInterval(autoRef.current)
else autoRef.current = window.setInterval(handleNext, AutoInterval)
return () => window.clearInterval(autoRef.current)
}, [auto, handleNext])
return (
<div className="screen play">
<Properties />
<ul
className="logs hide-scrollbar"
onClick={handleNext}
ref={logRef}
>
{logs.map((log, index) => (
<Log key={index} log={log} index={index} />
))}
</ul>
<div className="controls">
{!ended && (
<button className="primary" onClick={() => setAuto(!auto)}>
{auto ? '关闭自动' : '开启自动'}
</button>
)}
{ended && (
<button className="primary" onClick={handleGotoSummary}>
</button>
)}
</div>
</div>
)
}
export default Play
+43
View File
@@ -0,0 +1,43 @@
.screen.summary {
gap: 1rem;
ul.judge-list {
display: flex;
flex-direction: row;
gap: 0.125rem;
width: 100%;
> li {
display: flex;
flex-direction: column;
flex: 1 0;
justify-content: space-between;
text-align: center;
min-width: 4rem;
user-select: none;
span {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-weight: bold;
border: 0.0625rem solid var(--grade-color);
box-sizing: border-box;
height: 2rem;
color: var(--grade-color);
&:not(:first-child) {
border-top: none;
}
}
.level {
font-size: 0.95rem;
background-color: var(--grade-color);
color: var(--base-background-color);
}
}
}
ul.talent-list {
display: flex;
flex-direction: column;
gap: calc(0.5rem * var(--ratio));
width: 100%;
}
}
+82
View File
@@ -0,0 +1,82 @@
import { useEffect, useRef } from 'react'
import { usePicked, useEnd, useProfile, useIsClassic } from '@remake/hooks'
import { useEndJudge } from '@/hooks/judge'
import { properties, judgeDisplay } from '@/display'
import { toastAchvs, toastMsg } from '@/toast'
import Talent from '@/components/Talent'
import './Summary.css'
function Judges() {
const judges = useEndJudge()
return (
<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 = usePicked()
const hasInitialized = useRef(false)
useEffect(() => {
if (!profile.locked) return
if (!hasInitialized.current) {
for (const id of profile.locked) {
if (talents.has(id) && !picked.has(id)) {
picker(id)
}
}
hasInitialized.current = true
}
}, [profile.locked, talents, picked, picker])
return (
<ul className="talent-list">
{Array.from(talents, id => (
<li key={id} onClick={() => picker(id)}>
<Talent id={id} selected={picked.has(id)} />
</li>
))}
</ul>
)
}
export default function Summary() {
const isClassic = useIsClassic()
const [locked, picker, end] = useEnd()
const handlePicker = (id: number) => {
if (isClassic) picker(id)
else toastMsg('名人天赋不可锁定', 'summary-toast')
}
const handleEnd = () => {
const achievements = end()
toastAchvs(achievements)
}
return (
<div className="screen summary">
<Judges />
<div className="section">
<div className="title">
{isClassic
? '你可以锁定一个天赋,下辈子还能抽到'
: '名人天赋不可锁定'}
</div>
<TalentList picked={locked} picker={handlePicker} />
</div>
<button className="primary" onClick={handleEnd}>
</button>
</div>
)
}
+95
View File
@@ -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;}
}
}
}
+47
View File
@@ -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
+31
View File
@@ -0,0 +1,31 @@
export const properties = {
charm: '颜值',
intelligence: '智力',
strength: '体质',
money: '家境',
spirit: '快乐',
age: '享年',
summary: '总评',
}
const s = (str: string) => str.split('|')
const p = (prefix: string, arr: string[]) => arr.map(v => prefix + v)
const jbase = '地狱|折磨|不佳|普通|优秀|罕见|逆天|传说'
export const judges = {
summary: s(jbase),
charm: s(jbase),
money: s(jbase),
spirit: s('地狱|折磨|不幸|普通|幸福|极乐|天命'),
intelligence: s(jbase + '|识海|元神|仙魂'),
strength: s(jbase + '|凝气|筑基|金丹|元婴|仙体'),
age: s('胎死腹中|早夭|少年|盛年|中年|花甲|古稀|杖朝|南山|不老|修仙|仙寿'),
}
export const rates = {
times: p('抽到紫色概率', s('不变|翻倍|三倍|四倍|五倍|六倍')),
achievement: p('抽到橙色概率', s('不变|翻倍|三倍|四倍|五倍|六倍')),
}
export function judgeDisplay(key: keyof typeof judges, level: number) {
return judges[key][level]
}
+46
View File
@@ -0,0 +1,46 @@
import { useMemo } from 'react'
import { useSummary, useGameState } from '@remake/hooks'
import { judge, judgeGrade } from '@/config'
import type { Judges } from '@/config'
import { keys } from '@remake/vitex'
export interface Judge {
value: number
level: number
grade: number
}
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')
const current = state.props.current
return useMemo(() => {
const result = judges(current, ['age'])
return result
}, [current])
}
export const useEndJudge = () => {
const summary = useSummary()
const state = useGameState()
if (!state) throw new Error('Game state is not set')
const highest = state.props.highest
return useMemo(() => {
const props = { ...highest, summary }
const result = judges(props)
return result
}, [highest, summary])
}
+63
View File
@@ -0,0 +1,63 @@
import { useEffect, useCallback, useTransition } from 'react'
import { atom, useAtom } from 'jotai'
import { useConfigInject, useProfileInject, useRawProfile } from '@remake/hooks'
import { useUniqueInject, useUnique } from '@remake/hooks'
import { init, get, set } from '@/storage'
import { config } from '@/config'
const initedAtom = atom(false)
export const useInit = () => {
const configInject = useConfigInject()
const profileInject = useProfileInject()
const uniqueInject = useUniqueInject()
const [inited, setInited] = useAtom(initedAtom)
const loader = useCallback(async () => {
if (inited) return
configInject(config)
await init()
const profile = await get('profile')
const parsed = profile ? JSON.parse(profile) || {} : {}
profileInject({
...parsed,
times: parsed.times || 0,
achievements: new Set(parsed.achievements || []),
events: new Set(parsed.events || []),
talents: new Set(parsed.talents || []),
})
const unique = await get('unique')
if (unique) uniqueInject(JSON.parse(unique))
setInited(true)
}, [inited, configInject, profileInject, uniqueInject, setInited])
return [inited, loader] as const
}
export const useWatcher = () => {
const [profile] = useRawProfile()
const unique = useUnique()
const [inited] = useAtom(initedAtom)
const [p, saveProfile] = useTransition()
const [u, saveUnique] = useTransition()
useEffect(() => {
if (!inited || !profile) return
const str = JSON.stringify({
...profile,
times: profile.times,
achievements: Array.from(profile.achievements),
events: Array.from(profile.events),
talents: Array.from(profile.talents),
})
saveProfile(async () => {
await set('profile', str)
})
}, [inited, profile, saveProfile])
useEffect(() => {
if (!inited || !unique) return
const str = JSON.stringify(unique)
saveUnique(async () => {
await set('unique', str)
})
}, [inited, unique, saveUnique])
return p || u
}
+13
View File
@@ -0,0 +1,13 @@
import { createRoot } from 'react-dom/client'
import { StrictMode } from 'react'
import { Game } from './Game'
import './styles/colors.css'
import './styles/common.css'
const container = document.getElementById('remake')!
const root = createRoot(container)
root.render(
<StrictMode>
<Game />
</StrictMode>,
)
+15
View File
@@ -0,0 +1,15 @@
import { test, expect, describe } from 'vitest'
import { talents, achievements, events } from '@remake/data'
describe('Bili Toy Cloud Save Core', () => {
test('maxSizeProfile should pass ToyCloudSaveCore constraints', () => {
const json = JSON.stringify({
times: 999999,
locked: Array.from(talents.keys()),
talents: Array.from(talents.keys()),
achievements: Array.from(achievements.keys()),
events: Array.from(events.keys()),
})
expect(json.length).toBeLessThan((48 * 960 * 3) / 4)
})
})
+23
View File
@@ -0,0 +1,23 @@
import * as biliToy from '@remake/thirdparty-bili-toy'
import * as local from './local'
interface Storage {
init(): Promise<void>
get(key: string): Promise<string | null>
set(key: string, value: string): Promise<boolean>
}
const storage = {} as Storage
if (import.meta.env.VITE_CHANNEL === 'bili') {
storage.init = biliToy.init
storage.get = biliToy.get
storage.set = biliToy.set
} else {
storage.init = async () => {}
storage.get = local.get
storage.set = local.set
}
export const init = storage.init
export const get = storage.get
export const set = storage.set
+39
View File
@@ -0,0 +1,39 @@
if (localStorage.getItem('version') !== '3.0.0') {
const times = parseInt(localStorage.getItem('times') || '0')
if (times) {
const achievements = (
JSON.parse(localStorage.getItem('ACHV') || '[]') as [
number,
number,
][]
)
.sort((a, b) => a[1] - b[1])
.map(([id]) => id)
const events = JSON.parse(localStorage.getItem('AEVT') || '[]')
const talents = JSON.parse(localStorage.getItem('ATLT') || '[]')
const profile = { times, achievements, events, talents } as any
const lockedTalents = JSON.parse(
localStorage.getItem('extendTalent') || 'null',
)
if (lockedTalents) profile.locked = [lockedTalents]
localStorage.setItem('profile', JSON.stringify(profile))
localStorage.removeItem('times')
localStorage.removeItem('ACHV')
localStorage.removeItem('AEVT')
localStorage.removeItem('ATLT')
localStorage.removeItem('extendTalent')
const unique = localStorage.getItem('uniqueWaTaShi')
if (unique) localStorage.setItem('unique', unique)
localStorage.removeItem('uniqueWaTaShi')
}
localStorage.setItem('version', '3.0.0')
}
export async function get(key: string) {
return localStorage.getItem(key)
}
export async function set(key: string, value: string) {
localStorage.setItem(key, value)
return true
}
+22
View File
@@ -0,0 +1,22 @@
:root {
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);
--color-grade-2: light-dark(#b03fe4, #e09eff);
--color-grade-3: light-dark(#ffaf2d, #ffe48d);
--color-success: light-dark(#61b700, #b9ff69);
--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); }
.grade-1 { --grade-color: var(--color-grade-1); }
.grade-2 { --grade-color: var(--color-grade-2); }
.grade-3 { --grade-color: var(--color-grade-3); }
+134
View File
@@ -0,0 +1,134 @@
:root {
--ratio: 1;
--ratio-font: 1;
color: var(--base-text-color);
}
html { transition: all 0.2s ease-in-out; }
html[data-theme='light'] { color-scheme: light; }
html[data-theme='dark'] { color-scheme: dark; }
.hide-scrollbar,
html {
overflow-y: auto;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar { display: none; }
}
body {
min-height: calc(100dvh - 2rem);
width: calc(100dvw - 2rem);
display: grid;
place-items: center;
margin: 1rem;
padding: 0;
background: var(--base-background-color);
transition: all 0.2s ease-in-out;
}
html, body, button, input, select, textarea {
font-family:
-apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "PingFang SC",
"Noto Sans SC", "Noto Sans CJK SC", "Noto Sans",
"Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.font-mono {
font-family:
"SF Mono", SFMono-Regular, Menlo, Monaco, Consolas,
"Noto Sans Mono CJK SC", "Noto Sans Mono", "Noto Code",
ui-monospace, monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
--style-color: var(--base-text-color);
--style-background: var(--base-background-color);
--border-color: var(--style-color);
--background-color: var(--style-background);
--text-color: var(--style-color);
color: var(--text-color);
cursor: pointer;
box-sizing: border-box;
border: var(--border-color) 0.0625rem solid;
background: var(--background-color);
font-size: calc(1.2rem * var(--ratio-font));
font-weight: bold;
padding: 0 1rem;
height: calc(2.5rem * var(--ratio));
display: inline-flex;
align-items: center;
justify-content: center;
user-select: none;
transition: all 0.2s ease-in-out;
outline: none;
-webkit-tap-highlight-color: transparent;
@media (hover: hover) {
&:hover {
background: var(--style-color);
color: var(--style-background);
}
}
&:active {
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 {
display: flex;
align-items: center;
justify-content: center;
height: calc(3.5rem * var(--ratio));
font-size: calc(1.6rem * var(--ratio-font));
font-weight: bold;
}
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
input {
font-size: calc(1rem * var(--ratio-font));
padding: 0;
margin: 0;
border: none;
background: var(--base-background-color);
text-align: center;
-moz-appearance: textfield;
appearance: none;
color: var(--base-text-color);
transition: all 0.2s ease-in-out;
&:focus { outline: none; }
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
}
input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0; /* 移除某些浏览器下的默认留白 */
}
}
@media (max-width: 32rem) { :root { font-size: 2.88vw; } }
@media screen and (orientation: portrait) and (pointer: coarse) {
:root {
--ratio: 1.5;
--ratio-font: 1.2;
}
}
+40
View File
@@ -0,0 +1,40 @@
.achv-toast-container {
position: fixed;
width: min(calc(100dvw - 2rem), 48rem);
top: 6rem;
left: 1rem;
pointer-events: none;
user-select: none;
.toast-item {
display: flex;
position: absolute;
opacity: 1;
transition: all 0.2s ease-in-out;
&.visible {
animation: achv-slide-in 0.2s ease-out forwards;
}
&.hidden {
animation: achv-fade-out 0.2s ease-out forwards;
}
}
}
@keyframes achv-slide-in {
0% { opacity: 0; transform: translateX(-100%); }
100% { opacity: 1; transform: translateX(0); }
}
@keyframes achv-slide-out {
0% { opacity: 1; transform: translateX(0); }
100% { opacity: 0; transform: translateX(-100%); }
}
@keyframes achv-fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes achv-fade-out {
0% { opacity: 1; }
100% { opacity: 0; }
}
+49
View File
@@ -0,0 +1,49 @@
import { useToaster, toast } from 'react-hot-toast/headless'
import Achievement from '@/components/Achievement'
import './Achv.css'
export function ToastAchvContainer() {
const { toasts, handlers } = useToaster({ duration: 4000 }, 'achv-toast')
const { startPause, endPause, calculateOffset, updateHeight } = handlers
return (
<div
className="achv-toast-container"
onMouseEnter={startPause}
onMouseLeave={endPause}
>
{toasts.map(toast => {
const offset = calculateOffset(toast, {
reverseOrder: true,
gutter: 8,
})
return (
<div
className={`toast-item ${toast.visible ? 'visible' : 'hidden'}`}
key={toast.id}
ref={el => {
if (el && typeof toast.height !== 'number') {
const height = el.getBoundingClientRect().height
updateHeight(toast.id, height)
}
}}
style={{
top: `${offset}px`,
}}
>
{typeof toast.message == 'function'
? toast.message(toast)
: toast.message}
</div>
)
})}
</div>
)
}
export default ToastAchvContainer
export function toastAchvs(achievements: number[]) {
if (achievements.length === 0) return
for (const id of achievements) {
toast(<Achievement id={id} />, { id: '' + id, toasterId: 'achv-toast' })
}
}
+45
View File
@@ -0,0 +1,45 @@
.msg-toast-container {
position: fixed;
width: min(calc(100dvw - 2rem), 48rem);
top: 6rem;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
pointer-events: none;
user-select: none;
.toast-item {
display: flex;
position: absolute;
opacity: 1;
transition: all 0.2s ease-in-out;
&.visible {
animation: msg-slide-in 0.2s ease-out forwards;
}
&.hidden {
animation: msg-fade-out 0.2s ease-out forwards;
}
}
}
@keyframes msg-slide-in {
0% { opacity: 0; transform: translateY(-100%); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes msg-slide-out {
0% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-100%); }
}
@keyframes msg-fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes msg-fade-out {
0% { opacity: 1; }
100% { opacity: 0; }
}
+46
View File
@@ -0,0 +1,46 @@
import { useToaster, toast } from 'react-hot-toast/headless'
import Message from '@/components/Message'
import './Msg.css'
export function ToastMsgContainer() {
const { toasts, handlers } = useToaster({ duration: 1500 }, 'msg-toast')
const { startPause, endPause, calculateOffset, updateHeight } = handlers
return (
<div
className="msg-toast-container"
onMouseEnter={startPause}
onMouseLeave={endPause}
>
{toasts.map(toast => {
const offset = calculateOffset(toast, {
reverseOrder: true,
gutter: 8,
})
return (
<div
className={`toast-item ${toast.visible ? 'visible' : 'hidden'}`}
key={toast.id}
ref={el => {
if (el && typeof toast.height !== 'number') {
const height = el.getBoundingClientRect().height
updateHeight(toast.id, height)
}
}}
style={{
top: `${offset}px`,
}}
>
{typeof toast.message == 'function'
? toast.message(toast)
: toast.message}
</div>
)
})}
</div>
)
}
export default ToastMsgContainer
export function toastMsg(message: string, key: string) {
toast(<Message message={message} />, { id: key, toasterId: 'msg-toast' })
}
+13
View File
@@ -0,0 +1,13 @@
export * from './Msg'
export * from './Achv'
import ToastAchvContainer from './Achv'
import ToastMsgContainer from './Msg'
export const ToastContainer = () => (
<>
<ToastAchvContainer />
<ToastMsgContainer />
</>
)
export default ToastContainer
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"jsx": "react-jsx",
"types": ["react", "vite/client"],
"jsxImportSource": "react",
"paths": {
"@/*": ["./src/*"],
},
},
"include": ["**/*"]
}
+33
View File
@@ -0,0 +1,33 @@
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 },
define: { 'import.meta.env.VITE_CHANNEL': '"bili"' },
base: './',
build: {
outDir: 'dist/bili/remake',
chunkSizeWarningLimit: 1500,
rolldownOptions: {
output: {
codeSplitting: {
minSize: 1024,
groups: [dataSplitRule],
},
},
},
},
})
+31
View File
@@ -0,0 +1,31 @@
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 },
base: './',
build: {
chunkSizeWarningLimit: 1500,
rolldownOptions: {
output: {
codeSplitting: {
minSize: 1024,
groups: [dataSplitRule],
},
},
},
},
})
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Some files were not shown because too many files have changed in this diff Show More