0ad/binaries/data/mods/public/simulation/ai/common-api/shared.js
wraitii 9330975d54 Fix Athens not researching civ phase. Make the AI adapt automatically to changing the max gatherers on fields.
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.
2014-01-18 20:26:52 +00:00

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);