Author Topic: Modding Examples  (Read 2520 times)

dalolorn

  • Sentient
  • **
  • Posts: 199
  • Karma: +7/-0
  • ABEM Developer
    • View Profile
Modding Examples
« on: January 13, 2015, 04:40:51 AM »
I could just post the answer to Zminer's problems in his thread. But it's simpler to just bunch it up with anything else I might end up posting in the future. (Also, this provides a place for others to ask for help, which translates into more winning for the modding community, right?! :D)

DISCLAIMER: While copy-pasting this stuff into the game should work (barring any unnoticed syntax errors, which seem to be my signature), I would advise you not to actually use it until you've figured out the underlying logic. Not only does it help you troubleshoot the code on your own (or alter it), but it'll also help you understand the underlying mechanics. One does not become a veteran modder without such an understanding... ;)

Edit: Example in the post below. Making a list for future use.

« Last Edit: May 15, 2015, 12:30:52 PM by dalolorn »

dalolorn

  • Sentient
  • **
  • Posts: 199
  • Karma: +7/-0
  • ABEM Developer
    • View Profile
Re: Modding Examples
« Reply #1 on: January 13, 2015, 05:11:42 AM »
Okay, here we go.

Re-edit: By the way, the way the system's been set up right now, it basically forces the designer to choose between having several weaker transfer systems (not a problem with a NonContiguous subsystem, as that's always a one-per-ship item - though one could arguably make several subsystems with the same ability, invalidating my comment) or having one full-power transfer mechanism. In the unusual event that you want to change this back and have any instances of the ability transfer at the ship-wide total rate, replace all references to "AT_SysVar" with "AT_String", change "Sys.SupplyTransferRate:500" to "SV_SupplyTransferRate", and replace all references to "value.fromSys(abl.subsystem, efficiencyObj=abl.obj)" with "caster.blueprint.getEfficiencySum(getSubsystemVariable(value.str))".

data/abilities/subsystems/SupplyTransfer.txt:

Code: [Select]
Ability: SupplyTransfer
// I don't actually have localization for this subsystem. It's a rapidly mashed up Buster Beam.
Name: #ABL_SUPPLY_TRANSFER
Description: #ABL_SUPPLY_TRANSFER_DESC
Icon: HexagonSubsystems::6 * #e900ff

Range: 500

Target: targ = Object
TargetFilterAllied(targ) // Don't want to resupply bad guys, do we? :D (Not that it wouldn't be fun.)

