mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-14 08:23:52 +00:00
Merge pull request #48 from ldmonster/feat/animation-handling
[feat] animation: FSM-Based Animation System — Full Refactor
This commit is contained in:
commit
8c587ab13d
54 changed files with 5112 additions and 2099 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
|
@ -9,6 +9,8 @@
|
|||
[submodule "extern/FidelityFX-SDK"]
|
||||
path = extern/FidelityFX-SDK
|
||||
url = https://github.com/Kelsidavis/FidelityFX-SDK.git
|
||||
shallow = true
|
||||
update = none
|
||||
[submodule "extern/FidelityFX-FSR2"]
|
||||
path = extern/FidelityFX-FSR2
|
||||
url = https://github.com/Kelsidavis/FidelityFX-FSR2.git
|
||||
|
|
|
|||
|
|
@ -616,7 +616,17 @@ set(WOWEE_SOURCES
|
|||
src/rendering/spell_visual_system.cpp
|
||||
src/rendering/post_process_pipeline.cpp
|
||||
src/rendering/animation_controller.cpp
|
||||
src/rendering/animation_ids.cpp
|
||||
src/rendering/animation/animation_ids.cpp
|
||||
src/rendering/animation/emote_registry.cpp
|
||||
src/rendering/animation/footstep_driver.cpp
|
||||
src/rendering/animation/sfx_state_driver.cpp
|
||||
src/rendering/animation/anim_capability_probe.cpp
|
||||
src/rendering/animation/locomotion_fsm.cpp
|
||||
src/rendering/animation/combat_fsm.cpp
|
||||
src/rendering/animation/activity_fsm.cpp
|
||||
src/rendering/animation/mount_fsm.cpp
|
||||
src/rendering/animation/character_animator.cpp # Renamed from player_animator.cpp; npc_animator.cpp removed
|
||||
src/rendering/animation/animation_manager.cpp
|
||||
src/rendering/loading_screen.cpp
|
||||
|
||||
# UI
|
||||
|
|
|
|||
110
docs/ANIMATION_SYSTEM.md
Normal file
110
docs/ANIMATION_SYSTEM.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Animation System
|
||||
|
||||
Unified, FSM-based animation system for all characters (players, NPCs, companions).
|
||||
Every character uses the same `CharacterAnimator` — there is no separate NPC/Mob animator.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
AnimationController (thin adapter — bridges Renderer ↔ CharacterAnimator)
|
||||
└─ CharacterAnimator (FSM composer — implements ICharacterAnimator)
|
||||
├─ CombatFSM (stun, hit reaction, spell cast, melee, ranged, charge)
|
||||
├─ ActivityFSM (emote, loot, sit/stand/kneel/sleep)
|
||||
├─ LocomotionFSM (idle, walk, run, sprint, jump, swim, strafe)
|
||||
└─ MountFSM (mount idle, mount run, flight)
|
||||
|
||||
AnimationManager (registry of CharacterAnimator instances by ID)
|
||||
AnimCapabilitySet (probed once per model — cached resolved anim IDs)
|
||||
AnimCapabilityProbe (queries which animations a model supports)
|
||||
```
|
||||
|
||||
### Priority Resolution
|
||||
|
||||
`CharacterAnimator::resolveAnimation()` runs every frame. The first FSM to
|
||||
return a valid `AnimOutput` wins:
|
||||
|
||||
1. **Mount** — if mounted, return `MOUNT` (overrides everything)
|
||||
2. **Combat** — stun > hit reaction > spell > charge > melee/ranged > combat idle
|
||||
3. **Activity** — emote > loot > sit/stand transitions
|
||||
4. **Locomotion** — run/walk/sprint/jump/swim/strafe/idle
|
||||
|
||||
If no FSM produces a valid output, the last animation continues (STAY policy).
|
||||
|
||||
### Overlay Layer
|
||||
|
||||
After resolution, `applyOverlays()` substitutes stealth animation variants
|
||||
(stealth idle, stealth walk, stealth run) without changing sub-FSM state.
|
||||
|
||||
## File Map
|
||||
|
||||
### Headers (`include/rendering/animation/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `i_animator.hpp` | Base interface: `onEvent()`, `update()` |
|
||||
| `i_character_animator.hpp` | 20 virtual methods (combat, spells, emotes, mounts, etc.) |
|
||||
| `character_animator.hpp` | FSM composer — the single animator class |
|
||||
| `locomotion_fsm.hpp` | Movement states: idle, walk, run, sprint, jump, swim |
|
||||
| `combat_fsm.hpp` | Combat states: melee, ranged, spell cast, stun, hit reaction |
|
||||
| `activity_fsm.hpp` | Activity states: emote, loot, sit/stand/kneel |
|
||||
| `mount_fsm.hpp` | Mount states: idle, run, flight, taxi |
|
||||
| `anim_capability_set.hpp` | Probed capability flags + resolved animation IDs |
|
||||
| `anim_capability_probe.hpp` | Probes a model for available animations |
|
||||
| `anim_event.hpp` | `AnimEvent` enum (MOVE_START, MOVE_STOP, JUMP, etc.) |
|
||||
| `animation_manager.hpp` | Central registry of CharacterAnimator instances |
|
||||
| `weapon_type.hpp` | WeaponLoadout, RangedWeaponType enums |
|
||||
| `emote_registry.hpp` | Emote name → animation ID lookup |
|
||||
| `footstep_driver.hpp` | Footstep sound event driver |
|
||||
| `sfx_state_driver.hpp` | State-transition SFX (jump, land, swim enter/exit) |
|
||||
| `i_anim_renderer.hpp` | Interface for renderer animation queries |
|
||||
|
||||
### Sources (`src/rendering/animation/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `character_animator.cpp` | ICharacterAnimator implementation + priority resolver |
|
||||
| `locomotion_fsm.cpp` | Locomotion state transitions + resolve logic |
|
||||
| `combat_fsm.cpp` | Combat state transitions + resolve logic |
|
||||
| `activity_fsm.cpp` | Activity state transitions + resolve logic |
|
||||
| `mount_fsm.cpp` | Mount state transitions + resolve logic |
|
||||
| `anim_capability_probe.cpp` | Model animation probing |
|
||||
| `animation_manager.cpp` | Registry CRUD + bulk update |
|
||||
| `emote_registry.cpp` | Emote database |
|
||||
| `footstep_driver.cpp` | Footstep timing logic |
|
||||
| `sfx_state_driver.cpp` | SFX transition detection |
|
||||
|
||||
### Controller (`include/rendering/animation_controller.hpp` + `src/rendering/animation_controller.cpp`)
|
||||
|
||||
Thin adapter that:
|
||||
- Collects per-frame input from camera/renderer → `CharacterAnimator::FrameInput`
|
||||
- Forwards state changes (combat, emote, spell, mount, etc.) → `CharacterAnimator`
|
||||
- Reads `AnimOutput` → applies via `CharacterRenderer`
|
||||
- Owns footstep and SFX drivers
|
||||
|
||||
## Key Types
|
||||
|
||||
- **`AnimEvent`** — discrete events: `MOVE_START`, `MOVE_STOP`, `JUMP`, `LAND`, `MOUNT`, `DISMOUNT`, etc.
|
||||
- **`AnimOutput`** — result of FSM resolution: `{animId, loop, valid}`. `valid=false` means STAY.
|
||||
- **`AnimCapabilitySet`** — probed once per model load. Caches resolved IDs and capability flags.
|
||||
- **`CharacterAnimator::FrameInput`** — per-frame input struct (movement flags, timers, animation state queries).
|
||||
|
||||
## Adding a New Animation State
|
||||
|
||||
1. Decide which FSM owns the state (combat, activity, locomotion, or mount).
|
||||
2. Add the state enum to the FSM's `State` enum.
|
||||
3. Add transitions in the FSM's `resolve()` method.
|
||||
4. Add resolved ID fields to `AnimCapabilitySet` if the animation needs model probing.
|
||||
5. If the state needs external triggering, add a method to `ICharacterAnimator` and implement in `CharacterAnimator`.
|
||||
|
||||
## Tests
|
||||
|
||||
Each FSM has its own test file in `tests/`:
|
||||
- `test_locomotion_fsm.cpp`
|
||||
- `test_combat_fsm.cpp`
|
||||
- `test_activity_fsm.cpp`
|
||||
- `test_anim_capability.cpp`
|
||||
|
||||
Run all tests:
|
||||
```bash
|
||||
cd build && ctest --output-on-failure
|
||||
```
|
||||
|
|
@ -940,7 +940,9 @@ public:
|
|||
|
||||
// Spell cast animation callbacks — true=start cast/channel, false=finish/cancel
|
||||
// guid: caster (may be player or another unit), isChannel: channel vs regular cast
|
||||
using SpellCastAnimCallback = std::function<void(uint64_t guid, bool start, bool isChannel)>;
|
||||
// castType: DIRECTED (unit target), OMNI (self/no target), AREA (ground AoE)
|
||||
using SpellCastAnimCallback = std::function<void(uint64_t guid, bool start, bool isChannel,
|
||||
SpellCastType castType)>;
|
||||
void setSpellCastAnimCallback(SpellCastAnimCallback cb) { spellCastAnimCallback_ = std::move(cb); }
|
||||
|
||||
// Fired when the player's own spell cast fails (spellId of the failed spell).
|
||||
|
|
|
|||
|
|
@ -40,13 +40,25 @@ struct TalentTabEntry {
|
|||
|
||||
// ---- Spell / cast state ----
|
||||
|
||||
// Spell targeting classification for animation selection.
|
||||
// Derived from the spell packet's targetGuid field — NOT the player's UI target.
|
||||
// DIRECTED — spell targets a specific unit (Frostbolt, Heal, Shadow Bolt)
|
||||
// OMNI — self-cast / no explicit target (Arcane Explosion, buffs)
|
||||
// AREA — ground-targeted AoE (Blizzard, Rain of Fire, Flamestrike)
|
||||
enum class SpellCastType : uint8_t {
|
||||
DIRECTED = 0, // Has a specific unit target
|
||||
OMNI = 1, // Self / no target
|
||||
AREA = 2, // Ground-targeted AoE
|
||||
};
|
||||
|
||||
struct UnitCastState {
|
||||
bool casting = false;
|
||||
bool isChannel = false;
|
||||
uint32_t spellId = 0;
|
||||
float timeRemaining = 0.0f;
|
||||
float timeTotal = 0.0f;
|
||||
bool interruptible = true;
|
||||
bool casting = false;
|
||||
bool isChannel = false;
|
||||
uint32_t spellId = 0;
|
||||
float timeRemaining = 0.0f;
|
||||
float timeTotal = 0.0f;
|
||||
bool interruptible = true;
|
||||
SpellCastType castType = SpellCastType::OMNI;
|
||||
};
|
||||
|
||||
// ---- Equipment sets (WotLK) ----
|
||||
|
|
|
|||
110
include/rendering/animation/activity_fsm.hpp
Normal file
110
include/rendering/animation/activity_fsm.hpp
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#pragma once
|
||||
|
||||
#include "rendering/animation/anim_capability_set.hpp"
|
||||
#include "rendering/animation/anim_event.hpp"
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// ActivityFSM
|
||||
//
|
||||
// Pure logic state machine for non-combat activities: emote, loot, sit/sleep/kneel.
|
||||
// Sit chain (down → loop → up) auto-advances on one-shot completion.
|
||||
// All activities cancel on movement.
|
||||
// ============================================================================
|
||||
class ActivityFSM {
|
||||
public:
|
||||
enum class State : uint8_t {
|
||||
NONE,
|
||||
EMOTE,
|
||||
LOOTING, // One-shot LOOT anim
|
||||
LOOT_KNEELING, // KNEEL_LOOP until loot window closes
|
||||
LOOT_END, // One-shot KNEEL_END exit anim
|
||||
SIT_DOWN,
|
||||
SITTING,
|
||||
SIT_UP,
|
||||
};
|
||||
|
||||
struct Input {
|
||||
bool moving = false;
|
||||
bool sprinting = false;
|
||||
bool jumping = false;
|
||||
bool grounded = true;
|
||||
bool swimming = false;
|
||||
bool sitting = false; // Camera controller sitting state
|
||||
bool stunned = false;
|
||||
// Animation state query for one-shot completion detection
|
||||
uint32_t currentAnimId = 0;
|
||||
float currentAnimTime = 0.0f;
|
||||
float currentAnimDuration = 0.0f;
|
||||
bool haveAnimState = false;
|
||||
};
|
||||
|
||||
void onEvent(AnimEvent event);
|
||||
|
||||
/// Evaluate current state against input and capabilities.
|
||||
AnimOutput resolve(const Input& in, const AnimCapabilitySet& caps);
|
||||
|
||||
State getState() const { return state_; }
|
||||
void setState(State s) { state_ = s; }
|
||||
bool isActive() const { return state_ != State::NONE; }
|
||||
void reset();
|
||||
|
||||
// ── Emote management ────────────────────────────────────────────────
|
||||
void startEmote(uint32_t animId, bool loop);
|
||||
void cancelEmote();
|
||||
bool isEmoteActive() const { return emoteActive_; }
|
||||
uint32_t getEmoteAnimId() const { return emoteAnimId_; }
|
||||
|
||||
// ── Sit/sleep/kneel management ──────────────────────────────────────
|
||||
// WoW UnitStandStateType constants
|
||||
static constexpr uint8_t STAND_STATE_STAND = 0;
|
||||
static constexpr uint8_t STAND_STATE_SIT = 1;
|
||||
static constexpr uint8_t STAND_STATE_SIT_CHAIR = 2;
|
||||
static constexpr uint8_t STAND_STATE_SLEEP = 3;
|
||||
static constexpr uint8_t STAND_STATE_SIT_LOW = 4;
|
||||
static constexpr uint8_t STAND_STATE_SIT_MED = 5;
|
||||
static constexpr uint8_t STAND_STATE_SIT_HIGH = 6;
|
||||
static constexpr uint8_t STAND_STATE_DEAD = 7;
|
||||
static constexpr uint8_t STAND_STATE_KNEEL = 8;
|
||||
|
||||
void setStandState(uint8_t standState);
|
||||
uint8_t getStandState() const { return standState_; }
|
||||
|
||||
// ── Loot management ─────────────────────────────────────────────────
|
||||
void startLooting();
|
||||
void stopLooting();
|
||||
|
||||
static constexpr uint8_t PRIORITY = 30;
|
||||
|
||||
private:
|
||||
State state_ = State::NONE;
|
||||
|
||||
// Emote state
|
||||
bool emoteActive_ = false;
|
||||
uint32_t emoteAnimId_ = 0;
|
||||
bool emoteLoop_ = false;
|
||||
|
||||
// Sit/sleep/kneel transition animations
|
||||
uint8_t standState_ = 0;
|
||||
uint32_t sitDownAnim_ = 0;
|
||||
uint32_t sitLoopAnim_ = 0;
|
||||
uint32_t sitUpAnim_ = 0;
|
||||
bool sitDownAnimSeen_ = false; // Track whether one-shot has started playing
|
||||
bool sitUpAnimSeen_ = false;
|
||||
uint8_t sitDownFrames_ = 0; // Frames spent in SIT_DOWN (for safety timeout)
|
||||
uint8_t sitUpFrames_ = 0; // Frames spent in SIT_UP
|
||||
bool lootAnimSeen_ = false;
|
||||
uint8_t lootFrames_ = 0;
|
||||
bool lootEndAnimSeen_ = false;
|
||||
uint8_t lootEndFrames_ = 0;
|
||||
|
||||
void updateTransitions(const Input& in);
|
||||
bool oneShotComplete(const Input& in, uint32_t expectedAnimId) const;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
38
include/rendering/animation/anim_capability_probe.hpp
Normal file
38
include/rendering/animation/anim_capability_probe.hpp
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "rendering/animation/anim_capability_set.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
class Renderer;
|
||||
|
||||
// ============================================================================
|
||||
// AnimCapabilityProbe
|
||||
//
|
||||
// Scans a model's animation sequences once and caches the results in an
|
||||
// AnimCapabilitySet. All animation selection then uses the probed set
|
||||
// instead of per-frame hasAnimation() calls.
|
||||
// ============================================================================
|
||||
class AnimCapabilityProbe {
|
||||
public:
|
||||
AnimCapabilityProbe() = default;
|
||||
|
||||
/// Probe all animation capabilities for the given character instance.
|
||||
/// Returns a fully-populated AnimCapabilitySet.
|
||||
static AnimCapabilitySet probe(Renderer* renderer, uint32_t instanceId);
|
||||
|
||||
/// Probe mount animation capabilities (separate model).
|
||||
static AnimCapabilitySet probeMountModel(Renderer* renderer, uint32_t mountInstanceId);
|
||||
|
||||
private:
|
||||
/// Pick the first available animation from candidates for the given instance.
|
||||
/// Returns 0 if none available.
|
||||
static uint32_t pickFirst(Renderer* renderer, uint32_t instanceId,
|
||||
const uint32_t* candidates, size_t count);
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
147
include/rendering/animation/anim_capability_set.hpp
Normal file
147
include/rendering/animation/anim_capability_set.hpp
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// AnimFallbackPolicy
|
||||
//
|
||||
// Controls what happens when a requested animation is unavailable.
|
||||
// ============================================================================
|
||||
enum class AnimFallbackPolicy : uint8_t {
|
||||
STAY_IN_STATE, // Keep current animation (default for player)
|
||||
FIRST_AVAILABLE, // Try candidates list, stay if all fail
|
||||
NONE, // Do nothing (default for expired queue)
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// AnimOutput
|
||||
//
|
||||
// Unified animation selection result. When valid=false, callers should
|
||||
// keep the currently playing animation (STAY_IN_STATE policy).
|
||||
// ============================================================================
|
||||
struct AnimOutput {
|
||||
uint32_t animId = 0;
|
||||
bool loop = false;
|
||||
bool valid = false;
|
||||
|
||||
/// Construct a valid output.
|
||||
static AnimOutput ok(uint32_t id, bool looping) {
|
||||
return {id, looping, true};
|
||||
}
|
||||
/// Construct an invalid output (STAY policy).
|
||||
static AnimOutput stay() {
|
||||
return {0, false, false};
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// AnimCapabilitySet
|
||||
//
|
||||
// Probed once per model load. Caches which animations a model supports
|
||||
// and the resolved IDs (after fallback chains). This eliminates per-frame
|
||||
// hasAnimation() calls.
|
||||
// ============================================================================
|
||||
struct AnimCapabilitySet {
|
||||
// ── Locomotion resolved IDs ─────────────────────────────────────────
|
||||
uint32_t resolvedStand = 0;
|
||||
uint32_t resolvedWalk = 0;
|
||||
uint32_t resolvedRun = 0;
|
||||
uint32_t resolvedSprint = 0;
|
||||
uint32_t resolvedWalkBackwards = 0;
|
||||
uint32_t resolvedStrafeLeft = 0;
|
||||
uint32_t resolvedStrafeRight = 0;
|
||||
uint32_t resolvedRunLeft = 0;
|
||||
uint32_t resolvedRunRight = 0;
|
||||
uint32_t resolvedJumpStart = 0;
|
||||
uint32_t resolvedJump = 0; // Mid-air loop
|
||||
uint32_t resolvedJumpEnd = 0;
|
||||
uint32_t resolvedSwimIdle = 0;
|
||||
uint32_t resolvedSwim = 0;
|
||||
uint32_t resolvedSwimBackwards = 0;
|
||||
uint32_t resolvedSwimLeft = 0;
|
||||
uint32_t resolvedSwimRight = 0;
|
||||
|
||||
// ── Combat resolved IDs ─────────────────────────────────────────────
|
||||
uint32_t resolvedCombatIdle = 0;
|
||||
uint32_t resolvedMelee1H = 0;
|
||||
uint32_t resolvedMelee2H = 0;
|
||||
uint32_t resolvedMelee2HLoose = 0;
|
||||
uint32_t resolvedMeleeUnarmed = 0;
|
||||
uint32_t resolvedMeleeFist = 0;
|
||||
uint32_t resolvedMeleePierce = 0; // Dagger
|
||||
uint32_t resolvedMeleeOffHand = 0;
|
||||
uint32_t resolvedMeleeOffHandFist = 0;
|
||||
uint32_t resolvedMeleeOffHandPierce = 0;
|
||||
uint32_t resolvedMeleeOffHandUnarmed = 0;
|
||||
|
||||
// ── Ready stances ───────────────────────────────────────────────────
|
||||
uint32_t resolvedReady1H = 0;
|
||||
uint32_t resolvedReady2H = 0;
|
||||
uint32_t resolvedReady2HLoose = 0;
|
||||
uint32_t resolvedReadyUnarmed = 0;
|
||||
uint32_t resolvedReadyFist = 0;
|
||||
uint32_t resolvedReadyBow = 0;
|
||||
uint32_t resolvedReadyRifle = 0;
|
||||
uint32_t resolvedReadyCrossbow = 0;
|
||||
uint32_t resolvedReadyThrown = 0;
|
||||
|
||||
// ── Ranged attack resolved IDs ──────────────────────────────────────
|
||||
uint32_t resolvedFireBow = 0;
|
||||
uint32_t resolvedAttackRifle = 0;
|
||||
uint32_t resolvedAttackCrossbow = 0;
|
||||
uint32_t resolvedAttackThrown = 0;
|
||||
uint32_t resolvedLoadBow = 0;
|
||||
uint32_t resolvedLoadRifle = 0;
|
||||
|
||||
// ── Special attacks ─────────────────────────────────────────────────
|
||||
uint32_t resolvedSpecial1H = 0;
|
||||
uint32_t resolvedSpecial2H = 0;
|
||||
uint32_t resolvedSpecialUnarmed = 0;
|
||||
uint32_t resolvedShieldBash = 0;
|
||||
|
||||
// ── Activity resolved IDs ───────────────────────────────────────────
|
||||
uint32_t resolvedStandWound = 0;
|
||||
uint32_t resolvedSitDown = 0;
|
||||
uint32_t resolvedSitLoop = 0;
|
||||
uint32_t resolvedSitUp = 0;
|
||||
uint32_t resolvedKneel = 0;
|
||||
uint32_t resolvedDeath = 0;
|
||||
|
||||
// ── Stealth ─────────────────────────────────────────────────────────
|
||||
uint32_t resolvedStealthIdle = 0;
|
||||
uint32_t resolvedStealthWalk = 0;
|
||||
uint32_t resolvedStealthRun = 0;
|
||||
|
||||
// ── Misc ────────────────────────────────────────────────────────────
|
||||
uint32_t resolvedMount = 0;
|
||||
uint32_t resolvedUnsheathe = 0;
|
||||
uint32_t resolvedSheathe = 0;
|
||||
uint32_t resolvedStun = 0;
|
||||
uint32_t resolvedCombatWound = 0;
|
||||
uint32_t resolvedLoot = 0;
|
||||
|
||||
// ── Capability flags (bitfield) ─────────────────────────────────────
|
||||
bool hasStand : 1;
|
||||
bool hasWalk : 1;
|
||||
bool hasRun : 1;
|
||||
bool hasSprint : 1;
|
||||
bool hasWalkBackwards : 1;
|
||||
bool hasJump : 1;
|
||||
bool hasSwim : 1;
|
||||
bool hasMelee : 1;
|
||||
bool hasStealth : 1;
|
||||
bool hasDeath : 1;
|
||||
bool hasMount : 1;
|
||||
|
||||
// Default-initialize all flags to false
|
||||
AnimCapabilitySet()
|
||||
: hasStand(false), hasWalk(false), hasRun(false), hasSprint(false),
|
||||
hasWalkBackwards(false), hasJump(false), hasSwim(false),
|
||||
hasMelee(false), hasStealth(false), hasDeath(false), hasMount(false) {}
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
31
include/rendering/animation/anim_event.hpp
Normal file
31
include/rendering/animation/anim_event.hpp
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// AnimEvent
|
||||
//
|
||||
// Event-driven animation state transitions. Sub-FSMs react to these events
|
||||
// instead of polling conditions every frame.
|
||||
// ============================================================================
|
||||
enum class AnimEvent : uint8_t {
|
||||
MOVE_START, MOVE_STOP,
|
||||
SPRINT_START, SPRINT_STOP,
|
||||
JUMP, LANDED,
|
||||
SWIM_ENTER, SWIM_EXIT,
|
||||
COMBAT_ENTER, COMBAT_EXIT,
|
||||
STUN_ENTER, STUN_EXIT,
|
||||
SPELL_START, SPELL_STOP,
|
||||
HIT_REACT, CHARGE_START, CHARGE_END,
|
||||
EMOTE_START, EMOTE_STOP,
|
||||
LOOT_START, LOOT_STOP,
|
||||
SIT, STAND_UP,
|
||||
MOUNT, DISMOUNT,
|
||||
STEALTH_ENTER, STEALTH_EXIT,
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
56
include/rendering/animation/animation_manager.hpp
Normal file
56
include/rendering/animation/animation_manager.hpp
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#pragma once
|
||||
|
||||
// Renamed from PlayerAnimator/NpcAnimator dual-map → unified CharacterAnimator registry.
|
||||
// NpcAnimator removed — all characters use the same generic CharacterAnimator.
|
||||
#include "rendering/animation/character_animator.hpp"
|
||||
#include "rendering/animation/anim_capability_set.hpp"
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// AnimationManager
|
||||
//
|
||||
// Central registry for all character animators. Owned by Renderer, replaces
|
||||
// scattered AnimationController* passing.
|
||||
//
|
||||
// Single animator type:
|
||||
// CharacterAnimator — generic animator for any character (player, NPC,
|
||||
// companion). Full FSM composition with priority
|
||||
// resolver.
|
||||
//
|
||||
// AnimationController becomes a thin shim delegating to this manager
|
||||
// until all callsites are migrated.
|
||||
// ============================================================================
|
||||
class AnimationManager {
|
||||
public:
|
||||
AnimationManager() = default;
|
||||
|
||||
// ── Character animators ─────────────────────────────────────────────
|
||||
|
||||
/// Get or create a CharacterAnimator for the given instance ID.
|
||||
CharacterAnimator& getOrCreate(uint32_t instanceId);
|
||||
|
||||
/// Get existing CharacterAnimator (nullptr if not found).
|
||||
CharacterAnimator* get(uint32_t instanceId);
|
||||
|
||||
/// Remove a character animator.
|
||||
void remove(uint32_t instanceId);
|
||||
|
||||
// ── Per-frame ───────────────────────────────────────────────────────
|
||||
|
||||
/// Update all registered animators.
|
||||
void updateAll(float dt);
|
||||
|
||||
// ── Counts ──────────────────────────────────────────────────────────
|
||||
size_t count() const { return animators_.size(); }
|
||||
|
||||
private:
|
||||
std::unordered_map<uint32_t, std::unique_ptr<CharacterAnimator>> animators_;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
145
include/rendering/animation/character_animator.hpp
Normal file
145
include/rendering/animation/character_animator.hpp
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
#pragma once
|
||||
|
||||
#include "rendering/animation/i_character_animator.hpp"
|
||||
#include "rendering/animation/anim_capability_set.hpp"
|
||||
#include "rendering/animation/locomotion_fsm.hpp"
|
||||
#include "rendering/animation/combat_fsm.hpp"
|
||||
#include "rendering/animation/activity_fsm.hpp"
|
||||
#include "rendering/animation/mount_fsm.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// CharacterAnimator
|
||||
//
|
||||
// Generic animator for any character (player, NPC, companion).
|
||||
// Composes LocomotionFSM, CombatFSM, ActivityFSM, and MountFSM with
|
||||
// a priority resolver. Implements ICharacterAnimator.
|
||||
//
|
||||
// Priority order: Stun > HitReaction > Spell > Charge > Combat > Activity > Locomotion
|
||||
//
|
||||
// No idle fallback: if all FSMs return {valid=false}, last animation continues.
|
||||
//
|
||||
// Overlay layer (stealth, sprint) substitutes the resolved anim without
|
||||
// changing sub-FSM state.
|
||||
// ============================================================================
|
||||
class CharacterAnimator final : public ICharacterAnimator {
|
||||
public:
|
||||
CharacterAnimator();
|
||||
|
||||
// ── IAnimator ───────────────────────────────────────────────────────
|
||||
void onEvent(AnimEvent event) override;
|
||||
void update(float dt) override;
|
||||
|
||||
// ── ICharacterAnimator ──────────────────────────────────────────────
|
||||
void startSpellCast(uint32_t precast, uint32_t cast, bool loop, uint32_t finalize) override;
|
||||
void stopSpellCast() override;
|
||||
void triggerMeleeSwing() override;
|
||||
void triggerRangedShot() override;
|
||||
void triggerHitReaction(uint32_t animId) override;
|
||||
void triggerSpecialAttack(uint32_t spellId) override;
|
||||
void setEquippedWeaponType(const WeaponLoadout& loadout) override;
|
||||
void setEquippedRangedType(RangedWeaponType type) override;
|
||||
void playEmote(uint32_t animId, bool loop) override;
|
||||
void cancelEmote() override;
|
||||
void startLooting() override;
|
||||
void stopLooting() override;
|
||||
void setStunned(bool stunned) override;
|
||||
void setCharging(bool charging) override;
|
||||
void setStandState(uint8_t state) override;
|
||||
void setStealthed(bool stealth) override;
|
||||
void setInCombat(bool combat) override;
|
||||
void setLowHealth(bool low) override;
|
||||
void setSprintAuraActive(bool active) override;
|
||||
|
||||
// ── Configuration ───────────────────────────────────────────────────
|
||||
void setCapabilities(const AnimCapabilitySet& caps) { caps_ = caps; }
|
||||
const AnimCapabilitySet& getCapabilities() const { return caps_; }
|
||||
void setWeaponLoadout(const WeaponLoadout& loadout) { loadout_ = loadout; }
|
||||
const WeaponLoadout& getWeaponLoadout() const { return loadout_; }
|
||||
|
||||
// ── Mount ───────────────────────────────────────────────────────────
|
||||
void configureMountFSM(const MountFSM::MountAnimSet& anims, bool taxiFlight);
|
||||
void clearMountFSM();
|
||||
bool isMountActive() const { return mount_.isActive(); }
|
||||
MountFSM& getMountFSM() { return mount_; }
|
||||
|
||||
// ── Sub-FSM access (for transition queries) ─────────────────────────
|
||||
LocomotionFSM& getLocomotion() { return locomotion_; }
|
||||
const LocomotionFSM& getLocomotion() const { return locomotion_; }
|
||||
CombatFSM& getCombat() { return combat_; }
|
||||
const CombatFSM& getCombat() const { return combat_; }
|
||||
ActivityFSM& getActivity() { return activity_; }
|
||||
const ActivityFSM& getActivity() const { return activity_; }
|
||||
|
||||
// ── Last resolved output ────────────────────────────────────────────
|
||||
AnimOutput getLastOutput() const { return lastOutput_; }
|
||||
|
||||
// ── Input injection (set per-frame from AnimationController) ────────
|
||||
struct FrameInput {
|
||||
// From camera controller
|
||||
bool moving = false;
|
||||
bool sprinting = false;
|
||||
bool movingForward = false;
|
||||
bool movingBackward = false;
|
||||
bool autoRunning = false;
|
||||
bool strafeLeft = false;
|
||||
bool strafeRight = false;
|
||||
bool grounded = true;
|
||||
bool jumping = false;
|
||||
bool swimming = false;
|
||||
bool sitting = false;
|
||||
bool flyingActive = false;
|
||||
bool ascending = false;
|
||||
bool descending = false;
|
||||
bool jumpKeyPressed = false;
|
||||
float characterYaw = 0.0f;
|
||||
// Melee/ranged timers
|
||||
float meleeSwingTimer = 0.0f;
|
||||
float rangedShootTimer = 0.0f;
|
||||
uint32_t specialAttackAnimId = 0;
|
||||
uint32_t rangedAnimId = 0;
|
||||
// Animation state query
|
||||
uint32_t currentAnimId = 0;
|
||||
float currentAnimTime = 0.0f;
|
||||
float currentAnimDuration = 0.0f;
|
||||
bool haveAnimState = false;
|
||||
// Mount state query
|
||||
uint32_t curMountAnim = 0;
|
||||
float curMountTime = 0.0f;
|
||||
float curMountDuration = 0.0f;
|
||||
bool haveMountState = false;
|
||||
};
|
||||
|
||||
void setFrameInput(const FrameInput& input) { frameInput_ = input; }
|
||||
|
||||
private:
|
||||
AnimCapabilitySet caps_;
|
||||
WeaponLoadout loadout_;
|
||||
|
||||
LocomotionFSM locomotion_;
|
||||
CombatFSM combat_;
|
||||
ActivityFSM activity_;
|
||||
MountFSM mount_;
|
||||
|
||||
// Overlay flags
|
||||
bool stealthed_ = false;
|
||||
bool sprintAura_ = false;
|
||||
bool lowHealth_ = false;
|
||||
bool inCombat_ = false;
|
||||
|
||||
float lastDt_ = 0.0f;
|
||||
FrameInput frameInput_;
|
||||
AnimOutput lastOutput_;
|
||||
|
||||
/// Priority resolver: highest-priority active FSM wins.
|
||||
AnimOutput resolveAnimation();
|
||||
|
||||
/// Apply stealth/sprint overlays to the resolved animation.
|
||||
AnimOutput applyOverlays(AnimOutput base) const;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
120
include/rendering/animation/combat_fsm.hpp
Normal file
120
include/rendering/animation/combat_fsm.hpp
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#pragma once
|
||||
|
||||
#include "rendering/animation/anim_capability_set.hpp"
|
||||
#include "rendering/animation/anim_event.hpp"
|
||||
#include "rendering/animation/weapon_type.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// CombatFSM
|
||||
//
|
||||
// Pure logic state machine for combat animation. No renderer dependency.
|
||||
// States: INACTIVE · COMBAT_IDLE · MELEE_SWING · RANGED_SHOOT · RANGED_LOAD ·
|
||||
// SPELL_PRECAST · SPELL_CASTING · SPELL_FINALIZE · HIT_REACTION ·
|
||||
// STUNNED · CHARGE · UNSHEATHE · SHEATHE
|
||||
//
|
||||
// Stun overrides all combat states. Spell state cleared on interrupts.
|
||||
// offHandTurn_ alternation managed internally.
|
||||
// ============================================================================
|
||||
class CombatFSM {
|
||||
public:
|
||||
enum class State : uint8_t {
|
||||
INACTIVE,
|
||||
COMBAT_IDLE,
|
||||
MELEE_SWING,
|
||||
RANGED_SHOOT,
|
||||
RANGED_LOAD,
|
||||
SPELL_PRECAST,
|
||||
SPELL_CASTING,
|
||||
SPELL_FINALIZE,
|
||||
HIT_REACTION,
|
||||
STUNNED,
|
||||
CHARGE,
|
||||
UNSHEATHE,
|
||||
SHEATHE,
|
||||
};
|
||||
|
||||
struct Input {
|
||||
bool inCombat = false;
|
||||
bool grounded = true;
|
||||
bool jumping = false;
|
||||
bool swimming = false;
|
||||
bool moving = false;
|
||||
bool sprinting = false;
|
||||
bool lowHealth = false;
|
||||
float meleeSwingTimer = 0.0f; // >0 = melee active
|
||||
float rangedShootTimer = 0.0f; // >0 = ranged active
|
||||
uint32_t specialAttackAnimId = 0;
|
||||
uint32_t rangedAnimId = 0;
|
||||
// Animation state query for one-shot completion detection
|
||||
uint32_t currentAnimId = 0;
|
||||
float currentAnimTime = 0.0f;
|
||||
float currentAnimDuration = 0.0f;
|
||||
bool haveAnimState = false;
|
||||
// Whether model has specific one-shot animations
|
||||
bool hasUnsheathe = false;
|
||||
bool hasSheathe = false;
|
||||
};
|
||||
|
||||
void onEvent(AnimEvent event);
|
||||
|
||||
/// Evaluate current state against input and capabilities.
|
||||
AnimOutput resolve(const Input& in, const AnimCapabilitySet& caps,
|
||||
const WeaponLoadout& loadout);
|
||||
|
||||
State getState() const { return state_; }
|
||||
void setState(State s) { state_ = s; }
|
||||
|
||||
bool isStunned() const { return state_ == State::STUNNED; }
|
||||
bool isActive() const { return state_ != State::INACTIVE; }
|
||||
void reset();
|
||||
|
||||
// ── Spell cast management ───────────────────────────────────────────
|
||||
void startSpellCast(uint32_t precast, uint32_t cast, bool castLoop, uint32_t finalize);
|
||||
void stopSpellCast();
|
||||
void clearSpellState();
|
||||
|
||||
// ── Hit/stun management ─────────────────────────────────────────────
|
||||
void triggerHitReaction(uint32_t animId);
|
||||
void setStunned(bool stunned);
|
||||
void setCharging(bool charging);
|
||||
|
||||
static constexpr uint8_t PRIORITY = 50;
|
||||
|
||||
private:
|
||||
State state_ = State::INACTIVE;
|
||||
|
||||
// Spell cast sequence
|
||||
uint32_t spellPrecastAnimId_ = 0;
|
||||
uint32_t spellCastAnimId_ = 0;
|
||||
uint32_t spellFinalizeAnimId_ = 0;
|
||||
bool spellCastLoop_ = false;
|
||||
bool spellPrecastAnimSeen_ = false;
|
||||
uint8_t spellPrecastFrames_ = 0;
|
||||
bool spellFinalizeAnimSeen_ = false;
|
||||
uint8_t spellFinalizeFrames_ = 0;
|
||||
|
||||
// Hit reaction
|
||||
uint32_t hitReactionAnimId_ = 0;
|
||||
|
||||
// Stun
|
||||
bool stunned_ = false;
|
||||
|
||||
// Charge
|
||||
bool charging_ = false;
|
||||
|
||||
// Off-hand alternation for dual wielding
|
||||
bool offHandTurn_ = false;
|
||||
|
||||
/// Internal: update state transitions based on input.
|
||||
void updateTransitions(const Input& in);
|
||||
|
||||
/// Detect if a one-shot animation has completed.
|
||||
bool oneShotComplete(const Input& in, uint32_t expectedAnimId) const;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
77
include/rendering/animation/emote_registry.hpp
Normal file
77
include/rendering/animation/emote_registry.hpp
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// EmoteRegistry — extracted from AnimationController
|
||||
//
|
||||
// Owns all static emote data, DBC loading, emote text lookup, and
|
||||
// animation ID resolution. Singleton — loaded once on first use.
|
||||
// ============================================================================
|
||||
|
||||
struct EmoteInfo {
|
||||
uint32_t animId = 0;
|
||||
uint32_t dbcId = 0;
|
||||
bool loop = false;
|
||||
std::string textNoTarget;
|
||||
std::string textTarget;
|
||||
std::string othersNoTarget;
|
||||
std::string othersTarget;
|
||||
std::string command;
|
||||
};
|
||||
|
||||
class EmoteRegistry {
|
||||
public:
|
||||
static EmoteRegistry& instance();
|
||||
|
||||
/// Load emotes from DBC files (called once on first use).
|
||||
void loadFromDbc();
|
||||
|
||||
struct EmoteResult { uint32_t animId; bool loop; };
|
||||
|
||||
/// Look up an emote by chat command (e.g. "dance", "wave").
|
||||
std::optional<EmoteResult> findEmote(const std::string& command) const;
|
||||
|
||||
/// Get the animation ID for a DBC emote ID.
|
||||
uint32_t animByDbcId(uint32_t dbcId) const;
|
||||
|
||||
/// Get the emote state variant (looping) for a one-shot emote animation.
|
||||
uint32_t getStateVariant(uint32_t oneShotAnimId) const;
|
||||
|
||||
/// Get first-person emote text for a command.
|
||||
std::string textFor(const std::string& emoteName,
|
||||
const std::string* targetName = nullptr) const;
|
||||
|
||||
/// Get DBC ID for an emote command.
|
||||
uint32_t dbcIdFor(const std::string& emoteName) const;
|
||||
|
||||
/// Get third-person emote text by DBC ID.
|
||||
std::string textByDbcId(uint32_t dbcId,
|
||||
const std::string& senderName,
|
||||
const std::string* targetName = nullptr) const;
|
||||
|
||||
/// Get the full EmoteInfo for a command (nullptr if not found).
|
||||
const EmoteInfo* findInfo(const std::string& command) const;
|
||||
|
||||
private:
|
||||
EmoteRegistry() = default;
|
||||
EmoteRegistry(const EmoteRegistry&) = delete;
|
||||
EmoteRegistry& operator=(const EmoteRegistry&) = delete;
|
||||
|
||||
void loadFallbackEmotes();
|
||||
void buildDbcIdIndex();
|
||||
|
||||
bool loaded_ = false;
|
||||
std::unordered_map<std::string, EmoteInfo> emoteTable_;
|
||||
std::unordered_map<uint32_t, const EmoteInfo*> emoteByDbcId_;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
54
include/rendering/animation/footstep_driver.hpp
Normal file
54
include/rendering/animation/footstep_driver.hpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace wowee {
|
||||
|
||||
namespace audio { enum class FootstepSurface : uint8_t; }
|
||||
|
||||
namespace rendering {
|
||||
|
||||
class Renderer;
|
||||
|
||||
// ============================================================================
|
||||
// FootstepDriver — extracted from AnimationController
|
||||
//
|
||||
// Owns animation-driven footstep event detection, surface resolution,
|
||||
// and player/mount footstep tracking state.
|
||||
// ============================================================================
|
||||
class FootstepDriver {
|
||||
public:
|
||||
FootstepDriver() = default;
|
||||
|
||||
/// Process footstep events for this frame (called from Renderer::update).
|
||||
void update(float deltaTime, Renderer* renderer,
|
||||
bool mounted, uint32_t mountInstanceId, bool taxiFlight,
|
||||
bool isFootstepState);
|
||||
|
||||
/// Detect if a footstep event should trigger based on animation phase crossing.
|
||||
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs,
|
||||
float animationDurationMs);
|
||||
|
||||
/// Resolve the surface type under the character for footstep sound selection.
|
||||
audio::FootstepSurface resolveFootstepSurface(Renderer* renderer) const;
|
||||
|
||||
private:
|
||||
// Player footstep event tracking (animation-driven)
|
||||
uint32_t footstepLastAnimationId_ = 0;
|
||||
float footstepLastNormTime_ = 0.0f;
|
||||
bool footstepNormInitialized_ = false;
|
||||
|
||||
// Footstep surface cache (avoid expensive queries every step)
|
||||
mutable audio::FootstepSurface cachedFootstepSurface_{};
|
||||
mutable glm::vec3 cachedFootstepPosition_{0.0f, 0.0f, 0.0f};
|
||||
mutable float cachedFootstepUpdateTimer_{999.0f};
|
||||
|
||||
// Mount footstep tracking (separate from player's)
|
||||
uint32_t mountFootstepLastAnimId_ = 0;
|
||||
float mountFootstepLastNormTime_ = 0.0f;
|
||||
bool mountFootstepNormInitialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
31
include/rendering/animation/i_anim_renderer.hpp
Normal file
31
include/rendering/animation/i_anim_renderer.hpp
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include "rendering/animation/anim_capability_set.hpp"
|
||||
#include "rendering/animation/anim_event.hpp"
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline { struct M2Sequence; }
|
||||
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// IAnimRenderer
|
||||
//
|
||||
// Abstraction for renderer animation operations. Sub-FSMs and animators
|
||||
// talk to this interface, not to CharacterRenderer directly.
|
||||
// ============================================================================
|
||||
class IAnimRenderer {
|
||||
public:
|
||||
virtual void playAnimation(uint32_t instanceId, uint32_t animId, bool loop) = 0;
|
||||
virtual bool hasAnimation(uint32_t instanceId, uint32_t animId) const = 0;
|
||||
virtual bool getAnimationState(uint32_t instanceId, uint32_t& outAnimId,
|
||||
float& outTimeMs, float& outDurMs) const = 0;
|
||||
virtual bool getAnimationSequences(uint32_t instanceId,
|
||||
std::vector<pipeline::M2Sequence>& out) const = 0;
|
||||
virtual ~IAnimRenderer() = default;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
21
include/rendering/animation/i_animator.hpp
Normal file
21
include/rendering/animation/i_animator.hpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include "rendering/animation/anim_event.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// IAnimator
|
||||
//
|
||||
// Base interface for all entity animators. Common to player + NPC.
|
||||
// ============================================================================
|
||||
class IAnimator {
|
||||
public:
|
||||
virtual void onEvent(AnimEvent event) = 0;
|
||||
virtual void update(float dt) = 0;
|
||||
virtual ~IAnimator() = default;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
42
include/rendering/animation/i_character_animator.hpp
Normal file
42
include/rendering/animation/i_character_animator.hpp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include "rendering/animation/i_animator.hpp"
|
||||
#include "rendering/animation/weapon_type.hpp"
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// ICharacterAnimator
|
||||
//
|
||||
// Player-specific animation interface. Extends IAnimator with combat,
|
||||
// spell, emote, and mount operations.
|
||||
// ============================================================================
|
||||
class ICharacterAnimator : public IAnimator {
|
||||
public:
|
||||
virtual void startSpellCast(uint32_t precast, uint32_t cast, bool loop, uint32_t finalize) = 0;
|
||||
virtual void stopSpellCast() = 0;
|
||||
virtual void triggerMeleeSwing() = 0;
|
||||
virtual void triggerRangedShot() = 0;
|
||||
virtual void triggerHitReaction(uint32_t animId) = 0;
|
||||
virtual void triggerSpecialAttack(uint32_t spellId) = 0;
|
||||
virtual void setEquippedWeaponType(const WeaponLoadout& loadout) = 0;
|
||||
virtual void setEquippedRangedType(RangedWeaponType type) = 0;
|
||||
virtual void playEmote(uint32_t animId, bool loop) = 0;
|
||||
virtual void cancelEmote() = 0;
|
||||
virtual void startLooting() = 0;
|
||||
virtual void stopLooting() = 0;
|
||||
virtual void setStunned(bool stunned) = 0;
|
||||
virtual void setCharging(bool charging) = 0;
|
||||
virtual void setStandState(uint8_t state) = 0;
|
||||
virtual void setStealthed(bool stealth) = 0;
|
||||
virtual void setInCombat(bool combat) = 0;
|
||||
virtual void setLowHealth(bool low) = 0;
|
||||
virtual void setSprintAuraActive(bool active) = 0;
|
||||
virtual ~ICharacterAnimator() = default;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
79
include/rendering/animation/locomotion_fsm.hpp
Normal file
79
include/rendering/animation/locomotion_fsm.hpp
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#pragma once
|
||||
|
||||
#include "rendering/animation/anim_capability_set.hpp"
|
||||
#include "rendering/animation/anim_event.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// LocomotionFSM
|
||||
//
|
||||
// Pure logic state machine for movement animation. No renderer dependency.
|
||||
// States: IDLE · WALK · RUN · JUMP_START · JUMP_MID · JUMP_END · SWIM_IDLE · SWIM
|
||||
//
|
||||
// Grace timer is internal — no external locomotionStopGraceTimer_ needed.
|
||||
// ============================================================================
|
||||
class LocomotionFSM {
|
||||
public:
|
||||
enum class State : uint8_t {
|
||||
IDLE, WALK, RUN,
|
||||
JUMP_START, JUMP_MID, JUMP_END,
|
||||
SWIM_IDLE, SWIM,
|
||||
};
|
||||
|
||||
struct Input {
|
||||
bool moving = false;
|
||||
bool movingForward = false;
|
||||
bool sprinting = false;
|
||||
bool movingBackward = false;
|
||||
bool strafeLeft = false;
|
||||
bool strafeRight = false;
|
||||
bool grounded = true;
|
||||
bool jumping = false;
|
||||
bool swimming = false;
|
||||
bool sitting = false;
|
||||
bool sprintAura = false; // Sprint/Dash aura — use SPRINT anim
|
||||
float deltaTime = 0.0f;
|
||||
// Animation state for one-shot completion detection (jump start/end)
|
||||
uint32_t currentAnimId = 0;
|
||||
float currentAnimTime = 0.0f;
|
||||
float currentAnimDuration = 0.0f;
|
||||
bool haveAnimState = false;
|
||||
};
|
||||
|
||||
/// Process event and update internal state.
|
||||
void onEvent(AnimEvent event);
|
||||
|
||||
/// Evaluate current state against input and capabilities.
|
||||
/// Returns AnimOutput with valid=false if no change needed (STAY policy).
|
||||
AnimOutput resolve(const Input& in, const AnimCapabilitySet& caps);
|
||||
|
||||
State getState() const { return state_; }
|
||||
void setState(State s) { state_ = s; }
|
||||
void reset();
|
||||
|
||||
static constexpr uint8_t PRIORITY = 10;
|
||||
|
||||
private:
|
||||
State state_ = State::IDLE;
|
||||
|
||||
// Grace timer: short delay before switching from WALK/RUN to IDLE
|
||||
// to avoid flickering on network jitter
|
||||
float graceTimer_ = 0.0f;
|
||||
bool wasSprinting_ = false;
|
||||
|
||||
// One-shot tracking for jump start/end animations
|
||||
bool jumpStartSeen_ = false;
|
||||
bool jumpEndSeen_ = false;
|
||||
|
||||
static constexpr float kGraceSec = 0.12f;
|
||||
|
||||
/// Internal: update state transitions based on input.
|
||||
void updateTransitions(const Input& in, const AnimCapabilitySet& caps);
|
||||
bool oneShotComplete(const Input& in, uint32_t expectedAnimId) const;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
142
include/rendering/animation/mount_fsm.hpp
Normal file
142
include/rendering/animation/mount_fsm.hpp
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
#pragma once
|
||||
|
||||
#include "rendering/animation/anim_capability_set.hpp"
|
||||
#include "rendering/animation/anim_event.hpp"
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <random>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ============================================================================
|
||||
// MountFSM
|
||||
//
|
||||
// Self-contained mount animation state machine. Replaces the ~400-line
|
||||
// mounted branch of updateCharacterAnimation() and eliminates the `goto`.
|
||||
//
|
||||
// Owns: fidget timer, RNG, idle sound timer, mount action state.
|
||||
// All static RNG replaced with per-instance members.
|
||||
// ============================================================================
|
||||
class MountFSM {
|
||||
public:
|
||||
// Animation set discovered at mount time (property-based, not hardcoded)
|
||||
struct MountAnimSet {
|
||||
uint32_t jumpStart = 0;
|
||||
uint32_t jumpLoop = 0;
|
||||
uint32_t jumpEnd = 0;
|
||||
uint32_t rearUp = 0;
|
||||
uint32_t run = 0;
|
||||
uint32_t stand = 0;
|
||||
// Flight animations
|
||||
uint32_t flyIdle = 0;
|
||||
uint32_t flyForward = 0;
|
||||
uint32_t flyBackwards = 0;
|
||||
uint32_t flyLeft = 0;
|
||||
uint32_t flyRight = 0;
|
||||
uint32_t flyUp = 0;
|
||||
uint32_t flyDown = 0;
|
||||
std::vector<uint32_t> fidgets;
|
||||
};
|
||||
|
||||
enum class MountState : uint8_t {
|
||||
IDLE, RUN, JUMP_START, JUMP_LOOP, JUMP_LAND, REAR_UP, FLY,
|
||||
};
|
||||
|
||||
enum class MountAction : uint8_t { None, Jump, RearUp };
|
||||
|
||||
struct Input {
|
||||
bool moving = false;
|
||||
bool movingBackward = false;
|
||||
bool strafeLeft = false;
|
||||
bool strafeRight = false;
|
||||
bool grounded = true;
|
||||
bool jumpKeyPressed = false;
|
||||
bool flying = false;
|
||||
bool swimming = false;
|
||||
bool ascending = false;
|
||||
bool descending = false;
|
||||
bool taxiFlight = false;
|
||||
float deltaTime = 0.0f;
|
||||
float characterYaw = 0.0f;
|
||||
// Mount anim state query
|
||||
uint32_t curMountAnim = 0;
|
||||
float curMountTime = 0.0f;
|
||||
float curMountDuration = 0.0f;
|
||||
bool haveMountState = false;
|
||||
};
|
||||
|
||||
/// Output from evaluate(): what to play on rider + mount, and positioning data.
|
||||
struct Output {
|
||||
// Mount animation
|
||||
uint32_t mountAnimId = 0;
|
||||
bool mountAnimLoop = true;
|
||||
bool mountAnimChanged = false; // true = should call playAnimation
|
||||
|
||||
// Rider animation
|
||||
uint32_t riderAnimId = 0;
|
||||
bool riderAnimLoop = true;
|
||||
bool riderAnimChanged = false;
|
||||
|
||||
// Mount procedural motion
|
||||
float mountBob = 0.0f; // Vertical bob offset
|
||||
float mountPitch = 0.0f; // Pitch (forward lean)
|
||||
float mountRoll = 0.0f; // Roll (banking)
|
||||
|
||||
// Signals
|
||||
bool playJumpSound = false;
|
||||
bool playLandSound = false;
|
||||
bool playRearUpSound = false;
|
||||
bool playIdleSound = false;
|
||||
bool triggerMountJump = false; // Tell camera controller to jump
|
||||
bool fidgetStarted = false;
|
||||
};
|
||||
|
||||
void configure(const MountAnimSet& anims, bool taxiFlight);
|
||||
void clear();
|
||||
void onEvent(AnimEvent event);
|
||||
|
||||
/// Main evaluation: produces Output describing what to play.
|
||||
Output evaluate(const Input& in);
|
||||
|
||||
bool isActive() const { return active_; }
|
||||
MountState getState() const { return state_; }
|
||||
MountAction getAction() const { return action_; }
|
||||
const MountAnimSet& getAnims() const { return anims_; }
|
||||
|
||||
private:
|
||||
bool active_ = false;
|
||||
MountState state_ = MountState::IDLE;
|
||||
MountAction action_ = MountAction::None;
|
||||
uint32_t actionPhase_ = 0;
|
||||
|
||||
MountAnimSet anims_;
|
||||
bool taxiFlight_ = false;
|
||||
|
||||
// Fidget system — per-instance, not static
|
||||
float fidgetTimer_ = 0.0f;
|
||||
float nextFidgetTime_ = 8.0f;
|
||||
uint32_t activeFidget_ = 0;
|
||||
std::mt19937 rng_;
|
||||
|
||||
// Idle ambient sound timer
|
||||
float idleSoundTimer_ = 0.0f;
|
||||
float nextIdleSoundTime_ = 60.0f;
|
||||
|
||||
// Procedural lean
|
||||
float prevYaw_ = 0.0f;
|
||||
float roll_ = 0.0f;
|
||||
|
||||
// Last mount animation for change detection
|
||||
uint32_t lastMountAnim_ = 0;
|
||||
|
||||
/// Resolve the mount animation for the given input (non-taxi).
|
||||
uint32_t resolveGroundOrFlyAnim(const Input& in) const;
|
||||
|
||||
/// Check if an action animation has completed.
|
||||
bool actionAnimComplete(const Input& in) const;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
35
include/rendering/animation/sfx_state_driver.hpp
Normal file
35
include/rendering/animation/sfx_state_driver.hpp
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
class Renderer;
|
||||
class FootstepDriver;
|
||||
|
||||
// ============================================================================
|
||||
// SfxStateDriver — extracted from AnimationController
|
||||
//
|
||||
// Tracks state transitions for activity SFX (jump, landing, swim) and
|
||||
// mount ambient sounds.
|
||||
// ============================================================================
|
||||
class SfxStateDriver {
|
||||
public:
|
||||
SfxStateDriver() = default;
|
||||
|
||||
/// Track state transitions and trigger appropriate SFX.
|
||||
void update(float deltaTime, Renderer* renderer,
|
||||
bool mounted, bool taxiFlight,
|
||||
FootstepDriver& footstepDriver);
|
||||
|
||||
private:
|
||||
bool initialized_ = false;
|
||||
bool prevGrounded_ = true;
|
||||
bool prevJumping_ = false;
|
||||
bool prevFalling_ = false;
|
||||
bool prevSwimming_ = false;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
28
include/rendering/animation/weapon_type.hpp
Normal file
28
include/rendering/animation/weapon_type.hpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
/// Ranged weapon type for animation selection (bow/gun/crossbow/thrown)
|
||||
enum class RangedWeaponType : uint8_t { NONE = 0, BOW, GUN, CROSSBOW, THROWN };
|
||||
|
||||
// ============================================================================
|
||||
// WeaponLoadout — extracted from AnimationController
|
||||
//
|
||||
// Consolidates the 6 weapon boolean fields + inventory type + ranged type
|
||||
// into a single value type.
|
||||
// ============================================================================
|
||||
struct WeaponLoadout {
|
||||
uint32_t inventoryType = 0;
|
||||
bool is2HLoose = false; // Polearm or staff
|
||||
bool isFist = false; // Fist weapon
|
||||
bool isDagger = false; // Dagger (uses pierce variants)
|
||||
bool hasOffHand = false; // Has off-hand weapon (dual wield)
|
||||
bool hasShield = false; // Has shield equipped (for SHIELD_BASH)
|
||||
RangedWeaponType rangedType = RangedWeaponType::NONE;
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
|
|
@ -4,22 +4,31 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
#include <glm/glm.hpp>
|
||||
#include "rendering/animation/footstep_driver.hpp"
|
||||
#include "rendering/animation/sfx_state_driver.hpp"
|
||||
#include "rendering/animation/weapon_type.hpp"
|
||||
#include "rendering/animation/anim_capability_set.hpp"
|
||||
#include "rendering/animation/character_animator.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace audio { enum class FootstepSurface : uint8_t; }
|
||||
namespace rendering {
|
||||
|
||||
class Renderer;
|
||||
|
||||
/// Ranged weapon type for animation selection (bow/gun/crossbow/thrown)
|
||||
enum class RangedWeaponType : uint8_t { NONE = 0, BOW, GUN, CROSSBOW, THROWN };
|
||||
|
||||
// ============================================================================
|
||||
// AnimationController — extracted from Renderer (§4.2)
|
||||
// AnimationController — thin adapter wrapping CharacterAnimator
|
||||
//
|
||||
// Owns the character locomotion state machine, mount animation state,
|
||||
// emote system, footstep triggering, surface detection, melee combat
|
||||
// animation, and activity SFX transition tracking.
|
||||
// Bridges the Renderer world (camera state, CharacterRenderer, audio)
|
||||
// and the pure-logic CharacterAnimator + sub-FSMs. Public API unchanged.
|
||||
//
|
||||
// Responsibilities:
|
||||
// · Collect inputs from renderer/camera → CharacterAnimator::FrameInput
|
||||
// · Forward state-changing calls → CharacterAnimator
|
||||
// · Read AnimOutput from CharacterAnimator → apply via CharacterRenderer
|
||||
// · Mount discovery (needs renderer for sequence queries)
|
||||
// · Mount positioning (needs renderer for attachment queries)
|
||||
// · Melee/ranged resolution (needs renderer for sequence queries)
|
||||
// · Footstep and SFX drivers (already extracted)
|
||||
// ============================================================================
|
||||
class AnimationController {
|
||||
public:
|
||||
|
|
@ -28,6 +37,11 @@ public:
|
|||
|
||||
void initialize(Renderer* renderer);
|
||||
|
||||
/// Probe (or re-probe) animation capabilities for the current character model.
|
||||
/// Called once during initialize() / onCharacterFollow() and after model changes.
|
||||
void probeCapabilities();
|
||||
const AnimCapabilitySet& getCapabilities() const { return characterAnimator_.getCapabilities(); }
|
||||
|
||||
// ── Per-frame update hooks (called from Renderer::update) ──────────────
|
||||
// Runs the character animation state machine (mounted + unmounted).
|
||||
void updateCharacterAnimation();
|
||||
|
|
@ -47,7 +61,7 @@ public:
|
|||
// ── Emote support ──────────────────────────────────────────────────────
|
||||
void playEmote(const std::string& emoteName);
|
||||
void cancelEmote();
|
||||
bool isEmoteActive() const { return emoteActive_; }
|
||||
bool isEmoteActive() const { return characterAnimator_.getActivity().isEmoteActive(); }
|
||||
static std::string getEmoteText(const std::string& emoteName,
|
||||
const std::string* targetName = nullptr);
|
||||
static uint32_t getEmoteDbcId(const std::string& emoteName);
|
||||
|
|
@ -58,7 +72,7 @@ public:
|
|||
|
||||
// ── Targeting / combat ─────────────────────────────────────────────────
|
||||
void setTargetPosition(const glm::vec3* pos);
|
||||
void setInCombat(bool combat) { inCombat_ = combat; }
|
||||
void setInCombat(bool combat);
|
||||
bool isInCombat() const { return inCombat_; }
|
||||
const glm::vec3* getTargetPosition() const { return targetPosition_; }
|
||||
void resetCombatVisualState();
|
||||
|
|
@ -70,30 +84,19 @@ public:
|
|||
/// is2HLoose: true for polearms/staves (use ATTACK_2H_LOOSE instead of ATTACK_2H)
|
||||
void setEquippedWeaponType(uint32_t inventoryType, bool is2HLoose = false,
|
||||
bool isFist = false, bool isDagger = false,
|
||||
bool hasOffHand = false, bool hasShield = false) {
|
||||
equippedWeaponInvType_ = inventoryType;
|
||||
equippedIs2HLoose_ = is2HLoose;
|
||||
equippedIsFist_ = isFist;
|
||||
equippedIsDagger_ = isDagger;
|
||||
equippedHasOffHand_ = hasOffHand;
|
||||
equippedHasShield_ = hasShield;
|
||||
meleeAnimId_ = 0; // Force re-resolve on next swing
|
||||
}
|
||||
bool hasOffHand = false, bool hasShield = false);
|
||||
/// Play a special attack animation for a melee ability (spellId → SPECIAL_1H/2H/SHIELD_BASH/WHIRLWIND)
|
||||
void triggerSpecialAttack(uint32_t spellId);
|
||||
|
||||
// ── Sprint aura animation ────────────────────────────────────────────
|
||||
void setSprintAuraActive(bool active) { sprintAuraActive_ = active; }
|
||||
void setSprintAuraActive(bool active);
|
||||
|
||||
// ── Ranged combat ──────────────────────────────────────────────────────
|
||||
void setEquippedRangedType(RangedWeaponType type) {
|
||||
equippedRangedType_ = type;
|
||||
rangedAnimId_ = 0; // Force re-resolve
|
||||
}
|
||||
void setEquippedRangedType(RangedWeaponType type);
|
||||
/// Trigger a ranged shot animation (Auto Shot, Shoot, Throw)
|
||||
void triggerRangedShot();
|
||||
RangedWeaponType getEquippedRangedType() const { return equippedRangedType_; }
|
||||
void setCharging(bool charging) { charging_ = charging; }
|
||||
RangedWeaponType getEquippedRangedType() const { return weaponLoadout_.rangedType; }
|
||||
void setCharging(bool charging);
|
||||
bool isCharging() const { return charging_; }
|
||||
|
||||
// ── Spell casting ──────────────────────────────────────────────────────
|
||||
|
|
@ -122,7 +125,7 @@ public:
|
|||
|
||||
// ── Health-based idle ──────────────────────────────────────────────────
|
||||
/// When true, idle/combat-idle will prefer STAND_WOUND if the model has it.
|
||||
void setLowHealth(bool low) { lowHealth_ = low; }
|
||||
void setLowHealth(bool low);
|
||||
|
||||
// ── Stand state (sit/sleep/kneel transitions) ──────────────────────────
|
||||
// WoW UnitStandStateType constants
|
||||
|
|
@ -166,139 +169,55 @@ public:
|
|||
private:
|
||||
Renderer* renderer_ = nullptr;
|
||||
|
||||
// Character animation state machine
|
||||
enum class CharAnimState {
|
||||
IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING,
|
||||
SIT_UP, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE,
|
||||
SPELL_PRECAST, SPELL_CASTING, SPELL_FINALIZE, HIT_REACTION, STUNNED, LOOTING,
|
||||
UNSHEATHE, SHEATHE, // Weapon draw/put-away one-shot transitions
|
||||
RANGED_SHOOT, RANGED_LOAD // Ranged attack sequence: shoot → reload
|
||||
};
|
||||
CharAnimState charAnimState_ = CharAnimState::IDLE;
|
||||
float locomotionStopGraceTimer_ = 0.0f;
|
||||
bool locomotionWasSprinting_ = false;
|
||||
// ── CharacterAnimator: owns the complete character animation FSM ─────
|
||||
CharacterAnimator characterAnimator_;
|
||||
bool capabilitiesProbed_ = false;
|
||||
|
||||
// ── Animation change detection ───────────────────────────────────────
|
||||
uint32_t lastPlayerAnimRequest_ = UINT32_MAX;
|
||||
bool lastPlayerAnimLoopRequest_ = true;
|
||||
float lastDeltaTime_ = 0.0f;
|
||||
|
||||
// Emote state
|
||||
bool emoteActive_ = false;
|
||||
uint32_t emoteAnimId_ = 0;
|
||||
bool emoteLoop_ = false;
|
||||
|
||||
// Spell cast sequence state (PRECAST → CASTING → FINALIZE)
|
||||
uint32_t spellPrecastAnimId_ = 0; // One-shot wind-up (phase 1)
|
||||
uint32_t spellCastAnimId_ = 0; // Looping cast hold (phase 2)
|
||||
uint32_t spellFinalizeAnimId_ = 0; // One-shot release (phase 3)
|
||||
bool spellCastLoop_ = false;
|
||||
|
||||
// Hit reaction state
|
||||
uint32_t hitReactionAnimId_ = 0;
|
||||
|
||||
// Crowd control
|
||||
bool stunned_ = false;
|
||||
|
||||
// Health-based idle
|
||||
bool lowHealth_ = false;
|
||||
|
||||
// Stand state (sit/sleep/kneel)
|
||||
uint8_t standState_ = 0;
|
||||
uint32_t sitDownAnim_ = 0; // Transition-in animation (one-shot)
|
||||
uint32_t sitLoopAnim_ = 0; // Looping pose animation
|
||||
uint32_t sitUpAnim_ = 0; // Transition-out animation (one-shot)
|
||||
|
||||
// Stealth
|
||||
bool stealthed_ = false;
|
||||
|
||||
// Target facing
|
||||
// ── Externally-queried state (mirrored to CharacterAnimator) ────────────
|
||||
const glm::vec3* targetPosition_ = nullptr;
|
||||
bool inCombat_ = false;
|
||||
|
||||
// Footstep event tracking (animation-driven)
|
||||
uint32_t footstepLastAnimationId_ = 0;
|
||||
float footstepLastNormTime_ = 0.0f;
|
||||
bool footstepNormInitialized_ = false;
|
||||
|
||||
// Footstep surface cache (avoid expensive queries every step)
|
||||
mutable audio::FootstepSurface cachedFootstepSurface_{};
|
||||
mutable glm::vec3 cachedFootstepPosition_{0.0f, 0.0f, 0.0f};
|
||||
mutable float cachedFootstepUpdateTimer_{999.0f};
|
||||
|
||||
// Mount footstep tracking (separate from player's)
|
||||
uint32_t mountFootstepLastAnimId_ = 0;
|
||||
float mountFootstepLastNormTime_ = 0.0f;
|
||||
bool mountFootstepNormInitialized_ = false;
|
||||
|
||||
// SFX transition state
|
||||
bool sfxStateInitialized_ = false;
|
||||
bool sfxPrevGrounded_ = true;
|
||||
bool sfxPrevJumping_ = false;
|
||||
bool sfxPrevFalling_ = false;
|
||||
bool sfxPrevSwimming_ = false;
|
||||
|
||||
// Melee combat
|
||||
bool stunned_ = false;
|
||||
bool charging_ = false;
|
||||
|
||||
// ── Footstep event tracking (delegated to FootstepDriver) ────────────
|
||||
FootstepDriver footstepDriver_;
|
||||
|
||||
// ── SFX transition state (delegated to SfxStateDriver) ───────────────
|
||||
SfxStateDriver sfxStateDriver_;
|
||||
|
||||
// ── Melee combat (needs renderer for sequence queries) ───────────────
|
||||
float meleeSwingTimer_ = 0.0f;
|
||||
float meleeSwingCooldown_ = 0.0f;
|
||||
float meleeAnimDurationMs_ = 0.0f;
|
||||
uint32_t meleeAnimId_ = 0;
|
||||
uint32_t specialAttackAnimId_ = 0; // Non-zero during special attack (overrides resolveMeleeAnimId)
|
||||
uint32_t equippedWeaponInvType_ = 0;
|
||||
bool equippedIs2HLoose_ = false; // Polearm or staff
|
||||
bool equippedIsFist_ = false; // Fist weapon
|
||||
bool equippedIsDagger_ = false; // Dagger (uses pierce variants)
|
||||
bool equippedHasOffHand_ = false; // Has off-hand weapon (dual wield)
|
||||
bool equippedHasShield_ = false; // Has shield equipped (for SHIELD_BASH)
|
||||
bool meleeOffHandTurn_ = false; // Alternates main/off-hand swings
|
||||
uint32_t specialAttackAnimId_ = 0;
|
||||
WeaponLoadout weaponLoadout_;
|
||||
bool meleeOffHandTurn_ = false;
|
||||
|
||||
// Ranged weapon state
|
||||
RangedWeaponType equippedRangedType_ = RangedWeaponType::NONE;
|
||||
float rangedShootTimer_ = 0.0f; // Countdown for ranged attack animation
|
||||
uint32_t rangedAnimId_ = 0; // Cached ranged attack animation
|
||||
|
||||
// Mount animation capabilities (discovered at mount time, varies per model)
|
||||
struct MountAnimSet {
|
||||
uint32_t jumpStart = 0; // Jump start animation
|
||||
uint32_t jumpLoop = 0; // Jump airborne loop
|
||||
uint32_t jumpEnd = 0; // Jump landing
|
||||
uint32_t rearUp = 0; // Rear-up / special flourish
|
||||
uint32_t run = 0; // Run animation (discovered, don't assume)
|
||||
uint32_t stand = 0; // Stand animation (discovered)
|
||||
// Flight animations (discovered from mount model)
|
||||
uint32_t flyIdle = 0;
|
||||
uint32_t flyForward = 0;
|
||||
uint32_t flyBackwards = 0;
|
||||
uint32_t flyLeft = 0;
|
||||
uint32_t flyRight = 0;
|
||||
uint32_t flyUp = 0;
|
||||
uint32_t flyDown = 0;
|
||||
std::vector<uint32_t> fidgets; // Idle fidget animations (head turn, tail swish, etc.)
|
||||
};
|
||||
|
||||
enum class MountAction { None, Jump, RearUp };
|
||||
// ── Ranged weapon state ──────────────────────────────────────────────
|
||||
float rangedShootTimer_ = 0.0f;
|
||||
uint32_t rangedAnimId_ = 0;
|
||||
|
||||
// ── Mount state (discovery + positioning need renderer) ──────────────
|
||||
uint32_t mountInstanceId_ = 0;
|
||||
float mountHeightOffset_ = 0.0f;
|
||||
float mountPitch_ = 0.0f; // Up/down tilt (radians)
|
||||
float mountRoll_ = 0.0f; // Left/right banking (radians)
|
||||
int mountSeatAttachmentId_ = -1; // -1 unknown, -2 unavailable
|
||||
float mountPitch_ = 0.0f;
|
||||
float mountRoll_ = 0.0f; // External roll for taxi flights (lean roll computed by MountFSM)
|
||||
int mountSeatAttachmentId_ = -1;
|
||||
glm::vec3 smoothedMountSeatPos_ = glm::vec3(0.0f);
|
||||
bool mountSeatSmoothingInit_ = false;
|
||||
float prevMountYaw_ = 0.0f; // Previous yaw for turn rate calculation (procedural lean)
|
||||
float lastDeltaTime_ = 0.0f; // Cached for use in updateCharacterAnimation()
|
||||
MountAction mountAction_ = MountAction::None; // Current mount action (jump/rear-up)
|
||||
uint32_t mountActionPhase_ = 0; // 0=start, 1=loop, 2=end (for jump chaining)
|
||||
MountAnimSet mountAnims_; // Cached animation IDs for current mount
|
||||
float mountIdleFidgetTimer_ = 0.0f; // Timer for random idle fidgets
|
||||
float mountIdleSoundTimer_ = 0.0f; // Timer for ambient idle sounds
|
||||
uint32_t mountActiveFidget_ = 0; // Currently playing fidget animation ID (0 = none)
|
||||
bool taxiFlight_ = false;
|
||||
bool taxiAnimsLogged_ = false;
|
||||
bool sprintAuraActive_ = false; // Sprint/Dash aura active → use SPRINT anim
|
||||
bool sprintAuraActive_ = false;
|
||||
|
||||
// Private animation helpers
|
||||
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs);
|
||||
audio::FootstepSurface resolveFootstepSurface() const;
|
||||
// ── Private helpers ──────────────────────────────────────────────────
|
||||
uint32_t resolveMeleeAnimId();
|
||||
void updateMountedAnimation(float deltaTime);
|
||||
void applyMountPositioning(float mountBob, float mountRoll, float characterYaw);
|
||||
};
|
||||
|
||||
} // namespace rendering
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@ public:
|
|||
using StandUpCallback = std::function<void()>;
|
||||
void setStandUpCallback(StandUpCallback cb) { standUpCallback_ = std::move(cb); }
|
||||
|
||||
// Callback invoked when the player sits down via local input (X key).
|
||||
using SitDownCallback = std::function<void()>;
|
||||
void setSitDownCallback(SitDownCallback cb) { sitDownCallback_ = std::move(cb); }
|
||||
|
||||
// Callback invoked when auto-follow is cancelled by user movement input.
|
||||
using AutoFollowCancelCallback = std::function<void()>;
|
||||
void setAutoFollowCancelCallback(AutoFollowCancelCallback cb) { autoFollowCancelCallback_ = std::move(cb); }
|
||||
|
|
@ -310,6 +314,7 @@ private:
|
|||
// Movement callback
|
||||
MovementCallback movementCallback;
|
||||
StandUpCallback standUpCallback_;
|
||||
SitDownCallback sitDownCallback_;
|
||||
AutoFollowCancelCallback autoFollowCancelCallback_;
|
||||
|
||||
// Movement speeds
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#include "core/application.hpp"
|
||||
#include "core/coordinates.hpp"
|
||||
#include "core/profiler.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "rendering/animation_controller.hpp"
|
||||
#include <unordered_set>
|
||||
#include <cmath>
|
||||
|
|
@ -946,6 +946,16 @@ void Application::setState(AppState newState) {
|
|||
gameHandler->setStandState(rendering::AnimationController::STAND_STATE_STAND);
|
||||
}
|
||||
});
|
||||
cc->setSitDownCallback([this]() {
|
||||
if (gameHandler) {
|
||||
gameHandler->setStandState(rendering::AnimationController::STAND_STATE_SIT);
|
||||
}
|
||||
if (renderer) {
|
||||
if (auto* ac = renderer->getAnimationController()) {
|
||||
ac->setStandState(rendering::AnimationController::STAND_STATE_SIT);
|
||||
}
|
||||
}
|
||||
});
|
||||
cc->setAutoFollowCancelCallback([this]() {
|
||||
if (gameHandler) {
|
||||
gameHandler->cancelFollow();
|
||||
|
|
@ -3488,13 +3498,17 @@ void Application::setupUICallbacks() {
|
|||
});
|
||||
|
||||
// Spell cast animation callback — play cast animation on caster (player or NPC/other player)
|
||||
// Probes the model for the best available spell animation with fallback chain:
|
||||
// Regular cast: SPELL_CAST_DIRECTED(53) → SPELL_CAST_OMNI(54) → SPELL_CAST(32) → SPELL(2)
|
||||
// Channel: CHANNEL_CAST_DIRECTED(124) → CHANNEL_CAST_OMNI(125) → SPELL_CAST_DIRECTED(53) → SPELL(2)
|
||||
// For the local player, uses AnimationController state machine to prevent
|
||||
// COMBAT_IDLE from overriding the spell animation. For NPCs/other players,
|
||||
// calls playAnimation directly (they don't share the player state machine).
|
||||
gameHandler->setSpellCastAnimCallback([this](uint64_t guid, bool start, bool isChannel) {
|
||||
// WoW-accurate 3-phase spell animation sequence:
|
||||
// Phase 1: SPELL_PRECAST (31) — one-shot wind-up
|
||||
// Phase 2: READY_SPELL_DIRECTED/OMNI (51/52) — looping hold while cast bar fills
|
||||
// Phase 3: SPELL_CAST_DIRECTED/OMNI/AREA (53/54/33) — one-shot release at completion
|
||||
// Channels use CHANNEL_CAST_DIRECTED/OMNI (124/125) or SPELL_CHANNEL_DIRECTED_OMNI (201).
|
||||
// castType comes from the spell packet's targetGuid:
|
||||
// DIRECTED — spell targets a specific unit (Frostbolt, Heal)
|
||||
// OMNI — self-cast / no explicit target (Arcane Explosion, buffs)
|
||||
// AREA — ground-targeted AoE (Blizzard, Rain of Fire)
|
||||
gameHandler->setSpellCastAnimCallback([this](uint64_t guid, bool start, bool isChannel,
|
||||
game::SpellCastType castType) {
|
||||
if (!entitySpawner_) return;
|
||||
if (!renderer) return;
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
|
|
@ -3514,6 +3528,9 @@ void Application::setupUICallbacks() {
|
|||
if (instanceId == 0) instanceId = entitySpawner_->getPlayerInstanceId(guid);
|
||||
if (instanceId == 0) return;
|
||||
|
||||
const bool isDirected = (castType == game::SpellCastType::DIRECTED);
|
||||
const bool isArea = (castType == game::SpellCastType::AREA);
|
||||
|
||||
if (start) {
|
||||
// Detect fishing spells (channeled) — use FISHING_LOOP instead of generic cast
|
||||
auto isFishingSpell = [](uint32_t spellId) {
|
||||
|
|
@ -3532,74 +3549,94 @@ void Application::setupUICallbacks() {
|
|||
cr->playAnimation(instanceId, rendering::anim::FISHING_LOOP, true);
|
||||
}
|
||||
} else {
|
||||
// Spell animation sequence: PRECAST (one-shot) → CAST (loop) → FINALIZE (one-shot) → idle
|
||||
// Probe model for best available animations with fallback chains:
|
||||
// Regular cast: SPELL_CAST_DIRECTED → SPELL_CAST_OMNI → SPELL_CAST → SPELL
|
||||
// Channel: CHANNEL_CAST_DIRECTED → CHANNEL_CAST_OMNI → SPELL_CAST_DIRECTED → SPELL
|
||||
bool hasTarget = gameHandler->hasTarget();
|
||||
// Helper: pick first animation the model supports from a list
|
||||
auto pickFirst = [&](std::initializer_list<uint32_t> ids) -> uint32_t {
|
||||
for (uint32_t id : ids)
|
||||
if (cr->hasAnimation(instanceId, id)) return id;
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Phase 1: Precast wind-up (one-shot, non-channels only)
|
||||
uint32_t precastAnim = 0;
|
||||
if (!isChannel && cr->hasAnimation(instanceId, rendering::anim::SPELL_PRECAST)) {
|
||||
precastAnim = rendering::anim::SPELL_PRECAST;
|
||||
if (!isChannel) {
|
||||
precastAnim = pickFirst({rendering::anim::SPELL_PRECAST});
|
||||
}
|
||||
|
||||
// Phase 2: Cast hold (looping until stopSpellCast)
|
||||
static const uint32_t castDirected[] = {
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
};
|
||||
static const uint32_t castOmni[] = {
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
};
|
||||
static const uint32_t channelDirected[] = {
|
||||
rendering::anim::CHANNEL_CAST_DIRECTED,
|
||||
rendering::anim::CHANNEL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL
|
||||
};
|
||||
static const uint32_t channelOmni[] = {
|
||||
rendering::anim::CHANNEL_CAST_OMNI,
|
||||
rendering::anim::CHANNEL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL
|
||||
};
|
||||
const uint32_t* chain;
|
||||
// Phase 2: Cast hold (looping while cast bar fills / channel active)
|
||||
uint32_t castAnim = 0;
|
||||
if (isChannel) {
|
||||
chain = hasTarget ? channelDirected : channelOmni;
|
||||
// Channel hold: prefer DIRECTED/OMNI based on spell target classification
|
||||
if (isDirected) {
|
||||
castAnim = pickFirst({
|
||||
rendering::anim::CHANNEL_CAST_DIRECTED,
|
||||
rendering::anim::CHANNEL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CHANNEL_DIRECTED_OMNI,
|
||||
rendering::anim::READY_SPELL_DIRECTED,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
} else {
|
||||
// OMNI or AREA channels (Blizzard channel, Tranquility, etc.)
|
||||
castAnim = pickFirst({
|
||||
rendering::anim::CHANNEL_CAST_OMNI,
|
||||
rendering::anim::CHANNEL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CHANNEL_DIRECTED_OMNI,
|
||||
rendering::anim::READY_SPELL_OMNI,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
}
|
||||
} else {
|
||||
chain = hasTarget ? castDirected : castOmni;
|
||||
}
|
||||
uint32_t castAnim = rendering::anim::SPELL;
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
if (cr->hasAnimation(instanceId, chain[i])) {
|
||||
castAnim = chain[i];
|
||||
break;
|
||||
// Regular cast hold: READY_SPELL_DIRECTED/OMNI while cast bar fills
|
||||
if (isDirected) {
|
||||
castAnim = pickFirst({
|
||||
rendering::anim::READY_SPELL_DIRECTED,
|
||||
rendering::anim::READY_SPELL_OMNI,
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
} else {
|
||||
// OMNI (self-buff) or AREA (AoE targeting)
|
||||
castAnim = pickFirst({
|
||||
rendering::anim::READY_SPELL_OMNI,
|
||||
rendering::anim::READY_SPELL_DIRECTED,
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
}
|
||||
}
|
||||
if (castAnim == 0) castAnim = rendering::anim::SPELL;
|
||||
|
||||
// Phase 3: Finalization release (one-shot after cast ends)
|
||||
// Pick a different animation from the cast loop for visual variety
|
||||
static const uint32_t finalizeChain[] = {
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
};
|
||||
// Phase 3: Finalization release (one-shot after cast completes)
|
||||
// Animation chosen by spell target type: AREA → SPELL_CAST_AREA,
|
||||
// DIRECTED → SPELL_CAST_DIRECTED, OMNI → SPELL_CAST_OMNI
|
||||
uint32_t finalizeAnim = 0;
|
||||
if (isLocalPlayer && !isChannel) {
|
||||
for (uint32_t fa : finalizeChain) {
|
||||
if (fa != castAnim && cr->hasAnimation(instanceId, fa)) {
|
||||
finalizeAnim = fa;
|
||||
break;
|
||||
}
|
||||
if (isArea) {
|
||||
// Ground-targeted AoE: SPELL_CAST_AREA → SPELL_CAST_OMNI
|
||||
finalizeAnim = pickFirst({
|
||||
rendering::anim::SPELL_CAST_AREA,
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
} else if (isDirected) {
|
||||
// Single-target: SPELL_CAST_DIRECTED → SPELL_CAST_OMNI
|
||||
finalizeAnim = pickFirst({
|
||||
rendering::anim::SPELL_CAST_DIRECTED,
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
} else {
|
||||
// OMNI (self-buff, Arcane Explosion): SPELL_CAST_OMNI → SPELL_CAST_AREA
|
||||
finalizeAnim = pickFirst({
|
||||
rendering::anim::SPELL_CAST_OMNI,
|
||||
rendering::anim::SPELL_CAST_AREA,
|
||||
rendering::anim::SPELL_CAST,
|
||||
rendering::anim::SPELL
|
||||
});
|
||||
}
|
||||
if (finalizeAnim == 0 && cr->hasAnimation(instanceId, rendering::anim::SPELL))
|
||||
finalizeAnim = rendering::anim::SPELL;
|
||||
}
|
||||
|
||||
if (isLocalPlayer) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
#include "audio/npc_voice_manager.hpp"
|
||||
#include "pipeline/m2_loader.hpp"
|
||||
#include "pipeline/wmo_loader.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
#include "core/world_loader.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "core/entity_spawner.hpp"
|
||||
#include "core/appearance_composer.hpp"
|
||||
#include "core/window.hpp"
|
||||
|
|
|
|||
|
|
@ -1070,7 +1070,7 @@ void CombatHandler::setTarget(uint64_t guid) {
|
|||
// Clear previous target's cast bar on target change
|
||||
// (the new target's cast state is naturally fetched from spellHandler_->unitCastStates_ by GUID)
|
||||
|
||||
// Inform server of target selection (Phase 1)
|
||||
// Inform server of target selection
|
||||
if (owner_.isInWorld()) {
|
||||
auto packet = SetSelectionPacket::build(guid);
|
||||
owner_.socket->send(packet);
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ void EntityController::processOutOfRangeObjects(const std::vector<uint64_t>& gui
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: Extracted helper methods
|
||||
// Extracted helper methods
|
||||
// ============================================================
|
||||
|
||||
bool EntityController::extractPlayerAppearance(const std::map<uint16_t, uint32_t>& fields,
|
||||
|
|
@ -383,7 +383,7 @@ void EntityController::maybeDetectCoinageIndex(const std::map<uint16_t, uint32_t
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Update type dispatch
|
||||
// Update type dispatch
|
||||
// ============================================================
|
||||
|
||||
void EntityController::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItemCreated) {
|
||||
|
|
@ -404,10 +404,10 @@ void EntityController::applyUpdateObjectBlock(const UpdateBlock& block, bool& ne
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Concern-specific helpers
|
||||
// Concern-specific helpers
|
||||
// ============================================================
|
||||
|
||||
// 3i: Non-player transport child attachment — identical in CREATE/VALUES/MOVEMENT
|
||||
// Non-player transport child attachment — identical in CREATE/VALUES/MOVEMENT
|
||||
void EntityController::updateNonPlayerTransportAttachment(const UpdateBlock& block,
|
||||
const std::shared_ptr<Entity>& entity,
|
||||
ObjectType entityType) {
|
||||
|
|
@ -430,7 +430,7 @@ void EntityController::updateNonPlayerTransportAttachment(const UpdateBlock& blo
|
|||
}
|
||||
}
|
||||
|
||||
// 3f: Rebuild playerAuras from UNIT_FIELD_AURAS (Classic/vanilla only).
|
||||
// Rebuild playerAuras from UNIT_FIELD_AURAS (Classic/vanilla only).
|
||||
// blockFields is used to check if any aura field was updated in this packet.
|
||||
// entity->getFields() is used for reading the full accumulated state.
|
||||
// Normalises Classic harmful bit (0x02) to WotLK debuff bit (0x80) so
|
||||
|
|
@ -481,7 +481,7 @@ void EntityController::syncClassicAurasFromFields(const std::shared_ptr<Entity>&
|
|||
pendingEvents_.emit("UNIT_AURA", {"player"});
|
||||
}
|
||||
|
||||
// 3h: Detect player mount/dismount from UNIT_FIELD_MOUNTDISPLAYID changes
|
||||
// Detect player mount/dismount from UNIT_FIELD_MOUNTDISPLAYID changes
|
||||
void EntityController::detectPlayerMountChange(uint32_t newMountDisplayId,
|
||||
const std::map<uint16_t, uint32_t>& blockFields) {
|
||||
uint32_t old = owner_.currentMountDisplayId_;
|
||||
|
|
@ -528,7 +528,7 @@ void EntityController::detectPlayerMountChange(uint32_t newMountDisplayId,
|
|||
}
|
||||
}
|
||||
|
||||
// Phase 4: Resolve cached field indices once per handler call.
|
||||
// Resolve cached field indices once per handler call.
|
||||
EntityController::UnitFieldIndices EntityController::UnitFieldIndices::resolve() {
|
||||
return UnitFieldIndices{
|
||||
fieldIndex(UF::UNIT_FIELD_HEALTH),
|
||||
|
|
@ -579,7 +579,7 @@ EntityController::PlayerFieldIndices EntityController::PlayerFieldIndices::resol
|
|||
};
|
||||
}
|
||||
|
||||
// 3a: Create the appropriate Entity subclass from the block's object type.
|
||||
// Create the appropriate Entity subclass from the block's object type.
|
||||
std::shared_ptr<Entity> EntityController::createEntityFromBlock(const UpdateBlock& block) {
|
||||
switch (block.objectType) {
|
||||
case ObjectType::PLAYER:
|
||||
|
|
@ -596,7 +596,7 @@ std::shared_ptr<Entity> EntityController::createEntityFromBlock(const UpdateBloc
|
|||
}
|
||||
}
|
||||
|
||||
// 3b: Track player-on-transport state from movement blocks.
|
||||
// Track player-on-transport state from movement blocks.
|
||||
// Consolidates near-identical logic from CREATE and MOVEMENT handlers.
|
||||
// When updateMovementInfoPos is true (MOVEMENT), movementInfo.x/y/z are set
|
||||
// to the raw canonical position when not on a resolved transport.
|
||||
|
|
@ -644,7 +644,7 @@ void EntityController::applyPlayerTransportState(const UpdateBlock& block,
|
|||
}
|
||||
}
|
||||
|
||||
// 3c: Apply unit fields during CREATE — sets health/power/level/flags/displayId/etc.
|
||||
// Apply unit fields during CREATE — sets health/power/level/flags/displayId/etc.
|
||||
// Returns true if the entity is initially dead (health=0 or DYNFLAG_DEAD).
|
||||
bool EntityController::applyUnitFieldsOnCreate(const UpdateBlock& block,
|
||||
std::shared_ptr<Unit>& unit,
|
||||
|
|
@ -928,7 +928,7 @@ EntityController::UnitFieldUpdateResult EntityController::applyUnitFieldsOnUpdat
|
|||
return result;
|
||||
}
|
||||
|
||||
// 3d: Apply player stat fields (XP, coinage, combat stats, etc.).
|
||||
// Apply player stat fields (XP, coinage, combat stats, etc.).
|
||||
// Shared between CREATE and VALUES — isCreate controls event firing differences.
|
||||
bool EntityController::applyPlayerStatFields(const std::map<uint16_t, uint32_t>& fields,
|
||||
const PlayerFieldIndices& pfi,
|
||||
|
|
@ -1081,7 +1081,7 @@ bool EntityController::applyPlayerStatFields(const std::map<uint16_t, uint32_t>&
|
|||
return slotsChanged;
|
||||
}
|
||||
|
||||
// 3e: Dispatch entity spawn callbacks for units/players.
|
||||
// Dispatch entity spawn callbacks for units/players.
|
||||
// Consolidates player/creature spawn callback invocation from CREATE and VALUES handlers.
|
||||
// isDead = unitInitiallyDead (CREATE) or computed isDeadNow && !npcDeathNotified (VALUES).
|
||||
void EntityController::dispatchEntitySpawn(uint64_t guid, ObjectType objectType,
|
||||
|
|
@ -1133,7 +1133,7 @@ void EntityController::dispatchEntitySpawn(uint64_t guid, ObjectType objectType,
|
|||
}
|
||||
}
|
||||
|
||||
// 3g: Track online item/container objects during CREATE.
|
||||
// Track online item/container objects during CREATE.
|
||||
void EntityController::trackItemOnCreate(const UpdateBlock& block, bool& newItemCreated) {
|
||||
auto entryIt = block.fields.find(fieldIndex(UF::OBJECT_FIELD_ENTRY));
|
||||
auto stackIt = block.fields.find(fieldIndex(UF::ITEM_FIELD_STACK_COUNT));
|
||||
|
|
@ -1169,7 +1169,7 @@ void EntityController::trackItemOnCreate(const UpdateBlock& block, bool& newItem
|
|||
}
|
||||
}
|
||||
|
||||
// 3g: Update item stack count / durability / enchants for existing items during VALUES.
|
||||
// Update item stack count / durability / enchants for existing items during VALUES.
|
||||
void EntityController::updateItemOnValuesUpdate(const UpdateBlock& block,
|
||||
const std::shared_ptr<Entity>& entity) {
|
||||
bool inventoryChanged = false;
|
||||
|
|
@ -1275,7 +1275,7 @@ void EntityController::updateItemOnValuesUpdate(const UpdateBlock& block,
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Object-type handler struct definitions
|
||||
// Object-type handler struct definitions
|
||||
// ============================================================
|
||||
|
||||
struct EntityController::UnitTypeHandler : EntityController::IObjectTypeHandler {
|
||||
|
|
@ -1313,7 +1313,7 @@ struct EntityController::CorpseTypeHandler : EntityController::IObjectTypeHandle
|
|||
};
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Handler registry infrastructure
|
||||
// Handler registry infrastructure
|
||||
// ============================================================
|
||||
|
||||
void EntityController::initTypeHandlers() {
|
||||
|
|
@ -1331,7 +1331,7 @@ EntityController::IObjectTypeHandler* EntityController::getTypeHandler(ObjectTyp
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 6: Deferred event bus flush
|
||||
// Deferred event bus flush
|
||||
// ============================================================
|
||||
|
||||
void EntityController::flushPendingEvents() {
|
||||
|
|
@ -1342,7 +1342,7 @@ void EntityController::flushPendingEvents() {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Type-specific CREATE handlers
|
||||
// Type-specific CREATE handlers
|
||||
// ============================================================
|
||||
|
||||
void EntityController::onCreateUnit(const UpdateBlock& block, std::shared_ptr<Entity>& entity) {
|
||||
|
|
@ -1432,7 +1432,7 @@ void EntityController::onCreatePlayer(const UpdateBlock& block, std::shared_ptr<
|
|||
}
|
||||
}
|
||||
}
|
||||
// 3f: Classic aura sync on initial object create
|
||||
// Classic aura sync on initial object create
|
||||
if (block.guid == owner_.playerGuid) {
|
||||
syncClassicAurasFromFields(entity);
|
||||
}
|
||||
|
|
@ -1449,7 +1449,7 @@ void EntityController::onCreatePlayer(const UpdateBlock& block, std::shared_ptr<
|
|||
}
|
||||
}
|
||||
|
||||
// 3d: Player stat fields (self only)
|
||||
// Player stat fields (self only)
|
||||
if (block.guid == owner_.playerGuid) {
|
||||
// Auto-detect coinage index using the previous snapshot vs this full snapshot.
|
||||
maybeDetectCoinageIndex(owner_.lastPlayerFields_, block.fields);
|
||||
|
|
@ -1557,7 +1557,7 @@ void EntityController::onCreateCorpse(const UpdateBlock& block) {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Type-specific VALUES UPDATE handlers
|
||||
// Type-specific VALUES UPDATE handlers
|
||||
// ============================================================
|
||||
|
||||
void EntityController::handleDisplayIdChange(const UpdateBlock& block,
|
||||
|
|
@ -1602,15 +1602,15 @@ void EntityController::onValuesUpdatePlayer(const UpdateBlock& block, std::share
|
|||
UnitFieldIndices ufi = UnitFieldIndices::resolve();
|
||||
UnitFieldUpdateResult result = applyUnitFieldsOnUpdate(block, entity, unit, ufi);
|
||||
|
||||
// 3f: Classic aura sync from UNIT_FIELD_AURAS when those fields are updated
|
||||
// Classic aura sync from UNIT_FIELD_AURAS when those fields are updated
|
||||
if (block.guid == owner_.playerGuid) {
|
||||
syncClassicAurasFromFields(entity);
|
||||
}
|
||||
|
||||
// 3e: Display ID changed — re-spawn/model-change
|
||||
// Display ID changed — re-spawn/model-change
|
||||
handleDisplayIdChange(block, entity, unit, result);
|
||||
|
||||
// 3d: Self-player stat/inventory/quest field updates
|
||||
// Self-player stat/inventory/quest field updates
|
||||
if (block.guid == owner_.playerGuid) {
|
||||
const bool needCoinageDetectSnapshot =
|
||||
(owner_.pendingMoneyDelta_ != 0 && owner_.pendingMoneyDeltaTimer_ > 0.0f);
|
||||
|
|
@ -1682,7 +1682,7 @@ void EntityController::onValuesUpdateGameObject(const UpdateBlock& block, std::s
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Update type handlers (refactored with Phase 5 dispatch)
|
||||
// Update type handlers
|
||||
// ============================================================
|
||||
|
||||
void EntityController::handleCreateObject(const UpdateBlock& block, bool& newItemCreated) {
|
||||
|
|
@ -1716,7 +1716,7 @@ void EntityController::handleCreateObject(const UpdateBlock& block, bool& newIte
|
|||
// Add to manager
|
||||
entityManager.addEntity(block.guid, entity);
|
||||
|
||||
// Phase 5: Dispatch to type-specific handler
|
||||
// Dispatch to type-specific handler
|
||||
auto* handler = getTypeHandler(block.objectType);
|
||||
if (handler) handler->onCreate(block, entity, newItemCreated);
|
||||
|
||||
|
|
@ -1741,7 +1741,7 @@ void EntityController::handleValuesUpdate(const UpdateBlock& block) {
|
|||
entity->setField(field.first, field.second);
|
||||
}
|
||||
|
||||
// Phase 5: Dispatch to type-specific handler
|
||||
// Dispatch to type-specific handler
|
||||
auto* handler = getTypeHandler(entity->getType());
|
||||
if (handler) handler->onValuesUpdate(block, entity);
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
|
@ -5145,7 +5145,7 @@ const std::vector<std::string>& GameHandler::getJoinedChannels() const {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 1: Name Queries (delegated to EntityController)
|
||||
// Name Queries (delegated to EntityController)
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::queryPlayerName(uint64_t guid) {
|
||||
|
|
@ -5217,7 +5217,7 @@ void GameHandler::emitAllOtherPlayerEquipment() {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 2: Combat (delegated to CombatHandler)
|
||||
// Combat (delegated to CombatHandler)
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::startAutoAttack(uint64_t targetGuid) {
|
||||
|
|
@ -5372,7 +5372,7 @@ void GameHandler::requestPvpLog() {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 3: Spells
|
||||
// Spells
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
|
||||
|
|
@ -5489,7 +5489,7 @@ void GameHandler::sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, ui
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 4: Group/Party
|
||||
// Group/Party
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::inviteToGroup(const std::string& playerName) {
|
||||
|
|
@ -5606,7 +5606,7 @@ void GameHandler::turnInPetition(uint64_t petitionGuid) {
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// Phase 5: Loot, Gossip, Vendor
|
||||
// Loot, Gossip, Vendor
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::lootTarget(uint64_t guid) {
|
||||
|
|
|
|||
|
|
@ -845,7 +845,19 @@ void SpellHandler::handleSpellStart(network::Packet& packet) {
|
|||
return;
|
||||
}
|
||||
LOG_DEBUG("SMSG_SPELL_START: caster=0x", std::hex, data.casterUnit, std::dec,
|
||||
" spell=", data.spellId, " castTime=", data.castTime);
|
||||
" spell=", data.spellId, " castTime=", data.castTime,
|
||||
" target=0x", std::hex, data.targetGuid, std::dec);
|
||||
|
||||
// Classify spell targeting for animation selection:
|
||||
// DIRECTED — targets a specific other unit (Frostbolt, Heal)
|
||||
// OMNI — self-cast or no explicit target (Arcane Explosion, buffs)
|
||||
// AREA — ground-targeted AoE with no unit target (Blizzard, Rain of Fire)
|
||||
auto classifyCast = [](uint64_t targetGuid, uint64_t casterGuid) -> SpellCastType {
|
||||
if (targetGuid == 0) return SpellCastType::AREA;
|
||||
if (targetGuid == casterGuid) return SpellCastType::OMNI;
|
||||
return SpellCastType::DIRECTED;
|
||||
};
|
||||
const SpellCastType castType = classifyCast(data.targetGuid, data.casterUnit);
|
||||
|
||||
// Track cast bar for any non-player caster
|
||||
if (data.casterUnit != owner_.playerGuid && data.castTime > 0) {
|
||||
|
|
@ -856,8 +868,9 @@ void SpellHandler::handleSpellStart(network::Packet& packet) {
|
|||
s.timeTotal = data.castTime / 1000.0f;
|
||||
s.timeRemaining = s.timeTotal;
|
||||
s.interruptible = owner_.isSpellInterruptible(data.spellId);
|
||||
s.castType = castType;
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, true, false);
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, true, false, castType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -891,7 +904,7 @@ void SpellHandler::handleSpellStart(network::Packet& packet) {
|
|||
}
|
||||
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, true, false);
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, true, false, castType);
|
||||
}
|
||||
|
||||
// Hearthstone: pre-load terrain at bind point
|
||||
|
|
@ -954,8 +967,14 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
// Instant spell cast animation — if this wasn't a timed cast and isn't a
|
||||
// melee ability, play a brief spell cast animation (one-shot)
|
||||
if (!wasInTimedCast && !isMeleeAbility && !owner_.isProfessionSpell(data.spellId)) {
|
||||
// Classify instant spell from SPELL_GO packet target info
|
||||
SpellCastType goType = SpellCastType::OMNI;
|
||||
if (data.targetGuid != 0 && data.targetGuid != data.casterUnit)
|
||||
goType = SpellCastType::DIRECTED;
|
||||
else if (data.targetGuid == 0 && data.hitCount > 1)
|
||||
goType = SpellCastType::AREA;
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, true, false);
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, true, false, goType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -983,7 +1002,7 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
owner_.pendingGameObjectInteractGuid_ = 0;
|
||||
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, false, false);
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, false, false, SpellCastType::OMNI);
|
||||
}
|
||||
|
||||
if (owner_.addonEventCallback_)
|
||||
|
|
@ -1003,11 +1022,17 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
|
|||
// instant cast — play a brief one-shot spell animation before stopping
|
||||
auto castIt = unitCastStates_.find(data.casterUnit);
|
||||
bool wasTrackedCast = (castIt != unitCastStates_.end());
|
||||
// Classify NPC instant spell from SPELL_GO target info
|
||||
SpellCastType npcGoType = SpellCastType::OMNI;
|
||||
if (data.targetGuid != 0 && data.targetGuid != data.casterUnit)
|
||||
npcGoType = SpellCastType::DIRECTED;
|
||||
else if (data.targetGuid == 0 && data.hitCount > 1)
|
||||
npcGoType = SpellCastType::AREA;
|
||||
if (!wasTrackedCast && owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, true, false);
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, true, false, npcGoType);
|
||||
}
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, false, false);
|
||||
owner_.spellCastAnimCallback_(data.casterUnit, false, false, SpellCastType::OMNI);
|
||||
}
|
||||
bool targetsPlayer = false;
|
||||
for (const auto& tgt : data.hitTargets) {
|
||||
|
|
@ -2400,13 +2425,13 @@ void SpellHandler::handleSpellFailure(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, false, false);
|
||||
owner_.spellCastAnimCallback_(owner_.playerGuid, false, false, SpellCastType::OMNI);
|
||||
}
|
||||
} else {
|
||||
// Another unit's cast failed — clear their tracked cast bar
|
||||
unitCastStates_.erase(failGuid);
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(failGuid, false, false);
|
||||
owner_.spellCastAnimCallback_(failGuid, false, false, SpellCastType::OMNI);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3219,8 +3244,12 @@ void SpellHandler::handleChannelStart(network::Packet& packet) {
|
|||
" spell=", chanSpellId, " total=", chanTotalMs, "ms");
|
||||
|
||||
// Play channeling animation (looping)
|
||||
// Channel packets don't carry targetGuid — use player's current target as hint
|
||||
SpellCastType chanType = SpellCastType::OMNI;
|
||||
if (chanCaster == owner_.playerGuid && owner_.targetGuid != 0)
|
||||
chanType = SpellCastType::DIRECTED;
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(chanCaster, true, true);
|
||||
owner_.spellCastAnimCallback_(chanCaster, true, true, chanType);
|
||||
}
|
||||
|
||||
// Fire UNIT_SPELLCAST_CHANNEL_START for Lua addons
|
||||
|
|
@ -3260,7 +3289,7 @@ void SpellHandler::handleChannelUpdate(network::Packet& packet) {
|
|||
if (chanRemainMs == 0) {
|
||||
// Stop channeling animation — return to idle
|
||||
if (owner_.spellCastAnimCallback_) {
|
||||
owner_.spellCastAnimCallback_(chanCaster2, false, true);
|
||||
owner_.spellCastAnimCallback_(chanCaster2, false, true, SpellCastType::OMNI);
|
||||
}
|
||||
auto unitId = owner_.guidToUnitId(chanCaster2);
|
||||
if (!unitId.empty())
|
||||
|
|
|
|||
339
src/rendering/animation/activity_fsm.cpp
Normal file
339
src/rendering/animation/activity_fsm.cpp
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
#include "rendering/animation/activity_fsm.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ── One-shot completion helper ───────────────────────────────────────────────
|
||||
|
||||
bool ActivityFSM::oneShotComplete(const Input& in, uint32_t expectedAnimId) const {
|
||||
if (!in.haveAnimState) return false;
|
||||
return in.currentAnimId != expectedAnimId ||
|
||||
(in.currentAnimDuration > 0.1f && in.currentAnimTime >= in.currentAnimDuration - 0.05f);
|
||||
}
|
||||
|
||||
// ── Event handling ───────────────────────────────────────────────────────────
|
||||
|
||||
void ActivityFSM::onEvent(AnimEvent event) {
|
||||
switch (event) {
|
||||
case AnimEvent::EMOTE_START:
|
||||
// Handled by startEmote() with animId
|
||||
break;
|
||||
case AnimEvent::EMOTE_STOP:
|
||||
cancelEmote();
|
||||
break;
|
||||
case AnimEvent::LOOT_START:
|
||||
startLooting();
|
||||
break;
|
||||
case AnimEvent::LOOT_STOP:
|
||||
stopLooting();
|
||||
break;
|
||||
case AnimEvent::SIT:
|
||||
// Handled by setStandState()
|
||||
break;
|
||||
case AnimEvent::STAND_UP:
|
||||
if (state_ == State::SITTING || state_ == State::SIT_DOWN) {
|
||||
if (sitUpAnim_ != 0)
|
||||
state_ = State::SIT_UP;
|
||||
else
|
||||
state_ = State::NONE;
|
||||
}
|
||||
break;
|
||||
case AnimEvent::MOVE_START:
|
||||
case AnimEvent::JUMP:
|
||||
case AnimEvent::SWIM_ENTER:
|
||||
// Movement cancels all activities
|
||||
if (state_ == State::EMOTE) cancelEmote();
|
||||
if (state_ == State::LOOTING || state_ == State::LOOT_KNEELING || state_ == State::LOOT_END)
|
||||
state_ = State::NONE;
|
||||
if (state_ == State::SIT_UP || state_ == State::SIT_DOWN) {
|
||||
state_ = State::NONE;
|
||||
standState_ = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Emote management ─────────────────────────────────────────────────────────
|
||||
|
||||
void ActivityFSM::startEmote(uint32_t animId, bool loop) {
|
||||
emoteActive_ = true;
|
||||
emoteAnimId_ = animId;
|
||||
emoteLoop_ = loop;
|
||||
state_ = State::EMOTE;
|
||||
}
|
||||
|
||||
void ActivityFSM::cancelEmote() {
|
||||
emoteActive_ = false;
|
||||
emoteAnimId_ = 0;
|
||||
emoteLoop_ = false;
|
||||
if (state_ == State::EMOTE) state_ = State::NONE;
|
||||
}
|
||||
|
||||
// ── Sit/sleep/kneel ──────────────────────────────────────────────────────────
|
||||
|
||||
void ActivityFSM::setStandState(uint8_t standState) {
|
||||
if (standState == standState_) return;
|
||||
standState_ = standState;
|
||||
|
||||
if (standState == STAND_STATE_STAND) {
|
||||
// Standing up — exit via SIT_UP if we have an exit animation
|
||||
return;
|
||||
}
|
||||
|
||||
if (standState == STAND_STATE_SIT) {
|
||||
sitDownAnim_ = anim::SIT_GROUND_DOWN;
|
||||
sitLoopAnim_ = anim::SITTING;
|
||||
sitUpAnim_ = anim::SIT_GROUND_UP;
|
||||
sitDownAnimSeen_ = false;
|
||||
sitDownFrames_ = 0;
|
||||
state_ = State::SIT_DOWN;
|
||||
} else if (standState == STAND_STATE_SLEEP) {
|
||||
sitDownAnim_ = anim::SLEEP_DOWN;
|
||||
sitLoopAnim_ = anim::SLEEP;
|
||||
sitUpAnim_ = anim::SLEEP_UP;
|
||||
sitDownAnimSeen_ = false;
|
||||
sitDownFrames_ = 0;
|
||||
state_ = State::SIT_DOWN;
|
||||
} else if (standState == STAND_STATE_KNEEL) {
|
||||
sitDownAnim_ = anim::KNEEL_START;
|
||||
sitLoopAnim_ = anim::KNEEL_LOOP;
|
||||
sitUpAnim_ = anim::KNEEL_END;
|
||||
sitDownAnimSeen_ = false;
|
||||
sitDownFrames_ = 0;
|
||||
state_ = State::SIT_DOWN;
|
||||
} else if (standState >= STAND_STATE_SIT_CHAIR && standState <= STAND_STATE_SIT_HIGH) {
|
||||
// Chair variants — no transition animation, go directly to loop
|
||||
sitDownAnim_ = 0;
|
||||
sitUpAnim_ = 0;
|
||||
if (standState == STAND_STATE_SIT_LOW) {
|
||||
sitLoopAnim_ = anim::SIT_CHAIR_LOW;
|
||||
} else if (standState == STAND_STATE_SIT_HIGH) {
|
||||
sitLoopAnim_ = anim::SIT_CHAIR_HIGH;
|
||||
} else {
|
||||
sitLoopAnim_ = anim::SIT_CHAIR_MED;
|
||||
}
|
||||
state_ = State::SITTING;
|
||||
} else if (standState == STAND_STATE_DEAD) {
|
||||
sitDownAnim_ = 0;
|
||||
sitLoopAnim_ = 0;
|
||||
sitUpAnim_ = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Loot management ──────────────────────────────────────────────────────────
|
||||
|
||||
void ActivityFSM::startLooting() {
|
||||
state_ = State::LOOTING;
|
||||
lootAnimSeen_ = false;
|
||||
lootFrames_ = 0;
|
||||
}
|
||||
|
||||
void ActivityFSM::stopLooting() {
|
||||
if (state_ == State::LOOTING || state_ == State::LOOT_KNEELING) {
|
||||
state_ = State::LOOT_END;
|
||||
lootEndAnimSeen_ = false;
|
||||
lootEndFrames_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── State transitions ────────────────────────────────────────────────────────
|
||||
|
||||
void ActivityFSM::updateTransitions(const Input& in) {
|
||||
switch (state_) {
|
||||
case State::NONE:
|
||||
break;
|
||||
|
||||
case State::EMOTE:
|
||||
if (in.swimming || (in.jumping && !in.grounded) || in.moving || in.sitting) {
|
||||
cancelEmote();
|
||||
} else if (!emoteLoop_ && in.haveAnimState) {
|
||||
if (oneShotComplete(in, emoteAnimId_)) {
|
||||
cancelEmote();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case State::LOOTING:
|
||||
if (in.swimming || (in.jumping && !in.grounded) || in.moving) {
|
||||
state_ = State::NONE;
|
||||
} else if (in.haveAnimState) {
|
||||
if (in.currentAnimId == anim::LOOT) lootAnimSeen_ = true;
|
||||
if (lootAnimSeen_ && oneShotComplete(in, anim::LOOT)) {
|
||||
state_ = State::LOOT_KNEELING;
|
||||
}
|
||||
// Safety: if anim never seen (model lacks it), advance after a timeout
|
||||
if (!lootAnimSeen_ && ++lootFrames_ > 10) {
|
||||
state_ = State::LOOT_KNEELING;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case State::LOOT_KNEELING:
|
||||
if (in.swimming || (in.jumping && !in.grounded) || in.moving) {
|
||||
state_ = State::NONE;
|
||||
}
|
||||
// Stays in LOOT_KNEELING until stopLooting() transitions to LOOT_END
|
||||
break;
|
||||
|
||||
case State::LOOT_END:
|
||||
if (in.swimming || (in.jumping && !in.grounded) || in.moving) {
|
||||
state_ = State::NONE;
|
||||
} else if (in.haveAnimState) {
|
||||
if (in.currentAnimId == anim::KNEEL_END) lootEndAnimSeen_ = true;
|
||||
if (lootEndAnimSeen_ && oneShotComplete(in, anim::KNEEL_END)) {
|
||||
state_ = State::NONE;
|
||||
}
|
||||
// Safety timeout
|
||||
if (!lootEndAnimSeen_ && ++lootEndFrames_ > 10) {
|
||||
state_ = State::NONE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case State::SIT_DOWN:
|
||||
if (in.swimming) {
|
||||
state_ = State::NONE;
|
||||
standState_ = 0;
|
||||
} else if (!in.sitting) {
|
||||
// Stand up requested
|
||||
if (sitUpAnim_ != 0 && !in.moving) {
|
||||
sitUpAnimSeen_ = false;
|
||||
sitUpFrames_ = 0;
|
||||
state_ = State::SIT_UP;
|
||||
} else {
|
||||
state_ = State::NONE;
|
||||
standState_ = 0;
|
||||
}
|
||||
} else if (sitDownAnim_ != 0 && in.haveAnimState) {
|
||||
// Track whether the sit-down anim has started playing
|
||||
if (in.currentAnimId == sitDownAnim_) sitDownAnimSeen_ = true;
|
||||
// Only detect completion after the anim has been seen at least once
|
||||
if (sitDownAnimSeen_ && oneShotComplete(in, sitDownAnim_)) {
|
||||
state_ = State::SITTING;
|
||||
}
|
||||
// Safety: if animation was never seen after enough frames (model
|
||||
// may lack it), fall through to the sitting loop.
|
||||
if (!sitDownAnimSeen_ && ++sitDownFrames_ > 10) {
|
||||
state_ = State::SITTING;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case State::SITTING:
|
||||
if (in.swimming) {
|
||||
state_ = State::NONE;
|
||||
standState_ = 0;
|
||||
} else if (!in.sitting) {
|
||||
if (sitUpAnim_ != 0 && !in.moving) {
|
||||
sitUpAnimSeen_ = false;
|
||||
sitUpFrames_ = 0;
|
||||
state_ = State::SIT_UP;
|
||||
} else {
|
||||
state_ = State::NONE;
|
||||
standState_ = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case State::SIT_UP:
|
||||
if (in.swimming || in.moving) {
|
||||
state_ = State::NONE;
|
||||
standState_ = 0;
|
||||
} else if (in.haveAnimState) {
|
||||
uint32_t expected = sitUpAnim_ ? sitUpAnim_ : anim::SIT_GROUND_UP;
|
||||
// Track whether the sit-up anim has started playing
|
||||
if (in.currentAnimId == expected) sitUpAnimSeen_ = true;
|
||||
// Only detect completion after the anim has been seen at least once
|
||||
if (sitUpAnimSeen_ && oneShotComplete(in, expected)) {
|
||||
state_ = State::NONE;
|
||||
standState_ = 0;
|
||||
}
|
||||
// Safety: if animation was never seen after enough frames, finish
|
||||
if (!sitUpAnimSeen_ && ++sitUpFrames_ > 10) {
|
||||
state_ = State::NONE;
|
||||
standState_ = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Animation resolution ─────────────────────────────────────────────────────
|
||||
|
||||
AnimOutput ActivityFSM::resolve(const Input& in, const AnimCapabilitySet& /*caps*/) {
|
||||
updateTransitions(in);
|
||||
|
||||
if (state_ == State::NONE) return AnimOutput::stay();
|
||||
|
||||
uint32_t animId = 0;
|
||||
bool loop = true;
|
||||
|
||||
switch (state_) {
|
||||
case State::NONE:
|
||||
return AnimOutput::stay();
|
||||
|
||||
case State::EMOTE:
|
||||
animId = emoteAnimId_;
|
||||
loop = emoteLoop_;
|
||||
break;
|
||||
|
||||
case State::LOOTING:
|
||||
animId = anim::LOOT;
|
||||
loop = false;
|
||||
break;
|
||||
|
||||
case State::LOOT_KNEELING:
|
||||
animId = anim::KNEEL_LOOP;
|
||||
loop = true;
|
||||
break;
|
||||
|
||||
case State::LOOT_END:
|
||||
animId = anim::KNEEL_END;
|
||||
loop = false;
|
||||
break;
|
||||
|
||||
case State::SIT_DOWN:
|
||||
animId = sitDownAnim_ ? sitDownAnim_ : anim::SIT_GROUND_DOWN;
|
||||
loop = false;
|
||||
break;
|
||||
|
||||
case State::SITTING:
|
||||
animId = sitLoopAnim_ ? sitLoopAnim_ : anim::SITTING;
|
||||
loop = true;
|
||||
break;
|
||||
|
||||
case State::SIT_UP:
|
||||
animId = sitUpAnim_ ? sitUpAnim_ : anim::SIT_GROUND_UP;
|
||||
loop = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (animId == 0) return AnimOutput::stay();
|
||||
return AnimOutput::ok(animId, loop);
|
||||
}
|
||||
|
||||
// ── Reset ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void ActivityFSM::reset() {
|
||||
state_ = State::NONE;
|
||||
cancelEmote();
|
||||
standState_ = 0;
|
||||
sitDownAnim_ = 0;
|
||||
sitLoopAnim_ = 0;
|
||||
sitUpAnim_ = 0;
|
||||
sitDownAnimSeen_ = false;
|
||||
sitUpAnimSeen_ = false;
|
||||
sitDownFrames_ = 0;
|
||||
sitUpFrames_ = 0;
|
||||
lootAnimSeen_ = false;
|
||||
lootFrames_ = 0;
|
||||
lootEndAnimSeen_ = false;
|
||||
lootEndFrames_ = 0;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
287
src/rendering/animation/anim_capability_probe.cpp
Normal file
287
src/rendering/animation/anim_capability_probe.cpp
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
// ============================================================================
|
||||
// AnimCapabilityProbe
|
||||
//
|
||||
// Scans a model's animation capabilities once and returns an
|
||||
// AnimCapabilitySet with resolved IDs (after fallback chains).
|
||||
// Extracted from the scattered hasAnimation/pickFirstAvailable calls
|
||||
// in AnimationController.
|
||||
// ============================================================================
|
||||
|
||||
#include "rendering/animation/anim_capability_probe.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/m2_renderer.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
uint32_t AnimCapabilityProbe::pickFirst(Renderer* renderer, uint32_t instanceId,
|
||||
const uint32_t* candidates, size_t count) {
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
if (!charRenderer) return 0;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (charRenderer->hasAnimation(instanceId, candidates[i])) {
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
AnimCapabilitySet AnimCapabilityProbe::probe(Renderer* renderer, uint32_t instanceId) {
|
||||
AnimCapabilitySet caps;
|
||||
if (!renderer || instanceId == 0) return caps;
|
||||
|
||||
auto* cr = renderer->getCharacterRenderer();
|
||||
if (!cr) return caps;
|
||||
|
||||
auto has = [&](uint32_t id) -> bool {
|
||||
return cr->hasAnimation(instanceId, id);
|
||||
};
|
||||
|
||||
// Helper: pick first available from static array
|
||||
auto pick = [&](const uint32_t* candidates, size_t count) -> uint32_t {
|
||||
return pickFirst(renderer, instanceId, candidates, count);
|
||||
};
|
||||
|
||||
// ── Locomotion ──────────────────────────────────────────────────────
|
||||
// STAND is animation ID 0. Every M2 model has sequence 0 as its base idle.
|
||||
// Cannot use ternary `has(0) ? 0 : 0` — both branches are 0.
|
||||
caps.resolvedStand = anim::STAND;
|
||||
caps.hasStand = has(anim::STAND);
|
||||
|
||||
{
|
||||
static const uint32_t walkCands[] = {anim::WALK, anim::RUN};
|
||||
caps.resolvedWalk = pick(walkCands, 2);
|
||||
caps.hasWalk = (caps.resolvedWalk != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t runCands[] = {anim::RUN, anim::WALK};
|
||||
caps.resolvedRun = pick(runCands, 2);
|
||||
caps.hasRun = (caps.resolvedRun != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t sprintCands[] = {anim::SPRINT, anim::RUN, anim::WALK};
|
||||
caps.resolvedSprint = pick(sprintCands, 3);
|
||||
caps.hasSprint = (caps.resolvedSprint != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t walkBackCands[] = {anim::WALK_BACKWARDS, anim::WALK};
|
||||
caps.resolvedWalkBackwards = pick(walkBackCands, 2);
|
||||
caps.hasWalkBackwards = (caps.resolvedWalkBackwards != 0);
|
||||
}
|
||||
{
|
||||
static const uint32_t strafeLeftCands[] = {anim::SHUFFLE_LEFT, anim::RUN_LEFT, anim::WALK};
|
||||
caps.resolvedStrafeLeft = pick(strafeLeftCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t strafeRightCands[] = {anim::SHUFFLE_RIGHT, anim::RUN_RIGHT, anim::WALK};
|
||||
caps.resolvedStrafeRight = pick(strafeRightCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t runLeftCands[] = {anim::RUN_LEFT, anim::RUN};
|
||||
caps.resolvedRunLeft = pick(runLeftCands, 2);
|
||||
}
|
||||
{
|
||||
static const uint32_t runRightCands[] = {anim::RUN_RIGHT, anim::RUN};
|
||||
caps.resolvedRunRight = pick(runRightCands, 2);
|
||||
}
|
||||
|
||||
// ── Jump ────────────────────────────────────────────────────────────
|
||||
caps.resolvedJumpStart = has(anim::JUMP_START) ? anim::JUMP_START : 0;
|
||||
caps.resolvedJump = has(anim::JUMP) ? anim::JUMP : 0;
|
||||
caps.resolvedJumpEnd = has(anim::JUMP_END) ? anim::JUMP_END : 0;
|
||||
caps.hasJump = (caps.resolvedJumpStart != 0);
|
||||
|
||||
// ── Swim ────────────────────────────────────────────────────────────
|
||||
caps.resolvedSwimIdle = has(anim::SWIM_IDLE) ? anim::SWIM_IDLE : 0;
|
||||
caps.resolvedSwim = has(anim::SWIM) ? anim::SWIM : 0;
|
||||
caps.hasSwim = (caps.resolvedSwimIdle != 0 || caps.resolvedSwim != 0);
|
||||
{
|
||||
static const uint32_t swimBackCands[] = {anim::SWIM_BACKWARDS, anim::SWIM};
|
||||
caps.resolvedSwimBackwards = pick(swimBackCands, 2);
|
||||
}
|
||||
{
|
||||
static const uint32_t swimLeftCands[] = {anim::SWIM_LEFT, anim::SWIM};
|
||||
caps.resolvedSwimLeft = pick(swimLeftCands, 2);
|
||||
}
|
||||
{
|
||||
static const uint32_t swimRightCands[] = {anim::SWIM_RIGHT, anim::SWIM};
|
||||
caps.resolvedSwimRight = pick(swimRightCands, 2);
|
||||
}
|
||||
|
||||
// ── Melee combat (fallback chains match resolveMeleeAnimId) ─────────
|
||||
{
|
||||
static const uint32_t melee1HCands[] = {
|
||||
anim::ATTACK_1H, anim::ATTACK_2H, anim::ATTACK_UNARMED,
|
||||
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
|
||||
caps.resolvedMelee1H = pick(melee1HCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t melee2HCands[] = {
|
||||
anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED,
|
||||
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
|
||||
caps.resolvedMelee2H = pick(melee2HCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t melee2HLooseCands[] = {
|
||||
anim::ATTACK_2H_LOOSE_PIERCE, anim::ATTACK_2H_LOOSE,
|
||||
anim::ATTACK_2H, anim::ATTACK_1H, anim::ATTACK_UNARMED};
|
||||
caps.resolvedMelee2HLoose = pick(melee2HLooseCands, 5);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeUnarmedCands[] = {
|
||||
anim::ATTACK_UNARMED, anim::ATTACK_1H, anim::ATTACK_2H,
|
||||
anim::ATTACK_2H_LOOSE, anim::PARRY_UNARMED, anim::PARRY_1H};
|
||||
caps.resolvedMeleeUnarmed = pick(meleeUnarmedCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeFistCands[] = {
|
||||
anim::ATTACK_FIST_1H, anim::ATTACK_FIST_1H_OFF,
|
||||
anim::ATTACK_1H, anim::ATTACK_UNARMED,
|
||||
anim::PARRY_FIST_1H, anim::PARRY_1H};
|
||||
caps.resolvedMeleeFist = pick(meleeFistCands, 6);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleePierceCands[] = {
|
||||
anim::ATTACK_1H_PIERCE, anim::ATTACK_1H, anim::ATTACK_UNARMED};
|
||||
caps.resolvedMeleePierce = pick(meleePierceCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffCands[] = {
|
||||
anim::ATTACK_OFF, anim::ATTACK_1H, anim::ATTACK_UNARMED};
|
||||
caps.resolvedMeleeOffHand = pick(meleeOffCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffFistCands[] = {
|
||||
anim::ATTACK_FIST_1H_OFF, anim::ATTACK_OFF,
|
||||
anim::ATTACK_FIST_1H, anim::ATTACK_1H};
|
||||
caps.resolvedMeleeOffHandFist = pick(meleeOffFistCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffPierceCands[] = {
|
||||
anim::ATTACK_OFF_PIERCE, anim::ATTACK_OFF,
|
||||
anim::ATTACK_1H_PIERCE, anim::ATTACK_1H};
|
||||
caps.resolvedMeleeOffHandPierce = pick(meleeOffPierceCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t meleeOffUnarmedCands[] = {
|
||||
anim::ATTACK_UNARMED_OFF, anim::ATTACK_UNARMED,
|
||||
anim::ATTACK_OFF, anim::ATTACK_1H};
|
||||
caps.resolvedMeleeOffHandUnarmed = pick(meleeOffUnarmedCands, 4);
|
||||
}
|
||||
caps.hasMelee = (caps.resolvedMelee1H != 0 || caps.resolvedMeleeUnarmed != 0);
|
||||
|
||||
// ── Ready stances ───────────────────────────────────────────────────
|
||||
{
|
||||
static const uint32_t ready1HCands[] = {
|
||||
anim::READY_1H, anim::READY_2H, anim::READY_UNARMED};
|
||||
caps.resolvedReady1H = pick(ready1HCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t ready2HCands[] = {
|
||||
anim::READY_2H, anim::READY_2H_LOOSE, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReady2H = pick(ready2HCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t ready2HLooseCands[] = {
|
||||
anim::READY_2H_LOOSE, anim::READY_2H, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReady2HLoose = pick(ready2HLooseCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyUnarmedCands[] = {
|
||||
anim::READY_UNARMED, anim::READY_1H, anim::READY_FIST};
|
||||
caps.resolvedReadyUnarmed = pick(readyUnarmedCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyFistCands[] = {
|
||||
anim::READY_FIST_1H, anim::READY_FIST, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyFist = pick(readyFistCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyBowCands[] = {
|
||||
anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyBow = pick(readyBowCands, 3);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyRifleCands[] = {
|
||||
anim::READY_RIFLE, anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyRifle = pick(readyRifleCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyCrossbowCands[] = {
|
||||
anim::READY_CROSSBOW, anim::READY_BOW, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyCrossbow = pick(readyCrossbowCands, 4);
|
||||
}
|
||||
{
|
||||
static const uint32_t readyThrownCands[] = {
|
||||
anim::READY_THROWN, anim::READY_1H, anim::READY_UNARMED};
|
||||
caps.resolvedReadyThrown = pick(readyThrownCands, 3);
|
||||
}
|
||||
|
||||
// ── Ranged attacks ──────────────────────────────────────────────────
|
||||
{
|
||||
static const uint32_t fireBowCands[] = {anim::FIRE_BOW, anim::ATTACK_BOW};
|
||||
caps.resolvedFireBow = pick(fireBowCands, 2);
|
||||
}
|
||||
caps.resolvedAttackRifle = has(anim::ATTACK_RIFLE) ? anim::ATTACK_RIFLE : 0;
|
||||
{
|
||||
static const uint32_t attackCrossbowCands[] = {anim::ATTACK_CROSSBOW, anim::ATTACK_BOW};
|
||||
caps.resolvedAttackCrossbow = pick(attackCrossbowCands, 2);
|
||||
}
|
||||
caps.resolvedAttackThrown = has(anim::ATTACK_THROWN) ? anim::ATTACK_THROWN : 0;
|
||||
caps.resolvedLoadBow = has(anim::LOAD_BOW) ? anim::LOAD_BOW : 0;
|
||||
caps.resolvedLoadRifle = has(anim::LOAD_RIFLE) ? anim::LOAD_RIFLE : 0;
|
||||
|
||||
// ── Special attacks ─────────────────────────────────────────────────
|
||||
caps.resolvedSpecial1H = has(anim::SPECIAL_1H) ? anim::SPECIAL_1H : 0;
|
||||
caps.resolvedSpecial2H = has(anim::SPECIAL_2H) ? anim::SPECIAL_2H : 0;
|
||||
caps.resolvedSpecialUnarmed = has(anim::SPECIAL_UNARMED) ? anim::SPECIAL_UNARMED : 0;
|
||||
caps.resolvedShieldBash = has(anim::SHIELD_BASH) ? anim::SHIELD_BASH : 0;
|
||||
|
||||
// ── Combat idle ─────────────────────────────────────────────────────
|
||||
// Base combat idle — weapon-specific stances are resolved per-frame
|
||||
// using ready stance fields above
|
||||
caps.resolvedCombatIdle = has(anim::READY_1H) ? anim::READY_1H
|
||||
: (has(anim::READY_UNARMED) ? anim::READY_UNARMED : 0);
|
||||
|
||||
// ── Activity animations ─────────────────────────────────────────────
|
||||
caps.resolvedStandWound = has(anim::STAND_WOUND) ? anim::STAND_WOUND : 0;
|
||||
caps.resolvedDeath = has(anim::DEATH) ? anim::DEATH : 0;
|
||||
caps.hasDeath = (caps.resolvedDeath != 0);
|
||||
caps.resolvedLoot = has(anim::LOOT) ? anim::LOOT : 0;
|
||||
caps.resolvedSitDown = has(anim::SIT_GROUND_DOWN) ? anim::SIT_GROUND_DOWN : 0;
|
||||
caps.resolvedSitLoop = has(anim::SITTING) ? anim::SITTING : 0;
|
||||
caps.resolvedSitUp = has(anim::SIT_GROUND_UP) ? anim::SIT_GROUND_UP : 0;
|
||||
caps.resolvedKneel = has(anim::KNEEL_LOOP) ? anim::KNEEL_LOOP : 0;
|
||||
|
||||
// ── Stealth ─────────────────────────────────────────────────────────
|
||||
caps.resolvedStealthIdle = has(anim::STEALTH_STAND) ? anim::STEALTH_STAND : 0;
|
||||
caps.resolvedStealthWalk = has(anim::STEALTH_WALK) ? anim::STEALTH_WALK : 0;
|
||||
caps.resolvedStealthRun = has(anim::STEALTH_RUN) ? anim::STEALTH_RUN : 0;
|
||||
caps.hasStealth = (caps.resolvedStealthIdle != 0);
|
||||
|
||||
// ── Misc ────────────────────────────────────────────────────────────
|
||||
caps.resolvedMount = has(anim::MOUNT) ? anim::MOUNT : 0;
|
||||
caps.hasMount = (caps.resolvedMount != 0);
|
||||
caps.resolvedUnsheathe = has(anim::UNSHEATHE) ? anim::UNSHEATHE : 0;
|
||||
{
|
||||
static const uint32_t sheatheCands[] = {anim::SHEATHE, anim::HIP_SHEATHE};
|
||||
caps.resolvedSheathe = pick(sheatheCands, 2);
|
||||
}
|
||||
caps.resolvedStun = has(anim::STUN) ? anim::STUN : 0;
|
||||
caps.resolvedCombatWound = has(anim::COMBAT_WOUND) ? anim::COMBAT_WOUND : 0;
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
AnimCapabilitySet AnimCapabilityProbe::probeMountModel(Renderer* /*renderer*/, uint32_t /*mountInstanceId*/) {
|
||||
// Mount models use M2Renderer, not CharacterRenderer
|
||||
// For now, mount capabilities are handled separately via MountAnimSet discovery
|
||||
// This stub returns an empty set — mount animations are discovered in setMounted()
|
||||
AnimCapabilitySet caps;
|
||||
return caps;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
// animation_ids.cpp — Inverse lookup & DBC validation
|
||||
// Generated from animation_ids.hpp (452 constants, IDs 0–451)
|
||||
// ============================================================================
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
36
src/rendering/animation/animation_manager.cpp
Normal file
36
src/rendering/animation/animation_manager.cpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Renamed from PlayerAnimator/NpcAnimator dual-map → unified CharacterAnimator registry.
|
||||
// NpcAnimator methods removed — all characters use CharacterAnimator.
|
||||
#include "rendering/animation/animation_manager.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ── Character animators ──────────────────────────────────────────────────────
|
||||
|
||||
CharacterAnimator& AnimationManager::getOrCreate(uint32_t instanceId) {
|
||||
auto it = animators_.find(instanceId);
|
||||
if (it != animators_.end()) return *it->second;
|
||||
|
||||
auto [inserted, _] = animators_.emplace(instanceId, std::make_unique<CharacterAnimator>());
|
||||
return *inserted->second;
|
||||
}
|
||||
|
||||
CharacterAnimator* AnimationManager::get(uint32_t instanceId) {
|
||||
auto it = animators_.find(instanceId);
|
||||
return it != animators_.end() ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
void AnimationManager::remove(uint32_t instanceId) {
|
||||
animators_.erase(instanceId);
|
||||
}
|
||||
|
||||
// ── Update all ───────────────────────────────────────────────────────────────
|
||||
|
||||
void AnimationManager::updateAll(float dt) {
|
||||
for (auto& [id, animator] : animators_) {
|
||||
animator->update(dt);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
216
src/rendering/animation/character_animator.cpp
Normal file
216
src/rendering/animation/character_animator.cpp
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// Renamed from player_animator.cpp → character_animator.cpp
|
||||
// Class renamed: PlayerAnimator → CharacterAnimator
|
||||
// All animations are now generic (character-based, not player-specific).
|
||||
#include "rendering/animation/character_animator.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "game/inventory.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
CharacterAnimator::CharacterAnimator() = default;
|
||||
|
||||
// ── IAnimator ────────────────────────────────────────────────────────────────
|
||||
|
||||
void CharacterAnimator::onEvent(AnimEvent event) {
|
||||
locomotion_.onEvent(event);
|
||||
combat_.onEvent(event);
|
||||
activity_.onEvent(event);
|
||||
mount_.onEvent(event);
|
||||
}
|
||||
|
||||
void CharacterAnimator::update(float dt) {
|
||||
lastDt_ = dt;
|
||||
lastOutput_ = resolveAnimation();
|
||||
}
|
||||
|
||||
// ── ICharacterAnimator ──────────────────────────────────────────────────────
|
||||
|
||||
void CharacterAnimator::startSpellCast(uint32_t precast, uint32_t cast, bool loop, uint32_t finalize) {
|
||||
combat_.startSpellCast(precast, cast, loop, finalize);
|
||||
}
|
||||
|
||||
void CharacterAnimator::stopSpellCast() {
|
||||
combat_.stopSpellCast();
|
||||
}
|
||||
|
||||
void CharacterAnimator::triggerMeleeSwing() {
|
||||
// Melee is handled via timer in FrameInput — CombatFSM transitions automatically
|
||||
}
|
||||
|
||||
void CharacterAnimator::triggerRangedShot() {
|
||||
// Ranged is handled via timer in FrameInput — CombatFSM transitions automatically
|
||||
}
|
||||
|
||||
void CharacterAnimator::triggerHitReaction(uint32_t animId) {
|
||||
combat_.triggerHitReaction(animId);
|
||||
}
|
||||
|
||||
void CharacterAnimator::triggerSpecialAttack(uint32_t /*spellId*/) {
|
||||
// Special attack animation is injected via FrameInput::specialAttackAnimId
|
||||
}
|
||||
|
||||
void CharacterAnimator::setEquippedWeaponType(const WeaponLoadout& loadout) {
|
||||
loadout_ = loadout;
|
||||
}
|
||||
|
||||
void CharacterAnimator::setEquippedRangedType(RangedWeaponType type) {
|
||||
loadout_.rangedType = type;
|
||||
}
|
||||
|
||||
void CharacterAnimator::playEmote(uint32_t animId, bool loop) {
|
||||
activity_.startEmote(animId, loop);
|
||||
}
|
||||
|
||||
void CharacterAnimator::cancelEmote() {
|
||||
activity_.cancelEmote();
|
||||
}
|
||||
|
||||
void CharacterAnimator::startLooting() {
|
||||
activity_.startLooting();
|
||||
}
|
||||
|
||||
void CharacterAnimator::stopLooting() {
|
||||
activity_.stopLooting();
|
||||
}
|
||||
|
||||
void CharacterAnimator::setStunned(bool stunned) {
|
||||
combat_.setStunned(stunned);
|
||||
}
|
||||
|
||||
void CharacterAnimator::setCharging(bool charging) {
|
||||
combat_.setCharging(charging);
|
||||
}
|
||||
|
||||
void CharacterAnimator::setStandState(uint8_t state) {
|
||||
activity_.setStandState(state);
|
||||
}
|
||||
|
||||
void CharacterAnimator::setStealthed(bool stealth) {
|
||||
stealthed_ = stealth;
|
||||
}
|
||||
|
||||
void CharacterAnimator::setInCombat(bool combat) {
|
||||
inCombat_ = combat;
|
||||
}
|
||||
|
||||
void CharacterAnimator::setLowHealth(bool low) {
|
||||
lowHealth_ = low;
|
||||
}
|
||||
|
||||
void CharacterAnimator::setSprintAuraActive(bool active) {
|
||||
sprintAura_ = active;
|
||||
}
|
||||
|
||||
// ── Mount ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void CharacterAnimator::configureMountFSM(const MountFSM::MountAnimSet& anims, bool taxiFlight) {
|
||||
mount_.configure(anims, taxiFlight);
|
||||
}
|
||||
|
||||
void CharacterAnimator::clearMountFSM() {
|
||||
mount_.clear();
|
||||
}
|
||||
|
||||
// ── Priority resolver ────────────────────────────────────────────────────────
|
||||
|
||||
AnimOutput CharacterAnimator::resolveAnimation() {
|
||||
const auto& fi = frameInput_;
|
||||
|
||||
// ── Mount takes over everything ─────────────────────────────────────
|
||||
if (mount_.isActive()) {
|
||||
// MountFSM returns mount-specific output; rider anim is separate
|
||||
// For the main character animation, we return MOUNT (or flight variant)
|
||||
uint32_t riderAnim = caps_.resolvedMount ? caps_.resolvedMount : anim::MOUNT;
|
||||
return AnimOutput::ok(riderAnim, true);
|
||||
}
|
||||
|
||||
// ── Build combat input ──────────────────────────────────────────────
|
||||
CombatFSM::Input combatIn;
|
||||
combatIn.inCombat = inCombat_;
|
||||
combatIn.grounded = fi.grounded;
|
||||
combatIn.jumping = fi.jumping;
|
||||
combatIn.swimming = fi.swimming;
|
||||
combatIn.moving = fi.moving;
|
||||
combatIn.sprinting = fi.sprinting;
|
||||
combatIn.lowHealth = lowHealth_;
|
||||
combatIn.meleeSwingTimer = fi.meleeSwingTimer;
|
||||
combatIn.rangedShootTimer = fi.rangedShootTimer;
|
||||
combatIn.specialAttackAnimId = fi.specialAttackAnimId;
|
||||
combatIn.rangedAnimId = fi.rangedAnimId;
|
||||
combatIn.currentAnimId = fi.currentAnimId;
|
||||
combatIn.currentAnimTime = fi.currentAnimTime;
|
||||
combatIn.currentAnimDuration = fi.currentAnimDuration;
|
||||
combatIn.haveAnimState = fi.haveAnimState;
|
||||
combatIn.hasUnsheathe = caps_.resolvedUnsheathe != 0;
|
||||
combatIn.hasSheathe = caps_.resolvedSheathe != 0;
|
||||
|
||||
// ── Combat FSM (highest priority for non-mount) ─────────────────────
|
||||
auto combatOut = combat_.resolve(combatIn, caps_, loadout_);
|
||||
if (combatOut.valid) return applyOverlays(combatOut);
|
||||
|
||||
// ── Activity FSM (emote, loot, sit) ─────────────────────────────────
|
||||
ActivityFSM::Input actIn;
|
||||
actIn.moving = fi.moving;
|
||||
actIn.sprinting = fi.sprinting;
|
||||
actIn.jumping = fi.jumping;
|
||||
actIn.grounded = fi.grounded;
|
||||
actIn.swimming = fi.swimming;
|
||||
actIn.sitting = fi.sitting;
|
||||
actIn.stunned = combat_.isStunned();
|
||||
actIn.currentAnimId = fi.currentAnimId;
|
||||
actIn.currentAnimTime = fi.currentAnimTime;
|
||||
actIn.currentAnimDuration = fi.currentAnimDuration;
|
||||
actIn.haveAnimState = fi.haveAnimState;
|
||||
|
||||
auto actOut = activity_.resolve(actIn, caps_);
|
||||
if (actOut.valid) return actOut;
|
||||
|
||||
// ── Locomotion FSM (lowest priority) ────────────────────────────────
|
||||
LocomotionFSM::Input locoIn;
|
||||
locoIn.moving = fi.moving;
|
||||
locoIn.movingForward = fi.movingForward;
|
||||
locoIn.sprinting = fi.sprinting;
|
||||
locoIn.movingBackward = fi.movingBackward;
|
||||
locoIn.strafeLeft = fi.strafeLeft;
|
||||
locoIn.strafeRight = fi.strafeRight;
|
||||
locoIn.grounded = fi.grounded;
|
||||
locoIn.jumping = fi.jumping;
|
||||
locoIn.swimming = fi.swimming;
|
||||
locoIn.sitting = fi.sitting;
|
||||
locoIn.sprintAura = sprintAura_;
|
||||
locoIn.deltaTime = lastDt_;
|
||||
// Animation state for one-shot completion detection (jump start/end)
|
||||
locoIn.currentAnimId = fi.currentAnimId;
|
||||
locoIn.currentAnimTime = fi.currentAnimTime;
|
||||
locoIn.currentAnimDuration = fi.currentAnimDuration;
|
||||
locoIn.haveAnimState = fi.haveAnimState;
|
||||
|
||||
auto locoOut = locomotion_.resolve(locoIn, caps_);
|
||||
if (locoOut.valid) return applyOverlays(locoOut);
|
||||
|
||||
// All FSMs returned invalid → STAY (keep last animation)
|
||||
return AnimOutput::stay();
|
||||
}
|
||||
|
||||
// ── Overlay application ──────────────────────────────────────────────────────
|
||||
|
||||
AnimOutput CharacterAnimator::applyOverlays(AnimOutput base) const {
|
||||
if (!stealthed_) return base;
|
||||
|
||||
// Stealth substitution based on locomotion state
|
||||
auto locoState = locomotion_.getState();
|
||||
if (locoState == LocomotionFSM::State::IDLE) {
|
||||
if (caps_.resolvedStealthIdle) base.animId = caps_.resolvedStealthIdle;
|
||||
} else if (locoState == LocomotionFSM::State::WALK) {
|
||||
if (caps_.resolvedStealthWalk) base.animId = caps_.resolvedStealthWalk;
|
||||
} else if (locoState == LocomotionFSM::State::RUN) {
|
||||
if (caps_.resolvedStealthRun) base.animId = caps_.resolvedStealthRun;
|
||||
else if (caps_.resolvedStealthWalk) base.animId = caps_.resolvedStealthWalk;
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
459
src/rendering/animation/combat_fsm.cpp
Normal file
459
src/rendering/animation/combat_fsm.cpp
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
#include "rendering/animation/combat_fsm.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "game/inventory.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ── One-shot completion helper ───────────────────────────────────────────────
|
||||
|
||||
bool CombatFSM::oneShotComplete(const Input& in, uint32_t expectedAnimId) const {
|
||||
if (!in.haveAnimState) return false;
|
||||
// Renderer auto-returns one-shots to STAND; detect that OR normal completion
|
||||
return in.currentAnimId != expectedAnimId ||
|
||||
(in.currentAnimDuration > 0.1f && in.currentAnimTime >= in.currentAnimDuration - 0.05f);
|
||||
}
|
||||
|
||||
// ── Event handling ───────────────────────────────────────────────────────────
|
||||
|
||||
void CombatFSM::onEvent(AnimEvent event) {
|
||||
switch (event) {
|
||||
case AnimEvent::COMBAT_ENTER:
|
||||
if (state_ == State::INACTIVE)
|
||||
state_ = State::UNSHEATHE;
|
||||
break;
|
||||
case AnimEvent::COMBAT_EXIT:
|
||||
if (state_ == State::COMBAT_IDLE)
|
||||
state_ = State::SHEATHE;
|
||||
break;
|
||||
case AnimEvent::STUN_ENTER:
|
||||
clearSpellState();
|
||||
hitReactionAnimId_ = 0;
|
||||
stunned_ = true;
|
||||
state_ = State::STUNNED;
|
||||
break;
|
||||
case AnimEvent::STUN_EXIT:
|
||||
stunned_ = false;
|
||||
if (state_ == State::STUNNED)
|
||||
state_ = State::INACTIVE;
|
||||
break;
|
||||
case AnimEvent::HIT_REACT:
|
||||
// Handled by triggerHitReaction() with animId
|
||||
break;
|
||||
case AnimEvent::CHARGE_START:
|
||||
charging_ = true;
|
||||
clearSpellState();
|
||||
state_ = State::CHARGE;
|
||||
break;
|
||||
case AnimEvent::CHARGE_END:
|
||||
charging_ = false;
|
||||
if (state_ == State::CHARGE)
|
||||
state_ = State::INACTIVE;
|
||||
break;
|
||||
case AnimEvent::SWIM_ENTER:
|
||||
clearSpellState();
|
||||
hitReactionAnimId_ = 0;
|
||||
stunned_ = false;
|
||||
state_ = State::INACTIVE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Spell cast management ────────────────────────────────────────────────────
|
||||
|
||||
void CombatFSM::startSpellCast(uint32_t precast, uint32_t cast, bool castLoop, uint32_t finalize) {
|
||||
spellPrecastAnimId_ = precast;
|
||||
spellCastAnimId_ = cast;
|
||||
spellCastLoop_ = castLoop;
|
||||
spellFinalizeAnimId_ = finalize;
|
||||
spellPrecastAnimSeen_ = false;
|
||||
spellPrecastFrames_ = 0;
|
||||
spellFinalizeAnimSeen_ = false;
|
||||
spellFinalizeFrames_ = 0;
|
||||
state_ = (precast != 0) ? State::SPELL_PRECAST : State::SPELL_CASTING;
|
||||
}
|
||||
|
||||
void CombatFSM::stopSpellCast() {
|
||||
if (state_ != State::SPELL_PRECAST && state_ != State::SPELL_CASTING) return;
|
||||
spellFinalizeAnimSeen_ = false;
|
||||
spellFinalizeFrames_ = 0;
|
||||
state_ = State::SPELL_FINALIZE;
|
||||
}
|
||||
|
||||
void CombatFSM::clearSpellState() {
|
||||
spellPrecastAnimId_ = 0;
|
||||
spellCastAnimId_ = 0;
|
||||
spellCastLoop_ = false;
|
||||
spellFinalizeAnimId_ = 0;
|
||||
spellPrecastAnimSeen_ = false;
|
||||
spellPrecastFrames_ = 0;
|
||||
spellFinalizeAnimSeen_ = false;
|
||||
spellFinalizeFrames_ = 0;
|
||||
}
|
||||
|
||||
// ── Hit/stun ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void CombatFSM::triggerHitReaction(uint32_t animId) {
|
||||
// Don't interrupt swim/jump/stun states
|
||||
if (state_ == State::STUNNED) return;
|
||||
// Interrupt spell casting
|
||||
if (state_ == State::SPELL_PRECAST || state_ == State::SPELL_CASTING || state_ == State::SPELL_FINALIZE) {
|
||||
clearSpellState();
|
||||
}
|
||||
hitReactionAnimId_ = animId;
|
||||
state_ = State::HIT_REACTION;
|
||||
}
|
||||
|
||||
void CombatFSM::setStunned(bool stunned) {
|
||||
stunned_ = stunned;
|
||||
if (stunned) {
|
||||
if (state_ == State::SPELL_PRECAST || state_ == State::SPELL_CASTING || state_ == State::SPELL_FINALIZE) {
|
||||
clearSpellState();
|
||||
}
|
||||
hitReactionAnimId_ = 0;
|
||||
state_ = State::STUNNED;
|
||||
} else {
|
||||
if (state_ == State::STUNNED)
|
||||
state_ = State::INACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
void CombatFSM::setCharging(bool charging) {
|
||||
charging_ = charging;
|
||||
if (charging) {
|
||||
clearSpellState();
|
||||
hitReactionAnimId_ = 0;
|
||||
state_ = State::CHARGE;
|
||||
} else if (state_ == State::CHARGE) {
|
||||
state_ = State::INACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
// ── State transitions ────────────────────────────────────────────────────────
|
||||
|
||||
void CombatFSM::updateTransitions(const Input& in) {
|
||||
// Stun override: can't act while stunned
|
||||
if (stunned_ && state_ != State::STUNNED) {
|
||||
state_ = State::STUNNED;
|
||||
return;
|
||||
}
|
||||
|
||||
// Force melee/ranged overrides
|
||||
if (in.meleeSwingTimer > 0.0f && !stunned_ && in.grounded && !in.swimming) {
|
||||
if (state_ != State::MELEE_SWING) {
|
||||
clearSpellState();
|
||||
hitReactionAnimId_ = 0;
|
||||
}
|
||||
state_ = State::MELEE_SWING;
|
||||
return;
|
||||
}
|
||||
if (in.rangedShootTimer > 0.0f && !stunned_ && in.meleeSwingTimer <= 0.0f && in.grounded && !in.swimming) {
|
||||
if (state_ != State::RANGED_SHOOT) {
|
||||
clearSpellState();
|
||||
hitReactionAnimId_ = 0;
|
||||
}
|
||||
state_ = State::RANGED_SHOOT;
|
||||
return;
|
||||
}
|
||||
if (charging_ && !stunned_) {
|
||||
if (state_ != State::CHARGE) {
|
||||
clearSpellState();
|
||||
hitReactionAnimId_ = 0;
|
||||
}
|
||||
state_ = State::CHARGE;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state_) {
|
||||
case State::INACTIVE:
|
||||
if (in.inCombat && in.grounded && !in.swimming && !in.moving) {
|
||||
state_ = in.hasUnsheathe ? State::UNSHEATHE : State::COMBAT_IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::COMBAT_IDLE:
|
||||
if (in.swimming || in.jumping || !in.grounded || in.moving) {
|
||||
state_ = State::INACTIVE;
|
||||
} else if (!in.inCombat) {
|
||||
state_ = in.hasSheathe ? State::SHEATHE : State::INACTIVE;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::MELEE_SWING:
|
||||
if (in.meleeSwingTimer <= 0.0f) {
|
||||
if (in.swimming) {
|
||||
state_ = State::INACTIVE;
|
||||
} else if (in.inCombat && in.grounded) {
|
||||
state_ = State::COMBAT_IDLE;
|
||||
} else {
|
||||
state_ = State::INACTIVE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case State::RANGED_SHOOT:
|
||||
if (in.rangedShootTimer <= 0.0f) {
|
||||
if (in.swimming) {
|
||||
state_ = State::INACTIVE;
|
||||
} else if (in.inCombat && in.grounded) {
|
||||
state_ = State::RANGED_LOAD;
|
||||
} else {
|
||||
state_ = State::INACTIVE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case State::RANGED_LOAD:
|
||||
if (in.swimming || in.jumping || !in.grounded || in.moving) {
|
||||
state_ = State::INACTIVE;
|
||||
} else if (in.inCombat) {
|
||||
state_ = State::COMBAT_IDLE;
|
||||
} else {
|
||||
state_ = State::INACTIVE;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::SPELL_PRECAST:
|
||||
if (in.swimming || (in.jumping && !in.grounded) || (!in.grounded && !in.jumping)) {
|
||||
clearSpellState();
|
||||
state_ = State::INACTIVE;
|
||||
} else if (in.haveAnimState) {
|
||||
uint32_t expectedAnim = spellPrecastAnimId_ ? spellPrecastAnimId_ : anim::SPELL_PRECAST;
|
||||
if (in.currentAnimId == expectedAnim) spellPrecastAnimSeen_ = true;
|
||||
if (spellPrecastAnimSeen_ && oneShotComplete(in, expectedAnim)) {
|
||||
state_ = State::SPELL_CASTING;
|
||||
}
|
||||
if (!spellPrecastAnimSeen_ && ++spellPrecastFrames_ > 10) {
|
||||
state_ = State::SPELL_CASTING;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case State::SPELL_CASTING:
|
||||
if (in.swimming || (in.jumping && !in.grounded) || (!in.grounded && !in.jumping)) {
|
||||
clearSpellState();
|
||||
state_ = State::INACTIVE;
|
||||
} else if (in.moving) {
|
||||
clearSpellState();
|
||||
state_ = State::INACTIVE;
|
||||
}
|
||||
// Stays in SPELL_CASTING until stopSpellCast() is called externally
|
||||
break;
|
||||
|
||||
case State::SPELL_FINALIZE: {
|
||||
if (in.swimming || (in.jumping && !in.grounded)) {
|
||||
clearSpellState();
|
||||
state_ = State::INACTIVE;
|
||||
} else if (in.haveAnimState) {
|
||||
uint32_t expectedAnim = spellFinalizeAnimId_ ? spellFinalizeAnimId_
|
||||
: (spellCastAnimId_ ? spellCastAnimId_ : anim::SPELL);
|
||||
if (in.currentAnimId == expectedAnim) spellFinalizeAnimSeen_ = true;
|
||||
if (spellFinalizeAnimSeen_ && oneShotComplete(in, expectedAnim)) {
|
||||
clearSpellState();
|
||||
state_ = in.inCombat ? State::COMBAT_IDLE : State::INACTIVE;
|
||||
}
|
||||
// Safety: if finalize anim never seen (model lacks it), finish after timeout
|
||||
if (!spellFinalizeAnimSeen_ && ++spellFinalizeFrames_ > 10) {
|
||||
clearSpellState();
|
||||
state_ = in.inCombat ? State::COMBAT_IDLE : State::INACTIVE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case State::HIT_REACTION:
|
||||
if (in.swimming || in.moving) {
|
||||
hitReactionAnimId_ = 0;
|
||||
state_ = State::INACTIVE;
|
||||
} else if (in.haveAnimState) {
|
||||
uint32_t expectedAnim = hitReactionAnimId_ ? hitReactionAnimId_ : anim::COMBAT_WOUND;
|
||||
if (oneShotComplete(in, expectedAnim)) {
|
||||
hitReactionAnimId_ = 0;
|
||||
state_ = in.inCombat ? State::COMBAT_IDLE : State::INACTIVE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case State::STUNNED:
|
||||
if (!stunned_) {
|
||||
state_ = in.inCombat ? State::COMBAT_IDLE : State::INACTIVE;
|
||||
} else if (in.swimming) {
|
||||
stunned_ = false;
|
||||
state_ = State::INACTIVE;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::CHARGE:
|
||||
if (!charging_) {
|
||||
state_ = State::INACTIVE;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::UNSHEATHE:
|
||||
if (in.swimming || in.moving) {
|
||||
state_ = State::INACTIVE;
|
||||
} else if (in.haveAnimState && oneShotComplete(in, anim::UNSHEATHE)) {
|
||||
state_ = State::COMBAT_IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::SHEATHE:
|
||||
if (in.swimming || in.moving) {
|
||||
state_ = State::INACTIVE;
|
||||
} else if (in.inCombat) {
|
||||
state_ = State::COMBAT_IDLE;
|
||||
} else if (in.haveAnimState && oneShotComplete(in, anim::SHEATHE)) {
|
||||
state_ = State::INACTIVE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Animation resolution ─────────────────────────────────────────────────────
|
||||
|
||||
AnimOutput CombatFSM::resolve(const Input& in, const AnimCapabilitySet& caps,
|
||||
const WeaponLoadout& loadout) {
|
||||
updateTransitions(in);
|
||||
|
||||
if (state_ == State::INACTIVE) return AnimOutput::stay();
|
||||
|
||||
uint32_t animId = 0;
|
||||
bool loop = true;
|
||||
|
||||
switch (state_) {
|
||||
case State::INACTIVE:
|
||||
return AnimOutput::stay();
|
||||
|
||||
case State::COMBAT_IDLE:
|
||||
if (in.lowHealth && caps.resolvedStandWound) {
|
||||
animId = caps.resolvedStandWound;
|
||||
} else if (loadout.rangedType == RangedWeaponType::BOW) {
|
||||
animId = caps.resolvedReadyBow;
|
||||
} else if (loadout.rangedType == RangedWeaponType::GUN) {
|
||||
animId = caps.resolvedReadyRifle;
|
||||
} else if (loadout.rangedType == RangedWeaponType::CROSSBOW) {
|
||||
animId = caps.resolvedReadyCrossbow;
|
||||
} else if (loadout.rangedType == RangedWeaponType::THROWN) {
|
||||
animId = caps.resolvedReadyThrown;
|
||||
} else if (loadout.is2HLoose) {
|
||||
animId = caps.resolvedReady2HLoose;
|
||||
} else if (loadout.inventoryType == game::InvType::TWO_HAND) {
|
||||
animId = caps.resolvedReady2H;
|
||||
} else if (loadout.isFist) {
|
||||
animId = caps.resolvedReadyFist;
|
||||
} else if (loadout.inventoryType == game::InvType::NON_EQUIP) {
|
||||
animId = caps.resolvedReadyUnarmed;
|
||||
} else {
|
||||
animId = caps.resolvedReady1H;
|
||||
}
|
||||
loop = true;
|
||||
break;
|
||||
|
||||
case State::MELEE_SWING:
|
||||
if (in.specialAttackAnimId != 0) {
|
||||
animId = in.specialAttackAnimId;
|
||||
} else {
|
||||
// Resolve melee animation using probed capabilities + weapon loadout
|
||||
bool useOffHand = loadout.hasOffHand && offHandTurn_;
|
||||
offHandTurn_ = loadout.hasOffHand ? !offHandTurn_ : false;
|
||||
|
||||
if (useOffHand) {
|
||||
if (loadout.isFist) animId = caps.resolvedMeleeOffHandFist;
|
||||
else if (loadout.isDagger) animId = caps.resolvedMeleeOffHandPierce;
|
||||
else if (loadout.inventoryType == game::InvType::NON_EQUIP) animId = caps.resolvedMeleeOffHandUnarmed;
|
||||
else animId = caps.resolvedMeleeOffHand;
|
||||
} else if (loadout.isFist) {
|
||||
animId = caps.resolvedMeleeFist;
|
||||
} else if (loadout.isDagger) {
|
||||
animId = caps.resolvedMeleePierce;
|
||||
} else if (loadout.is2HLoose) {
|
||||
animId = caps.resolvedMelee2HLoose;
|
||||
} else if (loadout.inventoryType == game::InvType::TWO_HAND) {
|
||||
animId = caps.resolvedMelee2H;
|
||||
} else if (loadout.inventoryType == game::InvType::NON_EQUIP) {
|
||||
animId = caps.resolvedMeleeUnarmed;
|
||||
} else {
|
||||
animId = caps.resolvedMelee1H;
|
||||
}
|
||||
}
|
||||
if (animId == 0) animId = anim::STAND; // Melee must play something
|
||||
loop = false;
|
||||
break;
|
||||
|
||||
case State::RANGED_SHOOT:
|
||||
animId = in.rangedAnimId ? in.rangedAnimId : anim::ATTACK_BOW;
|
||||
loop = false;
|
||||
break;
|
||||
|
||||
case State::RANGED_LOAD:
|
||||
switch (loadout.rangedType) {
|
||||
case RangedWeaponType::BOW: animId = caps.resolvedLoadBow; break;
|
||||
case RangedWeaponType::GUN: animId = caps.resolvedLoadRifle; break;
|
||||
case RangedWeaponType::CROSSBOW: animId = caps.resolvedLoadBow; break;
|
||||
default: break;
|
||||
}
|
||||
loop = false;
|
||||
break;
|
||||
|
||||
case State::SPELL_PRECAST:
|
||||
animId = spellPrecastAnimId_ ? spellPrecastAnimId_ : anim::SPELL_PRECAST;
|
||||
loop = false;
|
||||
break;
|
||||
|
||||
case State::SPELL_CASTING:
|
||||
animId = spellCastAnimId_ ? spellCastAnimId_ : anim::SPELL;
|
||||
loop = spellCastLoop_;
|
||||
break;
|
||||
|
||||
case State::SPELL_FINALIZE:
|
||||
animId = spellFinalizeAnimId_ ? spellFinalizeAnimId_
|
||||
: (spellCastAnimId_ ? spellCastAnimId_ : anim::SPELL);
|
||||
loop = false;
|
||||
break;
|
||||
|
||||
case State::HIT_REACTION:
|
||||
animId = hitReactionAnimId_ ? hitReactionAnimId_
|
||||
: (caps.resolvedCombatWound ? caps.resolvedCombatWound : anim::COMBAT_WOUND);
|
||||
loop = false;
|
||||
break;
|
||||
|
||||
case State::STUNNED:
|
||||
animId = caps.resolvedStun ? caps.resolvedStun : anim::STUN;
|
||||
loop = true;
|
||||
break;
|
||||
|
||||
case State::CHARGE:
|
||||
animId = caps.resolvedRun ? caps.resolvedRun : anim::RUN;
|
||||
loop = true;
|
||||
break;
|
||||
|
||||
case State::UNSHEATHE:
|
||||
animId = caps.resolvedUnsheathe ? caps.resolvedUnsheathe : anim::UNSHEATHE;
|
||||
loop = false;
|
||||
break;
|
||||
|
||||
case State::SHEATHE:
|
||||
animId = caps.resolvedSheathe ? caps.resolvedSheathe : anim::SHEATHE;
|
||||
loop = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (animId == 0) return AnimOutput::stay();
|
||||
return AnimOutput::ok(animId, loop);
|
||||
}
|
||||
|
||||
// ── Reset ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void CombatFSM::reset() {
|
||||
state_ = State::INACTIVE;
|
||||
clearSpellState();
|
||||
hitReactionAnimId_ = 0;
|
||||
stunned_ = false;
|
||||
charging_ = false;
|
||||
offHandTurn_ = false;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
299
src/rendering/animation/emote_registry.cpp
Normal file
299
src/rendering/animation/emote_registry.cpp
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
#include "rendering/animation/emote_registry.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "pipeline/asset_manager.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include "pipeline/dbc_layout.hpp"
|
||||
#include "core/application.hpp"
|
||||
#include "core/logger.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ── Helper functions (moved from animation_controller.cpp) ───────────────────
|
||||
|
||||
static std::vector<std::string> parseEmoteCommands(const std::string& raw) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
for (char c : raw) {
|
||||
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_') {
|
||||
cur.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
|
||||
} else if (!cur.empty()) {
|
||||
out.push_back(cur);
|
||||
cur.clear();
|
||||
}
|
||||
}
|
||||
if (!cur.empty()) out.push_back(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool isLoopingEmote(const std::string& command) {
|
||||
static const std::unordered_set<std::string> kLooping = {
|
||||
"dance", "train", "dead", "eat", "work",
|
||||
};
|
||||
return kLooping.find(command) != kLooping.end();
|
||||
}
|
||||
|
||||
// Map one-shot emote animation IDs to their persistent EMOTE_STATE_* looping variants.
|
||||
// When a looping emote is played, we prefer the STATE variant if the model has it.
|
||||
static uint32_t getEmoteStateVariantStatic(uint32_t oneShotAnimId) {
|
||||
static const std::unordered_map<uint32_t, uint32_t> kStateMap = {
|
||||
{anim::EMOTE_DANCE, anim::EMOTE_STATE_DANCE},
|
||||
{anim::EMOTE_LAUGH, anim::EMOTE_STATE_LAUGH},
|
||||
{anim::EMOTE_POINT, anim::EMOTE_STATE_POINT},
|
||||
{anim::EMOTE_EAT, anim::EMOTE_STATE_EAT},
|
||||
{anim::EMOTE_ROAR, anim::EMOTE_STATE_ROAR},
|
||||
{anim::EMOTE_APPLAUD, anim::EMOTE_STATE_APPLAUD},
|
||||
{anim::EMOTE_WORK, anim::EMOTE_STATE_WORK},
|
||||
{anim::EMOTE_USE_STANDING, anim::EMOTE_STATE_USE_STANDING},
|
||||
{anim::EATING_LOOP, anim::EMOTE_STATE_EAT},
|
||||
};
|
||||
auto it = kStateMap.find(oneShotAnimId);
|
||||
return it != kStateMap.end() ? it->second : 0;
|
||||
}
|
||||
|
||||
static std::string replacePlaceholders(const std::string& text, const std::string* targetName) {
|
||||
if (text.empty()) return text;
|
||||
std::string out;
|
||||
out.reserve(text.size() + 16);
|
||||
for (size_t i = 0; i < text.size(); ++i) {
|
||||
if (text[i] == '%' && i + 1 < text.size() && text[i + 1] == 's') {
|
||||
if (targetName && !targetName->empty()) out += *targetName;
|
||||
i++;
|
||||
} else {
|
||||
out.push_back(text[i]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── EmoteRegistry implementation ─────────────────────────────────────────────
|
||||
|
||||
EmoteRegistry& EmoteRegistry::instance() {
|
||||
static EmoteRegistry inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
void EmoteRegistry::loadFromDbc() {
|
||||
if (loaded_) return;
|
||||
loaded_ = true;
|
||||
|
||||
auto* assetManager = core::Application::getInstance().getAssetManager();
|
||||
if (!assetManager) {
|
||||
LOG_WARNING("Emotes: no AssetManager");
|
||||
loadFallbackEmotes();
|
||||
return;
|
||||
}
|
||||
|
||||
auto emotesTextDbc = assetManager->loadDBC("EmotesText.dbc");
|
||||
auto emotesTextDataDbc = assetManager->loadDBC("EmotesTextData.dbc");
|
||||
if (!emotesTextDbc || !emotesTextDataDbc || !emotesTextDbc->isLoaded() || !emotesTextDataDbc->isLoaded()) {
|
||||
LOG_WARNING("Emotes: DBCs not available (EmotesText/EmotesTextData)");
|
||||
loadFallbackEmotes();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* activeLayout = pipeline::getActiveDBCLayout();
|
||||
const auto* etdL = activeLayout ? activeLayout->getLayout("EmotesTextData") : nullptr;
|
||||
const auto* emL = activeLayout ? activeLayout->getLayout("Emotes") : nullptr;
|
||||
const auto* etL = activeLayout ? activeLayout->getLayout("EmotesText") : nullptr;
|
||||
|
||||
std::unordered_map<uint32_t, std::string> textData;
|
||||
textData.reserve(emotesTextDataDbc->getRecordCount());
|
||||
for (uint32_t r = 0; r < emotesTextDataDbc->getRecordCount(); ++r) {
|
||||
uint32_t id = emotesTextDataDbc->getUInt32(r, etdL ? (*etdL)["ID"] : 0);
|
||||
std::string text = emotesTextDataDbc->getString(r, etdL ? (*etdL)["Text"] : 1);
|
||||
if (!text.empty()) textData.emplace(id, std::move(text));
|
||||
}
|
||||
|
||||
std::unordered_map<uint32_t, uint32_t> emoteIdToAnim;
|
||||
if (auto emotesDbc = assetManager->loadDBC("Emotes.dbc"); emotesDbc && emotesDbc->isLoaded()) {
|
||||
emoteIdToAnim.reserve(emotesDbc->getRecordCount());
|
||||
for (uint32_t r = 0; r < emotesDbc->getRecordCount(); ++r) {
|
||||
uint32_t emoteId = emotesDbc->getUInt32(r, emL ? (*emL)["ID"] : 0);
|
||||
uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2);
|
||||
if (animId != 0) emoteIdToAnim[emoteId] = animId;
|
||||
}
|
||||
}
|
||||
|
||||
emoteTable_.clear();
|
||||
emoteTable_.reserve(emotesTextDbc->getRecordCount());
|
||||
for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) {
|
||||
uint32_t recordId = emotesTextDbc->getUInt32(r, etL ? (*etL)["ID"] : 0);
|
||||
std::string cmdRaw = emotesTextDbc->getString(r, etL ? (*etL)["Command"] : 1);
|
||||
if (cmdRaw.empty()) continue;
|
||||
|
||||
uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 2);
|
||||
uint32_t animId = 0;
|
||||
auto animIt = emoteIdToAnim.find(emoteRef);
|
||||
if (animIt != emoteIdToAnim.end()) {
|
||||
animId = animIt->second;
|
||||
} else {
|
||||
animId = emoteRef;
|
||||
}
|
||||
|
||||
uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5);
|
||||
uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9);
|
||||
uint32_t othersTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersTargetTextID"] : 3);
|
||||
uint32_t othersNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersNoTargetTextID"] : 7);
|
||||
|
||||
std::string textTarget, textNoTarget, oTarget, oNoTarget;
|
||||
if (auto it = textData.find(senderTargetTextId); it != textData.end()) textTarget = it->second;
|
||||
if (auto it = textData.find(senderNoTargetTextId); it != textData.end()) textNoTarget = it->second;
|
||||
if (auto it = textData.find(othersTargetTextId); it != textData.end()) oTarget = it->second;
|
||||
if (auto it = textData.find(othersNoTargetTextId); it != textData.end()) oNoTarget = it->second;
|
||||
|
||||
for (const std::string& cmd : parseEmoteCommands(cmdRaw)) {
|
||||
if (cmd.empty()) continue;
|
||||
EmoteInfo info;
|
||||
info.animId = animId;
|
||||
info.dbcId = recordId;
|
||||
info.loop = isLoopingEmote(cmd);
|
||||
info.textNoTarget = textNoTarget;
|
||||
info.textTarget = textTarget;
|
||||
info.othersNoTarget = oNoTarget;
|
||||
info.othersTarget = oTarget;
|
||||
info.command = cmd;
|
||||
emoteTable_.emplace(cmd, std::move(info));
|
||||
}
|
||||
}
|
||||
|
||||
if (emoteTable_.empty()) {
|
||||
LOG_WARNING("Emotes: DBC loaded but no commands parsed, using fallback list");
|
||||
loadFallbackEmotes();
|
||||
} else {
|
||||
LOG_INFO("Emotes: loaded ", emoteTable_.size(), " commands from DBC");
|
||||
}
|
||||
|
||||
buildDbcIdIndex();
|
||||
}
|
||||
|
||||
void EmoteRegistry::loadFallbackEmotes() {
|
||||
if (!emoteTable_.empty()) return;
|
||||
emoteTable_ = {
|
||||
{"wave", {anim::EMOTE_WAVE, 0, false, "You wave.", "You wave at %s.", "%s waves.", "%s waves at %s.", "wave"}},
|
||||
{"bow", {anim::EMOTE_BOW, 0, false, "You bow down graciously.", "You bow down before %s.", "%s bows down graciously.", "%s bows down before %s.", "bow"}},
|
||||
{"laugh", {anim::EMOTE_LAUGH, 0, false, "You laugh.", "You laugh at %s.", "%s laughs.", "%s laughs at %s.", "laugh"}},
|
||||
{"point", {anim::EMOTE_POINT, 0, false, "You point over yonder.", "You point at %s.", "%s points over yonder.", "%s points at %s.", "point"}},
|
||||
{"cheer", {anim::EMOTE_CHEER, 0, false, "You cheer!", "You cheer at %s.", "%s cheers!", "%s cheers at %s.", "cheer"}},
|
||||
{"dance", {anim::EMOTE_DANCE, 0, true, "You burst into dance.", "You dance with %s.", "%s bursts into dance.", "%s dances with %s.", "dance"}},
|
||||
{"kneel", {anim::EMOTE_KNEEL, 0, false, "You kneel down.", "You kneel before %s.", "%s kneels down.", "%s kneels before %s.", "kneel"}},
|
||||
{"applaud", {anim::EMOTE_APPLAUD, 0, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "%s applauds. Bravo!", "%s applauds at %s. Bravo!", "applaud"}},
|
||||
{"shout", {anim::EMOTE_SHOUT, 0, false, "You shout.", "You shout at %s.", "%s shouts.", "%s shouts at %s.", "shout"}},
|
||||
{"chicken", {anim::EMOTE_CHICKEN, 0, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!",
|
||||
"With arms flapping, you strut around %s. Cluck, Cluck, Chicken!",
|
||||
"%s struts around. Cluck, Cluck, Chicken!", "%s struts around %s. Cluck, Cluck, Chicken!", "chicken"}},
|
||||
{"cry", {anim::EMOTE_CRY, 0, false, "You cry.", "You cry on %s's shoulder.", "%s cries.", "%s cries on %s's shoulder.", "cry"}},
|
||||
{"kiss", {anim::EMOTE_KISS, 0, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "%s blows a kiss into the wind.", "%s blows a kiss to %s.", "kiss"}},
|
||||
{"roar", {anim::EMOTE_ROAR, 0, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "%s roars with bestial vigor. So fierce!", "%s roars with bestial vigor at %s. So fierce!", "roar"}},
|
||||
{"salute", {anim::EMOTE_SALUTE, 0, false, "You salute.", "You salute %s with respect.", "%s salutes.", "%s salutes %s with respect.", "salute"}},
|
||||
{"rude", {anim::EMOTE_RUDE, 0, false, "You make a rude gesture.", "You make a rude gesture at %s.", "%s makes a rude gesture.", "%s makes a rude gesture at %s.", "rude"}},
|
||||
{"flex", {anim::EMOTE_FLEX, 0, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "%s flexes. Oooooh so strong!", "%s flexes at %s. Oooooh so strong!", "flex"}},
|
||||
{"shy", {anim::EMOTE_SHY, 0, false, "You smile shyly.", "You smile shyly at %s.", "%s smiles shyly.", "%s smiles shyly at %s.", "shy"}},
|
||||
{"beg", {anim::EMOTE_BEG, 0, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "%s begs everyone around. How pathetic.", "%s begs %s. How pathetic.", "beg"}},
|
||||
{"eat", {anim::EMOTE_EAT, 0, true, "You begin to eat.", "You begin to eat in front of %s.", "%s begins to eat.", "%s begins to eat in front of %s.", "eat"}},
|
||||
{"talk", {anim::EMOTE_TALK, 0, false, "You talk.", "You talk to %s.", "%s talks.", "%s talks to %s.", "talk"}},
|
||||
{"work", {anim::EMOTE_WORK, 0, true, "You begin to work.", "You begin to work near %s.", "%s begins to work.", "%s begins to work near %s.", "work"}},
|
||||
{"train", {anim::EMOTE_TRAIN, 0, true, "You let off a train whistle. Choo Choo!", "You let off a train whistle at %s. Choo Choo!", "%s lets off a train whistle. Choo Choo!", "%s lets off a train whistle at %s. Choo Choo!", "train"}},
|
||||
{"dead", {anim::EMOTE_DEAD, 0, true, "You play dead.", "You play dead in front of %s.", "%s plays dead.", "%s plays dead in front of %s.", "dead"}},
|
||||
};
|
||||
buildDbcIdIndex();
|
||||
}
|
||||
|
||||
void EmoteRegistry::buildDbcIdIndex() {
|
||||
emoteByDbcId_.clear();
|
||||
for (auto& [cmd, info] : emoteTable_) {
|
||||
if (info.dbcId != 0) {
|
||||
emoteByDbcId_.emplace(info.dbcId, &info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<EmoteRegistry::EmoteResult> EmoteRegistry::findEmote(const std::string& command) const {
|
||||
auto it = emoteTable_.find(command);
|
||||
if (it == emoteTable_.end()) return std::nullopt;
|
||||
const auto& info = it->second;
|
||||
if (info.animId == 0) return std::nullopt;
|
||||
return EmoteResult{info.animId, info.loop};
|
||||
}
|
||||
|
||||
uint32_t EmoteRegistry::animByDbcId(uint32_t dbcId) const {
|
||||
auto it = emoteByDbcId_.find(dbcId);
|
||||
if (it != emoteByDbcId_.end()) {
|
||||
return it->second->animId;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t EmoteRegistry::getStateVariant(uint32_t oneShotAnimId) const {
|
||||
return getEmoteStateVariantStatic(oneShotAnimId);
|
||||
}
|
||||
|
||||
const EmoteInfo* EmoteRegistry::findInfo(const std::string& command) const {
|
||||
auto it = emoteTable_.find(command);
|
||||
return it != emoteTable_.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
std::string EmoteRegistry::textFor(const std::string& emoteName,
|
||||
const std::string* targetName) const {
|
||||
auto it = emoteTable_.find(emoteName);
|
||||
if (it != emoteTable_.end()) {
|
||||
const auto& info = it->second;
|
||||
const std::string& base = (targetName ? info.textTarget : info.textNoTarget);
|
||||
if (!base.empty()) {
|
||||
return replacePlaceholders(base, targetName);
|
||||
}
|
||||
if (targetName && !targetName->empty()) {
|
||||
return "You " + info.command + " at " + *targetName + ".";
|
||||
}
|
||||
return "You " + info.command + ".";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
uint32_t EmoteRegistry::dbcIdFor(const std::string& emoteName) const {
|
||||
auto it = emoteTable_.find(emoteName);
|
||||
if (it != emoteTable_.end()) {
|
||||
return it->second.dbcId;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string EmoteRegistry::textByDbcId(uint32_t dbcId,
|
||||
const std::string& senderName,
|
||||
const std::string* targetName) const {
|
||||
auto it = emoteByDbcId_.find(dbcId);
|
||||
if (it == emoteByDbcId_.end()) return "";
|
||||
|
||||
const EmoteInfo& info = *it->second;
|
||||
|
||||
if (targetName && !targetName->empty()) {
|
||||
if (!info.othersTarget.empty()) {
|
||||
std::string out;
|
||||
out.reserve(info.othersTarget.size() + senderName.size() + targetName->size());
|
||||
bool firstReplaced = false;
|
||||
for (size_t i = 0; i < info.othersTarget.size(); ++i) {
|
||||
if (info.othersTarget[i] == '%' && i + 1 < info.othersTarget.size() && info.othersTarget[i + 1] == 's') {
|
||||
out += firstReplaced ? *targetName : senderName;
|
||||
firstReplaced = true;
|
||||
++i;
|
||||
} else {
|
||||
out.push_back(info.othersTarget[i]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return senderName + " " + info.command + "s at " + *targetName + ".";
|
||||
} else {
|
||||
if (!info.othersNoTarget.empty()) {
|
||||
return replacePlaceholders(info.othersNoTarget, &senderName);
|
||||
}
|
||||
return senderName + " " + info.command + "s.";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
207
src/rendering/animation/footstep_driver.cpp
Normal file
207
src/rendering/animation/footstep_driver.cpp
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
#include "rendering/animation/footstep_driver.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "rendering/camera_controller.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/terrain_manager.hpp"
|
||||
#include "rendering/wmo_renderer.hpp"
|
||||
#include "rendering/water_renderer.hpp"
|
||||
#include "rendering/swim_effects.hpp"
|
||||
#include "audio/audio_coordinator.hpp"
|
||||
#include "audio/footstep_manager.hpp"
|
||||
#include "audio/movement_sound_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ── Footstep event detection (moved from AnimationController) ────────────────
|
||||
|
||||
bool FootstepDriver::shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs) {
|
||||
if (animationDurationMs <= 1.0f) {
|
||||
footstepNormInitialized_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
float wrappedTime = animationTimeMs;
|
||||
while (wrappedTime >= animationDurationMs) {
|
||||
wrappedTime -= animationDurationMs;
|
||||
}
|
||||
if (wrappedTime < 0.0f) wrappedTime += animationDurationMs;
|
||||
float norm = wrappedTime / animationDurationMs;
|
||||
|
||||
if (animationId != footstepLastAnimationId_) {
|
||||
footstepLastAnimationId_ = animationId;
|
||||
footstepLastNormTime_ = norm;
|
||||
footstepNormInitialized_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!footstepNormInitialized_) {
|
||||
footstepNormInitialized_ = true;
|
||||
footstepLastNormTime_ = norm;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto crossed = [&](float eventNorm) {
|
||||
if (footstepLastNormTime_ <= norm) {
|
||||
return footstepLastNormTime_ < eventNorm && eventNorm <= norm;
|
||||
}
|
||||
return footstepLastNormTime_ < eventNorm || eventNorm <= norm;
|
||||
};
|
||||
|
||||
bool trigger = crossed(0.22f) || crossed(0.72f);
|
||||
footstepLastNormTime_ = norm;
|
||||
return trigger;
|
||||
}
|
||||
|
||||
audio::FootstepSurface FootstepDriver::resolveFootstepSurface(Renderer* renderer) const {
|
||||
auto* cameraController = renderer->getCameraController();
|
||||
if (!cameraController || !cameraController->isThirdPerson()) {
|
||||
return audio::FootstepSurface::STONE;
|
||||
}
|
||||
|
||||
const glm::vec3& p = renderer->getCharacterPosition();
|
||||
|
||||
float distSq = glm::dot(p - cachedFootstepPosition_, p - cachedFootstepPosition_);
|
||||
if (distSq < 2.25f && cachedFootstepUpdateTimer_ < 0.5f) {
|
||||
return cachedFootstepSurface_;
|
||||
}
|
||||
|
||||
cachedFootstepPosition_ = p;
|
||||
cachedFootstepUpdateTimer_ = 0.0f;
|
||||
|
||||
if (cameraController->isSwimming()) {
|
||||
cachedFootstepSurface_ = audio::FootstepSurface::WATER;
|
||||
return audio::FootstepSurface::WATER;
|
||||
}
|
||||
|
||||
auto* waterRenderer = renderer->getWaterRenderer();
|
||||
if (waterRenderer) {
|
||||
auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y);
|
||||
if (waterH && p.z < (*waterH + 0.25f)) {
|
||||
cachedFootstepSurface_ = audio::FootstepSurface::WATER;
|
||||
return audio::FootstepSurface::WATER;
|
||||
}
|
||||
}
|
||||
|
||||
auto* wmoRenderer = renderer->getWMORenderer();
|
||||
auto* terrainManager = renderer->getTerrainManager();
|
||||
if (wmoRenderer) {
|
||||
auto wmoFloor = wmoRenderer->getFloorHeight(p.x, p.y, p.z + 1.5f);
|
||||
auto terrainFloor = terrainManager ? terrainManager->getHeightAt(p.x, p.y) : std::nullopt;
|
||||
if (wmoFloor && (!terrainFloor || *wmoFloor >= *terrainFloor - 0.1f)) {
|
||||
cachedFootstepSurface_ = audio::FootstepSurface::STONE;
|
||||
return audio::FootstepSurface::STONE;
|
||||
}
|
||||
}
|
||||
|
||||
audio::FootstepSurface surface = audio::FootstepSurface::STONE;
|
||||
|
||||
if (terrainManager) {
|
||||
auto texture = terrainManager->getDominantTextureAt(p.x, p.y);
|
||||
if (texture) {
|
||||
std::string t = *texture;
|
||||
for (char& c : t) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
if (t.find("snow") != std::string::npos || t.find("ice") != std::string::npos) surface = audio::FootstepSurface::SNOW;
|
||||
else if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) surface = audio::FootstepSurface::GRASS;
|
||||
else if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) surface = audio::FootstepSurface::DIRT;
|
||||
else if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) surface = audio::FootstepSurface::WOOD;
|
||||
else if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) surface = audio::FootstepSurface::METAL;
|
||||
else if (t.find("stone") != std::string::npos || t.find("rock") != std::string::npos || t.find("cobble") != std::string::npos || t.find("brick") != std::string::npos) surface = audio::FootstepSurface::STONE;
|
||||
}
|
||||
}
|
||||
|
||||
cachedFootstepSurface_ = surface;
|
||||
return surface;
|
||||
}
|
||||
|
||||
// ── Footstep update (moved from AnimationController::updateFootsteps) ────────
|
||||
|
||||
void FootstepDriver::update(float deltaTime, Renderer* renderer,
|
||||
bool mounted, uint32_t mountInstanceId, bool taxiFlight,
|
||||
bool isFootstepState) {
|
||||
auto* footstepManager = renderer->getAudioCoordinator()->getFootstepManager();
|
||||
if (!footstepManager) return;
|
||||
|
||||
auto* characterRenderer = renderer->getCharacterRenderer();
|
||||
auto* cameraController = renderer->getCameraController();
|
||||
uint32_t characterInstanceId = renderer->getCharacterInstanceId();
|
||||
|
||||
footstepManager->update(deltaTime);
|
||||
cachedFootstepUpdateTimer_ += deltaTime;
|
||||
|
||||
bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 &&
|
||||
cameraController && cameraController->isThirdPerson() &&
|
||||
cameraController->isGrounded() && !cameraController->isSwimming();
|
||||
|
||||
if (canPlayFootsteps && mounted && mountInstanceId > 0 && !taxiFlight) {
|
||||
// Mount footsteps: use mount's animation for timing
|
||||
uint32_t animId = 0;
|
||||
float animTimeMs = 0.0f, animDurationMs = 0.0f;
|
||||
if (characterRenderer->getAnimationState(mountInstanceId, animId, animTimeMs, animDurationMs) &&
|
||||
animDurationMs > 1.0f && cameraController->isMoving()) {
|
||||
float wrappedTime = animTimeMs;
|
||||
while (wrappedTime >= animDurationMs) {
|
||||
wrappedTime -= animDurationMs;
|
||||
}
|
||||
if (wrappedTime < 0.0f) wrappedTime += animDurationMs;
|
||||
float norm = wrappedTime / animDurationMs;
|
||||
|
||||
if (animId != mountFootstepLastAnimId_) {
|
||||
mountFootstepLastAnimId_ = animId;
|
||||
mountFootstepLastNormTime_ = norm;
|
||||
mountFootstepNormInitialized_ = true;
|
||||
} else if (!mountFootstepNormInitialized_) {
|
||||
mountFootstepNormInitialized_ = true;
|
||||
mountFootstepLastNormTime_ = norm;
|
||||
} else {
|
||||
auto crossed = [&](float eventNorm) {
|
||||
if (mountFootstepLastNormTime_ <= norm) {
|
||||
return mountFootstepLastNormTime_ < eventNorm && eventNorm <= norm;
|
||||
}
|
||||
return mountFootstepLastNormTime_ < eventNorm || eventNorm <= norm;
|
||||
};
|
||||
if (crossed(0.25f) || crossed(0.75f)) {
|
||||
footstepManager->playFootstep(resolveFootstepSurface(renderer), true);
|
||||
}
|
||||
mountFootstepLastNormTime_ = norm;
|
||||
}
|
||||
} else {
|
||||
mountFootstepNormInitialized_ = false;
|
||||
}
|
||||
footstepNormInitialized_ = false;
|
||||
} else if (canPlayFootsteps && isFootstepState) {
|
||||
uint32_t animId = 0;
|
||||
float animTimeMs = 0.0f;
|
||||
float animDurationMs = 0.0f;
|
||||
if (characterRenderer->getAnimationState(characterInstanceId, animId, animTimeMs, animDurationMs) &&
|
||||
shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) {
|
||||
auto surface = resolveFootstepSurface(renderer);
|
||||
footstepManager->playFootstep(surface, cameraController->isSprinting());
|
||||
if (surface == audio::FootstepSurface::WATER) {
|
||||
if (renderer->getAudioCoordinator()->getMovementSoundManager()) {
|
||||
renderer->getAudioCoordinator()->getMovementSoundManager()->playWaterFootstep(audio::MovementSoundManager::CharacterSize::MEDIUM);
|
||||
}
|
||||
auto* swimEffects = renderer->getSwimEffects();
|
||||
auto* waterRenderer = renderer->getWaterRenderer();
|
||||
if (swimEffects && waterRenderer) {
|
||||
const glm::vec3& characterPosition = renderer->getCharacterPosition();
|
||||
auto wh = waterRenderer->getWaterHeightAt(characterPosition.x, characterPosition.y);
|
||||
if (wh) {
|
||||
swimEffects->spawnFootSplash(characterPosition, *wh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mountFootstepNormInitialized_ = false;
|
||||
} else {
|
||||
footstepNormInitialized_ = false;
|
||||
mountFootstepNormInitialized_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
283
src/rendering/animation/locomotion_fsm.cpp
Normal file
283
src/rendering/animation/locomotion_fsm.cpp
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
#include "rendering/animation/locomotion_fsm.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ── One-shot completion helper ─────────────────────────────────────────────────────────
|
||||
|
||||
bool LocomotionFSM::oneShotComplete(const Input& in, uint32_t expectedAnimId) const {
|
||||
if (!in.haveAnimState) return false;
|
||||
return in.currentAnimId != expectedAnimId ||
|
||||
(in.currentAnimDuration > 0.1f && in.currentAnimTime >= in.currentAnimDuration - 0.05f);
|
||||
}
|
||||
|
||||
// ── Event handling ───────────────────────────────────────────────────────────
|
||||
|
||||
void LocomotionFSM::onEvent(AnimEvent event) {
|
||||
switch (event) {
|
||||
case AnimEvent::SWIM_ENTER:
|
||||
state_ = State::SWIM_IDLE;
|
||||
break;
|
||||
case AnimEvent::SWIM_EXIT:
|
||||
state_ = State::IDLE;
|
||||
break;
|
||||
case AnimEvent::JUMP:
|
||||
if (state_ != State::SWIM_IDLE && state_ != State::SWIM) {
|
||||
jumpStartSeen_ = false;
|
||||
state_ = State::JUMP_START;
|
||||
}
|
||||
break;
|
||||
case AnimEvent::LANDED:
|
||||
if (state_ == State::JUMP_MID || state_ == State::JUMP_START) {
|
||||
jumpEndSeen_ = false;
|
||||
state_ = State::JUMP_END;
|
||||
}
|
||||
break;
|
||||
case AnimEvent::MOVE_START:
|
||||
graceTimer_ = kGraceSec;
|
||||
break;
|
||||
case AnimEvent::MOVE_STOP:
|
||||
// Grace timer handles the delay in updateTransitions
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── State transitions ────────────────────────────────────────────────────────
|
||||
|
||||
void LocomotionFSM::updateTransitions(const Input& in, const AnimCapabilitySet& caps) {
|
||||
// Update grace timer
|
||||
if (in.moving) {
|
||||
graceTimer_ = kGraceSec;
|
||||
wasSprinting_ = in.sprinting;
|
||||
} else {
|
||||
graceTimer_ = std::max(0.0f, graceTimer_ - in.deltaTime);
|
||||
}
|
||||
|
||||
const bool effectiveMoving = in.moving || graceTimer_ > 0.0f;
|
||||
const bool effectiveSprinting = in.sprinting || (!in.moving && effectiveMoving && wasSprinting_);
|
||||
|
||||
switch (state_) {
|
||||
case State::IDLE:
|
||||
if (in.swimming) {
|
||||
state_ = effectiveMoving ? State::SWIM : State::SWIM_IDLE;
|
||||
} else if (!in.grounded && in.jumping) {
|
||||
jumpStartSeen_ = false;
|
||||
state_ = State::JUMP_START;
|
||||
} else if (!in.grounded) {
|
||||
state_ = State::JUMP_MID;
|
||||
} else if (effectiveMoving && effectiveSprinting) {
|
||||
state_ = State::RUN;
|
||||
} else if (effectiveMoving) {
|
||||
state_ = State::WALK;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::WALK:
|
||||
if (in.swimming) {
|
||||
state_ = effectiveMoving ? State::SWIM : State::SWIM_IDLE;
|
||||
} else if (!in.grounded && in.jumping) {
|
||||
jumpStartSeen_ = false;
|
||||
state_ = State::JUMP_START;
|
||||
} else if (!in.grounded) {
|
||||
state_ = State::JUMP_MID;
|
||||
} else if (!effectiveMoving) {
|
||||
state_ = State::IDLE;
|
||||
} else if (effectiveSprinting) {
|
||||
state_ = State::RUN;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::RUN:
|
||||
if (in.swimming) {
|
||||
state_ = effectiveMoving ? State::SWIM : State::SWIM_IDLE;
|
||||
} else if (!in.grounded && in.jumping) {
|
||||
jumpStartSeen_ = false;
|
||||
state_ = State::JUMP_START;
|
||||
} else if (!in.grounded) {
|
||||
state_ = State::JUMP_MID;
|
||||
} else if (!effectiveMoving) {
|
||||
state_ = State::IDLE;
|
||||
} else if (!effectiveSprinting) {
|
||||
state_ = State::WALK;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::JUMP_START:
|
||||
if (in.swimming) {
|
||||
state_ = State::SWIM_IDLE;
|
||||
} else if (in.grounded) {
|
||||
state_ = State::JUMP_END;
|
||||
jumpEndSeen_ = false;
|
||||
} else if (caps.resolvedJumpStart == 0) {
|
||||
// Model doesn't have JUMP_START animation — skip to mid-air
|
||||
state_ = State::JUMP_MID;
|
||||
} else if (in.haveAnimState) {
|
||||
// Use the same resolved ID that resolve() outputs
|
||||
uint32_t expected = caps.resolvedJumpStart;
|
||||
if (in.currentAnimId == expected) jumpStartSeen_ = true;
|
||||
// Also detect completion via renderer's auto-STAND reset:
|
||||
// once the animation was seen and currentAnimId changed, it completed.
|
||||
if (jumpStartSeen_ && oneShotComplete(in, expected)) {
|
||||
state_ = State::JUMP_MID;
|
||||
}
|
||||
} else {
|
||||
// No animation state available — fall through after 1 frame
|
||||
state_ = State::JUMP_MID;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::JUMP_MID:
|
||||
if (in.swimming) {
|
||||
state_ = State::SWIM_IDLE;
|
||||
} else if (in.grounded) {
|
||||
state_ = State::JUMP_END;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::JUMP_END:
|
||||
if (in.swimming) {
|
||||
state_ = effectiveMoving ? State::SWIM : State::SWIM_IDLE;
|
||||
} else if (effectiveMoving) {
|
||||
// Movement overrides landing animation
|
||||
state_ = effectiveSprinting ? State::RUN : State::WALK;
|
||||
} else if (caps.resolvedJumpEnd == 0) {
|
||||
// Model doesn't have JUMP_END animation — go straight to IDLE
|
||||
state_ = State::IDLE;
|
||||
} else if (in.haveAnimState) {
|
||||
uint32_t expected = caps.resolvedJumpEnd;
|
||||
if (in.currentAnimId == expected) jumpEndSeen_ = true;
|
||||
// Only transition to IDLE after landing animation completes
|
||||
if (jumpEndSeen_ && oneShotComplete(in, expected)) {
|
||||
state_ = State::IDLE;
|
||||
}
|
||||
} else {
|
||||
state_ = State::IDLE;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::SWIM_IDLE:
|
||||
if (!in.swimming) {
|
||||
state_ = effectiveMoving ? State::WALK : State::IDLE;
|
||||
} else if (effectiveMoving) {
|
||||
state_ = State::SWIM;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::SWIM:
|
||||
if (!in.swimming) {
|
||||
state_ = effectiveMoving ? State::WALK : State::IDLE;
|
||||
} else if (!effectiveMoving) {
|
||||
state_ = State::SWIM_IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Animation resolution ─────────────────────────────────────────────────────
|
||||
|
||||
AnimOutput LocomotionFSM::resolve(const Input& in, const AnimCapabilitySet& caps) {
|
||||
updateTransitions(in, caps);
|
||||
|
||||
const bool pureStrafe = !in.movingForward && !in.movingBackward; // strafe without forward/back
|
||||
const bool anyStrafeLeft = in.strafeLeft && !in.strafeRight && pureStrafe;
|
||||
const bool anyStrafeRight = in.strafeRight && !in.strafeLeft && pureStrafe;
|
||||
|
||||
uint32_t animId = anim::STAND;
|
||||
bool animSelected = true;
|
||||
bool loop = true;
|
||||
|
||||
switch (state_) {
|
||||
case State::IDLE:
|
||||
animId = anim::STAND;
|
||||
break;
|
||||
|
||||
case State::WALK:
|
||||
if (in.movingBackward) {
|
||||
animId = caps.resolvedWalkBackwards ? caps.resolvedWalkBackwards
|
||||
: caps.resolvedWalk ? caps.resolvedWalk
|
||||
: anim::WALK_BACKWARDS;
|
||||
} else if (anyStrafeLeft) {
|
||||
animId = caps.resolvedStrafeLeft ? caps.resolvedStrafeLeft : anim::SHUFFLE_LEFT;
|
||||
} else if (anyStrafeRight) {
|
||||
animId = caps.resolvedStrafeRight ? caps.resolvedStrafeRight : anim::SHUFFLE_RIGHT;
|
||||
} else {
|
||||
animId = caps.resolvedWalk ? caps.resolvedWalk : anim::WALK;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::RUN:
|
||||
if (in.movingBackward) {
|
||||
animId = caps.resolvedWalkBackwards ? caps.resolvedWalkBackwards
|
||||
: caps.resolvedWalk ? caps.resolvedWalk
|
||||
: anim::WALK_BACKWARDS;
|
||||
} else if (anyStrafeLeft) {
|
||||
animId = caps.resolvedRunLeft ? caps.resolvedRunLeft
|
||||
: caps.resolvedRun ? caps.resolvedRun
|
||||
: anim::RUN;
|
||||
} else if (anyStrafeRight) {
|
||||
animId = caps.resolvedRunRight ? caps.resolvedRunRight
|
||||
: caps.resolvedRun ? caps.resolvedRun
|
||||
: anim::RUN;
|
||||
} else if (in.sprintAura) {
|
||||
animId = caps.resolvedSprint ? caps.resolvedSprint : anim::RUN;
|
||||
} else {
|
||||
animId = caps.resolvedRun ? caps.resolvedRun : anim::RUN;
|
||||
}
|
||||
break;
|
||||
|
||||
case State::JUMP_START:
|
||||
animId = caps.resolvedJumpStart ? caps.resolvedJumpStart : anim::JUMP_START;
|
||||
loop = false;
|
||||
break;
|
||||
case State::JUMP_MID:
|
||||
animId = caps.resolvedJump ? caps.resolvedJump : anim::JUMP;
|
||||
loop = true; // Must loop — long falls outlast a single play cycle
|
||||
break;
|
||||
case State::JUMP_END:
|
||||
animId = caps.resolvedJumpEnd ? caps.resolvedJumpEnd : anim::JUMP_END;
|
||||
loop = false;
|
||||
break;
|
||||
|
||||
case State::SWIM_IDLE:
|
||||
animId = caps.resolvedSwimIdle ? caps.resolvedSwimIdle : anim::SWIM_IDLE;
|
||||
break;
|
||||
|
||||
case State::SWIM:
|
||||
if (in.movingBackward) {
|
||||
animId = caps.resolvedSwimBackwards ? caps.resolvedSwimBackwards
|
||||
: caps.resolvedSwim ? caps.resolvedSwim
|
||||
: anim::SWIM;
|
||||
} else if (anyStrafeLeft) {
|
||||
animId = caps.resolvedSwimLeft ? caps.resolvedSwimLeft
|
||||
: caps.resolvedSwim ? caps.resolvedSwim
|
||||
: anim::SWIM;
|
||||
} else if (anyStrafeRight) {
|
||||
animId = caps.resolvedSwimRight ? caps.resolvedSwimRight
|
||||
: caps.resolvedSwim ? caps.resolvedSwim
|
||||
: anim::SWIM;
|
||||
} else {
|
||||
animId = caps.resolvedSwim ? caps.resolvedSwim : anim::SWIM;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!animSelected) return AnimOutput::stay();
|
||||
return AnimOutput::ok(animId, loop);
|
||||
}
|
||||
|
||||
// ── Reset ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void LocomotionFSM::reset() {
|
||||
state_ = State::IDLE;
|
||||
graceTimer_ = 0.0f;
|
||||
wasSprinting_ = false;
|
||||
jumpStartSeen_ = false;
|
||||
jumpEndSeen_ = false;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
342
src/rendering/animation/mount_fsm.cpp
Normal file
342
src/rendering/animation/mount_fsm.cpp
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
#include "rendering/animation/mount_fsm.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
// ── Configure / Clear ────────────────────────────────────────────────────────
|
||||
|
||||
void MountFSM::configure(const MountAnimSet& anims, bool taxiFlight) {
|
||||
anims_ = anims;
|
||||
taxiFlight_ = taxiFlight;
|
||||
active_ = true;
|
||||
state_ = MountState::IDLE;
|
||||
action_ = MountAction::None;
|
||||
actionPhase_ = 0;
|
||||
fidgetTimer_ = 0.0f;
|
||||
activeFidget_ = 0;
|
||||
idleSoundTimer_ = 0.0f;
|
||||
prevYaw_ = 0.0f;
|
||||
roll_ = 0.0f;
|
||||
lastMountAnim_ = 0;
|
||||
|
||||
// Seed per-instance RNG
|
||||
std::random_device rd;
|
||||
rng_.seed(rd());
|
||||
nextFidgetTime_ = std::uniform_real_distribution<float>(6.0f, 12.0f)(rng_);
|
||||
nextIdleSoundTime_ = std::uniform_real_distribution<float>(45.0f, 90.0f)(rng_);
|
||||
}
|
||||
|
||||
void MountFSM::clear() {
|
||||
active_ = false;
|
||||
state_ = MountState::IDLE;
|
||||
action_ = MountAction::None;
|
||||
actionPhase_ = 0;
|
||||
taxiFlight_ = false;
|
||||
anims_ = {};
|
||||
fidgetTimer_ = 0.0f;
|
||||
activeFidget_ = 0;
|
||||
idleSoundTimer_ = 0.0f;
|
||||
lastMountAnim_ = 0;
|
||||
}
|
||||
|
||||
// ── Event handling ───────────────────────────────────────────────────────────
|
||||
|
||||
void MountFSM::onEvent(AnimEvent event) {
|
||||
if (!active_) return;
|
||||
switch (event) {
|
||||
case AnimEvent::JUMP:
|
||||
// Jump only triggered via evaluate() input check
|
||||
break;
|
||||
case AnimEvent::DISMOUNT:
|
||||
clear();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
bool MountFSM::actionAnimComplete(const Input& in) const {
|
||||
return in.haveMountState && in.curMountDuration > 0.1f &&
|
||||
(in.curMountTime >= in.curMountDuration - 0.05f);
|
||||
}
|
||||
|
||||
uint32_t MountFSM::resolveGroundOrFlyAnim(const Input& in) const {
|
||||
const bool pureStrafe = !in.movingBackward;
|
||||
const bool anyStrafeLeft = in.strafeLeft && !in.strafeRight && pureStrafe;
|
||||
const bool anyStrafeRight = in.strafeRight && !in.strafeLeft && pureStrafe;
|
||||
|
||||
if (in.moving) {
|
||||
if (in.flying) {
|
||||
if (in.ascending) {
|
||||
return anims_.flyUp ? anims_.flyUp : (anims_.flyForward ? anims_.flyForward : anim::RUN);
|
||||
} else if (in.descending) {
|
||||
return anims_.flyDown ? anims_.flyDown : (anims_.flyForward ? anims_.flyForward : anim::RUN);
|
||||
} else if (anyStrafeLeft) {
|
||||
return anims_.flyLeft ? anims_.flyLeft : (anims_.flyForward ? anims_.flyForward : anim::RUN);
|
||||
} else if (anyStrafeRight) {
|
||||
return anims_.flyRight ? anims_.flyRight : (anims_.flyForward ? anims_.flyForward : anim::RUN);
|
||||
} else if (in.movingBackward) {
|
||||
return anims_.flyBackwards ? anims_.flyBackwards : (anims_.flyForward ? anims_.flyForward : anim::RUN);
|
||||
} else {
|
||||
return anims_.flyForward ? anims_.flyForward : (anims_.flyIdle ? anims_.flyIdle : anim::RUN);
|
||||
}
|
||||
} else if (in.swimming) {
|
||||
// Mounted swimming — simplified, no per-direction mount swim anims needed here
|
||||
// (the original code used pickMountAnim with mount-specific swim IDs)
|
||||
return anims_.run ? anims_.run : anim::RUN;
|
||||
} else if (anyStrafeLeft) {
|
||||
return anims_.run ? anims_.run : anim::RUN;
|
||||
} else if (anyStrafeRight) {
|
||||
return anims_.run ? anims_.run : anim::RUN;
|
||||
} else if (in.movingBackward) {
|
||||
return anims_.run ? anims_.run : anim::RUN;
|
||||
} else {
|
||||
return anim::RUN;
|
||||
}
|
||||
} else {
|
||||
// Idle
|
||||
if (in.swimming) {
|
||||
return anims_.stand ? anims_.stand : anim::STAND;
|
||||
} else if (in.flying) {
|
||||
if (in.ascending) {
|
||||
return anims_.flyUp ? anims_.flyUp : (anims_.flyIdle ? anims_.flyIdle : anim::STAND);
|
||||
} else if (in.descending) {
|
||||
return anims_.flyDown ? anims_.flyDown : (anims_.flyIdle ? anims_.flyIdle : anim::STAND);
|
||||
} else {
|
||||
return anims_.flyIdle ? anims_.flyIdle : (anims_.flyForward ? anims_.flyForward : anim::STAND);
|
||||
}
|
||||
} else {
|
||||
return anims_.stand ? anims_.stand : anim::STAND;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main evaluation ──────────────────────────────────────────────────────────
|
||||
|
||||
MountFSM::Output MountFSM::evaluate(const Input& in) {
|
||||
Output out;
|
||||
if (!active_) return out;
|
||||
|
||||
const float dt = in.deltaTime;
|
||||
|
||||
// ── Procedural lean ─────────────────────────────────────────────────
|
||||
if (!taxiFlight_ && in.moving && dt > 0.0f) {
|
||||
float turnRate = (in.characterYaw - prevYaw_) / dt;
|
||||
while (turnRate > 180.0f) turnRate -= 360.0f;
|
||||
while (turnRate < -180.0f) turnRate += 360.0f;
|
||||
float targetLean = std::clamp(turnRate * 0.15f, -0.25f, 0.25f);
|
||||
roll_ = roll_ + (targetLean - roll_) * (1.0f - std::exp(-6.0f * dt));
|
||||
} else {
|
||||
roll_ = roll_ + (0.0f - roll_) * (1.0f - std::exp(-8.0f * dt));
|
||||
}
|
||||
prevYaw_ = in.characterYaw;
|
||||
out.mountRoll = roll_;
|
||||
|
||||
// ── Rider animation ─────────────────────────────────────────────────
|
||||
out.riderAnimId = anim::MOUNT;
|
||||
out.riderAnimLoop = true;
|
||||
// (Flight rider variants handled by the caller via capability set, not here)
|
||||
|
||||
// ── Taxi flight branch ──────────────────────────────────────────────
|
||||
if (taxiFlight_) {
|
||||
// Try flight animations in preference order using discovered anims
|
||||
uint32_t taxiAnim = anim::STAND;
|
||||
if (anims_.flyForward) taxiAnim = anims_.flyForward;
|
||||
else if (anims_.flyIdle) taxiAnim = anims_.flyIdle;
|
||||
else if (anims_.run) taxiAnim = anims_.run;
|
||||
|
||||
out.mountAnimId = taxiAnim;
|
||||
out.mountAnimLoop = true;
|
||||
out.mountAnimChanged = (!in.haveMountState || in.curMountAnim != taxiAnim);
|
||||
|
||||
// Bob calculation for taxi
|
||||
if (in.moving && in.haveMountState && in.curMountDuration > 1.0f) {
|
||||
float wrappedTime = in.curMountTime;
|
||||
while (wrappedTime >= in.curMountDuration) wrappedTime -= in.curMountDuration;
|
||||
float norm = wrappedTime / in.curMountDuration;
|
||||
out.mountBob = std::sin(norm * 2.0f * 3.14159f * 2.0f) * 0.12f;
|
||||
}
|
||||
|
||||
lastMountAnim_ = out.mountAnimId;
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Jump/rear-up trigger ────────────────────────────────────────────
|
||||
if (in.jumpKeyPressed && in.grounded && action_ == MountAction::None) {
|
||||
if (in.moving && anims_.jumpLoop > 0) {
|
||||
action_ = MountAction::Jump;
|
||||
actionPhase_ = 1; // Start with loop directly (matching original)
|
||||
out.mountAnimId = anims_.jumpLoop;
|
||||
out.mountAnimLoop = true;
|
||||
out.mountAnimChanged = true;
|
||||
out.playJumpSound = true;
|
||||
out.triggerMountJump = true;
|
||||
lastMountAnim_ = out.mountAnimId;
|
||||
|
||||
// Bob calc
|
||||
if (in.haveMountState && in.curMountDuration > 1.0f) {
|
||||
float wrappedTime = in.curMountTime;
|
||||
while (wrappedTime >= in.curMountDuration) wrappedTime -= in.curMountDuration;
|
||||
float norm = wrappedTime / in.curMountDuration;
|
||||
out.mountBob = std::sin(norm * 2.0f * 3.14159f) * 0.12f;
|
||||
}
|
||||
return out;
|
||||
} else if (!in.moving && anims_.rearUp > 0) {
|
||||
action_ = MountAction::RearUp;
|
||||
actionPhase_ = 0;
|
||||
out.mountAnimId = anims_.rearUp;
|
||||
out.mountAnimLoop = false;
|
||||
out.mountAnimChanged = true;
|
||||
out.playRearUpSound = true;
|
||||
lastMountAnim_ = out.mountAnimId;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Handle active mount actions (jump chaining or rear-up) ──────────
|
||||
if (action_ != MountAction::None) {
|
||||
bool animFinished = actionAnimComplete(in);
|
||||
|
||||
if (action_ == MountAction::Jump) {
|
||||
if (actionPhase_ == 0 && animFinished && anims_.jumpLoop > 0) {
|
||||
actionPhase_ = 1;
|
||||
out.mountAnimId = anims_.jumpLoop;
|
||||
out.mountAnimLoop = true;
|
||||
out.mountAnimChanged = true;
|
||||
} else if (actionPhase_ == 0 && animFinished) {
|
||||
actionPhase_ = 1;
|
||||
out.mountAnimId = in.curMountAnim;
|
||||
} else if (actionPhase_ == 1 && in.grounded && anims_.jumpEnd > 0) {
|
||||
actionPhase_ = 2;
|
||||
out.mountAnimId = anims_.jumpEnd;
|
||||
out.mountAnimLoop = false;
|
||||
out.mountAnimChanged = true;
|
||||
out.playLandSound = true;
|
||||
} else if (actionPhase_ == 1 && in.grounded) {
|
||||
action_ = MountAction::None;
|
||||
out.mountAnimId = in.moving ? anims_.run : anims_.stand;
|
||||
out.mountAnimLoop = true;
|
||||
out.mountAnimChanged = true;
|
||||
} else if (actionPhase_ == 2 && animFinished) {
|
||||
action_ = MountAction::None;
|
||||
out.mountAnimId = in.moving ? anims_.run : anims_.stand;
|
||||
out.mountAnimLoop = true;
|
||||
out.mountAnimChanged = true;
|
||||
} else {
|
||||
out.mountAnimId = in.curMountAnim;
|
||||
}
|
||||
} else if (action_ == MountAction::RearUp) {
|
||||
if (animFinished) {
|
||||
action_ = MountAction::None;
|
||||
out.mountAnimId = in.moving ? anims_.run : anims_.stand;
|
||||
out.mountAnimLoop = true;
|
||||
out.mountAnimChanged = true;
|
||||
} else {
|
||||
out.mountAnimId = in.curMountAnim;
|
||||
}
|
||||
}
|
||||
|
||||
// Bob calc
|
||||
if (in.moving && in.haveMountState && in.curMountDuration > 1.0f) {
|
||||
float wrappedTime = in.curMountTime;
|
||||
while (wrappedTime >= in.curMountDuration) wrappedTime -= in.curMountDuration;
|
||||
float norm = wrappedTime / in.curMountDuration;
|
||||
out.mountBob = std::sin(norm * 2.0f * 3.14159f) * 0.12f;
|
||||
}
|
||||
lastMountAnim_ = out.mountAnimId;
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Normal movement animation resolution ────────────────────────────
|
||||
uint32_t mountAnimId = resolveGroundOrFlyAnim(in);
|
||||
|
||||
// ── Cancel active fidget on movement ────────────────────────────────
|
||||
if (in.moving && activeFidget_ != 0) {
|
||||
activeFidget_ = 0;
|
||||
out.mountAnimId = mountAnimId;
|
||||
out.mountAnimLoop = true;
|
||||
out.mountAnimChanged = true;
|
||||
lastMountAnim_ = out.mountAnimId;
|
||||
|
||||
// Bob calc
|
||||
if (in.haveMountState && in.curMountDuration > 1.0f) {
|
||||
float wrappedTime = in.curMountTime;
|
||||
while (wrappedTime >= in.curMountDuration) wrappedTime -= in.curMountDuration;
|
||||
float norm = wrappedTime / in.curMountDuration;
|
||||
out.mountBob = std::sin(norm * 2.0f * 3.14159f) * 0.12f;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── Check if active fidget completed ────────────────────────────────
|
||||
if (!in.moving && activeFidget_ != 0) {
|
||||
if (in.haveMountState) {
|
||||
if (in.curMountAnim != activeFidget_ ||
|
||||
in.curMountTime >= in.curMountDuration * 0.95f) {
|
||||
activeFidget_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Idle fidgets ────────────────────────────────────────────────────
|
||||
if (!in.moving && action_ == MountAction::None && activeFidget_ == 0 && !anims_.fidgets.empty()) {
|
||||
fidgetTimer_ += dt;
|
||||
if (fidgetTimer_ >= nextFidgetTime_) {
|
||||
std::uniform_int_distribution<size_t> dist(0, anims_.fidgets.size() - 1);
|
||||
uint32_t fidgetAnim = anims_.fidgets[dist(rng_)];
|
||||
activeFidget_ = fidgetAnim;
|
||||
fidgetTimer_ = 0.0f;
|
||||
nextFidgetTime_ = std::uniform_real_distribution<float>(6.0f, 12.0f)(rng_);
|
||||
|
||||
out.mountAnimId = fidgetAnim;
|
||||
out.mountAnimLoop = false;
|
||||
out.mountAnimChanged = true;
|
||||
out.fidgetStarted = true;
|
||||
lastMountAnim_ = out.mountAnimId;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
if (in.moving) fidgetTimer_ = 0.0f;
|
||||
|
||||
// ── Idle ambient sounds ─────────────────────────────────────────────
|
||||
if (!in.moving) {
|
||||
idleSoundTimer_ += dt;
|
||||
if (idleSoundTimer_ >= nextIdleSoundTime_) {
|
||||
out.playIdleSound = true;
|
||||
idleSoundTimer_ = 0.0f;
|
||||
nextIdleSoundTime_ = std::uniform_real_distribution<float>(45.0f, 90.0f)(rng_);
|
||||
}
|
||||
} else {
|
||||
idleSoundTimer_ = 0.0f;
|
||||
}
|
||||
|
||||
// ── Set output ──────────────────────────────────────────────────────
|
||||
out.mountAnimId = activeFidget_ != 0 ? activeFidget_ : mountAnimId;
|
||||
out.mountAnimLoop = (activeFidget_ == 0);
|
||||
// Only trigger playAnimation if animation actually changed and no action/fidget active
|
||||
if (action_ == MountAction::None && activeFidget_ == 0 &&
|
||||
(!in.haveMountState || in.curMountAnim != mountAnimId)) {
|
||||
out.mountAnimChanged = true;
|
||||
out.mountAnimId = mountAnimId;
|
||||
}
|
||||
|
||||
// Bob calculation
|
||||
if (in.moving && in.haveMountState && in.curMountDuration > 1.0f) {
|
||||
float wrappedTime = in.curMountTime;
|
||||
while (wrappedTime >= in.curMountDuration) wrappedTime -= in.curMountDuration;
|
||||
float norm = wrappedTime / in.curMountDuration;
|
||||
float bobSpeed = taxiFlight_ ? 2.0f : 1.0f;
|
||||
out.mountBob = std::sin(norm * 2.0f * 3.14159f * bobSpeed) * 0.12f;
|
||||
}
|
||||
|
||||
lastMountAnim_ = out.mountAnimId;
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
94
src/rendering/animation/sfx_state_driver.cpp
Normal file
94
src/rendering/animation/sfx_state_driver.cpp
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// ============================================================================
|
||||
// SfxStateDriver — extracted from AnimationController
|
||||
//
|
||||
// Tracks state transitions for activity SFX (jump, landing, swim) and
|
||||
// mount ambient sounds. Moved from AnimationController::updateSfxState().
|
||||
// ============================================================================
|
||||
|
||||
#include "rendering/animation/sfx_state_driver.hpp"
|
||||
#include "rendering/animation/footstep_driver.hpp"
|
||||
#include "rendering/renderer.hpp"
|
||||
#include "audio/audio_coordinator.hpp"
|
||||
#include "audio/activity_sound_manager.hpp"
|
||||
#include "audio/mount_sound_manager.hpp"
|
||||
#include "audio/music_manager.hpp"
|
||||
#include "rendering/camera_controller.hpp"
|
||||
|
||||
namespace wowee {
|
||||
namespace rendering {
|
||||
|
||||
void SfxStateDriver::update(float deltaTime, Renderer* renderer,
|
||||
bool mounted, bool taxiFlight,
|
||||
FootstepDriver& footstepDriver) {
|
||||
auto* activitySoundManager = renderer->getAudioCoordinator()->getActivitySoundManager();
|
||||
if (!activitySoundManager) return;
|
||||
|
||||
auto* cameraController = renderer->getCameraController();
|
||||
|
||||
activitySoundManager->update(deltaTime);
|
||||
if (cameraController && cameraController->isThirdPerson()) {
|
||||
bool grounded = cameraController->isGrounded();
|
||||
bool jumping = cameraController->isJumping();
|
||||
bool falling = cameraController->isFalling();
|
||||
bool swimming = cameraController->isSwimming();
|
||||
bool moving = cameraController->isMoving();
|
||||
|
||||
if (!initialized_) {
|
||||
prevGrounded_ = grounded;
|
||||
prevJumping_ = jumping;
|
||||
prevFalling_ = falling;
|
||||
prevSwimming_ = swimming;
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
// Jump detection
|
||||
if (jumping && !prevJumping_ && !swimming) {
|
||||
activitySoundManager->playJump();
|
||||
}
|
||||
|
||||
// Landing detection
|
||||
if (grounded && !prevGrounded_) {
|
||||
bool hardLanding = prevFalling_;
|
||||
activitySoundManager->playLanding(
|
||||
footstepDriver.resolveFootstepSurface(renderer), hardLanding);
|
||||
}
|
||||
|
||||
// Water transitions
|
||||
if (swimming && !prevSwimming_) {
|
||||
activitySoundManager->playWaterEnter();
|
||||
} else if (!swimming && prevSwimming_) {
|
||||
activitySoundManager->playWaterExit();
|
||||
}
|
||||
|
||||
activitySoundManager->setSwimmingState(swimming, moving);
|
||||
|
||||
if (renderer->getAudioCoordinator()->getMusicManager()) {
|
||||
renderer->getAudioCoordinator()->getMusicManager()->setUnderwaterMode(swimming);
|
||||
}
|
||||
|
||||
prevGrounded_ = grounded;
|
||||
prevJumping_ = jumping;
|
||||
prevFalling_ = falling;
|
||||
prevSwimming_ = swimming;
|
||||
} else {
|
||||
activitySoundManager->setSwimmingState(false, false);
|
||||
if (renderer->getAudioCoordinator()->getMusicManager()) {
|
||||
renderer->getAudioCoordinator()->getMusicManager()->setUnderwaterMode(false);
|
||||
}
|
||||
initialized_ = false;
|
||||
}
|
||||
|
||||
// Mount ambient sounds
|
||||
if (renderer->getAudioCoordinator()->getMountSoundManager()) {
|
||||
renderer->getAudioCoordinator()->getMountSoundManager()->update(deltaTime);
|
||||
if (cameraController && mounted) {
|
||||
bool isMoving = cameraController->isMoving();
|
||||
bool flying = taxiFlight || !cameraController->isGrounded();
|
||||
renderer->getAudioCoordinator()->getMountSoundManager()->setMoving(isMoving);
|
||||
renderer->getAudioCoordinator()->getMountSoundManager()->setFlying(flying);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -472,6 +472,11 @@ void CameraController::update(float deltaTime) {
|
|||
standUpCallback_();
|
||||
}
|
||||
|
||||
// Notify server when the player sits down via local input
|
||||
if (!prevSitting && sitting && sitDownCallback_) {
|
||||
sitDownCallback_();
|
||||
}
|
||||
|
||||
// Update eye height based on crouch state (smooth transition)
|
||||
float targetEyeHeight = sitting ? CROUCH_EYE_HEIGHT : STAND_EYE_HEIGHT;
|
||||
float heightLerpSpeed = 10.0f * deltaTime;
|
||||
|
|
@ -1364,9 +1369,14 @@ void CameraController::update(float deltaTime) {
|
|||
// Only snap when:
|
||||
// 1. Near ground (within step-up range above) - handles walking
|
||||
// 2. Actually falling from height (was airborne + falling fast)
|
||||
// Scale snap range with fall speed so slow falls don't teleport
|
||||
// while extreme speeds still catch geometry penetration.
|
||||
// 3. Was grounded + ground is close (grace for slopes)
|
||||
bool nearGround = (dz >= 0.0f && dz <= stepUp);
|
||||
bool airFalling = (!grounded && verticalVelocity < -5.0f);
|
||||
float airSnapRange = std::min(fallCatch,
|
||||
std::max(0.5f, std::abs(verticalVelocity) * physicsDeltaTime * 2.0f));
|
||||
bool airFalling = (!grounded && verticalVelocity < -5.0f
|
||||
&& dz >= -airSnapRange);
|
||||
bool slopeGrace = (grounded && verticalVelocity > -1.0f &&
|
||||
dz >= -0.25f && dz <= stepUp * 1.5f);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#include "rendering/character_preview.hpp"
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "rendering/vk_render_target.hpp"
|
||||
#include "rendering/vk_texture.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
* the original WoW Model Viewer (charcontrol.h, REGION_FAC=2).
|
||||
*/
|
||||
#include "rendering/character_renderer.hpp"
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "rendering/vk_context.hpp"
|
||||
#include "rendering/vk_texture.hpp"
|
||||
#include "rendering/vk_pipeline.hpp"
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout
|
|||
vkCreateDescriptorSetLayout(device, &ci, nullptr, &boneSetLayout_);
|
||||
}
|
||||
|
||||
// Phase 2.1: Instance data set layout (set 3): binding 0 = STORAGE_BUFFER (per-instance data)
|
||||
// Instance data set layout (set 3): binding 0 = STORAGE_BUFFER (per-instance data)
|
||||
{
|
||||
VkDescriptorSetLayoutBinding binding{};
|
||||
binding.binding = 0;
|
||||
|
|
@ -476,7 +476,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout
|
|||
}
|
||||
}
|
||||
|
||||
// Phase 2.1: Instance data SSBO — per-frame buffer holding per-instance transforms, fade, bones.
|
||||
// Instance data SSBO — per-frame buffer holding per-instance transforms, fade, bones.
|
||||
// Shader reads instanceData[push.instanceDataOffset + gl_InstanceIndex].
|
||||
{
|
||||
static_assert(sizeof(M2InstanceGPU) == 96, "M2InstanceGPU must be 96 bytes (std430)");
|
||||
|
|
@ -522,7 +522,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout
|
|||
}
|
||||
}
|
||||
|
||||
// Phase 2.3: GPU frustum culling — compute pipeline, buffers, descriptors.
|
||||
// GPU frustum culling — compute pipeline, buffers, descriptors.
|
||||
// Compute shader tests each instance bounding sphere against 6 frustum planes + distance.
|
||||
// Output: uint visibility[] read back by CPU to skip culled instances in sortedVisible_ build.
|
||||
{
|
||||
|
|
@ -1060,7 +1060,7 @@ void M2Renderer::shutdown() {
|
|||
}
|
||||
if (materialDescPool_) { vkDestroyDescriptorPool(device, materialDescPool_, nullptr); materialDescPool_ = VK_NULL_HANDLE; }
|
||||
if (boneDescPool_) { vkDestroyDescriptorPool(device, boneDescPool_, nullptr); boneDescPool_ = VK_NULL_HANDLE; }
|
||||
// Phase 2.1: Instance data SSBO cleanup (sets freed with instanceDescPool_)
|
||||
// Instance data SSBO cleanup (sets freed with instanceDescPool_)
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (instanceBuffer_[i]) { vmaDestroyBuffer(alloc, instanceBuffer_[i], instanceAlloc_[i]); instanceBuffer_[i] = VK_NULL_HANDLE; }
|
||||
instanceMapped_[i] = nullptr;
|
||||
|
|
@ -1068,7 +1068,7 @@ void M2Renderer::shutdown() {
|
|||
}
|
||||
if (instanceDescPool_) { vkDestroyDescriptorPool(device, instanceDescPool_, nullptr); instanceDescPool_ = VK_NULL_HANDLE; }
|
||||
|
||||
// Phase 2.3: GPU frustum culling compute pipeline + buffers cleanup
|
||||
// GPU frustum culling compute pipeline + buffers cleanup
|
||||
if (cullPipeline_) { vkDestroyPipeline(device, cullPipeline_, nullptr); cullPipeline_ = VK_NULL_HANDLE; }
|
||||
if (cullPipelineLayout_) { vkDestroyPipelineLayout(device, cullPipelineLayout_, nullptr); cullPipelineLayout_ = VK_NULL_HANDLE; }
|
||||
for (int i = 0; i < 2; i++) {
|
||||
|
|
@ -2404,7 +2404,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
|||
boneWorkIndices_.push_back(idx);
|
||||
}
|
||||
|
||||
// Phase 2: Compute bone matrices (expensive, parallel if enough work)
|
||||
// Compute bone matrices (expensive, parallel if enough work)
|
||||
const size_t animCount = boneWorkIndices_.size();
|
||||
if (animCount > 0) {
|
||||
static const size_t minParallelAnimInstances = std::max<size_t>(
|
||||
|
|
@ -2464,7 +2464,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
|
|||
}
|
||||
}
|
||||
|
||||
// Phase 3: Particle update (sequential — uses RNG, not thread-safe)
|
||||
// Particle update (sequential — uses RNG, not thread-safe)
|
||||
// Only iterate instances that have particle emitters (pre-built list).
|
||||
for (size_t idx : particleInstanceIndices_) {
|
||||
if (idx >= instances.size()) continue;
|
||||
|
|
@ -2518,7 +2518,7 @@ void M2Renderer::prepareRender(uint32_t frameIndex, const Camera& camera) {
|
|||
}
|
||||
}
|
||||
|
||||
// Phase 2.3: Dispatch GPU frustum culling compute shader.
|
||||
// Dispatch GPU frustum culling compute shader.
|
||||
// Called on the primary command buffer BEFORE the render pass begins so that
|
||||
// compute dispatch and memory barrier complete before secondary command buffers
|
||||
// read the visibility output in render().
|
||||
|
|
@ -2617,7 +2617,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
|
||||
lastDrawCallCount = 0;
|
||||
|
||||
// Phase 2.3: GPU cull results — dispatchCullCompute() already updated smoothedRenderDist_.
|
||||
// GPU cull results — dispatchCullCompute() already updated smoothedRenderDist_.
|
||||
// Use the cached value (set by dispatchCullCompute or fallback below).
|
||||
const uint32_t frameIndex = vkCtx_->getCurrentFrame();
|
||||
const uint32_t numInstances = std::min(static_cast<uint32_t>(instances.size()), MAX_CULL_INSTANCES);
|
||||
|
|
@ -2649,7 +2649,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
sortedVisible_.reserve(expectedVisible);
|
||||
}
|
||||
|
||||
// Phase 2.3: GPU frustum culling — build frustum only for CPU fallback path
|
||||
// GPU frustum culling — build frustum only for CPU fallback path
|
||||
Frustum frustum;
|
||||
if (!gpuCullAvailable) {
|
||||
const glm::mat4 vp = camera.getProjectionMatrix() * camera.getViewMatrix();
|
||||
|
|
@ -2661,10 +2661,10 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
const auto& instance = instances[i];
|
||||
|
||||
if (gpuCullAvailable) {
|
||||
// Phase 2.3: GPU already tested flags + distance + frustum
|
||||
// GPU already tested flags + distance + frustum
|
||||
if (!visibility[i]) continue;
|
||||
} else {
|
||||
// CPU fallback: same culling logic as before Phase 2.3
|
||||
// CPU fallback: same culling logic as before
|
||||
if (!instance.cachedIsValid || instance.cachedIsSmoke || instance.cachedIsInvisibleTrap) continue;
|
||||
|
||||
glm::vec3 toCam = instance.position - camPos;
|
||||
|
|
@ -2712,7 +2712,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
VkPipeline currentPipeline = VK_NULL_HANDLE;
|
||||
VkDescriptorSet currentMaterialSet = VK_NULL_HANDLE;
|
||||
|
||||
// Phase 2.1: Push constants now carry per-batch data only; per-instance data is in instance SSBO.
|
||||
// Push constants now carry per-batch data only; per-instance data is in instance SSBO.
|
||||
struct M2PushConstants {
|
||||
int32_t texCoordSet; // UV set index (0 or 1)
|
||||
int32_t isFoliage; // Foliage wind animation flag
|
||||
|
|
@ -2734,7 +2734,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
currentPipeline = opaquePipeline_;
|
||||
|
||||
// Bind dummy bone set (set 2) so non-animated draws have a valid binding.
|
||||
// Phase 2.4: Bind mega bone SSBO instead — all instances index into one buffer via boneBase.
|
||||
// Bind mega bone SSBO instead — all instances index into one buffer via boneBase.
|
||||
if (megaBoneSet_[frameIndex]) {
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
pipelineLayout_, 2, 1, &megaBoneSet_[frameIndex], 0, nullptr);
|
||||
|
|
@ -2743,18 +2743,18 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
pipelineLayout_, 2, 1, &dummyBoneSet_, 0, nullptr);
|
||||
}
|
||||
|
||||
// Phase 2.1: Bind instance data SSBO (set 3) — per-instance transforms, fade, bones
|
||||
// Bind instance data SSBO (set 3) — per-instance transforms, fade, bones
|
||||
if (instanceSet_[frameIndex]) {
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
pipelineLayout_, 3, 1, &instanceSet_[frameIndex], 0, nullptr);
|
||||
}
|
||||
|
||||
// Phase 2.1: Reset instance SSBO write cursor for this frame
|
||||
// Reset instance SSBO write cursor for this frame
|
||||
instanceDataCount_ = 0;
|
||||
auto* instSSBO = static_cast<M2InstanceGPU*>(instanceMapped_[frameIndex]);
|
||||
|
||||
// =====================================================================
|
||||
// Phase 2.1: Opaque pass — instanced draws grouped by (modelId, LOD)
|
||||
// Opaque pass — instanced draws grouped by (modelId, LOD)
|
||||
// =====================================================================
|
||||
// sortedVisible_ is already sorted by modelId so consecutive entries share
|
||||
// the same vertex/index buffer. Within each model group we sub-group by
|
||||
|
|
|
|||
|
|
@ -867,7 +867,7 @@ void Renderer::beginFrame() {
|
|||
// Update per-frame UBO with current camera/lighting state
|
||||
updatePerFrameUBO();
|
||||
|
||||
// --- Off-screen pre-passes (Phase 2.5: render graph) ---
|
||||
// --- Off-screen pre-passes ---
|
||||
// Build frame graph: registers pre-passes as graph nodes with dependencies.
|
||||
// compile() topologically sorts; execute() runs them with auto barriers.
|
||||
buildFrameGraph(nullptr);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Phase 0: Unit test infrastructure using Catch2 v3 (amalgamated)
|
||||
# Unit test infrastructure using Catch2 v3 (amalgamated)
|
||||
|
||||
# Catch2 amalgamated as a library target
|
||||
add_library(catch2_main STATIC
|
||||
|
|
@ -139,7 +139,7 @@ register_test_target(test_frustum)
|
|||
add_executable(test_animation_ids
|
||||
test_animation_ids.cpp
|
||||
${TEST_COMMON_SOURCES}
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/animation_ids.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/animation/animation_ids.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/pipeline/dbc_loader.cpp
|
||||
)
|
||||
target_include_directories(test_animation_ids PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
|
|
@ -148,6 +148,55 @@ target_link_libraries(test_animation_ids PRIVATE catch2_main)
|
|||
add_test(NAME animation_ids COMMAND test_animation_ids)
|
||||
register_test_target(test_animation_ids)
|
||||
|
||||
# ── test_locomotion_fsm ──────────────────────────────────────
|
||||
add_executable(test_locomotion_fsm
|
||||
test_locomotion_fsm.cpp
|
||||
${TEST_COMMON_SOURCES}
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/animation/locomotion_fsm.cpp
|
||||
)
|
||||
target_include_directories(test_locomotion_fsm PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_locomotion_fsm SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_locomotion_fsm PRIVATE catch2_main)
|
||||
add_test(NAME locomotion_fsm COMMAND test_locomotion_fsm)
|
||||
register_test_target(test_locomotion_fsm)
|
||||
|
||||
# ── test_combat_fsm ──────────────────────────────────────────
|
||||
add_executable(test_combat_fsm
|
||||
test_combat_fsm.cpp
|
||||
${TEST_COMMON_SOURCES}
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/animation/combat_fsm.cpp
|
||||
)
|
||||
target_include_directories(test_combat_fsm PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_combat_fsm SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_combat_fsm PRIVATE catch2_main)
|
||||
add_test(NAME combat_fsm COMMAND test_combat_fsm)
|
||||
register_test_target(test_combat_fsm)
|
||||
|
||||
# ── test_activity_fsm ────────────────────────────────────────
|
||||
add_executable(test_activity_fsm
|
||||
test_activity_fsm.cpp
|
||||
${TEST_COMMON_SOURCES}
|
||||
${CMAKE_SOURCE_DIR}/src/rendering/animation/activity_fsm.cpp
|
||||
)
|
||||
target_include_directories(test_activity_fsm PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_activity_fsm SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_activity_fsm PRIVATE catch2_main)
|
||||
add_test(NAME activity_fsm COMMAND test_activity_fsm)
|
||||
register_test_target(test_activity_fsm)
|
||||
|
||||
# NPC animator tests removed — NpcAnimator replaced by generic CharacterAnimator
|
||||
|
||||
# ── test_anim_capability ─────────────────────────────────────
|
||||
# Header-only struct tests — no source files needed
|
||||
add_executable(test_anim_capability
|
||||
test_anim_capability.cpp
|
||||
)
|
||||
target_include_directories(test_anim_capability PRIVATE ${TEST_INCLUDE_DIRS})
|
||||
target_include_directories(test_anim_capability SYSTEM PRIVATE ${TEST_SYSTEM_INCLUDE_DIRS})
|
||||
target_link_libraries(test_anim_capability PRIVATE catch2_main)
|
||||
add_test(NAME anim_capability COMMAND test_anim_capability)
|
||||
register_test_target(test_anim_capability)
|
||||
|
||||
# ── ASAN / UBSan for test targets ────────────────────────────
|
||||
if(WOWEE_ENABLE_ASAN AND NOT MSVC)
|
||||
foreach(_t IN LISTS ALL_TEST_TARGETS)
|
||||
|
|
|
|||
112
tests/test_activity_fsm.cpp
Normal file
112
tests/test_activity_fsm.cpp
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// ActivityFSM unit tests
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "rendering/animation/activity_fsm.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
|
||||
using namespace wowee::rendering;
|
||||
namespace anim = wowee::rendering::anim;
|
||||
|
||||
static AnimCapabilitySet makeActivityCaps() {
|
||||
AnimCapabilitySet caps;
|
||||
caps.resolvedStand = anim::STAND;
|
||||
caps.resolvedSitDown = anim::SIT_GROUND_DOWN;
|
||||
caps.resolvedSitLoop = anim::SITTING;
|
||||
caps.resolvedSitUp = anim::SIT_GROUND_UP;
|
||||
caps.resolvedKneel = anim::KNEEL_LOOP;
|
||||
caps.resolvedLoot = anim::LOOT;
|
||||
return caps;
|
||||
}
|
||||
|
||||
static ActivityFSM::Input idleInput() {
|
||||
ActivityFSM::Input in;
|
||||
in.grounded = true;
|
||||
return in;
|
||||
}
|
||||
|
||||
TEST_CASE("ActivityFSM: NONE by default", "[activity]") {
|
||||
ActivityFSM fsm;
|
||||
CHECK(fsm.getState() == ActivityFSM::State::NONE);
|
||||
CHECK_FALSE(fsm.isActive());
|
||||
}
|
||||
|
||||
TEST_CASE("ActivityFSM: emote starts and returns valid output", "[activity]") {
|
||||
ActivityFSM fsm;
|
||||
auto caps = makeActivityCaps();
|
||||
|
||||
fsm.startEmote(anim::EMOTE_WAVE, false);
|
||||
CHECK(fsm.getState() == ActivityFSM::State::EMOTE);
|
||||
CHECK(fsm.isEmoteActive());
|
||||
|
||||
auto in = idleInput();
|
||||
auto out = fsm.resolve(in, caps);
|
||||
REQUIRE(out.valid);
|
||||
CHECK(out.animId == anim::EMOTE_WAVE);
|
||||
CHECK(out.loop == false);
|
||||
}
|
||||
|
||||
TEST_CASE("ActivityFSM: emote cancelled by movement", "[activity]") {
|
||||
ActivityFSM fsm;
|
||||
auto caps = makeActivityCaps();
|
||||
|
||||
fsm.startEmote(anim::EMOTE_WAVE, false);
|
||||
|
||||
auto in = idleInput();
|
||||
in.moving = true;
|
||||
auto out = fsm.resolve(in, caps);
|
||||
|
||||
// After movement, emote should be cancelled
|
||||
CHECK_FALSE(fsm.isEmoteActive());
|
||||
CHECK(fsm.getState() == ActivityFSM::State::NONE);
|
||||
}
|
||||
|
||||
TEST_CASE("ActivityFSM: looting starts and stops", "[activity]") {
|
||||
ActivityFSM fsm;
|
||||
auto caps = makeActivityCaps();
|
||||
|
||||
fsm.startLooting();
|
||||
CHECK(fsm.getState() == ActivityFSM::State::LOOTING);
|
||||
|
||||
auto in = idleInput();
|
||||
auto out = fsm.resolve(in, caps);
|
||||
REQUIRE(out.valid);
|
||||
|
||||
// stopLooting transitions through LOOT_END (KNEEL_END one-shot) before NONE
|
||||
fsm.stopLooting();
|
||||
CHECK(fsm.getState() == ActivityFSM::State::LOOT_END);
|
||||
}
|
||||
|
||||
TEST_CASE("ActivityFSM: sit stand state triggers sit down", "[activity]") {
|
||||
ActivityFSM fsm;
|
||||
auto caps = makeActivityCaps();
|
||||
|
||||
fsm.setStandState(ActivityFSM::STAND_STATE_SIT);
|
||||
CHECK(fsm.getState() == ActivityFSM::State::SIT_DOWN);
|
||||
|
||||
auto in = idleInput();
|
||||
in.sitting = true;
|
||||
auto out = fsm.resolve(in, caps);
|
||||
REQUIRE(out.valid);
|
||||
}
|
||||
|
||||
TEST_CASE("ActivityFSM: cancel emote explicitly", "[activity]") {
|
||||
ActivityFSM fsm;
|
||||
auto caps = makeActivityCaps();
|
||||
|
||||
fsm.startEmote(anim::EMOTE_DANCE, true);
|
||||
CHECK(fsm.isEmoteActive());
|
||||
CHECK(fsm.getEmoteAnimId() == anim::EMOTE_DANCE);
|
||||
|
||||
fsm.cancelEmote();
|
||||
CHECK_FALSE(fsm.isEmoteActive());
|
||||
CHECK(fsm.getState() == ActivityFSM::State::NONE);
|
||||
}
|
||||
|
||||
TEST_CASE("ActivityFSM: reset clears all state", "[activity]") {
|
||||
ActivityFSM fsm;
|
||||
fsm.startEmote(anim::EMOTE_WAVE, false);
|
||||
CHECK(fsm.isActive());
|
||||
|
||||
fsm.reset();
|
||||
CHECK(fsm.getState() == ActivityFSM::State::NONE);
|
||||
CHECK_FALSE(fsm.isActive());
|
||||
}
|
||||
56
tests/test_anim_capability.cpp
Normal file
56
tests/test_anim_capability.cpp
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// AnimCapabilitySet unit tests
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "rendering/animation/anim_capability_set.hpp"
|
||||
|
||||
using namespace wowee::rendering;
|
||||
|
||||
TEST_CASE("AnimCapabilitySet: default-constructed has all zero IDs", "[capability]") {
|
||||
AnimCapabilitySet caps;
|
||||
|
||||
CHECK(caps.resolvedStand == 0);
|
||||
CHECK(caps.resolvedWalk == 0);
|
||||
CHECK(caps.resolvedRun == 0);
|
||||
CHECK(caps.resolvedSprint == 0);
|
||||
CHECK(caps.resolvedJumpStart == 0);
|
||||
CHECK(caps.resolvedJump == 0);
|
||||
CHECK(caps.resolvedJumpEnd == 0);
|
||||
CHECK(caps.resolvedSwimIdle == 0);
|
||||
CHECK(caps.resolvedSwim == 0);
|
||||
CHECK(caps.resolvedCombatIdle == 0);
|
||||
CHECK(caps.resolvedMelee1H == 0);
|
||||
CHECK(caps.resolvedMelee2H == 0);
|
||||
CHECK(caps.resolvedStun == 0);
|
||||
CHECK(caps.resolvedMount == 0);
|
||||
CHECK(caps.resolvedStealthIdle == 0);
|
||||
CHECK(caps.resolvedLoot == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("AnimCapabilitySet: default-constructed has all flags false", "[capability]") {
|
||||
AnimCapabilitySet caps;
|
||||
|
||||
CHECK_FALSE(caps.hasStand);
|
||||
CHECK_FALSE(caps.hasWalk);
|
||||
CHECK_FALSE(caps.hasRun);
|
||||
CHECK_FALSE(caps.hasSprint);
|
||||
CHECK_FALSE(caps.hasWalkBackwards);
|
||||
CHECK_FALSE(caps.hasJump);
|
||||
CHECK_FALSE(caps.hasSwim);
|
||||
CHECK_FALSE(caps.hasMelee);
|
||||
CHECK_FALSE(caps.hasStealth);
|
||||
CHECK_FALSE(caps.hasDeath);
|
||||
CHECK_FALSE(caps.hasMount);
|
||||
}
|
||||
|
||||
TEST_CASE("AnimOutput::ok creates valid output", "[capability]") {
|
||||
auto out = AnimOutput::ok(42, true);
|
||||
CHECK(out.valid);
|
||||
CHECK(out.animId == 42);
|
||||
CHECK(out.loop == true);
|
||||
}
|
||||
|
||||
TEST_CASE("AnimOutput::stay creates invalid output", "[capability]") {
|
||||
auto out = AnimOutput::stay();
|
||||
CHECK_FALSE(out.valid);
|
||||
CHECK(out.animId == 0);
|
||||
CHECK(out.loop == false);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// Animation ID validation tests — covers nameFromId() and validateAgainstDBC()
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "rendering/animation_ids.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
#include "pipeline/dbc_loader.hpp"
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
|
|
|||
125
tests/test_combat_fsm.cpp
Normal file
125
tests/test_combat_fsm.cpp
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// CombatFSM unit tests
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "rendering/animation/combat_fsm.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
|
||||
using namespace wowee::rendering;
|
||||
namespace anim = wowee::rendering::anim;
|
||||
|
||||
static AnimCapabilitySet makeCombatCaps() {
|
||||
AnimCapabilitySet caps;
|
||||
caps.resolvedStand = anim::STAND;
|
||||
caps.resolvedCombatIdle = anim::READY_UNARMED;
|
||||
caps.resolvedMelee1H = anim::ATTACK_1H;
|
||||
caps.resolvedMelee2H = anim::ATTACK_2H;
|
||||
caps.resolvedMeleeUnarmed = anim::ATTACK_UNARMED;
|
||||
caps.resolvedStun = anim::STUN;
|
||||
caps.resolvedUnsheathe = anim::UNSHEATHE;
|
||||
caps.resolvedSheathe = anim::SHEATHE;
|
||||
caps.hasMelee = true;
|
||||
return caps;
|
||||
}
|
||||
|
||||
static CombatFSM::Input combatInput() {
|
||||
CombatFSM::Input in;
|
||||
in.inCombat = true;
|
||||
in.grounded = true;
|
||||
return in;
|
||||
}
|
||||
|
||||
static WeaponLoadout unarmedLoadout() {
|
||||
return WeaponLoadout{}; // Default is unarmed (inventoryType=0)
|
||||
}
|
||||
|
||||
TEST_CASE("CombatFSM: INACTIVE by default", "[combat]") {
|
||||
CombatFSM fsm;
|
||||
CHECK(fsm.getState() == CombatFSM::State::INACTIVE);
|
||||
CHECK_FALSE(fsm.isActive());
|
||||
}
|
||||
|
||||
TEST_CASE("CombatFSM: INACTIVE → UNSHEATHE on COMBAT_ENTER event", "[combat]") {
|
||||
CombatFSM fsm;
|
||||
fsm.onEvent(AnimEvent::COMBAT_ENTER);
|
||||
CHECK(fsm.getState() == CombatFSM::State::UNSHEATHE);
|
||||
CHECK(fsm.isActive());
|
||||
}
|
||||
|
||||
TEST_CASE("CombatFSM: stun overrides active combat", "[combat]") {
|
||||
CombatFSM fsm;
|
||||
auto caps = makeCombatCaps();
|
||||
auto wl = unarmedLoadout();
|
||||
|
||||
// Enter combat
|
||||
fsm.onEvent(AnimEvent::COMBAT_ENTER);
|
||||
|
||||
// Stun
|
||||
fsm.setStunned(true);
|
||||
auto in = combatInput();
|
||||
auto out = fsm.resolve(in, caps, wl);
|
||||
|
||||
CHECK(fsm.isStunned());
|
||||
REQUIRE(out.valid);
|
||||
CHECK(out.animId == anim::STUN);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatFSM: stun does not override swimming", "[combat]") {
|
||||
CombatFSM fsm;
|
||||
auto caps = makeCombatCaps();
|
||||
auto wl = unarmedLoadout();
|
||||
|
||||
fsm.setStunned(true);
|
||||
auto in = combatInput();
|
||||
in.swimming = true;
|
||||
auto out = fsm.resolve(in, caps, wl);
|
||||
|
||||
// Swimming overrides combat entirely — FSM should go inactive
|
||||
// The exact behavior depends on implementation, but stun should not
|
||||
// force an animation while swimming
|
||||
CHECK(fsm.getState() != CombatFSM::State::STUNNED);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatFSM: spell cast sequence", "[combat]") {
|
||||
CombatFSM fsm;
|
||||
auto caps = makeCombatCaps();
|
||||
auto wl = unarmedLoadout();
|
||||
|
||||
fsm.startSpellCast(anim::SPELL_PRECAST, anim::SPELL, true, anim::SPELL_CAST);
|
||||
CHECK(fsm.getState() == CombatFSM::State::SPELL_PRECAST);
|
||||
|
||||
auto in = combatInput();
|
||||
auto out = fsm.resolve(in, caps, wl);
|
||||
REQUIRE(out.valid);
|
||||
CHECK(out.animId == anim::SPELL_PRECAST);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatFSM: stopSpellCast transitions to finalize", "[combat]") {
|
||||
CombatFSM fsm;
|
||||
auto caps = makeCombatCaps();
|
||||
auto wl = unarmedLoadout();
|
||||
|
||||
fsm.startSpellCast(0, anim::SPELL, true, anim::SPELL_CAST);
|
||||
// Skip precast (animId=0), go to casting
|
||||
fsm.setState(CombatFSM::State::SPELL_CASTING);
|
||||
|
||||
fsm.stopSpellCast();
|
||||
CHECK(fsm.getState() == CombatFSM::State::SPELL_FINALIZE);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatFSM: hit reaction", "[combat]") {
|
||||
CombatFSM fsm;
|
||||
auto caps = makeCombatCaps();
|
||||
auto wl = unarmedLoadout();
|
||||
|
||||
fsm.triggerHitReaction(anim::COMBAT_WOUND);
|
||||
CHECK(fsm.getState() == CombatFSM::State::HIT_REACTION);
|
||||
}
|
||||
|
||||
TEST_CASE("CombatFSM: reset clears all state", "[combat]") {
|
||||
CombatFSM fsm;
|
||||
fsm.onEvent(AnimEvent::COMBAT_ENTER);
|
||||
fsm.setStunned(true);
|
||||
|
||||
fsm.reset();
|
||||
CHECK(fsm.getState() == CombatFSM::State::INACTIVE);
|
||||
CHECK_FALSE(fsm.isStunned());
|
||||
}
|
||||
167
tests/test_locomotion_fsm.cpp
Normal file
167
tests/test_locomotion_fsm.cpp
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// LocomotionFSM unit tests
|
||||
#include <catch_amalgamated.hpp>
|
||||
#include "rendering/animation/locomotion_fsm.hpp"
|
||||
#include "rendering/animation/animation_ids.hpp"
|
||||
|
||||
using namespace wowee::rendering;
|
||||
namespace anim = wowee::rendering::anim;
|
||||
|
||||
// Helper: create a capability set with basic locomotion resolved
|
||||
static AnimCapabilitySet makeLocoCaps() {
|
||||
AnimCapabilitySet caps;
|
||||
caps.resolvedStand = anim::STAND;
|
||||
caps.resolvedWalk = anim::WALK;
|
||||
caps.resolvedRun = anim::RUN;
|
||||
caps.resolvedSprint = anim::SPRINT;
|
||||
caps.resolvedWalkBackwards = anim::WALK_BACKWARDS;
|
||||
caps.resolvedStrafeLeft = anim::SHUFFLE_LEFT;
|
||||
caps.resolvedStrafeRight = anim::SHUFFLE_RIGHT;
|
||||
caps.resolvedJumpStart = anim::JUMP_START;
|
||||
caps.resolvedJump = anim::JUMP;
|
||||
caps.resolvedJumpEnd = anim::JUMP_END;
|
||||
caps.resolvedSwimIdle = anim::SWIM_IDLE;
|
||||
caps.resolvedSwim = anim::SWIM;
|
||||
caps.hasStand = true;
|
||||
caps.hasWalk = true;
|
||||
caps.hasRun = true;
|
||||
caps.hasSprint = true;
|
||||
caps.hasWalkBackwards = true;
|
||||
caps.hasJump = true;
|
||||
caps.hasSwim = true;
|
||||
return caps;
|
||||
}
|
||||
|
||||
static LocomotionFSM::Input idle() {
|
||||
LocomotionFSM::Input in;
|
||||
in.deltaTime = 0.016f;
|
||||
return in;
|
||||
}
|
||||
|
||||
TEST_CASE("LocomotionFSM: IDLE → WALK on move start (non-sprinting)", "[locomotion]") {
|
||||
LocomotionFSM fsm;
|
||||
auto caps = makeLocoCaps();
|
||||
|
||||
auto in = idle();
|
||||
in.moving = true;
|
||||
auto out = fsm.resolve(in, caps);
|
||||
|
||||
REQUIRE(out.valid);
|
||||
REQUIRE(out.animId == anim::WALK);
|
||||
REQUIRE(out.loop == true);
|
||||
CHECK(fsm.getState() == LocomotionFSM::State::WALK);
|
||||
}
|
||||
|
||||
TEST_CASE("LocomotionFSM: IDLE → RUN on move start (sprinting)", "[locomotion]") {
|
||||
LocomotionFSM fsm;
|
||||
auto caps = makeLocoCaps();
|
||||
|
||||
auto in = idle();
|
||||
in.moving = true;
|
||||
in.sprinting = true;
|
||||
auto out = fsm.resolve(in, caps);
|
||||
|
||||
REQUIRE(out.valid);
|
||||
REQUIRE(out.animId == anim::RUN);
|
||||
CHECK(fsm.getState() == LocomotionFSM::State::RUN);
|
||||
}
|
||||
|
||||
TEST_CASE("LocomotionFSM: WALK → IDLE on move stop (after grace)", "[locomotion]") {
|
||||
LocomotionFSM fsm;
|
||||
auto caps = makeLocoCaps();
|
||||
|
||||
// Start walking
|
||||
auto in = idle();
|
||||
in.moving = true;
|
||||
fsm.resolve(in, caps);
|
||||
CHECK(fsm.getState() == LocomotionFSM::State::WALK);
|
||||
|
||||
// Stop moving — grace timer keeps walk for a bit
|
||||
in.moving = false;
|
||||
in.deltaTime = 0.2f; // > grace period (0.12s)
|
||||
auto out = fsm.resolve(in, caps);
|
||||
|
||||
CHECK(fsm.getState() == LocomotionFSM::State::IDLE);
|
||||
}
|
||||
|
||||
TEST_CASE("LocomotionFSM: WALK → JUMP_START on jump", "[locomotion]") {
|
||||
LocomotionFSM fsm;
|
||||
auto caps = makeLocoCaps();
|
||||
|
||||
// Start walking
|
||||
auto in = idle();
|
||||
in.moving = true;
|
||||
fsm.resolve(in, caps);
|
||||
|
||||
// Jump
|
||||
in.jumping = true;
|
||||
in.grounded = false;
|
||||
auto out = fsm.resolve(in, caps);
|
||||
|
||||
REQUIRE(out.valid);
|
||||
REQUIRE(out.animId == anim::JUMP_START);
|
||||
CHECK(fsm.getState() == LocomotionFSM::State::JUMP_START);
|
||||
}
|
||||
|
||||
TEST_CASE("LocomotionFSM: SWIM_IDLE → SWIM on move start while swimming", "[locomotion]") {
|
||||
LocomotionFSM fsm;
|
||||
auto caps = makeLocoCaps();
|
||||
|
||||
// Enter swim idle
|
||||
auto in = idle();
|
||||
in.swimming = true;
|
||||
fsm.resolve(in, caps);
|
||||
CHECK(fsm.getState() == LocomotionFSM::State::SWIM_IDLE);
|
||||
|
||||
// Start swimming
|
||||
in.moving = true;
|
||||
auto out = fsm.resolve(in, caps);
|
||||
|
||||
REQUIRE(out.valid);
|
||||
REQUIRE(out.animId == anim::SWIM);
|
||||
CHECK(fsm.getState() == LocomotionFSM::State::SWIM);
|
||||
}
|
||||
|
||||
TEST_CASE("LocomotionFSM: backward walking resolves WALK_BACKWARDS", "[locomotion]") {
|
||||
LocomotionFSM fsm;
|
||||
auto caps = makeLocoCaps();
|
||||
|
||||
auto in = idle();
|
||||
in.moving = true;
|
||||
in.movingBackward = true;
|
||||
auto out = fsm.resolve(in, caps);
|
||||
|
||||
REQUIRE(out.valid);
|
||||
// Should use WALK_BACKWARDS when available
|
||||
CHECK(out.animId == anim::WALK_BACKWARDS);
|
||||
}
|
||||
|
||||
TEST_CASE("LocomotionFSM: STAY when WALK_BACKWARDS missing from caps", "[locomotion]") {
|
||||
LocomotionFSM fsm;
|
||||
AnimCapabilitySet caps;
|
||||
caps.resolvedStand = anim::STAND;
|
||||
caps.resolvedWalk = anim::WALK;
|
||||
caps.hasStand = true;
|
||||
caps.hasWalk = true;
|
||||
// No WALK_BACKWARDS in caps
|
||||
|
||||
auto in = idle();
|
||||
in.moving = true;
|
||||
in.movingBackward = true;
|
||||
auto out = fsm.resolve(in, caps);
|
||||
|
||||
// Should still resolve something — falls back to walk
|
||||
REQUIRE(out.valid);
|
||||
}
|
||||
|
||||
TEST_CASE("LocomotionFSM: reset restores IDLE", "[locomotion]") {
|
||||
LocomotionFSM fsm;
|
||||
auto caps = makeLocoCaps();
|
||||
|
||||
auto in = idle();
|
||||
in.moving = true;
|
||||
fsm.resolve(in, caps);
|
||||
CHECK(fsm.getState() == LocomotionFSM::State::WALK);
|
||||
|
||||
fsm.reset();
|
||||
CHECK(fsm.getState() == LocomotionFSM::State::IDLE);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue