Skip to content

Commit

Permalink
Merge pull request #177 from cortex-command-community/getpixelshenani…
Browse files Browse the repository at this point in the history
…gans

Add functions to alter sprite bitmaps on the fly
  • Loading branch information
Causeless authored Jan 12, 2025
2 parents 510c61a + e5fff9a commit f9483a2
Show file tree
Hide file tree
Showing 6 changed files with 413 additions and 3 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- 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.

- Added Lua-accessible bitmap manipulation functions to `MOSprite`s:
```
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!)

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.

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).

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.

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.

SetAllVisibleSpritePixelIndexes(int colorIndex) - Simplified version of the above, sets all visible pixels of the currently visible sprite to the given color index.
```
- Added `Material` Lua function `GetColorIndex()`, which returns the color index of the calling material.

- 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.

- 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.
Expand Down Expand Up @@ -161,6 +177,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

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

- Fixed `MOSprite:UnRotateOffset()` giving the wrong results on HFLipped sprites.

- 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.

- Fixed issue where MOSR `Gib`s, `AEmitter` or `PEmitter` `Emission`s, and MetaMan `Player`s were not correctly accessible from script.
Expand Down
248 changes: 248 additions & 0 deletions Data/Base.rte/Scripts/DeformingBullet.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
---- TO USE:
-- Simply add the script to any MO capable of wounding (most commonly MOPixel bullets).
-- Example: ScriptPath = Base.rte/Scripts/DeformingBullet.lua

-- Min/max radius of the entry wound hole, including discoloured outer ring
local entryWoundRadius = {1, 2};

-- Min/max radius of the exit wound hole, including discoloured outer ring
local exitWoundRadius = {2, 3};

-- Whether or not the wounds should count towards GibWoundLimit of the MOSR; mostly for testing
local countTowardsWoundLimit = true;

-- How much to multiply the sharpness by for MOSR collisions only
local sharpnessMultiplier = 1;

function Create(self)
local var = {};
var.Pos = self.Pos;
var.Vel = self.Vel;
var.Sharpness = self.Sharpness;
var.ringPositions = {};
var.canPenetrate = true;
var.newPos = nil;
var.newVel = nil;
var.numberOfHits = 0;
self.Sharpness = -math.abs(self.Sharpness); -- Set sharpness value to be negative to preserve terrain destruction
self.var = var;
end

-- Returns a table with all unique colour indexes of the sprite, except transparency.
-- Used for the discoloured outer ring of the wound holes.
local function GetAllSpriteColors(MOSprite)
if (MOSprite ~= nil) then
local spriteSize = Vector(MOSprite:GetSpriteWidth()-1, MOSprite:GetSpriteHeight()-1);
local colorTable = {};
local colorCount = 0;
for y = 0, spriteSize.Y do
for x = 0, spriteSize.X do
local pixelColor = MOSprite:GetSpritePixelIndex(x, y, MOSprite.Frame);
if (pixelColor > 0) then
if (colorCount == 0) then
colorCount = colorCount + 1;
colorTable[colorCount] = pixelColor;
else
local i = 0;
local colorFound = false;
repeat
i = i + 1;
colorFound = pixelColor == colorTable[i];
until colorFound == true or i >= colorCount

if (colorFound == false) then
colorCount = colorCount + 1;
colorTable[colorCount] = pixelColor;
end
end
end
end
end

return colorTable;
else
return {};
end
end

-- Adds a given wound with accompanying hole in the sprite
local function addDeformWound(var, MO, radiusTable, rangeVector, absWoundPos, angleOffset, woundPresetName)
local MOSprite = ToMOSprite(MO);
local holeRadius = math.random(radiusTable[1], radiusTable[2]);
local woundEmitterOffset = Vector(holeRadius, 0):GetRadRotatedCopy(rangeVector.AbsRadAngle + angleOffset); -- Vector to push the created wound in from the new hole
local holeOffset = SceneMan:ShortestDistance(MO.Pos, absWoundPos, true);
local woundOffset = holeOffset + woundEmitterOffset; -- Push the wound MO inwards to make it visually spawn on the MO rather than thin air
local holePos = MOSprite:UnRotateOffset(holeOffset);
local woundPos = MOSprite:UnRotateOffset(woundOffset);

