Skip to content
GitLab
Explore
Sign in
Register
Primary navigation
Search or go to…
Project
Sharkey
Manage
Activity
Members
Labels
Plan
Issues
0
Issue boards
Milestones
Wiki
Requirements
Code
Merge requests
0
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Package Registry
Container Registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Dima Krasner
Sharkey
Commits
762fa6a8
Commit
762fa6a8
authored
1 year ago
by
syuilo
Browse files
Options
Downloads
Patches
Plain Diff
enhance(drop-and-fusion): make game engine headless for server-side running
parent
36fd7d17
No related branches found
Branches containing commit
No related tags found
Tags containing commit
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
packages/frontend/src/pages/drop-and-fusion.game.vue
+295
-143
295 additions, 143 deletions
packages/frontend/src/pages/drop-and-fusion.game.vue
packages/frontend/src/scripts/drop-and-fusion-engine.ts
+92
-267
92 additions, 267 deletions
packages/frontend/src/scripts/drop-and-fusion-engine.ts
with
387 additions
and
410 deletions
packages/frontend/src/pages/drop-and-fusion.game.vue
+
295
−
143
View file @
762fa6a8
...
...
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div
:class=
"[$style.frame, $style.frameH]"
>
<div
:class=
"$style.frameInner"
>
<MkButton
inline
small
@
click=
"hold"
>
HOLD
</MkButton>
<img
v-if=
"holdingStock"
:src=
"
game.
getTextureImageUrl(holdingStock.mono)"
style=
"width: 32px; margin-left: 8px; vertical-align: bottom;"
/>
<img
v-if=
"holdingStock"
:src=
"getTextureImageUrl(holdingStock.mono)"
style=
"width: 32px; margin-left: 8px; vertical-align: bottom;"
/>
</div>
<div
:class=
"[$style.frameInner, $style.stock]"
style=
"text-align: center;"
>
<TransitionGroup
...
...
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveToClass=
"$style.transition_stock_leaveTo"
:moveClass=
"$style.transition_stock_move"
>
<img
v-for=
"x in stock"
:key=
"x.id"
:src=
"
game.
getTextureImageUrl(x.mono)"
style=
"width: 32px; vertical-align: bottom;"
/>
<img
v-for=
"x in stock"
:key=
"x.id"
:src=
"getTextureImageUrl(x.mono)"
style=
"width: 32px; vertical-align: bottom;"
/>
</TransitionGroup>
</div>
</div>
...
...
@@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass=
"$style.transition_picked_move"
mode=
"out-in"
>
<img
v-if=
"currentPick"
:key=
"currentPick.id"
:src=
"
game.
getTextureImageUrl(currentPick.mono)"
:class=
"$style.currentMono"
:style=
"
{ marginBottom: -((currentPick?.mono.size * viewScale) / 2) + 'px', left: -((currentPick?.mono.size * viewScale) / 2) + 'px', width: `${currentPick?.mono.size * viewScale}px` }"/>
<img
v-if=
"currentPick"
:key=
"currentPick.id"
:src=
"getTextureImageUrl(currentPick.mono)"
:class=
"$style.currentMono"
:style=
"
{ marginBottom: -((currentPick?.mono.size * viewScale) / 2) + 'px', left: -((currentPick?.mono.size * viewScale) / 2) + 'px', width: `${currentPick?.mono.size * viewScale}px` }"/>
</Transition>
<template
v-if=
"dropReady && currentPick"
>
<img
src=
"/client-assets/drop-and-fusion/drop-arrow.svg"
:class=
"$style.currentMonoArrow"
/>
...
...
@@ -81,14 +81,17 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div
v-if=
"replaying"
:class=
"$style.replayIndicator"
><span
:class=
"$style.replayIndicatorText"
><i
class=
"ti ti-player-play"
></i>
{{ i18n.ts.replaying }}
</span></div>
</div>
<div
v-if=
"replaying"
style=
"display: flex;"
>
<div
:class=
"$style.frame"
style=
"flex: 1; margin-right: 10px;"
>
<div
:class=
"$style.frameInner"
>
<div
class=
"_buttonsCenter"
>
<MkButton
@
click=
"endReplay"
><i
class=
"ti ti-player-stop"
></i>
END REPLAY
</MkButton>
<MkButton
:primary=
"replayPlaybackRate === 2"
@
click=
"replayPlaybackRate = replayPlaybackRate === 2 ? 1 : 2"
><i
class=
"ti ti-player-track-next"
></i>
x2
</MkButton>
<MkButton
:primary=
"replayPlaybackRate === 4"
@
click=
"replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"
><i
class=
"ti ti-player-track-next"
></i>
x4
</MkButton>
</div>
<div
v-if=
"replaying"
:class=
"$style.frame"
>
<div
:class=
"$style.frameInner"
>
<div
style=
"background: #0004;"
>
<div
style=
"height: 10px; background: var(--accent); will-change: width;"
:style=
"{ width: `${(currentFrame / endedAtFrame) * 100}%` }"
></div>
</div>
</div>
<div
:class=
"$style.frameInner"
>
<div
class=
"_buttonsCenter"
>
<MkButton
@
click=
"endReplay"
><i
class=
"ti ti-player-stop"
></i>
END
</MkButton>
<MkButton
:primary=
"replayPlaybackRate === 2"
@
click=
"replayPlaybackRate = replayPlaybackRate === 2 ? 1 : 2"
><i
class=
"ti ti-player-track-next"
></i>
x2
</MkButton>
<MkButton
:primary=
"replayPlaybackRate === 4"
@
click=
"replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"
><i
class=
"ti ti-player-track-next"
></i>
x4
</MkButton>
</div>
</div>
</div>
...
...
@@ -140,6 +143,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<
script
lang=
"ts"
setup
>
import
{
onDeactivated
,
onMounted
,
onUnmounted
,
ref
,
shallowRef
,
watch
}
from
'
vue
'
;
import
*
as
Matter
from
'
matter-js
'
;
import
*
as
Misskey
from
'
misskey-js
'
;
import
{
definePageMetadata
}
from
'
@/scripts/page-metadata.js
'
;
import
MkRippleEffect
from
'
@/components/MkRippleEffect.vue
'
;
...
...
@@ -385,9 +389,6 @@ const SQUARE_MONOS: Mono[] = [{
spriteScale
:
1.12
,
}];
const
GAME_WIDTH
=
450
;
const
GAME_HEIGHT
=
600
;
const
props
=
defineProps
<
{
gameMode
:
'
normal
'
|
'
square
'
;
mute
:
boolean
;
...
...
@@ -397,12 +398,23 @@ const emit = defineEmits<{
(
ev
:
'
end
'
):
void
;
}
>
();
const
monoDefinitions
=
props
.
gameMode
===
'
normal
'
?
NORAML_MONOS
:
SQUARE_MONOS
;
let
viewScale
=
1
;
let
game
:
DropAndFusionGame
;
let
seed
:
string
=
Date
.
now
().
toString
()
;
let
containerElRect
:
DOMRect
|
null
=
null
;
let
seed
:
string
;
let
logs
:
ReturnType
<
DropAndFusionGame
[
'
getLogs
'
]
>
|
null
=
null
;
let
endedAtFrame
=
0
;
let
bgmNodes
:
ReturnType
<
typeof
sound
.
createSourceNode
>
|
null
=
null
;
let
renderer
:
Matter
.
Render
|
null
=
null
;
let
monoTextures
:
Record
<
string
,
Blob
>
=
{};
let
monoTextureUrls
:
Record
<
string
,
string
>
=
{};
let
tickRaf
:
number
|
null
=
null
;
let
game
=
new
DropAndFusionGame
({
seed
:
seed
,
monoDefinitions
,
});
attachGameEvents
();
const
containerEl
=
shallowRef
<
HTMLElement
>
();
const
canvasEl
=
shallowRef
<
HTMLCanvasElement
>
();
...
...
@@ -421,6 +433,7 @@ const highScore = ref<number | null>(null);
const
showConfig
=
ref
(
false
);
const
replaying
=
ref
(
false
);
const
replayPlaybackRate
=
ref
(
1
);
const
currentFrame
=
ref
(
0
);
const
bgmVolume
=
ref
(
defaultStore
.
state
.
dropAndFusion
.
bgmVolume
);
const
sfxVolume
=
ref
(
defaultStore
.
state
.
dropAndFusion
.
sfxVolume
);
...
...
@@ -434,50 +447,125 @@ watch(bgmVolume, (newValue) => {
}
});
watch
(
sfxVolume
,
(
newValue
)
=>
{
function
createRendererInstance
(
game
:
DropAndFusionGame
)
{
return
Matter
.
Render
.
create
({
engine
:
game
.
engine
,
canvas
:
canvasEl
.
value
!
,
options
:
{
width
:
game
.
GAME_WIDTH
,
height
:
game
.
GAME_HEIGHT
,
background
:
'
transparent
'
,
// transparent to hide
wireframeBackground
:
'
transparent
'
,
// transparent to hide
wireframes
:
false
,
showSleeping
:
false
,
pixelRatio
:
Math
.
max
(
2
,
window
.
devicePixelRatio
),
},
});
}
function
loadMonoTextures
()
{
async
function
loadSingleMonoTexture
(
mono
:
Mono
)
{
if
(
renderer
==
null
)
return
;
// Matter-js内にキャッシュがある場合はスキップ
if
(
renderer
.
textures
[
mono
.
img
])
return
;
let
src
=
mono
.
img
;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if
(
monoTextureUrls
[
mono
.
img
])
{
src
=
monoTextureUrls
[
mono
.
img
];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
}
else
if
(
monoTextures
[
mono
.
img
])
{
src
=
URL
.
createObjectURL
(
monoTextures
[
mono
.
img
]);
monoTextureUrls
[
mono
.
img
]
=
src
;
}
else
{
const
res
=
await
fetch
(
mono
.
img
);
const
blob
=
await
res
.
blob
();
monoTextures
[
mono
.
img
]
=
blob
;
src
=
URL
.
createObjectURL
(
blob
);
monoTextureUrls
[
mono
.
img
]
=
src
;
}
const
image
=
new
Image
();
image
.
src
=
src
;
renderer
.
textures
[
mono
.
img
]
=
image
;
}
return
Promise
.
all
(
monoDefinitions
.
map
(
x
=>
loadSingleMonoTexture
(
x
)));
}
function
getTextureImageUrl
(
mono
:
Mono
)
{
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if
(
game
)
{
game
.
setSfxVolume
(
props
.
mute
?
0
:
newValue
);
if
(
monoTextureUrls
[
mono
.
img
])
{
return
monoTextureUrls
[
mono
.
img
];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
}
else
if
(
monoTextures
[
mono
.
img
])
{
// Gameクラス内にキャッシュがある場合はそれを使う
const
out
=
URL
.
createObjectURL
(
monoTextures
[
mono
.
img
]);
monoTextureUrls
[
mono
.
img
]
=
out
;
return
out
;
}
else
{
return
mono
.
img
;
}
}
);
}
function
createGameInstance
()
{
return
new
DropAndFusionGame
({
width
:
GAME_WIDTH
,
height
:
GAME_HEIGHT
,
canvas
:
canvasEl
.
value
!
,
seed
:
seed
,
sfxVolume
:
props
.
mute
?
0
:
sfxVolume
.
value
,
...(
props
.
gameMode
===
'
normal
'
?
{
monoDefinitions
:
NORAML_MONOS
,
}
:
{
monoDefinitions
:
SQUARE_MONOS
,
function
tick
()
{
const
hasNextTick
=
game
.
tick
();
if
(
hasNextTick
)
{
tickRaf
=
window
.
requestAnimationFrame
(
tick
);
}
else
{
tickRaf
=
null
;
}
}
function
tickReplay
()
{
let
hasNextTick
;
for
(
let
i
=
0
;
i
<
replayPlaybackRate
.
value
;
i
++
)
{
const
log
=
logs
!
.
find
(
x
=>
x
.
frame
===
game
.
frame
);
if
(
log
)
{
switch
(
log
.
operation
)
{
case
'
drop
'
:
{
game
.
drop
(
log
.
x
);
break
;
}
case
'
hold
'
:
{
game
.
hold
();
break
;
}
case
'
surrender
'
:
{
game
.
surrender
();
break
;
}
default
:
break
;
}
),
});
}
hasNextTick
=
game
.
tick
();
currentFrame
.
value
=
game
.
frame
;
if
(
!
hasNextTick
)
break
;
}
if
(
hasNextTick
)
{
tickRaf
=
window
.
requestAnimationFrame
(
tickReplay
);
}
else
{
tickRaf
=
null
;
}
}
async
function
start
()
{
seed
=
Date
.
now
().
toString
();
game
=
createGameInstance
();
attachGameEvents
();
await
game
.
load
();
await
loadMonoTextures
();
renderer
=
createRendererInstance
(
game
);
Matter
.
Render
.
lookAt
(
renderer
,
{
min
:
{
x
:
0
,
y
:
0
},
max
:
{
x
:
game
.
GAME_WIDTH
,
y
:
game
.
GAME_HEIGHT
},
});
Matter
.
Render
.
run
(
renderer
);
game
.
start
();
window
.
requestAnimationFrame
(
tick
);
gameLoaded
.
value
=
true
;
if
(
bgmNodes
==
null
)
{
const
bgmBuffer
=
await
sound
.
loadAudio
(
'
/client-assets/drop-and-fusion/bgm_1.mp3
'
);
if
(
!
bgmBuffer
)
return
;
bgmNodes
=
sound
.
createSourceNode
(
bgmBuffer
,
{
volume
:
props
.
mute
?
0
:
bgmVolume
.
value
,
});
if
(
!
bgmNodes
)
return
;
bgmNodes
.
soundSource
.
loop
=
true
;
bgmNodes
.
soundSource
.
start
();
}
}
function
onClick
(
ev
:
MouseEvent
)
{
...
...
@@ -507,7 +595,7 @@ function onTouchmove(ev: TouchEvent) {
}
function
moveDropper
(
rect
:
DOMRect
,
x
:
number
)
{
dropperX
.
value
=
Math
.
min
(
rect
.
width
*
((
GAME_WIDTH
-
game
.
PLAYAREA_MARGIN
)
/
GAME_WIDTH
),
Math
.
max
(
rect
.
width
*
(
game
.
PLAYAREA_MARGIN
/
GAME_WIDTH
),
x
));
dropperX
.
value
=
Math
.
min
(
rect
.
width
*
((
game
.
GAME_WIDTH
-
game
.
PLAYAREA_MARGIN
)
/
game
.
GAME_WIDTH
),
Math
.
max
(
rect
.
width
*
(
game
.
PLAYAREA_MARGIN
/
game
.
GAME_WIDTH
),
x
));
}
function
hold
()
{
...
...
@@ -525,11 +613,17 @@ async function surrender() {
async
function
restart
()
{
reset
();
game
=
new
DropAndFusionGame
({
seed
:
seed
,
monoDefinitions
,
});
attachGameEvents
();
await
start
();
}
function
reset
()
{
game
.
dispose
();
dispose
();
seed
=
Date
.
now
().
toString
();
isGameOver
.
value
=
false
;
replaying
.
value
=
false
;
replayPlaybackRate
.
value
=
1
;
...
...
@@ -544,9 +638,12 @@ function reset() {
gameLoaded
.
value
=
false
;
}
function
end
()
{
function
dispose
()
{
game
.
dispose
();
bgmNodes
?.
soundSource
.
stop
();
Matter
.
Render
.
stop
(
renderer
);
if
(
tickRaf
)
{
window
.
cancelAnimationFrame
(
tickRaf
);
}
}
function
backToTitle
()
{
...
...
@@ -555,17 +652,28 @@ function backToTitle() {
function
replay
()
{
replaying
.
value
=
true
;
game
.
dispose
();
game
=
createGameInstance
();
dispose
();
game
=
new
DropAndFusionGame
({
seed
:
seed
,
monoDefinitions
,
replaying
:
true
,
});
attachGameEvents
();
os
.
promiseDialog
(
game
.
load
(),
async
()
=>
{
game
.
start
(
logs
!
);
os
.
promiseDialog
(
loadMonoTextures
(),
async
()
=>
{
renderer
=
createRendererInstance
(
game
);
Matter
.
Render
.
lookAt
(
renderer
,
{
min
:
{
x
:
0
,
y
:
0
},
max
:
{
x
:
game
.
GAME_WIDTH
,
y
:
game
.
GAME_HEIGHT
},
});
Matter
.
Render
.
run
(
renderer
);
game
.
start
();
window
.
requestAnimationFrame
(
tickReplay
);
});
}
function
endReplay
()
{
replaying
.
value
=
false
;
game
.
dispose
();
dispose
();
}
function
exportLog
()
{
...
...
@@ -579,6 +687,90 @@ function exportLog() {
os
.
success
();
}
function
updateSettings
<
K
extends
keyof
typeof
defaultStore
.
state
.
dropAndFusion
,
V
extends
typeof
defaultStore
.
state
.
dropAndFusion
[
K
],
>
(
key
:
K
,
value
:
V
)
{
const
changes
:
{
[
P
in
K
]?:
V
}
=
{};
changes
[
key
]
=
value
;
defaultStore
.
set
(
'
dropAndFusion
'
,
{
...
defaultStore
.
state
.
dropAndFusion
,
...
changes
,
});
}
function
loadImage
(
url
:
string
)
{
return
new
Promise
<
HTMLImageElement
>
(
res
=>
{
const
img
=
new
Image
();
img
.
src
=
url
;
img
.
addEventListener
(
'
load
'
,
()
=>
{
res
(
img
);
});
});
}
function
getGameImageDriveFile
()
{
return
new
Promise
<
Misskey
.
entities
.
DriveFile
|
null
>
(
res
=>
{
const
dcanvas
=
document
.
createElement
(
'
canvas
'
);
dcanvas
.
width
=
game
.
GAME_WIDTH
;
dcanvas
.
height
=
game
.
GAME_HEIGHT
;
const
ctx
=
dcanvas
.
getContext
(
'
2d
'
);
if
(
!
ctx
||
!
canvasEl
.
value
)
return
res
(
null
);
Promise
.
all
([
loadImage
(
'
/client-assets/drop-and-fusion/frame-light.svg
'
),
loadImage
(
'
/client-assets/drop-and-fusion/logo.png
'
),
]).
then
((
images
)
=>
{
const
[
frame
,
logo
]
=
images
;
ctx
.
fillStyle
=
'
#fff
'
;
ctx
.
fillRect
(
0
,
0
,
game
.
GAME_WIDTH
,
game
.
GAME_HEIGHT
);
ctx
.
drawImage
(
frame
,
0
,
0
,
game
.
GAME_WIDTH
,
game
.
GAME_HEIGHT
);
ctx
.
drawImage
(
canvasEl
.
value
!
,
0
,
0
,
game
.
GAME_WIDTH
,
game
.
GAME_HEIGHT
);
ctx
.
globalAlpha
=
0.7
;
ctx
.
drawImage
(
logo
,
game
.
GAME_WIDTH
*
0.55
,
6
,
game
.
GAME_WIDTH
*
0.45
,
game
.
GAME_WIDTH
*
0.45
*
(
logo
.
height
/
logo
.
width
));
ctx
.
globalAlpha
=
1
;
dcanvas
.
toBlob
(
blob
=>
{
if
(
!
blob
)
return
res
(
null
);
if
(
$i
==
null
)
return
res
(
null
);
const
formData
=
new
FormData
();
formData
.
append
(
'
file
'
,
blob
);
formData
.
append
(
'
name
'
,
`bubble-game-
${
Date
.
now
()}
.png`
);
formData
.
append
(
'
isSensitive
'
,
'
false
'
);
formData
.
append
(
'
comment
'
,
'
null
'
);
formData
.
append
(
'
i
'
,
$i
.
token
);
if
(
defaultStore
.
state
.
uploadFolder
)
{
formData
.
append
(
'
folderId
'
,
defaultStore
.
state
.
uploadFolder
);
}
window
.
fetch
(
apiUrl
+
'
/drive/files/create
'
,
{
method
:
'
POST
'
,
body
:
formData
,
})
.
then
(
response
=>
response
.
json
())
.
then
(
f
=>
{
res
(
f
);
});
},
'
image/png
'
);
dcanvas
.
remove
();
});
});
}
async
function
share
()
{
const
uploading
=
getGameImageDriveFile
();
os
.
promiseDialog
(
uploading
);
const
file
=
await
uploading
;
if
(
!
file
)
return
;
os
.
post
({
initialText
:
`#BubbleGame
MODE:
${
props
.
gameMode
}
SCORE:
${
score
.
value
}
(MAX CHAIN:
${
maxCombo
.
value
}
)`
,
initialFiles
:
[
file
],
instant
:
true
,
});
}
function
attachGameEvents
()
{
game
.
addListener
(
'
changeScore
'
,
value
=>
{
score
.
value
=
value
;
...
...
@@ -601,9 +793,22 @@ function attachGameEvents() {
game
.
addListener
(
'
changeHolding
'
,
value
=>
{
holdingStock
.
value
=
value
;
sound
.
playUrl
(
'
/client-assets/drop-and-fusion/hold.mp3
'
,
{
volume
:
0.5
*
sfxVolume
.
value
,
});
});
game
.
addListener
(
'
dropped
'
,
()
=>
{
game
.
addListener
(
'
dropped
'
,
(
x
)
=>
{
const
panV
=
x
-
game
.
PLAYAREA_MARGIN
;
const
panW
=
game
.
GAME_WIDTH
-
game
.
PLAYAREA_MARGIN
-
game
.
PLAYAREA_MARGIN
;
const
pan
=
((
panV
/
panW
)
-
0.5
)
*
2
;
sound
.
playUrl
(
'
/client-assets/drop-and-fusion/poi2.mp3
'
,
{
volume
:
sfxVolume
.
value
,
pan
,
playbackRate
:
replayPlaybackRate
.
value
,
});
if
(
replaying
.
value
)
return
;
dropReady
.
value
=
false
;
...
...
@@ -639,12 +844,17 @@ function attachGameEvents() {
});
game
.
addListener
(
'
gameOver
'
,
()
=>
{
sound
.
playUrl
(
'
/client-assets/drop-and-fusion/gameover.mp3
'
,
{
volume
:
sfxVolume
.
value
,
});
if
(
replaying
.
value
)
{
endReplay
();
return
;
}
logs
=
game
.
getLogs
();
endedAtFrame
=
game
.
frame
;
currentPick
.
value
=
null
;
dropReady
.
value
=
false
;
isGameOver
.
value
=
true
;
...
...
@@ -659,97 +869,28 @@ function attachGameEvents() {
});
}
});
}
function
updateSettings
<
K
extends
keyof
typeof
defaultStore
.
state
.
dropAndFusion
,
V
extends
typeof
defaultStore
.
state
.
dropAndFusion
[
K
],
>
(
key
:
K
,
value
:
V
)
{
const
changes
:
{
[
P
in
K
]?:
V
}
=
{};
changes
[
key
]
=
value
;
defaultStore
.
set
(
'
dropAndFusion
'
,
{
...
defaultStore
.
state
.
dropAndFusion
,
...
changes
,
});
}
game
.
addListener
(
'
sfx
'
,
(
type
,
params
)
=>
{
if
(
props
.
mute
)
return
;
function
loadImage
(
url
:
string
)
{
return
new
Promise
<
HTMLImageElement
>
(
res
=>
{
const
img
=
new
Image
();
img
.
src
=
url
;
img
.
addEventListener
(
'
load
'
,
()
=>
{
res
(
img
);
});
});
}
const
soundUrl
=
type
===
'
fusion
'
?
'
/client-assets/drop-and-fusion/bubble2.mp3
'
:
type
===
'
collision
'
?
'
/client-assets/drop-and-fusion/poi1.mp3
'
:
null
as
never
;
function
getGameImageDriveFile
()
{
return
new
Promise
<
Misskey
.
entities
.
DriveFile
|
null
>
(
res
=>
{
const
dcanvas
=
document
.
createElement
(
'
canvas
'
);
dcanvas
.
width
=
GAME_WIDTH
;
dcanvas
.
height
=
GAME_HEIGHT
;
const
ctx
=
dcanvas
.
getContext
(
'
2d
'
);
if
(
!
ctx
||
!
canvasEl
.
value
)
return
res
(
null
);
Promise
.
all
([
loadImage
(
'
/client-assets/drop-and-fusion/frame-light.svg
'
),
loadImage
(
'
/client-assets/drop-and-fusion/logo.png
'
),
]).
then
((
images
)
=>
{
const
[
frame
,
logo
]
=
images
;
ctx
.
fillStyle
=
'
#fff
'
;
ctx
.
fillRect
(
0
,
0
,
GAME_WIDTH
,
GAME_HEIGHT
);
ctx
.
drawImage
(
frame
,
0
,
0
,
GAME_WIDTH
,
GAME_HEIGHT
);
ctx
.
drawImage
(
canvasEl
.
value
!
,
0
,
0
,
GAME_WIDTH
,
GAME_HEIGHT
);
ctx
.
globalAlpha
=
0.7
;
ctx
.
drawImage
(
logo
,
GAME_WIDTH
*
0.55
,
6
,
GAME_WIDTH
*
0.45
,
GAME_WIDTH
*
0.45
*
(
logo
.
height
/
logo
.
width
));
ctx
.
globalAlpha
=
1
;
dcanvas
.
toBlob
(
blob
=>
{
if
(
!
blob
)
return
res
(
null
);
if
(
$i
==
null
)
return
res
(
null
);
const
formData
=
new
FormData
();
formData
.
append
(
'
file
'
,
blob
);
formData
.
append
(
'
name
'
,
`bubble-game-
${
Date
.
now
()}
.png`
);
formData
.
append
(
'
isSensitive
'
,
'
false
'
);
formData
.
append
(
'
comment
'
,
'
null
'
);
formData
.
append
(
'
i
'
,
$i
.
token
);
if
(
defaultStore
.
state
.
uploadFolder
)
{
formData
.
append
(
'
folderId
'
,
defaultStore
.
state
.
uploadFolder
);
}
window
.
fetch
(
apiUrl
+
'
/drive/files/create
'
,
{
method
:
'
POST
'
,
body
:
formData
,
})
.
then
(
response
=>
response
.
json
())
.
then
(
f
=>
{
res
(
f
);
});
},
'
image/png
'
);
dcanvas
.
remove
();
sound
.
playUrl
(
soundUrl
,
{
volume
:
params
.
volume
*
sfxVolume
.
value
,
pan
:
params
.
pan
,
playbackRate
:
params
.
pitch
*
replayPlaybackRate
.
value
,
});
});
}
async
function
share
()
{
const
uploading
=
getGameImageDriveFile
();
os
.
promiseDialog
(
uploading
);
const
file
=
await
uploading
;
if
(
!
file
)
return
;
os
.
post
({
initialText
:
`#BubbleGame
MODE:
${
props
.
gameMode
}
SCORE:
${
score
.
value
}
(MAX CHAIN:
${
maxCombo
.
value
}
)`
,
initialFiles
:
[
file
],
instant
:
true
,
});
}
useInterval
(()
=>
{
if
(
!
canvasEl
.
value
)
return
;
const
actualCanvasWidth
=
canvasEl
.
value
.
getBoundingClientRect
().
width
;
if
(
actualCanvasWidth
===
0
)
return
;
viewScale
=
actualCanvasWidth
/
GAME_WIDTH
;
viewScale
=
actualCanvasWidth
/
game
.
GAME_WIDTH
;
containerElRect
=
containerEl
.
value
?.
getBoundingClientRect
()
??
null
;
},
1000
,
{
immediate
:
false
,
afterMounted
:
true
});
...
...
@@ -763,15 +904,26 @@ onMounted(async () => {
highScore
.
value
=
null
;
}
start
();
await
start
();
const
bgmBuffer
=
await
sound
.
loadAudio
(
'
/client-assets/drop-and-fusion/bgm_1.mp3
'
);
if
(
!
bgmBuffer
)
return
;
bgmNodes
=
sound
.
createSourceNode
(
bgmBuffer
,
{
volume
:
props
.
mute
?
0
:
bgmVolume
.
value
,
});
if
(
!
bgmNodes
)
return
;
bgmNodes
.
soundSource
.
loop
=
true
;
bgmNodes
.
soundSource
.
start
();
});
onUnmounted
(()
=>
{
end
();
dispose
();
bgmNodes
?.
soundSource
.
stop
();
});
onDeactivated
(()
=>
{
end
();
dispose
();
bgmNodes
?.
soundSource
.
stop
();
});
definePageMetadata
({
...
...
This diff is collapsed.
Click to expand it.
packages/frontend/src/scripts/drop-and-fusion-engine.ts
+
92
−
267
View file @
762fa6a8
...
...
@@ -6,7 +6,6 @@
import
{
EventEmitter
}
from
'
eventemitter3
'
;
import
*
as
Matter
from
'
matter-js
'
;
import
seedrandom
from
'
seedrandom
'
;
import
*
as
sound
from
'
@/scripts/sound.js
'
;
export
type
Mono
=
{
id
:
string
;
...
...
@@ -39,41 +38,41 @@ export class DropAndFusionGame extends EventEmitter<{
changeCombo
:
(
newCombo
:
number
)
=>
void
;
changeStock
:
(
newStock
:
{
id
:
string
;
mono
:
Mono
}[])
=>
void
;
changeHolding
:
(
newHolding
:
{
id
:
string
;
mono
:
Mono
}
|
null
)
=>
void
;
dropped
:
()
=>
void
;
dropped
:
(
x
:
number
)
=>
void
;
fusioned
:
(
x
:
number
,
y
:
number
,
scoreDelta
:
number
)
=>
void
;
monoAdded
:
(
mono
:
Mono
)
=>
void
;
gameOver
:
()
=>
void
;
sfx
(
type
:
string
,
params
:
{
volume
:
number
;
pan
:
number
;
pitch
:
number
;
}):
void
;
}
>
{
private
PHYSICS_QUALITY_FACTOR
=
16
;
// 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
private
COMBO_INTERVAL
=
60
;
// frame
public
readonly
GAME_WIDTH
=
450
;
public
readonly
GAME_HEIGHT
=
600
;
public
readonly
DROP_INTERVAL
=
500
;
public
readonly
PLAYAREA_MARGIN
=
25
;
private
STOCK_MAX
=
4
;
private
TICK_DELTA
=
1000
/
60
;
// 60fps
private
loaded
=
false
;
private
frame
=
0
;
private
engine
:
Matter
.
Engine
;
private
render
:
Matter
.
Render
;
private
tickRaf
:
ReturnType
<
typeof
requestAnimationFrame
>
|
null
=
null
;
public
frame
=
0
;
public
engine
:
Matter
.
Engine
;
private
tickCallbackQueue
:
{
frame
:
number
;
callback
:
()
=>
void
;
}[]
=
[];
private
overflowCollider
:
Matter
.
Body
;
private
isGameOver
=
false
;
private
gameWidth
:
number
;
private
gameHeight
:
number
;
private
monoDefinitions
:
Mono
[]
=
[];
private
monoTextures
:
Record
<
string
,
Blob
>
=
{};
private
monoTextureUrls
:
Record
<
string
,
string
>
=
{};
private
rng
:
()
=>
number
;
private
logs
:
Log
[]
=
[];
private
replaying
=
false
;
private
sfxVolume
=
1
;
/**
* フィールドに出ていて、かつ合体の対象となるアイテム
*/
private
activeBodyIds
:
Matter
.
Body
[
'
id
'
][]
=
[];
/**
* fusion予約アイテムのペア
* TODO: これらのモノは光らせるなどの演出をすると視覚的に楽しそう
*/
private
fusionReservedPairs
:
{
bodyA
:
Matter
.
Body
;
bodyB
:
Matter
.
Body
}[]
=
[];
private
latestDroppedBodyId
:
Matter
.
Body
[
'
id
'
]
|
null
=
null
;
private
latestDroppedAt
=
0
;
...
...
@@ -99,30 +98,16 @@ export class DropAndFusionGame extends EventEmitter<{
this
.
emit
(
'
changeScore
'
,
value
);
}
private
comboIntervalId
:
number
|
null
=
null
;
public
replayPlaybackRate
=
1
;
constructor
(
opts
:
{
canvas
:
HTMLCanvasElement
;
width
:
number
;
height
:
number
;
monoDefinitions
:
Mono
[];
seed
:
string
;
sfxVolume
?:
number
;
})
{
constructor
(
env
:
{
monoDefinitions
:
Mono
[];
seed
:
string
;
replaying
?:
boolean
})
{
super
();
this
.
tick
=
this
.
tick
.
bind
(
this
);
this
.
replaying
=
!!
env
.
replaying
;
this
.
monoDefinitions
=
env
.
monoDefinitions
;
this
.
rng
=
seedrandom
(
env
.
seed
);
this
.
gameWidth
=
opts
.
width
;
this
.
gameHeight
=
opts
.
height
;
this
.
monoDefinitions
=
opts
.
monoDefinitions
;
this
.
rng
=
seedrandom
(
opts
.
seed
);
if
(
opts
.
sfxVolume
)
{
this
.
sfxVolume
=
opts
.
sfxVolume
;
}
this
.
tick
=
this
.
tick
.
bind
(
this
);
this
.
engine
=
Matter
.
Engine
.
create
({
constraintIterations
:
2
*
this
.
PHYSICS_QUALITY_FACTOR
,
...
...
@@ -138,22 +123,6 @@ export class DropAndFusionGame extends EventEmitter<{
enableSleeping
:
false
,
});
this
.
render
=
Matter
.
Render
.
create
({
engine
:
this
.
engine
,
canvas
:
opts
.
canvas
,
options
:
{
width
:
this
.
gameWidth
,
height
:
this
.
gameHeight
,
background
:
'
transparent
'
,
// transparent to hide
wireframeBackground
:
'
transparent
'
,
// transparent to hide
wireframes
:
false
,
showSleeping
:
false
,
pixelRatio
:
Math
.
max
(
2
,
window
.
devicePixelRatio
),
},
});
Matter
.
Render
.
run
(
this
.
render
);
this
.
engine
.
world
.
bodies
=
[];
//#region walls
...
...
@@ -170,13 +139,13 @@ export class DropAndFusionGame extends EventEmitter<{
const
thickness
=
100
;
Matter
.
Composite
.
add
(
this
.
engine
.
world
,
[
Matter
.
Bodies
.
rectangle
(
this
.
gameWidth
/
2
,
this
.
gameHeight
+
(
thickness
/
2
)
-
this
.
PLAYAREA_MARGIN
,
this
.
gameWidth
,
thickness
,
WALL_OPTIONS
),
Matter
.
Bodies
.
rectangle
(
this
.
gameWidth
+
(
thickness
/
2
)
-
this
.
PLAYAREA_MARGIN
,
this
.
gameHeight
/
2
,
thickness
,
this
.
gameHeight
,
WALL_OPTIONS
),
Matter
.
Bodies
.
rectangle
(
-
((
thickness
/
2
)
-
this
.
PLAYAREA_MARGIN
),
this
.
gameHeight
/
2
,
thickness
,
this
.
gameHeight
,
WALL_OPTIONS
),
Matter
.
Bodies
.
rectangle
(
this
.
GAME_WIDTH
/
2
,
this
.
GAME_HEIGHT
+
(
thickness
/
2
)
-
this
.
PLAYAREA_MARGIN
,
this
.
GAME_WIDTH
,
thickness
,
WALL_OPTIONS
),
Matter
.
Bodies
.
rectangle
(
this
.
GAME_WIDTH
+
(
thickness
/
2
)
-
this
.
PLAYAREA_MARGIN
,
this
.
GAME_HEIGHT
/
2
,
thickness
,
this
.
GAME_HEIGHT
,
WALL_OPTIONS
),
Matter
.
Bodies
.
rectangle
(
-
((
thickness
/
2
)
-
this
.
PLAYAREA_MARGIN
),
this
.
GAME_HEIGHT
/
2
,
thickness
,
this
.
GAME_HEIGHT
,
WALL_OPTIONS
),
]);
//#endregion
this
.
overflowCollider
=
Matter
.
Bodies
.
rectangle
(
this
.
gameWidth
/
2
,
0
,
this
.
gameWidth
,
200
,
{
this
.
overflowCollider
=
Matter
.
Bodies
.
rectangle
(
this
.
GAME_WIDTH
/
2
,
0
,
this
.
GAME_WIDTH
,
200
,
{
isStatic
:
true
,
isSensor
:
true
,
render
:
{
...
...
@@ -185,12 +154,6 @@ export class DropAndFusionGame extends EventEmitter<{
},
});
Matter
.
Composite
.
add
(
this
.
engine
.
world
,
this
.
overflowCollider
);
// fit the render viewport to the scene
Matter
.
Render
.
lookAt
(
this
.
render
,
{
min
:
{
x
:
0
,
y
:
0
},
max
:
{
x
:
this
.
gameWidth
,
y
:
this
.
gameHeight
},
});
}
private
createBody
(
mono
:
Mono
,
x
:
number
,
y
:
number
)
{
...
...
@@ -256,29 +219,69 @@ export class DropAndFusionGame extends EventEmitter<{
const
additionalScore
=
Math
.
round
(
currentMono
.
score
*
comboBonus
);
this
.
score
+=
additionalScore
;
// TODO: 効果音再生はコンポーネント側の責務なので移動するべき?
const
panV
=
newX
-
this
.
PLAYAREA_MARGIN
;
const
panW
=
this
.
gameWidth
-
this
.
PLAYAREA_MARGIN
-
this
.
PLAYAREA_MARGIN
;
const
pan
=
((
panV
/
panW
)
-
0.5
)
*
2
;
sound
.
playUrl
(
'
/client-assets/drop-and-fusion/bubble2.mp3
'
,
{
volume
:
this
.
sfxVolume
,
pan
,
playbackRate
:
nextMono
.
sfxPitch
*
this
.
replayPlaybackRate
,
});
this
.
emit
(
'
monoAdded
'
,
nextMono
);
this
.
emit
(
'
fusioned
'
,
newX
,
newY
,
additionalScore
);
const
panV
=
newX
-
this
.
PLAYAREA_MARGIN
;
const
panW
=
this
.
GAME_WIDTH
-
this
.
PLAYAREA_MARGIN
-
this
.
PLAYAREA_MARGIN
;
const
pan
=
((
panV
/
panW
)
-
0.5
)
*
2
;
this
.
emit
(
'
sfx
'
,
'
fusion
'
,
{
volume
:
1
,
pan
,
pitch
:
nextMono
.
sfxPitch
});
}
else
{
//const VELOCITY = 30;
//for (let i = 0; i < 10; i++) {
// const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(this.rng() * 3)))!, x + ((this.rng() * VELOCITY) - (VELOCITY / 2)), y + ((this.rng() * VELOCITY) - (VELOCITY / 2)));
// Matter.Composite.add(world, body);
// bodies.push(body);
//}
//sound.playUrl({
// type: 'syuilo/bubble2',
// volume: this.sfxVolume,
//});
// nop
}
}
private
onCollision
(
event
:
Matter
.
IEventCollision
<
Matter
.
Engine
>
)
{
const
minCollisionEnergyForSound
=
2.5
;
const
maxCollisionEnergyForSound
=
9
;
const
soundPitchMax
=
4
;
const
soundPitchMin
=
0.5
;
for
(
const
pairs
of
event
.
pairs
)
{
const
{
bodyA
,
bodyB
}
=
pairs
;
if
(
bodyA
.
id
===
this
.
overflowCollider
.
id
||
bodyB
.
id
===
this
.
overflowCollider
.
id
)
{
if
(
bodyA
.
id
===
this
.
latestDroppedBodyId
||
bodyB
.
id
===
this
.
latestDroppedBodyId
)
{
continue
;
}
this
.
gameOver
();
break
;
}
const
shouldFusion
=
(
bodyA
.
label
===
bodyB
.
label
)
&&
!
this
.
fusionReservedPairs
.
some
(
x
=>
x
.
bodyA
.
id
===
bodyA
.
id
||
x
.
bodyA
.
id
===
bodyB
.
id
||
x
.
bodyB
.
id
===
bodyA
.
id
||
x
.
bodyB
.
id
===
bodyB
.
id
);
if
(
shouldFusion
)
{
if
(
this
.
activeBodyIds
.
includes
(
bodyA
.
id
)
&&
this
.
activeBodyIds
.
includes
(
bodyB
.
id
))
{
this
.
fusion
(
bodyA
,
bodyB
);
}
else
{
this
.
fusionReservedPairs
.
push
({
bodyA
,
bodyB
});
this
.
tickCallbackQueue
.
push
({
frame
:
this
.
frame
+
6
,
callback
:
()
=>
{
this
.
fusionReservedPairs
=
this
.
fusionReservedPairs
.
filter
(
x
=>
x
.
bodyA
.
id
!==
bodyA
.
id
&&
x
.
bodyB
.
id
!==
bodyB
.
id
);
this
.
fusion
(
bodyA
,
bodyB
);
},
});
}
}
else
{
const
energy
=
pairs
.
collision
.
depth
;
if
(
energy
>
minCollisionEnergyForSound
)
{
const
volume
=
(
Math
.
min
(
maxCollisionEnergyForSound
,
energy
-
minCollisionEnergyForSound
)
/
maxCollisionEnergyForSound
)
/
4
;
const
panV
=
pairs
.
bodyA
.
label
===
'
_wall_
'
?
bodyB
.
position
.
x
-
this
.
PLAYAREA_MARGIN
:
pairs
.
bodyB
.
label
===
'
_wall_
'
?
bodyA
.
position
.
x
-
this
.
PLAYAREA_MARGIN
:
((
bodyA
.
position
.
x
+
bodyB
.
position
.
x
)
/
2
)
-
this
.
PLAYAREA_MARGIN
;
const
panW
=
this
.
GAME_WIDTH
-
this
.
PLAYAREA_MARGIN
-
this
.
PLAYAREA_MARGIN
;
const
pan
=
((
panV
/
panW
)
-
0.5
)
*
2
;
const
pitch
=
soundPitchMin
+
((
soundPitchMax
-
soundPitchMin
)
*
(
1
-
(
Math
.
min
(
10
,
energy
)
/
10
)));
this
.
emit
(
'
sfx
'
,
'
collision
'
,
{
volume
,
pan
,
pitch
});
}
}
}
}
...
...
@@ -293,50 +296,10 @@ export class DropAndFusionGame extends EventEmitter<{
private
gameOver
()
{
this
.
isGameOver
=
true
;
if
(
this
.
tickRaf
)
window
.
cancelAnimationFrame
(
this
.
tickRaf
);
this
.
tickRaf
=
null
;
this
.
emit
(
'
gameOver
'
);
// TODO: 効果音再生はコンポーネント側の責務なので移動するべき?
sound
.
playUrl
(
'
/client-assets/drop-and-fusion/gameover.mp3
'
,
{
volume
:
this
.
sfxVolume
,
});
}
/** テクスチャをすべてキャッシュする */
private
async
loadMonoTextures
()
{
async
function
loadSingleMonoTexture
(
mono
:
Mono
,
game
:
DropAndFusionGame
)
{
// Matter-js内にキャッシュがある場合はスキップ
if
(
game
.
render
.
textures
[
mono
.
img
])
return
;
let
src
=
mono
.
img
;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if
(
game
.
monoTextureUrls
[
mono
.
img
])
{
src
=
game
.
monoTextureUrls
[
mono
.
img
];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
}
else
if
(
game
.
monoTextures
[
mono
.
img
])
{
src
=
URL
.
createObjectURL
(
game
.
monoTextures
[
mono
.
img
]);
game
.
monoTextureUrls
[
mono
.
img
]
=
src
;
}
else
{
const
res
=
await
fetch
(
mono
.
img
);
const
blob
=
await
res
.
blob
();
game
.
monoTextures
[
mono
.
img
]
=
blob
;
src
=
URL
.
createObjectURL
(
blob
);
game
.
monoTextureUrls
[
mono
.
img
]
=
src
;
}
const
image
=
new
Image
();
image
.
src
=
src
;
game
.
render
.
textures
[
mono
.
img
]
=
image
;
}
return
Promise
.
all
(
this
.
monoDefinitions
.
map
(
x
=>
loadSingleMonoTexture
(
x
,
this
)));
}
public
start
(
logs
?:
Log
[])
{
if
(
!
this
.
loaded
)
throw
new
Error
(
'
game is not loaded yet
'
);
if
(
logs
)
this
.
replaying
=
true
;
public
start
()
{
for
(
let
i
=
0
;
i
<
this
.
STOCK_MAX
;
i
++
)
{
this
.
stock
.
push
({
id
:
this
.
rng
().
toString
(),
...
...
@@ -345,118 +308,20 @@ export class DropAndFusionGame extends EventEmitter<{
}
this
.
emit
(
'
changeStock
'
,
this
.
stock
);
// TODO: fusion予約状態のアイテムは光らせるなどの演出をすると楽しそう
let
fusionReservedPairs
:
{
bodyA
:
Matter
.
Body
;
bodyB
:
Matter
.
Body
}[]
=
[];
const
minCollisionEnergyForSound
=
2.5
;
const
maxCollisionEnergyForSound
=
9
;
const
soundPitchMax
=
4
;
const
soundPitchMin
=
0.5
;
Matter
.
Events
.
on
(
this
.
engine
,
'
collisionStart
'
,
(
event
)
=>
{
for
(
const
pairs
of
event
.
pairs
)
{
const
{
bodyA
,
bodyB
}
=
pairs
;
if
(
bodyA
.
id
===
this
.
overflowCollider
.
id
||
bodyB
.
id
===
this
.
overflowCollider
.
id
)
{
if
(
bodyA
.
id
===
this
.
latestDroppedBodyId
||
bodyB
.
id
===
this
.
latestDroppedBodyId
)
{
continue
;
}
this
.
gameOver
();
break
;
}
const
shouldFusion
=
(
bodyA
.
label
===
bodyB
.
label
)
&&
!
fusionReservedPairs
.
some
(
x
=>
x
.
bodyA
.
id
===
bodyA
.
id
||
x
.
bodyA
.
id
===
bodyB
.
id
||
x
.
bodyB
.
id
===
bodyA
.
id
||
x
.
bodyB
.
id
===
bodyB
.
id
);
if
(
shouldFusion
)
{
if
(
this
.
activeBodyIds
.
includes
(
bodyA
.
id
)
&&
this
.
activeBodyIds
.
includes
(
bodyB
.
id
))
{
this
.
fusion
(
bodyA
,
bodyB
);
}
else
{
fusionReservedPairs
.
push
({
bodyA
,
bodyB
});
this
.
tickCallbackQueue
.
push
({
frame
:
this
.
frame
+
6
,
callback
:
()
=>
{
fusionReservedPairs
=
fusionReservedPairs
.
filter
(
x
=>
x
.
bodyA
.
id
!==
bodyA
.
id
&&
x
.
bodyB
.
id
!==
bodyB
.
id
);
this
.
fusion
(
bodyA
,
bodyB
);
},
});
}
}
else
{
const
energy
=
pairs
.
collision
.
depth
;
if
(
energy
>
minCollisionEnergyForSound
)
{
// TODO: 効果音再生はコンポーネント側の責務なので移動するべき?
const
vol
=
((
Math
.
min
(
maxCollisionEnergyForSound
,
energy
-
minCollisionEnergyForSound
)
/
maxCollisionEnergyForSound
)
/
4
)
*
this
.
sfxVolume
;
const
panV
=
pairs
.
bodyA
.
label
===
'
_wall_
'
?
bodyB
.
position
.
x
-
this
.
PLAYAREA_MARGIN
:
pairs
.
bodyB
.
label
===
'
_wall_
'
?
bodyA
.
position
.
x
-
this
.
PLAYAREA_MARGIN
:
((
bodyA
.
position
.
x
+
bodyB
.
position
.
x
)
/
2
)
-
this
.
PLAYAREA_MARGIN
;
const
panW
=
this
.
gameWidth
-
this
.
PLAYAREA_MARGIN
-
this
.
PLAYAREA_MARGIN
;
const
pan
=
((
panV
/
panW
)
-
0.5
)
*
2
;
const
pitch
=
soundPitchMin
+
((
soundPitchMax
-
soundPitchMin
)
*
(
1
-
(
Math
.
min
(
10
,
energy
)
/
10
)));
sound
.
playUrl
(
'
/client-assets/drop-and-fusion/poi1.mp3
'
,
{
volume
:
vol
,
pan
,
playbackRate
:
pitch
*
this
.
replayPlaybackRate
,
});
}
}
}
});
if
(
logs
)
{
const
playTick
=
()
=>
{
for
(
let
i
=
0
;
i
<
this
.
replayPlaybackRate
;
i
++
)
{
this
.
frame
++
;
if
(
this
.
latestFusionedAt
<
this
.
frame
-
this
.
COMBO_INTERVAL
)
{
this
.
combo
=
0
;
}
const
log
=
logs
.
find
(
x
=>
x
.
frame
===
this
.
frame
-
1
);
if
(
log
)
{
switch
(
log
.
operation
)
{
case
'
drop
'
:
{
this
.
drop
(
log
.
x
);
break
;
}
case
'
hold
'
:
{
this
.
hold
();
break
;
}
case
'
surrender
'
:
{
this
.
surrender
();
break
;
}
default
:
break
;
}
}
this
.
tickCallbackQueue
=
this
.
tickCallbackQueue
.
filter
(
x
=>
{
if
(
x
.
frame
===
this
.
frame
)
{
x
.
callback
();
return
false
;
}
else
{
return
true
;
}
});
Matter
.
Engine
.
update
(
this
.
engine
,
this
.
TICK_DELTA
);
}
if
(
!
this
.
isGameOver
)
{
this
.
tickRaf
=
window
.
requestAnimationFrame
(
playTick
);
}
};
playTick
();
}
else
{
this
.
tick
();
}
Matter
.
Events
.
on
(
this
.
engine
,
'
collisionStart
'
,
this
.
onCollision
.
bind
(
this
));
}
public
getLogs
()
{
return
this
.
logs
;
}
p
rivate
tick
()
{
p
ublic
tick
()
{
this
.
frame
++
;
if
(
this
.
latestFusionedAt
<
this
.
frame
-
this
.
COMBO_INTERVAL
)
{
this
.
combo
=
0
;
}
this
.
tickCallbackQueue
=
this
.
tickCallbackQueue
.
filter
(
x
=>
{
if
(
x
.
frame
===
this
.
frame
)
{
x
.
callback
();
...
...
@@ -465,35 +330,12 @@ export class DropAndFusionGame extends EventEmitter<{
return
true
;
}
});
Matter
.
Engine
.
update
(
this
.
engine
,
this
.
TICK_DELTA
);
if
(
!
this
.
isGameOver
)
{
this
.
tickRaf
=
window
.
requestAnimationFrame
(
this
.
tick
);
}
}
public
async
load
()
{
await
this
.
loadMonoTextures
();
this
.
loaded
=
true
;
}
public
setSfxVolume
(
volume
:
number
)
{
this
.
sfxVolume
=
volume
;
}
Matter
.
Engine
.
update
(
this
.
engine
,
this
.
TICK_DELTA
);
public
getTextureImageUrl
(
mono
:
Mono
)
{
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if
(
this
.
monoTextureUrls
[
mono
.
img
])
{
return
this
.
monoTextureUrls
[
mono
.
img
];
const
hasNextTick
=
!
this
.
isGameOver
;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
}
else
if
(
this
.
monoTextures
[
mono
.
img
])
{
// Gameクラス内にキャッシュがある場合はそれを使う
const
out
=
URL
.
createObjectURL
(
this
.
monoTextures
[
mono
.
img
]);
this
.
monoTextureUrls
[
mono
.
img
]
=
out
;
return
out
;
}
else
{
return
mono
.
img
;
}
return
hasNextTick
;
}
public
getActiveMonos
()
{
...
...
@@ -502,6 +344,7 @@ export class DropAndFusionGame extends EventEmitter<{
public
drop
(
_x
:
number
)
{
if
(
this
.
isGameOver
)
return
;
// TODO: フレームで計算するようにすればリプレイかどうかのチェックは不要になる
if
(
!
this
.
replaying
&&
(
Date
.
now
()
-
this
.
latestDroppedAt
<
this
.
DROP_INTERVAL
))
return
;
const
head
=
this
.
stock
.
shift
()
!
;
...
...
@@ -512,7 +355,7 @@ export class DropAndFusionGame extends EventEmitter<{
this
.
emit
(
'
changeStock
'
,
this
.
stock
);
const
inputX
=
Math
.
round
(
_x
);
const
x
=
Math
.
min
(
this
.
gameWidth
-
this
.
PLAYAREA_MARGIN
-
(
head
.
mono
.
size
/
2
),
Math
.
max
(
this
.
PLAYAREA_MARGIN
+
(
head
.
mono
.
size
/
2
),
inputX
));
const
x
=
Math
.
min
(
this
.
GAME_WIDTH
-
this
.
PLAYAREA_MARGIN
-
(
head
.
mono
.
size
/
2
),
Math
.
max
(
this
.
PLAYAREA_MARGIN
+
(
head
.
mono
.
size
/
2
),
inputX
));
const
body
=
this
.
createBody
(
head
.
mono
,
x
,
50
+
head
.
mono
.
size
/
2
);
this
.
logs
.
push
({
frame
:
this
.
frame
,
...
...
@@ -523,18 +366,8 @@ export class DropAndFusionGame extends EventEmitter<{
this
.
activeBodyIds
.
push
(
body
.
id
);
this
.
latestDroppedBodyId
=
body
.
id
;
this
.
latestDroppedAt
=
Date
.
now
();
this
.
emit
(
'
dropped
'
);
this
.
emit
(
'
dropped
'
,
x
);
this
.
emit
(
'
monoAdded
'
,
head
.
mono
);
// TODO: 効果音再生はコンポーネント側の責務なので移動するべき?
const
panV
=
x
-
this
.
PLAYAREA_MARGIN
;
const
panW
=
this
.
gameWidth
-
this
.
PLAYAREA_MARGIN
-
this
.
PLAYAREA_MARGIN
;
const
pan
=
((
panV
/
panW
)
-
0.5
)
*
2
;
sound
.
playUrl
(
'
/client-assets/drop-and-fusion/poi2.mp3
'
,
{
volume
:
this
.
sfxVolume
,
pan
,
playbackRate
:
this
.
replayPlaybackRate
,
});
}
public
hold
()
{
...
...
@@ -561,17 +394,9 @@ export class DropAndFusionGame extends EventEmitter<{
this
.
emit
(
'
changeHolding
'
,
this
.
holding
);
this
.
emit
(
'
changeStock
'
,
this
.
stock
);
}
sound
.
playUrl
(
'
/client-assets/drop-and-fusion/hold.mp3
'
,
{
volume
:
0.5
*
this
.
sfxVolume
,
});
}
public
dispose
()
{
if
(
this
.
comboIntervalId
)
window
.
clearInterval
(
this
.
comboIntervalId
);
if
(
this
.
tickRaf
)
window
.
cancelAnimationFrame
(
this
.
tickRaf
);
this
.
tickRaf
=
null
;
Matter
.
Render
.
stop
(
this
.
render
);
Matter
.
World
.
clear
(
this
.
engine
.
world
,
false
);
Matter
.
Engine
.
clear
(
this
.
engine
);
}
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment