mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 02:27:43 +00:00
63a14738cd
## Description: Redesign the player info panel to match the bottom panel. Changes: - Added alliance timeout - Various css restyling Old: <img width="180" height="276" alt="image" src="https://github.com/user-attachments/assets/4ae8994b-868c-4eb8-b42a-85f0f0ec2f96" /> New: <img width="179" height="239" alt="image" src="https://github.com/user-attachments/assets/c29c34e5-5bfd-468e-9947-e0ac319fbccf" /> ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom --------- Co-authored-by: evanpelle <evanpelle@gmail.com>
238 lines
6.7 KiB
TypeScript
238 lines
6.7 KiB
TypeScript
import IntlMessageFormat from "intl-messageformat";
|
|
import { MessageType } from "../core/game/Game";
|
|
import { LangSelector } from "./LangSelector";
|
|
|
|
export function renderDuration(totalSeconds: number): string {
|
|
if (totalSeconds <= 0) return "0s";
|
|
const minutes = Math.floor(totalSeconds / 60);
|
|
const seconds = totalSeconds % 60;
|
|
let time = "";
|
|
if (minutes > 0) time += `${minutes}min `;
|
|
time += `${seconds}s`;
|
|
return time.trim();
|
|
}
|
|
|
|
export function renderTroops(troops: number): string {
|
|
return renderNumber(troops / 10);
|
|
}
|
|
|
|
export function renderNumber(
|
|
num: number | bigint,
|
|
fixedPoints?: number,
|
|
): string {
|
|
num = Number(num);
|
|
num = Math.max(num, 0);
|
|
|
|
if (num >= 10_000_000) {
|
|
const value = Math.floor(num / 100000) / 10;
|
|
return value.toFixed(fixedPoints ?? 1) + "M";
|
|
} else if (num >= 1_000_000) {
|
|
const value = Math.floor(num / 10000) / 100;
|
|
return value.toFixed(fixedPoints ?? 2) + "M";
|
|
} else if (num >= 100000) {
|
|
return Math.floor(num / 1000) + "K";
|
|
} else if (num >= 10000) {
|
|
const value = Math.floor(num / 100) / 10;
|
|
return value.toFixed(fixedPoints ?? 1) + "K";
|
|
} else if (num >= 1000) {
|
|
const value = Math.floor(num / 10) / 100;
|
|
return value.toFixed(fixedPoints ?? 2) + "K";
|
|
} else {
|
|
return Math.floor(num).toString();
|
|
}
|
|
}
|
|
|
|
export function createCanvas(): HTMLCanvasElement {
|
|
const canvas = document.createElement("canvas");
|
|
|
|
// Set canvas style to fill the screen
|
|
canvas.style.position = "fixed";
|
|
canvas.style.left = "0";
|
|
canvas.style.top = "0";
|
|
canvas.style.width = "100%";
|
|
canvas.style.height = "100%";
|
|
canvas.style.touchAction = "none";
|
|
|
|
return canvas;
|
|
}
|
|
/**
|
|
* A polyfill for crypto.randomUUID that provides fallback implementations
|
|
* for older browsers, particularly Safari versions < 15.4
|
|
*/
|
|
export function generateCryptoRandomUUID(): string {
|
|
// Type guard to check if randomUUID is available
|
|
if (crypto !== undefined && "randomUUID" in crypto) {
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
// Fallback using crypto.getRandomValues
|
|
if (crypto !== undefined && "getRandomValues" in crypto) {
|
|
return (([1e7] as any) + -1e3 + -4e3 + -8e3 + -1e11).replace(
|
|
/[018]/g,
|
|
(c: number): string =>
|
|
(
|
|
c ^
|
|
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
|
|
).toString(16),
|
|
);
|
|
}
|
|
|
|
// Last resort fallback using Math.random
|
|
// Note: This is less cryptographically secure but ensures functionality
|
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
|
|
/[xy]/g,
|
|
(c: string): string => {
|
|
const r: number = (Math.random() * 16) | 0;
|
|
const v: number = c === "x" ? r : (r & 0x3) | 0x8;
|
|
return v.toString(16);
|
|
},
|
|
);
|
|
}
|
|
|
|
export const translateText = (
|
|
key: string,
|
|
params: Record<string, string | number> = {},
|
|
): string => {
|
|
const self = translateText as any;
|
|
self.formatterCache ??= new Map();
|
|
self.lastLang ??= null;
|
|
|
|
const langSelector = document.querySelector("lang-selector") as LangSelector;
|
|
if (!langSelector) {
|
|
console.warn("LangSelector not found in DOM");
|
|
return key;
|
|
}
|
|
|
|
if (
|
|
!langSelector.translations ||
|
|
Object.keys(langSelector.translations).length === 0
|
|
) {
|
|
return key;
|
|
}
|
|
|
|
if (self.lastLang !== langSelector.currentLang) {
|
|
self.formatterCache.clear();
|
|
self.lastLang = langSelector.currentLang;
|
|
}
|
|
|
|
let message = langSelector.translations[key];
|
|
|
|
if (!message && langSelector.defaultTranslations) {
|
|
const defaultTranslations = langSelector.defaultTranslations;
|
|
if (defaultTranslations && defaultTranslations[key]) {
|
|
message = defaultTranslations[key];
|
|
}
|
|
}
|
|
|
|
if (!message) return key;
|
|
|
|
try {
|
|
const locale =
|
|
!langSelector.translations[key] && langSelector.currentLang !== "en"
|
|
? "en"
|
|
: langSelector.currentLang;
|
|
const cacheKey = `${key}:${locale}:${message}`;
|
|
let formatter = self.formatterCache.get(cacheKey);
|
|
|
|
if (!formatter) {
|
|
formatter = new IntlMessageFormat(message, locale);
|
|
self.formatterCache.set(cacheKey, formatter);
|
|
}
|
|
|
|
return formatter.format(params) as string;
|
|
} catch (e) {
|
|
console.warn("ICU format error", e);
|
|
return message;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Severity colors mapping for message types
|
|
*/
|
|
export const severityColors: Record<string, string> = {
|
|
fail: "text-red-400",
|
|
warn: "text-yellow-400",
|
|
success: "text-green-400",
|
|
info: "text-gray-200",
|
|
blue: "text-blue-400",
|
|
white: "text-white",
|
|
};
|
|
|
|
/**
|
|
* Gets the CSS classes for styling message types based on their severity
|
|
* @param type The message type to get styling for
|
|
* @returns CSS class string for the message type
|
|
*/
|
|
export function getMessageTypeClasses(type: MessageType): string {
|
|
switch (type) {
|
|
case MessageType.SAM_HIT:
|
|
case MessageType.CAPTURED_ENEMY_UNIT:
|
|
case MessageType.RECEIVED_GOLD_FROM_TRADE:
|
|
case MessageType.CONQUERED_PLAYER:
|
|
return severityColors["success"];
|
|
case MessageType.ATTACK_FAILED:
|
|
case MessageType.ALLIANCE_REJECTED:
|
|
case MessageType.ALLIANCE_BROKEN:
|
|
case MessageType.UNIT_CAPTURED_BY_ENEMY:
|
|
case MessageType.UNIT_DESTROYED:
|
|
return severityColors["fail"];
|
|
case MessageType.ATTACK_CANCELLED:
|
|
case MessageType.ATTACK_REQUEST:
|
|
case MessageType.ALLIANCE_ACCEPTED:
|
|
case MessageType.SENT_GOLD_TO_PLAYER:
|
|
case MessageType.SENT_TROOPS_TO_PLAYER:
|
|
case MessageType.RECEIVED_GOLD_FROM_PLAYER:
|
|
case MessageType.RECEIVED_TROOPS_FROM_PLAYER:
|
|
return severityColors["blue"];
|
|
case MessageType.MIRV_INBOUND:
|
|
case MessageType.NUKE_INBOUND:
|
|
case MessageType.HYDROGEN_BOMB_INBOUND:
|
|
case MessageType.SAM_MISS:
|
|
case MessageType.ALLIANCE_EXPIRED:
|
|
case MessageType.NAVAL_INVASION_INBOUND:
|
|
case MessageType.RENEW_ALLIANCE:
|
|
return severityColors["warn"];
|
|
case MessageType.CHAT:
|
|
case MessageType.ALLIANCE_REQUEST:
|
|
return severityColors["info"];
|
|
default:
|
|
console.warn(`Message type ${type} has no explicit color`);
|
|
return severityColors["white"];
|
|
}
|
|
}
|
|
|
|
export function getModifierKey(): string {
|
|
const isMac = /Mac/.test(navigator.userAgent);
|
|
if (isMac) {
|
|
return "⌘"; // Command key
|
|
} else {
|
|
return "Ctrl";
|
|
}
|
|
}
|
|
|
|
export function getAltKey(): string {
|
|
const isMac = /Mac/.test(navigator.userAgent);
|
|
if (isMac) {
|
|
return "⌥"; // Option key
|
|
} else {
|
|
return "Alt";
|
|
}
|
|
}
|
|
|
|
export function getGamesPlayed(): number {
|
|
try {
|
|
return parseInt(localStorage.getItem("gamesPlayed") ?? "0", 10) || 0;
|
|
} catch (error) {
|
|
console.warn("Failed to read games played from localStorage:", error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
export function incrementGamesPlayed(): void {
|
|
try {
|
|
localStorage.setItem("gamesPlayed", (getGamesPlayed() + 1).toString());
|
|
} catch (error) {
|
|
console.warn("Failed to increment games played in localStorage:", error);
|
|
}
|
|
}
|