User:MomBun/Sandbox
This page is about modding. See the modding overview for an abstract on modding. |
XML, Parameters, and First Steps
One of the first and important steps in adding a mutation to your mod is the file Mutations.XML, in which you'll have to add this:
<?xml version="1.0" encoding="utf-8" ?>
<mutations>
<category Name="[CATEGORY]">
<mutation Name="[NAME]" Cost="[NUMBER]" MaxSelected="[NUMBER]" Class="[CS CLASS]" Tile="Mutations/YOUR_IMAGE_HERE" Foreground="COLOR" Background="COLOR" </mutation>
</category>
</mutations>
For an in-game example of what to input, here is the XML code for Flaming Hands from Mutations.XML
<category Name="Physical" DisplayName="{{G|Physical Mutations}}" Property="PhysicalMutationShift" ForceProperty="PhysicalMutationForceShift" IncludeInMutatePool="true">
<mutation Name="Flaming Ray" Cost="4" MaxSelected="1" Class="FlamingHands" Exclusions="Freezing Ray" BearerDescription="the flaming-handed" Code="bh" Tile="Mutations/flaming_ray.bmp" />
</category>
Name
This is the name of the mutation as it will appear in game.
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.
Class
The name of the .cs Class object that is used to instantiate your mutation object.
MaxSelected
The maximum number of copies of this mutation that can be selected during character creation.
Tile
The tile you will be using for said mutation in-game.
By default most mutations use gold/yellow for background and brown for foreground but this can be customized for any color of your choice, see Modding:Tiles and Modding:Colors_&_Object_Rendering for further info on this.
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.
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.
Constructor
This is used by very few mutations, and its use case is as 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.
It should be noted that this parameter is both advance and old, and it shouldn't be necessary and potentially avoided.
C# Scripting, and Making The Mutation
Here is an example of a mutation that will add udders to your character, which will edit the title of your character to add udders, and make you moo occasionally.
using System;
using System.Collections.Generic;
using System.Text;
using XRL.Rules;
using XRL.Messages;
using ConsoleLib.Console;
// Namespace is a necessity here, and allows the game to find your mutation and add it to the game
namespace XRL.World.Parts.Mutation
{
// This is also necessary as it allows the game to save info related to your mutation, for further info on this, check out the guide on serialization at [[Modding:Serialization_(Saving/Loading)]]
[Serializable]
// This defines the class that you will call in Mutations.XML
class QudWiki_Udders : BaseMutation
{
// This sets the description for your mutation
public override string GetDescription()
{
return "You have udders!";
}
// This sets the description for what exactly your mutation does.
// It is good idea to make helper functions like the "ChanceToMoo" to make dynamic descriptions for changing rules for a mutation.
public override string GetLevelText(int Level)
{
return "{{rules|You have a " + ChanceToMoo(Level) + "% chance to moo per turn}}";
}
public int ChanceToMoo(int Level)
{
return Level;
}
// This is called every time the mutation changes level, and can be used to change things like damage.
// We don't use "ChanceToMoo" in this example so we can allow GetLevelText to use it.
public override bool ChangeLevel(int NewLevel)
{
return true;
}
// These two are called upon when an object gains said mutation and what happens, and is used to add or remove things as necessary
public override bool Mutate(GameObject MUTANT, int Level)
{
return base.Mutate(MUTANT, Level);
}
public override bool Unmutate(GameObject MUTANT)
{
return Base.Unmutate(MUTANT);
}
// Caves of Qud uses two different event systems so here is an example of each
// This is how you tell the "Minimal events" system we want to handle a specific type of event.
// We will listen for GetDisplayNameEvent.
public override bool WantEvent(int ID, int cascade)
{
return base.WantEvent(ID, cascade) || ID == GetDisplayNameEvent.ID;
}
// Handle the GetDisplayNameEvent
public override bool HandleEvent(GetDisplayNameEvent e)
{
var DescriptionBuilder = e.DB;
// should show up when you look at anything with the mutation.
DescriptionBuilder.AddWithClause("udders");
return true;
}
// This is how you handle the other style of qud events
// This is a much more effecient registration for this type of event and should be enabled.
// Tells the event system that all your registrations are handled in your Register(GameObject) method.
public override bool AllowStaticRegistration()
{
return true;
}
// First we must register the event
public override void Register(GameObject obj) {
obj.RegisterPartEvent(this, "EndTurn");
// Call the base Register method that we overrode.
base.Register(obj);
}
// Then we can handle the EndTurn type events here.
public override bool FireEvent(Event E)
{
if (E.ID == "EndTurn")
{
// Reusing the same method we used in GetLevelText means that both of them will remain accurate.
if (ChanceToMoo(Level).in100()) DidX("moo");
}
return base.FireEvent(E);
}
}
}
Bigger Example
TODO: Re-work the huge file of flaminghands.cs to be have comments and explain it , possibly with Gnarf's help if they are okay/free to do so
This is the source code from FlamingHands.cs
as an example of a more complex mutation.
using ConsoleLib.Console;
using System.Collections.Generic;
using System;
using XRL.UI;
namespace XRL.World.Parts.Mutation
{
/// <summary>
/// FlamingHands powers the "Flaming Ray" mutation. You can now choose a variant from hands, feet or face.
/// </summary>
[Serializable]
public class FlamingHands : BaseDefaultEquipmentMutation
{
public FlamingHands()
{
DisplayName = "Flaming Ray";
}
/// <summary>The <see cref="BodyPart.Type" /> we replace (chosen by variant selection.)</summary>
public string BodyPartType = "Hands";
/// <summary>Do we still need to create the object? Setup as a public for serialization purposes.</summary>
public bool CreateObject = true;
/// <summary>Sound file to play when attacking.</summary>
public string Sound = "Abilities/sfx_ability_mutation_flamingRay_attack";
[NonSerialized] private static GameObject _Projectile;
/// <summary>Create or retrive the already created Projectile game object.</summary>
private static GameObject Projectile
{
get
{
if (!GameObject.validate(ref _Projectile))
{
_Projectile = GameObject.createUnmodified("ProjectileFlamingHands");
}
return _Projectile;
}
}
/// <summary>We are request to be re-mutated automatically when our body is rebuilt. Thanks slog.</summary>
public override bool GeneratesEquipment()
{
return true;
}
public override void Register(GameObject Object)
{
Object.RegisterPartEvent(this, "AIGetOffensiveMutationList");
Object.RegisterPartEvent(this, "AttackerHit");
Object.RegisterPartEvent(this, "CommandFlamingHands");
base.Register(Object);
}
/// <summary>Show selected variant in character creation.</summary>
public override string GetCreateCharacterDisplayName()
{
return DisplayName + " (" + BodyPartType + ")";
}
public override string GetDescription()
{
BodyPart part = GetRegisteredSlot(BodyPartType, true);
if (part != null)
{
return "You emit a ray of flame from your " + part.GetOrdinalName() + ".";
}
else
{
return "You emit a ray of flame.";
}
}
public override string GetLevelText(int level)
{
string Ret = "Emits a 9-square ray of flame in the direction of your choice.\n";
Ret += "Damage: {{rules|" + ComputeDamage(level) + "}}\n";
Ret += "Cooldown: 10 rounds\n";
Ret += "Melee attacks heat opponents by {{rules|" + GetHeatOnHitAmount(level) + "}} degrees";
return Ret;
}
public string GetHeatOnHitAmount(int level)
{
return (level * 2) + "d8";
}
public string ComputeDamage(int level)
{
string Result = level + "d4";
if (ParentObject != null)
{
int LimbCount = ParentObject.Body.GetPartCount(BodyPartType);
if (LimbCount > 0)
{
Result += "+" + LimbCount;
}
}
else
{
Result += "+1";
}
return Result;
}
public string ComputeDamage() => ComputeDamage(Level);
public void Flame(Cell C, ScreenBuffer Buffer, bool doEffect = true)
{
string Damage = ComputeDamage();
if (C != null)
{
List<GameObject> Objects = C.GetObjectsInCell();
foreach (GameObject GO in Objects)
{
if( GO.PhaseMatches( ParentObject ) )
{
GO.TemperatureChange(310 + (25 * Level), Actor: ParentObject);
if( doEffect )
{
for (int x = 0; x < 5; x++) GO.ParticleText("&r" + (char)(219 + Rules.Stat.Random(0, 4)), 2.9f, 1);
for (int x = 0; x < 5; x++) GO.ParticleText("&R" + (char)(219 + Rules.Stat.Random(0, 4)), 2.9f, 1);
for (int x = 0; x < 5; x++) GO.ParticleText("&W" + (char)(219 + Rules.Stat.Random(0, 4)), 2.9f, 1);
}
}
}
int phase = ParentObject.GetPhase();
Rules.DieRoll dmgRoll = Damage.GetCachedDieRoll();
foreach (GameObject GO in C.GetObjectsWithPartReadonly("Combat"))
{
GO.TakeDamage(
Amount: dmgRoll.Resolve(),
Attributes: "Fire",
Owner: ParentObject,
Message: "from %o flames!",
Phase: phase
);
}
}
if( doEffect )
{
Buffer.Goto(C.X, C.Y);
string sColor = "&C";
int r = Rules.Stat.Random(1, 3);
if (r == 1) sColor = "&R";
if (r == 2) sColor = "&r";
if (r == 3) sColor = "&W";
r = Rules.Stat.Random(1, 3);
if (r == 1) sColor += "^R";
if (r == 2) sColor += "^r";
if (r == 3) sColor += "^W";
if( C.ParentZone == XRL.Core.XRLCore.Core.Game.ZoneManager.ActiveZone )
{
r = Rules.Stat.Random(1, 3);
Buffer.Write(sColor + (char)(219 + Rules.Stat.Random(0, 4)));
Popup._TextConsole.DrawBuffer(Buffer);
System.Threading.Thread.Sleep(10);
}
}
}
public static bool Cast(FlamingHands mutation = null, string level = "5-6")
{
if (mutation == null)
{
mutation = new FlamingHands();
mutation.Level = Rules.Stat.Roll(level);
mutation.ParentObject = XRL.Core.XRLCore.Core.Game.Player.Body;
}
ScreenBuffer Buffer = ScreenBuffer.GetScrapBuffer1(true);
Core.XRLCore.Core.RenderMapToBuffer(Buffer);
List<Cell> TargetCell = mutation.PickLine(9, AllowVis.Any, Snap: true, IgnoreLOS: true, Label:"Flaming Ray"); //TODO:TARGETLABEL
if (TargetCell == null || TargetCell.Count <= 0)
{
return false;
}
if (TargetCell.Count == 1 && mutation.ParentObject.IsPlayer())
{
if (UI.Popup.ShowYesNoCancel("Are you sure you want to target " + mutation.ParentObject.itself + "?") != DialogResult.Yes)
{
return false;
}
}
mutation.CooldownMyActivatedAbility(mutation.ActivatedAbilityID, Turns: 10);
mutation.UseEnergy(1000, "Physical Mutation Flaming Hands");
mutation.PlayWorldSound(mutation.Sound, combat: true);
for (int i = 0, j = Math.Min(TargetCell.Count, 10); i < j; i++)
{
if (TargetCell.Count == 1 || TargetCell[i] != mutation.ParentObject.CurrentCell)
{
mutation.Flame(TargetCell[i], Buffer);
}
if (i < j - 1 && TargetCell[i].IsSolidFor(Projectile: Projectile, Attacker: mutation.ParentObject))
{
break;
}
}
BodyPart part = mutation.GetRegisteredSlot(mutation.BodyPartType, false);
XDidY(mutation.ParentObject, "emit", extra: "a flaming ray" + (part != null ? " from " + mutation.ParentObject.its + " " + part.GetOrdinalName() : ""), terminalPunctuation: "!", ColorAsGoodFor: mutation.ParentObject);
return true;
}
public bool CheckObjectProperlyEquipped()
{
if( !CreateObject) return true;
return HasRegisteredSlot(BodyPartType) && GetRegisteredSlot(BodyPartType, false) != null;
}
public override bool FireEvent(Event E)
{
if (E.ID == "AttackerHit")
{
if (!CheckObjectProperlyEquipped())
{
return true;
}
GameObject Defender = E.GetGameObjectParameter("Defender");
if (Defender != null)
{
string Amount = GetHeatOnHitAmount(Level);
int MaxTemp = 400;
if ( (Rules.Stat.RollMax(Amount) > 0 && Defender.pPhysics.Temperature < MaxTemp) || (Rules.Stat.RollMax(Amount) < 0 && Defender.pPhysics.Temperature > MaxTemp))
{
Defender.TemperatureChange(Amount.RollCached(), Actor: E.GetGameObjectParameter("Attacker"), Phase: ParentObject.GetPhase());
}
}
}
if (E.ID == "AIGetOffensiveMutationList")
{
if (
CheckObjectProperlyEquipped()
&& E.GetIntParameter("Distance") <= 9
&& IsMyActivatedAbilityAIUsable(ActivatedAbilityID)
&& ParentObject.HasLOSTo(E.GetGameObjectParameter("Target"), UseTargetability: true)
)
{
E.AddAICommand("CommandFlamingHands");
}
}
else
if (E.ID == "CommandFlamingHands")
{
if (!CheckObjectProperlyEquipped())
{
if (ParentObject.IsPlayer())
{
UI.Popup.ShowFail("Your " + BodyPartType + " is too damaged to do that!");
}
return false;
}
if (!Cast(this))
{
return false;
}
}
return base.FireEvent(E);
}
private void AddAbility()
{
ActivatedAbilityID = AddMyActivatedAbility(
Name: "Flaming Ray",
Command: "CommandFlamingHands",
Class: "Physical Mutation",
Icon: "" + (char) 168,
Description: GetLevelText(Level)
);
}
public override bool ChangeLevel(int NewLevel)
{
var result = base.ChangeLevel(NewLevel);
// Update the ability description
if (MyActivatedAbility(ActivatedAbilityID) is ActivatedAbilityEntry ability) ability.Description = GetLevelText(Level);
return result;
}
[NonSerialized]
private static List<string> variants = new List<string> { "Hands", "Face", "Feet" };
public override List<string> GetVariants()
{
return variants;
}
public override void SetVariant(int n)
{
if( n < variants.Count )
{
BodyPartType = variants[n];
}
else
{
BodyPartType = variants[0];
}
base.SetVariant(n);
}
public void MakeFlaming( BodyPart part )
{
if( part == null ) return;
if( part.DefaultBehavior != null && part.DefaultBehavior.Blueprint != "Ghostly Flames" && !part.DefaultBehavior.pRender.DisplayName.Contains("{{fiery|flaming}}"))
{
part.DefaultBehavior.pRender.DisplayName = "{{fiery|flaming}} " + part.DefaultBehavior.pRender.DisplayName;
}
if( part.Parts != null )
{
for (int x = 0; x < part.Parts.Count; x++)
{
MakeFlaming(part.Parts[x]);
}
}
}
public override void OnDecorateDefaultEquipment(Body body)
{
if (CreateObject)
{
BodyPart part;
if (!HasRegisteredSlot(BodyPartType))
{
part = body.GetFirstPart(BodyPartType);
if (part != null)
{
RegisterSlot(BodyPartType, part);
}
else
{
;
}
}
else
{
part = GetRegisteredSlot(BodyPartType, false);
}
if (part != null && part.DefaultBehavior == null)
{
var FlamesObject = GameObject.create("Ghostly Flames");
FlamesObject.GetPart<Armor>().WornOn = BodyPartType;
part.DefaultBehavior = FlamesObject;
}
MakeFlaming(part);
if (BodyPartType == "Hands")
{
foreach (var hand in body.GetParts() )
{
if( hand.Type == "Hand") MakeFlaming(hand);
}
}
}
base.OnDecorateDefaultEquipment(body);
}
public override bool Mutate(GameObject GO, int Level)
{
AddAbility();
return base.Mutate(GO, Level);
}
public override bool Unmutate(GameObject GO)
{
RemoveMyActivatedAbility(ref ActivatedAbilityID);
return base.Unmutate(GO);
}
}
}
|