use crate::entities::tee::jump::Jump;
use crate::entities::{SpawnOrder, SpawnableEntity};
use crate::ids::PlayerUid;
use crate::map::{coord, GameFrontIterator, Map, MapTile, SwitchTile, TeleTile, Tile, TuneZone};
use crate::state::{Bug, BugKind, GameState, Player, TeamKind};
use bitflags::bitflags;
use std::f32::consts::PI;
use std::fmt::Write as _;
use std::str::FromStr;
use std::{fmt, mem};
use twgame_core::twsnap::enums::{ActiveWeapon, Powerup as PickupKind};
use twgame_core::twsnap::enums::{Direction, Sound};
use twgame_core::twsnap::time::{Duration, Instant};
use twgame_core::twsnap::{self, AnglePrecision, PositionPrecision, Snap};
use twgame_core::{normalize, Input};
use uuid::Uuid;
use vek::num_traits::clamp;
use vek::Vec2;
mod core;
pub mod hook;
pub mod jump;
pub mod weapon;
use crate::{state, SnapOuter};
pub(crate) use core::TeeCore;
pub(crate) const TEE_PROXIMITY: f32 = 28.0;
const TEE_PROXIMITY_SQUARED: f32 = TEE_PROXIMITY * TEE_PROXIMITY;
impl SpawnableEntity for Tee {
fn tick(&mut self, now: Instant, game_state: &mut GameState, snap_outer: &mut SnapOuter) {
if self.state.contains(TeeState::PAUSED) {
return;
}
let core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
self.input = core.input_apply_freeze(
self.input.clone(),
self.state.contains(TeeState::LIVE_FREEZE),
);
self.handle_tune_layer(&game_state.map, core);
self.core_tick(now, game_state);
self.handle_weapons(now, game_state, snap_outer);
self.post_core_tick(now, game_state);
}
fn snap(&self, now: Instant, game_state: &GameState, snapshot: &mut Snap) {
let player = snapshot.players.get_mut_default(self.player_uid.snap_id());
let mut snap_tee = twsnap::items::Tee {
tick: now,
target: Vec2::new(
PositionPrecision::from_bits(self.input.target_x),
PositionPrecision::from_bits(self.input.target_y),
),
..Default::default()
};
let core = game_state.tee_cores.get_tee(self.player_uid).unwrap();
core.snap(now, &mut snap_tee);
let y = self.input.target_y as f32;
let x = self.input.target_x as f32;
let tmp_angle = y.atan2(x);
if tmp_angle < -(PI / 2.0) {
snap_tee.angle = AnglePrecision::from_bits(((tmp_angle + (2.0 * PI)) * 256.0) as i32);
} else {
snap_tee.angle = AnglePrecision::from_bits((tmp_angle * 256.0) as i32);
}
self.hook.snap(&mut snap_tee);
self.weapon.snap(&mut snap_tee);
self.jump.snap(&mut snap_tee);
player.tee = Some(snap_tee);
}
fn is_marked_for_destroy(&self) -> bool {
self.state.intersects(TeeState::MARK_FOR_DESTROY)
}
fn spawn_order(&self) -> SpawnOrder {
self.spawn_order
}
fn player_uid(&self) -> PlayerUid {
self.player_uid
}
fn on_tee_swap(&mut self, pid1: PlayerUid, pid2: PlayerUid) {
self.hook.on_tee_swap(pid1, pid2)
}
}
#[derive(Debug, Clone, Copy)]
enum InputState {
None,
First,
Ready,
}
impl InputState {
fn new() -> Self {
Self::None
}
fn first(self) -> Self {
match self {
Self::None | Self::First => Self::First,
Self::Ready => Self::Ready,
}
}
fn is_ready(self) -> bool {
matches!(self, Self::Ready)
}
fn ready(self) -> Self {
match self {
Self::None => Self::None,
Self::First | Self::Ready => Self::Ready,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DdraceState {
None,
Started,
Finished,
}
bitflags! {
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct TeeState: u32 {
const LIVE_FREEZE = 0x1;
const DEEP = 0x2;
const HOOK_DISABLED = 0x20;
const JETPACK = 0x80;
const PAUSED = 0x100;
const MARK_FOR_DESTROY = 0x200;
}
}
struct TileBugDetection {
will_touch_kill: bool,
first_tele: Option<(TeleTile, u8)>,
}
#[derive(Clone, Debug)]
pub struct Tee {
player_uid: PlayerUid,
spawn_order: SpawnOrder,
input: Input,
prev_input: Input,
input_state: InputState,
prev_pos: Vec2<f32>,
state: TeeState,
jump: Jump,
hook: hook::Hook,
weapon: weapon::Weapon,
start_time: Option<Instant>,
ddrace_state: DdraceState,
tele_checkpoint: u8,
walljump_colliding: bool,
walljump_left_wall: bool,
tune_zone_old: TuneZone,
tune_zone: TuneZone,
}
impl Tee {
pub(crate) fn new(
player_uid: PlayerUid,
spawn_order: SpawnOrder,
tee_core: &mut TeeCore,
input: Input,
) -> Tee {
Tee {
player_uid,
spawn_order,
input: input.clone(),
prev_input: input.clone(),
input_state: InputState::new(),
prev_pos: tee_core.pos(),
state: TeeState::empty(),
jump: Jump::default(),
hook: hook::Hook::new(),
weapon: weapon::Weapon::new(),
start_time: None,
ddrace_state: DdraceState::None,
tele_checkpoint: 0,
walljump_colliding: false,
walljump_left_wall: false,
tune_zone_old: TuneZone::default(),
tune_zone: TuneZone::default(),
}
}
pub(crate) fn player_uid(&self) -> PlayerUid {
self.player_uid
}
fn fake_tuning(&self) -> i32 {
let mut fake_tuning = 0;
if self.state.intersects(TeeState::JETPACK) && self.weapon.active() != ActiveWeapon::Ninja {
fake_tuning |= 1;
}
fake_tuning
}
pub(crate) fn format_save(
&self,
now: Instant,
player: &Player,
tee_core: &TeeCore,
f: &mut String,
game_uuid: Uuid,
add_time_penalty: bool,
) -> fmt::Result {
let name = &player.name;
let alive = 1;
let paused = self.state.intersects(TeeState::PAUSED) as i32;
let needed_fake_tuning = self.fake_tuning();
let tee_finished = tee_core.has_finished() as i32;
let is_solo = tee_core.is_solo() as i32;
write!(
f,
"\n{name}\t{alive}\t{paused}\t{needed_fake_tuning}\t{tee_finished}\t{is_solo}"
)?;
self.weapon.format_save_1(f)?;
let infinite_jumps = self.jump.has_infinite_jumps() as i32;
let jetpack = self.state.contains(TeeState::JETPACK) as i32;
let ninja_jetpack = 0;
let freeze_time = tee_core.save_freeze_time();
let freeze_start = tee_core.save_freeze_start(now);
let deep_frozen = self.state.contains(TeeState::DEEP) as i32;
let endless_hook = self.hook.has_endless() as i32;
let ddrace_state = 1;
let hit_disabled_flags = tee_core.save_weapon_hit();
let collision_enabled = tee_core.save_tee_collision() as i32;
let tune_zone = self.tune_zone.to_save();
let tune_zone_old = 0;
let hook_hit_enabled = (!self.state.intersects(TeeState::HOOK_DISABLED)) as i32;
let mut time = now.duration_since(self.start_time.unwrap_or(now)).unwrap();
if add_time_penalty {
time = time + Duration::from_secs(60);
}
let time = time.ticks();
let pos_x = tee_core.pos().x as i32;
let pos_y = tee_core.pos().y as i32;
let prev_pos_x = self.prev_pos.x as i32;
let prev_pos_y = self.prev_pos.y as i32;
let tele_checkpoint = self.tele_checkpoint;
let last_penalty = 0;
let vel_x = tee_core.vel().x;
let vel_y = tee_core.vel().y;
write!(f,
"\t{infinite_jumps}\t{jetpack}\t{ninja_jetpack}\t{freeze_time}\t{freeze_start}\t{deep_frozen}\t{endless_hook}\t\
{ddrace_state}\t{hit_disabled_flags}\t{collision_enabled}\t{tune_zone}\t{tune_zone_old}\t{hook_hit_enabled}\t{time}\t\
{pos_x}\t{pos_y}\t{prev_pos_x}\t{prev_pos_y}\t\
{tele_checkpoint}\t{last_penalty}\t\
{pos_x}\t{pos_y}\t{vel_x:.6}\t{vel_y:.6}")?;
self.weapon.format_save_2(f)?;
self.jump.format_save(f)?;
self.hook.format_save_1(f)?;
let time_cp_broadcast_end_time = 0;
let last_time_cp = -1;
let last_time_cp_broadcasted = -1;
write!(
f,
"\t{time_cp_broadcast_end_time}\t{last_time_cp}\t{last_time_cp_broadcasted}"
)?;
for _ in 0..25 {
let current_time_cp: f32 = 0.0;
write!(f, "\t{current_time_cp:.6}")?;
}
let not_eligible_for_finish = 0;
let has_telegun_gun = tee_core.telegun_gun() as i32;
let has_telegun_laser = tee_core.telegun_laser() as i32;
let has_telegun_grenade = tee_core.telegun_grenade() as i32;
write!(
f,
"\t\
{not_eligible_for_finish}\t\
{has_telegun_gun}\t{has_telegun_laser}\t{has_telegun_grenade}\t\
{game_uuid}"
)?;
self.hook.format_save_2(f)?;
let input_direction = self.input.direction;
let input_jump = self.input.jump;
let input_fire = self.input.fire;
let input_hook = self.input.hook;
write!(
f,
"\t{input_direction}\t{input_jump}\t{input_fire}\t{input_hook}"
)?;
self.weapon.format_save_3(f)?;
let tee_started = self.start_time.is_some() as i32;
let live_frozen = self.state.contains(TeeState::LIVE_FREEZE) as i32;
write!(f, "\t{tee_started}\t{live_frozen}",)?;
self.weapon.format_save_4(now, f)?;
Ok(())
}
pub(crate) fn load(
&mut self,
now: Instant,
tee_core: &mut TeeCore,
mut tee_info: std::str::Split<char>,
) -> Uuid {
let _alive = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
let paused = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
self.state.set(TeeState::PAUSED, paused);
let _needed_fake_tuning = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
if tee_info.next().unwrap().parse::<i32>().unwrap() != 0 {
tee_core.on_finish();
}
tee_core.set_solo(tee_info.next().unwrap().parse::<i32>().unwrap() != 0);
self.weapon.load_1(&mut tee_info);
self.jump.load_infinite_jumps(&mut tee_info);
let jetpack = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
self.state.set(TeeState::JETPACK, jetpack);
let _ninja_jetpack = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
tee_core.load_freeze(now, &mut tee_info);
let deep_freeze = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
self.state.set(TeeState::DEEP, deep_freeze);
let endless_hook = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
self.hook.set_endless(endless_hook);
let _ddrace_state = tee_info.next().unwrap().parse::<i32>().unwrap();
let hit_disabled_flags = tee_info.next().unwrap().parse::<i32>().unwrap();
tee_core.load_weapon_hit(hit_disabled_flags);
let tee_collision_enabled = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
tee_core.set_tee_collision(tee_collision_enabled);
self.tune_zone = TuneZone::from_save(tee_info.next().unwrap().parse::<u8>().unwrap());
self.tune_zone_old = TuneZone::from_save(tee_info.next().unwrap().parse::<u8>().unwrap());
let hook_hit_enabled = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
self.state.set(TeeState::HOOK_DISABLED, !hook_hit_enabled);
let time = tee_info.next().unwrap().parse::<i32>().unwrap();
self.start_time = if time != 0 {
Some(now - Duration::from_ticks(time))
} else {
None
};
tee_core.load_pos(&mut tee_info);
self.prev_pos.x = tee_info.next().unwrap().parse::<i32>().unwrap() as f32;
self.prev_pos.y = tee_info.next().unwrap().parse::<i32>().unwrap() as f32;
self.tele_checkpoint = tee_info.next().unwrap().parse::<u8>().unwrap();
let _last_penalty = tee_info.next().unwrap().parse::<i32>().unwrap();
tee_info.nth(1).unwrap();
tee_core.load_vel(&mut tee_info);
self.weapon.load_2(&mut tee_info);
self.jump.load(&mut tee_info);
self.hook.load_1(&mut tee_info);
tee_info.nth(2).unwrap(); tee_info.nth(24).unwrap(); let _not_eligible_for_finish = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
let has_telegun_gun = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
tee_core.set_telegun_gun(has_telegun_gun);
let has_telegun_laser = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
tee_core.set_telegun_laser(has_telegun_laser);
let has_telegun_grenade = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
tee_core.set_telegun_grenade(has_telegun_grenade);
let game_uuid = Uuid::from_str(tee_info.next().unwrap()).unwrap();
self.hook.load_2(&mut tee_info);
self.input.direction = tee_info.next().unwrap().parse::<i32>().unwrap();
self.input.jump = tee_info.next().unwrap().parse::<i32>().unwrap();
self.input.fire = tee_info.next().unwrap().parse::<i32>().unwrap();
self.input.hook = tee_info.next().unwrap().parse::<i32>().unwrap();
self.weapon.load_3(&mut tee_info);
let _tee_started = tee_info.next().unwrap().parse::<i32>().unwrap();
let live_frozen = tee_info.next().unwrap().parse::<i32>().unwrap() != 0;
self.state.set(TeeState::LIVE_FREEZE, live_frozen);
self.weapon.load_4(&mut tee_info); game_uuid
}
pub(crate) fn received_input(&mut self) {
self.input_state = self.input_state.first();
}
pub(crate) fn on_direct_input(
&mut self,
now: Instant,
game_state: &mut GameState,
snap_outer: &mut SnapOuter,
) {
if !self.input_state.is_ready() {
self.input_state = self.input_state.ready();
return;
}
if let Some(wanted_weapon) = self.weapon.weapon_switch(&self.input, &self.prev_input) {
self.weapon.do_weapon_switch(wanted_weapon);
}
self.weapon.fire(
now,
game_state,
snap_outer,
self.tune_zone,
self.player_uid,
&self.input,
&self.prev_input,
);
}
fn handle_tune_layer(&mut self, map: &Map, core: &mut TeeCore) {
self.tune_zone_old = self.tune_zone;
self.tune_zone = map.tune_zone(core.pos());
}
fn core_tick(&mut self, now: Instant, game_state: &mut GameState) {
self.hook.release_if_requested(now, game_state);
let core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
let is_grounded = game_state.map.is_grounded(core.pos());
core.set_move_restrictions(&game_state.map);
core.apply_gravity(game_state.map.tuning(self.tune_zone));
if self.input.jump != 0 {
if let Some(jump) = self.jump.on_jump(is_grounded) {
core.on_jump(
&mut game_state.events,
game_state.map.tuning(self.tune_zone),
jump,
);
}
} else {
self.jump.on_no_jump();
}
let target_direction = normalize(Vec2::new(
self.input.target_x as f32,
self.input.target_y as f32,
));
if self.input.hook != 0 {
if self.hook.on_hook_input(
game_state.map.tuning(self.tune_zone),
core.pos(),
target_direction,
) {
}
} else {
self.hook.on_no_hook_input(core.pos());
}
core.apply_directions(
game_state.map.tuning(self.tune_zone),
is_grounded,
Direction::from(self.input.direction),
);
if is_grounded {
self.jump.on_ground();
}
self.hook.tick(
now,
game_state,
self.tune_zone,
self.player_uid,
self.state.intersects(TeeState::HOOK_DISABLED),
self.input.direction,
target_direction,
);
self.player_interaction(game_state);
let core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
core.cap_vel();
}
fn handle_weapons(
&mut self,
now: Instant,
game_state: &mut GameState,
snap_outer: &mut SnapOuter,
) {
self.weapon.handle_ninja(now, game_state, self.player_uid);
if self.state.intersects(TeeState::JETPACK) {
self.weapon
.handle_jetpack(game_state, &self.input, self.tune_zone, self.player_uid);
}
if self.weapon.decrement_reload_timer() {
self.weapon.fire(
now,
game_state,
snap_outer,
self.tune_zone,
self.player_uid,
&self.input,
&self.prev_input,
);
}
}
fn handle_player_collision(&self, game_state: &mut GameState) {
if game_state.map.tuning(self.tune_zone).player_collision == 0.0 {
return;
}
let tee_core = game_state.tee_cores.get_tee(self.player_uid).unwrap();
if tee_core.tee_collision_disabled() {
return;
}
let pos = tee_core.pos();
let mut vel = tee_core.vel();
for (_, other_tee) in game_state
.tee_order
.id_order()
.tees_except(&game_state.tee_cores, self.player_uid)
{
if other_tee.tee_collision_disabled() {
continue;
}
let distance = pos.distance(other_tee.pos());
if distance == 0.0 {
continue;
}
let dir = normalize(pos - other_tee.pos());
if distance < TEE_PROXIMITY * 1.25 {
let a = TEE_PROXIMITY * 1.45 - distance;
let velocity = if vel.magnitude() > 0.0001 {
1.0 - (normalize(vel).dot(dir) + 1.0) / 2.0
} else {
0.5
};
vel += dir * a * (velocity * 0.75);
vel *= 0.85;
}
}
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_vel(vel);
}
fn player_interaction(&mut self, game_state: &mut GameState) {
if game_state
.tee_cores
.get_tee(self.player_uid)
.unwrap()
.is_solo()
{
return;
}
self.handle_player_collision(game_state);
if !self.state.intersects(TeeState::HOOK_DISABLED) {
self.hook
.handle_player_hook(self.player_uid, game_state, self.tune_zone);
}
}
fn no_kill_immunity(&self, game_state: &GameState) -> bool {
game_state.globals.all_finished() || self.ddrace_state != DdraceState::Finished
}
fn post_core_tick(&mut self, now: Instant, game_state: &mut GameState) {
let no_kill_immunity = self.no_kill_immunity(game_state);
self.hook.post_core_tick();
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.reset_frozen_last_tick();
self.jump.post_core_tick();
let will_die = tee_core.is_in_kill_area(no_kill_immunity, &game_state.map);
let mut bug_detection = TileBugDetection {
will_touch_kill: will_die,
first_tele: None,
};
if game_state
.map
.is_tee_in_skippable_radius(tee_core.pos(), Tile::StartLine)
{
}
if game_state
.map
.is_tee_in_skippable_radius(tee_core.pos(), Tile::FinishLine)
{
}
let map = game_state.map.clone();
let mut any_tile_triggered = false;
let current_pos = tee_core.pos();
for tiles in map.tile_on_line(self.prev_pos, current_pos) {
if tiles
.clone()
.all(|tile| tile != MapTile::Tile(Tile::DoubleJumpRefresher))
{
self.jump.on_no_double_jump_refresher();
}
any_tile_triggered |=
self.handle_any_map_tile(now, game_state, tiles, &mut bug_detection);
}
if !any_tile_triggered {
let tiles = map.game_front(coord::to_int_without_round(current_pos));
self.handle_any_map_tile(now, game_state, tiles, &mut bug_detection);
}
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
if tee_core.is_on_stoppers() && tee_core.vel().y > 0.0 {
self.jump.on_ground();
self.jump.on_no_jump();
}
tee_core.apply_move_restrictions();
if will_die {
self.mark_for_destroy();
}
}
fn on_start_line(&mut self, now: Instant, game_state: &mut GameState) {
if matches!(self.ddrace_state, DdraceState::None) {
self.ddrace_state = DdraceState::Started;
}
if self.start_time.is_none() {
game_state.on_tee_start(now);
self.start_time = Some(now);
} else if matches!(game_state.globals.team_kind(), TeamKind::Team0) {
self.start_time = Some(now);
}
}
fn on_finish_line(&mut self, now: Instant, game_state: &mut GameState) {
if self.ddrace_state == DdraceState::Started {
self.ddrace_state = DdraceState::Finished;
}
if let Some(start_time) = self.start_time {
game_state
.tee_cores
.get_tee_mut(self.player_uid)
.unwrap()
.on_finish();
game_state.on_tee_finish(now, start_time, self.player_uid);
}
self.start_time = None;
}
fn handle_any_map_tile(
&mut self,
now: Instant,
game_state: &mut GameState,
tiles: GameFrontIterator,
bug_detection: &mut TileBugDetection,
) -> bool {
let mut any_tile_triggered = false;
for tile in tiles {
match tile {
MapTile::Tile(tile) => {
if tile != Tile::Air {
self.handle_tile(now, game_state, tile, bug_detection);
any_tile_triggered = true;
}
}
MapTile::SwitchTile(switch_tile) => {
self.handle_switch(now, game_state, switch_tile);
any_tile_triggered = true;
}
MapTile::TeleTile((tele_tile, tele_id)) => {
self.handle_tele(now, game_state, tele_tile, tele_id, bug_detection);
any_tile_triggered = true;
}
}
}
any_tile_triggered
}
fn handle_tile(
&mut self,
now: Instant,
game_state: &mut GameState,
tile: Tile,
bug_detection: &mut TileBugDetection,
) {
let mut action = true;
match tile {
Tile::Kill => {
if !bug_detection.will_touch_kill {
let pos = game_state.tee_cores.get_tee(self.player_uid).unwrap().pos();
if self.no_kill_immunity(game_state) {
game_state.bugs.push(Bug {
time: now,
kind: BugKind::SkipDeath,
pos: coord::to_int(pos),
});
} else {
game_state.bugs.push(Bug {
time: now,
kind: BugKind::KillImmunity,
pos: coord::to_int(pos),
})
}
}
}
Tile::Air
| Tile::Collision
| Tile::Unhookable
| Tile::OldHookThrough
| Tile::HookThroughOnly
| Tile::HookThrough
| Tile::HookThroughFromDown
| Tile::HookThroughFromLeft
| Tile::HookThroughFromUp
| Tile::HookThroughFromRight
| Tile::StopDown
| Tile::StopUp
| Tile::StopRight
| Tile::StopLeft
| Tile::StopUpDown
| Tile::StopLeftRight
| Tile::StopAll
| Tile::EvilGunTeleporter
| Tile::GunTeleporter => {
action = false;
}
Tile::Freeze => {
if !self.state.intersects(TeeState::DEEP) {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.freeze(now, Duration::from_secs(3));
}
}
Tile::Unfreeze => {
if !self.state.intersects(TeeState::DEEP) {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.unfreeze();
}
}
Tile::UnlockTeam => {
game_state.globals.unlock_team();
}
Tile::StartLine => {
self.on_start_line(now, game_state);
}
Tile::FinishLine => {
self.on_finish_line(now, game_state);
}
Tile::EnableEndlessHook => self.hook.set_endless(true),
Tile::DisableEndlessHook => self.hook.set_endless(false),
Tile::EnableSolo => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_solo(true);
}
Tile::DisableSolo => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_solo(false);
}
Tile::EnableJetpack => self.state.insert(TeeState::JETPACK),
Tile::DisableJetpack => self.state.remove(TeeState::JETPACK),
Tile::EnableWeaponHit => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.enable_weapon_hit();
}
Tile::DisableWeaponHit => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.disable_weapon_hit();
}
Tile::DeepFreezeEnable => self.state.insert(TeeState::DEEP),
Tile::DeepFreezeDisable => self.state.remove(TeeState::DEEP),
Tile::EnableInfiniteJumps => self.jump.set_infinite_jumps(true),
Tile::DisableInfiniteJumps => self.jump.set_infinite_jumps(false),
Tile::EnableTeeCollision => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_tee_collision(true);
}
Tile::DisableTeeCollision => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_tee_collision(false);
}
Tile::EnableTeeHook => self.state.remove(TeeState::HOOK_DISABLED),
Tile::DisableTeeHook => self.state.insert(TeeState::HOOK_DISABLED),
Tile::EnableTelegunGun => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_telegun_gun(true);
}
Tile::DisableTelegunGun => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_telegun_gun(false);
}
Tile::EnableTelegunLaser => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_telegun_laser(true);
}
Tile::DisableTelegunLaser => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_telegun_laser(false);
}
Tile::EnableTelegunGrenade => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_telegun_grenade(true);
}
Tile::DisableTelegunGrenade => {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_telegun_grenade(false);
}
Tile::LiveFreezeEnable => {
self.state.insert(TeeState::LIVE_FREEZE);
}
Tile::LiveFreezeDisable => {
self.state.remove(TeeState::LIVE_FREEZE);
}
Tile::DoubleJumpRefresher => self.jump.refresh_double_jump(),
Tile::Walljump => {
if self.walljump_colliding && self.walljump_left_wall {
let tee_core = game_state.tee_cores.get_tee(self.player_uid).unwrap();
if tee_core.vel().y > 0.9 {
self.walljump_left_wall = false;
self.jump.wall_jump();
}
}
}
}
if bug_detection.first_tele.is_some() && action {
let pos = game_state.tee_cores.get_tee(self.player_uid).unwrap().pos();
game_state.bugs.push(Bug {
time: now,
kind: BugKind::TelePowerup(tile),
pos: coord::to_int(pos),
})
}
}
fn handle_switch(
&mut self,
_now: Instant,
_game_state: &mut GameState,
switch_tile: SwitchTile,
) {
if let SwitchTile::JumpCountSetter(num_jumps) = switch_tile {
self.jump.set_num_jumps(num_jumps)
}
}
fn handle_tele(
&mut self,
now: Instant,
game_state: &mut GameState,
tele_tile: TeleTile,
tele_id: u8,
bug_detection: &mut TileBugDetection,
) {
if !matches!(tele_tile, TeleTile::Weapon | TeleTile::Hook) {
if let Some((prev_tele_tile, prev_tele_id)) = bug_detection.first_tele {
if prev_tele_tile != tele_tile || prev_tele_id != tele_id {
let pos = game_state.tee_cores.get_tee(self.player_uid).unwrap().pos();
game_state.bugs.push(Bug {
time: now,
kind: BugKind::SkipTele(prev_tele_tile, prev_tele_id),
pos: coord::to_int(pos),
})
}
} else {
bug_detection.first_tele = Some((tele_tile, tele_id));
}
}
match tele_tile {
TeleTile::Tee => {
if let Some(tele_out) =
game_state
.map
.select_tele_out(now, &mut game_state.prng, tele_id)
{
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_pos(tele_out);
self.hook.reset(tee_core.pos());
}
}
TeleTile::EvilTee => {
if let Some(tele_out) =
game_state
.map
.select_tele_out(now, &mut game_state.prng, tele_id)
{
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_pos(tele_out);
tee_core.reset_vel();
tee_core.other_tees_release_hook(now);
self.hook.reset(tee_core.pos());
}
}
TeleTile::Checkpoint => {
self.tele_checkpoint = tele_id;
}
TeleTile::Weapon => {}
TeleTile::Hook => {}
TeleTile::TeeCheckpointOut => {
let tele_out = game_state.select_tele_checkpoint_out(now, self.tele_checkpoint);
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_pos(tele_out);
self.hook.reset(tee_core.pos());
}
TeleTile::EvilTeeCheckpointOut => {
let tele_out = game_state.select_tele_checkpoint_out(now, self.tele_checkpoint);
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_pos(tele_out);
tee_core.reset_vel();
tee_core.other_tees_release_hook(now);
self.hook.reset(tee_core.pos());
}
}
}
pub(crate) fn post_tick(&mut self, now: Instant, game_state: &mut GameState) {
self.move_vel_ramp(game_state);
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.round();
self.hook.release_if_requested(now, game_state);
self.hook.round();
}
fn vel_ramp(value: f32, start: f32, range: f32, curvature: f32) -> f32 {
if value < start {
1.0
} else {
1.0 / curvature.powf((value - start) / range)
}
}
fn move_vel_ramp(&mut self, game_state: &mut GameState) {
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
let old_pos = tee_core.pos();
let old_vel = tee_core.vel();
tee_core.move_vel_ramp(&game_state.map, self.tune_zone);
let new_vel = tee_core.vel();
let new_pos = tee_core.pos();
self.walljump_colliding = false;
if new_vel.x < 0.001 && new_vel.x > -0.001 {
if old_vel.x != 0.0 {
self.walljump_colliding = true;
}
} else {
self.walljump_left_wall = true;
}
let tuning = game_state.map.tuning(self.tune_zone);
if !tee_core.tee_collision_disabled() && tuning.player_collision != 0.0 {
let distance = old_pos.distance(new_pos);
if distance > 0.0 {
let end = (distance + 1.0) as i32;
let mut last_pos = old_pos;
for i in 0..end {
let a = i as f32 / distance;
let next_pos = old_pos + (new_pos - old_pos) * a;
for (_, other_tee) in game_state
.tee_order
.id_order()
.tees_except(&game_state.tee_cores, self.player_uid)
{
if other_tee.tee_collision_disabled() {
continue;
}
let d = next_pos.distance(other_tee.pos());
if (0.0..TEE_PROXIMITY).contains(&d) {
let set_pos = if a > 0.0 {
last_pos
} else if new_pos.distance(other_tee.pos()) > d {
new_pos
} else {
old_pos
};
let tee_core =
game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_pos(set_pos);
return;
}
}
last_pos = next_pos;
}
}
}
self.prev_pos = old_pos;
let tee_core = game_state.tee_cores.get_tee_mut(self.player_uid).unwrap();
tee_core.set_pos(new_pos);
}
pub(crate) fn mark_for_destroy(&mut self) {
self.state.insert(TeeState::MARK_FOR_DESTROY);
}
pub(crate) fn store_new_input(&mut self, input: Input) {
self.prev_input = self.input.clone();
self.input = input;
}
pub(crate) fn closed_point_on_line(
line_start: Vec2<f32>,
line_end: Vec2<f32>,
target: Vec2<f32>,
) -> Option<Vec2<f32>> {
let c = target - line_start;
let mut v = line_end - line_start;
let d = (line_start - line_end).magnitude();
if d > 0.0 {
v = normalize(v);
let t = v.dot(c) / d;
Some(line_start + (line_end - line_start) * clamp(t, 0.0, 1.0))
} else {
None
}
}
pub(crate) fn on_pickup(
&mut self,
now: Instant,
tee_core: &mut TeeCore,
events: &mut state::Events,
kind: PickupKind,
) {
match kind {
PickupKind::Health => {
if tee_core.freeze(now, Duration::from_secs(3)) {
events.create_sound(tee_core.pos(), Sound::PickupHealth);
}
}
PickupKind::Armor => {
if self.weapon.remove_collectable() {
events.create_sound(tee_core.pos(), Sound::PickupArmor);
}
}
PickupKind::Shield(weapon) => {
if self.weapon.remove(weapon) {
events.create_sound(tee_core.pos(), Sound::PickupArmor);
}
}
PickupKind::Weapon(weapon) => {
if self.weapon.add(weapon, now) {
events.create_sound(tee_core.pos(), weapon.to_pickup_sound())
}
}
}
}
pub(crate) fn swap(&mut self, other: &mut Tee) {
mem::swap(self, other);
mem::swap(&mut self.player_uid, &mut other.player_uid);
mem::swap(&mut self.spawn_order, &mut other.spawn_order);
mem::swap(&mut self.input, &mut other.input);
mem::swap(&mut self.prev_pos, &mut other.prev_pos);
mem::swap(&mut self.input_state, &mut other.input_state);
}
pub(crate) fn reset_hook(&mut self, tee_uid: PlayerUid) {
self.hook.reset_hooked_player(tee_uid)
}
pub(crate) fn start_time(&self) -> Option<Instant> {
self.start_time
}
pub(crate) fn race_time(&self, now: Instant) -> Option<Duration> {
self.start_time
.map(|past_time| now.duration_since(past_time).unwrap())
}
}