automap.panel¶
The live party, drawn beside the map.
Gold Box Companion’s HUD is the model and it is the right one: one card per character carrying everything you glance at mid-fight, so that “who is hurt” is answered by looking rather than by reading.
Bars, not just numbers. 5 / 7 has to be read; a bar a third empty does
not, and mid-fight that is the whole difference. The numbers stay beside the
bar for when the exact value matters.
Everything here is presentation. The decoding is automap/live.py, which has no
Qt in it and is tested against captured bytes.
ColumnSplitter is here rather than in automap/window.py because the width
a card is drawn to is one of the widths it divides, and RosterPanel reads
it. It is the tab’s three columns and not a panel, which is the one thing in
this file that is not.
Module Attributes
A child of the |
Functions
|
The widget |
|
|
|
|
Classes
A proportion, with its numbers written across it. |
|
Where, when, which area, and what is on the party. |
|
The classes and level on a roster card: the second thing to give way. |
|
The character name on a roster card: the one thing on the top row that gives way. |
|
One character: name, class and level, AC and THAC0, bars, what is in hand, what is on them, and whether they are on quickfight. |
|
The automapper's three columns, and the widths a user drags them to. |
|
A push button that gives way instead of holding the window open. |
|
The same, for a checkbox whose label is a sentence. |
|
A dropdown whose floor is not the length of the area it is showing. |
|
A read-out that shortens rather than setting a floor under the window. |
|
A few icons in a line, painted from |
|
What the tab has done, and what it is waiting for. |
|
Every note in this area, with its square. |
|
The card's line of what is in hand: bounded in both axes. |
|
The cards, down the left. |
- class automap.panel.Bar[source]¶
Bases:
PyQt6.QtWidgets.QWidgetA proportion, with its numbers written across it.
Painted rather than assembled from a
QProgressBarbecause the colour carries meaning here – a hit point bar changes colour as it empties – and a styled progress bar fights the platform theme over exactly that.
- class automap.panel.BottomStrip[source]¶
Bases:
PyQt6.QtCore.QObjectWhere, when, which area, and what is on the party.
- SHORT = 21¶
The tallest this strip may hold the window open, whatever the UI font.
- effects¶
One icon row for the whole roster, above the square and the area name. It was a
QLabelwriting outBless Prayer Protection from Evil, 10' Radius, and the width of that is what made it unaffordable; Donald settled the shape on#142 (The party effects line is computed every poll and shown nowhere): “I think we should have ONE line for the entire roster with party effects… Icons will take up less space than text-only names of the spells. We can put the name of the spell in the tooltip of the icon.”
- show_effects(snap)[source]¶
One icon per spell the whole party is under, named in the tooltip.
What counts as the whole party is `Snapshot.whole_party_effects`, and it is not only the
$FFowner byte the decode named: Bless cast from the adventure menu writes one row per character and no$FFrow at all, so a rule that read only the owner byte drew nothing for the one party spell anybody has watched land.Nothing is drawn when nothing is running. The old shape was one line per character, blank on most of them, and eight blank lines is what got it removed; a strip that says “party effects: none” five times a second is the same mistake with one line instead of eight.
Nothing about the monsters, either. The row briefly counted them – “2 effects on monsters” under the party’s own spells – and Donald cut it on 2026-09-01: hovering the party’s row to be told a number about the orcs answers a question nobody asked, and it says neither which monster nor which spell. It was also unreachable in the one case that might have been interesting, because a party with nothing running draws no icons and a row no icons wide cannot be hovered at all. The combat view is where a monster’s effects will mean something.
A party effect no badge covers is drawn nowhere, and that is worth saying out loud rather than letting it look like a party with nothing running.
automap/live.py’s badge set is graded from the spell table – no save this project holds carries a party-wide effect at all – so an id turning up here means the set is a glyph short, and it goes to the debug log once per id for whoever comes to choose one.- Return type:
None
- show_state(state, snap=None)[source]¶
The map’s own state answers “where”; the snapshot answers the rest.
The square and facing come from
AutomapStateon purpose: it prefers the game’s own status line, which is right the moment the screen settles, where the memory copy at$49C0lags a move.- Return type:
None
- class automap.panel.CardClassLabel[source]¶
Bases:
automap.panel.ElidingLabelThe classes and level on a roster card: the second thing to give way.
CardNameLabelyields first and yields everything, which is enough on a machine whose fonts are this one’s. It was not enough on Windows: CI reported the Level up button drawn 99 of 102px inside a 220px column with the name already down toLAD..., because the classes and the button alone are wider than the column there. A plainQLabelcannot give way, so the button was the thing that got cut – which is the whole of#168, reappearing on a platform the fix was not measured on.So the order is: the name goes to nothing, then the classes shorten, and the button is never touched. That is Donald’s priority read down –
MU/Cis worse than a shortened name and better than a control cut in half, and the button is the one thing on the row a player has to be able to hit.SQUEEZED = 0for the same reason as the name’s: any floor measured here is a floor that cuts the button on a machine with a wider font.- SQUEEZED = 0¶
- class automap.panel.CardNameLabel[source]¶
Bases:
automap.panel.ElidingLabelThe character name on a roster card: the one thing on the top row that gives way.
The row holds the name, the classes and level, and the Level up button when the character has earned one, inside a column capped at 220px. The classes and the level are the character; the button is a control, and a control cut in half reads
Lev(#168). So the name yields all of the width, down to nothing if that is what it takes, and says it has by drawing an ellipsis.Donald’s reasoning, and it is what makes a shortened name the cheap side of the trade: “The level up button will never be there for very long. It will be clicked as soon as it appears. So, cutting off the name for a little while is fine.” A name is also the one thing on the row still recognisable from its first few letters –
LADY KATH...is unmistakably LADY KATHERINE, whereMU/CforMU/C/T L8is not.SQUEEZED = 0rather than a number measured here: the classes label and the button are both set in points, so how many pixels they take is the machine’s business, and any floor big enough on this one is a floor that cuts the button on a machine with a wider font.- SQUEEZED = 0¶
The widest this name may hold the row open. Zero, deliberately.
- class automap.panel.CharacterCard[source]¶
Bases:
PyQt6.QtCore.QObjectOne character: name, class and level, AC and THAC0, bars, what is in hand, what is on them, and whether they are on quickfight.
The Level Up button lives here and nowhere else. It sits at the right end of the class-and-level line, and it is hidden – not disabled – unless that character has the experience for another level. A button that is there only when it can be used needs no label saying which character it means: the card is the answer.
And it is hidden for a title whose trainer nobody has measured, which is every title but Pool of Radiance –
levelling, set from the window. The refusal isautomap.actions.level_up_blockersand it is enforced at the write as well; hiding the button is so that the feature is not offered and then withdrawn (#16).- __init__(root, index, parent=None)[source]¶
- Parameters:
root (QWidget)
index (int)
parent (QObject | None)
- classmethod chosen_class(who)[source]¶
Which class the button will raise. For the tooltip only – the window asks the record the same question before it writes.
- Return type:
str | None
- level_up_requested¶
The slot, and nothing else. The player is not asked which class. A multi-class character with two ready gets the one whose threshold after the level is highest, which keeps the trainer’s experience clamp as high as it goes and so usually leaves the other class still qualified; pressing again takes that one.
goldbox.levelup.best_next_classis the rule anddocs/135-levelling.mdis why.alias of
int
- levelling¶
Whether levelling is possible in this title at all. True until the window says otherwise, because a card built without one is Pool of Radiance’s – the same default every other per-title reader takes.
- readied_items: tuple[str, ...]¶
What is in hand, under the bars. Readied only – the whole inventory would swamp the card – and a blank line for a character carrying nothing readied: the absence is the information, and the word “none” is not. The line stays either way, so the cards below do not shift when a sword is put away.
- static ready_to_level(who)[source]¶
Which of this character’s classes have the experience for a level.
Every class is measured against the whole stored number. The trainer does not divide experience between a multi-class character’s classes – LADY KATHERINE, magic-user 1 / thief 7 with 70,100 points, was offered thief 8, whose threshold is 70,001 – so this asks each class the same question the school does.
- show_readied(items)[source]¶
One line of what is in hand, shortened to the room the card has.
The label holds the whole string and
ReadiedLabeldraws as much of it as fits, so nothing here measures a font: the elide is the painter’s and the card’s floor is a constant. The full list stays readable because the label sets no tooltip of its own and so answers with the frame’s, which already carries it.- Return type:
None
- class automap.panel.ColumnSplitter[source]¶
Bases:
PyQt6.QtCore.QObjectThe automapper’s three columns, and the widths a user drags them to.
The roster, the map and the reading column used to be three cells of a grid, two of them capped at a width chosen once.
#162made the two dividers draggable and the widths remembered, and Donald settled both of the questions that came with it:“a dragged width on a column should be remembered when Wish opens again” –
Settings.automap_columns, three numbers in the JSON;“Sure, let the user drag it down to nothing. As long as they can drag it back out when they do that.”
The second is the whole of the difficulty, because a column dragged shut has no width left to grab: what the user aims at afterwards is the divider, and if that went with the column the panel would be gone for good and the only way back would be editing the settings file by hand. Two things keep it reachable, and both are tested in
tests/test_columns.pyagainst a window built from a settings file that already holds a zero:the handle is
HANDLEwide rather than the style’s, and Qt keeps drawing it at the edge of a collapsed pane – measured atQRect(0, 0, 6, h)with the roster at zero;the width is restored with
QSplitter.setSizes, which honours a zero without hiding anything, so the divider is in the window on the first frame of a fresh start.
A column with a floor – the roster’s is the width of a card, the reading column’s is
AutomapBinding.SIDE_SQUEEZED– is therefore either wider than its floor or shut, with nothing in between. That is Qt’s own collapsing and it is the behaviour Donald asked for: dragging inwards past half the floor shuts the column, and dragging outwards opens it at the floor again.- COLUMNS = 3¶
- HANDLE = 6¶
How wide the divider is. Qt’s style answers 4 here, which is enough while there is a column beside it to aim at – but a column dragged shut leaves the divider as the only thing left to grab, and it is then the whole of the way back. Set rather than inherited for that one case.
- MAP_ASKS = 1¶
The map asks for nothing of its own at startup, because the window has not been shown yet and there is no width to divide. The stretch factors below hand it everything the two side columns do not want, on the first frame and on every resize after.
- MAP_AT = 1¶
Left to right, and the order the widths are written to the settings file in.
- ROSTER = 220¶
The roster’s width before anybody drags it, and the width a card is drawn to. Unchanged: Donald ruled on
#162that a multiclass character whose name will not fit is a corner case, that the default stays where it is, and that dragging is the answer for whoever meets it (#168).
- ROSTER_AT = 0¶
Left to right, and the order the widths are written to the settings file in.
- SIDE = 460¶
And the reading column’s. The panels hold short rows; past this they are mostly paper.
- SIDE_AT = 2¶
Left to right, and the order the widths are written to the settings file in.
- restore()[source]¶
Open at the remembered widths, or at the defaults.
Called before the window is shown, which is deliberate:
setSizesrecords what each column asked for and the splitter divides the real width against those the moment there is one, so the first frame the user sees is already the right shape rather than the default shape corrected afterwards.- Return type:
None
- class automap.panel.ElidingButton[source]¶
Bases:
PyQt6.QtWidgets.QPushButtonA push button that gives way instead of holding the window open.
- SQUEEZED = 64¶
A word and an ellipsis. Below this a button is a smear, and the row of them says nothing at all.
- class automap.panel.ElidingCheckBox[source]¶
Bases:
PyQt6.QtWidgets.QCheckBoxThe same, for a checkbox whose label is a sentence.
- SQUEEZED = 96¶
Wider than a button’s floor because the box itself eats the first 20.
- class automap.panel.ElidingComboBox[source]¶
Bases:
PyQt6.QtWidgets.QComboBoxA dropdown whose floor is not the length of the area it is showing.
- SQUEEZED = 110¶
Enough for a short area name. The whole row – maps and disk – is the item’s tooltip whatever width the box is.
- class automap.panel.ElidingLabel[source]¶
Bases:
PyQt6.QtWidgets.QLabelA read-out that shortens rather than setting a floor under the window.
The text a label is holding changes while the program runs – the bottom strip’s party effects line grows as spells land on the party – so a label that sets the window’s minimum width moves that minimum under the player.
- SQUEEZED = 44¶
- class automap.panel.IconRow[source]¶
Bases:
PyQt6.QtWidgets.QWidgetA few icons in a line, painted from
automap.icons.Painted rather than assembled from
QLabelpixmaps so the row costs one widget however many icons it holds, and so a card whose conditions change does not rebuild its layout.
- class automap.panel.MessagesPanel[source]¶
Bases:
PyQt6.QtCore.QObjectWhat the tab has done, and what it is waiting for.
Not a pop-up. An action’s result is something you asked for; putting it behind a modal box interrupts the game in the other window to tell you what you already wanted to know, and has to be dismissed before the map is usable again. It belongs on the page. Only a genuinely irreversible action still asks first, and that question is a dialog because it needs an answer.
The connection’s own state feeds the same panel, so “something else is attached to the emulator” is one more line here rather than a second mechanism in the status bar.
- LIMIT = 200¶
Kept lines. Long enough for a session’s worth of actions, short enough that the panel never becomes the reason the window is slow.
- say(text, detail='', alarm=False, dedup=True)[source]¶
One line, timestamped. Repeats of the last line are dropped.
The connection says the same thing on every tick while it waits, and a panel that wrote “waiting for the game” five times a second would bury the line you wanted.
The combat log passes `dedup=False`, because there the repeat is the point: “MAGNUS MISSES.” twice running is two misses, and swallowing the second would defeat the feature that exists to catch them.
- class automap.panel.NotesPanel[source]¶
Bases:
PyQt6.QtCore.QObjectEvery note in this area, with its square.
This is what makes notes useful for finding something again, which the icons on the map alone do not solve: the map answers “what is here”, the list answers “where was that trainer”.
Clicking a row emits
chosen; the window flashes the square. The list is rebuilt only when the notes actually change, so a poll does not throw away the row you were about to click.
- class automap.panel.ReadiedLabel[source]¶
Bases:
automap.panel.ElidingLabelThe card’s line of what is in hand: bounded in both axes.
Width is the first of the two. The item names are read off the player’s disk, and a card whose floor was the width of
BANDED MAIL +1, SHIELD +2, LONG SWORD +3would put that string under the whole window (#41). The floor is zero rather thanElidingLabel’s 44 because the condition badges share this row and are drawn at a fixed width: this line takes whatever they leave, however little that is. Donald settled the order – “I would rather see active effects than readied items. That is a fine trade-off as far as space goes.” (#161)Height is the reason for the subclass. There are eight of these, one per party slot, in a column that does not scroll, so a line that insisted on its own height would add eight of them to the window’s floor – and the roster is already the tallest thing on the automapper page with a full party.
SHORT = 0says the line gives way first: it is the least important row on the card, and it is only ever squeezed by a window already pushed to its minimum. Anywhere above that floor the line is drawn in full.- SHORT = 0¶
The tallest this line may hold the window open. Zero, deliberately.
- SQUEEZED = 0¶
the badges beside it are drawn whole and this takes the remainder.
- Type:
The widest this line may hold the row open. Zero
- class automap.panel.RosterPanel[source]¶
Bases:
PyQt6.QtCore.QObjectThe cards, down the left. Manages the 8 pre-created cards in the unified form.
level_up_requestedcarries the slot up from whichever card was clicked, and nothing else: which class gets the level is decided from the record. The panel does not run the action itself – it has no target and no confirmation dialog, and both belong to the window.- ask_for_room(showing)[source]¶
Ask the layout for the width a card needs, once there is one.
A
QScrollAreareports a small minimum in both axes. Hiding the height is the whole point – eight cards in a column that could not scroll put a 944px floor under the window (#135) – but hiding the width is not: with the cards behind the scroll area the roster column collapsed to the width of its own heading, and a card was cut off somewhere in the middle of the name.So the column asks for a card’s width, and only while it has something to show; an empty roster asks for nothing, which is what the cards themselves used to do by being hidden.
ColumnSplitter.ROSTERis where that width lives now. It was the column’s ownmaximumWidthin the form until#162made the divider draggable, and a cap is the one thing a draggable column cannot have. What it means here is unchanged – a card is drawn to it – and it is a floor rather than a cap in both directions: the column can be dragged wider, and Qt’s own collapsing still shuts it altogether.- Parameters:
showing (int)
- Return type:
None
- cards: list[CharacterCard]¶
- scroll¶
The column the cards scroll inside, and the column that holds it.
- set_levelling(allowed)[source]¶
Whether this title can be levelled at all, and so whether the Level up button belongs on a card. Held for cards not built yet.
- Parameters:
allowed (bool)
- Return type:
None
- set_message(text)[source]¶
No party to show. Says why rather than showing empty cards.
- Parameters:
text (str)
- Return type:
None
- automap.panel.child(root, kind, name)[source]¶
The widget
namein the form, orNoneand a line saying which.Every panel in this file is wired to
wish/window.uibyobjectName, and every one of them carries on without a widget it cannot find – which is right, because a panel has to survive being built against a smaller form, and wrong when nobody is told.strip_effectsis why this exists. It was deleted from the form in72ee9a9while the layout was being simplified, and for months afterwardsBottomStripworked out the party’s active effects five times a second and wrote them intoNone. Nothing errored and nothing was logged, so a player who cast Bless saw nothing and no report ever said why –#142 (The party effects line is computed every poll and shown nowhere).The debug log and not a dialog: a missing widget is a fault in the build, not something the player did or can do anything about.
- Parameters:
root (PyQt6.QtWidgets.QWidget)
name (str)
- automap.panel.hp_colour(fraction)[source]¶
- Parameters:
fraction (float)
- Return type:
PyQt6.QtGui.QColor
- automap.panel.log = <Logger wish.automap.panel (Level 51)>[source]¶
A child of the
wishlogger, sowish/debuglog.py’s handler takes these when the log is on and the level swallows them when it is off – withoutautomapimportingwish.
- automap.panel.shortened(hint, floor)[source]¶
hintwith its height capped atfloor. Never heightens anything.The height twin of
_squeezed, and the same argument in the other axis: the window’s minimum height was the sum of the row heights its bars happened to want, so a larger UI font dragged the window down past a 720-high screen with nothing open at all – 662 at the base font here and 805 at ten points more (#77).A row that is given less height than its font wants clips rather than eliding, so unlike the width caps this one is only ever reached by a window squeezed to its floor. Everything above the floor lays out exactly as it did.
- Parameters:
hint (PyQt6.QtCore.QSize)
floor (int)
- Return type:
PyQt6.QtCore.QSize