Commit afc1687d authored by timel's avatar timel

Merge branch 'dev/module-css' into 'main'

Dev/module css

See merge request mycard/Neos!249
parents a754c2c2 59b3f4d9
......@@ -24,3 +24,7 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# scss type
*.module.scss.d.ts
......@@ -57,6 +57,7 @@
"sass": "^1.61.0",
"typescript": "^4.9.5",
"vite": "^4.2.1",
"vite-plugin-sass-dts": "^1.3.8",
"vite-plugin-wasm-pack": "^0.1.12",
"vite-tsconfig-paths": "^4.0.8",
"vite-ydk-loader": "^0.0.2"
......@@ -6329,6 +6330,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
......@@ -17939,6 +17949,25 @@
"url": "https://opencollective.com/postcss/"
}
},
"node_modules/postcss-js": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"dependencies": {
"camelcase-css": "^2.0.1"
},
"engines": {
"node": "^12 || ^14 || >= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
"peerDependencies": {
"postcss": "^8.4.21"
}
},
"node_modules/postcss-lab-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz",
......@@ -26652,6 +26681,24 @@
}
}
},
"node_modules/vite-plugin-sass-dts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/vite-plugin-sass-dts/-/vite-plugin-sass-dts-1.3.8.tgz",
"integrity": "sha512-d6nHXyvYlFaZfO651SRR3PpYqOHFgetQ1WFPq4DZtPVABtSpEBUu3Qt3KE4+i1BLdPnhOvmXPDVvMFqfGaQYNA==",
"dev": true,
"dependencies": {
"postcss-js": "^4.0.1"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"postcss": "^8",
"prettier": "^2.7 || ^3",
"sass": "*",
"vite": "^3 || ^4"
}
},
"node_modules/vite-plugin-svgr": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-2.4.0.tgz",
......@@ -33221,6 +33268,12 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="
},
"camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true
},
"caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
......@@ -42258,6 +42311,15 @@
}
}
},
"postcss-js": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"requires": {
"camelcase-css": "^2.0.1"
}
},
"postcss-lab-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz",
......@@ -48969,6 +49031,15 @@
"rollup": "^3.18.0"
}
},
"vite-plugin-sass-dts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/vite-plugin-sass-dts/-/vite-plugin-sass-dts-1.3.8.tgz",
"integrity": "sha512-d6nHXyvYlFaZfO651SRR3PpYqOHFgetQ1WFPq4DZtPVABtSpEBUu3Qt3KE4+i1BLdPnhOvmXPDVvMFqfGaQYNA==",
"dev": true,
"requires": {
"postcss-js": "^4.0.1"
}
},
"vite-plugin-svgr": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-2.4.0.tgz",
.card-modal-desc {
.desc {
line-height: 1.6;
font-size: 14px;
font-family: var(--theme-font);
......
import "./Desc.scss";
import styles from "./Desc.module.scss";
import { Fragment } from "react";
export const Desc: React.FC<{ desc?: string }> = ({ desc = "" }) => {
if (!desc) return <></>;
return (
<div className="card-modal-desc">
{/* https://125naroom.com/web/2877 */}
{/* 牛逼的丸文字css教程 */}
<div className={styles.desc}>
<RegexWrapper
text={addSpaces(desc)}
re={/(①|②|③|④|⑤|⑥|⑦|⑧|⑨|⑩):.+?(?=((①|②|③|④|⑤|⑥|⑦|⑧|⑨|⑩):|$))/gs}
......@@ -43,7 +41,7 @@ const RegexWrapper: React.FC<{
const MaroListItem: React.FC<{ children: string }> = ({ children }) => {
return (
<div className="maro-item">
<div className={styles["maro-item"]}>
<span>{children[0]}</span>
<span>
<RegexWrapper
......@@ -58,7 +56,7 @@ const MaroListItem: React.FC<{ children: string }> = ({ children }) => {
const CircleListItem: React.FC<{ children: string }> = ({ children }) => {
return children ? (
<div className="maro-item">
<div className={styles["maro-item"]}>
<span>{children[0]}</span>
<span>{children.slice(1)}</span>
</div>
......
.card-modal-root {
.ant-drawer-content-wrapper {
.root {
:global(.ant-drawer-content-wrapper) {
box-shadow: none;
}
.card-modal-drawer {
width: 90%;
left: 10%;
--height: 640px;
top: calc((100% - var(--height)) / 2);
height: var(--height);
position: relative;
border-radius: 6px;
background: #242424;
.ant-drawer-header {
padding: 15px 0;
.ant-drawer-header-title {
flex-direction: row-reverse;
padding-left: 24px;
}
}
.drawer {
width: 90% !important;
left: 10%;
--height: 640px;
top: calc((100% - var(--height)) / 2);
height: var(--height) !important;
position: relative;
border-radius: 6px;
background: #242424;
:global(.ant-drawer-header) {
padding: 15px 0;
:global(.ant-drawer-header-title) {
flex-direction: row-reverse;
padding-left: 24px;
}
}
.card-modal-container {
position: relative;
height: 100%;
}
}
.card-modal-name {
font-weight: bold;
font-size: 1.2rem;
.container {
position: relative;
height: 100%;
}
.atkLine {
......@@ -46,7 +43,7 @@
row-gap: 10px;
}
.card-modal-info {
.info {
justify-content: space-between;
position: relative;
height: 204px; // TODO - fix this
......
import "./index.scss";
import styles from "./index.module.scss";
import { LeftOutlined } from "@ant-design/icons";
import { Divider, Drawer, Space, Tag } from "antd";
......@@ -57,14 +57,14 @@ export const CardModal = () => {
open={isOpen}
placement="left"
onClose={() => (store.isOpen = false)}
rootClassName="card-modal-root"
className="card-modal-drawer"
rootClassName={styles.root}
className={styles.drawer}
mask={false}
title={name}
closeIcon={<LeftOutlined />}
width={350}
>
<div className="card-modal-container">
<div className={styles.container}>
<Space
align="start"
size={18}
......@@ -75,7 +75,7 @@ export const CardModal = () => {
width={CARD_WIDTH}
style={{ borderRadius: 4 }}
/>
<Space direction="vertical" className="card-modal-info">
<Space direction="vertical" className={styles.info}>
<AtkLine atk={atk} def={def} />
<AttLine
types={extraCardTypes(types || 0)}
......@@ -89,26 +89,6 @@ export const CardModal = () => {
</Space>
<Divider style={{ margin: "14px 0" }}></Divider>
<Desc desc={desc} />
{/* {nonEffectInteractivies.map((interactive, idx) => {
return (
<button
key={idx}
className="card-modal-btn"
onClick={() => {
sendSelectIdleCmdResponse(interactive.response);
cardModal.isOpen = false;
// 清空互动性
for (const card of cardStore.inner) {
card.idleInteractivities = [];
}
}}
>
{interactive.desc}
</button>
);
})}
<EffectButton meta={meta} effectInteractivies={effectInteractivies} /> */}
</div>
</Drawer>
);
......@@ -129,7 +109,7 @@ const AttLine = (props: {
.map((t) => fetchStrings("!system", Type2StringCodeMap.get(t) || 0))
.join("/");
return (
<div className="attline">
<div className={styles.attline}>
{attribute && <Tag>{attribute}</Tag>}
{race && <Tag>{race}</Tag>}
{types && <Tag>{types}</Tag>}
......@@ -138,14 +118,14 @@ const AttLine = (props: {
};
const AtkLine = (props: { atk?: number; def?: number }) => (
<Space size={10} className="atkLine" direction="vertical">
<Space size={10} className={styles.atkLine} direction="vertical">
<div>
<div className="title">ATK</div>
<div className="number">{props.atk ?? "?"}</div>
<div className={styles.title}>ATK</div>
<div className={styles.number}>{props.atk ?? "?"}</div>
</div>
<div>
<div className="title">DEF</div>
<div className="number">{props.def ?? "?"}</div>
<div className={styles.title}>DEF</div>
<div className={styles.number}>{props.def ?? "?"}</div>
</div>
</Space>
);
......@@ -164,7 +144,7 @@ const _CounterLine = (props: { counters: { [type: number]: number } }) => {
return (
<>
{counters.map((counter) => (
<div className="card-modal-counter">{counter}</div>
<div>{counter}</div>
))}
</>
);
......
import "./index.scss";
import styles from "./index.module.scss";
import React, { CSSProperties } from "react";
import { useNavigate } from "react-router-dom";
......@@ -63,14 +63,14 @@ export const EndModal: React.FC = () => {
}}
onCancel={onReturn}
>
<div className="end-container">
<div className={styles["end-container"]}>
<p
className="result"
className={styles.result}
style={{ "--text-color": isWin ? "blue" : "red" } as CSSProperties}
>
{isWin ? "Win" : "Defeated"}
</p>
<p className="reason">{reason}</p>
<p className={styles.reason}>{reason}</p>
{isReplay ? <></> : <p>{fetchStrings("!system", 1340)}</p>}
</div>
</NeosModal>
......
.neos-message .ant-message-notice-content {
.message :global(.ant-message-notice-content) {
background-color: #333;
}
import "./index.scss";
import styles from "./index.module.scss";
import { message, notification } from "antd";
import React, { useEffect } from "react";
......@@ -103,7 +103,7 @@ export const showWaiting = (open: boolean) => {
type: "loading",
content: fetchStrings("!system", 1390),
key: waitingKey,
className: "neos-message",
className: styles["message"],
duration: 0,
});
clearTimeout(destoryTimer);
......
.neos-modal {
.modal {
position: fixed;
left: 0;
right: 0;
......@@ -10,7 +10,7 @@
transition: 0.3s;
}
.neos-modal-mini {
.mini {
top: 100% !important;
bottom: 0 !important;
transform: translateY(calc(50% - 66px));
......@@ -19,7 +19,7 @@
}
}
.neos-modal-wrap {
.wrap {
pointer-events: none;
}
......
import "./index.scss";
import styles from "./index.module.scss";
import { MinusOutlined, UpOutlined } from "@ant-design/icons";
import { Modal, type ModalProps } from "antd";
......@@ -20,14 +20,14 @@ export const NeosModal: React.FC<Props> = (props) => {
return (
<Modal
className={classNames("neos-modal", { "neos-modal-mini": mini })}
className={classNames(styles.modal, { [styles["mini"]]: mini })}
centered
maskClosable={true}
onCancel={() => setMini(!mini)}
closeIcon={mini ? <UpOutlined /> : <MinusOutlined />}
bodyStyle={{ padding: "10px 0" }}
mask={!mini}
wrapClassName={classNames({ "neos-modal-wrap": mini })}
wrapClassName={classNames({ [styles.wrap]: mini })}
closable={canBeMinimized}
{...props}
open={realOpen}
......
import "./index.scss";
import { INTERNAL_Snapshot as Snapshot, proxy, useSnapshot } from "valtio";
import { sendSelectMultiResponse, sendSelectSingleResponse } from "@/api";
......
.checkcard-container {
.container {
position: relative;
// padding-left: 10px;
// &::after {
// position: absolute;
// width: 3px;
// height: 100%;
// content: "";
// z-index: 1;
// left: 0;
// top: 0;
// background-color: rgb(0, 54, 189);
// }
.btns {
width: 100%;
top: 50%;
......@@ -23,12 +12,12 @@
transform: translateY(-50%);
}
}
.ant-pro-checkcard {
:global(.ant-pro-checkcard) {
border-radius: 4px;
overflow: hidden;
}
// 多选卡片的样式
.ant-pro-checkcard-checked {
:global(.ant-pro-checkcard-checked) {
&::before {
position: absolute;
width: 100%;
......@@ -43,3 +32,25 @@
}
}
}
.check-group {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
}
.check-card {
width: 100px;
aspect-ratio: var(--card-ratio);
margin-inline-end: 0;
margin-block-end: 0;
flex-shrink: 0;
}
.card {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
import "./index.scss";
import { CheckCard } from "@ant-design/pro-components";
import { Button, Card, Segmented, Space, Tooltip } from "antd";
import { CSSProperties, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import type { CardMeta, ygopro } from "@/api";
......@@ -13,26 +11,7 @@ import { YgoCard } from "@/ui/Shared";
import { groupBy } from "../../utils";
import { showCardModal } from "../CardModal";
import { NeosModal } from "../NeosModal";
const YgoCardStyle = {
width: "100%",
height: "100%",
position: "absolute",
left: 0,
top: 0,
};
const CheckCardStyle = {
width: 100,
aspectRatio: 5.9 / 8.6,
marginInlineEnd: 0,
marginBlockEnd: 0,
flexShrink: 0,
};
const CheckGroupStyle = {
display: "grid",
gridTemplateColumns: "repeat(5, 1fr)",
gap: 10,
};
import styles from "./index.module.scss";
export interface SelectCardsModalProps {
isOpen: boolean;
......@@ -148,88 +127,76 @@ export const SelectCardsModal: React.FC<SelectCardsModalProps> = ({
</>
}
>
<div className="check-container">
<Space
direction="vertical"
style={{ width: "100%", overflow: "hidden" }}
>
<Selector
zoneOptions={zoneOptions}
selectedZone={selectedZone}
onChange={setSelectedZone as any}
/>
{grouped.map(
(options, i) =>
options[0] === selectedZone && (
<div className="checkcard-container" key={i}>
<CheckCard.Group
onChange={(res) => {
setResult((isMultiple ? res : [res]) as any);
}}
// TODO 考虑如何设置默认值,比如只有一个的,就直接选中
multiple={isMultiple}
style={CheckGroupStyle}
>
{options[1].map((card, j) => (
<Tooltip
title={card.effectDesc}
placement="bottom"
key={j}
>
{/* 这儿必须有一个div,不然tooltip不生效 */}
<div>
<CheckCard
cover={
<YgoCard
code={card.meta.id}
style={YgoCardStyle as CSSProperties}
/>
}
style={CheckCardStyle}
value={card}
onClick={() => {
showCardModal(card);
}}
/>
</div>
</Tooltip>
))}
</CheckCard.Group>
</div>
)
)}
<p>
<span>
{/* TODO: 这里的字体可以调整下 */}
{selecteds.length > 0 ? fetchStrings("!system", 212) : ""}
</span>
</p>
<div style={CheckGroupStyle}>
{selecteds.map((card, i) => (
<Tooltip
title={card.effectDesc}
placement="bottom"
key={grouped.length + i}
>
<div>
<Card
cover={
<YgoCard
code={card.meta.id}
style={YgoCardStyle as CSSProperties}
/>
}
style={CheckCardStyle}
onClick={() => {
showCardModal(card);
}}
/>
</div>
</Tooltip>
))}
</div>
</Space>
</div>
<Space direction="vertical" style={{ width: "100%", overflow: "hidden" }}>
<Selector
zoneOptions={zoneOptions}
selectedZone={selectedZone}
onChange={setSelectedZone as any}
/>
{grouped.map(
(options, i) =>
options[0] === selectedZone && (
<div className={styles["container"]} key={i}>
<CheckCard.Group
onChange={(res) => {
setResult((isMultiple ? res : [res]) as any);
}}
// TODO 考虑如何设置默认值,比如只有一个的,就直接选中
multiple={isMultiple}
className={styles["check-group"]}
>
{options[1].map((card, j) => (
<Tooltip title={card.effectDesc} placement="bottom" key={j}>
{/* 这儿必须有一个div,不然tooltip不生效 */}
<div>
<CheckCard
cover={
<YgoCard
code={card.meta.id}
className={styles.card}
/>
}
className={styles["check-card"]}
value={card}
onClick={() => {
showCardModal(card);
}}
/>
</div>
</Tooltip>
))}
</CheckCard.Group>
</div>
)
)}
<p>
<span>
{/* TODO: 这里的字体可以调整下 */}
{selecteds.length > 0 ? fetchStrings("!system", 212) : ""}
</span>
</p>
<div className={styles["check-group"]}>
{selecteds.map((card, i) => (
<Tooltip
title={card.effectDesc}
placement="bottom"
key={grouped.length + i}
>
<div>
<Card
cover={
<YgoCard code={card.meta.id} className={styles.card} />
}
className={styles["check-card"]}
onClick={() => {
showCardModal(card);
}}
/>
</div>
</Tooltip>
))}
</div>
</Space>
</NeosModal>
);
};
......
.mat-bg {
display: flex;
flex-direction: column;
row-gap: var(--row-gap);
justify-content: center;
align-items: center;
background-color: transparent;
.row {
display: flex;
column-gap: var(--col-gap);
&.opponent {
flex-direction: row-reverse;
}
}
}
.block {
height: var(--block-height-m);
width: var(--block-width);
background: radial-gradient(#1d1d1d, #222);
position: relative;
&.extra {
margin-inline: calc(var(--block-width) / 2 + var(--col-gap) / 2);
}
&.szone {
height: var(--block-height-s);
}
&.highlight {
background: #102639;
cursor: pointer;
.triangle {
--color: #006eff;
transform: scale(1.5);
}
&:hover {
opacity: 0.7;
.triangle {
transform: scale(1.2);
}
}
}
.triangle {
width: 0;
height: 0;
--color: #333;
border-width: 4px;
border-style: solid;
position: absolute;
transition: 0.3s;
&:nth-of-type(1) {
border-color: var(--color) transparent transparent var(--color);
}
&:nth-of-type(2) {
border-color: var(--color) var(--color) transparent transparent;
right: 0;
}
&:nth-of-type(3) {
border-color: transparent var(--color) var(--color) transparent;
right: 0;
bottom: 0;
}
&:nth-of-type(4) {
border-color: transparent transparent var(--color) var(--color);
bottom: 0;
}
}
}
// 下面应该和moveToOutside、moveToGround对应
.other-blocks {
width: 0;
height: 0;
&.op {
transform: rotate(180deg);
}
position: absolute;
--height: var(--card-height-o);
--width: calc(var(--height) * var(--card-ratio));
--left: calc(
var(--col-gap) * 2 + var(--block-width) * 2.5 +
var(--block-outside-offset-x) + var(--width) / 2
);
--top: calc(
var(--row-gap) + var(--block-height-m) +
(var(--block-height-m) - var(--height)) / 2
);
.banish,
.field,
.graveyard,
.deck,
.extra-deck {
position: absolute;
transform: translate(-50%, -50%);
height: var(--height);
width: var(--width);
top: var(--top);
left: var(--left);
}
.field {
left: calc(-1 * var(--left));
}
.banish {
top: calc(var(--top) - var(--row-gap) - var(--height));
}
.deck {
--left: calc(
var(--deck-offset-x) + 2 * (var(--block-width) + var(--col-gap))
);
left: var(--left);
top: calc(
var(--deck-offset-y) + 2 * var(--block-height-m) + 2 * var(--row-gap)
);
transform: translate(-50%, -50%) rotate(calc(-1 * var(--deck-rotate-z)));
height: var(--deck-card-height);
width: calc(var(--deck-card-height) * var(--card-ratio));
&.extra-deck {
left: calc(-1 * var(--left));
transform: translate(-50%, -50%) rotate(var(--deck-rotate-z));
}
}
}
// 被禁用的样式
.disabled-cross {
width: 100%;
height: 100%;
cursor: not-allowed;
background: linear-gradient(
to top right,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) calc(50% - 1.5px),
red 50%,
rgba(0, 0, 0, 0) calc(50% + 1.5px),
rgba(0, 0, 0, 0) 100%
),
linear-gradient(
to bottom right,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) calc(50% - 1.5px),
red 50%,
rgba(0, 0, 0, 0) calc(50% + 1.5px),
rgba(0, 0, 0, 0) 100%
);
display: none;
}
.disabled-cross.show {
display: block;
}
.block.glowing {
--shadow-color: #13a1ff;
box-shadow: 0 0 3px 3px var(--shadow-color), 0 0 25px 2px #0099ff87;
background: var(--shadow-color);
border-radius: 2px;
.triangle {
display: none;
}
}
section#mat {
.mat-bg {
display: flex;
flex-direction: column;
row-gap: var(--row-gap);
justify-content: center;
align-items: center;
background-color: transparent;
.bg-row {
display: flex;
column-gap: var(--col-gap);
&.opponent {
flex-direction: row-reverse;
}
}
}
.block {
height: var(--block-height-m);
width: var(--block-width);
background: radial-gradient(#1d1d1d, #222);
position: relative;
&.extra {
margin-inline: calc(var(--block-width) / 2 + var(--col-gap) / 2);
}
&.szone {
height: var(--block-height-s);
}
&.highlight {
background: #102639;
cursor: pointer;
// animation: blink 1s linear infinite alternate;
.triangle {
--color: #006eff;
transform: scale(1.5);
}
&:hover {
opacity: 0.7;
.triangle {
transform: scale(1.2);
}
}
}
.triangle {
width: 0;
height: 0;
--color: #333;
border-width: 4px;
border-style: solid;
position: absolute;
transition: 0.3s;
&:nth-of-type(1) {
border-color: var(--color) transparent transparent var(--color);
}
&:nth-of-type(2) {
border-color: var(--color) var(--color) transparent transparent;
right: 0;
}
&:nth-of-type(3) {
border-color: transparent var(--color) var(--color) transparent;
right: 0;
bottom: 0;
}
&:nth-of-type(4) {
border-color: transparent transparent var(--color) var(--color);
bottom: 0;
}
}
}
// 下面应该和moveToOutside、moveToGround对应
.bg-other-blocks {
&.op {
transform: rotate(180deg);
}
position: absolute;
--height: var(--card-height-o);
--width: calc(var(--height) * var(--card-ratio));
--left: calc(
var(--col-gap) * 2 + var(--block-width) * 2.5 +
var(--block-outside-offset-x) + var(--width) / 2
);
--top: calc(
var(--row-gap) + var(--block-height-m) +
(var(--block-height-m) - var(--height)) / 2
);
.block {
position: absolute;
transform: translate(-50%, -50%);
height: var(--height);
width: var(--width);
top: var(--top);
left: var(--left);
}
.field {
left: calc(-1 * var(--left));
}
.banish {
top: calc(var(--top) - var(--row-gap) - var(--height));
}
.deck {
--left: calc(
var(--deck-offset-x) + 2 * (var(--block-width) + var(--col-gap))
);
left: var(--left);
top: calc(
var(--deck-offset-y) + 2 * var(--block-height-m) + 2 * var(--row-gap)
);
transform: translate(-50%, -50%) rotate(calc(-1 * var(--deck-rotate-z)));
height: var(--deck-card-height);
width: calc(var(--deck-card-height) * var(--card-ratio));
&.extra-deck {
left: calc(-1 * var(--left));
transform: translate(-50%, -50%) rotate(var(--deck-rotate-z));
}
}
}
}
// 被禁用的样式
.disabled-cross {
width: 100%;
height: 100%;
cursor: not-allowed;
background: linear-gradient(
to top right,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) calc(50% - 1.5px),
red 50%,
rgba(0, 0, 0, 0) calc(50% + 1.5px),
rgba(0, 0, 0, 0) 100%
),
linear-gradient(
to bottom right,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) calc(50% - 1.5px),
red 50%,
rgba(0, 0, 0, 0) calc(50% + 1.5px),
rgba(0, 0, 0, 0) 100%
);
display: none;
}
.disabled-cross.show {
display: block;
}
section#mat {
.block.glowing {
--card-shadow-color: #13a1ff;
box-shadow: 0 0 3px 3px var(--card-shadow-color), 0 0 25px 2px #0099ff87;
background: var(--card-shadow-color);
border-radius: 2px;
.triangle {
display: none;
}
}
}
import "./index.scss";
import styles from "./index.module.scss";
import classnames from "classnames";
import { type INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
......@@ -27,9 +27,9 @@ const BgBlock: React.FC<
}) => (
<div
{...rest}
className={classnames("block", className, {
highlight,
glowing,
className={classnames(styles.block, className, {
[styles.highlight]: highlight,
[styles.glowing]: glowing,
})}
>
{<DecoTriangles />}
......@@ -42,11 +42,11 @@ const BgExtraRow: React.FC<{
opSnap: Snapshot<BlockState[]>;
}> = ({ meSnap, opSnap }) => {
return (
<div className={classnames("bg-row")}>
<div className={classnames(styles.row)}>
{Array.from({ length: 2 }).map((_, i) => (
<BgBlock
key={i}
className="extra"
className={styles.extra}
onClick={() => {
onBlockClick(meSnap[i].interactivity);
onBlockClick(opSnap[i].interactivity);
......@@ -64,11 +64,11 @@ const BgRow: React.FC<{
opponent?: boolean;
snap: Snapshot<BlockState[]>;
}> = ({ szone = false, opponent = false, snap }) => (
<div className={classnames("bg-row", { opponent })}>
<div className={classnames(styles.row, { [styles.opponent]: opponent })}>
{Array.from({ length: 5 }).map((_, i) => (
<BgBlock
key={i}
className={classnames({ szone })}
className={classnames({ [styles.szone]: szone })}
onClick={() => onBlockClick(snap[i].interactivity)}
disabled={snap[i].disabled}
highlight={!!snap[i].interactivity}
......@@ -77,7 +77,7 @@ const BgRow: React.FC<{
</div>
);
const BgOtherBlocks: React.FC<{ me?: boolean }> = ({ me }) => {
const BgOtherBlocks: React.FC<{ op?: boolean }> = ({ op }) => {
useSnapshot(cardStore);
const meController = isMe(0) ? 0 : 1;
const judgeGlowing = (zone: ygopro.CardZone) =>
......@@ -88,12 +88,15 @@ const BgOtherBlocks: React.FC<{ me?: boolean }> = ({ me }) => {
const glowingGraveyard = judgeGlowing(ygopro.CardZone.GRAVE);
const glowingBanish = judgeGlowing(ygopro.CardZone.REMOVED);
return (
<div className={classnames("bg-other-blocks", { me, op: !me })}>
<BgBlock className="banish" glowing={me && glowingBanish} />
<BgBlock className="graveyard" glowing={me && glowingGraveyard} />
<BgBlock className="field" />
<BgBlock className="deck" />
<BgBlock className="deck extra-deck" glowing={me && glowingExtra} />
<div className={classnames(styles["other-blocks"], { [styles.op]: op })}>
<BgBlock className={styles.banish} glowing={!op && glowingBanish} />
<BgBlock className={styles.graveyard} glowing={!op && glowingGraveyard} />
<BgBlock className={styles.field} />
<BgBlock className={styles.deck} />
<BgBlock
className={classnames(styles.deck, styles["extra-deck"])}
glowing={!op && glowingExtra}
/>
</div>
);
};
......@@ -101,7 +104,7 @@ const BgOtherBlocks: React.FC<{ me?: boolean }> = ({ me }) => {
export const Bg: React.FC = () => {
const snap = useSnapshot(placeStore.inner);
return (
<div className="mat-bg">
<div className={styles["mat-bg"]}>
<BgRow snap={snap[ygopro.CardZone.SZONE].op} szone opponent />
<BgRow snap={snap[ygopro.CardZone.MZONE].op} opponent />
<BgExtraRow
......@@ -110,8 +113,8 @@ export const Bg: React.FC = () => {
/>
<BgRow snap={snap[ygopro.CardZone.MZONE].me} />
<BgRow snap={snap[ygopro.CardZone.SZONE].me} szone />
<BgOtherBlocks me />
<BgOtherBlocks />
<BgOtherBlocks op />
</div>
);
};
......@@ -127,11 +130,15 @@ const onBlockClick = (placeInteractivity: PlaceInteractivity) => {
const DecoTriangles: React.FC = () => (
<>
{Array.from({ length: 4 }).map((_, i) => (
<div className="triangle" key={i} />
<div className={styles.triangle} key={i} />
))}
</>
);
const DisabledCross: React.FC<{ disabled: boolean }> = ({ disabled }) => (
<div className={classnames("disabled-cross", { show: disabled })}></div>
<div
className={classnames(styles["disabled-cross"], {
[styles.show]: disabled,
})}
></div>
);
.mat-card {
position: absolute;
--card-height: 100px;
width: auto !important;
height: var(--card-height);
aspect-ratio: var(--card-ratio);
transform-style: preserve-3d;
.img-wrap {
transform-style: preserve-3d;
position: relative;
margin: auto auto;
top: 2%;
height: 96%;
width: 96%;
transform: translateZ(calc((var(--z) + var(--sub-z)) * 1px + 0.1px))
rotateY(calc(var(--ry) * 1deg));
transition: 0.2s scale;
cursor: pointer;
.cover,
.back {
width: 100% !important;
height: 100%;
position: absolute;
left: 0;
top: 0;
border-radius: 2px;
overflow: hidden;
}
.cover {
z-index: 1;
transform: translateZ(0.5px);
}
&.focusing .cover {
animation: focus 0.6s ease-in-out;
}
.back {
z-index: 0;
transform: translateZ(0px);
}
}
.shadow {
display: none;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: transparent;
// filter: blur(2px);
}
// 卡片被选中后的流光特效
// ref: https://github.com/Mr-majifu/Animated-Profile-Card02/blob/master/style.css
.streamer {
position: absolute;
inset: 0;
background: #000;
overflow: hidden;
}
.streamer::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 400%;
height: 80%;
background: linear-gradient(
transparent,
#45f3ff,
#45f3ff,
#45f3ff,
transparent
);
animation: stream 2s linear infinite;
}
.streamer::after {
content: "";
position: absolute;
/* https://developer.mozilla.org/en-US/docs/Web/CSS/inset */
inset: 3px;
background: #292929;
}
@keyframes stream {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.focus {
position: absolute;
width: calc(100% * var(--focus-scale));
height: calc(100% * var(--focus-scale));
left: calc((var(--focus-scale) - 1) * -50%);
top: calc((var(--focus-scale) - 1) * -50%);
outline: 3px solid #ddd;
filter: blur(calc(2px * 1.3 * var(--focus-scale)));
display: var(--focus-display);
opacity: var(--focus-opacity);
}
}
.mat-card.glowing .shadow {
--shadow-color: #0099ff;
display: block !important;
background: var(--shadow-color) !important;
border-radius: 5px;
box-shadow: 0 0 4px 0 var(--shadow-color), 0 0 25px 2px #0099ff87;
transform: translateZ(calc((var(--z)) * 1px + 0.1px));
}
@keyframes focus {
0% {
filter: brightness(1) contrast(1);
}
50% {
filter: brightness(1.5) contrast(1.1);
}
100% {
filter: brightness(1) contrast(1);
}
}
.dropdown {
:global(.ant-dropdown-menu) {
background-color: #333;
}
text-align: center;
}
.dropdown-disabled {
display: none;
}
section#mat {
.mat-card {
position: absolute;
--card-height: 100px;
width: auto !important;
height: var(--card-height);
aspect-ratio: var(--card-ratio);
transform-style: preserve-3d;
.card-img-wrap {
transform-style: preserve-3d;
position: relative;
margin: auto auto;
top: 2%;
height: 96%;
width: 96%;
transform: translateZ(calc((var(--z) + var(--sub-z)) * 1px + 0.1px))
rotateY(calc(var(--ry) * 1deg));
transition: 0.2s scale;
cursor: pointer;
&:hover {
}
.card-cover,
.card-back {
width: 100% !important;
height: 100%;
position: absolute;
left: 0;
top: 0;
border-radius: 2px;
overflow: hidden;
}
.card-cover {
z-index: 1;
transform: translateZ(0.5px);
}
&.focus .card-cover {
animation: focus 0.6s ease-in-out;
}
.card-back {
z-index: 0;
transform: translateZ(0px);
}
}
.card-shadow {
display: none;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: transparent;
// filter: blur(2px);
}
// 卡片被选中后的流光特效
// ref: https://github.com/Mr-majifu/Animated-Profile-Card02/blob/master/style.css
.card-streamer {
position: absolute;
inset: 0;
background: #000;
overflow: hidden;
}
.card-streamer::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 400%;
height: 80%;
background: linear-gradient(
transparent,
#45f3ff,
#45f3ff,
#45f3ff,
transparent
);
animation: stream 2s linear infinite;
}
.card-streamer::after {
content: "";
position: absolute;
/* https://developer.mozilla.org/en-US/docs/Web/CSS/inset */
inset: 3px;
background: #292929;
}
@keyframes stream {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.card-focus {
position: absolute;
width: calc(100% * var(--focus-scale));
height: calc(100% * var(--focus-scale));
left: calc((var(--focus-scale) - 1) * -50%);
top: calc((var(--focus-scale) - 1) * -50%);
outline: 3px solid #ddd;
filter: blur(calc(2px * 1.3 * var(--focus-scale)));
display: var(--focus-display);
opacity: var(--focus-opacity);
}
}
}
.mat-card.glowing .card-shadow {
--card-shadow-color: #0099ff;
display: block !important;
background: var(--card-shadow-color) !important;
border-radius: 5px;
box-shadow: 0 0 4px 0 var(--card-shadow-color), 0 0 25px 2px #0099ff87;
transform: translateZ(calc((var(--z)) * 1px + 0.1px));
}
@keyframes focus {
0% {
filter: brightness(1) contrast(1);
}
50% {
filter: brightness(1.5) contrast(1.1);
}
100% {
filter: brightness(1) contrast(1);
}
}
.card-dropdown {
.ant-dropdown-menu {
background-color: #333;
}
text-align: center;
}
.card-dropdown-disabled {
display: none;
}
import "./index.scss";
import styles from "./index.module.scss";
import { animated, to, useSpring } from "@react-spring/web";
import { Dropdown, type MenuProps } from "antd";
......@@ -39,7 +39,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
const card = cardStore.inner[idx];
const snap = useSnapshot(card);
const [styles, api] = useSpring<SpringApiProps>(
const [spring, api] = useSpring<SpringApiProps>(
() =>
({
x: 0,
......@@ -273,47 +273,51 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
return (
<animated.div
className={classnames("mat-card", { glowing })}
className={classnames(styles["mat-card"], { [styles.glowing]: glowing })}
style={
{
transform: to(
[styles.x, styles.y, styles.z, styles.rx, styles.ry, styles.rz],
[spring.x, spring.y, spring.z, spring.rx, spring.ry, spring.rz],
(x, y, z, rx, ry, rz) =>
`translate(${x}px, ${y}px) rotateX(${rx}deg) rotateZ(${rz}deg)`
),
"--z": styles.z,
"--sub-z": styles.subZ.to([0, 50, 100], [0, 200, 0]), // 中间高,两边低
"--ry": styles.ry,
height: styles.height,
zIndex: styles.zIndex,
"--focus-scale": styles.focusScale,
"--focus-display": styles.focusDisplay,
"--focus-opacity": styles.focusOpacity,
opacity: styles.opacity,
"--z": spring.z,
"--sub-z": spring.subZ.to([0, 50, 100], [0, 200, 0]), // 中间高,两边低
"--ry": spring.ry,
height: spring.height,
zIndex: spring.zIndex,
"--focus-scale": spring.focusScale,
"--focus-display": spring.focusDisplay,
"--focus-opacity": spring.focusOpacity,
opacity: spring.opacity,
} as any as CSSProperties
}
onClick={onClick}
>
<div className="card-focus" />
<div className="card-shadow" />
<div className={styles.focus} />
<div className={styles.shadow} />
<Dropdown
menu={dropdownMenu}
placement="top"
overlayClassName={classnames("card-dropdown", {
"card-dropdown-disabled": dropdownMenuDisabled,
overlayClassName={classnames(styles.dropdown, {
[styles["dropdown-disabled"]]: dropdownMenuDisabled,
})}
arrow
trigger={["click"]}
>
<div className={classnames("card-img-wrap", { focus: classFocus })}>
<div
className={classnames(styles["img-wrap"], {
[styles.focusing]: classFocus,
})}
>
<YgoCard
className={classnames("card-cover")}
className={styles.cover}
code={snap.code === 0 ? snap.meta.id : snap.code}
/>
<YgoCard className="card-back" isBack />
<YgoCard className={styles.back} isBack />
</div>
</Dropdown>
{snap.selected ? <div className="card-streamer" /> : <></>}
{snap.selected ? <div className={styles.streamer} /> : <></>}
</animated.div>
);
});
......
#life-bar-container {
.container {
position: fixed;
display: flex;
top: 0;
......
import "./index.scss";
import styles from "./index.module.scss";
import { Progress } from "antd";
import classNames from "classnames";
......@@ -56,7 +56,7 @@ export const LifeBar: React.FC = () => {
}, [currentPlayer]);
return (
<div id="life-bar-container">
<div className={styles.container}>
<LifeBarItem
active={!matStore.isMe(currentPlayer)}
name={snapPlayer.getOpPlayer().name ?? "?"}
......@@ -99,22 +99,24 @@ const LifeBarItem: React.FC<{
}}
>
<div
className={classNames("life-bar", {
className={classNames(styles["life-bar"], {
"life-bar-activated": active,
})}
>
<div className="name">{name}</div>
<div className="life">{<AnimatedNumbers animateToNumber={life} />}</div>
<div className={styles.name}>{name}</div>
<div className={styles.life}>
{<AnimatedNumbers animateToNumber={life} />}
</div>
</div>
{active && (
<div className="timer-container">
<div className={styles["timer-container"]}>
<Progress
type="circle"
percent={Math.floor((timeLimit / 240) * 100)}
strokeWidth={20}
size={14}
/>
<div className="timer-text">{timeText}</div>
<div className={styles["timer-text"]}>{timeText}</div>
</div>
)}
</div>
......
section#mat {
// margin-top: 200px;
// padding-top: 50px; // 先不管 后面调整
section.mat {
position: relative;
#camera {
width: "100%";
.camera {
height: 100%;
display: flex;
flex-direction: column;
......@@ -10,7 +9,7 @@ section#mat {
align-items: center;
// perspective: var(--perspective);
}
#plane {
.plane {
transform: translateX(0) translateY(0) translateZ(0)
rotateX(var(--plane-rotate-x));
width: fit-content;
......@@ -19,7 +18,7 @@ section#mat {
}
}
.mat-card-container {
.container {
position: absolute;
top: 50%;
left: 50%;
......
import "./index.scss";
import styles from "./index.module.scss";
import { useSnapshot } from "valtio";
......@@ -9,32 +9,27 @@ import { Card } from "../Card";
// 后面再改名
export const Mat: React.FC = () => {
const snap = useSnapshot(cardStore.inner);
return (
<section
id="mat"
style={{
width: "100%",
}}
>
<Plane>
<Bg />
<CardContainer>
{snap.map((_cardSnap, i) => (
<Card key={i} idx={i} />
))}
</CardContainer>
</Plane>
<section className={styles.mat}>
<div className={styles.camera}>
<div className={styles.plane}>
<Bg />
<div className={styles.container}>
<Cards />
</div>
</div>
</div>
</section>
);
};
const Plane: React.FC<React.PropsWithChildren> = ({ children }) => (
<div id="camera">
<div id="plane">{children}</div>
</div>
);
const CardContainer: React.FC<React.PropsWithChildren> = ({ children }) => (
<div className="mat-card-container">{children}</div>
);
const Cards: React.FC = () => {
const snap = useSnapshot(cardStore.inner);
return (
<>
{snap.map((_cardSnap, i) => (
<Card key={i} idx={i} />
))}
</>
);
};
import "./index.scss";
import styles from "./index.module.scss";
import {
ArrowRightOutlined,
......@@ -53,7 +53,12 @@ export const Menu = () => {
: 7;
// PhaseType, 中文, response, 是否显示
const phaseBind: [PhaseType, string, number, boolean][] = [
const phaseBind: [
phase: PhaseType,
label: string,
response: number,
show: boolean
][] = [
[PhaseType.DRAW, "抽卡阶段", -1, true],
[PhaseType.STANDBY, "准备阶段", -1, true],
[PhaseType.MAIN1, "主要阶段 1", -1, true],
......@@ -69,9 +74,9 @@ export const Menu = () => {
const phaseSwitchItems: MenuProps["items"] = phaseBind
.filter(([, , , show]) => show)
.map(([phase, str, response], i) => ({
key: i,
label: str,
.map(([phase, label, response], key) => ({
key,
label,
disabled: currentPhase >= phase,
onClick: () => {
if (response === 2) sendSelectIdleCmdResponse(response);
......@@ -95,37 +100,35 @@ export const Menu = () => {
const globalDisable = !matStore.isMe(currentPlayer);
return (
<>
<div className="menu-container">
<DropdownWithTitle
title="请选择要进入的阶段"
menu={{ items: phaseSwitchItems }}
<div className={styles["menu-container"]}>
<DropdownWithTitle
title="请选择要进入的阶段"
menu={{ items: phaseSwitchItems }}
disabled={globalDisable}
>
<Button
icon={<StepForwardFilled style={{ transform: "scale(1.5)" }} />}
type="text"
disabled={globalDisable}
>
<Button
icon={<StepForwardFilled style={{ transform: "scale(1.5)" }} />}
type="text"
disabled={globalDisable}
>
{phaseBind.find(([key]) => key === currentPhase)?.[1]}
</Button>
</DropdownWithTitle>
<Tooltip title="聊天室">
<Button
icon={<MessageFilled />}
type="text"
disabled={globalDisable}
></Button>
</Tooltip>
<DropdownWithTitle
title="是否投降?"
menu={{ items: surrenderMenuItems }}
{phaseBind.find(([key]) => key === currentPhase)?.[1]}
</Button>
</DropdownWithTitle>
<Tooltip title="聊天室">
<Button
icon={<MessageFilled />}
type="text"
disabled={globalDisable}
>
<Button icon={<CloseCircleFilled />} type="text"></Button>
</DropdownWithTitle>
</div>
</>
></Button>
</Tooltip>
<DropdownWithTitle
title="是否投降?"
menu={{ items: surrenderMenuItems }}
disabled={globalDisable}
>
<Button icon={<CloseCircleFilled />} type="text"></Button>
</DropdownWithTitle>
</div>
);
};
......
import "./index.scss";
import styles from "./index.module.scss";
import classNames from "classnames";
import { CSSProperties, useMemo } from "react";
......@@ -19,14 +19,14 @@ export const YgoCard: React.FC<Props> = (props) => {
className,
code = 0,
isBack = false,
width = 80,
width,
style,
onClick = () => {},
} = props;
return useMemo(
() => (
<img
className={classNames("ygo-card", className)}
className={classNames(styles["ygo-card"], className)}
src={getCardImgUrl(code, isBack)}
style={{ width, ...style }}
onClick={onClick}
......
......@@ -4,6 +4,8 @@ import svgr from "vite-plugin-svgr";
import ydkLoader from "vite-ydk-loader";
import tsconfigPaths from "vite-tsconfig-paths";
import wasmPack from "vite-plugin-wasm-pack";
import sassDts from "vite-plugin-sass-dts";
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
......@@ -15,9 +17,13 @@ export default defineConfig({
svgr(),
ydkLoader(),
tsconfigPaths(),
wasmPack("./rust-src")
wasmPack("./rust-src"),
sassDts({
enabledMode: ["development"],
sourceDir: path.resolve(__dirname, "./src"),
}),
],
resolve: {
extensions: [".js", ".json", ".ydk"],
extensions: [".mjs", ".js", ".mts", ".ts", ".jsx", ".tsx", ".json", ".ydk"],
},
});
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