diff --git a/src/game/localization.js b/src/game/localization.js new file mode 100644 index 0000000..7f94f5f --- /dev/null +++ b/src/game/localization.js @@ -0,0 +1,174 @@ +// Localization system for Night Ninja: Twilight Voyage +// Supports English and Vietnamese languages + +// Default language is Vietnamese +let currentLanguage = 'vi'; + +// Text data for all UI elements in the game +const localizationData = { + // Main Menu + 'gameTitle': { + 'en': 'NIGHT NINJA: TWILIGHT VOYAGE', + 'vi': 'NINJA ĐÊM: HÀNH TRÌNH HOÀNG HÔN' + }, + 'startGame': { + 'en': 'START GAME', + 'vi': 'BẮT ĐẦU CHƠI' + }, + 'levelSelect': { + 'en': 'LEVEL SELECT', + 'vi': 'CHỌN CẤP ĐỘ' + }, + 'settings': { + 'en': 'SETTINGS', + 'vi': 'CÀI ĐẶT' + }, + 'gameDescription': { + 'en': 'Guide the ninja through the shadows.\nAvoid light at all costs.\nRescue the princess... if you can.', + 'vi': 'Dẫn dắt ninja qua bóng tối.\nTránh ánh sáng bằng mọi giá.\nCứu công chúa... nếu bạn có thể.' + }, + 'instructions': { + 'en': 'Use arrow keys, WASD, or tap to move.', + 'vi': 'Sử dụng phím mũi tên, WASD, hoặc chạm để di chuyển.' + }, + + // Settings Menu + 'languageSettings': { + 'en': 'Language', + 'vi': 'Ngôn ngữ' + }, + 'english': { + 'en': 'English', + 'vi': 'Tiếng Anh' + }, + 'vietnamese': { + 'en': 'Vietnamese', + 'vi': 'Tiếng Việt' + }, + 'back': { + 'en': 'Back', + 'vi': 'Quay lại' + }, + + // Level Select + 'levelSelectTitle': { + 'en': 'SELECT LEVEL', + 'vi': 'CHỌN CẤP ĐỘ' + }, + 'levelNumber': { + 'en': 'Level', + 'vi': 'Cấp độ' + }, + + // Game UI + 'lives': { + 'en': 'Lives: ', + 'vi': 'Mạng: ' + }, + 'level': { + 'en': 'Level: ', + 'vi': 'Cấp độ: ' + }, + 'pause': { + 'en': 'PAUSE', + 'vi': 'TẠM DỪNG' + }, + 'menu': { + 'en': 'MENU', + 'vi': 'MENU' + }, + 'paused': { + 'en': 'PAUSED', + 'vi': 'TẠM DỪNG' + }, + 'resume': { + 'en': 'RESUME', + 'vi': 'TIẾP TỤC' + }, + 'restartLevel': { + 'en': 'RESTART LEVEL', + 'vi': 'CHƠI LẠI CẤP ĐỘ' + }, + 'detected': { + 'en': 'You have been detected!', + 'vi': 'Bạn đã bị phát hiện!' + }, + 'playAgain': { + 'en': 'PLAY AGAIN', + 'vi': 'CHƠI LẠI' + }, + 'princessDetected': { + 'en': 'The princess has detected the ninja! The entire palace is lit up!', + 'vi': 'Công chúa phát hiện ra ninja! Toàn bộ cung điện sáng đèn!' + }, + + // Game Over + 'gameOver': { + 'en': 'GAME OVER', + 'vi': 'GAME KẾT THÚC' + }, + 'theEnd': { + 'en': 'THE END', + 'vi': 'KẾT THÚC' + }, + 'caughtInLight': { + 'en': 'You were caught in the light!', + 'vi': 'Bạn đã bị bắt trong ánh sáng!' + }, + 'ninjaFailed': { + 'en': 'Unfortunately, in this life, the ninja cannot rescue the princess.', + 'vi': 'Thật tiếc, kiếp này ninja không thể giải cứu công chúa rồi.' + }, + 'tryAgain': { + 'en': 'TRY AGAIN', + 'vi': 'THỬ LẠI' + }, + 'mainMenu': { + 'en': 'MAIN MENU', + 'vi': 'MENU CHÍNH' + } +}; + +// Function to get text in the current language +function getText(key) { + if (localizationData[key] && localizationData[key][currentLanguage]) { + return localizationData[key][currentLanguage]; + } + // Fallback to English if the key or language is not found + if (localizationData[key] && localizationData[key]['en']) { + return localizationData[key]['en']; + } + // Return the key itself if no translation is found + return key; +} + +// Function to change the language +function setLanguage(language) { + if (language === 'en' || language === 'vi') { + currentLanguage = language; + // Save the language preference to localStorage + if (typeof localStorage !== 'undefined') { + localStorage.setItem('nntv-language', language); + } + return true; + } + return false; +} + +// Function to get the current language +function getLanguage() { + return currentLanguage; +} + +// Initialize language from localStorage if available +function initLanguage() { + if (typeof localStorage !== 'undefined') { + const savedLanguage = localStorage.getItem('nntv-language'); + if (savedLanguage === 'en' || savedLanguage === 'vi') { + currentLanguage = savedLanguage; + } + } +} + +// Export the functions and data +export { getText, setLanguage, getLanguage, initLanguage }; diff --git a/src/game/scenes/Boot.js b/src/game/scenes/Boot.js index ab2edb5..443e1ce 100644 --- a/src/game/scenes/Boot.js +++ b/src/game/scenes/Boot.js @@ -1,4 +1,5 @@ import Phaser from 'phaser'; +import { initLanguage } from '../localization'; export class Boot extends Phaser.Scene { constructor() { @@ -8,6 +9,9 @@ export class Boot extends Phaser.Scene { preload() { // Load any assets needed for the preloader scene // For this game, we don't need any fancy preloader assets + + // Initialize language settings + initLanguage(); } create() { diff --git a/src/game/scenes/Game.js b/src/game/scenes/Game.js index 5030c53..ec4fd4c 100644 --- a/src/game/scenes/Game.js +++ b/src/game/scenes/Game.js @@ -5,6 +5,7 @@ import { TurnManager } from '../objects/TurnManager'; import { LightingSystem } from '../objects/LightingSystem'; import { StaticGuard, RotatingGuard, BlinkingGuard, PatrollingGuard } from '../objects/Guard'; import { LevelManager } from '../levels/LevelManager'; +import { getText } from '../localization'; export class Game extends Phaser.Scene { constructor() { @@ -186,13 +187,13 @@ export class Game extends Phaser.Scene { const width = this.cameras.main.width; // Lives display - this.livesText = this.add.text(20, 20, `Lives: ${this.livesRemaining}`, { + this.livesText = this.add.text(20, 20, `${getText('lives')}${this.livesRemaining}`, { font: '18px Arial', fill: '#ffffff' }); // Level display - this.levelText = this.add.text(width - 20, 20, `Level: ${this.currentLevel}`, { + this.levelText = this.add.text(width - 20, 20, `${getText('level')}${this.currentLevel}`, { font: '18px Arial', fill: '#ffffff' }); @@ -202,7 +203,7 @@ export class Game extends Phaser.Scene { this.pauseButton = this.add.rectangle(width - 20, 60, 100, 30, 0x444444); this.pauseButton.setOrigin(1, 0); - this.pauseText = this.add.text(width - 70, 75, 'PAUSE', { + this.pauseText = this.add.text(width - 70, 75, getText('pause'), { font: '16px Arial', fill: '#ffffff' }); @@ -213,6 +214,23 @@ export class Game extends Phaser.Scene { this.togglePause(); }); + // Return to Main Menu button + this.returnButton = this.add.rectangle(width - 20, 100, 100, 30, 0x444444); + this.returnButton.setOrigin(1, 0); + + this.returnText = this.add.text(width - 70, 115, getText('menu'), { + font: '16px Arial', + fill: '#ffffff' + }); + this.returnText.setOrigin(0.5, 0.5); + + this.returnButton.setInteractive({ useHandCursor: true }) + .on('pointerover', () => this.returnButton.fillColor = 0x666666) + .on('pointerout', () => this.returnButton.fillColor = 0x444444) + .on('pointerdown', () => { + this.scene.start('MainMenu'); + }); + // Create pause menu (initially hidden) this.createPauseMenu(); } @@ -228,7 +246,7 @@ export class Game extends Phaser.Scene { const bg = this.add.rectangle(0, 0, 300, 250, 0x000000, 0.8); // Title - const title = this.add.text(0, -100, 'PAUSED', { + const title = this.add.text(0, -100, getText('paused'), { font: 'bold 32px Arial', fill: '#ffffff' }); @@ -236,7 +254,7 @@ export class Game extends Phaser.Scene { // Resume button const resumeButton = this.add.rectangle(0, -40, 200, 40, 0x444444); - const resumeText = this.add.text(0, -40, 'RESUME', { + const resumeText = this.add.text(0, -40, getText('resume'), { font: '20px Arial', fill: '#ffffff' }); @@ -244,7 +262,7 @@ export class Game extends Phaser.Scene { // Restart button const restartButton = this.add.rectangle(0, 20, 200, 40, 0x444444); - const restartText = this.add.text(0, 20, 'RESTART LEVEL', { + const restartText = this.add.text(0, 20, getText('restartLevel'), { font: '20px Arial', fill: '#ffffff' }); @@ -252,7 +270,7 @@ export class Game extends Phaser.Scene { // Main menu button const menuButton = this.add.rectangle(0, 80, 200, 40, 0x444444); - const menuText = this.add.text(0, 80, 'MAIN MENU', { + const menuText = this.add.text(0, 80, getText('mainMenu'), { font: '20px Arial', fill: '#ffffff' }); @@ -413,7 +431,7 @@ export class Game extends Phaser.Scene { bg.popup = 'detection'; // Đánh dấu phần tử thuộc về popup // Thông báo - const title = this.add.text(0, -60, 'Bạn đã bị phát hiện!', { + const title = this.add.text(0, -60, getText('detected'), { font: 'bold 28px Arial', fill: '#FF0000' }); @@ -424,7 +442,7 @@ export class Game extends Phaser.Scene { const restartButton = this.add.rectangle(0, 20, 200, 40, 0x444444); restartButton.popup = 'detection'; // Đánh dấu phần tử thuộc về popup - const restartText = this.add.text(0, 20, 'CHƠI LẠI', { + const restartText = this.add.text(0, 20, getText('playAgain'), { font: '20px Arial', fill: '#ffffff' }); @@ -469,7 +487,7 @@ export class Game extends Phaser.Scene { // Cập nhật hiển thị số mạng if (this.livesText) { - this.livesText.setText(`Lives: ${this.livesRemaining}`); + this.livesText.setText(`${getText('lives')}${this.livesRemaining}`); } // Đảm bảo tất cả các popup đã được đóng @@ -546,7 +564,7 @@ export class Game extends Phaser.Scene { if (!this.finalLevelMessage) { this.finalLevelMessage = this.add.text(width / 2, height / 4, - 'Công chúa phát hiện ra ninja! Toàn bộ cung điện sáng đèn!', { + getText('princessDetected'), { font: 'bold 20px Arial', fill: '#FF0000', backgroundColor: '#000000', diff --git a/src/game/scenes/GameOver.js b/src/game/scenes/GameOver.js index e424cc5..7b42975 100644 --- a/src/game/scenes/GameOver.js +++ b/src/game/scenes/GameOver.js @@ -1,4 +1,5 @@ import Phaser from 'phaser'; +import { getText } from '../localization'; export class GameOver extends Phaser.Scene { constructor() { @@ -13,16 +14,16 @@ export class GameOver extends Phaser.Scene { create() { const width = this.cameras.main.width; const height = this.cameras.main.height; - + // Different message based on whether this is the final level or not - let message = 'GAME OVER'; - let description = 'You were caught in the light!'; - + let message = getText('gameOver'); + let description = getText('caughtInLight'); + if (this.isLastLevel) { - message = 'THE END'; - description = 'Thật tiếc, kiếp này ninja không thể giải cứu công chúa rồi.'; + message = getText('theEnd'); + description = getText('ninjaFailed'); } - + // Add game over title const title = this.add.text(width / 2, height / 3, message, { font: 'bold 48px Arial', @@ -30,7 +31,7 @@ export class GameOver extends Phaser.Scene { align: 'center' }); title.setOrigin(0.5, 0.5); - + // Add description const descText = this.add.text(width / 2, height / 2, description, { font: '24px Arial', @@ -38,15 +39,15 @@ export class GameOver extends Phaser.Scene { align: 'center' }); descText.setOrigin(0.5, 0.5); - + // Add restart button const restartButton = this.add.rectangle(width / 2, height * 0.7, 200, 50, 0x444444); - const restartText = this.add.text(width / 2, height * 0.7, 'TRY AGAIN', { + const restartText = this.add.text(width / 2, height * 0.7, getText('tryAgain'), { font: '24px Arial', fill: '#ffffff' }); restartText.setOrigin(0.5, 0.5); - + // Make button interactive restartButton.setInteractive({ useHandCursor: true }) .on('pointerover', () => restartButton.fillColor = 0x666666) @@ -60,15 +61,15 @@ export class GameOver extends Phaser.Scene { this.scene.start('Game', { level: this.finalLevel, lives: 3 }); } }); - + // Add main menu button const menuButton = this.add.rectangle(width / 2, height * 0.8, 200, 50, 0x444444); - const menuText = this.add.text(width / 2, height * 0.8, 'MAIN MENU', { + const menuText = this.add.text(width / 2, height * 0.8, getText('mainMenu'), { font: '24px Arial', fill: '#ffffff' }); menuText.setOrigin(0.5, 0.5); - + // Make button interactive menuButton.setInteractive({ useHandCursor: true }) .on('pointerover', () => menuButton.fillColor = 0x666666) diff --git a/src/game/scenes/LevelSelect.js b/src/game/scenes/LevelSelect.js index 8c6aba6..b33945b 100644 --- a/src/game/scenes/LevelSelect.js +++ b/src/game/scenes/LevelSelect.js @@ -1,4 +1,5 @@ import Phaser from 'phaser'; +import { getText } from '../localization'; export class LevelSelect extends Phaser.Scene { constructor() { @@ -9,29 +10,29 @@ export class LevelSelect extends Phaser.Scene { create() { const width = this.cameras.main.width; const height = this.cameras.main.height; - + // Add title - const title = this.add.text(width / 2, 50, 'SELECT LEVEL', { + const title = this.add.text(width / 2, 50, getText('levelSelectTitle'), { font: 'bold 32px Arial', fill: '#ffffff', align: 'center' }); title.setOrigin(0.5, 0.5); - + // Create level buttons const buttonSize = 70; const padding = 20; const buttonsPerRow = 4; const startX = width / 2 - ((buttonsPerRow - 1) * (buttonSize + padding)) / 2; const startY = 150; - + for (let i = 0; i < this.totalLevels; i++) { const row = Math.floor(i / buttonsPerRow); const col = i % buttonsPerRow; - + const x = startX + col * (buttonSize + padding); const y = startY + row * (buttonSize + padding); - + // Create button const levelButton = this.add.rectangle(x, y, buttonSize, buttonSize, 0x444444); const levelText = this.add.text(x, y, `${i + 1}`, { @@ -39,7 +40,7 @@ export class LevelSelect extends Phaser.Scene { fill: '#ffffff' }); levelText.setOrigin(0.5, 0.5); - + // Make button interactive levelButton.setInteractive({ useHandCursor: true }) .on('pointerover', () => levelButton.fillColor = 0x666666) @@ -48,15 +49,15 @@ export class LevelSelect extends Phaser.Scene { this.scene.start('Game', { level: i + 1, lives: 3 }); }); } - + // Add back button const backButton = this.add.rectangle(width / 2, height - 70, 200, 50, 0x444444); - const backText = this.add.text(width / 2, height - 70, 'BACK TO MENU', { + const backText = this.add.text(width / 2, height - 70, getText('back'), { font: '20px Arial', fill: '#ffffff' }); backText.setOrigin(0.5, 0.5); - + // Make back button interactive backButton.setInteractive({ useHandCursor: true }) .on('pointerover', () => backButton.fillColor = 0x666666) diff --git a/src/game/scenes/MainMenu.js b/src/game/scenes/MainMenu.js index c83f6c3..edc84ca 100644 --- a/src/game/scenes/MainMenu.js +++ b/src/game/scenes/MainMenu.js @@ -1,4 +1,5 @@ import Phaser from 'phaser'; +import { getText } from '../localization'; export class MainMenu extends Phaser.Scene { constructor() { @@ -8,34 +9,42 @@ export class MainMenu extends Phaser.Scene { create() { const width = this.cameras.main.width; const height = this.cameras.main.height; - + // Add game title - const title = this.add.text(width / 2, height / 4, 'NIGHT NINJA: TWILIGHT VOYAGE', { + const title = this.add.text(width / 2, height / 4, getText('gameTitle'), { font: 'bold 32px Arial', fill: '#ffffff', align: 'center' }); title.setOrigin(0.5, 0.5); - + // Add ninja icon const ninjaCircle = this.add.circle(width / 2, height / 2 - 50, 30, 0x000000); - + // Add start button - const startButton = this.add.rectangle(width / 2, height / 2 + 50, 200, 50, 0x444444); - const startText = this.add.text(width / 2, height / 2 + 50, 'START GAME', { + const startButton = this.add.rectangle(width / 2, height / 2 + 20, 200, 50, 0x444444); + const startText = this.add.text(width / 2, height / 2 + 20, getText('startGame'), { font: '24px Arial', fill: '#ffffff' }); startText.setOrigin(0.5, 0.5); - + // Add level select button - const levelButton = this.add.rectangle(width / 2, height / 2 + 120, 200, 50, 0x444444); - const levelText = this.add.text(width / 2, height / 2 + 120, 'LEVEL SELECT', { + const levelButton = this.add.rectangle(width / 2, height / 2 + 80, 200, 50, 0x444444); + const levelText = this.add.text(width / 2, height / 2 + 80, getText('levelSelect'), { font: '24px Arial', fill: '#ffffff' }); levelText.setOrigin(0.5, 0.5); - + + // Add settings button + const settingsButton = this.add.rectangle(width / 2, height / 2 + 140, 200, 50, 0x444444); + const settingsText = this.add.text(width / 2, height / 2 + 140, getText('settings'), { + font: '24px Arial', + fill: '#ffffff' + }); + settingsText.setOrigin(0.5, 0.5); + // Make buttons interactive startButton.setInteractive({ useHandCursor: true }) .on('pointerover', () => startButton.fillColor = 0x666666) @@ -43,26 +52,33 @@ export class MainMenu extends Phaser.Scene { .on('pointerdown', () => { this.scene.start('Game', { level: 1, lives: 3 }); }); - + levelButton.setInteractive({ useHandCursor: true }) .on('pointerover', () => levelButton.fillColor = 0x666666) .on('pointerout', () => levelButton.fillColor = 0x444444) .on('pointerdown', () => { this.scene.start('LevelSelect'); }); - + + settingsButton.setInteractive({ useHandCursor: true }) + .on('pointerover', () => settingsButton.fillColor = 0x666666) + .on('pointerout', () => settingsButton.fillColor = 0x444444) + .on('pointerdown', () => { + this.scene.start('Settings'); + }); + // Add game description - const description = this.add.text(width / 2, height * 0.8, - 'Guide the ninja through the shadows.\nAvoid light at all costs.\nRescue the princess... if you can.', { + const description = this.add.text(width / 2, height * 0.8, + getText('gameDescription'), { font: '18px Arial', fill: '#cccccc', align: 'center' }); description.setOrigin(0.5, 0.5); - + // Add instructions - const instructions = this.add.text(width / 2, height * 0.9, - 'Use arrow keys, WASD, or tap to move.', { + const instructions = this.add.text(width / 2, height * 0.9, + getText('instructions'), { font: '16px Arial', fill: '#aaaaaa', align: 'center' diff --git a/src/game/scenes/Settings.js b/src/game/scenes/Settings.js new file mode 100644 index 0000000..0fe3069 --- /dev/null +++ b/src/game/scenes/Settings.js @@ -0,0 +1,119 @@ +import Phaser from 'phaser'; +import { getText, setLanguage, getLanguage } from '../localization'; + +export class Settings extends Phaser.Scene { + constructor() { + super('Settings'); + } + + create() { + const width = this.cameras.main.width; + const height = this.cameras.main.height; + + // Add settings title + const title = this.add.text(width / 2, height / 4, getText('settings'), { + font: 'bold 32px Arial', + fill: '#ffffff', + align: 'center' + }); + title.setOrigin(0.5, 0.5); + + // Add language settings label + const languageLabel = this.add.text(width / 2, height / 2 - 50, getText('languageSettings'), { + font: '24px Arial', + fill: '#ffffff' + }); + languageLabel.setOrigin(0.5, 0.5); + + // Add language options + const currentLanguage = getLanguage(); + + // English button + const englishButton = this.add.rectangle(width / 2 - 80, height / 2, 150, 50, + currentLanguage === 'en' ? 0x666666 : 0x444444); + const englishText = this.add.text(width / 2 - 80, height / 2, getText('english'), { + font: '20px Arial', + fill: '#ffffff' + }); + englishText.setOrigin(0.5, 0.5); + + // Vietnamese button + const vietnameseButton = this.add.rectangle(width / 2 + 80, height / 2, 150, 50, + currentLanguage === 'vi' ? 0x666666 : 0x444444); + const vietnameseText = this.add.text(width / 2 + 80, height / 2, getText('vietnamese'), { + font: '20px Arial', + fill: '#ffffff' + }); + vietnameseText.setOrigin(0.5, 0.5); + + // Back button + const backButton = this.add.rectangle(width / 2, height / 2 + 100, 200, 50, 0x444444); + const backText = this.add.text(width / 2, height / 2 + 100, getText('back'), { + font: '24px Arial', + fill: '#ffffff' + }); + backText.setOrigin(0.5, 0.5); + + // Make buttons interactive + englishButton.setInteractive({ useHandCursor: true }) + .on('pointerover', () => { + if (currentLanguage !== 'en') englishButton.fillColor = 0x555555; + }) + .on('pointerout', () => { + if (currentLanguage !== 'en') englishButton.fillColor = 0x444444; + }) + .on('pointerdown', () => { + this.changeLanguage('en'); + }); + + vietnameseButton.setInteractive({ useHandCursor: true }) + .on('pointerover', () => { + if (currentLanguage !== 'vi') vietnameseButton.fillColor = 0x555555; + }) + .on('pointerout', () => { + if (currentLanguage !== 'vi') vietnameseButton.fillColor = 0x444444; + }) + .on('pointerdown', () => { + this.changeLanguage('vi'); + }); + + backButton.setInteractive({ useHandCursor: true }) + .on('pointerover', () => backButton.fillColor = 0x666666) + .on('pointerout', () => backButton.fillColor = 0x444444) + .on('pointerdown', () => { + this.scene.start('MainMenu'); + }); + + // Store UI elements that need to be updated when language changes + this.uiElements = { + title, + languageLabel, + englishText, + vietnameseText, + backText, + englishButton, + vietnameseButton + }; + } + + changeLanguage(language) { + // Only update if the language actually changed + if (getLanguage() !== language) { + setLanguage(language); + this.updateUIText(); + + // Update button colors + this.uiElements.englishButton.fillColor = language === 'en' ? 0x666666 : 0x444444; + this.uiElements.vietnameseButton.fillColor = language === 'vi' ? 0x666666 : 0x444444; + } + } + + updateUIText() { + // Update all text elements with the new language + this.uiElements.title.setText(getText('settings')); + this.uiElements.languageLabel.setText(getText('languageSettings')); + this.uiElements.englishText.setText(getText('english')); + this.uiElements.vietnameseText.setText(getText('vietnamese')); + this.uiElements.backText.setText(getText('back')); + } +} diff --git a/src/main.js b/src/main.js index 14d03a1..9f85b3b 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,7 @@ import { MainMenu } from './game/scenes/MainMenu'; import { LevelSelect } from './game/scenes/LevelSelect'; import { Game } from './game/scenes/Game'; import { GameOver } from './game/scenes/GameOver'; +import { Settings } from './game/scenes/Settings'; const config = { type: Phaser.AUTO, @@ -12,7 +13,7 @@ const config = { height: 600, parent: 'app', backgroundColor: '#333333', - scene: [Boot, Preloader, MainMenu, LevelSelect, Game, GameOver], + scene: [Boot, Preloader, MainMenu, LevelSelect, Game, GameOver, Settings], physics: { default: 'arcade', arcade: {