Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.1.84

- Add Adrenaline Echo, a Unique trinket shardstone available at Epic, Legendary, Mythic, and Ancient tiers.
- When the trinket activates its full-adrenaline effect, trigger the equipped weapon's projectile area impact at the player's position. Staff of Embers creates an explosion around the player with its Epic Loot damage effects.
- Require a weapon whose own projectile has area damage. The effect costs no durability, stamina, health, or eitr.

## 0.1.82

- Save the builder's character name on newly built ships and carts so their owner labels work when the builder is offline or far away, including after a server restart.
Expand Down
4 changes: 2 additions & 2 deletions Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.1.83")]
[assembly: AssemblyFileVersion("0.1.83")]
[assembly: AssemblyVersion("0.1.84")]
[assembly: AssemblyFileVersion("0.1.84")]
2 changes: 1 addition & 1 deletion Source/Core/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ namespace PraetorisClient
public class PraetorisClientPlugin : BaseUnityPlugin
{
private const string ModName = "PraetorisClient";
private const string ModVersion = "0.1.83";
private const string ModVersion = "0.1.84";
private const string Author = "warpalicious";
private const string ModGUID = Author + "." + ModName;
private const string EpicLootGuid = "randyknapp.mods.epicloot";
Expand Down
208 changes: 208 additions & 0 deletions Source/EpicLoot/AdrenalineEchoRuntime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using EpicLoot.MagicItemEffects;
using HarmonyLib;
using UnityEngine;

namespace PraetorisClient
{
internal static class AdrenalineEchoRuntime
{
private static Attack? activeEcho;
private static Vector3 echoPosition;
private static bool collectingProjectiles;
private static readonly List<Projectile> EchoProjectiles = new List<Projectile>();
private static readonly FieldInfo PendingShot = AccessTools.Field(typeof(MultiShot), "_pendingShot");
private static readonly AccessTools.FieldRef<Humanoid, ItemDrop.ItemData> Trinket = AccessTools.FieldRefAccess<Humanoid, ItemDrop.ItemData>("m_trinketItem");
private static readonly Action<Attack> FireProjectileBurst =
AccessTools.MethodDelegate<Action<Attack>>(AccessTools.Method(typeof(Attack), "FireProjectileBurst"));

private static void OnTrinketActivated(Player player)
{
if (player != Player.m_localPlayer || player.IsDead() || activeEcho != null ||
Trinket(player) is not { m_equipped: true } trinket ||
trinket.m_shared.m_fullAdrenalineSE == null ||
EpicLootApiBridge.GetTotalActiveMagicEffectValue(player, trinket, PraetorisMagicEffects.AdrenalineEcho, 1f) <= 0f)
{
return;
}

ItemDrop.ItemData weapon = player.GetCurrentWeapon();
if (weapon?.m_shared.m_attack?.m_attackType != Attack.AttackType.Projectile ||
weapon.m_shared.m_itemType == ItemDrop.ItemData.ItemType.Bow ||
weapon.m_shared.m_skillType == Skills.SkillType.Bows ||
weapon.m_shared.m_skillType == Skills.SkillType.Crossbows ||
!string.IsNullOrEmpty(weapon.m_shared.m_ammoType))
{
return;
}

// Leave the attack that filled adrenaline before entering another attack's Harmony hooks.
// Epic Loot temporarily changes shared weapon damage while a projectile burst runs.
PraetorisClientPlugin.Instance?.StartCoroutine(FireAfterAttack(player, weapon));
}

private static IEnumerator FireAfterAttack(Player player, ItemDrop.ItemData weapon)
{
yield return null;
if (player == null || player != Player.m_localPlayer || player.IsDead() ||
player.GetCurrentWeapon() != weapon || !weapon.m_equipped)
{
yield break;
}

// Use only the weapon's own area-impact projectile, never arrows or bolts.
Projectile? projectile = weapon.m_shared.m_attack.m_attackProjectile?.GetComponent<Projectile>();
if (projectile == null || projectile.m_aoe <= 0f)
{
yield break;
}

Attack echo = weapon.m_shared.m_attack.Clone();
echo.m_projectiles = 1;
echo.m_projectileBursts = 1;
echo.m_perBurstResourceUsage = false;
echo.m_attackStamina = 0f;
echo.m_attackHealth = 0f;
echo.m_attackHealthPercentage = 0f;
echo.m_attackUseAdrenaline = 0f;
echo.m_blockReloadTime = 0f;
echo.m_requiresReload = false;
echo.m_destroyPreviousProjectile = false;
echo.m_consumeItem = false;
echo.m_toggleFlying = false;
echo.m_recoilPushback = 0f;
echo.m_selfDamage = 0;

// MultiShot stores its pending roll globally, including between a staff's bursts.
// Give the echo its own normal roll, then restore the attack already in progress.
object pendingShot = PendingShot.GetValue(null);
bool tripleShot = MultiShot.IsTripleShotActive;
int shotProjectiles = MultiShot.ShotProjectiles;
GameObject previousProjectile = weapon.m_lastProjectile;
float previousAttackTime = weapon.m_lastAttackTime;
activeEcho = echo;
echoPosition = player.transform.position;
collectingProjectiles = true;
try
{
echo.StartWithoutAnimation(player, player.GetComponent<Rigidbody>(), player.GetVisEquipment(), weapon, 1f);
collectingProjectiles = false;
// Finish all setup and burst hooks before impact. Epic Loot adds some impact
// effects after Projectile.Setup, including Explosive Arrows.
foreach (Projectile spawnedProjectile in EchoProjectiles)
{
if (spawnedProjectile != null)
{
spawnedProjectile.OnHit(null, echoPosition, false, Vector3.up);
}
}
}
catch (Exception exception)
{
PraetorisClientPlugin.Log.LogError("Adrenaline Echo failed: " + exception);
}
finally
{
activeEcho = null;
collectingProjectiles = false;
EchoProjectiles.Clear();
PendingShot.SetValue(null, pendingShot);
MultiShot.IsTripleShotActive = tripleShot;
MultiShot.ShotProjectiles = shotProjectiles;
weapon.m_lastProjectile = previousProjectile;
weapon.m_lastAttackTime = previousAttackTime;
}
}

[HarmonyPatch(typeof(Attack), nameof(Attack.GetProjectileSpawnPoint))]
private static class EchoPositionPatch
{
private static void Postfix(Attack __instance, ref Vector3 spawnPoint)
{
if (__instance == activeEcho) spawnPoint = echoPosition;
}
}

[HarmonyPatch(typeof(Projectile), nameof(Projectile.Setup))]
private static class EchoSetupPatch
{
[HarmonyPriority(Priority.Last)]
private static void Prefix(Projectile __instance, Character owner, ref Vector3 velocity)
{
if (!collectingProjectiles || owner != Player.m_localPlayer) return;
__instance.transform.position = echoPosition;
velocity = Vector3.zero;
__instance.m_hitOwner = false;
__instance.m_bounce = false;
__instance.m_onlyStopOnTerrain = false;
__instance.m_respawnItemOnHit = false;
EchoProjectiles.Add(__instance);
}
}

[HarmonyPatch(typeof(Player), nameof(Player.AddAdrenaline))]
private static class ActivationPatch
{
private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
// This branch runs only when full adrenaline activates equipment, including a
// refresh of an existing status effect and a fill from zero in one hit.
return new CodeMatcher(instructions)
.MatchStartForward(new CodeMatch(OpCodes.Ldfld,
AccessTools.Field(typeof(Player), nameof(Player.m_adrenalinePopEffects))))
.ThrowIfNotMatch("Cannot locate trinket activation for Adrenaline Echo.")
.Insert(new CodeInstruction(OpCodes.Ldarg_0),
new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(AdrenalineEchoRuntime), nameof(OnTrinketActivated))))
.InstructionEnumeration();
}
}

