This commit is contained in:
syuilo 2024-01-09 16:07:23 +09:00
parent 14aedc17ae
commit 14d4ffaa36
4 changed files with 119 additions and 17 deletions

View file

@ -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",

View file

@ -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' ? {

View file

@ -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);
} }

View file

@ -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