@iruka-edu/create-iruka-game 1.0.2 → 1.0.4

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
@@ -65,9 +65,17 @@ async function init() {
65
65
  name: "token",
66
66
  message: "Nh\u1EADp GitHub Token (\u0111\u1EC3 c\xE0i \u0111\u1EB7t SDK):",
67
67
  validate: (value) => value.length < 10 ? "Token kh\xF4ng h\u1EE3p l\u1EC7" : true
68
+ },
69
+ // [MỚI] Câu hỏi Install
70
+ {
71
+ type: "confirm",
72
+ name: "shouldInstall",
73
+ message: "T\u1EF1 \u0111\u1ED9ng t\u1EA3i th\u01B0 vi\u1EC7n ngay b\xE2y gi\u1EDD? (M\u1EA5t v\xE0i ph\xFAt)",
74
+ initial: false
75
+ // Mặc định là KHÔNG
68
76
  }
69
77
  ]);
70
- const { projectName, templateType, token } = response;
78
+ const { projectName, templateType, token, shouldInstall } = response;
71
79
  if (!projectName || !templateType || !token) process.exit(1);
72
80
  const root = import_node_path.default.join(process.cwd(), projectName);
73
81
  const templateDir = import_node_path.default.resolve(__dirname, `../templates/${templateType}`);