[HarmonyPatch(typeof(Attack), nameof(Attack.OnAttackTrigger))]
private static class EchoStaggerPatch
{
private static bool BlocksAttack(bool staggering, Attack attack) => staggering && attack != activeEcho;

private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions)
.MatchStartForward(new CodeMatch(instruction => instruction.Calls(
AccessTools.Method(typeof(Character), nameof(Character.IsStaggering)))))
.ThrowIfNotMatch("Cannot locate attack stagger check for Adrenaline Echo.")
.Advance(1)
.Insert(new CodeInstruction(OpCodes.Ldarg_0),
new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(EchoStaggerPatch), nameof(BlocksAttack))))
.InstructionEnumeration();
}
}

[HarmonyPatch(typeof(Attack), nameof(Attack.GetAttackEitr), new Type[] { })]
private static class EchoEitrPatch
{
private static bool Prefix(Attack __instance, ref float __result)
{
if (__instance != activeEcho) return true;
// Vanilla reads eitr cost from the weapon's shared template, not this clone.
__result = 0f;
return false;
}
}

[HarmonyPatch(typeof(Attack), nameof(Attack.ProjectileAttackTriggered))]
private static class EchoProjectilePatch
{
private static bool Prefix(Attack __instance)
{
if (__instance != activeEcho) return true;
// Use the normal patched burst with the real ItemData. This preserves Epic Loot
// damage, speed, multishot, projectile markers and weapon attribution on impact.
// The burst supplies the projectile effects without durability or reload changes.
FireProjectileBurst(__instance);
return false;
}
}
}
}
10 changes: 10 additions & 0 deletions Source/EpicLoot/PraetorisMagicEffects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal static class PraetorisMagicEffects
internal const string PointBlank = "PointBlank";
internal const string ReloadOnKill = "ReloadOnKill";
internal const string ArrowRain = "ArrowRain";
internal const string AdrenalineEcho = "AdrenalineEcho";
internal static readonly float[] PointBlankValues = { 5, 7, 10, 15, 20, 25 };
internal static readonly float[] PiercingShotValues = { 2, 3, 3, 4 };

