Implement rudimentary authn/authz, work on scene states, and add tRPC interfaces for scene states
Some checks failed
Build / build (push) Failing after 35s
Some checks failed
Build / build (push) Failing after 35s
This commit is contained in:
parent
22d1b8ceff
commit
68a91aa31a
13 changed files with 276 additions and 62 deletions
|
@ -3,6 +3,8 @@ import type { PlayerBroadcast } from "../shared/broadcast"
|
||||||
import { script } from "../shared/script"
|
import { script } from "../shared/script"
|
||||||
import type { SceneDefinition } from "../shared/script/types"
|
import type { SceneDefinition } from "../shared/script/types"
|
||||||
import { sceneTypesById } from "./scene-types"
|
import { sceneTypesById } from "./scene-types"
|
||||||
|
import type { Tagged } from "type-fest"
|
||||||
|
import type { SessionId } from "./session"
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
"player-broadcast": [PlayerBroadcast]
|
"player-broadcast": [PlayerBroadcast]
|
||||||
|
@ -31,6 +33,10 @@ export class Game {
|
||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removePlayer(sessionId: SessionId) {
|
||||||
|
this.currentScene.state.removePlayer(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
switchScene(sceneId: string) {
|
switchScene(sceneId: string) {
|
||||||
const definition = script.scenesById.get(sceneId)
|
const definition = script.scenesById.get(sceneId)
|
||||||
if (definition === undefined) throw new Error(`Unknown scene: ${sceneId}`)
|
if (definition === undefined) throw new Error(`Unknown scene: ${sceneId}`)
|
||||||
|
@ -43,9 +49,11 @@ export class Game {
|
||||||
this.eventBus.emit("player-broadcast", { type: "scene-changed", sceneId: sceneId })
|
this.eventBus.emit("player-broadcast", { type: "scene-changed", sceneId: sceneId })
|
||||||
}
|
}
|
||||||
|
|
||||||
withSceneState<Type extends keyof typeof sceneTypesById>(type: Type, block: (state: ReturnType<typeof sceneTypesById[Type]["createState"]>) => void) {
|
withSceneState<Type extends keyof typeof sceneTypesById, R>(type: Type, block: (state: ReturnType<typeof sceneTypesById[Type]["createState"]>) => R): R | null {
|
||||||
if (this.currentScene.definition.type === type) {
|
if (this.currentScene.definition.type === type) {
|
||||||
block(this.currentScene.state)
|
return block(this.currentScene.state)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ const expressApp = createExpressApp()
|
||||||
|
|
||||||
expressApp.use("/trpc", createTrpcMiddleware({
|
expressApp.use("/trpc", createTrpcMiddleware({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: ({ req, res }) => createContext(res),
|
createContext: ({ req, res }) => createContext(req, res),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (!isDev) expressApp.use(staticMiddleware(resolve(import.meta.dirname, "../dist")))
|
if (!isDev) expressApp.use(staticMiddleware(resolve(import.meta.dirname, "../dist")))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Game } from "../game"
|
import type { Game } from "../game"
|
||||||
import type { SceneDefinitionBase } from "../../shared/script/types"
|
import type { SceneDefinitionBase } from "../../shared/script/types"
|
||||||
|
import type { SessionId } from "../session"
|
||||||
|
|
||||||
export interface SceneType<DefinitionT extends SceneDefinitionBase, StateT extends SceneState> {
|
export interface SceneType<DefinitionT extends SceneDefinitionBase, StateT extends SceneState> {
|
||||||
id: DefinitionT["id"]
|
id: DefinitionT["id"]
|
||||||
|
@ -8,4 +9,5 @@ export interface SceneType<DefinitionT extends SceneDefinitionBase, StateT exten
|
||||||
|
|
||||||
export interface SceneState<EventT = any> {
|
export interface SceneState<EventT = any> {
|
||||||
getConnectionEvents(): EventT[]
|
getConnectionEvents(): EventT[]
|
||||||
|
removePlayer(sessionId: SessionId): void
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,87 @@
|
||||||
import { type SceneState, type SceneType } from "./base"
|
import { type SceneState, type SceneType } from "./base"
|
||||||
import type { Game } from "../game"
|
import type { Game } from "../game"
|
||||||
import type { ChoiceSceneDefinition, TextSceneDefinition } from "../../shared/script/types"
|
import type { ChoiceSceneDefinition, TextSceneDefinition } from "../../shared/script/types"
|
||||||
|
import type { ChoiceSceneEvent } from "../../shared/scene-types/choice"
|
||||||
|
import type { SessionId } from "../session"
|
||||||
|
import { getSuggestedInteractionId, type InteractionSceneEvent } from "../../shared/scene-types/interaction"
|
||||||
|
import type { SuggestedInteraction } from "../../shared/mutations"
|
||||||
|
|
||||||
export const ChoiceSceneType: SceneType<ChoiceSceneDefinition, SceneState> = {
|
export const ChoiceSceneType: SceneType<ChoiceSceneDefinition, ChoiceSceneState> = {
|
||||||
id: "choice",
|
id: "choice",
|
||||||
createState(game: Game, definition: ChoiceSceneDefinition): SceneState {
|
createState(game: Game, definition: ChoiceSceneDefinition): ChoiceSceneState {
|
||||||
return {
|
return new ChoiceSceneState(game, definition)
|
||||||
getConnectionEvents() {
|
}
|
||||||
return []
|
}
|
||||||
}
|
|
||||||
}
|
class ChoiceSceneState implements SceneState<ChoiceSceneEvent> {
|
||||||
|
private optionsById: Map<string, { votes: Set<SessionId> }> = new Map()
|
||||||
|
private votedOptionIdBySessionId: Map<SessionId, string> = new Map()
|
||||||
|
private isVotingConcluded: boolean = false
|
||||||
|
|
||||||
|
constructor(private game: Game, private definition: ChoiceSceneDefinition) {
|
||||||
|
definition.options.forEach(o => this.optionsById.set(o.id, { votes: new Set() }))
|
||||||
|
}
|
||||||
|
|
||||||
|
getConnectionEvents(): ChoiceSceneEvent[] {
|
||||||
|
const events: ChoiceSceneEvent[] = []
|
||||||
|
events.push(this.getVotesChangedEvent())
|
||||||
|
|
||||||
|
if (this.isVotingConcluded) events.push({ type: "voting-concluded" })
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
|
removePlayer(sessionId: SessionId) {
|
||||||
|
const votedOptionId = this.votedOptionIdBySessionId.get(sessionId)
|
||||||
|
if (votedOptionId !== undefined) {
|
||||||
|
const { votes } = this.optionsById.get(votedOptionId)!
|
||||||
|
votes.delete(sessionId)
|
||||||
|
this.emitVotesChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.votedOptionIdBySessionId.delete(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit(event: ChoiceSceneEvent) {
|
||||||
|
this.game.eventBus.emit("player-broadcast", { type: "scene-event", event })
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVotesChangedEvent(): ChoiceSceneEvent {
|
||||||
|
return { type: "votes-changed", votesByOptionId: Object.fromEntries(this.optionsById.entries().map(([id, { votes }]) => ([id, votes.size]))) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitVotesChanged() {
|
||||||
|
this.emit(this.getVotesChangedEvent())
|
||||||
|
}
|
||||||
|
|
||||||
|
setVote(optionId: string | null, sessionId: SessionId) {
|
||||||
|
const currentVotedOptionId = this.votedOptionIdBySessionId.get(sessionId)
|
||||||
|
if (currentVotedOptionId !== undefined) {
|
||||||
|
if (optionId === currentVotedOptionId) return
|
||||||
|
const { votes } = this.optionsById.get(currentVotedOptionId)!
|
||||||
|
votes.delete(sessionId)
|
||||||
|
this.votedOptionIdBySessionId.delete(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionId !== null) {
|
||||||
|
const { votes } = this.optionsById.get(optionId)!
|
||||||
|
votes.add(sessionId)
|
||||||
|
this.votedOptionIdBySessionId.set(sessionId, optionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emitVotesChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
concludeVoting() {
|
||||||
|
if (this.isVotingConcluded) return
|
||||||
|
this.isVotingConcluded = true
|
||||||
|
this.emit({ type: "voting-concluded"})
|
||||||
|
}
|
||||||
|
|
||||||
|
restartVoting() {
|
||||||
|
this.isVotingConcluded = false
|
||||||
|
this.optionsById.forEach(({ votes }) => votes.clear())
|
||||||
|
this.votedOptionIdBySessionId.clear()
|
||||||
|
this.emitVotesChanged()
|
||||||
|
this.emit({ type: "voting-restarted" })
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ import type { Game } from "../game"
|
||||||
import type { SuggestedInteraction } from "../../shared/mutations"
|
import type { SuggestedInteraction } from "../../shared/mutations"
|
||||||
import type { InteractionSceneDefinition } from "../../shared/script/types"
|
import type { InteractionSceneDefinition } from "../../shared/script/types"
|
||||||
import { getSuggestedInteractionId, type InteractionSceneEvent } from "../../shared/scene-types/interaction"
|
import { getSuggestedInteractionId, type InteractionSceneEvent } from "../../shared/scene-types/interaction"
|
||||||
|
import type { SessionId } from "../session"
|
||||||
|
|
||||||
export const InteractionSceneType = {
|
export const InteractionSceneType = {
|
||||||
id: "interaction",
|
id: "interaction",
|
||||||
|
@ -12,7 +13,8 @@ export const InteractionSceneType = {
|
||||||
} as const satisfies SceneType<InteractionSceneDefinition, InteractionSceneState>
|
} as const satisfies SceneType<InteractionSceneDefinition, InteractionSceneState>
|
||||||
|
|
||||||
export class InteractionSceneState implements SceneState<InteractionSceneEvent> {
|
export class InteractionSceneState implements SceneState<InteractionSceneEvent> {
|
||||||
private interactionQueue: Map<string, { interaction: SuggestedInteraction; votes: number }> = new Map()
|
private interactionQueue: Map<string, { interaction: SuggestedInteraction; votes: Set<SessionId> }> = new Map()
|
||||||
|
private suggestedInteractionIdBySessionId: Map<SessionId, string> = new Map()
|
||||||
private ongoingInteractionExecution: SuggestedInteraction | null = null
|
private ongoingInteractionExecution: SuggestedInteraction | null = null
|
||||||
private objectVisibilityById = new Map<string, boolean>()
|
private objectVisibilityById = new Map<string, boolean>()
|
||||||
|
|
||||||
|
@ -31,43 +33,61 @@ export class InteractionSceneState implements SceneState<InteractionSceneEvent>
|
||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
private emit(event: InteractionSceneEvent) {
|
removePlayer(sessionId: SessionId) {
|
||||||
this.game.emit("player-broadcast", { type: "scene-event", event })
|
const suggestedInteractionId = this.suggestedInteractionIdBySessionId.get(sessionId)
|
||||||
|
if (suggestedInteractionId !== undefined) {
|
||||||
|
const { votes } = this.interactionQueue.get(suggestedInteractionId)!
|
||||||
|
votes.delete(sessionId)
|
||||||
|
if (votes.size <= 0) this.interactionQueue.delete(suggestedInteractionId)
|
||||||
|
|
||||||
|
this.emitVotesChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
private getVotesChangedEvent() {
|
this.suggestedInteractionIdBySessionId.delete(sessionId)
|
||||||
return { type: "votes-changed", votesByInteractionId: Object.fromEntries(this.interactionQueue.entries().map(([id, { votes }]) => ([id, votes]))) } as const
|
}
|
||||||
|
|
||||||
|
private emit(event: InteractionSceneEvent) {
|
||||||
|
this.game.eventBus.emit("player-broadcast", { type: "scene-event", event })
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVotesChangedEvent(): InteractionSceneEvent {
|
||||||
|
return { type: "votes-changed", votesByInteractionId: Object.fromEntries(this.interactionQueue.entries().map(([id, { votes }]) => ([id, votes.size]))) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitVotesChanged() {
|
private emitVotesChanged() {
|
||||||
this.emit(this.getVotesChangedEvent())
|
this.emit(this.getVotesChangedEvent())
|
||||||
}
|
}
|
||||||
|
|
||||||
addInteractionVote(interaction: SuggestedInteraction) {
|
setInteractionVote(interaction: SuggestedInteraction | null, sessionId: SessionId) {
|
||||||
const id = getSuggestedInteractionId(interaction)
|
const newInteractionId = interaction === null ? null : getSuggestedInteractionId(interaction)
|
||||||
const existingItem = this.interactionQueue.get(id)
|
const currentSuggestedInteractionId = this.suggestedInteractionIdBySessionId.get(sessionId)
|
||||||
|
if (currentSuggestedInteractionId !== undefined) {
|
||||||
|
if (newInteractionId === currentSuggestedInteractionId) return
|
||||||
|
const { votes } = this.interactionQueue.get(currentSuggestedInteractionId)!
|
||||||
|
votes.delete(sessionId)
|
||||||
|
if (votes.size <= 0) this.interactionQueue.delete(currentSuggestedInteractionId)
|
||||||
|
this.suggestedInteractionIdBySessionId.delete(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newInteractionId !== null) {
|
||||||
|
const existingItem = this.interactionQueue.get(newInteractionId)
|
||||||
if (existingItem === undefined) {
|
if (existingItem === undefined) {
|
||||||
const item = {
|
const item = {
|
||||||
id,
|
id: newInteractionId,
|
||||||
votes: 1,
|
votes: new Set([sessionId]),
|
||||||
interaction
|
interaction: interaction!
|
||||||
}
|
}
|
||||||
|
|
||||||
this.interactionQueue.set(item.id, item)
|
this.interactionQueue.set(item.id, item)
|
||||||
this.emit({ type: "interaction-queued", interaction })
|
this.emit({ type: "interaction-queued", interaction: interaction! })
|
||||||
} else {
|
} else {
|
||||||
existingItem.votes += 1
|
existingItem.votes.add(sessionId)
|
||||||
this.emitVotesChanged()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeInteractionVote(id: string) {
|
this.suggestedInteractionIdBySessionId.set(sessionId, newInteractionId)
|
||||||
const item = this.interactionQueue.get(id)
|
|
||||||
if (item !== undefined) {
|
|
||||||
item.votes -= 1
|
|
||||||
if (item.votes <= 0) this.interactionQueue.delete(id)
|
|
||||||
this.emitVotesChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emitVotesChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
startInteractionExecution(interaction: SuggestedInteraction) {
|
startInteractionExecution(interaction: SuggestedInteraction) {
|
||||||
|
@ -75,12 +95,11 @@ export class InteractionSceneState implements SceneState<InteractionSceneEvent>
|
||||||
this.emit({ type: "interaction-execution-started", interaction })
|
this.emit({ type: "interaction-execution-started", interaction })
|
||||||
}
|
}
|
||||||
|
|
||||||
finishInteractionExecution(id: string, requireStarted: boolean = true) {
|
finishInteractionExecution(id: string, onlyIfOngoing: boolean = true) {
|
||||||
if (requireStarted && (this.ongoingInteractionExecution === null || id !== getSuggestedInteractionId(this.ongoingInteractionExecution))) return
|
if (onlyIfOngoing && (this.ongoingInteractionExecution === null || id !== getSuggestedInteractionId(this.ongoingInteractionExecution))) return
|
||||||
this.ongoingInteractionExecution = null
|
this.ongoingInteractionExecution = null
|
||||||
this.emit({ type: "interaction-execution-finished" })
|
this.emit({ type: "interaction-execution-finished" })
|
||||||
this.interactionQueue.delete(id)
|
this.removeInteractionFromQueue(id)
|
||||||
this.emitVotesChanged()
|
|
||||||
|
|
||||||
const interaction = this.definition.interactionsById.get(id)
|
const interaction = this.definition.interactionsById.get(id)
|
||||||
if (interaction === undefined) return
|
if (interaction === undefined) return
|
||||||
|
@ -104,8 +123,16 @@ export class InteractionSceneState implements SceneState<InteractionSceneEvent>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeInteractionQueueItem(id: string) {
|
cancelInteractionExecution(id: string, onlyIfOngoing: boolean = true) {
|
||||||
if (!this.interactionQueue.has(id)) return
|
if (onlyIfOngoing && (this.ongoingInteractionExecution === null || id !== getSuggestedInteractionId(this.ongoingInteractionExecution))) return
|
||||||
|
this.ongoingInteractionExecution = null
|
||||||
|
this.emit({ type: "interaction-execution-cancelled" })
|
||||||
|
}
|
||||||
|
|
||||||
|
removeInteractionFromQueue(id: string) {
|
||||||
|
const queueItem = this.interactionQueue.get(id)
|
||||||
|
if (queueItem === undefined) return
|
||||||
|
queueItem.votes.forEach(sessionId => this.suggestedInteractionIdBySessionId.delete(sessionId))
|
||||||
this.interactionQueue.delete(id)
|
this.interactionQueue.delete(id)
|
||||||
this.emitVotesChanged()
|
this.emitVotesChanged()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { type SceneState, type SceneType } from "./base"
|
import { type SceneState, type SceneType } from "./base"
|
||||||
import type { Game } from "../game"
|
import type { Game } from "../game"
|
||||||
import type { TextSceneDefinition } from "../../shared/script/types"
|
import type { TextSceneDefinition } from "../../shared/script/types"
|
||||||
|
import type { SessionId } from "../session"
|
||||||
|
|
||||||
export const TextSceneType: SceneType<TextSceneDefinition, SceneState> = {
|
export const TextSceneType: SceneType<TextSceneDefinition, SceneState> = {
|
||||||
id: "text",
|
id: "text",
|
||||||
|
@ -8,6 +9,9 @@ export const TextSceneType: SceneType<TextSceneDefinition, SceneState> = {
|
||||||
return {
|
return {
|
||||||
getConnectionEvents() {
|
getConnectionEvents() {
|
||||||
return []
|
return []
|
||||||
|
},
|
||||||
|
removePlayer(sessionId: SessionId) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
backend/session.ts
Normal file
3
backend/session.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import type { Tagged } from "type-fest"
|
||||||
|
|
||||||
|
export type SessionId = Tagged<string, "SessionId">
|
|
@ -1,14 +1,22 @@
|
||||||
import { initTRPC } from "@trpc/server"
|
import { initTRPC } from "@trpc/server"
|
||||||
import type { Response } from "express"
|
import type { Request, Response } from "express"
|
||||||
import superjson from "superjson"
|
import superjson from "superjson"
|
||||||
|
import type { SessionId } from "../session"
|
||||||
|
|
||||||
export interface Context {
|
export interface Context {
|
||||||
res?: Response
|
res?: Response
|
||||||
|
sessionId: SessionId
|
||||||
|
isCrew: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createContext(res: Response | undefined): Promise<Context> {
|
export async function createContext(req: Request, res: Response | undefined): Promise<Context> {
|
||||||
|
const sessionId = req.headers["auio-session"]
|
||||||
|
if (sessionId === null || typeof sessionId !== "string" || sessionId.length !== 64) throw new Error(`Missing or invalid session ID: ${sessionId}`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
res
|
res,
|
||||||
|
sessionId: sessionId as SessionId,
|
||||||
|
isCrew: req.headers["auio-crew-token"] === process.env.AUIO_CREW_TOKEN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
75
backend/trpc/crew.ts
Normal file
75
backend/trpc/crew.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { t } from "./base"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { game } from "../game"
|
||||||
|
import { suggestedInteractionSchema } from "../../shared/mutations"
|
||||||
|
import { TRPCError } from "@trpc/server"
|
||||||
|
|
||||||
|
const crewProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
|
if (!ctx.isCrew) throw new TRPCError({ code: "UNAUTHORIZED" })
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const crewRouter = t.router({
|
||||||
|
switchRoom: crewProcedure
|
||||||
|
.input(z.object({
|
||||||
|
roomId: z.string()
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
game.switchScene(input.roomId)
|
||||||
|
}),
|
||||||
|
|
||||||
|
interactionScene: t.router({
|
||||||
|
setObjectVisibility: crewProcedure
|
||||||
|
.input(z.object({
|
||||||
|
objectId: z.string(),
|
||||||
|
isVisible: z.boolean()
|
||||||
|
}))
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
game.withSceneState("interaction", s => s.setObjectVisibility(input.objectId, input.isVisible))
|
||||||
|
}),
|
||||||
|
|
||||||
|
startInteractionExecution: crewProcedure
|
||||||
|
.input(z.object({
|
||||||
|
interaction: suggestedInteractionSchema
|
||||||
|
}))
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
game.withSceneState("interaction", s => s.startInteractionExecution(input.interaction))
|
||||||
|
}),
|
||||||
|
|
||||||
|
finishInteractionExecution: crewProcedure
|
||||||
|
.input(z.object({
|
||||||
|
interactionId: z.string(),
|
||||||
|
onlyIfOngoing: z.boolean()
|
||||||
|
}))
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
game.withSceneState("interaction", s => s.finishInteractionExecution(input.interactionId, input.onlyIfOngoing))
|
||||||
|
}),
|
||||||
|
|
||||||
|
cancelInteractionExecution: crewProcedure
|
||||||
|
.input(z.object({
|
||||||
|
interactionId: z.string(),
|
||||||
|
onlyIfOngoing: z.boolean()
|
||||||
|
}))
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
game.withSceneState("interaction", s => s.cancelInteractionExecution(input.interactionId, input.onlyIfOngoing))
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeInteractionFromQueue: crewProcedure
|
||||||
|
.input(z.object({
|
||||||
|
interactionId: z.string()
|
||||||
|
}))
|
||||||
|
.mutation(({ input }) => {
|
||||||
|
game.withSceneState("interaction", s => s.removeInteractionFromQueue(input.interactionId))
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
choiceScene: t.router({
|
||||||
|
concludeVoting: crewProcedure.mutation(() => {
|
||||||
|
game.withSceneState("choice", s => s.concludeVoting())
|
||||||
|
}),
|
||||||
|
|
||||||
|
restartVoting: crewProcedure.mutation(() => {
|
||||||
|
game.withSceneState("choice", s => s.restartVoting())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,13 +0,0 @@
|
||||||
import { t } from "./base"
|
|
||||||
import { z } from "zod"
|
|
||||||
import { game } from "../game"
|
|
||||||
|
|
||||||
export const directorRouter = t.router({
|
|
||||||
switchRoom: t.procedure
|
|
||||||
.input(z.object({
|
|
||||||
roomId: z.string()
|
|
||||||
}))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
game.switchScene(input.roomId)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,16 +1,20 @@
|
||||||
import { t } from "./base"
|
import { t } from "./base"
|
||||||
import { playerRouter } from "./player"
|
import { playerRouter } from "./player"
|
||||||
import { directorRouter } from "./director"
|
import { crewRouter } from "./crew"
|
||||||
import { game } from "../game"
|
import { game } from "../game"
|
||||||
import { on } from "node:events"
|
import { on } from "node:events"
|
||||||
|
|
||||||
export const appRouter = t.router({
|
export const appRouter = t.router({
|
||||||
player: playerRouter,
|
player: playerRouter,
|
||||||
director: directorRouter,
|
director: crewRouter,
|
||||||
|
|
||||||
join: t.procedure
|
join: t.procedure
|
||||||
.subscription(async function*() {
|
.subscription(async function*({ signal, ctx }) {
|
||||||
const iterable = on(game.eventBus, "player-broadcast")
|
signal!.addEventListener("abort", () => {
|
||||||
|
game.removePlayer(ctx.sessionId)
|
||||||
|
})
|
||||||
|
|
||||||
|
const iterable = on(game.eventBus, "player-broadcast", { signal })
|
||||||
|
|
||||||
for (const broadcast of game.getConnectionPlayerBroadcasts()) {
|
for (const broadcast of game.getConnectionPlayerBroadcasts()) {
|
||||||
yield broadcast
|
yield broadcast
|
||||||
|
|
|
@ -1,5 +1,26 @@
|
||||||
import { t } from "./base"
|
import { t } from "./base"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { suggestedInteractionSchema } from "../../shared/mutations"
|
||||||
|
import { game } from "../game"
|
||||||
|
|
||||||
export const playerRouter = t.router({
|
export const playerRouter = t.router({
|
||||||
|
interactionScene: t.router({
|
||||||
|
setInteractionVote: t.procedure
|
||||||
|
.input(z.object({
|
||||||
|
interaction: z.nullable(suggestedInteractionSchema)
|
||||||
|
}))
|
||||||
|
.mutation(({ ctx, input }) => {
|
||||||
|
game.withSceneState("interaction", s => s.setInteractionVote(input.interaction, ctx.sessionId))
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
choiceScene: t.router({
|
||||||
|
setVote: t.procedure
|
||||||
|
.input(z.object({
|
||||||
|
optionId: z.nullable(z.string())
|
||||||
|
}))
|
||||||
|
.mutation(({ ctx, input }) => {
|
||||||
|
game.withSceneState("choice", s => s.setVote(input.optionId, ctx.sessionId))
|
||||||
|
}),
|
||||||
|
})
|
||||||
})
|
})
|
|
@ -1,2 +1,4 @@
|
||||||
export type ChoiceSceneEvent =
|
export type ChoiceSceneEvent =
|
||||||
| { type: "option-votes-changed"}
|
| { type: "votes-changed", votesByOptionId: Record<string, number> }
|
||||||
|
| { type: "voting-concluded" }
|
||||||
|
| { type: "voting-restarted" }
|
Loading…
Add table
Reference in a new issue