Modding:Mutations
This page is about modding. See the modding overview for an abstract on modding. |
Tutorial
First, include a mutations.xml in your mod that defines a new mutation. Mutations.xml sample, adding a simple mod
<?xml version="1.0" encoding="utf-8" ?>
<mutations>
<category Name="Physical">
<mutation Name="Udder" Cost="1" MaxSelected="1" Class="FreeholdTutorial_Udder" Exclusions="" Code="ea"></mutation>
</category>
</mutations>
Then add a new .cs file in your mod that implements the class. Here's a skeletal implementation of the entry above. It must be in the XRL.World.Parts.Mutation namespace and must ultimately descend from BaseMutation. (Though not necessarily directly, if you have a very complex mod)
Skeletal mutation example.
using System;
using System.Collections.Generic;
using System.Text;
using XRL.Rules;
using XRL.Messages;
using ConsoleLib.Console;
namespace XRL.World.Parts.Mutation
{
[Serializable]
class FreeholdTutorial_Udder : BaseMutation
{
public override void Register(GameObject Object)
{
}
public override string GetDescription()
{
return "";
}
public override string GetLevelText(int Level)
{
string Ret = "You have udders.\n";
return Ret;
}
public override bool WantEvent(int ID, int cascade)
{
return base.WantEvent(ID, cascade) || ID == BeforeRenderEvent.ID;
}
public override bool HandleEvent(BeforeRenderEvent e)
{
if (ParentObject.IsPlayer())
{
if (ParentObject.pPhysics != null && ParentObject.pPhysics.CurrentCell != null)
{
ParentObject.pPhysics.CurrentCell.ParentZone.AddLight(ParentObject.pPhysics.CurrentCell.X, ParentObject.pPhysics.CurrentCell.Y, Level, LightLevel.Darkvision);
}
}
return true;
}
public override bool FireEvent(Event E)
{
return base.FireEvent(E);
}
public override bool ChangeLevel(int NewLevel)
{
return true;
}
public override bool Mutate(GameObject GO, int Level)
{
return true;
}
public override bool Unmutate(GameObject GO)
{
return true;
}
}
}
Here's a full decompiled example of the Flaming Hands mutation from the game's source code. (Note that, because it's decompiled, some of the variables have very generic names, like gameObject
instead of something more descriptive, like target
)
using System;
using System.Collections.Generic;
using System.Threading;
using ConsoleLib.Console;
using XRL.Core;
using XRL.Rules;
using XRL.UI;
namespace XRL.World.Parts.Mutation
{
[Serializable]
public class FlamingHands : BaseMutation
{
public override bool GeneratesEquipment()
{
return true;
}
public override void Register(GameObject Object)
{
Object.RegisterPartEvent(this, "CommandFlamingHands");
Object.RegisterPartEvent(this, "AIGetOffensiveMutationList");
base.Register(Object);
}
public override string GetDescription()
{
return "You emit jets of flame from your hands.";
}
public override string GetLevelText(int Level)
{
return "Emits a 9-square ray of flame in the direction of your choice\n" + "Cooldown: 10 rounds\n" + "Damage: " + this.ComputeDamage(Level) + "\n" + "Cannot wear gloves";
}
public string ComputeDamage(int UseLevel)
{
string text = UseLevel + "d4";
if (this.ParentObject != null)
{
int partCount = this.ParentObject.GetPart<Body>().GetPartCount(this.BodyPartType);
if (partCount > 0)
{
text = text + "+" + partCount;
}
}
else
{
text += "+1";
}
return text;
}
public string ComputeDamage()
{
return this.ComputeDamage(base.Level);
}
public void Flame(Cell C, ScreenBuffer Buffer, bool doEffect = true)
{
string dice = this.ComputeDamage();
if (C != null)
{
foreach (GameObject gameObject in C.GetObjectsInCell())
{
if (gameObject.PhaseMatches(this.ParentObject))
{
gameObject.TemperatureChange(310 + 25 * base.Level, this.ParentObject, false, false, false, 0, null, null);
if (doEffect)
{
for (int i = 0; i < 5; i++)
{
gameObject.ParticleText("&r" + ((char)(219 + Stat.Random(0, 4))).ToString(), 2.9f, 1);
}
for (int j = 0; j < 5; j++)
{
gameObject.ParticleText("&R" + ((char)(219 + Stat.Random(0, 4))).ToString(), 2.9f, 1);
}
for (int k = 0; k < 5; k++)
{
gameObject.ParticleText("&W" + ((char)(219 + Stat.Random(0, 4))).ToString(), 2.9f, 1);
}
}
}
}
DieRoll cachedDieRoll = dice.GetCachedDieRoll();
foreach (GameObject gameObject2 in C.GetObjectsWithPartReadonly("Combat"))
{
if (gameObject2.PhaseMatches(this.ParentObject))
{
Damage damage = new Damage(cachedDieRoll.Resolve());
damage.AddAttribute("Fire");
damage.AddAttribute("Heat");
Event @event = Event.New("TakeDamage", 0, 0, 0);
@event.SetParameter("Damage", damage);
@event.SetParameter("Owner", this.ParentObject);
@event.SetParameter("Attacker", this.ParentObject);
@event.SetParameter("Message", "from %o flames!");
gameObject2.FireEvent(@event);
}
}
}
if (doEffect)
{
Buffer.Goto(C.X, C.Y);
string str = "&C";
int num = Stat.Random(1, 3);
if (num == 1)
{
str = "&R";
}
if (num == 2)
{
str = "&r";
}
if (num == 3)
{
str = "&W";
}
int num2 = Stat.Random(1, 3);
if (num2 == 1)
{
str += "^R";
}
if (num2 == 2)
{
str += "^r";
}
if (num2 == 3)
{
str += "^W";
}
if (C.ParentZone == XRLCore.Core.Game.ZoneManager.ActiveZone)
{
Stat.Random(1, 3);
Buffer.Write(str + ((char)(219 + Stat.Random(0, 4))).ToString(), true);
Popup._TextConsole.DrawBuffer(Buffer, null, false);
Thread.Sleep(10);
}
}
}
public static bool Cast(FlamingHands mutation = null, string level = "5-6")
{
if (mutation == null)
{
mutation = new FlamingHands();
mutation.Level = Stat.Roll(level, null);
mutation.ParentObject = XRLCore.Core.Game.Player.Body;
}
ScreenBuffer scrapBuffer = ScreenBuffer.GetScrapBuffer1(true);
XRLCore.Core.RenderMapToBuffer(scrapBuffer);
List<Cell> list = mutation.PickLine(9, AllowVis.Any, null, false, null);
if (list == null)
{
return true;
}
if (list.Count <= 0)
{
return true;
}
if (list != null)
{
if (list.Count == 1 && mutation.ParentObject.IsPlayer() && Popup.ShowYesNoCancel("Are you sure you want to target yourself?") != DialogResult.Yes)
{
return true;
}
mutation.CooldownMyActivatedAbility(mutation.FlamingHandsActivatedAbilityID, 10, null);
mutation.UseEnergy(1000);
mutation.PlayWorldSound(mutation.Sound, 0.5f, 0f, true, null);
int num = 0;
while (num < 9 && num < list.Count)
{
if (list.Count == 1 || list[num] != mutation.ParentObject.pPhysics.CurrentCell)
{
mutation.Flame(list[num], scrapBuffer, true);
}
foreach (GameObject gameObject in list[num].LoopObjectsWithPart("Physics"))
{
if (gameObject.pPhysics.Solid && gameObject.GetIntProperty("AllowMissiles", 0) == 0)
{
Forcefield part = gameObject.GetPart<Forcefield>();
if (part == null || !part.CanMissilePassFrom(mutation.ParentObject, null))
{
num = 999;
break;
}
}
}
num++;
}
}
return true;
}
public override bool FireEvent(Event E)
{
if (E.ID == "AIGetOffensiveMutationList")
{
if (E.GetIntParameter("Distance", 0) <= 9 && base.IsMyActivatedAbilityAIUsable(this.FlamingHandsActivatedAbilityID, null) && this.ParentObject.HasLOSTo(E.GetGameObjectParameter("Target"), true, true, null))
{
E.AddAICommand("CommandFlamingHands", 1, null, false);
}
}
else if (E.ID == "CommandFlamingHands")
{
return FlamingHands.Cast(this, "5-6");
}
return true;
}
public override bool ChangeLevel(int NewLevel)
{
if (GameObject.validate(ref this.FlamesObject))
{
this.FlamesObject.GetPart<TemperatureOnHit>().Amount = base.Level * 2 + "d8";
}
return base.ChangeLevel(NewLevel);
}
private void AddAbility()
{
this.FlamingHandsActivatedAbilityID = base.AddMyActivatedAbility("Flaming Hands", "CommandFlamingHands", "Physical Mutation", -1, null, "\a", false, false, false, false, false, false, null);
}
public override bool Mutate(GameObject GO, int Level)
{
this.Unmutate(GO);
if (this.CreateObject)
{
Body part = GO.GetPart<Body>();
if (part != null)
{
BodyPart firstPart = part.GetFirstPart(this.BodyPartType);
if (firstPart != null)
{
GO.FireEvent(Event.New("CommandForceUnequipObject", "BodyPart", firstPart));
this.FlamesObject = GameObject.create("Ghostly Flames");
this.FlamesObject.GetPart<Armor>().WornOn = firstPart.Type;
Event @event = Event.New("CommandForceEquipObject", 0, 0, 0);
@event.SetParameter("Object", this.FlamesObject);
@event.SetParameter("BodyPart", firstPart);
@event.SetSilent(true);
GO.FireEvent(@event);
this.AddAbility();
}
}
}
else
{
this.AddAbility();
}
this.ChangeLevel(Level);
return base.Mutate(GO, Level);
}
public override bool Unmutate(GameObject GO)
{
base.CleanUpMutationEquipment(GO, ref this.FlamesObject);
base.RemoveMyActivatedAbility(ref this.FlamingHandsActivatedAbilityID, null);
return base.Unmutate(GO);
}
public string BodyPartType = "Hands";
public bool CreateObject = true;
public string Sound = "burn_crackling";
public GameObject FlamesObject;
public Guid FlamingHandsActivatedAbilityID = Guid.Empty;
}
}
Your namespace should be XRL.World.Parts.Mutations, the class should be marked serializeable and derived from BaseMutation.
using System;
using System.Collections.Generic;
using System.Threading;
using ConsoleLib.Console;
using XRL.Core;
using XRL.Rules;
using XRL.UI;
namespace XRL.World.Parts.Mutation
{
[Serializable]
public class FlamingHands : BaseMutation
{
GetDescription and GetLevelText are called to generate the descriptive for a given level of the mutation.
public override string GetDescription()
{
return "You emit jets of flame from your hands.";
}
public override string GetLevelText(int Level)
{
return "Emits a 9-square ray of flame in the direction of your choice\n" + "Cooldown: 10 rounds\n" + "Damage: " + this.ComputeDamage(Level) + "\n" + "Cannot wear gloves";
}
Change level is called any time the mutation changes level.
public override bool ChangeLevel(int NewLevel)
{
if (GameObject.validate(ref this.FlamesObject))
{
this.FlamesObject.GetPart<TemperatureOnHit>().Amount = base.Level * 2 + "d8";
}
return base.ChangeLevel(NewLevel);
}
Mutate and Unmutate are called on an object when it gains or loses the mutation.
public override bool Mutate(GameObject GO, int Level)
public override bool Unmutate(GameObject GO)
BaseMutation derives from Part, so the typical event registration and handling functions are available
public override void Register(GameObject Object)
public override bool FireEvent(Event E)
XML Parameters and Additional Details
You may add the following elements to a <mutation> tag in Mutations.xml
- Name
- Class
- Cost
- MaxSelected
- Constructor (optional)
- Exclusions (optional)
- BearerDescription
- Code
Name
Name of the mutation as it appears in the character creation screen.
Class
The name of the .cs Class object that is used to instantiate your mutation object.
Cost
The mutation costs this many mutation points when a character selects it during the character creation process.
MaxSelected
The maximum number of copies of this mutation that can be selected during character creation.
Currently this is only used for Unstable Mutation and there is some hard-coded special handling of that mutation when it comes to details such as constructing a Build Library code for builds that include Unstable Mutation. It is not clear if this value can be set to more than 1 for a modded mutation without causing some problems.
Constructor
This element should be a string argument (or a comma-delimited string of arguments, if there are more than one) to pass to the mutation's class constructor. All such arguments are received as string parameters in the mutation class constructor. For example, Corrosive Gas Generation and Sleep Gas Generation both use the same mutation class, GasGeneration. However, Mutations.xml passes a different argument to the constructor, which indicates which type of gas should be used.
Mutations.xml
<mutation Name="Sleep Gas Generation" Cost="2" MaxSelected="1" Class="GasGeneration" Constructor="SleepGas" Exclusions="Corrosive Gas Generation" BearerDescription="those whe expel sleep gass" Code="bu"></mutation>
GasGeneration.cs
namespace XRL.World.Parts.Mutation
{
[Serializable]
public class GasGeneration : BaseMutation
{
public GasGeneration(string _GasObject)
{
this.GasObject = _GasObject;
this.SyncFromBlueprint();
}
The GasObject property is set to to "SleepGas" in this case, because that was the value provided in the Constructor element of Mutations.xml.
Theoretically you could create a new mutation that generates any type of gas simply by adding a single <mutation> tag to a Mutations.xml file. For example, this Mutations.xml file alone would create a new mutation called "Confusion Gas Generation"
<?xml version="1.0" encoding="utf-8" ?>
<mutations>
<category Name="Physical">
<mutation Name="Confusion Gas Generation" Cost="2" MaxSelected="1" Class="GasGeneration" Constructor="ConfusionGas" Exclusions="" BearerDescription="those who expel confusion gas" Code="zz"></mutation>
</category>
</mutations>
Exclusions
The Exclusions parameter defines mutations that should be considered mutually exclusive with this mutation. For example, you can only have one type of back-slot mutation, so the game defines the other three types of back-slot mutations as Exclusions in the Mutations.xml file.
Example from Mutations.xml:
<mutation Name="Stinger (Confusing Venom)" Cost="3" MaxSelected="1" Class="Stinger" Constructor="Confuse" Exclusions="Stinger (Paralyzing Venom),Stinger (Poisoning Venom),Wings" BearerDescription="those with stingers tipped with confusing venom" Code="bx"></mutation>
BearerDescription
It appears that this description is used in some of the random generation algorithms for villages and history in the game. For example, if a village reveres mutants with the Multiple Arms mutation, they might use the string defined in Mutations.xml ("the many-armed") to describe them in their praises or monuments.
Code
The Code element value is used when constructing a Build Library code for characters you create.
It is unclear if there is really a "best practice" for codes. Probably one should avoid using the codes used by base game mutations, but conflict with other mods may be inevitable. It would appear that this code can be longer than 2 characters, but that is an untested hypothesis.
|