返回提交历史
Modified
Common/DeathDomainCrescentVisualSystem.cs
+3
-1
Added
Common/DeathDomainTrailVisualSystem.cs
+191
-0
Modified
Common/MyPlayer.cs
+12
-7
Modified
Common/ReaperCombatDefinitions.cs
+14
-1
Modified
Common/ReaperCombatService.cs
+7
-0
Modified
Common/ReaperCrescentPrimitiveTextureSystem.cs
+2
-2
Added
Common/ReaperTargeting.cs
+24
-0
Modified
Projectiles/ReaperActionControllerProjectile.cs
+4
-7
Modified
Projectiles/ReaperBloodBladeProjectile.cs
+18
-2
Modified
Projectiles/ReaperDeathDomainRiftProjectile.cs
+70
-67
Modified
Projectiles/ReaperDeathPhantomProjectile.cs
+6
-4
Modified
Projectiles/ReaperProjectileHelper.cs
+4
-1
Modified
Projectiles/SickleSwingProjectile.cs
+76
-52
XFEstudio/DeathMod
修正死神挥斩命中与静止领域血痕
532e327
代码差异
13 个文件
+431
-144
@@ -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);
@@ -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
}
@@ -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;
@@ -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
@@ -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)
@@ -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++)
@@ -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
}
@@ -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);
@@ -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;
@@ -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
{
@@ -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)
@@ -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)