use crate::entities::tee::TEE_PROXIMITY;
use crate::entities::{laser, EntityItemNew, Laser, Projectile};
use crate::ids::PlayerUid;
use crate::map::TuneZone;
use crate::state::GameState;
use crate::{Input, SnapOuter};
use arrayvec::ArrayVec;
use std::fmt;
use std::fmt::Write as _;
use twgame_core::normalize;
use twgame_core::twsnap;
use twgame_core::twsnap::enums::{ActiveWeapon, CollectableWeapon, SelectableWeapon, Sound};
use twgame_core::twsnap::flags::GotWeapon;
use twgame_core::twsnap::time::{Duration, Instant};
use twgame_core::twsnap::vec2_from_bits;
use vek::Vec2;
#[derive(Clone, Debug)]
pub struct Weapon {
active: Option<SelectableWeapon>,
last: SelectableWeapon,
queued: Option<SelectableWeapon>,
ninja: Option<Ninja>,
got: GotWeapon,
reload_timer: Duration,
attack_tick: Instant,
}
impl Weapon {
pub fn new() -> Self {
Self {
active: Some(SelectableWeapon::Pistol),
last: SelectableWeapon::Hammer,
queued: None,
ninja: None,
got: GotWeapon::HAMMER | GotWeapon::PISTOL,
reload_timer: Duration::T0MS,
attack_tick: Instant::zero(),
}
}
pub fn format_save_1(&self, f: &mut String) -> fmt::Result {
let shotgun_got = self.got.contains(GotWeapon::SHOTGUN) as i32;
let grenade_got = self.got.contains(GotWeapon::GRENADE) as i32;
let laser_got = self.got.contains(GotWeapon::RIFLE) as i32;
let ninja_got = self.got.contains(GotWeapon::NINJA) as i32;
let last_weapon = self.last as i32;
let queued_weapon = self.queued.map(|s| s as i32).unwrap_or(-1);
write!(
f,
"\t\
0\t-1\t0\t1\t\
0\t-1\t0\t1\t\
0\t{shotgun_ammo}\t0\t{shotgun_got}\t\
0\t{grenade_ammo}\t0\t{grenade_got}\t\
0\t{laser_ammo}\t0\t{laser_got}\t\
0\t{ninja_ammo}\t0\t{ninja_got}\t\
{last_weapon}\t{queued_weapon}",
shotgun_ammo = -shotgun_got,
grenade_ammo = -grenade_got,
laser_ammo = -laser_got,
ninja_ammo = -ninja_got
)
}
pub fn format_save_2(&self, f: &mut String) -> fmt::Result {
let active_weapon = self.active() as i32;
write!(f, "\t{active_weapon}")
}
pub fn format_save_3(&self, f: &mut String) -> fmt::Result {
let reload_timer = self.reload_timer.ticks();
write!(f, "\t{reload_timer}")
}
pub fn format_save_4(&self, now: Instant, f: &mut String) -> fmt::Result {
if let Some(ninja) = self.ninja.as_ref() {
let activation_tick = now.duration_since(ninja.activation_tick).unwrap().ticks();
if let Some(dash) = ninja.dash.as_ref() {
let dir_x = dash.activation_dir.x;
let dir_y = dash.activation_dir.y;
let current_move_time = dash.current_move_time.ticks();
let old_vel_amount = dash.old_vel_amount;
write!(
f,
"\t{dir_x}\t{dir_y}\t{activation_tick}\t{current_move_time}\t{old_vel_amount}"
)
} else {
write!(f, "\t0.000000\t0.000000\t{activation_tick}\t0\t0\t")
}
} else {
write!(f, "\t0.000000\t0.000000\t0\t0\t0\t")
}
}
pub fn load_1(&mut self, tee_info: &mut std::str::Split<char>) {
self.got
.set(GotWeapon::HAMMER, tee_info.nth(3).unwrap() != "0");
self.got
.set(GotWeapon::PISTOL, tee_info.nth(3).unwrap() != "0");
self.got
.set(GotWeapon::SHOTGUN, tee_info.nth(3).unwrap() != "0");
self.got
.set(GotWeapon::GRENADE, tee_info.nth(3).unwrap() != "0");
self.got
.set(GotWeapon::RIFLE, tee_info.nth(3).unwrap() != "0");
self.got
.set(GotWeapon::NINJA, tee_info.nth(3).unwrap() != "0");
self.last =
SelectableWeapon::from_save(tee_info.next().unwrap().parse::<i32>().unwrap()).unwrap();
self.queued = SelectableWeapon::from_save(tee_info.next().unwrap().parse::<i32>().unwrap());
}
pub fn load_2(&mut self, tee_info: &mut std::str::Split<char>) {
self.active = SelectableWeapon::from_save(tee_info.next().unwrap().parse::<i32>().unwrap());
if self.active.is_none() {}
}
pub fn load_3(&mut self, tee_info: &mut std::str::Split<char>) {
self.reload_timer = Duration::from_ticks(tee_info.next().unwrap().parse::<i32>().unwrap());
}
pub fn load_4(&mut self, _tee_info: &mut std::str::Split<char>) {
if self.got.contains(GotWeapon::NINJA) {
self.got.remove(GotWeapon::NINJA);
self.active = Some(self.last);
}
}
pub fn active(&self) -> ActiveWeapon {
if let Some(active) = self.active {
active.into()
} else {
ActiveWeapon::Ninja
}
}
fn weapon_flag(from_selectable: SelectableWeapon) -> GotWeapon {
match from_selectable {
SelectableWeapon::Hammer => GotWeapon::HAMMER,
SelectableWeapon::Pistol => GotWeapon::PISTOL,
SelectableWeapon::Shotgun => GotWeapon::SHOTGUN,
SelectableWeapon::Grenade => GotWeapon::GRENADE,
SelectableWeapon::Rifle => GotWeapon::RIFLE,
}
}
fn weapon_active_flag(from_active: ActiveWeapon) -> GotWeapon {
match from_active {
ActiveWeapon::Hammer => GotWeapon::HAMMER,
ActiveWeapon::Pistol => GotWeapon::PISTOL,
ActiveWeapon::Shotgun => GotWeapon::SHOTGUN,
ActiveWeapon::Grenade => GotWeapon::GRENADE,
ActiveWeapon::Rifle => GotWeapon::RIFLE,
ActiveWeapon::Ninja => GotWeapon::NINJA,
}
}
pub fn decrement_reload_timer(&mut self) -> bool {
if let Some(decrement) = self.reload_timer.decrement() {
self.reload_timer = decrement;
false
} else {
true
}
}
pub fn weapon_switch(
&mut self,
cur_input: &Input,
prev_input: &Input,
) -> Option<SelectableWeapon> {
let mut wanted_weapon = self.active?;
if let Some(queued) = self.queued {
wanted_weapon = queued
}
let got = self.got.bits().count_ones();
if got == 0 {
return None;
}
let mut next = cur_input.count_weapon_next(prev_input);
let mut prev = cur_input.count_weapon_prev(prev_input);
while next > 0 {
wanted_weapon = wanted_weapon.next();
if self.got.contains(Weapon::weapon_flag(wanted_weapon)) {
next -= 1;
}
}
while prev > 0 {
wanted_weapon = wanted_weapon.prev();
if self.got.contains(Weapon::weapon_flag(wanted_weapon)) {
prev -= 1;
}
}
if cur_input.wanted_weapon != 0 {
wanted_weapon = SelectableWeapon::from_wanted_weapon(prev_input.wanted_weapon)?;
}
self.queued = None;
Some(wanted_weapon)
}
pub fn do_weapon_switch(&mut self, selected: SelectableWeapon) {
if self.reload_timer != Duration::T0MS
|| self.got.contains(GotWeapon::NINJA)
|| !self.got.contains(Weapon::weapon_flag(selected))
{
return;
}
self.set(selected.into())
}
fn set(&mut self, weapon: ActiveWeapon) {
if self.active() == weapon {
return;
}
if let Some(active) = self.active {
self.last = active;
}
self.active = weapon.into();
self.queued = None;
}
pub fn remove_collectable(&mut self) -> bool {
let collectable =
GotWeapon::SHOTGUN | GotWeapon::GRENADE | GotWeapon::RIFLE | GotWeapon::NINJA;
let removed_any = self.got.intersects(collectable);
if removed_any {
self.got.remove(collectable);
self.ninja = None;
if self.active != Some(SelectableWeapon::Pistol) {
self.active = Some(SelectableWeapon::Hammer);
}
}
if self.last.is_collectable() {
self.last = SelectableWeapon::Hammer;
}
removed_any
}
pub fn remove(&mut self, collectable: CollectableWeapon) -> bool {
let remove = collectable.into();
let removed_any = self.got.contains(remove);
if removed_any {
self.got.remove(remove);
if collectable == CollectableWeapon::Ninja {
self.active = Some(self.last);
}
self.last = SelectableWeapon::Pistol;
}
if collectable == CollectableWeapon::Ninja {
self.ninja = None;
}
let c: ActiveWeapon = collectable.into();
if self.active() == c {
self.active = Some(SelectableWeapon::Hammer);
}
removed_any
}
pub fn add(&mut self, collectable: CollectableWeapon, now: Instant) -> bool {
let add = collectable.into();
let added_any = !self.got.contains(add);
self.got |= add;
if collectable == CollectableWeapon::Ninja {
if let Some(ninja) = self.ninja.as_mut() {
ninja.refresh(now);
} else {
self.ninja = Some(Ninja::new(now));
}
}
added_any
}
pub(super) fn handle_ninja(
&mut self,
now: Instant,
game_state: &mut GameState,
tee_uid: PlayerUid,
) {
if let Some(ninja) = self.ninja.as_mut() {
self.active = None;
if ninja.is_time_up(now) {
self.remove(CollectableWeapon::Ninja);
return;
}
ninja.tick(game_state, tee_uid);
} else {
assert_ne!(self.active, None)
}
}
pub(super) fn handle_jetpack(
&mut self,
game_state: &mut GameState,
cur_input: &Input,
tune_zone: TuneZone,
tee_uid: PlayerUid,
) {
if self.active() == ActiveWeapon::Pistol && cur_input.firing() {
let direction = normalize(cur_input.cursor());
let strength = game_state.map.tuning(tune_zone).jetpack_strength;
let tee_core = game_state.tee_cores.get_tee_mut(tee_uid).unwrap();
tee_core.impact(direction * -1.0 * (strength / 100.0 / 6.11), false);
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn fire(
&mut self,
now: Instant,
game_state: &mut GameState,
snap_outer: &mut SnapOuter,
tune_zone: TuneZone,
owner: PlayerUid,
cur_input: &Input,
prev_input: &Input,
) {
if self.reload_timer != Duration::T0MS {
return;
}
let direction = normalize(cur_input.cursor());
let tee = game_state.tee_cores.get_tee(owner).unwrap();
let automatic = matches!(
self.active(),
ActiveWeapon::Shotgun | ActiveWeapon::Grenade | ActiveWeapon::Rifle
) || tee.frozen_last_tick();
if cur_input.count_weapon_fire(prev_input) == 0 && !(automatic && cur_input.firing()) {
return;
}
if tee.is_frozen() {
game_state
.events
.create_sound(tee.pos(), Sound::PlayerPainLong);
return;
}
if !self.got.contains(Weapon::weapon_active_flag(self.active())) {
return;
}
let tuning = game_state.map.tuning(tune_zone);
let proj_start_pos = tee.pos() + direction * TEE_PROXIMITY * 0.75;
match self.active() {
ActiveWeapon::Hammer => {
game_state.events.create_sound(tee.pos(), Sound::HammerFire);
let mut hit = false;
if tee.can_hammer() {
let tee_pos = tee.pos();
let mut iter = game_state
.tee_order
.spawn_order()
.tees_except_mut(&mut game_state.tee_cores, owner);
while let Some((_, target)) = iter.next() {
if target.is_solo() {
continue;
}
if target.pos().distance(proj_start_pos) >= TEE_PROXIMITY * 1.5 {
continue;
}
let particle_pos = if target.pos().distance(proj_start_pos) > 0.0 {
target.pos()
- normalize(target.pos() - proj_start_pos) * TEE_PROXIMITY * 0.5
} else {
proj_start_pos
};
let particle_pos = Vec2::new(particle_pos.x, particle_pos.y);
game_state.events.add_event(twsnap::Events::HammerHit(
twsnap::events::HammerHit {
pos: vec2_from_bits(particle_pos),
},
));
let dir = if tee_pos != target.pos() {
normalize(target.pos() - tee_pos)
} else {
Vec2::new(0.0, -1.0)
};
let strength = tuning.hammer_strength;
let tmp = normalize(dir + Vec2::new(0.0, -1.1)) * 10.0;
target.impact((Vec2::new(0.0, -1.0) + tmp) * strength, true);
target.unfreeze();
hit = true;
}
}
if hit {
self.reload_timer = Duration::from_ms_f32(tuning.hammer_hit_fire_delay);
} else {
self.reload_timer = Duration::from_ms_f32(tuning.hammer_fire_delay);
}
}
ActiveWeapon::Pistol => {
game_state.entities.add_next_tick(EntityItemNew::projectile(
Projectile::new_pistol(
game_state,
snap_outer,
owner,
proj_start_pos,
cur_input.cursor(),
now,
tune_zone,
),
));
self.reload_timer = Duration::from_ms_f32(tuning.gun_fire_delay);
}
ActiveWeapon::Shotgun => {
let shotgun_fire_delay = tuning.shotgun_fire_delay;
let laser = Laser::new(
now,
game_state,
snap_outer,
tee.pos(),
direction,
tuning.laser_reach,
tune_zone,
owner,
laser::Kind::Shotgun,
);
game_state
.entities
.add_next_tick(EntityItemNew::laser(laser));
self.reload_timer = Duration::from_ms_f32(shotgun_fire_delay);
}
ActiveWeapon::Grenade => {
game_state.entities.add_next_tick(EntityItemNew::projectile(
Projectile::new_grenade(
game_state,
snap_outer,
owner,
proj_start_pos,
cur_input.cursor(),
now,
tune_zone,
),
));
self.reload_timer = Duration::from_ms_f32(tuning.grenade_fire_delay);
}
ActiveWeapon::Rifle => {
let laser_fire_delay = tuning.laser_fire_delay;
let laser = Laser::new(
now,
game_state,
snap_outer,
tee.pos(),
direction,
tuning.laser_reach,
tune_zone,
owner,
laser::Kind::Rifle,
);
game_state
.entities
.add_next_tick(EntityItemNew::laser(laser));
self.reload_timer = Duration::from_ms_f32(laser_fire_delay);
}
ActiveWeapon::Ninja => {
let ninja = self.ninja.as_mut().unwrap();
ninja.dash(direction, tee.vel().magnitude());
self.reload_timer = Duration::from_ms_f32(tuning.ninja_fire_delay);
}
}
self.attack_tick = now;
}
pub fn snap(&self, tee: &mut twsnap::items::Tee) {
tee.weapon = self.active();
tee.attack_tick = self.attack_tick;
if self.got.contains(GotWeapon::HAMMER) {
tee.flags.insert(twsnap::flags::TeeFlags::WEAPON_HAMMER);
}
if self.got.contains(GotWeapon::PISTOL) {
tee.flags.insert(twsnap::flags::TeeFlags::WEAPON_GUN);
}
if self.got.contains(GotWeapon::SHOTGUN) {
tee.flags.insert(twsnap::flags::TeeFlags::WEAPON_SHOTGUN);
}
if self.got.contains(GotWeapon::GRENADE) {
tee.flags.insert(twsnap::flags::TeeFlags::WEAPON_GRENADE);
}
if self.got.contains(GotWeapon::RIFLE) {
tee.flags.insert(twsnap::flags::TeeFlags::WEAPON_LASER);
}
if self.got.contains(GotWeapon::NINJA) {
tee.flags.insert(twsnap::flags::TeeFlags::WEAPON_NINJA);
}
}
}
#[derive(Clone, Debug)]
struct NinjaDash {
activation_dir: Vec2<f32>,
current_move_time: Duration,
old_vel_amount: i32,
hit: ArrayVec<PlayerUid, 10>,
}
impl NinjaDash {
fn new(activation_dir: Vec2<f32>, old_vel_amount: i32) -> Self {
Self {
activation_dir,
current_move_time: Duration::T180MS,
old_vel_amount,
hit: ArrayVec::new(),
}
}
}
#[derive(Clone, Debug)]
struct Ninja {
activation_tick: Instant,
dash: Option<NinjaDash>,
}
impl Ninja {
fn new(activation_tick: Instant) -> Self {
Self {
activation_tick,
dash: None,
}
}
fn refresh(&mut self, now: Instant) {
self.activation_tick = now;
}
fn is_time_up(&self, now: Instant) -> bool {
now.duration_passed_since(self.activation_tick, Duration::from_secs(15))
}
fn dash(&mut self, activation_dir: Vec2<f32>, old_vel_amount: f32) {
self.dash = Some(NinjaDash::new(activation_dir, old_vel_amount as i32))
}
fn tick(&mut self, game_state: &mut GameState, tee_uid: PlayerUid) {
if let Some(dash) = &mut self.dash {
let tee_core = game_state.tee_cores.get_tee_mut(tee_uid).unwrap();
if let Some(remaining) = dash.current_move_time.decrement() {
dash.current_move_time = remaining;
let vel = dash.activation_dir * 50.0;
let (new_pos, _) = game_state.map.tee_move_box(tee_core.pos(), vel);
let old_pos = tee_core.pos();
tee_core.set_pos(new_pos);
tee_core.reset_vel();
if tee_core.is_solo() {
return;
}
const RADIUS: f32 = TEE_PROXIMITY * 2.0;
let mut iter = game_state
.tee_order
.spawn_order()
.tees_except_mut(&mut game_state.tee_cores, tee_uid);
while let Some((other_tee_uid, other_tee)) = iter.next() {
if !other_tee.can_be_ninjaed() {
continue;
}
if old_pos.distance(other_tee.pos()) >= RADIUS {
continue;
}
if dash.hit.contains(&other_tee_uid) {
continue;
}
other_tee.impact(Vec2::new(0.0, -10.0), false);
game_state
.events
.create_sound(other_tee.pos(), Sound::NinjaHit);
let _ = dash.hit.try_push(other_tee_uid);
}
} else {
tee_core.set_vel(dash.activation_dir * dash.old_vel_amount as f32);
self.dash = None;
}
}
}
}