/* eslint-disable no-shadow */

/**
 * @typedef {Object.<string,number>} dict
 */
var MATTIE = MATTIE || {};
MATTIE.multiplayer = MATTIE.multiplayer || {};
MATTIE.menus.multiplayer = MATTIE.menus.multiplayer || {};
MATTIE.scenes.multiplayer = MATTIE.scenes.multiplayer || {};
MATTIE.windows.multiplayer = MATTIE.windows.multiplayer || {};
MATTIE.multiplayer.emittedInit = false;
MATTIE.multiplayer.hasLoadedVars = false;

MATTIE.multiplayer.varSyncRequested = false;

MATTIE.multiplayer.maxPacketsPerSecond = 1500; // I dont know if this is useful
MATTIE.multiplayer.packetsThisSecond = 0;

setInterval(() => {
	MATTIE.multiplayer.packetsThisSecond = 0;
}, 1000);

var EventEmitter = require('events');

/**
 * @description the net controller class this handles all net traffic for the multiplayer mod.
 * it is made to send and receive json packets, that contain as many or as few commands as we want,
 * for instance, a packet could contain a move cmd, or a mov cmd and a bunch
 * of others.
 * @class
 */
class BaseNetController extends EventEmitter {
	constructor() {
		super();
		/** the player on the local machine
		 *  @type {PlayerModel}
		*/
		this.player = null;
		/** @description the id of this peer */
		this.peerId = null;
		/** @description has the game started? */
		this.started = false;
		/** @description list of player models */
		this.players = {};
		/** @type {PlayerModel[]} */
		this.netPlayers = {};

		this.transferRetries = 0;
		this.maxTransferRetries = 10;

		this.canTryToReconnect = false;
	}

	/**
	 * @description a method ment to be overriden by client and host
	 */
	connect(id) {

	}

	/**
	 * @deprecated dont use this
	 * @description if the user's accept button is a letter then when they hit accept for the connection code then it will
	 * append the letter aswell which will mess up the code.
	 */
	acceptBtnTypedConnectionFix() {
		const okayKeys = [];
		const objKeys = Object.keys(Input.keyMapper);

		// for all keyCodes in the key mapper.
		objKeys.forEach((objKey) => {
			// if a keyCode is mapped to the game command 'ok' -> ok is the accept/submit command
			if (Input.keyMapper[objKey] === 'ok') {
				// convert the keycode to a char and check if the char is alphanumeric
				const asciiKey = String.fromCharCode(objKey);
				if (asciiKey.match(/^[0-9a-z]+$/gi)) { okayKeys.push(asciiKey); }
			}
		});

		// if the hostId ends with any of the accept keys, remove it and try to connect again.
		let endsWithBadVar = false;
		okayKeys.forEach((okayLetterKey) => {
			if (this.hostId.toLowerCase().endsWith(okayLetterKey.toLowerCase())) endsWithBadVar = true;
		});

		let tempHost;
		if (endsWithBadVar) {
			tempHost = this.hostId.slice(0, -1);
			this.connect(tempHost, false);
		}
	}

	disconnectAllConns() {
		this.canTryToReconnect = true;
		if (this.self) {
			this.self.disconnect();
		}

		if (this.conn) {
			this.conn.close();
		}
	}

	reconnectAllConns() {
		if (this.self) {
			if (!this.self.disconnected) { this.self.reconnect(); }
		}
		this.setIsClient();
	}

	resetNet() {
		this.clearPeerId();
		this.clearControlVars();
		this.destroyAllConns();
	}

	destroyAllConns() {
		if (this.self) {
			this.self.destroy();
		}
		if (this.conn) {
			this.conn.destroy();
		}
	}

	clearPeerId() {
		this.peerId = null;
	}

	clearControlVars() {
		this.started = false;
		this.players = {};
		/** @type {Object.<string,PlayerModel>} */
		this.netPlayers = {};
		MATTIE.multiplayer.isClient = false;
		MATTIE.multiplayer.isHost = false;
		MATTIE.multiplayer.isActive = false;
	}

	setIsHost() {
		MATTIE.multiplayer.isActive = true;
		MATTIE.multiplayer.isClient = false;
		MATTIE.multiplayer.isHost = true;
	}

	setIsClient() {
		MATTIE.multiplayer.isActive = true;
		MATTIE.multiplayer.isHost = false;
		MATTIE.multiplayer.isClient = true;
	}

	/**
     * @description que a json object to be sent to the main connection.
     * For host this will send to all, for client this will send to host.
     * this is left blank intentionally as it is overridden by host and client
     * @param {*} obj the object to send
     */
	sendViaMainRoute(obj, excludedIds = []) {
		// MATTIE.multiplayer.packetsThisSecond++;
		// if(MATTIE.multiplayer.packetsThisSecond >= MATTIE.multiplayer.maxPacketsPerSecond){

		// }else{
		this.send(obj, excludedIds);
		// }
	}

	send(obj, excludedIds = []) {
		if (MATTIE.multiplayer.isClient) {
			this.sendHost(obj);
		} else if (MATTIE.multiplayer.isHost) {
			this.sendAll(obj, excludedIds);
		}
	}

	/**
     * @description a function that will preprocess the data for onData, before it is read/
     * this is overridden by host and client
     * @param data the data that was sent
     * @param conn the connection that is sending the data
     */
	preprocessData(data, conn) {

	}

	/**
     * @description the main controller receiving data
     * @param data the data obj
     * @param conn the connection object
     */
	onData(data, conn) {
		console.log(data);
		data = this.preprocessData(data, conn);
		const id = data.id;
		if (data.move) {
			this.onMoveData(data.move, id);
			return; // move data is sent by itself always
		}
		if (data.updateNetPlayers && MATTIE.multiplayer.isClient) { // only used by client
			this.onUpdateNetPlayersData(data.updateNetPlayers, data.id);
		}
		if (data.playerInfo && MATTIE.multiplayer.isHost) { // only used by host
			this.onPlayerInfoData(data.playerInfo);
		}
		if (data.startGame && MATTIE.multiplayer.isClient) { // only used by client
			this.onStartGameData(data.startGame);
		}
		if (data.syncedVars && MATTIE.multiplayer.isClient) { // used only by client
			this.onUpdateSyncedVarsData(data.syncedVars);
		}
		if (data.syncedSwitches) {
			this.onUpdateSyncedSwitchData(data.syncedSwitches);
		}
		if (data.requestedVarSync && MATTIE.multiplayer.isHost) { // used only by host
			this.emitUpdateSyncedVars();
		}
		if (data.transfer) {
			this.onTransferData(data.transfer, id);
		}
		if (data.ctrlSwitch) {
			if (SceneManager._scene.isActive() && MATTIE.multiplayer.varSyncer.syncedOnce) { this.onCtrlSwitchData(data.ctrlSwitch, id); }
		}
		if (data.cmd) {
			if (SceneManager._scene.isActive() && MATTIE.multiplayer.varSyncer.syncedOnce) { this.onCmdEventData(data.cmd, data.id); }
		}
		if (data.event) {
			if (SceneManager._scene.isActive() && MATTIE.multiplayer.varSyncer.syncedOnce) { this.onEventMoveEventData(data.event); }
		}
		if (data.battleStart) {
			this.onBattleStartData(data.battleStart, id);
		}
		if (data.battleEnd) {
			this.onBattleEndData(data.battleEnd, id);
		}
		if (data.ready) {
			this.onReadyData(data.ready, id);
		}
		if (data.turnEnd) {
			this.onTurnEndData(data.turnEnd, id);
		}
		if (data.equipChange) {
			this.onEquipmentChangeData(data.equipChange, id);
		}
		if (data.spawnEvent) {
			this.onEventSpawn(data.spawnEvent, data.id);
		}
		if (data.transformEnemy) {
			this.onTransformEventData(data.transformEnemy, data.id);
		}
		if (data.appearEnemy) {
			this.onAppearEnemyEventData(data.appearEnemy, data.id);
		}
		if (data.enemyState) {
			this.onEnemyStateEventData(data.enemyState, data.id);
		}
		if (data.saveEvent) {
			this.onSaveEventData();
		}
		if (data.spectate) {
			this.onSpectateEventData(data.spectate, data.id);
		}
		if (data.runTimeTroopSpawn) {
			this.onRuntimeTroopEvent(data.runTimeTroopSpawn, data.id);
		}
		if (data.pvpEvent) {
			this.onPvpEventData(data.pvpEvent, data.id);
		}
		if (data.transparentEvent) {
			this.onSetTransparentEventData(data.transparentEvent, data.id);
		}
		if (data.setCharImgEvent) {
			this.onSetCharacterImageEventData(data.setCharImgEvent, data.id);
		}
		if (data.dashEvent) {
			this.onDashEventData(data.dashEvent, data.id);
		}
		if (data.moveSpeedEvent) {
			this.onSpeedEventData(data.moveSpeedEvent, data.id);
		}
		if (data.marriageReq) {
			this.onMarriageRequestData(data.marriageReq, data.id);
		}
		if (data.marriageResponse) {
			this.onMarriageResponseData(data.marriageResponse, data.id);
		}
		if (data.saveEventLocationEvent) {
			this.onSaveEventLocationEventData(data.saveEventLocationEvent, data.id);
		}
	}

