var MATTIE = MATTIE || {};
MATTIE.multiplayer = MATTIE.multiplayer || {};
MATTIE.multiplayer.devTools = {};
MATTIE.multiplayer.enemyEmitter = {};
MATTIE.multiplayer.currentBattleEnemy = MATTIE.multiplayer.currentBattleEnemy || {};
MATTIE.multiplayer.combatEmitter = MATTIE.multiplayer.combatEmitter || {};
/** the max amount of time to spend in the sync state in ms */
MATTIE.multiplayer.combatEmitter.maxSyncTime = 1500;
MATTIE.multiplayer.combatEmitter.minSyncTime = 500;
MATTIE.BattleManagerStartTurn = BattleManager.startTurn;

MATTIE.multiplayer.combatEmitter.netExTurn = false;
MATTIE.multiplayer.ready = false;
MATTIE.multiplayer.waitingOnAllies = false;
/** @description true when the local client is in an extra turn (local or received from net) */
MATTIE.multiplayer.combatEmitter.inExtraTurn = false;
/** @description true when a remote player has an extra turn and local player should be blocked from input */
MATTIE.multiplayer.combatEmitter.netExTurnPending = false;

/** log info with the proper conditionals */
function BattleLog(str) {
	if (MATTIE.multiplayer.devTools.inBattleLogger) console.info(str);
}

//-----------------------------------------------------
// Deterministic Seeded PRNG for Cross-Client Sync
//-----------------------------------------------------

/**
 * @description mulberry32 — a simple, fast 32-bit seeded PRNG.
 * Returns a function that produces deterministic floats in [0, 1).
 * @param {number} seed unsigned 32-bit integer
 * @returns {Function} seeded Math.random replacement
 */
function mulberry32(seed) {
	var a = seed >>> 0;
	return function () {
		a |= 0;
		a = (a + 0x6D2B79F5) | 0;
		var t = Math.imul(a ^ (a >>> 15), 1 | a);
		// eslint-disable-next-line operator-assignment
		t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
		return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
	};
}

/**
 * @description Create a deterministic seed from shared battle state.
 * Both host and client derive the same seed for the same action invocation.
 * @param {Game_Action} action
 * @param {Game_Battler} subject
 * @param {Game_Battler} target
 * @param {number} callIndex per-turn invocation counter
 * @returns {number}
 */
function createBattleSeed(action, subject, target, callIndex) {
	var turnCount = $gameTroop ? $gameTroop.turnCount() : 0;
	var troopId = $gameTroop ? $gameTroop._troopId : 0;
	var skillId = (action && action._item) ? action._item._itemId : 0;
	var subjectIdx = 0;
	if (subject && typeof subject.index === 'function') subjectIdx = subject.index();
	else if (subject && typeof subject.actorId === 'function') subjectIdx = subject.actorId();
	var targetIdx = 0;
	if (target && typeof target.index === 'function') targetIdx = target.index();
	else if (target && typeof target.actorId === 'function') targetIdx = target.actorId();
	return ((turnCount * 73856093) ^ (troopId * 19349663) ^ (skillId * 83492791)
		^ (subjectIdx * 38197) ^ (targetIdx * 97127) ^ ((callIndex || 0) * 12582917)) >>> 0;
}

/** Per-turn counter so repeated invocations against the same target still get unique seeds */
BattleManager._deterministicSeedCounter = 0;

// Defensive input guard: block input for battlers without _exTurn during a net extra turn
MATTIE.multiplayer.combatEmitter._galvCanInput = Game_BattlerBase.prototype.canInput;
Game_BattlerBase.prototype.canInput = function () {
	if ((MATTIE.multiplayer.combatEmitter.netExTurnPending
		|| MATTIE.multiplayer.combatEmitter.netExTurn)
		&& !this._exTurn) {
		return false;
	}
	return MATTIE.multiplayer.combatEmitter._galvCanInput.call(this);
};

// Override setExTurn to immediately notify peers when an extra turn is detected.
// This fires during setupExTurn() → BEFORE the input phase begins, so the remote
// client can block input before the player ever sees the command window.
MATTIE.multiplayer.combatEmitter._originalSetExTurn = BattleManager.setExTurn;
BattleManager.setExTurn = function (status) {
	MATTIE.multiplayer.combatEmitter._originalSetExTurn.call(this, status);
	if (status) {
		var nc = MATTIE.multiplayer.getCurrentNetController();
		if (nc && $gameTroop.totalCombatants() > 1) {
			nc.sendViaMainRoute({ exTurnNotify: { active: true } });
		}
	}
};

MATTIE.multiplayer.battleManagerInit = BattleManager.initMembers;
BattleManager.initMembers = function () {
	MATTIE.multiplayer.battleManagerInit.call(this);
	this._netActors = [];
};

BattleManager.getNetBattlers = function () {
	if (!this._netActors) this._netActors = [];
	return this._netActors;
};

BattleManager.startAfterReady = function () {
	BattleManager._deterministicSeedCounter = 0;
	MATTIE.BattleManagerStartTurn.call(this);
};

/** exit the ready state, returning to the beginning of the input phase
 * TODO: instead of returning to the start of the input phase return to the last input so that players can easily correct mistakes without having to...
 *
*/
BattleManager.unready = function () {
	// Block unready when a remote player has an extra turn — local player must stay locked
	if (MATTIE.multiplayer.combatEmitter.netExTurnPending) {
		BattleLog('unready blocked — waiting for companion extra turn');
		return;
	}
	BattleManager.startInput();
	this._phase = 'input';
	BattleLog('unready!');
	MATTIE.multiplayer.BattleController.emitUnreadyEvent();
};

/** enter the ready state */
BattleManager.ready = function () {
	// Guard: prevent re-entry if already in ready phase (e.g., multiple extra-turn notifications)
	if (this._phase === 'ready') return;

	this._phase = 'ready';

	BattleLog('ready!');
	MATTIE.multiplayer.BattleController.emitReadyEvent(JSON.stringify(this.getAllPlayerActions()));
};

/** enter the syncing state */
BattleManager.sync = function () {
	this._phase = 'syncing';
	BattleLog('syncing');
	setTimeout(() => {
		this.doneSyncing(); // enter done syncing state after 5 seconds
	}, MATTIE.multiplayer.combatEmitter.maxSyncTime);
};

/** enter the done syncing state */
BattleManager.doneSyncing = function () {
	if (this._phase == 'syncing') {
		this._phase = 'doneSyncing';
		BattleLog('donesyncing');
	}
};

/** check if a battler exists in the local party */
Game_Battler.prototype.battlerInParty = function () {
	return $gameParty.battleMembers().contains(this);
};

/** finds all currently inputted actions for the local party and returns an array of them */
BattleManager.getAllPlayerActions = function () {
	const arr = [];
	this.makeActionOrders();
	this._actionBattlers.forEach((battler) => {
		if (battler.isAlive()) {
			/** @type {Game_Action} */
			const action = battler.currentAction();
			if (action) { // only do stuff if the action exists
				if (action.item() != null) {
					action.setNetTarget(MATTIE.multiplayer.getCurrentNetController().peerId);
					action._netActionSlot = arr.length;
					const actionSeed = `${Date.now().toString(36)}:${Math.random().toString(36).substr(2, 6)}:${arr.length}`;
					action._netActionUuid = action._netActionUuid
						|| `${MATTIE.multiplayer.getCurrentNetController().peerId}:${actionSeed}`;
					if (action._subjectActorId > 0) {
						action._netSubjectDataActorId = battler.actorId ? battler.actorId() : action._subjectActorId;
						action._netSubjectUUID = `actor:${MATTIE.multiplayer.getCurrentNetController().peerId}:${action._netSubjectDataActorId}`;
					} else {
						const enemy = battler;
						const enemyId = enemy && typeof enemy.enemyId === 'function' ? enemy.enemyId() : 0;
						action._netSubjectUUID = `enemy:${$gameTroop._troopId}:${action._subjectEnemyIndex}:${enemyId}`;
					}
					const actionTargets = action.makeTargets();
					const hasActorTarget = actionTargets.some((target) => target && target.isActor && target.isActor());
					if (hasActorTarget && !action.netPartyId) {
						action.setNetPartyId(MATTIE.multiplayer.getCurrentNetController().peerId);
					}
					action.preloadRng(actionTargets);
					if (MATTIE.multiplayer.pvp.inPVP) {
						const targetTroopId = $gameTroop.mapMemberIndexToTroopId(action._targetIndex);
						const targetActorId = MATTIE.multiplayer.pvp.PvpController.mapTroopToActor(targetTroopId);
						action.targetActorId = targetActorId;
						action.userBattlerIndex = battler.partyIndex();
					}
					arr.push(action);
				}
			}
		}
	});
	return (arr);
};

Game_Action.prototype.forceHit = function (obj) {
	if (typeof obj != 'undefined') {
		this._forcedHit = obj.res;
		this._preloadMissed = obj.miss;
		this._preloadEvade = obj.evade;
	} else {
		this._forcedHit = undefined;
		this._preloadMissed = undefined;
		this._preloadEvade = undefined;
	}
};

Game_Action.prototype.getNetSubjectPeerId = function () {
	const subject = this.subject ? this.subject() : null;
	const netController = MATTIE.multiplayer.getCurrentNetController();
	return this._netSubjectPeerId
		|| this._netTarget
		|| (subject && (subject.peerId || subject.netID))
		|| (netController && netController.peerId)
		|| 'local';
};

Game_Action.prototype.getNetActionUuid = function () {
	if (!this._netActionUuid) {
		const peerId = this.getNetSubjectPeerId();
		this._netActionUuid = `${peerId}:${Date.now().toString(36)}:${Math.random().toString(36).substr(2, 6)}`;
	}
	return this._netActionUuid;
};

Game_Action.prototype.getNetSubjectUuid = function () {
	if (this._netSubjectUUID) return this._netSubjectUUID;
	if (this._subjectActorId > 0) {
		return `actor:${this.getNetSubjectPeerId()}:${this._netSubjectDataActorId || this._subjectActorId}`;
	}
	const troopId = $gameTroop ? $gameTroop._troopId : 0;
	const subjectEnemyIndex = typeof this._subjectEnemyIndex === 'number' ? this._subjectEnemyIndex : -1;
	const subject = this.subject ? this.subject() : null;
	const subjectEnemyId = subject && typeof subject.enemyId === 'function' ? subject.enemyId() : 0;
	return `enemy:${troopId}:${subjectEnemyIndex}:${subjectEnemyId}`;
};

Game_Action.prototype.getNetTargetPeerId = function (target, id = MATTIE.multiplayer.getCurrentNetController().peerId) {
	const currentPeerId = MATTIE.multiplayer.getCurrentNetController().peerId;
	const isTargetActor = target && typeof target.isActor === 'function' && target.isActor();
	if (!isTargetActor) return 'troop';

	return this.netPartyId
		|| (target && (target.peerId || target.netID))
		|| this._netSubjectPeerId
		|| this._netTarget
		|| id
		|| currentPeerId
		|| 'local';
};

Game_Action.prototype.makeLegacyTargetResultsId = function (target) {
	return `${target.name() + (this.subject().isActor() ? `${this.subject()._classId}-` : '')}-${this._targetIndex}-${this._item._itemId}-`;
};

Game_Action.prototype.makeSimpleTargetResultsId = function (target) {
	const isSubjectActor = this._subjectActorId > 0;
	const subjectType = isSubjectActor ? 'actor' : 'enemy';
	let subjectId = this._netSubjectDataActorId || this._subjectActorId;
	if (!isSubjectActor) {
		subjectId = typeof this._subjectEnemyIndex === 'number' ? this._subjectEnemyIndex : -1;
	}

	const isTargetActor = target && typeof target.isActor === 'function' && target.isActor();
	const targetType = isTargetActor ? 'actor' : 'enemy';
	let targetId = this._targetIndex;
	if (isTargetActor) {
		targetId = target && typeof target.actorId === 'function' ? target.actorId() : this._targetIndex;
	} else {
		targetId = target && typeof target.enemyId === 'function' ? target.enemyId() : this._targetIndex;
	}
	const targetIndex = target && typeof target.index === 'function' ? target.index() : this._targetIndex;

	const itemClass = this._item && this._item._dataClass ? this._item._dataClass : 'skill';
	const itemId = this._item ? this._item._itemId : 0;

	return [
		`s:${subjectType}`,
		`sid:${subjectId}`,
		`t:${targetType}`,
		`tid:${targetId}`,
		`ti:${targetIndex}`,
		`it:${itemClass}:${itemId}`,
		`idx:${this._targetIndex}`,
	].join('|');
};

Game_Action.prototype.makeTargetResultMeta = function (target) {
	const isTargetActor = target && typeof target.isActor === 'function' && target.isActor();
	const itemClass = this._item && this._item._dataClass ? this._item._dataClass : 'skill';
	const itemId = this._item ? this._item._itemId : 0;
	const targetPeerId = this.getNetTargetPeerId(target);
	const subject = this.subject ? this.subject() : null;
	let subjectEnemyId = 0;
	if (this._subjectActorId <= 0 && subject && typeof subject.enemyId === 'function') {
		subjectEnemyId = subject.enemyId();
	}

	return {
		actionUuid: this.getNetActionUuid(),
		subjectUuid: this.getNetSubjectUuid(),
		subjectPeerId: this.getNetSubjectPeerId(),
		subjectActorId: this._subjectActorId > 0 ? (this._netSubjectDataActorId || this._subjectActorId) : 0,
		subjectEnemyIndex: typeof this._subjectEnemyIndex === 'number' ? this._subjectEnemyIndex : -1,
		subjectEnemyId,
		targetType: isTargetActor ? 'actor' : 'enemy',
		targetPeerId,
		targetActorId: isTargetActor && target && typeof target.actorId === 'function' ? target.actorId() : 0,
		targetEnemyId: !isTargetActor && target && typeof target.enemyId === 'function' ? target.enemyId() : 0,
		targetIndex: target && typeof target.index === 'function' ? target.index() : this._targetIndex,
		actionTargetIndex: this._targetIndex,
		itemClass,
		itemId,
	};
};

Game_Action.prototype.getUniqueTargetResultEntries = function (targets) {
	if (!targets || typeof targets !== 'object') return [];
	const seen = [];

	return Object.keys(targets)
		.map((key) => targets[key])
		.filter((entry) => {
			if (!entry || typeof entry !== 'object') return false;
			if (seen.indexOf(entry) !== -1) return false;
			seen.push(entry);
			return true;
		});
};

Game_Action.prototype.findTargetResultByMeta = function (target, targetEntries) {
	if (!Array.isArray(targetEntries) || targetEntries.length === 0) return null;

	const meta = this.makeTargetResultMeta(target);
	const matches = targetEntries.filter((entry) => {
		if (!entry._netMeta || typeof entry._netMeta !== 'object') return false;
		const entryMeta = entry._netMeta;

		if (entryMeta.itemClass !== meta.itemClass || entryMeta.itemId !== meta.itemId) return false;
		if (entryMeta.actionUuid && meta.actionUuid && entryMeta.actionUuid !== meta.actionUuid) return false;
		if (entryMeta.subjectUuid && meta.subjectUuid && entryMeta.subjectUuid !== meta.subjectUuid) return false;
		if (entryMeta.subjectPeerId && meta.subjectPeerId && entryMeta.subjectPeerId !== meta.subjectPeerId) return false;
		if (entryMeta.targetType !== meta.targetType) return false;
		if (entryMeta.targetPeerId && meta.targetPeerId && entryMeta.targetPeerId !== meta.targetPeerId) return false;
		if (entryMeta.subjectActorId !== meta.subjectActorId || entryMeta.subjectEnemyIndex !== meta.subjectEnemyIndex) return false;
		if (Object.prototype.hasOwnProperty.call(entryMeta, 'subjectEnemyId')
			&& entryMeta.subjectEnemyId !== meta.subjectEnemyId) return false;

		if (meta.targetType === 'actor') return entryMeta.targetActorId === meta.targetActorId;
		return entryMeta.targetEnemyId === meta.targetEnemyId;
	});

	if (matches.length === 1) return matches[0];
	if (matches.length === 0) return null;

	const exactIndexMatch = matches.find((entry) => entry._netMeta.targetIndex === meta.targetIndex);
	if (exactIndexMatch) return exactIndexMatch;

	const actionIndexMatch = matches.find((entry) => entry._netMeta.actionTargetIndex === meta.actionTargetIndex);
	if (actionIndexMatch) return actionIndexMatch;

	return matches[0];
};

Game_Action.prototype.makeTargetResultsId = function (target, id = MATTIE.multiplayer.getCurrentNetController().peerId) {
	const subject = this.subject ? this.subject() : null;
	const isSubjectActor = (this._subjectActorId > 0)
		|| (subject && typeof subject.isActor === 'function' && subject.isActor());
	const subjectType = isSubjectActor ? 'actor' : 'enemy';
	let subjectId = 0;
	if (isSubjectActor) {
		subjectId = this._netSubjectDataActorId || this._subjectActorId || 0;
		if (!subjectId && subject && typeof subject.actorId === 'function') subjectId = subject.actorId();
	} else {
		subjectId = typeof this._subjectEnemyIndex === 'number' ? this._subjectEnemyIndex : -1;
		if (subjectId < 0 && subject && typeof subject.index === 'function') subjectId = subject.index();
	}
	const subjectEnemyId = !isSubjectActor && subject && typeof subject.enemyId === 'function' ? subject.enemyId() : 0;
	const subjectPeerId = this._netSubjectPeerId
		|| this._netTarget
		|| (subject && (subject.peerId || subject.netID))
		|| MATTIE.multiplayer.getCurrentNetController().peerId
		|| 'local';
	const actionUuid = this.getNetActionUuid();

	const isTargetActor = target && typeof target.isActor === 'function' && target.isActor();
	const targetType = isTargetActor ? 'actor' : 'enemy';
	let targetId = 0;
	if (isTargetActor) {
		targetId = target && typeof target.actorId === 'function' ? target.actorId() : 0;
	} else {
		targetId = target && typeof target.enemyId === 'function' ? target.enemyId() : 0;
	}
	const targetIndex = target && typeof target.index === 'function' ? target.index() : this._targetIndex;
	const targetPeerId = this.getNetTargetPeerId(target, id);

	const itemClass = this._item && this._item._dataClass ? this._item._dataClass : 'skill';
	const itemId = this._item ? this._item._itemId : 0;

	return [
		`aid:${actionUuid}`,
		`s:${subjectType}`,
		`sid:${subjectId}`,
		`seid:${subjectEnemyId}`,
		`sp:${subjectPeerId}`,
		`t:${targetType}`,
		`tid:${targetId}`,
		`tp:${targetPeerId}`,
		`ti:${targetIndex}`,
		`it:${itemClass}:${itemId}`,
		`idx:${this._targetIndex}`,
	].join('|');
};

Game_Action.prototype.getTargetResultLookupIds = function (target, id = MATTIE.multiplayer.getCurrentNetController().peerId) {
	const currentPeerId = MATTIE.multiplayer.getCurrentNetController().peerId;
	const candidateIds = [
		id,
		this.netPartyId,
		this._netSubjectPeerId,
		this._netTarget,
		currentPeerId,
	].filter((candidate) => !!candidate);

	const lookups = [];
	candidateIds.forEach((candidateId) => {
		lookups.push(this.makeTargetResultsId(target, candidateId));
	});
	lookups.push(this.makeSimpleTargetResultsId(target));
	lookups.push(this.makeLegacyTargetResultsId(target));

	return [...new Set(lookups)];
};

Game_Action.prototype.findTargetResult = function (target, id = MATTIE.multiplayer.getCurrentNetController().peerId) {
	const targets = this.getTargetResults();
	if (!targets) return null;

	// Priority: Indexed lookup (peer-agnostic, guaranteed match if data was transmitted)
	const indexed = this._indexedTargetResults || BattleManager._indexedTargetResults;
	if (Array.isArray(indexed) && indexed.length > 0 && target) {
		const isTargetActor = typeof target.isActor === 'function' && target.isActor();
		if (isTargetActor) {
			const tActorId = typeof target.actorId === 'function' ? target.actorId() : 0;
			const match = indexed.find((e) => e.type === 'actor' && e.actorId === tActorId);
			if (match) return match.result;
		} else {
			const tEnemyId = typeof target.enemyId === 'function' ? target.enemyId() : 0;
			const tIndex = typeof target.index === 'function' ? target.index() : -1;
			const match = indexed.find((e) => e.type === 'enemy' && e.enemyId === tEnemyId && e.index === tIndex);
			if (match) return match.result;
		}
	}

	const resultIds = this.getTargetResultLookupIds(target, id);
	for (let i = 0; i < resultIds.length; i++) {
		const resultId = resultIds[i];
		if (Object.prototype.hasOwnProperty.call(targets, resultId)) {
			if (i > 0 && MATTIE.multiplayer.devTools.battleLogger) {
				console.warn(`[NetRNG] Fallback key hit ${resultId}`);
			}
			return targets[resultId];
		}
	}

	const targetEntries = this.getUniqueTargetResultEntries(targets);
	const metaMatchedResult = this.findTargetResultByMeta(target, targetEntries);
	if (metaMatchedResult) {
		if (MATTIE.multiplayer.devTools.battleLogger) {
			console.warn('[NetRNG] Metadata fallback hit for target result');
		}
		return metaMatchedResult;
	}

	if (this.isForOne && this.isForOne()) {
		if (targetEntries.length > 0) {
			const first = targetEntries[0];
			const allEquivalent = targetEntries.every((entry) => entry.miss === first.miss
				&& entry.evade === first.evade
				&& entry.res === first.res
				&& entry.crit === first.crit
				&& entry.dmg === first.dmg);

			if (allEquivalent) {
				if (MATTIE.multiplayer.devTools.battleLogger) {
					console.warn('[NetRNG] Using single-target deterministic fallback result');
				}
				return first;
			}
		}
	}

	if (MATTIE.multiplayer.devTools.battleLogger) {
		const targetMeta = this.makeTargetResultMeta(target);
		const availableKeys = Object.keys(targets);
		console.warn(`[NetRNG] Missing target result. Tried keys: ${resultIds.join(', ')}`);
		console.warn(`[NetRNG] Missing target meta: ${JSON.stringify(targetMeta)}`);
		console.warn(`[NetRNG] Available result keys(${availableKeys.length}): ${availableKeys.join(', ')}`);
	}

	return null;
};

Game_Action.prototype.makeEffectOutcomeKey = function (effect) {
	if (!effect || !this.item || !this.item()) return null;
	const effects = this.item().effects || [];
	const effectIndex = effects.indexOf(effect);
	if (effectIndex < 0) return null;
	return `${effect.code}:${effect.dataId}:${effectIndex}`;
};

Game_Action.prototype.computeDeterministicEffectOutcomes = function (target) {
	const outcomes = {};
	const item = this.item ? this.item() : null;
	if (!item || !Array.isArray(item.effects)) return outcomes;

	item.effects.forEach((effect) => {
		const key = this.makeEffectOutcomeKey(effect);
		if (!key) return;

		switch (effect.code) {
		case Game_Action.EFFECT_ADD_STATE:
			if (effect.dataId === 0) {
				const attackStateOutcomes = [];
				this.subject().attackStates().forEach((stateId) => {
					let chance = effect.value1;
					chance *= target.stateRate(stateId);
					chance *= this.subject().attackStatesRate(stateId);
					chance *= this.lukEffectRate(target);
					attackStateOutcomes.push({
						stateId,
						apply: Math.random() < chance,
					});
				});
				outcomes[key] = { states: attackStateOutcomes };
			} else {
				let chance = effect.value1;
				if (!this.isCertainHit()) {
					chance *= target.stateRate(effect.dataId);
					chance *= this.lukEffectRate(target);
				}
				outcomes[key] = {
					apply: Math.random() < chance,
				};
			}
			break;
		case Game_Action.EFFECT_REMOVE_STATE:
			outcomes[key] = {
				apply: Math.random() < effect.value1,
			};
			break;
		case Game_Action.EFFECT_ADD_DEBUFF: {
			const chance = target.debuffRate(effect.dataId) * this.lukEffectRate(target);
			outcomes[key] = {
				apply: Math.random() < chance,
			};
			break;
		}
		default:
			break;
		}
	});

	return outcomes;
};

Game_Action.prototype.getDeterministicEffectOutcome = function (target, effect) {
	const targetResult = this.findTargetResult(target);
	if (!targetResult || !targetResult.effectOutcomes) return null;
	const key = this.makeEffectOutcomeKey(effect);
	if (!key) return null;
	return targetResult.effectOutcomes[key] || null;
};
/**
 *
 * @param {Game_Battler[]} targets
 */
Game_Action.prototype.preloadRng = function (targets) {
	BattleManager.targetResults = {};
	BattleManager._indexedTargetResults = [];
	this.targetResults = {};
	this._indexedTargetResults = [];
	targets.forEach((target, targetIdx) => {
		const resultId = this.makeTargetResultsId(target);
		const simpleResultId = this.makeSimpleTargetResultsId(target);
		const legacyResultId = this.makeLegacyTargetResultsId(target);
		const targetResult = {};
		targetResult._netMeta = this.makeTargetResultMeta(target);

		this._preloadMissed = (Math.random() >= this.itemHit(target));
		this._preloadEvade = (!this._preloadMissed && Math.random() < this.itemEva(target));
		const crit = (Math.random() < this.itemCri(target));

		targetResult.crit = crit;
		targetResult.evade = this._preloadEvade;
		targetResult.miss = this._preloadMissed;
		targetResult.res = (!this._preloadMissed && !this._preloadEvade);
		if (Math.random() < this.itemCnt(target)) {
			targetResult.invokeType = 'counter';
		} else if (Math.random() < this.itemMrf(target)) {
			targetResult.invokeType = 'reflect';
		} else {
			targetResult.invokeType = 'normal';
		}
		targetResult.effectOutcomes = this.computeDeterministicEffectOutcomes(target);
		this.targetResults[resultId] = targetResult;
		if (simpleResultId !== resultId) this.targetResults[simpleResultId] = targetResult;
		if (legacyResultId !== resultId) this.targetResults[legacyResultId] = targetResult;
		targetResult.dmg = this.makeDamageValue(target, crit);

		// Indexed lookup entry — peer-agnostic canonical identifier
		const isTargetActor = target && typeof target.isActor === 'function' && target.isActor();
		const indexedEntry = { result: targetResult, idx: targetIdx };
		if (isTargetActor) {
			indexedEntry.type = 'actor';
			indexedEntry.actorId = typeof target.actorId === 'function' ? target.actorId() : 0;
		} else {
			indexedEntry.type = 'enemy';
			indexedEntry.enemyId = typeof target.enemyId === 'function' ? target.enemyId() : 0;
			indexedEntry.index = typeof target.index === 'function' ? target.index() : targetIdx;
		}
		this._indexedTargetResults.push(indexedEntry);

		if (MATTIE.multiplayer.pvp.inPVP) {
			if (targetResult.dmg >= target.hp) this.killingBlow = true;
			this.targetName = target.name();
			// update dmg vals for pvp

			const originalTarget = target;
			const originalTargetName = target.name();
			const baseDmg = targetResult.dmg;
			atkScaler = 0.05;

			// limbs only do 40% dmg
			targetResult.dmg = Math.ceil(MATTIE.util.clamp(baseDmg * atkScaler * 0.4, 6, 89));

			if (originalTargetName.toLowerCase().includes('torso')) {
				targetResult.dmg = Math.ceil(MATTIE.util.clamp(baseDmg * atkScaler, 6, 89));
				// battler.enemy().params[2] = MATTIE.util.clamp(actor.atk * atkScaler, 4, 100);// min damage 4
			} else if (originalTargetName.toLowerCase().includes('head')) {
				// battler.enemy().params[2] = MATTIE.util.clamp(actor.atk * atkScaler * 3, 4, 100);// 1.5x dmg to head
				// head does 3x dmg
				targetResult.dmg = Math.ceil(MATTIE.util.clamp(baseDmg * atkScaler * 3, 6, 99));
			}
		}
	});
	BattleManager.targetResults = Object.assign(BattleManager.targetResults || {}, this.targetResults);
	BattleManager._indexedTargetResults = (BattleManager._indexedTargetResults || []).concat(this._indexedTargetResults);
};

Game_Action.prototype.loadRng = function (results, id) {
	this._id = id;
	if (results && typeof results === 'object') {
		this.targetResults = Object.assign(this.targetResults || {}, results);
	} else {
		this.targetResults = this.targetResults || {};
	}

	if (MATTIE.multiplayer.devTools.battleLogger && Object.keys(this.targetResults).length === 0) {
		console.warn('[NetRNG] loadRng received no target results for action');
	}
};

Game_Action.prototype.loadIndexedResults = function (indexedResults) {
	if (Array.isArray(indexedResults)) {
		this._indexedTargetResults = indexedResults;
	}
};

Game_Action.prototype.getTargetResults = function () {
	return this.targetResults || BattleManager.targetResults;
};

MATTIE.multiplayer.Game_Action_itemEffectAddAttackState = Game_Action.prototype.itemEffectAddAttackState;
Game_Action.prototype.itemEffectAddAttackState = function (target, effect) {
	const deterministicOutcome = this.getDeterministicEffectOutcome(target, effect);
	if (deterministicOutcome && Array.isArray(deterministicOutcome.states)) {
		deterministicOutcome.states.forEach((stateOutcome) => {
			if (stateOutcome && stateOutcome.apply) {
				target.addState(stateOutcome.stateId);
				this.makeSuccess(target);
			}
		});
		return;
	}
	MATTIE.multiplayer.Game_Action_itemEffectAddAttackState.call(this, target, effect);
};

MATTIE.multiplayer.Game_Action_itemEffectAddNormalState = Game_Action.prototype.itemEffectAddNormalState;
Game_Action.prototype.itemEffectAddNormalState = function (target, effect) {
	const deterministicOutcome = this.getDeterministicEffectOutcome(target, effect);
	if (deterministicOutcome && Object.prototype.hasOwnProperty.call(deterministicOutcome, 'apply')) {
		if (deterministicOutcome.apply) {
			target.addState(effect.dataId);
			this.makeSuccess(target);
		}
		return;
	}
	MATTIE.multiplayer.Game_Action_itemEffectAddNormalState.call(this, target, effect);
};

MATTIE.multiplayer.Game_Action_itemEffectRemoveState = Game_Action.prototype.itemEffectRemoveState;
Game_Action.prototype.itemEffectRemoveState = function (target, effect) {
	const deterministicOutcome = this.getDeterministicEffectOutcome(target, effect);
	if (deterministicOutcome && Object.prototype.hasOwnProperty.call(deterministicOutcome, 'apply')) {
		if (deterministicOutcome.apply) {
			target.removeState(effect.dataId);
			this.makeSuccess(target);
		}
		return;
	}
	MATTIE.multiplayer.Game_Action_itemEffectRemoveState.call(this, target, effect);
};

MATTIE.multiplayer.Game_Action_itemEffectAddDebuff = Game_Action.prototype.itemEffectAddDebuff;
Game_Action.prototype.itemEffectAddDebuff = function (target, effect) {
	const deterministicOutcome = this.getDeterministicEffectOutcome(target, effect);
	if (deterministicOutcome && Object.prototype.hasOwnProperty.call(deterministicOutcome, 'apply')) {
		if (deterministicOutcome.apply) {
			target.addDebuff(effect.dataId, effect.value1);
			this.makeSuccess(target);
		}
		return;
	}
	MATTIE.multiplayer.Game_Action_itemEffectAddDebuff.call(this, target, effect);
};

MATTIE.multiplayer.Game_Action_Apply = Game_Action.prototype.apply;
Game_Action.prototype.apply = function (target) {
	const targetResult = this.findTargetResult(target);
	MATTIE.multiplayer.Game_Action_Apply.call(this, target);
	if (targetResult && target && target.result && target.result()) {
		if (Object.prototype.hasOwnProperty.call(targetResult, 'crit')) {
			target.result().critical = targetResult.crit;
		}
	}
};
MATTIE.multiplayer.Game_Action_testApply = Game_Action.prototype.testApply;
Game_Action.prototype.testApply = function (target) {
	if (target && target.result && target.result() && target.result().forceHit) {
		const result = this.findTargetResult(target);
		target.result().forceHit(result || undefined);
	}
	return MATTIE.multiplayer.Game_Action_testApply.call(this, target);
};

/** @description the base make damage value function for a game action */
MATTIE.multiplayer.Game_ActionmakeDamageValue = Game_Action.prototype.makeDamageValue;
Game_Action.prototype.makeDamageValue = function (target, critical) {
	const targetResult = this.findTargetResult(target);
	if (targetResult) {
		if (target && target.result && target.result() && target.result().forceHit) {
			if (Object.prototype.hasOwnProperty.call(targetResult, 'crit')) critical = targetResult.crit;
			if (Object.prototype.hasOwnProperty.call(targetResult, 'dmg')) return targetResult.dmg;
		}
	}
	// Desync detection: if this is a net action and we failed to find preloaded results,
	// flag it so turn-end sync can correct any drift
	const netPeerId = this._netSubjectPeerId || this._netTarget;
	if (!targetResult && netPeerId && netPeerId !== MATTIE.multiplayer.getCurrentNetController().peerId) {
		this._netDamageDesyncDetected = true;
		if (MATTIE.multiplayer.devTools.battleLogger) {
			const targetName = target && target.name ? target.name() : 'unknown';
			const skillId = this._item ? this._item._itemId : '?';
			console.warn(`[NetRNG] DESYNC: No preloaded result for net action (peer=${netPeerId},
				 skill=${skillId}, target=${targetName}). Falling back to local calculation.`);
		}
	}
	return MATTIE.multiplayer.Game_ActionmakeDamageValue.call(this, target, critical);
};

