User:MomBun/Sandbox

From Caves of Qud Wiki
Jump to navigation Jump to search
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 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);
        }

    }

}