@m2c2kit/cli 0.1.9 → 0.1.12

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.
package/dist/.env CHANGED
@@ -1 +1 @@
1
- CLI_VERSION=0.1.9
1
+ CLI_VERSION=0.1.12
package/dist/cli.js CHANGED
@@ -146,6 +146,10 @@ await yarg
146
146
  sourceFilePath: path.join(packageHomeFolderPath, "fonts", "roboto", "Roboto-Regular.ttf"),
147
147
  destinationFilePath: path.join(newFolderPath, "fonts", "roboto", "Roboto-Regular.ttf"),
148
148
  },
149
+ {
150
+ sourceFilePath: path.join(packageHomeFolderPath, "scripts", "post-install.mjs"),
151
+ destinationFilePath: path.join(newFolderPath, "post-install.mjs"),
152
+ },
149
153
  ];
150
154
  let errorCopyingFiles = false;
151
155
  copyFiles.forEach((c) => {
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import { resolve, basename } from "path";
4
+
5
+ const { dependencies } = JSON.parse(fs.readFileSync("./package.json"));
6
+
7
+ /** If this m2c2kit app uses surveys, then copy the required css
8
+ * files from node_modules so they can be bundled */
9
+ if (Object.keys(dependencies).includes("@m2c2kit/survey")) {
10
+ // does ./css folder exist? if not, create
11
+ if (!fs.existsSync("./css")) {
12
+ fs.mkdirSync("./css");
13
+ }
14
+
15
+ const cssDistDir = "./node_modules/@m2c2kit/survey/dist/css/";
16
+ const cssFiles = fs
17
+ .readdirSync(cssDistDir, {
18
+ withFileTypes: true,
19
+ })
20
+ .filter((dirent) => !dirent.isDirectory())
21
+ .map((dirent) => resolve(cssDistDir, dirent.name));
22
+
23
+ cssFiles.forEach((file) => {
24
+ const sourceContents = fs.readFileSync(file);
25
+ fs.writeFileSync(`./css/${basename(file)}`, sourceContents);
26
+ });
27
+ }
@@ -20,7 +20,7 @@
20
20
  height: 100vh;
21
21
  width: 100vw;
22
22
  ">
23
- <canvas style="height: 100vh; width: 100vw"></canvas>
23
+ <canvas style="height: 100vh; width: 100vw" id="m2c2-canvas"></canvas>
24
24
  <!-- If you don't want the game to start immediately, remove session.start()
25
25
  from the code and call session.start() somehow else, such as with
26
26
  the button shown below or a programmatic call -->
@@ -3,18 +3,19 @@
3
3
  "version": "1.0.0",
4
4
  "scripts": {
5
5
  "serve": "rollup -c --watch --configServe",
6
- "build": "rollup -c --configProd"
6
+ "build": "rollup -c --configProd",
7
+ "postinstall": "node ./post-install.mjs"
7
8
  },
8
9
  "private": true,
9
10
  "dependencies": {
10
- "@m2c2kit/core": "0.1.7",
11
- "@m2c2kit/addons": "0.1.7"
11
+ "@m2c2kit/core": "0.1.10",
12
+ "@m2c2kit/addons": "0.1.10"
12
13
  },
13
14
  "devDependencies": {
14
- "rollup": "2.63.0",
15
- "@rollup/plugin-typescript": "8.3.0",
15
+ "rollup": "2.70.0",
16
+ "@rollup/plugin-typescript": "8.3.1",
16
17
  "@rollup/plugin-node-resolve": "13.1.3",
17
- "@rollup/plugin-commonjs": "21.0.1",
18
+ "@rollup/plugin-commonjs": "21.0.2",
18
19
  "rollup-plugin-shim": "1.0.0",
19
20
  "rollup-plugin-copy": "3.4.0",
20
21
  "rollup-plugin-delete": "2.0.0",
@@ -58,7 +58,11 @@ export default (commandLineArgs) => {
58
58
  {
59
59
  src: "img/*",
60
60
  dest: `${outputFolder}/img`,
61
- },
61
+ },
62
+ {
63
+ src: "css/*",
64
+ dest: `${outputFolder}/css`,
65
+ },
62
66
  ],
63
67
  copyOnce: false,
64
68
  hook: "writeBundle",
@@ -1,29 +1,30 @@
1
1
  import {
2
- WebColors,
3
- Action,
4
2
  Game,
3
+ Action,
5
4
  Scene,
6
- Sprite,
7
- Point,
5
+ Shape,
8
6
  Label,
7
+ WebColors,
9
8
  LabelHorizontalAlignmentMode,
10
- Shape,
11
- Rect,
12
- GameOptions,
13
9
  GameParameters,
10
+ GameOptions,
14
11
  TrialSchema,
15
12
  Session,
16
- GameTrialEvent,
17
- GameLifecycleEvent,
18
13
  SessionLifecycleEvent,
19
- EventBase,
20
- EventType
14
+ ActivityDataEvent,
15
+ ActivityLifecycleEvent,
16
+ Sprite,
17
+ Timer,
21
18
  } from "@m2c2kit/core";
22
19
  import { Button, Instructions } from "@m2c2kit/addons";
23
20
 
24
21
  class {{className}} extends Game {
25
22
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
23
  constructor() {
24
+ /**
25
+ * These are configurable game parameters and their defaults.
26
+ * At run time, they can be changed with the setParameters() method.
27
+ */
27
28
  const defaultParameters: GameParameters = {
28
29
  ReadyTime: {
29
30
  value: 1000,
@@ -33,9 +34,16 @@ class {{className}} extends Game {
33
34
  TrialNum: { value: 3, description: "How many trials to run" },
34
35
  };
35
36
 
37
+ /**
38
+ * This describes all the data that will be generated by the assessment.
39
+ * At runtime, when a trial completes, the data will be returned to the
40
+ * session with a callback, along with this schema transformed into
41
+ * JSON Schema Draft-07 format.
42
+ */
36
43
  const demoTrialSchema: TrialSchema = {
37
44
  colorChosen: { type: "string", description: "the color that was picked" },
38
45
  correct: { type: "boolean", description: "was the answer correct?" },
46
+ responseTime: { type: "number", description: "response time (ms) to choose shape" }
39
47
  };
40
48
 
41
49
  const options: GameOptions = {
@@ -44,12 +52,12 @@ class {{className}} extends Game {
44
52
  uri: "https://your-repo-or-webpage-here",
45
53
  shortDescription: "A brief couple sentence description.",
46
54
  longDescription: "An extended, many-sentence description.",
47
- showFps: true,
55
+ showFps: false,
48
56
  trialSchema: demoTrialSchema,
49
57
  parameters: defaultParameters,
50
- // set this color so we can see the boundaries of the game during development,
51
- // but typically we would not set this
52
- bodyBackgroundColor: WebColors.Wheat,
58
+ // You can set this color so we can see the boundaries of the game during development,
59
+ // but typically we would not set this, so it is commented out
60
+ // bodyBackgroundColor: WebColors.Wheat,
53
61
  // note: using 2:1 aspect ratio, because that is closer to modern phones
54
62
  width: 400,
55
63
  height: 800,
@@ -80,6 +88,9 @@ class {{className}} extends Game {
80
88
  };
81
89
 
82
90
  super(options);
91
+ }
92
+
93
+ init(): void {
83
94
  // just for convenience, alias the variable game to "this"
84
95
  // (even though eslint doesn't like it)
85
96
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -97,6 +108,7 @@ class {{className}} extends Game {
97
108
  textFontSize: 24,
98
109
  titleFontSize: 30,
99
110
  image: "smiley",
111
+ imageMarginBottom: 24
100
112
  },
101
113
  {
102
114
  title: "{{appName}} Demo",
@@ -125,19 +137,19 @@ class {{className}} extends Game {
125
137
  const getReadyMessage = new Label({
126
138
  text: "Get Ready",
127
139
  fontSize: 24,
128
- position: new Point(200, 400),
140
+ position: { x: 200, y: 400 },
129
141
  });
130
142
  getReadyScene.addChild(getReadyMessage);
131
143
 
132
144
  // example of how to use an image. The image must be previously loaded
133
145
  const starSprite = new Sprite({
134
146
  imageName: "star",
135
- position: new Point(200, 500),
147
+ position: { x: 200, y: 500 },
136
148
  });
137
149
  getReadyScene.addChild(starSprite);
138
150
 
139
- // getReadyScene.setup() has a callback that is executed each time this scene is shown
140
- getReadyScene.setup(() => {
151
+ // getReadyScene.onSetup() has a callback that is executed each time this scene is shown
152
+ getReadyScene.onSetup(() => {
141
153
  getReadyScene.run(
142
154
  Action.Sequence([
143
155
  // Get the wait duration from the default game parameters, defined above
@@ -151,32 +163,32 @@ class {{className}} extends Game {
151
163
  );
152
164
  });
153
165
 
154
- // these entities before the setup() can be defined outside of a setup()
166
+ // these entities before the onSetup() can be defined outside of a onSetup()
155
167
  // because they exist through multiple trials
156
168
  // Their position and how they respond to interactions may differ across trials,
157
- // and that logic will be written within a setup()
169
+ // and that logic will be written within a onSetup()
158
170
  const chooseRectangleScene = new Scene({
159
171
  backgroundColor: WebColors.LightGray,
160
172
  });
161
173
  game.addScene(chooseRectangleScene);
162
174
  const redRect = new Shape({
163
- rect: new Rect({ width: 150, height: 100 }),
175
+ rect: { width: 150, height: 100 },
164
176
  fillColor: WebColors.Red,
165
177
  });
166
178
  chooseRectangleScene.addChild(redRect);
167
179
  const blueRect = new Shape({
168
- rect: new Rect({ width: 150, height: 100 }),
180
+ rect: { width: 150, height: 100 },
169
181
  fillColor: WebColors.Blue,
170
182
  });
171
183
  chooseRectangleScene.addChild(blueRect);
172
184
  const correctMessage = new Label({
173
185
  text: "CORRECT!",
174
- position: new Point(200, 500),
186
+ position: { x: 200, y: 500 },
175
187
  });
176
188
 
177
189
  const chooseMessage = new Label({
178
190
  text: "Choose the red rectangle",
179
- position: new Point(200, 200),
191
+ position: { x: 200, y: 200 },
180
192
  });
181
193
  chooseRectangleScene.addChild(chooseMessage);
182
194
 
@@ -184,32 +196,34 @@ class {{className}} extends Game {
184
196
  chooseRectangleScene.addChild(correctMessage);
185
197
  const wrongMessage = new Label({
186
198
  text: "WRONG!",
187
- position: new Point(200, 500),
199
+ position: {x: 200, y: 500 },
188
200
  });
189
201
 
190
202
  wrongMessage.hidden = true;
191
203
  chooseRectangleScene.addChild(wrongMessage);
192
204
 
193
- // chooseRectangleScene.setup() is passed a callback that is executed each
194
- // time this scene is shown. Within setup(), We will randomly decide on
205
+ // chooseRectangleScene.onSetup() is passed a callback that is executed each
206
+ // time this scene is shown. Within onSetup(), We will randomly decide on
195
207
  // what side the red rectangle is shown
196
- chooseRectangleScene.setup(() => {
208
+ chooseRectangleScene.onSetup(() => {
209
+ let responseTime = NaN;
197
210
  let redOnLeft = true;
198
211
  if (Math.random() > 0.5) {
199
212
  redOnLeft = false;
200
213
  }
201
214
 
202
215
  if (redOnLeft) {
203
- redRect.position = new Point(100, 300);
204
- blueRect.position = new Point(300, 300);
216
+ redRect.position = { x: 100, y: 300 };
217
+ blueRect.position = { x: 300, y: 300 };
205
218
  } else {
206
- redRect.position = new Point(300, 300);
207
- blueRect.position = new Point(100, 300);
219
+ redRect.position = { x: 300, y: 300 };
220
+ blueRect.position = { x: 100, y: 300 };;
208
221
  }
209
222
 
210
223
  // helper function to record the user's choice and
211
224
  // decide if we are done
212
225
  function recordUserInput(choseRedRect: boolean) {
226
+ game.addTrialData("responseTime", responseTime);
213
227
  game.addTrialData("correct", choseRedRect);
214
228
  if (choseRedRect) {
215
229
  game.addTrialData("colorChosen", "red");
@@ -231,6 +245,7 @@ class {{className}} extends Game {
231
245
  Action.Sequence([
232
246
  Action.Custom({
233
247
  callback: () => {
248
+ responseTime = Timer.elapsed("rt");
234
249
  // once a choice is made, don't allow additional taps
235
250
  redRect.isUserInteractionEnabled = false;
236
251
  blueRect.isUserInteractionEnabled = false;
@@ -264,6 +279,7 @@ class {{className}} extends Game {
264
279
  Action.Sequence([
265
280
  Action.Custom({
266
281
  callback: () => {
282
+ responseTime = Timer.elapsed("rt");
267
283
  redRect.isUserInteractionEnabled = false;
268
284
  blueRect.isUserInteractionEnabled = false;
269
285
  },
@@ -287,25 +303,25 @@ class {{className}} extends Game {
287
303
  });
288
304
  });
289
305
 
306
+ chooseRectangleScene.onAppear(() => {
307
+ Timer.removeAll();
308
+ Timer.start("rt");
309
+ });
310
+
290
311
  const endScene = new Scene();
291
312
  game.addScene(endScene);
292
313
  const doneLabel = new Label({
293
- text: `This will be reassigned in the setup() callback. If you see this, something went wrong!`,
294
- position: new Point(200, 300),
314
+ text: `This will be reassigned in the onSetup() callback. If you see this, something went wrong!`,
315
+ position: { x: 200, y: 300},
295
316
  });
296
317
  endScene.addChild(doneLabel);
297
318
 
298
319
  const startOverButton = new Button({
299
320
  text: "Start over",
300
- position: new Point(200, 600),
321
+ position: { x: 200, y: 600 },
301
322
  });
302
323
  startOverButton.isUserInteractionEnabled = true;
303
324
  startOverButton.onTapDown(() => {
304
- // in the setup() for the end scene, we animate the smiley sprite with
305
- // a move action. if the user taps the start over button before the
306
- // animation is completed, we should remove it by calling
307
- // removeAllActions()
308
- smileySprite.removeAllActions();
309
325
  game.initData();
310
326
  game.presentScene(getReadyScene);
311
327
  });
@@ -313,7 +329,7 @@ class {{className}} extends Game {
313
329
 
314
330
  const exitButton = new Button({
315
331
  text: "Exit",
316
- position: new Point(200, 675),
332
+ position: { x: 200, y: 675 },
317
333
  });
318
334
  exitButton.isUserInteractionEnabled = true;
319
335
  exitButton.onTapDown(() => {
@@ -325,69 +341,14 @@ class {{className}} extends Game {
325
341
  });
326
342
  endScene.addChild(exitButton);
327
343
 
328
- const smileySprite = new Sprite({ imageName: "smiley" });
329
- endScene.addChild(smileySprite);
330
-
331
- endScene.setup(() => {
344
+ endScene.onSetup(() => {
332
345
  doneLabel.text = `You did ${game.trialIndex} trials. You're done!`;
333
-
334
- // example of how to position a sprite and create an action to move it
335
- smileySprite.position = new Point(200, 500);
336
- smileySprite.run(
337
- Action.Move({ point: new Point(200, 100), duration: 3000 })
338
- );
339
346
  });
340
347
 
341
348
  game.entryScene = "instructions-01";
342
349
  }
343
350
  }
344
351
 
345
- // ===========================================================================
346
-
347
- //#region to support m2c2kit in Android WebView
348
- /** When running within an Android WebView, the below defines how the session
349
- * can communicate events back to the Android app. Note: names of this Android
350
- * namespace and its functions must match the corresponding Android code
351
- * in addJavascriptInterface() and @JavascriptInterface */
352
- // eslint-disable-next-line @typescript-eslint/no-namespace
353
- declare namespace Android {
354
- function onGameTrialComplete(gameTrialEventAsString: string): void;
355
- function onGameLifecycleChange(gameLifecycleEventAsString: string): void;
356
- function onSessionLifecycleChange(
357
- sessionLifecycleEventAsString: string
358
- ): void;
359
- /** if the Android native app will control the session execution and be
360
- * able to set custom game paraemters (which is probably what you want),
361
- * be sure that sessionManualStart() in the native code returns true */
362
- function sessionManualStart(): boolean;
363
- }
364
-
365
- function contextIsAndroidWebView(): boolean {
366
- return typeof Android !== "undefined";
367
- }
368
-
369
- function sendEventToAndroid(event: EventBase) {
370
- switch (event.eventType) {
371
- case EventType.sessionLifecycle: {
372
- Android.onSessionLifecycleChange(JSON.stringify(event));
373
- break;
374
- }
375
- case EventType.gameTrial: {
376
- Android.onGameTrialComplete(JSON.stringify(event));
377
- break;
378
- }
379
- case EventType.gameLifecycle: {
380
- Android.onGameLifecycleChange(JSON.stringify(event));
381
- break;
382
- }
383
- default:
384
- throw new Error(
385
- `attempt to send unknown event ${event.eventType} to Android`
386
- );
387
- }
388
- }
389
- //#endregion
390
-
391
352
  const game1 = new {{className}}();
392
353
  // default was 3 trials; this is how we can specify a different value
393
354
  game1.setParameters({ TrialNum: 2 });
@@ -395,68 +356,71 @@ game1.setParameters({ TrialNum: 2 });
395
356
  const session = new Session({
396
357
  activities: [game1],
397
358
  sessionCallbacks: {
398
- // onSessionLifecycleChange() will be called on events such
399
- // as when the session initialization is complete. Once initialized,
400
- // the session will automatically start, unless we're running
401
- // in an Android WebView and a manual start is desired.
402
- onSessionLifecycleChange: (event: SessionLifecycleEvent) => {
403
- if (event.initialized) {
404
- //#region to support m2c2kit in Android WebView
405
- if (contextIsAndroidWebView()) {
406
- sendEventToAndroid(event);
407
- }
408
- if (contextIsAndroidWebView() && Android.sessionManualStart()) {
409
- return;
410
- }
411
- //#endregion
359
+ /**
360
+ * onSessionLifecycleChange() will be called on events such
361
+ * as when the session initialization is complete or when it
362
+ * ends.
363
+ *
364
+ * Once initialized, the session will automatically start,
365
+ * unless we're running in an Android WebView AND a manual start
366
+ * is desired.
367
+ */
368
+ onSessionLifecycleChange: (ev: SessionLifecycleEvent) => {
369
+ if (ev.initialized) {
412
370
  session.start();
413
371
  }
414
- if (event.ended) {
372
+ if (ev.ended) {
415
373
  console.log("session ended");
416
- //#region to support m2c2kit in Android WebView
417
- if (contextIsAndroidWebView()) {
418
- sendEventToAndroid(event);
419
- }
420
- //#endregion
421
374
  }
422
375
  },
423
376
  },
424
- gameCallbacks: {
425
- // onGameTrialComplete() is where you insert code to post data to an API
426
- // or interop with a native function in the host app, if applicable
427
- onGameTrialComplete: (event: GameTrialEvent) => {
428
- console.log(`********** trial (index ${event.trialIndex}) complete`);
429
- console.log("data: " + JSON.stringify(event.gameData));
430
- console.log("trial schema: " + JSON.stringify(event.trialSchema));
431
- console.log("game parameters: " + JSON.stringify(event.gameParameters));
432
-
433
- //#region to support m2c2kit in Android WebView
434
- if (contextIsAndroidWebView()) {
435
- sendEventToAndroid(event);
436
- }
437
- //#endregion
377
+ activityCallbacks: {
378
+ /**
379
+ * onActivityDataCreate() is where you insert code to post data to an API
380
+ * or interop with a native function in the host app.
381
+ *
382
+ * newData is the data that was just generated by the completed trial
383
+ * data is all the data, cumulative of all trials, that have been generated.
384
+ *
385
+ * We separate out newData from data in case you want to alter the execution
386
+ * based on the most recent trial, e.g., maybe you want to stop after
387
+ * a certain user behavior or performance threshold in the just completed
388
+ * trial.
389
+ */
390
+ onActivityDataCreate: (ev: ActivityDataEvent) => {
391
+ console.log(`********** trial complete`);
392
+ console.log("newData: " + JSON.stringify(ev.newData));
393
+ console.log("newData schema: " + JSON.stringify(ev.newDataSchema));
394
+ console.log("data: " + JSON.stringify(ev.data));
395
+ console.log("data schema: " + JSON.stringify(ev.dataSchema));
396
+ console.log(
397
+ "activity parameters: " + JSON.stringify(ev.activityConfiguration)
398
+ );
438
399
  },
439
- onGameLifecycleChange: (event: GameLifecycleEvent) => {
440
- if (event.ended) {
441
- console.log(`ended game ${event.gameName}`);
400
+ /**
401
+ * onActivityLifecycleChange() notifies us when an activity, such
402
+ * as an assessment or a survey, has completed. Usually, however,
403
+ * we want to know when all the activities are done, so we'll
404
+ * look for the session ending via onSessionLifecycleChange
405
+ */
406
+ onActivityLifecycleChange: (ev: ActivityLifecycleEvent) => {
407
+ if (ev.ended) {
408
+ console.log(`ended activity ${ev.name}`);
442
409
  if (session.nextActivity) {
443
410
  session.advanceToNextActivity();
444
411
  } else {
445
412
  session.end();
446
413
  }
447
- //#region to support m2c2kit in Android WebView
448
- if (contextIsAndroidWebView()) {
449
- sendEventToAndroid(event);
450
- }
451
- //#endregion
452
414
  }
453
415
  },
454
416
  },
455
417
  });
456
418
 
457
- /** make session also available on window in case we want to control
419
+ /**
420
+ * Make session also available on window in case we want to control
458
421
  * the session through another means, such as other javascript or
459
- * browser code, or the Android WebView loadUrl() method */
422
+ * browser code, or the Android WebView loadUrl() method
423
+ * */
460
424
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
461
425
  (window as unknown as any).session = session;
462
426
  session.init();
@@ -19,6 +19,7 @@
19
19
  "build",
20
20
  "dist",
21
21
  "**/{{appName}}.bundle.js",
22
- "rollup.config.js"
22
+ "rollup.config.js",
23
+ "post-install.mjs"
23
24
  ]
24
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m2c2kit/cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.12",
4
4
  "description": "m2c2kit command line interface",
5
5
  "module": "dist/cli.js",
6
6
  "files": [
@@ -17,28 +17,28 @@
17
17
  "build": "npm run clean && npm run compile && npm run copy-files && npm run write-dotenv",
18
18
  "compile": "tsc",
19
19
  "clean": "rimraf dist/ && rimraf build/",
20
- "copy-files": "copyfiles -f build/src/cli.js dist && copyfiles templates/* fonts/**/* dist/",
20
+ "copy-files": "copyfiles -f build/src/cli.js dist && copyfiles scripts/* templates/* fonts/**/* dist/",
21
21
  "write-dotenv": "node write-dotenv.js"
22
22
  },
23
23
  "author": "",
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
26
  "axios": "0.24.0",
27
- "chalk": "5.0.0",
27
+ "chalk": "5.0.1",
28
28
  "conf": "10.1.1",
29
29
  "form-data": "4.0.0",
30
30
  "handlebars": "4.7.7",
31
- "ora": "6.0.1",
31
+ "ora": "6.1.0",
32
32
  "prompts": "2.4.2",
33
33
  "yargs": "17.3.1"
34
34
  },
35
35
  "devDependencies": {
36
- "@types/node": "17.0.8",
36
+ "@types/node": "17.0.21",
37
37
  "@types/prompts": "2.0.14",
38
- "@types/yargs": "17.0.8",
38
+ "@types/yargs": "17.0.9",
39
39
  "copyfiles": "2.4.1",
40
40
  "rimraf": "3.0.2",
41
41
  "tslib": "2.3.1",
42
- "typescript": "4.5.4"
42
+ "typescript": "4.6.2"
43
43
  }
44
44
  }