/** @description extend the is hit function to include if it is being forced to hit */
MATTIE.multiplayer.Game_ActionResultisHit = Game_ActionResult.prototype.isHit;
Game_ActionResult.prototype.isHit = function () {
	if (typeof this._forcedHit != 'undefined') {
		this.missed = this._preloadMissed;
		this.evaded = this._preloadEvade;
		return this._forcedHit && !this.missed && !this.evaded;
	}
	return (MATTIE.multiplayer.Game_ActionResultisHit.call(this));
};
/** @description set a action result as forced hit */
Game_ActionResult.prototype.forceHit = function (obj) {
	if (typeof obj != 'undefined') {
		this._forcedHit = obj.res;
		this._preloadMissed = obj.miss;
		this._preloadEvade = obj.evade;
	} else {
		this._forcedHit = undefined;
		this._preloadMissed = undefined;
		this._preloadEvade = undefined;
	}
};

Game_Action.prototype.setNetTarget = function (peerId) {
	this._netSubjectPeerId = peerId;
	this._netTarget = peerId;
};
MATTIE.multiplayer.gameActonSubject = Game_Action.prototype.subject;
Game_Action.prototype.subject = function () {
	const subjectPeerId = this._netSubjectPeerId || this._netTarget;
	const subjectDataActorId = this._netSubjectDataActorId || this._subjectActorId;
	if (subjectPeerId) {
		if (subjectPeerId != MATTIE.multiplayer.getCurrentNetController().peerId) {
			const netPlayer = MATTIE.multiplayer.getCurrentNetController().netPlayers[subjectPeerId];
			if (netPlayer && netPlayer.$netActors) {
				const actor = netPlayer.$netActors.dataActor(subjectDataActorId)
					|| netPlayer.$netActors.baseActor(subjectDataActorId);
				if (actor) return actor;
			}
		}
	}
	if (this._subjectActorId > 0) {
		const localActor = $gameParty.battleMembers().find((member) => member
			&& typeof member.actorId === 'function'
			&& member.actorId() === subjectDataActorId);
		return localActor || $gameActors.actor(subjectDataActorId);
	}
	return $gameTroop.members()[this._subjectEnemyIndex];
};

/** start the combat round */
BattleManager.startTurn = function () {
	if ($gameTroop.totalCombatants() == 1) { // if solo, start next phase
		MATTIE.BattleManagerStartTurn.call(this);
	} else { // if the player is fighting with allies enter "ready" state
		this.ready();
	}
};
BattleManager.getNextSubject = function () {
	if ($gameTroop.turnCount() <= 0) return null;
	this._performedBattlers = this._performedBattlers || [];
	this.makeActionOrders();
	for (;;) {
		var battlerArray = [];
		for (var i = 0; i < this._actionBattlers.length; ++i) {
			var obj = this._actionBattlers[i];
			if (!this._performedBattlers.contains(obj)) battlerArray.push(obj);
		}
		this._actionBattlers = battlerArray;
		var battler = this._actionBattlers.shift();
		if (!battler) return null;
		if (battler.isAlive()) {
			const currentAction = battler.currentAction ? battler.currentAction() : null;
			if (!currentAction) {
				if (MATTIE.multiplayer.devTools.battleLogger) {
					console.warn(`[NetAction] Skipping battler with no current action: ${battler.name ? battler.name() : 'unknown'}`);
				}
			} else {
				this._performedBattlers.push(battler);
				return battler;
			}
		}
	}
};
MATTIE.multiplayer.battlemanageronStart = BattleManager.startBattle;
BattleManager.startBattle = function () {
	MATTIE.multiplayer.combatEmitter.netExTurn = false;
	MATTIE.multiplayer.combatEmitter.netExTurnPending = false;
	MATTIE.multiplayer.ready = false;
	MATTIE.multiplayer.waitingOnAllies = false;
	// Defensive: clear any stale Galv extra-turn flag left over from a prior
	// fight (e.g. a net extra turn whose cleanup was interrupted). If it
	// remains true into the next fight, the 'input' branch will spuriously
	// set netExTurnPending and deadlock both clients.
	if (typeof Galv !== 'undefined' && Galv.EXTURN) Galv.EXTURN.active = false;
	MATTIE.multiplayer.battlemanageronStart.call(this);
	this._netActors = [];
	BattleManager._deterministicSeedCounter = 0;
	BattleManager._indexedTargetResults = [];

	// Trigger a net-battler refresh so Scene_Battle (if already created) rebuilds
	// sprites from the current $gameTroop._combatants.  This compensates for the
	// _netActors wipe above and ensures combatants registered between
	// createAllWindows() and start() are not permanently lost.
	MATTIE.multiplayer.BattleController.emitNetBattlerRefresh();
};

BattleManager.addNetActionBattler = function (battler, isExtraTurn) {
	if (!this._netActionBattlers) this._netActionBattlers = [];
	const netActionBattler = {};
	netActionBattler.battler = battler;
	netActionBattler.isExtraTurn = isExtraTurn;
	this._netActionBattlers.push(netActionBattler);
};

BattleManager.clearNetActionBuffer = function () {
	this._netActionBattlers = [];
};

/**
   * @description this function handles where enemies and local player attack in combat
   * @returns int
   */
Game_Battler.prototype.partyIndex = function () {
	const indexof = $gameParty.battleMembers().indexOf(this);
	return indexof != -1 ? indexof : MATTIE.multiplayer.config.scaling.enemyBattleIndex(this);
};

