mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 04:43:28 +00:00
Merge branch 'main' into local-attack
This commit is contained in:
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
@@ -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>) {
|
||||
|
||||
@@ -320,7 +320,7 @@ export class MatchmakingButton extends LitElement {
|
||||
window.showPage?.("page-account");
|
||||
}
|
||||
|
||||
private open() {
|
||||
public open() {
|
||||
this.matchmakingModal?.open();
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user