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