* Beschaffungsagent

Gibt gemäss Wachenbauplan Erweiterungen in Auftrag (coming soon) und beschafft Fahrzeuge

目前为 2024-10-16 提交的版本。查看 最新版本

// ==UserScript==
// @name        * Beschaffungsagent
// @namespace   bos-ernie.leitstellenspiel.de
// @version     1.0.0
// @license     BSD-3-Clause
// @author      BOS-Ernie
// @description Gibt gemäss Wachenbauplan Erweiterungen in Auftrag (coming soon) und beschafft Fahrzeuge
// @match       https://www.leitstellenspiel.de/buildings/*
// @icon        https://www.google.com/s2/favicons?sz=64&domain=leitstellenspiel.de
// @run-at      document-idle
// @grant       none
// @resource    https://forum.leitstellenspiel.de/index.php?thread/27183-script-beschaffungsagent/
// ==/UserScript==

(async function () {
  const unsupportedBuildingTypeIds = [1, 3, 4, 7, 8, 10];
  let currentVehicleTypes;

  let stationConfigurations = [];

  const containerId = "procurement-agent-container";
  const progressBarContainerId = "procurement-agent-progress-bar-container";

  const databaseName = "BosErnie_StationBlueprints";
  const objectStoreName = "main";
  const cacheKeyBlueprints = "blueprints";

  function openDB() {
    return new Promise((resolve, reject) => {
      const request = window.indexedDB.open(databaseName, 2);

      request.onerror = () => {
        reject("Failed to open the database");
      };

      request.onsuccess = event => {
        const db = event.target.result;

        resolve(db);
      };

      request.onupgradeneeded = event => {
        const db = event.target.result;

        if (!db.objectStoreNames.contains(objectStoreName)) {
          const objectStore = db.createObjectStore(objectStoreName);
          objectStore.createIndex("buildings", "buildings", { unique: false });
          objectStore.createIndex("vehicles", "vehicles", { unique: false });
        }
      };
    });
  }

  async function retrieveData(key) {
    const db = await openDB();

    return new Promise((resolve, reject) => {
      const transaction = db.transaction([objectStoreName], "readonly");
      const objectStore = transaction.objectStore(objectStoreName);

      const request = objectStore.get(key);

      request.onsuccess = event => {
        const data = event.target.result;
        resolve(data);
      };

      request.onerror = () => {
        reject("Failed to retrieve data");
      };
    });
  }

  async function initStationConfigurations() {
    stationConfigurations = await retrieveData(cacheKeyBlueprints);

    if (!stationConfigurations) {
      stationConfigurations = [];
    }
  }

  function getStationConfiguration(id) {
    return stationConfigurations[id];
  }

  function updateCurrentVehicleTypes() {
    const vehicleTypeIds = Array.from(document.getElementsByTagName("img"))
      .filter(e => e.getAttribute("vehicle_type_id") != null)
      .map(e => parseInt(e.getAttribute("vehicle_type_id"), 10)); // Parsing to Integer

    const frequencyMap = vehicleTypeIds.reduce((acc, id) => {
      acc[id] = (acc[id] || 0) + 1;
      return acc;
    }, {});

    currentVehicleTypes = Object.entries(frequencyMap).map(([id, quantity]) => ({
      id: parseInt(id, 10), // Ensure id is stored as an Integer
      quantity,
    }));
  }

  function getTotalNumberOfVehicles(vehicles) {
    return vehicles.reduce((acc, vehicle) => acc + vehicle.quantity, 0);
  }

  function isVehicleFleetComplete(vehicles) {
    for (let i = 0; i < vehicles.length; i++) {
      const vehicle = vehicles[i];
      let vehicleTypeAmount = currentVehicleTypes.filter(e => e.id === vehicle.id).length;
      if (vehicleTypeAmount < parseInt(vehicle.quantity)) {
        return false;
      }
    }

    return true;
  }

  async function completeVehicleFleet(target) {
    let vehicleBought = false;

    const buildingId = window.location.href.replace("https://www.leitstellenspiel.de/buildings/", "");

    const stationConfigurationId = target.getAttribute("vehicle-fleet-configuration-id");
    let stationConfiguration = getStationConfiguration(stationConfigurationId);

    if (isVehicleFleetComplete(stationConfiguration.vehicles)) {
      return;
    }

    initProgressBar(0, getTotalNumberOfVehicles(stationConfiguration.vehicles));
    for (let i = 0; i < stationConfiguration.vehicles.length; i++) {
      const vehicleConfiguration = stationConfiguration.vehicles[i];

      let numberCurrentVehicles = 0;
      const filteredCurrentVehicles = currentVehicleTypes.filter(e => e.id === vehicleConfiguration.id);
      if (filteredCurrentVehicles.length > 0) {
        numberCurrentVehicles = filteredCurrentVehicles[0].quantity;
      }

      if (numberCurrentVehicles === vehicleConfiguration.quantity) {
        advanceProgressBar(numberCurrentVehicles);
        continue;
      }

      const numberOfVehiclesToBuy = vehicleConfiguration.quantity - numberCurrentVehicles;

      for (let n = 0; n < numberOfVehiclesToBuy; n++) {
        advanceProgressBar();

        vehicleBought = true;

        await buyVehicle(buildingId, vehicleConfiguration.id);
      }
    }
    finishProgress();

    if (vehicleBought) {
      location.reload();
    }
  }

  async function buyVehicle(buildingId, vehicleTypeId) {
    await fetch(`/buildings/${buildingId}/vehicle/${buildingId}/${vehicleTypeId}/credits?building=${buildingId}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body: new URLSearchParams({
        _method: "get",
        authenticity_token: document.querySelector('meta[name="csrf-token"]').getAttribute("content"),
      }),
    });

    await new Promise(resolve => setTimeout(resolve, 100));
  }

  function initProgressBar(current, total) {
    const progress = document.createElement("div");
    progress.className = "progress-bar";
    progress.setAttribute("role", "progressbar");
    progress.setAttribute("aria-valuenow", current);
    progress.setAttribute("aria-valuemin", 0);
    progress.setAttribute("aria-valuemax", total);
    progress.style.width = (current / total) * 100 + "%";
    progress.innerText = current + " / " + total;

    const progressBar = document.createElement("div");
    progressBar.className = "progress";
    progressBar.style.margin = "0";
    progressBar.appendChild(progress);

    const progressBarContainer = document.getElementById(progressBarContainerId);
    progressBarContainer.appendChild(progressBar);
  }

  function getProgressBar() {
    return document.getElementById(progressBarContainerId).querySelector(".progress-bar");
  }

  function advanceProgressBar(increment = 1) {
    const progress = getProgressBar();

    const current = parseInt(progress.getAttribute("aria-valuenow"));
    const total = parseInt(progress.getAttribute("aria-valuemax"));
    const newCurrent = current + increment;

    progress.setAttribute("aria-valuenow", newCurrent.toString());
    progress.style.width = (newCurrent / total) * 100 + "%";
    progress.innerText = newCurrent + " / " + total;
  }

  function finishProgress() {
    const progress = getProgressBar();

    const total = parseInt(progress.getAttribute("aria-valuemax"));

    progress.setAttribute("aria-valuenow", total.toString());
    progress.style.width = "100%";
    progress.innerText = total + " / " + total;
  }

  function init(h1, buildingTypeId) {
    const buttonGroup = document.createElement("div");
    buttonGroup.className = "btn-group";

    for (const [id, configuration] of Object.entries(stationConfigurations)) {
      if (configuration.buildingTypeId !== buildingTypeId) {
        continue;
      }

      let className = "btn btn-default btn-xs";
      if (configuration.buildingNameRegexPattern) {
        const stationName = h1.innerText;
        if (stationName.match(new RegExp(configuration.buildingNameRegexPattern))) {
          className = "btn btn-primary btn-xs";
        }
      }

      let btn = document.createElement("a");
      btn.className = className;
      btn.setAttribute("vehicle-fleet-configuration-id", configuration.id);
      btn.innerText = configuration.name;

      if (configuration.enabled) {
        btn.addEventListener("click", function () {
          completeVehicleFleet(btn);
        });
      } else {
        btn.setAttribute("disabled", "disabled");
        btn.style.cursor = "not-allowed";
      }

      buttonGroup.appendChild(btn);
    }

    const progressBarContainer = document.createElement("div");
    progressBarContainer.id = progressBarContainerId;
    progressBarContainer.style.position = "relative";
    progressBarContainer.style.display = "inline-block";
    progressBarContainer.style.verticalAlign = "middle";
    progressBarContainer.style.marginLeft = "10px";
    progressBarContainer.style.width = "300px";

    const container = document.createElement("span");
    container.id = containerId;
    container.style.marginLeft = "10px";
    container.appendChild(buttonGroup);
    container.appendChild(progressBarContainer);

    const btnVehicleMarket = document.querySelector("a[href$='/vehicles/new']");
    btnVehicleMarket.parentNode.insertBefore(container, btnVehicleMarket.nextSibling);
  }

  async function main() {
    const h1 = document.querySelector("h1[building_type]");
    if (!h1) {
      return;
    }

    const buildingTypeId = parseInt(h1.getAttribute("building_type"));

    if (unsupportedBuildingTypeIds.indexOf(buildingTypeId) >= 0) {
      return;
    }

    await initStationConfigurations();
    init(h1, buildingTypeId);
    updateCurrentVehicleTypes();
  }

  main();
})();