Files
Leonid PershinandClaude Opus 4.8 04cf5f90b0 ProjectRR — нативный прототип на ассетах ProjectR
- Все 189 скелетов Spine сконвертированы в нативные Godot Skeleton2D (.tscn), без Spine в рантайме
- Меню + сцены: подземелье (карта комнат), просмотр спрайтов, нативные анимации
- Кастомизация цвета (кожа/волосы/глаза) через шейдер
- Вытащенные текстуры/UI/шрифты + нарезка спрайтов из атласа
- Датасет комнат для LoRA, офлайн-инструменты конвертации в tools/

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 16:29:21 +03:00

176 lines
7.0 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)