	//-----------------------------------------------------
	// Turn End Event
	//-----------------------------------------------------

	/**
     *
     * @description emit the turnend event, sending data to connected peers.
     * @param enemyHps {int[]} an array of ints corrispoding to the enemy hps
     *  @param enemyHps {int[][]} an array of arrays of ints which are the state ids of the enemies
     * @emits turnend
     */
	emitTurnEndEvent(enemyHps, enemyStates, actorData) {
		var obj = {};
		obj.turnEnd = {};
		obj.turnEnd.enemyHps = enemyHps;
		obj.turnEnd.enemyStates = enemyStates;
		obj.turnEnd.actorData = actorData;
		console.log(obj);
		this.emit('turnend');
		this.sendViaMainRoute(obj);
	}

	/**
     *
     * @param {*} data the turnEnd obj, for now just contains an array of enemy healths
     */
	onTurnEndData(data, id) {
		if ($gameTroop.getIdsInCombatWithExSelf().includes(id)) {
			const enemyHealthArr = data.enemyHps;
			const actorDataArr = data.actorData;
			const actorHealthArr = actorDataArr.map((data) => data.hp);
			const enemyStatesArr = data.enemyStates;
			if (actorDataArr) { this.syncActorData(actorDataArr, id); }
			if (enemyHealthArr) { this.syncEnemyHealths(enemyHealthArr); }
			if (enemyStatesArr) { this.syncEnemyStates(enemyStatesArr); }
			setTimeout(() => {
				BattleManager.doneSyncing(); // done syncing
			}, MATTIE.multiplayer.combatEmitter.minSyncTime);
		}
	}

	syncActorData(actorDataArr, partyId) {
		const party = this.netPlayers[partyId].battleMembers();
		for (let index = 0; index < actorDataArr.length; index++) {
			/** @type {Game_Actor} */
			const actor = party[index];
			const actorData = actorDataArr[index];
			const newActorHealth = actorData.hp;
			const newActorMana = actorData.mp;
			if (actor.hp > newActorHealth) {
				// actor.performDamage();
				// this.performCosmeticDamageAnimation($gameTroop.members()[0], [actor], 1);
			}
			actor.setHp(newActorHealth);
			actor.setMp(newActorMana);
		}

		// handle despawning chars for pvp
		if (MATTIE.multiplayer.pvp.inPVP) {
			// despawn dead chars
			Object.keys(this.netPlayers).forEach((key) => {
			/** @type {PlayerModel} */
				const player = this.netPlayers[key];
				player.battleMembers().forEach((member) => {
					console.log(member);
					if (member.isDead()) {
						const netActor = player.$netActors.netActor(member.actorId());
						const troopId = MATTIE.multiplayer.pvp.PvpController.mapActorToTroop(
							netActor.baseActorId || member.actorId(),
						); // get the troop for this actor
						const troop = $gameTroop._additionalTroops[troopId];
						console.log(troop);
						console.log(member.actorId());
						if (troop) { troop.despawn(); }
					}
				});
			});
		}
	}

	syncEnemyStates(enemyStatesArr) {
		for (let index = 0; index < $gameTroop.members().length; index++) {
			const enemy = $gameTroop.members()[index];
			const netStates = enemyStatesArr[index];
			if (netStates) {
				for (let j = 0; j < netStates.length; j++) {
					const state = netStates[j];
					if (!enemy.isStateAffected(state)) {
						enemy.addState(state);
					}
				}
			}
		}
	}

	syncEnemyHealths(enemyHealthArr) {
		for (let index = 0; index < $gameTroop.members().length; index++) {
			const enemy = $gameTroop.members()[index];
			if (enemy) {
				const orgHp = enemy.hp;
				enemy.setHp(Math.min(orgHp, enemyHealthArr[index]));
				if (orgHp > 0 && enemy.hp <= 0 && !enemy.hasState(enemy.deathStateId())) {
					enemy.addState(enemy.deathStateId());
					enemy.performCollapse();
					enemy.hide();
				}
			}
		}
	}

	//-----------------------------------------------------
	// Ready Event
	//-----------------------------------------------------

	/**
     * @description trigger the ready event. This event is called when a player enters the ready state in combat.
     * @param {Game_Action[]} an array of the actions that the player has entered.
     * @emits ready
     */
	emitReadyEvent(actions) {
		this.emit('ready');
		var obj = {};
		obj.ready = {};
		obj.ready.val = true;
		obj.ready.actions = actions;
		obj.ready.isExtraTurn = Galv.EXTURN.active;
		$gameTroop.setReadyIfExists(MATTIE.multiplayer.getCurrentNetController().peerId, 1); // set the player as ready in combat arr
		this.sendViaMainRoute(obj);
	}

	/**
     * @description trigger the unready event. This event is called when a player exits the ready state in combat.
     * @emits unready
     */
	emitUnreadyEvent() {
		this.emit('unready');
		var obj = {};
		obj.ready = {};
		obj.ready.val = false;
		obj.ready.isExtraTurn = Galv.EXTURN.active;
		$gameTroop.setReadyIfExists(MATTIE.multiplayer.getCurrentNetController().peerId, 0); // set the player as unready in combat arr
		this.sendViaMainRoute(obj);
	}

	/**
     * @description handles logic for readying and unreadying in combat.
     * @param {*} readyObj the net obj for the ready event
     * @param {*} senderId the sender's id
     */
	onReadyData(readyObj, senderId) {
		const val = readyObj.val;
		const isExtraTurn = readyObj.isExtraTurn;
		const id = senderId;
		if (MATTIE.multiplayer.currentBattleEvent) {
			if (MATTIE.multiplayer.currentBattleEvent.setReadyIfExists) {
				MATTIE.multiplayer.currentBattleEvent.setReadyIfExists(id, val); // set the player as unready in combat arr @legacy
				if (val === false)console.log('net unready recived');
			}
		}
		$gameTroop.setReadyIfExists(id, val, isExtraTurn); // set the player as unready in combat arr <- this one is actually used

		if ($gameTroop.getIdsInCombatWithExSelf().includes(id)) { // only setup net actions if we are in combat with that troop
			if (readyObj.actions) {
				const actions = JSON.parse(readyObj.actions);
				actions.forEach((action) => {
					let shouldAddAction = true;
					if (action) {
						let partyAction = false;
						if (action._item._dataClass === 'skill') {
							const tempAction = new Game_Action($gameActors.actor(1), 0);
							tempAction.setSkill(action._item._itemId);
							tempAction.isForAll();
							partyAction = tempAction.isForAll();
						}
						// wether the action is targeting all members of the party

						/** @type {PlayerModel} */
						const netPlayer = this.netPlayers[senderId];
						/** @type {Game_Actor} */
						const actor = netPlayer.$netActors.baseActor(action._subjectActorId);

						if (action.netPartyId) {
							if (!partyAction) {
								action.forcedTargets = [];
								// eslint-disable-next-line no-nested-ternary
								const targetedNetParty = action.netPartyId != this.peerId
									? this.netPlayers[action.netPartyId].battleMembers()
									: action.netPartyId == this.peerId
										? $gameParty.battleMembers()
										: null;
								if (targetedNetParty) action.forcedTargets.push(targetedNetParty[action._targetIndex]);
								action._netTarget = action.netPartyId;
							} else {
								if (!MATTIE.multiplayer.config.scaling.partyActionsTargetAll) {
									if (action.netPartyId != this.peerId) shouldAddAction = false;
								}
								MATTIE.multiplayer.BattleController.onPartyActionTargetingNet(action);
							}
						}
						if (shouldAddAction) {
							console.log(action);
							if (actor == null) {
								if (action._subjectActorId <= 0) {
									// is enemy action
									console.log($gameTroop.members()[action._subjectEnemyIndex]);
									console.log($gameTroop.members()[action._subjectEnemyIndex]);

									this.processNormalEnemyAction($gameTroop.members()[action._subjectEnemyIndex], action, isExtraTurn, senderId);
								} else {
									// is  bad data
								}
							} else if (!MATTIE.multiplayer.pvp.inPVP) {
								this.processNormalAction(actor, action, isExtraTurn, senderId);
							} else {
								this.processPvpAction(actor, action, isExtraTurn, senderId);
							}
						}
					}
				});
			}
		}
	}

	/**
     * @description add a action to a net battler when not in pvp
     * @param {Game_Actor} actor actor that these actions are for
     * @param {Game_Action} action the action itself
     * @param {bool} isExtraTurn whether this is an extra turn
     * @param {UUID} senderId id of the net user that sent these actions
     */
	processNormalAction(actor, action, isExtraTurn, senderId) {
		actor.partyIndex = () => this.netPlayers[senderId].battleMembers().indexOf(actor); // set the party index function
		actor.setCurrentAction(action);
		BattleManager.addNetActionBattler(actor, isExtraTurn);
	}

	/**
	 * @description add a action to an enemy
	 * @param {Game_Enemy} enemy actor that these actions are for
	 * @param {Game_Action} action the action itself
	 * @param {bool} isExtraTurn whether this is an extra turn
	 * @param {UUID} senderId id of the net user that sent these actions
	 */
	processNormalEnemyAction(enemy, action, isExtraTurn, senderId) {
		action._netTarget = senderId;

		enemy.setCurrentAction(action);
		BattleManager.addNetActionBattler(enemy, isExtraTurn);
		$gameTroop.members()[action._subjectEnemyIndex]._actions.forEach((act) => { act.subject = () => $gameTroop.members()[action._subjectEnemyIndex]; });

		/** @type {Game_Action} */
		const gameAction = enemy._actions[enemy._actions.length - 1];
		enemy._actions[enemy._actions.length - 1].cb = () => {
			if (action.targetResults) {
				if (Object.keys(action.targetResults).some((key) => action.targetResults[key].dmg > 0)) {
					enemy.requestEffect('whiten');
					gameAction.makeTargets().forEach((target) => {
						if (target instanceof Game_Actor) {
							/** @type {Game_Actor} */
							const battler = MATTIE.multiplayer.getCurrentNetController().netPlayers[senderId].$netActors.baseActor(target.actorId());
							if (battler) {
								$gameParty.leader().actorId();

								if (gameAction.item()) {
									const animId = gameAction.item().animationId;
									console.log(battler);
									setTimeout(() => {
										battler.startAnimation(animId);
										setTimeout(() => {
											battler.performDamage();
										}, 1200);
									}, 1500);
								}
							}
						}
					});
				}
			}
		};
	}

	/**
     * @description handle pvp action
     * @param {Game_Actor} actor actor that these actions are for
     * @param {Game_Action} action the action itself
     * @param {bool} isExtraTurn whether this is an extra turn
     * @param {UUID} senderId id of the net user that sent these actions
     */
	processPvpAction(actor, action, isExtraTurn, senderId) {
		let targetActor = $gameActors.actor(action.targetActorId);
		let legCut = false;
		let armCut = false;
		/** @type {Game_Enemy} */

		const originalTargetName = action.targetName;
		action.forcedTargets = [];
		const netActor = this.netPlayers[senderId].$netActors.netActor(actor.actorId());
		const troopId = MATTIE.multiplayer.pvp.PvpController.mapActorToTroop(netActor.baseActorId || actor.actorId()); // get the troop for this actor
		// const originalTarget = $dataTroops[troopId].members.find((member) => $dataEnemies[member.enemyId].name.includes(originalTargetName))
		// || $dataEnemies[$dataTroops[troopId].members[2].enemyId];
		if (troopId) {
			let targetActorIndex = 0;
			/** @type {Game_Enemy} */
			const i = 2;
			const enemies = $gameTroop._additionalTroops[troopId].baseMembers();
			let battler = enemies[i]; // just grab the first member for now
			for (let i = 2; battler.isDead() && i < enemies.length; i++) battler = enemies[i];
			actor.partyIndex = () => this.netPlayers[senderId].battleMembers().indexOf(actor); // just say thye go first for now to test
			action._netTarget = false;

			if (targetActor) {
				// handle replacements
			// that is leader can only be killed if all members have been killed first
				if ($gameParty.leader().actorId() === targetActor.actorId() && $gameParty.battleMembers().length > 1) {
					const battlers = $gameParty.battleMembers();
					const oldTarget = targetActor;
					targetActor = battlers[MATTIE.util.randBetween(1, battlers.length - 1)] || battlers[0];
					console.log(targetActor);
					action.cb = () => {
						BattleManager._logWindow.displaySubstitute(targetActor, oldTarget);
						MATTIE.msgAPI.footerMsg('Your loyal followers protect you!');
					};
				}

				battler.setCurrentAction(action);
				// change the damage formula to be raw damage stat.
				/** @type {rm.types.Item} */
				const clonedItem = JsonEx.makeDeepCopy(battler._actions[battler._actions.length - 1].item());
				clonedItem.damage.formula = 'a.atk';

				battler._actions[battler._actions.length - 1].item = function () {
					return clonedItem;
				};

				targetActorIndex = $gameParty._actors.indexOf(parseInt(targetActor.actorId(), 10));

				battler._actions[battler._actions.length - 1]._netTarget = false;
				battler._actions[battler._actions.length - 1].setSubject(battler);
				battler._actions[battler._actions.length - 1].setTarget(targetActorIndex);

				Object.keys(action.targetResults).forEach((key) => {
					const actor = targetActor;
					// handle damage
					if (originalTargetName.toLowerCase().includes('arm') && action.isKillingBlow) {
						armCut = true;
						battler.enemy().params[2] = 1;
						targetActor.addState(MATTIE.static.states.armCut);
					} else if (originalTargetName.toLowerCase().includes('leg') && action.isKillingBlow) {
						legCut = true;
						battler.enemy().params[2] = 1;
						targetActor.addState(MATTIE.static.states.legCut);
					} else {
						battler.enemy().params[2] = 0;
					}

					if (actor) {
						action.targetResults[battler._actions[battler._actions.length - 1]
							.makeTargetResultsId(actor, this.netPlayers[senderId].peerId)] = action.targetResults[key];
						action.targetResults[battler._actions[battler._actions.length - 1]
							.makeTargetResultsId(actor)] = action.targetResults[key];
					}
				});

				if (armCut || legCut) {
				/** @type {rm.types.Effect} */
					const effect = {};
					effect.code = 21;
					effect.dataId = MATTIE.static.states.bleeding;
					effect.value1 = 1;
					effect.value2 = 0;
					battler._actions[battler._actions.length - 1].item().effects.push(effect);
					effect.code = 21;
					effect.dataId = 3;
					effect.value1 = 1;
					effect.value2 = 0;
					battler._actions[battler._actions.length - 1].item().effects.push(effect);
					effect.code = 44;
					effect.dataId = 240;
					effect.value1 = 1;
					effect.value2 = 0;
					battler._actions[battler._actions.length - 1].item().effects.push(effect);
				}

				battler._actions[battler._actions.length - 1].loadRng(action.targetResults);
			}

			battler.partyIndex = () => action.userBattlerIndex + 1;
			BattleManager.addNetActionBattler(battler, isExtraTurn);
		}
	}

	//-----------------------------------------------------
	// Move Event
	//-----------------------------------------------------

	/**
     * @descriptiona function to emit the move event for the client
     * @emits moveEvent
     * @param {number} direction see below:
     * 8: up
     * 6: left
     * 4: right
     * 2: down
     */
	emitMoveEvent(direction, x = undefined, y = undefined, transfer = false) {
		if (!MATTIE.static.maps.onMenuMap()) { // only fire move event if not in menu
			this.emit('moveEvent', direction, x, y, transfer);
			const obj = {};
			obj.move = {};
			obj.move.d = direction;
			if (x) {
				obj.move.x = x;
				obj.move.y = y;
			}
			if (transfer) {
				obj.move.t = true;
			}
			this.sendViaMainRoute(obj);
		}
	}

	/**
     *  triggers when the receiving movement data from a netPlayer
     * @param {*} moveData up/down/left/right as num 8 6 4 2
     * @param {*} id the peer id of the player who moved
     */
	onMoveData(moveData, id) {
		if (this.netPlayers[id].isMarried) {
			MATTIE.marriageAPI.handleMove.call(this, moveData, id);
		} else {
			this.moveNetPlayer(moveData, id);
		}
	}

	/**
	 * @description smoothly move a net player to a location
	 * @param {*} numSteps number of steps to take
	 * @param {*} player the game char to move
	 * @param {*} delayPerStep the delay between each step
	 * @param {*} location {x:x, y:y} the location obj
	 */
	smoothMoveNetPlayer(numSteps, player, location, delayPerStep = 150) {
		for (let index = 0; index < numSteps; index++) {
			setTimeout(() => {
				try {
					if (SceneManager._scene instanceof Scene_Map) { player.moveTowardCharacter(location); }
				} catch (error) {
					console.warn('player smooth move being bad');
				}
			}, delayPerStep * index);
		}
	}

	/**
     * @description move a net player 1 tile
     * @param {*} moveData which direction on the key pad the player pressed, up/down/left/right as a number 8/6/4/2
     * @param {*} id the id of the peer who's player moved
     */
	moveNetPlayer(moveData, id) {
		if (this.netPlayers[id].map === $gameMap.mapId() && SceneManager._scene instanceof Scene_Map) {
			// only call if on same map and the local player is looking at scene_map IE: not in menu
			if (moveData.x) {
				const dist = Math.sqrt((moveData.x - $gamePlayer._x) ** 2 + (moveData.y - $gamePlayer._y) ** 2);
				if (moveData.t) {
					if (MATTIE.multiplayer.devTools.moveLogger)console.log('transfered char (due to request)');
					moveData.map = $gameMap.mapId();
					this.transferNetPlayer(moveData, id, false);
				} else if (dist > 10) {
					if (MATTIE.multiplayer.devTools.moveLogger)console.log('transfered char (due to dist)');
					moveData.map = $gameMap.mapId();
					this.transferNetPlayer(moveData, id, false);
				} else {
					if (MATTIE.multiplayer.devTools.moveLogger) console.log('tried to move char smoothly');
					const deltaX = moveData.x - this.netPlayers[id].$gamePlayer._x;
					const deltaY = moveData.y - this.netPlayers[id].$gamePlayer._y;
					const numSteps = Math.abs(deltaX) + Math.abs(deltaY);
					this.smoothMoveNetPlayer(numSteps, this.netPlayers[id].$gamePlayer, moveData, 75);
				}
			} else {
				try {
					this.netPlayers[id].$gamePlayer.moveOneTile(moveData.d);
				} catch (error) {
					if (MATTIE.multiplayer.devTools.moveLogger)console.warn(`something went wrong when moving the character${error}`);
				}
			}
		} else if (moveData.x) {
			// if player not on map or local player is in menu update pos ONLY
			this.netPlayers[id].$gamePlayer._x = moveData.x; // update pos only
			this.netPlayers[id].$gamePlayer._y = moveData.y; // update pos only
		}
	}

	//-----------------------------------------------------
	// Transfer Event
	//-----------------------------------------------------

	/** a function to emit the move event for the client
     * @emits transferEvent
     * @param {number} direction see below:
     * 8: up
     * 6: left
     * 4: right
     * 2: down
     */
	emitTransferEvent(transferObj) {
		this.emit('transferEvent', transferObj);
		this.sendViaMainRoute(transferObj);
	}

	/**
     *  triggers when the receiving transfer data from a netPlayer
     * @param {*} transData x,y,map
     * @param {*} id the peer id of the player who moved
     */
	onTransferData(transData, id) {
		this.transferNetPlayer(transData, id);
	}

	/**
     * @description transfer a net player to a location
     * @param {*} transData x,y,map
     * @param {*} id the id of the peer who's player moved
     */
	transferNetPlayer(transData, id, shouldSync = true) {
		const map = transData.map;
		this.netPlayers[id].setMap(map);

		// if not on the map try again later
		if (!(SceneManager._scene instanceof Scene_Map)) {
			// console.log('waiting to transfer');
			setTimeout(() => {
				// console.log('transfering');
				this.transferNetPlayer(transData, id, shouldSync);
			}, 1000);
			return;
		}

		// if on the correct scene
		if (this.transferRetries <= this.maxTransferRetries) {
			try {
				const x = transData.x;
				const y = transData.y;
				const map = transData.map;
				this.netPlayers[id].setMap(map);
				try {
					SceneManager._scene.updateCharacters();
					try {
						const deltaX = transData.x - this.netPlayers[id].$gamePlayer._x;
						const deltaY = transData.y - this.netPlayers[id].$gamePlayer._y;
						const numSteps = Math.abs(deltaX) + Math.abs(deltaY);
						let delay = 0;

						if (numSteps < 5) {
							const delayBetweenStep = 150;
							this.smoothMoveNetPlayer(numSteps, this.netPlayers[id].$gamePlayer, transData, delayBetweenStep);
							delay = numSteps * delayBetweenStep;
						}

						setTimeout(() => {
							this.netPlayers[id].$gamePlayer.reserveTransfer(map, x, y, 0, 0);
							this.netPlayers[id].$gamePlayer.performTransfer(shouldSync);
						}, delay);
					} catch (error) {
						console.warn('player was not on the map when transfer was called');
					}
				} catch (error) {
					console.warn('createSprites was called not on scene_map:');
					this.transferRetries++;
					setTimeout(() => {
						this.transferNetPlayer(transData, id, shouldSync);
					}, 500);
				}
			} catch (error) {
				console.warn(`something went wrong when transferring the character:${error}`);
			}
		} else {
			this.transferRetries = 0;
		}
	}

	//-----------------------------------------------------
	// Battle Start Event
	//-----------------------------------------------------

	/**
     * @description send the battle start event to connections
     * @param {*} eventId the id of the event tile the battle was triggered from
     * @param {*} mapId the id of the map that that tile is on
     * @param {*} troopId the troop id that the player is now incombat with
     */
	emitBattleStartEvent(eventId, mapId, troopId) {
		var obj = {};
		obj.battleStart = {};
		obj.battleStart.eventId = eventId;
		obj.battleStart.mapId = mapId;
		obj.battleStart.troopId = troopId;
		MATTIE.multiplayer.currentBattleEnemy = obj.battleStart;
		MATTIE.multiplayer.currentBattleEvent = $gameMap.event(eventId);
		this.emit('battleStartEvent', obj);
		this.sendViaMainRoute(obj);
		this.emitChangeInBattlersEvent(this.formatChangeInBattleObj(obj.eventId, obj.mapid, this.peerId));
		const event = $gameMap.event(obj.battleStart.eventId);
		if (event) event.addIdToCombatArr(this.peerId);
		this.battleStartAddCombatant(obj.battleStart.troopId, this.peerId);
		MATTIE.emitTransfer(); // emit transfer event to make sure player is positioned correctly on other players screens
	}

	onBattleStartData(battleStartObj, id) { // take the battleStartObj and set that enemy as "in combat" with id
		this.battleStartAddCombatant(battleStartObj.troopId, id);
		this.netPlayers[id].troopInCombatWith = battleStartObj.troopId;

		// handle all logic needed for the event tile
		if (battleStartObj.mapId == $gameMap.mapId()) { // event tile tracking can only be done on same screen
			if (MATTIE.multiplayer.devTools.battleLogger) console.info('net event battle start --on enemy host');
			var event = $gameMap.event(battleStartObj.eventId);
			event.addIdToCombatArr(id);
			event.lock();
			event._trueLock = true;
		}
		this.emitChangeInBattlersEvent(this.formatChangeInBattleObj(battleStartObj.eventId, battleStartObj.mapid, id));
	}

	/** called whenever anyone enters or leaves a battle, contains the id of the player and the battle */
	emitChangeInBattlersEvent(obj) {
		MATTIE.multiplayer.BattleController.emitNetBattlerRefresh();
		this.emit('battleChange', obj);
	}

	formatChangeInBattleObj(eventid, mapid, peerid) {
		var obj = {};
		obj.eventId = eventid;
		obj.mapId = mapid;
		obj.peerId = peerid;
		return obj;
	}

	/**
     * @description removes a combatant to the gameTroop's combat arr. This matches based on name and id,
     * this is to help with some of the spegetti that the dev does with battle events
     * @param {*} troopId
     * @param {*} id
     */
	battleEndRemoveCombatant(troopId, id) {
		if ($dataTroops[troopId]) {
			const troopName = $dataTroops[troopId].name;

			$dataTroops.forEach((element) => {
				if (element) {
					if (element.name === troopName) {
						if (element._combatants) {
							delete element._combatants[id];
						}
					}
				}
			});

			$gameTroop.removeIdFromCombatArr(id);
			if ($dataTroops[troopId]._combatants) {
				delete $dataTroops[troopId]._combatants[id];
			}
		}
	}

	/**
     * @description adds a combatant to the gameTroop's combat arr. This matches based on name and id, this is to help with some of the
     * spegetti that the dev does with battle events
     * @param {*} troopId
     * @param {*} id
     */
	battleStartAddCombatant(troopId, id) {
		const troopName = $dataTroops[troopId].name;
		$dataTroops.forEach((element) => {
			if (element) {
				if (element.name === troopName) {
					if (element._combatants) {
						element._combatants[id] = {};
						element._combatants[id].bool = 0;
						element._combatants[id].isExtraTurn = 0;
					} else {
						element._combatants = {};
						element._combatants[id] = {};
						element._combatants[id].bool = 0;
						element._combatants[id].isExtraTurn = 0;
					}
				}
			}
		});

		if ($gameTroop.name == troopName) {
			$gameTroop.addIdToCombatArr(id);
		}

		if ($gameTroop._troopId == troopId) {
			$gameTroop.addIdToCombatArr(id);
		} else if ($dataTroops[troopId]._combatants) {
			$dataTroops[troopId]._combatants[id] = {};
			$dataTroops[troopId]._combatants[id].bool = 0;
			$dataTroops[troopId]._combatants[id].isExtraTurn = 0;
		} else {
			$dataTroops[troopId]._combatants = {};
			$dataTroops[troopId]._combatants[id] = {};
			$dataTroops[troopId]._combatants[id].bool = 0;
			$dataTroops[troopId]._combatants[id].isExtraTurn = 0;
		}
	}

	//-----------------------------------------------------
	// Battle End Event
	//-----------------------------------------------------

	/**
     * a function to emit the battle end event
     * @param {int} troopId the id of the troop
     * @param {obj} enemy the net obj for an enemy
     */
	emitBattleEndEvent(troopId, enemy) {
		var obj = {};
		obj.battleEnd = enemy;
		obj.battleEnd.troopId = troopId;
		this.emit('battleEndEvent', obj);
		this.emitChangeInBattlersEvent(this.formatChangeInBattleObj(obj.eventId, obj.mapid, this.peerId));
		if ($gameMap.event(obj.battleEnd.eventId))$gameMap.event(obj.battleEnd.eventId).removeIdFromCombatArr(this.peerId);
		this.battleEndRemoveCombatant(obj.battleEnd.troopId, this.peerId);

		this.sendViaMainRoute(obj);

		Object.keys(this.netPlayers).forEach((key) => {
			const netPlayer = this.netPlayers[key];
			netPlayer.clearBattleOnlyMembers();
		});
	}

	onBattleEndData(battleEndObj, id) { // take the battleEndObj and set that enemy as "out of combat" with id
		this.battleEndRemoveCombatant(battleEndObj.troopId, id);
		this.netPlayers[id].troopInCombatWith = null;

		// handle all logic needed for the event tile
		if (battleEndObj.mapId == $gameMap.mapId()) { // event tile tracking can only be done on same screen
			if (MATTIE.multiplayer.devTools.enemyHostLogger) console.debug('net event battle end --on enemy host');
			console.debug('net player left event');
			var event = $gameMap.event(battleEndObj.eventId);
			event.removeIdFromCombatArr(id);

			if (!event.inCombat()) {
				setTimeout(() => {
					event.unlock();
					event._trueLock = false;
				}, MATTIE.multiplayer.runTime);
			} else event.lock();
		}
		this.emitChangeInBattlersEvent(this.formatChangeInBattleObj(battleEndObj.eventId, battleEndObj.mapid, id));
	}

	//-----------------------------------------------------
	// Player Sync Event
	//-----------------------------------------------------

	// this is an event used to sync players on the same map making sure everyone renders in promptly

	// /**
	//  * @description used by the client to request a sync from the host
	//  * @clientOnly
	//  * @emits requestSyncEvent
	//  */
	// emitRequestSyncEvent(){
	//     let obj = {};
	//     obj.requestPlayerSync = {};
	//     obj.requestPlayerSync.mapId = $gameMap.mapId();
	//     this.emit("requestSyncEvent");
	// }

	// /**
	//  * @description when the host recives a request to sync player data do this
	//  * @param {Object} syncObj the net obj for requestPlayerSync event
	//  * @param {UUID} senderId the sender's id
	//  */
	// onRequestSyncEventData(syncObj, senderId){
	//     let senderMapId = syncObj.mapId;

	//     let playersOnMap = this.netPlayers.filter(player=>player.mapId===senderMapId);
	//     let obj = {};
	//     obj.playerSyncData = {};
	//     obj.playerSyncData.playersOnMap
	// }

	//-----------------------------------------------------
	// Update Net Players Event
	//-----------------------------------------------------

	/**
     * handle when the host sends an updated list of netplayers
     * @emits updateNetPlayers
     */
	onUpdateNetPlayersData(netPlayers, id) {
		this.updateNetPlayers(netPlayers);
		this.updateNetPlayerFollowers(netPlayers);
	}

	/** updates net players
     * @emits updateNetPlayers
     */
	updateNetPlayers(netPlayers) {
		const keys = Object.keys(netPlayers);
		for (let index = 0; index < keys.length; index++) {
			const key = keys[index];
			const netPlayer = netPlayers[key];
			if (!this.netPlayers[key]) {
				this.initializeNetPlayer(netPlayer); // if this player hasn't been defined yet initialize it.
			} else {
				// replace any files that conflict with new data, otherwise keep old data
				this.netPlayers[key] = Object.assign(this.netPlayers[key], netPlayer);
			}
		}
		this.emit('updateNetPlayers', netPlayers);
	}

	updateNetPlayer(playerInfo) {
		const obj = {};
		obj[playerInfo.peerId] = playerInfo;
		this.updateNetPlayers(obj);
		this.emit('updateNetPlayers', [obj]);
	}

	updateNetPlayerFollowers(playerInfo) {
		if (playerInfo.peerId) {
			const netPlayer = this.netPlayers[playerInfo.peerId];
			if (netPlayer) netPlayer.setFollowers(playerInfo.followerIds);
		} else {
			const keys = Object.keys(this.netPlayers);
			for (let index = 0; index < keys.length; index++) {
				const key = keys[index];
				const player = playerInfo[key];
				if (player) {
					const netPlayer = this.netPlayers[player.peerId];
					if (netPlayer) netPlayer.setFollowers(player.followerIds);
				}
			}
		}
	}

	/** add a new player to the list of netPlayers. */
	initializeNetPlayer(playerInfo) {
		const name = playerInfo.name;
		const peerId = playerInfo.peerId;
		const actorId = playerInfo.actorId;
		const followerIds = playerInfo.followerIds;
		this.netPlayers[peerId] = new PlayerModel(name, actorId);
		this.netPlayers[peerId].followerIds = followerIds;
		this.netPlayers[peerId].setPeerId(peerId);
		if (!this.netPlayers[peerId].$gamePlayer) {
			this.netPlayers[peerId].initSecondaryGamePlayer();
		}
	}

	updatePlayerInfo() {
		var actor = $gameParty.leader();
		if (this.player.actorId !== actor.actorId()) { this.player.setActorId(actor.actorId()); }

		// update host/client about the new actor id /follower
		this.sendPlayerInfo();
	}

	/** @emits eventMoveEvent */
	emitEventMoveEvent(event) {
		this.onEventMoveEvent(event);
		this.emit('eventMoveEvent');
	}

	onEventMoveEvent(event) {
		const obj = {};
		obj.event = event;
		this.sendViaMainRoute(obj);
	}

	onEventMoveEventData(eventData) {
		/** @type {Game_Event} */
		const mapId = eventData.mapId;
		const event = $gameMap.event(eventData.id);
		if (event) {
			if (mapId === $gameMap.mapId()) {
				if (Math.abs(event._x - eventData.x) > 2 || Math.abs(event._y - eventData.y) > 2) {
					// event.processMoveCommand(eventData,false,true);
					event.setTransparent(false);
					event._x = eventData.x;
					event._y = eventData.y;
					event._realX = eventData.realX;
					event._realY = eventData.realY;
					event.update();
				}
				event.moveStraight(eventData.d, true);
			}
		}
	}

	//-----------------------------------------------------
	// Control Switch Event
	//-----------------------------------------------------

	/** @emits ctrlSwitch */
	emitSwitchEvent(ctrlSwitch) {
		if (!MATTIE.static.maps.onMenuMap()) {
			// only send switches if not in a menu
			const obj = {};
			obj.ctrlSwitch = ctrlSwitch;
			this.sendViaMainRoute(obj);
			this.emit('ctrlSwitch', obj);
		}
	}

	onCtrlSwitchData(ctrlSwitch, id) {
		if (MATTIE.multiplayer.devTools.eventLogger)console.debug('on ctrl switch data');
		const index = ctrlSwitch.i;
		const val = ctrlSwitch.b;
		const s = ctrlSwitch.s;
		if (MATTIE.multiplayer.devTools.eventLogger) { console.debug(index, val); }
		if (s == 0) {
			$gameSwitches.setValue(index, val, true);
		} else if (s == 1) {
			if (MATTIE.multiplayer.varSyncer.syncedOnce) { // self switches only update if vars are synced, this fixes menus.
				$gameSelfSwitches.setValue(index, val, true);
			}
		} else if (s == 2) {
			if (MATTIE.multiplayer.devTools.varLogger) { console.log(`NetPlayer Set ${index} to ${val}`); }
			$gameVariables.setValue(index, val, true);
		}
	}

	//-----------------------------------------------------
	// Update Random Vars Event
	//-----------------------------------------------------

	/**
     * @hostOnly this should only be used by the host
     * @description send the local synced vars to connection
     * @emits randomVars
     */
	emitUpdateSyncedVars() {
		const obj = {};
		obj.syncedVars = {};
		obj.syncedSwitches = {};
		if (MATTIE.multiplayer.hasLoadedVars) {
			MATTIE.static.variable.syncedVars.forEach((id) => {
				obj.syncedVars[id] = $gameVariables.value(id);
			});

			MATTIE.static.variable.secondarySyncedVars.forEach((id) => {
				obj.syncedVars[id] = $gameVariables.value(id);
			});

			MATTIE.static.switch.syncedSwitches.forEach((id) => {
				obj.syncedSwitches[id] = $gameSwitches.value(id);
			});
			this.sendViaMainRoute(obj);
			this.emit('randomVars', obj);
			MATTIE.multiplayer.varSyncRequested = false;
		} else {
			setTimeout(() => {
				this.emitUpdateSyncedVars();
			}, 1000);
		}
	}

	/**
     * @description used by the client to request vars to be synced
     * @emits requestedVarSync
     */
	emitRequestedVarSync() {
		console.log('client  var sync requested');
		const obj = {};
		obj.requestedVarSync = 1;
		this.sendViaMainRoute(obj);
		this.emit('requestedVarSync');
	}

	/**
     * @description process the data for synced var updates
     * @param {dict[]} syncedVars an array of key pair values
     */
	onUpdateSyncedVarsData(syncedVars) {
		MATTIE.multiplayer.varSyncer.shouldSync = false;
		MATTIE.multiplayer.varSyncer.syncedOnce = true;
		Object.keys(syncedVars).forEach((id) => {
			const val = syncedVars[id];
			$gameVariables.setValue(id, val, true); // last var as true to skip net broadcast
		});
	}

	/**
     * @description process the data for synced var updates
     * @param {{dict[]} syncedSwitch an array of key pair values
     */
	onUpdateSyncedSwitchData(syncedSwitch) {
		MATTIE.multiplayer.varSyncer.shouldSync = false;
		MATTIE.multiplayer.varSyncer.syncedOnce = true;
		Object.keys(syncedSwitch).forEach((id) => {
			const val = syncedSwitch[id];
			$gameSwitches.setValue(id, val, true); // last var as true to skip net broadcast
		});
	}

	//-----------------------------------------------------
	// Command Event
	//-----------------------------------------------------

	/** @emits commandEvent */
	emitCommandEvent(cmd) {
		const obj = {};
		obj.cmd = cmd;
		this.sendViaMainRoute(obj);
		this.emit('commandEvent', cmd);
	}

	/** the cmd object */
	onCmdEventData(cmd, peerId) {
		try {
			$gameMap.refreshIfNeeded();
			const params = cmd.parameters;
			const _character = $gameMap.event(params[0] > 0 ? params[0] : cmd.eventId);
			console.log('character');
			console.log(_character);
			if (cmd === MATTIE.static.rpg.commands.setMovementRoute) {
				if (_character) {
					const moveRoute = params[1];
					const tempCanPass = _character.canPass;
					_character.canPass = () => true;
					_character._moveRouteForcing = true;
					_character.setTransparent(false);
					_character._moveRoute = _character._moveRoute || {
						list: [
							{
								code: 0,
								parameters: [],
							},
						],
						repeat: true,
						skippable: false,
						wait: false,
					};

					const found = false;
					// whether the move is a duplicate
					const validMove = _character.getValidMove(moveRoute);

					// if (last20Steps[0].code === list[list.length - 1].code) {
					// 	console.log('valid by never finding start');
					// 	validMove = true;
					// }

					console.log(`validmove${validMove}`);

					if (validMove) {
						moveRoute.list.forEach((command) => {
							console.log(`moving with cmd;${command.code}`);
							_character.processMoveCommand(command, true);
						});
						_character.canPass = tempCanPass;
						_character._moveRouteForcing = false;
					}
				}
			}
		} catch (error) {
			console.log(error);
		}
	}

	//-----------------------------------------------------
	// Equipment Change Event
	//-----------------------------------------------------
	/**
     * @description emits the event for changing equipment
     * @emits equipChange
     * */
	emitEquipmentChange(actorId, itemSlot, itemId) {
		const obj = {};
		obj.equipChange = {};
		obj.equipChange.actorId = actorId;
		obj.equipChange.itemSlot = itemSlot;
		obj.equipChange.itemId = itemId;
		this.sendViaMainRoute(obj);
		this.emit('equipChange', obj);
	}

	/**
     * @description updates the given net party's actor's equipment
     * @param {*} equipChangeObj the net obj for equip change
     */
	onEquipmentChangeData(equipChangeObj, id) {
		const actorId = equipChangeObj.actorId;
		const itemSlot = equipChangeObj.itemSlot;
		const itemId = equipChangeObj.itemId;
		const actor = this.netPlayers[id].$netActors.baseActor(actorId);
		if (actor) {
			if (itemSlot === 0) {
				actor.forceChangeEquip(itemSlot, $dataWeapons[itemId], true);
			} else {
				actor.forceChangeEquip(itemSlot, $dataArmors[itemId], true);
			}
		}
	}

	//-----------------------------------------------------
	// Marriage Event
	//-----------------------------------------------------
	/**
     * @description emits the event for changing equipment
     * @emits marriageReq
	 * @param targetIds an array of the peerIds of all persons (excluding host involved in this request)
	 * @param response whether this is emitting a response to the request
	 * @param responseBool whether this player says yes or no to the response
     * */
	emitMarriageRequest(targetIds, response = false, responseBool = false) {
		if (!(typeof targetIds == 'object')) targetIds = [targetIds];
		const obj = {};
		obj.marriageReq = {};
		obj.marriageReq.targetIds = targetIds;
		this.sendViaMainRoute(obj);
		this.emit('marriageReq', obj);
	}

	/**
     * @description process the data of a marriage request
     * @param {*} marriageReqObj the net obj for marriagereq change
     */
	onMarriageRequestData(marriageReqObj, id) {
		const targetIds = marriageReqObj.targetIds;
		const hostId = id;
		if (targetIds.includes(this.peerId)) {
			MATTIE.msgAPI.showChoices(['Yes', 'NO'], 0, 0, (n) => {
				// emit a marraige response to everyone, 0 is yes as shown above in the choices
				this.emitMarriageResponse(n == 0, hostId, targetIds);
			}, `show love to ${this.netPlayers[id].name}?`);
		}
	}

	/**
	 * @description send the marraige response to all clients
	 * @emits marriageResponse
	 * @param {boolean} consent whether the local player said yes or no
	 * @param {boolean} hostId the id of the person who initiated the marriage request
	 */
	emitMarriageResponse(consent, hostId, targetIds) {
		const obj = {};
		obj.marriageResponse = {};
		obj.marriageResponse.consent = consent;
		obj.marriageResponse.hostId = hostId;
		obj.marriageResponse.targetIds = targetIds;
		this.emit('marriageResponse', obj);

		this.sendViaMainRoute(obj);
		// lock player in place and make them invisible while it forms
		this.player.conversationModel.marry(consent, false);
		if (consent) {
			this.player.marriedTo = targetIds;
			this.player.marriedTo.push(hostId);
			this.player.marriageHost = hostId;
			this.player.isMarried = true;
			this.player.isMarriageHost = false;
			this.updatePlayerInfo();
		}
	}

	/**
	 * @description process a marriage response
	 */
	onMarriageResponseData(marriageResponse, id) {
		const sender = this.netPlayers[id];
		const consent = marriageResponse.consent;
		// the id of the person who initialized the marriage
		const hostId = marriageResponse.hostId;
		const targetIds = marriageResponse.targetIds;
		if (consent) {
			if (hostId === this.peerId) {
				// if this is the host
				this.player.isMarried = true;
				this.player.marriedTo = targetIds;
				this.player.marriedTo.push(hostId);
				this.player.marriageHost = hostId;
				this.player.isMarriageHost = true;
				this.updatePlayerInfo();

				// display marriage event
				this.player.conversationModel.target = sender;
				this.player.conversationModel.marry(consent, true);
			}
		}
	}

	//-----------------------------------------------------
	// Spawned events (handles item drops and any event spawned at run time)
	//-----------------------------------------------------

	/**
     * @emits spawnEvent
     * @param {*} data data of the event
     */
	emitEventSpawn(data) {
		const obj = {};
		obj.spawnEvent = data;
		this.sendViaMainRoute(obj);
		this.emit('spawnEvent', obj);
	}

	/**
     *
     * @param {*} data event net obj
     * @param id unused
     */
	onEventSpawn(data, id) {
		const event = new MapEvent();
		event.data = data;
		try {
			event.spawn(data.x, data.y, true);
		} catch (error) {
			// setTimeout(() => {
			//     this.onEventSpawn(data,id)
			// }, 1000);
		}
	}

	//-----------------------------------------------------
	// Transform Event
	//-----------------------------------------------------

	/**
     * @description emit the transform enemy event
     * @emits transformEnemy
     * @param {*} enemyIndex the index of the enemy to transform
     * @param {*} transformIndex the transform index
     */
	emitTransformEvent(enemyIndex, transformIndex) {
		const obj = {};
		obj.transformEnemy = {};
		obj.transformEnemy.enemyIndex = enemyIndex;
		obj.transformEnemy.transformIndex = transformIndex;
		this.sendViaMainRoute(obj);
		this.emit('transformEnemy');
	}

