Commit c9d97b95 authored by timel's avatar timel Committed by Chunchi Che

feat: new mat

parent b028d9b2
......@@ -57,6 +57,8 @@ export async function fetchCard(id: number): Promise<CardMeta> {
return res.selectResult ? res.selectResult : { id, data: {}, text: {} };
}
window.fetchCard = fetchCard;
export function getCardStr(meta: CardMeta, idx: number): string | undefined {
switch (idx) {
case 0: {
......
......@@ -23,6 +23,7 @@ export namespace ygopro {
ONFIELD = 8,
FZONE = 9,
PZONE = 10,
TZONE = 11, // 还在想有没有什么好的解决方案
}
export enum CardPosition {
FACEUP_ATTACK = 0,
......
import { v4 as v4uuid } from "uuid";
import { ygopro } from "@/api";
import { fetchOverlayMeta, store, cardStore } from "@/stores";
import { fetchCard, ygopro } from "@/api";
import { fetchOverlayMeta, store, cardStore, CardType } from "@/stores";
type MsgMove = ygopro.StocGameMessage.MsgMove;
import { useConfig } from "@/config";
import { sleep } from "@/infra";
......@@ -11,30 +11,23 @@ import { REASON_MATERIAL } from "../../common";
const { matStore } = store;
const NeosConfig = useConfig();
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE, OVERLAY } =
ygopro.CardZone;
const OVERLAY_STACK: { uuid: string; code: number; sequence: number }[] = [];
const overlayStack: CardType[] = [];
export default async (move: MsgMove) => {
const code = move.code;
const from = move.from;
const to = move.to;
const reason = move.reason;
cardStore.move(
code,
{
zone: from.location,
controller: from.controler,
sequence: from.sequence,
},
{
zone: to.location,
controller: to.controler,
sequence: to.sequence,
}
);
// FIXME: 考虑超量素材的情况
// FIXME:需要考虑【卡名当作另一张卡】的情况
let uuid;
let chainIndex;
switch (from.location) {
......@@ -166,4 +159,103 @@ export default async (move: MsgMove) => {
break;
}
}
// card store
const fromCards = cardStore.at(from.location, from.controler);
const toCards = cardStore.at(to.location, to.controler);
const fromZone =
move.from.toArray()[1] === undefined
? ygopro.CardZone.TZONE
: from.location;
const toZone =
move.to.toArray()[1] === undefined ? ygopro.CardZone.TZONE : to.location;
// 处理token
let target: CardType;
if (fromZone === TZONE) {
// 召唤 token
target = cardStore.at(TZONE, from.controler)[0]; // 必有,随便取一个没用到的token
} else if (fromZone === OVERLAY) {
// 超量素材的去除
const xyzMoster = cardStore.at(MZONE, from.controler, from.sequence);
target = xyzMoster.overlayMaterials.splice(from.overlay_sequence, 1)[0];
target.xyzMonster = undefined;
} else {
target = cardStore.at(fromZone, from.controler, from.sequence);
}
(async () => {
const { text } = await fetchCard(code);
console.warn("---");
console.log(
"move",
text.name,
ygopro.CardZone[fromZone],
from.sequence,
"->",
ygopro.CardZone[toZone],
to.sequence
);
console.log("over", from.overlay_sequence, to.overlay_sequence);
console.log({ fromCards });
console.log({ target });
console.warn("---");
})();
if (toZone === OVERLAY) {
// 准备超量召唤,超量素材入栈
if (reason == REASON_MATERIAL) overlayStack.push(target);
// 超量素材的添加
else {
target.overlayMaterials.splice(to.overlay_sequence, 0, target);
target.xyzMonster = undefined;
}
}
if (toZone === MZONE && overlayStack.length) {
// 超量召唤
target.overlayMaterials = overlayStack.splice(0, overlayStack.length);
target.overlayMaterials.forEach((c) => (c.xyzMonster = target));
}
// 维护sequence
if ([HAND, GRAVE, REMOVED, DECK, EXTRA].includes(fromZone))
fromCards.forEach((c) => c.sequence > from.sequence && c.sequence--);
if ([HAND, GRAVE, REMOVED, DECK, EXTRA].includes(toZone))
toCards.forEach((c) => c.sequence >= to.sequence && c.sequence++);
target.zone = toZone;
target.controller = to.controler;
target.sequence = to.sequence;
target.code = code;
target.position = to.position;
// 注意,一个monster的overlayMaterials中的每一项都是一个cardType,
// 并且,overlayMaterials的idx就是超量素材的sequence。
// 如果一个card的zone是OVERLAY,那么它本身的sequence项是无意义的。
// 超量召唤:
// - 超量素材:toZone === OVERLAY, reason === REASON_MATERIAL
// - 超量怪兽:toZone === MZONE
// 解决方法是将超量素材放到一个list之中,等待超量怪兽的Move消息到来时从list中获取超量素材补充到超量怪兽的素材中
// 超量怪兽增加超量素材
// - 超量素材:toZone === OVERLAY, reason !== REASON_MATERIAL
// 这里要注意toZone和toSequence的不一致
// 超量素材(target)是cardStore.at(from.location, from.controler, from.sequence)
// 超量怪兽(xyzMonster)是cardStore.at(MZONE, to.controler, to.sequence)
// 超量怪兽失去超量素材
// - 超量素材:fromZone === OVERLAY
// 超量怪兽(xyzMonster)是cardStore.at(MZONE, from.controler, from.sequence)
// 超量素材(target)是xyzMoster.overlayMaterials[from.overlay_sequence]
// 在超量召唤/超量素材更改时候,target是超量素材,但同时也要维护超量怪兽的overlayMaterials
// token登场
// - token:fromZone === TZONE
// token离场
// - token:toZone === TZONE
};
import { ygopro } from "@/api";
import MsgPosChange = ygopro.StocGameMessage.MsgPosChange;
import { fetchEsHintMeta, matStore } from "@/stores";
import { fetchEsHintMeta, matStore, cardStore } from "@/stores";
export default (posChange: MsgPosChange) => {
const { location, controler, sequence } = posChange.card_info;
cardStore.at(location, controler, sequence).position = posChange.cur_position;
switch (location) {
case ygopro.CardZone.MZONE: {
matStore.monsters.of(controler)[sequence].location.position =
......
import { v4 as uuidv4 } from "uuid";
import { ygopro } from "@/api";
import { matStore } from "@/stores";
import { matStore, cardStore } from "@/stores";
type MsgReloadField = ygopro.StocGameMessage.MsgReloadField;
type ZoneActions = ygopro.StocGameMessage.MsgReloadField.ZoneAction[];
......@@ -61,4 +61,7 @@ function reloadDuelField(
.in(cardZone)
.of(controller)
.push(...cards);
// FIXME cardStore的逻辑不是很好处理...
// 以后再写
}
import { ygopro } from "@/api";
import { matStore } from "@/stores";
import { matStore, cardStore } from "@/stores";
import { zip } from "@/ui/Duel/utils";
type MsgShuffleHand = ygopro.StocGameMessage.MsgShuffleHand;
export default (shuffleHand: MsgShuffleHand) => {
const { hands: codes, player: controller } = shuffleHand;
const indexMap = new Map(codes.map((code, idx) => [code, idx]));
// 本质上是要将手卡的sequence变成和codes一样的顺序
const hands = cardStore.at(ygopro.CardZone.HAND, controller);
const t: Record<number, number[]> = {};
codes.forEach((code, sequence) => {
t[code] = t[code] || [];
t[code].push(sequence);
});
hands.forEach((hand) => {
const sequence = t[hand.code].shift();
if (sequence === undefined) {
throw new Error("手牌数量和洗牌后的数量不一致");
}
hand.sequence = sequence;
});
const uuids = matStore.hands.of(controller).map((hand) => hand.uuid);
const data = zip(uuids, codes).map(([uuid, id]) => {
return { uuid, id };
});
const indexMap = new Map(codes.map((code, idx) => [code, idx]));
matStore.hands.of(controller).sort((a, b) => {
const indexA = indexMap.get(a.occupant?.id ?? 0) ?? 0;
const indexB = indexMap.get(b.occupant?.id ?? 0) ?? 0;
......
......@@ -100,6 +100,8 @@ export default (start: ygopro.StocGameMessage.MsgStart) => {
.in(ygopro.CardZone.EXTRA)
.me.forEach((state) => (state.location.controler = 1 - opponent));
// 下面是cardStore的初始化
/** 自动从code推断出occupant */
const genCard = (o: CardType) => {
// FIXME 还没处理超量
......@@ -112,22 +114,36 @@ export default (start: ygopro.StocGameMessage.MsgStart) => {
return t;
};
const TOKEN_SIZE = 13; // 每人场上最多就只可能有13个token
const cards = flatten(
[start.deckSize1, start.extraSize1, start.deckSize2, start.extraSize2].map(
(length, i) =>
Array.from({ length }).map((_, sequence) =>
genCard({
uuid: v4uuid(),
code: 0,
controller: i < 2 ? 1 - opponent : opponent, // 前两个是自己的卡组,后两个是对手的卡组
zone: i % 2 ? ygopro.CardZone.EXTRA : ygopro.CardZone.DECK,
counters: {},
idleInteractivities: [],
sequence,
data: {},
text: {},
})
)
[
start.deckSize1,
start.extraSize1,
TOKEN_SIZE,
start.deckSize2,
start.extraSize2,
TOKEN_SIZE,
].map((length, i) =>
Array.from({ length }).map((_, sequence) =>
genCard({
// uuid: v4uuid(),
code: 0,
controller: i < 3 ? 1 - opponent : opponent, // 前3个是自己的卡组,后3个是对手的卡组
originController: i < 3 ? 1 - opponent : opponent,
zone: [
ygopro.CardZone.DECK,
ygopro.CardZone.EXTRA,
ygopro.CardZone.TZONE,
][i % 3],
counters: {},
idleInteractivities: [],
sequence,
data: {},
text: {},
isToken: !((i + 1) % 3),
overlayMaterials: [],
})
)
)
);
......
import { CardData, CardMeta, CardText, fetchCard, ygopro } from "@/api";
import { CardData, CardText, fetchCard, ygopro } from "@/api";
import { proxy } from "valtio";
import { Interactivity } from "./matStore/types";
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE } = ygopro.CardZone;
/**
* 场上某位置的状态,
* 以后会更名为 BlockState
*/
export interface CardType {
uuid: string; // 一张卡的唯一标识
// uuid: string; // FIXME 一张卡的唯一标识 一定需要这个吗?list的idx是不是就够了?
code: number;
data: CardData;
text: CardText;
controller?: number; // 控制这个位置的玩家,0或1
controller: number; // 控制这个位置的玩家,0或1
originController: number; // 在卡组构建之中持有这张卡的玩家,方便reloadField的使用
zone: ygopro.CardZone; // 怪兽区/魔法陷阱区/手牌/卡组/墓地/除外区
position?: ygopro.CardPosition; // 卡片的姿势:攻击还是守备
sequence: number; // 卡片在区域中的序号
......@@ -23,9 +22,11 @@ export interface CardType {
zone: ygopro.CardZone;
sequence: number;
}>; // 选择位置状态下的互动信息
overlay_materials?: CardMeta[]; // 超量素材, FIXME: 这里需要加上UUID
overlayMaterials: CardType[]; // 超量素材, FIXME: 这里需要加上UUID
xyzMonster?: CardType; // 超量怪兽(这张卡作为这个怪兽的超量素材)
counters: { [type: number]: number }; // 指示器
reload?: boolean; // 这个字段会在收到MSG_RELOAD_FIELD的时候设置成true,在收到MSG_UPDATE_DATE的时候设置成false
isToken: boolean; // 是否是token
}
class CardStore {
......@@ -46,25 +47,6 @@ class CardStore {
);
}
}
move(
code: number,
from: { zone: ygopro.CardZone; controller: number; sequence: number },
to: { zone: ygopro.CardZone; controller: number; sequence: number }
) {
// TODO:考虑超量素材的情况
const fromCards = this.at(from.zone, from.controller);
const toCards = this.at(to.zone, to.controller);
const target = this.at(from.zone, from.controller, from.sequence);
if ([HAND, GRAVE, REMOVED, DECK, EXTRA].includes(from.zone))
fromCards.forEach((c) => c.sequence > from.sequence && c.sequence--);
if ([HAND, GRAVE, REMOVED, DECK, EXTRA].includes(to.zone))
toCards.forEach((c) => c.sequence >= to.sequence && c.sequence++);
target.zone = to.zone;
target.controller = to.controller;
target.sequence = to.sequence;
target.code = code;
}
}
export const cardStore = proxy(new CardStore());
......
......@@ -141,7 +141,7 @@ const genDuelCardArray = (cardStates: CardState[], zone: ygopro.CardZone) => {
* 根据自己的先后手判断是否是自己
* 原本名字叫judgeSelf
*/
const isMe = (controller: number): boolean => {
export const isMe = (controller: number): boolean => {
switch (matStore.selfType) {
case 1:
// 自己是先攻
......
......@@ -2,40 +2,39 @@
// thanks!
@charset "utf-8";
ol, ul {
list-style: none;
ol,
ul {
list-style: none;
}
blockquote, q {
quotes: none;
blockquote,
q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
border-collapse: collapse;
border-spacing: 0;
}
#root {
display: flex;
margin: 0 auto;
text-align: center;
}
@import
url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css"),
"commom",
"header",
"login-form",
"sign-in";
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css"),
"commom", "header", "login-form", "sign-in";
body {
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font: 87.5%/1.5em 'Open Sans', sans-serif;
font: 87.5%/1.5em "Open Sans", sans-serif;
display: flex;
margin: 0;
place-items: center;
......@@ -44,20 +43,20 @@ body {
}
a {
text-decoration: none;
text-decoration: none;
}
input {
border: none;
font-family: 'Open Sans', Arial, sans-serif;
font-size: 14px;
line-height: 1.5em;
padding: 0;
-webkit-appearance: none;
border: none;
font-family: "Open Sans", Arial, sans-serif;
font-size: 14px;
line-height: 1.5em;
padding: 0;
-webkit-appearance: none;
}
p {
line-height: 1.5em;
line-height: 1.5em;
}
.clearfix {
......@@ -65,25 +64,28 @@ p {
&:before,
&:after {
content: ' ';
content: " ";
display: table;
}
&:after {
clear: both;
}
}
.container {
margin: 0 auto;
// left: 50%;
// position: fixed;
// top: 50%;
// transform: translate(-50%, -50%);
margin: 0 auto;
width: 100%;
max-width: 300px;
margin-top: 200px;
max-width: 300px;
margin-top: 200px;
}
.g-row {
margin: 0 auto;
width: 100%;
max-width: 1000px;
margin: 0 auto;
width: 100%;
max-width: 1000px;
}
......@@ -44,8 +44,9 @@ button:focus-visible {
position: fixed;
display: flex;
gap: 20px;
bottom: 20px;
top: 20px;
right: 20px;
z-index: 999;
}
#life-bar-container {
......@@ -62,12 +63,12 @@ button:focus-visible {
#life-bar {
padding: 0.8em 1.6em;
background-color: #A9A9A9;
background-color: #a9a9a9;
border-radius: 8px;
text-align: left;
border: 1px solid transparent;
color: black;
opacity: .4;
opacity: 0.4;
}
#camera {
......@@ -106,7 +107,8 @@ button:focus-visible {
rotate: calc(var(--opponent-deg) * (1 - var(--vertical)));
transform-style: preserve-3d;
z-index: 10;
animation: glow calc(var(--highlight-interval) * var(--highlight-on)) ease-out infinite alternate;
animation: glow calc(var(--highlight-interval) * var(--highlight-on)) ease-out
infinite alternate;
}
.card-defense {
......@@ -128,7 +130,8 @@ button:focus-visible {
transform: translateZ(var(--z));
translate: var(--x) var(--y);
rotate: calc(90deg + var(--opponent-deg));
animation: glow calc(var(--highlight-interval) * var(--highlight-on)) ease-out infinite alternate;
animation: glow calc(var(--highlight-interval) * var(--highlight-on)) ease-out
infinite alternate;
}
.card::after {
......@@ -154,7 +157,8 @@ button:focus-visible {
height: var(--block-height);
background-color: #333;
cursor: pointer;
animation: glow calc(var(--highlight-interval) * var(--highlight-on)) ease-out infinite alternate;
animation: glow calc(var(--highlight-interval) * var(--highlight-on)) ease-out
infinite alternate;
}
.block-extra {
......@@ -231,22 +235,26 @@ button:focus-visible {
@keyframes glow {
0% {
border-color: var(--highlight-color-x);
box-shadow: 0 0 5px rgba(0,255,0,.2), inset 0 0 5px rgba(0,255,0,.1), 0 1px 0 #393;
border-color: var(--highlight-color-x);
box-shadow: 0 0 5px rgba(0, 255, 0, 0.2), inset 0 0 5px rgba(0, 255, 0, 0.1),
0 1px 0 #393;
}
100% {
border-color: var(--highlight-color-y);
box-shadow: 0 0 20px rgba(0,255,0,.6), inset 0 0 10px rgba(0,255,0,.4), 0 1px 0 #6f6;
border-color: var(--highlight-color-y);
box-shadow: 0 0 20px rgba(0, 255, 0, 0.6),
inset 0 0 10px rgba(0, 255, 0, 0.4), 0 1px 0 #6f6;
}
}
@keyframes glow-hover {
0% {
border-color: #CBCC24;
box-shadow: 0 0 5px rgba(255,255,0,.2), inset 0 0 5px rgba(255,255,0,.1), 0 1px 0 #CBCC24;
border-color: #cbcc24;
box-shadow: 0 0 5px rgba(255, 255, 0, 0.2),
inset 0 0 5px rgba(255, 255, 0, 0.1), 0 1px 0 #cbcc24;
}
100% {
border-color: #F0F224;
box-shadow: 0 0 20px rgba(255,255,0,.6), inset 0 0 10px rgba(255,255,0,.4), 0 1px 0 #F0F224;
border-color: #f0f224;
box-shadow: 0 0 20px rgba(255, 255, 0, 0.6),
inset 0 0 10px rgba(255, 255, 0, 0.4), 0 1px 0 #f0f224;
}
}
......@@ -16,12 +16,15 @@ import {
import Mat from "./PlayMat";
import { Test } from "./Test";
import { Mat as NewMat } from "./NewPlayMat";
const NeosDuel = () => {
return (
<>
<Alert />
<Test />
{/* <Test /> */}
<Mat />
<NewMat />
<CardModal />
<CardListModal />
<HintNotification />
......
section#mat {
.mat-bg {
display: flex;
flex-direction: column;
row-gap: var(--row-gap);
justify-content: center;
align-items: center;
.bg-row {
display: flex;
column-gap: var(--col-gap);
}
}
.block {
height: var(--block-height-m);
width: var(--block-width);
background-color: gray;
&.extra {
margin-inline: calc(var(--block-width) / 2 + var(--col-gap) / 2);
}
&.szone {
height: var(--block-height-s);
}
}
}
import { type FC } from "react";
import classnames from "classnames";
import "./index.scss";
const BgRow: FC<{ isExtra?: boolean; isSzone?: boolean }> = ({
isExtra = false,
isSzone = false,
}) => (
<div className={classnames("bg-row")}>
{Array.from({ length: isExtra ? 2 : 5 }).map((_, i) => (
<div
key={i}
className={classnames("block", { extra: isExtra }, { szone: isSzone })}
></div>
))}
</div>
);
export const Bg: FC = () => {
return (
<div className="mat-bg">
<BgRow isSzone />
<BgRow />
<BgRow isExtra />
<BgRow />
<BgRow isSzone />
</div>
);
};
section#mat {
.mat-card {
position: absolute;
left: 0;
top: 0;
--card-height: 100px;
height: var(--card-height);
aspect-ratio: var(--card-ratio);
background-color: red;
}
}
import React, { type FC } from "react";
import classnames from "classnames";
import { CardType, cardStore, isMe } from "@/stores";
import "./index.scss";
import { useSnapshot, INTERNAL_Snapshot as Snapshot } from "valtio";
import { watch } from "valtio/utils";
import { useSpringRef, useSpring, animated, to } from "@react-spring/web";
import { matConfig } from "../utils";
import { ygopro } from "@/api";
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE, OVERLAY } =
ygopro.CardZone;
const {
BLOCK_WIDTH,
BLOCK_HEIGHT_M,
BLOCK_HEIGHT_S,
CARD_RATIO,
COL_GAP,
ROW_GAP,
} = matConfig;
export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
const state = cardStore.inner[idx];
const snap = useSnapshot(state);
const inintialCoord = calcCoordinate(state, !isMe(state.controller));
const api = useSpringRef();
const props = useSpring({
ref: api,
from: {
x: inintialCoord.translateX,
y: inintialCoord.translateY,
z: inintialCoord.translateZ,
rotateX: inintialCoord.rotateX,
rotateY: inintialCoord.rotateY,
rotateZ: inintialCoord.rotateZ,
height: inintialCoord.height,
},
});
watch((get) => {
const { zone, sequence, controller, xyzMonster } = get(state);
const coord = calcCoordinate(state, !isMe(state.controller));
api.start({
to: {
x: coord.translateX,
y: coord.translateY,
z: coord.translateZ,
rotateX: coord.rotateX,
rotateY: coord.rotateY,
rotateZ: coord.rotateZ,
height: coord.height,
},
});
});
return (
<animated.div
className="mat-card"
style={{
transform: to(
[
props.x,
props.y,
props.z,
props.rotateX,
props.rotateY,
props.rotateZ,
],
(x, y, z, rx, ry, rz) =>
`translate3d(${x}px, ${y}px, ${z}px) rotateZ(${rz}deg)`
),
height: props.height,
}}
>
{snap.text.name}
{(Math.random() * 1000).toFixed()}
</animated.div>
);
});
function calcCoordinate(
{ zone, sequence, position, xyzMonster }: CardType,
opponent: boolean
) {
const res = {
translateX: 0,
translateY: 0,
translateZ: 0,
rotateX: 0,
rotateY: 0,
rotateZ: 0,
height: 0,
};
let row = -1,
col = -1;
if ([MZONE, SZONE].includes(zone)) {
row =
zone === MZONE ? (sequence > 4 ? 2 : opponent ? 1 : 3) : opponent ? 0 : 4;
col = sequence > 4 ? (sequence > 5 ? 3 : 1) : sequence;
if (opponent) col = posHelper[col];
}
if (zone === OVERLAY && xyzMonster) {
const { zone, sequence } = xyzMonster;
row =
zone === MZONE ? (sequence > 4 ? 2 : opponent ? 1 : 3) : opponent ? 0 : 4;
col = sequence > 4 ? (sequence > 5 ? 3 : 1) : sequence;
if (opponent) col = posHelper[col];
}
const isField = zone === SZONE && sequence === 5;
if (isField) {
row = opponent ? 1 : 3;
col = opponent ? 5 : -1;
}
const _position =
zone === OVERLAY && xyzMonster ? xyzMonster.position : position;
const defense = [
ygopro.CardPosition.DEFENSE,
ygopro.CardPosition.FACEDOWN_DEFENSE,
ygopro.CardPosition.FACEUP_DEFENSE,
].includes(_position ?? 5);
res.rotateZ = opponent ? 180 : 0;
res.rotateZ += defense ? 90 : 0;
res.height = defense
? BLOCK_WIDTH.value
: zone === MZONE
? BLOCK_HEIGHT_M.value
: BLOCK_HEIGHT_S.value;
const blockPaddingX = (BLOCK_WIDTH.value - res.height * CARD_RATIO.value) / 2;
if (row > -1) {
// 说明是场上的卡
res.translateX = (BLOCK_WIDTH.value + COL_GAP.value) * col + blockPaddingX;
res.translateY =
ROW_GAP.value * row +
BLOCK_HEIGHT_M.value * Math.min(Math.max(0, row - 1), 3) +
BLOCK_HEIGHT_S.value * Math.ceil(row / 4);
}
console.log({ col, row });
return res;
}
const posHelper: Record<number, number> = {
0: 4,
1: 3,
2: 2,
3: 1,
4: 0,
5: 6,
6: 5,
};
section#mat {
margin-top: 200px;
padding-top: 50px; // 先不管 后面调整
position: relative;
#camera {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
perspective: var(--perspective);
transform-style: preserve-3d;
}
#plane {
transform: translateX(0) translateY(0) translateZ(0)
rotateX(var(--plane-rotate-z));
width: fit-content;
}
}
import type { FC, PropsWithChildren } from "react";
import "./index.scss";
import { Bg } from "../Bg";
import { Card } from "../Card";
import { type CSSConfig, toCssProperties, matConfig } from "../utils";
import { cardStore } from "@/stores";
import { useSnapshot } from "valtio";
// 后面再改名
export const Mat: FC = () => {
const snap = useSnapshot(cardStore.inner);
return (
<section
id="mat"
style={{
width: "100%",
// height: "100vh",
backgroundColor: "black",
...toCssProperties(matConfig),
}}
>
<Plane>
<Bg />
{snap.map((cardSnap, i) =>
cardSnap.zone ? <Card key={i} idx={i} /> : null
)}
</Plane>
</section>
);
};
const Plane: FC<PropsWithChildren> = ({ children }) => (
<div id="camera">
<div id="plane">{children}</div>
</div>
);
export * from "./Mat";
type CSSValue = [number, string] | number;
export type CSSConfig = Record<string, { value: number; unit: UNIT }>;
/** 转为CSS变量: BOARD_ROTATE_Z -> --board-rotate-z */
export const toCssProperties = (config: CSSConfig) =>
Object.entries(config)
.map(([k, v]) => ({
[`--${k
.split("_")
.map((s) => s.toLowerCase())
.join("-")}`]: `${v.value}${v.unit}`,
}))
.reduce((acc, cur) => ({ ...acc, ...cur }), {});
enum UNIT {
PX = "px",
DEG = "deg",
NONE = "",
}
export const matConfig = {
PERSPECTIVE: {
value: 1500,
unit: UNIT.PX,
},
PLANE_ROTATE_Z: {
value: 20,
unit: UNIT.DEG,
},
BLOCK_WIDTH: {
value: 120,
unit: UNIT.PX,
},
BLOCK_HEIGHT_M: {
value: 120,
unit: UNIT.PX,
}, // 主要怪兽区
BLOCK_HEIGHT_S: {
value: 110,
unit: UNIT.PX,
}, // 魔法陷阱区
ROW_GAP: {
value: 10,
unit: UNIT.PX,
},
COL_GAP: {
value: 10,
unit: UNIT.PX,
},
CARD_RATIO: {
value: 5.9 / 8.6,
unit: UNIT.NONE,
},
};
export * from "./cssConfig";
import { cardStore } from "@/stores";
import { useSnapshot } from "valtio";
import { subscribeKey, watch } from "valtio/utils";
import { FC, memo, useEffect, useState } from "react";
import { ygopro } from "@/api";
import {
......@@ -10,6 +11,7 @@ import {
} from "@react-spring/web";
export const Test = () => {
const snap = useSnapshot(cardStore.inner);
return (
<div
style={{
......@@ -22,46 +24,59 @@ export const Test = () => {
fontSize: 12,
}}
>
{cardStore.inner.map((cardState, i) => (
<Card idx={i} key={cardState.uuid} />
{snap.map((cardState, i) => (
<Card
idx={i}
key={i}
show={[
ygopro.CardZone.HAND,
ygopro.CardZone.MZONE,
ygopro.CardZone.SZONE,
ygopro.CardZone.GRAVE,
].includes(cardState.zone)}
/>
))}
</div>
);
};
export const Card: FC<{ idx: number }> = memo(({ idx }) => {
const snap = useSnapshot(cardStore.inner[idx]);
const [show, setShow] = useState(false);
const api = useSpringRef();
const props = useSpring({
ref: api,
from: { x: 0 },
});
useEffect(() => {
setShow(
[
ygopro.CardZone.HAND,
ygopro.CardZone.MZONE,
ygopro.CardZone.SZONE,
].includes(snap.zone)
);
api.start({
to: {
x: props.x.get() === 100 ? 0 : 100,
},
export const Card: FC<{ idx: number; show: boolean }> = memo(
({ idx, show }) => {
const snap = useSnapshot(cardStore.inner[idx]);
const api = useSpringRef();
const props = useSpring({
ref: api,
from: { x: 0 },
});
}, [snap.zone]); // 添加 show 到依赖项中
return show ? (
<animated.div
style={{
transform: props.x.to((value) => `translateX(${value}px)`),
background: "white",
}}
>
<div>code: {snap.code}</div>
<div>{(Math.random() * 100).toFixed(0)}</div>
</animated.div>
) : (
<></>
);
});
// subscribeKey(cardStore.inner[idx], "zone", (value) => {
// api.start({
// to: {
// x: value * 100,
// },
// });
// });
watch((get) => {
get(cardStore.inner[idx]);
const zone = get(cardStore.inner[idx]).zone;
api.start({
to: {
x: zone * 100,
},
});
});
return show ? (
<animated.div
style={{
transform: props.x.to((value) => `translateX(${value}px)`),
background: "white",
}}
>
<div>code: {snap.code}</div>
<div>{(Math.random() * 100).toFixed(0)}</div>
</animated.div>
) : (
<></>
);
},
(prev, next) => prev.show === next.show // 只有 show 变化时才会重新渲染
);
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