mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:20:45 +00:00
Merge branch 'v26'
This commit is contained in:
@@ -157,6 +157,7 @@
|
||||
"account_modal": {
|
||||
"title": "Account",
|
||||
"logged_in_as": "Logged in as {email}",
|
||||
"fetching_account": "Fetching account information...",
|
||||
"logged_in_with_discord": "Logged in with Discord",
|
||||
"recovery_email_sent": "Recovery email sent to {email}"
|
||||
},
|
||||
|
||||
+71
-7
@@ -130,9 +130,8 @@
|
||||
"disable_nations": "Deaktiviraj nacije",
|
||||
"instant_build": "Instantna gradnja",
|
||||
"infinite_gold": "Neomejeno zlata",
|
||||
"donate_gold": "Doniraj zlato",
|
||||
"infinite_troops": "Neomejene enote",
|
||||
"donate_troops": "Doniraj enote",
|
||||
"compact_map": "Mini mapa",
|
||||
"disable_nukes": "Deaktiviraj bombe",
|
||||
"enables_title": "Aktiviraj nastavitve",
|
||||
"start": "Začni igro"
|
||||
@@ -177,8 +176,10 @@
|
||||
"halkidiki": "Halkidiki",
|
||||
"straitofgibraltar": "Gibraltarska ožina",
|
||||
"italia": "Italija",
|
||||
"japan": "Japonska",
|
||||
"yenisei": "Jenisej",
|
||||
"pluto": "Pluto"
|
||||
"pluto": "Pluto",
|
||||
"montreal": "Montreal"
|
||||
},
|
||||
"map_categories": {
|
||||
"continental": "Celinska",
|
||||
@@ -196,8 +197,9 @@
|
||||
"join_lobby": "Pridruži se sobi",
|
||||
"checking": "Preverjanje sobe...",
|
||||
"not_found": "Sobe ni mogoče najti. Prosimo preverite ID in poskusite znova.",
|
||||
"error": "Prišlo je do napake. Prosimo, poskusite znova.",
|
||||
"joined_waiting": "Uspešna pridružitev! Čakanje na začetek igre..."
|
||||
"error": "Prišlo je do napake. Poskusite znova ali se obrnite na podporo.",
|
||||
"joined_waiting": "Uspešna pridružitev! Čakanje na začetek igre...",
|
||||
"version_mismatch": "Ta soba je bila ustvarjena z drugo verzijo igre. Napaka pridružitve."
|
||||
},
|
||||
"public_lobby": {
|
||||
"join": "Pridruži se naslednji igri",
|
||||
@@ -227,6 +229,7 @@
|
||||
"donate_gold": "Doniraj zlato",
|
||||
"infinite_troops": "Neomejene enote",
|
||||
"donate_troops": "Doniraj enote",
|
||||
"compact_map": "Mini mapa",
|
||||
"enables_title": "Aktiviraj nastavitve",
|
||||
"player": "Igralec",
|
||||
"players": "Igralci",
|
||||
@@ -314,6 +317,8 @@
|
||||
"territory_patterns_desc": "Izberite, ali želite v igri prikazati vzorce ozemlja",
|
||||
"performance_overlay_label": "Prekrivni sloj zmogljivosti",
|
||||
"performance_overlay_desc": "Preklopi prekrivni sloj zmogljivosti. Ko je omogočen, bo prikazan prekrivni sloj zmogljivosti. Med igro pritisnite shift-D za preklop.",
|
||||
"performance_overlay_enabled": "Prekrivanje zmogljivosti je omogočeno",
|
||||
"performance_overlay_disabled": "Prekrivanje zmogljivosti je onemogočeno",
|
||||
"easter_writing_speed_label": "Množitelj hitrosti pisanja",
|
||||
"easter_writing_speed_desc": "Prilagodite hitrost pretvarjanja, da programirate (x1–x100)",
|
||||
"easter_bug_count_label": "Število hroščev",
|
||||
@@ -494,7 +499,8 @@
|
||||
"nation": "Narod",
|
||||
"player": "Igralec",
|
||||
"team": "Ekipa",
|
||||
"d_troops": "Obrambne enote",
|
||||
"alliance_timeout": "Zavezništvo poteče čez",
|
||||
"troops": "Enote",
|
||||
"a_troops": "Napadalne enote",
|
||||
"gold": "Zlato",
|
||||
"ports": "Luka",
|
||||
@@ -585,7 +591,7 @@
|
||||
"choose_spawn": "Izberite začetno lokacijo"
|
||||
},
|
||||
"territory_patterns": {
|
||||
"title": "Izberite vzorec ozemlja",
|
||||
"title": "Izberi poslikavo območja",
|
||||
"purchase": "Nakup",
|
||||
"blocked": {
|
||||
"login": "Za dostop do tega vzorca morate biti prijavljeni.",
|
||||
@@ -644,5 +650,63 @@
|
||||
"radial_menu": {
|
||||
"delete_unit_title": "Pobriši enoto",
|
||||
"delete_unit_description": "Pritisni za odstranitev najbližjih enot"
|
||||
},
|
||||
"discord_user_header": {
|
||||
"avatar_alt": "Avatar"
|
||||
},
|
||||
"player_stats_table": {
|
||||
"building_stats": "Statistika zgradbe",
|
||||
"ship_arrivals": "Prihajajoče ladje",
|
||||
"nuke_stats": "Statistika bomb",
|
||||
"player_metrics": "Metrika igralca",
|
||||
"building": "Zgradba",
|
||||
"ship_type": "Vrsta ladje",
|
||||
"weapon": "Orožje",
|
||||
"built": "Zgrajeno",
|
||||
"destroyed": "Uničeno",
|
||||
"captured": "Ujeto",
|
||||
"lost": "Zgubljeno",
|
||||
"hits": "Zadetki",
|
||||
"launched": "Izstreljeno",
|
||||
"landed": "Pristanek",
|
||||
"sent": "Poslano",
|
||||
"arrived": "Prišlo",
|
||||
"attack": "Napad",
|
||||
"received": "Prejeto",
|
||||
"cancelled": "Prekinjeno",
|
||||
"count": "Število",
|
||||
"gold": "Zlato",
|
||||
"workers": "Delavci",
|
||||
"war": "Vojna",
|
||||
"trade": "Trgovanje",
|
||||
"steal": "Kraja",
|
||||
"unit": {
|
||||
"city": "Mesto",
|
||||
"port": "Luka",
|
||||
"defp": "Obrambni stolp",
|
||||
"saml": "SAM Izstreljevalec",
|
||||
"silo": "Izstreljevalec raket",
|
||||
"wshp": "Bojna ladja",
|
||||
"fact": "Tovarna",
|
||||
"trade": "Trgovska ladja",
|
||||
"trans": "Transportna ladja",
|
||||
"abomb": "Atomska bomba",
|
||||
"hbomb": "Vodikova bomba",
|
||||
"mirv": "MIRV",
|
||||
"mirvw": "MIRV bojna glava"
|
||||
}
|
||||
},
|
||||
"game_list": {
|
||||
"recent_games": "Nedavne igre",
|
||||
"game_id": "ID sobe",
|
||||
"mode": "Način",
|
||||
"mode_ffa": "Prosto za vse",
|
||||
"mode_team": "Ekipe",
|
||||
"view": "Poglej",
|
||||
"details": "Detajli",
|
||||
"started": "Začeto",
|
||||
"map": "Mapa",
|
||||
"difficulty": "Težavnost",
|
||||
"type": "Tip"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +130,8 @@
|
||||
"disable_nations": "禁用国家",
|
||||
"instant_build": "立即建造",
|
||||
"infinite_gold": "无限黄金",
|
||||
"donate_gold": "捐赠金币",
|
||||
"infinite_troops": "无限军队",
|
||||
"donate_troops": "捐赠军队",
|
||||
"compact_map": "小地图",
|
||||
"disable_nukes": "禁用核弹",
|
||||
"enables_title": "启用设置",
|
||||
"start": "开始游戏"
|
||||
@@ -177,8 +176,10 @@
|
||||
"halkidiki": "哈尔基季基",
|
||||
"straitofgibraltar": "直布罗陀海峡",
|
||||
"italia": "意大利",
|
||||
"japan": "日本",
|
||||
"yenisei": "叶尼塞河",
|
||||
"pluto": "冥王星"
|
||||
"pluto": "冥王星",
|
||||
"montreal": "蒙特利尔"
|
||||
},
|
||||
"map_categories": {
|
||||
"continental": "大陆",
|
||||
@@ -196,8 +197,9 @@
|
||||
"join_lobby": "加入房间",
|
||||
"checking": "正在确认房间...",
|
||||
"not_found": "找不到房间。请检查 ID 然后重试。",
|
||||
"error": "发生了错误,请再试一次。",
|
||||
"joined_waiting": "加入成功!正在等待游戏开始..."
|
||||
"error": "发生错误。请再试一次或联系支持人员。",
|
||||
"joined_waiting": "加入成功!正在等待游戏开始...",
|
||||
"version_mismatch": "这场游戏基于另一个版本,无法加入。"
|
||||
},
|
||||
"public_lobby": {
|
||||
"join": "加入下一场游戏",
|
||||
@@ -227,6 +229,7 @@
|
||||
"donate_gold": "捐赠金币",
|
||||
"infinite_troops": "无限军队",
|
||||
"donate_troops": "捐赠军队",
|
||||
"compact_map": "小地图",
|
||||
"enables_title": "启用设置",
|
||||
"player": "玩家",
|
||||
"players": "玩家",
|
||||
@@ -314,6 +317,8 @@
|
||||
"territory_patterns_desc": "选择是否在游戏中显示领土皮肤",
|
||||
"performance_overlay_label": "性能叠层",
|
||||
"performance_overlay_desc": "切换性能叠层。启用后将显示性能叠层。在游戏过程中按下 Shift+D 可进行切换。",
|
||||
"performance_overlay_enabled": "已启用性能叠层",
|
||||
"performance_overlay_disabled": "已禁用性能叠层",
|
||||
"easter_writing_speed_label": "写入速度乘数",
|
||||
"easter_writing_speed_desc": "调节你“假装写代码”的速度 (x1–x100)",
|
||||
"easter_bug_count_label": "Bug 计数",
|
||||
@@ -494,7 +499,8 @@
|
||||
"nation": "国家",
|
||||
"player": "玩家",
|
||||
"team": "队伍",
|
||||
"d_troops": "防守军队",
|
||||
"alliance_timeout": "结盟剩余时长",
|
||||
"troops": "军队",
|
||||
"a_troops": "进攻军队",
|
||||
"gold": "黄金",
|
||||
"ports": "港口",
|
||||
@@ -541,7 +547,7 @@
|
||||
},
|
||||
"relation": {
|
||||
"hostile": "敌对",
|
||||
"distrustful": "不可信",
|
||||
"distrustful": "可疑",
|
||||
"neutral": "中立",
|
||||
"friendly": "友好",
|
||||
"default": "默认"
|
||||
@@ -644,5 +650,63 @@
|
||||
"radial_menu": {
|
||||
"delete_unit_title": "删除单位",
|
||||
"delete_unit_description": "点击删除最近的单位"
|
||||
},
|
||||
"discord_user_header": {
|
||||
"avatar_alt": "头像"
|
||||
},
|
||||
"player_stats_table": {
|
||||
"building_stats": "建筑统计",
|
||||
"ship_arrivals": "船只抵达",
|
||||
"nuke_stats": "核弹统计",
|
||||
"player_metrics": "玩家指标",
|
||||
"building": "建筑",
|
||||
"ship_type": "船只类型",
|
||||
"weapon": "武器",
|
||||
"built": "已建造",
|
||||
"destroyed": "已摧毁",
|
||||
"captured": "已捕获",
|
||||
"lost": "损失",
|
||||
"hits": "击中",
|
||||
"launched": "已发射",
|
||||
"landed": "已登陆",
|
||||
"sent": "已发送",
|
||||
"arrived": "已抵达",
|
||||
"attack": "攻击",
|
||||
"received": "已收到",
|
||||
"cancelled": "已取消",
|
||||
"count": "总计",
|
||||
"gold": "黄金",
|
||||
"workers": "工人",
|
||||
"war": "战争",
|
||||
"trade": "交易",
|
||||
"steal": "偷窃",
|
||||
"unit": {
|
||||
"city": "城市",
|
||||
"port": "港口",
|
||||
"defp": "防守据点",
|
||||
"saml": "防空塔",
|
||||
"silo": "导弹发射井",
|
||||
"wshp": "军舰",
|
||||
"fact": "工厂",
|
||||
"trade": "贸易船",
|
||||
"trans": "运输船",
|
||||
"abomb": "原子弹",
|
||||
"hbomb": "氢弹",
|
||||
"mirv": "MIRV",
|
||||
"mirvw": "MIRV 弹头"
|
||||
}
|
||||
},
|
||||
"game_list": {
|
||||
"recent_games": "近期对局",
|
||||
"game_id": "游戏 ID",
|
||||
"mode": "模式",
|
||||
"mode_ffa": "混战",
|
||||
"mode_team": "团队",
|
||||
"view": "视图",
|
||||
"details": "详细信息",
|
||||
"started": "已开始",
|
||||
"map": "地图",
|
||||
"difficulty": "难度",
|
||||
"type": "类型"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 884 B |
+115
-11
@@ -1,9 +1,23 @@
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import {
|
||||
PlayerGame,
|
||||
PlayerStatsTree,
|
||||
UserMeResponse,
|
||||
} from "../core/ApiSchemas";
|
||||
import "./components/baseComponents/stats/DiscordUserHeader";
|
||||
import "./components/baseComponents/stats/GameList";
|
||||
import "./components/baseComponents/stats/PlayerStatsTable";
|
||||
import "./components/baseComponents/stats/PlayerStatsTree";
|
||||
import "./components/Difficulties";
|
||||
import "./components/PatternButton";
|
||||
import { discordLogin, getApiBase, getUserMe, logOut } from "./jwt";
|
||||
import {
|
||||
discordLogin,
|
||||
fetchPlayerById,
|
||||
getApiBase,
|
||||
getUserMe,
|
||||
logOut,
|
||||
} from "./jwt";
|
||||
import { isInIframe, translateText } from "./Utils";
|
||||
|
||||
@customElement("account-modal")
|
||||
@@ -14,12 +28,33 @@ export class AccountModal extends LitElement {
|
||||
};
|
||||
|
||||
@state() private email: string = "";
|
||||
@state() private isLoadingUser: boolean = false;
|
||||
|
||||
private loggedInEmail: string | null = null;
|
||||
private loggedInDiscord: string | null = null;
|
||||
private userMeResponse: UserMeResponse | null = null;
|
||||
private playerId: string | null = null;
|
||||
private statsTree: PlayerStatsTree | null = null;
|
||||
private recentGames: PlayerGame[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
document.addEventListener("userMeResponse", (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
if (customEvent.detail) {
|
||||
this.userMeResponse = customEvent.detail as UserMeResponse;
|
||||
this.playerId = this.userMeResponse?.player?.publicId;
|
||||
if (this.playerId === undefined) {
|
||||
this.statsTree = null;
|
||||
this.recentGames = [];
|
||||
}
|
||||
} else {
|
||||
this.statsTree = null;
|
||||
this.recentGames = [];
|
||||
this.requestUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
@@ -38,6 +73,16 @@ export class AccountModal extends LitElement {
|
||||
}
|
||||
|
||||
private renderInner() {
|
||||
if (this.isLoadingUser) {
|
||||
return html`
|
||||
<div class="flex flex-col items-center justify-center p-6 text-white">
|
||||
<p class="mb-2">${translateText("account_modal.fetching_account")}</p>
|
||||
<div
|
||||
class="w-6 h-6 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"
|
||||
></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (this.loggedInDiscord) {
|
||||
return this.renderLoggedInDiscord();
|
||||
} else if (this.loggedInEmail) {
|
||||
@@ -47,15 +92,39 @@ export class AccountModal extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private viewGame(gameId: string): void {
|
||||
this.close();
|
||||
const path = location.pathname;
|
||||
const { search } = location;
|
||||
const hash = `#join=${encodeURIComponent(gameId)}`;
|
||||
const newUrl = `${path}${search}${hash}`;
|
||||
|
||||
history.pushState({ join: gameId }, "", newUrl);
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
||||
}
|
||||
|
||||
private renderLoggedInDiscord() {
|
||||
return html`
|
||||
<div class="p-6">
|
||||
<div class="mb-4">
|
||||
<p class="text-white text-center mb-4">
|
||||
<div class="mb-4 text-center">
|
||||
<p class="text-white mb-4">
|
||||
Logged in with Discord as ${this.loggedInDiscord}
|
||||
</p>
|
||||
${this.logoutButton()}
|
||||
</div>
|
||||
<div class="flex flex-col items-center mt-2 mb-4">
|
||||
<discord-user-header
|
||||
.data=${this.userMeResponse?.user?.discord ?? null}
|
||||
></discord-user-header>
|
||||
<player-stats-tree-view
|
||||
.statsTree=${this.statsTree}
|
||||
></player-stats-tree-view>
|
||||
<hr class="w-2/3 border-gray-600 my-2" />
|
||||
<game-list
|
||||
.games=${this.recentGames}
|
||||
.onViewGame=${(id: string) => this.viewGame(id)}
|
||||
></game-list>
|
||||
</div>
|
||||
${this.logoutButton()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -208,13 +277,30 @@ export class AccountModal extends LitElement {
|
||||
discordLogin();
|
||||
}
|
||||
|
||||
public async open() {
|
||||
const userMe = await getUserMe();
|
||||
if (userMe) {
|
||||
this.loggedInEmail = userMe.user.email ?? null;
|
||||
this.loggedInDiscord = userMe.user.discord?.global_name ?? null;
|
||||
}
|
||||
public open() {
|
||||
this.modalEl?.open();
|
||||
this.isLoadingUser = true;
|
||||
|
||||
void getUserMe()
|
||||
.then((userMe) => {
|
||||
if (userMe) {
|
||||
this.loggedInEmail = userMe.user.email ?? null;
|
||||
this.loggedInDiscord = userMe.user.discord?.global_name ?? null;
|
||||
if (this.playerId) {
|
||||
this.loadFromApi(this.playerId);
|
||||
}
|
||||
} else {
|
||||
this.loggedInEmail = null;
|
||||
this.loggedInDiscord = null;
|
||||
}
|
||||
this.isLoadingUser = false;
|
||||
this.requestUpdate();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("Failed to fetch user info in AccountModal.open():", err);
|
||||
this.isLoadingUser = false;
|
||||
this.requestUpdate();
|
||||
});
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -228,6 +314,24 @@ export class AccountModal extends LitElement {
|
||||
// Refresh the page after logout to update the UI state
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
private async loadFromApi(playerId: string): Promise<void> {
|
||||
try {
|
||||
const data = await fetchPlayerById(playerId);
|
||||
if (!data) {
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
this.recentGames = data.games;
|
||||
this.statsTree = data.stats;
|
||||
|
||||
this.requestUpdate();
|
||||
} catch (err) {
|
||||
console.warn("Failed to load player data:", err);
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("account-button")
|
||||
|
||||
@@ -74,10 +74,11 @@ export async function fetchCosmetics(): Promise<Cosmetics | null> {
|
||||
export function patternRelationship(
|
||||
pattern: Pattern,
|
||||
colorPalette: { name: string; isArchived?: boolean } | null,
|
||||
userMeResponse: UserMeResponse | null,
|
||||
userMeResponse: UserMeResponse | false,
|
||||
affiliateCode: string | null,
|
||||
): "owned" | "purchasable" | "blocked" {
|
||||
const flares = userMeResponse?.player.flares ?? [];
|
||||
const flares =
|
||||
userMeResponse === false ? [] : (userMeResponse.player.flares ?? []);
|
||||
if (flares.includes("pattern:*")) {
|
||||
return "owned";
|
||||
}
|
||||
|
||||
+31
-1
@@ -1,5 +1,6 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import { isInIframe } from "./Utils";
|
||||
|
||||
const LEFT_FUSE = "gutter-ad-container-left";
|
||||
@@ -17,6 +18,31 @@ export class GutterAds extends LitElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
private readonly boundUserMeHandler = (event: Event) =>
|
||||
this.onUserMe((event as CustomEvent<UserMeResponse | false>).detail);
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener(
|
||||
"userMeResponse",
|
||||
this.boundUserMeHandler as EventListener,
|
||||
);
|
||||
}
|
||||
|
||||
private onUserMe(userMeResponse: UserMeResponse | false): void {
|
||||
const flares =
|
||||
userMeResponse === false ? [] : (userMeResponse.player.flares ?? []);
|
||||
const hasFlare = flares.some((flare) => flare.startsWith("pattern:"));
|
||||
if (hasFlare) {
|
||||
console.log("No ads because you have patterns");
|
||||
window.enableAds = false;
|
||||
} else {
|
||||
console.log("No flares, showing ads");
|
||||
this.show();
|
||||
window.enableAds = true;
|
||||
}
|
||||
}
|
||||
|
||||
private isScreenLargeEnough(): boolean {
|
||||
return window.innerWidth >= MIN_SCREEN_WIDTH;
|
||||
}
|
||||
@@ -52,6 +78,10 @@ export class GutterAds extends LitElement {
|
||||
this.isVisible = false;
|
||||
console.log("hiding GutterAds");
|
||||
this.destroyAds();
|
||||
document.removeEventListener(
|
||||
"userMeResponse",
|
||||
this.boundUserMeHandler as EventListener,
|
||||
);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -95,7 +125,7 @@ export class GutterAds extends LitElement {
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.destroyAds();
|
||||
this.hide();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
+4
-109
@@ -2,7 +2,6 @@ import version from "../../resources/version.txt";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import { EventBus } from "../core/EventBus";
|
||||
import { GameRecord, GameStartInfo, ID } from "../core/Schemas";
|
||||
import { ServerConfig } from "../core/configuration/Config";
|
||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import "./AccountModal";
|
||||
@@ -38,17 +37,17 @@ import {
|
||||
generateCryptoRandomUUID,
|
||||
incrementGamesPlayed,
|
||||
isInIframe,
|
||||
translateText,
|
||||
} from "./Utils";
|
||||
import "./components/NewsButton";
|
||||
import { NewsButton } from "./components/NewsButton";
|
||||
import "./components/baseComponents/Button";
|
||||
import "./components/baseComponents/Modal";
|
||||
import { discordLogin, getUserMe, isLoggedIn } from "./jwt";
|
||||
import { getUserMe, isLoggedIn } from "./jwt";
|
||||
import "./styles.css";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
enableAds: boolean;
|
||||
PageOS: {
|
||||
session: {
|
||||
newPageView: () => void;
|
||||
@@ -59,6 +58,7 @@ declare global {
|
||||
destroyZone: (id: string) => void;
|
||||
pageInit: (options?: any) => void;
|
||||
que: Array<() => void>;
|
||||
destroySticky: () => void;
|
||||
};
|
||||
ramp: {
|
||||
que: Array<() => void>;
|
||||
@@ -278,98 +278,12 @@ class Client {
|
||||
}),
|
||||
);
|
||||
|
||||
const config = await getServerConfigFromClient();
|
||||
if (!hasAllowedFlare(userMeResponse, config)) {
|
||||
if (userMeResponse === false) {
|
||||
// Login is required
|
||||
document.body.innerHTML = `
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
">
|
||||
<div style="
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 2em;
|
||||
margin: 5em;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
">
|
||||
<p style="margin-bottom: 1em;">${translateText("auth.login_required")}</p>
|
||||
<p style="margin-bottom: 1.5em;">${translateText("auth.redirecting")}</p>
|
||||
<div style="width: 100%; height: 8px; background-color: #444; border-radius: 4px; overflow: hidden;">
|
||||
<div style="
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background-color: #4caf50;
|
||||
animation: fillBar 5s linear forwards;
|
||||
"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-image"></div>
|
||||
<style>
|
||||
@keyframes fillBar {
|
||||
from { width: 0%; }
|
||||
to { width: 100%; }
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
setTimeout(discordLogin, 5000);
|
||||
} else {
|
||||
// Unauthorized
|
||||
document.body.innerHTML = `
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
font-family: sans-serif;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
">
|
||||
<div style="
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 2em;
|
||||
margin: 5em;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
">
|
||||
<p style="margin-bottom: 1em;">${translateText("auth.not_authorized")}</p>
|
||||
<p>${translateText("auth.contact_admin")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-image"></div>
|
||||
`;
|
||||
}
|
||||
return;
|
||||
} else if (userMeResponse === false) {
|
||||
// Not logged in
|
||||
this.patternsModal.onUserMe(null);
|
||||
} else {
|
||||
if (userMeResponse !== false) {
|
||||
// Authorized
|
||||
console.log(
|
||||
`Your player ID is ${userMeResponse.player.publicId}\n` +
|
||||
"Sharing this ID will allow others to view your game history and stats.",
|
||||
);
|
||||
this.patternsModal.onUserMe(userMeResponse);
|
||||
const flares = (userMeResponse.player.flares ?? []).filter((flare) =>
|
||||
flare.startsWith("pattern:"),
|
||||
);
|
||||
if (flares.length > 0) {
|
||||
console.log("Hiding gutter ads because you have patterns");
|
||||
this.gutterAds.hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -642,12 +556,6 @@ class Client {
|
||||
this.publicLobby.stop();
|
||||
incrementGamesPlayed();
|
||||
|
||||
try {
|
||||
window.PageOS.session.newPageView();
|
||||
} catch (e) {
|
||||
console.error("Error calling newPageView", e);
|
||||
}
|
||||
|
||||
document.querySelectorAll(".ad").forEach((ad) => {
|
||||
(ad as HTMLElement).style.display = "none";
|
||||
});
|
||||
@@ -689,7 +597,6 @@ class Client {
|
||||
window.fusetag.pageInit({
|
||||
blockingFuseIds: ["lhs_sticky_vrec", "rhs_sticky_vrec"],
|
||||
});
|
||||
this.gutterAds.show();
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
@@ -749,15 +656,3 @@ function getPersistentIDFromCookie(): string {
|
||||
|
||||
return newID;
|
||||
}
|
||||
|
||||
function hasAllowedFlare(
|
||||
userMeResponse: UserMeResponse | false,
|
||||
config: ServerConfig,
|
||||
) {
|
||||
const allowed = config.allowedFlares();
|
||||
if (allowed === undefined) return true;
|
||||
if (userMeResponse === false) return false;
|
||||
const flares = userMeResponse.player.flares;
|
||||
if (flares === undefined) return false;
|
||||
return allowed.length === 0 || allowed.some((f) => flares.includes(f));
|
||||
}
|
||||
|
||||
@@ -37,14 +37,24 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
|
||||
private affiliateCode: string | null = null;
|
||||
|
||||
private userMeResponse: UserMeResponse | null = null;
|
||||
private userMeResponse: UserMeResponse | false = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async onUserMe(userMeResponse: UserMeResponse | null) {
|
||||
if (userMeResponse === null) {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener(
|
||||
"userMeResponse",
|
||||
(event: CustomEvent<UserMeResponse | false>) => {
|
||||
this.onUserMe(event.detail);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async onUserMe(userMeResponse: UserMeResponse | false) {
|
||||
if (userMeResponse === false) {
|
||||
this.userSettings.setSelectedPatternName(undefined);
|
||||
this.selectedPattern = null;
|
||||
this.selectedColor = null;
|
||||
@@ -136,7 +146,11 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
}
|
||||
|
||||
private renderColorSwatchGrid(): TemplateResult {
|
||||
const hexCodes = (this.userMeResponse?.player.flares ?? [])
|
||||
const hexCodes = (
|
||||
this.userMeResponse === false
|
||||
? []
|
||||
: (this.userMeResponse.player.flares ?? [])
|
||||
)
|
||||
.filter((flare) => flare.startsWith("color:"))
|
||||
.map((flare) => "#" + flare.split(":")[1]);
|
||||
return html`
|
||||
|
||||
@@ -137,7 +137,6 @@ export function renderPatternPreview(
|
||||
width: number,
|
||||
height: number,
|
||||
): TemplateResult {
|
||||
console.log("renderPatternPreview", pattern);
|
||||
if (pattern === null) {
|
||||
return renderBlankPreview(width, height);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import miniBigSmoke from "../../../resources/sprites/bigsmoke.png";
|
||||
import buildingExplosion from "../../../resources/sprites/buildingExplosion.png";
|
||||
import conquestSword from "../../../resources/sprites/conquestSword.png";
|
||||
import dust from "../../../resources/sprites/dust.png";
|
||||
import miniExplosion from "../../../resources/sprites/miniExplosion.png";
|
||||
@@ -89,6 +90,15 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
|
||||
originX: 9,
|
||||
originY: 9,
|
||||
},
|
||||
[FxType.BuildingExplosion]: {
|
||||
url: buildingExplosion,
|
||||
frameWidth: 17,
|
||||
frameCount: 10,
|
||||
frameDuration: 70,
|
||||
looping: false,
|
||||
originX: 8,
|
||||
originY: 8,
|
||||
},
|
||||
[FxType.SinkingShip]: {
|
||||
url: sinkingShip,
|
||||
frameWidth: 16,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { GameStartingModal } from "../GameStartingModal";
|
||||
import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler";
|
||||
import { TransformHandler } from "./TransformHandler";
|
||||
import { UIState } from "./UIState";
|
||||
import { AdTimer } from "./layers/AdTimer";
|
||||
import { AlertFrame } from "./layers/AlertFrame";
|
||||
import { BuildMenu } from "./layers/BuildMenu";
|
||||
import { ChatDisplay } from "./layers/ChatDisplay";
|
||||
@@ -27,7 +28,6 @@ import { PlayerPanel } from "./layers/PlayerPanel";
|
||||
import { RailroadLayer } from "./layers/RailroadLayer";
|
||||
import { ReplayPanel } from "./layers/ReplayPanel";
|
||||
import { SettingsModal } from "./layers/SettingsModal";
|
||||
import { SpawnAd } from "./layers/SpawnAd";
|
||||
import { SpawnTimer } from "./layers/SpawnTimer";
|
||||
import { StructureIconsLayer } from "./layers/StructureIconsLayer";
|
||||
import { StructureLayer } from "./layers/StructureLayer";
|
||||
@@ -209,18 +209,19 @@ export function createRenderer(
|
||||
fpsDisplay.eventBus = eventBus;
|
||||
fpsDisplay.userSettings = userSettings;
|
||||
|
||||
const spawnAd = document.querySelector("spawn-ad") as SpawnAd;
|
||||
if (!(spawnAd instanceof SpawnAd)) {
|
||||
console.error("spawn ad not found");
|
||||
}
|
||||
spawnAd.g = game;
|
||||
|
||||
const alertFrame = document.querySelector("alert-frame") as AlertFrame;
|
||||
if (!(alertFrame instanceof AlertFrame)) {
|
||||
console.error("alert frame not found");
|
||||
}
|
||||
alertFrame.game = game;
|
||||
|
||||
const spawnTimer = document.querySelector("spawn-timer") as SpawnTimer;
|
||||
if (!(spawnTimer instanceof SpawnTimer)) {
|
||||
console.error("spawn timer not found");
|
||||
}
|
||||
spawnTimer.game = game;
|
||||
spawnTimer.transformHandler = transformHandler;
|
||||
|
||||
// When updating these layers please be mindful of the order.
|
||||
// Try to group layers by the return value of shouldTransform.
|
||||
// Not grouping the layers may cause excessive calls to context.save() and context.restore().
|
||||
@@ -246,7 +247,7 @@ export function createRenderer(
|
||||
uiState,
|
||||
playerPanel,
|
||||
),
|
||||
new SpawnTimer(game, transformHandler),
|
||||
spawnTimer,
|
||||
leaderboard,
|
||||
gameLeftSidebar,
|
||||
unitDisplay,
|
||||
@@ -260,7 +261,7 @@ export function createRenderer(
|
||||
playerPanel,
|
||||
headsUpMessage,
|
||||
multiTabModal,
|
||||
spawnAd,
|
||||
new AdTimer(game),
|
||||
alertFrame,
|
||||
fpsDisplay,
|
||||
];
|
||||
|
||||
@@ -9,6 +9,7 @@ export enum FxType {
|
||||
MiniSmokeAndFire = "MiniSmokeAndFire",
|
||||
MiniExplosion = "MiniExplosion",
|
||||
UnitExplosion = "UnitExplosion",
|
||||
BuildingExplosion = "BuildingExplosion",
|
||||
SinkingShip = "SinkingShip",
|
||||
Nuke = "Nuke",
|
||||
SAMExplosion = "SAMExplosion",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
const AD_SHOW_TICKS = 60 * 10; // 1 minute
|
||||
|
||||
export class AdTimer implements Layer {
|
||||
private isHidden: boolean = false;
|
||||
|
||||
constructor(private g: GameView) {}
|
||||
|
||||
init() {}
|
||||
|
||||
public async tick() {
|
||||
if (this.isHidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gameTicks = this.g.ticks() - this.g.config().numSpawnPhaseTurns();
|
||||
if (gameTicks > AD_SHOW_TICKS) {
|
||||
console.log("destroying sticky ads");
|
||||
window.fusetag?.que?.push(() => {
|
||||
window.fusetag?.destroySticky?.();
|
||||
});
|
||||
this.isHidden = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,6 +151,14 @@ export class FxLayer implements Layer {
|
||||
case UnitType.Train:
|
||||
this.onTrainEvent(unit);
|
||||
break;
|
||||
case UnitType.DefensePost:
|
||||
case UnitType.City:
|
||||
case UnitType.Port:
|
||||
case UnitType.MissileSilo:
|
||||
case UnitType.SAMLauncher:
|
||||
case UnitType.Factory:
|
||||
this.onStructureEvent(unit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +254,20 @@ export class FxLayer implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
onStructureEvent(unit: UnitView) {
|
||||
if (!unit.isActive()) {
|
||||
const x = this.game.x(unit.lastTile());
|
||||
const y = this.game.y(unit.lastTile());
|
||||
const explosion = new SpriteFx(
|
||||
this.animatedSpriteLoader,
|
||||
x,
|
||||
y,
|
||||
FxType.BuildingExplosion,
|
||||
);
|
||||
this.allFx.push(explosion);
|
||||
}
|
||||
}
|
||||
|
||||
onNukeEvent(unit: UnitView, radius: number) {
|
||||
if (!unit.isActive()) {
|
||||
if (!unit.reachedTarget()) {
|
||||
|
||||
@@ -193,6 +193,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
|
||||
private closeSend = () => {
|
||||
this.sendTarget = null;
|
||||
this.sendMode = "none";
|
||||
};
|
||||
|
||||
private confirmSend = (
|
||||
@@ -418,10 +419,15 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
}}
|
||||
/>`
|
||||
: ""}
|
||||
<h1 class="text-2xl font-bold tracking-[-0.01em] truncate text-zinc-50">
|
||||
${other.name()}
|
||||
</h1>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h2
|
||||
class="text-xl font-bold tracking-[-0.01em] text-zinc-50 truncate"
|
||||
title=${other.name()}
|
||||
>
|
||||
${other.name()}
|
||||
</h2>
|
||||
</div>
|
||||
${chip
|
||||
? html`<span
|
||||
class=${`inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs font-semibold ${chip.classes}`}
|
||||
@@ -445,28 +451,28 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
return html`
|
||||
<div class="mb-1 flex justify-between gap-2">
|
||||
<div
|
||||
class="inline-flex items-center gap-1.5 rounded-full bg-white/[0.04] px-2.5 py-1
|
||||
text-base font-semibold text-zinc-200"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg bg-white/[0.04] px-3 py-1.5
|
||||
text-white w-[140px] min-w-[140px] flex-shrink-0"
|
||||
>
|
||||
<span class="mr-0.5">💰</span>
|
||||
<span translate="no" class="inline-block w-[45px] text-right">
|
||||
<span translate="no" class="tabular-nums w-[5ch]font-semibold">
|
||||
${renderNumber(other.gold() || 0)}
|
||||
</span>
|
||||
<span class="opacity-95 whitespace-nowrap"
|
||||
>${translateText("player_panel.gold")}</span
|
||||
<span class="text-zinc-200 whitespace-nowrap">
|
||||
${translateText("player_panel.gold")}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="inline-flex items-center gap-1.5 rounded-full bg-white/[0.04] px-2.5 py-1
|
||||
text-base font-semibold text-zinc-200"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg bg-white/[0.04] px-3 py-1.5
|
||||
text-white w-[140px] min-w-[140px] flex-shrink-0"
|
||||
>
|
||||
<span class="mr-0.5">🛡️</span>
|
||||
<span translate="no" class="inline-block w-[45px] text-right">
|
||||
<span translate="no" class="tabular-nums w-[5ch] font-semibold">
|
||||
${renderTroops(other.troops() || 0)}
|
||||
</span>
|
||||
<span class="opacity-95 whitespace-nowrap"
|
||||
>${translateText("player_panel.troops")}</span
|
||||
<span class="text-zinc-200 whitespace-nowrap">
|
||||
${translateText("player_panel.troops")}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -476,32 +482,34 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
private renderStats(other: PlayerView, my: PlayerView) {
|
||||
return html`
|
||||
<!-- Betrayals -->
|
||||
<div class="grid grid-cols-[auto,1fr] gap-x-6 gap-y-2 text-base">
|
||||
<div class="grid grid-cols-[auto,1fr] gap-x-6 gap-y-2">
|
||||
<div
|
||||
class="flex items-center gap-2 font-semibold text-zinc-300 leading-snug"
|
||||
class="flex items-center gap-2 text-[15px] font-medium text-zinc-100 leading-snug"
|
||||
>
|
||||
<span aria-hidden="true">⚠️</span>
|
||||
<span>${translateText("player_panel.betrayals")}</span>
|
||||
</div>
|
||||
<div class="text-right font-semibold text-zinc-200">
|
||||
<div class="text-right text-[14px] font-semibold text-zinc-200">
|
||||
${other.data.betrayals ?? 0}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trading / Embargo -->
|
||||
<div class="grid grid-cols-[auto,1fr] gap-x-6 gap-y-2 text-base">
|
||||
<div class="grid grid-cols-[auto,1fr] gap-x-6 gap-y-2">
|
||||
<div
|
||||
class="flex items-center gap-2 font-semibold text-zinc-300 leading-snug"
|
||||
class="flex items-center gap-2 text-[15px] font-medium text-zinc-100 leading-snug"
|
||||
>
|
||||
<span aria-hidden="true">⚓</span>
|
||||
<span>${translateText("player_panel.trading")}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2 font-semibold">
|
||||
<div
|
||||
class="flex items-center justify-end gap-2 text-[14px] font-semibold"
|
||||
>
|
||||
${other.hasEmbargoAgainst(my)
|
||||
? html`<span class="text-[#f59e0b]"
|
||||
? html`<span class="text-amber-400"
|
||||
>${translateText("player_panel.stopped")}</span
|
||||
>`
|
||||
: html`<span class="text-emerald-400"
|
||||
: html`<span class="text-blue-400"
|
||||
>${translateText("player_panel.active")}</span
|
||||
>`}
|
||||
</div>
|
||||
@@ -512,60 +520,57 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
private renderAlliances(other: PlayerView) {
|
||||
const allies = other.allies();
|
||||
|
||||
const nameCollator = new Intl.Collator(undefined, { sensitivity: "base" });
|
||||
const alliesSorted = [...allies].sort((a, b) =>
|
||||
nameCollator.compare(a.name(), b.name()),
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="text-base select-none">
|
||||
<!-- Header -->
|
||||
<div class="select-none">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="font-semibold text-zinc-300 text-base">
|
||||
<div
|
||||
id="alliances-title"
|
||||
class="text-[15px] font-medium text-zinc-200"
|
||||
>
|
||||
${translateText("player_panel.alliances")}
|
||||
</div>
|
||||
<span
|
||||
aria-label="Alliance count"
|
||||
class="inline-flex items-center justify-center min-w-[20px] h-5 px-[6px] rounded-[10px]
|
||||
text-[12px] text-zinc-100 bg-white/10 border border-white/20"
|
||||
aria-labelledby="alliances-title"
|
||||
class="inline-flex items-center justify-center min-w-[20px] h-5 px-[6px] rounded-[10px]
|
||||
text-[12px] text-zinc-100 bg-white/10 border border-white/20"
|
||||
>
|
||||
${allies.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 rounded-lg border border-zinc-600 bg-zinc-800/80">
|
||||
<div
|
||||
class="max-h-[72px] overflow-y-auto p-2 text-zinc-200 text-[12.5px] leading-relaxed"
|
||||
<div
|
||||
class="rounded-lg bg-zinc-800/70 ring-1 ring-zinc-700/60 w-full min-w-0"
|
||||
>
|
||||
<ul
|
||||
class="max-h-[120px] overflow-y-auto p-2
|
||||
flex flex-wrap gap-1.5
|
||||
scrollbar-thin scrollbar-thumb-zinc-600 hover:scrollbar-thumb-zinc-500 scrollbar-track-zinc-800"
|
||||
role="list"
|
||||
aria-label="Alliance list"
|
||||
aria-labelledby="alliances-title"
|
||||
translate="no"
|
||||
>
|
||||
${allies.length > 0
|
||||
? allies.map((p) => {
|
||||
const color = p.territoryColor().toHex();
|
||||
return html`
|
||||
<div
|
||||
role="listitem"
|
||||
class="grid grid-cols-[16px_1fr] items-center gap-2 w-full h-[30px]
|
||||
px-2 rounded-lg border border-transparent text-left
|
||||
hover:bg-[#141821] hover:border-white/30 transition-colors"
|
||||
${alliesSorted.length === 0
|
||||
? html`<li class="text-zinc-400 text-[14px] px-1">
|
||||
${translateText("common.none")}
|
||||
</li>`
|
||||
: alliesSorted.map(
|
||||
(p) =>
|
||||
html`<li
|
||||
class="max-w-full inline-flex items-center gap-1.5
|
||||
rounded-md border border-white/10 bg-white/[0.05]
|
||||
px-2.5 py-1 text-[14px] text-zinc-100
|
||||
hover:bg-white/[0.08] active:scale-[0.99] transition"
|
||||
title=${p.name()}
|
||||
>
|
||||
<span
|
||||
class="inline-block w-3 h-3 rounded-full mr-2"
|
||||
style="background-color: ${color}"
|
||||
>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="truncate select-none pointer-events-none font-medium"
|
||||
>
|
||||
${p.name()}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
: html`
|
||||
<div class="py-2 text-zinc-300">
|
||||
${translateText("common.none")}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<span class="truncate">${p.name()}</span>
|
||||
</li>`,
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -580,7 +585,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
</div>
|
||||
<div class="text-right font-semibold">
|
||||
<span
|
||||
class="inline-flex items-center rounded-full px-2 py-0.5 text-base font-bold ${this.getExpiryColorClass(
|
||||
class="inline-flex items-center rounded-full px-2 py-0.5 text-[14px] font-bold ${this.getExpiryColorClass(
|
||||
this.allianceExpirySeconds,
|
||||
)}"
|
||||
>${this.allianceExpiryText}</span
|
||||
@@ -605,7 +610,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
const canEmbargo = this.actions?.interaction?.canEmbargo;
|
||||
|
||||
return html`
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div class="grid auto-cols-fr grid-flow-col gap-1">
|
||||
${actionButton({
|
||||
onClick: (e: MouseEvent) => this.handleChat(e, my, other),
|
||||
@@ -657,6 +662,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
})
|
||||
: ""}
|
||||
</div>
|
||||
<ui-divider></ui-divider>
|
||||
|
||||
<div class="grid auto-cols-fr grid-flow-col gap-1">
|
||||
${other !== my
|
||||
@@ -754,80 +760,85 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
|
||||
<div
|
||||
class="fixed inset-0 z-[1001] flex items-center justify-center overflow-auto
|
||||
bg-black/15 backdrop-blur-sm backdrop-brightness-110 pointer-events-auto"
|
||||
bg-black/15 backdrop-brightness-110 pointer-events-auto"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
@wheel=${(e: MouseEvent) => e.stopPropagation()}
|
||||
@click=${() => this.hide()}
|
||||
>
|
||||
<div
|
||||
class="pointer-events-auto max-h-[90vh] overflow-y-auto min-w-[240px] w-auto px-4 py-2"
|
||||
class="pointer-events-auto max-h-[90vh] min-w-[300px] max-w-[400px] px-4 py-2"
|
||||
@click=${(e: MouseEvent) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
class=${`relative mt-2 w-full bg-zinc-900/90 backdrop-blur-sm p-5 shadow-2xl rounded-xl text-zinc-200
|
||||
${other.isTraitor() ? "traitor-ring" : "ring-1 ring-zinc-700"}`}
|
||||
>
|
||||
<!-- Close button -->
|
||||
<button
|
||||
@click=${this.handleClose}
|
||||
class="absolute -top-3 -right-3 flex h-7 w-7 items-center justify-center
|
||||
rounded-full bg-zinc-700 text-white shadow hover:bg-red-500 transition-colors"
|
||||
aria-label=${translateText("common.close") || "Close"}
|
||||
title=${translateText("common.close") || "Close"}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
<div class="relative">
|
||||
<div
|
||||
class="flex flex-col gap-2 font-sans antialiased text-[14px] leading-relaxed"
|
||||
class="absolute inset-2 -z-10 rounded-2xl bg-black/25 backdrop-blur-[2px]"
|
||||
></div>
|
||||
<div
|
||||
class=${`relative w-full bg-zinc-900/95 p-6 rounded-2xl text-zinc-100 overflow-visible shadow-2xl shadow-black/50
|
||||
${other.isTraitor() ? "traitor-ring" : "ring-1 ring-white/5"}`}
|
||||
>
|
||||
<!-- Identity (flag, name, type, traitor, relation) -->
|
||||
<div class="mb-1">${this.renderIdentityRow(other, my)}</div>
|
||||
<!-- Close button -->
|
||||
<button
|
||||
@click=${this.handleClose}
|
||||
class="absolute -top-3 -right-3 flex h-7 w-7 items-center justify-center
|
||||
rounded-full bg-zinc-700 text-white shadow hover:bg-red-500 transition-colors"
|
||||
aria-label=${translateText("common.close") || "Close"}
|
||||
title=${translateText("common.close") || "Close"}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
|
||||
${this.sendTarget
|
||||
? html`
|
||||
<send-resource-modal
|
||||
.open=${this.sendMode !== "none"}
|
||||
.mode=${this.sendMode}
|
||||
.total=${this.sendMode === "troops"
|
||||
? myTroopsNum
|
||||
: myGoldNum}
|
||||
.uiState=${this.uiState}
|
||||
.myPlayer=${my}
|
||||
.target=${this.sendTarget}
|
||||
.gameView=${this.g}
|
||||
.eventBus=${this.eventBus}
|
||||
.format=${this.sendMode === "troops"
|
||||
? renderTroops
|
||||
: renderNumber}
|
||||
@confirm=${this.confirmSend}
|
||||
@close=${this.closeSend}
|
||||
></send-resource-modal>
|
||||
`
|
||||
: ""}
|
||||
<div
|
||||
class="flex flex-col gap-2 font-sans antialiased text-[14.5px] leading-relaxed"
|
||||
>
|
||||
<!-- Identity (flag, name, type, traitor, relation) -->
|
||||
<div class="mb-1">${this.renderIdentityRow(other, my)}</div>
|
||||
|
||||
<ui-divider></ui-divider>
|
||||
${this.sendTarget
|
||||
? html`
|
||||
<send-resource-modal
|
||||
.open=${this.sendMode !== "none"}
|
||||
.mode=${this.sendMode}
|
||||
.total=${this.sendMode === "troops"
|
||||
? myTroopsNum
|
||||
: myGoldNum}
|
||||
.uiState=${this.uiState}
|
||||
.myPlayer=${my}
|
||||
.target=${this.sendTarget}
|
||||
.gameView=${this.g}
|
||||
.eventBus=${this.eventBus}
|
||||
.format=${this.sendMode === "troops"
|
||||
? renderTroops
|
||||
: renderNumber}
|
||||
@confirm=${this.confirmSend}
|
||||
@close=${this.closeSend}
|
||||
></send-resource-modal>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<!-- Resources -->
|
||||
${this.renderResources(other)}
|
||||
<ui-divider></ui-divider>
|
||||
|
||||
<ui-divider></ui-divider>
|
||||
<!-- Resources -->
|
||||
${this.renderResources(other)}
|
||||
|
||||
<!-- Stats: betrayals / trading -->
|
||||
${this.renderStats(other, my)}
|
||||
<ui-divider></ui-divider>
|
||||
|
||||
<ui-divider></ui-divider>
|
||||
<!-- Stats: betrayals / trading -->
|
||||
${this.renderStats(other, my)}
|
||||
|
||||
<!-- Alliances list -->
|
||||
${this.renderAlliances(other)}
|
||||
<ui-divider></ui-divider>
|
||||
|
||||
<!-- Alliance time remaining -->
|
||||
${this.renderAllianceExpiry()}
|
||||
<!-- Alliances list -->
|
||||
${this.renderAlliances(other)}
|
||||
|
||||
<ui-divider></ui-divider>
|
||||
<!-- Alliance time remaining -->
|
||||
${this.renderAllianceExpiry()}
|
||||
|
||||
<!-- Actions -->
|
||||
${this.renderActions(my, other)}
|
||||
<ui-divider class="mt-1"></ui-divider>
|
||||
|
||||
<!-- Actions -->
|
||||
${this.renderActions(my, other)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -244,19 +244,22 @@ export class SendResourceModal extends LitElement {
|
||||
private renderHeader() {
|
||||
const name = this.target?.name?.() ?? "";
|
||||
return html`
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="mb-3 flex items-center justify-between relative">
|
||||
<h2
|
||||
id="send-title"
|
||||
class="text-lg font-semibold tracking-tight text-zinc-100"
|
||||
>
|
||||
${this.heading ?? this.i18n.title(name)}
|
||||
</h2>
|
||||
<!-- Close button -->
|
||||
<button
|
||||
class="rounded-md px-2 text-2xl leading-none text-zinc-300 hover:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-white/30"
|
||||
type="button"
|
||||
@click=${() => this.closeModal()}
|
||||
class="absolute -top-3 -right-3 flex h-7 w-7 items-center justify-center rounded-full bg-zinc-700 text-white shadow hover:bg-red-500 transition-colors focus-visible:ring-2 focus-visible:ring-white/30 focus:outline-none"
|
||||
aria-label=${this.i18n.closeLabel()}
|
||||
title=${this.i18n.closeLabel()}
|
||||
>
|
||||
×
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -264,7 +267,6 @@ export class SendResourceModal extends LitElement {
|
||||
|
||||
private renderAvailable() {
|
||||
const total = this.getTotalNumber();
|
||||
const cap = this.getCapacityLeft();
|
||||
|
||||
return html`
|
||||
<div class="mb-4 pb-3 border-b border-zinc-800">
|
||||
@@ -277,21 +279,6 @@ export class SendResourceModal extends LitElement {
|
||||
<span class="opacity-90">${this.i18n.availableChip()}</span>
|
||||
<span class="font-mono tabular-nums">${this.format(total)}</span>
|
||||
</span>
|
||||
|
||||
${cap !== null
|
||||
? html`
|
||||
<!-- Cap -->
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-full bg-amber-500/10 px-2 py-0.5 ring-1 ring-amber-400/40 text-amber-200"
|
||||
title=${this.i18n.capTooltip()}
|
||||
>
|
||||
<span class="opacity-90">${this.i18n.cap()}</span>
|
||||
<span class="font-mono tabular-nums"
|
||||
>${this.format(cap)}</span
|
||||
>
|
||||
</span>
|
||||
`
|
||||
: html``}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -554,9 +541,11 @@ export class SendResourceModal extends LitElement {
|
||||
const allowed = this.limitAmount(this.sendAmount);
|
||||
|
||||
return html`
|
||||
<div class="fixed inset-0 z-[1100] flex items-center justify-center p-4">
|
||||
<div
|
||||
class="absolute inset-0 z-[1100] flex items-center justify-center p-4"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-black/60 backdrop-blur-sm rounded-2xl"
|
||||
class="absolute inset-0 bg-black/60 rounded-2xl"
|
||||
@click=${() => this.closeModal()}
|
||||
></div>
|
||||
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { translateText } from "../../../client/Utils";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { getGamesPlayed } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
const AD_TYPE = "bottom_rail";
|
||||
const AD_CONTAINER_ID = "bottom-rail-ad-container";
|
||||
|
||||
@customElement("spawn-ad")
|
||||
export class SpawnAd extends LitElement implements Layer {
|
||||
public g: GameView;
|
||||
|
||||
@state()
|
||||
private isVisible: boolean = false;
|
||||
|
||||
@state()
|
||||
private adLoaded: boolean = false;
|
||||
|
||||
private gamesPlayed: number = 0;
|
||||
|
||||
// Override createRenderRoot to disable shadow DOM
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
static styles = css``;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.gamesPlayed = getGamesPlayed();
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
this.isVisible = true;
|
||||
this.loadAd();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
// Destroy the ad when hiding
|
||||
this.destroyAd();
|
||||
this.isVisible = false;
|
||||
this.adLoaded = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
public async tick() {
|
||||
if (
|
||||
!this.isVisible &&
|
||||
this.g.inSpawnPhase() &&
|
||||
this.g.ticks() > 10 &&
|
||||
this.gamesPlayed > 5
|
||||
) {
|
||||
console.log("not showing spawn ad");
|
||||
// this.show();
|
||||
}
|
||||
if (this.isVisible && !this.g.inSpawnPhase()) {
|
||||
console.log("hiding bottom left ad");
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private loadAd(): void {
|
||||
if (!window.ramp) {
|
||||
console.warn("Playwire RAMP not available");
|
||||
return;
|
||||
}
|
||||
if (this.adLoaded) {
|
||||
console.log("Ad already loaded, skipping");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.ramp.que.push(() => {
|
||||
window.ramp.spaAddAds([
|
||||
{
|
||||
type: AD_TYPE,
|
||||
selectorId: AD_CONTAINER_ID,
|
||||
},
|
||||
]);
|
||||
this.adLoaded = true;
|
||||
console.log("Playwire ad loaded:", AD_TYPE);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to load Playwire ad:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private destroyAd(): void {
|
||||
if (!window.ramp || !this.adLoaded) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.ramp.que.push(() => {
|
||||
window.ramp.destroyUnits("all");
|
||||
console.log("Playwire spawn ad destroyed");
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to destroy Playwire ad:", error);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
// Clean up ad when component is removed
|
||||
this.destroyAd();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="fixed bottom-0 left-0 w-full min-h-[100px] bg-gray-900 border border-gray-600 flex items-center justify-center z-50"
|
||||
>
|
||||
<div
|
||||
id="${AD_CONTAINER_ID}"
|
||||
class="w-full h-full flex items-center justify-center"
|
||||
>
|
||||
${!this.adLoaded
|
||||
? html`<span class="text-white text-sm"
|
||||
>${translateText("spawn_ad.loading")}</span
|
||||
>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,34 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { GameMode, Team } from "../../../core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class SpawnTimer implements Layer {
|
||||
@customElement("spawn-timer")
|
||||
export class SpawnTimer extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
public transformHandler: TransformHandler;
|
||||
|
||||
private ratios = [0];
|
||||
private colors = ["rgba(0, 128, 255, 0.7)", "rgba(0, 0, 0, 0.5)"];
|
||||
|
||||
constructor(
|
||||
private game: GameView,
|
||||
private transformHandler: TransformHandler,
|
||||
) {}
|
||||
private isVisible = false;
|
||||
|
||||
init() {}
|
||||
createRenderRoot() {
|
||||
this.style.position = "fixed";
|
||||
this.style.top = "0";
|
||||
this.style.left = "0";
|
||||
this.style.width = "100%";
|
||||
this.style.height = "7px";
|
||||
this.style.zIndex = "1000";
|
||||
this.style.pointerEvents = "none";
|
||||
return this;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.isVisible = true;
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.game.inSpawnPhase()) {
|
||||
@@ -21,6 +37,7 @@ export class SpawnTimer implements Layer {
|
||||
this.game.ticks() / this.game.config().numSpawnPhaseTurns(),
|
||||
];
|
||||
this.colors = ["rgba(0, 128, 255, 0.7)"];
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,6 +45,7 @@ export class SpawnTimer implements Layer {
|
||||
this.colors = [];
|
||||
|
||||
if (this.game.config().gameConfig().gameMode !== GameMode.Team) {
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -41,44 +59,52 @@ export class SpawnTimer implements Layer {
|
||||
|
||||
const theme = this.game.config().theme();
|
||||
const total = sumIterator(teamTiles.values());
|
||||
if (total === 0) return;
|
||||
if (total === 0) {
|
||||
this.requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [team, count] of teamTiles) {
|
||||
const ratio = count / total;
|
||||
this.ratios.push(ratio);
|
||||
this.colors.push(theme.teamColor(team).toRgbString());
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
renderLayer(context: CanvasRenderingContext2D) {
|
||||
if (this.ratios.length === 0 || this.colors.length === 0) return;
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const barHeight = 10;
|
||||
const barWidth = this.transformHandler.width();
|
||||
if (this.ratios.length === 0 || this.colors.length === 0) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.game.inSpawnPhase() &&
|
||||
this.game.config().gameConfig().gameMode !== GameMode.Team
|
||||
) {
|
||||
return;
|
||||
return html``;
|
||||
}
|
||||
|
||||
let x = 0;
|
||||
let filledRatio = 0;
|
||||
for (let i = 0; i < this.ratios.length && i < this.colors.length; i++) {
|
||||
const ratio = this.ratios[i] ?? 1 - filledRatio;
|
||||
const segmentWidth = barWidth * ratio;
|
||||
|
||||
context.fillStyle = this.colors[i];
|
||||
context.fillRect(x, 0, segmentWidth, barHeight);
|
||||
|
||||
x += segmentWidth;
|
||||
filledRatio += ratio;
|
||||
}
|
||||
return html`
|
||||
<div class="w-full h-full flex z-[999]">
|
||||
${this.ratios.map((ratio, i) => {
|
||||
const color = this.colors[i] || "rgba(0, 0, 0, 0.5)";
|
||||
return html`
|
||||
<div
|
||||
class="h-full transition-all duration-100 ease-in-out"
|
||||
style="width: ${ratio * 100}%; background-color: ${color};"
|
||||
></div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -221,5 +221,6 @@ export class TeamStats extends LitElement implements Layer {
|
||||
function formatPercentage(value: number): string {
|
||||
const perc = value * 100;
|
||||
if (Number.isNaN(perc)) return "0%";
|
||||
if (perc === 100) return "100%";
|
||||
return perc.toPrecision(2) + "%";
|
||||
}
|
||||
|
||||
@@ -132,12 +132,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
for (const pattern of Object.values(patterns?.patterns ?? {})) {
|
||||
for (const colorPalette of pattern.colorPalettes ?? []) {
|
||||
if (
|
||||
patternRelationship(
|
||||
pattern,
|
||||
colorPalette,
|
||||
me !== false ? me : null,
|
||||
null,
|
||||
) === "purchasable"
|
||||
patternRelationship(pattern, colorPalette, me, null) === "purchasable"
|
||||
) {
|
||||
const palette = patterns?.colorPalettes?.[colorPalette.name];
|
||||
if (palette) {
|
||||
|
||||
@@ -97,6 +97,13 @@
|
||||
src="https://cdn.fuseplatform.net/publift/tags/2/4121/fuse.js"
|
||||
></script>
|
||||
|
||||
<script>
|
||||
window.googletag = window.googletag || { cmd: [] };
|
||||
googletag.cmd.push(function () {
|
||||
googletag.pubads().set("page_url", "http://openfront.io ");
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Analytics -->
|
||||
<script
|
||||
async
|
||||
@@ -398,6 +405,7 @@
|
||||
</div>
|
||||
<settings-modal></settings-modal>
|
||||
<player-panel></player-panel>
|
||||
<spawn-timer></spawn-timer>
|
||||
<help-modal></help-modal>
|
||||
<dark-mode-button></dark-mode-button>
|
||||
<alert-frame></alert-frame>
|
||||
@@ -406,7 +414,6 @@
|
||||
<multi-tab-modal></multi-tab-modal>
|
||||
<news-modal></news-modal>
|
||||
<game-left-sidebar></game-left-sidebar>
|
||||
<spawn-ad></spawn-ad>
|
||||
<flag-input-modal></flag-input-modal>
|
||||
<fps-display></fps-display>
|
||||
<div
|
||||
|
||||
@@ -17,6 +17,7 @@ import { TileRef, euclDistFN } from "../game/GameMap";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { GameID } from "../Schemas";
|
||||
import { boundingBoxTiles, calculateBoundingBox, simpleHash } from "../Util";
|
||||
import { AllianceRequestExecution } from "./alliance/AllianceRequestExecution";
|
||||
import { ConstructionExecution } from "./ConstructionExecution";
|
||||
import { EmojiExecution } from "./EmojiExecution";
|
||||
import { structureSpawnTileValue } from "./nation/structureSpawnTileValue";
|
||||
@@ -221,7 +222,9 @@ export class FakeHumanExecution implements Execution {
|
||||
if (this.random.chance(20)) {
|
||||
const toAlly = this.random.randElement(enemies);
|
||||
if (this.player.canSendAllianceRequest(toAlly)) {
|
||||
this.player.createAllianceRequest(toAlly);
|
||||
this.mg.addExecution(
|
||||
new AllianceRequestExecution(this.player, toAlly.id()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ export class PlayerExecution implements Execution {
|
||||
if (main === undefined) throw new Error("No clusters");
|
||||
this.player.largestClusterBoundingBox = calculateBoundingBox(this.mg, main);
|
||||
const surroundedBy = this.surroundedBySamePlayer(main);
|
||||
if (surroundedBy && !this.player.isFriendly(surroundedBy)) {
|
||||
if (surroundedBy && !surroundedBy.isFriendly(this.player)) {
|
||||
this.removeCluster(main);
|
||||
}
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@ const _structureTypes: ReadonlySet<UnitType> = new Set([
|
||||
UnitType.SAMLauncher,
|
||||
UnitType.MissileSilo,
|
||||
UnitType.Port,
|
||||
UnitType.Factory,
|
||||
]);
|
||||
|
||||
export function isStructureType(type: UnitType): boolean {
|
||||
|
||||
Reference in New Issue
Block a user