#pragma once #include "BoneAnimatedModelNew.h" #include "render/Renderer.h" #include "render/TextureManager.h" #include "SparkEmitter.h" #include #include #include #include #include #include #include namespace ZL { class TextRenderer; enum class AnimationState { STAND = 0, WALK = 1, STAND_TO_ACTION = 2, ACTION_ATTACK = 3, ACTION_ATTACK_2 = 4, ACTION_IDLE = 5, ACTION_TO_STAND = 6, ACTION_TO_DEATH = 7, DEATH_IDLE = 8, }; class Character { public: using PathPlanner = std::function( const Eigen::Vector3f& start, const Eigen::Vector3f& end)>; void loadAnimation(AnimationState state, const std::string& filename, const std::string& zipFile = ""); void loadBinaryAnimation(AnimationState state, const std::string& filename, const std::string& zipFile = ""); // Assigns the given texture to a specific mesh (by name, as defined in the animation file). void setTexture(const std::string& meshName, std::shared_ptr texture); // Assigns the given texture to every mesh in every loaded animation. // Call AFTER loading animations so the mesh names are known. void setTexture(std::shared_ptr texture); void update(int64_t deltaMs); // onArrived is called once when the character reaches this target. // From Python you can chain further commands (walk, wait, trigger others). void setTarget(const Eigen::Vector3f& target, std::function onArrived = nullptr); void setPathPlanner(PathPlanner planner); void draw(Renderer& renderer); void drawShadowDepth(Renderer& renderer); void drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera); // Character-to-character collision (XZ-plane). Used by Location to keep // player/NPCs from walking through each other. float collisionRadius = 0.45f; // Movement/path introspection used for dynamic replanning. bool isMoving() const; const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; } Eigen::Vector3f getCurrentNavigationTarget() const; void forceReplan(); // Cancels any active walk target and clears waypoints so the character // stays at its current position. Used when an external push moves the // character and we don't want it to walk back to where it was. void stopInPlace(); void resetPlayerAfterDeath(); // attackDirection is a world-space horizontal vector pointing from the // attacker toward this character — i.e. the direction the hit pushes // them. A zero vector disables directional sparks (isotropic burst). void applyDamage(float damageAmount, const Eigen::Vector3f& attackDirection = Eigen::Vector3f::Zero()); // Configures the per-character hit-spark emitter with the shared spark // texture. Safe to call once per character after construction. void setupHitSparks(std::shared_ptr sparkTexture); // Call per-frame between update() and draw(), with the active camera // view matrix (world → eye). The emitter billboards and sorts its // particles against that camera before its VBO is uploaded. void prepareHitSparksForDraw(const Eigen::Matrix4f& cameraViewMatrix); // Draws the NPC name floating above this character in 2D screen space. // No-op for the player, for dead characters, or if npcName is empty. // The text is green for peaceful (canAttack == false) NPCs, red otherwise. void drawName(TextRenderer& textRenderer, const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& projectionMatrix); // Draws a horizontal health bar above this character in 2D screen space. // Visible only for the player and hostile NPCs (canAttack == true), and // only while hp > 0. The left portion is green (hp / initialHp) and the // remainder is red. void drawHealthBar(Renderer& renderer, const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& projectionMatrix); // Public: read by Game for camera tracking and ray-cast origin Eigen::Vector3f position = Eigen::Vector3f(0.f, 0.f, 0.f); float facingAngle = 0.0f; float targetFacingAngle = 0.0f; // Per-character tuning — set after construction, before first update float walkSpeed = 3.0f; float rotationSpeed = 4.0f; float modelScale = 0.12f; // Applied after scale, fixes model-space orientation (e.g. Blender Z-up exports) Eigen::Quaternionf modelCorrectionRotation = Eigen::Quaternionf::Identity(); std::string npcId; std::string npcName; float hp = 200.f; float initialHp = 200.f; // The position the NPC should return to if pushed too far away. // Initialised to the spawn position; updated whenever a script walk command is issued. Eigen::Vector3f homePosition = Eigen::Vector3f::Zero(); // Maximum allowed drift from homePosition before a return walk is triggered (metres). // Set to 0 to disable drift recovery for this character. float maxHomeDrift = 3.0f; int battle_state = 0; bool resetAnim = false; int attack = 0; AnimationState currentState = AnimationState::STAND; float attack_cooldown = 0.f; bool canAttack = false; Character* attackTarget = nullptr; // While standing still, smoothly rotate once to face this character. // Auto-cleared by update() the moment this character finishes rotating, // so it acts as a one-shot orient (e.g. an NPC turning to the player at // the start of a conversation, then holding that pose). Character* faceTarget = nullptr; bool isPlayer = false; bool enabled = true; bool useGpuSkinning = true; //bool useGpuSkinning = false; float interactionRadius = 0.0f; VertexRenderStruct weaponMesh; std::shared_ptr weaponTexture; Eigen::Matrix3f weaponInitialRotation = Eigen::Matrix3f::Identity(); Eigen::Vector3f weaponInitialPosition = Eigen::Vector3f::Zero(); std::string weaponAttachBoneName = "RightHand"; bool showWeapon = true; SparkEmitter hitSparkEmitter; float slowMoTimeRemaining = 0.0f; float slowMoTransitionTime = 0.5f; float slowMoActiveTime = 6.0f; float getSpeedMultiplier(float transitionProgress) const; // Called once when the death animation finishes and the character enters DEATH_IDLE. std::function onDeathAnimComplete; // Called whenever hp changes (e.g. on damage). Arguments: current hp, max hp. std::function onHpChanged; private: std::map animations; VertexRenderStruct modelMutable; VertexRenderStruct healthBarMesh; std::unordered_map> meshTextures; Eigen::Vector3f walkTarget = Eigen::Vector3f(0.f, 0.f, 0.f); Eigen::Vector3f requestedWalkTarget = Eigen::Vector3f(0.f, 0.f, 0.f); std::vector pathWaypoints; size_t currentWaypointIndex = 0; PathPlanner pathPlanner; std::function onArrivedCallback; static constexpr float WALK_THRESHOLD = 0.05f; static constexpr float TARGET_REPLAN_THRESHOLD = 0.25f; static constexpr float HOME_DRIFT_CHECK_INTERVAL = 5.0f; float homeDriftCheckTimer = HOME_DRIFT_CHECK_INTERVAL; // Returns the animation state to actually play/draw, falling back to IDLE // if the requested state has no loaded animation. AnimationState resolveActiveState() const; bool prepareGpuSkinning(); // Draws the weapon mesh attached to weaponAttachBoneName. // Caller must ensure the right shader is active with matching uniforms. void drawAttachedWeapon(Renderer& renderer); // GPU skinning: draw using shader-based skinning void drawGpuSkinning(Renderer& renderer); // Shadow: draw into depth map (no texture, no projection push) void drawShadowDepthGpuSkinning(Renderer& renderer); void drawShadowDepthCpu(Renderer& renderer); // Shadow: draw with shadow-aware shaders void drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera); void drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera); }; } // namespace ZL