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