diff --git a/src/client/pages/reversi/game.board.vue b/src/client/pages/reversi/game.board.vue
index 002a99a0b..96ea86547 100644
--- a/src/client/pages/reversi/game.board.vue
+++ b/src/client/pages/reversi/game.board.vue
@@ -57,7 +57,7 @@
{{ $t('_reversi.turnCount', { count: logPos }) }} {{ $t('_reversi.black') }}:{{ o.blackCount }} {{ $t('_reversi.white') }}:{{ o.whiteCount }} {{ $t('_reversi.total') }}:{{ o.blackCount + o.whiteCount }}
- {{ $t('_reversi.surrender') }}
+ {{ $t('_reversi.surrender') }}
@@ -76,6 +76,10 @@
{{ $t('_reversi.loopedMap') }}
{{ $t('_reversi.canPutEverywhere') }}
+
+
+
+
@@ -113,6 +117,7 @@ export default defineComponent({
o: null as Reversi,
logs: [],
logPos: 0,
+ watchers: [],
pollingClock: null,
faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight, fasCircle, farCircle, faPlay
};
@@ -198,12 +203,14 @@ export default defineComponent({
this.connection.on('set', this.onSet);
this.connection.on('rescue', this.onRescue);
this.connection.on('ended', this.onEnded);
+ this.connection.on('watchers', this.onWatchers);
},
beforeUnmount() {
this.connection.off('set', this.onSet);
this.connection.off('rescue', this.onRescue);
this.connection.off('ended', this.onEnded);
+ this.connection.off('watchers', this.onWatchers);
clearInterval(this.pollingClock);
},
@@ -309,6 +316,10 @@ export default defineComponent({
this.$forceUpdate();
},
+ onWatchers(users) {
+ this.watchers = users;
+ },
+
surrender() {
os.api('games/reversi/games/surrender', {
gameId: this.game.id
@@ -506,5 +517,18 @@ export default defineComponent({
}
}
}
+
+ > .watchers {
+ padding: 0 0 16px 0;
+
+ &:empty {
+ display: none;
+ }
+
+ > .avatar {
+ width: 32px;
+ height: 32px;
+ }
+ }
}
diff --git a/src/server/api/stream/channels/games/reversi-game.ts b/src/server/api/stream/channels/games/reversi-game.ts
index d03501971..ea62ab1e8 100644
--- a/src/server/api/stream/channels/games/reversi-game.ts
+++ b/src/server/api/stream/channels/games/reversi-game.ts
@@ -5,7 +5,8 @@ import Reversi from '../../../../../games/reversi/core';
import * as maps from '../../../../../games/reversi/maps';
import Channel from '../../channel';
import { ReversiGame } from '../../../../../models/entities/games/reversi/game';
-import { ReversiGames } from '../../../../../models';
+import { ReversiGames, Users } from '../../../../../models';
+import { User } from '../../../../../models/entities/user';
export default class extends Channel {
public readonly chName = 'gamesReversiGame';
@@ -13,17 +14,58 @@ export default class extends Channel {
public static requireCredential = false;
private gameId: ReversiGame['id'] | null = null;
+ private watchers: Record = {};
+ private emitWatchersIntervalId: any;
@autobind
public async init(params: any) {
this.gameId = params.gameId;
// Subscribe game stream
- this.subscriber.on(`reversiGameStream:${this.gameId}`, data => {
+ this.subscriber.on(`reversiGameStream:${this.gameId}`, this.onEvent);
+ this.emitWatchersIntervalId = setInterval(this.emitWatchers, 5000);
+
+ const game = await ReversiGames.findOne(this.gameId!);
+ if (game == null) throw new Error('game not found');
+
+ // 観戦者イベント
+ this.watch(game);
+ }
+
+ @autobind
+ private onEvent(data: any) {
+ if (data.type === 'watching') {
+ const id = data.body;
+ this.watchers[id] = new Date();
+ } else {
this.send(data);
+ }
+ }
+
+ @autobind
+ private async emitWatchers() {
+ const now = new Date();
+
+ // Remove not watching users
+ for (const [userId, date] of Object.entries(this.watchers)) {
+ if (now.getTime() - date.getTime() > 5000) delete this.watchers[userId];
+ }
+
+ const users = await Users.packMany(Object.keys(this.watchers), null, { detail: false });
+
+ this.send({
+ type: 'watchers',
+ body: users,
});
}
+ @autobind
+ public dispose() {
+ // Unsubscribe events
+ this.subscriber.off(`reversiGameStream:${this.gameId}`, this.onEvent);
+ clearInterval(this.emitWatchersIntervalId);
+ }
+
@autobind
public onMessage(type: string, body: any) {
switch (type) {
@@ -314,5 +356,17 @@ export default class extends Channel {
if (crc32.toString() !== game.crc32) {
this.send('rescue', await ReversiGames.pack(game, this.user));
}
+
+ // ついでに観戦者イベントを発行
+ this.watch(game);
+ }
+
+ @autobind
+ private watch(game: ReversiGame) {
+ if (this.user != null) {
+ if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) {
+ publishReversiGameStream(this.gameId!, 'watching', this.user.id);
+ }
+ }
}
}