change weapon for ranged skills

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
This commit is contained in:
Pavel Okhlopkov 2026-04-10 23:01:16 +03:00
parent fe1dc5e02b
commit 6ba0edc2fb
6 changed files with 176 additions and 4 deletions

View file

@ -74,6 +74,10 @@ public:
bool isWeaponsSheathed() const { return weaponsSheathed_; }
void toggleWeaponsSheathed() { weaponsSheathed_ = !weaponsSheathed_; }
// Ranged weapon swap: temporarily show ranged weapon in right hand
void showRangedWeapon(bool show);
bool isShowingRanged() const { return showingRanged_; }
// Saved skin state accessors (used by game_screen.cpp for equipment re-compositing)
const std::string& getBodySkinPath() const { return bodySkinPath_; }
const std::vector<std::string>& getUnderwearPaths() const { return underwearPaths_; }
@ -96,6 +100,7 @@ private:
uint32_t cloakTextureSlotIndex_ = 0;
bool weaponsSheathed_ = false;
bool showingRanged_ = false;
};
} // namespace core

View file

@ -943,6 +943,10 @@ public:
using MeleeSwingCallback = std::function<void(uint32_t spellId)>;
void setMeleeSwingCallback(MeleeSwingCallback cb) { meleeSwingCallback_ = std::move(cb); }
// Ranged weapon swap callback — show=true: swap to ranged weapon, false: back to melee
using RangedWeaponSwapCallback = std::function<void(bool show)>;
void setRangedWeaponSwapCallback(RangedWeaponSwapCallback cb) { rangedWeaponSwapCallback_ = std::move(cb); }
// Spell cast animation callbacks — true=start cast/channel, false=finish/cancel
// guid: caster (may be player or another unit), isChannel: channel vs regular cast
// castType: DIRECTED (unit target), OMNI (self/no target), AREA (ground AoE)
@ -2399,6 +2403,13 @@ public:
auto& knockBackCallbackRef() { return knockBackCallback_; }
auto& lootWindowCallbackRef() { return lootWindowCallback_; }
auto& meleeSwingCallbackRef() { return meleeSwingCallback_; }
auto& rangedWeaponSwapCallbackRef() { return rangedWeaponSwapCallback_; }
void suppressNextMeleeSwingAnim() { suppressMeleeSwingAnim_ = true; }
bool consumeSuppressMeleeSwingAnim() {
bool v = suppressMeleeSwingAnim_;
suppressMeleeSwingAnim_ = false;
return v;
}
auto& mountCallbackRef() { return mountCallback_; }
auto& npcAggroCallbackRef() { return npcAggroCallback_; }
auto& npcDeathCallbackRef() { return npcDeathCallback_; }
@ -3436,6 +3447,8 @@ private:
AppearanceChangedCallback appearanceChangedCallback_;
GhostStateCallback ghostStateCallback_;
MeleeSwingCallback meleeSwingCallback_;
RangedWeaponSwapCallback rangedWeaponSwapCallback_;
bool suppressMeleeSwingAnim_ = false;
// lastMeleeSwingMs_ moved to CombatHandler
SpellCastAnimCallback spellCastAnimCallback_;
SpellCastFailedCallback spellCastFailedCallback_;

View file

