From 3d9e42efca8792bcfa1be7bd6125cf732db50fdb Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 10 Jan 2024 11:38:49 +0900 Subject: enhance(drop-and-fusion): リプレイの倍速再生対応 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/scripts/drop-and-fusion-engine.ts | 79 +++++++++++----------- 1 file changed, 41 insertions(+), 38 deletions(-) (limited to 'packages/frontend/src/scripts') diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index 16fe87d97a..a59eb271ec 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -44,7 +44,7 @@ export class DropAndFusionGame extends EventEmitter<{ gameOver: () => void; }> { private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる - private COMBO_INTERVAL = 1000; + private COMBO_INTERVAL = 60; // frame public readonly DROP_INTERVAL = 500; public readonly PLAYAREA_MARGIN = 25; private STOCK_MAX = 4; @@ -76,7 +76,7 @@ export class DropAndFusionGame extends EventEmitter<{ private latestDroppedBodyId: Matter.Body['id'] | null = null; private latestDroppedAt = 0; - private latestFusionedAt = 0; + private latestFusionedAt = 0; // frame private stock: { id: string; mono: Mono }[] = []; private holding: { id: string; mono: Mono } | null = null; @@ -100,6 +100,8 @@ export class DropAndFusionGame extends EventEmitter<{ private comboIntervalId: number | null = null; + public replayPlaybackRate = 1; + constructor(opts: { canvas: HTMLCanvasElement; width: number; @@ -219,13 +221,12 @@ export class DropAndFusionGame extends EventEmitter<{ } private fusion(bodyA: Matter.Body, bodyB: Matter.Body) { - const now = Date.now(); - if (this.latestFusionedAt > now - this.COMBO_INTERVAL) { + if (this.latestFusionedAt > this.frame - this.COMBO_INTERVAL) { this.combo++; } else { this.combo = 1; } - this.latestFusionedAt = now; + this.latestFusionedAt = this.frame; // TODO: 単に位置だけでなくそれぞれの動きベクトルも融合する? const newX = (bodyA.position.x + bodyB.position.x) / 2; @@ -390,44 +391,43 @@ export class DropAndFusionGame extends EventEmitter<{ } }); - this.comboIntervalId = window.setInterval(() => { - if (this.latestFusionedAt < Date.now() - this.COMBO_INTERVAL) { - this.combo = 0; - } - }, 500); - if (logs) { const playTick = () => { - this.frame++; - 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; + for (let i = 0; i < this.replayPlaybackRate; i++) { + 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(); - return false; - } else { - return true; + 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); + Matter.Engine.update(this.engine, this.TICK_DELTA); + } if (!this.isGameOver) { this.tickRaf = window.requestAnimationFrame(playTick); @@ -446,6 +446,9 @@ export class DropAndFusionGame extends EventEmitter<{ private 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(); -- cgit v1.2.3-freya From 4bd9f664d7213e9d6d507ae1b8cb67e2b78e766a Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 10 Jan 2024 13:44:00 +0900 Subject: enhance(drop-and-fusion): some tweaks --- packages/frontend/src/pages/drop-and-fusion.vue | 1 + .../frontend/src/scripts/drop-and-fusion-engine.ts | 29 +++++++++++++++------- packages/frontend/src/scripts/sound.ts | 2 -- 3 files changed, 21 insertions(+), 11 deletions(-) (limited to 'packages/frontend/src/scripts') diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index f585519459..c5ab7a33f5 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -1028,6 +1028,7 @@ definePageMetadata({ bottom: 10px; padding: 6px 8px; color: #f00; + font-weight: bold; background: #0008; border-radius: 6px; pointer-events: none; diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index a59eb271ec..342e818905 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -157,6 +157,7 @@ export class DropAndFusionGame extends EventEmitter<{ //#region walls const WALL_OPTIONS: Matter.IChamferableBodyDefinition = { + label: '_wall_', isStatic: true, friction: 0.7, slop: 1.0, @@ -254,12 +255,14 @@ export class DropAndFusionGame extends EventEmitter<{ const additionalScore = Math.round(currentMono.score * comboBonus); this.score += additionalScore; - // TODO: 効果音再生はコンポーネント側の責務なので移動する - const pan = ((newX / this.gameWidth) - 0.5) * 2; + // 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, + playbackRate: nextMono.sfxPitch * this.replayPlaybackRate, }); this.emit('monoAdded', nextMono); @@ -293,7 +296,7 @@ export class DropAndFusionGame extends EventEmitter<{ this.tickRaf = null; this.emit('gameOver'); - // TODO: 効果音再生はコンポーネント側の責務なので移動する + // TODO: 効果音再生はコンポーネント側の責務なので移動するべき? sound.playUrl('/client-assets/drop-and-fusion/gameover.mp3', { volume: this.sfxVolume, }); @@ -377,14 +380,19 @@ export class DropAndFusionGame extends EventEmitter<{ } else { const energy = pairs.collision.depth; if (energy > minCollisionEnergyForSound) { - // TODO: 効果音再生はコンポーネント側の責務なので移動する + // TODO: 効果音再生はコンポーネント側の責務なので移動するべき? 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))); sound.playUrl('/client-assets/drop-and-fusion/poi1.mp3', { volume: vol, pan, - playbackRate: pitch, + playbackRate: pitch * this.replayPlaybackRate, }); } } @@ -518,11 +526,14 @@ export class DropAndFusionGame extends EventEmitter<{ this.emit('dropped'); this.emit('monoAdded', head.mono); - // TODO: 効果音再生はコンポーネント側の責務なので移動する - const pan = ((x / this.gameWidth) - 0.5) * 2; + // 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, }); } diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 142ddf87c9..05c8977ecf 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -99,7 +99,6 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; }) } if (options?.useCache ?? true) { if (cache.has(url)) { - if (_DEV_) console.log('use cache'); return cache.get(url) as AudioBuffer; } } @@ -128,7 +127,6 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; }) */ export function playMisskeySfx(operationType: OperationType) { const sound = defaultStore.state[`sound_${operationType}`]; - if (_DEV_) console.log('play', operationType, sound); if (sound.type == null || !canPlay) return; canPlay = false; -- cgit v1.2.3-freya From 36fd7d17cf1c71fa59eae445d05498a7bf5ab173 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 10 Jan 2024 19:54:59 +0900 Subject: enhance(drop-and-fusion): some tweaks --- .../frontend/src/pages/drop-and-fusion.game.vue | 1052 ++++++++++++++++++++ packages/frontend/src/pages/drop-and-fusion.vue | 982 +----------------- .../frontend/src/scripts/drop-and-fusion-engine.ts | 2 +- 3 files changed, 1088 insertions(+), 948 deletions(-) create mode 100644 packages/frontend/src/pages/drop-and-fusion.game.vue (limited to 'packages/frontend/src/scripts') diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue new file mode 100644 index 0000000000..acaebbadf7 --- /dev/null +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -0,0 +1,1052 @@ + + + + + + + diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index 9fb7ab2e23..7bd0eef000 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -4,10 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index 342e818905..d64c6015a5 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -33,6 +33,7 @@ type Log = { operation: 'surrender'; }; +// TODO: インスタンスを作り直さなくてもゲームをリスタートできるようにする export class DropAndFusionGame extends EventEmitter<{ changeScore: (newScore: number) => void; changeCombo: (newCombo: number) => void; @@ -307,7 +308,6 @@ export class DropAndFusionGame extends EventEmitter<{ async function loadSingleMonoTexture(mono: Mono, game: DropAndFusionGame) { // Matter-js内にキャッシュがある場合はスキップ if (game.render.textures[mono.img]) return; - console.log('loading', mono.img); let src = mono.img; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- cgit v1.2.3-freya From 762fa6a8d85691e2d5d94a46b23d7641feefd402 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 11 Jan 2024 12:34:03 +0900 Subject: enhance(drop-and-fusion): make game engine headless for server-side running --- .../frontend/src/pages/drop-and-fusion.game.vue | 438 ++++++++++++++------- .../frontend/src/scripts/drop-and-fusion-engine.ts | 359 +++++------------ 2 files changed, 387 insertions(+), 410 deletions(-) (limited to 'packages/frontend/src/scripts') diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue index acaebbadf7..3fefb49fae 100644 --- a/packages/frontend/src/pages/drop-and-fusion.game.vue +++ b/packages/frontend/src/pages/drop-and-fusion.game.vue @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
HOLD - +
- +
@@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only :moveClass="$style.transition_picked_move" mode="out-in" > - +