EXTRACTING SPRITES
PNG sprite sheets are stored in the directories:
"Animations" (characters, in-game sprites)
"Assets" (background and objects)
"Images" (UI, cutscenes, etc)
animation frame data is stored in .PLIST XML files
Key "frame" shows the X and Y coordinates, followed by the width and height of the crop.
"Offset" is how many transparent pixels to add to the sides of the crop.
If "rotated" is set to "true", reverse the width and height values in "frame", and rotate the crop counter-clockwise by 90 degrees.
"sourceColorRect" and "sourceSize" can be ignored.
the game assets were generated using TexturePacker (https://www.codeandweb.com/texturepacker), though as far as I can tell while it can load a sheet and isolate each frame, you can't feed it the PLIST or other data to do all the extra work for you.
gotta format the PLIST to TXT...!
(getting Python to cooperate with the PLIST files was a nightmare, so i had to cut them down into five lines of data per frame
remove linebreaks and spaces
remove -
remove
remove sourceSize{800,800}
replace \n
replace \n
replace {false}\n
replace {true}\n
replace \n
replace {{ {
replace }} }
import os
import re
from PIL import Image
def parse_txt(file_path):
with open(file_path, 'r') as f:
lines = [line.strip() for line in f.readlines() if line.strip()]
crops = []
i = 0
while i < len(lines):
output_path = lines[i]
frame_match = re.search(r'frame{(\d+),(\d+)},{(\d+),(\d+)}', lines[i + 1])
offset_match = re.search(r'offset{(\d+),(\d+)}', lines[i + 2])
rotated_match = re.search(r'rotated{(true|false)}', lines[i + 3])
if frame_match and offset_match and rotated_match:
x, y, w, h = map(int, frame_match.groups())
offset_x, offset_y = map(int, offset_match.groups())
rotated = rotated_match.group(1) == 'true'
if rotated:
w, h = h, w # Swap width and height for rotated images
crops.append({
"output_path": output_path,
"x": x,
"y": y,
"w": w,
"h": h,
"offset_x": offset_x,
"offset_y": offset_y,
"rotated": rotated
})
i += 5 # Move to the next crop section
return crops
def extract_images(source_image, crops):
img = Image.open(source_image)
for crop in crops:
cropped_img = img.crop((crop["x"], crop["y"], crop["x"] + crop["w"], crop["y"] + crop["h"]))
# Apply transparent offset
new_w = crop["w"] + crop["offset_x"]
new_h = crop["h"] + crop["offset_y"]
new_img = Image.new("RGBA", (new_w, new_h), (0, 0, 0, 0))
new_img.paste(cropped_img, (crop["offset_x"], crop["offset_y"]))
# Rotate if necessary
if crop["rotated"]:
new_img = new_img.rotate(-90, expand=True)
# Create output directory if necessary
output_dir = os.path.dirname(crop["output_path"])
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
# Save the image
new_img.save(crop["output_path"] + ".png")
if __name__ == "__main__":
txt_file = "hero1_animations_1.txt" # Path to your TXT file
source_image = "hero1_animations_1.png" # Path to your source PNG image
crops = parse_txt(txt_file)
extract_images(source_image, crops)
print("Image extraction complete!")
RECOLOURING SPRITES
enemies and other objects have areas coloured in RGB to designate they're for recolouring
(that is, colours that have 0 in two channels)
"Scripts/Data/enemies.lua" has palette data under "data:setRedColor", etc
I cannot wrap my head around the RGB separation/decompose functions in GIMP/Paint.net -- where's my alpha data??? -- so i used another dodgy AI python script to separate them
from PIL import Image
def isolate_color(image_path, output_prefix):
img = Image.open(image_path).convert("RGBA")
pixels = img.load()
width, height = img.size
red_img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
green_img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
blue_img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
red_pixels = red_img.load()
green_pixels = green_img.load()
blue_pixels = blue_img.load()
for x in range(width):
for y in range(height):
r, g, b, a = pixels[x, y]
if r > 0 and g == 0 and b == 0: # Pure red
red_pixels[x, y] = (r, g, b, a)
elif g > 0 and r == 0 and b == 0: # Pure green
green_pixels[x, y] = (r, g, b, a)
elif b > 0 and r == 0 and g == 0: # Pure blue
blue_pixels[x, y] = (r, g, b, a)
red_img.save(f"{output_prefix}_red.png")
green_img.save(f"{output_prefix}_green.png")
blue_img.save(f"{output_prefix}_blue.png")
# Example usage
image_path = "biker_animations.png"
output_prefix = "output"
isolate_color(image_path, output_prefix)
after that, open it in Paint.net
Adjustments > Hue & Saturation > set Saturation to 0
Adjustments > Levels, set top Input value to 128
new layer with appropriate colour, set to Multiply
it might not be 1:1 with the game but it'll do in a pinch!