From 407e5fa0cf1a0aeb8812717c8b1773918a7d8352 Mon Sep 17 00:00:00 2001 From: Evgeniy Sergeev Date: Thu, 18 Jun 2026 01:36:03 +0300 Subject: [PATCH] Implemented worm and sonic tank drawing --- .../Graphics/D2DistortionRenderable.cs | 78 +++++ .../Graphics/D2DistortionRenderer.cs | 171 ++++++++++ OpenRA.Mods.D2/Projectiles/D2SonicBeam.cs | 294 ++++++++++++++++++ .../Traits/Render/D2DistortionTrail.cs | 197 ++++++++++++ .../glsl/postprocess_textured_distortion.frag | 38 +++ mods/d2/mod.yaml | 1 + mods/d2/rules/sandworm.yaml | 15 +- mods/d2/rules/world.yaml | 6 + mods/d2/sequences/misc.yaml | 5 + mods/d2/sequences/sandworm.yaml | 17 +- mods/d2/weapons/other.yaml | 30 -- mods/d2/weapons/sound.yaml | 30 ++ 12 files changed, 828 insertions(+), 54 deletions(-) create mode 100644 OpenRA.Mods.D2/Graphics/D2DistortionRenderable.cs create mode 100644 OpenRA.Mods.D2/Graphics/D2DistortionRenderer.cs create mode 100644 OpenRA.Mods.D2/Projectiles/D2SonicBeam.cs create mode 100644 OpenRA.Mods.D2/Traits/Render/D2DistortionTrail.cs create mode 100644 mods/d2/glsl/postprocess_textured_distortion.frag create mode 100644 mods/d2/weapons/sound.yaml diff --git a/OpenRA.Mods.D2/Graphics/D2DistortionRenderable.cs b/OpenRA.Mods.D2/Graphics/D2DistortionRenderable.cs new file mode 100644 index 0000000..6f2e81e --- /dev/null +++ b/OpenRA.Mods.D2/Graphics/D2DistortionRenderable.cs @@ -0,0 +1,78 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 The d2 mod Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Primitives; + +namespace OpenRA.Mods.D2.Graphics +{ + public enum D2DistortionStyle { Sand, Sonic } + + public readonly struct D2DistortionRenderable : IRenderable, IFinalizedRenderable + { + readonly Sprite sprite; + readonly D2DistortionStyle style; + + public D2DistortionRenderable(WPos pos, Sprite sprite, D2DistortionStyle style) + { + Pos = pos; + this.sprite = sprite; + this.style = style; + } + + public WPos Pos { get; } + public int ZOffset => 0; + public bool IsDecoration => true; + + public IRenderable WithZOffset(int newOffset) { return this; } + public IRenderable OffsetBy(in WVec vec) + { + return new D2DistortionRenderable(Pos + vec, sprite, style); + } + + public IRenderable AsDecoration() { return this; } + public IFinalizedRenderable PrepareRender(WorldRenderer wr) + { + // Queue during PrepareRender instead of Render so each renderer has its work ready + // when its configured post-process pass runs. Sand currently uses AfterActors; sonic + // remains later on AfterWorld so its wave can bend the completed world image. + var renderStyle = style; + var renderer = wr.World.WorldActor.TraitsImplementing().FirstOrDefault(r => r.Accepts(renderStyle)); + if (renderer == null) + return this; + + renderer.DrawSprite(wr.Screen3DPxPosition(Pos), sprite, style); + + return this; + } + + public void Render(WorldRenderer wr) { } + + public void RenderDebugGeometry(WorldRenderer wr) + { + var bounds = ScreenBounds(wr); + if (!bounds.IsEmpty) + Game.Renderer.RgbaColorRenderer.DrawRect( + new float3(bounds.Left, bounds.Top, 0), + new float3(bounds.Right, bounds.Bottom, 0), 1, Color.Red); + } + + public Rectangle ScreenBounds(WorldRenderer wr) + { + var center = wr.Viewport.WorldToViewPx(wr.Screen3DPxPosition(Pos)); + var width = (int)Math.Ceiling(sprite.Size.X); + var height = (int)Math.Ceiling(sprite.Size.Y); + return new Rectangle(center.X - width / 2, center.Y - height / 2, width, height); + } + } +} diff --git a/OpenRA.Mods.D2/Graphics/D2DistortionRenderer.cs b/OpenRA.Mods.D2/Graphics/D2DistortionRenderer.cs new file mode 100644 index 0000000..516ad65 --- /dev/null +++ b/OpenRA.Mods.D2/Graphics/D2DistortionRenderer.cs @@ -0,0 +1,171 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 The d2 mod Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.D2.Graphics +{ + [TraitLocation(SystemActors.World)] + [Desc("Renders Dune 2 style screen-space pixel distortion effects.")] + public class D2DistortionRendererInfo : TraitInfo + { + public readonly string FragmentShader = "d2|glsl/postprocess_textured_distortion.frag"; + public readonly PostProcessPassType PassType = PostProcessPassType.AfterWorld; + public readonly HashSet Styles = new() { D2DistortionStyle.Sand, D2DistortionStyle.Sonic }; + + public override object Create(ActorInitializer init) { return new D2DistortionRenderer(this); } + } + + public sealed class D2DistortionRenderer : IRenderPostProcessPass, INotifyActorDisposing + { + // Renderables enqueue these records during normal world rendering. The post-process pass + // then replays OpenDUNE-style blurred sprites against the completed world texture. + readonly struct Distortion + { + public readonly D2DistortionStyle Style; + public readonly float3 Pos; + public readonly Sprite Sprite; + public readonly int BlurOffset; + + public Distortion(D2DistortionStyle style, float3 pos, Sprite sprite, int blurOffset) + { + Style = style; + Pos = pos; + Sprite = sprite; + BlurOffset = blurOffset; + } + } + + sealed class D2DistortionShaderBindings : IShaderBindings + { + public D2DistortionShaderBindings(string fragmentShader) + { + // Reuse the engine's textured post-process vertex shader. It interprets vertex + // positions as local screen-space pixel offsets from the Pos uniform and passes + // through sprite-sheet UVs for the fragment shader's mask lookup. + VertexShaderName = "postprocess_textured"; + VertexShaderCode = ShaderBindings.GetShaderCode("postprocess_textured.vert"); + FragmentShaderName = fragmentShader; + + using (var stream = Game.ModData.DefaultFileSystem.Open(fragmentShader)) + using (var reader = new StreamReader(stream)) + FragmentShaderCode = reader.ReadToEnd(); + } + + public string VertexShaderName { get; } + public string VertexShaderCode { get; } + public string FragmentShaderName { get; } + public string FragmentShaderCode { get; } + public int Stride => Attributes.Sum(a => a.Components * 4); + + public ShaderVertexAttribute[] Attributes { get; } = + { + new("aVertexPosition", ShaderVertexAttributeType.Float, 2, 0), + new("aVertexTexCoord", ShaderVertexAttributeType.Float, 2, 8) + }; + } + + readonly Renderer renderer; + readonly IShader shader; + readonly D2DistortionRendererInfo info; + readonly IVertexBuffer buffer; + readonly List distortions = new(); + readonly RenderPostProcessPassTexturedVertex[] vertices = new RenderPostProcessPassTexturedVertex[6]; + static readonly int[] BlurOffsets = { 1, 3, 2, 5, 4, 3, 2, 1 }; + static int blurIndex; + static int blurTickCounter; + const int BlurTickInterval = 6; + + public D2DistortionRenderer(D2DistortionRendererInfo info) + { + this.info = info; + renderer = Game.Renderer; + shader = renderer.CreateShader(new D2DistortionShaderBindings(info.FragmentShader)); + buffer = renderer.CreateVertexBuffer(6); + } + + public bool Accepts(D2DistortionStyle style) + { + return info.Styles.Contains(style); + } + + public void DrawSprite(float3 pos, Sprite sprite, D2DistortionStyle style) + { + // BlurOffset больше не вычисляется здесь — только в Draw(), + // чтобы blurIndex продвигался один раз за рендер-пасс, а не за спрайт. + distortions.Add(new Distortion(style, pos, sprite, 0)); + } + + PostProcessPassType IRenderPostProcessPass.Type => info.PassType; + bool IRenderPostProcessPass.Enabled => distortions.Count > 0; + + void UpdateVertices(in Distortion d) + { + var halfWidth = d.Sprite.Size.X / 2; + var halfHeight = d.Sprite.Size.Y / 2; + vertices[0] = new RenderPostProcessPassTexturedVertex(-halfWidth, -halfHeight, d.Sprite.Left, d.Sprite.Top); + vertices[1] = new RenderPostProcessPassTexturedVertex(halfWidth, -halfHeight, d.Sprite.Right, d.Sprite.Top); + vertices[2] = new RenderPostProcessPassTexturedVertex(halfWidth, halfHeight, d.Sprite.Right, d.Sprite.Bottom); + vertices[3] = new RenderPostProcessPassTexturedVertex(halfWidth, halfHeight, d.Sprite.Right, d.Sprite.Bottom); + vertices[4] = new RenderPostProcessPassTexturedVertex(-halfWidth, halfHeight, d.Sprite.Left, d.Sprite.Bottom); + vertices[5] = new RenderPostProcessPassTexturedVertex(-halfWidth, -halfHeight, d.Sprite.Left, d.Sprite.Top); + } + + void IRenderPostProcessPass.Draw(WorldRenderer wr) + { + if (++blurTickCounter >= BlurTickInterval) + { + blurTickCounter = 0; + blurIndex = (blurIndex + 1) % BlurOffsets.Length; + } + + var scroll = wr.Viewport.TopLeft; + var size = renderer.WorldFrameBufferSize; + var width = 2f / (renderer.WorldDownscaleFactor * size.Width); + var height = 2f / (renderer.WorldDownscaleFactor * size.Height); + + shader.SetVec("Scroll", scroll.X, scroll.Y); + shader.SetVec("p1", width, height); + shader.SetVec("p2", -1, -1); + shader.SetTexture("WorldTexture", Game.Renderer.WorldBufferSnapshot()); + shader.PrepareRender(); + + for (var i = 0; i < distortions.Count; i++) + { + var d = distortions[i]; + // Каждый сегмент получает следующий шаг таблицы — как три последовательных + // вызова GUI_DrawSprite в оригинале в рамках одного игрового кадра. + var blurOffset = BlurOffsets[(blurIndex + i) % BlurOffsets.Length]; + + UpdateVertices(d); + buffer.SetData(vertices, 6); + + shader.SetVec("Pos", d.Pos.X, d.Pos.Y); + shader.SetVec("Style", (float)d.Style); + shader.SetVec("BlurOffset", blurOffset); + shader.SetVec("MaskChannel", (float)d.Sprite.Channel); + shader.SetTexture("MaskTexture", d.Sprite.Sheet.GetTexture()); + renderer.DrawBatch(buffer, shader, 0, 6, PrimitiveType.TriangleList); + } + + distortions.Clear(); + } + + void INotifyActorDisposing.Disposing(Actor self) + { + buffer.Dispose(); + } + } +} diff --git a/OpenRA.Mods.D2/Projectiles/D2SonicBeam.cs b/OpenRA.Mods.D2/Projectiles/D2SonicBeam.cs new file mode 100644 index 0000000..79c5f0b --- /dev/null +++ b/OpenRA.Mods.D2/Projectiles/D2SonicBeam.cs @@ -0,0 +1,294 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 The d2 mod Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.GameRules; +using OpenRA.Graphics; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.D2.Graphics; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.D2.Projectiles +{ + [Desc("Dune 2 sonic beam that damages like AreaBeam but renders as screen-space distortion.")] + public class D2SonicBeamInfo : IProjectileInfo + { + [Desc("Projectile speed in WDist / tick, two values indicate a randomly picked velocity per beam.")] + public readonly WDist[] Speed = { new(128) }; + + [Desc("The maximum duration (in ticks) of each beam burst.")] + public readonly int Duration = 10; + + [Desc("The number of ticks between the beam causing warhead impacts in its area of effect.")] + public readonly int DamageInterval = 3; + + [Desc("The width of the beam.")] + public readonly WDist Width = new(512); + + [Desc("The shape of the beam. Kept for AreaBeam YAML compatibility.")] + public readonly BeamRenderableShape Shape = BeamRenderableShape.Cylindrical; + + [Desc("How far beyond the target the projectile keeps on travelling.")] + public readonly WDist BeyondTargetRange = new(0); + + [Desc("The minimum distance the beam travels.")] + public readonly WDist MinDistance = WDist.Zero; + + [Desc("Damage modifier applied at each range step.")] + public readonly int[] Falloff = { 100, 100 }; + + [Desc("Ranges at which each Falloff step is defined.")] + public readonly WDist[] Range = { WDist.Zero, new(int.MaxValue) }; + + [Desc("The maximum/constant/incremental inaccuracy used in conjunction with the InaccuracyType property.")] + public readonly WDist Inaccuracy = WDist.Zero; + + [Desc("Controls the way inaccuracy is calculated. Possible values are " + + "'Maximum' - scale from 0 to max with range, " + + "'PerCellIncrement' - scale from 0 with range, " + + "'Absolute' - use set value regardless of range.")] + public readonly InaccuracyType InaccuracyType = InaccuracyType.Maximum; + + [Desc("Can this projectile be blocked when hitting actors with an IBlocksProjectiles trait.")] + public readonly bool Blockable = false; + + [Desc("Does the beam follow the target.")] + public readonly bool TrackTarget = false; + + [Desc("Should the screen distortion be visually rendered?")] + public readonly bool RenderBeam = true; + + [Desc("Equivalent to sequence ZOffset. Kept for AreaBeam YAML compatibility.")] + public readonly int ZOffset = 0; + + [Desc("Kept for AreaBeam YAML compatibility. D2SonicBeam does not draw a colored beam.")] + public readonly Color Color = Color.Cyan; + + [Desc("Kept for AreaBeam YAML compatibility. D2SonicBeam does not draw a colored beam.")] + public readonly bool UsePlayerColor = false; + + public readonly string Image = "sonic_blast"; + [SequenceReference(nameof(Image))] + public readonly string Sequence = "idle"; + + public IProjectile Create(ProjectileArgs args) { return new D2SonicBeam(this, args); } + } + + public class D2SonicBeam : IProjectile, ISync + { + // This is intentionally a copy of AreaBeam's movement and damage model with only the + // visual Render() path swapped out. Keeping the tick logic local avoids changing Common + // projectile behavior while letting D2 draw the original-style screen distortion. + readonly D2SonicBeamInfo info; + readonly ProjectileArgs args; + readonly AttackBase actorAttackBase; + readonly WDist speed; + readonly WDist weaponRange; + readonly Sprite sprite; + + [Sync] + WPos headPos; + + [Sync] + WPos tailPos; + + [Sync] + WPos target; + + int length; + WAngle towardsTargetFacing; + int headTicks; + int tailTicks; + bool isHeadTravelling = true; + bool isTailTravelling; + bool continueTracking = true; + + bool IsBeamComplete => !isHeadTravelling && headTicks >= length && !isTailTravelling && tailTicks >= length; + + public D2SonicBeam(D2SonicBeamInfo info, ProjectileArgs args) + { + this.info = info; + this.args = args; + actorAttackBase = args.SourceActor.Trait(); + sprite = args.SourceActor.World.Map.Sequences.GetSequence(info.Image, info.Sequence).GetSprite(0); + + var world = args.SourceActor.World; + if (info.Speed.Length > 1) + speed = new WDist(world.SharedRandom.Next(info.Speed[0].Length, info.Speed[1].Length)); + else + speed = info.Speed[0]; + + headPos = args.Source; + tailPos = headPos; + + target = args.PassiveTarget; + if (info.Inaccuracy.Length > 0) + { + var maxInaccuracyOffset = OpenRA.Mods.Common.Util.GetProjectileInaccuracy(info.Inaccuracy.Length, info.InaccuracyType, args); + target += WVec.FromPDF(world.SharedRandom, 2) * maxInaccuracyOffset / 1024; + } + + towardsTargetFacing = (target - headPos).Yaw; + UpdateTargetForOvershoot(); + + length = Math.Max((target - headPos).Length / speed.Length, 1); + weaponRange = new WDist(OpenRA.Mods.Common.Util.ApplyPercentageModifiers(args.Weapon.Range.Length, args.RangeModifiers)); + } + + void UpdateTargetForOvershoot() + { + var dir = new WVec(0, -1024, 0).Rotate(WRot.FromYaw(towardsTargetFacing)); + var dist = (args.SourceActor.CenterPosition - target).Length; + int extraDist; + if (info.MinDistance.Length > dist) + { + if (info.MinDistance.Length - dist < info.BeyondTargetRange.Length) + extraDist = info.BeyondTargetRange.Length; + else + extraDist = info.MinDistance.Length - dist; + } + else + extraDist = info.BeyondTargetRange.Length; + + target += dir * extraDist / 1024; + } + + void TrackTarget() + { + if (!continueTracking) + return; + + if (args.GuidedTarget.IsValidFor(args.SourceActor)) + { + var guidedTargetPos = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(args.Source); + var targetDistance = new WDist((guidedTargetPos - args.Source).Length); + + if (targetDistance > weaponRange + info.BeyondTargetRange) + StopTargeting(); + else + { + target = guidedTargetPos; + towardsTargetFacing = (target - args.Source).Yaw; + + var dir = new WVec(0, -1024, 0).Rotate(WRot.FromYaw(towardsTargetFacing)); + target += dir * info.BeyondTargetRange.Length / 1024; + } + } + } + + void StopTargeting() + { + continueTracking = false; + isTailTravelling = true; + } + + public void Tick(World world) + { + if (info.TrackTarget) + TrackTarget(); + + if (++headTicks >= length) + { + headPos = target; + isHeadTravelling = false; + } + else if (isHeadTravelling) + headPos = WPos.LerpQuadratic(args.Source, target, WAngle.Zero, headTicks, length); + + if (tailTicks <= 0 && args.SourceActor.IsInWorld && !args.SourceActor.IsDead) + { + args.Source = args.CurrentSource(); + tailPos = args.Source; + } + + var outOfWeaponRange = weaponRange + info.BeyondTargetRange < new WDist((args.PassiveTarget - args.Source).Length); + if ((headTicks >= info.Duration && !isTailTravelling) || args.SourceActor.IsDead || + !actorAttackBase.IsAiming || outOfWeaponRange) + StopTargeting(); + + if (isTailTravelling) + { + if (++tailTicks >= length) + { + tailPos = target; + isTailTravelling = false; + } + else + tailPos = WPos.LerpQuadratic(args.Source, target, WAngle.Zero, tailTicks, length); + } + + if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, args.SourceActor.Owner, tailPos, headPos, info.Width, out var blockedPos)) + { + headPos = blockedPos; + target = headPos; + length = Math.Min(headTicks, length); + } + + if (headTicks % info.DamageInterval == 0) + { + var actors = world.FindActorsOnLine(tailPos, headPos, info.Width); + foreach (var a in actors) + { + var adjustedModifiers = args.DamageModifiers.Append(GetFalloff((args.Source - a.CenterPosition).Length)); + + var warheadArgs = new WarheadArgs(args) + { + ImpactOrientation = new WRot(WAngle.Zero, OpenRA.Mods.Common.Util.GetVerticalAngle(args.Source, target), args.CurrentMuzzleFacing()), + ImpactPosition = a.CenterPosition, + DamageModifiers = adjustedModifiers.ToArray(), + }; + + args.Weapon.Impact(Target.FromActor(a), warheadArgs); + } + } + + if (IsBeamComplete) + world.AddFrameEndTask(w => w.Remove(this)); + } + + public IEnumerable Render(WorldRenderer wr) + { + if (isHeadTravelling && info.RenderBeam && !wr.World.FogObscures(headPos)) + { + // OpenDUNE renders UNIT_SONIC_BLAST as a moving blurTile sprite + // (groundSpriteID 160, DISPLAYMODE_SINGLE_FRAME), not as a continuous + // colored beam. Keep the AreaBeam damage model above, but render one + // screen-space blur mask at the travelling projectile head. + return new[] + { + (IRenderable)new D2DistortionRenderable(headPos, sprite, D2DistortionStyle.Sonic) + }; + } + + return SpriteRenderable.None; + } + + int GetFalloff(int distance) + { + var inner = info.Range[0].Length; + for (var i = 1; i < info.Range.Length; i++) + { + var outer = info.Range[i].Length; + if (outer > distance) + return int2.Lerp(info.Falloff[i - 1], info.Falloff[i], distance - inner, outer - inner); + + inner = outer; + } + + return 0; + } + } +} diff --git a/OpenRA.Mods.D2/Traits/Render/D2DistortionTrail.cs b/OpenRA.Mods.D2/Traits/Render/D2DistortionTrail.cs new file mode 100644 index 0000000..2207cb1 --- /dev/null +++ b/OpenRA.Mods.D2/Traits/Render/D2DistortionTrail.cs @@ -0,0 +1,197 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 The d2 mod Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Mods.Common.Traits; +using OpenRA.Mods.D2.Graphics; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.D2.Traits.Render +{ + [Desc("Creates Dune 2 style screen-space distortion while an actor moves.")] + public class D2DistortionTrailInfo : ConditionalTraitInfo + { + public readonly D2DistortionStyle Style = D2DistortionStyle.Sand; + public readonly int Duration = 12; + public readonly HashSet TerrainTypes = new(); + public readonly string Image = "sandworm"; + [SequenceReference(nameof(Image))] + public readonly string Sequence = "idle"; + + public override object Create(ActorInitializer init) { return new D2DistortionTrail(this); } + } + + public class D2DistortionTrail : ConditionalTrait, ITick, INotifyAddedToWorld, IRender + { + WPos cachedPosition; + CPos activeCell; + WVec movementDirection = new(1024, 0, 0); + int idleTicks; + int animationTicks; + bool hasMoved; + Sprite sprite; + + // The original Sandworm is drawn three times per frame: at its + // current position, at its last recent cell, and at the cell + // before that (see viewport.c's dedicated sandworm block, which + // issues three separate DRAWSPRITE_FLAG_BLUR calls). Each of those + // draws advances the same global blur-offset cycle, so the three + // copies show different shift magnitudes at once -- that, not any + // per-pixel randomness, is what makes the original read as + // rippling ground rather than a single patch sliding as a block. + readonly CPos[] cellHistory = new CPos[2]; + int validHistoryCount; + + public D2DistortionTrail(D2DistortionTrailInfo info) + : base(info) { } + + protected override void Created(Actor self) + { + Reset(self); + sprite = self.World.Map.Sequences.GetSequence(Info.Image, Info.Sequence).GetSprite(0); + base.Created(self); + } + + void ITick.Tick(Actor self) + { + if (!self.IsInWorld || IsTraitDisabled) + return; + + animationTicks++; + + var previousPosition = cachedPosition; + var currentPosition = self.CenterPosition; + cachedPosition = currentPosition; + + var currentCell = self.World.Map.CellContaining(currentPosition); + var previousCell = self.World.Map.CellContaining(previousPosition); + + if (currentPosition == previousPosition) + { + if (idleTicks <= Info.Duration) + idleTicks++; + return; + } + + movementDirection = currentPosition - previousPosition; + idleTicks = 0; + hasMoved = true; + + if (currentCell != previousCell) + { + movementDirection = self.World.Map.CenterOfCell(currentCell) - self.World.Map.CenterOfCell(previousCell); + + for (var i = cellHistory.Length - 1; i > 0; i--) + cellHistory[i] = cellHistory[i - 1]; + cellHistory[0] = activeCell; + validHistoryCount = Math.Min(validHistoryCount + 1, cellHistory.Length); + } + + activeCell = currentCell; + } + + protected override void TraitEnabled(Actor self) + { + Reset(self); + } + + void INotifyAddedToWorld.AddedToWorld(Actor self) + { + Reset(self); + } + + void Reset(Actor self) + { + cachedPosition = self.CenterPosition; + activeCell = self.World.Map.CellContaining(cachedPosition); + movementDirection = new WVec(1024, 0, 0); + idleTicks = Info.Duration + 1; + animationTicks = 0; + hasMoved = false; + validHistoryCount = 0; + for (var i = 0; i < cellHistory.Length; i++) + cellHistory[i] = activeCell; + } + + bool ValidTerrain(Actor self, CPos cell) + { + if (!self.World.Map.Contains(cell)) + return false; + + var terrainType = self.World.Map.GetTerrainInfo(cell).Type; + return Info.TerrainTypes.Count == 0 || Info.TerrainTypes.Contains(terrainType); + } + + public IEnumerable Render(Actor self, WorldRenderer wr) + { + if (IsTraitDisabled || !hasMoved || idleTicks > Info.Duration) + yield break; + + // Current position, then up to two recently-occupied cells -- + // mirroring the original's current/targetLast/targetPreLast + // triple-draw. Each copy gets its own step in the blur cycle + // via a small Age offset, so they don't all show the identical + // shift at the same instant. + // The lead point follows the actor's exact continuous position + // (matching DuneLegacy's lastLocs_[0], which uses realX_/realY_ + // rather than a tile-snapped coordinate); only the trailing + // history below is snapped to cell centres. + if (ValidTerrain(self, activeCell)) + { + var pos = self.CenterPosition; + if (!self.World.FogObscures(pos)) + yield return new D2DistortionRenderable(pos, sprite, Info.Style); + } + + for (var i = 0; i < validHistoryCount; i++) + { + var cell = cellHistory[i]; + if (!ValidTerrain(self, cell)) + continue; + + var pos = self.World.Map.CenterOfCell(cell); + if (self.World.FogObscures(pos)) + continue; + + yield return new D2DistortionRenderable(pos, sprite, Info.Style); + } + } + + public IEnumerable ScreenBounds(Actor self, WorldRenderer wr) + { + if (IsTraitDisabled || !hasMoved || idleTicks > Info.Duration) + yield break; + + if (ValidTerrain(self, activeCell)) + { + var pos = self.CenterPosition; + if (!self.World.FogObscures(pos)) + yield return new D2DistortionRenderable(pos, sprite, Info.Style).ScreenBounds(wr); + } + + for (var i = 0; i < validHistoryCount; i++) + { + var cell = cellHistory[i]; + if (!ValidTerrain(self, cell)) + continue; + + var pos = self.World.Map.CenterOfCell(cell); + if (self.World.FogObscures(pos)) + continue; + + yield return new D2DistortionRenderable(pos, sprite, Info.Style).ScreenBounds(wr); + } + } + } +} diff --git a/mods/d2/glsl/postprocess_textured_distortion.frag b/mods/d2/glsl/postprocess_textured_distortion.frag new file mode 100644 index 0000000..fa5acae --- /dev/null +++ b/mods/d2/glsl/postprocess_textured_distortion.frag @@ -0,0 +1,38 @@ +#version {VERSION} +#ifdef GL_ES +precision mediump float; +#endif + +uniform sampler2D WorldTexture; +uniform sampler2D MaskTexture; +uniform float Style; +uniform float BlurOffset; +uniform float MaskChannel; + +in vec2 vTexCoord; +out vec4 fragColor; + +float MaskValue(vec4 texel) +{ + if (MaskChannel < 0.5) return texel.r; + if (MaskChannel < 1.5) return texel.g; + if (MaskChannel < 2.5) return texel.b; + return texel.a; +} + +void main() +{ + vec4 mask = texture(MaskTexture, vTexCoord); + if (MaskValue(mask) <= 0.0) + discard; + + ivec2 worldSize = textureSize(WorldTexture, 0); + ivec2 dst = ivec2(gl_FragCoord.xy); + ivec2 src = clamp(dst + ivec2(int(BlurOffset), 0), ivec2(0), worldSize - ivec2(1)); + + // OpenDUNE's blur draw uses the original sprite as a binary mask and copies + // pixels from a positive horizontal framebuffer offset inside that mask. + // The style uniform is kept active so one shader can serve the Sand/Sonic + // renderer instances configured in world.yaml. + fragColor = texelFetch(WorldTexture, src, 0) + vec4(0.0 * Style); +} diff --git a/mods/d2/mod.yaml b/mods/d2/mod.yaml index b5f2477..f62ed2f 100644 --- a/mods/d2/mod.yaml +++ b/mods/d2/mod.yaml @@ -264,6 +264,7 @@ Weapons: d2|weapons/largeguns.yaml d2|weapons/missiles.yaml d2|weapons/other.yaml + d2|weapons/sound.yaml d2|weapons/squads.yaml d2|weapons/spicebloom.yaml diff --git a/mods/d2/rules/sandworm.yaml b/mods/d2/rules/sandworm.yaml index 2dff95c..8b4c0f5 100644 --- a/mods/d2/rules/sandworm.yaml +++ b/mods/d2/rules/sandworm.yaml @@ -15,9 +15,6 @@ sandworm: Locomotor: worm Targetable: TargetTypes: Ground, Creep - WithSpriteBody: - WithAttackOverlay@mouth: - Sequence: mouth HiddenUnderFog: AppearsOnRadar: UseLocation: true @@ -35,14 +32,12 @@ sandworm: PingRadar: True RevealsShroud: Range: 5c0 - LeavesTrails: - Image: sandtrail - Sequences: traila, trailb, trailc - Palette: effect - Type: CenterPosition + D2DistortionTrail: + Style: Sand + Duration: 12 TerrainTypes: Sand, Dune, Spice - MovingInterval: 3 - RequiresCondition: !attacking + Image: sandworm + Sequence: blur RevealOnFire: Duration: 50 Radius: 2c512 diff --git a/mods/d2/rules/world.yaml b/mods/d2/rules/world.yaml index e045abc..10eb0f5 100644 --- a/mods/d2/rules/world.yaml +++ b/mods/d2/rules/world.yaml @@ -146,6 +146,12 @@ World: ActorSpawnManager: Actors: sandworm WarheadDebugOverlay: + D2DistortionRenderer@sand: + PassType: AfterActors + Styles: Sand + D2DistortionRenderer@sonic: + PassType: AfterWorld + Styles: Sonic D2TerrainLayer: D2BuildableTerrainLayer: ResourceLayer: diff --git a/mods/d2/sequences/misc.yaml b/mods/d2/sequences/misc.yaml index d08a239..174ea89 100644 --- a/mods/d2/sequences/misc.yaml +++ b/mods/d2/sequences/misc.yaml @@ -121,6 +121,11 @@ null: Filename: DATA.R16 Start: 3304 +sonic_blast: + idle: + Filename: UNITS1.SHP + Start: 9 + buildable: invalid: Filename: concfoot.shp diff --git a/mods/d2/sequences/sandworm.yaml b/mods/d2/sequences/sandworm.yaml index 08e05cd..c176cb3 100644 --- a/mods/d2/sequences/sandworm.yaml +++ b/mods/d2/sequences/sandworm.yaml @@ -7,20 +7,9 @@ sandworm: idle: Filename: UNITS1.SHP Start: 66 + blur: + Filename: UNITS1.SHP + Start: 10 icon: Filename: SHAPES.SHP Start: 93 - -sandtrail: - Defaults: - Length: 8 - Tick: 200 - ZOffset: -512 - traila: - Filename: sandtrail.shp - trailb: - Filename: sandtrail.shp - Frames: 2, 6, 4, 5, 0, 1, 3, 7 - trailc: - Filename: sandtrail.shp - Frames: 7, 4, 6, 5, 2, 0, 3, 1 diff --git a/mods/d2/weapons/other.yaml b/mods/d2/weapons/other.yaml index acc324c..91893a9 100644 --- a/mods/d2/weapons/other.yaml +++ b/mods/d2/weapons/other.yaml @@ -1,33 +1,3 @@ -Sound: - ReloadDelay: 90 - Range: 5c0 - Report: SONIC1.WAV - Projectile: AreaBeam - Speed: 0c128 - Duration: 4 # Has a length of 0c512 - DamageInterval: 3 # Travels 0c384 between impacts, will hit a target roughly three times - Width: 0c512 - Shape: Flat - Falloff: 100, 100, 50 - Range: 0, 6c0, 11c0 - BeyondTargetRange: 1c0 - Color: 00FFFFC8 - Warhead@1Dam: SpreadDamage - Range: 0, 32 - Falloff: 100, 100 - Damage: 60 - AffectsParent: false - ValidRelationships: Neutral, Enemy - DamageTypes: Prone50Percent, TriggerProne, SoundDeath - Warhead@2Dam: SpreadDamage - Range: 0, 32 - Falloff: 50, 50 # Only does half damage to friendly units - Damage: 60 - InvalidTargets: Sonictank # Does not affect friendly sonic tanks at all - AffectsParent: false - ValidRelationships: Ally - DamageTypes: Prone50Percent, TriggerProne, SoundDeath - Heal: ReloadDelay: 160 Range: 4c0 diff --git a/mods/d2/weapons/sound.yaml b/mods/d2/weapons/sound.yaml new file mode 100644 index 0000000..e556ac7 --- /dev/null +++ b/mods/d2/weapons/sound.yaml @@ -0,0 +1,30 @@ +Sound: + ReloadDelay: 90 + Range: 5c0 + Report: SONIC1.WAV + Projectile: D2SonicBeam + Speed: 0c128 + Duration: 4 # Has a length of 0c512 + DamageInterval: 3 # Travels 0c384 between impacts, will hit a target roughly three times + Width: 0c512 + Shape: Flat + Falloff: 100, 100, 50 + Range: 0, 6c0, 11c0 + BeyondTargetRange: 1c0 + Image: sonic_blast + Sequence: idle + Warhead@1Dam: SpreadDamage + Range: 0, 32 + Falloff: 100, 100 + Damage: 60 + AffectsParent: false + ValidRelationships: Neutral, Enemy + DamageTypes: Prone50Percent, TriggerProne, SoundDeath + Warhead@2Dam: SpreadDamage + Range: 0, 32 + Falloff: 50, 50 # Only does half damage to friendly units + Damage: 60 + InvalidTargets: Sonictank # Does not affect friendly sonic tanks at all + AffectsParent: false + ValidRelationships: Ally + DamageTypes: Prone50Percent, TriggerProne, SoundDeath