@iruka-edu/create-iruka-game 1.0.9 → 1.1.0

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/index.cjs CHANGED
@@ -202,6 +202,7 @@ dist
202
202
  }
203
203
  console.log(`
204
204
  \u{1F389} Project \u0111\xE3 s\u1EB5n s\xE0ng t\u1EA1i: ${import_picocolors.default.cyan(root)}`);
205
+ console.log(import_picocolors.default.green(`\u2728 Game c\u1EE7a b\u1EA1n \u0111\xE3 \u0111\u01B0\u1EE3c t\xEDch h\u1EE3p s\u1EB5n Iruka SDK v0.4. Safe & Ready!`));
205
206
  console.log(`\u{1F449} C\xE1c b\u01B0\u1EDBc ti\u1EBFp theo:`);
206
207
  console.log(` cd ${projectName}`);
207
208
  const userAgent = process.env.npm_config_user_agent || "";
package/package.json CHANGED
@@ -1,35 +1,35 @@
1
- {
2
- "name": "@iruka-edu/create-iruka-game",
3
- "version": "1.0.9",
4
- "description": "Create Iruka mini game (React + Phaser template)",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "create-iruka-game": "dist/index.cjs"
9
- },
10
- "scripts": {
11
- "build": "tsup src/index.ts --format cjs --clean",
12
- "dev": "tsup src/index.ts --format cjs --watch"
13
- },
14
- "dependencies": {
15
- "fs-extra": "^11.3.3",
16
- "picocolors": "^1.1.1",
17
- "prompts": "^2.4.2"
18
- },
19
- "devDependencies": {
20
- "@types/fs-extra": "^11.0.4",
21
- "@types/prompts": "^2.4.9",
22
- "terser": "^5.44.1",
23
- "tsup": "^8.0.0",
24
- "typescript": "^5.5.0"
25
- },
26
- "files": [
27
- "dist",
28
- "templates",
29
- "README.md"
30
- ],
31
- "publishConfig": {
32
- "access": "public",
33
- "registry": "https://registry.npmjs.org/"
34
- }
35
- }
1
+ {
2
+ "name": "@iruka-edu/create-iruka-game",
3
+ "version": "1.1.0",
4
+ "description": "Create Iruka mini game (React + Phaser template)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-iruka-game": "dist/index.cjs"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format cjs --clean",
12
+ "dev": "tsup src/index.ts --format cjs --watch"
13
+ },
14
+ "dependencies": {
15
+ "fs-extra": "^11.3.3",
16
+ "picocolors": "^1.1.1",
17
+ "prompts": "^2.4.2"
18
+ },
19
+ "devDependencies": {
20
+ "@types/fs-extra": "^11.0.4",
21
+ "@types/prompts": "^2.4.9",
22
+ "terser": "^5.44.1",
23
+ "tsup": "^8.0.0",
24
+ "typescript": "^5.5.0"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "templates",
29
+ "README.md"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org/"
34
+ }
35
+ }
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "phaser": "^3.80.0",
13
- "@iruka-edu/mini-game-sdk": "^0.3.3"
13
+ "@iruka-edu/mini-game-sdk": "^0.4.0"
14
14
  },
