use crate::entities::tee::TEE_PROXIMITY;
use crate::entities::Tee;
use crate::ids::PlayerUid;
use crate::map::{HookHit, TuneZone};
use crate::state::GameState;
use crate::tuning::Tuning;
use std::fmt;
use std::fmt::Write as _;
use twgame_core::normalize;
use twgame_core::twsnap::enums::HookState;
use twgame_core::twsnap::enums::Sound;
use twgame_core::twsnap::flags::TeeFlags;
use twgame_core::twsnap::items::Tee as SnapTee;
use twgame_core::twsnap::time::{Duration, Instant};
use twgame_core::twsnap::vec2_from_bits;
use vek::num_traits::SaturatingSub;
use vek::Vec2;
#[derive(Clone, Debug)]
pub struct Hook {
state: HookState,
tele_base: Option<Vec2<f32>>,
pos: Vec2<f32>,
direction: Vec2<f32>,
attached_tee: Option<PlayerUid>,
duration: Duration,
endless_hook: bool,
}
impl Hook {
pub fn new() -> Hook {
Hook {
state: HookState::Idle,
tele_base: None,
pos: Vec2::zero(),
direction: Vec2::zero(),
attached_tee: None,
duration: Duration::T0MS,
endless_hook: false,
}
}
fn hook_tick(&self) -> Duration {
if self.duration == Duration::T0MS {
Duration::T0MS
} else {
Duration::T120MS.saturating_sub(&self.duration)
}
}
pub fn format_save_1(&self, f: &mut String) -> fmt::Result {
let hook_pos_x = self.pos.x as i32;
let hook_pos_y = self.pos.y as i32;
let hook_dir_x = self.direction.x;
let hook_dir_y = self.direction.y;
let hook_tele_base = self.tele_base.unwrap_or(Vec2::zero());
let hook_tick = self.hook_tick().ticks();
let hook_state = self.state as i32;
write!(
f,
"\t\
{hook_pos_x}\t{hook_pos_y}\t{hook_dir_x:.6}\t{hook_dir_y:.6}\t\
{hook_tele_base_x}\t{hook_tele_base_y}\t{hook_tick}\t{hook_state}",
hook_tele_base_x = hook_tele_base.x,
hook_tele_base_y = hook_tele_base.y,
)
}
pub fn format_save_2(&self, f: &mut String) -> fmt::Result {
let hooked_player = -1;
let new_hook = self.tele_base.is_some() as i32;
write!(f, "\t{hooked_player}\t{new_hook}")
}
pub fn load_1(&mut self, tee_info: &mut std::str::Split<char>) {
self.pos.x = tee_info.next().unwrap().parse::<i32>().unwrap() as f32;
self.pos.y = tee_info.next().unwrap().parse::<i32>().unwrap() as f32;
self.direction.x = tee_info.next().unwrap().parse::<f32>().unwrap();
self.direction.y = tee_info.next().unwrap().parse::<f32>().unwrap();
self.tele_base = Some(Vec2::new(
tee_info.next().unwrap().parse::<i32>().unwrap() as f32,
tee_info.next().unwrap().parse::<i32>().unwrap() as f32,
));
let _hook_tick = tee_info.next().unwrap().parse::<i32>().unwrap();
let hook_state = tee_info.next().unwrap().parse::<i32>().unwrap();
self.state = HookState::from(hook_state);
}
pub fn load_2(&mut self, tee_info: &mut std::str::Split<char>) {
let _hooked_player = tee_info.next().unwrap().parse::<i32>().unwrap();
let new_hook = tee_info.next().unwrap_or("0").parse::<i32>().unwrap();
if new_hook == 0 {
self.tele_base = None;
}
}
pub fn on_hook_input(
&mut self,
tuning: &Tuning,
pos: Vec2<f32>,
target_dir: Vec2<f32>,
) -> bool {
if self.state == HookState::Idle {
self.state = HookState::Flying;
self.pos = pos + target_dir * TEE_PROXIMITY * 1.5;
self.direction = target_dir;
self.attached_tee = None;
self.duration = Duration::from_secs_f32(tuning.hook_duration)
.decrement()
.unwrap_or(Duration::T0MS)
.decrement()
.unwrap_or(Duration::T0MS);
return true;
}
false
}
pub fn on_no_hook_input(&mut self, tee_pos: Vec2<f32>) {
self.pos = tee_pos; self.attached_tee = None;
self.tele_base = None;
self.state = HookState::Idle;
}
pub fn reset(&mut self, core_pos: Vec2<f32>) {
self.attached_tee = None;
self.state = HookState::Retracted;
self.pos = core_pos;
}
pub fn release_if_requested(&mut self, now: Instant, game_state: &GameState) {
let Some(tee) = self.attached_tee else {
return;
};
let Some(core) = game_state.tee_cores.get_tee(tee) else {
return;
};
if core.last_hook_release() == now {
self.attached_tee = None;
self.state = HookState::Retracted;
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn tick(
&mut self,
now: Instant,
game_state: &mut GameState,
tune_zone: TuneZone,
tee_uid: PlayerUid,
tee_hook_disabled: bool,
direction: i32, target_dir: Vec2<f32>, ) {
match self.state {
HookState::Idle | HookState::Retracted => {}
HookState::RetractStart => self.state = HookState::Retracting,
HookState::Retracting => self.state = HookState::RetractEnd,
HookState::RetractEnd => {
self.state = HookState::Retracted;
}
HookState::Flying => {
let tee_core = game_state.tee_cores.get_tee(tee_uid).unwrap();
let tee_pos = tee_core.pos();
let tuning = game_state.map.tuning(tune_zone);
let mut new_pos = self.pos + self.direction * tuning.hook_fire_speed;
let hook_base = self.tele_base.unwrap_or(tee_core.pos());
if new_pos.distance(hook_base) > tuning.hook_length {
self.state = HookState::RetractStart;
new_pos = hook_base + normalize(new_pos - hook_base) * tuning.hook_length;
}
let hit = game_state.map.intersect_hook(self.pos, &mut new_pos);
let mut distance_squared: f32 = 0.0;
if tuning.player_hooking != 0.0 && !tee_core.is_solo() && !tee_hook_disabled {
for (uid, other_tee) in game_state
.tee_order
.id_order()
.tees_except(&game_state.tee_cores, tee_uid)
.filter(|(_, tee)| !tee.is_solo())
{
if let Some(closest_point) =
Tee::closed_point_on_line(self.pos, new_pos, other_tee.pos())
{
if other_tee.pos().distance(closest_point) < TEE_PROXIMITY + 2.0
&& (self.attached_tee.is_none()
|| self.pos.distance_squared(other_tee.pos())
< distance_squared)
{
game_state
.events
.create_sound(tee_pos, Sound::HookAttachPlayer);
self.state = HookState::Grabbed;
self.attached_tee = Some(uid);
distance_squared = self.pos.distance_squared(other_tee.pos());
}
}
}
}
if self.state == HookState::Flying {
self.pos = new_pos;
match hit {
Some(HookHit::Collision) => {
game_state
.events
.create_sound(tee_pos, Sound::HookAttachGround);
self.state = HookState::Grabbed;
}
Some(HookHit::Unhookable) => {
game_state.events.create_sound(tee_pos, Sound::HookNoattach);
self.state = HookState::RetractStart;
}
Some(HookHit::Tele(tele_id)) => {
if let Some(tele_out) =
game_state
.map
.select_tele_out(now, &mut game_state.prng, tele_id)
{
self.tele_base = Some(tele_out);
self.pos = tele_out + target_dir * TEE_PROXIMITY * 1.5;
self.direction = target_dir;
}
}
None => {}
}
}
}
HookState::Grabbed => {}
}
if self.state == HookState::Grabbed {
let tee_core = game_state.tee_cores.get_tee(tee_uid).unwrap();
let tee_pos = tee_core.pos();
if self.attached_tee.is_none() && self.pos.distance(tee_pos) > 46.0 {
let tuning = game_state.map.tuning(tune_zone);
let tee_core = game_state.tee_cores.get_tee_mut(tee_uid).unwrap();
let mut hook_vel = normalize(self.pos - tee_pos) * tuning.hook_drag_accel;
if hook_vel.y > 0.0 {
hook_vel.y *= 0.3;
}
if hook_vel.x < 0.0 && direction < 0 || hook_vel.x > 0.0 && direction > 0 {
hook_vel.x *= 0.95;
} else {
hook_vel.x *= 0.75;
}
let new_vel = hook_vel + tee_core.vel();
if new_vel.magnitude() < tuning.hook_drag_speed
|| new_vel.magnitude() < tee_core.vel().magnitude()
{
tee_core.set_vel(new_vel); }
}
if self.attached_tee.is_some() {
if let Some(duration) = self.duration.decrement() {
self.duration = duration;
} else {
self.attached_tee = None;
self.state = HookState::Retracted;
self.pos = tee_pos; }
}
}
}
pub fn handle_player_hook(
&self,
tee_uid: PlayerUid,
game_state: &mut GameState,
tune_zone: TuneZone,
) {
let tuning = game_state.map.tuning(tune_zone);
if tuning.player_hooking == 0.0 {
return;
}
let Some(other_tee) = self.attached_tee.as_ref() else {
return;
};
let Some([tee, other_tee]) = game_state.tee_cores.get_tees_mut([tee_uid, *other_tee])
else {
return;
};
if other_tee.is_solo() {
return;
}
let distance = tee.pos().distance(other_tee.pos());
let dir = normalize(tee.pos() - other_tee.pos());
if distance <= TEE_PROXIMITY * 1.5 {
return;
}
let accel = tuning.hook_drag_accel * (distance / tuning.hook_length);
let drag_speed = tuning.hook_drag_speed;
other_tee.satured_impact(dir * accel * 1.5, drag_speed);
tee.satured_impact(-accel * dir * 0.25, drag_speed);
}
pub fn set_endless(&mut self, activated: bool) {
self.endless_hook = activated;
}
pub fn has_endless(&self) -> bool {
self.endless_hook
}
pub fn post_core_tick(&mut self) {
if self.endless_hook {
self.duration = Duration::T980MS;
}
}
pub fn round(&mut self) {
self.pos.x = self.pos.x.round() as i32 as f32;
self.pos.y = self.pos.y.round() as i32 as f32;
self.direction.x = (self.direction.x * 256.0).round() as i32 as f32 / 256.0;
self.direction.y = (self.direction.y * 256.0).round() as i32 as f32 / 256.0;
}
pub fn snap(&self, tee: &mut SnapTee) {
if self.endless_hook {
tee.flags.insert(TeeFlags::ENDLESS_HOOK);
}
tee.hook_state = self.state;
tee.hook_tick = self.hook_tick();
tee.hook_pos = vec2_from_bits(self.pos.round());
tee.hook_direction = vec2_from_bits((self.direction * 256.0).round());
tee.hooked_player = self.attached_tee.map(|pid| pid.snap_id());
}
pub fn on_tee_swap(&mut self, pid1: PlayerUid, pid2: PlayerUid) {
if let Some(hooked_player) = self.attached_tee.as_mut() {
if *hooked_player == pid1 {
*hooked_player = pid2;
} else if *hooked_player == pid2 {
*hooked_player = pid1;
}
}
}
pub fn reset_hooked_player(&mut self, tee_uid: PlayerUid) {
if let Some(hooked_player) = self.attached_tee.as_ref() {
if *hooked_player == tee_uid {
self.attached_tee = None;
self.state = HookState::Retracted;
}
}
}
}