@ -324,6 +324,7 @@ bool AppearanceComposer::loadWeaponM2(const std::string& m2Path, pipeline::M2Mod
}
void AppearanceComposer::loadEquippedWeapons() {
showingRanged_ = false;
if (!renderer_ || !renderer_->getCharacterRenderer() || !assetManager_ || !assetManager_->isInitialized())
return;
if (!gameHandler_) return;
@ -354,9 +355,12 @@ void AppearanceComposer::loadEquippedWeapons() {
for (const auto& ws : weaponSlots) {
charRenderer->detachWeapon(charInstanceId, ws.attachmentId);
}
charRenderer->detachWeapon(charInstanceId, 1); // ranged may also use right hand
return;
}
bool rightHandFilled = false;
for (const auto& ws : weaponSlots) {
const auto& equipSlot = inventory.getEquipSlot(ws.slot);
@ -421,8 +425,123 @@ void AppearanceComposer::loadEquippedWeapons() {
weaponModel, weaponModelId, texturePath);
if (ok) {
LOG_INFO("Equipped weapon: ", m2Path, " at attachment ", ws.attachmentId);
if (ws.attachmentId == 1) rightHandFilled = true;
}
}
// --- RANGED slot (bow, gun, crossbow, thrown) ---
// Show ranged weapon in right hand when main hand is empty.
const auto& rangedSlot = inventory.getEquipSlot(game::EquipSlot::RANGED);
if (!rightHandFilled && !rangedSlot.empty() && rangedSlot.item.displayInfoId != 0) {
uint32_t displayInfoId = rangedSlot.item.displayInfoId;
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
if (recIdx >= 0) {
const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
std::string modelName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModel"] : 1);
std::string textureName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModelTexture"] : 3);
if (!modelName.empty()) {
std::string modelFile = modelName;
{
size_t dotPos = modelFile.rfind('.');
if (dotPos != std::string::npos) {
modelFile = modelFile.substr(0, dotPos) + ".m2";
} else {
modelFile += ".m2";
}
}
std::string m2Path = "Item\\ObjectComponents\\Weapon\\" + modelFile;
pipeline::M2Model weaponModel;
if (!loadWeaponM2(m2Path, weaponModel)) {
m2Path = "Item\\ObjectComponents\\Shield\\" + modelFile;
loadWeaponM2(m2Path, weaponModel);
}
if (weaponModel.vertices.size() > 0) {
std::string texturePath;
if (!textureName.empty()) {
texturePath = "Item\\ObjectComponents\\Weapon\\" + textureName + ".blp";
if (!assetManager_->fileExists(texturePath)) {
texturePath = "Item\\ObjectComponents\\Shield\\" + textureName + ".blp";
}
}
uint32_t weaponModelId = entitySpawner_->allocateWeaponModelId();
bool ok = charRenderer->attachWeapon(charInstanceId, 1,
weaponModel, weaponModelId, texturePath);
if (ok) {
LOG_INFO("Equipped ranged weapon: ", m2Path, " at attachment 1 (right hand)");
}
}
}
}
}
}
void AppearanceComposer::showRangedWeapon(bool show) {
if (show == showingRanged_) return;
showingRanged_ = show;
if (!renderer_ || !renderer_->getCharacterRenderer() || !gameHandler_ || !assetManager_ || !assetManager_->isInitialized())
return;
auto* charRenderer = renderer_->getCharacterRenderer();
uint32_t charInstanceId = renderer_->getCharacterInstanceId();
if (charInstanceId == 0) return;
if (!show) {
// Swap back to normal melee weapons
loadEquippedWeapons();
return;
}
auto& inventory = gameHandler_->getInventory();
const auto& rangedSlot = inventory.getEquipSlot(game::EquipSlot::RANGED);
if (rangedSlot.empty() || rangedSlot.item.displayInfoId == 0) return;
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
if (!displayInfoDbc) return;
uint32_t displayInfoId = rangedSlot.item.displayInfoId;
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
if (recIdx < 0) return;
const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
std::string modelName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModel"] : 1);
std::string textureName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModelTexture"] : 3);
if (modelName.empty()) return;
std::string modelFile = modelName;
{
size_t dotPos = modelFile.rfind('.');
if (dotPos != std::string::npos)
modelFile = modelFile.substr(0, dotPos) + ".m2";
else
modelFile += ".m2";
}
std::string m2Path = "Item\\ObjectComponents\\Weapon\\" + modelFile;
pipeline::M2Model weaponModel;
if (!loadWeaponM2(m2Path, weaponModel)) {
m2Path = "Item\\ObjectComponents\\Shield\\" + modelFile;
if (!loadWeaponM2(m2Path, weaponModel)) return;
}
std::string texturePath;
if (!textureName.empty()) {
texturePath = "Item\\ObjectComponents\\Weapon\\" + textureName + ".blp";
if (!assetManager_->fileExists(texturePath))
texturePath = "Item\\ObjectComponents\\Shield\\" + textureName + ".blp";
}
// Detach current right-hand weapon and attach ranged weapon
charRenderer->detachWeapon(charInstanceId, 1);
uint32_t weaponModelId = entitySpawner_->allocateWeaponModelId();
bool ok = charRenderer->attachWeapon(charInstanceId, 1, weaponModel, weaponModelId, texturePath);
if (ok) {
LOG_INFO("Swapped to ranged weapon: ", m2Path, " at attachment 1 (right hand)");
}
}
} // namespace core

View file

