From 1063d39de805a83169fc9ba1f841c1239be45da8 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 9 Jan 2024 21:15:56 +0900 Subject: [PATCH] enhnace(frontend): tweak game --- locales/index.d.ts | 3 + locales/ja-JP.yml | 3 + .../assets/drop-and-fusion/gameover.mp3 | Bin 0 -> 31346 bytes packages/frontend/package.json | 1 + .../frontend/src/pages/drop-and-fusion.vue | 131 ++++++++++++-- .../src/scripts/drop-and-fusion-engine.ts | 163 +++++++++++++++--- pnpm-lock.yaml | 5 +- 7 files changed, 267 insertions(+), 39 deletions(-) create mode 100644 packages/frontend/assets/drop-and-fusion/gameover.mp3 diff --git a/locales/index.d.ts b/locales/index.d.ts index 96bc9099d..df8441247 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1195,6 +1195,9 @@ export interface Locale { "bubbleGame": string; "sfx": string; "soundWillBePlayed": string; + "showReplay": string; + "replay": string; + "replaying": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c28fde56c..997ddf9c6 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1192,6 +1192,9 @@ enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" bubbleGame: "バブルゲーム" sfx: "効果音" soundWillBePlayed: "サウンドが再生されます" +showReplay: "リプレイを見る" +replay: "リプレイ" +replaying: "リプレイ中" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/frontend/assets/drop-and-fusion/gameover.mp3 b/packages/frontend/assets/drop-and-fusion/gameover.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..23b41c56995a8adc0033d46b946f967876073d1b GIT binary patch literal 31346 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

|o%EnUC=>z1GR2{(NK@)YZP5tpIK9|o2oRV& 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 s`_whL&MocP@0V7;w|TAY -

+
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only >
{{ comboPrev }} Chain!
-
+
-
+
SCORE:
MAX CHAIN:
-
- Restart - Share -
+
+
+
{{ i18n.ts.replaying }}
+
+
+
+
+ END REPLAY +
+
+
+
+
+
+ {{ i18n.ts.done }} + {{ i18n.ts.showReplay }} + {{ i18n.ts.share }} + Copy replay data
@@ -139,7 +153,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- Restart + Retry
@@ -168,6 +182,7 @@ import { DropAndFusionGame, Mono } from '@/scripts/drop-and-fusion-engine.js'; import * as sound from '@/scripts/sound.js'; import MkRange from '@/components/MkRange.vue'; import MkSwitch from '@/components/MkSwitch.vue'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; const NORMAL_BASE_SIZE = 30; const NORAML_MONOS: Mono[] = [{ @@ -401,6 +416,8 @@ const GAME_HEIGHT = 600; let viewScale = 1; let game: DropAndFusionGame; let containerElRect: DOMRect | null = null; +let seed: string; +let logs: ReturnType | null = null; const containerEl = shallowRef(); const canvasEl = shallowRef(); @@ -414,22 +431,25 @@ const comboPrev = ref(0); const maxCombo = ref(0); const dropReady = ref(true); const gameMode = ref<'normal' | 'square'>('normal'); -const gameOver = ref(false); +const isGameOver = ref(false); const gameStarted = ref(false); const highScore = ref(null); const showConfig = ref(false); +const replaying = ref(false); const mute = ref(false); const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume); const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume); function onClick(ev: MouseEvent) { if (!containerElRect) return; + if (replaying.value) return; const x = (ev.clientX - containerElRect.left) / viewScale; game.drop(x); } function onTouchend(ev: TouchEvent) { if (!containerElRect) return; + if (replaying.value) return; const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScale; game.drop(x); } @@ -454,9 +474,18 @@ function hold() { game.hold(); } -function restart() { +async function surrender() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.areYouSure, + }); + if (canceled) return; + game.surrender(); +} + +function end() { game.dispose(); - gameOver.value = false; + isGameOver.value = false; currentPick.value = null; dropReady.value = true; stock.value = []; @@ -467,6 +496,45 @@ function restart() { gameStarted.value = false; } +function replay() { + replaying.value = true; + game.dispose(); + game = new DropAndFusionGame({ + width: GAME_WIDTH, + height: GAME_HEIGHT, + canvas: canvasEl.value!, + seed: seed, + sfxVolume: mute.value ? 0 : sfxVolume.value, + ...( + gameMode.value === 'normal' ? { + monoDefinitions: NORAML_MONOS, + } : { + monoDefinitions: SQUARE_MONOS, + } + ), + }); + attachGameEvents(); + os.promiseDialog(game.load(), async () => { + game.start(logs!); + }); +} + +function endReplay() { + replaying.value = false; + game.dispose(); +} + +function exportLog() { + if (!logs) return; + const data = JSON.stringify({ + seed: seed, + date: new Date().toISOString(), + logs: logs, + }); + copyToClipboard(data); + os.success(); +} + function attachGameEvents() { game.addListener('changeScore', value => { score.value = value; @@ -492,9 +560,11 @@ function attachGameEvents() { }); game.addListener('dropped', () => { + if (replaying.value) return; + dropReady.value = false; window.setTimeout(() => { - if (!gameOver.value) { + if (!isGameOver.value) { dropReady.value = true; } }, game.DROP_INTERVAL); @@ -511,6 +581,8 @@ function attachGameEvents() { }); game.addListener('monoAdded', (mono) => { + if (replaying.value) return; + // 実績関連 if (mono.level === 10) { claimAchievement('bubbleGameExplodingHead'); @@ -523,9 +595,15 @@ function attachGameEvents() { }); game.addListener('gameOver', () => { + if (replaying.value) { + endReplay(); + return; + } + + logs = game.getLogs(); currentPick.value = null; dropReady.value = false; - gameOver.value = true; + isGameOver.value = true; if (score.value > (highScore.value ?? 0)) { highScore.value = score.value; @@ -551,10 +629,13 @@ async function start() { highScore.value = null; } + seed = Date.now().toString(); + game = new DropAndFusionGame({ width: GAME_WIDTH, height: GAME_HEIGHT, canvas: canvasEl.value!, + seed: seed, sfxVolume: mute.value ? 0 : sfxVolume.value, ...( gameMode.value === 'normal' ? { @@ -690,7 +771,7 @@ useInterval(() => { }, 1000, { immediate: false, afterMounted: true }); onDeactivated(() => { - restart(); + end(); }); definePageMetadata({ @@ -922,6 +1003,28 @@ definePageMetadata({ } } +.replayIndicator { + position: absolute; + z-index: 10; + left: 10px; + bottom: 10px; + padding: 6px 8px; + color: #f00; + background: #0008; + border-radius: 6px; + pointer-events: none; +} + +.replayIndicatorText { + animation: replayIndicator-blink 2s infinite; +} + +@keyframes replayIndicator-blink { + 0% { opacity: 1; } + 50% { opacity: 0; } + 100% { opacity: 1; } +} + @keyframes currentMonoArrow { 0% { transform: translateY(0); } 25% { transform: translateY(-8px); } diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index f71f3a668..9db93d153 100644 --- a/packages/frontend/src/scripts/drop-and-fusion-engine.ts +++ b/packages/frontend/src/scripts/drop-and-fusion-engine.ts @@ -5,6 +5,7 @@ import { EventEmitter } from 'eventemitter3'; import * as Matter from 'matter-js'; +import seedrandom from 'seedrandom'; import * as sound from '@/scripts/sound.js'; export type Mono = { @@ -20,6 +21,18 @@ export type Mono = { spriteScale: number; }; +type Log = { + frame: number; + operation: 'drop'; + x: number; +} | { + frame: number; + operation: 'hold'; +} | { + frame: number; + operation: 'surrender'; +}; + export class DropAndFusionGame extends EventEmitter<{ changeScore: (newScore: number) => void; changeCombo: (newCombo: number) => void; @@ -35,18 +48,23 @@ export class DropAndFusionGame extends EventEmitter<{ public readonly DROP_INTERVAL = 500; public readonly PLAYAREA_MARGIN = 25; private STOCK_MAX = 4; + private TICK_DELTA = 1000 / 60; // 60fps private loaded = false; + private frame = 0; private engine: Matter.Engine; private render: Matter.Render; - private runner: Matter.Runner; + private tickRaf: ReturnType | null = null; + private tickCallbackQueue: { frame: number; callback: () => void; }[] = []; private overflowCollider: Matter.Body; private isGameOver = false; - private gameWidth: number; private gameHeight: number; private monoDefinitions: Mono[] = []; private monoTextures: Record = {}; private monoTextureUrls: Record = {}; + private rng: () => number; + private logs: Log[] = []; + private replaying = false; private sfxVolume = 1; @@ -87,13 +105,17 @@ export class DropAndFusionGame extends EventEmitter<{ width: number; height: number; monoDefinitions: Mono[]; + seed: string; sfxVolume?: number; }) { super(); + this.tick = this.tick.bind(this); + this.gameWidth = opts.width; this.gameHeight = opts.height; this.monoDefinitions = opts.monoDefinitions; + this.rng = seedrandom(opts.seed); if (opts.sfxVolume) { this.sfxVolume = opts.sfxVolume; @@ -129,9 +151,6 @@ export class DropAndFusionGame extends EventEmitter<{ Matter.Render.run(this.render); - this.runner = Matter.Runner.create(); - Matter.Runner.run(this.runner, this.engine); - this.engine.world.bodies = []; //#region walls @@ -223,9 +242,12 @@ export class DropAndFusionGame extends EventEmitter<{ Matter.Composite.add(this.engine.world, body); // 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする - window.setTimeout(() => { - this.activeBodyIds.push(body.id); - }, 100); + this.tickCallbackQueue.push({ + frame: this.frame + 6, + callback: () => { + this.activeBodyIds.push(body.id); + }, + }); const comboBonus = 1 + ((this.combo - 1) / 5); const additionalScore = Math.round(currentMono.score * comboBonus); @@ -244,7 +266,7 @@ export class DropAndFusionGame extends EventEmitter<{ } else { //const VELOCITY = 30; //for (let i = 0; i < 10; i++) { - // const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(Math.random() * 3)))!, x + ((Math.random() * VELOCITY) - (VELOCITY / 2)), y + ((Math.random() * VELOCITY) - (VELOCITY / 2))); + // const body = createBody(FRUITS.find(x => x.level === (1 + Math.floor(this.rng() * 3)))!, x + ((this.rng() * VELOCITY) - (VELOCITY / 2)), y + ((this.rng() * VELOCITY) - (VELOCITY / 2))); // Matter.Composite.add(world, body); // bodies.push(body); //} @@ -255,10 +277,25 @@ export class DropAndFusionGame extends EventEmitter<{ } } + public surrender() { + this.logs.push({ + frame: this.frame, + operation: 'surrender', + }); + + this.gameOver(); + } + private gameOver() { this.isGameOver = true; - Matter.Runner.stop(this.runner); + if (this.tickRaf) window.cancelAnimationFrame(this.tickRaf); + this.tickRaf = null; this.emit('gameOver'); + + // TODO: 効果音再生はコンポーネント側の責務なので移動する + sound.playUrl('/client-assets/drop-and-fusion/gameover.mp3', { + volume: this.sfxVolume, + }); } /** テクスチャをすべてキャッシュする */ @@ -292,13 +329,14 @@ export class DropAndFusionGame extends EventEmitter<{ return Promise.all(this.monoDefinitions.map(x => loadSingleMonoTexture(x, this))); } - public start() { + public start(logs?: Log[]) { if (!this.loaded) throw new Error('game is not loaded yet'); + if (logs) this.replaying = true; for (let i = 0; i < this.STOCK_MAX; i++) { this.stock.push({ - id: Math.random().toString(), - mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); } this.emit('changeStock', this.stock); @@ -327,10 +365,13 @@ export class DropAndFusionGame extends EventEmitter<{ this.fusion(bodyA, bodyB); } else { fusionReservedPairs.push({ bodyA, bodyB }); - window.setTimeout(() => { - fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id); - this.fusion(bodyA, bodyB); - }, 100); + this.tickCallbackQueue.push({ + frame: this.frame + 6, + callback: () => { + fusionReservedPairs = fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id); + this.fusion(bodyA, bodyB); + }, + }); } } else { const energy = pairs.collision.depth; @@ -354,6 +395,69 @@ export class DropAndFusionGame extends EventEmitter<{ 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; + } + } + 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); + + if (!this.isGameOver) { + this.tickRaf = window.requestAnimationFrame(playTick); + } + }; + + playTick(); + } else { + this.tick(); + } + } + + public getLogs() { + return this.logs; + } + + private tick() { + this.frame++; + 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); + if (!this.isGameOver) { + this.tickRaf = window.requestAnimationFrame(this.tick); + } } public async load() { @@ -387,17 +491,22 @@ export class DropAndFusionGame extends EventEmitter<{ public drop(_x: number) { if (this.isGameOver) return; - if (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL) return; + if (!this.replaying && (Date.now() - this.latestDroppedAt < this.DROP_INTERVAL)) return; const head = this.stock.shift()!; this.stock.push({ - id: Math.random().toString(), - mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); this.emit('changeStock', this.stock); - const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), _x)); + const x = Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), Math.round(_x))); const body = this.createBody(head.mono, x, 50 + head.mono.size / 2); + this.logs.push({ + frame: this.frame, + operation: 'drop', + x, + }); Matter.Composite.add(this.engine.world, body); this.activeBodyIds.push(body.id); this.latestDroppedBodyId = body.id; @@ -416,6 +525,11 @@ export class DropAndFusionGame extends EventEmitter<{ public hold() { if (this.isGameOver) return; + this.logs.push({ + frame: this.frame, + operation: 'hold', + }); + if (this.holding) { const head = this.stock.shift()!; this.stock.unshift(this.holding); @@ -426,8 +540,8 @@ export class DropAndFusionGame extends EventEmitter<{ const head = this.stock.shift()!; this.holding = head; this.stock.push({ - id: Math.random().toString(), - mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], + id: this.rng().toString(), + mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], }); this.emit('changeHolding', this.holding); this.emit('changeStock', this.stock); @@ -440,8 +554,9 @@ export class DropAndFusionGame extends EventEmitter<{ public dispose() { if (this.comboIntervalId) window.clearInterval(this.comboIntervalId); + if (this.tickRaf) window.cancelAnimationFrame(this.tickRaf); + this.tickRaf = null; Matter.Render.stop(this.render); - Matter.Runner.stop(this.runner); Matter.World.clear(this.engine.world, false); Matter.Engine.clear(this.engine); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0f74de84..9d9822482 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -787,6 +787,9 @@ importers: sass: specifier: 1.69.5 version: 1.69.5 + seedrandom: + specifier: ^3.0.5 + version: 3.0.5 shiki: specifier: 0.14.7 version: 0.14.7 @@ -7401,7 +7404,7 @@ packages: hasBin: true peerDependencies: '@swc/core': ^1.2.66 - chokidar: ^3.5.1 + chokidar: 3.5.3 peerDependenciesMeta: chokidar: optional: true