enhance(drop-and-fusion): リプレイの倍速再生対応

This commit is contained in:
syuilo 2024-01-10 11:38:49 +09:00
parent 138a248a6c
commit 3d9e42efca
2 changed files with 52 additions and 39 deletions

View file

@ -103,7 +103,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="replaying" style="display: flex;"> <div v-if="replaying" style="display: flex;">
<div :class="$style.frame" style="flex: 1; margin-right: 10px;"> <div :class="$style.frame" style="flex: 1; margin-right: 10px;">
<div :class="$style.frameInner"> <div :class="$style.frameInner">
<MkButton @click="endReplay"><i class="ti ti-player-stop"></i> END REPLAY</MkButton> <div class="_buttonsCenter">
<MkButton @click="endReplay"><i class="ti ti-player-stop"></i> END REPLAY</MkButton>
<MkButton :primary="replayPlaybackRate === 2" @click="replayPlaybackRate = replayPlaybackRate === 2 ? 1 : 2"><i class="ti ti-player-track-next"></i> x2</MkButton>
<MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ti ti-player-track-next"></i> x4</MkButton>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -437,10 +441,15 @@ const gameStarted = ref(false);
const highScore = ref<number | null>(null); const highScore = ref<number | null>(null);
const showConfig = ref(false); const showConfig = ref(false);
const replaying = ref(false); const replaying = ref(false);
const replayPlaybackRate = ref(1);
const mute = ref(false); const mute = ref(false);
const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume); const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume);
const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume); const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume);
watch(replayPlaybackRate, (newValue) => {
game.replayPlaybackRate = newValue;
});
function onClick(ev: MouseEvent) { function onClick(ev: MouseEvent) {
if (!containerElRect) return; if (!containerElRect) return;
if (replaying.value) return; if (replaying.value) return;
@ -493,6 +502,7 @@ function end() {
game.dispose(); game.dispose();
isGameOver.value = false; isGameOver.value = false;
replaying.value = false; replaying.value = false;
replayPlaybackRate.value = 1;
currentPick.value = null; currentPick.value = null;
dropReady.value = true; dropReady.value = true;
stock.value = []; stock.value = [];

View file

@ -44,7 +44,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 +76,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 +100,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;
@ -219,13 +221,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;
@ -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) { 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 +446,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();