Commit 4f900064 authored by Chunchi Che's avatar Chunchi Che

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

Feat/ui/select place

See merge request mycard/Neos!50
parents 3bef189a 44ee48ca
......@@ -2,6 +2,7 @@ import { ygopro } from "../../../idl/ocgcore";
import { YgoProPacket } from "../../packet";
import { CTOS_RESPONSE } from "../../protoDecl";
import adaptSelectIdleCmdResponse from "./selectIdleCmd";
import adaptSelectPlaceResponse from "./selectPlace";
/*
* CTOS CTOS_RESPONSE
......@@ -22,6 +23,11 @@ export default class CtosResponsePacket extends YgoProPacket {
break;
}
case "select_place": {
extraData = adaptSelectPlaceResponse(response.select_place);
break;
}
default: {
break;
}
......
import { ygopro } from "../../../idl/ocgcore";
import { BufferWriter } from "../../bufferIO";
import { cardZoneToNumber } from "../../util";
export default (response: ygopro.CtosGameMsgResponse.SelectPlaceResponse) => {
const array = new Uint8Array(3);
const writer = new BufferWriter(array, true);
writer.writeUint8(response.player);
writer.writeUint8(cardZoneToNumber(response.zone));
writer.writeUint8(response.sequence);
return array;
};
......@@ -2,6 +2,9 @@
* 一些基础函数。
*
* */
import { ygopro } from "../idl/ocgcore";
export const UTF16_BUFFER_MAX_LEN = 20;
const FILLING_TOKEN: number = 0xcccc;
......@@ -72,3 +75,41 @@ export function utf8ArrayToStr(array: Uint8Array) {
return out;
}
export function cardZoneToNumber(zone: ygopro.CardZone): number {
switch (zone) {
case ygopro.CardZone.DECK: {
return 0x01;
}
case ygopro.CardZone.HAND: {
return 0x02;
}
case ygopro.CardZone.MZONE: {
return 0x04;
}
case ygopro.CardZone.SZONE: {
return 0x08;
}
case ygopro.CardZone.GRAVE: {
return 0x10;
}
case ygopro.CardZone.REMOVED: {
return 0x20;
}
case ygopro.CardZone.EXTRA: {
return 0x40;
}
case ygopro.CardZone.OVERLAY: {
return 0x80;
}
case ygopro.CardZone.ONFIELD: {
return 0x0c;
}
case ygopro.CardZone.FZONE: {
return 0x100;
}
case ygopro.CardZone.PZONE: {
return 0x200;
}
}
}
......@@ -131,3 +131,22 @@ export function sendSelectIdleCmdResponse(value: number) {
socketMiddleWare({ cmd: socketCmd.SEND, payload });
}
export function sendSelectPlaceResponse(value: {
controler: number;
zone: ygopro.CardZone;
sequence: number;
}) {
const response = new ygopro.YgoCtosMsg({
ctos_response: new ygopro.CtosGameMsgResponse({
select_place: new ygopro.CtosGameMsgResponse.SelectPlaceResponse({
player: value.controler,
zone: value.zone,
sequence: value.sequence,
}),
}),
});
const payload = new GameMsgResponse(response).serialize();
socketMiddleWare({ cmd: socketCmd.SEND, payload });
}
......@@ -126,7 +126,7 @@ export const addHandsInteractivityImpl: CaseReducer<
PayloadAction<{
player: number;
index: number;
interactivity: Interactivity;
interactivity: Interactivity<number>;
}>
> = (state, action) => {
const player = action.payload.player;
......
......@@ -23,17 +23,30 @@ import {
setCardModalImgUrlImpl,
setCardModalInteractiviesImpl,
} from "./modalSlice";
import {
MonsterState,
initMonstersImpl,
addMonsterPlaceSelectAbleImpl,
clearMonsterSelectInfoImpl,
} from "./monstersSlice";
export interface DuelState {
selfType?: number;
meInitInfo?: InitInfo; // 自己的初始状态
opInitInfo?: InitInfo; // 对手的初始状态
meHands?: Hands; // 自己的手牌
opHands?: Hands; // 对手的手牌
meMonsters?: MonsterState; // 自己的怪兽区状态
opMonsters?: MonsterState; // 对手的怪兽区状态
meTimeLimit?: TimeLimit; // 自己的计时
opTimeLimit?: TimeLimit; // 对手的计时
meHint?: HintState; // 自己的提示
opHint?: HintState; // 对手的提示
currentPlayer?: number; // 当前的操作方
currentPhase?: string; // 当前的阶段
......@@ -63,6 +76,11 @@ const duelSlice = createSlice({
clearHandsInteractivity: clearHandsInteractivityImpl,
addHandsInteractivity: addHandsInteractivityImpl,
// 怪兽区相关`Reducer`
initMonsters: initMonstersImpl,
addMonsterPlaceSelectAble: addMonsterPlaceSelectAbleImpl,
clearMonsterSelectInfo: clearMonsterSelectInfoImpl,
// UI相关`Reducer`
setCardModalIsOpen: setCardModalIsOpenImpl,
setCardModalText: setCardModalTextImpl,
......@@ -87,6 +105,9 @@ export const {
setCardModalText,
setCardModalImgUrl,
setCardModalInteractivies,
initMonsters,
addMonsterPlaceSelectAble,
clearMonsterSelectInfo,
} = duelSlice.actions;
export const selectDuelHsStart = (state: RootState) => {
return state.duel.meInitInfo != null;
......
import { judgeSelf, Monster, InteractType } from "./util";
import { PayloadAction, CaseReducer } from "@reduxjs/toolkit";
import { DuelState } from "./mod";
import { ygopro } from "../../api/ocgcore/idl/ocgcore";
import { RootState } from "../../store";
export interface MonsterState {
monsters: Monster[];
}
// 初始化自己的怪兽区状态
export const initMonstersImpl: CaseReducer<DuelState, PayloadAction<number>> = (
state,
action
) => {
const player = action.payload;
const monsters = {
monsters: [
{
sequence: 0,
},
{
sequence: 1,
},
{
sequence: 2,
},
{
sequence: 3,
},
{
sequence: 4,
},
],
};
if (judgeSelf(player, state)) {
state.meMonsters = monsters;
} else {
state.opMonsters = monsters;
}
};
export const addMonsterPlaceSelectAbleImpl: CaseReducer<
DuelState,
PayloadAction<[number, number]>
> = (state, action) => {
const controler = action.payload[0];
const sequence = action.payload[1];
const monsters = judgeSelf(controler, state)
? state.meMonsters
: state.opMonsters;
if (monsters) {
for (const monster of monsters.monsters) {
if (monster.sequence == sequence) {
monster.selectInfo = {
interactType: InteractType.PLACE_SELECTABLE,
response: {
controler,
zone: ygopro.CardZone.MZONE,
sequence,
},
};
}
}
}
};
export const clearMonsterSelectInfoImpl: CaseReducer<
DuelState,
PayloadAction<number>
> = (state, action) => {
const player = action.payload;
const monsters = judgeSelf(player, state)
? state.meMonsters
: state.opMonsters;
if (monsters) {
monsters.monsters = [];
}
};
export const selectMeMonsters = (state: RootState) =>
state.duel.meMonsters || { monsters: [] };
......@@ -6,6 +6,7 @@
import { CardMeta } from "../../api/cards";
import { DuelState } from "./mod";
import { Draft } from "@reduxjs/toolkit";
import { ygopro } from "../../api/ocgcore/idl/ocgcore";
/*
* 通过`player`和`selfType`判断是应该处理自己还是对手
......@@ -30,7 +31,7 @@ export function judgeSelf(player: number, state: Draft<DuelState>): boolean {
export interface Card {
meta: CardMeta;
transform: CardTransform;
interactivities: Interactivity[];
interactivities: Interactivity<number>[];
}
interface CardTransform {
......@@ -59,12 +60,24 @@ export enum InteractType {
SSET = 5,
// 可发动效果
ACTIVATE = 6,
// 可作为位置选择
PLACE_SELECTABLE = 7,
}
export interface Interactivity {
export interface Interactivity<T> {
interactType: InteractType;
// 如果`interactType`是`ACTIVATE`,这个字段是对应的效果编号
activateIndex?: number;
// 用户点击后,需要回传给服务端的`response`
response: number;
response: T;
}
export interface Monster {
sequence: number;
occupant?: CardMeta;
selectInfo?: Interactivity<{
controler: number;
zone: ygopro.CardZone;
sequence: number;
}>;
}
import { ygopro } from "../../api/ocgcore/idl/ocgcore";
import { AppDispatch } from "../../store";
import MsgSelectPlace = ygopro.StocGameMessage.MsgSelectPlace;
import { addMonsterPlaceSelectAble } from "../../reducers/duel/mod";
export default (selectPlace: MsgSelectPlace, dispatch: AppDispatch) => {
// TODO
if (selectPlace.count != 1) {
console.warn(`Unhandled case: ${selectPlace}`);
console.log(selectPlace);
return;
}
for (const place of selectPlace.places) {
switch (place.zone) {
case ygopro.CardZone.MZONE: {
dispatch(addMonsterPlaceSelectAble([place.controler, place.sequence]));
break;
}
default: {
console.warn(`Unhandled zoneType: ${place.zone}`);
}
}
}
};
import { ygopro } from "../../api/ocgcore/idl/ocgcore";
import { AppDispatch } from "../../store";
import { infoInit, setSelfType } from "../../reducers/duel/mod";
import { infoInit, setSelfType, initMonsters } from "../../reducers/duel/mod";
export default (
start: ygopro.StocGameMessage.MsgStart,
......@@ -27,4 +27,6 @@ export default (
},
])
);
dispatch(initMonsters(0));
dispatch(initMonsters(1));
};
......@@ -6,7 +6,7 @@ export default (scene: BABYLON.Scene) => {
const shape = CONFIG.CardSlotShape();
for (let i in xs) {
const slot = BABYLON.MeshBuilder.CreateBox(
const slot = BABYLON.MeshBuilder.CreatePlane(
`extraMonster${i}`,
shape,
scene
......@@ -24,7 +24,10 @@ export default (scene: BABYLON.Scene) => {
"extraMonsterMaterial",
scene
);
extraMonsterMaterial.diffuseColor = CONFIG.extraMonsterColor();
extraMonsterMaterial.diffuseTexture = new BABYLON.Texture(
`http://localhost:3030/images/card_slot.png`
);
extraMonsterMaterial.diffuseTexture.hasAlpha = true;
slot.material = extraMonsterMaterial;
}
};
......@@ -7,7 +7,7 @@ export default (scene: BABYLON.Scene) => {
const shape = CONFIG.CardSlotShape();
for (let i = 0; i < 5; i++) {
const slot = BABYLON.MeshBuilder.CreateBox(`magic${i}`, shape, scene);
const slot = BABYLON.MeshBuilder.CreatePlane(`magic${i}`, shape, scene);
// 位置
slot.position = new BABYLON.Vector3(
left + gap * i,
......@@ -18,7 +18,10 @@ export default (scene: BABYLON.Scene) => {
slot.rotation = CONFIG.CardSlotRotation();
// 材质
const magicMaterial = new BABYLON.StandardMaterial("magicMaterial", scene);
magicMaterial.diffuseColor = CONFIG.MagicColor();
magicMaterial.diffuseTexture = new BABYLON.Texture(
`http://localhost:3030/images/card_slot.png`
);
magicMaterial.diffuseTexture.hasAlpha = true;
slot.material = magicMaterial;
}
};
......@@ -21,6 +21,7 @@ import { selectCurrentPlayer } from "../../reducers/duel/turnSlice";
import CardModal from "./cardModal";
import HintNotification from "./hintNotification";
import { selectMeHands } from "../../reducers/duel/handsSlice";
import { selectMeMonsters } from "../../reducers/duel/monstersSlice";
// CONFIG
......@@ -28,6 +29,7 @@ const Duel = () => {
// ----- 数据获取 -----
const hands = useAppSelector(selectMeHands).cards;
const monsters = useAppSelector(selectMeMonsters).monsters;
const currentPlayer = useAppSelector(selectCurrentPlayer);
// ----- WebGL渲染 -----
......@@ -60,7 +62,7 @@ const Duel = () => {
renderMagics(scene);
// 怪兽区
renderMonsters(scene);
renderMonsters(monsters, scene);
// 创建额外怪兽区
renderExtraMonsters(scene);
......@@ -105,7 +107,7 @@ const Duel = () => {
engine.runRenderLoop(() => {
scene.render();
});
}, [canvasRef, hands, currentPlayer]);
}, [canvasRef, hands, monsters, currentPlayer]); // FIXME: 这里需要优化,应该分组件按需渲染
useEffect(() => {
// 监听状态变化,并实现动画
......
import * as BABYLON from "@babylonjs/core";
import * as CONFIG from "../../config/ui";
import { Monster } from "../../reducers/duel/util";
import { clearMonsterSelectInfo } from "../../reducers/duel/mod";
import { store } from "../../store";
import { sendSelectPlaceResponse } from "../../api/ocgcore/ocgHelper";
export default (scene: BABYLON.Scene) => {
export default (monsters: Monster[], scene: BABYLON.Scene) => {
const left = -2.15;
const gap = 1.05;
const shape = CONFIG.CardSlotShape();
for (let i = 0; i < 5; i++) {
const slot = BABYLON.MeshBuilder.CreateBox(`monster${i}`, shape, scene);
// 位置
slot.position = new BABYLON.Vector3(
left + gap * i,
shape.depth / 2 + CONFIG.Floating,
-1.35
for (const monster of monsters) {
const slot = BABYLON.MeshBuilder.CreatePlane(
`monster${monster.sequence}`,
shape,
scene
);
// 位置
setupMonsterTransform(slot, monster, left, gap, shape);
// 旋转
slot.rotation = CONFIG.CardSlotRotation();
// 材质
const monsterMaterial = new BABYLON.StandardMaterial(
"monsterMaterial",
scene
);
monsterMaterial.diffuseColor = CONFIG.MonsterColor();
slot.material = monsterMaterial;
setupMonsterMaterial(slot, monster, scene);
// 高亮
setupHintEdge(slot, monster);
// 事件管理
setupMonsterAction(slot, monster, scene);
}
};
function setupMonsterTransform(
mesh: BABYLON.Mesh,
state: Monster,
left: number,
gap: number,
shape: { width: number; height: number; depth: number }
) {
mesh.position = new BABYLON.Vector3(
left + gap * state.sequence,
shape.depth / 2 + CONFIG.Floating,
-1.35
);
}
function setupMonsterMaterial(
mesh: BABYLON.Mesh,
state: Monster,
scene: BABYLON.Scene
) {
const monsterMaterial = new BABYLON.StandardMaterial(
"monsterMaterial",
scene
);
monsterMaterial.diffuseTexture = state.occupant
? new BABYLON.Texture(
`https://cdn02.moecube.com:444/images/ygopro-images-zh-CN/${state.occupant.id}.jpg`
)
: new BABYLON.Texture(`http://localhost:3030/images/card_slot.png`);
monsterMaterial.diffuseTexture.hasAlpha = true;
mesh.material = monsterMaterial;
}
function setupHintEdge(mesh: BABYLON.Mesh, state: Monster) {
if (state.selectInfo) {
mesh.enableEdgesRendering();
mesh.edgesWidth = 2.0;
mesh.edgesColor = BABYLON.Color4.FromColor3(BABYLON.Color3.Yellow());
} else {
mesh.disableEdgesRendering();
}
}
function setupMonsterAction(
mesh: BABYLON.Mesh,
state: Monster,
scene: BABYLON.Scene
) {
const dispatch = store.dispatch;
mesh.actionManager = new BABYLON.ActionManager(scene);
// 监听点击事件
mesh.actionManager.registerAction(
new BABYLON.ExecuteCodeAction(
BABYLON.ActionManager.OnPickTrigger,
(_event) => {
if (state.selectInfo) {
sendSelectPlaceResponse(state.selectInfo.response);
dispatch(clearMonsterSelectInfo(0));
dispatch(clearMonsterSelectInfo(1));
}
}
)
);
}
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