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