Page 1 of 2

[WIP] Reduce Weapons Damage v0.10

Posted: Thu Oct 16, 2014 2:03 pm
by Lone_Wolf
https://bb.oolite.space/viewtopic.php?f=4&t=16925

In that thread i started work on an oxp that would reduce laser damage to player ship to compensate for the much stronger npc ships in 1.80.
I've thought about that idea, checked test results for combat related changes for 1.18 and come to the conclusion
reducing laser damage for both player and npc has a good chance of increasing player survival chance without making game easier.

This oxp will not function through an equipment item, but always have effect if installed.
For the time being both npc & player get the same damage reduction.

Re: [WIP] Reduce Weapons Damage

Posted: Thu Oct 16, 2014 2:49 pm
by Norby
My suggestions:

Code: Select all

if ( !ship.script ) ship.setScript("oolite-default-ship-script.js");
ship.script._RWD = this; // = worldScripts["Reduce Weapon Damage"]
The first line help on ships without script, the second allow to use a much faster pointer in ship script:

Code: Select all

player.ship.forwardShield += this._RWD._rwd_player_reduction; 
//this._RWD is set to worldScripts["Reduce Weapon Damage"] in shipSpawned

Re: [WIP] Reduce Weapons Damage

Posted: Thu Oct 16, 2014 5:19 pm
by Lone_Wolf
Norby wrote:
My suggestions:

Code: Select all

if ( !ship.script ) ship.setScript("oolite-default-ship-script.js");
ship.script._RWD = this; // = worldScripts["Reduce Weapon Damage"]
The first line help on ships without script, the second allow to use a much faster pointer in ship script:

Code: Select all

player.ship.forwardShield += this._RWD._rwd_player_reduction; 
//this._RWD is set to worldScripts["Reduce Weapon Damage"] in shipSpawned
Interesting change, norby.
I'll do my best to understand how it works.

I also found a flaw in the method i use :
It does seem shipSpawned is only called for non-player ships, so i'll have to rethink how to set things up.

Re: [WIP] Reduce Weapons Damage

Posted: Thu Oct 16, 2014 6:40 pm
by Tricky
Lone_Wolf wrote:
I also found a flaw in the method i use :
It does seem shipSpawned is only called for non-player ships, so i'll have to rethink how to set things up.
You could always try...

Code: Select all

this.shipSpawned(player.ship);
in this.startUpComplete in the world script.

You also have a logging bug here...

Code: Select all

