1+ ---- TO USE:
2+ -- Simply add the script to any MO capable of wounding (most commonly MOPixel bullets).
3+ -- Example: ScriptPath = Base.rte/DeformingBullet.lua
4+
5+ -- Min/max radius of the entry wound hole, including discoloured outer ring
6+ local entryWoundRadius = {1 , 2 };
7+
8+ -- Min/max radius of the exit wound hole, including discoloured outer ring
9+ local exitWoundRadius = {2 , 3 };
10+
11+ -- Whether or not the wounds should count towards GibWoundLimit of the MOSR; mostly for testing
12+ local countTowardsWoundLimit = true ;
13+
14+ -- How much to multiply the sharpness by for MOSR collisions only
15+ local sharpnessMultiplier = 1 ;
16+
17+ function Create (self )
18+ local var = {};
19+ var .Pos = self .Pos ;
20+ var .Vel = self .Vel ;
21+ var .Sharpness = self .Sharpness ;
22+ var .ringPositions = {};
23+ var .canPenetrate = true ;
24+ var .newPos = nil ;
25+ var .newVel = nil ;
26+ var .numberOfHits = 0 ;
27+ self .Sharpness = - math.abs (self .Sharpness ); -- Set sharpness value to be negative to preserve terrain destruction
28+ self .var = var ;
29+ end
30+
31+ -- Returns a table with all unique colour indexes of the sprite, except transparency.
32+ -- Used for the discoloured outer ring of the wound holes.
33+ local function GetAllSpriteColors (MOSprite )
34+ if (MOSprite ~= nil ) then
35+ local spriteSize = Vector (MOSprite :GetSpriteWidth ()- 1 , MOSprite :GetSpriteHeight ()- 1 );
36+ local colorTable = {};
37+ local colorCount = 0 ;
38+ for y = 0 , spriteSize .Y do
39+ for x = 0 , spriteSize .X do
40+ local pixelColor = MOSprite :GetSpritePixelIndex (x , y , MOSprite .Frame );
41+ if (pixelColor > 0 ) then
42+ if (colorCount == 0 ) then
43+ colorCount = colorCount + 1 ;
44+ colorTable [colorCount ] = pixelColor ;
45+ else
46+ local i = 0 ;
47+ local colorFound = false ;
48+ repeat
49+ i = i + 1 ;
50+ colorFound = pixelColor == colorTable [i ];
51+ until colorFound == true or i >= colorCount
52+
53+ if (colorFound == false ) then
54+ colorCount = colorCount + 1 ;
55+ colorTable [colorCount ] = pixelColor ;
56+ end
57+ end
58+ end
59+ end
60+ end
61+
62+ return colorTable ;
63+ else
64+ return {};
65+ end
66+ end
67+
68+ -- Adds a given wound with accompanying hole in the sprite
69+ local function addDeformWound (var , MO , radiusTable , rangeVector , absWoundPos , angleOffset , woundPresetName )
70+ local MOSprite = ToMOSprite (MO );
71+ local holeRadius = math.random (radiusTable [1 ], radiusTable [2 ]);
72+ local woundEmitterOffset = Vector (holeRadius , 0 ):GetRadRotatedCopy (rangeVector .AbsRadAngle + angleOffset ); -- Vector to push the created wound in from the new hole
73+ local holeOffset = SceneMan :ShortestDistance (MO .Pos , absWoundPos , true );
74+ local woundOffset = holeOffset + woundEmitterOffset ; -- Push the wound MO inwards to make it visually spawn on the MO rather than thin air
75+ local holePos = MOSprite :UnRotateOffset (holeOffset );
76+ local woundPos = MOSprite :UnRotateOffset (woundOffset );
77+
78+ -- Creates the wound at the default position if the presetname exists; script might bork if no wound is given
79+ local newWound = nil ;
80+ if (woundPresetName ~= " " ) then
81+ newWound = CreateAEmitter (woundPresetName );
82+ local inboundAngle = rangeVector :GetXFlipped (MO .HFlipped ).AbsRadAngle ;
83+ local woundAngle = inboundAngle - (MO .RotAngle * MO .FlipFactor ) + math.pi + angleOffset ; -- ... We should probably have an MOSprite:UnRotateAngle() function
84+ -- newWound.Lifetime = 50;
85+ -- newWound.BurstDamage = 0;
86+ MO :AddWound (newWound , woundPos , countTowardsWoundLimit );
87+ newWound .InheritedRotAngleOffset = woundAngle ;
88+ end
89+
90+ -- Makes a hole in the sprite, discolouring the outermost pixels instead of removing them.
91+ -- Iterates radially, could be made into a square with a distance check if coverage is spotty.
92+ for i = 0 , holeRadius do
93+ local circumference = holeRadius * 2 * math.pi ;
94+ local angleStep = (math.pi * 2 )/ circumference ;
95+ for q = 1 , circumference do
96+ local pos = Vector (i , 0 ):GetRadRotatedCopy (angleStep * q ).Ceilinged + (holePos - MOSprite .SpriteOffset );
97+ local color = 0 ; -- Default hole colour is transparent
98+
99+ -- If we're at the edge of the hole and the wound has any colours, set pixel colour to a random wound colour instead of transparent
100+ if (i == holeRadius and IsMOSprite (newWound )) then
101+ local colorTable = GetAllSpriteColors (ToMOSprite (newWound ));
102+ if (# colorTable > 0 ) then
103+ color = colorTable [math.random (1 , # colorTable )];
104+ end
105+ end
106+
107+ -- Change pixel colour on all frames of the sprite and, if we're at the edge, make a table of all valid positions on the outer ring
108+ for frame = 0 , MOSprite .FrameCount do
109+ if (MOSprite :SetSpritePixelIndex (pos .X , pos .Y , frame , color , 0 , false ) and i == holeRadius ) then
110+ table.insert (var .ringPositions , pos + MOSprite .SpriteOffset );
111+ end
112+ end
113+ end
114+ end
115+
116+ -- Attempts to displace all wound MOs within the radius to the edge of it
117+ for wound in MO :GetWounds () do
118+ local woundDist = wound .ParentOffset - holePos ;
119+ if (woundDist .Magnitude < holeRadius ) then
120+ -- Calculate a vector from hole centre to wound position and set it to equal the radius of the hole, pushing the wound out to the edge
121+ local newDist = Vector (woundDist .X , woundDist .Y );
122+ local newOffset = holePos + newDist :SetMagnitude (holeRadius );
123+ local bitmapOffset = newOffset - MOSprite .SpriteOffset ;
124+ -- If the calculated position isn't transparent, set parentoffset to this
125+ if (MOSprite :GetSpritePixelIndex (bitmapOffset .X , bitmapOffset .Y , MOSprite .Frame ) == - 2 ) then
126+ wound .ParentOffset = newOffset ;
127+ else
128+ -- If calculated position was invalid, pick a random position on the outside ring
129+ if (# var .ringPositions > 0 ) then
130+ local pos ;
131+ local bitmapPos ;
132+ local foundPixel = false ;
133+ repeat
134+ pos = table.remove (var .ringPositions , math.random (1 , # var .ringPositions ));
135+ bitmapPos = pos - MOSprite .SpriteOffset ;
136+ foundPixel = MOSprite :GetSpritePixelIndex (bitmapPos .X , bitmapPos .Y , MOSprite .Frame ) > 0 ;
137+ until
138+ # var .ringPositions <= 0 or foundPixel
139+
140+ if (foundPixel ) then
141+ wound .ParentOffset = pos ;
142+ else
143+ -- If, somehow, no valid position is found, delete the wound; this might need changing but is an edge case
144+ wound .ToDelete = true ;
145+ end
146+ else
147+ -- If there are no outer ring positions, delete the wound
148+ wound .ToDelete = true ;
149+ end
150+ end
151+ end
152+ end
153+
154+ return newWound ;
155+ end
156+
157+ function OnCollideWithMO (self , hitMO , hitMORootParent )
158+ local var = self .var ;
159+
160+ -- Calculate MOSR penetration power
161+ local penetration = self .Mass * var .Sharpness * var .Vel .Magnitude * sharpnessMultiplier ;
162+
163+ -- If the target isn't about to cease existing, the bullet hasn't penetrated this frame and the material of the MO is weak enough to penetrate, proceed
164+ if hitMO .ToDelete == false and var .canPenetrate and hitMO .Material .StructuralIntegrity <= penetration then
165+ var .canPenetrate = false ; -- Ensure this is only run once per frame
166+ local rangeVector = var .Vel / 3 ;
167+ local endPos = var .Pos + rangeVector ;
168+
169+ -- We do already have the MO but we need the point of impact
170+ local raycast = SceneMan :CastMORay (var .Pos , rangeVector , self .RootID , self .IgnoresWhichTeam , 0 , true , 0 );
171+
172+ if raycast ~= 255 then
173+ endPos = SceneMan :GetLastRayHitPos (); -- Point of impact, woo
174+ local MO = ToMOSRotating (MovableMan :GetMOFromID (raycast ));
175+ local MOSprite = ToMOSprite (MO );
176+ var .ringPositions = {}; -- Reset ring position table for this collision
177+ local maxPen = penetration / MO .Material .StructuralIntegrity ; -- Max penetration depth
178+ local penVec = rangeVector .Normalized ;
179+ local hitOffset = SceneMan :ShortestDistance (MO .Pos , endPos , true );
180+
181+ -- Add the entry wound
182+ addDeformWound (var , MO , entryWoundRadius , rangeVector , endPos , 0 , MO :GetEntryWoundPresetName ());
183+
184+ -- Bit of table bullshit for Lua performance; just use vectors in C++
185+ local startPos = {hitOffset .X , hitOffset .Y };
186+ local exitWoundPos = nil ;
187+ local penVecTable = {penVec .X , penVec .Y };
188+ local penUsed = 0 ;
189+ local pixelFound = false ;
190+ -- Check for exit wound
191+ for i = 1 , maxPen do
192+ local checkPos = Vector (startPos [1 ] + penVecTable [1 ]* i , startPos [2 ] + penVecTable [2 ]* i );
193+ checkPos = MOSprite :UnRotateOffset (checkPos );
194+ checkPos = checkPos - MOSprite .SpriteOffset ;
195+ local pixel = MOSprite :GetSpritePixelIndex (checkPos .X , checkPos .Y , MOSprite .Frame );
196+
197+ -- If we've found a valid pixel and the iterator exits the visible sprite, add exit wound at last found pixel
198+ if (pixelFound and pixel <= 0 ) then
199+ exitWoundPos = Vector (startPos [1 ] + penVecTable [1 ]* i , startPos [2 ] + penVecTable [2 ]* i );
200+ pixelFound = false ;
201+ end
202+
203+ -- If outside of sprite dimensions, break loop
204+ if (pixel < 0 ) then
205+ break ;
206+ end
207+
208+ -- If we find a visible pixel
209+ if (pixel > 0 ) then
210+ penUsed = penUsed + MO .Material .StructuralIntegrity ;
211+ pixelFound = true ;
212+ end
213+
214+ -- If all penetration has been spent, break loop
215+ if (penUsed >= penetration ) then
216+ break ;
217+ end
218+ end
219+
220+ -- If a valid exit wound position has been found, add exit wound and set bullet to appear out of this wound with appropriately reduced velocity
221+ if (exitWoundPos ) then
222+ local exitWound = addDeformWound (var , MO , exitWoundRadius , rangeVector , exitWoundPos + MO .Pos , math.pi , MO :GetExitWoundPresetName ());
223+ var .newVel = rangeVector * 3 * (1 - (penUsed / penetration ));
224+ var .newPos = exitWoundPos + MO .Pos ;
225+ self :SetWhichMOToNotHit (MO :GetRootParent (), 0.035 ); -- Makes sure the bullet only hits this MOSR once
226+ else
227+ self .ToDelete = true ;
228+ var .newVel = (endPos - self .Pos ) / 3 ; -- Attempts to prevent the bullet from visually bouncing off for one frame
229+ end
230+ end
231+ end
232+ end
233+
234+ function Update (self )
235+ local var = self .var ;
236+ var .canPenetrate = true ;
237+
238+ -- We have to set new velocities and positions in Update because it borks in OnCollideWithMO
239+ if (var .newVel ) then
240+ self .Vel = Vector (var .newVel .X , var .newVel .Y );
241+ var .newVel = nil ;
242+ end
243+
244+ if (var .newPos ) then
245+ self .Pos = Vector (var .newPos .X , var .newPos .Y );
246+ var .newPos = nil ;
247+ end
248+ end
0 commit comments