Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
N
Neos
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
love_飞影
Neos
Commits
f15b790f
Commit
f15b790f
authored
Jul 09, 2023
by
Chunchi Che
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'dev/dropmenu' into 'main'
optimize: card dropdown menu See merge request
mycard/Neos!242
parents
2e712f62
9d4a6da6
Changes
29
Hide whitespace changes
Inline
Side-by-side
Showing
29 changed files
with
438 additions
and
364 deletions
+438
-364
src/config/defaults.ts
src/config/defaults.ts
+1
-1
src/env.d.ts
src/env.d.ts
+1
-1
src/service/duel/move.ts
src/service/duel/move.ts
+8
-6
src/stores/matStore/types.ts
src/stores/matStore/types.ts
+1
-1
src/stores/placeStore.ts
src/stores/placeStore.ts
+10
-28
src/styles/core.scss
src/styles/core.scss
+5
-1
src/ui/Duel/Main.tsx
src/ui/Duel/Main.tsx
+1
-2
src/ui/Duel/Message/HintNotification/index.tsx
src/ui/Duel/Message/HintNotification/index.tsx
+3
-3
src/ui/Duel/Message/SelectCardsModal/index.tsx
src/ui/Duel/Message/SelectCardsModal/index.tsx
+1
-1
src/ui/Duel/PlayMat/Card/index.scss
src/ui/Duel/PlayMat/Card/index.scss
+6
-3
src/ui/Duel/PlayMat/Card/index.tsx
src/ui/Duel/PlayMat/Card/index.tsx
+174
-158
src/ui/Duel/PlayMat/Card/springs/attack.ts
src/ui/Duel/PlayMat/Card/springs/attack.ts
+1
-1
src/ui/Duel/PlayMat/Card/springs/focus.ts
src/ui/Duel/PlayMat/Card/springs/focus.ts
+8
-3
src/ui/Duel/PlayMat/Card/springs/index.ts
src/ui/Duel/PlayMat/Card/springs/index.ts
+1
-0
src/ui/Duel/PlayMat/Card/springs/moveToDeck.ts
src/ui/Duel/PlayMat/Card/springs/moveToDeck.ts
+5
-4
src/ui/Duel/PlayMat/Card/springs/moveToGround.ts
src/ui/Duel/PlayMat/Card/springs/moveToGround.ts
+37
-26
src/ui/Duel/PlayMat/Card/springs/moveToHand.ts
src/ui/Duel/PlayMat/Card/springs/moveToHand.ts
+6
-6
src/ui/Duel/PlayMat/Card/springs/moveToOutside.ts
src/ui/Duel/PlayMat/Card/springs/moveToOutside.ts
+8
-8
src/ui/Duel/PlayMat/Card/springs/moveToToken.ts
src/ui/Duel/PlayMat/Card/springs/moveToToken.ts
+8
-0
src/ui/Duel/PlayMat/Card/springs/types.ts
src/ui/Duel/PlayMat/Card/springs/types.ts
+2
-0
src/ui/Duel/PlayMat/Card/springs/utils.ts
src/ui/Duel/PlayMat/Card/springs/utils.ts
+12
-1
src/ui/Duel/PlayMat/LifeBar/index.scss
src/ui/Duel/PlayMat/LifeBar/index.scss
+49
-5
src/ui/Duel/PlayMat/LifeBar/index.tsx
src/ui/Duel/PlayMat/LifeBar/index.tsx
+89
-21
src/ui/Duel/PlayMat/Menu/index.scss
src/ui/Duel/PlayMat/Menu/index.scss
+0
-22
src/ui/Duel/PlayMat/Menu/index.tsx
src/ui/Duel/PlayMat/Menu/index.tsx
+0
-1
src/ui/Duel/PlayMat/Timer/index.scss
src/ui/Duel/PlayMat/Timer/index.scss
+0
-25
src/ui/Duel/PlayMat/Timer/index.tsx
src/ui/Duel/PlayMat/Timer/index.tsx
+0
-34
src/ui/Duel/PlayMat/index.ts
src/ui/Duel/PlayMat/index.ts
+0
-1
src/ui/Duel/PlayMat/utils/cssConfig.ts
src/ui/Duel/PlayMat/utils/cssConfig.ts
+1
-1
No files found.
src/config/defaults.ts
View file @
f15b790f
...
...
@@ -19,7 +19,7 @@ const defaultConfig: DefaultsConfig = {
const
aiModeConfig
:
DefaultsConfig
=
{
...
defaultConfig
,
defaultDeck
:
VITE_AI_MODE_DEFAULT_DECK
||
"
Hero
"
,
defaultPlayer
:
`AiKiller
${
Math
.
random
().
toString
(
36
).
slice
(
2
)}
}`
,
defaultPlayer
:
`AiKiller
-
${
Math
.
random
().
toString
(
36
).
slice
(
2
,
6
)}
}`
,
defaultPassword
:
"
AI
"
,
};
...
...
src/env.d.ts
View file @
f15b790f
...
...
@@ -22,6 +22,6 @@ declare global {
color
:
(
color
:
string
,
backgroundColor
?:
string
)
=>
(...
args
:
any
[]
)
=>
void
;
)
=>
(...
args
:
Parameters
<
console
.
log
>
)
=>
void
;
}
}
src/service/duel/move.ts
View file @
f15b790f
...
...
@@ -157,14 +157,15 @@ export default async (move: MsgMove) => {
target
.
location
=
to
;
// 维护完了之后,开始动画
await
eventbus
.
call
(
Task
.
Move
,
target
.
uuid
);
await
eventbus
.
call
(
Task
.
Move
,
target
.
uuid
,
from
.
zone
);
// 如果from或者to是手卡,那么需要刷新除了这张卡之外,这个玩家的所有手卡
if
([
from
.
zone
,
to
.
zone
].
includes
(
HAND
))
{
for
(
const
card
of
cardStore
.
at
(
HAND
,
target
.
location
.
controller
))
{
if
(
card
.
uuid
!==
target
.
uuid
)
{
await
eventbus
.
call
(
Task
.
Move
,
card
.
uuid
);
}
}
await
Promise
.
all
(
cardStore
.
at
(
HAND
,
target
.
location
.
controller
)
.
filter
((
c
)
=>
c
.
uuid
!==
target
.
uuid
)
.
map
(
async
(
c
)
=>
await
eventbus
.
call
(
Task
.
Move
,
c
.
uuid
))
);
}
// 超量素材位置跟随超量怪兽移动
...
...
@@ -177,6 +178,7 @@ export default async (move: MsgMove) => {
overlay
.
location
.
zone
=
to
.
zone
;
overlay
.
location
.
controller
=
to
.
controller
;
overlay
.
location
.
sequence
=
to
.
sequence
;
overlay
.
location
.
position
=
to
.
position
;
await
eventbus
.
call
(
Task
.
Move
,
overlay
.
uuid
);
}
...
...
src/stores/matStore/types.ts
View file @
f15b790f
...
...
@@ -93,7 +93,7 @@ export interface HintState {
}
export
interface
PhaseState
{
currentPhase
:
ygopro
.
StocGameMessage
.
MsgNewPhase
.
PhaseType
;
// TODO 当前的阶段 应该改成enum
currentPhase
:
ygopro
.
StocGameMessage
.
MsgNewPhase
.
PhaseType
;
enableBp
:
boolean
;
// 允许进入战斗阶段
enableM2
:
boolean
;
// 允许进入M2阶段
enableEp
:
boolean
;
// 允许回合结束
...
...
src/stores/placeStore.ts
View file @
f15b790f
...
...
@@ -20,39 +20,21 @@ export interface BlockState {
disabled
:
boolean
;
// 是否被禁用
}
const
genPLaces
=
(
n
:
number
):
BlockState
[]
=>
Array
.
from
({
length
:
n
}).
map
(()
=>
({
interactivity
:
undefined
,
disabled
:
false
,
}));
export
const
placeStore
=
proxy
({
inner
:
{
[
MZONE
]:
{
me
:
Array
.
from
({
length
:
7
}).
map
(
()
=>
({
interactivity
:
undefined
,
disabled
:
false
,
}
as
BlockState
)
),
op
:
Array
.
from
({
length
:
7
}).
map
(
()
=>
({
interactivity
:
undefined
,
disabled
:
false
,
}
as
BlockState
)
),
me
:
genPLaces
(
7
),
op
:
genPLaces
(
7
),
},
[
SZONE
]:
{
me
:
Array
.
from
({
length
:
6
}).
map
(
()
=>
({
interactivity
:
undefined
,
disabled
:
false
,
}
as
BlockState
)
),
op
:
Array
.
from
({
length
:
6
}).
map
(
()
=>
({
interactivity
:
undefined
,
disabled
:
false
,
}
as
BlockState
)
),
me
:
genPLaces
(
6
),
op
:
genPLaces
(
6
),
},
},
set
(
...
...
src/styles/core.scss
View file @
f15b790f
...
...
@@ -41,7 +41,11 @@ body {
margin
:
0
;
place-items
:
center
;
min-width
:
320px
;
min-height
:
100vh
;
position
:
fixed
;
top
:
0
;
bottom
:
0
;
left
:
0
;
right
:
0
;
}
a
{
...
...
src/ui/Duel/Main.tsx
View file @
f15b790f
...
...
@@ -14,7 +14,7 @@ import {
SortCardModal
,
YesNoModal
,
}
from
"
./Message
"
;
import
{
LifeBar
,
Mat
,
Menu
,
Timer
}
from
"
./PlayMat
"
;
import
{
LifeBar
,
Mat
,
Menu
}
from
"
./PlayMat
"
;
const
NeosDuel
=
()
=>
{
return
(
...
...
@@ -23,7 +23,6 @@ const NeosDuel = () => {
<
Alert
/>
<
Menu
/>
<
LifeBar
/>
<
Timer
/>
<
Mat
/>
<
CardModal
/>
<
CardListModal
/>
...
...
src/ui/Duel/Message/HintNotification/index.tsx
View file @
f15b790f
...
...
@@ -10,9 +10,9 @@ import { useConfig } from "@/config";
import
{
HandResult
,
matStore
}
from
"
@/stores
"
;
const
style
=
{
borderStyle
:
"
groove
"
,
borderRadius
:
"
8px
"
,
backgroundColor
:
"
#
303030
"
,
//
borderStyle: "groove",
//
borderRadius: "8px",
backgroundColor
:
"
#
444
"
,
};
const
NeosConfig
=
useConfig
();
...
...
src/ui/Duel/Message/SelectCardsModal/index.tsx
View file @
f15b790f
...
...
@@ -30,7 +30,7 @@ const CheckCardStyle = {
};
const
CheckGroupStyle
=
{
display
:
"
grid
"
,
gridTemplateColumns
:
"
repeat(
6
, 1fr)
"
,
gridTemplateColumns
:
"
repeat(
5
, 1fr)
"
,
gap
:
10
,
};
...
...
src/ui/Duel/PlayMat/Card/index.scss
View file @
f15b790f
...
...
@@ -13,7 +13,7 @@ section#mat {
top
:
2%
;
height
:
96%
;
width
:
96%
;
transform
:
translateZ
(
calc
(
var
(
--
z
)
*
1px
+
0
.1px
))
transform
:
translateZ
(
calc
(
(
var
(
--
z
)
+
var
(
--
sub-z
)
)
*
1px
+
0
.1px
))
rotateY
(
calc
(
var
(
--
ry
)
*
1deg
));
transition
:
0
.2s
scale
;
cursor
:
pointer
;
...
...
@@ -107,9 +107,12 @@ section#mat {
}
.mat-card.highlight
.card-shadow
{
--card-shadow-color
:
#0099ff
;
display
:
block
!
important
;
background
:
linear-gradient
(
to
right
,
#0079c6
,
#009cff
)
!
important
;
filter
:
blur
(
8px
);
background
:
var
(
--
card-shadow-color
)
!
important
;
border-radius
:
5px
;
box-shadow
:
0
0
4px
0
var
(
--
card-shadow-color
)
,
0
0
25px
2px
#0099ff
87
;
transform
:
translateZ
(
calc
((
var
(
--
z
))
*
1px
+
0
.1px
));
}
@keyframes
focus
{
...
...
src/ui/Duel/PlayMat/Card/index.tsx
View file @
f15b790f
...
...
@@ -3,9 +3,10 @@ import "./index.scss";
import
{
animated
,
to
,
useSpring
}
from
"
@react-spring/web
"
;
import
{
Dropdown
,
type
MenuProps
}
from
"
antd
"
;
import
classnames
from
"
classnames
"
;
import
React
,
{
type
CSSProperties
,
useEffect
,
useState
}
from
"
react
"
;
import
{
proxy
,
useSnapshot
}
from
"
valtio
"
;
import
React
,
{
type
CSSProperties
,
useEffect
,
use
Ref
,
use
State
}
from
"
react
"
;
import
{
useSnapshot
}
from
"
valtio
"
;
import
type
{
CardMeta
}
from
"
@/api
"
;
import
{
fetchStrings
,
getCardStr
,
...
...
@@ -30,6 +31,7 @@ import {
moveToGround
,
moveToHand
,
moveToOutside
,
moveToToken
,
}
from
"
./springs
"
;
import
type
{
SpringApiProps
}
from
"
./springs/types
"
;
...
...
@@ -54,29 +56,31 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
focusScale
:
1
,
focusDisplay
:
"
none
"
,
focusOpacity
:
1
,
subZ
:
0
,
}
satisfies
SpringApiProps
)
);
const
move
=
async
(
zone
:
ygopro
.
CardZone
)
=>
{
switch
(
zone
)
{
// FIXME: move不应该只根据目的地判断,还要根据先前的位置判断。例子是Token。
const
move
=
async
(
toZone
:
ygopro
.
CardZone
,
fromZone
?:
ygopro
.
CardZone
)
=>
{
switch
(
toZone
)
{
case
MZONE
:
case
SZONE
:
await
moveToGround
({
card
:
state
,
api
});
await
moveToGround
({
card
:
state
,
api
,
fromZone
});
break
;
case
HAND
:
await
moveToHand
({
card
:
state
,
api
});
await
moveToHand
({
card
:
state
,
api
,
fromZone
});
break
;
case
DECK
:
case
EXTRA
:
await
moveToDeck
({
card
:
state
,
api
});
await
moveToDeck
({
card
:
state
,
api
,
fromZone
});
break
;
case
GRAVE
:
case
REMOVED
:
await
moveToOutside
({
card
:
state
,
api
});
await
moveToOutside
({
card
:
state
,
api
,
fromZone
});
break
;
case
TZONE
:
// FIXME: 这里应该实现一个衍生物消散的动画,现在暂时让它在动画在展示上回到卡组
await
moveTo
Deck
({
card
:
state
,
api
});
await
moveTo
Token
({
card
:
state
,
api
,
fromZone
});
break
;
}
};
...
...
@@ -92,19 +96,22 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
// >>> 动画 >>>
/** 动画序列的promise */
let
animationQueue
:
Promise
<
unknown
>
=
new
Promise
<
void
>
((
rs
)
=>
rs
(
));
const
animationQueue
=
useRef
(
new
Promise
<
void
>
((
rs
)
=>
rs
()
));
const
addToAnimation
=
(
p
:
()
=>
Promise
<
void
>
)
=>
new
Promise
((
rs
)
=>
{
animationQueue
=
animationQueue
.
then
(
p
).
then
(
rs
);
animationQueue
.
current
=
animationQueue
.
current
.
then
(
p
).
then
(
rs
);
});
useEffect
(()
=>
{
eventbus
.
register
(
Task
.
Move
,
async
(
uuid
:
string
)
=>
{
if
(
uuid
===
state
.
uuid
)
{
await
addToAnimation
(()
=>
move
(
state
.
location
.
zone
));
eventbus
.
register
(
Task
.
Move
,
async
(
uuid
:
string
,
fromZone
?:
ygopro
.
CardZone
)
=>
{
if
(
uuid
===
state
.
uuid
)
{
await
addToAnimation
(()
=>
move
(
state
.
location
.
zone
,
fromZone
));
}
}
}
);
);
eventbus
.
register
(
Task
.
Focus
,
async
(
uuid
:
string
)
=>
{
if
(
uuid
===
state
.
uuid
)
{
...
...
@@ -139,7 +146,152 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
useEffect
(()
=>
{
setHighlight
(
!!
idleInteractivities
.
length
);
},
[
idleInteractivities
]);
const
menuItems
=
useSnapshot
(
dropdownMenu
);
const
[
dropdownMenu
,
setDropdownMenu
]
=
useState
({
items
:
[]
as
DropdownItem
[],
});
// 发动效果
// 1. 下拉菜单里面选择[召唤 / 特殊召唤 /.../效果发动]
// 2. 如果是非效果发动,那么直接选择哪张卡(单张卡直接选择那张)
// 3. 如果是效果发动,那么选择哪张卡,然后选择效果
const
handleDropdownMenu
=
(
cards
:
CardType
[],
isField
:
boolean
)
=>
{
const
map
=
new
Map
<
Interactivity
<
number
>
[
"
interactType
"
],
CardType
[]
>
();
cards
.
forEach
((
card
)
=>
{
card
.
idleInteractivities
.
forEach
(({
interactType
})
=>
{
if
(
!
map
.
has
(
interactType
))
{
map
.
set
(
interactType
,
[]);
}
map
.
get
(
interactType
)?.
push
(
card
);
});
});
const
actions
=
[...
map
.
entries
()];
const
nonEffectActions
=
actions
.
filter
(
([
action
])
=>
action
!==
InteractType
.
ACTIVATE
);
const
getNonEffectResponse
=
(
action
:
InteractType
,
card
:
CardType
)
=>
card
.
idleInteractivities
.
find
((
item
)
=>
item
.
interactType
===
action
)
!
.
response
;
const
nonEffectItem
:
DropdownItem
[]
=
nonEffectActions
.
map
(
([
action
,
cards
],
key
)
=>
({
key
,
label
:
interactTypeToString
(
action
),
onClick
:
async
()
=>
{
if
(
!
isField
)
{
// 单卡: 直接召唤/特殊召唤/...
const
card
=
cards
[
0
];
sendSelectIdleCmdResponse
(
getNonEffectResponse
(
action
,
card
));
}
else
{
// 场地: 选择卡片
// TODO: hint
const
option
=
await
displaySimpleSelectCardsModal
({
selectables
:
cards
.
map
((
card
)
=>
({
meta
:
card
.
meta
,
location
:
card
.
location
,
response
:
getNonEffectResponse
(
action
,
card
),
})),
});
sendSelectIdleCmdResponse
(
option
[
0
].
response
!
);
}
},
})
);
const
hasEffect
=
cards
.
reduce
(
(
prev
,
acc
)
=>
[
...
prev
,
...
acc
.
idleInteractivities
.
filter
(
({
interactType
})
=>
interactType
===
InteractType
.
ACTIVATE
),
],
[]
as
Interactivity
<
number
>
[]
).
length
>
0
;
const
effectItem
:
DropdownItem
=
{
key
:
nonEffectItem
.
length
,
label
:
interactTypeToString
(
InteractType
.
ACTIVATE
),
onClick
:
async
()
=>
{
let
card
:
CardType
;
if
(
!
isField
)
{
// 单卡: 直接发动这个卡的效果
card
=
cards
[
0
];
}
else
{
// 场地: 选择卡片
// TODO: hint
const
option
=
await
displaySimpleSelectCardsModal
({
selectables
:
cards
// 过滤掉不能发效果的卡
.
filter
(
(
card
)
=>
card
.
idleInteractivities
.
find
(
({
interactType
})
=>
interactType
===
InteractType
.
ACTIVATE
)
!==
undefined
)
.
map
((
card
)
=>
({
meta
:
card
.
meta
,
location
:
card
.
location
,
card
,
})),
});
card
=
option
[
0
].
card
!
as
any
;
// 一定会有的,有输入则定有输出
}
// 选择发动哪个效果
handleEffectActivation
(
card
.
idleInteractivities
.
filter
(
({
interactType
})
=>
interactType
===
InteractType
.
ACTIVATE
)
.
map
((
x
)
=>
({
desc
:
interactTypeToString
(
x
.
interactType
),
response
:
x
.
response
,
effectCode
:
x
.
activateIndex
,
})),
card
.
meta
);
},
};
const
items
=
[...
nonEffectItem
];
hasEffect
&&
items
.
push
(
effectItem
);
setDropdownMenu
({
items
});
};
const
onClick
=
()
=>
{
const
onCardClick
=
(
card
:
CardType
)
=>
{
// 中央弹窗展示选中卡牌信息
// TODO: 同一张卡片,是否重复点击会关闭CardModal?
displayCardModal
(
card
);
if
(
card
.
idleInteractivities
.
length
)
handleDropdownMenu
([
card
],
false
);
// 侧边栏展示超量素材信息
const
overlayMaterials
=
cardStore
.
findOverlay
(
card
.
location
.
zone
,
card
.
location
.
controller
,
card
.
location
.
sequence
);
if
(
overlayMaterials
.
length
>
0
)
{
displayCardListModal
({
isZone
:
false
,
monster
:
card
,
});
}
};
const
onFieldClick
=
(
card
:
CardType
)
=>
{
displayCardListModal
({
isZone
:
true
,
zone
:
card
.
location
.
zone
,
controller
:
card
.
location
.
controller
,
});
// 收集这个zone的所有交互,并且在下拉菜单之中显示
const
cards
=
cardStore
.
at
(
card
.
location
.
zone
,
card
.
location
.
controller
);
handleDropdownMenu
(
cards
,
true
);
};
if
([
MZONE
,
SZONE
,
HAND
].
includes
(
state
.
location
.
zone
))
{
onCardClick
(
state
);
}
else
if
([
EXTRA
,
GRAVE
,
REMOVED
].
includes
(
state
.
location
.
zone
))
{
onFieldClick
(
state
);
}
};
// <<< 效果 <<<
return
(
...
...
@@ -153,6 +305,7 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
`translate(${x}px, ${y}px) rotateX(${rx}deg) rotateZ(${rz}deg)`
),
"
--z
"
:
styles
.
z
,
"
--sub-z
"
:
styles
.
subZ
.
to
([
0
,
50
,
100
],
[
0
,
200
,
0
]),
// 中间高,两边低
"
--ry
"
:
styles
.
ry
,
height
:
styles
.
height
,
zIndex
:
styles
.
zIndex
,
...
...
@@ -161,23 +314,17 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
"
--focus-opacity
"
:
styles
.
focusOpacity
,
}
as
any
as
CSSProperties
}
onClick=
{
()
=>
{
if
([
MZONE
,
SZONE
,
HAND
].
includes
(
state
.
location
.
zone
))
{
onCardClick
(
state
);
}
else
if
([
EXTRA
,
GRAVE
,
REMOVED
].
includes
(
state
.
location
.
zone
))
{
onFieldClick
(
state
);
}
}
}
onClick=
{
onClick
}
>
<
div
className=
"card-focus"
/>
<
div
className=
"card-shadow"
/>
<
Dropdown
menu=
{
{
items
:
menuItems
.
value
as
any
}
}
placement=
"
bottom
"
menu=
{
dropdownMenu
}
placement=
"
top
"
overlayClassName=
"card-dropdown"
arrow
trigger=
{
[
"
click
"
]
}
// disabled={!highlight}
// disabled={!highlight}
// TODO: 这里的disable要考虑到field的情况,比如额外卡组
>
<
div
className=
{
classnames
(
"
card-img-wrap
"
,
{
focus
:
classFocus
})
}
>
<
YgoCard
...
...
@@ -192,37 +339,6 @@ export const Card: React.FC<{ idx: number }> = React.memo(({ idx }) => {
);
});
const
onCardClick
=
(
card
:
CardType
)
=>
{
// 中央弹窗展示选中卡牌信息
// TODO: 同一张卡片,是否重复点击会关闭CardModal?
displayCardModal
(
card
);
if
(
card
.
idleInteractivities
)
handleDropdownMenu
([
card
],
false
);
// 侧边栏展示超量素材信息
const
overlayMaterials
=
cardStore
.
findOverlay
(
card
.
location
.
zone
,
card
.
location
.
controller
,
card
.
location
.
sequence
);
if
(
overlayMaterials
.
length
>
0
)
{
displayCardListModal
({
isZone
:
false
,
monster
:
card
,
});
}
};
const
onFieldClick
=
(
card
:
CardType
)
=>
{
displayCardListModal
({
isZone
:
true
,
zone
:
card
.
location
.
zone
,
controller
:
card
.
location
.
controller
,
});
// 收集这个zone的所有交互,并且在下拉菜单之中显示
const
cards
=
cardStore
.
at
(
card
.
location
.
zone
,
card
.
location
.
controller
);
handleDropdownMenu
(
cards
,
true
);
};
// >>> 下拉菜单:点击动作 >>>
interface
Interactivy
{
desc
:
string
;
...
...
@@ -233,11 +349,10 @@ interface Interactivy {
type
DropdownItem
=
NonNullable
<
MenuProps
[
"
items
"
]
>
[
number
]
&
{
onClick
:
()
=>
void
;
};
const
dropdownMenu
=
proxy
({
value
:
[]
as
DropdownItem
[]
});
const
handleEffectActivation
=
(
effectInteractivies
:
Interactivy
[],
meta
?:
any
// FIXME: meta的类型
meta
?:
CardMeta
)
=>
{
if
(
!
effectInteractivies
.
length
)
return
;
else
if
(
effectInteractivies
.
length
===
1
)
{
...
...
@@ -259,103 +374,4 @@ const handleEffectActivation = (
}
};
// 发动效果
// 1. 下拉菜单里面选择[召唤 / 特殊召唤 /.../效果发动]
// 2. 如果是非效果发动,那么直接选择哪张卡(单张卡直接选择那张)
// 3. 如果是效果发动,那么选择哪张卡,然后选择效果
const
handleDropdownMenu
=
(
cards
:
CardType
[],
isField
:
boolean
)
=>
{
const
map
=
new
Map
<
Interactivity
<
number
>
[
"
interactType
"
],
CardType
[]
>
();
cards
.
forEach
((
card
)
=>
{
card
.
idleInteractivities
.
forEach
(({
interactType
})
=>
{
if
(
!
map
.
has
(
interactType
))
{
map
.
set
(
interactType
,
[]);
}
map
.
get
(
interactType
)?.
push
(
card
);
});
});
const
actions
=
[...
map
.
entries
()];
const
nonEffectActions
=
actions
.
filter
(
([
action
])
=>
action
!==
InteractType
.
ACTIVATE
);
const
getNonEffectResponse
=
(
action
:
InteractType
,
card
:
CardType
)
=>
card
.
idleInteractivities
.
find
((
item
)
=>
item
.
interactType
===
action
)
!
.
response
;
const
nonEffectItem
:
DropdownItem
[]
=
nonEffectActions
.
map
(
([
action
,
cards
],
key
)
=>
({
key
,
label
:
interactTypeToString
(
action
),
onClick
:
async
()
=>
{
if
(
!
isField
)
{
// 单卡: 直接召唤/特殊召唤/...
const
card
=
cards
[
0
];
sendSelectIdleCmdResponse
(
getNonEffectResponse
(
action
,
card
));
}
else
{
// 场地: 选择卡片
// TODO: hint
const
option
=
await
displaySimpleSelectCardsModal
({
selectables
:
cards
.
map
((
card
)
=>
({
meta
:
card
.
meta
,
location
:
card
.
location
,
response
:
getNonEffectResponse
(
action
,
card
),
})),
});
sendSelectIdleCmdResponse
(
option
[
0
].
response
!
);
}
},
})
);
const
hasEffect
=
cards
.
reduce
(
(
prev
,
acc
)
=>
[
...
prev
,
...
acc
.
idleInteractivities
.
filter
(
({
interactType
})
=>
interactType
===
InteractType
.
ACTIVATE
),
],
[]
as
Interactivity
<
number
>
[]
).
length
>
0
;
const
effectItem
:
DropdownItem
=
{
key
:
nonEffectItem
.
length
,
label
:
interactTypeToString
(
InteractType
.
ACTIVATE
),
onClick
:
async
()
=>
{
let
card
:
CardType
;
if
(
!
isField
)
{
// 单卡: 直接发动这个卡的效果
card
=
cards
[
0
];
}
else
{
// 场地: 选择卡片
// TODO: hint
const
option
=
await
displaySimpleSelectCardsModal
({
selectables
:
cards
// 过滤掉不能发效果的卡
.
filter
(
(
card
)
=>
card
.
idleInteractivities
.
find
(
({
interactType
})
=>
interactType
===
InteractType
.
ACTIVATE
)
!==
undefined
)
.
map
((
card
)
=>
({
meta
:
card
.
meta
,
location
:
card
.
location
,
card
,
})),
});
card
=
option
[
0
].
card
!
as
any
;
// 一定会有的,有输入则定有输出
}
// 选择发动哪个效果
handleEffectActivation
(
card
.
idleInteractivities
.
filter
(({
interactType
})
=>
interactType
===
InteractType
.
ACTIVATE
)
.
map
((
x
)
=>
({
desc
:
interactTypeToString
(
x
.
interactType
),
response
:
x
.
response
,
effectCode
:
x
.
activateIndex
,
})),
card
.
meta
);
},
};
dropdownMenu
.
value
=
nonEffectItem
;
hasEffect
&&
dropdownMenu
.
value
.
push
(
effectItem
);
};
// <<< 下拉菜单 <<<
src/ui/Duel/PlayMat/Card/springs/attack.ts
View file @
f15b790f
...
...
@@ -5,7 +5,7 @@ import { ygopro } from "@/api";
import
{
CardType
,
isMe
}
from
"
@/stores
"
;
import
{
matConfig
}
from
"
../../utils
"
;
import
{
SpringApi
}
from
"
./types
"
;
import
type
{
SpringApi
}
from
"
./types
"
;
import
{
asyncStart
}
from
"
./utils
"
;
const
{
BLOCK_WIDTH
,
BLOCK_HEIGHT_M
,
BLOCK_HEIGHT_S
,
COL_GAP
,
ROW_GAP
}
=
...
...
src/ui/Duel/PlayMat/Card/springs/focus.ts
View file @
f15b790f
import
{
ygopro
}
from
"
@/api
"
;
import
{
type
CardType
,
matStore
}
from
"
@/stores
"
;
import
{
SpringApi
}
from
"
./types
"
;
import
type
{
SpringApi
}
from
"
./types
"
;
import
{
asyncStart
}
from
"
./utils
"
;
/** 发动效果的动画 */
...
...
@@ -13,11 +13,16 @@ export const focus = async (props: { card: CardType; api: SpringApi }) => {
)
{
const
current
=
api
.
current
[
0
].
get
();
await
asyncStart
(
api
)({
y
:
current
.
y
+
(
matStore
.
isMe
(
card
.
location
.
controller
)
?
-
1
:
1
)
*
20
0
,
// TODO: 放到config之中
y
:
current
.
y
+
(
matStore
.
isMe
(
card
.
location
.
controller
)
?
-
1
:
1
)
*
12
0
,
// TODO: 放到config之中
ry
:
0
,
rz
:
0
,
});
await
asyncStart
(
api
)({
y
:
current
.
y
,
ry
:
current
.
ry
,
rz
:
current
.
rz
});
await
asyncStart
(
api
)({
y
:
current
.
y
,
ry
:
current
.
ry
,
rz
:
current
.
rz
,
z
:
current
.
z
,
});
}
else
{
await
asyncStart
(
api
)({
focusScale
:
1.5
,
...
...
src/ui/Duel/PlayMat/Card/springs/index.ts
View file @
f15b790f
...
...
@@ -4,3 +4,4 @@ export * from "./moveToDeck";
export
*
from
"
./moveToGround
"
;
export
*
from
"
./moveToHand
"
;
export
*
from
"
./moveToOutside
"
;
export
*
from
"
./moveToToken
"
;
src/ui/Duel/PlayMat/Card/springs/moveToDeck.ts
View file @
f15b790f
import
{
ygopro
}
from
"
@/api
"
;
import
{
type
CardType
,
isMe
}
from
"
@/stores
"
;
import
{
isMe
}
from
"
@/stores
"
;
import
{
matConfig
}
from
"
../../utils
"
;
import
{
SpringApi
}
from
"
./type
s
"
;
import
{
asyncStart
,
type
MoveFunc
}
from
"
./util
s
"
;
const
{
BLOCK_WIDTH
,
...
...
@@ -18,7 +18,7 @@ const {
const
{
DECK
,
EXTRA
}
=
ygopro
.
CardZone
;
export
const
moveToDeck
=
async
(
props
:
{
card
:
CardType
;
api
:
SpringApi
}
)
=>
{
export
const
moveToDeck
:
MoveFunc
=
async
(
props
)
=>
{
const
{
card
,
api
}
=
props
;
// report
const
{
location
}
=
card
;
...
...
@@ -41,7 +41,8 @@ export const moveToDeck = async (props: { card: CardType; api: SpringApi }) => {
let
rz
=
zone
===
EXTRA
?
DECK_ROTATE_Z
.
value
:
-
DECK_ROTATE_Z
.
value
;
rz
+=
isMe
(
controller
)
?
0
:
180
;
const
z
=
sequence
;
api
.
start
({
await
asyncStart
(
api
)({
x
,
y
,
z
,
...
...
src/ui/Duel/PlayMat/Card/springs/moveToGround.ts
View file @
f15b790f
import
{
easings
}
from
"
@react-spring/web
"
;
import
{
ygopro
}
from
"
@/api
"
;
import
{
type
CardType
,
isMe
}
from
"
@/stores
"
;
import
{
isMe
}
from
"
@/stores
"
;
import
{
matConfig
}
from
"
../../utils
"
;
import
{
SpringApi
}
from
"
./types
"
;
import
{
asyncStart
}
from
"
./utils
"
;
import
{
asyncStart
,
type
MoveFunc
}
from
"
./utils
"
;
const
{
BLOCK_WIDTH
,
...
...
@@ -16,13 +15,10 @@ const {
ROW_GAP
,
}
=
matConfig
;
const
{
MZONE
,
SZONE
}
=
ygopro
.
CardZone
;
const
{
MZONE
,
SZONE
,
TZONE
}
=
ygopro
.
CardZone
;
export
const
moveToGround
=
async
(
props
:
{
card
:
CardType
;
api
:
SpringApi
;
})
=>
{
const
{
card
,
api
}
=
props
;
export
const
moveToGround
:
MoveFunc
=
async
(
props
)
=>
{
const
{
card
,
api
,
fromZone
}
=
props
;
const
{
location
}
=
card
;
...
...
@@ -84,26 +80,41 @@ export const moveToGround = async (props: {
let
rz
=
isMe
(
controller
)
?
0
:
180
;
rz
+=
defence
?
90
:
0
;
const
ry
=
[
ygopro
.
CardPosition
.
FACEDOWN
,
ygopro
.
CardPosition
.
FACEDOWN_ATTACK
,
ygopro
.
CardPosition
.
FACEDOWN_DEFENSE
,
].
includes
(
position
??
5
)
?
180
:
0
;
// 动画
if
(
fromZone
===
TZONE
)
{
// 如果是Token,直接先移动到那个位置,然后再放大
api
.
set
({
x
,
y
,
ry
,
rz
,
height
:
0
,
});
}
else
{
await
asyncStart
(
api
)({
x
,
y
,
height
,
z
:
is_overlay
?
120
:
200
,
ry
,
rz
,
config
:
{
// mass: 0.5,
easing
:
easings
.
easeInOutSine
,
},
});
}
await
asyncStart
(
api
)({
x
,
y
,
height
,
z
:
is_overlay
?
120
:
200
,
ry
:
[
ygopro
.
CardPosition
.
FACEDOWN
,
ygopro
.
CardPosition
.
FACEDOWN_ATTACK
,
ygopro
.
CardPosition
.
FACEDOWN_DEFENSE
,
].
includes
(
position
??
5
)
?
180
:
0
,
rz
,
config
:
{
// mass: 0.5,
easing
:
easings
.
easeInOutSine
,
},
});
await
asyncStart
(
api
)({
z
:
0
,
zIndex
:
is_overlay
?
1
:
3
,
config
:
{
...
...
src/ui/Duel/PlayMat/Card/springs/moveToHand.ts
View file @
f15b790f
import
{
ygopro
}
from
"
@/api
"
;
import
{
cardStore
,
type
CardType
,
isMe
}
from
"
@/stores
"
;
import
{
cardStore
,
isMe
}
from
"
@/stores
"
;
import
{
matConfig
}
from
"
../../utils
"
;
import
{
SpringApi
}
from
"
./type
s
"
;
import
{
asyncStart
,
type
MoveFunc
}
from
"
./util
s
"
;
const
{
BLOCK_HEIGHT_M
,
...
...
@@ -16,7 +16,7 @@ const {
const
{
HAND
}
=
ygopro
.
CardZone
;
export
const
moveToHand
=
async
(
props
:
{
card
:
CardType
;
api
:
SpringApi
}
)
=>
{
export
const
moveToHand
:
MoveFunc
=
async
(
props
)
=>
{
const
{
card
,
api
}
=
props
;
const
{
sequence
,
controller
}
=
card
.
location
;
// 手卡会有很复杂的计算...
...
...
@@ -44,14 +44,14 @@ export const moveToHand = async (props: { card: CardType; api: SpringApi }) => {
const
negativeX
=
Math
.
sin
(
angle
)
*
r
;
const
negativeY
=
Math
.
cos
(
angle
)
*
r
+
HAND_CARD_HEIGHT
.
value
/
2
;
const
x
=
hand_circle_center_x
+
negativeX
*
(
isMe
(
controller
)
?
1
:
-
1
);
const
y
=
hand_circle_center_y
-
negativeY
+
140
;
// 常量 是手动调的 这里肯定有问题 有空来修
const
y
=
hand_circle_center_y
-
negativeY
+
140
;
//
FIXME:
常量 是手动调的 这里肯定有问题 有空来修
const
_rz
=
(
angle
*
180
)
/
Math
.
PI
;
a
pi
.
start
({
a
wait
asyncStart
(
api
)
({
x
:
isMe
(
controller
)
?
x
:
-
x
,
y
:
isMe
(
controller
)
?
y
:
-
y
,
z
:
0
,
z
:
sequence
+
5
,
rz
:
isMe
(
controller
)
?
_rz
:
180
-
_rz
,
ry
:
isMe
(
controller
)
?
0
:
180
,
height
:
HAND_CARD_HEIGHT
.
value
,
...
...
src/ui/Duel/PlayMat/Card/springs/moveToOutside.ts
View file @
f15b790f
import
{
ygopro
}
from
"
@/api
"
;
import
{
type
CardType
,
isMe
}
from
"
@/stores
"
;
import
{
isMe
}
from
"
@/stores
"
;
import
{
matConfig
}
from
"
../../utils
"
;
import
{
SpringApi
}
from
"
./type
s
"
;
import
{
asyncStart
,
type
MoveFunc
}
from
"
./util
s
"
;
const
{
BLOCK_WIDTH
,
BLOCK_HEIGHT_M
,
BLOCK_HEIGHT_S
,
COL_GAP
,
ROW_GAP
}
=
matConfig
;
const
{
GRAVE
}
=
ygopro
.
CardZone
;
export
const
moveToOutside
=
async
(
props
:
{
card
:
CardType
;
api
:
SpringApi
;
})
=>
{
export
const
moveToOutside
:
MoveFunc
=
async
(
props
)
=>
{
const
{
card
,
api
}
=
props
;
// report
const
{
zone
,
controller
,
position
}
=
card
.
location
;
const
{
zone
,
controller
,
position
,
sequence
}
=
card
.
location
;
let
x
=
(
BLOCK_WIDTH
.
value
+
COL_GAP
.
value
)
*
3
,
y
=
zone
===
GRAVE
?
BLOCK_HEIGHT_M
.
value
+
ROW_GAP
.
value
:
0
;
...
...
@@ -23,12 +20,15 @@ export const moveToOutside = async (props: {
x
=
-
x
;
y
=
-
y
;
}
a
pi
.
start
({
a
wait
asyncStart
(
api
)
({
x
,
y
,
z
:
0
,
height
:
BLOCK_HEIGHT_S
.
value
,
rz
:
isMe
(
controller
)
?
0
:
180
,
ry
:
[
ygopro
.
CardPosition
.
FACEDOWN
].
includes
(
position
)
?
180
:
0
,
subZ
:
100
,
zIndex
:
sequence
,
});
api
.
set
({
subZ
:
0
});
};
src/ui/Duel/PlayMat/Card/springs/moveToToken.ts
0 → 100644
View file @
f15b790f
import
{
asyncStart
,
type
MoveFunc
}
from
"
./utils
"
;
export
const
moveToToken
:
MoveFunc
=
async
(
props
)
=>
{
const
{
api
}
=
props
;
await
asyncStart
(
api
)({
height
:
0
,
});
};
src/ui/Duel/PlayMat/Card/springs/types.ts
View file @
f15b790f
...
...
@@ -14,6 +14,8 @@ export interface SpringApiProps {
focusDisplay
:
string
;
focusOpacity
:
number
;
// <<< focus
subZ
:
number
;
// 0 -> 100,这是为了让卡片移动过程中,稍微上浮一些,避免一些奇怪的遮挡问题
}
export
type
SpringApi
=
SpringRef
<
SpringApiProps
>
;
src/ui/Duel/PlayMat/Card/springs/utils.ts
View file @
f15b790f
import
{
type
SpringConfig
,
type
SpringRef
}
from
"
@react-spring/web
"
;
import
type
{
ygopro
}
from
"
@/api
"
;
import
{
type
CardType
}
from
"
@/stores
"
;
import
type
{
SpringApi
}
from
"
./types
"
;
export
const
asyncStart
=
<
T
extends
{}
>
(
api
:
SpringRef
<
T
>
)
=>
{
return
(
p
:
Partial
<
T
>
&
{
config
?:
SpringConfig
})
=>
new
Promise
((
resolve
)
=>
{
api
.
start
({
...
p
,
onRes
t
:
resolve
,
onRes
olve
:
resolve
,
});
});
};
export
type
MoveFunc
=
(
props
:
{
card
:
CardType
;
api
:
SpringApi
;
fromZone
?:
ygopro
.
CardZone
;
})
=>
Promise
<
void
>
;
src/ui/Duel/PlayMat/LifeBar/index.scss
View file @
f15b790f
...
...
@@ -3,20 +3,24 @@
display
:
flex
;
top
:
0
;
left
:
0
;
height
:
100vh
;
// FIXME: 100% on safari
bottom
:
0
;
flex-direction
:
column
;
justify-content
:
space-between
;
padding
:
20px
35px
;
padding
:
20px
;
margin-left
:
10px
;
pointer-events
:
none
;
z-index
:
100
;
--bg-color
:
#323232
;
width
:
200px
;
}
.life-bar
{
width
:
160px
;
position
:
relative
;
overflow
:
hidden
;
width
:
100%
;
color
:
white
;
background-color
:
#323232
;
background-color
:
var
(
--
bg-color
)
;
font-family
:
var
(
--
theme-font
);
border
:
1px
solid
#222
;
padding
:
1rem
;
padding-bottom
:
0
.6rem
;
border-radius
:
8px
;
...
...
@@ -32,3 +36,43 @@
font-size
:
1
.8rem
;
}
}
.timer-container
{
background-color
:
var
(
--
bg-color
);
border-radius
:
4px
;
padding
:
0
.4rem
1rem
;
font-size
:
0
.8rem
;
font-weight
:
bold
;
font-family
:
var
(
--
theme-font
);
width
:
fit-content
;
color
:
white
;
display
:
flex
;
gap
:
8px
;
align-items
:
center
;
overflow
:
hidden
;
position
:
relative
;
.timer-text
{
min-width
:
3
.25em
;
}
}
.floodlight
{
position
:
absolute
;
height
:
100%
;
width
:
40px
;
background-color
:
#aaa
;
top
:
0
;
right
:
0
;
filter
:
blur
(
30px
);
transform
:
skewX
(
-20deg
);
}
.floodlight-run
{
animation
:
floodlight
4s
linear
infinite
;
}
@keyframes
floodlight
{
0
%
{
right
:
-80px
;
}
100
%
{
right
:
calc
(
100%
+
80px
);
}
}
src/ui/Duel/PlayMat/LifeBar/index.tsx
View file @
f15b790f
import
"
./index.scss
"
;
import
{
Progress
}
from
"
antd
"
;
import
classNames
from
"
classnames
"
;
import
React
,
{
useEffect
}
from
"
react
"
;
import
React
,
{
useEffect
,
useState
}
from
"
react
"
;
import
AnimatedNumbers
from
"
react-animated-numbers
"
;
import
{
useSnapshot
}
from
"
valtio
"
;
import
{
useEnv
}
from
"
@/hook
"
;
import
{
matStore
,
playerStore
}
from
"
@/stores
"
;
// 三个候选方案
// https://snack.expo.dev/?platform=web
// https://github.com/heyman333/react-animated-numbers
// https://www.npmjs.com/package/react-countup?activeTab=dependents
export
const
LifeBar
:
React
.
FC
=
()
=>
{
const
snap
=
useSnapshot
(
matStore
.
initInfo
);
const
snap
InitInfo
=
useSnapshot
(
matStore
.
initInfo
);
const
snapPlayer
=
useSnapshot
(
playerStore
);
const
{
currentPlayer
}
=
useSnapshot
(
matStore
);
...
...
@@ -21,35 +22,102 @@ export const LifeBar: React.FC = () => {
const
[
opLife
,
setOpLife
]
=
React
.
useState
(
0
);
useEffect
(()
=>
{
setMeLife
(
snap
.
me
.
life
);
},
[
snap
.
me
.
life
]);
setMeLife
(
snapInitInfo
.
me
.
life
);
},
[
snapInitInfo
.
me
.
life
]);
useEffect
(()
=>
{
setOpLife
(
snapInitInfo
.
op
.
life
);
},
[
snapInitInfo
.
op
.
life
]);
const
snapTimeLimit
=
useSnapshot
(
matStore
.
timeLimits
);
const
[
myTimeLimit
,
setMyTimeLimit
]
=
useState
(
snapTimeLimit
.
me
);
const
[
opTimeLimit
,
setOpTimeLimit
]
=
useState
(
snapTimeLimit
.
op
);
useEffect
(()
=>
{
setMyTimeLimit
(
snapTimeLimit
.
me
);
},
[
snapTimeLimit
.
me
]);
useEffect
(()
=>
{
setOpTimeLimit
(
snapTimeLimit
.
op
);
},
[
snapTimeLimit
.
op
]);
useEffect
(()
=>
{
setInterval
(()
=>
{
setMyTimeLimit
((
time
)
=>
time
-
1
);
setOpTimeLimit
((
time
)
=>
time
-
1
);
},
1000
);
},
[]);
useEffect
(()
=>
{
setOpLife
(
snap
.
op
.
life
);
},
[
snap
.
op
.
life
]);
if
(
useEnv
().
VITE_IS_AI_MODE
)
{
// 如果是AI模式
// FIXME: 探索一个优雅的、判断当前是不是AI模式的方法,用户手动输入AI也是AI模式
setMyTimeLimit
(
240
);
setOpTimeLimit
(
240
);
}
},
[
currentPlayer
]);
return
(
<
div
id=
"life-bar-container"
>
<
LifeBarItem
active=
{
!
matStore
.
isMe
(
currentPlayer
)
}
name=
{
snapPlayer
.
getOpPlayer
().
name
??
"
?
"
}
life=
{
opLife
}
timeLimit=
{
opTimeLimit
}
isMe=
{
false
}
/>
<
LifeBarItem
active=
{
matStore
.
isMe
(
currentPlayer
)
}
name=
{
snapPlayer
.
getMePlayer
().
name
??
"
?
"
}
life=
{
meLife
}
timeLimit=
{
myTimeLimit
}
isMe=
{
true
}
/>
</
div
>
);
};
const
LifeBarItem
:
React
.
FC
<
{
active
:
boolean
;
name
:
string
;
life
:
number
;
timeLimit
:
number
;
isMe
:
boolean
;
}
>
=
({
active
,
name
,
life
,
timeLimit
,
isMe
})
=>
{
const
mm
=
Math
.
floor
(
timeLimit
/
60
);
const
ss
=
timeLimit
%
60
;
const
timeText
=
timeLimit
<
0
?
"
00:00
"
:
`
${
mm
<
10
?
"
0
"
+
mm
:
mm
}
:
${
ss
<
10
?
"
0
"
+
ss
:
ss
}
`
;
return
(
<
div
style=
{
{
flexDirection
:
isMe
?
"
column-reverse
"
:
"
column
"
,
overflow
:
"
hidden
"
,
display
:
"
flex
"
,
gap
:
"
0.5rem
"
,
position
:
"
relative
"
,
}
}
>
<
div
className=
{
classNames
(
"
life-bar
"
,
{
"
life-bar-activated
"
:
matStore
.
isMe
(
currentPlayer
)
,
"
life-bar-activated
"
:
active
,
})
}
>
<
div
className=
"name"
>
{
snapPlayer
.
getOpPlayer
().
name
}
</
div
>
<
div
className=
"life"
>
{
<
AnimatedNumbers
animateToNumber=
{
opLife
}
/>
}
</
div
>
<
div
className=
"name"
>
{
name
}
</
div
>
<
div
className=
"life"
>
{
<
AnimatedNumbers
animateToNumber=
{
life
}
/>
}
</
div
>
</
div
>
<
div
className=
{
classNames
(
"
life-bar
"
,
{
"
life-bar-activated
"
:
matStore
.
isMe
(
currentPlayer
),
})
}
>
<
div
className=
"name"
>
{
snapPlayer
.
getMePlayer
().
name
}
</
div
>
<
div
className=
"life"
>
<
AnimatedNumbers
animateToNumber=
{
meLife
}
/>
{
active
&&
(
<
div
className=
"timer-container"
>
<
Progress
type=
"circle"
percent=
{
Math
.
floor
((
timeLimit
/
240
)
*
100
)
}
strokeWidth=
{
20
}
size=
{
14
}
/>
<
div
className=
"timer-text"
>
{
timeText
}
</
div
>
<
div
className=
"floodlight floodlight-run"
/>
</
div
>
</
div
>
)
}
</
div
>
);
};
src/ui/Duel/PlayMat/Menu/index.scss
View file @
f15b790f
...
...
@@ -10,26 +10,4 @@
padding
:
8px
;
border-radius
:
6px
;
overflow
:
hidden
;
.floodlight
{
position
:
absolute
;
height
:
100%
;
width
:
40px
;
background-color
:
white
;
top
:
0
;
right
:
0
;
filter
:
blur
(
30px
);
transform
:
skewX
(
-20deg
);
}
.floodlight-run
{
animation
:
floodlight
1s
linear
infinite
;
}
}
@keyframes
floodlight
{
0
%
{
right
:
-80px
;
}
100
%
{
right
:
calc
(
100%
+
80px
);
}
}
src/ui/Duel/PlayMat/Menu/index.tsx
View file @
f15b790f
...
...
@@ -124,7 +124,6 @@ export const Menu = () => {
>
<
Button
icon=
{
<
CloseCircleFilled
/>
}
type=
"text"
></
Button
>
</
DropdownWithTitle
>
{
/* <div className="floodlight floodlight-run" /> */
}
</
div
>
</>
);
...
...
src/ui/Duel/PlayMat/Timer/index.scss
deleted
100644 → 0
View file @
2e712f62
#timer-container
{
position
:
fixed
;
display
:
flex
;
top
:
0
;
right
:
0
;
height
:
100vh
;
padding
:
20px
35px
;
flex-direction
:
column
;
pointer-events
:
none
;
}
.timer
{
width
:
100px
;
color
:
white
;
background-color
:
#323232
;
font-family
:
var
(
--
theme-font
);
border
:
1px
solid
#222
;
padding
:
1rem
;
padding-bottom
:
0
.6rem
;
border-radius
:
8px
;
text-align
:
center
;
display
:
flex
;
flex-direction
:
column
;
font-size
:
1
.2rem
;
}
src/ui/Duel/PlayMat/Timer/index.tsx
deleted
100644 → 0
View file @
2e712f62
import
"
./index.scss
"
;
import
React
,
{
useEffect
,
useState
}
from
"
react
"
;
import
{
useSnapshot
}
from
"
valtio
"
;
import
{
matStore
}
from
"
@/stores
"
;
export
const
Timer
:
React
.
FC
=
()
=>
{
const
[
time
,
setTime
]
=
useState
(
0
);
const
snap
=
useSnapshot
(
matStore
);
useEffect
(()
=>
{
const
interval
=
setInterval
(()
=>
{
if
(
time
>
0
)
{
setTime
((
time
)
=>
time
-
1
);
}
},
1000
);
return
()
=>
clearInterval
(
interval
);
},
[
time
]);
useEffect
(()
=>
{
setTime
(
snap
.
timeLimits
.
me
);
},
[
snap
.
timeLimits
.
me
]);
useEffect
(()
=>
{
setTime
(
snap
.
timeLimits
.
op
);
},
[
snap
.
timeLimits
.
op
]);
return
(
<
div
id=
"timer-container"
>
<
div
className=
"timer"
>
{
time
}
</
div
>
</
div
>
);
};
src/ui/Duel/PlayMat/index.ts
View file @
f15b790f
export
*
from
"
./LifeBar
"
;
export
*
from
"
./Mat
"
;
export
*
from
"
./Menu
"
;
export
*
from
"
./Timer
"
;
src/ui/Duel/PlayMat/utils/cssConfig.ts
View file @
f15b790f
...
...
@@ -65,7 +65,7 @@ export const matConfig = {
unit
:
UNIT
.
PX
,
},
HAND_CARD_HEIGHT
:
{
value
:
1
4
0
,
value
:
1
3
0
,
unit
:
UNIT
.
PX
,
},
DECK_OFFSET_X
:
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment