Stardew Valley - Nature Strikes Back
Click here for the mod on Nexus Mods ;
Here for the code on GitHub ;
Or here for the YouTube video .
To make this mod we need Harmony. We use the prefix and postfix patching functionality.
There are two different types of classes we patch: TerrainFeatures and the Tool class. The TerrainFeatures contain objects like the Tree, FruitTree, etc.
A patch class we make for these looks as follows:
[HarmonyPatch(typeof(Tree), "performToolAction")]
public class Tree_performToolAction_Patch
{
private static float treeHealth;
public static void Prefix(Tree __instance)
{
ModEntry.GMonitor.Log($"------------------------------------------------", LogLevel.Debug);
treeHealth = (float)__instance.health;
}
public static void Postfix(Tree __instance, Tool t, int explosion, Vector2 tileLocation, GameLocation location, ref bool __result)
{
ModEntry.GMonitor.Log($"Holding {Game1.player.CurrentTool}", LogLevel.Debug);
var newHealth = (float)__instance.health > 0f ? (float)__instance.health : 0f;
var healthDiff = treeHealth > newHealth ? treeHealth - newHealth : newHealth - treeHealth;
healthDiff = __instance.growthStage <= 3 ? 2f : healthDiff;
ModEntry.GMonitor.Log($"Health before: {treeHealth}, Health after: {(float)__instance.health}, Health diff: {healthDiff}.", LogLevel.Debug);
var tile = t.InitialParentTileIndex;
if (Game1.player.CurrentTool is MeleeWeapon)
{
tile = 189;
}
if (healthDiff > 0f)
{
Game1.player.health -= (int)healthDiff;
var facingDirection = Game1.player.FacingDirection;
switch (facingDirection)
{
// Facing up
case 0:
// Show tool swinging down
break;
// Facing right
case 1:
// Show tool swinging left
tile += 2;
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite("TileSheets\\tools", new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width, tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0, Game1.player.Position + new Vector2(48f, -150f), flicker: false, flipped: true, 1f, 0f, Color.White, 4f, 0f, 0f, 0f)
{
delayBeforeAnimationStart = 150
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite("TileSheets\\tools", new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width, tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0, Game1.player.Position + new Vector2(30f, -148f), flicker: false, flipped: true, 1f, 0f, Color.White, 4f, 0f, 6f, 0f)
{
delayBeforeAnimationStart = 180
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite("TileSheets\\tools", new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width, tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0, Game1.player.Position + new Vector2(4f, -132f), flicker: false, flipped: true, 1f, 0f, Color.White, 4f, 0f, 5.5f, 0f)
{
delayBeforeAnimationStart = 210
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite("TileSheets\\tools", new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width, tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0, Game1.player.Position + new Vector2(-10f, -106f), flicker: false, flipped: true, 1f, 0f, Color.White, 4f, 0f, 5f, 0f)
{
delayBeforeAnimationStart = 240
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite("TileSheets\\tools", new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width, tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 200f, 1, 0, Game1.player.Position + new Vector2(-10f, -76f), flicker: false, flipped: true, 1f, 0f, Color.White, 4f, 0f, 4.5f, 0f)
{
delayBeforeAnimationStart = 270
});
break;
// Facing down
case 2:
// Don't show tool swinging up, item is in front of it
break;
// Facing left
case 3:
// Don't show tool swinging down, farmer is in front of it
tile += 2;
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite("TileSheets\\tools", new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width, tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0, Game1.player.Position + new Vector2(-48f, -150f), flicker: false, flipped: false, 1f, 0f, Color.White, 4f, 0f, 0f, 0f)
{
delayBeforeAnimationStart = 150
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite("TileSheets\\tools", new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width, tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0, Game1.player.Position + new Vector2(-30f, -148f), flicker: false, flipped: false, 1f, 0f, Color.White, 4f, 0f, -6f, 0f)
{
delayBeforeAnimationStart = 180
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite("TileSheets\\tools", new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width, tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0, Game1.player.Position + new Vector2(-4f, -132f), flicker: false, flipped: false, 1f, 0f, Color.White, 4f, 0f, -5.5f, 0f)
{
delayBeforeAnimationStart = 210
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite("TileSheets\\tools", new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width, tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0, Game1.player.Position + new Vector2(10f, -106f), flicker: false, flipped: false, 1f, 0f, Color.White, 4f, 0f, -5f, 0f)
{
delayBeforeAnimationStart = 240
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite("TileSheets\\tools", new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width, tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 200f, 1, 0, Game1.player.Position + new Vector2(10f, -76f), flicker: false, flipped: false, 1f, 0f, Color.White, 4f, 0f, -4.5f, 0f)
{
delayBeforeAnimationStart = 270
});
break;
default:
ModEntry.GMonitor.Log($"Unknown farmer facing direction: {facingDirection}.");
break;
}
}
}
}
If the farmer did damage to the Tree, or the Tree growthstage is <= 3 we damage the farmer. The damage that the farmer takes is equal to the damage they did to the tree.
Then, if the farmer hits the tree from the left or right we show an animation that looks like the tree is hitting the farmer back. If the farmer hits the tree from the top or bottom we do not show this animation, since the tree or farmer would block (almost) all of it.
To apply this functionality to other terrain features we simply copy the code and change the object type (e.g. Tree -> FruitTree).
To make SMAPI actually patch the code we our Entry in ModEntry looks as follows:
public override void Entry(IModHelper helper)
{
GMonitor = this.Monitor;
var harmony = new Harmony(this.ModManifest.UniqueID);
harmony.PatchAll();
}
We also patch the DoFunction from the Tool file. We need to do this since objects like weeds, rocks, etc. do not have their own files. The code for this looks as follows:
[HarmonyPatch(typeof(Tool), "DoFunction")]
public class Tool_DoFunction_Patch
{
public static void Prefix(Tool __instance, GameLocation location, int x, int y, int power, Farmer who)
{
Utility.clampToTile(new Vector2(x, y));
int tileX = x / 64;
int tileY = y / 64;
Vector2 tileLocation = new Vector2(tileX, tileY);
StardewValley.Object o = null;
location.Objects.TryGetValue(tileLocation, out o);
if (o != null)
{
ModEntry.GMonitor.Log($"Object name: {o.Name}", LogLevel.Debug);
}
if (o != null && (o.Name.Equals("Stone") || o.Name.Contains("Boulder") || o.Name.Contains("Weed") || o.Name.Contains("Twig") || o.Name.Equals("Stick")))
{
ModEntry.GMonitor.Log($"Holding {Game1.player.CurrentTool}", LogLevel.Debug);
var healthDiff = 2f;
ModEntry.GMonitor.Log($"Health diff: {healthDiff}.", LogLevel.Debug);
var tile = __instance.InitialParentTileIndex;
if (Game1.player.CurrentTool is MeleeWeapon)
{
tile = 189;
}
if (healthDiff > 0f)
{
Game1.player.health -= (int)healthDiff;
var facingDirection = Game1.player.FacingDirection;
switch (facingDirection)
{
// Facing up
case 0:
// Show tool swinging down
break;
// Facing right
case 1:
// Show tool swinging left
tile += 2;
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite(
"TileSheets\\tools",
new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width,
tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0,
Game1.player.Position + new Vector2(48f, -150f), flicker: false, flipped: true, 1f,
0f, Color.White, 4f, 0f, 0f, 0f)
{
delayBeforeAnimationStart = 150
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite(
"TileSheets\\tools",
new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width,
tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0,
Game1.player.Position + new Vector2(30f, -148f), flicker: false, flipped: true, 1f,
0f, Color.White, 4f, 0f, 6f, 0f)
{
delayBeforeAnimationStart = 180
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite(
"TileSheets\\tools",
new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width,
tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0,
Game1.player.Position + new Vector2(4f, -132f), flicker: false, flipped: true, 1f,
0f, Color.White, 4f, 0f, 5.5f, 0f)
{
delayBeforeAnimationStart = 210
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite(
"TileSheets\\tools",
new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width,
tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0,
Game1.player.Position + new Vector2(-10f, -106f), flicker: false, flipped: true, 1f,
0f, Color.White, 4f, 0f, 5f, 0f)
{
delayBeforeAnimationStart = 240
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite(
"TileSheets\\tools",
new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width,
tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 200f, 1, 0,
Game1.player.Position + new Vector2(-10f, -76f), flicker: false, flipped: true, 1f,
0f, Color.White, 4f, 0f, 4.5f, 0f)
{
delayBeforeAnimationStart = 270
});
break;
// Facing down
case 2:
// Don't show tool swinging up, item is in front of it
break;
// Facing left
case 3:
// Don't show tool swinging down, farmer is in front of it
tile += 2;
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite(
"TileSheets\\tools",
new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width,
tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0,
Game1.player.Position + new Vector2(-48f, -150f), flicker: false, flipped: false,
1f, 0f, Color.White, 4f, 0f, 0f, 0f)
{
delayBeforeAnimationStart = 150
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite(
"TileSheets\\tools",
new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width,
tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0,
Game1.player.Position + new Vector2(-30f, -148f), flicker: false, flipped: false,
1f, 0f, Color.White, 4f, 0f, -6f, 0f)
{
delayBeforeAnimationStart = 180
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite(
"TileSheets\\tools",
new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width,
tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0,
Game1.player.Position + new Vector2(-4f, -132f), flicker: false, flipped: false, 1f,
0f, Color.White, 4f, 0f, -5.5f, 0f)
{
delayBeforeAnimationStart = 210
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite(
"TileSheets\\tools",
new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width,
tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 30f, 1, 0,
Game1.player.Position + new Vector2(10f, -106f), flicker: false, flipped: false, 1f,
0f, Color.White, 4f, 0f, -5f, 0f)
{
delayBeforeAnimationStart = 240
});
Game1.currentLocation.temporarySprites.Add(new TemporaryAnimatedSprite(
"TileSheets\\tools",
new Microsoft.Xna.Framework.Rectangle(tile * 16 % Game1.toolSpriteSheet.Width,
tile * 16 / Game1.toolSpriteSheet.Width * 16, 16, 32), 200f, 1, 0,
Game1.player.Position + new Vector2(10f, -76f), flicker: false, flipped: false, 1f,
0f, Color.White, 4f, 0f, -4.5f, 0f)
{
delayBeforeAnimationStart = 270
});
break;
default:
ModEntry.GMonitor.Log($"Unknown farmer facing direction: {facingDirection}.");
break;
}
}
}
}
}
Every time the DoFunction function gets called we check if the object on which the tool is used is a stone, boulder, weed, twig, or stick. If this is the case we make it hit back, otherwise we do nothing.