From 6f9678840680b0331eec5d0eb854610cc0cd58c5 Mon Sep 17 00:00:00 2001 From: Abdallah Bahrawi <140177728+abdallahbahrawi1@users.noreply.github.com> Date: Sat, 13 Sep 2025 19:21:21 +0300 Subject: [PATCH 01/20] fix: traitor bug when attacking immediately after initiating an alliance (#2044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes a critical race condition bug where players could unintentionally receive the traitor debuff when alliance requests were accepted mid-attack. Critical Bug Fixes #1866 **Root Cause:** Players could bypass UI alliance checks ( isFriendly() ) by accepting alliances and immediately attacking after that, causing the server to treat the attack as betrayal Solution: Added server-side alliance validation in AttackExecution.init() This ensures attacks on allies are blocked at the server level. - Once Bots and Nations decide to attack, they breaks the alliance. I added maybeConsiderBetrayal(), which currently always returns true. I’ll add proper logic for alliance-breaking soon on another PR; this didn’t exist in the code before. - [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 regression is found: abodcraft1 --------- Co-authored-by: evanpelle --- src/core/execution/AttackExecution.ts | 55 ++++---- src/core/execution/BotExecution.ts | 7 + src/core/execution/FakeHumanExecution.ts | 35 ++++- src/core/execution/utils/BotBehavior.ts | 6 +- tests/Attack.test.ts | 158 ++++++++++++++--------- tests/BotBehavior.test.ts | 156 ++++++++++++++++++++++ tests/core/game/GameImpl.test.ts | 15 +++ 7 files changed, 343 insertions(+), 89 deletions(-) diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 4b7da9165..402c3a0d5 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -16,8 +16,6 @@ import { FlatBinaryHeap } from "./utils/FlatBinaryHeap"; // adjust path if neede const malusForRetreat = 25; export class AttackExecution implements Execution { - private breakAlliance = false; - private wasAlliedAtInit = false; // Store alliance state at initialization private active: boolean = true; private toConquer = new FlatBinaryHeap(); @@ -62,6 +60,24 @@ export class AttackExecution implements Execution { ? mg.terraNullius() : mg.player(this._targetID); + if (this._owner === this.target) { + console.error(`Player ${this._owner} cannot attack itself`); + this.active = false; + return; + } + + // ALLIANCE CHECK — block attacks on friendly (ally or same team) + if (this.target.isPlayer()) { + const targetPlayer = this.target as Player; + if (this._owner.isFriendly(targetPlayer)) { + console.warn( + `${this._owner.displayName()} cannot attack ${targetPlayer.displayName()} because they are friendly (allied or same team)`, + ); + this.active = false; + return; + } + } + if (this.target && this.target.isPlayer()) { const targetPlayer = this.target as Player; if ( @@ -70,15 +86,10 @@ export class AttackExecution implements Execution { ) { // Don't let bots embargo since they can't trade anyway. targetPlayer.addEmbargo(this._owner, true); + this.rejectIncomingAllianceRequests(targetPlayer); } } - if (this._owner === this.target) { - console.error(`Player ${this._owner} cannot attack itself`); - this.active = false; - return; - } - if (this.target.isPlayer()) { if ( this.mg.config().numSpawnPhaseTurns() + @@ -148,11 +159,6 @@ export class AttackExecution implements Execution { } if (this.target.isPlayer()) { - // Store the alliance state at initialization time to prevent race conditions - this.wasAlliedAtInit = this._owner.isAlliedWith(this.target); - if (this.wasAlliedAtInit) { - this.breakAlliance = true; - } this.target.updateRelation(this._owner, -80); } } @@ -221,20 +227,8 @@ export class AttackExecution implements Execution { return; } - const alliance = targetPlayer - ? this._owner.allianceWith(targetPlayer) - : null; - if (this.breakAlliance && alliance !== null) { - this.breakAlliance = false; - this._owner.breakAlliance(alliance); - } - if ( - targetPlayer && - this._owner.isAlliedWith(targetPlayer) && - !this.wasAlliedAtInit - ) { + if (targetPlayer && this._owner.isFriendly(targetPlayer)) { // In this case a new alliance was created AFTER the attack started. - // We should retreat to avoid the attacker becoming a traitor. this.retreat(); return; } @@ -295,6 +289,15 @@ export class AttackExecution implements Execution { } } + private rejectIncomingAllianceRequests(target: Player) { + const request = this._owner + .incomingAllianceRequests() + .find((ar) => ar.requestor() === target); + if (request !== undefined) { + request.reject(); + } + } + private addNeighbors(tile: TileRef) { if (this.attack === null) { throw new Error("Attack not initialized"); diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index ddd635cc8..8535a9b81 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -69,6 +69,13 @@ export class BotExecution implements Execution { if (toAttack !== null) { const odds = this.bot.isFriendly(toAttack) ? 6 : 3; if (this.random.chance(odds)) { + // Check and break alliance before attacking if needed + const alliance = this.bot.allianceWith(toAttack); + + if (alliance !== null) { + this.bot.breakAlliance(alliance); + } + this.behavior.sendAttack(toAttack); return; } diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 94bbfa5d9..43929b93a 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -161,6 +161,30 @@ export class FakeHumanExecution implements Execution { this.maybeAttack(); } + /** + * TODO: Implement strategic betrayal logic + * Currently this just breaks alliances without strategic consideration. + * Future implementation should consider: + * - Relative strength (troop count, territory size) compared to target + * - Risk vs reward of betrayal + * - Potential impact on relations with other players + * - Timing (don't betray when already fighting other enemies) + * - Strategic value of target's territory + * - If target is distracted + */ + private maybeConsiderBetrayal(target: Player): boolean { + if (this.player === null) throw new Error("not initialized"); + + const alliance = this.player.allianceWith(target); + + if (!alliance) return false; + + this.player.breakAlliance(alliance); + + // Successfully broken an alliance + return true; + } + private maybeAttack() { if (this.player === null || this.behavior === null) { throw new Error("not initialized"); @@ -208,6 +232,7 @@ export class FakeHumanExecution implements Execution { const toAttack = this.random.chance(2) ? enemies[0] : this.random.randElement(enemies); + if (this.shouldAttack(toAttack)) { this.behavior.sendAttack(toAttack); return; @@ -228,9 +253,17 @@ export class FakeHumanExecution implements Execution { private shouldAttack(other: Player): boolean { if (this.player === null) throw new Error("not initialized"); + if (this.player.isOnSameTeam(other)) { return false; } + + // Consider betrayal for allies + if (this.player.isAlliedWith(other)) { + const canProceed = this.maybeConsiderBetrayal(other); + return canProceed; + } + if (this.player.isFriendly(other)) { if (this.shouldDiscourageAttack(other)) { return this.random.chance(200); @@ -396,7 +429,7 @@ export class FakeHumanExecution implements Execution { private maybeSendBoatAttack(other: Player) { if (this.player === null) throw new Error("not initialized"); - if (this.player.isOnSameTeam(other)) return; + if (this.player.isFriendly(other)) return; const closest = closestTwoTiles( this.mg, Array.from(this.player.borderTiles()).filter((t) => diff --git a/src/core/execution/utils/BotBehavior.ts b/src/core/execution/utils/BotBehavior.ts index 3cf85c249..91c7e03a9 100644 --- a/src/core/execution/utils/BotBehavior.ts +++ b/src/core/execution/utils/BotBehavior.ts @@ -230,7 +230,9 @@ export class BotBehavior { } sendAttack(target: Player | TerraNullius) { - if (target.isPlayer() && this.player.isOnSameTeam(target)) return; + // Skip attacking friendly targets (allies or teammates) - decision to break alliances should be made by caller + if (target.isPlayer() && this.player.isFriendly(target)) return; + const maxTroops = this.game.config().maxTroops(this.player); const reserveRatio = target.isPlayer() ? this.reserveRatio @@ -242,7 +244,7 @@ export class BotBehavior { new AttackExecution( troops, this.player, - target.isPlayer() ? target.id() : null, + target.isPlayer() ? target.id() : this.game.terraNullius().id(), ), ); } diff --git a/tests/Attack.test.ts b/tests/Attack.test.ts index 869e8814c..78f4928e7 100644 --- a/tests/Attack.test.ts +++ b/tests/Attack.test.ts @@ -113,9 +113,22 @@ describe("Attack", () => { }); }); +let playerA: Player; +let playerB: Player; + +function addPlayerToGame( + playerInfo: PlayerInfo, + game: Game, + tile: TileRef, +): Player { + game.addPlayer(playerInfo); + game.addExecution(new SpawnExecution(playerInfo, tile)); + return game.player(playerInfo.id); +} + describe("Attack race condition with alliance requests", () => { - it("should not mark attacker as traitor when alliance is formed after attack starts", async () => { - const game = await setup("ocean_and_land", { + beforeEach(async () => { + game = await setup("ocean_and_land", { infiniteGold: true, instantBuild: true, infiniteTroops: true, @@ -127,32 +140,22 @@ describe("Attack race condition with alliance requests", () => { null, "playerA_id", ); + playerA = addPlayerToGame(playerAInfo, game, game.ref(0, 10)); + const playerBInfo = new PlayerInfo( "playerB", PlayerType.Human, null, "playerB_id", ); - - game.addPlayer(playerAInfo); - game.addPlayer(playerBInfo); - - const playerA = game.player(playerAInfo.id); - const playerB = game.player(playerBInfo.id); - - // Spawn both players - const spawnA = game.ref(0, 10); - const spawnB = game.ref(0, 15); - - game.addExecution( - new SpawnExecution(playerAInfo, spawnA), - new SpawnExecution(playerBInfo, spawnB), - ); + playerB = addPlayerToGame(playerBInfo, game, game.ref(0, 10)); while (game.inSpawnPhase()) { game.executeNextTick(); } + }); + it("should not mark attacker as traitor when alliance is formed after attack starts", async () => { // Player A sends alliance request to Player B const allianceRequest = playerA.createAllianceRequest(playerB); expect(allianceRequest).not.toBeNull(); @@ -173,13 +176,14 @@ describe("Attack race condition with alliance requests", () => { playerA.id(), null, ); - game.addExecution(counterAttackExecution); // Player B accepts the alliance request if (allianceRequest) { allianceRequest.accept(); } + game.addExecution(counterAttackExecution); + // Execute a few ticks to process the attacks for (let i = 0; i < 5; i++) { game.executeNextTick(); @@ -188,57 +192,25 @@ describe("Attack race condition with alliance requests", () => { // Player A should not be marked as traitor because the alliance was formed after the attack started expect(playerA.isTraitor()).toBe(false); + expect(playerA.isAlliedWith(playerB)).toBe(true); + expect(playerB.isAlliedWith(playerA)).toBe(true); // The attacks should have retreated due to the alliance being formed expect(playerA.outgoingAttacks()).toHaveLength(0); expect(playerB.outgoingAttacks()).toHaveLength(0); }); - it("should mark attacker as traitor when alliance existed before attack", async () => { - const game = await setup("ocean_and_land", { - infiniteGold: true, - instantBuild: true, - infiniteTroops: true, - }); - - const playerAInfo = new PlayerInfo( - "playerA", - PlayerType.Human, - null, - "playerA_id", - ); - const playerBInfo = new PlayerInfo( - "playerB", - PlayerType.Human, - null, - "playerB_id", - ); - - game.addPlayer(playerAInfo); - game.addPlayer(playerBInfo); - - const playerA = game.player(playerAInfo.id); - const playerB = game.player(playerBInfo.id); - - // Spawn both players - const spawnA = game.ref(0, 10); - const spawnB = game.ref(0, 15); - - game.addExecution( - new SpawnExecution(playerAInfo, spawnA), - new SpawnExecution(playerBInfo, spawnB), - ); - - while (game.inSpawnPhase()) { - game.executeNextTick(); - } - + it("should prevent player from attacking allied player", async () => { // Create an alliance between Player A and Player B const allianceRequest = playerA.createAllianceRequest(playerB); if (allianceRequest) { allianceRequest.accept(); } - // Player A attacks Player B (should break the alliance) + // Verify alliance exists + expect(playerA.isAlliedWith(playerB)).toBe(true); + expect(playerB.isAlliedWith(playerA)).toBe(true); + + // Player A tries to attack Player B (should be blocked) const attackExecution = new AttackExecution( null, playerA, @@ -252,7 +224,73 @@ describe("Attack race condition with alliance requests", () => { game.executeNextTick(); } - // Player A should be marked as traitor because they attacked an ally - expect(playerA.isTraitor()).toBe(true); + // No ongoing attacks should exist for either side + expect(playerA.outgoingAttacks()).toHaveLength(0); + expect(playerB.outgoingAttacks()).toHaveLength(0); + expect(playerA.incomingAttacks()).toHaveLength(0); + expect(playerB.incomingAttacks()).toHaveLength(0); + }); + + test("should cancel alliance requests if the recipient attacks", async () => { + // Player A sends alliance request to Player B + const allianceRequest = playerA.createAllianceRequest(playerB); + expect(allianceRequest).not.toBeNull(); + expect(playerB.incomingAllianceRequests()).toHaveLength(1); + + // Player B attacks Player A + const attackExecution = new AttackExecution( + null, + playerB, + playerA.id(), + null, + ); + game.addExecution(attackExecution); + + // Execute a few ticks to process the attacks + for (let i = 0; i < 5; i++) { + game.executeNextTick(); + } + // Alliance request should be denied since player B attacked + expect(playerA.outgoingAllianceRequests()).toHaveLength(0); + expect(playerB.incomingAllianceRequests()).toHaveLength(0); + }); + + test("should cancel the proper alliance request among many", async () => { + // Add a new player to have more alliance requests + const playerCInfo = new PlayerInfo( + "playerB", + PlayerType.Human, + null, + "playerB_id", + ); + const playerC = addPlayerToGame(playerCInfo, game, game.ref(10, 10)); + + // Player A sends alliance request to Player B + const allianceRequestAtoB = playerA.createAllianceRequest(playerB); + expect(allianceRequestAtoB).not.toBeNull(); + + // Player C also sends alliance request to Player B + const allianceRequestCtoB = playerC.createAllianceRequest(playerB); + expect(allianceRequestCtoB).not.toBeNull(); + + expect(playerB.incomingAllianceRequests()).toHaveLength(2); + + // Player B attacks Player A + const attackExecution = new AttackExecution( + null, + playerB, + playerA.id(), + null, + ); + game.addExecution(attackExecution); + + // Execute a few ticks to process the attacks + for (let i = 0; i < 5; i++) { + game.executeNextTick(); + } + // Alliance request A->B should be denied since player B attacked + expect(playerA.outgoingAllianceRequests()).toHaveLength(0); + // However C->B should remain + expect(playerB.incomingAllianceRequests()).toHaveLength(1); }); }); diff --git a/tests/BotBehavior.test.ts b/tests/BotBehavior.test.ts index 71b14ac0b..eaa7f0571 100644 --- a/tests/BotBehavior.test.ts +++ b/tests/BotBehavior.test.ts @@ -227,3 +227,159 @@ describe("BotBehavior.handleAllianceExtensionRequests", () => { expect(mockGame.addExecution).not.toHaveBeenCalled(); }); }); + +describe("BotBehavior Attack Behavior", () => { + let game: Game; + let bot: Player; + let human: Player; + let botBehavior: BotBehavior; + + // Helper function for basic test setup + async function setupTestEnvironment() { + const testGame = await setup("big_plains", { + infiniteGold: true, + instantBuild: true, + infiniteTroops: true, + }); + + // Add players + const botInfo = new PlayerInfo( + "bot_test", + PlayerType.Bot, + null, + "bot_test", + ); + const humanInfo = new PlayerInfo( + "human_test", + PlayerType.Human, + null, + "human_test", + ); + testGame.addPlayer(botInfo); + testGame.addPlayer(humanInfo); + + const testBot = testGame.player("bot_test"); + const testHuman = testGame.player("human_test"); + + // Assign territories + let landTileCount = 0; + testGame.map().forEachTile((tile) => { + if (!testGame.map().isLand(tile)) return; + (landTileCount++ % 2 === 0 ? testBot : testHuman).conquer(tile); + }); + + // Add troops + testBot.addTroops(5000); + testHuman.addTroops(5000); + + // Skip spawn phase + while (testGame.inSpawnPhase()) { + testGame.executeNextTick(); + } + + const behavior = new BotBehavior( + new PseudoRandom(42), + testGame, + testBot, + 0.5, + 0.5, + 0.2, + ); + + return { testGame, testBot, testHuman, behavior }; + } + + // Helper functions for tile assignment + function assignAlternatingLandTiles( + game: Game, + players: Player[], + totalTiles: number, + ) { + let assigned = 0; + game.map().forEachTile((tile) => { + if (assigned >= totalTiles) return; + if (!game.map().isLand(tile)) return; + const player = players[assigned % players.length]; + player.conquer(tile); + assigned++; + }); + } + + beforeEach(async () => { + const env = await setupTestEnvironment(); + game = env.testGame; + bot = env.testBot; + human = env.testHuman; + botBehavior = env.behavior; + }); + + test("bot cannot attack allied player", () => { + // Form alliance (bot creates request to human) + const allianceRequest = bot.createAllianceRequest(human); + allianceRequest?.accept(); + + expect(bot.isAlliedWith(human)).toBe(true); + + // Count attacks before attempting attack + const attacksBefore = bot.outgoingAttacks().length; + + // Attempt attack (should be blocked) + botBehavior.sendAttack(human); + + // Execute a few ticks to process the attacks + for (let i = 0; i < 5; i++) { + game.executeNextTick(); + } + + expect(bot.isAlliedWith(human)).toBe(true); + expect(human.incomingAttacks()).toHaveLength(0); + // Should be same number of attacks (no new attack created) + expect(bot.outgoingAttacks()).toHaveLength(attacksBefore); + }); + + test("nation cannot attack allied player", () => { + // Create nation + const nationInfo = new PlayerInfo( + "nation_test", + PlayerType.FakeHuman, + null, + "nation_test", + ); + game.addPlayer(nationInfo); + const nation = game.player("nation_test"); + + // Use helper for tile assignment + assignAlternatingLandTiles(game, [bot, human, nation], 21); // 21 to ensure each gets 7 tiles + + nation.addTroops(1000); + + const nationBehavior = new BotBehavior( + new PseudoRandom(42), + game, + nation, + 0.5, + 0.5, + 0.2, + ); + + // Alliance between nation and human + const allianceRequest = nation.createAllianceRequest(human); + allianceRequest?.accept(); + + expect(nation.isAlliedWith(human)).toBe(true); + + const attacksBefore = nation.outgoingAttacks().length; + nation.addTroops(50_000); + + // Nation tries to attack ally (should be blocked) + nationBehavior.sendAttack(human); + + // Execute a few ticks to process the attacks + for (let i = 0; i < 5; i++) { + game.executeNextTick(); + } + + expect(nation.isAlliedWith(human)).toBe(true); + expect(nation.outgoingAttacks()).toHaveLength(attacksBefore); + }); +}); diff --git a/tests/core/game/GameImpl.test.ts b/tests/core/game/GameImpl.test.ts index a48cdb145..831036c2f 100644 --- a/tests/core/game/GameImpl.test.ts +++ b/tests/core/game/GameImpl.test.ts @@ -79,6 +79,12 @@ describe("GameImpl", () => { game.executeNextTick(); game.executeNextTick(); + // STEP 1: First betray (manually break alliance) + const alliance = attacker.allianceWith(defender); + expect(alliance).toBeTruthy(); + attacker.breakAlliance(alliance!); + + // STEP 2: Then attack after betrayal game.addExecution(new AttackExecution(100, attacker, defender.id())); do { @@ -86,6 +92,7 @@ describe("GameImpl", () => { } while (attacker.outgoingAttacks().length > 0); expect(attacker.isTraitor()).toBe(false); + expect(attacker.allianceWith(defender)).toBeFalsy(); }); test("Do become traitor when betraying active player", async () => { @@ -110,6 +117,13 @@ describe("GameImpl", () => { game.executeNextTick(); game.executeNextTick(); + // First betray (manually break alliance) + const alliance = attacker.allianceWith(defender); + expect(alliance).toBeTruthy(); + attacker.breakAlliance(alliance!); + + game.executeNextTick(); + game.addExecution(new AttackExecution(100, attacker, defender.id())); do { @@ -117,5 +131,6 @@ describe("GameImpl", () => { } while (attacker.outgoingAttacks().length > 0); expect(attacker.isTraitor()).toBe(true); + expect(attacker.allianceWith(defender)).toBeFalsy(); }); }); From 2b29dfbb9a9538a4213230c0583b202830ac57cb Mon Sep 17 00:00:00 2001 From: evanpelle Date: Fri, 26 Sep 2025 16:01:40 -0700 Subject: [PATCH 02/20] show steam wishlist on WinModal 25% of the time --- src/client/graphics/layers/WinModal.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts index 753572f2f..1b030ea3d 100644 --- a/src/client/graphics/layers/WinModal.ts +++ b/src/client/graphics/layers/WinModal.ts @@ -33,6 +33,8 @@ export class WinModal extends LitElement implements Layer { private _title: string; + private rand = Math.random(); + // Override to prevent shadow DOM creation createRenderRoot() { return this; @@ -93,6 +95,9 @@ export class WinModal extends LitElement implements Layer { } innerHtml() { + if (this.rand < 0.25) { + return this.steamWishlist(); + } return this.renderPatternButton(); } From a54af870c237f5943d205c0a7931ff30c6bf20bb Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 30 Sep 2025 13:18:03 -0700 Subject: [PATCH 03/20] Updateadstxt (#2121) ## Description: Update ads.txt ## 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 --- resources/ads.txt | 1011 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1011 insertions(+) create mode 100644 resources/ads.txt diff --git a/resources/ads.txt b/resources/ads.txt new file mode 100644 index 000000000..af580bd85 --- /dev/null +++ b/resources/ads.txt @@ -0,0 +1,1011 @@ +# Publift Fuse ads.txt # +# Updated: Sep 2025 # +# Contact: support@publift.com # + + +# Publift + +OWNERDOMAIN=openfront.io +MANAGERDOMAIN=publift.com +publift.com, 01K62JJMRS9RQ7CND6PN13CYK7, DIRECT + +# Google + +google.com, pub-5884294479391638, RESELLER, f08c47fec0942fa0 + + +# Pubmatic + +pubmatic.com, 156230, RESELLER, 5d62403b186f2ace +pubmatic.com, 156762, RESELLER, 5d62403b186f2ace +pubmatic.com, 156974, RESELLER, 5d62403b186f2ace +pubmatic.com, 157586, RESELLER, 5d62403b186f2ace + + +# AppNexus + +appnexus.com, 9623, DIRECT, f5ab79cb980f11d1 + + +# Rubicon + +rubiconproject.com, 11504, DIRECT, 0bfd66d529a55807 +rubiconproject.com, 20884, DIRECT, 0bfd66d529a55807 +rubiconproject.com, 17348, RESELLER, 0bfd66d529a55807 + + +# OpenX + +openx.com, 540717835, RESELLER, 6a698e2ec38604c6 +openx.com, 540938618, DIRECT, 6a698e2ec38604c6 +openx.com, 557939709, DIRECT, 6a698e2ec38604c6 + + +# Criteo + +themediagrid.com, U9IDX4, DIRECT, 35d5010d7789b49d + + +# Teads + +teads.tv, 19340, DIRECT, 15a9c44f6d26cbe1 + + +# Index Exchange + +indexexchange.com, 186270, RESELLER, 50b1c356f2c5c8fc + + +# Sovrn + +lijit.com, 267370, DIRECT, fafdf38b16bf6b2b #SOVRN +lijit.com, 267370-eb, DIRECT, fafdf38b16bf6b2b #SOVRN +openx.com, 538959099, RESELLER, 6a698e2ec38604c6 +pubmatic.com, 137711, RESELLER, 5d62403b186f2ace +pubmatic.com, 156212, RESELLER, 5d62403b186f2ace +rubiconproject.com, 17960, RESELLER, 0bfd66d529a55807 +appnexus.com, 1019, RESELLER, f5ab79cb980f11d1 +video.unrulymedia.com, 2444764291, RESELLER +krushmedia.com, AJxF6R572a9M6CaTvK, RESELLER, +motorik.io, 100463, RESELLER +smaato.com, 1100056344, RESELLER, 07bcf65f187117b4 +smartadserver.com, 4926, RESELLER, 060d053dcf45cbf3 + + +# GumGum + +gumgum.com,13654,DIRECT,ffdef49475d318a9 +rubiconproject.com,23434,RESELLER,0bfd66d529a55807 +pubmatic.com,157897,RESELLER,5d62403b186f2ace +appnexus.com,2758,RESELLER,f5ab79cb980f11d1 +contextweb.com,558355,RESELLER,89ff185a4c4e857c +openx.com,537149485,RESELLER,6a698e2ec38604c6 +improvedigital.com,1884,RESELLER +conversantmedia.com,100978,RESELLER,03113cd04947736d + + +# TripleLift + +triplelift.com, 3084, DIRECT, 6c33edb13117fd86 +triplelift.com, 3084-EB, DIRECT, 6c33edb13117fd86 +themediagrid.com, GODNC4, RESELLER, 35d5010d7789b49d + + +# Amazon + +aps.amazon.com,8b48e249-e9e6-4a52-8b48-396ea93403e8,DIRECT +pubmatic.com,160006,RESELLER,5d62403b186f2ace +pubmatic.com,160096,RESELLER,5d62403b186f2ace +pubmatic.com,157150,RESELLER,5d62403b186f2ace +appnexus.com,1908,RESELLER,f5ab79cb980f11d1 +smaato.com,1100044650,RESELLER,07bcf65f187117b4 +ad-generation.jp,12474,RESELLER,7f4ea9029ac04e53 +districtm.io,100962,RESELLER,3fd707be9c4527c3 +yieldmo.com,2719019867620450718,RESELLER +appnexus.com,3663,RESELLER,f5ab79cb980f11d1 +rhythmone.com,1654642120,RESELLER,a670c89d4a324e47 +yahoo.com,55029,RESELLER,e1a5b5b6e3255540 +gumgum.com,14141,RESELLER,ffdef49475d318a9 +admanmedia.com,726,RESELLER +sharethrough.com,7144eb80,RESELLER,d53b998a7bd4ecd2 +emxdgt.com,2009,RESELLER,1e1d41537f7cad7f +contextweb.com,562541,RESELLER,89ff185a4c4e857c +themediagrid.com,JTQKMP,RESELLER,35d5010d7789b49d +beachfront.com,14804,RESELLER,e2541279e8e2ca4d +improvedigital.com,2050,RESELLER +mintegral.com,10043,RESELLER,0aeed750c80d6423 +sonobi.com,7f5fa520f8,RESELLER,d1a215d9eb5aee9e +triplelift.com,2985,DIRECT,6c33edb13117fd86 +indexexchange.com,200570,DIRECT,50b1c356f2c5c8fc +media.net,8CUZ1MK22,RESELLER +risecodes.com,63832beef8189a00015cb6d3,RESELLER +uis.mobfox.com,93308,RESELLER,5529a3d1f59865be +mediago.io,045ac24b888bcf59a09731e7f0f2084f,RESELLER +adyoulike.com,7463c359225e043c111036d7a29affa5,RESELLER,4ad745ead2958bf7 +minutemedia.com,01gya4708ddm,RESELLER +visiblemeasures.com,1052,RESELLER +undertone.com,4205,RESELLER,d954590d0cb265b9 +imds.tv,82606,RESELLER,ae6c32151e71f19d +kargo.com,8824,RESELLER +nativo.com,5711,RESELLER,59521ca7cc5e9fee +start.io,123111883,RESELLER +supply.colossusssp.com,836,RESELLER,6c5b49d96ec1b458 +opera.com,pub12058951686464,RESELLER,55a0c5fd61378de3 +richaudience.com,MMuGgvZcVd,RESELLER + + +# ConnectAd + +connectad.io, 152, DIRECT, 85ac85a30c93b3e5 +adform.com, 768, RESELLER, 9f5210a2f0999e32 +rubiconproject.com, 26800, RESELLER, 0bfd66d529a55807 + +# ConnectAd - Extended +openx.com, 537145117, RESELLER, 6a698e2ec38604c6 +lijit.com, 244287, RESELLER, fafdf38b16bf6b2b + + +# AdaptMX + +amxrtb.com, 105199401, DIRECT +appnexus.com, 12290, RESELLER, f5ab79cb980f11d1 +adform.com, 2865, RESELLER +appnexus.com, 9393, RESELLER, f5ab79cb980f11d1 +openx.com, 559680764, RESELLER, 6a698e2ec38604c6 +rubiconproject.com, 23844, RESELLER, 0bfd66d529a55807 +pubmatic.com, 161527, RESELLER, 5d62403b186f2ace +pubmatic.com, 158355, RESELLER, 5d62403b186f2ace +lijit.com, 260380, RESELLER, fafdf38b16bf6b2b +appnexus.com, 11924, RESELLER, f5ab79cb980f11d1 +appnexus.com, 11786, RESELLER, f5ab79cb980f11d1 +sharethrough.com, a6a34444, RESELLER, d53b998a7bd4ecd2 + + +# Kargo + +kargo.com, 8538, DIRECT +rubiconproject.com, 11864, RESELLER, 0bfd66d529a55807 +appnexus.com, 8173, RESELLER, f5ab79cb980f11d1 +contextweb.com, 562001, RESELLER, 89ff185a4c4e857c +video.unrulymedia.com, 1858504412, RESELLER + + +# 33Across + +33across.com, 0010b00002QKn54AAD, DIRECT, bbea06d9c4d2853c #33Across #hb #tag + +# 33Across - Rubicon +rubiconproject.com, 16414, RESELLER, 0bfd66d529a55807 #33Across #hb #tag +rubiconproject.com, 21642, RESELLER, 0bfd66d529a55807 #33Across #hb #tag #viewable +rubiconproject.com, 21434, RESELLER, 0bfd66d529a55807 #33Across #tag #ebda +rubiconproject.com, 21720, RESELLER, 0bfd66d529a55807 #33Across EU #hb #tag + +# 33Across - PubMatic +pubmatic.com, 156423, RESELLER, 5d62403b186f2ace #33Across #hb #tag #video +pubmatic.com, 158136, RESELLER, 5d62403b186f2ace #33Across EU #hb #tag +pubmatic.com, 158569, RESELLER, 5d62403b186f2ace #33Across #tag #ebda + +# 33Across - AppNexus +appnexus.com, 10239, RESELLER, f5ab79cb980f11d1 #33Across #hb #tag #viewable +appnexus.com, 1001, RESELLER, f5ab79cb980f11d1 #33Across #tag +appnexus.com, 3135, RESELLER, f5ab79cb980f11d1 #33Across #tag + +# 33Across - OpenX +openx.com, 537120563, RESELLER, 6a698e2ec38604c6 #33Across #hb #tag #video +openx.com, 539392223, RESELLER, 6a698e2ec38604c6 #33Across #tag #ebda + +# 33Across - Verizon Media +yahoo.com, 57289, RESELLER, e1a5b5b6e3255540 #33Across #hb #tag + +# 33Across - Index Exchange +indexexchange.com, 190966, RESELLER, 50b1c356f2c5c8fc #33Across #tag #ebda +indexexchange.com, 191973, RESELLER, 50b1c356f2c5c8fc #33Across #hb #tag #viewable #video + +# 33Across - Conversant Media +conversantmedia.com, 100141, RESELLER, 03113cd04947736d #hb #tag #video +appnexus.com, 4052, RESELLER,f5ab79cb980f11d1 #33Across #hb +contextweb.com, 561998, RESELLER, 89ff185a4c4e857c #33Across #hb +openx.com, 540031703, RESELLER, 6a698e2ec38604c6 #33Across #hb +pubmatic.com, 158100, RESELLER, 5d62403b186f2ace #33Across #hb +yahoo.com, 55771, RESELLER, e1a5b5b6e3255540 #33Across #hb +appnerve.com, 187287, RESELLER #hb +smartyads.com,300045, RESELLER, fd2bde0ff2e62c5d #hb +e-planning.net,1bf7b5d803f178c4,RESELLER,c1ba615865ed87b2 +video.unrulymedia.com, 645663965, RESELLER + +# 33Across - Google +google.com, pub-9557089510405422, RESELLER, f08c47fec0942fa0 #33Across #tag + +# 33Across - Sonobi +sonobi.com, a416546bb7, RESELLER, d1a215d9eb5aee9e #33Across #tag #ebda + +# 33Across - Amazon +aps.amazon.com, 2840f06c-5d89-4853-a03e-3bfa567dd33c, DIRECT #33Across #tag +openx.com, 540191398, RESELLER, 6a698e2ec38604c6 +rubiconproject.com, 18020, RESELLER, 0bfd66d529a55807 +adtech.com, 12068, RESELLER, e1a5b5b6e3255540 #33Across #tag + +# 33Across - Pulsepoint +contextweb.com, 561516, RESELLER, 89ff185a4c4e857c #33Across #hb #tag + +# 33Across - TripleLift +triplelift.com, 12503, RESELLER, 6c33edb13117fd86 + +# 33Across - Acuity Ads +admanmedia.com, 1055, RESELLER +visiblemeasures.com, 1055, RESELLER +yahoo.com, 59674, RESELLER, e1a5b5b6e3255540 + +# 33Across - LoopMe +loopme.com,11575,RESELLER,6c8d5f95897a5a3b +xandr.com, 13799, RESELLER +lijit.com, 400766, RESELLER, fafdf38b16bf6b2b +freewheel.tv, 1137745, RESELLER +freewheel.tv, 1138513, RESELLER +sharethrough.com, 6qlnf8SY, RESELLER, d53b998a7bd4ecd2 +triplelift.com, 12158, RESELLER, 6c33edb13117fd86 +rubiconproject.com, 20744, RESELLER, 0bfd66d529a55807 +pubmatic.com, 158154, RESELLER, 5d62403b186f2ace +sonobi.com, b43e9530e7, RESELLER, d1a215d9eb5aee9e + +# 33Across - Unruly +video.unrulymedia.com, 2439829435, RESELLER + +# 33Across - OpenWeb +adyoulike.com, 1f301d3bcd723f5c372070bdfd142940, RESELLER, 4ad745ead2958bf7 +loopme.com, 11480, RESELLER, 6c8d5f95897a5a3b +pubmatic.com, 160925, RESELLER, 5d62403b186f2ace +rubiconproject.com, 20736, RESELLER, 0bfd66d529a55807 +spotim.market, sp_AYL2022, RESELLER, 077e5f709d15bdbb +betweendigital.com, 44774, RESELLER +lijit.com, 408376, RESELLER, fafdf38b16bf6b2b #SOVRN +lijit.com, 408376-eb, RESELLER, fafdf38b16bf6b2b #SOVRN +smartadserver.com, 4144, RESELLER +onetag.com, 7a07370227fc000, RESELLER +admixer.net, 5e789729-1e92-41ca-8b4f-987c6edae9fe, RESELLER +nativo.com, 5848, RESELLER, 59521ca7cc5e9fee +33across.com, 0015a00003HljHyAAJ, RESELLER +video.unrulymedia.com, 297618916, RESELLER +risecodes.com, 64c7a4acd6298f0001a7d867, RESELLER + +# 33Across - IQZone +iqzone.com,IQ299,RESELLER +zetaglobal.net,998,RESELLER +lijit.com,456186,RESELLER,fafdf38b16bf6b2b +video.unrulymedia.com,5336134699710583737,RESELLER +freewheel.tv,1599302,RESELLER +freewheel.tv,1599303,RESELLER + +# 33Across - Krush Media +krushmedia.com, AJxF6R615a9M6CaTvK, RESELLER +google.com, pub-7734005103835923, RESELLER, f08c47fec0942fa0 +loopme.com,12858,RESELLER,6c8d5f95897a5a3b +onetag.com, 848879e82ca5940, RESELLER +freewheel.tv, 1522514, RESELLER +freewheel.tv, 1522338, RESELLER +media.net, 8CUT87EOU, RESELLER + +# 33Across - BetweenX +betweendigital.com, 43962, RESELLER +lijit.com, 273644, RESELLER, fafdf38b16bf6b2b +contextweb.com, 562827, RESELLER, 89ff185a4c4e857c +pubmatic.com, 159668, RESELLER, 5d62403b186f2ace +onetag.com, 5d1628750185ace, RESELLER +smartadserver.com, 4467, RESELLER +richaudience.com, 4AoWPWXbVu, RESELLER + + +# Inmobi + +inmobi.com, 4fcebe6f9a714a95b066cfdbd5d354d4, DIRECT, 83e75a7ae333ca9d +rubiconproject.com, 11726, RESELLER, 0bfd66d529a55807 +rubiconproject.com, 12266, RESELLER, 0bfd66d529a55807 +conversantmedia.com, 40881, RESELLER, 03113cd04947736d +loopme.com, 9724, RESELLER, 6c8d5f95897a5a3b +lijit.com, 502742, RESELLER, fafdf38b16bf6b2b +pubmatic.com, 156931, RESELLER, 5d62403b186f2ace +pubmatic.com, 157097, RESELLER, 5d62403b186f2ace +verve.com, 5897, RESELLER, 0c8f5958fc2d6270 +blis.com, 33, RESELLER, 61453ae19a4b73f4 +pubmatic.com, 159035, RESELLER, 5d62403b186f2ace +axonix.com, 57716, RESELLER +thebrave.io, 1234568, RESELLER, c25b2154543746ac +se7en.es, 212430, RESELLER, 064bc410192443d8 +gamaigroup.com, 320201, RESELLER +iqzone.com, IQ87, RESELLER +yeahmobi.com, 5135082, RESELLER +pubnative.net, 1006951, RESELLER, d641df8625486a7b +appnexus.com, 14077, RESELLER, f5ab79cb980f11d1 +admanmedia.com, 2063, RESELLER +themediagrid.com, B8N9YH, RESELLER, 35d5010d7789b49d +video.unrulymedia.com, 188404962, RESELLER +contextweb.com, 558638, RESELLER, 89ff185a4c4e857c + + +# Nobid + +nobid.io, 22013853948, DIRECT +zetaglobal.net, 693, RESELLER +sharethrough.com, aRE1degH, RESELLER, d53b998a7bd4ecd2 +rubiconproject.com, 18694, RESELLER, 0bfd66d529a55807 +rubiconproject.com, 24434, RESELLER, 0bfd66d529a55807 +amxrtb.com, 105199579, RESELLER +33across.com, 0010b00002Mq2FYAAZ, RESELLER, bbea06d9c4d2853c +lijit.com, 273657, RESELLER, fafdf38b16bf6b2b +video.unrulymedia.com, 347774562, RESELLER +pubmatic.com, 159277, RESELLER, 5d62403b186f2ace +inmobi.com, 8f261ace12c3486ba2e0d2011cd97976, RESELLER, 83e75a7ae333ca9d +media.net, 8CUV34PJ4, RESELLER +rubiconproject.com, 15268, RESELLER, 0bfd66d529a55807 +yieldmo.com, 3819255242365542618, DIRECT +media.net, 8CU3M1HM4, DIRECT + + +# Smart + +smartadserver.com, 4191, RESELLER, 060d053dcf45cbf3 +smartadserver.com, 4191-OB, RESELLER, 060d053dcf45cbf3 +pubmatic.com, 156439, RESELLER, 5d62403b186f2ace +pubmatic.com, 154037, RESELLER, 5d62403b186f2ace +openx.com, 537149888, RESELLER, 6a698e2ec38604c6 +appnexus.com, 3703, RESELLER, f5ab79cb980f11d1 +loopme.com, 5679, RESELLER, 6c8d5f95897a5a3b +xad.com, 958, RESELLER, 81cbf0a75a5e0e9a +video.unrulymedia.com, 2564526802, RESELLER, 6f752381ad5ec0e5 +smaato.com, 1100044045, RESELLER, 07bcf65f187117b4 +pubnative.net, 1006576, RESELLER, d641df8625486a7b +verve.com, 15503, RESELLER, 0c8f5958fc2d6270 +adyoulike.com, b4bf4fdd9b0b915f746f6747ff432bde, RESELLER, 4ad745ead2958bf7 +axonix.com, 57264, RESELLER, bc385f2b4a87b721 +admanmedia.com, 43, RESELLER +sharethrough.com, OAW69Fon, RESELLER, d53b998a7bd4ecd2 +contextweb.com, 560288, RESELLER, 89ff185a4c4e857c +rhebus.works, 5252004478, RESELLER +contextweb.com, 563115, RESELLER, 89ff185a4c4e857c +ogury.com, ede8d6ba-5a3f-4dfa-85aa-8cfb3c42f970, RESELLER + + +# Adagio + +adagio.io, 1140, DIRECT + +# Adagio - Magnite +rubiconproject.com, 19116, RESELLER, 0bfd66d529a55807 + +# Adagio - Pubmatic +pubmatic.com, 159110, RESELLER, 5d62403b186f2ace + +# Adagio - Improve Digital +improvedigital.com, 1790, RESELLER + +# Adagio - Onetag +onetag.com, 6b859b96c564fbe, RESELLER + +# Adagio - Index Exchange +indexexchange.com, 194558, RESELLER, 50b1c356f2c5c8fc + +# Adagio - 33Across +33across.com, 0015a00002oUk4aAAC, RESELLER, bbea06d9c4d2853c + +# Adagio - Equativ +smartadserver.com, 3554, RESELLER + +# Adagio - Sovrn +lijit.com, 367236, RESELLER, fafdf38b16bf6b2b + +# Adagio - OpenX +openx.com, 558899373, RESELLER, 6a698e2ec38604c6 + +# Adagio - Triplelift +triplelift.com, 13482, RESELLER, 6c33edb13117fd86 + +# Adagio - E-Planning +e-planning.net, 83c06e81531537f4, RESELLER, c1ba615865ed87b2 +rubiconproject.com, 12186, RESELLER, 0bfd66d529a55807 +zetaglobal.net, 891, RESELLER +appnexus.com, 15941, RESELLER, f5ab79cb980f11d1 + +# Adagio - Illumin +admanmedia.com, 2216, RESELLER + +# Adagio - ConnectAd +connectad.io, 456, RESELLER, 85ac85a30c93b3e5 + + +# Sharethrough + +sharethrough.com, EU8CIOkx, RESELLER, d53b998a7bd4ecd2 + + +# Onetag + +onetag.com, 77253abd802c05e, DIRECT +onetag.com, 77253abd802c05e-OB, DIRECT +appnexus.com, 13099, RESELLER, f5ab79cb980f11d1 +pubmatic.com, 161593, RESELLER, 5d62403b186f2ace +rubiconproject.com, 11006, RESELLER, 0bfd66d529a55807 +video.unrulymedia.com, 586616193, RESELLER + + +# Ogury + +ogury.com, 5a93b205-d86c-4e96-a62e-01f593889ed0, DIRECT +appnexus.com, 11470, RESELLER, f5ab79cb980f11d1 +pubmatic.com, 163238, RESELLER, 5d62403b186f2ace +smartadserver.com, 4537, RESELLER, 060d053dcf45cbf3 +rubiconproject.com, 25198, RESELLER, 0bfd66d529a55807 +thebrave.io, 1234746, RESELLER, c25b2154543746ac +toponad.com, 16719c20554a4f, RESELLER, 1d49fe424a1a456d +video.unrulymedia.com, 533898005, RESELLER #Nexxen +dauup.com, 34141, RESELLER #Edge226 + + +# OMS + +onlinemediasolutions.com, 20605, DIRECT, b3868b187e4b6402 +onomagic.com, 206051, DIRECT +amxrtb.com, 105199514, RESELLER +pubmatic.com, 161332, RESELLER, 5d62403b186f2ace +rubiconproject.com, 20416, RESELLER, 0bfd66d529a55807 +onetag.com, 75753f1ebcc343c, RESELLER +lijit.com, 374814, RESELLER, fafdf38b16bf6b2b +openx.com, 537153209, RESELLER, 6a698e2ec38604c6 +media.net, 8CUB46Z7R, RESELLER +onetag.com, 7b561459c997848, RESELLER +audienciad.com, 206052, DIRECT +video.unrulymedia.com, 6694405583287859332, RESELLER +aps.amazon.com, 48266a61-b3d9-4cb7-b172-553abc6a42a4, RESELLER +yieldmo.com, 2757543169808605705, RESELLER +rubiconproject.com, 24364, RESELLER, 0bfd66d529a55807 +getmediamx.com, 1220605, DIRECT +appnexus.com, 11801, RESELLER, f5ab79cb980f11d1 +appnexus.com, 15629, RESELLER, f5ab79cb980f11d1 +appnexus.com, 15127, RESELLER, f5ab79cb980f11d1 +loopme.com, 12733, RESELLER, 6c8d5f95897a5a3b +themediagrid.com, IRK975, RESELLER, 35d5010d7789b49d +sonobi.com, 3aed893727, RESELLER, d1a215d9eb5aee9e +sharethrough.com, LxFeZvU4, RESELLER, d53b998a7bd4ecd2 +advibe.media, 820605, DIRECT +adyoulike.com, e9a771d72c076dbe3cafc2c6477f9238, RESELLER, 4ad745ead2958bf7 +33across.com, 001Pg00000eHKL7IAO, RESELLER, bbea06d9c4d2853c +adform.com, 3251, RESELLER, 9f5210a2f0999e32 + + +# Sonobi + +sonobi.com, 54feb57a02, DIRECT, d1a215d9eb5aee9e +freewheel.tv, sg1253047, RESELLER +freewheel.tv, 533600-r-523319, RESELLER + + +# Primis + +primis.tech, 30278, DIRECT, b6b21d256ef43532 +pubmatic.com, 156595, RESELLER, 5d62403b186f2ace +google.com, pub-1320774679920841, RESELLER, f08c47fec0942fa0 +openx.com, 540258065, RESELLER, 6a698e2ec38604c6 +rubiconproject.com, 20130, RESELLER, 0bfd66d529a55807 +freewheel.tv, 19133, RESELLER, 74e8e47458f74754 +smartadserver.com, 3436, RESELLER, 060d053dcf45cbf3 +indexexchange.com, 191923, RESELLER, 50b1c356f2c5c8fc +adform.com, 2078, RESELLER +media.net, 8CU695QH7, RESELLER +video.unrulymedia.com, 2338962694, RESELLER +triplelift.com, 8210, RESELLER, 6c33edb13117fd86 +sharethrough.com, flUyJowI, RESELLER, d53b998a7bd4ecd2 +appnexus.com, 16007, RESELLER, f5ab79cb980f11d1 +yahoo.com, 59260, RESELLER +sharethrough.com, jbYv3ec8, RESELLER, d53b998a7bd4ecd2 +stroeer.com, 22739, DIRECT +ottadvisors.com, 122034096467, RESELLER +video.unrulymedia.com, 776418614052335749, RESELLER +the-ozone-project.com, OZONEPRS0001, DIRECT +appnexus.com, 9979, RESELLER, f5ab79cb980f11d1 +openx.com, 540731760, RESELLER, 6a698e2ec38604c6 +pubmatic.com, 160557, RESELLER, 5d62403b186f2ace +indexexchange.com, 206233, RESELLER, 50b1c356f2c5c8fc +pmc.com, 1240739, DIRECT, 8dd52f825890bb44 +rubiconproject.com, 10278, RESELLER, 0bfd66d529a55807 + + +# Medianet + +media.net, 8CUC2JYNF, DIRECT +media.net, 8CU995W35, DIRECT +openx.com, 537100188, RESELLER, 6a698e2ec38604c6 +pubmatic.com, 159463, RESELLER, 5d62403b186f2ace +appnexus.com, 1356, RESELLER, f5ab79cb980f11d1 +google.com, pub-7439041255533808, RESELLER, f08c47fec0942fa0 +rubiconproject.com, 19396, RESELLER, 0bfd66d529a55807 +onetag.com, 5d49f482552c9b6, RESELLER +sharethrough.com, koRtppYA, RESELLER, d53b998a7bd4ecd2 +trustedstack.com, TS9V5HI46, RESELLER + + +# Yandex + +yandex.com, 97916741, RESELLER +improvedigital.com, 2031, RESELLER +betweendigital.com, 43554, RESELLER +uis.mobfox.com, 165, RESELLER +contextweb.com, 562899,RESELLER,89ff185a4c4e857c +hyperad.tech, 150, RESELLER +hyperad.tech, 215, RESELLER +google.com, pub-5533854580432370, RESELLER, f08c47fec0942fa0 + + +# Epsilon/Conversant Media + +conversantmedia.com, 41333, DIRECT, 03113cd04947736d +rubiconproject.com, 23644, RESELLER, 0bfd66d529a55807 +lijit.com, 411121, RESELLER, fafdf38b16bf6b2b #SOVRN +admanmedia.com, 2050, RESELLER + + +# Blockthrough + +blockthrough.com, 5708166709903360, DIRECT +pubmatic.com, 160377, RESELLER, 5d62403b186f2ace +indexexchange.com, 194341, RESELLER, 50b1c356f2c5c8fc +rubiconproject.com, 23718, RESELLER, 0bfd66d529a55807 +appnexus.com, 6979, RESELLER, f5ab79cb980f11d1 +lijit.com, 251666, RESELLER, fafdf38b16bf6b2b +lijit.com, 251666-eb, RESELLER, fafdf38b16bf6b2b +themediagrid.com, 7E2DLW, RESELLER +sharethrough.com, 9zUewtvl, RESELLER, d53b998a7bd4ecd2 +yahoo.com, 59531, RESELLER, e1a5b5b6e3255540 +smartadserver.com, 4342, RESELLER +smartadserver.com, 4012, RESELLER +contextweb.com, 562926, RESELLER, 89ff185a4c4e857c + + +# Vidazoo + +vidazoo.com, 66bd9d72ba3d00c6d7fcf12d, DIRECT, b6ada874b4d7d0b2 +rubiconproject.com, 17130, RESELLER, 0bfd66d529a55807 +lijit.com, 222372, RESELLER, fafdf38b16bf6b2b +themediagrid.com, 3AW9JB, RESELLER, 35d5010d7789b49d +pubmatic.com, 159988, RESELLER, 5d62403b186f2ace +appnexus.com, 2794, RESELLER, f5ab79cb980f11d1 +openx.com, 541017750, RESELLER, 6a698e2ec38604c6 +video.unrulymedia.com, 2743945877, RESELLER +media.net, 8CUN4Y5Y3, RESELLER +sharethrough.com, S2rESyUH, RESELLER, d53b998a7bd4ecd2 +supply.colossusssp.com, 181, RESELLER, 6c5b49d96ec1b458 +triplelift.com, 11883, RESELLER, 6c33edb13117fd86 +ottadvisors.com, 122744684168, RESELLER + + +# Smile Wanted + +smilewanted.com, 5288, DIRECT +rubiconproject.com, 19814, RESELLER, 0bfd66d529a55807 +appnexus.com, 10040, RESELLER, f5ab79cb980f11d1 +smartadserver.com, 2491, RESELLER +pubmatic.com, 158810, RESELLER, 5d62403b186f2ace +openx.com, 557083110, RESELLER, 6a698e2ec38604c6 +adform.com, 3027, RESELLER +video.unrulymedia.com, 1767448067723954599, RESELLER +sharethrough.com, TZ1ahFV8, RESELLER, d53b998a7bd4ecd2 +onetag.com, 7f5d22b0006ab5a, RESELLER + + +# iion + +iion.io, 10184, DIRECT, 013a29748465dc57 +adsparc.com, 2056, RESELLER +sonobi.com, 35f7241993, RESELLER, d1a215d9eb5aee9e +lijit.com, 270673, RESELLER, fafdf38b16bf6b2b +appnexus.com, 14538, RESELLER, f5ab79cb980f11d1 +adform.com, 2985, RESELLER, 9f5210a2f0999e32 +video.unrulymedia.com, 346830101, RESELLER, 29bc7d05d309e1bc +loopme.com, 11594, RESELLER, 6c8d5f95897a5a3b +improvedigital.com, 2226, RESELLER +rubiconproject.com, 25322, RESELLER, 0bfd66d529a55807 +pubmatic.com, 164778, RESELLER, 5d62403b186f2ace +smartadserver.com, 4618, RESELLER, 060d053dcf45cbf3 +onetag.com, 89dd525077ba15e, RESELLER +media.net, 8CU8564R6, RESELLER +sharethrough.com, 249198ac, RESELLER, d53b998a7bd4ecd2 +smilewanted.com, 5098, RESELLER +richaudience.com, 6InWSNO0Xo, RESELLER +insticator.com, f01725e4-53f4-40e0-95bb-c4206ee0b577, RESELLER, b3511ffcafb23a32 +adyoulike.com, c614fe3fe0114cbf1f9d7d878e6e7ee7, RESELLER, 4ad745ead2958bf7 +smaato.com, 1100057454, RESELLER, 07bcf65f187117b4 +thebrave.io, 1234647, RESELLER, c25b2154543746ac +admanmedia.com, 2160, RESELLER +minutemedia.com, 01j6arbm5tne, RESELLER +pubmatic.com, 161683, RESELLER, 5d62403b186f2ace +appnexus.com, 8381, RESELLER, f5ab79cb980f11d1 +rubiconproject.com, 17598, RESELLER, 0bfd66d529a55807 +cpmstar.com, 54333, RESELLER, 1b929e6459dfc260 +rubiconproject.com, 23330, RESELLER, 0bfd66d529a55807 +appnexus.com, 9624, RESELLER, f5ab79cb980f11d1 +openx.com, 541079309, RESELLER, 6a698e2ec38604c6 +risecodes.com, 67d9a758597e640001744af3, RESELLER +toponad.com, 168119612ada3d, RESELLER, 1d49fe424a1a456d +programmaticx.ai, 6244523, RESELLER, b42d42eb28400efa +datawrkz.com, 2585, RESELLER +eskimi.com, 2020001133, RESELLER +opera.com, pub13158599038336, RESELLER, 55a0c5fd61378de3 + + +# Kueez + +kueez.com, afdc2db641e0e1aaa6d4da5e9b438abf, DIRECT +appnexus.com, 8826,RESELLER, f5ab79cb980f11d1 +sharethrough.com, n98xDzeL, RESELLER, d53b998a7bd4ecd2 +media.net,8CU4JTRF9, RESELLER +lijit.com, 407406, RESELLER, fafdf38b16bf6b2b #SOVRN +yieldmo.com, 3133660606033240149, RESELLER +pubmatic.com, 162110, RESELLER, 5d62403b186f2ace +themediagrid.com, UOT45Z, RESELLER, 35d5010d7789b49d +openx.com, 557564833, RESELLER, 6a698e2ec38604c6 +rubiconproject.com, 16920, RESELLER, 0bfd66d529a55807 +sonobi.com, 4c4fba1717, RESELLER, d1a215d9eb5aee9e +smartadserver.com,4288,RESELLER,060d053dcf45cbf3 +onetag.com,6e053d779444c00, RESELLER +adform.com,2926,RESELLER +zetaglobal.com, 108, RESELLER +improvedigital.com,2106,RESELLER +33across.com, 0010b00002ODU4HAAX, RESELLER, bbea06d9c4d2853c +video.unrulymedia.com, 3486482593, RESELLER +start.io, 185522363, RESELLER +loopme.com, 11576, RESELLER, 6c8d5f95897a5a3b +trustedstack.com, TSIO6G4I5, RESELLER +themediagrid.com, 7H3QAS, RESELLER, 35d5010d7789b49d +smaato.com, 1100059464, RESELLER, 07bcf65f187117b4 +smaato.com, 1100004890, RESELLER, 07bcf65f187117b4 + + +# Seedtag + +seedtag.com,67a4eeefe958720006afb8aa, DIRECT +xandr.com, 4009, RESELLER, f5ab79cb980f11d1 +beachfront.com, 15250, RESELLER, e2541279e8e2ca4d +smartadserver.com, 3050, RESELLER +rubiconproject.com, 17280, RESELLER, 0bfd66d529a55807 +pubmatic.com, 157743, RESELLER, 5d62403b186f2ace +openx.com, 558758631, RESELLER, 6a698e2ec38604c6 +lijit.com, 397546, RESELLER, fafdf38b16bf6b2b +onetag.com, 75601b04186d260, RESELLER +sharethrough.com, AXS5NfBr, RESELLER, d53b998a7bd4ecd2 +loopme.com, 11712, RESELLER, 6c8d5f95897a5a3b +video.unrulymedia.com, 724823153, RESELLER +adform.com, 1889, RESELLER +33across.com, 0010b00002MptHCAAZ, RESELLER, bbea06d9c4d2853c +adyoulike.com, 83d15ef72d387a1e60e5a1399a2b0c03, RESELLER, 4ad745ead2958bf7 +improvedigital.com, 1680, RESELLER + + +# OptiDigital + +optidigital.com,p321,DIRECT +pubmatic.com,158939,RESELLER,5d62403b186f2ace +rubiconproject.com,20336,RESELLER,0bfd66d529a55807 +smartadserver.com,3379,RESELLER,060d053dcf45cbf3 +themediagrid.com,3ETIX5,RESELLER,35d5010d7789b49d +triplelift.com,8183,RESELLER,6c33edb13117fd86 +appnexus.com,12190,RESELLER,f5ab79cb980f11d1 +onetag.com,806eabb849d0326,RESELLER +rtbhouse.com,mSu1piUSmB9TF4AQDGk4,RESELLER +33across.com,001Pg00000HMy0YIAT,RESELLER,bbea06d9c4d2853c +e-planning.net,a76893b96338e7e9,RESELLER,c1ba615865ed87b2 +video.unrulymedia.com,731539260,RESELLER + + +# Datablocks + +datablocks.net, 2729574, DIRECT, a5dfa362888cedea +rubiconproject.com, 26288, RESELLER, 0bfd66d529a55807 +appnexus.com, 11794, RESELLER, f5ab79cb980f11d1 +openx.com, 541013810, RESELLER, 6a698e2ec38604c6 +pubmatic.com, 162168, RESELLER, 5d62403b186f2ace +sharethrough.com, a47bc2a5, RESELLER, d53b998a7bd4ecd2 +152media.info, 152M72, RESELLER +media.net, 8CUM6VBVM, RESELLER +onetag.com, 74c8f583aa2ba05, RESELLER +amxrtb.com, 105199438, RESELLER +gumgum.com,14204,RESELLER,ffdef49475d318a9 +lijit.com, 273900, RESELLER, fafdf38b16bf6b2b +sonobi.com, 911eaf6707, RESELLER, d1a215d9eb5aee9e +zetaglobal.net, 505, RESELLER + + +# Insticator/Cool Media + +insticator.com,ea7873c5-662b-4e03-a3f3-cec1a01d8a95,DIRECT,b3511ffcafb23a32 +sharethrough.com,Q9IzHdvp,RESELLER,d53b998a7bd4ecd2 +rubiconproject.com,17062,RESELLER,0bfd66d529a55807 +risecodes.com,6124caed9c7adb0001c028d8,RESELLER +pubmatic.com,95054,RESELLER,5d62403b186f2ace +video.unrulymedia.com,136898039,RESELLER +openx.com,558230700,RESELLER,6a698e2ec38604c6 +lijit.com,257618,RESELLER,fafdf38b16bf6b2b +appnexus.com,3695,RESELLER,f5ab79cb980f11d1 +minutemedia.com,01garg96c88b,RESELLER + + +# Rich Audience + +richaudience.com, uUzZIc9lYN, DIRECT +appnexus.com, 8233, RESELLER, f5ab79cb980f11d1 +pubmatic.com, 81564, RESELLER, 5d62403b186f2ace +pubmatic.com, 156538, RESELLER, 5d62403b186f2ace +rubiconproject.com, 13510, RESELLER, 0bfd66d529a55807 +adform.com, 1942, RESELLER +lijit.com, 249425, RESELLER, fafdf38b16bf6b2b +video.unrulymedia.com, 592728597, RESELLER +smartadserver.com, 2640, RESELLER +adyoulike.com, f1dfbb7f133fbdb25c96e7d85a5e628b, RESELLER, 4ad745ead2958bf7 +themediagrid.com, P19GFJ, RESELLER, 35d5010d7789b49d +onetag.com, 8d4b087143c49f0, RESELLER +openx.com, 539625136, RESELLER, 6a698e2ec38604c6 + + +# InfoLinks + +infolinks.com, 3434212, DIRECT +pubmatic.com, 156872, RESELLER, 5d62403b186f2ace +xandr.com, 3251, RESELLER +lijit.com, 268479, RESELLER, fafdf38b16bf6b2b +media.net, 8CUY6IX4H, RESELLER +openx.com, 543174347, RESELLER, 6a698e2ec38604c6 +video.unrulymedia.com, 2221906906, RESELLER +improvedigital.com, 2016, RESELLER +33across.com, 0010b00002CpYhEAAV, RESELLER +sharethrough.com, 1SghQadK, RESELLER +kueez.com, f3b3789cda3d7eb0e9fa1c41057da524, RESELLER + + +# Tappx + +tappx.com,43594,DIRECT,9f375a07da0318ec +tappx.com,43574,DIRECT,9f375a07da0318ec +pubmatic.com,92509,RESELLER,5d62403b186f2ace +pubmatic.com,158111,RESELLER,5d62403b186f2ace +smartadserver.com,1692,RESELLER,060d053dcf45cbf3 +loopme.com,11227,RESELLER,6c8d5f95897a5a3b +lijit.com,396126,RESELLER,fafdf38b16bf6b2b +rubiconproject.com,13856,RESELLER,0bfd66d529a55807 +sharethrough.com,iHIgeRWP,RESELLER,d53b998a7bd4ecd2 +videoheroes.tv,212473,RESELLER,064bc410192443d8 +video.unrulymedia.com,3341072718,RESELLER +thebrave.io,1234661,RESELLER,c25b2154543746ac +improvedigital.com,1934,RESELLER +33across.com,0010b00001siQHqAAM,RESELLER,bbea06d9c4d2853c +appnexus.com,10824,RESELLER,f5ab79cb980f11d1 +appnexus.com,9569,RESELLER,f5ab79cb980f11d1 +keenkale.com,170624,RESELLER +themediagrid.com,4FDQYH,RESELLER,35d5010d7789b49d +inmobi.com,ec6f6ceb8bb1440ba5455644ec96c275,RESELLER,83e75a7ae333ca9d + + +# Illumin + +admanmedia.com, 2305, DIRECT +pubmatic.com, 165117, RESELLER, 5d62403b186f2ace +pubmatic.com, 158481, RESELLER, 5d62403b186f2ace +pubmatic.com, 162974, RESELLER, 5d62403b186f2ace +rubiconproject.com, 14558, RESELLER, 0bfd66d529a55807 +rubiconproject.com, 25386, RESELLER, 0bfd66d529a55807 +openx.com, 540866936, RESELLER, 6a698e2ec38604c6 +appnexus.com, 15349, RESELLER, f5ab79cb980f11d1 +appnexus.com, 12700, RESELLER, f5ab79cb980f11d1 +video.unrulymedia.com, 3948367200, RESELLER +sharethrough.com, XeKuhSkz, RESELLER, d53b998a7bd4ecd2 +33across.com, 0015a00002egvRSAAY, RESELLER, bbea06d9c4d2853c +smartadserver.com, 3713, RESELLER, 060d053dcf45cbf3 +zetaglobal.net, 757, RESELLER +adyoulike.com, a2226c27fc2a6773f6a2b365e013513a, RESELLER, 4ad745ead2958bf7 +loopme.com, 11386, RESELLER, 6c8d5f95897a5a3b +adform.com, 2671, RESELLER +smaato.com, 1100058015, RESELLER, 07bcf65f187117b4 +richaudience.com, iKKFhsvJ2v, RESELLER +yieldmo.com, 2807970144171533194, RESELLER +conversantmedia.com, 100308, RESELLER, 03113cd04947736d +themediagrid.com, A8X5S7, RESELLER, 35d5010d7789b49d +lijit.com, 417620, RESELLER, fafdf38b16bf6b2b +sonobi.com, 7b37f8ccbc, RESELLER, d1a215d9eb5aee9e +triplelift.com, 12456, RESELLER, 6c33edb13117fd86 +amxrtb.com, 105199820, RESELLER +media.net, 8CU58PCO4, RESELLER +trustedstack.com, TS5UCV3O4, RESELLER +improvedigital.com, 2316, RESELLER + + +# Trustedstack + +trustedstack.com, TS6Q2XDD0, DIRECT +rubiconproject.com, 26144, RESELLER, 0bfd66d529a55807 +openx.com, 559911747, RESELLER, 6a698e2ec38604c6 +pubmatic.com, 164187, RESELLER, 5d62403b186f2ace +onetag.com, 87f58fe90234d0e, RESELLER +video.unrulymedia.com, 799061815, RESELLER +sharethrough.com, KGPeiFc6, RESELLER, d53b998a7bd4ecd2 +lijit.com, 551846, RESELLER, fafdf38b16bf6b2b + + +# Legacy or Unique + +playwire.com,1025558,DIRECT + +managerdomain=playwire.com +ownerdomain=openfront.io + +# Playwire Ads.txt file v10.2 +contact=sales@playwire.com + +# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +# @ @ +# @ @@ @ +# @ @@ @@@ @ +# @ @@@@@@@@@@ @@ @@@@@@@@@@ @@ @@@ @@ @@@@ @@@ @@@ @@@@@@@@ @@@@@@@@@ @ +# @ @@@ @@ @@ @@@@@@@@@@ @@ @@@ @@ @@@@@@ @@@ @@@ @@ @@@@@@@@@ @ +# @ @@@ @@ @@ @@@ @@ @@ @@@ @@@@ @@@@ @@@ @@ @@ @ +# @ @@@@@@@@ @@ @@@@@@@@ @@@@@@@@ @@ @@ @@@ @@ @@@@@@@ @ +# @ @@@ @@@@@@@@@ @ +# @ @ +# @ @ +# @ Content Owners, no other company will demand more for you than Playwire. @ +# @ Get the demand you want by working with Playwire to amplify your revenue and @ +# @ monetize your content at scale. Visit playwire.com to learn more. @ +# @ @ +# @ Advertisers, Playwire is your one partner and all-in-one solution. Supply, @ +# @ Creative, Display, Data, Social Extension, OOH, and Experiential - we manage @ +# @ your entire multiplatform media process with a focus on brand safety. You get @ +# @ more with Playwire. Visit playwire.com/advertisers for more information. @ +# @ @ +# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + +#A9 +aps.amazon.com,bd056b42-51db-43ce-9a8e-3b11319b5d1f,DIRECT +rubiconproject.com,18020,DIRECT,0bfd66d529a55807 +risecodes.com, 63832beef8189a00015cb6d3, DIRECT +appnexus.com,3663,DIRECT,f5ab79cb980f11d1 +appnexus.com,1908,DIRECT,f5ab79cb980f11d1 +appnexus.com,1356,DIRECT,f5ab79cb980f11d1 + +#Appnexus +appnexus.com, 7140, DIRECT, f5ab79cb980f11d1 + +#Blis +blis.com, 1223, DIRECT, 61453ae19a4b73f4 +rubiconproject.com, 16928, RESELLER, 0bfd66d529a55807 + +#Conversant +conversantmedia.com, 40510, DIRECT, 03113cd04947736d + +#Google +google.com, pub-3004679189235742, DIRECT, f08c47fec0942fa0 +google.com, pub-5812357352335075, DIRECT, f08c47fec0942fa0 +google.com, pub-9728353415461720, DIRECT, f08c47fec0942fa0 + +#Gumgum +gumgum.com, 11602, DIRECT, ffdef49475d318a9 +improvedigital.com, 1884, RESELLER +google.com, pub-3848273848634341, RESELLER, f08c47fec0942fa0 +contextweb.com, 558355, RESELLER, 89ff185a4c4e857c + +#Index Exchange +indexexchange.com, 192410, DIRECT, 50b1c356f2c5c8fc +indexexchange.com, 186266, DIRECT, 50b1c356f2c5c8fc +indexexchange.com, 193336, DIRECT, 50b1c356f2c5c8fc +indexexchange.com, 186779, DIRECT, 50b1c356f2c5c8fc +indexexchange.com, 209857, DIRECT, 50b1c356f2c5c8fc + +#Inmobi +inmobi.com, 6801ebe3b36d4afba25f67b2d0e9d9ed, DIRECT, 83e75a7ae333ca9d + +#Kulture +dxkulture.com, 12005, DIRECT + +#MediaGrid +themediagrid.com, 5A8D1U, DIRECT, 35d5010d7789b49d +themediagrid.com, 6QY7B4, DIRECT, 35d5010d7789b49d +themediagrid.com, KA3Q45, DIRECT, 35d5010d7789b49d + +#Media.net +media.net, 8CUIBM874, DIRECT +media.net, 8CUP47128, DIRECT +media.net, 8CU11059L, DIRECT + +#Nativo +nativo.com, 5722, DIRECT, 59521ca7cc5e9fee + +#Nexxen +video.unrulymedia.com, 3832544212, DIRECT + +#OpenX +openx.com, 537145215, DIRECT, 6a698e2ec38604c6 +openx.com, 542511403, DIRECT, 6a698e2ec38604c6 +openx.com, 543969731, DIRECT, 6a698e2ec38604c6 +openx.com, 539872073, DIRECT, 6a698e2ec38604c6 + +#Pubmatic +pubmatic.com, 158326, DIRECT, 5d62403b186f2ace +pubmatic.com, 159344, DIRECT, 5d62403b186f2ace + +#RiseCodes +risecodes.com, 6280c6f1899612000123320b, DIRECT +risecodes.com, 66fac9081efdb80001a3dc64, DIRECT +sharethrough.com, 5926d422, RESELLER, d53b998a7bd4ecd2 +pubmatic.com, 160295, RESELLER, 5d62403b186f2ace +rubiconproject.com, 23876, RESELLER, 0bfd66d529a55807 +xandr.com, 14082, RESELLER +media.net, 8CUQ6928Q, RESELLER +video.unrulymedia.com, 335119963, RESELLER +loopme.com, 11362, RESELLER, 6c8d5f95897a5a3b +lijit.com, 405318, RESELLER, fafdf38b16bf6b2b +yieldmo.com, 2754490424016969782, RESELLER +gumgum.com, 16112, RESELLER, ffdef49475d318a9 +onetag.com, 69f48c2160c8113, RESELLER + +#Rubicon +rubiconproject.com, 12556, DIRECT, 0bfd66d529a55807 +rubiconproject.com, 18110, DIRECT, 0bfd66d529a55807 +rubiconproject.com, 27232, DIRECT, 0bfd66d529a55807 #flex + +#Sharethrough +sharethrough.com, 5b0da9d4, DIRECT, d53b998a7bd4ecd2 +pubmatic.com, 156557, RESELLER, 5d62403b186f2ace +rubiconproject.com, 18694, RESELLER, 0bfd66d529a55807 +openx.com, 540274407, RESELLER, 6a698e2ec38604c6 +video.unrulymedia.com, 266978658, RESELLER +smartadserver.com, 5247, DIRECT, 060d053dcf45cbf3 +sharethrough.com, 5247, DIRECT, d53b998a7bd4ecd2 + +#Sonobi +sonobi.com, 2da7a08406, DIRECT, d1a215d9eb5aee9e +sonobi.com, 0f403b451d, DIRECT, d1a215d9eb5aee9e + +#Sovrn +lijit.com, 230297, DIRECT, fafdf38b16bf6b2b + +#Teads +teads.tv, 16934, DIRECT, 15a9c44f6d26cbe1 + +#TradeDesk +playwire.com, 1, DIRECT + +#TripleLift +triplelift.com, 6644, DIRECT, 6c33edb13117fd86 +triplelift.com, 6732, DIRECT, 6c33edb13117fd86 +triplelift.com, 6616, DIRECT, 6c33edb13117fd86 +triplelift.com, 6613, DIRECT, 6c33edb13117fd86 +triplelift.com, 6644-EB, DIRECT, 6c33edb13117fd86 +triplelift.com, 6616-EB, DIRECT, 6c33edb13117fd86 +triplelift.com, 6732-EB, DIRECT, 6c33edb13117fd86 +triplelift.com, 8916, DIRECT, 6c33edb13117fd86 +triplelift.com, 8916-EB, DIRECT, 6c33edb13117fd86 +triplelift.com, 13434, DIRECT, 6c33edb13117fd86 +triplelift.com, 13434-EB, DIRECT, 6c33edb13117fd86 +triplelift.com, 13598, DIRECT, 6c33edb13117fd86 +triplelift.com, 4703, DIRECT, 6c33edb13117fd86 + +#TrustX +trustx.org, 100128, DIRECT, 1d2c8a747a749d25 +trustx.org, 6163, DIRECT, 1d2c8a747a749d25 + +#Vidazoo +vidazoo.com, 66f94ed86cb7baf371858cad, DIRECT, b6ada874b4d7d0b2 +video.unrulymedia.com, 2743945877, RESELLER +rubiconproject.com, 17130, RESELLER, 0bfd66d529a55807 +appnexus.com, 2794, RESELLER, f5ab79cb980f11d1 +openx.com, 541017750, RESELLER, 6a698e2ec38604c6 +pubmatic.com, 159988, RESELLER, 5d62403b186f2ace +lijit.com, 222372, RESELLER, fafdf38b16bf6b2b + +#Wunderkind +wunderkind.co, 7202, DIRECT +themediagrid.com, N71MIF, DIRECT, 35d5010d7789b49d +pubmatic.com, 156512, RESELLER, 5d62403b186f2ace +indexexchange.com, 183753, RESELLER, 50b1c356f2c5c8fc +rubiconproject.com, 20986, RESELLER, 0bfd66d529a55807 + +#Yieldmo +yieldmo.com, 2990721854664024411, DIRECT + +#SHE +pmc.com, 1243186, DIRECT, 8dd52f825890bb44 +rubiconproject.com, 10278, RESELLER, 0bfd66d529a55807 \ No newline at end of file From 30856341cdd0c7782a7e19e9ca9996a92a872167 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 30 Sep 2025 15:03:46 -0700 Subject: [PATCH 04/20] Revert "fix: traitor bug when attacking immediately after initiating an alliance (#2044)" This reverts commit 6f9678840680b0331eec5d0eb854610cc0cd58c5. --- src/core/execution/AttackExecution.ts | 55 ++++---- src/core/execution/BotExecution.ts | 7 - src/core/execution/FakeHumanExecution.ts | 35 +---- src/core/execution/utils/BotBehavior.ts | 6 +- tests/Attack.test.ts | 158 +++++++++-------------- tests/BotBehavior.test.ts | 156 ---------------------- tests/core/game/GameImpl.test.ts | 15 --- 7 files changed, 89 insertions(+), 343 deletions(-) diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts index 402c3a0d5..4b7da9165 100644 --- a/src/core/execution/AttackExecution.ts +++ b/src/core/execution/AttackExecution.ts @@ -16,6 +16,8 @@ import { FlatBinaryHeap } from "./utils/FlatBinaryHeap"; // adjust path if neede const malusForRetreat = 25; export class AttackExecution implements Execution { + private breakAlliance = false; + private wasAlliedAtInit = false; // Store alliance state at initialization private active: boolean = true; private toConquer = new FlatBinaryHeap(); @@ -60,24 +62,6 @@ export class AttackExecution implements Execution { ? mg.terraNullius() : mg.player(this._targetID); - if (this._owner === this.target) { - console.error(`Player ${this._owner} cannot attack itself`); - this.active = false; - return; - } - - // ALLIANCE CHECK — block attacks on friendly (ally or same team) - if (this.target.isPlayer()) { - const targetPlayer = this.target as Player; - if (this._owner.isFriendly(targetPlayer)) { - console.warn( - `${this._owner.displayName()} cannot attack ${targetPlayer.displayName()} because they are friendly (allied or same team)`, - ); - this.active = false; - return; - } - } - if (this.target && this.target.isPlayer()) { const targetPlayer = this.target as Player; if ( @@ -86,10 +70,15 @@ export class AttackExecution implements Execution { ) { // Don't let bots embargo since they can't trade anyway. targetPlayer.addEmbargo(this._owner, true); - this.rejectIncomingAllianceRequests(targetPlayer); } } + if (this._owner === this.target) { + console.error(`Player ${this._owner} cannot attack itself`); + this.active = false; + return; + } + if (this.target.isPlayer()) { if ( this.mg.config().numSpawnPhaseTurns() + @@ -159,6 +148,11 @@ export class AttackExecution implements Execution { } if (this.target.isPlayer()) { + // Store the alliance state at initialization time to prevent race conditions + this.wasAlliedAtInit = this._owner.isAlliedWith(this.target); + if (this.wasAlliedAtInit) { + this.breakAlliance = true; + } this.target.updateRelation(this._owner, -80); } } @@ -227,8 +221,20 @@ export class AttackExecution implements Execution { return; } - if (targetPlayer && this._owner.isFriendly(targetPlayer)) { + const alliance = targetPlayer + ? this._owner.allianceWith(targetPlayer) + : null; + if (this.breakAlliance && alliance !== null) { + this.breakAlliance = false; + this._owner.breakAlliance(alliance); + } + if ( + targetPlayer && + this._owner.isAlliedWith(targetPlayer) && + !this.wasAlliedAtInit + ) { // In this case a new alliance was created AFTER the attack started. + // We should retreat to avoid the attacker becoming a traitor. this.retreat(); return; } @@ -289,15 +295,6 @@ export class AttackExecution implements Execution { } } - private rejectIncomingAllianceRequests(target: Player) { - const request = this._owner - .incomingAllianceRequests() - .find((ar) => ar.requestor() === target); - if (request !== undefined) { - request.reject(); - } - } - private addNeighbors(tile: TileRef) { if (this.attack === null) { throw new Error("Attack not initialized"); diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index 8535a9b81..ddd635cc8 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -69,13 +69,6 @@ export class BotExecution implements Execution { if (toAttack !== null) { const odds = this.bot.isFriendly(toAttack) ? 6 : 3; if (this.random.chance(odds)) { - // Check and break alliance before attacking if needed - const alliance = this.bot.allianceWith(toAttack); - - if (alliance !== null) { - this.bot.breakAlliance(alliance); - } - this.behavior.sendAttack(toAttack); return; } diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 43929b93a..94bbfa5d9 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -161,30 +161,6 @@ export class FakeHumanExecution implements Execution { this.maybeAttack(); } - /** - * TODO: Implement strategic betrayal logic - * Currently this just breaks alliances without strategic consideration. - * Future implementation should consider: - * - Relative strength (troop count, territory size) compared to target - * - Risk vs reward of betrayal - * - Potential impact on relations with other players - * - Timing (don't betray when already fighting other enemies) - * - Strategic value of target's territory - * - If target is distracted - */ - private maybeConsiderBetrayal(target: Player): boolean { - if (this.player === null) throw new Error("not initialized"); - - const alliance = this.player.allianceWith(target); - - if (!alliance) return false; - - this.player.breakAlliance(alliance); - - // Successfully broken an alliance - return true; - } - private maybeAttack() { if (this.player === null || this.behavior === null) { throw new Error("not initialized"); @@ -232,7 +208,6 @@ export class FakeHumanExecution implements Execution { const toAttack = this.random.chance(2) ? enemies[0] : this.random.randElement(enemies); - if (this.shouldAttack(toAttack)) { this.behavior.sendAttack(toAttack); return; @@ -253,17 +228,9 @@ export class FakeHumanExecution implements Execution { private shouldAttack(other: Player): boolean { if (this.player === null) throw new Error("not initialized"); - if (this.player.isOnSameTeam(other)) { return false; } - - // Consider betrayal for allies - if (this.player.isAlliedWith(other)) { - const canProceed = this.maybeConsiderBetrayal(other); - return canProceed; - } - if (this.player.isFriendly(other)) { if (this.shouldDiscourageAttack(other)) { return this.random.chance(200); @@ -429,7 +396,7 @@ export class FakeHumanExecution implements Execution { private maybeSendBoatAttack(other: Player) { if (this.player === null) throw new Error("not initialized"); - if (this.player.isFriendly(other)) return; + if (this.player.isOnSameTeam(other)) return; const closest = closestTwoTiles( this.mg, Array.from(this.player.borderTiles()).filter((t) => diff --git a/src/core/execution/utils/BotBehavior.ts b/src/core/execution/utils/BotBehavior.ts index 91c7e03a9..3cf85c249 100644 --- a/src/core/execution/utils/BotBehavior.ts +++ b/src/core/execution/utils/BotBehavior.ts @@ -230,9 +230,7 @@ export class BotBehavior { } sendAttack(target: Player | TerraNullius) { - // Skip attacking friendly targets (allies or teammates) - decision to break alliances should be made by caller - if (target.isPlayer() && this.player.isFriendly(target)) return; - + if (target.isPlayer() && this.player.isOnSameTeam(target)) return; const maxTroops = this.game.config().maxTroops(this.player); const reserveRatio = target.isPlayer() ? this.reserveRatio @@ -244,7 +242,7 @@ export class BotBehavior { new AttackExecution( troops, this.player, - target.isPlayer() ? target.id() : this.game.terraNullius().id(), + target.isPlayer() ? target.id() : null, ), ); } diff --git a/tests/Attack.test.ts b/tests/Attack.test.ts index 78f4928e7..869e8814c 100644 --- a/tests/Attack.test.ts +++ b/tests/Attack.test.ts @@ -113,22 +113,9 @@ describe("Attack", () => { }); }); -let playerA: Player; -let playerB: Player; - -function addPlayerToGame( - playerInfo: PlayerInfo, - game: Game, - tile: TileRef, -): Player { - game.addPlayer(playerInfo); - game.addExecution(new SpawnExecution(playerInfo, tile)); - return game.player(playerInfo.id); -} - describe("Attack race condition with alliance requests", () => { - beforeEach(async () => { - game = await setup("ocean_and_land", { + it("should not mark attacker as traitor when alliance is formed after attack starts", async () => { + const game = await setup("ocean_and_land", { infiniteGold: true, instantBuild: true, infiniteTroops: true, @@ -140,22 +127,32 @@ describe("Attack race condition with alliance requests", () => { null, "playerA_id", ); - playerA = addPlayerToGame(playerAInfo, game, game.ref(0, 10)); - const playerBInfo = new PlayerInfo( "playerB", PlayerType.Human, null, "playerB_id", ); - playerB = addPlayerToGame(playerBInfo, game, game.ref(0, 10)); + + game.addPlayer(playerAInfo); + game.addPlayer(playerBInfo); + + const playerA = game.player(playerAInfo.id); + const playerB = game.player(playerBInfo.id); + + // Spawn both players + const spawnA = game.ref(0, 10); + const spawnB = game.ref(0, 15); + + game.addExecution( + new SpawnExecution(playerAInfo, spawnA), + new SpawnExecution(playerBInfo, spawnB), + ); while (game.inSpawnPhase()) { game.executeNextTick(); } - }); - it("should not mark attacker as traitor when alliance is formed after attack starts", async () => { // Player A sends alliance request to Player B const allianceRequest = playerA.createAllianceRequest(playerB); expect(allianceRequest).not.toBeNull(); @@ -176,14 +173,13 @@ describe("Attack race condition with alliance requests", () => { playerA.id(), null, ); + game.addExecution(counterAttackExecution); // Player B accepts the alliance request if (allianceRequest) { allianceRequest.accept(); } - game.addExecution(counterAttackExecution); - // Execute a few ticks to process the attacks for (let i = 0; i < 5; i++) { game.executeNextTick(); @@ -192,25 +188,57 @@ describe("Attack race condition with alliance requests", () => { // Player A should not be marked as traitor because the alliance was formed after the attack started expect(playerA.isTraitor()).toBe(false); - expect(playerA.isAlliedWith(playerB)).toBe(true); - expect(playerB.isAlliedWith(playerA)).toBe(true); // The attacks should have retreated due to the alliance being formed expect(playerA.outgoingAttacks()).toHaveLength(0); expect(playerB.outgoingAttacks()).toHaveLength(0); }); - it("should prevent player from attacking allied player", async () => { + it("should mark attacker as traitor when alliance existed before attack", async () => { + const game = await setup("ocean_and_land", { + infiniteGold: true, + instantBuild: true, + infiniteTroops: true, + }); + + const playerAInfo = new PlayerInfo( + "playerA", + PlayerType.Human, + null, + "playerA_id", + ); + const playerBInfo = new PlayerInfo( + "playerB", + PlayerType.Human, + null, + "playerB_id", + ); + + game.addPlayer(playerAInfo); + game.addPlayer(playerBInfo); + + const playerA = game.player(playerAInfo.id); + const playerB = game.player(playerBInfo.id); + + // Spawn both players + const spawnA = game.ref(0, 10); + const spawnB = game.ref(0, 15); + + game.addExecution( + new SpawnExecution(playerAInfo, spawnA), + new SpawnExecution(playerBInfo, spawnB), + ); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + // Create an alliance between Player A and Player B const allianceRequest = playerA.createAllianceRequest(playerB); if (allianceRequest) { allianceRequest.accept(); } - // Verify alliance exists - expect(playerA.isAlliedWith(playerB)).toBe(true); - expect(playerB.isAlliedWith(playerA)).toBe(true); - - // Player A tries to attack Player B (should be blocked) + // Player A attacks Player B (should break the alliance) const attackExecution = new AttackExecution( null, playerA, @@ -224,73 +252,7 @@ describe("Attack race condition with alliance requests", () => { game.executeNextTick(); } - // No ongoing attacks should exist for either side - expect(playerA.outgoingAttacks()).toHaveLength(0); - expect(playerB.outgoingAttacks()).toHaveLength(0); - expect(playerA.incomingAttacks()).toHaveLength(0); - expect(playerB.incomingAttacks()).toHaveLength(0); - }); - - test("should cancel alliance requests if the recipient attacks", async () => { - // Player A sends alliance request to Player B - const allianceRequest = playerA.createAllianceRequest(playerB); - expect(allianceRequest).not.toBeNull(); - expect(playerB.incomingAllianceRequests()).toHaveLength(1); - - // Player B attacks Player A - const attackExecution = new AttackExecution( - null, - playerB, - playerA.id(), - null, - ); - game.addExecution(attackExecution); - - // Execute a few ticks to process the attacks - for (let i = 0; i < 5; i++) { - game.executeNextTick(); - } - // Alliance request should be denied since player B attacked - expect(playerA.outgoingAllianceRequests()).toHaveLength(0); - expect(playerB.incomingAllianceRequests()).toHaveLength(0); - }); - - test("should cancel the proper alliance request among many", async () => { - // Add a new player to have more alliance requests - const playerCInfo = new PlayerInfo( - "playerB", - PlayerType.Human, - null, - "playerB_id", - ); - const playerC = addPlayerToGame(playerCInfo, game, game.ref(10, 10)); - - // Player A sends alliance request to Player B - const allianceRequestAtoB = playerA.createAllianceRequest(playerB); - expect(allianceRequestAtoB).not.toBeNull(); - - // Player C also sends alliance request to Player B - const allianceRequestCtoB = playerC.createAllianceRequest(playerB); - expect(allianceRequestCtoB).not.toBeNull(); - - expect(playerB.incomingAllianceRequests()).toHaveLength(2); - - // Player B attacks Player A - const attackExecution = new AttackExecution( - null, - playerB, - playerA.id(), - null, - ); - game.addExecution(attackExecution); - - // Execute a few ticks to process the attacks - for (let i = 0; i < 5; i++) { - game.executeNextTick(); - } - // Alliance request A->B should be denied since player B attacked - expect(playerA.outgoingAllianceRequests()).toHaveLength(0); - // However C->B should remain - expect(playerB.incomingAllianceRequests()).toHaveLength(1); + // Player A should be marked as traitor because they attacked an ally + expect(playerA.isTraitor()).toBe(true); }); }); diff --git a/tests/BotBehavior.test.ts b/tests/BotBehavior.test.ts index eaa7f0571..71b14ac0b 100644 --- a/tests/BotBehavior.test.ts +++ b/tests/BotBehavior.test.ts @@ -227,159 +227,3 @@ describe("BotBehavior.handleAllianceExtensionRequests", () => { expect(mockGame.addExecution).not.toHaveBeenCalled(); }); }); - -describe("BotBehavior Attack Behavior", () => { - let game: Game; - let bot: Player; - let human: Player; - let botBehavior: BotBehavior; - - // Helper function for basic test setup - async function setupTestEnvironment() { - const testGame = await setup("big_plains", { - infiniteGold: true, - instantBuild: true, - infiniteTroops: true, - }); - - // Add players - const botInfo = new PlayerInfo( - "bot_test", - PlayerType.Bot, - null, - "bot_test", - ); - const humanInfo = new PlayerInfo( - "human_test", - PlayerType.Human, - null, - "human_test", - ); - testGame.addPlayer(botInfo); - testGame.addPlayer(humanInfo); - - const testBot = testGame.player("bot_test"); - const testHuman = testGame.player("human_test"); - - // Assign territories - let landTileCount = 0; - testGame.map().forEachTile((tile) => { - if (!testGame.map().isLand(tile)) return; - (landTileCount++ % 2 === 0 ? testBot : testHuman).conquer(tile); - }); - - // Add troops - testBot.addTroops(5000); - testHuman.addTroops(5000); - - // Skip spawn phase - while (testGame.inSpawnPhase()) { - testGame.executeNextTick(); - } - - const behavior = new BotBehavior( - new PseudoRandom(42), - testGame, - testBot, - 0.5, - 0.5, - 0.2, - ); - - return { testGame, testBot, testHuman, behavior }; - } - - // Helper functions for tile assignment - function assignAlternatingLandTiles( - game: Game, - players: Player[], - totalTiles: number, - ) { - let assigned = 0; - game.map().forEachTile((tile) => { - if (assigned >= totalTiles) return; - if (!game.map().isLand(tile)) return; - const player = players[assigned % players.length]; - player.conquer(tile); - assigned++; - }); - } - - beforeEach(async () => { - const env = await setupTestEnvironment(); - game = env.testGame; - bot = env.testBot; - human = env.testHuman; - botBehavior = env.behavior; - }); - - test("bot cannot attack allied player", () => { - // Form alliance (bot creates request to human) - const allianceRequest = bot.createAllianceRequest(human); - allianceRequest?.accept(); - - expect(bot.isAlliedWith(human)).toBe(true); - - // Count attacks before attempting attack - const attacksBefore = bot.outgoingAttacks().length; - - // Attempt attack (should be blocked) - botBehavior.sendAttack(human); - - // Execute a few ticks to process the attacks - for (let i = 0; i < 5; i++) { - game.executeNextTick(); - } - - expect(bot.isAlliedWith(human)).toBe(true); - expect(human.incomingAttacks()).toHaveLength(0); - // Should be same number of attacks (no new attack created) - expect(bot.outgoingAttacks()).toHaveLength(attacksBefore); - }); - - test("nation cannot attack allied player", () => { - // Create nation - const nationInfo = new PlayerInfo( - "nation_test", - PlayerType.FakeHuman, - null, - "nation_test", - ); - game.addPlayer(nationInfo); - const nation = game.player("nation_test"); - - // Use helper for tile assignment - assignAlternatingLandTiles(game, [bot, human, nation], 21); // 21 to ensure each gets 7 tiles - - nation.addTroops(1000); - - const nationBehavior = new BotBehavior( - new PseudoRandom(42), - game, - nation, - 0.5, - 0.5, - 0.2, - ); - - // Alliance between nation and human - const allianceRequest = nation.createAllianceRequest(human); - allianceRequest?.accept(); - - expect(nation.isAlliedWith(human)).toBe(true); - - const attacksBefore = nation.outgoingAttacks().length; - nation.addTroops(50_000); - - // Nation tries to attack ally (should be blocked) - nationBehavior.sendAttack(human); - - // Execute a few ticks to process the attacks - for (let i = 0; i < 5; i++) { - game.executeNextTick(); - } - - expect(nation.isAlliedWith(human)).toBe(true); - expect(nation.outgoingAttacks()).toHaveLength(attacksBefore); - }); -}); diff --git a/tests/core/game/GameImpl.test.ts b/tests/core/game/GameImpl.test.ts index 831036c2f..a48cdb145 100644 --- a/tests/core/game/GameImpl.test.ts +++ b/tests/core/game/GameImpl.test.ts @@ -79,12 +79,6 @@ describe("GameImpl", () => { game.executeNextTick(); game.executeNextTick(); - // STEP 1: First betray (manually break alliance) - const alliance = attacker.allianceWith(defender); - expect(alliance).toBeTruthy(); - attacker.breakAlliance(alliance!); - - // STEP 2: Then attack after betrayal game.addExecution(new AttackExecution(100, attacker, defender.id())); do { @@ -92,7 +86,6 @@ describe("GameImpl", () => { } while (attacker.outgoingAttacks().length > 0); expect(attacker.isTraitor()).toBe(false); - expect(attacker.allianceWith(defender)).toBeFalsy(); }); test("Do become traitor when betraying active player", async () => { @@ -117,13 +110,6 @@ describe("GameImpl", () => { game.executeNextTick(); game.executeNextTick(); - // First betray (manually break alliance) - const alliance = attacker.allianceWith(defender); - expect(alliance).toBeTruthy(); - attacker.breakAlliance(alliance!); - - game.executeNextTick(); - game.addExecution(new AttackExecution(100, attacker, defender.id())); do { @@ -131,6 +117,5 @@ describe("GameImpl", () => { } while (attacker.outgoingAttacks().length > 0); expect(attacker.isTraitor()).toBe(true); - expect(attacker.allianceWith(defender)).toBeFalsy(); }); }); From bc5f18dee8827906a80883905780a31607bf6b19 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 1 Oct 2025 12:05:10 -0700 Subject: [PATCH 05/20] Hide skins & account login if in iframe (#2126) ## Description: CrazyGames doesn't allow purchase (must be integrated into their sdk), so disable it on iframe for now. ## 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/AccountModal.ts | 6 +++++- src/client/Main.ts | 5 +++++ src/client/Utils.ts | 10 ++++++++++ src/client/graphics/layers/WinModal.ts | 4 ++-- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/client/AccountModal.ts b/src/client/AccountModal.ts index b55929222..93bf0ff96 100644 --- a/src/client/AccountModal.ts +++ b/src/client/AccountModal.ts @@ -4,7 +4,7 @@ import { UserMeResponse } from "../core/ApiSchemas"; import "./components/Difficulties"; import "./components/PatternButton"; import { discordLogin, getApiBase, getUserMe, logOut } from "./jwt"; -import { translateText } from "./Utils"; +import { isInIframe, translateText } from "./Utils"; @customElement("account-modal") export class AccountModal extends LitElement { @@ -268,6 +268,10 @@ export class AccountButton extends LitElement { } render() { + if (isInIframe()) { + return html``; + } + if (!this.isVisible) { return html``; } diff --git a/src/client/Main.ts b/src/client/Main.ts index 9d4f1eba8..8b48f79cc 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -34,6 +34,7 @@ import { UsernameInput } from "./UsernameInput"; import { generateCryptoRandomUUID, incrementGamesPlayed, + isInIframe, translateText, } from "./Utils"; import "./components/NewsButton"; @@ -207,6 +208,10 @@ class Client { const patternButton = document.getElementById( "territory-patterns-input-preview-button", ); + if (isInIframe() && patternButton) { + patternButton.style.display = "none"; + } + this.patternsModal instanceof TerritoryPatternsModal; if (patternButton === null) throw new Error("territory-patterns-input-preview-button"); diff --git a/src/client/Utils.ts b/src/client/Utils.ts index 51dff4258..289d730d0 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -189,3 +189,13 @@ export function incrementGamesPlayed(): void { console.warn("Failed to increment games played in localStorage:", error); } } + +export function isInIframe(): boolean { + try { + return window.self !== window.top; + } catch (e) { + // If we can't access window.top due to cross-origin restrictions, + // we're definitely in an iframe + return true; + } +} diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts index 1b030ea3d..850766bac 100644 --- a/src/client/graphics/layers/WinModal.ts +++ b/src/client/graphics/layers/WinModal.ts @@ -1,6 +1,6 @@ import { LitElement, TemplateResult, html } from "lit"; import { customElement, state } from "lit/decorators.js"; -import { translateText } from "../../../client/Utils"; +import { isInIframe, translateText } from "../../../client/Utils"; import { ColorPalette, Pattern } from "../../../core/CosmeticSchemas"; import { EventBus } from "../../../core/EventBus"; import { GameUpdateType } from "../../../core/game/GameUpdates"; @@ -95,7 +95,7 @@ export class WinModal extends LitElement implements Layer { } innerHtml() { - if (this.rand < 0.25) { + if (isInIframe() || this.rand < 0.25) { return this.steamWishlist(); } return this.renderPatternButton(); From f6839ffe992206afc25d6dd0655a5886ecea8de5 Mon Sep 17 00:00:00 2001 From: Vivacious Box Date: Wed, 8 Oct 2025 22:49:54 +0200 Subject: [PATCH 06/20] Fix icons and add mirv to build bar (#2154) ## Description: Fix blurry icons and add mirv to build bar image image ## 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: DISCORD_USERNAME --- src/client/InputHandler.ts | 12 ++++++-- src/client/UserSettingModal.ts | 23 ++++++++++----- .../graphics/layers/PlayerInfoOverlay.ts | 16 +++++------ src/client/graphics/layers/UnitDisplay.ts | 28 +++++++++++++------ 4 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 69eb3b2e7..61f4f9635 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -188,9 +188,10 @@ export class InputHandler { buildDefensePost: "Digit4", buildMissileSilo: "Digit5", buildSamLauncher: "Digit6", - buildAtomBomb: "Digit7", - buildHydrogenBomb: "Digit8", - buildWarship: "Digit9", + buildWarship: "Digit7", + buildAtomBomb: "Digit8", + buildHydrogenBomb: "Digit9", + buildMIRV: "Digit0", ...saved, }; @@ -409,6 +410,11 @@ export class InputHandler { this.uiState.ghostStructure = UnitType.Warship; } + if (e.code === this.keybinds.buildMIRV) { + e.preventDefault(); + this.uiState.ghostStructure = UnitType.MIRV; + } + // Shift-D to toggle performance overlay console.log(e.code, e.shiftKey, e.ctrlKey, e.altKey, e.metaKey); if (e.code === "KeyD" && e.shiftKey) { diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 06d989009..5cb6cc747 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -494,11 +494,20 @@ export class UserSettingModal extends LitElement { @change=${this.handleKeybindChange} > + + @@ -507,17 +516,17 @@ export class UserSettingModal extends LitElement { action="buildHydrogenBomb" label=${translateText("user_setting.build_hydrogen_bomb")} description=${translateText("user_setting.build_hydrogen_bomb_desc")} - defaultKey="Digit8" + defaultKey="Digit9" .value=${this.keybinds["buildHydrogenBomb"]?.key ?? ""} @change=${this.handleKeybindChange} > diff --git a/src/client/graphics/layers/PlayerInfoOverlay.ts b/src/client/graphics/layers/PlayerInfoOverlay.ts index 6936c9549..83505eb28 100644 --- a/src/client/graphics/layers/PlayerInfoOverlay.ts +++ b/src/client/graphics/layers/PlayerInfoOverlay.ts @@ -2,12 +2,12 @@ import { LitElement, TemplateResult, html } from "lit"; import { ref } from "lit-html/directives/ref.js"; import { customElement, property, state } from "lit/decorators.js"; import allianceIcon from "../../../../resources/images/AllianceIcon.svg"; -import portIcon from "../../../../resources/images/AnchorIcon.png"; import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg"; import cityIcon from "../../../../resources/images/CityIconWhite.svg"; import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg"; import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg"; -import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png"; +import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.svg"; +import portIcon from "../../../../resources/images/PortIcon.svg"; import samLauncherIcon from "../../../../resources/images/SamLauncherIconWhite.svg"; import { renderPlayerFlag } from "../../../core/CustomFlag"; import { EventBus } from "../../../core/EventBus"; @@ -364,18 +364,18 @@ export class PlayerInfoOverlay extends LitElement implements Layer { cityIcon, "player_info_overlay.cities", )} - ${this.displayUnitCount( - player, - UnitType.Port, - portIcon, - "player_info_overlay.ports", - )} ${this.displayUnitCount( player, UnitType.Factory, factoryIcon, "player_info_overlay.factories", )} + ${this.displayUnitCount( + player, + UnitType.Port, + portIcon, + "player_info_overlay.ports", + )} ${this.displayUnitCount( player, UnitType.MissileSilo, diff --git a/src/client/graphics/layers/UnitDisplay.ts b/src/client/graphics/layers/UnitDisplay.ts index a78766616..7d383036a 100644 --- a/src/client/graphics/layers/UnitDisplay.ts +++ b/src/client/graphics/layers/UnitDisplay.ts @@ -3,7 +3,8 @@ import { customElement } from "lit/decorators.js"; import warshipIcon from "../../../../resources/images/BattleshipIconWhite.svg"; import cityIcon from "../../../../resources/images/CityIconWhite.svg"; import factoryIcon from "../../../../resources/images/FactoryIconWhite.svg"; -import missileSiloIcon from "../../../../resources/images/MissileSiloUnit.png"; +import mirvIcon from "../../../../resources/images/MIRVIcon.svg"; +import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.svg"; import hydrogenBombIcon from "../../../../resources/images/MushroomCloudIconWhite.svg"; import atomBombIcon from "../../../../resources/images/NukeIconWhite.svg"; import portIcon from "../../../../resources/images/PortIcon.svg"; @@ -59,7 +60,8 @@ export class UnitDisplay extends LitElement implements Layer { config.isUnitDisabled(UnitType.SAMLauncher) && config.isUnitDisabled(UnitType.Warship) && config.isUnitDisabled(UnitType.AtomBomb) && - config.isUnitDisabled(UnitType.HydrogenBomb); + config.isUnitDisabled(UnitType.HydrogenBomb) && + config.isUnitDisabled(UnitType.MIRV); this.requestUpdate(); } @@ -78,6 +80,7 @@ export class UnitDisplay extends LitElement implements Layer { switch (item) { case UnitType.AtomBomb: case UnitType.HydrogenBomb: + case UnitType.MIRV: return ( this.cost(item) <= (player?.gold() ?? 0n) && (player?.units(UnitType.MissileSilo).length ?? 0) > 0 @@ -174,26 +177,33 @@ export class UnitDisplay extends LitElement implements Layer {
+ ${this.renderUnitItem( + warshipIcon, + this._warships, + UnitType.Warship, + "warship", + this.keybinds["buildWarship"]?.key ?? "7", + )} ${this.renderUnitItem( atomBombIcon, null, UnitType.AtomBomb, "atom_bomb", - this.keybinds["buildAtomBomb"]?.key ?? "7", + this.keybinds["buildAtomBomb"]?.key ?? "8", )} ${this.renderUnitItem( hydrogenBombIcon, null, UnitType.HydrogenBomb, "hydrogen_bomb", - this.keybinds["buildHydrogenBomb"]?.key ?? "8", + this.keybinds["buildHydrogenBomb"]?.key ?? "9", )} ${this.renderUnitItem( - warshipIcon, - this._warships, - UnitType.Warship, - "warship", - this.keybinds["buildWarship"]?.key ?? "9", + mirvIcon, + null, + UnitType.MIRV, + "mirv", + this.keybinds["buildMIRV"]?.key ?? "0", )}
From 4d3e358fbca0cb097533ea559e40027d3b992a50 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Thu, 9 Oct 2025 05:58:36 +0900 Subject: [PATCH 07/20] mls (v4.7) (#2152) ## Description: 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 Co-authored-by: evanpelle --- resources/lang/eo.json | 78 +++++++++++++++++++++++++++++++---- resources/lang/fr.json | 78 +++++++++++++++++++++++++++++++---- resources/lang/hu.json | 80 ++++++++++++++++++++++++++++++++---- resources/lang/ja.json | 78 +++++++++++++++++++++++++++++++---- resources/lang/ru.json | 76 +++++++++++++++++++++++++++++++--- resources/lang/uk.json | 92 +++++++++++++++++++++++++++++++++++------- 6 files changed, 433 insertions(+), 49 deletions(-) diff --git a/resources/lang/eo.json b/resources/lang/eo.json index 1f96be7c9..4703c919d 100644 --- a/resources/lang/eo.json +++ b/resources/lang/eo.json @@ -130,9 +130,8 @@ "disable_nations": "Malŝalti naciojn", "instant_build": "Tujkonstruaĵo", "infinite_gold": "Senfina oro", - "donate_gold": "Donaci oron", "infinite_troops": "Senfinaj trupoj", - "donate_troops": "Donaci trupojn", + "compact_map": "Karteto", "disable_nukes": "Malŝalti nukleajn armilojn", "enables_title": "Ebligi Agordojn", "start": "Komenci la ludon" @@ -177,8 +176,10 @@ "halkidiki": "Ĥalkidiko", "straitofgibraltar": "Ĝibraltara Markolo", "italia": "Italio", + "japan": "Japanio", "yenisei": "Jenisejo", - "pluto": "Plutono" + "pluto": "Plutono", + "montreal": "Montrealo" }, "map_categories": { "continental": "Kontinenta", @@ -196,8 +197,9 @@ "join_lobby": "Aliĝi al la ludejo", "checking": "Kontrolado de la ludejo...", "not_found": "Ludejo ne trovita. Bonvolu kontroli la ID kaj reprovi.", - "error": "Eraro okazis. Bonvolu provi denove.", - "joined_waiting": "Sukcese aliĝis! Atendante la komencon de la ludo..." + "error": "Eraro okazis. Bonvolu reprovi aŭ kontaktu helpon.", + "joined_waiting": "Sukcese aliĝis! Atendante la komencon de la ludo...", + "version_mismatch": "Ĉi tiu ludo estis kreita kun malsama versio. Ne eblas aliĝi." }, "public_lobby": { "join": "Kunigi la baldaŭan ludon", @@ -227,6 +229,7 @@ "donate_gold": "Donacu oron", "infinite_troops": "Senfinaj trupoj", "donate_troops": "Donacu trupojn", + "compact_map": "Karteto", "enables_title": "Ebligi Agordojn", "player": "Ludanto", "players": "Ludantoj", @@ -314,6 +317,8 @@ "territory_patterns_desc": "Elektu ĉu montri teritoriajn ŝablonajn dezajnojn en la ludo", "performance_overlay_label": "Efikeco Supermetaĵo", "performance_overlay_desc": "Ŝalti la efikecon supermetaĵon. Kiam ebligita, la efikeco supermetaĵo estos montrata. Premu la Majuskligan klavon\n+ D dum la ludo por ŝalti.", + "performance_overlay_enabled": "Funkciada supermetaĵo ŝaltita", + "performance_overlay_disabled": "Funkciada supermetaĵo malŝaltita", "easter_writing_speed_label": "Rapidskriba multiganto", "easter_writing_speed_desc": "Alĝustigu kiom rapide vi ŝajnigas kodi (x1–x100)", "easter_bug_count_label": "Nombro da cimoj", @@ -494,7 +499,8 @@ "nation": "Nacio", "player": "Ludanto", "team": "Teamo", - "d_troops": "Defendante trupoj", + "alliance_timeout": "Alianco finiĝas en", + "troops": "Trupoj", "a_troops": "Atakante trupoj", "gold": "Oro", "ports": "Havenoj", @@ -585,7 +591,7 @@ "choose_spawn": "Elektu komencan lokon" }, "territory_patterns": { - "title": "Elekti Teritoriajn Ŝablonojn", + "title": "Elekti Teritoria Ŝablono", "purchase": "Aĉeti", "blocked": { "login": "Vi devas esti ensalutinta por aliri ĉi tiun ŝablonon.", @@ -644,5 +650,63 @@ "radial_menu": { "delete_unit_title": "Forigi trupunon", "delete_unit_description": "Alklaku por forigi la plej proksiman trupunon" + }, + "discord_user_header": { + "avatar_alt": "Profilbildo" + }, + "player_stats_table": { + "building_stats": "Konstruaĵaj statistikoj", + "ship_arrivals": "Ŝipoj alvenoj", + "nuke_stats": "Bomboj statistikoj", + "player_metrics": "Ludanta statistikoj", + "building": "Konstruaĵo", + "ship_type": "Ŝipa tipo", + "weapon": "Armilo", + "built": "Konstruita", + "destroyed": "Detruita", + "captured": "Ekkaptita", + "lost": "Perdita", + "hits": "Frapoj", + "launched": "Lanĉita", + "landed": "Surterigita", + "sent": "Sendita", + "arrived": "Alveninta", + "attack": "Atako", + "received": "Ricevita", + "cancelled": "Nuligita", + "count": "Nombro", + "gold": "Oro", + "workers": "Laboristoj", + "war": "Milito", + "trade": "Komerci", + "steal": "Ŝteli", + "unit": { + "city": "Urbo", + "port": "Haveno", + "defp": "Defenda Posteno", + "saml": "SAM-lanĉilo", + "silo": "Misila Silo", + "wshp": "Militŝipo", + "fact": "Fabriko", + "trade": "Komerca ŝipo", + "trans": "Transporta ŝipo", + "abomb": "Atombombo", + "hbomb": "Hidrogenbombo", + "mirv": "MIRV", + "mirvw": "MIRV-kapo" + } + }, + "game_list": { + "recent_games": "Lastaj ludoj", + "game_id": "Ludo ID", + "mode": "Reĝimo", + "mode_ffa": "Ĉiu por si", + "mode_team": "Teamo", + "view": "Vido", + "details": "Detaloj", + "started": "Komencita", + "map": "Karto", + "difficulty": "Malfacileco", + "type": "Tipo" } } diff --git a/resources/lang/fr.json b/resources/lang/fr.json index f74148320..fda7077c5 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -130,9 +130,8 @@ "disable_nations": "Désactiver les nations", "instant_build": "Construction instantanée", "infinite_gold": "Or infini", - "donate_gold": "Donner de l'or", "infinite_troops": "Troupes infinies", - "donate_troops": "Donner des troupes", + "compact_map": "Mini-carte", "disable_nukes": "Désactiver les armes nucléaires", "enables_title": "Activer les paramètres", "start": "Commencer la partie" @@ -177,8 +176,10 @@ "halkidiki": "Chalcidique", "straitofgibraltar": "Détroit de Gibraltar", "italia": "Italie", + "japan": "Japon", "yenisei": "Ienisseï", - "pluto": "Pluton" + "pluto": "Pluton", + "montreal": "Montréal" }, "map_categories": { "continental": "Continental", @@ -196,8 +197,9 @@ "join_lobby": "Rejoindre le salon", "checking": "Vérification du salon...", "not_found": "Salon introuvable. Veuillez vérifier l'ID et réessayer.", - "error": "Une erreur est survenue. Veuillez réessayer.", - "joined_waiting": "Rejoint avec succès ! En attente du début de la partie..." + "error": "Une erreur s'est produite. Veuillez réessayer ou contacter le support.", + "joined_waiting": "Rejoint avec succès ! En attente du début de la partie...", + "version_mismatch": "Cette partie a été créée avec une version différente. Impossible de rejoindre." }, "public_lobby": { "join": "Rejoindre la prochaine partie", @@ -227,6 +229,7 @@ "donate_gold": "Donner de l'or", "infinite_troops": "Troupes infinies", "donate_troops": "Donner des troupes", + "compact_map": "Mini-carte", "enables_title": "Activer les paramètres", "player": "Joueur", "players": "Joueurs", @@ -314,6 +317,8 @@ "territory_patterns_desc": "Choisissez si vous voulez afficher les motifs de territoire en jeu", "performance_overlay_label": "Surcouche de performances", "performance_overlay_desc": "Activer/désactiver la surcouche de performances. Lorsqu'elle est activée, la surcouche de performances sera affichée. Appuyez sur Maj+D pendant le jeu pour l\"activer.", + "performance_overlay_enabled": "Affichage des performances activé", + "performance_overlay_disabled": "Affichage des performances désactivé", "easter_writing_speed_label": "Multiplicateur de vitesse d'écriture", "easter_writing_speed_desc": "Ajuster la vitesse à laquelle vous prétendez coder (x1–x100)", "easter_bug_count_label": "Nombre de bugs", @@ -494,7 +499,8 @@ "nation": "Nation", "player": "Joueur", "team": "Équipe", - "d_troops": "Troupes en défense", + "alliance_timeout": "L'alliance se termine dans", + "troops": "Troupes", "a_troops": "Troupes en attaque", "gold": "Or", "ports": "Ports", @@ -585,7 +591,7 @@ "choose_spawn": "Choisissez un emplacement de départ" }, "territory_patterns": { - "title": "Sélectionnez un motif de territoire", + "title": "Sélectionner un motif de territoire", "purchase": "Acheter", "blocked": { "login": "Vous devez être connecté pour accéder à ce motif.", @@ -644,5 +650,63 @@ "radial_menu": { "delete_unit_title": "Supprimer une unité", "delete_unit_description": "Cliquez pour supprimer l'unité la plus proche" + }, + "discord_user_header": { + "avatar_alt": "Avatar" + }, + "player_stats_table": { + "building_stats": "Statistiques de construction", + "ship_arrivals": "Arrivées de navire", + "nuke_stats": "Statistiques de bombes", + "player_metrics": "Statistiques du joueur", + "building": "Batîment", + "ship_type": "Type de navire", + "weapon": "Arme", + "built": "Construit", + "destroyed": "Détruit", + "captured": "Capturé", + "lost": "Perdu", + "hits": "Coups", + "launched": "Lancés", + "landed": "Atterris", + "sent": "Envoyés", + "arrived": "Arrivés", + "attack": "Attaque", + "received": "Reçu", + "cancelled": "Annulé", + "count": "Compte", + "gold": "Or", + "workers": "Ouvriers", + "war": "Guerre", + "trade": "Commercer", + "steal": "Voler", + "unit": { + "city": "Ville", + "port": "Port", + "defp": "Poste de défense", + "saml": "Lanceur de SAM", + "silo": "Silo à missiles", + "wshp": "Navire de guerre", + "fact": "Usine", + "trade": "Bateau de commerce", + "trans": "Bateau de transport", + "abomb": "Bombe atomique", + "hbomb": "Bombe à hydrogène", + "mirv": "MIRV", + "mirvw": "Ogive de MIRV" + } + }, + "game_list": { + "recent_games": "Parties récentes", + "game_id": "ID de la partie", + "mode": "Mode", + "mode_ffa": "Chacun pour soi", + "mode_team": "Équipe", + "view": "Vue", + "details": "Détails", + "started": "Débuté", + "map": "Carte", + "difficulty": "Difficulté", + "type": "Type" } } diff --git a/resources/lang/hu.json b/resources/lang/hu.json index 95a9982a1..343380f01 100644 --- a/resources/lang/hu.json +++ b/resources/lang/hu.json @@ -130,9 +130,8 @@ "disable_nations": "Letiltott nemzetek", "instant_build": "Instant építés", "infinite_gold": "Végtelen arany", - "donate_gold": "Arany adományomzása", "infinite_troops": "Végtelen katonák", - "donate_troops": "Katonák adományozása", + "compact_map": "Mini térkép", "disable_nukes": "Nukleáris fegyverek letiltása", "enables_title": "Beállítások engedélyezése", "start": "Játék indítása" @@ -177,8 +176,10 @@ "halkidiki": "Halkidiki", "straitofgibraltar": "Gibraltári-szoros", "italia": "Olaszország", + "japan": "Japán", "yenisei": "Jenisej", - "pluto": "Plútó" + "pluto": "Plútó", + "montreal": "Montreal" }, "map_categories": { "continental": "Kontinentális", @@ -196,8 +197,9 @@ "join_lobby": "Csatlakozás lobbyhoz", "checking": "Lobby ellenőrzése...", "not_found": "Lobby nem található. Kérlek, ellenőrizd az azonosítót, és próbáld újra.", - "error": "Hiba történt. Kérlek, próbáld újra.", - "joined_waiting": "Sikeresen csatlakoztál! Várakozás a játék kezdésére..." + "error": "Hiba történt. \nKérjük próbáld újra, vagy lépj kapcsolatba velünk.", + "joined_waiting": "Sikeresen csatlakoztál! Várakozás a játék kezdésére...", + "version_mismatch": "Ez a játék egy másik verzióval lett létrehozva. Nem lehet csatlakozni." }, "public_lobby": { "join": "Csatlakozás a következő játékhoz", @@ -227,6 +229,7 @@ "donate_gold": "Arany adományozása", "infinite_troops": "Végtelen katonák", "donate_troops": "Katonák adományozása", + "compact_map": "Mini térkép", "enables_title": "Beállítások engedélyezése", "player": "Játékos", "players": "Játékosok", @@ -264,10 +267,10 @@ }, "unit_type": { "city": "Város", - "defense_post": "Védelmi poszt", + "defense_post": "Védelmi állás", "port": "Kikötő", "warship": "Hadihajó", - "missile_silo": "Rakéta siló", + "missile_silo": "Rakétasiló", "sam_launcher": "Rakétaelhárító rendszer", "atom_bomb": "Atombomba", "hydrogen_bomb": "Hidrogénbomba", @@ -314,6 +317,8 @@ "territory_patterns_desc": "Válaszd ki, hogy megjelenjenek-e a területmintázatok a játékban", "performance_overlay_label": "Teljesítmény kijelző", "performance_overlay_desc": "Kapcsoló a teljesítmény kijelzőhöz. Ha engedélyezve van, a teljesítmény kijelző megjelenik. A játék közben nyomd meg a Shift-D-t a váltáshoz.", + "performance_overlay_enabled": "Teljesítmény kijelző engedélyezve", + "performance_overlay_disabled": "Teljesítmény kijelző letiltva", "easter_writing_speed_label": "Írási sebesség szorzó", "easter_writing_speed_desc": "Állítsd be, milyen gyorsan szimulálod a kódolást (x1–x100)", "easter_bug_count_label": "Hiba számláló", @@ -494,7 +499,8 @@ "nation": "Nemzet", "player": "Játékos", "team": "Csapat", - "d_troops": "Védekező katonák", + "alliance_timeout": "Szövetség felbomlik", + "troops": "Katonák", "a_troops": "Támadó katonák", "gold": "Arany", "ports": "Kikötők", @@ -644,5 +650,63 @@ "radial_menu": { "delete_unit_title": "Egység törlése", "delete_unit_description": "Kattints a legközelebbi egység törléséhez" + }, + "discord_user_header": { + "avatar_alt": "Profilkép" + }, + "player_stats_table": { + "building_stats": "Épületstatisztikák", + "ship_arrivals": "Hajóérkezések", + "nuke_stats": "Nukleáris statisztikák", + "player_metrics": "Játékos mutatók", + "building": "Épület", + "ship_type": "Hajó típus", + "weapon": "Fegyver", + "built": "Épített", + "destroyed": "Megsemmisült", + "captured": "Elfoglalva", + "lost": "Elveszett", + "hits": "Találatok", + "launched": "Elindított", + "landed": "Célba ért", + "sent": "Elküldött", + "arrived": "Megérkezett", + "attack": "Támadás", + "received": "Fogadott", + "cancelled": "Megszakítva", + "count": "Mennyiség", + "gold": "Arany", + "workers": "Munkások", + "war": "Háború", + "trade": "Kereskedelem", + "steal": "Lopás", + "unit": { + "city": "Város", + "port": "Kikötő", + "defp": "Védelmi állás", + "saml": "Rakétaelhárító rendszer", + "silo": "Rakétasiló", + "wshp": "Hadihajó", + "fact": "Gyár", + "trade": "Kereskedelmi hajó", + "trans": "Szállítóhajó", + "abomb": "Atombomba", + "hbomb": "Hidrogénbomba", + "mirv": "MIRV", + "mirvw": "MIRV robbanófej" + } + }, + "game_list": { + "recent_games": "Legutóbbi Játékok", + "game_id": "Játék azonosító", + "mode": "Mód", + "mode_ffa": "Mindenki mindenki ellen", + "mode_team": "Csapat", + "view": "Nézet", + "details": "Részletek", + "started": "Elkezdődött", + "map": "Térkép", + "difficulty": "Nehézség", + "type": "Típus" } } diff --git a/resources/lang/ja.json b/resources/lang/ja.json index 39432afd1..087cad4f0 100644 --- a/resources/lang/ja.json +++ b/resources/lang/ja.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": "バグの個数", @@ -494,7 +499,8 @@ "nation": "国家", "player": "プレイヤー", "team": "チーム", - "d_troops": "防衛兵士数", + "alliance_timeout": "同盟終了まで", + "troops": "軍隊", "a_troops": "攻撃兵士数", "gold": "資金", "ports": "港", @@ -585,7 +591,7 @@ "choose_spawn": "スタート地点を選んで下さい" }, "territory_patterns": { - "title": "領土の模様を選択", + "title": "領土スキンを選択", "purchase": "購入", "blocked": { "login": "このページにアクセスするにはログインしてください。", @@ -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": "SAMランチャー", + "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": "タイプ" } } diff --git a/resources/lang/ru.json b/resources/lang/ru.json index a7ea8c19f..a71adfafc 100644 --- a/resources/lang/ru.json +++ b/resources/lang/ru.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": "Количество багов", @@ -494,7 +499,8 @@ "nation": "Нация", "player": "Игрок", "team": "Команда", - "d_troops": "Войска защиты", + "alliance_timeout": "Конец союза через", + "troops": "Войска", "a_troops": "Войска атаки", "gold": "Золото", "ports": "Порты", @@ -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": "РГЧ ИН", + "mirvw": "Боеголовка РГЧ ИН" + } + }, + "game_list": { + "recent_games": "Недавние игры", + "game_id": "ID игры", + "mode": "Режим", + "mode_ffa": "Каждый против каждого", + "mode_team": "Команда", + "view": "Осмотреть", + "details": "Подробности", + "started": "Начато", + "map": "Карта", + "difficulty": "Сложность", + "type": "Тип" } } diff --git a/resources/lang/uk.json b/resources/lang/uk.json index 8c697f880..6e25d2ee8 100644 --- a/resources/lang/uk.json +++ b/resources/lang/uk.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": "Приєднатися до наступної гри", @@ -211,7 +213,7 @@ "enter_username": "Введіть своє ім'я гравця", "not_string": "Ім'я гравця має бути рядком.", "too_short": "Ім'я гравця повинно містити щонайменше {min} символів.", - "too_long": "Ім'я гравця не повинно перевищувати {max} символів.", + "too_long": "Довжина ім'я гравця не повинна перевищувати {max} символів.", "invalid_chars": "Ім'я гравця може містити лише латинські літери, цифри, пробіли, знаки підкреслення та [квадратні дужки]." }, "host_modal": { @@ -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": "Кількість багів", @@ -494,7 +499,8 @@ "nation": "Нація", "player": "Гравець", "team": "Команда", - "d_troops": "Оборонні війська", + "alliance_timeout": "Кінець союзу через", + "troops": "Війська", "a_troops": "Наступальні війська", "gold": "Золото", "ports": "Порти", @@ -511,14 +517,14 @@ "retreating": "відступає", "boat": "Човен", "alliance_request_status": "{name} {status} запрошення до союзу", - "alliance_accepted": "прийняв", - "alliance_rejected": "відхилив", + "alliance_accepted": "приймає", + "alliance_rejected": "відхиляє", "duration_second": "1 сек", "betrayal_description": "Ви розірвали союз із {name}, ставши ЗРАДНИКОМ (оборону знижено на {malusPercent}% протягом {durationText})", "duration_seconds_plural": "{seconds} сек", - "betrayed_you": "{name} розірвав союз із вами", - "about_to_expire": "Ваш союз із {name} скоро закінчиться!", - "alliance_expired": "Ваш союз із {name} закінчився", + "betrayed_you": "{name} розриває союз із вами", + "about_to_expire": "Союз із {name} скоро закінчиться!", + "alliance_expired": "Союз із {name} закінчився", "attack_request": "{name} просить вас атакувати {target}", "sent_emoji": "Надіслано {name}: {emoji}", "renew_alliance": "Запит на поновлення", @@ -526,7 +532,7 @@ "focus": "Оглянути", "accept_alliance": "Прийняти", "reject_alliance": "Відхилити", - "alliance_renewed": "Ваш союз із {name} було поновлено", + "alliance_renewed": "Союз із {name} було поновлено", "ignore": "Ігнорувати", "unit_voluntarily_deleted": "Об'єкт добровільно видалено" }, @@ -585,7 +591,7 @@ "choose_spawn": "Виберіть початкове розташування" }, "territory_patterns": { - "title": "Виберіть візерунок території", + "title": "Вибір візерунка території", "purchase": "Придбати", "blocked": { "login": "Ви повинні ввійти, щоб отримати доступ до цього візерунку.", @@ -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": "РГЧ ІН", + "mirvw": "Боєголовка РГЧ ІН" + } + }, + "game_list": { + "recent_games": "Нещодавні ігри", + "game_id": "ID гри", + "mode": "Режим", + "mode_ffa": "Всі проти всіх", + "mode_team": "Команда", + "view": "Оглянути", + "details": "Подробиці", + "started": "Почато", + "map": "Мапа", + "difficulty": "Складність", + "type": "Тип" } } From 020b0de8750fbea69e9f92c689d12916c6f71182 Mon Sep 17 00:00:00 2001 From: Vivacious Box Date: Thu, 9 Oct 2025 01:40:39 +0200 Subject: [PATCH 08/20] Fix forgotten mirv cursor (#2156) ## Description: Forgot to show cross cursor for mirv ## 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: Mr.Box --- src/client/graphics/layers/StructureDrawingUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/graphics/layers/StructureDrawingUtils.ts b/src/client/graphics/layers/StructureDrawingUtils.ts index d2c3cbb44..a3693dc74 100644 --- a/src/client/graphics/layers/StructureDrawingUtils.ts +++ b/src/client/graphics/layers/StructureDrawingUtils.ts @@ -18,9 +18,10 @@ export const STRUCTURE_SHAPES: Partial> = { [UnitType.DefensePost]: "octagon", [UnitType.SAMLauncher]: "square", [UnitType.MissileSilo]: "triangle", + [UnitType.Warship]: "cross", [UnitType.AtomBomb]: "cross", [UnitType.HydrogenBomb]: "cross", - [UnitType.Warship]: "cross", + [UnitType.MIRV]: "cross", }; export const LEVEL_SCALE_FACTOR = 3; export const ICON_SCALE_FACTOR_ZOOMED_IN = 3.5; From e895e53a1e69b694586971c665e9bc1688bd8521 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 8 Oct 2025 16:40:53 -0700 Subject: [PATCH 09/20] fix spawn highlighting bug & improve highlight ring (#2157) ## Description: The PR that added a highlight ring, broke highlights for other player spawns. This PR fixes that, and makes the highlight ring more visible. ## 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/graphics/layers/TerritoryLayer.ts | 59 +++++++++++--------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index ce84ad98b..59670db17 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -149,6 +149,11 @@ export class TerritoryLayer implements Layer { if (!this.game.inSpawnPhase()) { return; } + + this.spawnHighlight(); + } + + private spawnHighlight() { if (this.game.ticks() % 5 === 0) { return; } @@ -159,11 +164,18 @@ export class TerritoryLayer implements Layer { this.game.width(), this.game.height(), ); + + this.drawFocusedPlayerHighlight(); + const humans = this.game .playerViews() .filter((p) => p.type() === PlayerType.Human); + const focusedPlayer = this.game.focusedPlayer(); for (const human of humans) { + if (human === focusedPlayer) { + continue; + } const center = human.nameLocation(); if (!center) { continue; @@ -190,37 +202,34 @@ export class TerritoryLayer implements Layer { } } } + } + + private drawFocusedPlayerHighlight() { + const focusedPlayer = this.game.focusedPlayer(); + + if (!focusedPlayer) { + return; + } + const center = focusedPlayer.nameLocation(); + if (!center) { + return; + } // Breathing border animation - this.borderAnimTime += 1; - const minPadding = 3; - const maxPadding = 8; + this.borderAnimTime += 3; + const minPadding = 6; + const maxPadding = 12; // Range: [minPadding..maxPadding] const breathingPadding = minPadding + (maxPadding - minPadding) * (0.5 + 0.5 * Math.sin(this.borderAnimTime * 0.3)); - if (focusedPlayer) { - // Clear previous animated border - if (this.highlightContext) { - this.highlightContext.clearRect( - 0, - 0, - this.game.width(), - this.game.height(), - ); - } - - const center = focusedPlayer.nameLocation(); - if (center) { - this.drawBreathingRing( - center.x, - center.y, - breathingPadding, - this.theme.spawnHighlightColor(), - ); - } - } + this.drawBreathingRing( + center.x, + center.y, + breathingPadding, + this.theme.spawnHighlightColor(), + ); } init() { @@ -560,7 +569,7 @@ export class TerritoryLayer implements Layer { ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2); ctx.strokeStyle = color.toRgbString(); - ctx.lineWidth = 2; + ctx.lineWidth = 4; ctx.stroke(); } } From 187ef1f2dd5e702bc23fac3bd2795d5e40288797 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 8 Oct 2025 20:31:56 -0400 Subject: [PATCH 10/20] feat(PlayerExecution): downgrade defense posts on capture (#1957) ## Description: Closes https://github.com/openfrontio/OpenFrontIO/issues/1619. On capture, defense posts will be downgraded. On the live version this means defense posts will be destroyed, as defense posts can only be level 1. Misc. changes: - added `decreaserLevel` helper - cleaned up if/else in tick unit loop for clarity to avoid yet another nested layer Continuation of the stale PR, https://github.com/openfrontio/OpenFrontIO/pull/1622. ## 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: Discord username: `seekerreturns` --- src/core/execution/PlayerExecution.ts | 9 +- src/core/game/Game.ts | 1 + src/core/game/UnitImpl.ts | 12 +++ tests/core/executions/PlayerExecution.test.ts | 95 +++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 tests/core/executions/PlayerExecution.test.ts diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index bb348cdc5..66f5c6a57 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -30,9 +30,14 @@ export class PlayerExecution implements Execution { this.player.units().forEach((u) => { const tileOwner = this.mg!.owner(u.tile()); if (u.info().territoryBound) { - if (tileOwner.isPlayer()) { + if (tileOwner?.isPlayer()) { if (tileOwner !== this.player) { - this.mg!.player(tileOwner.id()).captureUnit(u); + if (u.type() === UnitType.DefensePost) { + this.mg!.player(tileOwner.id()).captureUnit(u); + u.decreaseLevel(this.mg!.player(tileOwner.id())); + } else { + this.mg!.player(tileOwner.id()).captureUnit(u); + } } } else { u.delete(); diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 03c828fcc..db48e0a2f 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -494,6 +494,7 @@ export interface Unit { // Upgradable Structures level(): number; increaseLevel(): void; + decreaseLevel(destroyer?: Player): void; // Warships setPatrolTile(tile: TileRef): void; diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 899e56d79..fc1eab97a 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -397,6 +397,18 @@ export class UnitImpl implements Unit { this.mg.addUpdate(this.toUpdate()); } + decreaseLevel(destroyer?: Player): void { + this._level--; + if ([UnitType.MissileSilo, UnitType.SAMLauncher].includes(this.type())) { + this._missileTimerQueue.pop(); + } + if (this._level <= 0) { + this.delete(true, destroyer); + return; + } + this.mg.addUpdate(this.toUpdate()); + } + trainType(): TrainType | undefined { return this._trainType; } diff --git a/tests/core/executions/PlayerExecution.test.ts b/tests/core/executions/PlayerExecution.test.ts new file mode 100644 index 000000000..bbb74b32b --- /dev/null +++ b/tests/core/executions/PlayerExecution.test.ts @@ -0,0 +1,95 @@ +import { PlayerExecution } from "../../../src/core/execution/PlayerExecution"; +import { + Game, + Player, + PlayerInfo, + PlayerType, + UnitType, +} from "../../../src/core/game/Game"; +import { setup } from "../../util/Setup"; +import { executeTicks } from "../../util/utils"; + +let game: Game; +let player: Player; +let otherPlayer: Player; + +describe("PlayerExecution", () => { + beforeEach(async () => { + game = await setup( + "big_plains", + { + infiniteGold: true, + instantBuild: true, + }, + [ + new PlayerInfo("player", PlayerType.Human, "client_id1", "player_id"), + new PlayerInfo("other", PlayerType.Human, "client_id2", "other_id"), + ], + ); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + player = game.player("player_id"); + otherPlayer = game.player("other_id"); + + game.addExecution(new PlayerExecution(player)); + game.addExecution(new PlayerExecution(otherPlayer)); + }); + + test("DefensePost lv. 1 is destroyed when tile owner changes", () => { + const tile = game.ref(50, 50); + player.conquer(tile); + const defensePost = player.buildUnit(UnitType.DefensePost, tile, {}); + + game.executeNextTick(); + expect(game.unitCount(UnitType.DefensePost)).toBe(1); + expect(defensePost.level()).toBe(1); + + otherPlayer.conquer(tile); + executeTicks(game, 2); + + expect(game.unitCount(UnitType.DefensePost)).toBe(0); + }); + + test("DefensePost lv. 2+ is downgraded when tile owner changes", () => { + const tile = game.ref(50, 50); + player.conquer(tile); + const defensePost = player.buildUnit(UnitType.DefensePost, tile, {}); + defensePost.increaseLevel(); + + expect(defensePost.level()).toBe(2); + expect(game.unitCount(UnitType.DefensePost)).toBe(2); // unitCount sums levels + expect(player.units(UnitType.DefensePost)).toHaveLength(1); + expect(defensePost.isActive()).toBe(true); + + otherPlayer.conquer(tile); + executeTicks(game, 2); + + expect(defensePost.level()).toBe(1); + expect(game.unitCount(UnitType.DefensePost)).toBe(1); + expect(otherPlayer.units(UnitType.DefensePost)).toHaveLength(1); + expect(defensePost.owner()).toBe(otherPlayer); + expect(defensePost.isActive()).toBe(true); + }); + + test("Non-DefensePost structures are transferred (not downgraded) when tile owner changes", () => { + const tile = game.ref(50, 50); + player.conquer(tile); + const city = player.buildUnit(UnitType.City, tile, {}); + + expect(game.unitCount(UnitType.City)).toBe(1); + expect(city.level()).toBe(1); + expect(city.owner()).toBe(player); + expect(city.isActive()).toBe(true); + + otherPlayer.conquer(tile); + executeTicks(game, 2); + + expect(game.unitCount(UnitType.City)).toBe(1); + expect(city.level()).toBe(1); + expect(city.owner()).toBe(otherPlayer); + expect(city.isActive()).toBe(true); + }); +}); From 3680d9cc1663a22f0e174d2c2de806c0ee78b923 Mon Sep 17 00:00:00 2001 From: Britton Fischer <55821122+nottirb@users.noreply.github.com> Date: Wed, 8 Oct 2025 18:28:26 -0700 Subject: [PATCH 11/20] fix: allies cannot annex your clusters (#2158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Fixes #1685 Continuation from #1924, which was auto-closed after the upstream repo force-pushed main and I synced my fork. This change ensures that allies are excluded from the `getMode()` call made by `getCapturingPlayer()` inside `removeCluster()`. - Previously, friendly neighbors were treated as candidates for capturing, leading to incorrect annexations in certain edge cases. - Added a small efficiency improvement by filtering out non-player and friendly neighbors up front to reduce total computations down-the-line. - Important: we can’t simply check if the `getMode(neighborsIDs)` result is a friendly. Doing so would cause the territory to go to nobody until the user is attacked. I believe the expected behavior is the largest neighboring enemy should take it automatically. Here's an example of the new behavior in an extreme edge case: Screenshot 2025-08-24 at 4 56 46 PM ## 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: nottirb --- src/core/Util.ts | 21 ------------ src/core/execution/PlayerExecution.ts | 47 +++++++++++++++------------ 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/src/core/Util.ts b/src/core/Util.ts index d74d27b11..fc5e6c58d 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -114,27 +114,6 @@ export function inscribed( ); } -export function getMode(list: Set): number { - // Count occurrences - const counts = new Map(); - for (const item of list) { - counts.set(item, (counts.get(item) ?? 0) + 1); - } - - // Find the item with the highest count - let mode = 0; - let maxCount = 0; - - for (const [item, count] of counts) { - if (count > maxCount) { - maxCount = count; - mode = item; - } - } - - return mode; -} - export function sanitize(name: string): string { return Array.from(name) .join("") diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index 66f5c6a57..32935eabb 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -2,7 +2,7 @@ import { Config } from "../configuration/Config"; import { Execution, Game, Player, UnitType } from "../game/Game"; import { GameImpl } from "../game/GameImpl"; import { GameMap, TileRef } from "../game/GameMap"; -import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util"; +import { calculateBoundingBox, inscribed, simpleHash } from "../Util"; export class PlayerExecution implements Execution { private readonly ticksPerClusterCalc = 20; @@ -214,22 +214,37 @@ export class PlayerExecution implements Execution { } private getCapturingPlayer(cluster: Set): Player | null { - const neighborsIDs = new Set(); + // Collect unique neighbor IDs (excluding self) as candidates + const candidatesIDs = new Set(); + const selfID = this.player.smallID(); + for (const t of cluster) { for (const neighbor of this.mg.neighbors(t)) { - if (this.mg.ownerID(neighbor) !== this.player.smallID()) { - neighborsIDs.add(this.mg.ownerID(neighbor)); + if (this.mg.ownerID(neighbor) !== selfID) { + candidatesIDs.add(this.mg.ownerID(neighbor)); } } } - let largestNeighborAttack: Player | null = null; - let largestTroopCount: number = 0; - for (const id of neighborsIDs) { + // Filter out friendly and non-player candidates + const neighbors = new Set(); + for (const id of candidatesIDs) { const neighbor = this.mg.playerBySmallID(id); - if (!neighbor.isPlayer() || this.player.isFriendly(neighbor)) { + if (!neighbor.isPlayer() || neighbor.isFriendly(this.player)) { continue; } + neighbors.add(neighbor); + } + + // If there are no enemies, return null + if (neighbors.size === 0) { + return null; + } + + // Get the largest attack from the neighbors + let largestNeighborAttack: Player | null = null; + let largestTroopCount = 0; + for (const neighbor of neighbors) { for (const attack of neighbor.outgoingAttacks()) { if (attack.target() === this.player) { if (attack.troops() > largestTroopCount) { @@ -239,20 +254,10 @@ export class PlayerExecution implements Execution { } } } - if (largestNeighborAttack !== null) { - return largestNeighborAttack; - } - // fall back to getting mode if no attacks - const mode = getMode(neighborsIDs); - if (!this.mg.playerBySmallID(mode).isPlayer()) { - return null; - } - const capturing = this.mg.playerBySmallID(mode); - if (!capturing.isPlayer()) { - return null; - } - return capturing; + // Return the largest neighbor attack + // If there is no largest neighbor attack, this will return null + return largestNeighborAttack; } private calculateClusters(): Set[] { From d070c5810cefa4cc66b4a701d95e3b4673201b70 Mon Sep 17 00:00:00 2001 From: Loymdayddaud <145969603+TheGiraffe3@users.noreply.github.com> Date: Thu, 9 Oct 2025 22:56:51 +0300 Subject: [PATCH 12/20] Increase the frequency of teams games (#1809) ## Description: Updates the frequency of teams games to fix #1808. ## 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: loymdayddaud Co-authored-by: TheGiraffe3 --- src/server/MapPlaylist.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index f264ad908..3ce5dfd65 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -126,16 +126,12 @@ export class MapPlaylist { const rand = new PseudoRandom(Date.now()); - const ffa1: GameMapType[] = rand.shuffleArray([...maps]); - const ffa2: GameMapType[] = rand.shuffleArray([...maps]); + const ffa: GameMapType[] = rand.shuffleArray([...maps]); const team: GameMapType[] = rand.shuffleArray([...maps]); this.mapsPlaylist = []; for (let i = 0; i < maps.length; i++) { - if (!this.addNextMap(this.mapsPlaylist, ffa1, GameMode.FFA)) { - return false; - } - if (!this.addNextMap(this.mapsPlaylist, ffa2, GameMode.FFA)) { + if (!this.addNextMap(this.mapsPlaylist, ffa, GameMode.FFA)) { return false; } if (!this.addNextMap(this.mapsPlaylist, team, GameMode.Team)) { From 0076996dcefee44e3d252da04e0ec0468446c63f Mon Sep 17 00:00:00 2001 From: Abdallah Bahrawi <140177728+abdallahbahrawi1@users.noreply.github.com> Date: Thu, 9 Oct 2025 23:19:05 +0300 Subject: [PATCH 13/20] Implement send resources modal (#2146) ## Description: Fixes https://github.com/openfrontio/OpenFrontIO/issues/2015 Implemented a new interactive modal component for sending troops/gold between players, replacing the previous automatic troop donation system. Screenshots s1 s2 s3 ## 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: abodcraft1 --------- Co-authored-by: evanpelle --- resources/lang/en.json | 35 +- src/client/graphics/layers/PlayerPanel.ts | 123 ++-- .../graphics/layers/SendResourceModal.ts | 588 ++++++++++++++++++ src/core/game/PlayerImpl.ts | 12 +- 4 files changed, 704 insertions(+), 54 deletions(-) create mode 100644 src/client/graphics/layers/SendResourceModal.ts diff --git a/resources/lang/en.json b/resources/lang/en.json index 860f3f7ae..27fe3366f 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -6,7 +6,18 @@ "lang_code": "en" }, "common": { - "close": "Close" + "close": "Close", + "available": "Available", + "preset_max": "Max", + "summary_send": "Send", + "summary_keep": "Keep", + "cancel": "Cancel", + "send": "Send", + "cap_label": "Cap", + "cap_tooltip": "Recipient’s remaining capacity", + "target_dead": "Target eliminated", + "target_dead_note": "You can't send resources to an eliminated player.", + "none": "None" }, "main": { "title": "OpenFront (ALPHA)", @@ -590,8 +601,6 @@ "troops": "Troops", "betrayals": "Betrayals", "traitor": "Traitor", - "stable": "Stable", - "trust": "Trust", "trading": "Trading", "active": "Active", "stopped": "Stopped", @@ -600,9 +609,6 @@ "nuke": "Nukes sent by them to you", "start_trade": "Start Trading", "stop_trade": "Stop Trading", - "yes": "Yes", - "no": "No", - "none": "None", "alliances": "Alliances", "flag": "Flag", "chat": "Chat", @@ -615,6 +621,23 @@ "send_gold": "Send Gold", "emotes": "Emojis" }, + "send_troops_modal": { + "title_with_name": "Send Troops to {name}", + "available_tooltip": "Your current available troops", + "min_keep": "Min keep", + "min_keep_pct": "(30%)", + "slider_tooltip": "{{percent}}% • {{amount}}", + "toggle_attack_bar_mode": "Use attack bar to send troops", + "warning_attackbar": "Once enabled, you can't open this modal directly. You'll only send troops via the attack bar.", + "aria_slider": "Troops slider", + "capacity_note": "Receiver can accept only {{amount}} right now." + }, + "send_gold_modal": { + "title_with_name": "Send Gold to {name}", + "available_tooltip": "Your current available gold", + "aria_slider": "Amount slider", + "slider_tooltip": "{{percent}}% • {{amount}}" + }, "replay_panel": { "replay_speed": "Replay speed", "game_speed": "Game speed", diff --git a/src/client/graphics/layers/PlayerPanel.ts b/src/client/graphics/layers/PlayerPanel.ts index d3c03bc48..3af01a0ec 100644 --- a/src/client/graphics/layers/PlayerPanel.ts +++ b/src/client/graphics/layers/PlayerPanel.ts @@ -28,8 +28,6 @@ import { CloseViewEvent, MouseUpEvent } from "../../InputHandler"; import { SendAllianceRequestIntentEvent, SendBreakAllianceIntentEvent, - SendDonateGoldIntentEvent, - SendDonateTroopsIntentEvent, SendEmbargoIntentEvent, SendEmojiIntentEvent, SendTargetPlayerIntentEvent, @@ -44,6 +42,7 @@ import { UIState } from "../UIState"; import { ChatModal } from "./ChatModal"; import { EmojiTable } from "./EmojiTable"; import { Layer } from "./Layer"; +import "./SendResourceModal"; @customElement("player-panel") export class PlayerPanel extends LitElement implements Layer { @@ -51,21 +50,17 @@ export class PlayerPanel extends LitElement implements Layer { public eventBus: EventBus; public emojiTable: EmojiTable; public uiState: UIState; + private actions: PlayerActions | null = null; private tile: TileRef | null = null; private _profileForPlayerId: number | null = null; - @state() - public isVisible: boolean = false; - - @state() - private allianceExpiryText: string | null = null; - - @state() - private allianceExpirySeconds: number | null = null; - - @state() - private otherProfile: PlayerProfile | null = null; + @state() private sendTarget: PlayerView | null = null; + @state() private sendMode: "troops" | "gold" | "none" = "none"; + @state() public isVisible: boolean = false; + @state() private allianceExpiryText: string | null = null; + @state() private allianceExpirySeconds: number | null = null; + @state() private otherProfile: PlayerProfile | null = null; private ctModal: ChatModal; @@ -138,6 +133,8 @@ export class PlayerPanel extends LitElement implements Layer { public hide() { this.isVisible = false; + this.sendMode = "none"; + this.sendTarget = null; this.requestUpdate(); } @@ -166,19 +163,23 @@ export class PlayerPanel extends LitElement implements Layer { this.hide(); } + private openSendTroops(target: PlayerView) { + this.sendTarget = target; + this.sendMode = "troops"; + } + + private openSendGold(target: PlayerView) { + this.sendTarget = target; + this.sendMode = "gold"; + } + private handleDonateTroopClick( e: Event, myPlayer: PlayerView, other: PlayerView, ) { e.stopPropagation(); - this.eventBus.emit( - new SendDonateTroopsIntentEvent( - other, - myPlayer.troops() * this.uiState.attackRatio, - ), - ); - this.hide(); + this.openSendTroops(other); } private handleDonateGoldClick( @@ -187,10 +188,20 @@ export class PlayerPanel extends LitElement implements Layer { other: PlayerView, ) { e.stopPropagation(); - this.eventBus.emit(new SendDonateGoldIntentEvent(other, null)); - this.hide(); + this.openSendGold(other); } + private closeSend = () => { + this.sendTarget = null; + }; + + private confirmSend = ( + e: CustomEvent<{ amount: number; closePanel?: boolean }>, + ) => { + this.closeSend(); + if (e.detail?.closePanel) this.hide(); + }; + private handleEmbargoClick( e: Event, myPlayer: PlayerView, @@ -312,10 +323,11 @@ export class PlayerPanel extends LitElement implements Layer { } private getExpiryColorClass(seconds: number | null): string { - if (seconds === null) return "text-white"; - if (seconds <= 30) return "text-red-400"; - if (seconds <= 60) return "text-yellow-400"; - return "text-emerald-400"; + if (seconds === null) return "text-white"; // Default color + + if (seconds <= 30) return "text-red-400"; // Last 30 seconds: Red + if (seconds <= 60) return "text-yellow-400"; // Last 60 seconds: Yellow + return "text-emerald-400"; // More than 60 seconds: Green } private getTraitorRemainingSeconds(player: PlayerView): number | null { @@ -433,31 +445,27 @@ export class PlayerPanel extends LitElement implements Layer { return html`
💰 - + ${renderNumber(other.gold() || 0)} - ${translateText("player_panel.gold")} + ${translateText("player_panel.gold")}
🛡️ - + ${renderTroops(other.troops() || 0)} - ${translateText("player_panel.troops")}
@@ -554,7 +562,7 @@ export class PlayerPanel extends LitElement implements Layer { }) : html`
- ${translateText("player_panel.none")} + ${translateText("common.none")}
`}
@@ -567,7 +575,7 @@ export class PlayerPanel extends LitElement implements Layer { if (this.allianceExpiryText === null) return html``; return html`
-
+
${translateText("player_panel.alliance_time_remaining")}
@@ -713,6 +721,8 @@ export class PlayerPanel extends LitElement implements Layer { return html``; } const other = owner as PlayerView; + const myGoldNum = my.gold(); + const myTroopsNum = Number(my.troops()); return html` + `; + } + + render() { + if (!this.open) return html``; + + const percent = this.percentOfBasis(this.sendAmount); + const allowed = this.limitAmount(this.sendAmount); + + return html` +
+
this.closeModal()} + >
+ + +
+ `; + } +} diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index cb395cc1a..d2287d886 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -576,7 +576,11 @@ export class PlayerImpl implements Player { } canDonateGold(recipient: Player): boolean { - if (!this.isFriendly(recipient)) { + if ( + !this.isAlive() || + !recipient.isAlive() || + !this.isFriendly(recipient) + ) { return false; } if ( @@ -599,7 +603,11 @@ export class PlayerImpl implements Player { } canDonateTroops(recipient: Player): boolean { - if (!this.isFriendly(recipient)) { + if ( + !this.isAlive() || + !recipient.isAlive() || + !this.isFriendly(recipient) + ) { return false; } if ( From fc38fed97246230e3c475a293f31d63ca2712c3c Mon Sep 17 00:00:00 2001 From: DevelopingTom Date: Thu, 9 Oct 2025 23:44:17 +0200 Subject: [PATCH 14/20] Fix target FX on retreating units (#2166) ## Description: Retreating naval invasions spams the target FX each frames. Changes: filter out retreating invasions. ## 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 --- src/client/graphics/layers/FxLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/graphics/layers/FxLayer.ts b/src/client/graphics/layers/FxLayer.ts index dbd433347..027b8dd6c 100644 --- a/src/client/graphics/layers/FxLayer.ts +++ b/src/client/graphics/layers/FxLayer.ts @@ -122,7 +122,7 @@ export class FxLayer implements Layer { const my = this.game.myPlayer(); if (!my) return; if (unit.owner() !== my) return; - if (!unit.isActive()) return; + if (!unit.isActive() || unit.retreating()) return; if (this.boatTargetFxByUnitId.has(unit.id())) return; const t = unit.targetTile(); if (t !== undefined) { From a8002a038ceae80e2437c0d1b75e510a3ee4240e Mon Sep 17 00:00:00 2001 From: Vivacious Box Date: Fri, 10 Oct 2025 00:31:22 +0200 Subject: [PATCH 15/20] Transparent railroads (#2153) ## Description: Make the railroads fade before disappearing ## 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: Mr. Box --- src/client/graphics/layers/RailroadLayer.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/client/graphics/layers/RailroadLayer.ts b/src/client/graphics/layers/RailroadLayer.ts index fd5782738..ee44db960 100644 --- a/src/client/graphics/layers/RailroadLayer.ts +++ b/src/client/graphics/layers/RailroadLayer.ts @@ -96,11 +96,15 @@ export class RailroadLayer implements Layer { renderLayer(context: CanvasRenderingContext2D) { this.updateRailColors(); - if (this.transformHandler.scale <= 2) { - // When zoomed out, don't show the railroads - // to prevent map clutter. + const scale = this.transformHandler.scale; + if (scale <= 1) { return; } + const rawAlpha = (scale - 1) / (2 - 1); // maps 1->0, 2->1 + const alpha = Math.max(0, Math.min(1, rawAlpha)); + + context.save(); + context.globalAlpha = alpha; context.drawImage( this.canvas, -this.game.width() / 2, @@ -108,14 +112,11 @@ export class RailroadLayer implements Layer { this.game.width(), this.game.height(), ); + context.restore(); } private handleRailroadRendering(railUpdate: RailroadUpdate) { for (const railRoad of railUpdate.railTiles) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const x = this.game.x(railRoad.tile); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const y = this.game.y(railRoad.tile); if (railUpdate.isActive) { this.paintRailroad(railRoad); } else { From cc49dc986ed3951cf3fc09415bc4b2c03140cbb2 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Thu, 9 Oct 2025 18:12:10 -0700 Subject: [PATCH 16/20] reduce coverage threshold --- jest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.ts b/jest.config.ts index 52d147124..09a0b8677 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -18,7 +18,7 @@ export default { global: { statements: 21.5, branches: 16.5, - lines: 22.0, + lines: 21.0, functions: 20.5, }, }, From 649e4d2ed7d60b8f004a82e52974c604b65a6ba5 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 9 Oct 2025 22:21:05 -0400 Subject: [PATCH 17/20] fix(PlayerExecution): remove alarming DefensePost destruction messages on capture (#2163) ## Description: Patches issue flagged from https://github.com/openfrontio/OpenFrontIO/pull/1957#issuecomment-3386398998. Right now for every single defense post capture, attackers receive two messages: - "Your Defense Post was destroyed" and "Captured Defense Post from ..." By downgrading before captures, behavior will now be: - defender receives "Your Defense Post was destroyed" - attacker receives no message unless capturing a lv2+ defense post (downgraded to lv 1), in which case they receive "Captured Defense Post from ..." ## 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: `seekerreturns` --------- Co-authored-by: evanpelle --- src/core/execution/PlayerExecution.ts | 39 ++++++++++++++++----------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index 32935eabb..fed23323d 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -27,23 +27,30 @@ export class PlayerExecution implements Execution { tick(ticks: number) { this.player.decayRelations(); - this.player.units().forEach((u) => { - const tileOwner = this.mg!.owner(u.tile()); - if (u.info().territoryBound) { - if (tileOwner?.isPlayer()) { - if (tileOwner !== this.player) { - if (u.type() === UnitType.DefensePost) { - this.mg!.player(tileOwner.id()).captureUnit(u); - u.decreaseLevel(this.mg!.player(tileOwner.id())); - } else { - this.mg!.player(tileOwner.id()).captureUnit(u); - } - } - } else { - u.delete(); - } + for (const u of this.player.units()) { + if (!u.info().territoryBound) { + continue; } - }); + + const owner = this.mg!.owner(u.tile()); + if (!owner?.isPlayer()) { + u.delete(); + continue; + } + if (owner === this.player) { + continue; + } + + const captor = this.mg!.player(owner.id()); + if (u.type() === UnitType.DefensePost) { + u.decreaseLevel(captor); + if (u.isActive()) { + captor.captureUnit(u); + } + } else { + captor.captureUnit(u); + } + } if (!this.player.isAlive()) { // Player has no tiles, delete any remaining units and gold From 4f73548a4e953c71d531ac0dd8fcc3ca27388840 Mon Sep 17 00:00:00 2001 From: DevelopingTom Date: Fri, 10 Oct 2025 04:26:53 +0200 Subject: [PATCH 18/20] Use larger responsive classes (#2167) ## Description: Issue: With low resolution screens, or simply by resizing the window, the build menu can be drawn above the event logs: image Changes: - use larger responsive Tailwind classes: https://github.com/user-attachments/assets/e583b70e-235a-4f5b-b10d-d032ac621984 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom Co-authored-by: evanpelle --- src/client/graphics/layers/UnitDisplay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/graphics/layers/UnitDisplay.ts b/src/client/graphics/layers/UnitDisplay.ts index 7d383036a..d2ae6a93f 100644 --- a/src/client/graphics/layers/UnitDisplay.ts +++ b/src/client/graphics/layers/UnitDisplay.ts @@ -127,7 +127,7 @@ export class UnitDisplay extends LitElement implements Layer { return html`