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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

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

XFEstudio/DeathMod

feat: add debug commands and transient domain holes

2caad32
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

7 个文件 +406 -64
Added Commands/ReaperDebugCommands.cs +91 -0
@@ -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 }
Modified Common/DeathDomainBackdropTextureSystem.cs +3 -64
@@ -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;
Modified Common/DeathDomainVisualSystem.cs +247 -0
@@ -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(
Modified Common/MyPlayer.cs +28 -0
@@ -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)
Modified Common/ReaperProgressionState.cs +17 -0
@@ -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];
Modified Localization/en-US.hjson +10 -0
@@ -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.
Modified Localization/zh-Hans.hjson +10 -0
@@ -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: 死亡领域已收回。