diff --git a/athena/MapData.tsx b/athena/MapData.tsx index b48749b7..3541a522 100644 --- a/athena/MapData.tsx +++ b/athena/MapData.tsx @@ -186,6 +186,7 @@ export default class MapData { private _hasNeutralUnits: boolean | null = null; private _activeUnitTypes: ReadonlyMap | null = null; + protected unitAccessibilityCache: Map; constructor( public readonly map: TileMap, @@ -213,6 +214,11 @@ export default class MapData { }), ); this.playerToTeam.set(0, -1); + this.unitAccessibilityCache = new Map(); + } + + getCache() { + return this.unitAccessibilityCache; } contains(size: { x: number; y: number }) { @@ -278,19 +284,18 @@ export default class MapData { : null; } - getTileInfo(vector: Vector, layer?: TileLayer) { + getTileInfo(vector: Vector, layer?: TileLayer, index?: number) { if (!this.contains(vector)) { throw new Error( `getTileInfo: Vector '${vector.x},${vector.y}' is not within the map limits of width '${this.size.width}' and height '${this.size.height}'.`, ); } - - return getTileInfo(this.map[this.getTileIndex(vector)], layer); + return getTileInfo(this.map[index ?? this.getTileIndex(vector)], layer); } - maybeGetTileInfo(vector: Vector, layer?: TileLayer) { + maybeGetTileInfo(vector: Vector, layer?: TileLayer, index?: number) { if (this.contains(vector)) { - return getTileInfo(this.map[this.getTileIndex(vector)], layer); + return getTileInfo(this.map[index ?? this.getTileIndex(vector)], layer); } } @@ -413,7 +418,8 @@ export default class MapData { value: T, ): T { const { map, size } = this; - for (let i = 0; i < map.length; i++) { + const length = map.length; + for (let i = 0; i < length; i++) { value = fn.call(this, value, indexToVector(i, size.width), i); } return value; @@ -447,7 +453,8 @@ export default class MapData { value: T, ): T { const { map, modifiers, size } = this; - for (let i = 0; i < map.length; i++) { + const length = map.length; + for (let i = 0; i < length; i++) { const field = map[i]; if (typeof field === 'number') { value = fn.call( @@ -499,7 +506,8 @@ export default class MapData { value: T, ): T { const { decorators, size } = this; - for (let i = 0; i < decorators.length; i++) { + const length = decorators.length; + for (let i = 0; i < length; i++) { const decorator = getDecorator(decorators[i]); if (decorator) { value = fn.call( diff --git a/athena/Radius.tsx b/athena/Radius.tsx index 38b08ed5..142fb1b4 100644 --- a/athena/Radius.tsx +++ b/athena/Radius.tsx @@ -11,7 +11,7 @@ import Vector from './map/Vector.tsx'; import MapData from './MapData.tsx'; type RadiusConfiguration = { - getCost(map: MapData, unit: Unit, vector: Vector): number; + getCost(map: MapData, unit: Unit, vector: Vector, index?: number): number; getResourceValue(unit: Unit): number; getTransitionCost( info: UnitInfo, @@ -37,38 +37,56 @@ export const RadiusItem = ( vector, }); -function isAccessibleBase(map: MapData, unit: Unit, vector: Vector) { - if (!map.contains(vector)) { - return false; +let currentUnitId: number; + +function getCostBase(map: MapData, unit: Unit, vector: Vector, index?: number) { + const tileInfo = map.maybeGetTileInfo(vector, undefined, index); + return tileInfo ? tileInfo.getMovementCost(unit.info) : -1; +} + +function getTransitionCostBase( + info: UnitInfo, + current: TileInfo, + parent: TileInfo, +) { + if (parent.group !== current.group) { + return parent.getTransitionCost(info) + current.getTransitionCost(info); } + return 0; +} +function isAccessibleBase(map: MapData, unit: Unit, vector: Vector) { const unitB = map.units.get(vector); if (unitB && map.isOpponent(unitB, unit)) { return false; } - const building = map.buildings.get(vector); - if (building && !building.info.isAccessibleBy(unit.info)) { - return false; - } + return !(building && !building.info.isAccessibleBy(unit.info)); +} - return true; +function isAccessible(map: MapData, unit: Unit, vector: Vector) { + const cache = map.getCache(); + const key = vector.hashCode(); + let accessible = cache.get(key); + if (accessible !== undefined && currentUnitId === unit.id) { + return accessible; + } + currentUnitId = unit.id; + accessible = isAccessibleBase(map, unit, vector); + cache.set(key, accessible); + return accessible; } export const MoveConfiguration = { - getCost: (map: MapData, unit: Unit, vector: Vector) => - map.maybeGetTileInfo(vector)?.getMovementCost(unit.info) || -1, + getCost: getCostBase, getResourceValue: (unit: Unit) => unit.fuel, - getTransitionCost: (info: UnitInfo, current: TileInfo, parent: TileInfo) => - (current.group !== parent.group && - parent.getTransitionCost(info) + current.getTransitionCost(info)) || - 0, - isAccessible: isAccessibleBase, + getTransitionCost: getTransitionCostBase, + isAccessible, } as const; const VisionConfiguration = { - getCost: (map: MapData, unit: Unit, vector: Vector) => - map.maybeGetTileInfo(vector)?.configuration.vision || -1, + getCost: (map: MapData, unit: Unit, vector: Vector, index?: number) => + map.maybeGetTileInfo(vector, undefined, index)?.configuration.vision || -1, getResourceValue: () => Number.POSITIVE_INFINITY, getTransitionCost: () => 0, isAccessible: (map: MapData, unit: Unit, vector: Vector) => @@ -81,29 +99,29 @@ function calculateRadius( start: Vector, radius: number, { - getCost, getResourceValue, getTransitionCost, isAccessible, }: RadiusConfiguration = MoveConfiguration, ): Map { - const { info } = unit; - const closed = new Array(map.size.width * map.size.height); + const closed: { [key: number]: 1 } = {}; const paths = new Map(); const queue = new FastPriorityQueue((a, b) => a.cost < b.cost); + const minRadius = Math.min(radius, getResourceValue(unit)); queue.add(RadiusItem(start)); - while (!queue.isEmpty()) { + let index: number = map.getTileIndex(start); + do { const { cost: parentCost, vector } = queue.poll()!; - const index = map.getTileIndex(vector); + index = map.getTileIndex(vector); if (closed[index]) { continue; } - closed[index] = true; + closed[index] = 1; const vectors = vector.adjacent(); - for (let i = 0; i < vectors.length; i++) { - const currentVector = vectors[i]; + const parentTileInfo = map.getTileInfo(vector, undefined, index); + for (const currentVector of vectors) { if (!map.contains(currentVector)) { continue; } @@ -111,37 +129,33 @@ function calculateRadius( if (closed[currentIndex]) { continue; } - const cost = getCost(map, unit, currentVector); + const currentTileInfo = map.getTileInfo( + currentVector, + undefined, + currentIndex, + ); + const cost = currentTileInfo.getMovementCost(unit.info); if (cost < 0 || !isAccessible(map, unit, currentVector)) { - closed[currentIndex] = true; + closed[currentIndex] = 1; continue; } const nextCost = parentCost + cost + - getTransitionCost( - info, - map.getTileInfo(vector), - map.getTileInfo(currentVector), - ); + getTransitionCost(unit.info, parentTileInfo, currentTileInfo); + if (nextCost > minRadius) { + continue; + } const previousPath = paths.get(currentVector); - if ( - nextCost <= radius && - (!previousPath || nextCost < previousPath.cost) && - nextCost <= getResourceValue(unit) - ) { - const item = { - cost: nextCost, - parent: vector, - vector: currentVector, - }; + if (!previousPath || nextCost < previousPath.cost) { + const item = RadiusItem(currentVector, nextCost, vector); paths.set(currentVector, item); if (nextCost < radius) { queue.add(item); } } } - } + } while (!queue.isEmpty()); return paths; } @@ -177,6 +191,7 @@ export function getPathCost( const seen = new Set([start]); let previousVector = start; let totalCost = 0; + const previousTileInfo = map.getTileInfo(previousVector); for (const vector of path) { if (seen.has(vector) || !map.contains(vector)) { @@ -195,11 +210,7 @@ export function getPathCost( totalCost += cost + - getTransitionCost( - info, - map.getTileInfo(vector), - map.getTileInfo(previousVector), - ); + getTransitionCost(info, map.getTileInfo(vector), previousTileInfo); if (totalCost > radius || totalCost > getResourceValue(unit)) { return -1; @@ -212,21 +223,28 @@ export function getPathCost( return !unitB || canLoad(map, unitB, unit, previousVector) ? totalCost : -1; } +function getVisionRange( + map: MapData, + unit: Unit, + start: Vector, + radius: number, +) { + const range = unit.isUnfolded() + ? 2 + : unit.info.type === EntityType.Infantry && + map.getTileInfo(start).type & TileTypes.Mountain + ? 1 + : 0; + return radius + range; +} + export function visible( map: MapData, unit: Unit, start: Vector, radius: number = unit.info.configuration.vision, ): ReadonlyMap { - const vision = - radius + - (unit.isUnfolded() - ? 2 - : unit.info.type === EntityType.Infantry && - map.getTileInfo(start).type & TileTypes.Mountain - ? 1 - : 0); - + const vision = getVisionRange(map, unit, start, radius); const visible = calculateRadius( map, unit, @@ -234,7 +252,6 @@ export function visible( vision, VisionConfiguration, ); - const player = map.getPlayer(unit); const canSeeHiddenFields = player.activeSkills.size && @@ -339,7 +356,8 @@ export function attackable( } const vectors = parent.vector.adjacent(); - for (let i = 0; i < vectors.length; i++) { + const length = vectors.length; + for (let i = 0; i < length; i++) { const vector = vectors[i]; if (map.contains(vector)) { const itemB = attackable.get(vector); diff --git a/athena/info/Tile.tsx b/athena/info/Tile.tsx index 37578e12..c77b9800 100644 --- a/athena/info/Tile.tsx +++ b/athena/info/Tile.tsx @@ -198,7 +198,7 @@ export class TileInfo { } getTransitionCost({ movementType }: { movementType: MovementType }): number { - return this.configuration.transitionCost?.get(movementType) || 0; + return this.configuration.transitionCost?.get(movementType) ?? 0; } isInaccessible() { diff --git a/athena/map/Vector.tsx b/athena/map/Vector.tsx index 80667b2b..fe52a06c 100644 --- a/athena/map/Vector.tsx +++ b/athena/map/Vector.tsx @@ -40,7 +40,7 @@ export default abstract class Vector { adjacent() { return ( - this.vectors || + this.vectors ?? (this.vectors = [ this.up(), this.right(), @@ -130,7 +130,8 @@ export function decodeVectorArray( array: ReadonlyArray, ): ReadonlyArray { const result = []; - for (let i = 0; i < array.length; i += 2) { + const length = array.length; + for (let i = 0; i < length; i += 2) { result.push(vec(array[i], array[i + 1])); } return result; diff --git a/hephaestus/jenkinsHash.tsx b/hephaestus/jenkinsHash.tsx index 2312e9ab..c1502091 100644 --- a/hephaestus/jenkinsHash.tsx +++ b/hephaestus/jenkinsHash.tsx @@ -1,7 +1,7 @@ const toUtf8 = (str: string) => { const result = []; - const len = str.length; - for (let i = 0; i < len; i++) { + const length = str.length; + for (let i = 0; i < length; i++) { let charcode = str.charCodeAt(i); if (charcode < 0x80) { result.push(charcode); @@ -36,8 +36,8 @@ const _jenkinsHash = (str: string): number => { const utf8 = toUtf8(str); let hash = 0; - const len = utf8.length; - for (let i = 0; i < len; i++) { + const length = utf8.length; + for (let i = 0; i < length; i++) { hash += utf8[i]; hash = (hash + (hash << 10)) >>> 0; hash ^= hash >>> 6;