diff --git a/config.py b/config.py index f3dafe1..8da3408 100644 --- a/config.py +++ b/config.py @@ -46,48 +46,48 @@ TEAM_COLORS = { # Weapon names WEAPON_NAMES = { - 'ROCKET': 'Rocket Launcher', - 'LIGHTNING': 'Lightning Gun', - 'RAILGUN': 'Railgun', - 'SHOTGUN': 'Shotgun', - 'GAUNTLET': 'Gauntlet', - 'GRENADE': 'Grenade Launcher', - 'PLASMA': 'Plasma Gun', - 'MACHINEGUN': 'Machine Gun' + 'ROCKET': '^8^1Rocket Launcher^7^0', + 'LIGHTNING': '^8^3Lightning Gun^7^0', + 'RAILGUN': '^8^2Railgun^7^0', + 'SHOTGUN': '^8^3Shotgun^7^0', + 'GAUNTLET': '^8^1Gauntlet^7^0', + 'GRENADE': '^8^2Grenade Launcher^7^0', + 'PLASMA': '^8^6Plasma Gun^7^0', + 'MACHINEGUN': '^8^3Machine Gun^7^0' } # Weapon names for kill messages WEAPON_KILL_NAMES = { - 'ROCKET': 'the Rocket Launcher', - 'LIGHTNING': 'the Lightning Gun', - 'RAILGUN': 'the Railgun', - 'SHOTGUN': 'the Shotgun', - 'GAUNTLET': 'the Gauntlet', - 'GRENADE': 'the Grenade Launcher', - 'PLASMA': 'the Plasma Gun', - 'MACHINEGUN': 'the Machine Gun' + 'ROCKET': 'the ^8^1Rocket Launcher', + 'LIGHTNING': 'the ^8^3Lightning Gun', + 'RAILGUN': 'the ^8^2Railgun', + 'SHOTGUN': 'the ^8^3Shotgun', + 'GAUNTLET': 'the ^8^1Gauntlet', + 'GRENADE': 'the ^8^2Grenade Launcher', + 'PLASMA': 'the ^8^6Plasma Gun', + 'MACHINEGUN': 'the ^8^3Machine Gun' } # Death messages DEATH_MESSAGES = { - 'FALLING': "%s^0%s^9 ^7cratered.", - 'HURT': "%s^0%s^9 ^7was in the wrong place.", - 'LAVA': "%s^0%s^9 ^7does a backflip into the lava.", - 'WATER': "%s^0%s^9 ^7sank like a rock.", - 'SLIME': "%s^0%s^9 ^7melted.", - 'CRUSH': "%s^0%s^9 ^7was crushed." + 'FALLING': "%s^8%s^0 ^7cratered.", + 'HURT': "%s^8%s^0 ^7was in the wrong place.", + 'LAVA': "%s^8%s^0 ^7does a backflip into the lava.", + 'WATER': "%s^8%s^0 ^7sank like a rock.", + 'SLIME': "%s^8%s^0 ^7melted.", + 'CRUSH': "%s^8%s^0 ^7was crushed." } # Powerup names and colors POWERUP_COLORS = { - 'Quad Damage': '^5Quad Damage^7', - 'Battle Suit': '^3Battle Suit^7', - 'Regeneration': '^1Regeneration^7', - 'Haste': '^3Haste^7', - 'Invisibility': '^5Invisibility^7', - 'Flight': '^5Flight^7', - 'Medkit': '^1Medkit^7', - 'MegaHealth': '^4MegaHealth^7' + 'Quad Damage': '^8^5Quad Damage^7^0', + 'Battle Suit': '^8^3Battle Suit^7^0', + 'Regeneration': '^8^1Regeneration^7^0', + 'Haste': '^8^3Haste^7^0', + 'Invisibility': '^8^5Invisibility^7^0', + 'Flight': '^8^5Flight^7^0', + 'Medkit': '^8^1Medkit^7^0', + 'MegaHealth': '^8^4MegaHealth^7^0' } # Curses color pairs diff --git a/formatter.py b/formatter.py index 044f053..d20d5b7 100644 --- a/formatter.py +++ b/formatter.py @@ -137,7 +137,7 @@ def format_chat_message(message, player_tracker): player_name = strip_color_codes(name_match.group(1).strip()) team_prefix = get_team_prefix(player_name, player_tracker) location_clean = strip_color_codes(location_part) - return f"{team_prefix}^0{player_part}^9 ^3{location_clean}^7:^5{message_part}" + return f"{team_prefix}^8{player_part}^0 ^3{location_clean}^7:^5{message_part}" # Team chat without location: (PlayerName): message colon_match = re.match(r'^(\([^)]+\)):(\s*.*)', clean_msg) @@ -149,7 +149,7 @@ def format_chat_message(message, player_tracker): if name_match: player_name = strip_color_codes(name_match.group(1).strip()) team_prefix = get_team_prefix(player_name, player_tracker) - return f"{team_prefix}^0{player_part}^9^5{message_part}\n" + return f"{team_prefix}^8{player_part}^0^5{message_part}\n" # Regular chat: PlayerName: message parts = clean_msg.split(':', 1) @@ -160,7 +160,7 @@ def format_chat_message(message, player_tracker): # Preserve original color-coded name original_parts = message.replace(chr(25), '').split(':', 1) if len(original_parts) == 2: - return f"{team_prefix}^0{original_parts[0]}^9:^2{original_parts[1]}" + return f"{team_prefix}^8{original_parts[0]}^0:^2{original_parts[1]}" return message @@ -193,7 +193,7 @@ def format_powerup_message(message, player_tracker): colored_powerup = POWERUP_COLORS.get(powerup_name, f'^6{powerup_name}^7') timestamp = time.strftime('%H:%M:%S') - return f"^3[^7{timestamp}^3]^7 {team_prefix}^0{player_name}^9 ^7got the {colored_powerup}!\n" + return f"^3[^7{timestamp}^3]^7 {team_prefix}^8{player_name}^0 ^7got the {colored_powerup}!\n" # Powerup carrier kill: "PlayerName killed the PowerupName carrier!" carrier_match = re.match(r'^(.+?)\s+killed the\s+(.+?)\s+carrier!', message) @@ -206,6 +206,6 @@ def format_powerup_message(message, player_tracker): colored_powerup = POWERUP_COLORS.get(powerup_name, f'^6{powerup_name}^7') timestamp = time.strftime('%H:%M:%S') - return f"^3[^7{timestamp}^3]^7 {team_prefix}^0{player_name}^9 ^7killed the {colored_powerup} ^7carrier!\n" + return f"^3[^7{timestamp}^3]^7 {team_prefix}^8{player_name}^0 ^7killed the {colored_powerup} ^7carrier!\n" return None diff --git a/main.py b/main.py index d01f4d4..4412b5d 100644 --- a/main.py +++ b/main.py @@ -43,11 +43,11 @@ def signal_handler(sig, frame): if quit_confirm_time is None or (current_time - quit_confirm_time) > 3: # First Ctrl-C or timeout expired - logger.warning("^1^0Press Ctrl-C again within 3 seconds to quit^0") + logger.warning("^1^8Press Ctrl-C again within 3 seconds to quit^0") quit_confirm_time = current_time else: # Second Ctrl-C within 3 seconds - logger.warning("^1^0Quittin'^9") + logger.warning("^1^8Quittin'^0") sys.exit(0) def parse_cvar_response(message, game_state, ui): @@ -146,7 +146,7 @@ def parse_player_events(message, game_state, ui): # Only print if this is NOT the Steam ID line if 'Steam ID' not in message: timestamp = time.strftime('%H:%M:%S') - ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^2connected^7\n") + ui.print_message(f"^3[^7{timestamp}^3]^7 ^8{player_name} ^9^2connected\n") return True @@ -163,7 +163,7 @@ def parse_player_events(message, game_state, ui): ui.update_server_info(game_state) timestamp = time.strftime('%H:%M:%S') - ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^1disconnected^7\n") + ui.print_message(f"^3[^7{timestamp}^3]^7 ^8{player_name} ^9^1disconnected\n") return True # Kick @@ -179,7 +179,7 @@ def parse_player_events(message, game_state, ui): ui.update_server_info(game_state) timestamp = time.strftime('%H:%M:%S') - ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^1was kicked^7\n") + ui.print_message(f"^3[^7{timestamp}^3]^7 ^8{player_name} ^1was kicked\n") return True # Inactivity @@ -195,7 +195,7 @@ def parse_player_events(message, game_state, ui): ui.update_server_info(game_state) timestamp = time.strftime('%H:%M:%S') - ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{player_name}^9 ^3dropped due to inactivity^7\n") + ui.print_message(f"^3[^7{timestamp}^3]^7 ^8{player_name}^0 ^3dropped due to inactivity^7\n") return True # Match renames: "OldName renamed to NewName" @@ -221,7 +221,7 @@ def parse_player_events(message, game_state, ui): game_state.player_tracker.rename_player(old_name, new_name) ui.update_server_info(game_state) timestamp = time.strftime('%H:%M:%S') - ui.print_message(f"^3[^7{timestamp}^3]^7 ^0{old_name}^9 ^6renamed to^7 ^0{new_name}^9\n") + ui.print_message(f"^3[^7{timestamp}^3]^7 ^8{old_name} ^6renamed to^7 ^8{new_name}\n") return True except Exception as e: @@ -374,7 +374,7 @@ def main_loop(screen): logger.info('Game initialization detected - refreshing server info') timestamp = time.strftime('%H:%M:%S') - ui.print_message(f"^3[^7{timestamp}^3] ^0^3Game initialized - Refreshing server info^9^7\n") + ui.print_message(f"^3[^7{timestamp}^3] ^8^3Game initialized - Refreshing server info^0^7\n") rcon.send_command(b'qlx_serverBrandName') rcon.send_command(b'g_factoryTitle') diff --git a/parser.py b/parser.py index 949f330..eac99f4 100644 --- a/parser.py +++ b/parser.py @@ -104,17 +104,17 @@ class EventParser: if team == old_team: return None - warmup = " ^0^3(Warmup)^9" if data.get('WARMUP', False) else "" + warmup = " ^8^3(Warmup)^0" if data.get('WARMUP', False) else "" team_messages = { - 'FREE': ' ^7joined the ^0fight^9', + 'FREE': ' ^7joined the ^8fight^0', 'SPECTATOR': ' ^7joined the ^3Spectators^7', 'RED': ' ^7joined the ^1RED Team^7', 'BLUE': ' ^7joined the ^4BLUE Team^7' } old_team_messages = { - 'FREE': 'the ^0fight^9', + 'FREE': 'the ^8fight^0', 'SPECTATOR': 'the ^3Spectators^7', 'RED': '^7the ^1RED Team^7', 'BLUE': '^7the ^4BLUE Team^7' @@ -124,7 +124,7 @@ class EventParser: old_team_msg = old_team_messages.get(old_team, f'team {old_team}') team_prefix = get_team_prefix(name, self.game_state.player_tracker) - return f"{team_prefix}^0{name}^9{team_msg} from {old_team_msg}{warmup}\n" + return f"{team_prefix}^8{name}^0{team_msg} from {old_team_msg}{warmup}\n" def _handle_death(self, data): """Handle PLAYER_DEATH and PLAYER_KILL events""" @@ -147,7 +147,7 @@ class EventParser: self.game_state.player_tracker.add_player(victim_name) victim_prefix = get_team_prefix(victim_name, self.game_state.player_tracker) - warmup = " ^0^3(Warmup)^9" if data.get('WARMUP', False) else "" + warmup = " ^8^3(Warmup)^0" if data.get('WARMUP', False) else "" score_prefix = "" # Environmental death (no killer) @@ -155,10 +155,10 @@ class EventParser: # -1 for environmental death if not data.get('WARMUP', False): self.game_state.player_tracker.update_score(victim_name, -1) - score_prefix = "^0^1[-1]^7^9 " + score_prefix = "^8^1[-1]^7^0 " mod = data.get('MOD', 'UNKNOWN') - msg_template = DEATH_MESSAGES.get(mod, "%s^0%s^9 ^1DIED FROM %s^7") + msg_template = DEATH_MESSAGES.get(mod, "%s^8%s^0 ^1DIED FROM %s^7") if mod in DEATH_MESSAGES: msg = msg_template % (victim_prefix, victim_name) @@ -183,18 +183,24 @@ class EventParser: # -1 for suicide if not data.get('WARMUP', False): self.game_state.player_tracker.update_score(victim_name, -1) - score_prefix = "^0^1[-1]^7^9 " + score_prefix = "^8^1[-1]^7^0 " weapon = killer.get('WEAPON', 'OTHER_WEAPON') - if weapon != 'OTHER_WEAPON': + if weapon == 'ROCKET': + return f"{score_prefix}{killer_prefix}^8{killer_name}^0 ^7blew herself up.{warmup}\n" + elif weapon == 'GRENADE': + return f"{score_prefix}{killer_prefix}^8{killer_name}^0 ^7tripped on her own grenade.{warmup}\n" + elif weapon == 'PLASMA': + return f"{score_prefix}{killer_prefix}^8{killer_name}^0 ^7melted herself.{warmup}\n" + else: weapon_name = WEAPON_NAMES.get(weapon, weapon) - return f"{score_prefix}{killer_prefix}^0{killer_name}^9 ^7committed suicide with the ^7{weapon_name}{warmup}\n" + return f"{score_prefix}{killer_prefix}^8{killer_name}^0 ^7committed suicide with the ^7{weapon_name}{warmup}\n" return None # Regular kill: +1 for killer if not data.get('WARMUP', False): self.game_state.player_tracker.update_score(killer_name, 1) - score_prefix = "^0^2[+1]^7^9 " + score_prefix = "^8^2[+1]^7^0 " else: score_prefix = "" @@ -202,7 +208,7 @@ class EventParser: weapon = killer.get('WEAPON', 'UNKNOWN') weapon_name = WEAPON_KILL_NAMES.get(weapon, f'the {weapon}') - return f"{score_prefix}{killer_prefix}^0{killer_name}^9 ^7fragged^7 {victim_prefix}^0{victim_name}^9 ^7with {weapon_name}{warmup}\n" + return f"{score_prefix}{killer_prefix}^8{killer_name}^0 ^7fragged^7 {victim_prefix}^8{victim_name}^0 ^7with {weapon_name}{warmup}\n" def _handle_round_over(self, data): """Handle ROUND_OVER events for CA""" @@ -222,10 +228,29 @@ class EventParser: """Handle PLAYER_MEDAL event""" name = data.get('NAME', 'Unknown') medal = data.get('MEDAL', 'UNKNOWN') - warmup = " ^0^3(Warmup)^7^9" if data.get('WARMUP', False) else "" + warmup = " ^8^3(Warmup)^7^0" if data.get('WARMUP', False) else "" team_prefix = get_team_prefix(name, self.game_state.player_tracker) - return f"{team_prefix}^0{name}^9 ^7got a medal: ^0^6{medal}{warmup}\n" + # RED Medals (^1) + if medal in ["FIRSTFRAG", "HUMILIATION", "REVENGE"]: + return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^1{medal}^0{warmup}\n" + # GREEN Medals (^2) + elif medal in ["MIDAIR", "PERFECT"]: + return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^2{medal}^0{warmup}\n" + # YELLOW Medals (^3) + elif medal in ["EXCELLENT", "HEADSHOT", "RAMPAGE"]: + return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^3{medal}^0{warmup}\n" + # BLUE Medals (^4) + elif medal in ["ASSIST", "DEFENSE", "QUADGOD"]: + return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^4{medal}^0{warmup}\n" + # CYAN Medals (^5) + elif medal in ["CAPTURE", "COMBOKILL", "IMPRESSIVE"]: + return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^5{medal}^0{warmup}\n" + # PINK Medals (^6) + elif medal in ["ACCURACY", "PERFORATED"]: + return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^6{medal}^0{warmup}\n" + else: + return f"{team_prefix}^8{name}^0 ^7got a medal: ^8^9^7{medal}^0{warmup}\n" def _handle_match_started(self, data): """Handle MATCH_STARTED event""" @@ -238,8 +263,8 @@ class EventParser: players.append(name) if players: - formatted = "^9 vs. ^0".join(players) - return f"^0^3Match has started - ^0^7{formatted}\n" + formatted = "^0 vs. ^8".join(players) + return f"^8^3Match has started - ^8^7{formatted}\n" return None @@ -279,4 +304,4 @@ class EventParser: weapon_name = WEAPON_NAMES.get(best_weapon, best_weapon) - return f"{team_prefix}^0^7{name}^9^7 K/D: {kills}/{deaths} | Best Weapon: {weapon_name} - Acc: {best_accuracy:.2f}% - Kills: {best_weapon_kills}\n" + return f"{team_prefix}^8^7{name}^0^7 K/D: {kills}/{deaths} | Best Weapon: {weapon_name} - Acc: {best_accuracy:.2f}% - Kills: {best_weapon_kills}\n" diff --git a/ui.py b/ui.py index 33fd440..1692825 100644 --- a/ui.py +++ b/ui.py @@ -41,7 +41,7 @@ class CursesHandler(logging.Handler): def print_colored(window, message, attributes=0): """ Print message with Quake color codes (^N) - ^0 = bold, ^1 = red, ^2 = green, ^3 = yellow, ^4 = blue, ^5 = cyan, ^6 = magenta, ^7 = white, ^9 = reset + ^0 = reset, ^1 = red, ^2 = green, ^3 = yellow, ^4 = blue, ^5 = cyan, ^6 = magenta, ^7 = white, ^8 = bold, ^9 = underline """ if not curses.has_colors: window.addstr(message) @@ -49,27 +49,31 @@ def print_colored(window, message, attributes=0): color = 0 bold = False + underline = False parse_color = False for ch in message: val = ord(ch) if parse_color: - if ch == '0': + if ch == '8': bold = True elif ch == '9': + underline = True + elif ch == '0': bold = False + underline = False elif ch == '7': color = 0 elif ord('1') <= val <= ord('6'): color = val - ord('0') else: - window.addch('^', curses.color_pair(color) | (curses.A_BOLD if bold else 0) | attributes) - window.addch(ch, curses.color_pair(color) | (curses.A_BOLD if bold else 0) | attributes) + window.addch('^', curses.color_pair(color) | (curses.A_BOLD if bold else 0) | (curses.A_UNDERLINE if underline else 0) | attributes) + window.addch(ch, curses.color_pair(color) | (curses.A_BOLD if bold else 0) | (curses.A_UNDERLINE if underline else 0) | attributes) parse_color = False elif ch == '^': parse_color = True else: - window.addch(ch, curses.color_pair(color) | (curses.A_BOLD if bold else 0) | attributes) + window.addch(ch, curses.color_pair(color) | (curses.A_BOLD if bold else 0) | (curses.A_UNDERLINE if underline else 0) | attributes) class UIManager: """Manages curses windows and display""" @@ -307,8 +311,8 @@ class UIManager: # Line 1: Hostname hostname = server_info.hostname - warmup_display = "^3Warmup: ^2YES^9" if server_info.warmup else "^3Warmup: ^1NO^9" - print_colored(self.info_window, f"^3Name:^0 {hostname} {warmup_display}\n", 0) + warmup_display = "^3Warmup: ^2YES^0" if server_info.warmup else "^3Warmup: ^1NO^0" + print_colored(self.info_window, f"^3Name:^8 {hostname} {warmup_display}\n", 0) # Line 2: Game info gametype = server_info.gametype @@ -317,12 +321,24 @@ class UIManager: fraglimit = server_info.fraglimit roundlimit = server_info.roundlimit caplimit = server_info.capturelimit - curclients = server_info.curclients + curclients = len(server_info.players) maxclients = server_info.maxclients + + # Context-sensitive limit display based on gametype + if gametype == 'Capture the Flag': + limit_display = f"^3^0| Capturelimit:^7^8 {caplimit}" + elif gametype == 'Clan Arena': + limit_display = f"^3^0| Roundlimit:^7^8 {roundlimit}" + elif gametype == 'Duel': + limit_display = f"^3^0| Timelimit:^7^8 {timelimit}" + elif gametype == 'Race': + limit_display = f"^3^0| Timelimit:^7^8 {timelimit}" + else: + limit_display = f"^3^0| Timelimit:^7^8 {timelimit} ^0^3| Fraglimit:^7^8 {fraglimit}" - print_colored(self.info_window, - f"^3^9Type:^7^0 {gametype} ^9^3| Map:^7^0 {mapname} ^9^3| Players:^7^0 {curclients}/{maxclients} " - f"^3^9| Limits (T/F/R/C):^7^0 {timelimit}/{fraglimit}/{roundlimit}/{caplimit}^9\n", 0) + print_colored(self.info_window, + f"^3^0Type:^7^8 {gametype} ^8^3| Map:^7^8 {mapname} ^0^3| Players:^7^8 {curclients}/{maxclients} " + f"{limit_display}^0\n", 0) # Blank lines to fill self.info_window.addstr("\n") @@ -351,7 +367,7 @@ class UIManager: red_score = f"{red_total:>3} " blue_score = f"{blue_total:>3} " - print_colored(self.info_window, f"^0^7{red_score}^1RED TEAM ^7{blue_score}^4BLUE TEAM\n", 0) + print_colored(self.info_window, f"^8^7{red_score}^9^1RED TEAM^0 ^7^8{blue_score}^9^4BLUE TEAM\n", 0) # Sort players by score within each team red_players_with_scores = [] @@ -410,10 +426,10 @@ class UIManager: red_pad = 24 - len(red_clean) - line = f"^0{red}^9{' ' * red_pad}^0{blue}^9\n" + line = f"^8{red}^0{' ' * red_pad}^8{blue}^0\n" print_colored(self.info_window, line, 0) else: - print_colored(self.info_window, f"^0 ^5FREE\n", 0) + print_colored(self.info_window, f" ^8^9^5FREE\n", 0) # Sort FREE players by score (highest first) free_players = teams['FREE'] free_players_with_scores = [] @@ -461,7 +477,7 @@ class UIManager: col1_pad = 24 - len(col1_clean) - line = f"^0{col1}^9{' ' * col1_pad}^0{col2}^9\n" + line = f"^8{col1}^0{' ' * col1_pad}^8{col2}^0\n" print_colored(self.info_window, line, 0) # Blank lines to fill @@ -469,7 +485,7 @@ class UIManager: # List spectators on one line spec_list = " ".join(spec_players) - line = f"^0 ^3SPEC^7 {spec_list}\n" + line = f" ^8^3SPEC^0:^7^8 {spec_list}\n" print_colored(self.info_window, line, 0) # Blank lines to fill @@ -480,3 +496,5 @@ class UIManager: print_colored(self.info_window, separator, 0) self.info_window.noutrefresh() + + curses.doupdate()