mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-19 18:43:53 +08:00
Compare commits
201
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30b4791813 | ||
|
|
9017780898 | ||
|
|
aff50e2521 | ||
|
|
0e8bf1131a | ||
|
|
bc2d89fa52 | ||
|
|
63cc529479 | ||
|
|
847d72c384 | ||
|
|
ef55eb703a | ||
|
|
f2e8fac4de | ||
|
|
a2e40230c7 | ||
|
|
10debc6515 | ||
|
|
b8d250906a | ||
|
|
d0f7dbb464 | ||
|
|
9c8478c102 | ||
|
|
4be851a591 | ||
|
|
71daeb3368 | ||
|
|
5031f540cb | ||
|
|
2d35eda931 | ||
|
|
f439141ded | ||
|
|
5a4821433a | ||
|
|
cd339c2103 | ||
|
|
5d50a4ea37 | ||
|
|
88536c7934 | ||
|
|
5353edd6a1 | ||
|
|
727fe5de77 | ||
|
|
d30b93ebf2 | ||
|
|
487a728eeb | ||
|
|
9df9e8776e | ||
|
|
b4177d7657 | ||
|
|
f60e279f62 | ||
|
|
72bf21d592 | ||
|
|
329a4e3a81 | ||
|
|
97988a95ba | ||
|
|
4043fa887a | ||
|
|
1540a5d142 | ||
|
|
42d874556c | ||
|
|
43751ad2db | ||
|
|
6fdbcea925 | ||
|
|
50411b82a0 | ||
|
|
988fcacd2d | ||
|
|
2b62a3a838 | ||
|
|
a10861eed9 | ||
|
|
3f60a7d052 | ||
|
|
3b6eeaeb3a | ||
|
|
c4e1ea1f0d | ||
|
|
e59ab00384 | ||
|
|
943fa8da23 | ||
|
|
1eda000ecc | ||
|
|
03bcb19942 | ||
|
|
d52d2d5d79 | ||
|
|
17c4d90bac | ||
|
|
7963700a37 | ||
|
|
f12a147093 | ||
|
|
59a10587a7 | ||
|
|
dd5a003801 | ||
|
|
ddc87ebf40 | ||
|
|
23bcae0b74 | ||
|
|
e075dfc23d | ||
|
|
9088ab0eb1 | ||
|
|
39388c6eaf | ||
|
|
0ce9b0ed02 | ||
|
|
aee4fcf164 | ||
|
|
5c0fe56915 | ||
|
|
b82da6c9a7 | ||
|
|
b1ffae9eaf | ||
|
|
fee986432d | ||
|
|
25fc74398a | ||
|
|
35c5fe36fb | ||
|
|
3f58e3deae | ||
|
|
788c864e71 | ||
|
|
680a4f8a5a | ||
|
|
8f46e286dd | ||
|
|
2eaf7a1ba2 | ||
|
|
6b58fa99cf | ||
|
|
5d1b623230 | ||
|
|
6e442c14c1 | ||
|
|
2eadb6fb23 | ||
|
|
d19afd7b1a | ||
|
|
98eda94f66 | ||
|
|
e0f5cb71c5 | ||
|
|
58140fd4d2 | ||
|
|
b44e0e65a5 | ||
|
|
cc460d6108 | ||
|
|
1dd1927612 | ||
|
|
21d4c9d568 | ||
|
|
35eac7d19d | ||
|
|
c1ed14385a | ||
|
|
50ad3d35e7 | ||
|
|
4df53d9f58 | ||
|
|
8a6f50dda6 | ||
|
|
49488505e2 | ||
|
|
dd64e8f44e | ||
|
|
b39772fd94 | ||
|
|
fc1135a621 | ||
|
|
d7ea91a1b8 | ||
|
|
53a8729968 | ||
|
|
ad93703a45 | ||
|
|
329c2f09c4 | ||
|
|
1f7434a133 | ||
|
|
145211ba9a | ||
|
|
1d58fd6bfd | ||
|
|
26d4e09bc8 | ||
|
|
49619fbaa2 | ||
|
|
751a0dec0f | ||
|
|
bc560c393b | ||
|
|
8eb0e44845 | ||
|
|
14f9340d0a | ||
|
|
150c991648 | ||
|
|
99640a5813 | ||
|
|
485c1bb48c | ||
|
|
5799cf635b | ||
|
|
066f1fd060 | ||
|
|
162eaffd1d | ||
|
|
b5a4db5224 | ||
|
|
09b3aeb8c5 | ||
|
|
c83ceef3c4 | ||
|
|
221e5219f0 | ||
|
|
145dfe964d | ||
|
|
bf9702e3a1 | ||
|
|
b3f1e6c9c5 | ||
|
|
960f3137c6 | ||
|
|
e38daf3ebb | ||
|
|
e72d16f811 | ||
|
|
dea6001234 | ||
|
|
67d7584a6e | ||
|
|
417e386412 | ||
|
|
c3d556ed80 | ||
|
|
af24e69848 | ||
|
|
6d2f1df0da | ||
|
|
037db837b2 | ||
|
|
e91fab8a94 | ||
|
|
b65ef06acd | ||
|
|
10527e4480 | ||
|
|
25d1d4f4bc | ||
|
|
b8fcb7203d | ||
|
|
79e21a1156 | ||
|
|
70d0b4460a | ||
|
|
40b1ba3542 | ||
|
|
274290cc5b | ||
|
|
fa6aa9bc7d | ||
|
|
b18c41349d | ||
|
|
b36f20c0d7 | ||
|
|
75a1d3c5aa | ||
|
|
78ab0fee06 | ||
|
|
1b8fe068f7 | ||
|
|
204635a84b | ||
|
|
21d6df56a4 | ||
|
|
514adffb9c | ||
|
|
0006634e5d | ||
|
|
fe79670273 | ||
|
|
f11af0da02 | ||
|
|
fbd56604c0 | ||
|
|
296e2665b5 | ||
|
|
70b6129c6e | ||
|
|
aa84c5f8a2 | ||
|
|
8b0a237ce4 | ||
|
|
6f89bc68fe | ||
|
|
48f7941e64 | ||
|
|
2557e76761 | ||
|
|
b2d556a9f4 | ||
|
|
b1faf87dc0 | ||
|
|
12b5d2d042 | ||
|
|
64dc0a5b1d | ||
|
|
4e63afbfc6 | ||
|
|
f8f863cada | ||
|
|
441e67a462 | ||
|
|
c01cdfd4e6 | ||
|
|
a4e609f927 | ||
|
|
ccb8be24d5 | ||
|
|
8d4cd08e2e | ||
|
|
a40c5e3951 | ||
|
|
06de7417a8 | ||
|
|
b4d1d757e1 | ||
|
|
574df2b670 | ||
|
|
a5c26a8d68 | ||
|
|
866acdb8b2 | ||
|
|
b833713afe | ||
|
|
7db04c03d7 | ||
|
|
b13c6aa488 | ||
|
|
1c1a2315a0 | ||
|
|
38ac352795 | ||
|
|
2c9392de8d | ||
|
|
71507e0164 | ||
|
|
74e8a7b34f | ||
|
|
5031b089ff | ||
|
|
8a265a2e84 | ||
|
|
fb7d78ef83 | ||
|
|
65be1b4f40 | ||
|
|
77f483bd18 | ||
|
|
a52178b5f8 | ||
|
|
1e4670c112 | ||
|
|
ed0a5de025 | ||
|
|
c73d2ebae6 | ||
|
|
f048e0bbc2 | ||
|
|
7654b2a212 | ||
|
|
cf1acb64bf | ||
|
|
6433c737d7 | ||
|
|
1e816faf00 | ||
|
|
e876fc7768 | ||
|
|
a59bce865f | ||
|
|
1003a78a2a |
@@ -0,0 +1,3 @@
|
||||
paths-ignore:
|
||||
- 'public/libs/**'
|
||||
- 'tests'
|
||||
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Build and Deploy
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
concurrency: ci-${{ github.ref }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🛎️ Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🛠️ Install Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: 🛠️ Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: 📦 Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- 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: apps/web/dist # The folder the action should deploy.
|
||||
@@ -1,31 +0,0 @@
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x, 16.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run build --if-present
|
||||
- run: npm test
|
||||
+4
-1
@@ -107,4 +107,7 @@ utils/xlsxTransform-*
|
||||
|
||||
/.idea
|
||||
|
||||
__localStorage.json
|
||||
__localStorage.json
|
||||
|
||||
template/public
|
||||
public/data
|
||||
Vendored
+8
-20
@@ -4,33 +4,21 @@
|
||||
// 欲了解更多信息,请访问: 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",
|
||||
"name": "test",
|
||||
"program": "${workspaceFolder}/test",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
"<node_internals>/**",
|
||||
"**/node_modumes/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "xlsxTransform",
|
||||
"program": "${workspaceFolder}/utils/xlsxTransform.js",
|
||||
"args": ["data"],
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}",
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node"
|
||||
},
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"js/ts.experimental.useTsgo": true
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+8
-51
@@ -1,62 +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 />
|
||||
|
||||
1. 下载项目代码。
|
||||
### 开发
|
||||
|
||||
```bash
|
||||
git clone https://github.com/VickScarlet/lifeRestart.git my-project
|
||||
cd my-project
|
||||
```
|
||||
|
||||
2. 进入目录安装依赖。
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
或者
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. 启动本地服务器。
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
或者
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. 启动完成后会自动打开浏览器访问 [http://localhost:8081/view/index.html](http://localhost:8081/view/index.html)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>控制台版本</strong></summary>
|
||||
<br />
|
||||
|
||||
```bash
|
||||
node repl
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
> 更多信息请参考 [官网文档](https://liferestart.syaro.io/)。
|
||||
pnpm build:data
|
||||
pnpm dev
|
||||
```
|
||||
@@ -1,61 +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 />
|
||||
|
||||
1. Clone project code.
|
||||
### Dev
|
||||
|
||||
```bash
|
||||
git clone git@github.com:VickScarlet/lifeRestart.git my-project
|
||||
cd my-project
|
||||
```
|
||||
|
||||
2. Installation dependence.
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
Or
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start local server.
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Or
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. After the startup is complete, will automatically open a browser and visit [http://localhost:8081/view/index.html](http://localhost:8081/view/index.html).
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Command Line Version</strong></summary>
|
||||
<br />
|
||||
|
||||
```bash
|
||||
node repl
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
> More instructions at [documentation](https://liferestart.syaro.io/).
|
||||
pnpm build:data
|
||||
pnpm dev
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
theme: jekyll-theme-cayman
|
||||
@@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ site.lang | default: "en-US" }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script data-ad-client="ca-pub-9857163863537600" async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
|
||||
{% seo %}
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link rel="preload" href="https://fonts.googleapis.com/css?family=Open+Sans:400,700&display=swap" as="style" type="text/css" crossorigin>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#157878">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="stylesheet" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
|
||||
{% include head-custom.html %}
|
||||
</head>
|
||||
<body>
|
||||
<a id="skip-to-content" href="#content">Skip to the content.</a>
|
||||
|
||||
<header class="page-header" role="banner">
|
||||
<h1 class="project-name">{{ page.title | default: site.title | default: site.github.repository_name }}</h1>
|
||||
<h2 class="project-tagline">{{ page.description | default: site.description | default: site.github.project_tagline }}</h2>
|
||||
<a href="/view/" class="btn">RESTART</a>
|
||||
<a href="/view/test.html" class="btn">Source Version</a>
|
||||
{% if site.github.is_project_page %}
|
||||
<a href="{{ site.github.repository_url }}" class="btn">View on GitHub</a>
|
||||
{% endif %}
|
||||
{% if site.show_downloads %}
|
||||
<a href="{{ site.github.zip_url }}" class="btn">Download .zip</a>
|
||||
<a href="{{ site.github.tar_url }}" class="btn">Download .tar.gz</a>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<main id="content" class="main-content" role="main">
|
||||
{{ content }}
|
||||
|
||||
<footer class="site-footer">
|
||||
{% if site.github.is_project_page %}
|
||||
<span class="site-footer-owner"><a href="{{ site.github.repository_url }}">{{ site.github.repository_name }}</a> is maintained by <a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a>.</span>
|
||||
{% endif %}
|
||||
<span class="site-footer-credits">This page was generated by <a href="https://pages.github.com">GitHub Pages</a>.</span>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,870 @@
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname } from 'path'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
import { readFile } from 'fs/promises'
|
||||
import Life from '../srco/modules/life.js'
|
||||
import $lang from '../srco/i18n/zh-cn.js'
|
||||
globalThis.$lang = $lang
|
||||
|
||||
globalThis.json = async fileName =>
|
||||
JSON.parse(await readFile(`${__dirname}/../public/data/${fileName}.json`))
|
||||
|
||||
globalThis.$$eventMap = new Map()
|
||||
globalThis.$$event = (tag, data) => {
|
||||
const listener = $$eventMap.get(tag)
|
||||
if (listener) listener.forEach(fn => fn(data))
|
||||
}
|
||||
globalThis.$$on = (tag, fn) => {
|
||||
let listener = $$eventMap.get(tag)
|
||||
if (!listener) {
|
||||
listener = new Set()
|
||||
$$eventMap.set(tag, listener)
|
||||
}
|
||||
listener.add(fn)
|
||||
}
|
||||
globalThis.$$off = (tag, fn) => {
|
||||
const listener = $$eventMap.get(tag)
|
||||
if (listener) listener.delete(fn)
|
||||
}
|
||||
|
||||
class App {
|
||||
constructor() {
|
||||
this.#life.config({
|
||||
defaultPropertyPoints: 20, // default number of points for a property
|
||||
talentSelectLimit: 3, // max number of talents that can be selected
|
||||
propertyAllocateLimit: [0, 10], // scoop of properties that can be allocated,
|
||||
defaultPropertys: { SPR: 5 }, // default properties
|
||||
talentConfig: {
|
||||
// config for talent
|
||||
talentPullCount: 10, // number of talents to pull from the talent pool
|
||||
talentRate: { 1: 100, 2: 10, 3: 1, total: 1000 }, // rate of talent pull
|
||||
additions: {
|
||||
TMS: [
|
||||
[10, { 2: 1 }],
|
||||
[30, { 2: 2 }],
|
||||
[50, { 2: 3 }],
|
||||
[70, { 2: 4 }],
|
||||
[100, { 2: 5 }],
|
||||
],
|
||||
CACHV: [
|
||||
[10, { 2: 1 }],
|
||||
[30, { 2: 2 }],
|
||||
[50, { 2: 3 }],
|
||||
[70, { 2: 4 }],
|
||||
[100, { 2: 5 }],
|
||||
],
|
||||
},
|
||||
},
|
||||
propertyConfig: {
|
||||
// config for property
|
||||
judge: {
|
||||
// type: [min, grade, judge]
|
||||
RTLT: [
|
||||
[0, 0],
|
||||
[0.3, 1],
|
||||
[0.6, 2],
|
||||
[0.9, 3],
|
||||
],
|
||||
REVT: [
|
||||
[0, 0],
|
||||
[0.2, 1],
|
||||
[0.4, 2],
|
||||
[0.6, 3],
|
||||
],
|
||||
TMS: [
|
||||
[0, 0, 'UI_Remake_Times_Judge_Level_0'],
|
||||
[10, 1, 'UI_Remake_Times_Judge_Level_1'],
|
||||
[30, 1, 'UI_Remake_Times_Judge_Level_2'],
|
||||
[50, 2, 'UI_Remake_Times_Judge_Level_3'],
|
||||
[70, 2, 'UI_Remake_Times_Judge_Level_4'],
|
||||
[100, 3, 'UI_Remake_Times_Judge_Level_5'],
|
||||
],
|
||||
CACHV: [
|
||||
[0, 0, 'UI_Achievement_Count_Judge_Level_0'],
|
||||
[10, 1, 'UI_Achievement_Count_Judge_Level_1'],
|
||||
[30, 1, 'UI_Achievement_Count_Judge_Level_2'],
|
||||
[50, 2, 'UI_Achievement_Count_Judge_Level_3'],
|
||||
[70, 2, 'UI_Achievement_Count_Judge_Level_4'],
|
||||
[100, 3, 'UI_Achievement_Count_Judge_Level_5'],
|
||||
],
|
||||
HCHR: [
|
||||
[0, 0, 'UI_Judge_Level_0'],
|
||||
[1, 0, 'UI_Judge_Level_1'],
|
||||
[2, 0, 'UI_Judge_Level_2'],
|
||||
[4, 0, 'UI_Judge_Level_3'],
|
||||
[7, 1, 'UI_Judge_Level_4'],
|
||||
[9, 2, 'UI_Judge_Level_5'],
|
||||
[11, 3, 'UI_Judge_Level_6'],
|
||||
],
|
||||
HMNY: [
|
||||
[0, 0, 'UI_Judge_Level_0'],
|
||||
[1, 0, 'UI_Judge_Level_1'],
|
||||
[2, 0, 'UI_Judge_Level_2'],
|
||||
[4, 0, 'UI_Judge_Level_3'],
|
||||
[7, 1, 'UI_Judge_Level_4'],
|
||||
[9, 2, 'UI_Judge_Level_5'],
|
||||
[11, 3, 'UI_Judge_Level_6'],
|
||||
],
|
||||
HSPR: [
|
||||
[0, 0, 'UI_Spirit_Judge_Level_0'],
|
||||
[1, 0, 'UI_Spirit_Judge_Level_1'],
|
||||
[2, 0, 'UI_Spirit_Judge_Level_2'],
|
||||
[4, 0, 'UI_Spirit_Judge_Level_3'],
|
||||
[7, 1, 'UI_Spirit_Judge_Level_4'],
|
||||
[9, 2, 'UI_Spirit_Judge_Level_5'],
|
||||
[11, 3, 'UI_Spirit_Judge_Level_6'],
|
||||
],
|
||||
HINT: [
|
||||
[0, 0, 'UI_Judge_Level_0'],
|
||||
[1, 0, 'UI_Judge_Level_1'],
|
||||
[2, 0, 'UI_Judge_Level_2'],
|
||||
[4, 0, 'UI_Judge_Level_3'],
|
||||
[7, 1, 'UI_Judge_Level_4'],
|
||||
[9, 2, 'UI_Judge_Level_5'],
|
||||
[11, 3, 'UI_Judge_Level_6'],
|
||||
[21, 3, 'UI_Intelligence_Judge_Level_7'],
|
||||
[131, 3, 'UI_Intelligence_Judge_Level_8'],
|
||||
[501, 3, 'UI_Intelligence_Judge_Level_9'],
|
||||
],
|
||||
HSTR: [
|
||||
[0, 0, 'UI_Judge_Level_0'],
|
||||
[1, 0, 'UI_Judge_Level_1'],
|
||||
[2, 0, 'UI_Judge_Level_2'],
|
||||
[4, 0, 'UI_Judge_Level_3'],
|
||||
[7, 1, 'UI_Judge_Level_4'],
|
||||
[9, 2, 'UI_Judge_Level_5'],
|
||||
[11, 3, 'UI_Judge_Level_6'],
|
||||
[21, 3, 'UI_Strength_Judge_Level_7'],
|
||||
[101, 3, 'UI_Strength_Judge_Level_8'],
|
||||
[401, 3, 'UI_Strength_Judge_Level_9'],
|
||||
[1001, 3, 'UI_Strength_Judge_Level_10'],
|
||||
[2001, 3, 'UI_Strength_Judge_Level_11'],
|
||||
],
|
||||
HAGE: [
|
||||
[0, 0, 'UI_AGE_Judge_Level_0'],
|
||||
[1, 0, 'UI_AGE_Judge_Level_1'],
|
||||
[10, 0, 'UI_AGE_Judge_Level_2'],
|
||||
[18, 0, 'UI_AGE_Judge_Level_3'],
|
||||
[40, 0, 'UI_AGE_Judge_Level_4'],
|
||||
[60, 1, 'UI_AGE_Judge_Level_5'],
|
||||
[70, 1, 'UI_AGE_Judge_Level_6'],
|
||||
[80, 2, 'UI_AGE_Judge_Level_7'],
|
||||
[90, 2, 'UI_AGE_Judge_Level_8'],
|
||||
[95, 3, 'UI_AGE_Judge_Level_9'],
|
||||
[100, 3, 'UI_AGE_Judge_Level_10'],
|
||||
[500, 3, 'UI_AGE_Judge_Level_11'],
|
||||
],
|
||||
SUM: [
|
||||
[0, 0, 'UI_Judge_Level_0'],
|
||||
[41, 0, 'UI_Judge_Level_1'],
|
||||
[50, 0, 'UI_Judge_Level_2'],
|
||||
[60, 0, 'UI_Judge_Level_3'],
|
||||
[80, 1, 'UI_Judge_Level_4'],
|
||||
[100, 2, 'UI_Judge_Level_5'],
|
||||
[110, 3, 'UI_Judge_Level_6'],
|
||||
[120, 3, 'UI_Judge_Level_7'],
|
||||
],
|
||||
},
|
||||
},
|
||||
characterConfig: {
|
||||
// config for character
|
||||
characterPullCount: 3,
|
||||
rateableKnife: 10,
|
||||
propertyWeight: [
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[3, 4],
|
||||
[4, 5],
|
||||
[5, 6],
|
||||
[6, 5],
|
||||
[7, 4],
|
||||
[8, 3],
|
||||
[9, 2],
|
||||
[10, 1],
|
||||
],
|
||||
talentWeight: [
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
[4, 2],
|
||||
[5, 1],
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Steps = {
|
||||
TALENT: 'talent',
|
||||
PROPERTY: 'property',
|
||||
TRAJECTORY: 'trajectory',
|
||||
SUMMARY: 'summary',
|
||||
}
|
||||
#language = 'zh-cn'
|
||||
#step = this.Steps.SUMMARY
|
||||
#life = new Life()
|
||||
#talentSelected = new Set()
|
||||
#talentExtend = new Set()
|
||||
#auto
|
||||
#isEnd
|
||||
#propertyAllocation
|
||||
#output
|
||||
#exit
|
||||
#interval
|
||||
#style = {
|
||||
warn: ['\x1B[93m', '\x1B[39m'], // Bright Yellow
|
||||
grade1: ['\x1B[94m', '\x1B[39m'], // Bright Blue
|
||||
grade2: ['\x1B[95m', '\x1B[39m'], // Bright Magenta
|
||||
grade3: ['\x1B[93m', '\x1B[39m'], // Bright Yellow
|
||||
grade1b: ['\x1B[94m\x1B[7m', '\x1B[0m'], // Bright Blue BG
|
||||
grade2b: ['\x1B[95m\x1B[7m', '\x1B[0m'], // Bright Magenta BG
|
||||
grade3b: ['\x1B[93m\x1B[7m', '\x1B[0m'], // Bright Yellow BG
|
||||
}
|
||||
#randomTalents
|
||||
|
||||
style(type, str) {
|
||||
const style = this.#style[type]
|
||||
if (!style) return str
|
||||
return `${style[0]}${str}${style[1]}`
|
||||
}
|
||||
|
||||
async initial() {
|
||||
this.output('Now Loading...')
|
||||
this.#talentExtend = localStorage.talentExtend
|
||||
await this.#life.initial(
|
||||
dataSet => json(`${this.#language}/${dataSet}`),
|
||||
json,
|
||||
)
|
||||
this.output(
|
||||
`\rLoading Complete.
|
||||
人生重开模拟器
|
||||
这垃圾人生一秒也不想待了
|
||||
|
||||
键入 \x1B[4m/remake\x1B[24m 开始游戏
|
||||
键入 \x1B[4m/help\x1B[24m 获取帮助`,
|
||||
true,
|
||||
)
|
||||
$$on('achievement', ({ name }) =>
|
||||
this.output(`
|
||||
-------------------------
|
||||
解锁成就【${name}】
|
||||
-------------------------
|
||||
`),
|
||||
)
|
||||
}
|
||||
|
||||
io(input, output, exit) {
|
||||
this.#output = output
|
||||
this.#exit = exit
|
||||
input(command => {
|
||||
const ret = this.repl(command)
|
||||
if (!ret) return
|
||||
if (typeof ret == 'string') return this.output(ret, true)
|
||||
if (Array.isArray(ret)) return this.output(...ret)
|
||||
const { message, isRepl } = ret
|
||||
return this.output(message, isRepl)
|
||||
})
|
||||
}
|
||||
|
||||
output(data, isRepl) {
|
||||
if (!this.#output) return
|
||||
this.#output(data, isRepl)
|
||||
}
|
||||
|
||||
exit(code) {
|
||||
if (this.#exit) this.#exit(code)
|
||||
process.exit(code)
|
||||
}
|
||||
|
||||
repl(command) {
|
||||
command = command.split(/\s+/)
|
||||
switch (command.shift()) {
|
||||
case 'r':
|
||||
case 'remake':
|
||||
case '/remake':
|
||||
return this.remake()
|
||||
|
||||
case 's':
|
||||
case 'select':
|
||||
case '/select':
|
||||
return this.select(...command)
|
||||
|
||||
case 'u':
|
||||
case 'unselect':
|
||||
case '/unselect':
|
||||
return this.unselect(...command)
|
||||
|
||||
case 'n':
|
||||
case 'next':
|
||||
case '/next':
|
||||
case '':
|
||||
return this.next(true)
|
||||
|
||||
case 'a':
|
||||
case 'alloc':
|
||||
case 'allocate':
|
||||
case 'attrib':
|
||||
case 'attribute':
|
||||
case '/alloc':
|
||||
case '/allocate':
|
||||
case '/attrib':
|
||||
case '/attribute':
|
||||
return this.attrib(...command)
|
||||
|
||||
case 'rd':
|
||||
case 'random':
|
||||
case '/random':
|
||||
return this.random()
|
||||
|
||||
case 'at':
|
||||
case 'auto':
|
||||
case '/auto':
|
||||
return this.auto(...command)
|
||||
|
||||
case 'x':
|
||||
case 'exit':
|
||||
case '/exit':
|
||||
return this.exit(0)
|
||||
|
||||
case '?':
|
||||
case 'h':
|
||||
case 'help':
|
||||
case '/?':
|
||||
case '/h':
|
||||
case '/help':
|
||||
default:
|
||||
return this.help(...command)
|
||||
}
|
||||
}
|
||||
|
||||
help(key) {
|
||||
switch (key) {
|
||||
case 'x':
|
||||
case 'exit':
|
||||
case '/exit':
|
||||
return `退出
|
||||
x, exit, /exit 命令同等效果`
|
||||
|
||||
case 'r':
|
||||
case 'remake':
|
||||
case '/remake':
|
||||
return `重开
|
||||
r, remake, /remake 命令同等效果`
|
||||
|
||||
case 's':
|
||||
case 'select':
|
||||
case '/select':
|
||||
return `选择
|
||||
s, select, /select 命令同等效果
|
||||
|
||||
Example: /select 1 2 3 意味着选择 1 2 3 三个天赋
|
||||
|
||||
/select <id1> [id2] [id3]
|
||||
|
||||
参数解释 <id1> 通常来说要指定至少一个id
|
||||
虽然不指定也可以
|
||||
[id2]
|
||||
[id3] 可以不指定`
|
||||
|
||||
case 'u':
|
||||
case 'unselect':
|
||||
case '/unselect':
|
||||
return `取消选择
|
||||
u, unselect,
|
||||
/unselect 命令同等效果
|
||||
|
||||
Example: /unselect 1 2 3
|
||||
意味着取消选择 1 2 3 三个天赋
|
||||
|
||||
参数解释 /unselect <id1> [id2] [id3]
|
||||
|
||||
<id1> 通常来说要指定至少一个id
|
||||
虽然不指定也可以
|
||||
[id2]
|
||||
[id3] 可以不指定`
|
||||
|
||||
case 'a':
|
||||
case 'alloc':
|
||||
case 'allocate':
|
||||
case 'attrib':
|
||||
case 'attribute':
|
||||
case '/alloc':
|
||||
case '/allocate':
|
||||
case '/attrib':
|
||||
case '/attribute':
|
||||
return `分配或查看属性点
|
||||
a, alloc, allocate, attrib, attribute
|
||||
/alloc, /allocate, /attrib, /attribute 命令同等效果
|
||||
|
||||
Example: /attribute
|
||||
/allocate STR 1
|
||||
/allocate INT -3
|
||||
/allocate CHR +5
|
||||
|
||||
效果 在属性分配时分配属性点
|
||||
在人生的过程中查看当前属性点
|
||||
|
||||
参数解释 /allocate <TAG> <[+/-]value>
|
||||
|
||||
<TAG> 表示要分配的属性标签
|
||||
可选有
|
||||
CHR, chr, c, C 表示颜值
|
||||
INT, int, i, I 表示智力
|
||||
STR, str, s, S 表示体质
|
||||
MNY, mny, m, M 表示家境
|
||||
必填
|
||||
|
||||
<[+/-]value>
|
||||
表示属性的调整
|
||||
其中
|
||||
+ 表示在当前基础上增加
|
||||
- 表示在当前基础上减少
|
||||
无符号表示直接设置为此值
|
||||
必填`
|
||||
|
||||
case 'n':
|
||||
case 'next':
|
||||
case '/next':
|
||||
return `继续
|
||||
n, next, /next 命令同等效果
|
||||
|
||||
效果 通常用于各步骤结束后
|
||||
例如: 选择天赋后
|
||||
分配属性后
|
||||
每个年龄事件后
|
||||
总评后
|
||||
继承天赋后`
|
||||
|
||||
case 'at':
|
||||
case 'auto':
|
||||
case '/auto':
|
||||
return `自动播放
|
||||
at, auto, /auto 命令同等效果
|
||||
|
||||
效果 用于人生的过程中
|
||||
每个年龄会自动下一年
|
||||
播放速度 1 秒 1 年`
|
||||
|
||||
case '?':
|
||||
case 'h':
|
||||
case 'help':
|
||||
case '/?':
|
||||
case '/h':
|
||||
case '/help':
|
||||
return `显示帮助
|
||||
?, h, help
|
||||
/?, /h, /help 命令同等效果
|
||||
|
||||
Example: /help
|
||||
/help /select
|
||||
|
||||
参数解释 /help [command]
|
||||
|
||||
[command] 要详细显示帮助的命令
|
||||
可以不填`
|
||||
}
|
||||
return `Help ---
|
||||
命令 说明 示例
|
||||
x
|
||||
exit
|
||||
/exit 退出 /exit
|
||||
|
||||
r
|
||||
remake
|
||||
/remake 重开 /remake
|
||||
|
||||
s
|
||||
select
|
||||
/select 选择天赋 /select <id1> [id2] [id3]
|
||||
|
||||
u
|
||||
unselect
|
||||
/unselect 取消选择 /unselect <id1> [id2] [id3]
|
||||
|
||||
a
|
||||
alloc
|
||||
allocate
|
||||
attrib
|
||||
attribute
|
||||
/alloc
|
||||
/allocate
|
||||
/attrib
|
||||
/attribute 分配或查看属性点 /allocate <TAG> <[+/-]value>
|
||||
|
||||
n
|
||||
next
|
||||
/next 继续 /next
|
||||
|
||||
at
|
||||
auto
|
||||
/auto 自动播放 /auto
|
||||
|
||||
?
|
||||
h
|
||||
help
|
||||
/?
|
||||
/h
|
||||
/help 显示帮助 /help [command]`
|
||||
}
|
||||
|
||||
auto(arg) {
|
||||
this.#auto = arg != 'off'
|
||||
return this.next(true)
|
||||
}
|
||||
|
||||
remake() {
|
||||
if (this.#talentExtend) {
|
||||
this.#life.talentExtend(this.#talentExtend)
|
||||
dumpLocalStorage()
|
||||
this.#talentExtend = null
|
||||
}
|
||||
|
||||
this.#isEnd = false
|
||||
this.#talentSelected.clear()
|
||||
this.#propertyAllocation = {
|
||||
CHR: 0,
|
||||
INT: 0,
|
||||
STR: 0,
|
||||
MNY: 0,
|
||||
SPR: 5,
|
||||
}
|
||||
this.#step = this.Steps.TALENT
|
||||
this.#randomTalents = this.#life.talentRandom()
|
||||
return this.list()
|
||||
}
|
||||
|
||||
select(...select) {
|
||||
switch (this.#step) {
|
||||
case this.Steps.TALENT:
|
||||
return this.talentSelect(...select)
|
||||
case this.Steps.SUMMARY:
|
||||
return this.talentExtend(...select)
|
||||
}
|
||||
}
|
||||
|
||||
unselect(...select) {
|
||||
switch (this.#step) {
|
||||
case this.Steps.TALENT:
|
||||
return this.talentUnSelect(...select)
|
||||
case this.Steps.SUMMARY:
|
||||
return this.talentExtendCancle(...select)
|
||||
}
|
||||
}
|
||||
|
||||
talentSelect(...select) {
|
||||
const warn = str => `${this.list()}\n${this.style('warn', str)}`
|
||||
for (const number of select) {
|
||||
const s = this.#randomTalents[number]
|
||||
if (!s) return warn(`${number} 为未知天赋`)
|
||||
if (this.#talentSelected.has(s)) continue
|
||||
if (this.#talentSelected.size == 3)
|
||||
return warn(
|
||||
'你只能选3个天赋。请使用 \x1B[4m/unselect\x1B[24m 取消选择你不想要的天赋',
|
||||
)
|
||||
|
||||
const exclusive = this.#life.exclude(
|
||||
Array.from(this.#talentSelected).map(({ id }) => id),
|
||||
s.id,
|
||||
)
|
||||
|
||||
if (exclusive != null)
|
||||
for (const { name, id } of this.#talentSelected)
|
||||
if (id == exclusive)
|
||||
return warn(
|
||||
`天赋【${s.name}】与已选择的天赋【${name}】冲突`,
|
||||
)
|
||||
|
||||
this.#talentSelected.add(s)
|
||||
}
|
||||
|
||||
return this.list()
|
||||
}
|
||||
|
||||
talentUnSelect(...select) {
|
||||
for (const number of select) {
|
||||
const s = this.#randomTalents[number]
|
||||
if (this.#talentSelected.has(s)) this.#talentSelected.delete(s)
|
||||
}
|
||||
return this.list()
|
||||
}
|
||||
|
||||
talentExtend(select) {
|
||||
const warn = str => `${this.list()}\n${this.style('warn', str)}`
|
||||
const list = Array.from(this.#talentSelected)
|
||||
const s = list[select]
|
||||
if (!s) return warn(`${select} 为未知天赋`)
|
||||
this.#talentExtend = s.id
|
||||
return this.list()
|
||||
}
|
||||
|
||||
talentExtendCancle() {
|
||||
this.#talentExtend = null
|
||||
return this.list()
|
||||
}
|
||||
|
||||
list() {
|
||||
let description, list, check
|
||||
switch (this.#step) {
|
||||
case this.Steps.TALENT:
|
||||
description = '🎉 请选择(\x1B[4m/select\x1B[24m)3 个天赋'
|
||||
list = this.#randomTalents
|
||||
check = talent => this.#talentSelected.has(talent)
|
||||
break
|
||||
case this.Steps.SUMMARY:
|
||||
description =
|
||||
'🎉 你可以选(\x1B[4m/select\x1B[24m)一个天赋继承'
|
||||
list = Array.from(this.#talentSelected)
|
||||
check = ({ id }) => this.#talentExtend == id
|
||||
break
|
||||
}
|
||||
if (!list) return ''
|
||||
|
||||
return [
|
||||
description,
|
||||
list.map((talent, i) =>
|
||||
this.style(
|
||||
`grade${talent.grade}b`,
|
||||
`${check(talent) ? '√' : ' '} ${i} ${talent.name}(${
|
||||
talent.description
|
||||
})`,
|
||||
),
|
||||
),
|
||||
]
|
||||
.flat()
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
next(enter) {
|
||||
const warn = (a, b) =>
|
||||
`${a}\n${this.style('warn', this.style('warn', b))}`
|
||||
switch (this.#step) {
|
||||
case this.Steps.TALENT:
|
||||
if (this.#talentSelected.size != 3)
|
||||
return warn(this.list(), `请选择 3 个天赋`)
|
||||
this.#step = this.Steps.PROPERTY
|
||||
this.#life.remake(
|
||||
Array.from(this.#talentSelected).map(({ id }) => id),
|
||||
)
|
||||
this.#propertyAllocation.total = this.#life.getPropertyPoints()
|
||||
return this.prop()
|
||||
case this.Steps.PROPERTY:
|
||||
const less = this.less()
|
||||
if (less > 0)
|
||||
return warn(this.prop(), `你还有 ${less} 属性点没有分配完`)
|
||||
this.#step = this.Steps.TRAJECTORY
|
||||
delete this.#propertyAllocation.total
|
||||
this.#life.start(this.#propertyAllocation)
|
||||
return this.trajectory(enter)
|
||||
case this.Steps.TRAJECTORY:
|
||||
if (!this.#isEnd) return this.trajectory(enter)
|
||||
this.#step = this.Steps.SUMMARY
|
||||
return `${this.summary()}\n\n${this.list()}`
|
||||
case this.Steps.SUMMARY:
|
||||
return this.remake()
|
||||
}
|
||||
}
|
||||
|
||||
trajectory(enter) {
|
||||
if (enter) {
|
||||
if (this.#interval) {
|
||||
clearInterval(this.#interval)
|
||||
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,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const trajectory = this.#life.next()
|
||||
const { age, content, isEnd } = trajectory
|
||||
if (isEnd) this.#isEnd = true
|
||||
return `${age}岁:\t${content
|
||||
.map(({ type, description, grade, name, postEvent }) => {
|
||||
switch (type) {
|
||||
case 'TLT':
|
||||
return `天赋【${name}】发动:${description}`
|
||||
case 'EVT':
|
||||
return (
|
||||
description + (postEvent ? `\n\t${postEvent}` : '')
|
||||
)
|
||||
}
|
||||
})
|
||||
.join('\n\t')}`
|
||||
}
|
||||
|
||||
prop() {
|
||||
const { CHR, INT, STR, MNY } = this.#propertyAllocation
|
||||
return `🎉 属性分配
|
||||
请使用 \x1B[4m/alloc\x1B[24m <TAG> <value> 分配属性
|
||||
剩余点数 ${this.less()}
|
||||
|
||||
属性(TAG) 当前值
|
||||
颜值(CHR) ${CHR}
|
||||
智力(INT) ${INT}
|
||||
体质(STR) ${STR}
|
||||
家境(MNY) ${MNY}
|
||||
`
|
||||
}
|
||||
|
||||
less() {
|
||||
const { total, CHR, INT, STR, MNY } = this.#propertyAllocation
|
||||
return total - CHR - INT - STR - MNY
|
||||
}
|
||||
|
||||
attrib(tag, value) {
|
||||
switch (this.#step) {
|
||||
case this.Steps.PROPERTY:
|
||||
return this.alloc(tag, value)
|
||||
|
||||
case this.Steps.TRAJECTORY:
|
||||
return this.showProperty()
|
||||
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
showProperty() {
|
||||
let property = this.#life.getLastRecord()
|
||||
return `当前属性
|
||||
|
||||
属性(TAG) 当前值
|
||||
颜值(CHR) ${property.CHR}
|
||||
智力(INT) ${property.INT}
|
||||
体质(STR) ${property.STR}
|
||||
家境(MNY) ${property.MNY}
|
||||
快乐(SPR) ${property.SPR}`
|
||||
}
|
||||
|
||||
alloc(tag, value) {
|
||||
const warn = str => `${this.prop()}\n${this.style('warn', str)}`
|
||||
if (!value) return warn('⚠ 分配的数值没有给定')
|
||||
const isSet = !(value[0] == '-' || value[0] == '+')
|
||||
|
||||
value = Number(value)
|
||||
if (isNaN(value)) return warn('⚠ 分配的数值不正确')
|
||||
|
||||
switch (tag) {
|
||||
case 'c':
|
||||
case 'chr':
|
||||
case 'C':
|
||||
tag = 'CHR'
|
||||
break
|
||||
case 'i':
|
||||
case 'int':
|
||||
case 'I':
|
||||
tag = 'INT'
|
||||
break
|
||||
case 's':
|
||||
case 'S':
|
||||
case 'str':
|
||||
tag = 'STR'
|
||||
break
|
||||
case 'm':
|
||||
case 'M':
|
||||
case 'mny':
|
||||
tag = 'MNY'
|
||||
break
|
||||
}
|
||||
|
||||
switch (tag) {
|
||||
case 'CHR':
|
||||
case 'INT':
|
||||
case 'STR':
|
||||
case 'MNY':
|
||||
break
|
||||
default:
|
||||
return warn('⚠ 未知的tag')
|
||||
}
|
||||
|
||||
if (isSet) value = value - this.#propertyAllocation[tag]
|
||||
|
||||
const tempLess = this.less() - value
|
||||
const tempSet = this.#propertyAllocation[tag] + value
|
||||
|
||||
if (tempLess < 0) return warn('⚠ 你没有更多的点数可以分配了')
|
||||
if (tempLess > this.#propertyAllocation.total || tempSet < 0)
|
||||
return warn('⚠ 不能分配负数属性')
|
||||
if (tempSet > 10) return warn('⚠ 单项属性最高分配10点')
|
||||
|
||||
this.#propertyAllocation[tag] += value
|
||||
|
||||
return this.prop()
|
||||
}
|
||||
|
||||
random() {
|
||||
switch (this.#step) {
|
||||
case this.Steps.TALENT:
|
||||
this.#talentSelected.clear()
|
||||
Array.from(this.#randomTalents)
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, 3)
|
||||
.forEach(talent => {
|
||||
this.#talentSelected.add(talent)
|
||||
})
|
||||
return this.list()
|
||||
case this.Steps.PROPERTY:
|
||||
let t = this.#propertyAllocation.total
|
||||
const arr = [10, 10, 10, 10]
|
||||
while (t > 0) {
|
||||
const sub =
|
||||
Math.round(Math.random() * (Math.min(t, 10) - 1)) + 1
|
||||
while (true) {
|
||||
const select = Math.floor(Math.random() * 4) % 4
|
||||
if (arr[select] - sub < 0) continue
|
||||
arr[select] -= sub
|
||||
t -= sub
|
||||
break
|
||||
}
|
||||
}
|
||||
this.#propertyAllocation.CHR = 10 - arr[0]
|
||||
this.#propertyAllocation.INT = 10 - arr[1]
|
||||
this.#propertyAllocation.STR = 10 - arr[2]
|
||||
this.#propertyAllocation.MNY = 10 - arr[3]
|
||||
return this.prop()
|
||||
case this.Steps.SUMMARY:
|
||||
this.#talentExtend = Array.from(this.#talentSelected).sort(
|
||||
() => Math.random() - 0.5,
|
||||
)[0].id
|
||||
return this.list()
|
||||
}
|
||||
}
|
||||
|
||||
summary() {
|
||||
const summaryData = this.#life.summary
|
||||
const format = (name, type) => {
|
||||
const { judge, grade, value } = summaryData[type]
|
||||
return this.style(
|
||||
`grade${grade}b`,
|
||||
`${name}:${value} ${$lang[judge]}`,
|
||||
)
|
||||
}
|
||||
|
||||
return [
|
||||
'🎉 总评',
|
||||
format('颜值', this.#life.PropertyTypes.HCHR),
|
||||
format('智力', this.#life.PropertyTypes.HINT),
|
||||
format('体质', this.#life.PropertyTypes.HSTR),
|
||||
format('家境', this.#life.PropertyTypes.HMNY),
|
||||
format('快乐', this.#life.PropertyTypes.HSPR),
|
||||
format('享年', this.#life.PropertyTypes.HAGE),
|
||||
format('总评', this.#life.PropertyTypes.SUM),
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,44 @@
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname } from 'path'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
import App from './app.js'
|
||||
import { readFile, writeFile } from 'fs/promises'
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
globalThis.localStorage = JSON.parse(
|
||||
await readFile(__dirname + '/__localStorage.json')
|
||||
)
|
||||
} catch (e) {
|
||||
globalThis.localStorage = {}
|
||||
}
|
||||
localStorage.getItem = key =>
|
||||
localStorage[key] === void 0 ? null : localStorage[key]
|
||||
localStorage.setItem = (key, value) => (localStorage[key] = value)
|
||||
|
||||
globalThis.dumpLocalStorage = async () =>
|
||||
await writeFile(
|
||||
__dirname + '/__localStorage.json',
|
||||
JSON.stringify(global.localStorage)
|
||||
)
|
||||
|
||||
const app = new App()
|
||||
app.io(
|
||||
repl => process.stdin.on('data', data => repl(data.toString().trim())),
|
||||
(data, isRepl) => process.stdout.write(`${data}${isRepl ? '\n>' : ''}`),
|
||||
code => process.exit(code)
|
||||
)
|
||||
await app.initial()
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
// process.stdin.setRawMode(true);
|
||||
|
||||
// process.openStdin().on('keypress', function (chunk, key) {
|
||||
// process.stdout.write('Get Chunk: ' + chunk + '\n');
|
||||
// if (key && key.ctrl && key.name == 'c') process.exit();
|
||||
// });
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -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,
|
||||
])
|
||||
@@ -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>
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"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",
|
||||
"preview": "bunx --bun vite preview",
|
||||
"lint": "bunx --bun eslint",
|
||||
"test": "bunx --bun vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remake/data": "workspace:*",
|
||||
"@remake/hooks": "workspace:*",
|
||||
"@remake/vitex": "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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
.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; }
|
||||
}
|
||||
}
|
||||
.saving {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
user-select: none;
|
||||
transition: opacity 0.3s;
|
||||
&.active {
|
||||
opacity: 1;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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); }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import './Github.css'
|
||||
export function Github() {
|
||||
return (
|
||||
<button
|
||||
className="github-btn"
|
||||
onClick={() => window.open('https://github.com/VickScarlet/remake')}
|
||||
>
|
||||
<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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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); }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,119 @@
|
||||
.screen.achv {
|
||||
height: calc(100dvh - 2rem);
|
||||
width: min(calc(100dvw - 2rem), 48rem);
|
||||
gap: 1rem;
|
||||
> .stats {
|
||||
> .details {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 0.0625rem solid var(--grade-color);
|
||||
color: var(--grade-color);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
user-select: none;
|
||||
> span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
> .title {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
> .value {
|
||||
height: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
> .prg {
|
||||
padding-bottom: 0.1875rem;
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 0.1875rem;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
transform: translateZ(-1px);
|
||||
}
|
||||
&::after {
|
||||
width: var(--progress, 0%);
|
||||
background: var(--grade-color);
|
||||
transition: width 0.2s ease-in-out;
|
||||
}
|
||||
&::before {
|
||||
width: 100%;
|
||||
border-top: 0.0625rem solid var(--grade-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
> .achievement-list {
|
||||
flex: 1 0;
|
||||
width: 100%;
|
||||
> ul {
|
||||
flex: 1 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
> li.achievement {
|
||||
margin-right: 0.0625rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--grade-color);
|
||||
border: 0.0625rem solid var(--grade-color);
|
||||
padding: 0 0.0625rem;
|
||||
box-sizing: border-box;
|
||||
transform-style: preserve-3d;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
>.name {
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
>.description {
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
&::before{
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: 0.0625rem;
|
||||
margin: -0.0625rem;
|
||||
transform: translateZ(1px);
|
||||
background: var(--base-background-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
&.colled {
|
||||
&::before{
|
||||
transform: translateZ(-1px);
|
||||
background: var(--grade-color);
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
@@ -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%; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
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)
|
||||
console.log('Achievements', 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
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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: ']'; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
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)
|
||||
console.debug('Achievements:', achievements)
|
||||
}, [ended, next])
|
||||
const handleGotoSummary = useCallback(() => {
|
||||
if (!ended) return
|
||||
const achievements = gotoSummary()
|
||||
toastAchvs(achievements)
|
||||
console.debug('Achievements:', 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
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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)
|
||||
console.log('Achievements', 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
.screen.thanks {
|
||||
height: calc(100dvh - 2rem);
|
||||
gap: 1rem;
|
||||
.person {
|
||||
--style-color: var(--custom-color, var(--base-text-color));
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--style-color);
|
||||
background: var(--base-background-color);
|
||||
transition: background 0.2s ease-in-out, color 0.2s ease-in-out;
|
||||
> .name {
|
||||
display: flex;
|
||||
text-wrap: wrap;
|
||||
word-break: break-all;
|
||||
min-height: 2.5rem;
|
||||
width: 12rem;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
align-items: center;
|
||||
text-shadow:
|
||||
-0.0625rem -0.0625rem 0.0625rem var(--base-shadow),
|
||||
0.0625rem -0.0625rem 0.0625rem var(--base-shadow),
|
||||
-0.0625rem 0.0625rem 0.0625rem var(--base-shadow),
|
||||
0.0625rem 0.0625rem 0.0625rem var(--base-shadow),
|
||||
0 -0.0625rem 0.0625rem var(--base-shadow),
|
||||
0 0.0625rem 0.0625rem var(--base-shadow),
|
||||
-0.0625rem 0 0.0625rem var(--base-shadow),
|
||||
0.0625rem 0 0.0625rem var(--base-shadow);
|
||||
}
|
||||
> .comment {
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
min-height: 2.5rem;
|
||||
flex: 1 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-shadow:
|
||||
-0.0625rem -0.0625rem 0.0625rem var(--base-shadow),
|
||||
0.0625rem -0.0625rem 0.0625rem var(--base-shadow),
|
||||
-0.0625rem 0.0625rem 0.0625rem var(--base-shadow),
|
||||
0.0625rem 0.0625rem 0.0625rem var(--base-shadow),
|
||||
0 -0.0625rem 0.0625rem var(--base-shadow),
|
||||
0 0.0625rem 0.0625rem var(--base-shadow),
|
||||
-0.0625rem 0 0.0625rem var(--base-shadow),
|
||||
0.0625rem 0 0.0625rem var(--base-shadow);
|
||||
}
|
||||
}
|
||||
> .group-1 {
|
||||
flex: 1 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
border: 0.0625rem solid var(--base-text-color);
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
> .person {
|
||||
margin: 0 0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
border: 0.0625rem solid var(--style-color);
|
||||
box-sizing: border-box;
|
||||
> .comment {
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
min-height: 2.5rem;
|
||||
flex: 1 0;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
> .group-2 {
|
||||
flex: 3 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
border: 0.0625rem solid var(--base-text-color);
|
||||
> .person {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-wrap: wrap;
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
margin-right: 0.0625rem;
|
||||
> .name { justify-content: center; }
|
||||
> .comment {display: none;}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useGoHome } from '@remake/hooks'
|
||||
import { specialThanks, SpecialThanksGroup as Group } from '@remake/data'
|
||||
import type { SpecialThanks } from '@remake/data'
|
||||
import { shuffle } from '@remake/vitex'
|
||||
import './Thanks.css'
|
||||
|
||||
const groups = Map.groupBy(specialThanks, item => item.group)
|
||||
|
||||
export function Person({ item }: { item: SpecialThanks }) {
|
||||
return (
|
||||
<li
|
||||
className="person"
|
||||
style={
|
||||
{
|
||||
'--custom-color': item.color,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<span className="name">{item.name}</span>
|
||||
{item.comment && <span className="comment">{item.comment}</span>}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export function Thanks() {
|
||||
const goHome = useGoHome()
|
||||
const group1 = shuffle(groups.get(Group.G1) ?? [])
|
||||
const group2 = shuffle(groups.get(Group.G2) ?? [])
|
||||
return (
|
||||
<div className="screen thanks">
|
||||
<button className="primary" onClick={goHome}>
|
||||
返回
|
||||
</button>
|
||||
<ul className="group group-1 hide-scrollbar">
|
||||
{group1.map((item, index) => (
|
||||
<Person key={index} item={item} />
|
||||
))}
|
||||
</ul>
|
||||
<ul className="group group-2 hide-scrollbar">
|
||||
{group2.map((item, index) => (
|
||||
<Person key={index} item={item} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Thanks
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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 { 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)
|
||||
const { profile, unique } = await get(['profile', 'unique'])
|
||||
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 || []),
|
||||
})
|
||||
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
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
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 extends string>(keys: Key[]) {
|
||||
return Object.fromEntries(keys.map(k => [k, localStorage.getItem(k)])) as {
|
||||
[K in Key]: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export async function set<Key extends string>(data: Record<Key, string>) {
|
||||
for (const key in data) {
|
||||
localStorage.setItem(key, data[key])
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -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); }
|
||||
@@ -0,0 +1,125 @@
|
||||
: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; }
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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,
|
||||
})
|
||||
console.log('toast offset', toast.id, offset)
|
||||
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
|
||||
console.log('toast height', toast.id, 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' })
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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,
|
||||
})
|
||||
console.log('toast offset', toast.id, offset)
|
||||
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
|
||||
console.log('toast height', toast.id, 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' })
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"jsx": "react-jsx",
|
||||
"types": ["react", "vite/client"],
|
||||
"jsxImportSource": "react",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
},
|
||||
},
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
const dataSplitRule = {
|
||||
test: /[\\/]packages[\\/]data[\\/]|@remake\/data/,
|
||||
priority: 40,
|
||||
name(id: string) {
|
||||
const name = id.split(/[\/\\]/).pop()!
|
||||
const baseName = name.substring(0, name.lastIndexOf('.'))
|
||||
if (baseName && baseName !== 'index') return `data-${baseName}`
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: { tsconfigPaths: true },
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1500,
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
codeSplitting: {
|
||||
minSize: 1024,
|
||||
groups: [dataSplitRule],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
-504
File diff suppressed because one or more lines are too long
-57046
File diff suppressed because it is too large
Load Diff
Binary file not shown.
-1572
File diff suppressed because it is too large
Load Diff
-12263
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,133 +0,0 @@
|
||||
$id,name,description,condition,grade,status,effect:SPR,effect:MNY,effect:CHR,effect:STR,effect:INT,exclusive[],exclusive[],exclusive[],exclusive[],exclusive[],exclusive[]
|
||||
序号,天赋名,括号中的内容,触发条件,稀有度,初始可用属性点,额外快乐,额外家境,额外颜值,额外体质,额外智力,互斥天赋,互斥天赋,互斥天赋,互斥天赋,互斥天赋,互斥天赋
|
||||
1001,随身玉佩,或许有护佑作用,,0,,,,,,,,,,,,
|
||||
1002,红肚兜,小时候死亡率降低,,0,,,,,,,,,,,,
|
||||
1003,生而为男,性别一定为男,,1,,,,,,,1004,1025,1024,1113,,
|
||||
1004,生而为女,性别一定为女,,1,,,,,,,1003,1024,1025,,,
|
||||
1005,动漫高手,入宅的可能性翻6倍,,2,,,,,,,,,,,,
|
||||
1006,乐天派,快乐+1,,0,,1,,,,,,,,,,
|
||||
1007,天赋异禀,初始可用属性点+2,,1,2,,,,,,,,,,,
|
||||
1008,天生抑郁,快乐-3,,0,,-3,,,,,,,,,,
|
||||
1009,网络巨魔,快乐+2,,1,,2,,,,,,,,,,
|
||||
1010,天龙人,你拥有北京户口,,2,,,,,,,1012,1013,1014,,,
|
||||
1011,独生子女,你没有兄弟姐妹,,0,,,,,,,,,,,,
|
||||
1012,乡间微风,你出生在农村,,0,,,,,,,1010,1013,1014,,,
|
||||
1013,城中高楼,你出生在城市,,0,,,,,,,1010,1012,1014,,,
|
||||
1014,美籍华人,你有美国国籍,,2,,,3,,,,1010,1012,1013,,,
|
||||
1015,家中老大,你最受父母宠爱,,1,,,,,,,,,,,,
|
||||
1016,水性良好,不会被淹死,,0,,,,,,,,,,,,
|
||||
1017,先天免疫,你不会得艾滋病,,0,,,,,,,,,,,,
|
||||
1018,人类进化,所有属性+1,,2,,1,1,1,1,1,,,,,,
|
||||
1019,超凡,初始可用属性点+4,,2,4,,,,,,,,,,,
|
||||
1020,父母美貌,颜值+2,,1,,,,2,,,,,,,,
|
||||
1021,红颜薄命,颜值+2,体质-2,,0,,,,2,-2,,,,,,,
|
||||
1022,属蛇,不会被蛇咬死,,0,,,,,,,,,,,,
|
||||
1023,半神,所有属性+2,,3,,2,2,2,2,2,,,,,,
|
||||
1024,人中龙凤,天生双重性别,,2,,,,,,,1003,1004,1025,,,
|
||||
1025,阴阳之外,天生无性别,,2,,,,,,,1003,1024,1004,1113,,
|
||||
1026,彩虹之下,可能和同性交往,,0,,,,,,,1113,,,,,
|
||||
1027,斩情证道,终生不恋爱结婚,,1,,,,,,,1113,,,,,
|
||||
1028,桃花连连,恋爱机会提升,,0,,,,,,,,,,,,
|
||||
1029,平安童年,12岁前父母都健在,,1,,,,,,,,,,,,
|
||||
1030,宠物大师,宠物不会意外死亡,,0,,,,,,,,,,,,
|
||||
1031,天生残疾,体质-2,,0,,,,,-2,,,,,,,
|
||||
1032,早产儿,所有属性-1,,0,,-1,-1,-1,-1,-1,,,,,,
|
||||
1033,十死无生,体质-10,,0,,,,,-10,,,,,,,
|
||||
1034,家运不顺,家境-2,,0,,,-2,,,,,,,,,
|
||||
1035,头着地,智力-2,,0,,,,,,-2,,,,,,
|
||||
1036,胎教,智力+1,,0,,,,,,1,,,,,,
|
||||
1037,班中红人,和同学容易处好关系,,0,,,,,,,,,,,,
|
||||
1038,骑士,能轻松学会骑车,,0,,,,,,,,,,,,
|
||||
1039,永远的神,电竞天才,,1,,,,,,,,,,,,
|
||||
1040,戒律,赌毒不沾,,0,,,,,,,,,,,,
|
||||
1041,丁克,不生孩子,,1,,,,,,,1113,,,,,
|
||||
1042,少数民族,高考+5分,,0,,,,,,,,,,,,
|
||||
1043,老司机,你和家人不会发生车祸,,0,,,,,,,,,,,,
|
||||
1044,低压,你的家人不会心脏病,,0,,,,,,,,,,,,
|
||||
1045,战功,你退伍后会当官,,0,,,,,,,,,,,,
|
||||
1046,不孕不育,你生不出孩子,,1,,,,,,,1113,,,,,
|
||||
1047,白头偕老,爱人至少能活到70岁,,1,,,,,,,,,,,,
|
||||
1048,神秘的小盒子,100岁时才能开启,,3,,,,,,,,,,,,
|
||||
1049,三十而立,30岁时家境+2,AGE?[30],0,,,2,,,,,,,,,
|
||||
1050,四十不惑,40岁时智力+2,AGE?[40],0,,,,,,2,,,,,,
|
||||
1051,知天命,50岁时智力、快乐+1,AGE?[50],0,,1,,,,1,,,,,,
|
||||
1052,耳顺,60岁时快乐+2,AGE?[60],0,,2,,,,,,,,,,
|
||||
1053,从心所欲,70岁时快乐+3,AGE?[70],0,,3,,,,,,,,,,
|
||||
1054,老当益壮,60岁时体质+2,AGE?[60],1,,,,,2,,,,,,,
|
||||
1055,鹤发童颜,70岁时颜值+3,AGE?[70],0,,,,3,,,,,,,,
|
||||
1056,学前启蒙,5岁时智力+2,AGE?[5],1,,,,,,2,,,,,,
|
||||
1057,十八变,18岁时颜值+2,AGE?[18],1,,,,2,,,,,,,,
|
||||
1058,迟来之财,90岁时家境+4,AGE?[90],0,,,4,,,,,,,,,
|
||||
1059,理财达人,30、40、50岁时家境+1,"AGE?[30,40,50]",0,,,1,,,,,,,,,
|
||||
1060,成熟,12、18岁时智力+1,"AGE?[12,18]",1,,,,,,1,,,,,,
|
||||
1061,形象管理,16、24岁时颜值+1,"AGE?[16,24]",1,,,,1,,,,,,,,
|
||||
1062,成年礼,18岁时快乐+1,AGE?[18],0,,1,,,,,,,,,,
|
||||
1063,开光之胎,初始可用属性点+1,,0,1,,,,,,,,,,,
|
||||
1064,天命,初始可用属性点+8,,3,8,,,,,,,,,,,
|
||||
1065,祖传药丸,功能不明,,1,,,,,,,,,,,,
|
||||
1066,精准扶贫,家境为0时家境+1,(MNY<1)&(MNY>-1),0,,,1,,,,,,,,,
|
||||
1067,乐天派,快乐为0时快乐+1,(SPR<1)&(SPR>-1),1,,1,,,,,,,,,,
|
||||
1068,命悬一线,体质为0时体质+1,(STR<1)&(STR>-1),0,,,,,1,,,,,,,
|
||||
1069,智可生财,若20岁时智力>8,家境+2,(AGE?[20])&(INT>8),0,,,2,,,,,,,,,
|
||||
1070,舔狗甚多,若20岁时颜值>8,快乐+2,(AGE?[20])&(CHR>8),0,,2,,,,,,,,,,
|
||||
1071,保胎丸,你不会胎死腹中,,0,,,,,,,,,,,,
|
||||
1072,白化病,你不会遭遇枪击,,0,,,,,,,,,,,,
|
||||
1073,佛宗,考上哈佛大学的几率提高,,0,,,,,,,,,,,,
|
||||
1074,悟道,智力>10时快乐+3,INT>10,1,,3,,,,,,,,,,
|
||||
1075,驻颜,体质>10时颜值+3,STR>10,0,,,,3,,,,,,,,
|
||||
1076,界限突破,体质>10时快乐+3,STR>10,1,,3,,,,,,,,,,
|
||||
1077,倾城,颜值>10时快乐+3,CHR>10,1,,3,,,,,,,,,,
|
||||
1078,训练有方,智力>10时体质+3,INT>10,0,,,,,3,,,,,,,
|
||||
1079,相由心生,智力>10时颜值+3,INT>10,0,,,,3,,,,,,,,
|
||||
1080,智多鑫,智力>10时家境+3,INT>10,0,,,3,,,,,,,,,
|
||||
1081,灵光,快乐>10时其他属性+1,SPR>10,0,,,1,1,1,1,,,,,,
|
||||
1082,天启,快乐>10时其他属性+2,SPR>10,1,,,2,2,2,2,,,,,,
|
||||
1083,神谕,快乐>10时其他属性+3,SPR>10,2,,,3,3,3,3,,,,,,
|
||||
1084,献祭,初始可用属性点-2,快乐+2,,0,-2,2,,,,,1122,,,,,
|
||||
1085,幸运儿,初始可用属性点-3,快乐+5,,1,-3,5,,,,,1122,,,,,
|
||||
1086,挑战者,初始可用点-10,,0,-10,,,,,,1122,,,,,
|
||||
1087,你不懂,家境>10时快乐+3,MNY>10,1,,3,,,,,,,,,,
|
||||
1088,整容,家境>10时颜值+3,MNY>10,0,,,,3,,,,,,,,
|
||||
1089,钻石健身卡,家境>10时体质+3,MNY>10,0,,,,,3,,,,,,,
|
||||
1090,身残志坚,体质<0时其他属性+1,STR<0,0,,1,1,1,,1,,,,,,
|
||||
1091,活死人,体质<-1时其他属性+2,STR<0,1,,2,2,2,,2,,,,,,
|
||||
1092,开一扇窗,颜值<0时其他属性+1,CHR<0,0,,1,1,,1,1,,,,,,
|
||||
1093,大额头,颜值-2,智力+2,,0,,,,-2,,2,,,,,,
|
||||
1094,痘痘脸,颜值-1,,0,,,,-1,,,,,,,,
|
||||
1095,潜能,家境<0时其他属性+1,MNY<0,0,,1,,1,1,1,,,,,,
|
||||
1096,哀兵,快乐<0时其他属性+1,SPR<0,0,,,1,1,1,1,,,,,,
|
||||
1097,苦痛侍僧,快乐<-1时其他属性+2,SPR<-1,1,,,2,2,2,2,,,,,,
|
||||
1098,觉醒,家境<-1时其他属性+2,MNY<-1,1,,2,,2,2,2,,,,,,
|
||||
1099,抖M,家境-2,快乐+2,,0,,2,-2,,,,,,,,,
|
||||
1100,海的女儿,颜值-2,初始可用属性点+1,,0,1,,,-2,,,,,,,,
|
||||
1101,进阶,所有属性>5时,所有属性+1,(SPR>5)&(MNY>5)&(CHR>5)&(STR>5)&(INT>5),0,,1,1,1,1,1,,,,,,
|
||||
1102,超进化,所有属性>5时,所有属性+2,(SPR>5)&(MNY>5)&(CHR>5)&(STR>5)&(INT>5),1,,2,2,2,2,2,,,,,,
|
||||
1103,白色胶囊,你10岁时无事发生,AGE?[10],0,,,,,,,,,,,,
|
||||
1104,紫色胶囊,跳过你的40~50岁,"AGE?[40,41,42,43,44,45,46,47,48,49,50]",2,,,,,,,,,,,,
|
||||
1105,蓝色胶囊,你20、30岁时无事发生,"AGE?[20,30]",1,,,,,,,,,,,,
|
||||
1106,健康饮食,你不吃洋快餐,,0,,,,,,,,,,,,
|
||||
1107,不想罢了,你不会上清华大学,,0,,,,,,,,,,,,
|
||||
1108,挑衅,你喜欢没事找事,,0,,,,,,,,,,,,
|
||||
1109,旅行者,你喜欢旅游,,0,,,,,,,,,,,,
|
||||
1110,水仙,你比较自恋,,0,,,,,,,,,,,,
|
||||
1111,缺一门,无效果,,0,,,,,,,,,,,,
|
||||
1112,异界来客,你可能听到一些绝密消息,,2,,,,,,,,,,,,
|
||||
1113,三胎人生,你尽可能生三胎,,1,,,,,,,1003,1025,1026,1027,1041,1046
|
||||
1114,橙色胶囊,跳过你的60~90岁,"AGE?[60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90]",3,,,,,,,,,,,,
|
||||
1115,宙斯,参加奥赛的几率提高,,0,,,,,,,,,,,,
|
||||
1116,为人民服务,考公务员时一定能考上,,0,,,,,,,,,,,,
|
||||
1117,表现良好,入狱会减刑,,0,,,,,,,,,,,,
|
||||
1118,小吉,运气稍微提升,,0,,,,,,,,,,,,
|
||||
1119,天秤座,据说做事很公平,,0,,,,,,,,,,,,
|
||||
1120,万里挑一,你很攻,,0,,,,,,,,,,,,
|
||||
1121,把握不住,你有强迫症,,0,,,,,,,,,,,,
|
||||
1122,急了急了,赶着投胎,不要初始属性了,,1,-20,,,,,,1084,1085,1086,,,
|
||||
1123,不离不弃,你不会离婚,,0,,,,,,,,,,,,
|
||||
1124,足量,身高不矮,,0,,,,,,,,,,,,
|
||||
1125,易胖体质,颜值更容易降低,,0,,,,,,,,,,,,
|
||||
1126,黄帝,种族主义者,,0,,,,,,,,,,,,
|
||||
1127,左撇子,习惯使用左手,,0,,,,,,,,,,,,
|
||||
1128,克苏鲁,&▓▓▓◆▓▓▓¥#▓@■.◆,,2,,,,,,,,,,,,
|
||||
1129,不连续存在,你还拥有其他人格,,2,,,,,,,,,,,,
|
||||
1130,占位符,少一个可选天赋,,0,,,,,,,,,,,,
|
||||
1131,魔法棒,不知道有什么用……,,2,,,,,,,,,,,,
|
||||
|
-1173
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
import { defineConfig } from 'eslint/config'
|
||||
import parser from '@typescript-eslint/parser'
|
||||
import plugin from '@typescript-eslint/eslint-plugin'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
ignores: ['dist/**', 'node_modules/**'],
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
},
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// @ts-ignore
|
||||
plugins: { '@typescript-eslint': plugin },
|
||||
},
|
||||
])
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
@@ -1,5 +0,0 @@
|
||||
# Life Restart
|
||||
|
||||
やり直すんだ。そして、次はうまくやる。[RESTART](view/index.html)
|
||||
|
||||
<iframe src="https://discord.com/widget?id=883382868427014255&theme=dark" width="350" height="500" allowtransparency="true" frameborder="0" sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts"></iframe>
|
||||
Vendored
-2
File diff suppressed because one or more lines are too long
Vendored
-2
File diff suppressed because one or more lines are too long
+46
-21
@@ -1,23 +1,48 @@
|
||||
{
|
||||
"name": "life_restart",
|
||||
"type": "module",
|
||||
"bin": "repl/index.js",
|
||||
"scripts": {
|
||||
"test": "node test",
|
||||
"xlsxTransform": "node utils/xlsxTransform.js data",
|
||||
"dev": "webpack serve --open /view/index.html",
|
||||
"build": "webpack --mode production"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.15.4",
|
||||
"@babel/preset-env": "^7.15.4",
|
||||
"babel-loader": "^8.2.2",
|
||||
"core-js": "^3.17.2",
|
||||
"webpack": "^5.51.2",
|
||||
"webpack-dev-server": "^4.1.0",
|
||||
"xlsx": "^0.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack-cli": "^4.8.0"
|
||||
}
|
||||
"name": "remake",
|
||||
"type": "module",
|
||||
"version": "3.0.0",
|
||||
"author": "Vick Scarlet <vick@syaro.io>",
|
||||
"scripts": {
|
||||
"dev": "pnpm --filter @remake/web dev",
|
||||
"dev:prod": "pnpm --filter @remake/web dev:prod",
|
||||
"build:web": "pnpm --filter @remake/web build",
|
||||
"build:data": "pnpm --filter @remake/data build",
|
||||
"preview": "pnpm --filter @remake/web preview",
|
||||
"start": "pnpm --filter @remake/console start",
|
||||
"lint": "bunx --bun eslint",
|
||||
"lint:console": "pnpm --filter @remake/console lint",
|
||||
"lint:web": "pnpm --filter @remake/web lint",
|
||||
"lint:core": "pnpm --filter @remake/core lint",
|
||||
"lint:data": "pnpm --filter @remake/data lint",
|
||||
"lint:condition": "pnpm --filter @remake/condition lint",
|
||||
"lint:hooks": "pnpm --filter @remake/hooks lint",
|
||||
"fix": "bunx --bun eslint --fix",
|
||||
"fix:console": "pnpm --filter @remake/console lint --fix",
|
||||
"fix:web": "pnpm --filter @remake/web lint --fix",
|
||||
"fix:core": "pnpm --filter @remake/core lint --fix",
|
||||
"fix:data": "pnpm --filter @remake/data lint --fix",
|
||||
"fix:condition": "pnpm --filter @remake/condition lint --fix",
|
||||
"fix:hooks": "pnpm --filter @remake/hooks lint --fix",
|
||||
"test": "pnpm --filter @remake/web test",
|
||||
"test:console": "pnpm --filter @remake/console test",
|
||||
"test:web": "pnpm --filter @remake/web test",
|
||||
"test:core": "pnpm --filter @remake/core test",
|
||||
"test:data": "pnpm --filter @remake/data test",
|
||||
"test:condition": "pnpm --filter @remake/condition test",
|
||||
"test:hooks": "pnpm --filter @remake/hooks test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "1.3.14",
|
||||
"@typescript-eslint/eslint-plugin": "^8.67.0",
|
||||
"@typescript-eslint/parser": "^8.67.0",
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
"eslint": "^10.8.1",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"apps/*",
|
||||
"thirdparty/*"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { check } from '.'
|
||||
|
||||
// 定义属性 mock 字典的类型契约
|
||||
interface MockProperties {
|
||||
[key: string]: number | number[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性注入高阶函数:模拟角色属性管理类
|
||||
*/
|
||||
function withProp(prop: MockProperties) {
|
||||
return (condition: string): boolean => check(prop, condition)
|
||||
}
|
||||
|
||||
describe('condition', () => {
|
||||
// 实例化一个带满各种脏数据(数字、数组、空数组)的强类型测试代理
|
||||
const check = withProp({
|
||||
n1: 0,
|
||||
n2: -10,
|
||||
n3: 10,
|
||||
nl1: [1, 2, 3],
|
||||
nl2: [0],
|
||||
nl3: [],
|
||||
})
|
||||
|
||||
test('gt(>)', () => {
|
||||
expect(check('n3>11')).toBe(false)
|
||||
expect(check('n3>10')).toBe(false)
|
||||
expect(check('n3>9')).toBe(true)
|
||||
})
|
||||
|
||||
test('gte(>=)', () => {
|
||||
expect(check('n3>=11')).toBe(false)
|
||||
expect(check('n3>=10')).toBe(true)
|
||||
expect(check('n3>=9')).toBe(true)
|
||||
})
|
||||
|
||||
test('lt(<)', () => {
|
||||
expect(check('n3<9')).toBe(false)
|
||||
expect(check('n3<10')).toBe(false)
|
||||
expect(check('n3<11')).toBe(true)
|
||||
})
|
||||
|
||||
test('lte(<=)', () => {
|
||||
expect(check('n3<=9')).toBe(false)
|
||||
expect(check('n3<=10')).toBe(true)
|
||||
expect(check('n3<=11')).toBe(true)
|
||||
})
|
||||
|
||||
test('eq(=)', () => {
|
||||
expect(check('n3=9')).toBe(false)
|
||||
expect(check('n3=10')).toBe(true)
|
||||
expect(check('n3=11')).toBe(false)
|
||||
expect(check('nl1=0')).toBe(false)
|
||||
expect(check('nl1=1')).toBe(true)
|
||||
expect(check('nl1=2')).toBe(true)
|
||||
expect(check('nl1=3')).toBe(true)
|
||||
})
|
||||
|
||||
test('ne(!=)', () => {
|
||||
expect(check('n3!=9')).toBe(true)
|
||||
expect(check('n3!=10')).toBe(false)
|
||||
expect(check('n3!=11')).toBe(true)
|
||||
expect(check('nl1!=0')).toBe(true)
|
||||
expect(check('nl1!=1')).toBe(false)
|
||||
expect(check('nl1!=2')).toBe(false)
|
||||
expect(check('nl1!=3')).toBe(false)
|
||||
})
|
||||
|
||||
test('in(?)', () => {
|
||||
expect(check('n3?[1,2,3]')).toBe(false)
|
||||
expect(check('n3?[10,11,12]')).toBe(true)
|
||||
expect(check('nl1?[0,1]')).toBe(true)
|
||||
expect(check('nl2?[1,2,3]')).toBe(false)
|
||||
expect(check('nl3?[1,2,3]')).toBe(false)
|
||||
expect(check('nl2?[]')).toBe(false)
|
||||
expect(check('nl3?[]')).toBe(false)
|
||||
})
|
||||
|
||||
test('notin(!)', () => {
|
||||
expect(check('n3![1,2,3]')).toBe(true)
|
||||
expect(check('n3![10,11,12]')).toBe(false)
|
||||
expect(check('nl1![0,1]')).toBe(false)
|
||||
expect(check('nl2![1,2,3]')).toBe(true)
|
||||
expect(check('nl3![1,2,3]')).toBe(true)
|
||||
expect(check('nl2![]')).toBe(true)
|
||||
expect(check('nl3![]')).toBe(true)
|
||||
})
|
||||
|
||||
test('and(&)', () => {
|
||||
expect(check('n1>=0&n2<0')).toBe(true)
|
||||
expect(check('n2>0&n3>0')).toBe(false)
|
||||
expect(check('n2<0&n3<0')).toBe(false)
|
||||
expect(check('n2<0&n3>0')).toBe(true)
|
||||
expect(check('n1=0&n2<0&n3>0')).toBe(true)
|
||||
expect(check('n1=0&n2>0&n3>0')).toBe(false)
|
||||
})
|
||||
|
||||
test('or(|)', () => {
|
||||
expect(check('n1>=0|n2<0')).toBe(true)
|
||||
expect(check('n2>0|n3>0')).toBe(true)
|
||||
expect(check('n2<0|n3<0')).toBe(true)
|
||||
expect(check('n2<0|n3>0')).toBe(true)
|
||||
expect(check('n2>0|n3<0')).toBe(false)
|
||||
expect(check('n1=0|n2<0|n3>0')).toBe(true)
|
||||
expect(check('n1=0|n2>0|n3>0')).toBe(true)
|
||||
expect(check('n1!=0|n2>0|n3<0')).toBe(false)
|
||||
})
|
||||
|
||||
test('mix', () => {
|
||||
expect(check('(n1=0|n2<0|n3>0)&(n1=0|n2>0|n3>0)')).toBe(true)
|
||||
expect(check('(n1=0|n2<0|n3>0)&(n1!=0|n2>0|n3<0)')).toBe(false)
|
||||
expect(check('n1=0|n2<0|n3>0&n1!=0|n2>0|n3<0')).toBe(true)
|
||||
expect(check('(n1>0|n1?[-10,0])&(n2>0|n3![0,1])')).toBe(true)
|
||||
expect(check('(n1>0&n1?[-10,0])|(n2<0&n3![0,1])')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,190 @@
|
||||
export type ConditionNode = string | ConditionTree
|
||||
export interface ConditionTree extends Array<ConditionNode> {}
|
||||
|
||||
export type PropertyContainer = Record<string, any>
|
||||
|
||||
export function parse(condition: string): ConditionTree {
|
||||
const conditions: ConditionTree = []
|
||||
const length = condition.length
|
||||
const stack: ConditionTree[] = []
|
||||
stack.unshift(conditions)
|
||||
let cursor = 0
|
||||
|
||||
const catchString = (i: number): void => {
|
||||
const str = condition.substring(cursor, i).trim()
|
||||
cursor = i
|
||||
if (str) stack[0]?.push(str)
|
||||
}
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
switch (condition[i]) {
|
||||
case ' ':
|
||||
continue
|
||||
|
||||
case '(': {
|
||||
catchString(i)
|
||||
cursor++
|
||||
const sub: ConditionTree = []
|
||||
stack[0]?.push(sub)
|
||||
stack.unshift(sub)
|
||||
break
|
||||
}
|
||||
case ')':
|
||||
catchString(i)
|
||||
cursor++
|
||||
stack.shift()
|
||||
break
|
||||
|
||||
case '|':
|
||||
case '&':
|
||||
catchString(i)
|
||||
catchString(i + 1)
|
||||
break
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
catchString(length)
|
||||
return conditions
|
||||
}
|
||||
|
||||
export function check(property: PropertyContainer, condition: string): boolean {
|
||||
const conditions = parse(condition)
|
||||
return checkParsedConditions(property, conditions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归计算已解析出来的多维语法树结果
|
||||
*/
|
||||
function checkParsedConditions(
|
||||
property: PropertyContainer,
|
||||
conditions: ConditionNode,
|
||||
): boolean {
|
||||
// 如果已经剥离到了最底层的纯字符串表达式(如 'AGE>10'),直接移交核心原子判定器
|
||||
if (!Array.isArray(conditions)) {
|
||||
return checkProp(property, conditions)
|
||||
}
|
||||
|
||||
if (conditions.length === 0) return true
|
||||
if (conditions.length === 1) {
|
||||
return checkParsedConditions(property, conditions[0]!)
|
||||
}
|
||||
|
||||
let ret = checkParsedConditions(property, conditions[0]!)
|
||||
for (let i = 1; i < conditions.length; i += 2) {
|
||||
const operator = conditions[i]
|
||||
const nextNode = conditions[i + 1]
|
||||
|
||||
// 防御性拦截,防止表达式残缺或配置错误引发崩溃
|
||||
if (nextNode === undefined) return false
|
||||
|
||||
switch (operator) {
|
||||
case '&':
|
||||
if (ret) {
|
||||
ret = checkParsedConditions(property, nextNode)
|
||||
}
|
||||
break
|
||||
case '|':
|
||||
if (ret) return true
|
||||
ret = checkParsedConditions(property, nextNode)
|
||||
break
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
function checkProp(property: PropertyContainer, condition: string): boolean {
|
||||
const length = condition.length
|
||||
let i = condition.search(/[><!?=]/)
|
||||
|
||||
// 防御处理:如果没有找到任何操作符,视为非法表达式直接拒签
|
||||
if (i === -1) return false
|
||||
|
||||
const prop = condition.substring(0, i)
|
||||
// 判断是否是双字符操作符(如 >=, <=, !=),若是则向前多吞一个字符位
|
||||
const isDoubleSymbol = condition[i + 1] === '='
|
||||
const symbol = condition.substring(i, (i += isDoubleSymbol ? 2 : 1))
|
||||
const d = condition.substring(i, length)
|
||||
|
||||
const propData = property[prop]
|
||||
const conditionData: number | any[] =
|
||||
d[0] === '[' ? JSON.parse(d) : Number(d)
|
||||
|
||||
switch (symbol) {
|
||||
case '>':
|
||||
return (propData ?? 0) > conditionData
|
||||
case '<':
|
||||
return (propData ?? 0) < conditionData
|
||||
case '>=':
|
||||
return (propData ?? 0) >= conditionData
|
||||
case '<=':
|
||||
return (propData ?? 0) <= conditionData
|
||||
case '=':
|
||||
if (propData instanceof Set) {
|
||||
return propData.has(conditionData)
|
||||
}
|
||||
if (Array.isArray(propData)) {
|
||||
return propData.includes(conditionData)
|
||||
}
|
||||
return (propData ?? 0) == conditionData
|
||||
case '!=':
|
||||
if (propData instanceof Set) {
|
||||
return !propData.has(conditionData)
|
||||
}
|
||||
if (Array.isArray(propData)) {
|
||||
return !propData.includes(conditionData)
|
||||
}
|
||||
return (propData ?? 0) != conditionData
|
||||
|
||||
case '?': // 🌟 包含判定符(如 属性值 是否在 [1,2,3] 数组范围内)
|
||||
if (propData instanceof Set) {
|
||||
if (Array.isArray(conditionData)) {
|
||||
for (const c of conditionData) {
|
||||
if (propData.has(c)) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(propData)) {
|
||||
if (Array.isArray(conditionData)) {
|
||||
for (const p of propData) {
|
||||
if (conditionData.includes(p)) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(conditionData)) {
|
||||
return conditionData.includes(propData)
|
||||
}
|
||||
return false
|
||||
|
||||
case '!': // 🌟 排除判定符(如 属性值 是否不存在于 [1,2,3] 数组中)
|
||||
if (propData instanceof Set) {
|
||||
if (Array.isArray(conditionData)) {
|
||||
for (const c of conditionData) {
|
||||
if (propData.has(c)) return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (Array.isArray(propData)) {
|
||||
if (Array.isArray(conditionData)) {
|
||||
for (const p of propData) {
|
||||
if (conditionData.includes(p)) return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (Array.isArray(conditionData)) {
|
||||
return !conditionData.includes(propData)
|
||||
}
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@remake/condition",
|
||||
"type": "module",
|
||||
"version": "3.0.0",
|
||||
"main": "./index.ts",
|
||||
"author": "Vick Scarlet <vick@syaro.io>",
|
||||
"scripts": {
|
||||
"lint": "bunx --bun eslint .",
|
||||
"test": "bun test"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@remake/core",
|
||||
"type": "module",
|
||||
"version": "3.0.0",
|
||||
"author": "Vick Scarlet <vick@syaro.io>",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "bunx --bun eslint .",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@remake/condition": "workspace:*",
|
||||
"@remake/data": "workspace:*",
|
||||
"@remake/vitex": "workspace:*",
|
||||
"immer": "^11.1.15"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./src/*.ts",
|
||||
"import": "./src/*.ts",
|
||||
"default": "./src/*.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { createState } from './state'
|
||||
import { trigger } from './achievement'
|
||||
import { AchievementOpportunity } from '@remake/data'
|
||||
import { enableMapSet } from 'immer'
|
||||
enableMapSet()
|
||||
|
||||
describe('Achievement', () => {
|
||||
const profile = {
|
||||
times: 0,
|
||||
talents: new Set([]),
|
||||
achievements: new Set([]),
|
||||
events: new Set([]),
|
||||
}
|
||||
const allocation = {
|
||||
charm: 5,
|
||||
intelligence: 5,
|
||||
strength: 5,
|
||||
money: 5,
|
||||
spirit: 5,
|
||||
}
|
||||
|
||||
test('trigger [times:500]', () => {
|
||||
const p = { ...profile, times: 500 }
|
||||
const result = trigger(
|
||||
AchievementOpportunity.End,
|
||||
createState(allocation, []),
|
||||
p,
|
||||
)
|
||||
expect(result.state.achievements).toContain(101) // 既视感
|
||||
expect(result.state.achievements).toContain(102) // 孟婆愁
|
||||
expect(result.state.achievements).toContain(103) // 所有人都是我
|
||||
expect(result.state.achievements).toContain(104) // Rewrite
|
||||
expect(result.triggers).toContain(101)
|
||||
expect(result.triggers).toContain(102)
|
||||
expect(result.triggers).toContain(103)
|
||||
expect(result.triggers).toContain(104)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Achievement, AchievementOpportunity } from '@remake/data'
|
||||
import { achievements } from '@remake/data'
|
||||
import type { GameState, ProfileState } from './state'
|
||||
import { createFlatState } from './state'
|
||||
import { check } from '@remake/condition'
|
||||
import { produce } from 'immer'
|
||||
import type { TriggerResult } from './game'
|
||||
|
||||
const OpportunityMap = Map.groupBy(
|
||||
achievements.keys(),
|
||||
a => achievements.get(a)!.opportunity,
|
||||
)
|
||||
|
||||
export function trigger(
|
||||
opportunity: AchievementOpportunity,
|
||||
state: GameState,
|
||||
profile: ProfileState,
|
||||
): TriggerResult<Achievement['id']> {
|
||||
const flatState = createFlatState(state, profile)
|
||||
const triggers = OpportunityMap.get(opportunity)!.filter(a => {
|
||||
if (state.achievements.has(a)) return false
|
||||
if (profile.achievements.has(a)) return false
|
||||
const { condition } = achievements.get(a)!
|
||||
return check(flatState, condition)
|
||||
})
|
||||
const newState = produce(state, draft => {
|
||||
draft.achievements = new Set([...draft.achievements, ...triggers])
|
||||
})
|
||||
return { state: newState, triggers }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { pullChara, uniqueGenerate } from './character'
|
||||
import { enableMapSet } from 'immer'
|
||||
enableMapSet()
|
||||
|
||||
describe('Character', () => {
|
||||
const sum = (map: Map<any, number>) =>
|
||||
Array.from(map.values()).reduce((acc, val) => acc + val, 0)
|
||||
test('pull', () => {
|
||||
let result = pullChara(
|
||||
{ count: 10, knife: 10 },
|
||||
{ times: 0, drawns: new Map() },
|
||||
)
|
||||
expect(result.characters).not.toContainValue(null)
|
||||
expect(result.characters.length).toBe(10)
|
||||
expect(result.times.times).toBe(10)
|
||||
expect(result.times.drawns.size).toBe(10)
|
||||
result = pullChara({ count: 3, knife: 10 }, result.times)
|
||||
expect(result.characters).not.toContainValue(null)
|
||||
expect(result.characters.length).toBe(3)
|
||||
expect(result.times.times).toBe(13)
|
||||
expect(sum(result.times.drawns)).toBe(13)
|
||||
result = pullChara({ count: 3, knife: 10 }, result.times)
|
||||
expect(result.characters).not.toContainValue(null)
|
||||
expect(result.characters.length).toBe(3)
|
||||
expect(result.times.times).toBe(16)
|
||||
expect(sum(result.times.drawns)).toBe(16)
|
||||
result = pullChara({ count: 20, knife: 1 }, result.times)
|
||||
expect(result.characters).not.toContainValue(null)
|
||||
expect(result.characters.length).toBe(20)
|
||||
expect(result.times.times).toBe(36)
|
||||
expect(sum(result.times.drawns)).toBe(36)
|
||||
})
|
||||
|
||||
const wr = (s: number, e: number) => {
|
||||
const length = e - s + 1
|
||||
return Array.from(
|
||||
{ length },
|
||||
(_, i) => [s + i, Math.min(i + 1, length - i)] as const,
|
||||
)
|
||||
}
|
||||
|
||||
test('unique', () => {
|
||||
const unique = uniqueGenerate({ prop: wr(0, 10), talent: wr(1, 5) })
|
||||
expect(unique.property).toHaveProperty('CHR')
|
||||
expect(unique.property).toHaveProperty('INT')
|
||||
expect(unique.property).toHaveProperty('STR')
|
||||
expect(unique.property).toHaveProperty('MNY')
|
||||
expect(unique.property.CHR).toBeGreaterThanOrEqual(0)
|
||||
expect(unique.property.INT).toBeGreaterThanOrEqual(0)
|
||||
expect(unique.property.STR).toBeGreaterThanOrEqual(0)
|
||||
expect(unique.property.MNY).toBeGreaterThanOrEqual(0)
|
||||
expect(unique.talent.length).toBeGreaterThanOrEqual(0)
|
||||
expect(unique.property.CHR).toBeLessThanOrEqual(10)
|
||||
expect(unique.property.INT).toBeLessThanOrEqual(10)
|
||||
expect(unique.property.STR).toBeLessThanOrEqual(10)
|
||||
expect(unique.property.MNY).toBeLessThanOrEqual(10)
|
||||
expect(unique.talent.length).toBeLessThanOrEqual(5)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { Character, CharacterProperty, Talent } from '@remake/data'
|
||||
import { characters, talents } from '@remake/data'
|
||||
import { type RNG, type WeightItem, pick, pickWeight } from '@remake/vitex'
|
||||
import type { Allocation } from './state'
|
||||
|
||||
export type BaseChara = Omit<Character, 'id' | 'name'>
|
||||
export type BaseAlloc = Omit<Allocation, 'spirit'>
|
||||
export interface UniqueGenCfg {
|
||||
prop: WeightItem<number>[]
|
||||
talent: WeightItem<number>[]
|
||||
}
|
||||
export function uniqueGenerate(config: UniqueGenCfg, rng?: RNG) {
|
||||
const CHR = pickWeight(config.prop, rng) ?? 0
|
||||
const INT = pickWeight(config.prop, rng) ?? 0
|
||||
const STR = pickWeight(config.prop, rng) ?? 0
|
||||
const MNY = pickWeight(config.prop, rng) ?? 0
|
||||
let count = pickWeight(config.talent, rng) ?? 0
|
||||
const property = { CHR, INT, STR, MNY }
|
||||
const ids = Array.from(talents.keys())
|
||||
const picked = new Set<Talent['id']>()
|
||||
while (count > 0) {
|
||||
const t = pick(ids, rng)!
|
||||
if (picked.has(t)) continue
|
||||
if (talents.get(t)!.exclusive) continue
|
||||
picked.add(t)
|
||||
count--
|
||||
}
|
||||
return { property, talent: Array.from(picked) } satisfies BaseChara
|
||||
}
|
||||
|
||||
export interface PullCharaOpt {
|
||||
count: number
|
||||
knife: number
|
||||
}
|
||||
export interface PullCharaTms {
|
||||
times: number
|
||||
drawns: Map<Character['id'], number>
|
||||
}
|
||||
export interface PullCharaRet {
|
||||
characters: Character['id'][]
|
||||
times: PullCharaTms
|
||||
}
|
||||
export function pullChara(
|
||||
opt: PullCharaOpt,
|
||||
tms: PullCharaTms = { times: 0, drawns: new Map() },
|
||||
rng?: RNG,
|
||||
): PullCharaRet {
|
||||
const { count, knife } = opt
|
||||
const drawns = new Map(tms.drawns)
|
||||
const picked = new Set<Character['id']>()
|
||||
for (let i = 0; i < count; i++) {
|
||||
const weightMap = deriveWeightMap(tms.times + i, knife, drawns)
|
||||
picked.forEach(id => weightMap.delete(id))
|
||||
const id = pickWeight(Array.from(weightMap.entries()), rng)!
|
||||
picked.add(id)
|
||||
drawns.set(id, (drawns.get(id) ?? 0) + 1)
|
||||
}
|
||||
return {
|
||||
characters: Array.from(picked),
|
||||
times: { times: tms.times + count, drawns },
|
||||
}
|
||||
}
|
||||
|
||||
function deriveWeightMap(
|
||||
times: number,
|
||||
knife: number,
|
||||
drawns: PullCharaTms['drawns'],
|
||||
): Map<Character['id'], number> {
|
||||
const max = Math.max(0, ...drawns.values())
|
||||
const base = times - knife * Math.floor((times - max) / (knife || 1)) || 1
|
||||
return new Map(
|
||||
Array.from(characters.keys(), id => [id, base - (drawns.get(id) ?? 0)]),
|
||||
)
|
||||
}
|
||||
|
||||
export function charaPropToBaseAlloc(props: CharacterProperty): BaseAlloc {
|
||||
return {
|
||||
charm: props.CHR,
|
||||
intelligence: props.INT,
|
||||
strength: props.STR,
|
||||
money: props.MNY,
|
||||
}
|
||||
}
|
||||
|
||||
function convert(chara: BaseChara) {
|
||||
return {
|
||||
allocation: charaPropToBaseAlloc(chara.property),
|
||||
talents: chara.talent,
|
||||
}
|
||||
}
|
||||
|
||||
export function startChara(id: Character['id']) {
|
||||
return convert(characters.get(id)!)
|
||||
}
|
||||
|
||||
export function startUnique(chara: BaseChara) {
|
||||
return convert(chara)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { expect, test, describe } from 'bun:test'
|
||||
import { createState } from './state'
|
||||
import { trigger } from './event'
|
||||
import { enableMapSet } from 'immer'
|
||||
enableMapSet()
|
||||
|
||||
describe('Event', () => {
|
||||
const profile = {
|
||||
times: 0,
|
||||
talents: new Set([]),
|
||||
achievements: new Set([]),
|
||||
events: new Set([]),
|
||||
}
|
||||
const allocation = {
|
||||
charm: 10,
|
||||
intelligence: 10,
|
||||
strength: 0,
|
||||
money: 0,
|
||||
spirit: 5,
|
||||
}
|
||||
|
||||
test('branch 10003:0 [玉佩]', () => {
|
||||
const result = trigger(
|
||||
10003,
|
||||
createState(allocation, [1001, 1002, 1003]),
|
||||
profile,
|
||||
)
|
||||
expect(result.state.life).toBe(1)
|
||||
expect(result.state.events).toEqual(new Set([10003, 10004]))
|
||||
expect(result.triggers).toEqual([10003, 10004])
|
||||
})
|
||||
|
||||
test('branch 10003:1 [死了]', () => {
|
||||
const result = trigger(10003, createState(allocation, []), profile)
|
||||
expect(result.state.life).toBe(0)
|
||||
expect(result.state.events).toEqual(new Set([10003, 10000]))
|
||||
expect(result.triggers).toEqual([10003, 10000])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import { type Event, events } from '@remake/data'
|
||||
import type { Properties, GameState, ProfileState } from './state'
|
||||
import { propsEffect, createFlatState } from './state'
|
||||
import { check as checkCondition } from '@remake/condition'
|
||||
import type { TriggerResult } from './game'
|
||||
import { produce } from 'immer'
|
||||
|
||||
export function check(
|
||||
event: Event['id'],
|
||||
state: GameState,
|
||||
profile: ProfileState,
|
||||
) {
|
||||
const { include, exclude, NoRandom } = events.get(event)!
|
||||
if (NoRandom) return false
|
||||
const flatState = createFlatState(state, profile)
|
||||
if (exclude && checkCondition(flatState, exclude)) return false
|
||||
if (include) return checkCondition(flatState, include)
|
||||
return true
|
||||
}
|
||||
|
||||
export function trigger(
|
||||
eventId: number,
|
||||
state: GameState,
|
||||
profile: ProfileState,
|
||||
): TriggerResult<Event['id']> {
|
||||
const { effect, branch } = events.get(eventId)!
|
||||
const newState = produce(state, draft => {
|
||||
draft.events.add(eventId)
|
||||
if (!effect) return
|
||||
if (effect.LIF) draft.life += effect.LIF
|
||||
const pe: Partial<Properties> = {}
|
||||
if (effect.CHR) pe.charm = effect.CHR
|
||||
if (effect.INT) pe.intelligence = effect.INT
|
||||
if (effect.STR) pe.strength = effect.STR
|
||||
if (effect.MNY) pe.money = effect.MNY
|
||||
if (effect.SPR) pe.spirit = effect.SPR
|
||||
draft.props = propsEffect(draft.props, pe)
|
||||
})
|
||||
const flatState = createFlatState(newState, profile)
|
||||
if (branch) {
|
||||
for (const { condition, event } of branch) {
|
||||
if (checkCondition(flatState, condition)) {
|
||||
const result = trigger(event, newState, profile)
|
||||
return {
|
||||
state: result.state,
|
||||
triggers: [eventId, ...result.triggers],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { state: newState, triggers: [eventId] }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user