-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Porting ExampleCritter and related (+ a bugfix somehow) (#3752)
* Updated ExampleCritter stuff NPC changed to be a frog because "Lava Snail" is too similar to "Magma Snail" added in 1.4, lava snail sprites left * Finalising Made some functions in TileDrawing public static for modder use, Changed sprites to look lava-ey, Fixed namespaces, Changed IL to load in Load(), Added localisation names, Fixed a big involving critter friendly being true (again) * Cleanup - Updated comments - Frog lava/water behavior fixed - Updated recipe group example * Various fixes, style fixes * Delete old textures, unused localization --------- Co-authored-by: JavidPack <javidpack@gmail.com>
- Loading branch information
Showing
21 changed files
with
349 additions
and
314 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
using Microsoft.Xna.Framework; | ||
using MonoMod.Cil; | ||
using System; | ||
using Terraria; | ||
using Terraria.Audio; | ||
using Terraria.GameContent.Bestiary; | ||
using Terraria.ID; | ||
using Terraria.ModLoader; | ||
using Terraria.ModLoader.Utilities; | ||
|
||
namespace ExampleMod.Content.NPCs | ||
{ | ||
/// <summary> | ||
/// This file shows off a critter npc. The unique thing about critters is how you can catch them with a bug net. | ||
/// The important bits are: Main.npcCatchable, NPC.catchItem, and Item.makeNPC. | ||
/// We will also show off adding an item to an existing RecipeGroup (see ExampleRecipes.AddRecipeGroups). | ||
/// Additionally, this example shows an involved IL edit. | ||
/// </summary> | ||
public class ExampleCritterNPC : ModNPC | ||
{ | ||
private const int ClonedNPCID = NPCID.Frog; // Easy to change type for your modder convenience | ||
|
||
public override void Load() { | ||
IL_Wiring.HitWireSingle += HookFrogStatue; | ||
} | ||
|
||
/// <summary> | ||
/// Change the following code sequence in Wiring.HitWireSingle | ||
/// <code> | ||
///case 61: | ||
///num115 = 361; | ||
/// </code> | ||
/// to | ||
/// <code> | ||
///case 61: | ||
///num115 = Main.rand.NextBool() ? 361 : NPC.type | ||
/// </code> | ||
/// This causes the frog statue to spawn this NPC 50% of the time | ||
/// </summary> | ||
/// <param name="ilContext"> </param> | ||
private void HookFrogStatue(ILContext ilContext) { | ||
try { | ||
// Obtain a cursor positioned before the first instruction of the method the cursor is used for navigating and modifying the il | ||
ILCursor ilCursor = new ILCursor(ilContext); | ||
|
||
// The exact location for this hook is very complex to search for due to the hook instructions not being unique and buried deep in control flow. Switch statements are sometimes compiled to if-else chains, and debug builds litter the code with no-ops and redundant locals. | ||
// In general you want to search using structure and function rather than numerical constants which may change across different versions or compile settings. Using local variable indices is almost always a bad idea. | ||
// We can search for | ||
// switch (*) | ||
// case 61: | ||
// num115 = 361; | ||
|
||
// In general you'd want to look for a specific switch variable, or perhaps the containing switch (type) { case 105: but the generated IL is really variable and hard to match in this case. | ||
// We'll just use the fact that there are no other switch statements with case 61 | ||
|
||
ILLabel[] targets = null; | ||
while (ilCursor.TryGotoNext(i => i.MatchSwitch(out targets))) { | ||
// Some optimizing compilers generate a sub so that all the switch cases start at 0: | ||
// ldc.i4.s 30 | ||
// sub | ||
// switch | ||
int offset = 0; | ||
if (ilCursor.Prev.MatchSub() && ilCursor.Prev.Previous.MatchLdcI4(out offset)) { | ||
; | ||
} | ||
|
||
// Get the label for case 61: if it exists | ||
int case61Index = 61 - offset; | ||
if (case61Index < 0 || case61Index >= targets.Length || targets[case61Index] is not ILLabel target) { | ||
continue; | ||
} | ||
|
||
// Move the cursor to case 61: | ||
ilCursor.GotoLabel(target); | ||
// Move the cursor after 361 is pushed onto the stack | ||
ilCursor.Index++; | ||
// There are lots of extra checks we could add here to make sure we're at the right spot, such as not encountering any branching instructions | ||
|
||
// Now we add additional code to modify the current value that will be assigned to num115 | ||
ilCursor.EmitDelegate((int originalAssign) => Main.rand.NextBool() ? originalAssign : NPC.type); | ||
|
||
// Hook applied successfully | ||
return; | ||
} | ||
|
||
// Couldn't find the right place to insert. | ||
throw new Exception("Hook location not found, switch(*) { case 61: ..."); | ||
} | ||
catch { | ||
// If there are any failures with the IL editing, this method will dump the IL to Logs/ILDumps/{Mod Name}/{Method Name}.txt | ||
MonoModHooks.DumpIL(ModContent.GetInstance<ExampleMod>(), ilContext); | ||
} | ||
} | ||
|
||
public override void SetStaticDefaults() { | ||
Main.npcFrameCount[Type] = Main.npcFrameCount[ClonedNPCID]; // Copy animation frames | ||
Main.npcCatchable[Type] = true; // This is for certain release situations | ||
|
||
// These three are typical critter values | ||
NPCID.Sets.CountsAsCritter[Type] = true; | ||
NPCID.Sets.TakesDamageFromHostilesWithoutBeingFriendly[Type] = true; | ||
NPCID.Sets.TownCritter[Type] = true; | ||
|
||
// The frog is immune to confused | ||
NPCID.Sets.SpecificDebuffImmunity[Type][BuffID.Confused] = true; | ||
|
||
// This is so it appears between the frog and the gold frog | ||
NPCID.Sets.NormalGoldCritterBestiaryPriority.Insert(NPCID.Sets.NormalGoldCritterBestiaryPriority.IndexOf(ClonedNPCID) + 1, Type); | ||
} | ||
|
||
public override void SetDefaults() { | ||
// width = 12; | ||
// height = 10; | ||
// aiStyle = 7; | ||
// damage = 0; | ||
// defense = 0; | ||
// lifeMax = 5; | ||
// HitSound = SoundID.NPCHit1; | ||
// DeathSound = SoundID.NPCDeath1; | ||
// catchItem = 2121; | ||
// Sets the above | ||
NPC.CloneDefaults(ClonedNPCID); | ||
|
||
NPC.catchItem = ModContent.ItemType<ExampleCritterItem>(); | ||
NPC.lavaImmune = true; | ||
AIType = ClonedNPCID; | ||
AnimationType = ClonedNPCID; | ||
} | ||
|
||
public override void SetBestiary(BestiaryDatabase database, BestiaryEntry bestiaryEntry) { | ||
bestiaryEntry.AddTags(BestiaryDatabaseNPCsPopulator.CommonTags.SpawnConditions.Biomes.TheUnderworld, | ||
new FlavorTextBestiaryInfoElement("The most adorable goodest spicy child. Do not dare be mean to him!")); | ||
} | ||
|
||
public override float SpawnChance(NPCSpawnInfo spawnInfo) { | ||
return SpawnCondition.Underworld.Chance * 0.1f; | ||
} | ||
|
||
public override void HitEffect(NPC.HitInfo hit) { | ||
if (NPC.life <= 0) { | ||
for (int i = 0; i < 6; i++) { | ||
Dust dust = Dust.NewDustDirect(NPC.position, NPC.width, NPC.height, DustID.Worm, 2 * hit.HitDirection, -2f); | ||
if (Main.rand.NextBool(2)) { | ||
dust.noGravity = true; | ||
dust.scale = 1.2f * NPC.scale; | ||
} | ||
else { | ||
dust.scale = 0.7f * NPC.scale; | ||
} | ||
} | ||
Gore.NewGore(NPC.GetSource_Death(), NPC.position, NPC.velocity, Mod.Find<ModGore>($"{Name}_Gore_Head").Type, NPC.scale); | ||
Gore.NewGore(NPC.GetSource_Death(), NPC.position, NPC.velocity, Mod.Find<ModGore>($"{Name}_Gore_Leg").Type, NPC.scale); | ||
} | ||
} | ||
|
||
public override Color? GetAlpha(Color drawColor) { | ||
// GetAlpha gives our Lava Frog a red glow. | ||
return drawColor with { | ||
R = 255, | ||
// Both these do the same in this situation, using these methods is useful. | ||
G = Utils.Clamp<byte>(drawColor.G, 175, 255), | ||
B = Math.Min(drawColor.B, (byte)75), | ||
A = 255 | ||
}; | ||
} | ||
|
||
public override bool PreAI() { | ||
// Kills the NPC if it hits water, honey or shimmer | ||
if (NPC.wet && !Collision.LavaCollision(NPC.position, NPC.width, NPC.height)) { // NPC.lavawet not 100% accurate for the frog | ||
// These 3 lines instantly kill the npc without showing damage numbers, dropping loot, or playing DeathSound. Use this for instant deaths | ||
NPC.life = 0; | ||
NPC.HitEffect(); | ||
NPC.active = false; | ||
SoundEngine.PlaySound(SoundID.NPCDeath16, NPC.position); // plays a fizzle sound | ||
} | ||
|
||
return true; | ||
} | ||
|
||
public override void OnCaughtBy(Player player, Item item, bool failed) { | ||
if (failed) { | ||
return; | ||
} | ||
|
||
Point npcTile = NPC.Center.ToTileCoordinates(); | ||
|
||
if (!WorldGen.SolidTile(npcTile.X, npcTile.Y)) { // Check if the tile the npc resides the most in is non solid | ||
Tile tile = Main.tile[npcTile]; | ||
tile.LiquidAmount = tile.LiquidType == LiquidID.Lava ? // Check if the tile has lava in it | ||
Math.Max((byte)Main.rand.Next(50, 150), tile.LiquidAmount) // If it does, then top up the amount | ||
: (byte)Main.rand.Next(50, 150); // If it doesn't, then overwrite the amount. Technically this distinction should never be needed bc it will burn but to be safe it's here | ||
tile.LiquidType = LiquidID.Lava; // Set the liquid type to lava | ||
WorldGen.SquareTileFrame(npcTile.X, npcTile.Y, true); // Update the surrounding area in the tilemap | ||
} | ||
} | ||
} | ||
|
||
public class ExampleCritterItem : ModItem | ||
{ | ||
public override void SetStaticDefaults() { | ||
ItemID.Sets.IsLavaBait[Type] = true; // While this item is not bait, this will require a lava bug net to catch. | ||
} | ||
|
||
public override void SetDefaults() { | ||
// useStyle = 1; | ||
// autoReuse = true; | ||
// useTurn = true; | ||
// useAnimation = 15; | ||
// useTime = 10; | ||
// maxStack = CommonMaxStack; | ||
// consumable = true; | ||
// width = 12; | ||
// height = 12; | ||
// makeNPC = 361; | ||
// noUseGraphic = true; | ||
|
||
// Cloning ItemID.Frog sets the preceding values | ||
Item.CloneDefaults(ItemID.Frog); | ||
Item.makeNPC = ModContent.NPCType<ExampleCritterNPC>(); | ||
Item.value += Item.buyPrice(0, 0, 30, 0); // Make this critter worth slightly more than the frog | ||
Item.rare = ItemRarityID.Blue; | ||
} | ||
} | ||
} |
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
using Microsoft.Xna.Framework; | ||
using Terraria; | ||
using Terraria.GameContent.Drawing; | ||
using Terraria.ID; | ||
using Terraria.ModLoader; | ||
using Terraria.ObjectData; | ||
|
||
namespace ExampleMod.Content.Tiles | ||
{ | ||
public class ExampleCritterCage : ModTile | ||
{ | ||
public override void SetStaticDefaults() { | ||
// Here we just copy a bunch of values from the frog cage tile | ||
TileID.Sets.CritterCageLidStyle[Type] = TileID.Sets.CritterCageLidStyle[TileID.FrogCage]; // This is how vanilla draws the roof of the cage | ||
Main.tileFrameImportant[Type] = Main.tileFrameImportant[TileID.FrogCage]; | ||
Main.tileLavaDeath[Type] = Main.tileLavaDeath[TileID.FrogCage]; | ||
Main.tileSolidTop[Type] = Main.tileSolidTop[TileID.FrogCage]; | ||
Main.tileTable[Type] = Main.tileTable[TileID.FrogCage]; | ||
AdjTiles = new int[] { TileID.FrogCage, TileID.GoldFrogCage }; // Just in case another mod uses the frog cage to craft | ||
AnimationFrameHeight = 36; | ||
|
||
// We can copy the TileObjectData directly from an existing tile to copy changes, if any, made to the TileObjectData template the original tile copied from. | ||
// In this case, the original FrogCage tile is an exact copy of TileObjectData.StyleSmallCage, so either approach works here. | ||
TileObjectData.newTile.CopyFrom(TileObjectData.GetTileData(TileID.FrogCage, 0)); | ||
// or TileObjectData.newTile.CopyFrom(TileObjectData.StyleSmallCage); | ||
TileObjectData.addTile(Type); | ||
|
||
// Since this tile is only used for a single item, we can reuse the item localization for the map entry. | ||
AddMapEntry(new Color(122, 217, 232), ModContent.GetInstance<ExampleCritterCageItem>().DisplayName); | ||
} | ||
|
||
public override void SetDrawPositions(int i, int j, ref int width, ref int offsetY, ref int height, ref short tileFrameX, ref short tileFrameY) { | ||
offsetY = 2; // From vanilla | ||
Main.critterCage = true; // Vanilla doesn't run the animation code for critters unless this is checked | ||
} | ||
|
||
public override void AnimateIndividualTile(int type, int i, int j, ref int frameXOffset, ref int frameYOffset) { | ||
Tile tile = Main.tile[i, j]; | ||
// The GetSmallAnimalCageFrame method utilizes some math to stagger each individual tile. First the top left tile is found, then those coordinates are passed into some math to stagger an index into Main.snail2CageFrame | ||
// Main.frogCageFrame is used since we want the same animation, but if we wanted a different frame count or a different animation timing, we could write our own by adapting vanilla code and placing the code in AnimateTile | ||
int tileCageFrameIndex = TileDrawing.GetSmallAnimalCageFrame(i, j, tile.TileFrameX, tile.TileFrameY); | ||
frameYOffset = Main.frogCageFrame[tileCageFrameIndex] * AnimationFrameHeight; | ||
} | ||
} | ||
|
||
public class ExampleCritterCageItem : ModItem | ||
{ | ||
public override void SetDefaults() { | ||
Item.CloneDefaults(ItemID.FrogCage); | ||
Item.createTile = ModContent.TileType<ExampleCritterCage>(); | ||
} | ||
|
||
public override void AddRecipes() { | ||
CreateRecipe() | ||
.AddIngredient(ItemID.Terrarium) | ||
.AddIngredient(ModContent.ItemType<NPCs.ExampleCritterItem>()) | ||
.Register(); | ||
} | ||
} | ||
} |
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.