mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 18:36:39 +00:00
Enable the @typescript-eslint/no-non-null-assertion eslint rule (#1899)
## Description: Enable the `@typescript-eslint/no-non-null-assertion` eslint rule. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced
This commit is contained in:
@@ -79,6 +79,7 @@ export default [
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-inferrable-types": "error",
|
||||
"@typescript-eslint/no-mixed-enums": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/prefer-as-const": "error",
|
||||
@@ -143,6 +144,7 @@ export default [
|
||||
rules: {
|
||||
// Disabled rules for tests, configs
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
|
||||
@@ -452,11 +452,7 @@ export class ClientGameRunner {
|
||||
return;
|
||||
}
|
||||
|
||||
this.findAndUpgradeNearestBuilding(tile);
|
||||
}
|
||||
|
||||
private findAndUpgradeNearestBuilding(clickedTile: TileRef) {
|
||||
this.myPlayer!.actions(clickedTile).then((actions) => {
|
||||
this.myPlayer.actions(tile).then((actions) => {
|
||||
const upgradeUnits: {
|
||||
unitId: number;
|
||||
unitType: UnitType;
|
||||
@@ -470,7 +466,7 @@ export class ClientGameRunner {
|
||||
.find((unit) => unit.id() === bu.canUpgrade);
|
||||
if (existingUnit) {
|
||||
const distance = this.gameView.manhattanDist(
|
||||
clickedTile,
|
||||
tile,
|
||||
existingUnit.tile(),
|
||||
);
|
||||
|
||||
|
||||
@@ -169,7 +169,8 @@ export class TerritoryPatternsModal extends LitElement {
|
||||
text-white text-xs font-medium rounded transition-colors"
|
||||
@click=${(e: Event) => {
|
||||
e.stopPropagation();
|
||||
handlePurchase(pattern.product!.priceId);
|
||||
if (!pattern.product) return;
|
||||
handlePurchase(pattern.product.priceId);
|
||||
}}
|
||||
>
|
||||
${translateText("territory_patterns.purchase")}
|
||||
@@ -362,9 +363,8 @@ export function generatePreviewDataUrl(
|
||||
): string {
|
||||
pattern ??= DEFAULT_PATTERN_B64;
|
||||
|
||||
if (patternCache.has(pattern)) {
|
||||
return patternCache.get(pattern)!;
|
||||
}
|
||||
const cached = patternCache.get(pattern);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
// Calculate canvas size
|
||||
const decoder = new PatternDecoder(pattern, base64url.decode);
|
||||
|
||||
@@ -582,7 +582,7 @@ export class Transport {
|
||||
} else {
|
||||
console.log(
|
||||
"WebSocket is not open. Current state:",
|
||||
this.socket!.readyState,
|
||||
this.socket?.readyState,
|
||||
);
|
||||
console.log("attempting reconnect");
|
||||
}
|
||||
|
||||
@@ -154,7 +154,9 @@ export class AnimatedSpriteLoader {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
canvas.getContext("2d")!.drawImage(img, 0, 0);
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("2D context not supported");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
this.animatedSpriteImageMap.set(typedFxType, canvas);
|
||||
} catch (err) {
|
||||
@@ -192,10 +194,8 @@ export class AnimatedSpriteLoader {
|
||||
const borderColor = theme.borderColor(owner);
|
||||
const spawnHighlightColor = theme.spawnHighlightColor();
|
||||
const key = `${fxType}-${owner.id()}`;
|
||||
let coloredCanvas: HTMLCanvasElement;
|
||||
if (this.coloredAnimatedSpriteCache.has(key)) {
|
||||
coloredCanvas = this.coloredAnimatedSpriteCache.get(key)!;
|
||||
} else {
|
||||
let coloredCanvas = this.coloredAnimatedSpriteCache.get(key);
|
||||
if (coloredCanvas === undefined) {
|
||||
coloredCanvas = colorizeCanvas(
|
||||
baseImage,
|
||||
territoryColor,
|
||||
|
||||
@@ -143,7 +143,9 @@ export function largestRectangleInHistogram(widths: number[]): Rectangle {
|
||||
const h = i === widths.length ? 0 : widths[i];
|
||||
|
||||
while (stack.length > 0 && h < widths[stack[stack.length - 1]]) {
|
||||
const height = widths[stack.pop()!];
|
||||
const lastIndex = stack.pop();
|
||||
if (lastIndex === undefined) break; // cannot happen due to the while guard
|
||||
const height = widths[lastIndex];
|
||||
const width = stack.length === 0 ? i : i - stack[stack.length - 1] - 1;
|
||||
|
||||
if (height * width > maxArea) {
|
||||
|
||||
@@ -120,7 +120,8 @@ export const colorizeCanvas = (
|
||||
canvas.width = source.width;
|
||||
canvas.height = source.height;
|
||||
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("2D context not supported");
|
||||
ctx.drawImage(source, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
@@ -177,9 +178,8 @@ export const getColoredSprite = (
|
||||
const borderColor: Colord = customBorderColor ?? theme.borderColor(owner);
|
||||
const spawnHighlightColor = theme.spawnHighlightColor();
|
||||
const key = computeSpriteKey(unit, territoryColor, borderColor);
|
||||
if (coloredSpriteCache.has(key)) {
|
||||
return coloredSpriteCache.get(key)!;
|
||||
}
|
||||
const cached = coloredSpriteCache.get(key);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const sprite = getSpriteForUnit(unit);
|
||||
if (sprite === null) {
|
||||
|
||||
@@ -189,8 +189,9 @@ export class ChatModal extends LitElement {
|
||||
}
|
||||
|
||||
private selectPhrase(phrase: QuickChatPhrase) {
|
||||
if (this.selectedCategory === null) return;
|
||||
this.selectedQuickChatKey = this.getFullQuickChatKey(
|
||||
this.selectedCategory!,
|
||||
this.selectedCategory,
|
||||
phrase.key,
|
||||
);
|
||||
this.selectedPhraseTemplate = translateText(
|
||||
|
||||
@@ -43,7 +43,10 @@ export class GameLeftSidebar extends LitElement implements Layer {
|
||||
|
||||
tick() {
|
||||
if (!this.playerTeam && this.game.myPlayer()?.team()) {
|
||||
this.playerTeam = this.game.myPlayer()!.team();
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (myPlayer !== null) {
|
||||
this.playerTeam = myPlayer.team();
|
||||
}
|
||||
if (this.playerTeam) {
|
||||
this.playerColor = this.game
|
||||
.config()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type Layer = {
|
||||
init?: () => void;
|
||||
tick?: () => void;
|
||||
tick?: () => void | Promise<void>;
|
||||
renderLayer?: (context: CanvasRenderingContext2D) => void;
|
||||
shouldTransform?: () => boolean;
|
||||
redraw?: () => void;
|
||||
|
||||
@@ -72,7 +72,7 @@ export class MainRadialMenu extends LitElement implements Layer {
|
||||
|
||||
init() {
|
||||
this.radialMenu.init();
|
||||
this.eventBus.on(ContextMenuEvent, (event) => {
|
||||
this.eventBus.on(ContextMenuEvent, async (event) => {
|
||||
const worldCoords = this.transformHandler.screenToWorldCoordinates(
|
||||
event.x,
|
||||
event.y,
|
||||
@@ -80,22 +80,26 @@ export class MainRadialMenu extends LitElement implements Layer {
|
||||
if (!this.game.isValidCoord(worldCoords.x, worldCoords.y)) {
|
||||
return;
|
||||
}
|
||||
if (this.game.myPlayer() === null) {
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (myPlayer === null) {
|
||||
return;
|
||||
}
|
||||
this.clickedTile = this.game.ref(worldCoords.x, worldCoords.y);
|
||||
this.game
|
||||
.myPlayer()!
|
||||
.actions(this.clickedTile)
|
||||
.then((actions) => {
|
||||
this.updatePlayerActions(
|
||||
this.game.myPlayer()!,
|
||||
actions,
|
||||
this.clickedTile!,
|
||||
event.x,
|
||||
event.y,
|
||||
);
|
||||
});
|
||||
const tile = this.game.ref(worldCoords.x, worldCoords.y);
|
||||
this.clickedTile = tile;
|
||||
try {
|
||||
const actions = await myPlayer.actions(tile);
|
||||
// Stale check: user might have clicked somewhere else already
|
||||
if (this.clickedTile !== tile) return;
|
||||
this.updatePlayerActions(
|
||||
myPlayer,
|
||||
actions,
|
||||
tile,
|
||||
event.x,
|
||||
event.y,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch player actions:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,16 +145,21 @@ export class MainRadialMenu extends LitElement implements Layer {
|
||||
async tick() {
|
||||
if (!this.radialMenu.isMenuVisible() || this.clickedTile === null) return;
|
||||
if (this.game.ticks() % 5 === 0) {
|
||||
this.game
|
||||
.myPlayer()!
|
||||
.actions(this.clickedTile)
|
||||
.then((actions) => {
|
||||
this.updatePlayerActions(
|
||||
this.game.myPlayer()!,
|
||||
actions,
|
||||
this.clickedTile!,
|
||||
);
|
||||
});
|
||||
const myPlayer = this.game.myPlayer();
|
||||
if (myPlayer === null) return;
|
||||
const tile = this.clickedTile;
|
||||
if (tile === null) return;
|
||||
try {
|
||||
const actions = await myPlayer.actions(tile);
|
||||
if (this.clickedTile !== tile) return; // stale
|
||||
this.updatePlayerActions(
|
||||
myPlayer,
|
||||
actions,
|
||||
tile,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Failed to refresh player actions:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
|
||||
${ref((el) => {
|
||||
if (el instanceof HTMLElement) {
|
||||
requestAnimationFrame(() => {
|
||||
renderPlayerFlag(player.cosmetics.flag!, el);
|
||||
renderPlayerFlag(player.cosmetics.flag, el);
|
||||
});
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -552,6 +552,7 @@ export class RadialMenu implements Layer {
|
||||
} else {
|
||||
content
|
||||
.append("image")
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
.attr("xlink:href", d.data.icon!)
|
||||
.attr("width", this.config.iconSize)
|
||||
.attr("height", this.config.iconSize)
|
||||
@@ -936,7 +937,11 @@ export class RadialMenu implements Layer {
|
||||
this.currentLevel = 0;
|
||||
this.menuStack = [];
|
||||
|
||||
this.currentMenuItems = this.rootMenu.subMenu!(this.params!);
|
||||
if (this.rootMenu.subMenu === undefined || this.params === null) {
|
||||
this.currentMenuItems = [];
|
||||
} else {
|
||||
this.currentMenuItems = this.rootMenu.subMenu(this.params);
|
||||
}
|
||||
|
||||
this.navigationInProgress = false;
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ export enum Slot {
|
||||
Delete = "delete",
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
const infoChatElement: MenuElement = {
|
||||
id: "info_chat",
|
||||
name: "chat",
|
||||
@@ -292,6 +293,7 @@ const infoEmojiElement: MenuElement = {
|
||||
return emojiElements;
|
||||
},
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
export const infoMenuElement: MenuElement = {
|
||||
id: Slot.Info,
|
||||
|
||||
@@ -46,7 +46,8 @@ export class ReplayPanel extends LitElement implements Layer {
|
||||
|
||||
tick() {
|
||||
if (!this.visible) return;
|
||||
if (this.game!.ticks() % 10 === 0) {
|
||||
if (!this.game) return;
|
||||
if (this.game.ticks() % 10 === 0) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,8 +232,9 @@ export class StructureIconsLayer implements Layer {
|
||||
private modifyVisibility(render: StructureRenderInfo) {
|
||||
const structureType =
|
||||
render.unit.type() === UnitType.Construction
|
||||
? render.unit.constructionType()!
|
||||
? render.unit.constructionType()
|
||||
: render.unit.type();
|
||||
if (structureType === undefined) return;
|
||||
const structureInfos = this.structures.get(structureType);
|
||||
|
||||
let focusStructure = false;
|
||||
@@ -341,14 +342,14 @@ export class StructureIconsLayer implements Layer {
|
||||
);
|
||||
return PIXI.Texture.EMPTY;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const structureType = isConstruction ? constructionType! : unit.type();
|
||||
const cacheKey = isConstruction
|
||||
? `construction-${structureType}` + (renderIcon ? "-icon" : "")
|
||||
: `${this.theme.territoryColor(unit.owner()).toRgbString()}-${structureType}` +
|
||||
(renderIcon ? "-icon" : "");
|
||||
if (this.textureCache.has(cacheKey)) {
|
||||
return this.textureCache.get(cacheKey)!;
|
||||
}
|
||||
const cached = this.textureCache.get(cacheKey);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const shape = STRUCTURE_SHAPES[structureType];
|
||||
const texture = shape
|
||||
@@ -379,7 +380,8 @@ export class StructureIconsLayer implements Layer {
|
||||
}
|
||||
structureCanvas.width = Math.ceil(iconSize);
|
||||
structureCanvas.height = Math.ceil(iconSize);
|
||||
const context = structureCanvas.getContext("2d")!;
|
||||
const context = structureCanvas.getContext("2d");
|
||||
if (!context) throw new Error("2D context not supported");
|
||||
|
||||
let borderColor: string;
|
||||
if (isConstruction) {
|
||||
@@ -561,7 +563,8 @@ export class StructureIconsLayer implements Layer {
|
||||
unit.type() === UnitType.Construction
|
||||
? unit.constructionType()
|
||||
: unit.type();
|
||||
const shape = STRUCTURE_SHAPES[unitType!];
|
||||
const shape =
|
||||
unitType !== undefined ? STRUCTURE_SHAPES[unitType] : undefined;
|
||||
if (shape !== undefined) {
|
||||
text.position.y = Math.round(-ICON_SIZE[shape] / 2 - 2);
|
||||
}
|
||||
@@ -598,7 +601,8 @@ export class StructureIconsLayer implements Layer {
|
||||
const imageCanvas = document.createElement("canvas");
|
||||
imageCanvas.width = image.width;
|
||||
imageCanvas.height = image.height;
|
||||
const ctx = imageCanvas.getContext("2d")!;
|
||||
const ctx = imageCanvas.getContext("2d");
|
||||
if (!ctx) throw new Error("2D context not supported");
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, imageCanvas.width, imageCanvas.height);
|
||||
ctx.globalCompositeOperation = "destination-in";
|
||||
|
||||
@@ -14,7 +14,7 @@ const ANIMATION_DURATIONS: Record<string, number> = {
|
||||
// TODO: Pass in cosmetics as a parameter when
|
||||
// remote cosmetics are implemented for custom flags
|
||||
export function renderPlayerFlag(
|
||||
flag: string,
|
||||
flag: string | undefined,
|
||||
target: HTMLElement,
|
||||
cosmetics: Cosmetics | undefined = undefined,
|
||||
) {
|
||||
@@ -23,7 +23,7 @@ export function renderPlayerFlag(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!flag.startsWith("!")) return;
|
||||
if (!flag?.startsWith("!")) return;
|
||||
|
||||
const code = flag.slice("!".length);
|
||||
const layers = code.split("_").map((segment) => {
|
||||
|
||||
@@ -23,10 +23,11 @@ export class EventBus {
|
||||
eventType: EventConstructor<T>,
|
||||
callback: (event: T) => void,
|
||||
): void {
|
||||
if (!this.listeners.has(eventType)) {
|
||||
this.listeners.set(eventType, []);
|
||||
let callbacks = this.listeners.get(eventType);
|
||||
if (callbacks === undefined) {
|
||||
callbacks = [];
|
||||
this.listeners.set(eventType, callbacks);
|
||||
}
|
||||
const callbacks = this.listeners.get(eventType)!;
|
||||
callbacks.push(callback as (event: GameEvent) => void);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,9 +53,8 @@ export class ColorAllocator {
|
||||
}
|
||||
|
||||
assignColor(id: string): Colord {
|
||||
if (this.assigned.has(id)) {
|
||||
return this.assigned.get(id)!;
|
||||
}
|
||||
const cached = this.assigned.get(id);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
if (this.availableColors.length === 0) {
|
||||
this.availableColors = [...this.fallbackColors];
|
||||
@@ -87,9 +86,8 @@ export class ColorAllocator {
|
||||
}
|
||||
|
||||
assignTeamPlayerColor(team: Team, playerId: string): Colord {
|
||||
if (this.teamPlayerColors.has(playerId)) {
|
||||
return this.teamPlayerColors.get(playerId)!;
|
||||
}
|
||||
const cached = this.teamPlayerColors.get(playerId);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const teamColors = this.getTeamColorVariations(team);
|
||||
const hashValue = simpleHash(playerId);
|
||||
|
||||
@@ -84,9 +84,9 @@ export class PastelTheme implements Theme {
|
||||
}
|
||||
|
||||
borderColor(player: PlayerView): Colord {
|
||||
if (this.borderColorCache.has(player.id())) {
|
||||
return this.borderColorCache.get(player.id())!;
|
||||
}
|
||||
const cached = this.borderColorCache.get(player.id());
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const tc = this.territoryColor(player).rgba;
|
||||
/* eslint-disable sort-keys */
|
||||
const color = colord({
|
||||
|
||||
@@ -81,9 +81,9 @@ export class PastelThemeDark implements Theme {
|
||||
}
|
||||
|
||||
borderColor(player: PlayerView): Colord {
|
||||
if (this.borderColorCache.has(player.id())) {
|
||||
return this.borderColorCache.get(player.id())!;
|
||||
}
|
||||
const cached = this.borderColorCache.get(player.id());
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const tc = this.territoryColor(player).rgba;
|
||||
const color = colord({
|
||||
r: Math.max(tc.r - 40, 0),
|
||||
|
||||
@@ -214,8 +214,9 @@ export class NukeExecution implements Execution {
|
||||
const targetRangeSquared =
|
||||
this.mg.config().defaultNukeTargetableRange() ** 2;
|
||||
const targetTile = this.nuke.targetTile();
|
||||
if (targetTile === undefined) return;
|
||||
this.nuke.setTargetable(
|
||||
this.isTargetable(targetTile!, this.nuke.tile(), targetRangeSquared),
|
||||
this.isTargetable(targetTile, this.nuke.tile(), targetRangeSquared),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,8 @@ export class PortExecution implements Execution {
|
||||
shouldSpawnTradeShip(): boolean {
|
||||
const numTradeShips = this.mg.unitCount(UnitType.TradeShip);
|
||||
const spawnRate = this.mg.config().tradeShipSpawnRate(numTradeShips);
|
||||
for (let i = 0; i < this.port!.level(); i++) {
|
||||
const level = this.port?.level() ?? 0;
|
||||
for (let i = 0; i < level; i++) {
|
||||
if (this.random.chance(spawnRate)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -131,21 +131,22 @@ export class TradeShipExecution implements Execution {
|
||||
}
|
||||
|
||||
private complete() {
|
||||
if (this.tradeShip === undefined) throw new Error("Not initialized");
|
||||
this.active = false;
|
||||
this.tradeShip!.delete(false);
|
||||
this.tradeShip.delete(false);
|
||||
const gold = this.mg
|
||||
.config()
|
||||
.tradeShipGold(
|
||||
this.tilesTraveled,
|
||||
this.tradeShip!.owner().unitCount(UnitType.Port),
|
||||
this.tradeShip.owner().unitCount(UnitType.Port),
|
||||
);
|
||||
|
||||
if (this.wasCaptured) {
|
||||
this.tradeShip!.owner().addGold(gold, this._dstPort.tile());
|
||||
this.tradeShip.owner().addGold(gold, this._dstPort.tile());
|
||||
this.mg.displayMessage(
|
||||
`Received ${renderNumber(gold)} gold from ship captured from ${this.origOwner.displayName()}`,
|
||||
MessageType.CAPTURED_ENEMY_UNIT,
|
||||
this.tradeShip!.owner().id(),
|
||||
this.tradeShip.owner().id(),
|
||||
gold,
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -168,6 +168,7 @@ export class TransportShipExecution implements Execution {
|
||||
this.lastMove = ticks;
|
||||
|
||||
if (this.boat.retreating()) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.dst = this.src!; // src is guaranteed to be set at this point
|
||||
}
|
||||
|
||||
|
||||
@@ -102,9 +102,11 @@ export class WarshipExecution implements Execution {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const patrolTile = this.warship.patrolTile();
|
||||
if (
|
||||
patrolTile !== undefined &&
|
||||
this.mg.euclideanDistSquared(
|
||||
this.warship.patrolTile()!,
|
||||
patrolTile,
|
||||
unit.tile(),
|
||||
) > patrolRangeSquared
|
||||
) {
|
||||
@@ -150,9 +152,11 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
private shootTarget() {
|
||||
const targetUnit = this.warship.targetUnit();
|
||||
if (targetUnit === undefined) return;
|
||||
const shellAttackRate = this.mg.config().warshipShellAttackRate();
|
||||
if (this.mg.ticks() - this.lastShellAttack > shellAttackRate) {
|
||||
if (this.warship.targetUnit()?.type() !== UnitType.TransportShip) {
|
||||
if (targetUnit?.type() !== UnitType.TransportShip) {
|
||||
// Warships don't need to reload when attacking transport ships.
|
||||
this.lastShellAttack = this.mg.ticks();
|
||||
}
|
||||
@@ -161,12 +165,12 @@ export class WarshipExecution implements Execution {
|
||||
this.warship.tile(),
|
||||
this.warship.owner(),
|
||||
this.warship,
|
||||
this.warship.targetUnit()!,
|
||||
targetUnit,
|
||||
),
|
||||
);
|
||||
if (!this.warship.targetUnit()!.hasHealth()) {
|
||||
if (!targetUnit.hasHealth()) {
|
||||
// Don't send multiple shells to target that can be oneshotted
|
||||
this.alreadySentShell.add(this.warship.targetUnit()!);
|
||||
this.alreadySentShell.add(targetUnit);
|
||||
this.warship.setTargetUnit(undefined);
|
||||
return;
|
||||
}
|
||||
@@ -174,16 +178,18 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
private huntDownTradeShip() {
|
||||
const targetUnit = this.warship.targetUnit();
|
||||
if (targetUnit === undefined) return;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
// target is trade ship so capture it.
|
||||
const result = this.pathfinder.nextTile(
|
||||
this.warship.tile(),
|
||||
this.warship.targetUnit()!.tile(),
|
||||
targetUnit.tile(),
|
||||
5,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
this.warship.owner().captureUnit(this.warship.targetUnit()!);
|
||||
this.warship.owner().captureUnit(targetUnit);
|
||||
this.warship.setTargetUnit(undefined);
|
||||
this.warship.move(this.warship.tile());
|
||||
return;
|
||||
@@ -201,16 +207,17 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
private patrol() {
|
||||
if (this.warship.targetTile() === undefined) {
|
||||
this.warship.setTargetTile(this.randomTile());
|
||||
if (this.warship.targetTile() === undefined) {
|
||||
let targetTile = this.warship.targetTile();
|
||||
if (targetTile === undefined) {
|
||||
targetTile = this.randomTile();
|
||||
this.warship.setTargetTile(targetTile);
|
||||
if (targetTile === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = this.pathfinder.nextTile(
|
||||
this.warship.tile(),
|
||||
this.warship.targetTile()!,
|
||||
targetTile,
|
||||
);
|
||||
switch (result.type) {
|
||||
case PathFindResultType.Completed:
|
||||
@@ -243,12 +250,14 @@ export class WarshipExecution implements Execution {
|
||||
const maxAttemptBeforeExpand = 500;
|
||||
let attempts = 0;
|
||||
let expandCount = 0;
|
||||
const patrolTile = this.warship.patrolTile();
|
||||
if (patrolTile === undefined) return;
|
||||
while (expandCount < 3) {
|
||||
const x =
|
||||
this.mg.x(this.warship.patrolTile()!) +
|
||||
this.mg.x(patrolTile) +
|
||||
this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2);
|
||||
const y =
|
||||
this.mg.y(this.warship.patrolTile()!) +
|
||||
this.mg.y(patrolTile) +
|
||||
this.random.nextInt(-warshipPatrolRange / 2, warshipPatrolRange / 2);
|
||||
if (!this.mg.isValidCoord(x, y)) {
|
||||
continue;
|
||||
|
||||
@@ -104,7 +104,8 @@ export class AttackImpl implements Attack {
|
||||
return null;
|
||||
}
|
||||
// No border tiles yet—use the source tile's location
|
||||
const tile: number = this.sourceTile()!;
|
||||
const tile = this.sourceTile();
|
||||
if (tile === null) return null;
|
||||
return new Cell(this._mg.map().x(tile), this._mg.map().y(tile));
|
||||
}
|
||||
|
||||
|
||||
@@ -651,8 +651,9 @@ export class GameImpl implements Game {
|
||||
"team",
|
||||
winner,
|
||||
...this.players()
|
||||
.filter((p) => p.team() === winner && p.clientID() !== null)
|
||||
.map((p) => p.clientID()!),
|
||||
.filter((p) => p.team() === winner)
|
||||
.map((p) => p.clientID())
|
||||
.filter((id): id is ClientID => id !== null),
|
||||
];
|
||||
} else {
|
||||
const clientId = winner.clientID();
|
||||
|
||||
@@ -220,8 +220,9 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
{ station: start, distance: 0 },
|
||||
];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { station, distance } = queue.shift()!;
|
||||
let head = 0;
|
||||
while (head < queue.length) {
|
||||
const { station, distance } = queue[head++];
|
||||
if (visited.has(station)) continue;
|
||||
visited.add(station);
|
||||
|
||||
@@ -244,8 +245,9 @@ export class RailNetworkImpl implements RailNetwork {
|
||||
const visited = new Set<TrainStation>();
|
||||
const queue = [start];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
let head = 0;
|
||||
while (head < queue.length) {
|
||||
const current = queue[head++];
|
||||
if (visited.has(current)) continue;
|
||||
visited.add(current);
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@ export function assignTeams(
|
||||
// Sort players into clan groups or no-clan list
|
||||
for (const player of players) {
|
||||
if (player.clan) {
|
||||
if (!clanGroups.has(player.clan)) {
|
||||
clanGroups.set(player.clan, []);
|
||||
let group = clanGroups.get(player.clan);
|
||||
if (group === undefined) {
|
||||
group = [];
|
||||
clanGroups.set(player.clan, group);
|
||||
}
|
||||
clanGroups.get(player.clan)!.push(player);
|
||||
group.push(player);
|
||||
} else {
|
||||
noClanPlayers.push(player);
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
}
|
||||
|
||||
// Process forward search
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const fwdCurrent = this.fwdOpenSet.poll()!.tile;
|
||||
|
||||
// Check if we've found a meeting point
|
||||
@@ -98,6 +99,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
this.expandNode(fwdCurrent, true);
|
||||
|
||||
// Process backward search
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const bwdCurrent = this.bwdOpenSet.poll()!.tile;
|
||||
|
||||
// Check if we've found a meeting point
|
||||
@@ -126,7 +128,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
const openSet = isForward ? this.fwdOpenSet : this.bwdOpenSet;
|
||||
const cameFrom = isForward ? this.fwdCameFrom : this.bwdCameFrom;
|
||||
|
||||
const tentativeGScore = gScore.get(current)! + this.graph.cost(neighbor);
|
||||
const tentativeGScore = (gScore.get(current) ?? 0) + this.graph.cost(neighbor);
|
||||
let penalty = 0;
|
||||
// With a direction change penalty, the path will get as straight as possible
|
||||
if (this.directionChangePenalty > 0) {
|
||||
@@ -141,7 +143,8 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
}
|
||||
|
||||
const totalG = tentativeGScore + penalty;
|
||||
if (!gScore.has(neighbor) || totalG < gScore.get(neighbor)!) {
|
||||
const g = gScore.get(neighbor);
|
||||
if (g === undefined || totalG < g) {
|
||||
cameFrom.set(neighbor, current);
|
||||
gScore.set(neighbor, totalG);
|
||||
const fScore =
|
||||
@@ -172,19 +175,23 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
|
||||
|
||||
// Reconstruct path from start to meeting point
|
||||
const fwdPath: NodeType[] = [this.meetingPoint];
|
||||
let current = this.meetingPoint;
|
||||
let current: NodeType = this.meetingPoint;
|
||||
|
||||
while (this.fwdCameFrom.has(current)) {
|
||||
current = this.fwdCameFrom.get(current)!;
|
||||
let f = this.fwdCameFrom.get(current);
|
||||
while (f !== undefined) {
|
||||
current = f;
|
||||
fwdPath.unshift(current);
|
||||
f = this.fwdCameFrom.get(current);
|
||||
}
|
||||
|
||||
// Reconstruct path from meeting point to goal
|
||||
current = this.meetingPoint;
|
||||
|
||||
while (this.bwdCameFrom.has(current)) {
|
||||
current = this.bwdCameFrom.get(current)!;
|
||||
let b = this.bwdCameFrom.get(current);
|
||||
while (b !== undefined) {
|
||||
current = b;
|
||||
fwdPath.push(current);
|
||||
b = this.bwdCameFrom.get(current);
|
||||
}
|
||||
|
||||
return fwdPath;
|
||||
|
||||
@@ -44,13 +44,17 @@ export class WorkerClient {
|
||||
break;
|
||||
|
||||
case "initialized":
|
||||
default:
|
||||
if (message.id && this.messageHandlers.has(message.id)) {
|
||||
const handler = this.messageHandlers.get(message.id)!;
|
||||
default: {
|
||||
if (message.id === undefined) return;
|
||||
const handler = this.messageHandlers.get(message.id);
|
||||
if (handler === undefined) return;
|
||||
try {
|
||||
handler(message);
|
||||
} finally {
|
||||
this.messageHandlers.delete(message.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -662,8 +662,8 @@ export class GameServer {
|
||||
|
||||
// Count occurrences of each hash
|
||||
for (const client of this.activeClients) {
|
||||
if (client.hashes.has(turnNumber)) {
|
||||
const clientHash = client.hashes.get(turnNumber)!;
|
||||
const clientHash = client.hashes.get(turnNumber);
|
||||
if (clientHash !== undefined) {
|
||||
counts.set(clientHash, (counts.get(clientHash) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
@@ -683,8 +683,8 @@ export class GameServer {
|
||||
let outOfSyncClients: Client[] = [];
|
||||
|
||||
for (const client of this.activeClients) {
|
||||
if (client.hashes.has(turnNumber)) {
|
||||
const clientHash = client.hashes.get(turnNumber)!;
|
||||
const clientHash = client.hashes.get(turnNumber);
|
||||
if (clientHash !== undefined) {
|
||||
if (clientHash !== mostCommonHash) {
|
||||
outOfSyncClients.push(client);
|
||||
}
|
||||
|
||||
@@ -103,14 +103,20 @@ export class MapPlaylist {
|
||||
const numAttempts = 10000;
|
||||
for (let i = 0; i < numAttempts; i++) {
|
||||
if (this.shuffleMapsPlaylist()) {
|
||||
log.info(`Generated map playlist in ${i} attempts`);
|
||||
return this.mapsPlaylist.shift()!;
|
||||
log.info(`Generated map playlist in ${i + 1} attempts`);
|
||||
const next = this.mapsPlaylist.shift();
|
||||
if (next !== undefined) return next;
|
||||
log.error("Playlist unexpectedly empty after successful shuffle; using fallback.");
|
||||
return { map: GameMapType.World, mode: GameMode.FFA };
|
||||
}
|
||||
}
|
||||
log.error("Failed to generate a valid map playlist");
|
||||
}
|
||||
// Even if it failed, playlist will be partially populated.
|
||||
return this.mapsPlaylist.shift()!;
|
||||
// Even if it failed, playlist may be partially populated.
|
||||
const fallback = this.mapsPlaylist.shift();
|
||||
if (fallback !== undefined) return fallback;
|
||||
log.error("Playlist empty after shuffle failure; using fallback.");
|
||||
return { map: GameMapType.World, mode: GameMode.FFA };
|
||||
}
|
||||
|
||||
private shuffleMapsPlaylist(): boolean {
|
||||
|
||||
@@ -131,10 +131,11 @@ function handleWinner(
|
||||
|
||||
// Add client vote
|
||||
const winnerKey = JSON.stringify(clientMsg.winner);
|
||||
if (!gs.winnerVotes.has(winnerKey)) {
|
||||
gs.winnerVotes.set(winnerKey, { ips: new Set(), winner: clientMsg });
|
||||
let potentialWinner = gs.winnerVotes.get(winnerKey);
|
||||
if (potentialWinner === undefined) {
|
||||
potentialWinner = { ips: new Set(), winner: clientMsg };
|
||||
gs.winnerVotes.set(winnerKey, potentialWinner);
|
||||
}
|
||||
const potentialWinner = gs.winnerVotes.get(winnerKey)!;
|
||||
potentialWinner.ips.add(client.ip);
|
||||
|
||||
const activeUniqueIPs = new Set(gs.activeClients.map((c) => c.ip));
|
||||
|
||||
Reference in New Issue
Block a user