Skip to content

Commit f9483a2

Browse files
authored
Merge pull request #177 from cortex-command-community/getpixelshenanigans
Add functions to alter sprite bitmaps on the fly
2 parents 510c61a + e5fff9a commit f9483a2

File tree

6 files changed

+413
-3
lines changed

6 files changed

+413
-3
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8181

8282
- New `Attachable` INI and Lua (R/W) properties `InheritsVelWhenDetached` and `InheritsAngularVelWhenDetached`, which determine how much of these velocities an attachable inherits from its parent when detached. Defaults to 1.
8383

84+
- Added Lua-accessible bitmap manipulation functions to `MOSprite`s:
85+
```
86+
GetSpritePixelIndex(int x, int y, int whichFrame) - Returns the color index of the pixel at the given coordinate on the given frame of the sprite ((0, 0) is the upper left corner!)
87+
88+
SetSpritePixelIndex(int x, int y, int whichFrame, int colorIndex, int ignoreIndex, bool invert) - Sets the color of the pixel at the given coordinate on the given frame of the sprite, skipping if the pixel has same color index as given in "ignoreIndex". If "invert" is set to true, only pixels of that color index are set.
89+
90+
GetAllSpritePixelPositions(const Vector& origin, float angle, bool hflipped, int whichFrame, int ignoreIndex, bool invert, bool includeChildren) - Returns a list of vectors pointing to the absolute positions of all pixels in the given frame of the sprite, rotated to match "angle", flipped to match "hflipped" and positioned around "origin", providing a full silhouette of the MOSprite. "IgnoreIndex" and "invert" are like above, "includeChildren" denotes whether or not to include all children of the MOSprite (no effect if not at least an MOSRotating).
91+
92+
GetAllVisibleSpritePixelPositions(bool includeChildren) - Simplified version of the above, returning a list of absolute positions of the visible pixels of the current frame of the sprite as it is currently drawn.
93+
94+
SetAllSpritePixelIndexes(int whichFrame, int colorIndex, int ignoreIndex, bool invert) - Sets all pixels in the given frame of the sprite to the given color index, ignoring and inverting as above.
95+
96+
SetAllVisibleSpritePixelIndexes(int colorIndex) - Simplified version of the above, sets all visible pixels of the currently visible sprite to the given color index.
97+
```
98+
- Added `Material` Lua function `GetColorIndex()`, which returns the color index of the calling material.
99+
84100
- New `ACraft` INI and Lua (R/W) property `CanEnterOrbit`, which determines whether a craft can enter orbit (and refund gold appropriately) or not. If false, default out-of-bounds deletion logic applies.
85101

86102
- New `MovableMan` function `GetMOsAtPosition(posX, posY, ignoreTeam, getsHitByMOsOnly)` that will return an iterator with all the `MovableObject`s that intersect that exact position with their sprite.
@@ -161,6 +177,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
161177

162178
- Fixed an issue where internal Lua functions OriginalDoFile, OriginalLoadFile, and OriginalRequire were polluting the global namespace. They have now been made inaccessible.
163179

180+
- Fixed `MOSprite:UnRotateOffset()` giving the wrong results on HFLipped sprites.
181+
164182
- Various fixes and improvements to inventory management when dual-wielding or carrying a shield, to stop situations where the actor unexpectedly puts their items away.
165183

166184
- Fixed issue where MOSR `Gib`s, `AEmitter` or `PEmitter` `Emission`s, and MetaMan `Player`s were not correctly accessible from script.
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/Scripts/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

Source/Entities/MOSprite.cpp

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ void MOSprite::Clear() {
4242
m_SettleMaterialDisabled = false;
4343
m_pEntryWound = 0;
4444
m_pExitWound = 0;
45+
m_SpriteModified = false;
4546
}
4647

