Commit 9f74038d authored by Chunchi Che's avatar Chunchi Che

Merge branch 'optimize/mat' into 'main'

Optimize/mat

See merge request mycard/Neos!175
parents cfd5585e e1c0f99c
Pipeline #21452 passed with stages
in 16 minutes and 56 seconds
This diff is collapsed.
export * from "./useEnv";
export * from "./useMeshClick";
// 一些自定义`Hook`
import { ActionEvent } from "@babylonjs/core";
import { IAction } from "@babylonjs/core/Actions/action.js";
import { ActionManager } from "@babylonjs/core/Actions/actionManager.js";
import { ExecuteCodeAction } from "@babylonjs/core/Actions/directActions.js";
import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh.js";
import { Mesh } from "@babylonjs/core/Meshes/mesh.js";
import { Nullable } from "@babylonjs/core/types.js";
import { MutableRefObject, useEffect, useRef } from "react";
export interface MeshEventType {
(env: ActionEvent): void;
}
type DependencyList = ReadonlyArray<unknown>;
/**
* rewritten useClick hook
*
* origion ref: https://github.com/brianzinn/react-babylonjs/blob/master/packages/react-babylonjs/src/hooks/utilityHooks.ts#L132
*
* Why i rewritten this?: https://github.com/brianzinn/react-babylonjs/issues/209
*
* @param onClick What would be passed in the OnPickTrigger from ActionManager
* @param ownRef to re-use a Ref you already have, otherwise one is created for you and returned.
* @param deps observation object
*/
export function useClick(
onClick: MeshEventType,
ownRef?: MutableRefObject<Nullable<Mesh>>,
deps?: DependencyList
): [MutableRefObject<Nullable<Mesh>>] {
const createdRef = useRef<Nullable<Mesh>>(null);
const ref = ownRef ?? createdRef;
useEffect(() => {
if (ref.current) {
if (ref.current instanceof AbstractMesh) {
const mesh = ref.current as Mesh;
if (!mesh.actionManager) {
mesh.actionManager = new ActionManager(mesh.getScene());
}
const action: Nullable<IAction> = mesh.actionManager.registerAction(
new ExecuteCodeAction(ActionManager.OnPickTrigger, function (
ev: any
) {
onClick(ev);
})
);
return () => {
// unregister on teardown
mesh.actionManager?.unregisterAction(action!);
};
} else {
console.warn("onClick hook only supports referencing Meshes");
}
}
}, [...(deps || []), ref]);
// todo: if use ref.current as dep, duplicate register action.
return [ref];
}
import { ygopro } from "@/api";
import { fetchEsHintMeta, matStore } from "@/stores";
import { zip } from "@/ui/Duel/utils";
export default (draw: ygopro.StocGameMessage.MsgDraw) => {
fetchEsHintMeta({ originMsg: "玩家抽卡时" });
matStore.hands.of(draw.player).add(draw.cards);
const deckLength = matStore.decks.of(draw.player).length;
const drawLength = draw.cards.length;
const popCards = matStore.decks
.of(draw.player)
.splice(deckLength - drawLength, drawLength);
const data = zip(popCards, draw.cards).map(([pop, hand]) => {
return { uuid: pop.uuid, id: hand };
});
matStore.hands.of(draw.player).add(data, ygopro.CardPosition.FACEUP_ATTACK);
};
import { v4 as v4uuid } from "uuid";
import { ygopro } from "@/api";
import { fetchOverlayMeta, store } from "@/stores";
type MsgMove = ygopro.StocGameMessage.MsgMove;
......@@ -6,7 +8,7 @@ import { REASON_MATERIAL } from "../../common";
const { matStore } = store;
const OVERLAY_STACK: { code: number; sequence: number }[] = [];
const OVERLAY_STACK: { uuid: string; code: number; sequence: number }[] = [];
export default (move: MsgMove) => {
const code = move.code;
......@@ -14,9 +16,9 @@ export default (move: MsgMove) => {
const to = move.to;
const reason = move.reason;
// TODO: 如果后面做动画的话,要考虑DECK的情况。
// 现在不会对DECK做判断
// FIXME: 考虑超量素材的情况
let uuid;
switch (from.location) {
case ygopro.CardZone.MZONE:
case ygopro.CardZone.SZONE: {
......@@ -26,14 +28,23 @@ export default (move: MsgMove) => {
];
target.occupant = undefined;
target.overlay_materials = [];
uuid = target.uuid;
// 需要重新分配UUID
target.uuid = v4uuid();
break;
}
case ygopro.CardZone.REMOVED:
case ygopro.CardZone.GRAVE:
case ygopro.CardZone.HAND:
case ygopro.CardZone.DECK:
case ygopro.CardZone.EXTRA: {
// 其余区域就是在list删掉这张卡
matStore.in(from.location).of(from.controler).remove(from.sequence);
const removed = matStore
.in(from.location)
.of(from.controler)
.remove(from.sequence);
uuid = removed.uuid;
break;
}
// 仅仅去除超量素材
......@@ -44,10 +55,6 @@ export default (move: MsgMove) => {
}
break;
}
default: {
console.log(`Unhandled zone type ${from.location}`);
break;
}
}
switch (to.location) {
......@@ -66,23 +73,36 @@ export default (move: MsgMove) => {
.in(to.location)
.of(to.controler)
.setOccupant(to.sequence, code, to.position);
if (uuid) {
matStore.in(to.location).of(to.controler)[to.sequence].uuid = uuid;
}
break;
}
case ygopro.CardZone.REMOVED:
case ygopro.CardZone.GRAVE:
case ygopro.CardZone.EXTRA:
case ygopro.CardZone.EXTRA: {
if (uuid) {
matStore
.in(to.location)
.of(to.controler)
.insert(uuid, code, to.sequence, to.position);
}
break;
}
case ygopro.CardZone.HAND: {
matStore
.in(to.location)
.of(to.controler)
.insert(code, to.sequence, to.position);
if (uuid) {
matStore
.in(to.location)
.of(to.controler)
.insert(uuid, code, to.sequence);
}
break;
}
case ygopro.CardZone.OVERLAY: {
if (reason == REASON_MATERIAL) {
if (reason == REASON_MATERIAL && uuid) {
// 超量素材在进行超量召唤时,若玩家未选择超量怪兽的位置,会“沉到决斗盘下面”,`reason`字段值是`REASON_MATERIAL`
// 这时候将它们放到一个栈中,待超量怪兽的Move消息到来时从栈中获取超量素材补充到状态中
OVERLAY_STACK.push({ code, sequence: to.overlay_sequence });
OVERLAY_STACK.push({ uuid, code, sequence: to.overlay_sequence });
} else {
// 其他情况下,比如“宵星的机神 丁吉尔苏”的“补充超量素材”效果,直接更新状态中
fetchOverlayMeta(to.controler, to.sequence, [code], true);
......
import { v4 as uuidv4 } from "uuid";
import { ygopro } from "@/api";
import { matStore } from "@/stores";
......@@ -40,9 +42,10 @@ function reloadDuelField(
const cards = zoneActions.map((action) => {
// FIXME: OVERLAY
return {
uuid: uuidv4(), // 因为是重连,所以这里重新申请UUID
location: {
controler: controller,
location: action.zone,
zone: action.zone,
position: action.position,
},
idleInteractivities: [],
......
import { ygopro } from "@/api";
import { matStore } from "@/stores";
import { zip } from "@/ui/Duel/utils";
type MsgShuffleHand = ygopro.StocGameMessage.MsgShuffleHand;
export default (shuffleHand: MsgShuffleHand) => {
const { hands: codes, player: controller } = shuffleHand;
const uuids = matStore.hands.of(controller).map((hand) => hand.uuid);
const data = zip(uuids, codes).map(([uuid, id]) => {
return { uuid, id };
});
matStore.hands.of(controller).length = 0;
matStore.hands.of(controller).add(codes);
matStore.hands.of(controller).add(data);
};
import { v4 as v4uuid } from "uuid";
import { ygopro } from "@/api";
import { store } from "@/stores";
......@@ -5,6 +7,10 @@ const { matStore } = store;
export default (start: ygopro.StocGameMessage.MsgStart) => {
matStore.selfType = start.playerType;
const opponent =
start.playerType == ygopro.StocGameMessage.MsgStart.PlayerType.FirstStrike
? 1
: 0;
matStore.initInfo.set(0, {
life: start.life1,
......@@ -22,6 +28,53 @@ export default (start: ygopro.StocGameMessage.MsgStart) => {
matStore.magics.of(0).forEach((x) => (x.location.controler = 0));
matStore.magics.of(1).forEach((x) => (x.location.controler = 1));
matStore.decks.of(0).add(Array(start.deckSize1).fill(0));
matStore.decks.of(1).add(Array(start.deckSize2).fill(0));
for (let i = 0; i < start.deckSize1; i++) {
matStore.decks.me.push({
uuid: v4uuid(),
occupant: {
id: 0,
data: {},
text: {},
},
location: {
controler: 1 - opponent,
zone: ygopro.CardZone.DECK,
},
counters: {},
idleInteractivities: [],
});
}
for (let i = 0; i < start.deckSize2; i++) {
matStore.decks.op.push({
uuid: v4uuid(),
occupant: {
id: 0,
data: {},
text: {},
},
location: {
controler: opponent,
zone: ygopro.CardZone.DECK,
},
counters: {},
idleInteractivities: [],
});
}
// 初始化对手的额外卡组
for (let i = 0; i < start.extraSize2; i++) {
matStore.extraDecks.op.push({
uuid: v4uuid(),
occupant: {
id: 0,
data: {},
text: {},
},
location: {
controler: opponent,
zone: ygopro.CardZone.EXTRA,
},
counters: {},
idleInteractivities: [],
});
}
};
import { cloneDeep } from "lodash-es";
import { v4 as v4uuid } from "uuid";
import { proxy } from "valtio";
import { ygopro } from "@/api";
......@@ -25,14 +26,16 @@ class CardArray extends Array<CardState> implements ArrayCardState {
public zone: ygopro.CardZone = ygopro.CardZone.MZONE;
public getController: () => number = () => 1;
private genCard = async (
uuid: string,
controller: number,
id: number,
position?: ygopro.CardPosition
) => ({
uuid,
occupant: await fetchCard(id, true),
location: {
controler: controller,
location: this.zone,
zone: this.zone,
position:
position == undefined ? ygopro.CardPosition.FACEUP_ATTACK : position,
},
......@@ -41,15 +44,25 @@ class CardArray extends Array<CardState> implements ArrayCardState {
});
// methods
remove(sequence: number) {
this.splice(sequence, 1);
return this.splice(sequence, 1)[0];
}
async insert(sequence: number, id: number, position?: ygopro.CardPosition) {
const card = await this.genCard(this.getController(), id, position);
async insert(
uuid: string,
id: number,
sequence: number,
position?: ygopro.CardPosition
) {
const card = await this.genCard(uuid, this.getController(), id, position);
this.splice(sequence, 0, card);
}
async add(ids: number[], position?: ygopro.CardPosition) {
async add(
data: { uuid: string; id: number }[],
position?: ygopro.CardPosition
) {
const cards = await Promise.all(
ids.map(async (id) => this.genCard(this.getController(), id, position))
data.map(async ({ uuid, id }) =>
this.genCard(uuid, this.getController(), id, position)
)
);
this.splice(this.length, 0, ...cards);
}
......@@ -127,12 +140,13 @@ const isMe = (controller: number): boolean => {
/**
* 生成一个指定长度的卡片数组
*/
const genBlock = (location: ygopro.CardZone, n: number) =>
const genBlock = (zone: ygopro.CardZone, n: number) =>
Array(n)
.fill(null)
.map((_) => ({
uuid: v4uuid(), // WARN: 这里其实应该不分配UUID
location: {
location,
zone,
},
idleInteractivities: [],
counters: {},
......
......@@ -14,15 +14,19 @@ export interface BothSide<T> {
*/
export interface DuelFieldState extends Array<CardState> {
/** 移除特定位置的卡片 */
remove: (sequence: number) => void;
remove: (sequence: number) => CardState;
/** 在指定位置插入卡片 */
insert: (
uuid: string,
id: number,
sequence: number,
position?: ygopro.CardPosition
) => Promise<void>;
/** 在末尾添加卡片 */
add: (ids: number[], position?: ygopro.CardPosition) => Promise<void>;
add: (
data: { uuid: string; id: number }[],
position?: ygopro.CardPosition
) => Promise<void>;
/** 设置占据这个位置的卡片信息 */
setOccupant: (
sequence: number,
......@@ -106,10 +110,11 @@ export interface InitInfo {
* 以后会更名为 BlockState
*/
export interface CardState {
uuid: string; // 一张卡的唯一标识
occupant?: CardMeta; // 占据此位置的卡牌元信息
location: {
controler?: number; // 控制这个位置的玩家,0或1
location: ygopro.CardZone; // 怪兽区/魔法陷阱区/手牌/卡组/墓地/除外区
zone: ygopro.CardZone; // 怪兽区/魔法陷阱区/手牌/卡组/墓地/除外区
position?: ygopro.CardPosition; // 卡片的姿势:攻击还是守备
}; // 位置信息,叫location的原因是为了和ygo对齐
idleInteractivities: Interactivity<number>[]; // IDLE状态下的互动信息
......@@ -118,11 +123,25 @@ export interface CardState {
zone: ygopro.CardZone;
sequence: number;
}>; // 选择位置状态下的互动信息
overlay_materials?: CardMeta[]; // 超量素材
overlay_materials?: CardMeta[]; // 超量素材, FIXME: 这里需要加上UUID
counters: { [type: number]: number }; // 指示器
reload?: boolean; // 这个字段会在收到MSG_RELOAD_FIELD的时候设置成true,在收到MSG_UPDATE_DATE的时候设置成false
}
export interface BlockState {
// 位置信息
location: {
controller: number;
zone: ygopro.CardZone;
};
// 选择位置状态下的互动信息
placeInteractivity?: Interactivity<{
controler: number;
zone: ygopro.CardZone;
sequence: number;
}>;
}
export interface Interactivity<T> {
interactType: InteractType;
// 如果`interactType`是`ACTIVATE`,这个字段是对应的效果编号
......
......@@ -2,34 +2,6 @@
// thanks!
@charset "utf-8";
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
......@@ -46,6 +18,11 @@ table {
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"),
......@@ -54,27 +31,16 @@ url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css")
"login-form",
"sign-in";
* {
box-sizing: border-box;
&:before,
&:after {
box-sizing: border-box;
}
}
body {
background-image: url(/neos-assets/background.png);
background-size:cover;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
background-attachment: fixed;
color: #606468;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font: 87.5%/1.5em 'Open Sans', sans-serif;
display: flex;
margin: 0;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
a {
......
#root {
margin: 0 auto;
text-align: center;
}
:root {
--perspective: 800px;
--scale: 1.35;
--board-rotate-z: 20deg;
--block-width: calc(80px * var(--scale));
--block-height: calc(80px * var(--scale));
--block-column-gap: calc(10px * var(--scale));
--block-row-gap: calc(10px * var(--scale));
--card-w-l-ratio: calc(5.9 / 8.6);
--deck-offset-x: calc(90px * var(--scale));
--deck-offset-y: calc(45px * var(--scale));
--deck-rotate-z: 30deg;
--opponent-deg: 180deg;
--hand-rotate: calc(var(--board-rotate-z) * -1);
--highlight-interval: 800ms;
--highlight-color-x: #393;
--highlight-color-y: #6f6;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
#controller {
position: fixed;
display: flex;
gap: 20px;
bottom: 20px;
right: 20px;
}
#life-bar-container {
position: fixed;
display: flex;
gap: 20px;
top: 20px;
right: 20px;
font-size: 1.5em;
font-weight: 500;
font-family: inherit;
flex-direction: column;
}
#life-bar {
padding: 0.8em 1.6em;
background-color: #A9A9A9;
border-radius: 8px;
border: 1px solid transparent;
color: black;
opacity: .4;
}
#camera {
perspective: var(--perspective);
transform-style: preserve-3d;
}
#board {
perspective-origin: center center;
transform: translateX(0) translateY(0) translateZ(0)
rotateX(var(--board-rotate-z));
transform-style: preserve-3d;
position: relative;
}
.card {
position: absolute;
left: 0;
top: 0;
--trans-time: 0.3s;
transition: var(--trans-time);
aspect-ratio: var(--card-w-l-ratio);
background-color: skyblue;
background: var(--card-img);
background-size: cover;
height: var(--block-height);
--x-margin-left: calc(
var(--c) * calc(var(--block-width) + var(--block-column-gap))
);
--x-padding: calc(
(var(--block-width) - var(--block-height) * var(--card-w-l-ratio)) / 2
);
--x: calc(var(--x-margin-left) + var(--x-padding));
--y: calc(var(--r) * calc(var(--block-height) + var(--block-row-gap)));
--z: calc(var(--h) * 1px);
transform: translateZ(var(--z)) rotateX(calc(var(--hand-rotate) * var(--vertical)));
translate: var(--x) var(--y);
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;
}
.card-defense {
--height: var(--block-width);
height: calc(var(--height));
--x-margin-left: calc(
var(--c) * calc(var(--block-width) + var(--block-column-gap))
);
--x-padding: calc(
(var(--block-width) - var(--height) * var(--card-w-l-ratio)) / 2
);
--y-margin-top: calc(
var(--r) * calc(var(--block-height) + var(--block-row-gap))
);
--y-padding: calc((var(--block-height) - var(--height)) / 2);
--x: calc(var(--x-margin-left) + var(--x-padding));
--y: calc(var(--y-margin-top) + var(--y-padding));
--z: calc(var(--h) * 1px);
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;
}
.card::after {
z-index: 9;
/* opacity: var(--shadow); */
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #0000003f;
filter: blur(5px);
transform: translateZ(calc(-1 * var(--z)));
}
.card:hover {
animation: glow-hover var(--highlight-interval) ease-out infinite alternate;
}
.block {
width: var(--block-width);
height: var(--block-height);
background-color: #333;
cursor: pointer;
animation: glow calc(var(--highlight-interval) * var(--highlight-on)) ease-out infinite alternate;
}
.block-extra {
margin-left: calc(var(--block-width) + var(--block-column-gap));
}
.block-row {
display: flex;
gap: var(--block-column-gap);
}
.block-left {
margin-left: calc((var(--block-width) + var(--block-column-gap)) * -1);
}
.block-right {
margin-right: calc((var(--block-width) + var(--block-column-gap)) * -1);
}
#board-bg {
display: flex;
flex-direction: column;
gap: var(--block-row-gap);
transform-style: preserve-3d;
}
@keyframes animation-fly {
0% {
transform: translateZ(1px);
}
50% {
transform: translateZ(80px);
}
100% {
transform: translateZ(1px);
}
}
@keyframes animation-fly-shadow {
0% {
transform: translateZ(0px);
/* background-color: #00000000; */
}
50% {
transform: translateZ(-80px);
/* background-color: #00000063; */
}
100% {
transform: translateZ(0px);
/* background-color: #00000000; */
}
}
.fly {
--trans-time: 0.3s;
animation: animation-fly var(--trans-time);
z-index: 10;
}
.fly::after {
--trans-time: 0.3s;
z-index: 9;
opacity: 1;
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #00000063;
filter: blur(5px);
animation: animation-fly-shadow var(--trans-time) ease-out;
}
@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;
}
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;
}
}
@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;
}
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;
}
}
import { Layout } from "antd";
import React from "react";
import { useConfig } from "@/config";
const NeosConfig = useConfig();
const layoutConfig = NeosConfig.ui.layout;
const { Header, Footer, Sider, Content } = Layout;
const headerStyle: React.CSSProperties = {
textAlign: "center",
alignContent: "center",
color: "#fff",
height: layoutConfig.header.height,
};
const contentStyle: React.CSSProperties = {
textAlign: "center",
minHeight: 120,
height: layoutConfig.content.height,
lineHeight: "120px",
paddingLeft: `${layoutConfig.sider.width}px`,
};
const siderStyle: React.CSSProperties = {
lineHeight: "120px",
position: "fixed",
overflow: "auto",
height: "100vh",
padding: "50px 20px",
color: "#fff",
};
const footerStyle: React.CSSProperties = {
textAlign: "center",
height: layoutConfig.footer.height,
color: "#fff",
paddingLeft: `${layoutConfig.sider.width}px`,
};
const NeosLayout = (props: {
sider: React.ReactNode;
header: React.ReactNode;
content: React.ReactNode;
footer: React.ReactNode;
}) => {
return (
<Layout hasSider>
<Sider style={siderStyle} width={layoutConfig.sider.width}>
{props.sider}
</Sider>
<Layout>
<Header style={headerStyle}>{props.header}</Header>
<Content style={contentStyle}>{props.content}</Content>
<Footer style={footerStyle}>{props.footer}</Footer>
</Layout>
</Layout>
);
};
export default NeosLayout;
import * as BABYLON from "@babylonjs/core";
import { Row } from "antd";
import React from "react";
import { Engine, Scene } from "react-babylonjs";
import { useConfig } from "@/config";
import NeosLayout from "./Layout";
import {
Alert,
CardListModal,
......@@ -14,39 +8,19 @@ import {
CheckCardModalV2,
CheckCardModalV3,
CheckCounterModal,
DuelTimeLine,
HintNotification,
OptionModal,
Phase,
PlayerStatus,
PositionModal,
SendBox,
SortCardModal,
YesNoModal,
} from "./Message";
import {
ExtraDeck,
Field,
Graveyard,
Hands,
Magics,
Monsters,
} from "./PlayMat";
import { BanishedZone } from "./PlayMat/BanishedZone";
import { CommonDeck } from "./PlayMat/Deck";
import Mat from "./PlayMat";
const NeosConfig = useConfig();
// Ref: https://github.com/brianzinn/react-babylonjs/issues/126
const NeosDuel = () => {
return (
<>
<Alert />
<NeosLayout
sider={<NeosSider />}
header={<PlayerStatus />}
content={<NeosCanvas />}
footer={<Phase />}
/>
<Mat />
<CardModal />
<CardListModal />
<HintNotification />
......@@ -62,65 +36,4 @@ const NeosDuel = () => {
);
};
const NeosSider = () => (
<>
<Row>
<DuelTimeLine />
</Row>
<Row>
<SendBox />
</Row>
</>
);
const NeosCanvas = () => (
<Engine antialias adaptToDeviceRatio canvasId="babylonJS">
<Scene>
<Camera />
<Light />
<Hands />
<Monsters />
<Magics />
<Field />
<CommonDeck />
<ExtraDeck />
<Graveyard />
<BanishedZone />
<Field />
<Ground />
</Scene>
</Engine>
);
const Camera = () => (
<freeCamera
name="duel-camera"
position={new BABYLON.Vector3(0, 8, -10)}
target={BABYLON.Vector3.Zero()}
></freeCamera>
);
const Light = () => (
<hemisphericLight
name="duel-light"
direction={new BABYLON.Vector3(1, 2.5, 1)}
intensity={0.7}
></hemisphericLight>
);
const Ground = () => {
const shape = NeosConfig.ui.ground;
const texture = new BABYLON.Texture(`${NeosConfig.assetsPath}/newfield.png`);
texture.hasAlpha = true;
return (
<ground name="duel-ground" width={shape.width} height={shape.height}>
<standardMaterial
name="duel-ground-mat"
diffuseTexture={texture}
></standardMaterial>
</ground>
);
};
export default NeosDuel;
......@@ -47,7 +47,11 @@ export const CardListModal = () => {
extra={
<img
alt={item.meta?.text.name}
src={`${NeosConfig.cardImgUrl}/${item.meta?.id}.jpg`}
src={
item.meta?.id
? `${NeosConfig.cardImgUrl}/${item.meta.id}.jpg`
: `${NeosConfig.assetsPath}/card_back.jpg`
}
style={{ width: CARD_WIDTH }}
/>
}
......
......@@ -8,7 +8,6 @@ export * from "./CheckCounterModal";
export * from "./DragModal";
export * from "./HintNotification";
export * from "./OptionModal";
export * from "./Phase";
export * from "./PositionModal";
export * from "./SendBox";
export * from "./SortCardModal";
......
import * as BABYLON from "@babylonjs/core";
import { useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { matStore } from "@/stores";
import { cardSlotRotation } from "../utils";
import { Depth, SingleSlot } from "./SingleSlot";
const NeosConfig = useConfig();
export const BanishedZone = () => {
const meBanishedZone = useSnapshot(matStore.banishedZones.me);
const opBanishedZone = useSnapshot(matStore.banishedZones.op);
return (
<>
<SingleSlot
// 因为singleSlot里面会有snap,所以这儿可以直接传入store
state={matStore.banishedZones.me}
position={banishedZonePosition(0, meBanishedZone.length)}
rotation={cardSlotRotation(false)}
/>
<SingleSlot
state={matStore.banishedZones.op}
position={banishedZonePosition(1, opBanishedZone.length)}
rotation={cardSlotRotation(true)}
/>
</>
);
};
const banishedZonePosition = (player: number, banishedZoneLength: number) => {
const x = player == 0 ? 3.2 : -3.2;
const y = (Depth * banishedZoneLength) / 2 + NeosConfig.ui.card.floating;
const z = player == 0 ? -0.7 : 0.7;
return new BABYLON.Vector3(x, y, z);
};
import "@/styles/mat.css";
import classnames from "classnames";
import React, { MouseEventHandler } from "react";
import { sendSelectPlaceResponse } from "@/api";
import {
CardState,
clearAllPlaceInteradtivities,
DuelFieldState,
} from "@/stores";
export const Block: React.FC<{
isExtra?: boolean;
highlight?: boolean;
onClick?: MouseEventHandler;
outerLeft?: boolean;
outerRight?: boolean;
}> = ({
isExtra = false,
highlight = false,
onClick,
outerLeft = false,
outerRight = false,
}) => (
<div
className={classnames("block", {
"block-extra": isExtra,
"block-left": outerLeft,
"block-right": outerRight,
})}
style={
{
"--highlight-on": highlight ? 1 : 0,
} as any
}
onClick={onClick}
/>
);
export function BlockRow<T extends DuelFieldState>(props: {
states: T;
leftState?: CardState;
rightState?: CardState;
}) {
return (
<div className="block-row">
{props.leftState ? (
<Block
highlight={props.leftState.placeInteractivity !== undefined}
onClick={() => {
onBlockClick(props.leftState!);
}}
outerLeft
/>
) : (
<></>
)}
{props.states.map((block, idx) => (
<Block
key={idx}
highlight={block.placeInteractivity !== undefined}
onClick={() => {
onBlockClick(block);
}}
/>
))}
{props.rightState ? (
<Block
highlight={props.rightState.placeInteractivity !== undefined}
onClick={() => {
onBlockClick(props.rightState!);
}}
outerRight
/>
) : (
<></>
)}
</div>
);
}
export const ExtraBlockRow: React.FC<{
meLeft: CardState;
meRight: CardState;
opLeft: CardState;
opRight: CardState;
}> = ({ meLeft, meRight, opLeft, opRight }) => (
<div className="block-row">
<Block
highlight={
meLeft.placeInteractivity !== undefined ||
opLeft.placeInteractivity !== undefined
}
isExtra={true}
onClick={() => {
onBlockClick(meLeft);
onBlockClick(opLeft);
}}
/>
<Block
highlight={
meRight.placeInteractivity !== undefined ||
opRight.placeInteractivity !== undefined
}
isExtra={true}
onClick={() => {
onBlockClick(meRight);
onBlockClick(opRight);
}}
/>
</div>
);
const onBlockClick = (state: CardState) => {
if (state.placeInteractivity) {
sendSelectPlaceResponse(state.placeInteractivity.response);
clearAllPlaceInteradtivities(0);
clearAllPlaceInteradtivities(1);
}
};
import "@/styles/mat.css";
import classnames from "classnames";
import React, { type CSSProperties, MouseEventHandler } from "react";
import { useConfig } from "@/config";
const NeosConfig = useConfig();
export const Card: React.FC<{
code: number;
row: number;
col: number;
hight: number;
opponent?: boolean;
defense?: boolean;
facedown?: boolean;
vertical?: boolean;
highlight?: boolean;
fly?: boolean;
transTime?: number;
onClick?: MouseEventHandler<{}>;
style?: CSSProperties;
}> = ({
code,
row,
col,
hight,
defense = false,
facedown = false,
opponent = false,
vertical = false,
highlight = false,
fly = false,
transTime = 0.3,
onClick,
style = {},
}) => (
<div
className={classnames("card", {
"card-defense": defense,
fly,
})}
style={
{
"--h": hight,
"--r": row,
"--c": col,
"--shadow": hight > 0 ? 1 : 0,
"--opponent-deg": opponent ? "180deg" : "0deg",
"--vertical": vertical ? 1 : 0,
"--trans-time": `${transTime}s`,
"--highlight-on": highlight ? 1 : 0,
"--card-img": facedown
? `url(${NeosConfig.assetsPath + "/card_back.jpg"})`
: `url(${NeosConfig.cardImgUrl + "/" + code + ".jpg"})`,
...style,
} as any
}
onClick={onClick}
></div>
);
import * as BABYLON from "@babylonjs/core";
import { useConfig } from "@/config";
import { cardSlotRotation } from "../utils";
import { Depth, SingleSlot } from "./SingleSlot";
const NeosConfig = useConfig();
import { useSnapshot } from "valtio";
import { matStore } from "@/stores";
export const CommonDeck = () => {
const meDeck = useSnapshot(matStore.decks.me);
const opDeck = useSnapshot(matStore.decks.op);
return (
<>
<SingleSlot
state={matStore.decks.me}
position={deckPosition(0, meDeck.length)}
rotation={cardSlotRotation(false)}
/>
<SingleSlot
state={matStore.decks.op}
position={deckPosition(1, opDeck.length)}
rotation={cardSlotRotation(true)}
/>
</>
);
};
const deckPosition = (player: number, deckLength: number) => {
const x = player == 0 ? 3.2 : -3.2;
const y = (Depth * deckLength) / 2 + NeosConfig.ui.card.floating;
const z = player == 0 ? -3.3 : 3.3;
return new BABYLON.Vector3(x, y, z);
};
import * as BABYLON from "@babylonjs/core";
import { useConfig } from "@/config";
import { cardSlotRotation } from "../utils";
import { Depth, SingleSlot } from "./SingleSlot";
const NeosConfig = useConfig();
import { useSnapshot } from "valtio";
import { matStore } from "@/stores";
export const ExtraDeck = () => {
const meExtraDeck = useSnapshot(matStore.extraDecks.me);
const opExtraDeck = useSnapshot(matStore.extraDecks.op);
return (
<>
<SingleSlot
state={matStore.extraDecks.me}
position={extraDeckPosition(0, meExtraDeck.length)}
rotation={cardSlotRotation(false)}
/>
<SingleSlot
state={matStore.extraDecks.op}
position={extraDeckPosition(1, opExtraDeck.length)}
rotation={cardSlotRotation(true)}
/>
</>
);
};
const extraDeckPosition = (player: number, deckLength: number) => {
const x = player == 0 ? -3.3 : 3.3;
const y = (Depth & deckLength) / 2 + NeosConfig.ui.card.floating;
const z = player == 0 ? -3.3 : 3.3;
return new BABYLON.Vector3(x, y, z);
};
import * as BABYLON from "@babylonjs/core";
import { useSnapshot } from "valtio";
import { ygopro } from "@/api";
import { useConfig } from "@/config";
import { clearAllPlaceInteradtivities, matStore } from "@/stores";
import { cardSlotRotation } from "../utils";
import { FixedSlot } from "./FixedSlot";
import { Depth } from "./SingleSlot";
const NeosConfig = useConfig();
export const Field = () => {
// 这儿的find可能是出于某种考虑,以后再深思
const meFieldState = matStore.magics.me[5];
const meField = useSnapshot(meFieldState);
const opFieldState = matStore.magics.op[5];
const opField = useSnapshot(opFieldState);
const clearPlaceInteractivitiesAction = (controller: number) =>
clearAllPlaceInteradtivities(controller, ygopro.CardZone.MZONE); // 应该是对的
return (
<>
{meField ? (
<FixedSlot
state={meFieldState}
sequence={0}
position={fieldPosition(0)}
rotation={cardSlotRotation(false)}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
) : (
<></>
)}
{opField ? (
<FixedSlot
state={opFieldState}
sequence={0}
position={fieldPosition(1)}
rotation={cardSlotRotation(true)}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
) : (
<></>
)}
</>
);
};
const fieldPosition = (player: number) => {
const x = player == 0 ? -3.3 : 3.3;
const y = Depth / 2 + NeosConfig.ui.card.floating;
const z = player == 0 ? -2.0 : 2.0;
return new BABYLON.Vector3(x, y, z);
};
import * as BABYLON from "@babylonjs/core";
import { useRef } from "react";
import { useSnapshot } from "valtio";
import { sendSelectPlaceResponse, ygopro } from "@/api";
import { useConfig } from "@/config";
import { useClick } from "@/hook";
import {
type CardState,
clearAllPlaceInteradtivities,
messageStore,
} from "@/stores";
import { interactTypeToString } from "../utils";
const NeosConfig = useConfig();
const transform = NeosConfig.ui.card.transform;
const defenceRotation = NeosConfig.ui.card.defenceRotation;
const cardDefenceRotation = new BABYLON.Vector3(
defenceRotation.x,
defenceRotation.y,
defenceRotation.z
);
export const FixedSlot = (props: {
state: CardState;
sequence: number;
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
deffenseRotation?: BABYLON.Vector3;
clearPlaceInteractivitiesAction: (controller: number) => void;
}) => {
const planeRef = useRef(null);
const snapState = useSnapshot(props.state);
const rotation =
snapState.location.position === ygopro.CardPosition.DEFENSE ||
snapState.location.position === ygopro.CardPosition.FACEUP_DEFENSE ||
snapState.location.position === ygopro.CardPosition.FACEDOWN_DEFENSE
? props.deffenseRotation || cardDefenceRotation
: props.rotation;
const edgesWidth = 2.0;
const edgesColor = BABYLON.Color4.FromColor3(BABYLON.Color3.Yellow());
const faceDown =
snapState.location.position === ygopro.CardPosition.FACEDOWN_DEFENSE ||
snapState.location.position === ygopro.CardPosition.FACEDOWN_ATTACK ||
snapState.location.position === ygopro.CardPosition.FACEDOWN;
useClick(
(_event) => {
if (snapState.placeInteractivity) {
sendSelectPlaceResponse(snapState.placeInteractivity.response);
// 其实不应该从外面传进来的...
// props.clearPlaceInteractivitiesAction(0);
// props.clearPlaceInteractivitiesAction(1);
clearAllPlaceInteradtivities(0);
clearAllPlaceInteradtivities(1);
} else if (snapState.occupant) {
// 中央弹窗展示选中卡牌信息
messageStore.cardModal.meta = snapState.occupant;
messageStore.cardModal.interactivies =
snapState.idleInteractivities.map((interactivity) => ({
desc: interactTypeToString(interactivity.interactType),
response: interactivity.response,
}));
messageStore.cardModal.counters = snapState.counters;
messageStore.cardModal.isOpen = true;
// 侧边栏展示超量素材信息
if (
snapState.overlay_materials &&
snapState.overlay_materials.length > 0
) {
messageStore.cardListModal.list =
snapState.overlay_materials?.map((overlay) => ({
meta: overlay,
interactivies: [],
})) || [];
messageStore.cardListModal.isOpen = true;
}
}
},
planeRef,
[snapState]
);
return (
<plane
name={`fixedslot-${props.sequence}`}
ref={planeRef}
width={transform.x}
height={transform.y}
position={props.position}
rotation={rotation}
enableEdgesRendering
edgesWidth={
snapState.placeInteractivity || snapState.idleInteractivities.length > 0
? edgesWidth
: 0
}
edgesColor={edgesColor}
>
<standardMaterial
name={`fixedslot-mat-${props.sequence}`}
diffuseTexture={
snapState.occupant
? faceDown
? new BABYLON.Texture(`${NeosConfig.assetsPath}/card_back.jpg`)
: new BABYLON.Texture(
`${NeosConfig.cardImgUrl}/${snapState.occupant.id}.jpg`
)
: new BABYLON.Texture(`${NeosConfig.assetsPath}/card_slot.png`)
}
alpha={snapState.occupant ? 1 : 0}
></standardMaterial>
</plane>
);
};
import * as BABYLON from "@babylonjs/core";
import { useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { matStore } from "@/stores";
import { cardSlotRotation } from "../utils";
import { Depth, SingleSlot } from "./SingleSlot";
const NeosConfig = useConfig();
export const Graveyard = () => {
const meGraveyard = useSnapshot(matStore.graveyards.me);
const opGraveyard = useSnapshot(matStore.graveyards.op);
return (
<>
<SingleSlot
state={matStore.graveyards.me}
position={graveyardPosition(0, meGraveyard.length)}
rotation={cardSlotRotation(false)}
/>
<SingleSlot
state={matStore.graveyards.op}
position={graveyardPosition(1, opGraveyard.length)}
rotation={cardSlotRotation(true)}
/>
</>
);
};
const graveyardPosition = (player: number, graveyardLength: number) => {
const x = player == 0 ? 3.2 : -3.2;
const y = (Depth * graveyardLength) / 2 + NeosConfig.ui.card.floating;
const z = player == 0 ? -2.0 : 2.0;
return new BABYLON.Vector3(x, y, z);
};
import * as BABYLON from "@babylonjs/core";
import { useEffect, useRef, useState } from "react";
import { useHover } from "react-babylonjs";
import { INTERNAL_Snapshot, useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { useClick } from "@/hook";
import { type CardState, matStore, messageStore } from "@/stores";
import { animated, useSpring } from "../spring";
import { interactTypeToString, zip } from "../utils";
const NeosConfig = useConfig();
const groundShape = NeosConfig.ui.ground;
const left = -(groundShape.width / 2);
const handShape = NeosConfig.ui.card.transform;
const rotation = NeosConfig.ui.card.handRotation;
const handRotation = new BABYLON.Vector3(rotation.x, rotation.y, rotation.z);
const hoverScaling = NeosConfig.ui.card.handHoverScaling;
export const Hands = () => {
const meHandsState = matStore.hands.me;
const opHandsState = matStore.hands.op;
const meHandsSnap = useSnapshot(meHandsState);
const opHandsSnap = useSnapshot(opHandsState);
const meHandPositions = handPositons(0, meHandsSnap);
const opHandPositions = handPositons(1, opHandsSnap);
return (
<>
{zip(meHandsState, meHandPositions).map(([hand, position], idx) => {
return (
<CHand
key={idx}
state={hand}
sequence={idx}
position={position}
rotation={handRotation}
/>
);
})}
{zip(opHandsState, opHandPositions).map(([hand, position], idx) => {
return (
<CHand
key={idx}
state={hand}
sequence={idx}
position={position}
rotation={handRotation}
back={true}
/>
);
})}
</>
);
};
const CHand = (props: {
state: CardState;
sequence: number;
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
back?: boolean;
}) => {
const hoverScale = new BABYLON.Vector3(
hoverScaling.x,
hoverScaling.y,
hoverScaling.z
);
const defaultScale = new BABYLON.Vector3(1, 1, 1);
const edgesWidth = 2.0;
const edgesColor = BABYLON.Color4.FromColor3(BABYLON.Color3.Yellow());
const planeRef = useRef(null);
const state = props.state;
const [hovered, setHovered] = useState(false);
const position = props.position;
const [spring, api] = useSpring(
() => ({
from: {
position,
},
config: {
mass: 1.0,
tension: 170,
friction: 900,
precision: 0.01,
velocity: 0.0,
clamp: true,
duration: 2000,
},
}),
[]
);
useEffect(() => {
api.start({
position,
});
}, [position]);
useHover(
() => setHovered(true),
() => setHovered(false),
planeRef
);
useClick(
() => {
if (state.occupant) {
messageStore.cardModal.meta = state.occupant;
}
messageStore.cardModal.interactivies = state.idleInteractivities.map(
(interactive) => ({
desc: interactTypeToString(interactive.interactType),
response: interactive.response,
})
);
messageStore.cardModal.isOpen = true;
},
planeRef,
[state]
);
return (
// @ts-ignore
<animated.transformNode name="">
<animated.plane
name={`hand-${props.sequence}`}
ref={planeRef}
width={handShape.x}
height={handShape.y}
scaling={hovered ? hoverScale : defaultScale}
position={spring.position}
rotation={props.rotation}
enableEdgesRendering
edgesWidth={
state.idleInteractivities.length > 0 || state.placeInteractivity
? edgesWidth
: 0
}
edgesColor={edgesColor}
>
<animated.standardMaterial
name={`hand-mat-${props.sequence}`}
diffuseTexture={
new BABYLON.Texture(
props.back
? `${NeosConfig.assetsPath}/card_back.jpg`
: `${NeosConfig.cardImgUrl}/${state.occupant?.id || 0}.jpg`
)
}
/>
</animated.plane>
</animated.transformNode>
);
};
const handPositons = (
player: number,
hands: INTERNAL_Snapshot<CardState[]>
) => {
const gap = groundShape.width / (hands.length - 1);
const x = (idx: number) =>
player == 0 ? left + gap * idx : -left - gap * idx;
const y = handShape.y / 2;
const z =
player == 0 ? -(groundShape.height / 2) - 1 : groundShape.height / 2 + 1;
return hands.map((_, idx) => new BABYLON.Vector3(x(idx), y, z));
};
import * as BABYLON from "@babylonjs/core";
import { type INTERNAL_Snapshot, useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { type CardState, matStore } from "@/stores";
import { cardSlotRotation, zip } from "../utils";
import { FixedSlot } from "./FixedSlot";
const NeosConfig = useConfig();
// TODO: use config
const left = -2.15;
const gap = 1.05;
const transform = NeosConfig.ui.card.transform;
export const Magics = () => {
const meMagicState = matStore.magics.me;
const opMagicState = matStore.magics.op;
const meMagicsSnap = useSnapshot(meMagicState);
const opMagicsSnap = useSnapshot(opMagicState);
const meMagicPositions = magicPositions(0, meMagicsSnap);
const opMagicPositions = magicPositions(1, opMagicsSnap);
const clearPlaceInteractivitiesAction = (controller: number) => {
console.warn("magic clearPlaceInteractivitiesAction");
matStore.magics.of(controller).clearPlaceInteractivity();
};
return (
<>
{zip(meMagicState, meMagicPositions)
.slice(0, 5)
.map(([magic, position], sequence) => {
return (
<FixedSlot
state={magic}
key={sequence}
sequence={sequence}
position={position}
rotation={cardSlotRotation(false)}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
);
})}
{zip(opMagicState, opMagicPositions)
.slice(0, 5)
.map(([magic, position], sequence) => {
return (
<FixedSlot
state={magic}
key={sequence}
sequence={sequence}
position={position}
rotation={cardSlotRotation(true)}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
);
})}
</>
);
};
const magicPositions = (
player: number,
magics: INTERNAL_Snapshot<CardState[]>
) => {
const x = (sequence: number) =>
player == 0 ? left + gap * sequence : -left - gap * sequence;
const y = transform.z / 2 + NeosConfig.ui.card.floating;
const z = player == 0 ? -2.6 : 2.6;
return magics.map((_, sequence) => new BABYLON.Vector3(x(sequence), y, z));
};
import "@/styles/mat.css";
import React from "react";
import { useSnapshot } from "valtio";
import { ygopro } from "@/api";
import { CardState, DuelFieldState, matStore, messageStore } from "@/stores";
import { interactTypeToString } from "../utils";
import { BlockRow, ExtraBlockRow } from "./Block";
import { Card } from "./Card";
import { Menu } from "./Menu";
import YgoZone = ygopro.CardZone;
import YgoPosition = ygopro.CardPosition;
type RenderCard = CardState & {
sequence: number;
opponent?: boolean;
};
const HIGH_SCALE = 0.1;
const DEFAULT_HIGH = 1;
export const Mat = () => {
const snap = useSnapshot(matStore);
const monsters = snap.monsters;
const magics = snap.magics;
const hands = snap.hands;
const grave = snap.graveyards;
const banished = snap.banishedZones;
const deck = snap.decks;
const extraDeck = snap.extraDecks;
const mapper =
(opponent?: boolean) => (state: CardState, sequence: number) => {
return {
sequence,
opponent,
...state,
};
};
const filter = (state: CardState) => state.occupant !== undefined;
const renderCards: RenderCard[] = monsters.me
.map(mapper())
.filter(filter)
.concat(monsters.op.map(mapper(true)).filter(filter))
.concat(magics.me.map(mapper()).filter(filter))
.concat(magics.op.map(mapper(true)).filter(filter))
.concat(hands.me.map(mapper()).filter(filter))
.concat(hands.op.map(mapper(true)).filter(filter))
.concat(grave.me.map(mapper()).filter(filter))
.concat(grave.op.map(mapper(true)).filter(filter))
.concat(banished.me.map(mapper()).filter(filter))
.concat(banished.op.map(mapper(true)).filter(filter))
.concat(deck.me.map(mapper()).filter(filter))
.concat(deck.op.map(mapper(true)).filter(filter))
.concat(extraDeck.me.map(mapper()).filter(filter))
.concat(extraDeck.op.map(mapper(true)).filter(filter));
renderCards.sort((card_a, card_b) => (card_a.uuid > card_b.uuid ? 1 : 0));
return (
<>
<Menu />
<div id="life-bar-container">
<div id="life-bar">{snap.initInfo.me.life}</div>
<div id="life-bar">{snap.initInfo.op.life}</div>
</div>
<div id="camera">
<div id="board">
<div id="board-bg">
<BlockRow states={magics.op.slice(0, 5) as DuelFieldState} />
<BlockRow
states={monsters.op.slice(0, 5) as DuelFieldState}
rightState={magics.op[5] as CardState}
/>
<ExtraBlockRow
meLeft={monsters.me[5] as CardState}
meRight={monsters.me[6] as CardState}
opLeft={monsters.op[5] as CardState}
opRight={monsters.op[6] as CardState}
/>
<BlockRow
states={monsters.me.slice(0, 5) as DuelFieldState}
leftState={magics.me[5] as CardState}
/>
<BlockRow states={magics.me.slice(0, 5) as DuelFieldState} />
</div>
{renderCards.map((card) => (
<Card
key={card.uuid}
code={card.occupant!.id}
row={cardStateToRow(card)}
col={cardStateToCol(card)}
hight={CardStateToHigh(card)}
defense={
card.location.position === YgoPosition.DEFENSE ||
card.location.position === YgoPosition.FACEDOWN_DEFENSE ||
card.location.position === YgoPosition.FACEUP_DEFENSE
}
facedown={CardStateToFaceDown(card)}
vertical={card.location.zone == YgoZone.HAND}
highlight={card.idleInteractivities.length > 0}
opponent={card.opponent}
onClick={
card.location.zone == YgoZone.SZONE ||
card.location.zone == YgoZone.MZONE ||
card.location.zone == YgoZone.HAND
? onCardClick(card)
: card.location.zone == YgoZone.DECK
? () => {}
: onFieldClick(renderCards, card.location.zone)
}
/>
))}
</div>
</div>
</>
);
};
function cardStateToRow(state: RenderCard): number {
if (state.opponent) {
switch (state.location.zone) {
case YgoZone.EXTRA:
case YgoZone.DECK:
return 0;
case YgoZone.HAND:
return -1;
case YgoZone.SZONE:
return state.sequence >= 5 ? 1 : 0;
case YgoZone.GRAVE:
return 1;
case YgoZone.MZONE:
return state.sequence >= 5 ? 2 : 1;
case YgoZone.REMOVED:
return 2;
default:
return 0;
}
} else {
switch (state.location.zone) {
case YgoZone.EXTRA:
case YgoZone.DECK:
return 4;
case YgoZone.HAND:
return 5;
case YgoZone.SZONE:
return state.sequence >= 5 ? 3 : 4;
case YgoZone.GRAVE:
return 3;
case YgoZone.MZONE:
return state.sequence >= 5 ? 2 : 3;
case YgoZone.REMOVED:
return 2;
default:
return 0;
}
}
}
function cardStateToCol(state: RenderCard): number {
if (state.opponent) {
switch (state.location.zone) {
case YgoZone.EXTRA:
return 5;
case YgoZone.HAND:
return 4 - state.sequence;
case YgoZone.SZONE:
return state.sequence >= 5 ? 5 : 4 - state.sequence;
case YgoZone.DECK:
case YgoZone.REMOVED:
case YgoZone.GRAVE:
return -1;
case YgoZone.MZONE:
return state.sequence >= 5
? state.sequence == 5
? 3
: 1
: 4 - state.sequence;
default:
return 0;
}
} else {
switch (state.location.zone) {
case YgoZone.EXTRA:
return -1;
case YgoZone.HAND:
return state.sequence;
case YgoZone.SZONE:
return state.sequence >= 5 ? -1 : state.sequence;
case YgoZone.DECK:
case YgoZone.REMOVED:
case YgoZone.GRAVE:
return 5;
case YgoZone.MZONE:
return state.sequence >= 5
? state.sequence == 5
? 1
: 3
: state.sequence;
default:
return 0;
}
}
}
function CardStateToHigh(state: RenderCard): number {
switch (state.location.zone) {
case YgoZone.EXTRA:
case YgoZone.DECK:
case YgoZone.REMOVED:
case YgoZone.GRAVE:
return state.sequence * HIGH_SCALE;
case YgoZone.SZONE:
case YgoZone.MZONE:
case YgoZone.HAND:
return DEFAULT_HIGH;
default:
return 0;
}
}
function CardStateToFaceDown(state: RenderCard): boolean {
const position = state.location.position;
return (
((position === YgoPosition.FACEDOWN ||
position === YgoPosition.FACEDOWN_ATTACK ||
position === YgoPosition.FACEDOWN_DEFENSE) &&
state.location.zone != YgoZone.HAND) ||
state.occupant!.id == 0
);
}
const onCardClick = (state: CardState) => () => {
const occupant = state.occupant;
if (occupant) {
// 中央弹窗展示选中卡牌信息
messageStore.cardModal.meta = occupant;
messageStore.cardModal.interactivies = state.idleInteractivities.map(
(interactivity) => ({
desc: interactTypeToString(interactivity.interactType),
response: interactivity.response,
})
);
messageStore.cardModal.counters = state.counters;
messageStore.cardModal.isOpen = true;
// 侧边栏展示超量素材信息
if (state.overlay_materials && state.overlay_materials.length > 0) {
messageStore.cardListModal.list =
state.overlay_materials?.map((overlay) => ({
meta: overlay,
interactivies: [],
})) || [];
messageStore.cardListModal.isOpen = true;
}
}
};
const onFieldClick = (states: Array<CardState>, zone: YgoZone) => () => {
const displayStates = states.filter((state) => state.location.zone == zone);
messageStore.cardListModal.list = displayStates.map((item) => ({
meta: item.occupant,
interactivies: item.idleInteractivities.map((interactivy) => ({
desc: interactTypeToString(interactivy.interactType),
response: interactivy.response,
})),
}));
messageStore.cardListModal.isOpen = true;
};
import "@/styles/mat.css";
import Icon from "@ant-design/icons";
import { Button, Modal, Space } from "antd";
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";
......@@ -18,7 +20,6 @@ import {
} from "@/stores";
const IconSize = "150%";
const SpaceSize = 16;
const PhaseButton = (props: {
text: string;
......@@ -40,7 +41,7 @@ const PhaseButton = (props: {
const { phase } = matStore;
export const Phase = () => {
export const Menu = () => {
const snapPhase = useSnapshot(phase);
const enableBp = snapPhase.enableBp;
const enableM2 = snapPhase.enableM2;
......@@ -81,7 +82,7 @@ export const Phase = () => {
};
return (
<Space wrap size={SpaceSize}>
<div id="controller">
<PhaseButton
icon={<Icon component={BattleSvg} style={{ fontSize: IconSize }} />}
enable={enableBp}
......@@ -130,6 +131,6 @@ export const Phase = () => {
</>
}
/>
</Space>
</div>
);
};
import "react-babylonjs";
import * as BABYLON from "@babylonjs/core";
import { type INTERNAL_Snapshot, useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { type CardState, matStore } from "@/stores";
import { cardSlotDefenceRotation, cardSlotRotation, zip } from "../utils";
import { FixedSlot } from "./FixedSlot";
const NeosConfig = useConfig();
const transform = NeosConfig.ui.card.transform;
const floating = NeosConfig.ui.card.floating;
const left = -2.15; // TODO: config
const gap = 1.05;
const clearPlaceInteractivitiesAction = (controller: number) => {
console.warn("monster clearPlaceInteractivitiesAction");
matStore.monsters.of(controller).clearPlaceInteractivity();
};
export const Monsters = () => {
const meMonstersStore = matStore.monsters.me;
const opMonstersStore = matStore.monsters.op;
const meMonstersSnap = useSnapshot(meMonstersStore);
const opMonstersSnap = useSnapshot(opMonstersStore);
const meMonsterPositions = monsterPositions(0, meMonstersSnap);
const opMonsterPositions = monsterPositions(1, opMonstersSnap);
return (
<>
{zip(meMonstersStore, meMonsterPositions)
.slice(0, 5)
.map(([monster, position], sequence) => (
<FixedSlot
state={monster}
key={sequence}
sequence={sequence}
position={position}
rotation={cardSlotRotation(false)}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
))}
{zip(opMonstersStore, opMonsterPositions)
.slice(0, 5)
.map(([monster, position], sequence) => (
<FixedSlot
state={monster}
key={sequence}
sequence={sequence}
position={position}
rotation={cardSlotRotation(true)}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
))}
<ExtraMonsters
meMonsters={meMonstersStore}
opMonsters={opMonstersStore}
/>
</>
);
};
// TODO: use props and redux
const ExtraMonsters = (props: {
meMonsters: CardState[];
opMonsters: CardState[];
}) => {
const meLeft = props.meMonsters[5];
const meRight = props.meMonsters[6];
const opLeft = props.opMonsters[5];
const opRight = props.opMonsters[6];
const leftPosition = new BABYLON.Vector3(-1.1, transform.z / 2 + floating, 0);
const rightPosition = new BABYLON.Vector3(1.1, transform.z / 2 + floating, 0);
const meRotation = cardSlotRotation(false);
const opRotation = cardSlotRotation(true);
return (
<>
<FixedSlot
state={meLeft}
sequence={5}
position={leftPosition}
rotation={meRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
<FixedSlot
state={meRight}
sequence={6}
position={rightPosition}
rotation={meRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
<FixedSlot
state={opLeft}
sequence={5}
position={rightPosition}
rotation={opRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
<FixedSlot
state={opRight}
sequence={6}
position={leftPosition}
rotation={opRotation}
deffenseRotation={cardSlotDefenceRotation()}
clearPlaceInteractivitiesAction={clearPlaceInteractivitiesAction}
/>
</>
);
};
const monsterPositions = (
player: number,
monsters: INTERNAL_Snapshot<CardState[]>
) => {
const x = (sequence: number) =>
player == 0 ? left + gap * sequence : -left - gap * sequence;
const y = transform.z / 2 + floating;
const z = player == 0 ? -1.35 : 1.35;
return monsters.map((_, sequence) => new BABYLON.Vector3(x(sequence), y, z));
};
import * as BABYLON from "@babylonjs/core";
import { useRef } from "react";
import { useSnapshot } from "valtio";
import { useConfig } from "@/config";
import { useClick } from "@/hook";
import { type CardState, messageStore } from "@/stores";
import { interactTypeToString } from "../utils";
const NeosConfig = useConfig();
const transform = NeosConfig.ui.card.transform;
export const Depth = 0.005;
export const SingleSlot = (props: {
state: CardState[];
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
}) => {
const snapState = useSnapshot(props.state);
const boxRef = useRef(null);
const edgeRender =
snapState.find((item) =>
item === undefined ? false : item.idleInteractivities.length > 0
) !== undefined;
const edgesWidth = 2.0;
const edgesColor = BABYLON.Color4.FromColor3(BABYLON.Color3.Yellow());
useClick(
(_event) => {
if (snapState.length != 0) {
messageStore.cardListModal.list = snapState
.filter(
(item) => item.occupant !== undefined && item.occupant.id !== 0
)
.map((item) => ({
meta: item.occupant,
interactivies: item.idleInteractivities.map((interactivy) => ({
desc: interactTypeToString(interactivy.interactType),
response: interactivy.response,
})),
}));
// dispatch(setCardListModalIsOpen(true));
messageStore.cardListModal.isOpen = true;
}
},
boxRef,
[snapState]
);
return (
<box
name="single-slot"
ref={boxRef}
scaling={
new BABYLON.Vector3(transform.x, transform.y, Depth * snapState.length)
}
position={props.position}
rotation={props.rotation}
enableEdgesRendering
edgesWidth={edgeRender ? edgesWidth : 0}
edgesColor={edgesColor}
>
<standardMaterial
name="single-slot-mat"
diffuseTexture={
new BABYLON.Texture(`${NeosConfig.assetsPath}/card_back.jpg`)
}
alpha={snapState.length == 0 ? 0 : 1}
/>
</box>
);
};
export * from "./BanishedZone";
export * from "./Deck";
export * from "./ExtraDeck";
export * from "./Field";
export * from "./FixedSlot";
export * from "./Graveyard";
export * from "./Hands";
export * from "./Magics";
export * from "./Monsters";
export * from "./SingleSlot";
import { Mat } from "./Mat";
export default Mat;
// Copyright (c) 2020 hooke
import { FluidValue } from "@react-spring/shared";
import {
AssignableKeys,
ComponentPropsWithRef,
ElementType,
} from "@react-spring/types";
import { CSSProperties, FC, ForwardRefExoticComponent } from "react";
import { Primitives } from "./primitives";
type AnimatedPrimitives = {
[P in Primitives]: AnimatedComponent<FC<JSX.IntrinsicElements[P]>>;
};
/** The type of the `animated()` function */
export type WithAnimated = {
<T extends ElementType>(wrappedComponent: T): AnimatedComponent<T>;
} & AnimatedPrimitives;
/** The type of an `animated()` component */
export type AnimatedComponent<T extends ElementType> =
ForwardRefExoticComponent<AnimatedProps<ComponentPropsWithRef<T>>>;
/** The props of an `animated()` component */
export type AnimatedProps<Props extends object> = {
[P in keyof Props]: P extends "ref" | "key"
? Props[P]
: AnimatedProp<Props[P]>;
};
// The animated prop value of a React element
type AnimatedProp<T> = [T, T] extends [infer T, infer DT]
? [DT] extends [never]
? never
: DT extends void
? undefined
: DT extends object
? [AssignableKeys<DT, CSSProperties>] extends [never]
? DT extends ReadonlyArray<any>
? AnimatedStyles<DT>
: DT
: AnimatedStyle<T>
: DT | AnimatedLeaf<T>
: never;
// An animated array of style objects
type AnimatedStyles<T extends ReadonlyArray<any>> = {
[P in keyof T]: [T[P]] extends [infer DT]
? DT extends object
? [AssignableKeys<DT, CSSProperties>] extends [never]
? DT extends ReadonlyArray<any>
? AnimatedStyles<DT>
: DT
: { [P in keyof DT]: AnimatedProp<DT[P]> }
: DT
: never;
};
// An animated object of style attributes
type AnimatedStyle<T> = [T, T] extends [infer T, infer DT]
? DT extends void
? undefined
: [DT] extends [never]
? never
: DT extends object
? { [P in keyof DT]: AnimatedStyle<DT[P]> }
: DT | AnimatedLeaf<T>
: never;
// An animated primitive (or an array of them)
type AnimatedLeaf<T> =
| Exclude<T, object | void>
| Extract<T, ReadonlyArray<number | string>> extends infer U
? [U] extends [never]
? never
: FluidValue<U | Exclude<T, object | void>>
: never;
// Copyright (c) 2020 hooke
import { Color3, Color4, Vector3 } from "@babylonjs/core";
import {
CustomPropsHandler,
ICustomPropsHandler,
PropChangeType,
PropertyUpdateProcessResult,
} from "react-babylonjs";
function parseRgbaString(rgba: string): number[] {
const arr: string[] = rgba.replace(/[^\d,]/g, "").split(",");
return arr.map((num) => parseInt(num, 10) / 255);
}
const Key = "react-babylon-spring";
export class CustomColor3StringHandler
implements ICustomPropsHandler<string, Color3>
{
get name() {
return `${Key}:Color3String`;
}
public propChangeType: string = PropChangeType.Color3;
accept(newProp: string): boolean {
return typeof newProp === "string";
}
process(
oldProp: string,
newProp: string
): PropertyUpdateProcessResult<Color3> {
if (oldProp !== newProp) {
return {
processed: true,
value: Color3.FromArray(parseRgbaString(newProp)),
};
}
return { processed: false, value: null };
}
}
export class CustomColor3ArrayHandler
implements ICustomPropsHandler<number[], Color3>
{
get name() {
return `${Key}:Color3Array`;
}
public propChangeType: string = PropChangeType.Color3;
accept(newProp: []): boolean {
return Array.isArray(newProp);
}
process(
oldProp: number[],
newProp: number[]
): PropertyUpdateProcessResult<Color3> {
if (oldProp === undefined || oldProp.length !== newProp.length) {
// console.log(`found diff length (${oldProp?.length}/${newProp?.length}) Color3Array new? ${oldProp === undefined}`)
return {
processed: true,
value: Color3.FromArray(newProp),
};
}
for (let i = 0; i < oldProp.length; i++) {
if (oldProp[i] !== newProp[i]) {
// console.log('found diff value Color3Array')
return {
processed: true,
value: Color3.FromArray(newProp),
};
}
}
return { processed: false, value: null };
}
}
export class CustomColor4StringHandler
implements ICustomPropsHandler<string, Color4>
{
get name() {
return `${Key}:Color4String`;
}
public propChangeType: string = PropChangeType.Color4;
accept(newProp: string): boolean {
return typeof newProp === "string";
}
process(
oldProp: string,
newProp: string
): PropertyUpdateProcessResult<Color4> {
if (oldProp !== newProp) {
// console.log('found diff Color4String')
return {
processed: true,
value: Color4.FromArray(parseRgbaString(newProp)),
};
}
return { processed: false, value: null };
}
}
export class CustomVector3ArrayHandler
implements ICustomPropsHandler<number[], Vector3>
{
get name() {
return `${Key}:Vector3Array`;
}
public propChangeType: string = PropChangeType.Vector3;
accept(newProp: []): boolean {
return Array.isArray(newProp);
}
process(
oldProp: number[],
newProp: number[]
): PropertyUpdateProcessResult<Vector3> {
if (oldProp === undefined || oldProp.length !== newProp.length) {
// console.log(`found diff length (${oldProp?.length}/${newProp?.length}) Color3Array new? ${oldProp === undefined}`)
return {
processed: true,
value: Vector3.FromArray(newProp),
};
}
for (let i = 0; i < oldProp.length; i++) {
if (oldProp[i] !== newProp[i]) {
return {
processed: true,
value: Vector3.FromArray(newProp),
};
}
}
return { processed: false, value: null };
}
}
CustomPropsHandler.RegisterPropsHandler(new CustomColor3StringHandler());
CustomPropsHandler.RegisterPropsHandler(new CustomColor3ArrayHandler());
CustomPropsHandler.RegisterPropsHandler(new CustomColor4StringHandler());
CustomPropsHandler.RegisterPropsHandler(new CustomVector3ArrayHandler());
// Copyright (c) 2020 hooke
import "./customProps";
import { createHost } from "@react-spring/animated";
import { Globals } from "@react-spring/core";
import { createStringInterpolator } from "@react-spring/shared";
import { applyInitialPropsToInstance } from "react-babylonjs";
import { WithAnimated } from "./animated";
import { primitives } from "./primitives";
// todo: frameLoop can use runRenderLoop
Globals.assign({
createStringInterpolator,
});
const host = createHost(primitives, {
applyAnimatedValues: applyInitialPropsToInstance,
});
export const animated = host.animated as WithAnimated;
export * from "./animated";
export * from "@react-spring/core";
// Copyright (c) 2020 hooke
import { intrinsicClassMap } from "react-babylonjs";
const elements = Object.keys(intrinsicClassMap);
/**
* https://github.com/react-spring/react-spring/blob/v9/targets/three/src/primitives.ts
*/
export type Primitives = keyof JSX.IntrinsicElements;
export const primitives = ["primitive"].concat(elements) as Primitives[];
import { Vector3 } from "@babylonjs/core";
import { useConfig } from "@/config";
const NeosConfig = useConfig();
const cardRotation = NeosConfig.ui.card.rotation;
const cardReverseRotation = NeosConfig.ui.card.reverseRotation;
const cardDefenceRotation = NeosConfig.ui.card.defenceRotation;
export function cardSlotRotation(reverse?: boolean) {
if (reverse) {
return new Vector3(
cardReverseRotation.x,
cardReverseRotation.y,
cardReverseRotation.z
);
} else {
return new Vector3(cardRotation.x, cardRotation.y, cardRotation.z);
}
}
export function cardSlotDefenceRotation() {
return new Vector3(
cardDefenceRotation.x,
cardDefenceRotation.y,
cardDefenceRotation.z
);
}
export * from "./cardSlot";
export * from "./interactTypeToString";
export * from "./zip";
......@@ -22,6 +22,7 @@ import {
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import rustInit from "rust-src";
import { v4 as v4uuid } from "uuid";
import { useSnapshot } from "valtio";
import YGOProDeck from "ygopro-deck-encode";
......@@ -144,7 +145,9 @@ const WaitRoom = () => {
const onDeckReady = async (deck: IDeck) => {
sendUpdateDeck(deck);
store.matStore.extraDecks.me.add(
deck.extra?.reverse() || [],
(deck.extra?.reverse() || []).map((id) => {
return { uuid: v4uuid(), id };
}),
ygopro.CardPosition.FACEDOWN_ATTACK
);
setChoseDeck(true);
......
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