diff --git a/DynamicReflections/DynamicReflections.cs b/DynamicReflections/DynamicReflections.cs index 35dff92..ee6df89 100644 --- a/DynamicReflections/DynamicReflections.cs +++ b/DynamicReflections/DynamicReflections.cs @@ -1,25 +1,30 @@ +using DynamicReflections.Framework.External.GenericModConfigMenu; +using DynamicReflections.Framework.Interfaces.Internal; +using DynamicReflections.Framework.Managers; +using DynamicReflections.Framework.Models; +using DynamicReflections.Framework.Models.Reflections; +using DynamicReflections.Framework.Models.Settings; +using DynamicReflections.Framework.Patches.Objects; +using DynamicReflections.Framework.Patches.SMAPI; +using DynamicReflections.Framework.Patches.Tiles; +using DynamicReflections.Framework.Patches.Tools; +using DynamicReflections.Framework.Utilities; using HarmonyLib; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; -using DynamicReflections.Framework.Models; using StardewValley; +using StardewValley.Buildings; +using StardewValley.Extensions; +using StardewValley.Locations; +using StardewValley.Menus; +using StardewValley.Objects; +using StardewValley.TerrainFeatures; using System; using System.Collections.Generic; using System.IO; -using DynamicReflections.Framework.Patches.SMAPI; -using DynamicReflections.Framework.Patches.Tiles; -using DynamicReflections.Framework.Patches.Tools; using System.Linq; -using DynamicReflections.Framework.Patches.Objects; -using DynamicReflections.Framework.Utilities; -using DynamicReflections.Framework.Managers; -using DynamicReflections.Framework.Models.Settings; using System.Text.Json; -using DynamicReflections.Framework.External.GenericModConfigMenu; -using StardewValley.Locations; -using StardewValley.Menus; -using DynamicReflections.Framework.Interfaces.Internal; namespace DynamicReflections { @@ -47,8 +52,11 @@ public class DynamicReflections : Mod // Water reflection variables internal static Dictionary npcToWaterReflectionPosition = new Dictionary(); - internal static readonly Dictionary waterTileCache = new(); internal static Vector2? waterReflectionPosition; + internal static readonly Dictionary waterTileCache = new Dictionary(); + internal static Vector2? waterReflectionPosition; internal static Vector2? waterReflectionTilePosition; + internal static readonly Dictionary> locationToWaterReflectionTerrainFeatures = new Dictionary>(); + internal static readonly Dictionary> locationToPuddleReflectionTerrainFeatures = new Dictionary>(); internal static bool shouldDrawWaterReflection; internal static bool isDrawingWaterReflection; internal static bool isFilteringWater; @@ -138,6 +146,9 @@ public override void Entry(IModHelper helper) helper.Events.GameLoop.DayStarted += OnDayStarted; helper.Events.GameLoop.DayEnding += OnDayEnding; helper.Events.GameLoop.GameLaunched += OnGameLaunched; + helper.Events.World.TerrainFeatureListChanged += OnTerrainFeatureListChanged; + helper.Events.World.LargeTerrainFeatureListChanged += OnLargeTerrainFeatureChanged; + helper.Events.World.BuildingListChanged += OnBuildingListChanged; } public override object GetApi() @@ -208,6 +219,17 @@ private void OnFurnitureListChanged(object sender, StardewModdingAPI.Events.Furn } } } + + // Handle cached reflections + foreach (var addedFurniture in e.Added) + { + HandleTerrainFeatureAddition(e.Location, new ReflectableFurniture(addedFurniture)); + } + + foreach (var removedFurniture in e.Removed) + { + HandleFurnitureRemoval(e.Location, removedFurniture); + } } private void OnWarped(object sender, StardewModdingAPI.Events.WarpedEventArgs e) @@ -319,17 +341,9 @@ private void OnUpdateTicked(object sender, StardewModdingAPI.Events.UpdateTicked // Hide the reflection if it will show up out of bounds on the map or not drawn on a water tile var waterReflectionPosition = DynamicReflections.waterReflectionTilePosition.Value; - for (int yOffset = -1; yOffset <= Math.Ceiling(playerOffset.Y); yOffset++) + if (IsTileReflective(waterReflectionPosition, (int)Math.Ceiling(playerOffset.Y))) { - var tilePosition = waterReflectionPosition + new Vector2(0, yOffset); - - if (IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X, (int)tilePosition.Y) is true - || IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X - 1, (int)tilePosition.Y) is true - || IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X + 1, (int)tilePosition.Y) is true) - { - DynamicReflections.shouldDrawWaterReflection = true; - break; - } + DynamicReflections.shouldDrawWaterReflection = true; } // Handle the wavy effect if enabled @@ -442,25 +456,16 @@ private void OnUpdateTicked(object sender, StardewModdingAPI.Events.UpdateTicked // Hide the reflection if it will show up out of bounds on the map // or not drawn on water tiles var waterReflectionPosition = npcPosition / 64f; - for (int yOffset = -1; yOffset <= Math.Ceiling(npcOffset.Y); yOffset++) + if (IsTileReflective(waterReflectionPosition, (int)Math.Ceiling(npcOffset.Y))) { - var tilePosition = waterReflectionPosition + new Vector2(0, yOffset); - - if (IsWaterReflectiveTile(location, (int)tilePosition.X - 1, (int)tilePosition.Y) is true - || IsWaterReflectiveTile(location, (int)tilePosition.X + 1, (int)tilePosition.Y) is true) + npcToWaterReflectionPosition[npc] = npcPosition; + if (isCompanion) { - npcToWaterReflectionPosition[npc] = npcPosition; - - if (isCompanion) - { - companionCount++; - } - else - { - npcCount++; - } - - break; + companionCount++; + } + else + { + npcCount++; } } } @@ -653,9 +658,9 @@ private void OnGameLaunched(object sender, StardewModdingAPI.Events.GameLaunched if (isFreshInstall || isNewerVersion) { // Handle new version behavior - if (isFreshInstall || (isNewerVersion && lastInstalledVersion.IsOlderThan("3.1.0"))) + if (isFreshInstall || (isNewerVersion && lastInstalledVersion.IsOlderThan("3.2.0"))) { - // Reset the WaterReflectionSettings + // Reset the default WaterReflectionSettings modConfig.WaterReflectionSettings.Reset(); } if (isFreshInstall || (isNewerVersion && lastInstalledVersion.IsOlderThan("3.1.1"))) @@ -675,6 +680,93 @@ private void OnGameLaunched(object sender, StardewModdingAPI.Events.GameLaunched LoadRenderers(); } + private void OnTerrainFeatureListChanged(object sender, StardewModdingAPI.Events.TerrainFeatureListChangedEventArgs e) + { + foreach (var addedTerrainFeature in e.Added) + { + HandleTerrainFeatureAddition(e.Location, new ReflectableTerrain(addedTerrainFeature.Value)); + } + + foreach (var removedTerrainFeature in e.Removed) + { + HandleTerrainFeatureRemoval(e.Location, removedTerrainFeature.Value); + } + } + + private void OnLargeTerrainFeatureChanged(object sender, StardewModdingAPI.Events.LargeTerrainFeatureListChangedEventArgs e) + { + foreach (var addedTerrainFeature in e.Added) + { + HandleTerrainFeatureAddition(e.Location, new ReflectableTerrain(addedTerrainFeature)); + } + + foreach (var removedTerrainFeature in e.Removed) + { + HandleTerrainFeatureRemoval(e.Location, removedTerrainFeature); + } + } + + private void OnBuildingListChanged(object sender, StardewModdingAPI.Events.BuildingListChangedEventArgs e) + { + foreach (var addedBuilding in e.Added) + { + HandleTerrainFeatureAddition(e.Location, new ReflectableBuilding(addedBuilding)); + } + + if (e.Removed.Count() > 0) + { + ResetLocationTerrainCache(e.Location); + } + } + + private void HandleTerrainFeatureAddition(GameLocation location, ReflectableObject reflectableObject) + { + if (location is null || locationToWaterReflectionTerrainFeatures.ContainsKey(location) is false || locationToPuddleReflectionTerrainFeatures.ContainsKey(location) is false) + { + return; + } + + if (IsTileReflective(reflectableObject.Tile, 3)) + { + locationToWaterReflectionTerrainFeatures[location].Add(reflectableObject); + locationToWaterReflectionTerrainFeatures[location] = locationToWaterReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + } + + if (IsTilePuddle(reflectableObject.Tile, 3)) + { + locationToPuddleReflectionTerrainFeatures[location].Add(reflectableObject); + locationToPuddleReflectionTerrainFeatures[location] = locationToPuddleReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + } + } + + private void HandleTerrainFeatureRemoval(GameLocation location, TerrainFeature terrainFeature) + { + if (location is null || locationToWaterReflectionTerrainFeatures.ContainsKey(location) is false || locationToPuddleReflectionTerrainFeatures.ContainsKey(location) is false) + { + return; + } + + locationToWaterReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableTerrain reflectableTerrain && reflectableTerrain.Terrain == terrainFeature); + locationToPuddleReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableTerrain reflectableTerrain && reflectableTerrain.Terrain == terrainFeature); + + locationToWaterReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + locationToPuddleReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + } + + private void HandleFurnitureRemoval(GameLocation location, Furniture furniture) + { + if (location is null || locationToWaterReflectionTerrainFeatures.ContainsKey(location) is false || locationToPuddleReflectionTerrainFeatures.ContainsKey(location) is false) + { + return; + } + + locationToWaterReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableFurniture reflectableFurniture && reflectableFurniture.Furniture == furniture); + locationToPuddleReflectionTerrainFeatures[location].RemoveWhere(t => t is ReflectableFurniture reflectableFurniture && reflectableFurniture.Furniture == furniture); + + locationToWaterReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + locationToPuddleReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + } + private void LoadContentPacks(bool silent = false) { // Clear the existing cache of custom buildings @@ -1415,7 +1507,7 @@ private Vector2 GetMirrorOffset(GameLocation location, int x, int y) return new Vector2(xOffsetValue, yOffsetValue); } - private bool IsWaterReflectiveTile(GameLocation location, int x, int y) + private static bool IsWaterReflectiveTile(GameLocation location, int x, int y) { if (location is null) { @@ -1538,5 +1630,145 @@ internal static IEnumerable GetActiveNPCs(GameLocation location) return Array.Empty(); } + + internal static IEnumerable GetWaterReflectionTerrainFeatures(GameLocation location) + { + if (location is null) + { + return Array.Empty(); + } + + if (locationToWaterReflectionTerrainFeatures.ContainsKey(location) is false) + { + ResetLocationTerrainCache(location); + } + + return locationToWaterReflectionTerrainFeatures[location]; + } + + internal static IEnumerable GetPuddleReflectionTerrainFeatures(GameLocation location) + { + if (location is null) + { + return Array.Empty(); + } + + if (locationToPuddleReflectionTerrainFeatures.ContainsKey(location) is false) + { + ResetLocationTerrainCache(location); + } + + return locationToPuddleReflectionTerrainFeatures[location]; + } + + internal static void ResetLocationTerrainCache(GameLocation location) + { + locationToWaterReflectionTerrainFeatures[location] = new List(); + locationToPuddleReflectionTerrainFeatures[location] = new List(); + + if (location.terrainFeatures is not null) + { + foreach (var terrainFeature in location.terrainFeatures.Values) + { + if (IsTileReflective(terrainFeature.Tile, 3)) + { + locationToWaterReflectionTerrainFeatures[location].Add(new ReflectableTerrain(terrainFeature)); + } + + if (IsTilePuddle(terrainFeature.Tile, 3)) + { + locationToPuddleReflectionTerrainFeatures[location].Add(new ReflectableTerrain(terrainFeature)); + } + } + } + + if (location.largeTerrainFeatures is not null) + { + foreach (var largeTerrainFeature in location.largeTerrainFeatures) + { + if (IsTileReflective(largeTerrainFeature.Tile, 3)) + { + locationToWaterReflectionTerrainFeatures[location].Add(new ReflectableTerrain(largeTerrainFeature)); + } + + if (IsTilePuddle(largeTerrainFeature.Tile, 3)) + { + locationToPuddleReflectionTerrainFeatures[location].Add(new ReflectableTerrain(largeTerrainFeature)); + } + } + } + + // Add buildings + if (location.buildings is not null) + { + foreach (var building in location.buildings) + { + var buildingTile = new Vector2(building.tileX.Value, building.tileY.Value + building.tilesHigh.Value); + if (IsTileReflective(buildingTile, 2)) + { + locationToWaterReflectionTerrainFeatures[location].Add(new ReflectableBuilding(building)); + } + + if (IsTilePuddle(buildingTile, 2)) + { + locationToPuddleReflectionTerrainFeatures[location].Add(new ReflectableBuilding(building)); + } + } + } + + // Add furniture + if (location.furniture is not null) + { + foreach (var furniture in location.furniture) + { + if (IsTileReflective(furniture.TileLocation, 3)) + { + locationToWaterReflectionTerrainFeatures[location].Add(new ReflectableFurniture(furniture)); + } + + if (IsTilePuddle(furniture.TileLocation, 3)) + { + locationToPuddleReflectionTerrainFeatures[location].Add(new ReflectableFurniture(furniture)); + } + } + } + + locationToWaterReflectionTerrainFeatures[location] = locationToWaterReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + locationToPuddleReflectionTerrainFeatures[location] = locationToPuddleReflectionTerrainFeatures[location].OrderBy(t => t.Tile.Y).ToList(); + } + + private static bool IsTileReflective(Vector2 startPosition, int yTileOffset) + { + for (int yOffset = -1; yOffset <= yTileOffset; yOffset++) + { + var tilePosition = startPosition + new Vector2(0, yOffset); + + if (IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X, (int)tilePosition.Y) is true + || IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X - 1, (int)tilePosition.Y) is true + || IsWaterReflectiveTile(Game1.currentLocation, (int)tilePosition.X + 1, (int)tilePosition.Y) is true) + { + return true; + } + } + + return false; + } + + private static bool IsTilePuddle(Vector2 startPosition, int yTileOffset, bool checkPuddles = false) + { + for (int yOffset = -1; yOffset <= yTileOffset; yOffset++) + { + var tilePosition = startPosition + new Vector2(0, yOffset); + + if (puddleManager.IsTilePuddle(Game1.currentLocation, (int)tilePosition.X, (int)tilePosition.Y) is true + || puddleManager.IsTilePuddle(Game1.currentLocation, (int)tilePosition.X - 1, (int)tilePosition.Y) is true + || puddleManager.IsTilePuddle(Game1.currentLocation, (int)tilePosition.X + 1, (int)tilePosition.Y) is true) + { + return true; + } + } + + return false; + } } } diff --git a/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs b/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs index f53aeaa..ad30b35 100644 --- a/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs +++ b/DynamicReflections/Framework/External/GenericModConfigMenu/GMCMHelper.cs @@ -37,6 +37,10 @@ public static void Register(IGenericModConfigMenuApi configApi, DynamicReflectio configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.ArePuddleReflectionsEnabled, value => DynamicReflections.modConfig.ArePuddleReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.puddle_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreNPCReflectionsEnabled, value => DynamicReflections.modConfig.AreNPCReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.npc_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreCompanionReflectionsEnabled, value => DynamicReflections.modConfig.AreCompanionReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.companion_reflections")); + configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreGrassReflectionsEnabled, value => DynamicReflections.modConfig.AreGrassReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.grass_reflections")); + configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreTerrainReflectionsEnabled, value => DynamicReflections.modConfig.AreTerrainReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.terrain_reflections")); + configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled, value => DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.player_buildings_reflections")); + configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreFurnitureReflectionsEnabled, value => DynamicReflections.modConfig.AreFurnitureReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.furniture_reflections")); configApi.AddBoolOption(ModManifest, () => DynamicReflections.modConfig.AreSkyReflectionsEnabled, value => DynamicReflections.modConfig.AreSkyReflectionsEnabled = value, () => Helper.Translation.Get("config.general_settings.sky_reflections")); configApi.AddKeybind(ModManifest, () => DynamicReflections.modConfig.QuickMenuKey, value => DynamicReflections.modConfig.QuickMenuKey = value, () => Helper.Translation.Get("config.general_settings.shortcut_key"), () => Helper.Translation.Get("config.general_settings.shortcut_key.description")); diff --git a/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs b/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs index 6de3b1f..708a574 100644 --- a/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs +++ b/DynamicReflections/Framework/External/GenericModConfigMenu/ModConfig.cs @@ -16,6 +16,10 @@ public class ModConfig public bool ArePuddleReflectionsEnabled { get; set; } = true; public bool AreNPCReflectionsEnabled { get; set; } = true; public bool AreCompanionReflectionsEnabled { get; set; } = true; + public bool AreGrassReflectionsEnabled { get; set; } = true; + public bool AreTerrainReflectionsEnabled { get; set; } = true; + public bool ArePlayerBuildingReflectionsEnabled { get; set; } = true; + public bool AreFurnitureReflectionsEnabled { get; set; } = true; public bool AreSkyReflectionsEnabled { get; set; } = true; public WaterSettings WaterReflectionSettings { get; set; } = new WaterSettings(); diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs new file mode 100644 index 0000000..c0a4471 --- /dev/null +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableBuilding.cs @@ -0,0 +1,33 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Buildings; + +namespace DynamicReflections.Framework.Models.Reflections +{ + public class ReflectableBuilding : ReflectableObject + { + public Building Building; + + public ReflectableBuilding(Building building) + { + Building = building; + Tile = new Vector2(building.tileX.Value, building.tileY.Value); + } + + public override void Draw(SpriteBatch spriteBatch) + { + Building.draw(spriteBatch); + } + + public override bool IsOnScreen() + { + return Utility.isOnScreen(Tile * 64, 8 * 64); + } + + public override bool IsEnabled() + { + return DynamicReflections.modConfig.ArePlayerBuildingReflectionsEnabled; + } + } +} diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableFurniture.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableFurniture.cs new file mode 100644 index 0000000..92f8e4b --- /dev/null +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableFurniture.cs @@ -0,0 +1,35 @@ +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.Objects; + +namespace DynamicReflections.Framework.Models.Reflections +{ + public class ReflectableFurniture : ReflectableObject + { + public Furniture Furniture; + + public ReflectableFurniture(Furniture furniture) + { + Furniture = furniture; + Tile = furniture.TileLocation; + } + + public override void Draw(SpriteBatch spriteBatch) + { + Furniture.isDrawingLocationFurniture = true; + Furniture.draw(spriteBatch, -1, -1); + Furniture.isDrawingLocationFurniture = false; + } + + public override bool IsOnScreen() + { + // Allow for three tile (3 * 64) spacing for trees and bushes + return Utility.isOnScreen(Tile * 64, 3 * 64); + } + + public override bool IsEnabled() + { + return DynamicReflections.modConfig.AreFurnitureReflectionsEnabled; + } + } +} diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableObject.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableObject.cs new file mode 100644 index 0000000..7df0051 --- /dev/null +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableObject.cs @@ -0,0 +1,14 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace DynamicReflections.Framework.Models.Reflections +{ + public abstract class ReflectableObject + { + public Vector2 Tile { get; set; } + + public abstract void Draw(SpriteBatch spriteBatch); + public abstract bool IsOnScreen(); + public abstract bool IsEnabled(); + } +} diff --git a/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs b/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs new file mode 100644 index 0000000..662bdc7 --- /dev/null +++ b/DynamicReflections/Framework/Models/Reflections/ReflectableTerrain.cs @@ -0,0 +1,49 @@ +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using StardewValley.TerrainFeatures; + +namespace DynamicReflections.Framework.Models.Reflections +{ + public class ReflectableTerrain : ReflectableObject + { + public TerrainFeature Terrain; + + public ReflectableTerrain(TerrainFeature terrain) + { + Terrain = terrain; + Tile = terrain.Tile; + } + + public override void Draw(SpriteBatch spriteBatch) + { + if (Terrain is Tree tree) + { + float alphaCache = tree.alpha; + tree.alpha = 1f; + + Terrain.draw(spriteBatch); + tree.alpha = alphaCache; + } + else + { + Terrain.draw(spriteBatch); + } + } + + public override bool IsOnScreen() + { + // Allow for three tile (3 * 64) spacing for trees and bushes + return Utility.isOnScreen(Tile * 64, 8 * 64); + } + + public override bool IsEnabled() + { + if (Terrain is Grass) + { + return DynamicReflections.modConfig.AreGrassReflectionsEnabled; + } + + return DynamicReflections.modConfig.AreTerrainReflectionsEnabled; + } + } +} diff --git a/DynamicReflections/Framework/Models/Settings/WaterSettings.cs b/DynamicReflections/Framework/Models/Settings/WaterSettings.cs index f7151b4..4aab1ff 100644 --- a/DynamicReflections/Framework/Models/Settings/WaterSettings.cs +++ b/DynamicReflections/Framework/Models/Settings/WaterSettings.cs @@ -57,7 +57,7 @@ public void Reset(WaterSettings referencedSettings = null) AreReflectionsEnabled = true; ReflectionDirection = Direction.South; ReflectionOverlay = Color.White; - PlayerReflectionOffset = new Vector2(0f, 0.5f); + PlayerReflectionOffset = new Vector2(0f, 1.5f); NPCReflectionOffset = new Vector2(0f, 0.7f); CompanionReflectionOffset = new Vector2(0f, 0.3f); IsReflectionWavy = true; diff --git a/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs b/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs index e5b4f8f..4038cfe 100644 --- a/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs +++ b/DynamicReflections/Framework/Patches/Locations/GameLocationPatch.cs @@ -35,6 +35,7 @@ internal void Apply(Harmony harmony) harmony.CreateReversePatcher(AccessTools.Method(_type, nameof(GameLocation.drawWater), new[] { typeof(SpriteBatch) }), new HarmonyMethod(GetType(), nameof(DrawWaterReversePatch))).Patch(); harmony.Patch(AccessTools.Method(_type, nameof(GameLocation.UpdateWhenCurrentLocation), new[] { typeof(GameTime) }), postfix: new HarmonyMethod(GetType(), nameof(UpdateWhenCurrentLocationPostfix))); + harmony.Patch(AccessTools.Method(_type, nameof(GameLocation.OnBuildingMoved), new[] { typeof(Building) }), postfix: new HarmonyMethod(GetType(), nameof(OnBuildingMovedPostfix))); } [HarmonyBefore(new string[] { "shekurika.WaterFish" })] @@ -154,6 +155,11 @@ private static void UpdateWhenCurrentLocationPostfix(GameLocation __instance, Ga } } + private static void OnBuildingMovedPostfix(GameLocation __instance, Building building) + { + DynamicReflections.ResetLocationTerrainCache(__instance); + } + private static void GenerateRipple(GameLocation location, Point puddleTile, bool playSound = false) { TemporaryAnimatedSprite splashSprite = new TemporaryAnimatedSprite("TileSheets\\animations", new Microsoft.Xna.Framework.Rectangle(0, 0, 64, 64), Game1.random.Next(50, 100), 9, 1, new Vector2(puddleTile.X, puddleTile.Y) * 64f, flicker: false, flipped: false, 0f, 0f, DynamicReflections.currentPuddleSettings.RippleColor, 1f, 0f, 0f, 0f); diff --git a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs index c1835ff..ef376f7 100644 --- a/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs +++ b/DynamicReflections/Framework/Patches/xTile/LayerPatch.cs @@ -95,7 +95,7 @@ private static bool DrawNormalPrefix(Layer __instance, IDisplayDevice displayDev } // Handle preliminary water reflection logic - if (DynamicReflections.shouldDrawWaterReflection is true) + if (DynamicReflections.modConfig.AreWaterReflectionsEnabled) { DynamicReflections.isFilteringWater = true; SpriteBatchToolkit.RenderWaterReflectionPlayerSprite(); diff --git a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs index c8e9f47..0dd3ef1 100644 --- a/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs +++ b/DynamicReflections/Framework/Utilities/SpriteBatchToolkit.cs @@ -1,8 +1,11 @@ +using DynamicReflections.Framework.Models.Reflections; using DynamicReflections.Framework.Patches.Tiles; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewValley; +using StardewValley.Buildings; +using StardewValley.TerrainFeatures; using System; using System.Collections.Generic; using System.Linq; @@ -387,7 +390,17 @@ internal static void RenderWaterReflectionPlayerSprite() // Draw the scene Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - DrawReflectionViaMatrix(); + // Draw terrain before player + RenderWaterReflectionTerrain(afterPlayer: false); + + // Draw player reflection (if near water tile) + if (DynamicReflections.shouldDrawWaterReflection) + { + DrawPlayerWaterReflection(); + } + + // Draw terrain after player + RenderWaterReflectionTerrain(beforePlayer: false); // Drop the render target SpriteBatchToolkit.StopRendering(); @@ -438,7 +451,6 @@ internal static void RenderWaterReflectionNPCs() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } - internal static void RenderPuddleReflectionNPCs() { if (Game1.currentLocation is null || Game1.currentLocation.characters is null) @@ -545,6 +557,152 @@ internal static void RenderPuddleReflectionNPCs() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } + internal static void RenderWaterReflectionTerrain(bool beforePlayer = true, bool afterPlayer = true) + { + if (Game1.currentLocation is null) + { + return; + } + + foreach (ReflectableObject reflectableObject in DynamicReflections.GetWaterReflectionTerrainFeatures(Game1.currentLocation)) + { + if (reflectableObject.IsEnabled() is false) + { + continue; + } + else if (beforePlayer is false && reflectableObject.Tile.Y <= Game1.player.Tile.Y) + { + continue; + } + else if (afterPlayer is false && reflectableObject.Tile.Y > Game1.player.Tile.Y) + { + continue; + } + else if (reflectableObject.IsOnScreen() is false) + { + continue; + } + + int yOffset = 0; + var spriteSortMode = SpriteSortMode.FrontToBack; + if (reflectableObject is ReflectableTerrain reflectableTerrain) + { + if (reflectableTerrain.Terrain is not Tree && reflectableTerrain.Terrain is not Bush && reflectableTerrain.Terrain is not Grass) + { + continue; + } + + yOffset = 48; + if (reflectableTerrain.Terrain is Tree) + { + yOffset = 72; + } + else if (reflectableTerrain.Terrain is Grass) + { + yOffset = 96; + spriteSortMode = SpriteSortMode.BackToFront; + } + } + else if (reflectableObject is ReflectableBuilding reflectableBuilding) + { + yOffset = (reflectableBuilding.Building.tilesHigh.Value * 64) - 20; + } + else if (reflectableObject is ReflectableFurniture reflectableFurniture) + { + yOffset = reflectableFurniture.Furniture.getTilesHigh() * 64; + } + + if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) + { + var scale = Matrix.CreateScale(1, -1, 1); + var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, reflectableObject.Tile * 64).Y + yOffset) * 2, 0); + + Game1.spriteBatch.Begin(spriteSortMode, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); + } + else + { + Game1.spriteBatch.Begin(spriteSortMode, BlendState.AlphaBlend, SamplerState.PointClamp); + } + + reflectableObject.Draw(Game1.spriteBatch); + + Game1.spriteBatch.End(); + } + } + + internal static void RenderPuddleReflectionTerrain(bool beforePlayer = true, bool afterPlayer = true) + { + if (Game1.currentLocation is null) + { + return; + } + + foreach (ReflectableObject reflectableObject in DynamicReflections.GetPuddleReflectionTerrainFeatures(Game1.currentLocation)) + { + if (reflectableObject.IsEnabled() is false) + { + continue; + } + else if (beforePlayer is false && reflectableObject.Tile.Y <= Game1.player.Tile.Y) + { + continue; + } + else if (afterPlayer is false && reflectableObject.Tile.Y > Game1.player.Tile.Y) + { + continue; + } + else if (reflectableObject.IsOnScreen() is false) + { + continue; + } + + int yOffset = 0; + var spriteSortMode = SpriteSortMode.FrontToBack; + if (reflectableObject is ReflectableTerrain reflectableTerrain) + { + if (reflectableTerrain.Terrain is not Tree && reflectableTerrain.Terrain is not Bush && reflectableTerrain.Terrain is not Grass) + { + continue; + } + + yOffset = 16; + if (reflectableTerrain.Terrain is Tree) + { + yOffset = 8; + } + else if (reflectableTerrain.Terrain is Grass) + { + yOffset = 48; + spriteSortMode = SpriteSortMode.BackToFront; + } + } + else if (reflectableObject is ReflectableBuilding reflectableBuilding) + { + yOffset = (reflectableBuilding.Building.tilesHigh.Value * 64) - 32; + } + else if (reflectableObject is ReflectableFurniture reflectableFurniture) + { + yOffset = reflectableFurniture.Furniture.getTilesHigh() * 16; + } + + if (DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionDirection == Models.Settings.Direction.South) + { + var scale = Matrix.CreateScale(1, -1, 1); + var position = Matrix.CreateTranslation(0, (Game1.GlobalToLocal(Game1.viewport, reflectableObject.Tile * 64).Y + yOffset) * 2, 0); + + Game1.spriteBatch.Begin(spriteSortMode, BlendState.AlphaBlend, SamplerState.PointClamp, rasterizerState: DynamicReflections.rasterizer, transformMatrix: scale * position); + } + else + { + Game1.spriteBatch.Begin(spriteSortMode, BlendState.AlphaBlend, SamplerState.PointClamp); + } + + reflectableObject.Draw(Game1.spriteBatch); + + Game1.spriteBatch.End(); + } + } + internal static void DrawPuddleReflection(Texture2D mask) { DynamicReflections.mirrorReflectionEffect.Parameters["Mask"].SetValue(mask); @@ -555,7 +713,11 @@ internal static void DrawPuddleReflection(Texture2D mask) Game1.spriteBatch.Draw(DynamicReflections.nightSkyRenderTarget, Vector2.Zero, Color.White); } - Game1.spriteBatch.Draw(DynamicReflections.playerPuddleReflectionRender, Vector2.Zero, DynamicReflections.currentPuddleSettings.ReflectionOverlay); + if (DynamicReflections.modConfig.ArePuddleReflectionsEnabled is true) + { + // Draw the player + Game1.spriteBatch.Draw(DynamicReflections.playerPuddleReflectionRender, Vector2.Zero, DynamicReflections.currentPuddleSettings.ReflectionOverlay); + } Game1.spriteBatch.Draw(DynamicReflections.npcPuddleReflectionRender, Vector2.Zero, DynamicReflections.currentPuddleSettings.ReflectionOverlay); @@ -597,46 +759,14 @@ internal static void RenderPuddleReflectionPlayerSprite() // Draw the scene Game1.graphics.GraphicsDevice.Clear(Color.Transparent); - var oldDirection = Game1.player.FacingDirection; - var oldSprite = Game1.player.FarmerSprite; - - // Original world position - var oldPosition = Game1.player.Position; - - // Where the reflection was previously drawn (world space) - var worldOffset = DynamicReflections.currentPuddleSettings.ReflectionOffset * 64f; - var targetWorld = oldPosition - worldOffset; + // Draw terrain before player + RenderPuddleReflectionTerrain(afterPlayer: false); - // Convert both positions to screen space to build an equivalent translation - var playerScreen = Game1.GlobalToLocal(Game1.viewport, oldPosition); - var targetScreen = Game1.GlobalToLocal(Game1.viewport, targetWorld); - var delta = targetScreen - playerScreen; + // Draw player reflection + DrawPlayerPuddleReflection(); - // Same vertical flip & pivot as before (across the player's original local Y) - var scale = Matrix.CreateScale(1f, -1f, 1f); - var pivot = Matrix.CreateTranslation(0f, playerScreen.Y * 2f, 0f); - - // Apply the offset as a pre-translation, then the original reflection matrix - var preTranslation = Matrix.CreateTranslation(delta.X, delta.Y, 0f); - var transform = preTranslation * scale * pivot; - - Game1.spriteBatch.Begin( - SpriteSortMode.FrontToBack, - BlendState.AlphaBlend, - SamplerState.PointClamp, - depthStencilState: null, - rasterizerState: DynamicReflections.rasterizer, - effect: null, - transformMatrix: transform - ); - - // Draw the player at their real position; transform handles reflection+offset - Game1.player.draw(Game1.spriteBatch); - - Game1.player.FacingDirection = oldDirection; - Game1.player.FarmerSprite = oldSprite; - - Game1.spriteBatch.End(); + // Draw terrain after player + RenderPuddleReflectionTerrain(beforePlayer: false); // Draw puddle ripples on top, unchanged Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp); @@ -651,8 +781,7 @@ internal static void RenderPuddleReflectionPlayerSprite() Game1.graphics.GraphicsDevice.Clear(Game1.bgColor); } - - internal static void DrawReflectionViaMatrix() + internal static void DrawPlayerWaterReflection() { // Cache what we’re going to touch so we can restore it var oldDirection = Game1.player.FacingDirection; @@ -667,8 +796,9 @@ internal static void DrawReflectionViaMatrix() var scale = Matrix.CreateScale(1f, -1f, 1f); // Pivot at the water reflection line (already computed in world space, convert to screen). + float yOffset = Game1.player.IsSitting() ? 16f : 0f; float pivotY = Game1.GlobalToLocal(Game1.viewport, DynamicReflections.waterReflectionPosition.Value).Y; - var position = Matrix.CreateTranslation(0f, pivotY * 2f, 0f); + var position = Matrix.CreateTranslation(0f, (pivotY + yOffset) * 2f, 0f); Game1.spriteBatch.Begin( SpriteSortMode.FrontToBack, @@ -707,13 +837,62 @@ internal static void DrawReflectionViaMatrix() Game1.spriteBatch.End(); } + internal static void DrawPlayerPuddleReflection() + { + var oldDirection = Game1.player.FacingDirection; + var oldSprite = Game1.player.FarmerSprite; + + // Original world position + var oldPosition = Game1.player.Position; + + // Where the reflection was previously drawn (world space) + var worldOffset = DynamicReflections.currentPuddleSettings.ReflectionOffset * 64f; + var targetWorld = oldPosition - worldOffset; + + // Convert both positions to screen space to build an equivalent translation + var playerScreen = Game1.GlobalToLocal(Game1.viewport, oldPosition); + var targetScreen = Game1.GlobalToLocal(Game1.viewport, targetWorld); + var delta = targetScreen - playerScreen; + + // Same vertical flip & pivot as before (across the player's original local Y) + float yOffset = Game1.player.IsSitting() ? 32f : 0f; + var scale = Matrix.CreateScale(1f, -1f, 1f); + var pivot = Matrix.CreateTranslation(0f, (playerScreen.Y + yOffset) * 2f, 0f); + + // Apply the offset as a pre-translation, then the original reflection matrix + var preTranslation = Matrix.CreateTranslation(delta.X, delta.Y, 0f); + var transform = preTranslation * scale * pivot; + + Game1.spriteBatch.Begin( + SpriteSortMode.FrontToBack, + BlendState.AlphaBlend, + SamplerState.PointClamp, + depthStencilState: null, + rasterizerState: DynamicReflections.rasterizer, + effect: null, + transformMatrix: transform + ); + + // Draw the player at their real position; transform handles reflection+offset + Game1.player.draw(Game1.spriteBatch); + + Game1.player.FacingDirection = oldDirection; + Game1.player.FarmerSprite = oldSprite; + + Game1.spriteBatch.End(); + } + internal static void DrawRenderedCharacters(bool isWavy = false) { - if (DynamicReflections.shouldDrawWaterReflection is true) + + if (DynamicReflections.modConfig.AreWaterReflectionsEnabled) { DynamicReflections.waterReflectionEffect.Parameters["ColorOverlay"].SetValue(DynamicReflections.modConfig.WaterReflectionSettings.ReflectionOverlay.ToVector4()); Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, effect: isWavy ? DynamicReflections.waterReflectionEffect : null); + + // Draw the player Game1.spriteBatch.Draw(DynamicReflections.playerWaterReflectionRender, Vector2.Zero, DynamicReflections.modConfig.GetCurrentWaterSettings(Game1.currentLocation).ReflectionOverlay); + Game1.spriteBatch.End(); } diff --git a/DynamicReflections/i18n/default.json b/DynamicReflections/i18n/default.json index 17649ba..cce25d4 100644 --- a/DynamicReflections/i18n/default.json +++ b/DynamicReflections/i18n/default.json @@ -8,6 +8,10 @@ "config.general_settings.puddle_reflections": "Enable Puddle Reflections", "config.general_settings.npc_reflections": "Enable NPC Reflections", "config.general_settings.companion_reflections": "Enable Companion Reflections", + "config.general_settings.grass_reflections": "Enable Grass Reflections", + "config.general_settings.terrain_reflections": "Enable Terrain Reflections", + "config.general_settings.player_buildings_reflections": "Enable Player Building Reflections", + "config.general_settings.furniture_reflections": "Enable Furniture Reflections", "config.general_settings.sky_reflections": "Enable Sky Reflections", "config.general_settings.link.click_here": "Click Here", "config.general_settings.link.return_main": "Return to the Main Page",