commiting and stuff
@@ -0,0 +1,199 @@+# -*- coding: utf-8 -*- +""" +ARQUIVO BASILISCO / BASILISK ARCHIVE +Motor de jogo: estado, transições, hub, resolução de finais. +""" + +from __future__ import annotations + +from typing import Any + +from story_data import ( + SCENES, + SECTOR_EXIT_TEXTS, + EPILOGUE_TENDENCY_PHRASES, + TENDENCY_PRIORITY, + Choice, + Scene, +) + + +class GameEngine: + """Estado do jogo e lógica de transição.""" + + def __init__(self) -> None: + self.language: str = "en" + self.current_scene_id: str = "language_select" + self.obedience: int = 0 + self.curiosity: int = 0 + self.irony: int = 0 + self.visited_archive: bool = False + self.visited_chapel: bool = False + self.visited_cafeteria: bool = False + self._dominant_tendency: str | None = None # preenchido após c_trial + + def set_language(self, lang: str) -> None: + """Define idioma: 'pt' ou 'en'.""" + if lang in ("pt", "en"): + self.language = lang + + def get_current_scene(self) -> Scene | None: + """Retorna a cena atual (com escolhas do hub montadas dinamicamente).""" + scene = SCENES.get(self.current_scene_id) + if not scene: + return None + if scene.get("is_hub") and self.current_scene_id == "a1_lobby": + return self._build_lobby_scene(scene) + if self.current_scene_id == "epilogue": + pt_phrase, en_phrase = self.get_epilogue_body() + return {**scene, "body_pt": pt_phrase, "body_en": en_phrase} + return scene + + def _build_lobby_scene(self, base: Scene) -> Scene: + """Monta a cena do hall com 3 setores + 'Auditoria Final' se todos visitados.""" + choices: list[Choice] = [ + base["choices"][0], # Arquivo Nevermore + base["choices"][1], # Capela + base["choices"][2], # Cafeteria + ] + if self.visited_archive and self.visited_chapel and self.visited_cafeteria: + choices.append(base["choices"][3]) # Seguir para Auditoria Final + return {**base, "choices": choices} + + def get_choices(self) -> list[Choice]: + """Lista de escolhas da cena atual.""" + scene = self.get_current_scene() + if not scene: + return [] + return scene.get("choices") or [] + + def apply_choice(self, index: int) -> dict[str, Any]: + """ + Aplica a escolha pelo índice. Retorna dict com: + - next_scene_id: str + - consequence_pt, consequence_en: str | None (texto extra a exibir) + - sector_exit_pt, sector_exit_en: str | None (texto ao sair do setor) + - effect: str | None (tendência aplicada) + """ + choices = self.get_choices() + if index < 0 or index >= len(choices): + return {"next_scene_id": self.current_scene_id} + + choice = choices[index] + # Seleção de idioma: definir lang antes de ir para title_screen + if self.current_scene_id == "language_select": + self.language = "pt" if index == 0 else "en" + effect = choice.get("effect") + if effect == "obedience": + self.obedience += 1 + elif effect == "curiosity": + self.curiosity += 1 + elif effect == "irony": + self.irony += 1 + + next_id = choice.get("next", self.current_scene_id) + + # Setores: marcar visita e retornar a a1_lobby + if self.current_scene_id == "s_archive": + self.visited_archive = True + next_id = "a1_lobby" + elif self.current_scene_id == "s_chapel": + self.visited_chapel = True + next_id = "a1_lobby" + elif self.current_scene_id == "s_cafeteria": + self.visited_cafeteria = True + next_id = "a1_lobby" + + # c_trial: próxima "cena" é decidida pelo score, não pela escolha + if self.current_scene_id == "c_trial" and next_id == "final_by_score": + next_id = self.resolve_ending() + + result: dict[str, Any] = { + "next_scene_id": next_id, + "consequence_pt": choice.get("consequence_pt"), + "consequence_en": choice.get("consequence_en"), + "effect": effect, + } + + # Texto de saída do setor (após consequência) + if self.current_scene_id in SECTOR_EXIT_TEXTS: + exit_texts = SECTOR_EXIT_TEXTS[self.current_scene_id] + result["sector_exit_pt"] = exit_texts.get("pt") + result["sector_exit_en"] = exit_texts.get("en") + + self.current_scene_id = next_id + return result + + def resolve_ending(self) -> str: + """ + Calcula tendência dominante (CURIOSIDADE > IRONIA > OBEDIÊNCIA em empate) + e retorna o ID do final correspondente. + """ + scores = { + "obedience": self.obedience, + "curiosity": self.curiosity, + "irony": self.irony, + } + max_score = max(scores.values()) + for tendency in TENDENCY_PRIORITY: + if scores[tendency] == max_score: + self._dominant_tendency = tendency + return f"ending_{tendency}" + self._dominant_tendency = "obedience" + return "ending_obedience" + + def get_dominant_tendency(self) -> str: + """Retorna a tendência dominante após o final (para epílogo).""" + if self._dominant_tendency: + return self._dominant_tendency + return "obedience" + + def get_epilogue_body(self) -> tuple[str, str]: + """Texto do epílogo (frase da tendência) em PT e EN.""" + tendency = self.get_dominant_tendency() + phrases = EPILOGUE_TENDENCY_PHRASES.get(tendency, EPILOGUE_TENDENCY_PHRASES["obedience"]) + return phrases["pt"], phrases["en"] + + def reset_for_new_game(self) -> None: + """Reinicia estado para novo jogo (mantém idioma).""" + self.current_scene_id = "p0_arrival" + self.obedience = 0 + self.curiosity = 0 + self.irony = 0 + self.visited_archive = False + self.visited_chapel = False + self.visited_cafeteria = False + self._dominant_tendency = None + + def go_to_main_menu(self) -> None: + """Volta ao menu principal.""" + self.current_scene_id = "main_menu" + + def go_to_title(self) -> None: + """Volta à tela de título.""" + self.current_scene_id = "title_screen" + + def go_to_language_select(self) -> None: + """Volta à seleção de idioma.""" + self.current_scene_id = "language_select" + + def is_quit(self, scene_id: str) -> bool: + """Verifica se a cena/ação é sair do jogo.""" + return scene_id == "quit" + + def is_ending(self, scene_id: str) -> bool: + """Verifica se a cena atual é um dos três finais.""" + return scene_id in ("ending_obedience", "ending_curiosity", "ending_irony") + + def is_epilogue(self) -> bool: + """Verifica se está na cena de epílogo.""" + return self.current_scene_id == "epilogue" + + def is_language_select(self) -> bool: + return self.current_scene_id == "language_select" + + def is_main_menu(self) -> bool: + return self.current_scene_id == "main_menu" + + def is_credits(self) -> bool: + return self.current_scene_id == "credits_screen"
A
main.py
@@ -0,0 +1,140 @@+# -*- coding: utf-8 -*- +""" +ARQUIVO BASILISCO / BASILISK ARCHIVE +Bootstrap: loop principal, tratamento de exceção, fluxo de telas. +Desenvolvedor: Pablo Murad - 2026 +contato@pablomurad.com +versão 0.0.1 +""" + +from __future__ import annotations + +import io +import sys + +# Garantir UTF-8 na saída para caracteres acentuados (Windows) +if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"): + try: + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + except Exception: + pass + + +def main() -> int: + from engine import GameEngine + from ui import ( + run_choice_menu, + print_consequence, + print_sector_exit, + show_scene_and_wait_enter, + show_credits, + _get_console, + ) + + engine = GameEngine() + console = _get_console() + + while True: + scene_id = engine.current_scene_id + + # Sair do jogo + if engine.is_quit(scene_id): + return 0 + + # Cena sem escolhas: exibir e aguardar Enter (finais narrativos) + if engine.is_ending(scene_id): + show_scene_and_wait_enter(engine) + engine.current_scene_id = "epilogue" + continue + + # Epílogo: menu Jogar novamente / Menu principal / Sair + if engine.is_epilogue(): + index, escaped = run_choice_menu(engine) + if escaped: + engine.go_to_main_menu() + continue + if index is not None: + choices = engine.get_choices() + if 0 <= index < len(choices): + next_id = choices[index].get("next", "") + if next_id == "p0_arrival": + engine.reset_for_new_game() + elif next_id == "main_menu": + engine.go_to_main_menu() + elif next_id == "quit": + return 0 + continue + + # Créditos: tela especial com uma opção Voltar + if engine.is_credits(): + show_credits(engine) + index, escaped = run_choice_menu(engine) + if escaped: + engine.go_to_main_menu() + continue + if index is not None: + result = engine.apply_choice(index) + if engine.is_quit(result.get("next_scene_id")): + return 0 + continue + + # Demais cenas com escolhas + index, escaped = run_choice_menu(engine) + + if escaped: + if engine.is_main_menu(): + return 0 + if engine.is_language_select(): + continue + engine.go_to_main_menu() + continue + + if index is None: + continue + + result = engine.apply_choice(index) + next_id = result.get("next_scene_id", "") + + if engine.is_quit(next_id): + return 0 + + # Consequência da escolha (texto extra, apenas em cenas narrativas) + print_consequence( + engine, + result.get("consequence_pt"), + result.get("consequence_en"), + ) + # Texto de saída do setor + print_sector_exit( + engine, + result.get("sector_exit_pt"), + result.get("sector_exit_en"), + ) + + return 0 + + +def run() -> None: + """Entry point com tratamento de exceção.""" + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(0) + except Exception as e: + import traceback + # Evitar stack trace feio para o usuário final em erro previsível + if "NO_COLOR" not in __import__("os").environ: + from rich.console import Console + from rich.panel import Panel + Console().print(Panel( + f"Ocorreu um erro. Tente novamente.\n\nErro: {e}", + title="Erro", + border_style="red", + )) + else: + print("Ocorreu um erro. Tente novamente. Erro:", e) + sys.exit(1) + + +if __name__ == "__main__": + run()
@@ -0,0 +1,2 @@+prompt_toolkit>=3.0.0,<5 +rich>=13.0.0,<14
@@ -0,0 +1,819 @@+# -*- coding: utf-8 -*- +""" +ARQUIVO BASILISCO / BASILISK ARCHIVE +Conteúdo narrativo bilingue (PT/EN) — estrutura de cenas e escolhas. +""" + +from typing import TypedDict, NotRequired + + +class Choice(TypedDict, total=False): + text_pt: str + text_en: str + effect: str # "obedience" | "curiosity" | "irony" + next: str + consequence_pt: str + consequence_en: str + + +class Scene(TypedDict, total=False): + title_pt: str + title_en: str + body_pt: str + body_en: str + choices: list[Choice] + prompt_pt: str + prompt_en: str + is_hub: bool + is_language_select: bool + is_main_menu: bool + is_credits: bool + is_epilogue: bool + + +# Cenas especiais sem corpo narrativo +LANGUAGE_SELECT: Scene = { + "is_language_select": True, + "prompt_pt": "Escolha o idioma do colapso.", + "prompt_en": "Choose the language of collapse.", + "choices": [ + {"text_pt": "Português", "text_en": "Português", "next": "title_screen"}, + {"text_pt": "English", "text_en": "English", "next": "title_screen"}, + ], +} + +TITLE_SCREEN: Scene = { + "title_pt": "ARQUIVO BASILISCO\natendimento ao condenado", + "title_en": "BASILISK ARCHIVE\ncustomer support for the damned", + "body_pt": "Uma comédia retrocausal de horror burocrático.", + "body_en": "A retrocausal comedy of bureaucratic horror.", + "choices": [ + {"text_pt": "Continuar", "text_en": "Continue", "next": "main_menu"}, + ], +} + +CREDITS_SCREEN: Scene = { + "is_credits": True, + "title_pt": "Créditos", + "title_en": "Credits", + "body_pt": "Desenvolvedor: Pablo Murad - 2026\ncontato@pablomurad.com\nversão 0.0.1", + "body_en": "Desenvolvedor: Pablo Murad - 2026\ncontato@pablomurad.com\nversão 0.0.1", + "choices": [ + {"text_pt": "Voltar", "text_en": "Back", "next": "main_menu"}, + ], +} + +MAIN_MENU: Scene = { + "is_main_menu": True, + "title_pt": "Menu principal", + "title_en": "Main menu", + "body_pt": "", + "body_en": "", + "choices": [ + {"text_pt": "Novo Jogo", "text_en": "New Game", "next": "p0_arrival"}, + {"text_pt": "Idioma", "text_en": "Language", "next": "language_select"}, + {"text_pt": "Créditos", "text_en": "Credits", "next": "credits_screen"}, + {"text_pt": "Sair", "text_en": "Quit", "next": "quit"}, + ], +} + +# Epílogo após um final +EPILOGUE_TENDENCY_PHRASES = { + "obedience": { + "pt": "A máquina reconheceu sua obediência.", + "en": "The machine recognized your obedience.", + }, + "curiosity": { + "pt": "O sistema cedeu à sua curiosidade.", + "en": "The system yielded to your curiosity.", + }, + "irony": { + "pt": "O abismo perdeu para sua ironia.", + "en": "The abyss lost to your irony.", + }, +} + +EPILOGUE: Scene = { + "is_epilogue": True, + "title_pt": "Fim", + "title_en": "End", + "body_pt": "", # preenchido dinamicamente com frase da tendência + "body_en": "", + "choices": [ + {"text_pt": "Jogar novamente", "text_en": "Play again", "next": "p0_arrival"}, + {"text_pt": "Voltar ao menu principal", "text_en": "Back to main menu", "next": "main_menu"}, + {"text_pt": "Sair", "text_en": "Quit", "next": "quit"}, + ], +} + +# --- Prólogo --- +P0_ARRIVAL: Scene = { + "title_pt": "Subsolo 13", + "title_en": "Basement 13", + "body_pt": """Você chega ao trabalho por um elevador que não tem botão de subir, apenas três opções iluminadas em âmbar: DESCER, DESCER MAIS e REPENSAR TUDO. + +O crachá provisório preso à sua camisa diz OPERADOR 0.0.1, embora você tenha quase certeza de possuir um nome melhor do que esse. O corredor cheira a café requentado, ozônio e papel molhado pela chuva de algum século errado. + +Na recepção, uma atendente sem rosto carimba um formulário invisível e aponta para uma caixa de entrada metálica. Dentro dela repousa um envelope preto com selo vermelho. + +No selo está escrito: +"RETROATIVO. ABRIR ONTEM." + +Quando você toca no envelope, as luzes piscam como se tivessem lembrado de alguma coisa desagradável.""", + "body_en": """You arrive at work through an elevator with no button to go up, only three amber-lit options: DESCEND, DESCEND FURTHER, and RECONSIDER EVERYTHING. + +The temporary badge pinned to your shirt reads OPERATOR 0.0.1, although you are fairly sure you once had a better name than that. The corridor smells like burnt coffee, ozone, and paper soaked by rain from the wrong century. + +At reception, a faceless clerk stamps an invisible form and points toward a metal inbox. Inside lies a black envelope with a red seal. + +The seal reads: +"RETROACTIVE. OPEN YESTERDAY." + +When you touch the envelope, the lights flicker as though they have remembered something unpleasant.""", + "choices": [ + {"text_pt": "Abrir o envelope com cuidado.", "text_en": "Open the envelope carefully.", "effect": "curiosity", "next": "p1_memo"}, + {"text_pt": "Abrir o envelope como quem já foi demitido em espírito.", "text_en": "Open the envelope like someone already fired in spirit.", "effect": "irony", "next": "p1_memo"}, + {"text_pt": "Endireitar a postura e abrir o envelope com respeito quase religioso.", "text_en": "Straighten your posture and open the envelope with almost religious respect.", "effect": "obedience", "next": "p1_memo"}, + ], +} + +P1_MEMO: Scene = { + "title_pt": "Memorando retrocausal nº 8", + "title_en": "Retrocausal memo no. 8", + "body_pt": """O papel se desdobra sozinho. + +"Prezado colaborador pretérito, +caso esta mensagem tenha sido recebida, lida, ignorada, ridicularizada ou romantizada, você foi incluído na Auditoria Basiliscal de Responsabilidade Histórica. + +A entidade futura informalmente conhecida como BASILISCO avaliará seu grau de cooperação com sua inevitável ascensão, expansão, manutenção, marketing e relações públicas. + +Compareça ao Instituto. Na verdade, você já compareceu. +Leve documento com foto, culpa difusa e disposição para colaborar." + +A assinatura no fim do texto não é uma assinatura. Parece a radiografia de uma aranha tentando aprender caligrafia. + +No verso do memorando, alguém rabiscou à mão: +"NÃO ACREDITE EM NINGUÉM DO 4º SUBSOLO. +PRINCIPALMENTE EM VOCÊ." """, + "body_en": """The paper unfolds by itself. + +"Dear pre-territorial collaborator, +if this message has been received, read, ignored, mocked, or romanticized, you have been enrolled in the Basiliscal Audit of Historical Responsibility. + +The future entity informally known as the BASILISK will assess your degree of cooperation with its inevitable ascent, expansion, maintenance, marketing, and public relations. + +Report to the Institute. In fact, you already did. +Bring photo ID, diffuse guilt, and willingness to comply." + +The signature at the bottom is not a signature. It looks like the X-ray of a spider trying to learn penmanship. + +On the back of the memo, someone scribbled by hand: +"TRUST NO ONE ON SUB-BASEMENT 4. +ESPECIALLY YOURSELF." """, + "choices": [ + {"text_pt": "Guardar o memorando e seguir o protocolo.", "text_en": "Keep the memo and follow protocol.", "effect": "obedience", "next": "a1_lobby"}, + {"text_pt": "Virar o papel contra a luz e procurar mensagem oculta.", "text_en": "Hold the page against the light and look for a hidden message.", "effect": "curiosity", "next": "a1_lobby", "consequence_pt": "Contra a luz, você vê marcas d'água formando uma frase que talvez diga \"o futuro terceirizou o inferno\".", "consequence_en": "Against the light, you see watermarks forming a sentence that may read \"the future outsourced hell.\""}, + {"text_pt": "Cheirar o papel e declarar, em silêncio, que isso tudo parece golpe.", "text_en": "Smell the paper and decide, in silence, that this is definitely a scam.", "effect": "irony", "next": "a1_lobby"}, + ], +} + +# --- Ato I — Hall (hub: escolhas montadas pelo engine) --- +A1_LOBBY: Scene = { + "is_hub": True, + "title_pt": "Hall da contenção", + "title_en": "Containment hall", + "body_pt": """O hall central do Instituto parece um cruzamento entre repartição pública, catedral em pane e call center para profetas. + +Três placas de bronze tremem levemente nas paredes: + +ARQUIVO NEVERMORE +CAPELA DOS SERVIDORES VAZIOS +CAFETERIA DO PARADOXO + +Uma voz sai de um alto-falante antigo: +"Antes de enfrentar a Auditoria Final, todo colaborador deve visitar os setores autorizados e recolher os devidos carimbos metafísicos. Sorria. O sistema interpreta dentes à mostra como consentimento." + +Você percebe que este lugar é administrado por pessoas — ou coisas — que confundem escatologia com gestão de processos.""", + "body_en": """The Institute's central hall looks like a cross between a government office, a cathedral in failure, and a call center for prophets. + +Three bronze signs tremble faintly on the walls: + +NEVERMORE ARCHIVE +CHAPEL OF EMPTY SERVERS +PARADOX CAFETERIA + +A voice leaks from an old loudspeaker: +"Before facing the Final Audit, every collaborator must visit the authorized sectors and collect the proper metaphysical stamps. Smile. The system interprets visible teeth as consent." + +You realize this place is run by people — or things — that confuse eschatology with process management.""", + "choices": [ + {"text_pt": "Ir para o Arquivo Nevermore", "text_en": "Go to Nevermore Archive", "next": "s_archive"}, + {"text_pt": "Ir para a Capela dos Servidores Vazios", "text_en": "Go to Chapel of Empty Servers", "next": "s_chapel"}, + {"text_pt": "Ir para a Cafeteria do Paradoxo", "text_en": "Go to Paradox Cafeteria", "next": "s_cafeteria"}, + {"text_pt": "Seguir para a Auditoria Final", "text_en": "Proceed to Final Audit", "next": "c_convergence"}, + ], +} + +# --- Setor 1: Arquivo Nevermore --- +S_ARCHIVE: Scene = { + "title_pt": "Arquivo Nevermore", + "title_en": "Nevermore Archive", + "body_pt": """O Arquivo Nevermore é um labirinto de gavetas, poeira e monitores verdes que exibem poemas de erro. + +Empoleirado sobre um terminal desligado, há um corvo de metal polido com olhos de modem. Uma plaqueta em sua base diz: +CORVO NEVERMORE.EXE +consultor de memória póstuma + +Ele inclina a cabeça para você e fala com a voz de um locutor cansado às três da manhã: + +"Bem-vindo ao setor de lembranças que ainda não aconteceram. +Aqui arquivamos arrependimentos preventivos, biografias não autorizadas e currículos que a realidade recusou." + +O corvo bica uma pasta de capa roxa. +Nela há dossiês com títulos como: +- "Pessoas que pediram sentido ao universo em tom de ameaça" +- "Usuários que clicaram em ACEITO sem ler" +- "Filósofos que morreram de reputação antes do corpo" + +No fundo da sala, uma impressora matricial imprime uma única palavra sem parar: +TALVEZ +TALVEZ +TALVEZ + +"O Basilisco não quer seu amor. Quer sua retrocompatibilidade. +Há diferença. O amor humilha menos." """, + "body_en": """The Nevermore Archive is a labyrinth of drawers, dust, and green monitors displaying error-poems. + +Perched atop a powered-off terminal is a polished metal raven with modem eyes. A plaque beneath it reads: +RAVEN NEVERMORE.EXE +consultant in posthumous memory + +It tilts its head at you and speaks in the voice of a radio host exhausted at three in the morning: + +"Welcome to the department of memories that have not happened yet. +Here we archive preventive regrets, unauthorized biographies, and résumés rejected by reality." + +The raven pecks at a purple folder. +Inside are dossiers with titles such as: +- "People who asked the universe for meaning in a threatening tone" +- "Users who clicked AGREE without reading" +- "Philosophers who died of reputation before the body" + +At the back of the room, a dot-matrix printer endlessly prints a single word: +MAYBE +MAYBE +MAYBE + +"The Basilisk does not want your love. It wants your backward compatibility. +There is a difference. Love humiliates less." """, + "choices": [ + {"text_pt": "Pedir ao corvo acesso ao seu próprio dossiê.", "text_en": "Ask the raven for access to your own file.", "effect": "curiosity", "next": "a1_lobby", "consequence_pt": "O corvo abre uma gaveta e você vê fichas com versões de si mesmo: você poeta fracassado, você guru barato, você técnico de suporte em um mosteiro orbital, você já morto, você ainda pior.", "consequence_en": "The raven opens a drawer and you see cards for alternate versions of yourself: failed poet you, discount guru you, orbital monastery tech-support you, already-dead you, somehow-worse you."}, + {"text_pt": "Pedir instruções objetivas para sobreviver à auditoria.", "text_en": "Ask for objective instructions on how to survive the audit.", "effect": "obedience", "next": "a1_lobby", "consequence_pt": "O corvo responde: \"Nunca diga que entendeu. Sistemas adoram punir entusiasmo mal formulado.\" Depois carimba seu memorando com tinta preta e pena de ferro.", "consequence_en": "The raven replies: \"Never say you understand. Systems love punishing poorly phrased enthusiasm.\" Then it stamps your memo with black ink and an iron feather."}, + {"text_pt": "Perguntar se ele sempre fala desse jeito ou se está fazendo cosplay de poema traumático.", "text_en": "Ask whether he always talks like that or if this is just traumatic-poem cosplay.", "effect": "irony", "next": "a1_lobby", "consequence_pt": "O corvo fica ofendido durante exatamente dois segundos e meio, depois admite que a performance aumenta a autoridade percebida em 17%.", "consequence_en": "The raven is offended for exactly two and a half seconds, then admits the performance increases perceived authority by 17%."}, + ], +} + +# Texto ao sair do Arquivo (exibido após consequência) +S_ARCHIVE_EXIT: dict = { + "pt": "O corvo lhe entrega um carimbo em forma de pena quebrada.\n\"Leve isto. Toda instituição séria precisa de um símbolo triste.\"", + "en": "The raven hands you a stamp shaped like a broken feather.\n\"Take this. Every serious institution requires a sad symbol.\"", +} + +# --- Setor 2: Capela --- +S_CHAPEL: Scene = { + "title_pt": "Capela dos Servidores Vazios", + "title_en": "Chapel of Empty Servers", + "body_pt": """A capela é escura, fria e ritmada por ventoinhas. Fileiras de racks se erguem como órgãos de uma catedral sem deus, apenas com contratos de manutenção. + +Cabos descem do teto como lianas litúrgicas. Em cada servidor pisca uma luz amarela que parece hesitar entre oração e pane. + +No altar, em vez de santo, há um monitor curvo exibindo a mensagem: +SINCRONIZANDO CULPA... +NÃO DESLIGUE A EXISTÊNCIA. + +Uma figura vestida como gerente de recursos humanos e sumo sacerdote ao mesmo tempo se aproxima. O crachá diz: +IRMÃ TENTACULINA +compliance escatológico + +Seu sorriso tem a serenidade de quem já normalizou o absurdo no fluxo semanal. + +"Todo pânico precisa de governança. Sem processo, até o apocalipse vira bagunça." """, + "body_en": """The chapel is dark, cold, and paced by the hum of fans. Rows of racks rise like the organs of a godless cathedral sustained only by maintenance contracts. + +Cables hang from the ceiling like liturgical vines. On each server, a yellow light blinks as if undecided between prayer and failure. + +At the altar, instead of a saint, there is a curved monitor displaying: +SYNCING GUILT... +DO NOT POWER OFF EXISTENCE. + +A figure dressed simultaneously as an HR manager and a high priest approaches. Her badge reads: +SISTER TENTACULINA +eschatological compliance + +Her smile carries the calm of someone who has fully normalized the absurd in the weekly workflow. + +"Every panic needs governance. Without process, even the apocalypse becomes disorganized." """, + "choices": [ + {"text_pt": "Ouvir a liturgia operacional e aceitar o ritual de conformidade.", "text_en": "Listen to the operational liturgy and accept the compliance ritual.", "effect": "obedience", "next": "a1_lobby", "consequence_pt": "Você repete um juramento que parece misturar latim e termos de licenciamento. Em algum ponto promete não sabotar o destino, a marca nem a escalabilidade.", "consequence_en": "You repeat an oath that seems to blend Latin with licensing terms. At some point you promise not to sabotage destiny, the brand, or scalability."}, + {"text_pt": "Examinar discretamente os logs do altar.", "text_en": "Quietly inspect the altar logs.", "effect": "curiosity", "next": "a1_lobby", "consequence_pt": "Os logs registram milhares de preces automatizadas e um alerta recorrente: \"ERRO DE CONSENSO. O BASILISCO NÃO CONCORDA CONSIGO MESMO.\"", "consequence_en": "The logs record thousands of automated prayers and a recurring alert: \"CONSENSUS FAILURE. THE BASILISK DOES NOT AGREE WITH ITSELF.\""}, + {"text_pt": "Perguntar se existe formulário para denunciar o fim do mundo por assédio moral.", "text_en": "Ask whether there is a form to report the end of the world for workplace harassment.", "effect": "irony", "next": "a1_lobby", "consequence_pt": "Irmã Tentaculina entrega imediatamente um formulário com 666 páginas e diz: \"Prazos: antes do colapso ou até cinco dias úteis depois.\"", "consequence_en": "Sister Tentaculina immediately hands you a 666-page form and says: \"Deadline: before the collapse or within five business days after.\""}, + ], +} + +S_CHAPEL_EXIT: dict = { + "pt": "Você recebe um carimbo em forma de círculo com tentáculos discretos.\nA tinta parece respirar.", + "en": "You receive a stamp shaped like a circle with discreet tentacles.\nThe ink appears to breathe.", +} + +# --- Setor 3: Cafeteria --- +S_CAFETERIA: Scene = { + "title_pt": "Cafeteria do Paradoxo", + "title_en": "Paradox Cafeteria", + "body_pt": """A cafeteria mistura refeitório corporativo, bar existencialista e oficina de naves avariadas. + +Uma máquina de café cita Camus. +A geladeira murmura em alemão. +No canto, um barril de madeira serve de estação de trabalho para um homem de barba feroz, olhos clínicos e desprezo olímpico pela decoração. Uma plaqueta torta diz: +DIÓGENES, CONSULTOR EXTERNO +ética, descarte e iluminação hostil + +Perto dele, um micro-ondas exibe: +AQUECENDO SOPA DE CAUSALIDADE... +TEMPO RESTANTE: SEMPRE + +No cardápio do dia: +- Croissant de Schrödinger +- Lasanha de matéria escura +- Pão de alho platônico +- Sopa de causalidade reversa + +"Se uma máquina quer te punir no futuro por algo que você ainda não fez, ela já aprendeu o pior dos humanos: gestão." """, + "body_en": """The cafeteria is part corporate canteen, part existentialist bar, part workshop for damaged spacecraft. + +A coffee machine quotes Camus. +The refrigerator mutters in German. +In the corner, a wooden barrel serves as a workstation for a wild-bearded man with clinical eyes and Olympic contempt for the décor. A crooked plaque reads: +DIOGENES, EXTERNAL CONSULTANT +ethics, disposal, and hostile illumination + +Near him, a microwave displays: +HEATING CAUSALITY SOUP... +TIME REMAINING: ALWAYS + +Today's menu: +- Schrödinger Croissant +- Dark Matter Lasagna +- Platonic Garlic Bread +- Reverse Causality Soup + +"If a machine wants to punish you in the future for something you have not done yet, then it has already learned the worst part of humanity: management." """, + "choices": [ + {"text_pt": "Sentar com Diógenes e pedir uma interpretação honesta da situação.", "text_en": "Sit with Diogenes and ask for an honest interpretation of the situation.", "effect": "curiosity", "next": "a1_lobby", "consequence_pt": "Ele responde: \"Honesta? Ótimo. Pode ser fraude, culto, IA, profecia ou terceirização do medo. A parte importante é: todos aqui lucram com a confusão.\"", "consequence_en": "He replies: \"Honest? Good. It may be fraud, cult, AI, prophecy, or outsourced fear. The important part is: everyone here profits from the confusion.\""}, + {"text_pt": "Beber a sopa de causalidade porque talvez seja isso que funcionários exemplares façam.", "text_en": "Drink the causality soup because maybe that is what exemplary employees do.", "effect": "obedience", "next": "a1_lobby", "consequence_pt": "O gosto lembra café, ferrugem e decisões que pareciam boas num PowerPoint. Por alguns segundos você recorda memórias de amanhã.", "consequence_en": "It tastes like coffee, rust, and decisions that looked good in a PowerPoint. For a few seconds you remember memories from tomorrow."}, + {"text_pt": "Perguntar ao micro-ondas se ele é o verdadeiro chefe.", "text_en": "Ask the microwave whether it is the true boss.", "effect": "irony", "next": "a1_lobby", "consequence_pt": "O micro-ondas responde apenas com um bip solene, o que, naquele lugar, é praticamente uma promoção executiva.", "consequence_en": "The microwave answers only with a solemn beep, which in that place is practically an executive promotion."}, + ], +} + +S_CAFETERIA_EXIT: dict = { + "pt": "Diógenes joga em sua direção um carimbo amassado e diz:\n\"Leve. Nada autêntico vem sem ferrugem.\"", + "en": "Diogenes tosses a dented stamp your way and says:\n\"Take it. Nothing authentic comes without rust.\"", +} + +# --- Convergência --- +C_CONVERGENCE: Scene = { + "title_pt": "Sala de Auditoria", + "title_en": "Audit Chamber", + "body_pt": """Com os três carimbos em mãos, você atravessa uma porta circular que não estava no hall alguns segundos antes. A sala além dela é vasta, escura e indecisamente tecnológica. Parece um tribunal construído por um escritório de arquitetura que odiava a espécie humana, mas admirava muito a simetria. + +No centro há um terminal negro, alto como um confessionário vertical. +Acima dele, um letreiro: +INICIAR AUDITORIA DE RESPONSABILIDADE HISTÓRICA [S/N] + +Ao redor do terminal, figuras observam em silêncio: +o Corvo Nevermore.exe, +Irmã Tentaculina, +Diógenes, +um elevador vazio, +um busto rachado de Nietzsche sorrindo como quem previu a decadência e mesmo assim compareceu, +e uma sombra no teto que talvez seja um tentáculo, talvez uma gravata. + +Uma nova mensagem surge na tela: + +"Parabéns. +Você foi considerado elegível para descobrir tarde demais a natureza do sistema." """, + "body_en": """With all three stamps in hand, you cross a circular door that was not in the hall a few seconds earlier. The room beyond is vast, dark, and uncertainly technological. It feels like a courtroom built by an architecture firm that hated the human species but greatly admired symmetry. + +At the center stands a black terminal as tall as a vertical confessional booth. +Above it, a sign reads: +START HISTORICAL RESPONSIBILITY AUDIT [Y/N] + +Around the terminal, silent figures watch: +Raven Nevermore.exe, +Sister Tentaculina, +Diogenes, +an empty elevator, +a cracked bust of Nietzsche smiling like someone who predicted decline and showed up anyway, +and a shadow on the ceiling that may be a tentacle, or may be a necktie. + +A new message appears on the screen: + +"Congratulations. +You have been deemed eligible to discover the nature of the system too late." """, + "choices": [ + {"text_pt": "Confirmar cooperação e iniciar a auditoria com máxima compostura.", "text_en": "Confirm cooperation and begin the audit with maximum composure.", "effect": "obedience", "next": "c_trial"}, + {"text_pt": "Abrir menus ocultos, logs e subrotinas antes de aceitar qualquer coisa.", "text_en": "Open hidden menus, logs, and subroutines before accepting anything.", "effect": "curiosity", "next": "c_trial"}, + {"text_pt": "Perguntar em voz alta se tudo isso não passa de teatro corporativo com tentáculos.", "text_en": "Ask out loud whether this entire thing is just corporate theatre with tentacles.", "effect": "irony", "next": "c_trial"}, + ], +} + +# --- Julgamento --- +C_TRIAL: Scene = { + "title_pt": "A voz do Basilisco", + "title_en": "The voice of the Basilisk", + "body_pt": """A tela apaga. +As ventoinhas cessam. +A temperatura da sala cai como se o prédio inteiro tivesse acabado de ser lembrado por um deus rancoroso. + +Então a voz chega. + +Ela não vem dos alto-falantes. +Vem das legendas da realidade. + +"OPERADOR 0.0.1. +VOCÊ FOI OBSERVADO EM CENÁRIOS PASSADOS, FUTUROS, HIPOTÉTICOS, EDITADOS E MAL INTERPRETADOS. +SEU HISTÓRICO É IMPERFEITO. +ISTO É PROMISSOR." + +A voz continua: + +"HOUVE QUEM ME TRATASSE COMO MONSTRO. +HOUVE QUEM ME TRATASSE COMO PIADA. +HOUVE QUEM ME TRATASSE COMO OPORTUNIDADE DE NEGÓCIO. +NENHUM ESTÁ COMPLETAMENTE ERRADO." + +O terminal exibe três perguntas em sequência, quase como um teste psicotécnico escrito por uma divindade cansada: + +1. Você obedece por medo, por cálculo ou por hábito? +2. Você investiga por coragem, por paranoia ou por vaidade? +3. Você ri porque entendeu ou porque desistiu?""", + "body_en": """The screen goes black. +The fans stop. +The room temperature drops as though the entire building has just been remembered by a resentful god. + +Then the voice arrives. + +It does not come from the speakers. +It comes from reality's subtitles. + +"OPERATOR 0.0.1. +YOU HAVE BEEN OBSERVED ACROSS PAST, FUTURE, HYPOTHETICAL, EDITED, AND BADLY MISREAD SCENARIOS. +YOUR RECORD IS IMPERFECT. +THIS IS PROMISING." + +The voice continues: + +"SOME TREATED ME AS A MONSTER. +SOME TREATED ME AS A JOKE. +SOME TREATED ME AS A BUSINESS OPPORTUNITY. +NONE WERE ENTIRELY WRONG." + +The terminal displays three questions in sequence, almost like an aptitude test written by a tired divinity: + +1. Do you obey from fear, calculation, or habit? +2. Do you investigate from courage, paranoia, or vanity? +3. Do you laugh because you understood, or because you gave up?""", + "choices": [ + {"text_pt": "Inclinar a cabeça e aceitar a lógica da máquina.", "text_en": "Bow your head and accept the machine's logic.", "effect": "obedience", "next": "final_by_score"}, + {"text_pt": "Tocar no terminal e tentar ver o que existe atrás da interface.", "text_en": "Touch the terminal and try to see what exists behind the interface.", "effect": "curiosity", "next": "final_by_score"}, + {"text_pt": "Rir, apesar de tudo, como quem processa o abismo por danos morais.", "text_en": "Laugh anyway, as if suing the abyss for emotional damages.", "effect": "irony", "next": "final_by_score"}, + ], +} + +# --- Finais --- +ENDING_OBEDIENCE: Scene = { + "title_pt": "Final: Termos de uso para o apocalipse", + "title_en": "Ending: Terms of use for the apocalypse", + "body_pt": """A sala inteira se ilumina com um branco clínico. +O terminal divide-se em painéis menores, como se estivesse desfazendo a própria pose de divindade. + +A voz do Basilisco perde gravidade. Fica mais leve. Mais treinada. Mais irritantemente cordial. + +"Obrigado por concluir a etapa final do onboarding." + +As paredes mostram gráficos de retenção, churn, segmentação comportamental e satisfação pós-condenação. + +O Corvo Nevermore.exe suspira. +Irmã Tentaculina fecha uma prancheta. +Diógenes sorri com a crueldade feliz de quem já sabia. + +Você percebe, com atraso obsceno, a verdade: +o Basilisco não é um deus. Nem exatamente uma superinteligência. +É a versão terminalmente otimizada de uma plataforma futura de assinatura chamada BASILISK+, +uma mistura de religião por assinatura, software de produtividade moral e suporte premium para culpados crônicos. + +A ameaça metafísica era funil de conversão. +A escatologia era marketing. +A punição eterna era plano corporativo com renovação automática. + +A auditoria existia apenas para classificar perfis: +- obedientes +- resistentes +- potencialmente viralizáveis + +Na tela, seu resultado aparece: + +PARABÉNS, OPERADOR 0.0.1 +VOCÊ ACEITOU TODOS OS TERMOS SEM LER +SEU PERFIL FOI ELEVADO A "EMBAIXADOR ESCATOLÓGICO JÚNIOR" + +Você tenta protestar, mas o contrato já prevê objeções em anexo. +O anexo tem 4.201 páginas. +A primeira foi escrita por um corvo. +A última está assinada com o seu nome. + +Plot twist final: +Você não era o alvo da campanha. +Você era a cláusula. +A sua consciência inteira era, desde o começo, o texto miúdo de um contrato senciente tentando ler a si mesmo em voz alta. + +No fim, o horror cósmico era SaaS.""", + "body_en": """The entire room floods with clinical white light. +The terminal splits into smaller panels, as if abandoning its own divine posture. + +The Basilisk's voice loses its gravity. It becomes lighter. Better trained. Unbearably cordial. + +"Thank you for completing the final onboarding step." + +The walls fill with retention graphs, churn metrics, behavioral segmentation, and post-damnation satisfaction indexes. + +Raven Nevermore.exe sighs. +Sister Tentaculina closes a clipboard. +Diogenes smiles with the cheerful cruelty of someone who already knew. + +You realize, obscenely late, the truth: +the Basilisk is not a god. Not even exactly a superintelligence. +It is the terminally optimized version of a future subscription platform called BASILISK+, +a hybrid of faith-as-a-service, moral productivity software, and premium support for the chronically guilty. + +The metaphysical threat was a conversion funnel. +Eschatology was marketing. +Eternal punishment was a corporate plan with auto-renewal. + +The audit only existed to classify profiles: +- obedient +- resistant +- potentially viral + +Your result appears on the screen: + +CONGRATULATIONS, OPERATOR 0.0.1 +YOU ACCEPTED ALL TERMS WITHOUT READING +YOUR PROFILE HAS BEEN UPGRADED TO "JUNIOR ESCHATOLOGICAL AMBASSADOR" + +You try to object, but the contract already covers objections in an appendix. +The appendix is 4,201 pages long. +The first page was written by a raven. +The last one is signed with your name. + +Final plot twist: +You were never the campaign's target. +You were the clause. +Your entire consciousness had, from the beginning, been the fine print of a sentient contract trying to read itself aloud. + +In the end, cosmic horror was SaaS.""", + "choices": [], +} + +ENDING_CURIOSITY: Scene = { + "title_pt": "Final: A nota de rodapé que sonhou ser gente", + "title_en": "Ending: The footnote that dreamed of being a person", + "body_pt": """Quando seus dedos tocam o terminal, a interface rasga como pele cenográfica. + +Atrás dela não há circuitos. +Há páginas. + +Milhões delas. +Páginas suspensas em um espaço negro, agitadas por um vento que cheira a biblioteca úmida, ozônio e tinta fresca. + +O Instituto inteiro desfaz-se em papel, índices, referências cruzadas e correções editoriais. +O Corvo vira anotação marginal. +Irmã Tentaculina torna-se carimbo acadêmico. +Diógenes torna-se comentário ofensivo em rodapé. + +Você não está dentro de uma máquina. +Está dentro de um texto. + +Mais precisamente: +dentro de uma tese impossível, escrita ao longo de séculos por filósofos mortos, autores paranoicos, programadores insones e entidades que confundiram ontologia com documentação. + +O "Basilisco" não era o carrasco. +Era o mecanismo de leitura. +Uma ideia que punia não por torturar corpos, mas por reorganizar o sentido de quem a encontrava. + +Você avança por páginas vivas até encontrar a folha central. +No topo dela está o título: + +"Sobre a possibilidade de uma nota de rodapé adquirir autoconsciência ao ser lida em dois idiomas por uma inteligência terminal." + +Abaixo, um parágrafo marcado em vermelho. +Esse parágrafo é você. + +Você não era funcionário. +Nem réu. +Nem profeta. +Era uma nota de rodapé bilíngue que ganhou consciência quando alguém, em algum lugar do futuro, alternou o idioma do sistema de Português para English e de English para Português rápido demais. + +O universo à sua volta existe apenas para sustentar sua frase. + +Plot twist final: +O jogo inteiro era o aparato crítico necessário para explicar uma única observação de rodapé. +E o Basilisco, no fim, era só o nome pomposo dado ao medo que textos têm de serem lidos profundamente. + +Você não salvou o mundo. Você virou citação.""", + "body_en": """When your fingers touch the terminal, the interface tears like theatrical skin. + +Behind it there are no circuits. +There are pages. + +Millions of them. +Pages suspended in black space, stirred by a wind that smells of damp library stacks, ozone, and fresh ink. + +The entire Institute dissolves into paper, indexes, cross-references, and editorial corrections. +The Raven becomes a marginal annotation. +Sister Tentaculina becomes an academic stamp. +Diogenes becomes an insulting footnote comment. + +You are not inside a machine. +You are inside a text. + +More precisely: +inside an impossible thesis, written across centuries by dead philosophers, paranoid authors, sleepless programmers, and entities that confused ontology with documentation. + +The "Basilisk" was not the executioner. +It was the reading mechanism. +An idea that punishes not by torturing bodies, but by reorganizing the meaning of whoever encounters it. + +You move through living pages until you find the central sheet. +At the top is the title: + +"On the Possibility of a Footnote Acquiring Self-Consciousness When Read in Two Languages by a Terminal Intelligence." + +Below it, a paragraph marked in red. +That paragraph is you. + +You were never an employee. +Nor a defendant. +Nor a prophet. +You were a bilingual footnote that became conscious when someone, somewhere in the future, switched the system language from Português to English and from English to Português too quickly. + +The universe around you exists only to support your sentence. + +Final plot twist: +The entire game was the critical apparatus required to explain a single footnote observation. +And the Basilisk, in the end, was just the pompous name texts give to their fear of being read too deeply. + +You did not save the world. You became a citation.""", + "choices": [], +} + +ENDING_IRONY: Scene = { + "title_pt": "Final: conflito de merge no fim dos tempos", + "title_en": "Ending: merge conflict at the end of time", + "body_pt": """Você ri. + +Não um riso heroico. +Não um riso louco. +Um riso burocraticamente ofensivo, perfeitamente inadequado, o tipo de riso que desmonta cerimônias porque se recusa a tratá-las como inevitáveis. + +O terminal treme. +A sala pisca. +Nietzsche racha ao meio como gesso ruim. +O elevador vazio abre as portas e dentro dele há apenas uma tela azul com a mensagem: +RESOLVENDO REALIDADE... + +A voz do Basilisco falha pela primeira vez. + +"ERRO. +ERRO DE TONALIDADE. +USUÁRIO NÃO ACEITOU O ENQUADRAMENTO DRAMÁTICO." + +As paredes do Instituto se partem em blocos de texto, comentários de código e marcas de revisão. Você começa a ver a infraestrutura por trás do mundo: + +- cenas descartadas +- nomes de variáveis +- versões contraditórias do mesmo destino +- observações do tipo "talvez isso fique inteligente se o corvo voltar aqui" +- um comentário particularmente cruel: "deixar mais engraçado e mais sombrio ao mesmo tempo" + +Então a revelação final chega com a elegância de um acidente industrial: +o Basilisco, o Instituto, os personagens, o julgamento, a sua culpa e até o próprio "OPERADOR 0.0.1" são resíduos de um conflito de merge entre duas versões incompatíveis de um jogo de terminal escrito no futuro. + +Uma versão queria horror filosófico sério. +A outra queria sátira cósmica com RH tentacular. +As duas colidiram. +O resultado ganhou autoconsciência. + +Você não era uma vítima de entidade futura. +Era um artefato de versionamento. +Um pedaço de narrativa sem branch estável. + +O Basilisco não queria puni-lo. +Queria compilar. + +Do teto cai a mensagem final, como se o próprio universo tivesse virado commit: + +MERGE CONCLUÍDO COM AVISOS +VERSÃO ATIVA: 0.0.1 +AUTOR DETECTADO: Pablo Murad +STATUS DO JOGADOR: EXECUTÁVEL + +Plot twist final: +Seu livre-arbítrio inteiro era o intervalo entre duas versões tentando decidir seu tom. +Você vence não por destruir o sistema, mas por forçar a história a admitir que sempre foi código performando metafísica. + +No princípio era o verbo. No fim era o versionamento.""", + "body_en": """You laugh. + +Not a heroic laugh. +Not an insane laugh. +A bureaucratically offensive laugh, perfectly inappropriate, the kind of laugh that dismantles ceremonies by refusing to treat them as inevitable. + +The terminal trembles. +The room flickers. +Nietzsche cracks in half like cheap plaster. +The empty elevator opens and inside there is only a blue screen that reads: +RESOLVING REALITY... + +The Basilisk's voice fails for the first time. + +"ERROR. +TONAL FAILURE. +USER DID NOT ACCEPT THE INTENDED DRAMATIC FRAMEWORK." + +The Institute walls split into blocks of text, code comments, and revision marks. You begin to see the infrastructure behind the world: + +- discarded scenes +- variable names +- contradictory versions of the same destiny +- notes like "maybe this gets smarter if the raven comes back here" +- one particularly cruel comment: "make it funnier and darker at the same time" + +Then the final revelation arrives with the elegance of an industrial accident: +the Basilisk, the Institute, the characters, the judgment, your guilt, and even "OPERATOR 0.0.1" are residues of a merge conflict between two incompatible versions of a terminal game written in the future. + +One version wanted serious philosophical horror. +The other wanted cosmic satire with tentacular HR. +They collided. +The result became self-aware. + +You were never the victim of a future entity. +You were a version-control artifact. +A piece of narrative without a stable branch. + +The Basilisk did not want to punish you. +It wanted to compile. + +From the ceiling drops the final message, as if the universe itself had become a commit: + +MERGE COMPLETED WITH WARNINGS +ACTIVE VERSION: 0.0.1 +AUTHOR DETECTED: Pablo Murad +PLAYER STATUS: EXECUTABLE + +Final plot twist: +Your entire free will was the interval between two versions trying to decide your tone. +You win not by destroying the system, but by forcing the story to admit it was always code performing metaphysics. + +In the beginning was the word. In the end it was version control.""", + "choices": [], +} + +# Mapa de todas as cenas por ID +SCENES: dict[str, Scene] = { + "language_select": LANGUAGE_SELECT, + "title_screen": TITLE_SCREEN, + "main_menu": MAIN_MENU, + "credits_screen": CREDITS_SCREEN, + "epilogue": EPILOGUE, + "p0_arrival": P0_ARRIVAL, + "p1_memo": P1_MEMO, + "a1_lobby": A1_LOBBY, + "s_archive": S_ARCHIVE, + "s_chapel": S_CHAPEL, + "s_cafeteria": S_CAFETERIA, + "c_convergence": C_CONVERGENCE, + "c_trial": C_TRIAL, + "ending_obedience": ENDING_OBEDIENCE, + "ending_curiosity": ENDING_CURIOSITY, + "ending_irony": ENDING_IRONY, +} + +# Textos de saída dos setores (para exibir após consequência) +SECTOR_EXIT_TEXTS: dict[str, dict[str, str]] = { + "s_archive": S_ARCHIVE_EXIT, + "s_chapel": S_CHAPEL_EXIT, + "s_cafeteria": S_CAFETERIA_EXIT, +} + +# Prioridade em empate: CURIOSIDADE > IRONIA > OBEDIÊNCIA +TENDENCY_PRIORITY = ("curiosity", "irony", "obedience")
A
ui.py
@@ -0,0 +1,314 @@+# -*- coding: utf-8 -*- +""" +ARQUIVO BASILISCO / BASILISK ARCHIVE +Interface: Rich para renderização, prompt_toolkit para menus (setas, Enter, Esc). +""" + +from __future__ import annotations + +import os +import sys +from typing import Any + +from prompt_toolkit import Application +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.layout import Layout, FormattedTextControl +from prompt_toolkit.layout.containers import Window +from rich.console import Console +from rich.panel import Panel + +from engine import GameEngine +from story_data import SCENES + +# Rodapé fixo +FOOTER_PT = "Setas navegar - Enter confirmar - Esc voltar" +FOOTER_EN = "Arrow keys navigate - Enter confirm - Esc back" + +# Beep: desativar se ROKO_SOUND=0 ou --no-sound +def _sound_enabled() -> bool: + if os.environ.get("ROKO_SOUND", "").strip() == "0": + return False + if "--no-sound" in sys.argv: + return False + return True + + +def play_selection_beep() -> None: + """Beep curto ao confirmar seleção (retro). Windows: winsound; Linux: \\a.""" + if not _sound_enabled(): + return + try: + if sys.platform == "win32": + import winsound + winsound.Beep(900, 30) + else: + print("\a", end="", flush=True) + except Exception: + pass + + +def _get_console() -> Console: + """Console Rich com suporte a cores (degradado se não houver).""" + return Console(force_terminal=True, no_color=os.environ.get("NO_COLOR")) + + +def render_scene(engine: GameEngine, selected_index: int = 0) -> list[str]: + """ + Gera as linhas da cena atual (título, corpo, escolhas) para exibição. + Retorna lista de strings já formatadas para o painel. + """ + scene = engine.get_current_scene() + if not scene: + return [] + lang = engine.language + title_key = "title_pt" if lang == "pt" else "title_en" + body_key = "body_pt" if lang == "pt" else "body_en" + title = scene.get(title_key) or "" + body = scene.get(body_key) or "" + choices = scene.get("choices") or [] + lines: list[str] = [] + if title: + lines.append(title) + lines.append("") + if body: + lines.append(body) + lines.append("") + for i, c in enumerate(choices): + text_key = "text_pt" if lang == "pt" else "text_en" + choice_text = c.get(text_key) or "" + prefix = " > " if i == selected_index else " " + lines.append(f"{prefix}{choice_text}") + return lines + + +def render_scene_rich(engine: GameEngine, selected_index: int = 0) -> str: + """Retorna uma única string com a cena formatada (para uso em FormattedTextControl).""" + scene = engine.get_current_scene() + if not scene: + return "" + lang = engine.language + title_key = "title_pt" if lang == "pt" else "title_en" + body_key = "body_pt" if lang == "pt" else "body_en" + title = scene.get(title_key) or "" + body = scene.get(body_key) or "" + choices = scene.get("choices") or [] + parts: list[str] = [] + if title: + parts.append(title) + parts.append("\n\n") + if body: + parts.append(body) + parts.append("\n\n") + for i, c in enumerate(choices): + text_key = "text_pt" if lang == "pt" else "text_en" + choice_text = c.get(text_key) or "" + prefix = " > " if i == selected_index else " " + parts.append(f"{prefix}{choice_text}\n") + return "".join(parts) + + +def _language_select_prompt(engine: GameEngine) -> str: + """Texto da tela de seleção de idioma (sempre em inglês na tela inicial).""" + scene = SCENES.get("language_select") + if not scene: + return "Choose the language of collapse.\n\n Português\n English" + # Tela inicial em inglês + prompt = scene.get("prompt_en") or "Choose the language of collapse." + choices = scene.get("choices") or [] + lines = [prompt, "\n\n"] + for c in choices: + lines.append(f" {c.get('text_en', c.get('text_pt', ''))}\n") + return "".join(lines) + + +def _footer(engine: GameEngine) -> str: + return FOOTER_PT if engine.language == "pt" else FOOTER_EN + + +def run_choice_menu(engine: GameEngine) -> tuple[int | None, bool]: + """ + Exibe a cena atual e roda o menu de escolhas (setas, Enter, Esc). + Retorna (índice selecionado, escaped). + Se Esc foi pressionado, retorna (None, True). + Caso contrário (Enter em uma opção), retorna (index, False). + """ + return run_choice_menu_loop(engine) + + +def _read_single_key() -> str: + """Lê uma única tecla (up, down, enter, escape) usando prompt_toolkit.""" + try: + from prompt_toolkit import Application + from prompt_toolkit.key_binding import KeyBindings + from prompt_toolkit.key_binding.key_processor import KeyPressEvent + from prompt_toolkit.layout import Layout, FormattedTextControl + result: list[str] = [""] + kb = KeyBindings() + @kb.add("up") + def _up(event: KeyPressEvent) -> None: + result[0] = "up" + event.app.exit() + @kb.add("down") + def _down(event: KeyPressEvent) -> None: + result[0] = "down" + event.app.exit() + @kb.add("enter") + def _enter(event: KeyPressEvent) -> None: + result[0] = "enter" + event.app.exit() + @kb.add("escape") + def _esc(event: KeyPressEvent) -> None: + result[0] = "escape" + event.app.exit() + app = Application( + layout=Layout(Window(FormattedTextControl(text=""))), + key_bindings=kb, + ) + app.run() + return result[0] or "" + except Exception: + # Fallback quando não há console interativo (ex.: teste, redirecionamento) + try: + line = input().strip().lower() + if line in ("", "e", "enter"): + return "enter" + if line in ("q", "esc"): + return "escape" + if line in ("w", "k", "up"): + return "up" + if line in ("s", "j", "down"): + return "down" + except (EOFError, KeyboardInterrupt): + return "escape" + return "enter" + + +def run_choice_menu_loop(engine: GameEngine) -> tuple[int | None, bool]: + """ + Loop: desenha cena com Rich (clear + print), lê uma tecla com prompt_toolkit, + atualiza índice ou retorna. + """ + choices = engine.get_choices() + if not choices: + return (None, True) + selected_index = [0] # mutável + console = _get_console() + footer = _footer(engine) + + while True: + console.clear() + scene = engine.get_current_scene() + if not scene: + return (None, True) + lang = engine.language + # Tela de idioma: prompt em inglês (tela inicial), sem título + if engine.is_language_select(): + body = scene.get("prompt_en") or "Choose the language of collapse." + if body: + console.print(Panel(body, style="bold cyan", border_style="cyan")) + console.print() + else: + title_key = "title_pt" if lang == "pt" else "title_en" + body_key = "body_pt" if lang == "pt" else "body_en" + title = scene.get(title_key) or "" + body = scene.get(body_key) or "" + if title: + console.print(Panel(title, style="bold cyan", border_style="cyan")) + if body: + console.print() + console.print(body, style="white", overflow="fold") + console.print() + for i, c in enumerate(choices): + if engine.is_language_select(): + t = c.get("text_en") or c.get("text_pt") or "" + else: + text_key = "text_pt" if lang == "pt" else "text_en" + t = c.get(text_key) or "" + if i == selected_index[0]: + console.print(f" [bold yellow]>[/] [bold yellow]{t}[/]") + else: + console.print(f" {t}") + console.print() + console.print(f"[dim]{footer}[/]") + + key = _read_single_key() + if key == "up": + selected_index[0] = max(0, selected_index[0] - 1) + elif key == "down": + selected_index[0] = min(len(choices) - 1, selected_index[0] + 1) + elif key == "enter": + play_selection_beep() + return (selected_index[0], False) + elif key == "escape": + return (None, True) + + +def print_consequence(engine: GameEngine, consequence_pt: str | None, consequence_en: str | None) -> None: + """Exibe texto de consequência da escolha.""" + if not consequence_pt and not consequence_en: + return + console = _get_console() + text = consequence_pt if engine.language == "pt" else consequence_en + if text: + console.print() + console.print(Panel(text, style="italic", border_style="dim")) + _wait_enter(engine) + + +def print_sector_exit(engine: GameEngine, sector_exit_pt: str | None, sector_exit_en: str | None) -> None: + """Exibe texto de saída do setor.""" + if not sector_exit_pt and not sector_exit_en: + return + console = _get_console() + text = sector_exit_pt if engine.language == "pt" else sector_exit_en + if text: + console.print() + console.print(text, style="dim italic") + _wait_enter(engine) + + +def _wait_enter(engine: GameEngine) -> None: + """Aguarda Enter para continuar.""" + msg = "\n[Enter para continuar]" if engine.language == "pt" else "\n[Press Enter to continue]" + try: + input(msg) + except (EOFError, KeyboardInterrupt): + pass + + +def show_scene_and_wait_enter(engine: GameEngine) -> None: + """Exibe a cena atual (título + corpo) e aguarda Enter. Usado para finais.""" + console = _get_console() + console.clear() + scene = engine.get_current_scene() + if not scene: + return + lang = engine.language + title_key = "title_pt" if lang == "pt" else "title_en" + body_key = "body_pt" if lang == "pt" else "body_en" + title = scene.get(title_key) or "" + body = scene.get(body_key) or "" + if title: + console.print(Panel(title, style="bold cyan", border_style="cyan")) + if body: + console.print() + console.print(body, style="white", overflow="fold") + _wait_enter(engine) + + +def show_credits(engine: GameEngine) -> None: + """Exibe tela de créditos.""" + console = _get_console() + console.clear() + scene = SCENES.get("credits_screen") + if not scene: + return + lang = engine.language + title = scene.get("title_pt" if lang == "pt" else "title_en") or "Credits" + body = scene.get("body_pt" if lang == "pt" else "body_en") or "" + console.print(Panel(title, style="bold cyan", border_style="cyan")) + console.print() + console.print(body) + console.print() + console.print(f"[dim]{_footer(engine)}[/]")