Clean up and refactor the Unit class (#769)

## Description:

* Merged similar fields so they can be reused (eg warshipTarget =>
targetUnit)
* simplified isCooldown api
* added "touch" method to send update to UI layer
* standardized on "undefined"


## Please complete the following:

- [ ] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

<DISCORD USERNAME>
evan

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Refactor**
- Unified and simplified how unit targets and cooldowns are managed
across all unit types, resulting in more consistent in-game behavior for
nukes, warships, trade ships, and SAM launchers.
- Updated naming and logic for unit targeting and cooldowns, improving
clarity in status displays and interactions.
- Reorganized unit interface and streamlined cooldown handling for
smoother gameplay experience.
- **Bug Fixes**
- Corrected visual indicators for nukes and warships to accurately
reflect their targets.
- **Tests**
- Updated automated tests to align with the new cooldown and targeting
logic.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
evanpelle
2025-05-17 17:45:10 -07:00
committed by GitHub
parent 0871c08858
commit 500b5fcfde
15 changed files with 167 additions and 192 deletions
+2 -5
View File
@@ -53,11 +53,8 @@ export class MissileSiloExecution implements Execution {
}
}
if (
this.silo.isCooldown() &&
this.silo.ticksLeftInCooldown(this.mg.config().SiloCooldown()) === 0
) {
this.silo.setCooldown(false);
if (this.silo.ticksLeftInCooldown() === 0) {
this.silo.touch();
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ export class MoveWarshipExecution implements Execution {
console.log("MoveWarshipExecution: warship is already dead");
return;
}
warship.setMoveTarget(this.position);
warship.setTargetTile(this.position);
this.active = false;
}
+1 -1
View File
@@ -156,7 +156,7 @@ export class NukeExecution implements Execution {
.units(UnitType.MissileSilo)
.find((silo) => silo.tile() === spawn);
if (silo) {
silo.setCooldown(true);
silo.launch();
}
return;
}
+7 -9
View File
@@ -135,10 +135,10 @@ export class SAMLauncherExecution implements Execution {
unit.owner() !== this.player && !this.player.isFriendly(unit.owner()),
)
.filter((unit) => {
const dst = unit.detonationDst();
const dst = unit.targetTile();
return (
this.sam !== null &&
dst !== null &&
dst !== undefined &&
this.mg.manhattanDist(dst, this.sam.tile()) <
this.MIRVWarheadProtectionRadius
);
@@ -149,19 +149,17 @@ export class SAMLauncherExecution implements Execution {
target = this.getSingleTarget();
}
if (
this.sam.isCooldown() &&
this.sam.ticksLeftInCooldown(this.mg.config().SAMCooldown()) === 0
) {
this.sam.setCooldown(false);
if (this.sam.ticksLeftInCooldown() === 0) {
// Touch SAM to update sprite to show not in cooldown.
this.sam.touch();
}
const isSingleTarget = target && !target.targetedBySAM();
if (
(isSingleTarget || mirvWarheadTargets.length > 0) &&
!this.sam.isCooldown()
!this.sam.isInCooldown()
) {
this.sam.setCooldown(true);
this.sam.launch();
const type =
mirvWarheadTargets.length > 0 ? UnitType.MIRVWarhead : target?.type();
if (type === undefined) throw new Error("Unknown unit type");
+2 -2
View File
@@ -94,7 +94,7 @@ export class TradeShipExecution implements Execution {
return;
} else {
this._dstPort = ports[0];
this.tradeShip.setDstPort(this._dstPort);
this.tradeShip.setTargetUnit(this._dstPort);
}
}
@@ -122,7 +122,7 @@ export class TradeShipExecution implements Execution {
break;
case PathFindResultType.Pending:
// Fire unit event to rerender.
this.tradeShip.move(this.tradeShip.tile());
this.tradeShip.touch();
break;
case PathFindResultType.NextTile:
this._dstPort.cachePut(this.tradeShip.tile(), result.tile);
+58 -53
View File
@@ -19,9 +19,9 @@ export class WarshipExecution implements Execution {
private _owner: Player;
private active = true;
private warship: Unit | null = null;
private mg: Game | null = null;
private mg: Game;
private target: Unit | null = null;
private target: Unit | undefined = undefined;
private pathfinder: PathFinder | null = null;
private patrolTile: TileRef | undefined;
@@ -35,6 +35,7 @@ export class WarshipExecution implements Execution {
) {}
init(mg: Game, ticks: number): void {
this.mg = mg;
if (!mg.hasPlayer(this.playerID)) {
console.log(`WarshipExecution: player ${this.playerID} not found`);
this.active = false;
@@ -42,7 +43,6 @@ export class WarshipExecution implements Execution {
}
this.pathfinder = PathFinder.Mini(mg, 5000);
this._owner = mg.player(this.playerID);
this.mg = mg;
this.patrolTile = this.patrolCenterTile;
this.random = new PseudoRandom(mg.ticks());
}
@@ -56,14 +56,14 @@ export class WarshipExecution implements Execution {
const result = this.pathfinder.nextTile(this.warship.tile(), target);
switch (result.type) {
case PathFindResultType.Completed:
this.warship.setMoveTarget(null);
this.warship.move(this.warship.tile());
this.warship.setTargetTile(undefined);
this.warship.touch();
return;
case PathFindResultType.NextTile:
this.warship.move(result.tile);
break;
case PathFindResultType.Pending:
this.warship.move(this.warship.tile());
this.warship.touch();
break;
case PathFindResultType.PathNotFound:
consolex.log(`path not found to target`);
@@ -72,7 +72,11 @@ export class WarshipExecution implements Execution {
}
private shoot() {
if (this.mg === null || this.warship === null || this.target === null) {
if (
this.mg === null ||
this.warship === null ||
this.target === undefined
) {
throw new Error("Warship not initialized");
}
const shellAttackRate = this.mg.config().warshipShellAttackRate();
@@ -89,7 +93,7 @@ export class WarshipExecution implements Execution {
if (!this.target.hasHealth()) {
// Don't send multiple shells to target that can be oneshotted
this.alreadySentShell.add(this.target);
this.target = null;
this.target = undefined;
return;
}
}
@@ -105,8 +109,11 @@ export class WarshipExecution implements Execution {
return;
}
}
this.warship.setWarshipTarget(this.target);
if (this.target === null || this.target.type() !== UnitType.TradeShip) {
this.warship.setTargetUnit(this.target);
if (
this.target === undefined ||
this.target.type() !== UnitType.TradeShip
) {
// Patrol unless we are hunting down a tradeship
const result = this.pathfinder.nextTile(
this.warship.tile(),
@@ -115,13 +122,13 @@ export class WarshipExecution implements Execution {
switch (result.type) {
case PathFindResultType.Completed:
this.patrolTile = undefined;
this.warship.move(this.warship.tile());
this.warship.touch();
break;
case PathFindResultType.NextTile:
this.warship.move(result.tile);
break;
case PathFindResultType.Pending:
this.warship.move(this.warship.tile());
this.warship.touch();
return;
case PathFindResultType.PathNotFound:
consolex.log(`path not found to patrol tile`);
@@ -153,13 +160,12 @@ export class WarshipExecution implements Execution {
this.active = false;
return;
}
if (this.target !== null && !this.target.isActive()) {
this.target = null;
if (this.target !== undefined && !this.target.isActive()) {
this.target = undefined;
}
const hasPort = this._owner.units(UnitType.Port).length > 0;
if (this.mg === null) throw new Error("Game not initialized");
const warship = this.warship;
if (warship === null) throw new Error("Warship not initialized");
if (warship === undefined) throw new Error("Warship not initialized");
const ships = this.mg
.nearbyUnits(
this.warship.tile(),
@@ -175,68 +181,67 @@ export class WarshipExecution implements Execution {
(unit.type() !== UnitType.TradeShip ||
(hasPort &&
this.warship !== null &&
unit.dstPort()?.owner() !== this.warship.owner() &&
!unit.dstPort()?.owner().isFriendly(this.warship.owner()) &&
unit.targetUnit()?.owner() !== this.warship.owner() &&
!unit.targetUnit()?.owner().isFriendly(this.warship.owner()) &&
unit.isSafeFromPirates() !== true)),
);
this.target =
ships.sort((a, b) => {
const { unit: unitA, distSquared: distA } = a;
const { unit: unitB, distSquared: distB } = b;
this.target = ships.sort((a, b) => {
const { unit: unitA, distSquared: distA } = a;
const { unit: unitB, distSquared: distB } = b;
// Prioritize Warships
if (
unitA.type() === UnitType.Warship &&
unitB.type() !== UnitType.Warship
)
return -1;
if (
unitA.type() !== UnitType.Warship &&
unitB.type() === UnitType.Warship
)
return 1;
// Prioritize Warships
if (
unitA.type() === UnitType.Warship &&
unitB.type() !== UnitType.Warship
)
return -1;
if (
unitA.type() !== UnitType.Warship &&
unitB.type() === UnitType.Warship
)
return 1;
// Then favor Transport Ships over Trade Ships
if (
unitA.type() === UnitType.TransportShip &&
unitB.type() !== UnitType.TransportShip
)
return -1;
if (
unitA.type() !== UnitType.TransportShip &&
unitB.type() === UnitType.TransportShip
)
return 1;
// Then favor Transport Ships over Trade Ships
if (
unitA.type() === UnitType.TransportShip &&
unitB.type() !== UnitType.TransportShip
)
return -1;
if (
unitA.type() !== UnitType.TransportShip &&
unitB.type() === UnitType.TransportShip
)
return 1;
// If both are the same type, sort by distance (lower `distSquared` means closer)
return distA - distB;
})[0]?.unit ?? null;
// If both are the same type, sort by distance (lower `distSquared` means closer)
return distA - distB;
})[0]?.unit;
const moveTarget = this.warship.moveTarget();
const moveTarget = this.warship.targetTile();
if (moveTarget) {
this.goToMoveTarget(moveTarget);
// If we have a "move target" then we cannot target trade ships as it
// requires moving.
if (this.target && this.target.type() === UnitType.TradeShip) {
this.target = null;
this.target = undefined;
}
} else if (!this.target || this.target.type() !== UnitType.TradeShip) {
this.patrol();
}
if (
this.target === null ||
this.target === undefined ||
!this.target.isActive() ||
this.target.owner() === this._owner ||
this.target.isSafeFromPirates() === true
) {
// In case another warship captured or destroyed target, or the target escaped into safe waters
this.target = null;
this.target = undefined;
return;
}
this.warship.setWarshipTarget(this.target);
this.warship.setTargetUnit(this.target);
// If we have a move target we do not want to go after trading ships
if (!this.target) {
@@ -258,7 +263,7 @@ export class WarshipExecution implements Execution {
switch (result.type) {
case PathFindResultType.Completed:
this._owner.captureUnit(this.target);
this.target = null;
this.target = undefined;
this.warship.move(this.warship.tile());
return;
case PathFindResultType.NextTile: