返回提交历史
Added
Commands/ReaperDebugCommands.cs
+91
-0
Modified
Common/DeathDomainBackdropTextureSystem.cs
+3
-64
Modified
Common/DeathDomainVisualSystem.cs
+247
-0
Modified
Common/MyPlayer.cs
+28
-0
Modified
Common/ReaperProgressionState.cs
+17
-0
Modified
Localization/en-US.hjson
+10
-0
Modified
Localization/zh-Hans.hjson
+10
-0
XFEstudio/DeathMod
feat: add debug commands and transient domain holes
2caad32
代码差异
7 个文件
+406
-64
@@ -0,0 +1,91 @@
1
using DeathMod.Common;
2
using DeathMod.Items;
3
using Microsoft.Xna.Framework;
4
using System;
5
using Terraria;
6
using Terraria.ID;
7
using Terraria.Localization;
8
using Terraria.ModLoader;
9
10
namespace DeathMod.Commands;
11
12
internal static class ReaperDebugMode
13
{
14
internal static bool Enabled => !Main.gameMenu
15
&& !string.IsNullOrWhiteSpace(Main.worldName)
16
&& Main.worldName.Contains("-debug", StringComparison.OrdinalIgnoreCase);
17
18
internal static bool TryGetPlayer(CommandCaller caller, out Player player)
19
{
20
player = caller.Player;
21
if (!Enabled)
22
{
23
caller.Reply(Language.GetTextValue("Mods.DeathMod.Commands.DebugWorldRequired"),
24
new Color(235, 80, 105));
25
return false;
26
}
27
if (player is null || !player.active)
28
{
29
caller.Reply(Language.GetTextValue("Mods.DeathMod.Commands.PlayerRequired"),
30
new Color(235, 80, 105));
31
return false;
32
}
33
if (!player.GetModPlayer<MyPlayer>().ServerCharacterStateReady)
34
{
35
caller.Reply(Language.GetTextValue("Mods.DeathMod.Commands.CharacterNotReady"),
36
new Color(235, 80, 105));
37
return false;
38
}
39
return true;
40
}
41
}
42
43
public sealed class ReaperDebugGiveCommand : ModCommand
44
{
45
public override string Command => "reaperdebuggive";
46
public override CommandType Type => CommandType.World;
47
public override string Usage => "/reaperdebuggive";
48
public override string Description => Language.GetTextValue("Mods.DeathMod.Commands.GiveDescription");
49
50
public override void Action(CommandCaller caller, string input, string[] args)
51
{
52
if (!ReaperDebugMode.TryGetPlayer(caller, out Player player))
53
return;
54
55
player.QuickSpawnItem(
56
player.GetSource_Misc("DeathMod:DebugResources"),
57
ModContent.ItemType<Soul>(),
58
999);
59
60
MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
61
for (ReaperStage stage = ReaperStage.StageI; stage <= ReaperStage.StageIII; stage++)
62
{
63
while (modPlayer.ReaperProgression.CanIssueSoulSeal(stage))
64
{
65
if (!ReaperProgressionSoulSealService.TryGrant(player, stage))
66
break;
67
}
68
}
69
70
caller.Reply(Language.GetTextValue("Mods.DeathMod.Commands.GiveSuccess"),
71
new Color(105, 235, 255));
72
}
73
}
74
75
public sealed class ReaperDebugResetCommand : ModCommand
76
{
77
public override string Command => "reaperdebugreset";
78
public override CommandType Type => CommandType.World;
79
public override string Usage => "/reaperdebugreset";
80
public override string Description => Language.GetTextValue("Mods.DeathMod.Commands.ResetDescription");
81
82
public override void Action(CommandCaller caller, string input, string[] args)
83
{
84
if (!ReaperDebugMode.TryGetPlayer(caller, out Player player))
85
return;
86
87
player.GetModPlayer<MyPlayer>().ResetReaperTreeForDebug();
88
caller.Reply(Language.GetTextValue("Mods.DeathMod.Commands.ResetSuccess"),
89
new Color(210, 115, 245));
90
}
91
}
@@ -90,74 +90,13 @@ internal sealed class DeathDomainBackdropTextureSystem : ModSystem
90
90
color = Vector3.Lerp(color, starColor, 0.42f + star * 0.58f);
91
91
}
92
92
93
// Sparse, fixed singularities live only on the farthest plate. They do not
94
// animate on their own: parallax reveals them like distant landmarks while
95
// the nearer mountains and blood mist intermittently hide them again.
96
ApplyDistantBlackHole(ref color, u, v, 0.18f, 0.27f, 0.034f, 0.22f, 1.7f);
97
ApplyDistantBlackHole(ref color, u, v, 1.43f, 0.64f, 0.022f, -0.38f, 4.9f);
98
ApplyDistantBlackHole(ref color, u, v, 2.72f, 0.36f, 0.029f, 0.31f, 7.3f);
99
ApplyDistantBlackHole(ref color, u, v, 3.61f, 0.72f, 0.019f, -0.17f, 9.8f);
93
// Background singularities are rendered as independent transient actors
94
// by DeathDomainVisualSystem. Keeping them out of this baked plate allows
95
// each one to form, live briefly, collapse, and leave a true empty interval.
100
96
101
97
return Opaque(color);
102
98
}
103
99
104
private static void ApplyDistantBlackHole(
105
ref Vector3 color,
106
float u,
107
float v,
108
float centerU,
109
float centerV,
110
float radius,
111
float tilt,
112
float seed)
113
{
114
float dx = u - centerU;
115
float dy = v - centerV;
116
float reach = radius * 3.35f;
117
if (Math.Abs(dx) > reach || Math.Abs(dy) > reach)
118
return;
119
120
float radialDistance = (float)Math.Sqrt(dx * dx + dy * dy);
121
if (radialDistance > reach)
122
return;
123
124
float angle = (float)Math.Atan2(dy, dx) - tilt;
125
126
float normalizedRadius = radialDistance / radius;
127
float edgeNoise = (float)Math.Sin(angle * 5f + seed) * 0.095f
128
+ (float)Math.Sin(angle * 11f - seed * 1.7f) * 0.042f;
129
float noisyEventHorizon = radius * (1f + edgeNoise);
130
131
// Match the player's domain core: a circular, noisy event horizon rather
132
// than an edge-on astronomical accretion disc.
133
float gravityWell = 1f - SmoothRange(radius * 0.96f, reach, radialDistance);
134
color *= 1f - gravityWell * 0.58f;
135
float core = 1f - SmoothRange(noisyEventHorizon * 0.78f, noisyEventHorizon, radialDistance);
136
color = Vector3.Lerp(color, new Vector3(0.0006f, 0f, 0.0035f), core * 0.992f);
137
138
float boundaryDistance = Math.Abs(radialDistance - noisyEventHorizon);
139
float noisyBoundary = 1f - SmoothRange(radius * 0.035f, radius * 0.19f, boundaryDistance);
140
float brokenBoundary = 0.48f + 0.52f * Smooth01(
141
((float)Math.Sin(angle * 9f + seed * 2.2f) + 0.55f) / 1.55f);
142
color += new Vector3(0.96f, 0.012f, 0.095f) * noisyBoundary * brokenBoundary * 0.82f;
143
144
// Five contracting spiral arms echo the suction waves around the player.
145
float spiralEnvelope = SmoothRange(0.94f, 1.08f, normalizedRadius)
146
* (1f - SmoothRange(2.75f, 3.35f, normalizedRadius));
147
float spiralPhase = angle * 5f - normalizedRadius * 10.5f + seed;
148
float spiralRidge = Smooth01(((float)Math.Sin(spiralPhase) + 0.3f) / 1.3f);
149
spiralRidge *= spiralRidge * spiralRidge;
150
float secondaryRidge = Smooth01(
151
((float)Math.Sin(spiralPhase + 1.25f + normalizedRadius * 1.7f) + 0.55f) / 1.55f);
152
secondaryRidge *= secondaryRidge;
153
color += new Vector3(0.72f, 0.008f, 0.07f)
154
* spiralEnvelope
155
* (spiralRidge * 0.72f + secondaryRidge * 0.2f);
156
157
float centralGlow = 1f - SmoothRange(0f, radius * 0.58f, radialDistance);
158
color += new Vector3(0.16f, 0.0015f, 0.028f) * centralGlow * centralGlow * 0.42f;
159
}
160
161
100
private static Color BuildMiddlePixel(float u, float v)
162
101
{
163
102
Vector3 color = Vector3.Zero;
@@ -460,6 +460,15 @@ internal class DeathDomainVisualSystem : ModSystem
460
460
noiseAmplitude,
461
461
masteryProgress,
462
462
reveal);
463
DrawTransientBackdropBlackHoles(
464
batch,
465
TextureAssets.MagicPixel.Value,
466
center,
467
new Vector2(coreRadius),
468
masteryProgress,
469
reveal,
470
coreRadius,
471
Main.worldID + index * 101);
463
472
}
464
473
}
465
474
}
@@ -524,6 +533,244 @@ internal class DeathDomainVisualSystem : ModSystem
524
533
viewHeight,
525
534
opacity);
526
535
}
536
537
DrawTransientBackdropBlackHoles(
538
batch,
539
TextureAssets.MagicPixel.Value,
540
destination + new Vector2(destinationWidth, destinationHeight) * 0.5f,
541
new Vector2(destinationWidth, destinationHeight) * 0.5f,
542
mastery,
543
reveal,
544
0f,
545
Main.worldID);
546
}
547
548
private static void DrawTransientBackdropBlackHoles(
549
SpriteBatch batch,
550
Texture2D pixel,
551
Vector2 center,
552
Vector2 halfExtent,
553
float mastery,
554
float domainReveal,
555
float clipRadius,
556
int seedGroup)
557
{
558
if (domainReveal <= 0.01f || halfExtent.X <= 8f || halfExtent.Y <= 8f)
559
return;
560
561
bool clipped = clipRadius > 0f;
562
int largeCount = clipped ? 1 : 2;
563
int smallCount = clipped ? 3 : 6;
564
float minimumExtent = Math.Min(halfExtent.X, halfExtent.Y);
565
float largeRadius = clipped
566
? MathHelper.Clamp(clipRadius * 0.13f, 13f, 52f)
567
: MathHelper.Clamp(minimumExtent * 0.115f, 58f, 138f);
568
float smallRadius = clipped
569
? MathHelper.Clamp(clipRadius * 0.058f, 7f, 25f)
570
: MathHelper.Clamp(minimumExtent * 0.043f, 20f, 52f);
571
572
for (int slot = 0; slot < largeCount + smallCount; slot++)
573
{
574
bool large = slot < largeCount;
575
int seed = seedGroup * 37 + slot * 193 + (large ? 701 : 1301);
576
if (!TryGetBackdropHoleAnimation(seed, large,
577
out float opacity, out float scale, out float collapse))
578
{
579
continue;
580
}
581
582
Vector2 position;
583
if (clipped)
584
{
585
float angle = Hash01(seed + 11) * MathHelper.TwoPi;
586
float distance = clipRadius * MathHelper.Lerp(
587
large ? 0.19f : 0.27f,
588
large ? 0.42f : 0.62f,
589
Hash01(seed + 23));
590
position = center + angle.ToRotationVector2() * distance;
591
}
592
else
593
{
594
position = center + new Vector2(
595
MathHelper.Lerp(-0.76f, 0.76f, Hash01(seed + 11)) * halfExtent.X,
596
MathHelper.Lerp(-0.68f, 0.68f, Hash01(seed + 23)) * halfExtent.Y);
597
}
598
599
float finalOpacity = opacity * domainReveal * MathHelper.Lerp(0.72f, 1f, mastery);
600
float rotation = Hash01(seed + 41) * MathHelper.TwoPi
601
+ Main.GlobalTimeWrappedHourly * (large ? 0.055f : -0.16f);
602
if (large)
603
{
604
DrawLargeBackdropBlackHole(batch, pixel, position,
605
largeRadius * scale, rotation, finalOpacity, collapse, mastery);
606
}
607
else
608
{
609
DrawSmallBackdropBlackHole(batch, pixel, position,
610
smallRadius * scale, rotation, finalOpacity, collapse, seed);
611
}
612
}
613
}
614
615
private static bool TryGetBackdropHoleAnimation(
616
int seed,
617
bool large,
618
out float opacity,
619
out float scale,
620
out float collapse)
621
{
622
float cycle = large ? 780f : 570f;
623
float visibleDuration = large ? 310f : 205f;
624
float introDuration = large ? 52f : 34f;
625
float outroDuration = large ? 72f : 48f;
626
float offset = Hash01(seed + 67) * cycle;
627
float age = PositiveModulo((float)Main.GameUpdateCount + offset, cycle);
628
if (age >= visibleDuration)
629
{
630
opacity = 0f;
631
scale = 0f;
632
collapse = 0f;
633
return false;
634
}
635
636
float intro = MathHelper.SmoothStep(0f, 1f, MathHelper.Clamp(age / introDuration, 0f, 1f));
637
float outro = MathHelper.SmoothStep(0f, 1f,
638
MathHelper.Clamp((visibleDuration - age) / outroDuration, 0f, 1f));
639
opacity = Math.Min(intro, outro);
640
collapse = 1f - outro;
641
float openingScale = MathHelper.Lerp(0.08f, 1f, 1f - (float)Math.Pow(1f - intro, 3f));
642
openingScale *= 1f + (float)Math.Sin(intro * MathHelper.Pi) * 0.12f;
643
scale = openingScale * MathHelper.Lerp(0.18f, 1f, outro);
644
return opacity > 0.005f && scale > 0.02f;
645
}
646
647
private static void DrawSmallBackdropBlackHole(
648
SpriteBatch batch,
649
Texture2D pixel,
650
Vector2 center,
651
float radius,
652
float rotation,
653
float opacity,
654
float collapse,
655
int seed)
656
{
657
if (radius <= 1f || opacity <= 0.001f)
658
return;
659
660
float verticalScale = MathHelper.Lerp(1f, 0.16f, collapse);
661
DeathDomainPrimitiveTextureSystem.DrawRadialGradient(
662
batch, center, radius * 1.62f, new Color(0, 0, 3) * (opacity * 0.86f));
663
DeathDomainPrimitiveTextureSystem.DrawRadialGradient(
664
batch, center, radius * 0.94f, Color.Black * opacity);
665
DrawEllipseArc(batch, pixel, center, radius * 1.34f, radius * 0.70f * verticalScale,
666
rotation, -2.65f, 2.2f, 30, new Color(176, 28, 255) * (opacity * 0.72f), 2.1f);
667
DrawEllipseArc(batch, pixel, center, radius * 1.62f, radius * 0.46f * verticalScale,
668
-rotation * 0.7f, 0.18f, 4.45f, 34, new Color(255, 28, 78) * (opacity * 0.82f), 1.35f);
669
for (int particle = 0; particle < 3; particle++)
670
{
671
float angle = rotation * (particle % 2 == 0 ? 1f : -0.8f)
672
+ particle * MathHelper.TwoPi / 3f + Hash01(seed + particle * 17) * 0.7f;
673
Vector2 position = center + new Vector2(
674
(float)Math.Cos(angle) * radius * 1.72f,
675
(float)Math.Sin(angle) * radius * 0.58f * verticalScale);
676
DrawCircle(batch, pixel, position, Math.Max(0.8f, radius * 0.055f), 8,
677
new Color(255, 104, 145) * opacity, 1.2f, angle);
678
}
679
}
680
681
private static void DrawLargeBackdropBlackHole(
682
SpriteBatch batch,
683
Texture2D pixel,
684
Vector2 center,
685
float radius,
686
float rotation,
687
float opacity,
688
float collapse,
689
float mastery)
690
{
691
if (radius <= 2f || opacity <= 0.001f)
692
return;
693
694
float verticalScale = MathHelper.Lerp(1f, 0.10f, collapse);
695
float coreX = radius * 1.18f;
696
float coreY = radius * 0.52f * verticalScale;
697
DeathDomainPrimitiveTextureSystem.DrawRadialGradient(
698
batch, center, radius * 2.25f, new Color(8, 0, 19) * (opacity * 0.68f));
699
DrawFilledEllipse(batch, pixel, center, coreX, Math.Max(1.2f, coreY),
700
new Color(0, 0, 2) * (opacity * 0.98f));
701
702
Color outer = Color.Lerp(new Color(116, 18, 215), new Color(225, 22, 82), mastery);
703
Color hot = Color.Lerp(new Color(238, 60, 166), new Color(255, 116, 82), mastery);
704
DrawEllipseArc(batch, pixel, center, radius * 2.12f, radius * 0.67f * verticalScale,
705
rotation, -2.92f, 2.48f, 54, outer * (opacity * 0.62f), 3.2f);
706
DrawEllipseArc(batch, pixel, center, radius * 1.72f, radius * 0.46f * verticalScale,
707
rotation + 0.08f, 0.05f, 3.12f, 44, hot * (opacity * 0.94f), 2.4f);
708
DrawEllipseArc(batch, pixel, center, radius * 2.52f, radius * 0.92f * verticalScale,
709
-rotation * 0.35f, -0.72f, 1.48f, 34, new Color(122, 86, 255) * (opacity * 0.42f), 1.3f);
710
711
// Offset lensing crescents make the large variant read as warped space,
712
// not as a scaled copy of the player's five-arm circular domain.
713
Vector2 lensOffset = rotation.ToRotationVector2() * radius * 0.19f;
714
DrawEllipseArc(batch, pixel, center + lensOffset, radius * 1.36f,
715
radius * 0.24f * verticalScale, rotation, -2.75f, -0.25f, 28,
716
new Color(255, 172, 196) * (opacity * 0.58f), 1.15f);
717
DrawEllipseArc(batch, pixel, center - lensOffset, radius * 1.36f,
718
radius * 0.24f * verticalScale, rotation, 0.36f, 2.75f, 28,
719
outer * (opacity * 0.46f), 1.05f);
720
}
721
722
private static void DrawFilledEllipse(
723
SpriteBatch batch,
724
Texture2D pixel,
725
Vector2 center,
726
float radiusX,
727
float radiusY,
728
Color color)
729
{
730
int bands = Math.Clamp((int)Math.Ceiling(radiusY), 6, 72);
731
for (int band = 0; band < bands; band++)
732
{
733
float normalizedY = (band + 0.5f) / bands * 2f - 1f;
734
float halfWidth = radiusX * (float)Math.Sqrt(Math.Max(0f, 1f - normalizedY * normalizedY));
735
float y = center.Y + normalizedY * radiusY;
736
DrawSegment(batch, pixel, new Vector2(center.X - halfWidth, y),
737
new Vector2(halfWidth * 2f, 0f), color, Math.Max(1f, radiusY * 2f / bands + 0.6f));
738
}
739
}
740
741
private static void DrawEllipseArc(
742
SpriteBatch batch,
743
Texture2D pixel,
744
Vector2 center,
745
float radiusX,
746
float radiusY,
747
float rotation,
748
float startAngle,
749
float endAngle,
750
int segments,
751
Color color,
752
float width)
753
{
754
Vector2 AxisPoint(float angle)
755
{
756
Vector2 local = new((float)Math.Cos(angle) * radiusX, (float)Math.Sin(angle) * radiusY);
757
return center + local.RotatedBy(rotation);
758
}
759
760
Vector2 previous = AxisPoint(startAngle);
761
for (int index = 1; index <= segments; index++)
762
{
763
float angle = MathHelper.Lerp(startAngle, endAngle, index / (float)segments);
764
Vector2 current = AxisPoint(angle);
765
DrawSegment(batch, pixel, previous, current - previous, color, width);
766
previous = current;
767
}
768
}
769
770
private static float Hash01(int seed)
771
{
772
double value = Math.Sin(seed * 12.9898 + 78.233) * 43758.5453;
773
return (float)(value - Math.Floor(value));
527
774
}
528
775
529
776
private static void GetUntransformedScreenBounds(
@@ -396,9 +396,27 @@ public class MyPlayer : ModPlayer
396
396
397
397
internal void ReceiveDeathDomainState(bool enabled)
398
398
{
399
bool opening = enabled && !DeathDomainEnabled;
399
400
DeathDomainEnabled = enabled;
400
401
if (!enabled)
401
402
deathDomainTimer = 0;
403
if (opening && Main.netMode != NetmodeID.Server && !Main.dedServ)
404
{
405
// Two restrained layers give the expansion both a low spatial rupture
406
// and a sharp soul-metal opening without requiring a custom asset.
407
SoundEngine.PlaySound(SoundID.Roar with
408
{
409
Volume = 0.42f,
410
Pitch = -0.48f,
411
PitchVariance = 0.06f
412
}, Player.Center);
413
SoundEngine.PlaySound(SoundID.Item122 with
414
{
415
Volume = 1.05f,
416
Pitch = -0.28f,
417
PitchVariance = 0.08f
418
}, Player.Center);
419
}
402
420
}
403
421
404
422
internal bool ServerCharacterStateReady => Main.netMode != NetmodeID.Server
@@ -801,6 +819,16 @@ public class MyPlayer : ModPlayer
801
819
SyncReaperCombat();
802
820
}
803
821
822
internal void ResetReaperTreeForDebug()
823
{
824
ReaperProgression.ResetTreeForDebug();
825
VisibleReaperForm = ReaperFormId.Base;
826
VisibleReaperStage = ReaperStage.StageI;
827
ClearReaperCombatRuntime(sync: true);
828
if (Main.netMode == NetmodeID.Server)
829
DeathMod.SendReaperProgressionTransaction(Player, Array.Empty<int>());
830
}
831
804
832
internal void WriteReaperProgression(BinaryWriter writer) => ReaperProgression.NetSend(writer);
805
833
806
834
internal void ReceiveReaperProgression(BinaryReader reader)
@@ -265,6 +265,23 @@ public sealed class ReaperProgressionState
265
265
LoadedDataVersion = CurrentDataVersion;
266
266
}
267
267
268
/// <summary>
269
/// Debug-only rollback of every sickle progression node. Issued soul seals are
270
/// retained and marked unspent so the same test character can replay all six
271
/// branches without farming bosses again.
272
/// </summary>
273
internal void ResetTreeForDebug()
274
{
275
foreach (ReaperBranchState branch in branches)
276
branch.Reset();
277
Array.Clear(commonNodeLevels);
278
Array.Clear(soulSealsSpent);
279
Array.Clear(soulSealsPending);
280
CurrentForm = ReaperFormId.Base;
281
LoadedDataVersion = CurrentDataVersion;
282
NormalizeSoulSealLedger();
283
}
284
268
285
public void SaveData(TagCompound rootTag, string key = DefaultSaveKey)
269
286
{
270
287
byte[] branchStages = new byte[branches.Length];
@@ -604,6 +604,16 @@ Mods: {
604
604
}
605
605
}
606
606
607
Commands: {
608
DebugWorldRequired: Debug commands are available only in worlds whose name contains "-debug".
609
PlayerRequired: This command must be run by an in-game player.
610
CharacterNotReady: Character data has not finished synchronizing with the server. Try again shortly.
611
GiveDescription: Grants 999 Soul Essence and all three tiers of Soul Seals in a -debug world.
612
GiveSuccess: Granted 999 Soul Essence; Awakening, Boundary, and Dominion Soul Seals are each stocked to the character cap of 6.
613
ResetDescription: Rolls back every character-owned sickle progression node in a -debug world.
614
ResetSuccess: Rolled back all six branch stages, 18 form skills, and every common node; issued Soul Seals are available again.
615
}
616
607
617
Messages: {
608
618
DeathDomainEnabled: Death Domain unfolded.
609
619
DeathDomainDisabled: Death Domain withdrawn.
@@ -604,6 +604,16 @@ Mods: {
604
604
}
605
605
}
606
606
607
Commands: {
608
DebugWorldRequired: 调试指令仅能在名称包含「-debug」的世界中使用。
609
PlayerRequired: 该指令必须由游戏中的玩家执行。
610
CharacterNotReady: 人物数据尚未完成服务端同步,请稍后重试。
611
GiveDescription: 在 -debug 世界中获得 999 灵魂精华与三个阶段的魂印。
612
GiveSuccess: 已获得 999 灵魂精华;初醒、破界、统御魂印均已补至人物上限 6 枚。
613
ResetDescription: 在 -debug 世界中回退人物的全部镰刀成长节点。
614
ResetSuccess: 已回退六分支阶段、18 项形态技能与全部通用节点;已签发魂印恢复为未消费状态。
615
}
616
607
617
Messages: {
608
618
DeathDomainEnabled: 死亡领域已展开。
609
619
DeathDomainDisabled: 死亡领域已收回。