mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-26 10:34:36 +00:00
Merge branch 'main' into features/footer
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user