-- Creates the wound at the default position if the presetname exists; script might bork if no wound is given
local newWound = nil;
if (woundPresetName ~= "") then
newWound = CreateAEmitter(woundPresetName);
local inboundAngle = rangeVector:GetXFlipped(MO.HFlipped).AbsRadAngle;
local woundAngle = inboundAngle - (MO.RotAngle * MO.FlipFactor) + math.pi + angleOffset; -- ... We should probably have an MOSprite:UnRotateAngle() function
-- newWound.Lifetime = 50;
-- newWound.BurstDamage = 0;
MO:AddWound(newWound, woundPos, countTowardsWoundLimit);
newWound.InheritedRotAngleOffset = woundAngle;
end

-- Makes a hole in the sprite, discolouring the outermost pixels instead of removing them.
-- Iterates radially, could be made into a square with a distance check if coverage is spotty.
for i = 0, holeRadius do
local circumference = holeRadius * 2 * math.pi;
local angleStep = (math.pi*2)/circumference;
for q = 1, circumference do
local pos = Vector(i, 0):GetRadRotatedCopy(angleStep*q).Ceilinged + (holePos - MOSprite.SpriteOffset);
local color = 0; -- Default hole colour is transparent

-- 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
if (i == holeRadius and IsMOSprite(newWound)) then
local colorTable = GetAllSpriteColors(ToMOSprite(newWound));
if (#colorTable > 0) then
color = colorTable[math.random(1, #colorTable)];
end
end

-- 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
for frame = 0, MOSprite.FrameCount do
if (MOSprite:SetSpritePixelIndex(pos.X, pos.Y, frame, color, 0, false) and i == holeRadius) then
table.insert(var.ringPositions, pos + MOSprite.SpriteOffset);
end
end
end
end

-- Attempts to displace all wound MOs within the radius to the edge of it
for wound in MO:GetWounds() do
local woundDist = wound.ParentOffset - holePos;
if (woundDist.Magnitude < holeRadius) then
-- 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
local newDist = Vector(woundDist.X, woundDist.Y);
local newOffset = holePos + newDist:SetMagnitude(holeRadius);
local bitmapOffset = newOffset - MOSprite.SpriteOffset;
-- If the calculated position isn't transparent, set parentoffset to this
if (MOSprite:GetSpritePixelIndex(bitmapOffset.X, bitmapOffset.Y, MOSprite.Frame) == -2) then
wound.ParentOffset = newOffset;
else
-- If calculated position was invalid, pick a random position on the outside ring
if (#var.ringPositions > 0) then
local pos;
local bitmapPos;
local foundPixel = false;
repeat
pos = table.remove(var.ringPositions, math.random(1, #var.ringPositions));
bitmapPos = pos - MOSprite.SpriteOffset;
foundPixel = MOSprite:GetSpritePixelIndex(bitmapPos.X, bitmapPos.Y, MOSprite.Frame) > 0;
until
#var.ringPositions <= 0 or foundPixel

if (foundPixel) then
wound.ParentOffset = pos;
else
-- If, somehow, no valid position is found, delete the wound; this might need changing but is an edge case
wound.ToDelete = true;
end
else
-- If there are no outer ring positions, delete the wound
wound.ToDelete = true;
end
end
end
end

return newWound;
end

function OnCollideWithMO(self, hitMO, hitMORootParent)
local var = self.var;

-- Calculate MOSR penetration power
local penetration = self.Mass * var.Sharpness * var.Vel.Magnitude * sharpnessMultiplier;

-- 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
if hitMO.ToDelete == false and var.canPenetrate and hitMO.Material.StructuralIntegrity <= penetration then
var.canPenetrate = false; -- Ensure this is only run once per frame
local rangeVector = var.Vel/3;
local endPos = var.Pos + rangeVector;

-- We do already have the MO but we need the point of impact
local raycast = SceneMan:CastMORay(var.Pos, rangeVector, self.RootID, self.IgnoresWhichTeam, 0, true, 0);

if raycast ~= 255 then
endPos = SceneMan:GetLastRayHitPos(); -- Point of impact, woo
local MO = ToMOSRotating(MovableMan:GetMOFromID(raycast));
local MOSprite = ToMOSprite(MO);
var.ringPositions = {}; -- Reset ring position table for this collision
local maxPen = penetration / MO.Material.StructuralIntegrity; -- Max penetration depth
local penVec = rangeVector.Normalized;
local hitOffset = SceneMan:ShortestDistance(MO.Pos, endPos, true);

-- Add the entry wound
addDeformWound(var, MO, entryWoundRadius, rangeVector, endPos, 0, MO:GetEntryWoundPresetName());

-- Bit of table bullshit for Lua performance; just use vectors in C++
local startPos = {hitOffset.X, hitOffset.Y};
local exitWoundPos = nil;
local penVecTable = {penVec.X, penVec.Y};
local penUsed = 0;
local pixelFound = false;
-- Check for exit wound
for i = 1, maxPen do
local checkPos = Vector(startPos[1] + penVecTable[1]*i, startPos[2] + penVecTable[2]*i);
checkPos = MOSprite:UnRotateOffset(checkPos);
checkPos = checkPos - MOSprite.SpriteOffset;
local pixel = MOSprite:GetSpritePixelIndex(checkPos.X, checkPos.Y, MOSprite.Frame);

-- If we've found a valid pixel and the iterator exits the visible sprite, add exit wound at last found pixel
if (pixelFound and pixel <= 0) then
exitWoundPos = Vector(startPos[1] + penVecTable[1]*i, startPos[2] + penVecTable[2]*i);
pixelFound = false;
end

-- If outside of sprite dimensions, break loop
if (pixel < 0) then
break;
end

-- If we find a visible pixel
if (pixel > 0) then
penUsed = penUsed + MO.Material.StructuralIntegrity;
pixelFound = true;
end

-- If all penetration has been spent, break loop
if (penUsed >= penetration) then
break;
end
end

-- 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
if (exitWoundPos) then
local exitWound = addDeformWound(var, MO, exitWoundRadius, rangeVector, exitWoundPos + MO.Pos, math.pi, MO:GetExitWoundPresetName());
var.newVel = rangeVector * 3 * (1-(penUsed / penetration));
var.newPos = exitWoundPos + MO.Pos;
self:SetWhichMOToNotHit(MO:GetRootParent(), 0.035); -- Makes sure the bullet only hits this MOSR once
else
self.ToDelete = true;
var.newVel = (endPos - self.Pos) / 3; -- Attempts to prevent the bullet from visually bouncing off for one frame
end
end
end
end

function Update(self)
local var = self.var;
var.canPenetrate = true;

-- We have to set new velocities and positions in Update because it borks in OnCollideWithMO
if (var.newVel) then
self.Vel = Vector(var.newVel.X, var.newVel.Y);
var.newVel = nil;
end

if (var.newPos) then
self.Pos = Vector(var.newPos.X, var.newPos.Y);
var.newPos = nil;
end
end
92 changes: 90 additions & 2 deletions Source/Entities/MOSprite.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ void MOSprite::Clear() {
m_SettleMaterialDisabled = false;
m_pEntryWound = 0;
m_pExitWound = 0;
m_SpriteModified = false;
}

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

if (m_SpriteModified) {
for (BITMAP* sprite : m_aSprite) {
destroy_bitmap(sprite);
}
}

if (!notInherited)
MovableObject::Destroy();
Clear();
Expand Down Expand Up @@ -346,9 +353,90 @@ Vector MOSprite::RotateOffset(const Vector& offset) const {
}

Vector MOSprite::UnRotateOffset(const Vector& offset) const {
Vector rotOff(offset.GetXFlipped(m_HFlipped));
Vector rotOff(offset);
rotOff /= const_cast<Matrix&>(m_Rotation);
return rotOff;
return rotOff.GetXFlipped(m_HFlipped);
}

int MOSprite::GetSpritePixelIndex(int x, int y, int whichFrame) const {
unsigned int clampedFrame = std::max(std::min(whichFrame, static_cast<int>(m_FrameCount) - 1), 0);
BITMAP* targetSprite = m_aSprite[clampedFrame];
if (is_inside_bitmap(targetSprite, x, y, 0)) {
return _getpixel(targetSprite, x, y);
}
return -1;
}

std::vector<Vector>* MOSprite::GetAllSpritePixelPositions(const Vector& origin, float angle, bool hflipped, int whichFrame, int ignoreIndex, bool invert, bool includeChildren) {
std::vector<Vector>* posList = new std::vector<Vector>();
unsigned int clampedFrame = std::max(std::min(whichFrame, static_cast<int>(m_FrameCount) - 1), 0);
int spriteSize = m_SpriteDiameter;
if (includeChildren && dynamic_cast<MOSRotating*>(this)) {
spriteSize = dynamic_cast<MOSRotating*>(this)->GetDiameter();
}
BITMAP* sprite = m_aSprite[clampedFrame];
BITMAP* temp = create_bitmap_ex(8, spriteSize, spriteSize);
rectfill(temp, 0, 0, temp->w - 1, temp->h - 1, 0);
Vector tempCentre = Vector(temp->w / 2, temp->h / 2);
Vector spriteCentre = Vector(sprite->w / 2, sprite->h / 2);

if (includeChildren) {
Draw(temp, m_Pos - tempCentre);
} else {
Vector offset = (tempCentre + (m_SpriteOffset + spriteCentre).GetXFlipped(m_HFlipped).RadRotate(m_Rotation.GetRadAngle()) - spriteCentre);
if (!hflipped) {
rotate_scaled_sprite(temp, sprite, offset.m_X, offset.m_Y, ftofix(GetAllegroAngle(-m_Rotation.GetDegAngle())), ftofix(m_Scale));
} else {
rotate_scaled_sprite_v_flip(temp, sprite, offset.m_X, offset.m_Y, ftofix(GetAllegroAngle(-m_Rotation.GetDegAngle())) + itofix(128), ftofix(m_Scale));
}
}

for (int y = 0; y < temp->h; y++) {
for (int x = 0; x < temp->w; x++) {
int pixelIndex = _getpixel(temp, x, y);
if (pixelIndex >= 0 && (pixelIndex != ignoreIndex) != invert) {
Vector pixelPos = (Vector(x, y) - tempCentre) + origin;
posList->push_back(pixelPos);
}
}
}

destroy_bitmap(temp);
return posList;
}

bool MOSprite::SetSpritePixelIndex(int x, int y, int whichFrame, int colorIndex, int ignoreIndex, bool invert) {
if (!m_SpriteModified) {
std::vector<BITMAP*> spriteList;

for (BITMAP* sprite : m_aSprite) {
BITMAP* spriteCopy = create_bitmap_ex(8, sprite->w, sprite->h);
rectfill(spriteCopy, 0, 0, spriteCopy->w - 1, spriteCopy->h - 1, 0);
draw_sprite(spriteCopy, sprite, 0, 0);
spriteList.push_back(spriteCopy);
}

m_aSprite = spriteList;
m_SpriteModified = true;
}

unsigned int clampedFrame = std::max(std::min(whichFrame, static_cast<int>(m_FrameCount) - 1), 0);
BITMAP* targetSprite = m_aSprite[clampedFrame];
if (is_inside_bitmap(targetSprite, x, y, 0) && (ignoreIndex < 0 || (_getpixel(targetSprite, x, y) != ignoreIndex) != invert)) {
_putpixel(targetSprite, x, y, colorIndex);
return true;
}
return false;
}

void MOSprite::SetAllSpritePixelIndexes(int whichFrame, int colorIndex, int ignoreIndex, bool invert) {
unsigned int clampedFrame = std::max(std::min(whichFrame, static_cast<int>(m_FrameCount) - 1), 0);
BITMAP* targetSprite = m_aSprite[clampedFrame];
for (int y = 0; y < targetSprite->h; y++) {
for (int x = 0; x < targetSprite->w; x++) {
SetSpritePixelIndex(x, y, clampedFrame, colorIndex, ignoreIndex, invert);
}
}
}

void MOSprite::Update() {
Expand Down
Loading

0 comments on commit f9483a2

Please sign in to comment.