parent
762fa6a8d8
commit
cf54c2ba47
16 changed files with 391 additions and 10 deletions
|
@ -679,9 +679,11 @@ function endReplay() {
|
|||
function exportLog() {
|
||||
if (!logs) return;
|
||||
const data = JSON.stringify({
|
||||
seed: seed,
|
||||
date: new Date().toISOString(),
|
||||
logs: logs,
|
||||
v: game.GAME_VERSION,
|
||||
m: props.gameMode,
|
||||
s: seed,
|
||||
d: new Date().toISOString(),
|
||||
l: DropAndFusionGame.serializeLogs(logs),
|
||||
});
|
||||
copyToClipboard(data);
|
||||
os.success();
|
||||
|
@ -723,8 +725,15 @@ function getGameImageDriveFile() {
|
|||
const [frame, logo] = images;
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, game.GAME_WIDTH, game.GAME_HEIGHT);
|
||||
|
||||
ctx.drawImage(frame, 0, 0, game.GAME_WIDTH, game.GAME_HEIGHT);
|
||||
ctx.drawImage(canvasEl.value!, 0, 0, game.GAME_WIDTH, game.GAME_HEIGHT);
|
||||
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = '16px bold sans-serif';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(`SCORE: ${score.value.toLocaleString()}`, 10, 10);
|
||||
|
||||
ctx.globalAlpha = 0.7;
|
||||
ctx.drawImage(logo, game.GAME_WIDTH * 0.55, 6, game.GAME_WIDTH * 0.45, game.GAME_WIDTH * 0.45 * (logo.height / logo.width));
|
||||
ctx.globalAlpha = 1;
|
||||
|
@ -765,7 +774,7 @@ async function share() {
|
|||
os.post({
|
||||
initialText: `#BubbleGame
|
||||
MODE: ${props.gameMode}
|
||||
SCORE: ${score.value} (MAX CHAIN: ${maxCombo.value})`,
|
||||
SCORE: ${score.value.toLocaleString()} (MAX CHAIN: ${maxCombo.value})`,
|
||||
initialFiles: [file],
|
||||
instant: true,
|
||||
});
|
||||
|
@ -859,6 +868,14 @@ function attachGameEvents() {
|
|||
dropReady.value = false;
|
||||
isGameOver.value = true;
|
||||
|
||||
misskeyApi('bubble-game/register', {
|
||||
seed,
|
||||
score: score.value,
|
||||
gameMode: props.gameMode,
|
||||
gameVersion: game.GAME_VERSION,
|
||||
logs: DropAndFusionGame.serializeLogs(logs),
|
||||
});
|
||||
|
||||
if (score.value > (highScore.value ?? 0)) {
|
||||
highScore.value = score.value;
|
||||
|
||||
|
|
|
@ -39,6 +39,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_gaps_s" style="padding: 16px;">
|
||||
<div><b>{{ i18n.ts.ranking }}</b> ({{ gameMode }})</div>
|
||||
<div v-if="ranking" class="_gaps_s">
|
||||
<div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
|
||||
<MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
|
||||
<MkUserName :user="r.user" :nowrap="true"/>
|
||||
<b style="margin-left: auto;">{{ r.score.toLocaleString() }} pt</b>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>{{ i18n.ts.loading }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner" style="padding: 16px;">
|
||||
<div style="font-weight: bold;">{{ i18n.ts._bubbleGame.howToPlay }}</div>
|
||||
|
@ -70,17 +85,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import XGame from './drop-and-fusion.game.vue';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||
|
||||
const gameMode = ref<'normal' | 'square'>('normal');
|
||||
const gameStarted = ref(false);
|
||||
const mute = ref(false);
|
||||
const ranking = ref(null);
|
||||
|
||||
watch(gameMode, async () => {
|
||||
ranking.value = await misskeyApiGet('bubble-game/ranking', { gameMode: gameMode.value });
|
||||
}, { immediate: true });
|
||||
|
||||
async function start() {
|
||||
gameStarted.value = true;
|
||||
|
@ -149,4 +170,13 @@ definePageMetadata({
|
|||
border-top: 1px solid #693410;
|
||||
border-bottom: 1px solid #ce8a5c;
|
||||
}
|
||||
|
||||
.rankingRecord {
|
||||
display: flex;
|
||||
line-height: 24px;
|
||||
padding-top: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: visible;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -32,7 +32,6 @@ type Log = {
|
|||
operation: 'surrender';
|
||||
};
|
||||
|
||||
// TODO: インスタンスを作り直さなくてもゲームをリスタートできるようにする
|
||||
export class DropAndFusionGame extends EventEmitter<{
|
||||
changeScore: (newScore: number) => void;
|
||||
changeCombo: (newCombo: number) => void;
|
||||
|
@ -46,12 +45,14 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
}> {
|
||||
private PHYSICS_QUALITY_FACTOR = 16; // 低いほどパフォーマンスが高いがガタガタして安定しなくなる、逆に高すぎても何故か不安定になる
|
||||
private COMBO_INTERVAL = 60; // frame
|
||||
public readonly GAME_VERSION = 1;
|
||||
public readonly GAME_WIDTH = 450;
|
||||
public readonly GAME_HEIGHT = 600;
|
||||
public readonly DROP_INTERVAL = 500;
|
||||
public readonly PLAYAREA_MARGIN = 25;
|
||||
private STOCK_MAX = 4;
|
||||
private TICK_DELTA = 1000 / 60; // 60fps
|
||||
|
||||
public frame = 0;
|
||||
public engine: Matter.Engine;
|
||||
private tickCallbackQueue: { frame: number; callback: () => void; }[] = [];
|
||||
|
@ -156,6 +157,10 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
Matter.Composite.add(this.engine.world, this.overflowCollider);
|
||||
}
|
||||
|
||||
private msToFrame(ms: number) {
|
||||
return Math.round(ms / this.TICK_DELTA);
|
||||
}
|
||||
|
||||
private createBody(mono: Mono, x: number, y: number) {
|
||||
const options: Matter.IBodyDefinition = {
|
||||
label: mono.id,
|
||||
|
@ -209,7 +214,7 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
|
||||
// 連鎖してfusionした場合の分かりやすさのため少し間を置いてからfusion対象になるようにする
|
||||
this.tickCallbackQueue.push({
|
||||
frame: this.frame + 6,
|
||||
frame: this.frame + this.msToFrame(100),
|
||||
callback: () => {
|
||||
this.activeBodyIds.push(body.id);
|
||||
},
|
||||
|
@ -261,7 +266,7 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
} else {
|
||||
this.fusionReservedPairs.push({ bodyA, bodyB });
|
||||
this.tickCallbackQueue.push({
|
||||
frame: this.frame + 6,
|
||||
frame: this.frame + this.msToFrame(100),
|
||||
callback: () => {
|
||||
this.fusionReservedPairs = this.fusionReservedPairs.filter(x => x.bodyA.id !== bodyA.id && x.bodyB.id !== bodyB.id);
|
||||
this.fusion(bodyA, bodyB);
|
||||
|
@ -396,6 +401,66 @@ export class DropAndFusionGame extends EventEmitter<{
|
|||
}
|
||||
}
|
||||
|
||||
public static serializeLogs(logs: Log[]) {
|
||||
const _logs: number[][] = [];
|
||||
|
||||
for (let i = 0; i < logs.length; i++) {
|
||||
const log = logs[i];
|
||||
const frameDelta = i === 0 ? log.frame : log.frame - logs[i - 1].frame;
|
||||
|
||||
switch (log.operation) {
|
||||
case 'drop':
|
||||
_logs.push([frameDelta, 0, log.x]);
|
||||
break;
|
||||
case 'hold':
|
||||
_logs.push([frameDelta, 1]);
|
||||
break;
|
||||
case 'surrender':
|
||||
_logs.push([frameDelta, 2]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return _logs;
|
||||
}
|
||||
|
||||
public static deserializeLogs(logs: number[][]) {
|
||||
const _logs: Log[] = [];
|
||||
|
||||
let frame = 0;
|
||||
|
||||
for (const log of logs) {
|
||||
const frameDelta = log[0];
|
||||
frame += frameDelta;
|
||||
|
||||
const operation = log[1];
|
||||
|
||||
switch (operation) {
|
||||
case 0:
|
||||
_logs.push({
|
||||
frame,
|
||||
operation: 'drop',
|
||||
x: log[2],
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
_logs.push({
|
||||
frame,
|
||||
operation: 'hold',
|
||||
});
|
||||
break;
|
||||
case 2:
|
||||
_logs.push({
|
||||
frame,
|
||||
operation: 'surrender',
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return _logs;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
Matter.World.clear(this.engine.world, false);
|
||||
Matter.Engine.clear(this.engine);
|
||||
|
|
|
@ -92,7 +92,6 @@ export type OperationType = typeof operationTypes[number];
|
|||
* @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
|
||||
*/
|
||||
export async function loadAudio(url: string, options?: { useCache?: boolean; }) {
|
||||
if (_DEV_) console.log('loading audio. opts:', options);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (ctx == null) {
|
||||
ctx = new AudioContext();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue