Commit a754c2c2 authored by timel's avatar timel

Merge branch 'dev/ui' into 'main'

Dev/ui

See merge request !246
parents 53a9bdf4 0cad7ff4
......@@ -12,12 +12,15 @@ export enum Task {
const getEnd = (task: Task) => `${task}-end`;
/** 在组件之中注册方法 */
const register = (task: Task, fn: (...args: any[]) => Promise<any>) => {
const register = <T extends unknown[]>(
task: Task,
fn: (...args: T) => Promise<boolean>
) => {
eventEmitter.on(
task,
async ({ taskId, args }: { taskId: string; args: any[] }) => {
await fn(...args);
eventEmitter.emit(getEnd(task), taskId);
async ({ taskId, args }: { taskId: string; args: T }) => {
const result = await fn(...args);
if (result) eventEmitter.emit(getEnd(task), taskId);
}
);
};
......
import { ygopro } from "@/api";
import { eventbus, sleep, Task } from "@/infra";
import { cardStore, fetchEsHintMeta } from "@/stores";
import { callCardAttack } from "@/ui/Duel/PlayMat/Card";
export default async (attack: ygopro.StocGameMessage.MsgAttack) => {
fetchEsHintMeta({
......@@ -16,18 +16,16 @@ export default async (attack: ygopro.StocGameMessage.MsgAttack) => {
if (attacker) {
if (attack.direct_attack) {
await eventbus.call(Task.Attack, attacker.uuid, true);
await callCardAttack(attacker.uuid, {
directAttack: true,
});
} else {
await eventbus.call(
Task.Attack,
attacker.uuid,
false,
attack.target_location
);
await callCardAttack(attacker.uuid, {
directAttack: false,
target: attack.target_location,
});
}
} else {
console.warn(`<Attack>attacker from ${attack.attacker_location} is null`);
}
await sleep(2000);
};
import { fetchCard, ygopro } from "@/api";
import { eventbus, sleep, Task } from "@/infra";
import { eventbus, Task } from "@/infra";
import { cardStore, fetchEsHintMeta, matStore } from "@/stores";
export default async (chaining: ygopro.StocGameMessage.MsgChaining) => {
......@@ -29,10 +29,6 @@ export default async (chaining: ygopro.StocGameMessage.MsgChaining) => {
// 发动效果动画
await eventbus.call(Task.Focus, target.uuid);
console.color("blue")(`${target.meta.text.name} chaining`);
// 临时办法,这里延迟800ms
// 长期:需要实现动画序列,一个动画完成后才执行下一个动画
await sleep(800);
} else {
console.warn(`<Chaining>target from ${location} is null`);
}
......
import { fetchCard, ygopro } from "@/api";
import { eventbus, sleep, Task } from "@/infra";
import { eventbus, Task } from "@/infra";
import { cardStore } from "@/stores";
export default async (confirmCards: ygopro.StocGameMessage.MsgConfirmCards) => {
......@@ -15,9 +15,6 @@ export default async (confirmCards: ygopro.StocGameMessage.MsgConfirmCards) => {
target.meta = meta;
// 动画
await eventbus.call(Task.Focus, target.uuid);
// 临时措施,延迟一会,让动画逐个展示
// 长期:需要实现动画序列,一个动画完成后才执行下一个动画
await sleep(500);
} else {
console.warn(`card of ${card} is null`);
}
......
import { fetchCard, ygopro } from "@/api";
import { eventbus, Task } from "@/infra";
import { cardStore, fetchEsHintMeta } from "@/stores";
import { callCardMove } from "@/ui/Duel/PlayMat/Card";
export default async (draw: ygopro.StocGameMessage.MsgDraw) => {
fetchEsHintMeta({ originMsg: "玩家抽卡时" });
......@@ -27,6 +27,6 @@ export default async (draw: ygopro.StocGameMessage.MsgDraw) => {
await Promise.all(
cardStore
.at(ygopro.CardZone.HAND, draw.player)
.map((card) => eventbus.call(Task.Move, card.uuid))
.map((card) => callCardMove(card.uuid))
);
};
import { ygopro } from "@/api";
import { sleep } from "@/infra";
import { matStore } from "@/stores";
import { showWaiting } from "@/ui/Duel/Message";
......@@ -93,7 +92,6 @@ let animation: Promise<unknown> = new Promise<void>((rs) => rs());
export default async function handleGameMsg(pb: ygopro.YgoStocMsg) {
animation = animation.then(() => _handleGameMsg(pb));
// _handleGameMsg(pb);
}
async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
......@@ -143,8 +141,6 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
}
case "move": {
await onMsgMove(msg.move);
await sleep(500);
break;
}
case "select_card": {
......@@ -269,7 +265,6 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
}
case "chaining": {
await onMsgChaining(msg.chaining);
break;
}
case "chain_solved": {
......@@ -324,7 +319,6 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
}
case "confirm_cards": {
await onConfirmCards(msg.confirm_cards);
break;
}
case "become_target": {
......
import { fetchCard, ygopro } from "@/api";
import { eventbus, Task } from "@/infra";
import { cardStore, CardType } from "@/stores";
import { callCardMove } from "@/ui/Duel/PlayMat/Card";
import { REASON_MATERIAL, TYPE_TOKEN } from "../../common";
......@@ -114,7 +114,7 @@ export default async (move: MsgMove) => {
overlayMaterial.location.zone = to.zone;
overlayMaterial.location.sequence = to.sequence;
await eventbus.call(Task.Move, overlayMaterial.uuid);
await callCardMove(overlayMaterial.uuid);
} else {
console.warn(
`<Move>overlayMaterial from zone=${location.zone}, controller=${location.controller}, sequence=${location.sequence}, overlay_sequence=${location.overlay_sequence} is null`
......@@ -157,15 +157,16 @@ export default async (move: MsgMove) => {
target.location = to;
// 维护完了之后,开始动画
await eventbus.call(Task.Move, target.uuid, from.zone);
const p = callCardMove(target.uuid, { fromZone: from.zone });
// 如果from或者to是手卡,那么需要刷新除了这张卡之外,这个玩家的所有手卡
if ([from.zone, to.zone].includes(HAND)) {
await Promise.all(
cardStore
.at(HAND, target.location.controller)
.filter((c) => c.uuid !== target.uuid)
.map(async (c) => await eventbus.call(Task.Move, c.uuid))
);
const pHands = cardStore
.at(HAND, target.location.controller)
.filter((c) => c.uuid !== target.uuid)
.map(async (c) => await callCardMove(c.uuid));
await Promise.all([p, ...pHands]);
} else {
await p;
}
// 超量素材位置跟随超量怪兽移动
......@@ -180,7 +181,7 @@ export default async (move: MsgMove) => {
overlay.location.sequence = to.sequence;
overlay.location.position = to.position;
await eventbus.call(Task.Move, overlay.uuid);
await callCardMove(overlay.uuid);
}
}
};
import { ygopro } from "@/api";
import MsgPosChange = ygopro.StocGameMessage.MsgPosChange;
import { eventbus, Task } from "@/infra";
import { cardStore, fetchEsHintMeta } from "@/stores";
import { callCardMove } from "@/ui/Duel/PlayMat/Card";
export default async (posChange: MsgPosChange) => {
const { location, controller, sequence } = posChange.card_info;
......@@ -10,7 +11,7 @@ export default async (posChange: MsgPosChange) => {
target.location.position = posChange.cur_position;
// TODO: 暂时用`Move`动画,后续可以单独实现一个改变表示形式的动画
await eventbus.call(Task.Move, target.uuid);
await callCardMove(target.uuid);
} else {
console.warn(`<PosChange>target from ${posChange.card_info} is null`);
}
......
import { ygopro } from "@/api";
import { eventbus, Task } from "@/infra";
import { cardStore } from "@/stores";
import { callCardMove } from "@/ui/Duel/PlayMat/Card";
type MsgShuffleHandExtra = ygopro.StocGameMessage.MsgShuffleHandExtra;
......@@ -14,27 +14,29 @@ export default async (shuffleHandExtra: MsgShuffleHandExtra) => {
hash.get(code)?.push(sequence);
});
for (const card of cards) {
const sequences = hash.get(card.code);
if (sequences !== undefined) {
const sequence = sequences.pop();
if (sequence !== undefined) {
card.location.sequence = sequence;
hash.set(card.code, sequences);
Promise.all(
cards.map(async (card) => {
const sequences = hash.get(card.code);
if (sequences !== undefined) {
const sequence = sequences.pop();
if (sequence !== undefined) {
card.location.sequence = sequence;
hash.set(card.code, sequences);
// 触发动画
await eventbus.call(Task.Move, card.uuid);
// 触发动画
await callCardMove(card.uuid);
} else {
console.warn(
`<ShuffleHandExtra>sequence poped is none, controller=${controller}, code=${card.code}, sequence=${sequence}`
);
}
} else {
console.warn(
`<ShuffleHandExtra>sequence poped is none, controller=${controller}, code=${card.code}, sequence=${sequence}`
`<ShuffleHandExtra>target from records is null, controller=${controller}, cards=${cards.map(
(card) => card.code
)}, codes=${codes}`
);
}
} else {
console.warn(
`<ShuffleHandExtra>target from records is null, controller=${controller}, cards=${cards.map(
(card) => card.code
)}, codes=${codes}`
);
}
}
})
);
};
import { ygopro } from "@/api";
import { eventbus, Task } from "@/infra";
import { cardStore } from "@/stores";
import { callCardMove } from "@/ui/Duel/PlayMat/Card";
import MsgShuffleSetCard = ygopro.StocGameMessage.MsgShuffleSetCard;
// 后端传过来的`from_locations`的列表是切洗前场上卡的location,它们在列表里面按照切洗后的顺序排列
......@@ -18,33 +18,36 @@ export default async (shuffleSetCard: MsgShuffleSetCard) => {
}
const count = from_locations.length;
for (let i = 0; i < count; i++) {
const from = from_locations[i];
const target = cardStore.at(from.zone, from.controller, from.sequence);
if (target) {
// 设置code为0,洗切后的code会由`UpdateData`指定
target.code = 0;
target.meta.id = 0;
target.meta.text.id = 0;
} else {
console.warn(`<ShuffleSetCard>target from ${from} is null`);
}
// 处理超量
const overlay_location = overlay_locations[i];
if (overlay_location.zone > 0) {
// 如果没有超量素材,后端会全传0
for (const overlay of cardStore.findOverlay(
from.zone,
from.controller,
from.sequence
)) {
// 更新sequence
overlay.location.sequence = overlay_location.sequence;
// 渲染动画
await eventbus.call(Task.Move, overlay.uuid);
// 这里其实有个疑惑,如果超量素材也跟着洗切的话,洗切的意义好像就没有了,感觉算是个k社没想好的设计?
Promise.all(
Array.from({ length: count }).map(async (_, i) => {
const from = from_locations[i];
const target = cardStore.at(from.zone, from.controller, from.sequence);
if (target) {
// 设置code为0,洗切后的code会由`UpdateData`指定
target.code = 0;
target.meta.id = 0;
target.meta.text.id = 0;
} else {
console.warn(`<ShuffleSetCard>target from ${from} is null`);
}
}
}
// 处理超量
const overlay_location = overlay_locations[i];
if (overlay_location.zone > 0) {
// 如果没有超量素材,后端会全传0
for (const overlay of cardStore.findOverlay(
from.zone,
from.controller,
from.sequence
)) {
// 更新sequence
overlay.location.sequence = overlay_location.sequence;
// 渲染动画
await callCardMove(overlay.uuid);
// 这里其实有个疑惑,如果超量素材也跟着洗切的话,洗切的意义好像就没有了,感觉算是个k社没想好的设计?
}
}
})
);
};
import { ygopro } from "@/api";
import { eventbus, Task } from "@/infra";
import { cardStore } from "@/stores";
import { callCardMove } from "@/ui/Duel/PlayMat/Card";
import MsgSwapGraveDeck = ygopro.StocGameMessage.MsgSwapGraveDeck;
const { DECK, GRAVE } = ygopro.CardZone;
......@@ -12,11 +12,11 @@ export default async (swapGraveDeck: MsgSwapGraveDeck) => {
for (const card of deck) {
card.location.zone = GRAVE;
await eventbus.call(Task.Move, card.uuid);
await callCardMove(card.uuid);
}
for (const card of grave) {
card.location.zone = DECK;
await eventbus.call(Task.Move, card.uuid);
await callCardMove(card.uuid);
}
};
import { fetchCard, ygopro } from "@/api";
import MsgUpdateData = ygopro.StocGameMessage.MsgUpdateData;
import { eventbus, Task } from "@/infra";
import { cardStore } from "@/stores";
import { callCardMove } from "@/ui/Duel/PlayMat/Card";
import MsgUpdateData = ygopro.StocGameMessage.MsgUpdateData;
export default async (updateData: MsgUpdateData) => {
const { player: controller, zone, actions } = updateData;
if (controller !== undefined && zone !== undefined && actions !== undefined) {
......@@ -28,7 +27,7 @@ export default async (updateData: MsgUpdateData) => {
// Currently only update position
target.location.position = action.location.position;
// animation
await eventbus.call(Task.Move, target.uuid);
await callCardMove(target.uuid);
}
}
if (action?.type_ >= 0) {
......
......@@ -34,24 +34,11 @@ const defaultProps: Omit<
const localStore = proxy(defaultProps);
export const SelectActionsModal: React.FC = () => {
const {
isOpen,
isChain,
min,
max,
single,
selecteds,
selectables,
mustSelects,
cancelable,
finishable,
totalLevels,
overflow,
} = useSnapshot(localStore);
const snap = useSnapshot(localStore);
const onSubmit = (options: Snapshot<Option[]>) => {
const values = options.map((option) => option.response!);
if (isChain) {
if (localStore.isChain) {
sendSelectSingleResponse(values[0]);
} else {
sendSelectMultiResponse(values);
......@@ -72,17 +59,7 @@ export const SelectActionsModal: React.FC = () => {
return (
<SelectCardsModal
{...{
isOpen,
min,
max,
single,
selecteds,
selectables,
mustSelects,
cancelable,
finishable,
totalLevels,
overflow,
...snap,
onSubmit,
onFinish,
onCancel,
......
......@@ -67,6 +67,54 @@ section#mat {
}
}
}
// 下面应该和moveToOutside、moveToGround对应
.bg-other-blocks {
&.op {
transform: rotate(180deg);
}
position: absolute;
--height: var(--card-height-o);
--width: calc(var(--height) * var(--card-ratio));
--left: calc(
var(--col-gap) * 2 + var(--block-width) * 2.5 +
var(--block-outside-offset-x) + var(--width) / 2
);
--top: calc(
var(--row-gap) + var(--block-height-m) +
(var(--block-height-m) - var(--height)) / 2
);
.block {
position: absolute;
transform: translate(-50%, -50%);
height: var(--height);
width: var(--width);
top: var(--top);
left: var(--left);
}
.field {
left: calc(-1 * var(--left));
}
.banish {
top: calc(var(--top) - var(--row-gap) - var(--height));
}
.deck {
--left: calc(
var(--deck-offset-x) + 2 * (var(--block-width) + var(--col-gap))
);
left: var(--left);
top: calc(
var(--deck-offset-y) + 2 * var(--block-height-m) + 2 * var(--row-gap)
);
transform: translate(-50%, -50%) rotate(calc(-1 * var(--deck-rotate-z)));
height: var(--deck-card-height);
width: calc(var(--deck-card-height) * var(--card-ratio));
&.extra-deck {
left: calc(-1 * var(--left));
transform: translate(-50%, -50%) rotate(var(--deck-rotate-z));
}
}
}
}
// 被禁用的样式
......@@ -92,8 +140,19 @@ section#mat {
);
display: none;
}
.block.disabled {
.disabled-cross {
display: block;
.disabled-cross.show {
display: block;
}
section#mat {
.block.glowing {
--card-shadow-color: #13a1ff;
box-shadow: 0 0 3px 3px var(--card-shadow-color), 0 0 25px 2px #0099ff87;
background: var(--card-shadow-color);
border-radius: 2px;
.triangle {
display: none;
}
}
}
......@@ -5,12 +5,38 @@ import { type INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import { sendSelectPlaceResponse, ygopro } from "@/api";
import {
BlockState,
type BlockState,
cardStore,
isMe,
type PlaceInteractivity,
placeStore,
} from "@/stores";
const BgBlock: React.FC<
React.HTMLProps<HTMLDivElement> & {
disabled?: boolean;
highlight?: boolean;
glowing?: boolean;
}
> = ({
disabled = false,
highlight = false,
glowing = false,
className,
...rest
}) => (
<div
{...rest}
className={classnames("block", className, {
highlight,
glowing,
})}
>
{<DecoTriangles />}
{<DisabledCross disabled={disabled} />}
</div>
);
const BgExtraRow: React.FC<{
meSnap: Snapshot<BlockState[]>;
opSnap: Snapshot<BlockState[]>;
......@@ -18,60 +44,74 @@ const BgExtraRow: React.FC<{
return (
<div className={classnames("bg-row")}>
{Array.from({ length: 2 }).map((_, i) => (
<div
<BgBlock
key={i}
className={classnames("block", "extra", {
highlight: !!meSnap[i].interactivity || !!opSnap[i].interactivity,
disabled: meSnap[i].disabled || opSnap[i].disabled,
})}
className="extra"
onClick={() => {
onBlockClick(meSnap[i].interactivity);
onBlockClick(opSnap[i].interactivity);
}}
>
{<DecoTriangles />}
{<DisabledCross />}
</div>
disabled={meSnap[i].disabled || opSnap[i].disabled}
highlight={!!meSnap[i].interactivity || !!opSnap[i].interactivity}
/>
))}
</div>
);
};
const BgRow: React.FC<{
isSzone?: boolean;
szone?: boolean;
opponent?: boolean;
snap: Snapshot<BlockState[]>;
}> = ({ isSzone = false, opponent = false, snap }) => (
}> = ({ szone = false, opponent = false, snap }) => (
<div className={classnames("bg-row", { opponent })}>
{Array.from({ length: 5 }).map((_, i) => (
<div
<BgBlock
key={i}
className={classnames("block", {
szone: isSzone,
highlight: !!snap[i].interactivity,
disabled: snap[i].disabled,
})}
className={classnames({ szone })}
onClick={() => onBlockClick(snap[i].interactivity)}
>
{<DecoTriangles />}
{<DisabledCross />}
</div>
disabled={snap[i].disabled}
highlight={!!snap[i].interactivity}
/>
))}
</div>
);
const BgOtherBlocks: React.FC<{ me?: boolean }> = ({ me }) => {
useSnapshot(cardStore);
const meController = isMe(0) ? 0 : 1;
const judgeGlowing = (zone: ygopro.CardZone) =>
!!cardStore
.at(zone, meController)
.reduce((sum, c) => (sum += c.idleInteractivities.length), 0);
const glowingExtra = judgeGlowing(ygopro.CardZone.EXTRA);
const glowingGraveyard = judgeGlowing(ygopro.CardZone.GRAVE);
const glowingBanish = judgeGlowing(ygopro.CardZone.REMOVED);
return (
<div className={classnames("bg-other-blocks", { me, op: !me })}>
<BgBlock className="banish" glowing={me && glowingBanish} />
<BgBlock className="graveyard" glowing={me && glowingGraveyard} />
<BgBlock className="field" />
<BgBlock className="deck" />
<BgBlock className="deck extra-deck" glowing={me && glowingExtra} />
</div>
);
};
export const Bg: React.FC = () => {
const snap = useSnapshot(placeStore.inner);
return (
<div className="mat-bg">
<BgRow snap={snap[ygopro.CardZone.SZONE].op} isSzone opponent />
<BgRow snap={snap[ygopro.CardZone.SZONE].op} szone opponent />
<BgRow snap={snap[ygopro.CardZone.MZONE].op} opponent />
<BgExtraRow
meSnap={snap[ygopro.CardZone.MZONE].me.slice(5, 7)}
opSnap={snap[ygopro.CardZone.MZONE].op.slice(5, 7)}
/>
<BgRow snap={snap[ygopro.CardZone.MZONE].me} />
<BgRow snap={snap[ygopro.CardZone.SZONE].me} isSzone />
<BgRow snap={snap[ygopro.CardZone.SZONE].me} szone />
<BgOtherBlocks me />
<BgOtherBlocks />
</div>
);
};
......@@ -92,4 +132,6 @@ const DecoTriangles: React.FC = () => (
</>
);
const DisabledCross: React.FC = () => <div className="disabled-cross"></div>;
const DisabledCross: React.FC<{ disabled: boolean }> = ({ disabled }) => (
<div className={classnames("disabled-cross", { show: disabled })}></div>
);
......@@ -106,7 +106,7 @@ section#mat {
}
}
.mat-card.highlight .card-shadow {
.mat-card.glowing .card-shadow {
--card-shadow-color: #0099ff;
display: block !important;
background: var(--card-shadow-color) !important;
......@@ -133,3 +133,7 @@ section#mat {
}
text-align: center;
}
.card-dropdown-disabled {
display: none;
}
......@@ -26,23 +26,20 @@ import {
import { interactTypeToString } from "../../utils";
import {
attack,
type AttackOptions,
focus,
moveToDeck,
moveToGround,
moveToHand,
moveToOutside,
moveToToken,
move,
type MoveOptions,
} from "./springs";
import type { SpringApiProps } from "./springs/types";
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE } =
ygopro.CardZone;
const { HAND, GRAVE, REMOVED, EXTRA, MZONE, SZONE, TZONE } = ygopro.CardZone;
export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
const state = cardStore.inner[idx];
const snap = useSnapshot(state);
const card = cardStore.inner[idx];
const snap = useSnapshot(card);
const [styles, api] = useSpring(
const [styles, api] = useSpring<SpringApiProps>(
() =>
({
x: 0,
......@@ -57,42 +54,17 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
focusDisplay: "none",
focusOpacity: 1,
subZ: 0,
opacity: 1,
} satisfies SpringApiProps)
);
// FIXME: move不应该只根据目的地判断,还要根据先前的位置判断。例子是Token。
const move = async (toZone: ygopro.CardZone, fromZone?: ygopro.CardZone) => {
switch (toZone) {
case MZONE:
case SZONE:
await moveToGround({ card: state, api, fromZone });
break;
case HAND:
await moveToHand({ card: state, api, fromZone });
break;
case DECK:
case EXTRA:
await moveToDeck({ card: state, api, fromZone });
break;
case GRAVE:
case REMOVED:
await moveToOutside({ card: state, api, fromZone });
break;
case TZONE:
// FIXME: 这里应该实现一个衍生物消散的动画,现在暂时让它在动画在展示上回到卡组
await moveToToken({ card: state, api, fromZone });
break;
}
};
// 每张卡都需要移动到初始位置
useEffect(() => {
move(state.location.zone);
addToAnimation(() => move({ card, api }));
}, []);
const [highlight, setHighlight] = useState(false);
const [glowing, setGrowing] = useState(false);
const [classFocus, setClassFocus] = useState(false);
// const [shadowOpacity, setShadowOpacity] = useState(0); // TODO: 透明度
// >>> 动画 >>>
/** 动画序列的promise */
......@@ -103,40 +75,32 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
animationQueue.current = animationQueue.current.then(p).then(rs);
});
const register = <T extends any[]>(
task: Task,
fn: (...args: T) => Promise<unknown>
) => {
eventbus.register(task, async (uuid, ...rest: T) => {
if (uuid === card.uuid) {
await fn(...rest);
return true;
} else return false;
});
};
useEffect(() => {
eventbus.register(
Task.Move,
async (uuid: string, fromZone?: ygopro.CardZone) => {
if (uuid === state.uuid) {
await addToAnimation(() => move(state.location.zone, fromZone));
}
}
);
register(Task.Move, async (options?: MoveOptions) => {
await addToAnimation(() => move({ card, api, options }));
});
eventbus.register(Task.Focus, async (uuid: string) => {
if (uuid === state.uuid) {
await addToAnimation(async () => {
setClassFocus(true);
setTimeout(() => setClassFocus(false), 1000);
await focus({ card: state, api });
});
}
register(Task.Focus, async () => {
setClassFocus(true);
setTimeout(() => setClassFocus(false), 1000); // TODO: 这儿为啥要这么写呢
await focus({ card, api });
});
eventbus.register(
Task.Attack,
async (
uuid: string,
directAttack: boolean,
target?: ygopro.CardLocation
) => {
if (uuid === state.uuid) {
await addToAnimation(() =>
attack({ card: state, api, target, directAttack })
);
}
}
);
register(Task.Attack, async (options: AttackOptions) => {
await addToAnimation(() => attack({ card, api, options }));
});
}, []);
// <<< 动画 <<<
......@@ -144,13 +108,19 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
// >>> 效果 >>>
const idleInteractivities = snap.idleInteractivities;
useEffect(() => {
setHighlight(!!idleInteractivities.length);
setGrowing(
!!idleInteractivities.length &&
[MZONE, SZONE, HAND, TZONE].includes(card.location.zone)
);
}, [idleInteractivities]);
const [dropdownMenu, setDropdownMenu] = useState({
items: [] as DropdownItem[],
});
// 是否禁用下拉菜单
const [dropdownMenuDisabled, setDropdownMenuDisabled] = useState(false);
// 发动效果
// 1. 下拉菜单里面选择[召唤 / 特殊召唤 /.../效果发动]
// 2. 如果是非效果发动,那么直接选择哪张卡(单张卡直接选择那张)
......@@ -165,6 +135,13 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
map.get(interactType)?.push(card);
});
});
if (!map.size) {
setDropdownMenuDisabled(true);
return;
} else {
setDropdownMenuDisabled(false);
}
const actions = [...map.entries()];
const nonEffectActions = actions.filter(
([action]) => action !== InteractType.ACTIVATE
......@@ -259,7 +236,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
// 中央弹窗展示选中卡牌信息
// TODO: 同一张卡片,是否重复点击会关闭CardModal?
displayCardModal(card);
if (card.idleInteractivities.length) handleDropdownMenu([card], false);
handleDropdownMenu([card], false);
// 侧边栏展示超量素材信息
const overlayMaterials = cardStore.findOverlay(
......@@ -286,17 +263,17 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
handleDropdownMenu(cards, true);
};
if ([MZONE, SZONE, HAND].includes(state.location.zone)) {
onCardClick(state);
} else if ([EXTRA, GRAVE, REMOVED].includes(state.location.zone)) {
onFieldClick(state);
if ([MZONE, SZONE, HAND].includes(card.location.zone)) {
onCardClick(card);
} else if ([EXTRA, GRAVE, REMOVED].includes(card.location.zone)) {
onFieldClick(card);
}
};
// <<< 效果 <<<
return (
<animated.div
className={classnames("mat-card", { highlight })}
className={classnames("mat-card", { glowing })}
style={
{
transform: to(
......@@ -312,6 +289,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
"--focus-scale": styles.focusScale,
"--focus-display": styles.focusDisplay,
"--focus-opacity": styles.focusOpacity,
opacity: styles.opacity,
} as any as CSSProperties
}
onClick={onClick}
......@@ -321,10 +299,11 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
<Dropdown
menu={dropdownMenu}
placement="top"
overlayClassName="card-dropdown"
overlayClassName={classnames("card-dropdown", {
"card-dropdown-disabled": dropdownMenuDisabled,
})}
arrow
trigger={["click"]}
// disabled={!highlight} // TODO: 这里的disable要考虑到field的情况,比如额外卡组
>
<div className={classnames("card-img-wrap", { focus: classFocus })}>
<YgoCard
......@@ -375,3 +354,12 @@ const handleEffectActivation = (
};
// <<< 下拉菜单 <<<
const call =
<Options,>(task: Task) =>
(uuid: string, options?: Options extends {} ? Options : never) =>
eventbus.call(task, uuid, options);
export const callCardMove = call<MoveOptions>(Task.Move);
export const callCardFocus = call(Task.Focus);
export const callCardAttack = call<AttackOptions>(Task.Attack);
// 暂时先简单实现攻击动画,后面有时间再慢慢优化
import { easings } from "@react-spring/web";
import { ygopro } from "@/api";
import { CardType, isMe } from "@/stores";
import { isMe } from "@/stores";
import { matConfig } from "@/ui/Shared";
import { matConfig } from "../../utils";
import type { SpringApi } from "./types";
import type { AttackFunc } from "./types";
import { asyncStart } from "./utils";
const { BLOCK_WIDTH, BLOCK_HEIGHT_M, BLOCK_HEIGHT_S, COL_GAP, ROW_GAP } =
matConfig;
export const attack = async (props: {
card: CardType;
api: SpringApi;
directAttack: boolean;
target?: ygopro.CardLocation;
}) => {
const { card, api, directAttack, target } = props;
export const attack: AttackFunc = async (props) => {
const { card, api, options } = props;
const current = api.current[0].get();
let x = current.x;
let y = current.y;
let rz = current.rz;
if (directAttack) {
if (options?.directAttack) {
// 直接攻击
y = BLOCK_HEIGHT_M.value + BLOCK_HEIGHT_S.value;
y = BLOCK_HEIGHT_M + BLOCK_HEIGHT_S;
if (isMe(card.location.controller)) {
y = -y;
}
} else if (target) {
} else if (options?.target) {
// 攻击`target`
const { controller, sequence } = target;
const { controller, sequence } = options.target;
if (sequence > 4) {
// 额外怪兽区
x = (sequence > 5 ? 1 : -1) * (BLOCK_WIDTH.value + COL_GAP.value);
x = (sequence > 5 ? 1 : -1) * (BLOCK_WIDTH + COL_GAP);
y = 0;
} else {
x = (sequence - 2) * (BLOCK_WIDTH.value + COL_GAP.value);
y = BLOCK_HEIGHT_M.value + ROW_GAP.value;
x = (sequence - 2) * (BLOCK_WIDTH + COL_GAP);
y = BLOCK_HEIGHT_M + ROW_GAP;
}
if (!isMe(controller)) {
......@@ -61,7 +55,7 @@ export const attack = async (props: {
await asyncStart(api)({
y:
current.y +
(BLOCK_HEIGHT_M.value / 2) * (isMe(card.location.controller) ? 1 : -1),
(BLOCK_HEIGHT_M / 2) * (isMe(card.location.controller) ? 1 : -1),
rz,
});
// 加速前冲
......
......@@ -11,19 +11,14 @@ export const focus = async (props: { card: CardType; api: SpringApi }) => {
card.location.zone == ygopro.CardZone.HAND ||
card.location.zone == ygopro.CardZone.DECK
) {
const current = api.current[0].get();
const current = { ...api.current[0].get() };
await asyncStart(api)({
y: current.y + (matStore.isMe(card.location.controller) ? -1 : 1) * 120, // TODO: 放到config之中
ry: 0,
rz: 0,
// rz: 0,
z: current.z + 50,
});
await asyncStart(api)({
y: current.y,
ry: current.ry,
rz: current.rz,
z: current.z,
});
await asyncStart(api)(current);
} else {
await asyncStart(api)({
focusScale: 1.5,
......
export * from "./attack";
export * from "./focus";
export * from "./moveToDeck";
export * from "./moveToGround";
export * from "./moveToHand";
export * from "./moveToOutside";
export * from "./moveToToken";
export * from "./move";
export * from "./types";
export * from "./utils";
import { ygopro } from "@/api";
import { moveToDeck } from "./moveToDeck";
import { moveToGround } from "./moveToGround";
import { moveToHand } from "./moveToHand";
import { moveToOutside } from "./moveToOutside";
import { moveToToken } from "./moveToToken";
import type { MoveFunc } from "./types";
const { HAND, GRAVE, REMOVED, DECK, EXTRA, MZONE, SZONE, TZONE } =
ygopro.CardZone;
export const move: MoveFunc = async (props) => {
const { card } = props;
switch (card.location.zone) {
case MZONE:
case SZONE:
await moveToGround(props);
break;
case HAND:
await moveToHand(props);
break;
case DECK:
case EXTRA:
await moveToDeck(props);
break;
case GRAVE:
case REMOVED:
await moveToOutside(props);
break;
case TZONE:
await moveToToken(props);
break;
}
};
import { ygopro } from "@/api";
import { isMe } from "@/stores";
import { matConfig } from "@/ui/Shared";
import { matConfig } from "../../utils";
import { asyncStart, type MoveFunc } from "./utils";
import type { MoveFunc } from "./types";
import { asyncStart } from "./utils";
const {
BLOCK_WIDTH,
BLOCK_HEIGHT_M,
BLOCK_HEIGHT_S,
COL_GAP,
ROW_GAP,
DECK_OFFSET_X,
......@@ -24,21 +24,16 @@ export const moveToDeck: MoveFunc = async (props) => {
const { location } = card;
const { controller, zone, sequence } = location;
const rightX = DECK_OFFSET_X.value + 2 * (BLOCK_WIDTH.value + COL_GAP.value);
const rightX = DECK_OFFSET_X + 2 * (BLOCK_WIDTH + COL_GAP);
const leftX = -rightX;
const bottomY =
DECK_OFFSET_Y.value +
2 * BLOCK_HEIGHT_M.value +
BLOCK_HEIGHT_S.value +
2 * ROW_GAP.value -
BLOCK_HEIGHT_S.value;
const bottomY = DECK_OFFSET_Y + 2 * BLOCK_HEIGHT_M + 2 * ROW_GAP;
const topY = -bottomY;
let x = isMe(controller) ? rightX : leftX;
let y = isMe(controller) ? bottomY : topY;
if (zone === EXTRA) {
x = isMe(controller) ? leftX : rightX;
}
let rz = zone === EXTRA ? DECK_ROTATE_Z.value : -DECK_ROTATE_Z.value;
let rz = zone === EXTRA ? DECK_ROTATE_Z : -DECK_ROTATE_Z;
rz += isMe(controller) ? 0 : 180;
const z = sequence;
......@@ -49,6 +44,6 @@ export const moveToDeck: MoveFunc = async (props) => {
rz,
ry: isMe(controller) ? (zone === DECK ? 180 : 0) : 180,
zIndex: z,
height: DECK_CARD_HEIGHT.value,
height: DECK_CARD_HEIGHT,
});
};
......@@ -2,9 +2,10 @@ import { easings } from "@react-spring/web";
import { ygopro } from "@/api";
import { isMe } from "@/stores";
import { matConfig } from "@/ui/Shared";
import { matConfig } from "../../utils";
import { asyncStart, type MoveFunc } from "./utils";
import type { MoveFunc } from "./types";
import { asyncStart } from "./utils";
const {
BLOCK_WIDTH,
......@@ -13,24 +14,20 @@ const {
CARD_RATIO,
COL_GAP,
ROW_GAP,
BLOCK_OUTSIDE_OFFSET_X,
CARD_HEIGHT_O,
} = matConfig;
const { MZONE, SZONE, TZONE } = ygopro.CardZone;
export const moveToGround: MoveFunc = async (props) => {
const { card, api, fromZone } = props;
const { card, api, options } = props;
const { location } = card;
const { controller, zone, sequence, position, is_overlay } = location;
// 根据zone计算卡片的宽度
const cardWidth =
zone === SZONE
? BLOCK_HEIGHT_S.value * CARD_RATIO.value
: BLOCK_HEIGHT_M.value * CARD_RATIO.value;
let height = zone === SZONE ? BLOCK_HEIGHT_S.value : BLOCK_HEIGHT_M.value;
let height = zone === SZONE ? BLOCK_HEIGHT_S : BLOCK_HEIGHT_M;
// 首先计算 x 和 y
let x = 0,
......@@ -38,28 +35,31 @@ export const moveToGround: MoveFunc = async (props) => {
switch (zone) {
case SZONE: {
if (sequence === 5) {
height = CARD_HEIGHT_O;
// 场地魔法
x = -(
3 * (BLOCK_WIDTH.value + COL_GAP.value) -
(BLOCK_WIDTH.value - cardWidth) / 2
BLOCK_WIDTH * 2.5 +
COL_GAP * 2 +
BLOCK_OUTSIDE_OFFSET_X +
CARD_HEIGHT_O * CARD_RATIO * 0.5
);
y = BLOCK_HEIGHT_M.value + ROW_GAP.value;
y = ROW_GAP + BLOCK_HEIGHT_M + (BLOCK_HEIGHT_M - CARD_HEIGHT_O) / 2;
} else {
x = (sequence - 2) * (BLOCK_WIDTH.value + COL_GAP.value);
x = (sequence - 2) * (BLOCK_WIDTH + COL_GAP);
y =
2 * (BLOCK_HEIGHT_M.value + ROW_GAP.value) -
(BLOCK_HEIGHT_M.value - BLOCK_HEIGHT_S.value) / 2;
2 * (BLOCK_HEIGHT_M + ROW_GAP) -
(BLOCK_HEIGHT_M - BLOCK_HEIGHT_S) / 2;
}
break;
}
case MZONE: {
if (sequence > 4) {
// 额外怪兽区
x = (sequence > 5 ? 1 : -1) * (BLOCK_WIDTH.value + COL_GAP.value);
x = (sequence > 5 ? 1 : -1) * (BLOCK_WIDTH + COL_GAP);
y = 0;
} else {
x = (sequence - 2) * (BLOCK_WIDTH.value + COL_GAP.value);
y = BLOCK_HEIGHT_M.value + ROW_GAP.value;
x = (sequence - 2) * (BLOCK_WIDTH + COL_GAP);
y = BLOCK_HEIGHT_M + ROW_GAP;
}
break;
}
......@@ -76,9 +76,8 @@ export const moveToGround: MoveFunc = async (props) => {
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;
height = defence ? BLOCK_WIDTH : height;
const rz = (isMe(controller) ? 0 : 180) + (defence ? 90 : 0);
const ry = [
ygopro.CardPosition.FACEDOWN,
......@@ -89,7 +88,8 @@ export const moveToGround: MoveFunc = async (props) => {
: 0;
// 动画
if (fromZone === TZONE) {
const isToken = options?.fromZone === TZONE;
if (isToken) {
// 如果是Token,直接先移动到那个位置,然后再放大
api.set({
x,
......@@ -107,8 +107,9 @@ export const moveToGround: MoveFunc = async (props) => {
ry,
rz,
config: {
// mass: 0.5,
easing: easings.easeInOutSine,
tension: 250,
clamp: true,
easing: easings.easeOutSine,
},
});
}
......@@ -116,13 +117,12 @@ export const moveToGround: MoveFunc = async (props) => {
await asyncStart(api)({
height,
z: 0,
subZ: isToken ? 100 : 0,
zIndex: is_overlay ? 1 : 3,
config: {
easing: easings.easeInOutQuad,
mass: 5,
tension: 300, // 170
friction: 12, // 26
easing: easings.easeInQuad,
clamp: true,
},
});
if (isToken) api.set({ subZ: 0 });
};
import { ygopro } from "@/api";
import { cardStore, isMe } from "@/stores";
import { matConfig } from "@/ui/Shared";
import { matConfig } from "../../utils";
import { asyncStart, type MoveFunc } from "./utils";
import type { MoveFunc } from "./types";
import { asyncStart } from "./utils";
const {
BLOCK_HEIGHT_M,
......@@ -22,27 +23,23 @@ export const moveToHand: MoveFunc = async (props) => {
// 手卡会有很复杂的计算...
const hand_circle_center_x = 0;
const hand_circle_center_y =
1 * BLOCK_HEIGHT_M.value +
1 * BLOCK_HEIGHT_S.value +
2 * ROW_GAP.value +
(HAND_MARGIN_TOP.value +
HAND_CARD_HEIGHT.value +
HAND_CIRCLE_CENTER_OFFSET_Y.value);
const hand_card_width = CARD_RATIO.value * HAND_CARD_HEIGHT.value;
BLOCK_HEIGHT_M +
BLOCK_HEIGHT_S +
2 * ROW_GAP +
(HAND_MARGIN_TOP + HAND_CARD_HEIGHT + HAND_CIRCLE_CENTER_OFFSET_Y);
const hand_card_width = CARD_RATIO * HAND_CARD_HEIGHT;
const THETA =
2 *
Math.atan(
hand_card_width /
2 /
(HAND_CIRCLE_CENTER_OFFSET_Y.value + HAND_CARD_HEIGHT.value)
hand_card_width / 2 / (HAND_CIRCLE_CENTER_OFFSET_Y + HAND_CARD_HEIGHT)
) *
0.9;
// 接下来计算每一张手卡
const hands_length = cardStore.at(HAND, controller).length;
const angle = (sequence - (hands_length - 1) / 2) * THETA;
const r = HAND_CIRCLE_CENTER_OFFSET_Y.value + HAND_CARD_HEIGHT.value / 2;
const r = HAND_CIRCLE_CENTER_OFFSET_Y + HAND_CARD_HEIGHT / 2;
const negativeX = Math.sin(angle) * r;
const negativeY = Math.cos(angle) * r + HAND_CARD_HEIGHT.value / 2;
const negativeY = Math.cos(angle) * r + HAND_CARD_HEIGHT / 2;
const x = hand_circle_center_x + negativeX * (isMe(controller) ? 1 : -1);
const y = hand_circle_center_y - negativeY + 140; // FIXME: 常量 是手动调的 这里肯定有问题 有空来修
......@@ -54,8 +51,8 @@ export const moveToHand: MoveFunc = async (props) => {
z: sequence + 5,
rz: isMe(controller) ? _rz : 180 - _rz,
ry: isMe(controller) ? 0 : 180,
height: HAND_CARD_HEIGHT.value,
height: HAND_CARD_HEIGHT,
zIndex: sequence,
// rx: -PLANE_ROTATE_X.value,
// rx: -PLANE_ROTATE_X,
});
};
import { ygopro } from "@/api";
import { isMe } from "@/stores";
import { matConfig } from "@/ui/Shared";
import { matConfig } from "../../utils";
import { asyncStart, type MoveFunc } from "./utils";
import type { MoveFunc } from "./types";
import { asyncStart } from "./utils";
const { BLOCK_WIDTH, BLOCK_HEIGHT_M, BLOCK_HEIGHT_S, COL_GAP, ROW_GAP } =
matConfig;
const {
BLOCK_WIDTH,
BLOCK_HEIGHT_M,
COL_GAP,
ROW_GAP,
CARD_HEIGHT_O,
BLOCK_OUTSIDE_OFFSET_X,
CARD_RATIO,
} = matConfig;
const { GRAVE } = ygopro.CardZone;
const { REMOVED } = ygopro.CardZone;
export const moveToOutside: MoveFunc = async (props) => {
const { card, api } = props;
// report
const { zone, controller, position, sequence } = card.location;
let x = (BLOCK_WIDTH.value + COL_GAP.value) * 3,
y = zone === GRAVE ? BLOCK_HEIGHT_M.value + ROW_GAP.value : 0;
let x =
BLOCK_WIDTH * 2.5 +
COL_GAP * 2 +
BLOCK_OUTSIDE_OFFSET_X +
CARD_HEIGHT_O * CARD_RATIO * 0.5,
y = ROW_GAP + BLOCK_HEIGHT_M + (BLOCK_HEIGHT_M - CARD_HEIGHT_O) / 2;
if (zone === REMOVED) y -= ROW_GAP + CARD_HEIGHT_O;
if (!isMe(controller)) {
x = -x;
y = -y;
......@@ -24,11 +36,14 @@ export const moveToOutside: MoveFunc = async (props) => {
x,
y,
z: 0,
height: BLOCK_HEIGHT_S.value,
height: CARD_HEIGHT_O,
rz: isMe(controller) ? 0 : 180,
ry: [ygopro.CardPosition.FACEDOWN].includes(position) ? 180 : 0,
subZ: 100,
zIndex: sequence,
config: {
tension: 140,
},
});
api.set({ subZ: 0 });
};
import { asyncStart, type MoveFunc } from "./utils";
import type { MoveFunc } from "./types";
import { asyncStart } from "./utils";
export const moveToToken: MoveFunc = async (props) => {
const { api } = props;
await asyncStart(api)({
height: 0,
opacity: 0,
});
api.set({ opacity: 1 });
};
import { type SpringRef } from "@react-spring/web";
import type { SpringRef } from "@react-spring/web";
import type { ygopro } from "@/api";
import type { CardType } from "@/stores";
export interface SpringApiProps {
x: number;
......@@ -9,6 +12,7 @@ export interface SpringApiProps {
rz: number;
zIndex: number;
height: number;
opacity: number;
// >>> focus
focusScale: number;
focusDisplay: string;
......@@ -19,3 +23,21 @@ export interface SpringApiProps {
}
export type SpringApi = SpringRef<SpringApiProps>;
type OptionsToFunc<Options> = (props: {
card: CardType;
api: SpringApi;
options?: Options;
}) => Promise<void>;
export interface MoveOptions {
fromZone?: ygopro.CardZone;
}
export type MoveFunc = OptionsToFunc<MoveOptions>;
export type AttackOptions =
| {
directAttack: true;
}
| { directAttack: false; target: ygopro.CardLocation };
export type AttackFunc = OptionsToFunc<AttackOptions>;
import { type SpringConfig, type SpringRef } from "@react-spring/web";
import type { ygopro } from "@/api";
import { type CardType } from "@/stores";
import type { SpringApi } from "./types";
export const asyncStart = <T extends {}>(api: SpringRef<T>) => {
return (p: Partial<T> & { config?: SpringConfig }) =>
new Promise((resolve) => {
......@@ -14,9 +9,3 @@ export const asyncStart = <T extends {}>(api: SpringRef<T>) => {
});
});
};
export type MoveFunc = (props: {
card: CardType;
api: SpringApi;
fromZone?: ygopro.CardZone;
}) => Promise<void>;
......@@ -54,25 +54,3 @@
min-width: 3.25em;
}
}
.floodlight {
position: absolute;
height: 100%;
width: 40px;
background-color: #aaa;
top: 0;
right: 0;
filter: blur(30px);
transform: skewX(-20deg);
}
.floodlight-run {
animation: floodlight 4s linear infinite;
}
@keyframes floodlight {
0% {
right: -80px;
}
100% {
right: calc(100% + 80px);
}
}
......@@ -115,7 +115,6 @@ const LifeBarItem: React.FC<{
size={14}
/>
<div className="timer-text">{timeText}</div>
<div className="floodlight floodlight-run" />
</div>
)}
</div>
......
// type CSSValue = [number, string] | number;
export type CSSConfig = Record<string, { value: number; unit: UNIT }>;
/** 转为CSS变量: BOARD_ROTATE_Z -> --board-rotate-z */
export const toCssProperties = (config: CSSConfig) =>
Object.entries(config)
.map(
([k, v]) =>
[
`--${k
.split("_")
.map((s) => s.toLowerCase())
.join("-")}`,
`${v.value}${v.unit}`,
] as [string, string]
)
.reduce((acc, cur) => [...acc, cur], [] as [string, string][]);
enum UNIT {
PX = "px",
DEG = "deg",
NONE = "",
}
export const matConfig = {
PERSPECTIVE: {
value: 1500,
unit: UNIT.PX,
},
PLANE_ROTATE_X: {
value: 0,
unit: UNIT.DEG,
},
BLOCK_WIDTH: {
value: 120,
unit: UNIT.PX,
},
BLOCK_HEIGHT_M: {
value: 120,
unit: UNIT.PX,
}, // 主要怪兽区
BLOCK_HEIGHT_S: {
value: 110,
unit: UNIT.PX,
}, // 魔法陷阱区
ROW_GAP: {
value: 10,
unit: UNIT.PX,
},
COL_GAP: {
value: 10,
unit: UNIT.PX,
},
CARD_RATIO: {
value: 5.9 / 8.6,
unit: UNIT.NONE,
},
HAND_MARGIN_TOP: {
value: 0,
unit: UNIT.PX,
},
HAND_CIRCLE_CENTER_OFFSET_Y: {
value: 2000,
unit: UNIT.PX,
},
HAND_CARD_HEIGHT: {
value: 130,
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,
},
DECK_CARD_HEIGHT: {
value: 120,
unit: UNIT.PX,
},
};
toCssProperties(matConfig).forEach(([k, v]) => {
document.body.style.setProperty(k, v);
});
......@@ -38,7 +38,7 @@ export const YgoCard: React.FC<Props> = (props) => {
const NeosConfig = useConfig();
function getCardImgUrl(code: number, back = false) {
export function getCardImgUrl(code: number, back = false) {
const ASSETS_BASE =
import.meta.env.BASE_URL === "/"
? NeosConfig.assetsPath
......
// 此文件目的是在js和CSS之间共享一些变量,并且这些变量是0运行时的。
type CSSConfig = Record<string, [number, UNIT]>;
/** 转为CSS变量: BOARD_ROTATE_Z -> --board-rotate-z */
const toCssProperties = (config: CSSConfig) =>
Object.entries(config)
.map(
([k, v]) =>
[
`--${k
.split("_")
.map((s) => s.toLowerCase())
.join("-")}`,
`${v[0]}${v[1]}`,
] as [string, string]
)
.reduce((acc, cur) => [...acc, cur], [] as [string, string][]);
enum UNIT {
PX = "px",
DEG = "deg",
NONE = "",
}
const matConfigWithUnit = {
PERSPECTIVE: [1500, UNIT.PX],
PLANE_ROTATE_X: [0, UNIT.DEG],
BLOCK_WIDTH: [120, UNIT.PX],
BLOCK_HEIGHT_M: [120, UNIT.PX],
BLOCK_HEIGHT_S: [110, UNIT.PX], // 魔法陷阱区
ROW_GAP: [10, UNIT.PX],
COL_GAP: [10, UNIT.PX],
CARD_RATIO: [5.9 / 8.6, UNIT.NONE],
HAND_MARGIN_TOP: [0, UNIT.PX],
HAND_CIRCLE_CENTER_OFFSET_Y: [2000, UNIT.PX],
HAND_CARD_HEIGHT: [130, UNIT.PX],
DECK_OFFSET_X: [140, UNIT.PX],
DECK_OFFSET_Y: [80, UNIT.PX],
DECK_ROTATE_Z: [30, UNIT.DEG],
DECK_CARD_HEIGHT: [120, UNIT.PX],
CARD_HEIGHT_O: [100, UNIT.PX], // 场地魔法/墓地/除外的卡片高度
BLOCK_OUTSIDE_OFFSET_X: [15, UNIT.PX],
} satisfies CSSConfig;
export const matConfig = Object.keys(matConfigWithUnit).reduce(
(prev, key) => ({
...prev,
// @ts-ignore
[key]: matConfigWithUnit[key][0],
}),
{} as Record<keyof typeof matConfigWithUnit, number>
);
toCssProperties(matConfigWithUnit).forEach(([k, v]) => {
document.body.style.setProperty(k, v);
});
export * from "./css";
export * from "./YgoCard";
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