mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 17:53:26 +00:00
26f5d40819
- Replace Webpack with Vite for faster client bundling and HMR. - Migrate tests from Jest to Vitest and update configuration. - Update Web Worker instantiation to standard ESM syntax. - Implement Env utility in `src/core` for safe, hybrid environment variable access (Vite vs Node). - Refactor configuration loaders to remove direct `process.env` dependencies in shared code. - Update TypeScript environment definitions and project scripts for the new toolchain. - Remove the [depracated usage of the husky](https://github.com/typicode/husky/releases/tag/v9.0.1). ## Description: migrate build system to Vite and test runner to Vitest & Remove depracated husky usage ## 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 - [ ] 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: wraith4081 --------- Co-authored-by: evanpelle <evanpelle@gmail.com>
224 lines
7.1 KiB
TypeScript
224 lines
7.1 KiB
TypeScript
import { AllPlayers, nukeTypes } from "../../core/game/Game";
|
|
import { GameView, PlayerView } from "../../core/game/GameView";
|
|
import allianceIcon from "/images/AllianceIcon.svg?url";
|
|
import allianceIconFaded from "/images/AllianceIconFaded.svg?url";
|
|
import allianceRequestBlackIcon from "/images/AllianceRequestBlackIcon.svg?url";
|
|
import allianceRequestWhiteIcon from "/images/AllianceRequestWhiteIcon.svg?url";
|
|
import crownIcon from "/images/CrownIcon.svg?url";
|
|
import disconnectedIcon from "/images/DisconnectedIcon.svg?url";
|
|
import embargoBlackIcon from "/images/EmbargoBlackIcon.svg?url";
|
|
import embargoWhiteIcon from "/images/EmbargoWhiteIcon.svg?url";
|
|
import nukeRedIcon from "/images/NukeIconRed.svg?url";
|
|
import nukeWhiteIcon from "/images/NukeIconWhite.svg?url";
|
|
import questionMarkIcon from "/images/QuestionMarkIcon.svg?url";
|
|
import targetIcon from "/images/TargetIcon.svg?url";
|
|
import traitorIcon from "/images/TraitorIcon.svg?url";
|
|
|
|
export type PlayerIconId =
|
|
| "crown"
|
|
| "traitor"
|
|
| "disconnected"
|
|
| "alliance"
|
|
| "alliance-request"
|
|
| "target"
|
|
| "emoji"
|
|
| "embargo"
|
|
| "nuke";
|
|
|
|
export type PlayerIconKind = "image" | "emoji";
|
|
|
|
export interface PlayerIconDescriptor {
|
|
id: PlayerIconId;
|
|
kind: PlayerIconKind;
|
|
/** Image URL for image icons */
|
|
src?: string;
|
|
/** Text content for emoji icons */
|
|
text?: string;
|
|
/** Whether the icon should be visually centered over the name */
|
|
center?: boolean;
|
|
}
|
|
|
|
export interface PlayerIconParams {
|
|
game: GameView;
|
|
player: PlayerView;
|
|
/** Whether the alliance icon (handshake) should be included */
|
|
includeAllianceIcon: boolean;
|
|
/** Player currently in first place, used for the crown icon */
|
|
firstPlace: PlayerView | null;
|
|
}
|
|
|
|
export function getFirstPlacePlayer(game: GameView): PlayerView | null {
|
|
const sorted = game
|
|
.playerViews()
|
|
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
|
|
|
|
return sorted.length > 0 ? sorted[0] : null;
|
|
}
|
|
|
|
export function getPlayerIcons(
|
|
params: PlayerIconParams,
|
|
): PlayerIconDescriptor[] {
|
|
const { game, player, includeAllianceIcon, firstPlace } = params;
|
|
|
|
const myPlayer = game.myPlayer();
|
|
const userSettings = game.config().userSettings();
|
|
const isDarkMode = userSettings?.darkMode() ?? false;
|
|
const emojisEnabled = userSettings?.emojis() ?? false;
|
|
|
|
const icons: PlayerIconDescriptor[] = [];
|
|
|
|
// Crown icon for first place
|
|
if (player === firstPlace) {
|
|
icons.push({ id: "crown", kind: "image", src: crownIcon });
|
|
}
|
|
|
|
// Traitor icon
|
|
if (player.isTraitor()) {
|
|
icons.push({ id: "traitor", kind: "image", src: traitorIcon });
|
|
}
|
|
|
|
// Disconnected icon
|
|
if (player.isDisconnected()) {
|
|
icons.push({ id: "disconnected", kind: "image", src: disconnectedIcon });
|
|
}
|
|
|
|
// Alliance icon
|
|
if (
|
|
includeAllianceIcon &&
|
|
myPlayer !== null &&
|
|
myPlayer.isAlliedWith(player)
|
|
) {
|
|
icons.push({ id: "alliance", kind: "image", src: allianceIcon });
|
|
}
|
|
|
|
// Alliance request icon (theme dependent)
|
|
if (myPlayer !== null && player.isRequestingAllianceWith(myPlayer)) {
|
|
const allianceRequestIcon = isDarkMode
|
|
? allianceRequestWhiteIcon
|
|
: allianceRequestBlackIcon;
|
|
icons.push({
|
|
id: "alliance-request",
|
|
kind: "image",
|
|
src: allianceRequestIcon,
|
|
});
|
|
}
|
|
|
|
// Target icon (centered on the map, but regular in overlays)
|
|
if (myPlayer !== null && new Set(myPlayer.transitiveTargets()).has(player)) {
|
|
icons.push({ id: "target", kind: "image", src: targetIcon, center: true });
|
|
}
|
|
|
|
// Emoji handling
|
|
if (emojisEnabled) {
|
|
const emojis = player
|
|
.outgoingEmojis()
|
|
.filter(
|
|
(emoji) =>
|
|
emoji.recipientID === AllPlayers ||
|
|
emoji.recipientID === myPlayer?.smallID(),
|
|
);
|
|
|
|
if (emojis.length > 0) {
|
|
icons.push({
|
|
id: "emoji",
|
|
kind: "emoji",
|
|
text: emojis[0].message,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Embargo icon (theme dependent)
|
|
if (myPlayer?.hasEmbargo(player)) {
|
|
const embargoIcon = isDarkMode ? embargoWhiteIcon : embargoBlackIcon;
|
|
icons.push({ id: "embargo", kind: "image", src: embargoIcon });
|
|
}
|
|
|
|
// Nuke icon (different color depending on whether the local player is the target)
|
|
const nukesSentByOtherPlayer = game.units(...nukeTypes).filter((unit) => {
|
|
const isSendingNuke = player.id() === unit.owner().id();
|
|
const notMyPlayer = !myPlayer || unit.owner().id() !== myPlayer.id();
|
|
return isSendingNuke && notMyPlayer && unit.isActive();
|
|
});
|
|
|
|
const isMyPlayerTarget = nukesSentByOtherPlayer.some((unit) => {
|
|
const detonationDst = unit.targetTile();
|
|
if (!detonationDst || !myPlayer) return false;
|
|
const targetId = game.owner(detonationDst).id();
|
|
return targetId === myPlayer.id();
|
|
});
|
|
|
|
if (nukesSentByOtherPlayer.length > 0) {
|
|
const icon = isMyPlayerTarget ? nukeRedIcon : nukeWhiteIcon;
|
|
icons.push({ id: "nuke", kind: "image", src: icon });
|
|
}
|
|
|
|
return icons;
|
|
}
|
|
|
|
export function createAllianceProgressIcon(
|
|
size: number,
|
|
fraction: number,
|
|
hasExtensionRequest: boolean,
|
|
darkMode: boolean,
|
|
): HTMLDivElement {
|
|
// Wrapper
|
|
const wrapper = document.createElement("div");
|
|
wrapper.setAttribute("data-icon", "alliance");
|
|
wrapper.setAttribute("dark-mode", darkMode.toString());
|
|
wrapper.style.position = "relative";
|
|
wrapper.style.width = `${size}px`;
|
|
wrapper.style.height = `${size}px`;
|
|
wrapper.style.display = "inline-block";
|
|
wrapper.style.flexShrink = "0";
|
|
|
|
// Base faded icon (full)
|
|
const base = document.createElement("img");
|
|
base.src = allianceIconFaded;
|
|
base.style.width = `${size}px`;
|
|
base.style.height = `${size}px`;
|
|
base.style.display = "block";
|
|
base.setAttribute("dark-mode", darkMode.toString());
|
|
wrapper.appendChild(base);
|
|
|
|
// Overlay container for green portion, clipped from the top via clip-path
|
|
const overlay = document.createElement("div");
|
|
overlay.className = "alliance-progress-overlay";
|
|
overlay.style.position = "absolute";
|
|
overlay.style.left = "0";
|
|
overlay.style.top = "0";
|
|
overlay.style.width = "100%";
|
|
overlay.style.height = "100%";
|
|
overlay.style.clipPath = computeAllianceClipPath(fraction);
|
|
|
|
const colored = document.createElement("img");
|
|
colored.src = allianceIcon; // green icon
|
|
colored.style.width = `${size}px`;
|
|
colored.style.height = `${size}px`;
|
|
colored.style.display = "block";
|
|
colored.setAttribute("dark-mode", darkMode.toString());
|
|
overlay.appendChild(colored);
|
|
|
|
wrapper.appendChild(overlay);
|
|
|
|
// Question mark overlay (shown when there's a pending extension request)
|
|
const questionMark = document.createElement("img");
|
|
questionMark.className = "alliance-question-mark";
|
|
questionMark.src = questionMarkIcon;
|
|
questionMark.style.position = "absolute";
|
|
questionMark.style.left = "0";
|
|
questionMark.style.top = "0";
|
|
questionMark.style.width = `${size}px`;
|
|
questionMark.style.height = `${size}px`;
|
|
questionMark.style.display = hasExtensionRequest ? "block" : "none";
|
|
questionMark.style.pointerEvents = "none";
|
|
questionMark.setAttribute("dark-mode", darkMode.toString());
|
|
wrapper.appendChild(questionMark);
|
|
|
|
return wrapper;
|
|
}
|
|
|
|
export function computeAllianceClipPath(fraction: number): string {
|
|
const topCut = 20 + (1 - fraction) * 80 * 0.78; // min 20%, max 82.40%
|
|
return `inset(${topCut.toFixed(2)}% -2px 0 -2px)`;
|
|
}
|