MATTIE.multiplayer.multiCombat.makeActionOrders = BattleManager.makeActionOrders;
BattleManager.makeActionOrders = function () {
	if (!this._netActionBattlers) this._netActionBattlers = [];
	MATTIE.multiplayer.multiCombat.makeActionOrders.call(this);
	const isNetExPhase = MATTIE.multiplayer.combatEmitter.netExTurn || MATTIE.multiplayer.combatEmitter.netExTurnPending;
	const currentNetBattlers = this._netActionBattlers.filter((netBattler) => {
		if (Galv.EXTURN.active || isNetExPhase) {
			return netBattler.isExtraTurn;
		}
		return true;
	}).map((netBattler) => netBattler.battler);
	let battlers = [];
	if (isNetExPhase) {
		// During a net extra turn, only remote extra-turn battlers act;
		// discard local _actionBattlers (which may include enemies from
		// asymmetric Galv detection).
		battlers = currentNetBattlers;
	} else {
		battlers = this._actionBattlers.concat(currentNetBattlers);
	}

	battlers.forEach((battler) => {
		battler.makeSpeed();
	});
	battlers.sort((a, b) => {
		let val = 0;
		const aAction = a.currentAction ? a.currentAction() : null;
		const bAction = b.currentAction ? b.currentAction() : null;
		const aSlot = aAction && typeof aAction._netActionSlot === 'number' ? aAction._netActionSlot : Number.MAX_SAFE_INTEGER;
		const bSlot = bAction && typeof bAction._netActionSlot === 'number' ? bAction._netActionSlot : Number.MAX_SAFE_INTEGER;
		val = a.partyIndex() - b.partyIndex(); // sort my index in order of decreasing
		if (val === 0) { // if index is same sort by if the battler is a player
			val = (a instanceof Game_Actor ? 1 : 0) - (b instanceof Game_Actor ? 1 : 0);
		}
		if (val === 0) { // if both palyers sort by speed
			val = b.speed() - a.speed();
		}

		if (val === 0) { // finnally sort by id
			if (a.isActor() && b.isActor()) {
				const aPeerId = a.peerId || a.netID || '';
				const bPeerId = b.peerId || b.netID || '';
				val = bPeerId.localeCompare(aPeerId);
			}
		}

		if (val === 0 && aSlot !== bSlot) {
			val = aSlot - bSlot;
		}

		if (val === 0) {
			const aUuid = aAction && aAction._netActionUuid ? aAction._netActionUuid : '';
			const bUuid = bAction && bAction._netActionUuid ? bAction._netActionUuid : '';
			val = aUuid.localeCompare(bUuid);
		}

		return val;
	});

	this._actionBattlers = battlers;
};

Game_Battler.prototype.setCurrentAction = function (action) {
	this.forceAction(action._item._itemId, action._targetIndex, action.forcedTargets);
	const netSubjectPeerId = action._netSubjectPeerId || action._netTarget;
	const netActionUuid = action._netActionUuid;
	const netSubjectUUID = action._netSubjectUUID;
	const netActionSlot = action._netActionSlot;
	this._actions[this._actions.length - 1]._netSubjectPeerId = netSubjectPeerId;
	this._actions[this._actions.length - 1]._netTarget = netSubjectPeerId;
	this._actions[this._actions.length - 1]._netActionUuid = netActionUuid;
	this._actions[this._actions.length - 1]._netSubjectUUID = netSubjectUUID;
	this._actions[this._actions.length - 1]._netActionSlot = netActionSlot;
	this._actions[this._actions.length - 1]._netSubjectDataActorId = action._netSubjectDataActorId;
	this._actions[this._actions.length - 1]._netSubjectEnemyId = action._netSubjectEnemyId;
	this._actions[this._actions.length - 1].netPartyId = action.netPartyId;
	this._actions[this._actions.length - 1].loadRng(action.targetResults);
	this._actions[this._actions.length - 1].loadIndexedResults(action._indexedTargetResults);
	this._actions[this._actions.length - 1].cb = action.cb;

	console.log(action.forcedTargets);
	if (action._item._dataClass === 'item') this._actions[this._actions.length - 1].setItem(action._item._itemId);
};
MATTIE.forceAction = Game_Battler.prototype.forceAction;
Game_Battler.prototype.forceAction = function (skillId, targetIndex, forcedTargets = []) {
	if (forcedTargets.length > 0) this.forcedTargets = forcedTargets;
	MATTIE.forceAction.call(this, skillId, targetIndex);
	if (forcedTargets.length > 0) this._actions[this._actions.length - 1].forcedTargets = forcedTargets;
};

MATTIE.maketargets = Game_Action.prototype.makeTargets;
/** override make targets to return forced target if we need */
Game_Action.prototype.makeTargets = function () {
	if (this.forcedTargets) { // net player targeting someone
		return this.forcedTargets;
	}

	// Only process netPartyId if it's defined and different from current peer
	if (this.netPartyId && this.netPartyId !== MATTIE.multiplayer.getCurrentNetController().peerId) { // host targeting net player
		const net = MATTIE.multiplayer.getCurrentNetController().netPlayers[this.netPartyId];
		let netParty = [];
		if (net && typeof net.battleMembers === 'function') {
			netParty = net.battleMembers();
			// Filter out dead/null members
			netParty = netParty.filter((m) => m && !m.isDead());
			if (netParty.length > 0) {
				return netParty;
			}
		}
		// Fallback to default if net party not found or empty
		console.warn(`[NetAction] Could not find valid targets for netPartyId ${this.netPartyId}`);
	}

	return MATTIE.maketargets.call(this);
};

Game_Action.prototype.setNetPartyId = function (id) {
	this.netPartyId = id;
};

/** check that all combatants on this event are ready */
BattleManager.checkAllPlayersReady = function () {
	return $gameTroop.allReady();
};

/** check that all combatants on this event are ready */
BattleManager.checkAllExtraTurn = function () {
	return $gameTroop.allExTurn();
};

/** check that all combatants on this event are ready */
BattleManager.checkSomeExtraTurn = function () {
	return $gameTroop.someExTurn();
};

/** check that all combatants on this event are ready */
BattleManager.allExTurnReady = function () {
	return $gameTroop.allExTurnReady();
};

