diff --git a/proprietary/images/airfieldicon2.png b/proprietary/images/airfieldicon2.png new file mode 100644 index 000000000..f343b0c0c Binary files /dev/null and b/proprietary/images/airfieldicon2.png differ diff --git a/resources/images/factoryicon.png b/resources/images/factoryicon.png new file mode 100644 index 000000000..ca005a0ff Binary files /dev/null and b/resources/images/factoryicon.png differ diff --git a/resources/lang/en.json b/resources/lang/en.json index 3697a4c0d..7c12086a9 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -265,6 +265,7 @@ "hospital": "Hospital", "academy": "Military Academy", "research_lab": "Research Lab", + "factory": "Factory", "airfield": "Airfield", "air_field": "Airfield", "fighter_jet": "Fighter Jet" @@ -353,6 +354,8 @@ "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_factory": "Build Factory", + "build_factory_desc": "Set the hotkey to build a Factory.", "build_missile_silo": "Build Missile Silo", "build_missile_silo_desc": "Set the hotkey to build a Missile Silo.", "build_sam_launcher": "Build SAM Launcher", diff --git a/src/client/BuildSettingsModal.ts b/src/client/BuildSettingsModal.ts new file mode 100644 index 000000000..f01cf67d2 --- /dev/null +++ b/src/client/BuildSettingsModal.ts @@ -0,0 +1,227 @@ +import { LitElement, html } from "lit"; +import { customElement, query, state } from "lit/decorators.js"; +import { UnitType } from "../core/game/Game"; +import { + isUpgradeableStructure, + maxStructureLevel, + tryParseUnitType, +} from "../core/game/Upgradeables"; +import "./components/baseComponents/Modal"; + +interface BuildSettingsItem { + id: string; + name: string; + icon: string; +} + +@customElement("build-settings-modal") +export class BuildSettingsModal extends LitElement { + @query("o-modal") private modalEl!: HTMLElement & { + open: () => void; + close: () => void; + isModalOpen: boolean; + }; + + @state() private items: BuildSettingsItem[] = []; + @state() private levels: Record = {}; + + /** Populate and open the modal. Loads persisted levels; defaults to 1 */ + public open( + structureTypes: UnitType[] = [], + unitIconMap: Record = {}, + ) { + const upgradeables = structureTypes.filter((t) => + isUpgradeableStructure(t), + ); + this.items = upgradeables.map((t) => { + const id = String(t); + return { + id, + name: id, + icon: unitIconMap[id] || "", + }; + }); + const persisted = this._loadPersisted(); + const lvls: Record = {}; + this.items.forEach((i) => { + const raw = persisted[i.id]; + const parsed = typeof raw === "number" && raw >= 1 ? raw : 1; + lvls[i.id] = this._applyCap(i.id, parsed); + }); + this.levels = lvls; + this.updateComplete.then(() => this.modalEl?.open()); + } + + private _loadPersisted(): Record { + try { + const json = localStorage.getItem("buildSettings.levels") ?? "{}"; + const data = JSON.parse(json); + if (data && typeof data === "object") + return data as Record; + } catch (_) { + /* ignore parse errors */ + } + return {}; + } + + private _persist() { + try { + localStorage.setItem("buildSettings.levels", JSON.stringify(this.levels)); + } catch (_) { + /* ignore quota issues */ + } + } + + // Apply structure-specific level caps via shared rule + private _applyCap(id: string, desired: number): number { + const t = tryParseUnitType(id); + if (!t) return Math.max(1, desired); + return Math.min(maxStructureLevel(t), Math.max(1, desired)); + } + + private _inc(id: string) { + const next = this._applyCap(id, (this.levels[id] ?? 1) + 1); + this.levels = { ...this.levels, [id]: next }; + this._persist(); + } + private _dec(id: string) { + const cur = this.levels[id] ?? 1; + const next = Math.max(1, cur - 1); + this.levels = { ...this.levels, [id]: next }; + this._persist(); + } + + render() { + // Light DOM: provide styles inline (avoids shadow + Tailwind conflicts). + return html` + + +
+ Default structure levels (persistent; capped where applicable) +
+
+ ${this.items.map( + (i) => html` +
+
+ ${i.name} +
${i.name}
+
+
+ + ${this.levels[i.id] ?? 1} + +
+
+ `, + )} +
+
+ `; + } + + createRenderRoot() { + return this; + } +} + +declare global { + interface HTMLElementTagNameMap { + "build-settings-modal": BuildSettingsModal; + } +} diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 630e629e2..56ef4a267 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -188,6 +188,8 @@ export class InputHandler { buildAirfield: "KeyI", buildHospital: "KeyO", buildAcademy: "KeyP", + buildResearchLab: "KeyL", + buildFactory: "KeyF", buildMissileSilo: "KeyH", buildSAMLauncher: "KeyJ", buildDefensePost: "KeyK", @@ -378,6 +380,7 @@ export class InputHandler { [this.keybinds.buildHospital]: UnitType.Hospital, [this.keybinds.buildAcademy]: UnitType.Academy, [this.keybinds.buildResearchLab]: UnitType.ResearchLab, + [this.keybinds.buildFactory]: UnitType.Factory, [this.keybinds.buildMissileSilo]: UnitType.MissileSilo, [this.keybinds.buildSAMLauncher]: UnitType.SAMLauncher, [this.keybinds.buildDefensePost]: UnitType.DefensePost, diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 6a9c5284d..e9d8dbcc7 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -12,6 +12,7 @@ import { } from "../core/game/Game"; import { TileRef } from "../core/game/GameMap"; import { PlayerView } from "../core/game/GameView"; +import { maxStructureLevel } from "../core/game/Upgradeables"; import { AllPlayersStats, ClientHashMessage, @@ -426,7 +427,6 @@ export class Transport { } this.socket.send(msg); } - onconnect(); }; this.socket.onmessage = (event: MessageEvent) => { try { @@ -677,11 +677,30 @@ export class Transport { this._lastBuildUnit = event.unit; this._lastBuildAt = now; + // Compute desired starting level for upgradeable structures from local settings. + let targetLevel: number | undefined; + try { + const raw = localStorage.getItem("buildSettings.levels"); + if (raw) { + const obj = JSON.parse(raw) as Record; + const key = String(event.unit); + const val = obj?.[key]; + if (typeof val === "number" && val > 1) { + // Enforce cap for missile silo / SAM launcher locally (server re-validates). + targetLevel = Math.min(maxStructureLevel(event.unit), val); + } + } + } catch { + // Ignore malformed local storage. + targetLevel = undefined; + } + this.sendIntent({ type: "build_unit", clientID: this.lobbyConfig.clientID, unit: event.unit, tile: event.tile, + targetLevel, }); } diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index a5badcd9f..bc5673bf4 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -636,6 +636,24 @@ export class UserSettingModal extends LitElement { @change=${this.handleKeybindChange} > + + + + + ${desiredLevel > 1 + ? html`L${desiredLevel}` + : ""} ${item.countable diff --git a/src/client/graphics/layers/ControlPanel2.ts b/src/client/graphics/layers/ControlPanel2.ts index 133675399..dc2535c36 100644 --- a/src/client/graphics/layers/ControlPanel2.ts +++ b/src/client/graphics/layers/ControlPanel2.ts @@ -12,6 +12,8 @@ import { } from "../../../core/game/Game"; import { GameView, PlayerView } from "../../../core/game/GameView"; import { getTechMeta, RESEARCH_TECH_IDS } from "../../../core/tech/TechEffects"; +// Ensure modal custom element registers at runtime +import "../../BuildSettingsModal"; import { INVESTMENT_REQUEST_EVENT, INVESTMENT_SYNC_EVENT, @@ -124,6 +126,7 @@ export class ControlPanel2 extends LitElement implements Layer { Hospital: "/images/HospitalIconWhite.svg", "Research Lab": "/images/researchlab.png", Academy: "/images/AcademyIconWhite.png", + Factory: "/images/factoryicon.png", Port: "/images/PortIcon.svg", "Missile Silo": "/images/MissileSiloIconWhite.svg", "SAM Launcher": "/images/SamLauncherIconWhite.svg", @@ -137,6 +140,7 @@ export class ControlPanel2 extends LitElement implements Layer { [UnitType.Hospital]: 1, [UnitType.ResearchLab]: 1.1, [UnitType.Academy]: 1, + [UnitType.Factory]: 1, [UnitType.Port]: 1, [UnitType.MissileSilo]: 1, [UnitType.SAMLauncher]: 1, @@ -179,6 +183,7 @@ export class ControlPanel2 extends LitElement implements Layer { UnitType.Hospital, UnitType.ResearchLab, UnitType.Academy, + UnitType.Factory, UnitType.City, ]; @@ -875,6 +880,41 @@ export class ControlPanel2 extends LitElement implements Layer { this.requestUpdate(); } + private _openBuildSettings() { + const modal = + (document.querySelector("build-settings-modal") as any) || + this._ensureBuildSettingsModal(); + if (!modal) { + console.warn("BuildSettingsModal element not found or failed to create"); + return; + } + const openFn = modal.open; + if (typeof openFn !== "function") { + // Fallback if element existed before registration; re-import then retry + import("../../BuildSettingsModal").then(() => { + const retryOpen = modal.open; + if (typeof retryOpen === "function") { + retryOpen.call(modal, this.StructureTypes, this.unitIconMap); + } else { + console.warn("BuildSettingsModal still missing open() after import"); + } + }); + return; + } + openFn.call(modal, this.StructureTypes, this.unitIconMap); + } + + private _ensureBuildSettingsModal(): HTMLElement | null { + let el = document.querySelector( + "build-settings-modal", + ) as HTMLElement | null; + if (!el) { + el = document.createElement("build-settings-modal"); + document.body.appendChild(el); + } + return el; + } + private _changeTab(tab: "Build" | "Attack" | "Economy" | "Bombers") { this.activeTab = tab; if (this.uiState.pendingBuildUnitType) { @@ -1184,6 +1224,7 @@ export class ControlPanel2 extends LitElement implements Layer { UnitType.Hospital, UnitType.Academy, UnitType.ResearchLab, + UnitType.Factory, ].map((s) => { return html`