@ -980,14 +980,23 @@ void Application::setState(AppState newState) {
if (renderer) {
// Ranged auto-attack spells: Auto Shot (75), Shoot (5019), Throw (2764)
if (spellId == 75 || spellId == 5019 || spellId == 2764) {
if (appearanceComposer_ && !appearanceComposer_->isShowingRanged())
appearanceComposer_->showRangedWeapon(true);
if (auto* ac = renderer->getAnimationController()) ac->triggerRangedShot();
} else if (spellId != 0) {
if (appearanceComposer_ && appearanceComposer_->isShowingRanged())
appearanceComposer_->showRangedWeapon(false);
if (auto* ac = renderer->getAnimationController()) ac->triggerSpecialAttack(spellId);
} else {
if (appearanceComposer_ && appearanceComposer_->isShowingRanged())
appearanceComposer_->showRangedWeapon(false);
if (auto* ac = renderer->getAnimationController()) ac->triggerMeleeSwing();
}
}
});
gameHandler->setRangedWeaponSwapCallback([this](bool show) {
if (appearanceComposer_) appearanceComposer_->showRangedWeapon(show);
});
gameHandler->setKnockBackCallback([this](float vcos, float vsin, float hspeed, float vspeed) {
if (renderer && renderer->getCameraController()) {
renderer->getCameraController()->applyKnockBack(vcos, vsin, hspeed, vspeed);
@ -1216,6 +1225,10 @@ void Application::update(float deltaTime) {
appearanceComposer_->setWeaponsSheathed(false);
appearanceComposer_->loadEquippedWeapons();
}
// Swap back to melee weapon when auto-attack stops
if (!autoAttacking && wasAutoAttacking_ && appearanceComposer_ && appearanceComposer_->isShowingRanged()) {
appearanceComposer_->showRangedWeapon(false);
}
wasAutoAttacking_ = autoAttacking;
}

View file

@ -206,14 +206,21 @@ void CombatHandler::startAutoAttack(uint64_t targetGuid) {
owner_.dismount();
}
// Client-side melee range gate to avoid starting "swing forever" loops when
// Client-side range gate to avoid starting "swing forever" loops when
// target is already clearly out of range.
if (auto target = owner_.getEntityManager().getEntity(targetGuid)) {
float dx = owner_.movementInfoRef().x - target->getLatestX();
float dy = owner_.movementInfoRef().y - target->getLatestY();
float dz = owner_.movementInfoRef().z - target->getLatestZ();
float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz);
if (dist3d > 8.0f) {
// Use longer range limit when a ranged weapon is equipped
const auto& rangedSlot = owner_.getInventory().getEquipSlot(game::EquipSlot::RANGED);
bool hasRangedWeapon = !rangedSlot.empty() &&
(rangedSlot.item.inventoryType == game::InvType::RANGED_BOW ||
rangedSlot.item.inventoryType == game::InvType::RANGED_GUN ||
rangedSlot.item.inventoryType == game::InvType::THROWN);
float maxRange = hasRangedWeapon ? 40.0f : 8.0f;
if (dist3d > maxRange) {
if (autoAttackRangeWarnCooldown_ <= 0.0f) {
owner_.addSystemChatMessage("Target is too far away.");
autoAttackRangeWarnCooldown_ = 1.25f;
@ -443,7 +450,14 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
lastMeleeSwingMs_ = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count());
if (owner_.meleeSwingCallbackRef()) owner_.meleeSwingCallbackRef()(0);
// Skip melee animation if a ranged shot was just triggered from
// SMSG_SPELL_GO (Auto Shot / Shoot / Throw). The ranged animation
// is already playing; firing the melee callback here would override it.
if (owner_.consumeSuppressMeleeSwingAnim()) {
LOG_DEBUG("Suppressed melee swing anim — ranged shot already triggered");
} else {
if (owner_.meleeSwingCallbackRef()) owner_.meleeSwingCallbackRef()(0);
}
}
if (!isPlayerAttacker && owner_.npcSwingCallbackRef()) {
owner_.npcSwingCallbackRef()(data.attackerGuid);

View file

@ -1025,8 +1025,16 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
if (!owner_.isProfessionSpell(data.spellId))
playSpellCastSound(data.spellId);
// Instant melee abilities → trigger attack animation
// Ranged auto-attack spells (Auto Shot, Shoot, Throw) complete as timed
// casts and are NOT classified as instant melee abilities, so trigger the
// ranged shot animation explicitly here.
uint32_t sid = data.spellId;
if (sid == 75 || sid == 5019 || sid == 2764) {
if (owner_.meleeSwingCallbackRef()) owner_.meleeSwingCallbackRef()(sid);
owner_.suppressNextMeleeSwingAnim();
}
// Instant melee abilities → trigger attack animation
bool isMeleeAbility = false;
if (!owner_.isProfessionSpell(sid)) {
owner_.loadSpellNameCache();