From 144920eb53451f2652c1e2860b9b741e368a2ba7 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Fri, 17 Oct 2025 04:49:46 +0200 Subject: [PATCH 1/8] Fix 100% owned in team stats (#2217) ## Description: When a team owns 100% of the land, it would show "1.0e2%" because of toPrecision(2). Fix this by keeping the same precision as for other numbers, but just returning 100 for 100%. It probably wasn't really noticed before since full land ownership doesn't occur much. Chances of 100% in team stats are a bit higher since fallout is now taken into account. Only a picture of the before situation: 100 percent ## 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: tryout33 --- src/client/graphics/layers/TeamStats.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/graphics/layers/TeamStats.ts b/src/client/graphics/layers/TeamStats.ts index ba8e9dbe7..a8503c229 100644 --- a/src/client/graphics/layers/TeamStats.ts +++ b/src/client/graphics/layers/TeamStats.ts @@ -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) + "%"; } From 4eaf3de5de09f60fcd8b9ebdfabb4511b8da3f6b Mon Sep 17 00:00:00 2001 From: DevelopingTom Date: Fri, 17 Oct 2025 04:58:13 +0200 Subject: [PATCH 2/8] Add destructed structure FX (#2210) ## Description: New FX on building destruction Icon level: https://github.com/user-attachments/assets/0ba5e557-a5d7-436f-8a58-2843d4c99332 Pixel art level: https://github.com/user-attachments/assets/12002df6-eb46-4853-b84f-4f81ce7c3528 ## 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: Evan --- resources/sprites/buildingExplosion.png | Bin 0 -> 884 bytes src/client/graphics/AnimatedSpriteLoader.ts | 10 +++++++++ src/client/graphics/fx/Fx.ts | 1 + src/client/graphics/layers/FxLayer.ts | 22 ++++++++++++++++++++ src/core/game/Game.ts | 1 + 5 files changed, 34 insertions(+) create mode 100644 resources/sprites/buildingExplosion.png diff --git a/resources/sprites/buildingExplosion.png b/resources/sprites/buildingExplosion.png new file mode 100644 index 0000000000000000000000000000000000000000..de827d4e06ba8a2cc8b854cbd9a64d2b6012c0f2 GIT binary patch literal 884 zcmV-)1B?8LP)Px&E=fc|RA_2(Ocyfe>!_SqLviNAExozMQC$^G)|_fCkdw%Y3LwRmho z=r|bFb9vx#lt=T+XTS&L@!b8y&cild_b`c@hl{Q+&p{n|J+P0hj|_RhCI|pPN}2f` zFsg(6_|5sZEgqY&yPsJ5pPn42f5`7Yey#Q8o@oLi=?}f$f$rlw9(yGHp~UCDK3Lb6 z=g{u-3gc-bL8age>1XwDfEq)pM8Rs4EpoX+5(9P$%vi7%sX`_e1t8=B03d`A*wghH zrmX-ML6sDw_gCe#?d)crp1T0619p4#BrYEI*7|XvU?0lE7qn@$0v~=)B=!w~waBPi z*nf}-Bp-+spkmy3X{Zg-d#)Y|)Ka-Cl-IKIUI<56Pfw2F{;HgTQ$efRI9N@Q&<8eG zj}XveL{_!e3;~t`{#{-(PEtwg1$0n2} zGm$1pIU+HP>cBzBN#9QjK`*N|cyy1X&-bHXb6L_kh<%pySWy*UTO+E1(|5U+D^cwn z#1e_@z(myD{Y0{*sc}Or=PmOb97|g`!EzO@UHUH5RDYjDXj-kXl?Tp)i>RUZO?}v< zKX3M~M_~nNu01L~uMLD=4X|lBZ)Kr$Or}(Vr1qjZlz2-(B~$o?Up(xsqUx(C=yXW% z^-k-jyghnii~Tm;9UO{jwL*^PJlma~R8h`Upm)josr{#mHnwi2q{HY7x=VS=R`93F zQzB=H3TNnfvUQt>&p!3*-#_B+;4mHIo9$!JFucNJ**AHZ#&0C3G3v0f6}#vk@>M9b zjN}r{ImIYL2@TGozivfUj4DIk@BNv1> = { 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, diff --git a/src/client/graphics/fx/Fx.ts b/src/client/graphics/fx/Fx.ts index 2aeb3ccf6..d4b206614 100644 --- a/src/client/graphics/fx/Fx.ts +++ b/src/client/graphics/fx/Fx.ts @@ -9,6 +9,7 @@ export enum FxType { MiniSmokeAndFire = "MiniSmokeAndFire", MiniExplosion = "MiniExplosion", UnitExplosion = "UnitExplosion", + BuildingExplosion = "BuildingExplosion", SinkingShip = "SinkingShip", Nuke = "Nuke", SAMExplosion = "SAMExplosion", diff --git a/src/client/graphics/layers/FxLayer.ts b/src/client/graphics/layers/FxLayer.ts index 027b8dd6c..e5ddf3831 100644 --- a/src/client/graphics/layers/FxLayer.ts +++ b/src/client/graphics/layers/FxLayer.ts @@ -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()) { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index db48e0a2f..978672084 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -202,6 +202,7 @@ const _structureTypes: ReadonlySet = new Set([ UnitType.SAMLauncher, UnitType.MissileSilo, UnitType.Port, + UnitType.Factory, ]); export function isStructureType(type: UnitType): boolean { From 19081bf21bb62dabb352761170022de411aebad6 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Fri, 17 Oct 2025 12:01:19 +0900 Subject: [PATCH 3/8] Add PlayerInfoModal (#2058) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: This PR adds the Player Status display to the AccountModal. スクリーンショット 2025-10-11 6 44 25 (origin pr:https://github.com/openfrontio/OpenFrontIO/pull/1758) ## 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: aotumuri --------- Co-authored-by: evanpelle --- resources/lang/en.json | 1 + src/client/AccountModal.ts | 126 +++++++++++++++++++++++++++++++++---- 2 files changed, 116 insertions(+), 11 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index a8a130767..1d14125cc 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -155,6 +155,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}" }, diff --git a/src/client/AccountModal.ts b/src/client/AccountModal.ts index 93bf0ff96..01762b942 100644 --- a/src/client/AccountModal.ts +++ b/src/client/AccountModal.ts @@ -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` +
+

${translateText("account_modal.fetching_account")}

+
+
+ `; + } 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`
-
-

+

+

Logged in with Discord as ${this.loggedInDiscord}

+ ${this.logoutButton()} +
+
+ + +
+ this.viewGame(id)} + >
- ${this.logoutButton()}
`; } @@ -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 { + 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") From 06de3f1e8f9534288a11a186246653df4eaae4cd Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Sat, 18 Oct 2025 06:27:47 +0900 Subject: [PATCH 4/8] mls (v4.8) (#2218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: This is the final MLS for v26. Starting from the next PR, we’ll move on to v27. mls for v26 Version identifier within MLS: v4.7 ## 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: Aotumuri --- resources/lang/sl.json | 78 +++++++++++++++++++++++++++++++++++---- resources/lang/zh-CN.json | 78 +++++++++++++++++++++++++++++++++++---- 2 files changed, 142 insertions(+), 14 deletions(-) diff --git a/resources/lang/sl.json b/resources/lang/sl.json index c6c4cb02e..b786336ae 100644 --- a/resources/lang/sl.json +++ b/resources/lang/sl.json @@ -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" } } diff --git a/resources/lang/zh-CN.json b/resources/lang/zh-CN.json index 2e748efa2..4b038a59c 100644 --- a/resources/lang/zh-CN.json +++ b/resources/lang/zh-CN.json @@ -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": "类型" } } From 373e3ef44abe21b4f415dd82d22cd9f6da0fb019 Mon Sep 17 00:00:00 2001 From: Abdallah Bahrawi <140177728+abdallahbahrawi1@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:55:54 +0300 Subject: [PATCH 5/8] fix Request icon stuck for nations (#2234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: fixes https://github.com/openfrontio/OpenFrontIO/issues/1955 Describe the PR. Bots previously created requests directly, skipping timeout cleanup and causing a stuck “request sent” icon when unhandled. Using AllianceRequestExecution makes bots follow the same lifecycle as humans, so requests expire correctly. ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] 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 - [ ] 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: abodcraft1 --------- Co-authored-by: Evan --- src/core/execution/FakeHumanExecution.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index edeab7417..3b8a7e94d 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -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()), + ); } } From fb3d2e2c6fbd91274aba95d83e2ca958d3fe4625 Mon Sep 17 00:00:00 2001 From: Abdallah Bahrawi <140177728+abdallahbahrawi1@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:05:14 +0300 Subject: [PATCH 6/8] Polished Player Panel (#2235) ## Description: - Polished Player Panel UI. - Made alliance list unstructured. - Removed blur background. ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] 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 - [ ] 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: abodcraft1 --------- Co-authored-by: Evan --- src/client/graphics/layers/PlayerPanel.ts | 245 +++++++++--------- .../graphics/layers/SendResourceModal.ts | 31 +-- 2 files changed, 138 insertions(+), 138 deletions(-) diff --git a/src/client/graphics/layers/PlayerPanel.ts b/src/client/graphics/layers/PlayerPanel.ts index 3af01a0ec..5ff2640a9 100644 --- a/src/client/graphics/layers/PlayerPanel.ts +++ b/src/client/graphics/layers/PlayerPanel.ts @@ -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 { }} />` : ""} -

