fix #182: port CSphere collision family — retail-faithful crowd wiggle (retires TS-45)
Humanoid creatures/players collide as body Spheres (ShadowShapeBuilder emits Sphere-type shadows for a Setup with Spheres + no CylSpheres), so player-vs-monster crowd contact ran through Transition.SphereCollision — a hand-rolled 3-D wall-slide (register TS-45), NOT a port of retail CSphere::intersects_sphere. It shaved no eps, force-pushed each contact RADIALLY to a fixed combinedR+1cm shell, ignored the head sphere, and always returned Slid. In a crowd the opposing radial de-penetration pushes from neighbours fight each other -> the player wedges and can't wiggle free (the user's live report). Port the full CSphere family verbatim — dispatcher 0x00537A80 + step_sphere_up / slide_sphere / land_on_sphere / collide_with_point / step_sphere_down — the direct analog of the 2026-07-05 CCylSphere port (#172). The grounded slide now routes through the shared crease SlideSphere (0x00537440, #116-Ghidra-confirmed) -> tangential shuffle along the contact toward gaps, retail-faithful. isCreature (target creature/missile) gates OFF the stand-on/land-on branches (2 & 5). ACE Sphere.cs = readable oracle; pseudocode doc 2026-07-07-csphere-collision-family. Retail-faithfulness verified: CTransition::validate_transition (0x0050aa70:272593) reverts curr_pos on any non-clean-OK step, so a deep-mutual-overlap start wedges in retail too — the realistic crowd-edge graze slides free (SphereCollisionFamilyTests slide-around trajectory: player grazes a creature's SW, curves around its west side, continues N). TS-45 retired, AP-84 added (PerfectClip TOI dead in M1.5). Core 2603/0, App 741/0. Pending user visual gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0cbe1102d9
commit
96ae274081
6 changed files with 908 additions and 85 deletions
|
|
@ -2792,7 +2792,13 @@ public sealed class Transition
|
|||
continue;
|
||||
}
|
||||
|
||||
result = SphereCollision(obj, sp);
|
||||
// Retail var_8_1 (pc:276846; ACE PhysicsObj.cs:409): the target is
|
||||
// a creature OR a missile (state & MISSILE_PS 0x40). Gates OFF the
|
||||
// "stand-on / land-on" branches (2 and 5) of the CSphere family —
|
||||
// you push against a creature horizontally but never rest on it.
|
||||
bool isCreature = (obj.State & 0x40u) != 0
|
||||
|| (obj.Flags & EntityCollisionFlags.IsCreature) != 0;
|
||||
result = SphereCollision(obj, sp, engine, isCreature);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -3117,114 +3123,340 @@ public sealed class Transition
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sphere collision test for <see cref="ShadowCollisionType.Sphere"/> objects.
|
||||
/// Uses a true 3-D sphere-sphere overlap test — no height clamp, no XY-only
|
||||
/// distance — matching retail's <c>CSphere::intersects_sphere @ 0x00537A80</c>.
|
||||
/// Retail <c>CSphere::intersects_sphere</c> dispatcher — port of
|
||||
/// <c>0x00537A80</c> (acclient_2013_pseudo_c.txt:321678). Full family:
|
||||
/// <c>collides_with_sphere</c> 0x005369e0, <c>step_sphere_up</c> 0x00537900,
|
||||
/// <c>slide_sphere</c> 0x00537440, <c>land_on_sphere</c> 0x005379a0,
|
||||
/// <c>collide_with_point</c> 0x00537230, <c>step_sphere_down</c> 0x00536d20.
|
||||
/// Pseudocode + settled ambiguities + ACE cross-reference:
|
||||
/// docs/research/2026-07-07-csphere-collision-family-pseudocode.md.
|
||||
///
|
||||
/// <para>
|
||||
/// Implements the subset of the 6-path dispatcher needed for static/placed
|
||||
/// Sphere objects: static overlap check (obstruction_ethereal / check_walkable /
|
||||
/// Contact-grounded paths), plus a 3-D outward push-back for the slide response.
|
||||
/// The swept quadratic from <see cref="CollisionPrimitives.SweptSphereHitsSphere"/>
|
||||
/// is used for the narrow-phase; the slide response mirrors the cylinder's
|
||||
/// wall-slide but pushes outward in 3-D (not XY-only).
|
||||
/// Player↔humanoid-creature collision runs HERE: humanoid Setups carry body
|
||||
/// <c>Spheres</c> (no CylSpheres) → <see cref="ShadowShapeBuilder.FromSetup"/>
|
||||
/// emits <see cref="ShadowCollisionType.Sphere"/>. Replaces the pre-2026-07-07
|
||||
/// hand-rolled 3-D wall-slide (register <b>TS-45</b>, retired here): that
|
||||
/// approximation shaved no ε, force-pushed the mover RADIALLY out to a fixed
|
||||
/// <c>combinedR + 1 cm</c> shell every contact, ignored the head sphere, and
|
||||
/// always returned Slid — so a player packed by a crowd of body-sphere
|
||||
/// creatures wedged (opposing radial pushes cancel) instead of shuffling out
|
||||
/// along the contact crease. The faithful family routes the grounded slide
|
||||
/// through the shared <see cref="SlideSphere"/> (the Ghidra-confirmed
|
||||
/// <c>CSphere::slide_sphere</c> 0x00537440 crease projection — #116), giving
|
||||
/// retail's tangential wiggle room. Driving repro: the user's live
|
||||
/// crowd-collision report, 2026-07-07.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// ACE oracle: <c>Sphere.IntersectsSphere</c> in
|
||||
/// <c>ACE.Server/Physics/Sphere.cs</c> — particularly the
|
||||
/// <c>ObstructionEthereal/Placement</c>, <c>CheckWalkable</c>, and
|
||||
/// <c>Contact</c> branches. Retail decomp cross-reference:
|
||||
/// <c>acclient_2013_pseudo_c.txt:321678</c>.
|
||||
/// The <see cref="ShadowEntry"/> already carries the wrapper overload's
|
||||
/// (0x00537fd0) work: Position = globalized center (entity frame at
|
||||
/// registration), Radius pre-scaled; CSphere uses full 3-D distance (no
|
||||
/// height clamp — unlike CCylSphere). Ethereal targets keep the existing
|
||||
/// early-OK consume site 1 (retail's void ethereal branch produces no block);
|
||||
/// the caller's Layer-2 override is the door passability mechanism, and the
|
||||
/// step-down pass never reaches here for ethereal targets (pc:276799 skip).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private TransitionState SphereCollision(ShadowEntry obj, SpherePath sp)
|
||||
/// <param name="isCreature">Retail <c>var_8_1</c> (pc:276846; ACE
|
||||
/// PhysicsObj.cs:409): the target is a creature OR a missile
|
||||
/// (<c>state & MISSILE_PS 0x40</c>). Gates OFF the "stand-on / land-on"
|
||||
/// branches (2 and 5) — you push against a creature horizontally but never
|
||||
/// rest on it. Computed by the caller.</param>
|
||||
private TransitionState SphereCollision(ShadowEntry obj, SpherePath sp, PhysicsEngine engine, bool isCreature)
|
||||
{
|
||||
// Consume site 1 — CSphere::intersects_sphere @ 0x00537ae4 (pc:321692).
|
||||
// When obstruction_ethereal is set (target is ETHEREAL-alone, state & 0x4),
|
||||
// the retail function is void and all paths in the ethereal branch return
|
||||
// without producing a COLLIDED/Slid result — the player is fully passable.
|
||||
// We mirror this by returning OK immediately, skipping all blocking paths.
|
||||
// Retail ref: acclient_2013_pseudo_c.txt:321692.
|
||||
// Ethereal-alone (state & 0x4): retail's void ethereal branch returns
|
||||
// without a blocking result. Mirror by returning OK immediately — the
|
||||
// caller's Layer-2 override handles non-static ethereal passability for
|
||||
// the other shape branches; only the Sphere branch early-outs here.
|
||||
// Unchanged from the pre-port behavior → every ethereal/door test stays
|
||||
// identical.
|
||||
if (sp.ObstructionEthereal)
|
||||
return TransitionState.OK;
|
||||
|
||||
var ci = CollisionInfo;
|
||||
Vector3 sphereCurrPos = sp.GlobalCurrCenter[0].Origin;
|
||||
Vector3 sphereCheckPos = sp.GlobalSphere[0].Origin;
|
||||
float sphRadius = sp.GlobalSphere[0].Radius;
|
||||
Vector3 sphMovement = sphereCheckPos - sphereCurrPos;
|
||||
var oi = ObjectInfo;
|
||||
var s0 = sp.GlobalSphere[0];
|
||||
Vector3 disp0 = s0.Origin - obj.Position;
|
||||
// radsum: ε shaved ONCE (0x00537acd). Resting flush against a monster is
|
||||
// a NON-overlap, so a shuffle that ends touching settles instead of
|
||||
// re-colliding every frame — the first wiggle-room ingredient TS-45 lost.
|
||||
float radsum = s0.Radius + obj.Radius - PhysicsGlobals.EPSILON;
|
||||
|
||||
// 3-D distance from check position to target sphere centre.
|
||||
// Unlike CCylSphere (which clips to a height range and uses XY-only
|
||||
// distance), CSphere uses the full 3-D Euclidean distance.
|
||||
// Retail anchor: CSphere::intersects_sphere @ 0x00537A80 —
|
||||
// the displacement vector is the full (x,y,z) delta, not XY-only.
|
||||
float dx = sphereCheckPos.X - obj.Position.X;
|
||||
float dy = sphereCheckPos.Y - obj.Position.Y;
|
||||
float dz = sphereCheckPos.Z - obj.Position.Z;
|
||||
float distSq = dx * dx + dy * dy + dz * dz;
|
||||
float combinedR = sphRadius + obj.Radius;
|
||||
float combinedRSq = combinedR * combinedR;
|
||||
bool hasHead = sp.NumSphere > 1;
|
||||
Vector3 disp1 = default;
|
||||
if (hasHead)
|
||||
disp1 = sp.GlobalSphere[1].Origin - obj.Position;
|
||||
|
||||
if (distSq >= combinedRSq)
|
||||
return TransitionState.OK; // not overlapping at check position
|
||||
|
||||
// ── Overlap detected — compute 3-D outward collision normal ──────
|
||||
float dist = MathF.Sqrt(distSq);
|
||||
Vector3 collisionNormal;
|
||||
if (dist < PhysicsGlobals.EPSILON)
|
||||
// ── Branch 1 (0x00537ae4): placement — detection only. ──
|
||||
if (sp.InsertType == InsertType.Placement)
|
||||
{
|
||||
// Sphere centers coincide — push back along reverse movement.
|
||||
float mLen = sphMovement.Length();
|
||||
if (mLen > PhysicsGlobals.EPSILON)
|
||||
collisionNormal = -sphMovement / mLen;
|
||||
else
|
||||
collisionNormal = Vector3.UnitX;
|
||||
}
|
||||
else
|
||||
{
|
||||
collisionNormal = new Vector3(dx / dist, dy / dist, dz / dist);
|
||||
if (SphereCollidesWithSphere(disp0, radsum))
|
||||
return TransitionState.Collided;
|
||||
if (hasHead && SphereCollidesWithSphere(disp1, radsum))
|
||||
return TransitionState.Collided;
|
||||
return TransitionState.OK;
|
||||
}
|
||||
|
||||
// ── Wall-slide response (mirrors CylinderCollision but in 3-D) ───
|
||||
// Project movement onto the plane perpendicular to the collision normal,
|
||||
// then push the slid position outside the combined-radius shell.
|
||||
float movementIntoWall = Vector3.Dot(sphMovement, collisionNormal);
|
||||
Vector3 projectedMovement = sphMovement - collisionNormal * movementIntoWall;
|
||||
|
||||
Vector3 slidPos = sphereCurrPos + projectedMovement;
|
||||
|
||||
// Ensure slid position is outside combined radius (3-D push).
|
||||
float sdx = slidPos.X - obj.Position.X;
|
||||
float sdy = slidPos.Y - obj.Position.Y;
|
||||
float sdz = slidPos.Z - obj.Position.Z;
|
||||
float sDistSq = sdx * sdx + sdy * sdy + sdz * sdz;
|
||||
float minDist = combinedR + 0.01f;
|
||||
if (sDistSq < minDist * minDist)
|
||||
// ── Branch 2 (0x00537af5): step-down probe — land on the top. ──
|
||||
if (sp.StepDown)
|
||||
{
|
||||
float sDist = MathF.Sqrt(sDistSq);
|
||||
if (sDist < PhysicsGlobals.EPSILON)
|
||||
if (isCreature)
|
||||
return TransitionState.OK; // §8.1 — never stand ON a creature/missile
|
||||
return SphereStepSphereDown(obj, sp, disp0, radsum);
|
||||
}
|
||||
|
||||
// ── Branch 3 (0x00537b27): walkable probe — occupancy blocks. ──
|
||||
if (sp.CheckWalkable)
|
||||
{
|
||||
if (SphereCollidesWithSphere(disp0, radsum))
|
||||
return TransitionState.Collided;
|
||||
if (hasHead && SphereCollidesWithSphere(disp1, radsum))
|
||||
return TransitionState.Collided;
|
||||
return TransitionState.OK;
|
||||
}
|
||||
|
||||
if (!sp.Collide)
|
||||
{
|
||||
// ── Branch 4 (0x00537de8): normal transition sweep. ──
|
||||
if ((oi.State & (ObjectInfoState.Contact | ObjectInfoState.OnWalkable)) != 0)
|
||||
{
|
||||
slidPos.X = obj.Position.X + collisionNormal.X * minDist;
|
||||
slidPos.Y = obj.Position.Y + collisionNormal.Y * minDist;
|
||||
slidPos.Z = obj.Position.Z + collisionNormal.Z * minDist;
|
||||
// Grounded: foot hit → step over / slide; head hit → slide.
|
||||
if (SphereCollidesWithSphere(disp0, radsum))
|
||||
return SphereStepSphereUp(obj, sp, engine, disp0, radsum);
|
||||
if (hasHead && SphereCollidesWithSphere(disp1, radsum))
|
||||
return SphereSlideSphere(obj, sp, 1);
|
||||
}
|
||||
else if ((oi.State & ObjectInfoState.PathClipped) != 0)
|
||||
{
|
||||
if (SphereCollidesWithSphere(disp0, radsum))
|
||||
return SphereCollideWithPoint(obj, sp, s0, radsum, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
float pushDist = minDist - sDist;
|
||||
slidPos.X += (sdx / sDist) * pushDist;
|
||||
slidPos.Y += (sdy / sDist) * pushDist;
|
||||
slidPos.Z += (sdz / sDist) * pushDist;
|
||||
// Airborne: foot hit → land on the sphere top; head hit → point hit.
|
||||
if (SphereCollidesWithSphere(disp0, radsum))
|
||||
return SphereLandOnSphere(obj, sp);
|
||||
if (hasHead && SphereCollidesWithSphere(disp1, radsum))
|
||||
return SphereCollideWithPoint(obj, sp, sp.GlobalSphere[1], radsum, 1);
|
||||
}
|
||||
return TransitionState.OK;
|
||||
}
|
||||
|
||||
Vector3 delta = slidPos - sphereCheckPos;
|
||||
sp.AddOffsetToCheckPos(delta);
|
||||
// ── Branch 5 (0x00537b80): collide-flag re-test — exact-TOI landing on a
|
||||
// sphere's curved top (a TILTED contact plane by the sphere-to-sphere
|
||||
// direction, unlike the cylinder's flat disc). Creature/missile-gated OFF;
|
||||
// in M1.5 only non-creature Sphere-shape statics reach it (rare — statics
|
||||
// are CylSphere/BSP). Ported per ACE Sphere.cs:369-414.
|
||||
if (isCreature)
|
||||
return TransitionState.OK; // §8.1
|
||||
|
||||
ci.SetCollisionNormal(collisionNormal);
|
||||
ci.SetSlidingNormal(collisionNormal);
|
||||
return TransitionState.Slid;
|
||||
bool hit0 = SphereCollidesWithSphere(disp0, radsum);
|
||||
if (!hit0 && !(hasHead && SphereCollidesWithSphere(disp1, radsum)))
|
||||
return TransitionState.OK;
|
||||
|
||||
// Block offset = 0 (continuous world frame; same note as SlideSphere's gDelta).
|
||||
Vector3 movement = sp.GlobalCurrCenter[0].Origin - s0.Origin;
|
||||
float radsumEps = radsum + PhysicsGlobals.EPSILON;
|
||||
float lenSq = movement.LengthSquared();
|
||||
if (MathF.Abs(lenSq) < PhysicsGlobals.EPSILON)
|
||||
return TransitionState.Collided;
|
||||
float diff = -Vector3.Dot(movement, disp0);
|
||||
float disc = diff * diff - (disp0.LengthSquared() - radsumEps * radsumEps) * lenSq;
|
||||
if (disc < 0f)
|
||||
return TransitionState.Collided; // defensive: retail reaches the TOI only via a colliding sphere (disc≥0)
|
||||
float tt = MathF.Sqrt(disc) + diff;
|
||||
if (tt > 1f) tt = diff * 2f - tt;
|
||||
float time = tt / lenSq;
|
||||
float timecheck = (1f - time) * sp.WalkInterp;
|
||||
if (timecheck >= sp.WalkInterp || timecheck < -0.1f)
|
||||
return TransitionState.Collided;
|
||||
movement *= time;
|
||||
Vector3 dispN = (disp0 + movement) / radsumEps;
|
||||
if (dispN.Z <= sp.WalkableAllowance) // !is_walkable_allowable — sphere top too steep to rest on
|
||||
return TransitionState.OK;
|
||||
Vector3 restPt = s0.Origin - dispN * s0.Radius;
|
||||
// is_water=1 verbatim retail (0x00536ecf). Do not "fix".
|
||||
var contactPlane = new Plane(dispN, -Vector3.Dot(dispN, restPt));
|
||||
ci.SetContactPlane(contactPlane, sp.CheckCellId, isWater: true);
|
||||
sp.WalkInterp = timecheck;
|
||||
sp.AddOffsetToCheckPos(movement);
|
||||
return TransitionState.Adjusted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CSphere::collides_with_sphere</c> (0x005369e0, pc:320964): pure
|
||||
/// 3-D overlap — <c>|disp|² <= radsum²</c>. <paramref name="disp"/> = the
|
||||
/// mover sphere center − target center. No XY/Z-band split — that's the
|
||||
/// cylinder.
|
||||
/// </summary>
|
||||
private static bool SphereCollidesWithSphere(Vector3 disp, float radsum)
|
||||
=> disp.LengthSquared() <= radsum * radsum;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CSphere::step_sphere_up</c> (0x00537900, pc:321611). Too tall to
|
||||
/// step over → slide; otherwise the generic step-up (<see cref="DoStepUp"/> =
|
||||
/// CTransition::step_up). Unlike the cylinder path, retail passes the
|
||||
/// UNNORMALIZED <c>global_curr_center − center</c> and does NOT rotate by the
|
||||
/// target frame (a sphere has no orientation, 0x0053794e). For a full-height
|
||||
/// creature the head clearance far exceeds step_up_height, so this always
|
||||
/// slides — the grounded-crowd path.
|
||||
/// </summary>
|
||||
private TransitionState SphereStepSphereUp(ShadowEntry obj, SpherePath sp,
|
||||
PhysicsEngine engine, Vector3 disp0, float radsum)
|
||||
{
|
||||
// radsum += ε here (dispatcher shaved it once; net = s0.r + obj.r) —
|
||||
// retail 0x0053790a / ACE Sphere.cs:688.
|
||||
float radsumEps = radsum + PhysicsGlobals.EPSILON;
|
||||
if (ObjectInfo.StepUpHeight < radsumEps - disp0.Z)
|
||||
return SphereSlideSphere(obj, sp, 0);
|
||||
|
||||
// Unnormalized center-to-center (retail 0x0053794e passes var_c raw).
|
||||
Vector3 n = sp.GlobalCurrCenter[0].Origin - obj.Position;
|
||||
|
||||
// engine==null only in bare unit-test transitions with no step-up
|
||||
// machinery — the retail-faithful fallback is the slide.
|
||||
if (engine is not null && DoStepUp(n, engine))
|
||||
return TransitionState.OK;
|
||||
|
||||
return sp.StepUpSlide(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CSphere::slide_sphere</c> (0x00537440 core via the 0x00537a10
|
||||
/// wrapper, pc:321403/321660): collision normal =
|
||||
/// <c>global_curr_center[sphereNum] − center</c> (normalized), then the shared
|
||||
/// crease projection (<see cref="SlideSphere"/>). THE wiggle-room primitive —
|
||||
/// the mover slides ALONG the tangent where the monster's side meets the
|
||||
/// ground, toward the gap, instead of being force-pushed radially out.
|
||||
/// </summary>
|
||||
private TransitionState SphereSlideSphere(ShadowEntry obj, SpherePath sp, int sphereNum)
|
||||
{
|
||||
Vector3 n = sp.GlobalCurrCenter[sphereNum].Origin - obj.Position;
|
||||
if (NormalizeCheckSmall(ref n))
|
||||
return TransitionState.Collided;
|
||||
return SlideSphere(n, sp.GlobalCurrCenter[sphereNum].Origin, sphereNum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CSphere::land_on_sphere</c> (0x005379a0, pc:321642): airborne
|
||||
/// foot hit — arm the Collide re-test (backup + flag) and relax the walkable
|
||||
/// allowance to LandingZ. The NEXT attempt's branch 5 rests the sphere on the
|
||||
/// top with the exact time-of-impact. Identical shape to
|
||||
/// <see cref="CylLandOnCylinder"/>.
|
||||
/// </summary>
|
||||
private TransitionState SphereLandOnSphere(ShadowEntry obj, SpherePath sp)
|
||||
{
|
||||
Vector3 n = sp.GlobalCurrCenter[0].Origin - obj.Position;
|
||||
if (NormalizeCheckSmall(ref n))
|
||||
return TransitionState.Collided;
|
||||
sp.SetCollide(n);
|
||||
sp.WalkableAllowance = PhysicsGlobals.LandingZ; // 0.0871557 (0x005379f2)
|
||||
return TransitionState.Adjusted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CSphere::collide_with_point</c> (0x00537230, pc:321327):
|
||||
/// PathClipped movers + airborne head-sphere hits. Non-PerfectClip records the
|
||||
/// center-to-center collision normal and hard-stops (the M1.5 load-bearing
|
||||
/// path — players never set PerfectClip). PerfectClip gets the exact
|
||||
/// time-of-impact reposition (missiles only — AP-84, dead in M1.5, ported per
|
||||
/// ACE Sphere.cs:175-210; re-verify vs Ghidra before missiles ship).
|
||||
/// </summary>
|
||||
private TransitionState SphereCollideWithPoint(ShadowEntry obj, SpherePath sp,
|
||||
Sphere checkSphere, float radsum, int sphereNum)
|
||||
{
|
||||
Vector3 gCenter = sp.GlobalCurrCenter[sphereNum].Origin;
|
||||
Vector3 globalOffset = gCenter - obj.Position;
|
||||
|
||||
if (!ObjectInfo.State.HasFlag(ObjectInfoState.PerfectClip))
|
||||
{
|
||||
if (!NormalizeCheckSmall(ref globalOffset))
|
||||
CollisionInfo.SetCollisionNormal(globalOffset);
|
||||
return TransitionState.Collided;
|
||||
}
|
||||
|
||||
// PerfectClip exact time-of-impact (AP-84 — dead in M1.5). Block offset = 0.
|
||||
Vector3 checkOffset = checkSphere.Origin - gCenter;
|
||||
double toi = FindSphereTimeOfCollision(checkOffset, globalOffset, radsum + PhysicsGlobals.EPSILON);
|
||||
if (toi < PhysicsGlobals.EPSILON || toi > 1.0)
|
||||
return TransitionState.Collided;
|
||||
Vector3 collisionOffset = checkOffset * (float)toi - checkOffset;
|
||||
Vector3 oldDisp = collisionOffset + checkSphere.Origin - obj.Position;
|
||||
CollisionInfo.SetCollisionNormal(oldDisp / radsum);
|
||||
sp.AddOffsetToCheckPos(oldDisp);
|
||||
return TransitionState.Adjusted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CSphere::step_sphere_down</c> (0x00536d20, pc:321133): during a
|
||||
/// step-down probe, land the foot sphere on a sphere's curved top — contact
|
||||
/// plane tilted by the sphere-to-sphere direction (unlike the cylinder's flat
|
||||
/// disc). Creature/missile-gated OFF at the dispatcher (branch 2). Ported per
|
||||
/// ACE Sphere.cs:617-664.
|
||||
/// </summary>
|
||||
private TransitionState SphereStepSphereDown(ShadowEntry obj, SpherePath sp,
|
||||
Vector3 disp0, float radsum)
|
||||
{
|
||||
bool hit = SphereCollidesWithSphere(disp0, radsum);
|
||||
if (!hit && sp.NumSphere > 1)
|
||||
{
|
||||
Vector3 disp1 = sp.GlobalSphere[1].Origin - obj.Position;
|
||||
hit = SphereCollidesWithSphere(disp1, radsum);
|
||||
}
|
||||
if (!hit)
|
||||
return TransitionState.OK;
|
||||
|
||||
float stepDown = sp.StepDownAmt * sp.WalkInterp;
|
||||
if (MathF.Abs(stepDown) < PhysicsGlobals.EPSILON)
|
||||
return TransitionState.Collided;
|
||||
|
||||
float radsumEps = radsum + PhysicsGlobals.EPSILON;
|
||||
// Curved-top height at this XY offset (the sphere equation solved for z).
|
||||
float underRoot = radsumEps * radsumEps - (disp0.X * disp0.X + disp0.Y * disp0.Y);
|
||||
if (underRoot < 0f)
|
||||
return TransitionState.Collided; // defensive: XY already outside radsum
|
||||
float val = MathF.Sqrt(underRoot);
|
||||
float scaledStep = (val - disp0.Z) / stepDown;
|
||||
float timecheck = (1f - scaledStep) * sp.WalkInterp;
|
||||
if (timecheck >= sp.WalkInterp || timecheck < -0.1f)
|
||||
return TransitionState.Collided;
|
||||
|
||||
float interp = stepDown * scaledStep;
|
||||
Vector3 dispN = new Vector3(disp0.X, disp0.Y, disp0.Z + interp) / radsumEps;
|
||||
if (dispN.Z <= sp.WalkableAllowance)
|
||||
return TransitionState.OK;
|
||||
|
||||
// Contact point on the TARGET sphere surface (retail 0x00536e9a:
|
||||
// this.center + this.radius·disp).
|
||||
Vector3 restPt = obj.Position + dispN * obj.Radius;
|
||||
var restPlane = new Plane(dispN, -Vector3.Dot(dispN, restPt));
|
||||
CollisionInfo.SetContactPlane(restPlane, sp.CheckCellId, isWater: true);
|
||||
sp.WalkInterp = timecheck;
|
||||
sp.AddOffsetToCheckPos(new Vector3(0f, 0f, interp));
|
||||
return TransitionState.Adjusted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sphere-vs-sphere swept time-of-collision (retail
|
||||
/// <c>CSphere::FindTimeOfCollision</c>; ACE Sphere.cs:232). Returns a 0-1
|
||||
/// fraction along <paramref name="movement"/>, or -1 for no forward hit. Used
|
||||
/// only by the (M1.5-dead) PerfectClip branch of
|
||||
/// <see cref="SphereCollideWithPoint"/>.
|
||||
/// </summary>
|
||||
private static double FindSphereTimeOfCollision(Vector3 movement, Vector3 spherePos, float radSum)
|
||||
{
|
||||
float distSq = movement.LengthSquared();
|
||||
if (distSq < PhysicsGlobals.EPSILON) return -1;
|
||||
float nonCollide = spherePos.LengthSquared() - radSum * radSum;
|
||||
if (nonCollide < PhysicsGlobals.EPSILON) return -1;
|
||||
float similar = -Vector3.Dot(spherePos, movement);
|
||||
double nonCollideB = (double)similar * similar - (double)nonCollide * distSq;
|
||||
if (nonCollideB < 0) return -1;
|
||||
double cDist = Math.Sqrt(nonCollideB);
|
||||
if (similar - cDist < 0)
|
||||
return -1 * (cDist + similar) / distSq;
|
||||
return -1 * (similar - cDist) / distSq;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue