<?php
/**
 * Explore
 *
 * @package OrbemStudio
 */

namespace OrbemStudio;

use WP_Error;
use WP_Post;

/**
 * Explore Class
 *
 * @package OrbemStudio
 */
class Explore
{

	/**
	 * Theme instance.
	 *
	 * @var Plugin
	 */
	public Plugin $plugin;

    /**
     * Class constructor.
     *
     * @param Plugin $plugin Plugin class.
     */
	public function __construct(Plugin $plugin)
    {
		$this->plugin          = $plugin;
        $this->plugin->explore = $this;
	}

    /**
     * Register API field.
     *
     * @action rest_api_init
     */
    public function createApiPostsMetaField(): void
    {
        $permission_callback = function() { return current_user_can( 'read' ); };
        $namespace           = 'orbemorder/v1';
        $google_function     = is_plugin_active('orbem-studio-pro/orbem-studio-pro.php') ? 'handleProGoogleOauthCallback' : 'handleGoogleOauthCallback';

        // Google oauth handle for logging in.
        register_rest_route($namespace, '/google-oauth-callback/', [
            'methods'             => 'POST',
            'callback'            => [$this, $google_function],
            'permission_callback' => '__return_true',
        ]);

        // Register route for getting event by location.
        register_rest_route($namespace, '/add-explore-points/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'addCharacterPoints'],
            'permission_callback' => $permission_callback
        ]);