- ${other.name()} -

+
+

+ ${other.name()} +

+
${chip ? html`
💰 - + ${renderNumber(other.gold() || 0)} - ${translateText("player_panel.gold")} + ${translateText("player_panel.gold")}
🛡️ - + ${renderTroops(other.troops() || 0)} - ${translateText("player_panel.troops")} + ${translateText("player_panel.troops")}
@@ -476,32 +482,34 @@ export class PlayerPanel extends LitElement implements Layer { private renderStats(other: PlayerView, my: PlayerView) { return html` -
+
${translateText("player_panel.betrayals")}
-
+
${other.data.betrayals ?? 0}
-
+
${translateText("player_panel.trading")}
-
+
${other.hasEmbargoAgainst(my) - ? html`${translateText("player_panel.stopped")}` - : html`${translateText("player_panel.active")}`}
@@ -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` -
- +
-
+
${translateText("player_panel.alliances")}
${allies.length}
-
-
+
    - ${allies.length > 0 - ? allies.map((p) => { - const color = p.territoryColor().toHex(); - return html` -
    + ${translateText("common.none")} + ` + : alliesSorted.map( + (p) => + html`
  • - - - - - ${p.name()} - -
  • - `; - }) - : html` -
    - ${translateText("common.none")} -
    - `} -
+ ${p.name()} + `, + )} +
`; @@ -580,7 +585,7 @@ export class PlayerPanel extends LitElement implements Layer {
${this.allianceExpiryText} +
${actionButton({ onClick: (e: MouseEvent) => this.handleChat(e, my, other), @@ -657,6 +662,7 @@ export class PlayerPanel extends LitElement implements Layer { }) : ""}
+
${other !== my @@ -754,80 +760,85 @@ export class PlayerPanel extends LitElement implements Layer {
e.preventDefault()} @wheel=${(e: MouseEvent) => e.stopPropagation()} @click=${() => this.hide()} >
e.stopPropagation()} > -
- - - +
+
- -
${this.renderIdentityRow(other, my)}
+ + - ${this.sendTarget - ? html` - - ` - : ""} +
+ +
${this.renderIdentityRow(other, my)}
- + ${this.sendTarget + ? html` + + ` + : ""} - - ${this.renderResources(other)} + - + + ${this.renderResources(other)} - - ${this.renderStats(other, my)} + - + + ${this.renderStats(other, my)} - - ${this.renderAlliances(other)} + - - ${this.renderAllianceExpiry()} + + ${this.renderAlliances(other)} - + + ${this.renderAllianceExpiry()} - - ${this.renderActions(my, other)} + + + + ${this.renderActions(my, other)} +
diff --git a/src/client/graphics/layers/SendResourceModal.ts b/src/client/graphics/layers/SendResourceModal.ts index c3de5319a..5e7b3a1ed 100644 --- a/src/client/graphics/layers/SendResourceModal.ts +++ b/src/client/graphics/layers/SendResourceModal.ts @@ -244,19 +244,22 @@ export class SendResourceModal extends LitElement { private renderHeader() { const name = this.target?.name?.() ?? ""; return html` -
+

