mirror of https://github.com/hykilpikonna/AquaDX
475 lines
19 KiB
C#
475 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using DB;
|
|
using Manager;
|
|
using MelonLoader;
|
|
using Vector4 = UnityEngine.Vector4;
|
|
|
|
namespace AquaMai.MaimaiDX2077;
|
|
|
|
public static class SlideDataBuilder
|
|
{
|
|
public readonly struct ArrowData(Complex point, Complex tangent, double length)
|
|
{
|
|
public readonly Complex Point = point;
|
|
public readonly Complex Tangent = tangent;
|
|
public readonly double Length = length;
|
|
}
|
|
|
|
public static List<ArrowData> BuildArrowData(ParametricSlidePath path)
|
|
{
|
|
var result = new List<ArrowData>();
|
|
var totalLength = path.GetPathLength();
|
|
var totalSegCount = path.Segments.Length;
|
|
|
|
var length = 0.0;
|
|
var segIdx = 0;
|
|
var isSwitching = false;
|
|
|
|
while (length < totalLength)
|
|
{
|
|
var t = length / totalLength;
|
|
var pt = path.GetPointAt(t);
|
|
var tg = path.GetTangentAt(t);
|
|
|
|
var t2 = (length + 10.0) / totalLength;
|
|
if ((path.GetTangentAt(t2) - tg).Magnitude < 0.2) // 0.2 -> ~ 11.48 deg apart
|
|
{
|
|
// use secant instead of tangent (for better visual quality)
|
|
tg = path.GetPointAt(t2) - pt;
|
|
tg /= tg.Magnitude;
|
|
}
|
|
|
|
if (isSwitching)
|
|
{
|
|
// around connecting point of 2 segments, smoothing the transition
|
|
var last = result[result.Count - 1];
|
|
var vec = pt - last.Point;
|
|
vec /= vec.Magnitude;
|
|
if ((tg - last.Tangent).Magnitude < 0.2)
|
|
{
|
|
var x = 0.5 * (last.Tangent + vec);
|
|
x /= x.Magnitude;
|
|
result[result.Count - 1] = new ArrowData(last.Point, x, last.Length);
|
|
tg = 0.5 * (tg + vec);
|
|
tg /= tg.Magnitude;
|
|
}
|
|
}
|
|
|
|
result.Add(new ArrowData(pt, tg, length));
|
|
isSwitching = false;
|
|
|
|
var nextLength = length + path.Segments[segIdx].ArrowDistance;
|
|
if (segIdx < totalSegCount - 1 && nextLength >= path.AccumulatedLengths[segIdx])
|
|
{
|
|
isSwitching = true;
|
|
|
|
if (path.Segments[segIdx].ParseMarker == ParametricSlidePath.ParseMarker.ForceAlign)
|
|
{
|
|
// in this case the next point is forced to be 1 unit after the connecting point
|
|
nextLength = path.AccumulatedLengths[segIdx] + path.Segments[segIdx + 1].ArrowDistance;
|
|
|
|
// P.S. 这种情况一般是出现在一条直线连接到外圈, 这个处理是为了让外圈的箭头对齐
|
|
}
|
|
|
|
if (path.Segments[segIdx + 1].ParseMarker == ParametricSlidePath.ParseMarker.SmoothAlign)
|
|
{
|
|
// arrow distance of the next segment is tempered in order to align arrow
|
|
var delta = path.AccumulatedLengths[segIdx + 1] - length;
|
|
var n = Math.Round(delta / MaiGeometry.DefaultDistance);
|
|
path.Segments[segIdx + 1].SetArrowDistance(delta / n);
|
|
nextLength = length + delta / n;
|
|
|
|
// P.S. 这种情况出现在 ppqq 圈进入外圈, 可以把转移轨道的箭头间距微调一下, 也是让外圈对齐
|
|
}
|
|
|
|
segIdx++;
|
|
}
|
|
|
|
length = nextLength;
|
|
}
|
|
|
|
// 把路径终点补上
|
|
result.Add(new ArrowData(path.GetPointAt(1.0), path.GetTangentAt(1.0), totalLength));
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert arrow data to sinmai format (Vector4)
|
|
/// </summary>
|
|
/// <param name="data">arrow data generated by BuildArrowData()</param>
|
|
/// <param name="starButton">button index of slide-star</param>
|
|
/// <param name="mirrorMode">mirror mode in user option</param>
|
|
/// <returns>sinmai format arrow data, referenced to slide-star</returns>
|
|
public static List<Vector4> ConvertAndRotateArrowData(IEnumerable<ArrowData> data, int starButton,
|
|
OptionMirrorID mirrorMode)
|
|
{
|
|
// SBGA 用 Vector4 存储了 slide 箭头的坐标与取向
|
|
// x, y 是平面坐标, z 是从起点到此处的路径长度 (px), w 是旋转的角度 (0 ~ 360 deg) (注意与切线方向差了 180 度)
|
|
// 坐标原点是屏幕中心, x 轴向右, y 轴向上
|
|
// w 的零点是朝向正右 (对应于箭头朝向正左), 逆时针为正方向
|
|
// 此外, sinmai 实际上是把所有 slide 路径相对于星星头存储的, 再在 SlideRoot 里通过 transform 转到合适的位置
|
|
// 判定区也是相对于星星头存储, 用 InputManager.ConvertTouchPanelRotatePush() 执行旋转
|
|
// 但是 slide code 定义的是绝对位置, 所以要逆向转回去, 以保证无论星星头在哪个键获取到的路径在处理过后都是一样的
|
|
// 然后还需要处理镜像的问题
|
|
|
|
var arrowList = new List<Vector4>();
|
|
var rotor = Complex.FromPolarCoordinates(1.0, Math.PI / 4.0 * starButton);
|
|
foreach (var arrow in data)
|
|
{
|
|
var pos = arrow.Point;
|
|
var tangent = arrow.Tangent;
|
|
switch (mirrorMode)
|
|
{
|
|
case OptionMirrorID.Normal:
|
|
break;
|
|
case OptionMirrorID.LR:
|
|
pos = Complex.Conjugate(pos) * -1.0;
|
|
tangent = Complex.Conjugate(tangent) * -1.0;
|
|
break;
|
|
case OptionMirrorID.UD:
|
|
pos = Complex.Conjugate(pos);
|
|
tangent = Complex.Conjugate(tangent);
|
|
break;
|
|
case OptionMirrorID.UDLR:
|
|
pos *= -1.0;
|
|
tangent *= -1.0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
pos *= rotor;
|
|
tangent *= rotor;
|
|
var angle = tangent.Phase * 180.0 / Math.PI + 180.0; // Phase is in [-PI, PI]
|
|
arrowList.Add(new Vector4((float) pos.Real, (float) pos.Imaginary, (float) arrow.Length, (float) angle));
|
|
}
|
|
return arrowList;
|
|
}
|
|
|
|
public readonly struct HitAreaData(double push, double release, int[] areas)
|
|
{
|
|
public readonly double PushDistance = push;
|
|
public readonly double ReleaseDistance = release;
|
|
public readonly int[] PanelAreas = areas;
|
|
}
|
|
|
|
public static readonly Dictionary<int, HitAreaData[]> HitAreasLookup = new Dictionary<int, HitAreaData[]>();
|
|
|
|
public static void InitializeHitAreasLookup()
|
|
{
|
|
for (var i = 0; i < 8; i++)
|
|
{
|
|
for (var j = 0; j < 8; j++)
|
|
{
|
|
var diff = (j - i) & 7; // you know this is actually % 8 ... for same negative number compat
|
|
int tmp, tmp2;
|
|
|
|
// Ai -> Aj
|
|
var key = (i << 5) | j;
|
|
switch (diff)
|
|
{
|
|
case 1:
|
|
case 7:
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.32, 0.68, [i]),
|
|
new HitAreaData(1.00, 1.00, [j])
|
|
];
|
|
break;
|
|
case 2:
|
|
tmp = (i + 1) & 7;
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.20, 0.38, [i]),
|
|
new HitAreaData(0.62, 0.80, [tmp, tmp | 8]),
|
|
new HitAreaData(1.00, 1.00, [j])
|
|
];
|
|
break;
|
|
case 6:
|
|
tmp = (i - 1) & 7;
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.20, 0.38, [i]),
|
|
new HitAreaData(0.62, 0.80, [tmp, tmp | 8]),
|
|
new HitAreaData(1.00, 1.00, [j])
|
|
];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// Bi -> Bj
|
|
key = ((i | 8) << 5) | (j | 8);
|
|
switch (diff)
|
|
{
|
|
case 1:
|
|
case 7:
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.44, 0.56, [i | 8]),
|
|
new HitAreaData(1.00, 1.00, [j | 8])
|
|
];
|
|
break;
|
|
case 2:
|
|
tmp = (i + 1) & 7;
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.22, 0.35, [i | 8]),
|
|
new HitAreaData(0.65, 0.78, [tmp | 8, 16]),
|
|
new HitAreaData(1.00, 1.00, [j | 8])
|
|
];
|
|
break;
|
|
case 6:
|
|
tmp = (i - 1) & 7;
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.22, 0.35, [i | 8]),
|
|
new HitAreaData(0.65, 0.78, [tmp | 8, 16]),
|
|
new HitAreaData(1.00, 1.00, [j | 8])
|
|
];
|
|
break;
|
|
case 3:
|
|
tmp = (i + 1) & 7;
|
|
tmp2 = (i + 2) & 7;
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.15, 0.28, [i | 8]),
|
|
new HitAreaData(0.48, 0.52, [tmp | 8, 16]),
|
|
new HitAreaData(0.72, 0.85, [tmp2 | 8, 16]),
|
|
new HitAreaData(1.00, 1.00, [j | 8])
|
|
];
|
|
break;
|
|
case 5:
|
|
tmp = (i - 1) & 7;
|
|
tmp2 = (i - 2) & 7;
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.15, 0.28, [i | 8]),
|
|
new HitAreaData(0.48, 0.52, [tmp | 8, 16]),
|
|
new HitAreaData(0.72, 0.85, [tmp2 | 8, 16]),
|
|
new HitAreaData(1.00, 1.00, [j | 8])
|
|
];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// Ai <-> Bj
|
|
key = (i << 5) | (j | 8);
|
|
var key2 = ((j | 8) << 5) | i;
|
|
switch (diff)
|
|
{
|
|
case 0:
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.60, 0.75, [i]),
|
|
new HitAreaData(1.00, 1.00, [j | 8])
|
|
];
|
|
HitAreasLookup[key2] =
|
|
[
|
|
new HitAreaData(0.25, 0.40, [j | 8]),
|
|
new HitAreaData(1.00, 1.00, [i])
|
|
];
|
|
break;
|
|
case 1:
|
|
case 7:
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.45, 0.77, [i]),
|
|
new HitAreaData(1.00, 1.00, [j | 8])
|
|
];
|
|
HitAreasLookup[key2] =
|
|
[
|
|
new HitAreaData(0.23, 0.55, [j | 8]),
|
|
new HitAreaData(1.00, 1.00, [i])
|
|
];
|
|
break;
|
|
case 3:
|
|
tmp = (i + 1) & 7;
|
|
tmp2 = (i + 2) & 7;
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.25, 0.34, [i]),
|
|
new HitAreaData(0.54, 0.68, [i | 8, tmp | 8]),
|
|
new HitAreaData(0.85, 0.90, [tmp2 | 8, 16]),
|
|
new HitAreaData(1.00, 1.00, [j | 8])
|
|
];
|
|
HitAreasLookup[key2] =
|
|
[
|
|
new HitAreaData(0.10, 0.15, [j | 8]),
|
|
new HitAreaData(0.32, 0.46, [tmp2 | 8, 16]),
|
|
new HitAreaData(0.66, 0.75, [i | 8, tmp | 8]),
|
|
new HitAreaData(1.00, 1.00, [i])
|
|
];
|
|
break;
|
|
case 5:
|
|
tmp = (i - 1) & 7;
|
|
tmp2 = (i - 2) & 7;
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.25, 0.34, [i]),
|
|
new HitAreaData(0.54, 0.68, [i | 8, tmp | 8]),
|
|
new HitAreaData(0.85, 0.90, [tmp2 | 8, 16]),
|
|
new HitAreaData(1.00, 1.00, [j | 8])
|
|
];
|
|
HitAreasLookup[key2] =
|
|
[
|
|
new HitAreaData(0.10, 0.15, [j | 8]),
|
|
new HitAreaData(0.32, 0.46, [tmp2 | 8, 16]),
|
|
new HitAreaData(0.66, 0.75, [i | 8, tmp | 8]),
|
|
new HitAreaData(1.00, 1.00, [i])
|
|
];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// C <-> Bj
|
|
key = (16 << 5) | (j | 8);
|
|
key2 = ((j | 8) << 5) | 16;
|
|
HitAreasLookup[key] =
|
|
[
|
|
new HitAreaData(0.50, 0.70, [16]),
|
|
new HitAreaData(1.00, 1.00, [j | 8])
|
|
];
|
|
HitAreasLookup[key2] =
|
|
[
|
|
new HitAreaData(0.30, 0.50, [j | 8]),
|
|
new HitAreaData(1.00, 1.00, [16])
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
public static List<HitAreaData> BuildHitAreas(ParametricSlidePath path)
|
|
{
|
|
var nodeList = new List<Tuple<int, double>>();
|
|
var totalLength = path.GetPathLength();
|
|
var count = (int)Math.Round(totalLength / 10.0);
|
|
int? lastNode = null;
|
|
var enterLength = 0.0;
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var t = (double)i / count;
|
|
var pt = path.GetPointAt(t);
|
|
int? node = null;
|
|
|
|
if (pt.Magnitude < 55.0)
|
|
{
|
|
node = 16;
|
|
}
|
|
else for (var j = 0; j < 8; j++)
|
|
{
|
|
var phi = Math.PI * (3.0 / 8.0 - j / 4.0);
|
|
if ((pt - Complex.FromPolarCoordinates(440.0, phi)).Magnitude < 80.0)
|
|
{
|
|
node = j;
|
|
break;
|
|
}
|
|
|
|
if ((pt - Complex.FromPolarCoordinates(210.0, phi)).Magnitude < 45.0)
|
|
{
|
|
node = j | 8;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (lastNode != node)
|
|
{
|
|
var length = t * totalLength;
|
|
if (lastNode == null)
|
|
{
|
|
enterLength = length;
|
|
}
|
|
else
|
|
{
|
|
nodeList.Add(new Tuple<int, double>(lastNode.Value, (length + enterLength) / 2.0));
|
|
if (node != null)
|
|
{
|
|
enterLength = length;
|
|
}
|
|
}
|
|
}
|
|
|
|
lastNode = node;
|
|
}
|
|
nodeList.Add(new Tuple<int, double>(lastNode!.Value, totalLength));
|
|
nodeList[0] = new Tuple<int, double>(nodeList[0].Item1, 0.0);
|
|
|
|
var result = new List<HitAreaData>();
|
|
result.Add(new HitAreaData(0.0, 0.0, [nodeList[0].Item1]));
|
|
for (var i = 1; i < nodeList.Count; i++)
|
|
{
|
|
var key = (nodeList[i - 1].Item1 << 5) | nodeList[i].Item1;
|
|
var segmentLength = nodeList[i].Item2 - nodeList[i - 1].Item2;
|
|
var data = HitAreasLookup[key];
|
|
var area = result[result.Count - 1];
|
|
result[result.Count - 1] = new HitAreaData(
|
|
area.PushDistance + segmentLength * data[0].PushDistance,
|
|
area.ReleaseDistance + segmentLength * data[0].ReleaseDistance,
|
|
area.PanelAreas
|
|
);
|
|
for (var j = 1; j < data.Length; j++)
|
|
{
|
|
result.Add(new HitAreaData(
|
|
segmentLength * (data[j].PushDistance - data[j - 1].ReleaseDistance),
|
|
segmentLength * (data[j].ReleaseDistance - data[j].PushDistance),
|
|
data[j].PanelAreas
|
|
));
|
|
}
|
|
}
|
|
|
|
double lastPushDistance = 0.0;
|
|
if (path.GetEndType(OptionMirrorID.Normal) == SlideType.Slide_Straight)
|
|
{
|
|
var diff = nodeList[nodeList.Count - 1].Item1 - nodeList[nodeList.Count - 2].Item1;
|
|
diff %= 8;
|
|
lastPushDistance = diff switch
|
|
{
|
|
1 or 2 or 6 or 7 => 130.0,
|
|
_ => 159.0
|
|
};
|
|
}
|
|
else
|
|
{
|
|
lastPushDistance = 175.0;
|
|
}
|
|
|
|
var last2ndArea = result[result.Count - 2];
|
|
var lastArea = result[result.Count - 1];
|
|
var distance = last2ndArea.ReleaseDistance + lastArea.PushDistance + lastArea.ReleaseDistance;
|
|
result[result.Count - 2] = new HitAreaData(last2ndArea.PushDistance, distance - lastPushDistance, last2ndArea.PanelAreas);
|
|
result[result.Count - 1] = new HitAreaData(lastPushDistance, 0.0, lastArea.PanelAreas);
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Convert hit area data to sinmai format (Vector4)
|
|
/// </summary>
|
|
/// <param name="data">hit area data generated by BuildHitAreas()</param>
|
|
/// <param name="starButton">button index of slide-star</param>
|
|
/// <param name="mirrorMode">mirror mode in user option</param>
|
|
/// <returns>sinmai format arrow data, referenced to slide-star</returns>
|
|
public static List<SlideManager.HitArea> ConvertAndRotateHitAreas(IEnumerable<HitAreaData> data, int starButton,
|
|
OptionMirrorID mirrorMode)
|
|
{
|
|
var hitAreaList = new List<SlideManager.HitArea>();
|
|
foreach (var hitAreaData in data)
|
|
{
|
|
var hitArea = new SlideManager.HitArea();
|
|
hitArea.PushDistance = hitAreaData.PushDistance;
|
|
hitArea.ReleaseDistance = hitAreaData.ReleaseDistance;
|
|
foreach (var pad in hitAreaData.PanelAreas)
|
|
{
|
|
var converted = MaiGeometry.MirrorInfo[(int) mirrorMode, pad];
|
|
converted = converted == 16 ? 16 : (converted - starButton) & 0b111 | converted & 0b1000;
|
|
hitArea.HitPoints.Add((InputManager.TouchPanelArea) converted);
|
|
}
|
|
hitAreaList.Add(hitArea);
|
|
}
|
|
return hitAreaList;
|
|
}
|
|
} |