Commit b1fba04e authored by Chunchi Che's avatar Chunchi Che

Merge branch 'feat/ui/chain' into 'main'

添加连锁特效(第一期)

See merge request mycard/Neos!357
parents 89b0e9c9 a1f58c83
...@@ -20,7 +20,8 @@ const { MZONE, SZONE, HAND, GRAVE, REMOVED, EXTRA } = ygopro.CardZone; ...@@ -20,7 +20,8 @@ const { MZONE, SZONE, HAND, GRAVE, REMOVED, EXTRA } = ygopro.CardZone;
export interface BlockState { export interface BlockState {
interactivity?: PlaceInteractivity; // 互动性 interactivity?: PlaceInteractivity; // 互动性
disabled: boolean; // 是否被禁用 disabled: boolean; // 是否被禁用
chainIndex: number[]; // 当前位置上的连锁序号。YGOPRO和MASTER DUEL的连锁都是和位置绑定的,因此在`PlaceStore`中记录连锁状态。 chainIndex: number[] /* 当前位置上的连锁序号。
YGOPRO和MASTER DUEL的连锁都是和位置绑定的,因此在`PlaceStore`中记录连锁状态。*/;
} }
const genPLaces = (n: number): BlockState[] => const genPLaces = (n: number): BlockState[] =>
......
...@@ -20,6 +20,7 @@ import { ...@@ -20,6 +20,7 @@ import {
} from "./Message"; } from "./Message";
import { LifeBar, Mat, Menu, Underlying } from "./PlayMat"; import { LifeBar, Mat, Menu, Underlying } from "./PlayMat";
import { ChatBox } from "./PlayMat/ChatBox"; import { ChatBox } from "./PlayMat/ChatBox";
import { HandChain } from "./PlayMat/HandChain";
export const Component: React.FC = () => { export const Component: React.FC = () => {
const { stage } = useSnapshot(sideStore); const { stage } = useSnapshot(sideStore);
...@@ -59,6 +60,7 @@ export const Component: React.FC = () => { ...@@ -59,6 +60,7 @@ export const Component: React.FC = () => {
<SimpleSelectCardsModal /> <SimpleSelectCardsModal />
<EndModal /> <EndModal />
<ChatBox /> <ChatBox />
<HandChain />
</> </>
); );
}; };
......
...@@ -9,20 +9,25 @@ import { ...@@ -9,20 +9,25 @@ import {
type PlaceInteractivity, type PlaceInteractivity,
placeStore, placeStore,
} from "@/stores"; } from "@/stores";
import { BgChain, ChainProps } from "@/ui/Shared";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
const { MZONE, SZONE, EXTRA, GRAVE, REMOVED } = ygopro.CardZone;
const BgBlock: React.FC< const BgBlock: React.FC<
React.HTMLProps<HTMLDivElement> & { React.HTMLProps<HTMLDivElement> & {
disabled?: boolean; disabled?: boolean;
highlight?: boolean; highlight?: boolean;
glowing?: boolean; glowing?: boolean;
chains: ChainProps;
} }
> = ({ > = ({
disabled = false, disabled = false,
highlight = false, highlight = false,
glowing = false, glowing = false,
className, className,
chains,
...rest ...rest
}) => ( }) => (
<div <div
...@@ -34,6 +39,7 @@ const BgBlock: React.FC< ...@@ -34,6 +39,7 @@ const BgBlock: React.FC<
> >
{<DecoTriangles />} {<DecoTriangles />}
{<DisabledCross disabled={disabled} />} {<DisabledCross disabled={disabled} />}
{<BgChain {...chains} />}
</div> </div>
); );
...@@ -53,6 +59,8 @@ const BgExtraRow: React.FC<{ ...@@ -53,6 +59,8 @@ const BgExtraRow: React.FC<{
}} }}
disabled={meSnap[i].disabled || opSnap[i].disabled} disabled={meSnap[i].disabled || opSnap[i].disabled}
highlight={!!meSnap[i].interactivity || !!opSnap[i].interactivity} highlight={!!meSnap[i].interactivity || !!opSnap[i].interactivity}
/* FIXME */
chains={{ chains: meSnap[i].chainIndex.concat(opSnap[i].chainIndex) }}
/> />
))} ))}
</div> </div>
...@@ -72,6 +80,7 @@ const BgRow: React.FC<{ ...@@ -72,6 +80,7 @@ const BgRow: React.FC<{
onClick={() => onBlockClick(snap[i].interactivity)} onClick={() => onBlockClick(snap[i].interactivity)}
disabled={snap[i].disabled} disabled={snap[i].disabled}
highlight={!!snap[i].interactivity} highlight={!!snap[i].interactivity}
chains={{ chains: snap[i].chainIndex }}
/> />
))} ))}
</div> </div>
...@@ -84,27 +93,46 @@ const BgOtherBlocks: React.FC<{ op?: boolean }> = ({ op }) => { ...@@ -84,27 +93,46 @@ const BgOtherBlocks: React.FC<{ op?: boolean }> = ({ op }) => {
!!cardStore !!cardStore
.at(zone, meController) .at(zone, meController)
.reduce((sum, c) => (sum += c.idleInteractivities.length), 0); .reduce((sum, c) => (sum += c.idleInteractivities.length), 0);
const glowingExtra = judgeGlowing(ygopro.CardZone.EXTRA); const glowingExtra = judgeGlowing(EXTRA);
const glowingGraveyard = judgeGlowing(ygopro.CardZone.GRAVE); const glowingGraveyard = judgeGlowing(GRAVE);
const glowingBanish = judgeGlowing(ygopro.CardZone.REMOVED); const glowingBanish = judgeGlowing(REMOVED);
const snap = useSnapshot(placeStore.inner); const snap = useSnapshot(placeStore.inner);
const field = op const field = op ? snap[SZONE].op[5] : snap[SZONE].me[5];
? snap[ygopro.CardZone.SZONE].op[5] const grave = op ? snap[GRAVE].op : snap[GRAVE].me;
: snap[ygopro.CardZone.SZONE].me[5]; const removed = op ? snap[REMOVED].op : snap[REMOVED].me;
const extra = op ? snap[EXTRA].op : snap[EXTRA].me;
const genChains = (states: Snapshot<BlockState[]>) => {
const chains: number[] = states.flatMap((state) => state.chainIndex);
chains.sort();
return chains;
};
return ( return (
<div className={classnames(styles["other-blocks"], { [styles.op]: op })}> <div className={classnames(styles["other-blocks"], { [styles.op]: op })}>
<BgBlock className={styles.banish} glowing={!op && glowingBanish} /> <BgBlock
<BgBlock className={styles.graveyard} glowing={!op && glowingGraveyard} /> className={styles.banish}
glowing={!op && glowingBanish}
chains={{ chains: genChains(removed), banish: true, op }}
/>
<BgBlock
className={styles.graveyard}
glowing={!op && glowingGraveyard}
chains={{ chains: genChains(grave), graveyard: true, op }}
/>
<BgBlock <BgBlock
className={styles.field} className={styles.field}
onClick={() => onBlockClick(field.interactivity)} onClick={() => onBlockClick(field.interactivity)}
disabled={field.disabled} disabled={field.disabled}
highlight={!!field.interactivity} highlight={!!field.interactivity}
chains={{ chains: field.chainIndex, field: true, op }}
/> />
<BgBlock className={styles.deck} /> <BgBlock className={styles.deck} chains={{ chains: [] }} />
<BgBlock <BgBlock
className={classnames(styles.deck, styles["extra-deck"])} className={classnames(styles.deck, styles["extra-deck"])}
glowing={!op && glowingExtra} glowing={!op && glowingExtra}
chains={{ chains: genChains(extra), extra: true, op }}
/> />
</div> </div>
); );
...@@ -114,14 +142,14 @@ export const Bg: React.FC = () => { ...@@ -114,14 +142,14 @@ export const Bg: React.FC = () => {
const snap = useSnapshot(placeStore.inner); const snap = useSnapshot(placeStore.inner);
return ( return (
<div className={styles["mat-bg"]}> <div className={styles["mat-bg"]}>
<BgRow snap={snap[ygopro.CardZone.SZONE].op} szone opponent /> <BgRow snap={snap[SZONE].op} szone opponent />
<BgRow snap={snap[ygopro.CardZone.MZONE].op} opponent /> <BgRow snap={snap[MZONE].op} opponent />
<BgExtraRow <BgExtraRow
meSnap={snap[ygopro.CardZone.MZONE].me.slice(5, 7)} meSnap={snap[MZONE].me.slice(5, 7)}
opSnap={snap[ygopro.CardZone.MZONE].op.slice(5, 7)} opSnap={snap[MZONE].op.slice(5, 7)}
/> />
<BgRow snap={snap[ygopro.CardZone.MZONE].me} /> <BgRow snap={snap[MZONE].me} />
<BgRow snap={snap[ygopro.CardZone.SZONE].me} szone /> <BgRow snap={snap[SZONE].me} szone />
<BgOtherBlocks /> <BgOtherBlocks />
<BgOtherBlocks op /> <BgOtherBlocks op />
</div> </div>
......
.container {
position: fixed;
display: flex;
overflow: hidden;
z-index: 2;
.me {
position: fixed;
bottom: 1rem;
right: 50%;
}
.op {
position: fixed;
top: 1rem;
left: 50%;
}
}
import { type INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import { ygopro } from "@/api";
import { BlockState, placeStore } from "@/stores";
import { BgChain } from "@/ui/Shared";
import styles from "./index.module.scss";
const { HAND } = ygopro.CardZone;
export const HandChain: React.FC = () => {
const snap = useSnapshot(placeStore.inner);
const me = snap[HAND].me;
const op = snap[HAND].op;
const genChains = (states: Snapshot<BlockState[]>) => {
const chains: number[] = states.flatMap((state) => state.chainIndex);
chains.sort();
return chains;
};
return (
<div className={styles.container}>
<div className={styles.me}>
<BgChain chains={genChains(me)} />
</div>
<div className={styles.op}>
<BgChain chains={genChains(op)} op />
</div>
</div>
);
};
.container {
position: relative;
width: 100%;
height: 100%;
min-width: 8rem;
max-width: 10rem;
display: flex;
justify-content: center;
align-items: center;
z-index: 2;
pointer-events: none;
}
.banish,
.graveyard {
left: 100%;
}
.field,
.extra-deck {
right: 100%;
}
.op {
transform: rotate(180deg);
}
.chain {
position: relative;
width: 50%;
height: 50%;
display: flex;
img {
width: 100%;
height: 100%;
object-fit: contain;
animation: rotate 5s linear infinite;
}
.text {
position: absolute;
font-size: 2rem;
font-weight: bold;
top: 50%;
left: 50%;
-webkit-text-stroke: 1px black;
transform: translate(-50%, -50%);
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
import classnames from "classnames";
import { useConfig } from "@/config";
import styles from "./index.module.scss";
const { assetsPath } = useConfig();
export interface ChainProps {
chains: readonly number[];
banish?: boolean;
graveyard?: boolean;
extra?: boolean;
field?: boolean;
op?: boolean;
}
/* 这里有个妥协的实现:墓地,除外区,额外卡组的连锁图标会被卡片遮挡,原因不明,
* 因此这里暂时采取移动一个身位的方式进行解决。最好的解决方案应该是UI上连锁图标和
* 场地解耦。 */
export const BgChain: React.FC<ChainProps> = ({
chains,
banish,
graveyard,
extra,
field,
op,
}) => (
<div
className={classnames(styles.container, {
[styles.banish]: banish,
[styles.graveyard]: graveyard,
[styles["extra-deck"]]: extra,
[styles.field]: field,
[styles.op]: op,
})}
>
{chains.map((chain) => (
<div className={styles.chain} key={chain}>
<img src={`${assetsPath}/chain.png`} />
<div className={styles.text}>{chain}</div>
</div>
))}
</div>
);
export * from "./Background"; export * from "./Background";
export * from "./CardEffectText"; export * from "./CardEffectText";
export * from "./Chain";
export * from "./chatHook"; export * from "./chatHook";
export * from "./css"; export * from "./css";
export * from "./DeckCard"; export * from "./DeckCard";
......
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