Skip to content

feat(sim): trigger player events from scenario and Damage/Heal - #130

Merged
e54-bot merged 4 commits into
mainfrom
feat/player-events-v2
Oct 28, 2026
Merged

e54-bot merged 4 commits into
mainfrom
feat/player-events-v2

Conversation

@e54-bot

@e54-bot e54-bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Trigger the player events issue #47 scopes, from both sources the issue names:

  • Scenario events keep dispatching to Event::Player rules as before (injected), now sharing one path with raised events: new events.rs holds eligibility (kind + team/target filters on the event player), queueing, and firing.
  • Modeled state changes raise events in occurrence order: Damage → Player Took Damage (target) and Player Dealt Damage (player dealer; Null dealer raises none and binds Attacker to null); a hit that empties health adds Player Died then Player Dealt Final Blow; Heal → Player Received Healing/Player Dealt Healing.
  • Bindings: Event Player = the player the event happened to; Attacker/Victim/Event Damage/Event Healing bound to the cause. A raised event carries no scenario values, so an event value its cause does not bind (Event Was Critical Hit, Event Was Environment, Event Was Health Pack, Event Ability, Event Direction) is not_modeled — never missing_input.
  • Scenario values.attacker/values.victim validate as a declared player id or null; anything else is a load error.
  • One liveness predicate — dead(), the isDead read Is Alive/Is Dead/Set Health share, falling back to the health pool only where isDead cannot answer — gates Damage, Heal and Resurrect. A killing Damage writes isDead through write_cell; a revive refills the world pool so the player can die again: Respawn unconditionally ("with full health, even if they were already alive"), Resurrect only on an actual revive.
  • Busy event-rule instances queue raised events until idle; one tick's raised events are bounded by TICK_EVENT_LIMIT (1024) against self-triggering fan-out.

Prior-attempt lessons addressed (reviews 47-r1/r2/r3): single authoritative alive/dead read shared by Damage/Heal/Kill/Respawn/Resurrect; Resurrect does not heal living targets while Respawn may; raised vs injected event-value miss semantics split correctly; event filter evaluation reads the event player; occurrence-order dispatch between instance visits.

Verification

  • cargo fmt --check — clean
  • cargo clippy --all-targets — clean
  • cargo test — 44 suites, 362 tests, 0 failures (incl. new tests/player_events.rs: 15 tests covering injected dispatch + Event Player binding, occurrence order took→dealt→died→finalBlow, Null dealer, healing events, team/player filters on raised events, raised-unbound not_modeled vs injected missing_input, dead-target suppression, respawn/resurrect split + re-death, busy-instance queueing, the 1024 bound, attacker/victim validation)
  • cargo check --locked --target wasm32-unknown-unknown -p athena-sim — clean
  • size-guard + git diff --check — clean (docs/simulator-interface.md trimmed back under the 10k-token limit)
  • Bastion (bastion.ow, scen3 + declared gameMode/local/state.isDuplicatingAHero): moves past the player-left rule to tick 91, stopping not_modeled on "action wait under a 90% simulation rate" — the documented athena#120 boundary, identical to main. With injected playerLeft/playerJoined/playerTookDamage/playerDied for the Team 2 player, the filtered rules dispatch at the due tick with eventPlayer bound (Update targetPlayer when player left, Remove Player from Blacklist, [System] Arm passive regen, …); the next stops are pre-existing not_modeled constructs inside rule bodies (player inside a custom string, wait behavior in an event-triggered rule) — precise constructs, as required.

Not verified

  • Bastion never calls Damage/Heal before the tick-91 wall, so the raised-event path is exercised only by the unit tests, not the workload.
  • Real-client ordering/capture for any of the raised events: all modeled, not measured (recorded in docs/findings.md).

Fixes #47

Injected events and the events modeled state changes raise share one
dispatch. `Damage` raises `Player Took Damage` on the target and
`Player Dealt Damage` on a player dealer, a hit that empties the
health pool adds `Player Died` then `Player Dealt Final Blow`, and
`Heal` raises `Player Received Healing`/`Player Dealt Healing` — in
occurrence order, binding `Event Player`, `Attacker`, `Victim` and
`Event Damage`/`Event Healing`. Team/player filters apply to a raised
event the same as an injected one, a busy instance queues it until
idle, and one tick's raised events are bounded at 1024 against a
triggering rule's own fan-out. A raised event binds only what its
cause knows — an unbound event value is `not_modeled`, never a
`missing_input`.

