From 985a2c3b46d5418048cef88e06cf1a7cf0da216f Mon Sep 17 00:00:00 2001 From: sgfost Date: Thu, 17 Jul 2025 14:55:15 -0700 Subject: [PATCH 01/26] feat: setup lite-interactive version with chat messages this is the 'simplified' version of the port of mars that uses the lite game 'engine' but will add on: - port of mars framing - communication - interactive events - treatments by varying LAU cards --- .../src/api/pomlite/multiplayer/response.ts | 2 + client/src/views/ProlificMultiplayerStudy.vue | 2 +- server/src/entity/LiteChatMessage.ts | 30 +++++++++++ server/src/entity/LiteGame.ts | 4 ++ server/src/entity/LitePlayer.ts | 4 ++ server/src/entity/ProlificStudy.ts | 4 ++ server/src/entity/index.ts | 1 + ...752790734574-AddLiteProlificInteractive.ts | 31 +++++++++++ .../src/rooms/pomlite/multiplayer/commands.ts | 1 + server/src/rooms/pomlite/multiplayer/index.ts | 37 +++++++++++-- server/src/rooms/pomlite/multiplayer/state.ts | 52 ++++++++++++++++--- server/src/rooms/pomlite/solo/index.ts | 4 +- server/src/rooms/pomlite/solo/state.ts | 10 ++-- server/src/services/litegame.ts | 18 +++++++ server/src/services/study.ts | 7 ++- shared/src/lite/requests.ts | 7 ++- shared/src/lite/types.ts | 22 +++++++- shared/src/types.ts | 1 + 18 files changed, 217 insertions(+), 20 deletions(-) create mode 100644 server/src/entity/LiteChatMessage.ts create mode 100644 server/src/migration/1752790734574-AddLiteProlificInteractive.ts diff --git a/client/src/api/pomlite/multiplayer/response.ts b/client/src/api/pomlite/multiplayer/response.ts index 19e8ef692..7cc8153ae 100644 --- a/client/src/api/pomlite/multiplayer/response.ts +++ b/client/src/api/pomlite/multiplayer/response.ts @@ -132,4 +132,6 @@ export const DEFAULT_STATE: LiteGameClientState = { canInvest: false, isRoundTransitioning: false, isWaitingToStart: true, + chatMessages: [], + chatEnabled: false, }; diff --git a/client/src/views/ProlificMultiplayerStudy.vue b/client/src/views/ProlificMultiplayerStudy.vue index cd54c6175..afcb77e28 100644 --- a/client/src/views/ProlificMultiplayerStudy.vue +++ b/client/src/views/ProlificMultiplayerStudy.vue @@ -217,7 +217,7 @@ export default class ProlificMultiplayerStudy extends Vue { private async joinLobby() { try { this.lobbyRoom = await this.$client.joinOrCreate(LITE_LOBBY_NAME, { - type: "prolificBaseline", + type: this.participantStatus.startingGameType, }); applyLiteLobbyResponses(this.lobbyRoom, this); this.lobbyApi.connect(this.lobbyRoom); diff --git a/server/src/entity/LiteChatMessage.ts b/server/src/entity/LiteChatMessage.ts new file mode 100644 index 000000000..2625d09a1 --- /dev/null +++ b/server/src/entity/LiteChatMessage.ts @@ -0,0 +1,30 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { LitePlayer } from "./LitePlayer"; +import { LiteGame } from "./LiteGame"; + +@Entity() +export class LiteChatMessage { + @PrimaryGeneratedColumn() + id!: number; + + @Column() + dateCreated!: Date; + + @ManyToOne(type => LitePlayer) + player!: LitePlayer; + + @Column() + playerId!: number; + + @Column() + message!: string; + + @ManyToOne(type => LiteGame) + game!: LiteGame; + + @Column() + gameId!: number; + + @Column() + round!: number; +} diff --git a/server/src/entity/LiteGame.ts b/server/src/entity/LiteGame.ts index 613b272dc..e134d6296 100644 --- a/server/src/entity/LiteGame.ts +++ b/server/src/entity/LiteGame.ts @@ -12,6 +12,7 @@ import { LiteGameTreatment, SoloGameTreatment } from "./LiteGameTreatment"; import { LiteMarsEventDeck, SoloMarsEventDeck } from "./LiteMarsEventDeck"; import { LitePlayer, SoloPlayer } from "./LitePlayer"; import { LiteGameRound, SoloGameRound } from "./LiteGameRound"; +import { LiteChatMessage } from "./LiteChatMessage"; import { LiteGameStatus, LiteGameType } from "@port-of-mars/shared/lite"; export abstract class BaseLiteGame { @@ -82,6 +83,9 @@ export class LiteGame extends BaseLiteGame { @OneToMany(() => LiteGameRound, round => round.game) rounds!: LiteGameRound[]; + @OneToMany(() => LiteChatMessage, message => message.game) + chatMessages!: LiteChatMessage[]; + @OneToOne(() => LiteMarsEventDeck, { nullable: false }) @JoinColumn() deck!: LiteMarsEventDeck; diff --git a/server/src/entity/LitePlayer.ts b/server/src/entity/LitePlayer.ts index d5a930bee..67f26e81d 100644 --- a/server/src/entity/LitePlayer.ts +++ b/server/src/entity/LitePlayer.ts @@ -13,6 +13,7 @@ import { SoloGame, LiteGame } from "./LiteGame"; import { Role, ROLES } from "@port-of-mars/shared/types"; import { LitePlayerDecision } from "./LitePlayerDecision"; import { LitePlayerVote } from "./LitePlayerVote"; +import { LiteChatMessage } from "./LiteChatMessage"; export abstract class BaseLitePlayer { @PrimaryGeneratedColumn() @@ -64,4 +65,7 @@ export class LitePlayer extends BaseLitePlayer { @OneToMany(() => LitePlayerVote, vote => vote.player) votes!: LitePlayerVote[]; + + @OneToMany(() => LiteChatMessage, message => message.player) + chatMessages!: LiteChatMessage[]; } diff --git a/server/src/entity/ProlificStudy.ts b/server/src/entity/ProlificStudy.ts index 067f918e9..39f6425c7 100644 --- a/server/src/entity/ProlificStudy.ts +++ b/server/src/entity/ProlificStudy.ts @@ -4,6 +4,7 @@ import { ProlificMultiplayerStudyParticipant, ProlificSoloStudyParticipant, } from "./ProlificStudyParticipant"; +import { LiteGameType } from "@port-of-mars/shared/lite/types"; export abstract class BaseProlificStudy { @PrimaryGeneratedColumn() @@ -37,4 +38,7 @@ export class ProlificSoloStudy extends BaseProlificStudy { export class ProlificMultiplayerStudy extends BaseProlificStudy { @OneToMany(type => ProlificMultiplayerStudyParticipant, participant => participant.study) participants!: Array; + + @Column({ default: "prolificBaseline" }) + gameType!: LiteGameType; } diff --git a/server/src/entity/index.ts b/server/src/entity/index.ts index 670eaccb3..1440bcf47 100644 --- a/server/src/entity/index.ts +++ b/server/src/entity/index.ts @@ -21,5 +21,6 @@ export * from "./LiteMarsEventDeck"; export * from "./LiteMarsEventDeckCard"; export * from "./LitePlayer"; export * from "./LitePlayerDecision"; +export * from "./LiteChatMessage"; export * from "./ProlificStudy"; export * from "./ProlificStudyParticipant"; diff --git a/server/src/migration/1752790734574-AddLiteProlificInteractive.ts b/server/src/migration/1752790734574-AddLiteProlificInteractive.ts new file mode 100644 index 000000000..aabd8af24 --- /dev/null +++ b/server/src/migration/1752790734574-AddLiteProlificInteractive.ts @@ -0,0 +1,31 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddLiteProlificInteractive1752790734574 implements MigrationInterface { + name = "AddLiteProlificInteractive1752790734574"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "lite_chat_message" ("id" SERIAL NOT NULL, "dateCreated" TIMESTAMP NOT NULL, "playerId" integer NOT NULL, "message" character varying NOT NULL, "gameId" integer NOT NULL, "round" integer NOT NULL, CONSTRAINT "PK_f31675cb7a773d75f5100660ff5" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `ALTER TABLE "prolific_multiplayer_study" ADD "gameType" character varying NOT NULL DEFAULT 'prolificBaseline'` + ); + await queryRunner.query( + `ALTER TABLE "lite_chat_message" ADD CONSTRAINT "FK_3e57003d074eb2cf18f58011601" FOREIGN KEY ("playerId") REFERENCES "lite_player"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "lite_chat_message" ADD CONSTRAINT "FK_a02ac0d70aa61c4e322385b6e55" FOREIGN KEY ("gameId") REFERENCES "lite_game"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "lite_chat_message" DROP CONSTRAINT "FK_a02ac0d70aa61c4e322385b6e55"` + ); + await queryRunner.query( + `ALTER TABLE "lite_chat_message" DROP CONSTRAINT "FK_3e57003d074eb2cf18f58011601"` + ); + await queryRunner.query(`ALTER TABLE "prolific_multiplayer_study" DROP COLUMN "gameType"`); + await queryRunner.query(`DROP TABLE "lite_chat_message"`); + } +} diff --git a/server/src/rooms/pomlite/multiplayer/commands.ts b/server/src/rooms/pomlite/multiplayer/commands.ts index a093e7f14..190d0b5ff 100644 --- a/server/src/rooms/pomlite/multiplayer/commands.ts +++ b/server/src/rooms/pomlite/multiplayer/commands.ts @@ -75,6 +75,7 @@ export class SetGameParamsCmd extends CmdWithoutPayload { defaults.threeEventsThreshold.min, threeEventsThresholdMax ); + this.state.chatEnabled = defaults.chatEnabled || false; this.state.updateVisibleCards(); } } diff --git a/server/src/rooms/pomlite/multiplayer/index.ts b/server/src/rooms/pomlite/multiplayer/index.ts index 6304b5220..ceff0f66e 100644 --- a/server/src/rooms/pomlite/multiplayer/index.ts +++ b/server/src/rooms/pomlite/multiplayer/index.ts @@ -1,11 +1,11 @@ import { Client, Delayed, Room } from "colyseus"; import { Dispatcher } from "@colyseus/command"; import * as http from "http"; -import { LiteGameState } from "@port-of-mars/server/rooms/pomlite/multiplayer/state"; +import { LiteGameState, ChatMessage } from "@port-of-mars/server/rooms/pomlite/multiplayer/state"; import { settings } from "@port-of-mars/server/settings"; import { getServices } from "@port-of-mars/server/services"; import { User } from "@port-of-mars/server/entity"; -import { Invest, LiteGameType, Vote } from "@port-of-mars/shared/lite"; +import { Invest, MultiplayerGameType, Vote } from "@port-of-mars/shared/lite"; import { LitePlayerUser, LiteRoleAssignment, Role } from "@port-of-mars/shared/types"; import { EndGameCmd, InitGameCmd, PlayerInvestCmd, SetFirstRoundCmd } from "./commands"; @@ -68,7 +68,7 @@ export class LiteGameRoom extends Room { }, 1000); } - async onCreate(options: { type?: LiteGameType; users: Array }) { + async onCreate(options: { type?: MultiplayerGameType; users: Array }) { logger.trace("LiteGameRoom '%s' created", this.roomId); const type = options.type || "prolificBaseline"; const userRoles = this.assignRoles(options.users); @@ -143,5 +143,36 @@ export class LiteGameRoom extends Room { }) ); }); + this.onMessage("send-chat-message", (client: Client, message: { message: string }) => { + this.handleChatMessage(client, message.message); + }); + } + + async handleChatMessage(client: Client, messageText: string) { + if (!this.state.chatEnabled) { + return; + } + const user = client.auth as User; + const player = this.state.getPlayer(client); + const chatMessage = new ChatMessage({ + username: user.username, + role: player.role, + message: messageText, + dateCreated: Date.now(), + round: this.state.round, + }); + this.state.chatMessages.push(chatMessage); + // persist to database + const { litegame } = getServices(); + try { + await litegame.createChatMessage( + this.state.gameId, + player.playerId, + messageText, + this.state.round + ); + } catch (error) { + logger.fatal(`Failed to persist chat message: ${error}`); + } } } diff --git a/server/src/rooms/pomlite/multiplayer/state.ts b/server/src/rooms/pomlite/multiplayer/state.ts index f55621c16..4939860a6 100644 --- a/server/src/rooms/pomlite/multiplayer/state.ts +++ b/server/src/rooms/pomlite/multiplayer/state.ts @@ -2,10 +2,11 @@ import { Schema, ArraySchema, type, MapSchema } from "@colyseus/schema"; import { EventCardData, LiteGameParams, - LiteGameType, + MultiplayerGameType, LiteGameStatus, TreatmentData, HiddenParams, + ChatMessageData, } from "@port-of-mars/shared/lite"; import { Role, LiteRoleAssignment } from "@port-of-mars/shared/types"; import { Client } from "colyseus"; @@ -13,6 +14,23 @@ import { settings } from "@port-of-mars/server/settings"; const logger = settings.logging.getLogger(__filename); +export class ChatMessage extends Schema { + @type("string") username = ""; + @type("string") role: Role = "Politician"; + @type("string") message = ""; + @type("number") dateCreated = 0; + @type("number") round = 0; + + constructor(data: ChatMessageData) { + super(); + this.username = data.username; + this.role = data.role; + this.message = data.message; + this.dateCreated = data.dateCreated; + this.round = data.round; + } +} + export class EventCard extends Schema { id = 0; @type("boolean") expired = false; @@ -57,7 +75,7 @@ export class Player extends Schema { } export class TreatmentParams extends Schema { - @type("string") gameType: LiteGameType = "prolificBaseline"; + @type("string") gameType: MultiplayerGameType = "prolificBaseline"; @type("boolean") isNumberOfRoundsKnown = false; @type("boolean") isEventDeckKnown = false; @type("string") thresholdInformation: "unknown" | "range" | "known" = "unknown"; @@ -67,7 +85,7 @@ export class TreatmentParams extends Schema { constructor(data?: TreatmentData) { super(); if (!data) return; - this.gameType = data.gameType as LiteGameType; + this.gameType = data.gameType as MultiplayerGameType; this.isNumberOfRoundsKnown = data.isNumberOfRoundsKnown; this.isEventDeckKnown = data.isEventDeckKnown; this.thresholdInformation = data.thresholdInformation; @@ -78,7 +96,7 @@ export class TreatmentParams extends Schema { export class LiteGameState extends Schema { @type("boolean") isWaitingToStart = true; - @type("string") type: LiteGameType = "prolificBaseline"; + @type("string") type: MultiplayerGameType = "prolificBaseline"; @type("string") status: LiteGameStatus = "incomplete"; @type("int8") systemHealth = LiteGameState.DEFAULTS.freeplay.systemHealthMax - @@ -94,6 +112,9 @@ export class LiteGameState extends Schema { @type([EventCard]) visibleEventCards = new ArraySchema(); @type("int32") activeCardId = -1; // refers to the deckCardId of the active (shown in modal) card + @type([ChatMessage]) chatMessages = new ArraySchema(); + @type("boolean") chatEnabled = false; + @type("boolean") canInvest = false; @type("boolean") isRoundTransitioning = false; @@ -108,7 +129,7 @@ export class LiteGameState extends Schema { // this one doesn't have to be a schema property since visibleEventCards already mirrors it eventCardDeck: Array = []; - constructor(data: { userRoles: LiteRoleAssignment; type: LiteGameType }) { + constructor(data: { userRoles: LiteRoleAssignment; type: MultiplayerGameType }) { super(); this.type = data.type; this.numPlayers = this.defaultParams.numPlayers || 3; @@ -217,7 +238,7 @@ export class LiteGameState extends Schema { return LiteGameState.DEFAULTS[this.type]; } - static DEFAULTS: Record = { + static DEFAULTS: Record = { // unused for now freeplay: { numPlayers: 3, @@ -233,6 +254,7 @@ export class LiteGameState extends Schema { points: 0, resources: 10, availableRoles: ["Politician", "Entrepreneur", "Researcher"], + chatEnabled: false, }, prolificBaseline: { numPlayers: 3, @@ -249,6 +271,7 @@ export class LiteGameState extends Schema { points: 0, resources: 10, availableRoles: ["Politician", "Entrepreneur", "Researcher"], + chatEnabled: false, }, prolificVariable: { numPlayers: 3, @@ -266,6 +289,23 @@ export class LiteGameState extends Schema { points: 0, resources: 10, availableRoles: ["Politician", "Entrepreneur", "Researcher"], + chatEnabled: false, + }, + prolificInteractive: { + numPlayers: 3, + maxRound: { min: 8, max: 12 }, + roundTransitionDuration: 3, + twoEventsThreshold: { min: 39, max: 39 }, // full game is 13 * numplayers + threeEventsThreshold: { min: 21, max: 21 }, // full game is 7 * numplayers + timeRemaining: 45, + eventTimeout: 15, + systemHealthMax: 60, // 3 * 20 matches the full game + systemHealthWear: 15, // 3 * 5 matches the full game + startingSystemHealth: 45, // (3 * 20) - (3 * 5) + points: 0, + resources: 10, + availableRoles: ["Politician", "Entrepreneur", "Researcher"], + chatEnabled: true, }, }; } diff --git a/server/src/rooms/pomlite/solo/index.ts b/server/src/rooms/pomlite/solo/index.ts index 498b43f26..a64d71f41 100644 --- a/server/src/rooms/pomlite/solo/index.ts +++ b/server/src/rooms/pomlite/solo/index.ts @@ -6,7 +6,7 @@ import { settings } from "@port-of-mars/server/settings"; import { getServices } from "@port-of-mars/server/services"; import { ApplyCardCmd, InitGameCmd, InvestCmd } from "./commands"; import { User } from "@port-of-mars/server/entity"; -import { EventContinue, Invest, SOLO_ROOM_NAME, LiteGameType } from "@port-of-mars/shared/lite"; +import { EventContinue, Invest, SOLO_ROOM_NAME, SoloGameType } from "@port-of-mars/shared/lite"; const logger = settings.logging.getLogger(__filename); @@ -25,7 +25,7 @@ export class SoloGameRoom extends Room { return this.clients[0]; } - onCreate(options: { type?: LiteGameType }) { + onCreate(options: { type?: SoloGameType }) { logger.trace("SoloGameRoom '%s' created", this.roomId); this.setState(new SoloGameState()); this.state.type = options.type || "freeplay"; diff --git a/server/src/rooms/pomlite/solo/state.ts b/server/src/rooms/pomlite/solo/state.ts index 001709096..cdaeb555c 100644 --- a/server/src/rooms/pomlite/solo/state.ts +++ b/server/src/rooms/pomlite/solo/state.ts @@ -3,7 +3,7 @@ import { EventCardData, LiteGameParams, LiteGameStatus, - LiteGameType, + SoloGameType, TreatmentData, } from "@port-of-mars/shared/lite"; @@ -45,7 +45,7 @@ export class Player extends Schema { } export class TreatmentParams extends Schema { - @type("string") gameType: LiteGameType = "freeplay"; + @type("string") gameType: SoloGameType = "freeplay"; @type("boolean") isNumberOfRoundsKnown = false; @type("boolean") isEventDeckKnown = false; @type("string") thresholdInformation: "unknown" | "range" | "known" = "unknown"; @@ -54,7 +54,7 @@ export class TreatmentParams extends Schema { constructor(data?: TreatmentData) { super(); if (!data) return; - this.gameType = data.gameType; + this.gameType = data.gameType as SoloGameType; this.isNumberOfRoundsKnown = data.isNumberOfRoundsKnown; this.isEventDeckKnown = data.isEventDeckKnown; this.thresholdInformation = data.thresholdInformation; @@ -63,7 +63,7 @@ export class TreatmentParams extends Schema { } export class SoloGameState extends Schema { - @type("string") type: LiteGameType = "freeplay"; + @type("string") type: SoloGameType = "freeplay"; @type("string") status: LiteGameStatus = "incomplete"; @type("int8") systemHealth = SoloGameState.DEFAULTS.freeplay.systemHealthMax - @@ -147,7 +147,7 @@ export class SoloGameState extends Schema { resources: 10, }; - static DEFAULTS: Record = { + static DEFAULTS: Record = { freeplay: { maxRound: { min: 6, max: 14 }, roundTransitionDuration: 3, diff --git a/server/src/services/litegame.ts b/server/src/services/litegame.ts index 189ebc38f..a00738d58 100644 --- a/server/src/services/litegame.ts +++ b/server/src/services/litegame.ts @@ -14,6 +14,7 @@ import { LiteMarsEventDeckCard, LitePlayer, LitePlayerDecision, + LiteChatMessage, SoloGame, SoloGameRound, SoloGameTreatment, @@ -587,6 +588,23 @@ export class LiteGameService extends BaseService { } } + async createChatMessage( + gameId: number, + playerId: number, + message: string, + round: number + ): Promise { + const chatMessageRepo = this.em.getRepository(LiteChatMessage); + const chatMessage = chatMessageRepo.create({ + gameId, + playerId, + message, + round, + dateCreated: new Date(), + }); + return await chatMessageRepo.save(chatMessage); + } + async exportEventCardsCsv(path: string, gameIds?: Array) { /** * export a flat csv of all event cards drawn in past multiplayer games specified by gameIds diff --git a/server/src/services/study.ts b/server/src/services/study.ts index fb6f9a664..c606cecc9 100644 --- a/server/src/services/study.ts +++ b/server/src/services/study.ts @@ -493,6 +493,7 @@ export class MultiplayerStudyService extends BaseStudyService { "prolificVariablePlayer", "prolificBaselinePlayer.game", "prolificVariablePlayer.game", + "study", ], }); } catch (e) { @@ -505,7 +506,10 @@ export class MultiplayerStudyService extends BaseStudyService { } if (!participant.prolificBaselinePlayer) { - return { status: "not-started" }; + return { + status: "not-started", + startingGameType: participant.study.gameType, + }; } const basePlayer = participant.prolificBaselinePlayer; @@ -547,6 +551,7 @@ export class MultiplayerStudyService extends BaseStudyService { } return { + startingGameType: participant.study.gameType, status, // in-progress users get their game type ...(status === "in-progress" && { inProgressGameType }), diff --git a/shared/src/lite/requests.ts b/shared/src/lite/requests.ts index 08dc4dd74..6c928b719 100644 --- a/shared/src/lite/requests.ts +++ b/shared/src/lite/requests.ts @@ -22,4 +22,9 @@ export interface Vote { binaryVote: boolean; } -export type LiteGameRequest = EventContinue | Invest | PlayerReady; +export interface SendChatMessage { + kind: "send-chat-message"; + message: string; +} + +export type LiteGameRequest = EventContinue | Invest | PlayerReady | SendChatMessage; diff --git a/shared/src/lite/types.ts b/shared/src/lite/types.ts index c93bc5bb2..bf2464ef6 100644 --- a/shared/src/lite/types.ts +++ b/shared/src/lite/types.ts @@ -34,7 +34,15 @@ export interface TreatmentData { instructions?: string; } -export type LiteGameType = "freeplay" | "prolificBaseline" | "prolificVariable"; +export type SoloGameType = "freeplay" | "prolificBaseline" | "prolificVariable"; + +export type MultiplayerGameType = + | "freeplay" + | "prolificBaseline" + | "prolificVariable" + | "prolificInteractive"; + +export type LiteGameType = SoloGameType | MultiplayerGameType; export type LiteGameStatus = "incomplete" | "victory" | "defeat"; @@ -57,6 +65,7 @@ export interface LiteGameParams { points: number; resources: number; availableRoles?: Array; + chatEnabled?: boolean; } export interface SoloGameClientState { @@ -98,9 +107,20 @@ export interface LiteGamePlayerClientState { isReadyToStart: boolean; } +export interface ChatMessageData { + id?: number; + username: string; + role: Role; + message: string; + dateCreated: number; + round: number; +} + export interface LiteGameClientState extends SoloGameClientState { players: Map; player: LiteGamePlayerClientState; numPlayers: number; isWaitingToStart: boolean; + chatMessages: ChatMessageData[]; + chatEnabled: boolean; } diff --git a/shared/src/types.ts b/shared/src/types.ts index e8bdddbdf..d5f532240 100644 --- a/shared/src/types.ts +++ b/shared/src/types.ts @@ -537,6 +537,7 @@ export interface ProlificSoloParticipantStatus { } export interface ProlificMultiplayerParticipantStatus { + startingGameType: LiteGameType; status: "not-started" | "in-progress" | "completed"; completionUrl?: string; inProgressGameType?: LiteGameType | null; From 2b721afead87c88d58edb2d6c7b4a8c3d4e95bb8 Mon Sep 17 00:00:00 2001 From: sgfost Date: Thu, 17 Jul 2025 16:38:32 -0700 Subject: [PATCH 02/26] feat: configurable game type per study --- client/src/components/admin/StudiesList.vue | 21 +++++++++++++++++++ client/src/views/admin/Studies.vue | 4 ++-- server/src/entity/ProlificStudy.ts | 6 +++--- ...52794083755-AddLiteProlificInteractive.ts} | 8 +++++-- server/src/routes/study.ts | 6 ++++-- server/src/services/study.ts | 5 ++++- shared/src/types.ts | 1 + 7 files changed, 41 insertions(+), 10 deletions(-) rename server/src/migration/{1752790734574-AddLiteProlificInteractive.ts => 1752794083755-AddLiteProlificInteractive.ts} (81%) diff --git a/client/src/components/admin/StudiesList.vue b/client/src/components/admin/StudiesList.vue index f832a9d9c..ba379c913 100644 --- a/client/src/components/admin/StudiesList.vue +++ b/client/src/components/admin/StudiesList.vue @@ -76,6 +76,14 @@ placeholder="The completion code from Prolific" > + + + Variable (uncertainty treatments)" }, + { value: "prolificInteractive", text: "Interactive (chat, interactive events, etc.)" }, + ]; + get addStudyModalId() { return `add-study-modal-${this.mode}`; } @@ -191,11 +205,18 @@ export default class StudiesList extends Vue { async created() { this.studyApi = new StudyAPI(this.$tstore, this.$ajax, this.mode); + if (this.mode === "multiplayer") { + this.studyFields.splice(this.studyFields.length - 1, 0, { + key: "gameType", + label: "Game Type", + }); + } await this.loadProlificStudies(); } resetStudy(): ProlificStudyData { return { + gameType: "prolificBaseline", studyId: "", description: "", completionCode: "", diff --git a/client/src/views/admin/Studies.vue b/client/src/views/admin/Studies.vue index f6a619455..01a522c4c 100644 --- a/client/src/views/admin/Studies.vue +++ b/client/src/views/admin/Studies.vue @@ -1,10 +1,10 @@