Commit b4e4a4e8 authored by Chunchi Che's avatar Chunchi Che

Merge branch 'dev/async_rebase' into 'dev/async'

Dev/async rebase

See merge request !234
parents c78b6b62 d0b7a3c9
Pipeline #22364 failed with stages
in 10 minutes and 22 seconds
......@@ -2,7 +2,7 @@
"version":4960,
"servers":[
{
"ip":"koishi-r.momobako.com",
"ip":"koishi.momobako.com",
"port":"7211"
}
],
......
This diff is collapsed.
......@@ -35,6 +35,7 @@ export const MSG_NEW_PHASE = 41;
export const MSG_HINT = 2;
export const MSG_SELECT_IDLE_CMD = 11;
export const MSG_SELECT_PLACE = 18;
export const MSG_SELECT_DISFIELD = 24;
export const MSG_MOVE = 50;
export const MSG_SELECT_CARD = 15;
export const MSG_SELECT_TRIBUTE = 20;
......@@ -63,3 +64,5 @@ export const MSG_ANNOUNCE_CARD = 142;
export const MSG_ANNOUNCE_NUMBER = 143;
export const MSG_TOSS_COIN = 130;
export const MSG_TOSS_DICE = 131;
export const MSG_SHUFFLE_SET_CARD = 36;
export const MSG_FIELD_DISABLED = 56;
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { BufferReader } from "../../../../../../rust-src/pkg/rust_src";
import MsgFieldDisabled = ygopro.StocGameMessage.MsgFieldDisabled;
import CardZone = ygopro.CardZone;
/*
* Msg Field Disabled
* @param - TODO
*
* @usage - 区域禁用
* */
export default (data: Uint8Array) => {
const reader = new BufferReader(data);
const flag = reader.readInt32();
const actions = [];
let filter = 0x1;
for (let i = 0; i < 5; i++, filter <<= 1) {
const disabled = (flag & filter) > 0;
actions.push(
new MsgFieldDisabled.Action({
controller: 0,
zone: CardZone.MZONE,
sequence: i,
disabled,
})
);
}
filter = 0x100;
for (let i = 0; i < 8; i++, filter <<= 1) {
const disabled = (flag & filter) > 0;
actions.push(
new MsgFieldDisabled.Action({
controller: 0,
zone: CardZone.SZONE,
sequence: i,
disabled,
})
);
}
filter = 0x10000;
for (let i = 0; i < 5; i++, filter <<= 1) {
const disabled = (flag & filter) > 0;
actions.push(
new MsgFieldDisabled.Action({
controller: 1,
zone: CardZone.MZONE,
sequence: i,
disabled,
})
);
}
filter = 0x1000000;
for (let i = 0; i < 8; i++, filter <<= 1) {
const disabled = (flag & filter) > 0;
actions.push(
new MsgFieldDisabled.Action({
controller: 1,
zone: CardZone.SZONE,
sequence: i,
disabled,
})
);
}
return new MsgFieldDisabled({
actions,
});
};
......@@ -14,6 +14,7 @@ import MsgAnnounceRace from "./announceRace";
import MsgAttack from "./attack";
import MsgDamage from "./damage";
import MsgDrawAdapter from "./draw";
import MsgFieldDisabledAdapter from "./fieldDisabled";
import MsgHintAdapter from "./hint";
import MsgNewPhaseAdapter from "./newPhase";
import MsgNewTurnAdapter from "./newTurn";
......@@ -33,6 +34,7 @@ import MsgSelectPositionAdapter from "./selectPosition";
import MsgSelectSum from "./selectSum";
import MsgSelectTributeAdapter from "./selectTribute";
import MsgSelectUnselectCardAdapter from "./selectUnselectCard";
import MsgShuffleSetCard from "./shuffleSetCard";
import MsgSortCard from "./sortCard";
import MsgStartAdapter from "./start";
import MsgTossAdapter from "./toss";
......@@ -95,6 +97,7 @@ export default class GameMsgAdapter implements StocAdapter {
break;
}
case GAME_MSG.MSG_SELECT_DISFIELD:
case GAME_MSG.MSG_SELECT_PLACE: {
gameMsg.select_place = MsgSelectPlaceAdapter(gameData);
......@@ -237,6 +240,16 @@ export default class GameMsgAdapter implements StocAdapter {
break;
}
case GAME_MSG.MSG_SHUFFLE_SET_CARD: {
gameMsg.shuffle_set_card = MsgShuffleSetCard(gameData);
break;
}
case GAME_MSG.MSG_FIELD_DISABLED: {
gameMsg.field_disabled = MsgFieldDisabledAdapter(gameData);
break;
}
default: {
gameMsg.unimplemented = new ygopro.StocGameMessage.MsgUnimplemented({
command: func,
......
......@@ -235,5 +235,14 @@
"repeatedType":"CardLocation"
}
]
},
"32":{
"protoType":"shuffle_deck",
"fields":[
{
"fieldName":"player",
"fieldType":"uint8"
}
]
}
}
......@@ -35,6 +35,7 @@ const MsgConstructorMap: Map<string, Constructor> = new Map([
["lp_update", ygopro.StocGameMessage.MsgLpUpdate],
["confirm_cards", ygopro.StocGameMessage.MsgConfirmCards],
["become_target", ygopro.StocGameMessage.MsgBecomeTarget],
["shuffle_deck", ygopro.StocGameMessage.MsgShuffleDeck],
]);
export interface penetrateType {
......
import { ygopro } from "@/api/ocgcore/idl/ocgcore";
import { BufferReaderExt } from "../../bufferIO";
import { numberToCardZone } from "../../util";
import MsgShuffleSetCard = ygopro.StocGameMessage.MsgShuffleSetCard;
/*
* Msg Shuffle Set Card
* @param - TODO
*
* @usage - 盖卡切洗
* */
export default (data: Uint8Array) => {
const reader = new BufferReaderExt(data);
const zone = numberToCardZone(reader.inner.readUint8());
const count = reader.inner.readUint8();
const from_locations = [];
const overlay_locations = []; // TODO: 这个字段是否有用?
for (let i = 0; i < count; i++) {
from_locations.push(reader.readCardLocation());
}
for (let i = 0; i < count; i++) {
overlay_locations.push(reader.readCardLocation());
}
return new MsgShuffleSetCard({
zone,
from_locations,
overlay_locations,
});
};
......@@ -10,7 +10,7 @@ export default (becomeTarget: ygopro.StocGameMessage.MsgBecomeTarget) => {
);
if (target) {
console.info(`${target.meta.text.name} become target`);
// TODO: 动画
target.selected = true;
} else {
console.warn(`<BecomeTarget>target from ${location} is null`);
}
......
......@@ -15,4 +15,12 @@ export default (_chainEnd: ygopro.StocGameMessage.MsgChainEnd) => {
console.warn(`<ChainEnd>target from ${chain} is null`);
}
}
// 目前selected字段只会涉及连锁过程某些卡成为效果对象,
// 因此在连锁结束的时候把selected标记清掉。
//
// TODO: 这里每次都要全部遍历一遍,后续可以优化下
for (const card of cardStore.inner) {
card.selected = false;
}
};
import { ygopro } from "@/api";
import { placeStore } from "@/stores";
import MsgFieldDisabled = ygopro.StocGameMessage.MsgFieldDisabled;
export default (fieldDisabled: MsgFieldDisabled) => {
for (const action of fieldDisabled.actions) {
switch (action.zone) {
case ygopro.CardZone.MZONE:
case ygopro.CardZone.SZONE:
placeStore.set(action.zone, action.controller, action.sequence, {
interactivity: undefined,
disabled: action.disabled,
});
break;
default:
console.warn("<FieldDisabled>zone is not MZONE nor SZONE!");
}
}
};
......@@ -12,6 +12,7 @@ import onMsgChaining from "./chaining";
import onMsgChainSolved from "./chainSolved";
import onConfirmCards from "./confirmCards";
import onMsgDraw from "./draw";
import onMsgFieldDisabled from "./fieldDisabled";
import onMsgFilpSummoned from "./flipSummoned";
import onMsgFlipSummoning from "./flipSummoning";
import onMsgHint from "./hint";
......@@ -35,7 +36,9 @@ import onMsgSelectTribute from "./selectTribute";
import onMsgSelectUnselectCard from "./selectUnselectCard";
import onMsgSelectYesNo from "./selectYesNo";
import onMsgSet from "./set";
import onMsgShuffleDeck from "./shuffleDeck";
import onMsgShuffleHand from "./shuffleHand";
import onMsgShuffleSetCard from "./shuffleSetCard";
import onMsgSortCard from "./sortCard";
import onMsgSpSummoned from "./spSummoned";
import onMsgSpSummoning from "./spSummoning";
......@@ -186,7 +189,7 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break;
}
case "update_data": {
onMsgUpdateData(msg.update_data);
await onMsgUpdateData(msg.update_data);
break;
}
......@@ -310,6 +313,21 @@ async function _handleGameMsg(pb: ygopro.YgoStocMsg) {
break;
}
case "shuffle_set_card": {
await onMsgShuffleSetCard(msg.shuffle_set_card);
break;
}
case "field_disabled": {
onMsgFieldDisabled(msg.field_disabled);
break;
}
case "shuffle_deck": {
onMsgShuffleDeck(msg.shuffle_deck);
break;
}
case "unimplemented": {
onUnimplemented(msg.unimplemented);
......
......@@ -14,12 +14,15 @@ export default (selectPlace: MsgSelectPlace) => {
case ygopro.CardZone.MZONE:
case ygopro.CardZone.SZONE:
placeStore.set(place.zone, place.controller, place.sequence, {
interactType: InteractType.PLACE_SELECTABLE,
response: {
controller: place.controller,
zone: place.zone,
sequence: place.sequence,
interactivity: {
interactType: InteractType.PLACE_SELECTABLE,
response: {
controller: place.controller,
zone: place.zone,
sequence: place.sequence,
},
},
disabled: false,
});
break;
}
......
import { ygopro } from "@/api";
import { cardStore } from "@/stores";
export default (shuffleDeck: ygopro.StocGameMessage.MsgShuffleDeck) => {
const player = shuffleDeck.player;
for (const card of cardStore.at(ygopro.CardZone.DECK, player)) {
// 把数据抹掉就好了
card.code = 0;
card.meta = { id: 0, data: {}, text: {} };
}
};
import { ygopro } from "@/api";
import { eventbus, Task } from "@/infra";
import { cardStore } from "@/stores";
import MsgShuffleSetCard = ygopro.StocGameMessage.MsgShuffleSetCard;
// 后端传过来的`from_locations`的列表是切洗前场上卡的location,它们在列表里面按照切洗后的顺序排列
export default async (shuffleSetCard: MsgShuffleSetCard) => {
const from_locations = shuffleSetCard.from_locations;
const overlay_locations = shuffleSetCard.overlay_locations;
if (from_locations.length == 0) {
console.error("<ShuffleSetCard>from_locations is empty");
return;
}
if (from_locations.length != overlay_locations.length) {
console.error(
"<ShuffleSetCard>length of from_locations and overlay_locations not matched"
);
}
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社没想好的设计?
}
}
}
};
......@@ -62,6 +62,7 @@ export default async (start: ygopro.StocGameMessage.MsgStart) => {
text: {},
},
isToken: !((i + 1) % 3),
selected: false,
})
)
)
......
import { ygopro } from "@/api";
import { fetchCard, ygopro } from "@/api";
import MsgUpdateData = ygopro.StocGameMessage.MsgUpdateData;
import { eventbus, Task } from "@/infra";
import { cardStore } from "@/stores";
export default (updateData: MsgUpdateData) => {
export default async (updateData: MsgUpdateData) => {
const { player: controller, zone, actions } = updateData;
if (controller !== undefined && zone !== undefined && actions !== undefined) {
const field = cardStore.at(zone, controller);
actions.forEach((action) => {
for (const action of actions) {
const sequence = action.location?.sequence;
if (typeof sequence !== "undefined") {
const target = field
.filter((card) => card.location.sequence === sequence)
.at(0);
if (target) {
const meta = target.meta;
// 目前只更新以下字段
if (action?.code >= 0) {
meta.id = action.code;
meta.text.id = action.code;
const newMeta = await fetchCard(action.code);
target.code = action.code;
target.meta = newMeta;
}
const meta = target.meta;
if (action.location !== undefined) {
target.location.position = action.location.position;
if (target.location.position != action.location.position) {
// Currently only update position
target.location.position = action.location.position;
// animation
await eventbus.call(Task.Move, target.uuid);
}
}
if (action?.type_ >= 0) {
meta.data.type = action.type_;
......@@ -48,10 +56,7 @@ export default (updateData: MsgUpdateData) => {
);
console.info(field);
}
if (target?.reload) {
target.reload = false;
}
}
});
}
}
};
......@@ -20,11 +20,10 @@ export interface CardType {
sequence: number;
}>; // 选择位置状态下的互动信息
counters: { [type: number]: number }; // 指示器
reload?: boolean; // 这个字段会在收到MSG_RELOAD_FIELD的时候设置成true,在收到MSG_UPDATE_DATE的时候设置成false
isToken: boolean; // 是否是token
chainIndex?: number /*连锁的序号,如果为空表示不在连锁
TODO: 目前是妥协的设计,因为其实一张卡是可以在同一个连锁链中被连锁多次的,这里为了避免太过复杂只保存最后的连锁序号*/;
selected: boolean; // 当前卡是否被选择成为效果的对象
}
class CardStore {
......
......@@ -15,31 +15,60 @@ export type PlaceInteractivity =
const { MZONE, SZONE } = ygopro.CardZone;
export interface BlockState {
interactivity?: PlaceInteractivity; // 互动性
disabled: boolean; // 是否被禁用
}
export const placeStore = proxy({
inner: {
[MZONE]: {
me: Array.from({ length: 7 }).map(() => undefined as PlaceInteractivity),
op: Array.from({ length: 7 }).map(() => undefined as PlaceInteractivity),
me: Array.from({ length: 7 }).map(
() =>
({
interactivity: undefined,
disabled: false,
} as BlockState)
),
op: Array.from({ length: 7 }).map(
() =>
({
interactivity: undefined,
disabled: false,
} as BlockState)
),
},
[SZONE]: {
me: Array.from({ length: 6 }).map(() => undefined as PlaceInteractivity),
op: Array.from({ length: 6 }).map(() => undefined as PlaceInteractivity),
me: Array.from({ length: 6 }).map(
() =>
({
interactivity: undefined,
disabled: false,
} as BlockState)
),
op: Array.from({ length: 6 }).map(
() =>
({
interactivity: undefined,
disabled: false,
} as BlockState)
),
},
},
set(
zone: ygopro.CardZone.MZONE | ygopro.CardZone.SZONE,
controller: number,
sequence: number,
placeInteractivity: PlaceInteractivity
state: BlockState
) {
placeStore.inner[zone][matStore.isMe(controller) ? "me" : "op"][sequence] =
placeInteractivity;
state;
},
clearAll() {
clearAllInteractivity() {
(["me", "op"] as const).forEach((who) => {
([MZONE, SZONE] as const).forEach((where) => {
placeStore.inner[where][who] = placeStore.inner[where][who].map(
() => undefined
placeStore.inner[where][who].forEach(
(block) => (block.interactivity = undefined)
);
});
});
......
import "./index.scss";
import classnames from "classnames";
import { type FC } from "react";
import { type CSSProperties, type FC } from "react";
import { type INTERNAL_Snapshot as Snapshot, useSnapshot } from "valtio";
import { sendSelectPlaceResponse, ygopro } from "@/api";
import { cardStore, type PlaceInteractivity, placeStore } from "@/stores";
import {
BlockState,
cardStore,
type PlaceInteractivity,
placeStore,
} from "@/stores";
// Block被禁用的样式
const BgDisabledStyle = {
background: `linear-gradient(
to top right,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) calc(50% - 1.5px),
red 50%,
rgba(0, 0, 0, 0) calc(50% + 1.5px),
rgba(0, 0, 0, 0) 100%
), linear-gradient(
to bottom right,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0) calc(50% - 1.5px),
red 50%,
rgba(0, 0, 0, 0) calc(50% + 1.5px),
rgba(0, 0, 0, 0) 100%
)`,
};
const BgExtraRow: FC<{
meSnap: Snapshot<PlaceInteractivity[]>;
opSnap: Snapshot<PlaceInteractivity[]>;
meSnap: Snapshot<BlockState[]>;
opSnap: Snapshot<BlockState[]>;
}> = ({ meSnap, opSnap }) => {
return (
<div className={classnames("bg-row")}>
......@@ -17,11 +41,16 @@ const BgExtraRow: FC<{
<div
key={i}
className={classnames("block", "extra", {
highlight: !!meSnap[i] || !!opSnap[i],
highlight: !!meSnap[i].interactivity || !!opSnap[i].interactivity,
})}
style={
meSnap[i].disabled || opSnap[i].disabled
? (BgDisabledStyle as CSSProperties)
: {}
}
onClick={() => {
onBlockClick(meSnap[i]);
onBlockClick(opSnap[i]);
onBlockClick(meSnap[i].interactivity);
onBlockClick(opSnap[i].interactivity);
}}
>
{<DecoTriangles />}
......@@ -34,7 +63,7 @@ const BgExtraRow: FC<{
const BgRow: FC<{
isSzone?: boolean;
opponent?: boolean;
snap: Snapshot<PlaceInteractivity[]>;
snap: Snapshot<BlockState[]>;
}> = ({ isSzone = false, opponent = false, snap }) => (
<div className={classnames("bg-row", { opponent })}>
{Array.from({ length: 5 }).map((_, i) => (
......@@ -42,9 +71,10 @@ const BgRow: FC<{
key={i}
className={classnames("block", {
szone: isSzone,
highlight: !!snap[i],
highlight: !!snap[i].interactivity,
})}
onClick={() => onBlockClick(snap[i])}
style={snap[i].disabled ? (BgDisabledStyle as CSSProperties) : {}}
onClick={() => onBlockClick(snap[i].interactivity)}
>
{<DecoTriangles />}
</div>
......@@ -72,7 +102,7 @@ const onBlockClick = (placeInteractivity: PlaceInteractivity) => {
if (placeInteractivity) {
sendSelectPlaceResponse(placeInteractivity.response);
cardStore.inner.forEach((card) => (card.idleInteractivities = []));
placeStore.clearAll();
placeStore.clearAllInteractivity();
}
};
......
......@@ -9,8 +9,10 @@ section#mat {
.card-img-wrap {
transform-style: preserve-3d;
position: relative;
height: 100%;
width: 100%;
margin: auto auto;
top: 2%;
height: 96%;
width: 96%;
transform: translateZ(calc(var(--z) * 1px + 0.1px))
rotateY(calc(var(--ry) * 1deg));
transition: 0.2s scale;
......@@ -47,6 +49,41 @@ section#mat {
background-color: transparent;
// filter: blur(2px);
}
// 卡片被选中后的流光特效
// ref: https://github.com/Mr-majifu/Animated-Profile-Card02/blob/master/style.css
.card-streamer {
position: absolute;
inset: 0;
background: #000;
overflow: hidden;
}
.card-streamer::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 400%;
height: 80%;
background: linear-gradient(transparent, #45f3ff,#45f3ff,#45f3ff,transparent);
animation: stream 2s linear infinite;
}
.card-streamer::after {
content: '';
position: absolute;
/* https://developer.mozilla.org/en-US/docs/Web/CSS/inset */
inset: 3px;
background: #292929;
}
@keyframes stream {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.card-focus {
position: absolute;
width: calc(100% * var(--focus-scale));
......
......@@ -241,6 +241,7 @@ export const Card: FC<{ idx: number }> = React.memo(({ idx }) => {
<YgoCard className="card-back" isBack />
</div>
</Dropdown>
{snap.selected ? <div className="card-streamer" /> : <></>}
</animated.div>
);
});
......
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