@m2c2kit/cli 0.1.8 → 0.1.11

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.8
1
+ CLI_VERSION=0.1.11
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.6",
11
- "@m2c2kit/addons": "0.1.6"
11
+ "@m2c2kit/core": "0.1.9",
12
+ "@m2c2kit/addons": "0.1.9"
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,26 +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,
13
+ SessionLifecycleEvent,
14
+ ActivityDataEvent,
15
+ ActivityLifecycleEvent,
16
+ Sprite,
17
+ Timer,
18
18
  } from "@m2c2kit/core";
19
19
  import { Button, Instructions } from "@m2c2kit/addons";
20
20
 
21
21
  class {{className}} extends Game {
22
22
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
- constructor(specifiedParameters?: any) {
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
+ */
24
28
  const defaultParameters: GameParameters = {
25
29
  ReadyTime: {
26
30
  value: 1000,
@@ -30,9 +34,16 @@ class {{className}} extends Game {
30
34
  TrialNum: { value: 3, description: "How many trials to run" },
31
35
  };
32
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
+ */
33
43
  const demoTrialSchema: TrialSchema = {
34
44
  colorChosen: { type: "string", description: "the color that was picked" },
35
45
  correct: { type: "boolean", description: "was the answer correct?" },
46
+ responseTime: { type: "number", description: "response time (ms) to choose shape" }
36
47
  };
37
48
 
38
49
  const options: GameOptions = {
@@ -41,12 +52,12 @@ class {{className}} extends Game {
41
52
  uri: "https://your-repo-or-webpage-here",
42
53
  shortDescription: "A brief couple sentence description.",
43
54
  longDescription: "An extended, many-sentence description.",
44
- showFps: true,
55
+ showFps: false,
45
56
  trialSchema: demoTrialSchema,
46
57
  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,
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,
50
61
  // note: using 2:1 aspect ratio, because that is closer to modern phones
51
62
  width: 400,
52
63
  height: 800,
@@ -76,7 +87,10 @@ class {{className}} extends Game {
76
87
  ],
77
88
  };
78
89
 
79
- super(options, specifiedParameters);
90
+ super(options);
91
+ }
92
+
93
+ init(): void {
80
94
  // just for convenience, alias the variable game to "this"
81
95
  // (even though eslint doesn't like it)
82
96
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -94,6 +108,7 @@ class {{className}} extends Game {
94
108
  textFontSize: 24,
95
109
  titleFontSize: 30,
96
110
  image: "smiley",
111
+ imageMarginBottom: 24
97
112
  },
98
113
  {
99
114
  title: "{{appName}} Demo",
@@ -122,19 +137,19 @@ class {{className}} extends Game {
122
137
  const getReadyMessage = new Label({
123
138
  text: "Get Ready",
124
139
  fontSize: 24,
125
- position: new Point(200, 400),
140
+ position: { x: 200, y: 400 },
126
141
  });
127
142
  getReadyScene.addChild(getReadyMessage);
128
143
 
129
144
  // example of how to use an image. The image must be previously loaded
130
145
  const starSprite = new Sprite({
131
146
  imageName: "star",
132
- position: new Point(200, 500),
147
+ position: { x: 200, y: 500 },
133
148
  });
134
149
  getReadyScene.addChild(starSprite);
135
150
 
136
- // getReadyScene.setup() has a callback that is executed each time this scene is shown
137
- getReadyScene.setup(() => {
151
+ // getReadyScene.onSetup() has a callback that is executed each time this scene is shown
152
+ getReadyScene.onSetup(() => {
138
153
  getReadyScene.run(
139
154
  Action.Sequence([
140
155
  // Get the wait duration from the default game parameters, defined above
@@ -148,32 +163,32 @@ class {{className}} extends Game {
148
163
  );
149
164
  });
150
165
 
151
- // 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()
152
167
  // because they exist through multiple trials
153
168
  // Their position and how they respond to interactions may differ across trials,
154
- // and that logic will be written within a setup()
169
+ // and that logic will be written within a onSetup()
155
170
  const chooseRectangleScene = new Scene({
156
171
  backgroundColor: WebColors.LightGray,
157
172
  });
158
173
  game.addScene(chooseRectangleScene);
159
174
  const redRect = new Shape({
160
- rect: new Rect({ width: 150, height: 100 }),
175
+ rect: { width: 150, height: 100 },
161
176
  fillColor: WebColors.Red,
162
177
  });
163
178
  chooseRectangleScene.addChild(redRect);
164
179
  const blueRect = new Shape({
165
- rect: new Rect({ width: 150, height: 100 }),
180
+ rect: { width: 150, height: 100 },
166
181
  fillColor: WebColors.Blue,
167
182
  });
168
183
  chooseRectangleScene.addChild(blueRect);
169
184
  const correctMessage = new Label({
170
185
  text: "CORRECT!",
171
- position: new Point(200, 500),
186
+ position: { x: 200, y: 500 },
172
187
  });
173
188
 
174
189
  const chooseMessage = new Label({
175
190
  text: "Choose the red rectangle",
176
- position: new Point(200, 200),
191
+ position: { x: 200, y: 200 },
177
192
  });
178
193
  chooseRectangleScene.addChild(chooseMessage);
179
194
 
@@ -181,32 +196,34 @@ class {{className}} extends Game {
181
196
  chooseRectangleScene.addChild(correctMessage);
182
197
  const wrongMessage = new Label({
183
198
  text: "WRONG!",
184
- position: new Point(200, 500),
199
+ position: {x: 200, y: 500 },
185
200
  });
186
201
 
187
202
  wrongMessage.hidden = true;
188
203
  chooseRectangleScene.addChild(wrongMessage);
189
204
 
190
- // chooseRectangleScene.setup() is passed a callback that is executed each
191
- // 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
192
207
  // what side the red rectangle is shown
193
- chooseRectangleScene.setup(() => {
208
+ chooseRectangleScene.onSetup(() => {
209
+ let responseTime = NaN;
194
210
  let redOnLeft = true;
195
211
  if (Math.random() > 0.5) {
196
212
  redOnLeft = false;
197
213
  }
198
214
 
199
215
  if (redOnLeft) {
200
- redRect.position = new Point(100, 300);
201
- blueRect.position = new Point(300, 300);
216
+ redRect.position = { x: 100, y: 300 };
217
+ blueRect.position = { x: 300, y: 300 };
202
218
  } else {
203
- redRect.position = new Point(300, 300);
204
- blueRect.position = new Point(100, 300);
219
+ redRect.position = { x: 300, y: 300 };
220
+ blueRect.position = { x: 100, y: 300 };;
205
221
  }
206
222
 
207
223
  // helper function to record the user's choice and
208
224
  // decide if we are done
209
225
  function recordUserInput(choseRedRect: boolean) {
226
+ game.addTrialData("responseTime", responseTime);
210
227
  game.addTrialData("correct", choseRedRect);
211
228
  if (choseRedRect) {
212
229
  game.addTrialData("colorChosen", "red");
@@ -228,6 +245,7 @@ class {{className}} extends Game {
228
245
  Action.Sequence([
229
246
  Action.Custom({
230
247
  callback: () => {
248
+ responseTime = Timer.elapsed("rt");
231
249
  // once a choice is made, don't allow additional taps
232
250
  redRect.isUserInteractionEnabled = false;
233
251
  blueRect.isUserInteractionEnabled = false;
@@ -261,6 +279,7 @@ class {{className}} extends Game {
261
279
  Action.Sequence([
262
280
  Action.Custom({
263
281
  callback: () => {
282
+ responseTime = Timer.elapsed("rt");
264
283
  redRect.isUserInteractionEnabled = false;
265
284
  blueRect.isUserInteractionEnabled = false;
266
285
  },
@@ -284,25 +303,25 @@ class {{className}} extends Game {
284
303
  });
285
304
  });
286
305
 
306
+ chooseRectangleScene.onAppear(() => {
307
+ Timer.removeAll();
308
+ Timer.start("rt");
309
+ });
310
+
287
311
  const endScene = new Scene();
288
312
  game.addScene(endScene);
289
313
  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),
314
+ text: `This will be reassigned in the onSetup() callback. If you see this, something went wrong!`,
315
+ position: { x: 200, y: 300},
292
316
  });
293
317
  endScene.addChild(doneLabel);
294
318
 
295
319
  const startOverButton = new Button({
296
320
  text: "Start over",
297
- position: new Point(200, 600),
321
+ position: { x: 200, y: 600 },
298
322
  });
299
323
  startOverButton.isUserInteractionEnabled = true;
300
324
  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
325
  game.initData();
307
326
  game.presentScene(getReadyScene);
308
327
  });
@@ -310,7 +329,7 @@ class {{className}} extends Game {
310
329
 
311
330
  const exitButton = new Button({
312
331
  text: "Exit",
313
- position: new Point(200, 675),
332
+ position: { x: 200, y: 675 },
314
333
  });
315
334
  exitButton.isUserInteractionEnabled = true;
316
335
  exitButton.onTapDown(() => {
@@ -322,78 +341,86 @@ class {{className}} extends Game {
322
341
  });
323
342
  endScene.addChild(exitButton);
324
343
 
325
- const smileySprite = new Sprite({ imageName: "smiley" });
326
- endScene.addChild(smileySprite);
327
-
328
- endScene.setup(() => {
344
+ endScene.onSetup(() => {
329
345
  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
346
  });
337
347
 
338
348
  game.entryScene = "instructions-01";
339
349
  }
340
350
  }
341
351
 
342
- // ============================================================================
343
-
344
- // When running within an Android webview, the below defines how the session
345
- // can communicate events to the Android app. Note: the names of this Android
346
- // namespace and its functions must match the corresponding Android code
347
- // in addJavascriptInterface() and @JavascriptInterface
348
- // eslint-disable-next-line @typescript-eslint/no-namespace
349
- declare namespace Android {
350
- function onGameTrialComplete(gameTrialEventAsString: string): void;
351
- function onGameLifecycleChange(gameLifecycleEventAsString: string): void;
352
- }
353
-
352
+ const game1 = new {{className}}();
354
353
  // default was 3 trials; this is how we can specify a different value
355
- const game1 = new {{className}}({ TrialNum: 2 });
354
+ game1.setParameters({ TrialNum: 2 });
356
355
 
357
356
  const session = new Session({
358
357
  activities: [game1],
359
- gameCallbacks: {
360
- // onGameTrialComplete() is where you insert code to post data to an API
361
- // or interop with a native function in the host app, if applicable
362
- onGameTrialComplete: (e: GameTrialEvent) => {
363
- console.log(`********** trial ${e.trialIndex} complete`);
364
- console.log("data: " + JSON.stringify(e.gameData));
365
- console.log("trial schema: " + JSON.stringify(e.trialSchema));
366
- console.log("game parameters: " + JSON.stringify(e.gameParameters));
367
-
368
- // callback to native Android app, if running in that context
369
- if (typeof Android !== "undefined") {
370
- Android.onGameTrialComplete(JSON.stringify(e));
358
+ sessionCallbacks: {
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) {
370
+ session.start();
371
+ }
372
+ if (ev.ended) {
373
+ console.log("session ended");
371
374
  }
372
375
  },
373
- // onGameLifecycleChange() is called when the game lifecycles changes
374
- onGameLifecycleChange: (e: GameLifecycleEvent) => {
375
- if (e.ended) {
376
- console.log(`user requested exit in game ${e.gameName}`);
377
- // this session has only one activity, but this is how it would go to
378
- // the next activity
376
+ },
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
+ );
399
+ },
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}`);
379
409
  if (session.nextActivity) {
380
410
  session.advanceToNextActivity();
381
- }
382
-
383
- // callback to native Android app, if running in that context
384
- if (typeof Android !== "undefined") {
385
- Android.onGameLifecycleChange(JSON.stringify(e));
411
+ } else {
412
+ session.end();
386
413
  }
387
414
  }
388
415
  },
389
416
  },
390
417
  });
391
418
 
392
- // make session also available on window in case we want to control
393
- // the session through another means
419
+ /**
420
+ * Make session also available on window in case we want to control
421
+ * the session through another means, such as other javascript or
422
+ * browser code, or the Android WebView loadUrl() method
423
+ * */
394
424
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
395
425
  (window as unknown as any).session = session;
396
-
397
- session.init().then(() => {
398
- session.start();
399
- });
426
+ session.init();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m2c2kit/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.11",
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
  }