Commit c45a5f2c authored by Chunchi Che's avatar Chunchi Che

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

Feat/ui/anime

See merge request !56
parents 5020a162 7abeaded
......@@ -8,6 +8,9 @@
"name": "neos-ts",
"version": "0.1.0",
"dependencies": {
"@react-spring/shared": "^9.6.1",
"@react-spring/types": "^9.6.1",
"@react-spring/web": "^9.6.1",
"@reduxjs/toolkit": "^1.8.6",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0",
......@@ -2294,6 +2297,73 @@
"react-dom": ">=16.9.0"
}
},
"node_modules/@react-spring/animated": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
"integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
"dependencies": {
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/core": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
"integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
"dependencies": {
"@react-spring/animated": "~9.6.1",
"@react-spring/rafz": "~9.6.1",
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-spring/donate"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/rafz": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
"integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ=="
},
"node_modules/@react-spring/shared": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
"integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
"dependencies": {
"@react-spring/rafz": "~9.6.1",
"@react-spring/types": "~9.6.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/types": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
"integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q=="
},
"node_modules/@react-spring/web": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.6.1.tgz",
"integrity": "sha512-X2zR6q2Z+FjsWfGAmAXlQaoUHbPmfuCaXpuM6TcwXPpLE1ZD4A1eys/wpXboFQmDkjnrlTmKvpVna1MjWpZ5Hw==",
"dependencies": {
"@react-spring/animated": "~9.6.1",
"@react-spring/core": "~9.6.1",
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.6.tgz",
......@@ -27683,6 +27753,56 @@
"rc-util": "^5.24.4"
}
},
"@react-spring/animated": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
"integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
"requires": {
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
}
},
"@react-spring/core": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
"integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
"requires": {
"@react-spring/animated": "~9.6.1",
"@react-spring/rafz": "~9.6.1",
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
}
},
"@react-spring/rafz": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
"integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ=="
},
"@react-spring/shared": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
"integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
"requires": {
"@react-spring/rafz": "~9.6.1",
"@react-spring/types": "~9.6.1"
}
},
"@react-spring/types": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
"integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q=="
},
"@react-spring/web": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.6.1.tgz",
"integrity": "sha512-X2zR6q2Z+FjsWfGAmAXlQaoUHbPmfuCaXpuM6TcwXPpLE1ZD4A1eys/wpXboFQmDkjnrlTmKvpVna1MjWpZ5Hw==",
"requires": {
"@react-spring/animated": "~9.6.1",
"@react-spring/core": "~9.6.1",
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
}
},
"@reduxjs/toolkit": {
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.6.tgz",
......@@ -3,6 +3,9 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@react-spring/shared": "^9.6.1",
"@react-spring/types": "^9.6.1",
"@react-spring/web": "^9.6.1",
"@reduxjs/toolkit": "^1.8.6",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.3.0",
......
......@@ -8,7 +8,6 @@ import { DuelState } from "./mod";
import { RootState } from "../../store";
import { fetchCard, CardMeta } from "../../api/cards";
import { judgeSelf, Hand, Interactivity } from "./util";
import * as UICONFIG from "../../config/ui";
export interface Hands {
// 注意:手牌的位置顺序是有约束的
......@@ -56,7 +55,6 @@ export const handsCase = (builder: ActionReducerMapBuilder<DuelState>) => {
} else {
state.meHands = { cards };
}
setHandsTransform(state.meHands.cards);
} else {
if (state.opHands) {
state.opHands.cards = state.opHands.cards.concat(cards);
......@@ -83,27 +81,6 @@ export const handsCase = (builder: ActionReducerMapBuilder<DuelState>) => {
});
};
// 更新手牌的位置和旋转信息
//
// TODO: 兼容对方手牌
function setHandsTransform(hands: Hand[]): void {
const groundShape = UICONFIG.GroundShape();
const handShape = UICONFIG.HandShape();
const gap = groundShape.width / (hands.length - 1);
const left = -(groundShape.width / 2);
hands.forEach((hand, idx, _) => {
hand.transform.position = {
x: left + gap * idx,
y: handShape.height / 2,
z: -(groundShape.height / 2) - 1,
};
const rotation = UICONFIG.HandRotation();
hand.transform.rotation = { x: rotation.x, y: rotation.y, z: rotation.z };
});
}
// 清空手牌互动性
export const clearHandsInteractivityImpl: CaseReducer<
DuelState,
......
......@@ -27,23 +27,9 @@ export function judgeSelf(player: number, state: Draft<DuelState>): boolean {
export interface Hand {
meta: CardMeta;
transform: CardTransform;
interactivities: Interactivity<number>[];
}
interface CardTransform {
position?: {
x: number;
y: number;
z: number;
};
rotation?: {
x: number;
y: number;
z: number;
};
}
export enum InteractType {
// 可普通召唤
SUMMON = 1,
......
......@@ -8,7 +8,10 @@ import {
selectCardModalImgUrl,
selectCardModalInteractivies,
} from "../../reducers/duel/modalSlice";
import { setCardModalIsOpen } from "../../reducers/duel/mod";
import {
setCardModalIsOpen,
clearHandsInteractivity,
} from "../../reducers/duel/mod";
import { Modal, Card, Button } from "antd";
import { sendSelectIdleCmdResponse } from "../../api/ocgcore/ocgHelper";
......@@ -45,6 +48,7 @@ const CardModal = () => {
onClick={() => {
sendSelectIdleCmdResponse(interactive.response);
dispatch(setCardModalIsOpen(false));
dispatch(clearHandsInteractivity(0));
}}
>
{interactive.desc}
......
......@@ -12,7 +12,12 @@ import {
import { store } from "../../store";
import { useHover } from "react-babylonjs";
import { useClick } from "./hook";
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import { useSpring, animated } from "./spring";
import { config } from "@react-spring/web";
const groundShape = CONFIG.GroundShape();
const left = -(groundShape.width / 2);
const Hands = () => {
const hands = useAppSelector(selectMeHands).cards;
......@@ -20,14 +25,22 @@ const Hands = () => {
return (
<>
{hands.map((hand, idx) => {
return <CHand state={hand} idx={idx} key={idx} />;
return (
<CHand
state={hand}
idx={idx}
key={idx}
gap={groundShape.width / (hands.length - 1)}
/>
);
})}
</>
);
};
const CHand = (props: { state: Hand; idx: number }) => {
const CHand = (props: { state: Hand; idx: number; gap: number }) => {
const handShape = CONFIG.HandShape();
const rotation = CONFIG.HandRotation();
const hoverScale = CONFIG.HandHoverScaling();
const defaultScale = new BABYLON.Vector3(1, 1, 1);
const planeRef = useRef(null);
......@@ -35,6 +48,46 @@ const CHand = (props: { state: Hand; idx: number }) => {
const [hovered, setHovered] = useState(false);
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: {
position,
},
config: {
mass: 1.0,
tension: 170,
friction: 900,
precision: 0.01,
velocity: 0.0,
clamp: true,
duration: 2000,
},
}),
[]
);
useEffect(() => {
const newPosition = new BABYLON.Vector3(
left + props.gap * props.idx,
handShape.height / 2,
-(groundShape.height / 2) - 1
);
api.start({
position: newPosition,
});
setPosition(newPosition);
}, [props.idx, props.gap]);
useHover(
() => setHovered(true),
() => setHovered(false),
......@@ -65,29 +118,18 @@ const CHand = (props: { state: Hand; idx: number }) => {
[state]
);
return (
<>
<plane
// @ts-ignore
<animated.transformNode name="">
<animated.plane
name={`hand-${idx}`}
ref={planeRef}
width={handShape.width}
height={handShape.height}
scaling={hovered ? hoverScale : defaultScale}
position={
new BABYLON.Vector3(
state.transform.position?.x,
state.transform.position?.y,
state.transform.position?.z
)
}
rotation={
new BABYLON.Vector3(
state.transform.rotation?.x,
state.transform.rotation?.y,
state.transform.rotation?.z
)
}
position={spring.position}
rotation={rotation}
>
<standardMaterial
<animated.standardMaterial
name={`hand-mat-${idx}`}
diffuseTexture={
new BABYLON.Texture(
......@@ -95,8 +137,8 @@ const CHand = (props: { state: Hand; idx: number }) => {
)
}
/>
</plane>
</>
</animated.plane>
</animated.transformNode>
);
};
......
......@@ -7,7 +7,12 @@ import { store } from "../../store";
import { useAppSelector } from "../../hook";
import { useRef } from "react";
import { sendSelectPlaceResponse } from "../../api/ocgcore/ocgHelper";
import { clearMagicSelectInfo } from "../../reducers/duel/mod";
import {
clearMagicSelectInfo,
setCardModalImgUrl,
setCardModalIsOpen,
setCardModalText,
} from "../../reducers/duel/mod";
import { ygopro } from "../../api/ocgcore/idl/ocgcore";
// TODO: use config
......@@ -51,6 +56,16 @@ const CMagic = (props: { state: Magic }) => {
sendSelectPlaceResponse(state.selectInfo.response);
dispatch(clearMagicSelectInfo(0));
dispatch(clearMagicSelectInfo(1));
} else if (state.occupant) {
dispatch(
setCardModalText([state.occupant.text.name, state.occupant.text.desc])
);
dispatch(
setCardModalImgUrl(
`https://cdn02.moecube.com:444/images/ygopro-images-zh-CN/${state.occupant.id}.jpg`
)
);
dispatch(setCardModalIsOpen(true));
}
},
planeRef,
......
......@@ -6,7 +6,12 @@ import { Monster } from "../../reducers/duel/util";
import "react-babylonjs";
import { useRef } from "react";
import { sendSelectPlaceResponse } from "../../api/ocgcore/ocgHelper";
import { clearMonsterSelectInfo } from "../../reducers/duel/mod";
import {
clearMonsterSelectInfo,
setCardModalImgUrl,
setCardModalIsOpen,
setCardModalText,
} from "../../reducers/duel/mod";
import { useAppSelector } from "../../hook";
import { selectMeMonsters } from "../../reducers/duel/monstersSlice";
import { ygopro } from "../../api/ocgcore/idl/ocgcore";
......@@ -47,8 +52,8 @@ const CommonMonster = (props: { state: Monster }) => {
const faceDown =
props.state.position === ygopro.CardPosition.FACEDOWN_DEFENSE ||
ygopro.CardPosition.FACEDOWN_ATTACK ||
ygopro.CardPosition.FACEDOWN;
props.state.position === ygopro.CardPosition.FACEDOWN_ATTACK ||
props.state.position === ygopro.CardPosition.FACEDOWN;
useClick(
(_event) => {
......@@ -56,6 +61,19 @@ const CommonMonster = (props: { state: Monster }) => {
sendSelectPlaceResponse(props.state.selectInfo.response);
dispatch(clearMonsterSelectInfo(0));
dispatch(clearMonsterSelectInfo(1));
} else if (props.state.occupant) {
dispatch(
setCardModalText([
props.state.occupant.text.name,
props.state.occupant.text.desc,
])
);
dispatch(
setCardModalImgUrl(
`https://cdn02.moecube.com:444/images/ygopro-images-zh-CN/${props.state.occupant.id}.jpg`
)
);
dispatch(setCardModalIsOpen(true));
}
},
planeRef,
......
// Copyright (c) 2020 hooke
import { CSSProperties, ForwardRefExoticComponent, FC } from "react";
import { FluidValue } from "@react-spring/shared";
import {
AssignableKeys,
ComponentPropsWithRef,
ElementType,
} from "@react-spring/types";
import { Primitives } from "./primitives";
type AnimatedPrimitives = {
[P in Primitives]: AnimatedComponent<FC<JSX.IntrinsicElements[P]>>;
};
/** The type of the `animated()` function */
export type WithAnimated = {
<T extends ElementType>(wrappedComponent: T): AnimatedComponent<T>;
} & AnimatedPrimitives;
/** The type of an `animated()` component */
export type AnimatedComponent<T extends ElementType> =
ForwardRefExoticComponent<AnimatedProps<ComponentPropsWithRef<T>>>;
/** The props of an `animated()` component */
export type AnimatedProps<Props extends object> = {
[P in keyof Props]: P extends "ref" | "key"
? Props[P]
: AnimatedProp<Props[P]>;
};
// The animated prop value of a React element
type AnimatedProp<T> = [T, T] extends [infer T, infer DT]
? [DT] extends [never]
? never
: DT extends void
? undefined
: DT extends object
? [AssignableKeys<DT, CSSProperties>] extends [never]
? DT extends ReadonlyArray<any>
? AnimatedStyles<DT>
: DT
: AnimatedStyle<T>
: DT | AnimatedLeaf<T>
: never;
// An animated array of style objects
type AnimatedStyles<T extends ReadonlyArray<any>> = {
[P in keyof T]: [T[P]] extends [infer DT]
? DT extends object
? [AssignableKeys<DT, CSSProperties>] extends [never]
? DT extends ReadonlyArray<any>
? AnimatedStyles<DT>
: DT
: { [P in keyof DT]: AnimatedProp<DT[P]> }
: DT
: never;
};
// An animated object of style attributes
type AnimatedStyle<T> = [T, T] extends [infer T, infer DT]
? DT extends void
? undefined
: [DT] extends [never]
? never
: DT extends object
? { [P in keyof DT]: AnimatedStyle<DT[P]> }
: DT | AnimatedLeaf<T>
: never;
// An animated primitive (or an array of them)
type AnimatedLeaf<T> =
| Exclude<T, object | void>
| Extract<T, ReadonlyArray<number | string>> extends infer U
? [U] extends [never]
? never
: FluidValue<U | Exclude<T, object | void>>
: never;
// Copyright (c) 2020 hooke
import { Color3, Color4, Vector3 } from "@babylonjs/core";
import {
CustomPropsHandler,
ICustomPropsHandler,
PropChangeType,
PropertyUpdateProcessResult,
} from "react-babylonjs";
function parseRgbaString(rgba: string): number[] {
const arr: string[] = rgba.replace(/[^\d,]/g, "").split(",");
return arr.map((num) => parseInt(num, 10) / 255);
}
const Key = "react-babylon-spring";
export class CustomColor3StringHandler
implements ICustomPropsHandler<string, Color3>
{
get name() {
return `${Key}:Color3String`;
}
public propChangeType: string = PropChangeType.Color3;
accept(newProp: string): boolean {
return typeof newProp === "string";
}
process(
oldProp: string,
newProp: string
): PropertyUpdateProcessResult<Color3> {
if (oldProp !== newProp) {
return {
processed: true,
value: Color3.FromArray(parseRgbaString(newProp)),
};
}
return { processed: false, value: null };
}
}
export class CustomColor3ArrayHandler
implements ICustomPropsHandler<number[], Color3>
{
get name() {
return `${Key}:Color3Array`;
}
public propChangeType: string = PropChangeType.Color3;
accept(newProp: []): boolean {
return Array.isArray(newProp);
}
process(
oldProp: number[],
newProp: number[]
): PropertyUpdateProcessResult<Color3> {
if (oldProp === undefined || oldProp.length !== newProp.length) {
// console.log(`found diff length (${oldProp?.length}/${newProp?.length}) Color3Array new? ${oldProp === undefined}`)
return {
processed: true,
value: Color3.FromArray(newProp),
};
}
for (let i = 0; i < oldProp.length; i++) {
if (oldProp[i] !== newProp[i]) {
// console.log('found diff value Color3Array')
return {
processed: true,
value: Color3.FromArray(newProp),
};
}
}
return { processed: false, value: null };
}
}
export class CustomColor4StringHandler
implements ICustomPropsHandler<string, Color4>
{
get name() {
return `${Key}:Color4String`;
}
public propChangeType: string = PropChangeType.Color4;
accept(newProp: string): boolean {
return typeof newProp === "string";
}
process(
oldProp: string,
newProp: string
): PropertyUpdateProcessResult<Color4> {
if (oldProp !== newProp) {
// console.log('found diff Color4String')
return {
processed: true,
value: Color4.FromArray(parseRgbaString(newProp)),
};
}
return { processed: false, value: null };
}
}
export class CustomVector3ArrayHandler
implements ICustomPropsHandler<number[], Vector3>
{
get name() {
return `${Key}:Vector3Array`;
}
public propChangeType: string = PropChangeType.Vector3;
accept(newProp: []): boolean {
return Array.isArray(newProp);
}
process(
oldProp: number[],
newProp: number[]
): PropertyUpdateProcessResult<Vector3> {
if (oldProp === undefined || oldProp.length !== newProp.length) {
// console.log(`found diff length (${oldProp?.length}/${newProp?.length}) Color3Array new? ${oldProp === undefined}`)
return {
processed: true,
value: Vector3.FromArray(newProp),
};
}
for (let i = 0; i < oldProp.length; i++) {
if (oldProp[i] !== newProp[i]) {
return {
processed: true,
value: Vector3.FromArray(newProp),
};
}
}
return { processed: false, value: null };
}
}
CustomPropsHandler.RegisterPropsHandler(new CustomColor3StringHandler());
CustomPropsHandler.RegisterPropsHandler(new CustomColor3ArrayHandler());
CustomPropsHandler.RegisterPropsHandler(new CustomColor4StringHandler());
CustomPropsHandler.RegisterPropsHandler(new CustomVector3ArrayHandler());
// Copyright (c) 2020 hooke
import { Globals } from "@react-spring/core";
import { createHost } from "@react-spring/animated";
import { createStringInterpolator } from "@react-spring/shared";
import { applyInitialPropsToInstance } from "react-babylonjs";
import { primitives } from "./primitives";
import { WithAnimated } from "./animated";
import "./customProps";
// todo: frameLoop can use runRenderLoop
Globals.assign({
createStringInterpolator,
});
const host = createHost(primitives, {
applyAnimatedValues: applyInitialPropsToInstance,
});
export const animated = host.animated as WithAnimated;
export * from "./animated";
export * from "@react-spring/core";
// Copyright (c) 2020 hooke
import { intrinsicClassMap } from "react-babylonjs";
const elements = Object.keys(intrinsicClassMap);
/**
* https://github.com/react-spring/react-spring/blob/v9/targets/three/src/primitives.ts
*/
export type Primitives = keyof JSX.IntrinsicElements;
export const primitives = ["primitive"].concat(elements) as Primitives[];
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