diff --git a/index.html b/index.html index a1f7473..c6f66ac 100644 --- a/index.html +++ b/index.html @@ -133,7 +133,6 @@ - diff --git a/src/game-v4.js b/src/game-v4.js index d6fc565..eed66ee 100644 --- a/src/game-v4.js +++ b/src/game-v4.js @@ -35,9 +35,12 @@ import { collectSalvage, createStarterProgression, deriveShipStats, + describeInventoryEntry, installModule, marketForPort, salvageForPirate, + scrapInventoryModule, + upgradePathForSlot, } from './progression/ship-loadout.js'; import { MODULE_CATALOG } from './progression/modules.js'; import { @@ -491,11 +494,23 @@ class GameScene extends Phaser.Scene { const salvage = wreck.getData?.('salvage'); if (!salvage) return; + const hullBefore = this.playerHull; + const repairAvailable = Math.max(0, salvage.repair || 0); + this.playerHull = Math.min( + this.playerStats?.maxHull || PLAYER_MAX_HULL, + this.playerHull + repairAvailable, + ); + const repairedHull = Math.max(0, this.playerHull - hullBefore); + this.progression = collectSalvage( this.progression, salvage, this.playerStats, ); + this.progression.lastSalvage = { + ...this.progression.lastSalvage, + repairedHull, + }; wreck.destroy(); const moduleName = salvage.moduleId @@ -504,14 +519,14 @@ class GameScene extends Phaser.Scene { const moduleCollected = this.progression.lastSalvage?.moduleCollected; const moduleText = moduleName ? moduleCollected - ? `\nSALVAGED: ${moduleName}` - : `\nHOLD FULL: ${moduleName}` + ? `\nMODULE STORED: ${moduleName}\nFIT OR SCRAP AT SHIPYARD` + : `\nSALVAGE HOLD FULL: ${moduleName}` : ''; this.status.setText( - `SALVAGE +¢${salvage.credits} +${salvage.scrap} SCRAP${moduleText}`, + `SALVAGE +¢${salvage.credits} +${salvage.scrap} SCRAP +${repairedHull} HULL${moduleText}`, ); - this.time.delayedCall(1400, () => { + this.time.delayedCall(2200, () => { if (this.gameState === 'playing') this.status.setText(''); }); } @@ -541,6 +556,7 @@ class GameScene extends Phaser.Scene { || Phaser.Input.Keyboard.JustDown(this.keys.closeShipyard); const purchaseRequested = this.pendingMobileActions.delete('purchase-module'); const mobileFit = this.pendingMobileActions.delete('fit-salvage'); + const scrapSalvage = this.pendingMobileActions.delete('scrap-salvage'); if (closeRequested && this.shipyardOpen) { this.shipyardOpen = false; @@ -592,25 +608,46 @@ class GameScene extends Phaser.Scene { } if (mobileFit || Phaser.Input.Keyboard.JustDown(this.keys.fitSalvage)) { - const moduleId = this.progression.inventory?.[0]; - if (!moduleId) { - this.status.setText('NO SALVAGED MODULES IN HOLD'); + if (!this.shipyardOpen || !docked) { + this.status.setText('FIT RECOVERED MODULES AT THE SHIPYARD'); } else { - const previousMaxHull = this.playerStats.maxHull; - this.progression = installModule(this.progression, moduleId); - this.applyProgressionStats(previousMaxHull); - const module = MODULE_CATALOG[moduleId]; - this.status.setText(`FITTED SALVAGE\n${module?.name || moduleId}`); - } - if (this.shipyardOpen) { + const salvageEntry = this.progression.inventory?.[0]; + const moduleId = typeof salvageEntry === 'string' + ? salvageEntry + : salvageEntry?.moduleId; + if (!moduleId) { + this.status.setText('SALVAGE HOLD IS EMPTY'); + } else { + const previousMaxHull = this.playerStats.maxHull; + this.progression = installModule(this.progression, moduleId); + this.applyProgressionStats(previousMaxHull); + const module = MODULE_CATALOG[moduleId]; + this.status.setText(`FITTED ${module?.name || moduleId}`); + } window.dispatchEvent(new CustomEvent('airship:shipyard-state', { detail: this.buildHudState(), })); } - this.time.delayedCall(1400, () => { - if (this.gameState === 'playing') this.status.setText(''); - }); } + + if (scrapSalvage) { + if (!this.shipyardOpen || !docked) { + this.status.setText('SCRAP RECOVERED MODULES AT THE SHIPYARD'); + } else { + const beforeScrap = this.progression.scrap || 0; + this.progression = scrapInventoryModule(this.progression, 0); + const gained = Math.max(0, (this.progression.scrap || 0) - beforeScrap); + this.status.setText( + this.progression.scrapped + ? `SALVAGED MODULE SCRAPPED +${gained} SCRAP` + : 'SALVAGE HOLD IS EMPTY', + ); + window.dispatchEvent(new CustomEvent('airship:shipyard-state', { + detail: this.buildHudState(), + })); + } + } + } createEnvironmentPhysics() { @@ -1014,6 +1051,7 @@ class GameScene extends Phaser.Scene { moduleId: null, scrap: Math.max(1, Math.round(cargoValue / 70)), credits: Math.max(15, Math.round(cargoValue * 0.45)), + repair: 0, condition: null, }; this.spawnCargoWreck(x, y, cargoSalvage); @@ -1661,8 +1699,14 @@ class GameScene extends Phaser.Scene { const marketUpgrade = docked ? bestAffordableMarketUpgrade(this.progression, portState.port) : null; - const salvageModuleId = this.progression?.inventory?.[0] || null; - const salvageModule = salvageModuleId ? MODULE_CATALOG[salvageModuleId] : null; + const salvageEntry = this.progression?.inventory?.[0] || null; + const salvageItem = describeInventoryEntry(salvageEntry); + const upgradePaths = Object.fromEntries( + ['engine', 'hull', 'cargo', 'rudder'].map((slot) => [ + slot, + upgradePathForSlot(this.progression.loadout, slot, marketStock), + ]), + ); return { gameState: this.gameState, @@ -1711,16 +1755,20 @@ class GameScene extends Phaser.Scene { id: module.id, name: module.name, slot: module.slot, + tier: module.tier ?? null, price: module.price, + effectSummary: module.effectSummary || '', })), + upgradePaths, marketUpgrade: marketUpgrade ? { id: marketUpgrade.id, name: marketUpgrade.name, price: marketUpgrade.price, } : null, - salvageModule: salvageModule ? { - id: salvageModule.id, - name: salvageModule.name, + salvageItem, + salvageModule: salvageItem ? { + id: salvageItem.moduleId, + name: salvageItem.name, } : null, }, world: { diff --git a/src/mobile-hud.js b/src/mobile-hud.js index aca9b4f..f8167d2 100644 --- a/src/mobile-hud.js +++ b/src/mobile-hud.js @@ -425,7 +425,7 @@ if (!isDesktopPointer.matches) { } #mobile-controls.controller-active .mobile-control.buy { - margin-left: -16%; + margin-left: 0; } #mobile-controls.controller-active .mobile-control.fit { @@ -551,7 +551,7 @@ if (!isDesktopPointer.matches) { } #mobile-controls.controller-active .mobile-control.buy { - margin-left: -86px; + margin-left: 0; } #mobile-controls.controller-active .mobile-control.fit { @@ -701,7 +701,6 @@ if (!isDesktopPointer.matches) { [...hud.querySelectorAll('[data-steam]')].map((el) => [el.dataset.steam, el]), ); const buyButton = root.querySelector('.mobile-control.buy'); - const fitButton = root.querySelector('.mobile-control.fit'); const radarContacts = radarPiece.querySelector('#steam-radar-contacts'); @@ -805,15 +804,10 @@ if (!isDesktopPointer.matches) { if (buyButton) { buyButton.hidden = !progression.docked; buyButton.disabled = !progression.docked; - buyButton.textContent = 'SHIPYARD'; - } - if (fitButton) { - const module = progression.salvageModule; - fitButton.hidden = !module; - fitButton.disabled = !module; - fitButton.textContent = module - ? `FIT\n${module.name}` - : 'FIT'; + const salvageCount = progression.inventoryCount || 0; + buyButton.textContent = salvageCount > 0 + ? `SHIPYARD\nSALVAGE ${salvageCount}` + : 'SHIPYARD'; } const statusText = String(state.status || '').trim(); diff --git a/src/progression/modules.js b/src/progression/modules.js index d4366c5..47d999e 100644 --- a/src/progression/modules.js +++ b/src/progression/modules.js @@ -16,34 +16,52 @@ export const SHIP_BASE_STATS = { export const MODULE_CATALOG = { 'stock-engine': { id: 'stock-engine', - name: 'Stock Twin Propellers', + name: 'Stock Engine', slot: 'engine', + tier: 0, rarity: 'common', price: 0, + effectSummary: 'Standard speed and acceleration', modifiers: {}, }, 'tuned-propellers': { id: 'tuned-propellers', - name: 'Engine Lv 1', + name: 'Engine Mk I', slot: 'engine', + tier: 1, rarity: 'improved', price: 800, + effectSummary: '+20 speed • +10 acceleration', modifiers: { maxSpeed: 20, acceleration: 10 }, }, 'corsair-overdrive': { id: 'corsair-overdrive', - name: 'Corsair Overdrive', + name: 'Engine Mk II', slot: 'engine', + tier: 2, rarity: 'rare', - price: 690, - modifiers: { maxSpeed: 34, acceleration: 18, turnRate: -5 }, + price: 1400, + effectSummary: '+38 speed • +20 acceleration • -2 handling', + modifiers: { maxSpeed: 38, acceleration: 20, turnRate: -2 }, }, + 'storm-turbines': { + id: 'storm-turbines', + name: 'Engine Mk III', + slot: 'engine', + tier: 3, + rarity: 'epic', + price: 2200, + effectSummary: '+55 speed • +32 acceleration • -4 handling', + modifiers: { maxSpeed: 55, acceleration: 32, turnRate: -4 }, + }, + 'standard-broadside': { id: 'standard-broadside', name: 'Standard Broadside', slot: 'weapon', rarity: 'common', price: 0, + effectSummary: 'Standard cannon battery', modifiers: {}, }, 'long-nine-cannons': { @@ -52,6 +70,7 @@ export const MODULE_CATALOG = { slot: 'weapon', rarity: 'improved', price: 360, + effectSummary: '+4 cannon damage • slower reload', modifiers: { cannonDamage: 4, reloadMs: 70 }, }, 'quicklock-breeches': { @@ -60,64 +79,160 @@ export const MODULE_CATALOG = { slot: 'weapon', rarity: 'rare', price: 620, + effectSummary: '+2 cannon damage • faster reload', modifiers: { cannonDamage: 2, reloadMs: -180 }, }, + 'timber-hull': { id: 'timber-hull', - name: 'Timber Hull', + name: 'Stock Hull', slot: 'hull', + tier: 0, rarity: 'common', price: 0, + effectSummary: 'Standard hull protection', modifiers: {}, }, 'reinforced-keel': { id: 'reinforced-keel', - name: 'Hull Plating Lv 1', + name: 'Hull Plating Mk I', slot: 'hull', + tier: 1, rarity: 'improved', price: 600, + effectSummary: '+28 hull • -7 speed', modifiers: { maxHull: 28, maxSpeed: -7 }, }, + 'armored-envelope': { + id: 'armored-envelope', + name: 'Hull Plating Mk II', + slot: 'hull', + tier: 2, + rarity: 'rare', + price: 1200, + effectSummary: '+55 hull • -9 speed', + modifiers: { maxHull: 55, maxSpeed: -9 }, + }, + 'ironclad-frame': { + id: 'ironclad-frame', + name: 'Hull Plating Mk III', + slot: 'hull', + tier: 3, + rarity: 'epic', + price: 2000, + effectSummary: '+85 hull • -12 speed', + modifiers: { maxHull: 85, maxSpeed: -12 }, + }, + + 'brass-compass': { + id: 'brass-compass', + name: 'Brass Compass', + slot: 'navigation', + rarity: 'common', + price: 0, + effectSummary: 'Standard navigation gear', + modifiers: {}, + }, 'stormglass-nav': { id: 'stormglass-nav', name: 'Stormglass Navigator', slot: 'navigation', rarity: 'rare', price: 560, + effectSummary: 'Reduced wind drift • +240 radar range', modifiers: { windExposure: -0.22, radarRange: 240 }, }, - 'brass-compass': { - id: 'brass-compass', - name: 'Brass Compass', - slot: 'navigation', - rarity: 'common', - price: 0, - modifiers: {}, - }, 'salvage-rig': { id: 'salvage-rig', name: 'Deck Salvage Rig', slot: 'utility', rarity: 'improved', price: 330, + effectSummary: '+2 salvage hold capacity', modifiers: { salvageCapacity: 2 }, }, + + 'standard-hold': { + id: 'standard-hold', + name: 'Stock Cargo Hold', + slot: 'cargo', + tier: 0, + rarity: 'common', + price: 0, + effectSummary: 'Standard cargo capacity', + modifiers: {}, + }, 'expanded-hold': { id: 'expanded-hold', - name: 'Cargo Hold Lv 1', + name: 'Cargo Hold Mk I', slot: 'cargo', + tier: 1, rarity: 'improved', price: 600, + effectSummary: '+4 cargo • -4 speed', modifiers: { cargoCapacity: 4, maxSpeed: -4 }, }, + 'merchant-hold': { + id: 'merchant-hold', + name: 'Cargo Hold Mk II', + slot: 'cargo', + tier: 2, + rarity: 'rare', + price: 1100, + effectSummary: '+8 cargo • +1 salvage slot • -6 speed', + modifiers: { cargoCapacity: 8, salvageCapacity: 1, maxSpeed: -6 }, + }, + 'deep-hold': { + id: 'deep-hold', + name: 'Cargo Hold Mk III', + slot: 'cargo', + tier: 3, + rarity: 'epic', + price: 1800, + effectSummary: '+12 cargo • +2 salvage slots • -9 speed', + modifiers: { cargoCapacity: 12, salvageCapacity: 2, maxSpeed: -9 }, + }, + + 'stock-rudder': { + id: 'stock-rudder', + name: 'Stock Rudder', + slot: 'rudder', + tier: 0, + rarity: 'common', + price: 0, + effectSummary: 'Standard handling', + modifiers: {}, + }, 'reinforced-rudder': { id: 'reinforced-rudder', - name: 'Rudder Lv 1', + name: 'Rudder Mk I', slot: 'rudder', + tier: 1, rarity: 'improved', price: 400, + effectSummary: '+12 handling', modifiers: { turnRate: 12 }, }, + 'balanced-rudder': { + id: 'balanced-rudder', + name: 'Rudder Mk II', + slot: 'rudder', + tier: 2, + rarity: 'rare', + price: 850, + effectSummary: '+22 handling • +6 braking', + modifiers: { turnRate: 22, brake: 6 }, + }, + 'gyro-rudder': { + id: 'gyro-rudder', + name: 'Rudder Mk III', + slot: 'rudder', + tier: 3, + rarity: 'epic', + price: 1500, + effectSummary: '+32 handling • +10 braking', + modifiers: { turnRate: 32, brake: 10 }, + }, }; export const STARTER_LOADOUT = { @@ -126,8 +241,8 @@ export const STARTER_LOADOUT = { hull: 'timber-hull', navigation: 'brass-compass', utility: null, - cargo: null, - rudder: null, + cargo: 'standard-hold', + rudder: 'stock-rudder', }; export const purchasableModules = () => Object.values(MODULE_CATALOG) diff --git a/src/progression/ship-loadout.js b/src/progression/ship-loadout.js index 33a524b..c095416 100644 --- a/src/progression/ship-loadout.js +++ b/src/progression/ship-loadout.js @@ -7,6 +7,11 @@ import { import { createSeededRandom } from '../world/seeded-random.js'; const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); +const romanTier = (tier) => ['Stock', 'Mk I', 'Mk II', 'Mk III'][tier] || `Mk ${tier}`; +const inventoryModuleId = (entry) => typeof entry === 'string' ? entry : entry?.moduleId || null; +const inventoryCondition = (entry) => typeof entry === 'string' + ? 1 + : clamp(Number(entry?.condition) || 1, 0.05, 1); export const createStarterProgression = () => ({ credits: 650, @@ -39,18 +44,82 @@ export const deriveShipStats = (loadout = STARTER_LOADOUT) => { return stats; }; +export const upgradePathForSlot = (loadout = STARTER_LOADOUT, slot, stock = []) => { + const path = Object.values(MODULE_CATALOG) + .filter((module) => module.slot === slot && Number.isInteger(module.tier)) + .sort((a, b) => a.tier - b.tier); + + const currentId = loadout?.[slot] || path[0]?.id || null; + const current = MODULE_CATALOG[currentId] || path[0] || null; + const currentTier = Number.isInteger(current?.tier) ? current.tier : 0; + const next = path.find((module) => module.tier === currentTier + 1) || null; + const stockIds = new Set((stock || []).map(({ id }) => id)); + + return { + slot, + maxTier: path.at(-1)?.tier || currentTier, + current: current ? { + id: current.id, + name: current.name, + tier: currentTier, + tierLabel: romanTier(currentTier), + effectSummary: current.effectSummary || '', + } : null, + next: next ? { + id: next.id, + name: next.name, + tier: next.tier, + tierLabel: romanTier(next.tier), + price: next.price, + effectSummary: next.effectSummary || '', + stocked: stockIds.has(next.id), + } : null, + }; +}; + +export const describeInventoryEntry = (entry) => { + const moduleId = inventoryModuleId(entry); + const module = MODULE_CATALOG[moduleId]; + if (!module) return null; + const condition = inventoryCondition(entry); + const scrapValue = Math.max( + 1, + Math.round(Math.max(100, module.price || 100) / 300 * (0.55 + condition)), + ); + + return { + moduleId, + name: module.name, + slot: module.slot, + tier: module.tier ?? null, + tierLabel: Number.isInteger(module.tier) ? romanTier(module.tier) : null, + effectSummary: module.effectSummary || '', + condition, + conditionPercent: Math.round(condition * 100), + scrapValue, + source: typeof entry === 'string' ? 'legacy' : entry?.source || 'salvage', + }; +}; + export const installModule = (progression, moduleId) => { const module = MODULE_CATALOG[moduleId]; if (!module) return { ...progression, installed: false, reason: 'unknown-module' }; + if (progression.loadout?.[module.slot] === moduleId) { + return { ...progression, installed: false, reason: 'already-installed' }; + } const inventory = [...(progression.inventory || [])]; - const inventoryIndex = inventory.indexOf(moduleId); + const inventoryIndex = inventory.findIndex((entry) => inventoryModuleId(entry) === moduleId); + const installedEntry = inventoryIndex >= 0 ? inventory[inventoryIndex] : null; if (inventoryIndex >= 0) inventory.splice(inventoryIndex, 1); const loadout = { ...(progression.loadout || STARTER_LOADOUT) }; const replaced = loadout[module.slot] || null; loadout[module.slot] = moduleId; - if (replaced && MODULE_CATALOG[replaced]?.price > 0) inventory.push(replaced); + + if (replaced && MODULE_CATALOG[replaced]?.price > 0) { + inventory.push({ moduleId: replaced, condition: 1, source: 'removed' }); + } return { ...progression, @@ -59,22 +128,35 @@ export const installModule = (progression, moduleId) => { installed: true, replaced, installedModule: moduleId, + installedCondition: installedEntry ? inventoryCondition(installedEntry) : 1, }; }; export const buyAndInstallModule = (progression, moduleId) => { const module = MODULE_CATALOG[moduleId]; if (!module || module.price <= 0) return { ...progression, purchased: false, reason: 'not-for-sale' }; + + if (Number.isInteger(module.tier)) { + const current = MODULE_CATALOG[progression.loadout?.[module.slot]]; + const currentTier = Number.isInteger(current?.tier) ? current.tier : 0; + if (module.tier !== currentTier + 1) { + return { ...progression, purchased: false, reason: 'tier-locked' }; + } + } + if ((progression.credits || 0) < module.price) { return { ...progression, purchased: false, reason: 'insufficient-credits' }; } - const purchased = { + const loadout = { ...(progression.loadout || STARTER_LOADOUT), [module.slot]: moduleId }; + return { ...progression, credits: progression.credits - module.price, - inventory: [...(progression.inventory || []), moduleId], + loadout, + purchased: true, + installed: true, + installedModule: moduleId, }; - return { ...installModule(purchased, moduleId), purchased: true }; }; export const marketForPort = (port) => { @@ -86,7 +168,10 @@ export const marketForPort = (port) => { return true; }); - return stock.sort((a, b) => a.price - b.price); + return stock.sort((a, b) => { + if (a.slot !== b.slot) return a.slot.localeCompare(b.slot); + return (a.tier ?? 99) - (b.tier ?? 99) || a.price - b.price; + }); }; export const salvageForPirate = ({ @@ -97,17 +182,23 @@ export const salvageForPirate = ({ } = {}) => { const random = createSeededRandom(`${seed}:${classKey}:${wave}:${sequence}`); const chance = classKey === 'captain' ? 0.86 : classKey === 'frigate' ? 0.62 : 0.42; - const modulePool = purchasableModules(); + const tierCap = classKey === 'captain' ? 3 : classKey === 'frigate' ? 2 : 1; + const modulePool = purchasableModules() + .filter((module) => !Number.isInteger(module.tier) || module.tier <= tierCap); const module = random() < chance ? modulePool[Math.floor(random() * modulePool.length)] : null; const scrap = 1 + Math.floor(random() * (classKey === 'captain' ? 8 : classKey === 'frigate' ? 5 : 3)); const credits = 20 + Math.floor(random() * (35 + wave * 8)); + const repairBase = classKey === 'captain' ? 10 : classKey === 'frigate' ? 7 : 4; + const repairRange = classKey === 'captain' ? 9 : classKey === 'frigate' ? 7 : 5; + const repair = repairBase + Math.floor(random() * repairRange) + Math.min(4, Math.floor(wave / 3)); return { moduleId: module?.id || null, scrap, credits, + repair, condition: module ? clamp(0.28 + random() * 0.67, 0.28, 0.95) : null, }; }; @@ -117,7 +208,11 @@ export const collectSalvage = (progression, salvage, stats = deriveShipStats(pro let moduleCollected = false; if (salvage?.moduleId && inventory.length < stats.salvageCapacity) { - inventory.push(salvage.moduleId); + inventory.push({ + moduleId: salvage.moduleId, + condition: salvage.condition ?? 1, + source: 'salvage', + }); moduleCollected = true; } @@ -130,11 +225,34 @@ export const collectSalvage = (progression, salvage, stats = deriveShipStats(pro }; }; +export const scrapInventoryModule = (progression, index = 0) => { + const inventory = [...(progression.inventory || [])]; + const entry = inventory[index]; + const details = describeInventoryEntry(entry); + if (!entry || !details) return { ...progression, scrapped: false, reason: 'empty-hold' }; + + inventory.splice(index, 1); + return { + ...progression, + inventory, + scrap: (progression.scrap || 0) + details.scrapValue, + scrapped: true, + lastScrap: details, + }; +}; + export const bestAffordableMarketUpgrade = (progression, port) => { const stats = deriveShipStats(progression.loadout); const candidates = marketForPort(port) + .filter((module) => { + if (!Number.isInteger(module.tier)) { + return progression.loadout?.[module.slot] !== module.id; + } + const current = MODULE_CATALOG[progression.loadout?.[module.slot]]; + const currentTier = Number.isInteger(current?.tier) ? current.tier : 0; + return module.tier === currentTier + 1; + }) .filter((module) => module.price <= (progression.credits || 0)) - .filter((module) => progression.loadout?.[module.slot] !== module.id) .map((module) => { const next = deriveShipStats({ ...progression.loadout, [module.slot]: module.id }); const score = (next.maxHull - stats.maxHull) * 0.7 diff --git a/src/shipyard-ui.js b/src/shipyard-ui.js index e57ccd0..ef3f187 100644 --- a/src/shipyard-ui.js +++ b/src/shipyard-ui.js @@ -6,10 +6,10 @@ import activeUrl from './assets/shipyard-ui/active.png?url'; import closeButtonUrl from './assets/shipyard-ui/close_button.png?url'; const UPGRADE_SLOTS = [ - { id: 'tuned-propellers', label: 'Engine Lv 1' }, - { id: 'reinforced-keel', label: 'Hull Plating Lv 1' }, - { id: 'expanded-hold', label: 'Cargo Hold Lv 1' }, - { id: 'reinforced-rudder', label: 'Rudder Lv 1' }, + { slot: 'engine', label: 'ENGINE' }, + { slot: 'hull', label: 'HULL' }, + { slot: 'cargo', label: 'CARGO' }, + { slot: 'rudder', label: 'RUDDER' }, ]; const dispatchAction = (action, extra = {}) => { @@ -58,9 +58,15 @@ overlay.innerHTML = `