Merge remote-tracking branch 'misskey-original/develop' into develop

This commit is contained in:
mattyatea 2024-01-08 00:40:16 +09:00
commit 801697ccdd
53 changed files with 1100 additions and 173 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 67 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

View file

@ -42,6 +42,7 @@ onMounted(() => {
<style lang="scss" module>
.root {
user-select: none;
pointer-events: none;
position: fixed;
width: 128px;

View file

@ -139,6 +139,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,
});
@ -212,6 +213,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,
};
@ -250,8 +252,9 @@ function refreshEndpointAndChannel() {
updatePaginationQuery();
}
// withRenotes
// IDTL
watch(() => [props.list, props.antenna, props.channel, props.role], refreshEndpointAndChannel);
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
//
refreshEndpointAndChannel();

View file

@ -5,15 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ props.emoji }}</span>
<span v-else>{{ emoji }}</span>
<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
</template>
<script lang="ts" setup>
import { computed, inject } from 'vue';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
import { defaultStore } from '@/store.js';
import { getEmojiName } from '@/scripts/emojilist.js';
import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import * as sound from '@/scripts/sound.js';
@ -30,9 +29,8 @@ const react = inject<((name: string) => void) | null>('react', null);
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
const url = computed(() => {
return char2path(props.emoji);
});
const url = computed(() => char2path(props.emoji));
const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
function computeTitle(event: PointerEvent): void {

View file

@ -7,11 +7,32 @@ 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 v-show="!gameStarted" :class="$style.root">
<div style="text-align: center;" class="_gaps">
<div :class="$style.frame">
<div :class="$style.frameInner">
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
</div>
</div>
<div :class="$style.frame">
<div :class="$style.frameInner">
<div class="_gaps" style="padding: 16px;">
<MkSelect v-model="gameMode">
<option value="normal">NORMAL</option>
<option value="square">SQUARE</option>
</MkSelect>
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
</div>
</div>
</div>
</div>
</div>
<div v-show="gameStarted" class="_gaps_s" :class="$style.root">
<div style="display: flex;">
<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
<div :class="$style.frameInner">
SCORE: <b><MkNumber :value="score"/></b>
<b>BUBBLE GAME</b>
<div>- {{ gameMode }} -</div>
</div>
</div>
<div :class="[$style.frame, $style.stock]" style="margin-left: auto;">
@ -25,15 +46,16 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass="$style.transition_stock_move"
>
<div v-for="x in stock" :key="x.id" style="display: inline-block;">
<img :src="x.fruit.img" style="width: 32px;"/>
<img :src="x.mono.img" style="width: 32px;"/>
</div>
</TransitionGroup>
</div>
</div>
</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"/>
<div ref="containerEl" :class="[$style.container, { [$style.gameOver]: gameOver }]" @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
<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 +66,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"
@ -52,19 +75,41 @@ SPDX-License-Identifier: AGPL-3.0-only
:moveClass="$style.transition_picked_move"
mode="out-in"
>
<img v-if="currentPick" :key="currentPick.id" :src="currentPick?.fruit.img" :class="$style.currentFruit" :style="{ top: -(currentPick?.fruit.size / 2) + 'px', left: (mouseX - (currentPick?.fruit.size / 2)) + 'px', width: `${currentPick?.fruit.size}px` }"/>
<img v-if="currentPick" :key="currentPick.id" :src="currentPick?.mono.img" :class="$style.currentMono" :style="{ top: -(currentPick?.mono.size / 2) + 'px', left: (mouseX - (currentPick?.mono.size / 2)) + 'px', width: `${currentPick?.mono.size}px` }"/>
</Transition>
<template v-if="dropReady">
<img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentFruitArrow" :style="{ top: (currentPick?.fruit.size / 2) + 10 + 'px', left: (mouseX - 10) + 'px', width: `20px` }"/>
<img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow" :style="{ top: (currentPick?.mono.size / 2) + 10 + 'px', left: (mouseX - 10) + 'px', width: `20px` }"/>
<div :class="$style.dropGuide" :style="{ left: (mouseX - 2) + 'px' }"/>
</template>
<div v-if="gameOver" :class="$style.gameOverLabel">
<div>GAME OVER!</div>
<div>SCORE: <MkNumber :value="score"/></div>
<div class="_gaps_s">
<img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/>
<div>SCORE: <MkNumber :value="score"/></div>
<div class="_buttonsCenter">
<MkButton primary rounded @click="restart">Restart</MkButton>
<MkButton primary rounded @click="share">Share</MkButton>
</div>
</div>
</div>
</div>
</div>
<MkButton @click="restart">Restart</MkButton>
<div style="display: flex;">
<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
<div :class="$style.frameInner">
<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="margin-left: auto;">
<div :class="$style.frameInner" style="text-align: center;">
</div>
</div>
</div>
<div :class="$style.frame">
<div :class="$style.frameInner">
<MkButton @click="restart">Restart</MkButton>
</div>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
@ -72,8 +117,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Matter from 'matter-js';
import { Ref, onMounted, ref, shallowRef } from 'vue';
import { onMounted, ref, shallowRef } from 'vue';
import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import * as sound from '@/scripts/sound.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
@ -81,18 +127,39 @@ 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';
import { i18n } from '@/i18n.js';
import { useInterval } from '@/scripts/use-interval.js';
import MkSelect from '@/components/MkSelect.vue';
import { apiUrl } from '@/config.js';
import { $i } from '@/account.js';
type Mono = {
id: string;
level: number;
size: number;
shape: 'circle' | 'rectangle';
score: number;
dropCandidate: boolean;
sfxPitch: number;
img: string;
imgSize: number;
spriteScale: number;
};
const containerEl = shallowRef<HTMLElement>();
const canvasEl = shallowRef<HTMLCanvasElement>();
const mouseX = ref(0);
const BASE_SIZE = 30;
const FRUITS = [{
const NORMAL_BASE_SIZE = 30;
const NORAML_MONOS: Mono[] = [{
id: '9377076d-c980-4d83-bdaf-175bc58275b7',
level: 10,
size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 512,
available: false,
dropCandidate: false,
sfxPitch: 0.25,
img: '/client-assets/drop-and-fusion/exploding_head.png',
imgSize: 256,
@ -100,9 +167,10 @@ const FRUITS = [{
}, {
id: 'be9f38d2-b267-4b1a-b420-904e22e80568',
level: 9,
size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 256,
available: false,
dropCandidate: false,
sfxPitch: 0.5,
img: '/client-assets/drop-and-fusion/face_with_symbols_on_mouth.png',
imgSize: 256,
@ -110,9 +178,10 @@ const FRUITS = [{
}, {
id: 'beb30459-b064-4888-926b-f572e4e72e0c',
level: 8,
size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 128,
available: false,
dropCandidate: false,
sfxPitch: 0.75,
img: '/client-assets/drop-and-fusion/cold_face.png',
imgSize: 256,
@ -120,9 +189,10 @@ const FRUITS = [{
}, {
id: 'feab6426-d9d8-49ae-849c-048cdbb6cdf0',
level: 7,
size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 64,
available: false,
dropCandidate: false,
sfxPitch: 1,
img: '/client-assets/drop-and-fusion/zany_face.png',
imgSize: 256,
@ -130,9 +200,10 @@ const FRUITS = [{
}, {
id: 'd6d8fed6-6d18-4726-81a1-6cf2c974df8a',
level: 6,
size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 32,
available: false,
dropCandidate: false,
sfxPitch: 1.5,
img: '/client-assets/drop-and-fusion/pleading_face.png',
imgSize: 256,
@ -140,9 +211,10 @@ const FRUITS = [{
}, {
id: '249c728e-230f-4332-bbbf-281c271c75b2',
level: 5,
size: BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 16,
available: true,
dropCandidate: true,
sfxPitch: 2,
img: '/client-assets/drop-and-fusion/face_with_open_mouth.png',
imgSize: 256,
@ -150,9 +222,10 @@ const FRUITS = [{
}, {
id: '23d67613-d484-4a93-b71e-3e81b19d6186',
level: 4,
size: BASE_SIZE * 1.25 * 1.25 * 1.25,
size: NORMAL_BASE_SIZE * 1.25 * 1.25 * 1.25,
shape: 'circle',
score: 8,
available: true,
dropCandidate: true,
sfxPitch: 2.5,
img: '/client-assets/drop-and-fusion/smiling_face_with_sunglasses.png',
imgSize: 256,
@ -160,9 +233,10 @@ const FRUITS = [{
}, {
id: '3cbd0add-ad7d-4685-bad0-29f6dddc0b99',
level: 3,
size: BASE_SIZE * 1.25 * 1.25,
size: NORMAL_BASE_SIZE * 1.25 * 1.25,
shape: 'circle',
score: 4,
available: true,
dropCandidate: true,
sfxPitch: 3,
img: '/client-assets/drop-and-fusion/grinning_squinting_face.png',
imgSize: 256,
@ -170,9 +244,10 @@ const FRUITS = [{
}, {
id: '8f86d4f4-ee02-41bf-ad38-1ce0ae457fb5',
level: 2,
size: BASE_SIZE * 1.25,
size: NORMAL_BASE_SIZE * 1.25,
shape: 'circle',
score: 2,
available: true,
dropCandidate: true,
sfxPitch: 3.5,
img: '/client-assets/drop-and-fusion/smiling_face_with_hearts.png',
imgSize: 256,
@ -180,34 +255,150 @@ const FRUITS = [{
}, {
id: '64ec4add-ce39-42b4-96cb-33908f3f118d',
level: 1,
size: BASE_SIZE,
size: NORMAL_BASE_SIZE,
shape: 'circle',
score: 1,
available: true,
dropCandidate: true,
sfxPitch: 4,
img: '/client-assets/drop-and-fusion/heart_suit.png',
imgSize: 256,
spriteScale: 1.12,
}] as const;
}];
const SQUARE_BASE_SIZE = 28;
const SQUARE_MONOS: Mono[] = [{
id: 'f75fd0ba-d3d4-40a4-9712-b470e45b0525',
level: 10,
size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 512,
dropCandidate: false,
sfxPitch: 0.25,
img: '/client-assets/drop-and-fusion/keycap_10.png',
imgSize: 256,
spriteScale: 1.12,
}, {
id: '7b70f4af-1c01-45fd-af72-61b1f01e03d1',
level: 9,
size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 256,
dropCandidate: false,
sfxPitch: 0.5,
img: '/client-assets/drop-and-fusion/keycap_9.png',
imgSize: 256,
spriteScale: 1.12,
}, {
id: '41607ef3-b6d6-4829-95b6-3737bf8bb956',
level: 8,
size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 128,
dropCandidate: false,
sfxPitch: 0.75,
img: '/client-assets/drop-and-fusion/keycap_8.png',
imgSize: 256,
spriteScale: 1.12,
}, {
id: '8a8310d2-0374-460f-bb50-ca9cd3ee3416',
level: 7,
size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 64,
dropCandidate: false,
sfxPitch: 1,
img: '/client-assets/drop-and-fusion/keycap_7.png',
imgSize: 256,
spriteScale: 1.12,
}, {
id: '1092e069-fe1a-450b-be97-b5d477ec398c',
level: 6,
size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 32,
dropCandidate: false,
sfxPitch: 1.5,
img: '/client-assets/drop-and-fusion/keycap_6.png',
imgSize: 256,
spriteScale: 1.12,
}, {
id: '2294734d-7bb8-4781-bb7b-ef3820abf3d0',
level: 5,
size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 16,
dropCandidate: true,
sfxPitch: 2,
img: '/client-assets/drop-and-fusion/keycap_5.png',
imgSize: 256,
spriteScale: 1.12,
}, {
id: 'ea8a61af-e350-45f7-ba6a-366fcd65692a',
level: 4,
size: SQUARE_BASE_SIZE * 1.25 * 1.25 * 1.25,
shape: 'rectangle',
score: 8,
dropCandidate: true,
sfxPitch: 2.5,
img: '/client-assets/drop-and-fusion/keycap_4.png',
imgSize: 256,
spriteScale: 1.12,
}, {
id: 'd0c74815-fc1c-4fbe-9953-c92e4b20f919',
level: 3,
size: SQUARE_BASE_SIZE * 1.25 * 1.25,
shape: 'rectangle',
score: 4,
dropCandidate: true,
sfxPitch: 3,
img: '/client-assets/drop-and-fusion/keycap_3.png',
imgSize: 256,
spriteScale: 1.12,
}, {
id: 'd8fbd70e-611d-402d-87da-1a7fd8cd2c8d',
level: 2,
size: SQUARE_BASE_SIZE * 1.25,
shape: 'rectangle',
score: 2,
dropCandidate: true,
sfxPitch: 3.5,
img: '/client-assets/drop-and-fusion/keycap_2.png',
imgSize: 256,
spriteScale: 1.12,
}, {
id: '35e476ee-44bd-4711-ad42-87be245d3efd',
level: 1,
size: SQUARE_BASE_SIZE,
shape: 'rectangle',
score: 1,
dropCandidate: true,
sfxPitch: 4,
img: '/client-assets/drop-and-fusion/keycap_1.png',
imgSize: 256,
spriteScale: 1.12,
}];
const GAME_WIDTH = 450;
const GAME_HEIGHT = 600;
const PHYSICS_QUALITY_FACTOR = 32; //
const PHYSICS_QUALITY_FACTOR = 16; //
let viewScaleX = 1;
let viewScaleY = 1;
const currentPick = shallowRef<{ id: string; fruit: typeof FRUITS[number] } | null>(null);
const stock = shallowRef<{ id: string; fruit: typeof FRUITS[number] }[]>([]);
const currentPick = shallowRef<{ id: string; mono: Mono } | null>(null);
const stock = shallowRef<{ id: string; mono: Mono }[]>([]);
const score = ref(0);
const combo = ref(0);
const comboPrev = ref(0);
const dropReady = ref(true);
const gameMode = ref<'normal' | 'square'>('normal');
const gameOver = ref(false);
const gameStarted = ref(false);
const highScore = ref<number | null>(null);
class Game extends EventEmitter<{
changeScore: (score: number) => void;
changeCombo: (combo: number) => void;
changeStock: (stock: { id: string; fruit: typeof FRUITS[number] }[]) => void;
changeStock: (stock: { id: string; mono: Mono }[]) => void;
dropped: () => void;
fusioned: (x: number, y: number, score: number) => void;
gameOver: () => void;
@ -215,13 +406,15 @@ 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;
private monoDefinitions: Mono[] = [];
/**
* フィールドに出ていてかつ合体の対象となるアイテム
*/
@ -231,7 +424,7 @@ class Game extends EventEmitter<{
private latestDroppedAt = 0;
private latestFusionedAt = 0;
private stock: { id: string; fruit: typeof FRUITS[number] }[] = [];
private stock: { id: string; mono: Mono }[] = [];
private _combo = 0;
private get combo() {
@ -251,9 +444,15 @@ class Game extends EventEmitter<{
this.emit('changeScore', value);
}
constructor() {
private comboIntervalId: number | null = null;
constructor(opts: {
monoDefinitions: Mono[];
}) {
super();
this.monoDefinitions = opts.monoDefinitions;
this.engine = Matter.Engine.create({
constraintIterations: 2 * PHYSICS_QUALITY_FACTOR,
positionIterations: 6 * PHYSICS_QUALITY_FACTOR,
@ -278,7 +477,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 +486,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 +507,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: {
@ -325,23 +524,32 @@ 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,
private createBody(mono: Mono, x: number, y: number) {
const options: Matter.IBodyDefinition = {
label: mono.id,
//density: 0.0005,
density: mono.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: {
texture: fruit.img,
xScale: (fruit.size / fruit.imgSize) * fruit.spriteScale,
yScale: (fruit.size / fruit.imgSize) * fruit.spriteScale,
texture: mono.img,
xScale: (mono.size / mono.imgSize) * mono.spriteScale,
yScale: (mono.size / mono.imgSize) * mono.spriteScale,
},
},
});
};
if (mono.shape === 'circle') {
return Matter.Bodies.circle(x, y, mono.size / 2, options);
} else if (mono.shape === 'rectangle') {
return Matter.Bodies.rectangle(x, y, mono.size, mono.size, options);
} else {
throw new Error('unrecognized shape');
}
}
private fusion(bodyA: Matter.Body, bodyB: Matter.Body) {
@ -360,11 +568,11 @@ class Game extends EventEmitter<{
Matter.Composite.remove(this.engine.world, [bodyA, bodyB]);
this.activeBodyIds = this.activeBodyIds.filter(x => x !== bodyA.id && x !== bodyB.id);
const currentFruit = FRUITS.find(y => y.id === bodyA.label)!;
const nextFruit = FRUITS.find(x => x.level === currentFruit.level + 1);
const currentMono = this.monoDefinitions.find(y => y.id === bodyA.label)!;
const nextMono = this.monoDefinitions.find(x => x.level === currentMono.level + 1);
if (nextFruit) {
const body = this.createBody(nextFruit, newX, newY);
if (nextMono) {
const body = this.createBody(nextMono, newX, newY);
Matter.Composite.add(this.engine.world, body);
// fusionfusion
@ -372,11 +580,12 @@ class Game extends EventEmitter<{
this.activeBodyIds.push(body.id);
}, 100);
const additionalScore = Math.round(currentFruit.score * (1 + (this.combo / 3)));
const comboBonus = 1 + ((this.combo - 1) / 5);
const additionalScore = Math.round(currentMono.score * comboBonus);
this.score += additionalScore;
const pan = ((newX / GAME_WIDTH) - 0.5) * 2;
sound.playRaw('syuilo/bubble2', 1, pan, nextFruit.sfxPitch);
sound.playRaw('syuilo/bubble2', 1, pan, nextMono.sfxPitch);
this.emit('fusioned', newX, newY, additionalScore);
} else {
@ -400,10 +609,10 @@ 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)],
mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
});
}
this.emit('changeStock', this.stock);
@ -411,8 +620,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 +648,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 +658,7 @@ class Game extends EventEmitter<{
}
});
window.setInterval(() => {
this.comboIntervalId = window.setInterval(() => {
if (this.latestFusionedAt < Date.now() - this.COMBO_INTERVAL) {
this.combo = 0;
}
@ -464,12 +673,12 @@ class Game extends EventEmitter<{
const st = this.stock.shift()!;
this.stock.push({
id: Math.random().toString(),
fruit: FRUITS.filter(x => x.available)[Math.floor(Math.random() * FRUITS.filter(x => x.available).length)],
mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)],
});
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 x = Math.min(GAME_WIDTH - this.PLAYAREA_MARGIN - (st.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (st.mono.size / 2), _x));
const body = this.createBody(st.mono, x, 50 + st.mono.size / 2);
Matter.Composite.add(this.engine.world, body);
this.activeBodyIds.push(body.id);
this.latestDroppedBodyId = body.id;
@ -480,6 +689,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);
@ -490,7 +700,7 @@ class Game extends EventEmitter<{
let game: Game;
function onClick(ev: MouseEvent) {
const rect = containerEl.value.getBoundingClientRect();
const rect = containerEl.value!.getBoundingClientRect();
const x = (ev.clientX - rect.left) / viewScaleX;
@ -498,7 +708,7 @@ function onClick(ev: MouseEvent) {
}
function onTouchend(ev: TouchEvent) {
const rect = containerEl.value.getBoundingClientRect();
const rect = containerEl.value!.getBoundingClientRect();
const x = (ev.changedTouches[0].clientX - rect.left) / viewScaleX;
@ -506,11 +716,11 @@ function onTouchend(ev: TouchEvent) {
}
function onMousemove(ev: MouseEvent) {
mouseX.value = ev.clientX - containerEl.value.getBoundingClientRect().left;
mouseX.value = ev.clientX - containerEl.value!.getBoundingClientRect().left;
}
function onTouchmove(ev: TouchEvent) {
mouseX.value = ev.touches[0].clientX - containerEl.value.getBoundingClientRect().left;
mouseX.value = ev.touches[0].clientX - containerEl.value!.getBoundingClientRect().left;
}
function restart() {
@ -522,9 +732,7 @@ function restart() {
score.value = 0;
combo.value = 0;
comboPrev.value = 0;
game = new Game();
attachGame();
game.start();
gameStarted.value = false;
}
function attachGame() {
@ -567,24 +775,91 @@ 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:' + gameMode.value,
value: highScore.value,
});
}
});
}
onMounted(() => {
game = new Game();
async function start() {
try {
highScore.value = await misskeyApi('i/registry/get', {
scope: ['dropAndFusionGame'],
key: 'highScore:' + gameMode.value,
});
} catch (err) {
}
gameStarted.value = true;
game = new Game(gameMode.value === 'normal' ? {
monoDefinitions: NORAML_MONOS,
} : {
monoDefinitions: SQUARE_MONOS,
});
attachGame();
game.start();
}
function getGameImageDriveFile() {
return new Promise<Misskey.entities.DriveFile | null>(res => {
canvasEl.value?.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');
});
}
async function share() {
const uploading = getGameImageDriveFile();
os.promiseDialog(uploading);
const file = await uploading;
if (!file) return;
os.post({
initialText: `#BubbleGame
MODE: ${gameMode.value}
SCORE: ${score.value}`,
initialFiles: [file],
});
}
useInterval(() => {
if (!canvasEl.value) return;
const actualCanvasWidth = canvasEl.value.getBoundingClientRect().width;
const actualCanvasHeight = canvasEl.value.getBoundingClientRect().height;
viewScaleX = actualCanvasWidth / GAME_WIDTH;
viewScaleY = actualCanvasHeight / GAME_HEIGHT;
}, 1000, { immediate: false, afterMounted: true });
onMounted(async () => {
});
definePageMetadata({
title: 'Drop & Fusion',
title: i18n.ts.bubbleGame,
icon: 'ti ti-apple',
});
</script>
@ -637,6 +912,8 @@ definePageMetadata({
}
.root {
margin: 0 auto;
max-width: 600px;
user-select: none;
* {
@ -667,7 +944,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 +955,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,34 +978,49 @@ 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 {
.currentMono {
position: absolute;
margin-top: 20px;
margin-top: 80px;
z-index: 2;
filter: drop-shadow(0 6px 16px #0007);
pointer-events: none;
user-select: none;
}
.currentFruitArrow {
.dropper {
position: absolute;
margin-top: 20px;
top: 0;
width: 70px;
margin-top: -10px;
margin-left: -30px;
z-index: 2;
filter: drop-shadow(0 6px 16px #0007);
pointer-events: none;
user-select: none;
}
.currentMonoArrow {
position: absolute;
margin-top: 100px;
z-index: 3;
animation: currentFruitArrow 2s ease infinite;
animation: currentMonoArrow 2s ease infinite;
pointer-events: none;
user-select: none;
}
.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;
@ -751,7 +1045,7 @@ definePageMetadata({
}
}
@keyframes currentFruitArrow {
@keyframes currentMonoArrow {
0% { transform: translateY(0); }
25% { transform: translateY(-8px); }
50% { transform: translateY(0); }

View file

@ -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,

View file

@ -532,7 +532,7 @@ export const routes = [{
component: page(() => import('./pages/clicker.vue')),
loginRequired: true,
}, {
path: '/drop-and-fusion',
path: '/bubble-game',
component: page(() => import('./pages/drop-and-fusion.vue')),
loginRequired: true,
}, {

View file

@ -218,7 +218,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't
};
}
function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'type'> {
function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiMfm, 'id' | 'type'> {
utils.assertObject(def);
const text = def.value.get('text');
@ -241,7 +241,7 @@ function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'typ
color: color?.value,
font: font?.value,
onClickEv: (evId: string) => {
if (onClickEv) call(onClickEv, values.STR(evId));
if (onClickEv) call(onClickEv, [values.STR(evId)]);
},
};
}

View file

@ -36,7 +36,8 @@ for (let i = 0; i < emojilist.length; i++) {
export const emojiCharByCategory = _charGroupByCategory;
export function getEmojiName(char: string): string | null {
const idx = _indexByChar.get(char);
// Colorize it because emojilist.json assumes that
const idx = _indexByChar.get(colorizeEmoji(char));
if (idx == null) {
return null;
} else {
@ -44,6 +45,10 @@ export function getEmojiName(char: string): string | null {
}
}
export function colorizeEmoji(char: string) {
return char.length === 1 ? `${char}\uFE0F` : char;
}
export interface CustomEmojiFolderTree {
value: string;
category: string;

View file

@ -29,8 +29,8 @@ function toolsMenuItems(): MenuItem[] {
icon: 'ti ti-cookie',
}, {
type: 'link',
to: '/drop-and-fusion',
text: 'Drop & Fusion',
to: '/bubble-game',
text: i18n.ts.bubbleGame,
icon: 'ti ti-apple',
}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
type: 'link',

View file

@ -80,13 +80,13 @@ import * as Misskey from 'misskey-js';
import { v4 as uuid } from 'uuid';
const props = defineProps<{
connection: any,
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse
}>();
const viewBoxX = ref<number>(50);
const viewBoxY = ref<number>(30);
const stats = ref<any[]>([]);
const stats = ref<Misskey.entities.ServerStats[]>([]);
const cpuGradientId = uuid();
const cpuMaskId = uuid();
const memGradientId = uuid();
@ -107,6 +107,7 @@ onMounted(() => {
props.connection.on('statsLog', onStatsLog);
props.connection.send('requestLog', {
id: Math.random().toString().substring(2, 10),
length: 50,
});
});
@ -115,7 +116,7 @@ onBeforeUnmount(() => {
props.connection.off('statsLog', onStatsLog);
});
function onStats(connStats) {
function onStats(connStats: Misskey.entities.ServerStats) {
stats.value.push(connStats);
if (stats.value.length > 50) stats.value.shift();
@ -136,8 +137,8 @@ function onStats(connStats) {
memP.value = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0);
}
function onStatsLog(statsLog) {
for (const revStats of [...statsLog].reverse()) {
function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
for (const revStats of statsLog.reverse()) {
onStats(revStats);
}
}

View file

@ -20,13 +20,13 @@ import * as Misskey from 'misskey-js';
import XPie from './pie.vue';
const props = defineProps<{
connection: any,
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse
}>();
const usage = ref<number>(0);
function onStats(stats) {
function onStats(stats: Misskey.entities.ServerStats) {
usage.value = stats.cpu;
}

View file

@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onUnmounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { useWidgetPropsManager, Widget, WidgetComponentExpose } from '../widget.js';
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget.js';
import XCpuMemory from './cpu-mem.vue';
import XNet from './net.vue';
import XCpu from './cpu.vue';
@ -54,11 +54,8 @@ const widgetPropsDef = {
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
const props = defineProps<WidgetComponentProps<WidgetProps>>();
const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const { widgetProps, configure, save } = useWidgetPropsManager(name,
widgetPropsDef,

View file

@ -22,7 +22,7 @@ import XPie from './pie.vue';
import bytes from '@/filters/bytes.js';
const props = defineProps<{
connection: any,
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse
}>();
@ -31,7 +31,7 @@ const total = ref<number>(0);
const used = ref<number>(0);
const free = ref<number>(0);
function onStats(stats) {
function onStats(stats: Misskey.entities.ServerStats) {
usage.value = stats.mem.active / props.meta.mem.total;
total.value = props.meta.mem.total;
used.value = stats.mem.active;

View file

@ -54,13 +54,13 @@ import * as Misskey from 'misskey-js';
import bytes from '@/filters/bytes.js';
const props = defineProps<{
connection: any,
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse
}>();
const viewBoxX = ref<number>(50);
const viewBoxY = ref<number>(30);
const stats = ref<any[]>([]);
const stats = ref<Misskey.entities.ServerStats[]>([]);
const inPolylinePoints = ref<string>('');
const outPolylinePoints = ref<string>('');
const inPolygonPoints = ref<string>('');
@ -77,6 +77,7 @@ onMounted(() => {
props.connection.on('statsLog', onStatsLog);
props.connection.send('requestLog', {
id: Math.random().toString().substring(2, 10),
length: 50,
});
});
@ -85,7 +86,7 @@ onBeforeUnmount(() => {
props.connection.off('statsLog', onStatsLog);
});
function onStats(connStats) {
function onStats(connStats: Misskey.entities.ServerStats) {
stats.value.push(connStats);
if (stats.value.length > 50) stats.value.shift();
@ -109,8 +110,8 @@ function onStats(connStats) {
outRecent.value = connStats.net.tx;
}
function onStatsLog(statsLog) {
for (const revStats of [...statsLog].reverse()) {
function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
for (const revStats of statsLog.reverse()) {
onStats(revStats);
}
}

View file

@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { describe, test, assert, afterEach } from 'vitest';
import { render, cleanup, type RenderResult } from '@testing-library/vue';
import { defaultStoreState } from './init.js';
import { getEmojiName } from '@/scripts/emojilist.js';
import { components } from '@/components/index.js';
import { directives } from '@/directives/index.js';
import MkEmoji from '@/components/global/MkEmoji.vue';
describe('Emoji', () => {
const renderEmoji = (emoji: string): RenderResult => {
return render(MkEmoji, {
props: { emoji },
global: { directives, components },
});
};
afterEach(() => {
cleanup();
defaultStoreState.emojiStyle = '';
});
describe('MkEmoji', () => {
test('Should render selector-less heart with color in native mode', async () => {
defaultStoreState.emojiStyle = 'native';
const mkEmoji = await renderEmoji('\u2764'); // monochrome heart
assert.ok(mkEmoji.queryByText('\u2764\uFE0F')); // colored heart
assert.ok(!mkEmoji.queryByText('\u2764'));
});
});
describe('Emoji list', () => {
test('Should get the name of the heart', () => {
assert.strictEqual(getEmojiName('\u2764'), 'heart');
});
});
});

View file

@ -17,21 +17,23 @@ updateI18n(locales['en-US']);
// XXX: misskey-js panics if WebSocket is not defined
vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; });
export const defaultStoreState: Record<string, unknown> = {
// なんかtestがうまいこと動かないのでここに書く
dataSaver: {
media: false,
avatar: false,
urlPreview: false,
code: false,
},
};
// XXX: defaultStore somehow becomes undefined in vitest?
vi.mock('@/store.js', () => {
return {
defaultStore: {
state: {
// なんかtestがうまいこと動かないのでここに書く
dataSaver: {
media: false,
avatar: false,
urlPreview: false,
code: false,
},
},
state: defaultStoreState,
},
};
});