// Modified this from the Buster Beam. All except the first line is optional.
IsToggleTarget(targ)
CooldownOnDeactivate(targ, 10)
PersistentBeamEffect(targ, #0000ff, Laser, Width = 2.0) // This should produce a nice, blue beam of energy just gently and uniformly transferring energy from one ship to the other.

// New modders: Notice the "SupplyTransfer::" prefix. This is because we're not calling from ability_effects.as, the default namespace for ability hooks,
        // but are instead calling from SupplyTransfer.as. Which is also in scripts/definitions.
SupplyTransfer::TransferSupplyFromSubsystem(targ, Sys.SupplyTransferRate:500) // Notice the ":500" after Sys.SupplyTransferRate. This is how you set a default value for a SysVar argument. My code, however, handles default values a bit differently to begin with, for reasons I'll explain below.

scripts/definitions/SupplyTransfer.as:

Code: [Select]
// I imported stuff I needed, and plenty of other stuff I almost certainly didn't need. Call me messy, call me paranoid... it's your call. :P

// I also overcommented it for teaching purposes. Sorry. :(
import hooks;
import abilities;
import artifacts;
from abilities import AbilityHook;
import orbitals;
import target_filters;
from generic_effects import GenericEffect;
import systems;
import bonus_effects;
from map_effects import MakePlanet, MakeStar;
import listed_values;
#section server
from objects.Artifact import createArtifact;
import bool getCheatsEverOn() from "cheats";
from game_start import generateNewSystem;
#section all

import statuses;
from statuses import StatusHook;
import planet_effects;
import tile_resources;
from bonus_effects import BonusEffect;

class TransferSupplyFromSubsystem : AbilityHook {
Document doc("Gives supplies to its target while draining its own supplies, with a rate determined by a subsystem value. If the caster is not a ship, the default transfer rate is used instead, and the supply rate is irrelevant.");
Argument objTarg(TT_Object);
Argument value("Subsystem Value", AT_SysVar, doc="The subsystem value you wish to use to regulate the transfer. For example, HyperdriveSpeed would be Sys.HyperdriveSpeed (SV_HyperdriveSpeed in the old version outlined in my edit at the top of this post) - the transfer rate is 1 unit of supply per unit of HyperdriveSpeed in such a case.");
// Technically, you don't need the 'default' variable below or any references to it.
// However, the ingame mod tools don't explain the ability of arguments such as SysVar to take 'default' values, nor do they show you how to do this.
// When you're on your own, it makes little difference. When you're working with someone you don't COMPLETELY trust not to misunderstand how this works, though...
Argument preset("Default Rate", AT_Decimal, "500.0", doc="The default transfer rate, used if the subsystem value could not be found (or is less than 0). Defaults to 500.");

// This checks if the caster has any supplies to give. Non-Ship objects don't have supplies at all, but let's be friendly and let them use the default supply rate instead of rejecting them.
        // Keep in mind that blueprinted orbitals ('Stations') belong to the Ship class despite sort of being an orbital.
bool canActivate(const Ability@ abl, const Targets@ targs, bool ignoreCost) const override {
Ship@ caster = cast<Ship>(abl.obj);
if(caster !is null && caster.Supply == 0)
return false;
return true;
}

bool isValidTarget(Empire@ emp, uint index, const Target@ targ) const {
// This is not JUST a target - this is THE target contained in objTarget. (Used in abilities with more than one target, which don't exist and are not yet supported by the GUI.)
if(index != uint(objTarg.integer))
return true;
// Self-explanatory.
if(targ.obj is null)
return false;
// So, the target's a Ship. But does it need resupply?
if(targ.obj.isShip) {
Ship@ target = cast<Ship>(targ.obj);
return target.Supply < target.MaxSupply;
}
// We didn't return anything above. It's not a Ship, so it can't have supplies - tell the player to find someone else to pester.
return false;
}

#section server
// At this point, we're already running the resupply, so we have to double-check all the stuff in the above functions, and a few extra things.
void tick(Ability@ abl, any@ data, double time) const {
if(abl.obj is null)
return; // Bang, we're dead, but the ability got one last tick off. Get this zombie out of our disintegrated faces.
Target@ storeTarg = objTarg.fromTarget(abl.targets); // Not quite sure how this works, but it works, I guess.
if(storeTarg is null)
return; // We have no target. How'd that happen? Honestly, though, tick() might be executing even if the ability isn't running, and this is more of the devs' code, so let's play it safe.

Object@ target = storeTarg.obj;
if(target is null)
return; // We have a target, but it's not an object. Shoot the idiot that allowed it to happen. xD (On second thought, don't - unless you want no more help from me. :P)

Ship@ targetShip = cast<Ship>(target);
if(targetShip is null || targetShip.Supply == targetShip.MaxSupply) // Simple. We can't fill the target with more supplies than it can carry. Arguably, we could try doing this anyway, but who knows what that'd do. Probably nothing good.
return;

Ship@ caster = cast<Ship>(abl.obj);
bool castedByShip = caster !is null; // If caster is null, it means we're not a Ship, so... move along.
if(castedByShip && caster.Supply == 0) // If the caster isn't a Ship, we don't need to check its supplies. If it is, though... may I inspect your luggage, Captain?
return;

float resupply = targetShip.MaxSupply - targetShip.Supply; // The maximum amount the target can receive.
float resupplyCap = 0; // The maximum the ability lets us give to the other ship. In case we can't get any values, don't give them anything.

// This is a slightly trickier bit. We don't want to give more supplies per tick than is allowed by the subsystem (or, if the subsystem isn't the one granting the ability, more than the fallback 'default' value.).
if(castedByShip && value.fromSys(abl.subsystem, efficiencyObj=abl.obj) > 0) { // We are a Ship, and we got this ability the way it's intended to be had.
resupplyCap = value.fromSys(abl.subsystem, efficiencyObj=abl.obj) * time; // Apparently, tick() functions have to know the amount of game time elapsed since the last tick, suggesting it's non-constant. The implications of this are clear.
// Also, read post #4 by GGLucas for a fairly in-depth explanation of how value.fromSys() works. Short of quoting or paraphrasing him, I really can't explain it as well as he did - it's still too new to me.
}
else {
resupplyCap = preset.decimal * time; // The 'preset' value is now only called if whoever wrote the ability didn't set a default value for 'value'. Still, better safe than sorry.
}
if(resupplyCap < resupply)
resupply = resupplyCap;

// Now that we know how many supplies we're allowed to give, we have to see if we have that many supplies onboard. IF we're a Ship.
if(castedByShip && caster.Supply < resupply)
resupply = caster.Supply;

// Finally we're done checking all the ways we could end up giving more than we can. Time to give stuff away!
if(castedByShip)
caster.consumeSupply(resupply); // We are not a Planet, an Orbital, an Artifact or some other thing, so we have limited supplies to hand out.
targetShip.refundSupply(resupply); // Ever wondered what'd happen if you fed these two functions a negative value? Well, you can try it, but you'll have to rework a few things in the hook's code to do it. I'd give the engine a fifty-fifty chance of NOT breaking into tears if you abused it like that, but I have never investigated these two functions.
}
#section all
};

Edit: Sorry, I forgot about the actual subsystem. Let's be lazy (or let's try to imitate Space Empires :D) and give the ability to the SupplyModule (Supply Storage).

data/subsystems/flagships/SupplyModule.txt:

Code: [Select]
Subsystem: SupplyModule
Name: #SUPPLYMODULE_NAME
Description: #SUPPLYMODULE_DESC
Picture: SubsystemButtonArt::8 * #b3ffef
BaseColor: #2fd6b3
Elevation: 3

Tags: DefaultUnlock, NonContiguous, NoCore, HasInternals, Category:Control
Tags: Ability:SupplyTransfer // Placed in its own tag category for emphasis. Also, because I'm not sure what happens if you place a comma in there and didn't want to read the scripts to find out.
Hull: Flagship, Station

Size := HexSize * Hexes
SupplyCapacity := Size * 5000
SupplyRate := Size * 6
SupplyTransferRate := Size * 3 // Half of the module's actual resupply rate can be used to resupply allies. Fair enough, eh?

Hex.LaborCost := 0.3 * HexSize + 0.8
Hex.BuildCost := 3 * HexSize + 2
Hex.MaintainCost := 8 * HexSize + 5

Hex.Resistance := 0.2
Hex.HP := 18 * HexSize
Hex.Mass := HexSize

Modifier: SupplyCapacityFactor(factor)
SupplyCapacity := SupplyCapacity * factor

Effect: LeakSupply
LeakPctPerSec = 1 / 20

Module: Default
Sprite: SupplyStorage
« Last Edit: January 30, 2015, 09:28:36 AM by dalolorn »

GGLucas

  • Dr. Evil
  • BMS Staff
  • Delusional
  • *
  • Posts: 1877
  • Karma: +300/-6
    • View Profile
Re: Modding Examples
« Reply #2 on: January 13, 2015, 10:38:02 AM »
You're not using getEfficiencySum correctly there :P

You would need to do something like:

Code: [Select]
caster.blueprint.getEfficiencySum(getSubsystemVariable(value.str))

Since it's a call on the ship's blueprint, and it takes a subsystem variable id, not string, so you need to look that up first.

There's a better way to do this with subsystem variables, but I noticed it doesn't take into account efficiency from damage, so I'm adding that. Once the next update hits I'll post an example here about how to more easily take a subsystem variable as an argument in an ability hook.

Code: [Select]
// I have no idea what this is. Possibly a check to see if you're not targeting yourself?
if(index != uint(objTarget.integer))
return true;

Abilities can ~technically~ have multiple targets. The UI doesn't deal with that because we haven't needed it, but this makes sure that the target that is being checked for validity is the target specified in the objTarget argument.

Code: [Select]
Target@ storeTarg = objTarg.fromTarget(abl.targets); // Not quite sure how this works, but it works, I guess.

abl.targets is the list of currently stored targets in the ability (separate from the targets used for activation, IsToggleTarget stores the target it's activated on into its matching stored target).

The fromTarget call on the argument returns the particular stored target from the list in the ability that the argument refers to.
« Last Edit: January 13, 2015, 10:42:21 AM by GGLucas »

dalolorn

  • Sentient
  • **
  • Posts: 199
  • Karma: +7/-0
  • ABEM Developer
    • View Profile
Re: Modding Examples
« Reply #3 on: January 13, 2015, 11:00:42 AM »
You're not using getEfficiencySum correctly there :P

You would need to do something like:

Code: [Select]
caster.blueprint.getEfficiencySum(getSubsystemVariable(value.str))

Since it's a call on the ship's blueprint, and it takes a subsystem variable id, not string, so you need to look that up first.

There's a better way to do this with subsystem variables, but I noticed it doesn't take into account efficiency from damage, so I'm adding that. Once the next update hits I'll post an example here about how to more easily take a subsystem variable as an argument in an ability hook.

... You'd think I checked my fully-functioning Interdictor code before posting that one...  :-[
« Last Edit: January 13, 2015, 11:03:38 AM by dalolorn »

GGLucas

  • Dr. Evil
  • BMS Staff
  • Delusional
  • *
  • Posts: 1877
  • Karma: +300/-6
    • View Profile
Re: Modding Examples
« Reply #4 on: January 14, 2015, 07:28:35 PM »
For the latest experimental build:

You can create an argument of type AT_SysVar. This is used for the graviton condensor. The hook says:

Code: [Select]
DealStellarDamageOverTime(targ, Sys.StellarDamage:7000000)
The :7000000 part specifies the default value if this ability is not added by a subsystem.

In the hook, it is used like this:

Code: [Select]
double amt = dmg_per_second.fromSys(abl.subsystem, efficiencyObj=abl.obj) * time;

The fromSys() call on the argument takes the subsystem stored in the ability (this can be null, but fromSys() deals with that), and determines the value to return based on the subsystem variable or the default passed in the AT_SysVar argument. The efficiencyObj argument is optional, and if specified the fromSys() function will also calculate the efficiciency due to damage and modify the value by that.

This is better than getEfficiencySum because it's cleaner, and because it only takes the value from the subsystem being used, not from all subsystems that have that variable (so two separate subsystems won't use the sum value of their variables, but each gets calculated separately).

