merge: upstream

This commit is contained in:
Marie 2024-01-11 00:57:57 +01:00
commit 4df3145993
No known key found for this signature in database
GPG key ID: 56569BBE47D2C828
11 changed files with 1209 additions and 988 deletions

View file

@ -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
View file

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

View file

@ -1240,6 +1240,13 @@ showReplay: "リプレイを見る"
replay: "リプレイ" replay: "リプレイ"
replaying: "リプレイ中" replaying: "リプレイ中"
_bubbleGame:
howToPlay: "遊び方"
_howToPlay:
section1: "位置を調整してハコにモノを落とします。"
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"
forExistingUsersDescription: "有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。" forExistingUsersDescription: "有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"

View file

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

View file

@ -262,15 +262,24 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30):
} }
const matched = new Map<string, EmojiScore>(); const matched = new Map<string, EmojiScore>();
//
//
emojiDb.some(x => { emojiDb.some(x => {
if (x.name.toLowerCase().startsWith(query) && !x.aliasOf) { if (x.name.toLowerCase() === query && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.name, { emoji: x, score: query.length + 1 }); matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
} }
return matched.size === max; return matched.size === max;
}); });
//
if (matched.size < max) {
emojiDb.some(x => {
if (x.name.startsWith(query) && !x.aliasOf) {
matched.set(x.name, { emoji: x, score: query.length + 1 });
}
return matched.size === max;
});
}
// //
if (matched.size < max) { if (matched.size < max) {
emojiDb.some(x => { emojiDb.some(x => {

View file

@ -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);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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,58 +380,62 @@ 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 = () => {
this.frame++; for (let i = 0; i < this.replayPlaybackRate; i++) {
const log = logs.find(x => x.frame === this.frame - 1); this.frame++;
if (log) { if (this.latestFusionedAt < this.frame - this.COMBO_INTERVAL) {
switch (log.operation) { this.combo = 0;
case 'drop': {
this.drop(log.x);
break;
}
case 'hold': {
this.hold();
break;
}
case 'surrender': {
this.surrender();
break;
}
default:
break;
} }
} const log = logs.find(x => x.frame === this.frame - 1);
this.tickCallbackQueue = this.tickCallbackQueue.filter(x => { if (log) {
if (x.frame === this.frame) { switch (log.operation) {
x.callback(); case 'drop': {
return false; this.drop(log.x);
} else { break;
return true; }
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); 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,
}); });
} }

View file

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

View file

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