BUG FIX: Gold double deduction + Rmoval of UnitType.Construction (#2378)

## Description:

- Removed the temporary UnitType.Construction and embedded construction
state into real units via isUnderConstruction().
- Centralized non-structure spawning to perform a single validation
right before unit creation/launch.
- Updated UI layers to render construction state without relying on the
removed enum.
- Adjusted and created tests to match the new flow and to cover the
no-refundscenarios.

# Tests updated 
- tests/economy/ConstructionGold.test.ts: covers structure cost
deduction and income, tolerant of passive income; ensures no refunds
during construction.
- tests/nukes/HydrogenAndMirv.test.ts: accounts for single-check launch
flow; MIRV test targets a player-owned tile; ensures launch after
payment.
- tests/client/graphics/UILayer.test.ts: mocks now provide
isUnderConstruction and real type strings;

## 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:

CrackeRR1

---------

Co-authored-by: Evan <evanpelle@gmail.com>
This commit is contained in:
CrackeRR11
2025-11-26 14:45:14 -08:00
committed by GitHub
co-authored by Evan
parent c341aafaf9
commit 8f53785a80
28 changed files with 528 additions and 275 deletions
@@ -452,7 +452,7 @@ export const deleteUnitElement: MenuElement = {
.units()
.filter(
(unit) =>
unit.constructionType() === undefined &&
!unit.isUnderConstruction() &&
unit.markedForDeletion() === false &&
params.game.manhattanDist(unit.tile(), params.tile) <=
DELETE_SELECTION_RADIUS,
@@ -143,19 +143,12 @@ export class SpriteFactory {
const screenPos = this.transformHandler.worldToScreenCoordinates(worldPos);
const isMarkedForDeletion = unit.markedForDeletion() !== false;
const isConstruction = unit.type() === UnitType.Construction;
const constructionType = unit.constructionType();
const structureType = isConstruction ? constructionType! : unit.type();
const isConstruction = unit.isUnderConstruction();
const structureType = unit.type();
const { type, stage } = options;
const { scale } = this.transformHandler;
if (type === "icon" || type === "dot") {
if (isConstruction && constructionType === undefined) {
console.warn(
`Unit ${unit.id()} is a construction but has no construction type.`,
);
return parentContainer;
}
const texture = this.createTexture(
structureType,
unit.owner(),
@@ -469,10 +469,7 @@ export class StructureIconsLayer implements Layer {
this.checkForOwnershipChange(render, unitView);
this.checkForLevelChange(render, unitView);
}
} else if (
this.structures.has(unitView.type()) ||
unitView.type() === UnitType.Construction
) {
} else if (this.structures.has(unitView.type())) {
this.addNewStructure(unitView);
}
}
@@ -485,10 +482,7 @@ export class StructureIconsLayer implements Layer {
}
private modifyVisibility(render: StructureRenderInfo) {
const structureType =
render.unit.type() === UnitType.Construction
? render.unit.constructionType()!
: render.unit.type();
const structureType = render.unit.type();
const structureInfos = this.structures.get(structureType);
let focusStructure = false;
@@ -529,10 +523,7 @@ export class StructureIconsLayer implements Layer {
render: StructureRenderInfo,
unit: UnitView,
) {
if (
render.underConstruction &&
render.unit.type() !== UnitType.Construction
) {
if (render.underConstruction && !unit.isUnderConstruction()) {
render.underConstruction = false;
render.iconContainer?.destroy();
render.dotContainer?.destroy();
@@ -580,10 +571,7 @@ export class StructureIconsLayer implements Layer {
: screenPos.y,
);
const type =
render.unit.type() === UnitType.Construction
? render.unit.constructionType()
: render.unit.type();
const type = render.unit.type();
const margin =
type !== undefined && STRUCTURE_SHAPES[type] !== undefined
? ICON_SIZE[STRUCTURE_SHAPES[type]]
@@ -637,7 +625,7 @@ export class StructureIconsLayer implements Layer {
this.createLevelSprite(unitView),
this.createDotSprite(unitView),
unitView.level(),
unitView.type() === UnitType.Construction,
unitView.isUnderConstruction(),
);
this.renders.push(render);
this.computeNewLocation(render);
+4 -4
View File
@@ -190,7 +190,7 @@ export class StructureLayer implements Layer {
)) {
this.paintCell(
new Cell(this.game.x(tile), this.game.y(tile)),
unit.type() === UnitType.Construction
unit.isUnderConstruction()
? underConstructionColor
: unit.owner().territoryColor(),
130,
@@ -199,7 +199,7 @@ export class StructureLayer implements Layer {
}
private handleUnitRendering(unit: UnitView) {
const unitType = unit.constructionType() ?? unit.type();
const unitType = unit.type();
const iconType = unitType;
if (!this.isUnitTypeSupported(unitType)) return;
@@ -208,7 +208,7 @@ export class StructureLayer implements Layer {
let borderColor = unit.owner().borderColor();
// Handle cooldown states and special icons
if (unit.type() === UnitType.Construction) {
if (unit.isUnderConstruction()) {
icon = this.unitIcons.get(iconType);
borderColor = underConstructionColor;
} else {
@@ -247,7 +247,7 @@ export class StructureLayer implements Layer {
unit: UnitView,
) {
let color = unit.owner().borderColor();
if (unit.type() === UnitType.Construction) {
if (unit.isUnderConstruction()) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
color = underConstructionColor;
}
@@ -90,6 +90,11 @@ export class TerritoryLayer implements Layer {
const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : [];
unitUpdates.forEach((update) => {
if (update.unitType === UnitType.DefensePost) {
// Only update borders if the defense post is not under construction
if (update.underConstruction) {
return; // Skip barrier creation while under construction
}
const tile = update.pos;
this.game
.bfs(tile, euclDistFN(tile, this.game.config().defensePostRange()))
+18 -24
View File
@@ -103,16 +103,12 @@ export class UILayer implements Layer {
}
onUnitEvent(unit: UnitView) {
const underConst = unit.isUnderConstruction();
if (underConst) {
this.createLoadingBar(unit);
return;
}
switch (unit.type()) {
case UnitType.Construction: {
const constructionType = unit.constructionType();
if (constructionType === undefined) {
// Skip units without construction type
return;
}
this.createLoadingBar(unit);
break;
}
case UnitType.Warship: {
this.drawHealthBar(unit);
break;
@@ -318,22 +314,20 @@ export class UILayer implements Layer {
if (!unit.isActive()) {
return 1;
}
switch (unit.type()) {
case UnitType.Construction: {
const constructionType = unit.constructionType();
if (constructionType === undefined) {
return 1;
}
const constDuration =
this.game.unitInfo(constructionType).constructionDuration;
if (constDuration === undefined) {
throw new Error("unit does not have constructionTime");
}
return (
(this.game.ticks() - unit.createdAt()) /
(constDuration === 0 ? 1 : constDuration)
);
const underConst = unit.isUnderConstruction();
if (underConst) {
const constDuration = this.game.unitInfo(
unit.type(),
).constructionDuration;
if (constDuration === undefined) {
throw new Error("unit does not have constructionTime");
}
return (
(this.game.ticks() - unit.createdAt()) /
(constDuration === 0 ? 1 : constDuration)
);
}
switch (unit.type()) {
case UnitType.MissileSilo:
case UnitType.SAMLauncher:
return !unit.markedForDeletion()