diff --git a/src/assets/item.cpp b/src/assets/item.cpp index 8f16038..224b724 100644 --- a/src/assets/item.cpp +++ b/src/assets/item.cpp @@ -24,6 +24,10 @@ std::shared_ptr 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::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; } \ No newline at end of file diff --git a/src/assets/item.hpp b/src/assets/item.hpp index 439f8f0..bf53d5b 100644 --- a/src/assets/item.hpp +++ b/src/assets/item.hpp @@ -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; diff --git a/src/collision/object_info.hpp b/src/collision/object_info.hpp index db550bb..4ef3d00 100644 --- a/src/collision/object_info.hpp +++ b/src/collision/object_info.hpp @@ -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; }; diff --git a/src/game/animal.cpp b/src/game/animal.cpp index f0efb27..9f3b68e 100644 --- a/src/game/animal.cpp +++ b/src/game/animal.cpp @@ -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) diff --git a/src/game/animal.hpp b/src/game/animal.hpp index c478d13..f9c872e 100644 --- a/src/game/animal.hpp +++ b/src/game/animal.hpp @@ -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); diff --git a/src/game/character.cpp b/src/game/character.cpp index 780006b..baf098b 100644 --- a/src/game/character.cpp +++ b/src/game/character.cpp @@ -1,8 +1,13 @@ #include "character.hpp" + +#include + #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(), glm::two_pi()) - glm::pi(); 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); diff --git a/src/game/character.hpp b/src/game/character.hpp index 9e3d2b6..b54b2ec 100644 --- a/src/game/character.hpp +++ b/src/game/character.hpp @@ -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 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 hitbone_names_; + + float health_ = 100.0f; + int64_t death_time_ = -1; + + std::function on_death_; }; } // namespace game \ No newline at end of file diff --git a/src/game/drivable_vehicle.cpp b/src/game/drivable_vehicle.cpp index 5c7c12c..63bad1f 100644 --- a/src/game/drivable_vehicle.cpp +++ b/src/game/drivable_vehicle.cpp @@ -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)); diff --git a/src/game/drivable_vehicle.hpp b/src/game/drivable_vehicle.hpp index 1f8bcc8..27951cb 100644 --- a/src/game/drivable_vehicle.hpp +++ b/src/game/drivable_vehicle.hpp @@ -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: diff --git a/src/game/enterable_world.cpp b/src/game/enterable_world.cpp index 2e4b00d..3185110 100644 --- a/src/game/enterable_world.cpp +++ b/src/game/enterable_world.cpp @@ -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: diff --git a/src/game/enterable_world.hpp b/src/game/enterable_world.hpp index e40b471..23ae742 100644 --- a/src/game/enterable_world.hpp +++ b/src/game/enterable_world.hpp @@ -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_characters_; - + glm::vec3 spawnpoint_{}; }; diff --git a/src/game/game.cpp b/src/game/game.cpp index 862481e..adfbf3b 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -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(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(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(passenger); if (!player_passenger) diff --git a/src/game/human_character.cpp b/src/game/human_character.cpp index 83d2952..d54f6b0 100644 --- a/src/game/human_character.cpp +++ b/src/game/human_character.cpp @@ -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 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_; } diff --git a/src/game/human_character.hpp b/src/game/human_character.hpp index 3398eb1..6c531c8 100644 --- a/src/game/human_character.hpp +++ b/src/game/human_character.hpp @@ -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 item); const std::shared_ptr& 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; }; } diff --git a/src/game/npc_character.cpp b/src/game/npc_character.cpp index 1254cfb..40fc24e 100644 --- a/src/game/npc_character.cpp +++ b/src/game/npc_character.cpp @@ -1,77 +1,274 @@ #include "npc_character.hpp" #include "openworld.hpp" #include "drivable_vehicle.hpp" +#include "utils/random.hpp" +#include "player_character.hpp" #include #include -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 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().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(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(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 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 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 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() / 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 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 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< target_speed * 1.1f && gas_) + else if (speed > target_speed * 1.1f && ((vehicle_in_ & (1< 0)) { - gas_ = false; - } - - if (gas_) - { - vin |= 1 << game::VIN_FORWARD; + // no gas + vehicle_in_ &= ~(1< target_speed * 1.4f) { - vin |= 1 << game::VIN_BACKWARD; + // brake + vehicle_in_ |= (1<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 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 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< 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_; } diff --git a/src/game/npc_character.hpp b/src/game/npc_character.hpp index c7bdb78..8ecc709 100644 --- a/src/game/npc_character.hpp +++ b/src/game/npc_character.hpp @@ -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 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 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 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 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 weapon_; + + net::EntNum enemy_num_ = 0; + HumanCharacter* enemy_ = nullptr; + int64_t enemy_time_ = 0; + + bool follow_enemy_ = false; }; } \ No newline at end of file diff --git a/src/game/openworld.cpp b/src/game/openworld.cpp index 5851ca3..4669955 100644 --- a/src/game/openworld.cpp +++ b/src/game/openworld.cpp @@ -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(twingo_tuning); - veh.SetPosition({110.0f, 100.0f, 5.0f}); + auto& veh = Spawn(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(col) * 4.0f, 165.0f + static_cast(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(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("ak47")); + + // hit target npc 2 + auto& npc2 = SpawnRandomNpc(); + npc2.SetPosition({80.0f, 100.0f, 5.0f}); + npc2.SetWeapon(std::make_shared("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(tuning); + auto& vehicle = Spawn(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(npc_tuning); + auto& npc = Spawn(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 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("uzi")); + } + + if (Chance(0.3f)) + { + auto& passenger = SpawnRandomNpc(); + passenger.Ride(&vehicle, 1); + + if (Chance(0.5f)) + { + passenger.SetWeapon(std::make_shared(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_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(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(item_def), 0, 5000, item_def->clip_size * 15); } void game::OpenWorld::RecoverPlayer(Player& player) diff --git a/src/game/openworld.hpp b/src/game/openworld.hpp index 0eaa8e2..86234e8 100644 --- a/src/game/openworld.hpp +++ b/src/game/openworld.hpp @@ -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 npcs_; float daytime_offset_ = 0.0f; + + // std::vector npcs_; + size_t num_npcs_ = 0; + }; } \ No newline at end of file diff --git a/src/game/player.cpp b/src/game/player.cpp index 3d89ed7..90717c3 100644 --- a/src/game/player.cpp +++ b/src/game/player.cpp @@ -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()) diff --git a/src/game/player.hpp b/src/game/player.hpp index 46dafe1..12a669f 100644 --- a/src/game/player.hpp +++ b/src/game/player.hpp @@ -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; }; } \ No newline at end of file diff --git a/src/game/player_character.cpp b/src/game/player_character.cpp index ae6e90f..6ff6a0e 100644 --- a/src/game/player_character.cpp +++ b/src/game/player_character.cpp @@ -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("airsniper"), false); + GiveAmmo("pellet", 50); // GiveItem(std::make_shared("ak47"), false); // GiveItem(std::make_shared("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(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(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); + +} diff --git a/src/game/player_character.hpp b/src/game/player_character.hpp index d699aab..940f3dc 100644 --- a/src/game/player_character.hpp +++ b/src/game/player_character.hpp @@ -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 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_; diff --git a/src/game/player_hud_data.hpp b/src/game/player_hud_data.hpp index 8380fac..29a628e 100644 --- a/src/game/player_hud_data.hpp +++ b/src/game/player_hud_data.hpp @@ -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, +}; + } \ No newline at end of file diff --git a/src/game/rideable.cpp b/src/game/rideable.cpp index 47c409d..c6f04bc 100644 --- a/src/game/rideable.cpp +++ b/src/game/rideable.cpp @@ -2,7 +2,12 @@ #include -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(); + } + +} diff --git a/src/game/rideable.hpp b/src/game/rideable.hpp index efe1730..59625d7 100644 --- a/src/game/rideable.hpp +++ b/src/game/rideable.hpp @@ -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 seats_; + + int64_t last_passenger_leave_time_ = -1; }; diff --git a/src/game/vehicle.cpp b/src/game/vehicle.cpp index 8404f7b..80442d5 100644 --- a/src/game/vehicle.cpp +++ b/src/game/vehicle.cpp @@ -5,6 +5,7 @@ #include "player.hpp" #include "player_input.hpp" #include "utils/random.hpp" +#include "utils/math.hpp" #include @@ -13,23 +14,24 @@ static std::shared_ptr 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(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(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); diff --git a/src/game/vehicle.hpp b/src/game/vehicle.hpp index e4f8a47..f4ae5bd 100644 --- a/src/game/vehicle.hpp +++ b/src/game/vehicle.hpp @@ -59,18 +59,25 @@ private: std::unique_ptr 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; diff --git a/src/game/world.cpp b/src/game/world.cpp index 5995a4c..1a55759 100644 --- a/src/game/world.cpp +++ b/src/game/world.cpp @@ -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 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_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)) { diff --git a/src/game/world.hpp b/src/game/world.hpp index 4d2b7c4..0983cac 100644 --- a/src/game/world.hpp +++ b/src/game/world.hpp @@ -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 item, int64_t despawn_time, + int64_t respawn_time, size_t ammo_count); + virtual ~World() = default; private: diff --git a/src/gameview/client_session.cpp b/src/gameview/client_session.cpp index 6ceaff2..005a9a8 100644 --- a/src/gameview/client_session.cpp +++ b/src/gameview/client_session.cpp @@ -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(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(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()); -} diff --git a/src/gameview/client_session.hpp b/src/gameview/client_session.hpp index e9773bd..0978b5a 100644 --- a/src/gameview/client_session.hpp +++ b/src/gameview/client_session.hpp @@ -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> remote_menus_; - - std::shared_ptr crosshair_texture_; }; } // namespace game::view \ No newline at end of file diff --git a/src/gui/player_hud.cpp b/src/gui/player_hud.cpp index 22be3fe..e6d6056 100644 --- a/src/gui/player_hud.cpp +++ b/src/gui/player_hud.cpp @@ -1,7 +1,10 @@ -#include "player_hud.hpp" - #include +#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); +} diff --git a/src/gui/player_hud.hpp b/src/gui/player_hud.hpp index 731f895..09b0bad 100644 --- a/src/gui/player_hud.hpp +++ b/src/gui/player_hud.hpp @@ -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 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; }; diff --git a/src/net/defs.hpp b/src/net/defs.hpp index 6a3ca21..d2d3582 100644 --- a/src/net/defs.hpp +++ b/src/net/defs.hpp @@ -49,6 +49,9 @@ enum MessageType : uint8_t // HUD ... MSG_HUD, + // DAMAGE ... + MSG_DAMAGE, + /*~~~~~~~~ Entity ~~~~~~~~*/ // ENTSPAWN data... MSG_ENTSPAWN,