Skip to content

Commit 5dde4cc

Browse files
Extend SAM Range to cover Hydros when stacked (#2351)
## Description: Implements SAM range extension for stacked SAMs to cover hydros as requested in #2347 and many times from users in discord. This implementation is as simple as possible: At level 5 and higher, SAMs extend their range to the range of a hydrogen bomb + 10 for a small safety margin. Levels 2-4 are interpolated between. Screenshot to show the sizes compared to a hydro: <img width="400" alt="image" src="https://github.com/user-attachments/assets/a857d66c-e3d4-467f-855f-3539cc90b719" /> Everything works together with the new range UI, although I might need to unify with / rebase on #2350. Not yet tested with #2348, but shouldn't be an issue. ## Input needed: - Should I add tests for this? - This is in effect a massive buff to SAMs, might be too strong. Popular ideas / suggestions from Discord to balance things: - Cap the SAM upgrade level at the maximum range (easy to do) - Alternative, instead of capping the level, decrease the range when missiles are reloading - Increase the cost scaling for SAMs per stack, and scale way higher (e.g. 1.5M > 3M > 4.5M > 6M or something like that) (UI integration unclear, breaks with existing cost logic) - Decrease SAM hit probability for Hydros I'm happy to implement any of these paths, or just roll with the simple way it's set up now, just let me know. ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: newyearnewphil --------- Co-authored-by: Evan <[email protected]>
1 parent 6fe81cb commit 5dde4cc

File tree

7 files changed

+70
-8
lines changed

7 files changed

+70
-8
lines changed

src/client/graphics/layers/SAMRadiusLayer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export class SAMRadiusLayer implements Layer {
163163
return {
164164
x: this.game.x(tile),
165165
y: this.game.y(tile),
166-
r: this.game.config().defaultSamRange(),
166+
r: this.game.config().samRange(sam.level()),
167167
owner: sam.owner().smallID(),
168168
};
169169
});

src/client/graphics/layers/StructureDrawingUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,14 +428,15 @@ export class SpriteFactory {
428428
type: UnitType,
429429
stage: PIXI.Container,
430430
pos: { x: number; y: number },
431+
level?: number,
431432
): PIXI.Container | null {
432433
if (stage === undefined) throw new Error("Not initialized");
433434
const parentContainer = new PIXI.Container();
434435
const circle = new PIXI.Graphics();
435436
let radius = 0;
436437
switch (type) {
437438
case UnitType.SAMLauncher:
438-
radius = this.game.config().defaultSamRange();
439+
radius = this.game.config().samRange(level ?? 1);
439440
break;
440441
case UnitType.Factory:
441442
radius = this.game.config().trainStationMaxRange();

src/client/graphics/layers/StructureIconsLayer.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export class StructureIconsLayer implements Layer {
5959
private ghostUnit: {
6060
container: PIXI.Container;
6161
range: PIXI.Container | null;
62+
rangeLevel?: number;
6263
buildableUnit: BuildableUnit;
6364
} | null = null;
6465
private pixicanvas: HTMLCanvasElement;
@@ -278,6 +279,9 @@ export class StructureIconsLayer implements Layer {
278279

279280
this.ghostUnit.buildableUnit = unit;
280281

282+
const targetLevel = this.resolveGhostRangeLevel(unit);
283+
this.updateGhostRange(targetLevel);
284+
281285
if (unit.canUpgrade) {
282286
this.potentialUpgrade = this.renders.find(
283287
(r) =>
@@ -370,12 +374,11 @@ export class StructureIconsLayer implements Layer {
370374
{ x: localX, y: localY },
371375
type,
372376
),
373-
range: this.factory.createRange(type, this.ghostStage, {
374-
x: localX,
375-
y: localY,
376-
}),
377+
range: null,
377378
buildableUnit: { type, canBuild: false, canUpgrade: false, cost: 0n },
378379
};
380+
const baseLevel = this.resolveGhostRangeLevel(this.ghostUnit.buildableUnit);
381+
this.updateGhostRange(baseLevel);
379382
}
380383

381384
private clearGhostStructure() {
@@ -397,6 +400,49 @@ export class StructureIconsLayer implements Layer {
397400
this.eventBus.emit(new GhostStructureChangedEvent(null));
398401
}
399402

403+
private resolveGhostRangeLevel(
404+
buildableUnit: BuildableUnit,
405+
): number | undefined {
406+
if (buildableUnit.type !== UnitType.SAMLauncher) {
407+
return undefined;
408+
}
409+
if (buildableUnit.canUpgrade !== false) {
410+
const existing = this.game.unit(buildableUnit.canUpgrade);
411+
if (existing) {
412+
return existing.level() + 1;
413+
} else {
414+
console.error("Failed to find existing SAMLauncher for upgrade");
415+
}
416+
}
417+
418+
return 1;
419+
}
420+
421+
private updateGhostRange(level?: number) {
422+
if (!this.ghostUnit) {
423+
return;
424+
}
425+
426+
if (this.ghostUnit.range && this.ghostUnit.rangeLevel === level) {
427+
return;
428+
}
429+
430+
this.ghostUnit.range?.destroy();
431+
this.ghostUnit.range = null;
432+
this.ghostUnit.rangeLevel = level;
433+
434+
const position = this.ghostUnit.container.position;
435+
const range = this.factory.createRange(
436+
this.ghostUnit.buildableUnit.type,
437+
this.ghostStage,
438+
{ x: position.x, y: position.y },
439+
level,
440+
);
441+
if (range) {
442+
this.ghostUnit.range = range;
443+
}
444+
}
445+
400446
private toggleStructures(toggleStructureType: UnitType[] | null): void {
401447
for (const [structureType, infos] of this.structures) {
402448
infos.visible =

src/core/configuration/Config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ export interface Config {
171171
defaultNukeTargetableRange(): number;
172172
defaultSamMissileSpeed(): number;
173173
defaultSamRange(): number;
174+
samRange(level: number): number;
175+
maxSamRange(): number;
174176
nukeDeathFactor(
175177
nukeType: NukeType,
176178
humans: number,

src/core/configuration/DefaultConfig.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,15 @@ export class DefaultConfig implements Config {
920920
return 70;
921921
}
922922

923+
samRange(level: number): number {
924+
// rational growth function (level 1 = 70, level 5 just above hydro range, asymptotically approaches 150)
925+
return this.maxSamRange() - 480 / (level + 5);
926+
}
927+
928+
maxSamRange(): number {
929+
return 150;
930+
}
931+
923932
defaultSamMissileSpeed(): number {
924933
return 12;
925934
}

src/core/execution/SAMLauncherExecution.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class SAMTargetingSystem {
5050

5151
private isInRange(tile: TileRef) {
5252
const samTile = this.sam.tile();
53-
const range = this.mg.config().defaultSamRange();
53+
const range = this.mg.config().samRange(this.sam.level());
5454
const rangeSquared = range * range;
5555
return this.mg.euclideanDistSquared(samTile, tile) <= rangeSquared;
5656
}
@@ -82,7 +82,7 @@ class SAMTargetingSystem {
8282

8383
public getSingleTarget(ticks: number): Target | null {
8484
// Look beyond the SAM range so it can preshot nukes
85-
const detectionRange = this.mg.config().defaultSamRange() * 2;
85+
const detectionRange = this.mg.config().maxSamRange() * 2;
8686
const nukes = this.mg.nearbyUnits(
8787
this.sam.tile(),
8888
detectionRange,

tests/util/TestConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export class TestConfig extends DefaultConfig {
5454
return 20;
5555
}
5656

57+
samRange(level: number): number {
58+
return 20;
59+
}
60+
5761
spawnImmunityDuration(): Tick {
5862
return 0;
5963
}

0 commit comments

Comments
 (0)