Source: level_editor.js

// Level editor features
// (in progress)

LEVEL_EDITOR_WRAPPER = 'level-editor-wrapper'
LEVEL_EDITOR = 'level-editor'

EDITED_LEVEL = 'edited-level-wrapper'
EDITED_LEVEL_PLAY = 'edited-level-play'

const Mode = {
  Place: 0,
  Erase: 1,
}
const Element = {
  Floor: 0,
  Target: 1,
  Box: 2,
  Player: 3,
}

/** Creates sample level to edit
 * 
 * @param {number} width 
 * @param {number} height 
 */
function create_new_level(width = 5, height = 5) {
  let level = new Level()
  level.width = width
  level.height = height
  level.player = [2, 2]
  level.targets.push([2, 1])
  level.boxes.push([2, 3])
  return level
}


function edit_level(level, game_state) {
  level = clone_level(level)

  close_all_menus()
  show(LEVEL_EDITOR_WRAPPER)
  show(LEVEL_EDITOR)
  hide(EDITED_LEVEL_PLAY)

  let level_editor = document.getElementById(LEVEL_EDITOR)
  level_editor.querySelector('.back-btn').onclick = _ => {
    hide(LEVEL_EDITOR_WRAPPER)
    open_level_editor_main_menu()
  }

  let name_input = level_editor.querySelector('#level-name-input')
  name_input.value = level.name

  let width_input = level_editor.querySelector('#level-width')
  let height_input = level_editor.querySelector('#level-height')
  let resize_level_button = level_editor.querySelector('#resize-level-btn')
  width_input.value = level.width
  height_input.value = level.height
  resize_level_button.onclick = _ => {
    resize_level_or_show_error(level, width_input.value, height_input.value)
    level_display = draw_level(level, on_tile_click)
    place_display(document.getElementById(EDITED_LEVEL), level_display.element)
  }

  let play_button = level_editor.querySelector('#play-edited-level-btn')
  play_button.onclick =
    _ => {
      if (validate_level_or_show_error(level))
        play_edited_level(level, game_state)
    }

  let error = document.getElementById('edited-level-error')
  let success = document.getElementById('saved-edited-level')
  hide('saved-edited-level')
  hide('edited-level-error')

  let save_button = level_editor.querySelector('#save-level-btn')
  save_button.onclick = _ => {
    let name = name_input.value
    let existsting_level = get_user_level_by_name(name, game_state)
    if (name.length == 0) {
      error.innerHTML = 'Please enter at least one character'
      hide('saved-edited-level')
      show('edited-level-error')
    }
    else if (existsting_level != undefined && existsting_level.index != level.index) {
      error.innerHTML = 'Level with this name already exists'
      hide('saved-edited-level')
      show('edited-level-error')
    }
    else if (validate_level_or_show_error(level)) {
      level.name = name
      save_user_level(level, game_state)
      generate_level_editor_menu(game_state)
      save_game_state(game_state)
      error.innerHTML = ''
      show('saved-edited-level')
    }
  }

  let remove_button = level_editor.querySelector('#remove-level-btn')
  remove_button.onclick = _ => {
    remove_user_level(level, game_state)
    save_game_state(game_state)
    generate_level_editor_menu()
    hide(LEVEL_EDITOR)
    show(LEVEL_EDITOR_MENU)
    show(LEVEL_EDITOR_EDIT_MENU)
  }

  let mode_buttons = level_editor.querySelector('#mode-buttons').querySelectorAll('.btn')
  let element_buttons = level_editor.querySelector('#element-buttons').querySelectorAll('.btn')
  reset_buttons_state(mode_buttons, element_buttons)

  let current_mode = 0
  let current_element = 0

  for (let i = 0; i < mode_buttons.length; i++)
    mode_buttons[i].onclick = _ => {
      mode_buttons[current_mode].classList.remove('active')
      mode_buttons[i].classList.add('active')
      current_mode = i
    }

  for (let i = 0; i < element_buttons.length; i++)
    element_buttons[i].onclick = _ => {
      element_buttons[current_element].classList.remove('active')
      element_buttons[i].classList.add('active')
      current_element = i
    }


  let on_tile_click =
    tile => {
      hide('saved-edited-level')
      hide('edited-level-error')
      apply_edition(level, tile, current_mode, current_element)
      level_display = draw_level(level, on_tile_click)
      place_display(document.getElementById(EDITED_LEVEL), level_display.element)
    }

  level_display = draw_level(level, on_tile_click)
  place_display(document.getElementById(EDITED_LEVEL), level_display.element)
}


