From 896c7dc36a5421f5910d4dc218a1c595b07b50b2 Mon Sep 17 00:00:00 2001 From: Dominik Roth Date: Thu, 13 Aug 2026 18:44:36 +0200 Subject: [PATCH] Fix silent proposal-popover repaint bug; add underground target marker "Accept as..." (proposal type-picker) and "Change type" (entity-edit) opened to a visibly empty/unchanged popover with no traceback: swapping an already-open Popover's child and re-popup()ing it reported the right size internally but the compositor never repainted the reused surface. Confirmed live via temporary debug instrumentation, not guessed. Fixed by popping the old popover down and opening a genuinely new one at the same anchor point instead of resizing in place. Also adds a Target.underground_tier (1-3) marker: a "Mark underground" entry in the entity-edit popover, rendered as the game's own Armor-tier additive badge stacked directly on the unit icon. The badge is scaled/positioned off its real opaque content (PIL bbox), not its PNG canvas, since the additive art carries a lot of off-center transparent padding; and overlaps down into the icon by a fixed pixel amount, since both shapes taper to a point at the seam and exact bbox-touching still read as a visible gap. Co-Authored-By: Claude Sonnet 5 --- TODO.md | 29 +++++ .../targets/additives/Additive_1Star.png | Bin 0 -> 4205 bytes .../targets/additives/Additive_2Star.png | Bin 0 -> 6261 bytes .../targets/additives/Additive_3Star.png | Bin 0 -> 8265 bytes .../targets/additives/Additive_4Star.png | Bin 0 -> 9936 bytes .../targets/additives/Additive_5Star.png | Bin 0 -> 9378 bytes .../targets/additives/Additive_Armor1.png | Bin 0 -> 7521 bytes .../targets/additives/Additive_Armor2.png | Bin 0 -> 9425 bytes .../targets/additives/Additive_Armor3.png | Bin 0 -> 11792 bytes src/fenigma/app.py | 116 +++++++++++++++++- src/fenigma/grid_widget.py | 89 +++++++++++++- src/fenigma/icons.py | 70 +++++++++++ src/fenigma/models.py | 6 + 13 files changed, 304 insertions(+), 6 deletions(-) create mode 100644 assets/icons/targets/additives/Additive_1Star.png create mode 100644 assets/icons/targets/additives/Additive_2Star.png create mode 100644 assets/icons/targets/additives/Additive_3Star.png create mode 100644 assets/icons/targets/additives/Additive_4Star.png create mode 100644 assets/icons/targets/additives/Additive_5Star.png create mode 100644 assets/icons/targets/additives/Additive_Armor1.png create mode 100644 assets/icons/targets/additives/Additive_Armor2.png create mode 100644 assets/icons/targets/additives/Additive_Armor3.png diff --git a/TODO.md b/TODO.md index c1262db..3ea6eb2 100644 --- a/TODO.md +++ b/TODO.md @@ -119,6 +119,35 @@ Status legend: [x] fixed+tested, [~] partially addressed, [ ] open/needs input accept attempt after that silently died before the ally/target ever got added, popover already closed by the time it happened. Fixed there; not a separate bug. +- [x] "Accept as…" (the type-picker submenu on a proposal, and "Change + type" on an already-placed entity) opening to a visibly empty/ + unchanged popover. This one left no traceback at all -- confirmed + live with temporary debug prints that the button's `clicked` signal + fires, the icon grid builds successfully (all N types), and + `Popover.set_child()` on the already-open outer popover reports the + right `visible=True`/width/height afterward... but the compositor + never actually repaints that reused surface, so nothing new ever + appeared on screen. Fixed by not resizing the existing open + popover at all: popping it down and opening a genuinely new one + (fresh native surface) at the same anchor point instead. Same fix + applied to both call sites (`_open_proposal_menu`'s `show_type`, + `_open_entity_menu`'s `show_type`, the latter refactored to share + the same `_reopen_with()` helper). +- [x] New: mark a Target as underground, at a hardening tier (1-3), + rendered as the game's own Armor-tier additive badge stacked on + the icon. `Target.underground_tier: int | None`, a "Mark + underground" entry in the entity-edit popover (tier picker reusing + the same fresh-popover fix above), and `GridCanvas` draws the + badge above the marker's icon, overlapping down into it by + `_ADDITIVE_OVERLAP_PX` -- both the diamond icon's top corner and + the badge's bottom are tapered to a near-point, not a flat edge, + so bbox-exact touching still read as a gap; a real pixel overlap + is what actually looks contiguous (confirmed against the game's + own stacked-badge screenshots). Badge is scaled/positioned off the + art's real opaque content (PIL `getbbox()`), not its PNG canvas -- + the additive files carry a lot of off-center transparent padding + that made the badge look tiny and floating if sized off the raw + canvas. ## Needs more scope / your input before I keep going diff --git a/assets/icons/targets/additives/Additive_1Star.png b/assets/icons/targets/additives/Additive_1Star.png new file mode 100644 index 0000000000000000000000000000000000000000..8f433617dce806aa1f4a05d84857aec0b3467224 GIT binary patch literal 4205 zcmeHL{ZmtC7C!I2`7i{C5FsKfO#Pxt1s1BHq*A}8vUR}`r&JAuc7<3(p<*S7nv04q zqg!;kF0f*7r(Nx;3yf9?C6d>Vij}XKb^Ty%;aW?Wjwn}IAzLun^M>yBf9UuNN#1+j zbD#5^=REJdl&?y8%g<+u4*>WjuUNJkfZ$gG6BPJi*!p=6e#mn+EMK~M#wQP<@67wv z028MtFI&9EO!}_{_TqMcQP+6tloIi)H2fg5fNi6o7%H4_zGsKT{# z`IiANdH5yiVIEB+$7Okng6cLVdfpCMfiYTF6n@VJgELUvCYptHG=%2RKG10 zkpaIdrCm+4qu6}N3l=t{8}!TI@aO|CnnGz(X~v3OxF_gWi^OTVMrL{FyC}u zJm<+5?jjQdZ(l#4t5v#UeyHeQdeT1-MmalpLG?~%pm@8lSU%>gfK`tM&J#?LR$9*> zg|ZZ>!qU<-2TP-SB&aJaE#VA#WhiyB5q@`aY#kJvR$5{?5Tz*l)4PiFU zmB}UW$laQnlBgNrK5F!EVD-Tk)ybqcM4vZHKcaoKsI-VXh1EDv3EdV{n>j){Dl6bm z&>v;eC7Bc@`UhM2qC#BU0TX#C*x-fMD)avHdbylLu*dxC)1Z@LJmyz~f zKeLXy|Gh^CH0{GD2gPr697S9ja4izr+*(w$`&+?}Hp6fwYrOTe+RcF{neJd}9Zmf; z`vWZ+yIM^8>!;cjy__k>{AG`4vH8hbkZQrMwgn>2*$nJ#I#PO<(+5>A{QUMB<%OJ% zeT@#kz{(7tyBV6(4X1Q<%~McWU_K}PMcDJgvSi^R!c2dP|@O^+(I&#?< zE$rKW*d}%zP8cz)0DTY+;Zarq6GJNRt>WuBf4CZMQI_|P`F9pCg0c7oBg=6MS0$4+ zU?1vm7~I(Yi%HO{?2$_wHasGt<*PE?mEwLv9h_awcpKIsx)*ZSA}K_OSZx)q6-RT} zh5Xfu+zbm}Qxpbck=U_8-j-)U8@XRAbfHVQ(xb*AYeoVw@cG??;%8cHA*q8NKg`tu z7&?y7*d*0U&nkIp`83@=3EV-VkGK*g9Up^LSu=iNiS5Li5eJ=zbQQ%67wx`9H_I(PAt;m9k*H3R9ZyFB6XP) z`j0DN^j4cuZW&DkeL&V5QWV~_kqIE5AHV6=2W8U7jX>Q6J;hR^gZJ{`?fzz_qfp|d ztlqFY#QtzDYGYeZalwN!km}Jj(ov8O^w@4oq&P}VV7=j-DqXF;lD}IsT?^9S>#@Y) z;SJA85H$#`g-@D5>V*V0n31g7bS!EtK4D}zNM@l4Su(zduHoRSlnsrCH06c0nq^qX z%>aFX5W$BBLJ99A%o!}`_tkCI@MJ*`C;>5Gl<=ab{<`sW4G5Z zdv~QTdOCY!-2sbxDg>rykc0h~OM2zd6)gHPw`0;ULc83o?@CnO44Coz6T!4NnyAEs z#{FAEayR#I<+WpzgS++)b?#M1qwziaPPh2FQrJbD*cS$=+hfvY$^vN_YQ&N~6l{~0 zBn;|T7|gH0K%2T&0~N|DC8CkDIadpD=-~^xl)}3HOR~mO?C{}X#-AEU*Er7qah5h}JO?aK?sfq|9N z_N;@{uWn3Oxx+p5%6d(-WuC}$h-V{HQzD~iJDns%oHK{m2YHkoN^|jiw0N1fS&vz> z?vpgIZblELx1KjT2z^M_F^eCUNsL_Vmf6)+ni`co*rxE3|FIHt$BpM+3tb8^LqJ1- z`QRWaa({k{%sj0x_Mdq03ryG5UL;wwD9#{h$1!e*rr%N(p2#I^ubE9R^B|A!yP6tO zqsn6wi>6yz%(r~YDl?rQW27*Dg<3V6o*a8sEK}PT~ z5=U}LJeO8F$PeVxV6hR)-&7h2xp=%GL*83Oira)_8icgSvLdJ@6y3cVfA|Qp7#$}1 zJ4>{l3;~uFf>#s*HdZ07#3S~NtgwQGcr}zg!^HCpZJo?zX&w Q{|At~JY`wklB_-d1uss7NdN!< literal 0 HcmV?d00001 diff --git a/assets/icons/targets/additives/Additive_2Star.png b/assets/icons/targets/additives/Additive_2Star.png new file mode 100644 index 0000000000000000000000000000000000000000..811ca0d2de741897b66ff83cb2b67267155d1e7a GIT binary patch literal 6261 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 EzkQivegFUf literal 0 HcmV?d00001 diff --git a/assets/icons/targets/additives/Additive_4Star.png b/assets/icons/targets/additives/Additive_4Star.png new file mode 100644 index 0000000000000000000000000000000000000000..73bc6bec7801c04715095429e57dbd0c6544d9da GIT binary patch literal 9936 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 literal 0 HcmV?d00001 diff --git a/assets/icons/targets/additives/Additive_5Star.png b/assets/icons/targets/additives/Additive_5Star.png new file mode 100644 index 0000000000000000000000000000000000000000..526d95c4cd5f264e333a3a68e66ccd835ea11552 GIT binary patch literal 9378 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 LU36q&^0xm5PDN$; literal 0 HcmV?d00001 diff --git a/assets/icons/targets/additives/Additive_Armor2.png b/assets/icons/targets/additives/Additive_Armor2.png new file mode 100644 index 0000000000000000000000000000000000000000..4ff94b53f1558249b16e47361afbd22c5abf9036 GIT binary patch literal 9425 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 g{(rsIY;|h&OTICT`uiYU@ka9l7R@>RPS}?J0Qe bool: + """SIGUSR1 handler: pickle {board, screenshot_import} so a + following relaunch can pick this session right back up. See the + SIGUSR1 registration in __init__ for why this exists.""" + try: + with open(self._DEV_SESSION_PATH, "wb") as f: + pickle.dump({"board": self.board, "screenshot_import": self.screenshot_import}, f) + print(f"fenigma: dev session dumped to {self._DEV_SESSION_PATH}", flush=True) + except Exception as exc: + print(f"fenigma: dev session dump failed: {exc!r}", flush=True) + return GLib.SOURCE_CONTINUE + + def _dev_maybe_restore_session(self) -> None: + """Counterpart to `_dev_dump_session_for_restart`: one-shot restore + on startup if a dump is sitting there. Sets `self.board`/ + `self.screenshot_import` directly (before the rest of __init__ + builds the widgets that reference them) but defers the actual + redraw to an idle callback, since `self.canvas` doesn't exist yet + at this point in __init__.""" + if not self._DEV_SESSION_PATH.exists(): + return + try: + with open(self._DEV_SESSION_PATH, "rb") as f: + data = pickle.load(f) + self.board = data["board"] + self.screenshot_import = data["screenshot_import"] + self._DEV_SESSION_PATH.unlink() + print("fenigma: dev session restored", flush=True) + except Exception as exc: + print(f"fenigma: dev session restore failed: {exc!r}", flush=True) + return + GLib.idle_add(self._dev_finish_session_restore) + + def _dev_finish_session_restore(self) -> bool: + self._refresh() + self._refresh_proposals() + if self.screenshot_import is not None: + self.canvas.set_screenshot(self.screenshot_import.overlay, self.screenshot_import.px_per_km) + return GLib.SOURCE_REMOVE + def _accept_proposal(self, proposal, type_=None) -> None: coord = _coord_from_proposal(proposal) if coord is None: @@ -794,15 +847,29 @@ class MainWindow(Adw.ApplicationWindow): # real pickable unit types (see their own comments in # models.py) and shouldn't have been offered as "what this # detected marker actually is". + grid = icons.build_target_type_grid( + detected, lambda t: accept(t), is_ally=(proposal.side == "friendly"), + ) scroller = Gtk.ScrolledWindow(propagate_natural_height=True, propagate_natural_width=True, max_content_height=340, hscrollbar_policy=Gtk.PolicyType.NEVER) - scroller.set_child(icons.build_target_type_grid( - detected, lambda t: accept(t), is_ally=(proposal.side == "friendly"), - )) + scroller.set_child(grid) box.append(scroller) + # Swapping the child of an ALREADY-open Popover and re-popup()ing + # it reports the right size internally (visible=True, sane + # width/height) but the compositor never actually repaints the + # reused surface -- confirmed live: nothing appears on screen no + # matter how many times it's reopened. Popping the OLD popover + # down and opening a genuinely NEW one (fresh native surface, + # same anchor point) instead of resizing the existing one + # sidesteps that. + nonlocal popover + old_popover = popover + popover = self._popover_at(x, y) popover.set_child(box) + old_popover.popdown() + popover.popup() show_main() popover.popup() @@ -1521,10 +1588,31 @@ class MainWindow(Adw.ApplicationWindow): # panel's own alive button offers, just reachable from the # map too rather than only from the sidebar. button(box, "Mark destroyed" if obj.alive else "Mark alive", toggle_alive) + # Underground is Target-only too, see Target.underground_tier's + # own comment -- no such thing as an underground Ally. + ug_label = ("Mark underground" if obj.underground_tier is None + else f"Underground (tier {obj.underground_tier})") + button(box, ug_label, show_underground) if not isinstance(obj, Nest): button(box, "Delete", delete, css="destructive-action") popover.set_child(box) + def _reopen_with(box) -> None: + """Swapping an ALREADY-open Popover's child via set_child() + alone reports the right size (visible=True, sane width/height) + but the compositor never actually repaints the reused surface + on some setups -- confirmed live, nothing appears on screen no + matter how many times it's reopened. Popping the OLD popover + down and opening a genuinely NEW one at the same anchor point + (fresh native surface, not an in-place resize) sidesteps it. + Shared by every page past show_main() in this menu.""" + nonlocal popover + old_popover = popover + popover = self._popover_at(x, y) + popover.set_child(box) + old_popover.popdown() + popover.popup() + def show_type(): box = page() heading(box, "Type") @@ -1542,7 +1630,7 @@ class MainWindow(Adw.ApplicationWindow): obj.type, lambda t: set_type(t), is_ally=isinstance(obj, Ally), )) box.append(scroller) - popover.set_child(box) + _reopen_with(box) def set_type(t): obj.type = t @@ -1551,6 +1639,26 @@ class MainWindow(Adw.ApplicationWindow): self.toast(f"{_display_name(obj)} is now a " f"{icons.target_type_label(t, isinstance(obj, Ally))}.") + def show_underground(): + box = page() + heading(box, "Underground") + scroller = Gtk.ScrolledWindow(propagate_natural_height=True, + propagate_natural_width=True, + max_content_height=340, + hscrollbar_policy=Gtk.PolicyType.NEVER) + scroller.set_child(icons.build_underground_tier_grid( + obj.underground_tier, lambda tier: set_underground(tier), + )) + box.append(scroller) + _reopen_with(box) + + def set_underground(tier): + obj.underground_tier = tier + self._refresh() + popover.popdown() + self.toast(f"{_display_name(obj)} is no longer underground." if tier is None + else f"{_display_name(obj)} is now underground (tier {tier}).") + def show_id(): box = page() heading(box, "ID") diff --git a/src/fenigma/grid_widget.py b/src/fenigma/grid_widget.py index 60843e1..005a4ed 100644 --- a/src/fenigma/grid_widget.py +++ b/src/fenigma/grid_widget.py @@ -12,6 +12,7 @@ from collections import namedtuple import cairo import gi import numpy as np +from PIL import Image as PILImage gi.require_version("Gtk", "4.0") gi.require_version("Gdk", "4.0") @@ -195,6 +196,44 @@ def _icon_for(category: str, obj) -> cairo.ImageSurface | None: return None +# path -> (surface, content_bbox) for additive badges specifically. +# Separate from _ICON_SURFACE_CACHE because these also need their real +# opaque content's bounding box: unlike the unit icons (already ~edge to +# edge in their own canvas, see _draw_icon_marker), the additive art +# (assets/icons/targets/additives/) sits inside a lot of transparent +# padding that isn't even centered -- scaling/positioning off the full +# 256x256 canvas made the badge look tiny and float with a visible gap +# above the icon it's supposed to touch. bbox is None for a path that +# failed to load, or (l, t, r, b) of its actual opaque pixels. +_ADDITIVE_CACHE: dict = {} + + +def _additive_surface(path) -> tuple: + if path not in _ADDITIVE_CACHE: + surface, bbox = None, None + try: + surface = cairo.ImageSurface.create_from_png(str(path)) + bbox = PILImage.open(str(path)).getbbox() + except Exception: + pass + _ADDITIVE_CACHE[path] = (surface, bbox) + return _ADDITIVE_CACHE[path] + + +def _additive_for(category: str, obj) -> tuple | None: + """The underground-tier badge overlaid on top of a Target's own icon, + or None. Target-only (see Target.underground_tier's own comment).""" + if category != "target": + return None + tier = getattr(obj, "underground_tier", None) + if tier is None: + return None + surface, bbox = _additive_surface(icons.underground_icon_path(tier)) + if surface is None: + return None + return (surface, bbox) + + class GridCanvas(Gtk.DrawingArea): def __init__(self, board: Board) -> None: super().__init__() @@ -817,7 +856,8 @@ class GridCanvas(Gtk.DrawingArea): dim=(category == "target" and not obj.alive) or obj.hidden, selected=(obj is self.selected), coord=obj.coord, extra_line=getattr(obj, "requested_time", None), - icon_surface=_icon_for(category, obj)) + icon_surface=_icon_for(category, obj), + additive=_additive_for(category, obj)) for category, obj in self.board.ambiguous_entities_all(): if self._excluded_from_map(obj): @@ -878,7 +918,8 @@ class GridCanvas(Gtk.DrawingArea): def _draw_marker(self, cr, view, point_km, color, label, canvas_width, canvas_height, *, hollow=False, dim=False, - selected=False, coord=None, extra_line=None, icon_surface=None) -> None: + selected=False, coord=None, extra_line=None, icon_surface=None, + additive=None) -> None: x, y = self._km_to_px(view, point_km) r, g, b = color alpha = 0.45 if dim else 1.0 @@ -898,6 +939,8 @@ class GridCanvas(Gtk.DrawingArea): # plain filled dot is more honest about the current zoom level. if not hollow and icon_surface is not None and view.cell_w >= ICON_MIN_CELL_PX: self._draw_icon_marker(cr, x, y, icon_surface, alpha) + if additive is not None: + self._draw_additive_badge(cr, x, y, additive, alpha) elif hollow: cr.new_path() # cairo's arc() draws a line from any stale current cr.set_source_rgba(r, g, b, alpha) # point (e.g. the last label's @@ -962,6 +1005,48 @@ class GridCanvas(Gtk.DrawingArea): cr.paint_with_alpha(alpha) cr.restore() + # How far the badge's content bbox sinks into the icon's, in the + # icon's own 32px box units. Both the diamond's top corner and the + # Armor badge's bottom are tapered to a near-point, not a flat edge + # (see assets/icons/targets/enemy/Enemy_Infantry.png and the Armor + # additives) -- lining up their bboxes exactly *touching* leaves them + # meeting at a single pixel with no visual mass on either side of it, + # which still reads as a gap. A real pixel overlap is what actually + # looks contiguous, confirmed against the game's own stacked-badge + # screenshots (stars/helmet/diamond all overlapping, not edge-to-edge). + _ADDITIVE_OVERLAP_PX = 10.0 + + def _draw_additive_badge(self, cr, x, y, additive, alpha) -> None: + """A badge (underground tier, currently the only additive) drawn + directly north of the icon marker, overlapping down into it by + `_ADDITIVE_OVERLAP_PX`, at the same full size as the marker + itself -- stacked above it rather than shrunk into a corner, so + it reads as its own clearly-legible symbol, not a tiny decoration + obscuring the unit icon it modifies. + + Scaled/positioned off the source art's actual opaque content + (`bbox`), not its full canvas: the additive PNGs carry a lot of + transparent padding that isn't even centered (see _ADDITIVE_CACHE's + comment), so sizing/placing off the raw canvas made the badge look + tiny and float with a visible gap above the icon -- using bbox + instead makes what's actually drawn sit right against it.""" + surface, bbox = additive + sw, sh = surface.get_width(), surface.get_height() + left, top, right, bottom = bbox if bbox is not None else (0, 0, sw, sh) + content_w, content_h = right - left, bottom - top + if content_w <= 0 or content_h <= 0: + return + box = 32.0 # same visual size as the icon marker's own box + scale = box / max(content_w, content_h) + icon_top = y - 16 # _draw_icon_marker's own box=32, centered on y + ty = icon_top - bottom * scale + self._ADDITIVE_OVERLAP_PX + cr.save() + cr.translate(x - (left + right) / 2 * scale, ty) + cr.scale(scale, scale) + cr.set_source_surface(surface, 0, 0) + cr.paint_with_alpha(alpha) + cr.restore() + def _draw_firing_arrows(self, cr, view) -> None: """Red arrow(s) Nest -> Target, for whatever's hovered or selected. Points at exactly the hovered/selected candidate when one is known diff --git a/src/fenigma/icons.py b/src/fenigma/icons.py index 1ef7efe..5a845d7 100644 --- a/src/fenigma/icons.py +++ b/src/fenigma/icons.py @@ -449,6 +449,76 @@ def target_type_icon_image(target_type: "TargetType", is_ally: bool = False, wid return _plain_dot(is_ally, width) +_ADDITIVES_DIR = _ICONS_DIR / "targets" / "additives" +UNDERGROUND_TIERS = (1, 2, 3) + + +def underground_icon_path(tier: int) -> Path: + """The badge overlaid on a Target's own icon when it's marked + underground at this tier (1..3, harder to hit = higher). Reuses the + game's own Armor-tier additive art (assets/icons/targets/additives/ + Additive_Armor{1,2,3}.png) rather than inventing bespoke "underground" + art of our own -- there's nothing else in the game's icon set for + "buried/fortified", and Armor's own visual (a plate) already reads + right for that.""" + return _ADDITIVES_DIR / f"Additive_Armor{tier}.png" + + +def underground_tier_image(tier: int | None, width: int = _TYPE_GRID_ICON_WIDTH) -> Gtk.Widget: + """A widget for one cell of the underground-tier picker: the additive + badge itself for a real tier, or a plain dot (this module's usual + 'nothing chosen' placeholder) for the "not underground" cell.""" + if tier is None: + return _plain_dot(False, width) + path = underground_icon_path(tier) + if path.exists(): + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(str(path), width, -1, True) + picture = Gtk.Picture.new_for_pixbuf(pixbuf) + picture.set_content_fit(Gtk.ContentFit.CONTAIN) + picture.set_can_shrink(True) + picture.set_size_request(pixbuf.get_width(), pixbuf.get_height()) + return picture + return _plain_dot(False, width) + + +def build_underground_tier_grid(selected: int | None, on_pick) -> Gtk.Widget: + """Same radio-style grid idea as build_target_type_grid, just over + (None, 1, 2, 3) instead of TargetType -- None first, as "not + underground" (clearing an existing tier) is exactly as valid a pick + as any real tier, not a separate "remove" action bolted on + afterward.""" + _ensure_icon_button_css() + leader: Gtk.ToggleButton | None = None + items = [None, *UNDERGROUND_TIERS] + + def make_button(tier: int | None) -> Gtk.Widget: + nonlocal leader + cell = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2, + margin_top=4, margin_bottom=4, margin_start=2, margin_end=2) + cell.append(underground_tier_image(tier)) + label = Gtk.Label(label="None" if tier is None else f"Tier {tier}", + wrap=False, single_line_mode=True, + justify=Gtk.Justification.CENTER, width_chars=9, + max_width_chars=9, ellipsize=Pango.EllipsizeMode.END) + label.add_css_class("caption") + label.add_css_class("dim-label") + cell.append(label) + btn = Gtk.ToggleButton(child=cell) + btn.add_css_class("flat") + btn.add_css_class(_ICON_BUTTON_CSS_CLASS) + btn.set_tooltip_text("Not underground" if tier is None else f"Underground, tier {tier}") + if leader is None: + leader = btn + else: + btn.set_group(leader) + if tier is selected: + btn.set_active(True) + btn.connect("clicked", lambda _b, tier=tier: on_pick(tier)) + return btn + + return _build_icon_grid(items, _TYPE_GRID_COLUMNS, make_button) + + def _has_own_icon(t: "TargetType", is_ally: bool) -> bool: """Whether THIS side specifically has real art for t -- as opposed to target_icon_path() quietly handing back the other side's icon because diff --git a/src/fenigma/models.py b/src/fenigma/models.py index d810469..2a9b26f 100644 --- a/src/fenigma/models.py +++ b/src/fenigma/models.py @@ -411,6 +411,10 @@ class Target: # raw string as printed, this app doesn't track a game clock to compare # it against, it's shown as-is for the player's own reference. requested_time: str | None = None + # None = not underground. 1..3 = underground, at that hardening tier + # (see icons.UNDERGROUND_TIERS) -- higher survives more. Target-only: + # there's no such thing as an underground Ally in this game. + underground_tier: int | None = None @property def name(self) -> str: @@ -770,6 +774,7 @@ class Board: "shell": t.shell.name if t.shell is not None else None, "assignment": t.assignment, "requested_time": t.requested_time, + "underground_tier": t.underground_tier, } for t in self.targets ], @@ -837,6 +842,7 @@ class Board: shell=Shell[t["shell"]] if t.get("shell") else None, assignment=t.get("assignment", "unassigned"), requested_time=t.get("requested_time"), + underground_tier=t.get("underground_tier"), ) for t in data.get("targets", []) ]