Commit 983baaf5 authored by timel's avatar timel Committed by Chunchi Che

feat: mat bg action

parent 4af2045b
......@@ -35,11 +35,9 @@ const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<BrowserRouter>
<ConfigProvider theme={{ algorithm: theme.darkAlgorithm }} locale={zhCN}>
<Neos />
</ConfigProvider>
</BrowserRouter>
</React.StrictMode>
<BrowserRouter>
<ConfigProvider theme={{ algorithm: theme.darkAlgorithm }} locale={zhCN}>
<Neos />
</ConfigProvider>
</BrowserRouter>
);
......@@ -4,6 +4,7 @@ import {
type Interactivity,
InteractType,
matStore,
cardStore,
} from "@/stores";
import MsgSelectBattleCmd = ygopro.StocGameMessage.MsgSelectBattleCmd;
......@@ -14,6 +15,9 @@ export default (selectBattleCmd: MsgSelectBattleCmd) => {
// 先清掉之前的互动性
clearAllIdleInteractivities(player);
cardStore.inner.forEach((card) => {
card.idleInteractivities = [];
});
cmds.forEach((cmd) => {
const interactType = battleTypeToInteracType(cmd.battle_type);
......@@ -38,6 +42,11 @@ export default (selectBattleCmd: MsgSelectBattleCmd) => {
interactType,
response: data.response,
});
cardStore.at(location, player)[sequence].idleInteractivities.push({
...tmp,
interactType,
response: data.response,
});
} else {
console.warn(`Undefined InteractType`);
}
......
......@@ -4,6 +4,7 @@ import {
type Interactivity,
InteractType,
matStore,
cardStore,
} from "@/stores";
import MsgSelectIdleCmd = ygopro.StocGameMessage.MsgSelectIdleCmd;
......@@ -14,6 +15,9 @@ export default (selectIdleCmd: MsgSelectIdleCmd) => {
// 先清掉之前的互动性
clearAllIdleInteractivities(player);
cardStore.inner.forEach((card) => {
card.idleInteractivities = [];
});
cmds.forEach((cmd) => {
const interactType = idleTypeToInteractType(cmd.idle_type);
......@@ -37,6 +41,11 @@ export default (selectIdleCmd: MsgSelectIdleCmd) => {
interactType,
response: data.response,
});
cardStore.at(location, player)[sequence].idleInteractivities.push({
...tmp,
interactType,
response: data.response,
});
} else {
console.warn(`Undefined InteractType`);
}
......
import { ygopro } from "@/api";
import { InteractType, matStore } from "@/stores";
import { InteractType, matStore, placeStore } from "@/stores";
type MsgSelectPlace = ygopro.StocGameMessage.MsgSelectPlace;
......@@ -9,6 +9,22 @@ export default (selectPlace: MsgSelectPlace) => {
return;
}
for (const place of selectPlace.places) {
switch (place.zone) {
case ygopro.CardZone.MZONE:
case ygopro.CardZone.SZONE:
placeStore.set(place.zone, place.controler, place.sequence, {
interactType: InteractType.PLACE_SELECTABLE,
response: {
controler: place.controler,
zone: place.zone,
sequence: place.sequence,
},
});
break;
}
}
for (const place of selectPlace.places) {
switch (place.zone) {
case ygopro.CardZone.MZONE: {
......
import { CardData, CardText, fetchCard, ygopro } from "@/api";
import { proxy } from "valtio";
import { Interactivity } from "./matStore/types";
import type { Interactivity } from "./matStore/types";
/**
* 场上某位置的状态,
......
......@@ -5,6 +5,7 @@ export * from "./messageStore";
export * from "./moraStore";
export * from "./playerStore";
export * from "./cardStore";
export * from "./placeStore";
import { proxy } from "valtio";
import { devtools } from "valtio/utils";
......@@ -16,6 +17,7 @@ import { messageStore } from "./messageStore";
import { moraStore } from "./moraStore";
import { playerStore } from "./playerStore";
import { cardStore } from "./cardStore";
import { placeStore } from "./placeStore";
export const store = proxy({
playerStore,
......@@ -25,6 +27,7 @@ export const store = proxy({
matStore, // 决斗盘
messageStore, // 决斗的信息,包括模态框
cardStore,
placeStore,
});
devtools(store, { name: "valtio store", enabled: true });
import { ygopro } from "@/api";
import { proxy } from "valtio";
import type { Interactivity } from "./matStore/types";
import { matStore } from "@/stores";
export type PlaceInteractivity =
| Interactivity<{
controler: number;
zone: ygopro.CardZone;
sequence: number;
}>
| undefined;
const { MZONE, SZONE } = ygopro.CardZone;
export const placeStore = proxy({
inner: {
[MZONE]: {
me: Array.from({ length: 7 }).map(() => undefined as PlaceInteractivity),
op: Array.from({ length: 7 }).map(() => undefined as PlaceInteractivity),
},
[SZONE]: {
me: Array.from({ length: 6 }).map(() => undefined as PlaceInteractivity),
op: Array.from({ length: 6 }).map(() => undefined as PlaceInteractivity),
},
},
set(
zone: ygopro.CardZone.MZONE | ygopro.CardZone.SZONE,
controller: number,
sequence: number,
placeInteractivity: PlaceInteractivity
) {
placeStore.inner[zone][matStore.isMe(controller) ? "me" : "op"][sequence] =
placeInteractivity;
},
clearAll() {
placeStore.inner[MZONE].me = placeStore.inner[MZONE].me.map(
() => undefined
);
placeStore.inner[MZONE].op = placeStore.inner[MZONE].op.map(
() => undefined
);
placeStore.inner[SZONE].me = placeStore.inner[SZONE].me.map(
() => undefined
);
placeStore.inner[SZONE].op = placeStore.inner[SZONE].op.map(
() => undefined
);
},
});
......@@ -9,6 +9,9 @@ section#mat {
.bg-row {
display: flex;
column-gap: var(--col-gap);
&.opponent {
flex-direction: row-reverse;
}
}
}
......@@ -23,5 +26,9 @@ section#mat {
&.szone {
height: var(--block-height-s);
}
&.highlight {
box-shadow: 0 0 0 1px #00b0ff, 0 0 13px 0px #0077ff,
0 0 11px 0 skyblue inset;
}
}
}
import { type FC } from "react";
import classnames from "classnames";
import "./index.scss";
import {
placeStore,
type PlaceInteractivity,
messageStore,
CardType,
cardStore,
} from "@/stores";
import { useSnapshot, type INTERNAL_Snapshot as Snapshot } from "valtio";
import { sendSelectPlaceResponse, ygopro } from "@/api";
import { interactTypeToString } from "../../utils";
const BgRow: FC<{ isExtra?: boolean; isSzone?: boolean }> = ({
isExtra = false,
isSzone = false,
}) => (
<div className={classnames("bg-row")}>
{Array.from({ length: isExtra ? 2 : 5 }).map((_, i) => (
const BgExtraRow: FC = () => {
return (
<div className={classnames("bg-row")}>
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className={classnames("block", "extra")}></div>
))}
</div>
);
};
const BgRow: FC<{
isSzone?: boolean;
opponent?: boolean;
snap: Snapshot<PlaceInteractivity[]>;
}> = ({ isSzone = false, opponent = false, snap }) => (
<div className={classnames("bg-row", { opponent })}>
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className={classnames("block", { extra: isExtra }, { szone: isSzone })}
className={classnames("block", {
szone: isSzone,
highlight: !!snap[i],
})}
onClick={() => onBlockClick(snap[i])}
></div>
))}
</div>
);
export const Bg: FC = () => {
const snap = useSnapshot(placeStore.inner);
return (
<div className="mat-bg">
<BgRow isSzone />
<BgRow />
<BgRow isExtra />
<BgRow />
<BgRow isSzone />
<BgRow snap={snap[ygopro.CardZone.SZONE].op} isSzone opponent />
<BgRow snap={snap[ygopro.CardZone.MZONE].op} opponent />
<BgExtraRow />
<BgRow snap={snap[ygopro.CardZone.MZONE].me} />
<BgRow snap={snap[ygopro.CardZone.SZONE].me} isSzone />
</div>
);
};
const onBlockClick = (placeInteractivity: PlaceInteractivity) => {
if (placeInteractivity) {
sendSelectPlaceResponse(placeInteractivity.response);
cardStore.inner.forEach((card) => (card.idleInteractivities = []));
placeStore.clearAll();
}
};
......@@ -12,7 +12,7 @@ section#mat {
position: relative;
height: 100%;
width: 100%;
transform: translateZ(calc(var(--z) * 1px))
transform: translateZ(calc(var(--z) * 1px + 0.1px))
rotateY(calc(var(--ry) * 1deg));
.card-cover,
.card-back {
......@@ -31,5 +31,18 @@ section#mat {
transform: translateZ(0px);
}
}
.card-shadow {
// position: absolute;
// left: 0;
// top: 0;
// width: 100%;
// height: 100%;
// background-color: #0000005e;
// filter: blur(2px);
}
}
}
.highlight {
box-shadow: 0 0 10px 2px #5db7ff;
}
import React, { useEffect, type CSSProperties, type FC } from "react";
import React, { useEffect, type CSSProperties, type FC, useState } from "react";
import { cardStore, messageStore, CardType } from "@/stores";
import "./index.scss";
import { useSnapshot } from "valtio";
......@@ -6,9 +6,10 @@ import { watch } from "valtio/utils";
import { useSpring, animated, to } from "@react-spring/web";
import { ygopro } from "@/api";
import { useConfig } from "@/config";
import { moveToDeck, moveToField, moveToHand, moveToOutside } from "./springs";
import { moveToDeck, moveToGround, moveToHand, moveToOutside } from "./springs";
import { ReportEnum } from "./springs/types";
import { interactTypeToString } from "../../utils";
import classnames from "classnames";
const NeosConfig = useConfig();
......@@ -35,7 +36,7 @@ export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
case MZONE:
case SZONE:
case OVERLAY:
moveToField({ card: state, api, report });
moveToGround({ card: state, api, report });
break;
case HAND:
moveToHand({ card: state, api, report });
......@@ -54,16 +55,28 @@ export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
reload(state.zone, false);
}, []);
const [highlight, setHighlight] = useState(false);
const [shadowOpacity, setShadowOpacity] = useState(0);
watch((get) => {
const { zone, sequence, controller, xyzMonster } = get(state);
const { zone, sequence, controller, xyzMonster, idleInteractivities } =
get(state);
reload(zone, true);
});
useEffect(() => {
setHighlight(!!snap.idleInteractivities.length);
}, [snap.idleInteractivities]);
// 在别的手卡更改时候,刷新这张手卡
eventBus.on(
ReportEnum.ReloadHand,
({ sequence, controller }: { sequence: number; controller: number }) => {
if (state.sequence !== sequence && state.controller === controller) {
if (
state.zone === HAND &&
state.sequence !== sequence &&
state.controller === controller
) {
reload(state.zone, false);
}
}
......@@ -71,7 +84,7 @@ export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
return (
<animated.div
className="mat-card"
className={classnames("mat-card", { highlight })}
style={
{
transform: to(
......@@ -82,17 +95,20 @@ export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
"--z": styles.z,
"--ry": styles.ry,
height: styles.height,
zIndex: styles.zIndex,
} as any as CSSProperties
}
onClick={() =>
[MZONE, SZONE, HAND].includes(state.zone) && onCardClick(state)
}
onClick={() => {
if ([MZONE, SZONE, HAND].includes(state.zone)) {
onCardClick(state);
}
}}
>
<div className="card-shadow" />
<div className="card-img-wrap">
<img className="card-cover" src={getCardImgUrl(snap.code)} alt="" />
<img className="card-back" src={getCardImgUrl(0, true)} alt="" />
</div>
<div className="card-shadow" />
</animated.div>
);
});
......@@ -108,7 +124,7 @@ function getCardImgUrl(code: number, back = false) {
return NeosConfig.cardImgUrl + "/" + code + ".jpg";
}
const onCardClick = (card: CardType) => () => {
const onCardClick = (card: CardType) => {
// 中央弹窗展示选中卡牌信息
messageStore.cardModal.meta = {
id: card.code,
......
export * from "./toField";
export * from "./toGround";
export * from "./toHand";
export * from "./toDeck";
export * from "./toOutside";
......@@ -48,16 +48,15 @@ export const moveToDeck = async (props: {
if (zone === EXTRA) {
x = isMe(controller) ? leftX : rightX;
}
let rz = isMe(controller) ? 180 - DECK_ROTATE_Z.value : -DECK_ROTATE_Z.value;
if (zone === EXTRA) {
rz = isMe(controller) ? DECK_ROTATE_Z.value : DECK_ROTATE_Z.value;
}
let rz = zone === EXTRA ? DECK_ROTATE_Z.value : -DECK_ROTATE_Z.value;
rz += isMe(controller) ? 0 : 180;
const z = sequence;
api.start({
x,
y,
z,
rz,
ry: isMe(controller) ? (zone === DECK ? 180 : 0) : 180,
zIndex: z,
height: DECK_CARD_HEIGHT.value,
});
......
......@@ -24,7 +24,7 @@ const {
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE, OVERLAY } =
ygopro.CardZone;
export const moveToField = async (props: {
export const moveToGround = async (props: {
card: CardType;
api: SpringApi;
report: boolean;
......@@ -40,7 +40,7 @@ export const moveToField = async (props: {
? BLOCK_HEIGHT_S.value * CARD_RATIO.value
: BLOCK_HEIGHT_M.value * CARD_RATIO.value;
const height = zone === SZONE ? BLOCK_HEIGHT_S.value : BLOCK_HEIGHT_M.value;
let height = zone === SZONE ? BLOCK_HEIGHT_S.value : BLOCK_HEIGHT_M.value;
// 首先计算 x 和 y
let x = 0,
......@@ -94,6 +94,17 @@ export const moveToField = async (props: {
y = -y;
}
// 判断是不是防御表示
const defence = [
ygopro.CardPosition.DEFENSE,
ygopro.CardPosition.FACEDOWN_DEFENSE,
ygopro.CardPosition.FACEUP_DEFENSE,
].includes(position ?? 5);
height = defence ? BLOCK_WIDTH.value : height;
let rz = isMe(controller) ? 0 : 180;
rz += defence ? 90 : 0;
// 动画
await asyncStart(api)({
x,
y,
......@@ -106,17 +117,17 @@ export const moveToField = async (props: {
].includes(position ?? 5)
? 180
: 0,
rz: isMe(controller) ? 0 : 180,
rz,
config: {
// mass: 0.5,
easing: easings.easeOutSine,
easing: easings.easeInSine,
},
});
await asyncStart(api)({
z: 0,
zIndex: overlayMaterials.length ? 3 : 1,
config: {
easing: easings.easeInSine,
easing: easings.easeOutSine,
mass: 5,
tension: 300, // 170
friction: 12, // 26
......
......@@ -70,8 +70,11 @@ export const moveToHand = async (props: {
api.start({
x: isMe(controller) ? x : -x,
y: isMe(controller) ? y : -y,
z: 0,
rz: isMe(controller) ? _rz : 180 - _rz,
ry: isMe(controller) ? 0 : 180,
height: HAND_CARD_HEIGHT.value,
zIndex: sequence,
// rx: -PLANE_ROTATE_X.value,
});
};
......@@ -34,14 +34,8 @@ export const moveToOutside = async (props: {
const { zone, sequence, controller, xyzMonster, position, overlayMaterials } =
card;
let x = 0,
y = 0;
if (zone === GRAVE) {
x = (BLOCK_WIDTH.value + COL_GAP.value) * 3;
y = BLOCK_HEIGHT_M.value + ROW_GAP.value;
} else if (zone === REMOVED) {
x = (BLOCK_WIDTH.value + COL_GAP.value) * 2;
}
let x = (BLOCK_WIDTH.value + COL_GAP.value) * 3,
y = zone === GRAVE ? BLOCK_HEIGHT_M.value + ROW_GAP.value : 0;
if (!isMe(controller)) {
x = -x;
y = -y;
......@@ -50,6 +44,7 @@ export const moveToOutside = async (props: {
x,
y,
z: 0,
height: BLOCK_HEIGHT_S.value,
rz: isMe(controller) ? 0 : 180,
});
};
......@@ -21,10 +21,8 @@ section#mat {
.mat-card-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
top: 50%;
left: 50%;
display: flex;
justify-content: center;
align-items: center;
......
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 {
fetchStrings,
sendSelectBattleCmdResponse,
sendSelectIdleCmdResponse,
sendSurrender,
ygopro,
} 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>
);
};
import PhaseType = ygopro.StocGameMessage.MsgNewPhase.PhaseType;
const { phase } = matStore;
......@@ -51,30 +29,30 @@ export const Menu = () => {
const [modalOpen, setModalOpen] = useState(false);
const response =
currentPhase === "BATTLE_START" ||
currentPhase === "BATTLE_STEP" ||
currentPhase === "DAMAGE" ||
currentPhase === "DAMAGE_GAL" ||
currentPhase === "BATTLE"
currentPhase === PhaseType.BATTLE_START ||
currentPhase === PhaseType.BATTLE_STEP ||
currentPhase === PhaseType.DAMAGE ||
currentPhase === PhaseType.DAMAGE_GAL ||
currentPhase === PhaseType.BATTLE
? 3
: 7;
const onBp = () => {
sendSelectIdleCmdResponse(6);
clearAllIdleInteractivities(0); // 为什么要clear两次?
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(1);
phase.enableBp = false;
};
const onM2 = () => {
sendSelectBattleCmdResponse(2);
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(1);
phase.enableM2 = false;
};
const onEp = () => {
sendSelectIdleCmdResponse(response);
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(0);
clearAllIdleInteractivities(1);
phase.enableEp = false;
};
const onSurrender = () => {
......@@ -83,30 +61,16 @@ export const Menu = () => {
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}
/>
<button disabled={!enableBp} onClick={onBp}>
{fetchStrings("!system", 80)}
</button>
<button disabled={!enableM2} onClick={onM2}>
进入主要阶段2
</button>
<button disabled={!enableEp} onClick={onEp}>
{fetchStrings("!system", 81)}
</button>
<button onClick={onSurrender}>{fetchStrings("!system", 1351)}</button>
<Modal
title="是否确认要投降?"
open={modalOpen}
......
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