Commit 875e48fc authored by timel's avatar timel

feat: timer

parent 00aa816c
...@@ -19,7 +19,7 @@ const defaultConfig: DefaultsConfig = { ...@@ -19,7 +19,7 @@ const defaultConfig: DefaultsConfig = {
const aiModeConfig: DefaultsConfig = { const aiModeConfig: DefaultsConfig = {
...defaultConfig, ...defaultConfig,
defaultDeck: VITE_AI_MODE_DEFAULT_DECK || "Hero", defaultDeck: VITE_AI_MODE_DEFAULT_DECK || "Hero",
defaultPlayer: `AiKiller${Math.random().toString(36).slice(2)}}`, defaultPlayer: `AiKiller-${Math.random().toString(36).slice(2, 6)}}`,
defaultPassword: "AI", defaultPassword: "AI",
}; };
......
...@@ -41,7 +41,11 @@ body { ...@@ -41,7 +41,11 @@ body {
margin: 0; margin: 0;
place-items: center; place-items: center;
min-width: 320px; min-width: 320px;
min-height: 100vh; position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
} }
a { a {
......
...@@ -14,7 +14,7 @@ import { ...@@ -14,7 +14,7 @@ import {
SortCardModal, SortCardModal,
YesNoModal, YesNoModal,
} from "./Message"; } from "./Message";
import { LifeBar, Mat, Menu, Timer } from "./PlayMat"; import { LifeBar, Mat, Menu } from "./PlayMat";
const NeosDuel = () => { const NeosDuel = () => {
return ( return (
...@@ -23,7 +23,6 @@ const NeosDuel = () => { ...@@ -23,7 +23,6 @@ const NeosDuel = () => {
<Alert /> <Alert />
<Menu /> <Menu />
<LifeBar /> <LifeBar />
<Timer />
<Mat /> <Mat />
<CardModal /> <CardModal />
<CardListModal /> <CardListModal />
......
...@@ -3,7 +3,7 @@ import "./index.scss"; ...@@ -3,7 +3,7 @@ import "./index.scss";
import { animated, to, useSpring } from "@react-spring/web"; import { animated, to, useSpring } from "@react-spring/web";
import { Dropdown, type MenuProps } from "antd"; import { Dropdown, type MenuProps } from "antd";
import classnames from "classnames"; import classnames from "classnames";
import React, { type CSSProperties, useEffect, useState, useRef } from "react"; import React, { type CSSProperties, useEffect, useRef, useState } from "react";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import type { CardMeta } from "@/api"; import type { CardMeta } from "@/api";
......
...@@ -3,20 +3,23 @@ ...@@ -3,20 +3,23 @@
display: flex; display: flex;
top: 0; top: 0;
left: 0; left: 0;
height: 100vh; // FIXME: 100% on safari bottom: 0;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
padding: 20px 35px; padding: 20px;
margin-left: 10px;
pointer-events: none; pointer-events: none;
z-index: 100; z-index: 100;
--bg-color: #323232;
width: 200px;
} }
.life-bar { .life-bar {
width: 160px; position: relative;
width: 100%;
color: white; color: white;
background-color: #323232; background-color: var(--bg-color);
font-family: var(--theme-font); font-family: var(--theme-font);
border: 1px solid #222;
padding: 1rem; padding: 1rem;
padding-bottom: 0.6rem; padding-bottom: 0.6rem;
border-radius: 8px; border-radius: 8px;
...@@ -32,3 +35,16 @@ ...@@ -32,3 +35,16 @@
font-size: 1.8rem; font-size: 1.8rem;
} }
} }
.timer-container {
background-color: var(--bg-color);
border-radius: 4px;
padding: 0.4rem 1rem;
font-size: 0.8rem;
font-weight: bold;
font-family: var(--theme-font);
width: fit-content;
color: white;
display: flex;
gap: 8px;
align-items: center;
}
import "./index.scss"; import "./index.scss";
import { Progress, Space } from "antd";
import classNames from "classnames"; import classNames from "classnames";
import React, { useEffect } from "react"; import React, { useEffect, useState } from "react";
import AnimatedNumbers from "react-animated-numbers"; import AnimatedNumbers from "react-animated-numbers";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { useEnv } from "@/hook";
import { matStore, playerStore } from "@/stores"; import { matStore, playerStore } from "@/stores";
// 三个候选方案 // 三个候选方案
// https://snack.expo.dev/?platform=web // https://snack.expo.dev/?platform=web
// https://github.com/heyman333/react-animated-numbers // https://github.com/heyman333/react-animated-numbers
...@@ -28,28 +29,89 @@ export const LifeBar: React.FC = () => { ...@@ -28,28 +29,89 @@ export const LifeBar: React.FC = () => {
setOpLife(snap.op.life); setOpLife(snap.op.life);
}, [snap.op.life]); }, [snap.op.life]);
const snapTimeLimit = useSnapshot(matStore.timeLimits);
const [myTimeLimit, setMyTimeLimit] = useState(snapTimeLimit.me);
const [opTimeLimit, setOpTimeLimit] = useState(snapTimeLimit.op);
useEffect(() => {
setMyTimeLimit(snapTimeLimit.me);
}, [snapTimeLimit.me]);
useEffect(() => {
setOpTimeLimit(snapTimeLimit.op);
}, [snapTimeLimit.op]);
useEffect(() => {
setInterval(() => {
setMyTimeLimit((time) => time - 1);
setOpTimeLimit((time) => time - 1);
}, 1000);
}, []);
useEffect(() => {
if (useEnv().VITE_IS_AI_MODE) {
// 如果是AI模式
// FIXME: 探索一个优雅的、判断当前是不是AI模式的方法,用户手动输入AI也是AI模式
setMyTimeLimit(240);
setOpTimeLimit(240);
}
}, [currentPlayer]);
return ( return (
<div id="life-bar-container"> <div id="life-bar-container">
<LifeBarItem
active={!matStore.isMe(currentPlayer)}
name={snapPlayer.getOpPlayer().name ?? "?"}
life={opLife}
timeLimit={opTimeLimit}
isMe={false}
/>
<LifeBarItem
active={matStore.isMe(currentPlayer)}
name={snapPlayer.getMePlayer().name ?? "?"}
life={meLife}
timeLimit={myTimeLimit}
isMe={true}
/>
</div>
);
};
const LifeBarItem: React.FC<{
active: boolean;
name: string;
life: number;
timeLimit: number;
isMe: boolean;
}> = ({ active, name, life, timeLimit, isMe }) => {
const mm = Math.floor(timeLimit / 60);
const ss = timeLimit % 60;
const timeText = `${mm < 10 ? "0" + mm : mm}:${ss < 10 ? "0" + ss : ss}`;
return (
<Space
direction="vertical"
style={{
flexDirection: isMe ? "column-reverse" : "column",
}}
size={12}
>
<div <div
className={classNames("life-bar", { className={classNames("life-bar", {
"life-bar-activated": matStore.isMe(currentPlayer), "life-bar-activated": active,
})} })}
> >
<div className="name">{snapPlayer.getOpPlayer().name}</div> <div className="name">{name}</div>
<div className="life"> <div className="life">{<AnimatedNumbers animateToNumber={life} />}</div>
{<AnimatedNumbers animateToNumber={opLife} />}
</div>
</div> </div>
<div {active && (
className={classNames("life-bar", { <div className="timer-container">
"life-bar-activated": matStore.isMe(currentPlayer), <Progress
})} type="circle"
> percent={(timeLimit / 240) * 100}
<div className="name">{snapPlayer.getMePlayer().name}</div> strokeWidth={20}
<div className="life"> size={14}
<AnimatedNumbers animateToNumber={meLife} /> />
<div className="timer-text">{timeText}</div>
</div> </div>
</div> )}
</div> </Space>
); );
}; };
...@@ -28,6 +28,7 @@ import { ...@@ -28,6 +28,7 @@ import {
} from "@/api"; } from "@/api";
import { cardStore, matStore } from "@/stores"; import { cardStore, matStore } from "@/stores";
import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType; import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType;
import { Timer } from "../Timer";
const { phase } = matStore; const { phase } = matStore;
const { useToken } = theme; const { useToken } = theme;
......
#timer-container {
position: fixed;
display: flex;
top: 0;
right: 0;
height: 100vh;
padding: 20px 35px;
flex-direction: column;
pointer-events: none;
}
.timer {
width: 100px;
color: white;
background-color: #323232;
font-family: var(--theme-font);
border: 1px solid #222;
padding: 1rem;
padding-bottom: 0.6rem;
border-radius: 8px;
text-align: center;
display: flex;
flex-direction: column;
font-size: 1.2rem;
}
import "./index.scss";
import React, { useEffect, useState } from "react";
import { useSnapshot } from "valtio";
import { matStore } from "@/stores";
export const Timer: React.FC = () => {
const [time, setTime] = useState(0);
const snap = useSnapshot(matStore);
useEffect(() => {
const interval = setInterval(() => {
if (time > 0) {
setTime((time) => time - 1);
}
}, 1000);
return () => clearInterval(interval);
}, [time]);
useEffect(() => {
setTime(snap.timeLimits.me);
}, [snap.timeLimits.me]);
useEffect(() => {
setTime(snap.timeLimits.op);
}, [snap.timeLimits.op]);
return (
<div id="timer-container">
<div className="timer">{time}</div>
</div>
);
};
export * from "./LifeBar"; export * from "./LifeBar";
export * from "./Mat"; export * from "./Mat";
export * from "./Menu"; export * from "./Menu";
export * from "./Timer";
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment