-
Notifications
You must be signed in to change notification settings - Fork 33
Enhance GroundCraft animations and recipes #1171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ACaiCat
merged 2 commits into
UnrealMultiple:master
from
loguhan:codex/groundcraft-v1-1
Jun 30, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,283 @@ | ||
| using Microsoft.Xna.Framework; | ||
|
|
||
| using Terraria; | ||
| using Terraria.ID; | ||
| using TerrariaApi.Server; | ||
|
|
||
| using TShockAPI; | ||
|
|
||
| namespace GroundCraft; | ||
|
|
||
| public sealed partial class GroundCraft | ||
| { | ||
| private void UpdateCraftAnimations() | ||
| { | ||
| for (int i = _craftAnimations.Count - 1; i >= 0; i--) | ||
| { | ||
| CraftAnimation animation = _craftAnimations[i]; | ||
| animation.Age++; | ||
|
|
||
| if (!AnimationItemsStillValid(animation)) | ||
| { | ||
| CancelCraftAnimation(animation); | ||
| _craftAnimations.RemoveAt(i); | ||
| continue; | ||
| } | ||
|
|
||
| int duration = AnimationDuration(animation); | ||
| float progress = Math.Clamp(animation.Age / (float)duration, 0f, 1f); | ||
| Vector2 target = CraftTarget(animation); | ||
|
|
||
| for (int j = 0; j < animation.Ingredients.Count; j++) | ||
| MoveAnimatedIngredient(animation, animation.Ingredients[j], j, animation.Ingredients.Count, progress, target, animation.Age, duration); | ||
|
|
||
| if (animation.Age < duration) | ||
| continue; | ||
|
|
||
| CompleteCraftAnimation(animation, target); | ||
| _craftAnimations.RemoveAt(i); | ||
| } | ||
| } | ||
|
|
||
| private static int AnimationDuration(CraftAnimation animation) | ||
| { | ||
| return animation.IsZenith ? ZenithAnimationTicks : CraftAnimationTicks; | ||
| } | ||
|
|
||
| private static Vector2 CraftTarget(CraftAnimation animation) | ||
| { | ||
| float lift = animation.IsZenith ? ZenithAnimationLiftPixels : CraftAnimationLiftPixels; | ||
| return animation.Center + new Vector2(0f, -lift); | ||
| } | ||
|
|
||
| private static bool AnimationItemsStillValid(CraftAnimation animation) | ||
| { | ||
| foreach (AnimatedIngredient ingredient in animation.Ingredients) | ||
| { | ||
| if (ingredient.Index < 0 || ingredient.Index >= Main.item.Length) | ||
| return false; | ||
|
|
||
| WorldItem item = Main.item[ingredient.Index]; | ||
| if (!item.active || item.type != ingredient.Type || item.stack != ingredient.Stack) | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| private static void MoveAnimatedIngredient( | ||
| CraftAnimation animation, | ||
| AnimatedIngredient ingredient, | ||
| int order, | ||
| int total, | ||
| float progress, | ||
| Vector2 target, | ||
| int age, | ||
| int duration) | ||
| { | ||
| WorldItem item = Main.item[ingredient.Index]; | ||
| Vector2 visualCenter = AnimatedCenter(animation, ingredient, order, total, progress, target); | ||
| int syncEvery = AnimationSyncInterval(animation); | ||
| float lookaheadTicks = Math.Max(1, syncEvery); | ||
| float lookaheadProgress = Math.Min(progress + lookaheadTicks / duration, 1f); | ||
| Vector2 predictedCenter = AnimatedCenter(animation, ingredient, order, total, lookaheadProgress, target); | ||
|
|
||
| item.position = visualCenter - new Vector2(ingredient.Width / 2f, ingredient.Height / 2f); | ||
| item.velocity = (predictedCenter - visualCenter) / lookaheadTicks; | ||
| LockAnimatedItem(item); | ||
|
|
||
| if (animation.IsZenith) | ||
| SpawnZenithItemTrail(visualCenter, item.velocity, order, age); | ||
|
|
||
| if (age == 1 || age % syncEvery == 0) | ||
| SyncItemNoGrab(ingredient.Index); | ||
| } | ||
|
|
||
| private static int AnimationSyncInterval(CraftAnimation animation) | ||
| { | ||
| return animation.IsZenith ? ZenithAnimationSyncEveryTicks : CraftAnimationSyncEveryTicks; | ||
| } | ||
|
|
||
| private static Vector2 AnimatedCenter(CraftAnimation animation, AnimatedIngredient ingredient, int order, int total, float progress, Vector2 target) | ||
| { | ||
| return animation.IsZenith | ||
| ? ZenithAnimatedCenter(animation, ingredient, order, total, progress, target) | ||
| : DefaultAnimatedCenter(ingredient, order, total, progress, target); | ||
| } | ||
|
|
||
| private static Vector2 DefaultAnimatedCenter(AnimatedIngredient ingredient, int order, int total, float progress, Vector2 target) | ||
| { | ||
| float eased = EaseOutCubic(progress); | ||
| float phase = MathHelper.TwoPi * (CraftAnimationTurns * progress + order / Math.Max(1f, total)); | ||
| float radius = CraftAnimationOrbitRadiusPixels * (1f - eased); | ||
| Vector2 spiral = new(MathF.Cos(phase) * radius, MathF.Sin(phase) * radius * 0.55f); | ||
| return Vector2.Lerp(ingredient.StartCenter, target, eased) + spiral; | ||
| } | ||
|
|
||
| private static Vector2 ZenithAnimatedCenter(CraftAnimation animation, AnimatedIngredient ingredient, int order, int total, float progress, Vector2 target) | ||
| { | ||
| float eased = EaseInOutCubic(progress); | ||
| float gather = EaseOutCubic(Math.Clamp(progress * 1.6f, 0f, 1f)); | ||
| float phase = MathHelper.TwoPi * (ZenithAnimationTurns * progress + order / Math.Max(1f, total)); | ||
| float radius = ZenithAnimationOrbitRadiusPixels * (1f - progress * progress); | ||
| Vector2 spiral = new(MathF.Cos(phase) * radius, MathF.Sin(phase) * radius * 0.42f); | ||
| float parabola = ZenithAnimationParabolaPixels * 4f * progress * (1f - progress); | ||
| Vector2 orbitCenter = Vector2.Lerp(animation.Center, target, eased) + new Vector2(0f, -parabola); | ||
| Vector2 orbitPoint = orbitCenter + spiral; | ||
|
|
||
| return Vector2.Lerp(ingredient.StartCenter, orbitPoint, gather); | ||
| } | ||
|
|
||
| private static float EaseOutCubic(float value) | ||
| { | ||
| float inverse = 1f - value; | ||
| return 1f - inverse * inverse * inverse; | ||
| } | ||
|
|
||
| private static float EaseInOutCubic(float value) | ||
| { | ||
| return value < 0.5f | ||
| ? 4f * value * value * value | ||
| : 1f - MathF.Pow(-2f * value + 2f, 3f) / 2f; | ||
| } | ||
|
|
||
| private void CompleteCraftAnimation(CraftAnimation animation, Vector2 target) | ||
| { | ||
| foreach (AnimatedIngredient ingredient in animation.Ingredients) | ||
| { | ||
| WorldItem item = Main.item[ingredient.Index]; | ||
| item.TurnToAir(true); | ||
| ClearConsumedItem(ingredient.Index); | ||
| _lockedItemIndexes.Remove(ingredient.Index); | ||
| _stableScans.Remove(ingredient.Index); | ||
| } | ||
|
|
||
| SpawnDeferredLeftovers(animation); | ||
| SpawnItem(animation.Recipe.OutputType, animation.OutputStack, target); | ||
| if (animation.IsZenith) | ||
| SpawnZenithFinale(target); | ||
| else | ||
| SpawnCraftEffect(target); | ||
|
|
||
| _runtime.CraftBatches++; | ||
| _runtime.Crafts += animation.CraftCount; | ||
| TShock.Log.ConsoleInfo(GetString($"[GroundCraft] 螺旋融合完成:{ItemName(animation.Recipe.OutputType)} x{animation.OutputStack}。")); | ||
| NotifyNearby(target, animation.Recipe, animation.OutputStack, animation.ConsumedStacks); | ||
| } | ||
|
|
||
| private void CancelCraftAnimation(CraftAnimation animation) | ||
| { | ||
| foreach (AnimatedIngredient ingredient in animation.Ingredients) | ||
| { | ||
| _lockedItemIndexes.Remove(ingredient.Index); | ||
| if (ingredient.Index < 0 || ingredient.Index >= Main.item.Length) | ||
| continue; | ||
|
|
||
| WorldItem item = Main.item[ingredient.Index]; | ||
| if (!item.active) | ||
| continue; | ||
|
|
||
| item.noGrabDelay = 0; | ||
| item.playerIndexTheItemIsReservedFor = 255; | ||
| item.velocity = Vector2.Zero; | ||
| SyncItem(ingredient.Index); | ||
| NetMessage.SendData(MessageID.ItemOwner, -1, -1, null, ingredient.Index); | ||
| } | ||
|
|
||
| SpawnDeferredLeftovers(animation); | ||
| } | ||
|
|
||
| private static void SpawnDeferredLeftovers(CraftAnimation animation) | ||
| { | ||
| foreach (DeferredLeftover leftover in animation.Leftovers) | ||
| SpawnItem(leftover.Type, leftover.Stack, leftover.Center); | ||
| } | ||
|
|
||
| private static void SpawnZenithItemTrail(Vector2 center, Vector2 velocity, int order, int age) | ||
| { | ||
| if ((age + order * ZenithTrailOrderOffsetTicks) % ZenithTrailEveryTicks != 0) | ||
| return; | ||
|
|
||
| Vector2 trailPoint = center; | ||
| if (velocity.LengthSquared() > 0.01f) | ||
| trailPoint -= Vector2.Normalize(velocity) * 14f; | ||
|
|
||
| NetMessage.SendData(MessageID.PoofOfSmoke, -1, -1, null, (int)trailPoint.X, trailPoint.Y); | ||
| } | ||
|
|
||
| private void ReleaseCraftAnimations() | ||
| { | ||
| foreach (CraftAnimation animation in _craftAnimations) | ||
| CancelCraftAnimation(animation); | ||
|
|
||
| _craftAnimations.Clear(); | ||
| _lockedItemIndexes.Clear(); | ||
| } | ||
|
|
||
| private void OnGetData(GetDataEventArgs args) | ||
| { | ||
| if (args.Handled || !TryReadLockedItemIndex(args, out int index)) | ||
| return; | ||
|
|
||
| if (!_lockedItemIndexes.Contains(index)) | ||
| return; | ||
|
|
||
| args.Handled = true; | ||
| ReassertLockedItem(index); | ||
| } | ||
|
|
||
| private static bool TryReadLockedItemIndex(GetDataEventArgs args, out int index) | ||
| { | ||
| index = -1; | ||
| int msgId = (int)args.MsgID; | ||
| return msgId switch | ||
| { | ||
| MessageID.SyncItem => TryReadItemIndexAt(args, 0, out index), | ||
| MessageID.ItemOwner => TryReadItemIndexAt(args, 0, out index), | ||
| MessageID.ReleaseItemOwnership => TryReadItemIndexAt(args, 0, out index), | ||
| MessageID.SyncItemDespawn => TryReadItemIndexAt(args, 0, out index), | ||
| _ => false | ||
| }; | ||
| } | ||
|
|
||
| private static bool TryReadItemIndexAt(GetDataEventArgs args, int payloadOffset, out int index) | ||
| { | ||
| index = -1; | ||
| const int itemIndexSize = sizeof(short); | ||
| if (payloadOffset < 0 || args.Length < payloadOffset + itemIndexSize || args.Index + payloadOffset + itemIndexSize > args.Msg.readBuffer.Length) | ||
| return false; | ||
|
|
||
| index = BitConverter.ToInt16(args.Msg.readBuffer, args.Index + payloadOffset); | ||
| return index >= 0 && index < Main.maxItems && index < Main.item.Length; | ||
| } | ||
|
|
||
| private static void LockAnimatedItem(int index) | ||
| { | ||
| WorldItem item = Main.item[index]; | ||
| LockAnimatedItem(item); | ||
| } | ||
|
|
||
| private static void LockAnimatedItem(WorldItem item) | ||
| { | ||
| item.noGrabDelay = CraftAnimationNoGrabDelay; | ||
| item.playerIndexTheItemIsReservedFor = 255; | ||
| item.ownIgnore = -1; | ||
| item.ownTime = 0; | ||
| item.beingGrabbed = false; | ||
| item.keepTime = Math.Max(item.keepTime, 10); | ||
| } | ||
|
|
||
| private void ReassertLockedItem(int index) | ||
| { | ||
| if (index < 0 || index >= Main.item.Length) | ||
| return; | ||
|
|
||
| WorldItem item = Main.item[index]; | ||
| if (!item.active) | ||
| return; | ||
|
|
||
| LockAnimatedItem(item); | ||
| SyncItemNoGrab(index); | ||
| NetMessage.SendData(MessageID.ItemOwner, -1, -1, null, index); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): 取消合成动画时似乎会导致配方材料堆叠数量被复制。
TryStartCraftAnimation会将每个材料物品的stack减少到被消耗的数量,并把多余的部分存放在DeferredLeftover中。完成时,CompleteCraftAnimation会移除这些材料物品(TurnToAir),并生成这些剩余物品,所以总的堆叠数量是正确的。而
CancelCraftAnimation则是把减少过堆叠的物品保留在世界中(只解锁/重置它们),同时仍然调用SpawnDeferredLeftovers。因此在取消之后,世界中会同时存在被消耗的堆叠和剩余的堆叠,相当于复制了物品。取消操作要么应该恢复原始堆叠并跳过生成剩余物品,要么像完成路径那样移除减少后的物品,只生成对应数量的堆叠。另一种方案是将任何对
stack的修改推迟到完成时再执行,这样完成和取消路径都会更简单,并且不会产生重复的物品。Original comment in English
issue (bug_risk): Cancelling a craft animation appears to duplicate ingredient stacks.
TryStartCraftAnimationreduces each ingredient item’sstackto the consumed amount and stores the remainder inDeferredLeftover. On completion,CompleteCraftAnimationremoves the ingredient items (TurnToAir) and spawns the leftovers, so total stacks stay correct.CancelCraftAnimationinstead leaves the reduced-stack items in the world (only unlocking/resetting them) and still callsSpawnDeferredLeftovers. So after cancellation you have both the consumed stacks and the leftovers, effectively duplicating items.Cancellation should either restore the original stacks and skip spawning leftovers, or mirror completion by removing the reduced items and only spawning the appropriate stacks. Alternatively, defer any
stackmodification until completion so both completion and cancellation paths are simpler and non-duplicating.