User:MomBun/Sandbox: Difference between revisions

From Caves of Qud Wiki
Jump to navigation Jump to search
(adding skeletal example with comments (Thank you Gnarf!))
No edit summary
Line 7: Line 7:
<mutations>
<mutations>
   <category Name="[CATEGORY]">
   <category Name="[CATEGORY]">
     <mutation Name="[NAME]" Cost="[NUMBER]" MaxSelected="[NUMBER]" Class="[C# ID]" Tile="Mutations/"YOUR_IMAGE_HERE"></mutation>
     <mutation Name="[NAME]" Cost="[NUMBER]" MaxSelected="[NUMBER]" Class="[CS CLASS]" Tile="Mutations/YOUR_IMAGE_HERE" Foreground="COLOR" Background="COLOR" </mutation>
  </category>
  </category>
</mutations>
</mutations>
Line 39: Line 39:
The tile you will be using for said mutation in-game.
The tile you will be using for said mutation in-game.


By default most mutations use gold/yellow for detail and brown for foreground but this can be customized for any color of your choice, see [[Modding:Tiles]] and [[Modding:Colors_%26_Object_Rendering]] for furtherr info on this.
By default most mutations use gold/yellow for detail and brown for foreground but this can be customized for any color of your choice, see [[Modding:Tiles]] and [[Modding:Colors_%26_Object_Rendering]] for further info on this.


=== Constructor ===
=== Constructor ===

Revision as of 03:09, 3 August 2022

This page is about modding. See the modding overview for an abstract on modding.
This page is about modding. See the modding overview for an abstract on modding.
For best results, it's recommended to have read the following topics before this one:

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 detail 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.

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.

Here are an example of how this is constructed from both Mutations.XML and GasGeneration.cs

<mutation Name="Sleep Gas Generation" Cost="2" MaxSelected="1" Class="GasGeneration" Constructor="SleepGas" Exclusions="Corrosive Gas Generation" BearerDescription="those who expel sleep gas" Code="bu"></mutation>
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"

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.

C# Scripting, and Making The Mutation

Here is an example of a mutation that will add udders to your character, editing the title of your character "With Udders" and occasionally moo

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 TEST_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
        public override string GetLevelText(int Level)
        {
            return  "Occasionally you will moo";
        }

        // 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")
            {
                // 1 in 100 chance to get "You moo." printed to the message log.
                if (1.in100()) DidX("moo");
            }
            return base.FireEvent(E);
        }

        // This is called every time the mutation changes level, and can be used to change things like damage.
        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 PLAYER, int Level)
        {
            return true;
        }

        public override bool Unmutate(GameObject PLAYER)
        {
            return true;
        }
    }
}

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
{

    [Serializable]
    public class FlamingHands : BaseDefaultEquipmentMutation
    {

        public string BodyPartType = "Hands";
        public bool CreateObject = true;
        public string Sound = "Abilities/sfx_ability_mutation_flamingRay_attack";

        public Guid FlamingHandsActivatedAbilityID = Guid.Empty;

        [NonSerialized] private static GameObject _Projectile;
        private static GameObject Projectile
        {
            get
            {
                if (!GameObject.validate(ref _Projectile))
                {
                    _Projectile = GameObject.createUnmodified("ProjectileFlamingHands");
                }
                return _Projectile;
            }
        }

        public override bool GeneratesEquipment()
        {
            return true;
        }

        public FlamingHands()
        {
            DisplayName = "Flaming Ray";
        }

        public override void Register(GameObject Object)
        {
            Object.RegisterPartEvent(this, "AIGetOffensiveMutationList");
            Object.RegisterPartEvent(this, "AttackerHit");
            Object.RegisterPartEvent(this, "CommandFlamingHands");
            base.Register(Object);
        }

        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 UseLevel)
        {
            string Result = UseLevel + "d4";
            if (ParentObject != null)
            {
                int LimbCount = ParentObject.Body.GetPartCount(BodyPartType);
                if (LimbCount > 0)
                {
                    Result += "+" + LimbCount;
                }
            }
            else
            {
                Result += "+1";
            }
            return Result;
        }

        public string ComputeDamage()
        {
            return 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.FlamingHandsActivatedAbilityID, 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(FlamingHandsActivatedAbilityID)
                    && 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);
        }

        public override bool ChangeLevel(int NewLevel)
        {
            return base.ChangeLevel(NewLevel);
        }

        private void AddAbility()
        {
            FlamingHandsActivatedAbilityID = AddMyActivatedAbility(
                Name: "Flaming Ray",
                Command: "CommandFlamingHands",
                Class: "Physical Mutation",
                Icon: "" + (char) 168
            );
        }

        [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 override void OnRegenerateDefaultEquipment(Body body)
        {
            base.OnRegenerateDefaultEquipment(body);
        }

        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 FlamingHandsActivatedAbilityID);
            return base.Unmutate(GO);
        }

    }

}