Fighting: spells, creatures, loot and quests

Architecture walkthrough, part 4 of 5

Walkthrough contents Part 4 of 5
  1. Overview
  2. Logging in: from the launcher to character select
  3. The wire: from bytes to packets to actions
  4. Into the world: zones, entities and movement
  5. Fighting: spells, creatures, loot and quests
  6. Behind the scenes: data, database, events and the website

The player is in a zone and presses an ability key. This page follows that cast through the server, then looks at the creatures on the other side of the fight, and what happens when one dies.

A spell cast

What happens when a player casts a spell The client sends a cast; CastSpellHandler, running in the player's connection process, passes it to the Zone.Instance as a cast_spell message. The zone asks Spell.Caster, pure functions that check the cast, pick targets with telegraphs and work out the effects, and Caster returns a list of actions. The zone carries them out: packets to players (ServerSpellStart, ServerSpellGo, ServerCombatLog and others), damage and auras (health, shields, effects over time), and timers for cast times and delays. When damage kills a creature (Zone.Creatures), the zone hands out loot (Zone.Loot), experience (KillExperience) and quest credit (PlayerQuests). Creatures cast the same way: their AI runs on the zone's tick and calls the same Caster. Client the player presses a key CastSpellHandler in the player's connection Zone.Instance cast_spell message Spell.Caster pure functions: checks, targets (telegraphs), effects actions Packets to players ServerSpellStart, Go, CombatLog... Damage, auras health, shields, effects over time Timers cast times, delays (send_after) Creature dies? Zone.Creatures Loot Zone.Loot Experience KillExperience Quest credit PlayerQuests Creatures cast the same way: their AI runs on the zone's tick and calls the same Caster.
  1. The client sends a cast message. CastSpellHandler (apps/bezgelor_protocol/lib/bezgelor_protocol/handler/cast_spell_handler.ex), in the player's connection process, reads which ability it is and sends it to the zone: Zone.Instance.cast_spell/3, a cast.
  2. The zone instance, which owns the player's spell state, hands the request to BezgelorWorld.Spell.Caster (apps/bezgelor_world/lib/bezgelor_world/spell/caster.ex).
  3. Spell.Caster decides everything about the cast: whether it is allowed (cooldowns, resources, range and the like), how long it takes, which units it hits and what each spell effect does to them.
  4. It doesn't do any of it itself. It returns a list of actions, plain data such as "send this packet to everyone in the zone", "take 120 health from this creature", "call me back in 1.5 seconds for the cast's end", "put this aura on that player".
  5. The zone carries the actions out: it sends the packets (ServerSpellStart, ServerSpellGo, ServerCombatLog and the rest), applies damage and auras, and sets timers with Process.send_after, which arrive later as ordinary messages in the zone's mailbox.

The split in steps 3 to 5 is deliberate. Spell.Caster is made of pure functions: given the caster's state and the request, it returns the new state and the actions, and touches nothing else (no processes, no database, no network). That makes it easy to test, since a test can run a whole cast without starting a zone, and it keeps the rule "only the zone changes the zone" simple to follow. Many of the world server's systems are built the same way: a pure module that decides, and the zone process that acts.

Where spells come from

Spells are data, not code. The client's own tables, extracted into apps/bezgelor_data/priv/data and held in memory (Behind the scenes), describe every ability: Spell4 (the spell: cast time, cooldown, costs, targeting), Spell4Effects (what it does, effect by effect), Spell4Telegraph and TelegraphDamage (the shapes on the ground). Spell4 rows are looked up by id; the Caster interprets them.

Each effect type is handled by a function: damage and healing (including damage over time), vital changes, proxies that trigger other spells, crowd control, property modifiers, quest objectives, teleports, mounts, taxis and others. Spell.SupportEffects and Spell.AuraEffects hold the less common ones, and Spell.EffectCoverage counts every effect the server runs, so the admin pages can show which effect types players reach that we don't handle yet. Damage numbers come from Spell.DamageCalculator.

Telegraphs

Most WildStar abilities hit an area drawn on the ground, the telegraph, rather than a single target. BezgelorCore.Telegraph (apps/bezgelor_core/lib/bezgelor_core/telegraph.ex), ported from NexusForever, places a telegraph at the caster's position and facing and answers "is this unit inside?" for each shape the client uses: circle, ring, square, cross, cone, pie, rectangle and long cone. The Caster uses it to pick the units an ability hits.

Creatures

Creatures live in the zone too. Zone.Creatures (apps/bezgelor_world/lib/bezgelor_world/zone/creatures.ex) holds their state, and each creature's behaviour is a small state machine in BezgelorCore.AI: idle, wandering, patrolling, in combat, evading back to its spawn point, dead. A creature in combat keeps a threat table of who has hurt it most.

Zone.CreatureCombat runs the fighting on every zone tick: an aggressive creature attacks a player who comes within 15 units, chases its target, swings at it, and gives up and walks home (leash) when it is pulled more than 40 units from where the fight started. Creatures cast their abilities through the same Spell.Caster as players, so a creature's spell follows the same rules as a player's. Values like these ranges, where neither the client's tables nor NexusForever settle them, are listed with their reasons in docs/design/designed-values.md.

Dungeon and raid bosses add scripts: BezgelorWorld.Scripts.* modules, and encounter blocks in the instance files under apps/bezgelor_data/priv/data/instances/, bound to creatures and maps by BezgelorWorld.Script.Registry. How they are built and tested is on How we build, test and tune instances.

Death, loot and experience

When damage brings a creature to zero health, the zone credits the kill:

  • Loot: Zone.Loot (zone/loot.ex) drops the loot as small units on the ground that the players can pick up, with group rules (need and greed rolls, round robin, master loot) and overflow to mail when bags are full. What drops is decided by the pure modules BezgelorWorld.Loot.Profile, Roller and Rules.
  • Experience: BezgelorWorld.KillExperience (kill_experience.ex) works out the experience for the kill from the creature's difficulty and the level difference. Neither the client's tables nor NexusForever give this amount, so the formula is designed from what the original developers said and players measured; the module explains it.
  • Quest credit: Zone.PlayerQuests advances any quest objective that counts this kill.

Some time later the zone sends itself a respawn message and the creature comes back.

When a player dies, the client shows its death screen, and the player can wake at the nearest Holo-Crypt or, in some places, where they fell (BezgelorWorld.Death, BezgelorWorld.HoloCrypt).

Quests

Each player's quests live with the player in the zone (Zone.PlayerQuests, zone/player_quests.ex). The rules for a quest log are pure functions in BezgelorWorld.Quest.QuestLog; what each objective type means is documented in the semantic registry. Every change is written to the database at once through BezgelorWorld.Quest.Store (Quest.DbStore in the server, an in-memory store in tests), so leaving one zone and entering the next never loses progress.

Where to look

  • apps/bezgelor_world/lib/bezgelor_world/spell/: the Caster and its helpers (caster.ex, auras.ex, damage_calculator.ex, ...).
  • apps/bezgelor_core/lib/bezgelor_core/telegraph.ex, ai.ex.
  • apps/bezgelor_world/lib/bezgelor_world/zone/creatures.ex, creature_combat.ex, loot.ex, player_quests.ex.
  • apps/bezgelor_world/lib/bezgelor_world/kill_experience.ex, quest/.
  • apps/bezgelor_world/test/e2e/: headless players casting, fighting and looting against the real protocol.

Next: Behind the scenes.