Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
333 changes: 235 additions & 98 deletions src/client/graphics/layers/StructureLayer.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/client/graphics/layers/UnitLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ export class UnitLayer implements Layer {
const targetable = unit.targetable();
if (!targetable) {
this.context.save();
this.context.globalAlpha = 0.4;
this.context.globalAlpha = 0.5;
}

const angle = angleByUnit?.get(unit) ?? this.getUnitAngle(unit);
Expand Down
9 changes: 1 addition & 8 deletions src/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,7 @@
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<title>Terratomic (ALPHA)</title>
<link
href="https://fonts.googleapis.com/css2?family=Anton&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap"
rel="stylesheet"
/>

<!-- Oswald now loaded locally via @font-face in styles.css -->

<link rel="manifest" href="../../resources/manifest.json" />
Expand Down
1 change: 0 additions & 1 deletion src/client/styles/layout/header.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
color: #fff; /* version badge color */
font-weight: 900;
font-size: 1rem; /* 10% smaller than original 1.25rem */
font-family: "Anton", "Bebas Neue", "Impact", sans-serif; /* Cold War feel */
letter-spacing: 1px;
text-transform: uppercase;
text-shadow:
Expand Down
1 change: 0 additions & 1 deletion src/core/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,6 @@ const IntentSchema = z.discriminatedUnion("type", [
BuildUnitIntentSchema,
PurchaseUpgradeIntentSchema,
UpgradeStructureIntentSchema,
UpgradeStructureIntentSchema,
ResearchTreeSelectIntentSchema,
EmbargoIntentSchema,
MoveWarshipIntentSchema,
Expand Down
8 changes: 5 additions & 3 deletions src/core/configuration/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,8 @@ export interface Config {
automationTroopRegenMultiplierNum(): number;
automationTroopRegenMultiplierDen(): number;

// Structure upgrade cost fraction per structure type (e.g., 4/5 for 80%)
structureUpgradeCostNum(type: UnitType): number;
structureUpgradeCostDen(type: UnitType): number;
// Structure upgrade cost multiplier per structure type (e.g., 0.8 for 80%)
structureUpgradeCostMultiplier(type: UnitType): number;

cargoPlaneGold(dist: number): Gold;
cargoPlaneSpawnRate(numberOfAirplanes: number): number;
Expand Down Expand Up @@ -232,7 +231,10 @@ export interface Config {
nukeAllianceBreakThreshold(): number;
defaultNukeSpeed(): number;
defaultNukeTargetableRange(): number;
defaultSamMissileSpeed(): number;
defaultSamRange(): number;
// Percentage (0..1) increase applied per SAM level beyond 1
samRangeUpgradePercent(): number;
nukeDeathFactor(humans: number, tilesOwned: number): number;
structureMinDist(): number;
isReplay(): boolean;
Expand Down
38 changes: 18 additions & 20 deletions src/core/configuration/DefaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1193,11 +1193,20 @@ export class DefaultConfig implements Config {
}

defaultNukeTargetableRange(): number {
return 120;
return 150;
}

defaultSamRange(): number {
return 80;
return 70;
}

samRangeUpgradePercent(): number {
// Each upgrade increases range by 35%; level 3 > H-bomb range, level 2 does not
return 0.35;
}

defaultSamMissileSpeed(): number {
return 12;
}

// Humans can be population, soldiers attacking, soldiers in boat etc.
Expand Down Expand Up @@ -1302,31 +1311,20 @@ export class DefaultConfig implements Config {
automationTroopRegenMultiplierDen(): number {
return 5;
}
// --- Structure upgrade cost fractions ---
structureUpgradeCostNum(type: UnitType): number {
switch (type) {
case UnitType.City:
case UnitType.Port:
case UnitType.Hospital:
case UnitType.Academy:
return 4; // Default 80% -> 4/5
case UnitType.MissileSilo:
return 1; // Missile silo: 50% -> 1/2
default:
return 1;
}
}
structureUpgradeCostDen(type: UnitType): number {
// --- Structure upgrade cost multipliers ---
structureUpgradeCostMultiplier(type: UnitType): number {
switch (type) {
case UnitType.City:
case UnitType.Port:
case UnitType.Hospital:
case UnitType.Academy:
return 5; // Default 80% -> 4/5
return 0.8; // Default 80%
case UnitType.MissileSilo:
return 2; // Missile silo: 50% -> 1/2
return 0.2; // Missile silo: 20%
case UnitType.SAMLauncher:
return 0.4; // SAM: 40%
default:
return 1;
return 1.0;
}
}
// --- Research system defaults ---
Expand Down
5 changes: 3 additions & 2 deletions src/core/execution/ExecutionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,14 @@ export class Executor {
case "upgrade_structure": {
const unit = player.units().find((u) => u.id() === intent.unitId);
if (!unit || unit.owner() !== player) return new NoOpExecution();
// Allow upgrades for City, Port, Hospital, Academy
// Allow upgrades for City, Port, Hospital, Academy, Missile Silo, SAM Launcher
const allowed =
intent.unitType === UnitType.City ||
intent.unitType === UnitType.Port ||
intent.unitType === UnitType.Hospital ||
intent.unitType === UnitType.Academy ||
intent.unitType === UnitType.MissileSilo;
intent.unitType === UnitType.MissileSilo ||
intent.unitType === UnitType.SAMLauncher;
if (!allowed || unit.type() !== intent.unitType) {
return new NoOpExecution();
}
Expand Down
85 changes: 55 additions & 30 deletions src/core/execution/NukeExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
MessageType,
Player,
TerraNullius,
TrajectoryTile,
Unit,
UnitType,
} from "../game/Game";
Expand All @@ -25,8 +26,6 @@ export class NukeExecution implements Execution {
private nuke: Unit | null = null;
private tilesToDestroyCache: Set<TileRef> | undefined;
private eligibleCities: Unit[] = [];

private random: PseudoRandom;
private pathFinder: ParabolaPathFinder;

constructor(
Expand All @@ -40,7 +39,6 @@ export class NukeExecution implements Execution {

init(mg: Game, ticks: number): void {
this.mg = mg;
this.random = new PseudoRandom(ticks);
if (this.speed === -1) {
this.speed = this.mg.config().defaultNukeSpeed();
}
Expand Down Expand Up @@ -112,10 +110,12 @@ export class NukeExecution implements Execution {
this.pathFinder.computeControlPoints(
spawn,
this.dst,
this.speed,
this.nukeType !== UnitType.MIRVWarhead,
);
this.nuke = this.player.buildUnit(this.nukeType, spawn, {
targetTile: this.dst,
trajectory: this.getTrajectory(this.dst),
});
this.maybeBreakAlliances(this.tilesToDestroy());
if (this.mg.hasOwner(this.dst)) {
Expand Down Expand Up @@ -191,27 +191,30 @@ export class NukeExecution implements Execution {
} else {
this.updateNukeTargetable();
this.nuke.move(nextTile);

if (this.nuke === null || this.nuke.targetedBySAM()) return;

const currentNuke = this.nuke;
const readyInterceptors = this.eligibleCities.filter(
(city) =>
(city.ticksLeftInCooldown() ?? 0) <= 0 &&
this.mg.euclideanDistSquared(currentNuke.tile(), city.tile()) <=
this.mg.config().citySamLaunchRange() *
this.mg.config().citySamLaunchRange(),
);

if (readyInterceptors.length > 0) {
readyInterceptors.sort(
(a, b) =>
this.mg.euclideanDistSquared(currentNuke.tile(), a.tile()) -
this.mg.euclideanDistSquared(currentNuke.tile(), b.tile()),
// Update index so SAM can interpolate future position
this.nuke.setTrajectoryIndex(this.pathFinder.currentIndex());

// City-based interception: attempt if in range and off cooldown
if (this.nuke !== null && !this.nuke.targetedBySAM()) {
const currentNuke = this.nuke;
const readyInterceptors = this.eligibleCities.filter(
(city) =>
(city.ticksLeftInCooldown() ?? 0) <= 0 &&
this.mg.euclideanDistSquared(currentNuke.tile(), city.tile()) <=
this.mg.config().citySamLaunchRange() *
this.mg.config().citySamLaunchRange(),
);

const closestInterceptor = readyInterceptors[0];
attemptInterception(currentNuke, this.mg, closestInterceptor);
if (readyInterceptors.length > 0) {
readyInterceptors.sort(
(a, b) =>
this.mg.euclideanDistSquared(currentNuke.tile(), a.tile()) -
this.mg.euclideanDistSquared(currentNuke.tile(), b.tile()),
);

const closestInterceptor = readyInterceptors[0];
attemptInterception(currentNuke, this.mg, closestInterceptor);
}
}
}
}
Expand All @@ -220,21 +223,43 @@ export class NukeExecution implements Execution {
return this.nuke;
}

private getTrajectory(target: TileRef): TrajectoryTile[] {
const trajectoryTiles: TrajectoryTile[] = [];
const targetRangeSquared =
this.mg.config().defaultNukeTargetableRange() ** 2;
const allTiles: TileRef[] = this.pathFinder.allTiles();
for (const tile of allTiles) {
trajectoryTiles.push({
tile,
targetable: this.isTargetable(target, tile, targetRangeSquared),
});
}

return trajectoryTiles;
}

private isTargetable(
targetTile: TileRef,
nukeTile: TileRef,
targetRangeSquared: number,
): boolean {
return (
this.mg.euclideanDistSquared(nukeTile, targetTile) < targetRangeSquared ||
(this.src !== undefined &&
this.src !== null &&
this.mg.euclideanDistSquared(this.src, nukeTile) < targetRangeSquared)
);
}

private updateNukeTargetable() {
if (this.nuke === null || this.nuke.targetTile() === undefined) {
return;
}
const targetRangeSquared =
this.mg.config().defaultNukeTargetableRange() *
this.mg.config().defaultNukeTargetableRange();
this.mg.config().defaultNukeTargetableRange() ** 2;
const targetTile = this.nuke.targetTile();
this.nuke.setTargetable(
this.mg.euclideanDistSquared(this.nuke.tile(), targetTile!) <
targetRangeSquared ||
(this.src !== undefined &&
this.src !== null &&
this.mg.euclideanDistSquared(this.src, this.nuke.tile()) <
targetRangeSquared),
this.isTargetable(targetTile!, this.nuke.tile(), targetRangeSquared),
);
}

Expand Down
Loading
Loading