//=============================================================================
// RPG Maker MZ - Region Base Plugin
//=============================================================================
// Version
// 1.0.1 2020/10/20 マップデータがロードされていないタイミングでアクターの特徴がスクリプト経由で参照されるとエラーになる問題を修正
// 1.0.0 2020/08/20 初版
//=============================================================================

/*:
 * @target MZ
 * @plugindesc Region Base Plugin
 * @author triacontane
 * @base PluginCommonBase
 * @orderAfter PluginCommonBase
 * @url
 *
 * @param regionList
 * @text Region List
 * @desc List of region data.
 * @default []
 * @type struct<Record>[]
 *
 * @param terrainTagList
 * @text Terrain Tag List
 * @desc List of terrain tag data.
 * @default []
 * @type struct<Record>[]
 *
 * @help RegionBase.js
 *
 * Offers a database of regions and terrain tags.
 * The specs generally follow RPG Maker MV Trinity.
 * The plug-in offers the following functions using regions and terrain tags as triggers.
 * -Event, player passage determination (4 dir included)
 * -Ladders, bushes, counters, damage floors
 * -Calling of common events (3 trigger types)
 * -Switches that turn ON only when a tile is entered
 * -Traits enabled only when a tile is entered
 * -Notes field
 *
 * The database can be referred to from scripts and external plug-ins with the following script.
 * Please note that when not configured, or if the index set to [0], the content will become undefined.
 *
 * $dataSystem.regions[ID];
 * $dataSystem.terrainTags[ID];
 */

/*~struct~Record:
 *
 * @param id
 * @text ID
 * @desc Target region or terrain tag ID.
 * @default 1
 * @type number
 *
 * @param name
 * @text Name
 * @desc Name for management. No special significance.
 * @default
 *
 * @param collisionForPlayer
 * @text Player Collision Determination
 * @desc Determines collision with player.
 * @default []
 * @type select[]
 * @option Impassable
 * @value collision_all
 * @option Cannot be passed from top
 * @value collision_up
 * @option Cannot be passed from right
 * @value collision_right
 * @option Cannot be passed from bottom
 * @value collision_down
 * @option Cannot be passed from left
 * @value collision_left
 *
 * @param collisionForEvent
 * @text Event Collision Determination
 * @desc Determines collision with event.
 * @default []
 * @type select[]
 * @option Impassable
 * @value collision_all
 * @option Cannot be passed from top
 * @value collision_up
 * @option Cannot be passed from right
 * @value collision_right
 * @option Cannot be passed from bottom
 * @value collision_down
 * @option Cannot be passed from left
 * @value collision_left
 *
 * @param through
 * @text Through
 * @desc Through settings. Enable to pass through impassable tiles. Collision determination takes priority.
 * @default false
 * @type boolean
 *
 * @param tileAttribute
 * @text Tile Attribute
 * @desc The tile attribute.
 * @default []
 * @type select[]
 * @option Ladder
 * @value ladder
 * @option Bush
 * @value bush
 * @option Counter
 * @value counter
 * @option Damage Floor
 * @value damage_floor
 *
 * @param commonEvent
 * @text Common Event
 * @desc Common event to be called.
 * @type struct<CommonEvent>[]
 *
 * @param switchId
 * @text SwitchID
 * @desc Switch that turns ON when tile is entered. Turns OFF when left.
 * @type switch
 *
 * @param traitsId
 * @text Traits
 * @desc Traits that are enabled only when the Player has entered the tile. Selected from Jobs for convenience.
 * @type class
 *
 * @param note
 * @text Notes
 * @desc The notes field. As with the regular database, meta information is always generated from here. Assumes usage in scripts.
 * @type multiline_string
 */

/*~struct~CommonEvent:
 *
 * @param id
 * @text ID
 * @desc Common Event ID.
 * @default 1
 * @type common_event
 *
 * @param trigger
 * @text Trigger
 * @desc Launch trigger for common events.
 * @default 0
 * @type select
 * @option Executes only once when the area is entered
 * @value 0
 * @option Executes when the player walks while in the area
 * @value 1
 * @option Executes only once when the area is left
 * @value 2
 */

