Merge branch 'develop' into feat-12909
This commit is contained in:
BIN
packages/frontend/assets/drop-and-fusion/dropper.png
Normal file
BIN
packages/frontend/assets/drop-and-fusion/dropper.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
28
packages/frontend/assets/drop-and-fusion/frame-dark.svg
Normal file
28
packages/frontend/assets/drop-and-fusion/frame-dark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 67 KiB |
28
packages/frontend/assets/drop-and-fusion/frame-light.svg
Normal file
28
packages/frontend/assets/drop-and-fusion/frame-light.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 66 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 68 KiB |
@@ -132,6 +132,7 @@ function connectChannel() {
|
||||
connection.on('mention', onNote);
|
||||
} else if (props.src === 'list') {
|
||||
connection = stream.useChannel('userList', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
});
|
||||
@@ -198,6 +199,7 @@ function updatePaginationQuery() {
|
||||
} else if (props.src === 'list') {
|
||||
endpoint = 'notes/user-list-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
};
|
||||
@@ -236,8 +238,9 @@ function refreshEndpointAndChannel() {
|
||||
updatePaginationQuery();
|
||||
}
|
||||
|
||||
// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる
|
||||
// IDが切り替わったら切り替え先のTLを表示させたい
|
||||
watch(() => [props.list, props.antenna, props.channel, props.role], refreshEndpointAndChannel);
|
||||
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
|
||||
|
||||
// 初回表示用
|
||||
refreshEndpointAndChannel();
|
||||
|
||||
@@ -7,11 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div class="_gaps_s" :class="$style.root" style="margin: 0 auto;" :style="{ maxWidth: GAME_WIDTH + 'px' }">
|
||||
<div class="_gaps_s" :class="$style.root" style="margin: 0 auto; max-width: 600px;">
|
||||
<div style="display: flex;">
|
||||
<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
|
||||
<div :class="$style.frameInner">
|
||||
SCORE: <b><MkNumber :value="score"/></b>
|
||||
<div>SCORE: <b><MkNumber :value="score"/></b></div>
|
||||
<div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/></b><b v-else>-</b></div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="[$style.frame, $style.stock]" style="margin-left: auto;">
|
||||
@@ -33,7 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div :class="$style.main">
|
||||
<div ref="containerEl" :class="[$style.container, { [$style.gameOver]: gameOver }]" @click.stop.prevent="onClick" @touchmove="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
|
||||
<img src="/client-assets/drop-and-fusion/frame.svg" :class="$style.mainFrameImg"/>
|
||||
<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
|
||||
<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
|
||||
<canvas ref="canvasEl" :class="$style.canvas"/>
|
||||
<Transition
|
||||
:enterActiveClass="$style.transition_combo_enterActive"
|
||||
@@ -44,6 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
>
|
||||
<div v-show="combo > 1" :class="$style.combo" :style="{ fontSize: `${100 + ((comboPrev - 2) * 15)}%` }">{{ comboPrev }} Chain!</div>
|
||||
</Transition>
|
||||
<img v-if="currentPick" src="/client-assets/drop-and-fusion/dropper.png" :class="$style.dropper" :style="{ left: mouseX + 'px' }"/>
|
||||
<Transition
|
||||
:enterActiveClass="$style.transition_picked_enterActive"
|
||||
:leaveActiveClass="$style.transition_picked_leaveActive"
|
||||
@@ -81,6 +84,8 @@ import * as os from '@/os.js';
|
||||
import MkNumber from '@/components/MkNumber.vue';
|
||||
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
||||
const containerEl = shallowRef<HTMLElement>();
|
||||
const canvasEl = shallowRef<HTMLCanvasElement>();
|
||||
@@ -191,7 +196,7 @@ const FRUITS = [{
|
||||
|
||||
const GAME_WIDTH = 450;
|
||||
const GAME_HEIGHT = 600;
|
||||
const PHYSICS_QUALITY_FACTOR = 32; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる
|
||||
const PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
|
||||
|
||||
let viewScaleX = 1;
|
||||
let viewScaleY = 1;
|
||||
@@ -203,6 +208,7 @@ const comboPrev = ref(0);
|
||||
const dropReady = ref(true);
|
||||
const gameOver = ref(false);
|
||||
const gameStarted = ref(false);
|
||||
const highScore = ref<number | null>(null);
|
||||
|
||||
class Game extends EventEmitter<{
|
||||
changeScore: (score: number) => void;
|
||||
@@ -215,10 +221,10 @@ class Game extends EventEmitter<{
|
||||
private COMBO_INTERVAL = 1000;
|
||||
public readonly DROP_INTERVAL = 500;
|
||||
private PLAYAREA_MARGIN = 25;
|
||||
private STOCK_MAX = 4;
|
||||
private engine: Matter.Engine;
|
||||
private render: Matter.Render;
|
||||
private runner: Matter.Runner;
|
||||
private detector: Matter.Detector;
|
||||
private overflowCollider: Matter.Body;
|
||||
private isGameOver = false;
|
||||
|
||||
@@ -251,6 +257,8 @@ class Game extends EventEmitter<{
|
||||
this.emit('changeScore', value);
|
||||
}
|
||||
|
||||
private comboIntervalId: number | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@@ -278,7 +286,7 @@ class Game extends EventEmitter<{
|
||||
wireframeBackground: 'transparent', // transparent to hide
|
||||
wireframes: false,
|
||||
showSleeping: false,
|
||||
pixelRatio: window.devicePixelRatio,
|
||||
pixelRatio: Math.max(2, window.devicePixelRatio),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -287,13 +295,13 @@ class Game extends EventEmitter<{
|
||||
this.runner = Matter.Runner.create();
|
||||
Matter.Runner.run(this.runner, this.engine);
|
||||
|
||||
this.detector = Matter.Detector.create();
|
||||
|
||||
this.engine.world.bodies = [];
|
||||
|
||||
//#region walls
|
||||
const WALL_OPTIONS: Matter.IChamferableBodyDefinition = {
|
||||
isStatic: true,
|
||||
friction: 0.7,
|
||||
slop: 1.0,
|
||||
render: {
|
||||
strokeStyle: 'transparent',
|
||||
fillStyle: 'transparent',
|
||||
@@ -308,7 +316,7 @@ class Game extends EventEmitter<{
|
||||
]);
|
||||
//#endregion
|
||||
|
||||
this.overflowCollider = Matter.Bodies.rectangle(GAME_WIDTH / 2, 0, GAME_WIDTH, 125, {
|
||||
this.overflowCollider = Matter.Bodies.rectangle(GAME_WIDTH / 2, 0, GAME_WIDTH, 200, {
|
||||
isStatic: true,
|
||||
isSensor: true,
|
||||
render: {
|
||||
@@ -328,11 +336,13 @@ class Game extends EventEmitter<{
|
||||
private createBody(fruit: typeof FRUITS[number], x: number, y: number) {
|
||||
return Matter.Bodies.circle(x, y, fruit.size / 2, {
|
||||
label: fruit.id,
|
||||
density: 0.0005,
|
||||
//density: 0.0005,
|
||||
density: fruit.size / 1000,
|
||||
restitution: 0.2,
|
||||
frictionAir: 0.01,
|
||||
restitution: 0.4,
|
||||
friction: 0.5,
|
||||
friction: 0.7,
|
||||
frictionStatic: 5,
|
||||
slop: 1.0,
|
||||
//mass: 0,
|
||||
render: {
|
||||
sprite: {
|
||||
@@ -372,7 +382,7 @@ class Game extends EventEmitter<{
|
||||
this.activeBodyIds.push(body.id);
|
||||
}, 100);
|
||||
|
||||
const additionalScore = Math.round(currentFruit.score * (1 + (this.combo / 3)));
|
||||
const additionalScore = Math.round(currentFruit.score * (1 + ((this.combo - 1) / 3)));
|
||||
this.score += additionalScore;
|
||||
|
||||
const pan = ((newX / GAME_WIDTH) - 0.5) * 2;
|
||||
@@ -400,7 +410,7 @@ class Game extends EventEmitter<{
|
||||
}
|
||||
|
||||
public start() {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let i = 0; i < this.STOCK_MAX; i++) {
|
||||
this.stock.push({
|
||||
id: Math.random().toString(),
|
||||
fruit: FRUITS.filter(x => x.available)[Math.floor(Math.random() * FRUITS.filter(x => x.available).length)],
|
||||
@@ -411,8 +421,8 @@ class Game extends EventEmitter<{
|
||||
// TODO: fusion予約状態のアイテムは光らせるなどの演出をすると楽しそう
|
||||
let fusionReservedPairs: { bodyA: Matter.Body; bodyB: Matter.Body }[] = [];
|
||||
|
||||
const minCollisionDepthForSound = 2.5;
|
||||
const maxCollisionDepthForSound = 9;
|
||||
const minCollisionEnergyForSound = 2.5;
|
||||
const maxCollisionEnergyForSound = 9;
|
||||
const soundPitchMax = 4;
|
||||
const soundPitchMin = 0.5;
|
||||
|
||||
@@ -439,8 +449,8 @@ class Game extends EventEmitter<{
|
||||
}
|
||||
} else {
|
||||
const energy = pairs.collision.depth;
|
||||
if (energy > minCollisionDepthForSound) {
|
||||
const vol = (Math.min(maxCollisionDepthForSound, energy - minCollisionDepthForSound) / maxCollisionDepthForSound) / 4;
|
||||
if (energy > minCollisionEnergyForSound) {
|
||||
const vol = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4;
|
||||
const pan = ((((bodyA.position.x + bodyB.position.x) / 2) / GAME_WIDTH) - 0.5) * 2;
|
||||
const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10)));
|
||||
sound.playRaw('syuilo/poi1', vol, pan, pitch);
|
||||
@@ -449,7 +459,7 @@ class Game extends EventEmitter<{
|
||||
}
|
||||
});
|
||||
|
||||
window.setInterval(() => {
|
||||
this.comboIntervalId = window.setInterval(() => {
|
||||
if (this.latestFusionedAt < Date.now() - this.COMBO_INTERVAL) {
|
||||
this.combo = 0;
|
||||
}
|
||||
@@ -469,7 +479,7 @@ class Game extends EventEmitter<{
|
||||
this.emit('changeStock', this.stock);
|
||||
|
||||
const x = Math.min(GAME_WIDTH - this.PLAYAREA_MARGIN - (st.fruit.size / 2), Math.max(this.PLAYAREA_MARGIN + (st.fruit.size / 2), _x));
|
||||
const body = this.createBody(st.fruit, x, st.fruit.size / 2);
|
||||
const body = this.createBody(st.fruit, x, 50 + st.fruit.size / 2);
|
||||
Matter.Composite.add(this.engine.world, body);
|
||||
this.activeBodyIds.push(body.id);
|
||||
this.latestDroppedBodyId = body.id;
|
||||
@@ -480,6 +490,7 @@ class Game extends EventEmitter<{
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
if (this.comboIntervalId) window.clearInterval(this.comboIntervalId);
|
||||
Matter.Render.stop(this.render);
|
||||
Matter.Runner.stop(this.runner);
|
||||
Matter.World.clear(this.engine.world, false);
|
||||
@@ -567,10 +578,28 @@ function attachGame() {
|
||||
currentPick.value = null;
|
||||
dropReady.value = false;
|
||||
gameOver.value = true;
|
||||
|
||||
if (score.value > (highScore.value ?? 0)) {
|
||||
highScore.value = score.value;
|
||||
|
||||
misskeyApi('i/registry/set', {
|
||||
scope: ['dropAndFusionGame'],
|
||||
key: 'highScore',
|
||||
value: highScore.value,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
try {
|
||||
highScore.value = await misskeyApi('i/registry/get', {
|
||||
scope: ['dropAndFusionGame'],
|
||||
key: 'highScore',
|
||||
});
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
game = new Game();
|
||||
|
||||
attachGame();
|
||||
@@ -667,7 +696,8 @@ definePageMetadata({
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
filter: drop-shadow(0 6px 16px #0007);
|
||||
// なんかiOSでちらつく
|
||||
//filter: drop-shadow(0 6px 16px #0007);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -677,7 +707,8 @@ definePageMetadata({
|
||||
display: block;
|
||||
z-index: 1;
|
||||
margin-top: -50px;
|
||||
max-width: 100%;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -699,13 +730,28 @@ definePageMetadata({
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-style: oblique;
|
||||
color: #fff;
|
||||
-webkit-text-stroke: 1px rgb(255, 145, 0);
|
||||
text-shadow: 0 0 6px #0005;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.currentFruit {
|
||||
position: absolute;
|
||||
margin-top: 20px;
|
||||
margin-top: 80px;
|
||||
z-index: 2;
|
||||
filter: drop-shadow(0 6px 16px #0007);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dropper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 70px;
|
||||
margin-top: -10px;
|
||||
margin-left: -30px;
|
||||
z-index: 2;
|
||||
filter: drop-shadow(0 6px 16px #0007);
|
||||
pointer-events: none;
|
||||
@@ -714,7 +760,7 @@ definePageMetadata({
|
||||
|
||||
.currentFruitArrow {
|
||||
position: absolute;
|
||||
margin-top: 20px;
|
||||
margin-top: 100px;
|
||||
z-index: 3;
|
||||
animation: currentFruitArrow 2s ease infinite;
|
||||
pointer-events: none;
|
||||
@@ -723,10 +769,10 @@ definePageMetadata({
|
||||
|
||||
.dropGuide {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
top: 120px;
|
||||
z-index: 3;
|
||||
width: 3px;
|
||||
height: calc(100% - 50px);
|
||||
height: calc(100% - 120px);
|
||||
background: #f002;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
|
||||
@@ -21,6 +21,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkButton primary :class="$style.button" inline @click="exportFavorites()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||
</MkFolder>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label><i class="ti ti-star"></i> {{ i18n.ts._exportOrImport.clips }}</template>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.export }}</template>
|
||||
<template #icon><i class="ti ti-download"></i></template>
|
||||
<MkButton primary :class="$style.button" inline @click="exportClips()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
|
||||
</MkFolder>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.followingList }}</template>
|
||||
<div class="_gaps_s">
|
||||
@@ -157,6 +165,10 @@ const exportFavorites = () => {
|
||||
misskeyApi('i/export-favorites', {}).then(onExportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
const exportClips = () => {
|
||||
misskeyApi('i/export-clips', {}).then(onExportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
const exportFollowing = () => {
|
||||
misskeyApi('i/export-following', {
|
||||
excludeMuting: excludeMutingUsers.value,
|
||||
|
||||
Reference in New Issue
Block a user