############################################################################################################### ### LINT+ REN'PY WORD COUNTER ################################################################################ ############################################################################################################### # # Thanks for downloading Lint+! ( https://kigyo.itch.io/renpy-word-counter ) # Below are a bunch of variables that let you customize the generated output according to your needs. # # If you like this tool, consider dropping a donation: https://ko-fi.com/kigyodev # Paying just $1 on itch.io also lets you access an additional feature: label-based statistics! # # Thank you, and may this tool help track your progress better than ever! :D # - KigyoDev ## Preferences: Counters ################################################################################################ # Line and word counts for every character # default value: True define wordcounter_characters = True # Line and word counts for each .rpy file # default value: True define wordcounter_files = True # Line and word counts for every character, within each .rpy file # default value: True define wordcounter_character_files = True # Number of menus and available choices in the game # default value: True define wordcounter_menu_choices = True # Show character/symbol counts, which might be too much detail - otherwise only displays number of dialogue blocks and words # default value: False define wordcounter_display_character_count = False # DONATE TO UNLOCK: # Line and word counts for every label # default value: True define wordcounter_labels = True # The old character statistics which only displays line counts # default value: False define config.lint_character_statistics = False ## Preferences: File paths ################################################################################################ # Name of the folder your script files are in, if any. This can make the generated output look nicer. # Example: "script/" # default value: "" define script_folder_path = "" # List of folders and files you want to ignore. # Example: if you want to make sure nothing in the "unused" folder and the "script.rpy" file is counted, write ["unused", "script.rpy"] # Note: Be careful with unintentionally matching filenames! Ignoring "no" will also ignore any folders and file names containing "no". # default value: [] define script_ignore_path = [] ## Preferences: Characters ################################################################################################ # List of equivalent characters that should be counted as one. Given ("x", "y"), "y" will be counted as "x". # Example: if "ann", "xann", and "nann" should all be considered "ann", write [("ann", "xann"), ("ann", "nann")] # default value: [] define wordcounter_same = [] # List of characters who should be hidden from the character statistics. They still contribute to the total file word count. # default value: ["extend"] define wordcounter_hidden = ["extend"] # List of characters who should not count towards any word counts. # default value: [] define wordcounter_uncounted = [] ############################################################################################################### ### The Code ################################################################################################## ############################################################################################################### init python: import collections # The main function def wordcounter(): all_stmts = list(renpy.game.script.all_stmts) all_stmts.sort(key=lambda n : n.filename) charastats = collections.defaultdict(Count) filestats = collections.defaultdict(Count) filecharastats = {} menu_count = 0 options_count = 0 unignored_name = "game/" + script_folder_path + "every file combined" filecharastats[unignored_name] = collections.defaultdict(Count) for node in all_stmts: if isinstance(node, renpy.ast.Say): speaker = node.who for i in wordcounter_same: if i[1] == speaker: speaker = i[0] break if not_ignored_path(node.filename) and speaker not in wordcounter_uncounted: filestats[unignored_name].add(node.what) filestats[node.filename].add(node.what) if node.filename not in filecharastats: filecharastats[node.filename] = collections.defaultdict(Count) if speaker not in wordcounter_hidden: charastats[speaker if speaker else 'narrator' ].add(node.what) filecharastats[node.filename][speaker if speaker else 'narrator' ].add(node.what) elif isinstance(node, renpy.ast.Menu): menu_count += 1 for l, c, b in node.items: options_count += 1 if renpy.config.developer and wordcounter_characters: print("\n") report_character_stats(charastats) if renpy.config.developer and wordcounter_files: print("\n") report_file_stats(filestats) if renpy.config.developer and wordcounter_character_files: print("\n") report_file_chara_stats(filestats, filecharastats) if renpy.config.developer and wordcounter_menu_choices: print("\n") report_menu_stats(menu_count, options_count) # This makes sure the above function is actually called whenever you use Lint config.lint_hooks.append(wordcounter) def not_ignored_path(filename): for i in script_ignore_path: if i in filename: return False return True # The print functions: def report_character_stats(charastats, title = True): if title: print("Character statistics:") count_to_char = collections.defaultdict(list) for char in charastats: count_to_char[charastats[char].blocks].append(char) for count, chars in sorted(count_to_char.items(), reverse=True): chars.sort() if len(chars) == 1: start = chars[0] + " has " end = humanize(charastats[chars[0]].words) elif len(chars) == 2: start = chars[0] + " and " + chars[1] + " have " end = humanize(charastats[chars[0]].words) + " and " + humanize(charastats[chars[1]].words) else: start = ", ".join(chars[:-1]) + ", and " + chars[-1] + " have " end = "" for char in chars[:-1]: end += humanize(charastats[char].words) + ", " end += "and " + humanize(charastats[chars[-1]].words) print(" * " + start + humanize(count) + (" block" if count == 1 else " blocks") + " of dialogue, and " + end + " words" + (" each." if len(chars) > 1 else ".") ) def report_file_stats(filestats): print("File statistics:") count_to_char = collections.defaultdict(list) for file in filestats: print(" * [" + file[5+len(script_folder_path):] + "] contains " + humanize(filestats[file].blocks) + " dialogue blocks and " + humanize(filestats[file].words) + " words.") def report_file_chara_stats(filestats, filecharastats): print("Detailed File statistics:") count_to_char = collections.defaultdict(list) for file in filestats: print("[" + file[5+len(script_folder_path):] + "] contains " + humanize(filestats[file].blocks) + " dialogue blocks and " + humanize(filestats[file].words) + " words:") report_character_stats(filecharastats[file], False) print("") def report_menu_stats(menu_count, options_count): print("Menu statistics:") print("The game has " + str(menu_count) + " menus, with a total of " + str(options_count) + " possible choices, \nfor an average of " + "{:,.2f}".format(options_count and options_count/menu_count or 0) + " choices per menu.") # Auxiliary functions directly copied from lint.py - I take no credit for these: def humanize(n): s = str(n) rv = [] for i, c in enumerate(reversed(s)): if i and not (i % 3): rv.insert(0, ',') rv.insert(0, c) return ''.join(rv) class Count(object): def __init__(self): self.blocks = 0 self.words = 0 self.characters = 0 def add(self, s): self.blocks += 1 self.words += len(s.split()) self.characters += len(s)