/*:ja
 * @target MZ
 * @plugindesc リージョンのデータベース提供します
 * @author トリアコンタン
 * @base PluginCommonBase
 * @orderAfter PluginCommonBase
 * @url
 *
 * @param regionList
 * @text リージョンリスト
 * @desc リージョンデータのリストです。
 * @default []
 * @type struct<Record>[]
 *
 * @param terrainTagList
 * @text 地形タグリスト
 * @desc 地形タグデータのリストです。
 * @default []
 * @type struct<Record>[]
 *
 * @help RegionBase.js
 *
 * リージョンおよび地形タグのデータベースを提供します。
 * 仕様はおおよそRPGツクールMV Trinityに準じています。
 * リージョンおよび地形タグをトリガーにして以下の機能を提供します。
 * ・イベント、プレイヤーに対する通行判定(4方向含む)
 * ・梯子、茂み、カウンター、ダメージ床
 * ・コモンイベントの呼び出し(トリガー3種類)
 * ・侵入している間だけONになるスイッチ
 * ・侵入している間だけ有効になる特徴
 * ・メモ欄
 *
 * 以下のスクリプトでデータベースをスクリプトや外部プラグインから参照できます。
 * 未設定もしくは添え字が[0]の場合の中身はundefinedとなるので注意してください。
 *
 * $dataSystem.regions[ID];
 * $dataSystem.terrainTags[ID];
 */

/*~struct~Record:
 *
 * @param id
 * @text ID
 * @desc 対象となるリージョンもしくは地形タグIDです。
 * @default 1
 * @type number
 *
 * @param name
 * @text 名前
 * @desc 管理上の名前です。特に意味はありません。
 * @default
 *
 * @param collisionForPlayer
 * @text プレイヤーとの衝突判定
 * @desc プレイヤーとの衝突判定です。
 * @default []
 * @type select[]
 * @option 通行不可
 * @value collision_all
 * @option 上方向通行不可
 * @value collision_up
 * @option 右方向通行不可
 * @value collision_right
 * @option 下方向通行不可
 * @value collision_down
 * @option 左方向通行不可
 * @value collision_left
 *
 * @param collisionForEvent
 * @text イベントとの衝突判定
 * @desc イベントとの衝突判定です。
 * @default []
 * @type select[]
 * @option 通行不可
 * @value collision_all
 * @option 上方向通行不可
 * @value collision_up
 * @option 右方向通行不可
 * @value collision_right
 * @option 下方向通行不可
 * @value collision_down
 * @option 左方向通行不可
 * @value collision_left
 *
 * @param through
 * @text すり抜け
 * @desc すり抜け設定です。有効になっていると通行不可のタイルを通過できます。衝突判定の方が優先されます。
 * @default false
 * @type boolean
 *
 * @param tileAttribute
 * @text タイル属性
 * @desc タイル属性です。
 * @default []
 * @type select[]
 * @option 梯子
 * @value ladder
 * @option 茂み
 * @value bush
 * @option カウンター
 * @value counter
 * @option ダメージ床
 * @value damage_floor
 *
 * @param commonEvent
 * @text コモンイベント
 * @desc 呼び出されるコモンイベントです。
 * @type struct<CommonEvent>[]
 *
 * @param switchId
 * @text スイッチID
 * @desc 侵入時にONになるスイッチです。離脱するとOFFになります。
 * @type switch
 *
 * @param traitsId
 * @text 特徴
 * @desc プレイヤーが侵入している場合に有効になる特徴です。便宜上、職業からの選択となります。
 * @type class
 *
 * @param note
 * @text メモ
 * @desc メモ欄です。通常のデータベースと同様に、ここからmeta情報が自動生成されます。スクリプトでの使用を想定しています。
 * @type multiline_string
 */

