Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 40 additions & 11 deletions src/client/graphics/layers/NukeTrajectoryPreviewLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,38 @@ export class NukeTrajectoryPreviewLayer implements Layer {
this.drawTrajectoryPreview(context);
}

private canBeInterceptedByAirDefense(
unit: ReturnType<GameView["nearbyUnits"]>[number]["unit"],
distSquared: number,
targetTile: TileRef,
playersToBreakAllianceWith: Set<number>,
): boolean {
if (
unit.owner().isMe() ||
(this.game.myPlayer()?.isFriendly(unit.owner()) &&
!playersToBreakAllianceWith.has(unit.owner().smallID()))
) {
return false;
}

const range = this.game.config().samRange(unit.level());
if (distSquared > range * range) {
return false;
}

if (unit.type() !== UnitType.Warship) {
return true;
}

if (unit.level() <= 1 || !this.game.isOcean(targetTile)) {
return false;
}

return (
this.game.euclideanDistSquared(unit.tile(), targetTile) <= range * range
);
}

/**
* Update trajectory preview - called from tick() to cache spawn tile via expensive player.buildables() call
* This only runs when target tile changes, minimizing worker thread communication
Expand Down Expand Up @@ -276,21 +308,18 @@ export class NukeTrajectoryPreviewLayer implements Layer {
// Check trajectory
for (let i = 0; i < this.trajectoryPoints.length; i++) {
const tile = this.trajectoryPoints[i];
for (const sam of this.game.nearbyUnits(
for (const airDefense of this.game.nearbyUnits(
tile,
this.game.config().maxSamRange(),
UnitType.SAMLauncher,
[UnitType.SAMLauncher, UnitType.Warship],
)) {
if (
sam.unit.owner().isMe() ||
(this.game.myPlayer()?.isFriendly(sam.unit.owner()) &&
!playersToBreakAllianceWith.has(sam.unit.owner().smallID()))
) {
continue;
}
if (
sam.distSquared <=
this.game.config().samRange(sam.unit.level()) ** 2
this.canBeInterceptedByAirDefense(
airDefense.unit,
airDefense.distSquared,
targetTile,
playersToBreakAllianceWith,
)
) {
this.targetedIndex = i;
break;
Expand Down
81 changes: 48 additions & 33 deletions src/client/graphics/layers/SAMRadiusLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ interface SAMRadius {
arcs: Interval[];
}

interface SamInfo {
interface AirDefenseInfo {
ownerId: number;
level: number;
tile: number;
type: UnitType;
}
/**
* Layer responsible for rendering SAM launcher defense radii
* Layer responsible for rendering air-defense radii.
*/
export class SAMRadiusLayer implements Layer {
private readonly samLaunchers: Map<number, SamInfo> = new Map(); // Track SAM launcher IDs -> SAM info
private readonly airDefenseUnits: Map<number, AirDefenseInfo> = new Map();
// track whether the stroke should be shown due to hover or due to an active build ghost
private hoveredShow: boolean = false;
private ghostShow: boolean = false;
Expand All @@ -43,7 +45,8 @@ export class SAMRadiusLayer implements Layer {
this.hoveredShow =
!!types &&
(types.indexOf(UnitType.SAMLauncher) !== -1 ||
types.indexOf(UnitType.City) !== -1);
types.indexOf(UnitType.City) !== -1 ||
types.indexOf(UnitType.Warship) !== -1);
this.updateVisibility();
}

Expand All @@ -54,7 +57,7 @@ export class SAMRadiusLayer implements Layer {
) {}

