= 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 * '''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: {{{ }}} 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"; }}} } // ====================================================================