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