log(this.name,this.ship,"no ship script attached, oxp won't work);
too many arguments and a missing quote.

Needs to be something like...

Code: Select all

log(this.name,ship + " - no ship script attached, oxp won't work");

Re: [WIP] Reduce Weapons Damage

Posted: Thu Oct 16, 2014 7:56 pm
by Lone_Wolf
calling shipSpawned from startupComplete works great, thanks for the tip tricky.
The typos are because i was to eager to post, hadn't tested it.

New code (down to 1 script):

Code: Select all

// Each ship in game will have a separate instance of this script

// using separate values for player / npc for flexibility .
this._rwd_player_reduction = 8;
this._rwd_npc_reduction = 8;


this.startUpComplete = function()
  {
    // this.shipSpawned is called by game for all non-player ships, but not for player.ship
    this.shipSpawned(player.ship);
  };


this.shipSpawned = function(ship)
  {
    if ( !ship || !ship.isPiloted || ship.isThargoid || ship.isStation ) { return };
    if ( !ship.script ) { ship.setScript("oolite-default-ship-script.js"); };
    if ( ship.script.shipTakingDamage ) 
      {
	// there is already a shipTakingDamage handler, store it
	ship.script._rwd_oldShipTakingDamage = ship.script.shipTakingDamage;
      };
    if ( ship.isPlayer )
      {	ship.script._rwd_reduction = worldScripts["Reduce Weapon Damage"]._rwd_player_reduction; }
    else
      { ship.script._rwd_reduction = worldScripts["Reduce Weapon Damage"]._rwd_npc_reduction; };
    var new_std = function(amount, whom, type)
      {
	amount = worldScripts["Reduce Weapon Damage"]._rwd_ShipTakingDamage(this.ship, amount, whom, type); 
	if ( ship.script._rwd_oldShipTakingDamage )
	  { 
	    ship.script._rwd_oldShipTakingDamage(amount, whom, type); //call the original function 
	   };
      };
    ship.script.shipTakingDamage = new_std;
};
  
this._rwd_ShipTakingDamage = function (whoami, amount, whom, type)
  {
    if ( type != "energy damage" ) { return amount; }; // only act upon laser / missile damage
    if ( !whoami.isPlayer )
      {
	// npc ship, all that's needed is reducing damage amount
	amount -= whoami.script._rwd_reduction;
      }
    else
      {
	/* if shields have absorbed hit, amount == 0
	* this means there's no direct way to determine the strength of the hit
	* simplest solution to reduce damage is to increase shield strength
	*/
	if ( amount ==0 ) 
	  {
	    // try to determine whether forward or aft shield is hit
	    if( whom && whom.isValid && ( player.ship.vectorForward.dot(player.ship.position.subtract(whom.position) ) > 0 ) ) 
	      { 
		// forward shield hit
		player.ship.forwardShield += whoami.script._rwd_reduction;
	      }
	    else
	      {
		// aft hit
		player.ship.aftShield += whoami.script._rwd_reduction;
	      };
	  };
      };
  return amount;
  };

Re: [WIP] Reduce Weapons Damage

Posted: Thu Oct 16, 2014 8:22 pm
by Norby
Within shipSpawned the "this" keyword is equal with worldScripts["Reduce Weapon Damage"] and much faster so you can write this:

Code: Select all

ship.script._rwd_reduction = this._rwd_player_reduction;
In this way you make a copy of the reduction variable into all ship script, which can go out of sync if the value will change in the main storage in your worldScript. Not a problem in your case but in general I recommend to copy the pointer only and store the value in one place as I suggested in my previous post.

Re: [WIP] Reduce Weapons Damage

Posted: Thu Oct 16, 2014 10:25 pm
by Lone_Wolf
Norby, i do understand what you mean but have reasons why i decided not to do it that way.

In javascript the context a function runs in depends on where it is called from, not where it is declared.

the this value is inherited from the calling function, which has inherited it's this from it's caller.

i see the following calling paths in this oxp :

(rwd == worldscripts["Reduce Weapon Damage"] to shorten text )
worldscript initialization > rwd.startUpComplete > rwd.shipSpawned (player ship)

populator > non-player shipSpawned handler > rwd.shipSpawned (all other ships)

player ship gets hit > player shipTakingDamage handler > rwd._rwd_ShipTakingDamage

non-player ship gets hit > non-player shipTakingDamage handler > rwd._rwd_ShipTakingDamage

This indicates that there are FOUR different calling contexts, each has their own this value.

The calling context is determined by the oolite gamecode, even if the 4 contexts are the same in 1.80, will they stay the same in other versions ?

about speed :
shipSpawned only gets called once per ship, while shipTakingDamage could be called thousands of times in a play sesssion.

shipscripts are attached to a specific ship.
I copied the reduction values into the shipscript as i assume this is faster then looking up a pointer to a different object.

Re: [WIP] Reduce Weapons Damage

Posted: Fri Oct 17, 2014 12:14 am
by Norby
No problem, my polishments comes from my style what you can ignore, I just want to say a bit more to proof my statemets.
Lone_Wolf wrote:
worldscript initialization > rwd.startUpComplete > rwd.shipSpawned (player ship)
populator > non-player shipSpawned handler > rwd.shipSpawned (all other ships)
These two are the same. You can try if you put a log(this.name, this); both into startUpComplete and shipSpawned, you will see the same in the log. In these places you can use "this" instad of "rwd" safely.

I do not think this will change anytime due to the dev team do not want to break the existing OXPs, except with a good reason. Before any drastical change we always get more warnings and enough time to react.

In ship scripts you must use a pointer to reach your worldScript, so within your new_std function you must use "rwd", but if you insert this pointer into a ship.script._RWD variable in shipSpawed then you must not seek after in every shipTakingDamage which save some processing time.
Lone_Wolf wrote:
I copied the reduction values into the shipscript as i assume this is faster then looking up a pointer to a different object.
I think this is true and I assumed that you choosed your way due to this reason so I warned you about a possible drawback.

For example if you want to decrease the reduction when the rank of the player step up a level then for an instant effect you need cycle through all ship scripts to change all values, else the values in the existing ships stay at the old value. Not a big problem so you can ignore, just an approach which maybe save some bug hunting time later.

Re: [WIP] Reduce Weapons Damage

Posted: Fri Oct 17, 2014 6:57 am
by cim
Lone_Wolf wrote:
In javascript the context a function runs in depends on where it is called from, not where it is declared.
Normally this works reasonably well in a "do what I mean" way.
Lone_Wolf wrote:
the this value is inherited from the calling function, which has inherited it's this from it's caller.
In Oolite, the 'this' for a function - unless you're using a callback initiated by the Oolite core (timers, frame callbacks, mission screen callbacks, a few others) should always be the script which runs it, with one exception which I'll note afterwards.

rwd.shipSpawned will always run in the context of this == rwd
rwd._rwd_ShipTakingDamage will also always run in that context.

The one that's slightly complicated is this one.

Code: Select all

var new_std = function(amount, whom, type)
      {
   amount = worldScripts["Reduce Weapon Damage"]._rwd_ShipTakingDamage(this.ship, amount, whom, type);
   if ( ship.script._rwd_oldShipTakingDamage )
     {
       ship.script._rwd_oldShipTakingDamage(amount, whom, type); //call the original function
      };
      };
    ship.script.shipTakingDamage = new_std;
You've defined this in the context of the worldscript, and specifically inside a function in that worldscript. This means that it has access to the variables that worldscript function call does, including 'ship'. When the function is called by the ship script, however, the 'this' variable - in the call to _rwd_ShipTakingDamage - will be the ship script itself, not the RWD world script.
Lone_Wolf wrote:
The calling context is determined by the oolite gamecode, even if the 4 contexts are the same in 1.80, will they stay the same in other versions ?
Yes. Changing that would break far too much.
Lone_Wolf wrote:
I copied the reduction values into the shipscript as i assume this is faster then looking up a pointer to a different object.
Yes, although that sort of low-level optimisation is rarely necessary.

In this case, faster would be to not copy it, and to have

Code: Select all

amount -= this._rwd_reduction;
in the _rwd_ShipTakingDamage method.


Even faster - and probably by quite a bit more than the above - use _rwd_ShipTakingDamage for the NPCs only, and take out all the isPlayer paths and checks in that code. Define a separate this.shipTakingDamage handler in RWD and use that to handle the rather different isPlayer case. (Remember, all world scripts also get any player ship script event - it's usually much easier to use world scripts for player events than it is the player ship script)



Right, the exception. Javascript has a number of methods - bind(), apply(), etc. - for telling a function to run with 'this' other than its default context. This is used in the PriorityAI library to force all the condition, behaviour and configuration routines to run in the context of the PriorityAI Controller, rather than a mix of the AI script and the AI Controller.

Code: Select all

var boundfn = function (a,b) { ... }.bind(worldScripts["Reduce Weapon Damage"];
This will now always run in the context of this == RWD.

Generally you don't need to do this, and if you're concerned about speed/memory use you shouldn't unless you already know it's necessary.

Re: [WIP] Reduce Weapons Damage

Posted: Fri Oct 17, 2014 4:16 pm
by Lone_Wolf
cim wrote:
In Oolite, the 'this' for a function - unless you're using a callback initiated by the Oolite core (timers, frame callbacks, mission screen callbacks, a few others) should always be the script which runs it, with one exception which I'll note afterwards.
script which runs it points exactly to the issue, it's determined at run-time and the code has no idea what this will be, until it's run (except in cases where it's defined clearly by oolite gamecode).
cim wrote:
The one that's slightly complicated is this one.
<snip>
You've defined this in the context of the worldscript, and specifically inside a function in that worldscript. This means that it has access to the variables that worldscript function call does, including 'ship'. When the function is called by the ship script, however, the 'this' variable - in the call to _rwd_ShipTakingDamage - will be the ship script itself, not the RWD world script.
personally I don't like this method, but npc_shields / hardships / customshields all use it.
I haven't found another way to replace the oolite default handler for shipTakingDamage (or any other handler).
Do you know a better / cleaner way ?

i will look into splitting the npc & player std handlers.

-------------------------
summary about calling context in the above code :

this.shipSpawned this == worldscripts["Reduce Weapon Damage"]

this._rwd_ShipTakingDamage :
in the declaration (var new-std) this == worldscripts["Reduce Weapon Damage"] .
When being called by oolite default shipTakingDamage handler, this == ship.script


--------------------------------------
norby wrote:
Lone_Wolf wrote:
I copied the reduction values into the shipscript as i assume this is faster then looking up a pointer to a different object.
I think this is true and I assumed that you choosed your way due to this reason so I warned you about a possible drawback.

For example if you want to decrease the reduction when the rank of the player step up a level then for an instant effect you need cycle through all ship scripts to change all values, else the values in the existing ships stay at the old value. Not a big problem so you can ignore, just an approach which maybe save some bug hunting time later.
Noted, i'll look into doing that to make the code more futureproof

Re: [WIP] Reduce Weapons Damage

Posted: Fri Oct 17, 2014 5:58 pm
by cim
Lone_Wolf wrote:
script which runs it points exactly to the issue, it's determined at run-time and the code has no idea what this will be, until it's run (except in cases where it's defined clearly by oolite gamecode).
No, not so. (Or, at least, it is clearly defined by the oolite gamecode and the JS language syntax in almost all cases) - the behaviour of this in Javascript is fairly complicated, but unless you define your own object types and constructors, in Oolite most of that complexity can be ignored.

The script which runs the code is the script that it belongs to. If you say

Code: Select all

{script}.method = function() { ... }
then however you call that function, unless you explicitly bind() or apply() it, its this will be the script you attached it to.

The reason this works is that scripts in Oolite are themselves JS objects. If you ask Oolite to load a JS script file, what you get is an object (of JS type Script and Oolite core type OOJSScript). This object then has a bunch of properties, some of which are functions, based on the definitions in the script file (and can also have them attached at run-time later).

When you then do {script}.method() you're asking that Script object to run the method - that's what I meant by "the script which runs it", which I could have been clearer about - and as with any JS object method call, this points to the owning object by default.

The exceptions - timers and other callbacks - happen because you don't want those to run in the context of their owning object (which is an internal implementation detail to Oolite) - so Oolite tries to be helpful by setting this to be what looks like the creating Script object. It doesn't always get it right, which is why you should explicitly bind() callbacks when you create them.

So if I define in my shipscript:

Code: Select all

this._help = function() { this.ship.broadcastDistressMessage(); }
then I can call that from a chain of method calls that begins in a ship script event (for that ship or a different ship), in a world script event, in a player equipment script and within the function this.ship will always refer to the ship that instance of the ship script is attached to.

So when you define this line in new_std

Code: Select all

amount = worldScripts["Reduce Weapon Damage"]._rwd_ShipTakingDamage(this.ship, amount, whom, type);
you're defining it inside the worldscript. If, still in the world script, you did new_std(a,w,t); then this refers to the world script (and this.ship is undefined). Having assigned the function to the ship script with ship.script.shipTakingDamage = new_std; then this - when that copy of the function runs later on the ship script event - refers to the ship script, and this.ship is the script.

Both, however, will call worldScripts["Reduce Weapon Damage"]._rwd_ShipTakingDamage and when this function runs, it is run by the worldscript object regardless of where it gets called from. You don't currently use this in that function, but if it did, it would point to the worldscript object.
Lone_Wolf wrote:
Do you know a better / cleaner way ?
No, that's about it for NPCs. (It fails in the case where the ship changes its ship script after creation, but that's rare). Generally you should try to avoid making global changes by editing ship scripts, but sometimes it's the only option to get a particular effect.

Incidentally, out of politeness to other OXPers, you should probably check that the ship has ship.autoWeapons == true before modifying its script, or you might break things.

Re: [WIP] Reduce Weapons Damage

Posted: Fri Oct 17, 2014 7:09 pm
by Lone_Wolf
ok, calling context in oolite js is making more sense now.

A somewhat related question :
suppose i have references to equipment condtion scripts & equiment scripts in equipment.plist .
also some shipscripts in shipdata.plist .

If those scripts are not listed in worldscripts.plist , where does oolite look for them ?
cim wrote:
Incidentally, out of politeness to other OXPers, you should probably check that the ship has ship.autoWeapons == true before modifying its script, or you might break things.
looked up that property, and i'll implement a check for it.

Re: [WIP] Reduce Weapons Damage

Posted: Fri Oct 17, 2014 7:33 pm
by cim
Lone_Wolf wrote:
suppose i have references to equipment condtion scripts & equiment scripts in equipment.plist .
also some shipscripts in shipdata.plist .

If those scripts are not listed in worldscripts.plist , where does oolite look for them ?
All scripts regardless of type are loaded from the Scripts folder. Unlike a worldscript, however, there's no global object containing a reference to the other script types.

Re: [WIP] Reduce Weapons Damage

Posted: Fri Oct 17, 2014 7:43 pm
by Lone_Wolf
Latest code, a test-release may be coming soon.

Code: Select all

"use strict";
this.name           = "Reduce Weapon Damage";
this.author         = "Lone_Wolf";
this.copyright      = "(C) 2014 Lone_Wolf.";
this.licence        = "CC-by-SA 4.0";
this.description    = "Reduce laser/missile damage for ships";
this.version        = "0.10";

// Each ship in game will have a separate instance of this script

// using separate values for player / npc for flexibility .
this._rwd_player_reduction = 8;
this._rwd_npc_reduction = 8;


this.startUpComplete = function()
  {
    // setup handler for player.ship . npc-ships are dealt with by this.shipSpawned handler
    if ( !player.ship.script ) { player.ship.setScript("oolite-default-ship-script.js"); }; // just in case
    if ( player.ship.script.shipTakingDamage ) 
      {
	// there is already a shipTakingDamage handler, store it
	player.ship.script._rwd_player_oldShipTakingDamage = ship.script.shipTakingDamage;
      };
    player.ship.script._RWD = this; // setup pointer
    var new_std = function(amount, whom, type)
      {
	worldScripts["Reduce Weapon Damage"]._rwd_player_ShipTakingDamage(this.ship, amount, whom, type); 
	if ( player.ship.script._rwd_player_oldShipTakingDamage )
	  { 
	    player.ship.script._rwd_oldShipTakingDamage(amount, whom, type); //call the original function 
	   };
      };
    player.ship.script.shipTakingDamage = new_std;
  };

this._rwd_player_ShipTakingDamage = function (whoami, amount, whom, type)
  {
    if ( type != "energy damage" ) { return; }; // only act upon laser / missile damage
    if ( amount !=0 ) { return; }; // if hit penetrated shields, do nothing
    /* shields have absorbed hit, there is no known way to determine the strength of the hit
    * simplest solution to reduce damage is to increase shield strength
    */
    // try to determine whether forward or aft shield is hit
    if( whom && whom.isValid && ( player.ship.vectorForward.dot(player.ship.position.subtract(whom.position) ) > 0 ) ) 
      { 
	// forward shield hit
	player.ship.forwardShield += player.ship.script._RWD._rwd_player_reduction;
      }
    else
      {
	// aft hit
	player.ship.aftShield += player.ship.script._RWD._rwd_player_reduction;
      };
  };

  

this.shipSpawned = function(ship)
  {
    // called for non-player ships only
    if ( !ship || !ship.isPiloted || ship.isThargoid || ship.isStation ) { return; }; // only change npc-ships
    if ( !ship.autoWeapons ) {return; }; // ship properties are not supposed to be altered
    if ( !ship.script ) { ship.setScript("oolite-default-ship-script.js"); }; // just in case
    if ( ship.script.shipTakingDamage ) 
      {
	// there is already a shipTakingDamage handler, store it
	ship.script._rwd_npc_oldShipTakingDamage = ship.script.shipTakingDamage;
      };
    ship.script._RWD = this;
    var new_std = function(amount, whom, type)
      {
	amount = worldScripts["Reduce Weapon Damage"]._rwd_npc_ShipTakingDamage(this.ship, amount, whom, type); 
	if ( ship.script._rwd_npc_oldShipTakingDamage )
	  { 
	    ship.script._rwd_npc_oldShipTakingDamage(amount, whom, type); //call the original function 
	   };
      };
    ship.script.shipTakingDamage = new_std;
};
  
this._rwd_npc_ShipTakingDamage = function (whoami, amount, whom, type)
  {
    if ( type != "energy damage" ) { return amount; }; // only act upon laser / missile damage
    // npc ship, all that's needed is reducing damage amount
    amount -= whoami.script._RWD._rwd_npc_reduction;
    return amount;
  };

Re: [WIP] Reduce Weapons Damage

Posted: Sat Oct 18, 2014 11:49 am
by Lone_Wolf
Code is working well in my playtesting, i feel it's time to discuss the concept.

NOTE : this oxp is intended for 1.80.x oolite only, as 1.81+ already has changes to improve combat balance.

1. auto_weapons

In play testing i noticed that the oolite_template_viper doesn't have an auto_weapons entry, so it defaults to false.
Also auto_weapons was introduced in oolite 1.79, so all ships from oxps before 1.79 will have auto_weapons = false also.

In short, if i only allow the new shipTakingDamage handler for npc-ships with auto_weapons = yes , older npc-ships will NOT have the increased resistance to attacks.
This will make them weaker against player ship AND against newer npc-ships (including most core ships) with auto_weapons = yes.
My gut feeling is that this is an undesired side effect.

2. effect of attacker/defender strength and/or quality

if we want to vary the reduction based on those, i see some possibilities :

defender :
defenses ( shields / energy banks ) stronger defense > lower reduction.
pilot skill (kill count / accuracy ): better pilot > lower reduction

Attacker :
weapons ( military laser, aft laser, side lasers ) : better weapons > increase reduction
pilot skill (kill count /accuracy ) : more likely to score hits, increase reduction ?