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
19 changes: 12 additions & 7 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@
"bots": "Bots: ",
"bots_disabled": "Disabled",
"disable_nations": "Disable Nations",
"instant_build": "Instant build",
"infinite_gold": "Infinite gold",
"infinite_troops": "Infinite troops",
"instant_build": "Instant Build",
"instant_research": "Instant Research",
"infinite_gold": "Infinite Gold",
"infinite_troops": "Infinite Troops",
"disable_nukes": "Disable Nukes",
"enables_title": "Enable Settings",
"start": "Start Game"
Expand Down Expand Up @@ -168,7 +169,7 @@
"fantasy": "Other"
},
"starting_gold": {
"label": "Starting gold",
"label": "Starting Gold",
"default": "0 (default)",
"millions": "{amount}M"
},
Expand Down Expand Up @@ -206,9 +207,10 @@
"bots": "Bots: ",
"bots_disabled": "Disabled",
"disable_nations": "Disable Nations",
"instant_build": "Instant build",
"infinite_gold": "Infinite gold",
"infinite_troops": "Infinite troops",
"instant_build": "Instant Build",
"instant_research": "Instant Research",
"infinite_gold": "Infinite Gold",
"infinite_troops": "Infinite Troops",
"peace_timer": "Protected Start",
"peace_timer_none": "None",
"peace_timer_minutes": "{minutes} minutes",
Expand Down Expand Up @@ -262,6 +264,7 @@
"mirv": "MIRV",
"hospital": "Hospital",
"academy": "Military Academy",
"research_lab": "Research Lab",
"airfield": "Airfield",
"air_field": "Airfield",
"fighter_jet": "Fighter Jet"
Expand Down Expand Up @@ -348,6 +351,8 @@
"build_hospital_desc": "Set the hotkey to build a Hospital.",
"build_academy": "Build Academy",
"build_academy_desc": "Set the hotkey to build an Academy.",
"build_research_lab": "Build Research Lab",
"build_research_lab_desc": "Set the hotkey to build a Research Lab.",
"build_missile_silo": "Build Missile Silo",
"build_missile_silo_desc": "Set the hotkey to build a Missile Silo.",
"build_sam_launcher": "Build SAM Launcher",
Expand Down
2 changes: 1 addition & 1 deletion src/client/HostLobbyModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ export class HostLobbyModal extends LitElement {
.checked=${this.instantResearchHumanOnly}
/>
<div class="option-card-title">
Instant Research
${translateText("host_modal.instant_research")}
</div>
</label>

Expand Down
1 change: 1 addition & 0 deletions src/client/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ export class InputHandler {
[this.keybinds.buildAirfield]: UnitType.Airfield,
[this.keybinds.buildHospital]: UnitType.Hospital,
[this.keybinds.buildAcademy]: UnitType.Academy,
[this.keybinds.buildResearchLab]: UnitType.ResearchLab,
[this.keybinds.buildMissileSilo]: UnitType.MissileSilo,
[this.keybinds.buildSAMLauncher]: UnitType.SAMLauncher,
[this.keybinds.buildDefensePost]: UnitType.DefensePost,
Expand Down
4 changes: 3 additions & 1 deletion src/client/SinglePlayerModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,9 @@ export class SinglePlayerModal extends LitElement {
@change=${this.handleInstantResearchHumanOnlyChange}
.checked=${this.instantResearchHumanOnly}
/>
<div class="option-card-title">Instant Research</div>
<div class="option-card-title">
${translateText("single_modal.instant_research")}
</div>
</label>

<label
Expand Down
2 changes: 2 additions & 0 deletions src/client/graphics/layers/BuildMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export class BuildMenu extends LitElement {
buildAirfield: "KeyI",
buildHospital: "KeyO",
buildAcademy: "KeyP",
buildResearchLab: "KeyL",
buildMissileSilo: "KeyH",
buildSAMLauncher: "KeyJ",
buildDefensePost: "KeyK",
Expand All @@ -246,6 +247,7 @@ export class BuildMenu extends LitElement {
[keybinds.buildAirfield]: UnitType.Airfield,
[keybinds.buildHospital]: UnitType.Hospital,
[keybinds.buildAcademy]: UnitType.Academy,
[keybinds.buildResearchLab]: UnitType.ResearchLab,
[keybinds.buildMissileSilo]: UnitType.MissileSilo,
[keybinds.buildSAMLauncher]: UnitType.SAMLauncher,
[keybinds.buildDefensePost]: UnitType.DefensePost,
Expand Down
5 changes: 4 additions & 1 deletion src/client/graphics/layers/PlayerInfoOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
UnitType.City,
UnitType.Hospital,
UnitType.Academy,
UnitType.ResearchLab,
UnitType.Port,
UnitType.Warship,
UnitType.MissileSilo,
Expand All @@ -227,6 +228,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
[UnitType.City]: "/images/CityIconWhite.svg",
[UnitType.Hospital]: "/images/HospitalIconWhite.svg",
[UnitType.Academy]: "/images/AcademyIconWhite.png",
[UnitType.ResearchLab]: "/images/researchlab.png",
[UnitType.Port]: "/images/PortIcon.svg",
[UnitType.Warship]: "/images/BattleshipIconWhite.svg",
[UnitType.MissileSilo]: "/images/MissileSiloIconWhite.svg",
Expand Down Expand Up @@ -345,7 +347,8 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
unitType === UnitType.City ||
unitType === UnitType.Port ||
unitType === UnitType.Hospital ||
unitType === UnitType.Academy
unitType === UnitType.Academy ||
unitType === UnitType.ResearchLab
? player.unitsOwned(unitType)
: player.units(unitType).length;

Expand Down
20 changes: 18 additions & 2 deletions src/client/graphics/layers/StructureLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export class StructureLayer implements Layer {
private lastAffordableForUpgradePort: boolean | null = null;
private lastAffordableForUpgradeHospital: boolean | null = null;
private lastAffordableForUpgradeAcademy: boolean | null = null;
private lastAffordableForUpgradeResearchLab: boolean | null = null;
private lastAffordableForUpgradeSilo: boolean | null = null;
private lastAffordableForUpgradeSAM: boolean | null = null;
// Client-side level tracking for structures (temporary)
Expand Down Expand Up @@ -341,6 +342,7 @@ export class StructureLayer implements Layer {
unit.type() !== UnitType.Port &&
unit.type() !== UnitType.Hospital &&
unit.type() !== UnitType.Academy &&
unit.type() !== UnitType.ResearchLab &&
unit.type() !== UnitType.MissileSilo &&
unit.type() !== UnitType.SAMLauncher
)
Expand All @@ -357,12 +359,16 @@ export class StructureLayer implements Layer {
const affordableAcademy = this.canAffordUpgradeForType(UnitType.Academy);
const affordableSilo = this.canAffordUpgradeForType(UnitType.MissileSilo);
const affordableSAM = this.canAffordUpgradeForType(UnitType.SAMLauncher);
const affordableResearchLab = this.canAffordUpgradeForType(
UnitType.ResearchLab,
);
if (!this.upgradeMode) {
if (
this.lastAffordableForUpgradeCity !== null ||
this.lastAffordableForUpgradePort !== null ||
this.lastAffordableForUpgradeHospital !== null ||
this.lastAffordableForUpgradeAcademy !== null ||
this.lastAffordableForUpgradeResearchLab !== null ||
this.lastAffordableForUpgradeSilo !== null ||
this.lastAffordableForUpgradeSAM !== null
) {
Expand All @@ -372,6 +378,7 @@ export class StructureLayer implements Layer {
r.unit.type() === UnitType.Port ||
r.unit.type() === UnitType.Hospital ||
r.unit.type() === UnitType.Academy ||
r.unit.type() === UnitType.ResearchLab ||
r.unit.type() === UnitType.MissileSilo ||
r.unit.type() === UnitType.SAMLauncher
) {
Expand All @@ -382,6 +389,7 @@ export class StructureLayer implements Layer {
this.lastAffordableForUpgradePort = null;
this.lastAffordableForUpgradeHospital = null;
this.lastAffordableForUpgradeAcademy = null;
this.lastAffordableForUpgradeResearchLab = null;
this.lastAffordableForUpgradeSilo = null;
this.lastAffordableForUpgradeSAM = null;
this.shouldRedraw = true;
Expand All @@ -407,13 +415,16 @@ export class StructureLayer implements Layer {
this.lastAffordableForUpgradeAcademy !== affordableAcademy;
const siloChanged = this.lastAffordableForUpgradeSilo !== affordableSilo;
const samChanged = this.lastAffordableForUpgradeSAM !== affordableSAM;
const labChanged =
this.lastAffordableForUpgradeResearchLab !== affordableResearchLab;
if (
cityChanged ||
portChanged ||
hospitalChanged ||
academyChanged ||
siloChanged ||
samChanged
samChanged ||
labChanged
) {
for (const r of this.renders) {
const t = r.unit.type();
Expand All @@ -422,6 +433,7 @@ export class StructureLayer implements Layer {
(portChanged && t === UnitType.Port) ||
(hospitalChanged && t === UnitType.Hospital) ||
(academyChanged && t === UnitType.Academy) ||
(labChanged && t === UnitType.ResearchLab) ||
(siloChanged && t === UnitType.MissileSilo) ||
(samChanged && t === UnitType.SAMLauncher)
) {
Expand All @@ -432,6 +444,7 @@ export class StructureLayer implements Layer {
this.lastAffordableForUpgradePort = affordablePort;
this.lastAffordableForUpgradeHospital = affordableHospital;
this.lastAffordableForUpgradeAcademy = affordableAcademy;
this.lastAffordableForUpgradeResearchLab = affordableResearchLab;
this.lastAffordableForUpgradeSilo = affordableSilo;
this.lastAffordableForUpgradeSAM = affordableSAM;
this.shouldRedraw = true;
Expand All @@ -446,6 +459,7 @@ export class StructureLayer implements Layer {
t !== UnitType.Port &&
t !== UnitType.Hospital &&
t !== UnitType.Academy &&
t !== UnitType.ResearchLab &&
t !== UnitType.MissileSilo &&
t !== UnitType.SAMLauncher
) {
Expand Down Expand Up @@ -602,6 +616,7 @@ export class StructureLayer implements Layer {
structureType === UnitType.Port ||
structureType === UnitType.Hospital ||
structureType === UnitType.Academy ||
structureType === UnitType.ResearchLab ||
structureType === UnitType.MissileSilo ||
structureType === UnitType.SAMLauncher) &&
this.shouldHighlight(unit)
Expand Down Expand Up @@ -869,13 +884,14 @@ export class StructureLayer implements Layer {
if (clickedUnit.owner() !== this.game.myPlayer()) {
return;
}
// In upgrade mode: attempt to upgrade structure (City/Port/Hospital/Academy/MissileSilo) immediately
// In upgrade mode: attempt to upgrade structure (City/Port/Hospital/Academy/ResearchLab/MissileSilo/SAMLauncher) immediately
if (
this.upgradeMode &&
(clickedUnit.type() === UnitType.City ||
clickedUnit.type() === UnitType.Port ||
clickedUnit.type() === UnitType.Hospital ||
clickedUnit.type() === UnitType.Academy ||
clickedUnit.type() === UnitType.ResearchLab ||
clickedUnit.type() === UnitType.MissileSilo ||
clickedUnit.type() === UnitType.SAMLauncher)
) {
Expand Down
1 change: 1 addition & 0 deletions src/client/utilities/RenderUnitTypeOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const unitOptions: { type: UnitType; translationKey: string }[] = [
{ type: UnitType.MIRV, translationKey: "unit_type.mirv" },
{ type: UnitType.Hospital, translationKey: "unit_type.hospital" },
{ type: UnitType.Academy, translationKey: "unit_type.academy" },
{ type: UnitType.ResearchLab, translationKey: "unit_type.research_lab" },
];

export function renderUnitTypeOptions({
Expand Down
1 change: 1 addition & 0 deletions src/core/configuration/DefaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,7 @@ export class DefaultConfig implements Config {
case UnitType.Port:
case UnitType.Hospital:
case UnitType.Academy:
case UnitType.ResearchLab:
return 0.8; // Default 80%
case UnitType.MissileSilo:
return 0.2; // Missile silo: 20%
Expand Down
3 changes: 2 additions & 1 deletion src/core/execution/ExecutionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,13 @@ 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, Missile Silo, SAM Launcher
// Allow upgrades for City, Port, Hospital, Academy, Research Lab, 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.ResearchLab ||
intent.unitType === UnitType.MissileSilo ||
intent.unitType === UnitType.SAMLauncher;
if (!allowed || unit.type() !== intent.unitType) {
Expand Down
14 changes: 13 additions & 1 deletion src/core/execution/PlayerExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,21 @@ export class PlayerExecution implements Execution {
const investment = Math.max(0, grossGold * investRate);
const A = this.config.researchAlpha();
const B = this.config.researchBeta();
const xTotal = A * Math.pow(investment, B);
let xTotal = A * Math.pow(investment, B);
if (!Number.isFinite(xTotal) || xTotal <= 0) return;

// Apply Research Lab multiplier: +40% for first, +20% for second, halving thereafter
// Upgrades count as multiples via effectiveUnits (level-weighted)
const labsEff = Math.max(
0,
(this.player as any).effectiveUnits?.(UnitType.ResearchLab) ?? 0,
);
if (labsEff > 0) {
const boostSum = (0.4 * (1 - Math.pow(0.5, labsEff))) / (1 - 0.5); // geometric series
const multiplier = 1 + boostSum; // caps at 1.8 as labs -> infinity
xTotal *= multiplier;
}

// Build researched set and available techs
const nodes = getTechNodes();
const researched = new Set<string>();
Expand Down
1 change: 1 addition & 0 deletions src/core/execution/UpgradeStructureExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class UpgradeStructureExecution implements Execution {
case UnitType.Port:
case UnitType.Hospital:
case UnitType.Academy:
case UnitType.ResearchLab:
case UnitType.MissileSilo:
case UnitType.SAMLauncher: {
const unitType = this.unit.type();
Expand Down
3 changes: 2 additions & 1 deletion src/core/game/PlayerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,8 @@ export class PlayerImpl implements Player {
u.type() === UnitType.City ||
u.type() === UnitType.Port ||
u.type() === UnitType.Hospital ||
u.type() === UnitType.Academy
u.type() === UnitType.Academy ||
u.type() === UnitType.ResearchLab
? baseMax + 1000 * Math.max(0, level - 1)
: baseMax;
const healthRatio = u.hasHealth()
Expand Down
11 changes: 11 additions & 0 deletions src/core/game/UnitImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,17 @@ export class UnitImpl implements Unit {
this.mg.addUpdate(this.toUpdate());
return;
}
case UnitType.ResearchLab: {
// Research Lab upgrades: increase level only (counts as multiples), no health changes
this._level += 1;
this._bonusMaxHealth += 1000;
const healed = Number(this._health) + 1000;
const capped = Math.min(healed, this.effectiveMaxHealth());
this._health = toInt(capped);
this._owner.invalidateEffectiveUnitsCache(UnitType.ResearchLab);
this.mg.addUpdate(this.toUpdate());
return;
}
default:
// Unsupported structure types: no-op for now
return;
Expand Down
Loading