Files
OpenFrontIO/src/client/Utils.ts
T
DevelopingTom 63a14738cd Redesign Player info overlay (#2000)
## 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>
2025-09-08 02:47:30 +00:00

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);
}
}