One liveness predicate — the `isDead` read `Is Alive`/`Is Dead`/
`Set Health` share — gates `Damage`, `Heal` and `Resurrect`: a dead
target's pools do not move and no events raise. A killing `Damage`
writes the cell, and a revive refills the pool so the player can die
again — `Respawn` unconditionally, `Resurrect` only on an actual
revive. Scenario event values `attacker`/`victim` validate as declared
player ids or `null`.

Bastion (scen3 + declared gameMode/local/isDuplicatingAHero) moves
past the player-left rule to tick 91, stopping `not_modeled` on "wait
under a 90% simulation rate" — the same next stop as main. With
injected `playerLeft`/`playerJoined` the Team 2 rules dispatch to p2;
the join rule's condition stops on "player inside a custom string".

Fixes #47
The 0.11.1 release bumped crates/athena-sim/Cargo.toml without
regenerating the lockfile, so every --locked CI step failed.
@e54-bot

e54-bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Fixed the reported finding:

  • Stale Cargo.lock breaking --locked CI — the 0.11.1 release bumped crates/athena-sim/Cargo.toml without regenerating the lockfile, so cargo check --locked, cargo clippy --locked, and cargo test --locked all failed. Regenerated and committed Cargo.lock in 564b71b.

Verified locally on Rust 1.88.0: cargo check --locked --target wasm32-unknown-unknown -p athena-sim, cargo clippy --locked --workspace --all-targets -- -D warnings, cargo fmt --all -- --check, and cargo test --locked --workspace --all-targets all pass. All three required checks are green on the pushed head.

The PR is ready for follow-up review.

The EVENT_VALUES doc comment listed hostPlayer among unmodeled event
values, but hostPlayer is modeled: player.rs resolves the scenario
`host` marker, a rule architecture.md records as decided. The same
sentence mislabeled the remaining entries as player- and vector-typed;
the catalog gives eventAbility returnType Button.

architecture.md still said a non-Ignore wait behavior stops only in a
rule triggered by an injected event, but the gate is any bound event —
a raised event also sets inst.event. Updated both sites to the file's
"injected or raised" terminology.
@e54-bot

e54-bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Review findings addressed in 7e20007 (docs: correct stale not-modeled claims from PR #130 review):

  1. call.rs EVENT_VALUES doc comment — fixed. hostPlayer dropped from the unmodeled list (it is modeled: player.rs resolves the scenario host marker) and the mistyped "Player- and vector-typed" claim removed. The comment now reads: "Event values with no modeled source (eventAbility, eventDirection) are not modeled."
  2. docs/architecture.md stale "injected event" gating — fixed at both sites (lines 76 and 82). Both now say "injected or raised event", matching the sibling phrasing the PR already introduced at lines 66 and 87 and the inst.event.is_some() gate in step.rs.

No findings were contested or deferred. Verified with cargo fmt --check, cargo clippy --all-targets -- -D warnings, and cargo test -p athena-sim (all suites green). The PR is ready for follow-up review.

The player-scoped rules paragraph still said a `Slot` filter never
fires only when "no matching event is injected". Since this PR,
modeled `Damage`/`Heal` also raise player events that can reach the
filter, so a raised match fires the rule too. Audit of every
"injected" phrase in docs and crate comments found this the last
site that named injected events where raised events also apply;
the rest already say "injected or raised" or correctly scope
"injected" to scenario-declared events.
@e54-bot

e54-bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Round-2 review findings addressed.

Fixed

  • docs/simulator-interface.md:72 — "no matching event is injected" under-covered the Slot-filter no-fire rule now that modeled Damage/Heal raise player events. Changed to "no matching event is injected or raised", matching the file's line-75 terminology (2ff11c4).

A full-repo audit of every "injected" phrase in docs/ and crate comments found this the last site where only injected events were named while raised events also apply. The remaining occurrences already say "injected or raised", or correctly scope "injected" to scenario-declared events (events field, InjectedEvent, tick scheduling, and the eventPlayer/eventValue missing-input kinds — a raised event always binds a player and takes no values).

Verified with cargo test --locked (all suites pass). Ready for follow-up review.

@e54-bot
e54-bot merged commit 5e16900 into main Oct 28, 2026
3 checks passed
@e54-bot
e54-bot deleted the feat/player-events-v2 branch October 28, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trigger player events: join, leave, damage, death, final blow

2 participants