diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 56ef4a267..f31451d13 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -393,6 +393,7 @@ export class InputHandler { this.uiState.upgradeMode = false; this.eventBus.emit(new ToggleUpgradeModeEvent(false)); } + // unit upgrade mode removed const cell = this.transformHandler.screenToWorldCoordinates( this.lastPointerX, this.lastPointerY, @@ -484,6 +485,7 @@ export class InputHandler { this.uiState.upgradeMode = false; this.eventBus.emit(new ToggleUpgradeModeEvent(false)); } + // unit upgrade mode removed this.eventBus.emit( new BuildUnitIntentEvent(this.uiState.pendingBuildUnitType, tile), ); diff --git a/src/client/Transport.ts b/src/client/Transport.ts index e9d8dbcc7..9b80c1093 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -12,7 +12,11 @@ import { } from "../core/game/Game"; import { TileRef } from "../core/game/GameMap"; import { PlayerView } from "../core/game/GameView"; -import { maxStructureLevel } from "../core/game/Upgradeables"; +import { + isUpgradeableUnit, + maxStructureLevel, + maxUnitLevel, +} from "../core/game/Upgradeables"; import { AllPlayersStats, ClientHashMessage, @@ -353,6 +357,7 @@ export class Transport { this.eventBus.on(SendKickPlayerIntentEvent, (e) => this.onSendKickPlayerIntent(e), ); + // unit upgrade intent removed } private startPing() { @@ -678,16 +683,27 @@ export class Transport { this._lastBuildAt = now; // Compute desired starting level for upgradeable structures from local settings. + // Compute desired starting level for upgradeable structures or units 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); + const key = String(event.unit); + if (isUpgradeableUnit(event.unit)) { + const rawUnits = localStorage.getItem("unitUpgradeSettings.levels"); + if (rawUnits) { + const obj = JSON.parse(rawUnits) as Record; + const val = obj?.[key]; + if (typeof val === "number" && val > 1) { + targetLevel = Math.min(maxUnitLevel(event.unit), val); + } + } + } else { + const rawStruct = localStorage.getItem("buildSettings.levels"); + if (rawStruct) { + const obj = JSON.parse(rawStruct) as Record; + const val = obj?.[key]; + if (typeof val === "number" && val > 1) { + targetLevel = Math.min(maxStructureLevel(event.unit), val); + } } } } catch { @@ -862,6 +878,8 @@ export class Transport { }); } + // unit upgrade intent handler removed + private sendIntent(intent: Intent) { if (this.isLocal || this.socket?.readyState === WebSocket.OPEN) { const msg = { diff --git a/src/client/UnitUpgradeSettingsModal.ts b/src/client/UnitUpgradeSettingsModal.ts new file mode 100644 index 000000000..fc6e8a9e0 --- /dev/null +++ b/src/client/UnitUpgradeSettingsModal.ts @@ -0,0 +1,234 @@ +import { LitElement, html } from "lit"; +import { customElement, query, state } from "lit/decorators.js"; +import { UnitType } from "../core/game/Game"; +import { + isUpgradeableUnit, + maxUnitLevel, + tryParseUnitType, +} from "../core/game/Upgradeables"; +import "./components/baseComponents/Modal"; + +interface UpgradeSettingsItem { + id: string; + name: string; + icon?: string; +} + +@customElement("unit-upgrade-settings-modal") +export class UnitUpgradeSettingsModal extends LitElement { + @query("o-modal") private modalEl!: HTMLElement & { + open: () => void; + close: () => void; + isModalOpen: boolean; + }; + + @state() private items: UpgradeSettingsItem[] = []; + @state() private levels: Record = {}; + + /** Populate and open the modal. Loads persisted levels; defaults to 1 */ + public open( + unitTypes: UnitType[] = [], + unitIconMap: Record = {}, + ) { + const upgradeables = unitTypes.filter((t) => isUpgradeableUnit(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("unitUpgradeSettings.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( + "unitUpgradeSettings.levels", + JSON.stringify(this.levels), + ); + } catch (_) { + /* ignore quota issues */ + } + } + + // Apply unit-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(maxUnitLevel(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() { + return html` + + +
+ Default combat unit levels (persistent; capped per unit type) +
+
+ ${this.items.map( + (i) => html` +
+
+ ${i.icon + ? html`${i.name}` + : html``} +
${i.name}
+
+
+ + ${this.levels[i.id] ?? 1} + +
+
+ `, + )} +
+
+ `; + } + + createRenderRoot() { + return this; + } +} + +declare global { + interface HTMLElementTagNameMap { + "unit-upgrade-settings-modal": UnitUpgradeSettingsModal; + } +} diff --git a/src/client/events/ToggleUnitUpgradeModeEvent.ts b/src/client/events/ToggleUnitUpgradeModeEvent.ts new file mode 100644 index 000000000..3a66b9459 --- /dev/null +++ b/src/client/events/ToggleUnitUpgradeModeEvent.ts @@ -0,0 +1,2 @@ +// Deprecated placeholder to maintain git history; file will be removed. +export {}; diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 0229cce8d..0fe4b3ec3 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -55,6 +55,7 @@ export function createRenderer( pendingBuildUnitType: null, multibuildEnabled: false, upgradeMode: false, + unitLevels: {}, }; //hide when the game renders @@ -239,7 +240,7 @@ export function createRenderer( // World-space ring overlay for Defense Posts/SAMs new RangeOverlayLayer(game, eventBus, transformHandler, uiState), structureLayer, - new UnitLayer(game, eventBus, transformHandler), + new UnitLayer(game, eventBus, transformHandler, uiState), new FxLayer(game), // Draw name labels in world space along with other transformed layers new NameLayer(game, transformHandler, eventBus), diff --git a/src/client/graphics/UIState.ts b/src/client/graphics/UIState.ts index 6fd5a1f0f..eacb9a805 100644 --- a/src/client/graphics/UIState.ts +++ b/src/client/graphics/UIState.ts @@ -7,4 +7,6 @@ export interface UIState { multibuildEnabled: boolean; // Whether the player is currently in city upgrade targeting mode upgradeMode: boolean; + // Local client-side unit levels (id -> level) + unitLevels: Record; } diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts index 56b491a3f..0d81bf4a8 100644 --- a/src/client/graphics/layers/BuildMenu.ts +++ b/src/client/graphics/layers/BuildMenu.ts @@ -24,7 +24,9 @@ import { Gold, UnitType, UpgradeType } from "../../../core/game/Game"; import { GameView } from "../../../core/game/GameView"; import { isUpgradeableStructure, + isUpgradeableUnit, maxStructureLevel, + maxUnitLevel, } from "../../../core/game/Upgradeables"; import { ToggleUpgradeModeEvent } from "../../events/ToggleUpgradeModeEvent"; import { CloseViewEvent } from "../../InputHandler"; @@ -443,6 +445,17 @@ export class BuildMenu extends LitElement { font-size: 9px; border: 1px solid var(--ui-border-muted); } + .build-level-chip { + position: absolute; + top: -5px; + left: -5px; + background-color: var(--ui-panel-shell-bottom); + color: var(--ui-text-light); + padding: 1px 5px; + border-radius: 10px; + font-size: 9px; + border: 1px solid var(--ui-border-muted); + } .build-hotkey { position: absolute; bottom: 2px; @@ -454,14 +467,26 @@ export class BuildMenu extends LitElement { background-color: var(--ui-panel-shell-top); border-color: var(--ui-border-muted); } + .build-button:not(:disabled):hover > .build-level-chip { + background-color: var(--ui-panel-shell-top); + border-color: var(--ui-border-muted); + } .build-button:not(:disabled):active > .build-count-chip { background-color: var(--ui-panel-shell-bottom); } + .build-button:not(:disabled):active > .build-level-chip { + background-color: var(--ui-panel-shell-bottom); + } .build-button:disabled > .build-count-chip { background-color: var(--ui-surface-dark); border-color: var(--ui-border-muted); cursor: not-allowed; } + .build-button:disabled > .build-level-chip { + background-color: var(--ui-surface-dark); + border-color: var(--ui-border-muted); + cursor: not-allowed; + } .build-count { font-weight: bold; font-size: 10px; @@ -502,22 +527,40 @@ export class BuildMenu extends LitElement { .config() .unitInfo(item.unitType) .cost(this.game.myPlayer()!); - if (!isUpgradeableStructure(item.unitType)) return base; - const desired = this._desiredLevel(item.unitType); - if (desired <= 1) return base; - const multiplier = this.game - .config() - .structureUpgradeCostMultiplier(item.unitType); - return aggregateStructureBuildCost( - this.game.config(), - this.game.myPlayer()!, - item.unitType, - desired, - multiplier, - ); + // Structures: use configured structure multiplier + if (isUpgradeableStructure(item.unitType)) { + const desired = this._desiredStructureLevel(item.unitType); + if (desired <= 1) return base; + const multiplier = this.game + .config() + .structureUpgradeCostMultiplier(item.unitType); + return aggregateStructureBuildCost( + this.game.config(), + this.game.myPlayer()!, + item.unitType, + desired, + multiplier, + ); + } + // Units: apply configured per-step multiplier for upgradeable combat units + if (isUpgradeableUnit(item.unitType)) { + const desired = this._desiredUnitLevel(item.unitType); + if (desired <= 1) return base; + const multiplier = this.game + .config() + .unitUpgradeCostMultiplier(item.unitType); + return aggregateStructureBuildCost( + this.game.config(), + this.game.myPlayer()!, + item.unitType, + desired, + multiplier, + ); + } + return base; } - private _desiredLevel(type: UnitType): number { + private _desiredStructureLevel(type: UnitType): number { try { const raw = localStorage.getItem("buildSettings.levels"); if (!raw) return 1; @@ -531,6 +574,20 @@ export class BuildMenu extends LitElement { } } + private _desiredUnitLevel(type: UnitType): number { + try { + const raw = localStorage.getItem("unitUpgradeSettings.levels"); + if (!raw) return 1; + const obj = JSON.parse(raw); + const key = String(type); + const val = obj?.[key]; + if (typeof val !== "number" || val < 1) return 1; + return Math.min(maxUnitLevel(type), val); + } catch (_) { + return 1; + } + } + private count(item: BuildItemDisplay): string { const player = this.game?.myPlayer(); if (!player) { @@ -576,8 +633,10 @@ export class BuildMenu extends LitElement { const price = this.game && this.game.myPlayer() ? this.cost(item) : 0; const desiredLevel = isUpgradeableStructure(item.unitType) - ? this._desiredLevel(item.unitType) - : 1; + ? this._desiredStructureLevel(item.unitType) + : isUpgradeableUnit(item.unitType) + ? this._desiredUnitLevel(item.unitType) + : 1; return html` isSpriteReady(unitView.type())) .forEach((unitView) => { - // Use cached sprite size to limit clear area to near sprite bounds + // Compute the same geometry used during draw to clear sprite + dot overlays const spriteSize = this.getSpriteSize(unitView); - const padding = 2; // small safety margin - const clearsize = spriteSize + padding; + const sizeMult = this.effectiveSizeMultiplier(unitView); + const newWidth = spriteSize * sizeMult; + const newHeight = spriteSize * sizeMult; + + // Badge overlay parameters: badge sits 1px outside top-right + const level = (unitView as any).level ? (unitView as any).level() : 1; + const badgeSize = Math.max(2, Math.min(3, Math.round(newWidth * 0.18))); + const offset = 1; + const overlayTop = badgeSize + offset; // extend upwards to cover outside badge + const extraRight = badgeSize + offset; // full right-side extension beyond sprite + + const padding = 2; // small safety margin around computed bounds + const maxHalfWidth = newWidth / 2 + extraRight; const lastX = this.game.x(unitView.lastTile()); const lastY = this.game.y(unitView.lastTile()); const angle = angleByUnit.get(unitView) ?? null; @@ -359,12 +374,14 @@ export class UnitLayer implements Layer { this.context.rotate(angle); this.context.translate(-lastX, -lastY); } - this.context.clearRect( - lastX - clearsize / 2, - lastY - clearsize / 2, - clearsize, - clearsize, - ); + + // Clear an axis-aligned box in the rotated space that covers the sprite and the dots above + const left = lastX - maxHalfWidth - padding; + const top = lastY - newHeight / 2 - overlayTop - padding; + const width = maxHalfWidth * 2 + padding * 2; + const height = newHeight + overlayTop + padding * 2; + this.context.clearRect(left, top, width, height); + if (angle !== null) { this.context.restore(); } @@ -787,24 +804,46 @@ export class UnitLayer implements Layer { } const angle = angleByUnit?.get(unit) ?? this.getUnitAngle(unit); + const cx = Math.round(x); + const cy = Math.round(y); + const newWidth = sprite.width * sizeMult; + const newHeight = sprite.width * sizeMult; // Keep aspect ratio square + if (angle !== null) { this.context.save(); - this.context.translate(x, y); + this.context.translate(cx, cy); this.context.rotate(angle); - this.context.translate(-x, -y); + this.context.translate(-cx, -cy); } - const newWidth = sprite.width * sizeMult; - const newHeight = sprite.width * sizeMult; // Keep aspect ratio square - this.context.drawImage( sprite, - Math.round(x - newWidth / 2), - Math.round(y - newHeight / 2), + cx - newWidth / 2, + cy - newHeight / 2, newWidth, newHeight, ); + // Draw a tiny top-right corner badge offset 1px outside the sprite + const level = unit.level ? unit.level() : 1; + // Tier color mapping: 1→bronze, 2→silver, 3→gold, 4+→platinum + const tierColor = + level >= 4 + ? "#E5E4E2" /* platinum */ + : level === 3 + ? "#FFD700" /* gold */ + : level === 2 + ? "#C0C0C0" /* silver */ + : "#CD7F32"; /* bronze */ + // Badge size: crisp 2–3 px depending on sprite size + const badgeSize = Math.max(2, Math.min(3, Math.round(newWidth * 0.18))); + // Offset 1px to the right and 1px above the sprite's top-right corner + const offset = 1; + const badgeLeft = Math.round(cx + newWidth / 2 + offset); + const badgeTop = Math.round(cy - newHeight / 2 - badgeSize - offset); + this.context.fillStyle = tierColor; + this.context.fillRect(badgeLeft, badgeTop, badgeSize, badgeSize); + if (angle !== null) { this.context.restore(); } @@ -815,6 +854,9 @@ export class UnitLayer implements Layer { } } + // Previously used for colored borders; kept for potential future styling + // but currently unused after switching to dot indicators. + private getUnitAngle(unit: UnitView): number | null { const lastTile = unit.lastTile(); const currentTile = unit.tile(); @@ -880,4 +922,21 @@ export class UnitLayer implements Layer { this.spriteSizeCache.set(t, size); return size; } + + // Mirror draw-time size multiplier decisions for clearing + private effectiveSizeMultiplier(unit: UnitView): number { + if ( + unit.type() === UnitType.Submarine && + unit.owner() === this.game.myPlayer() + ) { + const isAttacking = (unit as any).isAttacking?.() ?? false; + const isDetected = (unit as any).isDetectedByNavalUnit?.() ?? false; + const isOnCooldown = (unit as any).isCooldown?.() ?? false; + const isVisibleToEnemies = isAttacking || isDetected || isOnCooldown; + if (!isVisibleToEnemies) { + return 0.75; + } + } + return 1.0; + } } diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 6ab967017..d47b830f3 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -447,6 +447,7 @@ export const MoveFighterJetIntentSchema = BaseIntentSchema.extend({ unitId: z.number(), tile: z.number(), }); + export const BomberIntentSchema = BaseIntentSchema.extend({ type: z.literal("bomber_intent"), targetID: ID.nullable(), // who to attack diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 5f9045c25..60687124f 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -179,6 +179,8 @@ export interface Config { // Structure upgrade cost multiplier per structure type (e.g., 0.8 for 80%) structureUpgradeCostMultiplier(type: UnitType): number; + // Unit upgrade cost multiplier per unit type (e.g., 0.2 for 20%) + unitUpgradeCostMultiplier(type: UnitType): number; cargoPlaneGold(dist: number): Gold; cargoPlaneSpawnRate(numberOfAirplanes: number): number; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index c4e90d120..5e2f717f1 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -1361,6 +1361,17 @@ export class DefaultConfig implements Config { return 1.0; } } + // --- Unit upgrade cost multipliers --- + unitUpgradeCostMultiplier(type: UnitType): number { + switch (type) { + case UnitType.Warship: + case UnitType.FighterJet: + case UnitType.Submarine: + return 0.2; // Default 20% per step for upgradeable units + default: + return 1.0; + } + } // --- Research system defaults --- // f(x) = A * investment^B, where investment is gold allocated to research this tick researchAlpha(): number { diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index 63182c136..a7e708e43 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -11,7 +11,11 @@ import { UpgradeType, } from "../game/Game"; import { TileRef } from "../game/GameMap"; -import { maxStructureLevel } from "../game/Upgradeables"; +import { + isUpgradeableUnit, + maxStructureLevel, + maxUnitLevel, +} from "../game/Upgradeables"; import { AirfieldExecution } from "./AirfieldExecution"; import { DefensePostExecution } from "./DefensePostExecution"; import { FighterJetExecution } from "./FighterJetExecution"; @@ -87,9 +91,11 @@ export class ConstructionExecution implements Execution { this.player, this.constructionType, this.desiredLevel, - this.mg - .config() - .structureUpgradeCostMultiplier(this.constructionType), + isUpgradeableUnit(this.constructionType) + ? this.mg.config().unitUpgradeCostMultiplier(this.constructionType) + : this.mg + .config() + .structureUpgradeCostMultiplier(this.constructionType), ); if (this.player.gold() < total) { console.warn( @@ -119,7 +125,11 @@ export class ConstructionExecution implements Execution { this.player, this.constructionType, this.desiredLevel, - this.mg.config().structureUpgradeCostMultiplier(this.constructionType), + isUpgradeableUnit(this.constructionType) + ? this.mg.config().unitUpgradeCostMultiplier(this.constructionType) + : this.mg + .config() + .structureUpgradeCostMultiplier(this.constructionType), ); if (this.player.gold() < totalCost) { console.warn( @@ -185,17 +195,26 @@ export class ConstructionExecution implements Execution { break; case UnitType.Warship: this.mg.addExecution( - new WarshipExecution({ owner: player, patrolTile: this.tile }), + new WarshipExecution( + { owner: player, patrolTile: this.tile }, + this.desiredLevel, + ), ); break; case UnitType.Submarine: this.mg.addExecution( - new SubmarineExecution({ owner: player, patrolTile: this.tile }), + new SubmarineExecution( + { owner: player, patrolTile: this.tile }, + this.desiredLevel, + ), ); break; case UnitType.FighterJet: this.mg.addExecution( - new FighterJetExecution({ owner: player, patrolTile: this.tile }), + new FighterJetExecution( + { owner: player, patrolTile: this.tile }, + this.desiredLevel, + ), ); break; case UnitType.Port: @@ -278,7 +297,9 @@ export class ConstructionExecution implements Execution { private computeDesiredLevel(type: UnitType, target?: number): number { if (target === undefined || target < 1) return 1; - const cap = maxStructureLevel(type); + const cap = isUpgradeableUnit(type) + ? maxUnitLevel(type) + : maxStructureLevel(type); return Math.max(1, Math.min(cap, target)); } diff --git a/src/core/execution/FighterJetExecution.ts b/src/core/execution/FighterJetExecution.ts index e4bde9c10..07124f871 100644 --- a/src/core/execution/FighterJetExecution.ts +++ b/src/core/execution/FighterJetExecution.ts @@ -23,6 +23,7 @@ export class FighterJetExecution implements Execution { constructor( private input: (UnitParams & OwnerComp) | Unit, + private desiredLevel: number = 1, ) {} init(mg: GameImpl): void { @@ -42,6 +43,11 @@ export class FighterJetExecution implements Execution { this.fighterJet = this.input.owner.buildUnit(UnitType.FighterJet, spawn, { patrolTile: this.input.patrolTile, }); + const lvl = Math.max(1, this.desiredLevel | 0); + if (lvl > 1) { + (this.fighterJet as any)._level = lvl; + this.mg.addUpdate(this.fighterJet.toUpdate()); + } } } diff --git a/src/core/execution/SubmarineExecution.ts b/src/core/execution/SubmarineExecution.ts index 6d6e1f1b1..dc8377463 100644 --- a/src/core/execution/SubmarineExecution.ts +++ b/src/core/execution/SubmarineExecution.ts @@ -7,6 +7,7 @@ import { UnitParams, UnitType, } from "../game/Game"; +import { GameImpl } from "../game/GameImpl"; import { TileRef } from "../game/GameMap"; import { PathFindResultType } from "../pathfinding/AStar"; import { PathFinder } from "../pathfinding/PathFinding"; @@ -16,17 +17,18 @@ import { ShellExecution } from "./ShellExecution"; export class SubmarineExecution implements Execution { private random: PseudoRandom; private submarine: Unit; - private mg: Game; + private mg: GameImpl; private pathfinder: PathFinder; private lastShellAttack = 0; private alreadySentShell = new Set(); constructor( private input: (UnitParams & OwnerComp) | Unit, + private desiredLevel: number = 1, ) {} init(mg: Game, ticks: number): void { - this.mg = mg; + this.mg = mg as GameImpl; this.pathfinder = PathFinder.Mini(mg, 10_000, true, 100); this.random = new PseudoRandom(mg.ticks()); if (isUnit(this.input)) { @@ -45,6 +47,11 @@ export class SubmarineExecution implements Execution { this.submarine = this.input.owner.buildUnit(UnitType.Submarine, spawn, { patrolTile: this.input.patrolTile, }); + const lvl = Math.max(1, this.desiredLevel | 0); + if (lvl > 1) { + (this.submarine as any)._level = lvl; + this.mg.addUpdate(this.submarine.toUpdate()); + } } } diff --git a/src/core/execution/UpgradeUnitExecution.ts b/src/core/execution/UpgradeUnitExecution.ts new file mode 100644 index 000000000..c7218da91 --- /dev/null +++ b/src/core/execution/UpgradeUnitExecution.ts @@ -0,0 +1,2 @@ +// Deprecated placeholder; 'upgrade_unit' execution removed. +export {}; diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index 0974f8d9d..ec678d778 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -8,6 +8,7 @@ import { UnitType, UpgradeType, } from "../game/Game"; +import { GameImpl } from "../game/GameImpl"; import { TileRef } from "../game/GameMap"; import { PathFindResultType } from "../pathfinding/AStar"; import { PathFinder } from "../pathfinding/PathFinding"; @@ -18,7 +19,7 @@ import { ShellExecution } from "./ShellExecution"; export class WarshipExecution implements Execution { private random: PseudoRandom; private warship: Unit; - private mg: Game; + private mg: GameImpl; private pathfinder: PathFinder; private lastShellAttack = 0; private alreadySentShell = new Set(); @@ -28,10 +29,11 @@ export class WarshipExecution implements Execution { constructor( private input: (UnitParams & OwnerComp) | Unit, + private desiredLevel: number = 1, ) {} init(mg: Game, ticks: number): void { - this.mg = mg; + this.mg = mg as GameImpl; this.pathfinder = PathFinder.Mini(mg, 10_000, true, 100); this.random = new PseudoRandom(mg.ticks()); if (isUnit(this.input)) { @@ -50,6 +52,11 @@ export class WarshipExecution implements Execution { this.warship = this.input.owner.buildUnit(UnitType.Warship, spawn, { patrolTile: this.input.patrolTile, }); + const lvl = Math.max(1, this.desiredLevel | 0); + if (lvl > 1) { + (this.warship as any)._level = lvl; + this.mg.addUpdate(this.warship.toUpdate()); + } } this.pseudoRandom = new PseudoRandom(this.warship.id()); } diff --git a/src/core/game/Upgradeables.ts b/src/core/game/Upgradeables.ts index 9b6b33f72..13931c5fa 100644 --- a/src/core/game/Upgradeables.ts +++ b/src/core/game/Upgradeables.ts @@ -11,10 +11,21 @@ export const UPGRADEABLE_STRUCTURES: ReadonlySet = new Set([ UnitType.SAMLauncher, ]); +// Units that can be upgraded (placeholder list; logic TBD) +export const UPGRADEABLE_UNITS: ReadonlySet = new Set([ + UnitType.Warship, + UnitType.FighterJet, + UnitType.Submarine, +]); + export function isUpgradeableStructure(type: UnitType): boolean { return UPGRADEABLE_STRUCTURES.has(type); } +export function isUpgradeableUnit(type: UnitType): boolean { + return UPGRADEABLE_UNITS.has(type); +} + export function maxStructureLevel(type: UnitType): number { if (type === UnitType.MissileSilo || type === UnitType.SAMLauncher) { return 3; @@ -22,6 +33,20 @@ export function maxStructureLevel(type: UnitType): number { return isUpgradeableStructure(type) ? 99 : 1; } +// Return maximum upgrade level for upgradeable combat units. +// Warship & Submarine: 3 levels. Fighter Jet: 4 levels. Non-upgradeable units: 1. +export function maxUnitLevel(type: UnitType): number { + switch (type) { + case UnitType.FighterJet: + return 4; + case UnitType.Warship: + case UnitType.Submarine: + return 3; + default: + return 1; + } +} + // Resolve a UnitType value from a stored string value (String(UnitType.X)) export function tryParseUnitType(value: string): UnitType | null { for (const v of Object.values(UnitType) as UnitType[]) {