        // Register route for saving storage item.
        register_rest_route($namespace, '/save-storage-item/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'saveStorageItem'],
            'permission_callback' => $permission_callback
        ]);

        // Register route for getting event by location.
        register_rest_route($namespace, '/area/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'getOrbemArea'],
            'permission_callback' => '__return_true',
        ]);

        // Register route for getting item description.
        register_rest_route($namespace, '/get-item-description/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'getItemDescription'],
            'permission_callback' => '__return_true',
        ]);

        // Register route for getting event by location.
        register_rest_route($namespace, '/coordinates/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'saveCoordinates'],
            'permission_callback' => $permission_callback
        ]);

        // Register route for getting event by location.
        register_rest_route($namespace, '/resetexplore/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'resetExplore'],
            'permission_callback' => $permission_callback
        ]);

        // Register route for getting event by location.
        register_rest_route($namespace, '/addspell/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'addSpell'],
            'permission_callback' => $permission_callback
        ]);

        // Register route for saving settings.
        register_rest_route($namespace, '/save-settings/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'saveSettings'],
            'permission_callback' => $permission_callback
        ]);

        // Register route for saving enemy info.
        register_rest_route($namespace, '/enemy/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'saveEnemy'],
            'permission_callback' => $permission_callback
        ]);

        // Register route for equipping new item.
        register_rest_route($namespace, '/equip-explore-item/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'equipNewItem'],
            'permission_callback' => $permission_callback
        ]);

        // Register route for saving completed missions.
        register_rest_route($namespace, '/mission/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'saveMission'],
            'permission_callback' => $permission_callback
        ]);

        // Save draggable drop position.
        register_rest_route($namespace, '/save-drag/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'saveDrag'],
            'permission_callback' => $permission_callback
        ]);

        // Save draggable drop position.
        register_rest_route($namespace, '/save-materialized-item/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'saveMaterializedItem'],
            'permission_callback' => $permission_callback
        ]);

        // Add character to crew list.
        register_rest_route($namespace, '/add-character/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'addCharacter'],
            'permission_callback' => $permission_callback
        ]);

        // Add character to crew list.
        register_rest_route($namespace, '/enable-ability/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'enableAbility'],
            'permission_callback' => $permission_callback
        ]);

        // Set previous cutscene area.
        register_rest_route($namespace, '/set-previous-cutscene-area/', [
            'methods'             => 'POST',
            'callback'            => [$this, 'setPreviousCutsceneArea'],
            'permission_callback' => $permission_callback
        ]);
    }

    /**
     * The logged in user id. get_current_user_id() is unreliable inside REST
     * calls, so the user object is the only consistent source.
     * @return int
     */
    public static function currentUserId(): int
    {
        $user = wp_get_current_user();

        return (int) $user->ID;
    }

    /**
     * The user id save state belongs to, or 0 when the game plays as a guest.
     * @return int
     */
    public static function playerId(): int
    {
        if ('No' === get_option('explore_require_login', false)) {
            return 0;
        }

        return self::currentUserId();
    }

    /**
     * Call back function for rest route that saves the previous cutscene area.
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function setPreviousCutsceneArea(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data     = $request->get_json_params();
        $cutscene = isset($data['cutscene']) ? sanitize_text_field(wp_unslash($data['cutscene'])) : '';

        update_user_meta($userid, 'explore_previous_cutscene_area', $cutscene);

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Call back function for rest route that adds spell to the explore_magic user meta
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function addSpell(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data     = $request->get_json_params();
        $spell_id = isset($data['spellid']) ? intval($data['spellid']) : 0;

        if (0 < $spell_id) {
            $explore_magic  = get_user_meta($userid, 'explore_magic', true);
            $explore_magic  = false === empty($explore_magic) ? $explore_magic : ['defense' => [], 'offense' => []];
            $spell_type     = get_the_terms($spell_id, 'magic-type');
            $the_spell_type = '';

            if (false === is_wp_error($spell_type) && true === is_array($spell_type)) {
                foreach($spell_type as $type) {
                    if (true === in_array($type->slug, ['defense', 'offense'], true)) {
                        $the_spell_type = $type->slug;
                    }
                }
            }

            if ( '' !== $the_spell_type ) {
                $explore_magic[$the_spell_type][] = $spell_id;

                update_user_meta($userid, 'explore_magic', $explore_magic);

                return rest_ensure_response( [
                    'success' => true,
                    'data'    => esc_html__('Spell added', 'orbem-studio'),
                ] );
            } else {
                return rest_ensure_response( [
                    'success' => false,
                    'data'    => esc_html__('No type selected', 'orbem-studio'),
                ] );
            }
        }

        return rest_ensure_response( [
            'success' => false,
            'data'    => esc_html__('User id or spell id invalid', 'orbem-studio'),
        ] );
    }

    /**
     * Call back function for rest route that adds points to user's explore game.
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response
     */
    public function addCharacterPoints(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data   = $request->get_json_params();
        $type   = isset($data['type']) ? sanitize_text_field($data['type']) : '';
        $item   = $data['item'] ?? '';
        $item   = is_array($item) ? array_map('sanitize_text_field', $item) : sanitize_text_field($item);
        $amount = isset($data['amount']) ? intval($data['amount']) : 0;
        $reset  = isset($data['reset']) && 'true' === $data['reset'];

        if ('' === $type || '' === $item) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Invalid request data', 'orbem-studio'),
            ]);
        }

        $this->savePoint($userid, $type, $amount, $item, $reset);

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Save explore points to array.
     *
     * @param $userid
     * @param $type
     * @param $amount
     * @param $item
     * @param bool $reset Is this being called by reset function.
     * @return void
     */
    public function savePoint($userid, $type, $amount, $item, bool $reset = false): void
    {
        if (0 < $userid && false === empty($item)) {
            $current_explore_points = get_user_meta($userid, 'explore_points', true);
            $explore_points         = false === empty($current_explore_points) && is_array($current_explore_points) ? $current_explore_points : [
                'health'  => ['points' => 100, 'positions' => []],
                'mana'    => ['points' => 100, 'positions' => []],
                'point'   => ['points' => 0, 'positions' => []],
                'money'   => ['points' => 0, 'positions' => []],
                'gear'    => ['positions' => []],
                'weapons' => ['positions' => []]
            ];

            if ('communicate' === $type) {
                $current_communicates = get_user_meta($userid, 'explore_received_communicates', true);
                $current_communicates = is_array($current_communicates) ? $current_communicates : [];
                $value_to_add         = is_array($item) && isset($item[0]) ? intval($item[0]) : intval($item);

                if (false === isset($current_communicates[$amount]) || false === in_array($value_to_add, $current_communicates[$amount], true)) {
                    $current_communicates[$amount][] = $value_to_add;
                }

                update_user_meta($userid, 'explore_received_communicates', $current_communicates);
            }

            if (true === $reset) {
                $explore_points['health']['points'] = 100;
                $explore_points['mana']['points']   = 100;
            }

            if ('communicate' !== $type) {
                $explore_points[$type]['points'] = $amount;
            } else {
                $type = 'point';
            }

            $type = sanitize_key($type);

            // Add position to list of positions received points on.
            if (true === is_array($item)) {
                if (!isset($explore_points[$type]['positions']) || !is_array($explore_points[$type]['positions'])) {
                    $explore_points[$type]['positions'] = [];
                }

                $existing_values = array_intersect($item, $explore_points[$type]['positions']);
                foreach( $existing_values as $existing_value ) {
                    $item_index = array_search($existing_value, $item, true);
                    unset($item[$item_index]);
                }

                $explore_points[$type]['positions'] = array_merge($explore_points[$type]['positions'], $item);
            } elseif (false === in_array($item, $explore_points[$type]['positions']) ) {
                $explore_points[$type]['positions'][] = sanitize_text_field($item);
            }

            update_user_meta($userid, 'explore_points', $explore_points);
        }
    }

    /**
     * Call back function for rest route that save draggable items positions when dropped.
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function saveDrag(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data  = $request->get_json_params();
        $top   = isset($data['top']) ? intval($data['top']) : '';
        $left  = isset($data['left']) ? intval($data['left']) : '';
        $slug  = isset($data['slug']) ? sanitize_text_field(wp_unslash($data['slug'])) : '';

        if ('' === $top || '' === $left || '' === $slug) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Missing data point', 'orbem-studio'),
            ]);
        }

        $current_explore_drag = get_user_meta($userid, 'explore_drag_items', true);

        if (false === empty($current_explore_drag)) {
            $current_explore_drag[$slug] = [
                'top' => $top,
                'left' => $left,
            ];
        } else {
            $current_explore_drag = [$slug => [
                'top' => $top,
                'left' => $left,
            ]];
        }

        update_user_meta($userid, 'explore_drag_items', $current_explore_drag);

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Call back function for rest route that save materialized items per location when triggered.
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response
     */
    public function saveMaterializedItem(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data  = $request->get_json_params();
        $area  = isset($data['area']) ? sanitize_text_field($data['area']) : '';
        $item  = $data['item'] ?? '';

        $current_materialized_items = get_user_meta($userid, 'explore_materialized_items', true);
        $current_materialized_items = false === empty($current_materialized_items) ? $current_materialized_items : [];
        $final_items                = [];

        if (true === is_array($item)) {
            foreach( $item as $value ) {
                if ( true === empty($final_items[$area][$value])) {
                    $final_items[$area][] = is_numeric($value) ? intval($value) : sanitize_text_field(wp_unslash($value));
                }
            }
        } elseif ('' !== $item) {
            if ( true === empty($final_items[$area][$item])) {
                $final_items[$area][] = is_numeric($item) ? intval($item) : sanitize_text_field(wp_unslash($item));
            }
        }

        update_user_meta($userid, 'explore_materialized_items', array_merge($current_materialized_items, $final_items));

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Call back function for rest route that save materialized items per location when triggered.
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response
     */
    public function enableAbility(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data              = $request->get_json_params();
        $slug              = isset($data['slug']) ? sanitize_text_field($data['slug']) : '';
        $current_abilities = get_user_meta($userid, 'explore_abilities', true);
        $current_abilities = false === empty($current_abilities) ? $current_abilities : [];

        if ( false === in_array($slug, $current_abilities, true) ) {
            $current_abilities[] = $slug;
        }

        update_user_meta($userid, 'explore_abilities', $current_abilities);

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Call back function for rest route that save draggable items positions when dropped.
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function addCharacter(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data  = $request->get_json_params();
        $slug  = isset($data['slug']) ? sanitize_text_field(wp_unslash($data['slug'])) : '';

        if ('' === $slug || 0 >= $userid) {
            return rest_ensure_response([
                'success' => false,
                'data' => esc_html__('Missing slug or user id', 'orbem-studio'),
            ]);
        }

        $current_characters = get_user_meta($userid, 'explore_characters', true);

        if (false === empty($current_characters) && false === in_array($slug, $current_characters, true)) {
            $current_characters[] = $slug;
        } elseif (true === empty($current_characters)) {
            $current_characters = [$slug];
        }

        update_user_meta($userid, 'explore_characters', $current_characters);

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Call back function for rest route that saves fallen enemies in game.
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function saveEnemy(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data     = $request->get_json_params();
        $health   = isset($data['health']) ? intval($data['health']) : '';
        $position = isset($data['position']) ? sanitize_text_field(wp_unslash($data['position'])) : '';

        if (0 !== $health || '' === $position || 0 >= $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Missing required data point', 'orbem-studio'),
            ]);
        }

        $explore_enemies = get_user_meta($userid, 'explore_enemies', true);

        if (false === empty($explore_enemies)) {
            $explore_enemies[] = $position;
        } else {
            $explore_enemies = [$position];
        }

        update_user_meta($userid, 'explore_enemies', $explore_enemies);

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Call back function for rest route that saves completed missions.
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function saveMission(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data    = $request->get_json_params();
        $mission = isset($data['mission']) ? sanitize_text_field(wp_unslash($data['mission'])) : '';

        if ('' === $mission || 0 >= $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Missing mission or invalid user', 'orbem-studio'),
            ]);
        }

        $explore_missions = get_user_meta($userid, 'explore_missions', true);
        $explore_missions = is_array($explore_missions) ? $explore_missions : [];

        if (false === empty($explore_missions)) {
            if (false === in_array($mission, $explore_missions, true)) {
                $explore_missions[] = $mission;
            }
        } else {
            $explore_missions = [$mission];
        }

        update_user_meta($userid, 'explore_missions', $explore_missions);

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Call back function for rest route that equips a new item on the player.
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function equipNewItem(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data             = $request->get_json_params();
        $type             = isset($data['type']) ? sanitize_key($data['type']) : '';
        $item_id          = isset($data['itemid']) ? intval($data['itemid']) : '';
        $amount           = isset($data['amount']) ? intval($data['amount']) : '';
        $unequip          = false === empty($data['unequip']) ? 'true' === sanitize_text_field(wp_unslash($data['unequip'])) : '';
        $current_equipped = get_user_meta($userid, 'explore_current_' . $type, true);
        $current_equipped = false === empty($current_equipped) ? $current_equipped : [];
        $the_effect_type  = '';

        if (!$item_id || !get_post($item_id)) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Invalid item', 'orbem-studio'),
            ]);
        }

        $effect_type = '' !== $item_id ? get_post_meta($item_id, 'explore-value-type', true) : '';

        if ( true === in_array($effect_type, ['mana', 'health', 'power'], true)) {
            $the_effect_type = $effect_type;
        }

        if (false === $unequip && false === empty($current_equipped[$the_effect_type])) {
            if (true === is_array($current_equipped[$the_effect_type])) {
                foreach ($current_equipped[$the_effect_type] as $current_array) {
                    if (false === in_array($item_id, array_keys($current_array), true)) {
                        $current_equipped[$the_effect_type][] = [$item_id => $amount];
                    }
                }
            }
        } elseif (true === $unequip && false === empty($current_equipped[$the_effect_type])) {
            $equip_position = array_search($item_id, $current_equipped[$the_effect_type]);
            unset($current_equipped[$the_effect_type][$equip_position]);
        }

        // Weapons.
        if ('' === $unequip && $current_equipped !== [$item_id]) {
            $current_equipped = [$item_id];
        }

        if (0 >= $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Invalid user', 'orbem-studio'),
            ]);
        }

        update_user_meta(
            $userid,
            'explore_current_' . $type,
            $current_equipped
        );

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('equipped', 'orbem-studio') . esc_html($item_id),
        ] );
    }

    /**
     * Call back function for rest route that storages items.
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function saveStorageItem(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data                 = $request->get_json_params();
        $default_weapon       = get_option('explore_default_weapon', false);
        $default_weapon_obj   = false !== $default_weapon ?
            get_posts(
                [
                    'name'             => sanitize_text_field($default_weapon),
                    'posts_per_page'   => 1,
                    'post_type'        => 'explore-weapon',
                    'suppress_filters' => false,
                    'post_status'      => 'publish',
                    'no_found_rows'    => true,
                ]
            ) :
            '';
        $default_weapon_obj_id = true === isset($default_weapon_obj[0]) ? $default_weapon_obj[0]->ID : false;
        $id                    = isset($data['id']) ? intval($data['id']) : '';
        $value                 = isset($data['value']) ? intval($data['value']) : '';
        $type                  = isset($data['type']) ? sanitize_text_field(wp_unslash($data['type'])) : '';
        $name                  = isset($data['name']) ? sanitize_text_field(wp_unslash($data['name'])) : '';
        $remove                = isset($data['remove']) && 'true' === sanitize_text_field(wp_unslash($data['remove']));

        if ('' === $type || '' === $name || '' === $value || 0 >= $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Missing data point', 'orbem-studio'),
            ]);
        }

        if ('' === $id || !get_post($id)) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Invalid post id', 'orbem-studio'),
            ]);
        }

        $menu_map = match($type) {
            'gear' => 'gear',
            'weapons' => 'weapons',
            default => 'items'
        };

        $current_storage_items = get_user_meta($userid, 'explore_storage', true);
        $current_storage_items = false === empty($current_storage_items) ? $current_storage_items : [];
        $item_subtype          = get_post_meta($id, 'explore-value-type', true);
        $subtype               = '';
        $menu_map_array        = $current_storage_items[$menu_map] ?? [];

        if ($type !== $item_subtype) {
            $subtype = $item_subtype;
        }

        // If remove is true then remove the provided item.
        if (true === $remove) {
            foreach($menu_map_array as $index => $storage_item) {
                $storage_item_name = $storage_item['name'] ?? '';

                if ($name === $storage_item_name) {
                    if (false === empty($storage_item['count']) && 1 < $storage_item['count']) {
                        $menu_map_array[$index]['count'] = $storage_item['count'] - 1;
                    } else {
                        unset($menu_map_array[$index]);
                    }
                }
            }
        } else {
            $new_item = [
                'id'    => $id,
                'name'  => $name,
                'type'  => $type,
                'value' => $value,
            ];

            if (false === empty($subtype)) {
                $new_item['subtype'] = $subtype;
            }

            $has_dupe             = false;
            $default_weapon_array = false !== $default_weapon_obj_id ? ['name' => $default_weapon, 'type' => 'weapons', 'id' => $default_weapon_obj_id] : [];

            if (true === empty($current_storage_items)) {
                // Initialise the FULL storage structure (not the single-type
                // array) so appending below doesn't nest one type inside
                // another.
                $current_storage_items = [
                    'items'   => [],
                    'weapons' => false === empty($default_weapon_array) ? [$default_weapon_array] : [],
                    'gear'    => [],
                ];
                $menu_map_array = $current_storage_items[$menu_map] ?? [];
            } else {
                foreach ($menu_map_array as $index => $item) {
                    if ($name === ($item['name'] ?? '')) {
                        $count                           = $item['count'] ?? 1;
                        $menu_map_array[$index]['count'] = 'weapons'=== $menu_map ? 1 : $count + 1;
                        $has_dupe                        = true;
                    }
                }
            }

            if (false === $has_dupe) {
                $menu_map_array[] = $new_item;
            }
        }

        $current_storage_items[$menu_map] = $menu_map_array;

        update_user_meta($userid, 'explore_storage', $current_storage_items);

        if (true === in_array($menu_map, ['gear', 'weapons'])) {
            $this->savePoint($userid, $menu_map, 0 , $name);
        }

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Call back function for rest route that saves game settings.
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function saveSettings(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data    = $request->get_json_params();
        $music   = isset($data['music']) ? intval($data['music']) : '';
        $sfx     = isset($data['sfx']) ? intval($data['sfx']) : '';
        $talking = isset($data['talking']) ? intval($data['talking']) : '';

        if (!is_numeric($music)|| !is_numeric($sfx) || !is_numeric($talking)) {
            return rest_ensure_response( [
                'success' => false,
                'data'    => esc_html__('Missing data point', 'orbem-studio'),
            ] );
        }

        update_user_meta($userid, 'explore_settings', ['music' => $music, 'sfx' => $sfx, 'talking' => $talking,]);

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Call back function for rest route that adds points to user's explore game.
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function getOrbemArea(\WP_REST_Request $request): \WP_REST_Response
    {
        $orbem_studio_userid = self::playerId();

        // Get request data.
        $data                    = $request->get_json_params();
        $position                = isset($data['position']) ? sanitize_title(wp_unslash($data['position'])) : '';
        $characters              = $data['characters'] ?? '';
        $orbem_studio_characters = [];

        if (true === is_array($characters)) {
            foreach( $characters as $value ) {
                if ( true === empty($orbem_studio_characters[$value])) {
                    $orbem_studio_characters[] = is_numeric($value) ? intval($value) : sanitize_text_field(wp_unslash($value));
                }
            }
        } elseif ('' !== $characters) {
            $orbem_studio_characters[] = is_numeric($characters) ? intval($characters) : sanitize_text_field(wp_unslash($characters));
        }

        $area     = '' !== $position ? get_posts([
            'post_type'        => 'explore-area',
            'name'             => $position,
            'posts_per_page'   => 1,
            'suppress_filters' => false,
            'post_status'      => 'publish',
            'no_found_rows'    => true,
        ]) : '';

        if (true === empty($area[0])) {
            return rest_ensure_response( [
                'success' => false,
                'data'    => esc_html__('Area not provided', 'orbem-studio'),
            ] );
        }

        $area_id                       = $area[0]->ID ?? false;
        $is_area_cutscene              = false !== $area_id ? get_post_meta($area_id, 'explore-is-cutscene', true) : false;
        $orbem_studio_explore_points   = self::getExplorePoints($position);
        $explore_cutscenes             = self::getExplorePosts($position, 'explore-cutscene');
        $explore_minigames             = self::getExplorePosts($position, 'explore-minigame');
        $orbem_studio_explore_missions = self::getExplorePosts($position, 'explore-mission');
        $explore_walls                 = self::getExplorePosts($position, 'explore-wall');
        $explore_explainers            = self::getExplorePosts($position, 'explore-explainer');
        $explore_abilities             = self::getExploreAbilities();
        $explore_ability               = get_user_meta($orbem_studio_userid, 'explore_abilities', true);

        // HTML generated internally from trusted templates.
        $map_items             = self::getMapItemHTML($orbem_studio_explore_points, $position);
        $explainers_menu       = self::getExplainerHTML($explore_explainers, 'menu');
        $explainers_map        = self::getExplainerHTML($explore_explainers, 'map');
        $explainers_fullscreen = self::getExplainerHTML($explore_explainers, 'fullscreen');
        $minigames             = self::getMinigameHTML($explore_minigames);
        $map_communicate       = self::getMapCommunicateHTML($position, $orbem_studio_userid);
        $map_cutscenes         = self::getMapCutsceneHTML($explore_cutscenes, $position, $orbem_studio_userid);
        $map_abilities         = self::getMapAbilitiesHTML($explore_abilities);

        $is_admin = user_can(self::currentUserId(), 'manage_options');
        $dev_mode = '';

        ob_start();
        include_once $this->plugin->dir_path . '/templates/style-scripts.php';
        $area_item_styles_scripts = ob_get_clean();

        ob_start();
        include_once $this->plugin->dir_path . '/templates/components/explore-missions.php';
        $map_missions = ob_get_clean();

        ob_start();
        include_once $this->plugin->dir_path . '/templates/components/explore-characters.php';
        $map_characters = ob_get_clean();

        update_user_meta($orbem_studio_userid, 'current_location', $position);

        // Only administrators can view dev mode.
        if ($is_admin) {
            $dev_mode  = Dev_Mode::getDevModeHTML();
        }

        $start_direction = false !== $area_id ? get_post_meta($area_id, 'explore-start-direction', true) : '';
        $start_direction = false === empty($start_direction) ? $start_direction : 'down';
        $area_width = get_post_meta($area_id, 'explore-area-width', true);
        $area_height = get_post_meta($area_id, 'explore-area-height', true);

        return rest_ensure_response([
            'success' => true,
            'data'    => [
                'map-items'               => $map_items,
                'minigames'               => $minigames,
                'map-cutscenes'           => $map_cutscenes,
                'map-missions'            => $map_missions,
                'map-characters'          => $map_characters,
                'map-communicate'         => $map_communicate,
                'map-explainers'          => $explainers_map,
                'menu-explainers'         => $explainers_menu,
                'fullscreen-explainers'   => $explainers_fullscreen,
                'map-abilities'           => $map_abilities,
                'explore-ability'         => $explore_ability,
                'map-item-styles-scripts' => $area_item_styles_scripts,
                'start-top'               => false !== $area_id ? get_post_meta($area_id, 'explore-start-top', true) : '',
                'start-left'              => false !== $area_id ? get_post_meta($area_id, 'explore-start-left', true) : '',
                'area-height'             => false !== $area_id ? false === empty($area_height) ? $area_height . 'px' : '100%' : '100%',
                'area-width'              => false !== $area_id ? false === empty($area_width) ? $area_width . 'px' : '100%' : '100%',
                'start-direction'         => $start_direction,
                'map-svg'                 => self::getMapSVG($area[0]),
                'is-cutscene'             => $is_area_cutscene,
                'area-physics'            => false !== $area_id ? get_post_meta($area_id, 'explore-physics', true) : '',
                'area-scale'              => false !== $area_id ? self::getAreaScale($area_id) : 1,
                'dev-mode'                => $dev_mode,
            ],
        ] );
    }

    /**
     * Call back function for rest route that returns item description.
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function getItemDescription(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Get request data.
        $data  = $request->get_json_params();
        $item  = isset($data['id']) ? intval($data['id']) : false;

        if (false === $item) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Item id not provided', 'orbem-studio'),
            ]);
        }

        // Get content from the new explore-area post type.
        $item_obj = get_post($item);

        if (!$item_obj || false === in_array($item_obj->post_type, ['explore-point', 'explore-weapon', 'explore-gear'])) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Item provided is in correct or non existent', 'orbem-studio'),
            ]);
        }

        // Check if equipped.
        $gear_equipped = get_user_meta($userid, 'explore_current_gear', true);
        // Raw content for game engine; do not apply WordPress filters.
        $content       = $item_obj->post_content;
        $type          = get_post_meta($item_obj->ID, 'explore-value-type', true);
        $item_type     = '';

        if (true === in_array($type, ['mana', 'health', 'power'], true)) {
            $item_type = $type;
        }

        // Check equipped gear. IF so change button to unequip.
        if (false === empty($gear_equipped[$item_type]) && true === is_array($gear_equipped[$item_type])) {
            foreach ($gear_equipped[$item_type] as $current_array) {
                if (true === in_array($item, array_keys($current_array), true)) {
                    // Modify button label for equipped items (content is controlled).
                    $content = str_replace(['Equip', 'equip', 'Ununequip'],
                        ['Unequip', 'unequip', 'Unequip'],
                        $content);
                }
            }
        }

        // Return the post content for the supplied item.
        return rest_ensure_response( [
            'success' => true,
            'data'    => $content,
        ] );
    }

    /**
     * Call back function for rest route that adds coordinates user's explore game.
     * @param \WP_REST_Request $request The arg values from rest route.
     * @return \WP_REST_Response
     */
    public function saveCoordinates(\WP_REST_Request $request): \WP_REST_Response
    {
        $userid = self::playerId();

        // Endpoint intentionally accessible to all authenticated users.
        if (0 === $userid) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('User not authenticated', 'orbem-studio'),
            ]);
        }

        // Get request data.
        $data = $request->get_json_params();

        if (
            ! isset($data['left'], $data['top']) ||
            ! is_numeric($data['left']) ||
            ! is_numeric($data['top'])
        ) {
            return rest_ensure_response( [
                'success' => false,
                'data'    => esc_html__('Incorrect coordinate provided', 'orbem-studio'),
            ] );
        }

        $left = intval($data['left']);
        $top  = intval($data['top']);

        update_user_meta($userid, 'current_coordinates', ['left'=>$left,'top'=>$top]);

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Call back function to reset explore game.
     * @return \WP_REST_Response
     */
    public function resetExplore(): \WP_REST_Response
    {
        $userid = self::playerId();

        if (0 === $userid) {
            return rest_ensure_response( [
                'success' => false,
                'data'    => esc_html__('Invalid user provided', 'orbem-studio'),
            ] );
        }

        delete_user_meta($userid, 'current_coordinates');
        delete_user_meta($userid, 'current_location');
        delete_user_meta($userid, 'explore_points');
        delete_user_meta($userid, 'explore_enemies');
        delete_user_meta($userid, 'explore_missions');
        delete_user_meta($userid, 'explore_storage');
        delete_user_meta($userid, 'explore_magic');
        delete_user_meta($userid, 'explore_current_gear');
        delete_user_meta($userid, 'explore_current_weapons');
        delete_user_meta($userid, 'explore_drag_items');
        delete_user_meta($userid, 'explore_characters');
        delete_user_meta($userid, 'explore_materialized_items');
        delete_user_meta($userid, 'explore_abilities');
        delete_user_meta($userid, 'explore_received_communicates');
        delete_user_meta($userid, 'explore_previous_cutscene_area');

        return rest_ensure_response( [
            'success' => true,
            'data'    => esc_html__('success', 'orbem-studio'),
        ] );
    }

    /**
     * Get map SVG content.
     *
     * @param WP_Post $explore_area
     * @return string|false
     */
    public static function getMapSVG(WP_Post $explore_area): false|string
    {
        $map_url = get_the_post_thumbnail_url($explore_area->ID, 'full');

        if (empty($map_url)) {
            return false;
        }

        $response = wp_remote_get($map_url, ['timeout'   => 10]);

        if (is_wp_error($response)) {
            return false;
        }

        $image_data = wp_remote_retrieve_body($response);

        if (empty($image_data)) {
            return false;
        }

        $position = strpos($image_data, '<svg');

        if (false === $position) {
            return false;
        }

        return substr($image_data, $position);
    }

    /**
     * Get SVG content from image URL.
     *
     * @param string $image_url
     * @return string
     */
    public static function getSVGCode(string $image_url): string
    {
        if (empty( $image_url)) {
            return '';
        }

        $args = ['timeout' => 10];

        if ( wp_get_environment_type() === 'local' ) {
            $args['sslverify'] = false;
        }

        $response = wp_remote_get($image_url, $args);

        if (is_wp_error($response)) {
            return '';
        }

        $image_data = wp_remote_retrieve_body($response);

        if (empty($image_data)) {
            return '';
        }

        $position = strpos($image_data, '<svg');

        if (false === $position) {
            return '';
        }

        return substr($image_data, $position);
    }

    /**
     * Grab all the points you can collide with.
     *
     * @param string $position The area to get game posts from.
     * @return int[]|WP_Post[]
     */
    public static function getExplorePoints($position): array
    {
        $position   = sanitize_key($position);
        $first_area = sanitize_key(get_option('explore_first_area', ''));
        $target_area = $position ?: $first_area;

        if ('' === $target_area) {
            return [];
        }

        $args = [
            'posts_per_page' => -1,
            'post_type'      => ['explore-weapon', 'explore-area', 'explore-point', 'explore-character', 'explore-enemy', 'explore-sign', 'explore-wall'],
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
            'meta_query'     => [
                'relation' => 'OR',
                [
                    'key'     => 'explore-area',
                    'value'   => $target_area,
                    'compare' => '=',
                ],
                [
                    'key'     => 'explore-area',
                    'value'   => 's:' . strlen($target_area) . ':"' . $target_area . '";',
                    'compare' => 'LIKE',
                ],
            ],
            'no_found_rows'  => true,
            'post_status'    => 'publish',
        ];

        $main_character_slug = get_option('explore_main_character');

        if (false === empty($main_character_slug)) {
            $cache_key         = 'orbem_main_char_id_' . $main_character_slug;
            $main_character_id = wp_cache_get($cache_key, 'orbem_studio');

            if (false === $main_character_id) {
                global $wpdb;
                $main_character_id = $wpdb->get_var($wpdb->prepare(
                        "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type = 'explore-character' AND post_status = 'publish' LIMIT 1",
                        $main_character_slug
                ));

                wp_cache_set($cache_key, $main_character_id ?? 0, 'orbem_studio', HOUR_IN_SECONDS);
            }

            if ($main_character_id) {
                $args['post__not_in'] = [(int) $main_character_id];
            }
        }

        return get_posts($args);
    }

    /**
     * Clear main character ID cache when option updates.
     *
     * @action update_option_explore_main_character
     */
    public function clearMainCharacterCache(string $old_value, string $new_value): void
    {
        if (!empty($old_value)) {
            wp_cache_delete('orbem_main_char_id_' . sanitize_key($old_value), 'orbem_studio');
        }

        if (!empty($new_value)) {
            wp_cache_delete('orbem_main_char_id_' . sanitize_key($new_value), 'orbem_studio');
        }
    }

    /**
     * Return post object of currently equipped weapon.
     * @param string $weapon_name
     *
     * @return WP_Post|null
     */
    public static function getWeaponByName(string $weapon_name): ?WP_Post
    {
        if ('' === $weapon_name) {
            return null;
        }

        $args = [
            'post_type'      => 'explore-weapon',
            'name'           => sanitize_key($weapon_name),
            'posts_per_page' => 1,
            'post_status'    => 'publish',
            'no_found_rows'  => true,
        ];

        $posts = get_posts($args);

        if (false === empty($posts[0])) {
            return $posts[0];
        }

        return null;
    }

    /**
     * The image shown in the player's hand for a weapon. Uses the optional
     * explore-weapon-held-image override when set, otherwise falls back to the
     * weapon's featured image (the same art used for the map collectible).
     *
     * @param int $weapon_id
     * @return string Image URL, or '' when none is available.
     */
    public static function getWeaponHeldImage(int $weapon_id): string
    {
        if (0 >= $weapon_id) {
            return '';
        }

        $held = get_post_meta($weapon_id, 'explore-weapon-held-image', true);

        if (false === empty($held)) {
            return $held;
        }

        $thumb = get_the_post_thumbnail_url($weapon_id);

        return false === empty($thumb) ? $thumb : '';
    }

    /**
     * The zoom factor for an area as a decimal (e.g. 160% -> 1.6). Authors set
     * a percentage in the "Area Scale" field; empty or 100 means no zoom (1).
     *
     * @param int $area_id
     * @return float
     */
    public static function getAreaScale(int $area_id): float
    {
        if (0 >= $area_id) {
            return 1.0;
        }

        $percent = intval(get_post_meta($area_id, 'explore-area-scale', true));

        return 0 < $percent ? $percent / 100 : 1.0;
    }

    /**
     * Grab all the points you can collide with.
     * @param string $position
     * @param string $post_type
     * @return int[]|WP_Post[]
     */
    public static function getExplorePosts(string $position, string $post_type): array
    {
        $allowed_post_types = [
            'explore-point',
            'explore-character',
            'explore-enemy',
            'explore-sign',
            'explore-wall',
            'explore-minigame',
            'explore-mission',
            'explore-cutscene',
            'explore-explainer',
        ];

        if (false === in_array($post_type, $allowed_post_types, true)) {
            return [];
        }

        $first_area  = sanitize_key(get_option('explore_first_area', ''));
        $target_area = sanitize_key(false === empty($position) ? $position : $first_area);

        if ('' === $target_area) {
            return [];
        }

        $args = [
            'post_type'      => $post_type,
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
            'meta_query'     => [
                'relation' => 'OR',
                [
                    'key'     => 'explore-area',
                    'value'   => $target_area,
                    'compare' => '=',
                ],
                [
                    'key'     => 'explore-area',
                    'value'   => 's:' . strlen($target_area) . ':"' . $target_area . '";',
                    'compare' => 'LIKE',
                ],
            ],
            'posts_per_page' => -1,
            'post_status'    => 'publish',
            'no_found_rows'  => true,
        ];

        return get_posts($args);
    }

    /**
     * Grab all the abilities you can unlock.
     * @return int[]|WP_Post[]
     */
    public static function getExploreAbilities(): array
    {
        $args = [
            'post_type'      => 'explore-magic',
            'posts_per_page' => -1,
            'post_status'    => 'publish',
            'no_found_rows'  => true,
        ];

        return get_posts($args);
    }

    /**
     * Add map item styles.
     *
     * @action wp_head
     * @return void
     */
    public function inlineExploreStyles(): void
    {
        $game_page = get_option('explore_game_page', '');

        if (false === empty($game_page) && is_page($game_page)) {
            include_once $this->plugin->dir_path . '/templates/head-code.php';
        }
    }

    /**
     * Build html for map items.
     * @param array $explore_points
     * @param string $current_location
     * @return string
     */
    public static function getMapItemHTML(array $explore_points, string $current_location): string
    {
        $userid = self::playerId();
        $dead_ones = '';

        if (0 !== $userid) {
            $dead_ones = get_user_meta($userid, 'explore_enemies', true);
        }

        $html               = '';
        $dead_ones          = false === empty($dead_ones) ? $dead_ones : [];
        $health             = '';
        $explore_enemy_type = '';
        $area             = sanitize_key($current_location);
        $all_missions     = get_posts(
            [
                'post_type'      => 'explore-mission',
                'posts_per_page' => -1,
                'no_found_rows'  => true,
                'post_status'    => 'publish',
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
                'meta_query'     => [
                    'relation' => 'OR',
                    [
                        'key'     => 'explore-area',
                        'value'   => $area,
                        'compare' => '='
                    ],
                    [
                        'key'     => 'explore-area',
                        'value'   => 's:' . strlen($area) . ':"' . $area . '";',
                        'compare' => 'LIKE'
                    ]
                ]
            ]
        );

        $mission_trigger_html = '';

        // Grab mission trigger points.
        if (true === is_array($all_missions)) {
            foreach ($all_missions as $mission) {
                $mission_trigger = get_post_meta($mission->ID, 'explore-mission-trigger', true);
                $mission_trigger = false === empty($mission_trigger) ? $mission_trigger : '';
                $trigger_left    = $mission_trigger['left'] ?? '0';
                $trigger_top     = $mission_trigger['top'] ?? '0';
                $trigger_height  = $mission_trigger['height'] ?? '0';
                $trigger_width   = $mission_trigger['width'] ?? '0';

                if (false === empty($mission_trigger['top'])) {
                    $mission_trigger_html .= '<div id="' . esc_attr($mission->ID) . '-t" class="mission-trigger wp-block-group map-item ' . esc_attr($mission->post_name) . '-mission-trigger-map-item is-layout-flow wp-block-group-is-layout-flow"';
                    $mission_trigger_html .= ' style="left:' . esc_attr($trigger_left) . 'px;top:' . esc_attr($trigger_top) . 'px;height:' . esc_attr($trigger_height) . 'px; width:' . esc_attr($trigger_width) . 'px;"';
                    $mission_trigger_html .= ' data-trigger="true" data-triggee="' . esc_attr($mission->post_name) . '"';
                    $mission_trigger_html .= ' data-meta="explore-mission-trigger"';
                    $mission_trigger_html .= '></div>';
                }
            }
        }

        if (false === empty($explore_points)) {
            foreach ($explore_points as $explore_point) {
                $explore_point_meta = [];
                $raw_meta = get_post_meta($explore_point->ID);

                foreach ($raw_meta as $key => $values) {
                    $explore_point_meta[$key] = maybe_unserialize($values[0] ?? null);
                }

                $missions       = [];
                $enemy_missions = [];

                if ('explore-enemy' === $explore_point->post_type) {
                    $health             = $explore_point_meta['explore-health'] ?? '';
                    $explore_enemy_type = $explore_point_meta['explore-enemy-type'] ?? '';

                    if (true === isset($dead_ones[$explore_point->post_name])) {
                        continue;
                    }
                }

                $boss_waves                     = $explore_point_meta['explore-boss-waves'] ?? '';
                $value                          = $explore_point_meta['explore-value'] ?? '';
                $reward_value                   = $explore_point_meta['explore-reward-value'] ?? '';
                $timer                          = $explore_point_meta['explore-timer'] ?? '';
                $type                           = $explore_point_meta['explore-value-type'] ?? '';
                $interaction_type               = $explore_point_meta['explore-interaction-type'] ?? '';
                $breakable                      = 'breakable' === $interaction_type;
                $collectable                    = 'collectable' === $interaction_type;
                $draggable                      = 'draggable' === $interaction_type;
                $is_hazard                      = 'hazard' === $interaction_type;
                $clickable                      = 'clickable' === $interaction_type;
                $is_strong                      = $explore_point_meta['explore-is-strong'] ?? '';
                $is_strong                      = false === empty($is_strong) ? $is_strong : false;
                $top                            = $explore_point_meta['explore-top'] ?? '';
                $left                           = $explore_point_meta['explore-left'] ?? '';
                $height                         = $explore_point_meta['explore-height'] ?? '';
                $width                          = $explore_point_meta['explore-width'] ?? '';
                $walking_path                   = $explore_point_meta['explore-path'] ?? '';
                $walking_speed                  = $explore_point_meta['explore-speed'] ?? '';
                $time_between                   = $explore_point_meta['explore-time-between'] ?? '';
                $remove_after_cutscene          = $explore_point_meta['explore-remove-after-cutscene'] ?? '';
                $remove_after_mission           = $explore_point_meta['explore-remove-after-mission'] ?? '';
                $repeat                         = $explore_point_meta['explore-repeat'] ?? '';
                $disappear                      = $explore_point_meta['explore-disappear'] ?? '';
                $layer                          = $explore_point_meta['explore-layer'] ?? '';
                $passable                       = (($explore_point_meta['explore-passable'] ?? '') === 'yes');
                $interacted_with                = $explore_point_meta['explore-interacted'] ?? '';
                $crew_mate                      = $explore_point_meta['explore-crew-mate'] ?? '';
                $hitbox_inset                   = $explore_point_meta['explore-hitbox-inset'] ?? '';
                $path_trigger                   = $explore_point_meta['explore-path-trigger'] ?? '';
                $path_trigger                   = false === empty($path_trigger) ? $path_trigger : '';
                $path_trigger_left              = false === empty($path_trigger['left']) ? $path_trigger['left'] : '';
                $path_trigger_top               = false === empty($path_trigger['top']) ? $path_trigger['top'] : '';
                $path_trigger_height            = false === empty($path_trigger['height']) ? $path_trigger['height'] : '';
                $path_trigger_width             = false === empty($path_trigger['width']) ? $path_trigger['width'] : '';
                $path_trigger_cutscene          = false === empty($path_trigger['cutscene']) ? $path_trigger['cutscene'] : '';
                $materialize_item_trigger       = $explore_point_meta['explore-materialize-item-trigger'] ?? '';
                $materialize_after_cutscene     = $explore_point_meta['explore-materialize-after-cutscene'] ?? '';
                $materialize_after_mission      = $explore_point_meta['explore-materialize-after-mission'] ?? '';
                $materialize_after_item         = $explore_point_meta['explore-materialize-after-item'] ?? '';
                $redirect_path                  = $explore_point_meta['explore-redirect-path'] ?? '';
                $wanderer                       = $explore_point_meta['explore-wanderer'] ?? '';
                $materialize_item_trigger       = $materialize_item_trigger ?? false;
                $is_materialized_item_triggered = self::isMaterializedItemTriggered($explore_point->post_name, $current_location, $userid);
                $has_minigame                   = $explore_point_meta['explore-minigame'] ?? '';
                $hazard_remove                  = false;
                $explore_attack                 = $explore_point_meta['explore-attack'] ?? '';
                $weapon_strength                = false === empty($explore_attack) ? wp_json_encode($explore_attack) : '""';
                $weapon_projectile              = $explore_point_meta['explore-projectile'] ?? '';
                $weapon_projectile              = false === empty($weapon_projectile) ? $weapon_projectile : 'no';
                $weapon_visibility              = $explore_point_meta['explore-weapon-visibility'] ?? '';
                $weapon_visibility              = false === empty($weapon_visibility) ? $weapon_visibility : 'attack';
                $weapon_motion                  = $explore_point_meta['explore-weapon-motion'] ?? '';
                $weapon_motion                  = false === empty($weapon_motion) ? $weapon_motion : 'swing';
                $weapon_facing                  = $explore_point_meta['explore-weapon-facing'] ?? '';
                // Held size falls back to the weapon's placement (map) size.
                $weapon_held_width              = false === empty($explore_point_meta['explore-weapon-held-width']) ? $explore_point_meta['explore-weapon-held-width'] : ($explore_point_meta['explore-width'] ?? '');
                $weapon_held_height             = false === empty($explore_point_meta['explore-weapon-held-height']) ? $explore_point_meta['explore-weapon-held-height'] : ($explore_point_meta['explore-height'] ?? '');
                $weapon_resting                 = $explore_point_meta['explore-weapon-resting-position'] ?? '';
                $weapon_resting                 = false === empty($weapon_resting) ? $weapon_resting : 'in-hand';
                $weapon_range                   = intval($explore_point_meta['explore-weapon-range'] ?? 0);
                $weapon_vertical_offset         = intval($explore_point_meta['explore-weapon-vertical-offset'] ?? 0);
                $weapon_proj_cfg                = $explore_point_meta['explore-weapon-projectile'] ?? [];
                $weapon_proj_cfg                = is_array($weapon_proj_cfg) ? $weapon_proj_cfg : [];
                $weapon_proj_image              = $weapon_proj_cfg['image-url'] ?? '';
                $weapon_proj_width              = $weapon_proj_cfg['width'] ?? '';
                $weapon_proj_height             = $weapon_proj_cfg['height'] ?? '';
                $weapon_proj_facing             = $explore_point_meta['explore-weapon-projectile-facing'] ?? '';
                $weapon_ammo_cost               = intval($explore_point_meta['explore-weapon-ammo-cost'] ?? 0);
                $weapon_sound                   = $explore_point_meta['explore-weapon-sound'] ?? '';
                $rotation                       = $explore_point_meta['explore-rotation'] ?? '';
                $item_image                     = get_the_post_thumbnail_url($explore_point->ID);
                $video_override                 = $explore_point_meta['explore-video-override'] ?? '';
                $item_gravity                   = $explore_point_meta['explore-gravity'] ?? '';

                // Create onload class:
                $path_onload = true === empty($path_trigger_left) && true === empty($path_trigger_cutscene) && ('explore-character' === $explore_point->post_type || 'explore-enemy' === $explore_point->post_type) ? ' path-onload' : '';
                $classes     = $path_onload;

                if ('explore-character' === $explore_point->post_type && $passable) {
                    $classes .= ' passable';
                }

                if (
                        true !== $clickable &&
                        true !== $breakable &&
                        true !== $collectable &&
                        true !== $draggable &&
                        $passable &&
                        'explore-point' === $explore_point->post_type &&
                        true === empty($materialize_after_cutscene)
                ) {
                    $classes .= ' no-point';
                }

                if (true === $clickable) {
                    $layer = intval($layer) + 2;
                }

                // If it's an enemy, and they have health show or if not an enemy show.
                if (
                    'explore-enemy' !== $explore_point->post_type ||
                    false === in_array($explore_point->post_name, array_keys($dead_ones), true)
                ) {
                    // Highjack top/left if draggable and has save drag values.
                    // Read-only user meta used for client-side rendering only.
                    if (true === $draggable && 0 < $userid) {
                        $current_drag = get_user_meta($userid, 'explore_drag_items', true);

                        if (false === empty($current_drag) && true === isset($current_drag[$explore_point->post_name])) {
                            $top = $current_drag[$explore_point->post_name]['top'] ?? '0';
                            $left = $current_drag[$explore_point->post_name]['left'] ?? '0';
                        }
                    }

                    // Add z index change if layer number is defined.
                    $layer = false === empty($layer) ? 'z-index: ' . esc_attr($layer) . ';' : '';

                    $html .= '<div style="' . esc_attr($layer) . 'transform: rotate(' . esc_attr($rotation) . 'deg);left:' . esc_attr($left) . 'px; top:' . esc_attr($top) . 'px;" id="' . esc_attr($explore_point->ID) . '" data-genre="' . esc_attr($explore_point->post_type) . '" data-type="' . esc_attr($type) . '" data-value="' . esc_attr($value) . '"';
                    $html .= ' data-image="' . esc_attr($item_image) . '"';

                    // Reward granted to the player for killing this enemy.
                    if ('explore-enemy' === $explore_point->post_type && false === empty($reward_value)) {
                        $html .= ' data-reward-value="' . esc_attr($reward_value) . '"';
                    }

                    if ('explore-area' === $explore_point->post_type) {
                        $map_url = $explore_point_meta['explore-map'] ?? '';

                        $html .= ' data-map-url="' . esc_attr($map_url) . '"';
                    }

                    // Walls carry their texture so devmode knows a textured wall
                    // is a visible surface (Show Hidden leaves it alone and it
                    // stays selectable/movable regardless of that toggle).
                    if ('explore-wall' === $explore_point->post_type) {
                        $wall_texture = $explore_point_meta['explore-texture'] ?? '';
                        $wall_custom  = $explore_point_meta['explore-custom-texture'] ?? '';

                        // "Custom" uses the uploaded image; any other preset
                        // (other than Invisible) uses that preset. Anything else
                        // — or Custom without an upload — stays invisible.
                        if ('custom' === $wall_texture && false === empty($wall_custom)) {
                            $wall_texture_attr = 'custom';
                        } elseif (false === empty($wall_texture) && 'invisible' !== $wall_texture && 'custom' !== $wall_texture) {
                            $wall_texture_attr = $wall_texture;
                        } else {
                            $wall_texture_attr = 'invisible';
                        }

                        $html .= ' data-texture="' . esc_attr($wall_texture_attr) . '"';
                    }

                    if (false === empty($wanderer)) {
                        $html .= ' data-wanderer="' . esc_attr($wanderer) . '"';
                    }

                    // Explore character crew mate.
                    if ('explore-character' === $explore_point->post_type && 'yes' === $crew_mate) {
                        $html .= ' data-crewmate="' . esc_attr($crew_mate) . '"';
                    }

                    // Explore character crew mate.
                    if ('explore-character' === $explore_point->post_type && '' !== $hitbox_inset) {
                        $html .= ' data-hitbox-inset="' . esc_attr($hitbox_inset) . '"';
                    }

                    // If hazard, add hazard class.
                    if ($is_hazard) {
                        $html .= ' data-hazard="true"';
                    }

                    // Is item breakable.
                    if (true === $breakable) {
                        $html .= ' data-breakable="true"';
                    }

                    // Redirect path.
                    if (false === empty($redirect_path)) {
                        $html .= ' data-redirect-path="' . esc_url($redirect_path) . '"';
                    }

                    // Will disappear?
                    $html .= ' data-disappear="' . esc_attr($disappear) . '"';

                    // Will be passable?
                    if (true === $passable && 'explore-character' !== $explore_point->post_type) {
                        $html .= ' data-passable="true"';
                    }

                    if (false === empty($height) && false === empty($width)) {
                        $html .= ' data-height="' . esc_attr($height) . '"';
                        $html .= ' data-width="' . esc_attr($width) . '"';
                    }

                    // Interacted with image.
                    if (false === empty($interacted_with)) {
                        $html .= ' data-interacted="' . esc_url($interacted_with) . '"';
                    }

                    if ('explore-point' === $explore_point->post_type && false === empty($timer['time']) && 0 < intval($timer['time'])) {
                        $html .= ' data-timer="' . esc_attr($timer['time']) . '"';
                        $html .= ' data-timertriggee="' . esc_attr($timer['trigger']) . '"';
                    }

                    // Has Minigame.
                    if (false === empty($has_minigame)) {
                        $html .= ' data-minigame="' . esc_attr($has_minigame) . '"';
                    }

                    // Is strong.
                    if ('yes' === $is_strong) {
                        $html .= ' data-isstrong="yes"';
                    }

                    // Respects gravity.
                    if ('yes' === $item_gravity) {
                        $html .= ' data-gravity="yes"';
                    }

                    // Get item and enemy triggered missions.
                    if (0 < count($all_missions)) {
                        foreach ($all_missions as $mission) {
                            $trigger_item       = get_post_meta($mission->ID, 'explore-trigger-item', true);
                            $trigger_focus      = get_post_meta($mission->ID, 'explore-trigger-focus', true);
                            $trigger_item       = is_array($trigger_item) && false === empty($trigger_item) ? array_keys($trigger_item)[0] : $trigger_item;
                            $trigger_item       = is_array($trigger_item) ? '' : $trigger_item;
                            $trigger_focus      = is_array($trigger_focus) && false === empty($trigger_focus) ? array_keys($trigger_focus)[0] : $trigger_focus;
                            $trigger_focus      = is_array($trigger_focus) ? '' : $trigger_focus;
                            $enemy_trigger_item = get_post_meta($mission->ID, 'explore-trigger-enemy', true);

                            $point = $explore_point->post_name;

                            if (!empty($trigger_item) && str_contains($point, $trigger_item)) {
                                $missions[] = $mission;
                            }

                            if (!empty($trigger_focus) && str_contains($point, $trigger_focus)) {
                                $missions[] = $mission;
                            }

                            if (is_array($enemy_trigger_item) ? array_key_exists($point, $enemy_trigger_item) : $point === $enemy_trigger_item) {
                                $enemy_missions[] = $mission;
                            }
                        }
                    }

                    // Is item attached to a mission.
                    if (false === empty($missions[0])) {
                        $html .= ' data-mission="' . esc_attr($missions[0]->post_name) . '"';

                        $hazard_remove = get_post_meta($missions[0]->ID, 'explore-hazard-remove', true);
                        $hazard_remove = false === empty($hazard_remove) && true === in_array($explore_point->post_name, explode(',', $hazard_remove));
                    }

                    if (false === empty($enemy_missions[0])) {
                        $html .= ' data-mission="' . esc_attr($enemy_missions[0]->post_name) . '"';

                        $hazard_remove = get_post_meta($enemy_missions[0]->ID, 'explore-hazard-remove', true);
                        $hazard_remove = false === empty($hazard_remove) && true === in_array($explore_point->post_name, explode(',', $hazard_remove));
                    }

                    $explore_path = false === empty($walking_path) ? wp_json_encode($walking_path) : '[{"top":"0","left":"0"}]';

                    if ($walking_speed) {
                        $html .= ' data-speed="' . esc_attr($walking_speed) . '"';
                    }

                    if ($time_between) {
                        $html .= ' data-timebetween="' . esc_attr($time_between) . '"';
                    }

                    if ('[{"top":"0","left":"0"}]' !== $explore_path && true === in_array($explore_point->post_type, ['explore-character', 'explore-enemy'])) {
                        $html .= ' data-path=\'' . esc_attr($explore_path) . '\'';

                        if ('yes' === $repeat) {
                            $html .= ' data-repeat="true"';
                        }

                        if (false === empty($path_trigger_cutscene)) {
                            $html .= ' data-trigger-cutscene="' . esc_attr($path_trigger_cutscene) . '"';
                        }
                    }

                    if ('explore-weapon' === $explore_point->post_type) {
                        $html .= ' data-strength=\'' . esc_attr($weapon_strength) . '\'';
                        $html .= ' data-projectile="' . esc_attr($weapon_projectile) . '"';
                        $html .= ' data-visibility="' . esc_attr($weapon_visibility) . '"';
                        $html .= ' data-motion="' . esc_attr($weapon_motion) . '"';
                        $html .= ' data-facing="' . esc_attr($weapon_facing) . '"';
                        $html .= ' data-resting="' . esc_attr($weapon_resting) . '"';
                        $html .= ' data-range="' . esc_attr($weapon_range) . '"';
                        $html .= ' data-vertical-offset="' . esc_attr($weapon_vertical_offset) . '"';
                        $html .= ' data-projectile-image="' . esc_url($weapon_proj_image) . '"';
                        $html .= ' data-projectile-width="' . esc_attr($weapon_proj_width) . '"';
                        $html .= ' data-projectile-height="' . esc_attr($weapon_proj_height) . '"';
                        $html .= ' data-projectile-facing="' . esc_attr($weapon_proj_facing) . '"';
                        $html .= ' data-ammo-cost="' . esc_attr($weapon_ammo_cost) . '"';
                        $html .= ' data-held-width="' . esc_attr($weapon_held_width) . '"';
                        $html .= ' data-held-height="' . esc_attr($weapon_held_height) . '"';
                        $html .= ' data-held-image="' . esc_url(self::getWeaponHeldImage($explore_point->ID)) . '"';
                        $html .= ' data-sound="' . esc_url($weapon_sound) . '"';
                    }

                    if (true === $collectable || 'explore-weapon' === $explore_point->post_type) {
                        $html .= ' data-collectable="true"';
                    }

                    if (true === $clickable) {
                        $html .= ' data-clickable="true"';
                    }

                    // Materialize this item after this cutscene.
                    if (false === empty($materialize_after_cutscene)) {
                        $html .= ' data-showaftercutscene="' . esc_attr($materialize_after_cutscene) . '"';
                    }

                    // Materialize this item after this mission.
                    if (false === empty($materialize_after_mission)) {
                        $html .= ' data-showaftermission="' . esc_attr($materialize_after_mission) . '"';
                    }

                    // Materialize this item after another item is interacted with.
                    if (false === empty($materialize_after_item)) {
                        $html .= ' data-showafteritem="' . esc_attr($materialize_after_item) . '"';
                    }

                    if (true === $draggable) {
                        $html .= ' data-draggable="true"';
                    }

                    if (true === $hazard_remove) {
                        $html .= ' data-removable="true"';
                    }

                    // Remove this after cutscene specified in data att.
                    if (false === empty($remove_after_cutscene)) {
                        if (is_array($remove_after_cutscene)) {
                            $remove_after_cutscene = wp_json_encode($remove_after_cutscene);
                        }


                        $html .= ' data-removeaftercutscene=\'' . esc_attr($remove_after_cutscene) . '\'';
                    }

                    if (false === empty($remove_after_mission)) {
                        $html .= ' data-removeaftermission=\'' . esc_attr($remove_after_mission) . '\'';
                    }

                    $pulse_wave = false;
                    $barrage_wave = false;

                    // Enemy specific data-points.
                    if ('explore-enemy' === $explore_point->post_type) {
                        $speed             = $explore_point_meta['explore-speed'] ?? '';
                        $enemy_speed       = $explore_point_meta['explore-enemy-speed'] ?? '';
                        $projectile_rate   = $explore_point_meta['explore-projectile-rate'] ?? '5000';
                        $enemy_weapon_type = $explore_point_meta['explore-weapon-weakness'] ?? '';
                        $attack_display_time = $explore_point_meta['explore-attack-display-time'] ?? '';
                        $damage_time         = 'end' === ($explore_point_meta['explore-damage-time'] ?? '') ? 'end' : 'start';

                        if (false === empty($attack_display_time)) {
                            $html .= ' data-attack-display-time="' . esc_attr($attack_display_time) . '"';
                        }

                        $html .= ' data-damage-time="' . esc_attr($damage_time) . '"';

                        if (false === empty($enemy_weapon_type)) {
                            $html .= ' data-weapon="' . esc_attr($enemy_weapon_type) . '"';
                        }

                        if (false === empty($projectile_rate)) {
                            $html .= ' data-rate="' . esc_attr($projectile_rate) . '"';
                        }

                        $html .= ' data-health="' . esc_attr($health) . '" data-healthamount="' . esc_attr($health) . '" data-enemyspeed="' . esc_attr($enemy_speed) . '" data-speed="' . esc_attr($speed) . '" data-enemy-type="' . esc_attr($explore_enemy_type) . '"';
                        $wave_html = ' data-waves="';

                        // Boss waves.
                        if (true === is_array($boss_waves)) {
                            foreach ($boss_waves as $boss_wave_name => $boss_wave) {
                                if ('pulse-wave' === $boss_wave_name && 'on' === $boss_wave) {
                                    $pulse_wave = true;
                                }

                                if ('projectile' === $boss_wave_name && 'on' === $boss_wave) {
                                    $barrage_wave = true;
                                }

                                $wave_html .= $boss_wave_name . ',';
                            }
                        }

                        // Add boss waves.
                        $html .= wp_kses_post($wave_html) . '"';

                        $html .= ' class="wp-block-group enemy-item ' . esc_attr($explore_point->post_name) . '-map-item is-layout-flow wp-block-group-is-layout-flow' . esc_attr($classes) . '"';
                    } else {
                        $html .= ' class="wp-block-group map-item ' . esc_attr($explore_point->post_name) . '-map-item is-layout-flow wp-block-group-is-layout-flow' . esc_attr($classes) . '"';
                    }

                    $html .= '>';

                    if ('explore-character' === $explore_point->post_type) {
                        $character_hurt_sound = $explore_point_meta['explore-character-hurt-sound'] ?? false;

                        if (false === empty($character_hurt_sound)) {
                            $html .= '<span class="hurt-sound">';
                            $html .= '<audio src="' . esc_url($character_hurt_sound) . '" id="' . esc_attr($explore_point->post_name) . '-hs"></audio>';
                            $html .= '</span>';
                        }
                    }

                    // If item is video.
                    if (false === empty($video_override)) {
                        $html .= '<video style="position:absolute;z-index: 1;width: 100%;height:100%;top:0; left:0;" src="' . esc_url($video_override) . '" autoplay loop muted></video>';
                    }

                    if ('explore-sign' === $explore_point->post_type) {
                        $html .= '<div class="focus-content">';
                    }

                    // Raw content for game engine; do not apply WordPress filters.
                    $html .= true === in_array($explore_point->post_type, ['explore-character', 'explore-sign'], true) ? wp_kses_post($explore_point->post_content) : '';

                    if ('explore-sign' === $explore_point->post_type) {
                        $html .= '</div>';
                    }

                    // Projectile html for enemy.
                    if ('explore-enemy' === $explore_point->post_type && ('shooter' === $explore_enemy_type || true === $barrage_wave)) {
                        $projectile = $explore_point_meta['explore-projectile'] ?? '';

                        if (false !== $projectile) {
                            $projectile_width = $projectile['width'] ?? '0';
                            $projectile_height = $projectile['height'] ?? '0';
                            $projectile_image_url = $projectile['image-url'] ?? '';
                            $projectile_facing = $explore_point_meta['explore-projectile-facing'] ?? '';

                            $html .= '<div class="projectile" data-value="' . esc_attr($value) . '" data-projectile-facing="' . esc_attr($projectile_facing) . '"><img alt="projectile" style="width:' . esc_attr($projectile_width) . 'px; height: ' . esc_attr($projectile_height) . 'px;" src="' . esc_url($projectile_image_url) . '" /></div>';
                        }
                    }

                    if (true === in_array($explore_point->post_type, ['explore-character', 'explore-enemy'], true)) {
                        $character_info = self::getCharacterImages($explore_point, '');
                        $direction_images = $character_info['direction_images'] ?? false;

                        if ('explore-enemy' === $explore_point->post_type) {
                            $enemy_sound       = $explore_point_meta['explore-enemy-sound'] ?? false;
                            $enemy_hurt_sound  = $explore_point_meta['explore-enemy-hurt-sound'] ?? false;

                            $html .= '<div class="enemy-health-bar-wrapper"><span class="enemy-health-bar"></span></div>';

                            if ($enemy_sound) {
                                $html .= '<audio src="' . esc_url($enemy_sound) . '" id="' . esc_attr($explore_point->post_name) . '-s" loop></audio>';
                            }

                            if ($enemy_hurt_sound) {
                                $html .= '<span class="hurt-sound">';
                                $html .= '<audio src="' . esc_url($enemy_hurt_sound) . '" id="' . esc_attr($explore_point->post_name) . '-hs"></audio>';
                                $html .= '</span>';
                            }
                        }

                        if ($direction_images) {
                            foreach ($direction_images as $direction_label => $direction_image) {
                                $fight_animation = false !== stripos($direction_label, 'punch') ? ' fight-image' : '';

                                $html .= '<img height = "';
                                $html .= false === empty($character_info['height']) ? esc_attr($character_info['height']) : '185';
                                $html .= 'px" width = "';
                                $html .= false === empty($character_info['width']) ? esc_attr($character_info['width']) : '115';
                                $html .= 'px" class="character-icon' . esc_attr($fight_animation);
                                $html .= 'static' === $direction_label ? ' engage' : '';
                                $html .= '" id = "' . esc_attr($explore_point->post_name);
                                $html .= esc_attr($direction_label);
                                $html .= '" src = "';
                                $html .= esc_url($direction_image) . '" />';
                            }
                        }
                    }

                    // Boss data / html.
                    if ('explore-enemy' === $explore_point->post_type && false === empty($boss_waves)) {
                        if (true === $pulse_wave) {
                            $html .= '<div class="pulse-wave-container" data-value="' . esc_attr($value) . '" data-hazard="true">';
                            $html .= self::getSVGCode(
                                plugin_dir_url(dirname(__FILE__)) . 'assets/src/images/pulse.svg'
                            );
                            $html .= '</div>';
                        }
                    }

                    $html .= '</div>';

                    // Trigger HTML.
                    $projectile_trigger = $explore_point_meta['explore-projectile-trigger'] ?? '';

                    if ('explore-enemy' === $explore_point->post_type && false === empty($projectile_trigger['left'])) {
                        $projectile_trigger_width  = $projectile_trigger['width'] ?? '0';
                        $projectile_trigger_height = $projectile_trigger['height'] ?? '0';
                        $projectile_trigger_top    = $projectile_trigger['top'] ?? '0';
                        $projectile_trigger_left   = $projectile_trigger['left'];


                        $html .= '<div id="' . esc_attr($explore_point->ID) . '-t" class="wp-block-group map-item ' . esc_attr($explore_point->post_name) . '-trigger-map-item is-layout-flow wp-block-group-is-layout-flow"';
                        $html .= ' style="left:' . esc_attr($projectile_trigger_left) . 'px;top:' . esc_attr($projectile_trigger_top) . 'px;height:' . esc_attr($projectile_trigger_height) . 'px; width:' . esc_attr($projectile_trigger_width) . 'px;"';
                        $html .= ' data-trigger="true" data-triggee="' . esc_attr($explore_point->post_name) . '-map-item"';
                        $html .= ' data-meta="explore-projectile-trigger"';
                        $html .= '></div>';
                    }

                    // Trigger Walking Path.
                    if (true === in_array($explore_point->post_type, ['explore-enemy', 'explore-character'], true) && false === in_array('', [$path_trigger_width, $path_trigger_height], true)) {
                        $html .= '<div id="' . esc_attr($explore_point->ID) . '-t" class="path-trigger wp-block-group map-item ' . esc_attr($explore_point->post_name) . '-trigger-map-item is-layout-flow wp-block-group-is-layout-flow"';
                        $html .= ' style="left:' . esc_attr($path_trigger_left) . 'px;top:' . esc_attr($path_trigger_top) . 'px;height:' . esc_attr($path_trigger_height) . 'px; width:' . esc_attr($path_trigger_width) . 'px;"';
                        $html .= ' data-trigger="true" data-triggee="' . esc_attr($explore_point->post_name) . '-map-item"';
                        $html .= ' data-meta="explore-path-trigger"';
                        $html .= '></div>';
                    }

                    // Draggable Destination.
                    if (true === $draggable) {
                        $drag_dest = $explore_point_meta['explore-drag-dest'] ?? '';

                        // The drag-dest group is submitted by the meta box form even
                        // when left untouched, and its number sub-fields (top/left/
                        // width/height/offset) always save as "0" rather than an empty
                        // string (the number field template renders floatval('') as 0).
                        // So checking for "any non-empty sub-value" still treats an
                        // untouched group as configured. Use image/mission instead —
                        // neither has that coercion, so a non-empty value in either
                        // reliably signals the destination was actually set up.
                        // Otherwise a phantom destination element is rendered and
                        // blocks free dragging of items with no destination set.
                        $drag_dest_configured = is_array($drag_dest) &&
                            (false === empty($drag_dest['image']) || false === empty($drag_dest['mission']));

                        if ($drag_dest_configured) {
                            $drag_top = $drag_dest['top'] ?? '';
                            $drag_left = $drag_dest['left'] ?? '';
                            $drag_height = $drag_dest['height'] ?? '';
                            $drag_width = $drag_dest['width'] ?? '';
                            $drag_image = $drag_dest['image'] ?? '';
                            $drag_mission = $drag_dest['mission'] ?? '';
                            $remove = $drag_dest['remove-after'] ?? 'no';
                            $offset = $drag_dest['offset'] ?? '10';
                            $materialize_after_cutscene = $drag_dest['materialize-after-cutscene'] ?? 'none';

                            $html .= '<div id="' . esc_attr($explore_point->ID) . '-d" class="drag-dest wp-block-group map-item ' . esc_attr($explore_point->post_name) . '-drag-dest-map-item is-layout-flow wp-block-group-is-layout-flow"';
                            $html .= ' style="z-index:0;left:' . esc_attr($drag_left) . 'px;top:' . esc_attr($drag_top) . 'px;height:' . esc_attr($drag_height) . 'px; width:' . esc_attr($drag_width) . 'px;"';

                            if ('yes' === $remove) {
                                $html .= ' data-removable="true"';
                            }

                            if (false === empty($materialize_after_cutscene) && 'none' !== $materialize_after_cutscene) {
                                $html .= ' data-showaftercutscene="' . esc_attr($materialize_after_cutscene) . '"';
                            }

                            $html .= ' data-offset="' . esc_attr($offset) . '"';
                            $html .= ' data-meta="explore-drag-dest" ';
                            $html .= ' data-mission="' . esc_attr($drag_mission) . '">';
                            $html .= '<img height="' . esc_attr($drag_height) . 'px" width="' . esc_attr($drag_width) . 'px" src="' . esc_attr($drag_image) . '" alt="' . esc_attr($explore_point->post_title) . '-drag-dest">';
                            $html .= '</div>';
                        }
                    }

                    if (false === empty($materialize_item_trigger['top']) && false === $is_materialized_item_triggered) {
                        $materialize_item_top = $materialize_item_trigger['top'];
                        $materialize_item_left = $materialize_item_trigger['left'] ?? '';
                        $materialize_item_height = $materialize_item_trigger['height'] ?? '';
                        $materialize_item_width = $materialize_item_trigger['width'] ?? '';

                        $html .= '<div class="materialize-item-trigger wp-block-group map-item ' . esc_attr($explore_point->post_name) . '-materialize-item-map-item is-layout-flow wp-block-group-is-layout-flow" data-type="point" data-value="0"';
                        $html .= ' data-meta="explore-materialize-item-trigger"';
                        $html .= ' id="' . esc_attr($explore_point->ID) . '-t"';
                        $html .= ' style="z-index:0;left:' . esc_attr($materialize_item_left) . 'px;top:' . esc_attr($materialize_item_top) . 'px;height:' . esc_attr($materialize_item_height) . 'px; width:' . esc_attr($materialize_item_width) . 'px;"';
                        $html .= '>';
                        $html .= '</div>';
                    }
                }
            }
        }

        $indicator = get_option('explore_indicator_icon', plugin_dir_url(__FILE__) . '../assets/src/images/indicator.svg');
        $indicator = false === empty($indicator) ? $indicator : plugin_dir_url(__FILE__) . '../assets/src/images/indicator.svg';

        $html .= '<div class="indicator-icon"><img src="' . esc_url($indicator) . '" width="15" height="15" /></div>';


        // Trigger Mission complete.
        if (false === empty($mission_trigger_html)) {
            $html .= $mission_trigger_html;
        }

        return $html;
    }

    /**
     * @param string $item_name
     * @param string $location
     * @param integer $userid
     * @return bool
     */
    public static function isMaterializedItemTriggered(string $item_name, string $location, int $userid): bool
    {
        if (0 >= $userid) {
            $userid = self::playerId();
        }

        if (0 >= $userid) {
            return false;
        }

        $materialize_items = get_user_meta($userid, 'explore_materialized_items', true);

        if (
            ! is_array($materialize_items)
            || ! isset($materialize_items[$location])
            || ! is_array($materialize_items[$location])
        ) {
            return false;
        }

        return in_array($item_name, $materialize_items[$location], true);
    }

    /**
     * Build html for map items.
     * @param array $explore_cutscenes
     * @param string $position
     * @param integer $userid
     * @return string
     */
    public static function getMapCutsceneHTML(array $explore_cutscenes, string $position, int $userid): string
    {
        if (0 >= $userid) {
            $userid = self::playerId();
        }

        $html   = '';
        $area   = get_posts(
            [
                'post_type'      => 'explore-area',
                'name'           => sanitize_key($position),
                'posts_per_page' => 1,
                'no_found_rows'  => true,
                'post_status'    => 'publish',
            ]
        );

        $orbem_studio_player_name = 'Yes' === get_option('explore_player_name', false);
        $main_character           = get_option('explore_main_character', false);
        $is_area_cutscene         = false === empty($area[0]) && 'yes' === get_post_meta($area[0]->ID, 'explore-is-cutscene', true);
        $area_name                = false === empty($area[0]) ? $area[0]->post_name : '';

        foreach( $explore_cutscenes as $explore_cutscene ) {
            $cutscene_post_meta = [];
            $raw_meta = get_post_meta($explore_cutscene->ID);

            foreach ($raw_meta as $key => $values) {
                $cutscene_post_meta[$key] = maybe_unserialize($values[0] ?? null);
            }

            $character                  = $cutscene_post_meta['explore-character'] ?? '';
            $next_area                  = $cutscene_post_meta['explore-next-area'] ?? '';
            $minigame                   = $cutscene_post_meta['explore-cutscene-minigame'] ?? '';
            $mute_music                 = $cutscene_post_meta['explore-mute-music'] ?? '';
            $value_type                 = $cutscene_post_meta['explore-value-type'] ?? '';
            $remove_after_cutscene      = $cutscene_post_meta['explore-remove-after-cutscene'] ?? '';
            $remove_after_focus         = $cutscene_post_meta['explore-remove-after-focus'] ?? '';
            $value                      = $cutscene_post_meta['explore-value'] ?? '';
            $has_video                  = has_block('video', $explore_cutscene->post_content);
            $cutscene_trigger           = $cutscene_post_meta['explore-cutscene-trigger'] ?? '';
            $character_position         = $cutscene_post_meta['explore-cutscene-character-position'] ?? '';
            $next_area_position         = $cutscene_post_meta['explore-cutscene-next-area-position'] ?? '';
            $npc_face_me                = $cutscene_post_meta['explore-npc-face-me'] ?? '';
            $character_position_left    = $character_position['left'] ?? '';
            $character_position_top     = $character_position['top'] ?? '';
            $next_area_position_left    = $next_area_position['left'] ?? '';
            $next_area_position_top     = $next_area_position['top'] ?? '';
            $walking_path               = $cutscene_post_meta['explore-path-after-cutscene'] ?? '';
            $walking_speed              = $cutscene_post_meta['explore-speed'] ?? '';
            $time_between               = $cutscene_post_meta['explore-time-between'] ?? '';
            $character_position_trigger = $cutscene_post_meta['explore-cutscene-move-npc'] ?? '';
            $mission_cutscene           = $cutscene_post_meta['explore-mission-cutscene'] ?? '';
            $music                      = $cutscene_post_meta['explore-cutscene-music'] ?? '';
            $materialize_cutscene       = $cutscene_post_meta['explore-materialize-after-cutscene'] ?? ''; // The cutscene that materializes this cutscene.
            $materialize_after_mission  = $cutscene_post_meta['explore-materialize-after-mission'] ?? ''; // The mission that materializes this cutscene.
            $materialize_focus          = $cutscene_post_meta['explore-materialize-after-focus'] ?? ''; // The focus view that materializes this cutscene after closing.
            $materialize_explainer      = $cutscene_post_meta['explore-materialize-after-explainer'] ?? ''; // The explainer that materializes this cutscene after closing.
            $materialize_item           = $cutscene_post_meta['explore-materialize-after-item'] ?? ''; // The item that materializes this cutscene after interaction.
            $remove_item                = $cutscene_post_meta['explore-remove-after-item'] ?? ''; // The item that removes this cutscene after interaction.
            $mission_complete_cutscene  = $cutscene_post_meta['explore-mission-complete-cutscene'] ?? '';
            $boss_fight                 = $cutscene_post_meta['explore-cutscene-boss'] ?? '';
            $cutscene_trigger_type      = $cutscene_post_meta['explore-trigger-type'] ?? '';
            $cutscene_the_end           = $cutscene_post_meta['explore-the-end'] ?? '';
            $next_area_datapoint        = false === empty($next_area) ? ' data-nextarea="' . esc_attr($next_area) . '"' : '';
            $cutscene_name              = $explore_cutscene->post_name;
            $is_cutscene_triggered      = self::isMaterializedItemTriggered($explore_cutscene->post_name, $area_name, $userid);
            $communicate_engage         = $cutscene_post_meta['explore-engage-communicate'] ?? '';
            $character_ids              = [];

            $html .= '<div class="wp-block-group map-cutscene ' . esc_attr($cutscene_name) . '-map-cutscene is-layout-flow wp-block-group-is-layout-flow"';
            $html .= ' id="' . esc_attr($explore_cutscene->ID) . '"';

            if (false === empty($mission_cutscene)) {
                $html .= ' data-mission="' . esc_attr($mission_cutscene) . '"';
            }

            if (false === empty($cutscene_trigger_type)) {
                $html .= ' data-triggertype="' . esc_attr($cutscene_trigger_type) . '"';
            }

            if ('yes' === $cutscene_the_end) {
                $html .= ' data-the-end="yes"';
            }

            if (false === empty($npc_face_me)) {
                $html .= ' data-npcfaceme="' . esc_attr($npc_face_me) . '"';
            }

            if (false === empty($communicate_engage)) {
                $html .= ' data-communicate="' . esc_attr(str_replace(' ', '-', strtolower($communicate_engage))) . '"';
            }

            $explore_path = false === empty($walking_path) ? wp_json_encode($walking_path) : '[{"top":"0","left":"0"}]';

            if ( $walking_speed ) {
                $html .= ' data-speed="' . esc_attr($walking_speed) . '"';
            }

            if ( $time_between ) {
                $html .= ' data-timebetween="' . esc_attr($time_between) . '"';
            }

            if ('[{"top":"0","left":"0"}]' !== $explore_path) {
                $html .= ' data-path=\'' . esc_attr($explore_path) . '\'';
            }

                // Add for use in making cutscene triggered by touching character.
            if (false === empty($character)) {
                $html .= ' data-character="' . esc_attr($character) . '"';
            }

            // Add for type of value you receive once completing this cutscene.
            if (false === empty($value_type)) {
                $html .= ' data-type="' . esc_attr($value_type) . '"';
            }

            // Add for type of value you receive once completing this cutscene.
            if (false === empty($value)) {
                $html .= ' data-value="' . esc_attr($value) . '"';
            }

            if (false === empty($music)) {
                $html .= ' data-music="' . esc_attr($music) . '"';
            }

            if (false === empty($mute_music) && 'yes' === $mute_music) {
                $html .= ' data-mutemusic="' . esc_attr($mute_music) . '"';
            }

            // Minigame that triggers cutscene.
            if (false === empty($minigame) && false === is_array($minigame)) {
                $html .= ' data-minigame="' . esc_attr($minigame) . '"';
            }

            // Has video in content.
            if (true === $has_video) {
                $html .= ' data-video="true"';
            }

            // Boss Fight.
            if (false === empty($boss_fight)) {
                $html .= ' data-boss="' . esc_attr($boss_fight) . '"';
            }

            // Add data point for the mission that is complete by having this cutscene.
            if (false === empty($mission_complete_cutscene)) {
                $html .= ' data-missioncomplete="' . esc_attr($mission_complete_cutscene) . '"';
            }

            // Add character position point if selected.
            if (false === empty($character_position_trigger)) {
                $data = wp_json_encode([
                    [
                        'left'    => $character_position_left ?? 0,
                        'top'     => $character_position_top ?? 0,
                        'trigger' => $character_position_trigger['trigger'] ?? 'before',
                    ],
                ]);

                $html .= " data-character-position='" . esc_attr($data) . "'";
            }

            if (false === empty($next_area)) {
                $area_obj = get_posts(
                    [
                        'name'           => sanitize_key($next_area),
                        'post_type'      => 'explore-area',
                        'post_status'    => 'publish',
                        'posts_per_page' => 1,
                        'no_found_rows'  => true,
                    ]
                );

                $html .= $next_area_datapoint;

                if (false === empty($next_area_position_top)) {
                    $next_area_position_data = wp_json_encode([
                        'left' => $next_area_position_left,
                        'top'  => $next_area_position_top,
                    ]);

                    $html .= " data-nextarea-position='" . esc_attr( $next_area_position_data ) . "'";
                }

                if (isset($area_obj[0]->ID)) {
                    $html .= ' data-mapurl="' . esc_url(get_post_meta($area_obj[0]->ID, 'explore-map', true)) . '"';
                }
            }

            $html .= '>';

            $blocks = parse_blocks( (string) $explore_cutscene->post_content );

            if (is_array($blocks)) {
                foreach ($blocks as $block) {
                    $character_ids[] = $block['attrs']['selectedCharacter'] ?? '';
                }
            }

            $unique_character_ids = array_unique($character_ids);

            if (false === $is_area_cutscene) {
                $html .= '<div class="character-image-wrapper">';
                foreach($unique_character_ids as $character_id) {
                    if ( false === empty($character_id) ) {
                        $html .= '<div data-character="' . esc_attr($character_id) . '" class="cut-character"><img src="' . esc_url(get_the_post_thumbnail_url($character_id)) . '"/></div>';
                    }
                }
                $html .= '</div>';
            }

            $html .= '<div class="character-name-wrapper">';
            foreach($unique_character_ids as $character_id) {
                if ( false === empty($character_id) ) {
                    $character_slug = get_post_field('post_name', $character_id);
                    $character_name = get_post_meta($character_id, 'explore-character-name', true);
                    $character_name = true === $orbem_studio_player_name && $main_character === $character_slug ? '{{playerName}}' : $character_name;
                    $character_name = false === empty($character_name) ? $character_name : get_post_field('post_title', $character_id);

                    $html .= '<div data-character="' . esc_attr($character_id) . '" class="character-name">' . esc_html($character_name) . '</div>';
                }
            }

            // Raw content for game engine; do not apply WordPress filters.
            $html .= 'explore-area' !== $explore_cutscene->post_type ? wp_kses_post($explore_cutscene->post_content) : '';
            $html .= '</div>';

            if (true === $has_video) {
                $html .= '<span id="skip-cutscene-video">SKIP</span>';
            }

            $html .= '</div>';

            $path_trigger_left   = false === empty($cutscene_trigger['left']) && 0 !== $cutscene_trigger['left'] ? $cutscene_trigger['left'] : '0';
            $path_trigger_top    = false === empty($cutscene_trigger['top']) && 0 !== $cutscene_trigger['top'] ? $cutscene_trigger['top'] : '0';
            $path_trigger_height = false === empty($cutscene_trigger['height']) && 0 !== $cutscene_trigger['height'] ? $cutscene_trigger['height'] : '0';
            $path_trigger_width  = false === empty($cutscene_trigger['width']) && 0 !== $cutscene_trigger['width'] ? $cutscene_trigger['width'] : '0';

            // Trigger Cutscene.
            if (false === in_array( '0', [$path_trigger_width, $path_trigger_height], true) && false === $is_cutscene_triggered) {
                $html .= '<div id="' . esc_attr($explore_cutscene->ID) . '-t" class="cutscene-trigger wp-block-group map-item ' . esc_attr($explore_cutscene->post_name) . '-cutscene-trigger-map-item is-layout-flow wp-block-group-is-layout-flow"';
                $html .= ' style="left:' . esc_attr($path_trigger_left) . 'px;top:' . esc_attr($path_trigger_top) . 'px;height:' . esc_attr($path_trigger_height) . 'px; width:' . esc_attr($path_trigger_width) . 'px;"';
                $html .= ' data-trigger="true" data-triggee="' . esc_attr($explore_cutscene->post_name) . '"';
                $html .= ' data-triggertype="' . esc_attr($cutscene_trigger_type) . '"';

                // Remove this after cutscene specified in data att.
                if (false === empty($remove_after_cutscene)) {
                    if (is_array($remove_after_cutscene)) {
                        $remove_after_cutscene = wp_json_encode($remove_after_cutscene);
                    }

                    $html .= ' data-removeaftercutscene=\'' . esc_attr($remove_after_cutscene) . '\'';
                }
                if (false === empty($remove_after_focus)) {
                    $html .= ' data-removeafterfocus="' . esc_attr($remove_after_focus) . '"';
                }

                if (false === empty($materialize_cutscene)) {
                    $html .= ' data-materializecutscene="' . esc_attr($materialize_cutscene) . '"';
                }

                if (false === empty($materialize_after_mission)) {
                    $html .= ' data-materializemission="' . esc_attr($materialize_after_mission) . '"';
                }

                if (false === empty($materialize_focus)) {
                    $html .= ' data-materializefocus="' . esc_attr($materialize_focus) . '"';
                }

                if (false === empty($materialize_explainer)) {
                    $html .= ' data-materializeexplainer="' . esc_attr($materialize_explainer) . '"';
                }

                if (false === empty($materialize_item)) {
                    $html .= ' data-materializeitem="' . esc_attr($materialize_item) . '"';
                }

                if (false === empty($remove_item)) {
                    $html .= ' data-removeafteritem="' . esc_attr($remove_item) . '"';
                }

                $html .= ' data-meta="explore-cutscene-trigger"';
                $html .= '></div>';
            }
        }

        return $html;
    }

    /**
     * Build html for map items.
     *
     * @param string $location
     * @param integer $userid
     * @return string
     */
    public static function getMapCommunicateHTML(string $location, int $userid): string
    {
        if (0 >= $userid) {
            $userid = self::playerId();
        }

        $html   = '';
        $trhtml = '';

        $location_obj = get_posts(
            [
                'name'           => sanitize_key($location),
                'post_type'      => 'explore-area',
                'posts_per_page' => 1,
                'post_status'    => 'publish',
                'no_found_rows'  => true,
            ]
        );

        $location_communicate_type = isset($location_obj[0]->ID) ? get_post_meta($location_obj[0]->ID, 'explore-communicate-type', true) : '';
        $communication_type        = false === empty($location_communicate_type) ? get_term_by('name', $location_communicate_type, 'explore-communication-type') : '';
        $communication_background  = false === empty($communication_type) ? get_term_meta($communication_type->term_id, 'explore-background', true) : '';
        $current_received          = 0 < $userid ? get_user_meta($userid, 'explore_received_communicates', true) : '';

        if (false === empty($communication_background)) {
            $html .= '<div style="background: url(' . esc_url($communication_background) . ') no-repeat; background-size: contain;" class="communication-wrapper ' . esc_attr($communication_type->slug) . '-map-item" id="' . esc_attr($communication_type->term_id) . '">';

            $explore_communicates = get_posts(
                [
                    'posts_per_page' => -1,
                    'post_type'      => 'explore-communicate',
                    'post_status'    => 'publish',
                    'no_found_rows'  => true,
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
                    'tax_query'      => [
                        [
                            'taxonomy' => 'explore-communication-type',
                            'field'    => 'name',
                            'terms'    => sanitize_key($location_communicate_type),
                        ]
                    ],
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
                    'meta_query' => [
                        [
                            'key'     => 'explore-area',
                            'value'   => sanitize_key($location),
                            'compare' => '=',
                        ]
                    ],
                ]
            );

            $included_posts = get_posts([
                'posts_per_page' => 500,
                'post_type'      => 'explore-communicate',
                'post__in'       => (
                    is_array($current_received)
                    && isset($current_received[$communication_type->term_id])
                    && is_array($current_received[ $communication_type->term_id])
                ) ? $current_received[$communication_type->term_id] : [],
                'post_status'    => 'publish',
                'no_found_rows'  => true,
            ]);

            $explore_communicates = is_array($explore_communicates) ? $explore_communicates : [];
            $included_posts       = is_array($included_posts) ? $included_posts : [];
            $explore_communicates = array_unique(
                array_merge($explore_communicates, $included_posts),
                SORT_REGULAR
            );

            foreach ($explore_communicates as $explore_communicate) {
                $explore_communicate_meta = [];
                if (!$communication_type || !isset($communication_type->term_id, $communication_type->slug)) {
                    return '';
                }

                $raw_meta = get_post_meta($explore_communicate->ID);

                foreach ($raw_meta as $key => $values) {
                    $explore_communicate_meta[$key] = maybe_unserialize($values[0] ?? null);
                }

                $materialize_after_mission  = $explore_communicate_meta['explore-materialize-after-mission'] ?? ''; // The mission that makes this communicate appear.
                $mute_music                 = $explore_communicate_meta['explore-mute-music'] ?? '';
                $communicate_trigger_top    = $explore_communicate_meta['explore-top'] ?? '';
                $communicate_trigger_left   = $explore_communicate_meta['explore-left'] ?? '';
                $communicate_trigger_height = $explore_communicate_meta['explore-height'] ?? '';
                $communicate_trigger_width  = $explore_communicate_meta['explore-width'] ?? '';
                $communicate_type           = $explore_communicate_meta['explore-communicate-type'] ?? '';
                $mission_communicate        = $explore_communicate_meta['explore-mission-communicate'] ?? '';
                $music                      = $explore_communicate_meta['explore-communicate-music'] ?? '';
                $communicate_name           = $explore_communicate->post_name;

                $html .= '<div class="wp-block-group map-communicate ' . esc_attr($communicate_name) . '-map-communicate is-layout-flow wp-block-group-is-layout-flow"';
                $html .= ' id="' . esc_attr($explore_communicate->ID) . '"';

                // The mission that will start a communication.
                if (false === empty($mission_communicate)) {
                    $html .= ' data-mission="' . esc_attr($mission_communicate) . '"';
                }

                if (false === empty($music)) {
                    $html .= ' data-music="' . esc_attr($music) . '"';
                }

                if (false === empty($mute_music) && 'yes' === $mute_music) {
                    $html .= ' data-mutemusic="' . esc_attr($mute_music) . '"';
                }

                if (false === empty($communicate_type)) {
                    $html .= ' data-type="' . esc_attr($communicate_type) . '"';
                }

                $html .= '>';

                $blocks = parse_blocks((string) $explore_communicate->post_content);
                $character_id = '';

                if (is_array($blocks) && isset($blocks[0]['attrs']['selectedCharacter'])) {
                    $character_id = $blocks[0]['attrs']['selectedCharacter'];
                }

                $html .= '<div data-character="' . esc_attr($character_id) . '" class="communicate-character"><img src="' . esc_url(get_the_post_thumbnail_url($character_id)) . '"/></div>';
                $html .= '<div class="message-wrapper">';
                $html .= '<span class="communicate-name">' . esc_html(get_the_title($character_id)) . '</span>';
                // Raw content for game engine; do not apply WordPress filters.
                $html .= wp_kses_post($explore_communicate->post_content);
                $html .= '</div>';
                $html .= '</div>';

                // Trigger communicate.

                if ('' !== $communicate_trigger_top && '' !== $communicate_trigger_height) {
                    $trhtml .= '<div id="' . esc_attr($explore_communicate->ID) . '-t" class="communicate-trigger wp-block-group map-item ' . esc_attr($explore_communicate->post_name) . '-communicate-trigger-map-item is-layout-flow wp-block-group-is-layout-flow"';
                    $trhtml .= ' style="left:' . esc_attr($communicate_trigger_left) . 'px;top:' . esc_attr($communicate_trigger_top) . 'px;height:' . esc_attr($communicate_trigger_height) . 'px; width:' . esc_attr($communicate_trigger_width) . 'px;"';
                    $trhtml .= ' data-trigger="true" data-triggee="' . esc_attr($explore_communicate->post_name) . '"';
                    $trhtml .= ' data-materializemission="' . esc_attr($materialize_after_mission) . '"';
                    $trhtml .= ' data-meta="explore-communicate-trigger"';
                    $trhtml .= '></div>';
                }
            }

            $html .= '</div>';
            $html .= $trhtml;
        }

        return $html;
    }

    /**
     * Build html for minigame items.
     * @param array $explore_minigames
     *
     * @return string
     */
    public static function getMinigameHTML(array $explore_minigames): string
    {
        $html = '';

        foreach($explore_minigames as $minigame) {
            if (isset($minigame->ID)) {
                $minigame_mission      = get_post_meta($minigame->ID, 'explore-mission', true);
                $music                 = get_post_meta($minigame->ID, 'explore-minigame-music', true);
                $minigame_type         = get_post_meta($minigame->ID, 'explore-minigame-type', true);
                $binary_translate_word = get_post_meta($minigame->ID, 'explore-translate-binary-word', true);
                $draggable_images      = get_post_meta($minigame->ID, 'explore-draggable-items', true);

                $html .= '<div class="minigame ' . esc_attr($minigame->post_name) . '-minigame-item" data-music="' . esc_attr($music) . '" data-mission="' . esc_attr($minigame_mission) . '">';
                $html .= '<div class="computer-chip">' . self::getSVGCode(get_the_post_thumbnail_url($minigame->ID)) . '</div>';
                $html .= '<div class="draggable-images">';

                if (false === empty($draggable_images) && true === is_array($draggable_images)) {
                    foreach ($draggable_images as $draggable_image) {
                        $height = $draggable_image['height'] ?? '';
                        $width  = $draggable_image['width'] ?? '';
                        $style  = '';

                        if ($height !== '') {
                            $style .= 'height:' . intval($height) . 'px;';
                        }

                        if ($width !== '') {
                            $style .= 'width:' . intval($width) . 'px;';
                        }

                        $style_attr = $style !== '' ? ' style="' . esc_attr($style) . '"' : '';

                        $html .= '<img class="minigame-draggable-image" src="' . esc_url($draggable_image['draggable-item'] ?? '') . '"' . $style_attr . ' draggable="true" />';
                    }
                }

                $html .= '</div>';

                if (false === empty($minigame_type) && 'draggable' === $minigame_type && false === empty($binary_translate_word)) {
                    $html .= '<div class="minigame-programming" >';
                    $html .= '<div class="input-section">';
                    $html .= '<div class="programming-output">';
                    $html .= '<textarea style="max-width:100%;" cols="150" rows="20"></textarea>';
                    $html .= '</div>';
                    $html .= '</div>';
                    $html .= '<p class="programming-subject">Translate the <strong>' . esc_html($binary_translate_word) . '</strong> into binary:</p>';
                    $html .= '<img class="alignnone size-full wp-image-3674" src="' . esc_url(plugin_dir_url(__FILE__) . '../assets/src/images/binary.svg') . '" alt="" />';
                    $html .= '</div>';
                }

                $html .= '</div>';
            }
        }

        return $html;
    }

    /**
     * Build html for explainers.
     * @param array $explore_explainers
     * @param string $type
     *
     * @return string
     */
    public static function getExplainerHTML(array $explore_explainers, string $type): string
    {
        if (true === empty($explore_explainers) || true === empty($type)) {
            return '';
        }

        $html          = '';
        $border_color      = get_option('explore_explainer_border_color', '');
        $border_radius     = absint(get_option('explore_explainer_border_radius', '0'));
        $border_size       = absint(get_option('explore_explainer_border_size', '0'));
        $border_style      = get_option('explore_explainer_border_style', '');
        $font_color        = get_option('explore_explainer_font_color', '');
        $font_size         = absint(get_option('explore_explainer_font_size', '0'));
        $background_color  = get_option('explore_explainer_background_color', '');

        foreach($explore_explainers as $explainer) {
            $explainer_meta = [];
            $raw_meta = get_post_meta($explainer->ID);

            foreach ($raw_meta as $key => $values) {
                $explainer_meta[$key] = maybe_unserialize($values[0] ?? null);
            }

            $explainer_type = $explainer_meta['explore-explainer-type'] ?? '';
            $sound_byte     = $explainer_meta['explore-sound-byte'] ?? '';
            $mute_music     = $explainer_meta['explore-mute-music'] ?? '';

            if ($type === $explainer_type) {
                $trigger                    = $explainer_meta['explore-explainer-trigger'] ?? '';
                $explainer_left             = $explainer_meta['explore-left'] ?? '0';
                $explainer_top              = $explainer_meta['explore-top'] ?? '0';
                $width_value                = $explainer_meta['explore-width'] ?? '0';
                $close_on_click             = isset($explainer_meta['explore-click-close']) && 'yes' === $explainer_meta['explore-click-close'];
                $explainer_the_end          = ($explainer_meta['explore-the-end'] ?? '') === 'yes';
                $explainer_auto_close       = intval($explainer_meta['explore-auto-close'] ?? 0);
                $explainer_width            = ('fullscreen' === $type)
                    ? 'width: 100%; max-width:' . $width_value
                    : 'width:' . $width_value;
                $arrow_style                = $explainer_meta['explore-explainer-arrow'] ?? '';
                $materialize_after_cutscene = $explainer_meta['explore-materialize-after-cutscene'] ?? '';
                $materialize_after_mission  = $explainer_meta['explore-materialize-after-mission'] ?? '';
                $trigger                    = is_array($trigger) ? $trigger : [];
                $arrow_style                = is_array($arrow_style) ? $arrow_style : [];
                $path_trigger_top           = false === empty($trigger['top']) && '0' !== $trigger['top'] ? $trigger['top'] : false;
                $path_trigger_left          = false === empty($trigger['left']) && '0' !== $trigger['left'] ? $trigger['left'] : false;
                $path_trigger_width         = false === empty($trigger['width']) && '0' !== $trigger['width'] ? $trigger['width'] : false;
                $path_trigger_height        = false === empty($trigger['height']) && '0' !== $trigger['height'] ? $trigger['height'] : false;
                $arrow_img                  = get_option('explore_arrow_icon', plugin_dir_url(__FILE__) . '../assets/src/images/arrow-icon.svg');
                $arrow_img                  = false === empty($arrow_img) ? $arrow_img : plugin_dir_url(__FILE__) . '../assets/src/images/arrow-icon.svg';
                $orientation                = $arrow_style['orientation'] ?? 'top';
                $side                       = $arrow_style['side'] ?? 'right';
                $rotation                   = $arrow_style['rotate'] ?? '0';
                $arrow_style_css            = 'transform: rotate(' . esc_attr($rotation) . 'deg); ' . esc_attr($orientation) . ': -130px;' . ' ' . esc_attr($side) . ': 0;';
                $fullscreen                 = 'fullscreen' === $type ? ' fullscreen' : '';

                if (false !== $path_trigger_top) {
                    $html .= '<div id="' . esc_attr($explainer->ID) . '-t" data-trigger="true" class="' . esc_attr($explainer->post_name) . '-explainer-trigger-map-item explainer-trigger map-item" data-triggee="' . esc_attr($explainer->post_name) . '"';
                    $html .= ' data-meta="explore-explainer-trigger"';

                    // Materialize this item after this cutscene.
                    if (false === empty($materialize_after_cutscene)) {
                        $html .= ' data-showaftercutscene="' . esc_attr($materialize_after_cutscene) . '"';
                    }

                    // Mute current area music.
                    if (false === empty($mute_music)) {
                        $html .= ' data-mute-music="' . esc_attr($mute_music) . '"';
                    }

                    // Materialize this item after this mission.
                    if (false === empty($materialize_after_mission)) {
                        $html .= ' data-materializemission="' . esc_attr($materialize_after_mission) . '"';
                    }

                    $html .= ' style="left:' . esc_attr($path_trigger_left) . 'px;top:' . esc_attr($path_trigger_top) . 'px;height:' . esc_attr($path_trigger_height) . 'px; width:' . esc_attr($path_trigger_width) . 'px;"';
                    $html .= '></div>';
                }

                // Fullscreen explainers don't need top/left — they're centered
                // overlays. Map/menu explainers still need a top to render.
                if ('fullscreen' === $type || false === empty($explainer_top)) {
                    $html .= '<div id="' . esc_attr($explainer->ID) . '" class="' . esc_attr($explainer->post_name) . '-explainer-item explainer-container map-item' . esc_attr($fullscreen) . '"';
                    $html .= ' style="left:' . esc_attr($explainer_left) . 'px;top:' . esc_attr($explainer_top) . 'px;height:auto; ' . esc_attr($explainer_width) . 'px; border: ' . esc_attr($border_size) . 'px ' . esc_attr($border_style) . ' ' . esc_attr($border_color) . '; border-radius: ' . esc_attr($border_radius) . 'px;';

                    if (false === empty($font_color)) {
                        $html .= ' color: ' . esc_attr($font_color) . ';';
                    }

                    if (0 < $font_size) {
                        $html .= ' font-size: ' . esc_attr($font_size) . 'px;';
                    }

                    if (false === empty($background_color)) {
                        $html .= ' background-color: ' . esc_attr($background_color) . ';';
                    }

                    $html .= '"';
                    $html .= ' data-type="' . esc_attr($explainer_type) . '"';

                    if (false === empty($close_on_click)) {
                        $html .= ' data-clickclose="' . esc_attr($close_on_click) . '"';
                    }

                    if ($explainer_the_end) {
                        $html .= ' data-the-end="yes"';
                    }

                    if (0 < $explainer_auto_close) {
                        $html .= ' data-auto-close="' . esc_attr($explainer_auto_close) . '"';
                    }

                    $html .= ' data-type="' . esc_attr($explainer_type) . '"';
                    $html .= '>';
                    $html .= $arrow_img && 'fullscreen' !== $type ? '<img data-rotate="' . esc_attr($rotation) . '" width="120" height="120" style="'. esc_attr($arrow_style_css) . '" src="' . esc_url($arrow_img) . '" />' : '';
                    // Raw content for game engine; do not apply WordPress filters.
                    $html .= do_blocks($explainer->post_content);

                    if (false === empty($sound_byte)) {
                        $html .= '<audio id="' . esc_attr($explainer->ID) . '-s" src="' . esc_url($sound_byte) . '"></audio>';
                    }

                    $html .= '</div>';
                }
            }
        }

        return $html;
    }

    /**
     * Build html for map abilities.
     * @param array $explore_abilities
     *
     * @return string
     */
    public static function getMapAbilitiesHTML(array $explore_abilities): string
    {
        $html   = '';
        $userid = self::playerId();
        $magics = 0 !== $userid ? get_user_meta($userid, 'explore_magic', true) : [];

        foreach($explore_abilities as $explore_ability) {
            if (!isset($explore_ability->ID)) {
                continue;
            }

            if (false === is_array($magics) || false === in_array($explore_ability->ID, $magics, true)) {
                $explore_ability_id = $explore_ability->ID ?? '';
                $unlockable = '' !== $explore_ability_id ? get_post_meta($explore_ability->ID, 'explore-unlock-level', true) : '';

                $html .= '<div class="map-ability"';
                $html .= ' id="' . esc_attr($explore_ability_id) . '"';
                $html .= ' data-genre="explore-magic"';

                if (false === empty($unlockable)) {
                    $html .= ' data-unlockable="' . esc_attr($unlockable) . '"';
                }

                $html .= '></div>';
            }
        }

        return $html;
    }

    /**
     * Register post type for page components.
     *
     * @action init
     */
    public function registerPostType(): void
    {
        $post_types = [
            'explore-area'        => [
                'Area',
                'supports' => ['title']
            ],
            'explore-point'       => [
                'Item',
                'supports' => ['title', 'thumbnail']
            ],
            'explore-character'   => [
                'Character',
                'supports' => ['title', 'thumbnail']
            ],
            'explore-cutscene'    => [
                'Cutscene',
                'supports' => ['title', 'editor']
            ],
            'explore-enemy'       => [
                'Enemy',
                'supports' => ['title', 'thumbnail']
            ],
            'explore-weapon'      => [
                'Weapon',
                'supports' => ['title', 'editor', 'thumbnail']
            ],
            'explore-mission'     => [
                'Mission',
                'supports' => ['title']
            ],
            'explore-sign'        => [
                'Focus View',
                'supports' => ['title', 'editor', 'thumbnail']
            ],
            'explore-minigame'    => [
                'Minigame',
                'supports' => ['title', 'thumbnail']
            ],
            'explore-explainer'   => [
                'Explainer',
                'supports' => ['title', 'editor']
            ],
            'explore-wall'      => [
                'Wall',
                'supports' => ['title']
            ],
            'explore-communicate' => [
                'Communication',
                'supports' => ['title', 'editor', 'thumbnail']
            ],
        ];

        $taxo_types = [
            'explore-communication-type' => [
                'name'       => 'Communication Type',
                'post-types' => ['explore-communicate']
            ]
        ];

        foreach($post_types as $slug => $info) {
            $labels = [];

            if (isset($info[0])) {
                $singular = $info[0];
                $plural   = $info[0] . 's';

                $labels = [
                    'name'                  => esc_html($plural),
                    'singular_name'         => esc_html($singular),
                    'menu_name'             => esc_html($plural),
                    'name_admin_bar'        => esc_html($singular),

                    'attributes' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('%s Attributes', 'orbem-studio'),
                        $singular
                    ),

                    'parent_item_colon' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('Parent %s:', 'orbem-studio'),
                        $singular
                    ),

                    'all_items' => sprintf(
                    /* translators: %s: Plural post type name */
                        esc_html__('All %s', 'orbem-studio'),
                        $plural
                    ),

                    'add_new_item' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('Add New %s', 'orbem-studio'),
                        $singular
                    ),

                    'add_new' => esc_html__('Add New', 'orbem-studio'),

                    'new_item' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('New %s', 'orbem-studio'),
                        $singular
                    ),

                    'edit_item' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('Edit %s', 'orbem-studio'),
                        $singular
                    ),

                    'update_item' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('Update %s', 'orbem-studio'),
                        $singular
                    ),

                    'view_item' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('View %s', 'orbem-studio'),
                        $singular
                    ),

                    'view_items' => sprintf(
                    /* translators: %s: Plural post type name */
                        esc_html__('View %s', 'orbem-studio'),
                        $plural
                    ),

                    'search_items' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('Search %s', 'orbem-studio'),
                        $singular
                    ),

                    'not_found' => sprintf(
                    /* translators: %s: Plural post type name */
                        esc_html__('No %s found', 'orbem-studio'),
                        $plural
                    ),

                    'not_found_in_trash' => sprintf(
                    /* translators: %s: Plural post type name */
                        esc_html__('No %s found in Trash', 'orbem-studio'),
                        $plural
                    ),

                    'featured_image' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('%s Image', 'orbem-studio'),
                        $singular
                    ),

                    'set_featured_image' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('Set %s image', 'orbem-studio'),
                        $singular
                    ),

                    'remove_featured_image' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('Remove %s image', 'orbem-studio'),
                        $singular
                    ),

                    'use_featured_image' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('Use as %s image', 'orbem-studio'),
                        $singular
                    ),

                    'uploaded_to_this_item' => sprintf(
                    /* translators: %s: Singular post type name */
                        esc_html__('Uploaded to this %s', 'orbem-studio'),
                        $singular
                    ),

                    'items_list' => sprintf(
                    /* translators: %s: Plural post type name */
                        esc_html__('%s list', 'orbem-studio'),
                        $plural
                    ),

                    'items_list_navigation' => sprintf(
                    /* translators: %s: Plural post type name */
                        esc_html__('%s list navigation', 'orbem-studio'),
                        $plural
                    ),

                    'filter_items_list' => sprintf(
                    /* translators: %s: Plural post type name */
                        esc_html__('Filter %s list', 'orbem-studio'),
                        $plural
                    ),
                ];
            }

            $args = [
                'labels'             => $labels,
                'menu_icon'          => 'dashicons-location-alt',
                'public'             => false,
                'publicly_queryable' => false,
                'show_ui'            => true,
                'show_in_menu'       => true,
                'query_var'          => true,
                'rewrite'            => ['slug' => $slug],
                'capability_type'    => 'page',
                'has_archive'        => false,
                'hierarchical'       => false,
                'map_meta_cap'       => true,
                'menu_position'      => null,
                'show_in_rest'       => true,
                'supports'           => $info['supports'],
            ];

            register_post_type( $slug, $args );
        }

        foreach($taxo_types as $slug => $stuff) {
            // Add explore area sync with explore point taxo.
            $arg2s = [
                'label'             => esc_html($stuff['name']),
                'hierarchical'      => true,
                'public'            => true,
                'show_ui'           => true,
                'show_admin_column' => true,
                'query_var'         => true,
                'rewrite'           => ['slug' => $slug],
                'show_in_rest'      => true,
            ];

            register_taxonomy($slug, $stuff['post-types'], $arg2s);
        }
    }

    /**
     * Get current point width based on equipped weapons and gear.
     * @return array
     */
    public static function getCurrentPointWidth(): array
    {
        $userid = self::playerId();

        $base_amounts = [
            'health' => 100,
            'mana'   => 100,
            'power'  => 100,
            'money'  => 0,
        ];

        if (0 === $userid) {
            return $base_amounts;
        }

        $gear = get_user_meta($userid, 'explore_current_gear', true);

        if (!is_array($gear)) {
            return $base_amounts;
        }

        foreach ($base_amounts as $type => $base_value) {
            if (!empty($gear[$type]) && is_array($gear[$type])) {
                foreach ($gear[$type] as $gear_amount) {
                    if (is_array($gear_amount)) {
                        $value = (int) reset($gear_amount);
                        $base_amounts[$type] += $value;
                    }
                }
            }
        }

        return $base_amounts;
    }

    /**
     * Map of levels.
     * @return int[]
     */
    public static function getLevelMap(): array
    {
        return [
            0,
            200,
            600,
            1200,
            2000,
            3000,
            4200,
            5600,
            7200,
        ];
    }

    /**
     * Get current level.
     *
     * @return int
     */
    public static function getCurrentLevel(): int
    {
        $levels = self::getLevelMap();

        if (empty($levels) || !is_array($levels)) {
            return 1;
        }

        sort($levels, SORT_NUMERIC);

        $userid = self::playerId();

        if (0 === $userid) {
            return 1;
        }

        $meta   = get_user_meta($userid, 'explore_points', true);
        $points = 0;

        if (is_array($meta) && isset($meta['point']['points'])) {
            $points = (int) $meta['point']['points'];
        }

        $count = count($levels);

        for ($i = 0; $i < $count; $i++) {
            $current = $levels[$i];
            $next    = $levels[$i + 1] ?? null;

            if ($points === $current) {
                return $i + 1;
            }

            if (null !== $next && $points > $current && $points < $next) {
                return $i + 1;
            }
        }

        // If points exceed all thresholds, return max level
        return $count;
    }

    /**
     * register the mp3 paragraph block.
     *
     * @action enqueue_block_editor_assets
     * @return void
     */
    public function customRegisterParagraphMp3Block(): void
    {
        Plugin::enqueueScript('orbem-order/paragraph-mp3-block');
    }

    /**
     * Register new block category for orbem studio.
     *
     * @param array $categories The current block categories.
     * @param WP_Block_Editor_Context $context
     *
     * @filter block_categories_all
     */
    public function blockCategory(array $categories, \WP_Block_Editor_Context $context): array
    {
        if (empty($context->post)) {
            return $categories;
        }

        return array_merge(
            $categories,
            [
                [
                    'slug'  => 'orbem-order-studio',
                    'title' => esc_html__('Orbem Studio', 'orbem-studio'),
                ],
            ]
        );
    }

    /**
     * Get the main character's images.
     *
     * @param string|\WP_Post $main_character
     * @return array
     */
    public static function getCharacterImages(string|\WP_Post $main_character): array
    {
        $meta = [];

        if (is_string($main_character)) {
            $posts = get_posts([
                'post_type'      => ['explore-character', 'explore-enemy'],
                'name'           => sanitize_key($main_character),
                'post_status'    => 'publish',
                'posts_per_page' => 1,
                'no_found_rows'  => true,
            ]);

            if (empty($posts) || ! $posts[0] instanceof \WP_Post) {
                return [];
            }

            $main_character = $posts[0];
        }

        if (!isset($main_character->ID)) {
            return [];
        }

        $raw_meta = get_post_meta($main_character->ID);
        foreach ($raw_meta as $key => $values) {
            $meta[$key] = maybe_unserialize($values[0] ?? null);
        }

        $images        = $meta['explore-character-images'] ?? [];
        $weapon_images = $meta['explore-weapon-images'] ?? [];
        $character_hurt_sound = $meta['explore-character-hurt-sound'] ?? false;

        if (!is_array($images)) {
            $images = [];
        }

        if (is_array($weapon_images)) {
            $images = array_merge($images, $weapon_images);
        }

        if (empty($images)) {
            return [];
        }

        $name = $meta['explore-character-name'] ?? $main_character->post_title;

        return [
            'direction_images' => $images,
            'height'           => $meta['explore-height'] ?? '',
            'width'            => $meta['explore-width'] ?? '',
            'ability'          => $meta['explore-ability'] ?? '',
            'weapon'           => $meta['explore-weapon-choice'] ?? '',
            'hitbox-inset'     => $meta['explore-hitbox-inset'] ?? '',
            'id'               => $main_character->ID,
            'hurt_sound'       => $character_hurt_sound,
            'name'             => $name,
            'voice'            => $meta['explore-voice'] ?? '',
            'jump'             => $meta['explore-jump'] ?? '',
            'double-jump'      => $meta['explore-double-jump'] ?? '',
            'jump-sound'       => $meta['explore-jump-sound'] ?? false,
            'charge-glow-color'  => $meta['explore-charge-glow-color'] ?? '',
            'charge-glow-target' => $meta['explore-charge-glow-target'] ?? 'character',
            'normal-attack-display-time'  => $meta['explore-normal-attack-display-time'] ?? '',
            'heavy-attack-display-time'   => $meta['explore-heavy-attack-display-time'] ?? '',
            'charged-attack-display-time' => $meta['explore-charged-attack-display-time'] ?? '',
            'damage-time'      => 'end' === ($meta['explore-damage-time'] ?? '') ? 'end' : 'start',
            'speed'            => intval($meta['explore-speed'] ?? 0) ?: 16,
        ];
    }

    /**
     * Google SSO oauth callback.
     * @param \WP_REST_Request $request
     * @return \WP_REST_Response
     */
    public function handleGoogleOauthCallback(\WP_REST_Request $request): \WP_REST_Response
    {
        $data       = $request->get_json_params();
        $nonce      = isset($data['nonce']) ? sanitize_text_field($data['nonce']) : '';
        $credential = isset($data['credential']) ? sanitize_text_field(wp_unslash($data['credential'])) : '';
        $client_id  = get_option('explore_google_login_client_id', '');

        if (
            empty($nonce)
            || empty($credential)
            || empty($client_id)
            || !wp_verify_nonce($nonce, 'google_sso_nonce')
        ) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Missing or invalid data point', 'orbem-studio'),
            ]);
        }

        $response = wp_remote_get(
            'https://oauth2.googleapis.com/tokeninfo?id_token=' . rawurlencode($credential),
            ['timeout' => 10]
        );

        if (is_wp_error($response)) {
            return rest_ensure_response(['success' => false]);
        }

        $payload = json_decode(wp_remote_retrieve_body($response), true);

        if (empty($payload['aud']) || $payload['aud'] !== $client_id) {
            return rest_ensure_response([
                'success' => false,
                'data'    => esc_html__('Invalid credentials', 'orbem-studio'),
            ]);
        }

        $payload = json_decode(base64_decode(str_replace('_', '/', str_replace('-', '+', explode('.', $credential)[1]))), true);
        if ($payload['aud'] !== $client_id || $payload['iss'] !== 'https://accounts.google.com') {
            return rest_ensure_response([
                'success' => false,
                'data' => esc_html__('Invalid audience or issuer', 'orbem-studio'),
            ]);
        }

        if ($payload['email_verified'] !== 'true') {
            return rest_ensure_response([
                'success' => false,
                'data' => esc_html__('Invalid email provided to google', 'orbem-studio'),
            ]);
        }

        $email = sanitize_email($payload['email']);
        $user = get_user_by('email', $email);

        if (!$user) {
            return rest_ensure_response([
                'success' => false,
                'data' => esc_html__('User does not exist.', 'orbem-studio'),
            ]);
        }

        // Log in the user
        wp_set_current_user($user->ID);
        wp_set_auth_cookie($user->ID);

        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
        do_action('wp_login', $user->user_login, $user);

        return rest_ensure_response([
            'success' => true,
            'data' => esc_html__('Success', 'orbem-studio'),
        ]);
    }

    /**
     * Call back for google login.
     * @return false|string
     */
    public static function googleLogin(): false|string
    {
        ob_start();
        ?>
        <div id="g_id_onload"
             data-client_id="<?php echo esc_attr(get_option('explore_google_login_client_id', '')); ?>"
             data-nonce="<?php echo esc_attr(wp_create_nonce('google_sso_nonce')); ?>"
             data-context="signin"
             data-ux_mode="popup"
             data-callback="exploreHandleCredentialResponse"
             data-auto_prompt="false">
        </div>

        <div class="g_id_signin"
             data-type="standard"
             data-shape="rectangular"
             data-theme="outline"
             data-text="sign_in_with"
             data-size="large"
             data-logo_alignment="left">
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * Add all inline styles for game assets.
     * @action wp_enqueue_scripts
     *
     */
    public static function enqueueDynamicStyles() {
        // Prepare dynamic CSS
        $dynamic_css = self::generateDynamicStyles('');

        if (! empty($dynamic_css)) {
            wp_add_inline_style('orbem-order/app', $dynamic_css);
        }

        $menu_css = self::generateDynamicStyles('menu');

        if (! empty($menu_css)) {
            wp_add_inline_style('orbem-order/explore', $menu_css);
        }
    }

    /**
     * Generate dynamic inline CSS for game elements.
     * @param string $type
     * @return string
     */
    protected static function generateDynamicStyles(string $type): string {

        // Fetch options and user meta
        $first_area           = get_option('explore_first_area', false);
        $current_location     = get_user_meta(self::playerId(), 'current_location', true);
        $position             = ! empty( $current_location ) ? $current_location : get_post_field('post_name', $first_area);
        $explore_points       = self::getExplorePoints($position);

        $css = '';

        if ('menu' === $type) {
            $setting_icon           = get_option('explore_settings_icon', '');
            $setting_icon           = false === empty($setting_icon) ? $setting_icon : plugin_dir_url(__FILE__) . '../assets/src/images/settings-default.svg';
            $storage_icon           = get_option('explore_storage_icon', '');
            $storage_icon           = false === empty($storage_icon) ? $storage_icon : plugin_dir_url(__FILE__) . '../assets/src/images/storage-default.svg';
            $characters_icon        = get_option('explore_crew_icon', '');
            $characters_icon        = false === empty($characters_icon) ? $characters_icon : plugin_dir_url(__FILE__) . '../assets/src/images/crew-default.svg';
            $cutscene_border_color  = get_option('explore_cutscene_border_color', '');
            $cutscene_border_radius = get_option('explore_cutscene_border_radius', '');
            $cutscene_border_style  = get_option('explore_cutscene_border_style', '');
            $cutscene_border_size   = get_option('explore_cutscene_border_size', '');
            $character_hover_border = get_option('explore_crewmate_hover_border_color', '');
            $skip_button_color      = get_option('explore_skip_button_color', '');

            // Menu icons & hover
            return "
        #settings:not(.engage):before { content: url('" . esc_url($setting_icon) . "'); cursor: pointer; position: absolute; top: 50%; transform: translateY(-50%); width: 50px; }
        #storage:not(.engage):before { content: url('" . esc_url($storage_icon) . "'); cursor: pointer; position: absolute; top: 50%; transform: translateY(-50%); width: 50px; }
        #characters:not(.engage):before { content: url('" . esc_url($characters_icon) . "'); cursor: pointer; position: absolute; top: 50%; transform: translateY(-50%); width: 50px; }
        .map-cutscene .character-name { border: " . esc_attr($cutscene_border_color) . " solid 2px; }
        .map-cutscene .wp-block-orbem-paragraph-mp3 { border: " . esc_attr($cutscene_border_size) . "px " . esc_attr($cutscene_border_style) . " " . esc_attr($cutscene_border_color) . "; border-radius: " . esc_attr($cutscene_border_radius) . "px; }
        #skip-intro-video, #skip-cutscene-video { background: " . esc_attr($skip_button_color) . "; }
        html body .game-container #characters .characters-content .characters-form .character-list .character-item:hover { border: 4px solid " . esc_attr($character_hover_border) . "; }
        ";
        }

        // Explore map points
        if (is_array($explore_points)) {
            foreach ($explore_points as $point) {
                if (! isset($point->ID) || ! get_post($point->ID) || true === in_array($point->post_type, ['explore-character'], true)) {
                    continue;
                }

                $top     = get_post_meta($point->ID, 'explore-top', true) . 'px';
                $left    = get_post_meta($point->ID, 'explore-left', true) . 'px';
                $height  = get_post_meta($point->ID, 'explore-height', true) . 'px';
                $width   = get_post_meta($point->ID, 'explore-width', true) . 'px';
                $bg_url  = $point->post_type !== 'explore-enemy' ? get_the_post_thumbnail_url($point->ID) : '';
                $type    = $point->post_type === 'explore-enemy' ? '.enemy-item' : '.map-item';

                if ($bg_url && in_array($point->post_type, ['explore-weapon', 'explore-point', 'explore-sign'], true)) {
                    $thumb_id = get_post_thumbnail_id($point->ID);
                    if ($thumb_id) {
                        $bg_url .= '?v=' . get_post_modified_time('U', false, $thumb_id);
                    }
                }

                // Wall texture: tile a preset (or uploaded) texture across the
                // wall as a repeated background. Overrides the default invisible
                // wall when a texture other than "invisible" is chosen.
                $texture_css = '';

                if ('explore-wall' === $point->post_type) {
                    $texture     = get_post_meta($point->ID, 'explore-texture', true);
                    $custom      = get_post_meta($point->ID, 'explore-custom-texture', true);
                    $texture_scale = intval(get_post_meta($point->ID, 'explore-texture-scale', true));
                    $texture_scale = 0 < $texture_scale ? $texture_scale : 64;
                    $texture_url = '';

                    if ('custom' === $texture && false === empty($custom)) {
                        $texture_url = $custom;
                    } elseif (false === empty($texture) && 'invisible' !== $texture && 'custom' !== $texture) {
                        $texture_url = plugin_dir_url(__FILE__) . '../assets/src/images/textures/' . sanitize_file_name($texture) . '.svg';
                    }

                    if (false === empty($texture_url)) {
                        $texture_css = "background: url('" . esc_url($texture_url) . "') repeat; background-size: " . esc_attr($texture_scale) . "px " . esc_attr($texture_scale) . "px; image-rendering: pixelated;";
                    }

                    // Rounded wall corners (clips the texture to the rounded box).
                    $border_radius = intval(get_post_meta($point->ID, 'explore-wall-border-radius', true));
                    if (0 < $border_radius) {
                        $texture_css .= " border-radius: " . esc_attr($border_radius) . "px; overflow: hidden;";
                    }
                }

                $css .= "
                body .game-container .default-map {$type}.{$point->post_name}-map-item[data-genre='" . esc_attr($point->post_type) . "'] {
                    top: " . esc_attr($top) . ";
                    left: " . esc_attr($left) . ";
                    " . ($height !== '0px' ? 'height:' . esc_attr($height) . ';' : '') . "
                    " . ($width !== '0px' ? 'width:' . esc_attr($width) . ';' : '') . "
                    " . (! empty($texture_css) ? $texture_css : (! empty($bg_url) ? "background: url('" . esc_url($bg_url) . "') no-repeat; background-size: contain;" : '')) . "
                }
                ";
            }
        }

        // Custom CSS.
        $orbem_studio_custom_css = get_option('explore_custom_css');

        if (false === empty($orbem_studio_custom_css)) {
            $css .= $orbem_studio_custom_css;
        }

        return $css;
    }
}
