Source: basesManager.js

/**
* @class
 * Bases Manager
 * Manages the list of available bases and queries information from those (e.g. resource levels).
 * Only one base is run every turn.
 */

PETRA.BasesManager = function(Config)
{
	this.Config = Config;

	this.currentBase = 0;

	// Cache some quantities for performance.
	this.turnCache = {};

	// Deals with unit/structure without base.
	this.noBase = undefined;

	this.baseManagers = [];
};

PETRA.BasesManager.prototype.init = function(gameState)
{
	// Initialize base map. Each pixel is a base ID, or 0 if not or not accessible.
	this.basesMap = new API3.Map(gameState.sharedScript, "territory");

	this.noBase = new PETRA.BaseManager(gameState, this);
	this.noBase.init(gameState, PETRA.BaseManager.STATE_WITH_ANCHOR);
	this.noBase.accessIndex = 0;

	for (const cc of gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).values())
		if (cc.foundationProgress() === undefined)
			this.createBase(gameState, cc, PETRA.BaseManager.STATE_WITH_ANCHOR);
		else
			this.createBase(gameState, cc, PETRA.BaseManager.STATE_UNCONSTRUCTED);
};

/**
 * Initialization needed after deserialization (only called when deserialising).
 */
PETRA.BasesManager.prototype.postinit = function(gameState)
{
	// Rebuild the base maps from the territory indices of each base.
	this.basesMap = new API3.Map(gameState.sharedScript, "territory");
	for (const base of this.baseManagers)
		for (const j of base.territoryIndices)
			this.basesMap.map[j] = base.ID;

	for (const ent of gameState.getOwnEntities().values())
	{
		if (!ent.resourceDropsiteTypes() || !ent.hasClass("Structure"))
			continue;
		// Entities which have been built or have changed ownership after the last AI turn have no base.
		// they will be dealt with in the next checkEvents
		const baseID = ent.getMetadata(PlayerID, "base");
		if (baseID === undefined)
			continue;
		const base = this.getBaseByID(baseID);
		base.assignResourceToDropsite(gameState, ent);
	}
};

/**
 * Create a new base in the baseManager:
 * If an existing one without anchor already exist, use it.
 * Otherwise create a new one.
 * TODO when buildings, criteria should depend on distance
 */
PETRA.BasesManager.prototype.createBase = function(gameState, ent, type = PETRA.BaseManager.STATE_WITH_ANCHOR)
{
	const access = PETRA.getLandAccess(gameState, ent);
	let newbase;
	for (const base of this.baseManagers)
	{
		if (base.accessIndex != access)
			continue;
		if (type !== PETRA.BaseManager.STATE_ANCHORLESS && base.anchor)
			continue;
		if (type !== PETRA.BaseManager.STATE_ANCHORLESS)
		{
			// TODO we keep the first one, we should rather use the nearest if buildings
			// and possibly also cut on distance
			newbase = base;
			break;
		}
		else
		{
			// TODO here also test on distance instead of first
			if (newbase && !base.anchor)
				continue;
			newbase = base;
			if (newbase.anchor)
				break;
		}
	}

	if (this.Config.debug > 0)
	{
		API3.warn(" ----------------------------------------------------------");
		API3.warn(" BasesManager createBase entrance avec access " + access + " and type " + type);
		API3.warn(" with access " + uneval(this.baseManagers.map(base => base.accessIndex)) +
			  " and base nbr " + uneval(this.baseManagers.map(base => base.ID)) +
			  " and anchor " + uneval(this.baseManagers.map(base => !!base.anchor)));
	}

	if (!newbase)
	{
		newbase = new PETRA.BaseManager(gameState, this);
		newbase.init(gameState, type);
		this.baseManagers.push(newbase);
	}
	else
		newbase.reset(type);

	if (type !== PETRA.BaseManager.STATE_ANCHORLESS)
		newbase.setAnchor(gameState, ent);
	else
		newbase.setAnchorlessEntity(gameState, ent);

	return newbase;
};

/** TODO check if the new anchorless bases should be added to addBase */
PETRA.BasesManager.prototype.checkEvents = function(gameState, events)
{
	let addBase = false;

	for (const evt of events.Destroy)
	{
		// Let's check we haven't lost an important building here.
		if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] &&
			evt.metadata[PlayerID].base)
		{
			const ent = evt.entityObj;
			if (evt?.metadata?.[PlayerID]?.assignedResource)
				this.getBaseByID(evt.metadata[PlayerID].base).removeFromAssignedDropsite(ent);
			if (ent.owner() != PlayerID)
				continue;
			// A new base foundation was created and destroyed on the same (AI) turn
			if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2)
				continue;
			const base = this.getBaseByID(evt.metadata[PlayerID].base);
			if (ent.resourceDropsiteTypes() && ent.hasClass("Structure"))
				base.removeDropsite(gameState, ent);
			if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true)
				base.anchorLost(gameState, ent);
		}
	}

	for (const evt of events.EntityRenamed)
	{
		const ent = gameState.getEntityById(evt.newentity);
		if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined)
			continue;
		const base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
		if (!base.anchorId || base.anchorId != evt.entity)
			continue;
		base.anchorId = evt.newentity;
		base.anchor = ent;
	}

	for (const evt of events.Create)
	{
		// Let's check if we have a valuable foundation needing builders quickly
		// (normal foundations are taken care in baseManager.assignToFoundations)
		const ent = gameState.getEntityById(evt.entity);
		if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined)
			continue;

		if (ent.getMetadata(PlayerID, "base") == -1)	// Standard base around a cc
		{
			// Okay so let's try to create a new base around this.
			const newbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_UNCONSTRUCTED);
			// Let's get a few units from other bases there to build this.
			const builders = this.bulkPickWorkers(gameState, newbase, 10);
			if (builders !== false)
			{
				builders.forEach(worker => {
					worker.setMetadata(PlayerID, "base", newbase.ID);
					worker.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER);
					worker.setMetadata(PlayerID, "target-foundation", ent.id());
				});
			}
		}
		else if (ent.getMetadata(PlayerID, "base") == -2)	// anchorless base around a dock
		{
			const newbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_ANCHORLESS);
			// Let's get a few units from other bases there to build this.
			const builders = this.bulkPickWorkers(gameState, newbase, 4);
			if (builders != false)
			{
				builders.forEach(worker => {
					worker.setMetadata(PlayerID, "base", newbase.ID);
					worker.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER);
					worker.setMetadata(PlayerID, "target-foundation", ent.id());
				});
			}
		}
	}

	for (const evt of events.ConstructionFinished)
	{
		if (evt.newentity == evt.entity)  // repaired building
			continue;
		const ent = gameState.getEntityById(evt.newentity);
		if (!ent || ent.owner() != PlayerID)
			continue;
		if (ent.getMetadata(PlayerID, "base") === undefined)
			continue;
		const base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
		base.buildings.updateEnt(ent);
		if (ent.resourceDropsiteTypes())
			base.assignResourceToDropsite(gameState, ent);

		if (ent.getMetadata(PlayerID, "baseAnchor") === true)
		{
			if (base.constructing)
				base.constructing = false;
			addBase = true;
		}
	}

	for (const evt of events.OwnershipChanged)
	{
		if (evt.from == PlayerID)
		{
			const ent = gameState.getEntityById(evt.entity);
			if (!ent || ent.getMetadata(PlayerID, "base") === undefined)
				continue;
			const base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
			if (ent.resourceDropsiteTypes() && ent.hasClass("Structure"))
				base.removeDropsite(gameState, ent);
			if (ent.getMetadata(PlayerID, "baseAnchor") === true)
				base.anchorLost(gameState, ent);
		}

		if (evt.to != PlayerID)
			continue;
		const ent = gameState.getEntityById(evt.entity);
		if (!ent)
			continue;
		if (ent.hasClass("Unit"))
		{
			PETRA.getBestBase(gameState, ent).assignEntity(gameState, ent);
			continue;
		}
		if (ent.hasClass("CivCentre"))   // build a new base around it
		{
			let newbase;
			if (ent.foundationProgress() !== undefined)
				newbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_UNCONSTRUCTED);
			else
			{
				newbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_CAPTURED);
				addBase = true;
			}
			newbase.assignEntity(gameState, ent);
		}
		else
		{
			let base;
			// If dropsite on new island, create a base around it
			if (!ent.decaying() && ent.resourceDropsiteTypes())
				base = this.createBase(gameState, ent, PETRA.BaseManager.STATE_ANCHORLESS);
			else
				base = PETRA.getBestBase(gameState, ent) || this.noBase;
			base.assignEntity(gameState, ent);
		}
	}

	for (const evt of events.TrainingFinished)
	{
		for (const entId of evt.entities)
		{
			const ent = gameState.getEntityById(entId);
			if (!ent || !ent.isOwn(PlayerID))
				continue;

			// Assign it immediately to something useful to do.
			if (ent.getMetadata(PlayerID, "role") === PETRA.Worker.ROLE_WORKER)
			{
				let base;
				if (ent.getMetadata(PlayerID, "base") === undefined)
				{
					base = PETRA.getBestBase(gameState, ent);
					base.assignEntity(gameState, ent);
				}
				else
					base = this.getBaseByID(ent.getMetadata(PlayerID, "base"));
				base.reassignIdleWorkers(gameState, [ent]);
				base.workerObject.update(gameState, ent);
			}
			else if (ent.resourceSupplyType() && ent.position())
			{
				const type = ent.resourceSupplyType();
				if (!type.generic)
					continue;
				const dropsites = gameState.getOwnDropsites(type.generic);
				const pos = ent.position();
				const access = PETRA.getLandAccess(gameState, ent);
				let distmin = Math.min();
				let goal;
				for (const dropsite of dropsites.values())
				{
					if (!dropsite.position() || PETRA.getLandAccess(gameState, dropsite) != access)
						continue;
					const dist = API3.SquareVectorDistance(pos, dropsite.position());
					if (dist > distmin)
						continue;
					distmin = dist;
					goal = dropsite.position();
				}
				if (goal)
					ent.moveToRange(goal[0], goal[1]);
			}
		}
	}

	if (addBase)
		gameState.ai.HQ.handleNewBase(gameState);
};

/**
 * returns an entity collection of workers through BaseManager.pickBuilders
 * TODO: when same accessIndex, sort by distance
 */
PETRA.BasesManager.prototype.bulkPickWorkers = function(gameState, baseRef, number)
{
	const accessIndex = baseRef.accessIndex;
	if (!accessIndex)
		return false;
	const baseBest = this.baseManagers.slice();
	// We can also use workers without a base.
	baseBest.push(this.noBase);
	baseBest.sort((a, b) => {
		if (a.accessIndex == accessIndex && b.accessIndex != accessIndex)
			return -1;
		else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex)
			return 1;
		return 0;
	});

	let needed = number;
	const workers = new API3.EntityCollection(gameState.sharedScript);
	for (const base of baseBest)
	{
		if (base.ID == baseRef.ID)
			continue;
		base.pickBuilders(gameState, workers, needed);
		if (workers.length >= number)
			break;
		needed = number - workers.length;
	}
	if (!workers.length)
		return false;
	return workers;
};

/**
 * @return {Object} - Resources (estimation) still gatherable in our territory.
 */
PETRA.BasesManager.prototype.getTotalResourceLevel = function(gameState, resources = Resources.GetCodes(), proximity = ["nearby", "medium"])
{
	const total = {};
	for (const res of resources)
		total[res] = 0;
	for (const base of this.baseManagers)
		for (const res in total)
			total[res] += base.getResourceLevel(gameState, res, proximity);

	return total;
};

/**
 * Returns the current gather rate
 * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that.
 */
PETRA.BasesManager.prototype.GetCurrentGatherRates = function(gameState)
{
	if (!this.turnCache.currentRates)
	{
		const currentRates = {};
		for (const res of Resources.GetCodes())
			currentRates[res] = 0.5 * this.GetTCResGatherer(res);

		this.addGatherRates(gameState, currentRates);

		for (const res of Resources.GetCodes())
			currentRates[res] = Math.max(currentRates[res], 0);

		this.turnCache.currentRates = currentRates;
	}

	return this.turnCache.currentRates;
};

/** Some functions that register that we assigned a gatherer to a resource this turn */

/** Add a gatherer to the turn cache for this supply. */
PETRA.BasesManager.prototype.AddTCGatherer = function(supplyID)
{
	if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined)
		++this.turnCache.resourceGatherer[supplyID];
	else
	{
		if (!this.turnCache.resourceGatherer)
			this.turnCache.resourceGatherer = {};
		this.turnCache.resourceGatherer[supplyID] = 1;
	}
};

/** Remove a gatherer from the turn cache for this supply. */
PETRA.BasesManager.prototype.RemoveTCGatherer = function(supplyID)
{
	if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID])
		--this.turnCache.resourceGatherer[supplyID];
	else
	{
		if (!this.turnCache.resourceGatherer)
			this.turnCache.resourceGatherer = {};
		this.turnCache.resourceGatherer[supplyID] = -1;
	}
};

PETRA.BasesManager.prototype.GetTCGatherer = function(supplyID)
{
	if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID])
		return this.turnCache.resourceGatherer[supplyID];

	return 0;
};

/** The next two are to register that we assigned a gatherer to a resource this turn. */
PETRA.BasesManager.prototype.AddTCResGatherer = function(resource)
{
	const check = "resourceGatherer-" + resource;
	if (this.turnCache[check])
		++this.turnCache[check];
	else
		this.turnCache[check] = 1;

	if (this.turnCache.currentRates)
		this.turnCache.currentRates[resource] += 0.5;
};

PETRA.BasesManager.prototype.GetTCResGatherer = function(resource)
{
	const check = "resourceGatherer-" + resource;
	if (this.turnCache[check])
		return this.turnCache[check];

	return 0;
};

/**
 * flag a resource as exhausted
 */
PETRA.BasesManager.prototype.isResourceExhausted = function(resource)
{
	const check = "exhausted-" + resource;
	if (this.turnCache[check] == undefined)
		this.turnCache[check] = this.basesManager.isResourceExhausted(resource);

	return this.turnCache[check];
};

/**
 * returns the number of bases with a cc
 * ActiveBases includes only those with a built cc
 * PotentialBases includes also those with a cc in construction
 */
PETRA.BasesManager.prototype.numActiveBases = function()
{
	if (!this.turnCache.base)
		this.updateBaseCache();
	return this.turnCache.base.active;
};

PETRA.BasesManager.prototype.hasActiveBase = function()
{
	return !!this.numActiveBases();
};

PETRA.BasesManager.prototype.numPotentialBases = function()
{
	if (!this.turnCache.base)
		this.updateBaseCache();
	return this.turnCache.base.potential;
};

PETRA.BasesManager.prototype.hasPotentialBase = function()
{
	return !!this.numPotentialBases();
};

/**
 * Updates the number of active and potential bases.
 *		.potential {number} - Bases that may or may not still be a foundation.
 *		.active {number} - Usable bases.
 */
PETRA.BasesManager.prototype.updateBaseCache = function()
{
	this.turnCache.base = { "active": 0, "potential": 0 };
	for (const base of this.baseManagers)
	{
		if (!base.anchor)
			continue;
		++this.turnCache.base.potential;
		if (base.anchor.foundationProgress() === undefined)
			++this.turnCache.base.active;
	}
};

PETRA.BasesManager.prototype.resetBaseCache = function()
{
	this.turnCache.base = undefined;
};

PETRA.BasesManager.prototype.baselessBase = function()
{
	return this.noBase;
};

/**
 * @param {number} baseID
 * @return {Object} - The base corresponding to baseID.
 */
PETRA.BasesManager.prototype.getBaseByID = function(baseID)
{
	if (this.noBase.ID === baseID)
		return this.noBase;
	return this.baseManagers.find(base => base.ID === baseID);
};

/**
 * flag a resource as exhausted
 */
PETRA.BasesManager.prototype.isResourceExhausted = function(resource)
{
	return this.baseManagers.every(base =>
		!base.dropsiteSupplies[resource].nearby.length &&
		!base.dropsiteSupplies[resource].medium.length &&
		!base.dropsiteSupplies[resource].faraway.length);
};

/**
 * Count gatherers returning resources in the number of gatherers of resourceSupplies
 * to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps).
 */
PETRA.BasesManager.prototype.assignGatherers = function()
{
	for (const base of this.baseManagers)
		for (const worker of base.workers.values())
		{
			if (worker.unitAIState().split(".").indexOf("RETURNRESOURCE") === -1)
				continue;
			const orders = worker.unitAIOrderData();
			if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply"))
				continue;
			this.AddTCGatherer(orders[1].target);
		}
};

/**
 * Assign an entity to the closest base.
 * Used by the starting strategy.
 */
PETRA.BasesManager.prototype.assignEntity = function(gameState, ent, territoryIndex)
{
	let bestbase;
	for (const base of this.baseManagers)
	{
		if ((!ent.getMetadata(PlayerID, "base") || ent.getMetadata(PlayerID, "base") != base.ID) &&
		    base.territoryIndices.indexOf(territoryIndex) == -1)
			continue;
		base.assignEntity(gameState, ent);
		bestbase = base;
		break;
	}
	if (!bestbase)	// entity outside our territory
	{
		if (ent.hasClass("Structure") && !ent.decaying() && ent.resourceDropsiteTypes())
			bestbase = this.createBase(gameState, ent, PETRA.BaseManager.STATE_ANCHORLESS);
		else
			bestbase = PETRA.getBestBase(gameState, ent) || this.noBase;
		bestbase.assignEntity(gameState, ent);
	}
	// now assign entities garrisoned inside this entity
	if (ent.isGarrisonHolder() && ent.garrisoned().length)
		for (const id of ent.garrisoned())
			bestbase.assignEntity(gameState, gameState.getEntityById(id));
	// and find something useful to do if we already have a base
	if (ent.position() && bestbase.ID !== this.noBase.ID)
	{
		bestbase.assignRolelessUnits(gameState, [ent]);
		if (ent.getMetadata(PlayerID, "role") === PETRA.Worker.ROLE_WORKER)
		{
			bestbase.reassignIdleWorkers(gameState, [ent]);
			bestbase.workerObject.update(gameState, ent);
		}
	}
};

/**
 * Adds the gather rates of individual bases to a shared object.
 * @param {Object} gameState
 * @param {Object} rates - The rates to add the gather rates to.
 */
PETRA.BasesManager.prototype.addGatherRates = function(gameState, rates)
{
	for (const base of this.baseManagers)
		base.addGatherRates(gameState, rates);
};

/**
 * @param {number} territoryIndex
 * @return {number} - The ID of the base at the given territory index.
 */
PETRA.BasesManager.prototype.baseAtIndex = function(territoryIndex)
{
	return this.basesMap.map[territoryIndex];
};

/**
 * @param {number} territoryIndex
 */
PETRA.BasesManager.prototype.removeBaseFromTerritoryIndex = function(territoryIndex)
{
	const baseID = this.basesMap.map[territoryIndex];
	if (baseID == 0)
		return;
	const base = this.getBaseByID(baseID);
	if (base)
	{
		const index = base.territoryIndices.indexOf(territoryIndex);
		if (index != -1)
			base.territoryIndices.splice(index, 1);
		else
			API3.warn(" problem in headquarters::updateTerritories for base " + baseID);
	}
	else
		API3.warn(" problem in headquarters::updateTerritories without base " + baseID);
	this.basesMap.map[territoryIndex] = 0;
};

/**
 * @return {boolean} - Whether the index was added to a base.
 */
PETRA.BasesManager.prototype.addTerritoryIndexToBase = function(gameState, territoryIndex, passabilityMap)
{
	if (this.baseAtIndex(territoryIndex) != 0)
		return false;
	let landPassable = false;
	const ind = API3.getMapIndices(territoryIndex, gameState.ai.HQ.territoryMap, passabilityMap);
	let access;
	for (const k of ind)
	{
		if (!gameState.ai.HQ.landRegions[gameState.ai.accessibility.landPassMap[k]])
			continue;
		landPassable = true;
		access = gameState.ai.accessibility.landPassMap[k];
		break;
	}
	if (!landPassable)
		return false;
	let distmin = Math.min();
	let baseID;
	const pos = [gameState.ai.HQ.territoryMap.cellSize * (territoryIndex % gameState.ai.HQ.territoryMap.width + 0.5), gameState.ai.HQ.territoryMap.cellSize * (Math.floor(territoryIndex / gameState.ai.HQ.territoryMap.width) + 0.5)];
	for (const base of this.baseManagers)
	{
		if (!base.anchor || !base.anchor.position())
			continue;
		if (base.accessIndex != access)
			continue;
		const dist = API3.SquareVectorDistance(base.anchor.position(), pos);
		if (dist >= distmin)
			continue;
		distmin = dist;
		baseID = base.ID;
	}
	if (!baseID)
		return false;
	this.getBaseByID(baseID).territoryIndices.push(territoryIndex);
	this.basesMap.map[territoryIndex] = baseID;
	return true;
};

/** Reassign territories when a base is going to be deleted */
PETRA.BasesManager.prototype.reassignTerritories = function(deletedBase, territoryMap)
{
	const cellSize = territoryMap.cellSize;
	const width = territoryMap.width;
	for (let j = 0; j < territoryMap.length; ++j)
	{
		if (this.basesMap.map[j] != deletedBase.ID)
			continue;
		if (territoryMap.getOwnerIndex(j) != PlayerID)
		{
			API3.warn("Petra reassignTerritories: should never happen");
			this.basesMap.map[j] = 0;
			continue;
		}

		let distmin = Math.min();
		let baseID;
		const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
		for (const base of this.baseManagers)
		{
			if (!base.anchor || !base.anchor.position())
				continue;
			if (base.accessIndex != deletedBase.accessIndex)
				continue;
			const dist = API3.SquareVectorDistance(base.anchor.position(), pos);
			if (dist >= distmin)
				continue;
			distmin = dist;
			baseID = base.ID;
		}
		if (baseID)
		{
			this.getBaseByID(baseID).territoryIndices.push(j);
			this.basesMap.map[j] = baseID;
		}
		else
			this.basesMap.map[j] = 0;
	}
};

/**
 * We will loop only on one active base per turn.
 */
PETRA.BasesManager.prototype.update = function(gameState, queues, events)
{
	Engine.ProfileStart("BasesManager update");

	this.turnCache = {};
	this.assignGatherers();
	let nbBases = this.baseManagers.length;
	let activeBase = false;
	this.noBase.update(gameState, queues, events);
	while (!activeBase && nbBases != 0)
	{
		this.currentBase %= this.baseManagers.length;
		activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events);
		--nbBases;
		// TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]);
	}

	Engine.ProfileStop();
};

PETRA.BasesManager.prototype.Serialize = function()
{
	const properties = {
		"currentBase": this.currentBase
	};

	const baseManagers = [];
	for (const base of this.baseManagers)
		baseManagers.push(base.Serialize());

	return {
		"properties": properties,
		"noBase": this.noBase.Serialize(),
		"baseManagers": baseManagers
	};
};

PETRA.BasesManager.prototype.Deserialize = function(gameState, data)
{
	for (const key in data.properties)
		this[key] = data.properties[key];

	this.noBase = new PETRA.BaseManager(gameState, this);
	this.noBase.Deserialize(gameState, data.noBase);
	this.noBase.init(gameState, PETRA.BaseManager.STATE_WITH_ANCHOR);
	this.noBase.Deserialize(gameState, data.noBase);

	this.baseManagers = [];
	for (const basedata of data.baseManagers)
	{
		// The first call to deserialize set the ID base needed by entitycollections.
		const newbase = new PETRA.BaseManager(gameState, this);
		newbase.Deserialize(gameState, basedata);
		newbase.init(gameState, PETRA.BaseManager.STATE_WITH_ANCHOR);
		newbase.Deserialize(gameState, basedata);
		this.baseManagers.push(newbase);
	}
};