mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-28 01:06:49 +08:00
update: hooks, web
This commit is contained in:
@@ -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'
|
||||
export function App() {
|
||||
return <div>Hello</div>
|
||||
}
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { StrictMode } from 'react'
|
||||
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; } }
|
||||
Reference in New Issue
Block a user