test16 version as base for OOB gravity
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Checks the relative frame used by Sable's OBB collision.
|
||||
|
||||
Sable gives a ship block OBB the SubLevel pose q_ship and gives the custom player
|
||||
OBB q_entity. For an upright player relative to the logical deck, q_entity must be
|
||||
q_ship, so inverse(q_ship) * q_entity is identity.
|
||||
"""
|
||||
import math
|
||||
|
||||
|
||||
def qx(deg):
|
||||
a = math.radians(deg) / 2.0
|
||||
return (math.cos(a), math.sin(a), 0.0, 0.0) # w,x,y,z
|
||||
|
||||
|
||||
def conj(q):
|
||||
w,x,y,z=q
|
||||
return (w,-x,-y,-z)
|
||||
|
||||
|
||||
def mul(a,b):
|
||||
aw,ax,ay,az=a; bw,bx,by,bz=b
|
||||
return (
|
||||
aw*bw-ax*bx-ay*by-az*bz,
|
||||
aw*bx+ax*bw+ay*bz-az*by,
|
||||
aw*by-ax*bz+ay*bw+az*bx,
|
||||
aw*bz+ax*by-ay*bx+az*bw,
|
||||
)
|
||||
|
||||
|
||||
def rotate(q,v):
|
||||
p=(0.0,*v)
|
||||
r=mul(mul(q,p),conj(q))
|
||||
return r[1:]
|
||||
|
||||
|
||||
def close(a,b,eps=1e-9):
|
||||
return all(abs(x-y)<eps for x,y in zip(a,b))
|
||||
|
||||
for deg in (0,20,90,180):
|
||||
ship=qx(deg)
|
||||
entity=ship
|
||||
relative=mul(conj(ship),entity)
|
||||
world_up=rotate(entity,(0.0,1.0,0.0))
|
||||
world_gravity=rotate(ship,(0.0,-1.0,0.0))
|
||||
assert close(relative,(1.0,0.0,0.0,0.0)) or close(relative,(-1.0,0.0,0.0,0.0))
|
||||
assert abs(sum(a*b for a,b in zip(world_up,world_gravity))+1.0)<1e-9
|
||||
print(f"X {deg:3d}: relative={relative}, up={world_up}, gravity={world_gravity}")
|
||||
|
||||
print("OK: player OBB stays upright relative to the logical deck at 0/20/90/180 degrees")
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression checks for local creative-flight input and changing ship orientation."""
|
||||
import math
|
||||
|
||||
|
||||
def qx(deg):
|
||||
a = math.radians(deg) / 2.0
|
||||
return (math.cos(a), math.sin(a), 0.0, 0.0) # w,x,y,z
|
||||
|
||||
|
||||
def conj(q):
|
||||
w,x,y,z=q
|
||||
return (w,-x,-y,-z)
|
||||
|
||||
|
||||
def mul(a,b):
|
||||
aw,ax,ay,az=a; bw,bx,by,bz=b
|
||||
return (
|
||||
aw*bw-ax*bx-ay*by-az*bz,
|
||||
aw*bx+ax*bw+ay*bz-az*by,
|
||||
aw*by-ax*bz+ay*bw+az*bx,
|
||||
aw*bz+ax*by-ay*bx+az*bw,
|
||||
)
|
||||
|
||||
|
||||
def rotate(q,v):
|
||||
p=(0.0,*v)
|
||||
r=mul(mul(q,p),conj(q))
|
||||
return r[1:]
|
||||
|
||||
|
||||
def add(a,b): return tuple(x+y for x,y in zip(a,b))
|
||||
def sub(a,b): return tuple(x-y for x,y in zip(a,b))
|
||||
def close(a,b,e=1e-9): return all(abs(x-y)<e for x,y in zip(a,b))
|
||||
|
||||
# Stored local velocity is authoritative. LocalPlayer.aiStep adds a local-Y flight impulse,
|
||||
# already rotated into world space by Sable. Reconciliation must recover exactly that impulse,
|
||||
# even when the frame orientation changes between ticks.
|
||||
stored_local=(0.13,-0.04,0.27)
|
||||
ascend_local=(0.0,0.15,0.0)
|
||||
for previous,current in ((0,20),(20,90),(90,180),(180,10)):
|
||||
q=qx(current)
|
||||
expected_world=rotate(q,stored_local)
|
||||
actual_world=add(expected_world,rotate(q,ascend_local))
|
||||
external_world=sub(actual_world,expected_world)
|
||||
recovered_external=rotate(conj(q),external_world)
|
||||
reconciled=add(stored_local,recovered_external)
|
||||
assert close(recovered_external,ascend_local)
|
||||
assert close(reconciled,add(stored_local,ascend_local))
|
||||
print(f"{previous:3d}->{current:3d}: recovered flight impulse {recovered_external}")
|
||||
|
||||
# A frame rotation by itself must not manufacture a flight impulse.
|
||||
for deg in (0,20,90,180):
|
||||
q=qx(deg)
|
||||
expected=rotate(q,stored_local)
|
||||
recovered=rotate(conj(q),sub(expected,expected))
|
||||
assert close(recovered,(0.0,0.0,0.0))
|
||||
|
||||
print('OK: flight input is recovered in local Y and frame rotation adds no fake acceleration')
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic basis checks for the local/world gravity convention used by test11."""
|
||||
import math
|
||||
|
||||
|
||||
def q_axis(axis, degrees):
|
||||
a = math.radians(degrees) * 0.5
|
||||
s = math.sin(a)
|
||||
return (axis[0]*s, axis[1]*s, axis[2]*s, math.cos(a))
|
||||
|
||||
|
||||
def q_conj(q):
|
||||
return (-q[0], -q[1], -q[2], q[3])
|
||||
|
||||
|
||||
def q_mul(a, b):
|
||||
ax, ay, az, aw = a; bx, by, bz, bw = b
|
||||
return (
|
||||
aw*bx + ax*bw + ay*bz - az*by,
|
||||
aw*by - ax*bz + ay*bw + az*bx,
|
||||
aw*bz + ax*by - ay*bx + az*bw,
|
||||
aw*bw - ax*bx - ay*by - az*bz,
|
||||
)
|
||||
|
||||
|
||||
def rotate(q, v):
|
||||
r = q_mul(q_mul(q, (v[0], v[1], v[2], 0.0)), q_conj(q))
|
||||
return r[:3]
|
||||
|
||||
|
||||
def close(a, b, eps=1e-9):
|
||||
return all(abs(x-y) <= eps for x, y in zip(a, b))
|
||||
|
||||
|
||||
UP = (0.0, 1.0, 0.0)
|
||||
DOWN = (0.0, -1.0, 0.0)
|
||||
for angle in (0, 20, 90, 180):
|
||||
q = q_axis((1.0, 0.0, 0.0), angle)
|
||||
world_up = rotate(q, UP)
|
||||
world_gravity = rotate(q, DOWN)
|
||||
assert close(rotate(q_conj(q), world_up), UP)
|
||||
assert close(rotate(q_conj(q), world_gravity), DOWN)
|
||||
assert close(tuple(-x for x in world_up), world_gravity)
|
||||
print(f"X {angle:3d}: up={world_up!r}, gravity={world_gravity!r}")
|
||||
|
||||
q180 = q_axis((1.0, 0.0, 0.0), 180)
|
||||
assert close(rotate(q180, UP), (0.0, -1.0, 0.0))
|
||||
assert close(rotate(q180, DOWN), (0.0, 1.0, 0.0))
|
||||
print("OK: at 180 degrees jump points world-down and gravity points world-up")
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Checks that limb animation uses ship-local displacement, not world X/Z."""
|
||||
import math
|
||||
|
||||
|
||||
def qx(deg):
|
||||
a=math.radians(deg)/2
|
||||
return (math.cos(a),math.sin(a),0.0,0.0)
|
||||
|
||||
def conj(q):
|
||||
w,x,y,z=q; return (w,-x,-y,-z)
|
||||
|
||||
def mul(a,b):
|
||||
aw,ax,ay,az=a; bw,bx,by,bz=b
|
||||
return (aw*bw-ax*bx-ay*by-az*bz,
|
||||
aw*bx+ax*bw+ay*bz-az*by,
|
||||
aw*by-ax*bz+ay*bw+az*bx,
|
||||
aw*bz+ax*by-ay*bx+az*bw)
|
||||
|
||||
def rotate(q,v):
|
||||
r=mul(mul(q,(0.0,*v)),conj(q)); return r[1:]
|
||||
|
||||
def length_xz(v): return math.sqrt(v[0]*v[0]+v[2]*v[2])
|
||||
def length_local(v): return math.sqrt(v[0]*v[0]+v[2]*v[2])
|
||||
|
||||
walk=(0.0,0.0,0.25)
|
||||
for deg in (0,20,90,180):
|
||||
world=rotate(qx(deg),walk)
|
||||
recovered=rotate(conj(qx(deg)),world)
|
||||
assert abs(length_local(recovered)-0.25)<1e-9
|
||||
print(f"X {deg:3d}: world delta={world}, local walk distance={length_local(recovered):.3f}, vanilla world-XZ={length_xz(world):.3f}")
|
||||
|
||||
# At 90 degrees around X, forward walking is world-vertical: vanilla X/Z animation would be 0.
|
||||
assert length_xz(rotate(qx(90),walk)) < 1e-9
|
||||
print('OK: local animation still walks at 90 degrees while vanilla world-XZ would freeze')
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression check for the test9/test10 fake-impulse bug."""
|
||||
import math
|
||||
|
||||
|
||||
def qx(deg):
|
||||
a=math.radians(deg)/2
|
||||
return (math.cos(a),math.sin(a),0.0,0.0)
|
||||
|
||||
def conj(q):
|
||||
w,x,y,z=q; return (w,-x,-y,-z)
|
||||
|
||||
def mul(a,b):
|
||||
aw,ax,ay,az=a; bw,bx,by,bz=b
|
||||
return (aw*bw-ax*bx-ay*by-az*bz,
|
||||
aw*bx+ax*bw+ay*bz-az*by,
|
||||
aw*by-ax*bz+ay*bw+az*bx,
|
||||
aw*bz+ax*by-ay*bx+az*bw)
|
||||
|
||||
def rotate(q,v):
|
||||
r=mul(mul(q,(0.0,*v)),conj(q)); return r[1:]
|
||||
|
||||
def invrotate(q,v): return rotate(conj(q),v)
|
||||
def close(a,b,e=1e-9): return all(abs(x-y)<e for x,y in zip(a,b))
|
||||
|
||||
local=(0.17,0.42,-0.08)
|
||||
for deg in (0,20,45,90,180):
|
||||
world=rotate(qx(deg),local)
|
||||
recovered=invrotate(qx(deg),world)
|
||||
assert close(recovered,local)
|
||||
print(f"X {deg:3d}: local={local} -> world={world} -> local={recovered}")
|
||||
print('OK: changing ship orientation changes world representation without changing local velocity')
|
||||
Reference in New Issue
Block a user