diff --git a/.gitignore b/.gitignore index 5377046..241c37d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.yaml *.yml *.txt +*.rpy !vocab.txt !requirements.txt *.csv diff --git a/modules/kirikiri.py b/modules/kirikiri.py index 50baabf..39d5c6b 100644 --- a/modules/kirikiri.py +++ b/modules/kirikiri.py @@ -209,7 +209,7 @@ def translateKiriKiri(data, pbar, filename, jobList): i = 0 # Regex - speakerRegex = r'【(.*)】\[CR\]' + speakerRegex = r"【(.*)】\[CR\]" dialogueRegex = r"^\[text\](.*).*\[KeyWait\]|\[\w+\](.*)\[\/\w+\].*\[KeyWait\]" furiganaRegex = r'(\[eruby\sstr="(.*?)"\stext.*?\])' choicesRegex = r"^\s*\[button\d\sclickse=sys_decide.*text='(.*?)'.*" diff --git a/modules/main.py b/modules/main.py index fff1f5b..84eadc3 100644 --- a/modules/main.py +++ b/modules/main.py @@ -50,6 +50,7 @@ from modules.wolf2 import handleWOLF2 from modules.javascript import handleJavascript from modules.irissoft import handleIris from modules.regex import handleRegex +from modules.renpy import handleRenpy from modules.unity import handleUnity from modules.images import handleImages from modules.rpgmakerplugin import handlePlugin @@ -67,7 +68,7 @@ MODULES = [ ["Eushully", ["txt"], handleEushully], ["Alice", ["txt"], handleAlice], ["Tyrano", ["ks"], handleTyrano], - ["Kirikiri", ['ks', 'tjs', 'ssd', 'asd'], handleKirikiri], + ["Kirikiri", ["ks", "tjs", "ssd", "asd"], handleKirikiri], ["JSON", ["json"], handleJSON], ["Kansen", ["ks"], handleKansen], ["Lune", ["json"], handleLune], @@ -79,6 +80,7 @@ MODULES = [ ["Javascript", ["js"], handleJavascript], ["Iris", ["txt"], handleIris], ["Regex", ["txt"], handleRegex], + ["Renpy", ["rpy"], handleRenpy], ["Unity", ["txt"], handleUnity], ["Images", [""], handleImages], ] @@ -133,7 +135,7 @@ files to translate are in the /files folder and that you picked the right game e futures = [ executor.submit(MODULES[version][2], filename, estimate) for filename in os.listdir("files") - for m in MODULES[version][1] + for m in MODULES[version][1] if filename.endswith(m) and filename != ".gitkeep" ] for future in as_completed(futures): diff --git a/modules/renpy.py b/modules/renpy.py new file mode 100644 index 0000000..f1a4a77 --- /dev/null +++ b/modules/renpy.py @@ -0,0 +1,588 @@ +# Libraries +import json +import os +import re +import textwrap +import threading +import time +import traceback +import tiktoken +import openai +from pathlib import Path +from colorama import Fore +from dotenv import load_dotenv +from retry import retry +from tqdm import tqdm + +# Open AI +load_dotenv() +if os.getenv("api").replace(" ", "") != "": + openai.base_url = os.getenv("api") +openai.organization = os.getenv("org") +openai.api_key = os.getenv("key") + +# Globals +MODEL = os.getenv("model") +TIMEOUT = int(os.getenv("timeout")) +LANGUAGE = os.getenv("language").capitalize() +PROMPT = Path("prompt.txt").read_text(encoding="utf-8") +VOCAB = Path("vocab.txt").read_text(encoding="utf-8") +THREADS = int(os.getenv("threads")) +LOCK = threading.Lock() +WIDTH = int(os.getenv("width")) +LISTWIDTH = int(os.getenv("listWidth")) +NOTEWIDTH = 70 +MAXHISTORY = 10 +ESTIMATE = "" +TOKENS = [0, 0] +NAMESLIST = [] +NAMES = False # Output a list of all the character names found +BRFLAG = False # If the game uses
instead +FIXTEXTWRAP = True # Overwrites textwrap +IGNORETLTEXT = False # Ignores all translated text. +MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) +FILENAME = None + +# tqdm Globals +BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" +POSITION = 0 +LEAVE = False +PBAR = None + +# Pricing - Depends on the model https://openai.com/pricing +# Batch Size - GPT 3.5 Struggles past 15 lines per request. GPT4 struggles past 50 lines per request +# If you are getting a MISMATCH LENGTH error, lower the batch size. +if "gpt-3.5" in MODEL: + INPUTAPICOST = 0.002 + OUTPUTAPICOST = 0.002 + BATCHSIZE = 10 +elif "gpt-4" in MODEL: + INPUTAPICOST = 0.0025 + OUTPUTAPICOST = 0.01 + BATCHSIZE = 40 + + +def handleRenpy(filename, estimate): + global ESTIMATE + global FILENAME + FILENAME = filename + ESTIMATE = estimate + + if ESTIMATE: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + + # Print Total + totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") + + # Print any errors on maps + if len(MISMATCH) > 0: + return ( + totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET + ) + else: + return totalString + + else: + try: + with open( + "translated/" + filename, "w", encoding="utf8", errors="ignore" + ) as outFile: + start = time.time() + translatedData = openFiles(filename) + + # Print Result + end = time.time() + outFile.writelines(translatedData[0]) + tqdm.write(getResultString(translatedData, end - start, filename)) + with LOCK: + TOKENS[0] += translatedData[1][0] + TOKENS[1] += translatedData[1][1] + except Exception: + traceback.print_exc() + return "Fail" + + return getResultString(["", TOKENS, None], end - start, "TOTAL") + + +def getResultString(translatedData, translationTime, filename): + # File Print String + totalTokenstring = ( + Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" + "[Output: " + + str(translatedData[1][1]) + + "]" "[Cost: ${:,.4f}".format( + (translatedData[1][0] * 0.001 * INPUTAPICOST) + + (translatedData[1][1] * 0.001 * OUTPUTAPICOST) + ) + + "]" + ) + timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" + + if translatedData[2] == None: + # Success + return ( + filename + + ": " + + totalTokenstring + + timeString + + Fore.GREEN + + " \u2713 " + + Fore.RESET + ) + + else: + # Fail + try: + raise translatedData[2] + except Exception as e: + traceback.print_exc() + errorString = str(e) + Fore.RED + return ( + filename + + ": " + + totalTokenstring + + timeString + + Fore.RED + + " \u2717 " + + errorString + + Fore.RESET + ) + + +def openFiles(filename): + with open("files/" + filename, "r", encoding="utf8") as readFile: + translatedData = parseRenpy(readFile, filename) + + # Delete lines marked for deletion + finalData = [] + for line in translatedData[0]: + if line != "\\d\n": + finalData.append(line) + translatedData[0] = finalData + + return translatedData + + +def parseRenpy(readFile, filename): + global PBAR + totalTokens = [0, 0] + + # Read File into data + data = readFile.readlines() + + # Create Progress Bar + with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar: + pbar.desc = filename + PBAR = pbar + + try: + result = translateRenpy(data, []) + totalTokens[0] += result[0] + totalTokens[1] += result[1] + except Exception as e: + traceback.print_exc() + return [data, totalTokens, e] + return [data, totalTokens, None] + + +def translateRenpy(data, translatedList): + stringList = [] + currentGroup = [] + tokens = [0, 0] + speaker = "" + voice = False + global LOCK, ESTIMATE, FILENAME, PBAR + i = 0 + + while i < len(data): + voice = False + speaker = "" + lineRegexNoSpeaker = r'^\s\s\s\s"(.*)"' + lineRegexSpeaker = r'^\s\s\s\s(.+?)\s"(.*)"' + + # Grab Line + match = re.search(lineRegexSpeaker, data[i]) + if match: + response = getSpeaker(match.group(1)) + jaString = match.group(2) + speaker = response[0] + tokens[0] += response[1][0] + tokens[1] += response[1][1] + else: + match = re.search(lineRegexNoSpeaker, data[i]) + if match: + jaString = match.group(1) + + # Valid Line + if match and 'voice' not in data[i]: + originalString = jaString + # Pass 1 + if translatedList == []: + # Remove any textwrap + jaString = jaString.replace("\\n", " ") + + # Add String + if speaker: + stringList.append(f"[{speaker}]: {jaString.strip()}") + else: + stringList.append(jaString.strip()) + + # Pass 2 + else: + # Get Text + if translatedList: + # Grab and Pop + translatedText = translatedList[0] + translatedList.pop(0) + + # Set to None if empty list + if len(translatedList) <= 0: + translatedList = None + + # Remove speaker + if speaker != "": + matchSpeakerList = re.findall( + r"^\[?(.+?)\]?\s?[|:]\s?", translatedText + ) + translatedText = re.sub( + r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText + ) + + # Escape Quotes + translatedText = re.sub(r'[\\]*(")', '\\"', translatedText) + translatedText = re.sub(r"[\\]*(')", "\\'", translatedText) + + # Textwrap + translatedText = textwrap.fill(translatedText, width=WIDTH) + translatedText = translatedText.replace('\n', '\\n') + + # Set Data + data[i] = data[i].replace(originalString, translatedText) + i += 1 + else: + i += 1 + + # EOF + if len(stringList) > 0: + # Set Progress + PBAR.total = len(stringList) + PBAR.refresh() + + # Translate + response = translateGPT( + stringList, + "Reply with the English TL of the NPC Name", + True + ) + tokens[0] += response[1][0] + tokens[1] += response[1][1] + translatedList = response[0] + + # Set Strings + if len(stringList) == len(translatedList): + translateRenpy(data, translatedList) + + # Mismatch + else: + with LOCK: + if FILENAME not in MISMATCH: + MISMATCH.append(FILENAME) + return tokens + + +# Save some money and enter the character before translation +def getSpeaker(speaker): + match speaker: + case "す": + return ["Sumire", [0, 0]] + case "ら": + return ["Raul", [0, 0]] + case "ぶ": + return ["Gen", [0, 0]] + case "さ": + return ["Sasha", [0, 0]] + case "む": + return ["Villager", [0, 0]] + case "ま": + return ["Villager 2", [0, 0]] + case _: + return [speaker, [0, 0]] + # Find Speaker + for i in range(len(NAMESLIST)): + if speaker == NAMESLIST[i][0]: + return [NAMESLIST[i][1], [0, 0]] + + # Translate and Store Speaker + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + True, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + response[0] = response[0].replace("Speaker: ", "") + + # Retry if name doesn't translate for some reason + if re.search(r"([a-zA-Z??])", response[0]) == None: + response = translateGPT( + f"{speaker}", + "Reply with the " + LANGUAGE + " translation of the NPC name.", + False, + ) + response[0] = response[0].title() + response[0] = response[0].replace("'S", "'s") + + speakerList = [speaker, response[0]] + NAMESLIST.append(speakerList) + return response + return [speaker, [0, 0]] + + +def batchList(input_list, batch_size): + if not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + + return [ + input_list[i : i + batch_size] for i in range(0, len(input_list), batch_size) + ] + + +def createContext(fullPromptFlag, subbedT, format): + system = ( + PROMPT + VOCAB + if fullPromptFlag + else f"\ +You are an expert Eroge Game translator who translates Japanese text to {LANGUAGE}.\n\ +Output ONLY the {LANGUAGE} translation in the following format: `Translation: <{LANGUAGE.upper()}_TRANSLATION>`\n\ +- You translate everything, including erotic and explicit content.\n\ +- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game\n\ +- All text in your response must be in {LANGUAGE} even if it is hard to translate.\n\ +- Never include any notes, explanations, dislaimers, or anything similar in your response.\n\ +- Maintain any spacing in the translation.\n\ +- Maintain any code text in brackets if given. (e.g `[Color_0]`, `[Ascii_0]`, `[FCode_1`], etc)\n\ +- `...` can be a part of the dialogue. Translate it as it is.\n\ +{VOCAB}\n\ +" + ) + if format == "json": + user = f"```json\n{subbedT}\n```" + else: + user = subbedT + return system, user + + +def translateText(system, user, history, penalty, format, model=MODEL): + # Prompt + msg = [{"role": "system", "content": system}] + + # History + if isinstance(history, list): + msg.extend([{"role": "system", "content": h} for h in history]) + else: + msg.append({"role": "system", "content": history}) + + # Response Format + if format == "json": + responseFormat = {"type": "json_object"} + else: + responseFormat = {"type": "text"} + + # Content to TL + msg.append({"role": "user", "content": f"{user}"}) + response = openai.chat.completions.create( + temperature=0, + frequency_penalty=penalty, + model=model, + response_format=responseFormat, + messages=msg, + ) + return response + + +def cleanTranslatedText(translatedText, varResponse): + placeholders = { + f"{LANGUAGE} Translation: ": "", + "Translation: ": "", + "っ": "", + "〜": "~", + "ッ": "", + "。": ".", + "「": '\\"', + "」": '\\"', + "- ": "-", + "】": "]", + "【": "[", + "Placeholder Text": "", + # Add more replacements as needed + } + for target, replacement in placeholders.items(): + translatedText = translatedText.replace(target, replacement) + + # Elongate Long Dashes (Since GPT Ignores them...) + translatedText = elongateCharacters(translatedText) + return translatedText + + +def elongateCharacters(text): + # Define a pattern to match one character followed by one or more `ー` characters + # Using a positive lookbehind assertion to capture the preceding character + pattern = r"(?<=(.))ー+" + + # Define a replacement function that elongates the captured character + def repl(match): + char = match.group(1) # The character before the ー sequence + count = len(match.group(0)) - 1 # Number of ー characters + return char * count # Replace ー sequence with the character repeated + + # Use re.sub() to replace the pattern in the text + return re.sub(pattern, repl, text) + + +def extractTranslation(translatedTextList, is_list): + try: + translatedTextList = re.sub(r'\\"+\"([^,\n}])', r'\\"\1', translatedTextList) + translatedTextList = re.sub(r"(? instead FIXTEXTWRAP = True # Overwrites textwrap -IGNORETLTEXT = False # Ignores all translated text. +IGNORETLTEXT = True # Ignores all translated text. MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) BRACKETNAMES = False PBAR = None @@ -67,16 +67,16 @@ POSITION = 0 LEAVE = False # Dialogue / Scroll / Choices (Main Codes) -CODE401 = True -CODE405 = True -CODE102 = True +CODE401 = False +CODE405 = False +CODE102 = False # Optional CODE101 = False # Turn this one when names exist in 101 CODE408 = False # Warning, translates comments and can inflate costs. # Variables -CODE122 = False +CODE122 = True # Other CODE355655 = False @@ -938,6 +938,16 @@ def searchCodes(page, pbar, jobList, filename): i += 1 continue + # Validate Japanese Text + if ( + not re.search( + r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+", jaString + ) + and IGNORETLTEXT + ): + i += 1 + continue + # # For Retarded Devs # retardRegex = r'([\\]+[nN]\[[\\]+V\[\d*?\]\])' # match = re.search(retardRegex, jaString) @@ -1268,7 +1278,7 @@ def searchCodes(page, pbar, jobList, filename): ## Event Code: 122 [Set Variables] if "code" in codeList[i] and codeList[i]["code"] == 122 and CODE122 is True: # This is going to be the var being set. (IMPORTANT) - if codeList[i]["parameters"][0] not in list(range(155, 165)): + if codeList[i]["parameters"][0] not in list(range(0, 1000)): i += 1 continue @@ -1291,7 +1301,7 @@ def searchCodes(page, pbar, jobList, filename): # Set String matchedText = None - if len(re.findall(r"([\'\"])", jaString)) == 2: + if len(re.findall(r"([\'\"\`])", jaString)) == 2: matchedText = re.search(r"[\'\"\`](.*)[\'\"\`]", jaString) # else: # matchedText = re.search(r'(.*)', jaString) @@ -1323,9 +1333,7 @@ def searchCodes(page, pbar, jobList, filename): translatedText = translatedText.replace("\n", "\\n") # Set - codeList[i]["parameters"][4] = jaString.replace( - finalJAString, translatedText - ) + codeList[i]["parameters"][4] = f"`{translatedText}`" list122.pop(0) ## Event Code: 357 [Picture Text] [Optional] @@ -1494,6 +1502,7 @@ def searchCodes(page, pbar, jobList, filename): if "TextPicture" in headerString or "BalloonInBattle" in headerString: argVar = "text" + font = None ### Message Text First if argVar in codeList[i]["parameters"][3]: acExist = False @@ -1542,9 +1551,13 @@ def searchCodes(page, pbar, jobList, filename): if acExist: translatedText = f'\\ac {translatedText.replace('\n', '\n\\ac ')}' + # Check and Set Font + if "fontSize" in codeList[i]["parameters"][3]: + if font: + codeList[i]["parameters"][3]["fontSize"] = font + # Set codeList[i]["parameters"][3][argVar] = translatedText - # codeList[i]["parameters"][3]['fontSize'] = "18" list357.pop(0) ## Event Code: 657 [Picture Text] [Optional] @@ -2571,6 +2584,7 @@ def getSpeaker(speaker): return response return [speaker, [0, 0]] + def batchList(input_list, batch_size): if not isinstance(batch_size, int) or batch_size <= 0: raise ValueError("batch_size must be a positive integer") diff --git a/vocab.txt b/vocab.txt index f3381e5..2abf6fe 100644 --- a/vocab.txt +++ b/vocab.txt @@ -1,19 +1,10 @@ Here are some vocabulary and terms so that you know the proper spelling and translation. ``` # Game Characters -レイア (Leia) - Female -アウラ (Aura) - Female -ハヤト (Hayato) - Male -ルード (Rude) - Male -トマ (Toma) - Male -ガスト (Gust) - Male -マスル (Masuru) - Male -準騎士 (Junior Knight) - Male -カグヤ (Kaguya) - Unknown -クローネ (Krone) - Male -レックス (Rex) - Male -アイカ (Aika) - Unknown -トミー (Tommy) - Male +スミレ (Sumire) - Female +サシャ (Sasha) - Female +ラウル (Raul) - Male +ゲン (Gen) - Male # Lewd Terms マンコ (pussy) @@ -90,157 +81,6 @@ ME 音量 (ME Volume) w ((lol)) 巫女 (Shrine Maiden) コイツ (this bastard) +シュード (Schnood) -# Locations -真エンド (True End) -ドリンク&ベーカリー (Drink & Bakery) -喫茶クナイ (Cafe Kunai) -ミクダース民家 (Mikudas Residence) -ブラウン探偵事務所 (Brown Detective Agency) -ル王都 (Capital of Ru) -2ママワガ別荘 (2 Mama Waga Villa) -ルノースオーラ城レイア自室 (Lunorth Aura Castle Leia's Room) -エトワール (Etoile) -シュード地区 (Shude District) -レポロ (Reporo) -コル (Kor) -ステゴ (Stego) -リスカ (Riska) - -#Armors ------鎧系 (-----Armor Types) -布の服 (Cloth Clothes) -レザーベスト (Leather Vest) -冒険者の服 (Adventurer's Clothes) -ハードレザー (Hard Leather) -ブリガンダイン (Brigandine) -クロースアーマー (Cloth Armor) -レザーアーマー (Leather Armor) -ブロンズブレスト (Bronze Breastplate) -アイアンブレスト (Iron Breastplate) -ミスリルブレスト (Mithril Breastplate) -ドラゴンブレスト (Dragon Breastplate) -ブロンズアーマー (Bronze Armor) -アイアンアーマー (Iron Armor) -ミスリルアーマー (Mithril Armor) -ドラゴンアーマー (Dragon Armor) -木綿のローブ (Cotton Robe) -絹のマント (Silk Mantle) -術士のローブ (Sorcerer's Robe) -賢者のローブ (Sage's Robe) -エレメンタルマント (Elemental Mantle) ------盾系 (-----Shield Types) -バックラー (Buckler) -ラウンドバックラー (Round Buckler) -スパイクバックラー (Spike Buckler) -ミスリルバックラー (Mithril Buckler) -ドラゴンバックラー (Dragon Buckler) -ウッドシールド (Wood Shield) -ブロンズシールド (Bronze Shield) -アイアンシールド (Iron Shield) -ミスリルシールド (Mithril Shield) -ドラゴンシールド (Dragon Shield) -ウッドバングル (Wood Bangle) -ブロンズバングル (Bronze Bangle) -アイアンバングル (Iron Bangle) -ミスリルバングル (Mithril Bangle) -ドラゴンバングル (Dragon Bangle) ------兜系 (-----Helmets) -バンダナ (Bandana) -レザーバンダナ (Leather Bandana) -皮の帽子 (Leather Hat) -ターバン (Turban) -羽根付き帽子 (Feathered Hat) -レザーキャップ (Leather Cap) -ウッドキャップ (Wood Cap) -ブロンズキャップ (Bronze Cap) -アイアンキャップ (Iron Cap) -ミスリルキャップ (Mithril Cap) -ドラゴンキャップ (Dragon Cap) -チェインコイフ (Chain Coif) -ブロンズヘルム (Bronze Helm) -アイアンヘルム (Iron Helm) -ミスリルヘルム (Mithril Helm) -ドラゴンヘルム (Dragon Helm) -とんがり帽子 (Pointed Hat) -サークレット (Circlet) -シルバーサークレット (Silver Circlet) -ゴールドサークレット (Gold Circlet) -ミスリルサークレット (Mithril Circlet) -マスターサークレット (Master Circlet) ------装飾品系 (-----Accessories) -ガードストーン (Guard Stone) -ガードリング (Guard Ring) -パワーストーン (Power Stone) -パワーリング (Power Ring) -スピードストーン (Speed Stone) -スピードリング (Speed Ring) -ラッキーストーン (Lucky Stone) -ラッキーリング (Lucky Ring) -マジカルストーン (Magical Stone) -マジカルリング (Magical Ring) -シューターストーン (Shooter Stone) -シューターリング (Shooter Ring) -マスターストーン (Master Stone) -マスターリング (Master Ring) -ポイズンガード (Poison Guard) -ポイズンガードS (Poison Guard S) -メンタルガード (Mental Guard) -メンタルガードS (Mental Guard S) -TP+ (TP+) -エネミーレーダー (Enemy Radar) -ワーニングベル (Warning Bell) -TPストッカー (TP Stocker) -ガッツリング (Guts Ring) -MPキーパー (MP Keeper) -エレメンタルガード (Elemental Guard) -スペクタクル (Spectacle) - -#Enemies -ゴブリン (Goblin) -ノーム (Gnome) -クロウ (Crow) -トレント (Treant) -魔犬 (Hellhound) -バットン (Batton) -山賊 (Bandit) -ハッチン (Hatchin) -女王ハッチン (Queen Hatchin) -騎士 (Knight) -赤騎士 (Red Knight) -パンダン (Pandan) -キノコン (Kinokon) -マンドレ (Mandre) -トレントン (Trenton) -レッドラン (Redran) -サイクロップ (Cyclops) -ハーピィ (Harpy) -ミノタウロス (Minotaur) -強ミノタウロス (Strong Minotaur) -暴漢 (Thug) -ゴブゴブリン (Gobgoblin) -レジスタンス (Resistance) -騎士 (Knight) -バイバットン (Bybatton) -悪魔犬 (Demon Dog) -マキシマムシ (Maximum Bug) -ストーカー (Stalker) -ナナハッチン (Nanahatchin) -スライン (Sline) -ミミックン (Mimicun) -ドットレントン (Dot Renton) -ブタン (Butan) -ミツハッチン (Mitsuhatchin) -野盗 (Bandit) -鎧の男 (Armored Man) -ボスオークン (Boss Orkun) -オークン (Orkun) -シュードの男 (Shude Man) -ボスネークン (Boss Snakun) -コスネークン (Kosnakun) -グレートパンダン (Great Pandan) -テスト (Test) -ゴブリソ (Goblinso) -ボスネークン (Boss Snakun) -復讐者 (Avenger) ``` \ No newline at end of file