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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

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

XFEstudio/DeathMod

统一死神领域刀光并重制终焉斩击

981411c
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

10 个文件 +340 -119
Modified Common/DeathDomainBackdropTextureSystem.cs +86 -0
@@ -1,3 +1,4 @@
1 using DeathMod.Items;
1 2 using Microsoft.Xna.Framework;
2 3 using Microsoft.Xna.Framework.Graphics;
3 4 using System;
@@ -16,6 +17,9 @@ internal sealed class DeathDomainBackdropTextureSystem : ModSystem
16 17 {
17 18 internal const int LayerWidth = 4096;
18 19 internal const int LayerHeight = 1024;
20 internal const float ReferenceDomainDiameter =
21 (160f + DeathNecklace.RadiusGrowthPerLevel
22 * (DeathNecklace.MaxCoreLevel - 1)) * 2f;
19 23
20 24 private static readonly Texture2D?[] layers = new Texture2D?[3];
21 25
@@ -25,6 +29,88 @@ internal sealed class DeathDomainBackdropTextureSystem : ModSystem
25 29 return layers[index] ??= CreateLayer(index);
26 30 }
27 31
32 /// <summary>
33 /// Maps a world point into the exact parallax plate framing used by the Death
34 /// Necklace. Weapon crescents and persistent wounds use this instead of raw
35 /// texture-size UVs, so all three layers have the same scale, anchor and
36 /// parallax as the domain rather than forming a differently scaled collage.
37 /// </summary>
38 internal static Vector2 GetWorldUv(int layer, Vector2 world,
39 Vector2 anchorWorld, Vector2 apertureSize)
40 {
41 apertureSize.X = Math.Max(1f, apertureSize.X);
42 apertureSize.Y = Math.Max(1f, apertureSize.Y);
43 GetSourceFrame(layer, anchorWorld, apertureSize,
44 out float sourceOffsetX, out float sourceOffsetY,
45 out float viewWidth, out float viewHeight);
46 Vector2 normalized = (world - anchorWorld) / apertureSize
47 + new Vector2(0.5f);
48 return new Vector2(
49 (sourceOffsetX + normalized.X * viewWidth) / LayerWidth,
50 (sourceOffsetY + normalized.Y * viewHeight) / LayerHeight);
51 }
52
53 internal static Vector2 GetWorldUv(int layer, Vector2 world,
54 Vector2 anchorWorld)
55 => GetWorldUv(layer, world, anchorWorld,
56 new Vector2(ReferenceDomainDiameter));
57
58 /// <summary>
59 /// Uses the active owner's Death Necklace aperture when one exists. A
60 /// descended domain is sampled against exactly the current viewport; a
61 /// circular domain uses the same reference aperture as the necklace. Every
62 /// weapon mask belonging to that owner therefore reveals the same parallax
63 /// pixel at the same world position.
64 /// </summary>
65 internal static Vector2 GetMatchingDomainUv(int layer, Vector2 world,
66 int owner, Vector2 fallbackAnchor)
67 {
68 Vector2 anchor = fallbackAnchor;
69 Vector2 aperture = new(ReferenceDomainDiameter);
70 if (owner >= 0 && owner < Main.maxPlayers
71 && Main.player[owner] is Player player && player.active)
72 {
73 anchor = player.Center;
74 DeathDomainVisualSystem.TryGetMatchingBackdropAperture(owner,
75 out anchor, out aperture);
76 }
77 return GetWorldUv(layer, world, anchor, aperture);
78 }
79
80 internal static float GetLayerOpacity(int layer)
81 => layer switch { 0 => 1f, 1 => 0.9f, _ => 0.96f };
82
83 /// <summary>Returns the necklace's source rectangle for a world aperture.</summary>
84 internal static void GetSourceFrame(int layer, Vector2 worldCenter,
85 Vector2 apertureSize, out float sourceOffsetX,
86 out float sourceOffsetY, out float viewWidth, out float viewHeight)
87 {
88 layer = Math.Clamp(layer, 0, layers.Length - 1);
89 float referenceViewWidth = 1024f
90 * (layer switch { 0 => 0.68f, 1 => 0.9f, _ => 0.84f });
91 float referenceViewHeight = 512f
92 * (layer switch { 0 => 0.78f, 1 => 0.9f, _ => 0.86f });
93 float horizontalScale = Math.Max(0.001f,
94 apertureSize.X / ReferenceDomainDiameter);
95 float verticalScale = Math.Max(0.001f,
96 apertureSize.Y / ReferenceDomainDiameter);
97 viewWidth = Math.Min(LayerWidth,
98 referenceViewWidth * horizontalScale);
99 viewHeight = Math.Min(LayerHeight,
100 referenceViewHeight * verticalScale);
101
102 float worldWidth = Math.Max(1f, Main.maxTilesX * 16f);
103 float worldHeight = Math.Max(1f, Main.maxTilesY * 16f);
104 float worldX = MathHelper.Clamp(worldCenter.X / worldWidth, 0f, 1f);
105 float worldY = MathHelper.Clamp(worldCenter.Y / worldHeight, 0f, 1f);
106 float travelFactor = layer switch { 0 => 0.34f, 1 => 0.66f, _ => 1f };
107 float horizontalTravel = Math.Max(0f, LayerWidth - viewWidth)
108 * travelFactor;
109 sourceOffsetX = (LayerWidth - viewWidth - horizontalTravel) * 0.5f
110 + horizontalTravel * worldX;
111 sourceOffsetY = worldY * Math.Max(0f, LayerHeight - viewHeight);
112 }
113
28 114 /// <summary>
29 115 /// Samples the exact three procedural plates used by the necklace domain and
30 116 /// composites them in premultiplied-alpha order. Death weapon masks use this
Modified Common/DeathDomainCrescentVisualSystem.cs +55 -26
@@ -21,19 +21,25 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
21 21 private const int ResidualLifetimeFrames = 30;
22 22 private const float HalfSweep = ReaperCombatRegistry.DeathPrimaryHalfSweep;
23 23 private const float OuterRadiusRatio = 0.91f;
24 private const float MaximumThicknessRatio =
25 ReaperCombatRegistry.StandardCrescentThicknessRatio;
24 // The outside blade edge stays on the weapon tip. Additional size grows only
25 // toward the wielder, as requested, instead of increasing attack reach.
26 private const float MaximumThicknessRatio = 0.585f;
26 27
27 28 private static readonly Dictionary<long, CrescentDrawState> drawStates = [];
28 29 private static long nextVisualInstanceId;
29 30 private static BasicEffect? effect;
30 private static VertexPositionColorTexture[] vertices =
31 new VertexPositionColorTexture[(AngularSegments + 1) * (DepthBands + 1)];
31 private static VertexPositionColorTexture[][] layerVertices =
32 [
33 new VertexPositionColorTexture[(AngularSegments + 1) * (DepthBands + 1)],
34 new VertexPositionColorTexture[(AngularSegments + 1) * (DepthBands + 1)],
35 new VertexPositionColorTexture[(AngularSegments + 1) * (DepthBands + 1)]
36 ];
32 37 private static short[] indices = CreateIndices();
33 38 private static VertexPositionColorTexture[] rimVertices =
34 39 new VertexPositionColorTexture[AngularSegments * 6];
35 40
36 41 private readonly record struct CrescentDrawState(
42 int Owner,
37 43 Vector2 Center,
38 44 float Rotation,
39 45 float Radius,
@@ -56,7 +62,8 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
56 62 return nextVisualInstanceId;
57 63 }
58 64
59 internal static void Record(long visualInstanceId, Vector2 center,
65 internal static void Record(long visualInstanceId, int owner,
66 Vector2 center,
60 67 float rotation, float radius, float opacity, int swingDirection,
61 68 float lifeProgress, bool preserveInterior = false)
62 69 {
@@ -83,7 +90,7 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
83 90 return;
84 91 }
85 92
86 drawStates[visualInstanceId] = new CrescentDrawState(center, rotation,
93 drawStates[visualInstanceId] = new CrescentDrawState(owner, center, rotation,
87 94 radius, opacity, swingDirection < 0 ? -1 : 1,
88 95 MathHelper.Clamp(lifeProgress, 0f, 1f), preserveInterior,
89 96 Main.GameUpdateCount);
@@ -120,7 +127,7 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
120 127 nextVisualInstanceId = 0;
121 128 BasicEffect? oldEffect = effect;
122 129 effect = null;
123 vertices = [];
130 layerVertices = [];
124 131 indices = [];
125 132 rimVertices = [];
126 133 if (oldEffect is not null && !Main.dedServ)
@@ -170,6 +177,7 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
170 177 for (int layer = 0; layer < 3; layer++)
171 178 {
172 179 effect.Texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
180 VertexPositionColorTexture[] vertices = layerVertices[layer];
173 181 foreach (EffectPass pass in effect.CurrentTechnique.Passes)
174 182 {
175 183 pass.Apply();
@@ -201,16 +209,13 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
201 209 float progress = segment / (float)AngularSegments;
202 210 float angle = MathHelper.Lerp(-HalfSweep, HalfSweep, progress)
203 211 * state.SwingDirection;
204 float taper = (float)Math.Pow(Math.Max(0f,
205 Math.Sin(progress * MathHelper.Pi)), 0.67f);
206 taper *= 1f + 0.22f * (progress * 2f - 1f);
212 float taper = GetCrescentTaper(progress);
207 213 // The textured interior and the separately drawn hot blade edge must
208 214 // share this exact boundary. Noise belongs inside the material;
209 215 // perturbing the outer radius creates a visible air gap at the rim.
210 216 float outerRadius = state.Radius * OuterRadiusRatio;
211 float innerDistortion = ((float)Math.Sin(progress * 23f + 0.7f)
212 + (float)Math.Sin(progress * 47f - 1.4f) * 0.42f)
213 * state.Radius * 0.018f * taper;
217 float innerDistortion = GetInnerBoundaryNoise(state, progress,
218 taper);
214 219 float thickness = state.Radius * MaximumThicknessRatio * taper;
215 220 float innerRadius = outerRadius - thickness + innerDistortion;
216 221 float revealAlpha = 1f - SmoothStep(reveal - 0.024f,
@@ -241,13 +246,19 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
241 246 // lower alpha; depth never lets the ordinary world bleed through.
242 247 float alpha = revealAlpha * capAlpha * integrity;
243 248
244 // World-space UVs are deliberately independent of the current
245 // crescent angle, radius, and animation frame. Only the mask
246 // moves; the Death Domain scenery behind it never rotates.
247 Vector2 uv = new(world.X / DeathDomainBackdropTextureSystem.LayerWidth,
248 world.Y / DeathDomainBackdropTextureSystem.LayerHeight);
249 vertices[vertexIndex++] = new VertexPositionColorTexture(
250 new Vector3(screen, 0f), Color.White * alpha, uv);
249 // Each parallax layer uses the exact framing and scale of the
250 // Death Necklace. The mask moves, but its domain does not rotate.
251 for (int layer = 0; layer < 3; layer++)
252 {
253 Vector2 uv = DeathDomainBackdropTextureSystem.GetMatchingDomainUv(
254 layer, world, state.Owner, state.Center);
255 layerVertices[layer][vertexIndex] =
256 new VertexPositionColorTexture(
257 new Vector3(screen, 0f), Color.White * (alpha
258 * DeathDomainBackdropTextureSystem
259 .GetLayerOpacity(layer)), uv);
260 }
261 vertexIndex++;
251 262 }
252 263 }
253 264 }
@@ -391,13 +402,9 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
391 402 return false;
392 403 }
393 404
394 float taper = (float)Math.Pow(Math.Max(0f,
395 Math.Sin(progress * MathHelper.Pi)), 0.67f);
396 taper *= 1f + 0.22f * (progress * 2f - 1f);
405 float taper = GetCrescentTaper(progress);
397 406 float outerRadius = state.Radius * OuterRadiusRatio;
398 float innerDistortion = ((float)Math.Sin(progress * 23f + 0.7f)
399 + (float)Math.Sin(progress * 47f - 1.4f) * 0.42f)
400 * state.Radius * 0.018f * taper;
407 float innerDistortion = GetInnerBoundaryNoise(state, progress, taper);
401 408 float innerRadius = outerRadius
402 409 - state.Radius * MaximumThicknessRatio * taper
403 410 + innerDistortion;
@@ -413,6 +420,28 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
413 420 || preferOtherAtSharedBoundary;
414 421 }
415 422
423 private static float GetCrescentTaper(float progress)
424 {
425 float taper = (float)Math.Pow(Math.Max(0f,
426 Math.Sin(progress * MathHelper.Pi)), 0.67f);
427 return taper * (1f + 0.22f * (progress * 2f - 1f));
428 }
429
430 private static float GetInnerBoundaryNoise(CrescentDrawState state,
431 float progress, float taper)
432 {
433 // Continuous, layered wave noise keeps the centre-facing edge organic
434 // without the disconnected saw teeth produced by per-segment randomness.
435 float seed = state.Rotation * 1.37f
436 + state.Center.X * 0.0013f + state.Center.Y * 0.0019f;
437 float wave = (float)Math.Sin(progress * MathHelper.TwoPi * 5f + seed)
438 + (float)Math.Sin(progress * MathHelper.TwoPi * 11f
439 - seed * 0.71f) * 0.46f
440 + (float)Math.Sin(progress * MathHelper.TwoPi * 23f
441 + seed * 1.19f) * 0.19f;
442 return wave * state.Radius * 0.032f * taper;
443 }
444
416 445 private static float GetResidualProgress(CrescentDrawState state)
417 446 {
418 447 ulong age = Main.GameUpdateCount > state.UpdateTick
Modified Common/DeathDomainTrailVisualSystem.cs +87 -26
@@ -21,22 +21,29 @@ internal sealed class DeathDomainTrailVisualSystem : ModSystem
21 21 new VertexPositionColorTexture[MaximumPoints * 2];
22 22 private static VertexPositionColorTexture[] mergedOutlineVertices =
23 23 new VertexPositionColorTexture[4096];
24 private static VertexPositionColorTexture[] mergedBackdropVertices =
25 new VertexPositionColorTexture[4096];
24 private static VertexPositionColorTexture[][] mergedBackdropVertices =
25 [
26 new VertexPositionColorTexture[4096],
27 new VertexPositionColorTexture[4096],
28 new VertexPositionColorTexture[4096]
29 ];
26 30 private static readonly short[] indices = CreateIndices();
27 31 private static BasicEffect? effect;
28 32
29 33 private readonly record struct TrailDrawState(
34 int Owner,
30 35 IReadOnlyList<Vector2> Points,
31 36 float Width,
32 37 float Opacity,
33 38 bool MergeOverlappingRims,
39 float Fracture,
40 int Seed,
34 41 Vector4 Bounds,
35 42 ulong UpdateTick);
36 43
37 44 internal static void Record(int owner, int identity,
38 45 IReadOnlyList<Vector2> points, float width, float opacity,
39 bool mergeOverlappingRims = false)
46 bool mergeOverlappingRims = false, float fracture = 0f)
40 47 {
41 48 if (Main.dedServ || points.Count < 2 || width <= 0.5f
42 49 || opacity <= 0.001f)
@@ -53,13 +60,15 @@ internal sealed class DeathDomainTrailVisualSystem : ModSystem
53 60 drawStates[key] = existing with
54 61 {
55 62 Opacity = opacity,
63 Fracture = MathHelper.Clamp(fracture, 0f, 1f),
56 64 UpdateTick = Main.GameUpdateCount
57 65 };
58 66 return;
59 67 }
60 68
61 drawStates[key] = new TrailDrawState(points, width, opacity,
62 mergeOverlappingRims,
69 drawStates[key] = new TrailDrawState(owner, points, width, opacity,
70 mergeOverlappingRims, MathHelper.Clamp(fracture, 0f, 1f),
71 unchecked(identity * 397 ^ owner * 7919),
63 72 CalculateTrailBounds(points, width * 1.72f),
64 73 Main.GameUpdateCount);
65 74 }
@@ -141,10 +150,13 @@ internal sealed class DeathDomainTrailVisualSystem : ModSystem
141 150 DrawSolidRibbon(graphicsDevice, state, state.Width * 1.17f,
142 151 new Color(255, 187, 178, 235)
143 152 * (state.Opacity * 0.88f));
144 BuildVertices(state, state.Width, Color.White * state.Opacity);
145 153 effect.TextureEnabled = true;
146 154 for (int layer = 0; layer < 3; layer++)
147 155 {
156 BuildVertices(state, state.Width,
157 Color.White * (state.Opacity
158 * DeathDomainBackdropTextureSystem
159 .GetLayerOpacity(layer)), layer);
148 160 effect.Texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
149 161 foreach (EffectPass pass in effect.CurrentTechnique.Passes)
150 162 {
@@ -184,7 +196,8 @@ internal sealed class DeathDomainTrailVisualSystem : ModSystem
184 196 {
185 197 pass.Apply();
186 198 graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
187 mergedBackdropVertices, 0, backdropVertexCount / 3);
199 mergedBackdropVertices[layer], 0,
200 backdropVertexCount / 3);
188 201 }
189 202 }
190 203 }
@@ -211,6 +224,8 @@ internal sealed class DeathDomainTrailVisualSystem : ModSystem
211 224 Color color = baseColor * (state.Opacity * opacityMultiplier);
212 225 for (int segment = 0; segment < pointCount - 1; segment++)
213 226 {
227 if (!IsSegmentVisible(state, segment, pointCount))
228 continue;
214 229 Vector2 start = state.Points[segment];
215 230 Vector2 end = state.Points[segment + 1];
216 231 Vector2 startNormal = GetTrailNormal(state.Points, segment,
@@ -273,9 +288,10 @@ internal sealed class DeathDomainTrailVisualSystem : ModSystem
273 288 continue;
274 289 }
275 290 int pointCount = Math.Min(MaximumPoints, state.Points.Count);
276 Color color = Color.White * state.Opacity;
277 291 for (int segment = 0; segment < pointCount - 1; segment++)
278 292 {
293 if (!IsSegmentVisible(state, segment, pointCount))
294 continue;
279 295 Vector2 start = state.Points[segment];
280 296 Vector2 end = state.Points[segment + 1];
281 297 Vector2 startNormal = GetTrailNormal(state.Points, segment,
@@ -292,26 +308,41 @@ internal sealed class DeathDomainTrailVisualSystem : ModSystem
292 308 Vector2 rightStart = start + startNormal * startHalfWidth;
293 309 Vector2 leftEnd = end - endNormal * endHalfWidth;
294 310 Vector2 rightEnd = end + endNormal * endHalfWidth;
295 EnsureBatchCapacity(ref mergedBackdropVertices,
296 vertexIndex + 6);
297 WriteBackdropBatchVertex(ref vertexIndex, leftStart, color);
298 WriteBackdropBatchVertex(ref vertexIndex, rightStart, color);
299 WriteBackdropBatchVertex(ref vertexIndex, leftEnd, color);
300 WriteBackdropBatchVertex(ref vertexIndex, leftEnd, color);
301 WriteBackdropBatchVertex(ref vertexIndex, rightStart, color);
302 WriteBackdropBatchVertex(ref vertexIndex, rightEnd, color);
311 for (int layer = 0; layer < 3; layer++)
312 EnsureBatchCapacity(ref mergedBackdropVertices[layer],
313 vertexIndex + 6);
314 Vector2 anchor = GetBackgroundAnchor(state);
315 for (int layer = 0; layer < 3; layer++)
316 {
317 int layerVertexIndex = vertexIndex;
318 Color color = Color.White * (state.Opacity
319 * DeathDomainBackdropTextureSystem
320 .GetLayerOpacity(layer));
321 WriteBackdropBatchVertex(layer, state.Owner,
322 ref layerVertexIndex, leftStart, anchor, color);
323 WriteBackdropBatchVertex(layer, state.Owner,
324 ref layerVertexIndex, rightStart, anchor, color);
325 WriteBackdropBatchVertex(layer, state.Owner,
326 ref layerVertexIndex, leftEnd, anchor, color);
327 WriteBackdropBatchVertex(layer, state.Owner,
328 ref layerVertexIndex, leftEnd, anchor, color);
329 WriteBackdropBatchVertex(layer, state.Owner,
330 ref layerVertexIndex, rightStart, anchor, color);
331 WriteBackdropBatchVertex(layer, state.Owner,
332 ref layerVertexIndex, rightEnd, anchor, color);
333 }
334 vertexIndex += 6;
303 335 }
304 336 }
305 337 return vertexIndex;
306 338 }
307 339
308 private static void WriteBackdropBatchVertex(ref int vertexIndex,
309 Vector2 world, Color color)
340 private static void WriteBackdropBatchVertex(int layer, int owner,
341 ref int vertexIndex, Vector2 world, Vector2 anchor, Color color)
310 342 {
311 Vector2 uv = new(
312 world.X / DeathDomainBackdropTextureSystem.LayerWidth,
313 world.Y / DeathDomainBackdropTextureSystem.LayerHeight);
314 WriteBatchVertex(mergedBackdropVertices, ref vertexIndex, world,
343 Vector2 uv = DeathDomainBackdropTextureSystem.GetMatchingDomainUv(
344 layer, world, owner, anchor);
345 WriteBatchVertex(mergedBackdropVertices[layer], ref vertexIndex, world,
315 346 color, uv);
316 347 }
317 348
@@ -345,6 +376,30 @@ internal sealed class DeathDomainTrailVisualSystem : ModSystem
345 376 Math.Sin(MathHelper.Clamp(progress, 0f, 1f) * MathHelper.Pi)),
346 377 0.34f);
347 378
379 private static bool IsSegmentVisible(TrailDrawState state, int segment,
380 int pointCount)
381 {
382 if (state.Fracture <= 0.001f)
383 return true;
384 uint hash = unchecked((uint)(state.Seed * 747796405
385 + segment * 2891336453));
386 hash ^= hash >> 16;
387 hash *= 0x7FEB352Du;
388 hash ^= hash >> 15;
389 float random = (hash & 0x00FFFFFFu) / 16777215f;
390 float along = (segment + 0.5f) / Math.Max(1f, pointCount - 1f);
391 float ripple = 0.5f + 0.5f * (float)Math.Sin(
392 along * MathHelper.TwoPi * 5f + state.Seed * 0.017f);
393 float breakThreshold = 0.10f + random * 0.68f + ripple * 0.16f;
394 return Smooth01(state.Fracture) < breakThreshold;
395 }
396
397 private static float Smooth01(float value)
398 {
399 value = MathHelper.Clamp(value, 0f, 1f);
400 return value * value * (3f - 2f * value);
401 }
402
348 403 private static Vector4 CalculateTrailBounds(
349 404 IReadOnlyList<Vector2> points, float width)
350 405 {
@@ -372,6 +427,10 @@ internal sealed class DeathDomainTrailVisualSystem : ModSystem
372 427 && bounds.X <= right && bounds.Y <= bottom;
373 428 }
374 429
430 private static Vector2 GetBackgroundAnchor(TrailDrawState state)
431 => new((state.Bounds.X + state.Bounds.Z) * 0.5f,
432 (state.Bounds.Y + state.Bounds.W) * 0.5f);
433
375 434 private static void DrawSolidRibbon(GraphicsDevice graphicsDevice,
376 435 TrailDrawState state, float width, Color color)
377 436 {
@@ -389,7 +448,7 @@ internal sealed class DeathDomainTrailVisualSystem : ModSystem
389 448 }
390 449
391 450 private static int BuildVertices(TrailDrawState state, float width,
392 Color color)
451 Color color, int backdropLayer = -1)
393 452 {
394 453 int count = Math.Min(MaximumPoints, state.Points.Count);
395 454 for (int index = 0; index < count; index++)
@@ -408,9 +467,11 @@ internal sealed class DeathDomainTrailVisualSystem : ModSystem
408 467 {
409 468 Vector2 world = point + normal * (side == 0 ? -halfWidth : halfWidth);
410 469 Vector2 screen = world - Main.screenPosition;
411 Vector2 uv = new(
412 world.X / DeathDomainBackdropTextureSystem.LayerWidth,
413 world.Y / DeathDomainBackdropTextureSystem.LayerHeight);
470 Vector2 uv = backdropLayer >= 0
471 ? DeathDomainBackdropTextureSystem.GetMatchingDomainUv(
472 backdropLayer, world, state.Owner,
473 GetBackgroundAnchor(state))
474 : Vector2.Zero;
414 475 vertices[index * 2 + side] = new VertexPositionColorTexture(
415 476 new Vector3(screen, 0f), color, uv);
416 477 }
Modified Common/DeathDomainVisualSystem.cs +40 -13
@@ -72,6 +72,37 @@ internal class DeathDomainVisualSystem : ModSystem
72 72 ClearAnimationState();
73 73 }
74 74
75 /// <summary>
76 /// Supplies the same aperture currently used to draw an owner's necklace
77 /// domain. Weapon masks query this instead of re-deriving full-screen state,
78 /// so future domain unlock rules cannot make the two backgrounds diverge.
79 /// </summary>
80 internal static bool TryGetMatchingBackdropAperture(int owner,
81 out Vector2 anchorWorld, out Vector2 apertureSize)
82 {
83 anchorWorld = owner >= 0 && owner < Main.maxPlayers
84 && Main.player[owner].active
85 ? Main.player[owner].Center
86 : Main.screenPosition
87 + new Vector2(Main.screenWidth, Main.screenHeight) * 0.5f;
88 apertureSize = new Vector2(
89 DeathDomainBackdropTextureSystem.ReferenceDomainDiameter);
90 if (owner < 0 || owner >= Main.maxPlayers)
91 return false;
92
93 DeathDomainVisualSystem instance = ModContent
94 .GetInstance<DeathDomainVisualSystem>();
95 if (instance.domainAnimation[owner] <= 0.001f)
96 return false;
97 if (instance.cachedFullScreen[owner])
98 {
99 anchorWorld = Main.screenPosition
100 + new Vector2(Main.screenWidth, Main.screenHeight) * 0.5f;
101 apertureSize = new Vector2(Main.screenWidth, Main.screenHeight);
102 }
103 return true;
104 }
105
75 106 public override void PostDrawTiles()
76 107 {
77 108 Player localPlayer = Main.LocalPlayer;
@@ -793,19 +824,15 @@ internal class DeathDomainVisualSystem : ModSystem
793 824 out float viewWidth,
794 825 out float viewHeight)
795 826 {
796 float referenceViewWidth = 1024f * (layer switch { 0 => 0.68f, 1 => 0.9f, _ => 0.84f });
797 float referenceViewHeight = 512f * (layer switch { 0 => 0.78f, 1 => 0.9f, _ => 0.86f });
798 viewWidth = Math.Min(texture.Width, referenceViewWidth * horizontalScale);
799 viewHeight = Math.Min(texture.Height, referenceViewHeight * verticalScale);
800
801 float worldWidth = Math.Max(1f, Main.maxTilesX * 16f);
802 float worldHeight = Math.Max(1f, Main.maxTilesY * 16f);
803 float worldX = MathHelper.Clamp(worldCenter.X / worldWidth, 0f, 1f);
804 float worldY = MathHelper.Clamp(worldCenter.Y / worldHeight, 0f, 1f);
805 float travelFactor = layer switch { 0 => 0.34f, 1 => 0.66f, _ => 1f };
806 float horizontalTravel = Math.Max(0f, texture.Width - viewWidth) * travelFactor;
807 sourceOffsetX = (texture.Width - viewWidth - horizontalTravel) * 0.5f + horizontalTravel * worldX;
808 sourceOffsetY = worldY * Math.Max(0f, texture.Height - viewHeight);
827 _ = texture;
828 DeathDomainBackdropTextureSystem.GetSourceFrame(layer, worldCenter,
829 new Vector2(
830 DeathDomainBackdropTextureSystem.ReferenceDomainDiameter
831 * horizontalScale,
832 DeathDomainBackdropTextureSystem.ReferenceDomainDiameter
833 * verticalScale),
834 out sourceOffsetX, out sourceOffsetY,
835 out viewWidth, out viewHeight);
809 836 }
810 837
811 838 private static void DrawDomainRippleWaves(SpriteBatch batch, Vector2 center,
Modified Common/ReaperUltimateVisualSystem.cs +8 -24
@@ -400,10 +400,9 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
400 400 // letterbox reads as one cinematic composition and cannot resemble a
401 401 // misplaced child viewport.
402 402
403 float heartbeat = Pulse(progress, 0.985f, 0.018f) * opacity;
404 if (heartbeat > 0.001f)
405 batch.Draw(pixel, new Rectangle(0, 0, (int)Math.Ceiling(viewport.X), (int)Math.Ceiling(viewport.Y)),
406 Color.White * (heartbeat * 0.16f));
403 // Final impacts are expressed by world geometry, sound and camera
404 // response. A full-viewport white heartbeat obscured those details and
405 // became a one-frame white screen when several attacks overlapped.
407 406 }
408 407
409 408 private static void DrawBone(
@@ -689,7 +688,6 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
689 688 {
690 689 // A cool gray veil mutes the world without changing Terraria's shared shader state.
691 690 batch.Draw(pixel, viewportBounds, new Color(102, 128, 148) * (0.12f * opacity));
692 batch.Draw(pixel, viewportBounds, Color.White * (Pulse(progress, 0.91f, 0.055f) * 0.10f * opacity));
693 691
694 692 float reach = Math.Max(viewport.X, viewport.Y) * 0.68f;
695 693 for (int index = 0; index < 4; index++)
@@ -882,9 +880,10 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
882 880 float opacity,
883 881 float time)
884 882 {
885 // The eighteen damaging projectiles now own world-space, domain-filled
886 // wounds. This interface layer supplies atmosphere and the final
887 // shatter only; drawing another set here would pin cuts to the camera.
883 // The eighteen damaging projectiles own the world-space, domain-filled
884 // cuts and their track-aligned shatter. This layer is atmosphere only;
885 // screen-space blades or radial shards would drift with the camera and
886 // would not correspond to any preceding cut.
888 887 Rectangle screen = new(0, 0, (int)Math.Ceiling(viewport.X),
889 888 (int)Math.Ceiling(viewport.Y));
890 889 float suspended = Envelope(progress, 0.08f, 0.14f, 0.87f, 0.92f) * opacity;
@@ -892,22 +891,7 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
892 891 DrawGlow(batch, focus, 220f, new Color(120, 0, 38) * (suspended * 0.22f));
893 892
894 893 float shatter = Envelope(progress, 0.87f, 0.89f, 0.985f, 1f) * opacity;
895 if (shatter <= 0.001f)
896 return;
897 float scale = MathHelper.Clamp(Math.Min(viewport.X, viewport.Y) * 0.50f, 330f, 620f);
898 DrawReaperSilhouette(batch, pixel, focus - Vector2.UnitY * scale * 0.22f,
899 scale, new Color(2, 0, 3), new Color(205, 18, 56), shatter * 0.78f);
900 for (int shard = 0; shard < 20; shard++)
901 {
902 float angle = shard * MathHelper.TwoPi / 20f + time * 0.025f;
903 Vector2 direction = angle.ToRotationVector2();
904 float inner = 26f + shard % 4 * 8f;
905 float outer = MathHelper.Lerp(inner, scale * 0.68f,
906 Ease(shatter)) * (0.76f + shard % 3 * 0.10f);
907 DrawLayeredLine(batch, pixel, focus + direction * inner,
908 focus + direction * outer, new Color(35, 0, 14),
909 new Color(255, 66, 98), 9f, 1.5f, shatter * 0.72f);
910 }
894 batch.Draw(pixel, screen, new Color(18, 0, 8) * (shatter * 0.12f));
911 895 }
912 896
913 897 private static void DrawDeathLegacy(
Modified Common/ReaperVfxDirector.cs +11 -0
@@ -148,6 +148,17 @@ public sealed class ReaperVfxDirector : ModSystem
148 148 opacity = MathHelper.Clamp(opacity, 0f, MaximumFlashOpacity);
149 149 colorFrames = Math.Clamp(colorFrames, 0, 30);
150 150
151 // Bright full-viewport overlays read as a white-screen defect when rapid
152 // melee or passive events overlap. Keep their camera punch and vignette,
153 // but suppress the bright color plane entirely.
154 Vector3 rgb = color.ToVector3();
155 float luminance = rgb.X * 0.2126f + rgb.Y * 0.7152f + rgb.Z * 0.0722f;
156 if (luminance >= 0.64f)
157 {
158 opacity = 0f;
159 colorFrames = 0;
160 }
161
151 162 if (strength >= shakeStrength || shakeFrames <= 1)
152 163 {
153 164 shakeStrength = strength;
Modified Projectiles/ReaperActionControllerProjectile.cs +0 -20
@@ -156,8 +156,6 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
156 156 DrawSpecialStoryboard(player, grip);
157 157 else if (snapshot.Form == ReaperFormId.Infernal)
158 158 DrawInfernalUltimateRoute(TextureAssets.MagicPixel.Value);
159 else if (snapshot.Form == ReaperFormId.Death)
160 DrawDeathUltimateSwingCrescent(grip);
161 159
162 160 Texture2D texture = ModContent.Request<Texture2D>(
163 161 ReaperCombatRegistry.GetTexturePath(snapshot.Form, snapshot.Stage)).Value;
@@ -1287,24 +1285,6 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
1287 1285 }
1288 1286 }
1289 1287
1290 private void DrawDeathUltimateSwingCrescent(Vector2 grip)
1291 {
1292 if (timer < 20 || timer >= 128)
1293 return;
1294 int cut = Math.Clamp((timer - 20) / 6, 0, 17);
1295 float local = MathHelper.Clamp(((timer - 20) % 6) / 5f, 0f, 1f);
1296 int facing = Math.Abs(aim.X) > 0.05f ? Math.Sign(aim.X) : 1;
1297 int direction = (cut & 1) == 0 ? facing : -facing;
1298 float visibility = SmoothStep(MathHelper.Clamp(local / 0.22f, 0f, 1f))
1299 * (1f - SmoothStep(MathHelper.Clamp((local - 0.76f) / 0.24f, 0f, 1f)));
1300 float radius = ReaperCombatRegistry.DeathPrimaryCrescentRadius;
1301 Vector2 center = grip;
1302 float rotation = aim.ToRotation();
1303 ReaperCrescentPrimitiveTextureSystem.DrawDeathDomainCrescent(center,
1304 rotation, radius, visibility * 0.94f, 3, direction,
1305 Main.GlobalTimeWrappedHourly, local);
1306 }
1307
1308 1288 private void DrawSpecialStoryboard(Player player, Vector2 grip)
1309 1289 {
1310 1290 Texture2D pixel = TextureAssets.MagicPixel.Value;
Modified Projectiles/ReaperDeathDomainRiftProjectile.cs +3 -1
@@ -108,9 +108,11 @@ public sealed class ReaperDeathDomainRiftProjectile : ModProjectile
108 108 float opacity = GetOpacity();
109 109 if (Main.netMode != NetmodeID.Server)
110 110 {
111 float fracture = Smooth01(MathHelper.Clamp(
112 (30f - Projectile.timeLeft) / 30f, 0f, 1f));
111 113 DeathDomainTrailVisualSystem.Record(Projectile.owner,
112 114 Projectile.identity, points, TrailWidth, opacity,
113 mergeOverlappingRims: true);
115 mergeOverlappingRims: true, fracture: fracture);
114 116 }
115 117 if (Main.netMode != NetmodeID.MultiplayerClient)
116 118 TryHarvestIntersectingEnemies();
Modified Projectiles/ReaperStrikeProjectile.cs +49 -8
@@ -12,6 +12,8 @@ namespace DeathMod.Projectiles;
12 12
13 13 public sealed class ReaperStrikeProjectile : ModProjectile
14 14 {
15 private const int DeathCutPointCount = 65;
16 private readonly Vector2[] deathCutPoints = new Vector2[DeathCutPointCount];
15 17 private SickleCombatSnapshot snapshot;
16 18 private ReaperHitKind hitKind;
17 19 private ReaperStrikeShape shape;
@@ -96,6 +98,8 @@ public sealed class ReaperStrikeProjectile : ModProjectile
96 98 return;
97 99 age++;
98 100 int visualLifetime = GetVisualLifetime();
101 if (IsDeathUltimateWorldCut && Main.netMode != NetmodeID.Server)
102 RecordDeathUltimateWorldCut(visualLifetime);
99 103 Projectile.timeLeft = Math.Max(2, visualLifetime + 1 - age);
100 104 if (age == Math.Max(1, delay) && Main.netMode != NetmodeID.Server)
101 105 SpawnBurst();
@@ -123,7 +127,7 @@ public sealed class ReaperStrikeProjectile : ModProjectile
123 127 }
124 128 if (IsDeathUltimateWorldCut)
125 129 {
126 DrawDeathUltimateWorldCut();
130 DrawDeathUltimateShatterFragments();
127 131 return false;
128 132 }
129 133 // Void circle strikes are the invisible collision sweep of the held
@@ -254,17 +258,54 @@ public sealed class ReaperStrikeProjectile : ModProjectile
254 258 return Math.Max(18, 132 - phase * 6);
255 259 }
256 260
257 private void DrawDeathUltimateWorldCut()
261 private void RecordDeathUltimateWorldCut(int lifetime)
258 262 {
259 263 Vector2 direction = Projectile.velocity.SafeNormalize(Vector2.UnitX);
260 Vector2 worldCenter = Projectile.Center + direction * length * 0.5f;
264 for (int index = 0; index < deathCutPoints.Length; index++)
265 {
266 deathCutPoints[index] = Projectile.Center + direction * length
267 * (index / (float)(deathCutPoints.Length - 1));
268 }
261 269 float completion = Smooth01(age / 4f);
270 float fracture = Smooth01(MathHelper.Clamp(
271 (age - (lifetime - 12f)) / 12f, 0f, 1f));
272 float terminalFade = 1f - Smooth01(MathHelper.Clamp(
273 (fracture - 0.78f) / 0.22f, 0f, 1f));
274 DeathDomainTrailVisualSystem.Record(Projectile.owner,
275 Projectile.identity, deathCutPoints, Math.Max(54f, width * 2.5f),
276 completion * terminalFade * 0.98f,
277 mergeOverlappingRims: true, fracture: fracture);
278 }
279
280 private void DrawDeathUltimateShatterFragments()
281 {
262 282 int lifetime = GetVisualLifetime();
263 float fade = Smooth01((lifetime - age) / 10f);
264 float opacity = completion * fade * 0.96f;
265 ReaperCrescentPrimitiveTextureSystem.DrawDeathDomainBlade(
266 worldCenter - Main.screenPosition, direction.ToRotation(), length,
267 Math.Max(38f, width * 2.05f), opacity, completion);
283 float fracture = Smooth01(MathHelper.Clamp(
284 (age - (lifetime - 12f)) / 12f, 0f, 1f));
285 if (fracture <= 0.001f)
286 return;
287
288 Vector2 axis = Projectile.velocity.SafeNormalize(Vector2.UnitX);
289 Vector2 normal = axis.RotatedBy(MathHelper.PiOver2);
290 for (int shard = 0; shard < 7; shard++)
291 {
292 uint hash = unchecked((uint)(Projectile.identity * 747796405
293 + shard * 2891336453 + phase * 97));
294 hash ^= hash >> 16;
295 float along = 0.08f + (hash & 0xFFFFu) / 65535f * 0.84f;
296 float side = (shard & 1) == 0 ? -1f : 1f;
297 float tilt = 0.22f + ((hash >> 16) & 0xFFu) / 255f * 0.48f;
298 Vector2 origin = Projectile.Center + axis * length * along;
299 Vector2 direction = (normal * side + axis * tilt)
300 .SafeNormalize(normal * side);
301 float branchLength = (22f + shard % 3 * 9f)
302 * MathHelper.Lerp(0.35f, 1f, fracture);
303 Vector2 end = origin + direction * branchLength;
304 DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(origin, end,
305 new Color(30, 0, 14) * (fracture * 0.84f), 5.5f);
306 DeathDomainPrimitiveTextureSystem.DrawSmoothWorldLine(origin, end,
307 new Color(255, 34, 82) * (fracture * 0.78f), 1.7f);
308 }
268 309 }
269 310
270 311 private static float Smooth01(float value)
Modified Projectiles/SickleSwingProjectile.cs +1 -1
@@ -1023,7 +1023,7 @@ public sealed class SickleSwingProjectile : ModProjectile
1023 1023 GetDeathPrimaryCrescentGeometry(out Vector2 center, out float rotation,
1024 1024 out float reach, out int direction);
1025 1025 DeathDomainCrescentVisualSystem.Record(deathCrescentVisualInstanceId,
1026 center, rotation, reach, visibility * 0.92f,
1026 Projectile.owner, center, rotation, reach, visibility * 0.92f,
1027 1027 direction, animationProgress, DeathSpaceBreak);
1028 1028 }
1029 1029