@m2c2kit/cli 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,295 +1,377 @@
1
- import {
2
- WebColors,
3
- Action,
4
- Game,
5
- Scene,
6
- Sprite,
7
- Point,
8
- Label,
9
- LabelHorizontalAlignmentMode,
10
- Shape,
11
- Rect,
12
- GameData,
13
- } from "@m2c2kit/core";
14
-
15
- import { Button, Instructions } from "@m2c2kit/addons";
16
-
17
- const game = new Game();
18
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- (window as unknown as any).game = game;
20
-
21
- // game parameter defaults to be used if values are not provided
22
- // default parameters are not part of the m2c2kit engine, because parameters
23
- // are different for each game that might be written. Thus, define them here
24
- const defaults = {
25
- ReadyTime: 1000,
26
- TrialNum: 3,
27
- };
28
-
29
- // trial data variables names and types must be defined
30
- // valid types are "number", "string", "boolean", and "object"
31
- const demoTrialSchema = {
32
- correct: "boolean",
33
- };
34
-
35
- game
36
- .init({
37
- showFps: true,
38
- // set this color so we can see the boundaries of the game
39
- bodyBackgroundColor: WebColors.Wheat,
40
- // note: using 2:1 aspect ratio, because that is closer to modern phones
41
- width: 400,
42
- height: 800,
43
- defaultParameters: defaults,
44
- trialSchema: demoTrialSchema,
45
- // set stretch to true if you want to fill the screen
46
- stretch: false,
47
- // Roboto is included by default. Leave fontUrls unchanged, unless you want to use different font
48
- fontUrls: ["./fonts/roboto/Roboto-Regular.ttf"],
49
- // each svgImage below, you specify either a tag of an svg in the property "svgString" (as I do below)
50
- // or you specify a URL to an svg in the property "url", such as url: 'https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/android.svg'
51
- svgImages: [
52
- {
53
- name: "star",
54
- height: 60,
55
- width: 60,
56
- svgString:
57
- '<svg xmlns="http://www.w3.org/2000/svg" width="304" height="290"> <path d="M2,111 h300 l-242.7,176.3 92.7,-285.3 92.7,285.3 z" style="fill:#00FF00;stroke:#0000FF;stroke-width:15;stroke-linejoin:round"/></svg>',
58
- },
59
- ],
60
- })
61
- .then(() => {
62
- // callback provided to onTrialComplete is called when each trial is done
63
- game.onTrialComplete(
64
- (trialNumber: number, data: GameData, trialSchema: object) => {
65
- console.log(`Trial number ${trialNumber} completed.`);
66
- console.log(`Current Game data is: ${JSON.stringify(data)}`);
67
- console.log(`Trial schema is: ${JSON.stringify(trialSchema)}`);
68
- }
69
- );
70
-
71
- // callback provided to onAllTrialsComplete is called when all game trials are done
72
- game.onAllTrialsComplete((data: GameData, trialSchema: object) => {
73
- console.log(`All trials completed.`);
74
- console.log(`Complete Game data is: ${JSON.stringify(data)}`);
75
- console.log(`Trial schema is: ${JSON.stringify(trialSchema)}`);
76
- });
77
-
78
- const instructionsScenes = Instructions.Create({
79
- sceneNamePrefix: "instructions",
80
- backgroundColor: WebColors.White,
81
- nextButtonBackgroundColor: WebColors.Black,
82
- backButtonBackgroundColor: WebColors.Black,
83
- instructionScenes: [
84
- {
85
- title: "{{appName}} Demo",
86
- text: "For this activity, you will click the red rectangle. Open the developer console to see the trial data.",
87
- textFontSize: 24,
88
- titleFontSize: 30,
89
- },
90
- {
91
- title: "{{appName}} Demo",
92
- text: "Press START to begin!",
93
- textFontSize: 24,
94
- titleFontSize: 30,
95
- textAlignmentMode: LabelHorizontalAlignmentMode.center,
96
- nextButtonText: "START",
97
- nextButtonBackgroundColor: WebColors.Green,
98
- },
99
- ],
100
- // this is the scene to go to after the last instructions scene
101
- postInstructionsScene: "getReadyScene",
102
- });
103
- game.addScenes(instructionsScenes);
104
-
105
- // start at 0
106
- game.trialNumber = 0;
107
-
108
- const demoPage0 = new Scene({
109
- // Because this is the scene after the instructions, we must give
110
- // it a name and provide it to postInstructionsScene, above
111
- name: "getReadyScene",
112
- backgroundColor: WebColors.White,
113
- });
114
- game.addScene(demoPage0);
115
-
116
- const getReadyMessage = new Label({
117
- text: "Get Ready",
118
- fontSize: 24,
119
- position: new Point(200, 400),
120
- });
121
- demoPage0.addChild(getReadyMessage);
122
-
123
- // example of how to use an image. The image must be previously loaded
124
- const starSprite = new Sprite({
125
- imageName: "star",
126
- position: new Point(200, 500),
127
- });
128
- demoPage0.addChild(starSprite);
129
-
130
- // demoPage0.setup() has a callback that is executed each time this scene is shown
131
- demoPage0.setup(() => {
132
- demoPage0.run(
133
- Action.Sequence([
134
- // Get the wait duration from the default game parameters, defined above
135
- Action.Wait({ duration: game.getParameter("ReadyTime") }),
136
- Action.Custom({
137
- callback: () => {
138
- game.presentScene(demoPage1);
139
- },
140
- }),
141
- ])
142
- );
143
- });
144
-
145
- // these entities (demoPage1 through wrongMessage) can be defined outside of a setup()
146
- // because they exist through multiple trials
147
- // Their position and how they respond to interactions may differ across trials,
148
- // and that logic will be written within a setup()
149
- const demoPage1 = new Scene({ backgroundColor: WebColors.LightGray });
150
- game.addScene(demoPage1);
151
- const redRect = new Shape({
152
- rect: new Rect({ width: 150, height: 100 }),
153
- fillColor: WebColors.Red,
154
- });
155
- demoPage1.addChild(redRect);
156
- const blueRect = new Shape({
157
- rect: new Rect({ width: 150, height: 100 }),
158
- fillColor: WebColors.Blue,
159
- });
160
- demoPage1.addChild(blueRect);
161
- const correctMessage = new Label({
162
- text: "CORRECT!",
163
- position: new Point(200, 500),
164
- });
165
- correctMessage.hidden = true;
166
- demoPage1.addChild(correctMessage);
167
- const wrongMessage = new Label({
168
- text: "WRONG!",
169
- position: new Point(200, 500),
170
- });
171
- wrongMessage.hidden = true;
172
- demoPage1.addChild(wrongMessage);
173
-
174
- // demoPage1.setup() has a callback that is executed each time this scene is shown
175
- // we will randomly decide on what side the red rectangle is shown
176
- demoPage1.setup(() => {
177
- let redOnLeft = true;
178
- if (Math.random() > 0.5) {
179
- redOnLeft = false;
180
- }
181
-
182
- if (redOnLeft) {
183
- redRect.position = new Point(100, 300);
184
- blueRect.position = new Point(300, 300);
185
- } else {
186
- redRect.position = new Point(300, 300);
187
- blueRect.position = new Point(100, 300);
188
- }
189
-
190
- // helper function to record the user's choice and
191
- // decide if we are done
192
- function recordUserInput(choseRedRect: boolean) {
193
- game.addTrialData("correct", choseRedRect);
194
- game.lifecycle.trialComplete(
195
- game.trialNumber,
196
- game.data,
197
- game.trialSchema
198
- );
199
- game.trialNumber++;
200
- // have we finished the number of trials?
201
- if (game.trialNumber < game.getParameter<number>("TrialNum")) {
202
- game.presentScene(demoPage0);
203
- } else {
204
- game.presentScene(endPage);
205
- }
206
- }
207
-
208
- redRect.isUserInteractionEnabled = true;
209
- redRect.onTap(() => {
210
- redRect.run(
211
- Action.Sequence([
212
- Action.Custom({
213
- callback: () => {
214
- // once a choice is made, don't allow additional taps
215
- redRect.isUserInteractionEnabled = false;
216
- blueRect.isUserInteractionEnabled = false;
217
- },
218
- }),
219
- // short animation to shrink and grow button
220
- Action.Scale({ scale: 0.8, duration: 250 }),
221
- Action.Scale({ scale: 1, duration: 250 }),
222
- // Show the "CORRECT" message
223
- Action.Custom({
224
- callback: () => {
225
- correctMessage.hidden = false;
226
- },
227
- }),
228
- Action.Wait({ duration: 1000 }),
229
- // Handle the logic of recording data, and deciding if we should
230
- // do another trial in recordUserInput
231
- Action.Custom({
232
- callback: () => {
233
- correctMessage.hidden = true;
234
- recordUserInput(true);
235
- },
236
- }),
237
- ])
238
- );
239
- });
240
-
241
- blueRect.isUserInteractionEnabled = true;
242
- blueRect.onTap(() => {
243
- blueRect.run(
244
- Action.Sequence([
245
- Action.Custom({
246
- callback: () => {
247
- redRect.isUserInteractionEnabled = false;
248
- blueRect.isUserInteractionEnabled = false;
249
- },
250
- }),
251
- Action.Scale({ scale: 0.8, duration: 250 }),
252
- Action.Scale({ scale: 1, duration: 250 }),
253
- Action.Custom({
254
- callback: () => {
255
- wrongMessage.hidden = false;
256
- },
257
- }),
258
- Action.Wait({ duration: 1000 }),
259
- Action.Custom({
260
- callback: () => {
261
- wrongMessage.hidden = true;
262
- recordUserInput(false);
263
- },
264
- }),
265
- ])
266
- );
267
- });
268
- });
269
-
270
- const endPage = new Scene();
271
- game.addScene(endPage);
272
- const doneLabel = new Label({
273
- text: `This will be reassigned in the setup() callback. If you see this, something went wrong!`,
274
- position: new Point(200, 300),
275
- });
276
- endPage.addChild(doneLabel);
277
-
278
- const againButton = new Button({
279
- text: "Start over",
280
- position: new Point(200, 400),
281
- });
282
- againButton.isUserInteractionEnabled = true;
283
- againButton.onTap(() => {
284
- game.initData(game.trialSchema);
285
- game.presentScene(demoPage0);
286
- });
287
- endPage.addChild(againButton);
288
-
289
- endPage.setup(() => {
290
- doneLabel.text = `You did ${game.trialNumber} trials. You're done!`;
291
- game.lifecycle.allTrialsComplete(game.data, game.trialSchema);
292
- });
293
-
294
- game.start("instructions-01");
295
- });
1
+ import {
2
+ WebColors,
3
+ Action,
4
+ Game,
5
+ Scene,
6
+ Sprite,
7
+ Point,
8
+ Label,
9
+ LabelHorizontalAlignmentMode,
10
+ Shape,
11
+ Rect,
12
+ GameOptions,
13
+ GameParameters,
14
+ TrialSchema,
15
+ Session,
16
+ GameTrialEvent,
17
+ GameLifecycleEvent,
18
+ } from "@m2c2kit/core";
19
+ import { Button, Instructions } from "@m2c2kit/addons";
20
+
21
+ class {{className}} extends Game {
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ constructor(specifiedParameters?: any) {
24
+ const defaultParameters: GameParameters = {
25
+ ReadyTime: {
26
+ value: 1000,
27
+ description:
28
+ "How long the 'get ready' message scene is shown, milliseconds",
29
+ },
30
+ TrialNum: { value: 3, description: "How many trials to run" },
31
+ };
32
+
33
+ const demoTrialSchema: TrialSchema = {
34
+ colorChosen: { type: "string", description: "the color that was picked" },
35
+ correct: { type: "boolean", description: "was the answer correct?" },
36
+ };
37
+
38
+ const options: GameOptions = {
39
+ name: "{{appName}}",
40
+ version: "1.0.0",
41
+ uri: "https://your-repo-or-webpage-here",
42
+ shortDescription: "A brief couple sentence description.",
43
+ longDescription: "An extended, many-sentence description.",
44
+ showFps: true,
45
+ trialSchema: demoTrialSchema,
46
+ parameters: defaultParameters,
47
+ // set this color so we can see the boundaries of the game during development,
48
+ // but typically we would not set this
49
+ bodyBackgroundColor: WebColors.Wheat,
50
+ // note: using 2:1 aspect ratio, because that is closer to modern phones
51
+ width: 400,
52
+ height: 800,
53
+ // set stretch to true if you want to fill the screen on large windows
54
+ // (e.g., iPad, desktop browser)
55
+ stretch: false,
56
+ // Roboto is included by default. Leave fontUrls unchanged, unless you want to use different font
57
+ fontUrls: ["./fonts/roboto/Roboto-Regular.ttf"],
58
+ // for each image below, you specify either a tag of an svg in the property "svgString"
59
+ // or you specify a URL to an image in the property "url",
60
+ // such as url: 'https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/android.svg'
61
+ images: [
62
+ {
63
+ name: "star",
64
+ height: 60,
65
+ width: 60,
66
+ svgString:
67
+ '<svg xmlns="http://www.w3.org/2000/svg" width="304" height="290"> <path d="M2,111 h300 l-242.7,176.3 92.7,-285.3 92.7,285.3 z" style="fill:#00FF00;stroke:#0000FF;stroke-width:15;stroke-linejoin:round"/></svg>',
68
+ },
69
+ {
70
+ name: "smiley",
71
+ height: 64,
72
+ width: 64,
73
+ svgString:
74
+ '<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="451.451 -28.549 1657.098 1657.098" enable-background="new 451.451 -28.549 1657.098 1657.098" xml:space="preserve"><g><g><circle cx="992.682" cy="640.47" r="66.818"/></g><g><circle cx="1558.728" cy="640.47" r="66.818"/></g><g><path d="M1280,1600c-107.984,0-212.756-21.157-311.407-62.883c-95.268-40.295-180.819-97.973-254.279-171.432c-73.459-73.459-131.137-159.01-171.432-254.278C501.157,1012.757,480,907.984,480,800s21.157-212.756,62.883-311.407c40.295-95.268,97.973-180.819,171.432-254.279c73.46-73.459,159.011-131.137,254.279-171.432C1067.244,21.157,1172.016,0,1280,0s212.757,21.157,311.407,62.883c95.268,40.295,180.819,97.973,254.278,171.432c73.459,73.46,131.137,159.011,171.432,254.279C2058.843,587.244,2080,692.016,2080,800s-21.157,212.757-62.883,311.407c-40.295,95.268-97.973,180.819-171.432,254.278s-159.01,131.137-254.278,171.432C1492.757,1578.843,1387.984,1600,1280,1600z M1280,71.591c-401.646,0-728.409,326.763-728.409,728.409S878.354,1528.409,1280,1528.409S2008.409,1201.646,2008.409,800C2008.409,398.354,1681.646,71.591,1280,71.591z"/></g><g><path d="M1665.953,1071.004c-213.156,213.156-558.75,213.156-771.905,0c20.534-20.534,41.068-41.068,61.602-61.602c179.134,179.134,469.567,179.134,648.7,0C1624.884,1029.936,1645.418,1050.47,1665.953,1071.004z"/></g><g><path d="M1167.066,463.279c-96.31-96.31-252.459-96.31-348.768,0c16.971,16.971,33.941,33.941,50.912,50.912c68.192-68.192,178.753-68.192,246.945,0C1133.125,497.22,1150.096,480.249,1167.066,463.279z"/></g><g><path d="M1733.112,463.279c-96.31-96.31-252.459-96.31-348.768,0c16.971,16.971,33.941,33.941,50.912,50.912c68.192-68.192,178.753-68.192,246.945,0C1699.171,497.22,1716.142,480.249,1733.112,463.279z"/></g></g></svg>',
75
+ },
76
+ ],
77
+ };
78
+
79
+ super(options, specifiedParameters);
80
+ // just for convenience, alias the variable game to "this"
81
+ // (even though eslint doesn't like it)
82
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
83
+ const game = this;
84
+
85
+ const instructionsScenes = Instructions.Create({
86
+ sceneNamePrefix: "instructions",
87
+ backgroundColor: WebColors.White,
88
+ nextButtonBackgroundColor: WebColors.Black,
89
+ backButtonBackgroundColor: WebColors.Black,
90
+ instructionScenes: [
91
+ {
92
+ title: "{{appName}} Demo",
93
+ text: "For this task, you will click the red rectangle. Open the developer console to see the trial data and other output.",
94
+ textFontSize: 24,
95
+ titleFontSize: 30,
96
+ image: "smiley",
97
+ },
98
+ {
99
+ title: "{{appName}} Demo",
100
+ text: "Press START to begin!",
101
+ textFontSize: 24,
102
+ titleFontSize: 30,
103
+ textAlignmentMode: LabelHorizontalAlignmentMode.center,
104
+ textVerticalBias: 0.25,
105
+ nextButtonText: "START",
106
+ nextButtonBackgroundColor: WebColors.Green,
107
+ },
108
+ ],
109
+ // this is the name of the scene to go to after the last instructions scene
110
+ postInstructionsScene: "getReady",
111
+ });
112
+ game.addScenes(instructionsScenes);
113
+
114
+ const getReadyScene = new Scene({
115
+ // Because this is the scene after the instructions, we must give
116
+ // it a name and provide it to postInstructionsScene, above
117
+ name: "getReady",
118
+ backgroundColor: WebColors.White,
119
+ });
120
+ game.addScene(getReadyScene);
121
+
122
+ const getReadyMessage = new Label({
123
+ text: "Get Ready",
124
+ fontSize: 24,
125
+ position: new Point(200, 400),
126
+ });
127
+ getReadyScene.addChild(getReadyMessage);
128
+
129
+ // example of how to use an image. The image must be previously loaded
130
+ const starSprite = new Sprite({
131
+ imageName: "star",
132
+ position: new Point(200, 500),
133
+ });
134
+ getReadyScene.addChild(starSprite);
135
+
136
+ // getReadyScene.setup() has a callback that is executed each time this scene is shown
137
+ getReadyScene.setup(() => {
138
+ getReadyScene.run(
139
+ Action.Sequence([
140
+ // Get the wait duration from the default game parameters, defined above
141
+ Action.Wait({ duration: game.getParameter("ReadyTime") }),
142
+ Action.Custom({
143
+ callback: () => {
144
+ game.presentScene(chooseRectangleScene);
145
+ },
146
+ }),
147
+ ])
148
+ );
149
+ });
150
+
151
+ // these entities before the setup() can be defined outside of a setup()
152
+ // because they exist through multiple trials
153
+ // Their position and how they respond to interactions may differ across trials,
154
+ // and that logic will be written within a setup()
155
+ const chooseRectangleScene = new Scene({
156
+ backgroundColor: WebColors.LightGray,
157
+ });
158
+ game.addScene(chooseRectangleScene);
159
+ const redRect = new Shape({
160
+ rect: new Rect({ width: 150, height: 100 }),
161
+ fillColor: WebColors.Red,
162
+ });
163
+ chooseRectangleScene.addChild(redRect);
164
+ const blueRect = new Shape({
165
+ rect: new Rect({ width: 150, height: 100 }),
166
+ fillColor: WebColors.Blue,
167
+ });
168
+ chooseRectangleScene.addChild(blueRect);
169
+ const correctMessage = new Label({
170
+ text: "CORRECT!",
171
+ position: new Point(200, 500),
172
+ });
173
+
174
+ const chooseMessage = new Label({
175
+ text: "Choose the red rectangle",
176
+ position: new Point(200, 200),
177
+ });
178
+ chooseRectangleScene.addChild(chooseMessage);
179
+
180
+ correctMessage.hidden = true;
181
+ chooseRectangleScene.addChild(correctMessage);
182
+ const wrongMessage = new Label({
183
+ text: "WRONG!",
184
+ position: new Point(200, 500),
185
+ });
186
+
187
+ wrongMessage.hidden = true;
188
+ chooseRectangleScene.addChild(wrongMessage);
189
+
190
+ // chooseRectangleScene.setup() is passed a callback that is executed each
191
+ // time this scene is shown. Within setup(), We will randomly decide on
192
+ // what side the red rectangle is shown
193
+ chooseRectangleScene.setup(() => {
194
+ let redOnLeft = true;
195
+ if (Math.random() > 0.5) {
196
+ redOnLeft = false;
197
+ }
198
+
199
+ if (redOnLeft) {
200
+ redRect.position = new Point(100, 300);
201
+ blueRect.position = new Point(300, 300);
202
+ } else {
203
+ redRect.position = new Point(300, 300);
204
+ blueRect.position = new Point(100, 300);
205
+ }
206
+
207
+ // helper function to record the user's choice and
208
+ // decide if we are done
209
+ function recordUserInput(choseRedRect: boolean) {
210
+ game.addTrialData("correct", choseRedRect);
211
+ if (choseRedRect) {
212
+ game.addTrialData("colorChosen", "red");
213
+ } else {
214
+ game.addTrialData("colorChosen", "blue");
215
+ }
216
+ game.trialComplete();
217
+ // have we finished the number of trials?
218
+ if (game.trialIndex < game.getParameter<number>("TrialNum")) {
219
+ game.presentScene(getReadyScene);
220
+ } else {
221
+ game.presentScene(endScene);
222
+ }
223
+ }
224
+
225
+ redRect.isUserInteractionEnabled = true;
226
+ redRect.onTapDown(() => {
227
+ redRect.run(
228
+ Action.Sequence([
229
+ Action.Custom({
230
+ callback: () => {
231
+ // once a choice is made, don't allow additional taps
232
+ redRect.isUserInteractionEnabled = false;
233
+ blueRect.isUserInteractionEnabled = false;
234
+ },
235
+ }),
236
+ // short animation to shrink and grow button
237
+ Action.Scale({ scale: 0.8, duration: 250 }),
238
+ Action.Scale({ scale: 1, duration: 250 }),
239
+ // Show the "CORRECT" message
240
+ Action.Custom({
241
+ callback: () => {
242
+ correctMessage.hidden = false;
243
+ },
244
+ }),
245
+ Action.Wait({ duration: 1000 }),
246
+ // Handle the logic of recording data, and deciding if we should
247
+ // do another trial in recordUserInput
248
+ Action.Custom({
249
+ callback: () => {
250
+ correctMessage.hidden = true;
251
+ recordUserInput(true);
252
+ },
253
+ }),
254
+ ])
255
+ );
256
+ });
257
+
258
+ blueRect.isUserInteractionEnabled = true;
259
+ blueRect.onTapDown(() => {
260
+ blueRect.run(
261
+ Action.Sequence([
262
+ Action.Custom({
263
+ callback: () => {
264
+ redRect.isUserInteractionEnabled = false;
265
+ blueRect.isUserInteractionEnabled = false;
266
+ },
267
+ }),
268
+ Action.Scale({ scale: 0.8, duration: 250 }),
269
+ Action.Scale({ scale: 1, duration: 250 }),
270
+ Action.Custom({
271
+ callback: () => {
272
+ wrongMessage.hidden = false;
273
+ },
274
+ }),
275
+ Action.Wait({ duration: 1000 }),
276
+ Action.Custom({
277
+ callback: () => {
278
+ wrongMessage.hidden = true;
279
+ recordUserInput(false);
280
+ },
281
+ }),
282
+ ])
283
+ );
284
+ });
285
+ });
286
+
287
+ const endScene = new Scene();
288
+ game.addScene(endScene);
289
+ const doneLabel = new Label({
290
+ text: `This will be reassigned in the setup() callback. If you see this, something went wrong!`,
291
+ position: new Point(200, 300),
292
+ });
293
+ endScene.addChild(doneLabel);
294
+
295
+ const startOverButton = new Button({
296
+ text: "Start over",
297
+ position: new Point(200, 600),
298
+ });
299
+ startOverButton.isUserInteractionEnabled = true;
300
+ startOverButton.onTapDown(() => {
301
+ // in the setup() for the end scene, we animate the smiley sprite with
302
+ // a move action. if the user taps the start over button before the
303
+ // animation is completed, we should remove it by calling
304
+ // removeAllActions()
305
+ smileySprite.removeAllActions();
306
+ game.initData();
307
+ game.presentScene(getReadyScene);
308
+ });
309
+ endScene.addChild(startOverButton);
310
+
311
+ const exitButton = new Button({
312
+ text: "Exit",
313
+ position: new Point(200, 675),
314
+ });
315
+ exitButton.isUserInteractionEnabled = true;
316
+ exitButton.onTapDown(() => {
317
+ // hide the start over button
318
+ startOverButton.hidden = true;
319
+ // don't allow repeated taps on exit button
320
+ exitButton.isUserInteractionEnabled = false;
321
+ game.end();
322
+ });
323
+ endScene.addChild(exitButton);
324
+
325
+ const smileySprite = new Sprite({ imageName: "smiley" });
326
+ endScene.addChild(smileySprite);
327
+
328
+ endScene.setup(() => {
329
+ doneLabel.text = `You did ${game.trialIndex} trials. You're done!`;
330
+
331
+ // example of how to position a sprite and create an action to move it
332
+ smileySprite.position = new Point(200, 500);
333
+ smileySprite.run(
334
+ Action.Move({ point: new Point(200, 100), duration: 3000 })
335
+ );
336
+ });
337
+
338
+ game.entryScene = "instructions-01";
339
+ }
340
+ }
341
+
342
+ // default was 3 trials; this is how we can specify a different value
343
+ const game1 = new {{className}}({ TrialNum: 2 });
344
+
345
+ const session = new Session({
346
+ activities: [game1],
347
+ gameCallbacks: {
348
+ // onGameTrialComplete() is where you insert code to post data to an API
349
+ // or interop with a native function in the host app, if applicable
350
+ onGameTrialComplete: (e: GameTrialEvent) => {
351
+ console.log(`********** trial ${e.trialIndex} complete`);
352
+ console.log("data: " + JSON.stringify(e.gameData));
353
+ console.log("trial schema: " + JSON.stringify(e.trialSchema));
354
+ console.log("game parameters: " + JSON.stringify(e.gameParameters));
355
+ },
356
+ // onGameEnd() is called when the user is done
357
+ onGameEnd: (e: GameLifecycleEvent) => {
358
+ if (e.ended) {
359
+ console.log(`user requested exit in game ${e.gameName}`);
360
+ // this session has only one activity, but this is how it would go to
361
+ // the next activity
362
+ if (session.nextActivity) {
363
+ session.advanceToNextActivity();
364
+ }
365
+ }
366
+ },
367
+ },
368
+ });
369
+
370
+ // make session also available on window in case we want to control
371
+ // the session through another means
372
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
373
+ (window as unknown as any).session = session;
374
+
375
+ session.init().then(() => {
376
+ session.start();
377
+ });