champions-of-luneraria/Assets/Luneraria/Core/Model/Vec2.cs
Martin Stickelbroeck 9bf397e765 U1.5 Unity Core Integration (2/2): explizite float32-Rundung, Nachweise
Befund: Unitys Mono 6.13 rechnet float32-Zwischenwerte doppelt genau
(ECMA-335 I.12.1.3). Der erste Editor-Lauf fand 5 von 157 Golden-
Abweichungen: Blickrichtung bis 1,6e-3 rad; in mirror_match Runde 7 verliert
eine Einheit in Schritt 599 ihr Ziel, und der Kampf dauert 4,8 s länger.
Eine Toleranz ist dafür nicht begründbar.

Maßnahme: Vec2 rundet jeden Zwischenwert explizit auf float32 (conv.r4).
Keine Regeländerung, R1 unverändert. .NET 8: 402/402, Golden 0, Trace
byteidentisch zu vorher. Unity-Editor (Release und Debug): 402/402, Golden
bitgleich in allen Bereichen, Float-Probe 4000/4000.

- CoreIntegrationTests: keine Engine-Referenzen im Kern, Float-Arithmetik
  wie Godot.
- docs/port/U1.5-UNITY.md: Versionen, Lizenz, Pakete, Tests, Golden je
  Bereich, Float-Befund, Editor-Benchmark, fehlende Build-Module.
- docs/port/KNOWN-BEHAVIOR-DIFFERENCES.md mit "Legacy defects retained for
  parity" (R1) und F1.
- Protokolle unter docs/port/logs/u1_5/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:22:36 +02:00

143 lines
5.2 KiB
C#

using System;
namespace Luneraria.Core
{
/// <summary>
/// Ground vector (x, z) with 32-bit components and plain single-precision arithmetic, like
/// Godot's Vector2 (tools/godot-golden/float_probe.gd: 4000 of 4000 samples identical).
/// Scalars of the rules are doubles; they are rounded to float where they meet a vector.
/// Every intermediate result is rounded to float explicitly: ECMA-335 (I.12.1.3) allows a
/// runtime to keep float intermediates at higher precision, and Unity's Mono does. Without
/// the casts `x * x + y * y` differs in the last bit between Mono and .NET/IL2CPP/Godot,
/// and the combat dynamics amplify that into different decisions (docs/port/U1.5-UNITY.md).
/// </summary>
[Serializable]
public struct Vec2 : IEquatable<Vec2>
{
public float X;
public float Y;
public static readonly Vec2 Zero = new Vec2(0f, 0f);
public static readonly Vec2 Right = new Vec2(1f, 0f);
public static readonly Vec2 Left = new Vec2(-1f, 0f);
public Vec2(float x, float y)
{
X = x;
Y = y;
}
/// <summary>From doubles: each component rounded to float.</summary>
public Vec2(double x, double y)
{
X = (float)x;
Y = (float)y;
}
public static Vec2 operator +(Vec2 a, Vec2 b) { return new Vec2((float)(a.X + b.X), (float)(a.Y + b.Y)); }
public static Vec2 operator -(Vec2 a, Vec2 b) { return new Vec2((float)(a.X - b.X), (float)(a.Y - b.Y)); }
public static Vec2 operator -(Vec2 a) { return new Vec2(-a.X, -a.Y); }
public static Vec2 operator *(Vec2 a, double s)
{
float f = (float)s;
return new Vec2((float)(a.X * f), (float)(a.Y * f));
}
public static Vec2 operator *(double s, Vec2 a)
{
float f = (float)s;
return new Vec2((float)(f * a.X), (float)(f * a.Y));
}
public static Vec2 operator /(Vec2 a, double s)
{
float f = (float)s;
return new Vec2((float)(a.X / f), (float)(a.Y / f));
}
public static bool operator ==(Vec2 a, Vec2 b) { return a.X == b.X && a.Y == b.Y; }
public static bool operator !=(Vec2 a, Vec2 b) { return !(a == b); }
public float Length() { return (float)MathF.Sqrt(LengthSquared()); }
public float LengthSquared() { return (float)((float)(X * X) + (float)(Y * Y)); }
public float DistanceTo(Vec2 p)
{
return (float)MathF.Sqrt(DistanceSquaredTo(p));
}
public float DistanceSquaredTo(Vec2 p)
{
float dx = (float)(X - p.X);
float dy = (float)(Y - p.Y);
return (float)((float)(dx * dx) + (float)(dy * dy));
}
public float Dot(Vec2 p) { return (float)((float)(X * p.X) + (float)(Y * p.Y)); }
public Vec2 Normalized()
{
float l = LengthSquared();
if (l != 0f)
{
l = (float)MathF.Sqrt(l);
return new Vec2((float)(X / l), (float)(Y / l));
}
return this;
}
/// <summary>from + (to - from) * weight per component, weight rounded to float.</summary>
public Vec2 Lerp(Vec2 to, double weight)
{
float w = (float)weight;
return new Vec2((float)(X + (float)((float)(to.X - X) * w)), (float)(Y + (float)((float)(to.Y - Y) * w)));
}
public Vec2 LimitLength(double length)
{
float limit = (float)length;
float l = Length();
Vec2 v = this;
if (l > 0f && limit < l)
{
v.X = (float)(v.X / l);
v.Y = (float)(v.Y / l);
v.X = (float)(v.X * limit);
v.Y = (float)(v.Y * limit);
}
return v;
}
public Vec2 Rotated(double angle)
{
float a = (float)angle;
float sine = (float)MathF.Sin(a);
float cosi = (float)MathF.Cos(a);
return new Vec2((float)((float)(X * cosi) - (float)(Y * sine)), (float)((float)(X * sine) + (float)(Y * cosi)));
}
public bool Equals(Vec2 other) { return X.Equals(other.X) && Y.Equals(other.Y); }
public override bool Equals(object obj) { return obj is Vec2 other && Equals(other); }
public override int GetHashCode() { return (X.GetHashCode() * 397) ^ Y.GetHashCode(); }
public override string ToString() { return "(" + X.ToString("R", System.Globalization.CultureInfo.InvariantCulture) + ", " + Y.ToString("R", System.Globalization.CultureInfo.InvariantCulture) + ")"; }
}
/// <summary>Integer grid cell for the spatial buckets of a combat step.</summary>
public struct Vec2I : IEquatable<Vec2I>
{
public int X;
public int Y;
public Vec2I(int x, int y)
{
X = x;
Y = y;
}
public bool Equals(Vec2I other) { return X == other.X && Y == other.Y; }
public override bool Equals(object obj) { return obj is Vec2I other && Equals(other); }
public override int GetHashCode() { return (X * 73856093) ^ (Y * 19349663); }
}
}