A ghost is dead to the attack check, the action bar, the cast ladder and the cursor (#389)
A ghost could swing, got a lit action bar, a sword cursor over enemies, and an Attack button that did not refuse. benilla tested death as health 0 only, and a released ghost has health 1. Fixes #246. Fixes #248. Fixes #250. Fixes #251. - **One predicate** (`0x605f30`): dead or ghost is health 0 or below, or a player with the `PLAYER_FLAGS` ghost bit `0x10`. It lives on the object fields and every site uses it: the attack check (`0x612e10`), the usable walk's first check (`0x6e3dad`), summon, trade and the death screen. - **#248:** the cast ladder refuses a dead or ghost caster with `SPELL_FAILED_CASTER_DEAD` and sends nothing, unless the spell's `Attributes` has `0x800000` (`0x6e4c49`-`0x6e4c68`); the Attack action button takes it first ("You are dead"). - **#251:** the sword cursor also needs a live, unmounted, unpacified player in control (`0x482648`-`0x4826cb`). A right-click now decides attack from the reference's CanInteract/CanAttack fork (`0x60c18c`-`0x60c247`), not from the cursor kind, so a ghost right-clicking a hostile NPC with a service flag selects it and does nothing else, instead of opening its window. Tests: seven tests, each with a ghost at health 1; `scripts/check.sh` is green. To see it: release spirit. The action bar stays grey; Attack Target reads "Can't attack while dead."; hovering a hostile mob shows the pointer; right-clicking a hostile vendor selects it and opens nothing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
co-authored by
Claude Opus 5.5
parent
459a9a5697
commit
b8a32a06e7
@@ -325,6 +325,11 @@ impl ObjectFields {
|
||||
pub fn player_is_ghost(&self) -> bool {
|
||||
self.player_flags() & 0x10 != 0
|
||||
}
|
||||
/// Dead or a ghost, the reference's `0x605f30`: health 0, or for a player the ghost flag. Not
|
||||
/// feign death, which is `unit_reads_dead`; a creature has no `PLAYER_FLAGS`, so reads clear.
|
||||
pub fn is_dead_or_ghost(&self) -> bool {
|
||||
self.unit_is_dead() || self.player_is_ghost()
|
||||
}
|
||||
/// `PLAYER_FLAGS_HIDE_HELM` (`0x400`, `Player.h:325`), set by `CMSG_TOGGLE_HELM`; public.
|
||||
pub fn player_hides_helm(&self) -> bool {
|
||||
self.player_flags() & 0x400 != 0
|
||||
|
||||
@@ -755,6 +755,33 @@ fn dynamicobject_fields_read_the_live_blizzard_capture() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `0x605f30`: health 0 (`0x605f3b`), or `PLAYER_FLAGS & 0x10` (`0x605f59`); a released ghost has
|
||||
/// health 1, so only the flag catches it, and feign death is neither.
|
||||
#[test]
|
||||
fn dead_or_ghost_is_health_zero_or_the_ghost_flag() {
|
||||
let unit = |pairs: &[(u16, u32)]| ObjectFields::from_pairs(pairs);
|
||||
let alive = unit(&[(FIELD_UNIT_HEALTH, 100), (FIELD_UNIT_MAXHEALTH, 100)]);
|
||||
let corpse = unit(&[(FIELD_UNIT_HEALTH, 0), (FIELD_UNIT_MAXHEALTH, 100)]);
|
||||
let ghost = unit(&[
|
||||
(FIELD_UNIT_HEALTH, 1),
|
||||
(FIELD_UNIT_MAXHEALTH, 100),
|
||||
(FIELD_PLAYER_FLAGS, 0x10),
|
||||
]);
|
||||
let feigning = unit(&[
|
||||
(FIELD_UNIT_HEALTH, 100),
|
||||
(FIELD_UNIT_MAXHEALTH, 100),
|
||||
(FIELD_UNIT_DYNAMIC_FLAGS, 0x20),
|
||||
]);
|
||||
assert!(!alive.is_dead_or_ghost());
|
||||
assert!(corpse.is_dead_or_ghost());
|
||||
assert!(!ghost.unit_is_dead(), "a ghost has health");
|
||||
assert!(ghost.is_dead_or_ghost(), "the ghost-flag leg");
|
||||
assert!(
|
||||
!feigning.is_dead_or_ghost(),
|
||||
"feign death is 0x605f90's, not this"
|
||||
);
|
||||
}
|
||||
|
||||
/// `Unit::SetFeignDeath` sets only `UNIT_DYNFLAG_DEAD`: the raw predicates stay false, the
|
||||
/// reads-dead ones (`0x605f90`, `UnitHealth`, `UnitMana`) flip, and the maxima stay.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user