Table of Contents
Functions
issueCommand
- Overview:
- Issue a command (network message) to an entity or collection.
- Syntax:
- issueCommand(entity_or_collection, NMT_Goto);
- Parameters:
-
- either an entity- or entity collection object, message ID int, any further params needed by CNetMessage*
CommandFromJSArgs
- either an entity- or entity collection object, message ID int, any further params needed by CNetMessage*
- Returns:
- command in serialized form string
- Notes:
- It is now possible for JS to give network synchronized orders to entities by using the issueCommand method like this:
- selection[0].issueCommand(NMT_Goto, X, Y); or
- selection[0].issueCommand(NMT_Gather, targetResourceEntity);
- Sending to multiple entities is not yet possible, and I haven't even started on converting the input code to send JS events etc. I need to make some specs on what JS should be able to do and how it should do it - if nothing else to get a clear view of the new stuff. Well... tomorrow's problems -> tomorrow ;-)
// Call a function when the entity performs a certain event:
<Event On="Attack" Function="entity_event_attack" />
localPlayer.resource
SelectUnit (UnitIndex) // Set the current selection to a certain unit index in the current selection.
SelectGroup (2) // Select a given ctrl group (eg set the selection to the group mapped to Ctrl+2).
selection.length // Number of units in current selection; if (selection.length) returns true if units are currently selected.
selection[0] // Reference the entity that's first in the selection.
'''(all these unique conditions are kind of beyond me at the moment ... May have to get Mark/Simon to document all these properties, so we can isolate the generic from the specific.)
// Container type for damage properties (crush, hack, pierce).
dmg = new DamageType();
dmg.crush = parseInt(this.actions.attack.damage * this.actions.attack.crush);
dmg.hack = parseInt(this.actions.attack.damage * this.actions.attack.hack);
dmg.pierce = parseInt(this.actions.attack.damage * this.actions.attack.pierce);
// Inflict wound damage on a target.
evt.target.damage( dmg, this );
// Inflict wound damage on a target using a projectile.
evt.impacted.damage( this.damage, evt.originator );
if( evt.impacted.player == evt.originator.player )
console.write( "Friendly fire!" );
// Kill an entity. evt.target.kill();
// Get a function that's part of the GUI scope. getGUIGlobal().GiveResources("Ore", parseInt(this.traits.loot.ore));
// If unit is idle, tell it to attack the person attacking him.
if( this.isIdle() )
this.order( ORDER_ATTACK, evt.inflictor );
evt.defaultAction = NMT_Goto;
evt.defaultCursor = "arrow-default";
evt.defaultAction = NMT_AttackMelee;
evt.defaultCursor = "action-attack";
// The parameters for Projectile are:
// 1 - The actor to use as the projectile. There are two ways of specifying this:
// the first is by giving an entity. The projectile's actor is found by looking
// in the actor of that entity. This way is usual, and preferred - visual
// information, like the projectile model, should be stored in the actor files.
// The second way is to give a actor/file name string (e.g. "props/weapon/weap_
// arrow_front.xml"). This is only meant to be used for 'Acts of Gaia' where
// there is no originating entity. Right now, this entity is the one doing the
// firing, so pass this.
// 2 - Where the projectile is coming from. This can be an entity or a Vector3D.
// For now, that's also us.
// 3 - Where the projectile is going to. Again, either a vector (attack ground?)
// or an entity. Let's shoot at our target, lest we get people terribly confused.
// 4 - How fast the projectile should go. To keep things clear, we'll set it to
// just a bit faster than the average cavalry.
// 5 - Who fired it? Erm... yep, us again.
// 6 - The function you'd like to call when it hits an entity.
// There's also a seventh parameter, for a function to call when it misses (more
// accurately, when it hits the floor). At the moment, however, the default
// action (do nothing) is what we want.
// Parameters 5, 6, and 7 are all optional.
projectile = new Projectile( this, this, evt.target, 12.0, this, projectile_event_impact )
// We'll attach the damage information to the projectile, just to show you can
// do that like you can with most other objects. Could also do this by making
// the function we pass a closure.
projectile.damage = dmg;
// Finally, tell the engine not to send this event to anywhere else -
// in particular, this shouldn't get to the melee event handler, above.
evt.stopPropagation();
// Start the progress timer.
// - First parameter is target value (in this case, base build time in seconds)
// - Second parameter is increment per millisecond (use build rate modifier and correct for milliseconds)
// - Third parameter is the function to call when the timer finishes.
// - Fourth parameter is the scope under which to run that function (what the 'this' parameter should be)
this.actions.create.progress = new ProgressTimer( template.traits.creation.time, this.actions.create.construct / 1000, entity_create_complete, this )
// Code to find a free space around an object is tedious and slow, so
// I wrote it in C. Takes the template object so it can determine how
// much space it needs to leave.
position = this.getSpawnPoint( template );
if (!selection.length) // If no entity selected,
DudeSpawnPoint = new Vector3D(x, y, z);
new Entity(getEntityTemplate(MakeUnitName), DudeSpawnPoint, 1.0);
// writeConsole(MakeUnitName + " created at " + DudeSpawnPoint);
// Returns how many units selected.
if( selection.length > 0 )
return( selection[0] );
return( null );
// ====================================================================
function selectEntity(handler) {
endSelection();
startSelection(function (event) {
// Selection is performed when single-clicking the right mouse
// button.
if (event.button == SDL_BUTTON_RIGHT && event.clicks == 1)
{
handler(event.entity);
}
// End selection on first mouse-click
endSelection();
});
}
function selectLocation(handler) {
endSelection();
startSelection(function (event) {
// Selection is performed when single-clicking the right mouse
// button.
if (event.button == SDL_BUTTON_RIGHT && event.clicks == 1)
{
handler(event.x, event.y);
}
// End selection on first mouse-click
endSelection();
});
}
function startSelection(handler) {
gameView.startCustomSelection();
getGlobal().selectionWorldClickHandler=handler;
console.write("isSelecting(): "+isSelecting());
}
function endSelection() {
if (!isSelecting())
return;
gameView.endCustomSelection();
getGlobal().selectionWorldClickHandler = null;
}
function isSelecting() {
return getGlobal().selectionWorldClickHandler != null;
}
// The world-click handler - called whenever the user clicks the terrain function worldClickHandler(event) {
args=new Array(null, null);
console.write("worldClickHandler: button "+event.button+", clicks "+event.clicks);
if (isSelecting())
{
getGlobal().selectionWorldClickHandler(event);
return;
}
// Right button single- or double-clicks
if (event.button == SDL_BUTTON_RIGHT && event.clicks <= 2)
{
if (event.clicks == 1)
cmd = event.command;
else if (event.clicks == 2)
{
console.write("Issuing secondary command");
cmd = event.secondaryCommand;
}
}
else
return;
switch (cmd)
{
// location target commands
case NMT_Goto:
case NMT_Patrol:
if (event.queued)
{
cmd = NMT_AddWaypoint;
}
case NMT_AddWaypoint:
args[0]=event.x;
args[1]=event.y;
break;
// entity target commands
case NMT_AttackMelee:
case NMT_Gather:
args[0]=event.entity;
args[1]=null;
break;
default:
console.write("worldClickHandler: Unknown command: "+cmd);
return;
}
issueCommand(selection, cmd, args[0], args[1]);
}
addGlobalHandler("worldClick", worldClickHandler);
switch (SN_STATUS_PANE_COMMAND[list][tab].name)
{
case action_patrol:
// setCursor(...)
selectLocation(
function (x, y) {
issueCommand(selection, NMT_Patrol, x, y);
});
break;
case action_attack:
// setCursor(...)
selectEntity(
function (target) {
issueCommand(selection, NMT_AttackMelee, target);
});
break;
}
// Attempt to add the entry to the queue.
attempt_add_to_build_queue( selection[0], selection[0].traits.id.civ_code + "_" + SN_STATUS_PANE_COMMAND[list][tab].name, list, tab);
// ====================================================================
// Update-on-alteration trickery...
// We don't really want to update every single time we get a // selection-changed or property-changed event; that could happen // a lot. Instead, use this bunch of globals to cache any changes // that happened between GUI updates.
// This boolean determines whether the selection has been changed. var selectionChanged = false;
// This boolean determines what the template of the selected object // was when last we looked var selectionTemplate = null;
// This array holds the name of all properties that need to be updated var selectionPropertiesChanged = new Array();
// This array holds a list of all the objects we hold property-change // watches on var propertyWatches = new Array();
// This function resets all the update variables, above function resetUpdateVars() {
if( selectionChanged )
{
for( watchedObject in propertyWatches )
propertyWatches[watchedObject].unwatchAll( selectionWatchHandler ); // Remove the handler
propertyWatches = new Array();
if( selection[0] )
{
// Watch the object itself
selection[0].watchAll( selectionWatchHandler );
propertyWatches.push( selection[0] );
// And every parent back up the tree (changes there will affect
// displayed properties via inheritance)
var parent = selection[0].template
while( parent )
{
parent.watchAll( selectionWatchHandler );
propertyWatches.push( selection[0] );
parent = parent.parent;
}
}
}
selectionChanged = false;
if( selection[0] )
{
selectionTemplate = selection[0].template;
}
else
selectionTemplate = null;
selectionPropertiesChanged = new Array();
}
// This function returns whether we should update a particular statistic // in the GUI (e.g. "actions.attack") - this should happen if: the selection // changed, the selection had its template altered (changing lots of stuff) // or an assignment has been made to that stat or any property within that // stat. function shouldUpdateStat( statname ) {
if( selectionChanged || ( selectionTemplate != selection[0].template ) )
return( true );
for( var property in selectionPropertiesChanged )
{
// If property starts with statname
if( selectionPropertiesChanged[property].substring( 0, statname.length ) == statname )
return( true );
}
return( false );
}
// This function is a handler for the 'selectionChanged' event, // it updates the selectionChanged flag function selectionChangedHandler() {
selectionChanged = true;
}
// Register it. addGlobalHandler( "selectionChanged", selectionChangedHandler );
// This function is a handler for a watch event; it updates the // selectionPropertiesChanged array function selectionWatchHandler( propname, oldvalue, newvalue ) {
selectionPropertiesChanged.push( propname );
// This bit's important (watches allow the handler to change the value
// before it gets written; we don't want to affect things, so make sure
// the value we send back is the one that was going to be written)
return( newvalue );
}
function entity_event_attack( evt ) {
curr_hit = getGUIGlobal().newRandomSound("voice", "hit", this.traits.audio.path);
curr_hit.play();
// Attack logic.
dmg = new DamageType();
dmg.crush = parseInt(this.actions.attack.damage * this.actions.attack.crush);
dmg.hack = parseInt(this.actions.attack.damage * this.actions.attack.hack);
dmg.pierce = parseInt(this.actions.attack.damage * this.actions.attack.pierce);
evt.target.damage( dmg, this );
}
// ====================================================================
function entity_event_attack_ranged( evt ) {
// Create a projectile from us, to the target, that will do some damage when it hits them.
dmg = new DamageType();
dmg.crush = parseInt(this.actions.attack.damage * this.actions.attack.crush);
dmg.hack = parseInt(this.actions.attack.damage * this.actions.attack.hack);
dmg.pierce = parseInt(this.actions.attack.damage * this.actions.attack.pierce);
// The parameters for Projectile are:
// 1 - The actor to use as the projectile. There are two ways of specifying this:
// the first is by giving an entity. The projectile's actor is found by looking
// in the actor of that entity. This way is usual, and preferred - visual
// information, like the projectile model, should be stored in the actor files.
// The second way is to give a actor/file name string (e.g. "props/weapon/weap_
// arrow_front.xml"). This is only meant to be used for 'Acts of Gaia' where
// there is no originating entity. Right now, this entity is the one doing the
// firing, so pass this.
// 2 - Where the projectile is coming from. This can be an entity or a Vector3D.
// For now, that's also us.
// 3 - Where the projectile is going to. Again, either a vector (attack ground?)
// or an entity. Let's shoot at our target, lest we get people terribly confused.
// 4 - How fast the projectile should go. To keep things clear, we'll set it to
// just a bit faster than the average cavalry.
// 5 - Who fired it? Erm... yep, us again.
// 6 - The function you'd like to call when it hits an entity.
// There's also a seventh parameter, for a function to call when it misses (more
// accurately, when it hits the floor). At the moment, however, the default
// action (do nothing) is what we want.
// Parameters 5, 6, and 7 are all optional.
projectile = new Projectile( this, this, evt.target, 12.0, this, projectile_event_impact )
// We'll attach the damage information to the projectile, just to show you can
// do that like you can with most other objects. Could also do this by making
// the function we pass a closure.
projectile.damage = dmg;
// Finally, tell the engine not to send this event to anywhere else -
// in particular, this shouldn't get to the melee event handler, above.
evt.stopPropagation();
console.write( "Fire!" );
}
// ====================================================================
function projectile_event_impact( evt ) {
console.write( "Hit!" );
evt.impacted.damage( this.damage, evt.originator );
// Just so you know - there's no guarantee that evt.impacted is the thing you were
// aiming at. This function gets called when the projectile hits *anything*.
// For example:
if( evt.impacted.player == evt.originator.player )
console.write( "Friendly fire!" );
// The three properties of the ProjectileImpact event are:
// - impacted, the thing it hit
// - originator, the thing that fired it (the fifth parameter of Projectile's
// constructor) - may be null
// - position, the position the arrow was in the world when it hit.
// The properties of the ProjectileMiss event (the one that gets sent to the
// handler that was the seventh parameter of the constructor) are similar,
// but it doesn't have 'impacted' - for obvious reasons.
}
// ====================================================================
function entity_event_gather( evt ) {
if (this.actions.gather[evt.target.traits.supply.type][evt.target.traits.supply.subtype])
gather_amt = parseInt( this.actions.gather[evt.target.traits.supply.type][evt.target.traits.supply.subtype].speed );
else
gather_amt = parseInt( this.actions.gather[evt.target.traits.supply.type].speed );
if( evt.target.traits.supply.max > 0 )
{
if( evt.target.traits.supply.curr <= gather_amt )
{
gather_amt = evt.target.traits.supply.curr;
evt.target.kill();
}
evt.target.traits.supply.curr -= gather_amt;
this.player.resource[evt.target.traits.supply.type.toString().toUpperCase()] += gather_amt;
}
}
// ====================================================================
function entity_event_takesdamage( evt ) {
// Apply armour and work out how much damage we actually take
crushDamage = parseInt(evt.damage.crush - this.traits.armour.value * this.traits.armour.crush);
if ( crushDamage < 0 ) crushDamage = 0;
pierceDamage = parseInt(evt.damage.pierce - this.traits.armour.value * this.traits.armour.pierce);
if ( pierceDamage < 0 ) pierceDamage = 0;
hackDamage = parseInt(evt.damage.hack - this.traits.armour.value * this.traits.armour.hack);
if ( hackDamage < 0 ) hackDamage = 0;
totalDamage = parseInt(evt.damage.typeless + crushDamage + pierceDamage + hackDamage);
// Minimum of 1 damage
if( totalDamage < 1 ) totalDamage = 1;
this.traits.health.curr -= totalDamage;
if( this.traits.health.curr <= 0 )
{
// If the inflictor gains promotions, and he's capable of earning more ranks,
if (evt.inflictor.traits.up && evt.inflictor.traits.up.curr && evt.inflictor.traits.up.req && evt.inflictor.traits.up.newentity && evt.inflictor.traits.up.newentity != "")
{
// Give him the fallen's upgrade points (if he has any).
if (this.traits.loot.up)
evt.inflictor.traits.up.curr = parseInt(evt.inflictor.traits.up.curr) + parseInt(this.traits.loot.up);
// Notify player.
if (this.traits.id.specific)
console.write(this.traits.id.specific + " has earned " + this.traits.loot.up + " upgrade points!");
else
console.write("One of your units has earned " + this.traits.loot.up + " upgrade points!");
// If he now has maximum upgrade points for his rank,
if (evt.inflictor.traits.up.curr >= evt.inflictor.traits.up.req)
{
// Notify the player.
if (this.traits.id.specific)
console.write(this.traits.id.specific + " has gained a promotion!");
else
console.write("One of your units has gained a promotion!");
// Reset his upgrade points.
evt.inflictor.traits.up.curr = 0;
// Upgrade his portrait to the next level.
evt.inflictor.traits.id.icon_cell++;
// Transmogrify him into his next rank.
evt.inflictor.template = getEntityTemplate(evt.inflictor.traits.up.newentity);
}
}
// If the fallen is worth any loot,
if (this.traits.loot && (this.traits.loot.food || this.traits.loot.wood || this.traits.loot.stone || this.traits.loot.ore))
{
// Give the inflictor his resources.
if (this.traits.loot.food)
getGUIGlobal().GiveResources("Food", parseInt(this.traits.loot.food));
if (this.traits.loot.wood)
getGUIGlobal().GiveResources("Wood", parseInt(this.traits.loot.wood));
if (this.traits.loot.stone)
getGUIGlobal().GiveResources("Stone", parseInt(this.traits.loot.stone));
if (this.traits.loot.ore)
getGUIGlobal().GiveResources("Ore", parseInt(this.traits.loot.ore));
}
// Notify player.
if( evt.inflictor )
console.write( this.traits.id.generic + " got the point of " + evt.inflictor.traits.id.generic + "'s weapon." );
else
console.write( this.traits.id.generic + " died in mysterious circumstances." );
// Make him cry out in pain.
curr_pain = getGUIGlobal().newRandomSound("voice", "pain", this.traits.audio.path);
curr_pain.play();
// We've taken what we need. Kill the swine.
this.kill();
}
else if( evt.inflictor && this.actions.attack )
{
// If we're not already doing something else, take a measured response - hit 'em back.
// You know, I think this is quite possibly the first AI code the AI divlead has written
// for 0 A.D....
if( this.isIdle() )
this.order( ORDER_ATTACK, evt.inflictor );
}
}
function entity_event_targetchanged( evt ) {
// This event lets us know when the user moves his/her cursor to a different unit (provided this
// unit is selected) - use it to tell the engine what context cursor should be displayed, given
// the target.
// Attack iff there's a target, it's our enemy, and we're armed. Otherwise, if we can gather, and
// the target supplies, gather. If all else fails, move.
// ToString is needed because every property is actually an object (though that's usually
// hidden from you) and comparing an object to any other object in JavaScript (1.5, at least)
// yields false. ToString converts them to their actual values (i.e. the four character
// string) first.
evt.defaultAction = NMT_Goto;
evt.defaultCursor = "arrow-default";
if( evt.target )
{
if( this.actions.attack &&
( evt.target.player != gaiaPlayer ) &&
( evt.target.player != this.player ) )
{
evt.defaultAction = NMT_AttackMelee;
evt.defaultCursor = "action-attack";
}
if( this.actions.gather && evt.target.traits.supply &&
this.actions.gather[evt.target.traits.supply.type] &&
( ( evt.target.traits.supply.curr > 0 ) || ( evt.target.traits.supply.max == 0 ) ) )
{
evt.defaultAction = NMT_Gather;
// Set cursor (eg "action-gather-fruit").
evt.defaultCursor = "action-gather-" + evt.target.traits.supply.subtype;
}
}
}
// ====================================================================
function entity_event_prepareorder( evt ) {
// This event gives us a chance to veto any order we're given before we execute it.
// Not sure whether this really belongs here like this: the alternative is to override it in
// subtypes - then you wouldn't need to check tags, you could hardcode results.
switch( evt.orderType )
{
case ORDER_GOTO:
if( !this.actions.move )
evt.preventDefault();
break;
case ORDER_PATROL:
if( !this.actions.patrol )
evt.preventDefault();
break;
case ORDER_ATTACK:
if( !this.actions.attack ||
!evt.target )
evt.preventDefault();
break;
case ORDER_GATHER:
if( !this.actions.gather ||
!( this.actions.gather[evt.target.traits.supply.type] ) ||
( ( evt.target.traits.supply.curr == 0 ) && ( evt.target.traits.supply.max > 0 ) ) )
evt.preventDefault();
break;
default:
evt.preventDefault();
}
}
// ====================================================================
function entity_add_create_queue( template, list, tab ) {
// Make sure we have a queue to put things in...
if( !this.actions.create.queue )
this.actions.create.queue = new Array();
// Construct template object.
comboTemplate = template;
comboTemplate.list = list;
comboTemplate.tab = tab;
// Append to the end of this queue
this.actions.create.queue.push( template );
// If we're not already building something...
if( !this.actions.create.progress )
{
console.write( "Starting work on (unqueued) ", template.tag );
// Start the progress timer.
// - First parameter is target value (in this case, base build time in seconds)
// - Second parameter is increment per millisecond (use build rate modifier and correct for milliseconds)
// - Third parameter is the function to call when the timer finishes.
// - Fourth parameter is the scope under which to run that function (what the 'this' parameter should be)
this.actions.create.progress = new ProgressTimer( template.traits.creation.time, this.actions.create.construct / 1000, entity_create_complete, this )
}
}
// ====================================================================
// This is the syntax to add a function (or a property) to absolutely every entity.
Entity.prototype.add_create_queue = entity_add_create_queue;
// ====================================================================
function entity_create_complete() {
// Get the unit that was at the head of our queue, and remove it.
// (Oh, for information about all these nifty properties and functions
// of the Array object that I use, see the ECMA-262 documentation
// at http://www.mozilla.org/js/language/E262-3.pdf. Bit technical but
// the sections on 'Native ECMAScript Objects' are quite useful)
var template = this.actions.create.queue.shift();
// Code to find a free space around an object is tedious and slow, so
// I wrote it in C. Takes the template object so it can determine how
// much space it needs to leave.
position = this.getSpawnPoint( template );
// The above function returns null if it couldn't find a large enough space.
if( !position )
{
console.write( "Couldn't train unit - not enough space" );
// Oh well. The player's just lost all the resources and time they put into
// construction - serves them right for not paying attention to the land
// around their barracks, doesn't it?
}
else
{
created = new Entity( template, position );
// Above shouldn't ever fail, but just in case...
if( created )
{
console.write( "Created: ", template.tag );
// Entities start under Gaia control - make the controller
// the same as our controller
created.player = this.player;
}
}
// If there's something else in the build queue...
if( this.actions.create.queue.length > 0 )
{
// Start on the next item.
template = this.actions.create.queue[0];
console.write( "Starting work on (queued) ", template.tag );
this.actions.create.progress = new ProgressTimer( template.traits.creation.time, this.actions.create.construct / 1000, entity_create_complete, this )
}
else
{
// Otherwise, delete the timer.
this.actions.create.progress = null;
}
}
// ====================================================================
function attempt_add_to_build_queue( entity, create_tag, list, tab ) {
result = entity_CheckQueueReq (entity);
if (result == "true") // If the entry meets requirements to be added to the queue (eg sufficient resources)
{
// Cycle through all costs of this entry.
pool = entity.traits.creation.resource;
for( resource in pool )
{
switch( resource.toString().toUpperCase() )
{
case "POPULATION" || "HOUSING":
break;
default:
// Deduct the given quantity of resources.
localPlayer.resource[resource.toString().toUpperCase()] -= pool[resource].cost;
console.write("Spent " + pool[resource].cost + " " + resource + ".");
break;
}
}
// Add entity to queue.
console.write( "Adding ", create_tag, " to build queue..." );
entity.add_create_queue( getEntityTemplate( create_tag ), list, tab );
}
else // If not, output the error message.
console.write(result);
}
// ====================================================================
function entity_CheckQueueReq (entry) {
// Determines if the given entity meets requirements for production by the player, and returns an appropriate
// error string.
// A return value of 0 equals success -- entry meets requirements for production.
// Cycle through all resources that this item costs, and check the player can afford the cost.
resources = entry.traits.creation.resource;
for( resource in resources )
{
resourceU = resource.toString().toUpperCase();
switch( resourceU )
{
case "POPULATION":
// If the item costs more of this resource type than we have,
if (resources[resource].cost > (localPlayer.resource["HOUSING"]-localPlayer.resource[resourceU]))
{
// Return an error.
return ("Insufficient Housing; " + (resources[resource].cost-localPlayer.resource["HOUSING"]-localPlayer.resource.valueOf()[resourceU].toString()) + " required.");
}
break;
case "HOUSING": // Ignore housing. It's handled in combination with population.
break
default:
// If the item costs more of this resource type than we have,
if (resources[resource].cost > localPlayer.resource[resourceU])
{
// Return an error.
return ("Insufficient " + resource + "; " + (localPlayer.resource[resourceU]-resources[resource].cost)*-1 + " required.");
}
else
console.write("Player has at least " + resources[resource].cost + " " + resource + ".");
break;
}
}
// Check if another entity must first exist.
// Check if another tech must first be researched.
// Check if the limit for this type of entity has been reached.
// If we passed all checks, return success. Entity can be queued.
return "true";
}
// ====================================================================