返回提交历史
Modified
Common/DeathDomainCrescentVisualSystem.cs
+175
-38
Modified
Projectiles/SickleSwingProjectile.cs
+9
-2
XFEstudio/DeathMod
修复死神刀光世界残留与重叠融合
affed65
代码差异
2 个文件
+184
-40
@@ -25,12 +25,13 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
25
25
ReaperCombatRegistry.StandardCrescentThicknessRatio;
26
26
27
27
private static readonly Dictionary<long, CrescentDrawState> drawStates = [];
28
private static long nextVisualInstanceId;
28
29
private static BasicEffect? effect;
29
30
private static VertexPositionColorTexture[] vertices =
30
31
new VertexPositionColorTexture[(AngularSegments + 1) * (DepthBands + 1)];
31
32
private static short[] indices = CreateIndices();
32
33
private static VertexPositionColorTexture[] rimVertices =
33
new VertexPositionColorTexture[(AngularSegments + 1) * 2];
34
new VertexPositionColorTexture[AngularSegments * 6];
34
35
35
36
private readonly record struct CrescentDrawState(
36
37
Vector2 Center,
@@ -42,16 +43,48 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
42
43
bool PreserveInterior,
43
44
ulong UpdateTick);
44
45
45
internal static void Record(int owner, int identity, Vector2 center,
46
/// <summary>
47
/// Allocates a client-local identity for one physical swing. Projectile
48
/// identities and slots can be reused before a residual crescent expires at
49
/// maximum attack speed, so neither is safe as the draw-state key.
50
/// </summary>
51
internal static long AllocateVisualInstanceId()
52
{
53
nextVisualInstanceId++;
54
if (nextVisualInstanceId <= 0)
55
nextVisualInstanceId = 1;
56
return nextVisualInstanceId;
57
}
58
59
internal static void Record(long visualInstanceId, Vector2 center,
46
60
float rotation, float radius, float opacity, int swingDirection,
47
61
float lifeProgress, bool preserveInterior = false)
48
62
{
49
if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
63
if (Main.dedServ || visualInstanceId <= 0 || radius <= 1f
64
|| opacity <= 0.001f)
65
return;
66
67
if (drawStates.TryGetValue(visualInstanceId,
68
out CrescentDrawState existing))
69
{
70
// A slash is a cut made in world space, not an aura attached to the
71
// player. Keep its first center, orientation and size immutable;
72
// subsequent frames only advance the reveal frontier and refresh its
73
// residual clock. Already drawn material therefore stays exactly
74
// where the blade passed even if the player moves during the swing.
75
drawStates[visualInstanceId] = existing with
76
{
77
Opacity = opacity,
78
LifeProgress = Math.Max(existing.LifeProgress,
79
MathHelper.Clamp(lifeProgress, 0f, 1f)),
80
PreserveInterior = existing.PreserveInterior || preserveInterior,
81
UpdateTick = Main.GameUpdateCount
82
};
50
83
return;
84
}
51
85
52
long key = ((long)owner << 32) | (uint)identity;
53
drawStates[key] = new CrescentDrawState(center, rotation, radius,
54
opacity, swingDirection < 0 ? -1 : 1,
86
drawStates[visualInstanceId] = new CrescentDrawState(center, rotation,
87
radius, opacity, swingDirection < 0 ? -1 : 1,
55
88
MathHelper.Clamp(lifeProgress, 0f, 1f), preserveInterior,
56
89
Main.GameUpdateCount);
57
90
}
@@ -75,11 +108,16 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
75
108
drawStates.Remove(key);
76
109
}
77
110
78
public override void OnWorldUnload() => drawStates.Clear();
111
public override void OnWorldUnload()
112
{
113
drawStates.Clear();
114
nextVisualInstanceId = 0;
115
}
79
116
80
117
public override void Unload()
81
118
{
82
119
drawStates.Clear();
120
nextVisualInstanceId = 0;
83
121
BasicEffect? oldEffect = effect;
84
122
effect = null;
85
123
vertices = [];
@@ -114,13 +152,13 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
114
152
graphicsDevice.RasterizerState = RasterizerState.CullNone;
115
153
graphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
116
154
117
foreach (CrescentDrawState state in drawStates.Values)
155
foreach ((long visualInstanceId, CrescentDrawState state) in drawStates)
118
156
{
119
157
if (Main.GameUpdateCount > state.UpdateTick + ResidualLifetimeFrames)
120
158
continue;
121
159
BuildVertices(state);
122
160
DrawBackdropLayers(graphicsDevice);
123
DrawStableRim(graphicsDevice, state);
161
DrawStableRim(graphicsDevice, visualInstanceId, state);
124
162
}
125
163
}
126
164
@@ -215,7 +253,7 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
215
253
}
216
254
217
255
private static void DrawStableRim(GraphicsDevice graphicsDevice,
218
CrescentDrawState state)
256
long visualInstanceId, CrescentDrawState state)
219
257
{
220
258
if (effect is null)
221
259
return;
@@ -225,57 +263,156 @@ internal sealed class DeathDomainCrescentVisualSystem : ModSystem
225
263
if (opacity <= 0.001f)
226
264
return;
227
265
228
DrawRimStrip(graphicsDevice, state, state.Radius * 0.030f,
266
DrawRimStrip(graphicsDevice, visualInstanceId, state,
267
state.Radius * 0.030f,
229
268
new Color(238, 5, 60) * (opacity * 0.82f));
230
DrawRimStrip(graphicsDevice, state, state.Radius * 0.0125f,
269
DrawRimStrip(graphicsDevice, visualInstanceId, state,
270
state.Radius * 0.0125f,
231
271
new Color(255, 225, 220) * (opacity * 0.96f));
232
272
}
233
273
234
274
private static void DrawRimStrip(GraphicsDevice graphicsDevice,
235
CrescentDrawState state, float width, Color color)
275
long visualInstanceId, CrescentDrawState state, float width,
276
Color color)
236
277
{
237
278
if (effect is null)
238
279
return;
239
280
240
281
float reveal = Smooth01(state.LifeProgress);
241
float rotationCos = (float)Math.Cos(state.Rotation);
242
float rotationSin = (float)Math.Sin(state.Rotation);
243
282
float outerRadius = state.Radius * OuterRadiusRatio;
244
283
int vertexIndex = 0;
245
for (int segment = 0; segment <= AngularSegments; segment++)
284
for (int segment = 0; segment < AngularSegments; segment++)
246
285
{
247
float progress = segment / (float)AngularSegments;
248
float angle = MathHelper.Lerp(-HalfSweep, HalfSweep, progress)
249
* state.SwingDirection;
250
float revealAlpha = 1f - SmoothStep(reveal - 0.024f,
251
reveal + 0.010f, progress);
252
float capAlpha = SmoothStep(0f, 0.018f, progress)
253
* SmoothStep(0f, 0.018f, 1f - progress);
254
Color vertexColor = color * (revealAlpha * capAlpha);
255
Vector2 radial = angle.ToRotationVector2();
256
for (int side = 0; side < 2; side++)
257
{
258
float localRadius = outerRadius - side * width;
259
Vector2 local = radial * localRadius;
260
Vector2 rotated = new(
261
local.X * rotationCos - local.Y * rotationSin,
262
local.X * rotationSin + local.Y * rotationCos);
263
Vector2 screen = state.Center + rotated - Main.screenPosition;
264
rimVertices[vertexIndex++] = new VertexPositionColorTexture(
265
new Vector3(screen, 0f), vertexColor, Vector2.Zero);
266
}
286
float startProgress = segment / (float)AngularSegments;
287
float endProgress = (segment + 1f) / AngularSegments;
288
float middleProgress = (startProgress + endProgress) * 0.5f;
289
Vector2 overlapProbe = GetCrescentPoint(state, middleProgress,
290
outerRadius - width * 0.55f);
291
if (IsCoveredByAnotherCrescent(overlapProbe, visualInstanceId,
292
width * 0.78f))
293
continue;
294
295
Color startColor = color * GetRimVertexAlpha(startProgress, reveal);
296
Color endColor = color * GetRimVertexAlpha(endProgress, reveal);
297
Vector2 outerStart = GetCrescentPoint(state, startProgress,
298
outerRadius);
299
Vector2 innerStart = GetCrescentPoint(state, startProgress,
300
outerRadius - width);
301
Vector2 outerEnd = GetCrescentPoint(state, endProgress,
302
outerRadius);
303
Vector2 innerEnd = GetCrescentPoint(state, endProgress,
304
outerRadius - width);
305
306
WriteRimVertex(ref vertexIndex, outerStart, startColor);
307
WriteRimVertex(ref vertexIndex, innerStart, startColor);
308
WriteRimVertex(ref vertexIndex, outerEnd, endColor);
309
WriteRimVertex(ref vertexIndex, outerEnd, endColor);
310
WriteRimVertex(ref vertexIndex, innerStart, startColor);
311
WriteRimVertex(ref vertexIndex, innerEnd, endColor);
267
312
}
268
313
314
if (vertexIndex < 3)
315
return;
316
269
317
effect.TextureEnabled = false;
270
318
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
271
319
{
272
320
pass.Apply();
273
graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip,
274
rimVertices, 0, rimVertices.Length - 2);
321
graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
322
rimVertices, 0, vertexIndex / 3);
275
323
}
276
324
effect.TextureEnabled = true;
277
325
}
278
326
327
private static float GetRimVertexAlpha(float progress, float reveal)
328
{
329
float revealAlpha = 1f - SmoothStep(reveal - 0.024f,
330
reveal + 0.010f, progress);
331
float capAlpha = SmoothStep(0f, 0.018f, progress)
332
* SmoothStep(0f, 0.018f, 1f - progress);
333
return revealAlpha * capAlpha;
334
}
335
336
private static Vector2 GetCrescentPoint(CrescentDrawState state,
337
float progress, float radius)
338
{
339
float angle = MathHelper.Lerp(-HalfSweep, HalfSweep, progress)
340
* state.SwingDirection + state.Rotation;
341
return state.Center + angle.ToRotationVector2() * radius;
342
}
343
344
private static void WriteRimVertex(ref int vertexIndex, Vector2 world,
345
Color color)
346
{
347
rimVertices[vertexIndex++] = new VertexPositionColorTexture(
348
new Vector3(world - Main.screenPosition, 0f), color, Vector2.Zero);
349
}
350
351
private static bool IsCoveredByAnotherCrescent(Vector2 world,
352
long currentVisualInstanceId, float sharedBoundaryTolerance)
353
{
354
foreach ((long otherId, CrescentDrawState other) in drawStates)
355
{
356
if (otherId == currentVisualInstanceId
357
|| Main.GameUpdateCount
358
> other.UpdateTick + ResidualLifetimeFrames)
359
{
360
continue;
361
}
362
363
if (ContainsVisibleDomain(other, world,
364
preferOtherAtSharedBoundary: otherId > currentVisualInstanceId,
365
sharedBoundaryTolerance))
366
{
367
return true;
368
}
369
}
370
return false;
371
}
372
373
private static bool ContainsVisibleDomain(CrescentDrawState state,
374
Vector2 world, bool preferOtherAtSharedBoundary,
375
float sharedBoundaryTolerance)
376
{
377
Vector2 delta = world - state.Center;
378
float rotationCos = (float)Math.Cos(state.Rotation);
379
float rotationSin = (float)Math.Sin(state.Rotation);
380
Vector2 local = new(
381
delta.X * rotationCos + delta.Y * rotationSin,
382
-delta.X * rotationSin + delta.Y * rotationCos);
383
float radius = local.Length();
384
float angle = MathHelper.WrapAngle(local.ToRotation());
385
float progress = state.SwingDirection > 0
386
? (angle + HalfSweep) / (HalfSweep * 2f)
387
: (HalfSweep - angle) / (HalfSweep * 2f);
388
if (progress is < 0f or > 1f
389
|| progress > Smooth01(state.LifeProgress) + 0.010f)
390
{
391
return false;
392
}
393
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);
397
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;
401
float innerRadius = outerRadius
402
- state.Radius * MaximumThicknessRatio * taper
403
+ innerDistortion;
404
if (radius < innerRadius - 1f || radius > outerRadius + 1f)
405
return false;
406
407
// Strictly interior overlap always removes the internal edge. Coincident
408
// or near-coincident rims use instance order so exactly one copy remains;
409
// without this tolerance, both ribbons would classify each other's
410
// inward probe as interior and erase the shared exterior entirely.
411
float boundaryDepth = outerRadius - radius;
412
return boundaryDepth > sharedBoundaryTolerance
413
|| preferOtherAtSharedBoundary;
414
}
415
279
416
private static float GetResidualProgress(CrescentDrawState state)
280
417
{
281
418
ulong age = Main.GameUpdateCount > state.UpdateTick
@@ -40,6 +40,7 @@ public sealed class SickleSwingProjectile : ModProjectile
40
40
private Vector2 authoritativeVelocity;
41
41
private int authoritativeSwingDirection;
42
42
private int validHistorySamples;
43
private long deathCrescentVisualInstanceId;
43
44
private float animationProgress;
44
45
private float previousCollisionAngle;
45
46
private bool collisionAngleInitialized;
@@ -74,6 +75,7 @@ public sealed class SickleSwingProjectile : ModProjectile
74
75
? MathHelper.Clamp(primaryDeathTempo, 1f,
75
76
ReaperCombatRegistry.DeathPrimaryMaximumTempo)
76
77
: 1f;
78
deathCrescentVisualInstanceId = 0;
77
79
configured = true;
78
80
Projectile.damage = Math.Max(1, (int)Math.Round(snapshot.Damage * ReaperCombatRegistry.GetPrimaryDamageMultiplier(snapshot, phase)));
79
81
Projectile.originalDamage = snapshot.Damage;
@@ -1013,10 +1015,15 @@ public sealed class SickleSwingProjectile : ModProjectile
1013
1015
float visibility = GetDeathCrescentVisibility();
1014
1016
if (visibility <= 0.001f)
1015
1017
return;
1018
if (deathCrescentVisualInstanceId <= 0)
1019
{
1020
deathCrescentVisualInstanceId = DeathDomainCrescentVisualSystem
1021
.AllocateVisualInstanceId();
1022
}
1016
1023
GetDeathPrimaryCrescentGeometry(out Vector2 center, out float rotation,
1017
1024
out float reach, out int direction);
1018
DeathDomainCrescentVisualSystem.Record(Projectile.owner,
1019
Projectile.identity, center, rotation, reach, visibility * 0.92f,
1025
DeathDomainCrescentVisualSystem.Record(deathCrescentVisualInstanceId,
1026
center, rotation, reach, visibility * 0.92f,
1020
1027
direction, animationProgress, DeathSpaceBreak);
1021
1028
}
1022
1029