Bandit.RIP Community Costumes

Adds new costumes to the game

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Bandit.RIP Community Costumes
// @version      2.2
// @author       Dokuro
// @description  Adds new costumes to the game
// @license      Unlicense
// @match        https://bandit.rip/
// @match        https://bandit.rip/?join=*
// @grant        none
// @run-at       document-idle
// @namespace https://greasyfork.org/users/1552476
// ==/UserScript==

const MOD = {
	COSTUMES_LIST_JSON_URL: "https://dokuro-gh.github.io/data.json",
	COSTUMES_IMAGES_BASE_URL: "https://i.ibb.co/",

	// Unicode codepoints of invisible characters
	ENCODING_CHARACTERS: [8203, 8204, 8205, 8288, 10240, 12644],
};

async function start() {
	await waitGameLoad();
	deobfuscateVariables();
	setupUtilPrototypeFunctions();
	main();
}

async function waitGameLoad() {
	while (window._$ic == null) await sleep(100);
}

function deobfuscateVariables() {
	window.PROJECTILE_CLASSES = window._$9l;
	window.BANDIT_NAMES = window._$ky;

	MOD.ICON_GLOWS = window.BANDIT_NAMES.map((bandit) => {
		return classDatas[bandit].pfp;
	});

	let game = window._$ic;
	game.renderer.costumesData = game.renderer._$hb;

	let spawnMenu = game._$7q;

	game.spawnMenu = spawnMenu;
	window.game = game;

	let GameUtil = window._$hV;
	GameUtil.clone = GameUtil._$4P;
	window.GameUtil = GameUtil;

	window.Sprite = window._$9L;
}

function setupUtilPrototypeFunctions() {
	Object.defineProperty(Object.prototype, "iterate", {
		value: function (handler) {
			Object.keys(this).forEach((key, i) => {
				handler(key, this[key], i);
			});
		},
		configurable: true,
		writable: true,
		enumerable: false,
	});

	Object.defineProperty(Element.prototype, "appendChildren", {
		value: function (children) {
			let items = Array.from(children);

			items.forEach((node) => {
				this.appendChild(node);
			});

			return this;
		},
		configurable: true,
		writable: true,
		enumerable: false,
	});

	Object.defineProperty(Object.prototype, "keyOf", {
		value: function (find) {
			let keys = Object.keys(this);
			for (let i = 0; i < keys.length; i++) {
				let key = keys[i];
				if (this[key] == find)
					return key;
			}
		},
		configurable: true,
		writable: true,
		enumerable: false,
	});

	Object.defineProperty(Object.prototype, "injectBefore", {
		value: function (func, handler) {
			let function_name = this.keyOf(func);
			let orig = this[function_name];

			this[function_name] = function (...args) {
				let result = handler.apply(this, args);
				return orig.apply(this, result);
			};
		},
		configurable: true,
		writable: true,
		enumerable: false,
	});

	Object.defineProperty(Object.prototype, "injectAfter", {
		value: function (func, handler) {
			let funcName = this.keyOf(func);
			let orig = this[funcName];

			this[funcName] = function (...args) {
				let result = orig.apply(this, args);
				return handler.apply(this, [...args, result]);
			};
		},
		configurable: true,
		writable: true,
		enumerable: false,
	});
}

async function main() {
	let data = await fetchCostumesData();

	let costumeManager = new CostumeManager(data.costumes);
	costumeManager.load();

	let selectedCostumes = costumeManager.loadSavedCostumes();

	let uiManager = new UIManager(
		costumeManager,
		data.uiCostumes,
		data.previewIcons,
		selectedCostumes,
	);
	uiManager.setup();

	setupCostumesPatches(costumeManager, selectedCostumes);
	setupProjectilesFix();
}

async function fetchCostumesData() {
	let response = await fetch(MOD.COSTUMES_LIST_JSON_URL);
	return response.json();
}

class CostumeManager {
	constructor(costumesData) {
		this.costumesData = costumesData;
		this.costumesLoaded = {};
	}

	load() {
		this.typeIdLimits = {};
		this.types = {};

		this.costumesData.iterate((bandit, costumeTypes) => {
			let index = 0;

			this.typeIdLimits[bandit] = [];
			this.types[bandit] = [];

			costumeTypes.iterate((type, costumes, typeId) => {
				this.updateGameSpritesData(bandit, type + "preview", 0);

				costumes.forEach((path, costumeId) => {
					this.updateGameSpritesData(bandit, type, costumeId);

					index++;
				});

				this.types[bandit][typeId] = type;
				this.typeIdLimits[bandit][typeId] = index;
			});
		});
	}

	updateGameSpritesData(bandit, type, costumeId) {
		let name = this.getCostumeResourceName(bandit, type, costumeId);

		window.game.renderer.costumesData[name] = {
			k: name,
			ok: bandit,
		};

		window.spritesDatas[name] = GameUtil.clone(spritesDatas[bandit]);
		window.spritesDatas[name].base = name;
	}

	loadSavedCostumes() {
		let arr = {};

		window.BANDIT_NAMES.forEach((bandit) => {
			let val = JSON.parse(localStorage.getItem(bandit));
			if (val == null)
				return;

			if (!Object.keys(this.costumesData[bandit]).includes(val.type))
				return;

			if (val.costumeId >= this.costumesData[bandit][val.type].length)
				return;

			arr[bandit] = val;
		});

		return arr;
	}

	// Game generates 'solid' background based on resource name
	getCostumeResourceName(bandit, type, costumeId) {
		return (
			"/assets/sprites/" +
			bandit +
			"/" +
			bandit +
			"_" +
			type +
			"_" +
			costumeId +
			"_CC"
		);
	}

	decodeCostumeResourceName(resourceName) {
		let arr = resourceName.split("/").pop().split("_");
		return {
			bandit: arr[0],
			type: arr[1],
			costumeId: arr[2],
		};
	}

	loadCostume(resourceName) {
		let data = this.decodeCostumeResourceName(resourceName);
		this.downloadCostume(
			data.bandit,
			data.type,
			data.costumeId,
			resourceName,
		);
	}

	encodeBanditCostumeId(bandit, type, costumeId) {
		let typeId = this.types[bandit].keyOf(type);
		if (typeId == null)
			return null;

		return typeId == 0
			? costumeId
			: costumeId + this.typeIdLimits[bandit][typeId - 1];
	}

	decodeBanditCostumeId(bandit, banditCostumeId) {
		let typeId = this.typeIdLimits[bandit].findIndex((limit) => {
			return banditCostumeId < limit;
		});
		if (typeId == -1)
			return null;

		let type = this.types[bandit][typeId];
		let costumeId =
			typeId == 0
				? banditCostumeId
				: banditCostumeId - this.typeIdLimits[bandit][typeId - 1];

		return {
			type: type,
			costumeId: costumeId,
		};
	}

	downloadCostume(bandit, type, costumeId, resourceName) {
		let image = this.getCostumeImage(bandit, type, costumeId);
		let json = this.getBanditJson(bandit, type);

		// loaderReady
		if (!window.game.renderer._$9C)
			return;

		this.downloadCostumeGeneric(image, json, resourceName);
	}

	getBanditJson(bandit, type) {
		let json;
		if (type == bandit) {
			json = "/assets/sprites/" + bandit + "/" + bandit + ".json";
		} else if (type == "punk") {
			json = "/assets/sprites/pboi/costumes/pboi1.json";
		}

		return json;
	}

	getCostumeImage(bandit, type, costumeId) {
		let image;
		if (type == bandit) {
			image =
				MOD.COSTUMES_IMAGES_BASE_URL +
				this.costumesData[bandit][type][costumeId];
		} else if (type == "punk") {
			image =
				MOD.COSTUMES_IMAGES_BASE_URL +
				this.costumesData.pboi.punk[costumeId];
		}

		return image;
	}

	// Pixi loader loads image (_image resource) for sprite from .json
	// spritesheet by default. I don't want to change the .json, so I just
	// load _image resource myself.
	downloadCostumeGeneric(image, json, resourceName) {
		window.game.renderer._$9C = false;

		window.game.renderer.loader.add(resourceName, json);
		window.game.renderer.loader.add(resourceName + "_image", image);

		window.game.renderer.loader.load((loader, loaded) => {
			let json = loaded[resourceName];
			let image = loaded[resourceName + "_image"];

			let spritesheet = new PIXI.Spritesheet(
				image.texture.baseTexture,
				json.data,
				json.url,
			);

			spritesheet.parse(() => {
				json.spritesheet = spritesheet;
				json.textures = spritesheet.textures;
			});

			window.game.renderer._$qr[resourceName] = 1;
			window.game.renderer._$9C = true;
		});
	}

	async downloadCostumePreview(image, json, resourceName) {
		while (!window.game.renderer._$9C) await sleep(100);

		delete game.renderer.loader.resources[resourceName];
		delete game.renderer.loader.resources[resourceName + "_image"];
		this.downloadCostumeGeneric(image, json, resourceName);
	}
}

class UIManager {
	constructor(costumeManager, uiCostumes, previewIcons, selectedCostumes) {
		this.costumeManager = costumeManager;
		this.uiCostumes = uiCostumes;
		this.previewIcons = previewIcons;
		this.selectedCostumes = selectedCostumes;
		this.selectedBandit = game.spawnMenu._$hq;
	}

	setup() {
		this.setupStyles();
		this.updateGameButtons();
		this.setupPanels();
		this.setupFileButton();
	}

	setupStyles() {
		let spawnScreenContentEl = document.querySelector(
			".fgm-m-c > div:nth-child(1) > div:nth-child(1)",
		);
		spawnScreenContentEl.style.cssText = `
			display: grid;
			grid-template-columns: 1fr auto 1fr;
			align-items: center;
			overflow: hidden;
			width: 100%;
		`;

		let spawnScreenFgmEl = spawnScreenContentEl.children[0];
		spawnScreenFgmEl.style.cssText = `
			grid-column: 2;
		`;

		let invisiblePanelBlockingPointerEvents = document.querySelector(
			".frontpage-game-menu-l",
		);
		invisiblePanelBlockingPointerEvents.style.cssText = `
			pointer-events: none
		`;

		// TODO last row should start from the left
		let style = document.createElement("style");
		style.textContent = `
			.costumes-panel {
				grid-column: 1;
				justify-self: end;
				max-height: 70vh;
				overflow-y: auto;
				overflow-x: hidden;
				padding-right: 20px;
				text-align: right;
				margin-bottom: 250px;
				margin-top: 100px;
				display: none;
			}
			.costumes-panel.shown {
				display: inline-block;
			}
			.costume-button {
				width: 15vh;
				height: 15vh;
				display: inline-block;
				position: relative;
				margin: 5px;
				user-select: none;
				min-width: 85px;
				min-height: 85px;
				cursor: pointer;
				background: rgba(255, 255, 255, 0.08);
				container-type: size;
			}
			.costume-button.selected,
			.costume-button:hover {
				background: rgba(255, 255, 255, 0.25);
			}
			.costume-name {
				color: rgb(200, 200, 200);
				display: inline-block;
				position: absolute;
				left: 50%;
				top: 82%;
				height: 18%;
				width: 95%;
				font-size: 18cqh;
				line-height: 18cqh;
				transform: translate(-50%, 0);
				max-height: 16%;
				text-align: center;
			}
			.costume-icon {
				z-index: 1;
				max-width: 100%;
				margin: 0;
				padding: 0;
				max-height: 76%;
				display: block;
				transform: translate(-50%, 0);
				left: 50%;
				margin-top: 4%;
				width: auto;
				bottom: auto;
			}
		`;

		document.head.appendChild(style);
	}

	setupPanels() {
		this.panels = {};

		let spawnScreenContentEl = document.querySelector(
			".fgm-m-c > div:nth-child(1) > div:nth-child(1)",
		);

		window.BANDIT_NAMES.forEach((bandit) => {
			let panel = this.createPanel();
			panel.buttons = [];

			this.uiCostumes[bandit].iterate((type, value, i) => {
				panel.buttons[type] = [];
				panel.buttons[type + "preview"] = [];

				let button = this.generatePreviewButton(bandit, type);
				button.addEventListener("click", () => {
					this.tryEquipPreview(bandit, type);
				});

				panel.buttons[type + "preview"].push(button);
				panel.appendChild(button);
			});

			this.uiCostumes[bandit].iterate((type, value, i) => {
				let buttons = this.generateButtons(bandit, type);
				buttons.forEach((button, costumeId) => {
					button.addEventListener("click", () => {
						this.tryEquipGeneric(bandit, type, costumeId);
					});
				});

				panel.buttons[type].push(...buttons);
				panel.appendChildren(buttons);
			});

			let data = this.selectedCostumes[bandit];
			if (data != null) {
				this.selectButton(panel, data.type, data.costumeId);
				this.setBanditIconGeneric(bandit, data.type, data.costumeId);
				this.equipCostume(bandit, data.type, data.costumeId);
			}

			spawnScreenContentEl.prepend(panel);

			this.panels[bandit] = panel;
		});

		// selectedBandit
		this.showPanel(window.game.spawnMenu._$hq);
	}

	tryEquipGeneric(bandit, type, costumeId) {
		this.tryEquipCostume(bandit, type, costumeId, () => {
			this.setBanditIconGeneric(bandit, type, costumeId);
			return true;
			// TODO probably move selectedCostume[] = ... here so don't have to
			// check preview on spawn because it can be now stored separately
		});
	}

	// TODO fix invisible costume when cncel
	tryEquipPreview(bandit, type) {
		this.tryEquipCostume(bandit, type + "preview", 0, async () => {
			if (this.previewLoading)
				return false;

			let fileChange = this.waitFileChange();
			let fileCancel = this.waitFileCancel();

			this.openFile.click();

			let input = await Promise.any([fileChange, fileCancel]);
			if (input == null)
				return false;

			this.previewLoading = true;

			let result = await this.waitReaderOnLoad(input.target.files[0]);

			var image = "data:image/png;base64," + btoa(result.target.result);
			var json = this.costumeManager.getBanditJson(bandit, type);
			var resourceName = this.costumeManager.getCostumeResourceName(
				bandit,
				type + "preview",
				0,
			);

			this.costumeManager.downloadCostumePreview(
				image,
				json,
				resourceName,
			);

			this.previewLoading = false;
			this.setBanditIconPreview(bandit, type);

			return true;
		});
	}

	setBanditIconGeneric(bandit, type, costumeId) {
		this.setBanditIcon(
			bandit, // TODO CSSURL.from()
			"url(" +
				MOD.COSTUMES_IMAGES_BASE_URL +
				this.uiCostumes[bandit][type][costumeId].icon +
				")",
		);
	}

	setBanditIconPreview(bandit, type) {
		// TODO generate preview loaded bandit icon from spritesheet
		this.setBanditIcon(
			bandit,
			"url(" +
				MOD.COSTUMES_IMAGES_BASE_URL +
				this.previewIcons[bandit][type] +
				")",
		);
	}

	createPanel() {
		let panel = document.createElement("div");
		panel.className = "costumes-panel";

		return panel;
	}

	generatePreviewButton(bandit, type) {
		return this.createCostumeButton(
			"Preview (" + type + ")",
			MOD.COSTUMES_IMAGES_BASE_URL + this.previewIcons[bandit][type],
		);
	}

	generateButtons(bandit, type) {
		return this.uiCostumes[bandit][type].map((uiCostume) => {
			return this.createCostumeButton(
				uiCostume.name,
				MOD.COSTUMES_IMAGES_BASE_URL + uiCostume.icon,
			);
		});
	}

	createCostumeButton(name, icon) {
		let button = document.createElement("a");
		button.className = "costume-button";

		let iconGlowEl = document.createElement("img");
		iconGlowEl.className = "fgm-class-img fgm-class-img-glow costume-icon";
		iconGlowEl.src = icon;
		iconGlowEl.style.cssText = `
			transform:
				translate(-50%, 0) translateZ(-1px) scaleX(1.11) scaleY(1.06);
		`;
		button.appendChild(iconGlowEl);

		let iconEl = document.createElement("img");
		iconEl.className = "fgm-class-img costume-icon";
		iconEl.src = icon;
		button.appendChild(iconEl);

		let nameEl = document.createElement("span");
		nameEl.className = "costume-name";
		nameEl.textContent = name;
		if (name.length > 14) {
			// Good enough
			let size = 18 - Math.sqrt(name.length - 14) * 1.5;
			nameEl.style["font-size"] = size + "cqh";
		}
		button.appendChild(nameEl);

		return button;
	}

	async tryEquipCostume(bandit, type, costumeId, onEquip) {
		let panel = this.panels[bandit];
		let oldData = this.selectedCostumes[bandit];

		if (oldData == null) {
			if (!(await onEquip(bandit, type, costumeId)))
				return;

			this.selectButton(panel, type, costumeId);
			this.equipCostume(bandit, type, costumeId);

			return;
		}

		let oldType = oldData.type;
		let oldCostumeId = oldData.costumeId;

		if (oldType == type && oldCostumeId == costumeId) {
			this.deselectButton(panel, type, costumeId);
			this.unequipCostume(bandit);
			this.setBanditIcon(bandit, this.defaultIcons[bandit]);
		} else {
			if (!(await onEquip(bandit, type, costumeId)))
				return;

			this.deselectButton(panel, oldType, oldCostumeId);
			this.selectButton(panel, type, costumeId);
			this.equipCostume(bandit, type, costumeId);
		}
	}

	equipCostume(bandit, type, costumeId) {
		let data = {
			type: type,
			costumeId: costumeId,
		};
		this.selectedCostumes[bandit] = data;
		window.localStorage.setItem(bandit, JSON.stringify(data));
	}

	unequipCostume(bandit) {
		this.selectedCostumes[bandit] = null;
		window.localStorage.setItem(bandit, null);
	}

	selectButton(panel, type, costumeId) {
		panel.buttons[type][costumeId].classList.add("selected");
	}

	deselectButton(panel, type, costumeId) {
		panel.buttons[type][costumeId].classList.remove("selected");
	}

	showPanel(bandit) {
		this.panels[bandit].classList.add("shown");
	}

	hidePanel(bandit) {
		this.panels[bandit].classList.remove("shown");
	}

	setupFileButton() {
		this.openFile = document.createElement("input");
		this.openFile.type = "file";
		this.openFile.accept = "image/png";
		this.openFile.style.display = "none";
	}

	waitFileChange() {
		return new Promise((resolve) => {
			let handle = (input) => {
				resolve(input);
				this.openFile.removeEventListener("change", handle);
			};
			this.openFile.addEventListener("change", handle);
		});
	}

	waitFileCancel() {
		return new Promise((resolve) => {
			let handle = () => {
				resolve();
				this.openFile.removeEventListener("cancel", handle);
			};
			this.openFile.addEventListener("cancel", handle);
		});
	}

	waitReaderOnLoad(file) {
		var reader = new FileReader();
		reader.readAsBinaryString(file);

		return new Promise((resolve) => {
			reader.onload = (result) => {
				resolve(result);
			};
		});
	}

	updateGameButtons() {
		this.defaultIcons = {};

		let spawnScreenBanditsContainer =
			document.querySelector(".fgm-classes-c");

		window.BANDIT_NAMES.forEach((bandit) => {
			let banditButton = spawnScreenBanditsContainer.querySelector(
				".fgm-class-" + bandit,
			);

			banditButton.addEventListener("click", () => {
				if (this.selectedBandit != bandit) {
					this.showPanel(bandit);
					this.hidePanel(this.selectedBandit);
					this.selectedBandit = bandit;
				}
			});

			// TODO maybe move this elsewhere logically
			// had to place updateGameButtons above setupPanels
			let banditImg = banditButton.querySelector(".fgm-class-img");
			this.defaultIcons[bandit] = banditImg.style["background-image"];
		});
	}

	setBanditIcon(bandit, icon) {
		let cont = document.querySelector(".fgm-class-" + bandit);
		let imgs = cont.querySelectorAll(".fgm-class-img");
		imgs.forEach((el) => (el.style["background-image"] = icon));
	}
}

class Util {
	static baseTwoNums(n, base) {
		let high = Math.floor(n / base);
		let low = n % base;
		return [high, low];
	}

	// TODO sometimes one character is enough
	static encodeHiddenSymbols(n) {
		let a = this.baseTwoNums(n, MOD.ENCODING_CHARACTERS.length);
		let res =
			String.fromCodePoint(parseInt(MOD.ENCODING_CHARACTERS[a[0]])) +
			String.fromCodePoint(parseInt(MOD.ENCODING_CHARACTERS[a[1]]));

		return res;
	}

