def test_fastround(): assert fastround(1.1234543) == 1.0 assert fastround(1.1234543, 0) == 1.0 assert fastround(1.5, 0) == 2.0 assert fastround(1.1234543, 2) == 1.12 assert fastround(1.1234543, 3) == 1.123 assert fastround(1.1234543, 4) == 1.1235 assert fastround(1.1234543, 5) == 1.12345
def shots_to_kill_ranges( self, damage_target_type: DamageTargetType = DamageTargetType. INFANTRY_BASELINE, damage_location: DamageLocation = DamageLocation.TORSO, step: float = 0.1, precision_decimals: int = 2, ) -> Iterator[Tuple[float, int]]: if self.damage_range_delta > 0: previous_stk: Optional[int] = None for r in float_range(0.0, self.min_damage_range + step, step, precision_decimals): stk: int = self.shots_to_kill( distance=r, damage_target_type=damage_target_type, damage_location=damage_location, ) if previous_stk is None or stk != previous_stk: if r >= step: yield (fastround(r - step, precision_decimals), stk) else: yield (r, stk) previous_stk = stk else: yield ( 0.0, self.shots_to_kill( distance=self.max_damage_range, damage_target_type=damage_target_type, damage_location=damage_location, ), )
def generate_magdump_simulation( fire_group: FireGroup, runs: int = 1, control_time: int = 0, auto_burst_length: Optional[int] = None, recoil_compensation: bool = False, player_state: PlayerState = PlayerState.STANDING, width: Optional[int] = None, height: Optional[int] = None, ) -> Tuple[Optional[altair.HConcatChart], Dict[int, altair.HConcatChart]]: assert (width or height) and not (width and height) datapoints: List[dict] fire_modes_datapoints: Dict[int, List[dict]] = {} for fire_mode in fire_group.fire_modes: if fire_mode.max_consecutive_shots > 0: fire_mode_id: int = fire_mode.fire_mode_id datapoints = [] simulation: Iterator[Tuple[int, Tuple[float, float], List[Tuple[float, float]], float, Tuple[float, float], Tuple[float, float], ]] for simulation in (fire_mode.simulate_shots( shots=fire_mode.max_consecutive_shots, control_time=control_time, auto_burst_length=auto_burst_length, recoil_compensation=recoil_compensation, player_state=player_state, ) for _ in range(runs)): t: int cursor_coor: Tuple[float, float] pellets_coors: List[Tuple[float, float]] _cof: float _vertical_recoil: Tuple[float, float] _horizontal_recoil: Tuple[float, float] for ( t, cursor_coor, pellets_coors, _cof, _vertical_recoil, _horizontal_recoil, ) in simulation: cursor_x, cursor_y = cursor_coor cursor_x = fastround(cursor_x, PRECISION_DECIMALS) cursor_y = fastround(cursor_y, PRECISION_DECIMALS) datapoints.append({ "firemode": f"{fire_mode_type_resolver[fire_mode.fire_mode_type]} {'ADS' if fire_mode.is_ads else 'Hipfire'} ({fire_mode.fire_mode_id})", "time": t, "type": CURSOR, X: cursor_x, Y: cursor_y, }) for pellet_x, pellet_y in pellets_coors: pellet_x = fastround(pellet_x, PRECISION_DECIMALS) pellet_y = fastround(pellet_y, PRECISION_DECIMALS) datapoints.append({ "firemode": f"{fire_mode_type_resolver[fire_mode.fire_mode_type]} {'ADS' if fire_mode.is_ads else 'Hipfire'} ({fire_mode.fire_mode_id})", "time": t, "type": PELLET, X: pellet_x, Y: pellet_y, }) fire_modes_datapoints[fire_mode_id] = datapoints if not fire_modes_datapoints: return (None, {}) # Generate charts for fire group and individual fire groups fire_modes_charts: Dict[int, altair.HConcatChart] = {} # Fire modes for fire_mode_id, datapoints in fire_modes_datapoints.items(): chart_height: int chart_width: int min_x: float = min((d[X] for d in datapoints)) max_x: float = max((d[X] for d in datapoints)) min_y: float = min((d[Y] for d in datapoints)) max_y: float = max((d[Y] for d in datapoints)) if height: chart_height = height if max_y != min_y: chart_width = int( math.ceil((max_x - min_x) * height / (max_y - min_y))) else: chart_width = height elif width: chart_width = width if max_x != min_x: chart_height = int( math.ceil((max_y - min_y) * width / (max_x - min_x))) else: chart_height = width total_shots: int = len( list(filter(lambda x: x["type"] == CURSOR, datapoints))) total_pellets: int = len( list(filter(lambda x: x["type"] == PELLET, datapoints))) dataset: altair.Data = altair.Data(values=datapoints) chart: altair.Chart = (altair.Chart(dataset).mark_point().encode( x=altair.X( f"{X}:Q", axis=altair.Axis(title="horizontal angle (degrees)"), scale=altair.Scale(domain=(min_x, max_x)), ), y=altair.Y( f"{Y}:Q", axis=altair.Axis(title="vertical angle (degrees)"), scale=altair.Scale(domain=(min_y, max_y)), ), color=SIMULATION_POINT_TYPE_COLOR, opacity=SIMULATION_POINT_TYPE_OPACITY, tooltip=["time:Q", f"{X}:Q", f"{Y}:Q"], ).properties( width=chart_width, height=chart_height, title= f"{runs} magdumps, {total_shots} shots, {total_pellets} pellets", ).interactive()) legend: altair.Chart = (altair.Chart(dataset).mark_point().encode( y=altair.Y("type:N", axis=altair.Axis(orient="right")), color=SIMULATION_POINT_TYPE_COLOR, ).add_selection(SIMULATION_POINT_TYPE_SELECTION)) fire_modes_charts[fire_mode_id] = altair.hconcat(chart, legend) # Fire group all_datapoints: List[dict] = list( itertools.chain.from_iterable(fire_modes_datapoints.values())) fg_chart_height: int fg_chart_width: int fg_min_x: float = min((d[X] for d in all_datapoints)) fg_max_x: float = max((d[X] for d in all_datapoints)) fg_min_y: float = min((d[Y] for d in all_datapoints)) fg_max_y: float = max((d[Y] for d in all_datapoints)) if height: fg_chart_height = height if fg_max_y != fg_min_y: fg_chart_width = int( math.ceil( (fg_max_x - fg_min_x) * height / (fg_max_y - fg_min_y))) else: fg_chart_width = 0 elif width: fg_chart_width = width if fg_max_x != fg_min_x: fg_chart_height = int( math.ceil( (fg_max_y - fg_min_y) * width / (fg_max_x - fg_min_x))) else: fg_chart_height = 0 all_total_shots: int = len( list(filter(lambda x: x["type"] == CURSOR, all_datapoints))) // len(fire_modes_datapoints) all_total_pellets: int = len( list(filter(lambda x: x["type"] == PELLET, all_datapoints))) // len(fire_modes_datapoints) all_datapoints_pellets_only: List[dict] = list( filter(lambda x: x["type"] == PELLET, all_datapoints)) fg_dataset: altair.Data = altair.Data(values=all_datapoints_pellets_only) fg_chart: altair.Chart = (altair.Chart(fg_dataset).mark_point().encode( x=altair.X( f"{X}:Q", axis=altair.Axis(title="horizontal angle (degrees)"), scale=altair.Scale(domain=(fg_min_x, fg_max_x)), ), y=altair.Y( f"{Y}:Q", axis=altair.Axis(title="vertical angle (degrees)"), scale=altair.Scale(domain=(fg_min_y, fg_max_y)), ), color=SIMULATION_FIRE_MODE_COLOR, opacity=SIMULATION_FIRE_MODE_OPACITY, tooltip=["time:Q", f"{X}:Q", f"{Y}:Q"], ).properties( width=fg_chart_width, height=fg_chart_height, title= f"{runs} magdumps, {all_total_shots} shots, {all_total_pellets} pellets", ).interactive()) fg_legend: altair.Chart = (altair.Chart(fg_dataset).mark_point().encode( y=altair.Y("firemode:N", axis=altair.Axis(orient="right")), color=SIMULATION_FIRE_MODE_COLOR, ).add_selection(SIMULATION_FIRE_MODE_SELECTION)) return (altair.hconcat(fg_chart, fg_legend), fire_modes_charts)
def parse_fire_group_data( fg: dict, ammo_clip_size: int, ammo_total_capacity: int, heat_overheat_penalty_time: int, heat_bleed_off_rate: int, ) -> FireGroup: fg_id: int = int(fg["fire_group_id"]) if fg_id in FIRE_GROUP_DATA_FIXERS: FIRE_GROUP_DATA_FIXERS[fg_id](fg) try: # Fire modes fire_modes: List[FireMode] = [] _fm: dict for _fm in sorted( fg["fire_group_to_fire_modes"], key=lambda x: get(x, "fire_mode_id", int), ): fm: dict = _fm["fire_mode"] fm_to_pr: Optional[dict] = fm.get("fire_mode_to_projectile") pr: Optional[dict] = None if fm_to_pr is not None: pr = fm["fire_mode_to_projectile"]["projectile"] # Player states player_state_cone_of_fire: Dict[PlayerState, ConeOfFire] = {} player_state_can_ads: Dict[PlayerState, bool] = {} ps: dict for ps in sorted( fm["player_state_groups"], key=lambda x: get(x, "player_state_id", int), ): player_state_cone_of_fire[ PlayerState(get(ps, "player_state_id", int)) ] = ConeOfFire( max_angle=optget(ps, "cof_max", float, 0.0), min_angle=optget(ps, "cof_min", float, 0.0), bloom=optget(fm, "cof_recoil", float, 0.0), recovery_rate=optget(ps, "cof_recovery_rate", float, 0.0), recovery_delay=optget(ps, "cof_recovery_delay_ms", int, 0), multiplier=get(fm, "cof_scalar", float), moving_multiplier=get(fm, "cof_scalar_moving", float), pellet_spread=optget(fm, "cof_pellet_spread", float, 0.0), grow_rate=optget(ps, "cof_grow_rate", float), ) player_state_can_ads[PlayerState(get(ps, "player_state_id", int))] = ( get(ps, "can_iron_sight", int) == 1 ) # Direct damage direct_damage_profile: Optional[DamageProfile] = None if "max_damage" in fm: # Effect damage_direct_effect: Dict[str, str] = {} effect = fm["damage_direct_effect"] damage_direct_effect = {} damage_direct_effect["resist_type"] = ResistType( get(effect, "resist_type_id", int) ) damage_direct_effect["target_type"] = ( TargetType(get(effect, "target_type_id", int)) if "target_type_id" in effect else None ) direct_effect_type: dict = effect["effect_type"] damage_direct_effect["action"] = direct_effect_type["description"] for k, v in direct_effect_type.items(): if k.startswith("string") or k.startswith("param"): if k in effect: damage_direct_effect[v] = effect[k] # Profile direct_damage_profile = DamageProfile( max_damage=get(fm, "max_damage", int), max_damage_range=optget(fm, "max_damage_range", float, 0.0), min_damage=optget( fm, "min_damage", int, get(fm, "max_damage", int) ), min_damage_range=optget(fm, "min_damage_range", float, 0.0), pellets_count=optget(fm, "fire_pellets_per_shot", int, 1), resist_type=damage_direct_effect["resist_type"], location_multiplier={ DamageLocation.HEAD: fastround( 1.0 + optget(fm, "damage_head_multiplier", float, 0.0), 4 ), DamageLocation.LEGS: fastround( 1.0 + optget(fm, "damage_legs_multiplier", float, 0.0), 4 ), }, effect=damage_direct_effect or {}, ) if ( direct_damage_profile.min_damage == 0 and direct_damage_profile.min_damage_range == 0.0 ): direct_damage_profile.min_damage = direct_damage_profile.max_damage direct_damage_profile.min_damage_range = ( direct_damage_profile.max_damage_range ) # Indirect damage indirect_damage_profile: Optional[DamageProfile] = None if "max_damage_ind" in fm: # Effect damage_indirect_effect: Dict[str, str] = {} effect = fm["damage_indirect_effect"] damage_indirect_effect = {} damage_indirect_effect["resist_type"] = ResistType( get(effect, "resist_type_id", int) ) damage_indirect_effect["target_type"] = ( TargetType(get(effect, "target_type_id", int)) if "target_type_id" in effect else None ) indirect_effect_type: dict = effect["effect_type"] damage_indirect_effect["action"] = indirect_effect_type["description"] for k, v in indirect_effect_type.items(): if k.startswith("string") or k.startswith("param"): if k in effect: damage_indirect_effect[v] = effect[k] # Profile indirect_damage_profile = DamageProfile( max_damage=get(fm, "max_damage_ind", int), max_damage_range=get(fm, "max_damage_ind_radius", float), min_damage=get(fm, "min_damage_ind", int), min_damage_range=get(fm, "min_damage_ind_radius", float), resist_type=damage_indirect_effect["resist_type"], pellets_count=optget(fm, "fire_pellets_per_shot", int, 1), effect=damage_indirect_effect or {}, ) ammo: Optional[Ammo] = ( Ammo( ammo_per_shot=get(fm, "fire_ammo_per_shot", int), block_auto=optget(fm, "reload_block_auto", lambda x: int(x) == 1), continuous=optget(fm, "reload_continuous", lambda x: int(x) == 1), clip_size=ammo_clip_size, total_capacity=ammo_total_capacity, short_reload_time=get(fm, "reload_time_ms", int), reload_chamber_time=optget(fm, "reload_chamber_ms", int, 0), loop_start_time=optget(fm, "reload_loop_start_ms", int), loop_end_time=optget(fm, "reload_loop_end_ms", int), ) if optget(fm, "fire_ammo_per_shot", int, 0) > 0 else None ) heat: Optional[Heat] = ( Heat( total_capacity=get(fm, "heat_threshold", int), heat_per_shot=get(fm, "heat_per_shot", int), overheat_penalty_time=heat_overheat_penalty_time, recovery_delay=optget(fm, "heat_recovery_delay_ms", int, 0), recovery_rate=heat_bleed_off_rate, ) if optget(fm, "heat_per_shot", int, 0) > 0 else None ) fire_timing: FireTiming = FireTiming( is_automatic=optget(fm, "automatic", lambda x: int(x) == 1, False), refire_time=optget(fm, "fire_refire_ms", int, 0), fire_duration=optget(fm, "fire_duration_ms", int, 0), burst_length=optget(fm, "fire_burst_count", int), burst_refire_time=optget(fm, "fire_auto_fire_ms", int), delay=optget(fm, "fire_delay_ms", int, 0), charge_up_time=optget(fm, "fire_charge_up_ms", int, 0), spool_up_time=optget(fg, "spool_up_ms", int), spool_up_initial_refire_time=optget( fg, "spool_up_initial_refire_ms", int ), chamber_time=optget(fg, "chamber_duration_ms", int), ) recoil: Recoil = Recoil( max_angle=optget(fm, "recoil_angle_max", float, 0.0), min_angle=optget(fm, "recoil_angle_min", float, 0.0), max_vertical=optget(fm, "recoil_magnitude_max", float, 0.0), min_vertical=optget(fm, "recoil_magnitude_min", float, 0.0), vertical_increase=optget(fm, "recoil_increase", float, 0.0), vertical_crouched_increase=optget( fm, "recoil_increase_crouched", float, 0.0 ), max_horizontal=optget(fm, "recoil_horizontal_max", float, 0.0), min_horizontal=optget(fm, "recoil_horizontal_min", float, 0.0), horizontal_tolerance=optget(fm, "recoil_horizontal_tolerance", float), max_horizontal_increase=optget( fm, "recoil_horizontal_max_increase", float, 0.0 ), min_horizontal_increase=optget( fm, "recoil_horizontal_min_increase", float, 0.0 ), recovery_acceleration=optget( fm, "recoil_recovery_acceleration", float, 0.0 ), recovery_delay=optget(fm, "recoil_recovery_delay_ms", int, 0), recovery_rate=optget(fm, "recoil_recovery_rate", float, 0.0), first_shot_multiplier=optget( fm, "recoil_first_shot_modifier", float, 1.0 ), shots_at_min_magnitude=optget(fm, "recoil_shots_at_min_magnitude", int), max_total_magnitude=optget(fm, "recoil_max_total_magnitude", float), ) projectile: Optional[Projectile] = ( Projectile( turn_rate=optget(pr, "turn_rate", float), speed=optget(fm, "projectile_speed_override", float) or get(pr, "speed", float), max_speed=optget(pr, "speed_max", float), acceleration=optget(pr, "acceleration", float, 0.0), flight_type=ProjectileFlightType( get(pr, "projectile_flight_type_id", int) ), gravity=optget(pr, "gravity", float, 0.0), life_time=get(pr, "lifespan", lambda x: int(1_000 * float(x))), drag=optget(pr, "drag", float, 0.0), ) if pr is not None else None ) lock_on: Optional[LockOn] = ( LockOn( turn_rate=optget(pr, "turn_rate", float), life_time=get( pr, "lockon_lifespan", lambda x: int(1_000 * float(x)) ), seek_in_flight=optget( pr, "lockon_seek_in_flight", lambda x: int(x) == 1, False ), maintain=optget( fm, "lockon_maintain", lambda x: int(x) == 1, False ), required=optget( fm, "lockon_required", lambda x: int(x) == 1, False ), ) if pr is not None and ( optget(fm, "lockon_angle", float, None) or optget(pr, "lockon_lose_angle", float, None) or optget(fm, "lockon_acquire_ms", int, None) or optget(fm, "lockon_acquire_close_ms", int, None) or optget(fm, "lockon_acquire_far_ms", int, None) or optget(fm, "lockon_range", float, None) or optget(fm, "lockon_range_close", float, None) or optget(fm, "lockon_range_far", float, None) or optget(fm, "lockon_maintain", lambda x: int(x) == 1, None) or optget(fm, "lockon_required", lambda x: int(x) == 1, None) or optget(pr, "lockon_seek_in_flight", lambda x: int(x) == 1, None) ) else None ) # Fire Mode fire_mode: FireMode = FireMode( fire_mode_id=get(fm, "fire_mode_id", int), fire_mode_type=FireModeType(get(fm, "fire_mode_type_id", int)), description=fm["description"]["en"] if "description" in fm else "", is_ads=optget(fm, "iron_sights", lambda x: int(x) == 1, False), detect_range=optget(fm, "fire_detect_range", float, 0.0), turn_multiplier=optget(fm, "turn_modifier", float, 1.0), move_multiplier=optget(fm, "move_modifier", float, 1.0), direct_damage_profile=direct_damage_profile, indirect_damage_profile=indirect_damage_profile, zoom=optget(fm, "zoom_default", float, 1.0), sway_can_steady=optget(fm, "sway_can_steady", lambda x: int(x) == 1), sway_amplitude_x=optget(fm, "sway_amplitude_x", float), sway_amplitude_y=optget(fm, "sway_amplitude_y", float), sway_period_x=optget(fm, "sway_period_x", float), sway_period_y=optget(fm, "sway_period_y", float), ammo=ammo, heat=heat, fire_timing=fire_timing, recoil=recoil, projectile=projectile, lock_on=lock_on, player_state_cone_of_fire=player_state_cone_of_fire, player_state_can_ads=player_state_can_ads, ) fire_modes.append(fire_mode) fire_modes_descriptions: Set[str] = set( # type: ignore [ f.description if f.description not in {"Placeholder", ""} else None for f in fire_modes ] ) - {None} fg_description: str = "" if fire_modes_descriptions: fg_description = " / ".join(list(fire_modes_descriptions)) fire_group: FireGroup = FireGroup( fire_group_id=fg_id, description=fg_description, transition_time=optget(fg, "transition_duration_ms", int, 0), fire_modes=fire_modes, )
def simulate_shots( self, shots: int = -1, control_time: int = 0, auto_burst_length: Optional[int] = None, player_state: PlayerState = PlayerState.STANDING, recoil_compensation: bool = False, recoil_compensation_accuracy: float = 0.0, precision_decimals: int = 6, ) -> Iterator[Tuple[ int, # time Tuple[float, float], # cursor position List[Tuple[float, float]], # pellets positions float, # current CoF Tuple[float, float], # current min and max vertical recoil Tuple[float, float], # current min and max horizontal recoil ], ]: if shots == 0: yield (0, (0, 0), [], 0, (0, 0), (0, 0)) return # Cone of fire at player state cof: ConeOfFire = self.player_state_cone_of_fire[player_state] # Current state # Position; start at origin curr_x = 0.0 curr_y = 0.0 # Recoil parameters curr_max_vertical_recoil: float = self.recoil.max_vertical curr_min_vertical_recoil: float = self.recoil.min_vertical curr_max_horizontal_recoil: float = self.recoil.max_horizontal curr_min_horizontal_recoil: float = self.recoil.min_horizontal # CoF parameters curr_cof_angle: float = cof.min_cof_angle() # Loop previous_t: int = 0 t: int b: bool for t, b in self.generate_real_shot_timings( shots=shots, control_time=control_time, auto_burst_length=auto_burst_length): delta: int = t - previous_t # Scaling and recoveries ############################################################################ # After first shot, apply scaling and recoveries if t > 0: # CoF ######################################################################## cof_recovery_delay: int = self.fire_timing.refire_time + cof.recovery_delay # Under recovery delay -- bloom CoF if delta <= cof_recovery_delay: curr_cof_angle = cof.apply_bloom(current=curr_cof_angle) # Above recovery delay -- recover CoF else: curr_cof_angle = cof.recover( current=curr_cof_angle, time=delta - cof_recovery_delay, ) # Recoil ######################################################################## recoil_recovery_delay: int = self.fire_timing.refire_time + self.recoil.recovery_delay # Under recovery delay -- scale recoil if delta <= recoil_recovery_delay: # Vertical ( curr_min_vertical_recoil, curr_max_vertical_recoil, ) = self.recoil.scale_vertical( current_min=curr_min_vertical_recoil, current_max=curr_max_vertical_recoil, ) # Horizontal ( curr_min_horizontal_recoil, curr_max_horizontal_recoil, ) = self.recoil.scale_horizontal( current_min=curr_min_horizontal_recoil, current_max=curr_max_horizontal_recoil, ) # Above recovery delay and have a recovery rate -- recover recoil else: curr_x, curr_y = self.recoil.recover( current_x=curr_x, current_y=curr_y, time=delta - recoil_recovery_delay, ) # Recoil compensation ############################################################################ if recoil_compensation is True: if curr_y != 0.0: recenter_a: float if self.recoil.min_angle == self.recoil.max_angle: recenter_a = self.recoil.min_angle else: recenter_a = (self.recoil.min_angle + self.recoil.max_angle) / 2 if recenter_a != 0.0: curr_x -= curr_y / (cached_tan( math.radians(90 - recenter_a))) if recoil_compensation_accuracy > 0.0: curr_x += random.uniform( -recoil_compensation_accuracy, recoil_compensation_accuracy, ) curr_y = 0.0 if recoil_compensation_accuracy > 0.0: curr_y += random.uniform(-recoil_compensation_accuracy, recoil_compensation_accuracy) # Current result ############################################################################ # Result as a tuple of time, cursor position tuple and pellets positions tuples curr_result: Tuple[int, Tuple[float, float], List[Tuple[float, float]], float, Tuple[float, float], Tuple[float, float], ] = ( t, # time (curr_x, curr_y), # cursor [], # pellets curr_cof_angle, # current cof angle ( curr_min_vertical_recoil, curr_max_vertical_recoil, ), # current vertical recoil ( curr_min_horizontal_recoil, curr_max_horizontal_recoil, ), # current horizontal recoil ) # CoF simulation ############################################################################ cof_h: float cof_v: float if curr_cof_angle == 0.0: cof_h = 0.0 cof_v = 0.0 else: cof_h, cof_v = random_point_in_disk(radius=curr_cof_angle) # Individual pellets position for _ in range(self.direct_damage_profile.pellets_count if self. direct_damage_profile is not None else self. indirect_damage_profile.pellets_count if self. indirect_damage_profile is not None else 1): pellet_h: float pellet_v: float if cof.pellet_spread: pellet_h, pellet_v = random_point_in_disk( radius=cof.pellet_spread) curr_result[2].append(( fastround(curr_x + cof_h + pellet_h, precision_decimals), fastround(curr_y + cof_v + pellet_v, precision_decimals), )) else: curr_result[2].append(( fastround(curr_x + cof_h, precision_decimals), fastround(curr_y + cof_v, precision_decimals), )) # Recoil simulation ############################################################################ # Un-angled vertical recoil amplitude recoil_v: float if curr_max_vertical_recoil == curr_min_vertical_recoil: recoil_v = curr_max_vertical_recoil else: recoil_v = random.uniform(curr_min_vertical_recoil, curr_max_vertical_recoil) # FSM scaling of un-angled vertical recoil if b is True: recoil_v = recoil_v * self.recoil.first_shot_multiplier # Un-angled horizontal recoil amplitude recoil_h: float if curr_max_horizontal_recoil == curr_min_horizontal_recoil: recoil_h = curr_max_horizontal_recoil else: recoil_h = random.uniform(curr_min_horizontal_recoil, curr_max_horizontal_recoil) # Recoil angle recoil_a: float if (self.recoil.max_angle, self.recoil.min_angle) == (0.0, 0.0): recoil_a = 0.0 elif self.recoil.max_angle == self.recoil.min_angle: recoil_a = self.recoil.max_angle else: recoil_a = random.uniform(self.recoil.min_angle, self.recoil.max_angle) # Horizontal recoil direction recoil_h_direction: Literal[-1, 1] recoil_h_choices: Tuple[Literal[-1, 1], Literal[-1, 1]] = (-1, 1) if self.recoil.half_horizontal_tolerance: rta: float = cached_tan(math.radians(90 - recoil_a)) left_bound: float = ( (curr_y - self.recoil.half_horizontal_tolerance) / rta ) if recoil_a != 0.0 else -self.recoil.half_horizontal_tolerance right_bound: float = ( (curr_y + self.recoil.half_horizontal_tolerance) / rta ) if recoil_a != 0.0 else self.recoil.half_horizontal_tolerance if left_bound <= curr_x <= right_bound: recoil_h_direction = random.choice(recoil_h_choices) else: if curr_x > right_bound: recoil_h_direction = -1 else: recoil_h_direction = 1 else: recoil_h_direction = random.choice(recoil_h_choices) recoil_h *= recoil_h_direction # Angle horizontal and vertical recoil recoil_h_angled: float recoil_v_angled: float if recoil_a == 0.0: recoil_h_angled = recoil_h recoil_v_angled = recoil_v else: recoil_a_radians: float = math.radians(recoil_a) sin_recoil_a_radians: float = math.sin(recoil_a_radians) cos_recoil_a_radians: float = math.cos(recoil_a_radians) recoil_h_angled = (recoil_h * cos_recoil_a_radians + recoil_v * sin_recoil_a_radians) recoil_v_angled = (-recoil_h * sin_recoil_a_radians + recoil_v * cos_recoil_a_radians) # Yield yield curr_result # Update current position curr_x = fastround(curr_x + recoil_h_angled, precision_decimals) curr_y = fastround(curr_y + recoil_v_angled, precision_decimals) previous_t = t
print(f"Generated {len(infantry_weapons)} infantry weapons") wp: InfantryWeapon = next(x for x in infantry_weapons if x.item_id == 43) fm: FireMode = wp.fire_groups[0].fire_modes[1] # Simulation without recoil compensation sim = fm.simulate_shots(shots=fm.max_consecutive_shots) datapoints: List[dict] = [] for t, cursor_coor, pellets_coors, _cof, _vertical_recoil, _horizontal_recoil in sim: cursor_x, cursor_y = cursor_coor cursor_x = fastround(cursor_x, PRECISION_DECIMALS) cursor_y = fastround(cursor_y, PRECISION_DECIMALS) datapoints.append({"time": t, "type": "cursor", "x": cursor_x, "y": cursor_y}) for pellet_x, pellet_y in pellets_coors: pellet_x = fastround(pellet_x, PRECISION_DECIMALS) pellet_y = fastround(pellet_y, PRECISION_DECIMALS) datapoints.append({"time": t, "type": "pellet", "x": pellet_x, "y": pellet_y}) dataset = altair.Data(values=datapoints) chart = ( altair.Chart(dataset)