Merge branch 'main' into local-attack

This commit is contained in:
Aotumuri
2026-02-08 07:37:59 +09:00
committed by GitHub
19 changed files with 334 additions and 41 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 983 KiB

@@ -0,0 +1,35 @@
{
"name": "Yenisei",
"nations": [
{
"coordinates": [1050, 535],
"flag": "ru",
"name": "Baikalovsk"
},
{
"coordinates": [1120, 1315],
"flag": "ru",
"name": "Mungui"
},
{
"coordinates": [345, 1190],
"flag": "ru",
"name": "Polykarpovsk"
},
{
"coordinates": [335, 800],
"flag": "ru",
"name": "Central Island"
},
{
"coordinates": [75, 390],
"flag": "ru",
"name": "West Coast"
},
{
"coordinates": [420, 440],
"flag": "ru",
"name": "Northern Island"
}
]
}
+1
View File
@@ -67,6 +67,7 @@ var maps = []struct {
{Name: "didier"},
{Name: "didierfrance"},
{Name: "amazonriver"},
{Name: "yenisei"},
{Name: "big_plains", IsTest: true},
{Name: "half_land_half_ocean", IsTest: true},
{Name: "ocean_and_land", IsTest: true},
+4 -1
View File
@@ -187,7 +187,9 @@
"icon_request": "Envelope - Alliance request. This player has sent you an alliance request.",
"info_enemy_panel": "Enemy info panel",
"exit_confirmation": "Are you sure you want to exit the game?",
"bomb_direction": "Atom / Hydrogen bomb arc direction"
"bomb_direction": "Atom / Hydrogen bomb arc direction",
"icon_alt_player_leaderboard": "Player Leaderboard Icon",
"icon_alt_team_leaderboard": "Team Leaderboard Icon"
},
"single_modal": {
"title": "Solo",
@@ -718,6 +720,7 @@
"exit": "Exit Game",
"keep": "Keep Playing",
"spectate": "Spectate",
"requeue": "Play Again",
"wishlist": "Wishlist on Steam!",
"ofm_winter": "OpenFront Masters Winter Tournament!",
"ofm_winter_description": "Join the competitive tournament and compete against the best players",
+50
View File
@@ -0,0 +1,50 @@
{
"map": {
"height": 1500,
"num_land_tiles": 1207315,
"width": 1200
},
"map16x": {
"height": 375,
"num_land_tiles": 70747,
"width": 300
},
"map4x": {
"height": 750,
"num_land_tiles": 295140,
"width": 600
},
"name": "Yenisei",
"nations": [
{
"coordinates": [1050, 535],
"flag": "ru",
"name": "Baikalovsk"
},
{
"coordinates": [1120, 1315],
"flag": "ru",
"name": "Mungui"
},
{
"coordinates": [345, 1190],
"flag": "ru",
"name": "Polykarpovsk"
},
{
"coordinates": [335, 800],
"flag": "ru",
"name": "Central Island"
},
{
"coordinates": [75, 390],
"flag": "ru",
"name": "West Coast"
},
{
"coordinates": [420, 440],
"flag": "ru",
"name": "Northern Island"
}
]
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+26
View File
@@ -755,6 +755,32 @@ class Client {
if (decodedHash.startsWith("#refresh")) {
window.location.href = "/";
}
// Handle requeue parameter for ranked matchmaking
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.has("requeue")) {
// Remove only the requeue parameter, preserving other params and hash
searchParams.delete("requeue");
const newUrl =
window.location.pathname +
(searchParams.toString() ? "?" + searchParams.toString() : "") +
window.location.hash;
history.replaceState(null, "", newUrl);
// Wait for matchmaking button to be defined, then trigger its click handler
// This goes through username validation instead of bypassing it
customElements.whenDefined("matchmaking-button").then(() => {
const matchmakingButton = document.querySelector(
"matchmaking-button button",
) as HTMLButtonElement | null;
if (matchmakingButton) {
matchmakingButton.click();
} else {
console.warn(
"Requeue requested, but matchmaking button not found in DOM.",
);
}
});
}
}
private async handleJoinLobby(event: CustomEvent<JoinLobbyEvent>) {
+1 -1
View File
@@ -320,7 +320,7 @@ export class MatchmakingButton extends LitElement {
window.showPage?.("page-account");
}
private open() {
public open() {
this.matchmakingModal?.open();
}
+61 -34
View File
@@ -16,8 +16,11 @@ export class GameLeftSidebar extends LitElement implements Layer {
private isLeaderboardShow = false;
@state()
private isTeamLeaderboardShow = false;
@state()
private isVisible = false;
@state()
private isPlayerTeamLabelVisible = false;
@state()
private playerTeam: string | null = null;
private playerColor: Colord = new Colord("#FFFFFF");
@@ -59,7 +62,7 @@ export class GameLeftSidebar extends LitElement implements Layer {
this.requestUpdate();
}
if (!this.game.inSpawnPhase()) {
if (!this.game.inSpawnPhase() && this.isPlayerTeamLabelVisible) {
this.isPlayerTeamLabelVisible = false;
this.requestUpdate();
}
@@ -91,10 +94,65 @@ export class GameLeftSidebar extends LitElement implements Layer {
this.isVisible ? "translate-x-0" : "hidden"
}`}
>
<div class="flex items-center gap-4 xl:gap-6 text-white">
<div
class="cursor-pointer p-0.5 bg-gray-700/50 hover:bg-gray-600 border rounded-md border-slate-500 transition-colors"
@click=${this.toggleLeaderboard}
role="button"
tabindex="0"
@keydown=${(e: KeyboardEvent) => {
if (e.key === "Enter" || e.key === " " || e.code === "Space") {
e.preventDefault();
this.toggleLeaderboard();
}
}}
>
<img
src=${this.isLeaderboardShow
? leaderboardSolidIcon
: leaderboardRegularIcon}
alt=${translateText("help_modal.icon_alt_player_leaderboard") ||
"Player Leaderboard Icon"}
width="20"
height="20"
/>
</div>
${this.isTeamGame
? html`
<div
class="cursor-pointer p-0.5 bg-gray-700/50 hover:bg-gray-600 border rounded-md border-slate-500 transition-colors"
@click=${this.toggleTeamLeaderboard}
role="button"
tabindex="0"
@keydown=${(e: KeyboardEvent) => {
if (
e.key === "Enter" ||
e.key === " " ||
e.code === "Space"
) {
e.preventDefault();
this.toggleTeamLeaderboard();
}
}}
>
<img
src=${this.isTeamLeaderboardShow
? teamSolidIcon
: teamRegularIcon}
alt=${translateText(
"help_modal.icon_alt_team_leaderboard",
) || "Team Leaderboard Icon"}
width="20"
height="20"
/>
</div>
`
: null}
</div>
${this.isPlayerTeamLabelVisible
? html`
<div
class="flex items-center w-full h-8 lg:h-10 text-white py-1 lg:p-2"
class="flex items-center w-full text-white"
@contextmenu=${(e: Event) => e.preventDefault()}
>
${translateText("help_modal.ui_your_team")}
@@ -108,39 +166,8 @@ export class GameLeftSidebar extends LitElement implements Layer {
`
: null}
<div
class=${`flex items-center gap-2 text-white ${
this.isLeaderboardShow || this.isTeamLeaderboardShow ? "mb-2" : ""
}`}
class=${`block lg:flex flex-wrap ${this.isLeaderboardShow && this.isTeamLeaderboardShow ? "gap-2" : ""}`}
>
<div class="cursor-pointer" @click=${this.toggleLeaderboard}>
<img
src=${this.isLeaderboardShow
? leaderboardSolidIcon
: leaderboardRegularIcon}
alt="treeIcon"
width="20"
height="20"
/>
</div>
${this.isTeamGame
? html`
<div
class="cursor-pointer"
@click=${this.toggleTeamLeaderboard}
>
<img
src=${this.isTeamLeaderboardShow
? teamSolidIcon
: teamRegularIcon}
alt="treeIcon"
width="20"
height="20"
/>
</div>
`
: null}
</div>
<div class="block lg:flex flex-wrap gap-2">
<leader-board .visible=${this.isLeaderboardShow}></leader-board>
<team-stats
class="flex-1"
+4 -2
View File
@@ -177,7 +177,7 @@ export class Leaderboard extends LitElement implements Layer {
}
return html`
<div
class="max-h-[35vh] overflow-y-auto text-white text-xs md:text-xs lg:text-sm md:max-h-[50vh] ${this
class="max-h-[35vh] overflow-y-auto text-white text-xs md:text-xs lg:text-sm md:max-h-[50vh] mt-2 ${this
.visible
? ""
: "hidden"}"
@@ -265,7 +265,9 @@ export class Leaderboard extends LitElement implements Layer {
</div>
<button
class="mt-1 px-1.5 py-0.5 md:px-2 md:py-0.5 text-xs md:text-xs lg:text-sm border border-white/20 hover:bg-white/10 text-white mx-auto block"
class="mt-1 px-1.5 pb-0.5 md:px-2 text-xs md:text-xs lg:text-sm
border rounded-md border-slate-500 transition-colors
text-white mx-auto block hover:bg-white/10"
@click=${() => {
this.showTopFive = !this.showTopFive;
this.updateLeaderboard();
+1 -1
View File
@@ -132,7 +132,7 @@ export class TeamStats extends LitElement implements Layer {
return html`
<div
class="max-h-[30vh] overflow-y-auto grid bg-slate-800/70 w-full text-white text-xs md:text-sm"
class="max-h-[30vh] overflow-y-auto grid bg-slate-800/70 w-full text-white text-xs md:text-sm mt-2"
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
>
<div
+24 -1
View File
@@ -8,6 +8,7 @@ import {
} from "../../../client/Utils";
import { ColorPalette, Pattern } from "../../../core/CosmeticSchemas";
import { EventBus } from "../../../core/EventBus";
import { RankedType } from "../../../core/game/Game";
import { GameUpdateType } from "../../../core/game/GameUpdates";
import { GameView } from "../../../core/game/GameView";
import { getUserMe } from "../../Api";
@@ -37,6 +38,9 @@ export class WinModal extends LitElement implements Layer {
@state()
private isWin = false;
@state()
private isRankedGame = false;
@state()
private patternContent: TemplateResult | null = null;
@@ -75,11 +79,21 @@ export class WinModal extends LitElement implements Layer {
>
${translateText("win_modal.exit")}
</button>
${this.isRankedGame
? html`
<button
@click=${this._handleRequeue}
class="flex-1 px-3 py-3 text-base cursor-pointer bg-purple-600 text-white border-0 rounded-sm transition-all duration-200 hover:bg-purple-500 hover:-translate-y-px active:translate-y-px"
>
${translateText("win_modal.requeue")}
</button>
`
: null}
<button
@click=${this.hide}
class="flex-1 px-3 py-3 text-base cursor-pointer bg-blue-500/60 text-white border-0 rounded-sm transition-all duration-200 hover:bg-blue-500/80 hover:-translate-y-px active:translate-y-px"
>
${this.isWin
${this.game.myPlayer()?.isAlive()
? translateText("win_modal.keep")
: translateText("win_modal.spectate")}
</button>
@@ -251,6 +265,9 @@ export class WinModal extends LitElement implements Layer {
async show() {
crazyGamesSDK.gameplayStop();
await this.loadPatternContent();
// Check if this is a ranked game
this.isRankedGame =
this.game.config().gameConfig().rankedType === RankedType.OneVOne;
this.isVisible = true;
this.requestUpdate();
setTimeout(() => {
@@ -270,6 +287,12 @@ export class WinModal extends LitElement implements Layer {
window.location.href = "/";
}
private _handleRequeue() {
this.hide();
// Navigate to homepage and open matchmaking modal
window.location.href = "/?requeue";
}
init() {}
tick() {
+2
View File
@@ -118,6 +118,7 @@ export enum GameMapType {
Didier = "Didier",
DidierFrance = "Didier France",
AmazonRiver = "Amazon River",
Yenisei = "Yenisei",
}
export type GameMapName = keyof typeof GameMapType;
@@ -165,6 +166,7 @@ export const mapCategories: Record<string, GameMapType[]> = {
GameMapType.TwoLakes,
GameMapType.StraitOfHormuz,
GameMapType.AmazonRiver,
GameMapType.Yenisei,
],
fantasy: [
GameMapType.Pangaea,
+4 -1
View File
@@ -988,7 +988,10 @@ export class PlayerImpl implements Player {
}
const cost = this.mg.unitInfo(unitType).cost(this.mg, this);
if (!this.isAlive() || this.gold() < cost) {
if (
unitType !== UnitType.MIRVWarhead &&
(!this.isAlive() || this.gold() < cost)
) {
return false;
}
switch (unitType) {
+1
View File
@@ -66,6 +66,7 @@ const frequency: Partial<Record<GameMapName, number>> = {
AmazonRiver: 3,
Sierpinski: 10,
TheBox: 3,
Yenisei: 6,
};
interface MapWithMode {
@@ -0,0 +1,117 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { RankedType } from "../../../../src/core/game/Game";
vi.mock("../../../../src/client/Utils", () => ({
translateText: vi.fn((key: string) => {
const translations: Record<string, string> = {
"win_modal.exit": "Exit",
"win_modal.requeue": "Play Again",
"win_modal.keep": "Keep Playing",
"win_modal.spectate": "Spectate",
};
return translations[key] || key;
}),
getGamesPlayed: vi.fn(() => 10),
isInIframe: vi.fn(() => false),
TUTORIAL_VIDEO_URL: "https://example.com/tutorial",
}));
vi.mock("../../../../src/client/Api", () => ({
getUserMe: vi.fn(async () => null),
}));
vi.mock("../../../../src/client/Cosmetics", () => ({
fetchCosmetics: vi.fn(async () => []),
handlePurchase: vi.fn(),
patternRelationship: vi.fn(() => ({})),
}));
vi.mock("../../../../src/client/CrazyGamesSDK", () => ({
crazyGamesSDK: {
happytime: vi.fn(),
requestAd: vi.fn(),
gameplayStop: vi.fn(),
},
}));
describe("WinModal Requeue", () => {
let mockLocationHref = "";
beforeEach(() => {
mockLocationHref = "";
// Mock window.location.href using Object.defineProperty
const locationMock = {
get href() {
return mockLocationHref;
},
set href(value: string) {
mockLocationHref = value;
},
};
Object.defineProperty(window, "location", {
value: locationMock,
writable: true,
configurable: true,
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("isRankedGame detection", () => {
it("should detect ranked 1v1 game", () => {
const gameConfig = {
rankedType: RankedType.OneVOne,
};
const isRankedGame = gameConfig.rankedType === RankedType.OneVOne;
expect(isRankedGame).toBe(true);
});
it("should not detect non-ranked game", () => {
const gameConfig = {
rankedType: undefined,
};
const isRankedGame = gameConfig.rankedType === RankedType.OneVOne;
expect(isRankedGame).toBe(false);
});
});
describe("requeue navigation", () => {
it("should navigate to /?requeue when requeue is triggered", () => {
// Simulate the _handleRequeue behavior
const handleRequeue = () => {
window.location.href = "/?requeue";
};
handleRequeue();
expect(window.location.href).toBe("/?requeue");
});
it("should navigate to / when exit is triggered", () => {
// Simulate the _handleExit behavior
const handleExit = () => {
window.location.href = "/";
};
handleExit();
expect(window.location.href).toBe("/");
});
});
describe("requeue URL parameter handling", () => {
it("should parse requeue parameter from URL", () => {
const url = new URL("http://localhost:9000/?requeue");
const hasRequeue = url.searchParams.has("requeue");
expect(hasRequeue).toBe(true);
});
it("should not find requeue parameter when absent", () => {
const url = new URL("http://localhost:9000/");
const hasRequeue = url.searchParams.has("requeue");
expect(hasRequeue).toBe(false);
});
});
});