extends SceneTree # Собирает нативного персонажа (Skeleton2D + взвешенные Polygon2D + AnimationPlayer с # запечёнными анимациями) и СОХРАНЯЕТ как редактируемую сцену res://characters/elf1/elf1_native.tscn. # Запуск: godot ... --headless --script res://tools/build_tscn.gd const GEOM := "res://tools/rig/elf1_combat.json" const ANIMS := "res://tools/rig/elf1_combat_anims.json" const PAGE := "res://characters/elf1/spine/elf1_combat.png" const OUT := "res://characters/elf1/elf1_native.tscn" const VP := Vector2(1280, 720) var _root: Node2D var _skel: Skeleton2D var _bones := [] var _by_name := {} var _poly_by_slot := {} # имя слота -> Polygon2D var _base_rank := {} # имя слота -> setup-ранг (индекс в порядке отрисовки) func _initialize(): _build_rig() _build_anims() _set_owner(_root, _root) var packed := PackedScene.new() var err := packed.pack(_root) if err != OK: push_error("pack failed: %d" % err); quit(); return err = ResourceSaver.save(packed, OUT) print("SAVE ", OUT, " -> ", err, " (кости=%d, привязок=%d)" % [_bones.size(), _skel.get_child_count() - _bones_direct]) quit() var _bones_direct := 0 func _build_rig(): var f := FileAccess.open(GEOM, FileAccess.READ) var data: Dictionary = JSON.parse_string(f.get_as_text()); f.close() var tex: Texture2D = load(PAGE) var ts := tex.get_size() _root = Node2D.new(); _root.name = "ElfNative" var spine := Node2D.new(); spine.name = "SpineRoot"; _root.add_child(spine) _skel = Skeleton2D.new(); _skel.name = "Skeleton2D"; spine.add_child(_skel) for bd in data["bones"]: var b := Bone2D.new() b.name = bd["name"] b.set_autocalculate_length_and_angle(false) b.set_length(float(bd["length"])) b.set_bone_angle(0.0) var t := Transform2D(deg_to_rad(float(bd["rotation"])), Vector2(bd["scaleX"], bd["scaleY"]), 0.0, Vector2(bd["x"], bd["y"])) var parent = _by_name.get(bd["parent"], _skel) parent.add_child(b) b.transform = t; b.rest = t _bones.append(b); _by_name[bd["name"]] = b _bones_direct = _skel.get_child_count() var minv := Vector2(1e9, 1e9); var maxv := Vector2(-1e9, -1e9) var att_rank := 0 for att in data["attachments"]: var world: Array = att["world"]; var uv: Array = att["uv"] var tris: Array = att["triangles"]; var weights: Array = att["weights"] var vc := world.size() / 2 var poly := Polygon2D.new(); poly.name = String(att["slot"]); poly.texture = tex var pts := PackedVector2Array(); var uvs := PackedVector2Array() pts.resize(vc); uvs.resize(vc) for i in vc: var p := Vector2(world[i * 2], world[i * 2 + 1]); pts[i] = p uvs[i] = Vector2(uv[i * 2] * ts.x, uv[i * 2 + 1] * ts.y) minv = minv.min(p); maxv = maxv.max(p) poly.polygon = pts; poly.uv = uvs var polys := [] for t in range(0, tris.size(), 3): polys.append(PackedInt32Array([tris[t], tris[t + 1], tris[t + 2]])) poly.polygons = polys var c: Array = att["color"]; poly.modulate = Color(c[0], c[1], c[2], c[3]) poly.z_index = att_rank # базовый порядок отрисовки (setup) _skel.add_child(poly) poly.skeleton = poly.get_path_to(_skel) _poly_by_slot[String(att["slot"])] = poly _base_rank[String(att["slot"])] = att_rank att_rank += 1 var bw := {} for i in vc: for pair in weights[i]: var bi := int(pair[0]) if not bw.has(bi): var a := PackedFloat32Array(); a.resize(vc); bw[bi] = a bw[bi][i] = float(pair[1]) for bi in bw: poly.add_bone(_skel.get_path_to(_bones[bi]), bw[bi]) var size := maxv - minv; var center := (minv + maxv) * 0.5 var fit: float = min(VP.x * 0.55 / size.x, VP.y * 0.8 / size.y) spine.scale = Vector2(fit, -fit) spine.position = Vector2(VP.x * 0.5 - center.x * fit, VP.y * 0.5 + center.y * fit) func _build_anims(): var f := FileAccess.open(ANIMS, FileAccess.READ) var data: Dictionary = JSON.parse_string(f.get_as_text()); f.close() var fps := float(data["fps"]) var bone_names: Array = data["bones"] var slot_names: Array = data.get("slots", []) var anims: Dictionary = data["animations"] var player := AnimationPlayer.new(); player.name = "AnimationPlayer" _root.add_child(player) player.root_node = player.get_path_to(_skel) var lib := AnimationLibrary.new() for anim_name in anims: var a: Dictionary = anims[anim_name] var frames: Array = a["frames"]; var nf := frames.size() var anim := Animation.new(); anim.length = float(a["duration"]); anim.loop_mode = Animation.LOOP_LINEAR for bi in bone_names.size(): var bn: String = bone_names[bi] if not _by_name.has(bn): continue var rel := String(_skel.get_path_to(_by_name[bn])) var f0: Array = frames[0][bi] var pv := false; var rv := false; var sv := false for fr in range(1, nf): var v: Array = frames[fr][bi] if not pv and (not is_equal_approx(v[0], f0[0]) or not is_equal_approx(v[1], f0[1])): pv = true if not rv and not is_equal_approx(v[2], f0[2]): rv = true if not sv and (not is_equal_approx(v[3], f0[3]) or not is_equal_approx(v[4], f0[4])): sv = true _add_track(anim, rel + ":position", frames, bi, fps, nf, 0, pv) _add_track(anim, rel + ":rotation", frames, bi, fps, nf, 1, rv) _add_track(anim, rel + ":scale", frames, bi, fps, nf, 2, sv) # --- draw order -> z_index (дискретно) --- var draworder: Array = a.get("draworder", []) if not draworder.is_empty() and not slot_names.is_empty(): # ранг полигон-слота = позиция среди слотов-С-привязкой в порядке отрисовки var ranks := {} # имя слота -> [ранг по кадрам] for name in _poly_by_slot: ranks[name] = [] for fr in nf: var r := 0 for si in draworder[fr]: var sn: String = slot_names[si] if _poly_by_slot.has(sn): ranks[sn].append(r); r += 1 for sn in ranks: var seq: Array = ranks[sn] var vary := false for v in seq: if v != _base_rank[sn]: vary = true; break if not vary: continue var rel := String(_skel.get_path_to(_poly_by_slot[sn])) var ti := anim.add_track(Animation.TYPE_VALUE) anim.track_set_path(ti, rel + ":z_index") anim.value_track_set_update_mode(ti, Animation.UPDATE_DISCRETE) anim.track_set_interpolation_type(ti, Animation.INTERPOLATION_NEAREST) for fr in nf: anim.track_insert_key(ti, fr / fps, seq[fr]) lib.add_animation(anim_name, anim) player.add_animation_library("", lib) func _add_track(anim: Animation, path: String, frames: Array, bi: int, fps: float, nf: int, kind: int, vary: bool): if not vary: return var ti := anim.add_track(Animation.TYPE_VALUE) anim.track_set_path(ti, path) anim.track_set_interpolation_type(ti, Animation.INTERPOLATION_LINEAR) for fr in nf: var v: Array = frames[fr][bi] var val match kind: 0: val = Vector2(v[0], v[1]) 1: val = deg_to_rad(float(v[2])) 2: val = Vector2(v[3], v[4]) anim.track_insert_key(ti, fr / fps, val) func _set_owner(node: Node, owner: Node): for ch in node.get_children(): ch.owner = owner _set_owner(ch, owner)