Skip to content

Commit

Permalink
Porting ExampleCritter and related (+ a bugfix somehow) (#3752)
Browse files Browse the repository at this point in the history
* 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
Kogsey and JavidPack committed Sep 18, 2023
1 parent 08f5545 commit 2ab44f8
Show file tree
Hide file tree
Showing 21 changed files with 349 additions and 314 deletions.
7 changes: 4 additions & 3 deletions ExampleMod/Content/ExampleRecipes.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ExampleMod.Common;
using ExampleMod.Content.NPCs;
using Terraria;
using Terraria.ID;
using Terraria.Localization;
Expand All @@ -19,15 +20,15 @@ public class ExampleRecipes : ModSystem

public override void AddRecipeGroups() {
// Create a recipe group and store it
// Language.GetTextValue("LegacyMisc.37") is the word "Any" in english, and the corresponding word in other languages
// Language.GetTextValue("LegacyMisc.37") is the word "Any" in English, and the corresponding word in other languages
ExampleRecipeGroup = new RecipeGroup(() => $"{Language.GetTextValue("LegacyMisc.37")} {Lang.GetItemNameValue(ModContent.ItemType<Items.ExampleItem>())}",
ModContent.ItemType<Items.ExampleItem>(), ModContent.ItemType<Items.ExampleDataItem>());

// To avoid name collisions, when a modded items is the iconic or 1st item in a recipe group, name the recipe group: ModName:ItemName
RecipeGroup.RegisterGroup("ExampleMod:ExampleItem", ExampleRecipeGroup);

// Add an item to an existing Terraria recipeGroup
//RecipeGroup.recipeGroups[RecipeGroupID.Snails].ValidItems.Add(ModContent.ItemType<Items.ExampleCritter>());
// Add an item to an existing Terraria recipeGroup. ExampleCritterItem isn't gold but it serves as an example for this.
RecipeGroup.recipeGroups[RecipeGroupID.GoldenCritter].ValidItems.Add(ModContent.ItemType<ExampleCritterItem>());

// While an "IronBar" group exists, "SilverBar" does not. tModLoader will merge recipe groups registered with the same name, so if you are registering a recipe group with a vanilla item as the 1st item, you can register it using just the internal item name if you anticipate other mods wanting to use this recipe group for the same concept. By doing this, multiple mods can add to the same group without extra effort. In this case we are adding a SilverBar group. Don't store the RecipeGroup instance, it might not be used, use the same nameof(ItemID.ItemName) or RecipeGroupID returned from RegisterGroup when using Recipe.AddRecipeGroup instead.
RecipeGroup SilverBarRecipeGroup = new RecipeGroup(() => $"{Language.GetTextValue("LegacyMisc.37")} {Lang.GetItemNameValue(ItemID.SilverBar)}",
Expand Down
224 changes: 224 additions & 0 deletions ExampleMod/Content/NPCs/ExampleCritter.cs
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;
}
}
}
Binary file added ExampleMod/Content/NPCs/ExampleCritterItem.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ExampleMod/Content/NPCs/ExampleCritterNPC.png
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.
60 changes: 60 additions & 0 deletions ExampleMod/Content/Tiles/ExampleCritterCage.cs
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();
}
}
}
Binary file added ExampleMod/Content/Tiles/ExampleCritterCage.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ExampleMod/Content/Tiles/ExampleCritterCageItem.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions ExampleMod/Localization/en-US.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Mods: {

MinionBossMinion.DisplayName: Minion Boss Minion
PartyZombie.DisplayName: Party Zombie
ExampleCritterNPC.DisplayName: Lava Frog
}

Tiles: {
Expand Down Expand Up @@ -870,6 +871,16 @@ Mods: {
DisplayName: Example Swinging Energy Sword
Tooltip: ""
}

ExampleCritterItem: {
DisplayName: Lava Frog
Tooltip: A spicy boi
}

ExampleCritterCageItem: {
DisplayName: Lava Frog Cage
Tooltip: A sad spicy boi
}
}

# Projectile display names mainly show in chat when a player dies from it.
Expand Down
11 changes: 11 additions & 0 deletions ExampleMod/Localization/ru-RU.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Mods: {

// MinionBossMinion.DisplayName: Minion Boss Minion
// PartyZombie.DisplayName: Party Zombie
// ExampleCritterNPC.DisplayName: Lava Frog
}

Tiles: {
Expand Down Expand Up @@ -870,6 +871,16 @@ Mods: {
// DisplayName: Example Swinging Energy Sword
// Tooltip: ""
}

ExampleCritterItem: {
// DisplayName: Lava Frog
// Tooltip: A spicy boi
}

ExampleCritterCageItem: {
// DisplayName: Lava Frog Cage
// Tooltip: A sad spicy boi
}
}

# Projectile display names mainly show in chat when a player dies from it.
Expand Down
11 changes: 11 additions & 0 deletions ExampleMod/Localization/zh-Hans.hjson
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Mods: {

// MinionBossMinion.DisplayName: Minion Boss Minion
// PartyZombie.DisplayName: Party Zombie
// ExampleCritterNPC.DisplayName: Lava Frog
}

Tiles: {
Expand Down Expand Up @@ -870,6 +871,16 @@ Mods: {
// DisplayName: Example Swinging Energy Sword
// Tooltip: ""
}

ExampleCritterItem: {
// DisplayName: Lava Frog
// Tooltip: A spicy boi
}

ExampleCritterCageItem: {
// DisplayName: Lava Frog Cage
// Tooltip: A sad spicy boi
}
}

# Projectile display names mainly show in chat when a player dies from it.
Expand Down

0 comments on commit 2ab44f8

Please sign in to comment.