Commit 04463686 authored by Chunchi Che's avatar Chunchi Che

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

Feat/ui/op

See merge request mycard/Neos!58
parents 4d7402c3 1975fe75
# Neos
Web version of ygopro written in [React](https://reactjs.org/) + [Three.js](https://threejs.org/)/[Babylon.js](https://www.babylonjs.com/).
Web version of ygopro written in [React](https://reactjs.org/) + [Babylon.js](https://www.babylonjs.com/).
......@@ -26,8 +26,10 @@ export const ExclusionSlotShape = () => {
export const FieldSlotShape = () => {
return { width: 0.8, height: 1, depth: 0.2 };
};
export const CardSlotRotation = () => {
return new BABYLON.Vector3(1.55, 0, 0);
export const CardSlotRotation = (reverse: boolean) => {
return reverse
? new BABYLON.Vector3(1.55, 3.1, 0)
: new BABYLON.Vector3(1.55, 0, 0);
};
export const CardSlotDefenceRotation = () => {
return new BABYLON.Vector3(1.55, 1.55, 0);
......
......@@ -8,7 +8,7 @@ import {
import { DuelState } from "./mod";
import { ygopro } from "../../api/ocgcore/idl/ocgcore";
import { RootState } from "../../store";
import { CardMeta, fetchCard } from "../../api/cards";
import { fetchCard } from "../../api/cards";
export interface MagicState {
magics: Magic[];
......@@ -164,3 +164,5 @@ export const magicCase = (builder: ActionReducerMapBuilder<DuelState>) => {
export const selectMeMagics = (state: RootState) =>
state.duel.meMagics || { magics: [] };
export const selectOpMagics = (state: RootState) =>
state.duel.opMagics || { magics: [] };
......@@ -8,13 +8,13 @@ import {
import { DuelState } from "./mod";
import { ygopro } from "../../api/ocgcore/idl/ocgcore";
import { RootState } from "../../store";
import { CardMeta, fetchCard } from "../../api/cards";
import { fetchCard } from "../../api/cards";
export interface MonsterState {
monsters: Monster[];
}
// 初始化自己的怪兽区状态
// 初始化怪兽区状态
export const initMonstersImpl: CaseReducer<DuelState, PayloadAction<number>> = (
state,
action
......@@ -168,3 +168,5 @@ export const monsterCase = (builder: ActionReducerMapBuilder<DuelState>) => {
export const selectMeMonsters = (state: RootState) =>
state.duel.meMonsters || { monsters: [] };
export const selectOpMonsters = (state: RootState) =>
state.duel.opMonsters || { monsters: [] };
import * as BABYLON from "@babylonjs/core";
import { useAppSelector } from "../../hook";
import { selectMeHands } from "../../reducers/duel/handsSlice";
import { selectMeHands, selectOpHands } from "../../reducers/duel/handsSlice";
import * as CONFIG from "../../config/ui";
import { Hand, InteractType } from "../../reducers/duel/util";
import { Hand } from "../../reducers/duel/util";
import {
setCardModalImgUrl,
setCardModalIsOpen,
......@@ -14,22 +14,44 @@ import { useHover } from "react-babylonjs";
import { useClick } from "./hook";
import { useState, useRef, useEffect } from "react";
import { useSpring, animated } from "./spring";
import { zip, interactTypeToString } from "./util";
const groundShape = CONFIG.GroundShape();
const left = -(groundShape.width / 2);
const handShape = CONFIG.HandShape();
const handRotation = CONFIG.HandRotation();
const Hands = () => {
const hands = useAppSelector(selectMeHands).cards;
const meHands = useAppSelector(selectMeHands).cards;
const meHandPositions = handPositons(0, meHands);
const opHands = useAppSelector(selectOpHands).cards;
const opHandPositions = handPositons(1, opHands);
return (
<>
{hands.map((hand, idx) => {
{zip(meHands, meHandPositions).map(([hand, position], idx) => {
return (
<CHand
key={idx}
state={hand}
idx={idx}
sequence={idx}
position={position}
rotation={handRotation}
cover={(id) =>
`https://cdn02.moecube.com:444/images/ygopro-images-zh-CN/${id}.jpg`
}
/>
);
})}
{zip(opHands, opHandPositions).map(([hand, position], idx) => {
return (
<CHand
key={idx}
gap={groundShape.width / (hands.length - 1)}
state={hand}
sequence={idx}
position={position}
rotation={handRotation}
cover={(_) => `http://localhost:3030/images/card_back.jpg`}
/>
);
})}
......@@ -37,26 +59,23 @@ const Hands = () => {
);
};
const CHand = (props: { state: Hand; idx: number; gap: number }) => {
const handShape = CONFIG.HandShape();
const rotation = CONFIG.HandRotation();
const CHand = (props: {
state: Hand;
sequence: number;
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
cover: (id: number) => string;
}) => {
const hoverScale = CONFIG.HandHoverScaling();
const defaultScale = new BABYLON.Vector3(1, 1, 1);
const edgesWidth = 2.0;
const edgesColor = BABYLON.Color4.FromColor3(BABYLON.Color3.Yellow());
const planeRef = useRef(null);
const [state, idx] = [props.state, props.idx];
const state = props.state;
const [hovered, setHovered] = useState(false);
const position = props.position;
const dispatch = store.dispatch;
const [position, setPosition] = useState(
new BABYLON.Vector3(
left + props.gap * props.idx,
handShape.height / 2,
-(groundShape.height / 2) - 1
)
);
const [spring, api] = useSpring(
() => ({
from: {
......@@ -76,18 +95,10 @@ const CHand = (props: { state: Hand; idx: number; gap: number }) => {
);
useEffect(() => {
const newPosition = new BABYLON.Vector3(
left + props.gap * props.idx,
handShape.height / 2,
-(groundShape.height / 2) - 1
);
api.start({
position: newPosition,
position,
});
setPosition(newPosition);
}, [props.idx, props.gap]);
}, [position]);
useHover(
() => setHovered(true),
......@@ -122,54 +133,35 @@ const CHand = (props: { state: Hand; idx: number; gap: number }) => {
// @ts-ignore
<animated.transformNode name="">
<animated.plane
name={`hand-${idx}`}
name={`hand-${props.sequence}`}
ref={planeRef}
width={handShape.width}
height={handShape.height}
scaling={hovered ? hoverScale : defaultScale}
position={spring.position}
rotation={rotation}
rotation={props.rotation}
enableEdgesRendering
edgesWidth={state.interactivities.length == 0 ? 0 : edgesWidth}
edgesColor={edgesColor}
>
<animated.standardMaterial
name={`hand-mat-${idx}`}
diffuseTexture={
new BABYLON.Texture(
`https://cdn02.moecube.com:444/images/ygopro-images-zh-CN/${state.meta.id}.jpg`
)
}
name={`hand-mat-${props.sequence}`}
diffuseTexture={new BABYLON.Texture(props.cover(state.meta.id))}
/>
</animated.plane>
</animated.transformNode>
);
};
function interactTypeToString(t: InteractType): string {
switch (t) {
case InteractType.SUMMON: {
return "普通召唤";
}
case InteractType.SP_SUMMON: {
return "特殊召唤";
}
case InteractType.POS_CHANGE: {
return "改变表示形式";
}
case InteractType.MSET: {
return "前场放置";
}
case InteractType.SSET: {
return "后场放置";
}
case InteractType.ACTIVATE: {
return "发动效果";
}
default: {
return "未知选项";
}
}
}
const handPositons = (player: number, hands: Hand[]) => {
const gap = groundShape.width / (hands.length - 1);
const x = (idx: number) =>
player == 0 ? left + gap * idx : -left - gap * idx;
const y = handShape.height / 2;
const z =
player == 0 ? -(groundShape.height / 2) - 1 : groundShape.height / 2 + 1;
return hands.map((_, idx) => new BABYLON.Vector3(x(idx), y, z));
};
export default Hands;
import * as BABYLON from "@babylonjs/core";
import * as CONFIG from "../../config/ui";
import { selectMeMagics } from "../../reducers/duel/magicSlice";
import { selectMeMagics, selectOpMagics } from "../../reducers/duel/magicSlice";
import { useClick } from "./hook";
import { Magic } from "../../reducers/duel/util";
import { store } from "../../store";
......@@ -14,34 +14,52 @@ import {
setCardModalText,
} from "../../reducers/duel/mod";
import { ygopro } from "../../api/ocgcore/idl/ocgcore";
import { zip } from "./util";
// TODO: use config
const left = -2.15;
const gap = 1.05;
const shape = CONFIG.CardSlotShape();
const Magics = () => {
const magics = useAppSelector(selectMeMagics).magics;
const meMagics = useAppSelector(selectMeMagics).magics;
const meMagicPositions = magicPositions(0, meMagics);
const opMagics = useAppSelector(selectOpMagics).magics;
const opMagicPositions = magicPositions(1, opMagics);
return (
<>
{magics.map((magic) => {
return <CMagic state={magic} key={magic.sequence} />;
{zip(meMagics, meMagicPositions).map(([magic, position]) => {
return (
<CMagic
state={magic}
key={magic.sequence}
position={position}
rotation={CONFIG.CardSlotRotation(false)}
/>
);
})}
{zip(opMagics, opMagicPositions).map(([magic, position]) => {
return (
<CMagic
state={magic}
key={magic.sequence}
position={position}
rotation={CONFIG.CardSlotRotation(true)}
/>
);
})}
</>
);
};
const CMagic = (props: { state: Magic }) => {
const CMagic = (props: {
state: Magic;
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
}) => {
const state = props.state;
const planeRef = useRef(null);
const shape = CONFIG.CardSlotShape();
const position = new BABYLON.Vector3(
left + gap * state.sequence,
shape.depth / 2 + CONFIG.Floating,
-2.6
);
const rotation = CONFIG.CardSlotRotation();
const faceDown =
state.position === ygopro.CardPosition.FACEDOWN ||
state.position === ygopro.CardPosition.FACEDOWN_ATTACK ||
......@@ -78,8 +96,8 @@ const CMagic = (props: { state: Magic }) => {
ref={planeRef}
width={shape.width}
height={shape.height}
position={position}
rotation={rotation}
position={props.position}
rotation={props.rotation}
enableEdgesRendering
edgesWidth={state.selectInfo ? edgesWidth : 0}
edgesColor={edgesColor}
......@@ -103,4 +121,13 @@ const CMagic = (props: { state: Magic }) => {
);
};
const magicPositions = (player: number, magics: Magic[]) => {
const x = (sequence: number) =>
player == 0 ? left + gap * sequence : -left - gap * sequence;
const y = shape.depth / 2 + CONFIG.Floating;
const z = player == 0 ? -2.6 : 2.6;
return magics.map((magic) => new BABYLON.Vector3(x(magic.sequence), y, z));
};
export default Magics;
......@@ -12,6 +12,7 @@ import Field from "./field";
import Deck from "./deck";
import Exclusion from "./exclusion";
// Ref: https://github.com/brianzinn/react-babylonjs/issues/126
const NeosDuel = () => (
<>
<ReactReduxContext.Consumer>
......
......@@ -14,39 +14,67 @@ import {
setCardModalText,
} from "../../reducers/duel/mod";
import { useAppSelector } from "../../hook";
import { selectMeMonsters } from "../../reducers/duel/monstersSlice";
import {
selectMeMonsters,
selectOpMonsters,
} from "../../reducers/duel/monstersSlice";
import { ygopro } from "../../api/ocgcore/idl/ocgcore";
import { zip } from "./util";
const shape = CONFIG.CardSlotShape();
const left = -2.15; // TODO: config
const gap = 1.05;
const Monsters = () => {
const monsters = useAppSelector(selectMeMonsters).monsters;
const meMonsters = useAppSelector(selectMeMonsters).monsters;
const meMonsterPositions = monsterPositions(0, meMonsters);
const opMonsters = useAppSelector(selectOpMonsters).monsters;
const opMonsterPositions = monsterPositions(1, opMonsters);
return (
<>
{monsters.map((monster, idx) => {
return <CommonMonster state={monster} key={idx} />;
{zip(meMonsters, meMonsterPositions).map(([monster, position], idx) => {
return (
<CommonMonster
state={monster}
key={idx}
position={position}
rotation={CONFIG.CardSlotRotation(false)}
deffenseRotation={CONFIG.CardSlotDefenceRotation()}
/>
);
})}
{zip(opMonsters, opMonsterPositions).map(([monster, position], idx) => {
return (
<CommonMonster
state={monster}
key={idx}
position={position}
rotation={CONFIG.CardSlotRotation(true)}
deffenseRotation={CONFIG.CardSlotDefenceRotation()}
/>
);
})}
<ExtraMonsters />
<ExtraMonsters />
</>
);
};
const CommonMonster = (props: { state: Monster }) => {
const CommonMonster = (props: {
state: Monster;
position: BABYLON.Vector3;
rotation: BABYLON.Vector3;
deffenseRotation: BABYLON.Vector3;
}) => {
const planeRef = useRef(null);
const shape = CONFIG.CardSlotShape();
const position = new BABYLON.Vector3(
left + gap * props.state.sequence,
shape.depth / 2 + CONFIG.Floating,
-1.35
);
const rotation =
props.state.position === ygopro.CardPosition.DEFENSE ||
props.state.position === ygopro.CardPosition.FACEUP_DEFENSE ||
props.state.position === ygopro.CardPosition.FACEDOWN_DEFENSE
? CONFIG.CardSlotDefenceRotation()
: CONFIG.CardSlotRotation();
? props.deffenseRotation
: props.rotation;
const edgesWidth = 2.0;
const edgesColor = BABYLON.Color4.FromColor3(BABYLON.Color3.Yellow());
const dispatch = store.dispatch;
......@@ -88,7 +116,7 @@ const CommonMonster = (props: { state: Monster }) => {
ref={planeRef}
width={shape.width}
height={shape.height}
position={position}
position={props.position}
rotation={rotation}
enableEdgesRendering
edgesWidth={props.state.selectInfo ? edgesWidth : 0}
......@@ -119,7 +147,7 @@ const ExtraMonsters = () => {
const shape = CONFIG.CardSlotShape();
const position = (x: number) =>
new BABYLON.Vector3(x, shape.depth / 2 + CONFIG.Floating, 0);
const rotation = CONFIG.CardSlotRotation();
const rotation = CONFIG.CardSlotRotation(false);
return (
<>
......@@ -143,4 +171,15 @@ const ExtraMonsters = () => {
);
};
const monsterPositions = (player: number, monsters: Monster[]) => {
const x = (sequence: number) =>
player == 0 ? left + gap * sequence : -left - gap * sequence;
const y = shape.depth / 2 + CONFIG.Floating;
const z = player == 0 ? -1.35 : 1.35;
return monsters.map(
(monster) => new BABYLON.Vector3(x(monster.sequence), y, z)
);
};
export default Monsters;
import { InteractType } from "../../reducers/duel/util";
export function zip<S1, S2>(
firstCollection: Array<S1>,
lastCollection: Array<S2>
): Array<[S1, S2]> {
const length = Math.min(firstCollection.length, lastCollection.length);
const zipped: Array<[S1, S2]> = [];
for (let index = 0; index < length; index++) {
zipped.push([firstCollection[index], lastCollection[index]]);
}
return zipped;
}
export function interactTypeToString(t: InteractType): string {
switch (t) {
case InteractType.SUMMON: {
return "普通召唤";
}
case InteractType.SP_SUMMON: {
return "特殊召唤";
}
case InteractType.POS_CHANGE: {
return "改变表示形式";
}
case InteractType.MSET: {
return "前场放置";
}
case InteractType.SSET: {
return "后场放置";
}
case InteractType.ACTIVATE: {
return "发动效果";
}
default: {
return "未知选项";
}
}
}
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