	/**
     * @description transform an enemy based on net event
     * @param {*} enemyTransformObj the net obj for enemy transform
     * @param {*} id sender id, make sure local player is in combat with this troop before doing anything
     */
	onTransformEventData(enemyTransformObj, id) {
		if ($gameTroop.getIdsInCombatWithExSelf().includes(id)) {
			const enemyIndex = enemyTransformObj.enemyIndex;
			const transformIndex = enemyTransformObj.transformIndex;
			MATTIE.multiplayer.enemyCommandEmitter.transformEnemy(enemyIndex, transformIndex);
		}
	}

	//-----------------------------------------------------
	// Enemy Appear Event
	//-----------------------------------------------------

	/**
     * @description emit the transform enemy event
     * @emits appearEnemy
     * @param {*} enemyIndex the index of the enemy to appear
     */
	emitAppearEnemyEvent(enemyIndex) {
		const obj = {};
		obj.appearEnemy = {};
		obj.appearEnemy.enemyIndex = enemyIndex;
		this.sendViaMainRoute(obj);
		this.emit('appearEnemy');
	}

	/**
     * @description appear an enemy based on net event
     * @param {*} enemyAppearObj the net obj for enemy transform
     * @param {*} id sender id, make sure local player is in combat with this troop before doing anything
     */
	onAppearEnemyEventData(enemyAppearObj, id) {
		if ($gameTroop.getIdsInCombatWithExSelf().includes(id)) {
			const enemyIndex = enemyAppearObj.enemyIndex;
			MATTIE.multiplayer.enemyCommandEmitter.appearEnemy(enemyIndex);
		}
	}

	//-----------------------------------------------------
	// Enemy Add State Event
	//-----------------------------------------------------

	/**
     * @description emit the state enemy event
     * @emits enemyState
     * @param {int} enemyIndex the index of the enemy to appear
     * @param {bool} addState add or remove the state
     * @param {int} stateId the state id
     */
	emitEnemyStateEvent(enemyIndex, addState, stateId) {
		const obj = {};
		obj.enemyState = {};
		obj.enemyState.enemyIndex = enemyIndex;
		obj.enemyState.addState = addState;
		obj.enemyState.stateId = stateId;
		this.sendViaMainRoute(obj);
		this.emit('enemyState');
	}

	/**
     * @description additional actions that the ned controller needs to perform on specific states triggered
     * @param {int} enemyIndex the enemyIndex
     * @param {int} stateId the stateid
     * @param {bool} add true if the state should be added or removed
     */
	stateSpecificActions(enemyIndex, stateId, add) {
		const enemy = $gameTroop.members()[enemyIndex];
		if (enemy) {
			if (add) {
				switch (stateId) {
				case Game_Battler.prototype.deathStateId():
					if (!enemy.hasState(Game_Battler.prototype.deathStateId())) this.performCosmeticAttack(enemy);
					break;

				default:
					break;
				}
			}
		}
	}

	/**
     * @description change enemy state based on net eobj
     * @param {*} enemyStateObj the net obj for enemy state event
     * @param {*} id sender id, make sure local player is in combat with this troop before doing anything
     */
	onEnemyStateEventData(enemyStateObj, id) {
		if ($gameTroop.getIdsInCombatWithExSelf().includes(id)) {
			const enemyIndex = enemyStateObj.enemyIndex;
			const addState = enemyStateObj.addState;
			const stateId = enemyStateObj.stateId;
			this.stateSpecificActions(enemyIndex, stateId, addState);
			MATTIE.multiplayer.enemyCommandEmitter.stateChange(enemyIndex, addState, stateId);
		}
	}

	//-----------------------------------------------------
	// Runtime troop spawn Event
	//-----------------------------------------------------
	/**
     * @description emit the runtime troop event
     * @emits runtimeTroopSpawn
     * @param {*} troopId the id of the troop being added
     */
	emitRuntimeTroopEvent(troopId) {
		const obj = {};
		obj.runTimeTroopSpawn = troopId;
		this.sendViaMainRoute(obj);
		this.emit('runtimeTroopSpawn');
	}

	/**
     * @description process the enemiesdata object and add a new run time troop to the current combat if in combat with same troop as sender
     * @param {MATTIE.troopAPI.runtimeTroop[]} enemiesData
     * @param {UUID} id the sender's id
     */
	onRuntimeTroopEvent(troopId, id) {
		if ($gameTroop.getIdsInCombatWithExSelf().includes(id)) {
			if (troopId === MATTIE.static.troops.crowMauler) {
				MATTIE.betterCrowMauler.crowCont.invadeBattle(true);
			}
		}
	}

	//-----------------------------------------------------
	// Save Event
	//-----------------------------------------------------

	/**
     * @description used for syncing saves
     * @emits saveEvent
     */
	emitSaveEvent() {
		if (MATTIE.multiplayer.hasLoadedVars) {
			if (MATTIE.multiplayer.params.syncSaves) {
				const obj = {};
				obj.saveEvent = 1;
				this.emit('saveEvent');
				this.sendViaMainRoute(obj);
			}
		}
	}

	/**
     * @description open save menu
     */
	onSaveEventData() {
		if (MATTIE.multiplayer.hasLoadedVars) {
			const prevFunc = Scene_Save.prototype.helpWindowText;
			Scene_Save.prototype.helpWindowText = () => 'An ally trigged a save. Please choose a slot to save.';
			Game_Interpreter.prototype.command352();

			const previousOnSaveFileOk = Scene_Save.prototype.onSavefileOk;
			Scene_Save.prototype.onSavefileOk = function () {
				Scene_File.prototype.onSavefileOk.call(this);
				$gameSystem.onBeforeSave();
				if (DataManager.saveGame(this.savefileId(), false, true)) {
					this.onSaveSuccess();
				} else {
					this.onSaveFailure();
				}
				Scene_Save.prototype.onSavefileOk = previousOnSaveFileOk;
			};

			setTimeout(() => {
				// we are changing the getter for a few seconds just to retrive a string once and then go back to normal
				// this is easier than properly extending.
				Scene_Save.prototype.helpWindowText = prevFunc;
			}, 2000);
		}
	}

	//-----------------------------------------------------
	// Spectate Event
	//-----------------------------------------------------

	/**
     * @description emit the spectate event
     * @emits spectate
     * @param {boolean} bool  true if spectating, false if nolonger spectating
     * @param {UUID} id, the id of the user that is changing specatting
     */
	emitSpectateEvent(bool, id) {
		const obj = {};
		obj.spectate = {};
		obj.spectate.val = bool;
		obj.spectate.id = id;
		this.sendViaMainRoute(obj);
	}

	/**
     *
     * @param {*} spectateObj the net spectate event,
     * @param {*} id the sender id
     */
	onSpectateEventData(spectateObj, id) {
		const bool = spectateObj.val;
		const spectatorId = spectateObj.id;
		if (spectatorId === this.peerId) {
			this.player.setSpectate(bool, true);
		} else {
			this.netPlayers[spectatorId].setSpectate(bool, true);
		}
	}

	//-----------------------------------------------------
	// Pvp Event
	//-----------------------------------------------------
	/**
     * @description emit the pvp event to trigger combat with a net player
     * @param {*} targetedPlayerId
     * @param {bool} bool whether this is starting or ending a fight
     * @emits "pvpEvent"
     */
	emitPvpEvent(targetedPlayerId, bool = true) {
		const obj = {};
		obj.pvpEvent = {};
		obj.pvpEvent.start = bool;
		obj.pvpEvent.targetedPlayer = targetedPlayerId;
		this.sendViaMainRoute(obj);
		this.emit('pvpEvent');
	}

	/** @description process the pvp data event from the client and start a combat if they are targeting this player */
	onPvpEventData(pvpData, senderId) {
		const targetedPlayer = pvpData.targetedPlayer;
		const start = pvpData.start; // whether this event is saying a player joined or left
		if (start) {
			if (this.peerId === targetedPlayer) {
				MATTIE.multiplayer.pvp.PvpController.startCombatWith(senderId);
			} else {
				MATTIE.multiplayer.pvp.PvpController.onCombatantJoin(senderId, targetedPlayer);
			}
		} else {
			MATTIE.multiplayer.pvp.PvpController.onCombatantLeave(senderId, targetedPlayer);
		}
	}

	//-----------------------------------------------------
	// Set Transparent event
	//-----------------------------------------------------

	/**
	 * @description emit the set transparent event to the connected peers
	 * @param {boolean} bool whether the player is transparent or visible
	 * @emits transparentEvent
	 */
	emitSetTransparentEvent(bool) {
		const obj = { transparentEvent: {} };
		obj.transparentEvent.val = bool;
		this.emit('transparentEvent');
		this.sendViaMainRoute(obj);
	}