// override YANFly's update function to have the ready state aswell
BattleManager.update = function () {
	if (!this.isBusy() && !this.updateEvent()) {
		switch (this._phase) {
		case 'start':
			this.startInput();
			break;

		case 'input':
			if (MATTIE.multiplayer.combatEmitter.netExTurnPending) {
				// A remote player has an extra turn — skip input entirely
				var scene = SceneManager._scene;
				if (scene && scene.endCommandSelection) {
					scene.endCommandSelection();
				}
				this.ready();
				break;
			}
			// If Galv detected an extra turn locally but no local ACTOR has _exTurn,
			// only enemies or remote actors qualified (asymmetric AGI thresholds).
			// Treat this as a net extra turn — the remote player's extra turn takes priority.
			if (Galv.EXTURN.active && !$gameParty.battleMembers().some((m) => m._exTurn)) {
				MATTIE.multiplayer.combatEmitter.netExTurnPending = true;
				var scene = SceneManager._scene;
				if (scene && scene.endCommandSelection) {
					scene.endCommandSelection();
				}
				this.ready();
				break;
			}
			if (!$gameParty.canInput()) { // if the game party cannot input then they are ready
				this.ready();
			}
			break;
		case 'ready':
			// allow 'un-reading' to go back to previous state.
			// and if all players are ready proceed to turn state.

			MATTIE.multiplayer.waitingOnAllies = true;
			if (!MATTIE.multiplayer.combatEmitter.netExTurn) {
				if (MATTIE.multiplayer.combatEmitter.netExTurnPending) { // remote extra turn detected via packet
					if (this.allExTurnReady() && this.checkAllPlayersReady()) {
						// if atleast one player has an extra turn and all players with extra turn are ready.
						MATTIE.multiplayer.combatEmitter.netExTurn = true;
						// Ensure Galv guards fire (skip state ticks, turn count, endTurn processing)
						// even if local setupExTurn did not detect an actor extra turn.
						Galv.EXTURN.active = true;
						this.startAfterReady();
					}
				} else if (this.checkAllPlayersReady()) { // otherwise normal check all ready
					this.startAfterReady();
					setTimeout(() => {
						MATTIE.multiplayer.BattleController.emitUnreadyEvent(); // unready once the round is well and started
					}, 1000);
				}
			}

			break;
		case 'turn':
			MATTIE.multiplayer.waitingOnAllies = false;
			this.updateTurn();
			break;
		case 'action':
			this.updateAction();
			break;
		case 'phaseChange':
			this.updatePhase();
			break;
		case 'actionList':
			this.updateActionList();
			break;
		case 'actionTargetList':
			this.updateActionTargetList();
			break;
		case 'turnEnd':
			if (!MATTIE.multiplayer.combatEmitter.netExTurn) { // if a net player is taking an extra turn, dont unready
				MATTIE.multiplayer.BattleController.emitUnreadyEvent();
				MATTIE.multiplayer.BattleController.emitTurnEndEvent();
			}
			BattleManager.sync();
			break;
		case 'syncing':

			break;
		case 'doneSyncing':
			if (MATTIE.multiplayer.combatEmitter.netExTurn) {
				MATTIE.multiplayer.combatEmitter.netExTurn = false;
				MATTIE.multiplayer.combatEmitter.netExTurnPending = false;
				MATTIE.multiplayer.waitingOnAllies = false;
				MATTIE.multiplayer.ready = false;
				// Clear stale isExtraTurn flags so they don't persist into
				// the next normal turn and falsely trigger netExTurnPending.
				$gameTroop.clearExTurnFlags();
				// Reset Galv's global extra-turn flag and any local actor
				// _exTurn flags. We bypass updateTurnEnd() below (intentionally,
				// to skip state ticks/regen/turn-count), but updateTurnEnd is
				// also what normally calls setExTurn(false) and clears actor
				// _exTurn flags via Galv. Without this reset, Galv.EXTURN.active
				// stays true (forced true at the 'ready' branch above for the
				// remote-extra-turn receiver, and never cleared on the
				// originator either), which makes the next normal turn's
				// 'input' branch wrongly set netExTurnPending=true and
				// deadlocks both clients in the 'ready' phase waiting for
				// allExTurnReady() (which can never become true once the
				// isExtraTurn flags have been cleared above).
				if (typeof BattleManager.setExTurn === 'function') BattleManager.setExTurn(false);
				if ($gameParty && typeof $gameParty.battleMembers === 'function') {
					$gameParty.battleMembers().forEach((m) => { if (m) m._exTurn = false; });
				}
				// Extra turn resolved. Mirror Galv's local extra-turn endTurn:
				// skip state ticks, regen, turn count increment (those only
				// happen on a real full turn). Unready everyone so the
				// combat-ready array is clean, then go straight to startInput
				// for the next real turn.
				MATTIE.multiplayer.BattleController.emitUnreadyEvent();
				MATTIE.multiplayer.BattleController.emitTurnEndEvent();
				this.startInput();
			} else {
				this.updateTurnEnd();
			}

			break;
		case 'battleEnd':
			MATTIE.multiplayer.combatEmitter.netExTurnPending = false;
			MATTIE.multiplayer.BattleController.emitTurnEndEvent();
			this.updateBattleEnd();
			break;
		}
	} else if (this._phase === 'ready') {
		if (!this._readyBlockLogTimer || Date.now() - this._readyBlockLogTimer > 2000) {
			this._readyBlockLogTimer = Date.now();
			var _msgBusy = $gameMessage.isBusy();
			var _sprBusy = this._spriteset ? this._spriteset.isBusy() : 'no_spriteset';
			var _logBusy = this._logWindow ? this._logWindow.isBusy() : 'no_logWindow';
			console.warn(`[ReadyBlock] phase=ready but update blocked. isBusy components: msg=${_msgBusy} sprite=${_sprBusy} log=${_logBusy}`);
		}
	}
};

MATTIE.multiplayer.combatEmitter.startAction = BattleManager.startAction;
BattleManager.startAction = function () {
	var action = this._subject.currentAction();
	if (!action) {
		if (MATTIE.multiplayer.devTools.battleLogger) {
			console.warn('[NetAction] startAction called with no current action; skipping to prevent turn softlock');
		}
		if (this._subject && this._subject.removeCurrentAction) this._subject.removeCurrentAction();
		this.endAction();
		return;
	}
	const isNet = this._subject.isNetActor;
	MATTIE.multiplayer.BattleController.onSkillExecution(action, this._subject, isNet);
};

MATTIE.multiplayer.combatEmitter.invokeAction = BattleManager.invokeAction;
BattleManager.invokeAction = function (subject, target) {
	const action = subject && subject.currentAction ? subject.currentAction() : null;
	if (action && typeof action.findTargetResult === 'function') {
		const targetResult = action.findTargetResult(target);
		if (targetResult && targetResult.invokeType) {
			this._logWindow.push('pushBaseLine');
			if (targetResult.invokeType === 'counter') {
				this.invokeCounterAttack(subject, target);
			} else if (targetResult.invokeType === 'reflect') {
				this.invokeMagicReflection(subject, target);
			} else {
				this.invokeNormalAction(subject, target);
			}
			subject.setLastTarget(target);
			this._logWindow.push('popBaseLine');
			return;
		}
	}

	// No preloaded result — use seeded PRNG so both clients compute identical
	// random values for hit/miss/crit/variance/effects on this action.
	BattleManager._deterministicSeedCounter++;
	var seed = createBattleSeed(action, subject, target, BattleManager._deterministicSeedCounter);
	var rng = mulberry32(seed);
	var origRandom = Math.random;
	var origRandomInt = Math.randomInt;
	Math.random = rng;
	Math.randomInt = function (max) { return Math.floor(rng() * max); };

	if (MATTIE.multiplayer.devTools.battleLogger) {
		var subName = subject && subject.name ? subject.name() : '?';
		var tarName = target && target.name ? target.name() : '?';
		console.log(`[NetRNG] Seeded PRNG for non-preloaded action: ${subName} -> ${tarName} (seed=${seed}, call=${BattleManager._deterministicSeedCounter})`);
	}

	MATTIE.multiplayer.combatEmitter.invokeAction.call(this, subject, target);

	// Restore original random
	Math.random = origRandom;
	Math.randomInt = origRandomInt;
};