@@ -94,29 +102,45 @@ async function init() {
94
102
  //npm.pkg.github.com/:_authToken=${token}
95
103
  `;
96
104
  import_fs_extra.default.writeFileSync(import_node_path.default.join(root, ".npmrc"), npmrcContent.trim());
97
- const gitignoreContent = `
98
- node_modules
99
- .npmrc
100
- .env
105
+ const gitignoreSrc = import_node_path.default.join(root, "_gitignore");
106
+ const gitignoreDest = import_node_path.default.join(root, ".gitignore");
107
+ if (import_fs_extra.default.existsSync(gitignoreSrc)) {
108
+ import_fs_extra.default.renameSync(gitignoreSrc, gitignoreDest);
109
+ } else {
110
+ const basicGitignore = `node_modules
101
111
  dist
102
- .DS_Store
103
- `.trim();
104
- import_fs_extra.default.writeFileSync(import_node_path.default.join(root, ".gitignore"), gitignoreContent);
105
- console.log(import_picocolors.default.blue("\u{1F4E6} \u0110ang c\xE0i \u0111\u1EB7t th\u01B0 vi\u1EC7n..."));
112
+ .npmrc
113
+ .env`;
114
+ import_fs_extra.default.writeFileSync(gitignoreDest, basicGitignore);
115
+ }
116
+ if (shouldInstall) {
117
+ console.log(import_picocolors.default.blue("\u{1F4E6} \u0110ang c\xE0i \u0111\u1EB7t th\u01B0 vi\u1EC7n..."));
118
+ try {
119
+ const userAgent2 = process.env.npm_config_user_agent || "";
120
+ const pkgManager2 = userAgent2.startsWith("pnpm") ? "pnpm" : "npm";
121
+ (0, import_node_child_process.execSync)(`${pkgManager2} install`, { cwd: root, stdio: "inherit" });
122
+ console.log(import_picocolors.default.green(`
123
+ \u2705 C\xE0i \u0111\u1EB7t ho\xE0n t\u1EA5t!`));
124
+ } catch (e) {
125
+ console.error(import_picocolors.default.red("\u274C L\u1ED7i khi c\xE0i \u0111\u1EB7t. Vui l\xF2ng th\u1EED ch\u1EA1y th\u1EE7 c\xF4ng."));
126
+ }
127
+ } else {
128
+ console.log(import_picocolors.default.yellow(`
129
+ \u26A0\uFE0F \u0110\xE3 b\u1ECF qua b\u01B0\u1EDBc c\xE0i \u0111\u1EB7t.`));
130
+ }
106
131
  try {
107
- const userAgent = process.env.npm_config_user_agent || "";
108
- const pkgManager = userAgent.startsWith("pnpm") ? "pnpm" : "npm";
109
- (0, import_node_child_process.execSync)(`${pkgManager} install`, { cwd: root, stdio: "inherit" });
110
132
  (0, import_node_child_process.execSync)("git init", { cwd: root, stdio: "ignore" });
111
- console.log(import_picocolors.default.green(`
112
- \u2705 Th\xE0nh c\xF4ng! Game ${templateType} \u0111\xE3 s\u1EB5n s\xE0ng.`));
113
- console.log(`
114
- \u{1F449} Ch\u1EA1y l\u1EC7nh sau \u0111\u1EC3 b\u1EAFt \u0111\u1EA7u:
115
- `);
116
- console.log(import_picocolors.default.cyan(` cd ${projectName}`));
117
- console.log(import_picocolors.default.cyan(` ${pkgManager} run dev`));
118
- } catch (error) {
119
- console.error(import_picocolors.default.red("\u274C L\u1ED7i c\xE0i \u0111\u1EB7t. Ki\u1EC3m tra l\u1EA1i Token."));
133
+ } catch {
134
+ }
135
+ console.log(`
136
+ \u{1F389} Project \u0111\xE3 s\u1EB5n s\xE0ng t\u1EA1i: ${import_picocolors.default.cyan(root)}`);
137
+ console.log(`\u{1F449} C\xE1c b\u01B0\u1EDBc ti\u1EBFp theo:`);
138
+ console.log(` cd ${projectName}`);
139
+ const userAgent = process.env.npm_config_user_agent || "";
140
+ const pkgManager = userAgent.startsWith("pnpm") ? "pnpm" : "npm";
141
+ if (!shouldInstall) {
142
+ console.log(` ${pkgManager} install`);
120
143
  }
144
+ console.log(` ${pkgManager} run dev`);
121
145
  }
122
146
  init().catch((e) => console.error(e));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iruka-edu/create-iruka-game",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Create Iruka mini game (React + Phaser template)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,29 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ # Dependencies
11
+ node_modules
12
+ dist
13
+ dist-ssr
14
+ *.local
15
+
16
+ # Editor directories and files
17
+ .vscode/*
18
+ !.vscode/extensions.json
19
+ .idea
20
+ .DS_Store
21
+ *.suo
22
+ *.ntvs
23
+ *.njsproj
24
+ *.sln
25
+ *.sw?
26
+
27
+ # Security [QUAN TRỌNG]
28
+ .env
29
+ .npmrc
@@ -6,7 +6,7 @@
6
6
  <title>Basic Phaser Game</title>
7
7
  </head>
8
8
  <body>
9
- <div id="game-container"></div>
9
+ <div id="app"></div>
10
10
  <script type="module" src="/src/main.ts"></script>
11
11
  </body>
12
12
  </html>
@@ -0,0 +1,61 @@
1
+ import {
2
+ connectToHub,
3
+ type GameBridge,
4
+ type LaunchContext,
5
+ } from "@iruka-edu/game-core";
6
+
7
+ class GameAdapter {
8
+ private static instance: GameAdapter;
9
+ private bridge: GameBridge | null = null;
10
+
11
+ // Singleton pattern để truy cập mọi nơi
12
+ public static getInstance(): GameAdapter {
13
+ if (!GameAdapter.instance) {
14
+ GameAdapter.instance = new GameAdapter();
15
+ }
16
+ return GameAdapter.instance;
17
+ }
18
+
19
+ // 1. Kết nối (Gọi 1 lần ở main.ts)
20
+ public async init(): Promise<LaunchContext> {
21
+ try {
22
+ this.bridge = await connectToHub();
23
+ console.log("🔌 Connected to Hub. Context:", this.bridge.context);
24
+ return this.bridge.context;
25
+ } catch (err) {
26
+ console.error("Failed to connect", err);
27
+ throw err;
28
+ }
29
+ }
30
+
31
+ // Helper lấy context hiện tại (difficulty, locale...)
32
+ public getContext(): LaunchContext | undefined {
33
+ return this.bridge?.context;
34
+ }
35
+
36
+ // 2. Báo cáo Game Ready (khi tải xong assets)
37
+ public notifyReady() {
38
+ this.bridge?.send("EVT:READY");
39
+ }
40
+
41
+ // 3. Gửi điểm số / Kết thúc game
42
+ public submitResult(score: number, maxScore: number, data?: any) {
43
+ // Tự động tính % hoàn thành
44
+ const accuracy = maxScore > 0 ? score / maxScore : 0;
45
+
46
+ this.bridge?.send("EVT:COMPLETE", {
47
+ score,
48
+ accuracy,
49
+ completion: 1.0, // Đã chơi xong
50
+ timestamp: Date.now(),
51
+ detail: data, // Metadata thêm (số câu đúng, sai...)
52
+ });
53
+ }
54
+
55
+ // 4. Lưu game (Throttle save sẽ được handle ở Core hoặc ở đây)
56
+ public saveState(state: any) {
57
+ this.bridge?.send("REQUEST_SAVE", state);
58
+ }
59
+ }
60
+
61
+ export const adapter = GameAdapter.getInstance();
@@ -0,0 +1,9 @@
1
+ export const mockContext = {
2
+ difficulty: "medium",
3
+ locale: "vi",
4
+ userData: {
5
+ id: "user_123",
6
+ name: "Iruka Player",
7
+ },
8
+ config: {},
9
+ };
@@ -1,30 +1,47 @@
1
1
  import Phaser from "phaser";
2
+ import "./style.css";
3
+ import { adapter } from "./core/GameAdapter";
4
+ import { BootScene } from "./scenes/BootScene";
5
+ import { PreloadScene } from "./scenes/PreloadScene";
6
+ import { GameScene } from "./scenes/GameScene";
7
+ import { GAME_WIDTH, GAME_HEIGHT } from "./shared/constants";
2
8
 
3
- class DemoScene extends Phaser.Scene {
4
- constructor() {
5
- super("DemoScene");
6
- }
9
+ // Config Phaser
10
+ const config: Phaser.Types.Core.GameConfig = {
11
+ type: Phaser.AUTO,
12
+ width: GAME_WIDTH,
13
+ height: GAME_HEIGHT,
14
+ parent: "app",
15
+ scale: {
16
+ mode: Phaser.Scale.FIT,
17
+ autoCenter: Phaser.Scale.CENTER_BOTH,
18
+ },
19
+ scene: [BootScene, PreloadScene, GameScene],
20
+ };
7
21
 
8
- preload() {
9
- // Preload assets here
10
- }
22
+ // Hàm khởi tạo chính
23
+ async function bootstrap() {
24
+ try {
25
+ console.log("🎮 Initializing Game...");
26
+
27
+ // 1. Đợi kết nối SDK (hoặc Mock nếu chạy local)
28
+ // SDK game-core sẽ tự động handle Mock Mode nếu không phát hiện được Hub
29
+ const context = await adapter.init();
11
30
 
12
- create() {
13
- this.add
14
- .text(400, 300, "Iruka Basic Phaser Template", {
15
- fontSize: "32px",
16
- color: "#00ff00",
17
- })
18
- .setOrigin(0.5);
31
+ // 2. Sau khi có context, mới start game
32
+ const game = new Phaser.Game(config);
33
+
34
+ // 3. Truyền context vào Global Registry của Phaser để các Scene dùng
35
+ game.registry.set("context", context);
36
+ } catch (error) {
37
+ console.error("Critical Error:", error);
38
+ document.body.innerHTML = `
39
+ <div style="color: white; text-align: center;">
40
+ <h1>Không thể tải game :(</h1>
41
+ <p>${error instanceof Error ? error.message : "Lỗi kết nối SDK"}</p>
42
+ </div>
43
+ `;
19
44
  }
20
45
  }
21
46
 
22
- const config: Phaser.Types.Core.GameConfig = {
23
- type: Phaser.AUTO,
24
- width: 800,
25
- height: 600,
26
- scene: DemoScene,
27
- parent: "game-container",
28
- };
29
-
30
- new Phaser.Game(config);
47
+ bootstrap();
@@ -0,0 +1,16 @@
1
+ import { Scene } from "phaser";
2
+ import { SCENES } from "../shared/constants";
3
+
4
+ export class BootScene extends Scene {
5
+ constructor() {
6
+ super(SCENES.BOOT);
7
+ }
8
+
9
+ preload() {
10
+ // Load minimal assets for loading screen if needed
11
+ }
12
+
13
+ create() {
14
+ this.scene.start(SCENES.PRELOAD);
15
+ }
16
+ }
@@ -0,0 +1,112 @@
1
+ import { Scene } from "phaser";
2
+ import { adapter } from "../core/GameAdapter";
3
+ import type { LaunchContext } from "@iruka-edu/game-core";
4
+ import { SCENES } from "../shared/constants";
5
+
6
+ export class GameScene extends Scene {
7
+ private context!: LaunchContext;
8
+ private score: number = 0;
9
+ private scoreText!: Phaser.GameObjects.Text;
10
+
11
+ constructor() {
12
+ super(SCENES.GAME);
13
+ }
14
+
15
+ init() {
16
+ // Lấy context đã lưu ở bước bootstrap trong main.ts
17
+ this.context = this.registry.get("context");
18
+ console.log("Game Mode:", this.context.difficulty);
19
+ }
20
+
21
+ create() {
22
+ const { width, height } = this.scale;
23
+
24
+ // Background color based on difficulty
25
+ const bgColors: Record<string, number> = {
26
+ easy: 0x27ae60,
27
+ medium: 0x2980b9,
28
+ hard: 0xc0392b,
29
+ };
30
+
31
+ this.add
32
+ .rectangle(
33
+ 0,
34
+ 0,
35
+ width,
36
+ height,
37
+ bgColors[this.context.difficulty || "medium"]
38
+ )
39
+ .setOrigin(0);
40
+
41
+ // Welcome text
42
+ this.add
43
+ .text(width / 2, 100, "Iruka Mini Game", {
44
+ fontSize: "48px",
45
+ color: "#ffffff",
46
+ })
47
+ .setOrigin(0.5);
48
+
49
+ this.add
50
+ .text(width / 2, 160, `Mode: ${this.context.difficulty?.toUpperCase()}`, {
51
+ fontSize: "24px",
52
+ color: "#ffffff",
53
+ })
54
+ .setOrigin(0.5);
55
+
56
+ // Add Logo
57
+ const logo = this.add.image(width / 2, height / 2, "logo");
58
+ logo.setInteractive();
59
+
60
+ // Score Text
61
+ this.scoreText = this.add
62
+ .text(width / 2, height - 100, `Score: ${this.score}`, {
63
+ fontSize: "32px",
64
+ color: "#ffffff",
65
+ })
66
+ .setOrigin(0.5);
67
+
68
+ // Instruction
69
+ this.add
70
+ .text(width / 2, height - 50, "Click logo to score & finish", {
71
+ fontSize: "18px",
72
+ color: "#ecf0f1",
73
+ })
74
+ .setOrigin(0.5);
75
+
76
+ // Interaction
77
+ logo.on("pointerdown", () => {
78
+ this.handleWin();
79
+ });
80
+
81
+ // Animation loop
82
+ this.tweens.add({
83
+ targets: logo,
84
+ y: height / 2 - 20,
85
+ duration: 1000,
86
+ ease: "Sine.easeInOut",
87
+ yoyo: true,
88
+ loop: -1,
89
+ });
90
+ }
91
+
92
+ private handleWin() {
93
+ this.score += 100;
94
+ this.scoreText.setText(`Score: ${this.score}`);
95
+
96
+ // Gửi kết quả về Hub
97
+ adapter.submitResult(this.score, 100, { stars: 3 });
98
+
99
+ // Feedback
100
+ this.add
101
+ .text(
102
+ this.scale.width / 2,
103
+ this.scale.height / 2 + 100,
104
+ "RESULT SUBMITTED!",
105
+ {
106
+ fontSize: "40px",
107
+ color: "#f1c40f",
108
+ }
109
+ )
110
+ .setOrigin(0.5);
111
+ }
112
+ }
@@ -0,0 +1,56 @@
1
+ import { Scene } from "phaser";
2
+ import { SCENES } from "../shared/constants";
3
+ import { adapter } from "../core/GameAdapter";
4
+
5
+ export class PreloadScene extends Scene {
6
+ constructor() {
7
+ super(SCENES.PRELOAD);
8
+ }
9
+
10
+ preload() {
11
+ // Create loading bar
12
+ const width = this.cameras.main.width;
13
+ const height = this.cameras.main.height;
14
+
15
+ const progressBar = this.add.graphics();
16
+ const progressBox = this.add.graphics();
17
+ progressBox.fillStyle(0x222222, 0.8);
18
+ progressBox.fillRect(width / 2 - 160, height / 2 - 25, 320, 50);
19
+
20
+ const loadingText = this.make.text({
21
+ x: width / 2,
22
+ y: height / 2 - 50,
23
+ text: "Loading...",
24
+ style: {
25
+ font: "20px monospace",
26
+ color: "#ffffff",
27
+ },
28
+ });
29
+ loadingText.setOrigin(0.5, 0.5);
30
+
31
+ this.load.on("progress", (value: number) => {
32
+ progressBar.clear();
33
+ progressBar.fillStyle(0xffffff, 1);
34
+ progressBar.fillRect(width / 2 - 150, height / 2 - 15, 300 * value, 30);
35
+ });
36
+
37
+ this.load.on("complete", () => {
38
+ progressBar.destroy();
39
+ progressBox.destroy();
40
+ loadingText.destroy();
41
+
42
+ // Báo cáo SDK Ready
43
+ adapter.notifyReady();
44
+ });
45
+
46
+ // Tải assets mẫu
47
+ this.load.image(
48
+ "logo",
49
+ "https://raw.githubusercontent.com/photonstorm/phaser3-examples/master/public/assets/sprites/phaser3-logo.png"
50
+ );
51
+ }
52
+
53
+ create() {
54
+ this.scene.start(SCENES.GAME);
55
+ }
56
+ }
@@ -0,0 +1,9 @@
1
+ export const SCENES = {
2
+ BOOT: "BootScene",
3
+ PRELOAD: "PreloadScene",
4
+ GAME: "GameScene",
5
+ RESULT: "ResultScene",
6
+ };
7
+
8
+ export const GAME_WIDTH = 1280;
9
+ export const GAME_HEIGHT = 720;
@@ -0,0 +1,21 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ overflow: hidden;
9
+ background-color: #000;
10
+ display: flex;
11
+ justify-content: center;
12
+ align-items: center;
13
+ height: 100vh;
14
+ width: 100vw;
15
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
16
+ }
17
+
18
+ #app {
19
+ width: 100%;
20
+ height: 100%;
21
+ }
@@ -0,0 +1,29 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ # Dependencies
11
+ node_modules
12
+ dist
13
+ dist-ssr
14
+ *.local
15
+
16
+ # Editor directories and files
17
+ .vscode/*
18
+ !.vscode/extensions.json
19
+ .idea
20
+ .DS_Store
21
+ *.suo
22
+ *.ntvs
23
+ *.njsproj
24
+ *.sln
25
+ *.sw?
26
+
27
+ # Security [QUAN TRỌNG]
28
+ .env
29
+ .npmrc