Note that this only works if the ability is added through an "Ability:ID" tag in the subsystem, not if it's added through any hook. If it is added in another way than with a tag, it will just use the default value specified in the argument.


dalolorn

  • Sentient
  • **
  • Posts: 199
  • Karma: +7/-0
  • ABEM Developer
    • View Profile
Re: Modding Examples
« Reply #5 on: January 15, 2015, 02:03:22 AM »
For the latest experimental build:

You can create an argument of type AT_SysVar. This is used for the graviton condensor. The hook says:

Code: [Select]
DealStellarDamageOverTime(targ, Sys.StellarDamage:7000000)

Whoo, stars are no longer invulnerable! Anyway, AT_SysVar, got it.

Quote
The :7000000 part specifies the default value if this ability is not added by a subsystem.

Yeah, I saw it in a few places, but wasn't sure which cases it was possible to use that in.

Quote
In the hook, it is used like this:

Code: [Select]
double amt = dmg_per_second.fromSys(abl.subsystem, efficiencyObj=abl.obj) * time;

The fromSys() call on the argument takes the subsystem stored in the ability (this can be null, but fromSys() deals with that), and determines the value to return based on the subsystem variable or the default passed in the AT_SysVar argument. The efficiencyObj argument is optional, and if specified the fromSys() function will also calculate the efficiciency due to damage and modify the value by that.

Right, that one's good to know.

Quote
This is better than getEfficiencySum because it's cleaner, and because it only takes the value from the subsystem being used, not from all subsystems that have that variable (so two separate subsystems won't use the sum value of their variables, but each gets calculated separately).

I fail to see the relevance of this when you can (to my knowledge) only have one instance of an ability, no matter how many subsystems grant it?  ???

Edit: Useful for a subsystem-granted status, though.

Quote
Note that this only works if the ability is added through an "Ability:ID" tag in the subsystem, not if it's added through any hook. If it is added in another way than with a tag, it will just use the default value specified in the argument.

... About that, Alar said that he couldn't get more than one "Ability:ID" tag to work. That's why our subsystems grant second, third and fourth abilities via hooks. True, I didn't check his claims to see if he was doing it wrong, but...
« Last Edit: January 15, 2015, 05:12:08 AM by dalolorn »

GGLucas

  • Dr. Evil
  • BMS Staff
  • Delusional
  • *
  • Posts: 1877
  • Karma: +300/-6
    • View Profile
Re: Modding Examples
« Reply #6 on: January 15, 2015, 10:37:10 AM »
Quote
I fail to see the relevance of this when you can (to my knowledge) only have one instance of an ability, no matter how many subsystems grant it?

I don't know where you got that knowledge, but it's not true in the slightest :P

Quote
About that, Alar said that he couldn't get more than one "Ability:ID" tag to work.

Yes, that's unfortunately true. I'll see if I can fix that for the future.

EDIT: Should be fixed in the next update. Turned out easier than I thought it would be.
« Last Edit: January 15, 2015, 10:45:30 AM by GGLucas »

dalolorn

  • Sentient
  • **
  • Posts: 199
  • Karma: +7/-0
  • ABEM Developer
    • View Profile
Re: Modding Examples
« Reply #7 on: January 15, 2015, 11:20:14 AM »
I don't know where you got that knowledge, but it's not true in the slightest :P

Yeah, I noticed a screenshot of 4 Superlasers unlocking 4 Buster Beams (ludicrous, seeing as you can only realistically use one at a time with that range, but whatever) in ABEM after writing that, and forgot to go back and edit it. Sorry.

Quote
Yes, that's unfortunately true. I'll see if I can fix that for the future.

EDIT: Should be fixed in the next update. Turned out easier than I thought it would be.

Yay! :D

Edit: In any case, updating the code now.
« Last Edit: January 15, 2015, 01:45:58 PM by dalolorn »