|
|
@@ -73,12 +73,28 @@ def bounce_in():
|
|
|
return {"scale": _scale_keys(scale), "rotate": _rotate_keys(rot)}
|
|
|
|
|
|
|
|
|
+def taunt():
|
|
|
+ """Boss 嘲讽:上身前压,举剑炫耀。"""
|
|
|
+ scale = [(0, 1, 1), (0.14, 1.08, 0.94), (0.32, 0.98, 1.04), (0.55, 1, 1)]
|
|
|
+ rot = [(0, 0), (0.16, -6), (0.34, 5), (0.55, 0)]
|
|
|
+ return {"scale": _scale_keys(scale), "rotate": _rotate_keys(rot)}
|
|
|
+
|
|
|
+
|
|
|
+def explode_anim():
|
|
|
+ """网页预览兜底;真正多部件爆开在 build_parts_skeleton_json 内定义。"""
|
|
|
+ scale = [(0, 1, 1), (0.12, 1.2, 0.82), (0.35, 0.2, 0.2)]
|
|
|
+ rot = [(0, 0), (0.16, 12), (0.35, -24)]
|
|
|
+ return {"scale": _scale_keys(scale), "rotate": _rotate_keys(rot)}
|
|
|
+
|
|
|
+
|
|
|
ANIM_FACTORY = {
|
|
|
"idle": jiggle_idle,
|
|
|
"win": jiggle_win,
|
|
|
"attack": attack,
|
|
|
"hurt": hurt,
|
|
|
"bounce_in": bounce_in,
|
|
|
+ "taunt": taunt,
|
|
|
+ "explode": explode_anim,
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -185,6 +201,29 @@ def write_atlas(atlas_path, png_name, region_name, w, h):
|
|
|
f.write("\n".join(lines))
|
|
|
|
|
|
|
|
|
+def write_multi_atlas(atlas_path, png_name, atlas_w, atlas_h, regions):
|
|
|
+ lines = [
|
|
|
+ png_name,
|
|
|
+ f"size: {atlas_w},{atlas_h}",
|
|
|
+ "format: RGBA8888",
|
|
|
+ "filter: Linear,Linear",
|
|
|
+ "repeat: none",
|
|
|
+ ]
|
|
|
+ for r in regions:
|
|
|
+ lines.extend([
|
|
|
+ r["name"],
|
|
|
+ " rotate: false",
|
|
|
+ f" xy: {r['x']}, {r['y']}",
|
|
|
+ f" size: {r['w']}, {r['h']}",
|
|
|
+ f" orig: {r['w']}, {r['h']}",
|
|
|
+ " offset: 0, 0",
|
|
|
+ " index: -1",
|
|
|
+ ])
|
|
|
+ lines.append("")
|
|
|
+ with open(atlas_path, "w", encoding="utf-8") as f:
|
|
|
+ f.write("\n".join(lines))
|
|
|
+
|
|
|
+
|
|
|
def build_skeleton_json(char_id, w, h, animations):
|
|
|
"""单骨骼 region 绑定 + 程序化动画。骨骼原点在底部中心,便于"果冻不离地"挤压。"""
|
|
|
anims = {}
|
|
|
@@ -265,3 +304,110 @@ def build_character(char_id, image, out_dir, animations):
|
|
|
json.dump(skel, f, ensure_ascii=False, indent=2)
|
|
|
|
|
|
return png_path
|
|
|
+
|
|
|
+
|
|
|
+def build_parts_skeleton_json(char_id, parts, atlas_w, atlas_h, animations):
|
|
|
+ bones = [{"name": "root"}]
|
|
|
+ slots = []
|
|
|
+ attachments = {}
|
|
|
+ for p in parts:
|
|
|
+ name = p["id"]
|
|
|
+ parent = p.get("parent", "root")
|
|
|
+ bone = {"name": name, "parent": parent, "x": p.get("x", 0), "y": p.get("y", 0)}
|
|
|
+ bones.append(bone)
|
|
|
+ slots.append({"name": name, "bone": name, "attachment": name})
|
|
|
+ attachments[name] = {
|
|
|
+ name: {"x": 0, "y": 0, "width": p["w"], "height": p["h"]}
|
|
|
+ }
|
|
|
+
|
|
|
+ anims = {}
|
|
|
+ if "idle" in animations:
|
|
|
+ anims["idle"] = {"bones": {
|
|
|
+ "torso": {"scale": _scale_keys([(0, 1, 1), (0.6, 1.03, 0.97), (1.2, 1, 1)])},
|
|
|
+ "cape": {"rotate": _rotate_keys([(0, -2), (0.6, 3), (1.2, -2)])},
|
|
|
+ "head": {"rotate": _rotate_keys([(0, 0), (0.6, -2), (1.2, 0)])},
|
|
|
+ }}
|
|
|
+ if "taunt" in animations or "attack" in animations:
|
|
|
+ anims["taunt"] = {"bones": {
|
|
|
+ "torso": {"rotate": _rotate_keys([(0, 0), (0.18, -7), (0.45, 4), (0.7, 0)])},
|
|
|
+ "right_arm": {"rotate": _rotate_keys([(0, 0), (0.18, -38), (0.45, -18), (0.7, 0)])},
|
|
|
+ "greatsword": {"rotate": _rotate_keys([(0, 0), (0.18, -46), (0.45, -22), (0.7, 0)])},
|
|
|
+ "left_leg": {"scale": _scale_keys([(0, 1, 1), (0.25, 1.08, 0.92), (0.7, 1, 1)])},
|
|
|
+ }}
|
|
|
+ anims["attack"] = {"bones": {
|
|
|
+ "torso": {"rotate": _rotate_keys([(0, -4), (0.16, 9), (0.34, -3), (0.55, 0)])},
|
|
|
+ "right_arm": {"rotate": _rotate_keys([(0, -18), (0.16, 38), (0.34, -12), (0.55, 0)])},
|
|
|
+ "greatsword": {"rotate": _rotate_keys([(0, -28), (0.16, 60), (0.34, -20), (0.55, 0)])},
|
|
|
+ }}
|
|
|
+ if "hurt" in animations:
|
|
|
+ anims["hurt"] = {"bones": {
|
|
|
+ "torso": {"rotate": _rotate_keys([(0, 0), (0.08, 12), (0.22, -8), (0.42, 0)])},
|
|
|
+ "head": {"rotate": _rotate_keys([(0, 0), (0.08, 18), (0.22, -10), (0.42, 0)])},
|
|
|
+ "crack_core": {"scale": _scale_keys([(0, 0.8, 0.8), (0.12, 1.45, 1.45), (0.42, 1, 1)])},
|
|
|
+ }}
|
|
|
+ if "explode" in animations:
|
|
|
+ dirs = {
|
|
|
+ "head": (0, 260, 28), "torso": (0, 70, 0), "left_arm": (-260, 130, -80),
|
|
|
+ "right_arm": (260, 160, 85), "greatsword": (340, 240, 120),
|
|
|
+ "left_leg": (-170, -170, -45), "right_leg": (180, -160, 48),
|
|
|
+ "cape": (-280, 220, -120), "horn_left": (-170, 280, -70),
|
|
|
+ "horn_right": (170, 280, 70), "crack_core": (0, 30, 0),
|
|
|
+ }
|
|
|
+ bones_anim = {}
|
|
|
+ for p in parts:
|
|
|
+ dx, dy, rr = dirs.get(p["id"], (0, 160, 0))
|
|
|
+ bones_anim[p["id"]] = {
|
|
|
+ "translate": [{"time": 0, "x": 0, "y": 0}, {"time": 0.45, "x": dx, "y": dy}],
|
|
|
+ "rotate": _rotate_keys([(0, 0), (0.45, rr)]),
|
|
|
+ "scale": _scale_keys([(0, 1, 1), (0.18, 1.08, 1.08), (0.45, 0.35, 0.35)]),
|
|
|
+ }
|
|
|
+ anims["explode"] = {"bones": bones_anim}
|
|
|
+ if "win" in animations and "hurt" in anims:
|
|
|
+ anims["win"] = anims["hurt"]
|
|
|
+ if not anims:
|
|
|
+ anims["idle"] = {"bones": {"torso": jiggle_idle()}}
|
|
|
+
|
|
|
+ return {
|
|
|
+ "skeleton": {
|
|
|
+ "hash": char_id,
|
|
|
+ "spine": "4.0.00",
|
|
|
+ "x": -atlas_w / 2.0, "y": -atlas_h / 2.0,
|
|
|
+ "width": float(atlas_w), "height": float(atlas_h),
|
|
|
+ "images": "./", "audio": "",
|
|
|
+ },
|
|
|
+ "bones": bones,
|
|
|
+ "slots": slots,
|
|
|
+ "skins": [{"name": "default", "attachments": attachments}],
|
|
|
+ "animations": anims,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def build_parts_character(char_id, part_images, out_dir, animations, layout_parts):
|
|
|
+ """多部件 Boss:part_images {part_id: PIL RGBA} -> 单 atlas + 多骨骼 JSON。"""
|
|
|
+ os.makedirs(out_dir, exist_ok=True)
|
|
|
+ packed = []
|
|
|
+ x = 0
|
|
|
+ atlas_h = 0
|
|
|
+ for part in layout_parts:
|
|
|
+ pid = part["id"]
|
|
|
+ img = trim_to_content(remove_bg(part_images[pid]), pad=2)
|
|
|
+ w, h = img.size
|
|
|
+ packed.append({**part, "img": img, "x_atlas": x, "y_atlas": 0, "w": w, "h": h})
|
|
|
+ x += w + 2
|
|
|
+ atlas_h = max(atlas_h, h)
|
|
|
+ atlas_w = max(1, x)
|
|
|
+ atlas = Image.new("RGBA", (atlas_w, atlas_h), (0, 0, 0, 0))
|
|
|
+ regions = []
|
|
|
+ for p in packed:
|
|
|
+ atlas.paste(p["img"], (p["x_atlas"], 0), p["img"])
|
|
|
+ regions.append({"name": p["id"], "x": p["x_atlas"], "y": 0, "w": p["w"], "h": p["h"]})
|
|
|
+
|
|
|
+ png_name = f"{char_id}.png"
|
|
|
+ png_path = os.path.join(out_dir, png_name)
|
|
|
+ atlas.save(png_path)
|
|
|
+ write_multi_atlas(os.path.join(out_dir, f"{char_id}.atlas"), png_name, atlas_w, atlas_h, regions)
|
|
|
+
|
|
|
+ skel = build_parts_skeleton_json(char_id, packed, atlas_w, atlas_h, animations)
|
|
|
+ with open(os.path.join(out_dir, f"{char_id}.json"), "w", encoding="utf-8") as f:
|
|
|
+ json.dump(skel, f, ensure_ascii=False, indent=2)
|
|
|
+ return png_path
|