15
15
  "devDependencies": {
16
16
  "typescript": "^5.5.0",
@@ -0,0 +1,49 @@
1
+ import { BaseGame, type LaunchContext, createIrukaPluginConfig } from "@iruka-edu/mini-game-sdk";
2
+ import Phaser from "phaser";
3
+ import { PreloadScene } from "../scenes/Preloader";
4
+ import { PlayScene } from "../scenes/Play";
5
+
6
+ export class MyGame extends BaseGame {
7
+ private game: Phaser.Game | null = null;
8
+
9
+ /**
10
+ * This function runs only after:
11
+ * 1. Hub connection is successful
12
+ * 2. Context validation passes
13
+ */
14
+ protected async onInit(context: LaunchContext): Promise<void> {
15
+ console.log("🎮 Initializing Game with Context:", context);
16
+
17
+ const config: Phaser.Types.Core.GameConfig = {
18
+ type: Phaser.AUTO,
19
+ width: 800,
20
+ height: 600,
21
+ parent: "game-container",
22
+ backgroundColor: "#2c3e50",
23
+ scale: {
24
+ mode: Phaser.Scale.FIT,
25
+ autoCenter: Phaser.Scale.CENTER_BOTH,
26
+ },
27
+ scene: [PreloadScene, PlayScene],
28
+ // Register Iruka plugin
29
+ plugins: {
30
+ global: [createIrukaPluginConfig()]
31
+ }
32
+ };
33
+
34
+ this.game = new Phaser.Game(config);
35
+
36
+ // Make game globally accessible for plugin initialization
37
+ (window as any).phaserGame = this.game;
38
+
39
+ // Inject SDK into Registry for Scenes to access (legacy support)
40
+ this.game.registry.set("iruka-context", context);
41
+ // this.bridge is available from BaseGame
42
+ this.game.registry.set("iruka-bridge", this.bridge);
43
+ }
44
+
45
+ protected async onStart(): Promise<void> {
46
+ console.log("🚀 Game Started Lifecycle");
47
+ // Optional: Tracking logic or additional setup
48
+ }
49
+ }
@@ -1,21 +1,6 @@
1
- import Phaser from "phaser";
1
+ import { MyGame } from "./game/MyGame";
2
2
  import "./style.css";
3
- import { GameConfig } from "./config";
4
- import { Iruka } from "./iruka/adapter";
5
3
 
6
- /**
7
- * Entry point - Initialize SDK then start Phaser game
8
- */
9
- (async () => {
10
- try {
11
- // 1. Connect to Hub (or fallback to mock mode)
12
- await Iruka.init();
13
- console.log("✅ SDK Ready:", Iruka.isMock ? "Mock Mode" : "Connected to Hub");
14
-
15
- // 2. Start Phaser game
16
- new Phaser.Game(GameConfig);
17
- } catch (error) {
18
- console.error("❌ Failed to initialize:", error);
19
- Iruka.error("INIT_FAILED", error);
20
- }
21
- })();
4
+ // Launch Game
5
+ // BaseGame handles Handshake, Validation, and Loading UI automatically
6
+ new MyGame();
@@ -1,14 +1,12 @@
1
1
  import { Scene } from "phaser";
2
- import { Iruka } from "../iruka/adapter";
3
- import { GAME_WIDTH, GAME_HEIGHT } from "../config";
4
2
 
5
3
  /**
6
4
  * PlayScene - Main gameplay scene
7
5
  *
8
- * Access SDK context via Iruka.context:
9
- * - sessionId, gameId, locale, difficulty
10
- * - playerId, profile (name, avatar)
11
- * - seed (for reproducible randomness)
6
+ * Access SDK via this.iruka (Phaser Plugin):
7
+ * - this.iruka.submitScore(100)
8
+ * - this.iruka.completeGame({score, timeMs})
9
+ * - this.iruka.getContext() for sessionId, difficulty, etc.
12
10
  */
13
11
  export class PlayScene extends Scene {
14
12
  private score = 0;
@@ -23,8 +21,10 @@ export class PlayScene extends Scene {
23
21
  this.startTime = Date.now();
24
22
  this.score = 0;
25
23
 
26
- // Get context from SDK
27
- const { difficulty, profile, seed } = Iruka.context;
24
+ // Get context from SDK plugin
25
+ const context = (this as any).iruka?.getContext();
26
+ const { difficulty, player, seed } = context || {};
27
+ const profile = player || {};
28
28
  console.log(`🎮 Starting game | Difficulty: ${difficulty} | Seed: ${seed}`);
29
29
 
30
30
  this.setupUI(profile?.name || "Player");
@@ -32,20 +32,34 @@ export class PlayScene extends Scene {
32
32
  }
33
33
 
34
34
  private setupUI(playerName: string) {
35
+ const width = this.scale.width;
36
+ const height = this.scale.height;
37
+
35
38
  // Header
36
- this.add.text(20, 20, `Player: ${playerName}`, { fontSize: "18px", color: "#fff" });
39
+ this.add.text(20, 20, `Player: ${playerName}`, {
40
+ fontSize: "18px",
41
+ color: "#fff",
42
+ });
37
43
 
38
44
  // Score
39
45
  this.scoreText = this.add
40
- .text(GAME_WIDTH - 20, 20, "Score: 0", { fontSize: "20px", color: "#2ecc71" })
46
+ .text(width - 20, 20, "Score: 0", {
47
+ fontSize: "20px",
48
+ color: "#2ecc71",
49
+ })
41
50
  .setOrigin(1, 0);
42
51
 
43
52
  // Instructions
44
53
  this.add
45
- .text(GAME_WIDTH / 2, GAME_HEIGHT - 40, "Click anywhere to score | Press SPACE to finish", {
46
- fontSize: "14px",
47
- color: "#aaa",
48
- })
54
+ .text(
55
+ width / 2,
56
+ height - 40,
57
+ "Click anywhere to score | Press SPACE to finish",
58
+ {
59
+ fontSize: "14px",
60
+ color: "#aaa",
61
+ }
62
+ )
49
63
  .setOrigin(0.5);
50
64
  }
51
65
 
@@ -63,16 +77,22 @@ export class PlayScene extends Scene {
63
77
  private addScore(points: number) {
64
78
  this.score += points;
65
79
  this.scoreText.setText(`Score: ${this.score}`);
66
- Iruka.updateScore(this.score, points);
80
+
81
+ // Use Iruka plugin to submit score
82
+ (this as any).iruka?.submitScore(this.score, points);
67
83
  }
68
84
 
69
85
  private endGame() {
70
86
  const timeMs = Date.now() - this.startTime;
71
87
 
72
- // Submit result to Hub
73
- Iruka.submit(this.score, timeMs, {
74
- maxScore: 100,
75
- accuracy: Math.min(1, this.score / 100),
88
+ // Submit result to Hub using Iruka plugin
89
+ (this as any).iruka?.completeGame({
90
+ score: this.score,
91
+ timeMs,
92
+ extras: {
93
+ maxScore: 100,
94
+ accuracy: Math.min(1, this.score / 100),
95
+ },
76
96
  });
77
97
 
78
98
  console.log(`🏁 Game Over | Score: ${this.score} | Time: ${timeMs}ms`);
@@ -1,8 +1,8 @@
1
1
  import { Scene } from "phaser";
2
- import { Iruka } from "../iruka/adapter";
2
+ // Removed Iruka adapter import
3
3
 
4
4
  /**
5
- * PreloadScene - Load assets báo progress cho Hub
5
+ * PreloadScene - Load assets and report progress to Hub
6
6
  */
7
7
  export class PreloadScene extends Scene {
8
8
  constructor() {
@@ -12,10 +12,15 @@ export class PreloadScene extends Scene {
12
12
  preload() {
13
13
  this.createLoadingUI();
14
14
 
15
- // Báo progress cho Hub
15
+ // Report progress to Hub
16
16
  this.load.on("progress", (value: number) => {
17
17
  this.updateProgress(value);
18
- Iruka.updateProgress({ progress: value });
18
+ const bridge = this.registry.get("iruka-bridge");
19
+ bridge?.loading(value * 100); // SDK expects 0-100? or 0-1? IframeBridge says loading(progress: number)
20
+ // IframeBridge documentation should be checked. Usually 0-100 for UI.
21
+ // Wait, let's check game-core/bridge.ts or iframeBridge.ts.
22
+ // iframeBridge.ts: loading: (progress: number) => post("LOADING", { progress })
23
+ // Usually standard is 0-100.
19
24
  });
20
25
 
21
26
  // Load game assets
@@ -24,7 +29,9 @@ export class PreloadScene extends Scene {
24
29
 
25
30
  create() {
26
31
  // Báo Hub game đã sẵn sàng
27
- Iruka.ready(["score", "progress"]);
32
+ // Iruka.ready(["score", "progress"]);
33
+ const bridge = this.registry.get("iruka-bridge");
34
+ bridge?.ready(["score", "progress"]);
28
35
 
29
36
  // Chuyển sang màn chơi
30
37
  this.scene.start("Play");
@@ -34,17 +41,25 @@ export class PreloadScene extends Scene {
34
41
  const { width, height } = this.cameras.main;
35
42
 
36
43
  // Background
37
- this.add.graphics().fillStyle(0x222222, 0.8).fillRect(width / 2 - 160, height / 2 - 25, 320, 50);
44
+ this.add
45
+ .graphics()
46
+ .fillStyle(0x222222, 0.8)
47
+ .fillRect(width / 2 - 160, height / 2 - 25, 320, 50);
38
48
 
39
49
  // Progress bar
40
50
  const bar = this.add.graphics();
41
- const text = this.add.text(width / 2, height / 2, "0%", {
42
- fontSize: "18px",
43
- color: "#ffffff",
44
- }).setOrigin(0.5);
51
+ const text = this.add
52
+ .text(width / 2, height / 2, "0%", {
53
+ fontSize: "18px",
54
+ color: "#ffffff",
55
+ })
56
+ .setOrigin(0.5);
45
57
 
46
58
  this.load.on("progress", (value: number) => {
47
- bar.clear().fillStyle(0x3498db, 1).fillRect(width / 2 - 150, height / 2 - 15, 300 * value, 30);
59
+ bar
60
+ .clear()
61
+ .fillStyle(0x3498db, 1)
62
+ .fillRect(width / 2 - 150, height / 2 - 15, 300 * value, 30);
48
63
  text.setText(`${Math.round(value * 100)}%`);
49
64
  });
50
65
  }
@@ -1,28 +0,0 @@
1
- import Phaser from "phaser";
2
- import { PreloadScene } from "./scenes/Preloader";
3
- import { PlayScene } from "./scenes/Play";
4
-
5
- export const GAME_WIDTH = 800;
6
- export const GAME_HEIGHT = 600;
7
-
8
- export const GameConfig: Phaser.Types.Core.GameConfig = {
9
- type: Phaser.AUTO,
10
- width: GAME_WIDTH,
11
- height: GAME_HEIGHT,
12
- parent: "app",
13
-
14
- scale: {
15
- mode: Phaser.Scale.FIT,
16
- autoCenter: Phaser.Scale.CENTER_BOTH,
17
- },
18
-
19
- backgroundColor: "#2c3e50",
20
-
21
- // Disable physics if not needed (better performance)
22
- // physics: {
23
- // default: 'arcade',
24
- // arcade: { gravity: { x: 0, y: 300 }, debug: false }
25
- // },
26
-
27
- scene: [PreloadScene, PlayScene],
28
- };
@@ -1,121 +0,0 @@
1
- import {
2
- createIframeBridge,
3
- type IframeBridge,
4
- type LaunchContext,
5
- type HubCommand,
6
- } from "@iruka-edu/mini-game-sdk";
7
-
8
- /**
9
- * IrukaAdapter - Cầu nối giữa Phaser Game và Iruka Hub
10
- *
11
- * Sử dụng SDK để giao tiếp với Hub:
12
- * - await Iruka.init() - Khởi tạo và chờ INIT từ Hub
13
- * - Iruka.context - Lấy thông tin session/user
14
- * - Iruka.submit() - Gửi kết quả cuối game
15
- */
16
- class IrukaAdapter {
17
- private static instance: IrukaAdapter;
18
-
19
- public context!: LaunchContext;
20
- public isMock = false;
21
- private bridge!: IframeBridge;
22
- private initResolver?: (ctx: LaunchContext) => void;
23
-
24
- public static get use(): IrukaAdapter {
25
- return this.instance || (this.instance = new IrukaAdapter());
26
- }
27
-
28
- /**
29
- * Khởi tạo SDK và chờ INIT command từ Hub
30
- * Timeout 3s sẽ fallback sang Mock mode cho development
31
- */
32
- public async init(): Promise<LaunchContext> {
33
- return new Promise((resolve) => {
34
- // Tạo bridge với handler
35
- this.bridge = createIframeBridge(this.handleCommand.bind(this));
36
-
37
- // Timeout -> Mock mode
38
- const timeout = setTimeout(() => {
39
- console.warn("⚠️ Hub timeout, using Mock mode");
40
- this.context = this.createMockContext();
41
- this.isMock = true;
42
- resolve(this.context);
43
- }, 3000);
44
-
45
- // Lưu resolver để gọi khi nhận INIT
46
- this.initResolver = (ctx: LaunchContext) => {
47
- clearTimeout(timeout);
48
- this.context = ctx;
49
- this.isMock = false;
50
- resolve(ctx);
51
- };
52
- });
53
- }
54
-
55
- /** Báo Hub game đã sẵn sàng */
56
- public ready(capabilities: string[] = ["score", "progress"]) {
57
- this.bridge.ready(capabilities);
58
- }
59
-
60
- /** Cập nhật điểm */
61
- public updateScore(score: number, delta?: number) {
62
- this.bridge.score(score, delta);
63
- }
64
-
65
- /** Cập nhật tiến độ loading */
66
- public updateProgress(data: unknown) {
67
- this.bridge.progress(data);
68
- }
69
-
70
- /** Gửi kết quả cuối game */
71
- public submit(score: number, timeMs: number, extras?: unknown) {
72
- this.bridge.complete({ score, timeMs, extras });
73
- }
74
-
75
- /** Báo lỗi cho Hub */
76
- public error(message: string, detail?: unknown) {
77
- this.bridge.error(message, detail);
78
- }
79
-
80
- /** Cleanup khi game kết thúc */
81
- public dispose() {
82
- this.bridge.dispose();
83
- }
84
-
85
- // === Private ===
86
-
87
- private handleCommand(cmd: HubCommand) {
88
- switch (cmd.type) {
89
- case "INIT":
90
- this.initResolver?.(cmd.payload);
91
- break;
92
- case "START":
93
- console.log("🎮 Hub: START");
94
- break;
95
- case "PAUSE":
96
- console.log("⏸️ Hub: PAUSE");
97
- break;
98
- case "RESUME":
99
- console.log("▶️ Hub: RESUME");
100
- break;
101
- case "QUIT":
102
- console.log("🛑 Hub: QUIT");
103
- this.dispose();
104
- break;
105
- }
106
- }
107
-
108
- private createMockContext(): LaunchContext {
109
- return {
110
- sessionId: "mock-" + Date.now(),
111
- gameId: "{{GAME_ID}}",
112
- locale: "vi",
113
- difficulty: "medium",
114
- playerId: "dev-player",
115
- profile: { name: "Developer" },
116
- seed: Math.floor(Math.random() * 1000000),
117
- };
118
- }
119
- }
120
-
121
- export const Iruka = IrukaAdapter.use;