Skip to content

Instantly share code, notes, and snippets.

@megamaz
Last active January 11, 2025 04:13
Show Gist options
  • Save megamaz/f0516495ba87eb6369ae1dbc6188c31f to your computer and use it in GitHub Desktop.
Save megamaz/f0516495ba87eb6369ae1dbc6188c31f to your computer and use it in GitHub Desktop.
Optimizes Height for Elytra Movement in Celeste
from math import sin, cos, pi, sqrt, atan2, acos
from enum import Enum
import time
clipboard = None
try:
clipboard = __import__("clipboard")
except ModuleNotFoundError:
print("Module `clipboard` not found.\nThe clipboard module would automatically copy the output of the script to your clipboard.\nTo install, run `pip install clipboard` in a cmd (this is not a requirement)")
DELTA_TIME = 0.016666699201 # the exact in-game value when running at 60fps
MAX_ANGLE_CHANGE_INV_SPEED_FACTOR = 480.0
MIN_SPEED = 64.0
MAX_SPEED = 320.0
DECEL = 165.0
FAST_DECEL = 220.0
ACCEL = 90.0
STABLE_ANGLE = 0.2
ANGLE_RANGE = 2.0
RAD_TO_DEG = 180.0 / pi
DEG_TO_RAD = pi / 180.0
STABLE_ANGLE_DEG = STABLE_ANGLE * RAD_TO_DEG
STABLE_ANGLE_DEG_LEFT = (-STABLE_ANGLE_DEG + 360) % 360
##############################################################
############ VARIABLES YOU WANNA MODIFY AS A USER ############
##############################################################
ELYTRA_HOTKEY = "PE" # change this if you're using PP or something else
PRECISION_OUT = 4 # how many digits of precision are printed to the console
PRECISION_COMPUTE = 12 # how many digits of precision are calculated (breaks down at 17 but you shouldn't need more than 12)
HOLD_JUMP = False # this can allow you to get more height
JUMP_KEY = "J"
STOP_VALUE = 0.001 # values below this number will NOT be added to output.
DO_WIGGLE = False # whether or not to wiggle. You'll get an error if your frames below aren't setup right.
WIGGLE_FRAMES_HORIZONTAL = 2 # these two variables control the wiggle. horizontal controls the amount of frames where we hold 90 / 270, and stabilizer is the amount of frames we hold up to change our direction.
WIGGLE_FRAMES_STABILIZER = 2 # if DO_WIGGLE is true, neither of these should be 0.
WIGGLE_OFFSET = 0 # how much of wiggle to skip. This allows you to start later into the wiggle cycle rather than always starting on pulling up.
##############################################################
############ VARIABLES YOU WANNA MODIFY AS A USER ############
##############################################################
class Facings(Enum):
Left = -1
Right = 1
def frange(min, max, step):
value = min
while value < max:
value += step
yield value
def Approach(value, target, step):
if value > target:
return max(value - step, target)
elif value < target:
return min(value + step, target)
return target
# "santize" output (combine frames with repeated inputs)
def sanitizer(output):
new_out = [output.splitlines()[0]]
for line in output.splitlines()[1:]:
# check if the angles are identical
angle = line.split(",")[-1]
if angle == new_out[-1].split(",")[-1]:
length = int(new_out[-1].split(",")[0].strip())
length += 1
new_out[-1] = f"{length:4}{f',{JUMP_KEY}' if HOLD_JUMP else ''},{ELYTRA_HOTKEY},F,{angle}"
else:
new_out.append(line)
return new_out
def optimizer(initial_angle, initial_speed, facing, frames=100, do_wiggle=False, wig_frames_horizontal=0, wig_frames_stabilizer=0, wig_offset=0) -> str:
output = ""
wiggling = False # I have some really silly variable names
# False means we're stabilizing, True means we're going horizontal
wiggle_countdown = wig_frames_stabilizer
# do the offset
for _ in range(wig_offset):
wiggle_countdown -= 1
if wiggle_countdown <= 0:
wiggling = not wiggling
if wiggling:
wiggle_countdown = wig_frames_horizontal
else:
wiggle_countdown = wig_frames_stabilizer
for f in range(frames):
maxAngleChange = DELTA_TIME * MAX_ANGLE_CHANGE_INV_SPEED_FACTOR / initial_speed
angle_min = 0
# if we're moving "down" but above the stable angle
# then we don't want to optimize for immediate height
if initial_angle > 90 and initial_angle < 90 + STABLE_ANGLE_DEG:
angle_min = (acos((STABLE_ANGLE - (((initial_angle - 90) * pi)/180.0))))*RAD_TO_DEG - (maxAngleChange * RAD_TO_DEG)
angle_max = 180
init_min_angle = angle_min
init_max_angle = angle_max
best_yspeed = float("inf") # best speed gotten
best_xspeed = float("inf")
best_total_speed = float("inf")
best_angleI = 0 # best angle INPUT
best_angleF = 0 # best angle FLIGHT
step_size = 1
iteration = 1
end_prematurely = False # flag to end immediately
while iteration < PRECISION_COMPUTE and not end_prematurely:
for angle in frange(angle_min, angle_max, step_size):
# we want to prioritize more horizontal angles since that means less speed loss
# this ensures that if a more vertical angle gives the same speed as a more horizontal one, the more horizontal one is picked
# less speed loss
if initial_angle < 180:
angle = angle_max - angle + angle_min
# if we're flying down then we want to optimize for long-term speed
if initial_angle > 90 + STABLE_ANGLE_DEG and initial_speed <= MAX_SPEED:
angle = 0
end_prematurely = True
# wiggle
if wiggling:
angle = 90
end_prematurely = True
# simulate in-game speed changes
halfrange = ANGLE_RANGE / 2.0
newSpeed = initial_speed
yInput = -sin((angle + 90) * DEG_TO_RAD)
target = ((STABLE_ANGLE + halfrange * yInput) * RAD_TO_DEG) + 90
newAngle = Approach(initial_angle, target, (maxAngleChange * RAD_TO_DEG))
if sin((newAngle - 90) * DEG_TO_RAD) < sin(STABLE_ANGLE):
decel = FAST_DECEL if initial_speed > MAX_SPEED else DECEL
newSpeed = Approach(initial_speed, MIN_SPEED, DELTA_TIME * decel * abs(yInput))
else:
if initial_speed < MAX_SPEED:
newSpeed = Approach(initial_speed, MAX_SPEED, DELTA_TIME * ACCEL * abs(yInput))
ySpeed = newSpeed * -sin((90 - newAngle) * DEG_TO_RAD)
# since y speed is negative when moving up
if ySpeed < best_yspeed:
best_yspeed = ySpeed
best_total_speed = newSpeed
best_angleI = angle
best_angleF = newAngle
if end_prematurely:
break
# once we've found our best angle for this frame, narrow down precision
angle_min = best_angleI - 10/(10**iteration)
angle_max = best_angleI + 10/(10**iteration)
# prevent angles from exiting the bounds of their initial values
angle_min = max(init_min_angle, angle_min)
best_angleI = max(init_min_angle, best_angleI)
angle_max = min(init_max_angle, angle_max)
best_angleI = min(init_max_angle, best_angleI)
step_size /= 10.0
iteration += 1
# update the values for the next frame
initial_angle = best_angleF
initial_speed = best_total_speed
angle_hold = best_angleI
if facing == Facings.Left:
angle_hold = ((-best_angleI)%360)
output += f" 1{f',{JUMP_KEY}' if HOLD_JUMP else ''},{ELYTRA_HOTKEY},F,{angle_hold:.{PRECISION_OUT}f}\n"
# Wiggling
if do_wiggle:
wiggle_countdown -= 1
if wiggle_countdown <= 0:
wiggling = not wiggling
if wiggling:
wiggle_countdown = wig_frames_horizontal
else:
wiggle_countdown = wig_frames_stabilizer
new_out = sanitizer(output)
# new_out.append(f"9999{f',{JUMP_KEY}' if HOLD_JUMP else ''},{ELYTRA_HOTKEY},F,{0.0:.{PRECISION_OUT}f}")
return "\n".join(new_out)
def main():
data = []
print("Elytra Optimizer - if you run into issues, ping megamaz.\nFor more control over the output, check the variables at the start of the program to make sure they fit what you want to do.\nThe comments should explain what each of them does.\n")
# Verify variables aren't setup wrong
if DO_WIGGLE:
assert WIGGLE_FRAMES_HORIZONTAL > 0 and WIGGLE_FRAMES_STABILIZER > 0, "One of your WIGGLE_FRAMES variables is at or below 0. Make sure they are at least 1. If you don't want to wiggle, then set DO_WIGGLE to False."
print("Paste CelesteTAS Info here (hit enter twice when done):\n")
while True:
info = input("")
if info != "":
data.append(info)
else:
break
speedIndex = [1 if x.startswith("Speed") else 0 for x in data].index(1)
speedString = data[speedIndex][len("Speed: "):]
speedX = float(speedString.split(", ")[0])
speedY = float(speedString.split(", ")[1])
facing = Facings.Left if speedX < 0 else Facings.Right # flip the x speed if facing left
initial_speed = sqrt((speedX**2) + (speedY**2))
initial_angle = (((atan2(speedY, speedX * facing.value) * RAD_TO_DEG) + 90) + 360) % 360
# (x+360)%360 to remap to range [0-360] instead of [-180, 180]
print(f"initial flight angle: {initial_angle}\ninitial flight speed: {initial_speed}\n\n")
start = time.time()
result = optimizer(initial_angle, initial_speed, facing, 100, DO_WIGGLE, WIGGLE_FRAMES_HORIZONTAL, WIGGLE_FRAMES_STABILIZER, WIGGLE_OFFSET)
print(result)
if clipboard:
clipboard.copy(result)
print("Output copied to clipboard.")
input(f"\nDone in {time.time() - start}s.\nHit enter to start new session.")
if __name__ == "__main__":
print("hit ctrl+c to end (KeyboardInterrupt)\n")
while True:
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment