merge: upstream
This commit is contained in:
commit
fd69a2fbbd
74 changed files with 659 additions and 745 deletions
Binary file not shown.
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 139 KiB |
|
@ -45,6 +45,7 @@
|
|||
"crc-32": "^1.2.2",
|
||||
"cropperjs": "2.0.0-beta.4",
|
||||
"date-fns": "2.30.0",
|
||||
"defu": "^6.1.4",
|
||||
"escape-regexp": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
"eventemitter3": "5.0.1",
|
||||
|
@ -54,9 +55,9 @@
|
|||
"json5": "2.2.3",
|
||||
"katex": "0.16.9",
|
||||
"matter-js": "0.19.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"photoswipe": "5.4.3",
|
||||
"punycode": "2.3.1",
|
||||
"rollup": "4.9.6",
|
||||
|
@ -112,7 +113,7 @@
|
|||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@vitest/coverage-v8": "1.2.1",
|
||||
"@vitest/coverage-v8": "0.34.6",
|
||||
"@vue/runtime-core": "3.4.15",
|
||||
"acorn": "8.11.3",
|
||||
"cross-env": "7.0.3",
|
||||
|
@ -134,7 +135,7 @@
|
|||
"storybook": "7.6.10",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "1.2.1",
|
||||
"vitest": "0.34.6",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-eslint-parser": "9.4.0",
|
||||
"vue-tsc": "1.8.27"
|
||||
|
|
|
@ -163,7 +163,7 @@ const $i = signinRequired();
|
|||
|
||||
const props = defineProps<{
|
||||
game: Misskey.entities.ReversiGameDetailed;
|
||||
connection: Misskey.ChannelConnection;
|
||||
connection?: Misskey.ChannelConnection | null;
|
||||
}>();
|
||||
|
||||
const showBoardLabels = ref<boolean>(false);
|
||||
|
@ -240,10 +240,10 @@ watch(logPos, (v) => {
|
|||
|
||||
if (game.value.isStarted && !game.value.isEnded) {
|
||||
useInterval(() => {
|
||||
if (game.value.isEnded) return;
|
||||
if (game.value.isEnded || props.connection == null) return;
|
||||
const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString();
|
||||
if (_DEV_) console.log('crc32', crc32);
|
||||
props.connection.send('checkState', {
|
||||
props.connection.send('resync', {
|
||||
crc32: crc32,
|
||||
});
|
||||
}, 10000, { immediate: false, afterMounted: true });
|
||||
|
@ -267,7 +267,7 @@ function putStone(pos) {
|
|||
});
|
||||
|
||||
const id = Math.random().toString(36).slice(2);
|
||||
props.connection.send('putStone', {
|
||||
props.connection!.send('putStone', {
|
||||
pos: pos,
|
||||
id,
|
||||
});
|
||||
|
@ -283,22 +283,24 @@ const myTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
|
|||
const opTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
|
||||
|
||||
const TIMER_INTERVAL_SEC = 3;
|
||||
useInterval(() => {
|
||||
if (myTurnTimerRmain.value > 0) {
|
||||
myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
||||
}
|
||||
if (opTurnTimerRmain.value > 0) {
|
||||
opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
||||
}
|
||||
|
||||
if (iAmPlayer.value) {
|
||||
if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) {
|
||||
props.connection.send('claimTimeIsUp', {});
|
||||
if (!props.game.isEnded) {
|
||||
useInterval(() => {
|
||||
if (myTurnTimerRmain.value > 0) {
|
||||
myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
||||
}
|
||||
if (opTurnTimerRmain.value > 0) {
|
||||
opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC);
|
||||
}
|
||||
}
|
||||
}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
|
||||
|
||||
function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
||||
if (iAmPlayer.value) {
|
||||
if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) {
|
||||
props.connection!.send('claimTimeIsUp', {});
|
||||
}
|
||||
}
|
||||
}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
|
||||
}
|
||||
|
||||
async function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
||||
game.value.logs = Reversi.Serializer.serializeLogs([
|
||||
...Reversi.Serializer.deserializeLogs(game.value.logs),
|
||||
log,
|
||||
|
@ -309,17 +311,25 @@ function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
|
|||
if (log.id == null || !appliedOps.includes(log.id)) {
|
||||
switch (log.operation) {
|
||||
case 'put': {
|
||||
sound.playUrl('/client-assets/reversi/put.mp3', {
|
||||
volume: 1,
|
||||
playbackRate: 1,
|
||||
});
|
||||
|
||||
if (log.player !== engine.value.turn) { // = desyncが発生している
|
||||
const _game = await misskeyApi('reversi/show-game', {
|
||||
gameId: props.game.id,
|
||||
});
|
||||
restoreGame(_game);
|
||||
return;
|
||||
}
|
||||
|
||||
engine.value.putStone(log.pos);
|
||||
triggerRef(engine);
|
||||
|
||||
myTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
||||
opTurnTimerRmain.value = game.value.timeLimitForEachTurn;
|
||||
|
||||
sound.playUrl('/client-assets/reversi/put.mp3', {
|
||||
volume: 1,
|
||||
playbackRate: 1,
|
||||
});
|
||||
|
||||
checkEnd();
|
||||
break;
|
||||
}
|
||||
|
@ -366,9 +376,7 @@ function checkEnd() {
|
|||
}
|
||||
}
|
||||
|
||||
function onStreamRescue(_game) {
|
||||
console.log('rescue');
|
||||
|
||||
function restoreGame(_game) {
|
||||
game.value = deepClone(_game);
|
||||
|
||||
engine.value = Reversi.Serializer.restoreGame({
|
||||
|
@ -384,6 +392,12 @@ function onStreamRescue(_game) {
|
|||
checkEnd();
|
||||
}
|
||||
|
||||
function onStreamResynced(_game) {
|
||||
console.log('resynced');
|
||||
|
||||
restoreGame(_game);
|
||||
}
|
||||
|
||||
async function surrender() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
|
@ -434,27 +448,35 @@ function share() {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
props.connection.on('log', onStreamLog);
|
||||
props.connection.on('rescue', onStreamRescue);
|
||||
props.connection.on('ended', onStreamEnded);
|
||||
if (props.connection != null) {
|
||||
props.connection.on('log', onStreamLog);
|
||||
props.connection.on('resynced', onStreamResynced);
|
||||
props.connection.on('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
props.connection.on('log', onStreamLog);
|
||||
props.connection.on('rescue', onStreamRescue);
|
||||
props.connection.on('ended', onStreamEnded);
|
||||
if (props.connection != null) {
|
||||
props.connection.on('log', onStreamLog);
|
||||
props.connection.on('resynced', onStreamResynced);
|
||||
props.connection.on('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
props.connection.off('log', onStreamLog);
|
||||
props.connection.off('rescue', onStreamRescue);
|
||||
props.connection.off('ended', onStreamEnded);
|
||||
if (props.connection != null) {
|
||||
props.connection.off('log', onStreamLog);
|
||||
props.connection.off('resynced', onStreamResynced);
|
||||
props.connection.off('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
props.connection.off('log', onStreamLog);
|
||||
props.connection.off('rescue', onStreamRescue);
|
||||
props.connection.off('ended', onStreamEnded);
|
||||
if (props.connection != null) {
|
||||
props.connection.off('log', onStreamLog);
|
||||
props.connection.off('resynced', onStreamResynced);
|
||||
props.connection.off('ended', onStreamEnded);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="game == null || connection == null"><MkLoading/></div>
|
||||
<GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection"/>
|
||||
<div v-if="game == null || (!game.isEnded && connection == null)"><MkLoading/></div>
|
||||
<GameSetting v-else-if="!game.isStarted" :game="game" :connection="connection!"/>
|
||||
<GameBoard v-else :game="game" :connection="connection"/>
|
||||
</template>
|
||||
|
||||
|
@ -47,23 +47,25 @@ async function fetchGame() {
|
|||
if (connection.value) {
|
||||
connection.value.dispose();
|
||||
}
|
||||
connection.value = useStream().useChannel('reversiGame', {
|
||||
gameId: game.value.id,
|
||||
});
|
||||
connection.value.on('started', x => {
|
||||
game.value = x.game;
|
||||
});
|
||||
connection.value.on('canceled', x => {
|
||||
connection.value?.dispose();
|
||||
if (!game.value.isEnded) {
|
||||
connection.value = useStream().useChannel('reversiGame', {
|
||||
gameId: game.value.id,
|
||||
});
|
||||
connection.value.on('started', x => {
|
||||
game.value = x.game;
|
||||
});
|
||||
connection.value.on('canceled', x => {
|
||||
connection.value?.dispose();
|
||||
|
||||
if (x.userId !== $i.id) {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
text: i18n.ts._reversi.gameCanceled,
|
||||
});
|
||||
router.push('/reversi');
|
||||
}
|
||||
});
|
||||
if (x.userId !== $i.id) {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
text: i18n.ts._reversi.gameCanceled,
|
||||
});
|
||||
router.push('/reversi');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
@ -73,9 +73,8 @@ const src = computed({
|
|||
set: (x) => saveSrc(x),
|
||||
});
|
||||
const withRenotes = computed({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
get: () => (defaultStore.reactiveState.tl.value.filter?.withRenotes ?? saveTlFilter('withRenotes', true)),
|
||||
set: (x) => saveTlFilter('withRenotes', x),
|
||||
get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
|
||||
set: (x: boolean) => saveTlFilter('withRenotes', x),
|
||||
});
|
||||
const withReplies = computed({
|
||||
get: () => {
|
||||
|
@ -83,11 +82,10 @@ const withReplies = computed({
|
|||
if (['local', 'social'].includes(src.value) && onlyFiles.value) {
|
||||
return false;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
return defaultStore.reactiveState.tl.value.filter?.withReplies ?? saveTlFilter('withReplies', true);
|
||||
return defaultStore.reactiveState.tl.value.filter.withReplies;
|
||||
}
|
||||
},
|
||||
set: (x) => saveTlFilter('withReplies', x),
|
||||
set: (x: boolean) => saveTlFilter('withReplies', x),
|
||||
});
|
||||
const withBots = computed({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
|
@ -99,16 +97,14 @@ const onlyFiles = computed({
|
|||
if (['local', 'social'].includes(src.value) && withReplies.value) {
|
||||
return false;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
return defaultStore.reactiveState.tl.value.filter?.onlyFiles ?? saveTlFilter('onlyFiles', false);
|
||||
return defaultStore.reactiveState.tl.value.filter.onlyFiles;
|
||||
}
|
||||
},
|
||||
set: (x) => saveTlFilter('onlyFiles', x),
|
||||
set: (x: boolean) => saveTlFilter('onlyFiles', x),
|
||||
});
|
||||
const withSensitive = computed({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
get: () => (defaultStore.reactiveState.tl.value.filter?.withSensitive ?? saveTlFilter('withSensitive', true)),
|
||||
set: (x) => {
|
||||
get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
|
||||
set: (x: boolean) => {
|
||||
saveTlFilter('withSensitive', x);
|
||||
|
||||
// これだけはクライアント側で完結する処理なので手動でリロード
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import { BroadcastChannel } from 'broadcast-channel';
|
||||
import { defu } from 'defu';
|
||||
import { $i } from '@/account.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { get, set } from '@/scripts/idb-proxy.js';
|
||||
|
@ -80,6 +81,18 @@ export class Storage<T extends StateDef> {
|
|||
this.loaded = this.ready.then(() => this.load());
|
||||
}
|
||||
|
||||
private isPureObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
private mergeState<T>(value: T, def: T): T {
|
||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def);
|
||||
return defu(value, def) as T;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private async init(): Promise<void> {
|
||||
await this.migrate();
|
||||
|
||||
|
@ -89,11 +102,11 @@ export class Storage<T extends StateDef> {
|
|||
|
||||
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
|
||||
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
|
||||
this.reactiveState[k].value = this.state[k] = deviceState[k];
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], v.default);
|
||||
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
|
||||
this.reactiveState[k].value = this.state[k] = registryCache[k];
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], v.default);
|
||||
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
|
||||
this.reactiveState[k].value = this.state[k] = deviceAccountState[k];
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
|
||||
} else {
|
||||
this.reactiveState[k].value = this.state[k] = v.default;
|
||||
if (_DEV_) console.log('Use default value', k, v.default);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue