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> { ...@@ -57,6 +57,8 @@ export async function fetchCard(id: number): Promise<CardMeta> {
return res.selectResult ? res.selectResult : { id, data: {}, text: {} }; return res.selectResult ? res.selectResult : { id, data: {}, text: {} };
} }
window.fetchCard = fetchCard;
export function getCardStr(meta: CardMeta, idx: number): string | undefined { export function getCardStr(meta: CardMeta, idx: number): string | undefined {
switch (idx) { switch (idx) {
case 0: { case 0: {
......
...@@ -23,6 +23,7 @@ export namespace ygopro { ...@@ -23,6 +23,7 @@ export namespace ygopro {
ONFIELD = 8, ONFIELD = 8,
FZONE = 9, FZONE = 9,
PZONE = 10, PZONE = 10,
TZONE = 11, // 还在想有没有什么好的解决方案
} }
export enum CardPosition { export enum CardPosition {
FACEUP_ATTACK = 0, FACEUP_ATTACK = 0,
......
import { v4 as v4uuid } from "uuid"; import { v4 as v4uuid } from "uuid";
import { ygopro } from "@/api"; import { fetchCard, ygopro } from "@/api";
import { fetchOverlayMeta, store, cardStore } from "@/stores"; import { fetchOverlayMeta, store, cardStore, CardType } from "@/stores";
type MsgMove = ygopro.StocGameMessage.MsgMove; type MsgMove = ygopro.StocGameMessage.MsgMove;
import { useConfig } from "@/config"; import { useConfig } from "@/config";
import { sleep } from "@/infra"; import { sleep } from "@/infra";
...@@ -11,30 +11,23 @@ import { REASON_MATERIAL } from "../../common"; ...@@ -11,30 +11,23 @@ import { REASON_MATERIAL } from "../../common";
const { matStore } = store; const { matStore } = store;
const NeosConfig = useConfig(); 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 OVERLAY_STACK: { uuid: string; code: number; sequence: number }[] = [];
const overlayStack: CardType[] = [];
export default async (move: MsgMove) => { export default async (move: MsgMove) => {
const code = move.code; const code = move.code;
const from = move.from; const from = move.from;
const to = move.to; const to = move.to;
const reason = move.reason; 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: 考虑超量素材的情况
// FIXME:需要考虑【卡名当作另一张卡】的情况
let uuid; let uuid;
let chainIndex; let chainIndex;
switch (from.location) { switch (from.location) {
...@@ -166,4 +159,103 @@ export default async (move: MsgMove) => { ...@@ -166,4 +159,103 @@ export default async (move: MsgMove) => {
break; 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 { ygopro } from "@/api";
import MsgPosChange = ygopro.StocGameMessage.MsgPosChange; import MsgPosChange = ygopro.StocGameMessage.MsgPosChange;
import { fetchEsHintMeta, matStore } from "@/stores"; import { fetchEsHintMeta, matStore, cardStore } from "@/stores";
export default (posChange: MsgPosChange) => { export default (posChange: MsgPosChange) => {
const { location, controler, sequence } = posChange.card_info; const { location, controler, sequence } = posChange.card_info;
cardStore.at(location, controler, sequence).position = posChange.cur_position;
switch (location) { switch (location) {
case ygopro.CardZone.MZONE: { case ygopro.CardZone.MZONE: {
matStore.monsters.of(controler)[sequence].location.position = matStore.monsters.of(controler)[sequence].location.position =
......
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { matStore } from "@/stores"; import { matStore, cardStore } from "@/stores";
type MsgReloadField = ygopro.StocGameMessage.MsgReloadField; type MsgReloadField = ygopro.StocGameMessage.MsgReloadField;
type ZoneActions = ygopro.StocGameMessage.MsgReloadField.ZoneAction[]; type ZoneActions = ygopro.StocGameMessage.MsgReloadField.ZoneAction[];
...@@ -61,4 +61,7 @@ function reloadDuelField( ...@@ -61,4 +61,7 @@ function reloadDuelField(
.in(cardZone) .in(cardZone)
.of(controller) .of(controller)
.push(...cards); .push(...cards);
// FIXME cardStore的逻辑不是很好处理...
// 以后再写
} }
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { matStore } from "@/stores"; import { matStore, cardStore } from "@/stores";
import { zip } from "@/ui/Duel/utils";
type MsgShuffleHand = ygopro.StocGameMessage.MsgShuffleHand; type MsgShuffleHand = ygopro.StocGameMessage.MsgShuffleHand;
export default (shuffleHand: MsgShuffleHand) => { export default (shuffleHand: MsgShuffleHand) => {
const { hands: codes, player: controller } = shuffleHand; 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) => { matStore.hands.of(controller).sort((a, b) => {
const indexA = indexMap.get(a.occupant?.id ?? 0) ?? 0; const indexA = indexMap.get(a.occupant?.id ?? 0) ?? 0;
const indexB = indexMap.get(b.occupant?.id ?? 0) ?? 0; const indexB = indexMap.get(b.occupant?.id ?? 0) ?? 0;
......
...@@ -100,6 +100,8 @@ export default (start: ygopro.StocGameMessage.MsgStart) => { ...@@ -100,6 +100,8 @@ export default (start: ygopro.StocGameMessage.MsgStart) => {
.in(ygopro.CardZone.EXTRA) .in(ygopro.CardZone.EXTRA)
.me.forEach((state) => (state.location.controler = 1 - opponent)); .me.forEach((state) => (state.location.controler = 1 - opponent));
// 下面是cardStore的初始化
/** 自动从code推断出occupant */ /** 自动从code推断出occupant */
const genCard = (o: CardType) => { const genCard = (o: CardType) => {
// FIXME 还没处理超量 // FIXME 还没处理超量
...@@ -112,20 +114,34 @@ export default (start: ygopro.StocGameMessage.MsgStart) => { ...@@ -112,20 +114,34 @@ export default (start: ygopro.StocGameMessage.MsgStart) => {
return t; return t;
}; };
const TOKEN_SIZE = 13; // 每人场上最多就只可能有13个token
const cards = flatten( const cards = flatten(
[start.deckSize1, start.extraSize1, start.deckSize2, start.extraSize2].map( [
(length, i) => start.deckSize1,
start.extraSize1,
TOKEN_SIZE,
start.deckSize2,
start.extraSize2,
TOKEN_SIZE,
].map((length, i) =>
Array.from({ length }).map((_, sequence) => Array.from({ length }).map((_, sequence) =>
genCard({ genCard({
uuid: v4uuid(), // uuid: v4uuid(),
code: 0, code: 0,
controller: i < 2 ? 1 - opponent : opponent, // 前两个是自己的卡组,后两个是对手的卡组 controller: i < 3 ? 1 - opponent : opponent, // 前3个是自己的卡组,后3个是对手的卡组
zone: i % 2 ? ygopro.CardZone.EXTRA : ygopro.CardZone.DECK, originController: i < 3 ? 1 - opponent : opponent,
zone: [
ygopro.CardZone.DECK,
ygopro.CardZone.EXTRA,
ygopro.CardZone.TZONE,
][i % 3],
counters: {}, counters: {},
idleInteractivities: [], idleInteractivities: [],
sequence, sequence,
data: {}, data: {},
text: {}, 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 { proxy } from "valtio";
import { Interactivity } from "./matStore/types"; import { Interactivity } from "./matStore/types";
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE } = ygopro.CardZone;
/** /**
* 场上某位置的状态, * 场上某位置的状态,
* 以后会更名为 BlockState * 以后会更名为 BlockState
*/ */
export interface CardType { export interface CardType {
uuid: string; // 一张卡的唯一标识 // uuid: string; // FIXME 一张卡的唯一标识 一定需要这个吗?list的idx是不是就够了?
code: number; code: number;
data: CardData; data: CardData;
text: CardText; text: CardText;
controller?: number; // 控制这个位置的玩家,0或1 controller: number; // 控制这个位置的玩家,0或1
originController: number; // 在卡组构建之中持有这张卡的玩家,方便reloadField的使用
zone: ygopro.CardZone; // 怪兽区/魔法陷阱区/手牌/卡组/墓地/除外区 zone: ygopro.CardZone; // 怪兽区/魔法陷阱区/手牌/卡组/墓地/除外区
position?: ygopro.CardPosition; // 卡片的姿势:攻击还是守备 position?: ygopro.CardPosition; // 卡片的姿势:攻击还是守备
sequence: number; // 卡片在区域中的序号 sequence: number; // 卡片在区域中的序号
...@@ -23,9 +22,11 @@ export interface CardType { ...@@ -23,9 +22,11 @@ export interface CardType {
zone: ygopro.CardZone; zone: ygopro.CardZone;
sequence: number; sequence: number;
}>; // 选择位置状态下的互动信息 }>; // 选择位置状态下的互动信息
overlay_materials?: CardMeta[]; // 超量素材, FIXME: 这里需要加上UUID overlayMaterials: CardType[]; // 超量素材, FIXME: 这里需要加上UUID
xyzMonster?: CardType; // 超量怪兽(这张卡作为这个怪兽的超量素材)
counters: { [type: number]: number }; // 指示器 counters: { [type: number]: number }; // 指示器
reload?: boolean; // 这个字段会在收到MSG_RELOAD_FIELD的时候设置成true,在收到MSG_UPDATE_DATE的时候设置成false reload?: boolean; // 这个字段会在收到MSG_RELOAD_FIELD的时候设置成true,在收到MSG_UPDATE_DATE的时候设置成false
isToken: boolean; // 是否是token
} }
class CardStore { class CardStore {
...@@ -46,25 +47,6 @@ 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()); export const cardStore = proxy(new CardStore());
......
...@@ -141,7 +141,7 @@ const genDuelCardArray = (cardStates: CardState[], zone: ygopro.CardZone) => { ...@@ -141,7 +141,7 @@ const genDuelCardArray = (cardStates: CardState[], zone: ygopro.CardZone) => {
* 根据自己的先后手判断是否是自己 * 根据自己的先后手判断是否是自己
* 原本名字叫judgeSelf * 原本名字叫judgeSelf
*/ */
const isMe = (controller: number): boolean => { export const isMe = (controller: number): boolean => {
switch (matStore.selfType) { switch (matStore.selfType) {
case 1: case 1:
// 自己是先攻 // 自己是先攻
......
...@@ -2,15 +2,19 @@ ...@@ -2,15 +2,19 @@
// thanks! // thanks!
@charset "utf-8"; @charset "utf-8";
ol, ul { ol,
ul {
list-style: none; list-style: none;
} }
blockquote, q { blockquote,
q {
quotes: none; quotes: none;
} }
blockquote:before, blockquote:after, blockquote:before,
q:before, q:after { blockquote:after,
content: ''; q:before,
q:after {
content: "";
content: none; content: none;
} }
table { table {
...@@ -19,23 +23,18 @@ table { ...@@ -19,23 +23,18 @@ table {
} }
#root { #root {
display: flex;
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
} }
@import @import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css"),
url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css"), "commom", "header", "login-form", "sign-in";
"commom",
"header",
"login-form",
"sign-in";
body { body {
color-scheme: light dark; color-scheme: light dark;
color: rgba(255, 255, 255, 0.87); color: rgba(255, 255, 255, 0.87);
background-color: #242424; background-color: #242424;
font: 87.5%/1.5em 'Open Sans', sans-serif; font: 87.5%/1.5em "Open Sans", sans-serif;
display: flex; display: flex;
margin: 0; margin: 0;
place-items: center; place-items: center;
...@@ -49,7 +48,7 @@ a { ...@@ -49,7 +48,7 @@ a {
input { input {
border: none; border: none;
font-family: 'Open Sans', Arial, sans-serif; font-family: "Open Sans", Arial, sans-serif;
font-size: 14px; font-size: 14px;
line-height: 1.5em; line-height: 1.5em;
padding: 0; padding: 0;
...@@ -65,17 +64,20 @@ p { ...@@ -65,17 +64,20 @@ p {
&:before, &:before,
&:after { &:after {
content: ' '; content: " ";
display: table; display: table;
} }
&:after { &:after {
clear: both; clear: both;
} }
} }
.container { .container {
// left: 50%;
// position: fixed;
// top: 50%;
// transform: translate(-50%, -50%);
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
......
...@@ -44,8 +44,9 @@ button:focus-visible { ...@@ -44,8 +44,9 @@ button:focus-visible {
position: fixed; position: fixed;
display: flex; display: flex;
gap: 20px; gap: 20px;
bottom: 20px; top: 20px;
right: 20px; right: 20px;
z-index: 999;
} }
#life-bar-container { #life-bar-container {
...@@ -62,12 +63,12 @@ button:focus-visible { ...@@ -62,12 +63,12 @@ button:focus-visible {
#life-bar { #life-bar {
padding: 0.8em 1.6em; padding: 0.8em 1.6em;
background-color: #A9A9A9; background-color: #a9a9a9;
border-radius: 8px; border-radius: 8px;
text-align: left; text-align: left;
border: 1px solid transparent; border: 1px solid transparent;
color: black; color: black;
opacity: .4; opacity: 0.4;
} }
#camera { #camera {
...@@ -106,7 +107,8 @@ button:focus-visible { ...@@ -106,7 +107,8 @@ button:focus-visible {
rotate: calc(var(--opponent-deg) * (1 - var(--vertical))); rotate: calc(var(--opponent-deg) * (1 - var(--vertical)));
transform-style: preserve-3d; transform-style: preserve-3d;
z-index: 10; 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 { .card-defense {
...@@ -128,7 +130,8 @@ button:focus-visible { ...@@ -128,7 +130,8 @@ button:focus-visible {
transform: translateZ(var(--z)); transform: translateZ(var(--z));
translate: var(--x) var(--y); translate: var(--x) var(--y);
rotate: calc(90deg + var(--opponent-deg)); 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 { .card::after {
...@@ -154,7 +157,8 @@ button:focus-visible { ...@@ -154,7 +157,8 @@ button:focus-visible {
height: var(--block-height); height: var(--block-height);
background-color: #333; background-color: #333;
cursor: pointer; 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 { .block-extra {
...@@ -232,21 +236,25 @@ button:focus-visible { ...@@ -232,21 +236,25 @@ button:focus-visible {
@keyframes glow { @keyframes glow {
0% { 0% {
border-color: var(--highlight-color-x); 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; 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% { 100% {
border-color: var(--highlight-color-y); 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; 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 { @keyframes glow-hover {
0% { 0% {
border-color: #CBCC24; 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; 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% { 100% {
border-color: #F0F224; 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; 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 { ...@@ -16,12 +16,15 @@ import {
import Mat from "./PlayMat"; import Mat from "./PlayMat";
import { Test } from "./Test"; import { Test } from "./Test";
import { Mat as NewMat } from "./NewPlayMat";
const NeosDuel = () => { const NeosDuel = () => {
return ( return (
<> <>
<Alert /> <Alert />
<Test /> {/* <Test /> */}
<Mat /> <Mat />
<NewMat />
<CardModal /> <CardModal />
<CardListModal /> <CardListModal />
<HintNotification /> <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 { cardStore } from "@/stores";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import { subscribeKey, watch } from "valtio/utils";
import { FC, memo, useEffect, useState } from "react"; import { FC, memo, useEffect, useState } from "react";
import { ygopro } from "@/api"; import { ygopro } from "@/api";
import { import {
...@@ -10,6 +11,7 @@ import { ...@@ -10,6 +11,7 @@ import {
} from "@react-spring/web"; } from "@react-spring/web";
export const Test = () => { export const Test = () => {
const snap = useSnapshot(cardStore.inner);
return ( return (
<div <div
style={{ style={{
...@@ -22,35 +24,46 @@ export const Test = () => { ...@@ -22,35 +24,46 @@ export const Test = () => {
fontSize: 12, fontSize: 12,
}} }}
> >
{cardStore.inner.map((cardState, i) => ( {snap.map((cardState, i) => (
<Card idx={i} key={cardState.uuid} /> <Card
idx={i}
key={i}
show={[
ygopro.CardZone.HAND,
ygopro.CardZone.MZONE,
ygopro.CardZone.SZONE,
ygopro.CardZone.GRAVE,
].includes(cardState.zone)}
/>
))} ))}
</div> </div>
); );
}; };
export const Card: FC<{ idx: number }> = memo(({ idx }) => { export const Card: FC<{ idx: number; show: boolean }> = memo(
({ idx, show }) => {
const snap = useSnapshot(cardStore.inner[idx]); const snap = useSnapshot(cardStore.inner[idx]);
const [show, setShow] = useState(false);
const api = useSpringRef(); const api = useSpringRef();
const props = useSpring({ const props = useSpring({
ref: api, ref: api,
from: { x: 0 }, from: { x: 0 },
}); });
useEffect(() => { // subscribeKey(cardStore.inner[idx], "zone", (value) => {
setShow( // api.start({
[ // to: {
ygopro.CardZone.HAND, // x: value * 100,
ygopro.CardZone.MZONE, // },
ygopro.CardZone.SZONE, // });
].includes(snap.zone) // });
); watch((get) => {
get(cardStore.inner[idx]);
const zone = get(cardStore.inner[idx]).zone;
api.start({ api.start({
to: { to: {
x: props.x.get() === 100 ? 0 : 100, x: zone * 100,
}, },
}); });
}, [snap.zone]); // 添加 show 到依赖项中 });
return show ? ( return show ? (
<animated.div <animated.div
style={{ style={{
...@@ -64,4 +77,6 @@ export const Card: FC<{ idx: number }> = memo(({ idx }) => { ...@@ -64,4 +77,6 @@ export const Card: FC<{ idx: number }> = memo(({ idx }) => {
) : ( ) : (
<></> <></>
); );
}); },
(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