	static decodeHiddenSymbolsStr(str) {
		if (str.length < 2)
			return null;

		let cc1 = MOD.ENCODING_CHARACTERS.indexOf(
			str.charCodeAt(str.length - 1),
		);
		let cc2 = MOD.ENCODING_CHARACTERS.indexOf(
			str.charCodeAt(str.length - 2),
		);

		if (cc1 == -1 || cc2 == -1)
			return null;

		return cc1 + cc2 * MOD.ENCODING_CHARACTERS.length;
	}
}

function setupCostumesPatches(costumeManager, selectedCostumes) {
	game.renderer.loader.defaultQueryString = "";

	// Game.spawn_self
	window._$oJ.prototype._$17 = function (a) {
		if (this._$1G == true) {
			this.chat._$kF("You are currently spectating!");
			return -1;
		}

		// 11 lines added
		let bandit = a.c;
		let data = selectedCostumes[bandit];
		if (data != null) {
			let banditCostumeId = costumeManager.encodeBanditCostumeId(
				bandit,
				data.type,
				data.costumeId,
			);
			if (banditCostumeId != null)
				a.n += Util.encodeHiddenSymbols(banditCostumeId);
		}

		this.server.emit("spawn", a);
		this.chat._$nt();
	};

	function parseCostumeResourceName(bandit, nickname) {
		let banditCostumeId = Util.decodeHiddenSymbolsStr(nickname);
		if (banditCostumeId == null)
			return null;

		let data = costumeManager.decodeBanditCostumeId(
			bandit,
			banditCostumeId,
		);
		if (data == null)
			return null;

		return costumeManager.getCostumeResourceName(
			bandit,
			data.type,
			data.costumeId,
		);
	}

	// World.spawn_player
	window._$6G.prototype.injectBefore(
		window._$6G.prototype._$fM,
		(playerId, c) => {
			let bandit = c.c;
			if (bandit != null && bandit != "dummy") {
				let resourceName = parseCostumeResourceName(bandit, c.nickname);
				if (resourceName != null) c.cos = resourceName;
				else if (playerId == game.selfid) {
					let data = selectedCostumes[bandit];
					if (data != null && data.type.endsWith("preview")) {
						c.cos = costumeManager.getCostumeResourceName(
							bandit,
							data.type,
							0,
						);
					}
				}
			}

			return [playerId, c];
		},
	);

	// TODO just add more logic to sprite constructor. it calls this
	// Renderer.loadResource
	window._$fW.prototype._$f2 = function (a) {
		if (!(a in this.loader.resources)) {
			if (a.endsWith("_CC")) {
				costumeManager.loadCostume(a);
			} else if (a in this._$ha) {
				this._$q3();
			} else if (a in this._$jT) {
			} else {
				this._$ha[a] = a;
				this._$q3();
			}
		}

		return a;
	};
}

function setupProjectilesFix() {
	function createSprite(a, b, baseResource) {
		let sprite = new window.Sprite(a, b);
		if (baseResource != null) {
			sprite.base = baseResource;
			sprite.renderer._$f2(sprite.base);
		}

		return sprite;
	}

	// Renderer.draw
	window._$fW.prototype._$6l = function (a) {
		for (var b in a.entities) {
			var c = a.entities[b];
			if (!(c.type in this.spritesDatas)) {
				continue;
			}
			if (!(b in this.entities)) {
				// 1 line changed
				var d = createSprite(this, c.type, c.baseResource);

				this.entities[b] = d;
			}
			this.entities[b]._$qs(c);
		}
		for (var b in a._$4J) {
			this._$O(b);
		}
		if ("camerax" in a && "cameray" in a) {
			this.camerax = a.camerax;
			this.cameray = a.cameray;
		}
		this.maplimits = a["maplimits"];
	};

	window.PROJECTILE_CLASSES.iterate((key, value, i) => {
		if (!(value instanceof Function))
			return;

		// BaseProjectileEntity.create
		value.prototype.injectAfter(
			value.prototype._$5j,
			function (tick, result) {
				let caster = window.game.renderer.entities[this.pid];
				if (caster != null) {
					let bandit = caster.type;
					if (this._$4F != "bullet2" || bandit == "sniper")
						result.baseResource = caster._$hU.base;
				}

				return result;
			},
		);
	});
}

function sleep(ms) {
	return new Promise((resolve) => setTimeout(resolve, ms));
}

start();