init() {
// Listen for game updates to detect SAM launcher changes
// Listen for game updates to detect air-defense changes.
// Also listen for UI toggle structure events so we can show borders when
// the user is hovering the Atom/Hydrogen option (UnitDisplay emits
// ToggleStructureEvent with SAMLauncher included in the list).
Expand All @@ -68,14 +71,18 @@ export class SAMRadiusLayer implements Layer {
}

tick() {
// Check for updates to SAM launchers
// Check for updates to air-defense units
const unitUpdates = this.game.updatesSinceLastTick()?.[GameUpdateType.Unit];
if (unitUpdates) {
for (const update of unitUpdates) {
const unit = this.game.unit(update.id);
if (unit && unit.type() === UnitType.SAMLauncher) {
if (
unit &&
(unit.type() === UnitType.SAMLauncher ||
unit.type() === UnitType.Warship)
) {
if (this.hasChanged(unit)) {
this.needsRedraw = true; // A SAM changed: radiuses shall be recomputed when necessary
this.needsRedraw = true;
break;
}
}
Expand All @@ -86,6 +93,7 @@ export class SAMRadiusLayer implements Layer {
this.ghostShow =
this.uiState.ghostStructure === UnitType.MissileSilo ||
this.uiState.ghostStructure === UnitType.SAMLauncher ||
this.uiState.ghostStructure === UnitType.Warship ||
this.uiState.ghostStructure === UnitType.City ||
this.uiState.ghostStructure === UnitType.AtomBomb ||
this.uiState.ghostStructure === UnitType.HydrogenBomb;
Expand All @@ -95,7 +103,7 @@ export class SAMRadiusLayer implements Layer {
renderLayer(context: CanvasRenderingContext2D) {
if (this.visible) {
if (this.needsRedraw) {
// SAM changed: the radiuses needs to be updated
// Air-defense coverage changed, so the radii need to be recomputed.
this.computeCircleUnions();
this.needsRedraw = false;
}
Expand All @@ -120,39 +128,46 @@ export class SAMRadiusLayer implements Layer {
}

private hasChanged(unit: UnitView): boolean {
const samInfos = this.samLaunchers.get(unit.id());
const isNew = samInfos === undefined;
const active = unit.isActive();
const info = this.airDefenseUnits.get(unit.id());
const isTracked = info !== undefined;
const isRelevant =
unit.isActive() &&
(unit.type() === UnitType.SAMLauncher || unit.level() > 1);
const ownerId = unit.owner().smallID();
let hasChanges = isNew || !active; // was built or destroyed
hasChanges ||= !isNew && samInfos.ownerId !== ownerId; // Sam owner changed
hasChanges ||= !isNew && samInfos.level !== unit.level(); // Sam leveled up
let hasChanges = (!isTracked && isRelevant) || (isTracked && !isRelevant);
hasChanges ||= isTracked && info.ownerId !== ownerId;
hasChanges ||= isTracked && info.level !== unit.level();
hasChanges ||= isTracked && info.tile !== unit.tile();
hasChanges ||= isTracked && info.type !== unit.type();
return hasChanges;
}

private getAllSamRanges(): SAMRadius[] {
// Get all active SAM launchers
const samLaunchers = this.game
.units(UnitType.SAMLauncher)
.filter((unit) => unit.isActive());

// Update our tracking set
this.samLaunchers.clear();
samLaunchers.forEach((sam) =>
this.samLaunchers.set(sam.id(), {
ownerId: sam.owner().smallID(),
level: sam.level(),
private getAllAirDefenseRanges(): SAMRadius[] {
const airDefenseUnits = this.game
.units(UnitType.SAMLauncher, UnitType.Warship)
.filter(
(unit) =>
unit.isActive() &&
(unit.type() === UnitType.SAMLauncher || unit.level() > 1),
);

this.airDefenseUnits.clear();
airDefenseUnits.forEach((unit) =>
this.airDefenseUnits.set(unit.id(), {
ownerId: unit.owner().smallID(),
level: unit.level(),
tile: unit.tile(),
type: unit.type(),
}),
);

// Collect radius data
const radiuses = samLaunchers.map((sam) => {
const tile = sam.tile();
const radiuses = airDefenseUnits.map((unit) => {
const tile = unit.tile();
return {
x: this.game.x(tile),
y: this.game.y(tile),
r: this.game.config().samRange(sam.level()),
owner: sam.owner(),
r: this.game.config().samRange(unit.level()),
owner: unit.owner(),
arcs: [],
};
});
Expand Down Expand Up @@ -311,7 +326,7 @@ export class SAMRadiusLayer implements Layer {
* Compute for each circle which angular segments are NOT covered by any other circle
*/
private computeCircleUnions() {
this.samRanges = this.getAllSamRanges();
this.samRanges = this.getAllAirDefenseRanges();
for (let i = 0; i < this.samRanges.length; i++) {
const a = this.samRanges[i];
this.computeUncoveredArcIntervals(a, this.samRanges);
Expand Down
6 changes: 6 additions & 0 deletions src/client/graphics/layers/StructureDrawingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,12 @@ export class SpriteFactory {
case UnitType.SAMLauncher:
radius = this.game.config().samRange(level ?? 1);
break;
case UnitType.Warship:
if ((level ?? 1) <= 1) {
return null;
}
radius = this.game.config().samRange(level ?? 1);
break;
case UnitType.Factory:
radius = this.game.config().trainStationMaxRange();
break;
Expand Down
9 changes: 6 additions & 3 deletions src/client/graphics/layers/StructureIconsLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,19 +565,22 @@ export class StructureIconsLayer implements Layer {
private resolveGhostRangeLevel(
buildableUnit: BuildableUnit,
): number | undefined {
if (buildableUnit.type !== UnitType.SAMLauncher) {
if (
buildableUnit.type !== UnitType.SAMLauncher &&
buildableUnit.type !== UnitType.Warship
) {
return undefined;
}
if (buildableUnit.canUpgrade !== false) {
const existing = this.game.unit(buildableUnit.canUpgrade);
if (existing) {
return existing.level() + 1;
} else {
console.error("Failed to find existing SAMLauncher for upgrade");
console.error("Failed to find existing air-defense unit for upgrade");
}
}

return 1;
return buildableUnit.type === UnitType.Warship ? undefined : 1;
}

private updateGhostRange(level?: number, targetingAlly: boolean = false) {
Expand Down
4 changes: 4 additions & 0 deletions src/client/graphics/layers/UILayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export class UILayer implements Layer {
switch (unit.type()) {
case UnitType.Warship: {
this.drawHealthBar(unit);
if (unit.level() > 1 && unit.missileReadinesss() < 1) {
this.createLoadingBar(unit);
}
break;
}
case UnitType.City:
Expand Down Expand Up @@ -330,6 +333,7 @@ export class UILayer implements Layer {
switch (unit.type()) {
case UnitType.MissileSilo:
case UnitType.SAMLauncher:
case UnitType.Warship:
return !unit.markedForDeletion()
? unit.missileReadinesss()
: this.deletionProgress(this.game, unit);
Expand Down
33 changes: 17 additions & 16 deletions src/client/graphics/layers/UnitDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ export class UnitDisplay extends LitElement implements Layer {
private allDisabled = false;
private _hoveredUnit: PlayerBuildableUnitType | null = null;

private hoverStructureTypes(
unitType: PlayerBuildableUnitType,
): PlayerBuildableUnitType[] {
switch (unitType) {
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
return [UnitType.MissileSilo, UnitType.SAMLauncher];
case UnitType.Warship:
return [UnitType.Port, UnitType.Warship];
default:
return [unitType];
}
}

createRenderRoot() {
return this;
}
Expand Down Expand Up @@ -273,22 +287,9 @@ export class UnitDisplay extends LitElement implements Layer {
this.requestUpdate();
}}
@mouseenter=${() => {
switch (unitType) {
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
this.eventBus?.emit(
new ToggleStructureEvent([
UnitType.MissileSilo,
UnitType.SAMLauncher,
]),
);
break;
case UnitType.Warship:
this.eventBus?.emit(new ToggleStructureEvent([UnitType.Port]));
break;
default:
this.eventBus?.emit(new ToggleStructureEvent([unitType]));
}
this.eventBus?.emit(
new ToggleStructureEvent(this.hoverStructureTypes(unitType)),
);
}}
@mouseleave=${() =>
this.eventBus?.emit(new ToggleStructureEvent(null))}
Expand Down
1 change: 1 addition & 0 deletions src/core/configuration/DefaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ export class DefaultConfig implements Config {
UnitType.Warship,
),
maxHealth: 1000,
upgradable: true,
};
break;
case UnitType.Shell:
Expand Down
Loading
Loading