UO Unchained: UI_boss_health_bar.py

Created: 3 days ago on 09/13/2025, 03:07:01 AM
FileType: Razor Enhanced (Python)
Size: 40776
Category: UI

Description: display large boss health when nearby , includes a few but not all known bosses

""" UI Boss Health Bar - a Razor Enhanced Python Script for Ultima Online an auto displaying health bar for known bosses , a custom gump top center that is otherwise hidden an arpg style big red boss enemy health bar , phase markers , side image embellishments features to open classic uo healthbar and attack boss onsight option image elements can be toggled in settings ___.o.____ ..o\<{|[ | Boss Name | ]|}>\o.. TODO: - each boss different color tint presets ( red default , darkness purple , poison green , grey ) HOTKEY:: AutoStart on Login VERSION:: 20250829 """ import time DEBUG_MODE = False # Debug toggle , sends messages # SETTINGS GET_CUO_HEALTHBAR = True # If True, attempt to use CUO.OpenMobileHealthBar() to open the bosses in game healthbar AUTO_ATTACK_NEW_BOSSES = False # If True, auto-attack a boss the first time it's detected SHOW_PHASE_MARKERS = True # thirds SHOW_HEALTH_NUMBERS = False # not useful , most health is diplayed out of 25 # GUMP images toggle SIDE_IMAGES_ENABLED = True FLOURISH_ARROWS_ENABLED = True TOP_CENTER_IMAGE_ENABLED = True # + KNOWN_BOSSES_UO: base UO boss names (base -> display) # + KNOWN_BOSSES_CUSTOM: custom shard boss names # >>> KNOWN_BOSSES: merged KNOWN_BOSSES_UO = { "abyssal infernal": "Abyssal Infernal", "abyssmal horror": "Abyssmal Horror", "anon": "Anon", "barracoon the piper": "Barracoon the Piper", "barracoon": "ΒΈβ€ž.-β€’~¹°”ˆ Barracoon Λœβ€Β°ΒΉ~β€’-.β€ž.", "bone daemon": "Bone Daemon", "charybdis": "Charybdis", "chief paroxysmus": "Chief Paroxysmus", "cora the sorceress": "Cora the Sorceress", "corgul the soulbinder": "Corgul the Soulbinder", "dark father": "Dark Father", "darknight creeper": "Darknight Creeper", "dragon turtle": "Dragon Turtle", "dread horn": "Dread Horn", "drelgor the impaler": "Drelgor the Impaler", "exodus": "Exodus", #"fleshrenderer": "Fleshrenderer", "ilhenir the stained": "Ilhenir the Stained", "impaler": "Impaler", "juo'nar": "Juo'nar", "khal ankur": "Khal Ankur", "lady melisande": "Lady Melisande", "lord oaks": "Lord Oaks", "medusa": "Medusa", "mephitis": "Mephitis", "meraktus the tormented": "Meraktus the Tormented", "moltrog the void seer": "Moltrog The Void Seer", "monstrous interred grizzle": "Monstrous Interred Grizzle", "navrey night-eyes": "Navrey Night-Eyes", "neira the necromancer": "Neira the Necromancer", "osiredon the scalis enforcer": "Osiredon the Scalis Enforcer", "ozymandias": "Ozymandias", #"primeval lich": "Primeval Lich", "rikktor": "Rikktor", "semidar": "─ +† Semidar †+ ─", #"shadow knight": "Shadow Knight", "shadowlords": "Shadowlords", "shanty the pirate": "Shanty the Pirate", "shimmering effusion": "Shimmering Effusion", "silvani": "Silvani", "slasher of veils": "Slasher of Veils", "stygian dragon": "Stygian Dragon", "the harrower": "The Harrower", "travesty": "Travesty", "twaulo of the glade": "Twaulo of the Glade", "virtuebane": "Virtuebane", "vorothal the mindflayer": "Vorothal the Mindflayer", "zipactriotl": "Zipactriotl", } # unchained KNOWN_BOSSES_CUSTOM = { "ixir": "Ixir", "goliath": "Goliath", "yog-sothoth": "YOG-SOTHOTH", "skeleton king": "Skeleton King", "bog king": "Bog King", "celestus": "Celestus", "gargantua": "Gargantua", "gorgon": "Gorgon", # demon "norg": "Norg", # giant "malcanthet": "Malcanthet", "surgat": "Surgat", "eligor": "Eligor", # wizard "the undying": "The Undying", "lord of darkness": "Lord of Darkness", "sandstorm templar": "Sandstorm Templar", "a dread horn": "A Dread Horn", # miniboss in sandstorm "an abomination": "An Abomination", # miniboss in sandstorm "a flesh devourer": "-= I T T H A T D E V O U R S =-", # miniboss in wrong "test a harpy": "─ +† Semidar †+ ─", "test a giant rat": "Celestus", } # Merge with custom overriding defaults where keys overlap KNOWN_BOSSES = {**KNOWN_BOSSES_UO, **KNOWN_BOSSES_CUSTOM} #//========================================================================== #TIMING UPDATE_RATE = 1000 #ms # idle/default update rate FAST_UPDATE_RATE = 300 # ms # When a boss has been seen recently, use a faster update RECENT_BOSS_SEEN_MS = 5000 # Consider a boss "recently seen" if spotted within this window SCAN_RANGE_MAX = 30 # tiles to scan for boss candidates STALE_MS = 15000 # prune unseen candidates after 15s # Unique gump ID (large, unlikely to collide) GUMP_ID = 3411114999 GUMP_ART = { "BACKGROUND": 3500, # Standard gump background "BAR": 5210, # 12x16 pixel bar segment (tinted for fill) "BAR_BG": 2624, # Dark tile for depleted portion background } # UI image assets for side embellishments (dragon edges) # Keys match requested naming to reference IDs by descriptive name # Left: 0x28B4 | Right: 0x28BE UI_GUMP_IMAGES = { "ui_quest_edgeDragon_0x28B4": 0x28B4, # left edge "ui_quest_edgeDragon_0x28BE": 0x28BE, # right edge } # Flourish arrow gump images (rendered behind everything else) FLOURISH_IMAGES = { "left_flourish_arrow_red": 4235, # left flourish arrow red "right_flourish_arrow_red": 4236, # right flourish arrow red } # Dimensions and layout # Width stays a multiple of 12 for clean tiling of 12px bar art BAR_WIDTH = 432 # wider (36 * 12) BAR_HEIGHT = 25 # taller (will stack 12x16 segments vertically) PADDING = 6 # Padding inside background OUTLINE_THICKNESS = 2 # Visual border thickness OUTLINE_IMAGE_TINT = 37 # Tint hue for the bar's image outline (use a dark red/gray). Set to None for no tint. # Name label controls NAME_OFFSET_X = 0 # horizontal tweak from centered position NAME_OFFSET_Y = 17 # vertical offset from top padding (move down by increasing) NAME_DUPLICATE_COUNT = 2 # how many times to draw the main red label for boldness # Text width tuning for centering ( this is not good , not working well ) NAME_CHAR_W_LOWER = 5 NAME_CHAR_W_UPPER = 5 NAME_CHAR_W_DIGIT = 5 NAME_CHAR_W_SPACE = 5 NAME_CHAR_W_PUNCT = 5 NAME_CHAR_W_EXT = 5 # non-ascii decorative symbols NAME_EXTRA_OUTLINE_W = 0 # extra for glow/outline spread # Optional side images (left/right) to embellish the bar SIDE_IMAGE_W = 59 # approximate width of the dragon edge art SIDE_IMAGE_H = 25 # approximate height of the dragon edge art SIDE_IMAGE_GAP = 0 # spacing between side image and bar SIDE_IMAGE_LEFT_KEY = "ui_quest_edgeDragon_0x28B4" SIDE_IMAGE_RIGHT_KEY = "ui_quest_edgeDragon_0x28BE" # Stacking to make images appear bigger (simulated scaling) SIDE_IMAGE_STACK = True SIDE_IMAGE_GROW_PIX = 6 # visual growth by layering; 0 disables growth SIDE_IMAGE_TINT = 1250 # dark red tint for side embellishments (distinct from filigree) #2038 # Flourish arrow configuration (rendered behind everything else) FLOURISH_LEFT_OFFSET_X = 60 # left flourish position from left edge FLOURISH_RIGHT_OFFSET_X = 70 # right flourish position from right edge (double the left offset) FLOURISH_OFFSET_Y = -38 # vertical offset (negative = higher up) FLOURISH_TINT = 2999 # dark hue for flourishes # Top-center filigree (arch) image TOP_CENTER_IMAGE_ID = 30077 # center filigree arch TOP_CENTER_IMAGE_TINT = 2999 # even darker red hue; fallback to COLOR['bar_fill'] if needed TOP_CENTER_IMAGE_OFFSET_Y = -6 # place slightly above bar_y TOP_CENTER_IMAGE_OFFSET_X = 0 # fine-tune centering if needed TOP_CENTER_IMAGE_W = 64 # approximate width used for centering # Screen placement (top of screen) GUMP_X = 540 # X position; adjust to your preference GUMP_Y = 0 # Near the top edge # CUO Health Bar placement (middle of screen) CUO_HEALTHBAR_X = 500 # X position for CUO health bars CUO_HEALTHBAR_Y = 400 # Y position for CUO health bars # Colors COLOR = { "text": 2000, # White (generic text) "text_border": 0, # Black "bar_fill": 37, # Bright Red (health fill hue) #38 "bar_depleted": 2999, # Very dark gray "background_tint": 0, # Pure black tint for background "boss_name": 38, # Boss name in red "boss_name_glow": 0, # Outline/glow color (black) "phase_marker": 2999, # Dark gray for phase dividers } #//========================================================================== class BossHealthBar: def __init__(self): self.gump_id = GUMP_ID self.boss_serial = None self.active = False self.last_update = 0 self.update_delay_ms = UPDATE_RATE self.last_boss_seen_ms = 0 # timestamp (ms) when any boss candidate was last seen # Cache prior health to optionally animate loss , this isnt very useful because the server returns out of 25 , and boss health is slowly moving. self.prev_hits = None # Pre-calc total gump size extra_w = 0 if SIDE_IMAGES_ENABLED: grow = SIDE_IMAGE_GROW_PIX if SIDE_IMAGE_STACK else 0 extra_w = ((SIDE_IMAGE_W + grow) * 2) + (SIDE_IMAGE_GAP * 2) self.total_width = BAR_WIDTH + PADDING * 2 + extra_w # Ensure tall enough to show side images if enabled bar_area_h = BAR_HEIGHT + PADDING * 2 + 16 # room for name label side_h = (SIDE_IMAGE_H + PADDING * 2) if SIDE_IMAGES_ENABLED else 0 self.total_height = max(bar_area_h, side_h) # Boss candidates/info cache: serial -> info # info = { # 'name': str, 'key': str, 'display': str, # 'last_seen_ms': int, 'distance': int, # 'hits': int, 'hits_max': int, # 'x': int, 'y': int, 'map': int # } self.boss_info = {} def debug_message(self, msg, color=68): if DEBUG_MODE: try: Misc.SendMessage(f"[BOSSUI] {msg}", color) except Exception: try: print(f"[BOSSUI] {msg}") except Exception: pass def attempt_auto_attack(self, mob): """Attempt to attack a newly seen boss once if enabled.""" if not AUTO_ATTACK_NEW_BOSSES: return try: Player.Attack(mob) # In case a targeting cursor is up, send it to the mob if Target.HasTarget(): Target.TargetExecute(mob) self.debug_message(f"Auto-attack issued to newly detected boss: {getattr(mob, 'Name', mob.Serial)}") except Exception as e: self.debug_message(f"attempt_auto_attack error: {e}") def open_cuo_healthbar(self, mob): """Attempt to open CUO mobile health bar for the boss if enabled.""" if not GET_CUO_HEALTHBAR: return try: # CUO.OpenMobileHealthBar(mobileserial, x, y, custom) # Parameters: mobileserial (UInt32), x (Int32), y (Int32), custom (Boolean) CUO.OpenMobileHealthBar(mob.Serial, CUO_HEALTHBAR_X, CUO_HEALTHBAR_Y, True) self.debug_message(f"Opened CUO health bar for boss: {getattr(mob, 'Name', mob.Serial)} at ({CUO_HEALTHBAR_X}, {CUO_HEALTHBAR_Y})") except Exception as e: self.debug_message(f"open_cuo_healthbar error: {e}") #//========= Boss Detection & Matching =============== def get_boss_mobile(self): if not self.boss_serial: return None try: mob = Mobiles.FindBySerial(self.boss_serial) return mob except Exception: return None def normalize(self, s): try: return s.lower().strip() except Exception: return "" def match_known_boss(self, name): """Return (base_key, display) if name contains a known base boss name, else (None, None).""" n = self.normalize(name) if not n: return None, None for base, display in KNOWN_BOSSES.items(): b = self.normalize(base) if b and b in n: return base, display return None, None def distance_to_player(self, mob): try: p = Player.Position m = mob.Position return max(abs(p.X - m.X), abs(p.Y - m.Y)) except Exception: return 9999 def scan_for_boss_candidates(self): """Scan nearby mobiles and update boss_info for those matching KNOWN_BOSSES.""" try: f = Mobiles.Filter() f.Enabled = True f.RangeMax = SCAN_RANGE_MAX # Prefer hostiles; if unavailable, leave empty to include all # consider Lord Oaks or potential Blue bosses try: f.Notorieties.Add(3) # gray f.Notorieties.Add(4) # criminal f.Notorieties.Add(5) # orange/war f.Notorieties.Add(6) # red except Exception: pass mobs = Mobiles.ApplyFilter(f) now = int(time.time() * 1000) if mobs: self.debug_message(f"Scanning {len(mobs)} mobiles within {SCAN_RANGE_MAX} tiles...") for mob in mobs: try: if not mob or not mob.Name: continue key, display = self.match_known_boss(mob.Name) if not key: self.debug_message(f"Seen mobile (no match): {mob.Name} @ {mob.Position.X},{mob.Position.Y} (serial {mob.Serial})") continue dist = self.distance_to_player(mob) # only keep reasonably close if dist > SCAN_RANGE_MAX: self.debug_message(f"Match but out of range: {mob.Name} dist {dist}") continue is_new = mob.Serial not in self.boss_info info = self.boss_info.get(mob.Serial, {}) info.update({ 'name': mob.Name, 'key': key, 'display': display or mob.Name, 'last_seen_ms': now, 'distance': dist, 'hits': int(getattr(mob, 'Hits', 0) or 0), 'hits_max': int(getattr(mob, 'HitsMax', 1) or 1), 'x': getattr(mob.Position, 'X', 0), 'y': getattr(mob.Position, 'Y', 0), 'map': getattr(mob, 'Map', 0), }) self.boss_info[mob.Serial] = info if is_new: self.attempt_auto_attack(mob) self.open_cuo_healthbar(mob) # mark recent boss sighting self.last_boss_seen_ms = now self.debug_message(f"Matched boss candidate: [{info['display']}] serial {mob.Serial} dist {dist} HP {info['hits']}/{info['hits_max']}") except Exception: continue # prune stale to_del = [] for serial, info in self.boss_info.items(): if now - info.get('last_seen_ms', 0) > STALE_MS: to_del.append(serial) for serial in to_del: self.boss_info.pop(serial, None) if to_del: self.debug_message(f"Pruned {len(to_del)} stale candidates") except Exception as e: self.debug_message(f"scan_for_boss_candidates error: {e}") def auto_select_best_boss(self): """If no manual selection, choose the closest boss candidate.""" if self.boss_serial: return try: if not self.boss_info: return best = None best_dist = None for serial, info in self.boss_info.items(): dist = info.get('distance', 9999) if best_dist is None or dist < best_dist: best_dist = dist best = serial if best: self.boss_serial = best self.prev_hits = None self.debug_message(f"Auto-selected boss: {self.boss_info[best].get('display')} [{best}]") except Exception as e: self.debug_message(f"auto_select_best_boss error: {e}") #//======== GUMP UI =============== def estimate_label_width(self, text): try: w = 0 for ch in text: o = ord(ch) if ch == ' ': w += NAME_CHAR_W_SPACE elif '0' <= ch <= '9': w += NAME_CHAR_W_DIGIT elif 'a' <= ch <= 'z': w += NAME_CHAR_W_LOWER elif 'A' <= ch <= 'Z': w += NAME_CHAR_W_UPPER elif o > 127: w += NAME_CHAR_W_EXT else: # punctuation and others w += NAME_CHAR_W_PUNCT return w + NAME_EXTRA_OUTLINE_W except Exception: # fallback similar to old behavior return max(0, len(text) * 4) def draw_bar(self, gump, x, y, width, height, current, maximum): """Draw a horizontal bar with dark outline and solid red fill.""" width = max(0, int(width)) height = max(1, int(height)) # Outline area (slightly larger dark border) outline_x = x - OUTLINE_THICKNESS outline_y = y - OUTLINE_THICKNESS outline_w = width + OUTLINE_THICKNESS * 2 outline_h = height + OUTLINE_THICKNESS * 2 # Draw outline by tiling the bar sprite with precise thickness # Top border Gumps.AddImageTiled(gump, outline_x, outline_y, outline_w, OUTLINE_THICKNESS, GUMP_ART["BAR"]) # Bottom border Gumps.AddImageTiled(gump, outline_x, outline_y + outline_h - OUTLINE_THICKNESS, outline_w, OUTLINE_THICKNESS, GUMP_ART["BAR"]) # Left border Gumps.AddImageTiled(gump, outline_x, outline_y, OUTLINE_THICKNESS, outline_h, GUMP_ART["BAR"]) # Right border Gumps.AddImageTiled(gump, outline_x + outline_w - OUTLINE_THICKNESS, outline_y, OUTLINE_THICKNESS, outline_h, GUMP_ART["BAR"]) # Depleted background of the bar (use a dark tile so fill stands out) Gumps.AddImageTiled(gump, x, y, width, height, GUMP_ART["BAR_BG"]) # dark background tile # Filled portion (solid red) filled_w = int((current / float(maximum)) * width) if maximum > 0 else 0 filled_w = max(0, min(width, filled_w)) if filled_w > 0: # For small heights, a single row; for ~20px heights, use 3 stacked rows to hide seams. if height <= 16: rows_y = [y] elif height <= 32: # Top, bottom, and a centered overlap to cover the join line top_y = y bottom_y = y + (height - 16) mid_y = y + max(0, (height // 2) - 8) rows_y = [top_y, bottom_y, mid_y] else: # Larger heights: tile every 16px and add an extra middle overlap rows_y = [y + iy for iy in range(0, height, 16)] rows_y.append(y + max(0, (height // 2) - 8)) for ry in rows_y: for ix in range(0, filled_w, 12): Gumps.AddImage(gump, x + ix, ry, GUMP_ART["BAR"], COLOR["bar_fill"]) def add_name_label(self, gump, center_x, y, text): try: # BIG OUTER GLOW OUTLINE # Multi-layer glow: three radii of black around the red main label # Note: label font size is fixed; this simulates thicker presence offsets_r1 = [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)] offsets_r2 = [(-2, 0), (2, 0), (0, -2), (0, 2), (-2, -1), (-2, 1), (2, -1), (2, 1), (-1, -2), (1, -2), (-1, 2), (1, 2)] offsets_r3 = [ (-3, 0), (3, 0), (0, -3), (0, 3), (-3, -1), (-3, 1), (3, -1), (3, 1), (-1, -3), (1, -3), (-1, 3), (1, 3), (-2, -2), (2, -2), (-2, 2), (2, 2) ] for dx, dy in offsets_r3: Gumps.AddLabel(gump, center_x + dx, y + dy, COLOR["boss_name_glow"], text) for dx, dy in offsets_r2: Gumps.AddLabel(gump, center_x + dx, y + dy, COLOR["boss_name_glow"], text) for dx, dy in offsets_r1: Gumps.AddLabel(gump, center_x + dx, y + dy, COLOR["boss_name_glow"], text) # Main red label for _ in range(max(1, NAME_DUPLICATE_COUNT)): Gumps.AddLabel(gump, center_x, y, COLOR["boss_name"], text) except Exception as e: self.debug_message(f"add_name_label error: {e}") def draw_phase_markers(self, gump, x, y, width, height): if not SHOW_PHASE_MARKERS or width < 6: return try: one_third = x + int(width / 3) two_thirds = x + int((2 * width) / 3) # place markers vertically centered on the bar height marker_y = y + max(0, (height // 2) - 8) Gumps.AddLabel(gump, one_third, marker_y, COLOR["phase_marker"], "|") Gumps.AddLabel(gump, two_thirds, marker_y, COLOR["phase_marker"], "|") except Exception: pass def create_gump(self): try: gump = Gumps.CreateGump(movable=True) Gumps.AddPage(gump, 0) # Draw flourish arrows FIRST (behind everything else) if FLOURISH_ARROWS_ENABLED: try: # Left flourish arrow - moved more to the right and higher left_flourish_x = FLOURISH_LEFT_OFFSET_X left_flourish_y = (self.total_height // 2) + FLOURISH_OFFSET_Y Gumps.AddImage(gump, left_flourish_x, left_flourish_y, FLOURISH_IMAGES["left_flourish_arrow_red"], FLOURISH_TINT) # Right flourish arrow - moved double distance to the left and higher right_flourish_x = self.total_width - FLOURISH_RIGHT_OFFSET_X - 24 # assuming ~24px width for flourish right_flourish_y = (self.total_height // 2) + FLOURISH_OFFSET_Y Gumps.AddImage(gump, right_flourish_x, right_flourish_y, FLOURISH_IMAGES["right_flourish_arrow_red"], FLOURISH_TINT) except Exception as e: self.debug_message(f"flourish arrows error: {e}") # Ensure we have a candidate if none selected if not self.boss_serial: self.scan_for_boss_candidates() self.auto_select_best_boss() mob = self.get_boss_mobile() has_target = mob is not None and getattr(mob, 'Exists', True) # If no valid target, hide the gump and exit if not has_target: try: Gumps.CloseGump(self.gump_id) except Exception: pass self.debug_message("No valid boss target nearby; gump hidden.") return False # Title/Name name = "No Boss Selected" hits = 0 hits_max = 1 if has_target: try: raw_name = mob.Name if getattr(mob, 'Name', None) else f"[{mob.Serial}]" # Prefer cached display from boss_info if available info = self.boss_info.get(mob.Serial, {}) disp = info.get('display') if not disp: # Fallback: resolve via KNOWN_BOSSES matching _key, _disp = self.match_known_boss(raw_name) disp = _disp or raw_name name = disp hits = max(0, int(getattr(mob, 'Hits', 0))) hits_max = max(1, int(getattr(mob, 'HitsMax', 1))) except Exception: pass # Bar area # If side images are enabled, shift bar to the right to make room bar_x = PADDING if SIDE_IMAGES_ENABLED: bar_x += SIDE_IMAGE_W + SIDE_IMAGE_GAP bar_y = PADDING + 14 # leave space for name on top # Draw bar and container self.draw_bar(gump, bar_x, bar_y, BAR_WIDTH, BAR_HEIGHT, hits, hits_max) # Optional side images (left and right), centered vertically inside background if SIDE_IMAGES_ENABLED: try: left_id = UI_GUMP_IMAGES.get(SIDE_IMAGE_LEFT_KEY) right_id = UI_GUMP_IMAGES.get(SIDE_IMAGE_RIGHT_KEY) cy = max(PADDING, (self.total_height - SIDE_IMAGE_H) // 2) # Left image 10px further left (bounded at 0) grow = SIDE_IMAGE_GROW_PIX if SIDE_IMAGE_STACK else 0 left_x = PADDING - 10 if left_x < 0: left_x = 0 # Draw stacked copies to simulate larger image if enabled if SIDE_IMAGE_STACK and grow > 0: # Use small offset ring to thicken s = max(1, min(3, grow // 2)) offsets = [ (0, 0), (-s, 0), (s, 0), (0, -s), (0, s), (-s, -s), (s, -s), (-s, s), (s, s) ] for dx, dy in offsets: Gumps.AddImage(gump, left_x + dx, cy + dy, left_id, SIDE_IMAGE_TINT) else: Gumps.AddImage(gump, left_x, cy, left_id, SIDE_IMAGE_TINT) # Right image at inner right padding right_x = self.total_width - PADDING - (SIDE_IMAGE_W + (SIDE_IMAGE_GROW_PIX if SIDE_IMAGE_STACK else 0)) if SIDE_IMAGE_STACK and grow > 0: s = max(1, min(3, grow // 2)) offsets = [ (0, 0), (-s, 0), (s, 0), (0, -s), (0, s), (-s, -s), (s, -s), (-s, s), (s, s) ] for dx, dy in offsets: Gumps.AddImage(gump, right_x + dx, cy + dy, right_id, SIDE_IMAGE_TINT) else: Gumps.AddImage(gump, right_x, cy, right_id, SIDE_IMAGE_TINT) except Exception: pass # Top-center filigree image, centered over the bar (before dividers) if TOP_CENTER_IMAGE_ENABLED: try: center_x = bar_x + (BAR_WIDTH // 2) + TOP_CENTER_IMAGE_OFFSET_X img_x = center_x - (TOP_CENTER_IMAGE_W // 2) img_y = bar_y + TOP_CENTER_IMAGE_OFFSET_Y - 16 hue = TOP_CENTER_IMAGE_TINT if TOP_CENTER_IMAGE_TINT is not None else COLOR.get("bar_fill", 38) Gumps.AddImage(gump, img_x, img_y, TOP_CENTER_IMAGE_ID, hue) except Exception: pass # Draw phase dividers on top of images and bar self.draw_phase_markers(gump, bar_x, bar_y, BAR_WIDTH, BAR_HEIGHT) # Centered name label with improved width estimation and global offsets est_w = self.estimate_label_width(name) text_x = (self.total_width - est_w) // 2 + NAME_OFFSET_X text_y = PADDING + NAME_OFFSET_Y self.add_name_label(gump, text_x, text_y, name) # Optional numeric values centered on the bar (white with black outline) if SHOW_HEALTH_NUMBERS: val_text = f"{hits} / {hits_max}" val_x = (self.total_width // 2) - max(0, (len(val_text) * 4)) Gumps.AddLabel(gump, val_x + 1, bar_y + 3, COLOR["text_border"], val_text) Gumps.AddLabel(gump, val_x, bar_y + 2, COLOR["text"], val_text) # Send gump Gumps.SendGump(self.gump_id, Player.Serial, GUMP_X, GUMP_Y, gump.gumpDefinition, gump.gumpStrings) return True except Exception as e: self.debug_message(f"create_gump error: {e}", 33) return False def update(self): try: now = int(time.time() * 1000) if (now - self.last_update) >= self.update_delay_ms: self.last_update = now # Continuous scan maintains boss_info freshness self.scan_for_boss_candidates() # If manual target is gone, fall back to auto selection if self.boss_serial and not self.get_boss_mobile(): self.boss_serial = None if not self.boss_serial: self.auto_select_best_boss() # Adjust update pacing based on recent boss presence try: has_target = self.get_boss_mobile() is not None except Exception: has_target = False recent = has_target or (now - self.last_boss_seen_ms) <= RECENT_BOSS_SEEN_MS self.update_delay_ms = FAST_UPDATE_RATE if recent else UPDATE_RATE self.create_gump() except Exception as e: self.debug_message(f"update error: {e}") def start(self): self.active = True self.debug_message("Boss health bar started.") try: while self.active and Player.Connected: self.update() Misc.Pause(50) except Exception as e: self.debug_message(f"main loop error: {e}", 33) finally: self.stop() def stop(self): self.active = False try: Gumps.CloseGump(self.gump_id) except Exception: pass self.debug_message("Boss health bar stopped.") def main(): ui = BossHealthBar() ui.start() if __name__ == "__main__": main()

Version History

Original Version Saved - 9/13/2025, 3:07:01 AM - 3 days ago

UI_boss_health_bar.py

No changes to display
View list of scripts
Disclaimer: This is a fan made site and is not directly associated with Ultima Online or UO staff.