What the client holds about a fight in progress: the relation to every opponent and its numbers, the buffs
drawn beside you and beside each opponent, the row of combat actions and their cooldowns, and the messages
that switch targets, pursue, give and use an action. The deck BUILDER is another widget, in the character
sheet: character-sheet.md.
Where it lives
| What |
Where |
| The combat view |
Fightview, @RName("frv"), placed by GameUI.addchild place == "fight" into urpanel and held in GameUI.fv. ⚠️ GameUI.fv is never cleared: GameUI.cdestroy does not know the field, so a view that has left the tree is still referenced. Ask fv.hasparent(ui.root) before trusting it |
| The action row |
Fightsess, @RName("fsess"), placed by GameUI.addchild place == "fsess" as a direct child of the GameUI, with no field: gameui.getchild(Fightsess.class) finds it. It sizes itself to its parent and draws over the map, centred on the player. The server puts one up for a fight and destroys it after |
| The relations |
Fightview.lsrel, a LinkedList<Relation>. Relation is an inner class and not a widget: gobid, gst, ip, oip, lastact, lastuse, invalid, and two Bufflists, buffs and relbuffs, whose field initialisers add them to the Fightview itself |
| The target |
Fightview.current, set by setcur off uimsg "cur". null out of a fight, and null after a cur naming an id getrel does not know |
| Your side |
Fightview.buffs (your buffs in the fight), Fightview.lastact/lastuse (your last manoeuvre), Fightview.atkcs/atkct (the global cooldown) |
| Their side |
per Relation: buffs (drawn beside them), oip (their IP), lastact/lastuse (their last manoeuvre) |
| The action slots |
Fightsess.actions, a public Action[] sized by the server’s nact. Action{res, cs, ct}: the manoeuvre and its cooldown’s start and end in Utils.rtime() seconds |
| The give button |
GiveButton, @RName("give"), with a 2-bit state. The relation boxes (Relbox, Mainrel) copy rel.gst into it on every draw |
| The keys |
Fightsess.kb_acts: ten KeyBindings fgt/0..fgt/9, labelled “Combat action 1”..”Combat action 10” in the options (defaults 1..5 and Shift+1..5). kb_relcycle, fgt-cycle, is “Switch targets” (Ctrl+Tab; Shift walks the other way) |
The messages
| Message |
Widget |
Direction |
Arguments |
Effect |
new |
Fightview |
in |
gob, gst, ip, oip |
a new Relation goes to the front of lsrel |
del |
Fightview |
in |
gob |
Relation.remove() destroys its two lists and sets invalid; out of lsrel, and the target is cleared if it was this one |
upd |
Fightview |
in |
gob, gst, ip, oip |
Relation.give(gst), ip, oip |
used |
Fightview |
in |
res or null |
Fightview.use: your lastact, and lastuse = Utils.rtime() |
ruse |
Fightview |
in |
gob, res or null |
that relation’s lastact and lastuse |
cur |
Fightview |
in |
gob |
moves that relation to the front of lsrel and setcurs it |
atkc |
Fightview |
in |
ticks |
atkcs = now, atkct = now + ticks * 0.06 |
blk, atk |
Fightview |
in |
res / res, res |
stored in blk, batk, iatk, which nothing in the client reads |
act |
Fightsess |
in |
n, res? |
actions[n] = new Action(res), or null without a res |
acool |
Fightsess |
in |
n, ticks |
actions[n].cs = now, ct = now + ticks * 0.06 |
use |
Fightsess |
in |
n, nb? |
the two highlighted frames, use and useb; nothing in the client says what either means |
used |
Fightsess |
in |
— |
ignored there. The same name reaches Fightview, so a tap on the name alone must test the widget’s class |
click |
Fightview |
out |
gob, button |
a relation box’s portrait (Avaview) |
give |
Fightview |
out |
gob, button |
a relation box’s GiveButton; mousedown sends the mouse button |
prs |
Fightview |
out |
gob |
a relation box’s Pursue button |
bump |
Fightview |
out |
gob |
“Switch targets”: Fightsess.globtype rotates lsrel locally, then bumps the new front |
use |
Fightsess |
out |
n, 1, modflags, [place] |
a combat key. place is the map coordinate under the pointer, floored to OCache.posres, and only when the pick hits ground |
rel |
Fightsess |
out |
n |
the key’s release, posted through a Release fenced on the render |
The buffs of a fight
| What |
Where |
| Routing |
Fightview.addchild: ("buff", null) goes to Fightview.buffs, ("buff", gob) to that relation’s buffs, ("relbuff", gob) to its relbuffs. Each is a plain add, so a buff is a direct child of its list |
| Painting |
Fightsess.draw alone paints them: fv.buffs to the left of the player, fv.current.buffs to the right. Another relation’s buffs are painted only while it is the target, and relbuffs are painted by nothing. The lists themselves are hidden widgets, so a buff’s own Widget.c does not say where its icon is |
| ⚠️ A relation’s end |
Relation.remove() calls buffs.destroy() and relbuffs.destroy(). Widget.destroy runs remove() on the LIST only and rdispose() on the buffs, so every buff stays a child of a detached list, never runs remove(), and is not dest: buff.hasparent(ui.root) is the only test that goes false |
| ⚠️ The view outlives the fight |
Fightsess, the only painter, is destroyed with the fight, while the Fightview stays in the tree with the buffs of yours the server has not yet expired: a debuff can sit in Fightview.buffs after the last del, drawn by nothing. fv.buffs is what is drawn only while a Fightsess stands under the GameUI |
⚠️ children(Class) recurses |
On the Fightview it reaches every relation’s buffs. Read the lists one by one |
Where Fightsess paints
Every element is anchored on pcc, the player’s point in Fightsess’s own coordinates, from constants of the class: nothing is a widget of its own.
| Element |
Anchor |
| The player point |
pcc, written by updatepos() at the top of every draw from map.screenxf of the player gob, and pho, the offset up to the head. Without a player pcc keeps the centre presize gave it |
Your buffs (fv.buffs) |
pcc + (-buff.c.x - Buff.cframe.x - UI.scale(20), buff.c.y + pho - Buff.cframe.y): a row growing left from UI.scale(20) left of pcc |
| The target’s buffs |
pcc + (buff.c.x + UI.scale(20), buff.c.y + pho - Buff.cframe.y): growing right |
| The two IPs |
g.aimage at pcc ∓ (UI.scale(75), 0), aligned (1, 0.5) left and (0, 0.5) right. Painted only with a target, so fv.current.ip’s text has no box without one |
| The cooldown |
cdframe centred on pcc + cmc, cmc = UI.scale(0, 67), the ring a fellipse of UI.scale(24) under it |
| The last manoeuvres |
the icon centred on pcc + usec1 (yours) and usec2 (the target’s), UI.scale(∓65, 67), its useframe at useul - useframeo. Nothing is painted before the first |
| The action row |
actc(i): five to a row actpitch = UI.scale(50) apart, centred on pcc.x, the first row UI.scale(125) below pcc |
| Gotcha |
Detail |
⚠️ tooltip reads the last draw |
It recomputes nothing, so it hit-tests against the pcc the previous draw wrote |
⚠️ A buff’s c is not its slot |
Bufflist.arrange lays Bufflist.num (five) to a row, Bufflist.margin apart, and Buff.move(Coord, double) animates buff.c towards the slot over half a second; Buff.reqdestroy slides it down a frame as it fades. The list’s own sz is the slots’ bounding box, zero when empty |
⚠️ GOut.aimage truncates |
The offset is (int)(sz * -a), so a box computed for an aligned image takes the same casts |
⚠️ added() runs again on a re-home |
Widget.add0 calls it on every add, so Fightsess.added() re-reads fv and presize()s to the new parent. Fightsess.destroy() is reached by the server’s destroy (UI.destroy → reqdestroy), not when it dies as a descendant |
Threading and gotchas
| What |
Detail |
| The monitor |
The uimsgs are applied on a loader thread under synchronized(ui): lsrel, current, a relation’s ints and Fightsess.actions are written there. “Switch targets” reorders lsrel on the UI thread. Read all of it under the tree’s monitor |
⚠️ getrel throws |
Notfound for an id not in lsrel, so a del, upd or ruse for an unknown gob aborts that message |
| ⚠️ Whose number is whose |
ip is yours and oip theirs (Fightsess paints IP: n left and right), while Relation.lastact is theirs: the field names mix the two sides |
| ⚠️ The give bits |
GiveButton.draw paints state & 1 as the left half (ol open, sl shut) and state & 2 as the right half (or/sr), tinted red at 0, blue at 1, green at 2. The left half is the side the fight view paints as yours |
| ⚠️ A key off the map |
Fightsess.globtype sends no use while the pointer is outside the map view but still records the key as held, so a lone rel goes out on key-up |
⚠️ acool on an empty slot |
throws a NullPointerException inside Fightsess.uimsg; the server never sends one |
| Ticks |
atkc and acool count 0.06 s ticks |