mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 03:34:36 +00:00
Perf alloc (#3241)
If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #(issue number) ## Description: ## PR Title perf(core): reduce hot-path allocations & safe optimizations This PR brings in a set of allocation-focused optimizations in core hot paths ### Scope - `src/core/execution/NukeExecution.ts` - `src/core/execution/WarshipExecution.ts` - `src/core/game/UnitGrid.ts` - `src/core/game/PlayerImpl.ts` - `src/core/configuration/DefaultConfig.ts` - `src/core/execution/SAMLauncherExecution.ts` ### What Changed - `NukeExecution.detonate`: reduced call overhead/allocations by caching `mg`/`config`, avoiding repeated lookups, and using allocation-free loops (no `forEach` closures) in the diminishing-effect pass. - `WarshipExecution.findTargetUnit`: replaced allocate+sort flow with single-pass best-target selection. - `UnitGrid.nearbyUnits`: reduced call overhead and allocations via single-type fast path and cached query coordinates. - `PlayerImpl.units`: added fast paths for common small-arity type queries (1-3 unit types). - `DefaultConfig.unitInfo`: cached `UnitInfo` objects per `UnitType` to avoid repeated object/closure creation. - `SAMLauncherExecution` targeting: removed sort churn and streamlined target selection with single-pass hydrogen prioritization. ### Rebase - One conflict was resolved in `NukeExecution.detonate` by keeping `main`'s diminishing-effect-per-impacted-tile behavior, while retaining the allocation-reduction refactors. ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file - [ ] I have added relevant tests to the test directory - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME
This commit is contained in:
@@ -139,6 +139,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
||||
export class DefaultConfig implements Config {
|
||||
private pastelTheme: PastelTheme = new PastelTheme();
|
||||
private pastelThemeDark: PastelThemeDark = new PastelThemeDark();
|
||||
private unitInfoCache = new Map<UnitType, UnitInfo>();
|
||||
constructor(
|
||||
private _serverConfig: ServerConfig,
|
||||
private _gameConfig: GameConfig,
|
||||
@@ -323,30 +324,40 @@ export class DefaultConfig implements Config {
|
||||
}
|
||||
|
||||
unitInfo(type: UnitType): UnitInfo {
|
||||
const cached = this.unitInfoCache.get(type);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let info: UnitInfo;
|
||||
switch (type) {
|
||||
case UnitType.TransportShip:
|
||||
return {
|
||||
info = {
|
||||
cost: () => 0n,
|
||||
};
|
||||
break;
|
||||
case UnitType.Warship:
|
||||
return {
|
||||
info = {
|
||||
cost: this.costWrapper(
|
||||
(numUnits: number) => Math.min(1_000_000, (numUnits + 1) * 250_000),
|
||||
UnitType.Warship,
|
||||
),
|
||||
maxHealth: 1000,
|
||||
};
|
||||
break;
|
||||
case UnitType.Shell:
|
||||
return {
|
||||
info = {
|
||||
cost: () => 0n,
|
||||
damage: 250,
|
||||
};
|
||||
break;
|
||||
case UnitType.SAMMissile:
|
||||
return {
|
||||
info = {
|
||||
cost: () => 0n,
|
||||
};
|
||||
break;
|
||||
case UnitType.Port:
|
||||
return {
|
||||
info = {
|
||||
cost: this.costWrapper(
|
||||
(numUnits: number) =>
|
||||
Math.min(1_000_000, Math.pow(2, numUnits) * 125_000),
|
||||
@@ -356,16 +367,19 @@ export class DefaultConfig implements Config {
|
||||
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
|
||||
upgradable: true,
|
||||
};
|
||||
break;
|
||||
case UnitType.AtomBomb:
|
||||
return {
|
||||
info = {
|
||||
cost: this.costWrapper(() => 750_000, UnitType.AtomBomb),
|
||||
};
|
||||
break;
|
||||
case UnitType.HydrogenBomb:
|
||||
return {
|
||||
info = {
|
||||
cost: this.costWrapper(() => 5_000_000, UnitType.HydrogenBomb),
|
||||
};
|
||||
break;
|
||||
case UnitType.MIRV:
|
||||
return {
|
||||
info = {
|
||||
cost: (game: Game, player: Player) => {
|
||||
if (player.type() === PlayerType.Human && this.infiniteGold()) {
|
||||
return 0n;
|
||||
@@ -373,30 +387,35 @@ export class DefaultConfig implements Config {
|
||||
return 25_000_000n + game.stats().numMirvsLaunched() * 15_000_000n;
|
||||
},
|
||||
};
|
||||
break;
|
||||
case UnitType.MIRVWarhead:
|
||||
return {
|
||||
info = {
|
||||
cost: () => 0n,
|
||||
};
|
||||
break;
|
||||
case UnitType.TradeShip:
|
||||
return {
|
||||
info = {
|
||||
cost: () => 0n,
|
||||
};
|
||||
break;
|
||||
case UnitType.MissileSilo:
|
||||
return {
|
||||
info = {
|
||||
cost: this.costWrapper(() => 1_000_000, UnitType.MissileSilo),
|
||||
constructionDuration: this.instantBuild() ? 0 : 10 * 10,
|
||||
upgradable: true,
|
||||
};
|
||||
break;
|
||||
case UnitType.DefensePost:
|
||||
return {
|
||||
info = {
|
||||
cost: this.costWrapper(
|
||||
(numUnits: number) => Math.min(250_000, (numUnits + 1) * 50_000),
|
||||
UnitType.DefensePost,
|
||||
),
|
||||
constructionDuration: this.instantBuild() ? 0 : 5 * 10,
|
||||
};
|
||||
break;
|
||||
case UnitType.SAMLauncher:
|
||||
return {
|
||||
info = {
|
||||
cost: this.costWrapper(
|
||||
(numUnits: number) =>
|
||||
Math.min(3_000_000, (numUnits + 1) * 1_500_000),
|
||||
@@ -405,8 +424,9 @@ export class DefaultConfig implements Config {
|
||||
constructionDuration: this.instantBuild() ? 0 : 30 * 10,
|
||||
upgradable: true,
|
||||
};
|
||||
break;
|
||||
case UnitType.City:
|
||||
return {
|
||||
info = {
|
||||
cost: this.costWrapper(
|
||||
(numUnits: number) =>
|
||||
Math.min(1_000_000, Math.pow(2, numUnits) * 125_000),
|
||||
@@ -415,8 +435,9 @@ export class DefaultConfig implements Config {
|
||||
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
|
||||
upgradable: true,
|
||||
};
|
||||
break;
|
||||
case UnitType.Factory:
|
||||
return {
|
||||
info = {
|
||||
cost: this.costWrapper(
|
||||
(numUnits: number) =>
|
||||
Math.min(1_000_000, Math.pow(2, numUnits) * 125_000),
|
||||
@@ -426,13 +447,18 @@ export class DefaultConfig implements Config {
|
||||
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
|
||||
upgradable: true,
|
||||
};
|
||||
break;
|
||||
case UnitType.Train:
|
||||
return {
|
||||
info = {
|
||||
cost: () => 0n,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
assertNever(type);
|
||||
}
|
||||
|
||||
this.unitInfoCache.set(type, info);
|
||||
return info;
|
||||
}
|
||||
|
||||
private costWrapper(
|
||||
|
||||
@@ -252,28 +252,31 @@ export class NukeExecution implements Execution {
|
||||
throw new Error("Not initialized");
|
||||
}
|
||||
|
||||
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
|
||||
const mg = this.mg;
|
||||
const config = mg.config();
|
||||
|
||||
const magnitude = config.nukeMagnitudes(this.nuke.type());
|
||||
const toDestroy = this.tilesToDestroy();
|
||||
|
||||
// Retrieve all impacted players and the number of tiles
|
||||
const tilesPerPlayers = new Map<Player, number>();
|
||||
for (const tile of toDestroy) {
|
||||
const owner = this.mg.owner(tile);
|
||||
const owner = mg.owner(tile);
|
||||
if (owner.isPlayer()) {
|
||||
owner.relinquish(tile);
|
||||
const numTiles = tilesPerPlayers.get(owner);
|
||||
tilesPerPlayers.set(owner, numTiles === undefined ? 1 : numTiles + 1);
|
||||
tilesPerPlayers.set(owner, (tilesPerPlayers.get(owner) ?? 0) + 1);
|
||||
}
|
||||
if (this.mg.isLand(tile)) {
|
||||
this.mg.setFallout(tile, true);
|
||||
|
||||
if (mg.isLand(tile)) {
|
||||
mg.setFallout(tile, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Then compute the explosion effect on each player
|
||||
for (const [player, numImpactedTiles] of tilesPerPlayers) {
|
||||
const config = this.mg.config();
|
||||
const tilesBeforeNuke = player.numTilesOwned() + numImpactedTiles;
|
||||
const transportShips = player.units(UnitType.TransportShip);
|
||||
const outgoingAttacks = player.outgoingAttacks();
|
||||
const maxTroops = config.maxTroops(player);
|
||||
// nukeDeathFactor could compute the complete fallout in a single call instead
|
||||
for (let i = 0; i < numImpactedTiles; i++) {
|
||||
@@ -287,39 +290,45 @@ export class NukeExecution implements Execution {
|
||||
maxTroops,
|
||||
),
|
||||
);
|
||||
player.outgoingAttacks().forEach((attack) => {
|
||||
for (const attack of outgoingAttacks) {
|
||||
const attackTroops = attack.troops();
|
||||
const deaths = config.nukeDeathFactor(
|
||||
this.nukeType,
|
||||
attack.troops(),
|
||||
attackTroops,
|
||||
numTilesLeft,
|
||||
maxTroops,
|
||||
);
|
||||
attack.setTroops(attack.troops() - deaths);
|
||||
});
|
||||
transportShips.forEach((unit) => {
|
||||
attack.setTroops(attackTroops - deaths);
|
||||
}
|
||||
for (const unit of transportShips) {
|
||||
const unitTroops = unit.troops();
|
||||
const deaths = config.nukeDeathFactor(
|
||||
this.nukeType,
|
||||
unit.troops(),
|
||||
unitTroops,
|
||||
numTilesLeft,
|
||||
maxTroops,
|
||||
);
|
||||
unit.setTroops(unit.troops() - deaths);
|
||||
});
|
||||
unit.setTroops(unitTroops - deaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const outer2 = magnitude.outer * magnitude.outer;
|
||||
for (const unit of this.mg.units()) {
|
||||
const dst = this.dst;
|
||||
const destroyer = this.player;
|
||||
for (const unit of mg.units()) {
|
||||
const type = unit.type();
|
||||
if (
|
||||
unit.type() !== UnitType.AtomBomb &&
|
||||
unit.type() !== UnitType.HydrogenBomb &&
|
||||
unit.type() !== UnitType.MIRVWarhead &&
|
||||
unit.type() !== UnitType.MIRV &&
|
||||
unit.type() !== UnitType.SAMMissile
|
||||
type === UnitType.AtomBomb ||
|
||||
type === UnitType.HydrogenBomb ||
|
||||
type === UnitType.MIRVWarhead ||
|
||||
type === UnitType.MIRV ||
|
||||
type === UnitType.SAMMissile
|
||||
) {
|
||||
if (this.mg.euclideanDistSquared(this.dst, unit.tile()) < outer2) {
|
||||
unit.delete(true, this.player);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (mg.euclideanDistSquared(dst, unit.tile()) < outer2) {
|
||||
unit.delete(true, destroyer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,31 @@ class SAMTargetingSystem {
|
||||
}
|
||||
|
||||
updateUnreachableNukes(nearbyUnits: { unit: Unit; distSquared: number }[]) {
|
||||
const nearbyUnitSet = new Set(nearbyUnits.map((u) => u.unit.id()));
|
||||
if (this.precomputedNukes.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid per-tick allocations for the common case where only a few nukes are tracked.
|
||||
if (this.precomputedNukes.size <= 16) {
|
||||
for (const nukeId of this.precomputedNukes.keys()) {
|
||||
let found = false;
|
||||
for (const u of nearbyUnits) {
|
||||
if (u.unit.id() === nukeId) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
this.precomputedNukes.delete(nukeId);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const nearbyUnitSet = new Set<number>();
|
||||
for (const u of nearbyUnits) {
|
||||
nearbyUnitSet.add(u.unit.id());
|
||||
}
|
||||
for (const nukeId of this.precomputedNukes.keys()) {
|
||||
if (!nearbyUnitSet.has(nukeId)) {
|
||||
this.precomputedNukes.delete(nukeId);
|
||||
@@ -48,27 +72,27 @@ class SAMTargetingSystem {
|
||||
}
|
||||
}
|
||||
|
||||
private isInRange(tile: TileRef) {
|
||||
const samTile = this.sam.tile();
|
||||
const range = this.mg.config().samRange(this.sam.level());
|
||||
const rangeSquared = range * range;
|
||||
return this.mg.euclideanDistSquared(samTile, tile) <= rangeSquared;
|
||||
}
|
||||
|
||||
private tickToReach(currentTile: TileRef, tile: TileRef): number {
|
||||
return Math.ceil(
|
||||
this.mg.manhattanDist(currentTile, tile) / this.missileSpeed,
|
||||
);
|
||||
}
|
||||
|
||||
private computeInterceptionTile(unit: Unit): InterceptionTile | undefined {
|
||||
private computeInterceptionTile(
|
||||
unit: Unit,
|
||||
samTile: TileRef,
|
||||
rangeSquared: number,
|
||||
): InterceptionTile | undefined {
|
||||
const trajectory = unit.trajectory();
|
||||
const samTile = this.sam.tile();
|
||||
const currentIndex = unit.trajectoryIndex();
|
||||
const explosionTick: number = trajectory.length - currentIndex;
|
||||
for (let i = currentIndex; i < trajectory.length; i++) {
|
||||
const trajectoryTile = trajectory[i];
|
||||
if (trajectoryTile.targetable && this.isInRange(trajectoryTile.tile)) {
|
||||
if (
|
||||
trajectoryTile.targetable &&
|
||||
this.mg.euclideanDistSquared(samTile, trajectoryTile.tile) <=
|
||||
rangeSquared
|
||||
) {
|
||||
const nukeTickToReach = i - currentIndex;
|
||||
const samTickToReach = this.tickToReach(samTile, trajectoryTile.tile);
|
||||
const tickBeforeShooting = nukeTickToReach - samTickToReach;
|
||||
@@ -81,10 +105,14 @@ class SAMTargetingSystem {
|
||||
}
|
||||
|
||||
public getSingleTarget(ticks: number): Target | null {
|
||||
const samTile = this.sam.tile();
|
||||
const range = this.mg.config().samRange(this.sam.level());
|
||||
const rangeSquared = range * range;
|
||||
|
||||
// Look beyond the SAM range so it can preshot nukes
|
||||
const detectionRange = this.mg.config().maxSamRange() * 2;
|
||||
const nukes = this.mg.nearbyUnits(
|
||||
this.sam.tile(),
|
||||
samTile,
|
||||
detectionRange,
|
||||
[UnitType.AtomBomb, UnitType.HydrogenBomb],
|
||||
({ unit }) => {
|
||||
@@ -100,7 +128,7 @@ class SAMTargetingSystem {
|
||||
// Clear unreachable nukes that went out of range
|
||||
this.updateUnreachableNukes(nukes);
|
||||
|
||||
const targets: Array<Target> = [];
|
||||
let best: Target | null = null;
|
||||
for (const nuke of nukes) {
|
||||
const nukeId = nuke.unit.id();
|
||||
const cached = this.precomputedNukes.get(nukeId);
|
||||
@@ -111,7 +139,14 @@ class SAMTargetingSystem {
|
||||
}
|
||||
if (cached.tick === ticks) {
|
||||
// Time to shoot!
|
||||
targets.push({ tile: cached.tile, unit: nuke.unit });
|
||||
const target = { tile: cached.tile, unit: nuke.unit };
|
||||
if (
|
||||
best === null ||
|
||||
(target.unit.type() === UnitType.HydrogenBomb &&
|
||||
best.unit.type() !== UnitType.HydrogenBomb)
|
||||
) {
|
||||
best = target;
|
||||
}
|
||||
this.precomputedNukes.delete(nukeId);
|
||||
continue;
|
||||
}
|
||||
@@ -122,12 +157,23 @@ class SAMTargetingSystem {
|
||||
// Missed the planned tick (e.g was on cooldown), recompute a new interception tile if possible
|
||||
this.precomputedNukes.delete(nukeId);
|
||||
}
|
||||
const interceptionTile = this.computeInterceptionTile(nuke.unit);
|
||||
const interceptionTile = this.computeInterceptionTile(
|
||||
nuke.unit,
|
||||
samTile,
|
||||
rangeSquared,
|
||||
);
|
||||
if (interceptionTile !== undefined) {
|
||||
if (interceptionTile.tick <= 1) {
|
||||
// Shoot instantly
|
||||
|
||||
targets.push({ unit: nuke.unit, tile: interceptionTile.tile });
|
||||
const target = { unit: nuke.unit, tile: interceptionTile.tile };
|
||||
if (
|
||||
best === null ||
|
||||
(target.unit.type() === UnitType.HydrogenBomb &&
|
||||
best.unit.type() !== UnitType.HydrogenBomb)
|
||||
) {
|
||||
best = target;
|
||||
}
|
||||
} else {
|
||||
// Nuke will be reachable but not yet. Store the result.
|
||||
this.precomputedNukes.set(nukeId, {
|
||||
@@ -141,23 +187,7 @@ class SAMTargetingSystem {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
targets.sort((a: Target, b: Target) => {
|
||||
// Prioritize Hydrogen Bombs
|
||||
if (
|
||||
a.unit.type() === UnitType.HydrogenBomb &&
|
||||
b.unit.type() !== UnitType.HydrogenBomb
|
||||
)
|
||||
return -1;
|
||||
if (
|
||||
a.unit.type() !== UnitType.HydrogenBomb &&
|
||||
b.unit.type() === UnitType.HydrogenBomb
|
||||
)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
})[0] ?? null
|
||||
);
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,78 +76,77 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
|
||||
private findTargetUnit(): Unit | undefined {
|
||||
const hasPort = this.warship.owner().unitCount(UnitType.Port) > 0;
|
||||
const patrolRangeSquared = this.mg.config().warshipPatrolRange() ** 2;
|
||||
const mg = this.mg;
|
||||
const config = mg.config();
|
||||
const owner = this.warship.owner();
|
||||
const hasPort = owner.unitCount(UnitType.Port) > 0;
|
||||
const patrolTile = this.warship.patrolTile()!;
|
||||
const patrolRangeSquared = config.warshipPatrolRange() ** 2;
|
||||
|
||||
const ships = this.mg.nearbyUnits(
|
||||
const ships = mg.nearbyUnits(
|
||||
this.warship.tile()!,
|
||||
this.mg.config().warshipTargettingRange(),
|
||||
config.warshipTargettingRange(),
|
||||
[UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip],
|
||||
);
|
||||
const potentialTargets: { unit: Unit; distSquared: number }[] = [];
|
||||
|
||||
let bestUnit: Unit | undefined = undefined;
|
||||
let bestTypePriority = 0;
|
||||
let bestDistSquared = 0;
|
||||
|
||||
for (const { unit, distSquared } of ships) {
|
||||
if (
|
||||
unit.owner() === this.warship.owner() ||
|
||||
unit.owner() === owner ||
|
||||
unit === this.warship ||
|
||||
!this.warship.owner().canAttackPlayer(unit.owner(), true) ||
|
||||
!owner.canAttackPlayer(unit.owner(), true) ||
|
||||
this.alreadySentShell.has(unit)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (unit.type() === UnitType.TradeShip) {
|
||||
|
||||
const type = unit.type();
|
||||
if (type === UnitType.TradeShip) {
|
||||
if (
|
||||
!hasPort ||
|
||||
unit.isSafeFromPirates() ||
|
||||
unit.targetUnit()?.owner() === this.warship.owner() || // trade ship is coming to my port
|
||||
unit.targetUnit()?.owner().isFriendly(this.warship.owner()) // trade ship is coming to my ally
|
||||
unit.targetUnit()?.owner() === owner || // trade ship is coming to my port
|
||||
unit.targetUnit()?.owner().isFriendly(owner) // trade ship is coming to my ally
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
this.mg.euclideanDistSquared(
|
||||
this.warship.patrolTile()!,
|
||||
unit.tile(),
|
||||
) > patrolRangeSquared
|
||||
mg.euclideanDistSquared(patrolTile, unit.tile()) > patrolRangeSquared
|
||||
) {
|
||||
// Prevent warship from chasing trade ship that is too far away from
|
||||
// the patrol tile to prevent warships from wandering around the map.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
potentialTargets.push({ unit: unit, distSquared });
|
||||
|
||||
const typePriority =
|
||||
type === UnitType.TransportShip ? 0 : type === UnitType.Warship ? 1 : 2;
|
||||
|
||||
if (bestUnit === undefined) {
|
||||
bestUnit = unit;
|
||||
bestTypePriority = typePriority;
|
||||
bestDistSquared = distSquared;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match existing `sort()` semantics:
|
||||
// - Lower priority is better (TransportShip < Warship < TradeShip).
|
||||
// - For same type, smaller distance is better.
|
||||
// - For exact ties, keep the first encountered (stable sort behavior).
|
||||
if (
|
||||
typePriority < bestTypePriority ||
|
||||
(typePriority === bestTypePriority && distSquared < bestDistSquared)
|
||||
) {
|
||||
bestUnit = unit;
|
||||
bestTypePriority = typePriority;
|
||||
bestDistSquared = distSquared;
|
||||
}
|
||||
}
|
||||
|
||||
return potentialTargets.sort((a, b) => {
|
||||
const { unit: unitA, distSquared: distA } = a;
|
||||
const { unit: unitB, distSquared: distB } = b;
|
||||
|
||||
// Prioritize Transport Ships above all other units
|
||||
if (
|
||||
unitA.type() === UnitType.TransportShip &&
|
||||
unitB.type() !== UnitType.TransportShip
|
||||
)
|
||||
return -1;
|
||||
if (
|
||||
unitA.type() !== UnitType.TransportShip &&
|
||||
unitB.type() === UnitType.TransportShip
|
||||
)
|
||||
return 1;
|
||||
|
||||
// Then prioritize Warships.
|
||||
if (
|
||||
unitA.type() === UnitType.Warship &&
|
||||
unitB.type() !== UnitType.Warship
|
||||
)
|
||||
return -1;
|
||||
if (
|
||||
unitA.type() !== UnitType.Warship &&
|
||||
unitB.type() === UnitType.Warship
|
||||
)
|
||||
return 1;
|
||||
|
||||
// If both are the same type, sort by distance (lower `distSquared` means closer)
|
||||
return distA - distB;
|
||||
})[0]?.unit;
|
||||
return bestUnit;
|
||||
}
|
||||
|
||||
private shootTarget() {
|
||||
|
||||
@@ -214,11 +214,58 @@ export class PlayerImpl implements Player {
|
||||
}
|
||||
|
||||
units(...types: UnitType[]): Unit[] {
|
||||
if (types.length === 0) {
|
||||
const len = types.length;
|
||||
if (len === 0) {
|
||||
return this._units;
|
||||
}
|
||||
|
||||
// Fast paths for common small arity calls to avoid Set allocation.
|
||||
if (len === 1) {
|
||||
const t0 = types[0]!;
|
||||
const out: Unit[] = [];
|
||||
for (const u of this._units) {
|
||||
if (u.type() === t0) out.push(u);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (len === 2) {
|
||||
const t0 = types[0]!;
|
||||
const t1 = types[1]!;
|
||||
if (t0 === t1) {
|
||||
const out: Unit[] = [];
|
||||
for (const u of this._units) {
|
||||
if (u.type() === t0) out.push(u);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const out: Unit[] = [];
|
||||
for (const u of this._units) {
|
||||
const t = u.type();
|
||||
if (t === t0 || t === t1) out.push(u);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
if (len === 3) {
|
||||
const t0 = types[0]!;
|
||||
const t1 = types[1]!;
|
||||
const t2 = types[2]!;
|
||||
// Keep semantics identical for duplicates in types by using direct comparisons.
|
||||
const out: Unit[] = [];
|
||||
for (const u of this._units) {
|
||||
const t = u.type();
|
||||
if (t === t0 || t === t1 || t === t2) out.push(u);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const ts = new Set(types);
|
||||
return this._units.filter((u) => ts.has(u.type()));
|
||||
const out: Unit[] = [];
|
||||
for (const u of this._units) {
|
||||
if (ts.has(u.type())) out.push(u);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private numUnitsConstructed: Partial<Record<UnitType, number>> = {};
|
||||
|
||||
+50
-16
@@ -140,29 +140,63 @@ export class UnitGrid {
|
||||
includeUnderConstruction: boolean = false,
|
||||
): Array<{ unit: Unit | UnitView; distSquared: number }> {
|
||||
const nearby: Array<{ unit: Unit | UnitView; distSquared: number }> = [];
|
||||
const gm = this.gm;
|
||||
const x = gm.x(tile);
|
||||
const y = gm.y(tile);
|
||||
const { startGridX, endGridX, startGridY, endGridY } = this.getCellsInRange(
|
||||
tile,
|
||||
searchRange,
|
||||
);
|
||||
const rangeSquared = searchRange * searchRange;
|
||||
const typeSet = Array.isArray(types) ? new Set(types) : new Set([types]);
|
||||
|
||||
// `Array.isArray` does not reliably narrow `readonly T[]` in TS, so use a
|
||||
// cheap runtime check that narrows correctly for our string-backed UnitType.
|
||||
if (typeof types !== "string") {
|
||||
for (let cy = startGridY; cy <= endGridY; cy++) {
|
||||
for (let cx = startGridX; cx <= endGridX; cx++) {
|
||||
const cell = this.grid[cy][cx];
|
||||
for (const type of types) {
|
||||
const unitSet = cell.get(type);
|
||||
if (unitSet === undefined) continue;
|
||||
for (const unit of unitSet) {
|
||||
if (!unit.isActive()) continue;
|
||||
// Exclude units under construction by default (e.g., defense posts being built)
|
||||
// But include them for spacing checks
|
||||
if (!includeUnderConstruction && unit.isUnderConstruction())
|
||||
continue;
|
||||
const unitTile = unit.tile();
|
||||
const dx = gm.x(unitTile) - x;
|
||||
const dy = gm.y(unitTile) - y;
|
||||
const distSquared = dx * dx + dy * dy;
|
||||
if (distSquared > rangeSquared) continue;
|
||||
const value = { unit, distSquared };
|
||||
if (predicate !== undefined && !predicate(value)) continue;
|
||||
nearby.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nearby;
|
||||
}
|
||||
|
||||
const type = types;
|
||||
for (let cy = startGridY; cy <= endGridY; cy++) {
|
||||
for (let cx = startGridX; cx <= endGridX; cx++) {
|
||||
for (const type of typeSet) {
|
||||
const unitSet = this.grid[cy][cx].get(type);
|
||||
if (unitSet === undefined) continue;
|
||||
for (const unit of unitSet) {
|
||||
if (!unit.isActive()) continue;
|
||||
// Exclude units under construction by default (e.g., defense posts being built)
|
||||
// But include them for spacing checks
|
||||
if (!includeUnderConstruction && unit.isUnderConstruction())
|
||||
continue;
|
||||
const distSquared = this.squaredDistanceFromTile(unit, tile);
|
||||
if (distSquared > rangeSquared) continue;
|
||||
const value = { unit, distSquared };
|
||||
if (predicate !== undefined && !predicate(value)) continue;
|
||||
nearby.push(value);
|
||||
}
|
||||
const unitSet = this.grid[cy][cx].get(type);
|
||||
if (unitSet === undefined) continue;
|
||||
for (const unit of unitSet) {
|
||||
if (!unit.isActive()) continue;
|
||||
// Exclude units under construction by default (e.g., defense posts being built)
|
||||
// But include them for spacing checks
|
||||
if (!includeUnderConstruction && unit.isUnderConstruction()) continue;
|
||||
const unitTile = unit.tile();
|
||||
const dx = gm.x(unitTile) - x;
|
||||
const dy = gm.y(unitTile) - y;
|
||||
const distSquared = dx * dx + dy * dy;
|
||||
if (distSquared > rangeSquared) continue;
|
||||
const value = { unit, distSquared };
|
||||
if (predicate !== undefined && !predicate(value)) continue;
|
||||
nearby.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user