Source: game.js

// Main game module connecting core logic with HTML UI

GAME_FINISHED = 'game-finished-wrapper'
FINISH_GAME_BTN = 'finish-game-btn'
SATISFACTION_COUNTER = 'satisfaction-counter'
RESTART_BTN = 'restart-btn'
GAME_WRAPPER = 'game-wrapper'
GAME = 'game'


/** Keeps track of how many boxes are on the targets
* and displays it on the proper element
*/
class SatisfactionCounter {
  /**
   * 
   * @param {number} max_satisfaction 
   * @param {HTMLElement} element 
   */
  constructor(max_satisfaction, element) {
    this.satisfaction = 0
    this.max_satisfaction = max_satisfaction
    this.element = element
    this.update_element()
  }

  /** Increase number of satisfied boxes by number
   * 
   * @param {number} change 
   */
  add(change) {
    this.satisfaction += change
    this.update_element()
  }

  update_element() {
    this.element.innerText = `Satisfied: ${this.satisfaction} / ${this.max_satisfaction}`
  }
}

class BasicGameLogic {
  constructor(game_saver) { this.game_saver = game_saver }
  save_level(level) { this.game_saver.save_level(level) }
}

/** 
 * Starts level on the #game-wrapper element set in HTML
 */
function play_level_on_default_game_wrapper(
  level,
  original_level,
  level_saver,
  on_level_completed,
  after_level_completed
) {

  let element = document.getElementById('game-wrapper')
  remove_finish_button_from_game_wrapper()

  play_game_music()
  close_all_menus()
  open_game()
  link_back_to_main_menu_button(element)

  play_single_level(
    element,
    level,
    original_level,
    level_saver,
    on_level_completed,
    after_level_completed)
}

/** Play single level
 * 
 * @param {HTMLElement} element Element to put ui on 
 * @param {Level} level Initial state of the level
 * @param {Level} original_level Default (reset) state of the level
 * @param {LevelSaver} level_saver Object that saves levels
 * @param {function} on_level_completed Callback immediately on level completed
 * @param {function} after_level_completed Callback after user presses 'continue'
 */
function play_single_level(
  element,
  level,
  original_level,
  level_saver,
  on_level_completed,
  after_level_completed) {

  let ui = create_level_ui()
  let ui_wrapper = element.querySelector('.level-ui-wrapper')
  ui_wrapper.innerHTML = ''
  ui_wrapper.appendChild(ui)

  let level_data = { level: clone_level(level), display: draw_level(level) }
  let restart_button = ui.querySelector('.restart-btn')

  let logic = new BasicGameLogic(level_saver)
  logic.restart = restart_button.onclick =
    create_restart_function(
      element,
      original_level,
      level_saver,
      on_level_completed,
      after_level_completed)

  logic.complete = (level) => {
    on_level_completed(level)
    show_level_completed(level, after_level_completed)
  }

  logic.satisfaction_counter =
    create_satisfaction_counter(
      level,
      ui.querySelector('.satisfaction-counter'))

  play_level_at(element.querySelector('.level-wrapper'), level_data, logic)
}

/** Creates a basic level interface
 * 
 *  The interface consists of:
 *    - satisfaction counter
 *    - level display
 *    - information about controls
 *    - restart button
 */
function create_level_ui() {
  let ui = document.createElement('div')
  ui.classList.add('level-ui')

  let satisfaction_counter = document.createElement('div')
  satisfaction_counter.classList.add('satisfaction-counter')

  let level = document.createElement('div')
  level.classList.add('level-wrapper')

  let controls_text = document.createElement('span')
  controls_text.classList.add('controls-text')
  controls_text.innerHTML = 'Use arrows, WASD, or HJKL to move around <br> Press R to restart'

  let restart_button = document.createElement('button')
  restart_button.classList.add('restart-btn', 'btn')
  restart_button.innerHTML =
    `<ion-icon name='refresh-outline'></ion-icon>
     <span class='text'> Restart </span>`

  ui.appendChild(satisfaction_counter)
  ui.appendChild(level)
  ui.appendChild(controls_text)
  ui.appendChild(restart_button)

  return ui
}

/**
 * 
 * @param {HTMLElement} element 
 * @param {LevelData} level_data 
 * @param {GameLogic} logic 
 */
function play_level_at(
  element,
  level_data,
  logic,
) {
  place_display(element, level_data.display.element)
  link_controls(action => apply_game_action(action, level_data, logic))
}

function place_display(element, display_element) {
  element.innerHTML = ''
  element.appendChild(display_element)
}

function show_level_completed(level, after_level_completed) {
  let wrapper = document.getElementById('level-completed-wrapper')
  wrapper.classList.add('shown')
  document.getElementById('level-completed-continue-btn').onclick = _ => {
    wrapper.classList.remove('shown')
    after_level_completed(level)
  }
}

/** Initializes satisfaction counter
 * 
 * @param {Level} level 
 * @param {Element} element 
 */
function create_satisfaction_counter(level, element) {
  let satisfaction_counter = new SatisfactionCounter(level.boxes.length, element)
  satisfaction_counter.add(satisfied_boxes_count(level))
  return satisfaction_counter
}

function link_back_to_main_menu_button(game_element) {
  game_element.querySelector('.back-btn').onclick = _ => {
    unlink_controls()
    hide(GAME_WRAPPER)
    back_to_main_menu()
  }
}

function create_restart_function(
  element,
  original_level,
  level_saver,
  on_level_completed,
  after_level_completed) {
  return _ => {
    level_saver.save_level(original_level),
      play_single_level(
        element,
        clone_level(original_level),
        original_level,
        level_saver,
        on_level_completed,
        after_level_completed)
  }
}


/**
 * Play (continue) game - all levels sorted from easy to hard
 * @param {Game} game 
 * @param {GameState} game_state 
 * @param {Level[]} levels 
 */
function play_game(game, game_state, levels) {
  let game_saver = new BasicGameSaver(game, game_state)
  game_saver.save_game(game)
  let level = game.level;

  let on_level_completed = (level) => {
    game.score += level_score(level)
    let next = level.index + 1
    if (next < levels.length) {
      game.level = levels[next]
      game_saver.save_game(game)
    }
    else
      game_saver.finish_game(game)
  }

  let after_level_completed = (level) => {
    if (level.index + 1 < levels.length)
      play_game(game, game_state, levels)
    else
      finish_game(game, game_state, levels)
  }

  play_level_on_default_game_wrapper(
    level,
    levels[level.index],
    game_saver,
    on_level_completed,
    after_level_completed
  )

  add_finish_button_to_level_game_wrapper(game, game_saver, game_state, levels)
}

/**
 * Adds 'finish game' button
 * 
 * @param {Game} game 
 * @param {GameSaver} game_saver
 * @param {GameState} game_state 
 * @param {Level[]} levels 
 */
function add_finish_button_to_level_game_wrapper(game, game_saver, game_state, levels) {
  let finish_button = document.createElement('button')
  finish_button.id = 'finish-game-btn'
  finish_button.classList.add('btn', 'warning')
  finish_button.innerText = 'Finish game'
  finish_button.onclick =
    _ => {
      game_saver.finish_game(game)
      finish_game(game, game_state, levels)
    }

  let ui = document.getElementById('game-wrapper')
  let old_button = ui.querySelector('#finish-game-btn')
  if (old_button == null)
    ui.appendChild(finish_button)
}

/**
 * Removes 'finish game' button
 */
function remove_finish_button_from_game_wrapper() {
  let ui = document.getElementById('game-wrapper')
  let button = ui.querySelector('#finish-game-btn')
  if (button != undefined)
    ui.removeChild(button)
}

// Displays information after finishing the game
function show_finish_game_modal(game, game_state) {
  show(GAME_FINISHED)
  let score = document.getElementById('finished-game-score')
  score.innerText = `Score: ${game.score}`

  let continue_button = document.getElementById('game-finished-continue-btn')
  let view_ranking_button = document.getElementById('game-finished-view-ranking-btn')

  continue_button.onclick = _ => {
    hide(GAME_FINISHED)
    back_to_main_menu()
  }

  view_ranking_button.onclick = _ => {
    hide(GAME_FINISHED)
    show_ranking(game_state)
  }
}

// Finishes game, it can be caused either by user
// or when all levels are completed
function finish_game(game, game_state, levels) {
  unlink_controls()
  close_game()
  back_to_main_menu()
  generate_all_levels_menu(game_state, levels, play_game)
  show_finish_game_modal(game, game_state, levels)
}


function level_score(level) {
  if (!level.completed) return 0
  let score = level.boxes.length * difficulty_bonus(level.difficulty)
  score *= score
  score /= level.moves // penalty for many moves
  return Math.round(score * 1000)
}

function difficulty_bonus(difficulty) {
  if (difficulty == EASY) return 1;
  if (difficulty == MEDIUM) return 4;
  if (difficulty == HARD) return 16;
}

function open_game() { show('game-wrapper') }
function close_game() { hide('game-wrapper') }