forked from 0ad/0ad
wraitii
9330975d54
Fix auto-researched technologies not being properly researched on init. Fix the tests (amazing!). GarrisonHolder sends which entities were added or removed (mostly for AI purposes but could benefit other things). Streamline AIInterface events. This was SVN commit r14612.
437 lines
13 KiB
JavaScript
437 lines
13 KiB
JavaScript
var API3 = function(m)
|
|
{
|
|
|
|
// Shared script handling templates and basic terrain analysis
|
|
m.SharedScript = function(settings)
|
|
{
|
|
if (!settings)
|
|
return;
|
|
|
|
this._players = settings.players;
|
|
this._templates = settings.templates;
|
|
this._derivedTemplates = {};
|
|
this._techTemplates = settings.techTemplates;
|
|
|
|
this._entityMetadata = {};
|
|
for (var i in this._players)
|
|
this._entityMetadata[this._players[i]] = {};
|
|
|
|
// always create for 8 + gaia players, since _players isn't aware of the human.
|
|
this._techModifications = { 0 : {}, 1 : {}, 2 : {}, 3 : {}, 4 : {}, 5 : {}, 6 : {}, 7 : {}, 8 : {} };
|
|
|
|
// array of entity collections
|
|
this._entityCollections = [];
|
|
// each name is a reference to the actual one.
|
|
this._entityCollectionsName = {};
|
|
this._entityCollectionsByDynProp = {};
|
|
this._entityCollectionsUID = 0;
|
|
|
|
// A few notes about these maps. They're updated by checking for "create" and "destroy" events for all resources
|
|
// TODO: change the map when the resource amounts change for at least stone and metal mines.
|
|
this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal
|
|
this.CCResourceMaps = {}; // Contains maps showing the density of wood, stone and metal, optimized for CC placement.
|
|
// Resource maps data.
|
|
// By how much to divide the resource amount for plotting (ie a tree having 200 wood is "4").
|
|
this.decreaseFactor = {'wood': 50.0, 'stone': 90.0, 'metal': 90.0, 'food': 40.0};
|
|
|
|
this.turn = 0;
|
|
}
|
|
|
|
//Return a simple object (using no classes etc) that will be serialized
|
|
//into saved games
|
|
//TODO: that
|
|
m.SharedScript.prototype.Serialize = function()
|
|
{
|
|
return { "players" : this._players, "templates" : this._templates, "techTp" : this._techTemplates };
|
|
};
|
|
|
|
// Called after the constructor when loading a saved game, with 'data' being
|
|
// whatever Serialize() returned
|
|
m.SharedScript.prototype.Deserialize = function(data)
|
|
{
|
|
this._players = data.players;
|
|
this._templates = data.templates;
|
|
this._techTemplates = data.techTp;
|
|
this.isDeserialized = true;
|
|
};
|
|
|
|
// Components that will be disabled in foundation entity templates.
|
|
// (This is a bit yucky and fragile since it's the inverse of
|
|
// CCmpTemplateManager::CopyFoundationSubset and only includes components
|
|
// that our Template class currently uses.)
|
|
m.g_FoundationForbiddenComponents = {
|
|
"ProductionQueue": 1,
|
|
"ResourceSupply": 1,
|
|
"ResourceDropsite": 1,
|
|
"GarrisonHolder": 1,
|
|
};
|
|
|
|
// Components that will be disabled in resource entity templates.
|
|
// Roughly the inverse of CCmpTemplateManager::CopyResourceSubset.
|
|
m.g_ResourceForbiddenComponents = {
|
|
"Cost": 1,
|
|
"Decay": 1,
|
|
"Health": 1,
|
|
"UnitAI": 1,
|
|
"UnitMotion": 1,
|
|
"Vision": 1
|
|
};
|
|
|
|
m.SharedScript.prototype.GetTemplate = function(name)
|
|
{
|
|
if (this._templates[name])
|
|
return this._templates[name];
|
|
|
|
if (this._derivedTemplates[name])
|
|
return this._derivedTemplates[name];
|
|
|
|
// If this is a foundation template, construct it automatically
|
|
if (name.indexOf("foundation|") !== -1)
|
|
{
|
|
var base = this.GetTemplate(name.substr(11));
|
|
|
|
var foundation = {};
|
|
for (var key in base)
|
|
if (!m.g_FoundationForbiddenComponents[key])
|
|
foundation[key] = base[key];
|
|
|
|
this._derivedTemplates[name] = foundation;
|
|
return foundation;
|
|
}
|
|
else if (name.indexOf("resource|") !== -1)
|
|
{
|
|
var base = this.GetTemplate(name.substr(9));
|
|
|
|
var resource = {};
|
|
for (var key in base)
|
|
if (!m.g_ResourceForbiddenComponents[key])
|
|
resource[key] = base[key];
|
|
|
|
this._derivedTemplates[name] = resource;
|
|
return resource;
|
|
}
|
|
|
|
error("Tried to retrieve invalid template '"+name+"'");
|
|
return null;
|
|
};
|
|
|
|
// Initialize the shared component.
|
|
// We need to now the initial state of the game for this, as we will use it.
|
|
// This is called right at the end of the map generation.
|
|
m.SharedScript.prototype.init = function(state) {
|
|
this.ApplyTemplatesDelta(state);
|
|
|
|
this.passabilityClasses = state.passabilityClasses;
|
|
this.passabilityMap = state.passabilityMap;
|
|
this.players = this._players;
|
|
this.playersData = state.players;
|
|
this.territoryMap = state.territoryMap;
|
|
this.timeElapsed = state.timeElapsed;
|
|
this.barterPrices = state.barterPrices;
|
|
|
|
this._entities = {};
|
|
for (var id in state.entities)
|
|
{
|
|
// entropy generator
|
|
for (var p = 0; p < id; ++p)
|
|
Math.random();
|
|
this._entities[id] = new m.Entity(this, state.entities[id]);
|
|
}
|
|
// entity collection updated on create/destroy event.
|
|
this.entities = new m.EntityCollection(this, this._entities);
|
|
|
|
// create the terrain analyzer
|
|
this.terrainAnalyzer = new m.TerrainAnalysis();
|
|
this.terrainAnalyzer.init(this, state);
|
|
this.accessibility = new m.Accessibility();
|
|
this.accessibility.init(state, this.terrainAnalyzer);
|
|
|
|
// defined in TerrainAnalysis.js
|
|
this.createResourceMaps(this);
|
|
|
|
this.gameState = {};
|
|
for (var i in this._players)
|
|
{
|
|
this.gameState[this._players[i]] = new m.GameState();
|
|
this.gameState[this._players[i]].init(this,state,this._players[i]);
|
|
}
|
|
};
|
|
|
|
// General update of the shared script, before each AI's update
|
|
// applies entity deltas, and each gamestate.
|
|
m.SharedScript.prototype.onUpdate = function(state)
|
|
{
|
|
if (this.isDeserialized && this.turn !== 0)
|
|
{
|
|
this.isDeserialized = false;
|
|
this.init(state);
|
|
} else if (this.isDeserialized)
|
|
return;
|
|
// deals with updating based on create and destroy messages.
|
|
this.ApplyEntitiesDelta(state);
|
|
this.ApplyTemplatesDelta(state);
|
|
|
|
Engine.ProfileStart("onUpdate");
|
|
|
|
// those are dynamic and need to be reset as the "state" object moves in memory.
|
|
this.events = state.events;
|
|
this.passabilityClasses = state.passabilityClasses;
|
|
this.passabilityMap = state.passabilityMap;
|
|
this.playersData = state.players;
|
|
this.territoryMap = state.territoryMap;
|
|
this.timeElapsed = state.timeElapsed;
|
|
this.barterPrices = state.barterPrices;
|
|
|
|
for (var i in this.gameState)
|
|
this.gameState[i].update(this,state);
|
|
|
|
// TODO: merge those two with "ApplyEntitiesDelta" since after all they do the same.
|
|
this.updateResourceMaps(this, this.events);
|
|
this.terrainAnalyzer.updateMapWithEvents(this);
|
|
|
|
this.turn++;
|
|
|
|
Engine.ProfileStop();
|
|
};
|
|
|
|
m.SharedScript.prototype.ApplyEntitiesDelta = function(state)
|
|
{
|
|
Engine.ProfileStart("Shared ApplyEntitiesDelta");
|
|
|
|
var foundationFinished = {};
|
|
|
|
// by order of updating:
|
|
// we "Destroy" last because we want to be able to switch Metadata first.
|
|
var CreateEvents = state.events["Create"];
|
|
var DestroyEvents = state.events["Destroy"];
|
|
var RenamingEvents = state.events["EntityRenamed"];
|
|
var TrainingEvents = state.events["TrainingFinished"];
|
|
var ConstructionEvents = state.events["ConstructionFinished"];
|
|
var MetadataEvents = state.events["AIMetadata"];
|
|
var ownershipChangeEvents = state.events["OwnershipChanged"];
|
|
|
|
for (var i = 0; i < CreateEvents.length; ++i)
|
|
{
|
|
var evt = CreateEvents[i];
|
|
if (!state.entities[evt.entity])
|
|
continue; // Sometimes there are things like foundations which get destroyed too fast
|
|
|
|
this._entities[evt.entity] = new m.Entity(this, state.entities[evt.entity]);
|
|
this.entities.addEnt(this._entities[evt.entity]);
|
|
|
|
// Update all the entity collections since the create operation affects static properties as well as dynamic
|
|
for (var entCollection in this._entityCollections)
|
|
this._entityCollections[entCollection].updateEnt(this._entities[evt.entity]);
|
|
}
|
|
for (var i in RenamingEvents)
|
|
{
|
|
var evt = RenamingEvents[i];
|
|
// Switch the metadata
|
|
for (var p in this._players)
|
|
{
|
|
this._entityMetadata[this._players[p]][evt.newentity] = this._entityMetadata[this._players[p]][evt.entity];
|
|
this._entityMetadata[this._players[p]][evt.entity] = {};
|
|
}
|
|
}
|
|
for (var i in TrainingEvents)
|
|
{
|
|
var evt = TrainingEvents[i];
|
|
// Apply metadata stored in training queues
|
|
for each (var ent in evt.entities)
|
|
{
|
|
for (var key in evt.metadata)
|
|
{
|
|
this.setMetadata(evt.owner, this._entities[ent], key, evt.metadata[key])
|
|
}
|
|
}
|
|
}
|
|
for (var i in ConstructionEvents)
|
|
{
|
|
var evt = ConstructionEvents[i];
|
|
// we'll move metadata.
|
|
if (!this._entities[evt.entity])
|
|
continue;
|
|
var ent = this._entities[evt.entity];
|
|
var newEnt = this._entities[evt.newentity];
|
|
if (this._entityMetadata[ent.owner()] && this._entityMetadata[ent.owner()][evt.entity] !== undefined)
|
|
for (var key in this._entityMetadata[ent.owner()][evt.entity])
|
|
{
|
|
this.setMetadata(ent.owner(), newEnt, key, this._entityMetadata[ent.owner()][evt.entity][key])
|
|
}
|
|
foundationFinished[evt.entity] = true;
|
|
}
|
|
for (var i in MetadataEvents)
|
|
{
|
|
var evt = MetadataEvents[i];
|
|
if (!this._entities[evt.id])
|
|
continue; // might happen in some rare cases of foundations getting destroyed, perhaps.
|
|
// Apply metadata (here for buildings for example)
|
|
for (var key in evt.metadata)
|
|
{
|
|
this.setMetadata(evt.owner, this._entities[evt.id], key, evt.metadata[key])
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < DestroyEvents.length; ++i)
|
|
{
|
|
var evt = DestroyEvents[i];
|
|
// A small warning: javascript "delete" does not actually delete, it only removes the reference in this object.
|
|
// the "deleted" object remains in memory, and any older reference to it will still reference it as if it were not "deleted".
|
|
// Worse, they might prevent it from being garbage collected, thus making it stay alive and consuming ram needlessly.
|
|
// So take care, and if you encounter a weird bug with deletion not appearing to work correctly, this is probably why.
|
|
if (!this._entities[evt.entity])
|
|
continue;
|
|
|
|
if (foundationFinished[evt.entity])
|
|
evt["SuccessfulFoundation"] = true;
|
|
|
|
// The entity was destroyed but its data may still be useful, so
|
|
// remember the entity and this AI's metadata concerning it
|
|
evt.metadata = {};
|
|
evt.entityObj = this._entities[evt.entity];
|
|
for (var j in this._players)
|
|
evt.metadata[this._players[j]] = this._entityMetadata[this._players[j]][evt.entity];
|
|
|
|
for each (var entCol in this._entityCollections)
|
|
{
|
|
entCol.removeEnt(this._entities[evt.entity]);
|
|
}
|
|
this.entities.removeEnt(this._entities[evt.entity]);
|
|
|
|
delete this._entities[evt.entity];
|
|
for (var j in this._players)
|
|
delete this._entityMetadata[this._players[j]][evt.entity];
|
|
}
|
|
|
|
for (var id in state.entities)
|
|
{
|
|
var changes = state.entities[id];
|
|
|
|
for (var prop in changes)
|
|
{
|
|
this._entities[id]._entity[prop] = changes[prop];
|
|
|
|
this.updateEntityCollections(prop, this._entities[id]);
|
|
}
|
|
}
|
|
|
|
// apply per-entity aura-related changes.
|
|
// this supersedes tech-related changes.
|
|
for (var id in state.changedEntityTemplateInfo)
|
|
{
|
|
var changes = state.changedEntityTemplateInfo[id];
|
|
for each (var change in changes)
|
|
this._entities[id]._auraTemplateModif[change.variable] = change.value;
|
|
}
|
|
Engine.ProfileStop();
|
|
};
|
|
|
|
m.SharedScript.prototype.ApplyTemplatesDelta = function(state)
|
|
{
|
|
Engine.ProfileStart("Shared ApplyTemplatesDelta");
|
|
|
|
for (var player in state.changedTemplateInfo)
|
|
{
|
|
var playerDiff = state.changedTemplateInfo[player];
|
|
for (var template in playerDiff)
|
|
{
|
|
var changes = playerDiff[template];
|
|
if (!this._techModifications[player][template])
|
|
this._techModifications[player][template] = {};
|
|
for each (var change in changes)
|
|
this._techModifications[player][template][change.variable] = change.value;
|
|
}
|
|
}
|
|
Engine.ProfileStop();
|
|
};
|
|
|
|
m.SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection, noPush)
|
|
{
|
|
if (!noPush) {
|
|
this._entityCollections.push(entCollection);
|
|
}
|
|
entCollection.setUID(this._entityCollectionsUID);
|
|
for each (var prop in entCollection.dynamicProperties())
|
|
{
|
|
this._entityCollectionsByDynProp[prop] = this._entityCollectionsByDynProp[prop] || [];
|
|
this._entityCollectionsByDynProp[prop].push(entCollection);
|
|
}
|
|
this._entityCollectionsUID++;
|
|
};
|
|
|
|
m.SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection)
|
|
{
|
|
for (var i in this._entityCollections)
|
|
{
|
|
if (this._entityCollections[i].getUID() === entCollection.getUID())
|
|
{
|
|
this._entityCollections.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
for each (var prop in entCollection.dynamicProperties())
|
|
{
|
|
for (var i in this._entityCollectionsByDynProp[prop])
|
|
{
|
|
if (this._entityCollectionsByDynProp[prop][i].getUID() === entCollection.getUID())
|
|
{
|
|
this._entityCollectionsByDynProp[prop].splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
m.SharedScript.prototype.updateEntityCollections = function(property, ent)
|
|
{
|
|
if (this._entityCollectionsByDynProp[property] !== undefined)
|
|
{
|
|
for (var entCollectionid in this._entityCollectionsByDynProp[property])
|
|
{
|
|
this._entityCollectionsByDynProp[property][entCollectionid].updateEnt(ent);
|
|
}
|
|
}
|
|
}
|
|
|
|
m.SharedScript.prototype.setMetadata = function(player, ent, key, value)
|
|
{
|
|
var metadata = this._entityMetadata[player][ent.id()];
|
|
if (!metadata)
|
|
metadata = this._entityMetadata[player][ent.id()] = {};
|
|
metadata[key] = value;
|
|
|
|
this.updateEntityCollections('metadata', ent);
|
|
this.updateEntityCollections('metadata.' + key, ent);
|
|
};
|
|
m.SharedScript.prototype.getMetadata = function(player, ent, key)
|
|
{
|
|
var metadata = this._entityMetadata[player][ent.id()];
|
|
|
|
if (!metadata || !(key in metadata))
|
|
return undefined;
|
|
return metadata[key];
|
|
};
|
|
m.SharedScript.prototype.deleteMetadata = function(player, ent, key)
|
|
{
|
|
var metadata = this._entityMetadata[player][ent.id()];
|
|
|
|
if (!metadata || !(key in metadata))
|
|
return true;
|
|
metadata[key] = undefined;
|
|
delete metadata[key];
|
|
return true;
|
|
};
|
|
|
|
m.copyPrototype = function(descendant, parent) {
|
|
var sConstructor = parent.toString();
|
|
var aMatch = sConstructor.match( /\s*function (.*)\(/ );
|
|
if ( aMatch != null ) { descendant.prototype[aMatch[1]] = parent; }
|
|
for (var m in parent.prototype) {
|
|
descendant.prototype[m] = parent.prototype[m];
|
|
}
|
|
};
|
|
|
|
return m;
|
|
|
|
}(API3);
|
|
|