Skip to content
Binary file added resources/images/buildings/landMine1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,8 @@
"atom_bomb": "Atom Bomb",
"hydrogen_bomb": "Hydrogen Bomb",
"mirv": "MIRV",
"factory": "Factory"
"factory": "Factory",
"land_mine": "Land Mine"
},
"user_setting": {
"title": "User Settings",
Expand Down Expand Up @@ -425,6 +426,8 @@
"build_factory_desc": "Build a Factory under your cursor.",
"build_defense_post": "Build Defense Post",
"build_defense_post_desc": "Build a Defense Post under your cursor.",
"build_land_mine": "Build Land Mine",
"build_land_mine_desc": "Build a Land Mine under your cursor.",
"build_port": "Build Port",
"build_port_desc": "Build a Port under your cursor.",
"build_warship": "Build Warship",
Expand Down Expand Up @@ -574,7 +577,8 @@
"port": "Sends trade ships to generate gold",
"defense_post": "Increases defenses of nearby borders",
"city": "Increases max population",
"factory": "Creates railroads and spawns trains"
"factory": "Creates railroads and spawns trains",
"land_mine": "Explodes upon enemy capture"
},
"not_enough_money": "Not enough money"
},
Expand Down
6 changes: 6 additions & 0 deletions src/client/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export class InputHandler {
buildFactory: "Digit2",
buildPort: "Digit3",
buildDefensePost: "Digit4",
buildLandMine: "KeyM",
buildMissileSilo: "Digit5",
buildSamLauncher: "Digit6",
buildWarship: "Digit7",
Expand Down Expand Up @@ -402,6 +403,11 @@ export class InputHandler {
this.setGhostStructure(UnitType.DefensePost);
}

if (e.code === this.keybinds.buildLandMine) {
e.preventDefault();
this.setGhostStructure(UnitType.LandMine);
}

if (e.code === this.keybinds.buildMissileSilo) {
e.preventDefault();
this.setGhostStructure(UnitType.MissileSilo);
Expand Down
9 changes: 9 additions & 0 deletions src/client/UserSettingModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,15 @@ export class UserSettingModal extends LitElement {
@change=${this.handleKeybindChange}
></setting-keybind>

<setting-keybind
action="buildLandMine"
label=${translateText("user_setting.build_land_mine")}
description=${translateText("user_setting.build_land_mine_desc")}
defaultKey="KeyM"
.value=${this.keybinds["buildLandMine"]?.key ?? ""}
@change=${this.handleKeybindChange}
></setting-keybind>

<setting-keybind
action="buildMissileSilo"
label=${translateText("user_setting.build_missile_silo")}
Expand Down
8 changes: 8 additions & 0 deletions src/client/graphics/layers/BuildMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { UIState } from "../UIState";
import { Layer } from "./Layer";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
import landMineIcon from "/images/ExplosionIconWhite.svg?url";
import factoryIcon from "/images/FactoryIconWhite.svg?url";
import goldCoinIcon from "/images/GoldCoinIcon.svg?url";
import mirvIcon from "/images/MIRVIcon.svg?url";
Expand Down Expand Up @@ -103,6 +104,13 @@ export const buildTable: BuildItemDisplay[][] = [
key: "unit_type.defense_post",
countable: true,
},
{
unitType: UnitType.LandMine,
icon: landMineIcon,
description: "build_menu.desc.land_mine",
key: "unit_type.land_mine",
countable: true,
},
{
unitType: UnitType.City,
icon: cityIcon,
Expand Down
3 changes: 3 additions & 0 deletions src/client/graphics/layers/StructureDrawingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Cell, UnitType } from "../../../core/game/Game";
import { GameView, PlayerView, UnitView } from "../../../core/game/GameView";
import { TransformHandler } from "../TransformHandler";
import anchorIcon from "/images/AnchorIcon.png?url";
import landMineIcon from "/images/buildings/landMine1.png?url";
import cityIcon from "/images/CityIcon.png?url";
import factoryIcon from "/images/FactoryUnit.png?url";
import missileSiloIcon from "/images/MissileSiloUnit.png?url";
Expand All @@ -17,6 +18,7 @@ export const STRUCTURE_SHAPES: Partial<Record<UnitType, ShapeType>> = {
[UnitType.DefensePost]: "octagon",
[UnitType.SAMLauncher]: "square",
[UnitType.MissileSilo]: "triangle",
[UnitType.LandMine]: "circle",
[UnitType.Warship]: "cross",
[UnitType.AtomBomb]: "cross",
[UnitType.HydrogenBomb]: "cross",
Expand Down Expand Up @@ -62,6 +64,7 @@ export class SpriteFactory {
[UnitType.Port, { iconPath: anchorIcon, image: null }],
[UnitType.MissileSilo, { iconPath: missileSiloIcon, image: null }],
[UnitType.SAMLauncher, { iconPath: SAMMissileIcon, image: null }],
[UnitType.LandMine, { iconPath: landMineIcon, image: null }],
]);
constructor(
theme: Theme,
Expand Down
1 change: 1 addition & 0 deletions src/client/graphics/layers/StructureIconsLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export class StructureIconsLayer implements Layer {
[UnitType.Port, { visible: true }],
[UnitType.MissileSilo, { visible: true }],
[UnitType.SAMLauncher, { visible: true }],
[UnitType.LandMine, { visible: true }],
]);
private lastGhostQueryAt: number;
potentialUpgrade: StructureRenderInfo | undefined;
Expand Down
6 changes: 6 additions & 0 deletions src/client/graphics/layers/StructureLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { GameView, UnitView } from "../../../core/game/GameView";
import cityIcon from "/images/buildings/cityAlt1.png?url";
import factoryIcon from "/images/buildings/factoryAlt1.png?url";
import shieldIcon from "/images/buildings/fortAlt3.png?url";
import landMineIcon from "/images/buildings/landMine1.png?url";
import anchorIcon from "/images/buildings/port1.png?url";
import missileSiloIcon from "/images/buildings/silo1.png?url";
import SAMMissileIcon from "/images/buildings/silo4.png?url";
Expand Down Expand Up @@ -69,6 +70,11 @@ export class StructureLayer implements Layer {
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
},
[UnitType.LandMine]: {
icon: landMineIcon,
borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR,
territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR,
},
};

constructor(
Expand Down
11 changes: 11 additions & 0 deletions src/client/graphics/layers/UnitDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { UIState } from "../UIState";
import { Layer } from "./Layer";
import warshipIcon from "/images/BattleshipIconWhite.svg?url";
import cityIcon from "/images/CityIconWhite.svg?url";
import landMineIcon from "/images/ExplosionIconWhite.svg?url";
import factoryIcon from "/images/FactoryIconWhite.svg?url";
import mirvIcon from "/images/MIRVIcon.svg?url";
import missileSiloIcon from "/images/MissileSiloIconWhite.svg?url";
Expand All @@ -34,6 +35,7 @@ export class UnitDisplay extends LitElement implements Layer {
private _missileSilo = 0;
private _port = 0;
private _defensePost = 0;
private _landMine = 0;
private _samLauncher = 0;
private allDisabled = false;
private _hoveredUnit: UnitType | null = null;
Expand All @@ -59,6 +61,7 @@ export class UnitDisplay extends LitElement implements Layer {
config.isUnitDisabled(UnitType.Factory) &&
config.isUnitDisabled(UnitType.Port) &&
config.isUnitDisabled(UnitType.DefensePost) &&
config.isUnitDisabled(UnitType.LandMine) &&
config.isUnitDisabled(UnitType.MissileSilo) &&
config.isUnitDisabled(UnitType.SAMLauncher) &&
config.isUnitDisabled(UnitType.Warship) &&
Expand Down Expand Up @@ -108,6 +111,7 @@ export class UnitDisplay extends LitElement implements Layer {
this._missileSilo = player.totalUnitLevels(UnitType.MissileSilo);
this._port = player.totalUnitLevels(UnitType.Port);
this._defensePost = player.totalUnitLevels(UnitType.DefensePost);
this._landMine = player.totalUnitLevels(UnitType.LandMine);
this._samLauncher = player.totalUnitLevels(UnitType.SAMLauncher);
this._factories = player.totalUnitLevels(UnitType.Factory);
this._warships = player.totalUnitLevels(UnitType.Warship);
Expand Down Expand Up @@ -162,6 +166,13 @@ export class UnitDisplay extends LitElement implements Layer {
"defense_post",
this.keybinds["buildDefensePost"]?.key ?? "4",
)}
${this.renderUnitItem(
landMineIcon,
this._landMine,
UnitType.LandMine,
"land_mine",
this.keybinds["buildLandMine"]?.key ?? "M",
)}
${this.renderUnitItem(
missileSiloIcon,
this._missileSilo,
Expand Down
47 changes: 47 additions & 0 deletions src/core/GameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export async function createGameRunner(
game,
new Executor(game, gameStart.gameID, clientID),
callBack,
clientID,
);
gr.init();
return gr;
Expand All @@ -84,13 +85,15 @@ export class GameRunner {
private turns: Turn[] = [];
private currTurn = 0;
private isExecuting = false;
private myPlayer: Player | null = null;

private playerViewData: Record<PlayerID, NameViewData> = {};

constructor(
public game: Game,
private execManager: Executor,
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
private clientID: ClientID,
) {}

init() {
Expand Down Expand Up @@ -169,6 +172,9 @@ export class GameRunner {
const packedTileUpdates = updates[GameUpdateType.Tile].map((u) => u.update);
updates[GameUpdateType.Tile] = [];

// Filter out units that should be hidden from this player
this.filterHiddenUnits(updates);

this.callBack({
tick: this.game.ticks(),
packedTileUpdates: new BigUint64Array(packedTileUpdates),
Expand All @@ -179,6 +185,47 @@ export class GameRunner {
this.isExecuting = false;
}

/**
* Filters out unit updates for units that should be hidden from this client.
* Units with visibleToEnemies: false are only visible to their owner and allies.
*/
private filterHiddenUnits(updates: GameUpdates): void {
// Lazy-load our player reference
this.myPlayer ??= this.game.playerByClientID(this.clientID) ?? null;

updates[GameUpdateType.Unit] = updates[GameUpdateType.Unit].filter(
(unitUpdate) => {
const unitInfo = this.game.config().unitInfo(unitUpdate.unitType);

// If unit is visible to enemies (default), keep it
if (unitInfo.visibleToEnemies !== false) {
return true;
}

// Unit is hidden from enemies - check if we should see it
if (this.myPlayer === null) {
// Spectator - can't see hidden enemy units
return false;
}

// Check if we own or are allied with the owner
const ownerSmallID = unitUpdate.ownerID;
if (this.myPlayer.smallID() === ownerSmallID) {
return true; // We own it
}

// Check if owner is our ally
const owner = this.game.playerBySmallID(ownerSmallID);
if (owner.isPlayer() && this.myPlayer.isAlliedWith(owner)) {
return true; // Allied with owner
}

// Enemy unit that should be hidden
return false;
},
);
}

public playerActions(
playerID: PlayerID,
x?: number,
Expand Down
12 changes: 12 additions & 0 deletions src/core/configuration/DefaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,16 @@ export class DefaultConfig implements Config {
territoryBound: false,
experimental: true,
};
case UnitType.LandMine:
return {
cost: this.costWrapper(
(numUnits: number) => Math.min(250_000, 75_000 + numUnits * 50_000),
UnitType.LandMine,
),
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 5 * 10,
visibleToEnemies: false,
};
default:
assertNever(type);
}
Expand Down Expand Up @@ -917,6 +927,8 @@ export class DefaultConfig implements Config {
return { inner: 12, outer: 30 };
case UnitType.HydrogenBomb:
return { inner: 80, outer: 100 };
case UnitType.LandMine:
return { inner: 6, outer: 15 };
}
throw new Error(`Unknown nuke type: ${unitType}`);
}
Expand Down
5 changes: 5 additions & 0 deletions src/core/execution/ConstructionExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TileRef } from "../game/GameMap";
import { CityExecution } from "./CityExecution";
import { DefensePostExecution } from "./DefensePostExecution";
import { FactoryExecution } from "./FactoryExecution";
import { LandMineExecution } from "./LandMineExecution";
import { MirvExecution } from "./MIRVExecution";
import { MissileSiloExecution } from "./MissileSiloExecution";
import { NukeExecution } from "./NukeExecution";
Expand Down Expand Up @@ -144,6 +145,9 @@ export class ConstructionExecution implements Execution {
case UnitType.Factory:
this.mg.addExecution(new FactoryExecution(this.structure!));
break;
case UnitType.LandMine:
this.mg.addExecution(new LandMineExecution(this.structure!));
break;
default:
console.warn(
`unit type ${this.constructionType} cannot be constructed`,
Expand All @@ -160,6 +164,7 @@ export class ConstructionExecution implements Execution {
case UnitType.SAMLauncher:
case UnitType.City:
case UnitType.Factory:
case UnitType.LandMine:
return true;
default:
return false;
Expand Down
Loading
Loading