4748
int MOSprite::Create() {
@@ -235,6 +236,12 @@ void MOSprite::Destroy(bool notInherited) {
235236
// delete m_pEntryWound; Not doing this anymore since we're not owning
236237
// delete m_pExitWound;
237238

239+
if (m_SpriteModified) {
240+
for (BITMAP* sprite : m_aSprite) {
241+
destroy_bitmap(sprite);
242+
}
243+
}
244+
238245
if (!notInherited)
239246
MovableObject::Destroy();
240247
Clear();
@@ -346,9 +353,90 @@ Vector MOSprite::RotateOffset(const Vector& offset) const {
346353
}
347354

348355
Vector MOSprite::UnRotateOffset(const Vector& offset) const {
349-
Vector rotOff(offset.GetXFlipped(m_HFlipped));
356+
Vector rotOff(offset);
350357
rotOff /= const_cast<Matrix&>(m_Rotation);
351-
return rotOff;
358+
return rotOff.GetXFlipped(m_HFlipped);
359+
}
360+
361+
int MOSprite::GetSpritePixelIndex(int x, int y, int whichFrame) const {
362+
unsigned int clampedFrame = std::max(std::min(whichFrame, static_cast<int>(m_FrameCount) - 1), 0);
363+
BITMAP* targetSprite = m_aSprite[clampedFrame];
364+
if (is_inside_bitmap(targetSprite, x, y, 0)) {
365+
return _getpixel(targetSprite, x, y);
366+
}
367+
return -1;
368+
}
369+
370+
std::vector<Vector>* MOSprite::GetAllSpritePixelPositions(const Vector& origin, float angle, bool hflipped, int whichFrame, int ignoreIndex, bool invert, bool includeChildren) {
371+
std::vector<Vector>* posList = new std::vector<Vector>();
372+
unsigned int clampedFrame = std::max(std::min(whichFrame, static_cast<int>(m_FrameCount) - 1), 0);
373+
int spriteSize = m_SpriteDiameter;
374+
if (includeChildren && dynamic_cast<MOSRotating*>(this)) {
375+
spriteSize = dynamic_cast<MOSRotating*>(this)->GetDiameter();
376+
}
377+
BITMAP* sprite = m_aSprite[clampedFrame];
378+
BITMAP* temp = create_bitmap_ex(8, spriteSize, spriteSize);
379+
rectfill(temp, 0, 0, temp->w - 1, temp->h - 1, 0);
380+
Vector tempCentre = Vector(temp->w / 2, temp->h / 2);
381+
Vector spriteCentre = Vector(sprite->w / 2, sprite->h / 2);
382+
383+
if (includeChildren) {
384+
Draw(temp, m_Pos - tempCentre);
385+
} else {
386+
Vector offset = (tempCentre + (m_SpriteOffset + spriteCentre).GetXFlipped(m_HFlipped).RadRotate(m_Rotation.GetRadAngle()) - spriteCentre);
387+
if (!hflipped) {
388+
rotate_scaled_sprite(temp, sprite, offset.m_X, offset.m_Y, ftofix(GetAllegroAngle(-m_Rotation.GetDegAngle())), ftofix(m_Scale));
389+
} else {
390+
rotate_scaled_sprite_v_flip(temp, sprite, offset.m_X, offset.m_Y, ftofix(GetAllegroAngle(-m_Rotation.GetDegAngle())) + itofix(128), ftofix(m_Scale));
391+
}
392+
}
393+
394+
for (int y = 0; y < temp->h; y++) {
395+
for (int x = 0; x < temp->w; x++) {
396+
int pixelIndex = _getpixel(temp, x, y);
397+
if (pixelIndex >= 0 && (pixelIndex != ignoreIndex) != invert) {
398+
Vector pixelPos = (Vector(x, y) - tempCentre) + origin;
399+
posList->push_back(pixelPos);
400+
}
401+
}
402+
}
403+
404+
destroy_bitmap(temp);
405+
return posList;
406+
}
407+
408+
bool MOSprite::SetSpritePixelIndex(int x, int y, int whichFrame, int colorIndex, int ignoreIndex, bool invert) {
409+
if (!m_SpriteModified) {
410+
std::vector<BITMAP*> spriteList;
411+
412+
for (BITMAP* sprite : m_aSprite) {
413+
BITMAP* spriteCopy = create_bitmap_ex(8, sprite->w, sprite->h);
414+
rectfill(spriteCopy, 0, 0, spriteCopy->w - 1, spriteCopy->h - 1, 0);
415+
draw_sprite(spriteCopy, sprite, 0, 0);
416+
spriteList.push_back(spriteCopy);
417+
}
418+
419+
m_aSprite = spriteList;
420+
m_SpriteModified = true;
421+
}
422+
423+
unsigned int clampedFrame = std::max(std::min(whichFrame, static_cast<int>(m_FrameCount) - 1), 0);
424+
BITMAP* targetSprite = m_aSprite[clampedFrame];
425+
if (is_inside_bitmap(targetSprite, x, y, 0) && (ignoreIndex < 0 || (_getpixel(targetSprite, x, y) != ignoreIndex) != invert)) {
426+
_putpixel(targetSprite, x, y, colorIndex);
427+
return true;
428+
}
429+
return false;
430+
}
431+
432+
void MOSprite::SetAllSpritePixelIndexes(int whichFrame, int colorIndex, int ignoreIndex, bool invert) {
433+
unsigned int clampedFrame = std::max(std::min(whichFrame, static_cast<int>(m_FrameCount) - 1), 0);
434+
BITMAP* targetSprite = m_aSprite[clampedFrame];
435+
for (int y = 0; y < targetSprite->h; y++) {
436+
for (int x = 0; x < targetSprite->w; x++) {
437+
SetSpritePixelIndex(x, y, clampedFrame, colorIndex, ignoreIndex, invert);
438+
}
439+
}
352440
}
353441

354442
void MOSprite::Update() {

0 commit comments

Comments
 (0)