wip
This commit is contained in:
		| @@ -58,6 +58,7 @@ | |||||||
| 		"rollup": "4.9.1", | 		"rollup": "4.9.1", | ||||||
| 		"sanitize-html": "2.11.0", | 		"sanitize-html": "2.11.0", | ||||||
| 		"sass": "1.69.5", | 		"sass": "1.69.5", | ||||||
|  | 		"seedrandom": "^3.0.5", | ||||||
| 		"shiki": "0.14.7", | 		"shiki": "0.14.7", | ||||||
| 		"strict-event-emitter-types": "2.0.0", | 		"strict-event-emitter-types": "2.0.0", | ||||||
| 		"textarea-caret": "3.1.0", | 		"textarea-caret": "3.1.0", | ||||||
|   | |||||||
| @@ -98,6 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only | |||||||
| 						<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div> | 						<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div> | ||||||
| 						<div class="_buttonsCenter"> | 						<div class="_buttonsCenter"> | ||||||
| 							<MkButton primary rounded @click="restart">Restart</MkButton> | 							<MkButton primary rounded @click="restart">Restart</MkButton> | ||||||
|  | 							<MkButton primary rounded @click="replay">Replay</MkButton> | ||||||
| 							<MkButton primary rounded @click="share">Share</MkButton> | 							<MkButton primary rounded @click="share">Share</MkButton> | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| @@ -401,6 +402,7 @@ const GAME_HEIGHT = 600; | |||||||
| let viewScale = 1; | let viewScale = 1; | ||||||
| let game: DropAndFusionGame; | let game: DropAndFusionGame; | ||||||
| let containerElRect: DOMRect | null = null; | let containerElRect: DOMRect | null = null; | ||||||
|  | let seed: string; | ||||||
|  |  | ||||||
| const containerEl = shallowRef<HTMLElement>(); | const containerEl = shallowRef<HTMLElement>(); | ||||||
| const canvasEl = shallowRef<HTMLCanvasElement>(); | const canvasEl = shallowRef<HTMLCanvasElement>(); | ||||||
| @@ -418,18 +420,21 @@ const gameOver = ref(false); | |||||||
| const gameStarted = ref(false); | 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 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); | ||||||
|  |  | ||||||
| function onClick(ev: MouseEvent) { | function onClick(ev: MouseEvent) { | ||||||
| 	if (!containerElRect) return; | 	if (!containerElRect) return; | ||||||
|  | 	if (replaying.value) return; | ||||||
| 	const x = (ev.clientX - containerElRect.left) / viewScale; | 	const x = (ev.clientX - containerElRect.left) / viewScale; | ||||||
| 	game.drop(x); | 	game.drop(x); | ||||||
| } | } | ||||||
|  |  | ||||||
| function onTouchend(ev: TouchEvent) { | function onTouchend(ev: TouchEvent) { | ||||||
| 	if (!containerElRect) return; | 	if (!containerElRect) return; | ||||||
|  | 	if (replaying.value) return; | ||||||
| 	const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScale; | 	const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScale; | ||||||
| 	game.drop(x); | 	game.drop(x); | ||||||
| } | } | ||||||
| @@ -467,6 +472,29 @@ function restart() { | |||||||
| 	gameStarted.value = false; | 	gameStarted.value = false; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function replay() { | ||||||
|  | 	replaying.value = true; | ||||||
|  | 	const logs = game.getLogs(); | ||||||
|  | 	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, | ||||||
|  | 			} | ||||||
|  | 		), | ||||||
|  | 	}); | ||||||
|  | 	os.promiseDialog(game.load(), async () => { | ||||||
|  | 		game.start(logs); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  |  | ||||||
| function attachGameEvents() { | function attachGameEvents() { | ||||||
| 	game.addListener('changeScore', value => { | 	game.addListener('changeScore', value => { | ||||||
| 		score.value = value; | 		score.value = value; | ||||||
| @@ -551,10 +579,13 @@ async function start() { | |||||||
| 		highScore.value = null; | 		highScore.value = null; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	seed = Date.now().toString(); | ||||||
|  |  | ||||||
| 	game = new DropAndFusionGame({ | 	game = new DropAndFusionGame({ | ||||||
| 		width: GAME_WIDTH, | 		width: GAME_WIDTH, | ||||||
| 		height: GAME_HEIGHT, | 		height: GAME_HEIGHT, | ||||||
| 		canvas: canvasEl.value!, | 		canvas: canvasEl.value!, | ||||||
|  | 		seed: seed, | ||||||
| 		sfxVolume: mute.value ? 0 : sfxVolume.value, | 		sfxVolume: mute.value ? 0 : sfxVolume.value, | ||||||
| 		...( | 		...( | ||||||
| 			gameMode.value === 'normal' ? { | 			gameMode.value === 'normal' ? { | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
|  |  | ||||||
| import { EventEmitter } from 'eventemitter3'; | import { EventEmitter } from 'eventemitter3'; | ||||||
| import * as Matter from 'matter-js'; | import * as Matter from 'matter-js'; | ||||||
|  | import seedrandom from 'seedrandom'; | ||||||
| import * as sound from '@/scripts/sound.js'; | import * as sound from '@/scripts/sound.js'; | ||||||
|  |  | ||||||
| export type Mono = { | export type Mono = { | ||||||
| @@ -20,6 +21,15 @@ export type Mono = { | |||||||
| 	spriteScale: number; | 	spriteScale: number; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | type Log = { | ||||||
|  | 	frame: number; | ||||||
|  | 	operation: 'drop'; | ||||||
|  | 	x: number; | ||||||
|  | } | { | ||||||
|  | 	frame: number; | ||||||
|  | 	operation: 'hold'; | ||||||
|  | }; | ||||||
|  |  | ||||||
| export class DropAndFusionGame extends EventEmitter<{ | export class DropAndFusionGame extends EventEmitter<{ | ||||||
| 	changeScore: (newScore: number) => void; | 	changeScore: (newScore: number) => void; | ||||||
| 	changeCombo: (newCombo: number) => void; | 	changeCombo: (newCombo: number) => void; | ||||||
| @@ -35,18 +45,21 @@ export class DropAndFusionGame extends EventEmitter<{ | |||||||
| 	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; | ||||||
|  | 	private METTER_ENGINE_UPDATE_DELTA = 1000 / 60; // 60fps | ||||||
| 	private loaded = false; | 	private loaded = false; | ||||||
|  | 	private frame = 0; | ||||||
| 	private engine: Matter.Engine; | 	private engine: Matter.Engine; | ||||||
| 	private render: Matter.Render; | 	private render: Matter.Render; | ||||||
| 	private runner: Matter.Runner; | 	private matterEngineUpdateRaf: ReturnType<typeof requestAnimationFrame> | null = null; | ||||||
| 	private overflowCollider: Matter.Body; | 	private overflowCollider: Matter.Body; | ||||||
| 	private isGameOver = false; | 	private isGameOver = false; | ||||||
|  |  | ||||||
| 	private gameWidth: number; | 	private gameWidth: number; | ||||||
| 	private gameHeight: number; | 	private gameHeight: number; | ||||||
| 	private monoDefinitions: Mono[] = []; | 	private monoDefinitions: Mono[] = []; | ||||||
| 	private monoTextures: Record<string, Blob> = {}; | 	private monoTextures: Record<string, Blob> = {}; | ||||||
| 	private monoTextureUrls: Record<string, string> = {}; | 	private monoTextureUrls: Record<string, string> = {}; | ||||||
|  | 	private rng: () => number; | ||||||
|  | 	private logs: Log[] = []; | ||||||
|  |  | ||||||
| 	private sfxVolume = 1; | 	private sfxVolume = 1; | ||||||
|  |  | ||||||
| @@ -87,13 +100,17 @@ export class DropAndFusionGame extends EventEmitter<{ | |||||||
| 		width: number; | 		width: number; | ||||||
| 		height: number; | 		height: number; | ||||||
| 		monoDefinitions: Mono[]; | 		monoDefinitions: Mono[]; | ||||||
|  | 		seed: string; | ||||||
| 		sfxVolume?: number; | 		sfxVolume?: number; | ||||||
| 	}) { | 	}) { | ||||||
| 		super(); | 		super(); | ||||||
|  |  | ||||||
|  | 		this.tick = this.tick.bind(this); | ||||||
|  |  | ||||||
| 		this.gameWidth = opts.width; | 		this.gameWidth = opts.width; | ||||||
| 		this.gameHeight = opts.height; | 		this.gameHeight = opts.height; | ||||||
| 		this.monoDefinitions = opts.monoDefinitions; | 		this.monoDefinitions = opts.monoDefinitions; | ||||||
|  | 		this.rng = seedrandom(opts.seed); | ||||||
|  |  | ||||||
| 		if (opts.sfxVolume) { | 		if (opts.sfxVolume) { | ||||||
| 			this.sfxVolume = opts.sfxVolume; | 			this.sfxVolume = opts.sfxVolume; | ||||||
| @@ -129,9 +146,6 @@ export class DropAndFusionGame extends EventEmitter<{ | |||||||
|  |  | ||||||
| 		Matter.Render.run(this.render); | 		Matter.Render.run(this.render); | ||||||
|  |  | ||||||
| 		this.runner = Matter.Runner.create(); |  | ||||||
| 		Matter.Runner.run(this.runner, this.engine); |  | ||||||
|  |  | ||||||
| 		this.engine.world.bodies = []; | 		this.engine.world.bodies = []; | ||||||
|  |  | ||||||
| 		//#region walls | 		//#region walls | ||||||
| @@ -244,7 +258,7 @@ export class DropAndFusionGame extends EventEmitter<{ | |||||||
| 		} else { | 		} else { | ||||||
| 			//const VELOCITY = 30; | 			//const VELOCITY = 30; | ||||||
| 			//for (let i = 0; i < 10; i++) { | 			//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); | 			//	Matter.Composite.add(world, body); | ||||||
| 			//	bodies.push(body); | 			//	bodies.push(body); | ||||||
| 			//} | 			//} | ||||||
| @@ -257,7 +271,8 @@ export class DropAndFusionGame extends EventEmitter<{ | |||||||
|  |  | ||||||
| 	private gameOver() { | 	private gameOver() { | ||||||
| 		this.isGameOver = true; | 		this.isGameOver = true; | ||||||
| 		Matter.Runner.stop(this.runner); | 		if (this.matterEngineUpdateRaf) window.cancelAnimationFrame(this.matterEngineUpdateRaf); | ||||||
|  | 		this.matterEngineUpdateRaf = null; | ||||||
| 		this.emit('gameOver'); | 		this.emit('gameOver'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -292,13 +307,13 @@ export class DropAndFusionGame extends EventEmitter<{ | |||||||
| 		return Promise.all(this.monoDefinitions.map(x => loadSingleMonoTexture(x, this))); | 		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 (!this.loaded) throw new Error('game is not loaded yet'); | ||||||
|  |  | ||||||
| 		for (let i = 0; i < this.STOCK_MAX; i++) { | 		for (let i = 0; i < this.STOCK_MAX; i++) { | ||||||
| 			this.stock.push({ | 			this.stock.push({ | ||||||
| 				id: Math.random().toString(), | 				id: this.rng().toString(), | ||||||
| 				mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], | 				mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 		this.emit('changeStock', this.stock); | 		this.emit('changeStock', this.stock); | ||||||
| @@ -354,6 +369,47 @@ export class DropAndFusionGame extends EventEmitter<{ | |||||||
| 				this.combo = 0; | 				this.combo = 0; | ||||||
| 			} | 			} | ||||||
| 		}, 500); | 		}, 500); | ||||||
|  |  | ||||||
|  | 		if (logs) { | ||||||
|  | 			let playingFrame = 0; | ||||||
|  |  | ||||||
|  | 			const playTick = () => { | ||||||
|  | 				playingFrame++; | ||||||
|  | 				const log = logs.find(x => x.frame === playingFrame - 1); | ||||||
|  | 				if (log) { | ||||||
|  | 					switch (log.operation) { | ||||||
|  | 						case 'drop': { | ||||||
|  | 							this.drop(log.x); | ||||||
|  | 							break; | ||||||
|  | 						} | ||||||
|  | 						case 'hold': { | ||||||
|  | 							this.hold(); | ||||||
|  | 							break; | ||||||
|  | 						} | ||||||
|  | 						default: | ||||||
|  | 							break; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				Matter.Engine.update(this.engine, this.METTER_ENGINE_UPDATE_DELTA); | ||||||
|  |  | ||||||
|  | 				window.requestAnimationFrame(playTick); | ||||||
|  | 			}; | ||||||
|  |  | ||||||
|  | 			playTick(); | ||||||
|  | 		} else { | ||||||
|  | 			this.tick(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	public getLogs() { | ||||||
|  | 		return this.logs; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	private tick() { | ||||||
|  | 		this.frame++; | ||||||
|  | 		Matter.Engine.update(this.engine, this.METTER_ENGINE_UPDATE_DELTA); | ||||||
|  | 		this.matterEngineUpdateRaf = window.requestAnimationFrame(this.tick); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public async load() { | 	public async load() { | ||||||
| @@ -391,13 +447,18 @@ export class DropAndFusionGame extends EventEmitter<{ | |||||||
|  |  | ||||||
| 		const head = this.stock.shift()!; | 		const head = this.stock.shift()!; | ||||||
| 		this.stock.push({ | 		this.stock.push({ | ||||||
| 			id: Math.random().toString(), | 			id: this.rng().toString(), | ||||||
| 			mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], | 			mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(this.rng() * this.monoDefinitions.filter(x => x.dropCandidate).length)], | ||||||
| 		}); | 		}); | ||||||
| 		this.emit('changeStock', this.stock); | 		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.round(Math.min(this.gameWidth - this.PLAYAREA_MARGIN - (head.mono.size / 2), Math.max(this.PLAYAREA_MARGIN + (head.mono.size / 2), _x))); | ||||||
| 		const body = this.createBody(head.mono, x, 50 + head.mono.size / 2); | 		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); | 		Matter.Composite.add(this.engine.world, body); | ||||||
| 		this.activeBodyIds.push(body.id); | 		this.activeBodyIds.push(body.id); | ||||||
| 		this.latestDroppedBodyId = body.id; | 		this.latestDroppedBodyId = body.id; | ||||||
| @@ -416,6 +477,11 @@ export class DropAndFusionGame extends EventEmitter<{ | |||||||
| 	public hold() { | 	public hold() { | ||||||
| 		if (this.isGameOver) return; | 		if (this.isGameOver) return; | ||||||
|  |  | ||||||
|  | 		this.logs.push({ | ||||||
|  | 			frame: this.frame, | ||||||
|  | 			operation: 'hold', | ||||||
|  | 		}); | ||||||
|  |  | ||||||
| 		if (this.holding) { | 		if (this.holding) { | ||||||
| 			const head = this.stock.shift()!; | 			const head = this.stock.shift()!; | ||||||
| 			this.stock.unshift(this.holding); | 			this.stock.unshift(this.holding); | ||||||
| @@ -426,8 +492,8 @@ export class DropAndFusionGame extends EventEmitter<{ | |||||||
| 			const head = this.stock.shift()!; | 			const head = this.stock.shift()!; | ||||||
| 			this.holding = head; | 			this.holding = head; | ||||||
| 			this.stock.push({ | 			this.stock.push({ | ||||||
| 				id: Math.random().toString(), | 				id: this.rng().toString(), | ||||||
| 				mono: this.monoDefinitions.filter(x => x.dropCandidate)[Math.floor(Math.random() * this.monoDefinitions.filter(x => x.dropCandidate).length)], | 				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('changeHolding', this.holding); | ||||||
| 			this.emit('changeStock', this.stock); | 			this.emit('changeStock', this.stock); | ||||||
| @@ -440,8 +506,9 @@ export class DropAndFusionGame extends EventEmitter<{ | |||||||
|  |  | ||||||
| 	public dispose() { | 	public dispose() { | ||||||
| 		if (this.comboIntervalId) window.clearInterval(this.comboIntervalId); | 		if (this.comboIntervalId) window.clearInterval(this.comboIntervalId); | ||||||
|  | 		if (this.matterEngineUpdateRaf) window.cancelAnimationFrame(this.matterEngineUpdateRaf); | ||||||
|  | 		this.matterEngineUpdateRaf = null; | ||||||
| 		Matter.Render.stop(this.render); | 		Matter.Render.stop(this.render); | ||||||
| 		Matter.Runner.stop(this.runner); |  | ||||||
| 		Matter.World.clear(this.engine.world, false); | 		Matter.World.clear(this.engine.world, false); | ||||||
| 		Matter.Engine.clear(this.engine); | 		Matter.Engine.clear(this.engine); | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -787,6 +787,9 @@ importers: | |||||||
|       sass: |       sass: | ||||||
|         specifier: 1.69.5 |         specifier: 1.69.5 | ||||||
|         version: 1.69.5 |         version: 1.69.5 | ||||||
|  |       seedrandom: | ||||||
|  |         specifier: ^3.0.5 | ||||||
|  |         version: 3.0.5 | ||||||
|       shiki: |       shiki: | ||||||
|         specifier: 0.14.7 |         specifier: 0.14.7 | ||||||
|         version: 0.14.7 |         version: 0.14.7 | ||||||
| @@ -7401,7 +7404,7 @@ packages: | |||||||
|     hasBin: true |     hasBin: true | ||||||
|     peerDependencies: |     peerDependencies: | ||||||
|       '@swc/core': ^1.2.66 |       '@swc/core': ^1.2.66 | ||||||
|       chokidar: ^3.5.1 |       chokidar: 3.5.3 | ||||||
|     peerDependenciesMeta: |     peerDependenciesMeta: | ||||||
|       chokidar: |       chokidar: | ||||||
|         optional: true |         optional: true | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo