feat: localize

This commit is contained in:
tiennm99 committed 2025-04-27 19:57:25 +07:00
1 parent f766f7e3b8
commit f7c504219a
8 files changed
+387 -53

No files matched your search

+174
View File
@@ -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 };
+4
View File
@@ -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() {
+29 -11
View File
@@ -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',
+15 -14
View File
@@ -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)
+11 -10
View File
@@ -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)
+33 -17
View File
@@ -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'
+119
View File
@@ -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'));
}
}
+2 -1
View File
@@ -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: {