Expand All @@ -31,6 +32,15 @@ internal static class PraetorisMagicEffects
private static readonly string[] MagicEffectDefinitionJson =
{
@"{
""Type"": ""AdrenalineEcho"",
""CanBeAugmented"": false,
""CanBeDisenchanted"": false,
""CanBeRunified"": false,
""DisplayText"": ""Adrenaline Echo: Full adrenaline triggers your weapon's projectile area impact at your position."",
""Description"": ""When this trinket activates its full-adrenaline effect, trigger your equipped weapon's projectile area impact at your position, without travel. Uses the projectile's area damage and your weapon's enchantments. Requires a weapon whose own projectile has area damage, such as Staff of Embers. Costs no durability, stamina, health, or eitr."",
""Requirements"": { ""NoRoll"": true, ""AllowedItemTypes"": [ ""Trinket"" ] }
}",
@"{
""Type"": ""IncreaseEffectDuration"",
""DisplayText"": ""Effect Duration +{0:0.#}%"",
""Description"": ""Increase the duration of timed status effects applied by this item by +<b><color=yellow>X</color></b>%. Also affects supported timed status effects from equipped gear."",
Expand Down
40 changes: 29 additions & 11 deletions Source/EpicLoot/PraetorisShardstones.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ internal static class PraetorisShardstones
new ShardSpec((ShardType)0x50520003, "Piercing Shot", "Stormcaller", PraetorisMagicEffects.PiercingShot,
ShardCategory.Unique, ItemRarity.Epic, PraetorisMagicEffects.PiercingShotValues),
new ShardSpec((ShardType)0x50520005, "Arrow Rain", "Stormcaller", PraetorisMagicEffects.ArrowRain,
ShardCategory.Unique, ItemRarity.Epic, new float[] { 1, 1, 1, 1 })
ShardCategory.Unique, ItemRarity.Epic, new float[] { 1, 1, 1, 1 }),
new ShardSpec((ShardType)0x50520006, "Adrenaline Echo", "Stormcaller", PraetorisMagicEffects.AdrenalineEcho,
ShardCategory.Unique, ItemRarity.Epic, new float[] { 1, 1, 1, 1 }, ShardSlotCategory.Trinket)
};

internal static void Initialize()
Expand Down Expand Up @@ -105,22 +107,36 @@ internal static void AddDefinitions(ShardStonesConfig config)
{
if (config.Shards.TryGetValue(spec.Id, out ShardDefinition existing))
{
if (existing?.UniformEffect?.EffectType != spec.Effect)
ShardEffectDefinition? existingEffect = existing?.UniformEffect;
if (spec.Slot.HasValue && existing?.TypeEffects != null)
{
existing.TypeEffects.TryGetValue(spec.Slot.Value, out existingEffect);
}
if (existingEffect?.EffectType != spec.Effect)
{
throw new InvalidOperationException($"Shardstone ID {spec.Id} is already in use.");
}
continue;
}
config.Shards.Add(spec.Id, new ShardDefinition
ShardEffectDefinition effect = new ShardEffectDefinition
{
EffectType = spec.Effect,
ValuesPerRarity = new Dictionary<ItemRarity, float>(spec.Values)
};
ShardDefinition definition = new ShardDefinition
{
Category = spec.Category,
Rarities = spec.Values.Keys.ToList(),
UniformEffect = new ShardEffectDefinition
{
EffectType = spec.Effect,
ValuesPerRarity = new Dictionary<ItemRarity, float>(spec.Values)
}
});
Rarities = spec.Values.Keys.ToList()
};
if (spec.Slot.HasValue)
{
definition.TypeEffects.Add(spec.Slot.Value, effect);
}
else
{
definition.UniformEffect = effect;
}
config.Shards.Add(spec.Id, definition);
}
}

Expand Down Expand Up @@ -175,16 +191,18 @@ private sealed class ShardSpec
internal readonly string Name;
internal readonly string Template;
internal readonly string Effect;
internal readonly ShardSlotCategory? Slot;
internal readonly ShardCategory Category;
internal readonly Dictionary<ItemRarity, float> Values = new Dictionary<ItemRarity, float>();

internal ShardSpec(ShardType id, string name, string template, string effect,
ShardCategory category, ItemRarity firstRarity, float[] values)
ShardCategory category, ItemRarity firstRarity, float[] values, ShardSlotCategory? slot = null)
{
Id = id;
Name = name;
Template = template;
Effect = effect;
Slot = slot;
Category = category;
for (int index = 0; index < values.Length; index++)
{
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "PraetorisClient",
"description": "Client-side Praetoris tools for Discord linking, creative server bridges, RPC tracing, socket metrics, and RPC probes.",
"version_number": "0.1.83",
"version_number": "0.1.84",
"website_url": "https://discord.gg/3aaru2VyHJ",
"dependencies": [
"denikson-BepInExPack_Valheim-5.4.2333",
Expand Down
2 changes: 1 addition & 1 deletion thunderstore.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ schemaVersion = "0.0.1"
[package]
namespace = "praetoris"
name = "PraetorisClient"
versionNumber = "0.1.83"
versionNumber = "0.1.84"
description = "Client-side Praetoris tools for Discord linking, creative server bridges, RPC tracing, socket metrics, and RPC probes."
websiteUrl = "https://discord.gg/3aaru2VyHJ"
containsNsfwContent = false
Expand Down
Loading