merge: upstream
This commit is contained in:
commit
4df3145993
@ -9,6 +9,9 @@
|
|||||||
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
||||||
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
|
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
|
||||||
- Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように
|
- Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように
|
||||||
|
- Enhance: AiScriptを0.17.0に更新 [CHANGELOG](https://github.com/aiscript-dev/aiscript/blob/bb89d132b633a622d3cb0eff0d0cc7e476c0cfdd/CHANGELOG.md)
|
||||||
|
- 配列の範囲外・非整数のインデックスへの代入が完全禁止になるので注意
|
||||||
|
- Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように
|
||||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
||||||
|
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
@ -1242,6 +1242,14 @@ export interface Locale {
|
|||||||
"showReplay": string;
|
"showReplay": string;
|
||||||
"replay": string;
|
"replay": string;
|
||||||
"replaying": string;
|
"replaying": string;
|
||||||
|
"_bubbleGame": {
|
||||||
|
"howToPlay": string;
|
||||||
|
"_howToPlay": {
|
||||||
|
"section1": string;
|
||||||
|
"section2": string;
|
||||||
|
"section3": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
|
@ -1240,6 +1240,13 @@ showReplay: "リプレイを見る"
|
|||||||
replay: "リプレイ"
|
replay: "リプレイ"
|
||||||
replaying: "リプレイ中"
|
replaying: "リプレイ中"
|
||||||
|
|
||||||
|
_bubbleGame:
|
||||||
|
howToPlay: "遊び方"
|
||||||
|
_howToPlay:
|
||||||
|
section1: "位置を調整してハコにモノを落とします。"
|
||||||
|
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
|
||||||
|
section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
forExistingUsersDescription: "有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
|
forExistingUsersDescription: "有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
"@rollup/plugin-replace": "5.0.5",
|
"@rollup/plugin-replace": "5.0.5",
|
||||||
"@rollup/pluginutils": "5.1.0",
|
"@rollup/pluginutils": "5.1.0",
|
||||||
"@sharkey/sfm-js": "0.24.4",
|
"@sharkey/sfm-js": "0.24.4",
|
||||||
"@syuilo/aiscript": "0.16.0",
|
"@syuilo/aiscript": "0.17.0",
|
||||||
"@phosphor-icons/web": "^2.0.3",
|
"@phosphor-icons/web": "^2.0.3",
|
||||||
"@twemoji/parser": "15.0.0",
|
"@twemoji/parser": "15.0.0",
|
||||||
"@vitejs/plugin-vue": "5.0.2",
|
"@vitejs/plugin-vue": "5.0.2",
|
||||||
|
@ -262,14 +262,23 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30):
|
|||||||
}
|
}
|
||||||
|
|
||||||
const matched = new Map<string, EmojiScore>();
|
const matched = new Map<string, EmojiScore>();
|
||||||
|
// 完全一致(エイリアス込み)
|
||||||
|
emojiDb.some(x => {
|
||||||
|
if (x.name.toLowerCase() === query && !matched.has(x.aliasOf ?? x.name)) {
|
||||||
|
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
|
||||||
|
}
|
||||||
|
return matched.size === max;
|
||||||
|
});
|
||||||
|
|
||||||
// 前方一致(エイリアスなし)
|
// 前方一致(エイリアスなし)
|
||||||
|
if (matched.size < max) {
|
||||||
emojiDb.some(x => {
|
emojiDb.some(x => {
|
||||||
if (x.name.toLowerCase().startsWith(query) && !x.aliasOf) {
|
if (x.name.startsWith(query) && !x.aliasOf) {
|
||||||
matched.set(x.name, { emoji: x, score: query.length + 1 });
|
matched.set(x.name, { emoji: x, score: query.length + 1 });
|
||||||
}
|
}
|
||||||
return matched.size === max;
|
return matched.size === max;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 前方一致(エイリアス込み)
|
// 前方一致(エイリアス込み)
|
||||||
if (matched.size < max) {
|
if (matched.size < max) {
|
||||||
|
@ -221,6 +221,19 @@ watch(q, () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (customEmojisMap.has(newQ)) {
|
||||||
|
matches.add(customEmojisMap.get(newQ)!);
|
||||||
|
}
|
||||||
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
|
for (const emoji of emojis) {
|
||||||
|
if (emoji.aliases.some(alias => alias === newQ)) {
|
||||||
|
matches.add(emoji);
|
||||||
|
if (matches.size >= max) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matches.size >= max) return matches;
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
if (emoji.name.startsWith(newQ)) {
|
if (emoji.name.startsWith(newQ)) {
|
||||||
matches.add(emoji);
|
matches.add(emoji);
|
||||||
|
1052
packages/frontend/src/pages/drop-and-fusion.game.vue
Normal file
1052
packages/frontend/src/pages/drop-and-fusion.game.vue
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -33,6 +33,7 @@ type Log = {
|
|||||||
operation: 'surrender';
|
operation: 'surrender';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: インスタンスを作り直さなくてもゲームをリスタートできるようにする
|
||||||
export class DropAndFusionGame extends EventEmitter<{
|
export class DropAndFusionGame extends EventEmitter<{
|
||||||
changeScore: (newScore: number) => void;
|
changeScore: (newScore: number) => void;
|
||||||
changeCombo: (newCombo: number) => void;
|
changeCombo: (newCombo: number) => void;
|
||||||
@ -44,7 +45,7 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
gameOver: () => void;
|
gameOver: () => void;
|
||||||
}> {
|
}> {
|
||||||
private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
|
private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
|
||||||
private COMBO_INTERVAL = 1000;
|
private COMBO_INTERVAL = 60; // frame
|
||||||
public readonly DROP_INTERVAL = 500;
|
public readonly DROP_INTERVAL = 500;
|
||||||
public readonly PLAYAREA_MARGIN = 25;
|
public readonly PLAYAREA_MARGIN = 25;
|
||||||
private STOCK_MAX = 4;
|
private STOCK_MAX = 4;
|
||||||
@ -76,7 +77,7 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
private latestDroppedBodyId: Matter.Body['id'] | null = null;
|
private latestDroppedBodyId: Matter.Body['id'] | null = null;
|
||||||
|
|
||||||
private latestDroppedAt = 0;
|
private latestDroppedAt = 0;
|
||||||
private latestFusionedAt = 0;
|
private latestFusionedAt = 0; // frame
|
||||||
private stock: { id: string; mono: Mono }[] = [];
|
private stock: { id: string; mono: Mono }[] = [];
|
||||||
private holding: { id: string; mono: Mono } | null = null;
|
private holding: { id: string; mono: Mono } | null = null;
|
||||||
|
|
||||||
@ -100,6 +101,8 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
|
|
||||||
private comboIntervalId: number | null = null;
|
private comboIntervalId: number | null = null;
|
||||||
|
|
||||||
|
public replayPlaybackRate = 1;
|
||||||
|
|
||||||
constructor(opts: {
|
constructor(opts: {
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
width: number;
|
width: number;
|
||||||
@ -155,6 +158,7 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
|
|
||||||
//#region walls
|
//#region walls
|
||||||
const WALL_OPTIONS: Matter.IChamferableBodyDefinition = {
|
const WALL_OPTIONS: Matter.IChamferableBodyDefinition = {
|
||||||
|
label: '_wall_',
|
||||||
isStatic: true,
|
isStatic: true,
|
||||||
friction: 0.7,
|
friction: 0.7,
|
||||||
slop: 1.0,
|
slop: 1.0,
|
||||||
@ -219,13 +223,12 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fusion(bodyA: Matter.Body, bodyB: Matter.Body) {
|
private fusion(bodyA: Matter.Body, bodyB: Matter.Body) {
|
||||||
const now = Date.now();
|
if (this.latestFusionedAt > this.frame - this.COMBO_INTERVAL) {
|
||||||
if (this.latestFusionedAt > now - this.COMBO_INTERVAL) {
|
|
||||||
this.combo++;
|
this.combo++;
|
||||||
} else {
|
} else {
|
||||||
this.combo = 1;
|
this.combo = 1;
|
||||||
}
|
}
|
||||||
this.latestFusionedAt = now;
|
this.latestFusionedAt = this.frame;
|
||||||
|
|
||||||
// TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する?
|
// TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する?
|
||||||
const newX = (bodyA.position.x + bodyB.position.x) / 2;
|
const newX = (bodyA.position.x + bodyB.position.x) / 2;
|
||||||
@ -253,12 +256,14 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
const additionalScore = Math.round(currentMono.score * comboBonus);
|
const additionalScore = Math.round(currentMono.score * comboBonus);
|
||||||
this.score += additionalScore;
|
this.score += additionalScore;
|
||||||
|
|
||||||
// TODO: 効果音再生はコンポーネント側の責務なので移動する
|
// TODO: 効果音再生はコンポーネント側の責務なので移動するべき?
|
||||||
const pan = ((newX / this.gameWidth) - 0.5) * 2;
|
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', {
|
sound.playUrl('/client-assets/drop-and-fusion/bubble2.mp3', {
|
||||||
volume: this.sfxVolume,
|
volume: this.sfxVolume,
|
||||||
pan,
|
pan,
|
||||||
playbackRate: nextMono.sfxPitch,
|
playbackRate: nextMono.sfxPitch * this.replayPlaybackRate,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.emit('monoAdded', nextMono);
|
this.emit('monoAdded', nextMono);
|
||||||
@ -292,7 +297,7 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
this.tickRaf = null;
|
this.tickRaf = null;
|
||||||
this.emit('gameOver');
|
this.emit('gameOver');
|
||||||
|
|
||||||
// TODO: 効果音再生はコンポーネント側の責務なので移動する
|
// TODO: 効果音再生はコンポーネント側の責務なので移動するべき?
|
||||||
sound.playUrl('/client-assets/drop-and-fusion/gameover.mp3', {
|
sound.playUrl('/client-assets/drop-and-fusion/gameover.mp3', {
|
||||||
volume: this.sfxVolume,
|
volume: this.sfxVolume,
|
||||||
});
|
});
|
||||||
@ -303,7 +308,6 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
async function loadSingleMonoTexture(mono: Mono, game: DropAndFusionGame) {
|
async function loadSingleMonoTexture(mono: Mono, game: DropAndFusionGame) {
|
||||||
// Matter-js内にキャッシュがある場合はスキップ
|
// Matter-js内にキャッシュがある場合はスキップ
|
||||||
if (game.render.textures[mono.img]) return;
|
if (game.render.textures[mono.img]) return;
|
||||||
console.log('loading', mono.img);
|
|
||||||
|
|
||||||
let src = mono.img;
|
let src = mono.img;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
@ -376,29 +380,32 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
} else {
|
} else {
|
||||||
const energy = pairs.collision.depth;
|
const energy = pairs.collision.depth;
|
||||||
if (energy > minCollisionEnergyForSound) {
|
if (energy > minCollisionEnergyForSound) {
|
||||||
// TODO: 効果音再生はコンポーネント側の責務なので移動する
|
// TODO: 効果音再生はコンポーネント側の責務なので移動するべき?
|
||||||
const vol = ((Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4) * this.sfxVolume;
|
const vol = ((Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4) * this.sfxVolume;
|
||||||
const pan = ((((bodyA.position.x + bodyB.position.x) / 2) / this.gameWidth) - 0.5) * 2;
|
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)));
|
const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10)));
|
||||||
sound.playUrl('/client-assets/drop-and-fusion/poi1.mp3', {
|
sound.playUrl('/client-assets/drop-and-fusion/poi1.mp3', {
|
||||||
volume: vol,
|
volume: vol,
|
||||||
pan,
|
pan,
|
||||||
playbackRate: pitch,
|
playbackRate: pitch * this.replayPlaybackRate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.comboIntervalId = window.setInterval(() => {
|
|
||||||
if (this.latestFusionedAt < Date.now() - this.COMBO_INTERVAL) {
|
|
||||||
this.combo = 0;
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
if (logs) {
|
if (logs) {
|
||||||
const playTick = () => {
|
const playTick = () => {
|
||||||
|
for (let i = 0; i < this.replayPlaybackRate; i++) {
|
||||||
this.frame++;
|
this.frame++;
|
||||||
|
if (this.latestFusionedAt < this.frame - this.COMBO_INTERVAL) {
|
||||||
|
this.combo = 0;
|
||||||
|
}
|
||||||
const log = logs.find(x => x.frame === this.frame - 1);
|
const log = logs.find(x => x.frame === this.frame - 1);
|
||||||
if (log) {
|
if (log) {
|
||||||
switch (log.operation) {
|
switch (log.operation) {
|
||||||
@ -428,6 +435,7 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
Matter.Engine.update(this.engine, this.TICK_DELTA);
|
Matter.Engine.update(this.engine, this.TICK_DELTA);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isGameOver) {
|
if (!this.isGameOver) {
|
||||||
this.tickRaf = window.requestAnimationFrame(playTick);
|
this.tickRaf = window.requestAnimationFrame(playTick);
|
||||||
@ -446,6 +454,9 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
|
|
||||||
private tick() {
|
private tick() {
|
||||||
this.frame++;
|
this.frame++;
|
||||||
|
if (this.latestFusionedAt < this.frame - this.COMBO_INTERVAL) {
|
||||||
|
this.combo = 0;
|
||||||
|
}
|
||||||
this.tickCallbackQueue = this.tickCallbackQueue.filter(x => {
|
this.tickCallbackQueue = this.tickCallbackQueue.filter(x => {
|
||||||
if (x.frame === this.frame) {
|
if (x.frame === this.frame) {
|
||||||
x.callback();
|
x.callback();
|
||||||
@ -515,11 +526,14 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||||||
this.emit('dropped');
|
this.emit('dropped');
|
||||||
this.emit('monoAdded', head.mono);
|
this.emit('monoAdded', head.mono);
|
||||||
|
|
||||||
// TODO: 効果音再生はコンポーネント側の責務なので移動する
|
// TODO: 効果音再生はコンポーネント側の責務なので移動するべき?
|
||||||
const pan = ((x / this.gameWidth) - 0.5) * 2;
|
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', {
|
sound.playUrl('/client-assets/drop-and-fusion/poi2.mp3', {
|
||||||
volume: this.sfxVolume,
|
volume: this.sfxVolume,
|
||||||
pan,
|
pan,
|
||||||
|
playbackRate: this.replayPlaybackRate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +99,6 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
|
|||||||
}
|
}
|
||||||
if (options?.useCache ?? true) {
|
if (options?.useCache ?? true) {
|
||||||
if (cache.has(url)) {
|
if (cache.has(url)) {
|
||||||
if (_DEV_) console.log('use cache');
|
|
||||||
return cache.get(url) as AudioBuffer;
|
return cache.get(url) as AudioBuffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,7 +127,6 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
|
|||||||
*/
|
*/
|
||||||
export function playMisskeySfx(operationType: OperationType) {
|
export function playMisskeySfx(operationType: OperationType) {
|
||||||
const sound = defaultStore.state[`sound_${operationType}`];
|
const sound = defaultStore.state[`sound_${operationType}`];
|
||||||
if (_DEV_) console.log('play', operationType, sound);
|
|
||||||
if (sound.type == null || !canPlay) return;
|
if (sound.type == null || !canPlay) return;
|
||||||
|
|
||||||
canPlay = false;
|
canPlay = false;
|
||||||
|
@ -697,8 +697,8 @@ importers:
|
|||||||
specifier: 0.24.4
|
specifier: 0.24.4
|
||||||
version: 0.24.4
|
version: 0.24.4
|
||||||
'@syuilo/aiscript':
|
'@syuilo/aiscript':
|
||||||
specifier: 0.16.0
|
specifier: 0.17.0
|
||||||
version: 0.16.0
|
version: 0.17.0
|
||||||
'@twemoji/parser':
|
'@twemoji/parser':
|
||||||
specifier: 15.0.0
|
specifier: 15.0.0
|
||||||
version: 15.0.0
|
version: 15.0.0
|
||||||
@ -7584,8 +7584,8 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@syuilo/aiscript@0.16.0:
|
/@syuilo/aiscript@0.17.0:
|
||||||
resolution: {integrity: sha512-CXvoWOq6kmOSUQtKv0IEf7Ebfkk5PO1LxAgLqgRRPgssPvDvINCXu/gFNXKdapkFMkmX+Gj8qjemKR1vnUS4ZA==}
|
resolution: {integrity: sha512-3JtQ1rWJHMxQ3153zLCXMUOwrOgjPPYGBl0dPHhR0ohm4tn7okMQRugxMCT0t3YxByemb9FfiM6TUjd0tEGxdA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
seedrandom: 3.0.5
|
seedrandom: 3.0.5
|
||||||
stringz: 2.1.0
|
stringz: 2.1.0
|
||||||
|
Loading…
Reference in New Issue
Block a user