Merge branch 'main' into features/footer

This commit is contained in:
q8gazy
2025-02-10 02:54:37 +03:00
committed by GitHub
17 changed files with 176 additions and 134 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

+27 -9
View File
@@ -20,7 +20,7 @@ export class PublicLobby extends LitElement {
this.fetchAndUpdateLobbies();
this.lobbiesInterval = window.setInterval(
() => this.fetchAndUpdateLobbies(),
1000,
1000
);
}
@@ -60,6 +60,11 @@ export class PublicLobby extends LitElement {
const lobby = this.lobbies[0];
const timeRemaining = Math.max(0, Math.floor(lobby.msUntilStart / 1000));
// Format time to show minutes and seconds
const minutes = Math.floor(timeRemaining / 60);
const seconds = timeRemaining % 60;
const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
return html`
<button
@click=${() => this.lobbyClicked(lobby)}
@@ -68,12 +73,25 @@ export class PublicLobby extends LitElement {
: "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90"
>
<div class="text-lg md:text-2xl font-semibold mb-2">Next Game</div>
<div
class="flex flex-col gap-1 md:gap-2 text-blue-100 text-s md:text-lg"
>
<div>Starts in: ${timeRemaining}s</div>
<div>Players: ${lobby.numClients}</div>
<div>ID: ${lobby.id}</div>
<div class="flex items-center justify-center gap-4">
<div class="flex flex-col items-start">
<div class="text-md font-medium text-blue-100">
${lobby.gameConfig.gameMap}
</div>
</div>
<div class="flex flex-col items-start">
<div class="text-md font-medium text-blue-100">
${lobby.numClients}
${lobby.numClients === 1 ? "Player" : "Players"} waiting
</div>
</div>
<div class="flex items-center">
<div
class="min-w-20 text-sm font-medium px-2 py-1 bg-white/10 rounded-xl text-blue-100 text-center"
>
${timeDisplay}
</div>
</div>
</div>
</button>
`;
@@ -93,7 +111,7 @@ export class PublicLobby extends LitElement {
},
bubbles: true,
composed: true,
}),
})
);
} else {
this.dispatchEvent(
@@ -101,7 +119,7 @@ export class PublicLobby extends LitElement {
detail: { lobby: this.currLobby },
bubbles: true,
composed: true,
}),
})
);
this.currLobby = null;
}
+61 -43
View File
@@ -135,62 +135,80 @@ export class ControlPanel extends LitElement implements Layer {
</div>
</div>
<div class="relative mb-4 lg:mb-4 h-6 lg:h-6">
<div class="relative mb-4 lg:mb-4">
<label class="block text-white mb-1"
>Troops: ${renderTroops(this._troops)} | Workers:
${renderTroops(this._workers)}</label
>
<div
class="absolute h-2 bg-blue-500/60 rounded top-6 transition-all duration-300"
style="width: ${this.currentTroopRatio * 100}%"
></div>
<div
class="absolute w-4 h-4 bg-white border-2 border-blue-500 rounded-full top-5 -ml-2 cursor-pointer hover:scale-110 transition-transform"
style="left: ${this.targetTroopRatio * 100}%"
></div>
<input
type="range"
min="1"
max="100"
.value=${this.targetTroopRatio * 100}
@input=${(e: Event) => {
this.targetTroopRatio =
parseInt((e.target as HTMLInputElement).value) / 100;
this.onTroopChange(this.targetTroopRatio);
}}
class="absolute w-full top-3 m-0 opacity-0 cursor-pointer"
/>
<div class="relative h-8">
<!-- Background track -->
<div
class="absolute left-0 right-0 top-3 h-2 bg-white/20 rounded"
></div>
<!-- Fill track -->
<div
class="absolute left-0 top-3 h-2 bg-blue-500/60 rounded transition-all duration-300"
style="width: ${this.currentTroopRatio * 100}%"
></div>
<!-- Range input - exactly overlaying the visual elements -->
<input
type="range"
min="1"
max="100"
.value=${this.targetTroopRatio * 100}
@input=${(e: Event) => {
this.targetTroopRatio =
parseInt((e.target as HTMLInputElement).value) / 100;
this.onTroopChange(this.targetTroopRatio);
}}
class="absolute left-0 right-0 top-2 m-0 h-4 opacity-0 cursor-pointer"
/>
<!-- Handle -->
<div
class="absolute top-2 w-4 h-4 bg-white border-2 border-blue-500 rounded-full -ml-2 cursor-pointer hover:scale-110 transition-transform pointer-events-none"
style="left: ${this.targetTroopRatio * 100}%"
></div>
</div>
</div>
<div class="relative mb:0 lg:mb-4 h-10 lg:h-12">
<div class="relative mb-0 lg:mb-4">
<label class="block text-white mb-1"
>Attack Ratio: ${(this.attackRatio * 100).toFixed(0)}%</label
>
<div class="absolute w-full h-2 bg-white/20 rounded top-6"></div>
<div
class="absolute h-2 bg-red-500/60 rounded top-6 transition-all duration-300"
style="width: ${this.attackRatio * 100}%"
></div>
<div
class="absolute w-4 h-4 bg-white border-2 border-red-500 rounded-full top-5 -ml-2 cursor-pointer hover:scale-110 transition-transform"
style="left: ${this.attackRatio * 100}%"
></div>
<input
type="range"
min="1"
max="100"
.value=${this.attackRatio * 100}
@input=${(e: Event) => {
this.attackRatio =
parseInt((e.target as HTMLInputElement).value) / 100;
this.onAttackRatioChange(this.attackRatio);
}}
class="absolute w-full top-3 m-0 opacity-0 cursor-pointer"
/>
<div class="relative h-8">
<!-- Background track -->
<div
class="absolute left-0 right-0 top-3 h-2 bg-white/20 rounded"
></div>
<!-- Fill track -->
<div
class="absolute left-0 top-3 h-2 bg-red-500/60 rounded transition-all duration-300"
style="width: ${this.attackRatio * 100}%"
></div>
<!-- Range input - exactly overlaying the visual elements -->
<input
type="range"
min="1"
max="100"
.value=${this.attackRatio * 100}
@input=${(e: Event) => {
this.attackRatio =
parseInt((e.target as HTMLInputElement).value) / 100;
this.onAttackRatioChange(this.attackRatio);
}}
class="absolute left-0 right-0 top-2 m-0 h-4 opacity-0 cursor-pointer"
/>
<!-- Handle -->
<div
class="absolute top-2 w-4 h-4 bg-white border-2 border-red-500 rounded-full -ml-2 cursor-pointer hover:scale-110 transition-transform pointer-events-none"
style="left: ${this.attackRatio * 100}%"
></div>
</div>
</div>
</div>
`;
}
createRenderRoot() {
return this; // Disable shadow DOM to allow Tailwind styles
}
+14 -24
View File
@@ -25,7 +25,7 @@ class RenderInfo {
public lastRenderCalc: number,
public location: Cell,
public fontSize: number,
public element: HTMLElement,
public element: HTMLElement
) {}
}
@@ -49,7 +49,7 @@ export class NameLayer implements Layer {
private game: GameView,
private theme: Theme,
private transformHandler: TransformHandler,
private clientID: ClientID,
private clientID: ClientID
) {
this.traitorIconImage = new Image();
this.traitorIconImage.src = traitorIcon;
@@ -100,13 +100,7 @@ export class NameLayer implements Layer {
if (!this.seenPlayers.has(player)) {
this.seenPlayers.add(player);
this.renders.push(
new RenderInfo(
player,
0,
null,
0,
this.createPlayerElement(player),
),
new RenderInfo(player, 0, null, 0, this.createPlayerElement(player))
);
}
}
@@ -115,11 +109,11 @@ export class NameLayer implements Layer {
public renderLayer(mainContex: CanvasRenderingContext2D) {
const screenPosOld = this.transformHandler.worldToScreenCoordinates(
new Cell(0, 0),
new Cell(0, 0)
);
const screenPos = new Cell(
screenPosOld.x - window.innerWidth / 2,
screenPosOld.y - window.innerHeight / 2,
screenPosOld.y - window.innerHeight / 2
);
this.container.style.transform = `translate(${screenPos.x}px, ${screenPos.y}px) scale(${this.transformHandler.scale})`;
@@ -136,7 +130,7 @@ export class NameLayer implements Layer {
0,
0,
mainContex.canvas.width,
mainContex.canvas.height,
mainContex.canvas.height
);
}
@@ -191,7 +185,7 @@ export class NameLayer implements Layer {
const oldLocation = render.location;
render.location = new Cell(
render.player.nameLocation().x,
render.player.nameLocation().y,
render.player.nameLocation().y
);
// Calculate base size and scale
@@ -230,7 +224,7 @@ export class NameLayer implements Layer {
if (render.player === this.firstPlace) {
if (!existingCrown) {
iconsDiv.appendChild(
this.createIconElement(this.crownIconImage.src, iconSize, "crown"),
this.createIconElement(this.crownIconImage.src, iconSize, "crown")
);
}
} else if (existingCrown) {
@@ -242,11 +236,7 @@ export class NameLayer implements Layer {
if (render.player.isTraitor()) {
if (!existingTraitor) {
iconsDiv.appendChild(
this.createIconElement(
this.traitorIconImage.src,
iconSize,
"traitor",
),
this.createIconElement(this.traitorIconImage.src, iconSize, "traitor")
);
}
} else if (existingTraitor) {
@@ -261,8 +251,8 @@ export class NameLayer implements Layer {
this.createIconElement(
this.allianceIconImage.src,
iconSize,
"alliance",
),
"alliance"
)
);
}
} else if (existingAlliance) {
@@ -277,7 +267,7 @@ export class NameLayer implements Layer {
) {
if (!existingTarget) {
iconsDiv.appendChild(
this.createIconElement(this.targetIconImage.src, iconSize, "target"),
this.createIconElement(this.targetIconImage.src, iconSize, "target")
);
}
} else if (existingTarget) {
@@ -291,7 +281,7 @@ export class NameLayer implements Layer {
.filter(
(emoji) =>
emoji.recipientID == AllPlayers ||
emoji.recipientID == myPlayer?.smallID(),
emoji.recipientID == myPlayer?.smallID()
);
if (emojis.length > 0) {
@@ -324,7 +314,7 @@ export class NameLayer implements Layer {
private createIconElement(
src: string,
size: number,
id: string,
id: string
): HTMLImageElement {
const icon = document.createElement("img");
icon.src = src;
+9 -6
View File
@@ -11,6 +11,7 @@ import {
manhattanDistFN,
TileRef,
} from "../../../core/game/GameMap";
import { GameUpdateType } from "../../../core/game/GameUpdates";
enum Relationship {
Self,
@@ -48,9 +49,9 @@ export class UnitLayer implements Layer {
if (this.myPlayer == null) {
this.myPlayer = this.game.playerByClientID(this.clientID);
}
for (const unit of this.game.units()) {
if (unit.wasUpdated()) this.onUnitEvent(unit);
}
this.game.updatesSinceLastTick()?.[GameUpdateType.Unit]?.forEach((unit) => {
this.onUnitEvent(this.game.unit(unit.id));
});
}
init() {
@@ -79,9 +80,11 @@ export class UnitLayer implements Layer {
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
for (const unit of this.game.units()) {
this.onUnitEvent(unit);
}
this.game
?.updatesSinceLastTick()
?.[GameUpdateType.Unit]?.forEach((unit) => {
this.onUnitEvent(this.game.unit(unit.id));
});
}
private relationship(unit: UnitView): Relationship {
+21 -17
View File
@@ -74,6 +74,11 @@
gtag("js", new Date());
gtag("config", "AW-16702609763");
</script>
<script
async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7035513310742290"
crossorigin="anonymous"
></script>
</head>
<body
@@ -81,19 +86,18 @@
>
<!-- Main container with responsive padding -->
<!-- Logo section remains the same -->
<div
class="container mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8 flex-grow"
>
<img
src="../../resources/images/OpenFrontLogo.svg"
alt="OpenFront.io"
class="pt-6 md:pt-12 h-auto w-3/4 md:w-1/2 lg:w-1/3 mx-auto transform sm:scale-125 md:scale-150 lg:scale-175"
/>
<h3
class="font-sans text-center text-black pb-6 md:pb-12 text-sm sm:text-base lg:text-lg"
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8 flex-grow">
<div class="flex justify-center">
<img
src="../../resources/images/OpenFrontLogo.png"
alt="OpenFront.io"
/>
</div>
<div
class="flex justify-center text-sm font-bold mt-[-10px] pb-6 md:pb-12"
>
(v0.15.0)
</h3>
v0.15.0
</div>
<div
class="max-w-sm sm:max-w-md lg:max-w-lg xl:max-w-xl mx-auto p-2 pb-4"
@@ -161,15 +165,15 @@
class="bottom-0 w-full flex-col-reverse sm:flex-row z-50"
style="position: fixed; pointer-events: none"
>
<div class="" style="pointer-events: auto; max-width: max-content;">
<control-panel></control-panel>
</div>
<div
class="sm:fixed sm:right-0 sm:bottom-0 sm:flex justify-end"
style="pointer-events: auto; max-width: max-content;"
class="w-full sm:w-2/3 sm:fixed sm:right-0 sm:bottom-0 sm:flex justify-end"
style="pointer-events: auto"
>
<events-display></events-display>
</div>
<div class="w-full sm:w-1/3" style="pointer-events: auto">
<control-panel></control-panel>
</div>
</div>
<!-- Footer section -->
+3 -2
View File
@@ -85,6 +85,7 @@ export interface Lobby {
id: string;
msUntilStart?: number;
numClients?: number;
gameConfig?: GameConfig;
}
const GameConfigSchema = z.object({
@@ -96,7 +97,7 @@ const GameConfigSchema = z.object({
const SafeString = z
.string()
// Remove common dangerous characters and patterns
.regex(/^[a-zA-Z0-9\s.,!?@#$%&*()-_+=[\]{}|;:"'\/]+$/)
.regex(/^[a-zA-Z0-9\s.,!?@#$%&*()-_+=\[\]{}|;:"'\/]+$/)
// Reasonable max length to prevent DOS
.max(1000);
@@ -106,7 +107,7 @@ const EmojiSchema = z.string().refine(
},
{
message: "Must contain at least one emoji character",
},
}
);
const ID = z
.string()
+20 -20
View File
@@ -16,7 +16,7 @@ import { andFN, GameMap, manhattanDistFN, TileRef } from "./game/GameMap";
export function manhattanDistWrapped(
c1: Cell,
c2: Cell,
width: number,
width: number
): number {
// Calculate x distance
let dx = Math.abs(c1.x - c2.x);
@@ -36,7 +36,7 @@ export function within(value: number, min: number, max: number): number {
export function distSort(
gm: GameMap,
target: TileRef,
target: TileRef
): (a: TileRef, b: TileRef) => number {
return (a: TileRef, b: TileRef) => {
return gm.manhattanDist(a, target) - gm.manhattanDist(b, target);
@@ -45,7 +45,7 @@ export function distSort(
export function distSortUnit(
gm: GameMap,
target: Unit | TileRef,
target: Unit | TileRef
): (a: Unit, b: Unit) => number {
const targetRef = typeof target === "number" ? target : target.tile();
@@ -61,7 +61,7 @@ export function distSortUnit(
export function sourceDstOceanShore(
gm: Game,
src: Player,
tile: TileRef,
tile: TileRef
): [TileRef | null, TileRef | null] {
const dst = gm.owner(tile);
let srcTile = closestOceanShoreFromPlayer(gm, src, tile);
@@ -88,10 +88,10 @@ export function targetTransportTile(gm: Game, tile: TileRef): TileRef | null {
export function closestOceanShoreFromPlayer(
gm: GameMap,
player: Player,
target: TileRef,
target: TileRef
): TileRef | null {
const shoreTiles = Array.from(player.borderTiles()).filter((t) =>
gm.isOceanShore(t),
gm.isOceanShore(t)
);
if (shoreTiles.length == 0) {
return null;
@@ -101,12 +101,12 @@ export function closestOceanShoreFromPlayer(
const closestDistance = manhattanDistWrapped(
gm.cell(target),
gm.cell(closest),
gm.width(),
gm.width()
);
const currentDistance = manhattanDistWrapped(
gm.cell(target),
gm.cell(current),
gm.width(),
gm.width()
);
return currentDistance < closestDistance ? current : closest;
});
@@ -115,13 +115,13 @@ export function closestOceanShoreFromPlayer(
function closestOceanShoreTN(
gm: GameMap,
tile: TileRef,
searchDist: number,
searchDist: number
): TileRef {
const tn = Array.from(
gm.bfs(
tile,
andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist)),
),
andFN((_, t) => !gm.hasOwner(t), manhattanDistFN(tile, searchDist))
)
)
.filter((t) => gm.isOceanShore(t))
.sort((a, b) => gm.manhattanDist(tile, a) - gm.manhattanDist(tile, b));
@@ -143,7 +143,7 @@ export function simpleHash(str: string): number {
export function calculateBoundingBox(
gm: GameMap,
borderTiles: ReadonlySet<TileRef>,
borderTiles: ReadonlySet<TileRef>
): { min: Cell; max: Cell } {
let minX = Infinity,
minY = Infinity,
@@ -163,18 +163,18 @@ export function calculateBoundingBox(
export function calculateBoundingBoxCenter(
gm: GameMap,
borderTiles: ReadonlySet<TileRef>,
borderTiles: ReadonlySet<TileRef>
): Cell {
const { min, max } = calculateBoundingBox(gm, borderTiles);
return new Cell(
min.x + Math.floor((max.x - min.x) / 2),
min.y + Math.floor((max.y - min.y) / 2),
min.y + Math.floor((max.y - min.y) / 2)
);
}
export function inscribed(
outer: { min: Cell; max: Cell },
inner: { min: Cell; max: Cell },
inner: { min: Cell; max: Cell }
): boolean {
return (
outer.min.x <= inner.min.x &&
@@ -208,7 +208,7 @@ export function getMode(list: Set<number>): number {
export function sanitize(name: string): string {
return Array.from(name)
.join("")
.replace(/[^\p{L}\p{N}\s\p{Emoji}\p{Emoji_Component}]/gu, "");
.replace(/[^\p{L}\p{N}\s\p{Emoji}\p{Emoji_Component}\[\]]/gu, "");
}
export function processName(name: string): string {
@@ -238,7 +238,7 @@ export function processName(name: string): string {
// Add CSS for the emoji images
const withEmojiStyles = styledHTML.replace(
/<img/g,
'<img style="height: 1.2em; width: 1.2em; vertical-align: -0.2em; margin: 0 0.05em 0 0.1em;"',
'<img style="height: 1.2em; width: 1.2em; vertical-align: -0.2em; margin: 0 0.05em 0 0.1em;"'
);
// Sanitize the final HTML, allowing styles and specific attributes
@@ -262,7 +262,7 @@ export function CreateGameRecord(
turns: Turn[],
start: number,
end: number,
winner: ClientID | null,
winner: ClientID | null
): GameRecord {
const record: GameRecord = {
id: id,
@@ -289,7 +289,7 @@ export function CreateGameRecord(
}
record.players = players;
record.durationSeconds = Math.floor(
(record.endTimestampMS - record.startTimestampMS) / 1000,
(record.endTimestampMS - record.startTimestampMS) / 1000
);
record.num_turns = turns.length;
record.winner = winner;
@@ -303,7 +303,7 @@ export function assertNever(x: never): never {
export function generateID(): GameID {
const nanoid = customAlphabet(
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
8,
8
);
return nanoid();
}
+1 -1
View File
@@ -375,7 +375,7 @@ export class DefaultConfig implements Config {
difficultyMultiplier = 0.3;
break;
case Difficulty.Medium:
difficultyMultiplier = 0.6;
difficultyMultiplier = 0.5;
break;
case Difficulty.Hard:
difficultyMultiplier = 1;
+2 -2
View File
@@ -146,9 +146,9 @@ export class FakeHumanExecution implements Execution {
private shouldAttack(other: Player): boolean {
if (this.player.isAlliedWith(other)) {
if (this.shouldDiscourageAttack(other)) {
return this.random.chance(100);
return this.random.chance(200);
}
return this.random.chance(20);
return this.random.chance(50);
} else {
if (this.shouldDiscourageAttack(other)) {
return this.random.chance(4);
+1 -1
View File
@@ -68,7 +68,7 @@ export class WarshipExecution implements Execution {
const ships = this.mg
.units(UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip)
.filter(
(u) => this.mg.manhattanDist(u.tile(), this.warship.tile()) < 100
(u) => this.mg.manhattanDist(u.tile(), this.warship.tile()) < 130
)
.filter((u) => u.owner() != this.warship.owner())
.filter((u) => u != this.warship)
+3 -3
View File
@@ -333,10 +333,10 @@ export class GameView implements GameMap {
}
units(...types: UnitType[]): UnitView[] {
if (types.length == 0) {
return Array.from(this._units.values());
return Array.from(this._units.values()).filter((u) => u.isActive());
}
return Array.from(this._units.values()).filter((u) =>
types.includes(u.type())
return Array.from(this._units.values()).filter(
(u) => u.isActive() && types.includes(u.type())
);
}
unit(id: number): UnitView {
+4
View File
@@ -569,6 +569,10 @@ export class PlayerImpl implements Player {
}
switch (unitType) {
case UnitType.MIRV:
if (!this.mg.hasOwner(targetTile)) {
return false;
}
return this.nukeSpawn(targetTile);
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
return this.nukeSpawn(targetTile);
+1 -1
View File
@@ -13,7 +13,7 @@ const matcher = new RegExpMatcher({
export const MIN_USERNAME_LENGTH = 3;
export const MAX_USERNAME_LENGTH = 20;
const validPattern = /^[a-zA-Z0-9_ ]+$/;
const validPattern = /^[a-zA-Z0-9_\[\] ]+$/;
const shadowNames = [
"NicePeopleOnly",
+7 -4
View File
@@ -5,12 +5,15 @@ import { Client } from "./Client";
import { GamePhase, GameServer } from "./GameServer";
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
import { generateID } from "../core/Util";
import { PseudoRandom } from "../core/PseudoRandom";
export class GameManager {
private lastNewLobby: number = 0;
private games: GameServer[] = [];
private random = new PseudoRandom(123);
constructor(private config: ServerConfig) {}
public game(id: GameID): GameServer | null {
@@ -46,7 +49,7 @@ export class GameManager {
gameMap: GameMapType.World,
gameType: GameType.Private,
difficulty: Difficulty.Medium,
}),
})
);
return id;
}
@@ -54,7 +57,7 @@ export class GameManager {
hasActiveGame(gameID: GameID): boolean {
const game = this.games
.filter(
(g) => g.phase() == GamePhase.Lobby || g.phase() == GamePhase.Active,
(g) => g.phase() == GamePhase.Lobby || g.phase() == GamePhase.Active
)
.find((g) => g.id == gameID);
return game != null;
@@ -81,10 +84,10 @@ export class GameManager {
this.lastNewLobby = now;
lobbies.push(
new GameServer(generateID(), now, true, this.config, {
gameMap: GameMapType.World,
gameMap: this.random.randElement(Object.values(GameMapType)),
gameType: GameType.Public,
difficulty: Difficulty.Medium,
}),
})
);
}
+1 -1
View File
@@ -47,7 +47,7 @@ export class GameServer {
public readonly createdAt: number,
public readonly isPublic: boolean,
private config: ServerConfig,
private gameConfig: GameConfig,
public gameConfig: GameConfig,
) {}
public updateGameConfig(gameConfig: GameConfig): void {
+1
View File
@@ -195,6 +195,7 @@ function updateLobbies() {
id: g.id,
msUntilStart: g.startTime() - Date.now(),
numClients: g.numClients(),
gameConfig: g.gameConfig,
}))
.sort((a, b) => a.msUntilStart - b.msUntilStart),
});