diff --git a/RLBotPack/sideswipe/sideswipe.cfg b/RLBotPack/sideswipe/sideswipe.cfg new file mode 100644 index 00000000..f8eb71b1 --- /dev/null +++ b/RLBotPack/sideswipe/sideswipe.cfg @@ -0,0 +1,10 @@ +[Locations] +script_file = ./sideswipe.py +name = SideSwipe + +[Details] +developer = Eastvillage +description = Rocket League SideSwipe is Psyonix' mobile port of Rocket League. This script will emulate SideSwipe by forcing all players and the ball onto the YZ-plane. It is recommended to use the mutator Boost Amount: Recharge (fast) due to the lack of boost pads in the middle. Supports up to 5v5. +fun_fact = Actually better than the real SideSwipe +github https://github.com/NicEastvillage/SideSwipe +language = Python diff --git a/RLBotPack/sideswipe/sideswipe.py b/RLBotPack/sideswipe/sideswipe.py new file mode 100644 index 00000000..effcf60c --- /dev/null +++ b/RLBotPack/sideswipe/sideswipe.py @@ -0,0 +1,93 @@ +import math +import random +from time import sleep + +from rlbot.agents.base_script import BaseScript +from rlbot.utils.game_state_util import GameState, CarState, Physics, Vector3, BallState, Rotator + +from vec import Vec3 + + +class Tron(BaseScript): + def __init__(self): + super().__init__("SideSwipe") + self.is_kickoff = False + self.is_replay = True + self.last_score = (0, 0) + + def run(self): + while True: + packet = self.wait_game_tick_packet() + + ball_pos = Vec3(packet.game_ball.physics.location) + + car_states = {} + for index in range(packet.num_cars): + car = packet.game_cars[index] + pos = Vec3(car.physics.location) + vel = Vec3(car.physics.velocity) + new_pos = pos.yz() + new_vel = vel.yz() + car_state = CarState(Physics(location=new_pos.desired(), velocity=new_vel.desired())) + car_states[index] = car_state + + if ball_pos.x == 0 and ball_pos.y == 0 and 90 < ball_pos.z < 96 and packet.game_info.is_kickoff_pause: + # Kickoff + if not self.is_kickoff: + # It was not kickoff previous frame + + car_states = self.setup_kickoff(packet) + + self.is_kickoff = True + self.is_replay = False + else: + self.is_kickoff = False + + ball = packet.game_ball + pos = Vec3(ball.physics.location) + vel = Vec3(ball.physics.velocity) + + ball_state = BallState(Physics(location=pos.yz().desired(), velocity=vel.yz().desired())) + + game_state = GameState(cars=car_states, ball=ball_state) + if not self.is_replay: + self.set_game_state(game_state) + + new_score = (packet.teams[0].score, packet.teams[1].score) + if new_score != self.last_score: + self.is_replay = True + self.last_score = new_score + + def setup_kickoff(self, packet): + indexes = list(range(5)) + index_shuffle_table = {} + for i in range(5): + s = random.choice(indexes) + indexes.remove(s) + index_shuffle_table[i] = s + + next_blue = 0 + next_orange = 0 + car_states = {} + for index in range(packet.num_cars): + car = packet.game_cars[index] + pos_index = index_shuffle_table[next_blue] if car.team == 0 else index_shuffle_table[next_orange] + y = 5000 - pos_index * 500 + + if car.team == 0: + kickoff_pos = Vec3(0, -y, 25) + yaw = math.pi / 2.0 + next_blue += 1 + else: + kickoff_pos = Vec3(0, y, 25) + yaw = -math.pi / 2.0 + next_orange += 1 + + car_state = CarState(Physics(location=kickoff_pos.desired(), rotation=Rotator(yaw=yaw))) + car_states[index] = car_state + return car_states + + +if __name__ == "__main__": + script = Tron() + script.run() diff --git a/RLBotPack/sideswipe/vec.py b/RLBotPack/sideswipe/vec.py new file mode 100644 index 00000000..613cc589 --- /dev/null +++ b/RLBotPack/sideswipe/vec.py @@ -0,0 +1,71 @@ +import math +import random + +from rlbot.utils.game_state_util import Vector3 + + +class Vec3: + def __init__(self, x: float or 'Vec3'=0.0, y: float=0.0, z: float=0.0): + if hasattr(x, 'x'): + # We have been given a vector. Copy it + self.x = float(x.x) + self.y = float(x.y) if hasattr(x, 'y') else 0 + self.z = float(x.z) if hasattr(x, 'z') else 0 + else: + self.x = float(x) + self.y = float(y) + self.z = float(z) + + def __getitem__(self, item: int): + return (self.x, self.y, self.z)[item] + + def __add__(self, other: 'Vec3') -> 'Vec3': + return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) + + def __sub__(self, other: 'Vec3') -> 'Vec3': + return Vec3(self.x - other.x, self.y - other.y, self.z - other.z) + + def __neg__(self) -> 'Vec3': + return Vec3(-self.x, -self.y, -self.z) + + def __mul__(self, scale: float) -> 'Vec3': + return Vec3(self.x * scale, self.y * scale, self.z * scale) + + def __rmul__(self, scale: float) -> 'Vec3': + return self * scale + + def __truediv__(self, scale: float) -> 'Vec3': + scale = 1 / float(scale) + return self * scale + + def __str__(self): + return "Vec3(" + str(self.x) + ", " + str(self.y) + ", " + str(self.z) + ")" + + def dot(self, other: 'Vec3') -> float: + return self.x * other.x + self.y * other.y + self.z * other.z + + def mag2(self) -> float: + return self.dot(self) + + def mag(self) -> float: + return math.sqrt(self.mag2()) + + def longer_than(self, magnitude: float) -> bool: + return magnitude * magnitude < self.mag2() + + def unit(self) -> 'Vec3': + return self / self.mag() + + def yz(self): + return Vec3(0, self.y, self.z) + + def desired(self): + return Vector3(self.x, self.y, self.z) + + @staticmethod + def random() -> 'Vec3': + return Vec3( + 1 - 2 * random.random(), + 1 - 2 * random.random(), + 1 - 2 * random.random(), + ) diff --git a/RLBotPack/tron/orientation.py b/RLBotPack/tron/orientation.py new file mode 100644 index 00000000..bd883821 --- /dev/null +++ b/RLBotPack/tron/orientation.py @@ -0,0 +1,46 @@ +import math + +from vec import Vec3 + + +# This is a helper class for calculating directions relative to your car. You can extend it or delete if you want. +class Orientation: + """ + This class describes the orientation of an object from the rotation of the object. + Use this to find the direction of cars: forward, right, up. + It can also be used to find relative locations. + """ + + def __init__(self, rotation): + self.yaw = float(rotation.yaw) + self.roll = float(rotation.roll) + self.pitch = float(rotation.pitch) + + cr = math.cos(self.roll) + sr = math.sin(self.roll) + cp = math.cos(self.pitch) + sp = math.sin(self.pitch) + cy = math.cos(self.yaw) + sy = math.sin(self.yaw) + + self.forward = Vec3(cp * cy, cp * sy, sp) + self.right = Vec3(cy*sp*sr-cr*sy, sy*sp*sr+cr*cy, -cp*sr) + self.up = Vec3(-cr*cy*sp-sr*sy, -cr*sy*sp+sr*cy, cp*cr) + + +# Sometimes things are easier, when everything is seen from your point of view. +# This function lets you make any location the center of the world. +# For example, set center to your car's location and ori to your car's orientation, then the target will be +# relative to your car! +def relative_location(center: Vec3, ori: Orientation, target: Vec3) -> Vec3: + """ + Returns target as a relative location from center's point of view, using the given orientation. The components of + the returned vector describes: + * x: how far in front + * y: how far right + * z: how far above + """ + x = (target - center).dot(ori.forward) + y = (target - center).dot(ori.right) + z = (target - center).dot(ori.up) + return Vec3(x, y, z) \ No newline at end of file diff --git a/RLBotPack/tron/particle.py b/RLBotPack/tron/particle.py new file mode 100644 index 00000000..82629cd1 --- /dev/null +++ b/RLBotPack/tron/particle.py @@ -0,0 +1,27 @@ +import random +from dataclasses import dataclass +from typing import List, Tuple + +from vec import Vec3 + + +@dataclass +class Particle: + size: int + pos: Vec3 + vel: Vec3 + acc: Vec3 + drag: float + color: Tuple[int, int, int] + death_time: float + + def update(self): + self.vel = (1 - self.drag) * self.vel + 0.008333 * self.acc + self.pos = self.pos + 0.008333 * self.vel + + def render(self, renderer): + color = renderer.create_color(255, self.color[0], self.color[1], self.color[2]) + renderer.draw_rect_3d(self.pos, self.size, self.size, True, color) + if random.random() < 0.25: + spark = self.pos + 6 * Vec3.random() + renderer.draw_line_3d(self.pos, spark, color) diff --git a/RLBotPack/tron/requirements.txt b/RLBotPack/tron/requirements.txt new file mode 100644 index 00000000..aaea0713 --- /dev/null +++ b/RLBotPack/tron/requirements.txt @@ -0,0 +1 @@ +playsound \ No newline at end of file diff --git a/RLBotPack/tron/settings.py b/RLBotPack/tron/settings.py new file mode 100644 index 00000000..6df6e07e --- /dev/null +++ b/RLBotPack/tron/settings.py @@ -0,0 +1,7 @@ + +MAX_TRAIL_LENGTH = 200 +IGNORE_BOT_COLLISION = True +IGNORE_HUMAN_COLLISION = False +IGNORE_BALL_COLLISION = False +PLAY_SOUNDS = False +MIN_SOUND_INTERVAL = 0.15 diff --git a/RLBotPack/tron/sounds.py b/RLBotPack/tron/sounds.py new file mode 100644 index 00000000..dedb1a34 --- /dev/null +++ b/RLBotPack/tron/sounds.py @@ -0,0 +1,63 @@ +import random +from time import time +from dataclasses import dataclass +from pathlib import Path +from threading import Thread + +from playsound import playsound + +from settings import MIN_SOUND_INTERVAL, PLAY_SOUNDS + + +@dataclass +class TrailHitSound: + file: Path + strength_min: float + strength_max: float + + +class SoundPlayer: + def __init__(self): + path = Path(__file__).parent / "sounds" + self.trail_ball_hit_sounds = [ + TrailHitSound(path / "electro hum.wav", 100, 400), + TrailHitSound(path / "electro hum 2.wav", 100, 400), + TrailHitSound(path / "electro hum 3.wav", 300, 600), + TrailHitSound(path / "electro bounce light.wav", 500, 900), + TrailHitSound(path / "electro bounce light 2.wav", 400, 800), + TrailHitSound(path / "electro bounce medium.wav", 700, 1600), + TrailHitSound(path / "electro bounce heavy.wav", 1600, 3000), + ] + self.trail_car_hit_sounds = [ + TrailHitSound(path / "electro hum.wav", 100, 400), + TrailHitSound(path / "electro hum 2.wav", 100, 400), + TrailHitSound(path / "electro hum 3.wav", 300, 600), + TrailHitSound(path / "electro crash light.wav", 500, 900), + TrailHitSound(path / "electro crash light 2.wav", 400, 800), + TrailHitSound(path / "electro crash medium.wav", 700, 1600), + TrailHitSound(path / "electro crash heavy.wav", 1600, 3000), + ] + self.last_sound_time = time() + + def ball_hit(self, strength): + if not PLAY_SOUNDS: + return + sounds = list(filter(lambda s: s.strength_min <= strength <= s.strength_max, self.trail_ball_hit_sounds)) + if len(sounds) > 0 and self.last_sound_time < time() - MIN_SOUND_INTERVAL: + sound_file = random.choice(sounds).file + Thread(target=playsound, args=[str(sound_file)]).start() + self.last_sound_time = time() + + def car_hit(self, strength): + if not PLAY_SOUNDS: + return + sounds = list(filter(lambda s: s.strength_min <= strength <= s.strength_max, self.trail_car_hit_sounds)) + if len(sounds) > 0 and self.last_sound_time < time() - MIN_SOUND_INTERVAL: + sound_file = random.choice(sounds).file + Thread(target=playsound, args=[str(sound_file)]).start() + self.last_sound_time = time() + + +if __name__ == '__main__': + sounds = SoundPlayer() + sounds.ball_hit(400) diff --git a/RLBotPack/tron/sounds/electro bounce heavy.wav b/RLBotPack/tron/sounds/electro bounce heavy.wav new file mode 100644 index 00000000..cf1dfc75 Binary files /dev/null and b/RLBotPack/tron/sounds/electro bounce heavy.wav differ diff --git a/RLBotPack/tron/sounds/electro bounce light 2.wav b/RLBotPack/tron/sounds/electro bounce light 2.wav new file mode 100644 index 00000000..c9a74c30 Binary files /dev/null and b/RLBotPack/tron/sounds/electro bounce light 2.wav differ diff --git a/RLBotPack/tron/sounds/electro bounce light.wav b/RLBotPack/tron/sounds/electro bounce light.wav new file mode 100644 index 00000000..bb231985 Binary files /dev/null and b/RLBotPack/tron/sounds/electro bounce light.wav differ diff --git a/RLBotPack/tron/sounds/electro bounce medium.wav b/RLBotPack/tron/sounds/electro bounce medium.wav new file mode 100644 index 00000000..b06d0f2d Binary files /dev/null and b/RLBotPack/tron/sounds/electro bounce medium.wav differ diff --git a/RLBotPack/tron/sounds/electro crash heavy.wav b/RLBotPack/tron/sounds/electro crash heavy.wav new file mode 100644 index 00000000..0a2b6952 Binary files /dev/null and b/RLBotPack/tron/sounds/electro crash heavy.wav differ diff --git a/RLBotPack/tron/sounds/electro crash light 2.wav b/RLBotPack/tron/sounds/electro crash light 2.wav new file mode 100644 index 00000000..cef2f767 Binary files /dev/null and b/RLBotPack/tron/sounds/electro crash light 2.wav differ diff --git a/RLBotPack/tron/sounds/electro crash light.wav b/RLBotPack/tron/sounds/electro crash light.wav new file mode 100644 index 00000000..4d9bd20e Binary files /dev/null and b/RLBotPack/tron/sounds/electro crash light.wav differ diff --git a/RLBotPack/tron/sounds/electro crash medium.wav b/RLBotPack/tron/sounds/electro crash medium.wav new file mode 100644 index 00000000..60f5734c Binary files /dev/null and b/RLBotPack/tron/sounds/electro crash medium.wav differ diff --git a/RLBotPack/tron/sounds/electro hum 2.wav b/RLBotPack/tron/sounds/electro hum 2.wav new file mode 100644 index 00000000..501bc94f Binary files /dev/null and b/RLBotPack/tron/sounds/electro hum 2.wav differ diff --git a/RLBotPack/tron/sounds/electro hum 3.wav b/RLBotPack/tron/sounds/electro hum 3.wav new file mode 100644 index 00000000..2d76df8d Binary files /dev/null and b/RLBotPack/tron/sounds/electro hum 3.wav differ diff --git a/RLBotPack/tron/sounds/electro hum.wav b/RLBotPack/tron/sounds/electro hum.wav new file mode 100644 index 00000000..57af14eb Binary files /dev/null and b/RLBotPack/tron/sounds/electro hum.wav differ diff --git a/RLBotPack/tron/trail.py b/RLBotPack/tron/trail.py new file mode 100644 index 00000000..286bd864 --- /dev/null +++ b/RLBotPack/tron/trail.py @@ -0,0 +1,144 @@ +from dataclasses import dataclass + +from rlbot.utils.game_state_util import GameState, BallState, Physics, CarState + +from orientation import Orientation +from settings import IGNORE_BOT_COLLISION, IGNORE_HUMAN_COLLISION, IGNORE_BALL_COLLISION, MAX_TRAIL_LENGTH +from vec import Vec3 + + +class Trail: + def __init__(self, index, team): + self.index = index + self.team = team + self.points = [] + + self.duration = 13 + + self.segment_size = 115 + + def clear(self, renderer): + self.points = [] + renderer.begin_rendering(f"trail-{self.index}-top") + renderer.end_rendering() + renderer.begin_rendering(f"trail-{self.index}-mid") + renderer.end_rendering() + renderer.begin_rendering(f"trail-{self.index}-bottom") + renderer.end_rendering() + + def update(self, car, time): + ori = Orientation(car.physics.rotation) + pos = Vec3(car.physics.location) + Vec3(z=12) - 30 * ori.forward + if len(self.points) == 0: + # Initial point + point = TrailPoint(pos, time) + self.points.append(point) + else: + # Add points + prev = self.points[-1] + diff = pos - prev.pos + if diff.longer_than(self.segment_size): + point = TrailPoint(pos, time) + self.points.append(point) + + # Remove points + earliest = self.points[0] + if earliest.time + self.duration < time: + self.points = self.points[1:] + self.points = self.points[-MAX_TRAIL_LENGTH:] + + def do_collisions(self, script, packet): + ball_pos = Vec3(packet.game_ball.physics.location) + for i in range(len(self.points) - 2): + seg_start = self.points[i].pos + seg_end = self.points[i + 1].pos + seg = seg_end - seg_start + + # Ball + if not IGNORE_BALL_COLLISION: + ball_pos_from_seg_pov = ball_pos - seg_start + t = (ball_pos_from_seg_pov.dot(seg) / seg.dot(seg)) + ball_proj_seg = seg * t + seg_ball = (ball_pos_from_seg_pov - ball_proj_seg) + if 0 <= t <= 1 and not seg_ball.longer_than(100): + # Collision + seg_ball_u = seg_ball.unit() + vel = Vec3(packet.game_ball.physics.velocity) + refl_vel = vel - 1.9 * vel.dot(seg_ball_u) * seg_ball_u + ball_pos_moved = seg_start + ball_proj_seg + seg_ball_u * 101 + script.set_game_state(GameState(ball=BallState(physics=Physics( + location=ball_pos_moved.to_desired_vec(), + velocity=refl_vel.to_desired_vec()) + ))) + script.particle_burst( + packet.game_info.seconds_elapsed, + seg_start + ball_proj_seg + seg_ball_u * 10, + seg_ball_u, + int(1 + abs(vel.dot(seg_ball_u) / 300) ** 3), + self.team + ) + hit_strength = abs(seg_ball_u.dot(vel)) + script.sounds.ball_hit(hit_strength) + + # Cars + for car_index in range(packet.num_cars): + car = packet.game_cars[car_index] + if car.is_demolished \ + or (car.is_bot and IGNORE_BOT_COLLISION) \ + or (not car.is_bot and IGNORE_HUMAN_COLLISION): + continue + car_ori = Orientation(car.physics.rotation) + car_pos = Vec3(car.physics.location) + car_pos_from_seg_pov = car_pos - seg_start + t = (car_pos_from_seg_pov.dot(seg) / seg.dot(seg)) + car_proj_seg = seg * t + seg_car = (car_pos_from_seg_pov - car_proj_seg) + # seg_car_local = relative_location(Vec3(), car_ori, seg_car) + if 0 <= t <= 1 and not seg_car.longer_than(85): + # Collision + seg_car_u = seg_car.unit() + vel = Vec3(car.physics.velocity) + refl_vel = vel - 1.5 * vel.dot(seg_car_u) * seg_car_u + car_pos_moved = seg_start + car_proj_seg + seg_car_u * 86 + script.set_game_state(GameState(cars={car_index: CarState(physics=Physics( + location=car_pos_moved.to_desired_vec(), + velocity=refl_vel.to_desired_vec()) + )})) + script.particle_burst( + packet.game_info.seconds_elapsed, + seg_start + car_proj_seg + seg_car_u * 13, + seg_car_u, + int(1 + abs(vel.dot(seg_car_u) / 300) ** 3), + self.team + ) + hit_strength = abs(seg_car_u.dot(vel)) + script.sounds.car_hit(hit_strength) + + def render(self, renderer): + if len(self.points) > 1: + renderer.begin_rendering(f"trail-{self.index}-mid") + points = list(map(lambda p: p.pos, self.points)) + renderer.draw_polyline_3d(points, renderer.white()) + renderer.end_rendering() + + renderer.begin_rendering(f"trail-{self.index}-top") + blue = renderer.create_color(255, 0, 150, 255) + orange = renderer.orange() + points = list(map(lambda p: p.pos + Vec3(z=10), self.points)) + color = blue if self.team == 0 else orange + renderer.draw_polyline_3d(points, color) + renderer.end_rendering() + + renderer.begin_rendering(f"trail-{self.index}-bottom") + blue = renderer.create_color(255, 0, 150, 255) + orange = renderer.orange() + points = list(map(lambda p: p.pos + Vec3(z=-10), self.points)) + color = blue if self.team == 0 else orange + renderer.draw_polyline_3d(points, color) + renderer.end_rendering() + + +@dataclass +class TrailPoint: + pos: Vec3 + time: float \ No newline at end of file diff --git a/RLBotPack/tron/tron.cfg b/RLBotPack/tron/tron.cfg new file mode 100644 index 00000000..7c343300 --- /dev/null +++ b/RLBotPack/tron/tron.cfg @@ -0,0 +1,11 @@ +[Locations] +script_file = ./tron.py +name = Tron +requirements_file = ./requirements.txt + +[Details] +developer = Eastvillage +description = Tron game mode. Every car will leave a trail behind. By default, the ball and human players can collide with the trail (this among other things can be changed in settings.py). The script is not optimized and can be laggy with 3+ players. Debug rendering must be ON when using this script. +fun_fact = Reversing is not smart +github = https://github.com/NicEastvillage/Tron +language = Python diff --git a/RLBotPack/tron/tron.py b/RLBotPack/tron/tron.py new file mode 100644 index 00000000..907580fe --- /dev/null +++ b/RLBotPack/tron/tron.py @@ -0,0 +1,72 @@ +import random + +from rlbot.agents.base_script import BaseScript + +from particle import Particle +from sounds import SoundPlayer +from trail import Trail +from vec import Vec3 + + +class Tron(BaseScript): + def __init__(self): + super().__init__("Tron") + self.trails = [] + self.particles = [] + self.is_kickoff = False + self.sounds = SoundPlayer() + + def run(self): + while True: + packet = self.wait_game_tick_packet() + time = packet.game_info.seconds_elapsed + + ball_pos = Vec3(packet.game_ball.physics.location) + + if ball_pos.x == 0 and ball_pos.y == 0 and packet.game_info.is_kickoff_pause: + # Kickoff + if not self.is_kickoff: + # It was not kickoff previous frame + for trail in self.trails: + trail.clear(self.game_interface.renderer) + + self.is_kickoff = True + else: + self.is_kickoff = False + + # Update and render trails + for index in range(packet.num_cars): + car = packet.game_cars[index] + + if index >= len(self.trails): + self.trails.append(Trail(index, car.team)) + + trail = self.trails[index] + trail.update(car, time) + trail.do_collisions(self, packet) + trail.render(self.game_interface.renderer) + + # Particles + self.game_interface.renderer.begin_rendering("particles") + for particle in self.particles: + particle.update() + particle.render(self.game_interface.renderer) + self.game_interface.renderer.end_rendering() + self.particles = [particle for particle in self.particles if time < particle.death_time] + + def particle_burst(self, time: float, pos: Vec3, normal: Vec3, count: int, team: int): + color = (50, 180, 255) if team == 0 else (255, 168, 50) + self.particles.extend([Particle( + int(4 + 4 * random.random()), + pos, + 600 * (normal + 0.7 * Vec3.random()), + Vec3(z=-500), + 0.02, + color if random.random() < 0.55 else (255, 255, 255), # Some chance for white instead + time + 0.3 + 0.8 * random.random() + ) for _ in range(count)]) + + +if __name__ == "__main__": + script = Tron() + script.run() diff --git a/RLBotPack/tron/vec.py b/RLBotPack/tron/vec.py new file mode 100644 index 00000000..47e91c96 --- /dev/null +++ b/RLBotPack/tron/vec.py @@ -0,0 +1,68 @@ +import math +import random + +from rlbot.utils.game_state_util import Vector3 + + +class Vec3: + def __init__(self, x: float or 'Vec3'=0.0, y: float=0.0, z: float=0.0): + if hasattr(x, 'x'): + # We have been given a vector. Copy it + self.x = float(x.x) + self.y = float(x.y) if hasattr(x, 'y') else 0 + self.z = float(x.z) if hasattr(x, 'z') else 0 + else: + self.x = float(x) + self.y = float(y) + self.z = float(z) + + def __getitem__(self, item: int): + return (self.x, self.y, self.z)[item] + + def __add__(self, other: 'Vec3') -> 'Vec3': + return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) + + def __sub__(self, other: 'Vec3') -> 'Vec3': + return Vec3(self.x - other.x, self.y - other.y, self.z - other.z) + + def __neg__(self) -> 'Vec3': + return Vec3(-self.x, -self.y, -self.z) + + def __mul__(self, scale: float) -> 'Vec3': + return Vec3(self.x * scale, self.y * scale, self.z * scale) + + def __rmul__(self, scale: float) -> 'Vec3': + return self * scale + + def __truediv__(self, scale: float) -> 'Vec3': + scale = 1 / float(scale) + return self * scale + + def __str__(self): + return "Vec3(" + str(self.x) + ", " + str(self.y) + ", " + str(self.z) + ")" + + def dot(self, other: 'Vec3') -> float: + return self.x * other.x + self.y * other.y + self.z * other.z + + def mag2(self) -> float: + return self.dot(self) + + def mag(self) -> float: + return math.sqrt(self.mag2()) + + def longer_than(self, magnitude: float) -> bool: + return magnitude * magnitude < self.mag2() + + def unit(self) -> 'Vec3': + return self / self.mag() + + def to_desired_vec(self): + return Vector3(self.x, self.y, self.z) + + @staticmethod + def random() -> 'Vec3': + return Vec3( + 1 - 2 * random.random(), + 1 - 2 * random.random(), + 1 - 2 * random.random(), + )