Brodgar client

session:fight: Combat Schools and the Fight in Progress

One character’s manoeuvre-deck builder (its Martial Arts and Combat Schools tab) and the fight it is in, reached through its session. It answers what the character knows, its saved schools, what the loaded one has dealt to each hotkey and what that costs, and, while it fights, every opponent, the numbers the fight paints between you, the openings drawn beside each side, the combat row with its cooldowns and the manoeuvre each side used last.

local session = hafen.session():current()                      -- the character on screen
local target = session:fight():opponent():current()            -- nil out of a fight
if target then
  local ip = target:ip()                                       -- { mine = …, theirs = … }
  hafen.log():write("IP " .. ip.mine .. " against " .. ip.theirs .. " on " .. target:id())
end
hafen.event():on("OpponentAdded", function(opponent, fight_session)
  hafen.log():write(fight_session:character() .. " is fighting " .. opponent:id())
end)

Rule Detail
One character’s Every character configures its own deck against its own budget and fights its own fight: hafen.session():get("alt"):fight():opponent():current() answers about the alt. A hotkey slot, a school’s slot and an opponent’s gob id count inside one character, so a DeckCard, a School and an Opponent carry their character beside their key.
opponent:gob() answers in the login asked through That character’s own world, where the id came from: opponent:gob():exists() is that character’s line of sight, not the screen’s.
Before the tab has built, and out of a fight :maneuver():list(), :deck():list() and :school():list() are empty, and :summary() and :school():current() are nil, until the tab has built. Out of a fight, :opponent() and :opening() are empty, every :action() is :empty(), and :opponent():current(), :cooldown() and :last() are nil.
Reads are unprotected Nothing throws, and the verbs that act are under Write (protected). The opponents, the manoeuvres used, the openings and the combat row fire the fight events, and the schools are read on demand.

Read

Method Returns Permission Description
session:fight():maneuver():list(filter) Maneuver[] Unprotected Every manoeuvre and attack that character knows.
session:fight():maneuver():count(filter) number Unprotected How many match.
session:fight():maneuver():find(filter) Maneuver \| nil Unprotected The first that matches.
session:fight():deck() collection Unprotected The loaded school’s layout: one card per hotkey place, :list(filter), :count(filter), :find(filter), :get(n).
session:fight():summary() FightSummary \| nil Unprotected The action-point budget and what the loaded school spends of it.
session:fight():school() collection Unprotected The tab’s save list: School objects, :list(filter), :count(filter), :find(filter), :get(n).
session:fight():school():current() School \| nil Unprotected The school the tab marks as loaded. nil before the tab has built.
Rule Detail
filter A string filter matches the resource name and the display name. On the deck it matches the manoeuvre in the place, so one needle finds the same manoeuvre through either door, and an empty place matches none.
No :get on :maneuver() A manoeuvre is addressed by nothing you hold: a string is a search, a position is :list()[n].

A manoeuvre

Method Returns Permission Description
maneuver:res() string \| nil Unprotected The resource name, its identity.
maneuver:name() string \| nil Unprotected The display name.
maneuver:max() number Unprotected The most copies that character may deal into a deck.
maneuver:copies() number Unprotected How many copies the loaded school has dealt.
maneuver:exists() boolean Unprotected Whether that character still knows it. Always answers.
maneuver:info() Maneuver \| nil Unprotected A plain-table snapshot.

A deck card

One hotkey place of the loaded school’s layout, not the manoeuvre in it: session:fight():deck():get(n) is hotkey n, filled or not.

Method Returns Permission Description
card:index() number Unprotected Its 1-based position, the hotkey number and the n of :get(n). Always answers.
card:wire() number Unprotected The server’s 0-based place number. Always answers.
card:key() string \| nil Unprotected The hotkey label the tab paints under it, "1" to "5" and "⇧1" to "⇧5". nil past the labels the tab paints.
card:empty() boolean Unprotected Whether the place holds nothing. Always answers.
card:maneuver() Maneuver \| nil Unprotected The manoeuvre dealt here.
card:res() string \| nil Unprotected That manoeuvre’s resource name.
card:name() string \| nil Unprotected That manoeuvre’s display name.
card:copies() number \| nil Unprotected How many copies the deck holds.
card:info() DeckCard \| nil Unprotected A plain-table snapshot. nil for an empty place.
Rule Detail
Every place :list() holds one card per place of the layout, in hotkey order, whether it holds a manoeuvre or not. The server sets how many places there are.
:get(n) Takes the hotkey number. Any other number answers nil, 0 included, and so does every n before the tab has built. A string raises, naming :find(filter).
An empty place Every reader but :index(), :wire(), :key() and :empty() answers nil.
Identity Interned on character and place: session:fight():deck():get(1) == session:fight():deck():list()[1], and session:fight():deck() is the same object every call. A stashed card follows its hotkey when another school is loaded.
local deck = hafen.session():current():fight():deck()
for _, card in ipairs(deck:list()) do
  hafen.log():write(card:index() .. ": " .. (card:empty() and "(empty)" or (card:name() or card:res() or "?")))
end

The summary

Method Returns Permission Description
summary:spent() number \| nil Unprotected Action points the loaded school spends: the total the window paints beside the cap, the sum of maneuver:copies() over every known manoeuvre.
summary:maxActions() number \| nil Unprotected The action-point budget.
summary:exists() boolean Unprotected Whether that character’s tab is still up.
summary:info() FightSummary \| nil Unprotected A plain-table snapshot.

session:study():summary() is a summary of the same kind. It is a live object with its own :exists() and :info(), interned on its window, nil while the window is not up.

Schools

The tab’s save list: one School per slot the server gives that character, in the list’s order, whether the slot holds a school or not.

Method Returns Permission Description
school:index() number Unprotected Its 1-based position in the save list, the n of :get(n). Always answers.
school:wire() number Unprotected The server’s 0-based slot number. Always answers.
school:empty() boolean Unprotected Whether the slot is unused. Always answers.
school:name() string \| nil Unprotected The name the tab paints. nil when empty.
school:info() School \| nil Unprotected A plain-table snapshot. nil when empty.
Rule Detail
:get(n) Takes the 1-based position. Any other number answers nil, 0 included, and so does every n before the tab has built. A string raises, naming :find(filter).
filter A string filter matches the name, and an unused slot matches none. A name is a search, never a key: names are the player’s and need not be unique.
:current() The school the tab marks with a check, compared with ==: session:fight():school():current() == school. It can be an unused slot, since the tab marks the first until the server names one. :current(x) raises: loading one is school:load().
The name A used slot the server sent no name for reads "Saved school n", as the tab paints it. A rename typed into the tab reads at once, before a save carries it.
An unused slot :empty() is true, and :name() and :info() are nil.
Identity Interned on character and position: session:fight():school():get(1) == session:fight():school():list()[1], and session:fight():school() is the same object every call.
Loading and saving school:load() and school:save(), under fight.load and fight.save.
local schools = hafen.session():current():fight():school()
for _, school in ipairs(schools:list()) do
  local mark = (school == schools:current()) and " (loaded)" or ""
  hafen.log():write(school:index() .. ": " .. (school:name() or "unused") .. mark)
end

The fight in progress

Method Returns Permission Description
session:fight():opponent() collection Unprotected Every opponent that character is fighting, in the order the fight keeps: :list(filter), :count(filter), :find(filter), :get(gobId).
session:fight():opponent():current() Opponent \| nil Unprotected The target: the opponent the fight has picked. nil out of a fight.
session:fight():action() collection Unprotected The combat row: ten actions, one per combat key, :list(filter), :count(filter), :find(filter), :get(n).
session:fight():cooldown() number \| nil Unprotected How much of the global cooldown is left, a 0..1 fraction: 0 when the character can act. nil out of a fight.
session:fight():last() string \| nil Unprotected The resource name of the manoeuvre that character used last. nil before its first and out of a fight.
session:fight():opening() collection Unprotected Your openings: the row of icons the client paints over the map, to the left of that character, or where an addon holds its region, fight.opening.mine. Opening objects, :list(filter), :count(filter), :find(filter). No :get.
Rule Detail
One object session:fight():opponent(), :action() and :opening() are each the same object every call.
:last() A resource name rather than a Maneuver: an opponent’s need not be one the character knows, and ManeuverUsed hands the same string.
:get(gobId) Takes a gob id, the one thing the server publishes about an opponent. A miss is nil. A string filter is refused naming the function form: an opponent has no name of its own, and the creature’s is opponent:gob():name().
The order The fight’s own: a new opponent joins at the front and the target is moved to the front, so :list()[1] is usually the target but not always. The client’s “Switch targets” key reorders the list without a message, and no event says so.
:current() The distinguished member, compared with ==: session:fight():opponent():current() == opponent. :current(x) raises: switching the target is :set(opponent).
Openings Each icon is an Opening. The target’s row is painted to the right of that character, and opponent:opening() lists any opponent’s while you fight them.

An opponent

Method Returns Permission Description
opponent:id() number Unprotected The creature’s gob id. Always answers.
opponent:gob() Gob Unprotected The creature, in the login whose fight this is. Never nil.
opponent:exists() boolean Unprotected Whether that character is still fighting them. Always answers.
opponent:ip() {mine=, theirs=} \| nil Unprotected The initiative the fight paints between you, as whole numbers: mine beside your character, theirs beside them.
opponent:give() {mine=, theirs=} \| nil Unprotected The give button beside their portrait, as its two halves: mine the left, theirs the right. Booleans.
opponent:last() string \| nil Unprotected The resource name of the manoeuvre they used last. nil before their first.
opponent:opening() collection Unprotected Their openings: the row of icons the client paints over the map, to the right of your character while they are the target, or where an addon holds the region fight.opening.theirs. Opening objects, a view minted per call.
opponent:info() Opponent Unprotected A plain-table snapshot.
Rule Detail
The creature is the gob Name, health and position belong to the gob. What the fight holds about the relation is read here.
Once the fight with them ends :ip(), :give() and :last() read nil, :opening() is empty, :exists() is false and :info() is { id }. :id() and :gob() go on answering.
opponent:gob() is never nil Like session:world():gob():get(id): ask opponent:gob():exists().
Identity Interned on character and gob id: session:fight():opponent():get(id) == session:fight():opponent():get(id) and seen[opponent] = true work. A later fight with the same creature hands back the same object.
Events OpponentAdded, OpponentRemoved, OpponentChanged and OpponentSelected hand this object, and ManeuverUsed hands it beside a manoeuvre they used.

An opening

One icon the fight draws over the map: beside your character, listed by session:fight():opening(), or beside an opponent, listed by opponent:opening().

Method Returns Permission Description
opening:res() string \| nil Unprotected The resource name.
opening:name() string \| nil Unprotected The display name, once the resource has resolved.
opening:amount() number \| nil Unprotected Its own meter, a 0..1 fraction.
opening:remaining() number \| nil Unprotected The radial overlay, a 0..1 fraction: how much of its run is left.
opening:number() number \| nil Unprotected The integer badge drawn on the icon.
opening:opponent() Opponent \| nil Unprotected The opponent it is drawn beside. nil for one of yours, and once the fight no longer draws it.
opening:widget() Widget \| nil Unprotected The widget that holds it: the crossing back into the tree. The fight keeps that list hidden and paints the icon in a row of its own, so the widget does not mark where the icon is drawn. The row’s region, fight.opening.mine or fight.opening.theirs, does.
opening:exists() boolean Unprotected Whether the fight still draws it. Always answers.
opening:info() Opening \| nil Unprotected A plain-table snapshot.
Rule Detail
filter A string filter on either door matches the resource name and the display name. A miss is nil.
No :get An opening has no key: two can share a resource, and the server can replace a live opening’s resource under it. session:fight():opening():get(1) raises: session:fight():opening() has no verb 'get' — an opening has no key, since two can share a resource and the server can replace one under a live opening: session:fight():opening():find(needle) is the search and session:fight():opening():list()[n] takes a position.
Identity Interned per addon: both doors and the events hand the same object, so seen[opening] = true works. It re-reads its icon on every call, so a stashed one tracks its meters.
Res-only at first The display name and the meters arrive in a second server message, so every reader may answer nil just after an opening appears. Content-defined: amount, remaining and number are often absent.
Only while the fight draws them The rows exist while the fight lasts. One of yours that outlasts the fight is no longer drawn once it is over: it is not listed, opening:exists() is false, and OpeningRemoved fires for it when the fight ends. If it is still there when the next fight starts, it is drawn again and fires OpeningAdded again.
Once it is gone :exists() is false while :res(), :name() and the meters read the values it had, so an OpeningRemoved payload still reads.
Events OpeningAdded, OpeningChanged and OpeningRemoved hand this object, then the opponent it is drawn beside (nil for yours), then the session. OpeningRemoved still names the opponent after the fight with them has ended, when opening:opponent() answers nil. When the fight with an opponent ends, each of its openings fires OpeningRemoved once.
hafen.event():on("OpeningAdded", function(opening, opponent, session)
  local whose = opponent and ("on opponent " .. opponent:id()) or "on you"
  hafen.log():write((opening:name() or opening:res() or "an opening") .. " " .. whose)
end)

A combat action

The place a combat key presses: session:fight():action():get(n) is the one “Combat action n” presses, keys 1 to 5 and Shift+1 to 5 unless you rebound them. The row is the fight’s own, so every place is empty out of a fight.

Method Returns Permission Description
action:index() number Unprotected Its 1-based position, the n of :get(n) and of “Combat action n”. Always answers.
action:wire() number Unprotected The server’s 0-based slot number. Always answers.
action:empty() boolean Unprotected Whether the place holds nothing. Always answers.
action:res() string \| nil Unprotected The manoeuvre’s resource name.
action:name() string \| nil Unprotected Its display name.
action:maneuver() Maneuver \| nil Unprotected The same manoeuvre in session:fight():maneuver(), nil when that list has none.
action:cooldown() number \| nil Unprotected How much of its own cooldown is left, a 0..1 fraction: 0 when it can be used.
action:info() CombatAction \| nil Unprotected A plain-table snapshot. nil for an empty place.
Rule Detail
Ten places, always :list() is ten actions and :get(n) takes 1 to 10: the client’s ten combat keys. :get(0) raises naming action:wire(), and a position past ten names the range. A place past the server’s row is :empty().
The fight’s row, not the deck What the server put up for this fight. session:fight():deck() is the schools tab’s layout, which the tab keeps to itself until it is saved (school:save()).
An empty place Every reader but :index(), :wire() and :empty() answers nil.
filter A string matches the manoeuvre’s resource and display name, as on :maneuver(), so one needle finds the same manoeuvre through either door.
Identity Interned on character and place: session:fight():action():get(1) == session:fight():action():list()[1]. A stashed action follows its key from fight to fight.
Events CombatActionChanged when a place is set, cleared or its name resolves, never as a cooldown runs. The moment a manoeuvre is used is ManeuverUsed.
Using it action:use(mods, position), under fight.use.

Write (protected)

The client sends only shapes a player could compose.

Method Returns Permission Description
action:use(mods, position) the CombatAction fight.use Use that action, as tapping its combat key does.
session:fight():opponent():set(opponent) the collection fight.set Make that opponent the target, as the client’s “Switch targets” key does.
session:fight():pursue(opponent) session:fight() fight.pursue Press Pursue beside that opponent’s portrait.
session:fight():give(opponent, button) session:fight() fight.give Click the give button beside that opponent’s portrait.
school:load() the School fight.load Load that school, as selecting it in the tab and pressing Load does.
school:save() the School fight.save Save the tab’s layout into that slot, as selecting it and pressing Save does.
Rule Detail
Permission Each key declared in your manifest, or the group fight.*. An undeclared key raises naming it. One key covers every character (a key names the action, not the target).
A tap The row’s use and then its release, sent back to back from that character’s combat row: the key pressed and let go. There is no verb for holding one down.
A load or a save The tab’s load or save and then its use, sent back to back from that character’s tab with the school’s school:wire(): the button pressed with that slot selected. A save carries the slot, the school’s name when it has one, and the layout the tab holds, which session:fight():deck() reads. An unused slot is saved into, and takes the name the server gives it.
mods Optional and first: Shift = 1, Ctrl = 2, Alt = 4, as for slot:use(mods). The client’s own keys for actions 6 to 10 are Shift+1 to Shift+5, so pressing one of them carries Shift.
position Optional and second: a Position in that character’s world, the ground the client adds when a key is pressed with the pointer over the map. action:use(0, position) passes one without modifiers.
The opponent :set takes an Opponent or its gob id, the key :get takes, as session:speed():set takes what its :get takes. :pursue and :give take the Opponent.
button Optional: 1, the default, is the left button and 3 the right, as a click on the give button sends. What each does is the server’s.
Raises before anything is sent An addon that did not declare the verb’s key, naming it. A surplus argument. For action:use: a mods that is not a whole number 0..7, a position that is not a Position, no fight, an empty action (action:empty()) and one past the server’s row. For the three that take an opponent: anything but an Opponent (or, for :set, its gob id), one of another character’s fight, one that character no longer fights (opponent:exists() is false), and a button outside 1..3. For school:load() and school:save(): no tab yet (session:fight():school():current() is nil), a school past that character’s save list, and, for :load(), an unused school (school:empty()).
Asynchronous The server answers every write. A use: the cooldowns start, ManeuverUsed fires and session:fight():last() names it. A switch: :current() answers the old target until then, and OpponentSelected fires when it moves. A load or a save: session:fight():school():current(), the names and session:fight():deck() read what they did until its answer arrives. Nothing moves when the server refused one.
The outbound stream sees them Every message passes hafen.event():action() like the client’s own, so a handler can stop or rewrite it.

See Also