feat: SongCreator 씬 완성 — Beat Sage URL 지원, info.dat 메타데이터 자동 추출

- BeatSageUploader: audio_url 지원(UploadFromUrl), PollAndDownload 공통화, ZIP 500 오류 3회 재시도
- BeatSageConverter: info.dat 파싱(SongMetadata), BPM 자동 감지 → 노트 타이밍 변환에 적용
- SongCreatorManager: title/BPM 필수 입력 제거, 난이도 4개 자동 선택, GenerateFlowFromUrl 버그 수정
- NasPublisher: audioPath null 허용(URL 흐름에서 로컬 파일 없는 경우 스킵)
- .gitignore/.gitattributes 초기 설정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 23:37:34 +09:00
commit 4dad9e5d5b
1068 changed files with 175146 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
using DamageSystem;
using VRBeats.Events;
using UnityEngine;
using EzySlice;
namespace VRBeats
{
public class Cuttable : Damageable
{
[SerializeField] private OnDamageEvent onCut = null;
private Material insideMaterial = null;
private const float minCutVelocity = 0.25f;
private void Awake()
{
insideMaterial = GetComponent<MeshRenderer>().material;
}
public override void DoDamage(DamageInfo info)
{
onCut.Invoke(info);
var beatDamageInfo = info as BeatDamageInfo;
if (beatDamageInfo == null) return;
Vector3 cutDir = Vector3.right;
if (beatDamageInfo.velocity > minCutVelocity)
{
cutDir = CalculateCutDirection(info.hitDir, beatDamageInfo.hitObject.transform.up);
}
if (Cut(info.hitPoint, cutDir, insideMaterial))
{
Destroy(gameObject);
}
}
private Vector3 CalculateCutDirection(Vector3 hitDir, Vector3 saberUp)
{
Vector3 cutDir;
if (Mathf.Abs(hitDir.y) > Mathf.Abs(hitDir.x))
{
cutDir = Vector3.right * Mathf.Sign(hitDir.y);
Vector2 saberDir = new Vector2(saberUp.x, saberUp.z);
Vector2 planeDir = new Vector2(0.0f, 1.0f);
saberDir.Normalize();
planeDir.Normalize();
float saberAngle = Vector2.SignedAngle(saberDir, planeDir);
float hitAngle = Vector3.SignedAngle(hitDir, cutDir, Vector3.forward);
cutDir = Quaternion.Euler(0.0f, saberAngle, 90.0f - hitAngle) * cutDir;
}
else
{
Vector2 saberDir = new Vector2(saberUp.x, saberUp.y) * -1;
Vector2 planeDir = new Vector2(0.0f, 1.0f);
saberDir.Normalize();
planeDir.Normalize();
float saberAngle = Vector3.SignedAngle(saberUp, Vector3.forward, Vector3.right); ;
Vector2 hit = new Vector2(hitDir.x, hitDir.y);
Vector2 planeRight = new Vector2(1.0f, 0.0f);
float hitAngle = Vector2.SignedAngle(hit, planeRight);
cutDir = Quaternion.Euler(-saberAngle, 0.0f, -hitAngle) * Vector3.up;
}
return cutDir;
}
private bool Cut(Vector3 point, Vector3 up, Material mat)
{
GameObject[] parts = gameObject.SliceInstantiate(point, up, mat);
if (parts == null)
return false;
for (int n = 0; n < parts.Length; n++)
{
parts[n].AddComponent<DestroyOnBecameInvisible>();
}
Rigidbody rb = parts[0].AddComponent<Rigidbody>();
rb.AddForce(100.0f * up);
rb.AddForce(200.0f * transform.forward * -1);
rb = parts[1].AddComponent<Rigidbody>();
rb.AddForce(100.0f * up * -1.0f);
rb.AddForce(200.0f * transform.forward * -1);
return true;
}
}
}