XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/DeathMod

修正死神挥斩命中与静止领域血痕

532e327
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

13 个文件 +431 -144
Modified Common/DeathDomainCrescentVisualSystem.cs +3 -1
@@ -141,7 +141,9 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
141 141
142 142 private static void BuildVertices(CrescentDrawState state)
143 143 {
144 float reveal = Smooth01(state.LifeProgress / 0.42f);
144 // The frontier uses the same eased 0..1 clock as the held blade. Revealing
145 // the whole face at 42% left the weapon floating far behind its hot edge.
146 float reveal = Smooth01(state.LifeProgress);
145 147 float erosion = Smooth01((state.LifeProgress - 0.30f) / 0.66f);
146 148 float rotationCos = (float)Math.Cos(state.Rotation);
147 149 float rotationSin = (float)Math.Sin(state.Rotation);
Added Common/DeathDomainTrailVisualSystem.cs +191 -0
@@ -0,0 +1,191 @@
1 using Microsoft.Xna.Framework;
2 using Microsoft.Xna.Framework.Graphics;
3 using System;
4 using System.Collections.Generic;
5 using Terraria;
6 using Terraria.ModLoader;
7
8 namespace DeathMod.Common;
9
10 /// <summary>
11 /// Clips the stationary Death Domain backdrop into an arbitrary blade-tip ribbon.
12 /// World-space texture coordinates keep the scenery fixed while the red rim is
13 /// drawn later by the owning projectile.
14 /// </summary>
15 [Autoload(Side = ModSide.Client)]
16 internal sealed class DeathDomainTrailVisualSystem : ModSystem
17 {
18 private const int MaximumPoints = 24;
19 private static readonly Dictionary<long, TrailDrawState> drawStates = [];
20 private static readonly VertexPositionColorTexture[] vertices =
21 new VertexPositionColorTexture[MaximumPoints * 2];
22 private static readonly short[] indices = CreateIndices();
23 private static BasicEffect? effect;
24
25 private readonly record struct TrailDrawState(
26 IReadOnlyList<Vector2> Points,
27 float Width,
28 float Opacity,
29 ulong UpdateTick);
30
31 internal static void Record(int owner, int identity,
32 IReadOnlyList<Vector2> points, float width, float opacity)
33 {
34 if (Main.dedServ || points.Count < 2 || width <= 0.5f
35 || opacity <= 0.001f)
36 {
37 return;
38 }
39 long key = ((long)owner << 32) | (uint)identity;
40 drawStates[key] = new TrailDrawState(points, width, opacity,
41 Main.GameUpdateCount);
42 }
43
44 public override void PostUpdateEverything()
45 {
46 if (drawStates.Count == 0)
47 return;
48 List<long>? stale = null;
49 foreach ((long key, TrailDrawState state) in drawStates)
50 {
51 if (Main.GameUpdateCount <= state.UpdateTick + 1)
52 continue;
53 stale ??= [];
54 stale.Add(key);
55 }
56 if (stale is null)
57 return;
58 foreach (long key in stale)
59 drawStates.Remove(key);
60 }
61
62 public override void OnWorldUnload() => drawStates.Clear();
63
64 public override void Unload()
65 {
66 drawStates.Clear();
67 BasicEffect? oldEffect = effect;
68 effect = null;
69 if (oldEffect is not null && !Main.dedServ)
70 Main.QueueMainThreadAction(oldEffect.Dispose);
71 }
72
73 public override void PostDrawTiles()
74 {
75 if (Main.gameMenu || drawStates.Count == 0
76 || Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
77 {
78 return;
79 }
80
81 effect ??= new BasicEffect(graphicsDevice)
82 {
83 TextureEnabled = true,
84 VertexColorEnabled = true,
85 LightingEnabled = false,
86 FogEnabled = false
87 };
88 effect.World = Main.GameViewMatrix.TransformationMatrix;
89 effect.View = Matrix.Identity;
90 effect.Projection = Matrix.CreateOrthographicOffCenter(
91 0f, Main.screenWidth, Main.screenHeight, 0f, 0f, 1f);
92 graphicsDevice.BlendState = BlendState.AlphaBlend;
93 graphicsDevice.DepthStencilState = DepthStencilState.None;
94 graphicsDevice.RasterizerState = RasterizerState.CullNone;
95 graphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
96
97 foreach (TrailDrawState state in drawStates.Values)
98 {
99 if (Main.GameUpdateCount > state.UpdateTick + 1)
100 continue;
101 int pointCount = Math.Min(MaximumPoints, state.Points.Count);
102 if (pointCount < 2)
103 continue;
104 DrawSolidRibbon(graphicsDevice, state, state.Width * 1.72f,
105 new Color(174, 0, 44, 0) * (state.Opacity * 0.24f));
106 DrawSolidRibbon(graphicsDevice, state, state.Width * 1.40f,
107 new Color(244, 12, 66, 235) * state.Opacity);
108 DrawSolidRibbon(graphicsDevice, state, state.Width * 1.17f,
109 new Color(255, 187, 178, 235) * (state.Opacity * 0.88f));
110 BuildVertices(state, state.Width, Color.White * state.Opacity);
111 effect.TextureEnabled = true;
112 for (int layer = 0; layer < 3; layer++)
113 {
114 effect.Texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
115 foreach (EffectPass pass in effect.CurrentTechnique.Passes)
116 {
117 pass.Apply();
118 graphicsDevice.DrawUserIndexedPrimitives(
119 PrimitiveType.TriangleList, vertices, 0, pointCount * 2,
120 indices, 0, (pointCount - 1) * 2);
121 }
122 }
123 }
124 }
125
126 private static void DrawSolidRibbon(GraphicsDevice graphicsDevice,
127 TrailDrawState state, float width, Color color)
128 {
129 if (effect is null)
130 return;
131 int pointCount = BuildVertices(state, width, color);
132 effect.TextureEnabled = false;
133 foreach (EffectPass pass in effect.CurrentTechnique.Passes)
134 {
135 pass.Apply();
136 graphicsDevice.DrawUserIndexedPrimitives(
137 PrimitiveType.TriangleList, vertices, 0, pointCount * 2,
138 indices, 0, (pointCount - 1) * 2);
139 }
140 }
141
142 private static int BuildVertices(TrailDrawState state, float width,
143 Color color)
144 {
145 int count = Math.Min(MaximumPoints, state.Points.Count);
146 for (int index = 0; index < count; index++)
147 {
148 Vector2 point = state.Points[index];
149 Vector2 previous = state.Points[Math.Max(0, index - 1)];
150 Vector2 next = state.Points[Math.Min(count - 1, index + 1)];
151 Vector2 tangent = (next - previous).SafeNormalize(Vector2.UnitX);
152 Vector2 normal = tangent.RotatedBy(MathHelper.PiOver2);
153 float progress = index / Math.Max(1f, count - 1f);
154 float taper = (float)Math.Pow(Math.Max(0f,
155 Math.Sin(progress * MathHelper.Pi)), 0.34f);
156 float halfWidth = width * taper * 0.5f;
157
158 for (int side = 0; side < 2; side++)
159 {
160 Vector2 world = point + normal * (side == 0 ? -halfWidth : halfWidth);
161 Vector2 screen = world - Main.screenPosition;
162 Vector2 uv = new(
163 world.X / DeathDomainBackdropTextureSystem.LayerWidth,
164 world.Y / DeathDomainBackdropTextureSystem.LayerHeight);
165 vertices[index * 2 + side] = new VertexPositionColorTexture(
166 new Vector3(screen, 0f), color, uv);
167 }
168 }
169 return count;
170 }
171
172 private static short[] CreateIndices()
173 {
174 short[] result = new short[(MaximumPoints - 1) * 6];
175 int cursor = 0;
176 for (short index = 0; index < MaximumPoints - 1; index++)
177 {
178 short leftTop = (short)(index * 2);
179 short leftBottom = (short)(leftTop + 1);
180 short rightTop = (short)(leftTop + 2);
181 short rightBottom = (short)(leftTop + 3);
182 result[cursor++] = leftTop;
183 result[cursor++] = leftBottom;
184 result[cursor++] = rightTop;
185 result[cursor++] = rightTop;
186 result[cursor++] = leftBottom;
187 result[cursor++] = rightBottom;
188 }
189 return result;
190 }
191 }
Modified Common/MyPlayer.cs +12 -7
@@ -1033,8 +1033,7 @@ public class MyPlayer : ModPlayer
1033 1033 if (pending.NpcIndex < 0 || pending.NpcIndex >= Main.maxNPCs)
1034 1034 continue;
1035 1035 NPC target = Main.npc[pending.NpcIndex];
1036 if (!target.active || target.friendly || target.immortal
1037 || target.dontTakeDamage || target.life <= 0)
1036 if (!ReaperTargeting.IsValidWeaponTarget(target))
1038 1037 {
1039 1038 continue;
1040 1039 }
@@ -1220,7 +1219,7 @@ public class MyPlayer : ModPlayer
1220 1219 return;
1221 1220
1222 1221 NPC target = Main.npc[npcIndex];
1223 if (!target.active || target.friendly || target.immortal || target.dontTakeDamage || target.life <= 0)
1222 if (!ReaperTargeting.IsValidWeaponTarget(target))
1224 1223 return;
1225 1224
1226 1225 bool bossOrBossPart = target.boss
@@ -1301,13 +1300,17 @@ public class MyPlayer : ModPlayer
1301 1300 // chaseable is intentionally not used: vanilla NPCs toggle it during
1302 1301 // movement and transition states, which made dedicated-server harvests
1303 1302 // appear to skip otherwise valid enemies from one cycle to the next.
1304 if (npc.friendly || npc.immortal || npc.dontTakeDamage || npc.lifeMax <= 5 || npc.life <= 0)
1303 // Training dummies are excluded from the passive necklace domain even
1304 // though weapon-created Death cuts can explicitly strike them.
1305 if (ReaperTargeting.IsTrainingDummy(npc) || npc.friendly || npc.immortal
1306 || npc.dontTakeDamage || npc.lifeMax <= 5 || npc.life <= 0)
1305 1307 continue;
1306 1308
1307 1309 NPC target = npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].active
1308 1310 ? Main.npc[npc.realLife]
1309 1311 : npc;
1310 if (target.friendly || target.immortal || target.dontTakeDamage || target.lifeMax <= 5 || target.life <= 0
1312 if (ReaperTargeting.IsTrainingDummy(target) || target.friendly
1313 || target.immortal || target.dontTakeDamage || target.lifeMax <= 5 || target.life <= 0
1311 1314 || selectedTargets.Contains(target.whoAmI)
1312 1315 || !harvestEntireWorld && Vector2.DistanceSquared(Player.Center, target.Center) > radiusSquared)
1313 1316 {
@@ -1331,13 +1334,15 @@ public class MyPlayer : ModPlayer
1331 1334
1332 1335 foreach (NPC npc in Main.ActiveNPCs)
1333 1336 {
1334 if (npc.friendly || npc.immortal || npc.dontTakeDamage || npc.lifeMax <= 5 || npc.life <= 0)
1337 if (ReaperTargeting.IsTrainingDummy(npc) || npc.friendly || npc.immortal
1338 || npc.dontTakeDamage || npc.lifeMax <= 5 || npc.life <= 0)
1335 1339 continue;
1336 1340
1337 1341 NPC target = npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].active
1338 1342 ? Main.npc[npc.realLife]
1339 1343 : npc;
1340 if (target.friendly || target.immortal || target.dontTakeDamage || target.lifeMax <= 5 || target.life <= 0
1344 if (ReaperTargeting.IsTrainingDummy(target) || target.friendly
1345 || target.immortal || target.dontTakeDamage || target.lifeMax <= 5 || target.life <= 0
1341 1346 || !affectedTargets.Add(target.whoAmI))
1342 1347 {
1343 1348 continue;
Modified Common/ReaperCombatDefinitions.cs +14 -1
@@ -128,6 +128,16 @@ public readonly record struct SickleCombatSnapshot(
128 128
129 129 public static class ReaperCombatRegistry
130 130 {
131 // Death is intentionally presented at a boss-weapon scale. Keep the held,
132 // special, ultimate and phantom renderers on this single value so its blade
133 // can never drift away from the matching crescent/collision reach again.
134 public const float DeathWeaponDrawScale = 2.58f;
135 public const float CrescentOuterRadiusRatio = 0.91f;
136 public const float DeathPrimaryCrescentRadius = 645f;
137 public const float DeathPrimaryBladeReach = DeathPrimaryCrescentRadius
138 * CrescentOuterRadiusRatio;
139 public const float DeathPrimaryCollisionReach = 645f;
140
131 141 public static ReaperFormId ResolveUsableForm(ReaperProgressionState progression)
132 142 {
133 143 ReaperFormId form = progression.CurrentForm;
@@ -148,7 +158,10 @@ public static class ReaperCombatRegistry
148 158 public static int GetUseTime(ReaperFormId form, ReaperStage stage, int phase = 0)
149 159 {
150 160 if (form == ReaperFormId.Death)
151 return phase switch { 3 => 28, 4 => 30, 5 => 34, _ => 24 };
161 // Every primary beat shares one clock. Phase-dependent use times made
162 // auto-reuse alternately wait for the held projectile and then race it,
163 // which was perceived as random attack frequency at high tempo.
164 return 24;
152 165 return ReaperDefinitions.GetForm(form).BaseUseTime;
153 166 }
154 167
Modified Common/ReaperCombatService.cs +7 -0
@@ -514,6 +514,13 @@ public static class ReaperCombatService
514 514 modPlayer.RecordDeathCycleHit(data.ReaperPhase);
515 515 modPlayer.QueueDeathNecklaceHarvestFromReaperHit(target,
516 516 data.AttackAngle);
517 float primaryTempo = projectile.ModProjectile
518 is SickleSwingProjectile swing ? swing.DeathTempo : 1f;
519 ReaperDeathPhantomProjectile.Spawn(
520 projectile.GetSource_FromThis(), player.whoAmI, snapshot,
521 target.Center, data.AttackAngle.ToRotationVector2(),
522 primaryTempo, unchecked(data.ReaperActionId * 509
523 + target.whoAmI * 31 + data.ReaperPhase));
517 524 if (data.ReaperPhase == 1)
518 525 player.GetModPlayer<ReaperCombatPlayer>().ApplyDeathBloodHealing(damageDone);
519 526 if (data.ReaperPhase == 2)
Modified Common/ReaperCrescentPrimitiveTextureSystem.cs +2 -2
@@ -466,7 +466,7 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
466 466 Color[] pixels = new Color[TextureSize * TextureSize];
467 467 Color[] surface = deathDomainSurface ??= CreateDeathDomainSurface();
468 468 float antialias = 3f / TextureSize;
469 float reveal = Smooth01(lifeProgress / 0.42f);
469 float reveal = Smooth01(lifeProgress);
470 470 float erosion = Smooth01((lifeProgress - 0.30f) / 0.66f);
471 471 float edgeFade = 1f - Smooth01((lifeProgress - 0.96f) / 0.04f);
472 472
@@ -521,7 +521,7 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
521 521 TextureSize, false, SurfaceFormat.Color);
522 522 Color[] pixels = new Color[TextureSize * TextureSize];
523 523 float antialias = 3f / TextureSize;
524 float reveal = Smooth01(lifeProgress / 0.42f);
524 float reveal = Smooth01(lifeProgress);
525 525 float edgeFade = 1f - Smooth01((lifeProgress - 0.96f) / 0.04f);
526 526
527 527 for (int y = 0; y < TextureSize; y++)
Added Common/ReaperTargeting.cs +24 -0
@@ -0,0 +1,24 @@
1 using Terraria;
2 using Terraria.ID;
3
4 namespace DeathMod.Common;
5
6 internal static class ReaperTargeting
7 {
8 internal static bool IsTrainingDummy(NPC npc)
9 => npc.type == NPCID.TargetDummy;
10
11 /// <summary>
12 /// Weapon-created attacks are allowed to acquire and repeatedly strike the
13 /// vanilla training dummy. Passive world/domain scans deliberately use their
14 /// own stricter filter and must never call this helper for inclusion.
15 /// </summary>
16 internal static bool IsValidWeaponTarget(NPC npc)
17 {
18 if (!npc.active || npc.life <= 0 || npc.dontTakeDamage)
19 return false;
20 if (IsTrainingDummy(npc))
21 return true;
22 return !npc.friendly && !npc.immortal && npc.lifeMax > 5;
23 }
24 }
Modified Projectiles/ReaperActionControllerProjectile.cs +4 -7
@@ -164,7 +164,7 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
164 164 Vector2 normalizedAnchor = ReaperCombatRegistry.GetHandleAnchor(snapshot.Form, snapshot.Stage);
165 165 Vector2 anchor = new(texture.Width * normalizedAnchor.X, texture.Height * normalizedAnchor.Y);
166 166 float scale = snapshot.Form == ReaperFormId.Death
167 ? 1.72f
167 ? ReaperCombatRegistry.DeathWeaponDrawScale
168 168 : 0.9f + snapshot.StageNumber * 0.045f;
169 169 float correctedAngle = visualWeaponAngle
170 170 + ReaperCombatRegistry.GetTextureRotationCorrection(snapshot.Form, snapshot.Stage);
@@ -1267,12 +1267,9 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
1267 1267 int direction = (cut & 1) == 0 ? facing : -facing;
1268 1268 float visibility = SmoothStep(MathHelper.Clamp(local / 0.22f, 0f, 1f))
1269 1269 * (1f - SmoothStep(MathHelper.Clamp((local - 0.76f) / 0.24f, 0f, 1f)));
1270 float radius = 430f;
1271 Vector2 normal = aim.RotatedBy(MathHelper.PiOver2);
1272 Vector2 center = grip + aim * radius * 0.06f
1273 - normal * direction * radius * 0.10f;
1274 float rotation = aim.ToRotation()
1275 + MathHelper.Lerp(-0.11f, 0.09f, SmoothStep(local)) * direction;
1270 float radius = ReaperCombatRegistry.DeathPrimaryCrescentRadius;
1271 Vector2 center = grip;
1272 float rotation = aim.ToRotation();
1276 1273 ReaperCrescentPrimitiveTextureSystem.DrawDeathDomainCrescent(center,
1277 1274 rotation, radius, visibility * 0.94f, 3, direction,
1278 1275 Main.GlobalTimeWrappedHourly, local);
Modified Projectiles/ReaperBloodBladeProjectile.cs +18 -2
@@ -19,6 +19,9 @@ namespace DeathMod.Projectiles;
19 19 public sealed class ReaperBloodBladeProjectile : ModProjectile
20 20 {
21 21 private const int MaximumTrailPoints = 240;
22 private const int HomingDelayFrames = 10;
23 private const float TargetSearchRadius = 560f;
24 private const float TargetRetentionRadius = 720f;
22 25
23 26 private sealed class TrailPoint
24 27 {
@@ -297,10 +300,23 @@ public sealed class ReaperBloodBladeProjectile : ModProjectile
297 300 if (targetIndex >= 0 && targetIndex < Main.maxNPCs)
298 301 {
299 302 NPC current = Main.npc[targetIndex];
300 if (current.active && current.CanBeChasedBy(Projectile))
303 if (ReaperTargeting.IsValidWeaponTarget(current)
304 && (ReaperTargeting.IsTrainingDummy(current)
305 || current.CanBeChasedBy(Projectile))
306 && Vector2.DistanceSquared(Projectile.Center, current.Center)
307 <= TargetRetentionRadius * TargetRetentionRadius)
301 308 return current;
309 targetIndex = -1;
302 310 }
303 NPC? target = ReaperProjectileHelper.FindTarget(Projectile, 1800f);
311
312 // Give the blade a readable launch phase, then acquire only from a local
313 // bubble centred on its current position. It no longer turns immediately
314 // toward an enemy which happened to exist far across the screen at spawn.
315 if (activeTimer < HomingDelayFrames)
316 return null;
317
318 NPC? target = ReaperProjectileHelper.FindTarget(Projectile,
319 TargetSearchRadius);
304 320 targetIndex = target?.whoAmI ?? -1;
305 321 if (Main.netMode != NetmodeID.MultiplayerClient)
306 322 Projectile.netUpdate = true;
Modified Projectiles/ReaperDeathDomainRiftProjectile.cs +70 -67
@@ -11,19 +11,18 @@ using Terraria.ModLoader;
11 11 namespace DeathMod.Projectiles;
12 12
13 13 /// <summary>
14 /// A max-tempo Death swing leaves this world-space opening behind. The opening
15 /// is a window into the Death Domain, and every necklace cadence it applies the
16 /// necklace's complete harvest volley to every enemy intersecting the arc.
14 /// The immutable world-space wound traced by the tip of a max-tempo Death swing.
15 /// Its red edges enclose the Death Domain backdrop; enemies crossing the exact
16 /// recorded curve receive the equipped necklace's complete harvest cadence.
17 17 /// </summary>
18 18 public sealed class ReaperDeathDomainRiftProjectile : ModProjectile
19 19 {
20 20 private const int Lifetime = 300;
21 private const int ArcSamples = 56;
22 private const float HalfSweep = 2.43f;
21 private const int MaximumPoints = 24;
22 private const float TrailWidth = 30f;
23 23 private readonly int[] harvestCooldowns = new int[Main.maxNPCs];
24 private readonly List<Vector2> points = [];
24 25 private SickleCombatSnapshot snapshot;
25 private float radius;
26 private int swingDirection;
27 26 private int phase;
28 27 private int actionId;
29 28 private int age;
@@ -33,27 +32,49 @@ public sealed class ReaperDeathDomainRiftProjectile : ModProjectile
33 32
34 33 public override string Texture => "Terraria/Images/Projectile_0";
35 34
36 internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner,
37 in SickleCombatSnapshot snapshot, Vector2 center, float rotation,
38 float radius, int swingDirection, int phase, int actionId)
35 internal static int Spawn(Terraria.DataStructures.IEntitySource source,
36 int owner, in SickleCombatSnapshot snapshot,
37 Vector2[] newestFirstPoints, int validPointCount, int phase, int actionId)
39 38 {
40 if (Main.netMode == NetmodeID.MultiplayerClient)
39 if (Main.netMode == NetmodeID.MultiplayerClient || validPointCount < 2)
41 40 return -1;
42 int index = Projectile.NewProjectile(source, center, rotation.ToRotationVector2(),
43 ModContent.ProjectileType<ReaperDeathDomainRiftProjectile>(),
41
42 List<Vector2> ordered = [];
43 int usable = Math.Min(Math.Min(validPointCount, newestFirstPoints.Length),
44 MaximumPoints);
45 for (int index = usable - 1; index >= 0; index--)
46 {
47 Vector2 point = newestFirstPoints[index];
48 if (!float.IsFinite(point.X) || !float.IsFinite(point.Y))
49 continue;
50 if (ordered.Count == 0
51 || Vector2.DistanceSquared(ordered[^1], point) >= 5f * 5f)
52 {
53 ordered.Add(point);
54 }
55 }
56 if (ordered.Count < 2)
57 return -1;
58
59 Vector2 center = Vector2.Zero;
60 foreach (Vector2 point in ordered)
61 center += point;
62 center /= ordered.Count;
63 int projectileIndex = Projectile.NewProjectile(source, center,
64 Vector2.Zero, ModContent.ProjectileType<ReaperDeathDomainRiftProjectile>(),
44 65 0, 0f, owner);
45 if (index < 0 || index >= Main.maxProjectiles)
66 if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
46 67 return -1;
47 Projectile projectile = Main.projectile[index];
68 Projectile projectile = Main.projectile[projectileIndex];
48 69 if (projectile.ModProjectile is ReaperDeathDomainRiftProjectile rift)
49 rift.Configure(snapshot, rotation, radius, swingDirection, phase, actionId);
70 rift.Configure(snapshot, ordered, phase, actionId);
50 71 projectile.netUpdate = true;
51 return index;
72 return projectileIndex;
52 73 }
53 74
54 75 public override void SetStaticDefaults()
55 76 {
56 ProjectileID.Sets.DrawScreenCheckFluff[Type] = 2200;
77 ProjectileID.Sets.DrawScreenCheckFluff[Type] = 2400;
57 78 }
58 79
59 80 public override void SetDefaults()
@@ -82,23 +103,18 @@ public sealed class ReaperDeathDomainRiftProjectile : ModProjectile
82 103 if (!configured)
83 104 return;
84 105 age++;
106 float opacity = GetOpacity();
107 if (Main.netMode != NetmodeID.Server)
108 {
109 DeathDomainTrailVisualSystem.Record(Projectile.owner,
110 Projectile.identity, points, TrailWidth, opacity);
111 }
85 112 if (Main.netMode != NetmodeID.MultiplayerClient)
86 113 TryHarvestIntersectingEnemies();
87 114 }
88 115
89 116 public override bool PreDraw(ref Color lightColor)
90 {
91 if (!configured || Main.dedServ)
92 return false;
93 float reveal = Smooth01(age / 10f);
94 float fade = Smooth01(Projectile.timeLeft / 44f);
95 float pulse = 0.96f + (float)Math.Sin(
96 Main.GlobalTimeWrappedHourly * 4.8f + actionId * 0.013f) * 0.04f;
97 ReaperCrescentPrimitiveTextureSystem.DrawDeathDomainRiftCrescent(
98 Projectile.Center, Projectile.rotation, radius * pulse,
99 reveal * fade, swingDirection);
100 return false;
101 }
117 => false;
102 118
103 119 public override void SendExtraAI(BinaryWriter writer)
104 120 {
@@ -106,13 +122,13 @@ public sealed class ReaperDeathDomainRiftProjectile : ModProjectile
106 122 if (!configured)
107 123 return;
108 124 snapshot.Write(writer);
109 writer.Write(radius);
110 writer.Write((sbyte)swingDirection);
111 125 writer.Write((byte)Math.Clamp(phase, 0, byte.MaxValue));
112 126 writer.Write(actionId);
113 127 writer.Write((short)Math.Clamp(age, 0, short.MaxValue));
114 128 writer.Write((short)Math.Clamp(volleyCounter, 0, short.MaxValue));
115 writer.Write(Projectile.rotation);
129 writer.Write((byte)Math.Min(points.Count, MaximumPoints));
130 for (int index = 0; index < points.Count && index < MaximumPoints; index++)
131 writer.WriteVector2(points[index]);
116 132 }
117 133
118 134 public override void ReceiveExtraAI(BinaryReader reader)
@@ -125,39 +141,38 @@ public sealed class ReaperDeathDomainRiftProjectile : ModProjectile
125 141 return;
126 142 }
127 143 SickleCombatSnapshot incomingSnapshot = SickleCombatSnapshot.Read(reader);
128 float incomingRadius = reader.ReadSingle();
129 int incomingDirection = reader.ReadSByte();
130 144 int incomingPhase = reader.ReadByte();
131 145 int incomingAction = reader.ReadInt32();
132 146 int incomingAge = reader.ReadInt16();
133 147 int incomingVolley = reader.ReadInt16();
134 float incomingRotation = reader.ReadSingle();
148 int count = reader.ReadByte();
149 List<Vector2> incomingPoints = [];
150 for (int index = 0; index < count; index++)
151 incomingPoints.Add(reader.ReadVector2());
135 152 if (Main.netMode == NetmodeID.Server)
136 153 return;
137 154 snapshot = incomingSnapshot;
138 radius = incomingRadius;
139 swingDirection = incomingDirection < 0 ? -1 : 1;
140 155 phase = incomingPhase;
141 156 actionId = incomingAction;
142 157 age = incomingAge;
143 158 volleyCounter = incomingVolley;
144 Projectile.rotation = incomingRotation;
145 configured = true;
159 points.Clear();
160 points.AddRange(incomingPoints);
161 configured = points.Count >= 2;
146 162 }
147 163
148 private void Configure(in SickleCombatSnapshot value, float rotation,
149 float riftRadius, int direction, int sourcePhase, int sourceActionId)
164 private void Configure(in SickleCombatSnapshot value, List<Vector2> path,
165 int sourcePhase, int sourceActionId)
150 166 {
151 167 snapshot = value;
152 radius = MathHelper.Clamp(riftRadius, 160f, 720f);
153 swingDirection = direction < 0 ? -1 : 1;
168 points.Clear();
169 points.AddRange(path);
154 170 phase = Math.Max(0, sourcePhase);
155 171 actionId = sourceActionId;
156 172 age = 0;
157 173 volleyCounter = 0;
158 Projectile.rotation = rotation;
159 174 Projectile.timeLeft = Lifetime;
160 configured = true;
175 configured = points.Count >= 2;
161 176 serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
162 177 }
163 178
@@ -184,15 +199,15 @@ public sealed class ReaperDeathDomainRiftProjectile : ModProjectile
184 199 List<NPC> targets = [];
185 200 foreach (NPC npc in Main.ActiveNPCs)
186 201 {
187 if (npc.friendly || npc.immortal || npc.dontTakeDamage
188 || npc.lifeMax <= 5 || npc.life <= 0 || !IntersectsArc(npc.Hitbox))
202 if (!ReaperTargeting.IsValidWeaponTarget(npc)
203 || !IntersectsPath(npc.Hitbox))
189 204 {
190 205 continue;
191 206 }
192 207 NPC target = npc.realLife >= 0 && npc.realLife < Main.maxNPCs
193 208 && Main.npc[npc.realLife].active ? Main.npc[npc.realLife] : npc;
194 if (target.friendly || target.immortal || target.dontTakeDamage
195 || target.life <= 0 || !selected.Add(target.whoAmI)
209 if (!ReaperTargeting.IsValidWeaponTarget(target)
210 || !selected.Add(target.whoAmI)
196 211 || harvestCooldowns[target.whoAmI] > 0)
197 212 {
198 213 continue;
@@ -218,34 +233,22 @@ public sealed class ReaperDeathDomainRiftProjectile : ModProjectile
218 233 Projectile.netUpdate = true;
219 234 }
220 235
221 private bool IntersectsArc(Rectangle target)
236 private bool IntersectsPath(Rectangle target)
222 237 {
223 Vector2 previous = GetArcPoint(0);
224 238 float collisionPoint = 0f;
225 for (int index = 1; index < ArcSamples; index++)
239 for (int index = 1; index < points.Count; index++)
226 240 {
227 Vector2 current = GetArcPoint(index);
228 241 if (Collision.CheckAABBvLineCollision(target.TopLeft(), target.Size(),
229 previous, current, 32f, ref collisionPoint))
242 points[index - 1], points[index], TrailWidth, ref collisionPoint))
230 243 {
231 244 return true;
232 245 }
233 previous = current;
234 246 }
235 247 return false;
236 248 }
237 249
238 private Vector2 GetArcPoint(int index)
239 {
240 float progress = index / (float)(ArcSamples - 1);
241 float angle = MathHelper.Lerp(-HalfSweep, HalfSweep, progress);
242 float taper = (float)Math.Pow(Math.Max(0f,
243 Math.Sin(progress * MathHelper.Pi)), 0.67f);
244 float pathRadius = radius * (1f - 0.082f * taper);
245 Vector2 local = new((float)Math.Cos(angle),
246 (float)Math.Sin(angle) * swingDirection);
247 return Projectile.Center + local.RotatedBy(Projectile.rotation) * pathRadius;
248 }
250 private float GetOpacity()
251 => Smooth01(age / 8f) * Smooth01(Projectile.timeLeft / 44f);
249 252
250 253 private static float Smooth01(float value)
251 254 {
Modified Projectiles/ReaperDeathPhantomProjectile.cs +6 -4
@@ -134,11 +134,11 @@ public sealed class ReaperDeathPhantomProjectile : ModProjectile
134 134 Color shadow = new Color(32, 0, 18, 0) * (opacity * 0.58f);
135 135 Color edge = new Color(245, 24, 80, 0) * (opacity * 0.72f);
136 136 Main.EntitySpriteDraw(texture, drawPosition, null, shadow,
137 weaponAngle + correction - 0.12f, origin, 0.72f, effects);
137 weaponAngle + correction - 0.12f, origin, 1.08f, effects);
138 138 Main.EntitySpriteDraw(texture, drawPosition, null, edge,
139 weaponAngle + correction, origin, 0.62f, effects);
139 weaponAngle + correction, origin, 0.93f, effects);
140 140 Main.EntitySpriteDraw(texture, drawPosition, null, Color.White * (opacity * 0.34f),
141 weaponAngle + correction, origin, 0.57f, effects);
141 weaponAngle + correction, origin, 0.855f, effects);
142 142 return false;
143 143 }
144 144
@@ -206,7 +206,9 @@ public sealed class ReaperDeathPhantomProjectile : ModProjectile
206 206 float best = maximumDistance * maximumDistance;
207 207 foreach (NPC npc in Main.ActiveNPCs)
208 208 {
209 if (!npc.CanBeChasedBy(Projectile))
209 if (!ReaperTargeting.IsValidWeaponTarget(npc)
210 || !ReaperTargeting.IsTrainingDummy(npc)
211 && !npc.CanBeChasedBy(Projectile))
210 212 continue;
211 213 float distance = Vector2.DistanceSquared(npc.Center, anchor);
212 214 if (distance >= best)
Modified Projectiles/ReaperProjectileHelper.cs +4 -1
@@ -1,5 +1,6 @@
1 1 using Microsoft.Xna.Framework;
2 2 using Microsoft.Xna.Framework.Graphics;
3 using DeathMod.Common;
3 4 using Terraria;
4 5 using Terraria.GameContent;
5 6
@@ -12,7 +13,9 @@ internal static class ReaperProjectileHelper
12 13 NPC? target = null;
13 14 foreach (NPC npc in Main.ActiveNPCs)
14 15 {
15 if (!npc.CanBeChasedBy(projectile))
16 if (!ReaperTargeting.IsValidWeaponTarget(npc)
17 || !ReaperTargeting.IsTrainingDummy(npc)
18 && !npc.CanBeChasedBy(projectile))
16 19 continue;
17 20 float distance = Vector2.Distance(npc.Center, projectile.Center);
18 21 if (distance >= maximumDistance)
Modified Projectiles/SickleSwingProjectile.cs +76 -52