${this.heading ?? this.i18n.title(name)}

+
`; @@ -264,7 +267,6 @@ export class SendResourceModal extends LitElement { private renderAvailable() { const total = this.getTotalNumber(); - const cap = this.getCapacityLeft(); return html`
@@ -277,21 +279,6 @@ export class SendResourceModal extends LitElement { ${this.i18n.availableChip()} ${this.format(total)} - - ${cap !== null - ? html` - - - ${this.i18n.cap()} - ${this.format(cap)} - - ` - : html``}
`; @@ -554,9 +541,11 @@ export class SendResourceModal extends LitElement { const allowed = this.limitAmount(this.sendAmount); return html` -
+
this.closeModal()} >
From 4673808dc08bb933610735902d7657e8ebe385ea Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:06:23 +0200 Subject: [PATCH 7/8] Fix annex surrounded main cluster disconnected player (#2241) ## Description: When a main cluster is fully surrounded, the surrounding player is able to attack them (based on AttackLogic in DefaultConfig). But so far wasn't able to annex them. Fix: turned around an isFriendly check in PlayerExecution. This way if this.player is disconnected, they can get annexed, allied/team mate or not. This also means that in the edge case of surrounding player going AFK, the enclosed main cluster can attack the disconnected surrounding player and maybe fight it's way out of being enclosed. Meant as hotfix for v26. Reported here: https://discord.com/channels/1284581928254701718/1429252618995105923/1429252618995105923 ## 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: tryout33 --- src/core/execution/PlayerExecution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index 66e10c854..bc2491a72 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -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); } From dd9ad7472f68a681a2fba141d6e2c43b370ae8cd Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 23 Oct 2025 15:02:13 -0700 Subject: [PATCH 8/8] update header ads (#2266) ## Description: 1. Remove SpawnAds and replace it with AdTimer which will delete the in-game ad after the first minute. 2. remove login blocker UI, we don't use it anymore 3. convert TerritoryPatternsModal & GutterAds to use event based when checking for flares 4. remove window.PageOS.session.newPageView(); because it was throwing an exception 5. Convert SpawnTimer to a lit element to give it a higher z-index to stay above the header ad ## 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: evan --- src/client/Cosmetics.ts | 5 +- src/client/GutterAds.ts | 32 +++++- src/client/Main.ts | 113 +------------------ src/client/TerritoryPatternsModal.ts | 22 +++- src/client/components/PatternButton.ts | 1 - src/client/graphics/GameRenderer.ts | 19 ++-- src/client/graphics/layers/AdTimer.ts | 28 +++++ src/client/graphics/layers/SpawnAd.ts | 135 ----------------------- src/client/graphics/layers/SpawnTimer.ts | 74 +++++++++---- src/client/graphics/layers/WinModal.ts | 7 +- src/client/index.html | 9 +- 11 files changed, 153 insertions(+), 292 deletions(-) create mode 100644 src/client/graphics/layers/AdTimer.ts delete mode 100644 src/client/graphics/layers/SpawnAd.ts diff --git a/src/client/Cosmetics.ts b/src/client/Cosmetics.ts index c0a501d8b..8eeb1f74d 100644 --- a/src/client/Cosmetics.ts +++ b/src/client/Cosmetics.ts @@ -74,10 +74,11 @@ export async function fetchCosmetics(): Promise { 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"; } diff --git a/src/client/GutterAds.ts b/src/client/GutterAds.ts index b47f2939b..caa33623a 100644 --- a/src/client/GutterAds.ts +++ b/src/client/GutterAds.ts @@ -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).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() { diff --git a/src/client/Main.ts b/src/client/Main.ts index 2d61e78b6..e6297b63c 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -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"; @@ -36,17 +35,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; @@ -57,6 +56,7 @@ declare global { destroyZone: (id: string) => void; pageInit: (options?: any) => void; que: Array<() => void>; + destroySticky: () => void; }; ramp: { que: Array<() => void>; @@ -265,98 +265,12 @@ class Client { }), ); - const config = await getServerConfigFromClient(); - if (!hasAllowedFlare(userMeResponse, config)) { - if (userMeResponse === false) { - // Login is required - document.body.innerHTML = ` -
-
-

${translateText("auth.login_required")}

-

${translateText("auth.redirecting")}

-
-
-
-
-
-
- - `; - setTimeout(discordLogin, 5000); - } else { - // Unauthorized - document.body.innerHTML = ` -
-
-

${translateText("auth.not_authorized")}

-

${translateText("auth.contact_admin")}

-
-
-
- `; - } - 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(); - } } }; @@ -628,12 +542,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"; }); @@ -675,7 +583,6 @@ class Client { window.fusetag.pageInit({ blockingFuseIds: ["lhs_sticky_vrec", "rhs_sticky_vrec"], }); - this.gutterAds.show(); }); return true; } else { @@ -735,15 +642,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)); -} diff --git a/src/client/TerritoryPatternsModal.ts b/src/client/TerritoryPatternsModal.ts index baf75e7be..f13faf670 100644 --- a/src/client/TerritoryPatternsModal.ts +++ b/src/client/TerritoryPatternsModal.ts @@ -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) => { + 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` diff --git a/src/client/components/PatternButton.ts b/src/client/components/PatternButton.ts index 1b7146b13..9c35347e1 100644 --- a/src/client/components/PatternButton.ts +++ b/src/client/components/PatternButton.ts @@ -137,7 +137,6 @@ export function renderPatternPreview( width: number, height: number, ): TemplateResult { - console.log("renderPatternPreview", pattern); if (pattern === null) { return renderBlankPreview(width, height); } diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 4491dede9..8b8080dcb 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -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, ]; diff --git a/src/client/graphics/layers/AdTimer.ts b/src/client/graphics/layers/AdTimer.ts new file mode 100644 index 000000000..4184e6a43 --- /dev/null +++ b/src/client/graphics/layers/AdTimer.ts @@ -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; + } + } +} diff --git a/src/client/graphics/layers/SpawnAd.ts b/src/client/graphics/layers/SpawnAd.ts deleted file mode 100644 index f8f39294f..000000000 --- a/src/client/graphics/layers/SpawnAd.ts +++ /dev/null @@ -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` -
-
- ${!this.adLoaded - ? html`${translateText("spawn_ad.loading")}` - : ""} -
-
- `; - } -} diff --git a/src/client/graphics/layers/SpawnTimer.ts b/src/client/graphics/layers/SpawnTimer.ts index da96c89a1..393cf96d4 100644 --- a/src/client/graphics/layers/SpawnTimer.ts +++ b/src/client/graphics/layers/SpawnTimer.ts @@ -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` +
+ ${this.ratios.map((ratio, i) => { + const color = this.colors[i] || "rgba(0, 0, 0, 0.5)"; + return html` +
+ `; + })} +
+ `; } } diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts index 809eb5465..3e042afe1 100644 --- a/src/client/graphics/layers/WinModal.ts +++ b/src/client/graphics/layers/WinModal.ts @@ -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) { diff --git a/src/client/index.html b/src/client/index.html index 2dcf835e7..ae46d19e8 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -97,6 +97,13 @@ src="https://cdn.fuseplatform.net/publift/tags/2/4121/fuse.js" > + +