/*~struct~CommonEvent:
 *
 * @param id
 * @text ID
 * @desc コモンイベントIDです。
 * @default 1
 * @type common_event
 *
 * @param trigger
 * @text トリガー
 * @desc コモンイベントの起動トリガーです。
 * @default 0
 * @type select
 * @option エリアに侵入したときに一度だけ実行
 * @value 0
 * @option エリア内にいる間、歩く度に実行
 * @value 1
 * @option エリアから離脱したときに一度だけ実行
 * @value 2
 */

(() => {
    'use strict';
    const script = document.currentScript;
    const param  = PluginManagerEx.createParameter(script);

    const _DataManager_onLoad = DataManager.onLoad;
    DataManager.onLoad = function(object) {
        _DataManager_onLoad.apply(this, arguments);
        if ($dataSystem && $dataClasses && !$dataSystem.regions) {
            PluginManagerEx.setupRegionData(param.regionList, 'regions');
            PluginManagerEx.setupRegionData(param.terrainTagList, 'terrainTags');
        }
    };

    PluginManagerEx.setupRegionData = function(paramList, prop) {
        const dataList = [];
        $dataSystem[prop] = dataList;
        if (!Array.isArray(paramList)) {
            return;
        }
        paramList.forEach(item => {
            dataList[item.id] = item;
            const classData = $dataClasses[item.traitsId];
            if (classData) {
                item.traits = classData.traits;
            }
        });
        DataManager.extractArrayMetadata(dataList);
    };

    /**
     * Game_CharacterBase
     */
    const _Game_CharacterBase_isMapPassable = Game_CharacterBase.prototype.isMapPassable;
    Game_CharacterBase.prototype.isMapPassable = function(x, y, d) {
        $gameMap.setPassableSubject(this);
        return _Game_CharacterBase_isMapPassable.apply(this, arguments)
    };

    Game_CharacterBase.prototype.findCollisionData = function(x, y) {
        // abstract
    };

    Game_Event.prototype.findCollisionData = function(x, y) {
        return $gameMap.findArrayDataRegionAndTerrain(x, y, 'collisionForEvent');
    };

    Game_Player.prototype.findCollisionData = function(x, y) {
        return $gameMap.findArrayDataRegionAndTerrain(x, y, 'collisionForPlayer');
    };

    /**
     * Game_Player
     */
    const _Game_Player_update = Game_Player.prototype.update;
    Game_Player.prototype.update = function(sceneActive) {
        const wasMoving = this.isMoving();
        _Game_Player_update.apply(this, arguments);
        if (!this.isMoving() && wasMoving) {
            this.updateCurrentRegion();
            this.updateCurrentTerrainTags();
        }
    };

    Game_Player.prototype.updateCurrentRegion = function() {
        this._region = $gameMap.findCurrentRegion(this.x, this.y);
        this.updateCurrentRegionAndTerrain(this._region, this._prevRegion);
        this._prevRegion = this._region;
    };

    Game_Player.prototype.updateCurrentTerrainTags = function() {
        this._terrainTags = $gameMap.findCurrentTerrainTag(this.x, this.y);
        this.updateCurrentRegionAndTerrain(this._terrainTags, this._prevTerrainTags);
        this._prevTerrainTags = this._terrainTags;
    };

    Game_Player.prototype.updateCurrentRegionAndTerrain = function(current, prev) {
        this.checkRegionCommonTrigger(current || {}, prev || {});
        this.checkRegionSwitch(current || {}, prev || {});
    };

    Game_Player.prototype.checkRegionCommonTrigger = function(current, prev) {
        (current.commonEvent || []).forEach(event => {
            if (event.trigger === 0 && current.id !== prev.id) {
                $gameMap.setupDynamicCommon(event.id);
            } else if (event.trigger === 1) {
                $gameMap.setupDynamicCommon(event.id);
            }
        });
        (prev.commonEvent || []).forEach(event => {
            if (event.trigger === 2 && current.id !== prev.id) {
                $gameMap.setupDynamicCommon(event.id);
            }
        });
    };

    Game_Player.prototype.checkRegionSwitch = function(current, prev) {
        if (current.id !== prev.id) {
            if (current.switchId > 0) {
                $gameSwitches.setValue(current.switchId, true);
            }
            if (prev.switchId > 0) {
                $gameSwitches.setValue(prev.switchId, false);
            }
        }
    };

    Game_Player.prototype.appendRegionTraits = function(traitsObjects) {
        if ($gameMap.mapId() <= 0 || !$dataMap) {
            return traitsObjects;
        }
        const region = $gameMap.findCurrentRegion(this.x, this.y);
        if (region && region.traits) {
            traitsObjects.push(region);
        }
        const terrainTag = $gameMap.findCurrentTerrainTag(this.x, this.y);
        if (terrainTag && terrainTag.traits) {
            traitsObjects.push(terrainTag);
        }
        return traitsObjects;
    };

    /**
     * Game_Actor
     */
    const _Game_Actor_traitObjects = Game_Actor.prototype.traitObjects;
    Game_Actor.prototype.traitObjects = function() {
        const traitsObjects = _Game_Actor_traitObjects.apply(this, arguments);
        return $gamePlayer.appendRegionTraits(traitsObjects);
    };

    /**
     * Game_Map
     */
    Game_Map.prototype.setPassableSubject = function(character) {
        this._passableSubject = character;
    };

    const _Game_Map_isPassable = Game_Map.prototype.isPassable;
    Game_Map.prototype.isPassable = function(x, y, d) {
        const passable = _Game_Map_isPassable.apply(this, arguments);
        if (this.isCollidedByRegion(x, y, d)) {
            return false;
        } else if (this.isThroughByRegion(x, y)) {
            return true;
        } else {
            return passable;
        }
    };

    Game_Map.prototype.isCollidedByRegion = function(x, y, d) {
        const collision = this._passableSubject.findCollisionData(x, y);
        if (collision.length === 0) {
            return false;
        }
        return collision.includes('collision_all') ||
            (collision.includes('collision_up') && d === 8) ||
            (collision.includes('collision_right') && d === 6) ||
            (collision.includes('collision_left') && d === 4) ||
            (collision.includes('collision_down') && d === 2);
    };

    Game_Map.prototype.isThroughByRegion = function(x, y) {
        return this.findDataRegionAndTerrain(x, y, 'through');
    };

    const _Game_Map_checkLayeredTilesFlags = Game_Map.prototype.checkLayeredTilesFlags;
    Game_Map.prototype.checkLayeredTilesFlags = function(x, y, bit) {
        const result = _Game_Map_checkLayeredTilesFlags.apply(this, arguments);
        if (result) {
            return true;
        }
        const attribute = this.findArrayDataRegionAndTerrain(x, y, 'tileAttribute');
        switch (bit) {
            case 0x20:
                return attribute.includes('ladder');
            case 0x40:
                return attribute.includes('bush');
            case 0x80:
                return attribute.includes('counter');
            case 0x100:
                return attribute.includes('damage_floor');
        }
        return false;
    };

    Game_Map.prototype.findArrayDataRegionAndTerrain = function(x, y, prop) {
        const region = this.findCurrentRegion(x, y);
        const regionValue = region ? region[prop] : [];
        const terrain = this.findCurrentTerrainTag(x, y);
        const terrainValue = terrain ? terrain[prop] : [];
        return regionValue.concat(terrainValue);
    };

    Game_Map.prototype.findDataRegionAndTerrain = function(x, y, prop) {
        const region = this.findCurrentRegion(x, y);
        if (region && region[prop]) {
            return region[prop];
        }
        const terrain = this.findCurrentTerrainTag(x, y);
        if (terrain && terrain[prop]) {
            return terrain[prop]
        }
        return null;
    };

    Game_Map.prototype.findCurrentRegion = function(x, y) {
        return $dataSystem.regions[this.regionId(x, y)];
    };

    Game_Map.prototype.findCurrentTerrainTag = function(x, y) {
        return $dataSystem.terrainTags[this.terrainTag(x, y)];
    };
})();