	/**
	 * @description process and act upon the set transparent event when received.
	 * @param {Object} transparentEventData see emitSetTransparent event defined above for what this object looks like
	 * @param {UUID} id the id of the original sender
	 */
	onSetTransparentEventData(transparentEventData, id) {
		if (this.netPlayers[id]) { this.netPlayers[id].$gamePlayer.setTransparent(transparentEventData.val, true); }
	}

	//-----------------------------------------------------
	// Set CharecterImage event IE: outfit changes
	//-----------------------------------------------------

	/**
	 * @description emit the set charecter image event to the connected peers
	 * @param {string} characterName the name of the charsheet (the file within www/imgs/charecters excluding .png)
	 * @param {int} characterIndex the index within that sprite sheet
	 * @param {int} actorId the id of the actor this is occurring to
	 * @emits setCharImgEvent
	 */
	emitSetCharacterImageEvent(characterName, characterIndex, actorId) {
		const obj = { setCharImgEvent: {} };
		obj.setCharImgEvent.characterIndex = characterIndex;
		obj.setCharImgEvent.characterName = characterName;
		obj.setCharImgEvent.actorId = actorId;
		this.emit('setCharImgEvent');
		this.sendViaMainRoute(obj);
	}

	/**
	 * @description process and act upon the charecter image change event
	 * @param {Object} outfitChangeData the data object defined in the emitter above
	 * @param {UUID} id the id of the original sender
	 */
	onSetCharacterImageEventData(outfitChangeData, id) {
		console.log('outfit Aread');
		console.log(outfitChangeData);
		const charName = outfitChangeData.characterName;
		const charIndex = outfitChangeData.characterIndex;
		const actorId = outfitChangeData.actorId;

		const netPlayer = this.netPlayers[id];
		const netActors = netPlayer.$netActors;
		const actor = netActors.baseActor(actorId);

		console.log(netActors);
		console.log(actor);
		if (actor) {
			actor.setCharacterImage(charName, charIndex);
			actor.refresh(); // this likely does nothing, but its good practice to call here as the engine tends to do similarly
			netPlayer.$gamePlayer.refresh(); // this updates the actual display sprite
		}
		this.additionalOutfitActions(outfitChangeData, id);
	}

	/**
	 * @description additional actions separate to the actual forwarding of the outfit change event that also get run alongside it
	 * @param {Object} outfitChangeData the data object defined in the emitter above
	 * @param {UUID} id the id of the original sender
	 */
	additionalOutfitActions(outfitChangeData, id) {
		const charName = outfitChangeData.characterName;
		const charIndex = outfitChangeData.characterIndex;
		const actorId = outfitChangeData.actorId;
		const netPlayer = this.netPlayers[id];
		const netActors = netPlayer.$netActors;
		const actor = netActors.baseActor(actorId);

		// handle torch logic
		if (actorId === netPlayer.actorId) { // we only care about the main char for torches
			if (charName.toLowerCase().contains('torch')) {
				console.log('PLAYER HAS TORCH');
				netPlayer.$gamePlayer.setTorch(true);
			} else {
				console.log('PLAYER HAS NO TORCH :( *cries');
				netPlayer.$gamePlayer.setTorch(false);
			}
		}
	}

	//-----------------------------------------------------
	// Dash event
	//-----------------------------------------------------

	/**
	 * @description emit the dash event to clients
	 * @param {boolean} isDashing is this player dashing or not
	 * @emits dashEvent
	 */
	emitDashEvent(isDashing) {
		const obj = { dashEvent: { val: isDashing } };
		this.emit('dashEvent');
		this.sendViaMainRoute(obj);
	}

	/**
	 * @description process and perform actions when a dash event is received
	 * @param {Object} dashData the dash event net object defined in the method above
	 * @param {UUID} id the id of the original sender
	 */
	onDashEventData(dashData, id) {
		const isDashing = dashData.val;
		const gamePlayer = this.netPlayers[id].$gamePlayer;
		gamePlayer.isDashing = () => isDashing;
		gamePlayer._isDashing = isDashing;
	}

	//-----------------------------------------------------
	// Move speed change event
	//-----------------------------------------------------
	/**
	 * @description emit the event for a move speed change to clients
	 * @param {number} moveSpeed the new movespeed
	 * @emits "moveSpeedEvent"
	 */
	emitSpeedEvent(moveSpeed) {
		const obj = { moveSpeedEvent: { val: moveSpeed } };
		this.emit('moveSpeedEvent');
		this.sendViaMainRoute(obj);
	}

	/**
	 * @description process the move speed change event
	 * @param {Object} moveSpeedData see method above
	 * @param {Object} id the id of the original sender
	 */
	onSpeedEventData(moveSpeedData, id) {
		const newMoveSpeed = moveSpeedData.val;
		const gamePlayer = this.netPlayers[id].$gamePlayer;
		gamePlayer.setMoveSpeed(newMoveSpeed);
	}

	//-----------------------------------------------------
	// SaveEventLocationEvent (yanfly plugin handling)
	//-----------------------------------------------------

	/**
	 * @description send to all players the event location save event
	 * @param {int} mapId the id of the map
	 * @param {Game_Event} event the id of the event
	 * @emits saveEventLocationEvent
	 */
	emitSaveEventLocationEvent(mapId, event) {
		const obj = {};
		obj.saveEventLocationEvent = {};
		obj.saveEventLocationEvent.mapId = mapId;

		// since we dont wanna send a shit ton of data we will only send the parts of the event that we need to
		obj.saveEventLocationEvent.event = {};
		obj.saveEventLocationEvent.event.eventId = event.eventId();
		obj.saveEventLocationEvent.event.x = event.x;
		obj.saveEventLocationEvent.event.y = event.y;
		obj.saveEventLocationEvent.event.direction = event.direction();

		this.emit('saveEventLocationEvent');
		this.sendViaMainRoute(obj);
	}

	/**
	 * @description handle the event
	 * @param {*} saveEventLocationData an obj as structured in the method above
	 * @param {uuid} id the id of the original sender
	 */
	onSaveEventLocationEventData(saveEventLocationData, id) {
		const event = saveEventLocationData.event;
		const mapId = saveEventLocationData.mapId;

		// restructure our data so we can use it without editing yanfly's method
		const _eventId = event.eventId;
		event.eventId = () => _eventId;
		const _direction = event.direction;
		event.direction = () => _direction;

		$gameSystem.saveEventLocation(mapId, event, true);
	}

	//-----------------------------------------------------
	// MISC
	//-----------------------------------------------------

	/**
     * @description transfers all players on the current map to their locations on the current map
     */
	updatePlayersOnCurrentMap() {
		const keys = Object.keys(this.netPlayers);
		const idsOfPlayersOnSameMap = keys.filter((playerKey) => this.netPlayers[playerKey].isOnMap());
		for (let index = 0; index < idsOfPlayersOnSameMap.length; index++) {
			/** @type {PlayerModel} */
			const key = idsOfPlayersOnSameMap[index];
			const player = this.netPlayers[key];
			const transObj = {};
			transObj.x = player.$gamePlayer.x;
			transObj.y = player.$gamePlayer.y;
			transObj.map = player.map;
			this.transferNetPlayer(transObj, player.peerId);
		}
	}

	initEmitterOverrides() {
		if (MATTIE.multiplayer.isActive && !MATTIE.multiplayer.emittedInit) {
			MATTIE.multiplayer.emittedInit = true;
			MATTIE.multiplayer.gamePlayer.override.call(this);
		}
	}

	/**
     * @description perform a solely cosmetic attack
     * @param {Game_Battler} target
     */
	performCosmeticAttack(target = null) {
		return true;
		// eslint-disable-next-line no-unreachable
		const ids = $gameTroop.getIdsInCombatWithExSelf();
		if (ids) {
			if (ids.length > 0) {
				/** @type {Game_Actor} */
				const party = this.netPlayers[ids[MATTIE.util.randBetween(0, ids.length - 1)]].battleMembers();
				const subject = party[MATTIE.util.randBetween(0, party.length - 1)];

				subject.performAttack();
				if (target) {
					this.performCosmeticDamageAnimation(subject, [target], 1);
				}
			}
		}
	}

	/**
     * @description trigger a damage animation (purely cosmetic)
     * @param {Game_Battler} subject the subject performing the attack
     * @param {Game_Battler[]} targets the targets to display the animation on
     * @param {int} animId
     */
	performCosmeticDamageAnimation(subject, targets, animId) {
		BattleManager._logWindow.showAnimation(subject, targets, animId);
	}
}

// ignore this does nothing, just to get intellisense working. solely used to import into the types file for dev.
try {
	module.exports.BaseNetController = BaseNetController;
} catch (error) {
	// eslint-disable-next-line no-global-assign
	module = {};
	module.exports = {};
}

module.exports.BaseNetController = BaseNetController;