A friendly or resurrection spell that binds nothing now enters the unit targeting cursor, as `TryCast` does for any residual word without the enemy bit (`0x6e507b`–`0x6e50ab`). `autoSelfCast` boots at its reference default of `"0"`, and its `Deviates` row is gone. - **Binding.** The cursor binds a unit through a world click, and through `SpellTargetUnit` from the unit frames (`BindTarget 0x6e5b40`). The selection never moves. - **Relation checks.** Heals and buffs ask `CanAssist 0x6066f0` instead of reaction alone, so a friendly NPC without the PvP flag is no longer assistable. That is part of #50. - **Range.** `SpellCanTargetUnit` and the cursor answer per unit through `0x6e6460`'s unit leg, including the spell's min/max range (`6e677c`–`6e6802`). A click on a unit out of range raises "Out of range." or "Target too close" and keeps the cursor armed. The reference's abort there also strands the cursor, and that is not copied. - **Your own model** joins the world pick only for a word `0x6e61a0` admits (`0x850e`, no `AttributesEx 0x80000`), and never in first person. Fixes #136 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-Authored-By: Sam <samuv.eth@gmail.com>
86 lines
3.8 KiB
Rust
86 lines
3.8 KiB
Rust
//! The spell, the reference's `Spell_C`: the cast ladder and commit ([`cast_send`], `TryCast
|
|
//! 0x6e4b60` and `SendCast 0x6e54f0`), the target bind ([`cast_target`], `ArmCast 0x6e5250` and
|
|
//! `BindTarget 0x6e5b40`), the requirement validator ([`validator`], `0x6094f0`), the usable walk
|
|
//! ([`usable`], `IsSpellUsableNow 0x6e3d60`), the targeting cursor ([`targeting`], `0xcecac0`),
|
|
//! the in-flight slot, the cooldowns, the talent modifiers and their packet handlers ([`net`]).
|
|
|
|
use bevy::prelude::*;
|
|
|
|
use crate::char_select::ClientState;
|
|
use crate::ui_script::{UiFeed, UiInput};
|
|
use crate::ui_unit::UnitFeed;
|
|
use benilla_world::schedule::WorldStage;
|
|
|
|
mod cast_send;
|
|
pub(crate) mod cast_target;
|
|
pub(crate) mod cooldowns;
|
|
mod inflight;
|
|
mod mods;
|
|
pub(crate) mod net;
|
|
pub(crate) mod targeting;
|
|
pub(crate) mod usable;
|
|
pub(crate) mod validator;
|
|
|
|
// The one cast path: every caster takes [`CastLadder`] and commits through [`CastCommit`];
|
|
// `send_spell_cast` is private to `cast_send`, so no second send path can exist.
|
|
pub(crate) use cast_send::{CastCommit, CastLadder, TargetedBind};
|
|
pub(crate) use cast_target::AutoSelfCast;
|
|
pub(crate) use cooldowns::Cooldowns;
|
|
pub(crate) use inflight::{
|
|
inflight, ActiveChannel, AutoRepeatActive, LocalMoveStart, PendingCast, QueuedMeleeSpell,
|
|
SPELL_INTERRUPT_MOVEMENT,
|
|
};
|
|
pub(crate) use mods::{SpellModifiers, OP_COST};
|
|
// `TargetingWants` is exported for the ground reticle, which draws for the location word alone.
|
|
pub(crate) use targeting::{ground_cast_radius, PicksSelf, SpellTargeting, TargetingWants};
|
|
|
|
/// The local self-cancel's set: a reader of the in-flight state orders `.after(LocalCancel)` so a
|
|
/// cast ended by a move, jump or Esc drops its cast bar the same frame.
|
|
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub(crate) struct LocalCancel;
|
|
|
|
pub(crate) struct SpellPlugin;
|
|
|
|
impl Plugin for SpellPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.init_resource::<PendingCast>()
|
|
.init_resource::<QueuedMeleeSpell>()
|
|
.init_resource::<ActiveChannel>()
|
|
.init_resource::<LocalMoveStart>()
|
|
.init_resource::<AutoRepeatActive>()
|
|
.init_resource::<Cooldowns>()
|
|
.init_resource::<SpellModifiers>()
|
|
.init_resource::<AutoSelfCast>()
|
|
.init_resource::<SpellTargeting>()
|
|
.init_resource::<targeting::EnchantConfirmItem>()
|
|
.init_resource::<targeting::PicksSelf>()
|
|
.add_observer(cast_target::on_cvar)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
inflight::local_self_cancel
|
|
.in_set(UnitFeed)
|
|
.in_set(LocalCancel),
|
|
mods::track_class_family
|
|
.after(WorldStage::Net)
|
|
.before(UnitFeed),
|
|
// The state push runs before the input pass's `ToggleGameMenu` and the drain
|
|
// after it, so an Esc cancel lands before next frame's cursor reads the mode.
|
|
// After the old-target clear, the pet bar's writer in the feed: `"pet"`
|
|
// resolves off the bar.
|
|
targeting::feed_targeting_to_vm
|
|
.in_set(UnitFeed)
|
|
.after(crate::ui_pet::pet_stop_on_old_target_clear),
|
|
targeting::publish_picks_self.in_set(UiFeed),
|
|
targeting::drain_stop_targeting.after(UiInput),
|
|
// The item-target commit (`0x495d60`): after the input pass so a bag click
|
|
// binds the same frame; outside the target chain, as its clicks never reach
|
|
// the world.
|
|
targeting::commit_item_cast_on_pick.after(UiInput),
|
|
),
|
|
)
|
|
.add_systems(OnEnter(ClientState::InWorld), mods::clear_on_world_enter);
|
|
net::register(app);
|
|
}
|
|
}
|