Merge main into biome

This commit is contained in:
Scott Anderson
2025-08-24 21:35:37 -04:00
19 changed files with 183 additions and 45 deletions
+1 -4
View File
@@ -99,10 +99,7 @@ export default [
"function-call-argument-newline": ["error", "consistent"],
"max-depth": ["error", { max: 5 }],
"max-len": ["error", { code: 120 }],
"max-lines": [
"error",
{ max: 676, skipBlankLines: true, skipComments: true },
],
"max-lines": ["error", { max: 677, skipBlankLines: true, skipComments: true }],
"max-lines-per-function": ["error", { max: 561 }],
"no-loss-of-precision": "error",
"no-multi-spaces": "error",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 B

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 B

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 B

After

Width:  |  Height:  |  Size: 199 B

+14 -1
View File
@@ -66,6 +66,10 @@ export class TerritoryPatternsModal extends LitElement {
}
async onUserMe(userMeResponse: UserMeResponse | null) {
if (userMeResponse === null) {
this.userSettings.setSelectedPattern(undefined);
this.selectedPattern = undefined;
}
this.patterns = await patterns(userMeResponse);
this.me = userMeResponse;
this.requestUpdate();
@@ -143,7 +147,7 @@ export class TerritoryPatternsModal extends LitElement {
@mouseleave=${() => this.handleMouseLeave()}
>
<div class="text-sm font-bold mb-1">
${translateText(`territory_patterns.pattern.${pattern.name}`)}
${translatePatternName("territory_patterns.pattern", pattern.name)}
</div>
<div
class="preview-container"
@@ -419,3 +423,12 @@ export function generatePreviewDataUrl(
patternCache.set(pattern, dataUrl);
return dataUrl;
}
function translatePatternName(prefix: string, patternName: string): string {
const translation = translateText(`${prefix}.${patternName}`);
if (translation.startsWith(prefix)) {
// Translation was not found, fallback to pattern name
return patternName[0].toUpperCase() + patternName.substring(1);
}
return translation;
}
+9 -12
View File
@@ -70,6 +70,7 @@ type GameEvent = {
duration?: Tick;
focusID?: number;
unitView?: UnitView;
shouldDelete?: (game: GameView) => boolean;
};
@customElement("events-display")
@@ -205,7 +206,8 @@ export class EventsDisplay extends LitElement implements Layer {
let remainingEvents = this.events.filter((event) => {
if (this.game === undefined) return;
const shouldKeep =
this.game.ticks() - event.createdAt < (event.duration ?? 600);
this.game.ticks() - event.createdAt < (event.duration ?? 600) &&
!event.shouldDelete?.(this.game);
if (!shouldKeep && event.onDelete) {
event.onDelete();
}
@@ -468,18 +470,13 @@ export class EventsDisplay extends LitElement implements Layer {
},
],
createdAt: this.game.ticks(),
description: translateText("events_display.request_alliance", {
name: requestor.name(),
}),
duration: 150,
focusID: update.requestorID,
highlight: true,
onDelete: () =>
this.eventBus?.emit(
new SendAllianceReplyIntentEvent(requestor, recipient, false),
),
priority: 0,
type: MessageType.ALLIANCE_REQUEST,
duration: this.game.config().allianceRequestDuration() - 20, // 2 second buffer
shouldDelete: (game) => {
// Recipient sent a separate request, so they became allied without the recipient responding.
return requestor.isAlliedWith(recipient);
},
focusID: update.requestorID,
});
}
+1 -1
View File
@@ -49,7 +49,7 @@ export class ColorAllocator {
case ColoredTeams.Bot:
return botTeamColors;
default:
throw new Error(`Unknown team color: ${team}`);
return [this.assignColor(team)];
}
}
+1
View File
@@ -121,6 +121,7 @@ export type Config = {
shellLifetime(): number;
boatMaxNumber(): number;
allianceDuration(): Tick;
allianceRequestDuration(): Tick;
allianceRequestCooldown(): Tick;
temporaryEmbargoDuration(): Tick;
targetDuration(): Tick;
+3
View File
@@ -532,6 +532,9 @@ export class DefaultConfig implements Config {
targetCooldown(): Tick {
return 15 * 10;
}
allianceRequestDuration(): Tick {
return 20 * 10;
}
allianceRequestCooldown(): Tick {
return 30 * 10;
}
-1
View File
@@ -52,7 +52,6 @@ export class CityExecution implements Execution {
this.city.tile(),
this.mg.config().trainStationMaxRange(),
UnitType.Factory,
this.player.id(),
);
if (nearbyFactory) {
this.mg.addExecution(new TrainStationExecution(this.city));
-1
View File
@@ -97,7 +97,6 @@ export class PortExecution implements Execution {
this.port.tile(),
this.mg.config().trainStationMaxRange(),
UnitType.Factory,
this.player.id(),
);
if (nearbyFactory) {
this.mg.addExecution(new TrainStationExecution(this.port));
@@ -1,8 +1,15 @@
import { Execution, Game, Player, PlayerID } from "../../game/Game";
import {
AllianceRequest,
Execution,
Game,
Player,
PlayerID,
} from "../../game/Game";
export class AllianceRequestExecution implements Execution {
private req: AllianceRequest | null = null;
private active = true;
private recipient: Player | null = null;
private mg: Game | undefined;
constructor(
private readonly requestor: Player,
@@ -10,29 +17,50 @@ export class AllianceRequestExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
this.mg = mg;
if (!mg.hasPlayer(this.recipientID)) {
console.warn(
`AllianceRequestExecution recipient ${this.recipientID} not found`,
);
this.active = false;
return;
}
this.recipient = mg.player(this.recipientID);
const recipient = mg.player(this.recipientID);
if (!this.requestor.canSendAllianceRequest(recipient)) {
console.warn("cannot send alliance request");
this.active = false;
} else {
const incoming = recipient
.outgoingAllianceRequests()
.find((r) => r.recipient() === this.requestor);
if (incoming) {
// If the recipient already has pending alliance request,
// then accept it instead of creating a new one.
this.active = false;
incoming.accept();
} else {
this.req = this.requestor.createAllianceRequest(recipient);
}
}
}
tick(ticks: number): void {
if (this.recipient === null) {
throw new Error("Not initialized");
if (
this.req?.status() === "accepted" ||
this.req?.status() === "rejected"
) {
this.active = false;
return;
}
if (this.requestor.isFriendly(this.recipient)) {
console.warn("already allied");
} else if (!this.requestor.canSendAllianceRequest(this.recipient)) {
console.warn("recent or pending alliance request");
} else {
this.requestor.createAllianceRequest(this.recipient);
if (this.mg === undefined) throw new Error("Not initialized");
if (
this.mg.ticks() - (this.req?.createdAt() ?? 0) >
this.mg.config().allianceRequestDuration()
) {
this.req?.reject();
this.active = false;
}
this.active = false;
}
isActive(): boolean {
+8
View File
@@ -3,6 +3,8 @@ import { GameImpl } from "./GameImpl";
import { AllianceRequestUpdate, GameUpdateType } from "./GameUpdates";
export class AllianceRequestImpl implements AllianceRequest {
private status_: "pending" | "accepted" | "rejected" = "pending";
constructor(
private readonly requestor_: Player,
private readonly recipient_: Player,
@@ -10,6 +12,10 @@ export class AllianceRequestImpl implements AllianceRequest {
private readonly game: GameImpl,
) {}
status(): "pending" | "accepted" | "rejected" {
return this.status_;
}
requestor(): Player {
return this.requestor_;
}
@@ -23,9 +29,11 @@ export class AllianceRequestImpl implements AllianceRequest {
}
accept(): void {
this.status_ = "accepted";
this.game.acceptAllianceRequest(this);
}
reject(): void {
this.status_ = "rejected";
this.game.rejectAllianceRequest(this);
}
+2 -1
View File
@@ -352,6 +352,7 @@ export type AllianceRequest = {
requestor(): Player;
recipient(): Player;
createdAt(): Tick;
status(): "pending" | "accepted" | "rejected";
};
export type Alliance = {
@@ -670,7 +671,7 @@ export type Game = {
tile: TileRef,
searchRange: number,
type: UnitType,
playerId: PlayerID,
playerId?: PlayerID,
): boolean;
nearbyUnits(
tile: TileRef,
+1 -1
View File
@@ -753,7 +753,7 @@ export class GameImpl implements Game {
tile: TileRef,
searchRange: number,
type: UnitType,
playerId: PlayerID,
playerId?: PlayerID,
) {
return this.unitGrid.hasUnitNearby(tile, searchRange, type, playerId);
}
+6 -3
View File
@@ -396,9 +396,9 @@ export class PlayerImpl implements Player {
return false;
}
const hasPending =
this.incomingAllianceRequests().some((ar) => ar.requestor() === other) ||
this.outgoingAllianceRequests().some((ar) => ar.recipient() === other);
const hasPending = this.outgoingAllianceRequests().some(
(ar) => ar.recipient() === other,
);
if (hasPending) {
return false;
@@ -555,6 +555,9 @@ export class PlayerImpl implements Player {
}
canSendEmoji(recipient: Player | typeof AllPlayers): boolean {
if (recipient === this) {
return false;
}
const recipientID =
recipient === AllPlayers ? AllPlayers : recipient.smallID();
const prevMsgs = this.outgoingEmojis_.filter(
+19 -6
View File
@@ -170,12 +170,28 @@ export class UnitGrid {
return nearby;
}
private unitIsInRange(
unit: Unit | UnitView,
tile: TileRef,
rangeSquared: number,
playerId?: PlayerID,
): boolean {
if (!unit.isActive()) {
return false;
}
if (playerId !== undefined && unit.owner().id() !== playerId) {
return false;
}
const distSquared = this.squaredDistanceFromTile(unit, tile);
return distSquared <= rangeSquared;
}
// Return true if it finds an owned specific unit in range
hasUnitNearby(
tile: TileRef,
searchRange: number,
type: UnitType,
playerId: PlayerID,
playerId?: PlayerID,
): boolean {
const { startGridX, endGridX, startGridY, endGridY } = this.getCellsInRange(
tile,
@@ -187,11 +203,8 @@ export class UnitGrid {
const unitSet = this.grid[cy][cx].get(type);
if (unitSet === undefined) continue;
for (const unit of unitSet) {
if (unit.owner().id() === playerId && unit.isActive()) {
const distSquared = this.squaredDistanceFromTile(unit, tile);
if (distSquared <= rangeSquared) {
return true;
}
if (this.unitIsInRange(unit, tile, rangeSquared, playerId)) {
return true;
}
}
}
-1
View File
@@ -46,7 +46,6 @@ const frequency: Partial<Record<GameMapName, number>> = {
SouthAmerica: 5,
StraitOfGibraltar: 5,
World: 8,
Yenisei: 6,
};
type MapWithMode = {
+77
View File
@@ -0,0 +1,77 @@
import { Game, Player, PlayerType } from "../src/core/game/Game";
import { playerInfo, setup } from "./util/Setup";
import { AllianceRequestExecution } from "../src/core/execution/alliance/AllianceRequestExecution";
import { AllianceRequestReplyExecution } from "../src/core/execution/alliance/AllianceRequestReplyExecution";
let game: Game;
let player1: Player;
let player2: Player;
describe("AllianceRequestExecution", () => {
beforeEach(async () => {
game = await setup(
"plains",
{
infiniteGold: true,
instantBuild: true,
infiniteTroops: true,
},
[
playerInfo("player1", PlayerType.Human),
playerInfo("player2", PlayerType.Human),
playerInfo("player3", PlayerType.FakeHuman),
],
);
player1 = game.player("player1");
player1.conquer(game.ref(0, 0));
player2 = game.player("player2");
player2.conquer(game.ref(0, 1));
while (game.inSpawnPhase()) {
game.executeNextTick();
}
});
test("Can create alliance by replying", () => {
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
game.executeNextTick();
game.addExecution(
new AllianceRequestReplyExecution(player1.id(), player2, true),
);
game.executeNextTick();
game.executeNextTick();
expect(player1.isAlliedWith(player2)).toBeTruthy();
expect(player2.isAlliedWith(player1)).toBeTruthy();
});
test("Can create alliance by sending alliance request back", () => {
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
game.executeNextTick();
game.addExecution(new AllianceRequestExecution(player2, player1.id()));
game.executeNextTick();
expect(player1.isAlliedWith(player2)).toBeTruthy();
expect(player2.isAlliedWith(player1)).toBeTruthy();
});
test("Alliance request expires", () => {
game.config().allianceRequestDuration = () => 5;
game.addExecution(new AllianceRequestExecution(player1, player2.id()));
game.executeNextTick();
expect(player1.outgoingAllianceRequests()).toHaveLength(1);
for (let i = 0; i < 6; i++) {
game.executeNextTick();
}
expect(player1.outgoingAllianceRequests()).toHaveLength(0);
expect(player1.isAlliedWith(player2)).toBeFalsy();
expect(player2.isAlliedWith(player1)).toBeFalsy();
});
});