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

feat: basic animation

parent 0874d5ff
......@@ -23,6 +23,7 @@
"antd": "^5.4.0",
"axios": "^0.27.2",
"classnames": "^2.3.2",
"eventemitter3": "^5.0.1",
"google-protobuf": "^3.21.2",
"lodash-es": "^4.17.21",
"react": "^18.2.0",
......@@ -8945,9 +8946,9 @@
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/events": {
"version": "3.3.0",
......@@ -11299,6 +11300,11 @@
"node": ">=0.10.0"
}
},
"node_modules/http-proxy/node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
},
"node_modules/http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
......@@ -35195,9 +35201,9 @@
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
},
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"events": {
"version": "3.3.0",
......@@ -36888,6 +36894,13 @@
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
},
"dependencies": {
"eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
}
}
},
"http-proxy-agent": {
/// <reference types="react-scripts" />
/// <reference types="vite/client" />
/// <reference types="eventemitter3" />
interface ImportMetaEnv {
readonly VITE_IS_AI_MODE: boolean;
......@@ -13,3 +14,9 @@ interface ImportMeta {
// 定义一个全局的myExtraDeckCodes变量
declare var myExtraDeckCodes: number[];
import { EventEmitter } from "eventemitter3";
declare global {
var eventBus: EventEmitter;
}
......@@ -27,6 +27,10 @@ import { BrowserRouter } from "react-router-dom";
import Neos from "./ui/Neos";
import { EventEmitter } from "eventemitter3";
window.eventBus = new EventEmitter();
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
......
......@@ -155,7 +155,7 @@ button:focus-visible {
.block {
width: var(--block-width);
height: var(--block-height);
background-color: #333;
/* background-color: #333; */
cursor: pointer;
animation: glow calc(var(--highlight-interval) * var(--highlight-on)) ease-out
infinite alternate;
......
......@@ -17,13 +17,15 @@ import Mat from "./PlayMat";
import { Test } from "./Test";
import { Mat as NewMat } from "./NewPlayMat";
import { Menu } from "./NewPlayMat/Menu";
const NeosDuel = () => {
return (
<>
<Alert />
{/* <Test /> */}
<Mat />
{/* <Mat /> */}
<Menu />
<NewMat />
<CardModal />
<CardListModal />
......
......@@ -5,6 +5,7 @@ section#mat {
row-gap: var(--row-gap);
justify-content: center;
align-items: center;
background-color: transparent;
.bg-row {
display: flex;
column-gap: var(--col-gap);
......@@ -14,7 +15,8 @@ section#mat {
.block {
height: var(--block-height-m);
width: var(--block-width);
background-color: rgba(128, 128, 128, 0.447);
// background-color: rgba(128, 128, 128, 0.447);
box-shadow: 0 0 0 1px purple;
&.extra {
margin-inline: calc(var(--block-width) / 2 + var(--col-gap) / 2);
}
......
......@@ -6,6 +6,29 @@ section#mat {
--card-height: 100px;
height: var(--card-height);
aspect-ratio: var(--card-ratio);
background-color: red;
transform-style: preserve-3d;
.card-img-wrap {
transform-style: preserve-3d;
position: relative;
height: 100%;
width: 100%;
transform: rotateY(calc(var(--ry) * 1deg));
.card-cover,
.card-back {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
.card-cover {
z-index: 1;
transform: translateZ(0.5px);
}
.card-back {
z-index: 0;
transform: translateZ(0px);
}
}
}
}
import React, { type FC } from "react";
import React, { type CSSProperties, 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 { useSpringRef, useSpring, animated, to, CSS } from "@react-spring/web";
import { matConfig } from "../utils";
import { ygopro } from "@/api";
import { useConfig } from "@/config";
const NeosConfig = useConfig();
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE, OVERLAY } =
ygopro.CardZone;
......@@ -22,12 +25,15 @@ const {
HAND_MARGIN_TOP,
HAND_CARD_HEIGHT,
HAND_CIRCLE_CENTER_OFFSET_Y,
DECK_OFFSET_X,
DECK_OFFSET_Y,
DECK_ROTATE_Z,
} = 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 inintialCoord = calcCoordinate(state, false);
const api = useSpringRef();
const props = useSpring({
ref: api,
......@@ -41,9 +47,8 @@ export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
height: inintialCoord.height,
},
});
watch((get) => {
const { zone, sequence, controller, xyzMonster } = get(state);
const coord = calcCoordinate(state, !isMe(state.controller));
const reloadPosition = (report: boolean) => {
const coord = calcCoordinate(state, report);
api.start({
to: {
x: coord.translateX,
......@@ -55,40 +60,61 @@ export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
height: coord.height,
},
});
};
watch((get) => {
const { zone, sequence, controller, xyzMonster } = get(state);
reloadPosition(true);
});
// 在别的手卡更改时候,刷新这张手卡
eventBus.on(
"reload-hands",
({ sequence, controller }: { sequence: number; controller: number }) => {
if (state.sequence !== sequence && state.controller === controller) {
reloadPosition(false);
// console.warn('reload')
}
}
);
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) rotateX(${rx}deg) rotateZ(${rz}deg)`
),
height: props.height,
}}
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) rotateX(${rx}deg) rotateZ(${rz}deg)`
),
"--ry": props.rotateY,
height: props.height,
} as any as CSSProperties
}
>
{snap.text.name}
{(Math.random() * 1000).toFixed()}
<div className="card-img-wrap">
<img className="card-cover" src={getCardImgUrl(snap.code)} alt="" />
<img className="card-back" src={getCardImgUrl(0, true)} alt="" />
</div>
</animated.div>
);
});
function calcCoordinate(
{ zone, sequence, position, xyzMonster, controller }: CardType,
opponent: boolean
report: boolean
) {
const opponent = !isMe(controller);
const res = {
translateX: 0,
translateY: 0,
translateX: -BLOCK_WIDTH.value,
translateY: BLOCK_HEIGHT_M.value + BLOCK_HEIGHT_S.value + ROW_GAP.value * 2,
translateZ: 0,
rotateX: 0,
rotateY: 0,
......@@ -130,6 +156,16 @@ function calcCoordinate(
res.rotateZ = opponent ? 180 : 0;
res.rotateZ += defense ? 90 : 0;
res.rotateY = [
ygopro.CardPosition.FACEDOWN,
ygopro.CardPosition.FACEDOWN_ATTACK,
ygopro.CardPosition.FACEDOWN_DEFENSE,
].includes(_position ?? 5)
? 180
: res.rotateY;
res.rotateY = [DECK, EXTRA].includes(zone) ? 180 : 0;
zone === HAND && (res.rotateY = opponent ? 180 : 0);
res.height = defense
? BLOCK_WIDTH.value
: zone === MZONE
......@@ -147,19 +183,26 @@ function calcCoordinate(
BLOCK_HEIGHT_S.value * Math.ceil(row / 4);
}
if (zone === HAND && isMe(controller)) {
if (zone === HAND) {
// 得刷新除了这个卡以外所有的自己的手卡
if (report) {
eventBus.emit("reload-hands", {
controller,
sequence,
});
}
// 手卡会有很复杂的计算...
// 暂时先看成是我的手卡
const hand_circle_center_x =
(5 * BLOCK_WIDTH.value + 4 * COL_GAP.value) / 2;
const hand_circle_center_y =
2 * BLOCK_HEIGHT_M.value +
2 * BLOCK_HEIGHT_S.value +
4 * ROW_GAP.value +
HAND_MARGIN_TOP.value +
HAND_CARD_HEIGHT.value +
HAND_CIRCLE_CENTER_OFFSET_Y.value;
(3 * BLOCK_HEIGHT_M.value +
2 * BLOCK_HEIGHT_S.value +
4 * ROW_GAP.value) *
Number(!opponent) +
(HAND_MARGIN_TOP.value +
HAND_CARD_HEIGHT.value +
HAND_CIRCLE_CENTER_OFFSET_Y.value) *
(opponent ? -1 : 1);
const hand_card_width = CARD_RATIO.value * HAND_CARD_HEIGHT.value;
const THETA =
2 *
......@@ -167,7 +210,8 @@ function calcCoordinate(
hand_card_width /
2 /
(HAND_CIRCLE_CENTER_OFFSET_Y.value + HAND_CARD_HEIGHT.value)
);
) *
0.9;
// 接下来计算每一张手卡
const hands_length = cardStore.at(HAND, controller).length;
const angle = (sequence - (hands_length - 1) / 2) * THETA;
......@@ -175,14 +219,44 @@ function calcCoordinate(
const negativeX = Math.sin(angle) * r - hand_card_width / 2;
const negativeY = Math.cos(angle) * r + HAND_CARD_HEIGHT.value / 2;
const x = hand_circle_center_x + negativeX;
const y = hand_circle_center_y - negativeY;
const y =
hand_circle_center_y -
negativeY * (opponent ? -1 : 1) -
Number(opponent) * HAND_CARD_HEIGHT.value;
res.translateX = x;
res.translateY = y;
res.translateZ = 50;
res.rotateZ = (angle * 180) / Math.PI;
res.rotateZ =
((angle * 180) / Math.PI) * (opponent ? -1 : 1) + (opponent ? 180 : 0);
res.rotateX = -PLANE_ROTATE_X.value;
}
if (zone === DECK || zone === EXTRA) {
const leftX = -DECK_OFFSET_X.value;
const rightX =
DECK_OFFSET_X.value +
5 * BLOCK_WIDTH.value +
4 * COL_GAP.value -
CARD_RATIO.value * res.height;
const topY = -DECK_OFFSET_Y.value;
const bottomY =
DECK_OFFSET_Y.value +
3 * BLOCK_HEIGHT_M.value +
2 * BLOCK_HEIGHT_S.value +
4 * ROW_GAP.value -
BLOCK_HEIGHT_S.value;
res.translateX = opponent ? leftX : rightX;
res.translateY = opponent ? topY : bottomY;
if (zone === EXTRA) {
res.translateX = opponent ? rightX : leftX;
}
res.rotateZ = opponent ? -DECK_ROTATE_Z.value : 180 - DECK_ROTATE_Z.value;
if (zone === EXTRA) {
res.rotateZ = opponent ? DECK_ROTATE_Z.value : DECK_ROTATE_Z.value;
}
res.translateZ = sequence;
}
return res;
}
......@@ -195,3 +269,14 @@ const posHelper: Record<number, number> = {
5: 6,
6: 5,
};
function getCardImgUrl(code: number, back = false) {
const ASSETS_BASE =
import.meta.env.BASE_URL == "/"
? NeosConfig.assetsPath
: import.meta.env.BASE_URL + NeosConfig.assetsPath;
if (back) {
return ASSETS_BASE + "/card_back.jpg";
}
return NeosConfig.cardImgUrl + "/" + code + ".jpg";
}
section#mat {
margin-top: 200px;
padding-top: 50px; // 先不管 后面调整
// padding-top: 50px; // 先不管 后面调整
position: relative;
#camera {
height: 100%;
......@@ -9,11 +9,11 @@ section#mat {
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-x));
width: fit-content;
transform-style: preserve-3d;
}
}
......@@ -17,16 +17,14 @@ export const Mat: FC = () => {
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
)}
{snap.map((cardSnap, i) => (
<Card key={i} idx={i} />
))}
</Plane>
</section>
);
......
import "@/styles/mat.css";
import Icon from "@ant-design/icons";
import { Button, Modal } from "antd";
import { ReactComponent as BattleSvg } from "neos-assets/crossed-swords.svg";
import { ReactComponent as EpSvg } from "neos-assets/power-button.svg";
import { ReactComponent as Main2Svg } from "neos-assets/sword-in-stone.svg";
import { ReactComponent as SurrenderSvg } from "neos-assets/truce.svg";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import {
sendSelectBattleCmdResponse,
sendSelectIdleCmdResponse,
sendSurrender,
} from "@/api";
import {
clearAllIdleInteractivities as clearAllIdleInteractivities,
matStore,
} from "@/stores";
const IconSize = "150%";
const PhaseButton = (props: {
text: string;
enable: boolean;
onClick: () => void;
icon?: React.ReactNode;
}) => {
return (
<Button
icon={props.icon}
disabled={!props.enable}
onClick={props.onClick}
size="large"
>
{props.text}
</Button>
);
};
const { phase } = matStore;
export const Menu = () => {
const snapPhase = useSnapshot(phase);
const enableBp = snapPhase.enableBp;
const enableM2 = snapPhase.enableM2;
const enableEp = snapPhase.enableEp;
const currentPhase = snapPhase.currentPhase;
const [modalOpen, setModalOpen] = useState(false);
const response =
currentPhase === "BATTLE_START" ||
currentPhase === "BATTLE_STEP" ||
currentPhase === "DAMAGE" ||
currentPhase === "DAMAGE_GAL" ||
currentPhase === "BATTLE"
? 3
: 7;
const onBp = () => {
sendSelectIdleCmdResponse(6);
clearAllIdleInteractivities(0); // 为什么要clear两次?
clearAllIdleInteractivities(0);
phase.enableBp = false;
};
const onM2 = () => {
sendSelectBattleCmdResponse(2);
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(0);
phase.enableM2 = false;
};
const onEp = () => {
sendSelectIdleCmdResponse(response);
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(0);
phase.enableEp = false;
};
const onSurrender = () => {
setModalOpen(true);
};
return (
<div id="controller">
<PhaseButton
icon={<Icon component={BattleSvg} style={{ fontSize: IconSize }} />}
enable={enableBp}
text="战斗阶段"
onClick={onBp}
/>
<PhaseButton
icon={<Icon component={Main2Svg} style={{ fontSize: IconSize }} />}
enable={enableM2}
text="主要阶段2"
onClick={onM2}
/>
<PhaseButton
icon={<Icon component={EpSvg} style={{ fontSize: IconSize }} />}
enable={enableEp}
text="结束回合"
onClick={onEp}
/>
<PhaseButton
icon={<Icon component={SurrenderSvg} style={{ fontSize: IconSize }} />}
enable={true}
text="投降"
onClick={onSurrender}
/>
<Modal
title="是否确认要投降?"
open={modalOpen}
closable={false}
footer={
<>
<Button
onClick={() => {
sendSurrender();
setModalOpen(false);
}}
>
Yes
</Button>
<Button
onClick={() => {
setModalOpen(false);
}}
>
No
</Button>
</>
}
/>
</div>
);
};
......@@ -25,7 +25,7 @@ export const matConfig = {
unit: UNIT.PX,
},
PLANE_ROTATE_X: {
value: 20,
value: 0,
unit: UNIT.DEG,
},
BLOCK_WIDTH: {
......@@ -57,11 +57,23 @@ export const matConfig = {
unit: UNIT.PX,
},
HAND_CIRCLE_CENTER_OFFSET_Y: {
value: 800,
value: 2000,
unit: UNIT.PX,
},
HAND_CARD_HEIGHT: {
value: 120,
unit: UNIT.PX,
},
DECK_OFFSET_X: {
value: 140,
unit: UNIT.PX,
},
DECK_OFFSET_Y: {
value: 80,
unit: UNIT.PX,
},
DECK_ROTATE_Z: {
value: 30,
unit: UNIT.DEG,
},
};
// import { EventEmitter } from "eventemitter3";
// window.eventBus = new EventEmitter();
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