/** Resets level editor buttons to defautl state i.e. 'place floor'
 * 
 * @param {HTMLElement[]} mode_buttons 
 * @param {HTMLElement[]} element_buttons 
 */
function reset_buttons_state(mode_buttons, element_buttons) {
  for (let btn of mode_buttons)
    btn.classList.remove('active')
  mode_buttons[0].classList.add('active')

  for (let btn of element_buttons)
    btn.classList.remove('active')
  element_buttons[0].classList.add('active')
}


function validate_level_or_show_error(level) {
  if (!is_level_valid(level)) {
    let error = 'The level is invalid. <br> Level has to contain a player, at least one box, and as many boxes as targets'
    show_level_editor_error(error)
    return false
  }

  hide('edited-level-error')
  return true
}

function show_level_editor_error(innerHTML) {
  let error = document.getElementById('edited-level-error')
  error.innerHTML = innerHTML
  show('edited-level-error')
  hide('saved-edited-level')
}


/** Check whether the level is valid i.e.
  * Is player specified and there are as many boxes as targets
  */
function is_level_valid(level) {
  if (level.player === undefined) return false
  if (level.boxes.length == 0) return false
  if (level.boxes.length != level.targets.length) return false
  return true
}

/** Tries to change element at tile in given mode
 * 
 * @param {Level} level 
 * @param {number[]} tile 
 * @param {number} mode 
 * @param {number} element 
 */
function apply_edition(level, tile, mode, element) {
  if (mode == Mode.Place)
    place(level, tile, element)
  else
    erase(level, tile, element)
}

/** Tries to place element at tile
 * 
 * @param {Level} level 
 * @param {number[]} tile 
 * @param {number} element 
 */
function place(level, tile, element) {
  if (element == Element.Floor) {
    remove_wall(level, tile)
    remove_target(level, tile)
  }
  else if (element == Element.Target) {
    remove_wall(level, tile)
    remove_target(level, tile)
    level.targets.push(tile)
  }
  else if (element == Element.Box) {
    if (can_walk_into_without_pushing(level, tile)
      && !arrays_equal(tile, level.player))
      level.boxes.push(tile)
  }
  else if (element == Element.Player) {
    if (can_walk_into_without_pushing(level, tile))
      level.player = tile
  }
}

/** Tries to erase element at tile
 * 
 * @param {Level} level 
 * @param {number[]} tile 
 * @param {number} element 
 */
function erase(level, tile, element) {
  if (element == Element.Floor) {
    remove_target(level, tile)
    remove_box(level, tile)
    remove_wall(level, tile)
    if (arrays_equal(level.player, tile))
      level.player = undefined
    level.walls.push(tile)
  }
  else if (element == Element.Target) {
    let index = get_target_index(level, tile)
    if (index != undefined)
      level.targets.splice(index, 1)
  }
  else if (element == Element.Box) {
    let index = get_box_index(level, tile)
    if (index != undefined)
      level.boxes.splice(index, 1)
  }
  else if (element == Element.Player) {
    if (arrays_equal(level.player, tile))
      level.player = undefined
  }
}
/** Plays level without saving and upon completion goes back to editing
 * 
 * @param {Level} level 
 * @param {GameState} game_state 
 */
function play_edited_level(level, game_state) {
  show('edited-level-play')
  hide(LEVEL_EDITOR)
  element = document.getElementById('edited-level-play')
  element.querySelector('.back-btn').onclick =
    _ => edit_level(level, game_state)
  play_single_level(
    element,
    level,
    level,
    new DontSave(),
    _ => { },
    _ => {
      edit_level(level, game_state)
    },
  )
}

/** Try to resize level from provided text. If impossible show error
 * 
 * @param {Level} level 
 * @param {string} width_text 
 * @param {string} height_text 
 */
function resize_level_or_show_error(level, width_text, height_text) {
  let error = document.getElementById('edited-level-error')
  let width = parseInt(width_text)
  let height = parseInt(height_text)
  if (isNaN(width) || isNaN(height)) {
    error.innerText = 'Size has to be a number'
    show('edited-level-error')
  }
  else if (width < 1 || width > MAX_LEVEL_WIDTH || height < 1 || height > MAX_LEVEL_HEIGHT) {
    error.innerText = `Size has to be between 1x1 and ${MAX_LEVEL_WIDTH}x${MAX_LEVEL_HEIGHT}`
    show('edited-level-error')
  }
  else {
    resize_level(level, width, height)
    hide('edited-level-error')
  }
}