Compare commits
	
		
			13 Commits
		
	
	
		
			chat
			...
			bubble-gam
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 7627da62ee | ||
|   | 31a25f9332 | ||
|   | f85b9107e2 | ||
|   | 9181e3db7d | ||
|   | 028800c0bb | ||
|   | e33c8bf43a | ||
|   | 4918923635 | ||
|   | eda727c487 | ||
|   | 333fac00c8 | ||
|   | aa7dd98119 | ||
|   | c0d28358d6 | ||
|   | 17f368e228 | ||
|   | 14d4ffaa36 | 
							
								
								
									
										3
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1195,6 +1195,9 @@ export interface Locale { | ||||
|     "bubbleGame": string; | ||||
|     "sfx": string; | ||||
|     "soundWillBePlayed": string; | ||||
|     "showReplay": string; | ||||
|     "replay": string; | ||||
|     "replaying": string; | ||||
|     "_announcement": { | ||||
|         "forExistingUsers": string; | ||||
|         "forExistingUsersDescription": string; | ||||
|   | ||||
| @@ -1192,6 +1192,9 @@ enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" | ||||
| bubbleGame: "バブルゲーム" | ||||
| sfx: "効果音" | ||||
| soundWillBePlayed: "サウンドが再生されます" | ||||
| showReplay: "リプレイを見る" | ||||
| replay: "リプレイ" | ||||
| replaying: "リプレイ中" | ||||
|  | ||||
| _announcement: | ||||
|   forExistingUsers: "既存ユーザーのみ" | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/gameover.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/gameover.mp3
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -58,6 +58,7 @@ | ||||
| 		"rollup": "4.9.1", | ||||
| 		"sanitize-html": "2.11.0", | ||||
| 		"sass": "1.69.5", | ||||
| 		"seedrandom": "^3.0.5", | ||||
| 		"shiki": "0.14.7", | ||||
| 		"strict-event-emitter-types": "2.0.0", | ||||
| 		"textarea-caret": "3.1.0", | ||||
|   | ||||
| @@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: gameOver }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove"> | ||||
| 			<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove"> | ||||
| 				<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/> | ||||
| 				<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/> | ||||
| 				<canvas ref="canvasEl" :class="$style.canvas"/> | ||||
| @@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 				> | ||||
| 					<div v-show="combo > 1" :class="$style.combo" :style="{ fontSize: `${100 + ((comboPrev - 2) * 15)}%` }">{{ comboPrev }} Chain!</div> | ||||
| 				</Transition> | ||||
| 				<div :class="$style.dropperContainer" :style="{ left: dropperX + 'px' }"> | ||||
| 				<div v-if="!isGameOver && !replaying" :class="$style.dropperContainer" :style="{ left: dropperX + 'px' }"> | ||||
| 					<!--<img v-if="currentPick" src="/client-assets/drop-and-fusion/dropper.png" :class="$style.dropper" :style="{ left: dropperX + 'px' }"/>--> | ||||
| 					<Transition | ||||
| 						:enterActiveClass="$style.transition_picked_enterActive" | ||||
| @@ -91,16 +91,30 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 						<div :class="$style.dropGuide"/> | ||||
| 					</template> | ||||
| 				</div> | ||||
| 				<div v-if="gameOver" :class="$style.gameOverLabel"> | ||||
| 				<div v-if="isGameOver && !replaying" :class="$style.gameOverLabel"> | ||||
| 					<div class="_gaps_s"> | ||||
| 						<img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/> | ||||
| 						<div>SCORE: <MkNumber :value="score"/></div> | ||||
| 						<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div> | ||||
| 						<div class="_buttonsCenter"> | ||||
| 							<MkButton primary rounded @click="restart">Restart</MkButton> | ||||
| 							<MkButton primary rounded @click="share">Share</MkButton> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ti ti-player-play"></i> {{ i18n.ts.replaying }}</span></div> | ||||
| 			</div> | ||||
| 			<div v-if="replaying" style="display: flex;"> | ||||
| 				<div :class="$style.frame" style="flex: 1; margin-right: 10px;"> | ||||
| 					<div :class="$style.frameInner"> | ||||
| 						<MkButton @click="endReplay"><i class="ti ti-player-stop"></i> END REPLAY</MkButton> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div v-if="isGameOver" :class="$style.frame"> | ||||
| 				<div :class="$style.frameInner"> | ||||
| 					<div class="_buttonsCenter"> | ||||
| 						<MkButton primary rounded @click="end">{{ i18n.ts.done }}</MkButton> | ||||
| 						<MkButton primary rounded @click="replay">{{ i18n.ts.showReplay }}</MkButton> | ||||
| 						<MkButton primary rounded @click="share">{{ i18n.ts.share }}</MkButton> | ||||
| 						<MkButton rounded @click="exportLog">Copy replay data</MkButton> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div style="display: flex;"> | ||||
| @@ -139,7 +153,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 			</div> | ||||
| 			<div :class="$style.frame"> | ||||
| 				<div :class="$style.frameInner"> | ||||
| 					<MkButton @click="restart">Restart</MkButton> | ||||
| 					<MkButton danger @click="surrender">Retry</MkButton> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| @@ -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<DropAndFusionGame['getLogs']> | null = null; | ||||
|  | ||||
| const containerEl = shallowRef<HTMLElement>(); | ||||
| const canvasEl = shallowRef<HTMLCanvasElement>(); | ||||
| @@ -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<number | null>(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); } | ||||
|   | ||||
| @@ -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.tickCallbackQueue.push({ | ||||
| 				frame: this.frame + 6, | ||||
| 				callback: () => { | ||||
| 					this.activeBodyIds.push(body.id); | ||||
| 			}, 100); | ||||
| 				}, | ||||
| 			}); | ||||
|  | ||||
| 			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(() => { | ||||
| 						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); | ||||
| 						}, 100); | ||||
| 							}, | ||||
| 						}); | ||||
| 					} | ||||
| 				} 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); | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										5
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user