mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-19 18:43:53 +08:00
update: hooks, web
This commit is contained in:
@@ -6,24 +6,32 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession.
|
concurrency: ci-${{ github.ref }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout 🛎️
|
- name: 🛎️ Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: 🛠️ Install Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: 🛠️ Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 10
|
version: latest
|
||||||
|
|
||||||
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
|
- name: 📦 Install Dependencies
|
||||||
run: |
|
run: pnpm install
|
||||||
pnpm install
|
|
||||||
pnpm xlsx2json
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
- name: Deploy 🚀
|
- name: 🏗️ Build data
|
||||||
|
run: bun build:data
|
||||||
|
|
||||||
|
- name: 🏗️ Build web
|
||||||
|
run: bun build:web
|
||||||
|
|
||||||
|
- name: 🚀 Deploy
|
||||||
uses: JamesIves/github-pages-deploy-action@v4
|
uses: JamesIves/github-pages-deploy-action@v4
|
||||||
with:
|
with:
|
||||||
folder: template # The folder the action should deploy.
|
folder: apps/web/dist # The folder the action should deploy.
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"author": "Vick Scarlet <vick@syaro.io>",
|
"author": "Vick Scarlet <vick@syaro.io>",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run src/index.ts",
|
"start": "bun run src/index.ts",
|
||||||
"lint": "eslint .",
|
"lint": "bunx --bun eslint .",
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,32 +1,4 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"extends": "../../tsconfig.json",
|
||||||
// Environment setup & latest features
|
|
||||||
"lib": ["ESNext"],
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "Preserve",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"types": ["bun"],
|
|
||||||
|
|
||||||
// Bundler mode
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
// Best practices
|
|
||||||
"strict": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
|
||||||
"noImplicitAny": true
|
|
||||||
},
|
|
||||||
"include": ["**/*"]
|
"include": ["**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
<meta name="description" content="人生重开模拟器 - 这垃圾人生一秒也不想待了"/>
|
<meta name="description" content="人生重开模拟器 - 这垃圾人生一秒也不想待了"/>
|
||||||
<meta name="keywords" content="人生重开模拟器 liferestart life restart remake 人生重来"/>
|
<meta name="keywords" content="人生重开模拟器 liferestart life restart remake 人生重来"/>
|
||||||
<meta name="color-scheme" content="light dark" />
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>人生重开模拟器</title>
|
<title>人生重开模拟器</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="remake"></div>
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
<script type="module" src="src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+17
-10
@@ -4,24 +4,31 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"author": "Vick Scarlet <vick@syaro.io>",
|
"author": "Vick Scarlet <vick@syaro.io>",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "bunx --bun vite",
|
||||||
"build": "vite build",
|
"build": "bunx --bun vite build",
|
||||||
"preview": "vite preview",
|
"preview": "bunx --bun vite preview",
|
||||||
"lint": "eslint",
|
"lint": "bunx --bun eslint",
|
||||||
"test": "vitest"
|
"test": "bunx --bun vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@remake/core": "workspace:*",
|
|
||||||
"@remake/data": "workspace:*",
|
"@remake/data": "workspace:*",
|
||||||
"preact": "^10.26.9"
|
"@remake/hooks": "workspace:*",
|
||||||
|
"@remake/vitex": "workspace:*",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"jotai": "^2.20.2",
|
||||||
|
"react": "^19.2.8",
|
||||||
|
"react-dom": "^19.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@preact/preset-vite": "^2.10.2",
|
"@types/react": "^19.2.18",
|
||||||
"eslint-config-preact": "^2.0.0",
|
"@types/react-dom": "^19.2.4",
|
||||||
|
"@vitejs/plugin-react": "^6.0.5",
|
||||||
|
"eslint-config-react": "^1.1.7",
|
||||||
|
"jotai-devtools": "^0.14.0",
|
||||||
"vite": "^8.1.3",
|
"vite": "^8.1.3",
|
||||||
"vitest": "^4.1.10"
|
"vitest": "^4.1.10"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "preact"
|
"extends": "react"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.screen {
|
||||||
|
min-width: 32rem;
|
||||||
|
max-width: calc(100dvw - 2rem);
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
> * { width: 100%; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useInit } from '@/hooks/storage'
|
||||||
|
import { useStep, Step } from '@remake/hooks'
|
||||||
|
import Home from '@/containers/Home'
|
||||||
|
import Pick from '@/containers/TalentPick'
|
||||||
|
import Alloc from '@/containers/Alloc'
|
||||||
|
import Play from '@/containers/Play'
|
||||||
|
import Summary from '@/containers/Summary'
|
||||||
|
import { Loading } from './containers/Loading'
|
||||||
|
import './Game.css'
|
||||||
|
|
||||||
|
export function Game() {
|
||||||
|
const [inited, init] = useInit()
|
||||||
|
const step = useStep()
|
||||||
|
useEffect(() => {
|
||||||
|
if (!inited) init()
|
||||||
|
}, [inited])
|
||||||
|
if (!inited) return <Loading />
|
||||||
|
switch (step) {
|
||||||
|
case Step.Idle:
|
||||||
|
return <Home />
|
||||||
|
case Step.Mode:
|
||||||
|
case Step.Pick:
|
||||||
|
return <Pick />
|
||||||
|
case Step.Alloc:
|
||||||
|
return <Alloc />
|
||||||
|
case Step.Play:
|
||||||
|
return <Play />
|
||||||
|
case Step.Summary:
|
||||||
|
return <Summary />
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Game
|
||||||
@@ -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,29 @@
|
|||||||
|
import type { Talent } from '@remake/data/talent'
|
||||||
|
import TalentComponent from './Talent'
|
||||||
|
import talents from '@remake/data/talent'
|
||||||
|
import './Replaced.css'
|
||||||
|
|
||||||
|
export interface TalentProps {
|
||||||
|
id: Talent['id']
|
||||||
|
chains?: Talent['id'][]
|
||||||
|
}
|
||||||
|
export function Replaced({ id, chains }: TalentProps) {
|
||||||
|
return (
|
||||||
|
<ul className="replaced">
|
||||||
|
<li>
|
||||||
|
<TalentComponent id={id} selected={true} />
|
||||||
|
</li>
|
||||||
|
{chains?.map(id => {
|
||||||
|
const talent = talents.get(id)
|
||||||
|
if (!talent) return null
|
||||||
|
return (
|
||||||
|
<li key={id}>
|
||||||
|
<TalentComponent id={id} selected={false} />
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Replaced
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
.talent {
|
||||||
|
--color: var(--grade-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
gap: 1rem;
|
||||||
|
color: var(--color);
|
||||||
|
background: var(--background, var(--base-background-color));
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
border: 0.0625rem solid var(--grade-color);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
&.talent-grade-0 { --grade-color: var(--color-grade-0); }
|
||||||
|
&.talent-grade-1 { --grade-color: var(--color-grade-1); }
|
||||||
|
&.talent-grade-2 { --grade-color: var(--color-grade-2); }
|
||||||
|
&.talent-grade-3 { --grade-color: var(--color-grade-3); }
|
||||||
|
&.selected {
|
||||||
|
--color: var(--base-background-color);
|
||||||
|
--background: var(--grade-color);
|
||||||
|
&:hover { --background: oklch(from var(--grade-color) calc(l + 0.1) c h); }
|
||||||
|
}
|
||||||
|
&:hover { --background: oklch(from var(--grade-color) calc(l + 0.5) c h); }
|
||||||
|
.talent-description {
|
||||||
|
color: oklch(from var(--color) calc(l + 0.1) c h);
|
||||||
|
font-size: 0.6rem;
|
||||||
|
align-self: flex-end;
|
||||||
|
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{
|
||||||
|
&: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);}
|
||||||
|
.talent-description { color: oklch(from var(--color) calc(l - 0.2) c h); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { Talent } from '@remake/data/talent'
|
||||||
|
import talents from '@remake/data/talent'
|
||||||
|
import cx from 'classnames'
|
||||||
|
import './Talent.css'
|
||||||
|
|
||||||
|
export interface TalentProps {
|
||||||
|
id: Talent['id']
|
||||||
|
selected: boolean
|
||||||
|
}
|
||||||
|
export function TalentComponent({ id, selected }: TalentProps) {
|
||||||
|
const talent = talents.get(id)
|
||||||
|
if (!talent) return null
|
||||||
|
const grade = talent?.grade ?? 0
|
||||||
|
return (
|
||||||
|
<div className={cx('talent', `talent-grade-${grade}`, { selected })}>
|
||||||
|
<span className="talent-name">{talent.name}</span>
|
||||||
|
<span className="talent-description">{talent.description}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TalentComponent
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
.theme-toggle-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
--hover-color: rgba(0, 0, 0, 0.05);
|
||||||
|
&:hover { background-color: var(--hover-color);}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 { fill: oklch(from var(--base-text-color) calc(l + 0.2) c h); }
|
||||||
|
.sun-beams { stroke: oklch(from var(--base-text-color) calc(l + 0.2) c h); }
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] {
|
||||||
|
.theme-toggle-btn { --hover-color: rgba(255, 255, 255, 0.1); }
|
||||||
|
.mask-cutter-group { transform: translate(0, 0); }
|
||||||
|
.theme-svg { transform: rotate(375deg); }
|
||||||
|
.sun-center {
|
||||||
|
transform: scale(1.5);
|
||||||
|
fill: #fbbf24;
|
||||||
|
}
|
||||||
|
.sun-beams {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.6);
|
||||||
|
stroke: #fbbf24;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import './ThemeToggle.css'
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const [isDark, setIsDark] = useState(
|
||||||
|
() => matchMedia('(prefers-color-scheme: dark)').matches,
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.dataset.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)"
|
||||||
|
/>
|
||||||
|
<g className="sun-beams">
|
||||||
|
<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,60 @@
|
|||||||
|
import type { Config } from '@remake/hooks'
|
||||||
|
import type { TalentGrade } from '@remake/data/talent'
|
||||||
|
|
||||||
|
function gadm(grade: TalentGrade, value: number) {
|
||||||
|
return new Map([[grade, { mode: 'multiply', value }]])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdditonMap = {
|
||||||
|
times: [
|
||||||
|
{ value: 0, additions: new Map() },
|
||||||
|
{ value: 10, additions: gadm(2, 2) },
|
||||||
|
{ value: 30, additions: gadm(2, 3) },
|
||||||
|
{ value: 50, additions: gadm(2, 4) },
|
||||||
|
{ value: 70, additions: gadm(2, 5) },
|
||||||
|
{ value: 100, additions: gadm(2, 6) },
|
||||||
|
],
|
||||||
|
achievements: [
|
||||||
|
{ value: 0, additions: new Map() },
|
||||||
|
{ value: 10, additions: gadm(3, 2) },
|
||||||
|
{ value: 30, additions: gadm(3, 3) },
|
||||||
|
{ value: 50, additions: gadm(3, 4) },
|
||||||
|
{ value: 70, additions: gadm(3, 5) },
|
||||||
|
{ value: 100, additions: gadm(3, 6) },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BasePoints = 20
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
mode: 10,
|
||||||
|
pick: 3,
|
||||||
|
points: BasePoints,
|
||||||
|
allocate: 10,
|
||||||
|
pull: {
|
||||||
|
count: 10,
|
||||||
|
rate: {
|
||||||
|
base: new Map([
|
||||||
|
[0, 889],
|
||||||
|
[1, 100],
|
||||||
|
[2, 10],
|
||||||
|
[3, 1],
|
||||||
|
]),
|
||||||
|
additions: {
|
||||||
|
times: value => {
|
||||||
|
for (const item of AdditonMap.times.reverse())
|
||||||
|
if (value >= item.value) return item.additions
|
||||||
|
return new Map()
|
||||||
|
},
|
||||||
|
achievements: value => {
|
||||||
|
const size = value.size
|
||||||
|
for (const item of AdditonMap.achievements.reverse())
|
||||||
|
if (size >= item.value) return item.additions
|
||||||
|
return new Map()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
.screen.point-allocation {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
.allocation-input {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
border: 0.0625rem solid var(--base-text-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
button {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
height: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
line-height: 2rem;
|
||||||
|
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%;
|
||||||
|
> li {
|
||||||
|
flex: 1 0;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0;
|
||||||
|
> span {
|
||||||
|
width: 100%;
|
||||||
|
height: 2rem;
|
||||||
|
border: 0.0625rem solid var(--base-text-color);
|
||||||
|
border-bottom: none;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 2rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> li.left {
|
||||||
|
> span {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--base-background-color);
|
||||||
|
border: 0.0625rem solid var(--color-primary);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
> button {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
width: 6rem;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 0.5rem;
|
||||||
|
button { flex: 1 0; }
|
||||||
|
}
|
||||||
|
> ul.talent-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
li { width: 100%; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useAllocator, usePointRandomizer, useLeftPoints } from '@remake/hooks'
|
||||||
|
import { usePicked, useReplaced } from '@remake/hooks'
|
||||||
|
import type { AdditionalPoint } from '@remake/hooks'
|
||||||
|
import { properties } from '@/display'
|
||||||
|
import { keys } from '@remake/vitex'
|
||||||
|
import { BasePoints } from '@/config'
|
||||||
|
import { talents } from '@remake/data'
|
||||||
|
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 {
|
||||||
|
source: AdditionalPoint[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function PointsDetail({ source }: PointsDetailProps) {
|
||||||
|
return (
|
||||||
|
<ul className="points-detail">
|
||||||
|
<li>
|
||||||
|
<span>基础</span>
|
||||||
|
<span className="font-mono">{BasePoints}</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 picked = usePicked()
|
||||||
|
const {
|
||||||
|
talents: { chains },
|
||||||
|
additionalPoints: { source },
|
||||||
|
} = useReplaced()
|
||||||
|
const left = useLeftPoints()
|
||||||
|
const [allocation, allocator] = useAllocator()
|
||||||
|
const random = usePointRandomizer()
|
||||||
|
const [showDetail, setShowDetail] = useState(false)
|
||||||
|
const handleNext = () => {
|
||||||
|
// navigate('/life')
|
||||||
|
}
|
||||||
|
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">
|
||||||
|
<li className="left">
|
||||||
|
<span>剩余点数</span>
|
||||||
|
<button
|
||||||
|
className="primary font-mono"
|
||||||
|
onClick={() => setShowDetail(!showDetail)}
|
||||||
|
>
|
||||||
|
{left}
|
||||||
|
</button>
|
||||||
|
{showDetail && <PointsDetail source={source} />}
|
||||||
|
</li>
|
||||||
|
{keys(allocation).map(key => (
|
||||||
|
<li key={key}>
|
||||||
|
<span>{properties[key]}</span>
|
||||||
|
<AllocInput
|
||||||
|
point={allocation[key]}
|
||||||
|
onChange={value => allocator(key, value)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<button className="info" onClick={() => random()}>
|
||||||
|
随机分配
|
||||||
|
</button>
|
||||||
|
<button className="primary" onClick={() => {}}>
|
||||||
|
开始新人生
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default Alloc
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.screen.home {
|
||||||
|
gap: 5rem;
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
h1 {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { useRemake } from '@remake/hooks'
|
||||||
|
import './Home.css'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const remake = useRemake()
|
||||||
|
return (
|
||||||
|
<div className="screen home">
|
||||||
|
<div className="title">
|
||||||
|
<h1>人生重开模拟器</h1>
|
||||||
|
<h2>这垃圾人生一秒也不想待了</h2>
|
||||||
|
</div>
|
||||||
|
<button className="primary" onClick={remake}>
|
||||||
|
立即重开
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export function Loading() {
|
||||||
|
return (
|
||||||
|
<div className="screen loading">
|
||||||
|
<h2>载入中...</h2>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Loading
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Life() {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default function Summary() {
|
||||||
|
return (
|
||||||
|
<div className="screen summary">
|
||||||
|
<h2>人生总结</h2>
|
||||||
|
<p>最终人生评分: {}</p>
|
||||||
|
{/* 展现解锁的成就等 */}
|
||||||
|
<button onClick={() => {}}>并入存档,重回首页</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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: 0.5rem;
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0;
|
||||||
|
.talent {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// import { useEffect } from 'react'
|
||||||
|
import { useTalentPuller, useTalentPicker } from '@remake/hooks'
|
||||||
|
import { useTalentSubmit, useSubmitIsEnable } from '@remake/hooks'
|
||||||
|
import TalentComponent from '@/components/Talent'
|
||||||
|
import './TalentPick.css'
|
||||||
|
|
||||||
|
export default function TalentPick() {
|
||||||
|
const [pulled, puller] = useTalentPuller()
|
||||||
|
const [talents, picker] = useTalentPicker()
|
||||||
|
const [enabled, limit] = useSubmitIsEnable()
|
||||||
|
const submit = useTalentSubmit()
|
||||||
|
// useEffect(puller, [])
|
||||||
|
if (!pulled)
|
||||||
|
return (
|
||||||
|
<div className="screen talent-pick">
|
||||||
|
<button className="primary" onClick={() => puller()}>
|
||||||
|
十连抽!
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<div className="screen talent-pick">
|
||||||
|
<ul className="talent-list">
|
||||||
|
{pulled.map(id => (
|
||||||
|
<li key={id} onClick={() => picker(id)}>
|
||||||
|
<TalentComponent id={id} selected={talents.has(id)} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{enabled ? (
|
||||||
|
<button className="primary" onClick={() => submit()}>
|
||||||
|
下一步
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button className="error">请选取 {limit} 个天赋</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export const properties = {
|
||||||
|
charm: '颜值',
|
||||||
|
intelligence: '智力',
|
||||||
|
strength: '体质',
|
||||||
|
money: '家境',
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useCallback, useTransition } from 'react'
|
||||||
|
import { atom, useAtom } from 'jotai'
|
||||||
|
import { useConfigInject, useProfile, useProfileInject } 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 [inited, setInited] = useAtom(initedAtom)
|
||||||
|
const [_, startTransition] = useTransition()
|
||||||
|
const loader = useCallback(() => {
|
||||||
|
configInject(config)
|
||||||
|
startTransition(async () => {
|
||||||
|
const { profile } = await get(['profile'])
|
||||||
|
const parsed = profile ? JSON.parse(profile) || {} : {}
|
||||||
|
profileInject({
|
||||||
|
...parsed,
|
||||||
|
times: parsed.times || 0,
|
||||||
|
achievements: new Set(parsed.achievements || []),
|
||||||
|
events: new Set(parsed.events || []),
|
||||||
|
talents: new Set(parsed.talents || []),
|
||||||
|
})
|
||||||
|
setInited(true)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
return [inited, loader] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSaver = () => {
|
||||||
|
const [profile] = useProfile()
|
||||||
|
const saver = useCallback(() => {
|
||||||
|
const str = JSON.stringify({
|
||||||
|
times: profile.times,
|
||||||
|
achievements: Array.from(profile.achievements),
|
||||||
|
events: Array.from(profile.events),
|
||||||
|
talents: Array.from(profile.talents),
|
||||||
|
})
|
||||||
|
set({ profile: str })
|
||||||
|
}, [profile])
|
||||||
|
return saver
|
||||||
|
}
|
||||||
+16
-5
@@ -1,6 +1,17 @@
|
|||||||
import { render } from 'preact'
|
import { createRoot } from 'react-dom/client'
|
||||||
export function App() {
|
import { StrictMode } from 'react'
|
||||||
return <div>Hello</div>
|
import { Game } from './Game'
|
||||||
}
|
import { ThemeToggle } from './components/ThemeToggle'
|
||||||
|
import { Jotai } from './jotai'
|
||||||
|
import './styles/colors.css'
|
||||||
|
import './styles/common.css'
|
||||||
|
|
||||||
render(<App />, document.getElementById('app')!)
|
const container = document.getElementById('remake')!
|
||||||
|
const root = createRoot(container)
|
||||||
|
root.render(
|
||||||
|
<StrictMode>
|
||||||
|
<Jotai />
|
||||||
|
<ThemeToggle />
|
||||||
|
<Game />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { useAtomsDevtools } from 'jotai-devtools/utils'
|
||||||
|
export function Jotai() {
|
||||||
|
useAtomsDevtools('Store')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
export default Jotai
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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,14 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light dark;
|
||||||
|
--base-text-color: light-dark(#121314, #f9fafb);
|
||||||
|
--base-background-color: light-dark(#f7f7f7, #121314);
|
||||||
|
|
||||||
|
--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-primary: light-dark(#61b700, #b9ff69);
|
||||||
|
--color-info: light-dark(#454ad3, #b5b7ff);
|
||||||
|
--color-error: light-dark(#e42b2b, #ff6969);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
html { transition: all 0.2s ease-in-out; }
|
||||||
|
html[data-theme='light'] { color-scheme: light; }
|
||||||
|
html[data-theme='dark'] { color-scheme: dark; }
|
||||||
|
:root { color: var(--base-text-color); }
|
||||||
|
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);
|
||||||
|
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: 1.2rem;
|
||||||
|
padding: 0 1rem;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
--background-color: var(--style-color);
|
||||||
|
--text-color: var(--style-background);
|
||||||
|
}
|
||||||
|
&.primary { --style-color: var(--color-primary); }
|
||||||
|
&.info { --style-color: var(--color-info); }
|
||||||
|
&.error { --style-color: var(--color-error); }
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 1rem;
|
||||||
|
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.8vw; } }
|
||||||
+4
-30
@@ -1,39 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Environment setup & latest features
|
|
||||||
"lib": ["ESNext", "DOM"],
|
"lib": ["ESNext", "DOM"],
|
||||||
"target": "ESNext",
|
|
||||||
"module": "Preserve",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"allowJs": true,
|
"types": ["react", "vite/client"],
|
||||||
"types": ["preact"],
|
"jsxImportSource": "react",
|
||||||
|
|
||||||
// Bundler mode
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
// Best practices
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"jsxImportSource": "preact",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"],
|
||||||
"@assets/*": ["./assets/*"],
|
|
||||||
"react": ["./node_modules/preact/compat/"],
|
|
||||||
"react-dom": ["./node_modules/preact/compat/"]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
|
||||||
"noImplicitAny": true
|
|
||||||
},
|
},
|
||||||
"include": ["node_modules/vite/client.d.ts", "**/*"]
|
"include": ["**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import preact from '@preact/preset-vite'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [preact()],
|
plugins: [react()],
|
||||||
|
resolve: { tsconfigPaths: true },
|
||||||
})
|
})
|
||||||
|
|||||||
+6
-4
@@ -9,23 +9,25 @@
|
|||||||
"build:data": "bun --filter @remake/data build",
|
"build:data": "bun --filter @remake/data build",
|
||||||
"preview": "bun --filter @remake/web preview",
|
"preview": "bun --filter @remake/web preview",
|
||||||
"start": "bun --filter @remake/console start",
|
"start": "bun --filter @remake/console start",
|
||||||
"lint": "eslint",
|
"lint": "bunx --bun eslint",
|
||||||
"lint:console": "bun --filter @remake/console lint",
|
"lint:console": "bun --filter @remake/console lint",
|
||||||
"lint:web": "bun --filter @remake/web lint",
|
"lint:web": "bun --filter @remake/web lint",
|
||||||
"lint:core": "bun --filter @remake/core lint",
|
"lint:core": "bun --filter @remake/core lint",
|
||||||
"lint:data": "bun --filter @remake/data lint",
|
"lint:data": "bun --filter @remake/data lint",
|
||||||
"lint:condition": "bun --filter @remake/condition lint",
|
"lint:condition": "bun --filter @remake/condition lint",
|
||||||
|
"lint:hooks": "bun --filter @remake/hooks lint",
|
||||||
"test": "bun --filter @remake/web test",
|
"test": "bun --filter @remake/web test",
|
||||||
"test:console": "bun --filter @remake/console test",
|
"test:console": "bun --filter @remake/console test",
|
||||||
"test:web": "bun --filter @remake/web test",
|
"test:web": "bun --filter @remake/web test",
|
||||||
"test:core": "bun --filter @remake/core test",
|
"test:core": "bun --filter @remake/core test",
|
||||||
"test:data": "bun --filter @remake/data test",
|
"test:data": "bun --filter @remake/data test",
|
||||||
"test:condition": "bun --filter @remake/condition test"
|
"test:condition": "bun --filter @remake/condition test",
|
||||||
|
"test:hooks": "bun --filter @remake/hooks test"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "1.3.14",
|
"@types/bun": "1.3.14",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
"@typescript-eslint/eslint-plugin": "^8.66.0",
|
||||||
"@typescript-eslint/parser": "^8.65.0",
|
"@typescript-eslint/parser": "^8.66.0",
|
||||||
"@typescript/native": "npm:typescript@^7.0.2",
|
"@typescript/native": "npm:typescript@^7.0.2",
|
||||||
"eslint": "^10.8.0",
|
"eslint": "^10.8.0",
|
||||||
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ export function parse(condition: string): ConditionTree {
|
|||||||
const catchString = (i: number): void => {
|
const catchString = (i: number): void => {
|
||||||
const str = condition.substring(cursor, i).trim()
|
const str = condition.substring(cursor, i).trim()
|
||||||
cursor = i
|
cursor = i
|
||||||
if (str) {
|
if (str) stack[0]?.push(str)
|
||||||
stack[0]?.push(str)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
@@ -118,13 +116,13 @@ function checkProp(property: PropertyContainer, condition: string): boolean {
|
|||||||
|
|
||||||
switch (symbol) {
|
switch (symbol) {
|
||||||
case '>':
|
case '>':
|
||||||
return propData > conditionData
|
return (propData ?? 0) > conditionData
|
||||||
case '<':
|
case '<':
|
||||||
return propData < conditionData
|
return (propData ?? 0) < conditionData
|
||||||
case '>=':
|
case '>=':
|
||||||
return propData >= conditionData
|
return (propData ?? 0) >= conditionData
|
||||||
case '<=':
|
case '<=':
|
||||||
return propData <= conditionData
|
return (propData ?? 0) <= conditionData
|
||||||
case '=':
|
case '=':
|
||||||
if (propData instanceof Set) {
|
if (propData instanceof Set) {
|
||||||
return propData.has(conditionData)
|
return propData.has(conditionData)
|
||||||
@@ -132,7 +130,7 @@ function checkProp(property: PropertyContainer, condition: string): boolean {
|
|||||||
if (Array.isArray(propData)) {
|
if (Array.isArray(propData)) {
|
||||||
return propData.includes(conditionData)
|
return propData.includes(conditionData)
|
||||||
}
|
}
|
||||||
return propData == conditionData
|
return (propData ?? 0) == conditionData
|
||||||
case '!=':
|
case '!=':
|
||||||
if (propData instanceof Set) {
|
if (propData instanceof Set) {
|
||||||
return !propData.has(conditionData)
|
return !propData.has(conditionData)
|
||||||
@@ -140,7 +138,7 @@ function checkProp(property: PropertyContainer, condition: string): boolean {
|
|||||||
if (Array.isArray(propData)) {
|
if (Array.isArray(propData)) {
|
||||||
return !propData.includes(conditionData)
|
return !propData.includes(conditionData)
|
||||||
}
|
}
|
||||||
return propData != conditionData
|
return (propData ?? 0) != conditionData
|
||||||
|
|
||||||
case '?': // 🌟 包含判定符(如 属性值 是否在 [1,2,3] 数组范围内)
|
case '?': // 🌟 包含判定符(如 属性值 是否在 [1,2,3] 数组范围内)
|
||||||
if (propData instanceof Set) {
|
if (propData instanceof Set) {
|
||||||
|
|||||||
@@ -1,29 +1,4 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"extends": "../../tsconfig.json",
|
||||||
// Environment setup & latest features
|
|
||||||
"lib": ["ESNext"],
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "Preserve",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"types": ["bun"],
|
|
||||||
|
|
||||||
// Bundler mode
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
// Best practices
|
|
||||||
"strict": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
|
||||||
"noImplicitAny": true
|
|
||||||
},
|
|
||||||
"include": ["**/*"]
|
"include": ["**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"author": "Vick Scarlet <vick@syaro.io>",
|
"author": "Vick Scarlet <vick@syaro.io>",
|
||||||
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
@@ -15,5 +16,17 @@
|
|||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "../../package.json"
|
"extends": "../../package.json"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"import": "./src/index.ts",
|
||||||
|
"default": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"types": "./src/*.ts",
|
||||||
|
"import": "./src/*.ts",
|
||||||
|
"default": "./src/*.ts"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expect, test, describe } from 'bun:test'
|
import { expect, test, describe } from 'bun:test'
|
||||||
import { createState } from './state'
|
import { createState } from './state'
|
||||||
import { trigger } from './achievement'
|
import { trigger } from './achievement'
|
||||||
|
import { AchievementOpportunity } from '@remake/data'
|
||||||
import { enableMapSet } from 'immer'
|
import { enableMapSet } from 'immer'
|
||||||
enableMapSet()
|
enableMapSet()
|
||||||
|
|
||||||
@@ -20,7 +21,11 @@ describe('Achievement', () => {
|
|||||||
|
|
||||||
test('trigger [times:500]', () => {
|
test('trigger [times:500]', () => {
|
||||||
const p = { ...profile, times: 500 }
|
const p = { ...profile, times: 500 }
|
||||||
const result = trigger('END', createState(allocation, []), p)
|
const result = trigger(
|
||||||
|
AchievementOpportunity.End,
|
||||||
|
createState(allocation, []),
|
||||||
|
p,
|
||||||
|
)
|
||||||
expect(result.state.achievements).toContain(101) // 既视感
|
expect(result.state.achievements).toContain(101) // 既视感
|
||||||
expect(result.state.achievements).toContain(102) // 孟婆愁
|
expect(result.state.achievements).toContain(102) // 孟婆愁
|
||||||
expect(result.state.achievements).toContain(103) // 所有人都是我
|
expect(result.state.achievements).toContain(103) // 所有人都是我
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import type {
|
import type { Achievement, AchievementOpportunity } from '@remake/data'
|
||||||
Achievement,
|
import { achievements } from '@remake/data'
|
||||||
AchievementOpportunity,
|
import type { GameState, ProfileState } from './state'
|
||||||
} from '@remake/data/achievement'
|
|
||||||
import achievements from '@remake/data/achievement'
|
|
||||||
import type { GameState, ProfileState } from '@/state'
|
|
||||||
import { createFlatState } from './state'
|
import { createFlatState } from './state'
|
||||||
import { check } from '@remake/condition'
|
import { check } from '@remake/condition'
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import type { TriggerResult } from '@/game'
|
import type { TriggerResult } from './game'
|
||||||
|
|
||||||
const OpportunityMap = Map.groupBy(
|
const OpportunityMap = Map.groupBy(
|
||||||
achievements.keys(),
|
achievements.keys(),
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Event } from '@remake/data/event'
|
import type { Event } from '@remake/data/event'
|
||||||
import events from '@remake/data/event'
|
import events from '@remake/data/event'
|
||||||
import type { Properties } from '@/state'
|
import type { Properties } from './state'
|
||||||
import type { GameState, ProfileState } from '@/state'
|
import type { GameState, ProfileState } from './state'
|
||||||
import { propsEffect, createFlatState } from '@/state'
|
import { propsEffect, createFlatState } from './state'
|
||||||
import { check as checkCondition } from '@remake/condition'
|
import { check as checkCondition } from '@remake/condition'
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import type { TriggerResult } from '@/game'
|
import type { TriggerResult } from './game'
|
||||||
|
|
||||||
export function check(
|
export function check(
|
||||||
event: Event['id'],
|
event: Event['id'],
|
||||||
@@ -30,7 +30,7 @@ export function trigger(
|
|||||||
draft.events.add(eventId)
|
draft.events.add(eventId)
|
||||||
if (!effect) return
|
if (!effect) return
|
||||||
if (effect.LIF) draft.life += effect.LIF
|
if (effect.LIF) draft.life += effect.LIF
|
||||||
const pe = {} as Partial<Properties>
|
const pe: Partial<Properties> = {}
|
||||||
if (effect.CHR) pe.charm = effect.CHR
|
if (effect.CHR) pe.charm = effect.CHR
|
||||||
if (effect.INT) pe.intelligence = effect.INT
|
if (effect.INT) pe.intelligence = effect.INT
|
||||||
if (effect.STR) pe.strength = effect.STR
|
if (effect.STR) pe.strength = effect.STR
|
||||||
|
|||||||
+26
-39
@@ -1,26 +1,23 @@
|
|||||||
import ages from '@remake/data/age'
|
import type { Achievement, Event, Talent } from '@remake/data'
|
||||||
import { propsEffect } from '@/state'
|
import { ages, AchievementOpportunity as Ao } from '@remake/data'
|
||||||
import { enableMapSet, produce } from 'immer'
|
import type { Allocation, GameState, ProfileState } from './state'
|
||||||
|
import { createState, nextProfile, propsEffect } from './state'
|
||||||
|
import { summary as stateSummary } from './state'
|
||||||
|
import type { PullOptions, ReplacementResult } from './talent'
|
||||||
|
import type { AdditionalPoint, AdditionalPoints } from './talent'
|
||||||
|
import { pull, exclude, replacement, additionalPoints } from './talent'
|
||||||
|
import { trigger as ttr } from './talent'
|
||||||
|
import { trigger as atr } from './achievement'
|
||||||
|
import { trigger as etr, check as ec } from './event'
|
||||||
|
import type { RNG } from '@remake/vitex'
|
||||||
|
import { pickWeight } from '@remake/vitex'
|
||||||
|
import { produce, enableMapSet } from 'immer'
|
||||||
enableMapSet()
|
enableMapSet()
|
||||||
|
|
||||||
export type * as achievement from '@/achievement'
|
|
||||||
export type * as event from '@/event'
|
|
||||||
export type * as talent from '@/talent'
|
|
||||||
export type * as state from '@/state'
|
|
||||||
import type { GameState, ProfileState } from '@/state'
|
|
||||||
export type { GameState, ProfileState }
|
|
||||||
export interface TriggerResult<T> {
|
export interface TriggerResult<T> {
|
||||||
state: GameState
|
state: GameState
|
||||||
triggers: T[]
|
triggers: T[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export { pull } from '@/talent'
|
|
||||||
// export { summary, nextProfile } from '@/state'
|
|
||||||
|
|
||||||
import { replacement, additionalPoints } from '@/talent'
|
|
||||||
import type { ReplacementResult, AdditionalPoints } from '@/talent'
|
|
||||||
import type { RNG } from '@remake/vitex'
|
|
||||||
|
|
||||||
export interface TalentsPickedResult {
|
export interface TalentsPickedResult {
|
||||||
talents: ReplacementResult
|
talents: ReplacementResult
|
||||||
additionalPoints: AdditionalPoints
|
additionalPoints: AdditionalPoints
|
||||||
@@ -34,8 +31,6 @@ export function talentsPicked(
|
|||||||
return { talents: r, additionalPoints: ap }
|
return { talents: r, additionalPoints: ap }
|
||||||
}
|
}
|
||||||
|
|
||||||
import { createState } from '@/state'
|
|
||||||
|
|
||||||
export interface StartResult {
|
export interface StartResult {
|
||||||
state: GameState
|
state: GameState
|
||||||
achievements: Achievement['id'][]
|
achievements: Achievement['id'][]
|
||||||
@@ -45,19 +40,9 @@ export function start(
|
|||||||
...args: Parameters<typeof createState>
|
...args: Parameters<typeof createState>
|
||||||
): StartResult {
|
): StartResult {
|
||||||
const state = createState(...args)
|
const state = createState(...args)
|
||||||
const ar = achievementTrigger('START', state, profile)
|
const ar = atr(Ao.Start, state, profile)
|
||||||
return { state: ar.state, achievements: ar.triggers }
|
return { state: ar.state, achievements: ar.triggers }
|
||||||
}
|
}
|
||||||
|
|
||||||
import type { Achievement } from '@remake/data/achievement'
|
|
||||||
import type { Event } from '@remake/data/event'
|
|
||||||
import type { Talent } from '@remake/data/talent'
|
|
||||||
import { trigger as achievementTrigger } from '@/achievement'
|
|
||||||
import { trigger as eventTrigger } from '@/event'
|
|
||||||
import { trigger as talentTrigger } from '@/talent'
|
|
||||||
import { check as eventCheck } from '@/event'
|
|
||||||
import { pickWeight } from '@remake/vitex'
|
|
||||||
|
|
||||||
export interface NextResult {
|
export interface NextResult {
|
||||||
state: GameState
|
state: GameState
|
||||||
age: number
|
age: number
|
||||||
@@ -76,13 +61,13 @@ export function next(
|
|||||||
draft.props = propsEffect(state.props, { age: 1 })
|
draft.props = propsEffect(state.props, { age: 1 })
|
||||||
})
|
})
|
||||||
const age = s.props.current.age
|
const age = s.props.current.age
|
||||||
const tr = talentTrigger(s, profile, rng)
|
const tr = ttr(s, profile, rng)
|
||||||
const events = ages
|
const events = ages
|
||||||
.get(age)!
|
.get(age)!
|
||||||
.event.filter(([e]) => eventCheck(e, tr.state, profile))
|
.event.filter(([e]) => ec(e, tr.state, profile))
|
||||||
const event = pickWeight(events, rng)!
|
const event = pickWeight(events, rng)!
|
||||||
const er = eventTrigger(event, tr.state, profile)
|
const er = etr(event, tr.state, profile)
|
||||||
const ar = achievementTrigger('TRAJECTORY', er.state, profile)
|
const ar = atr(Ao.Trajectory, er.state, profile)
|
||||||
const end = ar.state.life < 1
|
const end = ar.state.life < 1
|
||||||
return {
|
return {
|
||||||
state: ar.state,
|
state: ar.state,
|
||||||
@@ -94,8 +79,6 @@ export function next(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import { summary as stateSummary } from '@/state'
|
|
||||||
|
|
||||||
export interface SummaryResult {
|
export interface SummaryResult {
|
||||||
state: GameState
|
state: GameState
|
||||||
summary: number
|
summary: number
|
||||||
@@ -105,12 +88,11 @@ export function summary(
|
|||||||
state: GameState,
|
state: GameState,
|
||||||
profile: ProfileState,
|
profile: ProfileState,
|
||||||
): SummaryResult {
|
): SummaryResult {
|
||||||
const ar = achievementTrigger('SUMMARY', state, profile)
|
const ar = atr(Ao.Summary, state, profile)
|
||||||
const s = stateSummary(ar.state)
|
const s = stateSummary(ar.state)
|
||||||
return { state: ar.state, summary: s, achievements: ar.triggers }
|
return { state: ar.state, summary: s, achievements: ar.triggers }
|
||||||
}
|
}
|
||||||
|
|
||||||
import { nextProfile } from '@/state'
|
|
||||||
export interface EndResult {
|
export interface EndResult {
|
||||||
profile: ProfileState
|
profile: ProfileState
|
||||||
achievements: Achievement['id'][]
|
achievements: Achievement['id'][]
|
||||||
@@ -121,7 +103,12 @@ export function end(
|
|||||||
profile: ProfileState,
|
profile: ProfileState,
|
||||||
lockedTalent?: Talent['id'],
|
lockedTalent?: Talent['id'],
|
||||||
) {
|
) {
|
||||||
const ar = achievementTrigger('END', state, profile)
|
const ar = atr(Ao.End, state, profile)
|
||||||
const p = nextProfile(profile, ar.state, lockedTalent)
|
const p = nextProfile(profile, ar.state, lockedTalent)
|
||||||
return { profile: p, achievements: ar.triggers }
|
return { profile: p, achievements: ar.triggers }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { pull, exclude }
|
||||||
|
export type { GameState, ProfileState, Allocation, RNG }
|
||||||
|
export type { PullOptions, ReplacementResult }
|
||||||
|
export type { AdditionalPoint, AdditionalPoints }
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from '@/game'
|
export * from './game'
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { expect, test, describe } from 'bun:test'
|
import { expect, test, describe } from 'bun:test'
|
||||||
import { parse, type ConditionNode } from '@remake/condition'
|
import { parse, type ConditionNode } from '@remake/condition'
|
||||||
|
|
||||||
import achievements from '@remake/data/achievement'
|
import { achievements, events, talents } from '@remake/data'
|
||||||
import events from '@remake/data/event'
|
|
||||||
import talents from '@remake/data/talent'
|
|
||||||
|
|
||||||
import type { GameState, ProfileState, Properties } from './state'
|
import type { GameState, ProfileState, Properties } from './state'
|
||||||
import { createFlatState, createHLProperties, propsEffect } from './state'
|
import { createFlatState, createHLProperties, propsEffect } from './state'
|
||||||
import { SupportedFlatStateKeys } from './state'
|
import { SupportedFlatStateKeys } from './state'
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { Talent } from '@remake/data/talent'
|
import type { Achievement, Event, Talent } from '@remake/data'
|
||||||
import type { Event } from '@remake/data/event'
|
|
||||||
import type { Achievement } from '@remake/data/achievement'
|
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import { sum } from '@remake/vitex'
|
import { sum, keys } from '@remake/vitex'
|
||||||
|
|
||||||
/** 基础的属性 */
|
/** 基础的属性 */
|
||||||
export interface Properties {
|
export interface Properties {
|
||||||
@@ -112,7 +110,7 @@ type FlatMapper<Key extends FlatStateKey> = (
|
|||||||
state: FlatTarget,
|
state: FlatTarget,
|
||||||
) => FlatState[Key]
|
) => FlatState[Key]
|
||||||
|
|
||||||
const FlatMappers = {
|
const FlatMappers: { [Key in FlatStateKey]: FlatMapper<Key> } = {
|
||||||
AGE: state => state.game.props.current.age,
|
AGE: state => state.game.props.current.age,
|
||||||
CHR: state => state.game.props.current.charm,
|
CHR: state => state.game.props.current.charm,
|
||||||
INT: state => state.game.props.current.intelligence,
|
INT: state => state.game.props.current.intelligence,
|
||||||
@@ -139,9 +137,9 @@ const FlatMappers = {
|
|||||||
ATLT: state => state.profile.talents,
|
ATLT: state => state.profile.talents,
|
||||||
AACH: state => state.profile.achievements,
|
AACH: state => state.profile.achievements,
|
||||||
SUM: ({ game }) => summary(game),
|
SUM: ({ game }) => summary(game),
|
||||||
} as { [Key in FlatStateKey]: FlatMapper<Key> }
|
}
|
||||||
|
|
||||||
export const SupportedFlatStateKeys = new Set(Object.keys(FlatMappers))
|
export const SupportedFlatStateKeys = new Set(keys(FlatMappers))
|
||||||
|
|
||||||
const flatStateHandle = {
|
const flatStateHandle = {
|
||||||
get<Key extends FlatStateKey>(target: FlatTarget, prop: Key) {
|
get<Key extends FlatStateKey>(target: FlatTarget, prop: Key) {
|
||||||
@@ -175,7 +173,7 @@ export function propsEffect(hlp: HLProperties, effect: Partial<Properties>) {
|
|||||||
export function highestProperties(a: Properties, b?: Properties): Properties {
|
export function highestProperties(a: Properties, b?: Properties): Properties {
|
||||||
if (!b) return { ...a }
|
if (!b) return { ...a }
|
||||||
const result = {} as Properties
|
const result = {} as Properties
|
||||||
for (const key of Object.keys(a) as (keyof Properties)[]) {
|
for (const key of keys(a)) {
|
||||||
result[key] = Math.max(a[key], b[key])
|
result[key] = Math.max(a[key], b[key])
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -184,7 +182,7 @@ export function highestProperties(a: Properties, b?: Properties): Properties {
|
|||||||
export function lowestProperties(a: Properties, b?: Properties): Properties {
|
export function lowestProperties(a: Properties, b?: Properties): Properties {
|
||||||
if (!b) return { ...a }
|
if (!b) return { ...a }
|
||||||
const result = {} as Properties
|
const result = {} as Properties
|
||||||
for (const key of Object.keys(a) as (keyof Properties)[]) {
|
for (const key of keys(a)) {
|
||||||
result[key] = Math.min(a[key], b[key])
|
result[key] = Math.min(a[key], b[key])
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { expect, test, describe } from 'bun:test'
|
|||||||
import type { ProfileState } from './state'
|
import type { ProfileState } from './state'
|
||||||
import type { PullOptions } from './talent'
|
import type { PullOptions } from './talent'
|
||||||
import { pull, exclude, additionalPoints, replacement } from './talent'
|
import { pull, exclude, additionalPoints, replacement } from './talent'
|
||||||
import { createState } from './state'
|
import { talents } from '@remake/data'
|
||||||
import talents from '@remake/data/talent'
|
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import { enableMapSet } from 'immer'
|
import { enableMapSet } from 'immer'
|
||||||
enableMapSet()
|
enableMapSet()
|
||||||
|
|||||||
+10
-10
@@ -1,12 +1,12 @@
|
|||||||
import type { Talent, TalentGrade } from '@remake/data/talent'
|
import type { Talent, TalentGrade } from '@remake/data'
|
||||||
import talents from '@remake/data/talent'
|
import { talents } from '@remake/data'
|
||||||
import type { Properties } from '@/state'
|
import type { Properties } from './state'
|
||||||
import type { GameState, ProfileState } from '@/state'
|
import type { GameState, ProfileState } from './state'
|
||||||
import { propsEffect, createFlatState } from './state'
|
import { propsEffect, createFlatState } from './state'
|
||||||
import { check } from '@remake/condition'
|
import { check } from '@remake/condition'
|
||||||
import { pick, pickWeight, type RNG } from '@remake/vitex'
|
import { pick, pickWeight, type RNG } from '@remake/vitex'
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import type { TriggerResult } from '@/game'
|
import type { TriggerResult } from './game'
|
||||||
|
|
||||||
const Grades: TalentGrade[] = [0, 1, 2, 3] as const
|
const Grades: TalentGrade[] = [0, 1, 2, 3] as const
|
||||||
const GradeMap = Map.groupBy(
|
const GradeMap = Map.groupBy(
|
||||||
@@ -16,10 +16,6 @@ const GradeMap = Map.groupBy(
|
|||||||
|
|
||||||
export const count = talents.size
|
export const count = talents.size
|
||||||
|
|
||||||
export function get(talent: number) {
|
|
||||||
return talents.get(talent)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PullRate = Map<TalentGrade, number>
|
export type PullRate = Map<TalentGrade, number>
|
||||||
export type PullRateAddition = {
|
export type PullRateAddition = {
|
||||||
mode: 'add' | 'multiply'
|
mode: 'add' | 'multiply'
|
||||||
@@ -64,7 +60,11 @@ export interface PullOptions {
|
|||||||
rate: RateOptions
|
rate: RateOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pull(options: PullOptions, profile: ProfileState, rng?: RNG) {
|
export function pull(
|
||||||
|
options: PullOptions,
|
||||||
|
profile: ProfileState,
|
||||||
|
rng?: RNG,
|
||||||
|
): Talent['id'][] {
|
||||||
const rate = Array.from(
|
const rate = Array.from(
|
||||||
talentRateWithAddition(options.rate, profile).entries(),
|
talentRateWithAddition(options.rate, profile).entries(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,32 +1,4 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"extends": "../../tsconfig.json",
|
||||||
// Environment setup & latest features
|
|
||||||
"lib": ["ESNext"],
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "Preserve",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"types": ["bun"],
|
|
||||||
|
|
||||||
// Bundler mode
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
// Best practices
|
|
||||||
"strict": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
|
||||||
"noImplicitAny": true
|
|
||||||
},
|
|
||||||
"include": ["**/*"]
|
"include": ["**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,15 @@
|
|||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"author": "Vick Scarlet <vick@syaro.io>",
|
"author": "Vick Scarlet <vick@syaro.io>",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bunx --bun v-transform@3.0.1 transform -t ts -w src -d dist \"**/[!~$]*.xlsx\" ",
|
"build": "bunx --bun v-transform transform -t ts -w src -d dist \"**/[!~$]*.xlsx\" --map",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"import": "./src/index.ts"
|
||||||
|
},
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./dist/*.ts",
|
"types": "./dist/*.ts",
|
||||||
"import": "./dist/*.ts"
|
"import": "./dist/*.ts"
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
/** 成就稀有度 */
|
/** 成就稀有度 */
|
||||||
export type AchievementGrade = 0 | 1 | 2 | 3
|
export enum AchievementGrade {
|
||||||
|
White = 0,
|
||||||
|
Blue = 1,
|
||||||
|
Purple = 2,
|
||||||
|
Orange = 3,
|
||||||
|
}
|
||||||
|
|
||||||
/** 成就触发时机 */
|
/** 成就触发时机 */
|
||||||
export type AchievementOpportunity =
|
export enum AchievementOpportunity {
|
||||||
| 'START' // 分配完成点数,点击开始新人生后
|
Start = 'START', // 分配完成点数,点击开始新人生后
|
||||||
| 'TRAJECTORY' // 每一年的人生经历中
|
Trajectory = 'TRAJECTORY', // 每一年的人生经历中
|
||||||
| 'SUMMARY' // 人生结束,点击人生总结后
|
Summary = 'SUMMARY', // 人生结束,点击人生总结后
|
||||||
| 'END' // 游戏完成,点击重开 重开次数在这之后才会+1
|
End = 'END', // 游戏完成,点击重开 重开次数在这之后才会+1
|
||||||
|
}
|
||||||
|
|
||||||
/** 成就 */
|
/** 成就 */
|
||||||
export type Achievement = {
|
export type Achievement = {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export * from '../dist/achievement'
|
||||||
|
export { default as achievements } from '../dist/achievement'
|
||||||
|
export * from '../dist/age'
|
||||||
|
export { default as ages } from '../dist/age'
|
||||||
|
export * from '../dist/character'
|
||||||
|
export { default as characters } from '../dist/character'
|
||||||
|
export * from '../dist/event'
|
||||||
|
export { default as events } from '../dist/event'
|
||||||
|
export * from '../dist/specialthanks'
|
||||||
|
export { default as specialThanks } from '../dist/specialthanks'
|
||||||
|
export * from '../dist/talent'
|
||||||
|
export { default as talents } from '../dist/talent'
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
/** 天赋稀有度 */
|
/** 天赋稀有度 */
|
||||||
export type TalentGrade = 0 | 1 | 2 | 3
|
export enum TalentGrade {
|
||||||
|
White = 0,
|
||||||
|
Blue = 1,
|
||||||
|
Purple = 2,
|
||||||
|
Orange = 3,
|
||||||
|
}
|
||||||
|
|
||||||
/** 天赋效果 */
|
/** 天赋效果 */
|
||||||
export type TalentEffect = {
|
export type TalentEffect = {
|
||||||
|
|||||||
@@ -1,29 +1,4 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"extends": "../../tsconfig.json",
|
||||||
// Environment setup & latest features
|
|
||||||
"lib": ["ESNext"],
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "Preserve",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"types": ["bun"],
|
|
||||||
|
|
||||||
// Bundler mode
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
// Best practices
|
|
||||||
"strict": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
|
||||||
"noImplicitAny": true
|
|
||||||
},
|
|
||||||
"include": ["**/*"]
|
"include": ["**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[test]
|
||||||
|
preload = ["./setup-tests.ts"] # 在运行测试前加载环境
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@remake/hooks",
|
||||||
|
"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/core": "workspace:*",
|
||||||
|
"@remake/data": "workspace:*",
|
||||||
|
"@remake/vitex": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"jotai": "^2.20.2",
|
||||||
|
"react": "^19.2.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@happy-dom/global-registrator": "^20.11.1",
|
||||||
|
"@testing-library/jest-dom": "^7.0.0",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@types/react": "^19.2.18",
|
||||||
|
"eslint-config-react": "^1.1.7"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"import": "./src/*.ts",
|
||||||
|
"types": "./src/*.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { GlobalRegistrator } from '@happy-dom/global-registrator'
|
||||||
|
GlobalRegistrator.register()
|
||||||
|
import { afterEach, expect } from 'bun:test'
|
||||||
|
import { cleanup } from '@testing-library/react'
|
||||||
|
import * as matchers from '@testing-library/jest-dom/matchers'
|
||||||
|
|
||||||
|
expect.extend(matchers)
|
||||||
|
|
||||||
|
// Optional: cleans up `render` after each test
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
import { useConfig } from './config'
|
||||||
|
import { useReplaced } from './talent'
|
||||||
|
import type { Allocation } from '@remake/core'
|
||||||
|
import type { RNG } from '@remake/vitex'
|
||||||
|
import { keys, shuffle, random as frandom } from '@remake/vitex'
|
||||||
|
|
||||||
|
const init: Allocation = { charm: 0, intelligence: 0, strength: 0, money: 0 }
|
||||||
|
const allocAtom = atom<Allocation>(init)
|
||||||
|
|
||||||
|
const alloced = (alloc: Allocation) =>
|
||||||
|
Object.values(alloc).reduce((a, b) => a + b, 0)
|
||||||
|
|
||||||
|
export const usePoints = () => {
|
||||||
|
const { points } = useConfig()
|
||||||
|
const { additionalPoints } = useReplaced()
|
||||||
|
return Math.max(points + additionalPoints.points, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLeftPoints = () => {
|
||||||
|
const points = usePoints()
|
||||||
|
const alloc = useAtomValue(allocAtom)
|
||||||
|
return points - alloced(alloc)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAllocator = () => {
|
||||||
|
const { allocate } = useConfig()
|
||||||
|
const total = usePoints()
|
||||||
|
const [alloc, setAlloc] = useAtom(allocAtom)
|
||||||
|
const allocator = useCallback(
|
||||||
|
(key: keyof Allocation, value: number) => {
|
||||||
|
setAlloc(prev => {
|
||||||
|
const left = total - alloced(prev) + prev[key]
|
||||||
|
const max = Math.min(left, allocate)
|
||||||
|
const final = Math.max(Math.min(max, value), 0)
|
||||||
|
if (prev[key] === final) return prev
|
||||||
|
return { ...prev, [key]: final }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[allocate, total],
|
||||||
|
)
|
||||||
|
return [alloc, allocator] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePointRandomizer = () => {
|
||||||
|
const { allocate } = useConfig()
|
||||||
|
const total = usePoints()
|
||||||
|
const setAlloc = useSetAtom(allocAtom)
|
||||||
|
return useCallback(
|
||||||
|
(rng?: RNG) => {
|
||||||
|
const suffled = shuffle(keys(init), rng)
|
||||||
|
let left = total
|
||||||
|
const alloc = { ...init }
|
||||||
|
while (suffled.length) {
|
||||||
|
const key = suffled.pop()!
|
||||||
|
const max = Math.min(allocate, left)
|
||||||
|
const min = Math.max(0, left - suffled.length * allocate)
|
||||||
|
const value = frandom(max, min, rng)
|
||||||
|
alloc[key] = value
|
||||||
|
left -= value
|
||||||
|
}
|
||||||
|
setAlloc(alloc)
|
||||||
|
},
|
||||||
|
[allocate, total],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAllocSubmit = () => {}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { atom, useSetAtom, useAtomValue } from 'jotai'
|
||||||
|
import type { PullOptions } from '@remake/core'
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
pick: number
|
||||||
|
pull: PullOptions
|
||||||
|
points: number
|
||||||
|
allocate: number
|
||||||
|
mode: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configAtom = atom<Config | null>(null)
|
||||||
|
|
||||||
|
export const useConfigInject = () => {
|
||||||
|
const setConfig = useSetAtom(configAtom)
|
||||||
|
return useCallback((config: Config) => setConfig(config), [setConfig])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useConfig = () => {
|
||||||
|
const config = useAtomValue(configAtom)
|
||||||
|
if (!config) throw new Error('Game config is not set')
|
||||||
|
return config
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export * from './config'
|
||||||
|
export * from './profile'
|
||||||
|
export * from './talent'
|
||||||
|
export * from './play'
|
||||||
|
export * from './alloc'
|
||||||
|
export type * from '@remake/core'
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { useTransition, useCallback } from 'react'
|
||||||
|
import { atom, useSetAtom, useAtomValue, useAtom } from 'jotai'
|
||||||
|
import { useConfig } from './config'
|
||||||
|
import { useProfile } from './profile'
|
||||||
|
import { end } from '@remake/core'
|
||||||
|
import type { GameState } from '@remake/core'
|
||||||
|
import type { Talent } from '@remake/data/talent'
|
||||||
|
|
||||||
|
export enum Step {
|
||||||
|
Idle = 'idle',
|
||||||
|
Mode = 'mode',
|
||||||
|
Pick = 'pick',
|
||||||
|
Alloc = 'alloc',
|
||||||
|
Play = 'play',
|
||||||
|
Summary = 'summary',
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepAtom = atom<Step>(Step.Idle)
|
||||||
|
const gameStateAtom = atom<GameState | null>(null)
|
||||||
|
|
||||||
|
export const useStep = () => useAtomValue(stepAtom)
|
||||||
|
export const useSetStep = () => useSetAtom(stepAtom)
|
||||||
|
|
||||||
|
export const useRemake = () => {
|
||||||
|
const config = useConfig()
|
||||||
|
const [profile] = useProfile()
|
||||||
|
const setStep = useSetAtom(stepAtom)
|
||||||
|
return useCallback(() => {
|
||||||
|
const step = profile.times < config.mode ? Step.Pick : Step.Mode
|
||||||
|
setStep(step)
|
||||||
|
}, [config, profile, setStep])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEnd = () => {
|
||||||
|
const [profile, setProfile] = useProfile()
|
||||||
|
const [step, setStep] = useAtom(stepAtom)
|
||||||
|
const [state, setState] = useAtom(gameStateAtom)
|
||||||
|
const [pending, startTransition] = useTransition()
|
||||||
|
const ender = useCallback(
|
||||||
|
(talent?: Talent['id']) => {
|
||||||
|
startTransition(async () => {
|
||||||
|
if (!state || step === Step.Summary)
|
||||||
|
throw new Error(
|
||||||
|
'Game state is not available or already ended.',
|
||||||
|
)
|
||||||
|
const { profile: next, achievements } = end(
|
||||||
|
state,
|
||||||
|
profile,
|
||||||
|
talent,
|
||||||
|
)
|
||||||
|
setStep(Step.Idle)
|
||||||
|
setState(null)
|
||||||
|
setProfile(next)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[state, step, profile, setStep, setState, setProfile],
|
||||||
|
)
|
||||||
|
return [pending, ender] as const
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { describe, test, expect } from 'bun:test'
|
||||||
|
import { screen, renderHook } from '@testing-library/react'
|
||||||
|
import { useProfile, useProfileInject } from './profile'
|
||||||
|
|
||||||
|
describe('profile', () => {
|
||||||
|
test('inject', () => {
|
||||||
|
expect(() => renderHook(() => useProfile())).toThrow()
|
||||||
|
// const inject = renderHook(() => useProfileInject())
|
||||||
|
})
|
||||||
|
test('no inject throw', () => {
|
||||||
|
expect(() => renderHook(() => useProfile())).toThrow()
|
||||||
|
// const inject = renderHook(() => useProfileInject())
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { atom, useAtom, useSetAtom } from 'jotai'
|
||||||
|
import type { ProfileState } from '@remake/core'
|
||||||
|
|
||||||
|
const profileAtom = atom<ProfileState | null>(null)
|
||||||
|
|
||||||
|
export const useProfile = () => {
|
||||||
|
const [profile, setProfile] = useAtom(profileAtom)
|
||||||
|
if (!profile) throw new Error('Profile is not loaded')
|
||||||
|
return [profile, setProfile] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useProfileInject = () => {
|
||||||
|
const setProfile = useSetAtom(profileAtom)
|
||||||
|
return useCallback(
|
||||||
|
(profile: ProfileState) => setProfile(profile),
|
||||||
|
[setProfile],
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { useState, useCallback } from 'react'
|
||||||
|
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
import { useConfig } from './config'
|
||||||
|
import { useProfile } from './profile'
|
||||||
|
import { useSetStep, Step } from './play'
|
||||||
|
import { pull, exclude, talentsPicked } from '@remake/core'
|
||||||
|
import type { TalentsPickedResult, RNG } from '@remake/core'
|
||||||
|
import type { Talent } from '@remake/data/talent'
|
||||||
|
|
||||||
|
const pickedAtom = atom(new Set<Talent['id']>())
|
||||||
|
const replacedAtom = atom<TalentsPickedResult | null>(null)
|
||||||
|
|
||||||
|
export const usePicked = () => {
|
||||||
|
const picked = useAtomValue(pickedAtom)
|
||||||
|
if (!picked) throw new Error('No talents picked')
|
||||||
|
return picked
|
||||||
|
}
|
||||||
|
export const useReplaced = () => {
|
||||||
|
const replaced = useAtomValue(replacedAtom)
|
||||||
|
if (!replaced) throw new Error('No talents replaced')
|
||||||
|
return replaced
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTalentPuller = () => {
|
||||||
|
const { pull: p } = useConfig()
|
||||||
|
const [profile] = useProfile()
|
||||||
|
const [pulled, setPulled] = useState<Talent['id'][] | null>(null)
|
||||||
|
const puller = useCallback(
|
||||||
|
(rng?: RNG) => setPulled(pull(p, profile, rng)),
|
||||||
|
// (rng?: RNG) =>
|
||||||
|
// setPulled([
|
||||||
|
// 1142, 1143, 1144, 1145, 1146, 1086, 1122, 1111, 1130, 1048,
|
||||||
|
// ]),
|
||||||
|
[p, profile, setPulled],
|
||||||
|
)
|
||||||
|
return [pulled, puller] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TalentPickerResult =
|
||||||
|
| {
|
||||||
|
type: 'ok'
|
||||||
|
talent?: never
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'not-enough'
|
||||||
|
talent?: never
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'exclude'
|
||||||
|
talent: Talent['id']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTalentPicker = () => {
|
||||||
|
const { pick } = useConfig()
|
||||||
|
const [picked, setPicked] = useAtom(pickedAtom)
|
||||||
|
const picker = useCallback(
|
||||||
|
(talent: Talent['id']): TalentPickerResult => {
|
||||||
|
if (!picked) {
|
||||||
|
setPicked(new Set([talent]))
|
||||||
|
return { type: 'ok' }
|
||||||
|
}
|
||||||
|
if (picked.has(talent)) {
|
||||||
|
const next = new Set(picked)
|
||||||
|
next.delete(talent)
|
||||||
|
setPicked(next)
|
||||||
|
return { type: 'ok' }
|
||||||
|
}
|
||||||
|
if (picked.size >= pick) return { type: 'not-enough' }
|
||||||
|
const e = exclude(talent, picked)
|
||||||
|
if (e) return { type: 'exclude', talent: e }
|
||||||
|
const next = new Set([...picked, talent])
|
||||||
|
setPicked(next)
|
||||||
|
return { type: 'ok' }
|
||||||
|
},
|
||||||
|
[pick, picked, setPicked],
|
||||||
|
)
|
||||||
|
return [picked, picker] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSubmitIsEnable = () => {
|
||||||
|
const { pick } = useConfig()
|
||||||
|
const picked = useAtomValue(pickedAtom)
|
||||||
|
const enabled = picked.size >= pick
|
||||||
|
return [enabled, pick] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTalentSubmit = () => {
|
||||||
|
const picked = useAtomValue(pickedAtom)
|
||||||
|
const setReplaced = useSetAtom(replacedAtom)
|
||||||
|
const [enabled] = useSubmitIsEnable()
|
||||||
|
const next = useSetStep()
|
||||||
|
return useCallback(
|
||||||
|
(rng?: RNG) => {
|
||||||
|
if (!enabled) throw new Error('Not enough talents picked')
|
||||||
|
setReplaced(talentsPicked(picked, rng))
|
||||||
|
next(Step.Alloc)
|
||||||
|
},
|
||||||
|
[enabled, picked, setReplaced, next],
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["react", "bun"],
|
||||||
|
},
|
||||||
|
"include": ["**/*"]
|
||||||
|
}
|
||||||
@@ -22,3 +22,18 @@ export function pickWeight<T>(items: WeightItem<T>[], rng?: RNG) {
|
|||||||
export function sum(arr: number[]) {
|
export function sum(arr: number[]) {
|
||||||
return arr.reduce((sum, v) => sum + v, 0)
|
return arr.reduce((sum, v) => sum + v, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shuffle<T>(array: T[], rng?: RNG): T[] {
|
||||||
|
const result = [...array]
|
||||||
|
for (let i = result.length - 1; i > 0; i--) {
|
||||||
|
const j = random(i, 0, rng)
|
||||||
|
const temp = result[i]
|
||||||
|
result[i] = result[j]!
|
||||||
|
result[j] = temp!
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keys<T extends object>(obj: T): (keyof T)[] {
|
||||||
|
return Object.keys(obj) as (keyof T)[]
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,29 +1,4 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"extends": "../../tsconfig.json",
|
||||||
// Environment setup & latest features
|
|
||||||
"lib": ["ESNext"],
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "Preserve",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"types": ["bun"],
|
|
||||||
|
|
||||||
// Bundler mode
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
// Best practices
|
|
||||||
"strict": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
|
||||||
"noImplicitAny": true
|
|
||||||
},
|
|
||||||
"include": ["**/*"]
|
"include": ["**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+883
-1924
File diff suppressed because it is too large
Load Diff
Vendored
+1
-23
@@ -1,29 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Environment setup & latest features
|
|
||||||
"lib": ["ESNext", "DOM"],
|
"lib": ["ESNext", "DOM"],
|
||||||
"target": "ESNext",
|
|
||||||
"module": "Preserve",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"types": [],
|
|
||||||
|
|
||||||
// Bundler mode
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
// Best practices
|
|
||||||
"strict": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
|
|
||||||
// Some stricter flags (disabled by default)
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
|
||||||
"noImplicitAny": true
|
|
||||||
},
|
},
|
||||||
"include": ["**/*"]
|
"include": ["**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": ["bun"],
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"noImplicitAny": true
|
||||||
|
},
|
||||||
|
"exclude": ["**/node_modules", "**/dist", "**/build"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user