add support for palette variations of sprites

This commit is contained in:
Lephenixnoir 2021-12-30 10:16:15 +01:00
parent 9fc160e36c
commit 1b2f0fbc15
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
12 changed files with 122 additions and 12 deletions

View File

@ -79,6 +79,9 @@ set(ASSETS
# Enemies: Gunslinger
assets-cg/enemies/gunslinger_left.aseprite
assets-cg/enemies/gunslinger_right.aseprite
# Enemies: Fire Slime
assets-cg/enemies/fire_slime_left.txt
assets-cg/enemies/fire_slime_right.txt
# Misc
assets-cg/font_damage_red.png
assets-cg/font_damage_white.png

View File

@ -14,6 +14,8 @@ def convert(input, output, params, target):
o = convert_animation(input, params)
elif params["custom-type"] == "aseprite-anim":
o = convert_aseprite_anim(input, output, params)
elif params["custom-type"] == "aseprite-anim-variation":
o = convert_aseprite_anim_variation(input, output, params)
elif params["custom-type"] == "tiled-tileset":
o = convert_tiled_tileset(input, output, params)
elif params["custom-type"] == "tiled-map":
@ -32,7 +34,7 @@ def convert_level(input, params):
"map": re.compile(r'[a-zA-Z_][a-zA-Z0-9-]*'),
"spawner": re.compile(r'\d+,\d+'),
"player_spawn": re.compile(r'\d+,\d+'),
"wave": re.compile(r'\d+s(\s+\d+\*[a-z]+/\d+)+'),
"wave": re.compile(r'\d+s(\s+\d+\*[a-z_]+/\d+)+'),
"delay": re.compile(r'\d+s')
}
with open(input, "r") as fp:
@ -95,7 +97,7 @@ def convert_level(input, params):
identity = 1
elif identity == "bat/2":
identity = 2
elif identity == "slime/4":
elif identity == "fire_slime/4":
identity = 3
elif identity == "gunslinger/8":
identity = 4
@ -231,26 +233,49 @@ def convert_animation(input, params):
return o
def _aseprite_render_cel(c):
def _aseprite_render_cel(c, indexed=False, palette=None, newpalette=None):
if c.chunk_type != 0x2005: # Cel
raise fxconv.FxconvError("Rendering a Cel without a Cel chunk")
raise fxconv.FxconvError("Rendering a Cel but it's not a Cel chunk")
if c.cel_type not in [0,2]:
raise fxconv.FxconvError("Rendering a Cel that is linked or tilemap")
colors = None
if indexed:
assert palette is not None and palette.chunk_type == 0x2019 # Palette
# py_aseprite swaps blue and green!!
colors = [(c["red"], c["blue"], c["green"], c["alpha"])
for c in palette.colors]
if newpalette is not None:
assert indexed and newpalette.chunk_type == 0x2019 # Palette
newcolors = [(c["red"], c["blue"], c["green"], c["alpha"])
for c in newpalette.colors]
img = Image.new("RGBA", (c.data['width'], c.data['height']))
pixels = []
offset = 0
for y in range(img.height):
for x in range(img.width):
r, g, b, a = c.data['data'][offset:offset+4]
if indexed:
i = c.data['data'][offset]
offset += 1
else:
r, g, b, a = c.data['data'][offset:offset+4]
offset += 4
# Remap palette if requested
if newpalette is not None:
r, g, b, a = newcolors[i]
elif indexed:
r, g, b, a = colors[i]
pixels.append((r, g, b, a))
offset += 4
img.putdata(pixels)
return img
def convert_aseprite_anim(input, output, params):
def convert_aseprite_anim(input, output, params, newpalette=None):
from aseprite import AsepriteFile
#---
@ -260,8 +285,12 @@ def convert_aseprite_anim(input, output, params):
with open(input, "rb") as fp:
ase = AsepriteFile(fp.read())
if ase.header.color_depth != 32:
raise fxconv.FxconvError("Only RGBA supported yet, sorry x_x")
if ase.header.color_depth == 8:
indexed = True
elif ase.header.color_depth == 32:
indexed = False
else:
raise fxconv.FxconvError("Only indexed/RGBA supported yet, sorry x_x")
# Find tags that give names to animations
tags = None
@ -283,6 +312,16 @@ def convert_aseprite_anim(input, output, params):
durations = [ase.frames[i].frame_duration for i in range(from_, to+1)]
print(" (" + ", ".join(f"{d} ms" for d in durations) + ")")
#---
# Find palette
#---
palette_chunk = None
for c in ase.frames[0].chunks:
if c.chunk_type == 0x2019: # Palette
palette_chunk = c
#---
# Generate PIL images for each frame
#---
@ -302,7 +341,8 @@ def convert_aseprite_anim(input, output, params):
x = [c for c in x if c.chunk_type == 0x2005]
chunk = x[chunk.layer_index]
cel = _aseprite_render_cel(chunk)
cel = _aseprite_render_cel(chunk, indexed, palette_chunk,
newpalette)
img.paste(cel, (chunk.x_pos, chunk.y_pos))
pil_frames.append(img)
@ -382,6 +422,27 @@ def convert_aseprite_anim(input, output, params):
return o
def convert_aseprite_anim_variation(input, output, params):
if "base" not in params or "palette" not in params:
raise fxconv.FxconvError("aseprite anim variation needs base/palette")
from aseprite import AsepriteFile
base = os.path.join(os.path.dirname(input), params["base"])
palette = os.path.join(os.path.dirname(input), params["palette"])
with open(palette, "rb") as fp:
ase = AsepriteFile(fp.read())
newpalette = None
for c in ase.frames[0].chunks:
if c.chunk_type == 0x2019: # Palette
newpalette = c
if newpalette is None:
raise fxconv.FxconvError("f{input} has no palette chunk")
return convert_aseprite_anim(base, output, params, newpalette)
def print_xml_tree(node, indent=0):
print(indent*" " + f"<{node.tag}> {node.attrib}")
for child in node:

View File

Binary file not shown.

View File

View File

@ -20,3 +20,17 @@ gunslinger_left.aseprite:
gunslinger_right.aseprite:
center: 8, 22
next: Idle=Idle, Walking=Walking, Hit=Idle, Fire=Reloading
*.txt:
custom-type: aseprite-anim-variation
name_regex: (.*)\.txt frames_\1
profile: p8
next: Idle=Idle, Walking=Walking, Hit=Idle
fire_slime_left.txt:
base: slime_left.aseprite
palette: fire_slime_palette.aseprite
fire_slime_right.txt:
base: slime_right.aseprite
palette: fire_slime_palette.aseprite

View File

@ -13,4 +13,6 @@ wave: 4s 8*bat/2
delay: 2s
wave: 8s 16*slime/1 8*bat/2
delay: 6s
wave: 2s 8*slime/1 8*bat/2 2*slime/4
wave: 2s 8*slime/1 8*bat/2 2*fire_slime/4
delay: 2s
wave: 8s 12*bat/2 4*fire_slime/4 1*gunslinger/8

View File

@ -57,6 +57,10 @@ ANIM2(gunslinger, Reloading);
ANIM2(gunslinger, Fire);
ANIM2(gunslinger, Hit);
ANIM2(gunslinger, Death);
ANIM2(fire_slime, Idle);
ANIM2(fire_slime, Walking);
ANIM2(fire_slime, Hit);
ANIM2(fire_slime, Death);
/* Player */
ANIM4(player, Idle);

View File

@ -86,6 +86,10 @@ extern anim_t anims_gunslinger_Reloading;
extern anim_t anims_gunslinger_Fire;
extern anim_t anims_gunslinger_Hit;
extern anim_t anims_gunslinger_Death;
extern anim_t anims_fire_slime_Idle;
extern anim_t anims_fire_slime_Walking;
extern anim_t anims_fire_slime_Hit;
extern anim_t anims_fire_slime_Death;
/* Player */
extern anim_t anims_player_Idle;

View File

@ -52,6 +52,25 @@ static enemy_t const bat_2 = {
.z = fix(0.75),
};
static enemy_t const fire_slime_4 = {
.name = "Fire slime",
.level = 4,
ANIMS(fire_slime, Idle, Walking, Idle, Hit, Death),
.hitbox = (rect){ -fix(3)/16, fix(4)/16, -fix(2)/16, fix(3)/16 },
.limits = {
.max_speed = fix(1),
.friction = fix(0.6),
},
.stats = {
.HP = fix(1.4),
.ATK = fix(1.2),
.MAG = fix(1.4),
.DEF = fix(1.4),
},
.shadow_size = 4,
.z = 0,
};
static enemy_t const gunslinger_8 = {
.name = "Gunslinger",
.level = 8,
@ -74,6 +93,7 @@ static enemy_t const gunslinger_8 = {
static enemy_t const * const enemies[] = {
[ENEMY_SLIME_1] = &slime_1,
[ENEMY_BAT_2] = &bat_2,
[ENEMY_FIRE_SLIME_4] = &fire_slime_4,
[ENEMY_GUNSLINGER_8] = &gunslinger_8,
};

View File

@ -37,7 +37,8 @@ enum {
/* ID 0 is used for the player! */
ENEMY_SLIME_1 = 1,
ENEMY_BAT_2 = 2,
ENEMY_GUNSLINGER_8 = 3,
ENEMY_FIRE_SLIME_4 = 3,
ENEMY_GUNSLINGER_8 = 4,
};
/* Get enemy data by ID. */

View File

@ -525,6 +525,7 @@ int main(void)
game_update_animations(&game, dt);
game_update_aoes(&game, dt);
game_update_particles(&game, dt);
// TODO: Kill out-of-bounds entities
game_remove_dead_entities(&game);
player_f->combo_delay -= dt;