Weapons and npcs pt. 3
This commit is contained in:
parent
f2dffd38e7
commit
639c1857c8
@ -24,6 +24,10 @@ std::shared_ptr<assets::Item> assets::Item::LoadFromFile(const std::string& path
|
||||
{
|
||||
iss >> item->name;
|
||||
}
|
||||
else if (command == "displayname")
|
||||
{
|
||||
item->displayname = ParseString(iss);
|
||||
}
|
||||
else if (command == "anim")
|
||||
{
|
||||
std::string anim_type, anim_name;
|
||||
@ -146,11 +150,20 @@ std::shared_ptr<assets::Item> assets::Item::LoadFromFile(const std::string& path
|
||||
{
|
||||
iss >> item->walk_speed_mult;
|
||||
}
|
||||
else if (command == "damage")
|
||||
{
|
||||
iss >> item->damage;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Unknown item command: " + command);
|
||||
}
|
||||
});
|
||||
|
||||
if (item->displayname.empty())
|
||||
{
|
||||
item->displayname = item->name;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
@ -40,6 +40,7 @@ struct Item
|
||||
{
|
||||
ItemType type = ITEM_NONE;
|
||||
std::string name;
|
||||
std::string displayname;
|
||||
|
||||
size_t slot = 0;
|
||||
|
||||
@ -71,6 +72,7 @@ struct Item
|
||||
float dispersion_max = 0.0f;
|
||||
float dispersion_shot = 0.0f;
|
||||
float dispersion_decay = 1.0f;
|
||||
float damage = 1.0f;
|
||||
|
||||
std::string aim_anim;
|
||||
std::string aiming_anim;
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
|
||||
namespace game
|
||||
{
|
||||
struct BulletInfo;
|
||||
struct DamageInfo;
|
||||
class HumanCharacter;
|
||||
}
|
||||
|
||||
namespace collision
|
||||
@ -42,6 +43,7 @@ enum ObjectFlag : ObjectFlags
|
||||
OF_NOTIFY_CONTACT = 2,
|
||||
OF_USABLE = 4,
|
||||
OF_DESTRUCTING = 8,
|
||||
OF_CRASH_DAMAGE = 16,
|
||||
};
|
||||
|
||||
struct ContactInfo
|
||||
@ -59,8 +61,9 @@ public:
|
||||
virtual void ActivateHitBones() {}
|
||||
|
||||
virtual void OnContact(const ContactInfo& info) {}
|
||||
virtual void OnBulletHit(const game::BulletInfo& bullet, const btCollisionObject* hit_object) {}
|
||||
virtual void ReceiveDamage(const game::DamageInfo& damage) {}
|
||||
|
||||
virtual game::HumanCharacter* GetResponsibleCharacter() { return nullptr; }
|
||||
|
||||
virtual ~ObjectCallback() = default;
|
||||
};
|
||||
|
||||
@ -22,6 +22,13 @@ void game::Animal::Update()
|
||||
Super::Update();
|
||||
}
|
||||
|
||||
void game::Animal::ReceiveDamage(const DamageInfo& damage)
|
||||
{
|
||||
Super::ReceiveDamage(damage);
|
||||
just_hit_ = true;
|
||||
hit_from_ = damage.from_pos;
|
||||
}
|
||||
|
||||
bool game::Animal::QueryUseTarget(PlayerCharacter& character, uint32_t target_id, UseTargetQueryResult& res)
|
||||
{
|
||||
if (character.GetRideable())
|
||||
@ -60,13 +67,6 @@ void game::Animal::SetRideableViewAngles(float yaw, float pitch)
|
||||
}
|
||||
}
|
||||
|
||||
void game::Animal::OnBulletHit(const game::BulletInfo& bullet, const std::string_view hit_bone)
|
||||
{
|
||||
just_hit_ = true;
|
||||
// attacker_ = bullet.shooter->GetEntNum();
|
||||
hit_from_ = bullet.start;
|
||||
}
|
||||
|
||||
void game::Animal::OnPassengerChanged(size_t seat_idx, HumanCharacter* passenger)
|
||||
{
|
||||
if (seat_idx == 0 && !passenger)
|
||||
|
||||
@ -25,6 +25,8 @@ public:
|
||||
|
||||
virtual void Update() override;
|
||||
|
||||
virtual void ReceiveDamage(const DamageInfo& damage) override;
|
||||
|
||||
virtual bool QueryUseTarget(PlayerCharacter& character, uint32_t target_id, UseTargetQueryResult& res) override;
|
||||
virtual void Use(PlayerCharacter& character, uint32_t target_id) override;
|
||||
|
||||
@ -32,8 +34,6 @@ public:
|
||||
virtual void SetRideableViewAngles(float yaw, float pitch) override;
|
||||
|
||||
protected:
|
||||
virtual void OnBulletHit(const game::BulletInfo& bullet, const std::string_view hit_bone) override;
|
||||
|
||||
virtual void OnPassengerChanged(size_t seat_idx, HumanCharacter* passenger) override;
|
||||
void SetUseMessage(const std::string& message);
|
||||
void AddAnimalSeat(const glm::vec3& offset);
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
#include "character.hpp"
|
||||
|
||||
#include <format>
|
||||
|
||||
#include "assets/cache.hpp"
|
||||
#include "net/utils.hpp"
|
||||
#include "utils/math.hpp"
|
||||
#include "world.hpp"
|
||||
#include "utils/random.hpp"
|
||||
|
||||
|
||||
game::Character::Character(World& world, const CharacterTuning& tuning)
|
||||
: Super(world, net::ET_CHARACTER), tuning_(tuning), bt_shape_(tuning_.shape.radius, tuning_.shape.height)
|
||||
@ -43,6 +48,8 @@ void game::Character::Update()
|
||||
SyncTransformFromController();
|
||||
UpdateMovement();
|
||||
UpdateAiming();
|
||||
UpdatePain();
|
||||
UpdateAnimAngles();
|
||||
UpdateActionAnim();
|
||||
root_.UpdateMatrix();
|
||||
UpdateHitBones();
|
||||
@ -77,20 +84,47 @@ void game::Character::SendInitData(Player& player, net::OutMessage& msg) const
|
||||
msg.WriteAt(fields_pos, fields);
|
||||
}
|
||||
|
||||
void game::Character::OnBulletHit(const game::BulletInfo& bullet, const btCollisionObject* hit_object)
|
||||
void game::Character::ReceiveDamage(const DamageInfo& damage)
|
||||
{
|
||||
std::string_view hit_name = "???";
|
||||
Super::ReceiveDamage(damage);
|
||||
|
||||
auto it = hitbone_names_.find(hit_object);
|
||||
if (it != hitbone_names_.end())
|
||||
if (!IsAlive())
|
||||
{
|
||||
hit_name = it->second;
|
||||
return; // already ded
|
||||
}
|
||||
|
||||
std::string text = "au! " + std::string(hit_name);
|
||||
GetWorld().SendChat(text);
|
||||
float actual_damage = damage.damage;
|
||||
|
||||
OnBulletHit(bullet, hit_name);
|
||||
if (damage.type == DAMAGE_BULLET)
|
||||
{
|
||||
std::string_view hit_name;
|
||||
|
||||
auto it = hitbone_names_.find(damage.hit_object);
|
||||
if (it != hitbone_names_.end())
|
||||
{
|
||||
hit_name = it->second;
|
||||
}
|
||||
|
||||
actual_damage *= GetHitBoneDamageMultiplier(hit_name);
|
||||
|
||||
}
|
||||
|
||||
health_ -= actual_damage;
|
||||
|
||||
// just died
|
||||
if (health_ <= 0.0f)
|
||||
{
|
||||
health_ = 0.0f;
|
||||
death_time_ = GetWorld().GetTime();
|
||||
|
||||
if (on_death_)
|
||||
on_death_();
|
||||
}
|
||||
|
||||
ApplyPain();
|
||||
|
||||
// std::string text = "au! " + std::string(hit_name);
|
||||
// GetWorld().SendChat(text);
|
||||
}
|
||||
|
||||
void game::Character::Attach(net::EntNum parentnum)
|
||||
@ -157,6 +191,11 @@ void game::Character::FinalizeFrame()
|
||||
hitbones_valid_ = false;
|
||||
}
|
||||
|
||||
int64_t game::Character::GetDeathTime() const
|
||||
{
|
||||
return GetWorld().GetTime() - death_time_;
|
||||
}
|
||||
|
||||
game::Character::~Character()
|
||||
{
|
||||
DeleteHitBones();
|
||||
@ -228,6 +267,23 @@ void game::Character::SendFire()
|
||||
auto msg = BeginEntMsg(net::EMSG_FIRE);
|
||||
}
|
||||
|
||||
void game::Character::ApplyPain()
|
||||
{
|
||||
pain_pitch_ = glm::clamp(pain_pitch_ + RandomFloat(glm::radians(-5.0f), glm::radians(20.0f)), glm::radians(-30.0f), glm::radians(30.0f));
|
||||
pain_yaw_ = glm::clamp(pain_yaw_ + RandomFloat(glm::radians(-20.0f), glm::radians(20.0f)), glm::radians(-30.0f), glm::radians(30.0f));
|
||||
}
|
||||
|
||||
float game::Character::GetHitBoneDamageMultiplier(const std::string_view hitbone)
|
||||
{
|
||||
if (hitbone == "head" || hitbone == "neck")
|
||||
return 3.0f;
|
||||
|
||||
if (hitbone == "torso1" || hitbone == "torso2")
|
||||
return 1.0f;
|
||||
|
||||
return 0.2f;
|
||||
}
|
||||
|
||||
void game::Character::SyncControllerTransform()
|
||||
{
|
||||
if (!controller_)
|
||||
@ -334,8 +390,8 @@ void game::Character::UpdateAiming()
|
||||
if (!aiming_)
|
||||
{
|
||||
delta = 6.0f / 25.0f;
|
||||
MoveToward(animstate_.yaw, 0.0f, delta);
|
||||
MoveToward(animstate_.pitch, 0.0f, delta);
|
||||
MoveToward(aim_yaw_, 0.0f, delta);
|
||||
MoveToward(aim_pitch_, 0.0f, delta);
|
||||
UpdateAimDirection();
|
||||
return;
|
||||
}
|
||||
@ -359,19 +415,19 @@ void game::Character::UpdateAiming()
|
||||
float yaw = glm::atan(-dir.x, dir.y);
|
||||
|
||||
auto target_pitch = glm::clamp(pitch, glm::radians(-60.0f), glm::radians(55.0f)); // clamp to make it less weird
|
||||
MoveToward(animstate_.pitch, target_pitch, delta);
|
||||
MoveToward(aim_pitch_, target_pitch, delta);
|
||||
|
||||
if (movement_ == CMT_DISABLED)
|
||||
{
|
||||
auto target_yaw = glm::mod(yaw + glm::pi<float>(), glm::two_pi<float>()) - glm::pi<float>();
|
||||
const float yaw_limit = glm::radians(120.0f);
|
||||
target_yaw = glm::clamp(target_yaw, -yaw_limit, yaw_limit);
|
||||
MoveToward(animstate_.yaw, target_yaw, delta);
|
||||
MoveToward(aim_yaw_, target_yaw, delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
Turn(yaw_, yaw, delta);
|
||||
MoveToward(animstate_.yaw, 0.0f, delta);
|
||||
MoveToward(aim_yaw_, 0.0f, delta);
|
||||
}
|
||||
|
||||
UpdateAimDirection();
|
||||
@ -381,8 +437,8 @@ void game::Character::UpdateAimDirection()
|
||||
{
|
||||
eye_pos_ = GetRoot().matrix * glm::vec4(0.0f, 0.0f, aim_z_offset_, 1.0f);
|
||||
|
||||
auto pitch = animstate_.pitch;
|
||||
auto yaw = yaw_ + animstate_.yaw;
|
||||
auto pitch = aim_pitch_;
|
||||
auto yaw = yaw_ + aim_yaw_;
|
||||
aim_dir_ = glm::vec3(-glm::sin(yaw) * glm::cos(pitch), glm::cos(yaw) * glm::cos(pitch), glm::sin(pitch));
|
||||
|
||||
if (parent_)
|
||||
@ -393,6 +449,19 @@ void game::Character::UpdateAimDirection()
|
||||
// GetWorld().Beam(eye_pos_, eye_pos_ + aim_dir_ * 100.0f, 0x0000FF, 1.0f / 25.0f);
|
||||
}
|
||||
|
||||
void game::Character::UpdatePain()
|
||||
{
|
||||
float delta = 1.5f / 25.0f;
|
||||
MoveToward(pain_yaw_, 0.0f, delta);
|
||||
MoveToward(pain_pitch_, 0.0f, delta);
|
||||
}
|
||||
|
||||
void game::Character::UpdateAnimAngles()
|
||||
{
|
||||
animstate_.yaw = aim_yaw_ + pain_yaw_;
|
||||
animstate_.pitch = aim_pitch_ + pain_pitch_;
|
||||
}
|
||||
|
||||
void game::Character::UpdateSyncState()
|
||||
{
|
||||
auto& state = sync_[sync_current_];
|
||||
@ -684,7 +753,7 @@ game::CharacterPhysicsController::CharacterPhysicsController(Character& characte
|
||||
bt_ghost_.setCollisionShape(&bt_shape);
|
||||
bt_ghost_.setCollisionFlags(btCollisionObject::CF_CHARACTER_OBJECT);
|
||||
|
||||
collision::SetObjectInfo(&bt_ghost_, collision::OT_ENTITY, 0, &character);
|
||||
collision::SetObjectInfo(&bt_ghost_, collision::OT_ENTITY, collision::OF_CRASH_DAMAGE, &character);
|
||||
|
||||
bt_world_.addCollisionObject(&bt_ghost_, btBroadphaseProxy::CharacterFilter,
|
||||
btBroadphaseProxy::StaticFilter | btBroadphaseProxy::DefaultFilter);
|
||||
|
||||
@ -67,8 +67,8 @@ public:
|
||||
|
||||
virtual void Update() override;
|
||||
virtual void SendInitData(Player& player, net::OutMessage& msg) const override;
|
||||
|
||||
virtual void OnBulletHit(const game::BulletInfo& bullet, const btCollisionObject* hit_object);
|
||||
|
||||
virtual void ReceiveDamage(const DamageInfo& damage) override;
|
||||
|
||||
virtual void Attach(net::EntNum parentnum) override;
|
||||
|
||||
@ -99,6 +99,12 @@ public:
|
||||
virtual void ActivateHitBones() override;
|
||||
virtual void FinalizeFrame() override;
|
||||
|
||||
float GetHealth() const { return health_; }
|
||||
bool IsAlive() const { return death_time_ < 0; }
|
||||
int64_t GetDeathTime() const;
|
||||
|
||||
void SetOnDeath(std::function<void()> cb) { on_death_ = std::move(cb); }
|
||||
|
||||
~Character() override;
|
||||
|
||||
protected:
|
||||
@ -115,8 +121,9 @@ protected:
|
||||
void SetAimTarget(const glm::vec3& target);
|
||||
void SetViewItem(const std::string& item_name);
|
||||
void SendFire();
|
||||
void ApplyPain();
|
||||
|
||||
virtual void OnBulletHit(const game::BulletInfo& bullet, const std::string_view hit_bone) {}
|
||||
virtual float GetHitBoneDamageMultiplier(const std::string_view hitbone);
|
||||
|
||||
private:
|
||||
void SyncControllerTransform();
|
||||
@ -125,6 +132,8 @@ private:
|
||||
void UpdateMovement();
|
||||
void UpdateAiming();
|
||||
void UpdateAimDirection();
|
||||
void UpdatePain();
|
||||
void UpdateAnimAngles();
|
||||
void UpdateSyncState();
|
||||
void SendUpdateMsg();
|
||||
CharacterSyncFieldFlags WriteState(net::OutMessage& msg, const CharacterSyncState& base) const;
|
||||
@ -165,6 +174,12 @@ private:
|
||||
float view_yaw_ = 0.0f;
|
||||
float view_pitch_ = 0.0f;
|
||||
|
||||
float aim_yaw_ = 0.0f;
|
||||
float aim_pitch_ = 0.0f;
|
||||
|
||||
float pain_yaw_ = 0.0f;
|
||||
float pain_pitch_ = 0.0f;
|
||||
|
||||
SkeletonInstance sk_;
|
||||
CharacterAnimState animstate_;
|
||||
|
||||
@ -193,6 +208,11 @@ private:
|
||||
size_t hitbones_timer_ = 0;
|
||||
bool hitbones_valid_ = false;
|
||||
std::map<const btCollisionObject*, std::string_view> hitbone_names_;
|
||||
|
||||
float health_ = 100.0f;
|
||||
int64_t death_time_ = -1;
|
||||
|
||||
std::function<void()> on_death_;
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
@ -3,7 +3,7 @@
|
||||
#include "utils/random.hpp"
|
||||
#include "input_mapping.hpp"
|
||||
|
||||
game::DrivableVehicle::DrivableVehicle(World& world, const VehicleTuning& tuning) : Vehicle(world, tuning), Usable(GetRoot().matrix), Rideable(*this, RIDEABLE_VEHICLE)
|
||||
game::DrivableVehicle::DrivableVehicle(World& world, const VehicleSpawnInfo& info) : Vehicle(world, info), Usable(GetRoot().matrix), Rideable(*this, RIDEABLE_VEHICLE)
|
||||
{
|
||||
InitSeats();
|
||||
OnPhysicsChanged();
|
||||
@ -18,6 +18,11 @@ void game::DrivableVehicle::Update()
|
||||
|
||||
}
|
||||
|
||||
game::HumanCharacter* game::DrivableVehicle::GetResponsibleCharacter()
|
||||
{
|
||||
return GetPassenger(0);
|
||||
}
|
||||
|
||||
void game::DrivableVehicle::SetTuning(const VehicleTuning& tuning)
|
||||
{
|
||||
Super::SetTuning(tuning);
|
||||
@ -53,6 +58,12 @@ void game::DrivableVehicle::Use(PlayerCharacter& character, uint32_t target_id)
|
||||
PlaySound("cardoor", 1.0f, RandomFloat(0.9f, 1.1f));
|
||||
}
|
||||
|
||||
void game::DrivableVehicle::ReceiveDamage(const DamageInfo& damage)
|
||||
{
|
||||
Super::ReceiveDamage(damage);
|
||||
OnRideableDamaged(damage);
|
||||
}
|
||||
|
||||
void game::DrivableVehicle::SetRideableInput(PlayerInputFlags in)
|
||||
{
|
||||
SetInputs(MapPlayerInputToVehicleInput(in));
|
||||
|
||||
@ -13,10 +13,12 @@ class DrivableVehicle : public Vehicle, public Usable, public Rideable
|
||||
public:
|
||||
using Super = Vehicle;
|
||||
|
||||
DrivableVehicle(World& world, const VehicleTuning& tuning);
|
||||
DrivableVehicle(World& world, const VehicleSpawnInfo& info);
|
||||
|
||||
virtual void Update() override;
|
||||
|
||||
virtual HumanCharacter* GetResponsibleCharacter() override;
|
||||
|
||||
virtual void SetTuning(const VehicleTuning& tuning) override;
|
||||
|
||||
virtual void OnPhysicsChanged() override;
|
||||
@ -24,6 +26,8 @@ public:
|
||||
virtual bool QueryUseTarget(PlayerCharacter& character, uint32_t target_id, UseTargetQueryResult& res) override;
|
||||
virtual void Use(PlayerCharacter& character, uint32_t target_id) override;
|
||||
|
||||
virtual void ReceiveDamage(const DamageInfo& damage) override;
|
||||
|
||||
virtual void SetRideableInput(PlayerInputFlags in) override;
|
||||
|
||||
protected:
|
||||
|
||||
@ -18,6 +18,20 @@ void game::EnterableWorld::PlayerInput(Player& player, PlayerInputType type, boo
|
||||
|
||||
auto character = it->second;
|
||||
|
||||
// check respawn
|
||||
if (type == IN_ATTACK_PRIMARY && enabled)
|
||||
{
|
||||
auto current_character = GetPlayerCharacter(player);
|
||||
|
||||
if (current_character->IsDead() && current_character->GetDeathTime() >= 3000)
|
||||
{
|
||||
auto& tuning = current_character->GetHumanTuning();
|
||||
CreatePlayerCharacter(player, tuning, spawnpoint_, 0.0f);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
// case IN_DEBUG1:
|
||||
|
||||
@ -27,13 +27,16 @@ public:
|
||||
|
||||
PlayerCharacter* GetPlayerCharacter(Player& player);
|
||||
|
||||
void SetSpawnPoint(const glm::vec3& pos) { spawnpoint_ = pos; }
|
||||
const glm::vec3& GetSpawnPoint() const { return spawnpoint_; }
|
||||
|
||||
private:
|
||||
PlayerCharacter& CreatePlayerCharacter(Player& player, const HumanCharacterTuning& tuning, const glm::vec3& position, float yaw);
|
||||
void RemovePlayerCharacter(Player& player);
|
||||
|
||||
private:
|
||||
std::map<Player*, PlayerCharacter*> player_characters_;
|
||||
|
||||
glm::vec3 spawnpoint_{};
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -4,9 +4,6 @@
|
||||
#include "player.hpp"
|
||||
#include "player_character.hpp"
|
||||
|
||||
static constexpr glm::vec3 openworld_spawn(100.0f, 100.0f, 1.0f);
|
||||
static constexpr glm::vec3 test_spawn(0.0f, 0.0f, 0.1f);
|
||||
|
||||
static uint32_t GetRandomColor24()
|
||||
{
|
||||
uint8_t r, g, b;
|
||||
@ -56,7 +53,7 @@ void game::Game::PlayerJoined(Player& player)
|
||||
tuning.clothes.push_back({"tshirt", GetRandomColor24()});
|
||||
tuning.clothes.push_back({"shorts", GetRandomColor24()});
|
||||
|
||||
openworld_->InsertPlayer(player, tuning, openworld_spawn, 0.0f);
|
||||
openworld_->InsertPlayer(player, tuning, openworld_->GetSpawnPoint(), 0.0f);
|
||||
}
|
||||
|
||||
void game::Game::PlayerViewAnglesChanged(Player& player, float yaw, float pitch)
|
||||
@ -158,18 +155,19 @@ game::PlayerCharacter& game::Game::MovePlayerToWorld(PlayerGameInfo& player_info
|
||||
void game::Game::MoveVehicleToWorld(DrivableVehicle& vehicle, EnterableWorld& new_world, const glm::vec3& pos,
|
||||
float yaw)
|
||||
{
|
||||
auto& tuning = vehicle.GetTuning();
|
||||
auto& new_vehicle = new_world.Spawn<DrivableVehicle>(tuning);
|
||||
new_vehicle.SetPosition(pos);
|
||||
// TODO: yaw
|
||||
|
||||
VehicleSpawnInfo vehicle_info{};
|
||||
vehicle_info.tuning = vehicle.GetTuning();
|
||||
vehicle_info.position = pos;
|
||||
vehicle_info.yaw = yaw;
|
||||
auto& new_vehicle = new_world.Spawn<DrivableVehicle>(vehicle_info);
|
||||
|
||||
// move passengers
|
||||
size_t num_seats = vehicle.GetNumSeats();
|
||||
for (size_t i = 0; i < num_seats; ++i)
|
||||
{
|
||||
auto passenger = vehicle.GetPassenger(i);
|
||||
if (!passenger)
|
||||
continue; // empty seat
|
||||
if (!passenger || !passenger->IsAlive())
|
||||
continue; // empty seat or ded
|
||||
|
||||
auto player_passenger = dynamic_cast<PlayerCharacter*>(passenger);
|
||||
if (!player_passenger)
|
||||
|
||||
@ -26,6 +26,19 @@ void game::HumanCharacter::Update()
|
||||
Super::Update();
|
||||
}
|
||||
|
||||
void game::HumanCharacter::ReceiveDamage(const DamageInfo& damage)
|
||||
{
|
||||
if (!IsAlive())
|
||||
return;
|
||||
|
||||
Super::ReceiveDamage(damage);
|
||||
|
||||
if (damage.inflictor && damage.inflictor != this)
|
||||
{
|
||||
damage.inflictor->OnDamageDealt(!IsAlive());
|
||||
}
|
||||
}
|
||||
|
||||
void game::HumanCharacter::SetRideable(Rideable* rideable, size_t seat_idx)
|
||||
{
|
||||
if (rideable == rideable_ && seat_idx == seat_idx_)
|
||||
@ -66,9 +79,17 @@ void game::HumanCharacter::Ride(Rideable* rideable, size_t seat_idx)
|
||||
|
||||
void game::HumanCharacter::Equip(std::shared_ptr<ItemInstance> item)
|
||||
{
|
||||
if (!IsAlive() && item)
|
||||
return;
|
||||
|
||||
pending_item_ = std::move(item);
|
||||
}
|
||||
|
||||
bool game::HumanCharacter::IsDead() const
|
||||
{
|
||||
return actionstate_ == ACTION_DEAD;
|
||||
}
|
||||
|
||||
game::HumanCharacter::~HumanCharacter()
|
||||
{
|
||||
Ride(nullptr, 0); // exit rideable
|
||||
@ -84,6 +105,11 @@ size_t game::HumanCharacter::GetAmmo(size_t required, const std::string& ammo_na
|
||||
return required; // unlimited by default
|
||||
}
|
||||
|
||||
bool game::HumanCharacter::IsOnFoot() const
|
||||
{
|
||||
return state_ == HS_ON_FOOT;
|
||||
}
|
||||
|
||||
int64_t game::HumanCharacter::GetTime() const
|
||||
{
|
||||
return GetWorld().GetTime();
|
||||
@ -122,7 +148,7 @@ void game::HumanCharacter::Fire()
|
||||
game::BulletInfo bullet{};
|
||||
bullet.start = GetEyePosition();
|
||||
bullet.end = bullet.start + ApplyRandomDispersion(GetAimDirection(), dispersion_) * range;
|
||||
bullet.damage = 1.0f;
|
||||
bullet.damage = item_->def->damage;
|
||||
bullet.shooter = this;
|
||||
GetWorld().FireBullet(bullet);
|
||||
|
||||
@ -197,6 +223,13 @@ void game::HumanCharacter::UpdateItemStuff()
|
||||
}
|
||||
}
|
||||
|
||||
void game::HumanCharacter::ClearItem()
|
||||
{
|
||||
auto item = item_;
|
||||
Equip(nullptr);
|
||||
SwitchItem();
|
||||
}
|
||||
|
||||
void game::HumanCharacter::PlayItemActionAnim(const std::string assets::Item::*anim, float speed)
|
||||
{
|
||||
if (!item_)
|
||||
@ -208,6 +241,34 @@ void game::HumanCharacter::PlayItemActionAnim(const std::string assets::Item::*a
|
||||
PlayActionAnim(item_->def.get()->*anim, speed);
|
||||
}
|
||||
|
||||
void game::HumanCharacter::PlayDeathAnim()
|
||||
{
|
||||
if (state_ == HS_ON_FOOT)
|
||||
{
|
||||
PlayActionAnim("die", 1.5f);
|
||||
}
|
||||
else if (state_ == HS_RIDING)
|
||||
{
|
||||
if (GetVehicle())
|
||||
{
|
||||
PlayActionAnim(IsDriver() ? "vehicle_drive_die" : "vehicle_passenger_die", 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayActionAnim("vehicle_passenger_die_animal", 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void game::HumanCharacter::TrySpawnLoot()
|
||||
{
|
||||
if (loot_spawned_)
|
||||
return;
|
||||
|
||||
loot_spawned_ = true;
|
||||
SpawnLoot();
|
||||
}
|
||||
|
||||
void game::HumanCharacter::UpdateState()
|
||||
{
|
||||
struct HumanCharacterStateTableEntry
|
||||
@ -309,7 +370,7 @@ game::HumanCharacterState game::HumanCharacter::StateOnFootUpdate()
|
||||
if (PopSignal(HSS_KNOCK_DOWN))
|
||||
return HS_KNOCKED_DOWN;
|
||||
|
||||
SetMovementType(aimheld_ ? CMT_DIRECTIONAL : CMT_TURN);
|
||||
SetMovementType(IsAlive() ? (aimheld_ ? CMT_DIRECTIONAL : CMT_TURN) : CMT_DISABLED);
|
||||
|
||||
return HS_ON_FOOT;
|
||||
}
|
||||
@ -432,6 +493,18 @@ void game::HumanCharacter::EnterActionState(ActionState state)
|
||||
PlayItemActionAnim(&assets::Item::raise_anim, -3.0f);
|
||||
break;
|
||||
|
||||
case ACTION_DIE:
|
||||
SetAiming(false);
|
||||
SetCanSprint(false);
|
||||
PlayDeathAnim();
|
||||
break;
|
||||
|
||||
case ACTION_DEAD:
|
||||
EnablePhysics(false);
|
||||
ClearItem();
|
||||
TrySpawnLoot();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -447,6 +520,9 @@ game::ActionState game::HumanCharacter::CheckActionStateTransition()
|
||||
switch (actionstate_)
|
||||
{
|
||||
case ACTION_IDLE:
|
||||
if (!IsAlive())
|
||||
return ACTION_DIE;
|
||||
|
||||
if (PendingItemSwitch())
|
||||
return ACTION_PUTAWAY;
|
||||
|
||||
@ -459,6 +535,9 @@ game::ActionState game::HumanCharacter::CheckActionStateTransition()
|
||||
return ACTION_IDLE;
|
||||
|
||||
case ACTION_RAISE:
|
||||
if (!IsAlive())
|
||||
return ACTION_DIE;
|
||||
|
||||
if (PendingItemSwitch())
|
||||
return ACTION_PUTAWAY;
|
||||
|
||||
@ -468,6 +547,9 @@ game::ActionState game::HumanCharacter::CheckActionStateTransition()
|
||||
return ACTION_RAISE;
|
||||
|
||||
case ACTION_AIM:
|
||||
if (!IsAlive())
|
||||
return ACTION_DIE;
|
||||
|
||||
if (!aimheld_ || !CanAim())
|
||||
return ACTION_UNAIM;
|
||||
|
||||
@ -477,6 +559,9 @@ game::ActionState game::HumanCharacter::CheckActionStateTransition()
|
||||
return ACTION_AIM;
|
||||
|
||||
case ACTION_AIMING:
|
||||
if (!IsAlive())
|
||||
return ACTION_DIE;
|
||||
|
||||
if (!aimheld_ || !CanAim())
|
||||
return ACTION_UNAIM;
|
||||
|
||||
@ -486,6 +571,9 @@ game::ActionState game::HumanCharacter::CheckActionStateTransition()
|
||||
return ACTION_AIMING;
|
||||
|
||||
case ACTION_FIRE:
|
||||
if (!IsAlive())
|
||||
return ACTION_DIE;
|
||||
|
||||
if (IsActionAnimDone())
|
||||
return ACTION_AIMING;
|
||||
|
||||
@ -502,6 +590,9 @@ game::ActionState game::HumanCharacter::CheckActionStateTransition()
|
||||
return ACTION_FIRE_REPEAT;
|
||||
|
||||
case ACTION_RELOAD:
|
||||
if (!IsAlive())
|
||||
return ACTION_DIE;
|
||||
|
||||
// SetAiming(aimheld_); // optional here
|
||||
if (IsActionAnimDone())
|
||||
{
|
||||
@ -512,6 +603,9 @@ game::ActionState game::HumanCharacter::CheckActionStateTransition()
|
||||
return ACTION_RELOAD;
|
||||
|
||||
case ACTION_UNAIM:
|
||||
if (!IsAlive())
|
||||
return ACTION_DIE;
|
||||
|
||||
if (aimheld_ && CanAim()) // start aiming again
|
||||
return ACTION_AIM;
|
||||
|
||||
@ -521,6 +615,9 @@ game::ActionState game::HumanCharacter::CheckActionStateTransition()
|
||||
return ACTION_UNAIM;
|
||||
|
||||
case ACTION_PUTAWAY:
|
||||
if (!IsAlive())
|
||||
return ACTION_DIE;
|
||||
|
||||
if (IsActionAnimDone())
|
||||
return ACTION_RAISE;
|
||||
|
||||
@ -529,6 +626,15 @@ game::ActionState game::HumanCharacter::CheckActionStateTransition()
|
||||
|
||||
return ACTION_PUTAWAY;
|
||||
|
||||
case ACTION_DIE:
|
||||
if (IsActionAnimDone())
|
||||
return ACTION_DEAD;
|
||||
|
||||
return ACTION_DIE;
|
||||
|
||||
case ACTION_DEAD:
|
||||
return ACTION_DEAD; // no way back :(
|
||||
|
||||
default:
|
||||
return actionstate_;
|
||||
}
|
||||
|
||||
@ -41,6 +41,8 @@ enum ActionState
|
||||
ACTION_RELOAD,
|
||||
ACTION_UNAIM,
|
||||
ACTION_PUTAWAY,
|
||||
ACTION_DIE,
|
||||
ACTION_DEAD,
|
||||
};
|
||||
|
||||
class HumanCharacter : public Character
|
||||
@ -51,6 +53,7 @@ public:
|
||||
HumanCharacter(World& world, const HumanCharacterTuning& tuning);
|
||||
|
||||
virtual void Update() override;
|
||||
virtual void ReceiveDamage(const DamageInfo& damage) override;
|
||||
|
||||
const HumanCharacterTuning& GetHumanTuning() const { return human_tuning_; }
|
||||
|
||||
@ -70,6 +73,12 @@ public:
|
||||
void Equip(std::shared_ptr<ItemInstance> item);
|
||||
const std::shared_ptr<ItemInstance>& GetHeldItem() const { return item_; }
|
||||
|
||||
virtual void OnRideableDamaged(const DamageInfo& damage) {}
|
||||
|
||||
virtual void OnDamageDealt(bool was_kill) {}
|
||||
|
||||
bool IsDead() const;
|
||||
|
||||
virtual ~HumanCharacter() override;
|
||||
|
||||
protected:
|
||||
@ -78,8 +87,11 @@ protected:
|
||||
virtual void OnHeldItemChanged() {}
|
||||
virtual bool HaveAmmo(const std::string& ammo_name);
|
||||
virtual size_t GetAmmo(size_t required, const std::string& ammo_name);
|
||||
virtual void SpawnLoot() {}
|
||||
|
||||
private:
|
||||
bool IsOnFoot() const;
|
||||
|
||||
protected:
|
||||
int64_t GetTime() const;
|
||||
bool CanAim();
|
||||
void SetAiming(bool aiming);
|
||||
@ -91,7 +103,10 @@ private:
|
||||
bool PendingItemSwitch();
|
||||
void SwitchItem();
|
||||
void UpdateItemStuff();
|
||||
void ClearItem();
|
||||
void PlayItemActionAnim(const std::string assets::Item::*anim, float speed = 1.0f);
|
||||
void PlayDeathAnim();
|
||||
void TrySpawnLoot();
|
||||
|
||||
void UpdateState();
|
||||
void SetSignal(HumanCharacterStateSignal signal);
|
||||
@ -146,6 +161,8 @@ private:
|
||||
|
||||
int64_t last_fire_time_ = 0;
|
||||
float dispersion_ = 0.0f;
|
||||
|
||||
bool loot_spawned_ = false;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,77 +1,274 @@
|
||||
#include "npc_character.hpp"
|
||||
#include "openworld.hpp"
|
||||
#include "drivable_vehicle.hpp"
|
||||
#include "utils/random.hpp"
|
||||
#include "player_character.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
|
||||
game::NpcCharacter::NpcCharacter(World& world, const HumanCharacterTuning& tuning) : Super(world, tuning) {
|
||||
UpdateVehicleState();
|
||||
static constexpr size_t PATH_NEXT_WAYPOINTS = 16;
|
||||
|
||||
game::NpcCharacter::NpcCharacter(World& world, const HumanCharacterTuning& tuning) : Super(world, tuning)
|
||||
{
|
||||
roads_ = world_.GetMap().GetGraph("roads");
|
||||
|
||||
EnterThinkState(THINKSTATE_IDLE);
|
||||
EnterDriverThinkState(DRIVERSTATE_NONE);
|
||||
}
|
||||
|
||||
void game::NpcCharacter::Update()
|
||||
{
|
||||
UpdateEnemy();
|
||||
Think();
|
||||
DriverThink();
|
||||
Super::Update();
|
||||
}
|
||||
|
||||
if (GetVehicle() && IsDriver())
|
||||
VehicleThink();
|
||||
void game::NpcCharacter::ReceiveDamage(const DamageInfo& damage)
|
||||
{
|
||||
Super::ReceiveDamage(damage);
|
||||
|
||||
if (IsAlive() && damage.inflictor)
|
||||
{
|
||||
MakeEnemy(damage.inflictor->GetEntNum());
|
||||
}
|
||||
}
|
||||
|
||||
void game::NpcCharacter::SetWeapon(std::shared_ptr<ItemInstance> weapon)
|
||||
{
|
||||
weapon_ = std::move(weapon);
|
||||
}
|
||||
|
||||
bool game::NpcCharacter::IsBored(int64_t time) const
|
||||
{
|
||||
return (think_state_ == THINKSTATE_IDLE && GetCurrentThinkStateDuration() > time &&
|
||||
driver_state_ == DRIVERSTATE_NONE && GetCurrentDriverThinkStateDuration() > time && !GetRideable());
|
||||
}
|
||||
|
||||
void game::NpcCharacter::Die()
|
||||
{
|
||||
DamageInfo damage{};
|
||||
damage.type = DAMAGE_OTHER;
|
||||
damage.damage = 100000.0f;
|
||||
ReceiveDamage(damage);
|
||||
}
|
||||
|
||||
void game::NpcCharacter::OnRideableChanged()
|
||||
{
|
||||
UpdateVehicleState();
|
||||
EnterThinkState(THINKSTATE_IDLE);
|
||||
}
|
||||
|
||||
void game::NpcCharacter::UpdateVehicleState()
|
||||
void game::NpcCharacter::OnRideableDamaged(const DamageInfo& damage)
|
||||
{
|
||||
roads_ = nullptr;
|
||||
path_.clear();
|
||||
gas_ = false;
|
||||
stuck_counter_ = 0;
|
||||
vehicle_state_ = NVT_NORMAL;
|
||||
speed_limit_ = 0.0f;
|
||||
|
||||
if (GetVehicle() && IsDriver())
|
||||
if (damage.inflictor)
|
||||
{
|
||||
roads_ = world_.GetMap().GetGraph("roads");
|
||||
MakeEnemy(damage.inflictor->GetEntNum());
|
||||
}
|
||||
}
|
||||
|
||||
seg_start_ = GetVehicle()->GetRootTransform().position;
|
||||
|
||||
size_t start_node = 0;
|
||||
float min_dist = std::numeric_limits<float>().infinity();
|
||||
void game::NpcCharacter::SpawnLoot()
|
||||
{
|
||||
if (weapon_)
|
||||
{
|
||||
size_t ammo = weapon_->def->clip_size * 5;
|
||||
GetWorld().CreateItemPickup(root_.GetGlobalPosition(), std::move(weapon_), 60000, 0, ammo);
|
||||
}
|
||||
}
|
||||
|
||||
void game::NpcCharacter::MakeEnemy(net::EntNum enemy_num)
|
||||
{
|
||||
// it must have been a mistake
|
||||
if (Chance(0.1f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// cant switch enemies that fast
|
||||
auto time = GetWorld().GetTime();
|
||||
if (enemy_num != enemy_num_ && time - enemy_time_ < 5000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// increase anger
|
||||
if (Chance(0.7f))
|
||||
{
|
||||
follow_enemy_ = true;
|
||||
}
|
||||
|
||||
// this mf is already my current enemy
|
||||
if (enemy_num == enemy_num_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// already have another enemy, likely stay focused on him
|
||||
if (enemy_num_ != 0 && Chance(0.7f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// he is npc and i am not his target, was SURELY a missclick
|
||||
auto enemy_npc = dynamic_cast<NpcCharacter*>(GetWorld().GetEntity(enemy_num));
|
||||
if (enemy_npc && enemy_npc->enemy_num_ != GetEntNum() && Chance(0.3f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// OK this one is now my enemy
|
||||
enemy_num_ = enemy_num;
|
||||
enemy_time_ = time;
|
||||
follow_enemy_ = false;
|
||||
UpdateEnemy();
|
||||
}
|
||||
|
||||
void game::NpcCharacter::UpdateEnemy()
|
||||
{
|
||||
if (enemy_num_ == 0)
|
||||
{
|
||||
enemy_ = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
enemy_ = dynamic_cast<HumanCharacter*>(GetWorld().GetEntity(enemy_num_));
|
||||
|
||||
if (!enemy_ || CheckEnemyLost())
|
||||
{
|
||||
ClearEnemy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (GetAiming())
|
||||
{
|
||||
SetAimTarget(enemy_->GetRoot().matrix * glm::vec4(0.0f, 0.0f, 1.7f, 1.0f));
|
||||
}
|
||||
|
||||
// in vehicle with me?????
|
||||
if (GetRideable() && enemy_->GetRideable() == GetRideable())
|
||||
{
|
||||
Ride(nullptr, 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool game::NpcCharacter::CheckEnemyLost()
|
||||
{
|
||||
if (!enemy_->IsAlive())
|
||||
{
|
||||
return true; // may he rest in peace
|
||||
}
|
||||
|
||||
const float max_dist = 150.0f;
|
||||
auto dist2 = glm::distance2(root_.GetGlobalPosition(), enemy_->GetRoot().GetGlobalPosition());
|
||||
if (dist2 > (max_dist * max_dist))
|
||||
return true; // lost
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool game::NpcCharacter::HasEnemy() const
|
||||
{
|
||||
return enemy_;
|
||||
}
|
||||
|
||||
void game::NpcCharacter::ClearEnemy()
|
||||
{
|
||||
enemy_num_ = 0;
|
||||
enemy_ = nullptr;
|
||||
enemy_time_ = 0;
|
||||
follow_enemy_ = false;
|
||||
}
|
||||
|
||||
bool game::NpcCharacter::WantsToFollowEnemy()
|
||||
{
|
||||
return HasEnemy() && follow_enemy_ && IsArmed();
|
||||
}
|
||||
|
||||
bool game::NpcCharacter::IsArmed() const
|
||||
{
|
||||
return weapon_.get() != nullptr;
|
||||
}
|
||||
|
||||
bool game::NpcCharacter::IsVehicleDriver() const
|
||||
{
|
||||
return GetVehicle() && IsDriver() && IsAlive();
|
||||
}
|
||||
|
||||
void game::NpcCharacter::ResetVehiclePath()
|
||||
{
|
||||
path_.clear();
|
||||
last_waypoint_idx_= 0;
|
||||
}
|
||||
|
||||
void game::NpcCharacter::FindVehiclePath(const glm::vec3& position)
|
||||
{
|
||||
if (path_.empty())
|
||||
{
|
||||
// path_.clear(); // make sure there is not 1 left
|
||||
// path_.push_back(position); // take current pos as first waypoint
|
||||
|
||||
// find closest waypoint as first
|
||||
size_t closest = 0;
|
||||
float closest_dist2 = 10000000000.0f;
|
||||
for (size_t i = 0; i < roads_->nodes.size(); ++i)
|
||||
{
|
||||
auto& node = roads_->nodes[i];
|
||||
float dist = glm::distance(node.position, seg_start_);
|
||||
if (dist < min_dist)
|
||||
auto d = position - roads_->nodes[i].position;
|
||||
auto dist2 = glm::dot(d, d);
|
||||
if (dist2 < closest_dist2)
|
||||
{
|
||||
min_dist = dist;
|
||||
start_node = i;
|
||||
closest_dist2 = dist2;
|
||||
closest = i;
|
||||
}
|
||||
}
|
||||
|
||||
path_.push_back(start_node);
|
||||
|
||||
path_.push_back(roads_->nodes[closest].position);
|
||||
last_waypoint_idx_ = closest;
|
||||
}
|
||||
}
|
||||
|
||||
void game::NpcCharacter::SelectNextNode()
|
||||
{
|
||||
size_t node = path_.back();
|
||||
size_t num_nbs = roads_->nodes[node].num_nbs;
|
||||
|
||||
if (num_nbs < 1)
|
||||
while (path_.size() < PATH_NEXT_WAYPOINTS)
|
||||
{
|
||||
const auto& pos = roads_->nodes[node].position;
|
||||
std::cout << "node " << node << " has no neighbors!!!1 position: " << pos.x << " " << pos.y << " " << pos.z
|
||||
<< std::endl;
|
||||
throw std::runtime_error("no neighbors");
|
||||
const auto& last = roads_->nodes[last_waypoint_idx_];
|
||||
if (last.num_nbs == 0)
|
||||
{
|
||||
throw std::runtime_error("dead end from waypoint: " + last_waypoint_idx_);
|
||||
}
|
||||
|
||||
size_t random_next_idx = rand() % last.num_nbs;
|
||||
size_t nb_idx = roads_->nbs[last.nbs + random_next_idx];
|
||||
path_.push_back(roads_->nodes[nb_idx].position);
|
||||
last_waypoint_idx_ = nb_idx;
|
||||
}
|
||||
|
||||
path_.push_back(roads_->nbs[roads_->nodes[node].nbs + (rand() % num_nbs)]);
|
||||
}
|
||||
|
||||
static float FindClosestPointOnSegment(const glm::vec3& p0, const glm::vec3& p1, const glm::vec3& pos)
|
||||
{
|
||||
glm::vec3 seg_dir = p1 - p0;
|
||||
float seg_len2 = glm::dot(seg_dir, seg_dir);
|
||||
if (seg_len2 < 0.0001f)
|
||||
return 0.0f; // segment is too short
|
||||
|
||||
float t = glm::dot(pos - p0, seg_dir) / seg_len2;
|
||||
// t = glm::clamp(t, 0.0f, 1.0f);
|
||||
return t;
|
||||
}
|
||||
|
||||
static glm::vec3 LookAhead(std::span<glm::vec3> path, float distance)
|
||||
{
|
||||
for (size_t i = 0; i < path.size() - 1; ++i)
|
||||
{
|
||||
const auto& p0 = path[i];
|
||||
const auto& p1 = path[i + 1];
|
||||
auto seg_dist = glm::distance(p0, p1);
|
||||
if (distance < seg_dist)
|
||||
{
|
||||
return glm::mix(p0, p1, distance / seg_dist);
|
||||
}
|
||||
|
||||
distance -= seg_dist;
|
||||
}
|
||||
|
||||
return path.back();
|
||||
}
|
||||
|
||||
static float GetTurnAngle2D(const glm::vec2& forward, const glm::vec2& to_target)
|
||||
{
|
||||
@ -96,191 +293,514 @@ static float GetTurnAngle(const glm::vec3& pos, const glm::quat& rot, const glm:
|
||||
return GetTurnAngle2D(forward_xy, to_target_xy);
|
||||
}
|
||||
|
||||
void game::NpcCharacter::VehicleThink()
|
||||
float CalculateNPCTargetSpeed(std::span<const glm::vec3> waypoints, const glm::quat& vehicleOrientation)
|
||||
{
|
||||
if (!roads_)
|
||||
return;
|
||||
// Configuration parameters (Tweak these to match your game's driving feel)
|
||||
const float MAX_SPEED_KMH = 100.0f; // Speed on a straight road
|
||||
const float MIN_SPEED_KMH = 20.0f; // Speed in a very sharp turn
|
||||
const float LOOK_AHEAD_DISTANCE = 200.0f; // How far ahead (in meters) the NPC cares about
|
||||
|
||||
auto vehicle = GetVehicle();
|
||||
|
||||
if (vehicle_state_ == NVT_REVERSING)
|
||||
// Safety check: We need at least the current position (0) and the next waypoint (1)
|
||||
// to calculate a heading. More waypoints improve the curve estimation.
|
||||
if (waypoints.size() < 2)
|
||||
{
|
||||
if (reversing_frames_ > 0)
|
||||
{
|
||||
--reversing_frames_;
|
||||
}
|
||||
else
|
||||
{
|
||||
vehicle_state_ = NVT_NORMAL;
|
||||
stuck_counter_ = 0;
|
||||
vehicle->SetInput(game::VIN_BACKWARD, false);
|
||||
}
|
||||
return;
|
||||
return MIN_SPEED_KMH;
|
||||
}
|
||||
|
||||
// Initialize vehicle forward vector from orientation
|
||||
glm::vec3 vehicleForward = vehicleOrientation * glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
|
||||
const auto& vehicle_trans = vehicle->GetRootTransform();
|
||||
float accumulatedSharpness = 0.0f;
|
||||
float distanceEvaluated = 0.0f;
|
||||
|
||||
const glm::vec3& pos = vehicle_trans.position;
|
||||
const glm::quat& rot = vehicle_trans.rotation;
|
||||
glm::vec3 forward = rot * glm::vec3{0.0f, 1.0f, 0.0f};
|
||||
// We start by checking the angle between the vehicle's current heading and the first segment
|
||||
glm::vec3 currentDir = vehicleForward;
|
||||
|
||||
// glm::vec3 target = s->roads.nodes[s->node].position;
|
||||
|
||||
//
|
||||
std::array<glm::vec3, 8> waypoints;
|
||||
|
||||
while (path_.size() < waypoints.size() - 1)
|
||||
for (size_t i = 0; i < waypoints.size() - 1; ++i)
|
||||
{
|
||||
SelectNextNode();
|
||||
}
|
||||
glm::vec3 segment = waypoints[i + 1] - waypoints[i];
|
||||
float segmentLength = glm::length(segment);
|
||||
|
||||
glm::vec3 node_pos = roads_->nodes[path_.front()].position;
|
||||
if (glm::distance(glm::vec2(pos), glm::vec2(node_pos)) < 6.0f && path_.size() > 1)
|
||||
{
|
||||
seg_start_ = node_pos;
|
||||
path_.pop_front();
|
||||
SelectNextNode();
|
||||
}
|
||||
if (segmentLength < 0.1f)
|
||||
continue; // Skip duplicate waypoints
|
||||
|
||||
glm::vec3 target_node_pos = roads_->nodes[path_.front()].position;
|
||||
|
||||
waypoints[0] = pos - glm::normalize(forward) * 3.0f;
|
||||
waypoints[1] = pos;
|
||||
|
||||
// find closest point on segment [seg_start -> target_node_pos]
|
||||
glm::vec3 seg_end = target_node_pos;
|
||||
glm::vec3 seg_dir = seg_end - seg_start_;
|
||||
float seg_len = glm::length(seg_dir);
|
||||
if (seg_len > 5.0f)
|
||||
{
|
||||
glm::vec3 seg_dir_norm = seg_dir / seg_len;
|
||||
float t = glm::clamp(glm::dot(pos - seg_start_, seg_dir_norm) / seg_len, 0.0f, 1.0f);
|
||||
waypoints[2] = seg_start_ + t * seg_dir;
|
||||
if (glm::distance(waypoints[1], target_node_pos) > 10.0f)
|
||||
{
|
||||
waypoints[2] += seg_dir_norm * 10.0f; // look a bit ahead on segment
|
||||
}
|
||||
else
|
||||
{
|
||||
waypoints[2] = target_node_pos;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
waypoints[2] = target_node_pos;
|
||||
}
|
||||
|
||||
for (size_t i = 3; i < waypoints.size(); ++i)
|
||||
{
|
||||
size_t path_idx = glm::min(i - 3, path_.size() - 1);
|
||||
waypoints[i] = roads_->nodes[path_[path_idx]].position;
|
||||
}
|
||||
|
||||
// decrease speed based on curvature
|
||||
const float base_speed = 100.0f;
|
||||
float target_speed = base_speed;
|
||||
float dist_accum = 0.0f;
|
||||
for (size_t i = 1; i < waypoints.size() - 1; ++i)
|
||||
{
|
||||
glm::vec3 dir1 = waypoints[i] - waypoints[i - 1];
|
||||
glm::vec3 dir2 = waypoints[i + 1] - waypoints[i];
|
||||
float dist = glm::length(dir1);
|
||||
dist_accum += dist;
|
||||
|
||||
glm::vec2 dir1_xy = glm::vec2{dir1.x, dir1.y};
|
||||
glm::vec2 dir2_xy = glm::vec2{dir2.x, dir2.y};
|
||||
|
||||
const float min_dir_length = 0.001f;
|
||||
float angle = glm::length(dir1_xy) > min_dir_length && glm::length(dir2_xy) > min_dir_length
|
||||
? GetTurnAngle2D(dir1_xy, dir2_xy)
|
||||
: 0.0f;
|
||||
// std::cout << "angle: " << angle << "\n";
|
||||
float abs_angle = fabsf(angle);
|
||||
if (abs_angle > glm::radians(7.0f))
|
||||
{
|
||||
// float speed_limit = 50.0f / abs_angle; // sharper turn -> lower speed
|
||||
// speed_limit *= dist_accum / 20.0f; // more distance to turn -> higher speed
|
||||
// speed_limit = glm::max(speed_limit, 20.0f);
|
||||
// max_speed = glm::min(max_speed, speed_limit);
|
||||
target_speed -=
|
||||
abs_angle * (base_speed / glm::pi<float>() / 2.0f) * 50.0f / glm::max(dist_accum - 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
if (dist_accum > 200.0f)
|
||||
// Accumulate distance. Stop looking ahead if it's too far down the road.
|
||||
distanceEvaluated += segmentLength;
|
||||
if (distanceEvaluated > LOOK_AHEAD_DISTANCE)
|
||||
break;
|
||||
|
||||
glm::vec3 nextDir = segment / segmentLength; // Normalized direction
|
||||
|
||||
// Dot product gives the cosine of the angle between segments (range: -1 to 1)
|
||||
// 1.0 means perfectly straight, 0.0 means 90-degree turn, -1.0 is a hairpin U-turn.
|
||||
float dot = glm::dot(currentDir, nextDir);
|
||||
|
||||
// Clamp to avoid float precision issues with acos/sqrt logic
|
||||
dot = glm::clamp(dot, -1.0f, 1.0f);
|
||||
|
||||
// Turn sharpness: 0.0 (straight) to 2.0 (180-degree turn)
|
||||
float sharpness = 1.0f - dot;
|
||||
|
||||
// Distance weighting: turns further away matter less
|
||||
float distanceWeight = 1.0f - (distanceEvaluated / LOOK_AHEAD_DISTANCE);
|
||||
distanceWeight = glm::max(distanceWeight, 0.0f);
|
||||
|
||||
// Accumulate weighted sharpness
|
||||
accumulatedSharpness += sharpness * distanceWeight;
|
||||
|
||||
// Move to the next segment
|
||||
currentDir = nextDir;
|
||||
}
|
||||
|
||||
target_speed = glm::clamp(target_speed, 25.0f, 100.0f);
|
||||
speed_limit_ = target_speed;
|
||||
// Map the sharpness to a speed limit.
|
||||
// If accumulatedSharpness is 0, t = 0 -> MAX_SPEED.
|
||||
// We cap sharpness sensitivity at 1.0 (roughly a sharp 90-degree turn nearby).
|
||||
float t = glm::clamp(accumulatedSharpness, 0.0f, 1.0f);
|
||||
|
||||
// std::cout << "target speed: " << target_speed << "\n";
|
||||
// Linear interpolation between max and min speed based on sharpness
|
||||
float targetSpeedKMH = glm::mix(MAX_SPEED_KMH, MIN_SPEED_KMH, t);
|
||||
|
||||
float angle = GetTurnAngle(pos, rot, waypoints[2]);
|
||||
return targetSpeedKMH;
|
||||
}
|
||||
|
||||
if (glm::distance(pos, last_pos_) < 2.0f)
|
||||
void game::NpcCharacter::UpdateVehicleInput(std::span<glm::vec3> actual_path)
|
||||
{
|
||||
auto vehicle = GetVehicle();
|
||||
auto vehicle_pos = vehicle->GetRoot().GetGlobalPosition();
|
||||
const auto& vehicle_rot = vehicle->GetRoot().local.rotation;
|
||||
|
||||
auto target_pos = LookAhead(actual_path, glm::max(2.0f, vehicle->GetSpeed() * 0.2f));
|
||||
// auto target_speed = 10.0f;
|
||||
std::span<glm::vec3> speed_path = actual_path;
|
||||
if (glm::distance2(actual_path[0], actual_path[1]) < 4.0f)
|
||||
{
|
||||
stuck_counter_++;
|
||||
if (stuck_counter_ > 100)
|
||||
{
|
||||
//s->state_str = "stuck (reverse)";
|
||||
//s->stuck_counter = 0;
|
||||
//s->vehicle.SetSteering(true, -angle); // try turn away
|
||||
|
||||
//s->vehicle.SetInputs(0); // stop
|
||||
//// stuck, go reverse for a while
|
||||
//s->vehicle.SetInput(game::VIN_BACKWARD, true);
|
||||
//s->vehicle.Schedule(2000, [s]() {
|
||||
// s->vehicle.SetInput(game::VIN_BACKWARD, false);
|
||||
// BotThink(s);
|
||||
//});
|
||||
|
||||
vehicle->SetSteering(true, -angle); // try turn away while reversing
|
||||
vehicle->SetInputs(0); // stop
|
||||
vehicle->SetInput(game::VIN_BACKWARD, true);
|
||||
vehicle_state_ = NVT_REVERSING;
|
||||
reversing_frames_ = 50; // reverse for 50 frames
|
||||
|
||||
// GetVehicleOld()->SetInputs(0); // stop
|
||||
// is_driver_ = false; // TODO: fix
|
||||
return;
|
||||
}
|
||||
speed_path = { actual_path.begin() + 1, actual_path.end() };
|
||||
}
|
||||
else
|
||||
|
||||
auto target_speed = CalculateNPCTargetSpeed(speed_path, vehicle_rot);
|
||||
|
||||
if (in_hurry_)
|
||||
{
|
||||
stuck_counter_ = 0;
|
||||
last_pos_ = pos;
|
||||
target_speed *= 4.0f;
|
||||
}
|
||||
|
||||
vehicle->SetSteering(true, angle);
|
||||
|
||||
game::VehicleInputFlags vin = 0;
|
||||
// set steering
|
||||
vehicle_steer_ = GetTurnAngle(vehicle_pos, vehicle_rot, target_pos);
|
||||
|
||||
// set input
|
||||
float speed = vehicle->GetSpeed();
|
||||
|
||||
// if (glm::distance(pos, target) < 10.0f)
|
||||
// {
|
||||
// target_speed = 20.0f;
|
||||
// }
|
||||
|
||||
if (speed < target_speed * 0.9f && !gas_)
|
||||
if (speed < target_speed * 0.9f && ((vehicle_in_ & (1<<VIN_FORWARD)) == 0))
|
||||
{
|
||||
gas_ = true;
|
||||
// gas
|
||||
vehicle_in_ |= (1<<VIN_FORWARD);
|
||||
}
|
||||
else if (speed > target_speed * 1.1f && gas_)
|
||||
else if (speed > target_speed * 1.1f && ((vehicle_in_ & (1<<VIN_FORWARD)) > 0))
|
||||
{
|
||||
gas_ = false;
|
||||
}
|
||||
|
||||
if (gas_)
|
||||
{
|
||||
vin |= 1 << game::VIN_FORWARD;
|
||||
// no gas
|
||||
vehicle_in_ &= ~(1<<VIN_FORWARD);
|
||||
}
|
||||
|
||||
if (speed > target_speed * 1.4f)
|
||||
{
|
||||
vin |= 1 << game::VIN_BACKWARD;
|
||||
// brake
|
||||
vehicle_in_ |= (1<<VIN_BACKWARD);
|
||||
}
|
||||
else
|
||||
{
|
||||
// dont brake
|
||||
vehicle_in_ &= ~(1<<VIN_BACKWARD);
|
||||
}
|
||||
|
||||
vehicle->SetInputs(vin);
|
||||
|
||||
// debug draw path
|
||||
// float beam_dist = 30.0f;
|
||||
// for (size_t i = 0; i < actual_path.size() - 1; ++i)
|
||||
// {
|
||||
// const auto& p0 = actual_path[i];
|
||||
// auto p1 = actual_path[i + 1];
|
||||
|
||||
// auto dist = glm::distance(p0, p1);
|
||||
// if (beam_dist < dist)
|
||||
// {
|
||||
// p1 = glm::mix(p0, p1, beam_dist / dist);
|
||||
// }
|
||||
|
||||
// GetWorld().Beam(p0, p1, i % 2 == 0 ? 0xFF0000FF : 0xFF0077FF, 1.5f / 25.0f);
|
||||
|
||||
// beam_dist -= dist;
|
||||
// if (beam_dist <= 0.0f)
|
||||
// break;
|
||||
// }
|
||||
// GetWorld().Beam(vehicle_pos, target_pos, 0xFFFF00FF, 1.5f / 25.0f);
|
||||
// GetWorld().BeamBox(target_pos - 0.05f, target_pos + 0.05f, 0xFFFF00FF, 1.5f / 25.0f);
|
||||
}
|
||||
|
||||
void game::NpcCharacter::UpdateVehicleInputToFollowPath()
|
||||
{
|
||||
auto vehicle = GetVehicle();
|
||||
if (!vehicle)
|
||||
return;
|
||||
|
||||
auto vehicle_pos = vehicle->GetRoot().GetGlobalPosition();
|
||||
|
||||
FindVehiclePath(vehicle_pos);
|
||||
|
||||
float seg_t;
|
||||
|
||||
// check if we reached next waypoint
|
||||
while (true)
|
||||
{
|
||||
auto seg_dir = path_[1] - path_[0];
|
||||
auto seg_len2 = glm::dot(seg_dir, seg_dir);
|
||||
auto seg_len = glm::sqrt(seg_len2);
|
||||
if (seg_len > 0.1f)
|
||||
{
|
||||
seg_t = glm::dot(vehicle_pos - path_[0], seg_dir) / seg_len2;
|
||||
if (seg_t < (1.0f - 3.0f / seg_len))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
path_.pop_front();
|
||||
FindVehiclePath(vehicle_pos);
|
||||
|
||||
}
|
||||
|
||||
seg_t = glm::clamp(seg_t, 0.0f, 1.0f);
|
||||
|
||||
const auto& p0 = path_[0];
|
||||
const auto& p1 = path_[1];
|
||||
auto on_segment = glm::mix(p0, p1, seg_t);
|
||||
|
||||
std::array<glm::vec3, PATH_NEXT_WAYPOINTS + 1> actual_path; // pos,on_segment,path[1],path[2]...
|
||||
actual_path[0] = vehicle_pos;
|
||||
actual_path[1] = on_segment;
|
||||
for (size_t i = 1; i < PATH_NEXT_WAYPOINTS; ++i)
|
||||
{
|
||||
actual_path[i + 2 - 1] = path_[i];
|
||||
}
|
||||
|
||||
UpdateVehicleInput(actual_path);
|
||||
}
|
||||
|
||||
void game::NpcCharacter::UpdateVehicleInputToFollowEnemy()
|
||||
{
|
||||
if (!enemy_)
|
||||
{
|
||||
vehicle_in_ = 0;
|
||||
vehicle_steer_ = 0.0f;
|
||||
return;
|
||||
}
|
||||
|
||||
auto vehicle = GetVehicle();
|
||||
if (!vehicle)
|
||||
return;
|
||||
|
||||
std::array<glm::vec3, 2> actual_path;
|
||||
actual_path[0] = vehicle->GetRoot().GetGlobalPosition();;
|
||||
actual_path[1] = enemy_->GetRoot().GetGlobalPosition();;
|
||||
|
||||
UpdateVehicleInput(actual_path);
|
||||
}
|
||||
|
||||
bool game::NpcCharacter::CheckStuck()
|
||||
{
|
||||
auto pos = root_.GetGlobalPosition();
|
||||
auto dist2 = glm::distance2(pos, last_pos_);
|
||||
|
||||
// far, not stuck
|
||||
if (dist2 > 4.0f)
|
||||
{
|
||||
last_pos_ = pos;
|
||||
stuck_counter_ = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
++stuck_counter_;
|
||||
|
||||
// close but only short time yet
|
||||
if (stuck_counter_ < 100)
|
||||
return false;
|
||||
|
||||
// stuck
|
||||
last_pos_ = pos;
|
||||
stuck_counter_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
static game::CharacterInputFlags GetRandomStrafeDir(float strafe_chance)
|
||||
{
|
||||
auto dir_choice = RandomFloat(0.0f, 1.0f);
|
||||
|
||||
if (dir_choice > strafe_chance)
|
||||
return 0;
|
||||
|
||||
return (dir_choice < strafe_chance * 0.5f) ? (1 << game::CIN_RIGHT) : (1 << game::CIN_LEFT);
|
||||
}
|
||||
|
||||
void game::NpcCharacter::Think()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
auto new_state = CheckThinkStateTransition();
|
||||
if (new_state == think_state_)
|
||||
break;
|
||||
|
||||
EnterThinkState(new_state);
|
||||
}
|
||||
}
|
||||
|
||||
void game::NpcCharacter::EnterThinkState(ThinkState state)
|
||||
{
|
||||
think_state_ = state;
|
||||
think_state_time_ = GetWorld().GetTime();
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case THINKSTATE_IDLE:
|
||||
SetAimHeld(false);
|
||||
SetFireHeld(false);
|
||||
Equip(nullptr);
|
||||
SetInputs(0);
|
||||
in_hurry_ = false;
|
||||
break;
|
||||
|
||||
case THINKSTATE_MAD_IDLE:
|
||||
SetAimHeld(false);
|
||||
SetFireHeld(false);
|
||||
Equip(weapon_);
|
||||
SetInputs(0);
|
||||
SetTargetThinkStateDuration(100, 300);
|
||||
break;
|
||||
|
||||
case THINKSTATE_MAD_AIM:
|
||||
SetAimHeld(true);
|
||||
SetFireHeld(false);
|
||||
Equip(weapon_);
|
||||
SetInputs(GetRandomStrafeDir(0.8f));
|
||||
SetTargetThinkStateDuration(200, 800);
|
||||
break;
|
||||
|
||||
case THINKSTATE_MAD_FIRE:
|
||||
SetAimHeld(true);
|
||||
SetFireHeld(true);
|
||||
Equip(weapon_);
|
||||
SetInputs(GetRandomStrafeDir(0.4f));
|
||||
SetTargetThinkStateDuration(200, 1200);
|
||||
break;
|
||||
|
||||
case THINKSTATE_SCARED:
|
||||
SetAimHeld(false);
|
||||
SetFireHeld(false);
|
||||
Equip(nullptr);
|
||||
SetInputs(0);
|
||||
in_hurry_ = true;
|
||||
SetTargetThinkStateDuration(5000, 10000);
|
||||
break;
|
||||
|
||||
case THINKSTATE_BRAINDEAD:
|
||||
SetAimHeld(true);
|
||||
SetFireHeld(true);
|
||||
SetInputs(0);
|
||||
in_hurry_ = false;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
game::ThinkState game::NpcCharacter::CheckThinkStateTransition()
|
||||
{
|
||||
if (!IsAlive())
|
||||
return THINKSTATE_BRAINDEAD;
|
||||
|
||||
switch (think_state_)
|
||||
{
|
||||
case THINKSTATE_IDLE:
|
||||
if (HasEnemy())
|
||||
return IsArmed() ? THINKSTATE_MAD_IDLE : THINKSTATE_SCARED;
|
||||
|
||||
return THINKSTATE_IDLE;
|
||||
|
||||
case THINKSTATE_MAD_IDLE:
|
||||
if (!HasEnemy() || !IsArmed())
|
||||
return THINKSTATE_IDLE;
|
||||
|
||||
if (HasThinkStateDurationElapsed() && CanAim())
|
||||
return THINKSTATE_MAD_AIM;
|
||||
|
||||
return THINKSTATE_MAD_IDLE;
|
||||
|
||||
case THINKSTATE_MAD_AIM:
|
||||
if (!HasEnemy() || !IsArmed())
|
||||
return THINKSTATE_IDLE;
|
||||
|
||||
if (GetCurrentThinkStateDuration() > 0 && !GetAiming()) // reload or sth
|
||||
return THINKSTATE_MAD_IDLE;
|
||||
|
||||
if (HasThinkStateDurationElapsed())
|
||||
return THINKSTATE_MAD_FIRE;
|
||||
|
||||
return THINKSTATE_MAD_AIM;
|
||||
|
||||
case THINKSTATE_MAD_FIRE:
|
||||
if (HasThinkStateDurationElapsed())
|
||||
return THINKSTATE_MAD_AIM;
|
||||
|
||||
return THINKSTATE_MAD_FIRE;
|
||||
|
||||
case THINKSTATE_SCARED:
|
||||
if (HasThinkStateDurationElapsed())
|
||||
return THINKSTATE_IDLE;
|
||||
|
||||
return THINKSTATE_SCARED;
|
||||
|
||||
default:
|
||||
return THINKSTATE_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
int64_t game::NpcCharacter::GetCurrentThinkStateDuration() const
|
||||
{
|
||||
return GetWorld().GetTime() - think_state_time_;
|
||||
}
|
||||
|
||||
void game::NpcCharacter::SetTargetThinkStateDuration(int duration_min, int duration_max)
|
||||
{
|
||||
if (duration_max == 0)
|
||||
{
|
||||
think_state_target_duration_ = duration_min;
|
||||
}
|
||||
else
|
||||
{
|
||||
think_state_target_duration_ = RandomInt(duration_min, duration_max);
|
||||
}
|
||||
}
|
||||
|
||||
bool game::NpcCharacter::HasThinkStateDurationElapsed() const
|
||||
{
|
||||
return GetCurrentThinkStateDuration() >= think_state_target_duration_;
|
||||
}
|
||||
|
||||
void game::NpcCharacter::DriverThink()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
auto new_state = CheckDriverThinkStateTransition();
|
||||
if (new_state == driver_state_)
|
||||
break;
|
||||
|
||||
EnterDriverThinkState(new_state);
|
||||
}
|
||||
|
||||
auto vehicle = GetVehicle();
|
||||
if (vehicle && IsDriver())
|
||||
{
|
||||
vehicle->SetSteering(true, vehicle_steer_);
|
||||
vehicle->SetInputs(vehicle_in_);
|
||||
}
|
||||
}
|
||||
|
||||
void game::NpcCharacter::EnterDriverThinkState(DriverThinkState state)
|
||||
{
|
||||
prev_driver_state_ = driver_state_;
|
||||
driver_state_ = state;
|
||||
driver_state_time_ = GetWorld().GetTime();
|
||||
|
||||
stuck_counter_ = 0;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case DRIVERSTATE_NONE:
|
||||
vehicle_in_ = 0;
|
||||
vehicle_steer_ = 0.0f;
|
||||
break;
|
||||
|
||||
case DRIVERSTATE_PATH_BEGIN:
|
||||
vehicle_in_ = 0;
|
||||
vehicle_steer_ = 0.0f;
|
||||
ResetVehiclePath();
|
||||
break;
|
||||
|
||||
case DRIVERSTATE_PATH:
|
||||
break;
|
||||
|
||||
case DRIVERSTATE_REVERSE:
|
||||
vehicle_in_ = 1<<VIN_BACKWARD; // go reverse
|
||||
vehicle_steer_ = -vehicle_steer_; // try turn away
|
||||
break;
|
||||
|
||||
case DRIVERSTATE_FOLLOW_ENEMY:
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
game::DriverThinkState game::NpcCharacter::CheckDriverThinkStateTransition()
|
||||
{
|
||||
switch (driver_state_)
|
||||
{
|
||||
case DRIVERSTATE_NONE:
|
||||
if (IsVehicleDriver() && GetCurrentDriverThinkStateDuration() > 1000) // take some time to think
|
||||
{
|
||||
if (WantsToFollowEnemy())
|
||||
return DRIVERSTATE_FOLLOW_ENEMY;
|
||||
|
||||
return DRIVERSTATE_PATH_BEGIN;
|
||||
}
|
||||
|
||||
return DRIVERSTATE_NONE;
|
||||
|
||||
case DRIVERSTATE_PATH_BEGIN:
|
||||
return DRIVERSTATE_PATH;
|
||||
|
||||
case DRIVERSTATE_PATH:
|
||||
if (!IsVehicleDriver())
|
||||
return DRIVERSTATE_NONE;
|
||||
|
||||
if (WantsToFollowEnemy())
|
||||
return DRIVERSTATE_NONE;
|
||||
|
||||
if (CheckStuck())
|
||||
return DRIVERSTATE_REVERSE;
|
||||
|
||||
// update input
|
||||
UpdateVehicleInputToFollowPath();
|
||||
|
||||
return DRIVERSTATE_PATH;
|
||||
|
||||
case DRIVERSTATE_FOLLOW_ENEMY:
|
||||
if (!IsVehicleDriver())
|
||||
return DRIVERSTATE_NONE;
|
||||
|
||||
if (!WantsToFollowEnemy())
|
||||
return DRIVERSTATE_NONE;
|
||||
|
||||
if (CheckStuck())
|
||||
return DRIVERSTATE_REVERSE;
|
||||
|
||||
UpdateVehicleInputToFollowEnemy();
|
||||
|
||||
return DRIVERSTATE_FOLLOW_ENEMY;
|
||||
|
||||
case DRIVERSTATE_REVERSE:
|
||||
if (!IsVehicleDriver())
|
||||
return DRIVERSTATE_NONE;
|
||||
|
||||
if (GetCurrentDriverThinkStateDuration() > 3000)
|
||||
return prev_driver_state_;
|
||||
|
||||
return DRIVERSTATE_REVERSE;
|
||||
|
||||
default:
|
||||
return DRIVERSTATE_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
int64_t game::NpcCharacter::GetCurrentDriverThinkStateDuration() const
|
||||
{
|
||||
return GetWorld().GetTime() - driver_state_time_;
|
||||
}
|
||||
|
||||
@ -2,16 +2,30 @@
|
||||
|
||||
#include "assets/map.hpp"
|
||||
#include "human_character.hpp"
|
||||
#include "vehicle.hpp"
|
||||
|
||||
namespace game
|
||||
{
|
||||
|
||||
class OpenWorld;
|
||||
|
||||
enum NpcVehicleThinkState
|
||||
enum ThinkState
|
||||
{
|
||||
NVT_NORMAL,
|
||||
NVT_REVERSING,
|
||||
THINKSTATE_IDLE,
|
||||
THINKSTATE_MAD_IDLE,
|
||||
THINKSTATE_MAD_AIM,
|
||||
THINKSTATE_MAD_FIRE,
|
||||
THINKSTATE_SCARED,
|
||||
THINKSTATE_BRAINDEAD,
|
||||
};
|
||||
|
||||
enum DriverThinkState
|
||||
{
|
||||
DRIVERSTATE_NONE,
|
||||
DRIVERSTATE_PATH_BEGIN,
|
||||
DRIVERSTATE_PATH,
|
||||
DRIVERSTATE_REVERSE,
|
||||
DRIVERSTATE_FOLLOW_ENEMY,
|
||||
};
|
||||
|
||||
class NpcCharacter : public HumanCharacter
|
||||
@ -23,26 +37,84 @@ public:
|
||||
|
||||
virtual void Update() override;
|
||||
|
||||
virtual void ReceiveDamage(const DamageInfo& damage) override;
|
||||
|
||||
void SetWeapon(std::shared_ptr<ItemInstance> weapon);
|
||||
|
||||
bool IsBored(int64_t time) const;
|
||||
void Die();
|
||||
|
||||
// net::EntNum GetEnemyNum() const { return enemy_num_; }
|
||||
|
||||
protected:
|
||||
virtual void OnRideableChanged() override;
|
||||
virtual void OnRideableDamaged(const DamageInfo& damage) override;
|
||||
virtual void SpawnLoot() override;
|
||||
|
||||
private:
|
||||
void UpdateVehicleState();
|
||||
void SelectNextNode();
|
||||
void VehicleThink();
|
||||
void MakeEnemy(net::EntNum enemy_num);
|
||||
void UpdateEnemy();
|
||||
bool CheckEnemyLost();
|
||||
bool HasEnemy() const;
|
||||
void ClearEnemy();
|
||||
bool WantsToFollowEnemy();
|
||||
|
||||
bool IsArmed() const;
|
||||
|
||||
bool IsVehicleDriver() const;
|
||||
void ResetVehiclePath();
|
||||
void FindVehiclePath(const glm::vec3& position);
|
||||
void UpdateVehicleInput(std::span<glm::vec3> path);
|
||||
void UpdateVehicleInputToFollowPath();
|
||||
void UpdateVehicleInputToFollowEnemy();
|
||||
bool CheckStuck();
|
||||
|
||||
void Think();
|
||||
void EnterThinkState(ThinkState state);
|
||||
ThinkState CheckThinkStateTransition();
|
||||
int64_t GetCurrentThinkStateDuration() const;
|
||||
void SetTargetThinkStateDuration(int duration_min, int duration_max = 0);
|
||||
bool HasThinkStateDurationElapsed() const;
|
||||
|
||||
void DriverThink();
|
||||
void EnterDriverThinkState(DriverThinkState state);
|
||||
DriverThinkState CheckDriverThinkStateTransition();
|
||||
int64_t GetCurrentDriverThinkStateDuration() const;
|
||||
|
||||
// void UpdateVehicleState();
|
||||
// void SelectNextNode();
|
||||
// void VehicleThink();
|
||||
|
||||
private:
|
||||
const assets::MapGraph* roads_;
|
||||
|
||||
ThinkState think_state_ = THINKSTATE_IDLE;
|
||||
int64_t think_state_time_ = 0;
|
||||
int64_t think_state_target_duration_ = 0;
|
||||
|
||||
// driver
|
||||
NpcVehicleThinkState vehicle_state_ = NVT_NORMAL;
|
||||
const assets::MapGraph* roads_;
|
||||
glm::vec3 seg_start_;
|
||||
std::deque<size_t> path_;
|
||||
bool gas_ = false;
|
||||
size_t stuck_counter_ = 0;
|
||||
size_t reversing_frames_ = 0;
|
||||
DriverThinkState driver_state_ = DRIVERSTATE_NONE;
|
||||
int64_t driver_state_time_ = 0;
|
||||
DriverThinkState prev_driver_state_ = DRIVERSTATE_NONE;
|
||||
|
||||
std::deque<glm::vec3> path_;
|
||||
size_t last_waypoint_idx_ = 0;
|
||||
glm::vec3 last_pos_ = glm::vec3(0.0f);
|
||||
float speed_limit_ = 0.0f;
|
||||
size_t stuck_counter_ = 0;
|
||||
|
||||
VehicleInputFlags vehicle_in_ = 0;
|
||||
float vehicle_steer_ = 0.0f;
|
||||
|
||||
bool in_hurry_ = false;
|
||||
|
||||
// mad
|
||||
std::shared_ptr<ItemInstance> weapon_;
|
||||
|
||||
net::EntNum enemy_num_ = 0;
|
||||
HumanCharacter* enemy_ = nullptr;
|
||||
int64_t enemy_time_ = 0;
|
||||
|
||||
bool follow_enemy_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
@ -13,13 +13,13 @@
|
||||
#include "tuning_world.hpp"
|
||||
#include "game.hpp"
|
||||
#include "cow.hpp"
|
||||
#include "utils/random.hpp"
|
||||
|
||||
namespace game
|
||||
{
|
||||
|
||||
} // namespace game
|
||||
|
||||
|
||||
static const char* GetRandomCarModel()
|
||||
{
|
||||
const char* vehicles[] = {"pickup_hd", "passat", "twingo", "polskifiat", "avia"};
|
||||
@ -51,34 +51,29 @@ static uint32_t GetRandomColor24()
|
||||
|
||||
game::OpenWorld::OpenWorld(Game& game) : EnterableWorld("openworld"), game_(game)
|
||||
{
|
||||
// spawn bots
|
||||
for (size_t i = 0; i < 100; ++i)
|
||||
{
|
||||
SpawnBot();
|
||||
}
|
||||
SetSpawnPoint(glm::vec3(100.0f, 100.0f, 1.0f));
|
||||
|
||||
// initial twingo
|
||||
VehicleTuning twingo_tuning;
|
||||
twingo_tuning.model = "twingo";
|
||||
twingo_tuning.parts["primarycolor"] = "orange";
|
||||
VehicleSpawnInfo twingo_info{};
|
||||
twingo_info.tuning.model = "twingo";
|
||||
twingo_info.tuning.parts["primarycolor"] = "orange";
|
||||
// twingo_tuning.primary_color = 0x0077FF;
|
||||
// twingo_tuning.wheels_idx = 1; // enkei
|
||||
// twingo_tuning.wheel_color = 0x00FF00;
|
||||
twingo_info.position = glm::vec3{110.0f, 100.0f, 5.0f};
|
||||
|
||||
auto& veh = Spawn<game::DrivableVehicle>(twingo_tuning);
|
||||
veh.SetPosition({110.0f, 100.0f, 5.0f});
|
||||
auto& veh = Spawn<game::DrivableVehicle>(twingo_info);
|
||||
|
||||
constexpr size_t in_row = 20;
|
||||
|
||||
for (size_t i = 0; i < 100; ++i)
|
||||
{
|
||||
Schedule(i * 40, [this, i] {
|
||||
Schedule(i * 160, [this, i] {
|
||||
size_t col = i % in_row;
|
||||
size_t row = i / in_row;
|
||||
glm::vec3 pos(62.0f + static_cast<float>(col) * 4.0f, 165.0f + static_cast<float>(row) * 7.0f, 7.0f);
|
||||
|
||||
auto& veh = SpawnRandomVehicle();
|
||||
veh.SetPosition(pos);
|
||||
SpawnRandomVehicle(pos, 0.0f);
|
||||
});
|
||||
}
|
||||
|
||||
@ -89,9 +84,11 @@ game::OpenWorld::OpenWorld(Game& game) : EnterableWorld("openworld"), game_(game
|
||||
CreateTuningGarage(loc.transform.position, glm::eulerAngles(loc.transform.rotation).x);
|
||||
}
|
||||
|
||||
CreateItemPickups("pickup_uzi", "uzi");
|
||||
CreateItemPickups("pickup_ak47", "ak47");
|
||||
CreateItemPickups("pickup_airsniper", "airsniper");
|
||||
CreatePermaItemPickups("pickup_uzi", "uzi");
|
||||
CreatePermaItemPickups("pickup_ak47", "ak47");
|
||||
CreatePermaItemPickups("pickup_airsniper", "airsniper");
|
||||
|
||||
SpawnNpcs();
|
||||
|
||||
// cow
|
||||
auto& cow = Spawn<Cow>(glm::vec3(0.0f, 0.0f, 2.0f), 0.0f);
|
||||
@ -100,7 +97,12 @@ game::OpenWorld::OpenWorld(Game& game) : EnterableWorld("openworld"), game_(game
|
||||
// hit target npc
|
||||
auto& npc = SpawnRandomNpc();
|
||||
npc.SetPosition({90.0f, 100.0f, 5.0f});
|
||||
npc.EnablePhysics(true);
|
||||
npc.SetWeapon(std::make_shared<ItemInstance>("ak47"));
|
||||
|
||||
// hit target npc 2
|
||||
auto& npc2 = SpawnRandomNpc();
|
||||
npc2.SetPosition({80.0f, 100.0f, 5.0f});
|
||||
npc2.SetWeapon(std::make_shared<ItemInstance>("airsniper"));
|
||||
}
|
||||
|
||||
void game::OpenWorld::Update(int64_t delta_time)
|
||||
@ -123,15 +125,47 @@ void game::OpenWorld::PlayerInput(Player& player, PlayerInputType type, bool ena
|
||||
Super::PlayerInput(player, type, enabled);
|
||||
}
|
||||
|
||||
game::DrivableVehicle& game::OpenWorld::SpawnRandomVehicle()
|
||||
void game::OpenWorld::SpawnNpcs()
|
||||
{
|
||||
game::VehicleTuning tuning;
|
||||
tuning.model = GetRandomCarModel();
|
||||
int64_t next_spawn_after = 10000;
|
||||
|
||||
if (num_npcs_ < 120)
|
||||
{
|
||||
SpawnNpcVehicleWithPassengers();
|
||||
next_spawn_after = RandomInt(100, 1000);
|
||||
}
|
||||
|
||||
Schedule(next_spawn_after, [this]{
|
||||
SpawnNpcs();
|
||||
});
|
||||
}
|
||||
|
||||
static void CheckVehicleAbandonment(game::DrivableVehicle& vehicle)
|
||||
{
|
||||
if (vehicle.IsAbandoned(60000 * 5)) // 5 min
|
||||
{
|
||||
vehicle.Remove();
|
||||
}
|
||||
else
|
||||
{
|
||||
vehicle.Schedule(RandomInt(0, 1000) + 10000, [&vehicle]{
|
||||
CheckVehicleAbandonment(vehicle);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
game::DrivableVehicle& game::OpenWorld::SpawnRandomVehicle(const glm::vec3& pos, float yaw, bool auto_despawn)
|
||||
{
|
||||
VehicleSpawnInfo vehicle_info;
|
||||
vehicle_info.tuning.model = GetRandomCarModel();
|
||||
vehicle_info.position = pos;
|
||||
vehicle_info.yaw = yaw;
|
||||
// tuning.primary_color = GetRandomColor24();
|
||||
|
||||
auto& vehicle = Spawn<game::DrivableVehicle>(tuning);
|
||||
auto& vehicle = Spawn<game::DrivableVehicle>(vehicle_info);
|
||||
// vehicle.SetNametag("bot (" + std::to_string(vehicle.GetEntNum()) + ")");
|
||||
|
||||
auto& tuning = vehicle_info.tuning;
|
||||
auto& tuning_list = vehicle.GetTuningList();
|
||||
|
||||
// make random tuning
|
||||
@ -166,34 +200,103 @@ game::DrivableVehicle& game::OpenWorld::SpawnRandomVehicle()
|
||||
|
||||
vehicle.SetTuning(tuning);
|
||||
|
||||
if (auto_despawn)
|
||||
{
|
||||
CheckVehicleAbandonment(vehicle);
|
||||
}
|
||||
|
||||
return vehicle;
|
||||
}
|
||||
|
||||
static void CheckNpcBoredom(game::NpcCharacter& npc)
|
||||
{
|
||||
if (!npc.IsAlive())
|
||||
return;
|
||||
|
||||
if (npc.IsBored(60000 * 5)) // 5 min doing nothing is insufferable
|
||||
{
|
||||
npc.Die();
|
||||
}
|
||||
else
|
||||
{
|
||||
npc.Schedule(RandomInt(0, 2000) + 10000, [&npc]{
|
||||
CheckNpcBoredom(npc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
game::NpcCharacter& game::OpenWorld::SpawnRandomNpc()
|
||||
{
|
||||
HumanCharacterTuning npc_tuning;
|
||||
npc_tuning.clothes.push_back({ "tshirt", GetRandomColor24() });
|
||||
npc_tuning.clothes.push_back({ "shorts", GetRandomColor24() });
|
||||
|
||||
return Spawn<NpcCharacter>(npc_tuning);
|
||||
auto& npc = Spawn<NpcCharacter>(npc_tuning);
|
||||
|
||||
npc.SetOnDeath([this, &npc] {
|
||||
npc.Schedule(15000, [&npc]{
|
||||
npc.Remove();
|
||||
});
|
||||
|
||||
if (num_npcs_ > 0)
|
||||
{
|
||||
--num_npcs_;
|
||||
}
|
||||
});
|
||||
|
||||
npc.Schedule(1000, [&npc]{
|
||||
CheckNpcBoredom(npc);
|
||||
});
|
||||
|
||||
++num_npcs_;
|
||||
|
||||
return npc;
|
||||
}
|
||||
|
||||
void game::OpenWorld::SpawnBot()
|
||||
static std::tuple<glm::vec3, float> GetRandomNodeAndRotation(const assets::MapGraph& graph)
|
||||
{
|
||||
size_t node_idx = rand() % graph.nodes.size();
|
||||
auto& node = graph.nodes[node_idx];
|
||||
|
||||
size_t nb_idx = graph.nbs[rand() % node.num_nbs];
|
||||
auto& nb = graph.nodes[nb_idx];
|
||||
|
||||
auto dir = node.position - nb.position;
|
||||
float yaw = glm::atan(-dir.x, dir.y);
|
||||
|
||||
return std::make_tuple(node.position, yaw);
|
||||
}
|
||||
|
||||
void game::OpenWorld::SpawnNpcVehicleWithPassengers()
|
||||
{
|
||||
auto roads = GetMap().GetGraph("roads");
|
||||
|
||||
if (!roads)
|
||||
{
|
||||
throw std::runtime_error("SpawnBot: no roads graph in map");
|
||||
throw std::runtime_error("SpawnNpcVehicleWithPassengers: no roads graph in map");
|
||||
}
|
||||
|
||||
size_t start_node = rand() % roads->nodes.size();
|
||||
|
||||
auto& vehicle = SpawnRandomVehicle();
|
||||
vehicle.SetPosition(roads->nodes[start_node].position + glm::vec3{0.0f, 0.0f, 5.0f});
|
||||
auto [pos, yaw] = GetRandomNodeAndRotation(*roads);
|
||||
auto& vehicle = SpawnRandomVehicle(pos, yaw, true);
|
||||
|
||||
auto& driver = SpawnRandomNpc();
|
||||
driver.Ride(&vehicle, 0);
|
||||
|
||||
if (Chance(0.5f))
|
||||
{
|
||||
driver.SetWeapon(std::make_shared<ItemInstance>("uzi"));
|
||||
}
|
||||
|
||||
if (Chance(0.3f))
|
||||
{
|
||||
auto& passenger = SpawnRandomNpc();
|
||||
passenger.Ride(&vehicle, 1);
|
||||
|
||||
if (Chance(0.5f))
|
||||
{
|
||||
passenger.SetWeapon(std::make_shared<ItemInstance>(Chance(0.4f) ? "ak47" : (Chance(0.5f) ? "uzi" : "airsniper")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void game::OpenWorld::CreateTuningGarage(const glm::vec3& position, float yaw)
|
||||
@ -250,51 +353,18 @@ void game::OpenWorld::CreateTuningGarage(const glm::vec3& position, float yaw)
|
||||
});
|
||||
}
|
||||
|
||||
void game::OpenWorld::CreateItemPickups(const std::string& loc_name, const std::string& item_name)
|
||||
void game::OpenWorld::CreatePermaItemPickups(const std::string& loc_name, const std::string& item_name)
|
||||
{
|
||||
for (auto locs = GetMap().GetLocations(loc_name); const auto& loc : locs)
|
||||
{
|
||||
CreateItemPickup(loc.transform.position, item_name);
|
||||
CreatePermaItemPickup(loc.transform.position, item_name);
|
||||
}
|
||||
}
|
||||
|
||||
void game::OpenWorld::CreateItemPickup(const glm::vec3& position, const std::string& item_name)
|
||||
void game::OpenWorld::CreatePermaItemPickup(const glm::vec3& position, const std::string& item_name)
|
||||
{
|
||||
auto item_def = assets::CacheManager::GetItem("data/" + item_name + ".item");
|
||||
|
||||
MarkerInfo marker_info{};
|
||||
marker_info.position = position;
|
||||
marker_info.type = MARKER_PICKUP;
|
||||
marker_info.color = 0xFFFFFF;
|
||||
marker_info.model = item_def->model_name;
|
||||
|
||||
auto& marker = Spawn<Marker>(marker_info);
|
||||
marker.SetUseTarget(
|
||||
"sebrat " + item_name,
|
||||
[](PlayerCharacter& character, UseTargetQueryResult& res) {
|
||||
res.enabled = true;
|
||||
res.delay = 0.1f;
|
||||
res.error_text = nullptr;
|
||||
return true;
|
||||
},
|
||||
[this, position, item_def,
|
||||
&marker](PlayerCharacter& character) {
|
||||
auto player = character.GetPlayer();
|
||||
if (!player)
|
||||
return;
|
||||
|
||||
character.GiveItem(std::make_shared<ItemInstance>(item_def->name));
|
||||
character.GiveAmmo(item_def->ammo_type, item_def->clip_size * 15);
|
||||
character.PlaySound("pickup_ammo");
|
||||
|
||||
player->SendChat("sebrals " + item_def->name);
|
||||
marker.SetUseable(false);
|
||||
marker.Remove();
|
||||
|
||||
Schedule(5000, [this, position, item_def]() {
|
||||
CreateItemPickup(position, item_def->name);
|
||||
});
|
||||
});
|
||||
CreateItemPickup(position, std::make_shared<ItemInstance>(item_def), 0, 5000, item_def->clip_size * 15);
|
||||
}
|
||||
|
||||
void game::OpenWorld::RecoverPlayer(Player& player)
|
||||
|
||||
@ -19,21 +19,25 @@ public:
|
||||
virtual void PlayerInput(Player& player, PlayerInputType type, bool enabled) override;
|
||||
|
||||
private:
|
||||
game::DrivableVehicle& SpawnRandomVehicle();
|
||||
void SpawnNpcs();
|
||||
game::DrivableVehicle& SpawnRandomVehicle(const glm::vec3& pos, float yaw, bool auto_despawn = false);
|
||||
game::NpcCharacter& SpawnRandomNpc();
|
||||
void SpawnBot();
|
||||
void SpawnNpcVehicleWithPassengers();
|
||||
|
||||
void CreateTuningGarage(const glm::vec3& position, float yaw);
|
||||
void CreateItemPickups(const std::string& loc_name, const std::string& item_name);
|
||||
void CreateItemPickup(const glm::vec3& position, const std::string& item);
|
||||
void CreatePermaItemPickups(const std::string& loc_name, const std::string& item_name);
|
||||
void CreatePermaItemPickup(const glm::vec3& position, const std::string& item);
|
||||
|
||||
void RecoverPlayer(Player& player);
|
||||
bool GetRecoveryPosition(const glm::vec3& current, glm::vec3& recovery);
|
||||
|
||||
private:
|
||||
Game& game_;
|
||||
std::vector<NpcCharacter*> npcs_;
|
||||
float daytime_offset_ = 0.0f;
|
||||
|
||||
// std::vector<NpcCharacter*> npcs_;
|
||||
size_t num_npcs_ = 0;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@ -36,6 +36,7 @@ void game::Player::Update()
|
||||
SyncWorld();
|
||||
SendMenuMsgs();
|
||||
UpdateCamera();
|
||||
SendDamageEvents();
|
||||
|
||||
// reset for next frame
|
||||
in_new_ = 0;
|
||||
@ -145,6 +146,13 @@ void game::Player::SetHudData(const PlayerHudData& hud_data)
|
||||
msg.Write(hud_data.ammo_total);
|
||||
}
|
||||
|
||||
if (hud_data.dead != hud_data_.dead)
|
||||
{
|
||||
fields |= PHUD_DEATH;
|
||||
hud_data_.dead = hud_data.dead;
|
||||
msg.Write(hud_data.dead);
|
||||
}
|
||||
|
||||
if (fields == 0)
|
||||
{
|
||||
DiscardMsg();
|
||||
@ -159,6 +167,11 @@ void game::Player::ResetHudData()
|
||||
SetHudData(PlayerHudData{});
|
||||
}
|
||||
|
||||
void game::Player::DisplayDamageEvent(DamageEventType type)
|
||||
{
|
||||
dmg_event_flags_ |= 1 << type;
|
||||
}
|
||||
|
||||
bool game::Player::GetView(glm::vec3& eye, glm::vec3& forward)
|
||||
{
|
||||
if (!world_)
|
||||
@ -236,6 +249,26 @@ void game::Player::SendWorldUpdateMsg()
|
||||
world_->PickLocalMsgs(*this, cull_pos_);
|
||||
}
|
||||
|
||||
void game::Player::SendDamageEvents()
|
||||
{
|
||||
if (dmg_event_flags_ & (1 << DAMAGE_EVENT_RECEIVED))
|
||||
SendDamageEvent(DAMAGE_EVENT_RECEIVED);
|
||||
|
||||
if (dmg_event_flags_ & (1 << DAMAGE_EVENT_DEALT))
|
||||
SendDamageEvent(DAMAGE_EVENT_DEALT);
|
||||
|
||||
if (dmg_event_flags_ & (1 << DAMAGE_EVENT_DEALT_KILL))
|
||||
SendDamageEvent(DAMAGE_EVENT_DEALT_KILL);
|
||||
|
||||
dmg_event_flags_ = 0;
|
||||
}
|
||||
|
||||
void game::Player::SendDamageEvent(DamageEventType type)
|
||||
{
|
||||
auto msg = BeginMsg(net::MSG_DAMAGE);
|
||||
msg.Write(type);
|
||||
}
|
||||
|
||||
void game::Player::SendEnv()
|
||||
{
|
||||
if (!world_ || last_env_time_ + 1000 > world_->GetTime())
|
||||
|
||||
@ -44,6 +44,8 @@ public:
|
||||
void SetHudData(const PlayerHudData& hud_data);
|
||||
void ResetHudData();
|
||||
|
||||
void DisplayDamageEvent(DamageEventType type);
|
||||
|
||||
const std::string& GetName() const { return name_; }
|
||||
|
||||
PlayerInputFlags GetInput() const { return in_; }
|
||||
@ -62,6 +64,8 @@ private:
|
||||
void UpdateCullPos();
|
||||
void SendWorldMsg();
|
||||
void SendWorldUpdateMsg();
|
||||
void SendDamageEvents();
|
||||
void SendDamageEvent(DamageEventType type);
|
||||
void SendEnv();
|
||||
|
||||
// entities sync
|
||||
@ -106,6 +110,7 @@ private:
|
||||
|
||||
// hud
|
||||
PlayerHudData hud_data_;
|
||||
uint8_t dmg_event_flags_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
#include "player_character.hpp"
|
||||
|
||||
#include "world.hpp"
|
||||
#include "input_mapping.hpp"
|
||||
#include "utils/random.hpp"
|
||||
|
||||
game::PlayerCharacter::PlayerCharacter(World& world, Player& player, const HumanCharacterTuning& tuning) : Super(world, tuning), player_(&player)
|
||||
{
|
||||
@ -11,6 +13,7 @@ game::PlayerCharacter::PlayerCharacter(World& world, Player& player, const Human
|
||||
|
||||
// give some shit
|
||||
GiveItem(std::make_shared<ItemInstance>("airsniper"), false);
|
||||
GiveAmmo("pellet", 50);
|
||||
// GiveItem(std::make_shared<ItemInstance>("ak47"), false);
|
||||
// GiveItem(std::make_shared<ItemInstance>("uzi"), false);
|
||||
}
|
||||
@ -31,6 +34,38 @@ void game::PlayerCharacter::Update()
|
||||
UpdateHudData();
|
||||
}
|
||||
|
||||
void game::PlayerCharacter::ReceiveDamage(const DamageInfo& damage)
|
||||
{
|
||||
if (!IsAlive())
|
||||
return;
|
||||
|
||||
Super::ReceiveDamage(damage);
|
||||
|
||||
if (player_)
|
||||
{
|
||||
player_->DisplayDamageEvent(DAMAGE_EVENT_RECEIVED);
|
||||
}
|
||||
|
||||
if (!IsAlive())
|
||||
{
|
||||
// just died
|
||||
std::string_view killer_name;
|
||||
auto killer_character = dynamic_cast<PlayerCharacter*>(damage.inflictor);
|
||||
if (killer_character)
|
||||
{
|
||||
auto killer_player = killer_character->GetPlayer();
|
||||
if (killer_player)
|
||||
{
|
||||
killer_name = killer_player->GetName();
|
||||
}
|
||||
}
|
||||
|
||||
SendDeathMessage(killer_name);
|
||||
SetNametag(std::string{});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void game::PlayerCharacter::ProcessInput(PlayerInputType type, bool enabled)
|
||||
{
|
||||
switch (type)
|
||||
@ -108,6 +143,19 @@ void game::PlayerCharacter::GiveAmmo(const std::string& ammo_name, size_t count)
|
||||
inventory_->ammo[ammo_name] += count;
|
||||
}
|
||||
|
||||
void game::PlayerCharacter::OnDamageDealt(bool was_kill)
|
||||
{
|
||||
if (!player_)
|
||||
return;
|
||||
|
||||
player_->DisplayDamageEvent(was_kill ? DAMAGE_EVENT_DEALT_KILL : DAMAGE_EVENT_DEALT);
|
||||
}
|
||||
|
||||
float game::PlayerCharacter::GetHitBoneDamageMultiplier(const std::string_view hitbone)
|
||||
{
|
||||
return 0.25f * Super::GetHitBoneDamageMultiplier(hitbone);
|
||||
}
|
||||
|
||||
void game::PlayerCharacter::OnRideableChanged()
|
||||
{
|
||||
UpdatePlayerCamera();
|
||||
@ -152,6 +200,24 @@ size_t game::PlayerCharacter::GetAmmo(size_t required, const std::string& ammo_n
|
||||
return give;
|
||||
}
|
||||
|
||||
void game::PlayerCharacter::SpawnLoot()
|
||||
{
|
||||
if (!inventory_)
|
||||
return;
|
||||
|
||||
for (auto& slot : inventory_->slots)
|
||||
{
|
||||
if (!slot)
|
||||
continue;
|
||||
|
||||
size_t ammo = GetAmmo(slot->def->clip_size * 5, slot->def->ammo_type);
|
||||
auto pos = root_.GetGlobalPosition() + glm::vec3(RandomFloat(-1.0f, 1.0f), RandomFloat(-1.0f, 1.0f), RandomFloat(-0.1f, 1.0f));
|
||||
GetWorld().CreateItemPickup(pos, std::move(slot), RandomInt(59000, 61000), 0, ammo);
|
||||
}
|
||||
|
||||
inventory_.reset();
|
||||
}
|
||||
|
||||
void game::PlayerCharacter::UpdatePlayerCamera()
|
||||
{
|
||||
if (!player_)
|
||||
@ -169,8 +235,8 @@ void game::PlayerCharacter::UpdatePlayerCamera()
|
||||
|
||||
void game::PlayerCharacter::UpdateInputs()
|
||||
{
|
||||
auto in = player_ ? player_->GetInput() : 0;
|
||||
|
||||
auto in = (player_ && IsAlive()) ? player_->GetInput() : 0;
|
||||
|
||||
if (auto rideable = GetRideable(); rideable)
|
||||
{
|
||||
SetInputs(0);
|
||||
@ -199,7 +265,7 @@ void game::PlayerCharacter::UpdateInputs()
|
||||
|
||||
void game::PlayerCharacter::UpdateAimTarget()
|
||||
{
|
||||
if (!player_)
|
||||
if (!player_ || !IsAlive())
|
||||
return;
|
||||
|
||||
glm::vec3 eye, forward;
|
||||
@ -221,7 +287,7 @@ void game::PlayerCharacter::UpdateAimTarget()
|
||||
|
||||
void game::PlayerCharacter::CheckItemSwitch()
|
||||
{
|
||||
if (!player_ || !inventory_)
|
||||
if (!player_ || !inventory_ || !IsAlive())
|
||||
return;
|
||||
|
||||
auto in = player_->GetNewInput();
|
||||
@ -245,7 +311,7 @@ void game::PlayerCharacter::CheckItemSwitch()
|
||||
void game::PlayerCharacter::UpdateUseTarget()
|
||||
{
|
||||
UseTargetQueryResult res{};
|
||||
auto new_use_target = world_.GetBestUseTarget(*this, res);
|
||||
auto new_use_target = IsAlive() ? world_.GetBestUseTarget(*this, res) : nullptr;
|
||||
|
||||
if (new_use_target != use_target_ || res.enabled != use_enabled_ || res.error_text != use_error_ || res.delay != use_delay_)
|
||||
{
|
||||
@ -274,6 +340,9 @@ void game::PlayerCharacter::UpdateUseTarget()
|
||||
|
||||
void game::PlayerCharacter::UseChanged(bool enabled)
|
||||
{
|
||||
if (!IsAlive())
|
||||
return;
|
||||
|
||||
if (!use_target_)
|
||||
{
|
||||
// exit rideable if not target
|
||||
@ -336,8 +405,10 @@ void game::PlayerCharacter::UpdateHudData()
|
||||
if (!player_)
|
||||
return;
|
||||
|
||||
hud_data_.health = 100;
|
||||
// general
|
||||
hud_data_.health = static_cast<uint8_t>(GetHealth());
|
||||
|
||||
// item
|
||||
const auto& item = GetHeldItem();
|
||||
hud_data_.ammo_loaded = item ? item->ammo : 0;
|
||||
|
||||
@ -351,6 +422,9 @@ void game::PlayerCharacter::UpdateHudData()
|
||||
}
|
||||
}
|
||||
|
||||
// death
|
||||
hud_data_.dead = IsDead() ? 1 : 0;
|
||||
|
||||
player_->SetHudData(hud_data_);
|
||||
}
|
||||
|
||||
@ -358,7 +432,7 @@ void game::PlayerCharacter::UpdateHudSlots()
|
||||
{
|
||||
hud_data_.weapon_slots = 0;
|
||||
|
||||
if (!inventory_)
|
||||
if (!inventory_ || IsDead())
|
||||
return;
|
||||
|
||||
for (size_t i = 0; i < 10; ++i)
|
||||
@ -369,3 +443,75 @@ void game::PlayerCharacter::UpdateHudSlots()
|
||||
|
||||
|
||||
}
|
||||
|
||||
static std::string_view GetRandomDeathMessageFormat()
|
||||
{
|
||||
switch (rand() % 8)
|
||||
{
|
||||
case 1:
|
||||
return "byl zneškodněn";
|
||||
case 2:
|
||||
return "chcíp";
|
||||
case 3:
|
||||
return "umříl";
|
||||
case 4:
|
||||
return "umřel";
|
||||
case 5:
|
||||
return "pošel";
|
||||
case 6:
|
||||
return "odešel na věčné časy";
|
||||
case 7:
|
||||
return "už není mezi námi";
|
||||
default:
|
||||
return "zesnul";
|
||||
}
|
||||
}
|
||||
|
||||
static std::string_view GetRandomDeathMessageFormatKilled()
|
||||
{
|
||||
switch (rand() % 8)
|
||||
{
|
||||
case 0:
|
||||
return "zneškodnil";
|
||||
case 1:
|
||||
return "vyřešil";
|
||||
case 2:
|
||||
return "zajebal";
|
||||
case 3:
|
||||
return "zlikvidoval";
|
||||
case 4:
|
||||
return "odstranil";
|
||||
case 5:
|
||||
return "terminoval";
|
||||
case 6:
|
||||
return "zabil";
|
||||
default:
|
||||
return "kilnul";
|
||||
}
|
||||
}
|
||||
|
||||
void game::PlayerCharacter::SendDeathMessage(std::string_view killer_name)
|
||||
{
|
||||
if (!player_)
|
||||
return;
|
||||
|
||||
std::string message;
|
||||
|
||||
if (killer_name.empty())
|
||||
{
|
||||
message += player_->GetName();
|
||||
message += "^r ";
|
||||
message += GetRandomDeathMessageFormat();
|
||||
}
|
||||
else
|
||||
{
|
||||
message += killer_name;
|
||||
message += "^r ";
|
||||
message += GetRandomDeathMessageFormatKilled();
|
||||
message += " ";
|
||||
message += player_->GetName();
|
||||
}
|
||||
|
||||
GetWorld().SendChat(message);
|
||||
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ public:
|
||||
|
||||
virtual void Update() override;
|
||||
|
||||
virtual void ReceiveDamage(const DamageInfo& damage) override;
|
||||
|
||||
void ProcessInput(PlayerInputType type, bool enabled);
|
||||
|
||||
void DetachFromPlayer();
|
||||
@ -29,12 +31,17 @@ public:
|
||||
void GiveItem(std::shared_ptr<ItemInstance> item, bool can_equip = true);
|
||||
void GiveAmmo(const std::string& ammo_name, size_t count);
|
||||
|
||||
protected:
|
||||
virtual void OnDamageDealt(bool was_kill) override;
|
||||
|
||||
protected:
|
||||
virtual float GetHitBoneDamageMultiplier(const std::string_view hitbone) override;
|
||||
|
||||
virtual void OnRideableChanged() override;
|
||||
virtual void OnAimingChanged() override;
|
||||
virtual void OnHeldItemChanged() override;
|
||||
virtual bool HaveAmmo(const std::string& ammo_name) override;
|
||||
virtual size_t GetAmmo(size_t required, const std::string& ammo_name) override;
|
||||
virtual void SpawnLoot() override;
|
||||
|
||||
private:
|
||||
void UpdatePlayerCamera();
|
||||
@ -52,6 +59,8 @@ private:
|
||||
void UpdateHudData();
|
||||
void UpdateHudSlots();
|
||||
|
||||
void SendDeathMessage(std::string_view killer_name);
|
||||
|
||||
private:
|
||||
Player* player_;
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ enum PlayerHudField : PlayerHudFields
|
||||
PHUD_ITEM = 4,
|
||||
PHUD_AMMO_LOADED = 8,
|
||||
PHUD_AMMO_TOTAL = 16,
|
||||
PHUD_DEATH = 32,
|
||||
};
|
||||
|
||||
struct PlayerHudData
|
||||
@ -21,14 +22,24 @@ struct PlayerHudData
|
||||
uint8_t health = 0;
|
||||
// TODO: stamina ?
|
||||
uint8_t weapon_slots = 0;
|
||||
|
||||
|
||||
// held item
|
||||
std::string held_item;
|
||||
uint8_t ammo_loaded = 0;
|
||||
uint32_t ammo_total = 0;
|
||||
|
||||
|
||||
// death
|
||||
uint8_t dead = 0;
|
||||
|
||||
// TODO: use target
|
||||
|
||||
};
|
||||
|
||||
enum DamageEventType : uint8_t
|
||||
{
|
||||
DAMAGE_EVENT_RECEIVED,
|
||||
DAMAGE_EVENT_DEALT,
|
||||
DAMAGE_EVENT_DEALT_KILL,
|
||||
};
|
||||
|
||||
}
|
||||
@ -2,7 +2,12 @@
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
game::Rideable::Rideable(Entity& entity, RideableType type) : entity_(entity), type_(type) {}
|
||||
#include "world.hpp"
|
||||
|
||||
game::Rideable::Rideable(Entity& entity, RideableType type) : entity_(entity), type_(type)
|
||||
{
|
||||
last_passenger_leave_time_ = entity_.GetWorld().GetTime();
|
||||
}
|
||||
|
||||
void game::Rideable::SetPassenger(size_t seat_idx, HumanCharacter* passenger)
|
||||
{
|
||||
@ -29,6 +34,7 @@ void game::Rideable::SetPassenger(size_t seat_idx, HumanCharacter* passenger)
|
||||
}
|
||||
|
||||
OnPassengerChanged(seat_idx, passenger);
|
||||
UpdateLeaveTime();
|
||||
}
|
||||
|
||||
game::HumanCharacter* game::Rideable::GetPassenger(size_t seat_idx) const
|
||||
@ -56,6 +62,28 @@ void game::Rideable::KickAll()
|
||||
}
|
||||
}
|
||||
|
||||
void game::Rideable::OnRideableDamaged(const DamageInfo& damage) const
|
||||
{
|
||||
for (const auto& seat : seats_)
|
||||
{
|
||||
if (seat.passenger)
|
||||
{
|
||||
seat.passenger->OnRideableDamaged(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool game::Rideable::IsAbandoned(int64_t time) const
|
||||
{
|
||||
// still someone in
|
||||
if (last_passenger_leave_time_ < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return entity_.GetWorld().GetTime() - last_passenger_leave_time_ >= time;
|
||||
}
|
||||
|
||||
game::Rideable::~Rideable()
|
||||
{
|
||||
// kick passengers
|
||||
@ -69,3 +97,25 @@ size_t game::Rideable::AddSeat(const glm::vec3& offset)
|
||||
seats_.emplace_back(seat);
|
||||
return seats_.size() - 1;
|
||||
}
|
||||
|
||||
void game::Rideable::UpdateLeaveTime()
|
||||
{
|
||||
size_t num_passengers = 0;
|
||||
for (const auto& seat : seats_)
|
||||
{
|
||||
if (seat.passenger)
|
||||
{
|
||||
++num_passengers;
|
||||
}
|
||||
}
|
||||
|
||||
if (num_passengers > 0)
|
||||
{
|
||||
last_passenger_leave_time_ = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
last_passenger_leave_time_ = entity_.GetWorld().GetTime();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -33,8 +33,12 @@ public:
|
||||
virtual void SetRideableInput(PlayerInputFlags in) {}
|
||||
virtual void SetRideableViewAngles(float yaw, float pitch) {}
|
||||
|
||||
void OnRideableDamaged(const DamageInfo& damage) const;
|
||||
|
||||
RideableType GetRideableType() const { return type_; }
|
||||
|
||||
bool IsAbandoned(int64_t time) const;
|
||||
|
||||
Entity& GetEntity() { return entity_; }
|
||||
const Entity& GetEntity() const { return entity_; }
|
||||
|
||||
@ -44,10 +48,15 @@ protected:
|
||||
size_t AddSeat(const glm::vec3& offset);
|
||||
virtual void OnPassengerChanged(size_t seat_idx, HumanCharacter* passenger) {}
|
||||
|
||||
private:
|
||||
void UpdateLeaveTime();
|
||||
|
||||
private:
|
||||
Entity& entity_;
|
||||
RideableType type_;
|
||||
std::vector<RideableSeat> seats_;
|
||||
|
||||
int64_t last_passenger_leave_time_ = -1;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include "player.hpp"
|
||||
#include "player_input.hpp"
|
||||
#include "utils/random.hpp"
|
||||
#include "utils/math.hpp"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
@ -13,23 +14,24 @@ static std::shared_ptr<const assets::VehicleModel> LoadVehicleModelByName(const
|
||||
return assets::CacheManager::GetVehicleModel("data/" + model_name + ".veh");
|
||||
}
|
||||
|
||||
game::Vehicle::Vehicle(World& world, const VehicleTuning& tuning)
|
||||
: Entity(world, net::ET_VEHICLE), tuning_(tuning), model_(LoadVehicleModelByName(tuning.model)),
|
||||
tuninglist_(VehicleTuningList::LoadFromFile("data/" + tuning.model + ".tun"))
|
||||
game::Vehicle::Vehicle(World& world, const VehicleSpawnInfo& info)
|
||||
: Entity(world, net::ET_VEHICLE), tuning_(info.tuning), model_(LoadVehicleModelByName(info.tuning.model)),
|
||||
tuninglist_(VehicleTuningList::LoadFromFile("data/" + info.tuning.model + ".tun"))
|
||||
{
|
||||
root_.local.position.z = 10.0f;
|
||||
root_.local.position = info.position;
|
||||
root_.local.rotation = glm::angleAxis(info.yaw, glm::vec3(0.0f, 0.0f, 1.0f));
|
||||
|
||||
wheels_.resize(model_->GetWheels().size());
|
||||
|
||||
ApplyTuning(tuning);
|
||||
ApplyTuning(info.tuning);
|
||||
|
||||
// init deform
|
||||
gfx::DeformGridInfo info{};
|
||||
info.min = glm::vec3(-1.0f, -2.5f, 0.10f);
|
||||
info.max = glm::vec3(1.0f, 2.0f, 1.8f);
|
||||
info.res = glm::ivec3(8, 16, 8);
|
||||
info.max_offset = 0.1f;
|
||||
deformgrid_ = std::make_unique<DeformGrid>(info);
|
||||
gfx::DeformGridInfo deform_info{};
|
||||
deform_info.min = glm::vec3(-1.0f, -2.5f, 0.10f);
|
||||
deform_info.max = glm::vec3(1.0f, 2.0f, 1.8f);
|
||||
deform_info.res = glm::ivec3(8, 16, 8);
|
||||
deform_info.max_offset = 0.1f;
|
||||
deformgrid_ = std::make_unique<DeformGrid>(deform_info);
|
||||
|
||||
Update();
|
||||
}
|
||||
@ -81,32 +83,32 @@ void game::Vehicle::OnContact(const collision::ContactInfo& info)
|
||||
if (info.impulse < 1000.0f)
|
||||
return;
|
||||
|
||||
if (health_ > 0.0f)
|
||||
{
|
||||
health_ -= info.impulse;
|
||||
ApplyDamage(info.impulse * 0.01f);
|
||||
Deform(info.pos, -glm::normalize(info.normal) * 0.1f, 1.0f);
|
||||
|
||||
if (health_ <= 0.0f) // just broken
|
||||
{
|
||||
PlaySound("breakwindow", 1.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
if (health_ <= 0.0f)
|
||||
{
|
||||
Deform(info.pos, -glm::normalize(info.normal) * 0.1f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void game::Vehicle::OnBulletHit(const game::BulletInfo& bullet, const btCollisionObject* hit_object)
|
||||
void game::Vehicle::ReceiveDamage(const DamageInfo& damage)
|
||||
{
|
||||
Super::OnBulletHit(bullet, hit_object);
|
||||
Super::ReceiveDamage(damage);
|
||||
|
||||
if (!physics_)
|
||||
return;
|
||||
|
||||
auto impulse = glm::normalize(bullet.end - bullet.start) * 100.0f;
|
||||
physics_->GetBtBody().activate();
|
||||
physics_->GetBtBody().applyCentralImpulse(btVector3(impulse.x, impulse.y, impulse.z));
|
||||
if (damage.type == DAMAGE_BULLET)
|
||||
{
|
||||
// TODO: adjust impulse
|
||||
auto impulse = damage.normal * -60.0f;
|
||||
auto& bt_body = physics_->GetBtBody();
|
||||
bt_body.activate();
|
||||
bt_body.applyImpulse(btVector3(impulse.x, impulse.y, impulse.z),
|
||||
btVector3(damage.impact_pos.x, damage.impact_pos.y, damage.impact_pos.z) -
|
||||
bt_body.getCenterOfMassPosition());
|
||||
|
||||
ApplyDamage(damage.damage * 0.2f);
|
||||
// Deform(damage.impact_pos, damage.normal * -0.1f, 1.0f);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void game::Vehicle::SetInput(VehicleInputType type, bool enable)
|
||||
@ -188,7 +190,11 @@ void game::Vehicle::ProcessInput()
|
||||
float steeringClamp = std::max(minsc, (1.f - (std::abs(speed) / sl)) * maxsc);
|
||||
// steeringClamp = .5f;
|
||||
float steeringSpeed = steeringClamp * 5.0f;
|
||||
if (steering_analog_)
|
||||
steeringSpeed *= 3.0f;
|
||||
|
||||
float steeringInc = steeringSpeed * t_delta;
|
||||
float steeringDec = steeringInc * 2.0f;
|
||||
|
||||
const bool in_forward = in_ & (1 << VIN_FORWARD);
|
||||
const bool in_backward = in_ & (1 << VIN_BACKWARD);
|
||||
@ -263,23 +269,9 @@ void game::Vehicle::ProcessInput()
|
||||
}
|
||||
else
|
||||
{
|
||||
if (steering_ < target_steering_)
|
||||
{
|
||||
steering_ += steeringInc;
|
||||
if (steering_ > target_steering_)
|
||||
steering_ = target_steering_;
|
||||
}
|
||||
else if (steering_ > target_steering_)
|
||||
{
|
||||
steering_ -= steeringInc;
|
||||
if (steering_ < target_steering_)
|
||||
steering_ = target_steering_;
|
||||
}
|
||||
|
||||
if (steering_ > steeringClamp)
|
||||
steering_ = steeringClamp;
|
||||
else if (steering_ < -steeringClamp)
|
||||
steering_ = -steeringClamp;
|
||||
auto target_steering_clamped = glm::clamp(target_steering_, -steeringClamp, steeringClamp);
|
||||
MoveToward(steering_, target_steering_clamped,
|
||||
glm::abs(target_steering_clamped) < glm::abs(steering_) ? steeringInc : steeringDec);
|
||||
}
|
||||
|
||||
auto& vehicle = physics_->GetBtVehicle();
|
||||
@ -537,6 +529,20 @@ void game::Vehicle::SendUpdateMsg()
|
||||
msg.WriteAt(fields_pos, fields);
|
||||
}
|
||||
|
||||
void game::Vehicle::ApplyDamage(float damage)
|
||||
{
|
||||
if (health_ <= 0.0f)
|
||||
return;
|
||||
|
||||
health_ -= damage;
|
||||
|
||||
if (health_ <= 0.0f) // just broken
|
||||
{
|
||||
PlaySound("breakwindow", 1.0f, 1.0f);
|
||||
health_ = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void game::Vehicle::WriteDeformSync(net::OutMessage& msg) const
|
||||
{
|
||||
const auto texels = deformgrid_->GetData();
|
||||
@ -568,6 +574,9 @@ void game::Vehicle::WriteDeformSync(net::OutMessage& msg) const
|
||||
|
||||
void game::Vehicle::Deform(const glm::vec3& pos, const glm::vec3& deform, float radius)
|
||||
{
|
||||
if (health_ > 0.0f)
|
||||
return;
|
||||
|
||||
net::PositionQ pos_q;
|
||||
net::PositionQ deform_q;
|
||||
net::EncodePosition(pos, pos_q);
|
||||
|
||||
@ -59,18 +59,25 @@ private:
|
||||
std::unique_ptr<btCollisionObject> bullet_hitbox_;
|
||||
};
|
||||
|
||||
struct VehicleSpawnInfo
|
||||
{
|
||||
glm::vec3 position;
|
||||
float yaw;
|
||||
VehicleTuning tuning;
|
||||
};
|
||||
|
||||
class Vehicle : public Entity
|
||||
{
|
||||
public:
|
||||
using Super = Entity;
|
||||
|
||||
Vehicle(World& world, const VehicleTuning& tuning);
|
||||
Vehicle(World& world, const VehicleSpawnInfo& info);
|
||||
|
||||
virtual void Update() override;
|
||||
virtual void SendInitData(Player& player, net::OutMessage& msg) const override;
|
||||
|
||||
virtual void OnContact(const collision::ContactInfo& info) override;
|
||||
virtual void OnBulletHit(const game::BulletInfo& bullet, const btCollisionObject* hit_object);
|
||||
virtual void ReceiveDamage(const DamageInfo& damage) override;
|
||||
|
||||
void SetInput(VehicleInputType type, bool enable);
|
||||
void SetInputs(VehicleInputFlags inputs) { in_ = inputs; }
|
||||
@ -103,6 +110,8 @@ private:
|
||||
VehicleSyncFieldFlags WriteState(net::OutMessage& msg, const VehicleSyncState& base) const;
|
||||
void SendUpdateMsg();
|
||||
|
||||
void ApplyDamage(float damage);
|
||||
|
||||
void WriteDeformSync(net::OutMessage& msg) const;
|
||||
void Deform(const glm::vec3& pos, const glm::vec3& deform, float radius);
|
||||
void SendDeformMsg(const net::PositionQ& pos, const net::PositionQ& deform);
|
||||
@ -136,7 +145,7 @@ private:
|
||||
|
||||
VehicleInputFlags in_ = 0;
|
||||
|
||||
float health_ = 10000.0f;
|
||||
float health_ = 100.0f;
|
||||
|
||||
float crash_intensity_ = 0.0f;
|
||||
size_t no_crash_frames_ = 0;
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
#include "utils/allocnum.hpp"
|
||||
#include "player_character.hpp"
|
||||
#include "net/utils.hpp"
|
||||
#include "marker.hpp"
|
||||
#include "utils/math.hpp"
|
||||
|
||||
game::World::World(std::string mapname) : Scheduler(time_ms_), map_(*this, std::move(mapname)) {}
|
||||
|
||||
@ -236,17 +238,31 @@ void game::World::FireBullet(const BulletInfo& bullet)
|
||||
return;
|
||||
}
|
||||
|
||||
hit_normal = glm::normalize(hit_normal);
|
||||
|
||||
auto obj_cb = collision::GetObjectCallback(hit_obj);
|
||||
obj_cb->OnBulletHit(bullet, hit_obj);
|
||||
|
||||
if (obj_cb)
|
||||
{
|
||||
// apply damage
|
||||
DamageInfo damage;
|
||||
damage.type = DAMAGE_BULLET;
|
||||
damage.damage = bullet.damage;
|
||||
damage.from_pos = bullet.start;
|
||||
damage.impact_pos = hit_pos;
|
||||
damage.inflictor = bullet.shooter;
|
||||
damage.hit_object = hit_obj;
|
||||
damage.normal = hit_normal;
|
||||
obj_cb->ReceiveDamage(damage);
|
||||
}
|
||||
|
||||
// TODO: remove
|
||||
const float box_extent = 0.1f;
|
||||
// const float box_extent = 0.1f;
|
||||
// BeamBox(hit_pos - box_extent, hit_pos + box_extent, GetMaterialColor(material), 1.0f);
|
||||
|
||||
// Beam(bullet.start, hit_pos, 0x0044DD, 0.04f);
|
||||
|
||||
// effect
|
||||
Effect(GetMaterialImpactFx(material), hit_pos, glm::normalize(hit_normal));
|
||||
Effect(GetMaterialImpactFx(material), hit_pos, hit_normal);
|
||||
}
|
||||
|
||||
void game::World::Beam(const glm::vec3& start, const glm::vec3& end, uint32_t color, float time)
|
||||
@ -301,6 +317,84 @@ void game::World::SendChat(const std::string& text)
|
||||
msg.Write(net::ChatMessage(text));
|
||||
}
|
||||
|
||||
void game::World::CreateItemPickup(const glm::vec3& position, std::shared_ptr<ItemInstance> item, int64_t despawn_time,
|
||||
int64_t respawn_time, size_t ammo_count)
|
||||
{
|
||||
MarkerInfo marker_info{};
|
||||
marker_info.position = position;
|
||||
marker_info.type = MARKER_PICKUP;
|
||||
marker_info.color = 0xFFFFFF;
|
||||
marker_info.model = item->def->model_name;
|
||||
|
||||
auto& marker = Spawn<Marker>(marker_info);
|
||||
marker.SetUseTarget(
|
||||
"sebrat ^ccc" + item->def->displayname,
|
||||
[](PlayerCharacter& character, UseTargetQueryResult& res) {
|
||||
res.enabled = true;
|
||||
res.delay = 0.1f;
|
||||
res.error_text = nullptr;
|
||||
return true;
|
||||
},
|
||||
[this, position, item, despawn_time, respawn_time, ammo_count, &marker](PlayerCharacter& character) {
|
||||
auto player = character.GetPlayer();
|
||||
if (!player)
|
||||
return;
|
||||
|
||||
character.GiveItem(item);
|
||||
|
||||
if (ammo_count > 0)
|
||||
{
|
||||
character.GiveAmmo(item->def->ammo_type, ammo_count);
|
||||
}
|
||||
|
||||
character.PlaySound("pickup_ammo");
|
||||
|
||||
player->SendChat("sebrals ^ccc" + item->def->displayname);
|
||||
marker.SetUseable(false);
|
||||
marker.Remove();
|
||||
|
||||
if (respawn_time > 0)
|
||||
{
|
||||
Schedule(respawn_time, [this, position, item, despawn_time, respawn_time, ammo_count]() {
|
||||
CreateItemPickup(position, item, despawn_time, respawn_time, ammo_count);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (despawn_time > 0)
|
||||
{
|
||||
marker.Schedule(despawn_time, [&marker]{
|
||||
marker.Remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static void ApplyCrashDamage(collision::ObjectCallback& obj_cb, collision::ObjectCallback* other_obj_cb, float impulse, const glm::vec3& normal, const glm::vec3& velocity)
|
||||
{
|
||||
if (glm::length(impulse) < 1000.0f)
|
||||
return;
|
||||
|
||||
if (normal.z < -0.707f)
|
||||
return;
|
||||
|
||||
// float velocity_magnitude = glm::length(velocity);
|
||||
float dmg = glm::mix(10.0f, 100.0f, impulse * 0.0001f);
|
||||
|
||||
// if (dmg < 10.0f)
|
||||
// return;
|
||||
|
||||
game::DamageInfo damage{};
|
||||
damage.type = game::DAMAGE_CRASH;
|
||||
damage.impulse = impulse;
|
||||
damage.inflictor = other_obj_cb ? other_obj_cb->GetResponsibleCharacter() : nullptr;
|
||||
damage.damage = dmg;
|
||||
|
||||
if (damage.inflictor)
|
||||
{
|
||||
obj_cb.ReceiveDamage(damage);
|
||||
}
|
||||
}
|
||||
|
||||
void game::World::HandleContacts()
|
||||
{
|
||||
auto& bt_world = GetBtWorld();
|
||||
@ -319,12 +413,22 @@ void game::World::HandleContacts()
|
||||
|
||||
if (cb && (flags & collision::OF_NOTIFY_CONTACT))
|
||||
{
|
||||
|
||||
collision::ContactInfo info;
|
||||
info.pos = glm::vec3(pos.x(), pos.y(), pos.z());
|
||||
info.normal = glm::vec3(normal.x(), normal.y(), normal.z());
|
||||
info.impulse = impulse;
|
||||
// info.other_velocity = glm::vec3(ov.x(), ov.y(), ov.z());
|
||||
cb->OnContact(info);
|
||||
}
|
||||
|
||||
if (cb && (flags & collision::OF_CRASH_DAMAGE))
|
||||
{
|
||||
auto ov = other_body->getLinearVelocity();
|
||||
auto other_obj_cb = collision::GetObjectCallback(other_body);
|
||||
ApplyCrashDamage(*cb, other_obj_cb, impulse, glm::normalize(glm::vec3(normal.x(), normal.y(), normal.z())),
|
||||
glm::vec3(ov.x(), ov.y(), ov.z()));
|
||||
}
|
||||
|
||||
if (type == collision::OT_MAP_OBJECT && (flags & collision::OF_DESTRUCTIBLE))
|
||||
{
|
||||
|
||||
@ -9,18 +9,39 @@
|
||||
#include "net/defs.hpp"
|
||||
#include "player_input.hpp"
|
||||
#include "usable.hpp"
|
||||
#include "item_instance.hpp"
|
||||
|
||||
namespace game
|
||||
{
|
||||
|
||||
enum DamageType
|
||||
{
|
||||
DAMAGE_OTHER,
|
||||
DAMAGE_BULLET,
|
||||
DAMAGE_CRASH,
|
||||
};
|
||||
|
||||
class HumanCharacter;
|
||||
|
||||
struct DamageInfo
|
||||
{
|
||||
DamageType type = DAMAGE_OTHER;
|
||||
float damage = 0.0f;
|
||||
float impulse = 0.0f;
|
||||
glm::vec3 from_pos{};
|
||||
glm::vec3 impact_pos{};
|
||||
glm::vec3 normal{};
|
||||
HumanCharacter* inflictor = nullptr;
|
||||
const btCollisionObject* hit_object = nullptr;
|
||||
};
|
||||
|
||||
struct BulletInfo
|
||||
{
|
||||
game::HumanCharacter* shooter;
|
||||
HumanCharacter* shooter;
|
||||
glm::vec3 start;
|
||||
glm::vec3 end;
|
||||
float damage;
|
||||
float impulse;
|
||||
};
|
||||
|
||||
class World : public collision::DynamicsWorld, public net::MsgProducer, public net::LocalMsgProducer, public Scheduler
|
||||
@ -72,6 +93,9 @@ public:
|
||||
|
||||
void SendChat(const std::string& text);
|
||||
|
||||
void CreateItemPickup(const glm::vec3& position, std::shared_ptr<ItemInstance> item, int64_t despawn_time,
|
||||
int64_t respawn_time, size_t ammo_count);
|
||||
|
||||
virtual ~World() = default;
|
||||
|
||||
private:
|
||||
|
||||
@ -12,8 +12,6 @@
|
||||
|
||||
game::view::ClientSession::ClientSession(App& app) : app_(app), hud_(app.GetTime())
|
||||
{
|
||||
crosshair_texture_ = assets::CacheManager::GetTexture("data/crosshair.png");
|
||||
|
||||
// send login
|
||||
auto msg = BeginMsg(net::MSG_ID);
|
||||
msg.Write<net::Version>(FEKAL_VERSION);
|
||||
@ -54,6 +52,9 @@ bool game::view::ClientSession::ProcessSingleMessage(net::MessageType type, net:
|
||||
case net::MSG_HUD:
|
||||
return ProcessHudMsg(msg);
|
||||
|
||||
case net::MSG_DAMAGE:
|
||||
return ProcessDamageMsg(msg);
|
||||
|
||||
case net::MSG_USETARGET:
|
||||
return ProcessUseTargetMsg(msg);
|
||||
|
||||
@ -100,6 +101,8 @@ void game::view::ClientSession::Update(const UpdateInfo& info)
|
||||
SendViewAngles(info.time);
|
||||
UpdateCamera(info);
|
||||
}
|
||||
|
||||
hud_.Update(info.delta_time);
|
||||
}
|
||||
|
||||
void game::view::ClientSession::Draw(gfx::DrawList& dlist, gfx::DrawListParams& params, gui::Context& gui)
|
||||
@ -110,7 +113,6 @@ void game::view::ClientSession::Draw(gfx::DrawList& dlist, gfx::DrawListParams&
|
||||
|
||||
if (world_->IsLoaded())
|
||||
{
|
||||
DrawCrosshair(gui);
|
||||
hud_.Draw(gui);
|
||||
}
|
||||
}
|
||||
@ -188,16 +190,18 @@ bool game::view::ClientSession::ProcessHudMsg(net::InMessage& msg)
|
||||
hud_data.held_item = item_name;
|
||||
|
||||
// determine clip size
|
||||
std::string displayname = hud_data.held_item;
|
||||
size_t clip_size = 0;
|
||||
size_t item_slot = 0;
|
||||
if (!hud_data.held_item.empty())
|
||||
{
|
||||
auto item = assets::CacheManager::GetItem("data/" + hud_data.held_item + ".item");
|
||||
displayname = item->displayname;
|
||||
clip_size = item->clip_size;
|
||||
item_slot = item->slot;
|
||||
}
|
||||
|
||||
hud_.SetItemInfo(hud_data.held_item, item_slot, clip_size);
|
||||
hud_.SetItemInfo(displayname, item_slot, clip_size);
|
||||
}
|
||||
|
||||
if (fields & PHUD_AMMO_LOADED)
|
||||
@ -216,6 +220,32 @@ bool game::view::ClientSession::ProcessHudMsg(net::InMessage& msg)
|
||||
hud_.SetTotalAmmo(static_cast<size_t>(hud_data.ammo_total));
|
||||
}
|
||||
|
||||
if (fields & PHUD_DEATH)
|
||||
{
|
||||
if (!msg.Read(hud_data.dead))
|
||||
return false;
|
||||
|
||||
hud_.SetDead(hud_data.dead > 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool game::view::ClientSession::ProcessDamageMsg(net::InMessage& msg)
|
||||
{
|
||||
DamageEventType type;
|
||||
if (!msg.Read(type))
|
||||
return false;
|
||||
|
||||
if (type == DAMAGE_EVENT_RECEIVED)
|
||||
{
|
||||
hud_.ShowDamageReceived();
|
||||
}
|
||||
else
|
||||
{
|
||||
hud_.ShowDamageDealt(type == DAMAGE_EVENT_DEALT_KILL);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -275,6 +305,8 @@ void game::view::ClientSession::UpdateCamera(const UpdateInfo& info)
|
||||
camera_controller_.SetAiming(camera_info_.flags & CAM_AIMING);
|
||||
camera_controller_.Update(info.delta_time);
|
||||
camera_controller_.Recalculate(world_.get());
|
||||
|
||||
hud_.SetDisplayCrosshair(camera_controller_.GetAimFactor() >= 0.5f);
|
||||
}
|
||||
|
||||
void game::view::ClientSession::DrawWorld(gfx::DrawList& dlist, gfx::DrawListParams& params, gui::Context& gui)
|
||||
@ -366,17 +398,3 @@ game::view::RemoteMenuView* game::view::ClientSession::FindMenu(net::MenuId id)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void game::view::ClientSession::DrawCrosshair(gui::Context& gui) const
|
||||
{
|
||||
if (camera_controller_.GetAimFactor() < 0.5f)
|
||||
return; // no aiming no crosshair
|
||||
|
||||
const float crosshair_size = 32.0f;
|
||||
|
||||
auto& viewport_size = gui.GetViewportSize();
|
||||
|
||||
auto p0 = viewport_size * 0.5f - crosshair_size * 0.5f;
|
||||
auto p1 = p0 + crosshair_size;
|
||||
|
||||
gui.DrawRect(p0, p1, 0xFFFFFFFF, crosshair_texture_.get());
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ private:
|
||||
bool ProcessCameraMsg(net::InMessage& msg);
|
||||
bool ProcessChatMsg(net::InMessage& msg);
|
||||
bool ProcessHudMsg(net::InMessage& msg);
|
||||
bool ProcessDamageMsg(net::InMessage& msg);
|
||||
bool ProcessUseTargetMsg(net::InMessage& msg);
|
||||
bool ProcessMenuMsg(net::InMessage& msg);
|
||||
|
||||
@ -55,8 +56,6 @@ private:
|
||||
bool ProcessMenuInput(game::PlayerInputType in);
|
||||
RemoteMenuView* FindMenu(net::MenuId id) const;
|
||||
|
||||
void DrawCrosshair(gui::Context& gui) const;
|
||||
|
||||
private:
|
||||
App& app_;
|
||||
|
||||
@ -72,8 +71,6 @@ private:
|
||||
gui::PlayerHud hud_;
|
||||
|
||||
std::vector<std::unique_ptr<RemoteMenuView>> remote_menus_;
|
||||
|
||||
std::shared_ptr<const gfx::Texture> crosshair_texture_;
|
||||
};
|
||||
|
||||
} // namespace game::view
|
||||
@ -1,7 +1,10 @@
|
||||
#include "player_hud.hpp"
|
||||
|
||||
#include <format>
|
||||
|
||||
#include "player_hud.hpp"
|
||||
#include "utils/math.hpp"
|
||||
|
||||
#include "assets/cache.hpp"
|
||||
|
||||
static uint32_t COLOR_ACTIVE = 0xFF00FFFF;
|
||||
static uint32_t COLOR_NORMAL = 0xFFFFFFFF;
|
||||
static uint32_t COLOR_DISABLED = 0xFFCCCCCC;
|
||||
@ -19,6 +22,8 @@ static uint32_t COLOR_ERROR = 0xFF7777FF;
|
||||
|
||||
gui::PlayerHud::PlayerHud(const float& time) : time_(time)
|
||||
{
|
||||
crosshair_texture_ = assets::CacheManager::GetTexture("data/crosshair.png");
|
||||
|
||||
UpdateWeaponSlotsText();
|
||||
}
|
||||
|
||||
@ -49,11 +54,31 @@ void gui::PlayerHud::SetUseTargetData(std::string text, std::string error_text,
|
||||
ut_end_time_ = delay > 0.01f ? ut_start_time_ + delay : ut_start_time_;
|
||||
}
|
||||
|
||||
void gui::PlayerHud::Draw(Context& ctx) const
|
||||
void gui::PlayerHud::ShowDamageReceived()
|
||||
{
|
||||
damage_received_factor_ = glm::min(damage_received_factor_ + 0.2f, 0.5f);
|
||||
}
|
||||
|
||||
void gui::PlayerHud::ShowDamageDealt(bool kill)
|
||||
{
|
||||
(kill ? damage_dealt_kill_factor_ : damage_dealt_factor_) = 1.0f;
|
||||
}
|
||||
|
||||
void gui::PlayerHud::Update(float delta_time)
|
||||
{
|
||||
MoveToward(damage_received_factor_, 0.0f, 1.0f * delta_time);
|
||||
MoveToward(damage_dealt_factor_, 0.0f, 5.0f * delta_time);
|
||||
MoveToward(damage_dealt_kill_factor_, 0.0f, 2.0f * delta_time);
|
||||
}
|
||||
|
||||
void gui::PlayerHud::Draw(Context& ctx) const
|
||||
{
|
||||
DrawPain(ctx);
|
||||
DrawCrosshair(ctx);
|
||||
DrawHealthBar(ctx);
|
||||
DrawItemInfo(ctx);
|
||||
DrawUseTarget(ctx);
|
||||
DrawDeathScreen(ctx);
|
||||
}
|
||||
|
||||
void gui::PlayerHud::UpdateWeaponSlotsText()
|
||||
@ -77,6 +102,41 @@ void gui::PlayerHud::UpdateWeaponSlotsText()
|
||||
}
|
||||
}
|
||||
|
||||
void gui::PlayerHud::DrawPain(Context& ctx) const
|
||||
{
|
||||
if (damage_received_factor_ <= 0.01f)
|
||||
return;
|
||||
|
||||
glm::vec4 color(1.0f, 0.3f, 0.3f, damage_received_factor_);
|
||||
ctx.DrawRect(glm::vec2(0.0f), ctx.GetViewportSize(), glm::packUnorm4x8(color));
|
||||
}
|
||||
|
||||
void gui::PlayerHud::DrawCrosshair(Context& ctx) const
|
||||
{
|
||||
if (!display_crosshair_)
|
||||
return;
|
||||
|
||||
glm::vec3 color(1.0f);
|
||||
if (damage_dealt_factor_ > 0.01f)
|
||||
{
|
||||
color = glm::mix(color, glm::vec3(0.3f), damage_dealt_factor_);
|
||||
}
|
||||
|
||||
if (damage_dealt_kill_factor_ > 0.01f)
|
||||
{
|
||||
color = glm::mix(color, glm::vec3(1.0f, 0.1f, 0.1f), damage_dealt_kill_factor_);
|
||||
}
|
||||
|
||||
const float crosshair_size = 32.0f;
|
||||
|
||||
auto& viewport_size = ctx.GetViewportSize();
|
||||
|
||||
auto p0 = viewport_size * 0.5f - crosshair_size * 0.5f;
|
||||
auto p1 = p0 + crosshair_size;
|
||||
|
||||
ctx.DrawRect(p0, p1, glm::packUnorm4x8(glm::vec4(color, 1.0f)), crosshair_texture_.get());
|
||||
}
|
||||
|
||||
void gui::PlayerHud::DrawHealthBar(Context& ctx) const
|
||||
{
|
||||
const float margin = 30.0f;
|
||||
@ -195,3 +255,11 @@ void gui::PlayerHud::DrawUseTarget(Context& ctx) const
|
||||
ctx.DrawRect(progress_p0, progress_p1_bar, COLOR_ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
void gui::PlayerHud::DrawDeathScreen(Context& ctx) const
|
||||
{
|
||||
if (!dead_)
|
||||
return;
|
||||
|
||||
ctx.DrawTextAligned("si chcíp", ctx.GetViewportSize() * 0.5f, glm::vec2(-0.5f), 0xFFFFFFFF, 3.0f);
|
||||
}
|
||||
|
||||
@ -20,20 +20,31 @@ public:
|
||||
|
||||
void SetUseTargetData(std::string text, std::string error_text, float delay);
|
||||
|
||||
void SetDisplayCrosshair(bool show) { display_crosshair_ = show; }
|
||||
void ShowDamageReceived();
|
||||
void ShowDamageDealt(bool kill);
|
||||
|
||||
void SetDead(bool dead) { dead_ = dead; }
|
||||
|
||||
void Update(float delta_time);
|
||||
void Draw(Context& ctx) const;
|
||||
|
||||
private:
|
||||
void UpdateWeaponSlotsText();
|
||||
|
||||
void DrawPain(Context& ctx) const;
|
||||
void DrawCrosshair(Context& ctx) const;
|
||||
void DrawHealthBar(Context& ctx) const;
|
||||
void DrawItemInfo(Context& ctx) const;
|
||||
|
||||
void DrawUseTarget(Context& ctx) const;
|
||||
void DrawDeathScreen(Context& ctx) const;
|
||||
|
||||
private:
|
||||
const float& time_;
|
||||
|
||||
// resources
|
||||
std::shared_ptr<const gfx::Texture> crosshair_texture_;
|
||||
|
||||
// general
|
||||
float health_ = 0.0f;
|
||||
|
||||
@ -54,8 +65,14 @@ private:
|
||||
float ut_start_time_ = 0.0f;
|
||||
float ut_end_time_ = 0.0f;
|
||||
|
||||
|
||||
|
||||
// crosshair & events
|
||||
bool display_crosshair_ = false;
|
||||
float damage_received_factor_ = 0.0f;
|
||||
float damage_dealt_factor_ = 0.0f;
|
||||
float damage_dealt_kill_factor_ = 0.0f;
|
||||
|
||||
// death
|
||||
bool dead_ = false;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -49,6 +49,9 @@ enum MessageType : uint8_t
|
||||
// HUD ...
|
||||
MSG_HUD,
|
||||
|
||||
// DAMAGE ...
|
||||
MSG_DAMAGE,
|
||||
|
||||
/*~~~~~~~~ Entity ~~~~~~~~*/
|
||||
// ENTSPAWN <EntNum> <EntType> data...
|
||||
MSG_ENTSPAWN,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user