mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:20:45 +00:00
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 <evanpelle@gmail.com>
This commit is contained in:
@@ -163,7 +163,7 @@ export class SAMRadiusLayer implements Layer {
|
||||
return {
|
||||
x: this.game.x(tile),
|
||||
y: this.game.y(tile),
|
||||
r: this.game.config().defaultSamRange(),
|
||||
r: this.game.config().samRange(sam.level()),
|
||||
owner: sam.owner().smallID(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -428,6 +428,7 @@ export class SpriteFactory {
|
||||
type: UnitType,
|
||||
stage: PIXI.Container,
|
||||
pos: { x: number; y: number },
|
||||
level?: number,
|
||||
): PIXI.Container | null {
|
||||
if (stage === undefined) throw new Error("Not initialized");
|
||||
const parentContainer = new PIXI.Container();
|
||||
@@ -435,7 +436,7 @@ export class SpriteFactory {
|
||||
let radius = 0;
|
||||
switch (type) {
|
||||
case UnitType.SAMLauncher:
|
||||
radius = this.game.config().defaultSamRange();
|
||||
radius = this.game.config().samRange(level ?? 1);
|
||||
break;
|
||||
case UnitType.Factory:
|
||||
radius = this.game.config().trainStationMaxRange();
|
||||
|
||||
@@ -59,6 +59,7 @@ export class StructureIconsLayer implements Layer {
|
||||
private ghostUnit: {
|
||||
container: PIXI.Container;
|
||||
range: PIXI.Container | null;
|
||||
rangeLevel?: number;
|
||||
buildableUnit: BuildableUnit;
|
||||
} | null = null;
|
||||
private pixicanvas: HTMLCanvasElement;
|
||||
@@ -278,6 +279,9 @@ export class StructureIconsLayer implements Layer {
|
||||
|
||||
this.ghostUnit.buildableUnit = unit;
|
||||
|
||||
const targetLevel = this.resolveGhostRangeLevel(unit);
|
||||
this.updateGhostRange(targetLevel);
|
||||
|
||||
if (unit.canUpgrade) {
|
||||
this.potentialUpgrade = this.renders.find(
|
||||
(r) =>
|
||||
@@ -370,12 +374,11 @@ export class StructureIconsLayer implements Layer {
|
||||
{ x: localX, y: localY },
|
||||
type,
|
||||
),
|
||||
range: this.factory.createRange(type, this.ghostStage, {
|
||||
x: localX,
|
||||
y: localY,
|
||||
}),
|
||||
range: null,
|
||||
buildableUnit: { type, canBuild: false, canUpgrade: false, cost: 0n },
|
||||
};
|
||||
const baseLevel = this.resolveGhostRangeLevel(this.ghostUnit.buildableUnit);
|
||||
this.updateGhostRange(baseLevel);
|
||||
}
|
||||
|
||||
private clearGhostStructure() {
|
||||
@@ -397,6 +400,49 @@ export class StructureIconsLayer implements Layer {
|
||||
this.eventBus.emit(new GhostStructureChangedEvent(null));
|
||||
}
|
||||
|
||||
private resolveGhostRangeLevel(
|
||||
buildableUnit: BuildableUnit,
|
||||
): number | undefined {
|
||||
if (buildableUnit.type !== UnitType.SAMLauncher) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private updateGhostRange(level?: number) {
|
||||
if (!this.ghostUnit) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ghostUnit.range && this.ghostUnit.rangeLevel === level) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ghostUnit.range?.destroy();
|
||||
this.ghostUnit.range = null;
|
||||
this.ghostUnit.rangeLevel = level;
|
||||
|
||||
const position = this.ghostUnit.container.position;
|
||||
const range = this.factory.createRange(
|
||||
this.ghostUnit.buildableUnit.type,
|
||||
this.ghostStage,
|
||||
{ x: position.x, y: position.y },
|
||||
level,
|
||||
);
|
||||
if (range) {
|
||||
this.ghostUnit.range = range;
|
||||
}
|
||||
}
|
||||
|
||||
private toggleStructures(toggleStructureType: UnitType[] | null): void {
|
||||
for (const [structureType, infos] of this.structures) {
|
||||
infos.visible =
|
||||
|
||||
@@ -171,6 +171,8 @@ export interface Config {
|
||||
defaultNukeTargetableRange(): number;
|
||||
defaultSamMissileSpeed(): number;
|
||||
defaultSamRange(): number;
|
||||
samRange(level: number): number;
|
||||
maxSamRange(): number;
|
||||
nukeDeathFactor(
|
||||
nukeType: NukeType,
|
||||
humans: number,
|
||||
|
||||
@@ -920,6 +920,15 @@ export class DefaultConfig implements Config {
|
||||
return 70;
|
||||
}
|
||||
|
||||
samRange(level: number): number {
|
||||
// rational growth function (level 1 = 70, level 5 just above hydro range, asymptotically approaches 150)
|
||||
return this.maxSamRange() - 480 / (level + 5);
|
||||
}
|
||||
|
||||
maxSamRange(): number {
|
||||
return 150;
|
||||
}
|
||||
|
||||
defaultSamMissileSpeed(): number {
|
||||
return 12;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class SAMTargetingSystem {
|
||||
|
||||
private isInRange(tile: TileRef) {
|
||||
const samTile = this.sam.tile();
|
||||
const range = this.mg.config().defaultSamRange();
|
||||
const range = this.mg.config().samRange(this.sam.level());
|
||||
const rangeSquared = range * range;
|
||||
return this.mg.euclideanDistSquared(samTile, tile) <= rangeSquared;
|
||||
}
|
||||
@@ -82,7 +82,7 @@ class SAMTargetingSystem {
|
||||
|
||||
public getSingleTarget(ticks: number): Target | null {
|
||||
// Look beyond the SAM range so it can preshot nukes
|
||||
const detectionRange = this.mg.config().defaultSamRange() * 2;
|
||||
const detectionRange = this.mg.config().maxSamRange() * 2;
|
||||
const nukes = this.mg.nearbyUnits(
|
||||
this.sam.tile(),
|
||||
detectionRange,
|
||||
|
||||
@@ -54,6 +54,10 @@ export class TestConfig extends DefaultConfig {
|
||||
return 20;
|
||||
}
|
||||
|
||||
samRange(level: number): number {
|
||||
return 20;
|
||||
}
|
||||
|
||||
spawnImmunityDuration(): Tick {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user