Skip to content

Commit 36ac50a

Browse files
committed
Added Lua script for deforming bullets as reference for future C++ implementation (not yet applied to anything vanilla)
1 parent ce65dc6 commit 36ac50a

File tree

1 file changed

+248
-0
lines changed

1 file changed

+248
-0
lines changed
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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

Comments
 (0)