Adapt for web, make CharacterState class
This commit is contained in:
1 parent
c891161d69
commit
0eb9d0be25
88 files changed
+810
-102277
No files matched your search
+358
-440
File diff suppressed because it is too large.
Load diff
+63
-162
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
#include "CharacterState.h"
|
||||
#include "BoneAnimatedModelNew.h"
|
||||
#include "render/Renderer.h"
|
||||
#include "render/TextureManager.h"
|
||||
@@ -15,39 +16,63 @@ 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:
|
||||
// Resolves a character index to a live Character pointer.
|
||||
// kNoTarget (-1) → nullptr, kPlayerIndex (-2) → Location::player, 0..n → npcs[n].
|
||||
using CharacterResolver = std::function<Character*(int index)>;
|
||||
|
||||
using PathPlanner = std::function<std::vector<Eigen::Vector3f>(
|
||||
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 = "");
|
||||
// All serializable game state (position, hp, flags, pathfinding, etc.)
|
||||
CharacterState state;
|
||||
|
||||
// Assigns the given texture to a specific mesh (by name, as defined in the animation file).
|
||||
// ---- Asset loading ----
|
||||
void loadAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile = "");
|
||||
void loadBinaryAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile = "");
|
||||
|
||||
// Assigns a texture to a specific mesh by name.
|
||||
void setTexture(const std::string& meshName, std::shared_ptr<Texture> texture);
|
||||
// Assigns the given texture to every mesh in every loaded animation.
|
||||
// Call AFTER loading animations so the mesh names are known.
|
||||
// Assigns one texture to every mesh in every loaded animation. Call AFTER loading animations.
|
||||
void setTexture(std::shared_ptr<ZL::Texture> 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<void()> onArrived = nullptr);
|
||||
|
||||
// Creates a fully-loaded Character from a CharacterState (including its creationInfo).
|
||||
// Loads all animations, textures, and weapon mesh/texture from the paths stored in creationInfo.
|
||||
static std::unique_ptr<Character> createFromState(CharacterState st, Renderer& renderer, const char* zipFile);
|
||||
|
||||
// ---- Per-frame update ----
|
||||
// resolver maps character indices to live pointers so attackTargetIndex / faceTargetIndex
|
||||
// can be dereferenced inside update(). Pass nullptr only if no targets are set.
|
||||
void update(int64_t deltaMs, const CharacterResolver& resolver = nullptr);
|
||||
|
||||
void setPathPlanner(PathPlanner planner);
|
||||
|
||||
// ---- Navigation (path data lives in state; pathPlanner lives here) ----
|
||||
void setTarget(const Eigen::Vector3f& target, std::function<void()> onArrived = nullptr);
|
||||
void forceReplan();
|
||||
void stopInPlace() { state.stopInPlace(); }
|
||||
|
||||
// ---- Reset ----
|
||||
void resetPlayerAfterDeath();
|
||||
|
||||
// ---- Damage ----
|
||||
void applyDamage(float damageAmount, const Eigen::Vector3f& attackDirection = Eigen::Vector3f::Zero());
|
||||
|
||||
// ---- Spark setup ----
|
||||
void setupHitSparks(std::shared_ptr<Texture> sparkTexture);
|
||||
void prepareHitSparksForDraw(const Eigen::Matrix4f& cameraViewMatrix);
|
||||
|
||||
// ---- HUD overlays ----
|
||||
void drawName(TextRenderer& textRenderer,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix);
|
||||
void drawHealthBar(Renderer& renderer,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix);
|
||||
|
||||
// ---- Draw passes ----
|
||||
void draw(Renderer& renderer);
|
||||
void drawShadowDepth(Renderer& renderer);
|
||||
void drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
||||
@@ -64,167 +89,44 @@ public:
|
||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
|
||||
const float* ambientColor, const float* fogColor);
|
||||
|
||||
// Character-to-character collision (XZ-plane). Used by Location to keep
|
||||
// player/NPCs from walking through each other.
|
||||
float collisionRadius = 0.45f;
|
||||
// ---- Query helpers (delegate to state) ----
|
||||
bool isMoving() const { return state.isMoving(); }
|
||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return state.getRequestedWalkTarget(); }
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const { return state.getCurrentNavigationTarget(); }
|
||||
float getHp() const { return state.getHp(); }
|
||||
void setHp(float newHp) { state.setHp(newHp); }
|
||||
|
||||
// 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();
|
||||
float getSpeedMultiplier(float transitionProgress) const;
|
||||
|
||||
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<Texture> 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 initialHp = 200.f;
|
||||
protected:
|
||||
float hp = 200.f;
|
||||
public:
|
||||
|
||||
float getHp() const {
|
||||
return hp;
|
||||
}
|
||||
|
||||
void setHp(float newHp) {
|
||||
hp = newHp;
|
||||
if (onHpChanged) onHpChanged(hp, initialHp);
|
||||
}
|
||||
|
||||
// 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;
|
||||
// ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ----
|
||||
VertexRenderStruct weaponMesh;
|
||||
std::shared_ptr<Texture> weaponTexture;
|
||||
Eigen::Matrix3f weaponInitialRotation = Eigen::Matrix3f::Identity();
|
||||
Eigen::Vector3f weaponInitialPosition = Eigen::Vector3f::Zero();
|
||||
std::string weaponAttachBoneName = "RightHand";
|
||||
bool showWeapon = true;
|
||||
Eigen::Matrix3f weaponInitialRotation = Eigen::Matrix3f::Identity();
|
||||
Eigen::Vector3f weaponInitialPosition = Eigen::Vector3f::Zero();
|
||||
std::string weaponAttachBoneName = "RightHand";
|
||||
|
||||
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<void()> onDeathAnimComplete;
|
||||
|
||||
// Called whenever hp changes (e.g. on damage). Arguments: current hp, max hp.
|
||||
std::function<void(float hp, float maxHp)> onHpChanged;
|
||||
|
||||
private:
|
||||
|
||||
std::map<AnimationState, BoneAnimationDataNew> animations;
|
||||
VertexRenderStruct modelMutable;
|
||||
VertexRenderStruct healthBarMesh;
|
||||
std::unordered_map<std::string, std::shared_ptr<Texture>> 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<Eigen::Vector3f> pathWaypoints;
|
||||
size_t currentWaypointIndex = 0;
|
||||
PathPlanner pathPlanner;
|
||||
std::function<void()> onArrivedCallback;
|
||||
|
||||
static constexpr float WALK_THRESHOLD = 0.05f;
|
||||
static constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
|
||||
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();
|
||||
void drawAttachedWeapon(Renderer& renderer);
|
||||
|
||||
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);
|
||||
// Night: draw with spot-light shaders
|
||||
void drawNightGpuSkinning(Renderer& renderer,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
@@ -233,7 +135,6 @@ private:
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
const float* ambientColor, const float* fogColor);
|
||||
// Night: draw with spot-light + shadow shaders
|
||||
void drawNightGpuSkinningWithShadow(Renderer& renderer,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "CharacterState.h"
|
||||
|
||||
namespace ZL {
|
||||
|
||||
bool CharacterState::isMoving() const {
|
||||
Eigen::Vector3f toTarget = walkTarget - position;
|
||||
toTarget.y() = 0.f;
|
||||
return !pathWaypoints.empty() || toTarget.norm() > kWalkThreshold;
|
||||
}
|
||||
|
||||
Eigen::Vector3f CharacterState::getCurrentNavigationTarget() const {
|
||||
if (!pathWaypoints.empty() && currentWaypointIndex < pathWaypoints.size()) {
|
||||
return pathWaypoints[currentWaypointIndex];
|
||||
}
|
||||
return walkTarget;
|
||||
}
|
||||
|
||||
void CharacterState::stopInPlace() {
|
||||
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||
requestedWalkTarget = walkTarget;
|
||||
pathWaypoints.clear();
|
||||
currentWaypointIndex = 0;
|
||||
onArrivedCallback = nullptr;
|
||||
}
|
||||
|
||||
void CharacterState::setHp(float newHp) {
|
||||
hp = newHp;
|
||||
if (hp < 0.f) hp = 0.f;
|
||||
if (onHpChanged) onHpChanged(hp, initialHp);
|
||||
}
|
||||
|
||||
} // namespace ZL
|
||||
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/Geometry>
|
||||
|
||||
namespace ZL {
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
// Paths and fixed parameters needed to recreate a Character's visual assets
|
||||
// from scratch (e.g. after loading a saved game).
|
||||
struct CharacterCreationInfo {
|
||||
// Each entry is (AnimationState, filepath). Binary .anim format is auto-detected.
|
||||
std::vector<std::pair<AnimationState, std::string>> animationPaths;
|
||||
// Per-mesh texture paths (mesh name → path); takes priority over singleTexturePath.
|
||||
std::unordered_map<std::string, std::string> meshTexturePaths;
|
||||
// Single shared texture path (used when setTexture(shared_ptr) was called on all meshes).
|
||||
std::string singleTexturePath;
|
||||
// Weapon asset paths and attachment setup
|
||||
std::string weaponMeshPath;
|
||||
std::string weaponTexturePath;
|
||||
std::string weaponAttachBoneName = "RightHand";
|
||||
Eigen::Vector3f weaponInitialPosition = Eigen::Vector3f::Zero();
|
||||
// Weapon orientation stored as a quaternion (serializable as 4 floats).
|
||||
Eigen::Quaternionf weaponInitialRotation = Eigen::Quaternionf::Identity();
|
||||
// Optional mesh-space transformations applied after loading the weapon mesh.
|
||||
Eigen::Vector3f weaponMeshMove = Eigen::Vector3f::Zero();
|
||||
float weaponMeshScale = 1.0f;
|
||||
};
|
||||
|
||||
// All serializable game state for a character.
|
||||
// This is a member of Character; rendering-only data lives in Character itself.
|
||||
class CharacterState {
|
||||
public:
|
||||
// Special index values for attackTargetIndex / faceTargetIndex
|
||||
static constexpr int kNoTarget = -1; // no target (was nullptr)
|
||||
static constexpr int kPlayerIndex = -2; // refers to Location::player
|
||||
|
||||
// Distance below which the character is considered to have reached its target.
|
||||
static constexpr float kWalkThreshold = 0.05f;
|
||||
|
||||
// --- Identity ---
|
||||
std::string npcId;
|
||||
std::string npcName;
|
||||
bool isPlayer = false;
|
||||
bool enabled = true;
|
||||
bool canAttack = false;
|
||||
|
||||
// --- Position / orientation ---
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float facingAngle = 0.0f;
|
||||
float targetFacingAngle = 0.0f;
|
||||
|
||||
// --- Health ---
|
||||
float hp = 200.f;
|
||||
float initialHp = 200.f;
|
||||
|
||||
// --- Combat flags ---
|
||||
int battle_state = 0;
|
||||
bool resetAnim = false;
|
||||
int attack = 0;
|
||||
AnimationState currentState = AnimationState::STAND;
|
||||
float attack_cooldown = 0.f;
|
||||
|
||||
// Weapon visibility toggle (changes during animation)
|
||||
bool showWeapon = true;
|
||||
|
||||
// --- Index-based character references (replace raw pointers) ---
|
||||
int attackTargetIndex = kNoTarget;
|
||||
int faceTargetIndex = kNoTarget;
|
||||
|
||||
// --- Home position for drift recovery ---
|
||||
Eigen::Vector3f homePosition = Eigen::Vector3f::Zero();
|
||||
float maxHomeDrift = 3.0f;
|
||||
|
||||
// --- Slow-motion effect ---
|
||||
float slowMoTimeRemaining = 0.0f;
|
||||
float slowMoActiveTime = 6.0f;
|
||||
float slowMoTransitionTime = 0.5f;
|
||||
|
||||
// --- Movement / visual parameters ---
|
||||
float walkSpeed = 3.0f;
|
||||
float rotationSpeed = 4.0f;
|
||||
float modelScale = 0.12f;
|
||||
Eigen::Quaternionf modelCorrectionRotation = Eigen::Quaternionf::Identity();
|
||||
bool useGpuSkinning = true;
|
||||
float collisionRadius = 0.45f;
|
||||
float interactionRadius = 0.0f;
|
||||
|
||||
// --- Pathfinding data (serializable parts) ---
|
||||
Eigen::Vector3f walkTarget = Eigen::Vector3f::Zero();
|
||||
Eigen::Vector3f requestedWalkTarget = Eigen::Vector3f::Zero();
|
||||
std::vector<Eigen::Vector3f> pathWaypoints;
|
||||
size_t currentWaypointIndex = 0;
|
||||
|
||||
// Internal drift-check timer (serializable so it survives save/load).
|
||||
float homeDriftCheckTimer = 5.0f;
|
||||
|
||||
// --- Non-serializable callbacks (re-wired after each load) ---
|
||||
std::function<void()> onArrivedCallback;
|
||||
std::function<void()> onDeathAnimComplete;
|
||||
std::function<void(float, float)> onHpChanged;
|
||||
|
||||
// --- Asset / creation info (used to reload visuals from a saved state) ---
|
||||
CharacterCreationInfo creationInfo;
|
||||
|
||||
// --- Pure-state methods ---
|
||||
bool isMoving() const;
|
||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; }
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||
void stopInPlace();
|
||||
float getHp() const { return hp; }
|
||||
void setHp(float newHp);
|
||||
};
|
||||
|
||||
} // namespace ZL
|
||||
+18
-14
@@ -14,6 +14,7 @@
|
||||
#include <android/log.h>
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef EMSCRIPTEN
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
@@ -326,10 +327,10 @@ namespace ZL
|
||||
menuManager.setChatUnread(idx, unread, msg);
|
||||
};
|
||||
if (locations["uni_interior"]->player)
|
||||
locations["uni_interior"]->player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
locations["uni_interior"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
for (auto& npc : locations["uni_interior"]->npcs) {
|
||||
if (npc && npc->canAttack) {
|
||||
npc->onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
||||
if (npc && npc->state.canAttack) {
|
||||
npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +369,7 @@ namespace ZL
|
||||
menuManager.setChatUnread(idx, unread, msg);
|
||||
};
|
||||
if (locations["uni_exterior"]->player)
|
||||
locations["uni_exterior"]->player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
locations["uni_exterior"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
|
||||
LocationSetup params_dorm;
|
||||
//params_dorm.gameObjectsJsonPath = "resources/config2/gameobjects_dorm.json";
|
||||
@@ -415,7 +416,7 @@ namespace ZL
|
||||
menuManager.setChatUnread(idx, unread, msg);
|
||||
};
|
||||
if (locations["location_dorm"]->player)
|
||||
locations["location_dorm"]->player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
locations["location_dorm"]->player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
|
||||
locations["location_dorm"]->onPlayerTaxiRequired = [this]() {
|
||||
menuManager.tutorialShowTaxiHint();
|
||||
@@ -453,14 +454,17 @@ namespace ZL
|
||||
return;
|
||||
}
|
||||
if (currentLocation)
|
||||
{
|
||||
currentLocation->scriptEngine.callLocationExitCallback();
|
||||
currentLocation->dialogueSystem.stopDialogue();
|
||||
}
|
||||
currentLocation = it->second;
|
||||
if (currentLocation->player) {
|
||||
currentLocation->player->position = destPos;
|
||||
currentLocation->player->state.position = destPos;
|
||||
currentLocation->player->setTarget(destPos);
|
||||
currentLocation->player->stopInPlace();
|
||||
currentLocation->player->facingAngle = destRotY;
|
||||
currentLocation->player->targetFacingAngle = destRotY;
|
||||
currentLocation->player->state.facingAngle = destRotY;
|
||||
currentLocation->player->state.targetFacingAngle = destRotY;
|
||||
}
|
||||
currentLocation->cameraAzimuth = destRotY;
|
||||
currentLocation->isDarklands = isDarklands;
|
||||
@@ -572,7 +576,7 @@ namespace ZL
|
||||
// Wire HP-change callbacks so all player instances update the health bar HUD.
|
||||
for (auto& [name, loc] : locations) {
|
||||
if (loc->player) {
|
||||
loc->player->onHpChanged = [this](float hp, float maxHp) {
|
||||
loc->player->state.onHpChanged = [this](float hp, float maxHp) {
|
||||
menuManager.updateHealthBar(hp, maxHp);
|
||||
};
|
||||
}
|
||||
@@ -1051,15 +1055,15 @@ namespace ZL
|
||||
//std::cout << "current y: " << y << std::endl;
|
||||
//y = y - 0.002;
|
||||
|
||||
std::cout << "Player pos: " << currentLocation->player->position.transpose() << std::endl;
|
||||
std::cout << "Player pos: " << currentLocation->player->state.position.transpose() << std::endl;
|
||||
//currentLocation->npcs[0]->walkSpeed -= 0.01f;
|
||||
//std::cout << "Walk speed: " << currentLocation->npcs[0]->walkSpeed << std::endl;
|
||||
break;
|
||||
|
||||
case SDLK_p:
|
||||
currentLocation = locations["uni_interior"];
|
||||
currentLocation->player->position = Eigen::Vector3f(-0.0189243, 0, -13.4314);
|
||||
currentLocation->player->setTarget(currentLocation->player->position);
|
||||
currentLocation->player->state.position = Eigen::Vector3f(-0.0189243, 0, -13.4314);
|
||||
currentLocation->player->setTarget(currentLocation->player->state.position);
|
||||
//std::cout << "Switched to location " << ((currentLocation == locations["location1"]) ? "1" : "2") << std::endl;
|
||||
break;
|
||||
|
||||
@@ -1151,12 +1155,12 @@ namespace ZL
|
||||
if (!currentLocation) return;
|
||||
|
||||
if (currentLocation->player) {
|
||||
currentLocation->player->slowMoTimeRemaining = currentLocation->player->slowMoActiveTime;
|
||||
currentLocation->player->state.slowMoTimeRemaining = currentLocation->player->state.slowMoActiveTime;
|
||||
}
|
||||
|
||||
for (auto& npc : currentLocation->npcs) {
|
||||
if (npc) {
|
||||
npc->slowMoTimeRemaining = npc->slowMoActiveTime;
|
||||
npc->state.slowMoTimeRemaining = npc->state.slowMoActiveTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+92
-93
@@ -87,20 +87,20 @@ namespace ZL
|
||||
player->weaponInitialPosition = Eigen::Vector3f(0, 0.09, 0.016);
|
||||
|
||||
player->setTexture(playerTexture);
|
||||
player->walkSpeed = 3.0f;
|
||||
player->rotationSpeed = 8.0f;
|
||||
player->state.walkSpeed = 3.0f;
|
||||
player->state.rotationSpeed = 8.0f;
|
||||
|
||||
player->modelScale = 1.f;
|
||||
player->modelCorrectionRotation = Eigen::Quaternionf(
|
||||
player->state.modelScale = 1.f;
|
||||
player->state.modelCorrectionRotation = Eigen::Quaternionf(
|
||||
Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())
|
||||
*
|
||||
Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitX())
|
||||
|
||||
|
||||
);
|
||||
|
||||
player->canAttack = true;
|
||||
player->isPlayer = true;
|
||||
player->position = params.playerPosition;
|
||||
player->state.canAttack = true;
|
||||
player->state.isPlayer = true;
|
||||
player->state.position = params.playerPosition;
|
||||
player->setTarget(params.playerPosition);
|
||||
player->setupHitSparks(sparkTexture);
|
||||
std::cout << "Load resurces step 9" << std::endl;
|
||||
@@ -111,10 +111,10 @@ namespace ZL
|
||||
npcs = GameObjectLoader::loadAndCreate_Npcs(params.npcsJsonPath, CONST_ZIP_FILE);
|
||||
for (auto& npc : npcs) {
|
||||
if (!npc) continue;
|
||||
npc->homePosition = Eigen::Vector3f(npc->position.x(), 0.f, npc->position.z());
|
||||
if (npc->canAttack) {
|
||||
npc->state.homePosition = Eigen::Vector3f(npc->state.position.x(), 0.f, npc->state.position.z());
|
||||
if (npc->state.canAttack) {
|
||||
npc->setupHitSparks(sparkTexture);
|
||||
npc->attackTarget = player.get();
|
||||
npc->state.attackTargetIndex = CharacterState::kPlayerIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,16 +385,16 @@ namespace ZL
|
||||
|
||||
const auto addCharacter = [&](const Character* other) {
|
||||
if (!other || other == self) return;
|
||||
if (other->getHp() <= 0.f || !other->enabled) return;
|
||||
if (other->getHp() <= 0.f || !other->state.enabled) return;
|
||||
if (other->isMoving()) return;
|
||||
|
||||
if (distancePointToSegmentXZ(other->position, start, end) > kDynamicObstacleInfluenceDist) {
|
||||
if (distancePointToSegmentXZ(other->state.position, start, end) > kDynamicObstacleInfluenceDist) {
|
||||
return;
|
||||
}
|
||||
|
||||
PathFinder::DynamicObstacle obs;
|
||||
obs.position = Eigen::Vector3f(other->position.x(), navigation->getFloorY(), other->position.z());
|
||||
obs.radius = (std::max)(0.0f, other->collisionRadius * 0.6f);
|
||||
obs.position = Eigen::Vector3f(other->state.position.x(), navigation->getFloorY(), other->state.position.z());
|
||||
obs.radius = (std::max)(0.0f, other->state.collisionRadius * 0.6f);
|
||||
dynamicObstacles.push_back(obs);
|
||||
};
|
||||
|
||||
@@ -540,12 +540,12 @@ namespace ZL
|
||||
|
||||
for (auto& npc : npcs) {
|
||||
if (npc->getHp() <= 0.f) {
|
||||
//std::cout << "[RAYCAST_NPC] " << npc->npcId << " is dead, skipping" << std::endl;
|
||||
//std::cout << "[RAYCAST_NPC] " << npc->state.npcId << " is dead, skipping" << std::endl;
|
||||
continue;
|
||||
}
|
||||
if (!npc->enabled) continue;
|
||||
const float dx = rayOrigin.x() - npc->position.x();
|
||||
const float dz = rayOrigin.z() - npc->position.z();
|
||||
if (!npc->state.enabled) continue;
|
||||
const float dx = rayOrigin.x() - npc->state.position.x();
|
||||
const float dz = rayOrigin.z() - npc->state.position.z();
|
||||
const float a = rayDir.x() * rayDir.x() + rayDir.z() * rayDir.z();
|
||||
if (a < 1e-6f) continue; // purely-vertical ray; not a real click case
|
||||
|
||||
@@ -562,7 +562,7 @@ namespace ZL
|
||||
float exitT = tFar;
|
||||
|
||||
// Clip against the cylinder's vertical extent.
|
||||
const float yMin = npc->position.y();
|
||||
const float yMin = npc->state.position.y();
|
||||
const float yMax = yMin + NPC_CLICK_HEIGHT;
|
||||
if (std::abs(rayDir.y()) > 1e-6f) {
|
||||
const float tYa = (yMin - rayOrigin.y()) / rayDir.y();
|
||||
@@ -617,7 +617,7 @@ namespace ZL
|
||||
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
|
||||
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||
|
||||
@@ -679,7 +679,7 @@ namespace ZL
|
||||
{
|
||||
if (!shadowMap) return;
|
||||
|
||||
const Eigen::Vector3f& sceneCenter = player ? player->position : Eigen::Vector3f::Zero();
|
||||
const Eigen::Vector3f& sceneCenter = player ? player->state.position : Eigen::Vector3f::Zero();
|
||||
shadowMap->updateLightSpaceMatrix(sceneCenter);
|
||||
|
||||
shadowMap->bind();
|
||||
@@ -788,7 +788,7 @@ namespace ZL
|
||||
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
|
||||
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||
|
||||
@@ -899,7 +899,7 @@ namespace ZL
|
||||
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
|
||||
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||
|
||||
@@ -998,7 +998,7 @@ namespace ZL
|
||||
if (player && !pointLights.empty()) {
|
||||
float minDist = FLT_MAX;
|
||||
for (int i = 0; i < static_cast<int>(pointLights.size()); ++i) {
|
||||
const float d = (pointLights[i].position - player->position).norm();
|
||||
const float d = (pointLights[i].position - player->state.position).norm();
|
||||
if (d < minDist) { minDist = d; nearestLightIdx = i; }
|
||||
}
|
||||
}
|
||||
@@ -1012,12 +1012,12 @@ namespace ZL
|
||||
if (!active && player) {
|
||||
if (pl.autoLightLimitX > 0.0f && pl.autoLightLimitY > 0.0f) {
|
||||
// Rectangular activation zone — independent of nearest-light heuristic
|
||||
const float dx = std::abs(player->position.x() - pl.position.x());
|
||||
const float dz = std::abs(player->position.z() - pl.position.z());
|
||||
const float dx = std::abs(player->state.position.x() - pl.position.x());
|
||||
const float dz = std::abs(player->state.position.z() - pl.position.z());
|
||||
active = (dx <= pl.autoLightLimitX && dz <= pl.autoLightLimitY);
|
||||
} else {
|
||||
// Legacy: only the nearest autoLight activates, with optional distance cap
|
||||
const float dist = (pl.position - player->position).norm();
|
||||
const float dist = (pl.position - player->state.position).norm();
|
||||
active = (i == nearestLightIdx) &&
|
||||
(pl.autoLightDistance <= 0.0f || dist <= pl.autoLightDistance);
|
||||
}
|
||||
@@ -1033,7 +1033,7 @@ namespace ZL
|
||||
if (player && !activeLightIndices.empty()) {
|
||||
float minDist = FLT_MAX;
|
||||
for (int idx : activeLightIndices) {
|
||||
const float d = (pointLights[idx].position - player->position).norm();
|
||||
const float d = (pointLights[idx].position - player->state.position).norm();
|
||||
if (d < minDist) { minDist = d; shadowLight = &pointLights[idx]; }
|
||||
}
|
||||
}
|
||||
@@ -1081,7 +1081,7 @@ namespace ZL
|
||||
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
const Eigen::Vector3f& camTarget = player ? player->position : Eigen::Vector3f::Zero();
|
||||
const Eigen::Vector3f& camTarget = player ? player->state.position : Eigen::Vector3f::Zero();
|
||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||
|
||||
@@ -1212,7 +1212,7 @@ namespace ZL
|
||||
|
||||
void Location::fireBumpCallbacks(Character* mover, Character* standing)
|
||||
{
|
||||
if (mover->isPlayer) {
|
||||
if (mover->state.isPlayer) {
|
||||
// Player walked into a standing NPC
|
||||
const int idx = findNpcIndex(standing);
|
||||
if (idx < 0) return;
|
||||
@@ -1221,7 +1221,7 @@ namespace ZL
|
||||
cooldown = NPC_BUMP_CALLBACK_COOLDOWN;
|
||||
scriptEngine.callNpcBumpedByPlayerCallback(idx);
|
||||
}
|
||||
} else if (standing->isPlayer) {
|
||||
} else if (standing->state.isPlayer) {
|
||||
// Moving NPC walked into the standing player
|
||||
const int idx = findNpcIndex(mover);
|
||||
if (idx < 0) return;
|
||||
@@ -1235,12 +1235,12 @@ namespace ZL
|
||||
|
||||
void Location::nudgeCharacterAside(Character* standing, const Eigen::Vector3f& awayFrom)
|
||||
{
|
||||
if (!standing || standing->isPlayer) return;
|
||||
if (!standing || standing->state.isPlayer) return;
|
||||
if (!navigation || !navigation->isReady()) return;
|
||||
|
||||
static constexpr float kNudgeDist = 1.2f;
|
||||
|
||||
Eigen::Vector3f dir = standing->position - awayFrom;
|
||||
Eigen::Vector3f dir = standing->state.position - awayFrom;
|
||||
dir.y() = 0.f;
|
||||
if (dir.norm() < 1e-3f) dir = Eigen::Vector3f(1.f, 0.f, 0.f);
|
||||
else dir.normalize();
|
||||
@@ -1248,7 +1248,7 @@ namespace ZL
|
||||
const float angles[] = { 0.f, static_cast<float>(M_PI * 0.5), static_cast<float>(-M_PI * 0.5), static_cast<float>(M_PI) };
|
||||
for (float angle : angles) {
|
||||
Eigen::Vector3f rotated = Eigen::AngleAxisf(angle, Eigen::Vector3f::UnitY()) * dir;
|
||||
Eigen::Vector3f candidate = standing->position + rotated * kNudgeDist;
|
||||
Eigen::Vector3f candidate = standing->state.position + rotated * kNudgeDist;
|
||||
candidate.y() = 0.f;
|
||||
if (navigation->isWalkable(candidate)) {
|
||||
standing->setTarget(candidate);
|
||||
@@ -1284,13 +1284,13 @@ namespace ZL
|
||||
Character* a = characters[i];
|
||||
Character* b = characters[j];
|
||||
if (!a || !b) continue;
|
||||
if (a->getHp() <= 0.f || b->getHp() <= 0.f || !a->enabled || !b->enabled) continue;
|
||||
if (a->getHp() <= 0.f || b->getHp() <= 0.f || !a->state.enabled || !b->state.enabled) continue;
|
||||
|
||||
const float minDist = a->collisionRadius + b->collisionRadius;
|
||||
const float minDist = a->state.collisionRadius + b->state.collisionRadius;
|
||||
if (minDist <= 0.f) continue;
|
||||
|
||||
const Eigen::Vector2f pa(a->position.x(), a->position.z());
|
||||
const Eigen::Vector2f pb(b->position.x(), b->position.z());
|
||||
const Eigen::Vector2f pa(a->state.position.x(), a->state.position.z());
|
||||
const Eigen::Vector2f pb(b->state.position.x(), b->state.position.z());
|
||||
const Eigen::Vector2f delta = pb - pa;
|
||||
const float dist = delta.norm();
|
||||
|
||||
@@ -1306,8 +1306,8 @@ namespace ZL
|
||||
const float penetration = (minDist - dist);
|
||||
const float push = penetration * 0.5f;
|
||||
|
||||
Eigen::Vector3f newA = a->position;
|
||||
Eigen::Vector3f newB = b->position;
|
||||
Eigen::Vector3f newA = a->state.position;
|
||||
Eigen::Vector3f newB = b->state.position;
|
||||
newA.x() -= normal.x() * push;
|
||||
newA.z() -= normal.y() * push;
|
||||
newA.y() = 0.f;
|
||||
@@ -1323,29 +1323,29 @@ namespace ZL
|
||||
const bool bOk = navigation->isWalkable(newB);
|
||||
|
||||
if (aOk && bOk) {
|
||||
a->position = newA;
|
||||
b->position = newB;
|
||||
a->state.position = newA;
|
||||
b->state.position = newB;
|
||||
}
|
||||
else if (aOk && !bOk) {
|
||||
a->position = newA;
|
||||
a->state.position = newA;
|
||||
}
|
||||
else if (!aOk && bOk) {
|
||||
b->position = newB;
|
||||
b->state.position = newB;
|
||||
}
|
||||
}
|
||||
else {
|
||||
a->position = newA;
|
||||
b->position = newB;
|
||||
a->state.position = newA;
|
||||
b->state.position = newB;
|
||||
}
|
||||
|
||||
if (a->isPlayer && !aWasMoving) a->stopInPlace();
|
||||
if (b->isPlayer && !bWasMoving) b->stopInPlace();
|
||||
if (a->state.isPlayer && !aWasMoving) a->stopInPlace();
|
||||
if (b->state.isPlayer && !bWasMoving) b->stopInPlace();
|
||||
|
||||
if (aWasMoving && !bWasMoving) {
|
||||
nudgeCharacterAside(b, a->position);
|
||||
nudgeCharacterAside(b, a->state.position);
|
||||
fireBumpCallbacks(a, b);
|
||||
} else if (bWasMoving && !aWasMoving) {
|
||||
nudgeCharacterAside(a, b->position);
|
||||
nudgeCharacterAside(a, b->state.position);
|
||||
fireBumpCallbacks(b, a);
|
||||
}
|
||||
}
|
||||
@@ -1369,7 +1369,7 @@ namespace ZL
|
||||
for (auto it = stuckPauseRemainingS.begin(); it != stuckPauseRemainingS.end();) {
|
||||
it->second -= deltaS;
|
||||
if (it->second <= 0.f) {
|
||||
if (!it->first->isPlayer) {
|
||||
if (!it->first->state.isPlayer) {
|
||||
it->first->forceReplan();
|
||||
}
|
||||
it = stuckPauseRemainingS.erase(it);
|
||||
@@ -1399,13 +1399,13 @@ namespace ZL
|
||||
if (!c) continue;
|
||||
auto it = lastCharacterPositions.find(c);
|
||||
if (it == lastCharacterPositions.end()) {
|
||||
lastCharacterPositions[c] = c->position;
|
||||
lastCharacterPositions[c] = c->state.position;
|
||||
continue;
|
||||
}
|
||||
|
||||
const Eigen::Vector3f prev = it->second;
|
||||
const float moved = (c->position - prev).norm();
|
||||
it->second = c->position;
|
||||
const float moved = (c->state.position - prev).norm();
|
||||
it->second = c->state.position;
|
||||
|
||||
if (moved > kMovedEps) {
|
||||
movers.push_back(c);
|
||||
@@ -1417,7 +1417,7 @@ namespace ZL
|
||||
c->stopInPlace();
|
||||
stuckFrameCounts[c] = 0;
|
||||
if (stuckPauseRemainingS.find(c) == stuckPauseRemainingS.end()) {
|
||||
if (!c->isPlayer) {
|
||||
if (!c->state.isPlayer) {
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_real_distribution<float> pauseDist(kStuckPauseMin, kStuckPauseMax);
|
||||
stuckPauseRemainingS[c] = pauseDist(rng);
|
||||
@@ -1442,7 +1442,7 @@ namespace ZL
|
||||
|
||||
for (Character* walker : characters) {
|
||||
if (!walker || walker == mover) continue;
|
||||
if (walker->getHp() <= 0.f || !walker->enabled) continue;
|
||||
if (walker->getHp() <= 0.f || !walker->state.enabled) continue;
|
||||
if (!walker->isMoving()) continue;
|
||||
|
||||
if (replanCooldownRemainingMs.find(walker) != replanCooldownRemainingMs.end()) {
|
||||
@@ -1450,7 +1450,7 @@ namespace ZL
|
||||
}
|
||||
|
||||
const Eigen::Vector3f nextTarget = walker->getCurrentNavigationTarget();
|
||||
const float distToSegment = distancePointToSegmentXZ(mover->position, walker->position, nextTarget);
|
||||
const float distToSegment = distancePointToSegmentXZ(mover->state.position, walker->state.position, nextTarget);
|
||||
if (distToSegment > kReplanTriggerDist) {
|
||||
continue;
|
||||
}
|
||||
@@ -1467,14 +1467,21 @@ namespace ZL
|
||||
intObj.update(delta);
|
||||
}
|
||||
|
||||
auto resolver = [&](int idx) -> Character* {
|
||||
if (idx == CharacterState::kNoTarget) return nullptr;
|
||||
if (idx == CharacterState::kPlayerIndex) return player.get();
|
||||
if (idx >= 0 && idx < static_cast<int>(npcs.size())) return npcs[idx].get();
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
if (player) {
|
||||
player->update(delta);
|
||||
dialogueSystem.update(static_cast<int>(delta));
|
||||
updateTriggerZones(player->position);
|
||||
player->update(delta, resolver);
|
||||
dialogueSystem.update(static_cast<int>(delta));
|
||||
updateTriggerZones(player->state.position);
|
||||
}
|
||||
for (auto& npc : npcs)
|
||||
{
|
||||
npc->update(delta);
|
||||
npc->update(delta, resolver);
|
||||
}
|
||||
|
||||
resolveCharacterCollisions();
|
||||
@@ -1489,7 +1496,7 @@ namespace ZL
|
||||
const Eigen::Vector3f& approachTarget = targetInteractiveObject->hasInteractionPosition
|
||||
? targetInteractiveObject->interactionPosition
|
||||
: targetInteractiveObject->position;
|
||||
float distToObject = (player->position - approachTarget).norm();
|
||||
float distToObject = (player->state.position - approachTarget).norm();
|
||||
|
||||
// If player is close enough to pick up the item
|
||||
if (distToObject <= targetInteractiveObject->approachRadius) {
|
||||
@@ -1516,21 +1523,20 @@ namespace ZL
|
||||
}
|
||||
|
||||
// Check if player reached target NPC for interaction.
|
||||
if (targetInteractNpc && targetInteractNpcIndex >= 0 && player) {
|
||||
float distToNpc = (player->position - targetInteractNpc->position).norm();
|
||||
if (targetInteractNpcIndex >= 0 && player) {
|
||||
float distToNpc = (player->state.position - npcs[targetInteractNpcIndex]->state.position).norm();
|
||||
if (distToNpc <= NPC_TALK_DISTANCE) {
|
||||
std::cout << "[NPC] Player reached NPC index " << targetInteractNpcIndex
|
||||
<< " (distance " << distToNpc << "); firing on_npc_interact" << std::endl;
|
||||
// Stop the player at the talk distance and have the NPC turn to face them.
|
||||
player->setTarget(player->position);
|
||||
targetInteractNpc->faceTarget = player.get();
|
||||
player->setTarget(player->state.position);
|
||||
npcs[targetInteractNpcIndex]->state.faceTargetIndex = CharacterState::kPlayerIndex;
|
||||
try {
|
||||
scriptEngine.callNpcInteractCallback(targetInteractNpcIndex);
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "[NPC] callback error: " << e.what() << std::endl;
|
||||
}
|
||||
targetInteractNpc = nullptr;
|
||||
targetInteractNpcIndex = -1;
|
||||
}
|
||||
}
|
||||
@@ -1538,7 +1544,7 @@ namespace ZL
|
||||
for (auto& tz : teleportZones) tz.update(static_cast<float>(delta));
|
||||
|
||||
if (targetTeleportZone && player) {
|
||||
float dist = (player->position - targetTeleportZone->position).norm();
|
||||
float dist = (player->state.position - targetTeleportZone->position).norm();
|
||||
if (dist <= targetTeleportZone->radius) {
|
||||
std::cout << "[TELEPORT] Player reached teleport zone '" << targetTeleportZone->id << "'" << std::endl;
|
||||
if (onTeleport) onTeleport(targetTeleportZone->destinationLocation, targetTeleportZone->destinationPosition, targetTeleportZone->destinationRotationY);
|
||||
@@ -1551,7 +1557,7 @@ namespace ZL
|
||||
for (auto& tz : teleportZones) {
|
||||
if (!tz.active || !tz.automatic) continue;
|
||||
const float autoR = tz.automaticRadius > 0.0f ? tz.automaticRadius : tz.radius;
|
||||
const float dist = (player->position - tz.position).norm();
|
||||
const float dist = (player->state.position - tz.position).norm();
|
||||
|
||||
if (!tz.automaticInitialized) {
|
||||
// If the player starts inside the zone (e.g. just arrived here via teleport),
|
||||
@@ -1582,7 +1588,7 @@ namespace ZL
|
||||
return;
|
||||
}
|
||||
|
||||
player->attackTarget = nullptr;
|
||||
player->state.attackTargetIndex = CharacterState::kNoTarget;
|
||||
|
||||
// Unproject click to ground plane (y=0) for Viola's walk target
|
||||
float ndcX = 2.0f * eventX / Environment::width - 1.0f;
|
||||
@@ -1596,7 +1602,7 @@ namespace ZL
|
||||
Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim);
|
||||
Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl);
|
||||
Eigen::Vector3f camUp(sinAzim * sinIncl, cosIncl, -cosAzim * sinIncl);
|
||||
const Eigen::Vector3f& playerPos = player ? player->position : Eigen::Vector3f::Zero();
|
||||
const Eigen::Vector3f& playerPos = player ? player->state.position : Eigen::Vector3f::Zero();
|
||||
Eigen::Vector3f camPos = playerPos + Eigen::Vector3f(-sinAzim * cosIncl, sinIncl, cosAzim * cosIncl) * Environment::zoom + Eigen::Vector3f(0,1,0);
|
||||
|
||||
Eigen::Vector3f rayDir = (camForward + camRight * (ndcX * aspect * tanHalfFov) + camUp * (ndcY * tanHalfFov)).normalized();
|
||||
@@ -1630,17 +1636,16 @@ namespace ZL
|
||||
std::cout << "[CLICK] *** SUCCESS: Clicked on interactive object: " << clickedObject->loadedObject.name << " ***" << std::endl;
|
||||
std::cout << "[CLICK] Object position: (" << clickedObject->position.x() << ", "
|
||||
<< clickedObject->position.y() << ", " << clickedObject->position.z() << ")" << std::endl;
|
||||
std::cout << "[CLICK] Player position: (" << player->position.x() << ", "
|
||||
<< player->position.y() << ", " << player->position.z() << ")" << std::endl;
|
||||
std::cout << "[CLICK] Player position: (" << player->state.position.x() << ", "
|
||||
<< player->state.position.y() << ", " << player->state.position.z() << ")" << std::endl;
|
||||
|
||||
targetInteractiveObject = clickedObject;
|
||||
targetInteractNpc = nullptr;
|
||||
targetInteractNpcIndex = -1;
|
||||
targetTeleportZone = nullptr;
|
||||
player->setTarget(clickedObject->hasInteractionPosition
|
||||
? clickedObject->interactionPosition
|
||||
: clickedObject->position);
|
||||
player->attackTarget = nullptr;
|
||||
player->state.attackTargetIndex = CharacterState::kNoTarget;
|
||||
std::cout << "[CLICK] Player moving to object..." << std::endl;
|
||||
}
|
||||
else {
|
||||
@@ -1648,7 +1653,7 @@ namespace ZL
|
||||
// Check if we clicked on an NPC
|
||||
Character* clickedNpc = raycastNpcs(camPos, rayDir);
|
||||
if (clickedNpc && player) {
|
||||
float distance = (player->position - clickedNpc->position).norm();
|
||||
float distance = (player->state.position - clickedNpc->state.position).norm();
|
||||
int npcIndex = -1;
|
||||
for (size_t i = 0; i < npcs.size(); ++i) {
|
||||
if (npcs[i].get() == clickedNpc) {
|
||||
@@ -1660,36 +1665,32 @@ namespace ZL
|
||||
targetInteractiveObject = nullptr;
|
||||
targetTeleportZone = nullptr;
|
||||
|
||||
if (clickedNpc->canAttack) {
|
||||
// Hostile NPC: combat logic walks the player in via attackTarget;
|
||||
// don't queue a Lua interaction during a fight.
|
||||
player->attackTarget = clickedNpc;
|
||||
targetInteractNpc = nullptr;
|
||||
if (clickedNpc->state.canAttack) {
|
||||
// Hostile NPC: combat logic walks the player in via attackTargetIndex.
|
||||
player->state.attackTargetIndex = npcIndex;
|
||||
targetInteractNpcIndex = -1;
|
||||
|
||||
if (distance <= clickedNpc->interactionRadius) {
|
||||
if (distance <= clickedNpc->state.interactionRadius) {
|
||||
std::cout << "[CLICK] Hostile NPC " << npcIndex << " in range; firing on_npc_interact" << std::endl;
|
||||
scriptEngine.callNpcInteractCallback(npcIndex);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Peaceful NPC: walk to them, fire on_npc_interact when within talk distance.
|
||||
player->attackTarget = nullptr;
|
||||
player->state.attackTargetIndex = CharacterState::kNoTarget;
|
||||
|
||||
if (distance <= NPC_TALK_DISTANCE) {
|
||||
// Already in talk range — fire immediately, stop, and face the player.
|
||||
std::cout << "[CLICK] *** SUCCESS: Clicked on NPC index: " << npcIndex << " (in range) ***" << std::endl;
|
||||
player->setTarget(player->position);
|
||||
clickedNpc->faceTarget = player.get();
|
||||
player->setTarget(player->state.position);
|
||||
npcs[npcIndex]->state.faceTargetIndex = CharacterState::kPlayerIndex;
|
||||
scriptEngine.callNpcInteractCallback(npcIndex);
|
||||
targetInteractNpc = nullptr;
|
||||
targetInteractNpcIndex = -1;
|
||||
}
|
||||
else {
|
||||
std::cout << "[CLICK] NPC " << npcIndex << " out of talk range (distance " << distance
|
||||
<< " > " << NPC_TALK_DISTANCE << "); walking to NPC..." << std::endl;
|
||||
player->setTarget(clickedNpc->position);
|
||||
targetInteractNpc = clickedNpc;
|
||||
player->setTarget(clickedNpc->state.position);
|
||||
targetInteractNpcIndex = npcIndex;
|
||||
}
|
||||
}
|
||||
@@ -1713,14 +1714,12 @@ namespace ZL
|
||||
std::cout << "[CLICK] Clicked teleport zone '" << clickedTeleport->id << "'" << std::endl;
|
||||
targetTeleportZone = clickedTeleport;
|
||||
targetInteractiveObject = nullptr;
|
||||
targetInteractNpc = nullptr;
|
||||
targetInteractNpcIndex = -1;
|
||||
player->setTarget(clickedTeleport->position);
|
||||
player->attackTarget = nullptr;
|
||||
player->state.attackTargetIndex = CharacterState::kNoTarget;
|
||||
} else {
|
||||
player->setTarget(Eigen::Vector3f(hit.x(), 0.f, hit.z()));
|
||||
player->attackTarget = nullptr;
|
||||
targetInteractNpc = nullptr;
|
||||
player->state.attackTargetIndex = CharacterState::kNoTarget;
|
||||
targetInteractNpcIndex = -1;
|
||||
targetTeleportZone = nullptr;
|
||||
if (onPlayerFloorWalk) onPlayerFloorWalk();
|
||||
|
||||
@@ -85,7 +85,6 @@ namespace ZL
|
||||
// "Walk to NPC, then fire on_npc_interact" — mirrors targetInteractiveObject
|
||||
// so a click from outside interactionRadius still leads to a Lua callback
|
||||
// once the player gets close enough.
|
||||
Character* targetInteractNpc = nullptr;
|
||||
int targetInteractNpcIndex = -1;
|
||||
|
||||
std::unique_ptr<TextRenderer> npcNameText;
|
||||
|
||||
@@ -446,9 +446,9 @@ namespace ZL
|
||||
data.rotationX = 0.0f;
|
||||
data.rotationY = rotDist(rng);
|
||||
data.rotationZ = 0.0f;
|
||||
data.positionX = loc.player->position.x();
|
||||
data.positionY = loc.player->position.y();
|
||||
data.positionZ = loc.player->position.z();
|
||||
data.positionX = loc.player->state.position.x();
|
||||
data.positionY = loc.player->state.position.y();
|
||||
data.positionZ = loc.player->state.position.z();
|
||||
data.scale = scaleDist(rng);
|
||||
|
||||
LoadedGameObject obj = GameObjectLoader::buildLoadedObject(data, loc.renderer, CONST_ZIP_FILE);
|
||||
|
||||
+24
-24
@@ -63,7 +63,7 @@ namespace ZL {
|
||||
}
|
||||
};
|
||||
}
|
||||
npcs[index]->homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
npcs[index]->state.homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb));
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace ZL {
|
||||
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
std::cout << "[script] npc_rotate_to: index " << index << " angle " << angle << " degrees (" << rad << " radians)" << std::endl;
|
||||
npcs[index]->targetFacingAngle = rad;
|
||||
npcs[index]->state.targetFacingAngle = rad;
|
||||
});
|
||||
|
||||
api.set_function("player_walk_to",
|
||||
@@ -96,7 +96,7 @@ namespace ZL {
|
||||
}
|
||||
};
|
||||
}
|
||||
loc->player->homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
loc->player->state.homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
loc->player->setTarget(Eigen::Vector3f(x, y, z), std::move(cb));
|
||||
});
|
||||
|
||||
@@ -327,11 +327,11 @@ namespace ZL {
|
||||
|
||||
api.set_function("get_player_x",
|
||||
[loc]() -> float {
|
||||
return loc->player ? loc->player->position.x() : 0.0f;
|
||||
return loc->player ? loc->player->state.position.x() : 0.0f;
|
||||
});
|
||||
api.set_function("get_player_z",
|
||||
[loc]() -> float {
|
||||
return loc->player ? loc->player->position.z() : 0.0f;
|
||||
return loc->player ? loc->player->state.position.z() : 0.0f;
|
||||
});
|
||||
|
||||
// set_player_hp(value) — sets the player's current HP directly.
|
||||
@@ -339,11 +339,11 @@ namespace ZL {
|
||||
[loc](float value) {
|
||||
if (loc->player)
|
||||
{
|
||||
loc->player->setHp(value);
|
||||
if (loc->player->currentState == AnimationState::ACTION_TO_DEATH || loc->player->currentState == AnimationState::DEATH_IDLE)
|
||||
loc->player->state.setHp(value);
|
||||
if (loc->player->state.currentState == AnimationState::ACTION_TO_DEATH || loc->player->state.currentState == AnimationState::DEATH_IDLE)
|
||||
{
|
||||
loc->player->currentState = AnimationState::STAND;
|
||||
loc->player->resetAnim = true;
|
||||
loc->player->state.currentState = AnimationState::STAND;
|
||||
loc->player->state.resetAnim = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -480,8 +480,8 @@ namespace ZL {
|
||||
return;
|
||||
}
|
||||
Eigen::Vector3f pos(x, y, z);
|
||||
npcs[index]->position = pos;
|
||||
npcs[index]->homePosition = pos;
|
||||
npcs[index]->state.position = pos;
|
||||
npcs[index]->state.homePosition = pos;
|
||||
npcs[index]->setTarget(pos);
|
||||
});
|
||||
|
||||
@@ -494,8 +494,8 @@ namespace ZL {
|
||||
return;
|
||||
}
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
npcs[index]->facingAngle = rad;
|
||||
npcs[index]->targetFacingAngle = rad;
|
||||
npcs[index]->state.facingAngle = rad;
|
||||
npcs[index]->state.targetFacingAngle = rad;
|
||||
});
|
||||
|
||||
// set_npc_enabled(index, enabled)
|
||||
@@ -506,30 +506,30 @@ namespace ZL {
|
||||
std::cerr << "[script] set_npc_enabled: index " << index << " out of range\n";
|
||||
return;
|
||||
}
|
||||
npcs[index]->enabled = value;
|
||||
npcs[index]->attack = 0;
|
||||
npcs[index]->attack_cooldown = 0;
|
||||
npcs[index]->currentState = AnimationState::STAND;
|
||||
if (npcs[index]->canAttack)
|
||||
npcs[index]->state.enabled = value;
|
||||
npcs[index]->state.attack = 0;
|
||||
npcs[index]->state.attack_cooldown = 0;
|
||||
npcs[index]->state.currentState = AnimationState::STAND;
|
||||
if (npcs[index]->state.canAttack)
|
||||
{
|
||||
npcs[index]->attackTarget = loc->player.get();
|
||||
npcs[index]->state.attackTargetIndex = CharacterState::kPlayerIndex;
|
||||
}
|
||||
npcs[index]->battle_state = 0;
|
||||
npcs[index]->state.battle_state = 0;
|
||||
});
|
||||
|
||||
api.set_function("player_set_position",
|
||||
[loc](float x, float y, float z) {
|
||||
Eigen::Vector3f pos(x, y, z);
|
||||
loc->player->position = pos;
|
||||
loc->player->homePosition = pos;
|
||||
loc->player->state.position = pos;
|
||||
loc->player->state.homePosition = pos;
|
||||
loc->player->setTarget(pos);
|
||||
});
|
||||
|
||||
api.set_function("player_set_rotation",
|
||||
[loc](float angle) {
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
loc->player->facingAngle = rad;
|
||||
loc->player->targetFacingAngle = rad;
|
||||
loc->player->state.facingAngle = rad;
|
||||
loc->player->state.targetFacingAngle = rad;
|
||||
});
|
||||
|
||||
// move_object(name, x, y, z, duration_sec [, on_complete])
|
||||
|
||||
@@ -345,13 +345,13 @@ namespace ZL {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto loadOptionalAnim = [&](AnimationState state, const std::string& path, const char* label) {
|
||||
auto loadOptionalAnim = [&](AnimationState animState, const std::string& path, const char* label) {
|
||||
if (path.empty()) return;
|
||||
try {
|
||||
if (path.size() >= 5 && path.substr(path.size() - 5) == ".anim")
|
||||
npc->loadBinaryAnimation(state, path, zipPath);
|
||||
npc->loadBinaryAnimation(animState, path, zipPath);
|
||||
else
|
||||
npc->loadAnimation(state, path, zipPath);
|
||||
npc->loadAnimation(animState, path, zipPath);
|
||||
std::cout << " Loaded " << label << " animation: " << path << std::endl;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "GameObjectLoader: Failed to load " << label
|
||||
@@ -366,35 +366,28 @@ namespace ZL {
|
||||
loadOptionalAnim(AnimationState::DEATH_IDLE, npcData.animationDeathIdlePath, "DEATH_IDLE");
|
||||
|
||||
try {
|
||||
if (!npcData.meshTextures.empty()) {
|
||||
for (const auto& entry : npcData.meshTextures) {
|
||||
//set_Texture(*npc, entry.first, CreateTextureDataFromPng(entry.second, zipPath.c_str()));
|
||||
npc->setTexture(entry.first, std::make_shared<Texture>(CreateTextureDataFromPng(entry.second, zipPath.c_str())));
|
||||
|
||||
for (const auto& entry : npcData.meshTextures) {
|
||||
npc->setTexture(entry.first, std::make_shared<Texture>(CreateTextureDataFromPng(entry.second, zipPath.c_str())));
|
||||
std::cout << " -> mesh '" << entry.first << "'" << std::endl;
|
||||
}
|
||||
} else {
|
||||
//set_Texture(*npc, CreateTextureDataFromPng(npcData.texturePath, zipPath.c_str()));
|
||||
npc->setTexture(std::make_shared<Texture>(CreateTextureDataFromPng(npcData.texturePath, zipPath.c_str())));
|
||||
std::cout << " Loaded texture: " << npcData.texturePath << std::endl;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "GameObjectLoader: Failed to load texture for '" << npcData.name << "': " << e.what() << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
npc->walkSpeed = npcData.walkSpeed;
|
||||
npc->rotationSpeed = npcData.rotationSpeed;
|
||||
npc->modelScale = npcData.modelScale;
|
||||
npc->position = Eigen::Vector3f(npcData.positionX, npcData.positionY, npcData.positionZ);
|
||||
npc->facingAngle = npcData.facingAngle;
|
||||
npc->setHp(npcData.hp);
|
||||
npc->initialHp = npcData.hp;
|
||||
npc->canAttack = npcData.canAttack;
|
||||
npc->enabled = npcData.enabled;
|
||||
npc->interactionRadius = npcData.interactionRadius;
|
||||
npc->npcId = npcData.id;
|
||||
npc->npcName = npcData.name;
|
||||
npc->state.walkSpeed = npcData.walkSpeed;
|
||||
npc->state.rotationSpeed = npcData.rotationSpeed;
|
||||
npc->state.modelScale = npcData.modelScale;
|
||||
npc->state.position = Eigen::Vector3f(npcData.positionX, npcData.positionY, npcData.positionZ);
|
||||
npc->state.facingAngle = npcData.facingAngle;
|
||||
npc->state.setHp(npcData.hp);
|
||||
npc->state.initialHp = npcData.hp;
|
||||
npc->state.canAttack = npcData.canAttack;
|
||||
npc->state.enabled = npcData.enabled;
|
||||
npc->state.interactionRadius = npcData.interactionRadius;
|
||||
npc->state.npcId = npcData.id;
|
||||
npc->state.npcName = npcData.name;
|
||||
|
||||
Eigen::Quaternionf corrRot = Eigen::Quaternionf::Identity();
|
||||
if (npcData.modelCorrectionRotX != 0.0f)
|
||||
@@ -403,9 +396,9 @@ namespace ZL {
|
||||
corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(npcData.modelCorrectionRotY, Eigen::Vector3f::UnitY())) * corrRot;
|
||||
if (npcData.modelCorrectionRotZ != 0.0f)
|
||||
corrRot = Eigen::Quaternionf(Eigen::AngleAxisf(npcData.modelCorrectionRotZ, Eigen::Vector3f::UnitZ())) * corrRot;
|
||||
npc->modelCorrectionRotation = corrRot;
|
||||
npc->state.modelCorrectionRotation = corrRot;
|
||||
|
||||
npc->setTarget(npc->position);
|
||||
npc->setTarget(npc->state.position);
|
||||
npcs.push_back(std::move(npc));
|
||||
|
||||
std::cout << "Successfully loaded NPC: " << npcData.name << " at ("
|
||||
|
||||
Reference in new issue
Block a user