Merge branch 'main' into feat/advertise-section

This commit is contained in:
Alex Besios
2026-04-08 15:53:29 +03:00
committed by GitHub
18 changed files with 198 additions and 43 deletions
+8
View File
@@ -64,6 +64,14 @@ Licensed under [CC0 1.0.](https://creativecommons.org/publicdomain/zero/1.0/lega
[NASA/JPL-Caltech](https://www.jpl.nasa.gov/images/pia10748-our-milky-way-gets-a-makeover-artist-concept/)
[Public Domain](https://www.jpl.nasa.gov/jpl-image-use-policy/)
### Strait Of Malacca Map
[https://commons.wikimedia.org/wiki/User:Sadalmelik#/media/File:Sumatra_Topography.png](https://commons.wikimedia.org/wiki/File:Sumatra_Topography.png#/media/File:Sumatra_Topography.png)
Sadalmelik - Own work
Topographic map of Sumatra. Created with GMT from publicly released SRTM data. For locator version, see Image:Sumatra Locator Topography.png
CC BY-SA 3.0
File:Sumatra Topography.png
## Icons
### [The Noun Project](https://thenounproject.com/)
+18 -2
View File
@@ -1,5 +1,7 @@
# API Usage
> **Warning:** Rate limits are very strict. Join the [Discord](https://discord.gg/K9zernJB5z) to request higher rate limits.
## Games
### List Game Metadata
@@ -207,10 +209,24 @@ GET https://api.openfront.io/public/clan/:clanTag/sessions
- `start` (optional): ISO 8601 timestamp
- `end` (optional): ISO 8601 timestamp
- `page` (optional): Page number, 1-200 (default: 1)
- `limit` (optional): Results per page, 1-50 (default: 20)
**Response:**
```json
{
"results": [ ... ],
"total": 150,
"page": 1,
"limit": 20
}
```
Results are ordered by game start time, newest first.
**Example**
```bash
curl https://api.openfront.io/public/clan/UN/sessions?start=2025-11-15T00:00:00Z &
end=2025-11-18T23:59:59Z
curl "https://api.openfront.io/public/clan/UN/sessions?start=2025-11-15T00:00:00Z&end=2025-11-18T23:59:59Z&limit=10&page=1"
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

@@ -0,0 +1,50 @@
{
"name": "straitofmalacca",
"nations": [
{
"coordinates": [1268, 730],
"name": "Singapore",
"flag": "sg"
},
{
"coordinates": [862, 41],
"name": "Thailand",
"flag": "th"
},
{
"coordinates": [1328, 1284],
"name": "Sumatra",
"flag": "id"
},
{
"coordinates": [1141, 1090],
"name": "Burmese Pythons",
"flag": "mm"
},
{
"coordinates": [210, 243],
"name": "Aceh",
"flag": "id"
},
{
"coordinates": [1011, 736],
"name": "Riau",
"flag": "id"
},
{
"coordinates": [600, 565],
"name": "Samosir",
"flag": "id"
},
{
"coordinates": [827, 999],
"name": "Barisan",
"flag": "id"
},
{
"coordinates": [1053, 333],
"name": "Malaysia",
"flag": "my"
}
]
}
+2 -1
View File
@@ -50,6 +50,7 @@ var maps = []struct {
{Name: "japan"},
{Name: "lisbon"},
{Name: "manicouagan"},
{Name: "straitofmalacca"},
{Name: "mars"},
{Name: "mena"},
{Name: "montreal"},
@@ -86,7 +87,7 @@ var maps = []struct {
{Name: "milkyway"},
{Name: "mediterranean"},
{Name: "greatlakes"},
{Name: "dyslexdria"},
{Name: "dyslexdria"},
{Name: "big_plains", IsTest: true},
{Name: "half_land_half_ocean", IsTest: true},
{Name: "ocean_and_land", IsTest: true},
+2 -1
View File
@@ -355,7 +355,8 @@
"milkyway": "Milky Way",
"mediterranean": "Mediterranean",
"dyslexdria": "Dyslexdria",
"greatlakes": "Great Lakes"
"greatlakes": "Great Lakes",
"straitofmalacca": "Strait Of Malacca"
},
"map_categories": {
"featured": "Featured",
@@ -0,0 +1,65 @@
{
"map": {
"height": 1644,
"num_land_tiles": 867389,
"width": 1832
},
"map16x": {
"height": 411,
"num_land_tiles": 51062,
"width": 458
},
"map4x": {
"height": 822,
"num_land_tiles": 212395,
"width": 916
},
"name": "straitofmalacca",
"nations": [
{
"coordinates": [1268, 730],
"flag": "sg",
"name": "Singapore"
},
{
"coordinates": [862, 41],
"flag": "th",
"name": "Thailand"
},
{
"coordinates": [1328, 1284],
"flag": "id",
"name": "Sumatra"
},
{
"coordinates": [1141, 1090],
"flag": "mm",
"name": "Burmese Pythons"
},
{
"coordinates": [210, 243],
"flag": "id",
"name": "Aceh"
},
{
"coordinates": [1011, 736],
"flag": "id",
"name": "Riau"
},
{
"coordinates": [600, 565],
"flag": "id",
"name": "Samosir"
},
{
"coordinates": [827, 999],
"flag": "id",
"name": "Barisan"
},
{
"coordinates": [1053, 333],
"flag": "my",
"name": "Malaysia"
}
]
}
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: 9.6 KiB

+5 -8
View File
@@ -789,23 +789,20 @@ export class HostLobbyModal extends BaseModal {
disabledUnits: this.disabledUnits,
spawnImmunityDuration: this.spawnImmunity
? spawnImmunityTicks
: undefined,
: null,
playerTeams: this.teamCount,
nations: sliderToNationsConfig(
this.nations,
this.defaultNationCount,
),
maxTimerValue:
this.maxTimer === true ? this.maxTimerValue : undefined,
maxTimerValue: this.maxTimer === true ? this.maxTimerValue : null,
goldMultiplier:
this.goldMultiplier === true
? this.goldMultiplierValue
: undefined,
this.goldMultiplier === true ? this.goldMultiplierValue : null,
startingGold:
this.startingGold === true && this.startingGoldValue !== undefined
? Math.round(this.startingGoldValue * 1_000_000)
: undefined,
disableAlliances: this.disableAlliances || undefined,
: null,
disableAlliances: this.disableAlliances || null,
} satisfies Partial<GameConfig>,
},
bubbles: true,
@@ -36,6 +36,7 @@ interface AttackLabel {
export class AttackingTroopsOverlay implements Layer {
private container: HTMLDivElement;
private labelTemplate: HTMLDivElement;
private labels = new Map<string, AttackLabel>();
// Guard against queuing multiple worker requests in the same tick window.
private inFlightRequest = false;
@@ -63,6 +64,8 @@ export class AttackingTroopsOverlay implements Layer {
this.container.style.zIndex = "4";
document.body.appendChild(this.container);
this.labelTemplate = this.createLabelTemplate();
this.onAlternateView = (e) => {
this.isVisible = !e.alternateView;
this.container.style.display = this.isVisible ? "" : "none";
@@ -235,28 +238,39 @@ export class AttackingTroopsOverlay implements Layer {
}
}
private createLabelElement(
attackerTroops: number,
defenderTroops: number,
isIncoming: boolean,
): HTMLDivElement {
private createLabelTemplate(): HTMLDivElement {
const el = document.createElement("div");
el.style.position = "absolute";
el.style.display = "none";
el.style.alignItems = "center";
el.style.gap = "3px";
el.style.width = "max-content";
el.style.whiteSpace = "nowrap";
el.style.fontSize = "11px";
el.style.fontWeight = "bold";
el.style.fontFamily = this.game.config().theme().font();
el.style.padding = "1px 4px";
el.style.borderRadius = "3px";
el.style.backgroundColor = "rgba(0,0,0,0.55)";
el.style.pointerEvents = "none";
el.style.lineHeight = "1.3";
// Smooth the label to its new position as the front line advances.
el.style.transition = "transform 0.2s ease-out";
el.style.width = "max-content";
const icon = document.createElement("img");
icon.style.width = "10px";
icon.style.height = "10px";
el.appendChild(icon);
const span = document.createElement("span");
span.style.minWidth = "25px";
el.appendChild(span);
return el;
}
private createLabelElement(
attackerTroops: number,
defenderTroops: number,
isIncoming: boolean,
): HTMLDivElement {
const el = this.labelTemplate.cloneNode(true) as HTMLDivElement;
el.style.fontFamily = this.game.config().theme().font();
this.updateLabelContent(el, attackerTroops, defenderTroops, isIncoming);
this.container.appendChild(el);
return el;
@@ -268,17 +282,8 @@ export class AttackingTroopsOverlay implements Layer {
defenderTroops: number,
isIncoming: boolean,
) {
// Reuse existing children to avoid DOM churn on every tick.
let icon = el.querySelector("img") as HTMLImageElement | null;
let span = el.querySelector("span") as HTMLSpanElement | null;
if (!icon || !span) {
icon = document.createElement("img");
icon.style.width = "10px";
icon.style.height = "10px";
span = document.createElement("span");
el.replaceChildren(icon, span);
}
const icon = el.children[0] as HTMLImageElement;
const span = el.children[1] as HTMLSpanElement;
if (isIncoming) {
icon.src = shieldIcon;
span.style.color = troopDefenceColor(attackerTroops, defenderTroops);
@@ -99,7 +99,10 @@ export class GameRightSidebar extends LitElement implements Layer {
const elapsedSeconds = Math.floor(gameTicks / 10); // 10 ticks per second
if (this.game.inSpawnPhase()) {
this.timer = maxTimerValue !== undefined ? maxTimerValue * 60 : 0;
this.timer =
maxTimerValue !== null && maxTimerValue !== undefined
? maxTimerValue * 60
: 0;
return;
}
@@ -107,7 +110,7 @@ export class GameRightSidebar extends LitElement implements Layer {
return;
}
if (maxTimerValue !== undefined) {
if (maxTimerValue !== null && maxTimerValue !== undefined) {
this.timer = Math.max(0, maxTimerValue * 60 - elapsedSeconds);
} else {
this.timer = elapsedSeconds;
@@ -179,6 +182,7 @@ export class GameRightSidebar extends LitElement implements Layer {
const timerColor =
this.game.config().gameConfig().maxTimerValue !== undefined &&
this.game.config().gameConfig().maxTimerValue !== null &&
this.timer < 60
? "text-red-400"
: "";
+5 -5
View File
@@ -247,15 +247,15 @@ export const GameConfigSchema = z.object({
infiniteTroops: z.boolean(),
instantBuild: z.boolean(),
disableNavMesh: z.boolean().optional(),
disableAlliances: z.boolean().optional(),
disableAlliances: z.boolean().nullable().optional(),
randomSpawn: z.boolean(),
maxPlayers: z.number().optional(),
maxTimerValue: z.number().int().min(1).max(120).optional(), // In minutes
spawnImmunityDuration: z.number().int().min(0).optional(), // In ticks
maxTimerValue: z.number().int().min(1).max(120).nullable().optional(), // In minutes
spawnImmunityDuration: z.number().int().min(0).nullable().optional(), // In ticks
disabledUnits: z.enum(UnitType).array().optional(),
playerTeams: TeamCountConfigSchema.optional(),
goldMultiplier: z.number().min(0.1).max(1000).optional(),
startingGold: z.number().int().min(0).max(1000000000).optional(),
goldMultiplier: z.number().min(0.1).max(1000).nullable().optional(),
startingGold: z.number().int().min(0).max(1000000000).nullable().optional(),
});
export const TeamSchema = z.string();
+2
View File
@@ -147,6 +147,7 @@ export enum GameMapType {
Mediterranean = "Mediterranean",
Dyslexdria = "Dyslexdria",
GreatLakes = "Great Lakes",
StraitOfMalacca = "Strait Of Malacca",
}
export type GameMapName = keyof typeof GameMapType;
@@ -200,6 +201,7 @@ export const mapCategories: Record<string, GameMapType[]> = {
GameMapType.Aegean,
GameMapType.Mediterranean,
GameMapType.GreatLakes,
GameMapType.StraitOfMalacca,
],
fantasy: [
GameMapType.Pangaea,
+7 -5
View File
@@ -141,7 +141,7 @@ export class GameServer {
this.gameConfig.donateTroops = gameConfig.donateTroops;
}
if (gameConfig.maxTimerValue !== undefined) {
this.gameConfig.maxTimerValue = gameConfig.maxTimerValue;
this.gameConfig.maxTimerValue = gameConfig.maxTimerValue ?? undefined;
}
if (gameConfig.instantBuild !== undefined) {
this.gameConfig.instantBuild = gameConfig.instantBuild;
@@ -150,7 +150,8 @@ export class GameServer {
this.gameConfig.randomSpawn = gameConfig.randomSpawn;
}
if (gameConfig.spawnImmunityDuration !== undefined) {
this.gameConfig.spawnImmunityDuration = gameConfig.spawnImmunityDuration;
this.gameConfig.spawnImmunityDuration =
gameConfig.spawnImmunityDuration ?? undefined;
}
if (gameConfig.gameMode !== undefined) {
this.gameConfig.gameMode = gameConfig.gameMode;
@@ -162,13 +163,14 @@ export class GameServer {
this.gameConfig.playerTeams = gameConfig.playerTeams;
}
if (gameConfig.goldMultiplier !== undefined) {
this.gameConfig.goldMultiplier = gameConfig.goldMultiplier;
this.gameConfig.goldMultiplier = gameConfig.goldMultiplier ?? undefined;
}
if (gameConfig.startingGold !== undefined) {
this.gameConfig.startingGold = gameConfig.startingGold;
this.gameConfig.startingGold = gameConfig.startingGold ?? undefined;
}
if (gameConfig.disableAlliances !== undefined) {
this.gameConfig.disableAlliances = gameConfig.disableAlliances;
this.gameConfig.disableAlliances =
gameConfig.disableAlliances ?? undefined;
}
}
+1
View File
@@ -88,6 +88,7 @@ const frequency: Partial<Record<GameMapName, number>> = {
Mediterranean: 6,
Dyslexdria: 8,
GreatLakes: 6,
StraitOfMalacca: 4,
};
const TEAM_WEIGHTS: { config: TeamCountConfig; weight: number }[] = [