diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2024-01-09 21:15:56 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2024-01-09 21:15:56 +0900 |
| commit | 1063d39de805a83169fc9ba1f841c1239be45da8 (patch) | |
| tree | 918b34ecce0df4b0956a0e06271910e42b68f2e8 /packages/frontend/src/scripts | |
| parent | update sound (diff) | |
| download | sharkey-1063d39de805a83169fc9ba1f841c1239be45da8.tar.gz sharkey-1063d39de805a83169fc9ba1f841c1239be45da8.tar.bz2 sharkey-1063d39de805a83169fc9ba1f841c1239be45da8.zip | |
enhnace(frontend): tweak game
Diffstat (limited to 'packages/frontend/src/scripts')
| -rw-r--r-- | packages/frontend/src/scripts/drop-and-fusion-engine.ts | 163 |
1 files changed, 139 insertions, 24 deletions
diff --git a/packages/frontend/src/scripts/drop-and-fusion-engine.ts b/packages/frontend/src/scripts/drop-and-fusion-engine.ts index f71f3a668e..9db93d1534 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<typeof requestAnimationFrame> | 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<string, Blob> = {}; private monoTextureUrls: Record<string, string> = {}; + 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); } |