Source: modules/Items.js

/** @module modules/Items */

import { Communicator } from "./Communicator.js";
import { Timer } from "./Timer.js";

/**
 * An item configuration that contains data required for DOM interactions.
 * @property {string} presentationName - The item name to be displayed in log entries.
 * @property {string} domElementId - The item's DOM element `id` attribute.
 * @property {number} startSpawnTimeSeconds - The time at which the event listener delegatee will
 * begin accepting click events when a new session is started. Defining a non-zero value results
 * in the item's event listener delegatee initially ignoring clicks until the item's spawn
 * interval has elapsed.
 * @property {number} spawnIntervalSeconds - The interval—in seconds—for which the
 * item's event listener delegatee ignores click events after it has accepted a click event.
 * @property {number} spawnTimeSeconds - The time—in session seconds—at which the item
 * will begin accepting clicks events.
 * @property {string} backgroundColorClass - The name of a CSS class that defines a background
 * color.
 * @property {string} backgroundImageClass - The name of a CSS class that defines a background
 * image.
 * @typedef {Object} Item
 */

/**
* Manages items and interacts with DOM elements related to them. Item event listener delegatees can
* accept or ignore browser `click` events depending on the item state. Item DOM elements are
* dynamically added to the DOM. Item event listener delegatees are dynamically added to the class
* object when the item elements are added to the DOM.
* @extends Communicator
*/
export class Items extends Communicator {
  /**
   * @property {Element} items - The parent DOM element that contains item DOM elements.
   * @type {Object.<string, Element>}
   */
  domElements = {
    items: document.getElementsByTagName("items")[0],
  };
  static name = "Items";
  name = "Items";

  /**
   * Sets the properties required to initialize the DOM.
   * @see {@link Communicator}
   */
  constructor(messageProxy) {
    super(messageProxy);
  }

  /**
   * Sets the initial state.
   * @method
   */
  initialize = () => {
    this.items = [
      {
        presentationName: "Red Armor",
        domElementId: "itemArmorRed",
        startSpawnTimeSeconds: 0,
        spawnIntervalSeconds: 25,
        spawnTimeSeconds: 0,
        backgroundColorClass: "background-color-red",
        backgroundImageClass: "background-image-armor",
      },
      {
        presentationName: "Yellow Armor",
        domElementId: "itemArmorYellow",
        startSpawnTimeSeconds: 0,
        spawnIntervalSeconds: 25,
        spawnTimeSeconds: 0,
        backgroundColorClass: "background-color-yellow",
        backgroundImageClass: "background-image-armor",
      },
      {
        presentationName: "Megahealth",
        domElementId: "itemHealthMega",
        startSpawnTimeSeconds: 0,
        spawnIntervalSeconds: 35,
        spawnTimeSeconds: 0,
        backgroundColorClass: "background-color-blue",
        backgroundImageClass: "background-image-megahealth",
      },
    ];
  }

  #items;

  /**
   * The items' configurations.
   * @member
   * @type {Array.<Item>}
   * @variation 0
   */
  get items() {
    return this.#items;
  }

  /**
   * Sets the items, then creates the items' DOM elements.
   * @method
   * @param {Array.<Item>} items - The items' configurations to set.
   * @variation 1
   */
  set items(items) {
    this.#items = items;
    this.createItemsDomElements(items);
  }

  /**
   * Creates a DOM element for each item, styles the elements, then creates event listener
   * delegatees.
   * @param {Array.<Items.Item>} items - The item configurations for which to create DOM elements
   * and event listener delegatees.
   */
  createItemsDomElements(items) {
    items.forEach((item) => {
      // Set the initial item spawn time
      item.spawnTimeSeconds += item.startSpawnTimeSeconds;
      const element = document.createElement("div");
      element.setAttribute("id", item.domElementId);
      element.classList.add("item", item.backgroundColorClass, item.backgroundImageClass);
      element.textContent = item.spawnIntervalSeconds.toString();
      this.domElements.items.append(element);
      this.domElements[item.domElementId] = element;
      // Create the event delegatee
      this[this.constructEventMethodName(element)] = (event) => {
        this.itemsElementClick(event);
      };
    });
  }

  /** Clear the items DOM element. */
  resetItemsDomElementText() {
    this.domElements.items.textContent = "";
  }

  /**
   * An event listener delegatee that determines if the item is accepting clicks to add points to
   * the session score, then creates a click log entry. The initial points value of a clicked item
   * is the item's spawn interval. One point is deducted per second that a click time follows
   * the item's spawn time, to a maximum value of the item's spawn time, meaning that the minimum
   * number of points that can be scored is zero.
   * @method
   * @param {Event} event - The DOM click event.
   */
  itemsElementClick = (event) => {
    if (this.state[Timer].process === "start") {
      // Find the item in `this.items` using the event target element's ID
      const item = this.items.find((item) => {
        return item.domElementId === event.target.getAttribute("id");
      });
      const spawnTimeAtClick = item.spawnTimeSeconds;
      const clickTime = this.state[Timer].seconds || 0;
      // A difference < 0 is early, and will be rejected
      const difference = clickTime - item.spawnTimeSeconds;

      // Pass the event to the delegatee
      if (difference >= 0) {
        // The time, in seconds, the event delegatee will begin accepting clicks at this time
        // This value is relative to the value returned by `timer.seconds()`
        item.spawnTimeSeconds = clickTime + item.spawnIntervalSeconds;
        // Subtract a point per second that a click follows `item.spawnIntervalSeconds`
        // A maximum of `item.spawnIntervalSeconds` points can be subtracted
        const points = (
          item.spawnIntervalSeconds - Math.min(difference, item.spawnIntervalSeconds)
        );

        // Select a color for the log entry
        let color;

        if (difference === 0) {
          color = "color-blue";
        } else if (difference <= 3) {
          color = "color-green";
        } else if (difference <= 5) {
          color = "color-yellow";
        } else {
          color = "color-red";
        }

        this.send("points", points);
        this.send("log", {
          "entry": `
            ${item.presentationName}: ${Timer.formatTime(clickTime)}
            - ${Timer.formatTime(spawnTimeAtClick)} = ${Timer.formatTime(difference)}
            late
          `,
          "cssClass": color,
        });
      // The item element is not interactive
      } else {
        this.send("log", {
          "entry":`${item.presentationName} clicked ${Math.abs(difference)} seconds early`,
          "cssClass": "color-grey",
        });
      }
    }
  }

  /**
   * Clears the items DOM element, resets each item's spawn time to 0, then recreates the items' DOM
   * elements and event listener delegatees.
   * @method
   */
  processReceive = (object, property, value) => {
    if (value === "end") {
      this.resetItemsDomElementText();
      // Create a deep copy of `this.items`
      const items = structuredClone(this.items);

      items.forEach((item) => {
        item.spawnTimeSeconds = 0;
      });

      this.items = items;
    }
  }
}